u1s1-cli 1.11.8 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agent-setup.d.ts +19 -3
  2. package/dist/agent-setup.js +85 -10
  3. package/dist/api.js +42 -34
  4. package/dist/bench-report.js +12 -9
  5. package/dist/bench.js +34 -31
  6. package/dist/brand.d.ts +2 -1
  7. package/dist/brand.js +38 -12
  8. package/dist/config.d.ts +11 -1
  9. package/dist/config.js +25 -8
  10. package/dist/context-usage.js +12 -9
  11. package/dist/deploy.js +49 -45
  12. package/dist/device-auth.js +7 -5
  13. package/dist/error-humanize.d.ts +1 -0
  14. package/dist/error-humanize.js +18 -6
  15. package/dist/feedback.js +48 -31
  16. package/dist/i18n.d.ts +21 -0
  17. package/dist/i18n.js +46 -0
  18. package/dist/import/index.js +64 -28
  19. package/dist/import/skills.js +72 -41
  20. package/dist/import/util.js +3 -2
  21. package/dist/import/write.js +2 -1
  22. package/dist/index.js +140 -49
  23. package/dist/login.js +39 -28
  24. package/dist/loop.js +18 -12
  25. package/dist/mcp/client.js +15 -14
  26. package/dist/mcp/command.d.ts +2 -2
  27. package/dist/mcp/command.js +109 -69
  28. package/dist/mcp/config.d.ts +22 -6
  29. package/dist/mcp/config.js +0 -0
  30. package/dist/mcp/extension.js +5 -3
  31. package/dist/mcp/tools.d.ts +2 -0
  32. package/dist/mcp/tools.js +10 -5
  33. package/dist/model.js +11 -10
  34. package/dist/nudges.d.ts +8 -2
  35. package/dist/nudges.js +22 -3
  36. package/dist/search-tools.js +7 -6
  37. package/dist/shell-doctor.js +9 -8
  38. package/dist/subagent.js +5 -4
  39. package/dist/tools.js +53 -37
  40. package/dist/update.js +29 -28
  41. package/dist/usage.d.ts +1 -1
  42. package/dist/usage.js +96 -43
  43. package/dist/web.js +2 -1
  44. package/dist/workflow/runner.js +33 -29
  45. package/dist/workflow/templates.js +70 -45
  46. package/dist/workflow/tool.js +15 -12
  47. package/package.json +4 -4
  48. package/scripts/patch-pi.js +347 -278
  49. package/scripts/render-regression.mjs +55 -4
package/dist/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。
package/dist/brand.js CHANGED
@@ -1,9 +1,18 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join, resolve, sep } from "node:path";
3
+ import { activeSite } from "./config.js";
4
+ import { isEnglish, tr } from "./i18n.js";
3
5
  import { siteLinks } from "./site.js";
4
6
  export const BRAND_NAME = "u1s1";
5
7
  export const BRAND_CN = "有一说一";
6
- export const BRAND_TAGLINE = "说人话的 AI 编程搭子";
8
+ /** 中文品牌名只在中国站界面出现;国际站只叫 u1s1。 */
9
+ export function brandTagline() {
10
+ return tr("说人话的 AI 编程搭子", "the plain-spoken AI coding assistant");
11
+ }
12
+ /** 横幅里品牌名后面那一行:中国站「有一说一 · 标语」,国际站只有英文标语。 */
13
+ function brandSubtitle() {
14
+ return isEnglish() ? "Plain-spoken AI coding assistant" : `${BRAND_CN} · ${brandTagline()}`;
15
+ }
7
16
  /**
8
17
  * 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
9
18
  * 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。
@@ -30,10 +39,10 @@ export function cwdAdvice(cwd, dirs = {}) {
30
39
  const here = resolve(cwd);
31
40
  const within = (dir) => here === dir || here.startsWith(dir + sep);
32
41
  if (within(configDir)) {
33
- return "当前在 u1s1 的配置目录里(存着登录凭据和设置),模型的文件工具会直接看到它们;建议 /exit 后 cd 到你的项目文件夹再运行 u1s1";
42
+ return tr("当前在 u1s1 的配置目录里(存着登录凭据和设置),模型的文件工具会直接看到它们;建议 /exit 后 cd 到你的项目文件夹再运行 u1s1", "You're inside u1s1's config folder (it holds your sign-in credentials and settings), so the model's file tools can see them. /exit, cd to your project folder, and run u1s1 again.");
34
43
  }
35
44
  if (here === home) {
36
- return "当前在用户主目录,模型找文件会翻遍整个主目录,又慢又容易改错地方;建议 /exit 后 cd 到项目文件夹再运行 u1s1";
45
+ return tr("当前在用户主目录,模型找文件会翻遍整个主目录,又慢又容易改错地方;建议 /exit 后 cd 到项目文件夹再运行 u1s1", "You're in your home folder, so the model will search all of it: slow, and easy to edit the wrong files. /exit, cd to a project folder, and run u1s1 again.");
37
46
  }
38
47
  return undefined;
39
48
  }
@@ -72,13 +81,25 @@ function announcementLine(theme, a) {
72
81
  }
73
82
  /** 空目录首跑引导:不让新手面对空白输入框想 prompt,给几句能照抄的开场白。 */
74
83
  function starterLines(theme) {
75
- return [
84
+ if (isEnglish()) {
85
+ return [
86
+ ` ${theme.fg("muted", "Empty folder? Just describe what you want to build, for example:")}`,
87
+ ` ${theme.fg("text", "\"Build a personal landing page with a short bio\"")}`,
88
+ ` ${theme.fg("text", "\"Make a birthday card page for a friend with a little animation\"")}`,
89
+ ` ${theme.fg("text", "\"Write a small tool that renames the photos in this folder by date\"")}`,
90
+ ];
91
+ }
92
+ const lines = [
76
93
  ` ${theme.fg("muted", "这个文件夹还是空的?把想做的直接说出来就行,比如:")}`,
77
94
  ` ${theme.fg("text", "「做一个自我介绍网页,做完帮我发布出去」")}`,
78
95
  ` ${theme.fg("text", "「做一个给朋友的生日祝福页面,要有点小动画」")}`,
79
96
  ` ${theme.fg("text", "「写一个把文件夹里照片按日期重命名的小工具」")}`,
80
- ` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`,
81
97
  ];
98
+ // 国际站没有网页托管(u1s1 deploy),不提发布
99
+ if (activeSite().id !== "intl") {
100
+ lines.push(` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`);
101
+ }
102
+ return lines;
82
103
  }
83
104
  /**
84
105
  * Startup hero, responsive to terminal width:
@@ -87,13 +108,18 @@ function starterLines(theme) {
87
108
  export function renderBrandHeader(theme, input) {
88
109
  const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${input.version}`)) +
89
110
  (input.notice ? ` ${theme.fg("accent", input.notice)}` : "");
90
- const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
91
- const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}${input.account ? ` · 账号 ${input.account}` : ""}`);
92
- const hints = theme.fg("dim", "/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断");
111
+ const brand = theme.fg("muted", brandSubtitle());
112
+ const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}${input.account ? tr(` · 账号 ${input.account}`, ` · account ${input.account}`) : ""}`);
113
+ const hints = theme.fg("dim", tr("/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断", "/help · Ctrl+V image · Shift+Enter newline · Esc stop"));
93
114
  const starter = input.starterTips ? starterLines(theme) : [];
94
115
  const warning = input.cwdWarning ? [` ${theme.fg("warning", "⚠")} ${theme.fg("warning", input.cwdWarning)}`] : [];
95
116
  if (input.width < ART_WIDTH + 4) {
96
- const lines = ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`, ` ${theme.fg("dim", "Ctrl+V 粘贴图片")}`];
117
+ const lines = [
118
+ "",
119
+ ` ${theme.fg("accent", "✻")} ${name}${isEnglish() ? "" : ` ${theme.fg("muted", BRAND_CN)}`}`,
120
+ ` ${dir}`,
121
+ ` ${theme.fg("dim", tr("Ctrl+V 粘贴图片", "Ctrl+V to paste images"))}`,
122
+ ];
97
123
  if (input.announcement)
98
124
  lines.push(` ${announcementLine(theme, input.announcement)}`);
99
125
  if (warning.length)
@@ -103,8 +129,8 @@ export function renderBrandHeader(theme, input) {
103
129
  return [...lines, ""];
104
130
  }
105
131
  const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
106
- // widest info row (hints) needs 46 cols beside the 28-col wordmark
107
- if (input.width < ART_WIDTH + 46) {
132
+ // widest info row (hints) needs 46 cols beside the 28-col wordmark (English hints are wider: 56)
133
+ if (input.width < ART_WIDTH + (isEnglish() ? 56 : 46)) {
108
134
  const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ` ${hints}`];
109
135
  if (input.announcement)
110
136
  lines.push(` ${announcementLine(theme, input.announcement)}`);
@@ -134,6 +160,6 @@ export function printConsoleBanner(version) {
134
160
  for (const line of HERO_ART)
135
161
  console.log(` ${line}`);
136
162
  console.log("");
137
- console.log(` ${BRAND_NAME} v${version} — ${BRAND_CN},${BRAND_TAGLINE}`);
163
+ console.log(tr(` ${BRAND_NAME} v${version} — ${BRAND_CN},${brandTagline()}`, ` ${BRAND_NAME} v${version} — ${brandTagline()}`));
138
164
  console.log("");
139
165
  }
package/dist/config.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface ApiModel {
39
39
  max_tokens: number;
40
40
  /** Older gateways omit this field; missing means unknown coverage. */
41
41
  free_package_eligible?: boolean;
42
+ /** 本站默认模型(网关按站点定:中国站 deepseek-flash,国际站另配);老网关也会发。 */
43
+ is_default?: boolean;
42
44
  /**
43
45
  * 网关建议的自动压缩预算(token);缺失/null 时 CLI 用内置默认 200K。
44
46
  * 用户设置 compaction.maxContextTokens 优先级更高(见 effectiveContextWindow)。
@@ -81,6 +83,8 @@ export interface ModelDef {
81
83
  cost: ModelCost | null;
82
84
  /** 网关下发的免费用量包覆盖标记;老网关缺失时为 undefined(不派生 note)。 */
83
85
  freeEligible?: boolean;
86
+ /** 网关标的本站默认模型(is_default)。 */
87
+ isDefault?: boolean;
84
88
  /** 网关按模型下发的自动压缩预算(token);缺失时用 DEFAULT_COMPACTION_BUDGET_TOKENS。 */
85
89
  compactAtTokens?: number;
86
90
  note: string;
@@ -101,7 +105,13 @@ export declare function effectiveContextWindow(m: Pick<ModelDef, "contextWindow"
101
105
  export declare const MODELS: ModelDef[];
102
106
  /** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
103
107
  export declare function setModelsFromApi(apiModels: ModelDef[]): void;
104
- export declare const DEFAULT_MODEL_ID: string;
108
+ /** 离线兜底列表的首个模型(中国站默认);没拉到目录时用它。 */
109
+ export declare const FALLBACK_DEFAULT_MODEL_ID: string;
110
+ /**
111
+ * 当前默认模型:优先网关标的 is_default(两站默认模型不同,国际站目录里没有 deepseek-flash),
112
+ * 老网关没标时退回兜底 id(在目录里的话),再退回目录第一个。
113
+ */
114
+ export declare function defaultModelId(): string;
105
115
  export declare function resolveModel(nameOrAlias: string): ModelDef | undefined;
106
116
  /** provider 注册键就是云端的端点 id(ep+uuid);models.json 清理旧条目按此形状识别。 */
107
117
  export declare const ENDPOINT_ID_RE: RegExp;
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { resetLang, setLangResolver, tr } from "./i18n.js";
7
8
  import { migrateStoredBaseUrl, SITES, siteOf } from "./site.js";
8
9
  const require = createRequire(import.meta.url);
9
10
  export const VERSION = require("../package.json").version;
@@ -80,6 +81,7 @@ export function apiModelToDef(m) {
80
81
  cacheWrite: 0,
81
82
  },
82
83
  freeEligible: typeof m.free_package_eligible === "boolean" ? m.free_package_eligible : undefined,
84
+ isDefault: m.is_default === true ? true : undefined,
83
85
  compactAtTokens: positiveInteger(m.compact_at_tokens),
84
86
  note: "",
85
87
  };
@@ -126,7 +128,8 @@ export const MODELS = [
126
128
  contextWindow: 1_048_576,
127
129
  maxTokens: 384_000,
128
130
  cost: null,
129
- note: "默认 · 免费用量包额度内免费,日常写代码首选,支持看图",
131
+ // 离线兜底列表的说明按语言现取(模块加载时还不知道站点)
132
+ get note() { return tr("默认 · 免费用量包额度内免费,日常写代码首选,支持看图", "Default · everyday coding, supports images"); },
130
133
  },
131
134
  {
132
135
  id: "x-ai/grok-4.6",
@@ -137,7 +140,7 @@ export const MODELS = [
137
140
  contextWindow: 500_000,
138
141
  maxTokens: 65_536,
139
142
  cost: null,
140
- note: "更强 · 但烧额度快约 20 倍,难题再用",
143
+ get note() { return tr("更强 · 但烧额度快约 20 倍,难题再用", "Stronger · about 20× the cost, for hard problems"); },
141
144
  },
142
145
  ];
143
146
  /**
@@ -146,21 +149,23 @@ export const MODELS = [
146
149
  * 让人切模型前就知道「这个不走免费包 / 大概贵多少」。
147
150
  */
148
151
  function deriveModelNote(m, base) {
152
+ // 国际站没有免费包:不提免费包,只给价格倍数
149
153
  if (m.freeEligible === true)
150
- return "免费用量包可抵扣";
154
+ return tr("免费用量包可抵扣", "");
151
155
  if (m.freeEligible === false) {
152
156
  const blended = m.cost ? (m.cost.input + m.cost.output) / 2 : 0;
153
157
  const baseBlended = base?.cost ? (base.cost.input + base.cost.output) / 2 : 0;
154
158
  const mult = baseBlended > 0 ? Math.round(blended / baseBlended) : 0;
155
159
  return mult >= 2
156
- ? `不走免费包 · 费用约为默认模型 ${mult} 倍`
157
- : "不走免费包,用余额或全模型包";
160
+ ? tr(`不走免费包 · 费用约为默认模型 ${mult} 倍`, `about ${mult}× the default model's price`)
161
+ : tr("不走免费包,用余额或全模型包", "");
158
162
  }
159
163
  return "";
160
164
  }
161
165
  /** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
162
166
  export function setModelsFromApi(apiModels) {
163
- const base = apiModels.find((m) => m.id === DEFAULT_MODEL_ID)
167
+ const base = apiModels.find((m) => m.isDefault)
168
+ ?? apiModels.find((m) => m.id === FALLBACK_DEFAULT_MODEL_ID)
164
169
  ?? apiModels.find((m) => m.freeEligible === true);
165
170
  for (const m of apiModels) {
166
171
  if (!m.note)
@@ -169,7 +174,17 @@ export function setModelsFromApi(apiModels) {
169
174
  MODELS.length = 0;
170
175
  MODELS.push(...apiModels);
171
176
  }
172
- export const DEFAULT_MODEL_ID = MODELS[0].id;
177
+ /** 离线兜底列表的首个模型(中国站默认);没拉到目录时用它。 */
178
+ export const FALLBACK_DEFAULT_MODEL_ID = MODELS[0].id;
179
+ /**
180
+ * 当前默认模型:优先网关标的 is_default(两站默认模型不同,国际站目录里没有 deepseek-flash),
181
+ * 老网关没标时退回兜底 id(在目录里的话),再退回目录第一个。
182
+ */
183
+ export function defaultModelId() {
184
+ return MODELS.find((m) => m.isDefault)?.id
185
+ ?? (MODELS.some((m) => m.id === FALLBACK_DEFAULT_MODEL_ID) ? FALLBACK_DEFAULT_MODEL_ID : MODELS[0]?.id)
186
+ ?? FALLBACK_DEFAULT_MODEL_ID;
187
+ }
173
188
  export function resolveModel(nameOrAlias) {
174
189
  const q = nameOrAlias.trim().toLowerCase();
175
190
  return MODELS.find((m) => m.id.toLowerCase() === q || m.aliases.includes(q));
@@ -312,7 +327,7 @@ export function resolvePreferredModel(cfg) {
312
327
  if (refValid(ref))
313
328
  return ref;
314
329
  }
315
- return { provider: PROVIDER_ID, id: DEFAULT_MODEL_ID };
330
+ return { provider: PROVIDER_ID, id: defaultModelId() };
316
331
  }
317
332
  export function loadConfig() {
318
333
  return configFromFile((readJsonFile(configFile) ?? {}));
@@ -359,6 +374,7 @@ export function configFromFile(file, env = process.env, currentCnApiBase = SITES
359
374
  export function activeSite() {
360
375
  return siteOf(loadConfig());
361
376
  }
377
+ setLangResolver(() => (activeSite().id === "intl" ? "en" : "zh"));
362
378
  /**
363
379
  * 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
364
380
  * (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
@@ -419,6 +435,7 @@ export function stripEnvOverrides(cfg, file, env = process.env) {
419
435
  export function saveConfig(cfg) {
420
436
  const file = (readJsonFile(configFile) ?? {});
421
437
  writePrivateFile(configFile, JSON.stringify(stripEnvOverrides(cfg, file), null, 2) + "\n");
438
+ resetLang();
422
439
  }
423
440
  /** Persist the user's preferred model to both stores. */
424
441
  export function persistPreferredModel(cfg, provider, modelId) {
@@ -1,4 +1,5 @@
1
1
  import { CUSTOM_ENDPOINTS, DEFAULT_COMPACTION_BUDGET_TOKENS, MODELS, PROVIDER_ID, } from "./config.js";
2
+ import { tr } from "./i18n.js";
2
3
  /** 1_048_576 → "1M",200_000 → "200K",184_000 → "184K",12_345 → "12.3K"。 */
3
4
  export function formatTokens(n) {
4
5
  if (n >= 1_000_000) {
@@ -23,25 +24,27 @@ export function contextEntryLines(input) {
23
24
  const def = findModelDef(input.model);
24
25
  const usage = input.usage;
25
26
  if (!usage || usage.contextWindow <= 0) {
26
- return ["当前模型没有上下文窗口信息,无法统计。"];
27
+ return [tr("当前模型没有上下文窗口信息,无法统计。", "The current model has no context window info, so usage can't be measured.")];
27
28
  }
28
29
  const budget = usage.contextWindow;
29
30
  const modelWindow = def?.contextWindow ?? 0;
30
31
  const lines = [];
31
32
  if (usage.tokens === null || usage.percent === null) {
32
- lines.push(`当前上下文:未知(刚压缩过,等下一次回复后才能统计) · 压缩预算 ${formatTokens(budget)}`);
33
+ lines.push(tr(`当前上下文:未知(刚压缩过,等下一次回复后才能统计) · 压缩预算 ${formatTokens(budget)}`, `Current context: unknown (just compacted; available after the next reply) · compaction budget ${formatTokens(budget)}`));
33
34
  }
34
35
  else {
35
- lines.push(`当前上下文:约 ${formatTokens(usage.tokens)} tokens,占压缩预算 ${formatTokens(budget)} 的 ${usage.percent.toFixed(1)}%`);
36
+ lines.push(tr(`当前上下文:约 ${formatTokens(usage.tokens)} tokens,占压缩预算 ${formatTokens(budget)} 的 ${usage.percent.toFixed(1)}%`, `Current context: ~${formatTokens(usage.tokens)} tokens, ${usage.percent.toFixed(1)}% of the ${formatTokens(budget)} compaction budget`));
36
37
  }
37
38
  const trigger = Math.max(0, budget - input.reserveTokens);
38
39
  const budgetOrigin = input.budgetSetting !== undefined
39
- ? "来自你的设置"
40
+ ? tr("来自你的设置", "from your settings")
40
41
  : def?.compactAtTokens
41
- ? "由 u1s1 按模型下发"
42
- : `默认 ${formatTokens(DEFAULT_COMPACTION_BUDGET_TOKENS)}`;
43
- const windowNote = modelWindow > budget ? `(模型窗口 ${formatTokens(modelWindow)},预算${budgetOrigin})` : `(${budgetOrigin})`;
44
- lines.push(`超过约 ${formatTokens(trigger)} tokens 会自动压缩 ${windowNote};/compact 可随时手动压缩`);
45
- lines.push("改预算:~/.u1s1/agent/settings.json 里 compaction.maxContextTokens(0 = 不限,按模型窗口);只影响自动压缩时机,不改模型能力");
42
+ ? tr("由 u1s1 按模型下发", "set by u1s1 for this model")
43
+ : tr(`默认 ${formatTokens(DEFAULT_COMPACTION_BUDGET_TOKENS)}`, `default ${formatTokens(DEFAULT_COMPACTION_BUDGET_TOKENS)}`);
44
+ const windowNote = modelWindow > budget
45
+ ? tr(`(模型窗口 ${formatTokens(modelWindow)},预算${budgetOrigin})`, `(model window ${formatTokens(modelWindow)}; budget ${budgetOrigin})`)
46
+ : `(${budgetOrigin})`;
47
+ lines.push(tr(`超过约 ${formatTokens(trigger)} tokens 会自动压缩 ${windowNote};/compact 可随时手动压缩`, `Auto-compacts above ~${formatTokens(trigger)} tokens ${windowNote}; run /compact to compact any time`));
48
+ lines.push(tr("改预算:~/.u1s1/agent/settings.json 里 compaction.maxContextTokens(0 = 不限,按模型窗口);只影响自动压缩时机,不改模型能力", "To change the budget, set compaction.maxContextTokens in ~/.u1s1/agent/settings.json (0 = no limit, use the model window). It only affects when auto-compaction kicks in, not what the model can do."));
46
49
  return lines;
47
50
  }