u1s1-cli 1.11.8 → 1.13.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 (49) hide show
  1. package/dist/agent-setup.d.ts +19 -3
  2. package/dist/agent-setup.js +85 -10
  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 +140 -49
  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.d.ts +2 -2
  27. package/dist/mcp/command.js +109 -69
  28. package/dist/mcp/config.d.ts +22 -6
  29. package/dist/mcp/config.js +0 -0
  30. package/dist/mcp/extension.js +5 -3
  31. package/dist/mcp/tools.d.ts +2 -0
  32. package/dist/mcp/tools.js +10 -5
  33. package/dist/model.js +11 -10
  34. package/dist/nudges.d.ts +8 -2
  35. package/dist/nudges.js +22 -3
  36. package/dist/search-tools.js +7 -6
  37. package/dist/shell-doctor.js +9 -8
  38. package/dist/subagent.js +5 -4
  39. package/dist/tools.js +53 -37
  40. package/dist/update.js +29 -28
  41. package/dist/usage.d.ts +1 -1
  42. package/dist/usage.js +96 -43
  43. package/dist/web.js +2 -1
  44. package/dist/workflow/runner.js +33 -29
  45. package/dist/workflow/templates.js +70 -45
  46. package/dist/workflow/tool.js +15 -12
  47. package/package.json +4 -4
  48. package/scripts/patch-pi.js +347 -278
  49. package/scripts/render-regression.mjs +55 -4
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/(脚本 + 进度存档都在这里)。 */