fix(portable): keep config center visible on startup

This commit is contained in:
hfshfg
2026-06-20 12:16:42 +08:00
parent 8cd5dbfea4
commit 1517152a19
7 changed files with 164 additions and 91 deletions

View File

@@ -168,7 +168,7 @@ input:focus { border-color: #ff6b35; }
<span class="check"></span> <span class="check"></span>
<h4>虾盘云 <span class="tag hot">首选</span><span class="tag">中转站</span></h4> <h4>虾盘云 <span class="tag hot">首选</span><span class="tag">中转站</span></h4>
<p>一个 Key 用 DeepSeek / Claude / GPT / 通义 等国内外全部大模型,无需翻墙</p> <p>一个 Key 用 DeepSeek / Claude / GPT / 通义 等国内外全部大模型,无需翻墙</p>
<a class="buy-link" href="https://u-claw.org/cloud.html" target="_blank">→ 注册获取 API Key</a> <a class="buy-link" href="https://u-claw.org/cloud.html" target="_blank" data-action-label="注册 / 充值 / 获取 API Key">→ 注册 / 充值 / 获取 API Key</a>
</div> </div>
<div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat"> <div class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
<span class="check"></span> <span class="check"></span>
@@ -255,7 +255,7 @@ input:focus { border-color: #ff6b35; }
<p class="desc" id="step2desc">请填写对应模型的 API Key</p> <p class="desc" id="step2desc">请填写对应模型的 API Key</p>
<div class="api-info" id="buyInfo"> <div class="api-info" id="buyInfo">
<h4>📌 如何获取 API Key</h4> <h4>📌 获取 / 充值 API Key</h4>
<div class="provider-links" id="providerLinks"></div> <div class="provider-links" id="providerLinks"></div>
</div> </div>
@@ -477,7 +477,7 @@ function setupStep2() {
linksDiv.innerHTML = ''; linksDiv.innerHTML = '';
if (buyLink) { if (buyLink) {
linksDiv.innerHTML = ` linksDiv.innerHTML = `
<a href="${buyLink.href}" target="_blank">🔑 ${name} — 获取 API Key</a> <a href="${buyLink.href}" target="_blank">🔑 ${name}${buyLink.dataset.actionLabel || '获取 API Key'}</a>
`; `;
} }
// Add common alternatives // Add common alternatives
@@ -498,7 +498,7 @@ function togglePw() {
function buildOpenClawConfig(provider, base, model, apiKey) { function buildOpenClawConfig(provider, base, model, apiKey) {
// Base config that OpenClaw expects // Base config that OpenClaw expects
const baseConfig = { const baseConfig = {
gateway: { auth: { mode: 'token', token: 'uclaw', scopes: ['operator.admin'] } }, gateway: { auth: { mode: 'token', token: 'uclaw', scopes: ['operator.admin'] }, remote: { token: 'uclaw' } },
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' }, commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() } meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() }
}; };

View File

@@ -166,35 +166,23 @@ OPENCLAW_MJS="$CORE_DIR/node_modules/openclaw/openclaw.mjs"
"$NODE_BIN" "$OPENCLAW_MJS" gateway run --allow-unconfigured --force --port $PORT & "$NODE_BIN" "$OPENCLAW_MJS" gateway run --allow-unconfigured --force --port $PORT &
GW_PID=$! GW_PID=$!
# ---- 11. 是否已配置模型openclaw.json 含 "providers" 即视为已配置 (issue #24) ---- # ---- 11. 立刻打开"启动首屏",给用户即时反馈(移植自 4.0 splash----
# 已配置:只开 Dashboard不再每次弹配置页。未配置首次开 Config Center 引导填 Key # 首屏 loading.html 自己轮询 /ready就绪后停在选择页不再自动冲进 Dashboard
MODEL_CONFIGURED=0
if grep -q '"providers"' "$CONFIG_FILE" 2>/dev/null; then
MODEL_CONFIGURED=1
fi
# ---- 12. 立刻打开"启动首屏",给用户即时反馈(移植自 4.0 splash----
# 首屏 loading.html 自己轮询 /readygateway 真就绪后自动跳 Dashboard——天然解决
# "gateway 没起就开 Dashboard 拒连"的问题(同 Windows issue #46/#48
echo -e " ${YELLOW}首次启动需准备运行环境,约 30-90 秒,请稍候...${NC}" echo -e " ${YELLOW}首次启动需准备运行环境,约 30-90 秒,请稍候...${NC}"
# 用 file:// URL 确保 query string?port=)能传给浏览器;裸路径 open 会把整串当文件名。 # 用 file:// URL 确保 query string?port=)能传给浏览器;裸路径 open 会把整串当文件名。
open "file://$UCLAW_DIR/lib/loading.html?port=$PORT&token=uclaw" 2>/dev/null || true open "file://$UCLAW_DIR/lib/loading.html?port=$PORT&token=uclaw" 2>/dev/null || true
# 首次未配置:再开 Config Center 引导填 Key # 每次都打开 Config Center,方便改模型、充值/获取 Key、连接微信等渠道。
if [ "$MODEL_CONFIGURED" != "1" ]; then open "http://127.0.0.1:18788/" 2>/dev/null || true
open "http://127.0.0.1:18788/" 2>/dev/null || true
fi
# ---- 12b. gateway 首轮预热(后台、静默、非阻塞)---- # ---- 11b. gateway 首轮预热(后台、静默、非阻塞)----
# 就绪后先唤醒 config/model 子系统,用户首次点发送时不再等。移植自 4.0 first-turn-prewarm。 # 就绪后先唤醒 config/model 子系统,用户首次点发送时不再等。移植自 4.0 first-turn-prewarm。
"$NODE_BIN" "$UCLAW_DIR/lib/prewarm.mjs" "$PORT" uclaw >/dev/null 2>&1 & "$NODE_BIN" "$UCLAW_DIR/lib/prewarm.mjs" "$PORT" uclaw >/dev/null 2>&1 &
# ---- 12c. 兜底:万一首屏页的 file:// fetch 被浏览器拦,仍轮询端口后开 Dashboard ---- # ---- 11c. 兜底:万一首屏页的 file:// fetch 被浏览器拦,仍静默轮询端口 ----
# 慢盘首启可达 90s+,轮询上限覆盖这段。最多 ~3 分钟180×1s # 慢盘首启可达 90s+,轮询上限覆盖这段。最多 ~3 分钟180×1s
( (
for i in $(seq 1 180); do for i in $(seq 1 180); do
if curl -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then if curl -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then
# 首屏页通常已自己跳转这里仅作兜底open 同一 URL 浏览器会复用已有标签。
[ "$MODEL_CONFIGURED" = "1" ] && open "http://127.0.0.1:$PORT/#token=uclaw" 2>/dev/null || true
exit 0 exit 0
fi fi
sleep 1 sleep 1

View File

@@ -49,11 +49,11 @@ if not exist "%DATA_DIR%\memory" mkdir "%DATA_DIR%\memory"
if not exist "%DATA_DIR%\backups" mkdir "%DATA_DIR%\backups" if not exist "%DATA_DIR%\backups" mkdir "%DATA_DIR%\backups"
if not exist "%DATA_DIR%\logs" mkdir "%DATA_DIR%\logs" if not exist "%DATA_DIR%\logs" mkdir "%DATA_DIR%\logs"
REM ── 加速:把"重 IO、可重建"的缓存从 U 盘搬到本机硬盘 ────────────────── REM Startup cache acceleration.
REM portable-cache.mjs 算出本机缓存目录(%LOCALAPPDATA%\U-Claw\slotUUID 隔离, REM portable-cache.mjs chooses a local-disk cache slot and redirects
REM 换盘符仍复用),并把 .openclaw\browser 做成 junction 指向本机盘。 REM .openclaw\browser there with a junction when possible.
REM 浏览器 user-data(几百 MB 随机小写)和 V8 编译缓存因此落本机 SSD不再拖慢 U 盘。 REM Browser user data and V8 compile cache stay off the USB drive.
REM 静默失败:取不到就跳过,缓存留 U 盘,照常启动。 REM If this fails, startup continues with the cache on the USB drive.
for /f "usebackq tokens=1,* delims==" %%a in (`""%NODE_BIN%" "%UCLAW_DIR%lib\portable-cache.mjs" "%STATE_DIR%" "%UCLAW_DIR%" 2^>nul"`) do ( for /f "usebackq tokens=1,* delims==" %%a in (`""%NODE_BIN%" "%UCLAW_DIR%lib\portable-cache.mjs" "%STATE_DIR%" "%UCLAW_DIR%" 2^>nul"`) do (
if "%%a"=="UCLAW_COMPILE_CACHE_DIR" set "NODE_COMPILE_CACHE=%%b" if "%%a"=="UCLAW_COMPILE_CACHE_DIR" set "NODE_COMPILE_CACHE=%%b"
if "%%a"=="UCLAW_CACHE_ROOT" set "UCLAW_CACHE_ROOT=%%b" if "%%a"=="UCLAW_CACHE_ROOT" set "UCLAW_CACHE_ROOT=%%b"
@@ -89,7 +89,7 @@ if not exist "%CORE_DIR%\node_modules" (
echo File system: NTFS recommended. exFAT/FAT32 will be very slow. echo File system: NTFS recommended. exFAT/FAT32 will be very slow.
echo. echo.
cd /d "%CORE_DIR%" cd /d "%CORE_DIR%"
REM npm 缓存留在盘内,避免污染系统 %APPDATA%\npm-cache拔盘不留痕 REM Keep npm cache inside the portable app instead of system APPDATA.
set "npm_config_cache=%APP_DIR%\.npm-cache" set "npm_config_cache=%APP_DIR%\.npm-cache"
call "%NPM_BIN%" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev call "%NPM_BIN%" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev
echo. echo.
@@ -144,9 +144,9 @@ echo Starting Config Center on port 18788...
set "CONFIG_SERVER=%UCLAW_DIR%config-server" set "CONFIG_SERVER=%UCLAW_DIR%config-server"
start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1 start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1
REM Config Server 就绪 —— 动态探测而非写死等待。 REM Wait for Config Server with polling instead of a fixed delay.
REM 它通常 <1s 就起来,写死 timeout /t 2 是白等。改为每 ~0.3s 探测 18788 REM It is usually ready in under one second. Poll about every 0.3 seconds,
REM 最多 ~6s20 次)兜底。监听到就立刻往下走。 REM up to about 6 seconds, then continue anyway.
set /a CFG_TRIES=0 set /a CFG_TRIES=0
:wait_config :wait_config
netstat -an | findstr ":18788 " | findstr "LISTENING" >nul 2>&1 netstat -an | findstr ":18788 " | findstr "LISTENING" >nul 2>&1
@@ -157,36 +157,25 @@ ping -n 1 -w 300 127.0.0.1 >nul 2>&1
goto :wait_config goto :wait_config
:config_ready :config_ready
REM IMPORTANT: 不要在 gateway 启动前就开 Dashboard 浏览器! REM Do not open Dashboard before the gateway is ready.
REM 慢 U 盘上 OpenClaw 首次启动要 staging bundled deps(几十秒), REM Slow USB drives may need tens of seconds to stage bundled deps.
REM 过早打开 http://127.0.0.1:18789 会"拒绝连接",是 issue #46/#48 的根因。 REM Open the local startup page now, and always open Config Center too.
REM 方案:立刻打开本地"启动首屏"loading.html给用户即时反馈移植自 4.0 splash REM Config Center lets users change models, recharge/get keys, and connect channels.
REM 首屏自己轮询 /readygateway 真就绪后自动跳 Dashboard——天然解决拒连。
REM 是否已配置模型openclaw.json 含 providers 即视为已配置 (issue #24)。 REM Open startup page with the gateway port and token in the query string.
REM 未配置(首次):先开 Config Center 引导填 Key。
set "MODEL_CONFIGURED="
findstr /C:"providers" "%STATE_DIR%\openclaw.json" >nul 2>&1
if %errorlevel%==0 set "MODEL_CONFIGURED=1"
REM 立刻开启动首屏(带 port 参数,就绪后自动跳 Dashboard
REM 用 file:/// URL正斜杠+ 转义 & ,确保 query string 能传给浏览器。
echo Opening startup screen... echo Opening startup screen...
set "LOADING_PATH=%UCLAW_DIR%lib\loading.html" set "LOADING_PATH=%UCLAW_DIR%lib\loading.html"
set "LOADING_URL=file:///%LOADING_PATH:\=/%?port=%PORT%&token=uclaw" set "LOADING_URL=file:///%LOADING_PATH:\=/%?port=%PORT%&token=uclaw"
start "" "%LOADING_URL%" start "" "%LOADING_URL%"
if not defined MODEL_CONFIGURED ( echo Opening Config Center...
echo First-time setup - opening Config Center... start "" http://127.0.0.1:18788/
start "" http://127.0.0.1:18788/
)
REM 后台等待器兜底:万一首屏页没起作用(浏览器拦本地 fetch 等), REM Fallback watcher: if the startup page cannot poll from file URLs,
REM 仍轮询端口gateway LISTENING 后开 Dashboard。 REM keep polling and reopen Config Center after the gateway is ready.
start /B "" cmd /c ""%UCLAW_DIR%lib\wait-gateway.bat" %PORT%" start /B "" cmd /c ""%UCLAW_DIR%lib\wait-gateway.bat" %PORT%"
REM gateway 首轮预热(后台、静默、非阻塞):就绪后先唤醒 config/model 子系统, REM Prewarm gateway in the background after it becomes ready.
REM 用户首次点发送时不再等。移植自 4.0 first-turn-prewarm。
start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\prewarm.mjs" %PORT% uclaw >nul 2>&1 start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\prewarm.mjs" %PORT% uclaw >nul 2>&1
echo. echo.
@@ -194,7 +183,7 @@ echo ========================================
echo Starting OpenClaw Gateway on port %PORT%... echo Starting OpenClaw Gateway on port %PORT%...
echo First run on a USB drive may take 30-90 seconds echo First run on a USB drive may take 30-90 seconds
echo (unpacking bundled components). Please wait; echo (unpacking bundled components). Please wait;
echo the Dashboard opens automatically when ready. echo Config Center is open for model, key, recharge, and channel setup.
echo DO NOT close this window while using U-Claw! echo DO NOT close this window while using U-Claw!
echo ======================================== echo ========================================
echo. echo.

View File

@@ -178,9 +178,15 @@ input:focus { border-color: #ff6b35; }
<!-- Step 1: Select Model --> <!-- Step 1: Select Model -->
<div class="section active" id="step1"> <div class="section active" id="step1">
<h2>选择 AI 模型</h2> <h2>选择 AI 模型</h2>
<p class="desc">推荐国内用户用 MiniMax / Kimi / DeepSeek</p> <p class="desc">最省心:用「虾盘云」一个 Key 调用国内外全部大模型(中转站)。也可用下面各家自己的官方 Key。</p>
<div class="model-grid"> <div class="model-grid">
<div class="model-card" data-provider="uclaw-cloud" data-base="https://api.u-claw.org/v1" data-model="deepseek-v4-flash">
<span class="check"></span>
<h4>虾盘云 <span class="tag hot">首选</span><span class="tag">中转站</span></h4>
<p>一个 Key 用 DeepSeek / Claude / GPT / 通义 等国内外全部大模型,无需翻墙</p>
<a class="buy-link" href="https://u-claw.org/cloud.html" target="_blank" data-action-label="注册 / 充值 / 获取 API Key">→ 注册 / 充值 / 获取 API Key</a>
</div>
<div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-Text-01"> <div class="model-card" data-provider="minimax" data-base="https://api.minimax.chat/v1" data-model="MiniMax-Text-01">
<span class="check"></span> <span class="check"></span>
<h4>MiniMax <span class="tag hot">推荐</span><span class="tag cn">国内</span></h4> <h4>MiniMax <span class="tag hot">推荐</span><span class="tag cn">国内</span></h4>
@@ -259,7 +265,7 @@ input:focus { border-color: #ff6b35; }
<p class="desc" id="step2desc">请填写对应模型的 API Key</p> <p class="desc" id="step2desc">请填写对应模型的 API Key</p>
<div class="api-info" id="buyInfo"> <div class="api-info" id="buyInfo">
<h4>📌 如何获取 API Key</h4> <h4>📌 获取 / 充值 API Key</h4>
<div class="provider-links" id="providerLinks"></div> <div class="provider-links" id="providerLinks"></div>
</div> </div>
@@ -456,7 +462,7 @@ function goStep(n) {
} }
const PROVIDER_NAMES = { const PROVIDER_NAMES = {
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek', 'uclaw-cloud': '虾盘云', minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包', zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq', openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
siliconflow: '硅基流动', custom: '自定义' siliconflow: '硅基流动', custom: '自定义'
@@ -474,7 +480,8 @@ function setupStep2() {
var linksDiv = document.getElementById('providerLinks'); var linksDiv = document.getElementById('providerLinks');
linksDiv.innerHTML = ''; linksDiv.innerHTML = '';
if (buyLink) { if (buyLink) {
linksDiv.innerHTML = '<a href="' + buyLink.href + '" target="_blank">🔑 ' + name + '获取 API Key</a>'; var action = buyLink.dataset.actionLabel || '获取 API Key';
linksDiv.innerHTML = '<a href="' + buyLink.href + '" target="_blank">🔑 ' + name + ' — ' + action + '</a>';
} }
linksDiv.innerHTML += '<a href="https://platform.minimaxi.com/" target="_blank">🔑 MiniMax — 国内推荐,注册送额度</a>' + linksDiv.innerHTML += '<a href="https://platform.minimaxi.com/" target="_blank">🔑 MiniMax — 国内推荐,注册送额度</a>' +
'<a href="https://open.bigmodel.cn/" target="_blank">🔑 智谱 GLM — 有免费额度</a>' + '<a href="https://open.bigmodel.cn/" target="_blank">🔑 智谱 GLM — 有免费额度</a>' +
@@ -492,7 +499,7 @@ function togglePw(id) {
// --- Build OpenClaw config --- // --- Build OpenClaw config ---
function buildConfig(provider, base, model, apiKey) { function buildConfig(provider, base, model, apiKey) {
var baseConfig = { var baseConfig = {
gateway: { auth: { mode: 'token', token: 'uclaw' } }, gateway: { auth: { mode: 'token', token: 'uclaw' }, remote: { token: 'uclaw' } },
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' }, commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() } meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() }
}; };

View File

@@ -7,7 +7,12 @@
<!-- <!--
启动首屏(移植自 v2 u-clawx 4.0 的 splash 思路)。 启动首屏(移植自 v2 u-clawx 4.0 的 splash 思路)。
双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。 双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。
本页每秒自查 gateway 是否就绪fetch /ready,就绪后自动跳转 Dashboard 本页每秒自查 gateway 是否就绪fetch /ready
就绪后【不自动跳转】,而是停在选择页,给两个大按钮:
🚀 进入 Dashboard开始聊天
⚙️ 配置 / 改模型(打开配置中心)
让用户自己决定去聊天还是改配置——避免"配过模型就被冲进 Dashboard、想改配置还得自己翻标签"。
端口通过 URL 查询参数传入loading.html?port=18789 端口通过 URL 查询参数传入loading.html?port=18789
(启动脚本打开时拼好。默认 18789。 (启动脚本打开时拼好。默认 18789。
@@ -63,22 +68,74 @@ h1 { font-size: 1.7em; font-weight: 600; margin-bottom: 8px; }
margin-top: 30px; font-size: .82em; opacity: .65; line-height: 1.6; margin-top: 30px; font-size: .82em; opacity: .65; line-height: 1.6;
} }
a.manual { color: #fff; text-decoration: underline; opacity: .9; } a.manual { color: #fff; text-decoration: underline; opacity: .9; }
/* —— 就绪后的选择页 —— */
.ready-wrap { display: none; }
body.ready .loading-wrap { display: none; }
body.ready .ready-wrap { display: block; }
.actions {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 26px;
}
.btn {
display: block;
text-decoration: none;
color: #fff;
border-radius: 14px;
padding: 20px 22px;
font-size: 1.15em;
font-weight: 600;
line-height: 1.35;
cursor: pointer;
border: 2px solid rgba(255,255,255,.35);
background: rgba(255,255,255,.12);
transition: transform .12s ease, background .2s ease, box-shadow .2s ease;
}
.btn:hover { transform: translateY(-2px); background: rgba(255,255,255,.22); box-shadow: 0 8px 24px rgba(0,0,0,.18); }
.btn:active { transform: translateY(0); }
.btn .sub { display: block; font-size: .68em; font-weight: 400; opacity: .8; margin-top: 4px; }
.btn-primary { background: #ff7a45; border-color: #ff7a45; }
.btn-primary:hover { background: #ff6a30; }
.foot { margin-top: 24px; font-size: .82em; opacity: .65; line-height: 1.6; }
</style> </style>
</head> </head>
<body> <body>
<div class="box"> <div class="box">
<div class="logo">🦞</div>
<h1>U-Claw 正在启动</h1> <!-- 启动中 -->
<div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会自动打开。</div> <div class="loading-wrap">
<div class="spinner"></div> <div class="logo">🦞</div>
<div class="bar"><i></i></div> <h1>U-Claw 正在启动</h1>
<div class="status" id="status">正在等待 OpenClaw 网关…</div> <div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会提示你下一步。</div>
<div class="elapsed" id="elapsed"></div> <div class="spinner"></div>
<div class="note"> <div class="bar"><i></i></div>
慢 U 盘首次启动通常 3090 秒。<br> <div class="status" id="status">正在等待 OpenClaw 网关…</div>
如果迟迟没反应,可手动打开 <div class="elapsed" id="elapsed"></div>
<a class="manual" id="manual" href="#">控制台</a> <div class="note">
慢 U 盘首次启动通常 3090 秒。<br>
如果迟迟没反应,可先打开
<a class="manual" id="manual" href="#">配置中心</a>
</div>
</div> </div>
<!-- 就绪后:让用户自己选 -->
<div class="ready-wrap">
<div class="logo">🦞</div>
<h1>U-Claw 已就绪</h1>
<div class="tip">选择下一步 — 直接开始聊天,或先去配置模型和渠道。</div>
<div class="actions">
<a class="btn btn-primary" id="goDash">🚀 进入 Dashboard
<span class="sub">开始和 AI 聊天</span>
</a>
<a class="btn" id="goConfig">⚙️ 配置 / 改模型
<span class="sub">填写或更换模型 Key、充值、连接微信等渠道</span>
</a>
</div>
<div class="foot">没配过模型或余额不足?先点「配置 / 改模型」处理 Key 或充值。</div>
</div>
</div> </div>
<script> <script>
@@ -90,10 +147,12 @@ a.manual { color: #fff; text-decoration: underline; opacity: .9; }
var dashboard = base + '/#token=' + encodeURIComponent(token); var dashboard = base + '/#token=' + encodeURIComponent(token);
var configCenter = 'http://127.0.0.1:18788/'; var configCenter = 'http://127.0.0.1:18788/';
document.getElementById('manual').href = dashboard; document.getElementById('manual').href = configCenter;
document.getElementById('goDash').href = dashboard;
document.getElementById('goConfig').href = configCenter;
var startedAt = Date.now(); var startedAt = Date.now();
var redirected = false; var ready = false;
function fmt(ms) { function fmt(ms) {
var s = Math.floor(ms / 1000); var s = Math.floor(ms / 1000);
@@ -101,23 +160,23 @@ a.manual { color: #fff; text-decoration: underline; opacity: .9; }
} }
function tickElapsed() { function tickElapsed() {
if (ready) return;
document.getElementById('elapsed').textContent = fmt(Date.now() - startedAt); document.getElementById('elapsed').textContent = fmt(Date.now() - startedAt);
} }
setInterval(tickElapsed, 1000); setInterval(tickElapsed, 1000);
tickElapsed(); tickElapsed();
function goDashboard() { function showChoice() {
if (redirected) return; if (ready) return;
redirected = true; ready = true;
document.getElementById('status').textContent = '网关已就绪,正在打开控制台…'; document.body.classList.add('ready');
setTimeout(function () { location.replace(dashboard); }, 400);
} }
function poll() { function poll() {
if (redirected) return; if (ready) return;
// /ready 就绪即。no-cors 下拿不到 status但 fetch 成功 resolve 就说明端口活了。 // /ready 就绪即显示选择页。no-cors 下拿不到 status但 fetch 成功 resolve 就说明端口活了。
fetch(base + '/ready', { mode: 'no-cors', cache: 'no-store' }) fetch(base + '/ready', { mode: 'no-cors', cache: 'no-store' })
.then(function () { goDashboard(); }) .then(function () { showChoice(); })
.catch(function () { .catch(function () {
// 端口还没起,继续等 // 端口还没起,继续等
setTimeout(poll, 1000); setTimeout(poll, 1000);
@@ -127,7 +186,7 @@ a.manual { color: #fff; text-decoration: underline; opacity: .9; }
// 兜底5 分钟还没起来,提示去控制台 // 兜底5 分钟还没起来,提示去控制台
setTimeout(function () { setTimeout(function () {
if (!redirected) { if (!ready) {
document.getElementById('status').innerHTML = document.getElementById('status').innerHTML =
'启动较慢,可先打开 <a class="manual" href="' + configCenter + '">配置中心</a> 检查'; '启动较慢,可先打开 <a class="manual" href="' + configCenter + '">配置中心</a> 检查';
} }

View File

@@ -1,14 +1,11 @@
@echo off @echo off
REM wait-gateway.bat - open Dashboard only after the Gateway is actually listening. REM wait-gateway.bat - fallback watcher after Windows-Start opens loading.html.
REM Fixes issue #46/#48: on slow USB drives the gateway needs tens of seconds to REM Slow USB drives can need tens of seconds before the gateway listens.
REM stage bundled deps on first run; opening http://127.0.0.1:PORT before it is
REM LISTENING shows "connection refused" and users think it is broken.
REM REM
REM 角色变更:现在 Windows-Start.bat 会先开 lib\loading.html 启动首屏,由首屏 REM Windows-Start opens Config Center immediately, so the ready path only
REM 自己轮询 /ready 并自动跳 Dashboard。本脚本退居"兜底"——万一首屏页的 REM exits quietly. On timeout, reopen Config Center as a recovery hint.
REM file:// fetch 被浏览器拦住没跳成,这里仍轮询端口、就绪后开 Dashboard。
REM Usage (called in background by Windows-Start.bat): wait-gateway.bat PORT REM Usage (called in background by Windows-Start.bat): wait-gateway.bat PORT
REM Polls every 2s, up to 150 tries (~5 min). Opens the browser once ready. REM Polls every 2 seconds, up to about 5 minutes.
set "PORT=%~1" set "PORT=%~1"
if "%PORT%"=="" set "PORT=18789" if "%PORT%"=="" set "PORT=18789"
@@ -23,8 +20,6 @@ timeout /t 2 /nobreak >nul
goto :wait_loop goto :wait_loop
:ready :ready
timeout /t 1 /nobreak >nul
start "" http://127.0.0.1:%PORT%/#token=uclaw
exit /b 0 exit /b 0
:timeout :timeout

View File

@@ -45,6 +45,41 @@ test('portable Windows launchers disable OpenClaw bonjour discovery', () => {
} }
}); });
test('Windows startup keeps Config Center available even after model setup', () => {
const script = readRepoFile('portable', 'Windows-Start.bat');
assert.match(
script,
/Opening Config Center[\s\S]*start "" http:\/\/127\.0\.0\.1:18788\//,
'Windows-Start.bat should always open Config Center for model/channel changes',
);
assert.doesNotMatch(
script,
/if not defined MODEL_CONFIGURED/,
'Config Center should not be gated on first-time setup only',
);
});
test('Windows gateway fallback does not force-open Dashboard', () => {
const script = readRepoFile('portable', 'lib', 'wait-gateway.bat');
assert.match(
script,
/:timeout[\s\S]*start "" http:\/\/127\.0\.0\.1:18788\//,
'wait-gateway.bat should return users to Config Center on timeout',
);
assert.doesNotMatch(
script,
/#token=uclaw/,
'fallback should not push configured users straight into Dashboard',
);
assert.doesNotMatch(
lineOf(script, ':ready') + '\n' + lineOf(script, 'exit /b 0'),
/start ""/,
'ready fallback should not open duplicate browser tabs',
);
});
test('PowerShell installer generated start.bat disables OpenClaw bonjour discovery', () => { test('PowerShell installer generated start.bat disables OpenClaw bonjour discovery', () => {
const script = readRepoFile('install', 'install.ps1'); const script = readRepoFile('install', 'install.ps1');