@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 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
@@ -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") opts.port = Number(argv[++i]);
45
- else if (a.startsWith("--port=")) opts.port = Number(a.split("=")[1]);
46
- else if (a === "-c" || a === "--cwd") opts.cwd = argv[++i];
47
- else if (a.startsWith("--cwd=")) opts.cwd = a.split("=")[1];
48
- else { console.error(`Unknown option: ${a}`); printHelp(); process.exit(1); }
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
  }
@@ -1,6 +1,6 @@
1
1
  # 架构设计文档
2
2
 
3
- > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v1.7.4 版本)。
3
+ > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v1.7.5 版本)。
4
4
 
5
5
  ---
6
6
 
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")且错误信息中含有 `<`、`&` 等字符时,聊天框中显示为 `&lt;`、`&amp;` 字面量。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "A ChatGPT/Gemini-style web UI for the pi coding agent, powered by pi's RPC mode.",
5
5
  "type": "module",
6
6
  "main": "server.js",
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: `⚠️ **${escapeHtml(errMsg)}**` });
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
- // event listeners
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));