@liguoshuai/pi-web-chat 1.7.5 → 1.7.6

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/docs/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.6] - 2026-07-26
11
+
12
+ ### Security
13
+ - **Markdown 渲染 HTML 属性逃逸注入安全漏洞修复 (HTML Attribute Breakout XSS Vulnerability)**:升级 `public/app.js` 中的 `escapeHtml` 严密性,将原有对 `&`、`<`、`>` 的处理,扩展至双引号 `"` (`&quot;`) 与单引号 `'` (`&#39;`),阻断由于模型生成恶意链接等引起的 `href` 属性逃逸与任意 HTML 属性/事件(XSS)注入风险。
14
+
15
+ ### Fixed
16
+ - **多设备与多标签页流式事件同步及中途连入追平优化 (Multi-device/Multi-tab Live Event Streaming Sync)**:
17
+ - 优化 `PiAgent` 事件录制范围:只要处于流式生成(Busy/Streaming)状态,即便当前已有在线浏览器连接,也会向 `eventBuffer` 持续录制。
18
+ - 优化缓存释放策略:若 Agent 仍处于 busy 生成状态,新连入的客户端消费完 `eventBuffer` 后不再立即清空 Buffer。
19
+ - 优化重连/中途断开录制:若最后一个客户端在 streaming 途中断开连接,只要生成任务仍在运行中,即不销毁当前的缓存。
20
+ - 以上多项优化完美实现了多台设备(例如手机、电脑)、多个浏览器 Tab 随时刷新或中途连入正在运行中的流式会话时,能够无缝重播已生成的上半段内容并追平后续实时流,极大地增强了多端多 Tab 的协同可靠性。
21
+
22
+ ---
23
+
10
24
  ## [1.7.5] - 2026-07-26
11
25
 
12
26
  ### Fixed
package/docs/ISSUES.md CHANGED
@@ -121,5 +121,30 @@
121
121
 
122
122
  **修复**:移除 `message_end` 中冗余的 `escapeHtml()`,统一由 Markdown 渲染器进行安全转义。
123
123
 
124
+ ---
125
+
126
+ ## 12. Markdown 渲染中潜在的 XSS 注入安全漏洞 (HTML Attribute Breakout)
127
+
128
+ **症状**:若模型返回了特制格式的 Markdown 链接(例如 `[点击](http://abc.com" style="..." onmouseover="alert(1))`),该链接渲染为 HTML 时能逃逸 `href="..."` 属性,注入任意 HTML 属性与恶意 JavaScript。
129
+
130
+ **根因**:原先 `escapeHtml()` 仅转义了 `&`、`<`、`>`,未能转义双引号 `"` 与单引号 `'`,使得属性逃逸攻击成为可能。
131
+
132
+ **修复**:修改 `public/app.js` 中的 `escapeHtml()` 实现,额外对 `"` (`&quot;`) 与 `'` (`&#39;`) 进行了严格转义,阻止任何 HTML 属性级别的注入攻击。
133
+
134
+ ---
135
+
136
+ ## 13. 多设备 / 多 Tab 流式对话时新连入客户端无法追平进度
137
+
138
+ **症状**:在某一会话处于流式文本生成(Streaming)状态时,若用户在另一台设备或新的浏览器标签页中打开相同会话,由于 `PiAgent` 检测到已连接客户端而不将流式事件写入 `eventBuffer`,新连入的客户端无法回放当前的流式消息,导致其显示为空白、缺页或卡在加载中。
139
+
140
+ **根因**:
141
+ - 原逻辑在 `onPiMessage` 中判断只有在无客户端连接时才缓存事件(`if (!this.hasWs) this.bufferEvent(obj)`)。
142
+ - 离线回放完毕后,会强制清空环形 Buffer,导致后续其他客户端连入时无内容可播。
143
+
144
+ **修复**:
145
+ - 在 `server.js` 的 `onPiMessage` 逻辑中,当 `PiAgent` 处于繁忙或流式对话过程中(`this.isBusy` 为真)即便有客户端在线也开启缓存。
146
+ - 修改 `detachWs`,当所有连接断开但 Agent 仍在生成中时,保留 `eventBuffer` 保证继续录制。
147
+ - 修改 `replayBuffered`,在 Agent 仍处于 busy 状态时消费后不清空缓存,使多个设备/多个 Tab 可多次或同时连入并安全回溯追平全部生成细节,并在流式终止时通过 `agent_settled` 进行最终的统一清理。
148
+
124
149
 
125
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "1.7.5",
3
+ "version": "1.7.6",
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
@@ -177,7 +177,11 @@ async function confirmCwdChange() {
177
177
 
178
178
  // ---- Markdown render (small, safe renderer) ----
179
179
  function escapeHtml(s) {
180
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
180
+ return s.replace(/&/g, "&amp;")
181
+ .replace(/</g, "&lt;")
182
+ .replace(/>/g, "&gt;")
183
+ .replace(/"/g, "&quot;")
184
+ .replace(/'/g, "&#39;");
181
185
  }
182
186
 
183
187
  async function copyToClipboard(text) {
package/server.js CHANGED
@@ -159,9 +159,13 @@ class PiAgent {
159
159
  detachWs(ws) {
160
160
  this.sockets.delete(ws);
161
161
  if (this.sockets.size === 0) {
162
- // Reset event buffer so we only capture events that happen while nobody is connected
163
- this.eventBuffer = [];
164
- this.bufferHead = 0;
162
+ // Reset event buffer only if NOT busy.
163
+ // If we are currently streaming/busy, we must retain the buffer so a reconnecting client
164
+ // can replay the full stream from the beginning of the active generation.
165
+ if (!this.isBusy) {
166
+ this.eventBuffer = [];
167
+ this.bufferHead = 0;
168
+ }
165
169
  // Browser closed. We do NOT kill the subprocess here: a background task
166
170
  // keeps running. We only arm the idle-kill, which fires once the agent
167
171
  // is truly idle (no streaming, no pending requests) for IDLE_TIMEOUT_MS.
@@ -326,8 +330,11 @@ class PiAgent {
326
330
  }
327
331
  // Forward every event / response to connected browsers as-is.
328
332
  this.wsSend(obj);
329
- // If nobody is listening, remember it so a reconnect can replay.
330
- if (!this.hasWs) this.bufferEvent(obj);
333
+ // If nobody is listening, or if the agent is currently busy (streaming), remember it
334
+ // so any reconnecting or newly connecting clients can replay and catch up.
335
+ if (!this.hasWs || this.isBusy) {
336
+ this.bufferEvent(obj);
337
+ }
331
338
  }
332
339
 
333
340
  bufferEvent(obj) {
@@ -353,9 +360,12 @@ class PiAgent {
353
360
  try { ws.send(JSON.stringify(ev)); } catch {}
354
361
  }
355
362
  try { ws.send(JSON.stringify({ type: "backfill_end", streaming: this.isBusy, state: this.state })); } catch {}
356
- // Clear buffer once consumed by the reconnecting client
357
- this.eventBuffer = [];
358
- this.bufferHead = 0;
363
+ // Only clear the buffer if the agent is not busy.
364
+ // If the agent is still busy, keep the buffer so that other clients or future reconnects can still catch up.
365
+ if (!this.isBusy) {
366
+ this.eventBuffer = [];
367
+ this.bufferHead = 0;
368
+ }
359
369
  }
360
370
 
361
371
  send(cmd) {