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.
- package/dist/agent-setup.d.ts +19 -3
- package/dist/agent-setup.js +85 -10
- package/dist/api.js +42 -34
- package/dist/bench-report.js +12 -9
- package/dist/bench.js +34 -31
- package/dist/brand.d.ts +2 -1
- package/dist/brand.js +38 -12
- package/dist/config.d.ts +11 -1
- package/dist/config.js +25 -8
- package/dist/context-usage.js +12 -9
- package/dist/deploy.js +49 -45
- package/dist/device-auth.js +7 -5
- package/dist/error-humanize.d.ts +1 -0
- package/dist/error-humanize.js +18 -6
- package/dist/feedback.js +48 -31
- package/dist/i18n.d.ts +21 -0
- package/dist/i18n.js +46 -0
- package/dist/import/index.js +64 -28
- package/dist/import/skills.js +72 -41
- package/dist/import/util.js +3 -2
- package/dist/import/write.js +2 -1
- package/dist/index.js +140 -49
- package/dist/login.js +39 -28
- package/dist/loop.js +18 -12
- package/dist/mcp/client.js +15 -14
- package/dist/mcp/command.d.ts +2 -2
- package/dist/mcp/command.js +109 -69
- package/dist/mcp/config.d.ts +22 -6
- package/dist/mcp/config.js +0 -0
- package/dist/mcp/extension.js +5 -3
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.js +10 -5
- package/dist/model.js +11 -10
- package/dist/nudges.d.ts +8 -2
- package/dist/nudges.js +22 -3
- package/dist/search-tools.js +7 -6
- package/dist/shell-doctor.js +9 -8
- package/dist/subagent.js +5 -4
- package/dist/tools.js +53 -37
- package/dist/update.js +29 -28
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +96 -43
- package/dist/web.js +2 -1
- package/dist/workflow/runner.js +33 -29
- package/dist/workflow/templates.js +70 -45
- package/dist/workflow/tool.js +15 -12
- package/package.json +4 -4
- package/scripts/patch-pi.js +347 -278
- 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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
? `${
|
|
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) => `${
|
|
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
|
-
...
|
|
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);
|
package/dist/workflow/runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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/(脚本 + 进度存档都在这里)。 */
|