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/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { contextProjectTrust, runPool, runSubagent, SUBAGENT_CONCURRENCY, SUBAGE
|
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { generateImage, renderPage, searchWeb } from "./api.js";
|
|
8
|
+
import { tr } from "./i18n.js";
|
|
8
9
|
const FETCH_TIMEOUT_MS = 20_000;
|
|
9
10
|
const MAX_FETCH_BYTES = 2_000_000;
|
|
10
11
|
const MAX_TEXT_CHARS = 30_000;
|
|
@@ -38,7 +39,7 @@ function htmlToText(html) {
|
|
|
38
39
|
export function truncate(text) {
|
|
39
40
|
if (text.length <= MAX_TEXT_CHARS)
|
|
40
41
|
return text;
|
|
41
|
-
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n
|
|
42
|
+
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n${tr(`…(内容过长已截断,共 ${text.length} 字符)`, `… (truncated; ${text.length} characters total)`)}`;
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
45
|
* 长网页按 30k 字符开窗返回:光截断而不给「怎么读下一段」,模型会拿前 30k 硬答
|
|
@@ -50,10 +51,10 @@ export function windowText(text, offset) {
|
|
|
50
51
|
const body = text.slice(start, end);
|
|
51
52
|
if (start === 0 && end === text.length)
|
|
52
53
|
return { text: body, truncated: false, nextOffset: null };
|
|
53
|
-
const head = start > 0 ? `(从第 ${start} 字符起继续)\n\n` : "";
|
|
54
|
+
const head = start > 0 ? tr(`(从第 ${start} 字符起继续)\n\n`, `(continuing from character ${start})\n\n`) : "";
|
|
54
55
|
const tail = end < text.length
|
|
55
|
-
? `\n\n…(内容过长已截断:本次为第 ${start}–${end} 字符,共 ${text.length} 字符;要继续读取,用同一 url 再调一次 web_fetch 并传 offset=${end}
|
|
56
|
-
: `\n\n(已到末尾:第 ${start}–${end} 字符,共 ${text.length}
|
|
56
|
+
? tr(`\n\n…(内容过长已截断:本次为第 ${start}–${end} 字符,共 ${text.length} 字符;要继续读取,用同一 url 再调一次 web_fetch 并传 offset=${end})`, `\n\n… (truncated: this is characters ${start}–${end} of ${text.length}; to keep reading, call web_fetch again with the same url and offset=${end})`)
|
|
57
|
+
: tr(`\n\n(已到末尾:第 ${start}–${end} 字符,共 ${text.length} 字符)`, `\n\n(end of page: characters ${start}–${end} of ${text.length})`);
|
|
57
58
|
return { text: `${head}${body}${tail}`, truncated: end < text.length, nextOffset: end < text.length ? end : null };
|
|
58
59
|
}
|
|
59
60
|
/** 同一页面带 offset 续读时直接命中,不再重新抓取(云端渲染是按次计费的)。 */
|
|
@@ -159,7 +160,7 @@ const SEARCH_QUERY_MAX_CHARS = 100;
|
|
|
159
160
|
export function createSearchTool(cfg) {
|
|
160
161
|
return defineTool({
|
|
161
162
|
name: "web_search",
|
|
162
|
-
label: "联网搜索",
|
|
163
|
+
label: tr("联网搜索", "Web search"),
|
|
163
164
|
description: "Search the web for current information. Use it for anything time-sensitive or outside the codebase: library docs, error messages, latest versions, recent events. Returns a short answer plus ranked results with URLs and snippets. Follow up with web_fetch to read a promising result in full.",
|
|
164
165
|
promptSnippet: "Search the internet for up-to-date information (docs, errors, versions, current events)",
|
|
165
166
|
promptGuidelines: [
|
|
@@ -181,8 +182,8 @@ export function createSearchTool(cfg) {
|
|
|
181
182
|
const [result, options, theme, context] = args;
|
|
182
183
|
const d = result.details;
|
|
183
184
|
const q = d?.query ? `"${previewLine(d.query, 80)}"` : "…";
|
|
184
|
-
const n = typeof d?.count === "number" ? ` · ${d.count}
|
|
185
|
-
return compactResultRender({ result, options, theme, context, summaryLine: `🔍 搜索 ${q}${n}` });
|
|
185
|
+
const n = typeof d?.count === "number" ? tr(` · ${d.count} 条结果`, ` · ${d.count} result${d.count === 1 ? "" : "s"}`) : "";
|
|
186
|
+
return compactResultRender({ result, options, theme, context, summaryLine: tr(`🔍 搜索 ${q}${n}`, `🔍 Search ${q}${n}`) });
|
|
186
187
|
},
|
|
187
188
|
async execute(_toolCallId, params, signal) {
|
|
188
189
|
const data = await searchWeb(cfg, { query: params.query, maxResults: params.maxResults, signal });
|
|
@@ -215,7 +216,7 @@ const SUBAGENT_MAX_TASKS = 10;
|
|
|
215
216
|
export function createSubagentTool(getParentModel) {
|
|
216
217
|
return defineTool({
|
|
217
218
|
name: "spawn_subagent",
|
|
218
|
-
label: "子任务",
|
|
219
|
+
label: tr("子任务", "Subtask"),
|
|
219
220
|
description: "Spawn an independent sub-agent with its own context window to complete a self-contained task, then return its final answer. " +
|
|
220
221
|
"Pass `tasks` (array) to run N sub-agents concurrently — ideal for embarrassingly parallel work like translating many locale files, " +
|
|
221
222
|
"reviewing several files, or researching multiple directions at once. Each sub-agent can read/write files and run commands. " +
|
|
@@ -250,10 +251,10 @@ export function createSubagentTool(getParentModel) {
|
|
|
250
251
|
const [result, options, theme, context] = args;
|
|
251
252
|
const d = result.details;
|
|
252
253
|
const summary = d && typeof d.count === "number"
|
|
253
|
-
? `🤖 子任务 ×${d.count}` +
|
|
254
|
+
? tr(`🤖 子任务 ×${d.count}`, `🤖 Subtasks ×${d.count}`) +
|
|
254
255
|
(typeof d.ok === "number" && d.failed ? ` · ✓${d.ok} ✗${d.failed}` : "") +
|
|
255
256
|
(typeof d.seconds === "number" ? ` · ${d.seconds}s` : "")
|
|
256
|
-
: "🤖 子任务";
|
|
257
|
+
: tr("🤖 子任务", "🤖 Subtask");
|
|
257
258
|
return compactResultRender({ result, options, theme, context, summaryLine: summary });
|
|
258
259
|
},
|
|
259
260
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -264,7 +265,7 @@ export function createSubagentTool(getParentModel) {
|
|
|
264
265
|
.filter(Boolean)
|
|
265
266
|
.slice(0, SUBAGENT_MAX_TASKS);
|
|
266
267
|
if (list.length === 0) {
|
|
267
|
-
throw new Error("task 和 tasks 至少填一个");
|
|
268
|
+
throw new Error(tr("task 和 tasks 至少填一个", "Provide at least one of task or tasks"));
|
|
268
269
|
}
|
|
269
270
|
const parentModel = getParentModel();
|
|
270
271
|
const timeoutMs = Math.min(60, params.timeout_minutes ?? SUBAGENT_TIMEOUT_MS / 60_000) * 60_000;
|
|
@@ -299,10 +300,13 @@ export function createSubagentTool(getParentModel) {
|
|
|
299
300
|
details: { ...details, seconds: undefined },
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
|
-
const lines = [
|
|
303
|
+
const lines = [
|
|
304
|
+
tr(`共 ${list.length} 个子任务,✓${details.ok} ✗${details.failed},用时 ${seconds}s`, `${list.length} subtasks, ✓${details.ok} ✗${details.failed}, took ${seconds}s`),
|
|
305
|
+
"",
|
|
306
|
+
];
|
|
303
307
|
outcomes.forEach((o, i) => {
|
|
304
|
-
lines.push(`## 任务 ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`, "");
|
|
305
|
-
lines.push(o?.ok ? o.text : `失败:${o?.text ?? "未知错误"}`, "", "---", "");
|
|
308
|
+
lines.push(tr(`## 任务 ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`, `## Task ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`), "");
|
|
309
|
+
lines.push(o?.ok ? o.text : tr(`失败:${o?.text ?? "未知错误"}`, `Failed: ${o?.text ?? "unknown error"}`), "", "---", "");
|
|
306
310
|
});
|
|
307
311
|
return {
|
|
308
312
|
content: [{ type: "text", text: truncate(lines.join("\n").trim()) }],
|
|
@@ -333,12 +337,13 @@ function refImageToPayload(ref) {
|
|
|
333
337
|
return s;
|
|
334
338
|
const path = resolve(s);
|
|
335
339
|
if (!existsSync(path))
|
|
336
|
-
throw new Error(`参考图不存在: ${s}`);
|
|
340
|
+
throw new Error(tr(`参考图不存在: ${s}`, `Reference image not found: ${s}`));
|
|
337
341
|
const mime = REF_IMAGE_MIME[extname(path).toLowerCase()];
|
|
338
|
-
if (!mime)
|
|
339
|
-
throw new Error(`参考图格式不支持: ${s}(支持 png/jpg/jpeg/webp/bmp/gif)`);
|
|
342
|
+
if (!mime) {
|
|
343
|
+
throw new Error(tr(`参考图格式不支持: ${s}(支持 png/jpg/jpeg/webp/bmp/gif)`, `Unsupported reference image format: ${s} (supported: png/jpg/jpeg/webp/bmp/gif)`));
|
|
344
|
+
}
|
|
340
345
|
if (statSync(path).size > MAX_REF_IMAGE_BYTES)
|
|
341
|
-
throw new Error(`参考图超过 10MB: ${s}`);
|
|
346
|
+
throw new Error(tr(`参考图超过 10MB: ${s}`, `Reference image exceeds 10MB: ${s}`));
|
|
342
347
|
return `data:${mime};base64,${readFileSync(path).toString("base64")}`;
|
|
343
348
|
}
|
|
344
349
|
/** 结果落盘路径:模型没指定时按时间戳起名,已存在就加序号,不覆盖旧图。 */
|
|
@@ -380,12 +385,16 @@ async function abortableDelay(ms, signal) {
|
|
|
380
385
|
}
|
|
381
386
|
class GeneratedImageTooLargeError extends Error {
|
|
382
387
|
}
|
|
388
|
+
function imageTooLargeMessage(maxBytes) {
|
|
389
|
+
const mib = Math.floor(maxBytes / 1024 / 1024);
|
|
390
|
+
return tr(`图片文件超过下载上限 ${mib} MiB`, `the image file exceeds the ${mib} MiB download limit`);
|
|
391
|
+
}
|
|
383
392
|
async function readGeneratedImageBody(resp, maxBytes) {
|
|
384
393
|
const declaredLength = Number(resp.headers.get("content-length"));
|
|
385
394
|
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
386
395
|
if (resp.body)
|
|
387
396
|
void resp.body.cancel("generated image body limit reached").catch(() => { });
|
|
388
|
-
throw new GeneratedImageTooLargeError(
|
|
397
|
+
throw new GeneratedImageTooLargeError(imageTooLargeMessage(maxBytes));
|
|
389
398
|
}
|
|
390
399
|
if (!resp.body)
|
|
391
400
|
return new Uint8Array();
|
|
@@ -399,7 +408,7 @@ async function readGeneratedImageBody(resp, maxBytes) {
|
|
|
399
408
|
break;
|
|
400
409
|
if (total + value.byteLength > maxBytes) {
|
|
401
410
|
void reader.cancel("generated image body limit reached").catch(() => { });
|
|
402
|
-
throw new GeneratedImageTooLargeError(
|
|
411
|
+
throw new GeneratedImageTooLargeError(imageTooLargeMessage(maxBytes));
|
|
403
412
|
}
|
|
404
413
|
chunks.push(value);
|
|
405
414
|
total += value.byteLength;
|
|
@@ -444,21 +453,22 @@ export async function downloadGeneratedImage(url, signal, options = {}) {
|
|
|
444
453
|
if (signal.aborted)
|
|
445
454
|
throw error;
|
|
446
455
|
if (error instanceof GeneratedImageTooLargeError) {
|
|
447
|
-
throw new Error(`图片已经生成,但${error.message}。不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, { cause: error });
|
|
456
|
+
throw new Error(tr(`图片已经生成,但${error.message}。不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, `The image was generated, but ${error.message}. Do not call generate_image again; it can be downloaded manually within 24 hours: ${url}`), { cause: error });
|
|
448
457
|
}
|
|
449
458
|
lastError = error;
|
|
450
459
|
if (attempt < attempts)
|
|
451
460
|
await abortableDelay(retryDelayMs * attempt, signal);
|
|
452
461
|
}
|
|
453
462
|
}
|
|
454
|
-
throw new Error(`图片已经生成,但下载失败(同一地址已重试 ${attempts} 次): ${errorDetail(lastError)}。` +
|
|
455
|
-
`不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, {
|
|
463
|
+
throw new Error(tr(`图片已经生成,但下载失败(同一地址已重试 ${attempts} 次): ${errorDetail(lastError)}。` +
|
|
464
|
+
`不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, `The image was generated, but downloading it failed (retried the same URL ${attempts} times): ${errorDetail(lastError)}. ` +
|
|
465
|
+
`Do not call generate_image again; it can be downloaded manually within 24 hours: ${url}`), { cause: lastError });
|
|
456
466
|
}
|
|
457
467
|
/** 生图工具:走 u1s1 网关代理火山方舟 Seedream,上游 key 不落到用户机器上。 */
|
|
458
468
|
export function createImageTool(cfg) {
|
|
459
469
|
return defineTool({
|
|
460
470
|
name: "generate_image",
|
|
461
|
-
label: "生成图片",
|
|
471
|
+
label: tr("生成图片", "Generate image"),
|
|
462
472
|
description: "Generate an image from a text prompt, or edit/compose existing images, using the Seedream image model. " +
|
|
463
473
|
"Saves the result as a local image file and displays it directly in supported chat UIs. " +
|
|
464
474
|
"Pass local file paths or http(s) URLs in `images` to edit an image or use references (style transfer, adding elements, combining up to 10 images). " +
|
|
@@ -499,7 +509,9 @@ export function createImageTool(cfg) {
|
|
|
499
509
|
const sizeNote = result.size ? ` (${result.size})` : "";
|
|
500
510
|
const mimeType = REF_IMAGE_MIME[urlExt] ?? "image/jpeg";
|
|
501
511
|
return {
|
|
502
|
-
content: [
|
|
512
|
+
content: [
|
|
513
|
+
{ type: "text", text: tr(`图片已保存并在对话中展示: ${path}${sizeNote}`, `Image saved and shown in the chat: ${path}${sizeNote}`) },
|
|
514
|
+
],
|
|
503
515
|
// displayImage 只给 UI 预览,不放进 content:避免每轮都把整张 2K/4K
|
|
504
516
|
// 图片送回模型占用 context。pi-web-ui 会从 tool details 序列化它。
|
|
505
517
|
details: {
|
|
@@ -541,16 +553,16 @@ async function fetchDirect(url, signal) {
|
|
|
541
553
|
// 用户主动打断不算失败,直接往外抛,不去碰云端渲染
|
|
542
554
|
if (signal?.aborted)
|
|
543
555
|
throw e;
|
|
544
|
-
throw new DirectFetchError(`打不开 ${url.href}: ${e.message}`, true);
|
|
556
|
+
throw new DirectFetchError(tr(`打不开 ${url.href}: ${e.message}`, `Couldn't open ${url.href}: ${e.message}`), true);
|
|
545
557
|
}
|
|
546
558
|
if (!resp.ok) {
|
|
547
559
|
void resp.body?.cancel("web fetch rejected HTTP response").catch(() => undefined);
|
|
548
|
-
throw new DirectFetchError(`${url.href} 返回 ${resp.status} ${resp.statusText}`, RENDER_WORTHY_STATUS.has(resp.status));
|
|
560
|
+
throw new DirectFetchError(tr(`${url.href} 返回 ${resp.status} ${resp.statusText}`, `${url.href} returned ${resp.status} ${resp.statusText}`), RENDER_WORTHY_STATUS.has(resp.status));
|
|
549
561
|
}
|
|
550
562
|
const type = resp.headers.get("content-type") ?? "";
|
|
551
563
|
if (!/text\/|json|xml|javascript/i.test(type)) {
|
|
552
564
|
void resp.body?.cancel("web fetch rejected non-text response").catch(() => undefined);
|
|
553
|
-
throw new DirectFetchError(`${url.href} 不是文本内容 (${type || "unknown"}),读不了`, false);
|
|
565
|
+
throw new DirectFetchError(tr(`${url.href} 不是文本内容 (${type || "unknown"}),读不了`, `${url.href} is not text content (${type || "unknown"}); can't read it`), false);
|
|
554
566
|
}
|
|
555
567
|
let bytes;
|
|
556
568
|
try {
|
|
@@ -560,7 +572,7 @@ async function fetchDirect(url, signal) {
|
|
|
560
572
|
if (signal?.aborted)
|
|
561
573
|
throw error;
|
|
562
574
|
const detail = error instanceof Error ? error.message : String(error);
|
|
563
|
-
throw new DirectFetchError(`${url.href} 正文读取失败: ${detail}`, true);
|
|
575
|
+
throw new DirectFetchError(tr(`${url.href} 正文读取失败: ${detail}`, `Failed to read the body of ${url.href}: ${detail}`), true);
|
|
564
576
|
}
|
|
565
577
|
const raw = decodeBody(bytes, type);
|
|
566
578
|
const isHtml = /html|xml/i.test(type);
|
|
@@ -580,7 +592,7 @@ export function createFetchTool(cfg) {
|
|
|
580
592
|
}
|
|
581
593
|
return defineTool({
|
|
582
594
|
name: "web_fetch",
|
|
583
|
-
label: "读取网页",
|
|
595
|
+
label: tr("读取网页", "Fetch page"),
|
|
584
596
|
description: "Fetch a URL and return its readable text content (HTML is stripped to text; JSON and plain text are returned as-is). Use it to read a page found via web_search, or any URL the user pasted. If the direct fetch fails or the page needs JavaScript, it automatically retries through a cloud browser that can also clear bot-check interstitials. Long pages are returned in 30,000-character windows: when the result says it was truncated, call again with the same url and the offset it gives to read the next part.",
|
|
585
597
|
promptSnippet: "Fetch a URL and convert it to readable text",
|
|
586
598
|
promptGuidelines: [
|
|
@@ -602,9 +614,13 @@ export function createFetchTool(cfg) {
|
|
|
602
614
|
const [result, options, theme, context] = args;
|
|
603
615
|
const d = result.details;
|
|
604
616
|
const u = previewLine(d?.url ?? "…", 90);
|
|
605
|
-
const n = typeof d?.chars === "number" ? ` · ${d.chars}
|
|
606
|
-
const win = d?.offset
|
|
607
|
-
|
|
617
|
+
const n = typeof d?.chars === "number" ? tr(` · ${d.chars} 字符`, ` · ${d.chars} chars`) : "";
|
|
618
|
+
const win = d?.offset
|
|
619
|
+
? tr(` · 从第 ${d.offset} 字符续读`, ` · continued from char ${d.offset}`)
|
|
620
|
+
: d?.truncated
|
|
621
|
+
? tr(" · 只读了开头", " · first part only")
|
|
622
|
+
: "";
|
|
623
|
+
const cloud = d?.via === "render" ? tr(" · ☁ 浏览器渲染", " · ☁ browser render") : "";
|
|
608
624
|
return compactResultRender({ result, options, theme, context, summaryLine: `🌐 ${u}${n}${win}${cloud}` });
|
|
609
625
|
},
|
|
610
626
|
async execute(_toolCallId, params, signal) {
|
|
@@ -614,15 +630,15 @@ export function createFetchTool(cfg) {
|
|
|
614
630
|
url = new URL(params.url.trim().replace(/^@/, ""));
|
|
615
631
|
}
|
|
616
632
|
catch {
|
|
617
|
-
throw new Error(`不是合法的网址: ${params.url}`);
|
|
633
|
+
throw new Error(tr(`不是合法的网址: ${params.url}`, `Not a valid URL: ${params.url}`));
|
|
618
634
|
}
|
|
619
635
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
620
|
-
throw new Error(`只支持 http/https,收到 ${url.protocol}`);
|
|
636
|
+
throw new Error(tr(`只支持 http/https,收到 ${url.protocol}`, `Only http/https is supported; got ${url.protocol}`));
|
|
621
637
|
}
|
|
622
638
|
const offset = Math.max(0, Math.floor(params.offset ?? 0));
|
|
623
639
|
const ok = (text, via, contentType) => {
|
|
624
640
|
rememberPage(url.href, { text, via, contentType });
|
|
625
|
-
const win = windowText(text || "(空白页面)", offset);
|
|
641
|
+
const win = windowText(text || tr("(空白页面)", "(empty page)"), offset);
|
|
626
642
|
return {
|
|
627
643
|
content: [{ type: "text", text: `# ${url.href}\n\n${win.text}` }],
|
|
628
644
|
details: { url: url.href, contentType, via, chars: text.length, offset, truncated: win.truncated, nextOffset: win.nextOffset },
|
|
@@ -643,7 +659,7 @@ export function createFetchTool(cfg) {
|
|
|
643
659
|
return ok(await renderViaGateway(url, signal), "render", "text/markdown");
|
|
644
660
|
}
|
|
645
661
|
catch (re) {
|
|
646
|
-
throw new Error(`${e.message};云端浏览器渲染也失败: ${re.message}`, { cause: re });
|
|
662
|
+
throw new Error(tr(`${e.message};云端浏览器渲染也失败: ${re.message}`, `${e.message}; cloud browser rendering also failed: ${re.message}`), { cause: re });
|
|
647
663
|
}
|
|
648
664
|
}
|
|
649
665
|
// 直连 200 但正文近乎空:大概率是 JS 渲染的 SPA,换浏览器再试;渲染失败就退回空壳
|
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;
|