pi-web-ui 0.94.0 → 0.95.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 (65) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +5 -6
  3. package/README.zh-CN.md +4 -5
  4. package/bin/pi-web-ui.mjs +751 -206
  5. package/dist/server/agent-service.js +1869 -85
  6. package/dist/server/approval-rules.js +550 -0
  7. package/dist/server/attachment-store.js +112 -0
  8. package/dist/server/attachments.js +62 -171
  9. package/dist/server/client-state.js +4 -0
  10. package/dist/server/compact-context-tool.js +125 -0
  11. package/dist/server/context-budget.js +317 -0
  12. package/dist/server/dangling-tools.js +162 -0
  13. package/dist/server/dsh/dsh-agent-service.js +50 -60
  14. package/dist/server/edit-soft-tool.js +70 -5
  15. package/dist/server/eval-tool.js +535 -0
  16. package/dist/server/files-service.js +35 -7
  17. package/dist/server/goal-service.js +240 -68
  18. package/dist/server/hashline-engine.js +665 -0
  19. package/dist/server/host-metrics.js +26 -2
  20. package/dist/server/http-proxy.js +11 -1
  21. package/dist/server/index.js +157 -9
  22. package/dist/server/lsp-tool.js +679 -0
  23. package/dist/server/model-admin.js +20 -0
  24. package/dist/server/office-parse.js +309 -0
  25. package/dist/server/patch-tool.js +91 -0
  26. package/dist/server/plan-manager.js +104 -0
  27. package/dist/server/plugin-api-catalog.js +296 -0
  28. package/dist/server/plugin-install-spec.js +196 -0
  29. package/dist/server/plugin-installer.js +73 -0
  30. package/dist/server/plugin-manifest-validate.js +305 -0
  31. package/dist/server/plugin-project.js +47 -11
  32. package/dist/server/plugin-tool-guard.js +120 -0
  33. package/dist/server/plugins.js +745 -161
  34. package/dist/server/process-utils.js +12 -6
  35. package/dist/server/scheduler-tasks.js +6 -0
  36. package/dist/server/serialize.js +13 -3
  37. package/dist/server/settings-service.js +108 -1
  38. package/dist/server/subagents.js +41 -0
  39. package/dist/server/terminals.js +69 -14
  40. package/dist/server/tool-approval.js +84 -0
  41. package/dist/server/tool-manager.js +214 -6
  42. package/dist/server/workspace-snapshot.js +113 -0
  43. package/package.json +3 -2
  44. package/plugin-sdk/README.md +25 -0
  45. package/plugin-sdk/index.d.ts +31 -38
  46. package/plugin-sdk/index.mjs +35 -19
  47. package/plugins/catalog.json +20 -0
  48. package/themes/ayu-light.css +6 -6
  49. package/themes/catppuccin-latte.css +6 -6
  50. package/themes/claude-code-dark.css +144 -0
  51. package/themes/codex.css +6 -6
  52. package/themes/dark-teal.css +20 -6
  53. package/themes/everforest-light.css +6 -6
  54. package/themes/geist.css +6 -6
  55. package/themes/gruvbox-light.css +6 -6
  56. package/themes/kanagawa-lotus.css +6 -6
  57. package/themes/rose-pine-dawn.css +6 -6
  58. package/themes/solarized-light.css +6 -6
  59. package/themes/vs-code-dark.css +146 -0
  60. package/web/dist/assets/{TerminalPanel-B-CMQuzm.js → TerminalPanel-DlGBF2MD.js} +1 -1
  61. package/web/dist/assets/index-BXnCQL9p.js +364 -0
  62. package/web/dist/assets/index-RpIGFyi_.css +1 -0
  63. package/web/dist/index.html +2 -2
  64. package/web/dist/assets/index-18jK6al2.js +0 -364
  65. package/web/dist/assets/index-B3S9MxnN.css +0 -1
package/bin/pi-web-ui.mjs CHANGED
@@ -84,6 +84,9 @@ function isZhLang() {
84
84
  return false;
85
85
  }
86
86
 
87
+ /** Resolved once: CLI messages below pick zh/en with `ZH ? zh : en` (only the chosen branch is evaluated). */
88
+ const ZH = isZhLang();
89
+
87
90
  const HELP_ZH = `pi-web-ui v${pkg.version} — web chat for the pi coding agent
88
91
 
89
92
  用法:
@@ -261,7 +264,7 @@ function parseFlags(argv) {
261
264
  i++;
262
265
  return argv[i];
263
266
  }
264
- fail(`缺少选项 ${flag} 的值`);
267
+ fail(ZH ? `缺少选项 ${flag} 的值` : `Option ${flag} is missing a value`);
265
268
  };
266
269
  switch (key) {
267
270
  case "--port":
@@ -326,7 +329,7 @@ function parseFlags(argv) {
326
329
  opts.help = true;
327
330
  break;
328
331
  default:
329
- if (key.startsWith("-")) fail(`未知选项: ${key}`);
332
+ if (key.startsWith("-")) fail(ZH ? `未知选项: ${key}` : `Unknown option: ${key}`);
330
333
  positionals.push(a);
331
334
  }
332
335
  }
@@ -351,11 +354,14 @@ function openBrowser(url) {
351
354
  if (res?.error) {
352
355
  if (res.error.code === "ENOENT") {
353
356
  console.warn(
354
- `[browser] 未找到打开器 (${res.error.path || "command not found"}),` +
355
- "headless 服务器可用 --no-browser 关闭自动打开",
357
+ ZH
358
+ ? `[browser] 未找到打开器 (${res.error.path || "command not found"}),` +
359
+ "headless 服务器可用 --no-browser 关闭自动打开"
360
+ : `[browser] no opener found (${res.error.path || "command not found"}), ` +
361
+ "headless servers can use --no-browser to disable auto-open",
356
362
  );
357
363
  } else {
358
- console.warn("[browser] 打开浏览器失败:", res.error.message);
364
+ console.warn(ZH ? "[browser] 打开浏览器失败:" : "[browser] failed to open browser:", res.error.message);
359
365
  }
360
366
  }
361
367
  }
@@ -372,7 +378,11 @@ function openBrowserWhenUp(url) {
372
378
  const attempt = () => {
373
379
  const req = httpGet(url, (res) => {
374
380
  res.resume();
375
- console.log(` 🌐 已自动打开浏览器:${url}(--no-browser 可关闭)`);
381
+ console.log(
382
+ ZH
383
+ ? ` 🌐 已自动打开浏览器:${url}(--no-browser 可关闭)`
384
+ : ` 🌐 Browser opened automatically: ${url} (--no-browser to disable)`,
385
+ );
376
386
  openBrowser(url);
377
387
  });
378
388
  req.setTimeout(1000, () => {
@@ -391,7 +401,8 @@ async function startForeground(opts) {
391
401
  if (opts.cwd) process.env.PI_WEB_CWD = resolve(opts.cwd);
392
402
  if (opts.dataDir) process.env.PI_WEB_DATA_DIR = resolve(opts.dataDir);
393
403
  if (opts.engine) {
394
- if (opts.engine !== "pi" && opts.engine !== "dsh") fail(`无效引擎: ${opts.engine}(仅支持 pi / dsh)`);
404
+ if (opts.engine !== "pi" && opts.engine !== "dsh")
405
+ fail(ZH ? `无效引擎: ${opts.engine}(仅支持 pi / dsh)` : `Invalid engine: ${opts.engine} (pi and dsh only)`);
395
406
  process.env.PI_WEB_ENGINE = opts.engine;
396
407
  }
397
408
  if (opts.host) process.env.PI_WEB_HOST = opts.host;
@@ -687,7 +698,11 @@ function installWinShortcut(opts) {
687
698
  if (existsSync(APP_ICO_SOURCE)) {
688
699
  copyFileSync(APP_ICO_SOURCE, winIcoPath());
689
700
  } else {
690
- console.log(`⚠ 未找到品牌图标 ${APP_ICO_SOURCE},快捷方式将使用默认图标`);
701
+ console.log(
702
+ ZH
703
+ ? `⚠ 未找到品牌图标 ${APP_ICO_SOURCE},快捷方式将使用默认图标`
704
+ : `⚠ Brand icon not found ${APP_ICO_SOURCE}, shortcut will use the default icon`,
705
+ );
691
706
  }
692
707
  const powershell = winPowershell();
693
708
  const ps = [
@@ -707,14 +722,26 @@ function installWinShortcut(opts) {
707
722
  encoding: "utf8",
708
723
  });
709
724
  if (res.status !== 0) {
710
- fail(`创建桌面快捷方式失败: ${(res.stderr || res.stdout || "").trim()}`);
725
+ fail(
726
+ ZH
727
+ ? `创建桌面快捷方式失败: ${(res.stderr || res.stdout || "").trim()}`
728
+ : `Failed to create desktop shortcut: ${(res.stderr || res.stdout || "").trim()}`,
729
+ );
711
730
  }
712
731
  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}`);
732
+ console.log(ZH ? `✅ 已创建桌面快捷方式: ${lnk}` : `✅ Desktop shortcut created: ${lnk}`);
733
+ console.log(
734
+ ZH
735
+ ? ` 双击 : 服务未运行则启动(隐藏窗口,无黑窗),就绪后自动打开浏览器`
736
+ : ` Launch : double-click starts the service if not running (hidden window, no console), auto-opens browser when ready`,
737
+ );
738
+ console.log(
739
+ ZH
740
+ ? ` 停止 : pi-web-ui server stop(快捷方式启动的实例也会一并停止)`
741
+ : ` Stop : pi-web-ui server stop (also stops instances launched by the shortcut)`,
742
+ );
743
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
744
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
718
745
  }
719
746
 
720
747
  /** macOS: double-clickable .command launcher (the .lnk equivalent). */
@@ -774,12 +801,24 @@ function installMacShortcut(opts) {
774
801
  }
775
802
  writeFileSync(path, script);
776
803
  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}`);
804
+ console.log(ZH ? `✅ 已创建桌面启动器: ${path}` : `✅ Desktop launcher created: ${path}`);
805
+ console.log(
806
+ ZH
807
+ ? ` 双击 : 确保服务运行并打开浏览器;未安装服务时在本终端前台运行`
808
+ : ` Launch : double-click ensures the service is running and opens browser; runs in foreground in this terminal if service not installed`,
809
+ );
810
+ console.log(
811
+ ZH
812
+ ? ` 说明 : macOS 没有 Windows 式快捷方式,这是等价的 .command 启动器;`
813
+ : ` Notes : macOS has no Windows-style shortcuts; this is the equivalent .command launcher;`,
814
+ );
815
+ console.log(
816
+ ZH
817
+ ? ` launchd 服务登录自启,图标主要用于快速「启动 + 打开浏览器」`
818
+ : ` launchd service auto-starts at login, icon mainly for quick “start + open browser”`,
819
+ );
820
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
821
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
783
822
  }
784
823
 
785
824
  /** Linux: launcher script run by the .desktop icon. */
@@ -855,10 +894,14 @@ Categories=Network;WebBrowser;
855
894
  ignoreError: true,
856
895
  silent: true,
857
896
  });
858
- console.log(`✅ 已创建桌面图标: ${desktopPath}`);
859
- console.log(` GNOME 若提示「不受信任的应用程序」,右键选择 Allow Launching`);
860
- console.log(` 端口 : ${port}`);
861
- console.log(` 目录 : ${cwd}`);
897
+ console.log(ZH ? `✅ 已创建桌面图标: ${desktopPath}` : `✅ Desktop icon created: ${desktopPath}`);
898
+ console.log(
899
+ ZH
900
+ ? ` GNOME 若提示「不受信任的应用程序」,右键选择 Allow Launching`
901
+ : ` If GNOME prompts “untrusted application”, right-click and choose Allow Launching`,
902
+ );
903
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
904
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
862
905
  }
863
906
 
864
907
  /** Remove desktop shortcut artifacts created by `server shortcut`. */
@@ -1072,12 +1115,21 @@ function serviceOptions(opts) {
1072
1115
  const name = opts.name ?? "pi-web-ui";
1073
1116
  const port = effectivePort(opts);
1074
1117
  if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
1075
- fail(`无效端口: ${port}`);
1118
+ fail(ZH ? `无效端口: ${port}` : `Invalid port: ${port}`);
1119
+ }
1120
+ // 服务默认工作区(issue #295):安装命令的当前目录不可靠(例如 Windows 提权提示符
1121
+ // 默认在 C:\WINDOWS\system32),且直接拿用户主目录当 agent cwd 会让 SDK 的
1122
+ // resourceLoader.reload 等同步目录扫描落在 $HOME 上 —— 家目录下的 iCloud 占位符、
1123
+ // 外部/网络卷坏挂载会让 scandir/open 在内核里挂起,整个事件循环假死(hello 后永远
1124
+ // 收不到 ready)。默认落到干净的 ~/pi-web-ui 子目录(不存在即建);显式 --cwd /
1125
+ // PI_WEB_CWD 保持原语义(不存在则报错)。前台启动仍默认当前目录。
1126
+ const explicitCwd = opts.cwd ?? process.env.PI_WEB_CWD;
1127
+ const cwd = resolve(explicitCwd ?? join(homedir(), "pi-web-ui"));
1128
+ if (explicitCwd) {
1129
+ if (!existsSync(cwd)) fail(ZH ? `工作目录不存在: ${cwd}` : `Working directory does not exist: ${cwd}`);
1130
+ } else if (!existsSync(cwd)) {
1131
+ mkdirSync(cwd, { recursive: true });
1076
1132
  }
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
1133
  let dataDir;
1082
1134
  if (opts.dataDir) {
1083
1135
  dataDir = resolve(opts.dataDir);
@@ -1086,7 +1138,8 @@ function serviceOptions(opts) {
1086
1138
  }
1087
1139
  // 引擎/监听地址/agent 配置目录:flag 优先,环境变量后备(token 不走命令行,仅环境变量)。
1088
1140
  const engine = opts.engine ?? process.env.PI_WEB_ENGINE ?? "pi";
1089
- if (engine !== "pi" && engine !== "dsh") fail(`无效引擎: ${engine}(仅支持 pi / dsh)`);
1141
+ if (engine !== "pi" && engine !== "dsh")
1142
+ fail(ZH ? `无效引擎: ${engine}(仅支持 pi / dsh)` : `Invalid engine: ${engine} (pi and dsh only)`);
1090
1143
  const host = opts.host ?? process.env.PI_WEB_HOST;
1091
1144
  const agentDir = opts.agentDir ? resolve(opts.agentDir) : process.env.PI_CODING_AGENT_DIR;
1092
1145
  return { name, port, cwd, dataDir, engine, host, agentDir };
@@ -1138,13 +1191,23 @@ function installLaunchd(opts) {
1138
1191
  mkdirSync(dirname(plist), { recursive: true });
1139
1192
  writeFileSync(plist, content);
1140
1193
  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 可在桌面创建「一键启动」图标`);
1194
+ console.log(ZH ? `✅ 已安装并启动 launchd 服务 ${label}` : `✅ launchd service installed and started ${label}`);
1195
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
1196
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
1197
+ console.log(ZH ? ` 访问 : http://localhost:${port}` : ` URL : http://localhost:${port}`);
1198
+ console.log(
1199
+ ZH ? ` 日志 : /tmp/pi-web-ui.log /tmp/pi-web-ui.err` : ` Log : /tmp/pi-web-ui.log /tmp/pi-web-ui.err`,
1200
+ );
1201
+ console.log(
1202
+ ZH
1203
+ ? ` 管理 : pi-web-ui server status|restart|stop|uninstall`
1204
+ : ` Manage : pi-web-ui server status|restart|stop|uninstall`,
1205
+ );
1206
+ console.log(
1207
+ ZH
1208
+ ? ` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`
1209
+ : ` Tip : pi-web-ui server shortcut creates a “one-click launch” icon on the desktop`,
1210
+ );
1148
1211
  }
1149
1212
 
1150
1213
  /** Elevate only the privileged operation, not the CLI that captures the user's environment.
@@ -1155,7 +1218,8 @@ function runSystemdRoot(command, args, input) {
1155
1218
  input,
1156
1219
  stdio: input === undefined ? "inherit" : ["pipe", "inherit", "inherit"],
1157
1220
  });
1158
- if (result.error) fail(`无法执行 ${command}: ${result.error.message}`);
1221
+ if (result.error)
1222
+ fail(ZH ? `无法执行 ${command}: ${result.error.message}` : `Cannot run ${command}: ${result.error.message}`);
1159
1223
  if (result.status !== 0) process.exit(result.status ?? 1);
1160
1224
  }
1161
1225
 
@@ -1183,13 +1247,23 @@ function installSystemd(opts) {
1183
1247
  runSystemdRoot("systemctl", ["enable", `${name}.service`]);
1184
1248
  // enable --now does not restart an already-active unit after configuration changes.
1185
1249
  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 可在桌面创建「一键启动」图标`);
1250
+ console.log(
1251
+ ZH ? `✅ 已安装并启动 systemd 服务 ${name}.service` : `✅ systemd service installed and started ${name}.service`,
1252
+ );
1253
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
1254
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
1255
+ console.log(ZH ? ` 访问 : http://localhost:${port}` : ` URL : http://localhost:${port}`);
1256
+ console.log(ZH ? ` 日志 : journalctl -u ${name}.service -f` : ` Log : journalctl -u ${name}.service -f`);
1257
+ console.log(
1258
+ ZH
1259
+ ? ` 管理 : pi-web-ui server status|restart|stop|uninstall`
1260
+ : ` Manage : pi-web-ui server status|restart|stop|uninstall`,
1261
+ );
1262
+ console.log(
1263
+ ZH
1264
+ ? ` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`
1265
+ : ` Tip : pi-web-ui server shortcut creates a “one-click launch” icon on the desktop`,
1266
+ );
1193
1267
  }
1194
1268
 
1195
1269
  function uninstallLaunchd(opts) {
@@ -1202,8 +1276,12 @@ function uninstallLaunchd(opts) {
1202
1276
  });
1203
1277
  if (existsSync(plist)) rmSync(plist);
1204
1278
  removeShortcut(name);
1205
- console.log(`🗑 已卸载 ${label}(plist 已删除,不再开机自启)`);
1206
- console.log(`🗑 已移除桌面快捷方式`);
1279
+ console.log(
1280
+ ZH
1281
+ ? `🗑 已卸载 ${label}(plist 已删除,不再开机自启)`
1282
+ : `🗑 Uninstalled ${label} (plist deleted, no longer auto-starts at login)`,
1283
+ );
1284
+ console.log(ZH ? `🗑 已移除桌面快捷方式` : `🗑 Desktop shortcut removed`);
1207
1285
  }
1208
1286
 
1209
1287
  function uninstallSystemd(opts) {
@@ -1216,8 +1294,12 @@ function uninstallSystemd(opts) {
1216
1294
  if (existsSync(unitPath)) rmSync(unitPath);
1217
1295
  run("systemctl", ["daemon-reload"]);
1218
1296
  removeShortcut(name);
1219
- console.log(`🗑 已卸载 ${name}.service(不再开机自启)`);
1220
- console.log(`🗑 已移除桌面快捷方式`);
1297
+ console.log(
1298
+ ZH
1299
+ ? `🗑 已卸载 ${name}.service(不再开机自启)`
1300
+ : `🗑 Uninstalled ${name}.service (no longer auto-starts at login)`,
1301
+ );
1302
+ console.log(ZH ? `🗑 已移除桌面快捷方式` : `🗑 Desktop shortcut removed`);
1221
1303
  }
1222
1304
 
1223
1305
  function installWindows(opts) {
@@ -1233,7 +1315,7 @@ function installWindows(opts) {
1233
1315
  if (opts.print) {
1234
1316
  console.log(`# ${ps1Path}\n${ps1}`);
1235
1317
  console.log(`# ${vbsPath}\n${vbs}`);
1236
- console.log(`# 登录自启(HKCU Run 键,无需管理员)`);
1318
+ console.log(ZH ? `# 登录自启(HKCU Run 键,无需管理员)` : `# Login auto-start (HKCU Run key, no admin required)`);
1237
1319
  console.log(` reg add "HKCU\\${winRunKeyName()}" /v ${name} /t REG_SZ /d "${runValue}" /f`);
1238
1320
  return;
1239
1321
  }
@@ -1258,15 +1340,31 @@ function installWindows(opts) {
1258
1340
  }
1259
1341
  winRunKeySet(name, runValue);
1260
1342
  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 可在桌面创建「一键启动」图标`);
1343
+ console.log(
1344
+ ZH
1345
+ ? `✅ 已安装并启动 ${name}(登录自启 · HKCU Run 键 · 无需管理员)`
1346
+ : `✅ Installed and started ${name} (login auto-start · HKCU Run key · no admin required)`,
1347
+ );
1348
+ console.log(ZH ? ` 窗口 : wscript 隐藏启动,无黑窗` : ` Window : wscript hidden launch, no console`);
1349
+ console.log(ZH ? ` 端口 : ${port}` : ` Port : ${port}`);
1350
+ console.log(ZH ? ` 目录 : ${cwd}` : ` Directory : ${cwd}`);
1351
+ console.log(ZH ? ` 访问 : http://localhost:${port}` : ` URL : http://localhost:${port}`);
1352
+ console.log(ZH ? ` 日志 : ${winLogPath(name)}` : ` Log : ${winLogPath(name)}`);
1353
+ console.log(
1354
+ ZH
1355
+ ? ` 说明 : 崩溃后 10 秒自动重启(看门狗);stop 停止,uninstall 移除`
1356
+ : ` Notes : auto-restarts 10 s after crash (watchdog); stop to stop, uninstall to remove`,
1357
+ );
1358
+ console.log(
1359
+ ZH
1360
+ ? ` 管理 : pi-web-ui server status|restart|stop|uninstall`
1361
+ : ` Manage : pi-web-ui server status|restart|stop|uninstall`,
1362
+ );
1363
+ console.log(
1364
+ ZH
1365
+ ? ` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`
1366
+ : ` Tip : pi-web-ui server shortcut creates a “one-click launch” icon on the desktop`,
1367
+ );
1270
1368
  }
1271
1369
 
1272
1370
  function uninstallWindows(opts) {
@@ -1282,8 +1380,12 @@ function uninstallWindows(opts) {
1282
1380
  if (existsSync(f)) rmSync(f);
1283
1381
  }
1284
1382
  removeShortcut(name);
1285
- console.log(`🗑 已卸载 ${name}(登录自启已移除,不再开机自启)`);
1286
- console.log(`🗑 已移除桌面快捷方式`);
1383
+ console.log(
1384
+ ZH
1385
+ ? `🗑 已卸载 ${name}(登录自启已移除,不再开机自启)`
1386
+ : `🗑 Uninstalled ${name} (login auto-start removed, no longer auto-starts at login)`,
1387
+ );
1388
+ console.log(ZH ? `🗑 已移除桌面快捷方式` : `🗑 Desktop shortcut removed`);
1287
1389
  }
1288
1390
 
1289
1391
  // ---------------------------------------------------------------------------
@@ -1340,24 +1442,40 @@ function controlCommand(opts, cmd) {
1340
1442
  async function printLiveStatus(opts) {
1341
1443
  const st = await controlCommand(opts, "status");
1342
1444
  if (!st || !st.ok) {
1343
- console.log(" (服务器未运行或控制通道不可达 — 启动后可查 server status 实时信息)");
1445
+ console.log(
1446
+ ZH
1447
+ ? " (服务器未运行或控制通道不可达 — 启动后可查 server status 实时信息)"
1448
+ : " (server not running or control channel unreachable — start it, then check server status for live info)",
1449
+ );
1344
1450
  return;
1345
1451
  }
1346
- console.log(" --- 实时状态 (control socket) ---");
1347
- console.log(` 版本 : ${st.version} · PID ${st.pid}`);
1348
- console.log(` 目录 : ${st.cwd}`);
1452
+ console.log(ZH ? " --- 实时状态 (control socket) ---" : " --- live status (control socket) ---");
1453
+ console.log(ZH ? ` 版本 : ${st.version} · PID ${st.pid}` : ` Version : ${st.version} · PID ${st.pid}`);
1454
+ console.log(ZH ? ` 目录 : ${st.cwd}` : ` Directory : ${st.cwd}`);
1349
1455
  // 启动来源(server/launch-origin.ts):有 supervisor = 这个进程退出后会被自动
1350
1456
  // 拉起(server restart / 更新面板的「重启服务」才有意义)。
1351
1457
  console.log(
1352
- ` 启动 : ${
1353
- st.service
1354
- ? `pi-web-ui 服务(${st.service.supervisor} · ${st.service.name})`
1355
- : "前台 / 开发模式(无 supervisor,退出不自动重启)"
1356
- }`,
1458
+ ZH
1459
+ ? ` 启动 : ${
1460
+ st.service
1461
+ ? `pi-web-ui 服务(${st.service.supervisor} · ${st.service.name})`
1462
+ : "前台 / 开发模式(无 supervisor,退出不自动重启)"
1463
+ }`
1464
+ : ` Started : ${
1465
+ st.service
1466
+ ? `pi-web-ui service (${st.service.supervisor} · ${st.service.name})`
1467
+ : "foreground / dev mode (no supervisor, exit does not auto-restart)"
1468
+ }`,
1357
1469
  );
1358
- console.log(` 排空 : ${st.quiesced ? `是(自 ${new Date(st.quiescedSince).toLocaleString()})` : "否"}`);
1359
1470
  console.log(
1360
- ` 连接 : ${st.connectedClients} 个浏览器 · ${st.activeConversations} 个运行中对话 · ${st.pendingMessages} 条排队消息`,
1471
+ ZH
1472
+ ? ` 排空 : ${st.quiesced ? `是(自 ${new Date(st.quiescedSince).toLocaleString()})` : "否"}`
1473
+ : ` Drain : ${st.quiesced ? `yes (since ${new Date(st.quiescedSince).toLocaleString()})` : "no"}`,
1474
+ );
1475
+ console.log(
1476
+ ZH
1477
+ ? ` 连接 : ${st.connectedClients} 个浏览器 · ${st.activeConversations} 个运行中对话 · ${st.pendingMessages} 条排队消息`
1478
+ : ` Conns : ${st.connectedClients} browser · ${st.activeConversations} active session · ${st.pendingMessages} queued messages`,
1361
1479
  );
1362
1480
  }
1363
1481
 
@@ -1365,13 +1483,22 @@ async function printLiveStatus(opts) {
1365
1483
  async function setQuiesce(opts, on) {
1366
1484
  const st = await controlCommand(opts, on ? "quiesce" : "unquiesce");
1367
1485
  if (!st || !st.ok) {
1368
- fail(`服务器未运行或控制通道不可达(${controlPath(opts)})`);
1486
+ fail(
1487
+ ZH
1488
+ ? `服务器未运行或控制通道不可达(${controlPath(opts)})`
1489
+ : `Server not running or control channel unreachable (${controlPath(opts)})`,
1490
+ );
1369
1491
  }
1370
1492
  console.log(
1371
1493
  on
1372
- ? "⏸ 已进入排空模式(quiesce):拒绝新的对话/消息/编辑,存量运行继续跑完。\n" +
1494
+ ? ZH
1495
+ ? "⏸ 已进入排空模式(quiesce):拒绝新的对话/消息/编辑,存量运行继续跑完。\n" +
1373
1496
  " 跑完后用 pi-web-ui server unquiesce 恢复。"
1374
- : "▶ 已解除排空模式(unquiesce):恢复接收新的对话/消息/编辑。",
1497
+ : "⏸ Quiesce mode active: new conversations/messages/edits rejected; existing runs continue to completion.\n" +
1498
+ " Use pi-web-ui server unquiesce to resume when done."
1499
+ : ZH
1500
+ ? "▶ 已解除排空模式(unquiesce):恢复接收新的对话/消息/编辑。"
1501
+ : "▶ Unquiesced: now accepting new conversations/messages/edits.",
1375
1502
  );
1376
1503
  }
1377
1504
 
@@ -1389,9 +1516,15 @@ function controlService(action, opts) {
1389
1516
  encoding: "utf8",
1390
1517
  });
1391
1518
  const state = (res.stdout.match(/state = (\w+)/) ?? [])[1] ?? "loaded";
1392
- console.log(`${label}: ${state}(已加载,开机自启中)`);
1519
+ console.log(
1520
+ ZH ? `${label}: ${state}(已加载,开机自启中)` : `${label}: ${state} (loaded, auto-start enabled)`,
1521
+ );
1393
1522
  } else {
1394
- console.log(`${label}: 未安装(运行 pi-web-ui server install 安装)`);
1523
+ console.log(
1524
+ ZH
1525
+ ? `${label}: 未安装(运行 pi-web-ui server install 安装)`
1526
+ : `${label}: not installed (run pi-web-ui server install)`,
1527
+ );
1395
1528
  }
1396
1529
  return;
1397
1530
  }
@@ -1402,18 +1535,25 @@ function controlService(action, opts) {
1402
1535
  } else {
1403
1536
  const plist = launchAgentPlist(name);
1404
1537
  if (!existsSync(plist)) {
1405
- fail(`找不到 ${plist},请先运行 pi-web-ui server install`);
1538
+ fail(
1539
+ ZH
1540
+ ? `找不到 ${plist},请先运行 pi-web-ui server install`
1541
+ : `Cannot find ${plist}, please run pi-web-ui server install first`,
1542
+ );
1406
1543
  }
1407
1544
  run("launchctl", ["bootstrap", `gui/${uid()}`, plist]);
1408
1545
  }
1409
- console.log(`✅ 已启动 ${label}`);
1546
+ console.log(ZH ? `✅ 已启动 ${label}` : `✅ Started ${label}`);
1410
1547
  return;
1411
1548
  }
1412
1549
 
1413
1550
  if (action === "restart") {
1414
- if (!loaded()) fail(`${label} 未加载,请先 pi-web-ui server start`);
1551
+ if (!loaded())
1552
+ fail(
1553
+ ZH ? `${label} 未加载,请先 pi-web-ui server start` : `${label} not loaded, run pi-web-ui server start first`,
1554
+ );
1415
1555
  run("launchctl", ["kickstart", "-k", target]);
1416
- console.log(`✅ 已重启 ${label}`);
1556
+ console.log(ZH ? `✅ 已重启 ${label}` : `✅ Restarted ${label}`);
1417
1557
  return;
1418
1558
  }
1419
1559
 
@@ -1422,11 +1562,15 @@ function controlService(action, opts) {
1422
1562
  ignoreError: true,
1423
1563
  silent: true,
1424
1564
  });
1425
- console.log(`⏹ 已停止 ${label}(已卸载,不再开机自启;start 恢复)`);
1565
+ console.log(
1566
+ ZH
1567
+ ? `⏹ 已停止 ${label}(已卸载,不再开机自启;start 恢复)`
1568
+ : `⏹ Stopped ${label} (uninstalled, auto-start removed; start to resume)`,
1569
+ );
1426
1570
  return;
1427
1571
  }
1428
1572
 
1429
- fail(`未知操作: ${action}`);
1573
+ fail(ZH ? `未知操作: ${action}` : `Unknown operation: ${action}`);
1430
1574
  }
1431
1575
 
1432
1576
  if (isLinux) {
@@ -1448,22 +1592,41 @@ function controlService(action, opts) {
1448
1592
  const pid = winReadPid(name);
1449
1593
  const instAlive = pid && pidAlive(pid);
1450
1594
  if (legacy) {
1451
- console.log(`${name}: 旧版计划任务安装(未迁移)`);
1452
- console.log(` 提示 : 重新执行 server install 可迁移到登录自启模式(删除任务,无需管理员)`);
1453
- if (instAlive) console.log(` 实例 : 运行中 (PID ${pid})`);
1595
+ console.log(
1596
+ ZH ? `${name}: 旧版计划任务安装(未迁移)` : `${name}: legacy scheduled task install (not migrated)`,
1597
+ );
1598
+ console.log(
1599
+ ZH
1600
+ ? ` 提示 : 重新执行 server install 可迁移到登录自启模式(删除任务,无需管理员)`
1601
+ : ` Hint : re-run server install to migrate to login auto-start mode (removes task, no admin required)`,
1602
+ );
1603
+ if (instAlive) console.log(ZH ? ` 实例 : 运行中 (PID ${pid})` : ` Instance : running (PID ${pid})`);
1454
1604
  return;
1455
1605
  }
1456
1606
  if (!installed) {
1457
- console.log(`${name}: 未安装(运行 pi-web-ui server install 安装)`);
1458
- if (instAlive) console.log(` 快捷方式实例 : 运行中 (PID ${pid})`);
1607
+ console.log(
1608
+ ZH
1609
+ ? `${name}: 未安装(运行 pi-web-ui server install 安装)`
1610
+ : `${name}: not installed (run pi-web-ui server install)`,
1611
+ );
1612
+ if (instAlive)
1613
+ console.log(ZH ? ` 快捷方式实例 : 运行中 (PID ${pid})` : ` Shortcut : running (PID ${pid})`);
1459
1614
  return;
1460
1615
  }
1461
- console.log(`${name}: 已安装(登录自启 · HKCU Run 键 · wscript 隐藏启动,无黑窗)`);
1616
+ console.log(
1617
+ ZH
1618
+ ? `${name}: 已安装(登录自启 · HKCU Run 键 · wscript 隐藏启动,无黑窗)`
1619
+ : `${name}: installed (login auto-start · HKCU Run key · wscript silent launch, no console window)`,
1620
+ );
1462
1621
  if (instAlive) {
1463
- console.log(` 运行状态 : 运行中 (PID ${pid})`);
1464
- console.log(` 日志 : ${winLogPath(name)}`);
1622
+ console.log(ZH ? ` 运行状态 : 运行中 (PID ${pid})` : ` Status : running (PID ${pid})`);
1623
+ console.log(ZH ? ` 日志 : ${winLogPath(name)}` : ` Log : ${winLogPath(name)}`);
1465
1624
  } else {
1466
- console.log(` 运行状态 : 未运行(pi-web-ui server start 启动)`);
1625
+ console.log(
1626
+ ZH
1627
+ ? ` 运行状态 : 未运行(pi-web-ui server start 启动)`
1628
+ : ` Status : not running (pi-web-ui server start to launch)`,
1629
+ );
1467
1630
  }
1468
1631
  return;
1469
1632
  }
@@ -1471,17 +1634,26 @@ function controlService(action, opts) {
1471
1634
  if (action === "start") {
1472
1635
  if (legacy) {
1473
1636
  run("schtasks", ["/Run", "/TN", name]);
1474
- console.log(`✅ 已启动 ${name}(旧版计划任务,建议重新 server install 迁移)`);
1637
+ console.log(
1638
+ ZH
1639
+ ? `✅ 已启动 ${name}(旧版计划任务,建议重新 server install 迁移)`
1640
+ : `✅ Started ${name} (legacy scheduled task, recommend re-running server install to migrate)`,
1641
+ );
1475
1642
  return;
1476
1643
  }
1477
- if (!installed) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
1644
+ if (!installed)
1645
+ fail(
1646
+ ZH
1647
+ ? `${name} 不存在,请先运行 pi-web-ui server install`
1648
+ : `${name} does not exist, run pi-web-ui server install first`,
1649
+ );
1478
1650
  const pid = winReadPid(name);
1479
1651
  if (pid && pidAlive(pid)) {
1480
- console.log(`✅ ${name} 已在运行 (PID ${pid})`);
1652
+ console.log(ZH ? `✅ ${name} 已在运行 (PID ${pid})` : `✅ ${name} already running (PID ${pid})`);
1481
1653
  return;
1482
1654
  }
1483
1655
  launchWinService(winVbsPath(name));
1484
- console.log(`✅ 已启动 ${name}`);
1656
+ console.log(ZH ? `✅ 已启动 ${name}` : `✅ Started ${name}`);
1485
1657
  return;
1486
1658
  }
1487
1659
 
@@ -1492,13 +1664,22 @@ function controlService(action, opts) {
1492
1664
  silent: true,
1493
1665
  });
1494
1666
  run("schtasks", ["/Run", "/TN", name]);
1495
- console.log(`✅ 已重启 ${name}(旧版计划任务,建议重新 server install 迁移)`);
1667
+ console.log(
1668
+ ZH
1669
+ ? `✅ 已重启 ${name}(旧版计划任务,建议重新 server install 迁移)`
1670
+ : `✅ Restarted ${name} (legacy scheduled task, recommend re-running server install to migrate)`,
1671
+ );
1496
1672
  return;
1497
1673
  }
1498
- if (!installed) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
1674
+ if (!installed)
1675
+ fail(
1676
+ ZH
1677
+ ? `${name} 不存在,请先运行 pi-web-ui server install`
1678
+ : `${name} does not exist, run pi-web-ui server install first`,
1679
+ );
1499
1680
  stopWinInstance(name);
1500
1681
  launchWinService(winVbsPath(name));
1501
- console.log(`✅ 已重启 ${name}`);
1682
+ console.log(ZH ? `✅ 已重启 ${name}` : `✅ Restarted ${name}`);
1502
1683
  return;
1503
1684
  }
1504
1685
 
@@ -1509,18 +1690,30 @@ function controlService(action, opts) {
1509
1690
  silent: true,
1510
1691
  });
1511
1692
  stopWinInstance(name);
1512
- console.log(`⏹ 已停止 ${name}(旧版计划任务;重新 server install 可迁移到登录自启)`);
1693
+ console.log(
1694
+ ZH
1695
+ ? `⏹ 已停止 ${name}(旧版计划任务;重新 server install 可迁移到登录自启)`
1696
+ : `⏹ Stopped ${name} (legacy scheduled task; re-run server install to migrate to login auto-start)`,
1697
+ );
1513
1698
  return;
1514
1699
  }
1515
1700
  stopWinInstance(name);
1516
- console.log(`⏹ 已停止 ${name}(自启保留;uninstall 移除)`);
1701
+ console.log(
1702
+ ZH
1703
+ ? `⏹ 已停止 ${name}(自启保留;uninstall 移除)`
1704
+ : `⏹ Stopped ${name} (auto-start preserved; uninstall to remove)`,
1705
+ );
1517
1706
  return;
1518
1707
  }
1519
1708
 
1520
- fail(`未知操作: ${action}`);
1709
+ fail(ZH ? `未知操作: ${action}` : `Unknown operation: ${action}`);
1521
1710
  }
1522
1711
 
1523
- fail(`不支持的系统服务平台: ${process.platform}(仅 macOS / Linux / Windows)`);
1712
+ fail(
1713
+ ZH
1714
+ ? `不支持的系统服务平台: ${process.platform}(仅 macOS / Linux / Windows)`
1715
+ : `Unsupported system service platform: ${process.platform} (macOS / Linux / Windows only)`,
1716
+ );
1524
1717
  }
1525
1718
 
1526
1719
  // ---------------------------------------------------------------------------
@@ -1530,7 +1723,8 @@ function controlService(action, opts) {
1530
1723
  /** 合法插件 id(同 server/plugins.ts 的 ID_RE)。 */
1531
1724
  const PLUGIN_ID_RE = /^[A-Za-z0-9_-]+$/;
1532
1725
 
1533
- const PLUGIN_HELP = `用法:
1726
+ const PLUGIN_HELP = ZH
1727
+ ? `用法:
1534
1728
  pi-web-ui plugin create <id> [选项] 生成最小可跑的插件骨架
1535
1729
  pi-web-ui install <源> [选项] 安装 GitHub 上的界面插件
1536
1730
  pi-web-ui install --catalog <目录> [选项] 同步插件市场目录并逐条安装
@@ -1573,6 +1767,50 @@ upgrade-sdk 选项:
1573
1767
  plugins 选项:
1574
1768
  --check-updates 逐个对比最近安装版本与远端 HEAD,列出可更新插件
1575
1769
  --rollback <id> 回滚到最近一份更新前备份(<dataDir>/plugin-backups/)
1770
+ `
1771
+ : `Usage:
1772
+ pi-web-ui plugin create <id> [options] generate a minimal runnable plugin skeleton
1773
+ pi-web-ui install <source> [options] install a UI plugin from GitHub
1774
+ pi-web-ui install --catalog <catalog> [options] sync plugin catalog and install each entry
1775
+ pi-web-ui plugin create <id> [options] generate plugin skeleton (minimal|ui-slot|agent-tool|renderer)
1776
+ pi-web-ui plugin upgrade-sdk [id] [options] refresh SDK copy in installed plugins (only when SDK_VERSION mismatches)
1777
+ pi-web-ui uninstall <id> [options] uninstall an installed UI plugin
1778
+ pi-web-ui plugins [options] list installed UI plugins
1779
+
1780
+ Source formats (choose one):
1781
+ owner/repo shorthand
1782
+ https://github.com/owner/repo full URL (.git optional)
1783
+ https://github.com/o/r/tree/dev/sub/dir specify branch + subdirectory in repo
1784
+ append #branch-or-tag to any of the above specify branch/tag (e.g. owner/repo#v1.2)
1785
+ /path/to/plugin-dir local directory (offline dev/debug)
1786
+ Catalog format (for --catalog):
1787
+ https://example.com/catalog.json remote catalog document (array or {entries:[...]})
1788
+ /path/to/catalog.json local catalog document (absolute path)
1789
+ install options:
1790
+ --name <id> plugin directory name/id (default: repo name or manifest.id; alphanumeric -_ only)
1791
+ --data-dir <dir> data directory (default ~/.pi-web or $PI_WEB_DATA_DIR)
1792
+ --force overwrite if target directory exists (auto-backup before overwrite)
1793
+ --build force source build: install build deps and compile in an isolated temp directory
1794
+ (manifest.build or package.json scripts.build),
1795
+ replaces target only on success with complete artifacts; no partial state on failure
1796
+ --no-build skip build even if plugin is source-only (mutually exclusive with --build; plugin will not load without artifacts)
1797
+ --catalog <catalog> catalog sync mode: read catalog → atomically write installable list → install/update each entry
1798
+ (installed entries skipped by default; add --force to update; single failure does not abort batch)
1799
+ --replace used with --catalog: replace installable list entirely (default merges by id, preserving old entries)
1800
+
1801
+ create options:
1802
+ --template <t> minimal (default, no permissions) | ui-slot | agent-tool | renderer
1803
+ --dir <dir> plugin parent directory (default <data-dir>/plugins; or use --data-dir to set data directory)
1804
+ --force overwrite if target directory exists
1805
+ --with-test also generate index.test.mjs (node --test + createMockHost minimal unit test; requires in-package SDK)
1806
+
1807
+ upgrade-sdk options:
1808
+ [id] refresh only this plugin (default: refresh all plugins with an sdk copy)
1809
+ --dir <dir> plugin parent directory (default <data-dir>/plugins; or use --data-dir to set data directory)
1810
+
1811
+ plugins options:
1812
+ --check-updates compare each installed version against remote HEAD, list updatable plugins
1813
+ --rollback <id> roll back to the most recent pre-update backup (<dataDir>/plugin-backups/)
1576
1814
  `;
1577
1815
 
1578
1816
  function pluginDataDir(opts) {
@@ -1586,7 +1824,12 @@ function parsePluginSource(rawSpec) {
1586
1824
  const hash = spec.indexOf("#");
1587
1825
  if (hash >= 0) {
1588
1826
  ref = spec.slice(hash + 1).trim();
1589
- if (!ref) fail(`无效的源 "${rawSpec}":# 后缺少分支/tag 名`);
1827
+ if (!ref)
1828
+ fail(
1829
+ ZH
1830
+ ? `无效的源 "${rawSpec}":# 后缺少分支/tag 名`
1831
+ : `Invalid source "${rawSpec}": branch/tag name missing after #`,
1832
+ );
1590
1833
  spec = spec.slice(0, hash).replace(/\/+$/, "");
1591
1834
  }
1592
1835
  // ssh 形式转 https 拉取(不要求本机配 ssh key);URL 去掉协议前缀统一按路径段解析
@@ -1597,9 +1840,17 @@ function parsePluginSource(rawSpec) {
1597
1840
  if (url) [, spec] = url;
1598
1841
  }
1599
1842
  const segs = spec.split("/").filter(Boolean);
1600
- if (segs.length < 2) fail(`无法识别的插件源 "${rawSpec}"\n${PLUGIN_HELP}`);
1843
+ if (segs.length < 2)
1844
+ fail(
1845
+ ZH ? `无法识别的插件源 "${rawSpec}"\n${PLUGIN_HELP}` : `Unrecognized plugin source "${rawSpec}"\n${PLUGIN_HELP}`,
1846
+ );
1601
1847
  for (const s of segs) {
1602
- if (s === "." || s === "..") fail(`无效的源 "${rawSpec}":路径段不能是 . 或 ..`);
1848
+ if (s === "." || s === "..")
1849
+ fail(
1850
+ ZH
1851
+ ? `无效的源 "${rawSpec}":路径段不能是 . 或 ..`
1852
+ : `Invalid source "${rawSpec}": path segments cannot be . or ..`,
1853
+ );
1603
1854
  }
1604
1855
  const [owner, repo] = segs;
1605
1856
  let subpath;
@@ -1627,10 +1878,14 @@ async function acquireRepo(src, tmpDir) {
1627
1878
  timeout: 300_000,
1628
1879
  });
1629
1880
  if (res.status === 0 && existsSync(dst)) return dst;
1630
- console.log("· git clone 失败,回退到 tarball 直连下载…");
1881
+ console.log(
1882
+ ZH
1883
+ ? "· git clone 失败,回退到 tarball 直连下载…"
1884
+ : "· git clone failed, falling back to direct tarball download…",
1885
+ );
1631
1886
  }
1632
1887
  const url = `https://codeload.github.com/${src.owner}/${src.repo}/tar.gz/${src.ref || "HEAD"}`;
1633
- console.log(`· 下载 ${url}`);
1888
+ console.log(ZH ? `· 下载 ${url}` : `· Downloading ${url}`);
1634
1889
  // 注意:这里不用 fail()/process.exit —— async 上下文里还有未关闭的 socket 时
1635
1890
  // 直接退出会触发 Windows libuv "UV_HANDLE_CLOSING" 断言崩溃;改为 throw,
1636
1891
  // 由 pluginInstallCmd 捕获后设 exitCode 让事件循环自然排空。
@@ -1638,13 +1893,19 @@ async function acquireRepo(src, tmpDir) {
1638
1893
  try {
1639
1894
  res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
1640
1895
  } catch (err) {
1641
- throw new Error(`下载失败:${err?.message ?? err}\n 请检查网络/代理后重试。`);
1896
+ throw new Error(
1897
+ ZH
1898
+ ? `下载失败:${err?.message ?? err}\n 请检查网络/代理后重试。`
1899
+ : `Download failed: ${err?.message ?? err}\n Check your network/proxy and retry.`,
1900
+ );
1642
1901
  }
1643
1902
  if (!res.ok)
1644
1903
  throw new Error(
1645
- `下载失败 HTTP ${res.status}:${url}` +
1904
+ (ZH ? `下载失败 HTTP ${res.status}:${url}` : `Download failed HTTP ${res.status}: ${url}`) +
1646
1905
  (res.status === 404
1647
- ? "\n 仓库/分支不存在,或为私有仓库(私有仓库请先在本机配置好 git 凭据再重试,会优先走 git clone)。"
1906
+ ? ZH
1907
+ ? "\n 仓库/分支不存在,或为私有仓库(私有仓库请先在本机配置好 git 凭据再重试,会优先走 git clone)。"
1908
+ : "\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
1909
  : ""),
1649
1910
  );
1650
1911
  writeFileSync(join(tmpDir, "src.tar.gz"), Buffer.from(await res.arrayBuffer()));
@@ -1655,9 +1916,19 @@ async function acquireRepo(src, tmpDir) {
1655
1916
  cwd: tmpDir,
1656
1917
  stdio: "inherit",
1657
1918
  });
1658
- if (tarRes.status !== 0) fail("tar 解压失败(可重试,或手动下载 release 包解压)");
1919
+ if (tarRes.status !== 0)
1920
+ fail(
1921
+ ZH
1922
+ ? "tar 解压失败(可重试,或手动下载 release 包解压)"
1923
+ : "tar extraction failed (retry, or manually download and extract the release archive)",
1924
+ );
1659
1925
  const entries = readdirSync(extractTo);
1660
- if (entries.length !== 1) fail("tarball 解压结果异常(顶层应只有一个目录)");
1926
+ if (entries.length !== 1)
1927
+ fail(
1928
+ ZH
1929
+ ? "tarball 解压结果异常(顶层应只有一个目录)"
1930
+ : "tarball extraction result unexpected (top level should contain exactly one directory)",
1931
+ );
1661
1932
  return join(extractTo, entries[0]);
1662
1933
  }
1663
1934
 
@@ -1683,18 +1954,30 @@ function findManifestDirs(root) {
1683
1954
  function locatePluginRoot(checkout, subpath, repoLabel) {
1684
1955
  if (subpath) {
1685
1956
  const dir = join(checkout, ...subpath.split("/"));
1686
- if (!existsSync(join(dir, "manifest.json"))) fail(`子目录 "${subpath}" 里没有 manifest.json`);
1957
+ if (!existsSync(join(dir, "manifest.json")))
1958
+ fail(ZH ? `子目录 "${subpath}" 里没有 manifest.json` : `Subdirectory "${subpath}" has no manifest.json`);
1687
1959
  return dir;
1688
1960
  }
1689
1961
  if (existsSync(join(checkout, "manifest.json"))) return checkout;
1690
1962
  const hits = findManifestDirs(checkout);
1691
- if (hits.length === 0) fail(`"${repoLabel}" 里没找到 manifest.json —— 不是 pi-web-ui 界面插件`);
1963
+ if (hits.length === 0)
1964
+ fail(
1965
+ ZH
1966
+ ? `"${repoLabel}" 里没找到 manifest.json —— 不是 pi-web-ui 界面插件`
1967
+ : `"${repoLabel}" does not contain manifest.json — not a pi-web-ui UI plugin`,
1968
+ );
1692
1969
  if (hits.length > 1)
1693
1970
  fail(
1694
- `${repoLabel} 里有多个插件(多个 manifest.json),请用子目录写法指定其中一个:\n ` +
1971
+ (ZH
1972
+ ? `${repoLabel} 里有多个插件(多个 manifest.json),请用子目录写法指定其中一个:\n `
1973
+ : `${repoLabel} contains multiple plugins (multiple manifest.json files), use a subdirectory path to specify one:\n `) +
1695
1974
  hits.map((h) => `${repoLabel}/${relative(checkout, h).split(/[\\/]/).join("/")}`).join("\n "),
1696
1975
  );
1697
- console.log(`· 插件位于子目录: ${relative(checkout, hits[0]).split(/[\\/]/).join("/")}`);
1976
+ console.log(
1977
+ ZH
1978
+ ? `· 插件位于子目录: ${relative(checkout, hits[0]).split(/[\\/]/).join("/")}`
1979
+ : `· Plugin found in subdirectory: ${relative(checkout, hits[0]).split(/[\\/]/).join("/")}`,
1980
+ );
1698
1981
  return hits[0];
1699
1982
  }
1700
1983
 
@@ -1742,7 +2025,12 @@ function runBuildCommand(cmd, cwd, label) {
1742
2025
  timeout: 600_000,
1743
2026
  env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false", NO_COLOR: "1" },
1744
2027
  });
1745
- if (res.status !== 0) throw new Error(`${label} 失败(退出码 ${res.status ?? "?"}):${cmd}`);
2028
+ if (res.status !== 0)
2029
+ throw new Error(
2030
+ ZH
2031
+ ? `${label} 失败(退出码 ${res.status ?? "?"}):${cmd}`
2032
+ : `${label} failed (exit code ${res.status ?? "?"}): ${cmd}`,
2033
+ );
1746
2034
  }
1747
2035
 
1748
2036
  /**
@@ -1756,16 +2044,27 @@ function buildPluginSource(pluginRoot, tmpDir, manifest) {
1756
2044
  const plan = resolveBuildPlan(pluginRoot, manifest);
1757
2045
  if (!plan)
1758
2046
  throw new Error(
1759
- "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build",
2047
+ ZH
2048
+ ? "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build"
2049
+ : "Plugin has no build declaration: add build.command to manifest.json (or scripts.build in package.json), or remove --build",
1760
2050
  );
1761
2051
  const buildDir = join(tmpDir, "build");
1762
2052
  mkdirSync(buildDir, { recursive: true });
1763
2053
  cpSync(pluginRoot, buildDir, { recursive: true, filter: PLUGIN_COPY_FILTER });
1764
- runBuildCommand(plan.install, buildDir, "安装构建依赖");
1765
- runBuildCommand(plan.command, buildDir, "构建");
2054
+ runBuildCommand(plan.install, buildDir, ZH ? "安装构建依赖" : "installing build dependencies");
2055
+ runBuildCommand(plan.command, buildDir, ZH ? "构建" : "building");
1766
2056
  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(", ")}`);
2057
+ if (missing.length)
2058
+ throw new Error(
2059
+ ZH
2060
+ ? `构建产物缺失:${missing.join(", ")}(manifest.build.outputs 声明)`
2061
+ : `Build artifacts missing: ${missing.join(", ")} (declared in manifest.build.outputs)`,
2062
+ );
2063
+ console.log(
2064
+ ZH
2065
+ ? `· 构建完成,产物齐全:${plan.outputs.join(", ")}`
2066
+ : `· Build complete, all artifacts present: ${plan.outputs.join(", ")}`,
2067
+ );
1769
2068
  return buildDir;
1770
2069
  }
1771
2070
 
@@ -1778,14 +2077,19 @@ function buildPluginSource(pluginRoot, tmpDir, manifest) {
1778
2077
  * --no-build 保留旧的“装个空目录”行为(脚本化镜像/检查用),并明确打印跳过原因。
1779
2078
  */
1780
2079
  function decideBuildAction({ pluginRoot, manifest, build, noBuild }) {
1781
- if (build && noBuild) throw new Error("--build 与 --no-build 不能同时用(二选一)");
2080
+ if (build && noBuild)
2081
+ throw new Error(
2082
+ ZH ? "--build 与 --no-build 不能同时用(二选一)" : "--build and --no-build cannot be used together (pick one)",
2083
+ );
1782
2084
  const plan = resolveBuildPlan(pluginRoot, manifest);
1783
2085
  const artifactsMissing =
1784
2086
  !existsSync(join(pluginRoot, "index.mjs")) && !existsSync(join(pluginRoot, "client", "entry.mjs"));
1785
2087
  if (build) {
1786
2088
  if (!plan)
1787
2089
  throw new Error(
1788
- "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build",
2090
+ ZH
2091
+ ? "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build"
2092
+ : "Plugin has no build declaration: add build.command to manifest.json (or scripts.build in package.json), or remove --build",
1789
2093
  );
1790
2094
  return { mode: "explicit", plan };
1791
2095
  }
@@ -1820,7 +2124,11 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1820
2124
  try {
1821
2125
  manifest = JSON.parse(readFileSync(join(pluginRoot, "manifest.json"), "utf8"));
1822
2126
  } catch (err) {
1823
- throw new Error(`manifest.json 不是合法 JSON:${err?.message ?? err}`);
2127
+ throw new Error(
2128
+ ZH
2129
+ ? `manifest.json 不是合法 JSON:${err?.message ?? err}`
2130
+ : `manifest.json is not valid JSON: ${err?.message ?? err}`,
2131
+ );
1824
2132
  }
1825
2133
  // 构建决策(issue #150 的 --build + issue #165 的自动推断):构建在临时目录里完成,
1826
2134
  // 成功后才进入覆盖流程——构建失败 = 目标目录完全没被动过(上一版插件照常可用)。
@@ -1828,7 +2136,9 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1828
2136
  let installRoot = pluginRoot;
1829
2137
  if (decision.mode === "explicit" || decision.mode === "auto") {
1830
2138
  console.log(
1831
- `· 源码构建(${decision.mode === "auto" ? "自动推断:有构建声明但无产物" : "--build"}):先 ${decision.plan.install},再 ${decision.plan.command}`,
2139
+ ZH
2140
+ ? `· 源码构建(${decision.mode === "auto" ? "自动推断:有构建声明但无产物" : "--build"}):先 ${decision.plan.install},再 ${decision.plan.command}`
2141
+ : `· Source build (${decision.mode === "auto" ? "auto-detected: build declaration present but no artifacts" : "--build"}): first ${decision.plan.install}, then ${decision.plan.command}`,
1832
2142
  );
1833
2143
  try {
1834
2144
  installRoot = buildPluginSource(pluginRoot, tmp, manifest);
@@ -1836,7 +2146,11 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1836
2146
  throw new Error(`${err?.message ?? err}`);
1837
2147
  }
1838
2148
  } else if (decision.mode === "skipped") {
1839
- console.log("· 跳过构建(--no-build):目录里没有产物,装上后该插件不会被加载");
2149
+ console.log(
2150
+ ZH
2151
+ ? "· 跳过构建(--no-build):目录里没有产物,装上后该插件不会被加载"
2152
+ : "· Skipping build (--no-build): no artifacts in directory, plugin will not load after install",
2153
+ );
1840
2154
  }
1841
2155
  // 默认 id:子目录名 > 仓库名 > 本地目录名
1842
2156
  const sourceName = src?.subpath ? src.subpath.split("/").pop() : (src?.repo ?? localCandidate.split(/[\\/]/).pop());
@@ -1845,13 +2159,23 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1845
2159
  .replace(/[^A-Za-z0-9_-]/g, "-")
1846
2160
  .replace(/^-+|-+$/g, "") || "plugin";
1847
2161
  const id = name ?? fallbackId;
1848
- if (!PLUGIN_ID_RE.test(id)) throw new Error(`非法插件 id "${id}"(仅限字母数字-_,可用 --name <id> 自定义)`);
2162
+ if (!PLUGIN_ID_RE.test(id))
2163
+ throw new Error(
2164
+ ZH
2165
+ ? `非法插件 id "${id}"(仅限字母数字-_,可用 --name <id> 自定义)`
2166
+ : `Invalid plugin id "${id}" (alphanumeric and -_ only; use --name <id> to set a custom name)`,
2167
+ );
1849
2168
  const target = join(pluginsDir, id);
1850
2169
  let backupTs = null;
1851
2170
  let prevConfig = null;
1852
2171
  const CONFIG_NAME = "config.json";
1853
2172
  if (existsSync(target)) {
1854
- if (!force) throw new Error(`插件目录已存在:${target}\n 加 --force 覆盖,或用 --name <id> 换个名字。`);
2173
+ if (!force)
2174
+ throw new Error(
2175
+ ZH
2176
+ ? `插件目录已存在:${target}\n 加 --force 覆盖,或用 --name <id> 换个名字。`
2177
+ : `Plugin directory already exists: ${target}\n Add --force to overwrite, or use --name <id> to choose a different name.`,
2178
+ );
1855
2179
  // 更新前备份旧版本(<dataDir>/plugin-backups/<id>-<ts>/,保留最近 3 份),
1856
2180
  // 失败时自动回滚。备份与安装同 filter:不带 .git/node_modules。
1857
2181
  backupTs = ensurePluginBackup(dataDir, id, { source: rawSpec });
@@ -1869,9 +2193,17 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
1869
2193
  } catch (err) {
1870
2194
  // 拷贝失败 → 有备份则自动回滚,保持旧版本可用
1871
2195
  if (backupTs && restorePluginBackup(dataDir, id)) {
1872
- throw new Error(`插件更新失败:${err?.message ?? err}\n 已自动回滚到更新前版本。`);
2196
+ throw new Error(
2197
+ ZH
2198
+ ? `插件更新失败:${err?.message ?? err}\n 已自动回滚到更新前版本。`
2199
+ : `Plugin update failed: ${err?.message ?? err}\n Automatically rolled back to the previous version.`,
2200
+ );
1873
2201
  }
1874
- throw new Error(`插件更新失败:${err?.message ?? err}\n (无可用备份,请重新 install --force)`);
2202
+ throw new Error(
2203
+ ZH
2204
+ ? `插件更新失败:${err?.message ?? err}\n (无可用备份,请重新 install --force)`
2205
+ : `Plugin update failed: ${err?.message ?? err}\n (no backup available, re-run install --force)`,
2206
+ );
1875
2207
  }
1876
2208
  if (prevConfig !== null && !existsSync(join(target, CONFIG_NAME))) {
1877
2209
  writeFileSync(join(target, CONFIG_NAME), prevConfig);
@@ -1969,7 +2301,12 @@ function normalizeCatalogEntries(raw) {
1969
2301
  : raw && typeof raw === "object" && Array.isArray(raw.entries)
1970
2302
  ? raw.entries
1971
2303
  : null;
1972
- if (!list) throw new Error('目录 JSON 需为数组,或 {"entries": [...]} 形状');
2304
+ if (!list)
2305
+ throw new Error(
2306
+ ZH
2307
+ ? '目录 JSON 需为数组,或 {"entries": [...]} 形状'
2308
+ : 'Catalog JSON must be an array or {"entries": [...]} shape',
2309
+ );
1973
2310
  const entries = [];
1974
2311
  let skipped = 0;
1975
2312
  for (const it of list) {
@@ -1983,24 +2320,38 @@ function normalizeCatalogEntries(raw) {
1983
2320
  /** 读目录文档:http(s) 拉取(30s 超时),其余当本地绝对路径(与服务端同口径)。 */
1984
2321
  async function readCatalogDocumentText(source) {
1985
2322
  const src = String(source ?? "").trim();
1986
- if (!src) throw new Error("缺少目录来源(--catalog <url 或本地绝对路径>)");
2323
+ if (!src)
2324
+ throw new Error(
2325
+ ZH
2326
+ ? "缺少目录来源(--catalog <url 或本地绝对路径>)"
2327
+ : "Missing catalog source (--catalog <url or local absolute path>)",
2328
+ );
1987
2329
  if (/^https?:\/\//i.test(src)) {
1988
2330
  let res;
1989
2331
  try {
1990
2332
  res = await fetch(src, { redirect: "follow", signal: AbortSignal.timeout(30_000) });
1991
2333
  } catch (err) {
1992
- throw new Error(`拉取目录失败:${err?.message ?? err}`);
2334
+ throw new Error(ZH ? `拉取目录失败:${err?.message ?? err}` : `Failed to fetch catalog: ${err?.message ?? err}`);
1993
2335
  }
1994
- if (!res.ok) throw new Error(`拉取目录失败:HTTP ${res.status}`);
2336
+ if (!res.ok)
2337
+ throw new Error(ZH ? `拉取目录失败:HTTP ${res.status}` : `Failed to fetch catalog: HTTP ${res.status}`);
1995
2338
  const text = await res.text();
1996
- if (text.length > 1024 * 1024) throw new Error("目录文档过大(> 1024 KB)");
2339
+ if (text.length > 1024 * 1024)
2340
+ throw new Error(ZH ? "目录文档过大(> 1024 KB)" : "Directory document too large (> 1024 KB)");
1997
2341
  return text;
1998
2342
  }
1999
- if (!isAbsolute(src)) throw new Error("本地目录文档需为绝对路径(远端用 http(s) URL)");
2343
+ if (!isAbsolute(src))
2344
+ throw new Error(
2345
+ ZH
2346
+ ? "本地目录文档需为绝对路径(远端用 http(s) URL)"
2347
+ : "Local directory document must be an absolute path (remote: use http(s) URL)",
2348
+ );
2000
2349
  try {
2001
2350
  return readFileSync(src, "utf8");
2002
2351
  } catch (err) {
2003
- throw new Error(`读取目录文件失败:${err?.message ?? err}`);
2352
+ throw new Error(
2353
+ ZH ? `读取目录文件失败:${err?.message ?? err}` : `Failed to read directory file: ${err?.message ?? err}`,
2354
+ );
2004
2355
  }
2005
2356
  }
2006
2357
 
@@ -2031,7 +2382,7 @@ async function installCatalogCmd(opts) {
2031
2382
  try {
2032
2383
  raw = JSON.parse(text);
2033
2384
  } catch (err) {
2034
- fail(`目录 JSON 解析失败:${err?.message ?? err}`);
2385
+ fail(ZH ? `目录 JSON 解析失败:${err?.message ?? err}` : `Directory JSON parse failed: ${err?.message ?? err}`);
2035
2386
  }
2036
2387
  let entries;
2037
2388
  let skipped = 0;
@@ -2054,14 +2405,20 @@ async function installCatalogCmd(opts) {
2054
2405
  writeCatalogFileEntries(customPath, next);
2055
2406
  }
2056
2407
  console.log(
2057
- `· 目录同步:${entries.length} 条合法${skipped ? `(丢弃 ${skipped} 条非法)` : ""}${opts.replace ? "(整体替换)" : "(按 id 合并)"} → ${customPath}`,
2408
+ ZH
2409
+ ? `· 目录同步:${entries.length} 条合法${skipped ? `(丢弃 ${skipped} 条非法)` : ""}${opts.replace ? "(整体替换)" : "(按 id 合并)"} → ${customPath}`
2410
+ : `· Directory sync: ${entries.length} valid${skipped ? `(discarded ${skipped} invalid)` : ""}${opts.replace ? "(full replace)" : "(merge by id)"} → ${customPath}`,
2058
2411
  );
2059
2412
  let okCount = 0;
2060
2413
  let failCount = 0;
2061
2414
  let skipCount = 0;
2062
2415
  for (const e of entries) {
2063
2416
  if (existsSync(join(pluginsDir, e.id)) && !opts.force) {
2064
- console.log(`· 跳过 ${e.id}(已安装,加 --force 更新)`);
2417
+ console.log(
2418
+ ZH
2419
+ ? `· 跳过 ${e.id}(已安装,加 --force 更新)`
2420
+ : `· Skipping ${e.id} (already installed, use --force to update)`,
2421
+ );
2065
2422
  skipCount++;
2066
2423
  continue;
2067
2424
  }
@@ -2074,14 +2431,20 @@ async function installCatalogCmd(opts) {
2074
2431
  noBuild: opts.noBuild === true,
2075
2432
  dataDir,
2076
2433
  });
2077
- console.log(`✔ ${e.id} 安装成功`);
2434
+ console.log(ZH ? `✔ ${e.id} 安装成功` : `✔ ${e.id} installed`);
2078
2435
  okCount++;
2079
2436
  } catch (err) {
2080
- console.error(`✖ ${e.id} 安装失败:${err?.message ?? err}`);
2437
+ console.error(
2438
+ ZH ? `✖ ${e.id} 安装失败:${err?.message ?? err}` : `✖ ${e.id} failed to install: ${err?.message ?? err}`,
2439
+ );
2081
2440
  failCount++;
2082
2441
  }
2083
2442
  }
2084
- console.log(`✔ 目录安装完成:${okCount} 成功 / ${failCount} 失败 / ${skipCount} 跳过(已安装)`);
2443
+ console.log(
2444
+ ZH
2445
+ ? `✔ 目录安装完成:${okCount} 成功 / ${failCount} 失败 / ${skipCount} 跳过(已安装)`
2446
+ : `✔ Directory install complete: ${okCount} succeeded / ${failCount} failed / ${skipCount} skipped (already installed)`,
2447
+ );
2085
2448
  if (failCount) process.exitCode = 1;
2086
2449
  }
2087
2450
 
@@ -2363,23 +2726,45 @@ function buildPluginScaffold(id, template, useSdk, withTest = false) {
2363
2726
  function validateScaffoldManifest(manifest, dirName) {
2364
2727
  const warnings = [];
2365
2728
  for (const k of ["id", "name", "version", "description"]) {
2366
- if (typeof manifest[k] !== "string" || !manifest[k].trim()) warnings.push(`manifest 缺少必填字段 "${k}"`);
2729
+ if (typeof manifest[k] !== "string" || !manifest[k].trim())
2730
+ warnings.push(ZH ? `manifest 缺少必填字段 "${k}"` : `manifest missing required field "${k}"`);
2367
2731
  }
2368
2732
  if (manifest.id && manifest.id !== dirName)
2369
- warnings.push(`manifest.id "${manifest.id}" 与目录名 "${dirName}" 不一致(以目录名为准)`);
2733
+ warnings.push(
2734
+ ZH
2735
+ ? `manifest.id "${manifest.id}" 与目录名 "${dirName}" 不一致(以目录名为准)`
2736
+ : `manifest.id "${manifest.id}" and directory name "${dirName}" is inconsistent (directory name takes precedence)`,
2737
+ );
2370
2738
  if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
2371
- warnings.push(`manifest.version "${manifest.version}" 不是 x.y.z 格式`);
2739
+ warnings.push(
2740
+ ZH
2741
+ ? `manifest.version "${manifest.version}" 不是 x.y.z 格式`
2742
+ : `manifest.version "${manifest.version}" is not in x.y.z format`,
2743
+ );
2372
2744
  }
2373
2745
  const api = manifest.apiVersion ?? 1;
2374
2746
  if (typeof api !== "number" || api > 2) {
2375
- warnings.push(`manifest.apiVersion=${JSON.stringify(api)} 高于宿主 v2,插件会被拒绝激活`);
2747
+ warnings.push(
2748
+ ZH
2749
+ ? `manifest.apiVersion=${JSON.stringify(api)} 高于宿主 v2,插件会被拒绝激活`
2750
+ : `manifest.apiVersion=${JSON.stringify(api)} exceeds host v2, plugin will be rejected on activation`,
2751
+ );
2376
2752
  }
2377
2753
  if (manifest.permissions !== undefined) {
2378
2754
  const ok = Array.isArray(manifest.permissions) && manifest.permissions.every((p) => typeof p === "string" && p);
2379
- if (!ok) warnings.push(`manifest.permissions 须是字符串数组(如 ["ui"])`);
2755
+ if (!ok)
2756
+ warnings.push(
2757
+ ZH
2758
+ ? `manifest.permissions 须是字符串数组(如 ["ui"])`
2759
+ : `manifest.permissions must be a string array (e.g. ["ui"])`,
2760
+ );
2380
2761
  }
2381
2762
  if (manifest.view === false && !(Array.isArray(manifest.renderers) && manifest.renderers.length > 0)) {
2382
- warnings.push(`view:false 但未声明 renderers —— 插件将没有任何界面`);
2763
+ warnings.push(
2764
+ ZH
2765
+ ? `view:false 但未声明 renderers —— 插件将没有任何界面`
2766
+ : `view:false but no renderers declared — plugin will have no UI`,
2767
+ );
2383
2768
  }
2384
2769
  return warnings;
2385
2770
  }
@@ -2398,15 +2783,29 @@ function installedSdkVersion(sdkFile) {
2398
2783
  /** 包内 SDK 版本(单源:plugin-sdk/index.mjs 的 `export const SDK_VERSION`)。 */
2399
2784
  function packageSdkVersion() {
2400
2785
  const sdkSrc = pluginSdkSource();
2401
- if (!sdkSrc) fail(`包内无 plugin-sdk(找不到 plugin-sdk/index.mjs),无法刷新`);
2786
+ if (!sdkSrc)
2787
+ fail(
2788
+ ZH
2789
+ ? `包内无 plugin-sdk(找不到 plugin-sdk/index.mjs),无法刷新`
2790
+ : `No plugin-sdk in package (plugin-sdk/index.mjs not found), cannot refresh`,
2791
+ );
2402
2792
  let src;
2403
2793
  try {
2404
2794
  src = readFileSync(sdkSrc, "utf8");
2405
2795
  } catch {
2406
- fail(`包内 plugin-sdk/index.mjs 不可读,无法刷新`);
2796
+ fail(
2797
+ ZH
2798
+ ? `包内 plugin-sdk/index.mjs 不可读,无法刷新`
2799
+ : `plugin-sdk/index.mjs in package is unreadable, cannot refresh`,
2800
+ );
2407
2801
  }
2408
2802
  const m = src.match(/export const SDK_VERSION\s*=\s*["']([^"']+)["']/);
2409
- if (!m) fail(`包内 plugin-sdk/index.mjs 无 SDK_VERSION 导出,无法刷新(请升级 pi-web-ui)`);
2803
+ if (!m)
2804
+ fail(
2805
+ ZH
2806
+ ? `包内 plugin-sdk/index.mjs 无 SDK_VERSION 导出,无法刷新(请升级 pi-web-ui)`
2807
+ : `plugin-sdk/index.mjs in package has no SDK_VERSION export, cannot refresh (please upgrade pi-web-ui)`,
2808
+ );
2410
2809
  return { version: m[1], file: sdkSrc };
2411
2810
  }
2412
2811
 
@@ -2421,15 +2820,21 @@ function pluginUpgradeSdkCmd(argv) {
2421
2820
  const { version: latest, file: sdkSrc } = packageSdkVersion();
2422
2821
  const parentDir = opts.dir ? resolve(opts.dir) : join(pluginDataDir(opts), "plugins");
2423
2822
  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 查看已装列表)`);
2823
+ if (only && !PLUGIN_ID_RE.test(only))
2824
+ fail(ZH ? `非法插件 id "${only}"(仅限字母数字-_)` : `Invalid plugin id "${only}" (alphanumeric and -_ only)`);
2825
+ if (only && !existsSync(join(parentDir, only)))
2826
+ fail(
2827
+ ZH
2828
+ ? `未安装插件 "${only}"(pi-web-ui plugins 查看已装列表)`
2829
+ : `Plugin not installed: "${only}" (pi-web-ui plugins to see installed list)`,
2830
+ );
2426
2831
  let entries;
2427
2832
  try {
2428
2833
  entries = readdirSync(parentDir, { withFileTypes: true })
2429
2834
  .filter((e) => e.isDirectory() && PLUGIN_ID_RE.test(e.name))
2430
2835
  .map((e) => e.name);
2431
2836
  } catch {
2432
- fail(`读插件目录失败: ${parentDir}`);
2837
+ fail(ZH ? `读插件目录失败: ${parentDir}` : `Failed to read plugin directory: ${parentDir}`);
2433
2838
  }
2434
2839
  const targets = only ? [only] : entries;
2435
2840
  let upgraded = 0;
@@ -2440,26 +2845,43 @@ function pluginUpgradeSdkCmd(argv) {
2440
2845
  const sdkFile = join(parentDir, id, "sdk", "index.mjs");
2441
2846
  if (!existsSync(sdkFile)) {
2442
2847
  skipped++;
2443
- console.log(`- ${id}:无 sdk/index.mjs 拷贝,跳过`);
2848
+ console.log(ZH ? `- ${id}:无 sdk/index.mjs 拷贝,跳过` : `- ${id}: no sdk/index.mjs copy, skipping`);
2444
2849
  continue;
2445
2850
  }
2446
2851
  const cur = installedSdkVersion(sdkFile);
2447
2852
  if (cur === latest) {
2448
2853
  fresh++;
2449
- console.log(`✔ ${id}:已是最新(SDK ${latest})`);
2854
+ console.log(ZH ? `✔ ${id}:已是最新(SDK ${latest})` : `✔ ${id}: already up to date (SDK ${latest})`);
2450
2855
  continue;
2451
2856
  }
2452
2857
  copyFileSync(sdkSrc, sdkFile);
2453
2858
  const r = spawnSync(NODE, ["--check", sdkFile], { stdio: "ignore" });
2454
- if (r.status !== 0) broken.push(`${sdkFile} 未通过 node --check(磁盘/权限异常?请手动检查)`);
2859
+ if (r.status !== 0)
2860
+ broken.push(
2861
+ ZH
2862
+ ? `${sdkFile} 未通过 node --check(磁盘/权限异常?请手动检查)`
2863
+ : `${sdkFile} failed node --check (disk/permission issue? please check manually)`,
2864
+ );
2455
2865
  else {
2456
2866
  upgraded++;
2457
- console.log(`✔ ${id}:SDK ${cur ?? "未知旧版"} → ${latest}`);
2867
+ console.log(
2868
+ ZH
2869
+ ? `✔ ${id}:SDK ${cur ?? "未知旧版"} → ${latest}`
2870
+ : `✔ ${id}: SDK ${cur ?? "unknown old version"} → ${latest}`,
2871
+ );
2458
2872
  }
2459
2873
  }
2460
2874
  for (const w of broken) console.log(`⚠ ${w}`);
2461
- console.log(`共 ${targets.length} 个插件:刷新 ${upgraded} 个,已最新 ${fresh} 个,跳过 ${skipped} 个。`);
2462
- console.log(` 生效: 服务运行中插件重载(或刷新浏览器)后新 SDK 生效;未运行则下次启动生效。`);
2875
+ console.log(
2876
+ ZH
2877
+ ? `共 ${targets.length} 个插件:刷新 ${upgraded} 个,已最新 ${fresh} 个,跳过 ${skipped} 个。`
2878
+ : `Total ${targets.length} plugin(s): refreshed ${upgraded}, up to date ${fresh}, skipped ${skipped}.`,
2879
+ );
2880
+ console.log(
2881
+ ZH
2882
+ ? ` 生效: 服务运行中插件重载(或刷新浏览器)后新 SDK 生效;未运行则下次启动生效。`
2883
+ : ` Effect: new SDK takes effect after plugin reload (or browser refresh) while the service is running; if not running, takes effect on next start.`,
2884
+ );
2463
2885
  }
2464
2886
 
2465
2887
  function pluginCreateCmd(argv) {
@@ -2470,12 +2892,21 @@ function pluginCreateCmd(argv) {
2470
2892
  return;
2471
2893
  }
2472
2894
  const id = positionals[0];
2473
- if (!PLUGIN_ID_RE.test(id)) fail(`非法插件 id "${id}"(仅限字母数字-_)`);
2895
+ if (!PLUGIN_ID_RE.test(id))
2896
+ fail(ZH ? `非法插件 id "${id}"(仅限字母数字-_)` : `Invalid plugin id "${id}" (alphanumeric and -_ only)`);
2474
2897
  const template = opts.template ?? "minimal";
2475
- if (!PLUGIN_TEMPLATES.includes(template)) fail(`未知模板 "${template}"(可选 ${PLUGIN_TEMPLATES.join("|")})`);
2898
+ if (!PLUGIN_TEMPLATES.includes(template))
2899
+ fail(
2900
+ ZH
2901
+ ? `未知模板 "${template}"(可选 ${PLUGIN_TEMPLATES.join("|")})`
2902
+ : `Unknown template "${template}" (options: ${PLUGIN_TEMPLATES.join("|")})`,
2903
+ );
2476
2904
  const parentDir = opts.dir ? resolve(opts.dir) : join(pluginDataDir(opts), "plugins");
2477
2905
  const target = join(parentDir, id);
2478
- if (existsSync(target) && opts.force !== true) fail(`目标已存在: ${target}(加 --force 覆盖)`);
2906
+ if (existsSync(target) && opts.force !== true)
2907
+ fail(
2908
+ ZH ? `目标已存在: ${target}(加 --force 覆盖)` : `Target already exists: ${target} (add --force to overwrite)`,
2909
+ );
2479
2910
  if (existsSync(target)) rmSync(target, { recursive: true, force: true });
2480
2911
  mkdirSync(join(target, "client"), { recursive: true });
2481
2912
  const sdkSrc = pluginSdkSource();
@@ -2492,25 +2923,57 @@ function pluginCreateCmd(argv) {
2492
2923
  try {
2493
2924
  manifest = JSON.parse(readFileSync(join(target, "manifest.json"), "utf8"));
2494
2925
  } catch {
2495
- fail(`生成的 manifest.json 解析失败(请检查磁盘/权限)`);
2926
+ fail(
2927
+ ZH
2928
+ ? `生成的 manifest.json 解析失败(请检查磁盘/权限)`
2929
+ : `Generated manifest.json failed to parse (please check disk/permissions)`,
2930
+ );
2496
2931
  }
2497
2932
  const warnings = validateScaffoldManifest(manifest, id);
2498
2933
  if (wantTest && !useSdk)
2499
- warnings.push(`已加 --with-test 但包内无 plugin-sdk,跳过 index.test.mjs(无 SDK 版没有 createMockHost 可用)`);
2934
+ warnings.push(
2935
+ ZH
2936
+ ? `已加 --with-test 但包内无 plugin-sdk,跳过 index.test.mjs(无 SDK 版没有 createMockHost 可用)`
2937
+ : `--with-test specified but no plugin-sdk in package, skipping index.test.mjs (no createMockHost available without SDK)`,
2938
+ );
2500
2939
  const checkFiles = ["index.mjs", "client/entry.mjs"];
2501
2940
  if (wantTest && useSdk) checkFiles.push("index.test.mjs");
2502
2941
  for (const rel of checkFiles) {
2503
2942
  const r = spawnSync(NODE, ["--check", join(target, rel)], { stdio: "ignore" });
2504
- if (r.status !== 0) warnings.push(`${rel} 未通过 node --check(请检查生成文件)`);
2943
+ if (r.status !== 0)
2944
+ warnings.push(
2945
+ ZH
2946
+ ? `${rel} 未通过 node --check(请检查生成文件)`
2947
+ : `${rel} failed node --check (please check generated files)`,
2948
+ );
2505
2949
  }
2506
- console.log(`✔ 已生成插件骨架 ${id}(模板 ${template})`);
2507
- console.log(` 位置: ${target}`);
2950
+ console.log(
2951
+ ZH ? `✔ 已生成插件骨架 ${id}(模板 ${template})` : `✔ Plugin scaffold generated: ${id} (template: ${template})`,
2952
+ );
2953
+ console.log(ZH ? ` 位置: ${target}` : ` Location: ${target}`);
2508
2954
  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)`);
2955
+ console.log(
2956
+ ZH
2957
+ ? ` 单测: node --test ${join(target, "index.test.mjs")}(createMockHost harness)`
2958
+ : ` Unit test: node --test ${join(target, "index.test.mjs")} (createMockHost harness)`,
2959
+ );
2960
+ if (!useSdk)
2961
+ console.log(
2962
+ ZH
2963
+ ? `⚠ 未找到包内 plugin-sdk,已生成无 SDK 依赖版本(逻辑等价,详见 plugin-sdk/README.md)`
2964
+ : `⚠ No plugin-sdk found in package, generated SDK-free version (logically equivalent, see plugin-sdk/README.md)`,
2965
+ );
2511
2966
  for (const w of warnings) console.log(`⚠ ${w}`);
2512
- console.log(` 生效: 服务运行中刷新浏览器即可加载(或发 plugins_reload);未运行则下次启动生效。`);
2513
- console.log(` 下一步: 打开 ${join(target, "README.md")}(改名 → 写逻辑 → 刷新验证)`);
2967
+ console.log(
2968
+ ZH
2969
+ ? ` 生效: 服务运行中刷新浏览器即可加载(或发 plugins_reload);未运行则下次启动生效。`
2970
+ : ` Effect: reload the browser to load while the service is running (or send plugins_reload); if not running, takes effect on next start.`,
2971
+ );
2972
+ console.log(
2973
+ ZH
2974
+ ? ` 下一步: 打开 ${join(target, "README.md")}(改名 → 写逻辑 → 刷新验证)`
2975
+ : ` Next: open ${join(target, "README.md")} (rename → write logic → refresh to verify)`,
2976
+ );
2514
2977
  }
2515
2978
 
2516
2979
  async function pluginInstallCmd(argv) {
@@ -2524,15 +2987,24 @@ async function pluginInstallCmd(argv) {
2524
2987
  if (opts.catalog !== undefined) {
2525
2988
  if (positionals.length !== 0)
2526
2989
  fail(
2527
- `用法: pi-web-ui install --catalog <目录> [--data-dir <dir>] [--force] [--build|--no-build] [--replace]\n${PLUGIN_HELP}`,
2990
+ ZH
2991
+ ? `用法: pi-web-ui install --catalog <目录> [--data-dir <dir>] [--force] [--build|--no-build] [--replace]\n${PLUGIN_HELP}`
2992
+ : `Usage: pi-web-ui install --catalog <dir> [--data-dir <dir>] [--force] [--build|--no-build] [--replace]\n${PLUGIN_HELP}`,
2993
+ );
2994
+ if (opts.name)
2995
+ fail(
2996
+ ZH
2997
+ ? "--catalog 模式下 --name 无意义(目录名/id 来自目录条目)"
2998
+ : "--name has no effect in --catalog mode (directory name/id comes from directory entries)",
2528
2999
  );
2529
- if (opts.name) fail("--catalog 模式下 --name 无意义(目录名/id 来自目录条目)");
2530
3000
  await installCatalogCmd(opts);
2531
3001
  return;
2532
3002
  }
2533
3003
  if (positionals.length !== 1)
2534
3004
  fail(
2535
- `用法: pi-web-ui install <源> [--name <id>] [--data-dir <dir>] [--force] [--build|--no-build]\n${PLUGIN_HELP}`,
3005
+ ZH
3006
+ ? `用法: pi-web-ui install <源> [--name <id>] [--data-dir <dir>] [--force] [--build|--no-build]\n${PLUGIN_HELP}`
3007
+ : `Usage: pi-web-ui install <source> [--name <id>] [--data-dir <dir>] [--force] [--build|--no-build]\n${PLUGIN_HELP}`,
2536
3008
  );
2537
3009
  try {
2538
3010
  const { id, target, manifest } = await installOnePlugin({
@@ -2544,11 +3016,17 @@ async function pluginInstallCmd(argv) {
2544
3016
  dataDir: pluginDataDir(opts),
2545
3017
  });
2546
3018
  console.log(
2547
- `✔ 已安装插件 ${id}${manifest.name && manifest.name !== id ? `(${manifest.name})` : ""}${manifest.version ? ` v${manifest.version}` : ""}`,
3019
+ ZH
3020
+ ? `✔ 已安装插件 ${id}${manifest.name && manifest.name !== id ? `(${manifest.name})` : ""}${manifest.version ? ` v${manifest.version}` : ""}`
3021
+ : `✔ Plugin installed: ${id}${manifest.name && manifest.name !== id ? `(${manifest.name})` : ""}${manifest.version ? ` v${manifest.version}` : ""}`,
2548
3022
  );
2549
3023
  if (manifest.description) console.log(` ${manifest.description}`);
2550
- console.log(` 位置: ${target}`);
2551
- console.log(` 生效: 服务运行中刷新浏览器即可加载;未运行则下次启动生效。卸载: pi-web-ui uninstall ${id}`);
3024
+ console.log(ZH ? ` 位置: ${target}` : ` Location: ${target}`);
3025
+ console.log(
3026
+ ZH
3027
+ ? ` 生效: 服务运行中刷新浏览器即可加载;未运行则下次启动生效。卸载: pi-web-ui uninstall ${id}`
3028
+ : ` 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}`,
3029
+ );
2552
3030
  } catch (err) {
2553
3031
  fail(`${err?.message ?? err}`);
2554
3032
  }
@@ -2562,11 +3040,20 @@ function pluginUninstallCmd(argv) {
2562
3040
  return;
2563
3041
  }
2564
3042
  const id = positionals[0];
2565
- if (!PLUGIN_ID_RE.test(id)) fail(`非法插件 id: ${id}`);
3043
+ if (!PLUGIN_ID_RE.test(id)) fail(ZH ? `非法插件 id: ${id}` : `Invalid plugin id: ${id}`);
2566
3044
  const target = join(pluginDataDir(opts), "plugins", id);
2567
- if (!existsSync(target)) fail(`未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`);
3045
+ if (!existsSync(target))
3046
+ fail(
3047
+ ZH
3048
+ ? `未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`
3049
+ : `Plugin not installed: "${id}" (pi-web-ui plugins to see installed list)`,
3050
+ );
2568
3051
  rmSync(target, { recursive: true, force: true });
2569
- console.log(`✔ 已卸载插件 ${id} —— 运行中的服务刷新浏览器后消失。`);
3052
+ console.log(
3053
+ ZH
3054
+ ? `✔ 已卸载插件 ${id} —— 运行中的服务刷新浏览器后消失。`
3055
+ : `✔ Plugin uninstalled: ${id} — disappears after a browser refresh while the service is running.`,
3056
+ );
2570
3057
  }
2571
3058
 
2572
3059
  function pluginListCmd(argv) {
@@ -2579,12 +3066,26 @@ function pluginListCmd(argv) {
2579
3066
  // --rollback <id>:回滚到最近一份更新前备份
2580
3067
  if (opts.rollback) {
2581
3068
  const id = String(opts.rollback);
2582
- if (!PLUGIN_ID_RE.test(id)) fail(`非法插件 id: ${id}`);
3069
+ if (!PLUGIN_ID_RE.test(id)) fail(ZH ? `非法插件 id: ${id}` : `Invalid plugin id: ${id}`);
2583
3070
  const target = join(dataDir, "plugins", id);
2584
- if (!existsSync(target)) fail(`未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`);
3071
+ if (!existsSync(target))
3072
+ fail(
3073
+ ZH
3074
+ ? `未安装插件 "${id}"(pi-web-ui plugins 查看已装列表)`
3075
+ : `Plugin not installed: "${id}" (pi-web-ui plugins to see installed list)`,
3076
+ );
2585
3077
  const ts = restorePluginBackup(dataDir, id);
2586
- if (!ts) fail(`插件 "${id}" 没有更新备份(从未覆盖安装 / 备份已用完)`);
2587
- console.log(`✔ 已回滚插件 ${id} 到 ${ts} 的快照 —— 运行中的服务刷新浏览器后生效。`);
3078
+ if (!ts)
3079
+ fail(
3080
+ ZH
3081
+ ? `插件 "${id}" 没有更新备份(从未覆盖安装 / 备份已用完)`
3082
+ : `Plugin "${id}" has no update backup (never overwrite-installed / backup exhausted)`,
3083
+ );
3084
+ console.log(
3085
+ ZH
3086
+ ? `✔ 已回滚插件 ${id} 到 ${ts} 的快照 —— 运行中的服务刷新浏览器后生效。`
3087
+ : `✔ Plugin rolled back: ${id} to ${ts} snapshot — takes effect after a browser refresh while the service is running.`,
3088
+ );
2588
3089
  return;
2589
3090
  }
2590
3091
  // --check-updates:对比各插件记录的最后安装 sha 与远端 HEAD(git ls-remote)
@@ -2611,22 +3112,38 @@ function pluginListCmd(argv) {
2611
3112
  }
2612
3113
  }
2613
3114
  if (rows.length === 0) {
2614
- console.log(`尚未安装任何界面插件(目录: ${pluginsDir})\n安装示例: pi-web-ui install owner/repo`);
3115
+ console.log(
3116
+ ZH
3117
+ ? `尚未安装任何界面插件(目录: ${pluginsDir})\n安装示例: pi-web-ui install owner/repo`
3118
+ : `No UI plugins installed yet (directory: ${pluginsDir})\nInstall example: pi-web-ui install owner/repo`,
3119
+ );
2615
3120
  return;
2616
3121
  }
2617
- console.log(`已安装的界面插件(${pluginsDir}):\n${rows.join("\n")}`);
3122
+ console.log(
3123
+ ZH
3124
+ ? `已安装的界面插件(${pluginsDir}):\n${rows.join("\n")}`
3125
+ : `Installed UI plugins (${pluginsDir}):\n${rows.join("\n")}`,
3126
+ );
2618
3127
  }
2619
3128
 
2620
3129
  async function checkUpdatesCmd(dataDir) {
2621
- console.log("检查界面插件更新(git ls-remote 对比最近安装版本)…\n");
3130
+ console.log(
3131
+ ZH
3132
+ ? "检查界面插件更新(git ls-remote 对比最近安装版本)…\n"
3133
+ : "Checking UI plugin updates (git ls-remote vs. last installed version)…\n",
3134
+ );
2622
3135
  let rows;
2623
3136
  try {
2624
3137
  rows = await checkPluginUpdates(dataDir);
2625
3138
  } catch (err) {
2626
- fail(`更新检查失败:${err?.message ?? err}`);
3139
+ fail(ZH ? `更新检查失败:${err?.message ?? err}` : `Update check failed: ${err?.message ?? err}`);
2627
3140
  }
2628
3141
  if (rows.length === 0) {
2629
- console.log(`尚未安装任何带来源记录的界面插件(目录: ${join(dataDir, "plugins")})`);
3142
+ console.log(
3143
+ ZH
3144
+ ? `尚未安装任何带来源记录的界面插件(目录: ${join(dataDir, "plugins")})`
3145
+ : `No UI plugins with origin records installed yet (directory: ${join(dataDir, "plugins")})`,
3146
+ );
2630
3147
  return;
2631
3148
  }
2632
3149
  let any = false;
@@ -2634,17 +3151,31 @@ async function checkUpdatesCmd(dataDir) {
2634
3151
  const label = r.name && r.name !== r.id ? `${r.id}(${r.name})` : r.id;
2635
3152
  if (r.updatable) {
2636
3153
  console.log(
2637
- ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} 可更新(已装 ${r.localSha ?? "未知"} → 远端 ${r.remoteSha})`,
3154
+ ZH
3155
+ ? ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} 可更新(已装 ${r.localSha ?? "未知"} → 远端 ${r.remoteSha})`
3156
+ : ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} Updatable (installed ${r.localSha ?? "unknown"} → remote ${r.remoteSha})`,
3157
+ );
3158
+ console.log(
3159
+ ZH
3160
+ ? ` 更新: pi-web-ui install ${r.source} --name ${r.id} --force`
3161
+ : ` Update: pi-web-ui install ${r.source} --name ${r.id} --force`,
2638
3162
  );
2639
- console.log(` 更新: pi-web-ui install ${r.source} --name ${r.id} --force`);
2640
3163
  any = true;
2641
3164
  } else if (r.remoteSha) {
2642
- console.log(` ✓ ${label}${r.version ? ` v${r.version}` : ""} 已是最新(${r.remoteSha})`);
3165
+ console.log(
3166
+ ZH
3167
+ ? ` ✓ ${label}${r.version ? ` v${r.version}` : ""} 已是最新(${r.remoteSha})`
3168
+ : ` ✓ ${label}${r.version ? ` v${r.version}` : ""} Up to date (${r.remoteSha})`,
3169
+ );
2643
3170
  } else {
2644
- console.log(` ? ${label} ${r.error ?? "无法检查"}(来源: ${r.source})`);
3171
+ console.log(
3172
+ ZH
3173
+ ? ` ? ${label} ${r.error ?? "无法检查"}(来源: ${r.source})`
3174
+ : ` ? ${label} ${r.error ?? "cannot check"} (source: ${r.source})`,
3175
+ );
2645
3176
  }
2646
3177
  }
2647
- if (!any) console.log("\n全部插件均为最新版本。");
3178
+ if (!any) console.log(ZH ? "\n全部插件均为最新版本。" : "\nAll plugins are up to date.");
2648
3179
  }
2649
3180
 
2650
3181
  async function serverCmd(argv) {
@@ -2655,12 +3186,15 @@ async function serverCmd(argv) {
2655
3186
  }
2656
3187
  if (positionals.length === 0) {
2657
3188
  console.log(HELP);
2658
- console.log("--- 当前服务状态 ---");
3189
+ console.log(ZH ? "--- 当前服务状态 ---" : "--- Current service status ---");
2659
3190
  controlService("status", opts);
2660
3191
  return;
2661
3192
  }
2662
3193
  const action = positionals[0];
2663
- if (positionals.length > 1) fail(`多余的参数: ${positionals.slice(1).join(" ")}`);
3194
+ if (positionals.length > 1)
3195
+ fail(
3196
+ ZH ? `多余的参数: ${positionals.slice(1).join(" ")}` : `Unexpected argument: ${positionals.slice(1).join(" ")}`,
3197
+ );
2664
3198
  switch (action) {
2665
3199
  case "shortcut": {
2666
3200
  if (isWin) {
@@ -2670,7 +3204,9 @@ async function serverCmd(argv) {
2670
3204
  } else if (isLinux) {
2671
3205
  installLinuxShortcut(opts);
2672
3206
  } else {
2673
- fail(`不支持的系统服务平台: ${process.platform}`);
3207
+ fail(
3208
+ ZH ? `不支持的系统服务平台: ${process.platform}` : `Unsupported system service platform: ${process.platform}`,
3209
+ );
2674
3210
  }
2675
3211
  break;
2676
3212
  }
@@ -2682,7 +3218,9 @@ async function serverCmd(argv) {
2682
3218
  } else if (isWin) {
2683
3219
  installWindows(opts);
2684
3220
  } else {
2685
- fail(`不支持的系统服务平台: ${process.platform}`);
3221
+ fail(
3222
+ ZH ? `不支持的系统服务平台: ${process.platform}` : `Unsupported system service platform: ${process.platform}`,
3223
+ );
2686
3224
  }
2687
3225
  break;
2688
3226
  }
@@ -2694,7 +3232,9 @@ async function serverCmd(argv) {
2694
3232
  } else if (isWin) {
2695
3233
  uninstallWindows(opts);
2696
3234
  } else {
2697
- fail(`不支持的系统服务平台: ${process.platform}`);
3235
+ fail(
3236
+ ZH ? `不支持的系统服务平台: ${process.platform}` : `Unsupported system service platform: ${process.platform}`,
3237
+ );
2698
3238
  }
2699
3239
  break;
2700
3240
  }
@@ -2715,7 +3255,9 @@ async function serverCmd(argv) {
2715
3255
  break;
2716
3256
  default:
2717
3257
  fail(
2718
- `未知操作: ${action}(install / shortcut / uninstall / start / stop / restart / status / quiesce / unquiesce)`,
3258
+ ZH
3259
+ ? `未知操作: ${action}(install / shortcut / uninstall / start / stop / restart / status / quiesce / unquiesce)`
3260
+ : `Unknown operation: ${action} (install / shortcut / uninstall / start / stop / restart / status / quiesce / unquiesce)`,
2719
3261
  );
2720
3262
  }
2721
3263
  }
@@ -2766,7 +3308,10 @@ async function main() {
2766
3308
  console.log(HELP);
2767
3309
  return;
2768
3310
  }
2769
- if (positionals.length > 0) fail(`未知命令: ${positionals[0]}(--help 查看用法)`);
3311
+ if (positionals.length > 0)
3312
+ fail(
3313
+ ZH ? `未知命令: ${positionals[0]}(--help 查看用法)` : `Unknown command: ${positionals[0]} (--help for usage)`,
3314
+ );
2770
3315
  await startForeground(opts);
2771
3316
  }
2772
3317