@izhimu/qq 0.2.0 → 0.2.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 -21
- package/README.md +388 -388
- package/dist/src/utils/markdown.d.ts +2 -5
- package/dist/src/utils/markdown.js +74 -69
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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
CHANGED
|
@@ -1,388 +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>
|
|
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>
|
|
@@ -3,22 +3,19 @@ export declare class MarkdownToText {
|
|
|
3
3
|
private maskPrefix;
|
|
4
4
|
private maskCounter;
|
|
5
5
|
/**
|
|
6
|
-
* 主入口:将 Markdown
|
|
7
|
-
* @param markdown 原始 Markdown 字符串
|
|
6
|
+
* 主入口:将 Markdown 转换为纯文本(移动端优化版)
|
|
8
7
|
*/
|
|
9
8
|
convert(markdown: string): string;
|
|
10
9
|
/**
|
|
11
10
|
* 保护代码块
|
|
12
|
-
* 支持 ```language 和 ~~~ 两种写法
|
|
13
11
|
*/
|
|
14
12
|
private maskCodeBlocks;
|
|
15
13
|
/**
|
|
16
14
|
* 保护行内代码
|
|
17
|
-
* `code` -> 使用单引号包裹,区别于普通文本
|
|
18
15
|
*/
|
|
19
16
|
private maskInlineCode;
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
18
|
+
* 还原掩码内容
|
|
22
19
|
*/
|
|
23
20
|
private unmaskContent;
|
|
24
21
|
/**
|
|
@@ -1,116 +1,121 @@
|
|
|
1
1
|
export class MarkdownToText {
|
|
2
|
-
// 用于暂存被保护的代码块,防止被正则误伤
|
|
3
2
|
codeBlockStore = new Map();
|
|
4
|
-
maskPrefix = '%%
|
|
3
|
+
maskPrefix = '%%MD-MASK-';
|
|
5
4
|
maskCounter = 0;
|
|
6
5
|
/**
|
|
7
|
-
* 主入口:将 Markdown
|
|
8
|
-
* @param markdown 原始 Markdown 字符串
|
|
6
|
+
* 主入口:将 Markdown 转换为纯文本(移动端优化版)
|
|
9
7
|
*/
|
|
10
8
|
convert(markdown) {
|
|
11
9
|
if (!markdown)
|
|
12
10
|
return '';
|
|
13
|
-
//
|
|
11
|
+
// 初始化
|
|
14
12
|
this.codeBlockStore.clear();
|
|
15
13
|
this.maskCounter = 0;
|
|
16
14
|
let text = markdown;
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// ============================================================
|
|
16
|
+
// 阶段 1: 保护性预处理 (Protect)
|
|
17
|
+
// ============================================================
|
|
19
18
|
text = this.maskCodeBlocks(text);
|
|
20
19
|
text = this.maskInlineCode(text);
|
|
21
|
-
//
|
|
22
|
-
// 2
|
|
20
|
+
// ============================================================
|
|
21
|
+
// 阶段 2: 优先处理特殊标签 (Priority Tags)
|
|
22
|
+
// ============================================================
|
|
23
|
+
// 2.1 图片 -> [图片] url(保留 URL,放在同一行)
|
|
24
|
+
text = text.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (_match, alt, url) => {
|
|
25
|
+
const displayText = alt ? `[图片: ${alt}]` : '[图片]';
|
|
26
|
+
return `${displayText} ${url}`;
|
|
27
|
+
});
|
|
28
|
+
// 2.2 自动链接 <http://...> -> http://...
|
|
29
|
+
text = text.replace(/<((?:https?|ftp|email|mailto):[^>]+)>/g, '$1');
|
|
30
|
+
// 2.3 普通链接 [Text](url) -> Text: url(冒号分隔,更清晰)
|
|
31
|
+
text = text.replace(/\[([^\]]+)]\(([^)]+)\)/g, (_match, linkText, url) => {
|
|
32
|
+
// 如果链接文本就是 URL,只显示一次
|
|
33
|
+
if (linkText === url || linkText.trim() === url.trim()) {
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
return `${linkText}: ${url}`;
|
|
37
|
+
});
|
|
38
|
+
// ============================================================
|
|
39
|
+
// 阶段 3: 结构化转换 & 清理 (Structure & Clean)
|
|
40
|
+
// ============================================================
|
|
41
|
+
// 3.1 预处理换行和分割线标签
|
|
23
42
|
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
24
|
-
text = text.replace(/<hr\s*\/?>/gi, '\n
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
text = text.replace(
|
|
29
|
-
|
|
30
|
-
text = text.replace(
|
|
31
|
-
|
|
32
|
-
text = text.replace(/^(
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
text = text.replace(/<hr\s*\/?>/gi, '\n---\n');
|
|
44
|
+
// 3.2 安全清理 HTML 标签
|
|
45
|
+
text = text.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, '');
|
|
46
|
+
text = text.replace(/<!--[\s\S]*?-->/g, '');
|
|
47
|
+
text = text.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, '');
|
|
48
|
+
// 3.3 标题 -> 简洁样式
|
|
49
|
+
text = text.replace(/^#\s+(.*)$/gm, '\n【$1】\n');
|
|
50
|
+
text = text.replace(/^##\s+(.*)$/gm, '\n■ $1\n');
|
|
51
|
+
text = text.replace(/^(#{3,6})\s+(.*)$/gm, '\n▸ $2\n');
|
|
52
|
+
// 3.4 Markdown 分割线
|
|
53
|
+
text = text.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '---');
|
|
54
|
+
// 3.5 引用
|
|
55
|
+
text = text.replace(/^(>+)\s?(.*)$/gm, (_match, arrows, content) => {
|
|
56
|
+
const level = arrows.length;
|
|
57
|
+
return level > 1 ? ` ${content}` : `${content}`;
|
|
37
58
|
});
|
|
38
|
-
//
|
|
39
|
-
text = text.replace(/^(\s*)-\s\[x]\s/gim, '$1
|
|
40
|
-
text = text.replace(/^(\s*)-\s\[\s]\s/gim, '$1
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
text = text.replace(/^(\s*)[-*+]\s+(.*)$/gm, '$1• $2');
|
|
44
|
-
// 有序列表 1. 2. 通常不需要改动,保留原样即可
|
|
45
|
-
// 2.7 表格 (Tables)
|
|
46
|
-
// 移除对齐行 |---|---|
|
|
59
|
+
// 3.6 任务列表 & 无序列表
|
|
60
|
+
text = text.replace(/^(\s*)-\s\[x]\s/gim, '$1✓ ');
|
|
61
|
+
text = text.replace(/^(\s*)-\s\[\s]\s/gim, '$1□ ');
|
|
62
|
+
text = text.replace(/^(\s*)[-*+]\s+(.*)$/gm, '$1· $2');
|
|
63
|
+
// 3.7 表格
|
|
47
64
|
text = text.replace(/^\s*\|?[\s\-:|]+\|?\s*$/gm, '');
|
|
48
|
-
// 将 | Cell | Cell | 转换为空格分隔,尽量保持一行
|
|
49
65
|
text = text.replace(/^\|(.*)\|$/gm, (_match, content) => {
|
|
50
|
-
|
|
51
|
-
return content.split('|').map((s) => s.trim()).join(' ');
|
|
66
|
+
return content.split('|').map((s) => s.trim()).join(' | ');
|
|
52
67
|
});
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
68
|
+
// ============================================================
|
|
69
|
+
// 阶段 4: 行内格式 (Inline Formatting)
|
|
70
|
+
// ============================================================
|
|
71
|
+
// 4.1 粗体 -> 保留文字
|
|
72
|
+
text = text.replace(/(\*\*|__)([\s\S]*?)\1/g, '$2');
|
|
73
|
+
// 4.2 斜体 -> 保留文字
|
|
59
74
|
text = text.replace(/([*_])([\s\S]*?)\1/g, '$2');
|
|
60
|
-
//
|
|
75
|
+
// 4.3 删除线 -> 保留文字
|
|
61
76
|
text = text.replace(/~~([\s\S]*?)~~/g, '$1');
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// 3.5 链接 (Links) -> Text (URL)
|
|
67
|
-
// 排除锚点链接或空链接
|
|
68
|
-
text = text.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1 ($2)');
|
|
69
|
-
// 处理自动链接 <http://example.com>
|
|
70
|
-
text = text.replace(/<((?:https?|ftp|email):[^>]+)>/g, '$1');
|
|
71
|
-
// --- 阶段 4: 还原与收尾 (Restore & Finalize) ---
|
|
72
|
-
// 4.1 还原代码块
|
|
77
|
+
// ============================================================
|
|
78
|
+
// 阶段 5: 还原与收尾 (Restore & Finalize)
|
|
79
|
+
// ============================================================
|
|
80
|
+
// 5.1 还原代码块
|
|
73
81
|
text = this.unmaskContent(text);
|
|
74
|
-
//
|
|
82
|
+
// 5.2 解码 HTML 实体
|
|
75
83
|
text = this.decodeHtmlEntities(text);
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
text = text.replace(
|
|
84
|
+
// 5.3 最终排版优化
|
|
85
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
86
|
+
text = text.replace(/[ \t]+/g, ' ');
|
|
87
|
+
text = text.replace(/^\s+|\s+$/gm, '');
|
|
88
|
+
text = text.trim();
|
|
79
89
|
return text;
|
|
80
90
|
}
|
|
81
91
|
/**
|
|
82
92
|
* 保护代码块
|
|
83
|
-
* 支持 ```language 和 ~~~ 两种写法
|
|
84
93
|
*/
|
|
85
94
|
maskCodeBlocks(text) {
|
|
86
|
-
|
|
87
|
-
const codeBlockRegex = /(`{3,}|~{3,})(\w*)\n([\s\S]*?)\1/g;
|
|
95
|
+
const codeBlockRegex = /(`{3,}|~{3,})(\w*)\n?([\s\S]*?)\n?\1/g;
|
|
88
96
|
return text.replace(codeBlockRegex, (_match, _fence, lang, code) => {
|
|
89
|
-
const key = `${this.maskPrefix}
|
|
90
|
-
const langTag = lang ? `
|
|
91
|
-
|
|
92
|
-
const formatted = `\n────────────────────${langTag}\n${code.replace(/^\n+|\n+$/g, '')}\n────────────────────\n`;
|
|
97
|
+
const key = `${this.maskPrefix}BLOCK-${this.maskCounter++}`;
|
|
98
|
+
const langTag = lang ? `[${lang}]\n` : '';
|
|
99
|
+
const formatted = `\n${langTag}${code.trim()}\n`;
|
|
93
100
|
this.codeBlockStore.set(key, formatted);
|
|
94
101
|
return key;
|
|
95
102
|
});
|
|
96
103
|
}
|
|
97
104
|
/**
|
|
98
105
|
* 保护行内代码
|
|
99
|
-
* `code` -> 使用单引号包裹,区别于普通文本
|
|
100
106
|
*/
|
|
101
107
|
maskInlineCode(text) {
|
|
102
108
|
return text.replace(/`([^`]+)`/g, (_match, code) => {
|
|
103
|
-
const key = `${this.maskPrefix}
|
|
104
|
-
this.codeBlockStore.set(key, `
|
|
109
|
+
const key = `${this.maskPrefix}INLINE-${this.maskCounter++}`;
|
|
110
|
+
this.codeBlockStore.set(key, `'${code}'`);
|
|
105
111
|
return key;
|
|
106
112
|
});
|
|
107
113
|
}
|
|
108
114
|
/**
|
|
109
|
-
*
|
|
115
|
+
* 还原掩码内容
|
|
110
116
|
*/
|
|
111
117
|
unmaskContent(text) {
|
|
112
|
-
|
|
113
|
-
const maskRegex = new RegExp(`${this.maskPrefix}\\w+_\\d+`, 'g');
|
|
118
|
+
const maskRegex = new RegExp(`${this.maskPrefix}[\\w-]+`, 'g');
|
|
114
119
|
return text.replace(maskRegex, (key) => {
|
|
115
120
|
return this.codeBlockStore.get(key) || '';
|
|
116
121
|
});
|