u1s1-cli 1.11.7 → 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.
- package/README.md +5 -5
- package/dist/agent-setup.d.ts +7 -1
- package/dist/agent-setup.js +34 -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 +7 -2
- package/dist/brand.js +44 -13
- package/dist/config.d.ts +28 -2
- package/dist/config.js +52 -14
- package/dist/context-usage.js +12 -9
- package/dist/deploy.js +53 -47
- package/dist/device-auth.js +7 -5
- package/dist/embed.d.ts +2 -1
- package/dist/embed.js +2 -1
- package/dist/error-humanize.d.ts +3 -1
- package/dist/error-humanize.js +24 -9
- package/dist/feedback.d.ts +4 -2
- package/dist/feedback.js +60 -39
- 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 +153 -54
- package/dist/login.d.ts +13 -2
- package/dist/login.js +84 -27
- package/dist/loop.js +18 -12
- package/dist/mcp/client.js +15 -14
- package/dist/mcp/command.js +76 -48
- package/dist/mcp/extension.js +5 -3
- package/dist/mcp/tools.js +6 -5
- package/dist/model.js +12 -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/site.d.ts +71 -0
- package/dist/site.js +123 -0
- package/dist/subagent.js +5 -4
- package/dist/tools.js +53 -37
- package/dist/update.js +47 -39
- package/dist/usage.d.ts +6 -4
- package/dist/usage.js +111 -53
- 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 +3 -3
- package/scripts/patch-pi.js +243 -278
- package/scripts/render-regression.mjs +55 -4
package/dist/login.js
CHANGED
|
@@ -2,8 +2,10 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { hostname, platform } from "node:os";
|
|
3
3
|
import { fetchMe, readJsonResponseCapped } from "./api.js";
|
|
4
4
|
import { printConsoleBanner } from "./brand.js";
|
|
5
|
-
import { loadConfig, saveConfig } from "./config.js";
|
|
5
|
+
import { loadConfig, loadFileConfig, saveConfig } from "./config.js";
|
|
6
6
|
import { generateDeviceKeyPair, hasDeviceCredential } from "./device-auth.js";
|
|
7
|
+
import { isEnglish, tr } from "./i18n.js";
|
|
8
|
+
import { isSiteUrl, parseSiteArg, siteLinks, siteOf, SITES } from "./site.js";
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
8
10
|
const VERSION = require("../package.json").version;
|
|
9
11
|
const MAX_DEVICE_AUTH_RESPONSE_BYTES = 64 * 1024;
|
|
@@ -32,7 +34,7 @@ function tryOpenBrowser(url) {
|
|
|
32
34
|
.catch(() => { });
|
|
33
35
|
}
|
|
34
36
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35
|
-
/** baseUrl 形如 https://api
|
|
37
|
+
/** baseUrl 形如 https://api.<站点主域>/v1;auth 路由挂在同一域名的根路径。 */
|
|
36
38
|
export function apiOrigin(cfg) {
|
|
37
39
|
return cfg.baseUrl.replace(/\/v1\/?$/, "");
|
|
38
40
|
}
|
|
@@ -140,44 +142,92 @@ export async function pollDeviceLogin(origin, start, outcome) {
|
|
|
140
142
|
outcome.reason = "timeout";
|
|
141
143
|
return null;
|
|
142
144
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
/** `u1s1 login [--site cn|global] [key]`;--site 也接受 --site=<值>,global 的别名 intl。 */
|
|
146
|
+
export function parseLoginArgs(rest) {
|
|
147
|
+
const out = {};
|
|
148
|
+
for (let i = 0; i < rest.length; i++) {
|
|
149
|
+
const arg = rest[i];
|
|
150
|
+
if (arg === "--site" || arg.startsWith("--site=")) {
|
|
151
|
+
const raw = arg === "--site" ? rest[++i] : arg.slice("--site=".length);
|
|
152
|
+
const site = parseSiteArg(raw);
|
|
153
|
+
if (!site) {
|
|
154
|
+
return { error: tr(`--site 只接受 cn 或 global(收到:${raw ?? "空"})`, `--site accepts cn or global (got: ${raw ?? "nothing"})`) };
|
|
155
|
+
}
|
|
156
|
+
out.site = site;
|
|
157
|
+
}
|
|
158
|
+
else if (out.key === undefined) {
|
|
159
|
+
out.key = arg;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `--site` 切站:把配置里的 baseUrl 换成该站 apiBase 并落盘,再走该站的设备登录。
|
|
166
|
+
* 两站账号不通,换到另一个站时本机旧凭证已无用,一并清掉(不是同一站就不动)。
|
|
167
|
+
* U1S1_BASE_URL 仍优先:设了它本次照旧连它,--site 只写进配置文件。
|
|
168
|
+
*/
|
|
169
|
+
function applySiteChoice(cfg, site) {
|
|
170
|
+
const target = SITES[site];
|
|
171
|
+
const envBase = process.env["U1S1_BASE_URL"];
|
|
172
|
+
// 原来就在这个站(含历史主域 api.u1s1.io)才保留凭证;另一站或自定义网关的凭证在目标站无效
|
|
173
|
+
const switching = !isSiteUrl(envBase ? loadFileConfig().baseUrl : cfg.baseUrl, site);
|
|
174
|
+
const cleared = switching
|
|
175
|
+
? { apiKey: undefined, deviceToken: undefined, deviceId: undefined, devicePrivateJwk: undefined, devicePublicJwk: undefined, email: undefined }
|
|
176
|
+
: {};
|
|
177
|
+
if (envBase) {
|
|
178
|
+
saveConfig({ ...cfg, ...cleared, baseUrl: target.apiBase });
|
|
179
|
+
// 环境变量优先,界面语言仍跟 U1S1_BASE_URL 所在站
|
|
180
|
+
console.log(tr(` 已把 ${target.origin} 写入配置;但环境变量 U1S1_BASE_URL 优先,本次仍连 ${envBase}。`, ` Saved ${target.origin} to your config, but U1S1_BASE_URL takes precedence; still connecting to ${envBase} this time.`));
|
|
181
|
+
return { ...cfg, ...cleared };
|
|
182
|
+
}
|
|
183
|
+
const next = { ...cfg, ...cleared, baseUrl: target.apiBase };
|
|
184
|
+
// saveConfig 之后界面语言已换成目标站的(resetLang),下面起按新站语言说话
|
|
185
|
+
saveConfig(next);
|
|
186
|
+
if (switching) {
|
|
187
|
+
console.log(tr(` 已切换到 ${target.origin}(两站账号不通,需要在该站重新登录)。`, ` Switched to ${target.origin}. Accounts aren't shared between sites, so please sign in here.`));
|
|
188
|
+
}
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
export async function login(keyArg, opts = {}) {
|
|
192
|
+
let cfg = loadConfig();
|
|
145
193
|
let credential;
|
|
194
|
+
if (opts.site)
|
|
195
|
+
cfg = applySiteChoice(cfg, opts.site);
|
|
146
196
|
if (keyArg?.trim()) {
|
|
147
|
-
console.log(" API Key 不能证明请求来自 u1s1 客户端,将改用浏览器批准本机设备。");
|
|
197
|
+
console.log(tr(" API Key 不能证明请求来自 u1s1 客户端,将改用浏览器批准本机设备。", " An API key can't prove requests come from the u1s1 client; approving this device in the browser instead."));
|
|
148
198
|
}
|
|
149
199
|
printConsoleBanner(VERSION);
|
|
150
200
|
const origin = apiOrigin(cfg);
|
|
151
201
|
const failure = {};
|
|
152
202
|
// 慢网下这一步最长 15 秒,横幅之后一片安静像是卡死;先说一声在连
|
|
153
|
-
console.log(" 正在连接 u1s1 服务器…");
|
|
203
|
+
console.log(tr(" 正在连接 u1s1 服务器…", " Connecting to the u1s1 server…"));
|
|
154
204
|
const start = await startDeviceLogin(origin, failure);
|
|
155
205
|
if (!start) {
|
|
156
206
|
if (failure.message) {
|
|
157
207
|
// 服务器有明确说法(限流/风控等),原样转达,别伪装成网络问题
|
|
158
|
-
console.error(` 服务器没有接受登录请求:${failure.message}`);
|
|
208
|
+
console.error(tr(` 服务器没有接受登录请求:${failure.message}`, ` The server rejected the sign-in request: ${failure.message}`));
|
|
159
209
|
if (failure.status === 429)
|
|
160
|
-
console.error(" 稍等几分钟再运行 u1s1 login 重试即可。");
|
|
210
|
+
console.error(tr(" 稍等几分钟再运行 u1s1 login 重试即可。", " Wait a few minutes, then run u1s1 login again."));
|
|
161
211
|
}
|
|
162
212
|
else {
|
|
163
|
-
console.error(" 连不上 u1s1 服务器,请检查网络后重试。");
|
|
164
|
-
console.error(" 如果网络正常,可能是 u1s1 版本太旧,运行 u1s1 update 升级后再试。");
|
|
213
|
+
console.error(tr(" 连不上 u1s1 服务器,请检查网络后重试。", " Can't reach the u1s1 server. Check your network and try again."));
|
|
214
|
+
console.error(tr(" 如果网络正常,可能是 u1s1 版本太旧,运行 u1s1 update 升级后再试。", " If your network is fine, your u1s1 may be too old; run u1s1 update and try again."));
|
|
165
215
|
}
|
|
166
216
|
process.exit(1);
|
|
167
217
|
}
|
|
168
|
-
console.log(" 用浏览器登录并批准这台设备:");
|
|
218
|
+
console.log(tr(" 用浏览器登录并批准这台设备:", " Sign in with your browser and approve this device:"));
|
|
169
219
|
console.log("");
|
|
170
220
|
console.log(` ${start.verify_url}`);
|
|
171
221
|
console.log("");
|
|
172
|
-
console.log(" 正在尝试打开浏览器;如果没有自动打开,把上面这行网址复制到浏览器打开。");
|
|
173
|
-
console.log(" 批准后,本机会用设备私钥为每次 API 请求签名。");
|
|
222
|
+
console.log(tr(" 正在尝试打开浏览器;如果没有自动打开,把上面这行网址复制到浏览器打开。", " Opening your browser; if it doesn't open, copy the URL above into your browser."));
|
|
223
|
+
console.log(tr(" 批准后,本机会用设备私钥为每次 API 请求签名。", " Once approved, this machine signs every API request with its device key."));
|
|
174
224
|
tryOpenBrowser(start.verify_url);
|
|
175
225
|
console.log("");
|
|
176
|
-
console.log(" 等待浏览器里完成批准……(按 Ctrl+C 取消)");
|
|
226
|
+
console.log(tr(" 等待浏览器里完成批准……(按 Ctrl+C 取消)", " Waiting for approval in the browser… (Ctrl+C to cancel)"));
|
|
177
227
|
const waitStartedAt = Date.now();
|
|
178
228
|
const waitTimer = setInterval(() => {
|
|
179
229
|
const mins = Math.round((Date.now() - waitStartedAt) / 60_000);
|
|
180
|
-
console.log(` 仍在等待批准(已等 ${mins} 分钟)
|
|
230
|
+
console.log(tr(` 仍在等待批准(已等 ${mins} 分钟),在浏览器里点「批准」后这里会自动继续。`, ` Still waiting for approval (${mins} min so far). Click "Approve" in the browser and this will continue automatically.`));
|
|
181
231
|
}, 120_000);
|
|
182
232
|
const outcome = {};
|
|
183
233
|
try {
|
|
@@ -188,11 +238,11 @@ export async function login(keyArg) {
|
|
|
188
238
|
}
|
|
189
239
|
if (!credential) {
|
|
190
240
|
if (outcome.reason === "expired" || outcome.reason === "timeout") {
|
|
191
|
-
console.error(" 一直没等到浏览器里的批准,这个登录链接已失效。");
|
|
192
|
-
console.error(" 重新运行 u1s1 会生成新的登录链接;网页打不开的话检查一下网络。");
|
|
241
|
+
console.error(tr(" 一直没等到浏览器里的批准,这个登录链接已失效。", " No approval arrived from the browser and this sign-in link has expired."));
|
|
242
|
+
console.error(tr(" 重新运行 u1s1 会生成新的登录链接;网页打不开的话检查一下网络。", " Run u1s1 again for a new sign-in link; if the page won't load, check your network."));
|
|
193
243
|
}
|
|
194
244
|
else {
|
|
195
|
-
console.error(" 没能取得设备凭证,请确认网络正常后重试。");
|
|
245
|
+
console.error(tr(" 没能取得设备凭证,请确认网络正常后重试。", " Couldn't get a device credential. Check your network and try again."));
|
|
196
246
|
}
|
|
197
247
|
process.exit(1);
|
|
198
248
|
}
|
|
@@ -200,34 +250,41 @@ export async function login(keyArg) {
|
|
|
200
250
|
// 浏览器批准已经成功,先把凭证落盘;后面的账号信息确认失败也不用重走登录
|
|
201
251
|
saveConfig(next);
|
|
202
252
|
const me = await fetchMe(next).catch((e) => {
|
|
203
|
-
console.log(" ✓ 设备已批准,登录凭证已保存。");
|
|
204
|
-
console.error(` 只是暂时没连上服务器确认账号信息(${e.message})
|
|
205
|
-
console.error(" 稍后直接运行 u1s1 即可,不需要重新登录。");
|
|
253
|
+
console.log(tr(" ✓ 设备已批准,登录凭证已保存。", " ✓ Device approved and credentials saved."));
|
|
254
|
+
console.error(tr(` 只是暂时没连上服务器确认账号信息(${e.message})。`, ` Couldn't confirm your account details just now (${e.message}).`));
|
|
255
|
+
console.error(tr(" 稍后直接运行 u1s1 即可,不需要重新登录。", " Just run u1s1 later; no need to sign in again."));
|
|
206
256
|
process.exit(0);
|
|
207
257
|
});
|
|
208
258
|
const tpu = me.tokens_per_usd ?? 0;
|
|
209
259
|
let quotaNote;
|
|
210
|
-
if (
|
|
260
|
+
if (siteOf(next).id === "intl") {
|
|
261
|
+
// 国际站没有免费包/每日免费额度,只报按量余额(USD)
|
|
262
|
+
quotaNote = tr(`余额 $${me.bonus_balance_usd.toFixed(2)}`, `balance $${me.bonus_balance_usd.toFixed(2)}`);
|
|
263
|
+
}
|
|
264
|
+
else if (me.free_claim) {
|
|
211
265
|
// 新网关且免费包可领/可续,登录成功时顺手提醒
|
|
266
|
+
const dashboard = siteLinks(siteOf(next)).dashboard;
|
|
212
267
|
quotaNote =
|
|
213
268
|
me.free_claim === "first"
|
|
214
|
-
?
|
|
215
|
-
:
|
|
269
|
+
? tr(`有免费用量包可领,去 ${dashboard} 查看`, `a free usage pack is available — see ${dashboard}`)
|
|
270
|
+
: tr(`免费用量包状态有更新,去 ${dashboard} 查看`, `your free usage pack status changed — see ${dashboard}`);
|
|
216
271
|
}
|
|
217
272
|
else if (tpu > 0) {
|
|
218
273
|
const tok = (usd) => {
|
|
219
274
|
const t = usd * tpu;
|
|
275
|
+
if (isEnglish())
|
|
276
|
+
return t >= 1e6 ? `${Math.round((t / 1e6) * 10) / 10}M` : `${Math.round(t / 1e3)}K`;
|
|
220
277
|
if (t >= 1e8)
|
|
221
278
|
return `${Math.round((t / 1e8) * 10) / 10} 亿`;
|
|
222
279
|
return `${Math.round(t / 1e4).toLocaleString("en-US")} 万`;
|
|
223
280
|
};
|
|
224
|
-
quotaNote = `今日免费还剩约 ${tok(me.daily_free_remaining_usd)} Token
|
|
281
|
+
quotaNote = tr(`今日免费还剩约 ${tok(me.daily_free_remaining_usd)} Token`, `about ${tok(me.daily_free_remaining_usd)} free tokens left today`);
|
|
225
282
|
}
|
|
226
283
|
else {
|
|
227
284
|
// 老网关:没有 tokens_per_usd,退回金额显示
|
|
228
|
-
quotaNote = `今日免费还剩 $${me.daily_free_remaining_usd.toFixed(2)},永久余额 $${me.remaining_usd.toFixed(2)}
|
|
285
|
+
quotaNote = tr(`今日免费还剩 $${me.daily_free_remaining_usd.toFixed(2)},永久余额 $${me.remaining_usd.toFixed(2)}`, `$${me.daily_free_remaining_usd.toFixed(2)} free left today, balance $${me.remaining_usd.toFixed(2)}`);
|
|
229
286
|
}
|
|
230
|
-
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""},${quotaNote}
|
|
287
|
+
console.log(tr(` ✓ 登录成功${me.email ? `(${me.email})` : ""},${quotaNote}。`, ` ✓ Signed in${me.email ? ` as ${me.email}` : ""}; ${quotaNote}.`));
|
|
231
288
|
// 邮箱记进 config,启动横幅直接显示「账号」,不用再问「我现在登的是哪个号」
|
|
232
289
|
const withAccount = { ...next, email: me.email ?? undefined };
|
|
233
290
|
saveConfig(withAccount);
|
package/dist/loop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tr } from "./i18n.js";
|
|
1
2
|
/**
|
|
2
3
|
* /loop —— 会话级定时循环执行提示词(对标 Claude Code 的 /loop)。
|
|
3
4
|
*
|
|
@@ -34,12 +35,16 @@ function parseIntervalMs(token) {
|
|
|
34
35
|
}
|
|
35
36
|
function formatInterval(ms) {
|
|
36
37
|
if (ms >= 3_600_000 && ms % 3_600_000 === 0) {
|
|
37
|
-
return `${ms / 3_600_000}
|
|
38
|
+
return tr(`${ms / 3_600_000}小时`, `${ms / 3_600_000}h`);
|
|
38
39
|
}
|
|
39
40
|
if (ms % 60_000 === 0) {
|
|
40
|
-
return `${ms / 60_000}
|
|
41
|
+
return tr(`${ms / 60_000}分钟`, `${ms / 60_000}m`);
|
|
41
42
|
}
|
|
42
|
-
return `${Math.round(ms / 1_000)}
|
|
43
|
+
return tr(`${Math.round(ms / 1_000)}秒`, `${Math.round(ms / 1_000)}s`);
|
|
44
|
+
}
|
|
45
|
+
/** 「每5分钟」/ "every 5m" */
|
|
46
|
+
function everyInterval(ms) {
|
|
47
|
+
return tr(`每${formatInterval(ms)}`, `every ${formatInterval(ms)}`);
|
|
43
48
|
}
|
|
44
49
|
function preview(text) {
|
|
45
50
|
return text.length > PROMPT_PREVIEW_MAX ? `${text.slice(0, PROMPT_PREVIEW_MAX)}…` : text;
|
|
@@ -86,7 +91,7 @@ function parseArgs(ui, raw) {
|
|
|
86
91
|
}
|
|
87
92
|
const prompt = rest.join(" ").trim();
|
|
88
93
|
if (!prompt) {
|
|
89
|
-
ui.notify("用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态;停止用 /loop stop", "error");
|
|
94
|
+
ui.notify(tr("用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态;停止用 /loop stop", "Usage: /loop [interval] <prompt>, e.g. /loop 5m check the build status; /loop stop to stop"), "error");
|
|
90
95
|
return null;
|
|
91
96
|
}
|
|
92
97
|
return { intervalMs: intervalMs ?? DEFAULT_INTERVAL_MS, prompt };
|
|
@@ -95,22 +100,22 @@ export function registerLoopCommand(pi) {
|
|
|
95
100
|
// 会话重置(/clear、新会话)时循环随之消亡——和 Claude Code 的会话级语义一致
|
|
96
101
|
pi.on("session_start", (_event, ctx) => stopLoop(ctx.ui));
|
|
97
102
|
pi.registerCommand("loop", {
|
|
98
|
-
description: "定时重复执行提示词,如 /loop 5m 检查部署状态;/loop stop 停止",
|
|
103
|
+
description: tr("定时重复执行提示词,如 /loop 5m 检查部署状态;/loop stop 停止", "Repeat a prompt on an interval, e.g. /loop 5m check the build status; /loop stop to stop"),
|
|
99
104
|
handler: async (args, ctx) => {
|
|
100
105
|
const input = args.trim();
|
|
101
106
|
if (!input) {
|
|
102
107
|
ctx.ui.notify(timer
|
|
103
|
-
? `🔁
|
|
104
|
-
: "用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态", "info");
|
|
108
|
+
? tr(`🔁 当前循环:${everyInterval(activeIntervalMs)} · ${preview(activePrompt)}(/loop stop 停止)`, `🔁 Current loop: ${everyInterval(activeIntervalMs)} · ${preview(activePrompt)} (/loop stop to stop)`)
|
|
109
|
+
: tr("用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态", "Usage: /loop [interval] <prompt>, e.g. /loop 5m check the build status"), "info");
|
|
105
110
|
return;
|
|
106
111
|
}
|
|
107
112
|
if (/^(stop|off|取消|停止)$/i.test(input)) {
|
|
108
113
|
if (!timer) {
|
|
109
|
-
ctx.ui.notify("没有正在运行的循环", "info");
|
|
114
|
+
ctx.ui.notify(tr("没有正在运行的循环", "No loop is running"), "info");
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
stopLoop(ctx.ui);
|
|
113
|
-
ctx.ui.notify("⏹ 循环已停止", "info");
|
|
118
|
+
ctx.ui.notify(tr("⏹ 循环已停止", "⏹ Loop stopped"), "info");
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
const parsed = parseArgs(ctx.ui, input);
|
|
@@ -128,14 +133,15 @@ export function registerLoopCommand(pi) {
|
|
|
128
133
|
});
|
|
129
134
|
}
|
|
130
135
|
catch (e) {
|
|
131
|
-
|
|
136
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
137
|
+
ctx.ui.notify(tr(`loop 执行失败: ${reason}`, `Loop run failed: ${reason}`), "error");
|
|
132
138
|
}
|
|
133
139
|
};
|
|
134
140
|
// 第一轮立刻跑:用户写完命令就想看到结果,不该干等一个间隔
|
|
135
141
|
fire();
|
|
136
142
|
timer = setInterval(fire, activeIntervalMs);
|
|
137
|
-
ctx.ui.setStatus(STATUS_KEY, `🔁
|
|
138
|
-
ctx.ui.notify(`🔁
|
|
143
|
+
ctx.ui.setStatus(STATUS_KEY, `🔁 ${everyInterval(activeIntervalMs)} · ${preview(activePrompt)}`);
|
|
144
|
+
ctx.ui.notify(tr(`🔁 已开始:现在先跑第一轮,之后${everyInterval(activeIntervalMs)}一次「${preview(activePrompt)}」`, `🔁 Started: running the first round now, then ${everyInterval(activeIntervalMs)}: "${preview(activePrompt)}"`), "info");
|
|
139
145
|
},
|
|
140
146
|
});
|
|
141
147
|
}
|
package/dist/mcp/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { VERSION } from "../config.js";
|
|
3
|
+
import { tr } from "../i18n.js";
|
|
3
4
|
/**
|
|
4
5
|
* 极简 MCP 客户端:只做 initialize / tools/list / tools/call 三件事,
|
|
5
6
|
* 支持 stdio(本机进程,按行 JSON-RPC)和 Streamable HTTP(POST JSON,
|
|
@@ -29,8 +30,8 @@ function withTimeout(promise, timeoutMs, signal, what) {
|
|
|
29
30
|
signal?.removeEventListener("abort", onAbort);
|
|
30
31
|
fn(v);
|
|
31
32
|
};
|
|
32
|
-
const onAbort = () => finish(reject)(new McpError(`${what}
|
|
33
|
-
const timer = setTimeout(() => finish(reject)(new McpError(`${what} 超过 ${Math.round(timeoutMs / 1000)}s
|
|
33
|
+
const onAbort = () => finish(reject)(new McpError(tr(`${what} 已取消`, `${what} was canceled`)));
|
|
34
|
+
const timer = setTimeout(() => finish(reject)(new McpError(tr(`${what} 超过 ${Math.round(timeoutMs / 1000)}s 未响应`, `${what} timed out after ${Math.round(timeoutMs / 1000)}s`))), timeoutMs);
|
|
34
35
|
promise.then(finish(resolve), finish(reject));
|
|
35
36
|
if (signal?.aborted)
|
|
36
37
|
onAbort();
|
|
@@ -92,12 +93,12 @@ class StdioTransport {
|
|
|
92
93
|
// spawn 失败(ENOENT 等)不会再有 exit 事件
|
|
93
94
|
if (child.pid === undefined)
|
|
94
95
|
process.removeListener("exit", onParentExit);
|
|
95
|
-
this.fail(new McpError(`启动 ${this.command} 失败:${err.message}`));
|
|
96
|
+
this.fail(new McpError(tr(`启动 ${this.command} 失败:${err.message}`, `Failed to start ${this.command}: ${err.message}`)));
|
|
96
97
|
});
|
|
97
98
|
child.on("exit", (code, sig) => {
|
|
98
99
|
process.removeListener("exit", onParentExit);
|
|
99
100
|
const tail = this.stderrTail.slice(-5).join("\n");
|
|
100
|
-
this.fail(new McpError(
|
|
101
|
+
this.fail(new McpError(`${tr("MCP 服务进程已退出", "MCP server process exited")}(${sig ?? `code ${code ?? 0}`})${tail ? `\n${tail}` : ""}`));
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
104
|
fail(err) {
|
|
@@ -146,7 +147,7 @@ class StdioTransport {
|
|
|
146
147
|
throw this.exited;
|
|
147
148
|
const stdin = this.child?.stdin;
|
|
148
149
|
if (!stdin || stdin.destroyed)
|
|
149
|
-
throw new McpError("MCP 服务进程 stdin 已关闭");
|
|
150
|
+
throw new McpError(tr("MCP 服务进程 stdin 已关闭", "MCP server process stdin is closed"));
|
|
150
151
|
stdin.write(`${JSON.stringify(msg)}\n`);
|
|
151
152
|
}
|
|
152
153
|
request(msg, timeoutMs, signal) {
|
|
@@ -161,7 +162,7 @@ class StdioTransport {
|
|
|
161
162
|
reject(err);
|
|
162
163
|
}
|
|
163
164
|
});
|
|
164
|
-
return withTimeout(promise, timeoutMs, signal, msg.method ?? "请求").finally(() => this.pending.delete(id));
|
|
165
|
+
return withTimeout(promise, timeoutMs, signal, msg.method ?? tr("请求", "request")).finally(() => this.pending.delete(id));
|
|
165
166
|
}
|
|
166
167
|
async notify(msg) {
|
|
167
168
|
this.write(msg);
|
|
@@ -170,7 +171,7 @@ class StdioTransport {
|
|
|
170
171
|
const child = this.child;
|
|
171
172
|
if (!child)
|
|
172
173
|
return;
|
|
173
|
-
this.fail(new McpError("MCP 连接已关闭"));
|
|
174
|
+
this.fail(new McpError(tr("MCP 连接已关闭", "MCP connection closed")));
|
|
174
175
|
try {
|
|
175
176
|
child.stdin?.end();
|
|
176
177
|
}
|
|
@@ -252,10 +253,10 @@ class HttpTransport {
|
|
|
252
253
|
}
|
|
253
254
|
catch (err) {
|
|
254
255
|
if (signal?.aborted)
|
|
255
|
-
throw new McpError(`${msg.method ?? "请求"}
|
|
256
|
+
throw new McpError(tr(`${msg.method ?? "请求"} 已取消`, `${msg.method ?? "request"} was canceled`));
|
|
256
257
|
if (ctrl.signal.aborted)
|
|
257
|
-
throw new McpError(`${msg.method ?? "请求"} 超过 ${Math.round(timeoutMs / 1000)}s
|
|
258
|
-
throw new McpError(`连接 ${this.url} 失败:${err.message}`);
|
|
258
|
+
throw new McpError(tr(`${msg.method ?? "请求"} 超过 ${Math.round(timeoutMs / 1000)}s 未响应`, `${msg.method ?? "request"} timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
259
|
+
throw new McpError(tr(`连接 ${this.url} 失败:${err.message}`, `Failed to connect to ${this.url}: ${err.message}`));
|
|
259
260
|
}
|
|
260
261
|
finally {
|
|
261
262
|
clearTimeout(timer);
|
|
@@ -270,7 +271,7 @@ class HttpTransport {
|
|
|
270
271
|
if (!resp.ok) {
|
|
271
272
|
const text = (await resp.text().catch(() => "")).slice(0, 300);
|
|
272
273
|
if (resp.status === 401 || resp.status === 403) {
|
|
273
|
-
throw new McpError(`服务拒绝访问(HTTP ${resp.status}),请检查 headers 里的鉴权信息${text ? `:${text}` : ""}`, resp.status);
|
|
274
|
+
throw new McpError(tr(`服务拒绝访问(HTTP ${resp.status}),请检查 headers 里的鉴权信息${text ? `:${text}` : ""}`, `Access denied by server (HTTP ${resp.status}); check the auth headers${text ? `: ${text}` : ""}`), resp.status);
|
|
274
275
|
}
|
|
275
276
|
throw new McpError(`HTTP ${resp.status}${text ? `:${text}` : ""}`, resp.status);
|
|
276
277
|
}
|
|
@@ -292,13 +293,13 @@ class HttpTransport {
|
|
|
292
293
|
return m;
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
|
-
throw new McpError(`服务没有返回 ${msg.method ?? "请求"} 的结果(content-type ${ctype || "unknown"})`);
|
|
296
|
+
throw new McpError(tr(`服务没有返回 ${msg.method ?? "请求"} 的结果(content-type ${ctype || "unknown"})`, `Server returned no result for ${msg.method ?? "request"} (content-type ${ctype || "unknown"})`));
|
|
296
297
|
}
|
|
297
298
|
async notify(msg) {
|
|
298
299
|
const resp = await this.post(msg, 10_000);
|
|
299
300
|
// 规范要求 202;有些实现回 200/204,都当成功。鉴权失败要冒出来
|
|
300
301
|
if (resp.status === 401 || resp.status === 403)
|
|
301
|
-
throw new McpError(`服务拒绝访问(HTTP ${resp.status})`, resp.status);
|
|
302
|
+
throw new McpError(tr(`服务拒绝访问(HTTP ${resp.status})`, `Access denied by server (HTTP ${resp.status})`), resp.status);
|
|
302
303
|
await resp.body?.cancel().catch(() => { });
|
|
303
304
|
}
|
|
304
305
|
async close() {
|
|
@@ -409,7 +410,7 @@ export class McpClient {
|
|
|
409
410
|
async rpc(method, params, timeoutMs, signal) {
|
|
410
411
|
const transport = this.transport;
|
|
411
412
|
if (!transport)
|
|
412
|
-
throw new McpError("尚未连接");
|
|
413
|
+
throw new McpError(tr("尚未连接", "Not connected"));
|
|
413
414
|
const id = this.nextId++;
|
|
414
415
|
const resp = await transport.request({ jsonrpc: "2.0", id, method, params }, timeoutMs, signal);
|
|
415
416
|
if (resp.error) {
|