@liguoshuai/pi-web-chat 2.9.0 → 2.11.2
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 +102 -1
- package/package.json +3 -3
- package/public/app.js +48 -9
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,107 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.11.2] - 2026-09-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Web 端 `submitSteer` 发送失败静默丢弃 (`clients/web/public/app.js`)**:`sendWs` 返回值未检查,WebSocket 在检查与发送之间断开时,指导指令被静默丢弃但 UI 已显示"已插入指导指令"。现先检查 `sendWs` 返回值,失败时提示用户并触发重连,不再乐观更新 UI。
|
|
14
|
+
- **Web 端 `abortGeneration` 发送失败导致 UI 锁死 (`clients/web/public/app.js`)**:`sendWs({ type: "abort" })` 返回值未检查,发送失败时 `state.aborting` 永久为 `true`,UI 锁定在中止状态。现检查返回值,失败时回退 `state.aborting` 并提示用户。
|
|
15
|
+
- **Web 端 `submitPrompt` 失败回退遗漏定时器清理 (`clients/web/public/app.js`)**:发送失败回退时未调用 `stopStreamingTimer()` 且未重置 `turnStartedAt` / `streamingMsgDurationEl`,导致 100ms 定时器持续向已分离 DOM 节点写入。现已补充清理。
|
|
16
|
+
- **Android `sendSteer` / `abort` / `switchSession` 发送返回值未检查 (`clients/android/.../ChatRepository.kt`)**:`sendRaw` 返回的 `Boolean` 被忽略。`abort` 无条件设置 `_isStreaming = false` 即使中止未送达服务端;`switchSession` 在发送前清空消息列表,发送失败时导致状态不同步。现均检查返回值并在失败时回退。
|
|
17
|
+
- **HarmonyOS `abort` / `newSession` / `switchSession` 发送返回值未检查 (`clients/harmony/.../ChatViewModel.ets`)**:与 Android 同类问题。`abort` 无条件重置 `isStreaming`;`newSession` / `switchSession` 在发送前清空消息,发送失败时导致状态不同步。现均检查 `wsManager.send()` 返回值并在失败时回退。
|
|
18
|
+
- **HarmonyOS `remote_user_prompt` 消息重复 (`clients/harmony/.../ChatViewModel.ets`)**:本地已发送的用户消息与服务端 `remote_user_prompt` 回声重复添加。现增加去重检查,若倒数第二条用户消息内容相同则跳过。
|
|
19
|
+
- **HarmonyOS `init()` 未重置流式状态 (`clients/harmony/.../ChatViewModel.ets`)**:切换服务器 URL 重新初始化时,残留的 `isStreaming = true` 导致孤立流式气泡。现 `init()` 中重置 `isStreaming = false`。
|
|
20
|
+
- **Android 编译失败修复 (`clients/android/.../ChatScreen.kt`)**:补充 `SmartToy` 与 `Psychology` 图标的 `import` 声明,修复替顶部胶囊换用 Material 图标后因缺失导入导致的 `Unresolved reference`,`assembleDebug` 可正常产出 APK。
|
|
21
|
+
- **服务端单元测试 CI 挂起修复 (`server/test/server.test.js` + `server/fixtures/pi-stub.mjs`)**:CI runner 未安装 `pi` CLI,`PiAgent.start()` 执行 `spawn("pi")` 时报 `ENOENT`,导致 `WebSocket gateway NEW_SESSION` 断言失败、HTTP 服务未关闭,最终使 `node --test` 进程长时间挂起。现引入无操作长驻 stub 并重写 `config.piBin`,使网关单测自包含、不再依赖外部 `pi` 二进制。
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- 全端版本号统一递增至 2.11.2(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## [2.11.1] - 2026-09-03
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **Android 构建失败修复 (`clients/android/.../ChatRepository.kt`)**:修复 `connect` 函数声明被写坏导致的语法错误(`fun connectcwd: String = ...` → `fun connect(cwd: String = ..., sessionPath: String? = null)`),该错误导致 `compileDebugKotlin` 失败、APK 无法产出,并连带触发 `ChatViewModel` 中多处 `Unresolved reference 'connect'`。修复后 Android Debug APK 可正常构建。
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- 全端版本号统一递增至 2.11.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## [2.11.0] - 2026-09-03
|
|
39
|
+
|
|
40
|
+
### Changed & Beautified (Android)
|
|
41
|
+
- **应用图标焕新 (`clients/android`)**:弃用系统默认图标,新增基于 π 品牌图形与翡翠绿渐变的 **自适应启动图标 (Adaptive Icon)**,包含背景渐变、白色圆角 π 字形与点缀色,支持普通与圆形图标形态。
|
|
42
|
+
- **顶部胶囊图标美化 (TopBar Pills)**:将模型选择 (🤖) 与思考级别 (🧠) 的简单 Emoji 替换为 **彩色圆形徽章 + Material 图标**(`SmartToy` 机器人 / `Psychology` 大脑),并同步美化工作目录胶囊图标,视觉更加精致统一。
|
|
43
|
+
- **对话框气泡美化 (Message Bubbles)**:
|
|
44
|
+
- 用户消息气泡升级为 **翡翠绿渐变背景 + 护色描边**,圆角更柔和、内边距更舒适。
|
|
45
|
+
- 助手头像由扁平半透明圆形升级为 **彩色渐变圆形徽章**(白色 π),辨识度与质感更佳。
|
|
46
|
+
- **样例提示词间距优化 (Suggestion Chips)**:解决提示词芯片过于紧凑的问题,引入图标 + 标签的卡片式芯片,并增大水平与垂直间距、内边距,视觉更舒展。
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- 全端版本号统一递增至 2.11.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## [2.10.1] - 2026-09-03
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- **HarmonyOS 监听器泄漏修复 (`clients/harmony/.../ChatViewModel.ets`)**:`init()` 每次调用都新增 WebSocket 状态与消息监听器但不移除旧的,导致重复调用时监听器堆积、消息被重复处理。现已保存监听器引用并在重新初始化时先移除旧监听器。
|
|
57
|
+
- **HarmonyOS WebSocket 重连竞争修复 (`clients/harmony/.../WebSocketManager.ets`)**:`connect()` 替换已有连接时,旧 socket 的异步 `on('close')` 回调仍会触发 `scheduleReconnect()`,导致连接抖动。现已通过捕获 ws 实例引用,在陈旧回调中跳过重连逻辑。
|
|
58
|
+
- **Android WebSocket 连接泄漏修复 (`clients/android/.../WebSocketClient.kt`)**:`connect()` 未关闭已有 WebSocket 连接,在重连场景下旧连接资源泄漏。现已在 `connect()` 开头主动关闭并清理旧连接。
|
|
59
|
+
- **Android 协程作用域泄漏修复 (`clients/android/.../ChatRepository.kt` / `ChatViewModel.kt`)**:`reconnect()` 创建新 `ChatRepository` 但未取消旧实例的协程作用域,导致协程泄漏。现已新增 `close()` 方法并在重连时调用。
|
|
60
|
+
- **协议层冗余代码清理 (`packages/protocol/src/index.js`)**:`formatDuration` 函数前两个条件分支完全相同,移除冗余的 `ms < 1000` 分支。
|
|
61
|
+
- **HarmonyOS 冗余代码清理 (`clients/harmony/.../Index.ets`)**:同步移除 `formatDuration` 中相同的冗余分支。
|
|
62
|
+
- **Web 端 Token 泄漏防护 (`clients/web/public/app.js`)**:从 URL 提取 auth token 后立即使用 `history.replaceState` 清除 URL 中的 token 参数,防止通过 Referer 头、截图或分享链接泄漏。
|
|
63
|
+
- **Web 端代码围栏匹配修复 (`clients/web/public/app.js`)**:`renderMarkdown` 中 ```` 围栏匹配改为仅在行首匹配,避免将行内三反引号误判为代码围栏。
|
|
64
|
+
- **Web 端 SVG 元素命名空间修复 (`clients/web/public/app.js`)**:DOM 辅助函数 `el()` 对 SVG 标签使用 `createElementNS` 创建,修复 SVG 图标渲染问题。
|
|
65
|
+
- **Web 端发送失败处理增强 (`clients/web/public/app.js`)**:`sendWs` 返回布尔值,`submitPrompt` 在 WebSocket 断开时回退流式状态并提示用户,`setComposerAborting(true)` 正确设置 `state.aborting`。
|
|
66
|
+
- **Android API 基址规范化 (`clients/android/.../ApiService.kt`)**:构造函数中自动去除 `baseUrl` 尾部斜杠,避免拼接出双斜杠 URL。
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- 全端版本号统一递增至 2.10.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## [2.10.0] - 2026-09-02
|
|
73
|
+
|
|
74
|
+
### Added & Aligned
|
|
75
|
+
- **Android 原生客户端与移动 Web 端全要素对齐 (`clients/android`)**:
|
|
76
|
+
- **顶部栏元素完整对齐 (TopBar Alignment)**:
|
|
77
|
+
- 新增 **工作目录胶囊按钮 (CWD Pill)**:展示当前工作目录,点击弹出工作目录切换对话框(包含路径输入与 `~`, `~/.pi`, `/tmp` 快捷芯片)。
|
|
78
|
+
- 新增 **模型选择胶囊按钮 (Model Pill)**:展示当前模型名称与 `★ 默认` 徽章,点击弹出支持搜索、厂商归类、能力标签(🧠 推理、👁️ 视觉、🛠️ 工具)的模型选择对话框。
|
|
79
|
+
- 新增 **深度思考级别胶囊按钮 (Thinking Pill)**:展示当前思考深度,点击弹出 `Off`、`Minimal`、`Low`、`Medium`、`High`、`Max` 等推理级别选择对话框。
|
|
80
|
+
- 新增 **会话导出按钮 (Export Chat)**:一键将完整对话导出为标准 Markdown 格式并唤起系统分享。
|
|
81
|
+
- **设置入口单点统一**:移除侧边抽屉顶部多余的重复设置按钮,保留顶部栏统一单一设置入口,避免操作歧义。
|
|
82
|
+
- **空白引导页全要素对齐 (Empty State Alignment)**:
|
|
83
|
+
- 引入 `π` 经典头像与提示文案。
|
|
84
|
+
- 引入 **当前模型信息横幅 (Empty Model Banner)**:展示当前模型名称、默认徽章、🧠 思考 / 👁️ 视觉 / 🛠️ 工具能力标签及“切换模型”快捷按钮。
|
|
85
|
+
- 引入 **快捷指令芯片 (Suggestion Chips)**:提供“列出当前目录文件”、“总结这个项目”、“代码审查”等一键触发指令。
|
|
86
|
+
- **底部输入区域与富文本展示对齐 (Composer & Message Bubbles)**:
|
|
87
|
+
- 增加待发送附件预览栏(带缩略图与删除 `✕` 按钮)。
|
|
88
|
+
- 增加图片全屏查看画廊模式 (Image Lightbox Modal)。
|
|
89
|
+
- 助手回复内容 Markdown 解析优化,代码块增加语言标签与“复制代码”独立按钮。
|
|
90
|
+
- 增加底部操作提示文案:“pi 会执行命令与读写你的文件 —— 请注意操作内容。”
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- 全端版本号统一递增至 2.10.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## [2.9.1] - 2026-09-02
|
|
98
|
+
|
|
99
|
+
### Fixed & Optimized
|
|
100
|
+
- **网关路由健壮性增强 (`server/src/routes.js`)**:修复 `/api/sessions` 历史会话列表中若存在数值格式时间戳导致 `localeCompare` 抛错的问题,优化为安全兼容数字与 ISO 字符串的多格式时间戳倒序排列。
|
|
101
|
+
- **Web 前端异常流式保护 (`clients/web/public/app.js`)**:在 WebSocket 消息处理中补充对 `error` 与 `pi_exit` 事件的完备响应,遇到异常时自动终结流式状态、解除输入框锁定并提示用户。
|
|
102
|
+
- **Android 原生端异常退出流式重置 (`clients/android`)**:`ChatRepository.kt` 补充对 `error` 和 `pi_exit` 事件的处理,避免在子进程异常退出时状态滞留在流式中。
|
|
103
|
+
- **HarmonyOS 原生端异常流式重置 (`clients/harmony`)**:`ChatViewModel.ets` 补充对 `error` 和 `pi_exit` 事件的处理,确保鸿蒙界面在异常中断时能正常完成并释放输入。
|
|
104
|
+
- **跨端通信协议 Schema 完备性更新 (`packages/protocol/src/schema.json`)**:补充 `remote_user_steer` 与 `extension_ui_request` 到服务端消息类型枚举定义。
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
- 全端版本号统一递增至 2.9.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
10
111
|
## [2.9.0] - 2026-09-02
|
|
11
112
|
|
|
12
113
|
### Added
|
|
@@ -624,4 +725,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
624
725
|
| **Deprecated** | 即将移除 |
|
|
625
726
|
| **Removed** | 已移除 |
|
|
626
727
|
| **Fixed** | Bug 修复 |
|
|
627
|
-
| **Security** | 安全相关修复 |
|
|
728
|
+
| **Security** | 安全相关修复 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.2",
|
|
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",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"express": "^4.21.2",
|
|
44
44
|
"ws": "^8.18.0",
|
|
45
|
-
"@liguoshuai/pi-chat-
|
|
46
|
-
"@liguoshuai/pi-chat-
|
|
45
|
+
"@liguoshuai/pi-chat-server": "2.11.2",
|
|
46
|
+
"@liguoshuai/pi-chat-protocol": "2.11.2"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "node --check server.js && node --check bin/pi-web-chat.js && node --check public/app.js",
|
package/public/app.js
CHANGED
|
@@ -26,6 +26,10 @@ function getAuthToken() {
|
|
|
26
26
|
const urlToken = new URLSearchParams(window.location.search).get("token");
|
|
27
27
|
if (urlToken) {
|
|
28
28
|
localStorage.setItem("pi_auth_token", urlToken);
|
|
29
|
+
// Strip token from URL to prevent leakage via Referer, screenshots, or shared links
|
|
30
|
+
const cleanUrl = new URL(window.location.href);
|
|
31
|
+
cleanUrl.searchParams.delete("token");
|
|
32
|
+
window.history.replaceState({}, "", cleanUrl.toString());
|
|
29
33
|
return urlToken;
|
|
30
34
|
}
|
|
31
35
|
return localStorage.getItem("pi_auth_token") || "";
|
|
@@ -302,11 +306,14 @@ function renderMarkdown(md) {
|
|
|
302
306
|
const parts = [];
|
|
303
307
|
let rest = md;
|
|
304
308
|
while (rest.length) {
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
// Only match ``` at the beginning of a line (or start of string) to avoid
|
|
310
|
+
// misinterpreting inline triple-backticks as code fences.
|
|
311
|
+
const fenceMatch = rest.match(/(?:^|\n)```/);
|
|
312
|
+
if (!fenceMatch) {
|
|
307
313
|
parts.push({ kind: "md", text: rest });
|
|
308
314
|
rest = "";
|
|
309
315
|
} else {
|
|
316
|
+
const fenceIdx = fenceMatch.index + (fenceMatch[0].length - 3);
|
|
310
317
|
if (fenceIdx > 0) parts.push({ kind: "md", text: rest.slice(0, fenceIdx) });
|
|
311
318
|
rest = rest.slice(fenceIdx + 3);
|
|
312
319
|
// optional language on this line
|
|
@@ -505,7 +512,8 @@ function mdTable(lines) {
|
|
|
505
512
|
// ---- DOM helpers ----
|
|
506
513
|
const $ = (sel, root = document) => root.querySelector(sel);
|
|
507
514
|
const el = (tag, props = {}, children = []) => {
|
|
508
|
-
const
|
|
515
|
+
const SVG_TAGS = new Set(["svg","rect","path","circle","line","polyline","polygon","ellipse","g","defs","use","text","tspan","linearGradient","radialGradient","stop","clipPath","mask","pattern","filter","feGaussianBlur","feOffset","feMerge","feMergeNode","animate","animateTransform","animateMotion"]);
|
|
516
|
+
const n = SVG_TAGS.has(tag) ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
|
|
509
517
|
for (const [k, v] of Object.entries(props)) {
|
|
510
518
|
if (k === "class") n.className = v;
|
|
511
519
|
else if (k === "html") n.innerHTML = v;
|
|
@@ -1939,8 +1947,9 @@ function connectWs(opts = {}) {
|
|
|
1939
1947
|
}
|
|
1940
1948
|
|
|
1941
1949
|
function sendWs(obj) {
|
|
1942
|
-
if (!state.ws || state.ws.readyState !== 1) return;
|
|
1950
|
+
if (!state.ws || state.ws.readyState !== 1) return false;
|
|
1943
1951
|
state.ws.send(JSON.stringify(obj));
|
|
1952
|
+
return true;
|
|
1944
1953
|
}
|
|
1945
1954
|
|
|
1946
1955
|
function handlePiMessage(obj) {
|
|
@@ -2265,6 +2274,13 @@ function handlePiMessage(obj) {
|
|
|
2265
2274
|
case "extension_ui_request":
|
|
2266
2275
|
handleExtensionUiRequest(obj);
|
|
2267
2276
|
break;
|
|
2277
|
+
case "error":
|
|
2278
|
+
finalizeStreamingMsg();
|
|
2279
|
+
state.streaming = false;
|
|
2280
|
+
state.aborting = false;
|
|
2281
|
+
setComposerAborting(false);
|
|
2282
|
+
showToast(`⚠️ ${obj.message || obj.code || "发生错误"}`);
|
|
2283
|
+
break;
|
|
2268
2284
|
case "pi_exit":
|
|
2269
2285
|
finalizeStreamingMsg();
|
|
2270
2286
|
state.streaming = false;
|
|
@@ -2926,7 +2942,9 @@ function updateComposerUI() {
|
|
|
2926
2942
|
}
|
|
2927
2943
|
|
|
2928
2944
|
function setComposerAborting(yes) {
|
|
2929
|
-
if (
|
|
2945
|
+
if (yes) {
|
|
2946
|
+
state.aborting = true;
|
|
2947
|
+
} else {
|
|
2930
2948
|
state.aborting = false;
|
|
2931
2949
|
const inner = $("#composerInner");
|
|
2932
2950
|
if (inner) inner.classList.remove("aborting");
|
|
@@ -2951,7 +2969,12 @@ function abortGeneration() {
|
|
|
2951
2969
|
updateComposerUI();
|
|
2952
2970
|
const hint = $(".composer-hint");
|
|
2953
2971
|
if (hint) hint.textContent = "中止当前任务中…";
|
|
2954
|
-
sendWs({ type: "abort" })
|
|
2972
|
+
if (!sendWs({ type: "abort" })) {
|
|
2973
|
+
state.aborting = false;
|
|
2974
|
+
updateComposerUI();
|
|
2975
|
+
if (hint) hint.textContent = "中止失败:WebSocket 连接已断开。正在尝试重连…";
|
|
2976
|
+
scheduleReconnect(0);
|
|
2977
|
+
}
|
|
2955
2978
|
}
|
|
2956
2979
|
|
|
2957
2980
|
function submitSteer() {
|
|
@@ -2966,6 +2989,12 @@ function submitSteer() {
|
|
|
2966
2989
|
return;
|
|
2967
2990
|
}
|
|
2968
2991
|
|
|
2992
|
+
if (!sendWs({ type: "steer", message: text })) {
|
|
2993
|
+
if (hint) hint.textContent = "发送失败:WebSocket 连接已断开。正在尝试重连…";
|
|
2994
|
+
scheduleReconnect(0);
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2969
2998
|
appendMessageNode("user", { text, isSteer: true, ts: Date.now() });
|
|
2970
2999
|
ta.value = "";
|
|
2971
3000
|
autoResize();
|
|
@@ -2975,8 +3004,6 @@ function submitSteer() {
|
|
|
2975
3004
|
setTimeout(() => {
|
|
2976
3005
|
if (hint) hint.textContent = "pi 会执行命令与读写你的文件 —— 请注意操作内容。";
|
|
2977
3006
|
}, 4000);
|
|
2978
|
-
|
|
2979
|
-
sendWs({ type: "steer", message: text });
|
|
2980
3007
|
}
|
|
2981
3008
|
|
|
2982
3009
|
function submitPrompt() {
|
|
@@ -3053,7 +3080,19 @@ function submitPrompt() {
|
|
|
3053
3080
|
}
|
|
3054
3081
|
}
|
|
3055
3082
|
}
|
|
3056
|
-
sendWs({ type: "prompt", message: text, images: imagesToSend })
|
|
3083
|
+
if (!sendWs({ type: "prompt", message: text, images: imagesToSend })) {
|
|
3084
|
+
state.streaming = false;
|
|
3085
|
+
state.aborting = false;
|
|
3086
|
+
setComposerAborting(false);
|
|
3087
|
+
stopStreamingTimer();
|
|
3088
|
+
state.turnStartedAt = null;
|
|
3089
|
+
state.streamingMsgDurationEl = null;
|
|
3090
|
+
if (state.streamingMsg) { state.streamingMsg.remove(); state.streamingMsg = null; }
|
|
3091
|
+
state.streamingItems = [];
|
|
3092
|
+
const hint = $(".composer-hint");
|
|
3093
|
+
if (hint) hint.textContent = "发送失败:WebSocket 连接已断开。正在尝试重连…";
|
|
3094
|
+
scheduleReconnect(0);
|
|
3095
|
+
}
|
|
3057
3096
|
}
|
|
3058
3097
|
|
|
3059
3098
|
function autoResize() {
|