@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 CHANGED
@@ -1,10 +1,15 @@
1
1
  # pi-web-chat
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@liguoshuai/pi-web-chat.svg)](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen.svg)](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
- > 项目主页:https://github.com/liguoshuai-1990/pi-web-chat
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. 安装 pi 编程代理 (pi agent)
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. 克隆与启动 pi-web-chat
100
+ ### 4. 安装与启动 pi-web-chat
96
101
 
97
- #### 克隆仓库与安装依赖
102
+ #### 方式 A:通过 npm / pnpm 全局安装(推荐,最快捷)
98
103
 
99
104
  ```bash
100
- git clone https://github.com/liguoshuai-1990/pi-web-chat.git
101
- cd pi-web-chat
102
- npm install
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
- #### 启动 Web 服务
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
- 👉 **http://localhost:3000**
141
+ ---
142
+
143
+ ### 5. 打开浏览器体验
144
+
145
+ Web 服务启动后,在浏览器中访问:
146
+ 👉 **http://localhost:3000** (或自定义的端口)
114
147
 
115
148
  ---
116
149
 
117
- ### 5. 命令行启动选项与全局 CLI(可选)
150
+ ### 6. CLI 命令行参数与选项
118
151
 
119
- 你也可以通过 `bin/pi-web-chat.js` 指定工作目录或监听端口:
152
+ `pi-web-chat` CLI 提供了便捷的命令行参数与环境变量支持:
120
153
 
121
- ```bash
122
- # 指定端口和工作目录 (cwd)
123
- node bin/pi-web-chat.js --port 8080 --cwd /path/to/your/project
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
- # 或通过 npm 全局安装后在任意目录下启动
126
- npm install -g .
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
- ## [1.10.3] - 2026-08-30
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": "1.10.3",
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 fetch(`${API}/api/config${cwdParam}`);
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 fetch(`${API}/api/validate-dir?path=${encodeURIComponent(rawPath)}`);
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 fetch(`${API}/api/sessions?cwd=${encodeURIComponent(cwd)}`);
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 fetch(`${API}/api/session?file=${encodeURIComponent(file)}`, {
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 fetch(`${API}/api/session?file=${encodeURIComponent(file)}`);
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 url = `${proto}://${location.host}/ws?cwd=${cwd}${sess}`;
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 fetch(`${API}/api/set-default-model`, {
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