u1s1-cli 1.7.0 → 1.7.1

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.
@@ -377,6 +377,10 @@ export default async function (pi) {
377
377
  // ---- 汇总统计 ----
378
378
  let counts = {};
379
379
  let errors = 0;
380
+ // 工具调用被折叠后,这一行摘要是新手了解「AI 刚才做了什么」的唯一入口,内置工具名给人话
381
+ const TOOL_SUMMARY_LABELS: Record<string, string> = {
382
+ read: "读文件", write: "写文件", edit: "改文件", bash: "跑命令", grep: "搜内容", find: "找文件", ls: "列目录",
383
+ };
380
384
  pi.on("agent_start", async () => {
381
385
  counts = {};
382
386
  errors = 0;
@@ -392,14 +396,14 @@ export default async function (pi) {
392
396
  if (total === 0) return;
393
397
  const parts = Object.entries(counts)
394
398
  .sort((a, b) => b[1] - a[1])
395
- .map(([name, n]) => name + "×" + n);
399
+ .map(([name, n]) => (TOOL_SUMMARY_LABELS[name] ?? name) + "×" + n);
396
400
  if (errors > 0) parts.push("✗ " + errors);
397
401
  pi.appendEntry("tool-summary", { total, summary: parts.join(" · ") });
398
402
  });
399
403
  pi.registerEntryRenderer("tool-summary", (entry, _opts, theme) => {
400
404
  const d = entry.data;
401
405
  let text = theme.fg("muted", "⚙ ");
402
- text += theme.fg("toolTitle", theme.bold(d.total + " tool" + (d.total > 1 ? "s" : "")));
406
+ text += theme.fg("toolTitle", theme.bold(d.total + " 次工具调用"));
403
407
  text += theme.fg("dim", " · " + d.summary);
404
408
  return new Text(text, 0, 0);
405
409
  });
package/dist/api.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface MeResponse {
24
24
  packages?: MePackage[];
25
25
  /** 可领取的免费包:'first' 首月包 / 'renew' 年度包 / null 已有生效中的 */
26
26
  free_claim?: "first" | "renew" | null;
27
+ /** 支付通道是否开放;老网关没有该字段,按开放处理(保留充值入口) */
28
+ pay_enabled?: boolean;
27
29
  }
28
30
  export interface MePackage {
29
31
  id: number;
@@ -63,6 +65,9 @@ export interface ModelsResponse {
63
65
  /** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
64
66
  export declare class AuthError extends Error {
65
67
  }
68
+ /** 403:登录着但被服务端拒绝(封禁/停用/设备不受信任),重新登录也没用,进 TUI 每条消息都会失败。 */
69
+ export declare class AccessDeniedError extends Error {
70
+ }
66
71
  export declare function fetchModels(cfg: CliConfig): Promise<ModelsResponse>;
67
72
  /**
68
73
  * 会话内公告轮询用的轻量接口:免鉴权、服务端有缓存,回的就是 /v1/models 里的 announcement
package/dist/api.js CHANGED
@@ -57,6 +57,9 @@ export async function readJsonResponseCapped(resp, maxBytes = MAX_API_RESPONSE_B
57
57
  /** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
58
58
  export class AuthError extends Error {
59
59
  }
60
+ /** 403:登录着但被服务端拒绝(封禁/停用/设备不受信任),重新登录也没用,进 TUI 每条消息都会失败。 */
61
+ export class AccessDeniedError extends Error {
62
+ }
60
63
  /** 网关错误壳统一是 { error: { message } };解析不出来时回退到状态码提示。 */
61
64
  async function errorMessageFromResponse(resp, fallback) {
62
65
  const body = jsonRecord(await readJsonResponseCapped(resp, MAX_API_ERROR_BYTES).catch(() => null));
@@ -76,6 +79,8 @@ export async function fetchModels(cfg) {
76
79
  }
77
80
  if (resp.status === 401)
78
81
  throw new AuthError("登录已失效,请重新运行 u1s1 login");
82
+ if (resp.status === 403)
83
+ throw new AccessDeniedError(await errorMessageFromResponse(resp, "账号当前无法使用 u1s1"));
79
84
  if (!resp.ok)
80
85
  throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
81
86
  const body = await readJsonResponseCapped(resp);
package/dist/brand.js CHANGED
@@ -54,7 +54,7 @@ function starterLines(theme) {
54
54
  ` ${theme.fg("text", "「做一个自我介绍网页,做完帮我发布出去」")}`,
55
55
  ` ${theme.fg("text", "「做一个给朋友的生日祝福页面,要有点小动画」")}`,
56
56
  ` ${theme.fg("text", "「写一个把文件夹里照片按日期重命名的小工具」")}`,
57
- ` ${theme.fg("muted", "网页做好后,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`,
57
+ ` ${theme.fg("muted", "网页做好后,输入 /exit 回到终端,运行 u1s1 deploy --public 一键上线,拿到可分享的链接发给朋友")}`,
58
58
  ];
59
59
  }
60
60
  /**
@@ -55,10 +55,30 @@ export function extractRequestId(raw) {
55
55
  const id = payload?.err["request_id"];
56
56
  return typeof id === "string" && id.length > 0 && id.length <= 128 ? id : undefined;
57
57
  }
58
+ /**
59
+ * 非 JSON 的失败体(Cloudflare 5xx HTML 页、纯文本网关错误)原样直出就是
60
+ * `Error: 503 <!DOCTYPE html>…` 一大坨。只认得出状态码就给一句人话,
61
+ * 状态码数字保留在尾巴里,pi 的重试分类(5xx/429 可重试)不受影响。
62
+ */
63
+ function humanizeOpaqueError(raw) {
64
+ const status = /^\s*(\d{3})\b/.exec(raw)?.[1];
65
+ if (!status)
66
+ return undefined;
67
+ const opaque = /<\s*(?:!doctype|html)/i.test(raw) || raw.length > 400;
68
+ if (!opaque)
69
+ return undefined;
70
+ const n = Number(status);
71
+ const body = n >= 500
72
+ ? "服务暂时不可用,稍等再试;持续出现可看状态页 https://u1s1.io/status"
73
+ : n === 429
74
+ ? "请求太频繁,稍等再试"
75
+ : "请求被拒绝,重试仍失败可运行 u1s1 feedback 反馈";
76
+ return `${body} (HTTP ${status})`;
77
+ }
58
78
  export function humanizeModelError(raw, now = Date.now()) {
59
79
  const payload = parseErrorPayload(raw);
60
80
  if (!payload)
61
- return undefined;
81
+ return humanizeOpaqueError(raw);
62
82
  const { err, jsonStart } = payload;
63
83
  const message = typeof err["message"] === "string" ? err["message"].trim() : "";
64
84
  if (!message)
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { registerLoopCommand } from "./loop.js";
9
9
  import { ensureSearchTools } from "./search-tools.js";
10
10
  import { ensureUsableShell } from "./shell-doctor.js";
11
11
  import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
12
- import { AuthError, fetchModels, loadCustomEndpoints, readJsonResponseCapped } from "./api.js";
12
+ import { AccessDeniedError, AuthError, fetchModels, loadCustomEndpoints, readJsonResponseCapped } from "./api.js";
13
13
  import { ensureSigningProxy } from "./device-auth.js";
14
14
  import { installChildEnvGuard } from "./secret-env.js";
15
15
  import { DEPLOY_HINT_TEXT, markNudge, shouldShowDeployHint } from "./nudges.js";
@@ -93,6 +93,7 @@ function installPendingUpdate() {
93
93
  return;
94
94
  const { latest, installCmd } = pendingUpdate;
95
95
  console.log(`\n⬆ 正在更新到 v${latest}…`);
96
+ console.log(" (可能需要几分钟,期间请不要关闭窗口或按 Ctrl+C,中途打断会装出半截)");
96
97
  try {
97
98
  execSync(installCmd, { stdio: "inherit" });
98
99
  console.log(`✅ 已更新到 v${latest},下次运行 u1s1 生效。`);
@@ -200,6 +201,12 @@ async function runAgent(cfg, args) {
200
201
  return undefined;
201
202
  });
202
203
  }
204
+ else if (e instanceof AccessDeniedError) {
205
+ // 封禁/停用/设备不受信任:进 TUI 后每条消息都会失败,不如现在就把原因说清楚并退出
206
+ console.error(` ${e.message}`);
207
+ console.error(" 这个账号当前无法使用 u1s1。如有疑问可发邮件到 contact@u1s1.io,或在 https://u1s1.io/dashboard 提交工单。");
208
+ process.exit(1);
209
+ }
203
210
  else {
204
211
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
205
212
  }
@@ -543,6 +550,12 @@ async function run() {
543
550
  process.exitCode = 1;
544
551
  return;
545
552
  }
553
+ // `u1s1 login --help` 不该真的去登录、`logout --help` 不该真的退出:先看是不是在问用法
554
+ if (cmd && SIMPLE_COMMAND_HELP[cmd] && (args[1] === "--help" || args[1] === "-h")) {
555
+ for (const line of SIMPLE_COMMAND_HELP[cmd])
556
+ console.log(line);
557
+ return;
558
+ }
546
559
  if (cmd === "deploy") {
547
560
  const { deployCommand, printDeployHelp } = await import("./deploy.js");
548
561
  // --help 不该先被拽去登录
@@ -622,8 +635,9 @@ async function run() {
622
635
  }
623
636
  // 拼错的子命令(如 `u1s1 depoly`)不该被静默当成第一条消息发给模型白烧 token。
624
637
  // 只拦「单个短英文词 + 与已知命令编辑距离很近」的情况,正常 prompt 不受影响。
638
+ // `u1s1 delpoy list` 这类带参数的拼错也拦;长句(≥4 个词)才当 prompt 放行
625
639
  if (cmd
626
- && args.length === 1
640
+ && args.length <= 3
627
641
  && /^[a-z][a-z0-9-]{1,15}$/.test(cmd)
628
642
  && !KNOWN_COMMANDS.includes(cmd)) {
629
643
  const near = KNOWN_COMMANDS.find((k) => editDistance(k, cmd) <= (k.length <= 4 ? 1 : 2));
@@ -642,7 +656,28 @@ async function run() {
642
656
  // autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
643
657
  installPendingUpdate();
644
658
  }
645
- const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "feedback", "help", "web"];
659
+ const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "feedback", "help"];
660
+ /** 没有自己 --help 处理的简单子命令:问用法时只打印说明,不执行任何动作。 */
661
+ const SIMPLE_COMMAND_HELP = {
662
+ login: [
663
+ "用法: u1s1 login",
664
+ " 在浏览器里批准这台设备登录 u1s1;没有账号会引导注册。",
665
+ " 登录信息保存在 ~/.u1s1/config.json,退出登录用 u1s1 logout。",
666
+ ],
667
+ logout: [
668
+ "用法: u1s1 logout",
669
+ " 退出本机登录,并注销这台设备在服务端的授权(连不上服务器时只清本机)。",
670
+ ],
671
+ usage: [
672
+ "用法: u1s1 usage",
673
+ " 查看剩余额度:免费包、已购用量包与余额。会话内也可输入 /usage。",
674
+ ],
675
+ update: [
676
+ "用法: u1s1 update",
677
+ " 检查并升级到最新版;npm 安装的走 npm,便携版原地自更新。",
678
+ " 不想每次退出自动更新:在 ~/.u1s1/agent/settings.json 设 \"autoUpdate\": false",
679
+ ],
680
+ };
646
681
  /** 经典 Levenshtein,命令名都很短,O(nm) 足够。 */
647
682
  function editDistance(a, b) {
648
683
  const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
package/dist/login.js CHANGED
@@ -149,6 +149,8 @@ export async function login(keyArg) {
149
149
  printConsoleBanner(VERSION);
150
150
  const origin = apiOrigin(cfg);
151
151
  const failure = {};
152
+ // 慢网下这一步最长 15 秒,横幅之后一片安静像是卡死;先说一声在连
153
+ console.log(" 正在连接 u1s1 服务器…");
152
154
  const start = await startDeviceLogin(origin, failure);
153
155
  if (!start) {
154
156
  if (failure.message) {
package/dist/nudges.d.ts CHANGED
@@ -18,4 +18,4 @@ export declare function hasNudge(key: NudgeKey, file?: string): boolean;
18
18
  export declare function markNudge(key: NudgeKey, file?: string): void;
19
19
  /** 会话里首次写/改文件后要不要提示 deploy:没提示过、也没部署过才提示。 */
20
20
  export declare function shouldShowDeployHint(file?: string): boolean;
21
- export declare const DEPLOY_HINT_TEXT = "\u505A\u597D\u4E86\u60F3\u7ED9\u670B\u53CB\u770B\uFF1F\u8FD0\u884C u1s1 deploy \u4E00\u952E\u4E0A\u7EBF";
21
+ export declare const DEPLOY_HINT_TEXT = "\u505A\u597D\u4E86\u60F3\u7ED9\u670B\u53CB\u770B\uFF1F\u8F93\u5165 /exit \u56DE\u5230\u7EC8\u7AEF,\u8FD0\u884C u1s1 deploy \u4E00\u952E\u4E0A\u7EBF";
package/dist/nudges.js CHANGED
@@ -37,4 +37,4 @@ export function shouldShowDeployHint(file = nudgesFile) {
37
37
  const state = readNudges(file);
38
38
  return state.deploy_hint_shown === undefined && state.deploy_done === undefined;
39
39
  }
40
- export const DEPLOY_HINT_TEXT = "做好了想给朋友看?运行 u1s1 deploy 一键上线";
40
+ export const DEPLOY_HINT_TEXT = "做好了想给朋友看?输入 /exit 回到终端,运行 u1s1 deploy 一键上线";
package/dist/update.js CHANGED
@@ -147,6 +147,7 @@ async function portableSelfUpdate(latest) {
147
147
  process.exit(0);
148
148
  }
149
149
  console.log(`正在下载安装脚本并升级到 v${latest}…`);
150
+ console.log("(可能需要几分钟,期间请不要关闭窗口)");
150
151
  let script;
151
152
  try {
152
153
  const download = await downloadInstallScript();
package/dist/usage.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { type MeResponse } from "./api.js";
2
2
  export declare const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
3
- /** 报告尾巴的两条行动入口:邀请(永久加量)与充值(撞线用户的直接出口,运营清单 A2)。 */
4
- export declare function usageCtaLines(): string[];
3
+ /**
4
+ * 报告尾巴的两条行动入口:邀请(永久加量)与充值(撞线用户的直接出口,运营清单 A2)。
5
+ * 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
6
+ * 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
7
+ */
8
+ export declare function usageCtaLines(me?: Pick<MeResponse, "pay_enabled">): string[];
5
9
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
6
10
  export declare function usageReportLines(me: MeResponse): string[];
7
11
  /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
package/dist/usage.js CHANGED
@@ -94,7 +94,7 @@ function packageUsageLines(me, tokensPerUsd) {
94
94
  lines.push(" 其他模型(如 DeepSeek V4 Pro)不走免费包,可用余额或全模型包 → https://u1s1.io/dashboard");
95
95
  lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
96
96
  lines.push("");
97
- lines.push(...usageCtaLines());
97
+ lines.push(...usageCtaLines(me));
98
98
  return lines;
99
99
  }
100
100
  function legacyUsageLines(me, tokensPerUsd) {
@@ -118,15 +118,22 @@ function legacyUsageLines(me, tokensPerUsd) {
118
118
  lines.push(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
119
119
  }
120
120
  lines.push("");
121
- lines.push(...usageCtaLines());
121
+ lines.push(...usageCtaLines(me));
122
122
  return lines;
123
123
  }
124
124
  export const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
125
- /** 报告尾巴的两条行动入口:邀请(永久加量)与充值(撞线用户的直接出口,运营清单 A2)。 */
126
- export function usageCtaLines() {
125
+ /**
126
+ * 报告尾巴的两条行动入口:邀请(永久加量)与充值(撞线用户的直接出口,运营清单 A2)。
127
+ * 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
128
+ * 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
129
+ */
130
+ export function usageCtaLines(me) {
131
+ const payClosed = me?.pay_enabled === false;
127
132
  return [
128
133
  " 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard",
129
- ` 额度不够用?充值 → ${TOPUP_URL}`,
134
+ payClosed
135
+ ? " 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → https://u1s1.io/dashboard"
136
+ : ` 额度不够用?充值 → ${TOPUP_URL}`,
130
137
  ];
131
138
  }
132
139
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -271,6 +271,156 @@ function patchCompactUi() {
271
271
  }
272
272
  }
273
273
 
274
+ // ---- TUI 零散英文汉化(与 pnpm coding-agent patch 同步,patch-pi.test.ts 校验双轨一致) ----
275
+ // 新手最常撞见的几句:Esc 中断后的 "Operation aborted"、每条报错前缀 "Error:"、
276
+ // 每轮都在转的 "Working..."、选择器脚注的 "navigate/select/cancel"、/model 列表只显示 id
277
+ // 看不到免费/价格提示、/settings 整页英文。行为不变,只换文案;选择器改为渲染 name(带提示)。
278
+ const ASSISTANT_TEXT_REPLACEMENTS = [
279
+ ['theme.fg("error", "Response was truncated before completion.")', 'theme.fg("error", "回复超出单次长度上限被截断了,输入「继续」可以接着写。")'],
280
+ [': "Operation aborted";', ': "已中断";'],
281
+ ['const errorMsg = message.errorMessage || "Unknown error";', 'const errorMsg = message.errorMessage || "未知错误(反复出现可运行 u1s1 feedback 反馈)";'],
282
+ ['theme.fg("error", `Error: ${errorMsg}`)', 'theme.fg("error", `出错:${errorMsg}`)'],
283
+ ];
284
+ const WORKING_TEXT_REPLACEMENTS = [
285
+ ['defaultWorkingMessage = "Working...";', 'defaultWorkingMessage = "思考中…";'],
286
+ ['defaultHiddenThinkingLabel = "Thinking...";', 'defaultHiddenThinkingLabel = "思考中…";'],
287
+ ];
288
+ // 脚注提示(↑↓ navigate · enter select · esc cancel …)在十几个组件里各写各的,
289
+ // 在 keyHint/rawKeyHint 出口处统一查表翻译,新组件加进来也能覆盖到。
290
+ const KEY_HINT_REPLACEMENTS = [
291
+ [
292
+ 'export function keyHint(keybinding, description) {',
293
+ 'const U1S1_HINTS = { cancel: "取消", "to cancel": "取消", "to cancel,": "取消,", navigate: "上下选择", submit: "提交", "to submit": "提交", save: "保存", select: "选择", confirm: "确认", scope: "切换范围", "to expand": "展开", "to collapse": "收起", "to run bash": "直接跑命令", "to run bash (no context)": "跑命令(不进上下文)", toggle: "切换", "cycle inherit/+/-": "循环 继承/+/-", "for commands": "输入命令", commands: "命令", bash: "命令", close: "关闭", "to close": "关闭", "to attach": "拖入文件即附加", "skip setup": "跳过设置", "switch mode": "切换模式", newline: "换行", "to delete to end": "删到行尾", sort: "排序", named: "只看已命名", rename: "重命名", delete: "删除", "to exit": "退出", "to cycle models": "切换模型", "external editor": "外部编辑器", "clear/exit": "清空/退出" };\nconst u1s1Hint = (d) => U1S1_HINTS[d] ?? d;\nexport function keyHint(keybinding, description) {',
294
+ ],
295
+ { from: 'theme.fg("muted", ` ${description}`)', to: 'theme.fg("muted", ` ${u1s1Hint(description)}`)', expected: 2 },
296
+ ];
297
+ const MODEL_SELECTOR_REPLACEMENTS = [
298
+ // 两处 modelText(有/无两空格缩进)同一规则覆盖;测试的空白折叠匹配不认双空格
299
+ { from: '${item.id}`;', to: '${item.model.name || item.id}`;', expected: 2 },
300
+ ['Model Name: ${selected.model.name}`)', '模型 ID: ${selected.model.id}`)'],
301
+ ['No matching models")', '没有匹配的模型")'],
302
+ ['theme.fg("muted", " · default")', 'theme.fg("muted", " · 默认")'],
303
+ ['const allText = this.scope === "all" ? theme.fg("accent", "all") : theme.fg("muted", "all");', 'const allText = this.scope === "all" ? theme.fg("accent", "全部") : theme.fg("muted", "全部");'],
304
+ ['const scopedText = this.scope === "scoped" ? theme.fg("accent", "scoped") : theme.fg("muted", "scoped");', 'const scopedText = this.scope === "scoped" ? theme.fg("accent", "已配置") : theme.fg("muted", "已配置");'],
305
+ ['theme.fg("muted", "Scope: ")', 'theme.fg("muted", "范围: ")'],
306
+ ['theme.fg("muted", " (all/scoped)")', 'theme.fg("muted", " (全部/已配置)")'],
307
+ ['refreshStatusMessage = "Refreshing model catalogs…";', 'refreshStatusMessage = "正在刷新模型列表…";'],
308
+ ['"Model catalogs refreshed."', '"模型列表已刷新。"'],
309
+ ['const hintText = "Only showing models from configured providers. Use /login to add providers.";', 'const hintText = "只显示已配置渠道的模型。";'],
310
+ ];
311
+ const SETTINGS_REPLACEMENTS = [
312
+ ['off: "No reasoning",', 'off: "不推理",'],
313
+ ['minimal: "Very brief reasoning (~1k tokens)",', 'minimal: "极简推理(约 1k token)",'],
314
+ ['low: "Light reasoning (~2k tokens)",', 'low: "轻度推理(约 2k token)",'],
315
+ ['medium: "Moderate reasoning (~8k tokens)",', 'medium: "中等推理(约 8k token)",'],
316
+ ['high: "Deep reasoning (~16k tokens)",', 'high: "深度推理(约 16k token)",'],
317
+ ['xhigh: "Extra-high reasoning (~32k tokens)",', 'xhigh: "超深推理(约 32k token)",'],
318
+ ['max: "Maximum reasoning",', 'max: "最大推理",'],
319
+ ['ask: "Ask",', 'ask: "每次询问",'],
320
+ ['always: "Always trust",', 'always: "始终信任",'],
321
+ ['never: "Never trust",', 'never: "从不信任",'],
322
+ ['new SelectSubmenu("Theme", "Select a theme, or choose Automatic to follow terminal appearance."', 'new SelectSubmenu("主题", "选择一个主题,或选「自动」跟随终端明暗。"'],
323
+ ['label: "Automatic",', 'label: "自动",'],
324
+ ['description: "Use separate themes for light and dark terminal appearance",', 'description: "终端明暗各用一套主题",'],
325
+ ['theme.fg("accent", "Automatic Theme")', 'theme.fg("accent", "自动主题")'],
326
+ ['theme.fg("muted", "Choose themes for terminal light and dark appearance.")', 'theme.fg("muted", "分别为终端的浅色/深色外观选主题。")'],
327
+ ['theme.fg("muted", "Light/dark detection requires terminal support.")', 'theme.fg("muted", "明暗检测需要终端支持。")'],
328
+ ['label: "Light theme",', 'label: "浅色主题",'],
329
+ ['description: "Theme to use in automatic mode when the terminal is light",', 'description: "自动模式下终端为浅色时使用的主题",'],
330
+ ['label: "Dark theme",', 'label: "深色主题",'],
331
+ ['description: "Theme to use in automatic mode when the terminal is dark",', 'description: "自动模式下终端为深色时使用的主题",'],
332
+ ['label: "Apply",', 'label: "应用",'],
333
+ ['description: "Save and go back",', 'description: "保存并返回",'],
334
+ ['label: "Change mode",', 'label: "切换模式",'],
335
+ ['description: "Switch to one theme for light and dark",', 'description: "明暗共用一个主题",'],
336
+ ['label: "Auto-compact",', 'label: "自动压缩上下文",'],
337
+ ['description: "Automatically compact context when it gets too large",', 'description: "上下文过长时自动压缩",'],
338
+ ['label: "Steering mode",', 'label: "插话模式",'],
339
+ ['description: "Enter while streaming queues steering messages. \'one-at-a-time\': deliver one, wait for response. \'all\': deliver all at once.",', 'description: "回复进行中按 Enter 发送插话。one-at-a-time:发一条等回复;all:一次全发。",'],
340
+ ['label: "Follow-up mode",', 'label: "追问模式",'],
341
+ ['description: `${followUpKey} queues follow-up messages until agent stops. \'one-at-a-time\': deliver one, wait for response. \'all\': deliver all at once.`,', 'description: `${followUpKey} 把追问排队到本轮结束再发。one-at-a-time:发一条等回复;all:一次全发。`,'],
342
+ ['label: "Transport",', 'label: "传输方式",'],
343
+ ['description: "Preferred transport for providers that support multiple transports",', 'description: "支持多种传输方式的渠道优先用哪种",'],
344
+ ['label: "HTTP idle timeout",', 'label: "HTTP 空闲超时",'],
345
+ ['description: "Maximum idle gap while waiting for HTTP headers or body chunks. Disable for local models that pause longer than five minutes.",', 'description: "等待响应头或数据块的最长空闲间隔;本地模型停顿超过五分钟可关闭。",'],
346
+ ['label: "Hide thinking",', 'label: "隐藏思考过程",'],
347
+ ['description: "Hide thinking blocks in assistant responses",', 'description: "不显示回复里的思考块",'],
348
+ ['label: "Mermaid diagrams",', 'label: "Mermaid 图表",'],
349
+ ['description: "Render Mermaid code blocks as Unicode diagrams",', 'description: "把 Mermaid 代码块渲染成字符图",'],
350
+ ['label: "Cache miss notices",', 'label: "缓存未命中提示",'],
351
+ ['description: "Show transcript notices for significant prompt-cache misses and compaction costs",', 'description: "提示明显的提示词缓存未命中与压缩开销",'],
352
+ ['label: "Collapse changelog",', 'label: "折叠更新日志",'],
353
+ ['description: "Show condensed changelog after updates",', 'description: "更新后只显示精简的更新日志",'],
354
+ ['label: "Quiet startup",', 'label: "安静启动",'],
355
+ ['description: "Disable verbose printing at startup",', 'description: "启动时不打印冗长信息",'],
356
+ ['label: "Install telemetry",', 'label: "安装统计",'],
357
+ ['description: "Send an anonymous version/update ping after changelog-detected updates",', 'description: "更新后匿名上报一次版本信息",'],
358
+ ['label: "Default project trust",', 'label: "默认项目信任",'],
359
+ ['description: "Fallback behavior when no extension or saved trust decision decides project trust",', 'description: "没有扩展或已保存决定时,项目信任的兜底行为",'],
360
+ ['label: "Double-escape action",', 'label: "双击 Esc 动作",'],
361
+ ['description: "Action when pressing Escape twice with empty editor",', 'description: "输入框为空时连按两次 Esc 的动作",'],
362
+ ['label: "Tree filter mode",', 'label: "会话树筛选",'],
363
+ ['description: "Default filter when opening /tree",', 'description: "打开 /tree 时的默认筛选",'],
364
+ ['label: "Warnings",', 'label: "警告提示",'],
365
+ ['description: "Enable or disable individual warnings",', 'description: "逐项开关各类警告",'],
366
+ ['label: "Default thinking level per model",', 'label: "各模型默认推理强度",'],
367
+ ['description: `Override the default thinking level for specific models. ${cycleThinkingKey} cycles in-session.`,', 'description: `按模型覆盖默认推理强度;会话内用 ${cycleThinkingKey} 切换。`,'],
368
+ ['description: "Select a model to configure",', 'description: "选择要设置的模型",'],
369
+ ['label: "No models available",', 'label: "没有可用模型",'],
370
+ ['description: "Log in to a provider or configure an API key first",', 'description: "请先登录",'],
371
+ ['description: "Select default thinking level for this model",', 'description: "选择这个模型的默认推理强度",'],
372
+ ['label: "(clear override)",', 'label: "(清除覆盖)",'],
373
+ ['description: `Revert to global default (${config.thinkingLevel})`,', 'description: `恢复为全局默认(${config.thinkingLevel})`,'],
374
+ ['label: "TUI mode",', 'label: "界面模式",'],
375
+ ['description: "Interface layout; fullscreen mode is experimental",', 'description: "界面布局;全屏模式为实验功能",'],
376
+ ['label: "Fullscreen exit output",', 'label: "退出全屏时的输出",'],
377
+ ['description: "Print the transcript or only a session resume hint when exiting fullscreen mode",', 'description: "退出全屏时打印完整对话,还是只留一条恢复会话提示",'],
378
+ ['label: "Fullscreen scrollbar",', 'label: "全屏滚动条",'],
379
+ ['description: "Scrollbar behavior in fullscreen mode; has no effect in regular mode",', 'description: "全屏模式下的滚动条行为;普通模式无效",'],
380
+ ['label: "Fullscreen copy on select",', 'label: "全屏选中即复制",'],
381
+ ['description: "Automatically copy selected text in fullscreen mode; disable to copy selections with Ctrl+X",', 'description: "全屏模式下选中文字自动复制;关闭后用 Ctrl+X 复制",'],
382
+ ['label: "Theme",', 'label: "主题",'],
383
+ ['description: "Color theme for the interface",', 'description: "界面配色主题",'],
384
+ ];
385
+
386
+ const TUI_TEXT_TARGETS = [
387
+ [["modes", "interactive", "components", "assistant-message.js"], ASSISTANT_TEXT_REPLACEMENTS, "回复状态文案"],
388
+ [["modes", "interactive", "interactive-mode.js"], WORKING_TEXT_REPLACEMENTS, "工作中提示"],
389
+ [["modes", "interactive", "components", "keybinding-hints.js"], KEY_HINT_REPLACEMENTS, "按键脚注"],
390
+ [["modes", "interactive", "components", "model-selector.js"], MODEL_SELECTOR_REPLACEMENTS, "模型选择器"],
391
+ [["modes", "interactive", "components", "settings-selector.js"], SETTINGS_REPLACEMENTS, "设置菜单"],
392
+ ];
393
+
394
+ function patchTuiText() {
395
+ for (const piDir of findPackageDirs("@earendil-works/pi-coding-agent")) {
396
+ for (const [parts, rules, label] of TUI_TEXT_TARGETS) {
397
+ const target = join(piDir, "dist", ...parts);
398
+ try {
399
+ let text = readFileSync(target, "utf8");
400
+ let applied = 0;
401
+ for (const rule of rules) {
402
+ const { from, to, expected } = Array.isArray(rule) ? { from: rule[0], to: rule[1], expected: 1 } : rule;
403
+ if (text.includes(to)) {
404
+ applied++;
405
+ continue;
406
+ }
407
+ if (text.split(from).length !== expected + 1) continue;
408
+ text = text.split(from).join(to);
409
+ applied++;
410
+ }
411
+ if (applied < rules.length) {
412
+ console.log(
413
+ `[u1s1] 提示: pi 版本可能已更新,${label}汉化补丁只应用了 ${applied}/${rules.length} 处(不影响使用)`,
414
+ );
415
+ }
416
+ writeFileSync(target, text);
417
+ } catch {
418
+ continue;
419
+ }
420
+ }
421
+ }
422
+ }
423
+
274
424
  function patchMainScreenAutowrap() {
275
425
  const tuiDirs = findTuiDirs();
276
426
  if (tuiDirs.length === 0) {
@@ -329,6 +479,7 @@ try {
329
479
  patchSlashCommands();
330
480
  patchExtensionError();
331
481
  patchHotkeys();
482
+ patchTuiText();
332
483
  patchMainScreenAutowrap();
333
484
  } catch (err) {
334
485
  console.log(`[u1s1] 提示: pi 运行时补丁未生效(${err?.message ?? err}),不影响安装`);