@izhimu/qq 0.1.1
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/LICENSE +21 -0
- package/README.md +388 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +19 -0
- package/dist/src/adapters/message.d.ts +8 -0
- package/dist/src/adapters/message.js +152 -0
- package/dist/src/channel.d.ts +7 -0
- package/dist/src/channel.js +312 -0
- package/dist/src/core/config.d.ts +22 -0
- package/dist/src/core/config.js +32 -0
- package/dist/src/core/connection.d.ts +65 -0
- package/dist/src/core/connection.js +328 -0
- package/dist/src/core/dispatch.d.ts +54 -0
- package/dist/src/core/dispatch.js +285 -0
- package/dist/src/core/request.d.ts +26 -0
- package/dist/src/core/request.js +115 -0
- package/dist/src/core/runtime.d.ts +16 -0
- package/dist/src/core/runtime.js +48 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +98 -0
- package/dist/src/types/index.d.ts +261 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/utils/cqcode.d.ts +33 -0
- package/dist/src/utils/cqcode.js +102 -0
- package/dist/src/utils/index.d.ts +51 -0
- package/dist/src/utils/index.js +250 -0
- package/dist/src/utils/log.d.ts +6 -0
- package/dist/src/utils/log.js +23 -0
- package/dist/src/utils/markdown.d.ts +29 -0
- package/dist/src/utils/markdown.js +137 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 izhimu
|
|
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,388 @@
|
|
|
1
|
+
# @izhimu/qq
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://opensource.org/licenses/MIT">
|
|
5
|
+
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://docs.openclaw.ai/">
|
|
8
|
+
<img src="https://img.shields.io/badge/OpenClaw-Plugin-blueviolet.svg" alt="OpenClaw">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/botuniverse/onebot-11">
|
|
11
|
+
<img src="https://img.shields.io/badge/Protocol-OneBot_11-yellow.svg" alt="Protocol">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://www.typescriptlang.org/">
|
|
14
|
+
<img src="https://img.shields.io/badge/Language-TypeScript-blue.svg" alt="TypeScript">
|
|
15
|
+
</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<strong>QQ Channel Plugin for OpenClaw</strong>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
通过 NapCat WebSocket API 连接 QQ 机器人,支持私聊、群聊、图片、回复等多种消息类型
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 目录
|
|
29
|
+
|
|
30
|
+
- [功能特性](#功能特性)
|
|
31
|
+
- [安装](#安装)
|
|
32
|
+
- [快速开始](#快速开始)
|
|
33
|
+
- [配置](#配置)
|
|
34
|
+
- [使用方法](#使用方法)
|
|
35
|
+
- [架构设计](#架构设计)
|
|
36
|
+
- [API 文档](#api-文档)
|
|
37
|
+
- [开发指南](#开发指南)
|
|
38
|
+
- [故障排查](#故障排查)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 功能特性
|
|
43
|
+
|
|
44
|
+
- **多渠道支持** - 同时支持 QQ 私聊和群聊
|
|
45
|
+
- **消息类型** - 文本、@提及、图片、表情、语音、文件、回复
|
|
46
|
+
- **实时通信** - WebSocket 全双工连接,支持自动重连
|
|
47
|
+
- **心跳监测** - 内置健康检查和连接状态监控
|
|
48
|
+
- **交互式配置** - 提供向导式配置界面
|
|
49
|
+
- **TypeScript** - 完整的类型定义和类型安全
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
### 前提条件
|
|
56
|
+
|
|
57
|
+
1. 安装 [OpenClaw](https://docs.openclaw.ai/) >= 2026.2.1
|
|
58
|
+
2. 安装并配置 [NapCat](https://github.com/NapNeko/NapCatQQ) WebSocket 服务
|
|
59
|
+
|
|
60
|
+
### 通过 OpenClaw CLI 安装
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 安装插件
|
|
64
|
+
openclaw plugins install @izhimu/qq
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 本地开发安装
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# 克隆仓库
|
|
71
|
+
git clone <repository-url>
|
|
72
|
+
cd openclaw-channel-qq
|
|
73
|
+
|
|
74
|
+
# 安装依赖
|
|
75
|
+
npm install
|
|
76
|
+
|
|
77
|
+
# 构建项目
|
|
78
|
+
npm run build
|
|
79
|
+
|
|
80
|
+
# 安装到 OpenClaw
|
|
81
|
+
openclaw plugins install /path/to/openclaw-channel-qq
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 快速开始
|
|
87
|
+
|
|
88
|
+
### 1. 配置 NapCat
|
|
89
|
+
|
|
90
|
+
在 NapCat 的 `config.yml` 中启用 WebSocket:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
ws:
|
|
94
|
+
servers:
|
|
95
|
+
- url: ws://0.0.0.0:3001
|
|
96
|
+
token: "" # 如需认证请设置
|
|
97
|
+
enableHeart: true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. 配置 OpenClaw
|
|
101
|
+
|
|
102
|
+
运行交互式配置向导:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
openclaw onboard
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
或手动编辑配置:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"channels": {
|
|
113
|
+
"qq": {
|
|
114
|
+
"wsUrl": "ws://127.0.0.1:3001",
|
|
115
|
+
"accessToken": "",
|
|
116
|
+
"enabled": true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3. 启动 Gateway
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
openclaw gateway restart
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 配置
|
|
131
|
+
|
|
132
|
+
### 配置选项
|
|
133
|
+
|
|
134
|
+
| 属性 | 类型 | 必填 | 默认值 | 描述 |
|
|
135
|
+
|------|------|------|--------|------|
|
|
136
|
+
| `wsUrl` | `string` | 是 | - | NapCat WebSocket 地址 |
|
|
137
|
+
| `accessToken` | `string` | 否 | `""` | 访问令牌(如配置了认证) |
|
|
138
|
+
| `enabled` | `boolean` | 否 | `true` | 是否启用该账号 |
|
|
139
|
+
|
|
140
|
+
### 配置示例
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"channels": {
|
|
145
|
+
"qq": {
|
|
146
|
+
"wsUrl": "ws://127.0.0.1:3001",
|
|
147
|
+
"accessToken": "your-token",
|
|
148
|
+
"enabled": true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 使用方法
|
|
157
|
+
|
|
158
|
+
### 发送消息
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# 发送私聊消息
|
|
162
|
+
openclaw message send "你好!" --to qq:private:123456789
|
|
163
|
+
|
|
164
|
+
# 发送群消息
|
|
165
|
+
openclaw message send "大家好!" --to qq:group:123456
|
|
166
|
+
|
|
167
|
+
# 带回复的消息
|
|
168
|
+
openclaw message send "回复你的消息" --to qq:private:123456789 --reply-to <message-id>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 检查状态
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# 查看频道状态
|
|
175
|
+
openclaw channels
|
|
176
|
+
|
|
177
|
+
# 查看日志
|
|
178
|
+
openclaw logs --channel qq
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 消息目标格式
|
|
182
|
+
|
|
183
|
+
| 类型 | 格式 | 示例 |
|
|
184
|
+
|------|------|------|
|
|
185
|
+
| 私聊 | `private:<QQ号>` | `qq:private:123456789` |
|
|
186
|
+
| 群聊 | `group:<群号>` | `qq:group:123456` |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 架构设计
|
|
191
|
+
|
|
192
|
+
### 项目结构
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
openclaw-channel-qq/
|
|
196
|
+
├── src/
|
|
197
|
+
│ ├── channel.ts # 插件主入口
|
|
198
|
+
│ ├── core/
|
|
199
|
+
│ │ ├── connection.ts # WebSocket 连接管理
|
|
200
|
+
│ │ ├── dispatch.ts # 事件分发器
|
|
201
|
+
│ │ ├── request.ts # API 请求处理
|
|
202
|
+
│ │ ├── runtime.ts # 运行时状态管理
|
|
203
|
+
│ │ └── config.ts # 配置解析
|
|
204
|
+
│ ├── adapters/
|
|
205
|
+
│ │ └── message.ts # 消息格式转换
|
|
206
|
+
│ ├── types/
|
|
207
|
+
│ │ └── index.ts # TypeScript 类型定义
|
|
208
|
+
│ ├── utils/
|
|
209
|
+
│ │ ├── index.ts # 工具函数
|
|
210
|
+
│ │ ├── log.ts # 日志工具
|
|
211
|
+
│ │ ├── markdown.ts # Markdown 处理
|
|
212
|
+
│ │ └── cqcode.ts # CQ 码解析
|
|
213
|
+
│ └── onboarding.ts # 交互式配置向导
|
|
214
|
+
├── docs/
|
|
215
|
+
│ ├── napcat-websocket-api.md # NapCat API 文档
|
|
216
|
+
│ └── plugin-development-guide.md # 插件开发指南
|
|
217
|
+
├── openclaw.plugin.json # 插件清单
|
|
218
|
+
├── package.json
|
|
219
|
+
└── tsconfig.json
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 核心组件
|
|
223
|
+
|
|
224
|
+
#### 1. ConnectionManager
|
|
225
|
+
|
|
226
|
+
负责 WebSocket 连接的生命周期管理:
|
|
227
|
+
- 连接建立和断开
|
|
228
|
+
- 自动重连机制(指数退避)
|
|
229
|
+
- 心跳监测
|
|
230
|
+
- 请求/响应关联
|
|
231
|
+
|
|
232
|
+
#### 2. MessageAdapter
|
|
233
|
+
|
|
234
|
+
处理 NapCat 与 OpenClaw 消息格式的双向转换:
|
|
235
|
+
- 入站消息解析(NapCat → OpenClaw)
|
|
236
|
+
- 出站消息构建(OpenClaw → NapCat)
|
|
237
|
+
- 支持多种消息段类型
|
|
238
|
+
|
|
239
|
+
#### 3. EventDispatcher
|
|
240
|
+
|
|
241
|
+
处理 NapCat 上报的各类事件:
|
|
242
|
+
- 消息事件(私聊、群聊)
|
|
243
|
+
- 通知事件(戳一戳、撤回等)
|
|
244
|
+
- 元事件(心跳、生命周期)
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## API 文档
|
|
249
|
+
|
|
250
|
+
### 支持的消息类型
|
|
251
|
+
|
|
252
|
+
| 类型 | 入站 | 出站 | 说明 |
|
|
253
|
+
|------|------|------|------|
|
|
254
|
+
| `text` | ✓ | ✓ | 文本消息 |
|
|
255
|
+
| `at` | ✓ | ✓ | @提及 |
|
|
256
|
+
| `image` | ✓ | ✓ | 图片 |
|
|
257
|
+
| `face` | ✓ | - | QQ 表情 |
|
|
258
|
+
| `reply` | ✓ | ✓ | 消息回复 |
|
|
259
|
+
| `record` | ✓ | - | 语音消息 |
|
|
260
|
+
| `file` | ✓ | - | 文件 |
|
|
261
|
+
| `json` | ✓ | - | JSON 富文本 |
|
|
262
|
+
|
|
263
|
+
### OneBot 11 接口
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// 发送消息
|
|
267
|
+
{
|
|
268
|
+
action: 'send_msg',
|
|
269
|
+
params: {
|
|
270
|
+
message_type: 'private' | 'group',
|
|
271
|
+
user_id?: string, // 私聊时必填
|
|
272
|
+
group_id?: string, // 群聊时必填
|
|
273
|
+
message: NapCatMessage[] | string
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 获取消息
|
|
278
|
+
{
|
|
279
|
+
action: 'get_msg',
|
|
280
|
+
params: {
|
|
281
|
+
message_id: number
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 获取运行状态
|
|
286
|
+
{
|
|
287
|
+
action: 'get_status',
|
|
288
|
+
params: {}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
完整 API 文档请参考 [NapCat WebSocket API 文档](./docs/napcat-websocket-api.md)。
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 开发指南
|
|
297
|
+
|
|
298
|
+
### 开发环境设置
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# 安装依赖
|
|
302
|
+
npm install
|
|
303
|
+
|
|
304
|
+
# 开发模式(热重载)
|
|
305
|
+
npm run dev
|
|
306
|
+
|
|
307
|
+
# 构建
|
|
308
|
+
npm run build
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 目录结构说明
|
|
312
|
+
|
|
313
|
+
| 目录 | 说明 |
|
|
314
|
+
|------|------|
|
|
315
|
+
| `src/core/` | 核心功能模块 |
|
|
316
|
+
| `src/adapters/` | 适配器(消息转换等) |
|
|
317
|
+
| `src/types/` | TypeScript 类型定义 |
|
|
318
|
+
| `src/utils/` | 工具函数 |
|
|
319
|
+
| `docs/` | 文档 |
|
|
320
|
+
| `openspec/` | OpenSpec 规范 |
|
|
321
|
+
|
|
322
|
+
### 插件开发参考
|
|
323
|
+
|
|
324
|
+
详细开发指南请参考 [插件开发指南](./docs/plugin-development-guide.md)。
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 故障排查
|
|
329
|
+
|
|
330
|
+
### 连接问题
|
|
331
|
+
|
|
332
|
+
1. **检查 NapCat 是否运行**
|
|
333
|
+
```bash
|
|
334
|
+
# 检查服务状态
|
|
335
|
+
curl http://localhost:3001/get_status
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
2. **验证 WebSocket 配置**
|
|
339
|
+
- 确认 `wsUrl` 与 NapCat 配置一致
|
|
340
|
+
- 检查访问令牌是否正确
|
|
341
|
+
|
|
342
|
+
3. **防火墙设置**
|
|
343
|
+
- 确保 WebSocket 端口未被防火墙拦截
|
|
344
|
+
|
|
345
|
+
### 消息发送失败
|
|
346
|
+
|
|
347
|
+
1. **检查账号状态**
|
|
348
|
+
```bash
|
|
349
|
+
openclaw channels
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
2. **查看日志**
|
|
353
|
+
```bash
|
|
354
|
+
openclaw logs --channel qq --verbose
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
3. **验证消息格式**
|
|
358
|
+
- 确保目标格式正确:`private:<QQ>` 或 `group:<群号>`
|
|
359
|
+
|
|
360
|
+
### 常见问题
|
|
361
|
+
|
|
362
|
+
| 问题 | 可能原因 | 解决方案 |
|
|
363
|
+
|------|----------|----------|
|
|
364
|
+
| 连接失败 | NapCat 未启动 | 启动 NapCat 服务 |
|
|
365
|
+
| 认证失败 | accessToken 错误 | 检查配置中的令牌 |
|
|
366
|
+
| 消息未收到 | 账号被禁用 | 检查 `enabled: true` |
|
|
367
|
+
| 图片发送失败 | URL 不可访问 | 确保图片 URL 可公网访问 |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 相关链接
|
|
372
|
+
|
|
373
|
+
- [OpenClaw 官方文档](https://docs.openclaw.ai/)
|
|
374
|
+
- [OpenClaw 插件开发指南](https://docs.openclaw.ai/plugin)
|
|
375
|
+
- [NapCat GitHub](https://github.com/NapNeko/NapCatQQ)
|
|
376
|
+
- [OneBot 11 协议标准](https://github.com/botuniverse/onebot-11)
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 开源协议
|
|
381
|
+
|
|
382
|
+
[MIT](LICENSE) © izhimu
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
<p align="center">
|
|
387
|
+
如有问题或建议,欢迎提交 Issue 或 PR
|
|
388
|
+
</p>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ NapCat Plugin Entry Point
|
|
3
|
+
* Exports the plugin for OpenClaw to load
|
|
4
|
+
*/
|
|
5
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
6
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
7
|
+
declare const plugin: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
configSchema: typeof emptyPluginConfigSchema;
|
|
12
|
+
register(api: OpenClawPluginApi): void;
|
|
13
|
+
};
|
|
14
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ NapCat Plugin Entry Point
|
|
3
|
+
* Exports the plugin for OpenClaw to load
|
|
4
|
+
*/
|
|
5
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
6
|
+
import { qqPlugin } from "./src/channel.js";
|
|
7
|
+
import { setRuntime } from "./src/core/runtime.js";
|
|
8
|
+
import { CHANNEL_ID } from "./src/core/config.js";
|
|
9
|
+
const plugin = {
|
|
10
|
+
id: CHANNEL_ID,
|
|
11
|
+
name: "QQ",
|
|
12
|
+
description: "QQ channel plugin for OpenClaw using NapCat WebSocket API",
|
|
13
|
+
configSchema: emptyPluginConfigSchema,
|
|
14
|
+
register(api) {
|
|
15
|
+
setRuntime(api.runtime);
|
|
16
|
+
api.registerChannel({ plugin: qqPlugin });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export default plugin;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Type Adapters for NapCat <-> OpenClaw conversion
|
|
3
|
+
*
|
|
4
|
+
* Optimized for maintainability with clear structure and minimal duplication.
|
|
5
|
+
*/
|
|
6
|
+
import type { NapCatMessage, OpenClawMessage } from '../types/index.js';
|
|
7
|
+
export declare function openClawToNapCatMessage(content: OpenClawMessage[], replyToId?: string): NapCatMessage[];
|
|
8
|
+
export declare function napCatToOpenClawMessage(segments: NapCatMessage[] | string): Promise<OpenClawMessage[]>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Type Adapters for NapCat <-> OpenClaw conversion
|
|
3
|
+
*
|
|
4
|
+
* Optimized for maintainability with clear structure and minimal duplication.
|
|
5
|
+
*/
|
|
6
|
+
import { Logger as log, extractImageUrl, getEmojiForFaceId } from '../utils/index.js';
|
|
7
|
+
import { CQCodeUtils } from '../utils/cqcode.js';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// CQ Code Parsing
|
|
10
|
+
// =============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Convert CQNode to NapCatMessageSegment
|
|
13
|
+
*/
|
|
14
|
+
function cqNodeToNapCat(node) {
|
|
15
|
+
return {
|
|
16
|
+
type: node.type,
|
|
17
|
+
data: node.data,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse CQ codes using CQCodeUtils and convert to NapCatMessageSegment[]
|
|
22
|
+
*/
|
|
23
|
+
function parseCQCode(text) {
|
|
24
|
+
const nodes = CQCodeUtils.parse(text);
|
|
25
|
+
return nodes.map(cqNodeToNapCat);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Normalize message to segments array (handles string or array format)
|
|
29
|
+
*/
|
|
30
|
+
function normalizeMessage(message) {
|
|
31
|
+
if (typeof message === 'string') {
|
|
32
|
+
return parseCQCode(message);
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(message)) {
|
|
35
|
+
log.warn('adapters', `Invalid message format: ${typeof message}`);
|
|
36
|
+
return [{ type: 'text', data: { text: String(message) } }];
|
|
37
|
+
}
|
|
38
|
+
return message;
|
|
39
|
+
}
|
|
40
|
+
function parseJsonSegment(segment) {
|
|
41
|
+
try {
|
|
42
|
+
const rawData = segment.data.data.trim();
|
|
43
|
+
let jsonData;
|
|
44
|
+
try {
|
|
45
|
+
jsonData = JSON.parse(rawData);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
log.warn('adapters', `Failed to parse JSON message: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
const result = {
|
|
51
|
+
type: 'json',
|
|
52
|
+
data: rawData,
|
|
53
|
+
};
|
|
54
|
+
if (jsonData?.prompt && jsonData.prompt.trim() !== '') {
|
|
55
|
+
result.prompt = jsonData.prompt;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
log.warn('adapters', `Failed to parse JSON message: ${error}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// NapCat -> OpenClaw Adapters (Inbound)
|
|
66
|
+
// =============================================================================
|
|
67
|
+
function napCatToOpenClaw(segment) {
|
|
68
|
+
const data = segment.data;
|
|
69
|
+
switch (segment.type) {
|
|
70
|
+
case 'text':
|
|
71
|
+
return { type: 'text', text: String(data.text || '') };
|
|
72
|
+
case 'at':
|
|
73
|
+
return {
|
|
74
|
+
type: 'at',
|
|
75
|
+
userId: String(data.qq || ''),
|
|
76
|
+
isAll: data.qq === 'all',
|
|
77
|
+
};
|
|
78
|
+
case 'image': {
|
|
79
|
+
const url = extractImageUrl(data);
|
|
80
|
+
return url ? { type: 'image', url, summary: data.summary } : null;
|
|
81
|
+
}
|
|
82
|
+
case 'reply':
|
|
83
|
+
return { type: 'reply', messageId: String(data.id || '') };
|
|
84
|
+
case 'face':
|
|
85
|
+
return { type: 'text', text: getEmojiForFaceId(String(data.id || '')) };
|
|
86
|
+
case 'record':
|
|
87
|
+
return data.path ? {
|
|
88
|
+
type: 'audio',
|
|
89
|
+
path: String(data.path),
|
|
90
|
+
file: String(data.file || ''),
|
|
91
|
+
url: data.url,
|
|
92
|
+
fileSize: data.file_size ? parseInt(String(data.file_size), 10) : undefined,
|
|
93
|
+
} : null;
|
|
94
|
+
case 'file':
|
|
95
|
+
return {
|
|
96
|
+
type: 'file',
|
|
97
|
+
fileId: String(data.file || ''),
|
|
98
|
+
fileSize: data.file_size ? parseInt(String(data.file_size), 10) : undefined
|
|
99
|
+
};
|
|
100
|
+
case 'json':
|
|
101
|
+
return parseJsonSegment(segment);
|
|
102
|
+
default:
|
|
103
|
+
log.warn('adapters', `Unknown message type (inbound): ${segment.type}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// OpenClaw -> NapCat Adapters (Outbound)
|
|
109
|
+
// =============================================================================
|
|
110
|
+
function openClawSegmentToNapCat(content) {
|
|
111
|
+
switch (content.type) {
|
|
112
|
+
case 'text':
|
|
113
|
+
return { type: 'text', data: { text: content.text } };
|
|
114
|
+
case 'at':
|
|
115
|
+
return { type: 'at', data: { qq: content.isAll ? 'all' : content.userId } };
|
|
116
|
+
case 'image':
|
|
117
|
+
return { type: 'image', data: { file: content.url, url: content.url } };
|
|
118
|
+
case 'reply':
|
|
119
|
+
return { type: 'reply', data: { id: content.messageId } };
|
|
120
|
+
case 'audio':
|
|
121
|
+
// These types are inbound-only for now
|
|
122
|
+
log.warn('adapters', `Unsupported outbound type: ${content.type}`);
|
|
123
|
+
return null;
|
|
124
|
+
default:
|
|
125
|
+
log.warn('adapters', `Unknown content type (outbound): ${content.type}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function openClawToNapCatMessage(content, replyToId) {
|
|
130
|
+
const segments = [];
|
|
131
|
+
if (replyToId) {
|
|
132
|
+
segments.push({ type: 'reply', data: { id: replyToId } });
|
|
133
|
+
}
|
|
134
|
+
for (const item of content) {
|
|
135
|
+
const segment = openClawSegmentToNapCat(item);
|
|
136
|
+
if (segment) {
|
|
137
|
+
segments.push(segment);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return segments;
|
|
141
|
+
}
|
|
142
|
+
export async function napCatToOpenClawMessage(segments) {
|
|
143
|
+
const normalized = normalizeMessage(segments);
|
|
144
|
+
const content = [];
|
|
145
|
+
for (const segment of normalized) {
|
|
146
|
+
const result = napCatToOpenClaw(segment);
|
|
147
|
+
if (result) {
|
|
148
|
+
content.push(result);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return content;
|
|
152
|
+
}
|