@liguoshuai/pi-web-chat 1.6.0 → 1.7.1

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
@@ -8,39 +8,135 @@
8
8
 
9
9
  ---
10
10
 
11
- ## 📸 功能
12
-
13
- - 💬 **流式对话** —— 文本逐字流式渲染,带打字光标
14
- - 🗂️ **会话历史** —— 自动读取 `~/.pi/agent/sessions/` 下所有历史会话,按首条用户消息做标题
15
- - 🆕 **新建 / 切换会话** —— 通过左侧栏点击或生成中新建
16
- - ⚙️ **工具调用折叠块** —— bash / read / write / edit 等以可折叠卡片显示,实时输出
17
- - 💭 **思考过程折叠块** —— 模型 thinking 以独立可折叠卡片显示
18
- - 🧩 **模型切换** —— 顶栏模型 pill,覆盖所有 pi 已配置的模型
19
- - ⏹️ **中止生成** —— 生成中点击发送按钮即可中断
20
- - ⌨️ **Markdown 渲染** —— 代码块、表格、列表、链接、引用、标题
21
- - 🖥️ **工作目录绑定** —— 每个 ws 连接由 cwd 决定会话范围(默认 `~`)
11
+ ## 📸 功能特性
12
+
13
+ - 💬 **流式对话** —— 文本逐字流式渲染,带打字光标与 Markdown 高亮
14
+ - **后台任务持久化** —— 关闭网页/标签页不中断正在生成的任务,重新进入自动回放(Backfill)追平进度
15
+ - 🌐 **多端/多设备同步协同** —— 多个浏览器标签页或多设备(手机/电脑)可同时连入同一会话,实时双向同步
16
+ - 🧭 **运行时实时插入指令 (Steer)** —— 生成过程中可随时插入转向指令指导 AI 调整后续行动
17
+ - 🗂️ **会话历史与管理** —— 自动读取并显示 `~/.pi/agent/sessions/` 下所有历史会话,实时自动落盘
18
+ - ⚙️ **工具与思考过程折叠** —— bash / read / write / edit 等工具调用与 thinking 思考卡片实时展开/收起
19
+ - 🧩 **动态模型切换** —— 顶栏模型胶囊,零延迟覆盖所有 pi 已配置的模型
20
+ - 🔄 **断线自动重连与心跳** —— 带指数退避自动重连、双向心跳 Ping/Pong 检测
21
+ - 📋 **快捷复制** —— 支持代码块、工具指令、回答全文一键复制
22
+ - 📱 **移动端响应式** —— Viewport `100dvh` 优化、顶栏常驻固定、滚动置顶/置底快捷悬浮按钮(FAB)
22
23
 
23
24
  ---
24
25
 
25
26
  ## 🚀 快速开始
26
27
 
28
+ ### 1. 环境准备
29
+ - **Node.js**: `>= 18.0.0`
30
+ - **操作系统**: Linux / macOS / WSL (Windows)
31
+
32
+ ---
33
+
34
+ ### 2. 安装 pi 编程代理 (pi agent)
35
+
36
+ `pi-web-chat` 通过 RPC 模式 (`pi --mode rpc`) 与底层 `pi` 命令行 Agent 子进程通信。若系统中尚未安装 `pi`,请选择以下任一方式进行全局安装:
37
+
38
+ #### 方式 A:通过 npm / pnpm 全局安装(推荐)
39
+
40
+ ```bash
41
+ npm install -g --ignore-scripts @earendil-works/pi-coding-agent
42
+ ```
43
+ > 💡 `--ignore-scripts` 可跳过依赖包中的生命周期脚本,安装更干净高效。
44
+
45
+ #### 方式 B:通过 Shell 官方安装脚本
46
+
47
+ ```bash
48
+ curl -fsSL https://pi.dev/install.sh | sh
49
+ ```
50
+
51
+ #### 验证安装
52
+ 安装完成后,在终端运行以下命令验证:
53
+
27
54
  ```bash
55
+ pi --version
56
+ # 输出版本号(例如 0.82.1)即表示安装成功
57
+ ```
58
+
59
+ ---
60
+
61
+ ### 3. 配置模型 Provider 与 API Key
62
+
63
+ `pi` 支持 Anthropic、OpenAI、OpenRouter、DeepSeek、SiliconFlow 等多种 LLM 模型 Provider。在使用前需配置好 API Key 或授权凭证:
64
+
65
+ #### 方法 A:设置环境变量(推荐)
66
+ 在终端或 Shell 配置文件(如 `~/.bashrc` 或 `~/.zshrc`)中导出对应的 API Key:
67
+
68
+ ```bash
69
+ # 使用 Anthropic (Claude)
70
+ export ANTHROPIC_API_KEY="sk-ant-..."
71
+
72
+ # 使用 OpenRouter
73
+ export OPENROUTER_API_KEY="sk-or-v1-..."
74
+
75
+ # 使用 OpenAI / DeepSeek / 其它 OpenAI 兼容 API
76
+ export OPENAI_API_KEY="sk-..."
77
+ ```
78
+
79
+ #### 方法 B:使用 pi CLI 交互式登录
80
+ 在终端输入 `pi` 命令启动交互模式,然后输入 `/login` 按照提示选择 Provider 并绑定账号或密钥:
81
+
82
+ ```bash
83
+ pi
84
+ # 进入交互界面后输入:
85
+ /login
86
+ # 按照提示选择 Provider 并输入 Key,配置完成后输入 /quit 退出
87
+ ```
88
+
89
+ ---
90
+
91
+ ### 4. 克隆与启动 pi-web-chat
92
+
93
+ #### 克隆仓库与安装依赖
94
+
95
+ ```bash
96
+ git clone https://github.com/liguoshuai-1990/pi-web-chat.git
28
97
  cd pi-web-chat
29
98
  npm install
99
+ ```
30
100
 
31
- # 确保 pi 已安装并已配置好至少一个 provider/model:
32
- # pi (交互模式 → 运行 /login 选择 provider,或设置 API key)
101
+ #### 启动 Web 服务
102
+
103
+ ```bash
33
104
  npm start
34
- # → http://localhost:3000
35
105
  ```
36
106
 
37
- ### 环境变量
107
+ #### 打开浏览器体验
108
+ 在浏览器中访问:
109
+ 👉 **http://localhost:3000**
38
110
 
39
- | 变量 | 默认值 | 说明 |
40
- | -------------------- | --------------------------------------- | --------------------------- |
41
- | `PORT` | `3000` | Web 服务监听端口 |
42
- | `PI_BIN` | 自动探测(`~/.npm-global/bin/pi` 等) | 显式指定 pi 可执行文件路径 |
43
- | `PI_SESSIONS_DIR` | `~/.pi/agent/sessions` | pi 的 session 存储目录 |
111
+ ---
112
+
113
+ ### 5. 命令行启动选项与全局 CLI(可选)
114
+
115
+ 你也可以通过 `bin/pi-web-chat.js` 指定工作目录或监听端口:
116
+
117
+ ```bash
118
+ # 指定端口和工作目录 (cwd)
119
+ node bin/pi-web-chat.js --port 8080 --cwd /path/to/your/project
120
+
121
+ # 或通过 npm 全局安装后在任意目录下启动
122
+ npm install -g .
123
+ pi-web-chat --port 8080
124
+ ```
125
+
126
+ ---
127
+
128
+ ## ⚙️ 环境变量配置
129
+
130
+ | 变量 | 默认值 | 说明 |
131
+ | ---- | ------ | ---- |
132
+ | `PORT` | `3000` | Web 服务监听端口 |
133
+ | `PI_BIN` | 自动探测(`~/.npm-global/bin/pi`、`/usr/local/bin/pi` 或 `PATH`) | 显式指定 pi 可执行文件绝对路径 |
134
+ | `PI_SESSIONS_DIR` | `~/.pi/agent/sessions` | pi 的 session 存储目录 |
135
+ | `IDLE_TIMEOUT_MS` | `300000` (5分钟) | 真正空闲(无连接+非流式)后的进程回收超时(0 为禁用回收) |
136
+ | `MAX_AGENT_LIFETIME_MS` | `1800000` (30分钟) | 单个 Agent 进程后台生存硬上限(0 为无上限) |
137
+ | `EVENT_BUFFER_SIZE` | `2000` | 离线环形 Buffer 允许缓存的最大事件条数 |
138
+ | `MAX_CONCURRENT_AGENTS` | `0` (无限制) | 进程池最大并发 Agent 进程数量 |
139
+ | `IDLE_DROP_HEAP` | `false` | 进入空闲时是否给 V8 引擎 GC 提示(需 `--expose-gc`) |
44
140
 
45
141
  ---
46
142
 
@@ -49,31 +145,38 @@ npm start
49
145
  ```
50
146
  pi-web-chat/
51
147
  ├── README.md 本文件
52
- ├── DESIGN.md 设计文档(架构、数据流、决策)
53
- ├── ISSUES.md 历次问题排查与修复
54
- ├── CHANGELOG.md 版本变更日志
55
148
  ├── package.json
56
149
  ├── package-lock.json
57
- ├── server.js Express + WebSocket,桥接 pi RPC
58
- ├── .gitignore
59
- └── public/
60
- ├── index.html 单页 UI
61
- ├── app.js 前端逻辑
62
- └── style.css ChatGPT/Gemini 风格样式
150
+ ├── server.js Express + WebSocket,桥接 pi RPC 与进程池管理
151
+ ├── bin/
152
+ └── pi-web-chat.js CLI 可执行入口
153
+ ├── public/
154
+ ├── index.html 单页 UI
155
+ │ ├── app.js 前端逻辑与状态机
156
+ │ └── style.css ChatGPT/Gemini 风格样式
157
+ └── docs/ 项目文档库
158
+ ├── ARCHITECTURE.md 架构设计文档
159
+ ├── DESIGN.md 详细设计与决策文档
160
+ ├── ISSUES.md 历次问题排查与修补记录
161
+ └── CHANGELOG.md 版本变更日志
63
162
  ```
64
163
 
65
164
  ---
66
165
 
67
- ## 🏗️ 架构
166
+ ## 🏗️ 架构概览
68
167
 
69
168
  ```
70
- 浏览器 ──WebSocket(/ws?cwd=...&session=...)──► Node server.js ──stdin/stdout(JSONL)──► pi --mode rpc
71
- (一个 ws 连接 spawn 一个 pi 子进程)
72
-
73
- ├──REST /api/sessions ──► 直读 JSONL 列历史
74
- └──REST /api/session?file=... ─► 重建根→叶路径
169
+ 浏览器 (多 Tab / 多设备) ──WebSocket(/ws?cwd=...&session=...)──► Node server.js
170
+ (Session Key 进程池 activeAgents)
171
+
172
+ ├──► pi --mode rpc (后台持久化 Worker)
173
+ ├──► REST /api/sessions ──► 列会话历史
174
+ ├──► REST /api/session ────► 构建对话链
175
+ └──► REST /api/agents ─────► 实时监控看板
75
176
  ```
76
177
 
178
+ 详细架构说明与设计决策请参阅 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) 与 [docs/DESIGN.md](docs/DESIGN.md)。
179
+
77
180
  ---
78
181
 
79
182
  ## 📝 License
@@ -94,11 +197,6 @@ MIT
94
197
  ./scripts/install-service.sh 8080
95
198
  ```
96
199
 
97
- 脚本会:
98
- 1. 在 `~/.config/systemd/user/pi-web-chat.service` 生成 unit
99
- 2. `systemctl --user daemon-reload && systemctl --user enable --now pi-web-chat`
100
- 3. 设置 `Restart=on-failure` 自动重启
101
-
102
200
  查看状态 / 日志:
103
201
  ```bash
104
202
  systemctl --user status pi-web-chat
@@ -110,7 +208,7 @@ journalctl --user -u pi-web-chat -f
110
208
  > sudo loginctl enable-linger $USER
111
209
  > ```
112
210
 
113
- 卸载:
211
+ 卸载服务:
114
212
  ```bash
115
213
  systemctl --user disable --now pi-web-chat
116
214
  rm ~/.config/systemd/user/pi-web-chat.service
@@ -5,6 +5,7 @@
5
5
  import { fileURLToPath } from "url";
6
6
  import { dirname, resolve } from "path";
7
7
  import { spawn } from "child_process";
8
+ import os from "os";
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -49,7 +50,16 @@ function parseArgs(argv) {
49
50
  return opts;
50
51
  }
51
52
 
53
+ function normalizeCwd(dir) {
54
+ if (!dir) return os.homedir();
55
+ if (dir.startsWith("~")) {
56
+ return resolve(os.homedir(), dir.slice(1).replace(/^[/\\]/, ""));
57
+ }
58
+ return resolve(dir);
59
+ }
60
+
52
61
  const opts = parseArgs(process.argv.slice(2));
62
+ opts.cwd = normalizeCwd(opts.cwd);
53
63
 
54
64
  // Spawn server.js as a child so we can forward signals cleanly.
55
65
  // Use --expose-gc so idle agents can call global.gc() to release heap (IDLE_DROP_HEAP=1).
@@ -0,0 +1,180 @@
1
+ # 架构设计文档
2
+
3
+ > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v1.7.0 版本)。
4
+
5
+ ---
6
+
7
+ ## 1. 高层架构
8
+
9
+ ```
10
+ ┌─────────────────┐ WebSocket (JSON) ┌──────────────────────┐
11
+ │ Browser Tab A │ ◄──────────────────────────────────────► │ │
12
+ └─────────────────┘ │ │
13
+ ┌─────────────────┐ │ Node.js server │
14
+ │ Browser Tab B │ ◄──────────────────────────────────────► │ (server.js) │
15
+ └─────────────────┘ │ │
16
+ │ ┌────────────────┐ │
17
+ │ │ PiAgent Pool │ │
18
+ │ │ (activeAgents) │ │
19
+ │ └───────┬────────┘ │
20
+ └──────────┼───────────┘
21
+
22
+ spawn child_process │ (1 agent per session key)
23
+
24
+ ┌──────────────────────┐
25
+ │ pi --mode rpc │
26
+ │ (后台 Agent 子进程) │
27
+ │ stdin/stdout │
28
+ └──────────┬───────────┘
29
+
30
+ ┌──────────────────┼──────────────────┐
31
+ ▼ ▼ ▼
32
+ ~/.pi/agent/sessions/ ~/.pi/agent/ 配置/扩展/
33
+ --home-zrlgs--/ extensions/ skills/
34
+
35
+
36
+ *.jsonl (append-only tree)
37
+ ```
38
+
39
+ **核心设计原则**:
40
+ - **按 Session Key 池化进程**:以 `${cwd}:${resolvedSessionPath}` 作为唯一 Key,多标签页或多设备连接同一个 Session 时共享同一个后台 `PiAgent` 进程。
41
+ - **后台持久化运行(Headless Persistence)**:网页断开(如关闭标签页)不会立即终止进程。只要 Agent 处于流式生成或有未响应请求状态,后台进程会坚持跑完当前任务。
42
+ - **离线事件环形缓存与回放(Ring Buffer & Backfill)**:无客户端连接时,pi 输出的事件自动存入环形 Buffer(默认 2000 条)。客户端重连时通过 `backfill_start` → 离线事件 → `backfill_end` 进行增量补齐。
43
+ - **真正空闲回收(True-Idle Timeout)**:仅在“无 WebSocket 连接”且“进程彻底进入 idle(非 streaming、无挂起 RPC)”时,才启动 `IDLE_TIMEOUT_MS` 空闲回收倒计时。
44
+ - **磁盘 JSONL 为唯一真理(Single Source of Truth)**:会话历史与树状分支全由 pi 本身维护在磁盘 `.jsonl` 文件中,Web 端通过 REST 接口与 RPC 消息与文件保持同步。
45
+ - **`pi` 可执行文件自动探查与派生**:服务端通过 `resolvePiBin()` 自动定位全局安装的 `pi` 可执行文件(优先使用 `PI_BIN` 环境变量 -> 检查 `~/.npm-global/bin/pi` -> `/usr/local/bin/pi` -> `/usr/bin/pi` -> 系统 `PATH`),派生 `pi --mode rpc --session-dir ...` 子进程。
46
+
47
+ ---
48
+
49
+ ## 2. 关键数据流
50
+
51
+ ### 2.1 打开已有会话与增量回放(Backfill)
52
+
53
+ ```
54
+ Browser server.js PiAgent (RPC)
55
+ │ │ │
56
+ ├─ GET /api/session?file=X ──────►│ 读 JSONL 主干消息 │
57
+ │◄─── { header, entries[] }──────┤ │
58
+ │ │ │
59
+ ├─ 渲染历史消息 │ │
60
+ │ │ │
61
+ ├─ WS /ws?cwd=...&session=X ─────►│ 查找/创建 activeAgents[Key] │
62
+ │ ├─► attachWs(ws) │
63
+ │ ├─► 发现有离线缓存事件 │
64
+ │◄─── { type: "backfill_start" }─┤ │
65
+ │◄─── N 条离线事件 (text_delta…) │ (将离线事件批量回放给当前 ws) │
66
+ │◄─── { type: "backfill_end" }───┤ │
67
+ │ │ │
68
+ ├─ 矫正当前 streaming 与 Composer 状态 │
69
+ ```
70
+
71
+ ### 2.2 新建会话与发送消息
72
+
73
+ ```
74
+ Browser server.js pi RPC 子进程
75
+ │ │ │
76
+ ├─ WS /ws?cwd=... (无 session) │ spawn pi --mode rpc │
77
+ │ │◄─ pi 创建新 session jsonl │
78
+ │◄─── ws open (状态记为已连接) │ │
79
+ │ │ │
80
+ ├─ sendWs({type:"prompt", ...})─►│────────────────────────────────►│
81
+ │ │ ├─ agent_start (state=streaming)
82
+ │◄─── message_update/text_delta ─┼◄────────────────────────────────┤
83
+ │◄─── agent_settled │◄────────────────────────────────┤ (state=idle)
84
+ │ │ ├─ session 文件实时追加
85
+ ```
86
+
87
+ ### 2.3 中途插话 / 转向(Mid-turn Steering)
88
+
89
+ ```
90
+ Browser server.js pi RPC 子进程
91
+ │ │ │
92
+ ├─ Agent 处于 streaming 状态 ────┼─────────────────────────────────┤ (正在执行长任务/工具)
93
+ ├─ sendWs({type:"steer", ...})──►│────────────────────────────────►│
94
+ │ │ ├─ 注入 steer 命令
95
+ │◄─── agent 收到并调整后续思考 ────┼◄────────────────────────────────┤
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 3. 关键模块职责
101
+
102
+ ### 3.1 `server.js`
103
+
104
+ | 模块/函数 | 职责 |
105
+ |-----------|------|
106
+ | `PiAgent` 类 | 管理单个 `pi --mode rpc` 子进程:维护状态(`idle`/`streaming`)、离线事件 Buffer、挂起请求表、真·空闲回收定时器与硬生存上限定时器。 |
107
+ | `activeAgents` Map | 以 `${cwd}:${sessionFile}` 为 Key 的进程池,实现多端/多标签页共享。 |
108
+ | `GET /api/sessions` | 扫描 `~/.pi/agent/sessions/**/*.jsonl`,读取 header 做 cwd 过滤,按更新时间排序返回。 |
109
+ | `GET /api/session` | 读取单个 `.jsonl` 文件,按 parentId 关系重构叶子到根节点的标准对话链。 |
110
+ | `GET /api/agents` | 暴露后台活跃 Agent 的状态信息(运行状态、客户端数量、存活时长、离线 Buffer 消息数、最近提示词等)。 |
111
+ | `WebSocketServer` | 处理连接建立与断开,分发 `prompt`、`steer`、`abort`、`ping` 等指令,并在 `SIGINT`/`SIGTERM` 时进行优雅关闭。 |
112
+
113
+ ### 3.2 `public/app.js`
114
+
115
+ | 模块/函数 | 职责 |
116
+ |-----------|------|
117
+ | `state` | 全局状态对象:维护 `wsConnected`、`streaming`、`isBackfilling`、`activeToolCalls`、`sessionId`、`wsGen` 等。 |
118
+ | `connectWs(opts)` | 管理 WebSocket 生命周期:携带代次 `wsGen` 防止旧消息污染,处理重连逻辑与心跳。 |
119
+ | `handlePiMessage(obj)` | 核心事件分发器:处理 `backfill_start`/`backfill_end`、16 种 pi 事件以及状态同步。 |
120
+ | `ensureStreamingMsg / refreshStreamingContent` | 增量渲染/更新正在流式的文本、思考过程(Thinking)与工具卡片。 |
121
+ | `renderMarkdown` | 自研轻量 Markdown 渲染器(代码块抠出保护、Html Escape、行内语法转换)。 |
122
+
123
+ ### 3.3 `public/style.css`
124
+
125
+ - 暗色与明色响应式样式变量。
126
+ - 左侧 260px 侧边栏与右侧主聊天区自适应布局。
127
+ - 移动端 Fixed 顶栏、Viewport height 100dvh 适配与悬浮置顶按钮(FAB)。
128
+
129
+ ---
130
+
131
+ ## 4. 关键设计决策
132
+
133
+ | 决策点 | 选择 | 理由 |
134
+ |--------|------|------|
135
+ | **进程池粒度** | **Session Key 池化** | 允许同一会话在多设备/多 Tab 间同步,同时保证进程资源不重复浪费。 |
136
+ | **断连处理** | **后台继续跑 + 环形 Buffer 回放** | 解决关闭标签页导致未完成任务断掉的问题;用环形 Buffer 防止长时间离线膨胀内存。 |
137
+ | **回收策略** | **真·空闲倒计时(True-Idle Timeout)** | 区分“无连接”与“真正的空闲”,保障长时间后台生成任务不被中断。 |
138
+ | **通讯协议** | **直接透传 pi 原生 JSONL** | 不做二次包装,保持与 pi RPC 协议同频更新。 |
139
+ | **持久化** | **复用 pi 本身 .jsonl** | 零额外存储依赖,Web 端与终端命令完全同源共享会话。 |
140
+
141
+ ---
142
+
143
+ ## 5. 配置与环境变量
144
+
145
+ | 环境变量 | 默认值 | 说明 |
146
+ |----------|--------|------|
147
+ | `PORT` | `3000` | HTTP 与 WebSocket 监听端口 |
148
+ | `PI_BIN` | 自动探测 | pi 可执行文件绝对路径 |
149
+ | `PI_SESSIONS_DIR` | `~/.pi/agent/sessions` | 会话 JSONL 文件存取目录 |
150
+ | `IDLE_TIMEOUT_MS` | `300000` (5分钟) | 真正空闲(无连接+非流式)后的回收超时(0 为禁用回收) |
151
+ | `MAX_AGENT_LIFETIME_MS` | `1800000` (30分钟) | 单个 Agent 进程后台生存硬上限(0 为无上限) |
152
+ | `EVENT_BUFFER_SIZE` | `2000` | 离线环形 Buffer 允许缓存的最大事件条数 |
153
+ | `MAX_CONCURRENT_AGENTS` | `0` (无限制) | 进程池最大并发 Agent 进程数量 |
154
+ | `IDLE_DROP_HEAP` | `false` | 进入空闲时是否给 V8 引擎 GC 提示(需 `--expose-gc`) |
155
+
156
+ ---
157
+
158
+ ## 6. 目录结构
159
+
160
+ ```
161
+ pi-web-chat/
162
+ ├── README.md # 主说明文档
163
+ ├── package.json
164
+ ├── package-lock.json
165
+ ├── server.js # Express + WebSocket 服务器与 PiAgent 管理
166
+ ├── bin/
167
+ │ └── pi-web-chat.js # 可执行入口
168
+ ├── public/
169
+ │ ├── index.html # 单页 UI
170
+ │ ├── app.js # 前端逻辑与状态机
171
+ │ └── style.css # CSS 样式
172
+ ├── docs/ # 所有文档统一收纳
173
+ │ ├── ARCHITECTURE.md # 架构设计文档(本文件)
174
+ │ ├── DESIGN.md # 详细设计与决策文档
175
+ │ ├── ISSUES.md # 排查与修补记录
176
+ │ └── CHANGELOG.md # 版本变更日志
177
+ └── scripts/ # 服务安装/卸载脚本
178
+ ├── install-service.sh
179
+ └── pi-web-chat.service
180
+ ```
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.0] - 2026-07-26
11
+
12
+ ### Added
13
+ - **前端流式渲染防抖节流 (rAF UI Throttling)**:使用 `requestAnimationFrame` 节流高频 Token/Delta 事件,消除倒水式输出时的 DOM 频繁销毁与全量重塑,大幅提升高频打字时的流畅帧率,显著降低 CPU 和发热消耗。
14
+ - **后端 Session 列表 mtime 内存缓存 (Session Metadata Caching)**:`/api/sessions` 引入基于文件修改时间(`mtimeMs`)的元数据内存缓存。对无改动的历史 Session 文件跳过磁盘读取与全量 JSON 逐行解析,大幅提升侧边栏列表加载响应速度。
15
+ - **离线事件 Buffer 算法优化 (Ring Buffer O(1) Push)**:将 `PiAgent.bufferEvent` 从 $O(N)$ 复杂度的 `Array.shift()` 改为 $O(1)$ 的指针环形队列,消除无客户端连接时的数组平移消耗。
16
+ - **WebSocket 垃圾连接自动回收 (Dead Socket Cleanup)**:在 `wsSend` 广播消息时动态检测并自动剔除已处于关闭状态(`CLOSING`/`CLOSED`)的垃圾 Socket 引用,避免泄露。
17
+
18
+ ---
19
+
10
20
  ## [1.6.0] - 2026-07-26
11
21
 
12
22
  ### Added
@@ -153,14 +163,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
153
163
  - 多模型切换(pi 配置的所有 provider/model)
154
164
  - 响应式深色主题(ChatGPT/Gemini 风格)
155
165
 
156
- ### Technical Debt (已知)
157
- - 图片上传未接
158
- - 多 cwd / 项目切换器未做
159
- - fork / tree / clone 浏览未接
160
- - 浅/深主题切换未做
161
- - 双端实时同步未做
162
- - 鉴权未做
163
-
164
166
  ---
165
167
 
166
168
  ## Legend
package/docs/DESIGN.md ADDED
@@ -0,0 +1,88 @@
1
+ # 详细设计与决策文档
2
+
3
+ ## 目标
4
+
5
+ 将 [pi](https://pi.dev) 终端编码代理包装为一个**类 ChatGPT / Gemini 的 Web 界面**,使用户能够:
6
+
7
+ - 在浏览器中与 pi 展开对话,享受流式文本输出、思考过程卡片、Markdown 渲染与工具调用可视化;
8
+ - 与终端共享会话历史(一份存储、两端可见);
9
+ - 支持后台持久化运行:关闭标签页或切换设备不打断正在生成的任务,重新进入时自动追进度。
10
+
11
+ 非目标:
12
+ - 引入复杂的前端构建链(保持单页原生 JS/CSS,无打包工具);
13
+ - 替代 pi 本身的文件存储层或重新实现数据库。
14
+
15
+ ---
16
+
17
+ ## 核心设计与进程模型
18
+
19
+ ### 进程池化与后台持久化运行
20
+
21
+ ```
22
+ Browser Tab 1 ──┐
23
+ ├── WebSocket ──► server.js (activeAgents[Key]) ──stdio (JSONL)──► pi (rpc mode)
24
+ Browser Tab 2 ──┘
25
+ ```
26
+
27
+ 与早期“1 个 WebSocket = 1 个子进程”不同,当前设计以 **Session Key**(`${cwd}:${resolvedSessionPath}`)作为唯一标识:
28
+
29
+ 1. **多端/多标签页共享**:多个浏览器标签页或多个设备打开同一个 Session 时,附加到同一个 `PiAgent` 实例。
30
+ 2. **后台任务持久化**:关闭网页仅表示断开 WebSocket 连接,`PiAgent` 会继续运行直到当前的流式任务结束。
31
+ 3. **离线 Buffer 与回放**:在无客户端连接期间,pi 抛出的事件将缓存在 `eventBuffer` 环形队列中。新客户端连接后,服务端通过 `backfill_start` → 增量事件 → `backfill_end` 进行追回。
32
+ 4. **真·空闲回收(True-Idle Timeout)**:仅在“无 WebSocket 连接”且“真正 Idle”(非 streaming,无挂起 RPC 请求)时启动 `IDLE_TIMEOUT_MS`(默认 5 分钟)倒计时回收进程。
33
+
34
+ ---
35
+
36
+ ## 数据流设计
37
+
38
+ ### 1. 消息发送与流式渲染
39
+
40
+ ```
41
+ 1. 浏览器发送 JSON: {"type": "prompt", "message": "..."}
42
+ 2. 服务端转给对应 Session 的 PiAgent
43
+ 3. PiAgent 更新状态为 streaming,写入 pi 的 stdin
44
+ 4. pi 往 stdout 输出事件流 (agent_start / text_delta / toolcall_start / agent_end / agent_settled)
45
+ 5. 服务端广播给所有连入该 Session 的 WebSocket,并写入离线 Buffer(若无连接)
46
+ 6. 浏览器 handlePiMessage 增量更新 DOM(文本、思考块、工具卡片)
47
+ ```
48
+
49
+ ### 2. 中途中断与转向(Steering)
50
+
51
+ 在代理正在执行长任务或工具调用时,用户可以输入补充指令:
52
+ - **Abort**:发送 `{"type": "abort"}` 中断当前生成。
53
+ - **Steer**:发送 `{"type": "steer", "message": "..."}` 插入中途指令,指导代理调整后续行动。
54
+
55
+ ---
56
+
57
+ ## 关键设计决策与权衡
58
+
59
+ ### 1. 渲染策略:重构 vs 增量 DOM
60
+
61
+ 针对流式输出中出现的文本、思考块(Thinking)、工具卡片交错问题:
62
+ - 选择**每次更新时重构当前 assistant 消息的内容节点**。因为单次回复文本长度通常有限,原生 DOM 节点重写开销在 5ms 以内,极大地简化了复杂的节点插入与折叠逻辑。
63
+
64
+ ### 2. 代次标记(`wsGen`)
65
+
66
+ 解决浏览器在极短时间内快速切换 WebSocket 会话时的竞态:
67
+ - 全局分配单调递增的 `wsGen` 代次。每次建立新 WebSocket 时递增。在接收消息前比对 `ws._gen === wsGen`,避免旧连接迟到的消息破坏新会话的状态。
68
+
69
+ ### 3. Markdown 渲染:Escape-First 零依赖
70
+
71
+ 不使用第三方大型 Markdown 渲染库,采用原生轻量处理:
72
+ - 在渲染入口统一对文本做 HTML Escape 转义(防止 XSS 攻击);
73
+ - 先将围栏代码块(Fenced Code Blocks)抠出暂存,再做行内语法转换,最后还原代码块。
74
+
75
+ ---
76
+
77
+ ## 消息事件与 UI 控件对照表
78
+
79
+ | pi RPC 事件 | 触发动作 / 前端 UI 表现 |
80
+ |-------------|-------------------------|
81
+ | `agent_start` | 切换发送按钮为“停止/中止”状态,标志进入流式生成状态 |
82
+ | `message_start` | 在聊天区域末尾追加新的 Assistant 消息卡片 |
83
+ | `message_update` / `text_delta` | 累加到文本缓冲并重新渲染 Markdown |
84
+ | `message_update` / `thinking_delta` | 累加到 Thinking 折叠卡片并实时显示 |
85
+ | `message_update` / `toolcall_start` | 创建可视化工具调用卡片(可折叠) |
86
+ | `tool_execution_start / update / end` | 动态更新工具卡片的输入参数、标准输出与执行状态 |
87
+ | `agent_end` / `agent_settled` | 恢复发送按钮,标记流式生成结束,触发侧边栏会话列表刷新 |
88
+ | `backfill_start` / `backfill_end` | 前端标记处于历史回放状态,回放期间暂停自动滚动,回放结束后一次性同步状态并滚动到底部 |
package/docs/ISSUES.md ADDED
@@ -0,0 +1,66 @@
1
+ # 问题排查与修补记录
2
+
3
+ 项目开发与演进过程中遇到并修复的技术问题与设计优化记录。
4
+
5
+ ---
6
+
7
+ ## 1. 侧边栏 `cwd` 路径编码错误
8
+
9
+ **症状**:API `GET /api/sessions?cwd=/home/zrlgs` 拿不到任何结果。
10
+
11
+ **根因**:第一版用了 `cwd.replace(/\//g, "-")` 把 `/home/zrlgs` 编码成 `-home-zrlgs`,但 pi 实际上用的是 `--home-zrlgs--`(前后都带 `-`)。
12
+
13
+ **修复**:弃用编码猜测,扫描 sessions 根目录下所有子目录,读每个 jsonl 的 header 里 `cwd` 字段,再按 `cwd` 严格比对过滤。
14
+
15
+ ---
16
+
17
+ ## 2. 关闭网页标签页导致正在生成的 pi 任务中断
18
+
19
+ **症状**:在用户提交了一个复杂 Prompt(或正在跑 Bash 工具)后关闭网页,AI 回答直接中断,下次连入时拿不到完整结果。
20
+
21
+ **根因**:原先 WebSocket 断开会直接触发 5 分钟倒计时杀进程;即使倒计时未到,没有客户端连入时生成的事件也被直接抛弃,未在内存中保留。
22
+
23
+ **修复**:
24
+ - 引入 **PiAgent 进程池化** 与 **真·空闲回收(True-Idle Timeout)**:只有在“无 WS 连接”且“真正 Idle”(非 streaming、无挂起 RPC)时才倒计时。
25
+ - 引入 **离线事件环形 Buffer(Event Ring Buffer)**:无客户端时缓存在 Buffer,新连接重连时发送 `backfill_start` → 增量事件 → `backfill_end` 进行追回并一次性同步状态。
26
+
27
+ ---
28
+
29
+ ## 3. 局域网 / 移动端 Safari 100vh 动态导航栏遮挡底部输入框
30
+
31
+ **症状**:移动端 Safari 浏览器打开 Web 界面时,底部输入框被 iOS 动态地址栏遮挡,且顶栏在滚动时会跟着滑走。
32
+
33
+ **修复**:
34
+ - 在 CSS 中使用 `100dvh` (Dynamic Viewport Height) 替代 `100vh`。
35
+ - 将顶栏改为 `sticky` / `fixed` 布局,保障移动端交互体验。
36
+ - 增加悬浮置顶/置底快捷按钮(FAB)。
37
+
38
+ ---
39
+
40
+ ## 4. WebSocket 代次竞态 —— 旧 socket 残留消息污染新会话
41
+
42
+ **症状**:点“新对话”快速发消息时,前一次会话延迟到达的 `agent_end` / `agent_settled` 事件跑进新会话,导致新会话 `streaming` 状态被错误覆盖。
43
+
44
+ **修复**:
45
+ - 引入全局 `wsGen` 单调递增计数器,给每个 WebSocket 分配 `_gen` 编号。
46
+ - 接收消息前比对代次,丢弃非当前代次发来的旧事件。
47
+
48
+ ---
49
+
50
+ ## 5. `/api/session` 中局部变量遮蔽 Node `path` 模块
51
+
52
+ **症状**:在调用 `/api/session` 接口获取历史对话时,出现 `TypeError: path.basename is not a function` 报错。
53
+
54
+ **根因**:函数内部在处理 jsonl 包含的文件条目时,使用了 `const path = ...` 局部变量名,屏蔽了 Node.js 顶层的 `import path from "node:path"` 模块。
55
+
56
+ **修复**:重命名局部变量为 `filePath` / `resolvedPath`,消除变量同名遮蔽。
57
+
58
+ ---
59
+
60
+ ## 6. 多设备 / 多 Tab 共享会话不同步
61
+
62
+ **症状**:同一个 Session 在手机和电脑上同时打开时,手机发送的消息电脑看不到,状态互相覆盖。
63
+
64
+ **修复**:
65
+ - 以 Session Key 为粒度管理 `PiAgent` 实例。
66
+ - 任何一个客户端发送 `prompt` 或 `steer` 指令时,通过 `agent.wsSend(..., senderWs)` 广播同步给连入该 Session 的其他客户端。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
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",
@@ -11,8 +11,8 @@
11
11
  "server.js",
12
12
  "bin/",
13
13
  "public/",
14
+ "docs/",
14
15
  "README.md",
15
- "CHANGELOG.md",
16
16
  "LICENSE"
17
17
  ],
18
18
  "scripts": {