Files
u-claw/portable/lib/prewarm.mjs
hfshfg f715aef9c9 perf(portable): U盘启动加速 — 缓存搬本机 + 启动首屏 + 首轮预热
便携版从 U 盘启动慢,瓶颈在 U 盘随机小写 IO + 首屏无反馈 + 首轮冷启动。
移植 v2 u-clawx 4.0 的 4 个可在纯脚本层复刻的手段(均零依赖、静默失败):

- portable-cache.mjs: 把浏览器 user-data(几百 MB 随机小写)+ V8 编译缓存
  从 U 盘搬到本机 SSD(win junction / mac symlink 重定向 .openclaw/browser,
  NODE_COMPILE_CACHE 落本机)。UUID 隔离让换盘符仍命中同一份缓存。
  业务数据(openclaw.json/memory/账号)仍留 U 盘,便携性不变。
- loading.html: 双击即弹的启动首屏,自轮询 /ready,gateway 真就绪后自动跳
  Dashboard——天然规避"gateway 没起就开 Dashboard 拒连"(issue #46/#48)。
- prewarm.mjs: gateway 首轮预热,后台静默唤醒 config/model 子系统。
- Windows-Start.bat: 等 config-server 改写死 timeout 为动态轮询 18788。
- wait-gateway.bat 退居兜底(首屏页负责主跳转)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:38:05 +08:00

92 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// prewarm.mjs — gateway 首轮预热(移植自 v2 u-clawx 4.0 的 first-turn-prewarm 思路)
//
// 问题:用户首次点"发送"时gateway 才去加载配置、解析 provider 鉴权、拉模型列表,
// 慢 U 盘 + 冷启动下这一下要等好几秒。
// 方案gateway 端口 LISTENING 后,后台静默地把这些"首轮才会触发"的子系统先唤醒一遍:
// 轮询 /ready → 命中后依次 GET /status、/models带 token让 config/model
// 缓存在 runtime 内存里热起来。用户真正发第一条消息时就不再等。
//
// 设计原则(与 lib/ 其它脚本一致):
// - 纯 Node、零依赖只用 fetch
// - 静默失败:任何错误都不抛、不影响 gateway
// - 后台 detach 跑(启动脚本用 start /B 或 &),绝不阻塞
// - 短超时、有上限,不会挂着不退
//
// CLI 用法(启动脚本后台调用):
// node prewarm.mjs <PORT> [TOKEN]
// 默认 PORT=18789, TOKEN=uclaw
const READY_TIMEOUT_MS = 90_000; // 最多等 gateway 就绪 90s慢 U 盘首启可能要这么久)
const POLL_INTERVAL_MS = 1_000;
const STEP_TIMEOUT_MS = 8_000;
function log(msg) {
// 写 stderr不污染可能被采集的 stdout启动脚本一般 >nul 丢弃
try { process.stderr.write(`[prewarm] ${msg}\n`); } catch { /* 静默 */ }
}
async function httpGet(url, token, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, {
signal: controller.signal,
headers: token ? { 'x-openclaw-token': token } : {},
});
// 读掉 body 让对端真正干完活(否则可能短路)
await res.text().catch(() => {});
return res.ok || res.status === 401; // 401 也算"服务在",说明端口活了
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
async function waitReady(base, token, deadline) {
while (Date.now() < deadline) {
if (await httpGet(`${base}/ready`, token, 3_000)) return true;
if (await httpGet(`${base}/healthz`, token, 3_000)) return true;
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
return false;
}
export async function prewarm({ port = 18789, token = 'uclaw' } = {}) {
const base = `http://127.0.0.1:${port}`;
const deadline = Date.now() + READY_TIMEOUT_MS;
const ready = await waitReady(base, token, deadline);
if (!ready) {
log(`gateway not ready within ${READY_TIMEOUT_MS}ms, skip`);
return { ok: false, reason: 'not-ready' };
}
// 依次唤醒statusgateway/config→ modelsprovider 鉴权 + 模型目录)
const steps = ['/status', '/models'];
const failed = [];
const started = Date.now();
for (const path of steps) {
const t0 = Date.now();
const ok = await httpGet(`${base}${path}`, token, STEP_TIMEOUT_MS);
log(`step ${path} ${ok ? 'ok' : 'failed'} ${Date.now() - t0}ms`);
if (!ok) failed.push(path);
}
log(`done ${Date.now() - started}ms failed=${failed.length ? failed.join(',') : 'none'}`);
return { ok: true, failed };
}
import { pathToFileURL } from 'node:url';
const isMain = (() => {
try { return !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; }
catch { return false; }
})();
if (isMain) {
const port = parseInt(process.argv[2], 10) || parseInt(process.env.UCLAW_GATEWAY_PORT, 10) || 18789;
const token = process.argv[3] || process.env.UCLAW_GATEWAY_TOKEN || 'uclaw';
prewarm({ port, token })
.then(() => process.exit(0))
.catch((err) => { log(`fatal ${err && err.message}`); process.exit(0); });
}