pi-web-ui 0.8.5 → 0.8.7
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/dist/server/agent-service.js +76 -15
- package/dist/server/index.js +19 -2
- package/package.json +1 -1
- package/web/dist/assets/{index-Dmb4daY8.js → index-BPM4qIYE.js} +35 -35
- package/web/dist/assets/index-DicNqNt1.css +41 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Ton8iALZ.css +0 -41
|
@@ -955,18 +955,23 @@ export class ClientSession {
|
|
|
955
955
|
async checkUpdate() {
|
|
956
956
|
const current = ClientSession.currentAppVersion();
|
|
957
957
|
try {
|
|
958
|
-
|
|
958
|
+
// Fetch the full package doc (not /latest): it carries the per-version
|
|
959
|
+
// publish timestamps so the UI can hint when a version was JUST
|
|
960
|
+
// published and the registry/CDN caches may not have caught up yet.
|
|
961
|
+
const res = await fetch("https://registry.npmjs.org/pi-web-ui", {
|
|
959
962
|
signal: AbortSignal.timeout(8_000),
|
|
960
963
|
});
|
|
961
964
|
if (!res.ok)
|
|
962
965
|
throw new Error(`HTTP ${res.status}`);
|
|
963
966
|
const data = (await res.json());
|
|
964
|
-
const latest = data
|
|
967
|
+
const latest = data["dist-tags"]?.latest ?? null;
|
|
968
|
+
const latestPublishedAt = latest && data.time ? (data.time[latest] ?? null) : null;
|
|
965
969
|
const upToDate = latest === null || ClientSession.compareVersions(current, latest) >= 0;
|
|
966
970
|
this.emit({
|
|
967
971
|
type: "update_status",
|
|
968
972
|
current,
|
|
969
973
|
latest,
|
|
974
|
+
latestPublishedAt,
|
|
970
975
|
upToDate,
|
|
971
976
|
pendingRestart: this.pendingRestart,
|
|
972
977
|
});
|
|
@@ -976,12 +981,48 @@ export class ClientSession {
|
|
|
976
981
|
type: "update_status",
|
|
977
982
|
current,
|
|
978
983
|
latest: null,
|
|
984
|
+
latestPublishedAt: null,
|
|
979
985
|
upToDate: false,
|
|
980
986
|
pendingRestart: this.pendingRestart,
|
|
981
987
|
error: `检查更新失败:${err.message}`,
|
|
982
988
|
});
|
|
983
989
|
}
|
|
984
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* After `npm i -g`, confirm the on-disk package this process serves from
|
|
993
|
+
* actually changed to the new version and is complete. Windows npm updates
|
|
994
|
+
* can fail partway (locked files / Defender / npm rollback) and leave the
|
|
995
|
+
* global install without its bin links — restarting into that is a silent
|
|
996
|
+
* crash (web/dist missing + `pi-web-ui` no longer on PATH). Returns null
|
|
997
|
+
* when OK, else a human-readable problem description.
|
|
998
|
+
*/
|
|
999
|
+
static verifyGlobalInstall() {
|
|
1000
|
+
try {
|
|
1001
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1002
|
+
const pkgRoot = resolve(here, "..", "..");
|
|
1003
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
1004
|
+
if (!pkg.version || pkg.version === ClientSession.currentAppVersion()) {
|
|
1005
|
+
return `安装目录版本未变化(${pkg.version ?? "未知"})`;
|
|
1006
|
+
}
|
|
1007
|
+
if (!existsSync(join(pkgRoot, "web", "dist", "index.html"))) {
|
|
1008
|
+
return "web/dist/index.html 缺失(前端产物未安装完整)";
|
|
1009
|
+
}
|
|
1010
|
+
if (!existsSync(join(pkgRoot, "bin", "pi-web-ui.mjs"))) {
|
|
1011
|
+
return "bin/pi-web-ui.mjs 缺失";
|
|
1012
|
+
}
|
|
1013
|
+
if (process.platform === "win32") {
|
|
1014
|
+
const prefix = dirname(process.execPath);
|
|
1015
|
+
const hasShim = existsSync(join(prefix, "pi-web-ui.cmd")) ||
|
|
1016
|
+
existsSync(join(prefix, "pi-web-ui.ps1"));
|
|
1017
|
+
if (!hasShim)
|
|
1018
|
+
return "pi-web-ui 命令入口(bin 链接)未生成";
|
|
1019
|
+
}
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
catch (err) {
|
|
1023
|
+
return `读取安装目录失败:${err.message}`;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
985
1026
|
/** npm i -g pi-web-ui@latest — the new code only takes effect after a restart. */
|
|
986
1027
|
async updateApp() {
|
|
987
1028
|
try {
|
|
@@ -991,34 +1032,50 @@ export class ClientSession {
|
|
|
991
1032
|
text: "正在更新 pi-web-ui(npm i -g pi-web-ui@latest)…",
|
|
992
1033
|
});
|
|
993
1034
|
const { code, out } = await this.runAsync("npm", ["i", "-g", "pi-web-ui@latest"], 180_000);
|
|
994
|
-
if (code
|
|
995
|
-
this.pendingRestart = true;
|
|
1035
|
+
if (code !== 0) {
|
|
996
1036
|
this.emit({
|
|
997
1037
|
type: "update_result",
|
|
998
|
-
ok:
|
|
999
|
-
detail: out.slice(0, 400)
|
|
1038
|
+
ok: false,
|
|
1039
|
+
detail: `npm i 失败(${code ?? "timeout"}):${out.slice(0, 400)}`,
|
|
1000
1040
|
});
|
|
1001
|
-
const autoRestart = this.onUpdateReady?.() ?? false;
|
|
1002
1041
|
this.emit({
|
|
1003
1042
|
type: "notice",
|
|
1004
|
-
level: "
|
|
1005
|
-
text:
|
|
1006
|
-
? "✅ 已更新 pi-web-ui,正在自动重启…"
|
|
1007
|
-
: "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
|
|
1043
|
+
level: "error",
|
|
1044
|
+
text: `更新 pi-web-ui 失败(${code ?? "timeout"}):${out.slice(0, 300)}`,
|
|
1008
1045
|
});
|
|
1046
|
+
return;
|
|
1009
1047
|
}
|
|
1010
|
-
|
|
1048
|
+
// npm reported success, but on Windows the replacement can be partial
|
|
1049
|
+
// (locked files, rollback) — restarting into a broken install is a
|
|
1050
|
+
// crash with no hint. Verify before handing over.
|
|
1051
|
+
const problem = ClientSession.verifyGlobalInstall();
|
|
1052
|
+
if (problem) {
|
|
1011
1053
|
this.emit({
|
|
1012
1054
|
type: "update_result",
|
|
1013
1055
|
ok: false,
|
|
1014
|
-
detail: `npm i
|
|
1056
|
+
detail: `npm i 成功但安装不完整(${problem})。请手动执行 npm i -g pi-web-ui@latest 修复后再重启服务。`,
|
|
1015
1057
|
});
|
|
1016
1058
|
this.emit({
|
|
1017
1059
|
type: "notice",
|
|
1018
1060
|
level: "error",
|
|
1019
|
-
text:
|
|
1061
|
+
text: `更新未完整生效(${problem})。请手动执行 npm i -g pi-web-ui@latest 修复`,
|
|
1020
1062
|
});
|
|
1063
|
+
return;
|
|
1021
1064
|
}
|
|
1065
|
+
this.pendingRestart = true;
|
|
1066
|
+
this.emit({
|
|
1067
|
+
type: "update_result",
|
|
1068
|
+
ok: true,
|
|
1069
|
+
detail: out.slice(0, 400),
|
|
1070
|
+
});
|
|
1071
|
+
const autoRestart = this.onUpdateReady?.() ?? false;
|
|
1072
|
+
this.emit({
|
|
1073
|
+
type: "notice",
|
|
1074
|
+
level: "info",
|
|
1075
|
+
text: autoRestart
|
|
1076
|
+
? "✅ 已更新 pi-web-ui,正在自动重启…"
|
|
1077
|
+
: "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
|
|
1078
|
+
});
|
|
1022
1079
|
}
|
|
1023
1080
|
catch (err) {
|
|
1024
1081
|
this.emit({
|
|
@@ -1751,7 +1808,11 @@ export class ClientSession {
|
|
|
1751
1808
|
isStreaming,
|
|
1752
1809
|
});
|
|
1753
1810
|
}
|
|
1754
|
-
this.emit({
|
|
1811
|
+
this.emit({
|
|
1812
|
+
type: "conversations",
|
|
1813
|
+
conversations,
|
|
1814
|
+
activeId: this.activeId,
|
|
1815
|
+
});
|
|
1755
1816
|
}
|
|
1756
1817
|
/** List persisted sessions for this client, newest first. */
|
|
1757
1818
|
/** Push the persisted session list to the client (client-requested). */
|
package/dist/server/index.js
CHANGED
|
@@ -89,13 +89,31 @@ app.get("/api/file", async (req, res) => {
|
|
|
89
89
|
// from the repo root. In dev, Vite serves the UI on :5173 and proxies /ws.
|
|
90
90
|
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <pkg>/server
|
|
91
91
|
const pkgRoot = resolve(here, "..", "..");
|
|
92
|
+
/** Set in the env of the replacement child spawned by a self-update restart. */
|
|
93
|
+
const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
|
|
92
94
|
const webDist = join(pkgRoot, "web", "dist");
|
|
93
95
|
if (existsSync(webDist)) {
|
|
94
96
|
app.use(express.static(webDist));
|
|
95
97
|
app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
|
|
96
|
-
|
|
98
|
+
// Callback form: a failed stat here (npm i -g is mid-replacement of the
|
|
99
|
+
// package dir) responds 503 instead of crashing the request pipeline
|
|
100
|
+
// with an unhandled ENOENT stack trace.
|
|
101
|
+
res.sendFile(join(webDist, "index.html"), (err) => {
|
|
102
|
+
if (err && !res.headersSent) {
|
|
103
|
+
res.status(503).send("正在更新 pi-web-ui,请稍后刷新…");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
97
106
|
});
|
|
98
107
|
}
|
|
108
|
+
else if (process.env[RESTART_CHILD_ENV]) {
|
|
109
|
+
// Auto-restart replacement of a self-update whose npm install did not
|
|
110
|
+
// complete (Windows: locked files / rollback can leave the global package
|
|
111
|
+
// without web/dist). Fail loudly with a repair hint instead of serving a
|
|
112
|
+
// UI-less 404 with no explanation.
|
|
113
|
+
console.error("✖ 更新后的安装不完整(缺少 web/dist/index.html)。\n" +
|
|
114
|
+
" 请手动执行 npm i -g pi-web-ui@latest 修复后重新启动。");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
99
117
|
const httpServer = createServer(app);
|
|
100
118
|
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
101
119
|
// Heartbeat: lets clients detect half-open connections (server killed without
|
|
@@ -120,7 +138,6 @@ join(DATA_DIR, "client-state.json"));
|
|
|
120
138
|
// foreground runs get a replacement child that waits for our port to free.
|
|
121
139
|
// Docker containers can't self-restart (the orchestrator owns that), so they
|
|
122
140
|
// keep the manual-restart notice.
|
|
123
|
-
const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
|
|
124
141
|
function scheduleUpdateRestart() {
|
|
125
142
|
const isLaunchd = process.platform === "darwin" && process.ppid === 1;
|
|
126
143
|
const isSystemd = process.platform === "linux" && !!process.env.INVOCATION_ID;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|