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>
<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">→ 注册获取 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 class="model-card" data-provider="deepseek" data-base="https://api.deepseek.com/v1" data-model="deepseek-chat">
<span class="check"></span>
@@ -255,7 +255,7 @@ input:focus { border-color: #ff6b35; }
<p class="desc" id="step2desc">请填写对应模型的 API Key</p>
<div class="api-info" id="buyInfo">
<h4>📌 如何获取 API Key</h4>
<h4>📌 获取 / 充值 API Key</h4>
<div class="provider-links" id="providerLinks"></div>
</div>
@@ -477,7 +477,7 @@ function setupStep2() {
linksDiv.innerHTML = '';
if (buyLink) {
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
@@ -498,7 +498,7 @@ function togglePw() {
function buildOpenClawConfig(provider, base, model, apiKey) {
// Base config that OpenClaw expects
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' },
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 &
GW_PID=$!
# ---- 11. 是否已配置模型openclaw.json 含 "providers" 即视为已配置 (issue #24) ----
# 已配置:只开 Dashboard不再每次弹配置页。未配置首次开 Config Center 引导填 Key
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
# ---- 11. 立刻打开"启动首屏",给用户即时反馈(移植自 4.0 splash----
# 首屏 loading.html 自己轮询 /ready就绪后停在选择页不再自动冲进 Dashboard
echo -e " ${YELLOW}首次启动需准备运行环境,约 30-90 秒,请稍候...${NC}"
# 用 file:// URL 确保 query string?port=)能传给浏览器;裸路径 open 会把整串当文件名。
open "file://$UCLAW_DIR/lib/loading.html?port=$PORT&token=uclaw" 2>/dev/null || true
# 首次未配置:再开 Config Center 引导填 Key
if [ "$MODEL_CONFIGURED" != "1" ]; then
open "http://127.0.0.1:18788/" 2>/dev/null || true
fi
# 每次都打开 Config Center,方便改模型、充值/获取 Key、连接微信等渠道。
open "http://127.0.0.1:18788/" 2>/dev/null || true
# ---- 12b. gateway 首轮预热(后台、静默、非阻塞)----
# ---- 11b. gateway 首轮预热(后台、静默、非阻塞)----
# 就绪后先唤醒 config/model 子系统,用户首次点发送时不再等。移植自 4.0 first-turn-prewarm。
"$NODE_BIN" "$UCLAW_DIR/lib/prewarm.mjs" "$PORT" uclaw >/dev/null 2>&1 &
# ---- 12c. 兜底:万一首屏页的 file:// fetch 被浏览器拦,仍轮询端口后开 Dashboard ----
# ---- 11c. 兜底:万一首屏页的 file:// fetch 被浏览器拦,仍静默轮询端口 ----
# 慢盘首启可达 90s+,轮询上限覆盖这段。最多 ~3 分钟180×1s
(
for i in $(seq 1 180); do
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
fi
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%\logs" mkdir "%DATA_DIR%\logs"
REM ── 加速:把"重 IO、可重建"的缓存从 U 盘搬到本机硬盘 ──────────────────
REM portable-cache.mjs 算出本机缓存目录(%LOCALAPPDATA%\U-Claw\slotUUID 隔离,
REM 换盘符仍复用),并把 .openclaw\browser 做成 junction 指向本机盘。
REM 浏览器 user-data(几百 MB 随机小写)和 V8 编译缓存因此落本机 SSD不再拖慢 U 盘。
REM 静默失败:取不到就跳过,缓存留 U 盘,照常启动。
REM Startup cache acceleration.
REM portable-cache.mjs chooses a local-disk cache slot and redirects
REM .openclaw\browser there with a junction when possible.
REM Browser user data and V8 compile cache stay off the USB drive.
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 (
if "%%a"=="UCLAW_COMPILE_CACHE_DIR" set "NODE_COMPILE_CACHE=%%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.
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"
call "%NPM_BIN%" install --registry=https://registry.npmmirror.com --ignore-scripts --no-audit --no-fund --omit=dev
echo.
@@ -144,9 +144,9 @@ echo Starting Config Center on port 18788...
set "CONFIG_SERVER=%UCLAW_DIR%config-server"
start /B "" "%NODE_BIN%" "%CONFIG_SERVER%\server.js" >nul 2>&1
REM Config Server 就绪 —— 动态探测而非写死等待。
REM 它通常 <1s 就起来,写死 timeout /t 2 是白等。改为每 ~0.3s 探测 18788
REM 最多 ~6s20 次)兜底。监听到就立刻往下走。
REM Wait for Config Server with polling instead of a fixed delay.
REM It is usually ready in under one second. Poll about every 0.3 seconds,
REM up to about 6 seconds, then continue anyway.
set /a CFG_TRIES=0
:wait_config
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
:config_ready
REM IMPORTANT: 不要在 gateway 启动前就开 Dashboard 浏览器!
REM 慢 U 盘上 OpenClaw 首次启动要 staging bundled deps(几十秒),
REM 过早打开 http://127.0.0.1:18789 会"拒绝连接",是 issue #46/#48 的根因。
REM 方案:立刻打开本地"启动首屏"loading.html给用户即时反馈移植自 4.0 splash
REM 首屏自己轮询 /readygateway 真就绪后自动跳 Dashboard——天然解决拒连。
REM Do not open Dashboard before the gateway is ready.
REM Slow USB drives may need tens of seconds to stage bundled deps.
REM Open the local startup page now, and always open Config Center too.
REM Config Center lets users change models, recharge/get keys, and connect channels.
REM 是否已配置模型openclaw.json 含 providers 即视为已配置 (issue #24)。
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 能传给浏览器。
REM Open startup page with the gateway port and token in the query string.
echo Opening startup screen...
set "LOADING_PATH=%UCLAW_DIR%lib\loading.html"
set "LOADING_URL=file:///%LOADING_PATH:\=/%?port=%PORT%&token=uclaw"
start "" "%LOADING_URL%"
if not defined MODEL_CONFIGURED (
echo First-time setup - opening Config Center...
start "" http://127.0.0.1:18788/
)
echo Opening Config Center...
start "" http://127.0.0.1:18788/
REM 后台等待器兜底:万一首屏页没起作用(浏览器拦本地 fetch 等),
REM 仍轮询端口gateway LISTENING 后开 Dashboard。
REM Fallback watcher: if the startup page cannot poll from file URLs,
REM keep polling and reopen Config Center after the gateway is ready.
start /B "" cmd /c ""%UCLAW_DIR%lib\wait-gateway.bat" %PORT%"
REM gateway 首轮预热(后台、静默、非阻塞):就绪后先唤醒 config/model 子系统,
REM 用户首次点发送时不再等。移植自 4.0 first-turn-prewarm。
REM Prewarm gateway in the background after it becomes ready.
start /B "" "%NODE_BIN%" "%UCLAW_DIR%lib\prewarm.mjs" %PORT% uclaw >nul 2>&1
echo.
@@ -194,7 +183,7 @@ echo ========================================
echo Starting OpenClaw Gateway on port %PORT%...
echo First run on a USB drive may take 30-90 seconds
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 ========================================
echo.

View File

@@ -178,9 +178,15 @@ input:focus { border-color: #ff6b35; }
<!-- Step 1: Select Model -->
<div class="section active" id="step1">
<h2>选择 AI 模型</h2>
<p class="desc">推荐国内用户用 MiniMax / Kimi / DeepSeek</p>
<p class="desc">最省心:用「虾盘云」一个 Key 调用国内外全部大模型(中转站)。也可用下面各家自己的官方 Key。</p>
<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">
<span class="check"></span>
<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>
<div class="api-info" id="buyInfo">
<h4>📌 如何获取 API Key</h4>
<h4>📌 获取 / 充值 API Key</h4>
<div class="provider-links" id="providerLinks"></div>
</div>
@@ -456,7 +462,7 @@ function goStep(n) {
}
const PROVIDER_NAMES = {
minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
'uclaw-cloud': '虾盘云', minimax: 'MiniMax', kimi: 'Kimi (月之暗面)', deepseek: 'DeepSeek',
zai: '智谱 GLM', qwen: '通义千问', doubao: '豆包',
openai: 'OpenAI', anthropic: 'Anthropic Claude', groq: 'Groq',
siliconflow: '硅基流动', custom: '自定义'
@@ -474,7 +480,8 @@ function setupStep2() {
var linksDiv = document.getElementById('providerLinks');
linksDiv.innerHTML = '';
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>' +
'<a href="https://open.bigmodel.cn/" target="_blank">🔑 智谱 GLM — 有免费额度</a>' +
@@ -492,7 +499,7 @@ function togglePw(id) {
// --- Build OpenClaw config ---
function buildConfig(provider, base, model, apiKey) {
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' },
meta: { lastTouchedVersion: '2026.3.13', lastTouchedAt: new Date().toISOString() }
};

View File

@@ -7,7 +7,12 @@
<!--
启动首屏(移植自 v2 u-clawx 4.0 的 splash 思路)。
双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。
本页每秒自查 gateway 是否就绪fetch /ready,就绪后自动跳转 Dashboard
本页每秒自查 gateway 是否就绪fetch /ready
就绪后【不自动跳转】,而是停在选择页,给两个大按钮:
🚀 进入 Dashboard开始聊天
⚙️ 配置 / 改模型(打开配置中心)
让用户自己决定去聊天还是改配置——避免"配过模型就被冲进 Dashboard、想改配置还得自己翻标签"。
端口通过 URL 查询参数传入loading.html?port=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;
}
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>
</head>
<body>
<div class="box">
<div class="logo">🦞</div>
<h1>U-Claw 正在启动</h1>
<div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会自动打开。</div>
<div class="spinner"></div>
<div class="bar"><i></i></div>
<div class="status" id="status">正在等待 OpenClaw 网关…</div>
<div class="elapsed" id="elapsed"></div>
<div class="note">
慢 U 盘首次启动通常 3090 秒。<br>
如果迟迟没反应,可手动打开
<a class="manual" id="manual" href="#">控制台</a>
<!-- 启动中 -->
<div class="loading-wrap">
<div class="logo">🦞</div>
<h1>U-Claw 正在启动</h1>
<div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会提示你下一步。</div>
<div class="spinner"></div>
<div class="bar"><i></i></div>
<div class="status" id="status">正在等待 OpenClaw 网关…</div>
<div class="elapsed" id="elapsed"></div>
<div class="note">
慢 U 盘首次启动通常 3090 秒。<br>
如果迟迟没反应,可先打开
<a class="manual" id="manual" href="#">配置中心</a>
</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>
<script>
@@ -90,10 +147,12 @@ a.manual { color: #fff; text-decoration: underline; opacity: .9; }
var dashboard = base + '/#token=' + encodeURIComponent(token);
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 redirected = false;
var ready = false;
function fmt(ms) {
var s = Math.floor(ms / 1000);
@@ -101,23 +160,23 @@ a.manual { color: #fff; text-decoration: underline; opacity: .9; }
}
function tickElapsed() {
if (ready) return;
document.getElementById('elapsed').textContent = fmt(Date.now() - startedAt);
}
setInterval(tickElapsed, 1000);
tickElapsed();
function goDashboard() {
if (redirected) return;
redirected = true;
document.getElementById('status').textContent = '网关已就绪,正在打开控制台…';
setTimeout(function () { location.replace(dashboard); }, 400);
function showChoice() {
if (ready) return;
ready = true;
document.body.classList.add('ready');
}
function poll() {
if (redirected) return;
// /ready 就绪即。no-cors 下拿不到 status但 fetch 成功 resolve 就说明端口活了。
if (ready) return;
// /ready 就绪即显示选择页。no-cors 下拿不到 status但 fetch 成功 resolve 就说明端口活了。
fetch(base + '/ready', { mode: 'no-cors', cache: 'no-store' })
.then(function () { goDashboard(); })
.then(function () { showChoice(); })
.catch(function () {
// 端口还没起,继续等
setTimeout(poll, 1000);
@@ -127,7 +186,7 @@ a.manual { color: #fff; text-decoration: underline; opacity: .9; }
// 兜底5 分钟还没起来,提示去控制台
setTimeout(function () {
if (!redirected) {
if (!ready) {
document.getElementById('status').innerHTML =
'启动较慢,可先打开 <a class="manual" href="' + configCenter + '">配置中心</a> 检查';
}

View File

@@ -1,14 +1,11 @@
@echo off
REM wait-gateway.bat - open Dashboard only after the Gateway is actually listening.
REM Fixes issue #46/#48: on slow USB drives the gateway needs tens of seconds to
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 wait-gateway.bat - fallback watcher after Windows-Start opens loading.html.
REM Slow USB drives can need tens of seconds before the gateway listens.
REM
REM 角色变更:现在 Windows-Start.bat 会先开 lib\loading.html 启动首屏,由首屏
REM 自己轮询 /ready 并自动跳 Dashboard。本脚本退居"兜底"——万一首屏页的
REM file:// fetch 被浏览器拦住没跳成,这里仍轮询端口、就绪后开 Dashboard。
REM Windows-Start opens Config Center immediately, so the ready path only
REM exits quietly. On timeout, reopen Config Center as a recovery hint.
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"
if "%PORT%"=="" set "PORT=18789"
@@ -23,8 +20,6 @@ timeout /t 2 /nobreak >nul
goto :wait_loop
:ready
timeout /t 1 /nobreak >nul
start "" http://127.0.0.1:%PORT%/#token=uclaw
exit /b 0
: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', () => {
const script = readRepoFile('install', 'install.ps1');