@liguoshuai/pi-web-chat 1.10.3 → 2.0.0
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 +64 -19
- package/docs/CHANGELOG.md +22 -1
- package/package.json +12 -8
- package/public/app.js +50 -8
- package/public/style.css +8 -0
- package/scripts/install-service.sh +0 -0
- package/scripts/uninstall-service.sh +0 -0
- package/server.js +16 -1130
package/README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# pi-web-chat
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
|
|
3
7
|
一个 [pi](https://pi.dev) 编程代理的 Web 界面,风格参考 ChatGPT / Gemini ——
|
|
4
8
|
左侧历史会话侧边栏 + 右侧对话区 + 底部输入框。底层通过 pi 的 **RPC 模式**
|
|
5
9
|
(`pi --mode rpc`) 与 pi 子进程通信,前端走 WebSocket 流式渲染。
|
|
6
10
|
|
|
7
|
-
>
|
|
11
|
+
> 📦 **NPM 软件包**:[`@liguoshuai/pi-web-chat`](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)
|
|
12
|
+
> 🔗 **项目主页**:https://github.com/liguoshuai-1990/pi-web-chat
|
|
8
13
|
|
|
9
14
|
---
|
|
10
15
|
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
|
|
36
41
|
---
|
|
37
42
|
|
|
38
|
-
### 2.
|
|
43
|
+
### 2. 安装底层 pi 编程代理 (pi agent)
|
|
39
44
|
|
|
40
45
|
`pi-web-chat` 通过 RPC 模式 (`pi --mode rpc`) 与底层 `pi` 命令行 Agent 子进程通信。若系统中尚未安装 `pi`,请选择以下任一方式进行全局安装:
|
|
41
46
|
|
|
@@ -92,39 +97,79 @@ pi
|
|
|
92
97
|
|
|
93
98
|
---
|
|
94
99
|
|
|
95
|
-
### 4.
|
|
100
|
+
### 4. 安装与启动 pi-web-chat
|
|
96
101
|
|
|
97
|
-
####
|
|
102
|
+
#### 方式 A:通过 npm / pnpm 全局安装(推荐,最快捷)
|
|
98
103
|
|
|
99
104
|
```bash
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
# 使用 npm 全局安装
|
|
106
|
+
npm install -g @liguoshuai/pi-web-chat
|
|
107
|
+
|
|
108
|
+
# 或使用 pnpm 全局安装
|
|
109
|
+
pnpm add -g @liguoshuai/pi-web-chat
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
安装完成后,在终端任意目录下直接运行 `pi-web-chat` 命令启动:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 启动 Web 服务(默认监听 3000 端口,工作目录为当前目录)
|
|
116
|
+
pi-web-chat
|
|
117
|
+
|
|
118
|
+
# 自定义端口和指定工作目录 (cwd)
|
|
119
|
+
pi-web-chat --port 8080 --cwd /path/to/your/project
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### 方式 B:使用 npx 免安装即时运行
|
|
123
|
+
|
|
124
|
+
无需全局安装,直接通过 npx 快速启动:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx @liguoshuai/pi-web-chat
|
|
128
|
+
# 或指定参数
|
|
129
|
+
npx @liguoshuai/pi-web-chat --port 8080 --cwd /path/to/your/project
|
|
103
130
|
```
|
|
104
131
|
|
|
105
|
-
####
|
|
132
|
+
#### 方式 C:通过 Git 源码克隆与启动(适合二次开发)
|
|
106
133
|
|
|
107
134
|
```bash
|
|
135
|
+
git clone https://github.com/liguoshuai-1990/pi-web-chat.git
|
|
136
|
+
cd pi-web-chat
|
|
137
|
+
npm install
|
|
108
138
|
npm start
|
|
109
139
|
```
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 5. 打开浏览器体验
|
|
144
|
+
|
|
145
|
+
Web 服务启动后,在浏览器中访问:
|
|
146
|
+
👉 **http://localhost:3000** (或自定义的端口)
|
|
114
147
|
|
|
115
148
|
---
|
|
116
149
|
|
|
117
|
-
###
|
|
150
|
+
### 6. CLI 命令行参数与选项
|
|
118
151
|
|
|
119
|
-
|
|
152
|
+
`pi-web-chat` CLI 提供了便捷的命令行参数与环境变量支持:
|
|
120
153
|
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
```text
|
|
155
|
+
用法:
|
|
156
|
+
pi-web-chat [选项]
|
|
157
|
+
|
|
158
|
+
选项:
|
|
159
|
+
-p, --port <number> 指定 Web 服务监听端口(默认: 3000,或读取 PORT 环境变量)
|
|
160
|
+
-c, --cwd <path> 指定 pi 会话初始工作目录(默认: 当前终端路径 / 用户主目录)
|
|
161
|
+
-h, --help 显示帮助信息
|
|
162
|
+
|
|
163
|
+
环境变量:
|
|
164
|
+
PORT 服务端口(同 --port)
|
|
165
|
+
PI_BIN pi 可执行文件绝对路径(未设置时自动探测)
|
|
166
|
+
PI_SESSIONS_DIR pi 会话数据存储目录(默认: ~/.pi/agent/sessions)
|
|
124
167
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
pi-web-chat --port 8080
|
|
168
|
+
运行示例:
|
|
169
|
+
pi-web-chat # 默认端口 3000 启动
|
|
170
|
+
pi-web-chat --port 8080 # 指定 8080 端口启动
|
|
171
|
+
pi-web-chat --port 8080 --cwd ~/projects/my-app # 指定端口与初始工作区
|
|
172
|
+
PORT=4000 pi-web-chat # 通过环境变量指定端口
|
|
128
173
|
```
|
|
129
174
|
|
|
130
175
|
---
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## [
|
|
10
|
+
## [2.0.0] - 2026-08-30
|
|
11
|
+
|
|
12
|
+
### Major Architecture Refactoring (Monorepo & Multi-Client Ecosystem)
|
|
13
|
+
- **Monorepo 多端大仓库架构升级**:
|
|
14
|
+
- 项目重构为 `pi-chat` Monorepo,采用 pnpm Workspaces 进行多包统一管理。
|
|
15
|
+
- 核心架构拆分为 `clients/web`、`clients/android`、`clients/harmony`、`server`(VPS 网关)与 `packages/protocol`(跨端共享协议)。
|
|
16
|
+
- **服务端网关统一解耦与单真源设计 (`@pi-chat/server`)**:
|
|
17
|
+
- 提取 `@pi-chat/server` 为统一后端网关核心,支持全双工 WebSocket、SSE 打字机增量推送(`/api/stream`)、多端统一 Token 鉴权(`AUTH_TOKEN`)、30s 心跳保活与空闲进程内存自动回收。
|
|
18
|
+
- `clients/web/server.js` 瘦身为极简适配层,保持对 `@liguoshuai/pi-web-chat` npm 发行版及 `pi-web-chat` 全局 CLI 的 100% 独立向后兼容。
|
|
19
|
+
- **Android 原生端应用骨架 (`clients/android`)**:
|
|
20
|
+
- 基于 Kotlin + Jetpack Compose + Material 3 + OkHttp WebSocket + Coroutines/StateFlow 搭建,支持生成 APK 安装包。
|
|
21
|
+
- **华为鸿蒙 OS 原生端应用骨架 (`clients/harmony`)**:
|
|
22
|
+
- 基于 ArkTS + 声明式 ArkUI (Stage 模型,API 12+) + `@ohos.net.webSocket` 搭建,支持生成 HAP 安装包。
|
|
23
|
+
- **跨端标准通信协议 (`@pi-chat/protocol`)**:
|
|
24
|
+
- 统一 JSON Schema 标准、TypeScript 定义与自动容错校验器,三端交互格式完全对齐。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### Added & Published
|
|
29
|
+
- **NPM 官方包发布与全局命令行安装支持 (Official NPM Package & Global CLI)**:
|
|
30
|
+
- 正式发布软件包至 npm 官方注册表([`@liguoshuai/pi-web-chat`](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)),支持通过 `npm i -g @liguoshuai/pi-web-chat` 或 `pnpm add -g @liguoshuai/pi-web-chat` 全局安装并随时随地使用 `pi-web-chat` 命令启动。
|
|
31
|
+
- 支持通过 `npx @liguoshuai/pi-web-chat` 免安装即时启动,并在 README 中完善了全局安装、npx 启动与源码安装三种模式的快速指引。
|
|
11
32
|
|
|
12
33
|
### Fixed & Improved
|
|
13
34
|
- **移动端附件导入功能全面重构与修复 (Mobile Attachment Import Fix)**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"start": "node server.js",
|
|
21
|
-
"dev": "node --watch server.js",
|
|
22
|
-
"test": "node --test",
|
|
23
|
-
"prepare": "node -e \"import('fs').then(fs => { try { fs.chmodSync('bin/pi-web-chat.js', 0o755); } catch {} })\""
|
|
24
|
-
},
|
|
25
19
|
"keywords": [
|
|
26
20
|
"pi",
|
|
27
21
|
"coding-agent",
|
|
@@ -39,11 +33,21 @@
|
|
|
39
33
|
},
|
|
40
34
|
"homepage": "https://github.com/liguoshuai-1990/pi-web-chat#readme",
|
|
41
35
|
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
42
39
|
"engines": {
|
|
43
40
|
"node": ">=18"
|
|
44
41
|
},
|
|
45
42
|
"dependencies": {
|
|
43
|
+
"@liguoshuai/pi-chat-protocol": "2.0.0",
|
|
44
|
+
"@liguoshuai/pi-chat-server": "2.0.0",
|
|
46
45
|
"express": "^4.21.2",
|
|
47
46
|
"ws": "^8.18.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"start": "node server.js",
|
|
50
|
+
"dev": "node --watch server.js",
|
|
51
|
+
"test": "node --test"
|
|
48
52
|
}
|
|
49
|
-
}
|
|
53
|
+
}
|
package/public/app.js
CHANGED
|
@@ -20,6 +20,37 @@ window.onerror = function (message, source, lineno, colno, error) {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const API = ""; // same origin
|
|
23
|
+
|
|
24
|
+
function getAuthToken() {
|
|
25
|
+
try {
|
|
26
|
+
const urlToken = new URLSearchParams(window.location.search).get("token");
|
|
27
|
+
if (urlToken) {
|
|
28
|
+
localStorage.setItem("pi_auth_token", urlToken);
|
|
29
|
+
return urlToken;
|
|
30
|
+
}
|
|
31
|
+
return localStorage.getItem("pi_auth_token") || "";
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function authFetch(url, options = {}) {
|
|
38
|
+
const token = getAuthToken();
|
|
39
|
+
const headers = { ...(options.headers || {}) };
|
|
40
|
+
if (token) {
|
|
41
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
42
|
+
}
|
|
43
|
+
return fetch(url, { ...options, headers }).then(async (res) => {
|
|
44
|
+
if (res.status === 401) {
|
|
45
|
+
const entered = prompt("Pi Gateway 开启了访问鉴权,请输入访问 Token:");
|
|
46
|
+
if (entered) {
|
|
47
|
+
localStorage.setItem("pi_auth_token", entered.trim());
|
|
48
|
+
window.location.reload();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return res;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
23
54
|
const state = {
|
|
24
55
|
ws: null,
|
|
25
56
|
wsConnected: false,
|
|
@@ -120,7 +151,7 @@ async function loadServerConfig() {
|
|
|
120
151
|
}
|
|
121
152
|
try {
|
|
122
153
|
const cwdParam = state.cwd ? `?cwd=${encodeURIComponent(state.cwd)}` : "";
|
|
123
|
-
const res = await
|
|
154
|
+
const res = await authFetch(`${API}/api/config${cwdParam}`);
|
|
124
155
|
const data = await res.json();
|
|
125
156
|
if (data.home) state.homeDir = data.home;
|
|
126
157
|
if (data.serverCwd) state.serverCwd = data.serverCwd;
|
|
@@ -199,7 +230,7 @@ async function confirmCwdChange() {
|
|
|
199
230
|
if (!rawPath) return;
|
|
200
231
|
|
|
201
232
|
try {
|
|
202
|
-
const res = await
|
|
233
|
+
const res = await authFetch(`${API}/api/validate-dir?path=${encodeURIComponent(rawPath)}`);
|
|
203
234
|
const data = await res.json();
|
|
204
235
|
if (data.ok && data.path) {
|
|
205
236
|
let recents = [];
|
|
@@ -489,7 +520,7 @@ const el = (tag, props = {}, children = []) => {
|
|
|
489
520
|
// ---- Sidebar / sessions ----
|
|
490
521
|
async function refreshSessions() {
|
|
491
522
|
const cwd = state.cwd || "";
|
|
492
|
-
const res = await
|
|
523
|
+
const res = await authFetch(`${API}/api/sessions?cwd=${encodeURIComponent(cwd)}`);
|
|
493
524
|
const data = await res.json();
|
|
494
525
|
renderSidebar(data.sessions || []);
|
|
495
526
|
}
|
|
@@ -579,7 +610,7 @@ async function deleteSession(file, title) {
|
|
|
579
610
|
return;
|
|
580
611
|
}
|
|
581
612
|
try {
|
|
582
|
-
const res = await
|
|
613
|
+
const res = await authFetch(`${API}/api/session?file=${encodeURIComponent(file)}`, {
|
|
583
614
|
method: "DELETE",
|
|
584
615
|
});
|
|
585
616
|
const data = await res.json();
|
|
@@ -633,7 +664,7 @@ function initMobileToolbarFab() {
|
|
|
633
664
|
async function syncSessionHistory(file, force = false) {
|
|
634
665
|
if (!file) return;
|
|
635
666
|
try {
|
|
636
|
-
const res = await
|
|
667
|
+
const res = await authFetch(`${API}/api/session?file=${encodeURIComponent(file)}`);
|
|
637
668
|
if (!res.ok) return;
|
|
638
669
|
const data = await res.json();
|
|
639
670
|
if (!data || data.error) return;
|
|
@@ -1567,7 +1598,9 @@ function connectWs(opts = {}) {
|
|
|
1567
1598
|
state.activeToolCalls.clear();
|
|
1568
1599
|
}
|
|
1569
1600
|
|
|
1570
|
-
const
|
|
1601
|
+
const token = getAuthToken();
|
|
1602
|
+
const tokParam = token ? `&token=${encodeURIComponent(token)}` : "";
|
|
1603
|
+
const url = `${proto}://${location.host}/ws?cwd=${cwd}${sess}${tokParam}`;
|
|
1571
1604
|
const ws = new WebSocket(url);
|
|
1572
1605
|
state.ws = ws;
|
|
1573
1606
|
ws._gen = myGen;
|
|
@@ -1617,6 +1650,15 @@ function connectWs(opts = {}) {
|
|
|
1617
1650
|
let obj;
|
|
1618
1651
|
try { obj = JSON.parse(ev.data); } catch { return; }
|
|
1619
1652
|
if (obj.type === "pong") return;
|
|
1653
|
+
|
|
1654
|
+
if (obj.type === "error" && (obj.code === "unauthorized" || (obj.message && obj.message.toLowerCase().includes("unauthorized")))) {
|
|
1655
|
+
const entered = prompt("Pi Gateway 开启了访问鉴权,请输入访问 Token:");
|
|
1656
|
+
if (entered) {
|
|
1657
|
+
localStorage.setItem("pi_auth_token", entered.trim());
|
|
1658
|
+
initWebSocket({ explicitNewSession: false, isReconnect: true });
|
|
1659
|
+
}
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1620
1662
|
handlePiMessage(obj);
|
|
1621
1663
|
};
|
|
1622
1664
|
}
|
|
@@ -2202,7 +2244,7 @@ function updateEmptyStateModelInfo() {
|
|
|
2202
2244
|
|
|
2203
2245
|
async function setDefaultModel(m) {
|
|
2204
2246
|
try {
|
|
2205
|
-
const res = await
|
|
2247
|
+
const res = await authFetch(`${API}/api/set-default-model`, {
|
|
2206
2248
|
method: "POST",
|
|
2207
2249
|
headers: { "Content-Type": "application/json" },
|
|
2208
2250
|
body: JSON.stringify({
|
|
@@ -2515,7 +2557,7 @@ function updateComposerUI() {
|
|
|
2515
2557
|
if (sendBtn) {
|
|
2516
2558
|
sendBtn.classList.add("stop");
|
|
2517
2559
|
sendBtn.disabled = !state.wsConnected;
|
|
2518
|
-
sendBtn.textContent = "
|
|
2560
|
+
sendBtn.textContent = "";
|
|
2519
2561
|
sendBtn.title = "中止当前生成";
|
|
2520
2562
|
}
|
|
2521
2563
|
if (steerBtn) {
|
package/public/style.css
CHANGED
|
@@ -1026,6 +1026,14 @@ body {
|
|
|
1026
1026
|
.composer .send-btn.stop { background: var(--danger); }
|
|
1027
1027
|
.composer .send-btn.stop:hover:not(:disabled) { background: #f87171; }
|
|
1028
1028
|
.composer .send-btn.stop:active:not(:disabled) { transform: scale(0.95); }
|
|
1029
|
+
.composer .send-btn.stop:empty::after {
|
|
1030
|
+
content: "";
|
|
1031
|
+
width: 12px;
|
|
1032
|
+
height: 12px;
|
|
1033
|
+
background: #fff;
|
|
1034
|
+
border-radius: 2px;
|
|
1035
|
+
display: block;
|
|
1036
|
+
}
|
|
1029
1037
|
.composer .steer-btn {
|
|
1030
1038
|
display: inline-flex;
|
|
1031
1039
|
align-items: center;
|
|
File without changes
|
|
File without changes
|