pi-web-ui 0.94.1 → 0.96.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 (100) hide show
  1. package/CHANGELOG.md +91 -2
  2. package/README.md +5 -6
  3. package/README.zh-CN.md +4 -5
  4. package/bin/pi-web-ui.mjs +910 -237
  5. package/dist/server/agent-service.js +2655 -231
  6. package/dist/server/approval-rules.js +596 -0
  7. package/dist/server/attachment-store.js +129 -0
  8. package/dist/server/attachments.js +92 -174
  9. package/dist/server/bg-servers.js +45 -7
  10. package/dist/server/claim-files-tool.js +4 -8
  11. package/dist/server/claim-store.js +4 -2
  12. package/dist/server/client-state.js +67 -14
  13. package/dist/server/compact-context-tool.js +126 -0
  14. package/dist/server/composer-drafts.js +9 -0
  15. package/dist/server/context-budget.js +317 -0
  16. package/dist/server/control-socket.js +57 -28
  17. package/dist/server/conversation-read-tool.js +7 -17
  18. package/dist/server/dangling-tools.js +229 -0
  19. package/dist/server/delegate-task.js +25 -28
  20. package/dist/server/dsh/dsh-agent-service.js +69 -66
  21. package/dist/server/edit-soft-tool.js +84 -16
  22. package/dist/server/eval-tool.js +588 -0
  23. package/dist/server/file-archives.js +16 -5
  24. package/dist/server/files-service.js +65 -18
  25. package/dist/server/goal-service.js +371 -77
  26. package/dist/server/hashline-engine.js +703 -0
  27. package/dist/server/host-guard.js +94 -0
  28. package/dist/server/host-metrics.js +26 -2
  29. package/dist/server/i18n.js +3 -3
  30. package/dist/server/index.js +456 -80
  31. package/dist/server/lsp-tool.js +1371 -0
  32. package/dist/server/mcp-bridge.js +47 -3
  33. package/dist/server/model-admin.js +112 -32
  34. package/dist/server/office-parse.js +375 -0
  35. package/dist/server/patch-tool.js +85 -0
  36. package/dist/server/permission-preset.js +25 -0
  37. package/dist/server/plan-manager.js +114 -0
  38. package/dist/server/plugin-api-catalog.js +296 -0
  39. package/dist/server/plugin-catalog-sync.js +36 -19
  40. package/dist/server/plugin-catalog.js +11 -4
  41. package/dist/server/plugin-facilities.js +92 -21
  42. package/dist/server/plugin-install-spec.js +196 -0
  43. package/dist/server/plugin-installer.js +73 -0
  44. package/dist/server/plugin-llm.js +71 -65
  45. package/dist/server/plugin-manifest-validate.js +305 -0
  46. package/dist/server/plugin-project.js +117 -13
  47. package/dist/server/plugin-tool-guard.js +120 -0
  48. package/dist/server/plugin-updater.js +115 -12
  49. package/dist/server/plugins.js +950 -205
  50. package/dist/server/present-files-tool.js +9 -12
  51. package/dist/server/process-utils.js +16 -6
  52. package/dist/server/prompt-composer.js +9 -0
  53. package/dist/server/protocol-version.js +1 -1
  54. package/dist/server/read-tool.js +69 -30
  55. package/dist/server/resolve-global-sdk.js +30 -16
  56. package/dist/server/schedule-agent-tool.js +12 -15
  57. package/dist/server/scheduler-tasks.js +6 -0
  58. package/dist/server/sdk-origin.js +18 -2
  59. package/dist/server/serialize.js +111 -14
  60. package/dist/server/settings-service.js +112 -3
  61. package/dist/server/skill-tool.js +5 -7
  62. package/dist/server/subagent-templates.js +51 -0
  63. package/dist/server/subagents.js +83 -66
  64. package/dist/server/terminals.js +248 -81
  65. package/dist/server/tool-approval.js +84 -0
  66. package/dist/server/tool-manager.js +237 -12
  67. package/dist/server/tool-overrides.js +59 -0
  68. package/dist/server/update-check.js +28 -1
  69. package/dist/server/uploads.js +17 -2
  70. package/dist/server/wait-subscription-scan.js +18 -21
  71. package/dist/server/workspace-snapshot.js +113 -0
  72. package/dist/server/ws-client-id.js +29 -0
  73. package/dist/server/ws-pending-queue.js +60 -0
  74. package/extensions/webui.ts +60 -2
  75. package/package.json +5 -3
  76. package/plugin-sdk/README.md +25 -0
  77. package/plugin-sdk/index.d.ts +31 -38
  78. package/plugin-sdk/index.mjs +35 -19
  79. package/plugins/catalog.json +40 -0
  80. package/themes/aetheris.css +457 -0
  81. package/themes/ayu-light.css +6 -6
  82. package/themes/catppuccin-latte.css +6 -6
  83. package/themes/claude-code-dark.css +144 -0
  84. package/themes/codex.css +6 -6
  85. package/themes/everforest-light.css +6 -6
  86. package/themes/geist.css +6 -6
  87. package/themes/gruvbox-light.css +6 -6
  88. package/themes/kanagawa-lotus.css +6 -6
  89. package/themes/rose-pine-dawn.css +6 -6
  90. package/themes/solarized-light.css +6 -6
  91. package/themes/vs-code-dark.css +146 -0
  92. package/themes/zhupi-dark.css +658 -0
  93. package/themes/zhupi.css +711 -0
  94. package/web/dist/assets/{TerminalPanel-MVoxpJOA.js → TerminalPanel-DDChcYOh.js} +1 -1
  95. package/web/dist/assets/index-Do9RgJC3.js +366 -0
  96. package/web/dist/assets/index-DryOsILO.css +1 -0
  97. package/web/dist/assets/{markdown-eUQn_o9D.js → markdown-DXwnfD9T.js} +1 -1
  98. package/web/dist/index.html +3 -3
  99. package/web/dist/assets/index-B3S9MxnN.css +0 -1
  100. package/web/dist/assets/index-DVLrHI2E.js +0 -364
package/bin/pi-web-ui.mjs CHANGED
@@ -27,6 +27,7 @@
27
27
  import { spawn, spawnSync } from "node:child_process";
28
28
  import { createConnection } from "node:net";
29
29
  import { get as httpGet } from "node:http";
30
+ import { createInterface } from "node:readline/promises";
30
31
  import {
31
32
  ensureBackup as ensurePluginBackup,
32
33
  restoreBackup as restorePluginBackup,
@@ -55,9 +56,9 @@ import { fileURLToPath } from "node:url";
55
56
  const BIN_DIR = dirname(fileURLToPath(import.meta.url));
56
57
  /** <pkg>/dist/server/index.js — the actual server entry. */
57
58
  const SERVER_ENTRY = join(BIN_DIR, "..", "dist", "server", "index.js");
58
- /** 可选的「优先用全局/祖先那份 pi SDK」解析钩子(issue #260,`PI_WEB_SDK=global` 才生效)。
59
+ /** 「机器上有更新的 pi 副本就跟随它」解析钩子(issue #260;#321 起默认启用)。
59
60
  * 必须早于任何 SDK 静态 import 加载 —— 所以每条启动路径都把它当 `--import` 传进去;
60
- * 默认不启用时它自己什么都不做(见 server/resolve-global-sdk.ts)。 */
61
+ * PI_WEB_SDK=bundled 显式钉死自带副本(见 server/resolve-global-sdk.ts)。 */
61
62
  const SDK_HOOK = join(BIN_DIR, "..", "dist", "server", "resolve-global-sdk.js");
62
63
  /** dist 可能是旧构建(没有这个钩子文件)—— 只有文件在才注入:`--import <missing>` 会让
63
64
  * CLI/服务直接起不来,那比少个开关严重得多。 */
@@ -84,6 +85,9 @@ function isZhLang() {
84
85
  return false;
85
86
  }
86
87
 
88
+ /** Resolved once: CLI messages below pick zh/en with `ZH ? zh : en` (only the chosen branch is evaluated). */
89
+ const ZH = isZhLang();
90
+
87
91
  const HELP_ZH = `pi-web-ui v${pkg.version} — web chat for the pi coding agent
88
92
 
89
93
  用法:
@@ -105,8 +109,9 @@ server 选项:
105
109
  --engine <pi|dsh> 智能体引擎(默认 $PI_WEB_ENGINE 或 pi)
106
110
  --host <addr> 监听地址(默认 $PI_WEB_HOST 或 127.0.0.1;0.0.0.0 供局域网/容器)
107
111
  --agent-dir <dir> pi 配置目录(默认 $PI_CODING_AGENT_DIR 或 ~/.pi/agent)
108
- --name <name> 服务名(默认 pi-web-ui;macOS 的 launchd label
109
- 为 com.xingshuyin.pi-web-ui,自定义名时为 com.<name>.server)
112
+ --name <name> 服务名(默认 pi-web-ui;仅限字母/数字/-/_,字母或数字开头,
113
+ 最长 64 字符。macOS 的 launchd label 为 com.xingshuyin.pi-web-ui,
114
+ 自定义名时为 com.<name>.server)
110
115
  --print 只打印将生成的配置文件,不实际安装
111
116
 
112
117
  平台: macOS → launchd 用户代理 · Linux → systemd · Windows → 登录自启 Run 键
@@ -125,7 +130,8 @@ server 选项:
125
130
  install 选项: --name <id> 自定义插件目录名(默认取仓库名)
126
131
  --data-dir <dir> 数据目录(默认 ~/.pi-web)
127
132
  --force 目标已存在时覆盖
128
- --build 强制源码构建(隔离目录编译,见下)
133
+ --build 强制源码构建(隔离目录编译,见下;不加 --build 时若插件只有
134
+ 源码,交互终端会询问是否构建并需输入 y 确认,非交互环境跳过)
129
135
  --no-build 即使只有源码也不构建(产物缺失的插件装上后不加载)
130
136
  --catalog <url> 目录同步模式:读目录文档 → 写可安装列表 → 逐条安装
131
137
  --replace 配合 --catalog:整体替换列表(默认按 id 合并)
@@ -158,7 +164,8 @@ Server options:
158
164
  --engine <pi|dsh> Agent engine (default $PI_WEB_ENGINE or pi)
159
165
  --host <addr> Listen address (default $PI_WEB_HOST or 127.0.0.1; 0.0.0.0 for LAN/containers)
160
166
  --agent-dir <dir> pi config directory (default $PI_CODING_AGENT_DIR or ~/.pi/agent)
161
- --name <name> Service name (default pi-web-ui; macOS launchd label is
167
+ --name <name> Service name (default pi-web-ui; alphanumeric, "-" and "_" only,
168
+ starting with a letter or digit, max 64 chars. macOS launchd label is
162
169
  com.xingshuyin.pi-web-ui, or com.<name>.server for custom names)
163
170
  --print Only print generated config files (no actual install)
164
171
 
@@ -178,7 +185,9 @@ UI plugins (installed into <data-dir>/plugins/; refresh browser to activate whil
178
185
  install options: --name <id> Custom plugin directory name (default: repo name)
179
186
  --data-dir <dir> Data directory (default: ~/.pi-web)
180
187
  --force Overwrite if target already exists
181
- --build Force a source build (isolated build, see below)
188
+ --build Force a source build (isolated build, see below). Without it,
189
+ a source-only plugin asks for confirmation on a TTY (answer y to
190
+ build) and skips the build in non-interactive environments
182
191
  --no-build Never build, even for source-only plugins
183
192
  --catalog <url> Catalog mode: read a catalog document, write the
184
193
  installable list, then install every entry
@@ -261,7 +270,7 @@ function parseFlags(argv) {
261
270
  i++;
262
271
  return argv[i];
263
272
  }
264
- fail(`缺少选项 ${flag} 的值`);
273
+ fail(ZH ? `缺少选项 ${flag} 的值` : `Option ${flag} is missing a value`);
265
274
  };
266
275
  switch (key) {
267
276
  case "--port":
@@ -326,7 +335,7 @@ function parseFlags(argv) {
326
335
  opts.help = true;
327
336
  break;
328
337
  default:
329
- if (key.startsWith("-")) fail(`未知选项: ${key}`);
338
+ if (key.startsWith("-")) fail(ZH ? `未知选项: ${key}` : `Unknown option: ${key}`);
330
339
  positionals.push(a);
331
340
  }
332
341
  }
@@ -351,11 +360,14 @@ function openBrowser(url) {
351
360
  if (res?.error) {
352
361
  if (res.error.code === "ENOENT") {
353
362
  console.warn(
354
- `[browser] 未找到打开器 (${res.error.path || "command not found"}),` +
355
- "headless 服务器可用 --no-browser 关闭自动打开",
363
+ ZH
364
+ ? `[browser] 未找到打开器 (${res.error.path || "command not found"}),` +
365
+ "headless 服务器可用 --no-browser 关闭自动打开"
366
+ : `[browser] no opener found (${res.error.path || "command not found"}), ` +
367
+ "headless servers can use --no-browser to disable auto-open",
356
368
  );
357
369
  } else {
358
- console.warn("[browser] 打开浏览器失败:", res.error.message);
370
+ console.warn(ZH ? "[browser] 打开浏览器失败:" : "[browser] failed to open browser:", res.error.message);
359
371
  }
360
372
  }
361
373
  }
@@ -372,7 +384,11 @@ function openBrowserWhenUp(url) {
372
384
  const attempt = () => {
373
385
  const req = httpGet(url, (res) => {
374
386
  res.resume();
375
- console.log(` 🌐 已自动打开浏览器:${url}(--no-browser 可关闭)`);
387
+ console.log(
388
+ ZH
389
+ ? ` 🌐 已自动打开浏览器:${url}(--no-browser 可关闭)`
390
+ : ` 🌐 Browser opened automatically: ${url} (--no-browser to disable)`,
391
+ );
376
392
  openBrowser(url);
377
393
  });
378
394
  req.setTimeout(1000, () => {
@@ -391,7 +407,8 @@ async function startForeground(opts) {
391
407
  if (opts.cwd) process.env.PI_WEB_CWD = resolve(opts.cwd);
392
408
  if (opts.dataDir) process.env.PI_WEB_DATA_DIR = resolve(opts.dataDir);
393
409
  if (opts.engine) {
394
- if (opts.engine !== "pi" && opts.engine !== "dsh") fail(`无效引擎: ${opts.engine}(仅支持 pi / dsh)`);
410
+ if (opts.engine !== "pi" && opts.engine !== "dsh")
411
+ fail(ZH ? `无效引擎: ${opts.engine}(仅支持 pi / dsh)` : `Invalid engine: ${opts.engine} (pi and dsh only)`);
395
412
  process.env.PI_WEB_ENGINE = opts.engine;
396
413
  }
397
414
  if (opts.host) process.env.PI_WEB_HOST = opts.host;
@@ -508,8 +525,16 @@ function winIcoPath() {
508
525
  return join(winServiceDir(), APP_ICO_NAME);
509
526
  }
510
527
 
511
- /** Full path to Windows PowerShell 5.1. */
528
+ /** Full path to Windows PowerShell. Prefers pwsh.exe (PowerShell 7) when it is
529
+ * resolvable on PATH: some machines ship Windows PowerShell 5.1 as a broken
530
+ * stub whose launch fails silently, so probe for a working shell first and
531
+ * fall back to the built-in path. */
512
532
  function winPowershell() {
533
+ const which = spawnSync("where.exe", ["pwsh.exe"], { encoding: "utf8" });
534
+ if (which.status === 0) {
535
+ const first = (which.stdout ?? "").trim().split(/\r?\n/)[0];
536
+ if (first) return first;
537
+ }
513
538
  return join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
514
539
  }
515
540
 
@@ -524,12 +549,13 @@ function winWscript() {
524
549
  * Resolve the real node binary. fnm/volta/nvm shims (e.g. fnm_multishells)
525
550
  * point into temp dirs that vanish when the installing shell exits — the
526
551
  * baked-in launcher scripts must use the stable real path instead.
552
+ * Also applies resolveNode() so Bun-hosted pi never bakes in pi.exe.
527
553
  */
528
554
  function realNode() {
529
555
  try {
530
- return realpathSync(process.execPath);
556
+ return realpathSync(resolveNode());
531
557
  } catch {
532
- return process.execPath;
558
+ return resolveNode();
533
559
  }
534
560
  }
535
561
 
@@ -671,7 +697,11 @@ function buildWinHiddenVbs(ps1Path) {
671
697
  function installWinShortcut(opts) {
672
698
  const { name, port, cwd, dataDir, engine, host, agentDir } = serviceOptions(opts);
673
699
  const env = serviceEnv(port, cwd, dataDir, engine, host, agentDir);
674
- const url = `http://localhost:${port}`;
700
+ // The server binds 127.0.0.1 by default; PowerShell 7's Invoke-WebRequest
701
+ // resolves `localhost` to ::1 first and hangs until TimeoutSec when nothing
702
+ // listens on IPv6, making the health probe misfire ("server not running").
703
+ // Pin the probe (and the opened URL) to the IPv4 loopback the server uses.
704
+ const url = `http://127.0.0.1:${port}`;
675
705
  const ps1Path = winShortcutPs1Path(name);
676
706
  const ps1 = buildWinShortcutPs1(env, cwd, name, url, winLogPath(name), winPidFilePath(name));
677
707
  if (opts.print) {
@@ -687,7 +717,11 @@ function installWinShortcut(opts) {
687
717
  if (existsSync(APP_ICO_SOURCE)) {
688
718
  copyFileSync(APP_ICO_SOURCE, winIcoPath());
689
719
  } else {
690
- console.log(`⚠ 未找到品牌图标 ${APP_ICO_SOURCE},快捷方式将使用默认图标`);
720
+ console.log(
721
+ ZH
722
+ ? `⚠ 未找到品牌图标 ${APP_ICO_SOURCE},快捷方式将使用默认图标`
723
+ : `⚠ Brand icon not found ${APP_ICO_SOURCE}, shortcut will use the default icon`,
724
+ );
691
725
  }
692
726
  const powershell = winPowershell();
693
727
  const ps = [
@@ -707,14 +741,26 @@ function installWinShortcut(opts) {
707
741
  encoding: "utf8",
708
742
  });
709
743
  if (res.status !== 0) {
710
- fail(`创建桌面快捷方式失败: ${(res.stderr || res.stdout || "").trim()}`);
744
+ fail(
745
+ ZH
746
+ ? `创建桌面快捷方式失败: ${(res.stderr || res.stdout || "").trim()}`
747
+ : `Failed to create desktop shortcut: ${(res.stderr || res.stdout || "").trim()}`,
748
+ );
711
749
  }
712
750
  const lnk = (res.stdout ?? "").trim();
713
- console.log(`✅ 已创建桌面快捷方式: ${lnk}`);
714
- console.log(` 双击 : 服务未运行则启动(隐藏窗口,无黑窗),就绪后自动打开浏览器`);
715
- console.log(` 停止 : pi-web-ui server stop(快捷方式启动的实例也会一并停止)`);
716
- console.log(` 端口 : ${port}`);
717
- console.log(` 目录 : ${cwd}`);
751
+ console.log(ZH ? `✅ 已创建桌面快捷方式: ${lnk}` : `✅ Desktop shortcut created: ${lnk}`);
752
+ console.log(
753
+ ZH
754
+ ? ` 双击 : 服务未运行则启动(隐藏窗口,无黑窗),就绪后自动打开浏览器`
755
+ : ` Launch : double-click starts the service if not running (hidden window, no console), auto-opens browser when ready`,
756
+ );
757
+ console.log(
758
+ ZH
759
+ ? ` 停止 : pi-web-ui server stop(快捷方式启动的实例也会一并停止)`
760
+ : ` Stop : pi-web-ui server stop (also stops instances launched by the shortcut)`,
761
+ );
762
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
763
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
718
764
  }
719
765
 
720
766
  /** macOS: double-clickable .command launcher (the .lnk equivalent). */
@@ -774,12 +820,24 @@ function installMacShortcut(opts) {
774
820
  }
775
821
  writeFileSync(path, script);
776
822
  chmodSync(path, 0o755);
777
- console.log(`✅ 已创建桌面启动器: ${path}`);
778
- console.log(` 双击 : 确保服务运行并打开浏览器;未安装服务时在本终端前台运行`);
779
- console.log(` 说明 : macOS 没有 Windows 式快捷方式,这是等价的 .command 启动器;`);
780
- console.log(` launchd 服务登录自启,图标主要用于快速「启动 + 打开浏览器」`);
781
- console.log(` 端口 : ${port}`);
782
- console.log(` 目录 : ${cwd}`);
823
+ console.log(ZH ? `✅ 已创建桌面启动器: ${path}` : `✅ Desktop launcher created: ${path}`);
824
+ console.log(
825
+ ZH
826
+ ? ` 双击 : 确保服务运行并打开浏览器;未安装服务时在本终端前台运行`
827
+ : ` Launch : double-click ensures the service is running and opens browser; runs in foreground in this terminal if service not installed`,
828
+ );
829
+ console.log(
830
+ ZH
831
+ ? ` 说明 : macOS 没有 Windows 式快捷方式,这是等价的 .command 启动器;`
832
+ : ` Notes : macOS has no Windows-style shortcuts; this is the equivalent .command launcher;`,
833
+ );
834
+ console.log(
835
+ ZH
836
+ ? ` launchd 服务登录自启,图标主要用于快速「启动 + 打开浏览器」`
837
+ : ` launchd service auto-starts at login, icon mainly for quick “start + open browser”`,
838
+ );
839
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
840
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
783
841
  }
784
842
 
785
843
  /** Linux: launcher script run by the .desktop icon. */
@@ -855,10 +913,14 @@ Categories=Network;WebBrowser;
855
913
  ignoreError: true,
856
914
  silent: true,
857
915
  });
858
- console.log(`✅ 已创建桌面图标: ${desktopPath}`);
859
- console.log(` GNOME 若提示「不受信任的应用程序」,右键选择 Allow Launching`);
860
- console.log(` 端口 : ${port}`);
861
- console.log(` 目录 : ${cwd}`);
916
+ console.log(ZH ? `✅ 已创建桌面图标: ${desktopPath}` : `✅ Desktop icon created: ${desktopPath}`);
917
+ console.log(
918
+ ZH
919
+ ? ` GNOME 若提示「不受信任的应用程序」,右键选择 Allow Launching`
920
+ : ` If GNOME prompts “untrusted application”, right-click and choose Allow Launching`,
921
+ );
922
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
923
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
862
924
  }
863
925
 
864
926
  /** Remove desktop shortcut artifacts created by `server shortcut`. */
@@ -943,19 +1005,53 @@ function launchWinService(vbsPath) {
943
1005
  child.unref();
944
1006
  }
945
1007
 
1008
+ /**
1009
+ * PID 文件记录的是看门狗 PowerShell 的 $PID(winPowershell() 可能选 pwsh.exe),
1010
+ * node 是它的子进程 —— 这三者是本服务实例的合理映像名。taskkill /T /F 是树级强杀,
1011
+ * PID 若已退出被系统复用就会误杀无辜进程,所以杀前必须核对映像名。
1012
+ */
1013
+ const WIN_SERVICE_IMAGE_RE = /^(powershell|pwsh|node)\.exe$/i;
1014
+
1015
+ /** tasklist 查询 PID 对应的进程映像名;查询失败/无此进程返回 null(无法核实)。 */
1016
+ function winPidImageName(pid) {
1017
+ const res = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { encoding: "utf8" });
1018
+ if (res.status !== 0) return null;
1019
+ const line = (res.stdout ?? "").split(/\r?\n/).find((l) => l.trim());
1020
+ if (!line) return null; // tasklist 的 "INFO: 无任务" 输出没有 CSV 行
1021
+ return (line.match(/^"([^"]+)"/) ?? [])[1] ?? null;
1022
+ }
1023
+
946
1024
  /** Kill a running Windows instance via its PID file (whole tree). */
947
1025
  function stopWinInstance(name) {
948
1026
  const pid = winReadPid(name);
949
1027
  if (pid) {
950
1028
  if (pidAlive(pid)) {
951
- run("taskkill", ["/PID", String(pid), "/T", "/F"], {
952
- ignoreError: true,
953
- silent: true,
954
- });
955
- // taskkill /F 异步生效:等到进程树真正退出(restart 需要避免端口竞争)
956
- const deadline = Date.now() + 5000;
957
- while (Date.now() < deadline && pidAlive(pid)) {
958
- spawnSync("ping", ["-n", "2", "127.0.0.1"], { stdio: "ignore" });
1029
+ // PID 可能已退出并被系统复用给别的进程:杀前核对映像名,不符则警告并跳过,
1030
+ // 绝不 taskkill /T /F 盲杀。macOS/Linux 无按 PID 杀的路径(launchctl/systemctl
1031
+ // 按服务单元管理),只有 Windows 需要这道核对。
1032
+ const image = winPidImageName(pid);
1033
+ if (image === null) {
1034
+ console.warn(
1035
+ ZH
1036
+ ? `⚠ 无法核实 PID ${pid} 的进程映像名(tasklist 不可用或进程已消失),跳过强制结束以防误杀`
1037
+ : `⚠ Cannot verify the image name of PID ${pid} (tasklist unavailable or process gone), skipping force-kill to avoid killing an unrelated process`,
1038
+ );
1039
+ } else if (!WIN_SERVICE_IMAGE_RE.test(image)) {
1040
+ console.warn(
1041
+ ZH
1042
+ ? `⚠ PID ${pid} 现在是 ${image},不是本服务的进程(PID 可能已被复用),跳过强制结束`
1043
+ : `⚠ PID ${pid} is now ${image}, not a pi-web-ui process (PID may have been reused), skipping force-kill`,
1044
+ );
1045
+ } else {
1046
+ run("taskkill", ["/PID", String(pid), "/T", "/F"], {
1047
+ ignoreError: true,
1048
+ silent: true,
1049
+ });
1050
+ // taskkill /F 异步生效:等到进程树真正退出(restart 需要避免端口竞争)
1051
+ const deadline = Date.now() + 5000;
1052
+ while (Date.now() < deadline && pidAlive(pid)) {
1053
+ spawnSync("ping", ["-n", "2", "127.0.0.1"], { stdio: "ignore" });
1054
+ }
959
1055
  }
960
1056
  }
961
1057
  rmSync(winPidFilePath(name), { force: true });
@@ -1067,17 +1163,44 @@ function effectivePort(opts) {
1067
1163
  return String(opts.port ?? process.env.PI_WEB_PORT ?? "8787");
1068
1164
  }
1069
1165
 
1166
+ /**
1167
+ * 服务名(--name)统一校验。name 会被原样拼进 systemd unit 文件名
1168
+ * (/etc/systemd/system/<name>.service)、Windows 启动脚本/PID 文件路径
1169
+ * (%APPDATA%\pi-web-ui\<name>.ps1) 与 HKCU Run 键值名 —— 不限字符集时,
1170
+ * 路径分隔符/引号/空格都能进这些拼接点(路径穿越或 unit 内容注入)。
1171
+ * 收紧为:字母或数字开头,仅字母/数字/-/_,最长 64 字符。
1172
+ */
1173
+ function serviceName(opts) {
1174
+ const name = opts.name ?? "pi-web-ui";
1175
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name))
1176
+ fail(
1177
+ ZH
1178
+ ? `无效服务名 "${name}"(仅限字母/数字/-/_,字母或数字开头,最长 64 字符)`
1179
+ : `Invalid service name "${name}" (alphanumeric, "-" and "_" only; must start with a letter or digit; max 64 chars)`,
1180
+ );
1181
+ return name;
1182
+ }
1183
+
1070
1184
  /** Shared option normalization for install. */
1071
1185
  function serviceOptions(opts) {
1072
- const name = opts.name ?? "pi-web-ui";
1186
+ const name = serviceName(opts);
1073
1187
  const port = effectivePort(opts);
1074
1188
  if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
1075
- fail(`无效端口: ${port}`);
1189
+ fail(ZH ? `无效端口: ${port}` : `Invalid port: ${port}`);
1190
+ }
1191
+ // 服务默认工作区(issue #295):安装命令的当前目录不可靠(例如 Windows 提权提示符
1192
+ // 默认在 C:\WINDOWS\system32),且直接拿用户主目录当 agent cwd 会让 SDK 的
1193
+ // resourceLoader.reload 等同步目录扫描落在 $HOME 上 —— 家目录下的 iCloud 占位符、
1194
+ // 外部/网络卷坏挂载会让 scandir/open 在内核里挂起,整个事件循环假死(hello 后永远
1195
+ // 收不到 ready)。默认落到干净的 ~/pi-web-ui 子目录(不存在即建);显式 --cwd /
1196
+ // PI_WEB_CWD 保持原语义(不存在则报错)。前台启动仍默认当前目录。
1197
+ const explicitCwd = opts.cwd ?? process.env.PI_WEB_CWD;
1198
+ const cwd = resolve(explicitCwd ?? join(homedir(), "pi-web-ui"));
1199
+ if (explicitCwd) {
1200
+ if (!existsSync(cwd)) fail(ZH ? `工作目录不存在: ${cwd}` : `Working directory does not exist: ${cwd}`);
1201
+ } else if (!existsSync(cwd)) {
1202
+ mkdirSync(cwd, { recursive: true });
1076
1203
  }
1077
- // 服务默认以用户主目录为工作目录:安装命令的当前目录不可靠(例如 Windows 提权提示符
1078
- // 默认在 C:\WINDOWS\system32),主目录跨平台可预期;前台启动仍默认当前目录。
1079
- const cwd = resolve(opts.cwd ?? process.env.PI_WEB_CWD ?? homedir());
1080
- if (!existsSync(cwd)) fail(`工作目录不存在: ${cwd}`);
1081
1204
  let dataDir;
1082
1205
  if (opts.dataDir) {
1083
1206
  dataDir = resolve(opts.dataDir);
@@ -1086,7 +1209,8 @@ function serviceOptions(opts) {
1086
1209
  }
1087
1210
  // 引擎/监听地址/agent 配置目录:flag 优先,环境变量后备(token 不走命令行,仅环境变量)。
1088
1211
  const engine = opts.engine ?? process.env.PI_WEB_ENGINE ?? "pi";
1089
- if (engine !== "pi" && engine !== "dsh") fail(`无效引擎: ${engine}(仅支持 pi / dsh)`);
1212
+ if (engine !== "pi" && engine !== "dsh")
1213
+ fail(ZH ? `无效引擎: ${engine}(仅支持 pi / dsh)` : `Invalid engine: ${engine} (pi and dsh only)`);
1090
1214
  const host = opts.host ?? process.env.PI_WEB_HOST;
1091
1215
  const agentDir = opts.agentDir ? resolve(opts.agentDir) : process.env.PI_CODING_AGENT_DIR;
1092
1216
  return { name, port, cwd, dataDir, engine, host, agentDir };
@@ -1138,13 +1262,23 @@ function installLaunchd(opts) {
1138
1262
  mkdirSync(dirname(plist), { recursive: true });
1139
1263
  writeFileSync(plist, content);
1140
1264
  run("launchctl", ["bootstrap", `gui/${uid()}`, plist]);
1141
- console.log(`✅ 已安装并启动 launchd 服务 ${label}`);
1142
- console.log(` 端口 : ${port}`);
1143
- console.log(` 目录 : ${cwd}`);
1144
- console.log(` 访问 : http://localhost:${port}`);
1145
- console.log(` 日志 : /tmp/pi-web-ui.log /tmp/pi-web-ui.err`);
1146
- console.log(` 管理 : pi-web-ui server status|restart|stop|uninstall`);
1147
- console.log(` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`);
1265
+ console.log(ZH ? `✅ 已安装并启动 launchd 服务 ${label}` : `✅ launchd service installed and started ${label}`);
1266
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
1267
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
1268
+ console.log(ZH ? ` 访问 : http://localhost:${port}` : ` URL : http://localhost:${port}`);
1269
+ console.log(
1270
+ ZH ? ` 日志 : /tmp/pi-web-ui.log /tmp/pi-web-ui.err` : ` Log : /tmp/pi-web-ui.log /tmp/pi-web-ui.err`,
1271
+ );
1272
+ console.log(
1273
+ ZH
1274
+ ? ` 管理 : pi-web-ui server status|restart|stop|uninstall`
1275
+ : ` Manage : pi-web-ui server status|restart|stop|uninstall`,
1276
+ );
1277
+ console.log(
1278
+ ZH
1279
+ ? ` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`
1280
+ : ` Tip : pi-web-ui server shortcut creates a “one-click launch” icon on the desktop`,
1281
+ );
1148
1282
  }
1149
1283
 
1150
1284
  /** Elevate only the privileged operation, not the CLI that captures the user's environment.
@@ -1155,7 +1289,8 @@ function runSystemdRoot(command, args, input) {
1155
1289
  input,
1156
1290
  stdio: input === undefined ? "inherit" : ["pipe", "inherit", "inherit"],
1157
1291
  });
1158
- if (result.error) fail(`无法执行 ${command}: ${result.error.message}`);
1292
+ if (result.error)
1293
+ fail(ZH ? `无法执行 ${command}: ${result.error.message}` : `Cannot run ${command}: ${result.error.message}`);
1159
1294
  if (result.status !== 0) process.exit(result.status ?? 1);
1160
1295
  }
1161
1296
 
@@ -1183,17 +1318,27 @@ function installSystemd(opts) {
1183
1318
  runSystemdRoot("systemctl", ["enable", `${name}.service`]);
1184
1319
  // enable --now does not restart an already-active unit after configuration changes.
1185
1320
  runSystemdRoot("systemctl", ["restart", `${name}.service`]);
1186
- console.log(`✅ 已安装并启动 systemd 服务 ${name}.service`);
1187
- console.log(` 端口 : ${port}`);
1188
- console.log(` 目录 : ${cwd}`);
1189
- console.log(` 访问 : http://localhost:${port}`);
1190
- console.log(` 日志 : journalctl -u ${name}.service -f`);
1191
- console.log(` 管理 : pi-web-ui server status|restart|stop|uninstall`);
1192
- console.log(` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`);
1321
+ console.log(
1322
+ ZH ? `✅ 已安装并启动 systemd 服务 ${name}.service` : `✅ systemd service installed and started ${name}.service`,
1323
+ );
1324
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
1325
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
1326
+ console.log(ZH ? ` 访问 : http://localhost:${port}` : ` URL : http://localhost:${port}`);
1327
+ console.log(ZH ? ` 日志 : journalctl -u ${name}.service -f` : ` Log : journalctl -u ${name}.service -f`);
1328
+ console.log(
1329
+ ZH
1330
+ ? ` 管理 : pi-web-ui server status|restart|stop|uninstall`
1331
+ : ` Manage : pi-web-ui server status|restart|stop|uninstall`,
1332
+ );
1333
+ console.log(
1334
+ ZH
1335
+ ? ` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`
1336
+ : ` Tip : pi-web-ui server shortcut creates a “one-click launch” icon on the desktop`,
1337
+ );
1193
1338
  }
1194
1339
 
1195
1340
  function uninstallLaunchd(opts) {
1196
- const name = opts.name ?? "pi-web-ui";
1341
+ const name = serviceName(opts);
1197
1342
  const label = serviceLabel(name);
1198
1343
  const plist = launchAgentPlist(name);
1199
1344
  run("launchctl", ["bootout", `gui/${uid()}/${label}`], {
@@ -1202,12 +1347,16 @@ function uninstallLaunchd(opts) {
1202
1347
  });
1203
1348
  if (existsSync(plist)) rmSync(plist);
1204
1349
  removeShortcut(name);
1205
- console.log(`🗑 已卸载 ${label}(plist 已删除,不再开机自启)`);
1206
- console.log(`🗑 已移除桌面快捷方式`);
1350
+ console.log(
1351
+ ZH
1352
+ ? `🗑 已卸载 ${label}(plist 已删除,不再开机自启)`
1353
+ : `🗑 Uninstalled ${label} (plist deleted, no longer auto-starts at login)`,
1354
+ );
1355
+ console.log(ZH ? `🗑 已移除桌面快捷方式` : `🗑 Desktop shortcut removed`);
1207
1356
  }
1208
1357
 
1209
1358
  function uninstallSystemd(opts) {
1210
- const name = opts.name ?? "pi-web-ui";
1359
+ const name = serviceName(opts);
1211
1360
  ensureRootForSystemctl();
1212
1361
  run("systemctl", ["disable", "--now", `${name}.service`], {
1213
1362
  ignoreError: true,
@@ -1216,8 +1365,12 @@ function uninstallSystemd(opts) {
1216
1365
  if (existsSync(unitPath)) rmSync(unitPath);
1217
1366
  run("systemctl", ["daemon-reload"]);
1218
1367
  removeShortcut(name);
1219
- console.log(`🗑 已卸载 ${name}.service(不再开机自启)`);
1220
- console.log(`🗑 已移除桌面快捷方式`);
1368
+ console.log(
1369
+ ZH
1370
+ ? `🗑 已卸载 ${name}.service(不再开机自启)`
1371
+ : `🗑 Uninstalled ${name}.service (no longer auto-starts at login)`,
1372
+ );
1373
+ console.log(ZH ? `🗑 已移除桌面快捷方式` : `🗑 Desktop shortcut removed`);
1221
1374
  }
1222
1375
 
1223
1376
  function installWindows(opts) {
@@ -1233,7 +1386,7 @@ function installWindows(opts) {
1233
1386
  if (opts.print) {
1234
1387
  console.log(`# ${ps1Path}\n${ps1}`);
1235
1388
  console.log(`# ${vbsPath}\n${vbs}`);
1236
- console.log(`# 登录自启(HKCU Run 键,无需管理员)`);
1389
+ console.log(ZH ? `# 登录自启(HKCU Run 键,无需管理员)` : `# Login auto-start (HKCU Run key, no admin required)`);
1237
1390
  console.log(` reg add "HKCU\\${winRunKeyName()}" /v ${name} /t REG_SZ /d "${runValue}" /f`);
1238
1391
  return;
1239
1392
  }
@@ -1258,19 +1411,35 @@ function installWindows(opts) {
1258
1411
  }
1259
1412
  winRunKeySet(name, runValue);
1260
1413
  launchWinService(vbsPath);
1261
- console.log(`✅ 已安装并启动 ${name}(登录自启 · HKCU Run 键 · 无需管理员)`);
1262
- console.log(` 窗口 : wscript 隐藏启动,无黑窗`);
1263
- console.log(` 端口 : ${port}`);
1264
- console.log(` 目录 : ${cwd}`);
1265
- console.log(` 访问 : http://localhost:${port}`);
1266
- console.log(` 日志 : ${winLogPath(name)}`);
1267
- console.log(` 说明 : 崩溃后 10 秒自动重启(看门狗);stop 停止,uninstall 移除`);
1268
- console.log(` 管理 : pi-web-ui server status|restart|stop|uninstall`);
1269
- console.log(` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`);
1414
+ console.log(
1415
+ ZH
1416
+ ? `✅ 已安装并启动 ${name}(登录自启 · HKCU Run 键 · 无需管理员)`
1417
+ : `✅ Installed and started ${name} (login auto-start · HKCU Run key · no admin required)`,
1418
+ );
1419
+ console.log(ZH ? ` 窗口 : wscript 隐藏启动,无黑窗` : ` Window : wscript hidden launch, no console`);
1420
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
1421
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
1422
+ console.log(ZH ? ` 访问 : http://localhost:${port}` : ` URL : http://localhost:${port}`);
1423
+ console.log(ZH ? ` 日志 : ${winLogPath(name)}` : ` Log : ${winLogPath(name)}`);
1424
+ console.log(
1425
+ ZH
1426
+ ? ` 说明 : 崩溃后 10 秒自动重启(看门狗);stop 停止,uninstall 移除`
1427
+ : ` Notes : auto-restarts 10 s after crash (watchdog); stop to stop, uninstall to remove`,
1428
+ );
1429
+ console.log(
1430
+ ZH
1431
+ ? ` 管理 : pi-web-ui server status|restart|stop|uninstall`
1432
+ : ` Manage : pi-web-ui server status|restart|stop|uninstall`,
1433
+ );
1434
+ console.log(
1435
+ ZH
1436
+ ? ` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`
1437
+ : ` Tip : pi-web-ui server shortcut creates a “one-click launch” icon on the desktop`,
1438
+ );
1270
1439
  }
1271
1440
 
1272
1441
  function uninstallWindows(opts) {
1273
- const name = opts.name ?? "pi-web-ui";
1442
+ const name = serviceName(opts);
1274
1443
  winRunKeyDelete(name);
1275
1444
  // 历史计划任务(旧版本 install 可能注册过)
1276
1445
  if (winTaskExists(name)) {
@@ -1282,8 +1451,12 @@ function uninstallWindows(opts) {
1282
1451
  if (existsSync(f)) rmSync(f);
1283
1452
  }
1284
1453
  removeShortcut(name);
1285
- console.log(`🗑 已卸载 ${name}(登录自启已移除,不再开机自启)`);
1286
- console.log(`🗑 已移除桌面快捷方式`);
1454
+ console.log(
1455
+ ZH
1456
+ ? `🗑 已卸载 ${name}(登录自启已移除,不再开机自启)`
1457
+ : `🗑 Uninstalled ${name} (login auto-start removed, no longer auto-starts at login)`,
1458
+ );
1459
+ console.log(ZH ? `🗑 已移除桌面快捷方式` : `🗑 Desktop shortcut removed`);
1287
1460
  }
1288
1461
 
1289
1462
  // ---------------------------------------------------------------------------
@@ -1340,24 +1513,40 @@ function controlCommand(opts, cmd) {
1340
1513
  async function printLiveStatus(opts) {
1341
1514
  const st = await controlCommand(opts, "status");
1342
1515
  if (!st || !st.ok) {
1343
- console.log(" (服务器未运行或控制通道不可达 — 启动后可查 server status 实时信息)");
1516
+ console.log(
1517
+ ZH
1518
+ ? " (服务器未运行或控制通道不可达 — 启动后可查 server status 实时信息)"
1519
+ : " (server not running or control channel unreachable — start it, then check server status for live info)",
1520
+ );
1344
1521
  return;
1345
1522
  }
1346
- console.log(" --- 实时状态 (control socket) ---");
1347
- console.log(` 版本 : ${st.version} · PID ${st.pid}`);
1348
- console.log(` 目录 : ${st.cwd}`);
1523
+ console.log(ZH ? " --- 实时状态 (control socket) ---" : " --- live status (control socket) ---");
1524
+ console.log(ZH ? ` 版本 : ${st.version} · PID ${st.pid}` : ` Version : ${st.version} · PID ${st.pid}`);
1525
+ console.log(ZH ? ` 目录 : ${st.cwd}` : ` Directory : ${st.cwd}`);
1349
1526
  // 启动来源(server/launch-origin.ts):有 supervisor = 这个进程退出后会被自动
1350
1527
  // 拉起(server restart / 更新面板的「重启服务」才有意义)。
1351
1528
  console.log(
1352
- ` 启动 : ${
1353
- st.service
1354
- ? `pi-web-ui 服务(${st.service.supervisor} · ${st.service.name})`
1355
- : "前台 / 开发模式(无 supervisor,退出不自动重启)"
1356
- }`,
1529
+ ZH
1530
+ ? ` 启动 : ${
1531
+ st.service
1532
+ ? `pi-web-ui 服务(${st.service.supervisor} · ${st.service.name})`
1533
+ : "前台 / 开发模式(无 supervisor,退出不自动重启)"
1534
+ }`
1535
+ : ` Started : ${
1536
+ st.service
1537
+ ? `pi-web-ui service (${st.service.supervisor} · ${st.service.name})`
1538
+ : "foreground / dev mode (no supervisor, exit does not auto-restart)"
1539
+ }`,
1540
+ );
1541
+ console.log(
1542
+ ZH
1543
+ ? ` 排空 : ${st.quiesced ? `是(自 ${new Date(st.quiescedSince).toLocaleString()})` : "否"}`
1544
+ : ` Drain : ${st.quiesced ? `yes (since ${new Date(st.quiescedSince).toLocaleString()})` : "no"}`,
1357
1545
  );
1358
- console.log(` 排空 : ${st.quiesced ? `是(自 ${new Date(st.quiescedSince).toLocaleString()})` : "否"}`);
1359
1546
  console.log(
1360
- ` 连接 : ${st.connectedClients} 个浏览器 · ${st.activeConversations} 个运行中对话 · ${st.pendingMessages} 条排队消息`,
1547
+ ZH
1548
+ ? ` 连接 : ${st.connectedClients} 个浏览器 · ${st.activeConversations} 个运行中对话 · ${st.pendingMessages} 条排队消息`
1549
+ : ` Conns : ${st.connectedClients} browser · ${st.activeConversations} active session · ${st.pendingMessages} queued messages`,
1361
1550
  );
1362
1551
  }
1363
1552
 
@@ -1365,18 +1554,27 @@ async function printLiveStatus(opts) {
1365
1554
  async function setQuiesce(opts, on) {
1366
1555
  const st = await controlCommand(opts, on ? "quiesce" : "unquiesce");
1367
1556
  if (!st || !st.ok) {
1368
- fail(`服务器未运行或控制通道不可达(${controlPath(opts)})`);
1557
+ fail(
1558
+ ZH
1559
+ ? `服务器未运行或控制通道不可达(${controlPath(opts)})`
1560
+ : `Server not running or control channel unreachable (${controlPath(opts)})`,
1561
+ );
1369
1562
  }
1370
1563
  console.log(
1371
1564
  on
1372
- ? "⏸ 已进入排空模式(quiesce):拒绝新的对话/消息/编辑,存量运行继续跑完。\n" +
1565
+ ? ZH
1566
+ ? "⏸ 已进入排空模式(quiesce):拒绝新的对话/消息/编辑,存量运行继续跑完。\n" +
1373
1567
  " 跑完后用 pi-web-ui server unquiesce 恢复。"
1374
- : "▶ 已解除排空模式(unquiesce):恢复接收新的对话/消息/编辑。",
1568
+ : "⏸ Quiesce mode active: new conversations/messages/edits rejected; existing runs continue to completion.\n" +
1569
+ " Use pi-web-ui server unquiesce to resume when done."
1570
+ : ZH
1571
+ ? "▶ 已解除排空模式(unquiesce):恢复接收新的对话/消息/编辑。"
1572
+ : "▶ Unquiesced: now accepting new conversations/messages/edits.",
1375
1573
  );
1376
1574
  }
1377
1575
 
1378
1576
  function controlService(action, opts) {
1379
- const name = opts.name ?? "pi-web-ui";
1577
+ const name = serviceName(opts);
1380
1578
 
1381
1579
  if (isMac) {
1382
1580
  const label = serviceLabel(name);
@@ -1389,9 +1587,15 @@ function controlService(action, opts) {
1389
1587
  encoding: "utf8",
1390
1588
  });
1391
1589
  const state = (res.stdout.match(/state = (\w+)/) ?? [])[1] ?? "loaded";
1392
- console.log(`${label}: ${state}(已加载,开机自启中)`);
1590
+ console.log(
1591
+ ZH ? `${label}: ${state}(已加载,开机自启中)` : `${label}: ${state} (loaded, auto-start enabled)`,
1592
+ );
1393
1593
  } else {
1394
- console.log(`${label}: 未安装(运行 pi-web-ui server install 安装)`);
1594
+ console.log(
1595
+ ZH
1596
+ ? `${label}: 未安装(运行 pi-web-ui server install 安装)`
1597
+ : `${label}: not installed (run pi-web-ui server install)`,
1598
+ );
1395
1599
  }
1396
1600
  return;
1397
1601
  }
@@ -1402,18 +1606,25 @@ function controlService(action, opts) {
1402
1606
  } else {
1403
1607
  const plist = launchAgentPlist(name);
1404
1608
  if (!existsSync(plist)) {
1405
- fail(`找不到 ${plist},请先运行 pi-web-ui server install`);
1609
+ fail(
1610
+ ZH
1611
+ ? `找不到 ${plist},请先运行 pi-web-ui server install`
1612
+ : `Cannot find ${plist}, please run pi-web-ui server install first`,
1613
+ );
1406
1614
  }
1407
1615
  run("launchctl", ["bootstrap", `gui/${uid()}`, plist]);
1408
1616
  }
1409
- console.log(`✅ 已启动 ${label}`);
1617
+ console.log(ZH ? `✅ 已启动 ${label}` : `✅ Started ${label}`);
1410
1618
  return;
1411
1619
  }
1412
1620
 
1413
1621
  if (action === "restart") {
1414
- if (!loaded()) fail(`${label} 未加载,请先 pi-web-ui server start`);
1622
+ if (!loaded())
1623
+ fail(
1624
+ ZH ? `${label} 未加载,请先 pi-web-ui server start` : `${label} not loaded, run pi-web-ui server start first`,
1625
+ );
1415
1626
  run("launchctl", ["kickstart", "-k", target]);
1416
- console.log(`✅ 已重启 ${label}`);
1627
+ console.log(ZH ? `✅ 已重启 ${label}` : `✅ Restarted ${label}`);
1417
1628
  return;
1418
1629
  }
1419
1630
 
@@ -1422,11 +1633,15 @@ function controlService(action, opts) {
1422
1633
  ignoreError: true,
1423
1634
  silent: true,
1424
1635
  });
1425
- console.log(`⏹ 已停止 ${label}(已卸载,不再开机自启;start 恢复)`);
1636
+ console.log(
1637
+ ZH
1638
+ ? `⏹ 已停止 ${label}(已卸载,不再开机自启;start 恢复)`
1639
+ : `⏹ Stopped ${label} (uninstalled, auto-start removed; start to resume)`,
1640
+ );
1426
1641
  return;
1427
1642
  }
1428
1643
 
1429
- fail(`未知操作: ${action}`);
1644
+ fail(ZH ? `未知操作: ${action}` : `Unknown operation: ${action}`);
1430
1645
  }
1431
1646
 
1432
1647
  if (isLinux) {
@@ -1448,22 +1663,41 @@ function controlService(action, opts) {
1448
1663
  const pid = winReadPid(name);
1449
1664
  const instAlive = pid && pidAlive(pid);
1450
1665
  if (legacy) {
1451
- console.log(`${name}: 旧版计划任务安装(未迁移)`);
1452
- console.log(` 提示 : 重新执行 server install 可迁移到登录自启模式(删除任务,无需管理员)`);
1453
- if (instAlive) console.log(` 实例 : 运行中 (PID ${pid})`);
1666
+ console.log(
1667
+ ZH ? `${name}: 旧版计划任务安装(未迁移)` : `${name}: legacy scheduled task install (not migrated)`,
1668
+ );
1669
+ console.log(
1670
+ ZH
1671
+ ? ` 提示 : 重新执行 server install 可迁移到登录自启模式(删除任务,无需管理员)`
1672
+ : ` Hint : re-run server install to migrate to login auto-start mode (removes task, no admin required)`,
1673
+ );
1674
+ if (instAlive) console.log(ZH ? ` 实例 : 运行中 (PID ${pid})` : ` Instance : running (PID ${pid})`);
1454
1675
  return;
1455
1676
  }
1456
1677
  if (!installed) {
1457
- console.log(`${name}: 未安装(运行 pi-web-ui server install 安装)`);
1458
- if (instAlive) console.log(` 快捷方式实例 : 运行中 (PID ${pid})`);
1678
+ console.log(
1679
+ ZH
1680
+ ? `${name}: 未安装(运行 pi-web-ui server install 安装)`
1681
+ : `${name}: not installed (run pi-web-ui server install)`,
1682
+ );
1683
+ if (instAlive)
1684
+ console.log(ZH ? ` 快捷方式实例 : 运行中 (PID ${pid})` : ` Shortcut : running (PID ${pid})`);
1459
1685
  return;
1460
1686
  }
1461
- console.log(`${name}: 已安装(登录自启 · HKCU Run 键 · wscript 隐藏启动,无黑窗)`);
1687
+ console.log(
1688
+ ZH
1689
+ ? `${name}: 已安装(登录自启 · HKCU Run 键 · wscript 隐藏启动,无黑窗)`
1690
+ : `${name}: installed (login auto-start · HKCU Run key · wscript silent launch, no console window)`,
1691
+ );
1462
1692
  if (instAlive) {
1463
- console.log(` 运行状态 : 运行中 (PID ${pid})`);
1464
- console.log(` 日志 : ${winLogPath(name)}`);
1693
+ console.log(ZH ? ` 运行状态 : 运行中 (PID ${pid})` : ` Status : running (PID ${pid})`);
1694
+ console.log(ZH ? ` 日志 : ${winLogPath(name)}` : ` Log : ${winLogPath(name)}`);
1465
1695
  } else {
1466
- console.log(` 运行状态 : 未运行(pi-web-ui server start 启动)`);
1696
+ console.log(
1697
+ ZH
1698
+ ? ` 运行状态 : 未运行(pi-web-ui server start 启动)`
1699
+ : ` Status : not running (pi-web-ui server start to launch)`,
1700
+ );
1467
1701
  }
1468
1702
  return;
1469
1703
  }
@@ -1471,17 +1705,26 @@ function controlService(action, opts) {
1471
1705
  if (action === "start") {
1472
1706
  if (legacy) {
1473
1707
  run("schtasks", ["/Run", "/TN", name]);
1474
- console.log(`✅ 已启动 ${name}(旧版计划任务,建议重新 server install 迁移)`);
1708
+ console.log(
1709
+ ZH
1710
+ ? `✅ 已启动 ${name}(旧版计划任务,建议重新 server install 迁移)`
1711
+ : `✅ Started ${name} (legacy scheduled task, recommend re-running server install to migrate)`,
1712
+ );
1475
1713
  return;
1476
1714
  }
1477
- if (!installed) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
1715
+ if (!installed)
1716
+ fail(
1717
+ ZH
1718
+ ? `${name} 不存在,请先运行 pi-web-ui server install`
1719
+ : `${name} does not exist, run pi-web-ui server install first`,
1720
+ );
1478
1721
  const pid = winReadPid(name);
1479
1722
  if (pid && pidAlive(pid)) {
1480
- console.log(`✅ ${name} 已在运行 (PID ${pid})`);
1723
+ console.log(ZH ? `✅ ${name} 已在运行 (PID ${pid})` : `✅ ${name} already running (PID ${pid})`);
1481
1724
  return;
1482
1725
  }
1483
1726
  launchWinService(winVbsPath(name));
1484
- console.log(`✅ 已启动 ${name}`);
1727
+ console.log(ZH ? `✅ 已启动 ${name}` : `✅ Started ${name}`);
1485
1728
  return;
1486
1729
  }
1487
1730
 
@@ -1492,13 +1735,22 @@ function controlService(action, opts) {
1492
1735
  silent: true,
1493
1736
  });
1494
1737
  run("schtasks", ["/Run", "/TN", name]);
1495
- console.log(`✅ 已重启 ${name}(旧版计划任务,建议重新 server install 迁移)`);
1738
+ console.log(
1739
+ ZH
1740
+ ? `✅ 已重启 ${name}(旧版计划任务,建议重新 server install 迁移)`
1741
+ : `✅ Restarted ${name} (legacy scheduled task, recommend re-running server install to migrate)`,
1742
+ );
1496
1743
  return;
1497
1744
  }
1498
- if (!installed) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
1745
+ if (!installed)
1746
+ fail(
1747
+ ZH
1748
+ ? `${name} 不存在,请先运行 pi-web-ui server install`
1749
+ : `${name} does not exist, run pi-web-ui server install first`,
1750
+ );
1499
1751
  stopWinInstance(name);
1500
1752
  launchWinService(winVbsPath(name));
1501
- console.log(`✅ 已重启 ${name}`);
1753
+ console.log(ZH ? `✅ 已重启 ${name}` : `✅ Restarted ${name}`);
1502
1754
  return;
1503
1755
  }
1504
1756
 
@@ -1509,18 +1761,30 @@ function controlService(action, opts) {
1509
1761
  silent: true,
1510
1762
  });
1511
1763
  stopWinInstance(name);
1512
- console.log(`⏹ 已停止 ${name}(旧版计划任务;重新 server install 可迁移到登录自启)`);
1764
+ console.log(
1765
+ ZH
1766
+ ? `⏹ 已停止 ${name}(旧版计划任务;重新 server install 可迁移到登录自启)`
1767
+ : `⏹ Stopped ${name} (legacy scheduled task; re-run server install to migrate to login auto-start)`,
1768
+ );
1513
1769
  return;
1514
1770
  }
1515
1771
  stopWinInstance(name);
1516
- console.log(`⏹ 已停止 ${name}(自启保留;uninstall 移除)`);
1772
+ console.log(
1773
+ ZH
1774
+ ? `⏹ 已停止 ${name}(自启保留;uninstall 移除)`
1775
+ : `⏹ Stopped ${name} (auto-start preserved; uninstall to remove)`,
1776
+ );
1517
1777
  return;
1518
1778
  }
1519
1779
 
1520
- fail(`未知操作: ${action}`);
1780
+ fail(ZH ? `未知操作: ${action}` : `Unknown operation: ${action}`);
1521
1781
  }
1522
1782
 
1523
- fail(`不支持的系统服务平台: ${process.platform}(仅 macOS / Linux / Windows)`);
1783
+ fail(
1784
+ ZH
1785
+ ? `不支持的系统服务平台: ${process.platform}(仅 macOS / Linux / Windows)`
1786
+ : `Unsupported system service platform: ${process.platform} (macOS / Linux / Windows only)`,
1787
+ );
1524
1788
  }
1525
1789
 
1526
1790
  // ---------------------------------------------------------------------------
@@ -1530,7 +1794,8 @@ function controlService(action, opts) {
1530
1794
  /** 合法插件 id(同 server/plugins.ts 的 ID_RE)。 */
1531
1795
  const PLUGIN_ID_RE = /^[A-Za-z0-9_-]+$/;
1532
1796
 
1533
- const PLUGIN_HELP = `用法:
1797
+ const PLUGIN_HELP = ZH
1798
+ ? `用法:
1534
1799
  pi-web-ui plugin create <id> [选项] 生成最小可跑的插件骨架
1535
1800
  pi-web-ui install <源> [选项] 安装 GitHub 上的界面插件
1536
1801
  pi-web-ui install --catalog <目录> [选项] 同步插件市场目录并逐条安装
@@ -1573,6 +1838,50 @@ upgrade-sdk 选项:
1573
1838
  plugins 选项:
1574
1839
  --check-updates 逐个对比最近安装版本与远端 HEAD,列出可更新插件
1575
1840
  --rollback <id> 回滚到最近一份更新前备份(<dataDir>/plugin-backups/)
1841
+ `
1842
+ : `Usage:
1843
+ pi-web-ui plugin create <id> [options] generate a minimal runnable plugin skeleton
1844
+ pi-web-ui install <source> [options] install a UI plugin from GitHub
1845
+ pi-web-ui install --catalog <catalog> [options] sync plugin catalog and install each entry
1846
+ pi-web-ui plugin create <id> [options] generate plugin skeleton (minimal|ui-slot|agent-tool|renderer)
1847
+ pi-web-ui plugin upgrade-sdk [id] [options] refresh SDK copy in installed plugins (only when SDK_VERSION mismatches)
1848
+ pi-web-ui uninstall <id> [options] uninstall an installed UI plugin
1849
+ pi-web-ui plugins [options] list installed UI plugins
1850
+
1851
+ Source formats (choose one):
1852
+ owner/repo shorthand
1853
+ https://github.com/owner/repo full URL (.git optional)
1854
+ https://github.com/o/r/tree/dev/sub/dir specify branch + subdirectory in repo
1855
+ append #branch-or-tag to any of the above specify branch/tag (e.g. owner/repo#v1.2)
1856
+ /path/to/plugin-dir local directory (offline dev/debug)
1857
+ Catalog format (for --catalog):
1858
+ https://example.com/catalog.json remote catalog document (array or {entries:[...]})
1859
+ /path/to/catalog.json local catalog document (absolute path)
1860
+ install options:
1861
+ --name <id> plugin directory name/id (default: repo name or manifest.id; alphanumeric -_ only)
1862
+ --data-dir <dir> data directory (default ~/.pi-web or $PI_WEB_DATA_DIR)
1863
+ --force overwrite if target directory exists (auto-backup before overwrite)
1864
+ --build force source build: install build deps and compile in an isolated temp directory
1865
+ (manifest.build or package.json scripts.build),
1866
+ replaces target only on success with complete artifacts; no partial state on failure
1867
+ --no-build skip build even if plugin is source-only (mutually exclusive with --build; plugin will not load without artifacts)
1868
+ --catalog <catalog> catalog sync mode: read catalog → atomically write installable list → install/update each entry
1869
+ (installed entries skipped by default; add --force to update; single failure does not abort batch)
1870
+ --replace used with --catalog: replace installable list entirely (default merges by id, preserving old entries)
1871
+
1872
+ create options:
1873
+ --template <t> minimal (default, no permissions) | ui-slot | agent-tool | renderer
1874
+ --dir <dir> plugin parent directory (default <data-dir>/plugins; or use --data-dir to set data directory)
1875
+ --force overwrite if target directory exists
1876
+ --with-test also generate index.test.mjs (node --test + createMockHost minimal unit test; requires in-package SDK)
1877
+
1878
+ upgrade-sdk options:
1879
+ [id] refresh only this plugin (default: refresh all plugins with an sdk copy)
1880
+ --dir <dir> plugin parent directory (default <data-dir>/plugins; or use --data-dir to set data directory)
1881
+
1882
+ plugins options:
1883
+ --check-updates compare each installed version against remote HEAD, list updatable plugins
1884
+ --rollback <id> roll back to the most recent pre-update backup (<dataDir>/plugin-backups/)
1576
1885
  `;
1577
1886
 
1578
1887
  function pluginDataDir(opts) {
@@ -1586,7 +1895,12 @@ function parsePluginSource(rawSpec) {
1586
1895
  const hash = spec.indexOf("#");
1587
1896
  if (hash >= 0) {
1588
1897
  ref = spec.slice(hash + 1).trim();
1589
- if (!ref) fail(`无效的源 "${rawSpec}":# 后缺少分支/tag 名`);
1898
+ if (!ref)
1899
+ fail(
1900
+ ZH
1901
+ ? `无效的源 "${rawSpec}":# 后缺少分支/tag 名`
1902
+ : `Invalid source "${rawSpec}": branch/tag name missing after #`,
1903
+ );
1590
1904
  spec = spec.slice(0, hash).replace(/\/+$/, "");
1591
1905
  }
1592
1906
  // ssh 形式转 https 拉取(不要求本机配 ssh key);URL 去掉协议前缀统一按路径段解析
@@ -1597,9 +1911,17 @@ function parsePluginSource(rawSpec) {
1597
1911
  if (url) [, spec] = url;
1598
1912
  }
1599
1913
  const segs = spec.split("/").filter(Boolean);
1600
- if (segs.length < 2) fail(`无法识别的插件源 "${rawSpec}"\n${PLUGIN_HELP}`);
1914
+ if (segs.length < 2)
1915
+ fail(
1916
+ ZH ? `无法识别的插件源 "${rawSpec}"\n${PLUGIN_HELP}` : `Unrecognized plugin source "${rawSpec}"\n${PLUGIN_HELP}`,
1917
+ );
1601
1918
  for (const s of segs) {
1602
- if (s === "." || s === "..") fail(`无效的源 "${rawSpec}":路径段不能是 . 或 ..`);
1919
+ if (s === "." || s === "..")
1920
+ fail(
1921
+ ZH
1922
+ ? `无效的源 "${rawSpec}":路径段不能是 . 或 ..`
1923
+ : `Invalid source "${rawSpec}": path segments cannot be . or ..`,
1924
+ );
1603
1925
  }
1604
1926
  const [owner, repo] = segs;
1605
1927
  let subpath;
@@ -1627,10 +1949,14 @@ async function acquireRepo(src, tmpDir) {
1627
1949
  timeout: 300_000,
1628
1950
  });
1629
1951
  if (res.status === 0 && existsSync(dst)) return dst;
1630
- console.log("· git clone 失败,回退到 tarball 直连下载…");
1952
+ console.log(
1953
+ ZH
1954
+ ? "· git clone 失败,回退到 tarball 直连下载…"
1955
+ : "· git clone failed, falling back to direct tarball download…",
1956
+ );
1631
1957
  }
1632
1958
  const url = `https://codeload.github.com/${src.owner}/${src.repo}/tar.gz/${src.ref || "HEAD"}`;
1633
- console.log(`· 下载 ${url}`);
1959
+ console.log(ZH ? `· 下载 ${url}` : `· Downloading ${url}`);
1634
1960
  // 注意:这里不用 fail()/process.exit —— async 上下文里还有未关闭的 socket 时
1635
1961
  // 直接退出会触发 Windows libuv "UV_HANDLE_CLOSING" 断言崩溃;改为 throw,
1636
1962
  // 由 pluginInstallCmd 捕获后设 exitCode 让事件循环自然排空。
@@ -1638,13 +1964,19 @@ async function acquireRepo(src, tmpDir) {
1638
1964
  try {
1639
1965
  res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
1640
1966
  } catch (err) {
1641
- throw new Error(`下载失败:${err?.message ?? err}\n 请检查网络/代理后重试。`);
1967
+ throw new Error(
1968
+ ZH
1969
+ ? `下载失败:${err?.message ?? err}\n 请检查网络/代理后重试。`
1970
+ : `Download failed: ${err?.message ?? err}\n Check your network/proxy and retry.`,
1971
+ );
1642
1972
  }
1643
1973
  if (!res.ok)
1644
1974
  throw new Error(
1645
- `下载失败 HTTP ${res.status}:${url}` +
1975
+ (ZH ? `下载失败 HTTP ${res.status}:${url}` : `Download failed HTTP ${res.status}: ${url}`) +
1646
1976
  (res.status === 404
1647
- ? "\n 仓库/分支不存在,或为私有仓库(私有仓库请先在本机配置好 git 凭据再重试,会优先走 git clone)。"
1977
+ ? ZH
1978
+ ? "\n 仓库/分支不存在,或为私有仓库(私有仓库请先在本机配置好 git 凭据再重试,会优先走 git clone)。"
1979
+ : "\n Repository/branch does not exist, or is private (for private repos, configure git credentials locally first and retry; git clone will be tried first)."
1648
1980
  : ""),
1649
1981
  );
1650
1982
  writeFileSync(join(tmpDir, "src.tar.gz"), Buffer.from(await res.arrayBuffer()));
@@ -1655,9 +1987,19 @@ async function acquireRepo(src, tmpDir) {
1655
1987
  cwd: tmpDir,
1656
1988
  stdio: "inherit",
1657
1989
  });
1658
- if (tarRes.status !== 0) fail("tar 解压失败(可重试,或手动下载 release 包解压)");
1990
+ if (tarRes.status !== 0)
1991
+ fail(
1992
+ ZH
1993
+ ? "tar 解压失败(可重试,或手动下载 release 包解压)"
1994
+ : "tar extraction failed (retry, or manually download and extract the release archive)",
1995
+ );
1659
1996
  const entries = readdirSync(extractTo);
1660
- if (entries.length !== 1) fail("tarball 解压结果异常(顶层应只有一个目录)");
1997
+ if (entries.length !== 1)
1998
+ fail(
1999
+ ZH
2000
+ ? "tarball 解压结果异常(顶层应只有一个目录)"
2001
+ : "tarball extraction result unexpected (top level should contain exactly one directory)",
2002
+ );
1661
2003
  return join(extractTo, entries[0]);
1662
2004
  }
1663
2005
 
@@ -1683,18 +2025,30 @@ function findManifestDirs(root) {
1683
2025
  function locatePluginRoot(checkout, subpath, repoLabel) {
1684
2026
  if (subpath) {
1685
2027
  const dir = join(checkout, ...subpath.split("/"));
1686
- if (!existsSync(join(dir, "manifest.json"))) fail(`子目录 "${subpath}" 里没有 manifest.json`);
2028
+ if (!existsSync(join(dir, "manifest.json")))
2029
+ fail(ZH ? `子目录 "${subpath}" 里没有 manifest.json` : `Subdirectory "${subpath}" has no manifest.json`);
1687
2030
  return dir;
1688
2031
  }
1689
2032
  if (existsSync(join(checkout, "manifest.json"))) return checkout;
1690
2033
  const hits = findManifestDirs(checkout);
1691
- if (hits.length === 0) fail(`"${repoLabel}" 里没找到 manifest.json —— 不是 pi-web-ui 界面插件`);
2034
+ if (hits.length === 0)
2035
+ fail(
2036
+ ZH
2037
+ ? `"${repoLabel}" 里没找到 manifest.json —— 不是 pi-web-ui 界面插件`
2038
+ : `"${repoLabel}" does not contain manifest.json — not a pi-web-ui UI plugin`,
2039
+ );
1692
2040
  if (hits.length > 1)
1693
2041
  fail(
1694
- `${repoLabel} 里有多个插件(多个 manifest.json),请用子目录写法指定其中一个:\n ` +
2042
+ (ZH
2043
+ ? `${repoLabel} 里有多个插件(多个 manifest.json),请用子目录写法指定其中一个:\n `
2044
+ : `${repoLabel} contains multiple plugins (multiple manifest.json files), use a subdirectory path to specify one:\n `) +
1695
2045
  hits.map((h) => `${repoLabel}/${relative(checkout, h).split(/[\\/]/).join("/")}`).join("\n "),
1696
2046
  );
1697
- console.log(`· 插件位于子目录: ${relative(checkout, hits[0]).split(/[\\/]/).join("/")}`);
2047
+ console.log(
2048
+ ZH
2049
+ ? `· 插件位于子目录: ${relative(checkout, hits[0]).split(/[\\/]/).join("/")}`
2050
+ : `· Plugin found in subdirectory: ${relative(checkout, hits[0]).split(/[\\/]/).join("/")}`,
2051
+ );
1698
2052
  return hits[0];
1699
2053
  }
1700
2054
 
@@ -1742,7 +2096,12 @@ function runBuildCommand(cmd, cwd, label) {
1742
2096
  timeout: 600_000,
1743
2097
  env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false", NO_COLOR: "1" },
1744
2098
  });
1745
- if (res.status !== 0) throw new Error(`${label} 失败(退出码 ${res.status ?? "?"}):${cmd}`);
2099
+ if (res.status !== 0)
2100
+ throw new Error(
2101
+ ZH
2102
+ ? `${label} 失败(退出码 ${res.status ?? "?"}):${cmd}`
2103
+ : `${label} failed (exit code ${res.status ?? "?"}): ${cmd}`,
2104
+ );
1746
2105
  }
1747
2106
 
1748
2107
  /**
@@ -1756,16 +2115,27 @@ function buildPluginSource(pluginRoot, tmpDir, manifest) {
1756
2115
  const plan = resolveBuildPlan(pluginRoot, manifest);
1757
2116
  if (!plan)
1758
2117
  throw new Error(
1759
- "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build",
2118
+ ZH
2119
+ ? "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build"
2120
+ : "Plugin has no build declaration: add build.command to manifest.json (or scripts.build in package.json), or remove --build",
1760
2121
  );
1761
2122
  const buildDir = join(tmpDir, "build");
1762
2123
  mkdirSync(buildDir, { recursive: true });
1763
2124
  cpSync(pluginRoot, buildDir, { recursive: true, filter: PLUGIN_COPY_FILTER });
1764
- runBuildCommand(plan.install, buildDir, "安装构建依赖");
1765
- runBuildCommand(plan.command, buildDir, "构建");
2125
+ runBuildCommand(plan.install, buildDir, ZH ? "安装构建依赖" : "installing build dependencies");
2126
+ runBuildCommand(plan.command, buildDir, ZH ? "构建" : "building");
1766
2127
  const missing = plan.outputs.filter((o) => !existsSync(join(buildDir, o)));
1767
- if (missing.length) throw new Error(`构建产物缺失:${missing.join(", ")}(manifest.build.outputs 声明)`);
1768
- console.log(`· 构建完成,产物齐全:${plan.outputs.join(", ")}`);
2128
+ if (missing.length)
2129
+ throw new Error(
2130
+ ZH
2131
+ ? `构建产物缺失:${missing.join(", ")}(manifest.build.outputs 声明)`
2132
+ : `Build artifacts missing: ${missing.join(", ")} (declared in manifest.build.outputs)`,
2133
+ );
2134
+ console.log(
2135
+ ZH
2136
+ ? `· 构建完成,产物齐全:${plan.outputs.join(", ")}`
2137
+ : `· Build complete, all artifacts present: ${plan.outputs.join(", ")}`,
2138
+ );
1769
2139
  return buildDir;
1770
2140
  }
1771
2141
 
@@ -1773,20 +2143,29 @@ function buildPluginSource(pluginRoot, tmpDir, manifest) {
1773
2143
  * 构建决策(issue #165:--build 自动推断)。
1774
2144
  *
1775
2145
  * 只有源码、没有产物(index.mjs 与 client/entry.mjs 都缺)且存在可解析的构建声明 =
1776
- * “不构建这次安装必死”,此时默认直接构建( previously 只打印一行提示,等用户滚回去重加
1777
- * --build)。产物已提交的仓库不受影响(mode=none,什么都不跑)。
2146
+ * “不构建这次安装必死”,此前会直接自动构建;但构建命令来自远端 manifest/package.json,
2147
+ * 经 shell:true 执行属于任意命令执行面,非交互环境下静默跑等于替用户放行。
2148
+ * 收紧后(审计修复):mode=auto 只表示“有构建声明且必要”,是否真正执行由
2149
+ * installOnePlugin 二次把关 —— 交互 TTY 且用户输入 y 才跑,否则跳过并警告。
1778
2150
  * --no-build 保留旧的“装个空目录”行为(脚本化镜像/检查用),并明确打印跳过原因。
1779
2151
  */
1780
- function decideBuildAction({ pluginRoot, manifest, build, noBuild }) {
1781
- if (build && noBuild) throw new Error("--build 与 --no-build 不能同时用(二选一)");
2152
+ function decideBuildAction({ pluginRoot, manifest, build, noBuild, isCatalog = false }) {
2153
+ if (build && noBuild)
2154
+ throw new Error(
2155
+ ZH ? "--build 与 --no-build 不能同时用(二选一)" : "--build and --no-build cannot be used together (pick one)",
2156
+ );
1782
2157
  const plan = resolveBuildPlan(pluginRoot, manifest);
1783
2158
  const artifactsMissing =
1784
2159
  !existsSync(join(pluginRoot, "index.mjs")) && !existsSync(join(pluginRoot, "client", "entry.mjs"));
1785
2160
  if (build) {
1786
- if (!plan)
2161
+ if (!plan) {
2162
+ if (isCatalog) return { mode: "none", plan };
1787
2163
  throw new Error(
1788
- "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build",
2164
+ ZH
2165
+ ? "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build"
2166
+ : "Plugin has no build declaration: add build.command to manifest.json (or scripts.build in package.json), or remove --build",
1789
2167
  );
2168
+ }
1790
2169
  return { mode: "explicit", plan };
1791
2170
  }
1792
2171
  if (plan && artifactsMissing) {
@@ -1796,12 +2175,31 @@ function decideBuildAction({ pluginRoot, manifest, build, noBuild }) {
1796
2175
  return { mode: "none", plan };
1797
2176
  }
1798
2177
 
2178
+ /**
2179
+ * 交互确认:插件声明的构建命令将经 shell 在本机执行(install + command 都来自
2180
+ * 远端 manifest/package.json),必须用户显式输入 y 才跑。仅 TTY 下会被调用。
2181
+ */
2182
+ async function confirmSourceBuild(plan) {
2183
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2184
+ try {
2185
+ const answer = await rl.question(
2186
+ ZH
2187
+ ? `⚠ 插件声明了以下构建命令,将在本机以 shell 执行:\n 1) ${plan.install}\n 2) ${plan.command}\n 确认执行?输入 y 继续,其他任意键跳过构建: `
2188
+ : `⚠ The plugin declares the following build commands, which will run through a shell on this machine:\n 1) ${plan.install}\n 2) ${plan.command}\n Proceed? Enter y to run, anything else to skip the build: `,
2189
+ );
2190
+ const normalized = answer.trim().toLowerCase();
2191
+ return normalized === "y" || normalized === "yes";
2192
+ } finally {
2193
+ rl.close();
2194
+ }
2195
+ }
2196
+
1799
2197
  /**
1800
2198
  * 装一个插件(单源模式与目录模式共用):拉取/定位 → 读 manifest → 构建决策 →
1801
2199
  * 覆盖(备份+保留 config.json)→ 落盘 → 记录来源/sha。
1802
2200
  * 失败抛 Error(目录模式逐条 try/catch 继续下一条,单源模式由调用方转 fail)。
1803
2201
  */
1804
- async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir }) {
2202
+ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir, isCatalog = false }) {
1805
2203
  const pluginsDir = join(dataDir, "plugins");
1806
2204
  const localCandidate = resolve(rawSpec.replace(/^file:\/\//, ""));
1807
2205
  const isLocal = existsSync(localCandidate);
@@ -1820,15 +2218,48 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1820
2218
  try {
1821
2219
  manifest = JSON.parse(readFileSync(join(pluginRoot, "manifest.json"), "utf8"));
1822
2220
  } catch (err) {
1823
- throw new Error(`manifest.json 不是合法 JSON:${err?.message ?? err}`);
2221
+ throw new Error(
2222
+ ZH
2223
+ ? `manifest.json 不是合法 JSON:${err?.message ?? err}`
2224
+ : `manifest.json is not valid JSON: ${err?.message ?? err}`,
2225
+ );
1824
2226
  }
1825
2227
  // 构建决策(issue #150 的 --build + issue #165 的自动推断):构建在临时目录里完成,
1826
2228
  // 成功后才进入覆盖流程——构建失败 = 目标目录完全没被动过(上一版插件照常可用)。
1827
- const decision = decideBuildAction({ pluginRoot, manifest, build: build === true, noBuild: noBuild === true });
2229
+ const decision = decideBuildAction({
2230
+ pluginRoot,
2231
+ manifest,
2232
+ build: build === true,
2233
+ noBuild: noBuild === true,
2234
+ isCatalog,
2235
+ });
2236
+ // 授权把关(审计修复):--build 显式传参行为不变;mode=auto 的构建命令来自
2237
+ // 远端 manifest/package.json 且经 shell:true 执行 —— 非交互环境绝不自动跑
2238
+ // (跳过并警告),交互 TTY 也要用户显式输入 y 才执行。
2239
+ let authorized = decision.mode === "explicit";
2240
+ if (decision.mode === "auto") {
2241
+ if (!process.stdin.isTTY) {
2242
+ console.warn(
2243
+ ZH
2244
+ ? "⚠ 插件声明了构建命令但未授权执行,已跳过构建 —— 传 --build 或手动构建后再安装"
2245
+ : "⚠ Plugin declares build commands but they were not authorized; skipping the build — pass --build or build manually before installing",
2246
+ );
2247
+ } else {
2248
+ authorized = await confirmSourceBuild(decision.plan);
2249
+ if (!authorized)
2250
+ console.warn(
2251
+ ZH
2252
+ ? "⚠ 未确认执行构建命令,已跳过构建 —— 需要时传 --build 或手动构建后再安装"
2253
+ : "⚠ Build commands not confirmed; skipping the build — pass --build or build manually if needed",
2254
+ );
2255
+ }
2256
+ }
1828
2257
  let installRoot = pluginRoot;
1829
- if (decision.mode === "explicit" || decision.mode === "auto") {
2258
+ if (authorized) {
1830
2259
  console.log(
1831
- `· 源码构建(${decision.mode === "auto" ? "自动推断:有构建声明但无产物" : "--build"}):先 ${decision.plan.install},再 ${decision.plan.command}`,
2260
+ ZH
2261
+ ? `· 源码构建(${decision.mode === "auto" ? "自动推断:有构建声明但无产物" : "--build"}):先 ${decision.plan.install},再 ${decision.plan.command}`
2262
+ : `· Source build (${decision.mode === "auto" ? "auto-detected: build declaration present but no artifacts" : "--build"}): first ${decision.plan.install}, then ${decision.plan.command}`,
1832
2263
  );
1833
2264
  try {
1834
2265
  installRoot = buildPluginSource(pluginRoot, tmp, manifest);
@@ -1836,7 +2267,17 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1836
2267
  throw new Error(`${err?.message ?? err}`);
1837
2268
  }
1838
2269
  } else if (decision.mode === "skipped") {
1839
- console.log("· 跳过构建(--no-build):目录里没有产物,装上后该插件不会被加载");
2270
+ console.log(
2271
+ ZH
2272
+ ? "· 跳过构建(--no-build):目录里没有产物,装上后该插件不会被加载"
2273
+ : "· Skipping build (--no-build): no artifacts in directory, plugin will not load after install",
2274
+ );
2275
+ } else if (decision.mode === "auto") {
2276
+ console.log(
2277
+ ZH
2278
+ ? "· 已跳过构建:目录里没有产物,装上后该插件不会被加载(授权后重装可补产物)"
2279
+ : "· Build skipped: no artifacts in directory, plugin will not load after install (re-install with authorization to build)",
2280
+ );
1840
2281
  }
1841
2282
  // 默认 id:子目录名 > 仓库名 > 本地目录名
1842
2283
  const sourceName = src?.subpath ? src.subpath.split("/").pop() : (src?.repo ?? localCandidate.split(/[\\/]/).pop());
@@ -1845,13 +2286,23 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1845
2286
  .replace(/[^A-Za-z0-9_-]/g, "-")
1846
2287
  .replace(/^-+|-+$/g, "") || "plugin";
1847
2288
  const id = name ?? fallbackId;
1848
- if (!PLUGIN_ID_RE.test(id)) throw new Error(`非法插件 id "${id}"(仅限字母数字-_,可用 --name <id> 自定义)`);
2289
+ if (!PLUGIN_ID_RE.test(id))
2290
+ throw new Error(
2291
+ ZH
2292
+ ? `非法插件 id "${id}"(仅限字母数字-_,可用 --name <id> 自定义)`
2293
+ : `Invalid plugin id "${id}" (alphanumeric and -_ only; use --name <id> to set a custom name)`,
2294
+ );
1849
2295
  const target = join(pluginsDir, id);
1850
2296
  let backupTs = null;
1851
2297
  let prevConfig = null;
1852
2298
  const CONFIG_NAME = "config.json";
1853
2299
  if (existsSync(target)) {
1854
- if (!force) throw new Error(`插件目录已存在:${target}\n 加 --force 覆盖,或用 --name <id> 换个名字。`);
2300
+ if (!force)
2301
+ throw new Error(
2302
+ ZH
2303
+ ? `插件目录已存在:${target}\n 加 --force 覆盖,或用 --name <id> 换个名字。`
2304
+ : `Plugin directory already exists: ${target}\n Add --force to overwrite, or use --name <id> to choose a different name.`,
2305
+ );
1855
2306
  // 更新前备份旧版本(<dataDir>/plugin-backups/<id>-<ts>/,保留最近 3 份),
1856
2307
  // 失败时自动回滚。备份与安装同 filter:不带 .git/node_modules。
1857
2308
  backupTs = ensurePluginBackup(dataDir, id, { source: rawSpec });
@@ -1869,9 +2320,17 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1869
2320
  } catch (err) {
1870
2321
  // 拷贝失败 → 有备份则自动回滚,保持旧版本可用
1871
2322
  if (backupTs && restorePluginBackup(dataDir, id)) {
1872
- throw new Error(`插件更新失败:${err?.message ?? err}\n 已自动回滚到更新前版本。`);
2323
+ throw new Error(
2324
+ ZH
2325
+ ? `插件更新失败:${err?.message ?? err}\n 已自动回滚到更新前版本。`
2326
+ : `Plugin update failed: ${err?.message ?? err}\n Automatically rolled back to the previous version.`,
2327
+ );
1873
2328
  }
1874
- throw new Error(`插件更新失败:${err?.message ?? err}\n (无可用备份,请重新 install --force)`);
2329
+ throw new Error(
2330
+ ZH
2331
+ ? `插件更新失败:${err?.message ?? err}\n (无可用备份,请重新 install --force)`
2332
+ : `Plugin update failed: ${err?.message ?? err}\n (no backup available, re-run install --force)`,
2333
+ );
1875
2334
  }
1876
2335
  if (prevConfig !== null && !existsSync(join(target, CONFIG_NAME))) {
1877
2336
  writeFileSync(join(target, CONFIG_NAME), prevConfig);
@@ -1969,7 +2428,12 @@ function normalizeCatalogEntries(raw) {
1969
2428
  : raw && typeof raw === "object" && Array.isArray(raw.entries)
1970
2429
  ? raw.entries
1971
2430
  : null;
1972
- if (!list) throw new Error('目录 JSON 需为数组,或 {"entries": [...]} 形状');
2431
+ if (!list)
2432
+ throw new Error(
2433
+ ZH
2434
+ ? '目录 JSON 需为数组,或 {"entries": [...]} 形状'
2435
+ : 'Catalog JSON must be an array or {"entries": [...]} shape',
2436
+ );
1973
2437
  const entries = [];
1974
2438
  let skipped = 0;
1975
2439
  for (const it of list) {
@@ -1983,24 +2447,38 @@ function normalizeCatalogEntries(raw) {
1983
2447
  /** 读目录文档:http(s) 拉取(30s 超时),其余当本地绝对路径(与服务端同口径)。 */
1984
2448
  async function readCatalogDocumentText(source) {
1985
2449
  const src = String(source ?? "").trim();
1986
- if (!src) throw new Error("缺少目录来源(--catalog <url 或本地绝对路径>)");
2450
+ if (!src)
2451
+ throw new Error(
2452
+ ZH
2453
+ ? "缺少目录来源(--catalog <url 或本地绝对路径>)"
2454
+ : "Missing catalog source (--catalog <url or local absolute path>)",
2455
+ );
1987
2456
  if (/^https?:\/\//i.test(src)) {
1988
2457
  let res;
1989
2458
  try {
1990
2459
  res = await fetch(src, { redirect: "follow", signal: AbortSignal.timeout(30_000) });
1991
2460
  } catch (err) {
1992
- throw new Error(`拉取目录失败:${err?.message ?? err}`);
2461
+ throw new Error(ZH ? `拉取目录失败:${err?.message ?? err}` : `Failed to fetch catalog: ${err?.message ?? err}`);
1993
2462
  }
1994
- if (!res.ok) throw new Error(`拉取目录失败:HTTP ${res.status}`);
2463
+ if (!res.ok)
2464
+ throw new Error(ZH ? `拉取目录失败:HTTP ${res.status}` : `Failed to fetch catalog: HTTP ${res.status}`);
1995
2465
  const text = await res.text();
1996
- if (text.length > 1024 * 1024) throw new Error("目录文档过大(> 1024 KB)");
2466
+ if (text.length > 1024 * 1024)
2467
+ throw new Error(ZH ? "目录文档过大(> 1024 KB)" : "Directory document too large (> 1024 KB)");
1997
2468
  return text;
1998
2469
  }
1999
- if (!isAbsolute(src)) throw new Error("本地目录文档需为绝对路径(远端用 http(s) URL)");
2470
+ if (!isAbsolute(src))
2471
+ throw new Error(
2472
+ ZH
2473
+ ? "本地目录文档需为绝对路径(远端用 http(s) URL)"
2474
+ : "Local directory document must be an absolute path (remote: use http(s) URL)",
2475
+ );
2000
2476
  try {
2001
2477
  return readFileSync(src, "utf8");
2002
2478
  } catch (err) {
2003
- throw new Error(`读取目录文件失败:${err?.message ?? err}`);
2479
+ throw new Error(
2480
+ ZH ? `读取目录文件失败:${err?.message ?? err}` : `Failed to read directory file: ${err?.message ?? err}`,
2481
+ );
2004
2482
  }
2005
2483
  }
2006
2484
 
@@ -2031,7 +2509,7 @@ async function installCatalogCmd(opts) {
2031
2509
  try {
2032
2510
  raw = JSON.parse(text);
2033
2511
  } catch (err) {
2034
- fail(`目录 JSON 解析失败:${err?.message ?? err}`);
2512
+ fail(ZH ? `目录 JSON 解析失败:${err?.message ?? err}` : `Directory JSON parse failed: ${err?.message ?? err}`);
2035
2513
  }
2036
2514
  let entries;
2037
2515
  let skipped = 0;
@@ -2054,14 +2532,20 @@ async function installCatalogCmd(opts) {
2054
2532
  writeCatalogFileEntries(customPath, next);
2055
2533
  }
2056
2534
  console.log(
2057
- `· 目录同步:${entries.length} 条合法${skipped ? `(丢弃 ${skipped} 条非法)` : ""}${opts.replace ? "(整体替换)" : "(按 id 合并)"} → ${customPath}`,
2535
+ ZH
2536
+ ? `· 目录同步:${entries.length} 条合法${skipped ? `(丢弃 ${skipped} 条非法)` : ""}${opts.replace ? "(整体替换)" : "(按 id 合并)"} → ${customPath}`
2537
+ : `· Directory sync: ${entries.length} valid${skipped ? `(discarded ${skipped} invalid)` : ""}${opts.replace ? "(full replace)" : "(merge by id)"} → ${customPath}`,
2058
2538
  );
2059
2539
  let okCount = 0;
2060
2540
  let failCount = 0;
2061
2541
  let skipCount = 0;
2062
2542
  for (const e of entries) {
2063
2543
  if (existsSync(join(pluginsDir, e.id)) && !opts.force) {
2064
- console.log(`· 跳过 ${e.id}(已安装,加 --force 更新)`);
2544
+ console.log(
2545
+ ZH
2546
+ ? `· 跳过 ${e.id}(已安装,加 --force 更新)`
2547
+ : `· Skipping ${e.id} (already installed, use --force to update)`,
2548
+ );
2065
2549
  skipCount++;
2066
2550
  continue;
2067
2551
  }
@@ -2073,15 +2557,22 @@ async function installCatalogCmd(opts) {
2073
2557
  build: opts.build === true,
2074
2558
  noBuild: opts.noBuild === true,
2075
2559
  dataDir,
2560
+ isCatalog: true,
2076
2561
  });
2077
- console.log(`✔ ${e.id} 安装成功`);
2562
+ console.log(ZH ? `✔ ${e.id} 安装成功` : `✔ ${e.id} installed`);
2078
2563
  okCount++;
2079
2564
  } catch (err) {
2080
- console.error(`✖ ${e.id} 安装失败:${err?.message ?? err}`);
2565
+ console.error(
2566
+ ZH ? `✖ ${e.id} 安装失败:${err?.message ?? err}` : `✖ ${e.id} failed to install: ${err?.message ?? err}`,
2567
+ );
2081
2568
  failCount++;
2082
2569
  }
2083
2570
  }
2084
- console.log(`✔ 目录安装完成:${okCount} 成功 / ${failCount} 失败 / ${skipCount} 跳过(已安装)`);
2571
+ console.log(
2572
+ ZH
2573
+ ? `✔ 目录安装完成:${okCount} 成功 / ${failCount} 失败 / ${skipCount} 跳过(已安装)`
2574
+ : `✔ Directory install complete: ${okCount} succeeded / ${failCount} failed / ${skipCount} skipped (already installed)`,
2575
+ );
2085
2576
  if (failCount) process.exitCode = 1;
2086
2577
  }
2087
2578
 
@@ -2363,23 +2854,45 @@ function buildPluginScaffold(id, template, useSdk, withTest = false) {
2363
2854
  function validateScaffoldManifest(manifest, dirName) {
2364
2855
  const warnings = [];
2365
2856
  for (const k of ["id", "name", "version", "description"]) {
2366
- if (typeof manifest[k] !== "string" || !manifest[k].trim()) warnings.push(`manifest 缺少必填字段 "${k}"`);
2857
+ if (typeof manifest[k] !== "string" || !manifest[k].trim())
2858
+ warnings.push(ZH ? `manifest 缺少必填字段 "${k}"` : `manifest missing required field "${k}"`);
2367
2859
  }
2368
2860
  if (manifest.id && manifest.id !== dirName)
2369
- warnings.push(`manifest.id "${manifest.id}" 与目录名 "${dirName}" 不一致(以目录名为准)`);
2861
+ warnings.push(
2862
+ ZH
2863
+ ? `manifest.id "${manifest.id}" 与目录名 "${dirName}" 不一致(以目录名为准)`
2864
+ : `manifest.id "${manifest.id}" and directory name "${dirName}" is inconsistent (directory name takes precedence)`,
2865
+ );
2370
2866
  if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
2371
- warnings.push(`manifest.version "${manifest.version}" 不是 x.y.z 格式`);
2867
+ warnings.push(
2868
+ ZH
2869
+ ? `manifest.version "${manifest.version}" 不是 x.y.z 格式`
2870
+ : `manifest.version "${manifest.version}" is not in x.y.z format`,
2871
+ );
2372
2872
  }
2373
2873
  const api = manifest.apiVersion ?? 1;
2374
2874
  if (typeof api !== "number" || api > 2) {
2375
- warnings.push(`manifest.apiVersion=${JSON.stringify(api)} 高于宿主 v2,插件会被拒绝激活`);
2875
+ warnings.push(
2876
+ ZH
2877
+ ? `manifest.apiVersion=${JSON.stringify(api)} 高于宿主 v2,插件会被拒绝激活`
2878
+ : `manifest.apiVersion=${JSON.stringify(api)} exceeds host v2, plugin will be rejected on activation`,
2879
+ );
2376
2880
  }
2377
2881
  if (manifest.permissions !== undefined) {
2378
2882
  const ok = Array.isArray(manifest.permissions) && manifest.permissions.every((p) => typeof p === "string" && p);
2379
- if (!ok) warnings.push(`manifest.permissions 须是字符串数组(如 ["ui"])`);
2883
+ if (!ok)
2884
+ warnings.push(
2885
+ ZH
2886
+ ? `manifest.permissions 须是字符串数组(如 ["ui"])`
2887
+ : `manifest.permissions must be a string array (e.g. ["ui"])`,
2888
+ );
2380
2889
  }
2381
2890
  if (manifest.view === false && !(Array.isArray(manifest.renderers) && manifest.renderers.length > 0)) {
2382
- warnings.push(`view:false 但未声明 renderers —— 插件将没有任何界面`);
2891
+ warnings.push(
2892
+ ZH
2893
+ ? `view:false 但未声明 renderers —— 插件将没有任何界面`
2894
+ : `view:false but no renderers declared — plugin will have no UI`,
2895
+ );
2383
2896
  }
2384
2897
  return warnings;
2385
2898
  }
@@ -2398,15 +2911,29 @@ function installedSdkVersion(sdkFile) {
2398
2911
  /** 包内 SDK 版本(单源:plugin-sdk/index.mjs 的 `export const SDK_VERSION`)。 */
2399
2912
  function packageSdkVersion() {
2400
2913
  const sdkSrc = pluginSdkSource();
2401
- if (!sdkSrc) fail(`包内无 plugin-sdk(找不到 plugin-sdk/index.mjs),无法刷新`);
2914
+ if (!sdkSrc)
2915
+ fail(
2916
+ ZH
2917
+ ? `包内无 plugin-sdk(找不到 plugin-sdk/index.mjs),无法刷新`
2918
+ : `No plugin-sdk in package (plugin-sdk/index.mjs not found), cannot refresh`,
2919
+ );
2402
2920
  let src;
2403
2921
  try {
2404
2922
  src = readFileSync(sdkSrc, "utf8");
2405
2923
  } catch {
2406
- fail(`包内 plugin-sdk/index.mjs 不可读,无法刷新`);
2924
+ fail(
2925
+ ZH
2926
+ ? `包内 plugin-sdk/index.mjs 不可读,无法刷新`
2927
+ : `plugin-sdk/index.mjs in package is unreadable, cannot refresh`,
2928
+ );
2407
2929
  }
2408
2930
  const m = src.match(/export const SDK_VERSION\s*=\s*["']([^"']+)["']/);
2409
- if (!m) fail(`包内 plugin-sdk/index.mjs 无 SDK_VERSION 导出,无法刷新(请升级 pi-web-ui)`);
2931
+ if (!m)
2932
+ fail(
2933
+ ZH
2934
+ ? `包内 plugin-sdk/index.mjs 无 SDK_VERSION 导出,无法刷新(请升级 pi-web-ui)`
2935
+ : `plugin-sdk/index.mjs in package has no SDK_VERSION export, cannot refresh (please upgrade pi-web-ui)`,
2936
+ );
2410
2937
  return { version: m[1], file: sdkSrc };
2411
2938
  }
2412
2939
 
@@ -2421,15 +2948,21 @@ function pluginUpgradeSdkCmd(argv) {
2421
2948
  const { version: latest, file: sdkSrc } = packageSdkVersion();
2422
2949
  const parentDir = opts.dir ? resolve(opts.dir) : join(pluginDataDir(opts), "plugins");
2423
2950
  const only = positionals.length === 1 ? positionals[0] : null;
2424
- if (only && !PLUGIN_ID_RE.test(only)) fail(`非法插件 id "${only}"(仅限字母数字-_)`);
2425
- if (only && !existsSync(join(parentDir, only))) fail(`未安装插件 "${only}"(pi-web-ui plugins 查看已装列表)`);
2951
+ if (only && !PLUGIN_ID_RE.test(only))
2952
+ fail(ZH ? `非法插件 id "${only}"(仅限字母数字-_)` : `Invalid plugin id "${only}" (alphanumeric and -_ only)`);
2953
+ if (only && !existsSync(join(parentDir, only)))
2954
+ fail(
2955
+ ZH
2956
+ ? `未安装插件 "${only}"(pi-web-ui plugins 查看已装列表)`
2957
+ : `Plugin not installed: "${only}" (pi-web-ui plugins to see installed list)`,
2958
+ );
2426
2959
  let entries;
2427
2960
  try {
2428
2961
  entries = readdirSync(parentDir, { withFileTypes: true })
2429
2962
  .filter((e) => e.isDirectory() && PLUGIN_ID_RE.test(e.name))
2430
2963
  .map((e) => e.name);
2431
2964
  } catch {
2432
- fail(`读插件目录失败: ${parentDir}`);
2965
+ fail(ZH ? `读插件目录失败: ${parentDir}` : `Failed to read plugin directory: ${parentDir}`);
2433
2966
  }
2434
2967
  const targets = only ? [only] : entries;
2435
2968
  let upgraded = 0;
@@ -2440,26 +2973,43 @@ function pluginUpgradeSdkCmd(argv) {
2440
2973
  const sdkFile = join(parentDir, id, "sdk", "index.mjs");
2441
2974
  if (!existsSync(sdkFile)) {
2442
2975
  skipped++;
2443
- console.log(`- ${id}:无 sdk/index.mjs 拷贝,跳过`);
2976
+ console.log(ZH ? `- ${id}:无 sdk/index.mjs 拷贝,跳过` : `- ${id}: no sdk/index.mjs copy, skipping`);
2444
2977
  continue;
2445
2978
  }
2446
2979
  const cur = installedSdkVersion(sdkFile);
2447
2980
  if (cur === latest) {
2448
2981
  fresh++;
2449
- console.log(`✔ ${id}:已是最新(SDK ${latest})`);
2982
+ console.log(ZH ? `✔ ${id}:已是最新(SDK ${latest})` : `✔ ${id}: already up to date (SDK ${latest})`);
2450
2983
  continue;
2451
2984
  }
2452
2985
  copyFileSync(sdkSrc, sdkFile);
2453
2986
  const r = spawnSync(NODE, ["--check", sdkFile], { stdio: "ignore" });
2454
- if (r.status !== 0) broken.push(`${sdkFile} 未通过 node --check(磁盘/权限异常?请手动检查)`);
2987
+ if (r.status !== 0)
2988
+ broken.push(
2989
+ ZH
2990
+ ? `${sdkFile} 未通过 node --check(磁盘/权限异常?请手动检查)`
2991
+ : `${sdkFile} failed node --check (disk/permission issue? please check manually)`,
2992
+ );
2455
2993
  else {
2456
2994
  upgraded++;
2457
- console.log(`✔ ${id}:SDK ${cur ?? "未知旧版"} → ${latest}`);
2995
+ console.log(
2996
+ ZH
2997
+ ? `✔ ${id}:SDK ${cur ?? "未知旧版"} → ${latest}`
2998
+ : `✔ ${id}: SDK ${cur ?? "unknown old version"} → ${latest}`,
2999
+ );
2458
3000
  }
2459
3001
  }
2460
3002
  for (const w of broken) console.log(`⚠ ${w}`);
2461
- console.log(`共 ${targets.length} 个插件:刷新 ${upgraded} 个,已最新 ${fresh} 个,跳过 ${skipped} 个。`);
2462
- console.log(` 生效: 服务运行中插件重载(或刷新浏览器)后新 SDK 生效;未运行则下次启动生效。`);
3003
+ console.log(
3004
+ ZH
3005
+ ? `共 ${targets.length} 个插件:刷新 ${upgraded} 个,已最新 ${fresh} 个,跳过 ${skipped} 个。`
3006
+ : `Total ${targets.length} plugin(s): refreshed ${upgraded}, up to date ${fresh}, skipped ${skipped}.`,
3007
+ );
3008
+ console.log(
3009
+ ZH
3010
+ ? ` 生效: 服务运行中插件重载(或刷新浏览器)后新 SDK 生效;未运行则下次启动生效。`
3011
+ : ` Effect: new SDK takes effect after plugin reload (or browser refresh) while the service is running; if not running, takes effect on next start.`,
3012
+ );
2463
3013
  }
2464
3014
 
2465
3015
  function pluginCreateCmd(argv) {
@@ -2470,12 +3020,21 @@ function pluginCreateCmd(argv) {
2470
3020
  return;
2471
3021
  }
2472
3022
  const id = positionals[0];
2473
- if (!PLUGIN_ID_RE.test(id)) fail(`非法插件 id "${id}"(仅限字母数字-_)`);
3023
+ if (!PLUGIN_ID_RE.test(id))
3024
+ fail(ZH ? `非法插件 id "${id}"(仅限字母数字-_)` : `Invalid plugin id "${id}" (alphanumeric and -_ only)`);
2474
3025
  const template = opts.template ?? "minimal";
2475
- if (!PLUGIN_TEMPLATES.includes(template)) fail(`未知模板 "${template}"(可选 ${PLUGIN_TEMPLATES.join("|")})`);
3026
+ if (!PLUGIN_TEMPLATES.includes(template))
3027
+ fail(
3028
+ ZH
3029
+ ? `未知模板 "${template}"(可选 ${PLUGIN_TEMPLATES.join("|")})`
3030
+ : `Unknown template "${template}" (options: ${PLUGIN_TEMPLATES.join("|")})`,
3031
+ );
2476
3032
  const parentDir = opts.dir ? resolve(opts.dir) : join(pluginDataDir(opts), "plugins");
2477
3033
  const target = join(parentDir, id);
2478
- if (existsSync(target) && opts.force !== true) fail(`目标已存在: ${target}(加 --force 覆盖)`);
3034
+ if (existsSync(target) && opts.force !== true)
3035
+ fail(
3036
+ ZH ? `目标已存在: ${target}(加 --force 覆盖)` : `Target already exists: ${target} (add --force to overwrite)`,
3037
+ );
2479
3038
  if (existsSync(target)) rmSync(target, { recursive: true, force: true });
2480
3039
  mkdirSync(join(target, "client"), { recursive: true });
2481
3040
  const sdkSrc = pluginSdkSource();
@@ -2492,25 +3051,57 @@ function pluginCreateCmd(argv) {
2492
3051
  try {
2493
3052
  manifest = JSON.parse(readFileSync(join(target, "manifest.json"), "utf8"));
2494
3053
  } catch {
2495
- fail(`生成的 manifest.json 解析失败(请检查磁盘/权限)`);
3054
+ fail(
3055
+ ZH
3056
+ ? `生成的 manifest.json 解析失败(请检查磁盘/权限)`
3057
+ : `Generated manifest.json failed to parse (please check disk/permissions)`,
3058
+ );
2496
3059
  }
2497
3060
  const warnings = validateScaffoldManifest(manifest, id);
2498
3061
  if (wantTest && !useSdk)
2499
- warnings.push(`已加 --with-test 但包内无 plugin-sdk,跳过 index.test.mjs(无 SDK 版没有 createMockHost 可用)`);
3062
+ warnings.push(
3063
+ ZH
3064
+ ? `已加 --with-test 但包内无 plugin-sdk,跳过 index.test.mjs(无 SDK 版没有 createMockHost 可用)`
3065
+ : `--with-test specified but no plugin-sdk in package, skipping index.test.mjs (no createMockHost available without SDK)`,
3066
+ );
2500
3067
  const checkFiles = ["index.mjs", "client/entry.mjs"];
2501
3068
  if (wantTest && useSdk) checkFiles.push("index.test.mjs");
2502
3069
  for (const rel of checkFiles) {
2503
3070
  const r = spawnSync(NODE, ["--check", join(target, rel)], { stdio: "ignore" });
2504
- if (r.status !== 0) warnings.push(`${rel} 未通过 node --check(请检查生成文件)`);
3071
+ if (r.status !== 0)
3072
+ warnings.push(
3073
+ ZH
3074
+ ? `${rel} 未通过 node --check(请检查生成文件)`
3075
+ : `${rel} failed node --check (please check generated files)`,
3076
+ );
2505
3077
  }
2506
- console.log(`✔ 已生成插件骨架 ${id}(模板 ${template})`);
2507
- console.log(` 位置: ${target}`);
3078
+ console.log(
3079
+ ZH ? `✔ 已生成插件骨架 ${id}(模板 ${template})` : `✔ Plugin scaffold generated: ${id} (template: ${template})`,
3080
+ );
3081
+ console.log(ZH ? ` 位置: ${target}` : ` Location: ${target}`);
2508
3082
  if (wantTest && useSdk)
2509
- console.log(` 单测: node --test ${join(target, "index.test.mjs")}(createMockHost harness)`);
2510
- if (!useSdk) console.log(`⚠ 未找到包内 plugin-sdk,已生成无 SDK 依赖版本(逻辑等价,详见 plugin-sdk/README.md)`);
3083
+ console.log(
3084
+ ZH
3085
+ ? ` 单测: node --test ${join(target, "index.test.mjs")}(createMockHost harness)`
3086
+ : ` Unit test: node --test ${join(target, "index.test.mjs")} (createMockHost harness)`,
3087
+ );
3088
+ if (!useSdk)
3089
+ console.log(
3090
+ ZH
3091
+ ? `⚠ 未找到包内 plugin-sdk,已生成无 SDK 依赖版本(逻辑等价,详见 plugin-sdk/README.md)`
3092
+ : `⚠ No plugin-sdk found in package, generated SDK-free version (logically equivalent, see plugin-sdk/README.md)`,
3093
+ );
2511
3094
  for (const w of warnings) console.log(`⚠ ${w}`);
2512
- console.log(` 生效: 服务运行中刷新浏览器即可加载(或发 plugins_reload);未运行则下次启动生效。`);
2513
- console.log(` 下一步: 打开 ${join(target, "README.md")}(改名 → 写逻辑 → 刷新验证)`);
3095
+ console.log(
3096
+ ZH
3097
+ ? ` 生效: 服务运行中刷新浏览器即可加载(或发 plugins_reload);未运行则下次启动生效。`
3098
+ : ` Effect: reload the browser to load while the service is running (or send plugins_reload); if not running, takes effect on next start.`,
3099
+ );
3100
+ console.log(
3101
+ ZH
3102
+ ? ` 下一步: 打开 ${join(target, "README.md")}(改名 → 写逻辑 → 刷新验证)`
3103
+ : ` Next: open ${join(target, "README.md")} (rename → write logic → refresh to verify)`,
3104
+ );
2514
3105
  }
2515
3106
 
2516
3107
  async function pluginInstallCmd(argv) {
@@ -2524,15 +3115,24 @@ async function pluginInstallCmd(argv) {
2524
3115
  if (opts.catalog !== undefined) {
2525
3116
  if (positionals.length !== 0)
2526
3117
  fail(
2527
- `用法: pi-web-ui install --catalog <目录> [--data-dir <dir>] [--force] [--build|--no-build] [--replace]\n${PLUGIN_HELP}`,
3118
+ ZH
3119
+ ? `用法: pi-web-ui install --catalog <目录> [--data-dir <dir>] [--force] [--build|--no-build] [--replace]\n${PLUGIN_HELP}`
3120
+ : `Usage: pi-web-ui install --catalog <dir> [--data-dir <dir>] [--force] [--build|--no-build] [--replace]\n${PLUGIN_HELP}`,
3121
+ );
3122
+ if (opts.name)
3123
+ fail(
3124
+ ZH
3125
+ ? "--catalog 模式下 --name 无意义(目录名/id 来自目录条目)"
3126
+ : "--name has no effect in --catalog mode (directory name/id comes from directory entries)",
2528
3127
  );
2529
- if (opts.name) fail("--catalog 模式下 --name 无意义(目录名/id 来自目录条目)");
2530
3128
  await installCatalogCmd(opts);
2531
3129
  return;
2532
3130
  }
2533
3131
  if (positionals.length !== 1)
2534
3132
  fail(
2535
- `用法: pi-web-ui install <源> [--name <id>] [--data-dir <dir>] [--force] [--build|--no-build]\n${PLUGIN_HELP}`,
3133
+ ZH
3134
+ ? `用法: pi-web-ui install <源> [--name <id>] [--data-dir <dir>] [--force] [--build|--no-build]\n${PLUGIN_HELP}`
3135
+ : `Usage: pi-web-ui install <source> [--name <id>] [--data-dir <dir>] [--force] [--build|--no-build]\n${PLUGIN_HELP}`,
2536
3136
  );
2537
3137
  try {
2538
3138
  const { id, target, manifest } = await installOnePlugin({
@@ -2544,11 +3144,17 @@ async function pluginInstallCmd(argv) {
2544
3144
  dataDir: pluginDataDir(opts),
2545
3145
  });
2546
3146
  console.log(
2547
- `✔ 已安装插件 ${id}${manifest.name && manifest.name !== id ? `(${manifest.name})` : ""}${manifest.version ? ` v${manifest.version}` : ""}`,
3147
+ ZH
3148
+ ? `✔ 已安装插件 ${id}${manifest.name && manifest.name !== id ? `(${manifest.name})` : ""}${manifest.version ? ` v${manifest.version}` : ""}`
3149
+ : `✔ Plugin installed: ${id}${manifest.name && manifest.name !== id ? `(${manifest.name})` : ""}${manifest.version ? ` v${manifest.version}` : ""}`,
2548
3150
  );
2549
3151
  if (manifest.description) console.log(` ${manifest.description}`);
2550
- console.log(` 位置: ${target}`);
2551
- console.log(` 生效: 服务运行中刷新浏览器即可加载;未运行则下次启动生效。卸载: pi-web-ui uninstall ${id}`);
3152
+ console.log(ZH ? ` 位置: ${target}` : ` Location: ${target}`);
3153
+ console.log(
3154
+ ZH
3155
+ ? ` 生效: 服务运行中刷新浏览器即可加载;未运行则下次启动生效。卸载: pi-web-ui uninstall ${id}`
3156
+ : ` Effect: reload the browser to load while the service is running; if not running, takes effect on next start. Uninstall: pi-web-ui uninstall ${id}`,
3157
+ );
2552
3158
  } catch (err) {
2553
3159
  fail(`${err?.message ?? err}`);
2554
3160
  }
@@ -2562,11 +3168,20 @@ function pluginUninstallCmd(argv) {
2562
3168
  return;
2563
3169
  }
2564
3170
  const id = positionals[0];
2565
- if (!PLUGIN_ID_RE.test(id)) fail(`非法插件 id: ${id}`);
3171
+ if (!PLUGIN_ID_RE.test(id)) fail(ZH ? `非法插件 id: ${id}` : `Invalid plugin id: ${id}`);
2566
3172
  const target = join(pluginDataDir(opts), "plugins", id);
2567
- if (!existsSync(target)) fail(`未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`);
3173
+ if (!existsSync(target))
3174
+ fail(
3175
+ ZH
3176
+ ? `未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`
3177
+ : `Plugin not installed: "${id}" (pi-web-ui plugins to see installed list)`,
3178
+ );
2568
3179
  rmSync(target, { recursive: true, force: true });
2569
- console.log(`✔ 已卸载插件 ${id} —— 运行中的服务刷新浏览器后消失。`);
3180
+ console.log(
3181
+ ZH
3182
+ ? `✔ 已卸载插件 ${id} —— 运行中的服务刷新浏览器后消失。`
3183
+ : `✔ Plugin uninstalled: ${id} — disappears after a browser refresh while the service is running.`,
3184
+ );
2570
3185
  }
2571
3186
 
2572
3187
  function pluginListCmd(argv) {
@@ -2579,12 +3194,26 @@ function pluginListCmd(argv) {
2579
3194
  // --rollback <id>:回滚到最近一份更新前备份
2580
3195
  if (opts.rollback) {
2581
3196
  const id = String(opts.rollback);
2582
- if (!PLUGIN_ID_RE.test(id)) fail(`非法插件 id: ${id}`);
3197
+ if (!PLUGIN_ID_RE.test(id)) fail(ZH ? `非法插件 id: ${id}` : `Invalid plugin id: ${id}`);
2583
3198
  const target = join(dataDir, "plugins", id);
2584
- if (!existsSync(target)) fail(`未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`);
3199
+ if (!existsSync(target))
3200
+ fail(
3201
+ ZH
3202
+ ? `未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`
3203
+ : `Plugin not installed: "${id}" (pi-web-ui plugins to see installed list)`,
3204
+ );
2585
3205
  const ts = restorePluginBackup(dataDir, id);
2586
- if (!ts) fail(`插件 "${id}" 没有更新备份(从未覆盖安装 / 备份已用完)`);
2587
- console.log(`✔ 已回滚插件 ${id} 到 ${ts} 的快照 —— 运行中的服务刷新浏览器后生效。`);
3206
+ if (!ts)
3207
+ fail(
3208
+ ZH
3209
+ ? `插件 "${id}" 没有更新备份(从未覆盖安装 / 备份已用完)`
3210
+ : `Plugin "${id}" has no update backup (never overwrite-installed / backup exhausted)`,
3211
+ );
3212
+ console.log(
3213
+ ZH
3214
+ ? `✔ 已回滚插件 ${id} 到 ${ts} 的快照 —— 运行中的服务刷新浏览器后生效。`
3215
+ : `✔ Plugin rolled back: ${id} to ${ts} snapshot — takes effect after a browser refresh while the service is running.`,
3216
+ );
2588
3217
  return;
2589
3218
  }
2590
3219
  // --check-updates:对比各插件记录的最后安装 sha 与远端 HEAD(git ls-remote)
@@ -2611,22 +3240,38 @@ function pluginListCmd(argv) {
2611
3240
  }
2612
3241
  }
2613
3242
  if (rows.length === 0) {
2614
- console.log(`尚未安装任何界面插件(目录: ${pluginsDir})\n安装示例: pi-web-ui install owner/repo`);
3243
+ console.log(
3244
+ ZH
3245
+ ? `尚未安装任何界面插件(目录: ${pluginsDir})\n安装示例: pi-web-ui install owner/repo`
3246
+ : `No UI plugins installed yet (directory: ${pluginsDir})\nInstall example: pi-web-ui install owner/repo`,
3247
+ );
2615
3248
  return;
2616
3249
  }
2617
- console.log(`已安装的界面插件(${pluginsDir}):\n${rows.join("\n")}`);
3250
+ console.log(
3251
+ ZH
3252
+ ? `已安装的界面插件(${pluginsDir}):\n${rows.join("\n")}`
3253
+ : `Installed UI plugins (${pluginsDir}):\n${rows.join("\n")}`,
3254
+ );
2618
3255
  }
2619
3256
 
2620
3257
  async function checkUpdatesCmd(dataDir) {
2621
- console.log("检查界面插件更新(git ls-remote 对比最近安装版本)…\n");
3258
+ console.log(
3259
+ ZH
3260
+ ? "检查界面插件更新(git ls-remote 对比最近安装版本)…\n"
3261
+ : "Checking UI plugin updates (git ls-remote vs. last installed version)…\n",
3262
+ );
2622
3263
  let rows;
2623
3264
  try {
2624
3265
  rows = await checkPluginUpdates(dataDir);
2625
3266
  } catch (err) {
2626
- fail(`更新检查失败:${err?.message ?? err}`);
3267
+ fail(ZH ? `更新检查失败:${err?.message ?? err}` : `Update check failed: ${err?.message ?? err}`);
2627
3268
  }
2628
3269
  if (rows.length === 0) {
2629
- console.log(`尚未安装任何带来源记录的界面插件(目录: ${join(dataDir, "plugins")})`);
3270
+ console.log(
3271
+ ZH
3272
+ ? `尚未安装任何带来源记录的界面插件(目录: ${join(dataDir, "plugins")})`
3273
+ : `No UI plugins with origin records installed yet (directory: ${join(dataDir, "plugins")})`,
3274
+ );
2630
3275
  return;
2631
3276
  }
2632
3277
  let any = false;
@@ -2634,17 +3279,31 @@ async function checkUpdatesCmd(dataDir) {
2634
3279
  const label = r.name && r.name !== r.id ? `${r.id}(${r.name})` : r.id;
2635
3280
  if (r.updatable) {
2636
3281
  console.log(
2637
- ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} 可更新(已装 ${r.localSha ?? "未知"} → 远端 ${r.remoteSha})`,
3282
+ ZH
3283
+ ? ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} 可更新(已装 ${r.localSha ?? "未知"} → 远端 ${r.remoteSha})`
3284
+ : ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} Updatable (installed ${r.localSha ?? "unknown"} → remote ${r.remoteSha})`,
3285
+ );
3286
+ console.log(
3287
+ ZH
3288
+ ? ` 更新: pi-web-ui install ${r.source} --name ${r.id} --force`
3289
+ : ` Update: pi-web-ui install ${r.source} --name ${r.id} --force`,
2638
3290
  );
2639
- console.log(` 更新: pi-web-ui install ${r.source} --name ${r.id} --force`);
2640
3291
  any = true;
2641
3292
  } else if (r.remoteSha) {
2642
- console.log(` ✓ ${label}${r.version ? ` v${r.version}` : ""} 已是最新(${r.remoteSha})`);
3293
+ console.log(
3294
+ ZH
3295
+ ? ` ✓ ${label}${r.version ? ` v${r.version}` : ""} 已是最新(${r.remoteSha})`
3296
+ : ` ✓ ${label}${r.version ? ` v${r.version}` : ""} Up to date (${r.remoteSha})`,
3297
+ );
2643
3298
  } else {
2644
- console.log(` ? ${label} ${r.error ?? "无法检查"}(来源: ${r.source})`);
3299
+ console.log(
3300
+ ZH
3301
+ ? ` ? ${label} ${r.error ?? "无法检查"}(来源: ${r.source})`
3302
+ : ` ? ${label} ${r.error ?? "cannot check"} (source: ${r.source})`,
3303
+ );
2645
3304
  }
2646
3305
  }
2647
- if (!any) console.log("\n全部插件均为最新版本。");
3306
+ if (!any) console.log(ZH ? "\n全部插件均为最新版本。" : "\nAll plugins are up to date.");
2648
3307
  }
2649
3308
 
2650
3309
  async function serverCmd(argv) {
@@ -2655,12 +3314,15 @@ async function serverCmd(argv) {
2655
3314
  }
2656
3315
  if (positionals.length === 0) {
2657
3316
  console.log(HELP);
2658
- console.log("--- 当前服务状态 ---");
3317
+ console.log(ZH ? "--- 当前服务状态 ---" : "--- Current service status ---");
2659
3318
  controlService("status", opts);
2660
3319
  return;
2661
3320
  }
2662
3321
  const action = positionals[0];
2663
- if (positionals.length > 1) fail(`多余的参数: ${positionals.slice(1).join(" ")}`);
3322
+ if (positionals.length > 1)
3323
+ fail(
3324
+ ZH ? `多余的参数: ${positionals.slice(1).join(" ")}` : `Unexpected argument: ${positionals.slice(1).join(" ")}`,
3325
+ );
2664
3326
  switch (action) {
2665
3327
  case "shortcut": {
2666
3328
  if (isWin) {
@@ -2670,7 +3332,9 @@ async function serverCmd(argv) {
2670
3332
  } else if (isLinux) {
2671
3333
  installLinuxShortcut(opts);
2672
3334
  } else {
2673
- fail(`不支持的系统服务平台: ${process.platform}`);
3335
+ fail(
3336
+ ZH ? `不支持的系统服务平台: ${process.platform}` : `Unsupported system service platform: ${process.platform}`,
3337
+ );
2674
3338
  }
2675
3339
  break;
2676
3340
  }
@@ -2682,7 +3346,9 @@ async function serverCmd(argv) {
2682
3346
  } else if (isWin) {
2683
3347
  installWindows(opts);
2684
3348
  } else {
2685
- fail(`不支持的系统服务平台: ${process.platform}`);
3349
+ fail(
3350
+ ZH ? `不支持的系统服务平台: ${process.platform}` : `Unsupported system service platform: ${process.platform}`,
3351
+ );
2686
3352
  }
2687
3353
  break;
2688
3354
  }
@@ -2694,7 +3360,9 @@ async function serverCmd(argv) {
2694
3360
  } else if (isWin) {
2695
3361
  uninstallWindows(opts);
2696
3362
  } else {
2697
- fail(`不支持的系统服务平台: ${process.platform}`);
3363
+ fail(
3364
+ ZH ? `不支持的系统服务平台: ${process.platform}` : `Unsupported system service platform: ${process.platform}`,
3365
+ );
2698
3366
  }
2699
3367
  break;
2700
3368
  }
@@ -2715,7 +3383,9 @@ async function serverCmd(argv) {
2715
3383
  break;
2716
3384
  default:
2717
3385
  fail(
2718
- `未知操作: ${action}(install / shortcut / uninstall / start / stop / restart / status / quiesce / unquiesce)`,
3386
+ ZH
3387
+ ? `未知操作: ${action}(install / shortcut / uninstall / start / stop / restart / status / quiesce / unquiesce)`
3388
+ : `Unknown operation: ${action} (install / shortcut / uninstall / start / stop / restart / status / quiesce / unquiesce)`,
2719
3389
  );
2720
3390
  }
2721
3391
  }
@@ -2766,7 +3436,10 @@ async function main() {
2766
3436
  console.log(HELP);
2767
3437
  return;
2768
3438
  }
2769
- if (positionals.length > 0) fail(`未知命令: ${positionals[0]}(--help 查看用法)`);
3439
+ if (positionals.length > 0)
3440
+ fail(
3441
+ ZH ? `未知命令: ${positionals[0]}(--help 查看用法)` : `Unknown command: ${positionals[0]} (--help for usage)`,
3442
+ );
2770
3443
  await startForeground(opts);
2771
3444
  }
2772
3445