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.
Files changed (3) hide show
  1. package/README.md +216 -350
  2. package/dist/server/cli.js +428 -11
  3. 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
- - 微信 bot 通道
6
- - 本地原生 `Claude Code`
7
- - 本地原生 `Codex CLI`
5
+ - WeChat direct 通道
6
+ - 本机原生 `Claude Code`
7
+ - 本机原生 `Codex CLI`
8
8
 
9
- 接起来,让你可以直接在微信里和本机上的 `Claude Code` / `Codex` 对话,并保留本地原生会话恢复能力。
9
+ 接起来,让 WeChat 用户可以直接驱动本机上的原生 CLI 会话,并尽量保留原生 resume 能力。
10
10
 
11
- 这个项目的定位是:
11
+ 这个仓库的定位不是另起一套 agent runtime,也不是 ACP bridge。它的目标一直是:
12
12
 
13
- - **本地运行**
14
- - **微信 -> 本地原生 CLI**
15
- - **Native Provider Bridge**
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
- - 微信 direct 模式接入,走官方 `getupdates` / `sendmessage`
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
- - bridge 主实现
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
- ### 方式 A:作为 npm 包全局安装(推荐普通用户)
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
- > 该包依赖原生模块 `better-sqlite3`,安装时会优先下载预编译二进制;少数环境(非主流架构 / 离线)可能需要本地编译工具链(Node ≥ 20、Python、C++ 编译器)。
106
-
107
- 安装后会得到一个全局命令 `claude-codex-wechat`:
55
+ 安装后会得到全局命令:
108
56
 
109
57
  ```bash
110
- claude-codex-wechat init # 在 ~/.claude-codex-wechat/ 写入默认配置
111
- claude-codex-wechat doctor # 检查配置、前端产物、claude/codex 可执行文件
112
- claude-codex-wechat start # 前台启动 daemon(默认命令)
113
- claude-codex-wechat print-config # 打印当前配置
114
- claude-codex-wechat help # 查看全部命令
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
- 启动后访问 `http://127.0.0.1:8787`(端口由 `BRIDGE_PORT` 控制)。
65
+ 说明:
118
66
 
119
- 前置条件:本机已安装并登录好 `Claude Code` / `Codex CLI`,且准备好微信 bot 的 `token` / `accountId`。
67
+ - 该包依赖 `better-sqlite3`
68
+ - 大多数常见平台会直接下载预编译二进制
69
+ - 少数环境可能需要本地编译工具链
70
+ - Node 版本要求:`>=20`
120
71
 
121
- ### 方式 B:从源码开发
72
+ ### 从源码安装
122
73
 
123
74
  ```bash
124
- cd /path/to/claude-codex-wechat
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
- ```bash
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
- - `dist/web/` —— 前端静态资源(生产态由 `@fastify/static` 服务)
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
- mkdir -p ~/.claude-codex-wechat
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
- `claude-codex-wechat start` 默认前台运行。npm 本身不负责守护,长期常驻建议交给成熟的进程管理器。下面三种任选其一。
160
+ - 启动 daemon
161
+ - 在同一进程里挂 Vite middleware
162
+ - 提供本地管理页与前端热更新
208
163
 
209
- ### 方式一:pm2(跨平台,最简单)
164
+ 开发检查:
210
165
 
211
166
  ```bash
212
- npm install -g pm2
213
- pm2 start claude-codex-wechat --name ccwx -- start
214
- pm2 logs ccwx # 看日志
215
- pm2 restart ccwx # 重启
216
- pm2 startup && pm2 save # 开机自启
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
- BRIDGE_PORT=8787 pm2 start claude-codex-wechat --name ccwx -- start
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
- ### 方式二:systemd(Linux)
212
+ 当前支持:
226
213
 
227
- 创建 `/etc/systemd/system/claude-codex-wechat.service`(把 `User` 和 `ExecStart` 路径换成你的实际值,`which claude-codex-wechat` 可查路径):
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
- 创建 `~/Library/LaunchAgents/com.claude-codex-wechat.plist`(把 `ProgramArguments` 第一项换成 `which claude-codex-wechat` 的实际路径):
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
- ```bash
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
- ### 1. 首次使用或微信 token 失效后恢复
293
+ - `npm version patch --no-git-tag-version`
301
294
 
302
- 最推荐的正式入口:
295
+ 也就是说:
303
296
 
304
- ```bash
305
- bash ./scripts/recover-weixin-runtime.sh
306
- ```
297
+ - 每次成功发布都会自动把版本加一位 `patch`
298
+ - 版本 bump 发生在真正 publish 之前
307
299
 
308
- 如果你要直接做 Codex 验收模式:
300
+ 完整的发布前校验定义在 [release.sh](./release.sh),`prepublishOnly` 只负责自动递增版本,避免在 `npm publish` 阶段重复再跑一遍测试和构建。
309
301
 
310
- ```bash
311
- BRIDGE_DEFAULT_PROVIDER=codex bash ./scripts/recover-weixin-runtime.sh
312
- ```
302
+ ### 直接发布
313
303
 
314
- 这条命令会自动完成:
304
+ 如果不需要自动提交版本变更:
315
305
 
316
- 1. 拉取新的微信登录二维码
317
- 2. 生成并自动打开二维码 SVG
318
- 3. 等待扫码确认
319
- 4. 写出新的微信凭据文件
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
- BRIDGE_DEFAULT_PROVIDER=codex
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
- 那么 runtime 启动后会自动把默认 provider 切到 `codex`。
318
+ ### 使用 release.sh
329
319
 
330
- 二维码文件默认路径:
320
+ 如果希望:
331
321
 
332
- ```text
333
- /tmp/bridge-weixin-login-qr.svg
334
- ```
322
+ - 发布前先提交当前工作区
323
+ - 发布后把自动 bump 的版本提交回当前分支
324
+ - 自动 `git push`
335
325
 
336
- 扫码状态文件默认路径:
326
+ 使用:
337
327
 
338
- ```text
339
- /tmp/bridge-weixin-login-state.json
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
- /tmp/bridge-weixin-credentials.json
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
- BRIDGE_PORT=8788 bash ./scripts/check-runtime-readiness.sh
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
- ### 2. WeChatPanel
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 --resume <providerSessionId>
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
- 按 bridge 标题:
369
+ 如果包名是否可用要先确认:
449
370
 
450
371
  ```bash
451
- claude -r '<完整标题>'
372
+ npm view claude-codex-wechat
452
373
  ```
453
374
 
454
- 要让 `claude -r` 真正可用,通常至少要满足:
375
+ ## 常见问题
455
376
 
456
- - `providerResumeTitleSynced = true`
457
- - `providerResumeHistorySynced = true`
377
+ ### `npm publish` `EPERM scandir ~/.Trash`
458
378
 
459
- ### Codex
379
+ 一般是因为你不在项目目录,而是在家目录执行了 `npm publish`。
460
380
 
461
- 按 session id:
381
+ 先确认:
462
382
 
463
383
  ```bash
464
- codex resume <providerSessionId>
384
+ pwd
465
385
  ```
466
386
 
467
- 按 bridge thread name:
387
+ 应该在:
468
388
 
469
389
  ```bash
470
- codex resume '<thread_name>'
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
- - `scripts/recover-weixin-runtime.sh`
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
- - `scripts/check-runtime-readiness.sh`
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
- bash ./scripts/recover-weixin-runtime.sh
405
+ npm config get registry
406
+ npm whoami
407
+ node -p "require('./package.json').name"
499
408
  ```
500
409
 
501
- 如果要直接推进 Codex 的真实闭环验收,建议用这一组:
410
+ ### 本地必须走代理
411
+
412
+ 临时发布可直接这样:
502
413
 
503
414
  ```bash
504
- BRIDGE_DEFAULT_PROVIDER=codex bash ./scripts/recover-weixin-runtime.sh
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
- - [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) —— 架构总览:整体架构、数据流、目录结构
557
- - [docs/README.md](./docs/README.md)
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)
@@ -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 existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
6
- import { dirname as dirname10, join as join10 } from "node:path";
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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 = dirname10(fileURLToPath(import.meta.url));
4015
- var webRoot = join10(here, "..", "web");
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 (!existsSync8(webRoot)) {
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 = dirname10(configPath);
4405
+ const dir = dirname11(configPath);
4058
4406
  mkdirSync6(dir, { recursive: true });
4059
- if (existsSync8(configPath)) {
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", existsSync8(configPath) ? configPath : `\u7F3A\u5931 (${configPath})\uFF0C\u8FD0\u884C init \u521B\u5EFA`);
4085
- report("\u524D\u7AEF\u4EA7\u7269", existsSync8(webRoot) ? webRoot : `\u7F3A\u5931 (${webRoot})`);
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 (existsSync8(configPath)) {
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 (!existsSync8(configPath)) {
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.0",
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 run build"
24
+ "prepublishOnly": "npm version patch --no-git-tag-version"
25
25
  },
26
26
  "dependencies": {
27
27
  "@fastify/static": "^9.1.3",