172 lines
8.0 KiB
HTML
172 lines
8.0 KiB
HTML
<!doctype html>
|
||||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|||
|
|
<title data-i18n="wake.title">Starting your workspace</title>
|
|||
|
|
<link rel="stylesheet" href="/design.css" />
|
|||
|
|
<!-- 档案 81 · R5:平台多语言运行时(默认英语) -->
|
|||
|
|
<script src="/i18n.js"></script>
|
|||
|
|
<style>
|
|||
|
|
/* 档案 77「AI 核心」加载体 —— 与实例页恢复浮层**同一套视觉语言**(配色跟随平台主题)。
|
|||
|
|
2026-09-25 与浮层同步改造(用户:「去掉那个圆角矩形旋转的样式,改为一个闪烁的点,绕着图标圆环转圈」):
|
|||
|
|
撤掉「conic 扫描弧(1.5s 整圈转) + 反向虚线环(7s 整圈转) + 三颗轨道粒子(3.4s)」——
|
|||
|
|
它们转的都是**环/弧本身**,观感像一整圈边框在滚;
|
|||
|
|
改为 `.wk-orbit > i`:**1 个亮点沿圆环巡游 + 呼吸式闪烁**,与浮层 `.__dsh-orbit` 逐项同构
|
|||
|
|
(一圈 2.6s、闪烁 1.3s 对齐;本页无图标,故轨道就挂在既有的 `.wk-ring` 上)。 */
|
|||
|
|
.wk-orb{position:relative;width:104px;height:104px;margin:2px auto 16px}
|
|||
|
|
.wk-halo{position:absolute;inset:-18px;border-radius:50%;
|
|||
|
|
background:radial-gradient(circle, rgba(83,74,183,.20), rgba(139,124,246,.12) 46%, transparent 70%);
|
|||
|
|
animation:wk-halo 2.6s ease-in-out infinite}
|
|||
|
|
.wk-ring{position:absolute;inset:18px;border-radius:50%;border:1px solid rgba(83,74,183,.22)}
|
|||
|
|
.wk-core{position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;
|
|||
|
|
background:radial-gradient(circle, #ffffff 0%, #6B5FD6 46%, rgba(83,74,183,0) 74%);
|
|||
|
|
box-shadow:0 0 12px rgba(83,74,183,.55), 0 0 30px rgba(139,124,246,.35);
|
|||
|
|
animation:wk-core 1.6s ease-in-out infinite}
|
|||
|
|
/* 绕环巡游的亮点:轨道 = 与 .wk-ring 同尺寸,故点恒压在环线上跑(不跑到环外)。 */
|
|||
|
|
.wk-orbit{position:absolute;inset:18px;animation:wk-spin 2.6s linear infinite}
|
|||
|
|
.wk-orbit i{position:absolute;left:50%;top:-3px;width:6px;height:6px;margin-left:-3px;border-radius:50%;
|
|||
|
|
background:radial-gradient(circle, #ffffff 0%, #8B7CF6 46%, rgba(139,124,246,0) 78%);
|
|||
|
|
box-shadow:0 0 8px rgba(139,124,246,.92), 0 0 18px rgba(83,74,183,.5);
|
|||
|
|
animation:wk-blink 1.3s ease-in-out infinite}
|
|||
|
|
@keyframes wk-spin{to{transform:rotate(360deg)}}
|
|||
|
|
@keyframes wk-core{0%,100%{transform:scale(.82);opacity:.6}50%{transform:scale(1.12);opacity:1}}
|
|||
|
|
@keyframes wk-halo{0%,100%{opacity:.45;transform:scale(.96)}50%{opacity:.9;transform:scale(1.04)}}
|
|||
|
|
@keyframes wk-blink{0%,100%{opacity:.22;transform:scale(.78)}50%{opacity:1;transform:scale(1.18)}}
|
|||
|
|
@keyframes wkb{0%{margin-left:-40%}100%{margin-left:100%}}
|
|||
|
|
.wk-bar{height:4px;border-radius:999px;background:rgba(127,127,127,.18);overflow:hidden;margin:16px 0 8px}
|
|||
|
|
.wk-bar > i{display:block;height:100%;width:40%;border-radius:999px;
|
|||
|
|
background:linear-gradient(90deg, rgba(83,74,183,0), #534AB7 45%, #8B7CF6 70%, rgba(83,74,183,0));
|
|||
|
|
animation:wkb 1.6s ease-in-out infinite}
|
|||
|
|
.wk-label{font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.16em;
|
|||
|
|
color:#8a8f98;text-transform:uppercase;margin-bottom:10px}
|
|||
|
|
.wk-step{font-size:13px;color:#8a8f98;min-height:20px;text-align:center}
|
|||
|
|
.wk-acts{margin-top:16px;text-align:center}
|
|||
|
|
.wk-note{margin-top:14px;font-size:12px;line-height:1.7;color:#8a8f98;text-align:center}
|
|||
|
|
@media (prefers-reduced-motion: reduce){
|
|||
|
|
.wk-orbit,.wk-orbit i,.wk-core,.wk-halo,.wk-bar > i{animation:none!important}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px">
|
|||
|
|
<div class="card" style="max-width:420px;width:100%;text-align:center">
|
|||
|
|
<div class="wk-orb" id="spin">
|
|||
|
|
<div class="wk-halo"></div>
|
|||
|
|
<div class="wk-ring"></div><div class="wk-core"></div>
|
|||
|
|
<div class="wk-orbit"><i></i></div>
|
|||
|
|
</div>
|
|||
|
|
<h1 class="page-title" style="margin-bottom:6px" data-i18n="wake.h1">Starting your workspace…</h1>
|
|||
|
|
<p class="page-sub" style="margin-bottom:0" data-i18n="wake.sub">Idle instances get reclaimed; the first visit takes a few seconds to spin it back up.</p>
|
|||
|
|
<div class="wk-bar"><i></i></div>
|
|||
|
|
<div class="wk-step" id="step" data-i18n="wake.connecting">Connecting…</div>
|
|||
|
|
<div class="wk-label">dsh · auto recovery</div>
|
|||
|
|
<div class="wk-acts" id="acts" style="display:none">
|
|||
|
|
<button class="btn primary" id="retry" data-i18n="common.retry">Retry</button>
|
|||
|
|
</div>
|
|||
|
|
<p class="wk-note" id="note" data-i18n="wake.note">
|
|||
|
|
If nothing happens for a while, click “Retry”. This page redirects automatically — no manual refresh needed.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<script>
|
|||
|
|
(function () {
|
|||
|
|
var NEXT = new URLSearchParams(location.search).get('next') || ''
|
|||
|
|
var step = document.getElementById('step')
|
|||
|
|
var acts = document.getElementById('acts')
|
|||
|
|
var note = document.getElementById('note')
|
|||
|
|
var tried = 0
|
|||
|
|
var MAX = 3
|
|||
|
|
|
|||
|
|
function show(text) { step.textContent = text }
|
|||
|
|
|
|||
|
|
function fail(msg) {
|
|||
|
|
document.getElementById('spin').style.display = 'none'
|
|||
|
|
acts.style.display = 'block'
|
|||
|
|
note.textContent = msg
|
|||
|
|
show(I18N.t('wake.failed'))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只允许跳回**本站注册域**(运行时从 hostname 推导,⛔ 不写死域名)。
|
|||
|
|
// 规则:去掉最左一段即到注册域 —— `guest.example.com` ⇒ `example.com`。
|
|||
|
|
function registrableDomain() {
|
|||
|
|
var h = location.hostname
|
|||
|
|
if (!h || h === 'localhost' || /^\d+(\.\d+){3}$/.test(h)) return h
|
|||
|
|
var parts = h.split('.')
|
|||
|
|
return parts.length > 2 ? parts.slice(1).join('.') : h
|
|||
|
|
}
|
|||
|
|
function safeNext(u) {
|
|||
|
|
try {
|
|||
|
|
var x = new URL(u, location.href)
|
|||
|
|
var d = registrableDomain()
|
|||
|
|
if (!d) return ''
|
|||
|
|
var hop = '.' + d
|
|||
|
|
return x.hostname === d || x.hostname.slice(-hop.length) === hop ? x.href : ''
|
|||
|
|
} catch (e) { return '' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function wake() {
|
|||
|
|
tried += 1
|
|||
|
|
show(tried === 1 ? I18N.t('wake.launching') : I18N.t('wake.retrying', { n: tried }))
|
|||
|
|
try {
|
|||
|
|
// 复用平台已有的登录直达接口:它内部会 launch(实例不在则拉起)并返回带 token 的直达 URL
|
|||
|
|
var res = await fetch('/api/dsh/enter', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'content-type': 'application/json' },
|
|||
|
|
body: JSON.stringify({}),
|
|||
|
|
})
|
|||
|
|
if (res.status === 401) { location.replace('/login.html'); return }
|
|||
|
|
var j = await res.json().catch(function () { return {} })
|
|||
|
|
var url = j.url || j.redirect || ''
|
|||
|
|
if (res.ok && url) {
|
|||
|
|
show(I18N.t('wake.ready'))
|
|||
|
|
location.replace(url)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (res.ok && !url) { // 已登录但需要重走门户
|
|||
|
|
location.replace('/portal.html')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (j.error === 'instance_circuit_open') { // 档案 78:被熔断冷却,重试没有意义
|
|||
|
|
var waitMin = Math.max(1, Math.ceil((j.retryAfterMs || 0) / 60000))
|
|||
|
|
fail(I18N.t('wake.circuit', { min: waitMin }))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
throw new Error(j.error || ('HTTP ' + res.status))
|
|||
|
|
} catch (err) {
|
|||
|
|
if (tried < MAX) {
|
|||
|
|
show(I18N.t('wake.notReady'))
|
|||
|
|
setTimeout(wake, 2000)
|
|||
|
|
} else {
|
|||
|
|
fail(I18N.t('wake.giveUp', { msg: (err && err.message ? err.message : I18N.t('wake.unknownErr')) }))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById('retry').addEventListener('click', function () {
|
|||
|
|
tried = 0
|
|||
|
|
document.getElementById('spin').style.display = ''
|
|||
|
|
acts.style.display = 'none'
|
|||
|
|
note.textContent = I18N.t('wake.noteShort')
|
|||
|
|
wake()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 若 nginx 带了 next(原始目标地址),唤醒后优先回那里
|
|||
|
|
var saved = safeNext(NEXT)
|
|||
|
|
if (saved) { sessionStorage.setItem('dsh.wake.next', saved) }
|
|||
|
|
|
|||
|
|
var t = setInterval(function () {
|
|||
|
|
var n = sessionStorage.getItem('dsh.wake.next')
|
|||
|
|
if (n && n !== NEXT) {
|
|||
|
|
clearInterval(t)
|
|||
|
|
// 直接以原始目标为跳转终点(进入后再由实例侧校验 token)
|
|||
|
|
NEXT = n
|
|||
|
|
}
|
|||
|
|
}, 1000)
|
|||
|
|
setTimeout(function () { clearInterval(t) }, 30000)
|
|||
|
|
|
|||
|
|
wake()
|
|||
|
|
})()
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|