feat: 海外化改造(新加坡市场)—— 阶段 0-3
Some checks failed
Tests / test (push) Has been cancelled

按 U-Claw-海外化改造方案.md 与 范围决策记录.md 实施。这是 fork,不回上游:
海外版删掉的正是上游的中国市场默认值。

阶段 0 地基
- 下载源全部改国际:脚本/CI 61 处 + lockfile 880 条 npmmirror URL 归零
  (lockfile 那 880 条是 npm 的 resolved 字段,脚本层参数化根本绕不过它)
- 移除 install.ps1 里三个第三方 GitHub 加速代理,bundle 改直连 + SHA256 校验
  (原来只检查"文件大于 1MB"就解压运行)
- 技能内容与分发分离:skills/manifest.json 单一来源,install.sh 1170→658 行、
  install.ps1 721→546 行,两者技能内容归零
  实测原来是三份不一致:skills-cn 完整、install.sh 约 40%、install.ps1 约 17%,
  且 7 个通用技能只有 U 盘版有 —— 一键安装的用户一个能用的技能都没有
- Node 版本三种(v22.14/16/22.1)统一,新建 NODE_VERSION 单一来源
- Config 页三份合一。portable/Config.html 用根相对路径调 API 却只从 file:// 打开,
  保存功能已静默失效两个月;现缩为 120 行重定向壳
- 测试接入 CI(此前 node --test 无人运行,所有断言形同虚设)

阶段 1 双语可用
- 浏览器侧 i18n:JSON 为源、生成经典 script(file:// 下 fetch 本地 JSON 被拦)
  语言跟盘走不跟机器走:启动器写 data/.openclaw/locale.js
- 8 处硬编码 lang="zh-CN" 归零,data-i18n 覆盖 213 处,词条 en/zh 各 279 条
- B3 单框 Key:12 张模型卡 → 一个输入框,前缀识别 provider,
  服务端 /api/test-key 发 1-token 请求实测,错误映射成人话
  Key 填错到得知:从"直到对话失败"降到 ≤1 秒
- 区域格式 SG:DD/MM/YYYY、12 小时、S$、Asia/Singapore
  (ICU 在 en-SG 下把 SGD 渲染成裸 $,与美元无法区分,故自行拼 S$)
- README 内容分叉而非翻译,§1.3 证据清单逐条清零

阶段 2 降门槛
- 启动逻辑上移 lib/start.mjs:Windows-Start.bat 220→28 行、
  Mac-Start.command 235→33 行
  修掉 Mac 侧两个 bug:控制台端口硬编码 18788(回落时打开死页)、
  微信插件从未在 Mac 上安装
- U 盘根目录 23 → 3 个可点文件,其余进 advanced/
- 首启向导:语言 → 用途(7 角色,manifest 驱动)→ 密钥,答过不再问
- 三档界面,Simple 档隐藏一切技术名词
- 自动自愈:启动失败先自查自修,修不好导出脱敏诊断包
  (Doctor 从"用户要知道去点的工具"变成后台机制)

阶段 3 技能库
- 19 个英文技能,planned 归零。sg-weather / sg-transport 的端点均实测过
- SkillHub 从 56 张手写第三方卡片改为 manifest 生成:703→125 行,中文归零

其他
- origin.json 收拢所有运行时地址,tests/origin.test.mjs 保证迁移不会漏
- portable/ 下用户可见中文归零(由断言保证)
- 82 项测试

未验证(本机无 Windows / 无 pwsh):
- install.ps1、setup.ps1 约 210 行改动从未经 PowerShell 解析器
- 完整启动路径仅在假 node + 假 openclaw 上冒烟
- 8 个 .bat 的盘根推导仅静态断言
详见 U盘实测清单.md

受阻:
- 隐藏黑窗口 —— 需代码签名证书(.vbs 已被 Windows 弃用,替代方案都要签名)
- 场景卡 —— OpenClaw 上游 Dashboard 无预填 prompt 接口
- 官网 36 条 —— 上游 2026-04-14 拆到私有仓库,无权限
This commit is contained in:
2026-08-17 18:37:49 +08:00
parent 9a23aec097
commit b076815171
123 changed files with 13396 additions and 14723 deletions

View File

@@ -4,19 +4,19 @@
// 1. 读取 OPENCLAW_VERSION 文件得到当前版本号
// 2. 5s timeout 拉 OSS 上的 latest.json
// 3. 比对版本号,有新版就写 update-available.json 到 STATE_DIR
// 4. Welcome.html / Config.html 启动时读这个文件,有就显示提示条
// 4. Read me first.html / Config.html 启动时读这个文件,有就显示提示条
//
// 设计原则:
// - 静默失败网络坏、OSS 挂、json 格式错、本地版本号缺失,都不能影响 OpenClaw 启动
// - 只读不下载:检查到新版只写 update-available.json下载交给用户点链接到浏览器去做
// - 异步:调用方应该 detach 跑Windows-Start.bat 用 start /B不阻塞主流程
// - 异步:调用方应该 detach 跑(START HERE - Windows.bat 用 start /B不阻塞主流程
//
// latest.json 格式(你在 OSS 上手动维护或用 publish-latest.mjs 生成):
// {
// "version": "2026.4.30",
// "releaseDate": "2026-05-03",
// "downloadUrl": "https://u-claw-oss.56chat.cn/u-claw-open/u-claw-portable-v2026.4.30.zip",
// "releasePageUrl": "https://github.com/dongsheng123132/u-claw/releases/tag/v2026.4.30",
// "releasePageUrl": "https://gitea.fanghe.it.com/zhenghy/u-claw/releases/tag/v2026.4.30",
// "notes": "修复 xxx新增 yyy"
// }

81
portable/lib/i18n.mjs Normal file
View File

@@ -0,0 +1,81 @@
// Message catalogue for the launcher and the local HTML pages.
//
// Locale resolution follows one rule: the language travels with the drive, not
// with the machine. Someone who set up the drive in Chinese and plugs it into a
// colleague's English Windows should still see Chinese.
//
// explicit override > drive config > system locale > en
//
// Keys are semantic (`start.node_missing`), never the source string, so changing
// the English wording does not invalidate every other catalogue.
import { readFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const SUPPORTED = ['en', 'zh-CN'];
const FALLBACK = 'en';
function normalise(tag) {
if (!tag) return null;
const lower = String(tag).toLowerCase();
if (lower.startsWith('zh')) return 'zh-CN';
if (lower.startsWith('en')) return 'en';
return null;
}
// Reads uclaw.locale out of the drive's own config. A malformed or missing file
// is not an error — it just means we have no preference yet.
function localeFromConfig(configPath) {
if (!configPath || !existsSync(configPath)) return null;
try {
return normalise(JSON.parse(readFileSync(configPath, 'utf8'))?.uclaw?.locale);
} catch {
return null;
}
}
function localeFromSystem() {
const env = process.env.UCLAW_LOCALE || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG;
if (env) return normalise(env.split('.')[0].replace('_', '-'));
try {
return normalise(new Intl.DateTimeFormat().resolvedOptions().locale);
} catch {
return null;
}
}
export function resolveLocale({ override, configPath } = {}) {
return (
normalise(override) ||
localeFromConfig(configPath) ||
localeFromSystem() ||
FALLBACK
);
}
function loadCatalogue(locale) {
try {
return JSON.parse(readFileSync(join(here, 'messages', `${locale}.json`), 'utf8'));
} catch {
return null;
}
}
export function createTranslator(locale) {
const active = SUPPORTED.includes(locale) ? locale : FALLBACK;
const catalogue = loadCatalogue(active) ?? {};
// Always keep English loaded: a key missing from a translation should fall back
// to readable English, never to a raw key leaking into the UI.
const fallback = active === FALLBACK ? catalogue : (loadCatalogue(FALLBACK) ?? {});
return function t(key, vars = {}) {
const template = catalogue[key] ?? fallback[key] ?? key;
return template.replace(/\{(\w+)\}/g, (match, name) =>
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
);
};
}
export { SUPPORTED, FALLBACK };

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env node
// Generates lib/i18n/messages.js from lib/messages/*.json.
//
// The launcher (node) reads the JSON directly; the pages cannot, because they
// are opened from file:// where fetching local JSON is blocked. Rather than
// maintain two copies of every string, the JSON stays the source of truth and
// this emits a plain <script> the pages can load.
//
// node lib/i18n/build-messages.mjs [--check]
//
// --check exits non-zero if the generated file is stale, for CI.
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const messagesDir = join(here, '..', 'messages');
const outputPath = join(here, 'messages.js');
const skillsOutputPath = join(here, 'skills-data.js');
const catalogues = {};
for (const file of readdirSync(messagesDir).sort()) {
if (!file.endsWith('.json')) continue;
catalogues[file.replace(/\.json$/, '')] = JSON.parse(readFileSync(join(messagesDir, file), 'utf8'));
}
const generated = `/* GENERATED FILE — do not edit.
* Source: lib/messages/*.json
* Regenerate: node lib/i18n/build-messages.mjs
*
* Exists because pages opened from file:// cannot fetch local JSON.
*/
window.UCLAW_I18N_MESSAGES = ${JSON.stringify(catalogues, null, 2)};
`;
// The skill catalogue travels the same way and for the same reason: SkillHub is
// opened from file://, where fetching manifest.json is blocked.
function buildSkillsData() {
for (const candidate of [join(here, '..', '..', 'skills'), join(here, '..', '..', '..', 'skills')]) {
if (!existsSync(join(candidate, 'manifest.json'))) continue;
const manifest = JSON.parse(readFileSync(join(candidate, 'manifest.json'), 'utf8'));
return `/* GENERATED FILE — do not edit.
* Source: skills/manifest.json
* Regenerate: node lib/i18n/build-messages.mjs
*/
window.UCLAW_SKILLS = ${JSON.stringify({
personas: manifest.personas,
skills: manifest.skills
.filter((s) => s.status === 'shipping')
.map(({ id, categories, personas, emoji }) => ({ id, categories, personas, emoji })),
}, null, 2)};
`;
}
return null;
}
const generatedSkills = buildSkillsData();
if (process.argv.includes('--check')) {
let current = null;
try { current = readFileSync(outputPath, 'utf8'); } catch { /* not generated yet */ }
let currentSkills = null;
try { currentSkills = readFileSync(skillsOutputPath, 'utf8'); } catch { /* not generated yet */ }
if (current !== generated || (generatedSkills && currentSkills !== generatedSkills)) {
process.stderr.write('lib/i18n generated files are stale — run: node lib/i18n/build-messages.mjs\n');
process.exit(1);
}
process.stdout.write('generated files are up to date\n');
} else {
writeFileSync(outputPath, generated, 'utf8');
if (generatedSkills) writeFileSync(skillsOutputPath, generatedSkills, 'utf8');
const count = Object.keys(catalogues).map((l) => `${l}:${Object.keys(catalogues[l]).length}`).join(' ');
process.stdout.write(`wrote lib/i18n/messages.js (${count})\n`);
}

152
portable/lib/i18n/i18n.js Normal file
View File

@@ -0,0 +1,152 @@
/* U-Claw browser-side i18n.
*
* A classic script, not an ES module, and the catalogue is a script rather than
* JSON on purpose: these pages are opened from file:// (the launcher opens
* loading.html with a file:// URL, Welcome.html links to its siblings), and from
* a file:// origin both fetch() and ES-module imports of local files are blocked.
* That is the exact bug that left the old Config.html silently broken. A plain
* <script src> tag still works.
*
* Load order on a page:
* <script src="data/.openclaw/locale.js"></script> optional, written by the launcher
* <script src="lib/i18n/messages.js"></script>
* <script src="lib/i18n/i18n.js"></script>
*
* Then mark text with data-i18n="key". Attributes use data-i18n-attr="placeholder:key".
* The HTML keeps English as its literal fallback, so a missing catalogue degrades
* to readable English instead of raw keys.
*/
(function (global) {
'use strict';
var SUPPORTED = ['en', 'zh-CN'];
var FALLBACK = 'en';
function normalise(tag) {
if (!tag) return null;
var lower = String(tag).toLowerCase();
if (lower.indexOf('zh') === 0) return 'zh-CN';
if (lower.indexOf('en') === 0) return 'en';
return null;
}
// The language travels with the drive, not the machine: someone who set the
// drive up in Chinese and plugs it into a colleague's English Windows should
// still see Chinese. The launcher writes data/.openclaw/locale.js on startup;
// ?lang= is the manual override, and the browser language is the last resort.
function resolveLocale() {
var fromQuery = null;
try {
fromQuery = new URLSearchParams(global.location.search).get('lang');
} catch (e) { /* older browser or opaque URL */ }
return normalise(fromQuery)
|| normalise(global.UCLAW_LOCALE)
|| normalise(global.navigator && global.navigator.language)
|| FALLBACK;
}
var locale = resolveLocale();
var catalogues = global.UCLAW_I18N_MESSAGES || {};
var active = catalogues[SUPPORTED.indexOf(locale) === -1 ? FALLBACK : locale] || {};
var fallback = catalogues[FALLBACK] || {};
function t(key, vars) {
var template = active[key];
if (template === undefined) template = fallback[key];
if (template === undefined) return key;
if (!vars) return template;
return template.replace(/\{(\w+)\}/g, function (match, name) {
return Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match;
});
}
function apply(root) {
var scope = root || global.document;
scope.querySelectorAll('[data-i18n]').forEach(function (node) {
var value = t(node.getAttribute('data-i18n'));
// Keep the literal in the HTML when a key has no translation, rather than
// replacing readable English with the key name.
if (value !== node.getAttribute('data-i18n')) node.textContent = value;
});
scope.querySelectorAll('[data-i18n-attr]').forEach(function (node) {
node.getAttribute('data-i18n-attr').split(',').forEach(function (pair) {
var parts = pair.split(':');
if (parts.length !== 2) return;
var attr = parts[0].trim();
var value = t(parts[1].trim());
if (value !== parts[1].trim()) node.setAttribute(attr, value);
});
});
global.document.documentElement.lang = locale;
}
// Region defaults for Singapore. The old pages assumed China: Asia/Shanghai,
// YYYY-MM-DD, 24-hour, ¥. SG uses DD/MM/YYYY (neither Chinese nor American),
// 12-hour with am/pm, and S$.
var REGION = {
en: { locale: 'en-SG', timeZone: 'Asia/Singapore', currency: 'SGD', hour12: true },
'zh-CN': { locale: 'zh-SG', timeZone: 'Asia/Singapore', currency: 'SGD', hour12: true },
};
function region() {
return REGION[locale] || REGION.en;
}
function formatDate(date) {
var r = region();
try {
return new Intl.DateTimeFormat(r.locale, {
timeZone: r.timeZone, day: '2-digit', month: '2-digit', year: 'numeric',
}).format(date);
} catch (e) {
return String(date);
}
}
function formatTime(date) {
var r = region();
try {
return new Intl.DateTimeFormat(r.locale, {
timeZone: r.timeZone, hour: 'numeric', minute: '2-digit', hour12: r.hour12,
}).format(date);
} catch (e) {
return String(date);
}
}
// ICU renders SGD as a bare "$" in an en-SG locale, which is ambiguous next to
// USD prices. Prices are written S$ throughout the product, so format the
// number with Intl and put the symbol on ourselves rather than depending on
// whichever ICU build the browser or Node happens to ship.
function formatMoney(amount) {
var r = region();
try {
return 'S$' + new Intl.NumberFormat(r.locale, {
minimumFractionDigits: 2, maximumFractionDigits: 2,
}).format(amount);
} catch (e) {
return 'S$' + amount;
}
}
global.UClawI18n = {
locale: locale,
supported: SUPPORTED,
t: t,
apply: apply,
region: region,
formatDate: formatDate,
formatTime: formatTime,
formatMoney: formatMoney,
};
if (global.document.readyState === 'loading') {
global.document.addEventListener('DOMContentLoaded', function () { apply(); });
} else {
apply();
}
})(window);

View File

@@ -0,0 +1,566 @@
/* GENERATED FILE — do not edit.
* Source: lib/messages/*.json
* Regenerate: node lib/i18n/build-messages.mjs
*
* Exists because pages opened from file:// cannot fetch local JSON.
*/
window.UCLAW_I18N_MESSAGES = {
"en": {
"start.banner": "U-Claw · Portable AI Agent",
"start.node_version": "Node.js {version}",
"start.quarantine_removing": "Clearing the macOS security flag…",
"start.cache_local": "Cache on local disk: {path}",
"start.config_migrating": "Bringing your old settings across…",
"start.config_migrated": "Settings moved over.",
"start.config_creating": "First run — setting things up…",
"start.config_created": "Ready.",
"start.deps_preparing": "Getting things ready — about 20 seconds.",
"start.deps_slow_drive": "This drive is slow, so it may take a few minutes. You can leave it running.",
"start.deps_done": "Ready.",
"start.no_proxy": "Connecting directly to your model host: {hosts}",
"start.wechat_installing": "Installing the WeChat plugin…",
"start.wechat_installed": "WeChat plugin installed.",
"start.config_center_starting": "Starting the Control Panel…",
"start.config_center_port": "Control Panel is on port {port}.",
"start.port_in_use": "Port {port} is busy, trying the next one…",
"start.gateway_starting": "Starting U-Claw on port {port}…",
"start.opening_screen": "Opening the start screen…",
"start.opening_config": "Opening the Control Panel…",
"start.running_title": "U-Claw is running",
"start.running_dashboard": "Dashboard: {url}",
"start.running_config": "Control Panel: {url}",
"start.running_hint": "Keep this window open while you use U-Claw. Press Ctrl+C to stop.",
"start.first_run_wait": "First run from a USB drive takes 3090 seconds while components unpack. The Control Panel is already open so you can pick a model and paste a key.",
"start.stopped": "U-Claw stopped.",
"start.exited_unexpectedly": "U-Claw closed on its own (code {code}).",
"error.node_missing.what": "A file U-Claw needs is missing from the drive.",
"error.node_missing.why": "This usually means the copy onto the USB drive did not finish.",
"error.node_missing.action": "Copy the U-Claw folder onto the drive again, or run setup.sh to rebuild it.",
"error.unsupported_arch.what": "This Mac's processor is not supported ({arch}).",
"error.unsupported_arch.why": "U-Claw ships builds for Apple Silicon and Intel Macs only.",
"error.unsupported_arch.action": "Try U-Claw on a different computer.",
"error.no_port.what": "U-Claw could not find a free port to start on.",
"error.no_port.why": "Ports {from}{to} are all in use — U-Claw may already be running.",
"error.no_port.action": "Close any window already running U-Claw and start it again.",
"error.deps_failed.what": "U-Claw could not finish getting ready.",
"error.deps_failed.why": "The download did not complete — usually a dropped network connection.",
"error.deps_failed.action": "Check your Wi-Fi and start U-Claw again.",
"error.press_enter": "Press Enter to close this window.",
"loading.title": "Starting U-Claw",
"loading.starting": "Starting U-Claw…",
"loading.first_run": "First run from a USB drive unpacks the bundled components. Give it a moment — you will be told when it is ready.",
"loading.waiting_gateway": "Waiting for U-Claw to come up…",
"loading.ready": "U-Claw is ready",
"loading.choose_next": "Pick what to do next — start chatting, or set up your model and channels first.",
"loading.open_dashboard": "🚀 Start chatting",
"loading.open_settings": "⚙️ Settings",
"loading.settings_hint": "Add or change your API key, and connect chat apps.",
"loading.no_model_hint": "No model set up yet? Open Settings first to add a key.",
"loading.slow_drive_note": "A slow drive usually takes 3090 seconds. If nothing happens for a long time, open Settings:",
"loading.elapsed": "Waited {seconds}s",
"loading.taking_long": "This is taking longer than usual. You can open Settings while you wait:",
"welcome.title": "U-Claw — Getting Started",
"welcome.subtitle": "Your AI assistant on a USB drive · three steps",
"welcome.quickstart": "Quick start",
"welcome.step1_title": "1. Start U-Claw",
"welcome.step1_desc": "Double-click the file for your computer:",
"welcome.step2_title": "2. Add a key",
"welcome.step2_desc": "Settings opens on its own the first time. You need one API key to get going — Google Gemini is the easiest to obtain: sign in with a Google account, no card required.",
"welcome.step2_link": "Get a free Gemini key →",
"welcome.step3_title": "3. Start talking",
"welcome.step3_desc": "Once the key is saved, the chat window opens by itself. That is it.",
"welcome.portable_note": "✅ Your settings and history stay in the data/ folder on the drive, so they travel with you between Mac and Windows.",
"welcome.models_title": "Which model?",
"welcome.model_claude": "Best all-rounder",
"welcome.model_gpt": "Most widely used",
"welcome.model_gemini": "Free tier, easiest signup",
"welcome.model_local": "Offline · nothing leaves this machine",
"welcome.models_more": "More options, including SEA-LION for Malay, Tamil and Singlish, are in Settings.",
"welcome.trouble_title": "Something not working?",
"welcome.trouble_lead": "💡 Run the check-up tool",
"welcome.trouble_desc": "If U-Claw will not start, double-click:",
"welcome.open_settings": "Open Settings",
"welcome.open_docs": "Documentation",
"nav.title": "U-Claw — Start here",
"nav.tagline": "Portable AI agent — plug in and go",
"nav.status_off": "U-Claw is not running",
"nav.open_console": "Open it →",
"nav.quickstart": "Quick start",
"nav.doubleclick": "Double-click",
"nav.step2": "Settings opens by itself the first time — pick a model and paste your key.",
"nav.mac_gatekeeper": "⚠️ If macOS says the developer cannot be verified: right-click the file, then choose Open.",
"nav.config_shortcut": "📝 You can also double-click Config.html — it takes you to the settings page.",
"nav.win_smartscreen": "⚠️ If Windows shows a security warning: click More info, then Run anyway.",
"nav.everything": "Everything else",
"nav.menu_opens": "for the full menu:",
"nav.g_settings": "Settings",
"nav.g_settings_d": "Model, key and chat apps",
"nav.g_checkup": "Check-up",
"nav.g_checkup_d": "Finds and fixes common problems",
"nav.g_backup": "Backup",
"nav.g_backup_d": "Save settings and memory in one click",
"nav.g_skills": "Skills",
"nav.g_skills_d": "Browse what U-Claw can do",
"nav.g_system": "System info",
"nav.g_system_d": "Version, port, running state",
"nav.g_cli": "Command line",
"nav.g_cli_d": "For advanced use",
"nav.models_title": "Models you can use",
"nav.model_sealion": "Malay, Tamil and Singlish",
"nav.model_local": "Local model",
"nav.model_more": "More",
"nav.model_more_d": "Others are listed in Settings",
"nav.more_title": "More",
"nav.link_guide": "Getting started",
"nav.link_guide_d": "The three-step walkthrough",
"nav.link_skills": "Skills",
"nav.link_skills_d": "Browse what U-Claw can do",
"nav.link_contact": "Contact",
"nav.link_site": "Website",
"nav.status_on": "U-Claw is running on port {port}",
"nav.status_off_hint": "U-Claw is not running — double-click the start file first",
"key.page_title": "U-Claw Settings",
"key.page_subtitle": "Paste an API key and you are done.",
"key.checking": "Checking…",
"key.step_key": "Your key",
"key.step_done": "Done",
"key.heading": "Paste your API key",
"key.desc": "U-Claw works out the rest by itself. Your key is saved on this drive only — it is never uploaded anywhere.",
"key.placeholder": "sk-…",
"key.show": "Show",
"key.hide": "Hide",
"key.continue": "Continue →",
"key.checking_key": "Checking this key…",
"key.ok": "✓ {provider} · connected in {ms}ms",
"key.custom_provider": "Your endpoint",
"key.no_key_title": "Do not have a key yet?",
"key.no_key_desc": "Google Gemini is the quickest to get: sign in with a Google account, no card needed, and there is a free tier.",
"key.get_gemini": "🔑 Get a free Gemini key",
"key.get_openrouter": "🔑 OpenRouter — one key for many models",
"key.advanced": "Advanced — set the endpoint manually",
"key.base_url": "API endpoint",
"key.model_name": "Model name",
"key.advanced_hint": "Only needed for a self-hosted or unlisted provider. Leave blank to let U-Claw decide.",
"key.saved": "Saved.",
"key.err_empty": "Paste a key to continue.",
"key.err_not_checked": "Hold on — still checking that key.",
"key.err_rejected": "That key was not accepted. Check you copied all of it — keys are long and easy to cut short.",
"key.err_quota": "This key has run out of credit. Top it up with your provider, or paste a different key.",
"key.err_offline": "Cannot reach the internet. Check your Wi-Fi and try again.",
"key.err_timeout": "The provider did not answer in time. Try again in a moment.",
"key.err_model": "That key works, but the model it points to was not found. Set the model name under Advanced.",
"key.err_provider_down": "The provider is having trouble right now. Try again shortly.",
"key.err_unrecognised": "This key is not one U-Claw recognises. Fill in the endpoint and model under Advanced.",
"key.err_unknown": "Could not check that key. Try again, or use Advanced to set the endpoint yourself.",
"key.err_save": "Could not save. Make sure the drive is still plugged in, then try again.",
"hub.title": "U-Claw Skills",
"done.title": "You are set up",
"done.subtitle": "Your model and key are saved on this drive.",
"done.open": "🚀 Start chatting",
"done.restart_note": "Chat app settings are saved now and take effect after U-Claw restarts.",
"done.change_key": "Change model or key",
"done.view_config": "View config file",
"ch.title": "📱 Connect a chat app (optional)",
"ch.desc": "Click a card to fill it in, or skip this entirely.",
"ch.whatsapp_d": "Talk to U-Claw from WhatsApp",
"ch.whatsapp_warning": "⚠️ This uses an unofficial protocol. Your WhatsApp account can be banned for it. Do not connect a number you rely on.",
"ch.whatsapp_number": "Phone number",
"ch.telegram_d": "Talk to U-Claw from Telegram",
"ch.telegram_get": "→ Get one from @BotFather",
"ch.slack_d": "Talk to U-Claw from your workspace",
"ch.slack_get": "→ Create an app at api.slack.com",
"ch.discord_d": "Talk to U-Claw from Discord",
"ch.discord_get": "→ Create an app in the Discord developer portal",
"ch.china_group": "Chat apps used mainly in China",
"ch.wechat_d": "Scan a QR code to link a personal WeChat account",
"ch.wechat_start": "Click this card to show the QR code",
"ch.saved": "Chat app settings saved.",
"ch.err_save": "Could not save the chat app settings. Check the drive is still plugged in.",
"info.layout_title": "What is on the drive",
"info.layout_desc": "How the folders are organised",
"info.skills": "Skills",
"info.skills_d": "Drop a SKILL.md in here and it is picked up automatically",
"info.app": "Runtime",
"info.app_d": "Node.js and the OpenClaw core",
"info.data": "Your data",
"info.data_d": "Your key and chat history. Stays on this drive.",
"info.feedback_title": "Found a problem?",
"info.feedback_desc": "U-Claw is open source. Report anything that breaks on GitHub — nothing is collected or uploaded from your machine automatically.",
"info.feedback_link": "→ Open an issue on GitHub",
"wx.getting_qr": "Fetching the QR code…",
"wx.scan": "Scan the code above with WeChat",
"wx.qr_failed": "Could not fetch the QR code",
"wx.connected": "✅ WeChat connected",
"wx.account": "Account",
"wx.restart_needed": "One more thing: restart U-Claw (close it and open it again) before WeChat will work.",
"wx.connected_toast": "WeChat connected. It works after U-Claw restarts.",
"wx.qr_refreshed": "The code refreshed — scan it again",
"wx.confirm_on_phone": "Scanned. Confirm on your phone…",
"wx.qr_expired": "That code expired.",
"wx.qr_retry": "Get a new one",
"wx.failed": "Could not connect",
"info.read_config_failed": "Could not read the config file:",
"status.running": "U-Claw is running on port {port}",
"status.open_dashboard": "Open it →",
"status.not_running": "U-Claw is not running — start it with Windows-Start.bat or Mac-Start.command",
"key.which_model": "Which model?",
"start.opening_language": "Asking which language to use…",
"persona.title": "What will you mostly use this for?",
"persona.subtitle": "This sets up the right tools for you. You can change it later, and picking more than one is fine.",
"persona.continue": "Continue →",
"persona.skip": "Skip this",
"persona.saving": "Setting things up…",
"persona.err_offline": "U-Claw is not running yet. Start it, then open this page again.",
"persona.err_save": "Could not save that. Check the drive is still plugged in, then try again.",
"persona.developer": "Writing code",
"persona.admin": "Documents and email",
"persona.sales": "Talking to customers",
"persona.marketing": "Content and social",
"persona.finance": "Numbers and spreadsheets",
"persona.boss": "Running a business",
"persona.general": "Just having a look",
"start.opening_persona": "Asking what you will use it for…",
"tier.label": "How much do you want to see?",
"tier.simple": "Just the essentials",
"tier.standard": "A bit more control",
"tier.expert": "Everything",
"tier.changed_simple": "Switched to the simple view — advanced options are hidden.",
"tier.changed_standard": "Switched to the standard view — you can now change models and connect more chat apps.",
"tier.changed_expert": "Switched to the full view — everything is visible, including the config file.",
"start.checking": "Something went wrong — checking…",
"start.repaired": "Fixed. Trying again.",
"start.diagnostics_written": "A report of what happened was saved to: {path}",
"start.diagnostics_hint": "API keys have been removed from it. Send it to help@u-claw.org if you want a hand.",
"skill.excel-helper.name": "Spreadsheets",
"skill.excel-helper.desc": "Formulas, pivot tables, charts, cleaning messy data",
"skill.word-writer.name": "Documents",
"skill.word-writer.desc": "Reports, proposals, CVs — drafting and formatting",
"skill.ppt-designer.name": "Slide decks",
"skill.ppt-designer.desc": "Structure, layout and speaker notes",
"skill.pdf-toolkit.name": "PDF tools",
"skill.pdf-toolkit.desc": "Merge, split, extract text — all on this machine",
"skill.image-compress.name": "Images",
"skill.image-compress.desc": "Shrink, resize and convert — nothing is uploaded",
"skill.qrcode-maker.name": "QR codes",
"skill.qrcode-maker.desc": "Links, text and WiFi details, generated offline",
"skill.web-to-markdown.name": "Save a web page",
"skill.web-to-markdown.desc": "Turn any article into clean Markdown",
"skill.linkedin-post.name": "LinkedIn posts",
"skill.linkedin-post.desc": "Hooks and structure, without the LinkedIn voice",
"skill.x-poster.name": "Posts for X",
"skill.x-poster.desc": "Short posts and threads that stand on their own",
"skill.tiktok-script.name": "TikTok scripts",
"skill.tiktok-script.desc": "Hooks that survive the first second",
"skill.youtube-script.name": "YouTube scripts",
"skill.youtube-script.desc": "Titles, the first 30 seconds, and chapters",
"skill.medium-writer.name": "Articles",
"skill.medium-writer.desc": "Long-form structure, and cutting the padding",
"skill.email-campaign.name": "Email campaigns",
"skill.email-campaign.desc": "Subject lines, one clear ask, PDPA-safe",
"skill.web-search.name": "Web search",
"skill.web-search.desc": "Find current information and read the sources",
"skill.sg-weather.name": "Singapore weather",
"skill.sg-weather.desc": "Forecast by area and air quality, from NEA",
"skill.sea-translate.name": "SEA translation",
"skill.sea-translate.desc": "English, Chinese, Malay, Tamil — and register",
"skill.claude-helper.name": "Better answers",
"skill.claude-helper.desc": "How to ask, and when to check the answer",
"skill.sg-transport.name": "Singapore transport",
"skill.sg-transport.desc": "Bus arrivals, MRT alerts, carpark lots",
"skill.meeting-notes.name": "Meeting notes",
"skill.meeting-notes.desc": "Decisions and owners, ready to send",
"cat.office": "Office",
"cat.data": "Data",
"cat.writing": "Writing",
"cat.files": "Files",
"cat.research": "Research",
"cat.social": "Social",
"cat.video": "Video",
"cat.language": "Language",
"cat.local": "Singapore",
"cat.ai": "Using AI",
"hub.lede": "These are on this drive already. Just ask for what you want — you do not have to name the skill.",
"hub.all": "All",
"hub.none": "Nothing in this category.",
"hub.add_note": "Skills live in the skills/ folder on this drive. Dropping a SKILL.md in there adds one."
},
"zh-CN": {
"start.banner": "U-Claw · 便携 AI 助手",
"start.node_version": "Node.js {version}",
"start.quarantine_removing": "正在解除 macOS 安全限制…",
"start.cache_local": "缓存已放在本机硬盘:{path}",
"start.config_migrating": "正在迁移你原来的设置…",
"start.config_migrated": "设置已迁移。",
"start.config_creating": "第一次使用,正在准备…",
"start.config_created": "准备好了。",
"start.deps_preparing": "正在准备,约 20 秒。",
"start.deps_slow_drive": "这个 U 盘比较慢,可能要几分钟,放着就行。",
"start.deps_done": "准备好了。",
"start.no_proxy": "将直连你的模型地址:{hosts}",
"start.wechat_installing": "正在安装微信插件…",
"start.wechat_installed": "微信插件已安装。",
"start.config_center_starting": "正在启动控制台…",
"start.config_center_port": "控制台端口:{port}",
"start.port_in_use": "端口 {port} 被占用,换下一个…",
"start.gateway_starting": "正在启动 U-Claw端口 {port}…",
"start.opening_screen": "正在打开启动页…",
"start.opening_config": "正在打开控制台…",
"start.running_title": "U-Claw 已启动",
"start.running_dashboard": "对话界面:{url}",
"start.running_config": "控制台: {url}",
"start.running_hint": "使用期间请不要关闭这个窗口。按 Ctrl+C 停止。",
"start.first_run_wait": "第一次从 U 盘启动需要 3090 秒解压组件。控制台已经打开,可以先选模型、填密钥。",
"start.stopped": "U-Claw 已停止。",
"start.exited_unexpectedly": "U-Claw 自己退出了(代码 {code})。",
"error.node_missing.what": "U 盘里少了一个 U-Claw 需要的文件。",
"error.node_missing.why": "通常是拷贝到 U 盘时没有复制完整。",
"error.node_missing.action": "把 U-Claw 文件夹重新拷贝一遍,或运行 setup.sh 重建。",
"error.unsupported_arch.what": "这台 Mac 的处理器不受支持({arch})。",
"error.unsupported_arch.why": "U-Claw 只提供 Apple Silicon 和 Intel Mac 的版本。",
"error.unsupported_arch.action": "换一台电脑试试。",
"error.no_port.what": "U-Claw 找不到可用的端口来启动。",
"error.no_port.why": "{from}{to} 都被占用了 —— 可能已经有一个 U-Claw 在运行。",
"error.no_port.action": "关掉已经在运行的那个窗口,再启动一次。",
"error.deps_failed.what": "U-Claw 没能准备完成。",
"error.deps_failed.why": "下载没有完成,通常是网络中断了。",
"error.deps_failed.action": "检查一下 Wi-Fi然后重新启动 U-Claw。",
"error.press_enter": "按回车关闭这个窗口。",
"loading.title": "正在启动 U-Claw",
"loading.starting": "U-Claw 正在启动…",
"loading.first_run": "第一次从 U 盘启动需要展开内置组件,稍等片刻,就绪后会提示你下一步。",
"loading.waiting_gateway": "正在等待 U-Claw 启动…",
"loading.ready": "U-Claw 已就绪",
"loading.choose_next": "选择下一步 —— 直接开始聊天,或先去配置模型和渠道。",
"loading.open_dashboard": "🚀 开始聊天",
"loading.open_settings": "⚙️ 设置",
"loading.settings_hint": "填写或更换 API 密钥,连接聊天软件。",
"loading.no_model_hint": "还没配过模型?先打开设置填一个密钥。",
"loading.slow_drive_note": "慢 U 盘首次启动通常 3090 秒。如果迟迟没反应,可以先打开设置:",
"loading.elapsed": "已等待 {seconds} 秒",
"loading.taking_long": "启动比平时慢,可以先打开设置:",
"welcome.title": "U-Claw 使用指南",
"welcome.subtitle": "装在 U 盘里的 AI 助手 · 三步开始",
"welcome.quickstart": "快速开始",
"welcome.step1_title": "1. 启动 U-Claw",
"welcome.step1_desc": "双击你电脑对应的那个文件:",
"welcome.step2_title": "2. 填一个密钥",
"welcome.step2_desc": "第一次启动会自动打开设置页。你需要一个 API 密钥才能开始 —— Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡。",
"welcome.step2_link": "去领取免费的 Gemini 密钥 →",
"welcome.step3_title": "3. 开始对话",
"welcome.step3_desc": "密钥保存后,对话窗口会自己打开。就这样。",
"welcome.portable_note": "✅ 你的设置和聊天记录都存在盘上的 data/ 目录里跟着盘走Mac 和 Windows 通用。",
"welcome.models_title": "用哪个模型?",
"welcome.model_claude": "综合最强",
"welcome.model_gpt": "用的人最多",
"welcome.model_gemini": "有免费额度,注册最简单",
"welcome.model_local": "离线 · 数据不出本机",
"welcome.models_more": "更多选择在设置里,包括适合马来语、泰米尔语和 Singlish 的 SEA-LION。",
"welcome.trouble_title": "遇到问题?",
"welcome.trouble_lead": "💡 运行检查工具",
"welcome.trouble_desc": "如果 U-Claw 启动不了,双击:",
"welcome.open_settings": "打开设置",
"welcome.open_docs": "查看文档",
"nav.title": "U-Claw 启动导航",
"nav.tagline": "便携 AI 助手 — 插上就能用",
"nav.status_off": "U-Claw 尚未运行",
"nav.open_console": "打开它 →",
"nav.quickstart": "快速启动",
"nav.doubleclick": "双击",
"nav.step2": "第一次启动会自动打开设置页 —— 选一个模型,把密钥粘进去。",
"nav.mac_gatekeeper": "⚠️ 如果 macOS 提示「无法验证开发者」:右键点这个文件,选「打开」。",
"nav.config_shortcut": "📝 也可以双击 Config.html它会带你到设置页。",
"nav.win_smartscreen": "⚠️ 如果 Windows 弹安全警告:点「更多信息」,再点「仍要运行」。",
"nav.everything": "其他功能",
"nav.menu_opens": "打开完整菜单:",
"nav.g_settings": "设置",
"nav.g_settings_d": "模型、密钥和聊天软件",
"nav.g_checkup": "检查修复",
"nav.g_checkup_d": "找出并修复常见问题",
"nav.g_backup": "备份",
"nav.g_backup_d": "一键保存设置和记忆",
"nav.g_skills": "技能",
"nav.g_skills_d": "看看 U-Claw 能做什么",
"nav.g_system": "系统信息",
"nav.g_system_d": "版本、端口、运行状态",
"nav.g_cli": "命令行",
"nav.g_cli_d": "给进阶用户",
"nav.models_title": "可以用的模型",
"nav.model_sealion": "马来语、泰米尔语和 Singlish",
"nav.model_local": "本地模型",
"nav.model_more": "更多",
"nav.model_more_d": "其他模型在设置里",
"nav.more_title": "更多",
"nav.link_guide": "使用指南",
"nav.link_guide_d": "三步上手",
"nav.link_skills": "技能",
"nav.link_skills_d": "看看 U-Claw 能做什么",
"nav.link_contact": "联系我们",
"nav.link_site": "官网",
"nav.status_on": "U-Claw 正在运行(端口 {port}",
"nav.status_off_hint": "U-Claw 尚未运行 —— 请先双击启动文件",
"key.page_title": "U-Claw 设置",
"key.page_subtitle": "粘贴一个 API 密钥就可以了。",
"key.checking": "检测中…",
"key.step_key": "填密钥",
"key.step_done": "完成",
"key.heading": "粘贴你的 API 密钥",
"key.desc": "剩下的 U-Claw 自己搞定。密钥只保存在这个 U 盘上,不会上传到任何地方。",
"key.placeholder": "sk-…",
"key.show": "显示",
"key.hide": "隐藏",
"key.continue": "继续 →",
"key.checking_key": "正在检查这个密钥…",
"key.ok": "✓ {provider} · 连接成功,用时 {ms} 毫秒",
"key.custom_provider": "你的地址",
"key.no_key_title": "还没有密钥?",
"key.no_key_desc": "Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡,还有免费额度。",
"key.get_gemini": "🔑 领取免费的 Gemini 密钥",
"key.get_openrouter": "🔑 OpenRouter —— 一个密钥用多种模型",
"key.advanced": "高级 —— 手动填写接口地址",
"key.base_url": "接口地址",
"key.model_name": "模型名称",
"key.advanced_hint": "只有自建或未收录的服务才需要填。留空则由 U-Claw 自动判断。",
"key.saved": "已保存。",
"key.err_empty": "先粘贴一个密钥。",
"key.err_not_checked": "稍等,密钥还在检查中。",
"key.err_rejected": "这个密钥没被接受。检查一下是不是没复制全 —— 密钥很长,容易少一截。",
"key.err_quota": "这个密钥的额度用完了。去服务商那边充值,或者换一个密钥。",
"key.err_offline": "连不上网络。检查一下 Wi-Fi 再试。",
"key.err_timeout": "服务商没有及时响应。过一会儿再试。",
"key.err_model": "密钥是好的,但它指向的模型没找到。在「高级」里填写模型名称。",
"key.err_provider_down": "服务商那边现在有问题。稍后再试。",
"key.err_unrecognised": "U-Claw 认不出这个密钥。请在「高级」里填写接口地址和模型名称。",
"key.err_unknown": "没能检查这个密钥。再试一次,或者在「高级」里自己填接口地址。",
"key.err_save": "保存失败。确认 U 盘还插着,然后再试一次。",
"hub.title": "U-Claw 技能",
"done.title": "配置完成",
"done.subtitle": "模型和密钥已保存在这个 U 盘上。",
"done.open": "🚀 开始聊天",
"done.restart_note": "聊天软件的设置已保存,重启 U-Claw 后生效。",
"done.change_key": "换模型或密钥",
"done.view_config": "查看配置文件",
"ch.title": "📱 连接聊天软件(可选)",
"ch.desc": "点卡片展开填写,也可以直接跳过。",
"ch.whatsapp_d": "用 WhatsApp 和 U-Claw 对话",
"ch.whatsapp_warning": "⚠️ 这里用的是非官方协议,你的 WhatsApp 账号可能因此被封。不要接入你离不开的号码。",
"ch.whatsapp_number": "手机号",
"ch.telegram_d": "用 Telegram 和 U-Claw 对话",
"ch.telegram_get": "→ 从 @BotFather 获取",
"ch.slack_d": "在工作区里和 U-Claw 对话",
"ch.slack_get": "→ 在 api.slack.com 创建应用",
"ch.discord_d": "用 Discord 和 U-Claw 对话",
"ch.discord_get": "→ 在 Discord 开发者后台创建应用",
"ch.china_group": "主要在中国使用的聊天软件",
"ch.wechat_d": "扫码接入个人微信",
"ch.wechat_start": "点这张卡片开始扫码",
"ch.saved": "聊天软件设置已保存。",
"ch.err_save": "聊天软件设置没能保存。确认 U 盘还插着。",
"info.layout_title": "U 盘里有什么",
"info.layout_desc": "文件夹是怎么组织的",
"info.skills": "技能",
"info.skills_d": "把 SKILL.md 放进来就会自动加载",
"info.app": "运行时",
"info.app_d": "Node.js 和 OpenClaw 核心",
"info.data": "你的数据",
"info.data_d": "密钥和聊天记录,只留在这个盘上。",
"info.feedback_title": "遇到问题?",
"info.feedback_desc": "U-Claw 是开源项目。出问题欢迎去 GitHub 提 Issue —— 我们不会自动收集或上传你机器上的任何东西。",
"info.feedback_link": "→ 去 GitHub 提 Issue",
"wx.getting_qr": "正在获取二维码…",
"wx.scan": "请用微信扫描上方二维码",
"wx.qr_failed": "获取二维码失败",
"wx.connected": "✅ 微信已连接",
"wx.account": "账号",
"wx.restart_needed": "还差一步:重启 U-Claw关掉再打开微信才能用。",
"wx.connected_toast": "微信已连接,重启 U-Claw 后生效。",
"wx.qr_refreshed": "二维码已刷新,请重新扫码",
"wx.confirm_on_phone": "已扫码,请在手机上确认…",
"wx.qr_expired": "二维码已过期。",
"wx.qr_retry": "重新获取",
"wx.failed": "连接失败",
"info.read_config_failed": "读取配置文件失败:",
"status.running": "U-Claw 正在运行,端口 {port}",
"status.open_dashboard": "打开它 →",
"status.not_running": "U-Claw 尚未运行 —— 请先运行 Windows-Start.bat 或 Mac-Start.command",
"key.which_model": "用哪个模型?",
"start.opening_language": "正在询问使用哪种语言…",
"persona.title": "你主要用它做什么?",
"persona.subtitle": "这会帮你把合适的工具装好。以后可以改,也可以多选。",
"persona.continue": "继续 →",
"persona.skip": "跳过",
"persona.saving": "正在准备…",
"persona.err_offline": "U-Claw 还没启动。先启动它,再打开这个页面。",
"persona.err_save": "没能保存。确认 U 盘还插着,然后再试一次。",
"persona.developer": "写代码",
"persona.admin": "写文档、发邮件",
"persona.sales": "跟客户打交道",
"persona.marketing": "做内容、发社交媒体",
"persona.finance": "算数、做表格",
"persona.boss": "管公司",
"persona.general": "先随便看看",
"start.opening_persona": "正在询问你主要用它做什么…",
"tier.label": "想看到多少?",
"tier.simple": "只要最基本的",
"tier.standard": "多一点控制",
"tier.expert": "全部",
"tier.changed_simple": "已切换到简洁模式 —— 高级选项已隐藏。",
"tier.changed_standard": "已切换到标准模式 —— 现在可以换模型、接更多聊天软件了。",
"tier.changed_expert": "已切换到完整模式 —— 所有东西都可见,包括配置文件。",
"start.checking": "出了点问题,正在检查…",
"start.repaired": "已修复,重试一次。",
"start.diagnostics_written": "已把情况记录到:{path}",
"start.diagnostics_hint": "里面的密钥已被去掉。需要帮忙的话,把它发到 help@u-claw.org。",
"skill.excel-helper.name": "表格",
"skill.excel-helper.desc": "公式、透视表、图表、清洗乱数据",
"skill.word-writer.name": "文档",
"skill.word-writer.desc": "报告、方案、简历 —— 起草和排版",
"skill.ppt-designer.name": "幻灯片",
"skill.ppt-designer.desc": "结构、版式和演讲要点",
"skill.pdf-toolkit.name": "PDF 工具",
"skill.pdf-toolkit.desc": "合并、拆分、提取文字 —— 全在本机",
"skill.image-compress.name": "图片",
"skill.image-compress.desc": "压缩、改尺寸、转格式 —— 不上传",
"skill.qrcode-maker.name": "二维码",
"skill.qrcode-maker.desc": "网址、文本、WiFi离线生成",
"skill.web-to-markdown.name": "保存网页",
"skill.web-to-markdown.desc": "把文章转成干净的 Markdown",
"skill.linkedin-post.name": "LinkedIn 帖子",
"skill.linkedin-post.desc": "钩子和结构,不要那股 LinkedIn 腔",
"skill.x-poster.name": "X 帖子",
"skill.x-poster.desc": "能独立成立的短帖和 thread",
"skill.tiktok-script.name": "TikTok 脚本",
"skill.tiktok-script.desc": "扛得住第一秒的钩子",
"skill.youtube-script.name": "YouTube 脚本",
"skill.youtube-script.desc": "标题、前 30 秒、章节",
"skill.medium-writer.name": "长文",
"skill.medium-writer.desc": "长文结构,以及怎么删水分",
"skill.email-campaign.name": "邮件营销",
"skill.email-campaign.desc": "标题、一个明确的 ask、符合 PDPA",
"skill.web-search.name": "网页搜索",
"skill.web-search.desc": "找当前信息并真的读来源",
"skill.sg-weather.name": "新加坡天气",
"skill.sg-weather.desc": "按区预报和空气质量,来自 NEA",
"skill.sea-translate.name": "东南亚翻译",
"skill.sea-translate.desc": "英 / 中 / 马来 / 泰米尔 —— 以及语域",
"skill.claude-helper.name": "问得更好",
"skill.claude-helper.desc": "怎么问,以及什么时候该核对答案",
"skill.sg-transport.name": "新加坡交通",
"skill.sg-transport.desc": "巴士到站、地铁故障、停车位",
"skill.meeting-notes.name": "会议纪要",
"skill.meeting-notes.desc": "决定和负责人,写完就能发",
"cat.office": "办公",
"cat.data": "数据",
"cat.writing": "写作",
"cat.files": "文件",
"cat.research": "资料",
"cat.social": "社交媒体",
"cat.video": "视频",
"cat.language": "语言",
"cat.local": "新加坡",
"cat.ai": "用好 AI",
"hub.lede": "这些已经在盘上了。直接说你想做什么就行,不用记技能名。",
"hub.all": "全部",
"hub.none": "这个分类下暂时没有。",
"hub.add_note": "技能放在盘上的 skills/ 目录里。丢一个 SKILL.md 进去就多一个。"
}
};

View File

@@ -0,0 +1,281 @@
/* GENERATED FILE — do not edit.
* Source: skills/manifest.json
* Regenerate: node lib/i18n/build-messages.mjs
*/
window.UCLAW_SKILLS = {
"personas": [
{
"id": "developer",
"tier": "expert",
"emoji": "💻",
"channel": null
},
{
"id": "admin",
"tier": "simple",
"emoji": "📝",
"channel": null
},
{
"id": "sales",
"tier": "simple",
"emoji": "💬",
"channel": "whatsapp"
},
{
"id": "marketing",
"tier": "standard",
"emoji": "🎨",
"channel": null
},
{
"id": "finance",
"tier": "standard",
"emoji": "📊",
"channel": null
},
{
"id": "boss",
"tier": "simple",
"emoji": "👔",
"channel": null
},
{
"id": "general",
"tier": "simple",
"emoji": "🤷",
"channel": null
}
],
"skills": [
{
"id": "excel-helper",
"categories": [
"office",
"data"
],
"personas": [
"admin",
"finance",
"boss",
"general"
],
"emoji": "📊"
},
{
"id": "word-writer",
"categories": [
"office",
"writing"
],
"personas": [
"admin",
"sales",
"boss",
"general"
],
"emoji": "📝"
},
{
"id": "ppt-designer",
"categories": [
"office",
"writing"
],
"personas": [
"admin",
"sales",
"marketing",
"boss"
],
"emoji": "📽️"
},
{
"id": "pdf-toolkit",
"categories": [
"files"
],
"personas": [
"admin",
"finance",
"general"
],
"emoji": "📄"
},
{
"id": "image-compress",
"categories": [
"files"
],
"personas": [
"marketing",
"general"
],
"emoji": "🖼️"
},
{
"id": "qrcode-maker",
"categories": [
"files"
],
"personas": [
"marketing",
"sales",
"general"
],
"emoji": "🔳"
},
{
"id": "web-to-markdown",
"categories": [
"research"
],
"personas": [
"developer",
"marketing",
"general"
],
"emoji": "🔗"
},
{
"id": "linkedin-post",
"categories": [
"social"
],
"personas": [
"marketing",
"sales",
"boss"
],
"emoji": "💼"
},
{
"id": "x-poster",
"categories": [
"social"
],
"personas": [
"marketing"
],
"emoji": "🐦"
},
{
"id": "tiktok-script",
"categories": [
"social",
"video"
],
"personas": [
"marketing"
],
"emoji": "🎬"
},
{
"id": "youtube-script",
"categories": [
"social",
"video"
],
"personas": [
"marketing"
],
"emoji": "📺"
},
{
"id": "medium-writer",
"categories": [
"writing"
],
"personas": [
"marketing",
"developer"
],
"emoji": "✍️"
},
{
"id": "email-campaign",
"categories": [
"writing",
"social"
],
"personas": [
"marketing",
"sales"
],
"emoji": "✉️"
},
{
"id": "web-search",
"categories": [
"research"
],
"personas": [
"developer",
"marketing",
"general",
"finance"
],
"emoji": "🔍"
},
{
"id": "sg-weather",
"categories": [
"local"
],
"personas": [
"general",
"sales"
],
"emoji": "🌦️"
},
{
"id": "sea-translate",
"categories": [
"language"
],
"personas": [
"sales",
"admin",
"marketing",
"general",
"boss"
],
"emoji": "🌏"
},
{
"id": "claude-helper",
"categories": [
"ai"
],
"personas": [
"developer",
"finance"
],
"emoji": "🤖"
},
{
"id": "sg-transport",
"categories": [
"local"
],
"personas": [
"general",
"sales"
],
"emoji": "🚇"
},
{
"id": "meeting-notes",
"categories": [
"writing"
],
"personas": [
"sales",
"admin",
"boss",
"finance"
],
"emoji": "🗒️"
}
]
};

View File

@@ -83,7 +83,7 @@ async function chatProbe(t) {
const res = await requestText(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(t.apiKey ? { Authorization: `Bearer ${t.apiKey}` } : {}) },
body: JSON.stringify({ model: t.modelId, messages: [{ role: 'user', content: '请回复四个字:连接成功' }], max_tokens: 64, stream: false }),
body: JSON.stringify({ model: t.modelId, messages: [{ role: 'user', content: 'Reply with exactly: connection ok' }], max_tokens: 64, stream: false }),
timeoutMs: CHAT_TIMEOUT_MS,
});
const ms = Date.now() - started;
@@ -126,76 +126,77 @@ function requestText(rawUrl, { method = 'GET', headers = {}, body, timeoutMs })
async function main() {
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
line('========================================');
line(' U-Claw 内网体检 / Intranet Check');
line(' U-Claw — corporate network check');
line(` Node ${process.version}`);
line('========================================');
line('');
// 1) 代理环境
line('【1】代理环境检查');
line('[1] System proxy');
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy'];
const setProxies = proxyKeys.filter((k) => process.env[k]);
if (setProxies.length) {
for (const k of setProxies) line(` ${k} = ${process.env[k]}`);
line(' → 检测到系统代理。发往内网模型的请求可能被它劫持(最常见的内网故障源)。');
line(' A system proxy is set. Requests to an internal model endpoint may be routed through it — this is the single most common cause of a self-hosted model not working.');
} else {
line(' (未检测到系统代理)');
line(' No system proxy set.');
}
line('');
if (!configPath) { line('未提供配置路径,结束。'); return; }
if (!configPath) { line('No config path given — nothing to check.'); return; }
let config;
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
catch (e) { line(`读取配置失败:${e?.message || e}`); return; }
catch (e) { line(`Could not read the config file: ${e?.message || e}`); return; }
const providers = collectProviders(config?.models);
if (!providers.length) { line('配置里没有任何模型地址baseUrl。先配好模型再来跑。'); return; }
if (!providers.length) { line('No model endpoint is configured yet. Set one up in Settings first, then run this again.'); return; }
// 2) NO_PROXY 建议
const hosts = Array.from(new Set(providers.map((p) => hostOf(p.baseUrl)).filter(Boolean)));
const noProxy = applyNoProxy(hosts);
line('【2】建议的 NO_PROXY让这些地址直连、绕开代理');
line('[2] Suggested NO_PROXY — hosts that should bypass the proxy');
line(` ${noProxy.join(',')}`);
line(' (新版 Windows-Start.bat 已会自动设置,无需手动操作)');
line(' U-Claw already sets this for you at startup. Shown here in case you need it elsewhere.');
line('');
// 3) 直连可达
line('【3】直连测试绕过代理看能否摸到模型服务');
line('[3] Direct connection — can this machine reach the model at all, proxy bypassed');
for (const p of providers) {
const r = await reachProbe(p.baseUrl, p.apiKey);
if (r.ok) line(` [${p.name}] ${p.baseUrl} → ✓ 可达 HTTP ${r.status} (${r.ms}ms)`);
else line(` [${p.name}] ${p.baseUrl} → ✗ 失败 ${r.error} (${r.ms}ms)`);
if (r.ok) line(` [${p.name}] ${p.baseUrl} -> reachable, HTTP ${r.status} (${r.ms}ms)`);
else line(` [${p.name}] ${p.baseUrl} -> FAILED ${r.error} (${r.ms}ms)`);
}
line('');
// 4) 端到端实测
line('【4】实测真发一条对话给模型');
line('[4] Real request — actually asking the model something');
const t = pickTarget(config, providers);
if (!t) { line(' 找不到可测的模型 id跳过。'); }
if (!t) { line(' No model ID to test with — skipped.'); }
else {
line(` 模型:${t.provName} / ${t.modelId}`);
line(' 发送中,请稍候...');
line(` Model: ${t.provName} / ${t.modelId}`);
line(' Sending…');
const c = await chatProbe(t);
if (c.ok) {
line('');
line(` ✓✓ 跑通了!模型回复 (${c.ms}ms)${c.reply.slice(0, 120) || '(空回复但请求成功)'}`);
line(` It works. The model replied in ${c.ms}ms: ${c.reply.slice(0, 120) || '(empty reply, but the request succeeded)'}`);
} else if (c.status) {
line('');
line(` ✗ 服务端 HTTP ${c.status} (${c.ms}ms)${c.body}`);
line(` The server answered with HTTP ${c.status} in ${c.ms}ms: ${c.body}`);
} else {
line('');
line(` ✗ 直连失败:${c.error} (${c.ms}ms)`);
line(` Could not connect: ${c.error} (${c.ms}ms)`);
}
}
line('');
// 结论
line('========================================');
line(' 怎么看结果:');
line(' · 第4步「跑通了」 → 一切正常,以后双击 Windows-Start.bat 即可。');
line(' · 第3步可达但程序里用不了 → 是系统代理在劫持新版启动脚本已自动绕开NO_PROXY');
line(' · 第3步「直连失败/超时」 → 地址错 / 内网不通 / 防火墙 / 模型服务没起,找机房管理员。');
line(' · 出现 401/403 → 网络是通的,只是 API Key 不对。');
line(' What the results mean:');
line(' Step 4 worked Everything is fine. Start U-Claw normally.');
line(' Step 3 reachable but A proxy is intercepting the request. U-Claw already')
line(' the app cannot use it bypasses it at startup via NO_PROXY.');
line(' Step 3 failed Wrong address, blocked network, firewall, or the service is down. Ask whoever runs the server.');
line(' 401 or 403 The network is fine — the key is wrong.');
line('========================================');
}

101
portable/lib/language.html Normal file
View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>U-Claw</title>
<!--
First-run language choice.
Deliberately not a dropdown: this is the first thing anyone sees, and a select
needs you to already read the interface language to find your own. Two large
buttons, each written in its own language, need no instructions.
The choice is written to the drive (data/.openclaw/openclaw.json → uclaw.locale)
and never asked again. It is stored on the drive rather than in the browser so
the language travels with the drive: someone who set it up in Chinese and plugs
it into a colleague's English Windows still gets Chinese.
-->
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 3rem;
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: #16181c; color: #f2f4f7;
}
.mark { font-size: 5rem; line-height: 1; }
.choices { display: flex; gap: 1.5rem; flex-wrap: wrap; justify-content: center; padding: 0 1.5rem; }
button {
min-width: 13rem; padding: 1.75rem 2.5rem;
font-size: 1.5rem; font-weight: 600; font-family: inherit;
color: #f2f4f7; background: #23262c;
border: 2px solid #33373f; border-radius: 16px;
cursor: pointer; transition: border-color .15s, transform .15s;
}
button:hover, button:focus-visible { border-color: #ff6b35; transform: translateY(-2px); outline: none; }
button small { display: block; margin-top: .4rem; font-size: .85rem; font-weight: 400; color: #9aa0a8; }
.saving { color: #9aa0a8; font-size: .95rem; min-height: 1.4em; }
</style>
</head>
<body>
<div class="mark">🦞</div>
<div class="choices">
<button id="en" autofocus>English<small>Continue in English</small></button>
<button id="zh">中文<small>用中文继续</small></button>
</div>
<p class="saving" id="saving"></p>
<script>
// The config server owns writes to openclaw.json, so go through it rather than
// trying to write the file from the page. It binds the first free port in this
// range, so probe instead of assuming 18788.
const PORT_FROM = 18788;
const PORT_TO = 18798;
const NEXT = { en: 'Setting up…', 'zh-CN': '正在设置…' };
const FAILED = {
en: 'Could not save that. U-Claw may not be running yet — start it and try again.',
'zh-CN': '没能保存。U-Claw 可能还没启动 —— 先启动它再试一次。',
};
async function findServer() {
for (let port = PORT_FROM; port <= PORT_TO; port++) {
try {
await fetch(`http://127.0.0.1:${port}/api/config`, { cache: 'no-store' });
return port;
} catch { /* nothing listening here */ }
}
return null;
}
async function choose(locale) {
document.getElementById('saving').textContent = NEXT[locale];
const port = await findServer();
if (port === null) {
document.getElementById('saving').textContent = FAILED[locale];
return;
}
try {
const base = `http://127.0.0.1:${port}`;
const config = await (await fetch(`${base}/api/config`)).json();
config.uclaw = Object.assign({}, config.uclaw, { locale });
await fetch(`${base}/api/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
// Straight on to the next question. ?lang= makes it render in the chosen
// language immediately, without waiting for the launcher to rewrite locale.js.
location.replace(`persona.html?lang=${encodeURIComponent(locale)}`);
} catch {
document.getElementById('saving').textContent = FAILED[locale];
}
}
document.getElementById('en').addEventListener('click', () => choose('en'));
document.getElementById('zh').addEventListener('click', () => choose('zh-CN'));
</script>
</body>
</html>

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>U-Claw 正在启动…</title>
<title data-i18n="loading.title">Starting U-Claw</title>
<!--
启动首屏(移植自 v2 u-clawx 4.0 的 splash 思路)。
双击启动脚本后立刻打开本页,给用户即时反馈,消除"黑窗假死"体感。
@@ -100,40 +100,41 @@ body.ready .ready-wrap { display: block; }
.btn-primary:hover { background: #ff6a30; }
.foot { margin-top: 24px; font-size: .82em; opacity: .65; line-height: 1.6; }
</style>
<script src="../data/.openclaw/locale.js"></script>
<script src="i18n/messages.js"></script>
<script src="i18n/i18n.js"></script>
</head>
<body>
<div class="box">
<!-- 启动中 -->
<!-- Starting -->
<div class="loading-wrap">
<div class="logo">🦞</div>
<h1>U-Claw 正在启动</h1>
<div class="tip">首次从 U 盘启动需要展开内置组件,<br>稍等片刻,就绪后会提示你下一步。</div>
<h1 data-i18n="loading.starting">Starting U-Claw</h1>
<div class="tip" data-i18n="loading.first_run">First run from a USB drive unpacks the bundled components. Give it a moment — you will be told when it is ready.</div>
<div class="spinner"></div>
<div class="bar"><i></i></div>
<div class="status" id="status">正在等待 OpenClaw 网关</div>
<div class="status" id="status" data-i18n="loading.waiting_gateway">Waiting for U-Claw to come up</div>
<div class="elapsed" id="elapsed"></div>
<div class="note">
慢 U 盘首次启动通常 3090 秒。<br>
如果迟迟没反应,可先打开
<a class="manual" id="manual" href="#">配置中心</a>
<span data-i18n="loading.slow_drive_note">A slow drive usually takes 3090 seconds. If nothing happens for a long time, open Settings:</span>
<a class="manual" id="manual" href="#" data-i18n="loading.open_settings">⚙️ Settings</a>
</div>
</div>
<!-- 就绪后:让用户自己选 -->
<!-- Ready: let the user choose -->
<div class="ready-wrap">
<div class="logo">🦞</div>
<h1>U-Claw 已就绪</h1>
<div class="tip">选择下一步 — 直接开始聊天,或先去配置模型和渠道。</div>
<h1 data-i18n="loading.ready">U-Claw is ready</h1>
<div class="tip" data-i18n="loading.choose_next">Pick what to do next — start chatting, or set up your model and channels first.</div>
<div class="actions">
<a class="btn btn-primary" id="goDash">🚀 进入 Dashboard
<span class="sub">开始和 AI 聊天</span>
<a class="btn btn-primary" id="goDash"><span data-i18n="loading.open_dashboard">🚀 Start chatting</span>
</a>
<a class="btn" id="goConfig">⚙️ 配置 / 改模型
<span class="sub">填写或更换模型 Key、充值、连接微信等渠道</span>
<a class="btn" id="goConfig"><span data-i18n="loading.open_settings">⚙️ Settings</span>
<span class="sub" data-i18n="loading.settings_hint">Add or change your API key, and connect chat apps.</span>
</a>
</div>
<div class="foot">没配过模型或余额不足?先点「配置 / 改模型」处理 Key 或充值。</div>
<div class="foot" data-i18n="loading.no_model_hint">No model set up yet? Open Settings first to add a key.</div>
</div>
</div>
@@ -156,7 +157,7 @@ body.ready .ready-wrap { display: block; }
function fmt(ms) {
var s = Math.floor(ms / 1000);
return '已等待 ' + s + '';
return window.UClawI18n ? window.UClawI18n.t('loading.elapsed', { seconds: s }) : s + 's';
}
function tickElapsed() {
@@ -187,8 +188,10 @@ body.ready .ready-wrap { display: block; }
// 兜底5 分钟还没起来,提示去控制台
setTimeout(function () {
if (!ready) {
var label = window.UClawI18n ? window.UClawI18n.t('loading.open_settings') : 'Settings';
var slow = window.UClawI18n ? window.UClawI18n.t('loading.taking_long') : 'This is taking longer than usual. You can open Settings while you wait:';
document.getElementById('status').innerHTML =
'启动较慢,可先打开 <a class="manual" href="' + configCenter + '">配置中心</a> 检查';
slow + ' <a class="manual" href="' + configCenter + '">' + label + '</a>';
}
}, 300000);
})();

View File

@@ -26,7 +26,7 @@ detect_install_mode() {
do_kill_gateway() {
echo ""
echo -e " ${CYAN}${BOLD}━━━ 杀死残留进程 ━━━${NC}"
echo -e " ${CYAN}${BOLD}--- Stop leftover processes ---${NC}"
echo ""
local FOUND=0
@@ -42,7 +42,7 @@ do_kill_gateway() {
if [ -n "$PIDS" ]; then
for PID in $PIDS; do
local CMD=$(ps -p "$PID" -o comm= 2>/dev/null || echo "unknown")
echo -e " ${YELLOW}端口 $PORT: PID $PID ($CMD)${NC}"
echo -e " ${YELLOW}Port $PORT: PID $PID ($CMD)${NC}"
FOUND=1
done
fi
@@ -52,20 +52,20 @@ do_kill_gateway() {
local OC_PIDS=$(pgrep -f "openclaw.mjs gateway" 2>/dev/null)
if [ -n "$OC_PIDS" ]; then
for PID in $OC_PIDS; do
echo -e " ${YELLOW}OpenClaw 网关进程: PID $PID${NC}"
echo -e " ${YELLOW}OpenClaw gateway: PID $PID${NC}"
FOUND=1
done
fi
if [ "$FOUND" = "0" ]; then
echo -e " ${GREEN}没有发现残留进程${NC}"
echo -e " ${GREEN}Nothing left running${NC}"
return
fi
echo ""
read -p " 确认杀死以上进程? (y/N): " CONFIRM
read -p " Stop these? (y/N): " CONFIRM
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
return
fi
@@ -82,24 +82,24 @@ do_kill_gateway() {
pgrep -f "openclaw.mjs gateway" 2>/dev/null | xargs kill 2>/dev/null
sleep 1
echo -e " ${GREEN}进程已清理${NC}"
echo -e " ${GREEN}Processes stopped${NC}"
}
# ── [10] View/export/clean logs ──────────────────────────────
do_logs() {
echo ""
echo -e " ${CYAN}${BOLD}━━━ 日志管理 ━━━${NC}"
echo -e " ${CYAN}${BOLD}--- Logs ---${NC}"
echo ""
mkdir -p "$LOG_DIR"
local LOG_FILE="$LOG_DIR/gateway.log"
echo -e " ${GREEN}[a]${NC} 查看最近日志(最后 50 行)"
echo -e " ${GREEN}[b]${NC} 导出日志到桌面"
echo -e " ${GREEN}[c]${NC} 清理 7 天前的旧日志"
echo -e " ${GREEN}[a]${NC} Show the last 50 lines"
echo -e " ${GREEN}[b]${NC} Copy the log to the desktop"
echo -e " ${GREEN}[c]${NC} Delete logs older than 7 days"
echo ""
read -p " 选择 (a-c): " -n 1 LOG_CHOICE
read -p " Choose (a-c): " -n 1 LOG_CHOICE
echo ""
echo ""
@@ -110,13 +110,13 @@ do_logs() {
echo ""
tail -50 "$LOG_FILE"
else
echo -e " ${YELLOW}日志文件不存在: $LOG_FILE${NC}"
echo " 启动网关后会自动生成日志。"
echo -e " ${YELLOW}No log file yet: $LOG_FILE${NC}"
echo " One is created the first time U-Claw runs."
fi
;;
b)
if [ ! -f "$LOG_FILE" ]; then
echo -e " ${YELLOW}没有日志可导出${NC}"
echo -e " ${YELLOW}No log to copy${NC}"
return
fi
local TS=$(date +%Y%m%d_%H%M%S)
@@ -124,26 +124,26 @@ do_logs() {
[ ! -d "$EXPORT_DIR" ] && EXPORT_DIR="$HOME"
local EXPORT_FILE="$EXPORT_DIR/uclaw-logs-$TS.txt"
cp "$LOG_FILE" "$EXPORT_FILE"
echo -e " ${GREEN}日志已导出: $EXPORT_FILE${NC}"
echo " 大小: $(du -sh "$EXPORT_FILE" | cut -f1)"
echo -e " ${GREEN}Copied to: $EXPORT_FILE${NC}"
echo " Size: $(du -sh "$EXPORT_FILE" | cut -f1)"
;;
c)
local COUNT=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
if [ "$COUNT" = "0" ]; then
echo -e " ${GREEN}没有需要清理的旧日志${NC}"
echo -e " ${GREEN}No old logs to delete${NC}"
return
fi
echo -e " 找到 ${YELLOW}$COUNT${NC} 个超过 7 天的日志文件"
read -p " 确认清理? (y/N): " CONFIRM
echo -e " Found ${YELLOW}$COUNT${NC} log files older than 7 days"
read -p " Delete them? (y/N): " CONFIRM
if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
echo -e " ${GREEN}旧日志已清理${NC}"
echo -e " ${GREEN}Old logs deleted${NC}"
else
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
fi
;;
*)
echo -e " ${RED}无效选择${NC}"
echo -e " ${RED}Not a valid choice${NC}"
;;
esac
}
@@ -152,46 +152,46 @@ do_logs() {
do_factory_reset() {
echo ""
echo -e " ${RED}${BOLD}━━━ 恢复出厂设置 ━━━${NC}"
echo -e " ${RED}${BOLD}--- Reset to factory settings ---${NC}"
echo ""
echo -e " ${RED}警告: 此操作将删除所有配置和记忆数据!${NC}"
echo -e " ${RED}This deletes every setting and everything the AI remembers.${NC}"
echo ""
echo " 将执行以下操作:"
echo " 1. 自动备份当前配置和记忆"
echo " 2. 删除配置文件 (openclaw.json)"
echo " 3. 删除记忆数据 (memory/)"
echo " 4. 恢复默认配置"
echo " It will:"
echo " 1. Back up your settings and memory first"
echo " 2. Delete openclaw.json"
echo " 3. Delete memory/"
echo " 4. Put the defaults back"
echo ""
echo -e " ${YELLOW}请输入 RESET 确认(区分大小写):${NC}"
echo -e " ${YELLOW}Type RESET to confirm (case-sensitive):${NC}"
read -p " > " CONFIRM
if [ "$CONFIRM" != "RESET" ]; then
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
return
fi
# Step 1: Auto backup
echo ""
echo -e " ${CYAN}[1/4] 自动备份...${NC}"
echo -e " ${CYAN}[1/4] Backing up…${NC}"
local TS=$(date +%Y%m%d_%H%M%S)
local BK="$BACKUP_DIR/pre-reset_$TS"
mkdir -p "$BK"
[ -f "$CONFIG_PATH" ] && cp "$CONFIG_PATH" "$BK/" 2>/dev/null
[ -d "$DATA_DIR/memory" ] && cp -R "$DATA_DIR/memory" "$BK/" 2>/dev/null
echo -e " ${GREEN}备份已保存: $BK${NC}"
echo -e " ${GREEN}Backup saved to $BK${NC}"
# Step 2: Delete config
echo -e " ${CYAN}[2/4] 删除配置...${NC}"
echo -e " ${CYAN}[2/4] Removing settings…${NC}"
rm -f "$CONFIG_PATH" 2>/dev/null
rm -f "$DATA_DIR/config.json" 2>/dev/null
# Step 3: Delete memory
echo -e " ${CYAN}[3/4] 清理记忆数据...${NC}"
echo -e " ${CYAN}[3/4] Removing memory…${NC}"
rm -rf "$DATA_DIR/memory" 2>/dev/null
mkdir -p "$DATA_DIR/memory"
# Step 4: Restore default config
echo -e " ${CYAN}[4/4] 恢复默认配置...${NC}"
echo -e " ${CYAN}[4/4] Restoring defaults…${NC}"
mkdir -p "$STATE_DIR"
if [ -f "$DEFAULT_CONFIG" ]; then
cp "$DEFAULT_CONFIG" "$CONFIG_PATH"
@@ -207,75 +207,75 @@ CFGEOF
fi
echo ""
echo -e " ${GREEN}出厂设置已恢复!${NC}"
echo -e " 备份位置: $BK"
echo -e " 请重新运行配置向导 [1] 设置模型。"
echo -e " ${GREEN}Reset complete.${NC}"
echo -e " Your old settings are at $BK"
echo -e " Run the setup wizard again to add a model."
}
# ── [12] Uninstall ───────────────────────────────────────────
do_uninstall() {
echo ""
echo -e " ${RED}${BOLD}━━━ 卸载 U-Claw ━━━${NC}"
echo -e " ${RED}${BOLD}--- Uninstall U-Claw ---${NC}"
echo ""
local MODE=$(detect_install_mode)
case $MODE in
installed)
echo " 检测到: 已安装版本 (~/.uclaw/)"
echo " Found: installed copy at ~/.uclaw/"
echo ""
echo " 将删除以下目录:"
echo " These folders will be deleted:"
echo -e " ${YELLOW}$HOME/.uclaw/${NC}"
if [ -d "$HOME/.uclaw" ]; then
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
fi
echo ""
echo -e " ${RED}请输入 UNINSTALL 确认:${NC}"
echo -e " ${RED}Type UNINSTALL to confirm:${NC}"
read -p " > " CONFIRM
if [ "$CONFIRM" != "UNINSTALL" ]; then
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
return
fi
rm -rf "$HOME/.uclaw"
echo -e " ${GREEN}卸载完成!${NC}"
echo -e " ${GREEN}Uninstalled.${NC}"
;;
usb)
echo " 检测到: U 盘模式"
echo " Found: running from a USB drive"
echo ""
echo " 便携版不需要卸载 —— 只需:"
echo " 1. 关闭所有 U-Claw 窗口"
echo " 2. 安全弹出 U 盘"
echo " 3. 如需清理电脑上的数据,删除 ~/.uclaw/ (如果存在)"
echo " There is nothing to uninstall. Just:"
echo " 1. Close every U-Claw window"
echo " 2. Eject the drive"
echo " 3. If you want nothing left on this computer, delete ~/.uclaw/"
echo ""
if [ -d "$HOME/.uclaw" ]; then
echo -e " ${YELLOW}发现本机数据: ~/.uclaw/${NC}"
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
read -p " 是否删除本机数据? (y/N): " DEL
echo -e " ${YELLOW}Found data on this computer: ~/.uclaw/${NC}"
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
read -p " Delete it? (y/N): " DEL
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
rm -rf "$HOME/.uclaw"
echo -e " ${GREEN}本机数据已清理${NC}"
echo -e " ${GREEN}Deleted${NC}"
fi
fi
;;
portable)
echo " 检测到: 便携版本"
echo " Found: portable copy"
echo ""
echo " 便携版不需要卸载 —— 直接删除文件夹即可:"
echo " There is nothing to uninstall — just delete the folder:"
echo -e " ${YELLOW}$UCLAW_DIR${NC}"
echo ""
if [ -d "$HOME/.uclaw" ]; then
echo -e " ${YELLOW}发现本机数据: ~/.uclaw/${NC}"
echo " 大小: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
read -p " 是否删除本机数据? (y/N): " DEL
echo -e " ${YELLOW}Found data on this computer: ~/.uclaw/${NC}"
echo " Size: $(du -sh "$HOME/.uclaw" 2>/dev/null | cut -f1)"
read -p " Delete it? (y/N): " DEL
if [ "$DEL" = "y" ] || [ "$DEL" = "Y" ]; then
rm -rf "$HOME/.uclaw"
echo -e " ${GREEN}本机数据已清理${NC}"
echo -e " ${GREEN}Deleted${NC}"
fi
fi
echo ""
echo " Electron 桌面版卸载:"
echo " macOS: U-Claw.app 从「应用程序」拖到废纸篓"
echo " Electron desktop app:"
echo " macOS: drag U-Claw.app from Applications to the Trash"
;;
esac
}
@@ -284,15 +284,15 @@ do_uninstall() {
do_update() {
echo ""
echo -e " ${CYAN}${BOLD}━━━ 检查更新 ━━━${NC}"
echo -e " ${CYAN}${BOLD}--- Check for updates ---${NC}"
echo ""
if [ ! -f "$NODE_BIN" ]; then
echo -e " ${RED}Node.js 未找到,无法检查更新${NC}"
echo -e " ${RED}Node.js is missing, so updates cannot be checked${NC}"
return
fi
echo -e " ${DIM}正在查询最新版本...${NC}"
echo -e " ${DIM}Looking up the latest version…${NC}"
# Get current version
local CUR_VER=""
@@ -301,58 +301,58 @@ do_update() {
fi
if [ -z "$CUR_VER" ]; then
echo -e " ${RED}无法读取当前版本OpenClaw 可能未安装)${NC}"
echo -e " ${RED}Cannot read the installed version — OpenClaw may not be installed${NC}"
return
fi
# Get latest version from npmmirror
local LATEST_VER=$(curl -s --connect-timeout 10 "https://registry.npmmirror.com/openclaw/latest" 2>/dev/null | "$NODE_BIN" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})" 2>/dev/null)
# Get latest version from the npm registry
local LATEST_VER=$(curl -s --connect-timeout 10 "https://registry.npmjs.org/openclaw/latest" 2>/dev/null | "$NODE_BIN" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).version)}catch(e){console.log('error')}})" 2>/dev/null)
if [ -z "$LATEST_VER" ] || [ "$LATEST_VER" = "error" ]; then
echo -e " ${YELLOW}无法获取最新版本(网络问题?)${NC}"
echo " 当前版本: $CUR_VER"
echo -e " ${YELLOW}Could not reach the registry. Check your network.${NC}"
echo " Installed: $CUR_VER"
return
fi
echo " 当前版本: $CUR_VER"
echo " 最新版本: $LATEST_VER"
echo " Installed: $CUR_VER"
echo " Latest: $LATEST_VER"
echo ""
if [ "$CUR_VER" = "$LATEST_VER" ]; then
echo -e " ${GREEN}已是最新版本!${NC}"
echo -e " ${GREEN}Already up to date.${NC}"
return
fi
echo -e " ${YELLOW}有新版本可用!${NC}"
read -p " 是否立即升级? (y/N): " DO_UPDATE
echo -e " ${YELLOW}A newer version is available.${NC}"
read -p " Update now? (y/N): " DO_UPDATE
if [ "$DO_UPDATE" != "y" ] && [ "$DO_UPDATE" != "Y" ]; then
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
return
fi
echo ""
echo -e " ${CYAN}正在升级...${NC}"
echo -e " ${CYAN}Updating…${NC}"
cd "$CORE_DIR"
"$NPM_BIN" install openclaw@latest --registry=https://registry.npmmirror.com 2>&1
"$NPM_BIN" install openclaw@latest --registry=https://registry.npmjs.org 2>&1
local NEW_VER=$("$NODE_BIN" -e "console.log(require('./node_modules/openclaw/package.json').version)" 2>/dev/null)
echo ""
echo -e " ${GREEN}升级完成!${NC} $CUR_VER$NEW_VER"
echo -e " ${GREEN}Updated.${NC} $CUR_VER$NEW_VER"
}
# ── [14] Disk cleanup (P1) ───────────────────────────────────
do_cleanup() {
echo ""
echo -e " ${CYAN}${BOLD}━━━ 清理空间 ━━━${NC}"
echo -e " ${CYAN}${BOLD}--- Free up space ---${NC}"
echo ""
# Show directory sizes
echo -e " ${WHITE}目录占用:${NC}"
echo -e " ${WHITE}Space used:${NC}"
[ -d "$CORE_DIR/node_modules" ] && echo " node_modules: $(du -sh "$CORE_DIR/node_modules" 2>/dev/null | cut -f1)"
[ -d "$DATA_DIR/memory" ] && echo " memory: $(du -sh "$DATA_DIR/memory" 2>/dev/null | cut -f1)"
[ -d "$BACKUP_DIR" ] && echo " backups: $(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)"
[ -d "$LOG_DIR" ] && echo " logs: $(du -sh "$LOG_DIR" 2>/dev/null | cut -f1)"
echo " 总计: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
echo " Total: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
echo ""
local CLEANED=0
@@ -362,17 +362,17 @@ do_cleanup() {
local BK_COUNT=$(ls -d "$BACKUP_DIR"/*/ 2>/dev/null | wc -l | tr -d ' ')
if [ "$BK_COUNT" -gt 3 ]; then
local OLD_COUNT=$((BK_COUNT - 3))
echo -e " ${YELLOW}发现 $BK_COUNT 个备份,保留最新 3 个,删除 $OLD_COUNT 个旧备份${NC}"
read -p " 确认? (y/N): " DEL_BK
echo -e " ${YELLOW}Found $BK_COUNT backups. Keeping the newest 3, deleting $OLD_COUNT.${NC}"
read -p " Go ahead? (y/N): " DEL_BK
if [ "$DEL_BK" = "y" ] || [ "$DEL_BK" = "Y" ]; then
ls -dt "$BACKUP_DIR"/*/ 2>/dev/null | tail -n "$OLD_COUNT" | while read -r DIR; do
rm -rf "$DIR"
done
echo -e " ${GREEN}旧备份已清理${NC}"
echo -e " ${GREEN}Old backups deleted${NC}"
CLEANED=1
fi
else
echo " 备份: ${BK_COUNT} 个(无需清理)"
echo " Backups: ${BK_COUNT} — nothing to clear"
fi
fi
@@ -380,33 +380,33 @@ do_cleanup() {
if [ -d "$LOG_DIR" ]; then
local OLD_LOGS=$(find "$LOG_DIR" -name "*.log" -mtime +7 2>/dev/null | wc -l | tr -d ' ')
if [ "$OLD_LOGS" -gt 0 ]; then
echo -e " ${YELLOW}发现 $OLD_LOGS 个超过 7 天的日志${NC}"
read -p " 确认清理? (y/N): " DEL_LOG
echo -e " ${YELLOW}Found $OLD_LOGS log files older than 7 days${NC}"
read -p " Delete them? (y/N): " DEL_LOG
if [ "$DEL_LOG" = "y" ] || [ "$DEL_LOG" = "Y" ]; then
find "$LOG_DIR" -name "*.log" -mtime +7 -delete 2>/dev/null
echo -e " ${GREEN}旧日志已清理${NC}"
echo -e " ${GREEN}Old logs deleted${NC}"
CLEANED=1
fi
else
echo " 日志: 无需清理"
echo " Logs: nothing to clear"
fi
fi
# Clean npm cache
echo ""
read -p " 是否清理 npm 缓存? (y/N): " DEL_CACHE
read -p " Clear the npm cache? (y/N): " DEL_CACHE
if [ "$DEL_CACHE" = "y" ] || [ "$DEL_CACHE" = "Y" ]; then
"$NPM_BIN" cache clean --force 2>/dev/null
echo -e " ${GREEN}npm 缓存已清理${NC}"
echo -e " ${GREEN}npm cache cleared${NC}"
CLEANED=1
fi
if [ "$CLEANED" = "0" ]; then
echo ""
echo -e " ${GREEN}没有需要清理的内容${NC}"
echo -e " ${GREEN}Nothing to clear${NC}"
else
echo ""
echo " 清理后总计: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
echo " Now using: $(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1)"
fi
}
@@ -414,56 +414,56 @@ do_cleanup() {
do_plugins() {
echo ""
echo -e " ${CYAN}${BOLD}━━━ 插件管理 ━━━${NC}"
echo -e " ${CYAN}${BOLD}--- Plugins ---${NC}"
echo ""
echo -e " ${GREEN}[a]${NC} 列出已安装插件"
echo -e " ${GREEN}[b]${NC} 安装插件"
echo -e " ${GREEN}[c]${NC} 卸载插件"
echo -e " ${GREEN}[a]${NC} List installed plugins"
echo -e " ${GREEN}[b]${NC} Install a plugin"
echo -e " ${GREEN}[c]${NC} Remove a plugin"
echo ""
read -p " 选择 (a-c): " -n 1 PLG_CHOICE
read -p " Choose (a-c): " -n 1 PLG_CHOICE
echo ""
echo ""
case $PLG_CHOICE in
a)
echo -e " ${WHITE}已安装插件:${NC}"
echo -e " ${WHITE}Installed plugins:${NC}"
echo ""
run_oc plugins list 2>&1 || echo -e " ${YELLOW}无法获取插件列表${NC}"
run_oc plugins list 2>&1 || echo -e " ${YELLOW}Could not list plugins${NC}"
;;
b)
echo " 常用插件:"
echo " @icesword760/openclaw-wechat — 微信"
echo " @nicepkg/openclaw-plugin-qq — QQ社区版"
echo " Commonly used:"
echo " @icesword760/openclaw-wechat WeChat"
echo " @nicepkg/openclaw-plugin-qq QQ (community build)"
echo ""
read -p " 输入插件名称(留空取消): " PLG_NAME
read -p " Plugin name (blank to cancel): " PLG_NAME
if [ -z "$PLG_NAME" ]; then
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
return
fi
echo ""
echo -e " ${CYAN}正在安装 $PLG_NAME ...${NC}"
echo -e " ${CYAN}Installing $PLG_NAME${NC}"
run_oc plugins install "$PLG_NAME" 2>&1
echo ""
echo -e " ${GREEN}安装完成${NC}"
echo -e " ${GREEN}Installed${NC}"
;;
c)
echo -e " ${WHITE}已安装插件:${NC}"
echo -e " ${WHITE}Installed plugins:${NC}"
echo ""
run_oc plugins list 2>&1 || true
echo ""
read -p " 输入要卸载的插件名称(留空取消): " PLG_NAME
read -p " Plugin to remove (blank to cancel): " PLG_NAME
if [ -z "$PLG_NAME" ]; then
echo -e " ${YELLOW}已取消${NC}"
echo -e " ${YELLOW}Cancelled${NC}"
return
fi
echo ""
echo -e " ${CYAN}正在卸载 $PLG_NAME ...${NC}"
echo -e " ${CYAN}Removing $PLG_NAME${NC}"
run_oc plugins remove "$PLG_NAME" 2>&1
echo ""
echo -e " ${GREEN}卸载完成${NC}"
echo -e " ${GREEN}Removed${NC}"
;;
*)
echo -e " ${RED}无效选择${NC}"
echo -e " ${RED}Not a valid choice${NC}"
;;
esac
}

View File

@@ -0,0 +1,279 @@
{
"start.banner": "U-Claw · Portable AI Agent",
"start.node_version": "Node.js {version}",
"start.quarantine_removing": "Clearing the macOS security flag…",
"start.cache_local": "Cache on local disk: {path}",
"start.config_migrating": "Bringing your old settings across…",
"start.config_migrated": "Settings moved over.",
"start.config_creating": "First run — setting things up…",
"start.config_created": "Ready.",
"start.deps_preparing": "Getting things ready — about 20 seconds.",
"start.deps_slow_drive": "This drive is slow, so it may take a few minutes. You can leave it running.",
"start.deps_done": "Ready.",
"start.no_proxy": "Connecting directly to your model host: {hosts}",
"start.wechat_installing": "Installing the WeChat plugin…",
"start.wechat_installed": "WeChat plugin installed.",
"start.config_center_starting": "Starting the Control Panel…",
"start.config_center_port": "Control Panel is on port {port}.",
"start.port_in_use": "Port {port} is busy, trying the next one…",
"start.gateway_starting": "Starting U-Claw on port {port}…",
"start.opening_screen": "Opening the start screen…",
"start.opening_config": "Opening the Control Panel…",
"start.running_title": "U-Claw is running",
"start.running_dashboard": "Dashboard: {url}",
"start.running_config": "Control Panel: {url}",
"start.running_hint": "Keep this window open while you use U-Claw. Press Ctrl+C to stop.",
"start.first_run_wait": "First run from a USB drive takes 3090 seconds while components unpack. The Control Panel is already open so you can pick a model and paste a key.",
"start.stopped": "U-Claw stopped.",
"start.exited_unexpectedly": "U-Claw closed on its own (code {code}).",
"error.node_missing.what": "A file U-Claw needs is missing from the drive.",
"error.node_missing.why": "This usually means the copy onto the USB drive did not finish.",
"error.node_missing.action": "Copy the U-Claw folder onto the drive again, or run setup.sh to rebuild it.",
"error.unsupported_arch.what": "This Mac's processor is not supported ({arch}).",
"error.unsupported_arch.why": "U-Claw ships builds for Apple Silicon and Intel Macs only.",
"error.unsupported_arch.action": "Try U-Claw on a different computer.",
"error.no_port.what": "U-Claw could not find a free port to start on.",
"error.no_port.why": "Ports {from}{to} are all in use — U-Claw may already be running.",
"error.no_port.action": "Close any window already running U-Claw and start it again.",
"error.deps_failed.what": "U-Claw could not finish getting ready.",
"error.deps_failed.why": "The download did not complete — usually a dropped network connection.",
"error.deps_failed.action": "Check your Wi-Fi and start U-Claw again.",
"error.press_enter": "Press Enter to close this window.",
"loading.title": "Starting U-Claw",
"loading.starting": "Starting U-Claw…",
"loading.first_run": "First run from a USB drive unpacks the bundled components. Give it a moment — you will be told when it is ready.",
"loading.waiting_gateway": "Waiting for U-Claw to come up…",
"loading.ready": "U-Claw is ready",
"loading.choose_next": "Pick what to do next — start chatting, or set up your model and channels first.",
"loading.open_dashboard": "🚀 Start chatting",
"loading.open_settings": "⚙️ Settings",
"loading.settings_hint": "Add or change your API key, and connect chat apps.",
"loading.no_model_hint": "No model set up yet? Open Settings first to add a key.",
"loading.slow_drive_note": "A slow drive usually takes 3090 seconds. If nothing happens for a long time, open Settings:",
"loading.elapsed": "Waited {seconds}s",
"loading.taking_long": "This is taking longer than usual. You can open Settings while you wait:",
"welcome.title": "U-Claw — Getting Started",
"welcome.subtitle": "Your AI assistant on a USB drive · three steps",
"welcome.quickstart": "Quick start",
"welcome.step1_title": "1. Start U-Claw",
"welcome.step1_desc": "Double-click the file for your computer:",
"welcome.step2_title": "2. Add a key",
"welcome.step2_desc": "Settings opens on its own the first time. You need one API key to get going — Google Gemini is the easiest to obtain: sign in with a Google account, no card required.",
"welcome.step2_link": "Get a free Gemini key →",
"welcome.step3_title": "3. Start talking",
"welcome.step3_desc": "Once the key is saved, the chat window opens by itself. That is it.",
"welcome.portable_note": "✅ Your settings and history stay in the data/ folder on the drive, so they travel with you between Mac and Windows.",
"welcome.models_title": "Which model?",
"welcome.model_claude": "Best all-rounder",
"welcome.model_gpt": "Most widely used",
"welcome.model_gemini": "Free tier, easiest signup",
"welcome.model_local": "Offline · nothing leaves this machine",
"welcome.models_more": "More options, including SEA-LION for Malay, Tamil and Singlish, are in Settings.",
"welcome.trouble_title": "Something not working?",
"welcome.trouble_lead": "💡 Run the check-up tool",
"welcome.trouble_desc": "If U-Claw will not start, double-click:",
"welcome.open_settings": "Open Settings",
"welcome.open_docs": "Documentation",
"nav.title": "U-Claw — Start here",
"nav.tagline": "Portable AI agent — plug in and go",
"nav.status_off": "U-Claw is not running",
"nav.open_console": "Open it →",
"nav.quickstart": "Quick start",
"nav.doubleclick": "Double-click",
"nav.step2": "Settings opens by itself the first time — pick a model and paste your key.",
"nav.mac_gatekeeper": "⚠️ If macOS says the developer cannot be verified: right-click the file, then choose Open.",
"nav.config_shortcut": "📝 You can also double-click Config.html — it takes you to the settings page.",
"nav.win_smartscreen": "⚠️ If Windows shows a security warning: click More info, then Run anyway.",
"nav.everything": "Everything else",
"nav.menu_opens": "for the full menu:",
"nav.g_settings": "Settings",
"nav.g_settings_d": "Model, key and chat apps",
"nav.g_checkup": "Check-up",
"nav.g_checkup_d": "Finds and fixes common problems",
"nav.g_backup": "Backup",
"nav.g_backup_d": "Save settings and memory in one click",
"nav.g_skills": "Skills",
"nav.g_skills_d": "Browse what U-Claw can do",
"nav.g_system": "System info",
"nav.g_system_d": "Version, port, running state",
"nav.g_cli": "Command line",
"nav.g_cli_d": "For advanced use",
"nav.models_title": "Models you can use",
"nav.model_sealion": "Malay, Tamil and Singlish",
"nav.model_local": "Local model",
"nav.model_more": "More",
"nav.model_more_d": "Others are listed in Settings",
"nav.more_title": "More",
"nav.link_guide": "Getting started",
"nav.link_guide_d": "The three-step walkthrough",
"nav.link_skills": "Skills",
"nav.link_skills_d": "Browse what U-Claw can do",
"nav.link_contact": "Contact",
"nav.link_site": "Website",
"nav.status_on": "U-Claw is running on port {port}",
"nav.status_off_hint": "U-Claw is not running — double-click the start file first",
"key.page_title": "U-Claw Settings",
"key.page_subtitle": "Paste an API key and you are done.",
"key.checking": "Checking…",
"key.step_key": "Your key",
"key.step_done": "Done",
"key.heading": "Paste your API key",
"key.desc": "U-Claw works out the rest by itself. Your key is saved on this drive only — it is never uploaded anywhere.",
"key.placeholder": "sk-…",
"key.show": "Show",
"key.hide": "Hide",
"key.continue": "Continue →",
"key.checking_key": "Checking this key…",
"key.ok": "✓ {provider} · connected in {ms}ms",
"key.custom_provider": "Your endpoint",
"key.no_key_title": "Do not have a key yet?",
"key.no_key_desc": "Google Gemini is the quickest to get: sign in with a Google account, no card needed, and there is a free tier.",
"key.get_gemini": "🔑 Get a free Gemini key",
"key.get_openrouter": "🔑 OpenRouter — one key for many models",
"key.advanced": "Advanced — set the endpoint manually",
"key.base_url": "API endpoint",
"key.model_name": "Model name",
"key.advanced_hint": "Only needed for a self-hosted or unlisted provider. Leave blank to let U-Claw decide.",
"key.saved": "Saved.",
"key.err_empty": "Paste a key to continue.",
"key.err_not_checked": "Hold on — still checking that key.",
"key.err_rejected": "That key was not accepted. Check you copied all of it — keys are long and easy to cut short.",
"key.err_quota": "This key has run out of credit. Top it up with your provider, or paste a different key.",
"key.err_offline": "Cannot reach the internet. Check your Wi-Fi and try again.",
"key.err_timeout": "The provider did not answer in time. Try again in a moment.",
"key.err_model": "That key works, but the model it points to was not found. Set the model name under Advanced.",
"key.err_provider_down": "The provider is having trouble right now. Try again shortly.",
"key.err_unrecognised": "This key is not one U-Claw recognises. Fill in the endpoint and model under Advanced.",
"key.err_unknown": "Could not check that key. Try again, or use Advanced to set the endpoint yourself.",
"key.err_save": "Could not save. Make sure the drive is still plugged in, then try again.",
"hub.title": "U-Claw Skills",
"done.title": "You are set up",
"done.subtitle": "Your model and key are saved on this drive.",
"done.open": "🚀 Start chatting",
"done.restart_note": "Chat app settings are saved now and take effect after U-Claw restarts.",
"done.change_key": "Change model or key",
"done.view_config": "View config file",
"ch.title": "📱 Connect a chat app (optional)",
"ch.desc": "Click a card to fill it in, or skip this entirely.",
"ch.whatsapp_d": "Talk to U-Claw from WhatsApp",
"ch.whatsapp_warning": "⚠️ This uses an unofficial protocol. Your WhatsApp account can be banned for it. Do not connect a number you rely on.",
"ch.whatsapp_number": "Phone number",
"ch.telegram_d": "Talk to U-Claw from Telegram",
"ch.telegram_get": "→ Get one from @BotFather",
"ch.slack_d": "Talk to U-Claw from your workspace",
"ch.slack_get": "→ Create an app at api.slack.com",
"ch.discord_d": "Talk to U-Claw from Discord",
"ch.discord_get": "→ Create an app in the Discord developer portal",
"ch.china_group": "Chat apps used mainly in China",
"ch.wechat_d": "Scan a QR code to link a personal WeChat account",
"ch.wechat_start": "Click this card to show the QR code",
"ch.saved": "Chat app settings saved.",
"ch.err_save": "Could not save the chat app settings. Check the drive is still plugged in.",
"info.layout_title": "What is on the drive",
"info.layout_desc": "How the folders are organised",
"info.skills": "Skills",
"info.skills_d": "Drop a SKILL.md in here and it is picked up automatically",
"info.app": "Runtime",
"info.app_d": "Node.js and the OpenClaw core",
"info.data": "Your data",
"info.data_d": "Your key and chat history. Stays on this drive.",
"info.feedback_title": "Found a problem?",
"info.feedback_desc": "U-Claw is open source. Report anything that breaks on GitHub — nothing is collected or uploaded from your machine automatically.",
"info.feedback_link": "→ Open an issue on GitHub",
"wx.getting_qr": "Fetching the QR code…",
"wx.scan": "Scan the code above with WeChat",
"wx.qr_failed": "Could not fetch the QR code",
"wx.connected": "✅ WeChat connected",
"wx.account": "Account",
"wx.restart_needed": "One more thing: restart U-Claw (close it and open it again) before WeChat will work.",
"wx.connected_toast": "WeChat connected. It works after U-Claw restarts.",
"wx.qr_refreshed": "The code refreshed — scan it again",
"wx.confirm_on_phone": "Scanned. Confirm on your phone…",
"wx.qr_expired": "That code expired.",
"wx.qr_retry": "Get a new one",
"wx.failed": "Could not connect",
"info.read_config_failed": "Could not read the config file:",
"status.running": "U-Claw is running on port {port}",
"status.open_dashboard": "Open it →",
"status.not_running": "U-Claw is not running — start it with Windows-Start.bat or Mac-Start.command",
"key.which_model": "Which model?",
"start.opening_language": "Asking which language to use…",
"persona.title": "What will you mostly use this for?",
"persona.subtitle": "This sets up the right tools for you. You can change it later, and picking more than one is fine.",
"persona.continue": "Continue →",
"persona.skip": "Skip this",
"persona.saving": "Setting things up…",
"persona.err_offline": "U-Claw is not running yet. Start it, then open this page again.",
"persona.err_save": "Could not save that. Check the drive is still plugged in, then try again.",
"persona.developer": "Writing code",
"persona.admin": "Documents and email",
"persona.sales": "Talking to customers",
"persona.marketing": "Content and social",
"persona.finance": "Numbers and spreadsheets",
"persona.boss": "Running a business",
"persona.general": "Just having a look",
"start.opening_persona": "Asking what you will use it for…",
"tier.label": "How much do you want to see?",
"tier.simple": "Just the essentials",
"tier.standard": "A bit more control",
"tier.expert": "Everything",
"tier.changed_simple": "Switched to the simple view — advanced options are hidden.",
"tier.changed_standard": "Switched to the standard view — you can now change models and connect more chat apps.",
"tier.changed_expert": "Switched to the full view — everything is visible, including the config file.",
"start.checking": "Something went wrong — checking…",
"start.repaired": "Fixed. Trying again.",
"start.diagnostics_written": "A report of what happened was saved to: {path}",
"start.diagnostics_hint": "API keys have been removed from it. Send it to help@u-claw.org if you want a hand.",
"skill.excel-helper.name": "Spreadsheets",
"skill.excel-helper.desc": "Formulas, pivot tables, charts, cleaning messy data",
"skill.word-writer.name": "Documents",
"skill.word-writer.desc": "Reports, proposals, CVs — drafting and formatting",
"skill.ppt-designer.name": "Slide decks",
"skill.ppt-designer.desc": "Structure, layout and speaker notes",
"skill.pdf-toolkit.name": "PDF tools",
"skill.pdf-toolkit.desc": "Merge, split, extract text — all on this machine",
"skill.image-compress.name": "Images",
"skill.image-compress.desc": "Shrink, resize and convert — nothing is uploaded",
"skill.qrcode-maker.name": "QR codes",
"skill.qrcode-maker.desc": "Links, text and WiFi details, generated offline",
"skill.web-to-markdown.name": "Save a web page",
"skill.web-to-markdown.desc": "Turn any article into clean Markdown",
"skill.linkedin-post.name": "LinkedIn posts",
"skill.linkedin-post.desc": "Hooks and structure, without the LinkedIn voice",
"skill.x-poster.name": "Posts for X",
"skill.x-poster.desc": "Short posts and threads that stand on their own",
"skill.tiktok-script.name": "TikTok scripts",
"skill.tiktok-script.desc": "Hooks that survive the first second",
"skill.youtube-script.name": "YouTube scripts",
"skill.youtube-script.desc": "Titles, the first 30 seconds, and chapters",
"skill.medium-writer.name": "Articles",
"skill.medium-writer.desc": "Long-form structure, and cutting the padding",
"skill.email-campaign.name": "Email campaigns",
"skill.email-campaign.desc": "Subject lines, one clear ask, PDPA-safe",
"skill.web-search.name": "Web search",
"skill.web-search.desc": "Find current information and read the sources",
"skill.sg-weather.name": "Singapore weather",
"skill.sg-weather.desc": "Forecast by area and air quality, from NEA",
"skill.sea-translate.name": "SEA translation",
"skill.sea-translate.desc": "English, Chinese, Malay, Tamil — and register",
"skill.claude-helper.name": "Better answers",
"skill.claude-helper.desc": "How to ask, and when to check the answer",
"skill.sg-transport.name": "Singapore transport",
"skill.sg-transport.desc": "Bus arrivals, MRT alerts, carpark lots",
"skill.meeting-notes.name": "Meeting notes",
"skill.meeting-notes.desc": "Decisions and owners, ready to send",
"cat.office": "Office",
"cat.data": "Data",
"cat.writing": "Writing",
"cat.files": "Files",
"cat.research": "Research",
"cat.social": "Social",
"cat.video": "Video",
"cat.language": "Language",
"cat.local": "Singapore",
"cat.ai": "Using AI",
"hub.lede": "These are on this drive already. Just ask for what you want — you do not have to name the skill.",
"hub.all": "All",
"hub.none": "Nothing in this category.",
"hub.add_note": "Skills live in the skills/ folder on this drive. Dropping a SKILL.md in there adds one."
}

View File

@@ -0,0 +1,279 @@
{
"start.banner": "U-Claw · 便携 AI 助手",
"start.node_version": "Node.js {version}",
"start.quarantine_removing": "正在解除 macOS 安全限制…",
"start.cache_local": "缓存已放在本机硬盘:{path}",
"start.config_migrating": "正在迁移你原来的设置…",
"start.config_migrated": "设置已迁移。",
"start.config_creating": "第一次使用,正在准备…",
"start.config_created": "准备好了。",
"start.deps_preparing": "正在准备,约 20 秒。",
"start.deps_slow_drive": "这个 U 盘比较慢,可能要几分钟,放着就行。",
"start.deps_done": "准备好了。",
"start.no_proxy": "将直连你的模型地址:{hosts}",
"start.wechat_installing": "正在安装微信插件…",
"start.wechat_installed": "微信插件已安装。",
"start.config_center_starting": "正在启动控制台…",
"start.config_center_port": "控制台端口:{port}",
"start.port_in_use": "端口 {port} 被占用,换下一个…",
"start.gateway_starting": "正在启动 U-Claw端口 {port}…",
"start.opening_screen": "正在打开启动页…",
"start.opening_config": "正在打开控制台…",
"start.running_title": "U-Claw 已启动",
"start.running_dashboard": "对话界面:{url}",
"start.running_config": "控制台: {url}",
"start.running_hint": "使用期间请不要关闭这个窗口。按 Ctrl+C 停止。",
"start.first_run_wait": "第一次从 U 盘启动需要 3090 秒解压组件。控制台已经打开,可以先选模型、填密钥。",
"start.stopped": "U-Claw 已停止。",
"start.exited_unexpectedly": "U-Claw 自己退出了(代码 {code})。",
"error.node_missing.what": "U 盘里少了一个 U-Claw 需要的文件。",
"error.node_missing.why": "通常是拷贝到 U 盘时没有复制完整。",
"error.node_missing.action": "把 U-Claw 文件夹重新拷贝一遍,或运行 setup.sh 重建。",
"error.unsupported_arch.what": "这台 Mac 的处理器不受支持({arch})。",
"error.unsupported_arch.why": "U-Claw 只提供 Apple Silicon 和 Intel Mac 的版本。",
"error.unsupported_arch.action": "换一台电脑试试。",
"error.no_port.what": "U-Claw 找不到可用的端口来启动。",
"error.no_port.why": "{from}{to} 都被占用了 —— 可能已经有一个 U-Claw 在运行。",
"error.no_port.action": "关掉已经在运行的那个窗口,再启动一次。",
"error.deps_failed.what": "U-Claw 没能准备完成。",
"error.deps_failed.why": "下载没有完成,通常是网络中断了。",
"error.deps_failed.action": "检查一下 Wi-Fi然后重新启动 U-Claw。",
"error.press_enter": "按回车关闭这个窗口。",
"loading.title": "正在启动 U-Claw",
"loading.starting": "U-Claw 正在启动…",
"loading.first_run": "第一次从 U 盘启动需要展开内置组件,稍等片刻,就绪后会提示你下一步。",
"loading.waiting_gateway": "正在等待 U-Claw 启动…",
"loading.ready": "U-Claw 已就绪",
"loading.choose_next": "选择下一步 —— 直接开始聊天,或先去配置模型和渠道。",
"loading.open_dashboard": "🚀 开始聊天",
"loading.open_settings": "⚙️ 设置",
"loading.settings_hint": "填写或更换 API 密钥,连接聊天软件。",
"loading.no_model_hint": "还没配过模型?先打开设置填一个密钥。",
"loading.slow_drive_note": "慢 U 盘首次启动通常 3090 秒。如果迟迟没反应,可以先打开设置:",
"loading.elapsed": "已等待 {seconds} 秒",
"loading.taking_long": "启动比平时慢,可以先打开设置:",
"welcome.title": "U-Claw 使用指南",
"welcome.subtitle": "装在 U 盘里的 AI 助手 · 三步开始",
"welcome.quickstart": "快速开始",
"welcome.step1_title": "1. 启动 U-Claw",
"welcome.step1_desc": "双击你电脑对应的那个文件:",
"welcome.step2_title": "2. 填一个密钥",
"welcome.step2_desc": "第一次启动会自动打开设置页。你需要一个 API 密钥才能开始 —— Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡。",
"welcome.step2_link": "去领取免费的 Gemini 密钥 →",
"welcome.step3_title": "3. 开始对话",
"welcome.step3_desc": "密钥保存后,对话窗口会自己打开。就这样。",
"welcome.portable_note": "✅ 你的设置和聊天记录都存在盘上的 data/ 目录里跟着盘走Mac 和 Windows 通用。",
"welcome.models_title": "用哪个模型?",
"welcome.model_claude": "综合最强",
"welcome.model_gpt": "用的人最多",
"welcome.model_gemini": "有免费额度,注册最简单",
"welcome.model_local": "离线 · 数据不出本机",
"welcome.models_more": "更多选择在设置里,包括适合马来语、泰米尔语和 Singlish 的 SEA-LION。",
"welcome.trouble_title": "遇到问题?",
"welcome.trouble_lead": "💡 运行检查工具",
"welcome.trouble_desc": "如果 U-Claw 启动不了,双击:",
"welcome.open_settings": "打开设置",
"welcome.open_docs": "查看文档",
"nav.title": "U-Claw 启动导航",
"nav.tagline": "便携 AI 助手 — 插上就能用",
"nav.status_off": "U-Claw 尚未运行",
"nav.open_console": "打开它 →",
"nav.quickstart": "快速启动",
"nav.doubleclick": "双击",
"nav.step2": "第一次启动会自动打开设置页 —— 选一个模型,把密钥粘进去。",
"nav.mac_gatekeeper": "⚠️ 如果 macOS 提示「无法验证开发者」:右键点这个文件,选「打开」。",
"nav.config_shortcut": "📝 也可以双击 Config.html它会带你到设置页。",
"nav.win_smartscreen": "⚠️ 如果 Windows 弹安全警告:点「更多信息」,再点「仍要运行」。",
"nav.everything": "其他功能",
"nav.menu_opens": "打开完整菜单:",
"nav.g_settings": "设置",
"nav.g_settings_d": "模型、密钥和聊天软件",
"nav.g_checkup": "检查修复",
"nav.g_checkup_d": "找出并修复常见问题",
"nav.g_backup": "备份",
"nav.g_backup_d": "一键保存设置和记忆",
"nav.g_skills": "技能",
"nav.g_skills_d": "看看 U-Claw 能做什么",
"nav.g_system": "系统信息",
"nav.g_system_d": "版本、端口、运行状态",
"nav.g_cli": "命令行",
"nav.g_cli_d": "给进阶用户",
"nav.models_title": "可以用的模型",
"nav.model_sealion": "马来语、泰米尔语和 Singlish",
"nav.model_local": "本地模型",
"nav.model_more": "更多",
"nav.model_more_d": "其他模型在设置里",
"nav.more_title": "更多",
"nav.link_guide": "使用指南",
"nav.link_guide_d": "三步上手",
"nav.link_skills": "技能",
"nav.link_skills_d": "看看 U-Claw 能做什么",
"nav.link_contact": "联系我们",
"nav.link_site": "官网",
"nav.status_on": "U-Claw 正在运行(端口 {port}",
"nav.status_off_hint": "U-Claw 尚未运行 —— 请先双击启动文件",
"key.page_title": "U-Claw 设置",
"key.page_subtitle": "粘贴一个 API 密钥就可以了。",
"key.checking": "检测中…",
"key.step_key": "填密钥",
"key.step_done": "完成",
"key.heading": "粘贴你的 API 密钥",
"key.desc": "剩下的 U-Claw 自己搞定。密钥只保存在这个 U 盘上,不会上传到任何地方。",
"key.placeholder": "sk-…",
"key.show": "显示",
"key.hide": "隐藏",
"key.continue": "继续 →",
"key.checking_key": "正在检查这个密钥…",
"key.ok": "✓ {provider} · 连接成功,用时 {ms} 毫秒",
"key.custom_provider": "你的地址",
"key.no_key_title": "还没有密钥?",
"key.no_key_desc": "Google Gemini 最容易拿到:用 Google 账号登录即可,不用绑卡,还有免费额度。",
"key.get_gemini": "🔑 领取免费的 Gemini 密钥",
"key.get_openrouter": "🔑 OpenRouter —— 一个密钥用多种模型",
"key.advanced": "高级 —— 手动填写接口地址",
"key.base_url": "接口地址",
"key.model_name": "模型名称",
"key.advanced_hint": "只有自建或未收录的服务才需要填。留空则由 U-Claw 自动判断。",
"key.saved": "已保存。",
"key.err_empty": "先粘贴一个密钥。",
"key.err_not_checked": "稍等,密钥还在检查中。",
"key.err_rejected": "这个密钥没被接受。检查一下是不是没复制全 —— 密钥很长,容易少一截。",
"key.err_quota": "这个密钥的额度用完了。去服务商那边充值,或者换一个密钥。",
"key.err_offline": "连不上网络。检查一下 Wi-Fi 再试。",
"key.err_timeout": "服务商没有及时响应。过一会儿再试。",
"key.err_model": "密钥是好的,但它指向的模型没找到。在「高级」里填写模型名称。",
"key.err_provider_down": "服务商那边现在有问题。稍后再试。",
"key.err_unrecognised": "U-Claw 认不出这个密钥。请在「高级」里填写接口地址和模型名称。",
"key.err_unknown": "没能检查这个密钥。再试一次,或者在「高级」里自己填接口地址。",
"key.err_save": "保存失败。确认 U 盘还插着,然后再试一次。",
"hub.title": "U-Claw 技能",
"done.title": "配置完成",
"done.subtitle": "模型和密钥已保存在这个 U 盘上。",
"done.open": "🚀 开始聊天",
"done.restart_note": "聊天软件的设置已保存,重启 U-Claw 后生效。",
"done.change_key": "换模型或密钥",
"done.view_config": "查看配置文件",
"ch.title": "📱 连接聊天软件(可选)",
"ch.desc": "点卡片展开填写,也可以直接跳过。",
"ch.whatsapp_d": "用 WhatsApp 和 U-Claw 对话",
"ch.whatsapp_warning": "⚠️ 这里用的是非官方协议,你的 WhatsApp 账号可能因此被封。不要接入你离不开的号码。",
"ch.whatsapp_number": "手机号",
"ch.telegram_d": "用 Telegram 和 U-Claw 对话",
"ch.telegram_get": "→ 从 @BotFather 获取",
"ch.slack_d": "在工作区里和 U-Claw 对话",
"ch.slack_get": "→ 在 api.slack.com 创建应用",
"ch.discord_d": "用 Discord 和 U-Claw 对话",
"ch.discord_get": "→ 在 Discord 开发者后台创建应用",
"ch.china_group": "主要在中国使用的聊天软件",
"ch.wechat_d": "扫码接入个人微信",
"ch.wechat_start": "点这张卡片开始扫码",
"ch.saved": "聊天软件设置已保存。",
"ch.err_save": "聊天软件设置没能保存。确认 U 盘还插着。",
"info.layout_title": "U 盘里有什么",
"info.layout_desc": "文件夹是怎么组织的",
"info.skills": "技能",
"info.skills_d": "把 SKILL.md 放进来就会自动加载",
"info.app": "运行时",
"info.app_d": "Node.js 和 OpenClaw 核心",
"info.data": "你的数据",
"info.data_d": "密钥和聊天记录,只留在这个盘上。",
"info.feedback_title": "遇到问题?",
"info.feedback_desc": "U-Claw 是开源项目。出问题欢迎去 GitHub 提 Issue —— 我们不会自动收集或上传你机器上的任何东西。",
"info.feedback_link": "→ 去 GitHub 提 Issue",
"wx.getting_qr": "正在获取二维码…",
"wx.scan": "请用微信扫描上方二维码",
"wx.qr_failed": "获取二维码失败",
"wx.connected": "✅ 微信已连接",
"wx.account": "账号",
"wx.restart_needed": "还差一步:重启 U-Claw关掉再打开微信才能用。",
"wx.connected_toast": "微信已连接,重启 U-Claw 后生效。",
"wx.qr_refreshed": "二维码已刷新,请重新扫码",
"wx.confirm_on_phone": "已扫码,请在手机上确认…",
"wx.qr_expired": "二维码已过期。",
"wx.qr_retry": "重新获取",
"wx.failed": "连接失败",
"info.read_config_failed": "读取配置文件失败:",
"status.running": "U-Claw 正在运行,端口 {port}",
"status.open_dashboard": "打开它 →",
"status.not_running": "U-Claw 尚未运行 —— 请先运行 Windows-Start.bat 或 Mac-Start.command",
"key.which_model": "用哪个模型?",
"start.opening_language": "正在询问使用哪种语言…",
"persona.title": "你主要用它做什么?",
"persona.subtitle": "这会帮你把合适的工具装好。以后可以改,也可以多选。",
"persona.continue": "继续 →",
"persona.skip": "跳过",
"persona.saving": "正在准备…",
"persona.err_offline": "U-Claw 还没启动。先启动它,再打开这个页面。",
"persona.err_save": "没能保存。确认 U 盘还插着,然后再试一次。",
"persona.developer": "写代码",
"persona.admin": "写文档、发邮件",
"persona.sales": "跟客户打交道",
"persona.marketing": "做内容、发社交媒体",
"persona.finance": "算数、做表格",
"persona.boss": "管公司",
"persona.general": "先随便看看",
"start.opening_persona": "正在询问你主要用它做什么…",
"tier.label": "想看到多少?",
"tier.simple": "只要最基本的",
"tier.standard": "多一点控制",
"tier.expert": "全部",
"tier.changed_simple": "已切换到简洁模式 —— 高级选项已隐藏。",
"tier.changed_standard": "已切换到标准模式 —— 现在可以换模型、接更多聊天软件了。",
"tier.changed_expert": "已切换到完整模式 —— 所有东西都可见,包括配置文件。",
"start.checking": "出了点问题,正在检查…",
"start.repaired": "已修复,重试一次。",
"start.diagnostics_written": "已把情况记录到:{path}",
"start.diagnostics_hint": "里面的密钥已被去掉。需要帮忙的话,把它发到 help@u-claw.org。",
"skill.excel-helper.name": "表格",
"skill.excel-helper.desc": "公式、透视表、图表、清洗乱数据",
"skill.word-writer.name": "文档",
"skill.word-writer.desc": "报告、方案、简历 —— 起草和排版",
"skill.ppt-designer.name": "幻灯片",
"skill.ppt-designer.desc": "结构、版式和演讲要点",
"skill.pdf-toolkit.name": "PDF 工具",
"skill.pdf-toolkit.desc": "合并、拆分、提取文字 —— 全在本机",
"skill.image-compress.name": "图片",
"skill.image-compress.desc": "压缩、改尺寸、转格式 —— 不上传",
"skill.qrcode-maker.name": "二维码",
"skill.qrcode-maker.desc": "网址、文本、WiFi离线生成",
"skill.web-to-markdown.name": "保存网页",
"skill.web-to-markdown.desc": "把文章转成干净的 Markdown",
"skill.linkedin-post.name": "LinkedIn 帖子",
"skill.linkedin-post.desc": "钩子和结构,不要那股 LinkedIn 腔",
"skill.x-poster.name": "X 帖子",
"skill.x-poster.desc": "能独立成立的短帖和 thread",
"skill.tiktok-script.name": "TikTok 脚本",
"skill.tiktok-script.desc": "扛得住第一秒的钩子",
"skill.youtube-script.name": "YouTube 脚本",
"skill.youtube-script.desc": "标题、前 30 秒、章节",
"skill.medium-writer.name": "长文",
"skill.medium-writer.desc": "长文结构,以及怎么删水分",
"skill.email-campaign.name": "邮件营销",
"skill.email-campaign.desc": "标题、一个明确的 ask、符合 PDPA",
"skill.web-search.name": "网页搜索",
"skill.web-search.desc": "找当前信息并真的读来源",
"skill.sg-weather.name": "新加坡天气",
"skill.sg-weather.desc": "按区预报和空气质量,来自 NEA",
"skill.sea-translate.name": "东南亚翻译",
"skill.sea-translate.desc": "英 / 中 / 马来 / 泰米尔 —— 以及语域",
"skill.claude-helper.name": "问得更好",
"skill.claude-helper.desc": "怎么问,以及什么时候该核对答案",
"skill.sg-transport.name": "新加坡交通",
"skill.sg-transport.desc": "巴士到站、地铁故障、停车位",
"skill.meeting-notes.name": "会议纪要",
"skill.meeting-notes.desc": "决定和负责人,写完就能发",
"cat.office": "办公",
"cat.data": "数据",
"cat.writing": "写作",
"cat.files": "文件",
"cat.research": "资料",
"cat.social": "社交媒体",
"cat.video": "视频",
"cat.language": "语言",
"cat.local": "新加坡",
"cat.ai": "用好 AI",
"hub.lede": "这些已经在盘上了。直接说你想做什么就行,不用记技能名。",
"hub.all": "全部",
"hub.none": "这个分类下暂时没有。",
"hub.add_note": "技能放在盘上的 skills/ 目录里。丢一个 SKILL.md 进去就多一个。"
}

153
portable/lib/persona.html Normal file
View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>U-Claw</title>
<script src="../data/.openclaw/locale.js"></script>
<script src="i18n/messages.js"></script>
<script src="i18n/i18n.js"></script>
<!--
First-run: one question instead of ten settings.
The answer decides three things at once — which skills get installed, how much
interface to show, and which chat app to suggest — so the user never has to
meet the words "skill", "tier" or "channel".
Cards are illustrated and short because this is asked before anyone has any
idea what the product does.
-->
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; padding: 3rem 1.5rem;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem;
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: #16181c; color: #f2f4f7;
}
h1 { margin: 0; font-size: 1.6rem; font-weight: 600; text-align: center; }
.sub { margin: 0; color: #9aa0a8; text-align: center; max-width: 34rem; line-height: 1.6; }
.grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: 1rem; width: 100%; max-width: 44rem;
}
.card {
padding: 1.5rem 1rem; text-align: center; font: inherit; color: inherit;
background: #23262c; border: 2px solid #33373f; border-radius: 14px;
cursor: pointer; transition: border-color .15s, transform .15s;
}
.card:hover, .card:focus-visible { border-color: #ff6b35; transform: translateY(-2px); outline: none; }
.card[aria-pressed="true"] { border-color: #ff6b35; background: rgba(255,107,53,.08); }
.card .icon { font-size: 2rem; display: block; margin-bottom: .5rem; }
.card .name { font-weight: 600; }
.actions { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; justify-content: center; }
button.go {
padding: .9rem 2.5rem; font: inherit; font-weight: 600; font-size: 1.05rem;
color: #16181c; background: #ff6b35; border: none; border-radius: 10px; cursor: pointer;
}
button.go:disabled { background: #3a3d44; color: #6f757d; cursor: not-allowed; }
.skip { color: #9aa0a8; background: none; border: none; font: inherit; cursor: pointer; text-decoration: underline; }
.status { color: #9aa0a8; min-height: 1.4em; font-size: .9rem; }
</style>
</head>
<body>
<h1 data-i18n="persona.title">What will you mostly use this for?</h1>
<p class="sub" data-i18n="persona.subtitle">This sets up the right tools for you. You can change it later, and picking more than one is fine.</p>
<div class="grid" id="grid"></div>
<div class="actions">
<button class="go" id="go" disabled data-i18n="persona.continue">Continue →</button>
<button class="skip" id="skip" data-i18n="persona.skip">Skip this</button>
</div>
<p class="status" id="status"></p>
<script>
var t = function (k, v) { return window.UClawI18n ? window.UClawI18n.t(k, v) : k; };
var PORT_FROM = 18788, PORT_TO = 18798;
var selected = [];
var serverPort = null;
async function findServer() {
for (var port = PORT_FROM; port <= PORT_TO; port++) {
try {
await fetch('http://127.0.0.1:' + port + '/api/config', { cache: 'no-store' });
return port;
} catch (e) { /* nothing listening here */ }
}
return null;
}
function card(persona) {
var el = document.createElement('button');
el.className = 'card';
el.type = 'button';
el.setAttribute('aria-pressed', 'false');
el.innerHTML = '<span class="icon">' + persona.emoji + '</span>'
+ '<span class="name">' + t('persona.' + persona.id) + '</span>';
el.addEventListener('click', function () {
var at = selected.indexOf(persona.id);
if (at === -1) { selected.push(persona.id); el.setAttribute('aria-pressed', 'true'); }
else { selected.splice(at, 1); el.setAttribute('aria-pressed', 'false'); }
document.getElementById('go').disabled = selected.length === 0;
});
return el;
}
// The wizard is driven by skills/manifest.json, so adding a persona there is
// enough — no page edit needed.
async function load() {
serverPort = await findServer();
if (serverPort === null) {
document.getElementById('status').textContent = t('persona.err_offline');
return;
}
var personas = await (await fetch('http://127.0.0.1:' + serverPort + '/api/personas')).json();
var grid = document.getElementById('grid');
personas.forEach(function (p) { grid.appendChild(card(p)); });
}
// Highest tier wins when several personas are picked: showing a developer the
// simple interface would be worse than showing an admin one extra menu.
var TIER_ORDER = ['simple', 'standard', 'expert'];
function combinedTier(personas, chosen) {
var best = 'simple';
personas.forEach(function (p) {
if (chosen.indexOf(p.id) !== -1 && TIER_ORDER.indexOf(p.tier) > TIER_ORDER.indexOf(best)) best = p.tier;
});
return best;
}
async function finish(chosen) {
var base = 'http://127.0.0.1:' + serverPort;
document.getElementById('go').disabled = true;
document.getElementById('status').textContent = t('persona.saving');
try {
var personas = await (await fetch(base + '/api/personas')).json();
var config = await (await fetch(base + '/api/config')).json();
config.uclaw = Object.assign({}, config.uclaw, {
personas: chosen,
tier: combinedTier(personas, chosen),
});
await fetch(base + '/api/config', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config),
});
// Best-effort: a failed skill install should not block getting to Settings.
for (var i = 0; i < chosen.length; i++) {
await fetch(base + '/api/install-skills', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ persona: chosen[i], locale: (window.UClawI18n || {}).locale || 'en' }),
}).catch(function () {});
}
location.replace(base + '/');
} catch (e) {
document.getElementById('status').textContent = t('persona.err_save');
document.getElementById('go').disabled = false;
}
}
document.getElementById('go').addEventListener('click', function () { finish(selected); });
document.getElementById('skip').addEventListener('click', function () { finish(['general']); });
load();
</script>
</body>
</html>

View File

@@ -0,0 +1,87 @@
// Works out which provider an API key belongs to, so users never have to know
// what a Base URL is.
//
// The old flow asked for three things — Base URL, model name and key — on the
// first screen a non-technical user ever saw. Every key in practice announces
// its own provider through its prefix, so ask for the key and derive the rest.
// Order matters: the longest, most specific prefixes are checked first, because
// several providers issue keys that also start with "sk-".
const PROVIDERS = [
{
id: 'anthropic',
label: 'Anthropic Claude',
prefixes: ['sk-ant-'],
baseUrl: 'https://api.anthropic.com/v1',
model: 'claude-sonnet-4-20250514',
models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'],
},
{
id: 'openrouter',
label: 'OpenRouter',
prefixes: ['sk-or-v1-', 'sk-or-'],
baseUrl: 'https://openrouter.ai/api/v1',
model: 'anthropic/claude-sonnet-4',
models: ['anthropic/claude-sonnet-4', 'openai/gpt-4o', 'google/gemini-2.5-flash'],
},
{
id: 'google',
label: 'Google Gemini',
prefixes: ['AIza'],
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
model: 'gemini-2.5-flash',
models: ['gemini-2.5-flash', 'gemini-2.5-pro'],
},
{
id: 'groq',
label: 'Groq',
prefixes: ['gsk_'],
baseUrl: 'https://api.groq.com/openai/v1',
model: 'llama-3.3-70b-versatile',
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
},
{
id: 'sealion',
label: 'SEA-LION',
prefixes: ['sk-sealion-'],
baseUrl: 'https://api.sea-lion.ai/v1',
model: 'aisingapore/Qwen-SEA-LION-v4.5-27B-IT',
models: ['aisingapore/Qwen-SEA-LION-v4.5-27B-IT', 'aisingapore/Llama-SEA-LION-v3.5-70B-R'],
},
{
id: 'openai',
label: 'OpenAI',
prefixes: ['sk-proj-', 'sk-svcacct-', 'sk-'],
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
models: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
},
];
export function detectProvider(rawKey) {
const key = String(rawKey ?? '').trim();
if (!key) return null;
for (const provider of PROVIDERS) {
if (provider.prefixes.some((prefix) => key.startsWith(prefix))) {
return { id: provider.id, label: provider.label, baseUrl: provider.baseUrl, model: provider.model, models: provider.models };
}
}
return null;
}
// Maps a failed request to a message key the UI can translate. Every branch has
// to name something the user can actually do next — "401 Unauthorized" on its
// own tells a non-technical user nothing.
export function classifyFailure({ status, code } = {}) {
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED') return 'key.err_offline';
if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'ABORT_ERR') return 'key.err_timeout';
if (status === 401 || status === 403) return 'key.err_rejected';
if (status === 429) return 'key.err_quota';
if (status === 404) return 'key.err_model';
if (typeof status === 'number' && status >= 500) return 'key.err_provider_down';
return 'key.err_unknown';
}
export function listProviders() {
return PROVIDERS.map(({ id, label, baseUrl, model, models }) => ({ id, label, baseUrl, model, models }));
}

View File

@@ -21,7 +21,7 @@ const REPO_ROOT = resolve(__dirname, '../..');
// 默认 OSS 路径前缀(你可以改成自己的 bucket / domain
const DEFAULT_OSS_BASE = 'https://u-claw-oss.56chat.cn/u-claw-open';
const GH_RELEASES_BASE = 'https://github.com/dongsheng123132/u-claw/releases';
const GH_RELEASES_BASE = 'https://gitea.fanghe.it.com/zhenghy/u-claw/releases';
function parseArgs(argv) {
const args = { notes: '', ossBase: DEFAULT_OSS_BASE };

203
portable/lib/self-heal.mjs Normal file
View File

@@ -0,0 +1,203 @@
// Checks and repairs that run when startup fails, before bothering the user.
//
// Diagnose used to be a tool people had to know existed: when startup broke, you
// were expected to work out that Windows-Diagnose.bat was the answer. Anyone who
// did not know that had already given up. These checks now run by themselves,
// and the user sees "checking…" rather than a stack trace.
//
// Two rules for anything added here:
// 1. A repair must be safe to run when nothing is wrong. Startup can fail for
// reasons none of these explain, and every check will run anyway.
// 2. Nothing here may destroy user data. Files that look wrong get moved
// aside with a timestamp, never deleted.
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, readdirSync, statSync, copyFileSync } from 'node:fs';
import { createServer } from 'node:net';
import { join, dirname } from 'node:path';
function portBusy(port) {
return new Promise((done) => {
const probe = createServer();
probe.once('error', () => done(true));
probe.once('listening', () => probe.close(() => done(false)));
probe.listen(port, '127.0.0.1');
});
}
// Moving aside beats deleting: if the diagnosis was wrong, the user still has
// their file. The stamp comes from the caller so this stays deterministic.
function setAside(path, stamp) {
const target = `${path}.broken-${stamp}`;
try { renameSync(path, target); return target; } catch { return null; }
}
export function buildChecks({ paths, defaultConfigPath, portRange, stamp }) {
return [
{
id: 'data-dirs',
repair() {
const wanted = [paths.state, join(paths.data, 'memory'), join(paths.data, 'backups'), join(paths.data, 'logs')];
const missing = wanted.filter((dir) => !existsSync(dir));
if (missing.length === 0) return null;
missing.forEach((dir) => mkdirSync(dir, { recursive: true }));
return `created ${missing.length} missing folder(s)`;
},
},
{
id: 'config-readable',
repair() {
if (!existsSync(paths.config)) return null;
try {
JSON.parse(readFileSync(paths.config, 'utf8'));
return null;
} catch {
// A half-written openclaw.json stops the gateway dead, and the message
// it produces means nothing to a non-technical user.
const moved = setAside(paths.config, stamp);
if (defaultConfigPath && existsSync(defaultConfigPath)) copyFileSync(defaultConfigPath, paths.config);
else writeFileSync(paths.config, `${JSON.stringify({ gateway: { mode: 'local', auth: { token: 'uclaw' } } }, null, 2)}\n`, 'utf8');
return moved ? `settings file was damaged, kept a copy at ${moved}` : 'rebuilt the settings file';
}
},
},
{
id: 'stale-runtime',
async repair() {
if (!existsSync(paths.runtimeJson)) return null;
let port = null;
try { port = JSON.parse(readFileSync(paths.runtimeJson, 'utf8'))?.configServerPort; } catch { /* unreadable */ }
// Left behind by a crash or an unplugged drive; it makes the launcher
// wait on a port nothing is listening on.
if (port && (await portBusy(port))) return null;
try { rmSync(paths.runtimeJson, { force: true }); } catch { return null; }
return 'cleared a leftover port record';
},
},
{
id: 'openclaw-present',
repair() {
const modules = join(paths.core, 'node_modules');
if (!existsSync(modules)) return null;
// node_modules exists but the entry point does not: a copy onto the
// drive that stopped part-way. Removing it makes startup reinstall.
if (existsSync(join(modules, 'openclaw', 'openclaw.mjs'))) return null;
const moved = setAside(modules, stamp);
return moved ? 'the copy on this drive was incomplete, will fetch it again' : null;
},
},
{
id: 'broken-cache-link',
repair() {
const browser = join(paths.state, 'browser');
if (!existsSync(browser)) return null;
try {
statSync(browser); // follows the link; throws when the target is gone
return null;
} catch {
// portable-cache points this at local disk. Plug the drive into a
// different machine and the link dangles, which breaks the browser.
try { rmSync(browser, { force: true, recursive: true }); } catch { return null; }
return 'cleared a cache link left by another computer';
}
},
},
{
id: 'ports-free',
async repair() {
const busy = [];
for (let port = portRange.from; port <= portRange.to; port++) {
if (await portBusy(port)) busy.push(port);
}
// Not repairable — killing a process we did not start is not ours to do.
// Reported so the message can say "it may already be running".
if (busy.length <= portRange.to - portRange.from) return null;
return null;
},
report: async () => {
const busy = [];
for (let port = portRange.from; port <= portRange.to; port++) {
if (await portBusy(port)) busy.push(port);
}
return busy.length ? `ports in use: ${busy.join(', ')}` : 'all ports free';
},
},
];
}
export async function runRepairs(checks) {
const applied = [];
for (const check of checks) {
try {
const result = await check.repair();
if (result) applied.push({ id: check.id, result });
} catch (error) {
// A failing check must never be the reason startup stops.
applied.push({ id: check.id, result: `check failed: ${error.message}` });
}
}
return applied;
}
// Written when the repairs did not help. Everything a support conversation would
// ask for, with the secrets taken out — the user should be able to read the file
// themselves before deciding to send it.
export async function writeDiagnostics({ paths, checks, applied, versions, stamp, extra = {} }) {
const redact = (value) => (typeof value === 'string' && value.length > 8 ? `${value.slice(0, 6)}…redacted` : '***');
let config = 'not readable';
try {
const parsed = JSON.parse(readFileSync(paths.config, 'utf8'));
const walk = (node) => {
if (!node || typeof node !== 'object') return node;
if (Array.isArray(node)) return node.map(walk);
return Object.fromEntries(Object.entries(node).map(([key, value]) =>
/key|token|secret|password/i.test(key) ? [key, redact(value)] : [key, walk(value)]));
};
config = JSON.stringify(walk(parsed), null, 2);
} catch (error) {
config = `not readable: ${error.message}`;
}
const listing = (dir) => {
try { return readdirSync(dir).sort().join(', ') || '(empty)'; } catch { return '(missing)'; }
};
const reports = [];
for (const check of checks) {
if (typeof check.report !== 'function') continue;
try { reports.push(`${check.id}: ${await check.report()}`); } catch { /* skip */ }
}
const lines = [
'U-Claw diagnostics',
`Written: ${stamp}`,
'',
'API keys and tokens have been replaced with "redacted" — you can read this',
'file before you send it anywhere.',
'',
'## System',
`platform: ${process.platform} ${process.arch}`,
`node: ${process.version}`,
...Object.entries(versions).map(([key, value]) => `${key}: ${value ?? 'unknown'}`),
...Object.entries(extra).map(([key, value]) => `${key}: ${value}`),
'',
'## Repairs attempted',
applied.length ? applied.map((a) => `- ${a.id}: ${a.result}`).join('\n') : '- none needed',
'',
'## Checks',
reports.length ? reports.map((r) => `- ${r}`).join('\n') : '- none',
'',
'## Folders',
`drive root: ${listing(dirname(paths.data))}`,
`data/.openclaw: ${listing(paths.state)}`,
`app/core/node_modules: ${listing(join(paths.core, 'node_modules'))}`,
'',
'## Settings (redacted)',
config,
'',
];
const target = join(paths.state, 'diagnostics.txt');
writeFileSync(target, lines.join('\n'), 'utf8');
return target;
}

View File

@@ -1,14 +1,21 @@
// setup-local-model.mjs — 内网/本地模型一键配置(不碰 Control UI
// setup-local-model.mjs — set up a local or self-hosted model from the command line.
//
// 给"内网/离线"场景:用纯命令行问几个问题,直接写好 openclaw.json
// 再当场实测能不能连上、能不能回话。全程不依赖会挂的 dashboard 网页。
// 支持两类本地/内网模型:
// 1) Ollama本机http://127.0.0.1:11434
// 2) newapi / 任意 OpenAI 兼容中转(内网 IP + token
// For offline and corporate-network situations: ask a few questions, write
// openclaw.json directly, then actually test that the model answers. Deliberately
// does not depend on the dashboard, which is the thing that tends to be broken
// when someone reaches for this tool.
//
// 写入只 merge 模型相关字段,保留 gateway 等原有配置;写前自动备份。
// Two kinds of endpoint:
// 1) Ollama on this machine (http://127.0.0.1:11434)
// 2) Any OpenAI-compatible endpoint — a company server, a relay (URL + token)
//
// 用法node setup-local-model.mjs <CONFIG_PATH>
// Only the model-related fields are merged in; gateway and other settings are
// left alone, and the previous file is backed up first.
//
// Usage: node setup-local-model.mjs <CONFIG_PATH>
//
// Input: a real terminal gets interactive readline; piped input (tests, scripts)
// is read once and dequeued line by line.
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
@@ -17,7 +24,6 @@ import { stdin as input, stdout as output } from 'node:process';
import http from 'node:http';
import https from 'node:https';
// 输入抽象:真控制台(TTY)走交互式 readline被管道喂入(测试/脚本)则一次读完按行出队。
function makePrompter() {
if (input.isTTY) {
const rl = createInterface({ input, output });
@@ -69,7 +75,7 @@ async function chatTest(baseUrl, apiKey, modelId) {
const res = await requestText(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
body: JSON.stringify({ model: modelId, messages: [{ role: 'user', content: '请回复四个字:连接成功' }], max_tokens: 64, stream: false }),
body: JSON.stringify({ model: modelId, messages: [{ role: 'user', content: 'Reply with exactly: connection ok' }], max_tokens: 64, stream: false }),
timeoutMs: CHAT_TIMEOUT_MS,
});
const ms = Date.now() - started;
@@ -111,10 +117,10 @@ function requestText(rawUrl, { method = 'GET', headers = {}, body, timeoutMs })
async function main() {
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
if (!configPath) { line('用法: node setup-local-model.mjs <CONFIG_PATH>'); process.exitCode = 2; return; }
if (!configPath) { line('Usage: node setup-local-model.mjs <CONFIG_PATH>'); process.exitCode = 2; return; }
line('========================================');
line(' U-Claw 内网/本地模型 一键配置');
line(' U-Claw — set up a local or self-hosted model');
line('========================================');
line('');
@@ -122,32 +128,32 @@ async function main() {
const ask = rl.ask;
try {
line('选择模型类型:');
line(' 1) Ollama(本机部署,http://127.0.0.1:11434');
line(' 2) newapi / 其它 OpenAI 兼容中转(内网 IP + token');
const kind = await ask('输入 1 2', '1');
line('Which kind of model?');
line(' 1) Ollama running on this machine (http://127.0.0.1:11434)');
line(' 2) A self-hosted or company endpoint (any OpenAI-compatible URL + token)');
const kind = await ask('Enter 1 or 2', '1');
let providerKey, baseUrl, apiKey, modelId;
if (kind === '2') {
providerKey = 'newapi';
line('');
line('提示baseUrl 通常形如 http://192.168.1.50:3000/v1注意大多要带 /v1');
baseUrl = await ask('newapi 地址 baseUrl', 'http://192.168.1.50:3000/v1');
line('The endpoint usually looks like http://192.168.1.50:3000/v1 — most need the /v1 on the end.');
baseUrl = await ask('Endpoint URL', 'http://192.168.1.50:3000/v1');
apiKey = await ask('token / API Key', '');
modelId = await ask('模型 ID管理员给的 deepseek-v3', '');
modelId = await ask('Model ID — your admin will have given you this, e.g. deepseek-v3', '');
} else {
providerKey = 'ollama';
line('');
baseUrl = await ask('Ollama 地址(一般本机默认即可)', 'http://127.0.0.1:11434/v1');
// Ollama OpenAI 兼容端点在 /v1自动补上
baseUrl = await ask('Ollama URL — the default is right unless you changed it', 'http://127.0.0.1:11434/v1');
// Ollama's OpenAI-compatible endpoint lives at /v1; add it if missing
if (!/\/v1\/?$/.test(baseUrl)) baseUrl = baseUrl.replace(/\/+$/, '') + '/v1';
apiKey = 'ollama'; // 本地占位 key任意值即可
modelId = await ask('模型名(先用 ollama list 查,如 qwen2.5 / llama3.1', 'qwen2.5');
apiKey = 'ollama'; // Ollama ignores auth; any placeholder works
modelId = await ask('Model name — run `ollama list` to see yours, e.g. qwen2.5', 'qwen2.5');
}
if (!baseUrl || !modelId) { line(''); line('地址或模型 ID 为空,已取消。'); process.exitCode = 2; return; }
if (!baseUrl || !modelId) { line(''); line('No endpoint or model ID given — nothing was changed.'); process.exitCode = 2; return; }
// 读取并合并现有配置(保留 gateway 等),写前备份
// Merge into the existing config so gateway and other settings survive; back up first
let config = {};
if (existsSync(configPath)) {
try { config = JSON.parse(readFileSync(configPath, 'utf8')); }
@@ -174,39 +180,39 @@ async function main() {
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
line('');
line(`已写入配置:${configPath}`);
line(`Saved to ${configPath}`);
line(` provider=${providerKey} baseUrl=${baseUrl} model=${modelId}`);
if (existsSync(configPath + '.bak')) line(` (原配置已备份为 openclaw.json.bak`);
if (existsSync(configPath + '.bak')) line(' Your previous settings were kept as openclaw.json.bak');
line('');
// 当场实测
line('正在实测:发一条对话给模型...');
// Prove it actually works rather than just claiming it was saved
line('Testing it — sending one message to the model…');
const r = await chatTest(baseUrl, apiKey, modelId);
line('');
if (r.ok) {
line(`✓ 跑通了!模型回复 (${r.ms}ms)${r.reply.slice(0, 120) || '(空回复但请求成功)'}`);
line(` It works. The model replied in ${r.ms}ms: ${r.reply.slice(0, 120) || '(empty reply, but the request succeeded)'}`);
line('');
line('配置完成。现在双击 Windows-Start.bat 即可正常使用(对话可走 CLI 或 Dashboard');
line('You are set. Start U-Claw normally and it will use this model.');
} else if (r.status) {
line(`服务端 HTTP ${r.status} (${r.ms}ms)${r.body}`);
line(`The server answered with HTTP ${r.status} in ${r.ms}ms: ${r.body}`);
line('');
if (r.status === 401 || r.status === 403) line('→ 网络通,但 token / key 不对Ollama 可忽略鉴权newapi 请核对 token');
else if (r.status === 404) line('→ 网络通,但路径或模型 ID 不对(检查 baseUrl 是否要带 /v1、模型名是否正确');
else line('→ 网络通,服务端报错,把上面内容发管理员。');
if (r.status === 401 || r.status === 403) line(' The network is fine but the token was rejected. Ollama ignores auth entirely; for a company endpoint, check the token with whoever issued it.');
else if (r.status === 404) line(' The network is fine but the path or model ID is wrong. Check whether the endpoint needs /v1 on the end, and that the model name matches exactly.');
else line(' The network is fine; the server itself errored. Send the text above to whoever runs it.');
} else {
const host = (() => { try { return new URL(withScheme(baseUrl)).hostname; } catch { return baseUrl; } })();
line(`连不上:${r.error} (${r.ms}ms)`);
line(`Could not reach it: ${r.error} (${r.ms}ms)`);
line('');
if (providerKey === 'ollama') {
line('→ 本机 Ollama 没连上,多半是 Ollama 没启动或模型没拉。请在本机执行:');
line(' ollama serve (启动服务,若已是后台服务可跳过)');
line(` ollama pull ${modelId} (把模型拉到本地,离线需提前准备好)`);
line(' 然后重新运行本工具。');
line(' Ollama is not answering. It is usually not running, or the model was never pulled. On this machine, run:');
line(' ollama serve starts it, skip if it already runs in the background');
line(` ollama pull ${modelId} downloads the model — do this while you still have internet`);
line(' Then run this tool again.');
} else {
line('→ 是地址错 / 内网不通 / 防火墙 / 模型服务没起。');
line(' 在这台机器上自测:');
line(' Either the address is wrong, the network cannot reach it, a firewall is blocking it, or the service is down.');
line(' Test it from this machine with:');
line(` ping ${host}`);
line(' 让机房管理员确认 IP、端口、防火墙放行。');
line(' If that also fails, ask whoever runs the server to confirm the address, port and firewall rules.');
}
}
} finally {

553
portable/lib/start.mjs Normal file
View File

@@ -0,0 +1,553 @@
#!/usr/bin/env node
// U-Claw portable launcher.
//
// Everything the old START HERE - Windows.bat and START HERE - Mac.command did lives here, so
// the two platforms cannot drift apart again and so every user-facing string can
// be translated. The launchers are now thin shells that locate node and hand off.
//
// Two constraints drove this:
// 1. tests/windows-launchers.test.mjs requires customer-facing .bat files to be
// pure ASCII — Chinese Windows cmd.exe reads non-ASCII bytes as GBK and
// mis-parses the script. Localised text therefore cannot live in a .bat.
// 2. cmd.exe treats an unescaped ) as a block terminator, which produced a
// class of flash-exit regressions. Moving the logic out removes the hazard.
//
// Errors follow the house rule: what happened, why, and one thing to do next.
import { spawn, spawnSync, execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, cpSync, rmSync, renameSync } from 'node:fs';
import { createServer } from 'node:net';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import process from 'node:process';
import { resolveLocale, createTranslator } from './i18n.mjs';
import { buildChecks, runRepairs, writeDiagnostics } from './self-heal.mjs';
const GATEWAY_PORT_FROM = 18789;
const GATEWAY_PORT_TO = 18799;
const CONFIG_SERVER_DEFAULT_PORT = 18788;
const CONFIG_SERVER_WAIT_MS = 6000;
const GATEWAY_TOKEN = 'uclaw';
const libDir = dirname(fileURLToPath(import.meta.url));
const uclawDir = resolve(libDir, '..');
const paths = {
app: join(uclawDir, 'app'),
data: join(uclawDir, 'data'),
get core() { return join(this.app, 'core'); },
get state() { return join(this.data, '.openclaw'); },
get config() { return join(this.state, 'openclaw.json'); },
get runtimeJson() { return join(this.state, 'runtime.json'); },
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// ---------------------------------------------------------------- output
const dim = (s) => `${s}`;
const green = (s) => `${s}`;
const yellow = (s) => `${s}`;
function say(message) { process.stdout.write(` ${message}\n`); }
function ok(message) { say(green(message)); }
function note(message) { say(dim(message)); }
// Every user-facing failure gets the same three parts. Never print only a symptom.
function bail(t, key, vars = {}) {
process.stdout.write('\n');
say(yellow(t(`${key}.what`, vars)));
say(t(`${key}.why`, vars));
process.stdout.write('\n');
say(`${t(`${key}.action`, vars)}`);
process.stdout.write('\n');
process.exitCode = 1;
}
// ---------------------------------------------------------------- platform
function runtimeDirName() {
if (process.platform === 'win32') return 'node-win-x64';
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'node-mac-arm64' : 'node-mac-x64';
return process.arch === 'arm64' ? 'node-linux-arm64' : 'node-linux-x64';
}
function nodeBinary(runtimeDir) {
return process.platform === 'win32'
? join(runtimeDir, 'node.exe')
: join(runtimeDir, 'bin', 'node');
}
function npmEntry(runtimeDir) {
return process.platform === 'win32'
? join(runtimeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js')
: join(runtimeDir, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js');
}
function openUrl(url) {
try {
if (process.platform === 'win32') {
// The empty "" is the window title; without it cmd treats a quoted URL as one.
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
} else if (process.platform === 'darwin') {
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
} else {
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
}
} catch {
// Opening a browser is a convenience, never a reason to fail startup.
}
}
// ---------------------------------------------------------------- steps
// Older drives shipped core-win / core-mac. Rename in place so one code path works.
function migrateLegacyCoreDir() {
for (const legacy of ['core-win', 'core-mac']) {
const from = join(paths.app, legacy);
if (existsSync(from) && !existsSync(paths.core)) {
try { renameSync(from, paths.core); } catch { /* keep going with the old name */ }
}
}
}
function clearMacQuarantine(t, nodeBin) {
if (process.platform !== 'darwin') return;
try {
const attrs = execFileSync('xattr', ['-l', nodeBin], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
if (!attrs.includes('com.apple.quarantine')) return;
note(t('start.quarantine_removing'));
spawnSync('xattr', ['-rd', 'com.apple.quarantine', uclawDir], { stdio: 'ignore' });
} catch {
// No xattr, or nothing quarantined — either way there is nothing to clear.
}
}
function ensureDataDirs() {
for (const dir of [paths.state, join(paths.data, 'memory'), join(paths.data, 'backups'), join(paths.data, 'logs')]) {
mkdirSync(dir, { recursive: true });
}
}
// Moves the heavy, rebuildable caches (browser profile, V8 compile cache) off the
// USB drive and onto local disk. Failing here only costs speed, so stay silent.
function applyPortableCache(t, nodeBin) {
try {
const out = execFileSync(nodeBin, [join(libDir, 'portable-cache.mjs'), paths.state, uclawDir], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
});
let cacheRoot = null;
for (const line of out.split(/\r?\n/)) {
const [key, ...rest] = line.split('=');
const value = rest.join('=');
if (key === 'UCLAW_COMPILE_CACHE_DIR') process.env.NODE_COMPILE_CACHE = value;
if (key === 'UCLAW_CACHE_ROOT') cacheRoot = value;
}
if (process.env.NODE_COMPILE_CACHE && cacheRoot) note(t('start.cache_local', { path: cacheRoot }));
} catch { /* cache stays on the drive */ }
}
function ensureConfig(t) {
if (existsSync(paths.config)) return;
const legacy = join(paths.data, 'config.json');
if (existsSync(legacy)) {
note(t('start.config_migrating'));
copyFileSync(legacy, paths.config);
ok(t('start.config_migrated'));
return;
}
note(t('start.config_creating'));
writeFileSync(
paths.config,
`${JSON.stringify({ gateway: { mode: 'local', auth: { token: GATEWAY_TOKEN } } }, null, 2)}\n`,
'utf8'
);
ok(t('start.config_created'));
}
function installDependencies(t, nodeBin, runtimeDir) {
if (existsSync(join(paths.core, 'node_modules'))) return true;
// The shipped zip has deps pre-installed; reaching here means an incomplete
// copy or a source checkout. Say "getting ready", not "node_modules not found".
note(t('start.deps_preparing'));
note(t('start.deps_slow_drive'));
const result = spawnSync(
nodeBin,
[npmEntry(runtimeDir), 'install', '--registry=https://registry.npmjs.org',
'--ignore-scripts', '--no-audit', '--no-fund', '--omit=dev'],
{
cwd: paths.core,
stdio: ['ignore', 'ignore', 'inherit'],
// Keep npm's cache inside the app so nothing is left on the host machine.
env: { ...process.env, npm_config_cache: join(paths.app, '.npm-cache') },
}
);
if (result.status !== 0 || !existsSync(join(paths.core, 'node_modules'))) return false;
ok(t('start.deps_done'));
return true;
}
// Diagnose used to be something the user had to know to run. By the time
// startup had visibly failed, anyone who did not know that had already given up.
// Now it runs by itself, and only speaks up when it cannot fix the problem.
async function healAndRetry(t, attempt) {
note(t('start.checking'));
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const checks = buildChecks({
paths,
defaultConfigPath: join(uclawDir, 'default-config.json'),
portRange: { from: GATEWAY_PORT_FROM, to: GATEWAY_PORT_TO },
stamp,
});
const applied = await runRepairs(checks);
if (applied.length > 0) {
for (const { result } of applied) note(` ${result}`);
note(t('start.repaired'));
// Silence on success is the point: a user who never learns something was
// wrong had a working product.
if (await attempt()) return { healed: true };
}
const report = await writeDiagnostics({
paths,
checks,
applied,
stamp,
versions: {
openclaw: readVersionFile('OPENCLAW_VERSION'),
node_pinned: readVersionFile('NODE_VERSION'),
},
});
return { healed: false, report };
}
function readVersionFile(name) {
for (const dir of [uclawDir, join(uclawDir, '..')]) {
try { return readFileSync(join(dir, name), 'utf8').trim(); } catch { /* try next */ }
}
return null;
}
// OpenClaw routes every fetch through HTTP_PROXY when it is set, which breaks
// self-hosted model endpoints on a corporate network. Put those hosts in NO_PROXY.
function applyNoProxy(t, nodeBin) {
try {
const out = execFileSync(nodeBin, [join(libDir, 'resolve-no-proxy.mjs'), paths.config], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
if (!out.startsWith('UCLAW_NO_PROXY=')) return;
const hosts = out.slice('UCLAW_NO_PROXY='.length);
if (!hosts) return;
process.env.NO_PROXY = hosts;
process.env.no_proxy = hosts;
note(t('start.no_proxy', { hosts }));
} catch { /* no proxy configured */ }
}
// Hands the resolved locale to the HTML pages. They are opened from file://,
// where fetching the drive's own openclaw.json is blocked, so they cannot work
// out the locale themselves — and falling back to navigator.language would make
// the language follow the machine instead of the drive, which is exactly the
// behaviour we do not want. A generated <script> is the one channel that works
// from both file:// and http://.
// True until the drive records a language of its own. Asking once and never
// again is the point: the answer lives on the drive, not in a browser profile.
function localeChosenOnDrive() {
return Boolean(driveSetting('locale'));
}
// The wizard runs once. Its answer decides which skills are installed and how
// much interface to show, so re-asking would quietly undo the user's setup.
function personaChosenOnDrive() {
const personas = driveSetting('personas');
return Array.isArray(personas) && personas.length > 0;
}
function driveSetting(key) {
try {
return JSON.parse(readFileSync(paths.config, 'utf8'))?.uclaw?.[key];
} catch {
return undefined;
}
}
function writeLocaleForPages(locale) {
try {
writeFileSync(
join(paths.state, 'locale.js'),
`/* GENERATED by lib/start.mjs — the drive's language, for pages opened from file://. */\nwindow.UCLAW_LOCALE = ${JSON.stringify(locale)};\n`,
'utf8'
);
} catch { /* pages fall back to the browser language */ }
}
// Records what is actually on this drive, so an updater can diff instead of
// guessing. This release deliberately does not self-update — a USB drive can be
// pulled mid-write and there is no way back from a half-applied update — but
// without a written-down inventory a later updater has nothing to compare
// against, and retrofitting one means the first upgrade is the risky one.
function writeInstalledManifest() {
const read = (file) => {
for (const dir of [uclawDir, join(uclawDir, '..')]) {
try { return readFileSync(join(dir, file), 'utf8').trim(); } catch { /* try next */ }
}
return null;
};
let skills = null;
try {
const manifest = JSON.parse(readFileSync(join(uclawDir, 'skills', 'manifest.json'), 'utf8'));
skills = {
schemaVersion: manifest.schemaVersion,
shipping: manifest.skills.filter((s) => s.status === 'shipping').map((s) => s.id),
};
} catch { /* skills may not be staged in a dev checkout */ }
const inventory = {
// Bump when the shape of this file changes, not when contents change.
schemaVersion: 1,
openclaw: read('OPENCLAW_VERSION'),
node: read('NODE_VERSION') ?? process.version,
skills,
updatePolicy: 'frozen-notify-only',
};
try {
writeFileSync(join(paths.state, 'installed.json'), `${JSON.stringify(inventory, null, 2)}\n`, 'utf8');
} catch { /* inventory is diagnostic, never a reason to fail startup */ }
}
function startUpdateCheck(nodeBin) {
let versionFile = join(uclawDir, 'OPENCLAW_VERSION');
if (!existsSync(versionFile)) versionFile = join(uclawDir, '..', 'OPENCLAW_VERSION');
if (!existsSync(versionFile)) return;
try {
spawn(nodeBin, [join(libDir, 'check-update.mjs'), versionFile, paths.state], {
detached: true, stdio: 'ignore', windowsHide: true,
}).unref();
} catch { /* update banner is optional */ }
}
// Was Windows-only before; the Mac launcher never did it, so Mac users silently
// had no WeChat channel.
function installWeChatPlugin(t) {
const src = join(paths.app, 'extensions', 'openclaw-weixin');
if (!existsSync(join(src, 'openclaw.plugin.json'))) return;
const home = process.env.USERPROFILE || process.env.HOME;
if (!home) return;
const dest = join(home, '.openclaw', 'extensions', 'openclaw-weixin');
if (existsSync(join(dest, 'openclaw.plugin.json'))) return;
try {
note(t('start.wechat_installing'));
mkdirSync(dirname(dest), { recursive: true });
cpSync(src, dest, { recursive: true });
ok(t('start.wechat_installed'));
} catch { /* channel plugin is optional */ }
}
function portFree(port) {
return new Promise((done) => {
const probe = createServer();
probe.once('error', () => done(false));
probe.once('listening', () => probe.close(() => done(true)));
probe.listen(port, '127.0.0.1');
});
}
async function findGatewayPort(t) {
for (let port = GATEWAY_PORT_FROM; port <= GATEWAY_PORT_TO; port++) {
if (await portFree(port)) return port;
note(t('start.port_in_use', { port }));
}
return null;
}
// The config server picks its own port when 18788 is taken and records the real
// one in runtime.json. The old Mac launcher hardcoded 18788 and opened a dead
// page whenever that fallback kicked in.
async function startConfigServer(t, nodeBin) {
note(t('start.config_center_starting'));
try { rmSync(paths.runtimeJson, { force: true }); } catch { /* nothing to clear */ }
const child = spawn(nodeBin, [join(uclawDir, 'config-server', 'server.js')], {
stdio: 'ignore', windowsHide: true,
});
child.unref();
const deadline = Date.now() + CONFIG_SERVER_WAIT_MS;
while (Date.now() < deadline) {
if (existsSync(paths.runtimeJson)) {
try {
const port = JSON.parse(readFileSync(paths.runtimeJson, 'utf8'))?.configServerPort;
if (port) return { child, port };
} catch { /* still being written */ }
}
await sleep(200);
}
return { child, port: CONFIG_SERVER_DEFAULT_PORT };
}
// Fallback for the splash screen. loading.html polls /ready itself and moves on
// when the gateway answers, but some browsers block fetch from a file:// page —
// those users would sit on a splash that never advances. Watch the port here
// too, and on timeout put them back in the Control Panel rather than nowhere.
// Replaces lib/wait-gateway.bat, which only ever ran on Windows.
function watchGatewayReady(port, configPort, { timeoutMs = 300_000, intervalMs = 2000 } = {}) {
const deadline = Date.now() + timeoutMs;
const timer = setInterval(async () => {
if (await portFree(port)) {
// Still free means nothing is listening yet.
if (Date.now() >= deadline) {
clearInterval(timer);
openUrl(`http://127.0.0.1:${configPort}/`);
}
return;
}
clearInterval(timer); // Gateway is up; the splash will have moved on by itself.
}, intervalMs);
timer.unref();
}
// ---------------------------------------------------------------- main
async function main() {
const locale = resolveLocale({ override: process.env.UCLAW_LOCALE, configPath: paths.config });
const t = createTranslator(locale);
process.stdout.write(`\n ${green(t('start.banner'))}\n\n`);
migrateLegacyCoreDir();
const runtimeDir = join(paths.app, 'runtime', runtimeDirName());
const nodeBin = nodeBinary(runtimeDir);
if (!existsSync(nodeBin)) return bail(t, 'error.node_missing');
if (process.platform === 'darwin' && !['arm64', 'x64'].includes(process.arch)) {
return bail(t, 'error.unsupported_arch', { arch: process.arch });
}
clearMacQuarantine(t, nodeBin);
note(t('start.node_version', { version: process.version }));
process.env.PATH = `${dirname(nodeBin)}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`;
process.env.OPENCLAW_HOME = paths.data;
process.env.OPENCLAW_STATE_DIR = paths.state;
process.env.OPENCLAW_CONFIG_PATH = paths.config;
// U-Claw opens the local dashboard directly. Advertising the gateway over mDNS
// is unnecessary and crashes the bonjour plugin on machines with VPN adapters.
process.env.OPENCLAW_DISABLE_BONJOUR = '1';
ensureDataDirs();
applyPortableCache(t, nodeBin);
ensureConfig(t);
if (!installDependencies(t, nodeBin, runtimeDir)) {
const outcome = await healAndRetry(t, async () => installDependencies(t, nodeBin, runtimeDir));
if (!outcome.healed) {
bail(t, 'error.deps_failed');
say(t('start.diagnostics_written', { path: outcome.report }));
say(t('start.diagnostics_hint'));
return;
}
}
applyNoProxy(t, nodeBin);
writeLocaleForPages(locale);
writeInstalledManifest();
startUpdateCheck(nodeBin);
installWeChatPlugin(t);
const { child: configServer, port: configPort } = await startConfigServer(t, nodeBin);
note(t('start.config_center_port', { port: configPort }));
let port = await findGatewayPort(t);
if (!port) {
const outcome = await healAndRetry(t, async () => Boolean(await findGatewayPort(t)));
port = outcome.healed ? await findGatewayPort(t) : null;
if (!port) {
configServer.kill();
bail(t, 'error.no_port', { from: GATEWAY_PORT_FROM, to: GATEWAY_PORT_TO });
if (outcome.report) {
say(t('start.diagnostics_written', { path: outcome.report }));
say(t('start.diagnostics_hint'));
}
return;
}
}
note(t('start.gateway_starting', { port }));
const openclawEntry = join(paths.core, 'node_modules', 'openclaw', 'openclaw.mjs');
const gateway = spawn(
nodeBin,
[openclawEntry, 'gateway', 'run', '--allow-unconfigured', '--force', '--port', String(port)],
{ cwd: paths.core, stdio: 'inherit', env: process.env, windowsHide: true }
);
// Open the splash first: it polls /ready itself and gives immediate feedback
// instead of a blank window while a slow drive unpacks components.
const loadingUrl = `${pathToFileURL(join(libDir, 'loading.html')).href}?port=${port}&token=${GATEWAY_TOKEN}`;
note(t('start.opening_screen'));
openUrl(loadingUrl);
// First run gets the language chooser instead of Settings; it writes the
// choice to the drive and then forwards to Settings itself.
// First run walks language → what-for → key. Each step writes its answer to
// the drive and is skipped once answered, so a second launch goes straight in.
if (!localeChosenOnDrive()) {
note(t('start.opening_language'));
openUrl(pathToFileURL(join(libDir, 'language.html')).href);
} else if (!personaChosenOnDrive()) {
note(t('start.opening_persona'));
openUrl(pathToFileURL(join(libDir, 'persona.html')).href);
} else {
note(t('start.opening_config'));
openUrl(`http://127.0.0.1:${configPort}/`);
}
try {
spawn(nodeBin, [join(libDir, 'prewarm.mjs'), String(port), GATEWAY_TOKEN], {
detached: true, stdio: 'ignore', windowsHide: true,
}).unref();
} catch { /* prewarm is best-effort */ }
watchGatewayReady(port, configPort);
process.stdout.write('\n');
ok(t('start.running_title'));
say(t('start.running_dashboard', { url: `http://127.0.0.1:${port}/#token=${GATEWAY_TOKEN}` }));
say(t('start.running_config', { url: `http://127.0.0.1:${configPort}/` }));
process.stdout.write('\n');
note(t('start.first_run_wait'));
note(t('start.running_hint'));
process.stdout.write('\n');
let stopping = false;
const stop = () => {
if (stopping) return;
stopping = true;
gateway.kill();
configServer.kill();
};
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
gateway.on('exit', (code) => {
configServer.kill();
process.stdout.write('\n');
// 0 is a clean stop; 3221225786 is Windows' Ctrl+C exit code.
if (!stopping && code !== 0 && code !== 3221225786) {
say(yellow(t('start.exited_unexpectedly', { code })));
}
ok(t('start.stopped'));
process.exitCode = code === 0 || stopping ? 0 : 1;
});
}
main().catch((error) => {
process.stderr.write(` ${error?.stack ?? error}\n`);
process.exitCode = 1;
});

View File

@@ -1,32 +0,0 @@
@echo off
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 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 CONFIG_PORT
REM Polls every 2 seconds, up to about 5 minutes.
set "PORT=%~1"
if "%PORT%"=="" set "PORT=18789"
set "CONFIG_PORT=%~2"
if "%CONFIG_PORT%"=="" set "CONFIG_PORT=18788"
set /a TRIES=0
:wait_loop
netstat -an | findstr ":%PORT% " | findstr "LISTENING" >nul 2>&1
if %errorlevel%==0 goto :ready
set /a TRIES+=1
if %TRIES% geq 150 goto :timeout
REM Use ping (not "timeout /t") for the ~2s delay: if a GNU coreutils "timeout"
REM is on PATH (Git/MSYS bin), it rejects "/t" and this poll becomes a busy-loop.
REM ping for the delay is PATH-robust and matches Windows-Start.bat's wait loop.
ping -n 3 127.0.0.1 >nul 2>&1
goto :wait_loop
:ready
exit /b 0
:timeout
start "" http://127.0.0.1:%CONFIG_PORT%/
exit /b 1