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.
- package/CHANGELOG.md +62 -0
- package/README.md +5 -6
- package/README.zh-CN.md +4 -5
- package/bin/pi-web-ui.mjs +751 -206
- package/dist/server/agent-service.js +1869 -85
- package/dist/server/approval-rules.js +550 -0
- package/dist/server/attachment-store.js +112 -0
- package/dist/server/attachments.js +62 -171
- package/dist/server/client-state.js +4 -0
- package/dist/server/compact-context-tool.js +125 -0
- package/dist/server/context-budget.js +317 -0
- package/dist/server/dangling-tools.js +162 -0
- package/dist/server/dsh/dsh-agent-service.js +50 -60
- package/dist/server/edit-soft-tool.js +70 -5
- package/dist/server/eval-tool.js +535 -0
- package/dist/server/files-service.js +35 -7
- package/dist/server/goal-service.js +240 -68
- package/dist/server/hashline-engine.js +665 -0
- package/dist/server/host-metrics.js +26 -2
- package/dist/server/http-proxy.js +11 -1
- package/dist/server/index.js +157 -9
- package/dist/server/lsp-tool.js +679 -0
- package/dist/server/model-admin.js +20 -0
- package/dist/server/office-parse.js +309 -0
- package/dist/server/patch-tool.js +91 -0
- package/dist/server/plan-manager.js +104 -0
- package/dist/server/plugin-api-catalog.js +296 -0
- package/dist/server/plugin-install-spec.js +196 -0
- package/dist/server/plugin-installer.js +73 -0
- package/dist/server/plugin-manifest-validate.js +305 -0
- package/dist/server/plugin-project.js +47 -11
- package/dist/server/plugin-tool-guard.js +120 -0
- package/dist/server/plugins.js +745 -161
- package/dist/server/process-utils.js +12 -6
- package/dist/server/scheduler-tasks.js +6 -0
- package/dist/server/serialize.js +13 -3
- package/dist/server/settings-service.js +108 -1
- package/dist/server/subagents.js +41 -0
- package/dist/server/terminals.js +69 -14
- package/dist/server/tool-approval.js +84 -0
- package/dist/server/tool-manager.js +214 -6
- package/dist/server/workspace-snapshot.js +113 -0
- package/package.json +3 -2
- package/plugin-sdk/README.md +25 -0
- package/plugin-sdk/index.d.ts +31 -38
- package/plugin-sdk/index.mjs +35 -19
- package/plugins/catalog.json +20 -0
- package/themes/ayu-light.css +6 -6
- package/themes/catppuccin-latte.css +6 -6
- package/themes/claude-code-dark.css +144 -0
- package/themes/codex.css +6 -6
- package/themes/dark-teal.css +20 -6
- package/themes/everforest-light.css +6 -6
- package/themes/geist.css +6 -6
- package/themes/gruvbox-light.css +6 -6
- package/themes/kanagawa-lotus.css +6 -6
- package/themes/rose-pine-dawn.css +6 -6
- package/themes/solarized-light.css +6 -6
- package/themes/vs-code-dark.css +146 -0
- package/web/dist/assets/{TerminalPanel-B-CMQuzm.js → TerminalPanel-DlGBF2MD.js} +1 -1
- package/web/dist/assets/index-BXnCQL9p.js +364 -0
- package/web/dist/assets/index-RpIGFyi_.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-18jK6al2.js +0 -364
- 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
|
-
|
|
355
|
-
"
|
|
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(
|
|
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")
|
|
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(
|
|
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(
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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(
|
|
860
|
-
|
|
861
|
-
|
|
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")
|
|
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(
|
|
1146
|
-
|
|
1147
|
-
|
|
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)
|
|
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(
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
console.log(`
|
|
1190
|
-
console.log(`
|
|
1191
|
-
console.log(`
|
|
1192
|
-
console.log(`
|
|
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(
|
|
1206
|
-
|
|
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(
|
|
1220
|
-
|
|
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(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
console.log(`
|
|
1267
|
-
console.log(`
|
|
1268
|
-
console.log(`
|
|
1269
|
-
console.log(`
|
|
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(
|
|
1286
|
-
|
|
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(
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
?
|
|
1494
|
+
? ZH
|
|
1495
|
+
? "⏸ 已进入排空模式(quiesce):拒绝新的对话/消息/编辑,存量运行继续跑完。\n" +
|
|
1373
1496
|
" 跑完后用 pi-web-ui server unquiesce 恢复。"
|
|
1374
|
-
|
|
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(
|
|
1519
|
+
console.log(
|
|
1520
|
+
ZH ? `${label}: ${state}(已加载,开机自启中)` : `${label}: ${state} (loaded, auto-start enabled)`,
|
|
1521
|
+
);
|
|
1393
1522
|
} else {
|
|
1394
|
-
console.log(
|
|
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(
|
|
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())
|
|
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(
|
|
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(
|
|
1452
|
-
|
|
1453
|
-
|
|
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(
|
|
1458
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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)
|
|
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 === "..")
|
|
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(
|
|
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(
|
|
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
|
-
?
|
|
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)
|
|
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)
|
|
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")))
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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)
|
|
1768
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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))
|
|
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)
|
|
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(
|
|
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(
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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))
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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())
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
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))
|
|
2425
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
2462
|
-
|
|
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))
|
|
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))
|
|
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)
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
2507
|
-
|
|
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(
|
|
2510
|
-
|
|
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(
|
|
2513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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))
|
|
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(
|
|
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))
|
|
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)
|
|
2587
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
|