按 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:
@@ -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
81
portable/lib/i18n.mjs
Normal 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 };
|
||||
75
portable/lib/i18n/build-messages.mjs
Normal file
75
portable/lib/i18n/build-messages.mjs
Normal 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
152
portable/lib/i18n/i18n.js
Normal 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);
|
||||
566
portable/lib/i18n/messages.js
Normal file
566
portable/lib/i18n/messages.js
Normal 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 30–90 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 30–90 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 盘启动需要 30–90 秒解压组件。控制台已经打开,可以先选模型、填密钥。",
|
||||
"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 盘首次启动通常 30–90 秒。如果迟迟没反应,可以先打开设置:",
|
||||
"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 进去就多一个。"
|
||||
}
|
||||
};
|
||||
281
portable/lib/i18n/skills-data.js
Normal file
281
portable/lib/i18n/skills-data.js
Normal 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": "🗒️"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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
101
portable/lib/language.html
Normal 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>
|
||||
@@ -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 盘首次启动通常 30–90 秒。<br>
|
||||
如果迟迟没反应,可先打开
|
||||
<a class="manual" id="manual" href="#">配置中心</a>。
|
||||
<span data-i18n="loading.slow_drive_note">A slow drive usually takes 30–90 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);
|
||||
})();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
279
portable/lib/messages/en.json
Normal file
279
portable/lib/messages/en.json
Normal 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 30–90 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 30–90 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."
|
||||
}
|
||||
279
portable/lib/messages/zh-CN.json
Normal file
279
portable/lib/messages/zh-CN.json
Normal 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 盘启动需要 30–90 秒解压组件。控制台已经打开,可以先选模型、填密钥。",
|
||||
"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 盘首次启动通常 30–90 秒。如果迟迟没反应,可以先打开设置:",
|
||||
"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
153
portable/lib/persona.html
Normal 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>
|
||||
87
portable/lib/provider-detect.mjs
Normal file
87
portable/lib/provider-detect.mjs
Normal 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 }));
|
||||
}
|
||||
@@ -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
203
portable/lib/self-heal.mjs
Normal 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;
|
||||
}
|
||||
@@ -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
553
portable/lib/start.mjs
Normal 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) => `[2m${s}[0m`;
|
||||
const green = (s) => `[32m${s}[0m`;
|
||||
const yellow = (s) => `[33m${s}[0m`;
|
||||
|
||||
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;
|
||||
});
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user