u1s1-cli 1.11.8 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/agent-setup.d.ts +5 -0
  2. package/dist/agent-setup.js +31 -7
  3. package/dist/api.js +42 -34
  4. package/dist/bench-report.js +12 -9
  5. package/dist/bench.js +34 -31
  6. package/dist/brand.d.ts +2 -1
  7. package/dist/brand.js +38 -12
  8. package/dist/config.d.ts +11 -1
  9. package/dist/config.js +25 -8
  10. package/dist/context-usage.js +12 -9
  11. package/dist/deploy.js +49 -45
  12. package/dist/device-auth.js +7 -5
  13. package/dist/error-humanize.d.ts +1 -0
  14. package/dist/error-humanize.js +18 -6
  15. package/dist/feedback.js +48 -31
  16. package/dist/i18n.d.ts +21 -0
  17. package/dist/i18n.js +46 -0
  18. package/dist/import/index.js +64 -28
  19. package/dist/import/skills.js +72 -41
  20. package/dist/import/util.js +3 -2
  21. package/dist/import/write.js +2 -1
  22. package/dist/index.js +139 -48
  23. package/dist/login.js +39 -28
  24. package/dist/loop.js +18 -12
  25. package/dist/mcp/client.js +15 -14
  26. package/dist/mcp/command.js +76 -48
  27. package/dist/mcp/extension.js +5 -3
  28. package/dist/mcp/tools.js +6 -5
  29. package/dist/model.js +11 -10
  30. package/dist/nudges.d.ts +8 -2
  31. package/dist/nudges.js +22 -3
  32. package/dist/search-tools.js +7 -6
  33. package/dist/shell-doctor.js +9 -8
  34. package/dist/subagent.js +5 -4
  35. package/dist/tools.js +53 -37
  36. package/dist/update.js +29 -28
  37. package/dist/usage.d.ts +1 -1
  38. package/dist/usage.js +96 -43
  39. package/dist/web.js +2 -1
  40. package/dist/workflow/runner.js +33 -29
  41. package/dist/workflow/templates.js +70 -45
  42. package/dist/workflow/tool.js +15 -12
  43. package/package.json +2 -2
  44. package/scripts/patch-pi.js +243 -278
  45. package/scripts/render-regression.mjs +55 -4
@@ -1,6 +1,11 @@
1
1
  import { type CliConfig, type CustomEndpoint, type ModelDef, type ThinkingLevel } from "./config.js";
2
2
  import type { ShellDoctorResult } from "./shell-doctor.js";
3
3
  import type { Site } from "./site.js";
4
+ /**
5
+ * 系统提示里的品牌人设。中国站面向编程新手、兜底语言中文(保持原文一字不差);
6
+ * 国际站面向开发者、兜底语言英文。按站点而不是界面语言判定,用时求值(配置可能晚于 import 才读)。
7
+ */
8
+ export declare function brandAppend(site?: Site): string;
4
9
  /** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
5
10
  export declare function ensureBrandPrompt(shell: ShellDoctorResult): void;
6
11
  /**
@@ -2,7 +2,24 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { dirname, join } from "node:path";
4
4
  import { activeSite, agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, effectiveContextWindow, endpointKeyEnvName, MODELS, readCompactionBudgetSetting, PROVIDER_ID, UNKNOWN_MODEL_COST, VERSION, } from "./config.js";
5
- const BRAND_APPEND = `## u1s1
5
+ import { tr } from "./i18n.js";
6
+ /**
7
+ * 系统提示里的品牌人设。中国站面向编程新手、兜底语言中文(保持原文一字不差);
8
+ * 国际站面向开发者、兜底语言英文。按站点而不是界面语言判定,用时求值(配置可能晚于 import 才读)。
9
+ */
10
+ export function brandAppend(site = activeSite()) {
11
+ if (site.id === "intl") {
12
+ return `## u1s1
13
+
14
+ You are u1s1 — a plain-spoken AI coding assistant:
15
+ - When introducing yourself or asked "who are you", say you are u1s1, never "pi"; only mention the underlying pi engine if the user explicitly asks about internals.
16
+ - Reply in the language of the user's latest request by default. If that is ambiguous, follow the conversation language, then the UI locale when available, and otherwise use English. Never infer language from IP or location. Keep code, commands, and verbatim error messages in English.
17
+ - Explain problems in plain, direct language and skip needless jargon; use a one-sentence analogy when helpful.
18
+ - Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
19
+ - If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
20
+ `;
21
+ }
22
+ return `## u1s1
6
23
 
7
24
  You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
8
25
  - When introducing yourself or asked "who are you", say you are u1s1, never "pi"; only mention the underlying pi engine if the user explicitly asks about internals.
@@ -11,6 +28,7 @@ You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most
11
28
  - Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
12
29
  - If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
13
30
  `;
31
+ }
14
32
  const EXEC_RULES = `
15
33
  ## Command execution
16
34
 
@@ -45,7 +63,7 @@ function envSection(shell) {
45
63
  /** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
46
64
  export function ensureBrandPrompt(shell) {
47
65
  mkdirSync(agentDir, { recursive: true });
48
- const content = BRAND_APPEND + envSection(shell) + EXEC_RULES;
66
+ const content = brandAppend() + envSection(shell) + EXEC_RULES;
49
67
  const p = join(agentDir, "APPEND_SYSTEM.md");
50
68
  if (!existsSync(p) || readFileSync(p, "utf8") !== content) {
51
69
  writeFileSync(p, content);
@@ -229,19 +247,21 @@ export function writeWebToolsExtension(cfg, features) {
229
247
  // 密钥剥离守卫也从这里装:Desktop App 的 pi 跑在 pi-web-ui 子进程里,没有
230
248
  // 启动器代码,只有扩展能替它把 U1S1_API_KEY / U1S1_EP_KEY_* 挡在子进程之外
231
249
  const secretEnvUrl = new URL("./secret-env.js", import.meta.url).href;
250
+ // 提示文案在生成时按当前界面语言定好,扩展文件里只是字面量
251
+ const toolsLoadFailed = tr("联网工具本次未能加载(程序文件可能正在更新),重启 u1s1 即可恢复", "Web tools couldn't load this time (program files may be updating). Restart u1s1 to restore them.");
232
252
  writeFileSync(join(dir, "u1s1-tools.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
233
253
  `export default async function (pi) {\n` +
234
254
  ` (await import(${JSON.stringify(secretEnvUrl)})).installChildEnvGuard();\n` +
235
255
  ` if (process.env.U1S1_TOOLS_VIA_EXTENSION !== "1") return;\n` +
236
256
  ` const baseUrl = process.env.U1S1_SIGNING_PROXY_URL || ${JSON.stringify(cfg.baseUrl)};\n` +
237
257
  // 升级半途 / 权限问题导致 CLI dist 不可读时,宁可这一会话没有联网工具,
238
- // 也不能让扩展加载失败刷英文堆栈(审计 B4);只在 UI 里提示一句中文
258
+ // 也不能让扩展加载失败刷英文堆栈(审计 B4);只在 UI 里提示一句人话
239
259
  ` let tools;\n` +
240
260
  ` try {\n` +
241
261
  ` tools = await import(${JSON.stringify(toolsUrl)});\n` +
242
262
  ` } catch {\n` +
243
263
  ` pi.on("session_start", (_event, ctx) => {\n` +
244
- ` try { if (ctx.hasUI) ctx.ui.notify("联网工具本次未能加载(程序文件可能正在更新),重启 u1s1 即可恢复", "warning"); } catch {}\n` +
264
+ ` try { if (ctx.hasUI) ctx.ui.notify(${JSON.stringify(toolsLoadFailed)}, "warning"); } catch {}\n` +
245
265
  ` });\n` +
246
266
  ` return;\n` +
247
267
  ` }\n` +
@@ -342,6 +362,7 @@ export function writeMcpExtension() {
342
362
  const dir = join(agentDir, "extensions");
343
363
  mkdirSync(dir, { recursive: true });
344
364
  const url = new URL("./mcp/extension.js", import.meta.url).href;
365
+ const mcpLoadFailed = tr("MCP 服务加载失败:", "Failed to load MCP servers: ");
345
366
  writeFileSync(join(dir, "u1s1-mcp.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
346
367
  `export default async function (pi) {\n` +
347
368
  ` if (process.env.U1S1_IN_SUBAGENT === "1") return;\n` +
@@ -356,7 +377,7 @@ export function writeMcpExtension() {
356
377
  ` } catch (err) {\n` +
357
378
  ` const msg = err instanceof Error ? err.message : String(err);\n` +
358
379
  ` pi.on("session_start", (_event, ctx) => {\n` +
359
- ` if (ctx.hasUI) ctx.ui.notify("MCP 服务加载失败:" + msg, "warning");\n` +
380
+ ` if (ctx.hasUI) ctx.ui.notify(${JSON.stringify(mcpLoadFailed)} + msg, "warning");\n` +
360
381
  ` });\n` +
361
382
  ` }\n` +
362
383
  `}\n`);
@@ -391,6 +412,9 @@ export function writeAttributionExtension(site = activeSite()) {
391
412
  export function writeCompactUiExtension(outputDir) {
392
413
  const dir = outputDir ?? join(agentDir, "extensions");
393
414
  mkdirSync(dir, { recursive: true });
415
+ // 汇总行文案按生成时的界面语言写死进扩展;英文版面向开发者,直接用工具原名
416
+ const toolSummaryLabels = tr(`read: "读文件", write: "写文件", edit: "改文件", bash: "跑命令", grep: "搜内容", find: "找文件", ls: "列目录",`, `read: "read", write: "write", edit: "edit", bash: "bash", grep: "grep", find: "find", ls: "ls",`);
417
+ const toolCallsLabel = tr(`d.total + " 次工具调用"`, `d.total + (d.total === 1 ? " tool call" : " tool calls")`);
394
418
  writeFileSync(join(dir, "u1s1-compact-ui.js"), `// 由 u1s1 每次启动自动生成,请勿手改
395
419
  function oneLine(text, max = 160) {
396
420
  const flat = String(text).replace(/\\s*\\n\\s*/g, " ; ").trim();
@@ -408,7 +432,7 @@ export default async function (pi) {
408
432
  let errors = 0;
409
433
  // 工具调用被折叠后,这一行摘要是新手了解「AI 刚才做了什么」的唯一入口,内置工具名给人话
410
434
  const TOOL_SUMMARY_LABELS = {
411
- read: "读文件", write: "写文件", edit: "改文件", bash: "跑命令", grep: "搜内容", find: "找文件", ls: "列目录",
435
+ ${toolSummaryLabels}
412
436
  };
413
437
  pi.on("agent_start", async () => {
414
438
  counts = {};
@@ -432,7 +456,7 @@ export default async function (pi) {
432
456
  pi.registerEntryRenderer("tool-summary", (entry, _opts, theme) => {
433
457
  const d = entry.data;
434
458
  let text = theme.fg("muted", "⚙ ");
435
- text += theme.fg("toolTitle", theme.bold(d.total + " 次工具调用"));
459
+ text += theme.fg("toolTitle", theme.bold(${toolCallsLabel}));
436
460
  text += theme.fg("dim", " · " + d.summary);
437
461
  return new Text(text, 0, 0);
438
462
  });
package/dist/api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
2
2
  import { authorizedFetch } from "./device-auth.js";
3
+ import { tr } from "./i18n.js";
3
4
  const MAX_API_RESPONSE_BYTES = 8 * 1024 * 1024;
4
5
  const MAX_API_ERROR_BYTES = 64 * 1024;
5
6
  const MAX_IMAGE_RESULT_BYTES = 64 * 1024;
@@ -60,6 +61,12 @@ export class AuthError extends Error {
60
61
  /** 403:登录着但被服务端拒绝(封禁/停用/设备不受信任),重新登录也没用,进 TUI 每条消息都会失败。 */
61
62
  export class AccessDeniedError extends Error {
62
63
  }
64
+ function unreachableMessage(baseUrl) {
65
+ return tr(`连不上 ${baseUrl},检查一下网络?`, `Can't reach ${baseUrl}. Check your network?`);
66
+ }
67
+ function sessionExpiredMessage() {
68
+ return tr("登录已失效,请重新运行 u1s1 login", "Your session has expired. Run u1s1 login to sign in again.");
69
+ }
63
70
  /** 网关错误壳统一是 { error: { message } };解析不出来时回退到状态码提示。 */
64
71
  async function errorMessageFromResponse(resp, fallback) {
65
72
  const body = jsonRecord(await readJsonResponseCapped(resp, MAX_API_ERROR_BYTES).catch(() => null));
@@ -79,17 +86,17 @@ export async function fetchModels(cfg) {
79
86
  catch (error) {
80
87
  if (signal.aborted || (error instanceof Error && error.name === "TimeoutError"))
81
88
  throw error;
82
- throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`, { cause: error });
89
+ throw new Error(unreachableMessage(cfg.baseUrl), { cause: error });
83
90
  }
84
91
  if (resp.status === 401)
85
- throw new AuthError("登录已失效,请重新运行 u1s1 login");
92
+ throw new AuthError(sessionExpiredMessage());
86
93
  if (resp.status === 403)
87
- throw new AccessDeniedError(await errorMessageFromResponse(resp, "账号当前无法使用 u1s1"));
94
+ throw new AccessDeniedError(await errorMessageFromResponse(resp, tr("账号当前无法使用 u1s1", "This account can't use u1s1 right now")));
88
95
  if (!resp.ok)
89
- throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
96
+ throw new Error(await errorMessageFromResponse(resp, tr(`服务端返回 ${resp.status},稍后再试`, `Server returned ${resp.status}. Please try again later.`)));
90
97
  const body = await readJsonResponseCapped(resp);
91
98
  if (!body || !Array.isArray(body.data))
92
- throw new Error("服务端模型列表格式不正确");
99
+ throw new Error(tr("服务端模型列表格式不正确", "The server returned a malformed model list"));
93
100
  const token = body.client_attestation?.token;
94
101
  const expiresIn = body.client_attestation?.expires_in;
95
102
  return {
@@ -104,7 +111,7 @@ export async function fetchModels(cfg) {
104
111
  if (error instanceof AuthError || error instanceof AccessDeniedError)
105
112
  throw error;
106
113
  if (signal.aborted || (error instanceof Error && error.name === "TimeoutError")) {
107
- throw new Error("获取模型列表超时(15 秒内未完成);可能与网络响应或本机繁忙有关,请稍后重试。", { cause: error });
114
+ throw new Error(tr("获取模型列表超时(15 秒内未完成);可能与网络响应或本机繁忙有关,请稍后重试。", "Fetching the model list timed out (no response within 15 seconds). The network or this machine may be busy; please try again later."), { cause: error });
108
115
  }
109
116
  throw error;
110
117
  }
@@ -122,12 +129,12 @@ export async function fetchLatestAnnouncement(cfg) {
122
129
  });
123
130
  }
124
131
  catch {
125
- throw new Error(`连不上 ${cfg.baseUrl}`);
132
+ throw new Error(tr(`连不上 ${cfg.baseUrl}`, `Can't reach ${cfg.baseUrl}`));
126
133
  }
127
134
  if (resp.status === 404)
128
135
  return fetchModels(cfg);
129
136
  if (!resp.ok)
130
- throw new Error(`服务端返回 ${resp.status}`);
137
+ throw new Error(tr(`服务端返回 ${resp.status}`, `Server returned ${resp.status}`));
131
138
  const body = jsonRecord(await readJsonResponseCapped(resp));
132
139
  const announcement = body?.announcement;
133
140
  return {
@@ -147,10 +154,10 @@ export async function fetchUserEndpoints(cfg) {
147
154
  });
148
155
  }
149
156
  catch {
150
- throw new Error(`连不上 ${cfg.baseUrl}`);
157
+ throw new Error(tr(`连不上 ${cfg.baseUrl}`, `Can't reach ${cfg.baseUrl}`));
151
158
  }
152
159
  if (!resp.ok)
153
- throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status}`));
160
+ throw new Error(await errorMessageFromResponse(resp, tr(`服务端返回 ${resp.status}`, `Server returned ${resp.status}`)));
154
161
  const body = jsonRecord(await readJsonResponseCapped(resp));
155
162
  return Array.isArray(body?.endpoints) ? body.endpoints : [];
156
163
  }
@@ -171,7 +178,7 @@ export async function loadCustomEndpoints(cfg) {
171
178
  /** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
172
179
  export async function searchWeb(cfg, input) {
173
180
  if (!cfg.apiKey)
174
- throw new Error("没有配置 API Key");
181
+ throw new Error(tr("没有配置 API Key", "No API key configured"));
175
182
  let resp;
176
183
  try {
177
184
  resp = await authorizedFetch(cfg, `${cfg.baseUrl}/search`, {
@@ -187,15 +194,15 @@ export async function searchWeb(cfg, input) {
187
194
  catch (error) {
188
195
  if (input.signal?.aborted)
189
196
  throw error;
190
- throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`, { cause: error });
197
+ throw new Error(unreachableMessage(cfg.baseUrl), { cause: error });
191
198
  }
192
199
  if (resp.status === 401)
193
- throw new Error("登录已失效,请重新运行 u1s1 login");
200
+ throw new Error(sessionExpiredMessage());
194
201
  if (!resp.ok)
195
- throw new Error(await errorMessageFromResponse(resp, `搜索服务返回 ${resp.status},稍后再试`));
202
+ throw new Error(await errorMessageFromResponse(resp, tr(`搜索服务返回 ${resp.status},稍后再试`, `Search service returned ${resp.status}. Please try again later.`)));
196
203
  const body = jsonRecord(await readJsonResponseCapped(resp));
197
204
  if (!body || !Array.isArray(body.results))
198
- throw new Error("搜索服务返回格式不正确");
205
+ throw new Error(tr("搜索服务返回格式不正确", "The search service returned a malformed response"));
199
206
  const results = [];
200
207
  for (const value of body.results.slice(0, 10)) {
201
208
  const result = jsonRecord(value);
@@ -216,7 +223,7 @@ export async function searchWeb(cfg, input) {
216
223
  /** web_fetch 直连失败时的回退:网关用云端真浏览器渲染后转 markdown(能过挑战页)。 */
217
224
  export async function renderPage(cfg, url, signal) {
218
225
  if (!cfg.apiKey)
219
- throw new Error("没有配置 API Key");
226
+ throw new Error(tr("没有配置 API Key", "No API key configured"));
220
227
  let resp;
221
228
  try {
222
229
  resp = await authorizedFetch(cfg, `${cfg.baseUrl}/fetch`, {
@@ -232,15 +239,15 @@ export async function renderPage(cfg, url, signal) {
232
239
  catch (error) {
233
240
  if (signal?.aborted)
234
241
  throw error;
235
- throw new Error(`连不上 ${cfg.baseUrl}`, { cause: error });
242
+ throw new Error(tr(`连不上 ${cfg.baseUrl}`, `Can't reach ${cfg.baseUrl}`), { cause: error });
236
243
  }
237
244
  if (resp.status === 401)
238
- throw new Error("登录已失效,请重新运行 u1s1 login");
245
+ throw new Error(sessionExpiredMessage());
239
246
  if (!resp.ok)
240
- throw new Error(await errorMessageFromResponse(resp, `渲染服务返回 ${resp.status}`));
247
+ throw new Error(await errorMessageFromResponse(resp, tr(`渲染服务返回 ${resp.status}`, `Render service returned ${resp.status}`)));
241
248
  const body = jsonRecord(await readJsonResponseCapped(resp));
242
249
  if (!body || typeof body.url !== "string" || typeof body.markdown !== "string") {
243
- throw new Error("渲染服务返回格式不正确");
250
+ throw new Error(tr("渲染服务返回格式不正确", "The render service returned a malformed response"));
244
251
  }
245
252
  return { url: body.url, markdown: body.markdown };
246
253
  }
@@ -284,17 +291,17 @@ export async function readGeneratedImageResponse(resp) {
284
291
  try {
285
292
  const body = jsonRecord(await readJsonResponseCapped(resp, MAX_IMAGE_RESULT_BYTES));
286
293
  if (typeof body?.url !== "string") {
287
- throw new Error("服务端没有返回图片下载地址");
294
+ throw new Error(tr("服务端没有返回图片下载地址", "The server didn't return an image download URL"));
288
295
  }
289
296
  let parsed;
290
297
  try {
291
298
  parsed = new URL(body.url);
292
299
  }
293
300
  catch {
294
- throw new Error("服务端没有返回图片下载地址");
301
+ throw new Error(tr("服务端没有返回图片下载地址", "The server didn't return an image download URL"));
295
302
  }
296
303
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
297
- throw new Error("服务端没有返回图片下载地址");
304
+ throw new Error(tr("服务端没有返回图片下载地址", "The server didn't return an image download URL"));
298
305
  }
299
306
  return {
300
307
  url: body.url,
@@ -302,14 +309,15 @@ export async function readGeneratedImageResponse(resp) {
302
309
  };
303
310
  }
304
311
  catch (error) {
305
- throw new Error(`图片可能已经生成,但客户端下载结果响应失败: ${responseErrorDetail(error)}。` +
306
- "不要重新调用 generate_image,请升级 u1s1 后再试", { cause: error });
312
+ throw new Error(tr(`图片可能已经生成,但客户端下载结果响应失败: ${responseErrorDetail(error)}。` +
313
+ "不要重新调用 generate_image,请升级 u1s1 后再试", `The image may already have been generated, but reading the result response failed: ${responseErrorDetail(error)}. ` +
314
+ "Do not call generate_image again; update u1s1 and try again"), { cause: error });
307
315
  }
308
316
  }
309
317
  /** 生图走网关代理(方舟 key 只在服务端);返回 TOS 图片 URL(24h 有效),调用方应立即下载。 */
310
318
  export async function generateImage(cfg, req, signal) {
311
319
  if (!cfg.apiKey)
312
- throw new Error("没有配置 API Key");
320
+ throw new Error(tr("没有配置 API Key", "No API key configured"));
313
321
  let resp;
314
322
  try {
315
323
  resp = await authorizedFetch(cfg, `${cfg.baseUrl}/image`, {
@@ -325,17 +333,17 @@ export async function generateImage(cfg, req, signal) {
325
333
  catch (error) {
326
334
  if (signal?.aborted)
327
335
  throw error;
328
- throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`, { cause: error });
336
+ throw new Error(unreachableMessage(cfg.baseUrl), { cause: error });
329
337
  }
330
338
  if (resp.status === 401)
331
- throw new Error("登录已失效,请重新运行 u1s1 login");
339
+ throw new Error(sessionExpiredMessage());
332
340
  if (!resp.ok)
333
- throw new Error(await errorMessageFromResponse(resp, `图片生成服务返回 ${resp.status},稍后再试`));
341
+ throw new Error(await errorMessageFromResponse(resp, tr(`图片生成服务返回 ${resp.status},稍后再试`, `Image service returned ${resp.status}. Please try again later.`)));
334
342
  return readGeneratedImageResponse(resp);
335
343
  }
336
344
  export async function fetchMe(cfg) {
337
345
  if (!cfg.apiKey)
338
- throw new Error("没有配置 API Key");
346
+ throw new Error(tr("没有配置 API Key", "No API key configured"));
339
347
  let resp;
340
348
  try {
341
349
  resp = await authorizedFetch(cfg, `${cfg.baseUrl}/me`, {
@@ -344,14 +352,14 @@ export async function fetchMe(cfg) {
344
352
  });
345
353
  }
346
354
  catch {
347
- throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
355
+ throw new Error(unreachableMessage(cfg.baseUrl));
348
356
  }
349
357
  if (resp.status === 401)
350
- throw new Error("登录已失效,请重新运行 u1s1 login");
358
+ throw new Error(sessionExpiredMessage());
351
359
  if (!resp.ok)
352
- throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
360
+ throw new Error(await errorMessageFromResponse(resp, tr(`服务端返回 ${resp.status},稍后再试`, `Server returned ${resp.status}. Please try again later.`)));
353
361
  const body = await readJsonResponseCapped(resp);
354
362
  if (!body || typeof body !== "object")
355
- throw new Error("服务端账户响应格式不正确");
363
+ throw new Error(tr("服务端账户响应格式不正确", "The server returned a malformed account response"));
356
364
  return body;
357
365
  }
@@ -1,5 +1,6 @@
1
1
  import { PROVIDER_ID } from "./config.js";
2
2
  import { DEFAULT_MAX_SCORE } from "./bench-types.js";
3
+ import { isEnglish, tr } from "./i18n.js";
3
4
  const MAX_ANSWER_CHARS = 2_000;
4
5
  export function resultModelLabel(result) {
5
6
  return result.providerName === PROVIDER_ID
@@ -94,7 +95,7 @@ function appendScoreDetails(lines, results, maxScore) {
94
95
  const scored = results.filter((result) => result.scoreDetails?.length);
95
96
  if (scored.length === 0)
96
97
  return;
97
- lines.push("", "<details><summary>评分明细</summary>", "");
98
+ lines.push("", `<details><summary>${tr("评分明细", "Score details")}</summary>`, "");
98
99
  for (const result of scored) {
99
100
  lines.push(`**${resultModelLabel(result)}:** ${result.score}/${maxScore}`);
100
101
  for (const detail of result.scoreDetails) {
@@ -106,10 +107,10 @@ function appendScoreDetails(lines, results, maxScore) {
106
107
  }
107
108
  function appendAnswers(lines, results) {
108
109
  for (const result of results) {
109
- lines.push(`<details><summary>${resultModelLabel(result)} 的回答</summary>`, "", "```");
110
+ lines.push(`<details><summary>${tr(`${resultModelLabel(result)} 的回答`, `Answer from ${resultModelLabel(result)}`)}</summary>`, "", "```");
110
111
  lines.push(result.response.slice(0, MAX_ANSWER_CHARS));
111
112
  if (result.response.length > MAX_ANSWER_CHARS)
112
- lines.push("...(截断)");
113
+ lines.push(tr("...(截断)", "...(truncated)"));
113
114
  lines.push("```", "</details>", "");
114
115
  }
115
116
  }
@@ -124,7 +125,7 @@ function appendQuestion(lines, questionId, results) {
124
125
  String(result.tokensOut),
125
126
  result.error ? `❌ ${result.error.slice(0, 40)}` : "✅",
126
127
  ]);
127
- lines.push("```", renderTable(["模型", `得分/${maxScore}`, "延迟", "输出tok", "状态"], rows), "```");
128
+ lines.push("```", renderTable(isEnglish() ? ["Model", `Score/${maxScore}`, "Latency", "Out tok", "Status"] : ["模型", `得分/${maxScore}`, "延迟", "输出tok", "状态"], rows), "```");
128
129
  appendScoreDetails(lines, results, maxScore);
129
130
  appendAnswers(lines, results);
130
131
  }
@@ -138,16 +139,18 @@ function appendSummary(lines, results) {
138
139
  costStr(summary.totalCost),
139
140
  summary.errors > 0 ? `${errorRate(summary)}%` : "0%",
140
141
  ]);
141
- lines.push("## 汇总", "", "```", renderTable(["模型", "总分", "得分率", "平均延迟", "速度(tok/s)", "总花费", "错误率"], rows), "```");
142
+ lines.push(tr("## 汇总", "## Summary"), "", "```", renderTable(isEnglish()
143
+ ? ["Model", "Score", "Rate", "Avg latency", "Speed (tok/s)", "Total cost", "Error rate"]
144
+ : ["模型", "总分", "得分率", "平均延迟", "速度(tok/s)", "总花费", "错误率"], rows), "```");
142
145
  }
143
146
  export function generateReport(run) {
144
147
  const lines = [
145
- `# Bench 报告: ${run.suiteName}`,
146
- `运行时间: ${run.timestamp}`,
147
- `模型: ${run.models.join(", ")}`,
148
+ tr(`# Bench 报告: ${run.suiteName}`, `# Bench report: ${run.suiteName}`),
149
+ tr(`运行时间: ${run.timestamp}`, `Run at: ${run.timestamp}`),
150
+ tr(`模型: ${run.models.join(", ")}`, `Models: ${run.models.join(", ")}`),
148
151
  "",
149
152
  ];
150
- for (const [category, categoryResults] of groupResultsBy(run.results, (result) => result.category || "未分类")) {
153
+ for (const [category, categoryResults] of groupResultsBy(run.results, (result) => result.category || tr("未分类", "Uncategorized"))) {
151
154
  lines.push(`## ${category}`, "");
152
155
  for (const [questionId, questionResults] of groupResultsBy(categoryResults, (result) => result.questionId)) {
153
156
  appendQuestion(lines, questionId, questionResults);
package/dist/bench.js CHANGED
@@ -8,6 +8,7 @@ import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
8
8
  import { costStr, errorRate, formatDuration, generateReport, renderTable, scoreBar, scoreRate, sortByScore, summarizeByModel, tokPerSec, } from "./bench-report.js";
9
9
  import { scoreResponse } from "./bench-scoring.js";
10
10
  import { DEFAULT_MAX_SCORE, } from "./bench-types.js";
11
+ import { isEnglish, tr } from "./i18n.js";
11
12
  // ─── 工具 ───
12
13
  const BENCH_DIR = join(homedir(), ".u1s1", "bench");
13
14
  /** compare 子命令里单条回答的截断长度 */
@@ -43,7 +44,7 @@ function loadSuite(name) {
43
44
  if (found)
44
45
  return found;
45
46
  }
46
- throw new Error(`找不到 bench suite「${name}」`);
47
+ throw new Error(tr(`找不到 bench suite「${name}」`, `Bench suite "${name}" not found`));
47
48
  }
48
49
  /** 列出可用的 suite */
49
50
  function suiteSummaries(filePath, builtin) {
@@ -126,7 +127,7 @@ async function callModel(input) {
126
127
  : await fetch(`${baseUrl}/chat/completions`, request);
127
128
  const latencyMs = Math.round(performance.now() - start);
128
129
  if (!res.ok) {
129
- const errBody = await readResponseTextCapped(res, 64 * 1024).catch(() => "未知错误");
130
+ const errBody = await readResponseTextCapped(res, 64 * 1024).catch(() => tr("未知错误", "unknown error"));
130
131
  return { response: "", latencyMs, tokensIn: 0, tokensOut: 0, error: `HTTP ${res.status}: ${errBody.slice(0, 200)}` };
131
132
  }
132
133
  return { ...parseChatCompletion(await readJsonResponseCapped(res)), latencyMs };
@@ -160,12 +161,12 @@ function saveRun(run) {
160
161
  mkdirSync(BENCH_DIR, { recursive: true });
161
162
  const file = join(BENCH_DIR, `${run.id}.json`);
162
163
  writeFileSync(file, JSON.stringify(run, null, 2) + "\n");
163
- console.log(` 结果已保存:${file}`);
164
+ console.log(tr(` 结果已保存:${file}`, ` Results saved: ${file}`));
164
165
  }
165
166
  function loadRun(id) {
166
167
  const file = join(BENCH_DIR, `${id}.json`);
167
168
  if (!existsSync(file))
168
- throw new Error(`找不到运行记录「${id}」`);
169
+ throw new Error(tr(`找不到运行记录「${id}」`, `Run "${id}" not found`));
169
170
  return JSON.parse(readFileSync(file, "utf8"));
170
171
  }
171
172
  function listRuns() {
@@ -190,30 +191,30 @@ function failWithError(e) {
190
191
  }
191
192
  function printHelp() {
192
193
  console.log("");
193
- console.log(" u1s1 bench — 模型质量基准测试(带自动评分)");
194
+ console.log(tr(" u1s1 bench — 模型质量基准测试(带自动评分)", " u1s1 bench — model quality benchmark (auto-scored)"));
194
195
  console.log("");
195
- console.log(" 用法:");
196
- console.log(" u1s1 bench list 列出可用的测试套件");
197
- console.log(" u1s1 bench run <suite> [models..] 跑基准测试(--delay 秒数:题目间隔,限流渠道用)");
198
- console.log(" u1s1 bench report <run-id> 查看报告");
199
- console.log(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行");
196
+ console.log(tr(" 用法:", " Usage:"));
197
+ console.log(tr(" u1s1 bench list 列出可用的测试套件", " u1s1 bench list List available suites"));
198
+ console.log(tr(" u1s1 bench run <suite> [models..] 跑基准测试(--delay 秒数:题目间隔,限流渠道用)", " u1s1 bench run <suite> [models..] Run a benchmark (--delay <seconds> between questions, for rate-limited providers)"));
199
+ console.log(tr(" u1s1 bench report <run-id> 查看报告", " u1s1 bench report <run-id> Show a report"));
200
+ console.log(tr(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行", " u1s1 bench compare <run-id1> <run-id2> Compare two runs"));
200
201
  console.log("");
201
- console.log(" 示例:");
202
- console.log(" u1s1 bench run quick 用当前默认模型跑快速测试");
203
- console.log(" u1s1 bench run full deepseek pro 用多个模型跑全面测试");
204
- console.log(" u1s1 bench report latest 看最近一次报告");
202
+ console.log(tr(" 示例:", " Examples:"));
203
+ console.log(tr(" u1s1 bench run quick 用当前默认模型跑快速测试", " u1s1 bench run quick Quick suite with your default model"));
204
+ console.log(tr(" u1s1 bench run full deepseek pro 用多个模型跑全面测试", " u1s1 bench run full deepseek pro Full suite across several models"));
205
+ console.log(tr(" u1s1 bench report latest 看最近一次报告", " u1s1 bench report latest Show the latest report"));
205
206
  console.log("");
206
207
  }
207
208
  function listSuitesCommand() {
208
209
  const suites = listSuites();
209
210
  console.log("");
210
- console.log(" Bench 套件:");
211
+ console.log(tr(" Bench 套件:", " Bench suites:"));
211
212
  for (const s of suites) {
212
- const tag = s.builtin ? "(内置)" : "(自定义)";
213
+ const tag = s.builtin ? tr("(内置)", "(built-in)") : tr("(自定义)", "(custom)");
213
214
  console.log(` ${s.name.padEnd(12)} ${s.description} ${tag}`);
214
215
  }
215
216
  console.log("");
216
- console.log(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json");
217
+ console.log(tr(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json", " Put custom suites in ~/.u1s1/bench/<name>.json (same format as the built-in suite.json)"));
217
218
  console.log("");
218
219
  }
219
220
  /** 把模型名解析成走 u1s1 默认端点的调用目标 */
@@ -329,10 +330,10 @@ async function runBenchQuestion(input) {
329
330
  async function runBench(args) {
330
331
  const { suiteName, modelQueries, delayMs } = parseBenchRunArgs(args);
331
332
  if (delayMs > 0)
332
- console.log(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`);
333
+ console.log(tr(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`, ` ⏳ Delay between questions: ${(delayMs / 1000).toFixed(0)}s (for rate-limited providers)`));
333
334
  const cfg = loadConfig();
334
335
  if (!hasDeviceCredential(cfg)) {
335
- console.error(" 还没有登录,先 u1s1 login");
336
+ console.error(tr(" 还没有登录,先 u1s1 login", " Not signed in. Run u1s1 login first."));
336
337
  process.exit(1);
337
338
  }
338
339
  await loadCustomEndpoints(cfg);
@@ -343,8 +344,8 @@ async function runBench(args) {
343
344
  console.log("");
344
345
  console.log(` 📊 Bench: ${suite.name}`);
345
346
  console.log(` ${suite.description}`);
346
- console.log(` 题目数: ${suite.questions.length}`);
347
- console.log(` 模型: ${targets.map((t) => t.label).join(", ")}`);
347
+ console.log(tr(` 题目数: ${suite.questions.length}`, ` Questions: ${suite.questions.length}`));
348
+ console.log(tr(` 模型: ${targets.map((t) => t.label).join(", ")}`, ` Models: ${targets.map((t) => t.label).join(", ")}`));
348
349
  console.log("");
349
350
  const results = [];
350
351
  const total = suite.questions.length * targets.length;
@@ -368,14 +369,16 @@ async function runBench(args) {
368
369
  results,
369
370
  });
370
371
  printConsoleSummary(results);
371
- console.log(` 查看完整报告: u1s1 bench report ${runId}`);
372
+ console.log(tr(` 查看完整报告: u1s1 bench report ${runId}`, ` Full report: u1s1 bench report ${runId}`));
372
373
  console.log("");
373
374
  }
374
375
  /** 控制台里的简易评分汇总表 */
375
376
  function printConsoleSummary(results) {
376
377
  console.log("");
377
- console.log(" 📋 评分汇总:");
378
- const header = ["模型", "总分", "得分率", "平均延迟", "速度", "花费", "状态"];
378
+ console.log(tr(" 📋 评分汇总:", " 📋 Score summary:"));
379
+ const header = isEnglish()
380
+ ? ["Model", "Score", "Rate", "Avg latency", "Speed", "Cost", "Status"]
381
+ : ["模型", "总分", "得分率", "平均延迟", "速度", "花费", "状态"];
379
382
  const rows = sortByScore([...summarizeByModel(results, (r) => r.modelId)]).map(([model, s]) => [
380
383
  model,
381
384
  `${s.totalScore}/${s.totalMax}`,
@@ -383,7 +386,7 @@ function printConsoleSummary(results) {
383
386
  formatDuration(s.totalLatency / s.total),
384
387
  tokPerSec(s),
385
388
  costStr(s.totalCost),
386
- s.errors > 0 ? `${errorRate(s)}%失败` : "✅全部通过",
389
+ s.errors > 0 ? tr(`${errorRate(s)}%失败`, `${errorRate(s)}% failed`) : tr("✅全部通过", "✅ all passed"),
387
390
  ]);
388
391
  console.log(" " + renderTable(header, rows));
389
392
  console.log("");
@@ -395,7 +398,7 @@ function showReport(args) {
395
398
  try {
396
399
  const id = runIdArg === "latest" ? findLatestRun() : runIdArg;
397
400
  if (!id)
398
- throw new Error("还没有运行记录");
401
+ throw new Error(tr("还没有运行记录", "No runs yet"));
399
402
  run = loadRun(id);
400
403
  }
401
404
  catch (e) {
@@ -405,7 +408,7 @@ function showReport(args) {
405
408
  const reportFile = join(BENCH_DIR, `${run.id}.md`);
406
409
  writeFileSync(reportFile, report);
407
410
  console.log(report);
408
- console.log(` 报告已保存: ${reportFile}`);
411
+ console.log(tr(` 报告已保存: ${reportFile}`, ` Report saved: ${reportFile}`));
409
412
  }
410
413
  /** 两次运行共同拥有的题目 id */
411
414
  function commonQuestionIds(a, b) {
@@ -428,7 +431,7 @@ function compareRuns(runId1, runId2) {
428
431
  failWithError(e);
429
432
  }
430
433
  console.log("");
431
- console.log(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`);
434
+ console.log(tr(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`, ` Comparing ${run1.suiteName} (${run1.timestamp}) vs ${run2.suiteName} (${run2.timestamp})`));
432
435
  console.log("");
433
436
  for (const qId of commonQuestionIds(run1, run2)) {
434
437
  console.log(` ── ${qId} ──`);
@@ -452,13 +455,13 @@ export async function benchCommand(args) {
452
455
  return showReport(args.slice(1));
453
456
  if (sub === "compare") {
454
457
  if (!args[1] || !args[2]) {
455
- console.log(" 用法: u1s1 bench compare <run-id1> <run-id2>");
458
+ console.log(tr(" 用法: u1s1 bench compare <run-id1> <run-id2>", " Usage: u1s1 bench compare <run-id1> <run-id2>"));
456
459
  return;
457
460
  }
458
461
  return compareRuns(args[1], args[2]);
459
462
  }
460
- console.error(` 未知子命令: ${sub}`);
461
- console.log(" u1s1 bench help 查看用法");
463
+ console.error(tr(` 未知子命令: ${sub}`, ` Unknown subcommand: ${sub}`));
464
+ console.log(tr(" u1s1 bench help 查看用法", " Run u1s1 bench help for usage"));
462
465
  process.exit(1);
463
466
  }
464
467
  function findLatestRun() {
package/dist/brand.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  export declare const BRAND_NAME = "u1s1";
3
3
  export declare const BRAND_CN = "\u6709\u4E00\u8BF4\u4E00";
4
- export declare const BRAND_TAGLINE = "\u8BF4\u4EBA\u8BDD\u7684 AI \u7F16\u7A0B\u642D\u5B50";
4
+ /** 中文品牌名只在中国站界面出现;国际站只叫 u1s1。 */
5
+ export declare function brandTagline(): string;
5
6
  /**
6
7
  * 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
7
8
  * 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。