@liguoshuai/pi-web-chat 2.9.0 → 2.12.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 +163 -1
- package/package.json +3 -3
- package/public/app.js +65 -27
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,168 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.12.2] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Web 端发送消息后输入框锁死并误显“正在中止…” (`clients/web/public/app.js`)**:`submitPrompt` 发送成功后调用 `setComposerAborting(true)`(现已更名为 `setComposerStreaming`),其内部误将 `state.aborting` 置为 `true`,导致 `updateComposerUI` 进入“中止中”分支:输入框占位符显示“正在中止当前任务…”,发送按钮变成 `⏳` 并置为 `disabled`,UI 锁死直至任务结束。同时 `agent_start` / `backfill_end` / `updateState` 三处进入流式状态的路径也存在同样的误用。现将语义统一为 `setComposerStreaming`:进入流式状态时始终将 `state.aborting` 清理为 `false`,仅刷新 `updateComposerUI` 与 `renderModelPill`;而“中止”仅由用户点击中止时通过 `abortGeneration` 单独置位,彻底解决发送消息后对话框锁死、误显“中止…”的问题。
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- 全端版本号统一递增至 2.12.2(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
17
|
+
|
|
18
|
+
## [2.12.1] - 2026-09-04
|
|
19
|
+
|
|
20
|
+
### Improved
|
|
21
|
+
- **耗时时长格式化进位与边界修复 (`packages/protocol`, `clients/web/public/app.js`)**:
|
|
22
|
+
- 重构 `formatDuration` 算法,避免毫秒数在接近整分边界(例如 `59950ms`、`119950ms`)时因浮点数取模四舍五入导致输出 `"0m 60s"` 或 `"1m 60s"` 异常格式,统一进位并补齐单元测试。
|
|
23
|
+
- **会话列表排序时间戳健壮性增强 (`server/src/routes.js`)**:
|
|
24
|
+
- 在 `/api/sessions` 排序中加入严格的 `Number.isFinite` 与 `Date.parse` 解析校验,防止无效或非标准日期字符串导致 `NaN` 比较破坏 JavaScript 引擎排序稳定性。
|
|
25
|
+
- **流式事件回放与请求 ID 边界保护 (`server/src/agent.js`)**:
|
|
26
|
+
- 优化 WebSocket / SSE 缓冲区回放遍历逻辑,精准依据缓冲区环形溢出状态重放事件,过滤潜在空槽位;
|
|
27
|
+
- 改进 `PiAgent.send` 在子进程异常终止时对于数字 `0` 作为请求 ID 的正确保留与响应返回。
|
|
28
|
+
- **Android 客户端协程生命周期防御 (`clients/android/.../ChatRepository.kt`)**:
|
|
29
|
+
- 仓库层使用 `SupervisorJob()` 配合 `Dispatchers.IO`,避免子协程失败导致整个仓库层作用域被级联取消。
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- 全端版本号统一递增至 2.12.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
33
|
+
|
|
34
|
+
## [2.12.0] - 2026-09-04
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- **Android 构建产物自定义重命名与 Release 加固配置 (`clients/android/app/build.gradle.kts`, `proguard-rules.pro`)**:
|
|
38
|
+
- 构建产物根据版本号和构建类型重命名为语义化规范格式:`pi-chat-v{versionName}-debug.apk` 与 `pi-chat-v{versionName}-release.apk`。
|
|
39
|
+
- 启用 Release 版本的混淆加固与代码资源缩减(`isMinifyEnabled = true`, `isShrinkResources = true`),配置完整的 ProGuard / R8 保护规则(数据模型、Kotlinx Serialization、OkHttp、Compose 与协程)。
|
|
40
|
+
- 配置默认签名回退支持,使 Release APK 可开箱即用构建。
|
|
41
|
+
- **Android 会话删除功能 (`clients/android/.../ApiService.kt`, `ChatRepository.kt`, `ChatViewModel.kt`, `ChatScreen.kt`)**:
|
|
42
|
+
- 新增 `DELETE /api/session?file=...` 网络请求与仓库层方法,并在侧边栏每个会话项右侧提供删除按钮及二次确认弹窗。
|
|
43
|
+
|
|
44
|
+
### Improved
|
|
45
|
+
- **Android 侧边栏抽屉界面优化与对齐 Web 体验 (`clients/android/.../ChatScreen.kt`)**:
|
|
46
|
+
- 抽屉宽度适配调整为 `280dp`,改善手机端视觉比例;
|
|
47
|
+
- 会话项展示信息全面对齐 Web:新增格式化时间戳(如 `9月4日 15:30`)、消息条数展示(`· N 条`)与流式任务运行中微动徽标(`运行中`);
|
|
48
|
+
- 选中会话样式重构:高亮背景(`BgHover`)+ 左侧强调色指示条(`Accent`),会话行间距与内边距紧凑精致化;
|
|
49
|
+
- 搜索框高度重构为紧凑式 `34dp`,包含搜索图标与一键清空按钮;
|
|
50
|
+
- 侧边栏底部补齐 `pi.dev` 与 `pi-web-chat` 外部链接跳转,统一展示版本信息。
|
|
51
|
+
- **Android 顶栏排版与信息层级重构 (`clients/android/.../ChatScreen.kt`)**:
|
|
52
|
+
- 顶部导航栏高度规整至 `56dp`,居中展示当前对话主标题,并在下方紧凑排列工作目录(CWD)、模型名称(含 `★ 默认` 徽标)和思考推理等级胶囊;
|
|
53
|
+
- 左右两侧合理布局抽屉按钮、分享导出按钮及设置按钮,排版清晰美观不拥挤,彻底解决信息遮挡与丢失问题。
|
|
54
|
+
- **Android 底部输入框体验与样式重构 (`clients/android/.../ChatScreen.kt`)**:
|
|
55
|
+
- 输入框整体采用 Web 端 ChatGPT 风格圆角胶囊卡片(`RoundedCornerShape(22.dp)`),内边距与高度严格贴合文本行高;
|
|
56
|
+
- 采用 `BasicTextField` 配合优雅的占位符提示,支持多行平滑自适应扩展;
|
|
57
|
+
- 发送按钮、指导指令按钮及中止按钮尺寸规整(32dp 圆形/胶囊),并对齐底部安全区。
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- 全端版本号统一递增至 2.12.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
61
|
+
|
|
62
|
+
## [2.11.3] - 2026-09-03
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
- **HarmonyOS 端配置持久化与初始加载缺失 (`clients/harmony/...`)**:`PreferencesHelper` 虽然实现了服务端 URL 的存储与获取,但未在 App 启动/初始化流程中实际调用。现已在 `Index.ets` 的 `aboutToAppear()` 中调用 `PreferencesHelper.getServerUrl()` 动态读取已保存的 URL 进行初始化,并在“保存并重连”时调用 `PreferencesHelper.setServerUrl()` 写入持久化存储,解决应用重启后配置丢失问题。
|
|
66
|
+
- **HarmonyOS 端 UI 界面版本号硬编码滞后 (`clients/harmony/.../Index.ets`)**:侧边栏抽屉底部硬编码展示旧版本号 `v2.10.1`,现已更新为当前全仓最新的 `v2.11.3`,与 Monorepo 版本数严格对齐。
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- 全端版本号统一递增至 2.11.3(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
70
|
+
|
|
71
|
+
## [2.11.2] - 2026-09-03
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- **Web 端 `submitSteer` 发送失败静默丢弃 (`clients/web/public/app.js`)**:`sendWs` 返回值未检查,WebSocket 在检查与发送之间断开时,指导指令被静默丢弃但 UI 已显示"已插入指导指令"。现先检查 `sendWs` 返回值,失败时提示用户并触发重连,不再乐观更新 UI。
|
|
75
|
+
- **Web 端 `abortGeneration` 发送失败导致 UI 锁死 (`clients/web/public/app.js`)**:`sendWs({ type: "abort" })` 返回值未检查,发送失败时 `state.aborting` 永久为 `true`,UI 锁定在中止状态。现检查返回值,失败时回退 `state.aborting` 并提示用户。
|
|
76
|
+
- **Web 端 `submitPrompt` 失败回退遗漏定时器清理 (`clients/web/public/app.js`)**:发送失败回退时未调用 `stopStreamingTimer()` 且未重置 `turnStartedAt` / `streamingMsgDurationEl`,导致 100ms 定时器持续向已分离 DOM 节点写入。现已补充清理。
|
|
77
|
+
- **Android `sendSteer` / `abort` / `switchSession` 发送返回值未检查 (`clients/android/.../ChatRepository.kt`)**:`sendRaw` 返回的 `Boolean` 被忽略。`abort` 无条件设置 `_isStreaming = false` 即使中止未送达服务端;`switchSession` 在发送前清空消息列表,发送失败时导致状态不同步。现均检查返回值并在失败时回退。
|
|
78
|
+
- **HarmonyOS `abort` / `newSession` / `switchSession` 发送返回值未检查 (`clients/harmony/.../ChatViewModel.ets`)**:与 Android 同类问题。`abort` 无条件重置 `isStreaming`;`newSession` / `switchSession` 在发送前清空消息,发送失败时导致状态不同步。现均检查 `wsManager.send()` 返回值并在失败时回退。
|
|
79
|
+
- **HarmonyOS `remote_user_prompt` 消息重复 (`clients/harmony/.../ChatViewModel.ets`)**:本地已发送的用户消息与服务端 `remote_user_prompt` 回声重复添加。现增加去重检查,若倒数第二条用户消息内容相同则跳过。
|
|
80
|
+
- **HarmonyOS `init()` 未重置流式状态 (`clients/harmony/.../ChatViewModel.ets`)**:切换服务器 URL 重新初始化时,残留的 `isStreaming = true` 导致孤立流式气泡。现 `init()` 中重置 `isStreaming = false`。
|
|
81
|
+
- **Android 编译失败修复 (`clients/android/.../ChatScreen.kt`)**:补充 `SmartToy` 与 `Psychology` 图标的 `import` 声明,修复替顶部胶囊换用 Material 图标后因缺失导入导致的 `Unresolved reference`,`assembleDebug` 可正常产出 APK。
|
|
82
|
+
- **服务端单元测试 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` 二进制。
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
- 全端版本号统一递增至 2.11.2(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## [2.11.1] - 2026-09-03
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
- **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 可正常构建。
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
- 全端版本号统一递增至 2.11.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## [2.11.0] - 2026-09-03
|
|
100
|
+
|
|
101
|
+
### Changed & Beautified (Android)
|
|
102
|
+
- **应用图标焕新 (`clients/android`)**:弃用系统默认图标,新增基于 π 品牌图形与翡翠绿渐变的 **自适应启动图标 (Adaptive Icon)**,包含背景渐变、白色圆角 π 字形与点缀色,支持普通与圆形图标形态。
|
|
103
|
+
- **顶部胶囊图标美化 (TopBar Pills)**:将模型选择 (🤖) 与思考级别 (🧠) 的简单 Emoji 替换为 **彩色圆形徽章 + Material 图标**(`SmartToy` 机器人 / `Psychology` 大脑),并同步美化工作目录胶囊图标,视觉更加精致统一。
|
|
104
|
+
- **对话框气泡美化 (Message Bubbles)**:
|
|
105
|
+
- 用户消息气泡升级为 **翡翠绿渐变背景 + 护色描边**,圆角更柔和、内边距更舒适。
|
|
106
|
+
- 助手头像由扁平半透明圆形升级为 **彩色渐变圆形徽章**(白色 π),辨识度与质感更佳。
|
|
107
|
+
- **样例提示词间距优化 (Suggestion Chips)**:解决提示词芯片过于紧凑的问题,引入图标 + 标签的卡片式芯片,并增大水平与垂直间距、内边距,视觉更舒展。
|
|
108
|
+
|
|
109
|
+
### Changed
|
|
110
|
+
- 全端版本号统一递增至 2.11.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## [2.10.1] - 2026-09-03
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
- **HarmonyOS 监听器泄漏修复 (`clients/harmony/.../ChatViewModel.ets`)**:`init()` 每次调用都新增 WebSocket 状态与消息监听器但不移除旧的,导致重复调用时监听器堆积、消息被重复处理。现已保存监听器引用并在重新初始化时先移除旧监听器。
|
|
118
|
+
- **HarmonyOS WebSocket 重连竞争修复 (`clients/harmony/.../WebSocketManager.ets`)**:`connect()` 替换已有连接时,旧 socket 的异步 `on('close')` 回调仍会触发 `scheduleReconnect()`,导致连接抖动。现已通过捕获 ws 实例引用,在陈旧回调中跳过重连逻辑。
|
|
119
|
+
- **Android WebSocket 连接泄漏修复 (`clients/android/.../WebSocketClient.kt`)**:`connect()` 未关闭已有 WebSocket 连接,在重连场景下旧连接资源泄漏。现已在 `connect()` 开头主动关闭并清理旧连接。
|
|
120
|
+
- **Android 协程作用域泄漏修复 (`clients/android/.../ChatRepository.kt` / `ChatViewModel.kt`)**:`reconnect()` 创建新 `ChatRepository` 但未取消旧实例的协程作用域,导致协程泄漏。现已新增 `close()` 方法并在重连时调用。
|
|
121
|
+
- **协议层冗余代码清理 (`packages/protocol/src/index.js`)**:`formatDuration` 函数前两个条件分支完全相同,移除冗余的 `ms < 1000` 分支。
|
|
122
|
+
- **HarmonyOS 冗余代码清理 (`clients/harmony/.../Index.ets`)**:同步移除 `formatDuration` 中相同的冗余分支。
|
|
123
|
+
- **Web 端 Token 泄漏防护 (`clients/web/public/app.js`)**:从 URL 提取 auth token 后立即使用 `history.replaceState` 清除 URL 中的 token 参数,防止通过 Referer 头、截图或分享链接泄漏。
|
|
124
|
+
- **Web 端代码围栏匹配修复 (`clients/web/public/app.js`)**:`renderMarkdown` 中 ```` 围栏匹配改为仅在行首匹配,避免将行内三反引号误判为代码围栏。
|
|
125
|
+
- **Web 端 SVG 元素命名空间修复 (`clients/web/public/app.js`)**:DOM 辅助函数 `el()` 对 SVG 标签使用 `createElementNS` 创建,修复 SVG 图标渲染问题。
|
|
126
|
+
- **Web 端发送失败处理增强 (`clients/web/public/app.js`)**:`sendWs` 返回布尔值,`submitPrompt` 在 WebSocket 断开时回退流式状态并提示用户,`setComposerAborting(true)` 正确设置 `state.aborting`。
|
|
127
|
+
- **Android API 基址规范化 (`clients/android/.../ApiService.kt`)**:构造函数中自动去除 `baseUrl` 尾部斜杠,避免拼接出双斜杠 URL。
|
|
128
|
+
|
|
129
|
+
### Changed
|
|
130
|
+
- 全端版本号统一递增至 2.10.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## [2.10.0] - 2026-09-02
|
|
134
|
+
|
|
135
|
+
### Added & Aligned
|
|
136
|
+
- **Android 原生客户端与移动 Web 端全要素对齐 (`clients/android`)**:
|
|
137
|
+
- **顶部栏元素完整对齐 (TopBar Alignment)**:
|
|
138
|
+
- 新增 **工作目录胶囊按钮 (CWD Pill)**:展示当前工作目录,点击弹出工作目录切换对话框(包含路径输入与 `~`, `~/.pi`, `/tmp` 快捷芯片)。
|
|
139
|
+
- 新增 **模型选择胶囊按钮 (Model Pill)**:展示当前模型名称与 `★ 默认` 徽章,点击弹出支持搜索、厂商归类、能力标签(🧠 推理、👁️ 视觉、🛠️ 工具)的模型选择对话框。
|
|
140
|
+
- 新增 **深度思考级别胶囊按钮 (Thinking Pill)**:展示当前思考深度,点击弹出 `Off`、`Minimal`、`Low`、`Medium`、`High`、`Max` 等推理级别选择对话框。
|
|
141
|
+
- 新增 **会话导出按钮 (Export Chat)**:一键将完整对话导出为标准 Markdown 格式并唤起系统分享。
|
|
142
|
+
- **设置入口单点统一**:移除侧边抽屉顶部多余的重复设置按钮,保留顶部栏统一单一设置入口,避免操作歧义。
|
|
143
|
+
- **空白引导页全要素对齐 (Empty State Alignment)**:
|
|
144
|
+
- 引入 `π` 经典头像与提示文案。
|
|
145
|
+
- 引入 **当前模型信息横幅 (Empty Model Banner)**:展示当前模型名称、默认徽章、🧠 思考 / 👁️ 视觉 / 🛠️ 工具能力标签及“切换模型”快捷按钮。
|
|
146
|
+
- 引入 **快捷指令芯片 (Suggestion Chips)**:提供“列出当前目录文件”、“总结这个项目”、“代码审查”等一键触发指令。
|
|
147
|
+
- **底部输入区域与富文本展示对齐 (Composer & Message Bubbles)**:
|
|
148
|
+
- 增加待发送附件预览栏(带缩略图与删除 `✕` 按钮)。
|
|
149
|
+
- 增加图片全屏查看画廊模式 (Image Lightbox Modal)。
|
|
150
|
+
- 助手回复内容 Markdown 解析优化,代码块增加语言标签与“复制代码”独立按钮。
|
|
151
|
+
- 增加底部操作提示文案:“pi 会执行命令与读写你的文件 —— 请注意操作内容。”
|
|
152
|
+
|
|
153
|
+
### Changed
|
|
154
|
+
- 全端版本号统一递增至 2.10.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## [2.9.1] - 2026-09-02
|
|
159
|
+
|
|
160
|
+
### Fixed & Optimized
|
|
161
|
+
- **网关路由健壮性增强 (`server/src/routes.js`)**:修复 `/api/sessions` 历史会话列表中若存在数值格式时间戳导致 `localeCompare` 抛错的问题,优化为安全兼容数字与 ISO 字符串的多格式时间戳倒序排列。
|
|
162
|
+
- **Web 前端异常流式保护 (`clients/web/public/app.js`)**:在 WebSocket 消息处理中补充对 `error` 与 `pi_exit` 事件的完备响应,遇到异常时自动终结流式状态、解除输入框锁定并提示用户。
|
|
163
|
+
- **Android 原生端异常退出流式重置 (`clients/android`)**:`ChatRepository.kt` 补充对 `error` 和 `pi_exit` 事件的处理,避免在子进程异常退出时状态滞留在流式中。
|
|
164
|
+
- **HarmonyOS 原生端异常流式重置 (`clients/harmony`)**:`ChatViewModel.ets` 补充对 `error` 和 `pi_exit` 事件的处理,确保鸿蒙界面在异常中断时能正常完成并释放输入。
|
|
165
|
+
- **跨端通信协议 Schema 完备性更新 (`packages/protocol/src/schema.json`)**:补充 `remote_user_steer` 与 `extension_ui_request` 到服务端消息类型枚举定义。
|
|
166
|
+
|
|
167
|
+
### Changed
|
|
168
|
+
- 全端版本号统一递增至 2.9.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
10
172
|
## [2.9.0] - 2026-09-02
|
|
11
173
|
|
|
12
174
|
### Added
|
|
@@ -624,4 +786,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
624
786
|
| **Deprecated** | 即将移除 |
|
|
625
787
|
| **Removed** | 已移除 |
|
|
626
788
|
| **Fixed** | Bug 修复 |
|
|
627
|
-
| **Security** | 安全相关修复 |
|
|
789
|
+
| **Security** | 安全相关修复 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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.12.2",
|
|
46
|
+
"@liguoshuai/pi-chat-protocol": "2.12.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;
|
|
@@ -552,14 +560,12 @@ function formatMessageTime(ts) {
|
|
|
552
560
|
// ---- Duration Formatter ----
|
|
553
561
|
function formatDuration(ms) {
|
|
554
562
|
if (ms == null || isNaN(ms) || ms < 0) return "";
|
|
555
|
-
|
|
563
|
+
const totalSecs = Math.round(ms / 1000);
|
|
564
|
+
if (totalSecs < 60) {
|
|
556
565
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
557
566
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
const mins = Math.floor(ms / 60000);
|
|
562
|
-
const secs = Math.round((ms % 60000) / 1000);
|
|
567
|
+
const mins = Math.floor(totalSecs / 60);
|
|
568
|
+
const secs = totalSecs % 60;
|
|
563
569
|
return `${mins}m ${secs}s`;
|
|
564
570
|
}
|
|
565
571
|
|
|
@@ -733,7 +739,7 @@ function startNewSession() {
|
|
|
733
739
|
state.streamingItems = [];
|
|
734
740
|
state.streamingMsg = null;
|
|
735
741
|
state.activeToolCalls.clear();
|
|
736
|
-
|
|
742
|
+
setComposerStreaming(false);
|
|
737
743
|
try {
|
|
738
744
|
window.history.replaceState({}, "", window.location.pathname);
|
|
739
745
|
} catch {}
|
|
@@ -853,7 +859,7 @@ async function loadSession(file) {
|
|
|
853
859
|
state.streamingItems = [];
|
|
854
860
|
state.streamingMsg = null;
|
|
855
861
|
state.activeToolCalls.clear();
|
|
856
|
-
|
|
862
|
+
setComposerStreaming(false);
|
|
857
863
|
try {
|
|
858
864
|
const newUrl = window.location.pathname + "?session=" + encodeURIComponent(file);
|
|
859
865
|
window.history.replaceState({ session: file }, "", newUrl);
|
|
@@ -1939,8 +1945,9 @@ function connectWs(opts = {}) {
|
|
|
1939
1945
|
}
|
|
1940
1946
|
|
|
1941
1947
|
function sendWs(obj) {
|
|
1942
|
-
if (!state.ws || state.ws.readyState !== 1) return;
|
|
1948
|
+
if (!state.ws || state.ws.readyState !== 1) return false;
|
|
1943
1949
|
state.ws.send(JSON.stringify(obj));
|
|
1950
|
+
return true;
|
|
1944
1951
|
}
|
|
1945
1952
|
|
|
1946
1953
|
function handlePiMessage(obj) {
|
|
@@ -1972,7 +1979,7 @@ function handlePiMessage(obj) {
|
|
|
1972
1979
|
// After replay, sync the composer / streaming state to what the server thinks.
|
|
1973
1980
|
if (obj.streaming) {
|
|
1974
1981
|
state.streaming = true;
|
|
1975
|
-
|
|
1982
|
+
setComposerStreaming(true);
|
|
1976
1983
|
ensureStreamingMsg();
|
|
1977
1984
|
refreshStreamingContent();
|
|
1978
1985
|
// If ring buffer overflowed while client was away, sync full session history in background
|
|
@@ -1983,7 +1990,7 @@ function handlePiMessage(obj) {
|
|
|
1983
1990
|
finalizeStreamingMsg();
|
|
1984
1991
|
state.streaming = false;
|
|
1985
1992
|
state.aborting = false;
|
|
1986
|
-
|
|
1993
|
+
setComposerStreaming(false);
|
|
1987
1994
|
if (state.currentSessionFile) {
|
|
1988
1995
|
syncSessionHistory(state.currentSessionFile, true);
|
|
1989
1996
|
}
|
|
@@ -2031,7 +2038,7 @@ function handlePiMessage(obj) {
|
|
|
2031
2038
|
}
|
|
2032
2039
|
} else if (obj.command === "abort") {
|
|
2033
2040
|
state.aborting = false;
|
|
2034
|
-
|
|
2041
|
+
setComposerStreaming(false);
|
|
2035
2042
|
}
|
|
2036
2043
|
else if (obj.command === "switch_session" && obj.success) {
|
|
2037
2044
|
// ask pi for current state so we can get session id, name
|
|
@@ -2067,7 +2074,7 @@ function handlePiMessage(obj) {
|
|
|
2067
2074
|
case "agent_start":
|
|
2068
2075
|
state.streaming = true;
|
|
2069
2076
|
state.aborting = false;
|
|
2070
|
-
|
|
2077
|
+
setComposerStreaming(true);
|
|
2071
2078
|
ensureStreamingMsg();
|
|
2072
2079
|
refreshStreamingContent();
|
|
2073
2080
|
refreshSessions();
|
|
@@ -2079,7 +2086,7 @@ function handlePiMessage(obj) {
|
|
|
2079
2086
|
finalizeStreamingMsg();
|
|
2080
2087
|
state.streaming = false;
|
|
2081
2088
|
state.aborting = false;
|
|
2082
|
-
|
|
2089
|
+
setComposerStreaming(false);
|
|
2083
2090
|
sendWs({ type: "get_state" });
|
|
2084
2091
|
refreshSessions(); // titles may have changed
|
|
2085
2092
|
break;
|
|
@@ -2265,11 +2272,18 @@ function handlePiMessage(obj) {
|
|
|
2265
2272
|
case "extension_ui_request":
|
|
2266
2273
|
handleExtensionUiRequest(obj);
|
|
2267
2274
|
break;
|
|
2275
|
+
case "error":
|
|
2276
|
+
finalizeStreamingMsg();
|
|
2277
|
+
state.streaming = false;
|
|
2278
|
+
state.aborting = false;
|
|
2279
|
+
setComposerStreaming(false);
|
|
2280
|
+
showToast(`⚠️ ${obj.message || obj.code || "发生错误"}`);
|
|
2281
|
+
break;
|
|
2268
2282
|
case "pi_exit":
|
|
2269
2283
|
finalizeStreamingMsg();
|
|
2270
2284
|
state.streaming = false;
|
|
2271
2285
|
state.aborting = false;
|
|
2272
|
-
|
|
2286
|
+
setComposerStreaming(false);
|
|
2273
2287
|
$("#connDot").style.color = "var(--danger)";
|
|
2274
2288
|
break;
|
|
2275
2289
|
default:
|
|
@@ -2358,14 +2372,14 @@ function updateState(d) {
|
|
|
2358
2372
|
if (d.isStreaming) {
|
|
2359
2373
|
if (!state.streaming) {
|
|
2360
2374
|
state.streaming = true;
|
|
2361
|
-
|
|
2375
|
+
setComposerStreaming(true);
|
|
2362
2376
|
ensureStreamingMsg();
|
|
2363
2377
|
}
|
|
2364
2378
|
} else if (state.streaming && !state.isBackfilling) {
|
|
2365
2379
|
finalizeStreamingMsg();
|
|
2366
2380
|
state.streaming = false;
|
|
2367
2381
|
state.aborting = false;
|
|
2368
|
-
|
|
2382
|
+
setComposerStreaming(false);
|
|
2369
2383
|
refreshSessions();
|
|
2370
2384
|
}
|
|
2371
2385
|
}
|
|
@@ -2925,9 +2939,12 @@ function updateComposerUI() {
|
|
|
2925
2939
|
}
|
|
2926
2940
|
}
|
|
2927
2941
|
|
|
2928
|
-
function
|
|
2942
|
+
function setComposerStreaming(yes) {
|
|
2943
|
+
// Entering/exiting the streaming state must never turn on the "aborting"
|
|
2944
|
+
// flag — otherwise the composer would lock and show "正在中止…" while the
|
|
2945
|
+
// model is actually just running. Abort is a separate transient state.
|
|
2946
|
+
state.aborting = false;
|
|
2929
2947
|
if (!yes) {
|
|
2930
|
-
state.aborting = false;
|
|
2931
2948
|
const inner = $("#composerInner");
|
|
2932
2949
|
if (inner) inner.classList.remove("aborting");
|
|
2933
2950
|
const hint = $(".composer-hint");
|
|
@@ -2951,7 +2968,12 @@ function abortGeneration() {
|
|
|
2951
2968
|
updateComposerUI();
|
|
2952
2969
|
const hint = $(".composer-hint");
|
|
2953
2970
|
if (hint) hint.textContent = "中止当前任务中…";
|
|
2954
|
-
sendWs({ type: "abort" })
|
|
2971
|
+
if (!sendWs({ type: "abort" })) {
|
|
2972
|
+
state.aborting = false;
|
|
2973
|
+
updateComposerUI();
|
|
2974
|
+
if (hint) hint.textContent = "中止失败:WebSocket 连接已断开。正在尝试重连…";
|
|
2975
|
+
scheduleReconnect(0);
|
|
2976
|
+
}
|
|
2955
2977
|
}
|
|
2956
2978
|
|
|
2957
2979
|
function submitSteer() {
|
|
@@ -2966,6 +2988,12 @@ function submitSteer() {
|
|
|
2966
2988
|
return;
|
|
2967
2989
|
}
|
|
2968
2990
|
|
|
2991
|
+
if (!sendWs({ type: "steer", message: text })) {
|
|
2992
|
+
if (hint) hint.textContent = "发送失败:WebSocket 连接已断开。正在尝试重连…";
|
|
2993
|
+
scheduleReconnect(0);
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2969
2997
|
appendMessageNode("user", { text, isSteer: true, ts: Date.now() });
|
|
2970
2998
|
ta.value = "";
|
|
2971
2999
|
autoResize();
|
|
@@ -2975,8 +3003,6 @@ function submitSteer() {
|
|
|
2975
3003
|
setTimeout(() => {
|
|
2976
3004
|
if (hint) hint.textContent = "pi 会执行命令与读写你的文件 —— 请注意操作内容。";
|
|
2977
3005
|
}, 4000);
|
|
2978
|
-
|
|
2979
|
-
sendWs({ type: "steer", message: text });
|
|
2980
3006
|
}
|
|
2981
3007
|
|
|
2982
3008
|
function submitPrompt() {
|
|
@@ -3023,7 +3049,7 @@ function submitPrompt() {
|
|
|
3023
3049
|
|
|
3024
3050
|
state.streaming = true;
|
|
3025
3051
|
state.aborting = false;
|
|
3026
|
-
|
|
3052
|
+
setComposerStreaming(true);
|
|
3027
3053
|
ensureStreamingMsg(now);
|
|
3028
3054
|
refreshStreamingContent();
|
|
3029
3055
|
|
|
@@ -3053,7 +3079,19 @@ function submitPrompt() {
|
|
|
3053
3079
|
}
|
|
3054
3080
|
}
|
|
3055
3081
|
}
|
|
3056
|
-
sendWs({ type: "prompt", message: text, images: imagesToSend })
|
|
3082
|
+
if (!sendWs({ type: "prompt", message: text, images: imagesToSend })) {
|
|
3083
|
+
state.streaming = false;
|
|
3084
|
+
state.aborting = false;
|
|
3085
|
+
setComposerStreaming(false);
|
|
3086
|
+
stopStreamingTimer();
|
|
3087
|
+
state.turnStartedAt = null;
|
|
3088
|
+
state.streamingMsgDurationEl = null;
|
|
3089
|
+
if (state.streamingMsg) { state.streamingMsg.remove(); state.streamingMsg = null; }
|
|
3090
|
+
state.streamingItems = [];
|
|
3091
|
+
const hint = $(".composer-hint");
|
|
3092
|
+
if (hint) hint.textContent = "发送失败:WebSocket 连接已断开。正在尝试重连…";
|
|
3093
|
+
scheduleReconnect(0);
|
|
3094
|
+
}
|
|
3057
3095
|
}
|
|
3058
3096
|
|
|
3059
3097
|
function autoResize() {
|