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