qq-codex-bridge 0.1.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/.env.example +58 -0
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/bin/qq-codex-bridge.js +11 -0
- package/dist/apps/bridge-daemon/src/bootstrap.js +100 -0
- package/dist/apps/bridge-daemon/src/cli.js +141 -0
- package/dist/apps/bridge-daemon/src/config.js +109 -0
- package/dist/apps/bridge-daemon/src/debug-codex-workers.js +309 -0
- package/dist/apps/bridge-daemon/src/dev-launch.js +73 -0
- package/dist/apps/bridge-daemon/src/dev.js +28 -0
- package/dist/apps/bridge-daemon/src/http-server.js +36 -0
- package/dist/apps/bridge-daemon/src/main.js +57 -0
- package/dist/apps/bridge-daemon/src/thread-command-handler.js +197 -0
- package/dist/packages/adapters/codex-desktop/src/cdp-session.js +189 -0
- package/dist/packages/adapters/codex-desktop/src/codex-desktop-driver.js +1259 -0
- package/dist/packages/adapters/codex-desktop/src/composer-heuristics.js +11 -0
- package/dist/packages/adapters/codex-desktop/src/health.js +7 -0
- package/dist/packages/adapters/codex-desktop/src/reply-parser.js +10 -0
- package/dist/packages/adapters/qq/src/qq-api-client.js +232 -0
- package/dist/packages/adapters/qq/src/qq-channel-adapter.js +22 -0
- package/dist/packages/adapters/qq/src/qq-gateway-client.js +295 -0
- package/dist/packages/adapters/qq/src/qq-gateway-session-store.js +64 -0
- package/dist/packages/adapters/qq/src/qq-gateway.js +62 -0
- package/dist/packages/adapters/qq/src/qq-media-downloader.js +246 -0
- package/dist/packages/adapters/qq/src/qq-media-parser.js +144 -0
- package/dist/packages/adapters/qq/src/qq-normalizer.js +35 -0
- package/dist/packages/adapters/qq/src/qq-sender.js +241 -0
- package/dist/packages/adapters/qq/src/qq-stt.js +189 -0
- package/dist/packages/domain/src/driver.js +7 -0
- package/dist/packages/domain/src/message.js +7 -0
- package/dist/packages/domain/src/session.js +7 -0
- package/dist/packages/orchestrator/src/bridge-orchestrator.js +143 -0
- package/dist/packages/orchestrator/src/job-runner.js +5 -0
- package/dist/packages/orchestrator/src/media-context.js +90 -0
- package/dist/packages/orchestrator/src/qq-outbound-draft.js +38 -0
- package/dist/packages/orchestrator/src/qq-outbound-format.js +51 -0
- package/dist/packages/orchestrator/src/qqbot-skill-context.js +13 -0
- package/dist/packages/orchestrator/src/session-key.js +6 -0
- package/dist/packages/ports/src/conversation.js +1 -0
- package/dist/packages/ports/src/qq.js +1 -0
- package/dist/packages/ports/src/store.js +1 -0
- package/dist/packages/store/src/message-repo.js +53 -0
- package/dist/packages/store/src/session-repo.js +80 -0
- package/dist/packages/store/src/sqlite.js +64 -0
- package/package.json +60 -0
package/.env.example
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# =========================
|
|
2
|
+
# QQ 官方机器人(必填)
|
|
3
|
+
# =========================
|
|
4
|
+
QQBOT_APP_ID=你的AppID
|
|
5
|
+
QQBOT_CLIENT_SECRET=你的ClientSecret
|
|
6
|
+
|
|
7
|
+
# =========================
|
|
8
|
+
# Codex Desktop
|
|
9
|
+
# =========================
|
|
10
|
+
CODEX_APP_NAME=Codex
|
|
11
|
+
CODEX_REMOTE_DEBUGGING_PORT=9229
|
|
12
|
+
|
|
13
|
+
# =========================
|
|
14
|
+
# Bridge 运行时
|
|
15
|
+
# =========================
|
|
16
|
+
QQ_CODEX_DATABASE_PATH=runtime/qq-codex-bridge.sqlite
|
|
17
|
+
|
|
18
|
+
# 这几个变量当前主要用于兼容旧 webhook 调试链路;
|
|
19
|
+
# 默认主链路是 QQ gateway WebSocket,不需要额外配置公网 webhook。
|
|
20
|
+
QQ_CODEX_LISTEN_HOST=127.0.0.1
|
|
21
|
+
QQ_CODEX_LISTEN_PORT=3100
|
|
22
|
+
QQ_CODEX_WEBHOOK_PATH=/webhooks/qq
|
|
23
|
+
|
|
24
|
+
# 是否启用 QQ markdown 文本发送。
|
|
25
|
+
# 默认推荐保持关闭,让普通文本尽量按原生聊天气泡渲染。
|
|
26
|
+
QQBOT_MARKDOWN_SUPPORT=false
|
|
27
|
+
|
|
28
|
+
# =========================
|
|
29
|
+
# STT(语音转文字)
|
|
30
|
+
# =========================
|
|
31
|
+
#
|
|
32
|
+
# 默认推荐:先不配置任何 QQBOT_STT_*。
|
|
33
|
+
# 这样项目会优先使用 QQ 事件里自带的 asr_refer_text。
|
|
34
|
+
#
|
|
35
|
+
# 如果你希望增强语音转写质量,再从下面 3 种模式中选一种启用。
|
|
36
|
+
|
|
37
|
+
# ---- 方案 A:OpenAI 兼容 STT ----
|
|
38
|
+
# QQBOT_STT_ENABLED=true
|
|
39
|
+
# QQBOT_STT_PROVIDER=openai-compatible
|
|
40
|
+
# QQBOT_STT_BASE_URL=https://api.openai.com/v1
|
|
41
|
+
# QQBOT_STT_API_KEY=你的APIKey
|
|
42
|
+
# QQBOT_STT_MODEL=whisper-1
|
|
43
|
+
|
|
44
|
+
# ---- 方案 B:火山引擎 Flash STT ----
|
|
45
|
+
# QQBOT_STT_ENABLED=true
|
|
46
|
+
# QQBOT_STT_PROVIDER=volcengine-flash
|
|
47
|
+
# QQBOT_STT_MODEL=bigmodel
|
|
48
|
+
# QQBOT_STT_APP_ID=你的AppID
|
|
49
|
+
# QQBOT_STT_ACCESS_KEY=你的AccessKey
|
|
50
|
+
# QQBOT_STT_RESOURCE_ID=volc.bigasr.auc_turbo
|
|
51
|
+
# QQBOT_STT_ENDPOINT=https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash
|
|
52
|
+
|
|
53
|
+
# ---- 方案 C:本地离线 whisper.cpp ----
|
|
54
|
+
# QQBOT_STT_ENABLED=true
|
|
55
|
+
# QQBOT_STT_PROVIDER=local-whisper-cpp
|
|
56
|
+
# QQBOT_STT_BINARY_PATH=/absolute/path/to/whisper-cli
|
|
57
|
+
# QQBOT_STT_MODEL_PATH=/absolute/path/to/ggml-model.bin
|
|
58
|
+
# QQBOT_STT_LANGUAGE=zh
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qq-codex-bridge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# qq-codex-bridge
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://pnpm.io/)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
一个把 **QQ 官方机器人会话** 桥接到 **Codex Desktop** 的开源实验项目。
|
|
11
|
+
|
|
12
|
+
它的核心目标很直接:
|
|
13
|
+
|
|
14
|
+
- 在 QQ 私聊 / 群聊里接收用户消息
|
|
15
|
+
- 按会话把消息映射到 Codex Desktop 线程
|
|
16
|
+
- 用 CDP 驱动 Codex Desktop 发送消息、切线程、读取回复
|
|
17
|
+
- 把 Codex 的文本、图片、文件、语音转写结果继续回送到 QQ
|
|
18
|
+
|
|
19
|
+
项目当前已经能跑真实链路,但仍然属于 **实验性可用** 阶段,更适合开发者联调、研究和二次改造,而不是直接当成稳定生产系统。
|
|
20
|
+
|
|
21
|
+
## 文档导航
|
|
22
|
+
|
|
23
|
+
- [快速开始](#快速开始)
|
|
24
|
+
- [FAQ 与故障排查](./docs/faq.md)
|
|
25
|
+
- [架构说明](./docs/architecture.md)
|
|
26
|
+
- [在线 Wiki(GitNexus 自动生成)](https://gistcdn.githack.com/983033995/3206d28d1bd71323166bc511b8681620/raw/index.html#overview)
|
|
27
|
+
- [测试说明](./docs/testing.md)
|
|
28
|
+
- [变更记录](./CHANGELOG.md)
|
|
29
|
+
- [贡献指南](./CONTRIBUTING.md)
|
|
30
|
+
- [安全策略](./SECURITY.md)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 项目特性
|
|
35
|
+
|
|
36
|
+
### 核心链路
|
|
37
|
+
|
|
38
|
+
- QQ 官方 Bot WebSocket gateway 入站
|
|
39
|
+
- QQ 私聊 / 群聊会话隔离
|
|
40
|
+
- Codex Desktop 启动检查与 CDP 连接
|
|
41
|
+
- QQ 消息映射到 Codex 线程
|
|
42
|
+
- Codex 回复增量采集并多次回传到 QQ
|
|
43
|
+
- SQLite 持久化会话、入站记录、出站任务
|
|
44
|
+
|
|
45
|
+
### 媒体与 STT
|
|
46
|
+
|
|
47
|
+
- QQ 附件下载与上下文注入
|
|
48
|
+
- 图片
|
|
49
|
+
- 语音
|
|
50
|
+
- 视频
|
|
51
|
+
- 文件
|
|
52
|
+
- 语音转文字
|
|
53
|
+
- QQ 自带 `asr_refer_text` 回退
|
|
54
|
+
- `openai-compatible`
|
|
55
|
+
- `volcengine-flash`
|
|
56
|
+
- 本地离线 `whisper.cpp`
|
|
57
|
+
- QQ 媒体回传
|
|
58
|
+
- 图片
|
|
59
|
+
- 音频
|
|
60
|
+
- 视频
|
|
61
|
+
- 文件
|
|
62
|
+
|
|
63
|
+
### Codex 回复处理
|
|
64
|
+
|
|
65
|
+
- 富文本链接提取
|
|
66
|
+
- 有序列表编号保留
|
|
67
|
+
- 代码块序列化为 fenced markdown
|
|
68
|
+
- 表格结构保留
|
|
69
|
+
- 长耗时任务回复采集窗口延长
|
|
70
|
+
- 同一轮回复中的媒体结果继续跟进,不再只截前几段文本
|
|
71
|
+
|
|
72
|
+
### 线程管理
|
|
73
|
+
|
|
74
|
+
仅私聊可用:
|
|
75
|
+
|
|
76
|
+
| 用途 | 完整命令 | 简写 |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 查看最近活跃线程 | `/threads` | `/t` |
|
|
79
|
+
| 查看当前绑定线程 | `/thread current` | `/tc` |
|
|
80
|
+
| 切换到指定线程 | `/thread use <序号>` | `/tu <序号>` |
|
|
81
|
+
| 新建线程 | `/thread new <标题>` | `/tn <标题>` |
|
|
82
|
+
| 基于最近对话 fork 线程 | `/thread fork <标题>` | `/tf <标题>` |
|
|
83
|
+
| 查看帮助 | `/help` | - |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 适用场景
|
|
88
|
+
|
|
89
|
+
- 想直接在 QQ 里把 Codex 当成一个“会话型桌面代理”来用
|
|
90
|
+
- 需要把图片、语音、文件上下文桥接给 Codex
|
|
91
|
+
- 想研究 Codex Desktop 的 CDP 自动化与回复采集
|
|
92
|
+
- 想复用 QQ 官方 Bot 能力,但不依赖 OpenClaw 宿主
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 项目效果
|
|
97
|
+
|
|
98
|
+
### Markdown 与代码回复
|
|
99
|
+
|
|
100
|
+
Codex 的列表、代码块、表格会尽量保留结构后再发给 QQ。
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
### 图片理解
|
|
105
|
+
|
|
106
|
+
发送图片给机器人,Codex 可以结合图片内容继续回答。
|
|
107
|
+
|
|
108
|
+

|
|
109
|
+
|
|
110
|
+
### 线程管理
|
|
111
|
+
|
|
112
|
+
在 QQ 私聊中直接查看最近活跃线程,并快速切换。
|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+
### AI 生图回传
|
|
117
|
+
|
|
118
|
+
Codex 调用图片生成工具后,桥接会把成品图片继续回传到 QQ。
|
|
119
|
+
|
|
120
|
+

|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 技术架构
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
QQ Official Bot Gateway
|
|
128
|
+
│
|
|
129
|
+
▼
|
|
130
|
+
QqGatewayClient / QqGateway
|
|
131
|
+
│
|
|
132
|
+
▼
|
|
133
|
+
BridgeOrchestrator
|
|
134
|
+
│
|
|
135
|
+
├── SessionStore / TranscriptStore (SQLite)
|
|
136
|
+
├── QqSender / QqApiClient
|
|
137
|
+
└── CodexDesktopDriver
|
|
138
|
+
│
|
|
139
|
+
└── Chrome DevTools Protocol
|
|
140
|
+
│
|
|
141
|
+
└── Codex Desktop
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
更贴近实际运行时的链路是:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
QQ 消息
|
|
148
|
+
-> 归一化 / 附件下载 / STT
|
|
149
|
+
-> 构造成 Codex 入站上下文
|
|
150
|
+
-> 注入 Codex Desktop
|
|
151
|
+
-> 轮询最新 assistant unit
|
|
152
|
+
-> 增量 draft
|
|
153
|
+
-> QQ 文本 / 媒体发送
|
|
154
|
+
-> SQLite 记录入站与出站任务
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 和官方项目的关系
|
|
160
|
+
|
|
161
|
+
这个项目参考了:
|
|
162
|
+
|
|
163
|
+
- [tencent-connect/openclaw-qqbot](https://github.com/tencent-connect/openclaw-qqbot)
|
|
164
|
+
- [openclaw/openclaw](https://github.com/openclaw/openclaw)
|
|
165
|
+
|
|
166
|
+
主要借鉴了这些思路:
|
|
167
|
+
|
|
168
|
+
- QQ gateway WebSocket 入站
|
|
169
|
+
- 出站消息路由与媒体发送
|
|
170
|
+
- 语音 / 文件处理方式
|
|
171
|
+
- Markdown 分块发送思路
|
|
172
|
+
- “增量回复 -> 分段回传”的设计方向
|
|
173
|
+
|
|
174
|
+
但两者仍然不同:
|
|
175
|
+
|
|
176
|
+
| 项目 | 运行位置 | 宿主 |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `openclaw-qqbot` | OpenClaw 插件 | OpenClaw |
|
|
179
|
+
| `qq-codex-bridge` | 本地 Node.js 进程 | Codex Desktop |
|
|
180
|
+
|
|
181
|
+
所以这个项目不是 OpenClaw 插件移植版,而是 **面向 Codex Desktop 的独立桥接实现**。
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 项目结构
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
apps/bridge-daemon/
|
|
189
|
+
src/
|
|
190
|
+
main.ts
|
|
191
|
+
bootstrap.ts
|
|
192
|
+
config.ts
|
|
193
|
+
thread-command-handler.ts
|
|
194
|
+
debug-codex-workers.ts
|
|
195
|
+
|
|
196
|
+
packages/adapters/qq/
|
|
197
|
+
src/
|
|
198
|
+
qq-gateway-client.ts
|
|
199
|
+
qq-gateway.ts
|
|
200
|
+
qq-api-client.ts
|
|
201
|
+
qq-sender.ts
|
|
202
|
+
qq-media-downloader.ts
|
|
203
|
+
qq-media-parser.ts
|
|
204
|
+
qq-stt.ts
|
|
205
|
+
|
|
206
|
+
packages/adapters/codex-desktop/
|
|
207
|
+
src/
|
|
208
|
+
cdp-session.ts
|
|
209
|
+
codex-desktop-driver.ts
|
|
210
|
+
composer-heuristics.ts
|
|
211
|
+
reply-parser.ts
|
|
212
|
+
|
|
213
|
+
packages/orchestrator/
|
|
214
|
+
src/
|
|
215
|
+
bridge-orchestrator.ts
|
|
216
|
+
media-context.ts
|
|
217
|
+
qq-outbound-draft.ts
|
|
218
|
+
qq-outbound-format.ts
|
|
219
|
+
qqbot-skill-context.ts
|
|
220
|
+
|
|
221
|
+
packages/store/
|
|
222
|
+
src/
|
|
223
|
+
sqlite.ts
|
|
224
|
+
session-repo.ts
|
|
225
|
+
message-repo.ts
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 环境要求
|
|
231
|
+
|
|
232
|
+
- macOS
|
|
233
|
+
- Node.js 20+
|
|
234
|
+
- 已安装 Codex Desktop
|
|
235
|
+
- Codex Desktop 可通过远程调试端口暴露 page target
|
|
236
|
+
- QQ 官方机器人 `AppID` 和 `ClientSecret`
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 快速开始
|
|
241
|
+
|
|
242
|
+
### 1. 生成当前目录配置
|
|
243
|
+
|
|
244
|
+
推荐直接用 `npx`:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npx qq-codex-bridge init
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
如果你更喜欢先全局安装:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm i -g qq-codex-bridge
|
|
254
|
+
qq-codex-bridge init
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
这一步会把包内置的模板写成当前目录下的 `.env`。
|
|
258
|
+
|
|
259
|
+
### 2. 填写 `.env`
|
|
260
|
+
|
|
261
|
+
最少需要配置这两个变量:
|
|
262
|
+
|
|
263
|
+
- `QQBOT_APP_ID`
|
|
264
|
+
- `QQBOT_CLIENT_SECRET`
|
|
265
|
+
|
|
266
|
+
### 3. 启动桥接
|
|
267
|
+
|
|
268
|
+
临时运行:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npx qq-codex-bridge
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
如果已经全局安装:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
qq-codex-bridge
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
默认端口仍然是:
|
|
281
|
+
|
|
282
|
+
- `CODEX_REMOTE_DEBUGGING_PORT=9229`
|
|
283
|
+
|
|
284
|
+
正式 CLI 会先检查 Codex Desktop 的 CDP 端口;如果尚未启动,会尽量自动拉起 Codex Desktop 后再继续启动桥接。
|
|
285
|
+
|
|
286
|
+
正常启动日志类似:
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
[qq-codex-bridge] codex desktop ready { launched: true|false, remoteDebuggingPort: 9229 }
|
|
290
|
+
[qq-codex-bridge] ready { transport: 'qq-gateway-websocket', accountKey: 'qqbot:default' }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 4. 在 QQ 中联调
|
|
294
|
+
|
|
295
|
+
建议先按这个顺序测试:
|
|
296
|
+
|
|
297
|
+
1. 普通文本
|
|
298
|
+
2. 一条会让 Codex 分阶段回答的问题
|
|
299
|
+
3. 一条语音
|
|
300
|
+
4. 一张图片
|
|
301
|
+
5. `/t` 与 `/tu 2`
|
|
302
|
+
|
|
303
|
+
### 5. 说明
|
|
304
|
+
|
|
305
|
+
- 这是“**无需源码启动**”,不是“**无依赖运行**”
|
|
306
|
+
- 你仍然需要本机已经安装 **Codex Desktop**
|
|
307
|
+
- 当前推荐平台仍然是 **macOS**
|
|
308
|
+
|
|
309
|
+
### 6. 开发者源码启动
|
|
310
|
+
|
|
311
|
+
如果你是要参与开发、调试源码或跑测试,再走下面这条路径:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
git clone https://github.com/983033995/qq-codex-bridge.git
|
|
315
|
+
cd qq-codex-bridge
|
|
316
|
+
pnpm install
|
|
317
|
+
cp .env.example .env
|
|
318
|
+
pnpm dev
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## STT 配置
|
|
324
|
+
|
|
325
|
+
项目支持三层语音转写策略。
|
|
326
|
+
|
|
327
|
+
### 1. 零配置模式
|
|
328
|
+
|
|
329
|
+
不配置任何 `QQBOT_STT_*` 时:
|
|
330
|
+
|
|
331
|
+
- 优先使用 QQ 事件里的 `asr_refer_text`
|
|
332
|
+
- 如果 QQ 没返回 ASR,再回退到附件占位
|
|
333
|
+
|
|
334
|
+
这是最适合开源项目默认体验的模式。
|
|
335
|
+
|
|
336
|
+
### 2. 云端 STT
|
|
337
|
+
|
|
338
|
+
支持:
|
|
339
|
+
|
|
340
|
+
- `openai-compatible`
|
|
341
|
+
- `volcengine-flash`
|
|
342
|
+
|
|
343
|
+
适合需要更高转写稳定性的人。
|
|
344
|
+
|
|
345
|
+
### 3. 本地离线 STT
|
|
346
|
+
|
|
347
|
+
支持:
|
|
348
|
+
|
|
349
|
+
- `local-whisper-cpp`
|
|
350
|
+
|
|
351
|
+
适合重视隐私、不想依赖云端 API 的人。
|
|
352
|
+
|
|
353
|
+
### STT 日志
|
|
354
|
+
|
|
355
|
+
当前会输出这些 STT 日志:
|
|
356
|
+
|
|
357
|
+
- `qq stt started`
|
|
358
|
+
- `qq stt completed`
|
|
359
|
+
- `qq stt produced no transcript`
|
|
360
|
+
- `qq stt fallback used`
|
|
361
|
+
- `qq stt failed`
|
|
362
|
+
|
|
363
|
+
日志中会带:
|
|
364
|
+
|
|
365
|
+
- `provider`
|
|
366
|
+
- `file`
|
|
367
|
+
- `extension`
|
|
368
|
+
- `durationMs`
|
|
369
|
+
- `hasAsrReferText`
|
|
370
|
+
- `transcriptPreview`
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## 当前实现上的一些保护逻辑
|
|
375
|
+
|
|
376
|
+
这几个点是项目和“最小 demo”相比比较重要的部分:
|
|
377
|
+
|
|
378
|
+
- **重复 QQ 入站抑制**
|
|
379
|
+
短时间内同一会话、同一正文、同一媒体指纹的重复消息会被拦下,避免同一句话多次注入 Codex。
|
|
380
|
+
|
|
381
|
+
- **长耗时任务回复采集窗口延长**
|
|
382
|
+
图片生成、长搜索、长工具执行不再因为默认 30 秒窗口被提前截断。
|
|
383
|
+
|
|
384
|
+
- **单条 draft 发送失败不再截断整轮回复**
|
|
385
|
+
某一条 QQ 发送失败时,后续 draft 仍会继续尝试发送。
|
|
386
|
+
|
|
387
|
+
- **可恢复错误不会把整个桥接打成不可用**
|
|
388
|
+
比如 `reply_timeout` 这类错误会被当成可恢复错误记录,不再直接把会话打成 `needs_rebind`。
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## 已知限制
|
|
393
|
+
|
|
394
|
+
- 核心能力依赖 Codex Desktop 当前版本的 DOM 结构和 CDP 可见性,桌面端改版后可能需要跟着适配。
|
|
395
|
+
- 对 Codex 回复的增量采集仍然是 **基于页面快照的伪流式**,不是官方内部事件流。
|
|
396
|
+
- QQ 客户端自己的消息样式、Markdown 支持、媒体卡片展示不完全可控。
|
|
397
|
+
- 线程管理命令目前只在 **QQ 私聊** 中开放。
|
|
398
|
+
- 某些极端场景下,如果 Codex Desktop 页面结构变化很大,线程定位与回复提取可能失效。
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 调试建议
|
|
403
|
+
|
|
404
|
+
### 查看类型检查
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
pnpm run check
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 运行测试
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
pnpm test
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 调试 Codex page / worker
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
pnpm run debug:codex-workers -- --duration-ms 12000
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 安全提醒
|
|
425
|
+
|
|
426
|
+
- `.env` 里会包含 QQ Bot、STT 等敏感密钥,不要提交到仓库
|
|
427
|
+
- 如果你把项目分享给别人,请务必轮换已经暴露过的密钥
|
|
428
|
+
- 本项目会处理用户消息、附件、语音与本地文件路径,联调时请注意隐私边界
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## 贡献
|
|
433
|
+
|
|
434
|
+
欢迎 issue、讨论和 PR。
|
|
435
|
+
|
|
436
|
+
在提交改动前,建议至少执行:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
pnpm run check
|
|
440
|
+
pnpm test
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
更多约定请看:
|
|
444
|
+
|
|
445
|
+
- [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
446
|
+
- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
|
447
|
+
- [SECURITY.md](./SECURITY.md)
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## License
|
|
452
|
+
|
|
453
|
+
本仓库使用 [MIT License](./LICENSE)。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import("../dist/apps/bridge-daemon/src/cli.js")
|
|
4
|
+
.then(({ runCliFromProcess }) => runCliFromProcess())
|
|
5
|
+
.catch((error) => {
|
|
6
|
+
console.error("[qq-codex-bridge] fatal:", error instanceof Error ? error.message : String(error));
|
|
7
|
+
if (error instanceof Error && error.stack) {
|
|
8
|
+
console.error(" stack:", error.stack);
|
|
9
|
+
}
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { QqApiClient } from "../../../packages/adapters/qq/src/qq-api-client.js";
|
|
3
|
+
import { createQqChannelAdapter } from "../../../packages/adapters/qq/src/qq-channel-adapter.js";
|
|
4
|
+
import { FileQqGatewaySessionStore } from "../../../packages/adapters/qq/src/qq-gateway-session-store.js";
|
|
5
|
+
import { CdpSession } from "../../../packages/adapters/codex-desktop/src/cdp-session.js";
|
|
6
|
+
import { CodexDesktopDriver } from "../../../packages/adapters/codex-desktop/src/codex-desktop-driver.js";
|
|
7
|
+
import { BridgeSessionStatus } from "../../../packages/domain/src/session.js";
|
|
8
|
+
import { BridgeOrchestrator } from "../../../packages/orchestrator/src/bridge-orchestrator.js";
|
|
9
|
+
import { buildCodexInboundText } from "../../../packages/orchestrator/src/media-context.js";
|
|
10
|
+
import { formatQqOutboundDraft } from "../../../packages/orchestrator/src/qq-outbound-format.js";
|
|
11
|
+
import { enrichQqOutboundDraft } from "../../../packages/orchestrator/src/qq-outbound-draft.js";
|
|
12
|
+
import { shouldInjectQqbotSkillContext } from "../../../packages/orchestrator/src/qqbot-skill-context.js";
|
|
13
|
+
import { SqliteTranscriptStore } from "../../../packages/store/src/message-repo.js";
|
|
14
|
+
import { SqliteSessionStore } from "../../../packages/store/src/session-repo.js";
|
|
15
|
+
import { createSqliteDatabase } from "../../../packages/store/src/sqlite.js";
|
|
16
|
+
import { loadConfigFromEnv } from "./config.js";
|
|
17
|
+
export function bootstrap() {
|
|
18
|
+
const config = loadConfigFromEnv(process.env);
|
|
19
|
+
const db = createSqliteDatabase(config.databasePath);
|
|
20
|
+
const sessionStore = new SqliteSessionStore(db);
|
|
21
|
+
const transcriptStore = new SqliteTranscriptStore(db);
|
|
22
|
+
const qqApiClient = new QqApiClient(config.qqBot.appId, config.qqBot.clientSecret, {
|
|
23
|
+
markdownSupport: config.qqBot.markdownSupport
|
|
24
|
+
});
|
|
25
|
+
const accountKey = "qqbot:default";
|
|
26
|
+
const qqGatewaySessionStore = new FileQqGatewaySessionStore(path.join(path.dirname(config.databasePath), "qq-gateway-session.json"), accountKey, config.qqBot.appId);
|
|
27
|
+
const adapters = {
|
|
28
|
+
qq: createQqChannelAdapter({
|
|
29
|
+
accountKey,
|
|
30
|
+
appId: config.qqBot.appId,
|
|
31
|
+
apiClient: qqApiClient,
|
|
32
|
+
sessionStore: qqGatewaySessionStore,
|
|
33
|
+
mediaDownloadDir: path.join(path.dirname(config.databasePath), "media"),
|
|
34
|
+
stt: config.qqBot.stt
|
|
35
|
+
}),
|
|
36
|
+
codexDesktop: new CodexDesktopDriver(new CdpSession({
|
|
37
|
+
appName: config.codexDesktop.appName,
|
|
38
|
+
remoteDebuggingPort: config.codexDesktop.remoteDebuggingPort
|
|
39
|
+
}))
|
|
40
|
+
};
|
|
41
|
+
const conversationProvider = {
|
|
42
|
+
runTurn: async (message, options) => {
|
|
43
|
+
await adapters.codexDesktop.ensureAppReady();
|
|
44
|
+
const session = await sessionStore.getSession(message.sessionKey);
|
|
45
|
+
const currentBinding = session
|
|
46
|
+
&& session.status === BridgeSessionStatus.Active
|
|
47
|
+
? {
|
|
48
|
+
sessionKey: session.sessionKey,
|
|
49
|
+
codexThreadRef: session.codexThreadRef
|
|
50
|
+
}
|
|
51
|
+
: null;
|
|
52
|
+
const binding = await adapters.codexDesktop.openOrBindSession(message.sessionKey, currentBinding);
|
|
53
|
+
const skillContextKey = shouldInjectQqbotSkillContext(message)
|
|
54
|
+
? `${binding.codexThreadRef ?? "unbound"}:qqbot-skill-v2`
|
|
55
|
+
: null;
|
|
56
|
+
const shouldIncludeSkillContext = skillContextKey !== null && session?.skillContextKey !== skillContextKey;
|
|
57
|
+
await adapters.codexDesktop.sendUserMessage(binding, {
|
|
58
|
+
...message,
|
|
59
|
+
text: buildCodexInboundText(message, {
|
|
60
|
+
includeSkillContext: shouldIncludeSkillContext
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
if (session?.codexThreadRef !== binding.codexThreadRef) {
|
|
64
|
+
await sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
|
|
65
|
+
}
|
|
66
|
+
if (shouldIncludeSkillContext) {
|
|
67
|
+
await sessionStore.updateSkillContextKey(message.sessionKey, skillContextKey);
|
|
68
|
+
}
|
|
69
|
+
const drafts = await adapters.codexDesktop.collectAssistantReply(binding, {
|
|
70
|
+
onDraft: options?.onDraft
|
|
71
|
+
? async (draft) => {
|
|
72
|
+
await options.onDraft(formatQqOutboundDraft(enrichQqOutboundDraft({
|
|
73
|
+
...draft,
|
|
74
|
+
replyToMessageId: message.messageId
|
|
75
|
+
})));
|
|
76
|
+
}
|
|
77
|
+
: undefined
|
|
78
|
+
});
|
|
79
|
+
return drafts.map((draft) => formatQqOutboundDraft(enrichQqOutboundDraft({
|
|
80
|
+
...draft,
|
|
81
|
+
replyToMessageId: message.messageId
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const orchestrator = new BridgeOrchestrator({
|
|
86
|
+
sessionStore,
|
|
87
|
+
transcriptStore,
|
|
88
|
+
conversationProvider,
|
|
89
|
+
qqEgress: adapters.qq.egress
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
config,
|
|
93
|
+
db,
|
|
94
|
+
sessionStore,
|
|
95
|
+
transcriptStore,
|
|
96
|
+
adapters,
|
|
97
|
+
orchestrator,
|
|
98
|
+
qqGatewaySessionStore
|
|
99
|
+
};
|
|
100
|
+
}
|