u1s1-cli 1.11.8 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/agent-setup.d.ts +5 -0
  2. package/dist/agent-setup.js +31 -7
  3. package/dist/api.js +42 -34
  4. package/dist/bench-report.js +12 -9
  5. package/dist/bench.js +34 -31
  6. package/dist/brand.d.ts +2 -1
  7. package/dist/brand.js +38 -12
  8. package/dist/config.d.ts +11 -1
  9. package/dist/config.js +25 -8
  10. package/dist/context-usage.js +12 -9
  11. package/dist/deploy.js +49 -45
  12. package/dist/device-auth.js +7 -5
  13. package/dist/error-humanize.d.ts +1 -0
  14. package/dist/error-humanize.js +18 -6
  15. package/dist/feedback.js +48 -31
  16. package/dist/i18n.d.ts +21 -0
  17. package/dist/i18n.js +46 -0
  18. package/dist/import/index.js +64 -28
  19. package/dist/import/skills.js +72 -41
  20. package/dist/import/util.js +3 -2
  21. package/dist/import/write.js +2 -1
  22. package/dist/index.js +139 -48
  23. package/dist/login.js +39 -28
  24. package/dist/loop.js +18 -12
  25. package/dist/mcp/client.js +15 -14
  26. package/dist/mcp/command.js +76 -48
  27. package/dist/mcp/extension.js +5 -3
  28. package/dist/mcp/tools.js +6 -5
  29. package/dist/model.js +11 -10
  30. package/dist/nudges.d.ts +8 -2
  31. package/dist/nudges.js +22 -3
  32. package/dist/search-tools.js +7 -6
  33. package/dist/shell-doctor.js +9 -8
  34. package/dist/subagent.js +5 -4
  35. package/dist/tools.js +53 -37
  36. package/dist/update.js +29 -28
  37. package/dist/usage.d.ts +1 -1
  38. package/dist/usage.js +96 -43
  39. package/dist/web.js +2 -1
  40. package/dist/workflow/runner.js +33 -29
  41. package/dist/workflow/templates.js +70 -45
  42. package/dist/workflow/tool.js +15 -12
  43. package/package.json +2 -2
  44. package/scripts/patch-pi.js +243 -278
  45. package/scripts/render-regression.mjs +55 -4
package/dist/update.js CHANGED
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { activeSite, isPortableInstall } from "./config.js";
9
+ import { tr } from "./i18n.js";
9
10
  import { installCommand } from "./site.js";
10
11
  import { readJsonResponseCapped, readResponseTextCapped } from "./api.js";
11
12
  const require = createRequire(import.meta.url);
@@ -81,7 +82,7 @@ export function buildWindowsUpdateLauncherScript() {
81
82
  "$inner = '-NoProfile -ExecutionPolicy Bypass -NoExit -File \"' + $PSScriptRoot + '\\update.ps1\"'",
82
83
  "$ps = (Get-Command pwsh -ErrorAction SilentlyContinue).Source",
83
84
  "if (-not $ps) { $ps = (Get-Command powershell -ErrorAction SilentlyContinue).Source }",
84
- "if (-not $ps) { throw '找不到可用的 PowerShell' }",
85
+ tr("if (-not $ps) { throw '找不到可用的 PowerShell' }", "if (-not $ps) { throw 'No usable PowerShell found' }"),
85
86
  "Start-Process -FilePath $ps -ArgumentList $inner",
86
87
  "",
87
88
  ].join("\n");
@@ -90,7 +91,7 @@ export function buildWindowsUpdateLauncherScript() {
90
91
  export async function fetchReleaseText(name, maxBytes, timeoutMs = 30_000) {
91
92
  const url = new URL(`/releases/${name}`, releaseOrigin());
92
93
  if (url.protocol !== "https:")
93
- throw new Error(`拒绝非 https 发布源: ${url.href}`);
94
+ throw new Error(tr(`拒绝非 https 发布源: ${url.href}`, `Refusing non-https release source: ${url.href}`));
94
95
  const res = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
95
96
  if (!res.ok)
96
97
  throw new Error(`${name}: HTTP ${res.status}`);
@@ -125,7 +126,7 @@ export async function downloadInstallScript() {
125
126
  ]);
126
127
  const check = verifyInstallScript(script, sums);
127
128
  if (check === "mismatch")
128
- throw new Error("安装脚本与发布清单的 SHA256 不符,已拒绝执行");
129
+ throw new Error(tr("安装脚本与发布清单的 SHA256 不符,已拒绝执行", "Install script SHA256 doesn't match the release manifest; refusing to run it"));
129
130
  return { script, verified: check === "verified" };
130
131
  }
131
132
  /**
@@ -140,9 +141,9 @@ export async function downloadInstallScript() {
140
141
  async function portableSelfUpdate(latest) {
141
142
  const site = activeSite();
142
143
  if (process.platform === "win32") {
143
- console.log(`正在打开更新窗口(升级到 v${latest})…`);
144
- console.log("更新在新窗口里进行,本窗口会退出;装完后重新打开终端运行 u1s1 即可。");
145
- console.log(`若新窗口没有出现,可手动在 PowerShell 运行: ${installCommand(site, "ps1")}`);
144
+ console.log(tr(`正在打开更新窗口(升级到 v${latest})…`, `Opening the update window (updating to v${latest})…`));
145
+ console.log(tr("更新在新窗口里进行,本窗口会退出;装完后重新打开终端运行 u1s1 即可。", "The update runs in a new window and this one will exit. When it's done, open a new terminal and run u1s1."));
146
+ console.log(tr(`若新窗口没有出现,可手动在 PowerShell 运行: ${installCommand(site, "ps1")}`, `If no window appears, run this in PowerShell: ${installCommand(site, "ps1")}`));
146
147
  // 不走 `cmd /c start`:start 只把**带引号**的首参当窗口标题,而 node 对
147
148
  // 无空格参数不加引号,标题会被当成命令执行(0.13.1 翻车现场)。也不走
148
149
  // spawn detached:Windows 上 detached 是 DETACHED_PROCESS——子进程**没有**
@@ -156,25 +157,25 @@ async function portableSelfUpdate(latest) {
156
157
  writeFileSync(join(dir, "launch.ps1"), buildWindowsUpdateLauncherScript());
157
158
  const res = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(dir, "launch.ps1")], { stdio: "ignore", windowsHide: true, timeout: 30_000 });
158
159
  if (res.error || res.status !== 0) {
159
- console.error("无法拉起更新窗口。请手动在 PowerShell 里运行:");
160
+ console.error(tr("无法拉起更新窗口。请手动在 PowerShell 里运行:", "Couldn't open the update window. Run this in PowerShell:"));
160
161
  console.error(` ${installCommand(site, "ps1")}`);
161
162
  process.exit(1);
162
163
  }
163
164
  // 立刻退出释放 node.exe 的文件锁,让安装器能替换安装目录
164
165
  process.exit(0);
165
166
  }
166
- console.log(`正在下载安装脚本并升级到 v${latest}…`);
167
- console.log("(可能需要几分钟,期间请不要关闭窗口)");
167
+ console.log(tr(`正在下载安装脚本并升级到 v${latest}…`, `Downloading the install script and updating to v${latest}…`));
168
+ console.log(tr("(可能需要几分钟,期间请不要关闭窗口)", "(This may take a few minutes; please keep this window open)"));
168
169
  let script;
169
170
  try {
170
171
  const download = await downloadInstallScript();
171
172
  script = download.script;
172
173
  if (!download.verified) {
173
- console.log("⚠ 发布清单里没有安装脚本的校验和(旧版发布),跳过脚本校验;安装包本身仍会校验。");
174
+ console.log(tr("⚠ 发布清单里没有安装脚本的校验和(旧版发布),跳过脚本校验;安装包本身仍会校验。", "⚠ The release manifest has no checksum for the install script (older release); skipping that check. The package itself is still verified."));
174
175
  }
175
176
  }
176
177
  catch (e) {
177
- console.error(`安装脚本下载失败:${e.message}。请手动运行:`);
178
+ console.error(tr(`安装脚本下载失败:${e.message}。请手动运行:`, `Couldn't download the install script: ${e.message}. Run this manually:`));
178
179
  console.error(` ${installCommand(site, "sh")}`);
179
180
  process.exit(1);
180
181
  }
@@ -185,7 +186,7 @@ async function portableSelfUpdate(latest) {
185
186
  }
186
187
  catch {
187
188
  // 安装脚本已经打印了失败原因与重试提示(多半是到官网的链路慢),这里不再重复一整段
188
- console.error("\n自动升级没有完成,稍后重试 u1s1 update 即可;已安装版本不受影响。");
189
+ console.error(tr("\n自动升级没有完成,稍后重试 u1s1 update 即可;已安装版本不受影响。", "\nThe update didn't finish. Try u1s1 update again later; your installed version is unaffected."));
189
190
  process.exit(1);
190
191
  }
191
192
  // 脚本退出码 0 ≠ 升级成功:安装脚本认的是官网 LATEST 版本源,它若认为
@@ -200,27 +201,27 @@ async function portableSelfUpdate(latest) {
200
201
  // 老版布局没这文件,确认不了就不下结论
201
202
  }
202
203
  if (installedVer === latest) {
203
- console.log(`\n✅ 已升级到 v${latest},重新运行 u1s1 生效。`);
204
+ console.log(tr(`\n✅ 已升级到 v${latest},重新运行 u1s1 生效。`, `\n✅ Updated to v${latest}. Restart u1s1 to use it.`));
204
205
  }
205
206
  else {
206
- console.error(`\n⚠ 安装脚本跑完了,但没装上 v${latest}(官网版本源可能滞后,当前仍是 v${installedVer ?? VERSION})。`);
207
- console.error(" 请稍后重试 u1s1 update,或手动运行:");
207
+ console.error(tr(`\n⚠ 安装脚本跑完了,但没装上 v${latest}(官网版本源可能滞后,当前仍是 v${installedVer ?? VERSION})。`, `\n⚠ The install script finished but v${latest} wasn't installed (the release feed may lag; still on v${installedVer ?? VERSION}).`));
208
+ console.error(tr(" 请稍后重试 u1s1 update,或手动运行:", " Try u1s1 update again later, or run:"));
208
209
  console.error(` ${installCommand(site, "sh")}`);
209
210
  process.exit(1);
210
211
  }
211
212
  }
212
213
  export async function update() {
213
- console.log("正在检查更新…");
214
+ console.log(tr("正在检查更新…", "Checking for updates…"));
214
215
  const latest = await getLatestVersion();
215
216
  if (!latest) {
216
- console.error("无法获取最新版本。请检查网络连接后重试。");
217
+ console.error(tr("无法获取最新版本。请检查网络连接后重试。", "Couldn't get the latest version. Check your connection and try again."));
217
218
  process.exit(1);
218
219
  }
219
220
  if (compareVersions(latest, VERSION) <= 0) {
220
- console.log(`当前 v${VERSION} 已是最新版本 ✓`);
221
+ console.log(tr(`当前 v${VERSION} 已是最新版本 ✓`, `v${VERSION} is the latest version ✓`));
221
222
  return;
222
223
  }
223
- console.log(`发现新版本 v${latest} (当前 v${VERSION})`);
224
+ console.log(tr(`发现新版本 v${latest} (当前 v${VERSION})`, `New version v${latest} available (current v${VERSION})`));
224
225
  // 便携版:npm 装到全局目录,但 PATH 先命中便携目录,怎么更都还是旧版。
225
226
  // 正确方式是重跑官网安装脚本整包替换 —— 这里直接替用户跑,不再让人手抄命令。
226
227
  if (isPortableInstall()) {
@@ -233,31 +234,31 @@ export async function update() {
233
234
  const required = requiredNodeVersion();
234
235
  if (required && compareVersions(process.versions.node, required) < 0) {
235
236
  const cmd = process.platform === "win32"
236
- ? `${installCommand(activeSite(), "ps1")} (在 PowerShell 里运行)`
237
+ ? tr(`${installCommand(activeSite(), "ps1")} (在 PowerShell 里运行)`, `${installCommand(activeSite(), "ps1")} (run in PowerShell)`)
237
238
  : installCommand(activeSite(), "sh");
238
239
  console.log("");
239
- console.log(`⚠ 你电脑的 Node.js 是 v${process.versions.node},新版 u1s1 建议 v${required} 或更新。`);
240
- console.log(" 推荐用官网安装命令重装,自带合适的 Node,一步到位:");
240
+ console.log(tr(`⚠ 你电脑的 Node.js 是 v${process.versions.node},新版 u1s1 建议 v${required} 或更新。`, `⚠ Your Node.js is v${process.versions.node}; the new u1s1 recommends v${required} or later.`));
241
+ console.log(tr(" 推荐用官网安装命令重装,自带合适的 Node,一步到位:", " We recommend reinstalling with the official installer, which bundles a suitable Node:"));
241
242
  console.log(` ${cmd}`);
242
- console.log(" 下面仍会继续用 npm 更新;更新后如果运行异常,再用上面的命令重装即可。");
243
+ console.log(tr(" 下面仍会继续用 npm 更新;更新后如果运行异常,再用上面的命令重装即可。", " Continuing with the npm update anyway; if u1s1 misbehaves afterwards, reinstall with the command above."));
243
244
  console.log("");
244
245
  }
245
- console.log(`正在用 ${pm} 更新 ${PACKAGE_NAME}…`);
246
+ console.log(tr(`正在用 ${pm} 更新 ${PACKAGE_NAME}…`, `Updating ${PACKAGE_NAME} with ${pm}…`));
246
247
  try {
247
248
  // npm 压掉 EBADENGINE 等警告墙,对新手只有噪音;出错时 error 仍会显示
248
249
  const args = pm === "npm"
249
250
  ? ["install", "-g", "--loglevel=error", `${PACKAGE_NAME}@latest`]
250
251
  : ["add", "-g", `${PACKAGE_NAME}@latest`];
251
252
  execFileSync(pm, args, { stdio: "inherit" });
252
- console.log(`\n✅ 已更新到 v${latest},重启 u1s1 后生效。`);
253
+ console.log(tr(`\n✅ 已更新到 v${latest},重启 u1s1 后生效。`, `\n✅ Updated to v${latest}. Restart u1s1 to use it.`));
253
254
  }
254
255
  catch {
255
256
  // 再让用户手抄同一条命令多半还是失败(最常见是全局目录没写权限),
256
257
  // 直接给能跳出循环的路:官网安装脚本(装进用户目录,不需要管理员权限)
257
- console.error("\n更新失败(上面是包管理器的报错,常见原因是全局目录没有写权限)。");
258
- console.error("推荐改用官网安装脚本重装,不需要管理员权限,一步到位:");
258
+ console.error(tr("\n更新失败(上面是包管理器的报错,常见原因是全局目录没有写权限)。", "\nUpdate failed (see the package manager error above; usually no write access to the global directory)."));
259
+ console.error(tr("推荐改用官网安装脚本重装,不需要管理员权限,一步到位:", "We recommend reinstalling with the official installer instead; no admin rights needed:"));
259
260
  console.error(process.platform === "win32"
260
- ? ` ${installCommand(activeSite(), "ps1")} (在 PowerShell 里运行)`
261
+ ? tr(` ${installCommand(activeSite(), "ps1")} (在 PowerShell 里运行)`, ` ${installCommand(activeSite(), "ps1")} (run in PowerShell)`)
261
262
  : ` ${installCommand(activeSite(), "sh")}`);
262
263
  process.exit(1);
263
264
  }
package/dist/usage.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare function topupUrl(site?: Site): string;
11
11
  export declare function usageCtaLines(me?: Pick<MeResponse, "pay_enabled">, site?: Site): string[];
12
12
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
13
13
  export declare function usageReportLines(me: MeResponse, site?: Site): string[];
14
- /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
14
+ /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成提示文字而不是抛出。 */
15
15
  export declare function usageEntryData(): Promise<{
16
16
  lines?: string[];
17
17
  error?: string;
package/dist/usage.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { fetchMe } from "./api.js";
2
2
  import { activeSite, loadConfig } from "./config.js";
3
3
  import { hasDeviceCredential } from "./device-auth.js";
4
+ import { isEnglish, tr } from "./i18n.js";
4
5
  import { siteLinks, siteOf } from "./site.js";
5
6
  function bar(ratio, width = 24) {
6
7
  const filled = Math.round(Math.max(0, Math.min(1, ratio)) * width);
@@ -21,17 +22,35 @@ function fmtTokensCn(tokens) {
21
22
  return sig2(t / 1e4).toLocaleString("en-US") + " 万";
22
23
  return sig2(t).toLocaleString("en-US");
23
24
  }
24
- const PACKAGE_LABELS = {
25
- free_first: "首月免费包",
26
- free_yearly: "年度免费包",
27
- invite: "邀请赠送",
28
- new_user: "新用户赠送",
29
- login_checkin: "登录打卡",
30
- login_checkin_bonus: "打卡加成",
31
- payment_delay_gift: "临时加量包",
32
- topup_daily: "每日加量包",
33
- admin_grant: "官方赠送",
34
- };
25
+ // 英文界面用 K/M/B,同样 2 位有效数字
26
+ function fmtTokensEn(tokens) {
27
+ const t = Number(tokens);
28
+ if (!Number.isFinite(t) || t <= 0)
29
+ return "0";
30
+ const sig2 = (n) => Number(n.toPrecision(2));
31
+ if (t >= 1e9)
32
+ return `${sig2(t / 1e9)}B`;
33
+ if (t >= 1e6)
34
+ return `${sig2(t / 1e6)}M`;
35
+ if (t >= 1e3)
36
+ return `${sig2(t / 1e3)}K`;
37
+ return String(Math.round(t));
38
+ }
39
+ function fmtTokens(tokens) {
40
+ return isEnglish() ? fmtTokensEn(tokens) : fmtTokensCn(tokens);
41
+ }
42
+ // 用时求值:模块加载时还不知道站点/语言
43
+ const packageLabels = () => ({
44
+ free_first: tr("首月免费包", "First-month free pack"),
45
+ free_yearly: tr("年度免费包", "Yearly free pack"),
46
+ invite: tr("邀请赠送", "Invite reward"),
47
+ new_user: tr("新用户赠送", "New-user gift"),
48
+ login_checkin: tr("登录打卡", "Check-in"),
49
+ login_checkin_bonus: tr("打卡加成", "Check-in bonus"),
50
+ payment_delay_gift: tr("临时加量包", "Temporary pack"),
51
+ topup_daily: tr("每日加量包", "Daily pack"),
52
+ admin_grant: tr("官方赠送", "Granted by u1s1"),
53
+ });
35
54
  function sumOptional(a, b) {
36
55
  return a == null && b == null ? null : (a ?? 0) + (b ?? 0);
37
56
  }
@@ -53,25 +72,31 @@ function groupPackages(packages) {
53
72
  }
54
73
  function packageScopeNote(pkg) {
55
74
  if (pkg.kind === "login_checkin") {
56
- return "仅限 u1s1 客户端使用 · 全模型可用";
75
+ return tr("仅限 u1s1 客户端使用 · 全模型可用", "u1s1 clients only · all models");
57
76
  }
58
77
  if (pkg.scope === "free") {
59
78
  // 只有按日发放的免费包(首月/年度包的每日额度)才 0 点恢复;一次性免费包用完就没了。
60
- return pkg.daily_tokens != null ? "免费包适用模型 · 每日额度北京时间 0 点恢复" : "免费包适用模型";
79
+ return pkg.daily_tokens != null
80
+ ? tr("免费包适用模型 · 每日额度北京时间 0 点恢复", "Free-tier models · daily quota resets at 00:00 Beijing time (UTC+8)")
81
+ : tr("免费包适用模型", "Free-tier models");
61
82
  }
62
- return "仅限 u1s1 客户端使用 · 免费包适用模型";
83
+ return tr("仅限 u1s1 客户端使用 · 免费包适用模型", "u1s1 clients only · free-tier models");
63
84
  }
64
85
  function packageLabel(pkg) {
65
86
  const count = pkg.count > 1 ? ` ×${pkg.count}` : "";
66
- return `${PACKAGE_LABELS[pkg.kind] ?? pkg.kind}${count}`.padEnd(5, " ");
87
+ const label = `${packageLabels()[pkg.kind] ?? pkg.kind}${count}`;
88
+ // 中文靠全角空格对齐到第 13 列;英文标签长短不一,直接跟两个空格
89
+ return isEnglish() ? `${label} ` : label.padEnd(5, " ");
67
90
  }
68
91
  function packageLines(pkg) {
69
92
  const isDaily = pkg.daily_tokens != null;
70
93
  const total = isDaily ? (pkg.daily_tokens ?? 0) : (pkg.total_tokens ?? 0);
71
94
  const ratio = total > 0 ? pkg.remaining / total : 0;
72
- const expiry = pkg.expires_at ? `${pkg.expires_at.slice(0, 10)} 到期` : "永不过期";
95
+ const expiry = pkg.expires_at
96
+ ? tr(`${pkg.expires_at.slice(0, 10)} 到期`, `expires ${pkg.expires_at.slice(0, 10)}`)
97
+ : tr("永不过期", "never expires");
73
98
  return [
74
- ` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)}`,
99
+ tr(` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)}`, ` ${packageLabel(pkg)} ${fmtTokensEn(pkg.remaining)} / ${fmtTokensEn(total)}${isDaily ? "/day" : ""} left ${bar(ratio)}`),
75
100
  ` ${packageScopeNote(pkg)} · ${expiry}`,
76
101
  ];
77
102
  }
@@ -79,23 +104,23 @@ function packageUsageLines(me, tokensPerUsd, site) {
79
104
  const packages = me.packages ?? [];
80
105
  const lines = [];
81
106
  if (packages.length === 0)
82
- lines.push(" 用量包 (无生效中的用量包)");
107
+ lines.push(tr(" 用量包 (无生效中的用量包)", " Packs (no active usage packs)"));
83
108
  for (const pkg of groupPackages(packages))
84
109
  lines.push(...packageLines(pkg));
85
110
  if (me.bonus_balance_usd > 0) {
86
111
  const balance = tokensPerUsd > 0
87
- ? `${fmtTokensCn(me.bonus_balance_usd * tokensPerUsd)} Token`
112
+ ? `${fmtTokens(me.bonus_balance_usd * tokensPerUsd)} Token`
88
113
  : `$${me.bonus_balance_usd.toFixed(2)}`;
89
- lines.push(` 余额(按量) ${balance}`);
114
+ lines.push(tr(` 余额(按量) ${balance}`, ` Balance ${balance}`));
90
115
  }
91
116
  const monthlyUsage = tokensPerUsd > 0
92
- ? `约 ${fmtTokensCn(me.mtd_usd * tokensPerUsd)} Token($${me.mtd_usd.toFixed(2)})`
117
+ ? tr(`约 ${fmtTokensCn(me.mtd_usd * tokensPerUsd)} Token($${me.mtd_usd.toFixed(2)})`, `~${fmtTokensEn(me.mtd_usd * tokensPerUsd)} tokens ($${me.mtd_usd.toFixed(2)})`)
93
118
  : `$${me.mtd_usd.toFixed(2)}`;
94
- lines.push(` 本月已用 ${monthlyUsage}`);
119
+ lines.push(tr(` 本月已用 ${monthlyUsage}`, ` This month ${monthlyUsage}`));
95
120
  lines.push("");
96
- lines.push(" 免费用量包覆盖 DeepSeek Flash、GLM-5.3 Flash、Qwen3.8 Flash 和联网搜索。");
97
- lines.push(` 其他模型不走免费包,可用余额或全模型包 → ${siteLinks(site).dashboard}`);
98
- lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
121
+ lines.push(tr(" 免费用量包覆盖 DeepSeek Flash、GLM-5.3 Flash、Qwen3.8 Flash 和联网搜索。", " Free packs cover DeepSeek Flash, GLM-5.3 Flash, Qwen3.8 Flash and web search."));
122
+ lines.push(tr(` 其他模型不走免费包,可用余额或全模型包 → ${siteLinks(site).dashboard}`, ` Other models use your balance or an all-model pack → ${siteLinks(site).dashboard}`));
123
+ lines.push(tr(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。", " Token counts are estimates at the default model's price; pricier models use them up faster."));
99
124
  lines.push("");
100
125
  lines.push(...usageCtaLines(me, site));
101
126
  return lines;
@@ -106,24 +131,49 @@ function legacyUsageLines(me, tokensPerUsd, site) {
106
131
  const freeRatio = freeTotal > 0 ? freeRemain / freeTotal : 0;
107
132
  const lines = [];
108
133
  if (tokensPerUsd > 0) {
109
- const tokens = (usd) => `${fmtTokensCn(usd * tokensPerUsd)} Token`;
110
- lines.push(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tokensPerUsd)} / ${tokens(freeTotal)} ${bar(freeRatio)}`);
111
- lines.push(" DeepSeek Flash · 北京时间 0 点恢复");
112
- lines.push(` 永久余额 ${tokens(me.remaining_usd)}`);
113
- lines.push(` 本月已用 约 ${tokens(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`);
134
+ const tokens = (usd) => `${fmtTokens(usd * tokensPerUsd)} Token`;
135
+ lines.push(tr(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tokensPerUsd)} / ${tokens(freeTotal)} ${bar(freeRatio)}`, ` Free today ${fmtTokensEn(freeRemain * tokensPerUsd)} / ${tokens(freeTotal)} left ${bar(freeRatio)}`));
136
+ lines.push(tr(" DeepSeek Flash · 北京时间 0 点恢复", " DeepSeek Flash · resets at 00:00 Beijing time (UTC+8)"));
137
+ lines.push(tr(` 永久余额 ${tokens(me.remaining_usd)}`, ` Balance ${tokens(me.remaining_usd)}`));
138
+ lines.push(tr(` 本月已用 约 ${tokens(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`, ` This month ~${tokens(me.mtd_usd)} ($${me.mtd_usd.toFixed(2)})`));
114
139
  lines.push("");
115
- lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
140
+ lines.push(tr(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。", " Token counts are estimates at the default model's price; pricier models use them up faster."));
116
141
  }
117
142
  else {
118
- lines.push(` 今日免费 $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`);
119
- lines.push(" DeepSeek Flash · 北京时间 0 点恢复");
120
- lines.push(` 永久余额 $${me.remaining_usd.toFixed(2)}`);
121
- lines.push(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
143
+ lines.push(tr(` 今日免费 $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`, ` Free today $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`));
144
+ lines.push(tr(" DeepSeek Flash · 北京时间 0 点恢复", " DeepSeek Flash · resets at 00:00 Beijing time (UTC+8)"));
145
+ lines.push(tr(` 永久余额 $${me.remaining_usd.toFixed(2)}`, ` Balance $${me.remaining_usd.toFixed(2)}`));
146
+ lines.push(tr(` 本月成本 $${me.mtd_usd.toFixed(2)}`, ` This month $${me.mtd_usd.toFixed(2)}`));
122
147
  }
123
148
  lines.push("");
124
149
  lines.push(...usageCtaLines(me, site));
125
150
  return lines;
126
151
  }
152
+ /**
153
+ * 国际站额度报告:没有免费包/每日免费额度/打卡/邀请(网关 withSiteGifts 全部置零),
154
+ * 只给按量余额(USD)、本月用量和充值入口。万一有运营手工发的包,照列但不写
155
+ * 「免费包适用模型」之类中国站口径的说明。国际站网关都是新版,不走 legacy 分支。
156
+ */
157
+ function intlUsageLines(me, site) {
158
+ const lines = [];
159
+ for (const pkg of groupPackages(me.packages ?? [])) {
160
+ const isDaily = pkg.daily_tokens != null;
161
+ const total = isDaily ? (pkg.daily_tokens ?? 0) : (pkg.total_tokens ?? 0);
162
+ const ratio = total > 0 ? pkg.remaining / total : 0;
163
+ const expiry = pkg.expires_at
164
+ ? tr(`${pkg.expires_at.slice(0, 10)} 到期`, `expires ${pkg.expires_at.slice(0, 10)}`)
165
+ : tr("永不过期", "never expires");
166
+ lines.push(tr(` ${packageLabel(pkg)} 还剩 ${fmtTokensCn(pkg.remaining)} / ${fmtTokensCn(total)}${isDaily ? "/天" : ""} ${bar(ratio)} · ${expiry}`, ` ${packageLabel(pkg)} ${fmtTokensEn(pkg.remaining)} / ${fmtTokensEn(total)}${isDaily ? "/day" : ""} left ${bar(ratio)} · ${expiry}`));
167
+ }
168
+ lines.push(tr(` 余额 $${me.bonus_balance_usd.toFixed(2)}`, ` Balance $${me.bonus_balance_usd.toFixed(2)}`));
169
+ lines.push(tr(` 本月已用 $${me.mtd_usd.toFixed(2)}`, ` This month $${me.mtd_usd.toFixed(2)}`));
170
+ lines.push("");
171
+ const { dashboard, topup } = siteLinks(site);
172
+ lines.push(me.pay_enabled === false
173
+ ? tr(` 充值通道即将开放 → ${dashboard}`, ` Top-ups are coming soon → ${dashboard}`)
174
+ : tr(` 余额不够用?充值 → ${topup}`, ` Need more? Top up → ${topup}`));
175
+ return lines;
176
+ }
127
177
  /** 当前站点的充值深链(仪表盘充值卡片)。 */
128
178
  export function topupUrl(site = activeSite()) {
129
179
  return siteLinks(site).topup;
@@ -138,25 +188,28 @@ export function usageCtaLines(me, site = activeSite()) {
138
188
  const payClosed = me?.pay_enabled === false;
139
189
  const { dashboard, topup } = siteLinks(site);
140
190
  return [
141
- ` 每天到仪表盘打卡领免费包;邀请朋友双方都加量 → ${dashboard}`,
191
+ tr(` 每天到仪表盘打卡领免费包;邀请朋友双方都加量 → ${dashboard}`, ` Check in daily on the dashboard for free packs; invite friends and you both get more → ${dashboard}`),
142
192
  payClosed
143
- ? ` 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → ${dashboard}`
144
- : ` 额度不够用?充值 → ${topup}`,
193
+ ? tr(` 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → ${dashboard}`, ` Top-ups are coming soon; meanwhile grab extra packs and check in on the dashboard → ${dashboard}`)
194
+ : tr(` 额度不够用?充值 → ${topup}`, ` Need more? Top up → ${topup}`),
145
195
  ];
146
196
  }
147
197
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
148
198
  export function usageReportLines(me, site = activeSite()) {
149
199
  const tokensPerUsd = me.tokens_per_usd ?? 0;
200
+ const body = site.id === "intl"
201
+ ? intlUsageLines(me, site)
202
+ : me.packages ? packageUsageLines(me, tokensPerUsd, site) : legacyUsageLines(me, tokensPerUsd, site);
150
203
  return [
151
- ` 账号 ${me.email ?? "(未绑定邮箱)"}`,
152
- ...(me.packages ? packageUsageLines(me, tokensPerUsd, site) : legacyUsageLines(me, tokensPerUsd, site)),
204
+ tr(` 账号 ${me.email ?? "(未绑定邮箱)"}`, ` Account ${me.email ?? "(no email linked)"}`),
205
+ ...body,
153
206
  ];
154
207
  }
155
- /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
208
+ /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成提示文字而不是抛出。 */
156
209
  export async function usageEntryData() {
157
210
  const cfg = loadConfig();
158
211
  if (!hasDeviceCredential(cfg))
159
- return { error: "还没登录,先在终端运行 u1s1 login" };
212
+ return { error: tr("还没登录,先在终端运行 u1s1 login", "Not signed in. Run u1s1 login in your terminal first.") };
160
213
  try {
161
214
  return { lines: usageReportLines(await fetchMe(cfg), siteOf(cfg)) };
162
215
  }
@@ -167,7 +220,7 @@ export async function usageEntryData() {
167
220
  export async function usage() {
168
221
  const cfg = loadConfig();
169
222
  if (!hasDeviceCredential(cfg)) {
170
- console.error("还没登录,先跑 u1s1 login");
223
+ console.error(tr("还没登录,先跑 u1s1 login", "Not signed in. Run u1s1 login first."));
171
224
  process.exit(1);
172
225
  }
173
226
  const me = await fetchMe(cfg).catch((e) => {
package/dist/web.js CHANGED
@@ -7,6 +7,7 @@ import { ensureSearchTools } from "./search-tools.js";
7
7
  import { ensureUsableShell } from "./shell-doctor.js";
8
8
  import { fetchModels, loadCustomEndpoints } from "./api.js";
9
9
  import { ensureSigningProxy } from "./device-auth.js";
10
+ import { tr } from "./i18n.js";
10
11
  let webModelsDir;
11
12
  let webOfficialCfg;
12
13
  function processModelsPath() {
@@ -67,7 +68,7 @@ export async function prepareWebEnv(cfg) {
67
68
  clientAttestationExpiresInSeconds = attestationTtl;
68
69
  }
69
70
  catch (e) {
70
- console.error(" 获取模型列表失败,使用内置列表:", e.message);
71
+ console.error(tr(" 获取模型列表失败,使用内置列表:", " Couldn't fetch the model list; using the built-in list:"), e.message);
71
72
  }
72
73
  await endpointsReady;
73
74
  ensureDefaultSettings(MODELS);
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
3
3
  import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import vm from "node:vm";
5
5
  import { dirname, resolve } from "node:path";
6
+ import { tr } from "../i18n.js";
6
7
  import { runPool, runSubagent, SUBAGENT_CONCURRENCY } from "../subagent.js";
7
8
  // ---- WorkflowRunner:执行模型生成的编排脚本(路线 A:主进程 vm 上下文) ----
8
9
  // 定位「防误不防恶」:vm 非硬安全边界,靠白名单注入 + 静态校验 + 脚本落盘可审查兜底。
@@ -70,30 +71,30 @@ export function validateScript(code) {
70
71
  new vm.Script(`(async () => {\n${code}\n})()`, { filename: "workflow.mjs" });
71
72
  }
72
73
  catch (e) {
73
- errors.push(`语法错误:${e.message}`);
74
+ errors.push(tr(`语法错误:${e.message}`, `Syntax error: ${e.message}`));
74
75
  return errors;
75
76
  }
76
77
  const bare = stripCommentsAndStrings(code);
77
78
  const forbidden = [
78
79
  [/\brequire\s*\(/, "require()"],
79
- [/\bimport\s*\(/, "动态 import()"],
80
- [/^\s*import\s/m, "静态 import"],
81
- [/\bprocess\b/, "process 对象"],
80
+ [/\bimport\s*\(/, tr("动态 import()", "dynamic import()")],
81
+ [/^\s*import\s/m, tr("静态 import", "static import")],
82
+ [/\bprocess\b/, tr("process 对象", "the process object")],
82
83
  [/\bglobalThis\b/, "globalThis"],
83
84
  [/\bfetch\s*\(/, "fetch()"],
84
85
  [/\beval\s*\(/, "eval()"],
85
86
  [/\bFunction\s*\(/, "new Function()"],
86
- [/\.constructor\b/, ".constructor(原型链逃逸)"],
87
- [/\bprototype\b/, "prototype(原型链逃逸)"],
87
+ [/\.constructor\b/, tr(".constructor(原型链逃逸)", ".constructor (prototype-chain escape)")],
88
+ [/\bprototype\b/, tr("prototype(原型链逃逸)", "prototype (prototype-chain escape)")],
88
89
  [/\b__proto__\b/, "__proto__"],
89
90
  [/\bWebAssembly\b/, "WebAssembly"],
90
- [/node:/, "node: 内置模块"],
91
+ [/node:/, tr("node: 内置模块", "node: built-in modules")],
91
92
  [/child_process/, "child_process"],
92
- [/\bfs\b\s*\./, "fs 模块"],
93
+ [/\bfs\b\s*\./, tr("fs 模块", "the fs module")],
93
94
  ];
94
95
  for (const [re, label] of forbidden) {
95
96
  if (re.test(bare))
96
- errors.push(`脚本使用了沙箱之外的能力:${label}`);
97
+ errors.push(tr(`脚本使用了沙箱之外的能力:${label}`, `The script uses something outside the sandbox: ${label}`));
97
98
  }
98
99
  return errors;
99
100
  }
@@ -109,7 +110,8 @@ export async function runWorkflow(opts) {
109
110
  let timedOut = false;
110
111
  const overallTimer = setTimeout(() => {
111
112
  timedOut = true;
112
- internal.abort(new Error(`整个工作流超时(${Math.round((opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS) / 60_000)} 分钟)`));
113
+ const minutes = Math.round((opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS) / 60_000);
114
+ internal.abort(new Error(tr(`整个工作流超时(${minutes} 分钟)`, `The workflow timed out (${minutes} minutes)`)));
113
115
  }, opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS);
114
116
  const store = new ProgressStore(opts.progressPath);
115
117
  const budgetTokens = opts.budgetTokens ?? WORKFLOW_DEFAULT_BUDGET_TOKENS;
@@ -146,12 +148,12 @@ export async function runWorkflow(opts) {
146
148
  const o = typeof input === "string" ? { task: input } : (input ?? {});
147
149
  const task = typeof o.task === "string" ? o.task.trim() : "";
148
150
  if (!task)
149
- throw new Error("subagent() 需要 task 字符串");
151
+ throw new Error(tr("subagent() 需要 task 字符串", "subagent() needs a task string"));
150
152
  const model = typeof o.model === "string" && o.model ? o.model : undefined;
151
153
  if (internal.signal.aborted)
152
- throw new Error("工作流已中止,不再派发新子任务");
154
+ throw new Error(tr("工作流已中止,不再派发新子任务", "The workflow was aborted; no new subtasks will be started"));
153
155
  if (budgetTokens > 0 && spentTokens >= budgetTokens) {
154
- throw new Error(`token 预算已用尽(已用 ${spentTokens.toLocaleString()} / 上限 ${budgetTokens.toLocaleString()}),不再派发新子任务`);
156
+ throw new Error(tr(`token 预算已用尽(已用 ${spentTokens.toLocaleString()} / 上限 ${budgetTokens.toLocaleString()}),不再派发新子任务`, `Token budget exhausted (used ${spentTokens.toLocaleString()} / limit ${budgetTokens.toLocaleString()}); no new subtasks will be started`));
155
157
  }
156
158
  const key = taskKey(task, model);
157
159
  if (opts.resume) {
@@ -194,7 +196,7 @@ export async function runWorkflow(opts) {
194
196
  catch (error) {
195
197
  stats.failed++;
196
198
  progress.failed = stats.failed;
197
- const msg = error.message || "未知错误";
199
+ const msg = error.message || tr("未知错误", "Unknown error");
198
200
  store.append({ key, task: task.slice(0, 120), ok: false, text: msg, ms: Date.now() - t0 });
199
201
  throw new Error(msg, { cause: error });
200
202
  }
@@ -209,20 +211,20 @@ export async function runWorkflow(opts) {
209
211
  /** 注入沙箱的 judge():廉价无工具模型调用,严格回答 PASS/FAIL,转成布尔。 */
210
212
  async function judgeImpl(question) {
211
213
  if (typeof question !== "string" || !question.trim())
212
- throw new Error("judge() 需要问题字符串");
214
+ throw new Error(tr("judge() 需要问题字符串", "judge() needs a question string"));
213
215
  const answer = await subagentImpl({
214
- task: `回答以下判定问题。只允许回答一个词:PASS 或 FAIL,不要任何其他内容。\n\n判定问题:${question.trim()}`,
216
+ task: tr(`回答以下判定问题。只允许回答一个词:PASS 或 FAIL,不要任何其他内容。\n\n判定问题:${question.trim()}`, `Answer the following yes/no check. Reply with exactly one word, PASS or FAIL, and nothing else.\n\nCheck: ${question.trim()}`),
215
217
  noTools: true,
216
218
  });
217
219
  const pass = /\bPASS\b/i.test(answer) && !/\bFAIL\b/i.test(answer);
218
220
  if (!pass && !/\bFAIL\b/i.test(answer))
219
- logs.push(`⚠ judge 回答无法解析,按 FAIL 处理:${answer.slice(0, 120)}`);
221
+ logs.push(tr(`⚠ judge 回答无法解析,按 FAIL 处理:${answer.slice(0, 120)}`, `⚠ Couldn't parse the judge answer; treating it as FAIL: ${answer.slice(0, 120)}`));
220
222
  return pass;
221
223
  }
222
224
  /** barrier 扇出:全部并发(封顶排队),单个失败该项为 null,不拖垮整批。 */
223
225
  async function parallel(thunks) {
224
226
  if (!Array.isArray(thunks))
225
- throw new Error("parallel() 需要一个 thunk 数组");
227
+ throw new Error(tr("parallel() 需要一个 thunk 数组", "parallel() needs an array of thunks"));
226
228
  const results = [];
227
229
  await runPool({
228
230
  items: thunks,
@@ -231,7 +233,7 @@ export async function runWorkflow(opts) {
231
233
  results[i] = await Promise.resolve()
232
234
  .then(thunk)
233
235
  .catch((e) => {
234
- logs.push(`✗ 任务 ${i + 1} 失败:${e.message}`);
236
+ logs.push(tr(`✗ 任务 ${i + 1} 失败:${e.message}`, `✗ Task ${i + 1} failed: ${e.message}`));
235
237
  return null;
236
238
  });
237
239
  },
@@ -242,9 +244,9 @@ export async function runWorkflow(opts) {
242
244
  /** 无屏障流水线:每个 item 独立穿过所有 stage,item 失败只废自己。 */
243
245
  async function pipeline(items, stages) {
244
246
  if (!Array.isArray(items))
245
- throw new Error("pipeline() 第一个参数需要数组");
247
+ throw new Error(tr("pipeline() 第一个参数需要数组", "pipeline() needs an array as its first argument"));
246
248
  if (!Array.isArray(stages) || stages.length === 0)
247
- throw new Error("pipeline() 第二个参数需要非空 stage 数组");
249
+ throw new Error(tr("pipeline() 第二个参数需要非空 stage 数组", "pipeline() needs a non-empty array of stages as its second argument"));
248
250
  const results = [];
249
251
  await runPool({
250
252
  items: items,
@@ -256,7 +258,7 @@ export async function runWorkflow(opts) {
256
258
  current = await stages[s](current);
257
259
  }
258
260
  catch (e) {
259
- logs.push(`✗ item ${i + 1} 在第 ${s + 1} 阶段失败:${e.message}`);
261
+ logs.push(tr(`✗ item ${i + 1} 在第 ${s + 1} 阶段失败:${e.message}`, `✗ Item ${i + 1} failed at stage ${s + 1}: ${e.message}`));
260
262
  current = null;
261
263
  break;
262
264
  }
@@ -307,7 +309,8 @@ export async function runWorkflow(opts) {
307
309
  const aborted = internal.signal.aborted;
308
310
  const budgetExhausted = budgetTokens > 0 && spentTokens >= budgetTokens;
309
311
  const details = { ...stats, seconds, aborted, spentTokens, budgetExhausted };
310
- const lines = [`工作流结束,用时 ${seconds}s · 子任务 ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · 续跑命中 ${stats.cached}` : ""} · 用 ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / 预算 ${budgetTokens.toLocaleString()}` : " (不限)"}${timedOut ? " · ⏱ 整体超时" : ""}${aborted && !timedOut ? " · 已中止" : ""}`, ""];
312
+ const header = tr(`工作流结束,用时 ${seconds}s · 子任务 ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · 续跑命中 ${stats.cached}` : ""} · 用 ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / 预算 ${budgetTokens.toLocaleString()}` : " (不限)"}${timedOut ? " · ⏱ 整体超时" : ""}${aborted && !timedOut ? " · 已中止" : ""}`, `Workflow finished in ${seconds}s · subtasks ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · resumed from cache ${stats.cached}` : ""} · used ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / budget ${budgetTokens.toLocaleString()}` : " (no limit)"}${timedOut ? " · ⏱ timed out" : ""}${aborted && !timedOut ? " · aborted" : ""}`);
313
+ const lines = [header, ""];
311
314
  if (completionValue !== undefined) {
312
315
  let shown;
313
316
  try {
@@ -316,17 +319,18 @@ export async function runWorkflow(opts) {
316
319
  catch {
317
320
  shown = String(completionValue);
318
321
  }
319
- lines.push("**返回值**", "", shown.slice(0, 3000), "");
322
+ lines.push(tr("**返回值**", "**Return value**"), "", shown.slice(0, 3000), "");
320
323
  }
321
324
  if (scriptError)
322
- lines.push("**脚本报错**", "", "```", scriptError, "```", "");
325
+ lines.push(tr("**脚本报错**", "**Script error**"), "", "```", scriptError, "```", "");
323
326
  if (logs.length > 0) {
324
- lines.push("**运行日志**", "", ...logs.slice(0, MAX_LOG_LINES).map((l) => `- ${l.slice(0, 300)}`));
325
- if (logs.length > MAX_LOG_LINES)
326
- lines.push(`- …(另有 ${logs.length - MAX_LOG_LINES} 条省略)`);
327
+ lines.push(tr("**运行日志**", "**Run log**"), "", ...logs.slice(0, MAX_LOG_LINES).map((l) => `- ${l.slice(0, 300)}`));
328
+ if (logs.length > MAX_LOG_LINES) {
329
+ lines.push(tr(`- …(另有 ${logs.length - MAX_LOG_LINES} 条省略)`, `- … (${logs.length - MAX_LOG_LINES} more omitted)`));
330
+ }
327
331
  lines.push("");
328
332
  }
329
- lines.push(`进度存档:${opts.progressPath}(失败重跑可加 resume=true 续跑)`);
333
+ lines.push(tr(`进度存档:${opts.progressPath}(失败重跑可加 resume=true 续跑)`, `Progress saved to: ${opts.progressPath} (to rerun after failures, add resume=true to continue)`));
330
334
  return { report: lines.join("\n").trim(), details };
331
335
  }
332
336
  /** 工作区目录:项目根 .u1s1/workflows/(脚本 + 进度存档都在这里)。 */