claude-codex-wechat 0.1.0 → 0.1.6
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 +216 -350
- package/dist/server/cli.js +428 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,100 +1,50 @@
|
|
|
1
1
|
# claude-codex-wechat
|
|
2
2
|
|
|
3
|
-
`claude-codex-wechat`
|
|
3
|
+
`claude-codex-wechat` 是一个本地 bridge daemon。它把:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- WeChat direct 通道
|
|
6
|
+
- 本机原生 `Claude Code`
|
|
7
|
+
- 本机原生 `Codex CLI`
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
接起来,让 WeChat 用户可以直接驱动本机上的原生 CLI 会话,并尽量保留原生 resume 能力。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
这个仓库的定位不是另起一套 agent runtime,也不是 ACP bridge。它的目标一直是:
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- **不是 ACP bridge**
|
|
13
|
+
- WeChat 作为人类控制面
|
|
14
|
+
- 本地原生 CLI 作为真实执行面
|
|
15
|
+
- 两边尽量共享同一条原生会话恢复链路
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
## 当前能力
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
当前实现覆盖的主能力:
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
- WeChat direct 模式接入
|
|
22
|
+
- WeChat 轮询收消息与主动回消息
|
|
23
|
+
- 本地原生 `Claude Code` / `Codex CLI` 会话桥接
|
|
24
|
+
- 会话持久化与当前会话恢复
|
|
25
|
+
- 原生 Claude / Codex recoverable session 列表与 attach
|
|
26
|
+
- 本地管理页
|
|
27
|
+
- npm 全局安装与生产 CLI 入口
|
|
28
|
+
- 本地发布 npm 包
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
- 微信扫码登录与 bot token 获取
|
|
26
|
-
- 微信消息路由到本地 `Claude Code` / `Codex CLI`
|
|
27
|
-
- 会话持久化到 SQLite
|
|
28
|
-
- 原生 Claude / Codex 会话扫描
|
|
29
|
-
- 原生会话手动接入
|
|
30
|
-
- 原生会话自动接入
|
|
31
|
-
- 桥接会话与原生会话绑定持久化
|
|
32
|
-
- 管理页支持:
|
|
33
|
-
- pairing 审批
|
|
34
|
-
- 授权用户管理
|
|
35
|
-
- provider 切换
|
|
36
|
-
- 会话停止 / 归档
|
|
37
|
-
- 权限审批
|
|
38
|
-
- 原生恢复修复
|
|
39
|
-
- Claude 原生恢复能力:
|
|
40
|
-
- `claude --resume <sessionId>`
|
|
41
|
-
- `claude -r '<完整标题>'`
|
|
42
|
-
- Codex 原生恢复能力:
|
|
43
|
-
- `codex resume <sessionId>`
|
|
44
|
-
- `codex resume '<thread_name>'`
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## 当前实现目标
|
|
49
|
-
|
|
50
|
-
这个仓库的核心目标不是“做一个微信聊天 UI”,而是:
|
|
51
|
-
|
|
52
|
-
1. 把微信消息稳定接进本地 bridge
|
|
53
|
-
2. 把 bridge 会话稳定映射到本地原生 Claude / Codex 会话
|
|
54
|
-
3. 让微信侧会话尽可能和本地原生会话恢复链路对齐
|
|
55
|
-
|
|
56
|
-
其中 Claude 这条链路尤其强调:
|
|
57
|
-
|
|
58
|
-
- bridge session 有自己的 `resumeTitle`
|
|
59
|
-
- 原生会话文件里有对应标题
|
|
60
|
-
- `~/.claude/history.jsonl` 里也有对应标题
|
|
61
|
-
|
|
62
|
-
只有这几层都对齐,`claude -r '<完整标题>'` 才真正可用。
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## 目录说明
|
|
30
|
+
## 项目结构
|
|
67
31
|
|
|
68
32
|
主要目录:
|
|
69
33
|
|
|
70
34
|
- `src/`
|
|
71
|
-
-
|
|
72
|
-
- 微信通道
|
|
73
|
-
- provider 接入
|
|
74
|
-
- daemon / admin routes / web
|
|
35
|
+
- daemon、provider、session、channel、web 主实现
|
|
75
36
|
- `tests/`
|
|
76
|
-
-
|
|
77
|
-
- bridge 运行态测试
|
|
78
|
-
- 微信 direct 流程测试
|
|
79
|
-
- 前端交互测试
|
|
37
|
+
- 单元测试、运行态测试、前端测试
|
|
80
38
|
- `scripts/`
|
|
81
|
-
-
|
|
82
|
-
- 联调启动脚本
|
|
83
|
-
- 诊断脚本
|
|
39
|
+
- 诊断与辅助脚本
|
|
84
40
|
- `docs/`
|
|
85
|
-
-
|
|
86
|
-
- 对齐过程文档
|
|
87
|
-
- 参考实现文档
|
|
88
|
-
|
|
89
|
-
脚本分层索引见:
|
|
90
|
-
|
|
91
|
-
- [scripts/README.md](./scripts/README.md)
|
|
41
|
+
- 设计说明、对齐文档、参考资料
|
|
92
42
|
|
|
93
|
-
|
|
43
|
+
脚本说明见 [scripts/README.md](./scripts/README.md)。
|
|
94
44
|
|
|
95
45
|
## 安装
|
|
96
46
|
|
|
97
|
-
###
|
|
47
|
+
### 作为 npm 包安装
|
|
98
48
|
|
|
99
49
|
```bash
|
|
100
50
|
npm install -g claude-codex-wechat
|
|
@@ -102,55 +52,50 @@ npm install -g claude-codex-wechat
|
|
|
102
52
|
pnpm add -g claude-codex-wechat
|
|
103
53
|
```
|
|
104
54
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
安装后会得到一个全局命令 `claude-codex-wechat`:
|
|
55
|
+
安装后会得到全局命令:
|
|
108
56
|
|
|
109
57
|
```bash
|
|
110
|
-
claude-codex-wechat
|
|
111
|
-
claude-codex-wechat
|
|
112
|
-
claude-codex-wechat
|
|
113
|
-
claude-codex-wechat
|
|
114
|
-
claude-codex-wechat
|
|
58
|
+
claude-codex-wechat help
|
|
59
|
+
claude-codex-wechat init
|
|
60
|
+
claude-codex-wechat doctor
|
|
61
|
+
claude-codex-wechat start
|
|
62
|
+
claude-codex-wechat print-config
|
|
115
63
|
```
|
|
116
64
|
|
|
117
|
-
|
|
65
|
+
说明:
|
|
118
66
|
|
|
119
|
-
|
|
67
|
+
- 该包依赖 `better-sqlite3`
|
|
68
|
+
- 大多数常见平台会直接下载预编译二进制
|
|
69
|
+
- 少数环境可能需要本地编译工具链
|
|
70
|
+
- Node 版本要求:`>=20`
|
|
120
71
|
|
|
121
|
-
###
|
|
72
|
+
### 从源码安装
|
|
122
73
|
|
|
123
74
|
```bash
|
|
124
|
-
cd /
|
|
75
|
+
cd /Users/liuyuhua/github/claude-codex-wechat
|
|
125
76
|
pnpm install
|
|
126
77
|
```
|
|
127
78
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
## 本地启动(开发模式)
|
|
131
|
-
|
|
132
|
-
一条命令同时启动后端与前端管理页(前端以 Vite middleware 模式嵌入同一进程,含热更新):
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
pnpm dev
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
启动后访问 `http://127.0.0.1:8787`(端口由 `BRIDGE_PORT` 控制)。
|
|
79
|
+
## CLI 命令
|
|
139
80
|
|
|
140
|
-
|
|
81
|
+
生产态 CLI 入口在 `dist/server/cli.js`,通过 `bin` 暴露为 `claude-codex-wechat`。
|
|
141
82
|
|
|
142
|
-
|
|
143
|
-
pnpm typecheck
|
|
144
|
-
pnpm test
|
|
145
|
-
pnpm build # = vite build(前端 -> dist/web)+ esbuild(server -> dist/server/cli.js)
|
|
146
|
-
```
|
|
83
|
+
可用命令:
|
|
147
84
|
|
|
148
|
-
|
|
85
|
+
- `claude-codex-wechat start`
|
|
86
|
+
- 前台启动 daemon
|
|
87
|
+
- `claude-codex-wechat init`
|
|
88
|
+
- 写默认配置到 `~/.claude-codex-wechat/config.json`
|
|
89
|
+
- `claude-codex-wechat doctor`
|
|
90
|
+
- 检查配置、前端产物、`claude`/`codex` 可执行文件
|
|
91
|
+
- `claude-codex-wechat print-config`
|
|
92
|
+
- 打印当前配置文件
|
|
93
|
+
- `claude-codex-wechat help`
|
|
94
|
+
- 显示帮助
|
|
149
95
|
|
|
150
|
-
|
|
151
|
-
- `dist/server/cli.js` —— CLI / daemon 入口(对应 `bin` 字段)
|
|
96
|
+
默认监听地址:
|
|
152
97
|
|
|
153
|
-
|
|
98
|
+
- `http://127.0.0.1:8787`
|
|
154
99
|
|
|
155
100
|
## 配置
|
|
156
101
|
|
|
@@ -160,18 +105,16 @@ pnpm build # = vite build(前端 -> dist/web)+ esbuild(server -> di
|
|
|
160
105
|
~/.claude-codex-wechat/config.json
|
|
161
106
|
```
|
|
162
107
|
|
|
163
|
-
|
|
108
|
+
推荐先初始化:
|
|
164
109
|
|
|
165
110
|
```bash
|
|
166
|
-
|
|
167
|
-
cp config.example.json ~/.claude-codex-wechat/config.json
|
|
111
|
+
claude-codex-wechat init
|
|
168
112
|
```
|
|
169
113
|
|
|
170
114
|
最小配置示例:
|
|
171
115
|
|
|
172
116
|
```json
|
|
173
117
|
{
|
|
174
|
-
"databasePath": "/Users/you/.claude-codex-wechat/bridge.sqlite",
|
|
175
118
|
"wechat": {
|
|
176
119
|
"enabled": true,
|
|
177
120
|
"baseUrl": "https://ilinkai.weixin.qq.com",
|
|
@@ -185,11 +128,15 @@ cp config.example.json ~/.claude-codex-wechat/config.json
|
|
|
185
128
|
"codex": {
|
|
186
129
|
"command": "/opt/homebrew/bin/codex"
|
|
187
130
|
}
|
|
131
|
+
},
|
|
132
|
+
"bridge": {
|
|
133
|
+
"defaultProvider": "claude-code",
|
|
134
|
+
"defaultWorkspace": "/absolute/path/to/workspace"
|
|
188
135
|
}
|
|
189
136
|
}
|
|
190
137
|
```
|
|
191
138
|
|
|
192
|
-
|
|
139
|
+
环境变量:
|
|
193
140
|
|
|
194
141
|
- `BRIDGE_PORT`
|
|
195
142
|
- `BRIDGE_CONFIG`
|
|
@@ -200,31 +147,94 @@ cp config.example.json ~/.claude-codex-wechat/config.json
|
|
|
200
147
|
- `BRIDGE_CLAUDE_COMMAND`
|
|
201
148
|
- `BRIDGE_CODEX_COMMAND`
|
|
202
149
|
|
|
203
|
-
|
|
150
|
+
## 本地开发
|
|
151
|
+
|
|
152
|
+
开发模式启动:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pnpm dev
|
|
156
|
+
```
|
|
204
157
|
|
|
205
|
-
|
|
158
|
+
这会:
|
|
206
159
|
|
|
207
|
-
|
|
160
|
+
- 启动 daemon
|
|
161
|
+
- 在同一进程里挂 Vite middleware
|
|
162
|
+
- 提供本地管理页与前端热更新
|
|
208
163
|
|
|
209
|
-
|
|
164
|
+
开发检查:
|
|
210
165
|
|
|
211
166
|
```bash
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
167
|
+
pnpm typecheck
|
|
168
|
+
pnpm test
|
|
169
|
+
pnpm build
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
构建产物:
|
|
173
|
+
|
|
174
|
+
- `dist/web/`
|
|
175
|
+
- 前端静态资源
|
|
176
|
+
- `dist/server/cli.js`
|
|
177
|
+
- 生产 CLI / daemon 入口
|
|
178
|
+
|
|
179
|
+
## 生产运行
|
|
180
|
+
|
|
181
|
+
生产态通常按下面方式运行:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
claude-codex-wechat start
|
|
217
185
|
```
|
|
218
186
|
|
|
219
|
-
|
|
187
|
+
注意:
|
|
188
|
+
|
|
189
|
+
- 这是前台进程
|
|
190
|
+
- npm 本身不负责守护
|
|
191
|
+
- 长期常驻建议交给进程管理器
|
|
192
|
+
- 如果默认端口已被本程序的后台服务占用,`start` 会先停掉后台服务,再以前台模式启动
|
|
193
|
+
- 如果端口被别的进程占用,`start` 会直接报出占用 PID 和命令,不会强杀陌生进程
|
|
194
|
+
|
|
195
|
+
### 安装后后台运行
|
|
196
|
+
|
|
197
|
+
参考 `happier` 的方式,这个仓库现在提供 `service` 命令,把当前 CLI 注册成操作系统服务,而不是在 Node 进程里自己 daemonize。
|
|
198
|
+
|
|
199
|
+
常用命令:
|
|
220
200
|
|
|
221
201
|
```bash
|
|
222
|
-
|
|
202
|
+
claude-codex-wechat service install
|
|
203
|
+
claude-codex-wechat service start
|
|
204
|
+
claude-codex-wechat service restart
|
|
205
|
+
claude-codex-wechat service status
|
|
206
|
+
claude-codex-wechat service logs
|
|
207
|
+
claude-codex-wechat service tail
|
|
208
|
+
claude-codex-wechat service stop
|
|
209
|
+
claude-codex-wechat service uninstall
|
|
223
210
|
```
|
|
224
211
|
|
|
225
|
-
|
|
212
|
+
当前支持:
|
|
226
213
|
|
|
227
|
-
|
|
214
|
+
- macOS:`launchd`(`~/Library/LaunchAgents/`)
|
|
215
|
+
- Linux:`systemd --user`(`~/.config/systemd/user/`)
|
|
216
|
+
|
|
217
|
+
推荐安装后直接这样:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
claude-codex-wechat init
|
|
221
|
+
claude-codex-wechat service install
|
|
222
|
+
claude-codex-wechat service status
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### pm2
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npm install -g pm2
|
|
229
|
+
pm2 start claude-codex-wechat --name ccwx -- start
|
|
230
|
+
pm2 logs ccwx
|
|
231
|
+
pm2 restart ccwx
|
|
232
|
+
pm2 startup && pm2 save
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### systemd
|
|
236
|
+
|
|
237
|
+
示例:
|
|
228
238
|
|
|
229
239
|
```ini
|
|
230
240
|
[Unit]
|
|
@@ -235,7 +245,6 @@ After=network.target
|
|
|
235
245
|
Type=simple
|
|
236
246
|
User=youruser
|
|
237
247
|
Environment=BRIDGE_PORT=8787
|
|
238
|
-
# 如需指定配置文件:Environment=BRIDGE_CONFIG=/home/youruser/.claude-codex-wechat/config.json
|
|
239
248
|
ExecStart=/usr/bin/claude-codex-wechat start
|
|
240
249
|
Restart=on-failure
|
|
241
250
|
RestartSec=5
|
|
@@ -244,17 +253,9 @@ RestartSec=5
|
|
|
244
253
|
WantedBy=multi-user.target
|
|
245
254
|
```
|
|
246
255
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
sudo systemctl daemon-reload
|
|
251
|
-
sudo systemctl enable --now claude-codex-wechat
|
|
252
|
-
sudo journalctl -u claude-codex-wechat -f # 看日志
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### 方式三:launchd(macOS)
|
|
256
|
+
### launchd
|
|
256
257
|
|
|
257
|
-
|
|
258
|
+
示例:
|
|
258
259
|
|
|
259
260
|
```xml
|
|
260
261
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -277,282 +278,147 @@ sudo journalctl -u claude-codex-wechat -f # 看日志
|
|
|
277
278
|
<true/>
|
|
278
279
|
<key>KeepAlive</key>
|
|
279
280
|
<true/>
|
|
280
|
-
<key>StandardOutPath</key>
|
|
281
|
-
<string>/tmp/claude-codex-wechat.out.log</string>
|
|
282
|
-
<key>StandardErrorPath</key>
|
|
283
|
-
<string>/tmp/claude-codex-wechat.err.log</string>
|
|
284
281
|
</dict>
|
|
285
282
|
</plist>
|
|
286
283
|
```
|
|
287
284
|
|
|
288
|
-
|
|
285
|
+
## 发布 npm 包
|
|
289
286
|
|
|
290
|
-
|
|
291
|
-
launchctl load ~/Library/LaunchAgents/com.claude-codex-wechat.plist
|
|
292
|
-
launchctl list | grep claude-codex-wechat # 确认在跑
|
|
293
|
-
# 卸载:launchctl unload ~/Library/LaunchAgents/com.claude-codex-wechat.plist
|
|
294
|
-
```
|
|
287
|
+
当前仓库只保留本地发布流,不再使用 GitHub Action 自动发布。
|
|
295
288
|
|
|
296
|
-
|
|
289
|
+
### 发布行为
|
|
297
290
|
|
|
298
|
-
|
|
291
|
+
本地执行 `npm publish` 时,会自动触发:
|
|
299
292
|
|
|
300
|
-
|
|
293
|
+
- `npm version patch --no-git-tag-version`
|
|
301
294
|
|
|
302
|
-
|
|
295
|
+
也就是说:
|
|
303
296
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
```
|
|
297
|
+
- 每次成功发布都会自动把版本加一位 `patch`
|
|
298
|
+
- 版本 bump 发生在真正 publish 之前
|
|
307
299
|
|
|
308
|
-
|
|
300
|
+
完整的发布前校验定义在 [release.sh](./release.sh),`prepublishOnly` 只负责自动递增版本,避免在 `npm publish` 阶段重复再跑一遍测试和构建。
|
|
309
301
|
|
|
310
|
-
|
|
311
|
-
BRIDGE_DEFAULT_PROVIDER=codex bash ./scripts/recover-weixin-runtime.sh
|
|
312
|
-
```
|
|
302
|
+
### 直接发布
|
|
313
303
|
|
|
314
|
-
|
|
304
|
+
如果不需要自动提交版本变更:
|
|
315
305
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
5. 启动最新 bridge runtime
|
|
306
|
+
```bash
|
|
307
|
+
cd /Users/liuyuhua/github/claude-codex-wechat
|
|
308
|
+
npm publish --access public --registry=https://registry.npmjs.org/
|
|
309
|
+
```
|
|
321
310
|
|
|
322
|
-
|
|
311
|
+
如果必须走代理:
|
|
323
312
|
|
|
324
313
|
```bash
|
|
325
|
-
|
|
314
|
+
cd /Users/liuyuhua/github/claude-codex-wechat
|
|
315
|
+
HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890 npm publish --access public --registry=https://registry.npmjs.org/
|
|
326
316
|
```
|
|
327
317
|
|
|
328
|
-
|
|
318
|
+
### 使用 release.sh
|
|
329
319
|
|
|
330
|
-
|
|
320
|
+
如果希望:
|
|
331
321
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
322
|
+
- 发布前先提交当前工作区
|
|
323
|
+
- 发布后把自动 bump 的版本提交回当前分支
|
|
324
|
+
- 自动 `git push`
|
|
335
325
|
|
|
336
|
-
|
|
326
|
+
使用:
|
|
337
327
|
|
|
338
|
-
```
|
|
339
|
-
/
|
|
328
|
+
```bash
|
|
329
|
+
cd /Users/liuyuhua/github/claude-codex-wechat
|
|
330
|
+
chmod +x release.sh
|
|
331
|
+
./release.sh
|
|
340
332
|
```
|
|
341
333
|
|
|
342
|
-
|
|
334
|
+
脚本默认会使用本地代理:
|
|
343
335
|
|
|
344
336
|
```text
|
|
345
|
-
|
|
346
|
-
/tmp/bridge-weixin.env
|
|
337
|
+
http://127.0.0.1:7890
|
|
347
338
|
```
|
|
348
339
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
- 这两个 `/tmp` 文件仍然会生成,方便联调和显式 `source`
|
|
352
|
-
- 扫码成功后,helper 现在也会自动把新凭据回写到 `~/.claude-codex-wechat/config.json`
|
|
353
|
-
- 之后直接执行 `pnpm dev`,默认就会继续读取这份正式配置
|
|
354
|
-
|
|
355
|
-
最少要同步这几个字段:
|
|
356
|
-
|
|
357
|
-
- `wechat.baseUrl`
|
|
358
|
-
- `wechat.token`
|
|
359
|
-
- `wechat.accountId`
|
|
360
|
-
|
|
361
|
-
否则你会看到一种典型现象:
|
|
362
|
-
|
|
363
|
-
- 扫码后当前 runtime 能用
|
|
364
|
-
- 重启 `pnpm dev` 后又读不到刚才的新凭据
|
|
365
|
-
- 看起来像“每次重启都要重新扫码”
|
|
366
|
-
|
|
367
|
-
### 2. 启动成功后检查 bridge 是否真的连通
|
|
340
|
+
如果你要覆盖默认代理:
|
|
368
341
|
|
|
369
342
|
```bash
|
|
370
|
-
|
|
343
|
+
cd /Users/liuyuhua/github/claude-codex-wechat
|
|
344
|
+
chmod +x release.sh
|
|
345
|
+
HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890 ./release.sh
|
|
371
346
|
```
|
|
372
347
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
- `weixin_connected`
|
|
376
|
-
- `weixin_status`
|
|
377
|
-
- `weixin_last_error`
|
|
378
|
-
|
|
379
|
-
如果是正常连通,通常应该看到:
|
|
380
|
-
|
|
381
|
-
- `weixin_connected=true`
|
|
382
|
-
- `weixin_status=connected`
|
|
383
|
-
|
|
384
|
-
### 3. 发微信消息开始对话
|
|
385
|
-
|
|
386
|
-
给当前 bot 账号发一条文本消息。
|
|
387
|
-
|
|
388
|
-
bridge 收到消息后会:
|
|
389
|
-
|
|
390
|
-
- 自动授权用户(如果设置中开启)
|
|
391
|
-
- 创建或接入 bridge session
|
|
392
|
-
- 默认用当前设置的 provider 对话
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
|
|
396
|
-
## 管理页有哪些功能
|
|
397
|
-
|
|
398
|
-
当前管理页主要分成 4 块:
|
|
399
|
-
|
|
400
|
-
### 1. Dashboard
|
|
401
|
-
|
|
402
|
-
- daemon 状态
|
|
403
|
-
- provider 状态
|
|
404
|
-
- 活跃会话数
|
|
405
|
-
- 待处理权限数
|
|
348
|
+
这个脚本会:
|
|
406
349
|
|
|
407
|
-
|
|
350
|
+
- 先检查 `npm whoami`
|
|
351
|
+
- 先执行 `pnpm typecheck`
|
|
352
|
+
- 再执行 `pnpm build`
|
|
353
|
+
- 提交当前改动
|
|
354
|
+
- 以默认参数执行 `npm publish --access public --registry=https://registry.npmjs.org/`
|
|
355
|
+
- 提交自动 bump 后的 `package.json`
|
|
356
|
+
- 推送到当前分支
|
|
408
357
|
|
|
409
|
-
|
|
410
|
-
- pairing 待审批列表
|
|
411
|
-
- 批准 / 拒绝 pairing
|
|
412
|
-
- 已授权用户管理
|
|
413
|
-
- 撤销授权
|
|
414
|
-
- 扫描可恢复原生会话
|
|
415
|
-
- 手动接入原生会话
|
|
416
|
-
- 自动接入原生会话
|
|
417
|
-
- Claude recoverable session 修复
|
|
418
|
-
|
|
419
|
-
### 3. SessionsPanel
|
|
420
|
-
|
|
421
|
-
- 查看 bridge 会话
|
|
422
|
-
- 查看推荐恢复命令
|
|
423
|
-
- 查看 provider resume 命令
|
|
424
|
-
- 停止会话
|
|
425
|
-
- 归档会话
|
|
426
|
-
- 查看原生可达路径
|
|
427
|
-
- 查看绑定来源
|
|
428
|
-
- 单个 / 批量修复 Claude 原生恢复元数据
|
|
429
|
-
|
|
430
|
-
### 4. PermissionsPanel
|
|
431
|
-
|
|
432
|
-
- 批准 / 拒绝 / 中止高风险权限请求
|
|
433
|
-
|
|
434
|
-
---
|
|
435
|
-
|
|
436
|
-
## 原生恢复能力
|
|
437
|
-
|
|
438
|
-
### Claude
|
|
439
|
-
|
|
440
|
-
当前仓库支持两种恢复方式:
|
|
441
|
-
|
|
442
|
-
按原生 session id:
|
|
358
|
+
### 发布前建议检查
|
|
443
359
|
|
|
444
360
|
```bash
|
|
445
|
-
claude
|
|
361
|
+
cd /Users/liuyuhua/github/claude-codex-wechat
|
|
362
|
+
npm whoami
|
|
363
|
+
npm config get registry
|
|
364
|
+
npm pack --dry-run
|
|
365
|
+
pnpm typecheck
|
|
366
|
+
pnpm build
|
|
446
367
|
```
|
|
447
368
|
|
|
448
|
-
|
|
369
|
+
如果包名是否可用要先确认:
|
|
449
370
|
|
|
450
371
|
```bash
|
|
451
|
-
claude
|
|
372
|
+
npm view claude-codex-wechat
|
|
452
373
|
```
|
|
453
374
|
|
|
454
|
-
|
|
375
|
+
## 常见问题
|
|
455
376
|
|
|
456
|
-
|
|
457
|
-
- `providerResumeHistorySynced = true`
|
|
377
|
+
### `npm publish` 报 `EPERM scandir ~/.Trash`
|
|
458
378
|
|
|
459
|
-
|
|
379
|
+
一般是因为你不在项目目录,而是在家目录执行了 `npm publish`。
|
|
460
380
|
|
|
461
|
-
|
|
381
|
+
先确认:
|
|
462
382
|
|
|
463
383
|
```bash
|
|
464
|
-
|
|
384
|
+
pwd
|
|
465
385
|
```
|
|
466
386
|
|
|
467
|
-
|
|
387
|
+
应该在:
|
|
468
388
|
|
|
469
389
|
```bash
|
|
470
|
-
codex
|
|
390
|
+
/Users/liuyuhua/github/claude-codex-wechat
|
|
471
391
|
```
|
|
472
392
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
## 脚本怎么分
|
|
476
|
-
|
|
477
|
-
### 正式使用入口
|
|
393
|
+
### `Public registration is not allowed`
|
|
478
394
|
|
|
479
|
-
|
|
480
|
-
最推荐的恢复入口
|
|
481
|
-
- `scripts/weixin-login-helper.ts`
|
|
482
|
-
单独扫码与落盘凭据
|
|
483
|
-
- `scripts/start-runtime-check.sh`
|
|
484
|
-
启动最新 runtime 并打印关键状态
|
|
395
|
+
通常是下面几类问题:
|
|
485
396
|
|
|
486
|
-
|
|
397
|
+
- `registry` 不是官方 npm
|
|
398
|
+
- 正在往私有 registry 发包
|
|
399
|
+
- scoped package 没带 `--access public`
|
|
400
|
+
- 账号没有该包名或 scope 的发布权限
|
|
487
401
|
|
|
488
|
-
|
|
489
|
-
看 bridge 是否真的连通
|
|
490
|
-
- `scripts/check-runtime-recovery.sh`
|
|
491
|
-
看 bridge session 恢复字段
|
|
492
|
-
- `scripts/check-weixin-updates.ts`
|
|
493
|
-
直接打微信官方 `getupdates`
|
|
494
|
-
|
|
495
|
-
如果只想记一个命令:
|
|
402
|
+
检查:
|
|
496
403
|
|
|
497
404
|
```bash
|
|
498
|
-
|
|
405
|
+
npm config get registry
|
|
406
|
+
npm whoami
|
|
407
|
+
node -p "require('./package.json').name"
|
|
499
408
|
```
|
|
500
409
|
|
|
501
|
-
|
|
410
|
+
### 本地必须走代理
|
|
411
|
+
|
|
412
|
+
临时发布可直接这样:
|
|
502
413
|
|
|
503
414
|
```bash
|
|
504
|
-
|
|
505
|
-
BRIDGE_PORT=8788 WAIT_SECONDS=120 bash ./scripts/check-codex-wechat-flow.sh
|
|
415
|
+
HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890 npm publish --access public --registry=https://registry.npmjs.org/
|
|
506
416
|
```
|
|
507
417
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
## 当前已知边界
|
|
511
|
-
|
|
512
|
-
这个仓库当前已经把工程内能做的链路基本铺平了,但仍有一个非常现实的边界:
|
|
513
|
-
|
|
514
|
-
- 微信 bot 新授权出来的 token 可能会很快再次 `session timeout`
|
|
515
|
-
|
|
516
|
-
也就是说,即使:
|
|
517
|
-
|
|
518
|
-
- 扫码成功
|
|
519
|
-
- 新 token 落盘成功
|
|
520
|
-
- 最新 runtime 能启动
|
|
521
|
-
|
|
522
|
-
微信官方 `getupdates` 会话本身仍可能在很短时间后再次失效。
|
|
523
|
-
|
|
524
|
-
如果出现:
|
|
525
|
-
|
|
526
|
-
- `weixin_connected=false`
|
|
527
|
-
- `weixin_status=session_timeout`
|
|
528
|
-
- `weixin_last_error=weixin_get_updates_failed:-14:session timeout`
|
|
529
|
-
|
|
530
|
-
这更像是微信侧登录 / updates 会话稳定性问题,而不一定是这个 bridge 的业务逻辑问题。
|
|
531
|
-
|
|
532
|
-
---
|
|
533
|
-
|
|
534
|
-
## 怎么判断真正成功
|
|
535
|
-
|
|
536
|
-
一个最小闭环应至少满足:
|
|
537
|
-
|
|
538
|
-
1. `check-runtime-readiness.sh` 显示:
|
|
539
|
-
- `weixin_connected=true`
|
|
540
|
-
- `weixin_status=connected`
|
|
541
|
-
2. 给 bot 发一条真实微信消息后:
|
|
542
|
-
- `/api/channel/sessions` 出现新会话
|
|
543
|
-
3. Claude 会话里看到:
|
|
544
|
-
- `providerResumeTitleSynced=true`
|
|
545
|
-
- `providerResumeHistorySynced=true`
|
|
546
|
-
4. `claude -r '<完整标题>'` 真能恢复
|
|
547
|
-
|
|
548
|
-
如果要验 Codex,则再满足:
|
|
549
|
-
|
|
550
|
-
5. `codex resume '<thread_name>'` 真能恢复
|
|
551
|
-
|
|
552
|
-
---
|
|
553
|
-
|
|
554
|
-
## 更多文档
|
|
418
|
+
## 相关文件
|
|
555
419
|
|
|
556
|
-
- [
|
|
557
|
-
- [
|
|
420
|
+
- [package.json](./package.json)
|
|
421
|
+
- [release.sh](./release.sh)
|
|
558
422
|
- [scripts/README.md](./scripts/README.md)
|
|
423
|
+
- [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
|
|
424
|
+
- [docs/wechatbot-usage-guide.md](./docs/wechatbot-usage-guide.md)
|
package/dist/server/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __cjsCreateRequire } from 'node:module'; const require = __cjsCreateRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { dirname as
|
|
5
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6
|
+
import { dirname as dirname11, join as join11 } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// src/daemon/bootstrap.ts
|
|
@@ -3989,6 +3989,27 @@ async function resolveProviderCommands(providers) {
|
|
|
3989
3989
|
};
|
|
3990
3990
|
}
|
|
3991
3991
|
|
|
3992
|
+
// src/daemon/portGuard.ts
|
|
3993
|
+
import { execFile } from "node:child_process";
|
|
3994
|
+
import { promisify } from "node:util";
|
|
3995
|
+
var execFileAsync = promisify(execFile);
|
|
3996
|
+
async function findListeningProcess(port) {
|
|
3997
|
+
if (process.platform === "win32") return null;
|
|
3998
|
+
try {
|
|
3999
|
+
const { stdout } = await execFileAsync("lsof", ["-n", "-P", `-iTCP:${port}`, "-sTCP:LISTEN"]);
|
|
4000
|
+
const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
4001
|
+
const record = lines.slice(1)[0];
|
|
4002
|
+
if (!record) return null;
|
|
4003
|
+
const parts = record.trim().split(/\s+/);
|
|
4004
|
+
const command = parts[0] ?? "";
|
|
4005
|
+
const pid = Number(parts[1] ?? "");
|
|
4006
|
+
if (!command || !Number.isFinite(pid)) return null;
|
|
4007
|
+
return { pid, command };
|
|
4008
|
+
} catch {
|
|
4009
|
+
return null;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
|
|
3992
4013
|
// src/daemon/staticFrontend.ts
|
|
3993
4014
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3994
4015
|
import { join as join9 } from "node:path";
|
|
@@ -4010,9 +4031,318 @@ function attachStaticFrontend(webRoot2) {
|
|
|
4010
4031
|
};
|
|
4011
4032
|
}
|
|
4012
4033
|
|
|
4034
|
+
// src/daemon/service.ts
|
|
4035
|
+
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
4036
|
+
import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
4037
|
+
import { homedir as homedir7 } from "node:os";
|
|
4038
|
+
import { dirname as dirname10, join as join10 } from "node:path";
|
|
4039
|
+
import { createReadStream } from "node:fs";
|
|
4040
|
+
import { execFile as execFile2, spawn as spawn6 } from "node:child_process";
|
|
4041
|
+
import { createInterface } from "node:readline";
|
|
4042
|
+
import { promisify as promisify2 } from "node:util";
|
|
4043
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
4044
|
+
async function installService(context) {
|
|
4045
|
+
if (process.platform === "darwin") {
|
|
4046
|
+
const spec = buildLaunchdSpec(context);
|
|
4047
|
+
await mkdir4(dirname10(spec.plistPath), { recursive: true });
|
|
4048
|
+
await mkdir4(dirname10(spec.stdoutPath), { recursive: true });
|
|
4049
|
+
await writeFile5(spec.plistPath, renderLaunchdPlist(spec), "utf8");
|
|
4050
|
+
await runLaunchctl(["unload", spec.plistPath]).catch(() => void 0);
|
|
4051
|
+
await runLaunchctl(["load", spec.plistPath]);
|
|
4052
|
+
await runLaunchctl(["kickstart", "-k", `gui/${process.getuid?.() ?? 0}/${spec.label}`]);
|
|
4053
|
+
return await readServiceStatus(context);
|
|
4054
|
+
}
|
|
4055
|
+
if (process.platform === "linux") {
|
|
4056
|
+
const spec = buildSystemdUserSpec(context);
|
|
4057
|
+
await mkdir4(dirname10(spec.unitPath), { recursive: true });
|
|
4058
|
+
await mkdir4(dirname10(spec.stdoutPath), { recursive: true });
|
|
4059
|
+
await writeFile5(spec.unitPath, renderSystemdUnit(spec), "utf8");
|
|
4060
|
+
await runSystemctl(["--user", "daemon-reload"]);
|
|
4061
|
+
await runSystemctl(["--user", "enable", "--now", spec.unitName]);
|
|
4062
|
+
return await readServiceStatus(context);
|
|
4063
|
+
}
|
|
4064
|
+
throw new Error("service_install_not_supported_on_this_platform");
|
|
4065
|
+
}
|
|
4066
|
+
async function startService(context) {
|
|
4067
|
+
if (process.platform === "darwin") {
|
|
4068
|
+
const spec = buildLaunchdSpec(context);
|
|
4069
|
+
await ensureFileExists(spec.plistPath, "service_not_installed");
|
|
4070
|
+
await runLaunchctl(["load", spec.plistPath]).catch(() => void 0);
|
|
4071
|
+
await runLaunchctl(["kickstart", "-k", `gui/${process.getuid?.() ?? 0}/${spec.label}`]);
|
|
4072
|
+
return await readServiceStatus(context);
|
|
4073
|
+
}
|
|
4074
|
+
if (process.platform === "linux") {
|
|
4075
|
+
const spec = buildSystemdUserSpec(context);
|
|
4076
|
+
await ensureFileExists(spec.unitPath, "service_not_installed");
|
|
4077
|
+
await runSystemctl(["--user", "start", spec.unitName]);
|
|
4078
|
+
return await readServiceStatus(context);
|
|
4079
|
+
}
|
|
4080
|
+
throw new Error("service_start_not_supported_on_this_platform");
|
|
4081
|
+
}
|
|
4082
|
+
async function stopService(context) {
|
|
4083
|
+
if (process.platform === "darwin") {
|
|
4084
|
+
const spec = buildLaunchdSpec(context);
|
|
4085
|
+
if (!existsSync8(spec.plistPath)) return await readServiceStatus(context);
|
|
4086
|
+
await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
|
|
4087
|
+
return await readServiceStatus(context);
|
|
4088
|
+
}
|
|
4089
|
+
if (process.platform === "linux") {
|
|
4090
|
+
const spec = buildSystemdUserSpec(context);
|
|
4091
|
+
if (!existsSync8(spec.unitPath)) return await readServiceStatus(context);
|
|
4092
|
+
await runSystemctl(["--user", "stop", spec.unitName]).catch(() => void 0);
|
|
4093
|
+
return await readServiceStatus(context);
|
|
4094
|
+
}
|
|
4095
|
+
throw new Error("service_stop_not_supported_on_this_platform");
|
|
4096
|
+
}
|
|
4097
|
+
async function uninstallService(context) {
|
|
4098
|
+
if (process.platform === "darwin") {
|
|
4099
|
+
const spec = buildLaunchdSpec(context);
|
|
4100
|
+
if (existsSync8(spec.plistPath)) {
|
|
4101
|
+
await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
|
|
4102
|
+
await rm2(spec.plistPath, { force: true });
|
|
4103
|
+
}
|
|
4104
|
+
return await readServiceStatus(context);
|
|
4105
|
+
}
|
|
4106
|
+
if (process.platform === "linux") {
|
|
4107
|
+
const spec = buildSystemdUserSpec(context);
|
|
4108
|
+
if (existsSync8(spec.unitPath)) {
|
|
4109
|
+
await runSystemctl(["--user", "disable", "--now", spec.unitName]).catch(() => void 0);
|
|
4110
|
+
await rm2(spec.unitPath, { force: true });
|
|
4111
|
+
await runSystemctl(["--user", "daemon-reload"]).catch(() => void 0);
|
|
4112
|
+
}
|
|
4113
|
+
return await readServiceStatus(context);
|
|
4114
|
+
}
|
|
4115
|
+
throw new Error("service_uninstall_not_supported_on_this_platform");
|
|
4116
|
+
}
|
|
4117
|
+
async function restartService(context) {
|
|
4118
|
+
if (process.platform === "darwin") {
|
|
4119
|
+
const spec = buildLaunchdSpec(context);
|
|
4120
|
+
await ensureFileExists(spec.plistPath, "service_not_installed");
|
|
4121
|
+
await runLaunchctl(["kickstart", "-k", `gui/${process.getuid?.() ?? 0}/${spec.label}`]);
|
|
4122
|
+
return await readServiceStatus(context);
|
|
4123
|
+
}
|
|
4124
|
+
if (process.platform === "linux") {
|
|
4125
|
+
const spec = buildSystemdUserSpec(context);
|
|
4126
|
+
await ensureFileExists(spec.unitPath, "service_not_installed");
|
|
4127
|
+
await runSystemctl(["--user", "restart", spec.unitName]);
|
|
4128
|
+
return await readServiceStatus(context);
|
|
4129
|
+
}
|
|
4130
|
+
throw new Error("service_restart_not_supported_on_this_platform");
|
|
4131
|
+
}
|
|
4132
|
+
async function readServiceLogs(context, lines = 120) {
|
|
4133
|
+
const status = await readServiceStatus(context);
|
|
4134
|
+
const stdoutTail = await tailFile(status.stdoutPath, lines);
|
|
4135
|
+
const stderrTail = await tailFile(status.stderrPath, lines);
|
|
4136
|
+
return [
|
|
4137
|
+
`[stdout] ${status.stdoutPath}`,
|
|
4138
|
+
stdoutTail || "(empty)",
|
|
4139
|
+
"",
|
|
4140
|
+
`[stderr] ${status.stderrPath}`,
|
|
4141
|
+
stderrTail || "(empty)"
|
|
4142
|
+
].join("\n");
|
|
4143
|
+
}
|
|
4144
|
+
async function tailServiceLogs(context) {
|
|
4145
|
+
const status = await readServiceStatus(context);
|
|
4146
|
+
await tailFiles([status.stdoutPath, status.stderrPath]);
|
|
4147
|
+
}
|
|
4148
|
+
async function readServiceStatus(context) {
|
|
4149
|
+
if (process.platform === "darwin") {
|
|
4150
|
+
const spec = buildLaunchdSpec(context);
|
|
4151
|
+
const installed = existsSync8(spec.plistPath);
|
|
4152
|
+
const running = installed ? await runLaunchctl(["print", `gui/${process.getuid?.() ?? 0}/${spec.label}`]).then(() => true, () => false) : false;
|
|
4153
|
+
return {
|
|
4154
|
+
manager: "launchd",
|
|
4155
|
+
installed,
|
|
4156
|
+
running,
|
|
4157
|
+
serviceFilePath: spec.plistPath,
|
|
4158
|
+
label: spec.label,
|
|
4159
|
+
stdoutPath: spec.stdoutPath,
|
|
4160
|
+
stderrPath: spec.stderrPath
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
if (process.platform === "linux") {
|
|
4164
|
+
const spec = buildSystemdUserSpec(context);
|
|
4165
|
+
const installed = existsSync8(spec.unitPath);
|
|
4166
|
+
const running = installed ? await runSystemctl(["--user", "is-active", "--quiet", spec.unitName]).then(() => true, () => false) : false;
|
|
4167
|
+
return {
|
|
4168
|
+
manager: "systemd-user",
|
|
4169
|
+
installed,
|
|
4170
|
+
running,
|
|
4171
|
+
serviceFilePath: spec.unitPath,
|
|
4172
|
+
label: spec.unitName,
|
|
4173
|
+
stdoutPath: spec.stdoutPath,
|
|
4174
|
+
stderrPath: spec.stderrPath
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
throw new Error("service_status_not_supported_on_this_platform");
|
|
4178
|
+
}
|
|
4179
|
+
function buildLaunchdSpec(context) {
|
|
4180
|
+
const stateDir = join10(homedir7(), ".claude-codex-wechat");
|
|
4181
|
+
return {
|
|
4182
|
+
label: "com.claude-codex-wechat",
|
|
4183
|
+
plistPath: join10(homedir7(), "Library", "LaunchAgents", "com.claude-codex-wechat.plist"),
|
|
4184
|
+
programArgs: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "start"],
|
|
4185
|
+
workingDirectory: homedir7(),
|
|
4186
|
+
stdoutPath: join10(stateDir, "logs", "service.stdout.log"),
|
|
4187
|
+
stderrPath: join10(stateDir, "logs", "service.stderr.log"),
|
|
4188
|
+
environment: buildServiceEnvironment(context)
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
function buildSystemdUserSpec(context) {
|
|
4192
|
+
const stateDir = join10(homedir7(), ".claude-codex-wechat");
|
|
4193
|
+
return {
|
|
4194
|
+
unitName: "claude-codex-wechat.service",
|
|
4195
|
+
unitPath: join10(homedir7(), ".config", "systemd", "user", "claude-codex-wechat.service"),
|
|
4196
|
+
execStart: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "start"],
|
|
4197
|
+
workingDirectory: homedir7(),
|
|
4198
|
+
stdoutPath: join10(stateDir, "logs", "service.stdout.log"),
|
|
4199
|
+
stderrPath: join10(stateDir, "logs", "service.stderr.log"),
|
|
4200
|
+
environment: buildServiceEnvironment(context)
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
function buildServiceEnvironment(context) {
|
|
4204
|
+
const env = {
|
|
4205
|
+
PATH: process.env.PATH ?? "",
|
|
4206
|
+
HOME: process.env.HOME ?? homedir7()
|
|
4207
|
+
};
|
|
4208
|
+
const configPath = context.configPath ?? process.env.BRIDGE_CONFIG;
|
|
4209
|
+
const port = context.port ?? Number(process.env.BRIDGE_PORT ?? 8787);
|
|
4210
|
+
if (configPath) env.BRIDGE_CONFIG = configPath;
|
|
4211
|
+
if (Number.isFinite(port)) env.BRIDGE_PORT = String(port);
|
|
4212
|
+
for (const key of [
|
|
4213
|
+
"BRIDGE_WECHAT_ENABLED",
|
|
4214
|
+
"BRIDGE_WECHAT_BASE_URL",
|
|
4215
|
+
"BRIDGE_WECHAT_TOKEN",
|
|
4216
|
+
"BRIDGE_WECHAT_ACCOUNT_ID",
|
|
4217
|
+
"BRIDGE_CLAUDE_COMMAND",
|
|
4218
|
+
"BRIDGE_CODEX_COMMAND",
|
|
4219
|
+
"HTTP_PROXY",
|
|
4220
|
+
"HTTPS_PROXY",
|
|
4221
|
+
"http_proxy",
|
|
4222
|
+
"https_proxy"
|
|
4223
|
+
]) {
|
|
4224
|
+
const value = process.env[key];
|
|
4225
|
+
if (value) env[key] = value;
|
|
4226
|
+
}
|
|
4227
|
+
return env;
|
|
4228
|
+
}
|
|
4229
|
+
function renderLaunchdPlist(spec) {
|
|
4230
|
+
const programArgs = spec.programArgs.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
|
|
4231
|
+
const envEntries = Object.entries(spec.environment).map(([key, value]) => ` <key>${escapeXml(key)}</key>
|
|
4232
|
+
<string>${escapeXml(value)}</string>`).join("\n");
|
|
4233
|
+
return [
|
|
4234
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
4235
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
4236
|
+
'<plist version="1.0">',
|
|
4237
|
+
"<dict>",
|
|
4238
|
+
" <key>Label</key>",
|
|
4239
|
+
` <string>${escapeXml(spec.label)}</string>`,
|
|
4240
|
+
" <key>ProgramArguments</key>",
|
|
4241
|
+
" <array>",
|
|
4242
|
+
programArgs,
|
|
4243
|
+
" </array>",
|
|
4244
|
+
" <key>WorkingDirectory</key>",
|
|
4245
|
+
` <string>${escapeXml(spec.workingDirectory)}</string>`,
|
|
4246
|
+
" <key>EnvironmentVariables</key>",
|
|
4247
|
+
" <dict>",
|
|
4248
|
+
envEntries,
|
|
4249
|
+
" </dict>",
|
|
4250
|
+
" <key>RunAtLoad</key>",
|
|
4251
|
+
" <true/>",
|
|
4252
|
+
" <key>KeepAlive</key>",
|
|
4253
|
+
" <true/>",
|
|
4254
|
+
" <key>StandardOutPath</key>",
|
|
4255
|
+
` <string>${escapeXml(spec.stdoutPath)}</string>`,
|
|
4256
|
+
" <key>StandardErrorPath</key>",
|
|
4257
|
+
` <string>${escapeXml(spec.stderrPath)}</string>`,
|
|
4258
|
+
"</dict>",
|
|
4259
|
+
"</plist>",
|
|
4260
|
+
""
|
|
4261
|
+
].join("\n");
|
|
4262
|
+
}
|
|
4263
|
+
function renderSystemdUnit(spec) {
|
|
4264
|
+
const envEntries = Object.entries(spec.environment).map(([key, value]) => `Environment=${quoteSystemdEnv(`${key}=${value}`)}`).join("\n");
|
|
4265
|
+
return [
|
|
4266
|
+
"[Unit]",
|
|
4267
|
+
"Description=claude-codex-wechat bridge daemon",
|
|
4268
|
+
"After=network.target",
|
|
4269
|
+
"",
|
|
4270
|
+
"[Service]",
|
|
4271
|
+
"Type=simple",
|
|
4272
|
+
`WorkingDirectory=${spec.workingDirectory}`,
|
|
4273
|
+
`ExecStart=${spec.execStart.map(quoteSystemdArg).join(" ")}`,
|
|
4274
|
+
envEntries,
|
|
4275
|
+
`StandardOutput=append:${spec.stdoutPath}`,
|
|
4276
|
+
`StandardError=append:${spec.stderrPath}`,
|
|
4277
|
+
"Restart=on-failure",
|
|
4278
|
+
"RestartSec=5",
|
|
4279
|
+
"",
|
|
4280
|
+
"[Install]",
|
|
4281
|
+
"WantedBy=default.target",
|
|
4282
|
+
""
|
|
4283
|
+
].join("\n");
|
|
4284
|
+
}
|
|
4285
|
+
function escapeXml(value) {
|
|
4286
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4287
|
+
}
|
|
4288
|
+
function quoteSystemdArg(value) {
|
|
4289
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
4290
|
+
}
|
|
4291
|
+
function quoteSystemdEnv(value) {
|
|
4292
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
4293
|
+
}
|
|
4294
|
+
async function runLaunchctl(args) {
|
|
4295
|
+
await execFileAsync2("launchctl", args);
|
|
4296
|
+
}
|
|
4297
|
+
async function runSystemctl(args) {
|
|
4298
|
+
await execFileAsync2("systemctl", args);
|
|
4299
|
+
}
|
|
4300
|
+
async function ensureFileExists(path, errorCode) {
|
|
4301
|
+
if (!existsSync8(path)) throw new Error(errorCode);
|
|
4302
|
+
await readFile6(path, "utf8");
|
|
4303
|
+
}
|
|
4304
|
+
async function tailFile(path, lines) {
|
|
4305
|
+
if (!existsSync8(path)) return "";
|
|
4306
|
+
const content = await readFile6(path, "utf8");
|
|
4307
|
+
return content.split(/\r?\n/).slice(-lines).join("\n").trim();
|
|
4308
|
+
}
|
|
4309
|
+
async function tailFiles(paths) {
|
|
4310
|
+
const readers = paths.filter((path) => existsSync8(path)).map((path) => {
|
|
4311
|
+
const rl = createInterface({
|
|
4312
|
+
input: createReadStream(path, { encoding: "utf8", start: Math.max(0, fileSizeHint(path) - 8192) }),
|
|
4313
|
+
crlfDelay: Infinity
|
|
4314
|
+
});
|
|
4315
|
+
rl.on("line", (line) => {
|
|
4316
|
+
process.stdout.write(`[${path}] ${line}
|
|
4317
|
+
`);
|
|
4318
|
+
});
|
|
4319
|
+
return rl;
|
|
4320
|
+
});
|
|
4321
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
const child = spawn6("tail", ["-f", ...paths.filter((path) => existsSync8(path))], { stdio: ["ignore", "pipe", "inherit"] });
|
|
4325
|
+
child.stdout.on("data", (chunk) => {
|
|
4326
|
+
process.stdout.write(String(chunk));
|
|
4327
|
+
});
|
|
4328
|
+
await new Promise((resolve, reject) => {
|
|
4329
|
+
child.on("exit", () => resolve());
|
|
4330
|
+
child.on("error", reject);
|
|
4331
|
+
}).finally(() => {
|
|
4332
|
+
for (const reader of readers) reader.close();
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
function fileSizeHint(path) {
|
|
4336
|
+
try {
|
|
4337
|
+
return statSync(path).size;
|
|
4338
|
+
} catch {
|
|
4339
|
+
return 0;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4013
4343
|
// src/cli.ts
|
|
4014
|
-
var here =
|
|
4015
|
-
var webRoot =
|
|
4344
|
+
var here = dirname11(fileURLToPath(import.meta.url));
|
|
4345
|
+
var webRoot = join11(here, "..", "web");
|
|
4016
4346
|
async function main() {
|
|
4017
4347
|
const command = process.argv[2];
|
|
4018
4348
|
switch (command) {
|
|
@@ -4026,6 +4356,9 @@ async function main() {
|
|
|
4026
4356
|
case "doctor":
|
|
4027
4357
|
await cmdDoctor();
|
|
4028
4358
|
return;
|
|
4359
|
+
case "service":
|
|
4360
|
+
await cmdService(process.argv.slice(3));
|
|
4361
|
+
return;
|
|
4029
4362
|
case "print-config":
|
|
4030
4363
|
cmdPrintConfig();
|
|
4031
4364
|
return;
|
|
@@ -4042,21 +4375,36 @@ async function main() {
|
|
|
4042
4375
|
}
|
|
4043
4376
|
}
|
|
4044
4377
|
async function cmdStart() {
|
|
4045
|
-
if (!
|
|
4378
|
+
if (!existsSync9(webRoot)) {
|
|
4046
4379
|
console.error(`\u627E\u4E0D\u5230\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269: ${webRoot}`);
|
|
4047
4380
|
console.error("\u8BF7\u5148\u8FD0\u884C\u6784\u5EFA (pnpm build) \u540E\u518D\u542F\u52A8\uFF0C\u6216\u91CD\u65B0\u5B89\u88C5\u5B8C\u6574\u7684 npm \u5305\u3002");
|
|
4048
4381
|
process.exitCode = 1;
|
|
4049
4382
|
return;
|
|
4050
4383
|
}
|
|
4384
|
+
const context = createServiceContext();
|
|
4385
|
+
const port = context.port ?? 8787;
|
|
4386
|
+
const occupiedBy = await findListeningProcess(port);
|
|
4387
|
+
if (occupiedBy) {
|
|
4388
|
+
const serviceStatus = await readServiceStatus(context).catch(() => null);
|
|
4389
|
+
if (serviceStatus?.installed && serviceStatus.running) {
|
|
4390
|
+
console.log(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u540E\u53F0\u670D\u52A1\u5360\u7528\uFF0C\u5148\u505C\u6B62\u670D\u52A1\u518D\u4EE5\u524D\u53F0\u6A21\u5F0F\u542F\u52A8\u3002`);
|
|
4391
|
+
await stopService(context);
|
|
4392
|
+
} else {
|
|
4393
|
+
console.error(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u5360\u7528: PID=${occupiedBy.pid} COMMAND=${occupiedBy.command}`);
|
|
4394
|
+
console.error("\u8BF7\u5148\u505C\u6B62\u5360\u7528\u8FDB\u7A0B\uFF0C\u6216\u6539\u7528 `claude-codex-wechat service stop` \u505C\u6389\u540E\u53F0\u670D\u52A1\u3002");
|
|
4395
|
+
process.exitCode = 1;
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4051
4399
|
await startDaemon({
|
|
4052
4400
|
attachFrontend: attachStaticFrontend(webRoot)
|
|
4053
4401
|
});
|
|
4054
4402
|
}
|
|
4055
4403
|
function cmdInit() {
|
|
4056
4404
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
4057
|
-
const dir =
|
|
4405
|
+
const dir = dirname11(configPath);
|
|
4058
4406
|
mkdirSync6(dir, { recursive: true });
|
|
4059
|
-
if (
|
|
4407
|
+
if (existsSync9(configPath)) {
|
|
4060
4408
|
console.log(`\u914D\u7F6E\u5DF2\u5B58\u5728\uFF0C\u672A\u8986\u76D6: ${configPath}`);
|
|
4061
4409
|
return;
|
|
4062
4410
|
}
|
|
@@ -4081,13 +4429,13 @@ function cmdInit() {
|
|
|
4081
4429
|
async function cmdDoctor() {
|
|
4082
4430
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
4083
4431
|
console.log("claude-codex-wechat doctor\n");
|
|
4084
|
-
report("\u914D\u7F6E\u6587\u4EF6",
|
|
4085
|
-
report("\u524D\u7AEF\u4EA7\u7269",
|
|
4432
|
+
report("\u914D\u7F6E\u6587\u4EF6", existsSync9(configPath) ? configPath : `\u7F3A\u5931 (${configPath})\uFF0C\u8FD0\u884C init \u521B\u5EFA`);
|
|
4433
|
+
report("\u524D\u7AEF\u4EA7\u7269", existsSync9(webRoot) ? webRoot : `\u7F3A\u5931 (${webRoot})`);
|
|
4086
4434
|
const claude = await findExecutable("claude");
|
|
4087
4435
|
report("claude \u53EF\u6267\u884C", claude ?? "\u672A\u627E\u5230\uFF0C\u9700\u5148\u5B89\u88C5\u5E76\u767B\u5F55 Claude Code");
|
|
4088
4436
|
const codex = await findExecutable("codex");
|
|
4089
4437
|
report("codex \u53EF\u6267\u884C", codex ?? "\u672A\u627E\u5230\uFF08\u4EC5\u4F7F\u7528 Claude \u65F6\u53EF\u5FFD\u7565\uFF09");
|
|
4090
|
-
if (
|
|
4438
|
+
if (existsSync9(configPath)) {
|
|
4091
4439
|
const config = loadBridgeConfig(configPath);
|
|
4092
4440
|
const wechat = config.wechat;
|
|
4093
4441
|
report("\u5FAE\u4FE1\u542F\u7528", wechat?.enabled ? "\u662F" : "\u5426");
|
|
@@ -4097,16 +4445,84 @@ async function cmdDoctor() {
|
|
|
4097
4445
|
}
|
|
4098
4446
|
function cmdPrintConfig() {
|
|
4099
4447
|
const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
|
|
4100
|
-
if (!
|
|
4448
|
+
if (!existsSync9(configPath)) {
|
|
4101
4449
|
console.error(`\u914D\u7F6E\u4E0D\u5B58\u5728: ${configPath}\uFF08\u8FD0\u884C init \u521B\u5EFA\uFF09`);
|
|
4102
4450
|
process.exitCode = 1;
|
|
4103
4451
|
return;
|
|
4104
4452
|
}
|
|
4105
4453
|
console.log(readFileSync7(configPath, "utf8"));
|
|
4106
4454
|
}
|
|
4455
|
+
async function cmdService(args) {
|
|
4456
|
+
const action = args[0] ?? "status";
|
|
4457
|
+
const context = createServiceContext();
|
|
4458
|
+
switch (action) {
|
|
4459
|
+
case "install": {
|
|
4460
|
+
const status = await installService(context);
|
|
4461
|
+
printServiceStatus("service installed", status);
|
|
4462
|
+
return;
|
|
4463
|
+
}
|
|
4464
|
+
case "start": {
|
|
4465
|
+
const status = await startService(context);
|
|
4466
|
+
printServiceStatus("service started", status);
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
case "stop": {
|
|
4470
|
+
const status = await stopService(context);
|
|
4471
|
+
printServiceStatus("service stopped", status);
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
case "restart": {
|
|
4475
|
+
const status = await restartService(context);
|
|
4476
|
+
printServiceStatus("service restarted", status);
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
case "logs": {
|
|
4480
|
+
console.log(await readServiceLogs(context));
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
case "tail": {
|
|
4484
|
+
await tailServiceLogs(context);
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
case "uninstall": {
|
|
4488
|
+
const status = await uninstallService(context);
|
|
4489
|
+
printServiceStatus("service uninstalled", status);
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
case "status": {
|
|
4493
|
+
const status = await readServiceStatus(context);
|
|
4494
|
+
printServiceStatus("service status", status);
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
default:
|
|
4498
|
+
console.error(`\u672A\u77E5 service \u5B50\u547D\u4EE4: ${action}
|
|
4499
|
+
`);
|
|
4500
|
+
printUsage();
|
|
4501
|
+
process.exitCode = 1;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
function createServiceContext() {
|
|
4505
|
+
return {
|
|
4506
|
+
cliEntrypointPath: fileURLToPath(import.meta.url),
|
|
4507
|
+
nodePath: process.execPath,
|
|
4508
|
+
configPath: process.env.BRIDGE_CONFIG ?? defaultConfigPath(),
|
|
4509
|
+
port: Number(process.env.BRIDGE_PORT ?? 8787)
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4107
4512
|
function report(label, value) {
|
|
4108
4513
|
console.log(` ${label.padEnd(14)}: ${value}`);
|
|
4109
4514
|
}
|
|
4515
|
+
function printServiceStatus(title, status) {
|
|
4516
|
+
console.log(`claude-codex-wechat ${title}
|
|
4517
|
+
`);
|
|
4518
|
+
report("service manager", status.manager);
|
|
4519
|
+
report("installed", status.installed ? "yes" : "no");
|
|
4520
|
+
report("running", status.running ? "yes" : "no");
|
|
4521
|
+
report("label", status.label);
|
|
4522
|
+
report("service file", status.serviceFilePath);
|
|
4523
|
+
report("stdout log", status.stdoutPath);
|
|
4524
|
+
report("stderr log", status.stderrPath);
|
|
4525
|
+
}
|
|
4110
4526
|
function printUsage() {
|
|
4111
4527
|
console.log(`claude-codex-wechat \u2014 \u672C\u5730 WeChat \u2194 Claude/Codex bridge daemon
|
|
4112
4528
|
|
|
@@ -4117,6 +4533,7 @@ function printUsage() {
|
|
|
4117
4533
|
start \u542F\u52A8 daemon\uFF08\u9ED8\u8BA4\u547D\u4EE4\uFF0C\u524D\u53F0\u8FD0\u884C\uFF09
|
|
4118
4534
|
init \u5728 ~/.claude-codex-wechat/ \u521B\u5EFA\u9ED8\u8BA4\u914D\u7F6E
|
|
4119
4535
|
doctor \u68C0\u67E5\u914D\u7F6E\u3001\u524D\u7AEF\u4EA7\u7269\u4E0E claude/codex \u53EF\u6267\u884C\u6587\u4EF6
|
|
4536
|
+
service \u7BA1\u7406\u540E\u53F0\u670D\u52A1\uFF08install/start/stop/restart/status/logs/tail/uninstall\uFF09
|
|
4120
4537
|
print-config \u6253\u5370\u5F53\u524D\u914D\u7F6E\u6587\u4EF6\u5185\u5BB9
|
|
4121
4538
|
help \u663E\u793A\u672C\u5E2E\u52A9
|
|
4122
4539
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-codex-wechat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build:web": "vite build",
|
|
22
22
|
"build:server": "node scripts/build-server.mjs",
|
|
23
23
|
"build": "vite build && node scripts/build-server.mjs",
|
|
24
|
-
"prepublishOnly": "npm
|
|
24
|
+
"prepublishOnly": "npm version patch --no-git-tag-version"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@fastify/static": "^9.1.3",
|