@liguoshuai/pi-web-chat 1.7.4 → 1.7.5
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 +8 -0
- package/bin/pi-web-chat.js +34 -6
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHANGELOG.md +13 -0
- package/docs/ISSUES.md +23 -0
- package/package.json +1 -1
- package/public/app.js +6 -7
- package/server.js +1 -1
package/README.md
CHANGED
|
@@ -154,6 +154,10 @@ pi-web-chat/
|
|
|
154
154
|
│ ├── index.html 单页 UI
|
|
155
155
|
│ ├── app.js 前端逻辑与状态机
|
|
156
156
|
│ └── style.css ChatGPT/Gemini 风格样式
|
|
157
|
+
├── scripts/ Systemd 服务安装与管理脚本
|
|
158
|
+
│ ├── pi-web-chat.service Systemd user unit 模板
|
|
159
|
+
│ ├── install-service.sh 一键安装与启动脚本
|
|
160
|
+
│ └── uninstall-service.sh 一键卸载脚本
|
|
157
161
|
└── docs/ 项目文档库
|
|
158
162
|
├── ARCHITECTURE.md 架构设计文档
|
|
159
163
|
├── DESIGN.md 详细设计与决策文档
|
|
@@ -210,6 +214,10 @@ journalctl --user -u pi-web-chat -f
|
|
|
210
214
|
|
|
211
215
|
卸载服务:
|
|
212
216
|
```bash
|
|
217
|
+
# 使用一键卸载脚本
|
|
218
|
+
./scripts/uninstall-service.sh
|
|
219
|
+
|
|
220
|
+
# 或手动清理:
|
|
213
221
|
systemctl --user disable --now pi-web-chat
|
|
214
222
|
rm ~/.config/systemd/user/pi-web-chat.service
|
|
215
223
|
systemctl --user daemon-reload
|
package/bin/pi-web-chat.js
CHANGED
|
@@ -37,15 +37,43 @@ Examples:
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function parseArgs(argv) {
|
|
40
|
-
const opts = { port: process.env.PORT || 3000, cwd: process.env.HOME };
|
|
40
|
+
const opts = { port: process.env.PORT || 3000, cwd: process.env.HOME || os.homedir() };
|
|
41
41
|
for (let i = 0; i < argv.length; i++) {
|
|
42
42
|
const a = argv[i];
|
|
43
43
|
if (a === "-h" || a === "--help") { printHelp(); process.exit(0); }
|
|
44
|
-
if (a === "-p" || a === "--port")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if (a === "-p" || a === "--port") {
|
|
45
|
+
const val = argv[++i];
|
|
46
|
+
if (!val || isNaN(Number(val))) {
|
|
47
|
+
console.error(`Error: --port requires a valid number`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
opts.port = Number(val);
|
|
51
|
+
} else if (a.startsWith("--port=")) {
|
|
52
|
+
const val = a.split("=")[1];
|
|
53
|
+
if (!val || isNaN(Number(val))) {
|
|
54
|
+
console.error(`Error: --port requires a valid number`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
opts.port = Number(val);
|
|
58
|
+
} else if (a === "-c" || a === "--cwd") {
|
|
59
|
+
const val = argv[++i];
|
|
60
|
+
if (!val) {
|
|
61
|
+
console.error(`Error: --cwd requires a path`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
opts.cwd = val;
|
|
65
|
+
} else if (a.startsWith("--cwd=")) {
|
|
66
|
+
const val = a.split("=")[1];
|
|
67
|
+
if (!val) {
|
|
68
|
+
console.error(`Error: --cwd requires a path`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
opts.cwd = val;
|
|
72
|
+
} else {
|
|
73
|
+
console.error(`Unknown option: ${a}`);
|
|
74
|
+
printHelp();
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
49
77
|
}
|
|
50
78
|
return opts;
|
|
51
79
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.7.5] - 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **初始化 CWD 配置加载与 WebSocket 建立竞态修复 (Async CWD Init Race Condition)**:
|
|
14
|
+
- 前端 `init()` 声明为异步函数,优先 `await loadServerConfig()` 完成服务器配置(`serverCwd` / `homeDir`)与本地缓存读取后,再发起 WebSocket 连接或加载会话,彻底消除首次加载时 agent 子进程工作目录与前端显示不一致的问题。
|
|
15
|
+
- 服务端 `normalizeCwd(dir)` 在 `dir` 为空时默认使用 `process.cwd()`(服务启动目录),保证前后端默认工作目录始终精确统一。
|
|
16
|
+
- **错误信息二次 HTML 转义修复 (Double Escape on Error Messages)**:移除 `message_end` 中冗余的 `escapeHtml()` 调用,避免错误信息包含特殊符号时被 Markdown 渲染器二次转义显示为实体编码字符。
|
|
17
|
+
- **CLI 参数解析健壮性增强 (CLI Arguments Validation)**:`bin/pi-web-chat.js` 增加对 `-p/--port` 与 `-c/--cwd` 选项值的合法性检验与防越界保护,避免 `NaN` 或 `undefined`。
|
|
18
|
+
- **输入法合成事件优化 (IME Keycode Handling)**:输入框 `keydown` 事件追加 `e.keyCode !== 229` 判断,进一步增强各平台中文输入法选词回车时的兼容性。
|
|
19
|
+
- **Systemd 安装脚本优化 (Systemd Absolute ExecStart Path)**:`scripts/install-service.sh` 生成 unit 文件时将 `ExecStart` 明确为绝对路径 `$NODE_BIN "$PROJECT_DIR/server.js"`。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
10
23
|
## [1.7.4] - 2026-07-26
|
|
11
24
|
|
|
12
25
|
### Added
|
package/docs/ISSUES.md
CHANGED
|
@@ -99,4 +99,27 @@
|
|
|
99
99
|
- 给 `.session-list` 增加 `min-height: 0`,给 `.sidebar-bottom` 增加 `flex-shrink: 0`。
|
|
100
100
|
- 引入 `safe-area-inset-bottom` 底部安全区自适应内边距,并将 z-index 提升至 105,确保手机端完整可见。
|
|
101
101
|
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 10. 页面初次加载时服务端配置加载与 WebSocket 连接的竞态
|
|
105
|
+
|
|
106
|
+
**症状**:在未设置 localStorage CWD 时打开页面,前端顶栏胶囊显示为当前服务启动目录(`serverCwd`),但后端生成的 agent 子进程的工作目录实际上是用户的 home 目录(`~`),导致执行文件操作或命令时位置不一致。
|
|
107
|
+
|
|
108
|
+
**根因**:前端 `init()` 中 `loadServerConfig()` 异步发起 `/api/config` 请求,但没有等待返回就同步调用了 `connectWs({})`。此时 `state.cwd` 尚为空字符串 `""`,WebSocket URL 为 `/ws?cwd=`,后端 fallback 成了 `home()`。等到 fetch 返回后 `state.cwd` 虽被赋予 `serverCwd`,但 WebSocket 连接和 `PiAgent` 已经按 `home()` 创建。
|
|
109
|
+
|
|
110
|
+
**修复**:
|
|
111
|
+
- 前端 `init()` 改为异步函数,显式 `await loadServerConfig()` 保证配置解析完毕后再建立 WebSocket 连接或加载会话。
|
|
112
|
+
- 后端 `normalizeCwd(dir)` 在 `dir` 为空时优先返回 `process.cwd()`,其次回退到 `home()`,确保前后端默认工作目录始终精确一致。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 11. 模型异常响应错误信息二次转义 (Double Escape)
|
|
117
|
+
|
|
118
|
+
**症状**:当模型返回错误(如 stopReason="error")且错误信息中含有 `<`、`&` 等字符时,聊天框中显示为 `<`、`&` 字面量。
|
|
119
|
+
|
|
120
|
+
**根因**:`public/app.js` 在 `message_end` 处理中对 `errMsg` 调用了一次 `escapeHtml()`,随后 `refreshStreamingContent()` 调用 `renderMarkdown()` 时又内部进行了一次 `escapeHtml()`。
|
|
121
|
+
|
|
122
|
+
**修复**:移除 `message_end` 中冗余的 `escapeHtml()`,统一由 Markdown 渲染器进行安全转义。
|
|
123
|
+
|
|
124
|
+
|
|
102
125
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1226,7 +1226,7 @@ function handlePiMessage(obj) {
|
|
|
1226
1226
|
const parsed = JSON.parse(errMsg);
|
|
1227
1227
|
if (parsed.error?.message) errMsg = parsed.error.message;
|
|
1228
1228
|
} catch {}
|
|
1229
|
-
state.streamingItems.push({ type: "text", text: `⚠️ **${
|
|
1229
|
+
state.streamingItems.push({ type: "text", text: `⚠️ **${errMsg}**` });
|
|
1230
1230
|
refreshStreamingContent();
|
|
1231
1231
|
}
|
|
1232
1232
|
break;
|
|
@@ -1668,11 +1668,13 @@ function autoResize() {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
|
|
1670
1670
|
// ---- Init ----
|
|
1671
|
-
function init() {
|
|
1671
|
+
async function init() {
|
|
1672
1672
|
// Default cwd to home (server uses home default too).
|
|
1673
1673
|
state.cwd = document.body.dataset.cwd || "";
|
|
1674
1674
|
|
|
1675
|
-
//
|
|
1675
|
+
// Load server config & restore saved CWD before connecting WebSocket
|
|
1676
|
+
await loadServerConfig();
|
|
1677
|
+
|
|
1676
1678
|
// event listeners
|
|
1677
1679
|
$("#btnNew").addEventListener("click", () => {
|
|
1678
1680
|
if (state.streaming) {
|
|
@@ -1716,7 +1718,7 @@ function init() {
|
|
|
1716
1718
|
updateComposerUI();
|
|
1717
1719
|
});
|
|
1718
1720
|
ta.addEventListener("keydown", (e) => {
|
|
1719
|
-
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
1721
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing && e.keyCode !== 229) {
|
|
1720
1722
|
e.preventDefault();
|
|
1721
1723
|
if (state.streaming) {
|
|
1722
1724
|
if (ta.value.trim()) {
|
|
@@ -1826,9 +1828,6 @@ function init() {
|
|
|
1826
1828
|
});
|
|
1827
1829
|
}
|
|
1828
1830
|
|
|
1829
|
-
// Load server config & restore saved CWD
|
|
1830
|
-
loadServerConfig();
|
|
1831
|
-
|
|
1832
1831
|
// Status badge click to reconnect
|
|
1833
1832
|
const connStatusEl = $("#connStatus");
|
|
1834
1833
|
if (connStatusEl) {
|
package/server.js
CHANGED
|
@@ -37,7 +37,7 @@ const PI_BIN = resolvePiBin();
|
|
|
37
37
|
function home() { return os.homedir(); }
|
|
38
38
|
|
|
39
39
|
function normalizeCwd(dir) {
|
|
40
|
-
if (!dir) return home();
|
|
40
|
+
if (!dir) return process.cwd() || home();
|
|
41
41
|
let resolved = dir;
|
|
42
42
|
if (dir.startsWith("~")) {
|
|
43
43
|
resolved = path.join(home(), dir.slice(1));
|