pi-web-ui 0.51.0 → 0.53.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/README.md +1 -1
- package/bin/pi-web-ui.mjs +210 -180
- package/deploy/com.xingshuyin.pi-web-ui.plist +1 -1
- package/deploy/nginx-subpath.conf +1 -1
- package/deploy/pi-web-ui-task.xml +3 -20
- package/deploy/pi-web-ui.service +1 -1
- package/dist/server/agent-service.js +58 -5
- package/dist/server/index.js +2 -2
- package/package.json +2 -2
- package/web/dist/assets/{TerminalPanel-uB-DaBC5.js → TerminalPanel-CgkoEcJf.js} +1 -1
- package/web/dist/assets/{index-GrSZ6qyt.js → index-KGJNsUEy.js} +13 -13
- package/web/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ npm i -g --allow-scripts=node-pty,@google/genai,protobufjs pi-web-ui@latest
|
|
|
154
154
|
|
|
155
155
|
```bash
|
|
156
156
|
pi-web-ui # foreground, http://localhost:8787
|
|
157
|
-
|
|
157
|
+
PI_WEB_PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
**Stop**
|
package/bin/pi-web-ui.mjs
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
* - macOS → launchd 用户代理,label 默认 com.xingshuyin.pi-web-ui
|
|
18
18
|
* (--name 自定义时 com.<name>.server),无需 sudo
|
|
19
19
|
* - Linux → systemd 单元 <name>.service(/etc/systemd/system/,自动 sudo)
|
|
20
|
-
* - Windows →
|
|
21
|
-
*
|
|
20
|
+
* - Windows → 登录自启 Run 键(HKCU,无需管理员)+ wscript 隐藏启动(无黑窗);
|
|
21
|
+
* PowerShell 启动脚本 / VBS 启动器 / PID 文件生成在
|
|
22
22
|
* %APPDATA%\pi-web-ui\
|
|
23
23
|
*
|
|
24
|
-
* 环境变量(前台与系统服务均适用):
|
|
24
|
+
* 环境变量(前台与系统服务均适用):PI_WEB_PORT / PI_WEB_CWD / PI_WEB_DATA_DIR /
|
|
25
25
|
* PI_CODING_AGENT_DIR。
|
|
26
26
|
*/
|
|
27
|
-
import { spawnSync } from "node:child_process";
|
|
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
30
|
import {
|
|
@@ -77,15 +77,15 @@ const HELP = `pi-web-ui v${pkg.version} — web chat for the pi coding agent
|
|
|
77
77
|
pi-web-ui --version / --help
|
|
78
78
|
|
|
79
79
|
server 选项:
|
|
80
|
-
--port <n> 端口(默认 8787,或 $
|
|
81
|
-
--cwd <dir> 工作目录(默认 $PI_WEB_CWD
|
|
80
|
+
--port <n> 端口(默认 8787,或 $PI_WEB_PORT)
|
|
81
|
+
--cwd <dir> 工作目录(默认 $PI_WEB_CWD 或用户主目录;前台启动默认当前目录)
|
|
82
82
|
--data-dir <dir> 会话数据目录(默认 <cwd>/.pi-web)
|
|
83
83
|
--name <name> 服务名(默认 pi-web-ui;macOS 的 launchd label
|
|
84
84
|
为 com.xingshuyin.pi-web-ui,自定义名时为 com.<name>.server)
|
|
85
85
|
--print 只打印将生成的配置文件,不实际安装
|
|
86
86
|
|
|
87
|
-
平台: macOS → launchd 用户代理 · Linux → systemd · Windows →
|
|
88
|
-
(
|
|
87
|
+
平台: macOS → launchd 用户代理 · Linux → systemd · Windows → 登录自启 Run 键
|
|
88
|
+
(HKCU 写入无需管理员;wscript 隐藏启动无黑窗;stop 停止,uninstall 移除)
|
|
89
89
|
快捷方式: Windows → 桌面 .lnk · macOS → 桌面 .command 启动器 · Linux → 桌面 .desktop 图标
|
|
90
90
|
|
|
91
91
|
界面插件(安装到 <data-dir>/plugins/,服务运行中刷新浏览器即生效):
|
|
@@ -101,7 +101,7 @@ server 选项:
|
|
|
101
101
|
--force 目标已存在时覆盖
|
|
102
102
|
|
|
103
103
|
环境变量(前台与系统服务均适用):
|
|
104
|
-
|
|
104
|
+
PI_WEB_PORT / PI_WEB_CWD / PI_WEB_DATA_DIR / PI_CODING_AGENT_DIR
|
|
105
105
|
`;
|
|
106
106
|
|
|
107
107
|
/** Minimum Node required by the pi SDK (its dist uses `import … with { type: "json" }`). */
|
|
@@ -258,12 +258,10 @@ function openBrowserWhenUp(url) {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
async function startForeground(opts) {
|
|
261
|
-
if (opts.port) process.env.
|
|
261
|
+
if (opts.port) process.env.PI_WEB_PORT = opts.port;
|
|
262
262
|
if (opts.cwd) process.env.PI_WEB_CWD = resolve(opts.cwd);
|
|
263
263
|
if (opts.dataDir) process.env.PI_WEB_DATA_DIR = resolve(opts.dataDir);
|
|
264
|
-
const url = `http://localhost:${
|
|
265
|
-
opts.port ?? process.env.PORT ?? "8787",
|
|
266
|
-
)}`;
|
|
264
|
+
const url = `http://localhost:${effectivePort(opts)}`;
|
|
267
265
|
await import(pathToFileURL(SERVER_ENTRY).href);
|
|
268
266
|
if (!opts.noBrowser) openBrowserWhenUp(url);
|
|
269
267
|
}
|
|
@@ -307,7 +305,7 @@ function systemdUnitPath(name) {
|
|
|
307
305
|
return `/etc/systemd/system/${name}.service`;
|
|
308
306
|
}
|
|
309
307
|
|
|
310
|
-
/** Windows: per-user config dir (%APPDATA%\pi-web-ui) holding the
|
|
308
|
+
/** Windows: per-user config dir (%APPDATA%\pi-web-ui) holding the ps1/vbs launchers + pid file. */
|
|
311
309
|
function winServiceDir() {
|
|
312
310
|
return join(
|
|
313
311
|
process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
|
|
@@ -323,12 +321,17 @@ function winPs1Path(name) {
|
|
|
323
321
|
return join(winServiceDir(), `${name}.ps1`);
|
|
324
322
|
}
|
|
325
323
|
|
|
324
|
+
function winVbsPath(name) {
|
|
325
|
+
return join(winServiceDir(), `${name}.vbs`);
|
|
326
|
+
}
|
|
327
|
+
|
|
326
328
|
function winTaskXmlPath(name) {
|
|
327
329
|
return join(winServiceDir(), `${name}.xml`);
|
|
328
330
|
}
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
/** Windows log file (per service name — multiple services must not share one log). */
|
|
333
|
+
function winLogPath(name) {
|
|
334
|
+
return join(homedir(), name === "pi-web-ui" ? "pi-web-ui.log" : `pi-web-ui-${name}.log`);
|
|
332
335
|
}
|
|
333
336
|
|
|
334
337
|
/** True when a scheduled task with this name exists (schtasks exits 0). */
|
|
@@ -339,6 +342,38 @@ function winTaskExists(name) {
|
|
|
339
342
|
);
|
|
340
343
|
}
|
|
341
344
|
|
|
345
|
+
/** Windows: per-user autostart registry key (HKCU — writable without admin). */
|
|
346
|
+
function winRunKeyName() {
|
|
347
|
+
return "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** True when the per-user autostart Run key value exists. */
|
|
351
|
+
function winRunKeyInstalled(name) {
|
|
352
|
+
return (
|
|
353
|
+
spawnSync("reg", ["query", `HKCU\\${winRunKeyName()}`, "/v", name], {
|
|
354
|
+
stdio: "ignore",
|
|
355
|
+
}).status === 0
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Set the per-user autostart Run key value (no admin needed for HKCU). */
|
|
360
|
+
function winRunKeySet(name, value) {
|
|
361
|
+
run(
|
|
362
|
+
"reg",
|
|
363
|
+
["add", `HKCU\\${winRunKeyName()}`, "/v", name, "/t", "REG_SZ", "/d", value, "/f"],
|
|
364
|
+
{ silent: true },
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Remove the per-user autostart Run key value (missing key is a no-op). */
|
|
369
|
+
function winRunKeyDelete(name) {
|
|
370
|
+
run(
|
|
371
|
+
"reg",
|
|
372
|
+
["delete", `HKCU\\${winRunKeyName()}`, "/v", name, "/f"],
|
|
373
|
+
{ ignoreError: true, silent: true },
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
342
377
|
// ---------------------------------------------------------------------------
|
|
343
378
|
// 桌面快捷方式(server shortcut)
|
|
344
379
|
// ---------------------------------------------------------------------------
|
|
@@ -434,10 +469,10 @@ function shQuote(s) {
|
|
|
434
469
|
|
|
435
470
|
/**
|
|
436
471
|
* Windows shortcut launcher. Double-click the .lnk → this script runs hidden:
|
|
437
|
-
* server already up → just open the browser;
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
* uninstall` can terminate it too.
|
|
472
|
+
* server already up → just open the browser; autostart service installed
|
|
473
|
+
* (HKCU Run key + wscript/VBS launcher) → start it (manageable via `server
|
|
474
|
+
* stop`); otherwise run the server in the foreground of this hidden window and
|
|
475
|
+
* record its PID so `server stop` / `server uninstall` can terminate it too.
|
|
441
476
|
*/
|
|
442
477
|
function buildWinShortcutPs1(env, cwd, taskName, url, logPath, pidPath) {
|
|
443
478
|
const sets = Object.entries(env)
|
|
@@ -447,13 +482,13 @@ function buildWinShortcutPs1(env, cwd, taskName, url, logPath, pidPath) {
|
|
|
447
482
|
return [
|
|
448
483
|
"# Generated by: pi-web-ui server shortcut (rerun to change)",
|
|
449
484
|
"# Runs hidden from the desktop shortcut: if the server is already up it",
|
|
450
|
-
"# opens the browser;
|
|
451
|
-
"# the server runs in the foreground of this hidden window and
|
|
452
|
-
"# recorded so `server stop` / `server uninstall` can
|
|
485
|
+
"# opens the browser; the autostart service (if installed) is started;",
|
|
486
|
+
"# otherwise the server runs in the foreground of this hidden window and",
|
|
487
|
+
"# its PID is recorded so `server stop` / `server uninstall` can stop it.",
|
|
453
488
|
"$ErrorActionPreference = 'Continue'",
|
|
454
489
|
`$url = ${psQuote(url)}`,
|
|
455
490
|
`$health = ${psQuote(url + "/api/health")}`,
|
|
456
|
-
`$
|
|
491
|
+
`$svcName = ${psQuote(taskName)}`,
|
|
457
492
|
`$pidFile = ${psQuote(pidPath)}`,
|
|
458
493
|
`$log = ${psQuote(logPath)}`,
|
|
459
494
|
"",
|
|
@@ -465,9 +500,10 @@ function buildWinShortcutPs1(env, cwd, taskName, url, logPath, pidPath) {
|
|
|
465
500
|
"# 已在运行 → 直接打开浏览器",
|
|
466
501
|
"if (Test-Up) { Open-Browser; exit 0 }",
|
|
467
502
|
"",
|
|
468
|
-
"#
|
|
469
|
-
"
|
|
470
|
-
"
|
|
503
|
+
"# 已安装自启服务(HKCU Run 键 + wscript 隐藏启动)→ 走服务启动(server stop 可管理)",
|
|
504
|
+
"$vbs = Join-Path $env:APPDATA ('pi-web-ui\\' + $svcName + '.vbs')",
|
|
505
|
+
"if (Test-Path $vbs) {",
|
|
506
|
+
" wscript.exe $vbs",
|
|
471
507
|
" for ($i = 0; $i -lt 120; $i++) {",
|
|
472
508
|
" Start-Sleep -Milliseconds 250",
|
|
473
509
|
" if (Test-Up) { Open-Browser; exit 0 }",
|
|
@@ -496,13 +532,15 @@ function buildWinShortcutPs1(env, cwd, taskName, url, logPath, pidPath) {
|
|
|
496
532
|
|
|
497
533
|
|
|
498
534
|
/**
|
|
499
|
-
* Build the tiny VBS launcher
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
535
|
+
* Build the tiny VBS launcher that starts a .ps1 *without* creating any console
|
|
536
|
+
* host: wscript.exe has no console, and WScript.Shell.Run(…, 0, False) launches
|
|
537
|
+
* the child hidden and returns at once (0 = hidden window, False = don't wait).
|
|
538
|
+
* Used by both the desktop .lnk and the logon autostart service. Note:
|
|
539
|
+
* `powershell -WindowStyle Hidden` alone is unreliable when Windows itself
|
|
540
|
+
* spawns the process (Task Scheduler) — the console window can still show;
|
|
541
|
+
* this launcher never creates one at all.
|
|
504
542
|
*/
|
|
505
|
-
function
|
|
543
|
+
function buildWinHiddenVbs(ps1Path) {
|
|
506
544
|
const cmd = `${winPowershell()} -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${ps1Path}"`;
|
|
507
545
|
// VBScript 字符串没有 \" 转义(也没有 \uXXXX),内嵌引号必须写成 "";
|
|
508
546
|
// 不能用 JSON.stringify —— 它输出 \" 会在 VBScript 里提前结束字符串(语句未结束 800A0401),
|
|
@@ -534,7 +572,7 @@ function installWinShortcut(opts) {
|
|
|
534
572
|
cwd,
|
|
535
573
|
name,
|
|
536
574
|
url,
|
|
537
|
-
winLogPath(),
|
|
575
|
+
winLogPath(name),
|
|
538
576
|
winPidFilePath(name),
|
|
539
577
|
);
|
|
540
578
|
if (opts.print) {
|
|
@@ -545,7 +583,7 @@ function installWinShortcut(opts) {
|
|
|
545
583
|
writeFileSync(ps1Path, "\uFEFF" + ps1, "utf8"); // PS 5.1 需要 BOM
|
|
546
584
|
// wscript host + VBS launcher: no console window / taskbar black box on double-click.
|
|
547
585
|
const vbsPath = winShortcutVbsPath(name);
|
|
548
|
-
writeFileSync(vbsPath, "\uFEFF" +
|
|
586
|
+
writeFileSync(vbsPath, "\uFEFF" + buildWinHiddenVbs(ps1Path), "utf16le"); // wscript 只认 UTF-16/ANSI,UTF-8 BOM 会报“无效字符”,中文路径用 utf16le + BOM
|
|
549
587
|
// 把品牌图标准备好:复制到用户目录(.lnk 图标指向稳定路径)
|
|
550
588
|
if (existsSync(APP_ICO_SOURCE)) {
|
|
551
589
|
copyFileSync(APP_ICO_SOURCE, winIcoPath());
|
|
@@ -768,85 +806,38 @@ function psQuote(s) {
|
|
|
768
806
|
}
|
|
769
807
|
|
|
770
808
|
/**
|
|
771
|
-
* Build the PowerShell launcher the
|
|
772
|
-
*
|
|
773
|
-
* hidden
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
809
|
+
* Build the PowerShell launcher the autostart service runs. Windows never sees
|
|
810
|
+
* a console from it: the HKCU Run key launches wscript.exe → VBS → powershell
|
|
811
|
+
* (hidden), so no black box can appear. The script sets the env, cd's to the
|
|
812
|
+
* workspace, records its PID to <name>.pid (for `server stop`), then runs node
|
|
813
|
+
* inside a watchdog loop — if node exits, it restarts after 10s (same
|
|
814
|
+
* philosophy as launchd KeepAlive / systemd Restart=always). `server stop`
|
|
815
|
+
* force-kills the recorded PID tree, which takes the loop down with it.
|
|
778
816
|
*/
|
|
779
|
-
function buildWinStartPs1(env, cwd, logPath) {
|
|
817
|
+
function buildWinStartPs1(env, cwd, logPath, pidPath) {
|
|
780
818
|
const sets = Object.entries(env)
|
|
781
819
|
.map(([k, v]) => `$env:${k} = ${psQuote(v)}`)
|
|
782
820
|
.join("\r\n");
|
|
783
821
|
return [
|
|
784
822
|
"# Generated by: pi-web-ui server install (rerun to change)",
|
|
785
|
-
"#
|
|
823
|
+
"# Runs the server with no console window (wscript+VBS launcher) and",
|
|
824
|
+
"# restarts it if it crashes (watchdog, like launchd/systemd).",
|
|
786
825
|
sets,
|
|
787
826
|
`Set-Location ${psQuote(cwd)}`,
|
|
788
|
-
|
|
827
|
+
`$PID | Out-File -Encoding ascii ${psQuote(pidPath)}`,
|
|
828
|
+
"try {",
|
|
829
|
+
" while ($true) {",
|
|
830
|
+
` & ${psQuote(realNode())} ${psQuote(SERVER_ENTRY)} *>> ${psQuote(logPath)}`,
|
|
831
|
+
" Start-Sleep 10",
|
|
832
|
+
" }",
|
|
833
|
+
"} finally {",
|
|
834
|
+
` Remove-Item ${psQuote(pidPath)} -ErrorAction SilentlyContinue`,
|
|
835
|
+
"}",
|
|
789
836
|
"",
|
|
790
837
|
].join("\r\n");
|
|
791
838
|
}
|
|
792
839
|
|
|
793
|
-
|
|
794
|
-
* Build the Task Scheduler XML for a user task: LogonTrigger (starts at logon,
|
|
795
|
-
* like a launchd agent — no admin needed), InteractiveToken, auto-restart on
|
|
796
|
-
* failure. The task runs the PowerShell launcher via
|
|
797
|
-
* powershell.exe -WindowStyle Hidden, so no console window ever appears.
|
|
798
|
-
*/
|
|
799
|
-
function buildWinTaskXml(ps1Path, cwd) {
|
|
800
|
-
const powershell = winPowershell();
|
|
801
|
-
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
802
|
-
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
803
|
-
<RegistrationInfo>
|
|
804
|
-
<Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
|
|
805
|
-
</RegistrationInfo>
|
|
806
|
-
<Triggers>
|
|
807
|
-
<LogonTrigger>
|
|
808
|
-
<Enabled>true</Enabled>
|
|
809
|
-
</LogonTrigger>
|
|
810
|
-
</Triggers>
|
|
811
|
-
<Principals>
|
|
812
|
-
<Principal id="Author">
|
|
813
|
-
<LogonType>InteractiveToken</LogonType>
|
|
814
|
-
<RunLevel>LeastPrivilege</RunLevel>
|
|
815
|
-
</Principal>
|
|
816
|
-
</Principals>
|
|
817
|
-
<Settings>
|
|
818
|
-
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
819
|
-
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
820
|
-
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
821
|
-
<AllowHardTerminate>true</AllowHardTerminate>
|
|
822
|
-
<StartWhenAvailable>false</StartWhenAvailable>
|
|
823
|
-
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
824
|
-
<IdleSettings>
|
|
825
|
-
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
826
|
-
<RestartOnIdle>false</RestartOnIdle>
|
|
827
|
-
</IdleSettings>
|
|
828
|
-
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
829
|
-
<Enabled>true</Enabled>
|
|
830
|
-
<Hidden>false</Hidden>
|
|
831
|
-
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
832
|
-
<WakeToRun>false</WakeToRun>
|
|
833
|
-
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
834
|
-
<Priority>7</Priority>
|
|
835
|
-
<RestartOnFailure>
|
|
836
|
-
<Interval>PT1M</Interval>
|
|
837
|
-
<Count>3</Count>
|
|
838
|
-
</RestartOnFailure>
|
|
839
|
-
</Settings>
|
|
840
|
-
<Actions Context="Author">
|
|
841
|
-
<Exec>
|
|
842
|
-
<Command>${esc(powershell)}</Command>
|
|
843
|
-
<Arguments>-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${esc(ps1Path)}"</Arguments>
|
|
844
|
-
<WorkingDirectory>${esc(cwd)}</WorkingDirectory>
|
|
845
|
-
</Exec>
|
|
846
|
-
</Actions>
|
|
847
|
-
</Task>
|
|
848
|
-
`;
|
|
849
|
-
}
|
|
840
|
+
|
|
850
841
|
|
|
851
842
|
function esc(s) {
|
|
852
843
|
return s
|
|
@@ -856,6 +847,35 @@ function esc(s) {
|
|
|
856
847
|
.replace(/"/g, """);
|
|
857
848
|
}
|
|
858
849
|
|
|
850
|
+
/** Start the autostart service now: wscript runs the VBS hidden and exits at once. */
|
|
851
|
+
function launchWinService(vbsPath) {
|
|
852
|
+
const child = spawn(winWscript(), [vbsPath], {
|
|
853
|
+
detached: true,
|
|
854
|
+
stdio: "ignore",
|
|
855
|
+
windowsHide: true,
|
|
856
|
+
});
|
|
857
|
+
child.unref();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Kill a running Windows instance via its PID file (whole tree). */
|
|
861
|
+
function stopWinInstance(name) {
|
|
862
|
+
const pid = winReadPid(name);
|
|
863
|
+
if (pid) {
|
|
864
|
+
if (pidAlive(pid)) {
|
|
865
|
+
run("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
866
|
+
ignoreError: true,
|
|
867
|
+
silent: true,
|
|
868
|
+
});
|
|
869
|
+
// taskkill /F 异步生效:等到进程树真正退出(restart 需要避免端口竞争)
|
|
870
|
+
const deadline = Date.now() + 5000;
|
|
871
|
+
while (Date.now() < deadline && pidAlive(pid)) {
|
|
872
|
+
spawnSync("ping", ["-n", "2", "127.0.0.1"], { stdio: "ignore" });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
rmSync(winPidFilePath(name), { force: true });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
859
879
|
/** Build the launchd plist XML. */
|
|
860
880
|
function buildPlist(label, cwd, env) {
|
|
861
881
|
const entries = Object.entries(env)
|
|
@@ -936,14 +956,21 @@ function ensureRootForSystemctl() {
|
|
|
936
956
|
process.exit(res.status ?? 1);
|
|
937
957
|
}
|
|
938
958
|
|
|
959
|
+
/** Resolve the effective HTTP port: --port > $PI_WEB_PORT > legacy $PORT > 8787. */
|
|
960
|
+
function effectivePort(opts) {
|
|
961
|
+
return String(opts.port ?? process.env.PI_WEB_PORT ?? process.env.PORT ?? "8787");
|
|
962
|
+
}
|
|
963
|
+
|
|
939
964
|
/** Shared option normalization for install. */
|
|
940
965
|
function serviceOptions(opts) {
|
|
941
966
|
const name = opts.name ?? "pi-web-ui";
|
|
942
|
-
const port =
|
|
967
|
+
const port = effectivePort(opts);
|
|
943
968
|
if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
|
|
944
969
|
fail(`无效端口: ${port}`);
|
|
945
970
|
}
|
|
946
|
-
|
|
971
|
+
// 服务默认以用户主目录为工作目录:安装命令的当前目录不可靠(例如 Windows 提权提示符
|
|
972
|
+
// 默认在 C:\WINDOWS\system32),主目录跨平台可预期;前台启动仍默认当前目录。
|
|
973
|
+
const cwd = resolve(opts.cwd ?? process.env.PI_WEB_CWD ?? homedir());
|
|
947
974
|
if (!existsSync(cwd)) fail(`工作目录不存在: ${cwd}`);
|
|
948
975
|
let dataDir;
|
|
949
976
|
if (opts.dataDir) {
|
|
@@ -956,7 +983,7 @@ function serviceOptions(opts) {
|
|
|
956
983
|
|
|
957
984
|
function serviceEnv(port, cwd, dataDir) {
|
|
958
985
|
const env = {
|
|
959
|
-
|
|
986
|
+
PI_WEB_PORT: port,
|
|
960
987
|
PI_WEB_CWD: cwd,
|
|
961
988
|
};
|
|
962
989
|
// Interactive Windows tasks inherit the user's PATH; only systemd/launchd
|
|
@@ -1051,59 +1078,65 @@ function installWindows(opts) {
|
|
|
1051
1078
|
const { name, port, cwd, dataDir } = serviceOptions(opts);
|
|
1052
1079
|
const env = serviceEnv(port, cwd, dataDir);
|
|
1053
1080
|
const ps1Path = winPs1Path(name);
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
const
|
|
1081
|
+
const vbsPath = winVbsPath(name);
|
|
1082
|
+
const pidPath = winPidFilePath(name);
|
|
1083
|
+
const ps1 = buildWinStartPs1(env, cwd, winLogPath(name), pidPath);
|
|
1084
|
+
const vbs = buildWinHiddenVbs(ps1Path);
|
|
1085
|
+
// HKCU Run 键值:wscript.exe 以隐藏方式启动 VBS(无控制台,登录后自启)
|
|
1086
|
+
const runValue = `"${winWscript()}" "${vbsPath}"`;
|
|
1057
1087
|
if (opts.print) {
|
|
1058
1088
|
console.log(`# ${ps1Path}\n${ps1}`);
|
|
1059
|
-
console.log(`# ${
|
|
1089
|
+
console.log(`# ${vbsPath}\n${vbs}`);
|
|
1090
|
+
console.log(`# 登录自启(HKCU Run 键,无需管理员)`);
|
|
1091
|
+
console.log(` reg add "HKCU\\${winRunKeyName()}" /v ${name} /t REG_SZ /d "${runValue}" /f`);
|
|
1060
1092
|
return;
|
|
1061
1093
|
}
|
|
1062
1094
|
mkdirSync(dirname(ps1Path), { recursive: true });
|
|
1063
1095
|
// UTF-8 with BOM: Windows PowerShell 5.1 misreads BOM-less UTF-8 as ANSI.
|
|
1064
1096
|
writeFileSync(ps1Path, "\uFEFF" + ps1, "utf8");
|
|
1065
|
-
//
|
|
1097
|
+
// wscript host + VBS launcher: no console window / taskbar black box ever.
|
|
1098
|
+
writeFileSync(vbsPath, "\uFEFF" + vbs, "utf16le"); // wscript 只认 UTF-16/ANSI,UTF-8 BOM 会报“无效字符”
|
|
1099
|
+
// 迁移:移除旧版 .cmd 包装与历史计划任务(普通用户下 schtasks 无法创建,改为 Run 键)。
|
|
1066
1100
|
if (existsSync(winCmdPath(name))) rmSync(winCmdPath(name));
|
|
1067
|
-
// schtasks /Create /XML requires a UTF-16 file (with BOM).
|
|
1068
|
-
writeFileSync(xmlPath, "\uFEFF" + xml, "utf16le");
|
|
1069
1101
|
if (winTaskExists(name)) {
|
|
1102
|
+
run("schtasks", ["/End", "/TN", name], {
|
|
1103
|
+
ignoreError: true,
|
|
1104
|
+
silent: true,
|
|
1105
|
+
});
|
|
1106
|
+
// /End 异步生效:稍候再删任务,避免新实例与旧实例端口竞争
|
|
1107
|
+
spawnSync("ping", ["-n", "2", "127.0.0.1"], { stdio: "ignore" });
|
|
1070
1108
|
run("schtasks", ["/Delete", "/TN", name, "/F"], {
|
|
1071
1109
|
ignoreError: true,
|
|
1072
1110
|
silent: true,
|
|
1073
1111
|
});
|
|
1074
1112
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
console.log(`✅
|
|
1113
|
+
winRunKeySet(name, runValue);
|
|
1114
|
+
launchWinService(vbsPath);
|
|
1115
|
+
console.log(`✅ 已安装并启动 ${name}(登录自启 · HKCU Run 键 · 无需管理员)`);
|
|
1116
|
+
console.log(` 窗口 : wscript 隐藏启动,无黑窗`);
|
|
1078
1117
|
console.log(` 端口 : ${port}`);
|
|
1079
1118
|
console.log(` 目录 : ${cwd}`);
|
|
1080
1119
|
console.log(` 访问 : http://localhost:${port}`);
|
|
1081
|
-
console.log(` 日志 : ${winLogPath()}`);
|
|
1082
|
-
console.log(
|
|
1083
|
-
` 说明 : 登录后自启(与 launchd 用户代理一致);stop 停止,uninstall 移除`,
|
|
1084
|
-
);
|
|
1120
|
+
console.log(` 日志 : ${winLogPath(name)}`);
|
|
1121
|
+
console.log(` 说明 : 崩溃后 10 秒自动重启(看门狗);stop 停止,uninstall 移除`);
|
|
1085
1122
|
console.log(` 管理 : pi-web-ui server status|restart|stop|uninstall`);
|
|
1086
1123
|
console.log(` 提示 : pi-web-ui server shortcut 可在桌面创建「一键启动」图标`);
|
|
1087
1124
|
}
|
|
1088
1125
|
|
|
1089
1126
|
function uninstallWindows(opts) {
|
|
1090
1127
|
const name = opts.name ?? "pi-web-ui";
|
|
1128
|
+
winRunKeyDelete(name);
|
|
1129
|
+
// 历史计划任务(旧版本 install 可能注册过)
|
|
1091
1130
|
if (winTaskExists(name)) {
|
|
1092
1131
|
run("schtasks", ["/Delete", "/TN", name, "/F"], { ignoreError: true });
|
|
1093
1132
|
}
|
|
1094
|
-
|
|
1133
|
+
// 运行中的实例(服务或快捷方式启动,均记录 PID 文件)
|
|
1134
|
+
stopWinInstance(name);
|
|
1135
|
+
for (const f of [winCmdPath(name), winPs1Path(name), winVbsPath(name), winTaskXmlPath(name)]) {
|
|
1095
1136
|
if (existsSync(f)) rmSync(f);
|
|
1096
1137
|
}
|
|
1097
|
-
// 快捷方式启动的隐藏实例
|
|
1098
|
-
const pid = winReadPid(name);
|
|
1099
|
-
if (pid && pidAlive(pid)) {
|
|
1100
|
-
run("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
1101
|
-
ignoreError: true,
|
|
1102
|
-
silent: true,
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
1138
|
removeShortcut(name);
|
|
1106
|
-
console.log(`🗑 已卸载 ${name}
|
|
1139
|
+
console.log(`🗑 已卸载 ${name}(登录自启已移除,不再开机自启)`);
|
|
1107
1140
|
console.log(`🗑 已移除桌面快捷方式`);
|
|
1108
1141
|
}
|
|
1109
1142
|
|
|
@@ -1123,7 +1156,7 @@ function controlPath(opts) {
|
|
|
1123
1156
|
? resolve(process.env.PI_WEB_DATA_DIR)
|
|
1124
1157
|
: join(homedir(), ".pi-web");
|
|
1125
1158
|
return isWin
|
|
1126
|
-
? `\\\\.\\pipe\\pi-web-ui-${
|
|
1159
|
+
? `\\\\.\\pipe\\pi-web-ui-${effectivePort(opts)}`
|
|
1127
1160
|
: join(dir, "pi-web-ui.sock");
|
|
1128
1161
|
}
|
|
1129
1162
|
|
|
@@ -1259,81 +1292,78 @@ function controlService(action, opts) {
|
|
|
1259
1292
|
}
|
|
1260
1293
|
|
|
1261
1294
|
if (isWin) {
|
|
1262
|
-
const
|
|
1295
|
+
const installed = winRunKeyInstalled(name);
|
|
1296
|
+
const legacy = !installed && winTaskExists(name); // 旧版计划任务安装(未迁移)
|
|
1263
1297
|
|
|
1264
1298
|
if (action === "status") {
|
|
1265
1299
|
const pid = winReadPid(name);
|
|
1266
1300
|
const instAlive = pid && pidAlive(pid);
|
|
1267
|
-
if (
|
|
1268
|
-
console.log(`${name}:
|
|
1269
|
-
|
|
1301
|
+
if (legacy) {
|
|
1302
|
+
console.log(`${name}: 旧版计划任务安装(未迁移)`);
|
|
1303
|
+
console.log(` 提示 : 重新执行 server install 可迁移到登录自启模式(删除任务,无需管理员)`);
|
|
1304
|
+
if (instAlive) console.log(` 实例 : 运行中 (PID ${pid})`);
|
|
1270
1305
|
return;
|
|
1271
1306
|
}
|
|
1272
|
-
|
|
1273
|
-
// unlike `schtasks /Query` tables on localized Windows.
|
|
1274
|
-
const ps = spawnSync(
|
|
1275
|
-
"powershell.exe",
|
|
1276
|
-
[
|
|
1277
|
-
"-NoProfile",
|
|
1278
|
-
"-NonInteractive",
|
|
1279
|
-
"-Command",
|
|
1280
|
-
"$t=Get-ScheduledTask -TaskName '" +
|
|
1281
|
-
name +
|
|
1282
|
-
"' -ErrorAction SilentlyContinue;" +
|
|
1283
|
-
"if(!$t){'NOT_INSTALLED';exit}" +
|
|
1284
|
-
"$i=$t|Get-ScheduledTaskInfo;" +
|
|
1285
|
-
"'State: '+$t.State;" +
|
|
1286
|
-
"'LastRunTime: '+$i.LastRunTime;" +
|
|
1287
|
-
"'LastTaskResult: '+$i.LastTaskResult",
|
|
1288
|
-
],
|
|
1289
|
-
{ encoding: "utf8" },
|
|
1290
|
-
);
|
|
1291
|
-
if (ps.status !== 0 || (ps.stdout ?? "").includes("NOT_INSTALLED")) {
|
|
1307
|
+
if (!installed) {
|
|
1292
1308
|
console.log(`${name}: 未安装(运行 pi-web-ui server install 安装)`);
|
|
1309
|
+
if (instAlive) console.log(` 快捷方式实例 : 运行中 (PID ${pid})`);
|
|
1293
1310
|
return;
|
|
1294
1311
|
}
|
|
1295
|
-
console.log(`${name}:
|
|
1296
|
-
if (
|
|
1297
|
-
|
|
1298
|
-
|
|
1312
|
+
console.log(`${name}: 已安装(登录自启 · HKCU Run 键 · wscript 隐藏启动,无黑窗)`);
|
|
1313
|
+
if (instAlive) {
|
|
1314
|
+
console.log(` 运行状态 : 运行中 (PID ${pid})`);
|
|
1315
|
+
console.log(` 日志 : ${winLogPath(name)}`);
|
|
1316
|
+
} else {
|
|
1317
|
+
console.log(` 运行状态 : 未运行(pi-web-ui server start 启动)`);
|
|
1299
1318
|
}
|
|
1300
1319
|
return;
|
|
1301
1320
|
}
|
|
1302
1321
|
|
|
1303
1322
|
if (action === "start") {
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1323
|
+
if (legacy) {
|
|
1324
|
+
run("schtasks", ["/Run", "/TN", name]);
|
|
1325
|
+
console.log(`✅ 已启动 ${name}(旧版计划任务,建议重新 server install 迁移)`);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (!installed) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
|
|
1329
|
+
const pid = winReadPid(name);
|
|
1330
|
+
if (pid && pidAlive(pid)) {
|
|
1331
|
+
console.log(`✅ ${name} 已在运行 (PID ${pid})`);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
launchWinService(winVbsPath(name));
|
|
1306
1335
|
console.log(`✅ 已启动 ${name}`);
|
|
1307
1336
|
return;
|
|
1308
1337
|
}
|
|
1309
1338
|
|
|
1310
1339
|
if (action === "restart") {
|
|
1311
|
-
if (
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1340
|
+
if (legacy) {
|
|
1341
|
+
run("schtasks", ["/End", "/TN", name], {
|
|
1342
|
+
ignoreError: true,
|
|
1343
|
+
silent: true,
|
|
1344
|
+
});
|
|
1345
|
+
run("schtasks", ["/Run", "/TN", name]);
|
|
1346
|
+
console.log(`✅ 已重启 ${name}(旧版计划任务,建议重新 server install 迁移)`);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (!installed) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
|
|
1350
|
+
stopWinInstance(name);
|
|
1351
|
+
launchWinService(winVbsPath(name));
|
|
1317
1352
|
console.log(`✅ 已重启 ${name}`);
|
|
1318
1353
|
return;
|
|
1319
1354
|
}
|
|
1320
1355
|
|
|
1321
1356
|
if (action === "stop") {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
ignoreError: true,
|
|
1331
|
-
silent: true,
|
|
1332
|
-
});
|
|
1333
|
-
console.log(`⏹ 已停止快捷方式实例 (PID ${pid})`);
|
|
1334
|
-
}
|
|
1335
|
-
rmSync(winPidFilePath(name), { force: true });
|
|
1357
|
+
if (legacy) {
|
|
1358
|
+
run("schtasks", ["/End", "/TN", name], {
|
|
1359
|
+
ignoreError: true,
|
|
1360
|
+
silent: true,
|
|
1361
|
+
});
|
|
1362
|
+
stopWinInstance(name);
|
|
1363
|
+
console.log(`⏹ 已停止 ${name}(旧版计划任务;重新 server install 可迁移到登录自启)`);
|
|
1364
|
+
return;
|
|
1336
1365
|
}
|
|
1366
|
+
stopWinInstance(name);
|
|
1337
1367
|
console.log(`⏹ 已停止 ${name}(自启保留;uninstall 移除)`);
|
|
1338
1368
|
return;
|
|
1339
1369
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pi-web-ui behind nginx at a sub-path: http://<host>:83/pi/
|
|
2
|
-
# Backend app: http://127.0.0.1:8787 (default
|
|
2
|
+
# Backend app: http://127.0.0.1:8787 (default PI_WEB_PORT env)
|
|
3
3
|
#
|
|
4
4
|
# IMPORTANT: pi-web-ui 0.23+ checks the WebSocket Origin against the request
|
|
5
5
|
# Host (hostname AND port). Every proxied location MUST forward the original
|