@izhimu/qq 0.3.2 → 0.5.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/README.md +51 -2
- package/dist/src/adapters/message.d.ts +1 -1
- package/dist/src/adapters/message.js +7 -1
- package/dist/src/channel.js +63 -6
- package/dist/src/core/config.d.ts +58 -0
- package/dist/src/core/config.js +38 -3
- package/dist/src/core/dispatch.js +193 -35
- package/dist/src/core/request.d.ts +13 -1
- package/dist/src/core/request.js +33 -0
- package/dist/src/core/runtime.d.ts +8 -2
- package/dist/src/core/runtime.js +36 -0
- package/dist/src/types/index.d.ts +56 -15
- package/dist/src/utils/markdown.js +3 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
29
|
---
|
|
30
|
-
|
|
30
|
+

|
|
31
31
|
## 目录
|
|
32
32
|
|
|
33
33
|
- [功能特性](#功能特性)
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
```bash
|
|
68
68
|
# 安装插件
|
|
69
69
|
openclaw plugins install @izhimu/qq
|
|
70
|
+
|
|
71
|
+
# 更新插件
|
|
72
|
+
openclaw plugins update @izhimu/qq
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
### 本地开发安装
|
|
@@ -141,6 +144,10 @@ openclaw gateway restart
|
|
|
141
144
|
| `wsUrl` | `string` | 是 | - | NapCat WebSocket 地址 |
|
|
142
145
|
| `accessToken` | `string` | 否 | `""` | 访问令牌(如配置了认证) |
|
|
143
146
|
| `enabled` | `boolean` | 否 | `true` | 是否启用该账号 |
|
|
147
|
+
| `markdownFormat` | `boolean` | 否 | `true` | 是否启用 Markdown 格式化转换 |
|
|
148
|
+
| `messageDirect` | `object` | 否 | - | 私聊全局配置(策略、黑白名单) |
|
|
149
|
+
| `messageGroup` | `object` | 否 | - | 群组全局配置(@响应、戳一戳、唤醒词等) |
|
|
150
|
+
| `messageGroupsCustom` | `object` | 否 | `{}` | 特定群组的独立配置 |
|
|
144
151
|
|
|
145
152
|
### 配置示例
|
|
146
153
|
|
|
@@ -150,7 +157,19 @@ openclaw gateway restart
|
|
|
150
157
|
"qq": {
|
|
151
158
|
"wsUrl": "ws://127.0.0.1:3001",
|
|
152
159
|
"accessToken": "your-token",
|
|
153
|
-
"enabled": true
|
|
160
|
+
"enabled": true,
|
|
161
|
+
"markdownFormat": true,
|
|
162
|
+
"messageDirect": {
|
|
163
|
+
"policy": "allow",
|
|
164
|
+
"denyFrom": ["12345678"]
|
|
165
|
+
},
|
|
166
|
+
"messageGroup": {
|
|
167
|
+
"requireMention": true,
|
|
168
|
+
"requirePoke": true,
|
|
169
|
+
"policy": "allowlist",
|
|
170
|
+
"allowFrom": ["123456"],
|
|
171
|
+
"wakeWord": "小艺"
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
}
|
|
@@ -259,6 +278,7 @@ openclaw-channel-qq/
|
|
|
259
278
|
| `reply` | ✓ | ✓ | 消息回复 |
|
|
260
279
|
| `record` | ✓ | ✓ | 语音消息 |
|
|
261
280
|
| `file` | ✓ | ✓ | 文件 |
|
|
281
|
+
| `video` | ✓ | - | 视频消息 |
|
|
262
282
|
| `json` | ✓ | - | JSON 富文本 |
|
|
263
283
|
|
|
264
284
|
### OneBot 11 接口
|
|
@@ -371,6 +391,35 @@ npm run build
|
|
|
371
391
|
|
|
372
392
|
## 更新日志
|
|
373
393
|
|
|
394
|
+
### [0.5.0] - 2026-03-11
|
|
395
|
+
|
|
396
|
+
#### 新增
|
|
397
|
+
- **多媒体支持增强**:新增对视频(`video`)消息类型的入站支持。
|
|
398
|
+
- **细粒度访问控制**:
|
|
399
|
+
- 新增 `messageDirect` 和 `messageGroup` 配置项,支持私聊和群组的独立策略控制(`allow`, `deny`, `allowlist`)。
|
|
400
|
+
- 支持基于用户 ID 的黑白名单过滤。
|
|
401
|
+
- **群组触发增强**:
|
|
402
|
+
- 支持自定义群组唤醒词(`wakeWord`)。
|
|
403
|
+
- 支持开启/关闭戳一戳(`requirePoke`)响应。
|
|
404
|
+
- 支持针对特定群组进行独立配置(`messageGroupsCustom`)。
|
|
405
|
+
- **Markdown 优化**:新增 `markdownFormat` 开关,可选择是否将 Markdown 转换为纯文本。
|
|
406
|
+
|
|
407
|
+
#### 优化
|
|
408
|
+
- **配置结构重构**:重构了配置解析逻辑,提高了配置项的灵活性。
|
|
409
|
+
- **消息文本转换**:优化了多媒体消息(图片、视频、音频、文件等)在日志和历史记录中的纯文本展示效果。
|
|
410
|
+
- **稳定性**:增加了 Gateway Context 的预检,提升了系统的健壮性。
|
|
411
|
+
|
|
412
|
+
### [0.4.0] - 2026-03-07
|
|
413
|
+
|
|
414
|
+
#### 新增
|
|
415
|
+
- 群 At 模式(`groupAtMode`)- 开启后只有被 @ 或 @全体成员 时才回复
|
|
416
|
+
- 登录信息存储功能,获取并保存当前登录 QQ 号
|
|
417
|
+
- 消息中断处理机制,新消息到来时正确终止上一条消息的回复
|
|
418
|
+
|
|
419
|
+
#### 修复
|
|
420
|
+
- 修复 abort 后 deliver 仍然发送已终止消息的问题
|
|
421
|
+
- 修复 abortController 状态检查不准确的问题(使用独立 aborted 标志)
|
|
422
|
+
|
|
374
423
|
### [0.3.0] - 2026-02-12
|
|
375
424
|
|
|
376
425
|
#### 新增
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Optimized for maintainability with clear structure and minimal duplication.
|
|
5
5
|
*/
|
|
6
|
-
import type { NapCatMessage, OpenClawMessage } from '../types
|
|
6
|
+
import type { NapCatMessage, OpenClawMessage } from '../types';
|
|
7
7
|
export declare function openClawToNapCatMessage(content: OpenClawMessage[]): NapCatMessage[];
|
|
8
8
|
export declare function napCatToOpenClawMessage(segments: NapCatMessage[] | string): Promise<OpenClawMessage[]>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Optimized for maintainability with clear structure and minimal duplication.
|
|
5
5
|
*/
|
|
6
6
|
import { Logger as log, extractImageUrl, getEmojiForFaceId } from '../utils/index.js';
|
|
7
|
-
import { CQCodeUtils } from '../utils
|
|
7
|
+
import { CQCodeUtils } from '../utils';
|
|
8
8
|
import { getMsg } from "../core/request.js";
|
|
9
9
|
// =============================================================================
|
|
10
10
|
// CQ Code Parsing
|
|
@@ -94,6 +94,12 @@ async function napCatToOpenClaw(segment) {
|
|
|
94
94
|
senderId: String(response.data.sender.user_id),
|
|
95
95
|
sender: response.data.sender.nickname
|
|
96
96
|
};
|
|
97
|
+
case 'video':
|
|
98
|
+
return {
|
|
99
|
+
type: 'video',
|
|
100
|
+
url: String(data.url || ''),
|
|
101
|
+
fileSize: data.file_size ? parseInt(String(data.file_size), 10) : undefined,
|
|
102
|
+
};
|
|
97
103
|
case 'face':
|
|
98
104
|
return { type: 'text', text: getEmojiForFaceId(String(data.id || '')) };
|
|
99
105
|
case 'record':
|
package/dist/src/channel.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* QQ NapCat Plugin for OpenClaw
|
|
3
3
|
* Main plugin entry point
|
|
4
4
|
*/
|
|
5
|
-
import { buildChannelConfigSchema, setAccountEnabledInConfigSection, deleteAccountFromConfigSection
|
|
5
|
+
import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from "openclaw/plugin-sdk";
|
|
6
6
|
import { messageIdToString, markdownToText, buildMediaMessage, Logger as log } from "./utils/index.js";
|
|
7
|
-
import { setContext, setContextStatus, clearContext, setConnection, getConnection, clearConnection } from "./core/runtime.js";
|
|
7
|
+
import { setContext, setContextStatus, clearContext, setConnection, getConnection, clearConnection, setLoginInfo, getContext } from "./core/runtime.js";
|
|
8
8
|
import { ConnectionManager } from "./core/connection.js";
|
|
9
9
|
import { openClawToNapCatMessage } from "./adapters/message.js";
|
|
10
10
|
import { listQQAccountIds, resolveQQAccount, QQConfigSchema, CHANNEL_ID } from "./core/config.js";
|
|
11
|
-
import { eventListener, sendMsg, getStatus } from "./core/request.js";
|
|
11
|
+
import { eventListener, sendMsg, getStatus, getLoginInfo, getFriendList, getGroupList } from "./core/request.js";
|
|
12
12
|
import { qqOnboardingAdapter } from "./onboarding.js";
|
|
13
13
|
export const qqPlugin = {
|
|
14
14
|
id: CHANNEL_ID,
|
|
@@ -16,7 +16,7 @@ export const qqPlugin = {
|
|
|
16
16
|
id: CHANNEL_ID,
|
|
17
17
|
label: "QQ",
|
|
18
18
|
selectionLabel: "QQ",
|
|
19
|
-
docsPath: "/
|
|
19
|
+
docsPath: "extensions/qq",
|
|
20
20
|
blurb: "通过 NapCat WebSocket 连接 QQ 机器人",
|
|
21
21
|
quickstartAllowFrom: true,
|
|
22
22
|
},
|
|
@@ -104,11 +104,14 @@ export const qqPlugin = {
|
|
|
104
104
|
}),
|
|
105
105
|
probeAccount: async () => {
|
|
106
106
|
const status = await getStatus();
|
|
107
|
+
const ok = status.status === "ok";
|
|
107
108
|
setContextStatus({
|
|
109
|
+
linked: ok,
|
|
110
|
+
running: ok,
|
|
108
111
|
lastProbeAt: Date.now(),
|
|
109
112
|
});
|
|
110
113
|
return {
|
|
111
|
-
ok:
|
|
114
|
+
ok: ok,
|
|
112
115
|
status: status.retcode,
|
|
113
116
|
error: status.status === "failed" ? status.msg : null,
|
|
114
117
|
};
|
|
@@ -176,6 +179,14 @@ export const qqPlugin = {
|
|
|
176
179
|
try {
|
|
177
180
|
await connection.start();
|
|
178
181
|
setConnection(connection);
|
|
182
|
+
// 获取登录信息
|
|
183
|
+
const info = await getLoginInfo();
|
|
184
|
+
if (info.data) {
|
|
185
|
+
setLoginInfo({
|
|
186
|
+
userId: info.data.user_id.toString(),
|
|
187
|
+
nickname: info.data.nickname,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
179
190
|
// Update start time
|
|
180
191
|
setContextStatus({
|
|
181
192
|
running: true,
|
|
@@ -210,6 +221,24 @@ export const qqPlugin = {
|
|
|
210
221
|
});
|
|
211
222
|
clearContext();
|
|
212
223
|
},
|
|
224
|
+
},
|
|
225
|
+
directory: {
|
|
226
|
+
self: async () => {
|
|
227
|
+
const info = await getLoginInfo();
|
|
228
|
+
if (!info.data) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
log.debug('directory', `self: ${JSON.stringify(info.data)}`);
|
|
232
|
+
return {
|
|
233
|
+
kind: "user",
|
|
234
|
+
id: info.data.user_id.toString(),
|
|
235
|
+
name: info.data.nickname,
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
listPeers: getFriends,
|
|
239
|
+
listPeersLive: getFriends,
|
|
240
|
+
listGroups: getGroups,
|
|
241
|
+
listGroupsLive: getGroups,
|
|
213
242
|
}
|
|
214
243
|
};
|
|
215
244
|
async function outboundSend(ctx) {
|
|
@@ -221,8 +250,18 @@ async function outboundSend(ctx) {
|
|
|
221
250
|
const chatType = type === "group" ? "group" : "private";
|
|
222
251
|
const chatId = id || to;
|
|
223
252
|
const content = [];
|
|
253
|
+
const context = getContext();
|
|
254
|
+
if (!context) {
|
|
255
|
+
log.warn('dispatch', `No gateway context`);
|
|
256
|
+
return {
|
|
257
|
+
channel: CHANNEL_ID,
|
|
258
|
+
messageId: "",
|
|
259
|
+
error: new Error(`No gateway context`),
|
|
260
|
+
deliveredAt: Date.now(),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
224
263
|
if (text) {
|
|
225
|
-
content.push({ type: "text", text: markdownToText(text) });
|
|
264
|
+
content.push({ type: "text", text: context.account.markdownFormat ? markdownToText(text) : text });
|
|
226
265
|
}
|
|
227
266
|
if (mediaUrl) {
|
|
228
267
|
content.push(buildMediaMessage(mediaUrl));
|
|
@@ -265,3 +304,21 @@ async function outboundSend(ctx) {
|
|
|
265
304
|
};
|
|
266
305
|
}
|
|
267
306
|
}
|
|
307
|
+
async function getFriends() {
|
|
308
|
+
const friendList = await getFriendList();
|
|
309
|
+
log.debug('directory', `friendList: ${JSON.stringify(friendList.data)}`);
|
|
310
|
+
return (friendList.data || []).map((friend) => ({
|
|
311
|
+
kind: "user",
|
|
312
|
+
id: friend.user_id.toString(),
|
|
313
|
+
name: friend.nickname,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
async function getGroups() {
|
|
317
|
+
const groupList = await getGroupList();
|
|
318
|
+
log.debug('directory', `groupList: ${JSON.stringify(groupList.data)}`);
|
|
319
|
+
return (groupList.data || []).map((group) => ({
|
|
320
|
+
kind: "group",
|
|
321
|
+
id: group.group_id.toString(),
|
|
322
|
+
name: group.group_name,
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
@@ -15,8 +15,66 @@ export declare function listQQAccountIds(cfg: OpenClawConfig): string[];
|
|
|
15
15
|
export declare function resolveQQAccount(params: {
|
|
16
16
|
cfg: OpenClawConfig;
|
|
17
17
|
}): QQConfig;
|
|
18
|
+
export declare const QQDirectConfigSchema: z.ZodObject<{
|
|
19
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
20
|
+
allow: "allow";
|
|
21
|
+
deny: "deny";
|
|
22
|
+
allowlist: "allowlist";
|
|
23
|
+
}>>;
|
|
24
|
+
allowFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
25
|
+
denyFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
export declare const QQGroupConfigSchema: z.ZodObject<{
|
|
28
|
+
requireMention: z.ZodDefault<z.ZodBoolean>;
|
|
29
|
+
requirePoke: z.ZodDefault<z.ZodBoolean>;
|
|
30
|
+
historyLimit: z.ZodDefault<z.ZodNumber>;
|
|
31
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
32
|
+
allow: "allow";
|
|
33
|
+
deny: "deny";
|
|
34
|
+
allowlist: "allowlist";
|
|
35
|
+
}>>;
|
|
36
|
+
allowFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
37
|
+
denyFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
38
|
+
wakeWord: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strip>;
|
|
18
40
|
export declare const QQConfigSchema: z.ZodObject<{
|
|
19
41
|
wsUrl: z.ZodDefault<z.ZodString>;
|
|
20
42
|
accessToken: z.ZodDefault<z.ZodString>;
|
|
21
43
|
enable: z.ZodDefault<z.ZodBoolean>;
|
|
44
|
+
markdownFormat: z.ZodDefault<z.ZodBoolean>;
|
|
45
|
+
messageDirect: z.ZodObject<{
|
|
46
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
47
|
+
allow: "allow";
|
|
48
|
+
deny: "deny";
|
|
49
|
+
allowlist: "allowlist";
|
|
50
|
+
}>>;
|
|
51
|
+
allowFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
52
|
+
denyFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
messageGroup: z.ZodObject<{
|
|
55
|
+
requireMention: z.ZodDefault<z.ZodBoolean>;
|
|
56
|
+
requirePoke: z.ZodDefault<z.ZodBoolean>;
|
|
57
|
+
historyLimit: z.ZodDefault<z.ZodNumber>;
|
|
58
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
59
|
+
allow: "allow";
|
|
60
|
+
deny: "deny";
|
|
61
|
+
allowlist: "allowlist";
|
|
62
|
+
}>>;
|
|
63
|
+
allowFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
64
|
+
denyFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
65
|
+
wakeWord: z.ZodOptional<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
messageGroupsCustom: z.ZodOptional<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
68
|
+
requireMention: z.ZodDefault<z.ZodBoolean>;
|
|
69
|
+
requirePoke: z.ZodDefault<z.ZodBoolean>;
|
|
70
|
+
historyLimit: z.ZodDefault<z.ZodNumber>;
|
|
71
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
72
|
+
allow: "allow";
|
|
73
|
+
deny: "deny";
|
|
74
|
+
allowlist: "allowlist";
|
|
75
|
+
}>>;
|
|
76
|
+
allowFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
77
|
+
denyFrom: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
|
|
78
|
+
wakeWord: z.ZodOptional<z.ZodString>;
|
|
79
|
+
}, z.core.$strip>>>>;
|
|
22
80
|
}, z.core.$strip>;
|
package/dist/src/core/config.js
CHANGED
|
@@ -23,6 +23,22 @@ export function resolveQQAccount(params) {
|
|
|
23
23
|
enabled: config?.enabled !== false,
|
|
24
24
|
wsUrl: config?.wsUrl ?? "",
|
|
25
25
|
accessToken: config?.accessToken,
|
|
26
|
+
markdownFormat: config?.markdownFormat ?? true,
|
|
27
|
+
messageDirect: {
|
|
28
|
+
policy: config?.messageDirect?.policy ?? "allow",
|
|
29
|
+
allowFrom: config?.messageDirect?.allowFrom ?? [],
|
|
30
|
+
denyFrom: config?.messageDirect?.denyFrom ?? [],
|
|
31
|
+
},
|
|
32
|
+
messageGroup: {
|
|
33
|
+
requireMention: config?.messageGroup?.requireMention ?? true,
|
|
34
|
+
requirePoke: config?.messageGroup?.requirePoke ?? true,
|
|
35
|
+
policy: config?.messageGroup?.policy ?? "allow",
|
|
36
|
+
historyLimit: config?.messageGroup?.historyLimit ?? 20,
|
|
37
|
+
allowFrom: config?.messageGroup?.allowFrom ?? [],
|
|
38
|
+
denyFrom: config?.messageGroup?.denyFrom ?? [],
|
|
39
|
+
wakeWord: config?.messageGroup?.wakeWord ?? undefined,
|
|
40
|
+
},
|
|
41
|
+
messageGroupsCustom: config?.messageGroupsCustom ?? {},
|
|
26
42
|
};
|
|
27
43
|
}
|
|
28
44
|
/**
|
|
@@ -31,9 +47,28 @@ export function resolveQQAccount(params) {
|
|
|
31
47
|
const wsUrlRegex = /^wss?:\/\/[\w.-]+(:\d+)?(\/[\w./-]*)?$/;
|
|
32
48
|
const wsUrlSchema = z.string()
|
|
33
49
|
.regex(wsUrlRegex, { message: "Invalid WebSocket URL format. Expected: ws://host:port or wss://host:port" })
|
|
34
|
-
.default("ws://127.0.0.1:3001")
|
|
50
|
+
.default("ws://127.0.0.1:3001")
|
|
51
|
+
.describe("NapCat Websocket 连接地址");
|
|
52
|
+
export const QQDirectConfigSchema = z.object({
|
|
53
|
+
policy: z.enum(["allow", "deny", "allowlist"]).default("allow").describe("私聊策略"),
|
|
54
|
+
allowFrom: z.array(z.string()).default([]).describe("允许的用户").optional(),
|
|
55
|
+
denyFrom: z.array(z.string()).default([]).describe("拒绝的用户").optional(),
|
|
56
|
+
}).describe("私聊全局配置");
|
|
57
|
+
export const QQGroupConfigSchema = z.object({
|
|
58
|
+
requireMention: z.boolean().default(true).describe("群组是否需要@响应"),
|
|
59
|
+
requirePoke: z.boolean().default(true).describe("群组支持戳一戳响应"),
|
|
60
|
+
historyLimit: z.number().default(20).describe("群组历史记录信息条数"),
|
|
61
|
+
policy: z.enum(["allow", "deny", "allowlist"]).default("allow").describe("群组策略"),
|
|
62
|
+
allowFrom: z.array(z.string()).default([]).describe("群组允许的用户").optional(),
|
|
63
|
+
denyFrom: z.array(z.string()).default([]).describe("群组拒绝的用户").optional(),
|
|
64
|
+
wakeWord: z.string().describe("群组唤醒词").optional(),
|
|
65
|
+
}).describe("群组全局配置");
|
|
35
66
|
export const QQConfigSchema = z.object({
|
|
36
67
|
wsUrl: wsUrlSchema,
|
|
37
|
-
accessToken: z.string().default("access-token"),
|
|
38
|
-
enable: z.boolean().default(true)
|
|
68
|
+
accessToken: z.string().default("access-token").describe("NapCat Websocket Token"),
|
|
69
|
+
enable: z.boolean().default(true).describe("是否启用"),
|
|
70
|
+
markdownFormat: z.boolean().default(true).describe("是否启动 Markdown 格式化转换"),
|
|
71
|
+
messageDirect: QQDirectConfigSchema,
|
|
72
|
+
messageGroup: QQGroupConfigSchema,
|
|
73
|
+
messageGroupsCustom: z.record(z.string(), QQGroupConfigSchema).default({}).describe("特定群组配置").optional(),
|
|
39
74
|
});
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Message Dispatch Module
|
|
3
3
|
* Handles routing and dispatching incoming messages to the AI
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { buildPendingHistoryContextFromMap, clearHistoryEntries, recordPendingHistoryEntry, resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk";
|
|
6
|
+
import { getRuntime, getContext, getSession, clearSession, updateSession, getLoginInfo, historyCache } from './runtime.js';
|
|
6
7
|
import { getFile, sendMsg, setInputStatus } from './request.js';
|
|
7
8
|
import { napCatToOpenClawMessage, openClawToNapCatMessage } from '../adapters/message.js';
|
|
8
9
|
import { Logger as log, markdownToText, buildMediaMessage } from '../utils/index.js';
|
|
@@ -13,26 +14,38 @@ import { CHANNEL_ID } from "./config.js";
|
|
|
13
14
|
* For replies, includes quoted message content if available
|
|
14
15
|
*/
|
|
15
16
|
async function contentToPlainText(content) {
|
|
16
|
-
|
|
17
|
-
.filter(c => c.type !== 'image' && c.type !== 'audio' && c.type !== 'file')
|
|
18
|
-
.map((c) => {
|
|
17
|
+
const results = await Promise.all(content.map(async (c) => {
|
|
19
18
|
switch (c.type) {
|
|
20
19
|
case 'text':
|
|
21
|
-
return
|
|
20
|
+
return c.text;
|
|
22
21
|
case 'at':
|
|
23
|
-
|
|
22
|
+
const target = c.isAll ? '@全体成员' : `@${c.userId}`;
|
|
23
|
+
return `[提及]${target}`;
|
|
24
|
+
case 'image':
|
|
25
|
+
return `[图片]${c.url}`;
|
|
26
|
+
case 'audio':
|
|
27
|
+
return `[音频]${c.path}`;
|
|
28
|
+
case 'video':
|
|
29
|
+
return `[视频]${c.url}`;
|
|
30
|
+
case 'file': {
|
|
31
|
+
const fileInfo = await getFile({ file_id: c.fileId });
|
|
32
|
+
if (!fileInfo.data?.file)
|
|
33
|
+
return null;
|
|
34
|
+
return `[文件]${fileInfo.data.file}`;
|
|
35
|
+
}
|
|
24
36
|
case 'json':
|
|
25
|
-
return `[JSON]\n\`\`\`json\n${c.data}\n\`\`\``;
|
|
26
|
-
case 'reply':
|
|
27
|
-
const senderInfo = c.sender && c.senderId ? `${c.sender}(${c.senderId})` : '未知用户';
|
|
28
|
-
const replyMsg = c.message ?? '
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
return `[JSON]\n\n\`\`\`json\n${c.data}\n\`\`\``;
|
|
38
|
+
case 'reply': {
|
|
39
|
+
const senderInfo = c.sender && c.senderId ? `${c.sender}(${c.senderId})` : '(未知用户)';
|
|
40
|
+
const replyMsg = c.message ?? '(无法获取原消息)';
|
|
41
|
+
const quotedContent = `${senderInfo}:\n${replyMsg}`.replace(/^/gm, '> ');
|
|
42
|
+
return `[回复]\n\n${quotedContent}`;
|
|
43
|
+
}
|
|
32
44
|
default:
|
|
33
|
-
return
|
|
45
|
+
return null;
|
|
34
46
|
}
|
|
35
|
-
})
|
|
47
|
+
}));
|
|
48
|
+
return results.filter((v) => v !== null).join('\n');
|
|
36
49
|
}
|
|
37
50
|
async function contextToMedia(content) {
|
|
38
51
|
const hasMedia = content.some(c => c.type === 'image' || c.type === 'audio' || c.type === 'file');
|
|
@@ -70,8 +83,16 @@ async function contextToMedia(content) {
|
|
|
70
83
|
return;
|
|
71
84
|
}
|
|
72
85
|
async function sendText(isGroup, chatId, text) {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
86
|
+
const contextText = text.replace(/NO_REPLY\s*$/, '');
|
|
87
|
+
const context = getContext();
|
|
88
|
+
if (!context) {
|
|
89
|
+
log.warn('dispatch', `No gateway context`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const messageSegments = [{
|
|
93
|
+
type: 'text',
|
|
94
|
+
data: { text: context.account.markdownFormat ? markdownToText(contextText) : contextText }
|
|
95
|
+
}];
|
|
75
96
|
try {
|
|
76
97
|
await sendMsg({
|
|
77
98
|
message_type: isGroup ? 'group' : 'private',
|
|
@@ -104,7 +125,7 @@ async function sendMedia(isGroup, chatId, mediaUrl) {
|
|
|
104
125
|
* Dispatch an incoming message to the AI for processing
|
|
105
126
|
*/
|
|
106
127
|
export async function dispatchMessage(params) {
|
|
107
|
-
|
|
128
|
+
let { chatType, chatId, senderId, senderName, messageId, content, media, timestamp, targetId } = params;
|
|
108
129
|
const runtime = getRuntime();
|
|
109
130
|
if (!runtime) {
|
|
110
131
|
log.warn('dispatch', `Plugin runtime not available`);
|
|
@@ -116,31 +137,64 @@ export async function dispatchMessage(params) {
|
|
|
116
137
|
return;
|
|
117
138
|
}
|
|
118
139
|
const isGroup = chatType === 'group';
|
|
119
|
-
const
|
|
120
|
-
|
|
140
|
+
const config = context.account;
|
|
141
|
+
// At 模式处理
|
|
142
|
+
if (isGroup) {
|
|
143
|
+
const isMention = mention(content, chatId, targetId);
|
|
144
|
+
if (!isMention) {
|
|
145
|
+
log.debug('dispatch', `Skipping group message (not mentioned)`);
|
|
146
|
+
const groupConfig = getGroupConfig(chatId, config);
|
|
147
|
+
recordPendingHistoryEntry({
|
|
148
|
+
historyMap: historyCache,
|
|
149
|
+
historyKey: chatId,
|
|
150
|
+
limit: groupConfig.historyLimit ?? 20,
|
|
151
|
+
entry: {
|
|
152
|
+
sender: `${senderName}(${senderId})`,
|
|
153
|
+
body: content,
|
|
154
|
+
timestamp: timestamp,
|
|
155
|
+
messageId: messageId,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
121
162
|
cfg: context.cfg,
|
|
122
163
|
channel: CHANNEL_ID,
|
|
164
|
+
accountId: context.accountId,
|
|
123
165
|
peer: {
|
|
124
|
-
kind:
|
|
125
|
-
id:
|
|
166
|
+
kind: isGroup ? "group" : "direct",
|
|
167
|
+
id: chatId,
|
|
126
168
|
},
|
|
169
|
+
runtime: runtime.channel,
|
|
170
|
+
sessionStore: context.cfg.session?.store
|
|
127
171
|
});
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
172
|
+
// 终止信号
|
|
173
|
+
const session = getSession(route.sessionKey);
|
|
174
|
+
if (session.abortController) {
|
|
175
|
+
session.abortController.abort();
|
|
176
|
+
session.aborted = true;
|
|
177
|
+
log.info('dispatch', `Aborted previous session`);
|
|
178
|
+
}
|
|
179
|
+
if (isGroup) {
|
|
180
|
+
const groupConfig = getGroupConfig(chatId, config);
|
|
181
|
+
content = buildPendingHistoryContextFromMap({
|
|
182
|
+
historyMap: historyCache,
|
|
183
|
+
historyKey: chatId,
|
|
184
|
+
limit: groupConfig.historyLimit ?? 20,
|
|
185
|
+
currentMessage: content,
|
|
186
|
+
formatEntry: (e) => `${e.sender}: ${e.body}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
|
190
|
+
const { storePath, body } = buildEnvelope({
|
|
131
191
|
channel: CHANNEL_ID,
|
|
132
|
-
from:
|
|
192
|
+
from: fromLabel,
|
|
133
193
|
body: content,
|
|
134
194
|
timestamp,
|
|
135
|
-
chatType: isGroup ? 'group' : 'direct',
|
|
136
|
-
sender: {
|
|
137
|
-
id: senderId,
|
|
138
|
-
name: senderName,
|
|
139
|
-
},
|
|
140
|
-
envelope: envelopeOptions,
|
|
141
195
|
});
|
|
142
196
|
log.debug('dispatch', `Inbound envelope: ${body}`);
|
|
143
|
-
const fromAddress =
|
|
197
|
+
const fromAddress = `qq:${fromLabel}`;
|
|
144
198
|
const toAddress = `qq:${chatId}`;
|
|
145
199
|
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
|
146
200
|
Body: body,
|
|
@@ -151,6 +205,7 @@ export async function dispatchMessage(params) {
|
|
|
151
205
|
SessionKey: route.sessionKey,
|
|
152
206
|
AccountId: route.accountId,
|
|
153
207
|
ChatType: isGroup ? 'group' : 'direct',
|
|
208
|
+
ConversationLabel: fromLabel,
|
|
154
209
|
SenderId: senderId,
|
|
155
210
|
SenderName: senderName,
|
|
156
211
|
Provider: CHANNEL_ID,
|
|
@@ -164,8 +219,18 @@ export async function dispatchMessage(params) {
|
|
|
164
219
|
OriginatingTo: toAddress,
|
|
165
220
|
});
|
|
166
221
|
log.info('dispatch', `Dispatching to agent ${route.agentId}, session: ${route.sessionKey}`);
|
|
222
|
+
await runtime.channel.session.recordInboundSession({
|
|
223
|
+
storePath,
|
|
224
|
+
sessionKey: route.sessionKey,
|
|
225
|
+
ctx: ctxPayload,
|
|
226
|
+
onRecordError(err) {
|
|
227
|
+
log.error('dispatch', `Failed to record inbound session: ${err}`);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
167
230
|
const messagesConfig = runtime.channel.reply.resolveEffectiveMessagesConfig(context.cfg, route.agentId);
|
|
168
231
|
try {
|
|
232
|
+
session.abortController = new AbortController();
|
|
233
|
+
updateSession(route.sessionKey, session);
|
|
169
234
|
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
170
235
|
ctx: ctxPayload,
|
|
171
236
|
cfg: context.cfg,
|
|
@@ -184,6 +249,14 @@ export async function dispatchMessage(params) {
|
|
|
184
249
|
}
|
|
185
250
|
},
|
|
186
251
|
deliver: async (payload, info) => {
|
|
252
|
+
if (session.aborted) {
|
|
253
|
+
session.aborted = false;
|
|
254
|
+
log.info('dispatch', `aborted skipping`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (isGroup) {
|
|
258
|
+
clearHistoryEntries({ historyMap: historyCache, historyKey: chatId });
|
|
259
|
+
}
|
|
187
260
|
log.info('dispatch', `deliver(${info.kind}): ${JSON.stringify(payload)}`);
|
|
188
261
|
if (payload.text && !payload.text.startsWith('MEDIA:')) {
|
|
189
262
|
await sendText(isGroup, chatId, payload.text);
|
|
@@ -205,7 +278,9 @@ export async function dispatchMessage(params) {
|
|
|
205
278
|
await sendText(isGroup, chatId, `[错误]\n${String(err)}`);
|
|
206
279
|
},
|
|
207
280
|
},
|
|
208
|
-
replyOptions: {
|
|
281
|
+
replyOptions: {
|
|
282
|
+
abortSignal: session.abortController?.signal,
|
|
283
|
+
},
|
|
209
284
|
});
|
|
210
285
|
log.info('dispatch', `Dispatch completed`);
|
|
211
286
|
}
|
|
@@ -220,12 +295,17 @@ export async function dispatchMessage(params) {
|
|
|
220
295
|
event_type: 2
|
|
221
296
|
});
|
|
222
297
|
}
|
|
298
|
+
clearSession(route.sessionKey);
|
|
223
299
|
}
|
|
224
300
|
}
|
|
225
301
|
/**
|
|
226
302
|
* Handle group message event
|
|
227
303
|
*/
|
|
228
304
|
export async function handleGroupMessage(event) {
|
|
305
|
+
if (!allow(event.user_id.toString(), event.group_id.toString())) {
|
|
306
|
+
log.debug('dispatch', `Ignoring group message from ${event.user_id}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
229
309
|
const content = await napCatToOpenClawMessage(event.message);
|
|
230
310
|
const plainText = await contentToPlainText(content);
|
|
231
311
|
const media = await contextToMedia(content);
|
|
@@ -245,6 +325,10 @@ export async function handleGroupMessage(event) {
|
|
|
245
325
|
* Handle private message event
|
|
246
326
|
*/
|
|
247
327
|
export async function handlePrivateMessage(event) {
|
|
328
|
+
if (!allow(event.user_id.toString())) {
|
|
329
|
+
log.debug('dispatch', `Ignoring message from ${event.user_id}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
248
332
|
const content = await napCatToOpenClawMessage(event.message);
|
|
249
333
|
const plainText = await contentToPlainText(content);
|
|
250
334
|
const media = await contextToMedia(content);
|
|
@@ -270,6 +354,10 @@ function extractPokeActionText(rawInfo) {
|
|
|
270
354
|
return actionItem?.txt || '戳了戳';
|
|
271
355
|
}
|
|
272
356
|
export async function handlePokeEvent(event) {
|
|
357
|
+
if (!allow(event.user_id.toString(), event.group_id?.toString())) {
|
|
358
|
+
log.debug('dispatch', `Poke from ${event.user_id} is not allowed`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
273
361
|
const actionText = extractPokeActionText(event.raw_info);
|
|
274
362
|
log.info('dispatch', `Poke from ${event.user_id}: ${actionText}`);
|
|
275
363
|
const pokeMessage = actionText || '戳了戳';
|
|
@@ -281,7 +369,77 @@ export async function handlePokeEvent(event) {
|
|
|
281
369
|
senderId: String(event.user_id),
|
|
282
370
|
senderName: String(event.user_id),
|
|
283
371
|
messageId: `poke_${event.user_id}_${Date.now()}`,
|
|
284
|
-
content: `[动作]
|
|
372
|
+
content: `[动作]${pokeMessage}`,
|
|
285
373
|
timestamp: Date.now(),
|
|
374
|
+
targetId: String(event.target_id),
|
|
286
375
|
});
|
|
287
376
|
}
|
|
377
|
+
function getGroupConfig(groupId, config) {
|
|
378
|
+
log.debug('dispatch', `All Custom config: ${JSON.stringify(config.messageGroupsCustom)}`);
|
|
379
|
+
let groupConfig = config.messageGroupsCustom[groupId];
|
|
380
|
+
if (!groupConfig) {
|
|
381
|
+
groupConfig = config.messageGroup;
|
|
382
|
+
log.debug('dispatch', `Use global config: ${JSON.stringify(groupConfig)}`);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
groupConfig = {
|
|
386
|
+
...config.messageGroup,
|
|
387
|
+
...groupConfig,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
log.debug('dispatch', `Final config: ${JSON.stringify(groupConfig)}`);
|
|
391
|
+
return groupConfig;
|
|
392
|
+
}
|
|
393
|
+
function allow(userId, groupId) {
|
|
394
|
+
const context = getContext();
|
|
395
|
+
if (!context) {
|
|
396
|
+
log.warn('dispatch', `No gateway context`);
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
let config = groupId ? getGroupConfig(groupId, context.account) : context.account.messageDirect;
|
|
400
|
+
return allowJudgment(config, userId);
|
|
401
|
+
}
|
|
402
|
+
function allowJudgment(config, userId) {
|
|
403
|
+
if (config.denyFrom?.includes(userId)) {
|
|
404
|
+
log.debug('dispatch', `User ${userId} is denied`);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
if (config.policy === 'allow') {
|
|
408
|
+
log.debug('dispatch', `User ${userId} is allowed`);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (config.policy === 'deny') {
|
|
412
|
+
log.debug('dispatch', `User ${userId} is denied`);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (config.allowFrom?.includes(userId)) {
|
|
416
|
+
log.debug('dispatch', `User ${userId} is allowed`);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
function mention(content, groupId, targetId) {
|
|
422
|
+
const context = getContext();
|
|
423
|
+
if (!context) {
|
|
424
|
+
log.warn('dispatch', `No gateway context`);
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
let config = getGroupConfig(groupId, context.account);
|
|
428
|
+
const loginInfo = getLoginInfo();
|
|
429
|
+
const isMentionEnabled = !!config?.requireMention;
|
|
430
|
+
const isPokeEnabled = !!config?.requirePoke;
|
|
431
|
+
const isWakeEnabled = !!config?.wakeWord?.trim();
|
|
432
|
+
if (!isMentionEnabled && !isPokeEnabled && !isWakeEnabled) {
|
|
433
|
+
log.debug('dispatch', 'All requires are disabled, returning true by default.');
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
const requireMention = isMentionEnabled &&
|
|
437
|
+
(content.includes('[提及]@全体成员') ||
|
|
438
|
+
(!!loginInfo?.userId && content.includes(`[提及]@${loginInfo.userId}`)));
|
|
439
|
+
const requirePoke = isPokeEnabled &&
|
|
440
|
+
(content.includes('[动作]') && targetId === loginInfo?.userId);
|
|
441
|
+
const requireWake = isWakeEnabled &&
|
|
442
|
+
content.includes(config.wakeWord ?? "");
|
|
443
|
+
log.debug('dispatch', `Require mention: ${requireMention}, require poke: ${requirePoke}, require wake: ${requireWake}`);
|
|
444
|
+
return requireMention || requirePoke || requireWake;
|
|
445
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GetFileReq, GetFileResp, GetMsgReq, GetMsgResp, GetStatusResp, NapCatResp, SendMsgReq, SendMsgResp, SetInputStatusReq } from "../types";
|
|
1
|
+
import type { GetFileReq, GetFileResp, GetMsgReq, GetMsgResp, GetStatusResp, GetLoginInfoResp, NapCatResp, SendMsgReq, SendMsgResp, SetInputStatusReq, GetFriendListResp, GetGroupListResp } from "../types";
|
|
2
2
|
/**
|
|
3
3
|
* 事件监听
|
|
4
4
|
* @param event
|
|
@@ -28,3 +28,15 @@ export declare function setInputStatus(params: SetInputStatusReq): Promise<NapCa
|
|
|
28
28
|
* 获取状态
|
|
29
29
|
*/
|
|
30
30
|
export declare function getStatus(): Promise<NapCatResp<GetStatusResp>>;
|
|
31
|
+
/**
|
|
32
|
+
* 获取登录信息
|
|
33
|
+
*/
|
|
34
|
+
export declare function getLoginInfo(): Promise<NapCatResp<GetLoginInfoResp>>;
|
|
35
|
+
/**
|
|
36
|
+
* 获取好友列表
|
|
37
|
+
*/
|
|
38
|
+
export declare function getFriendList(): Promise<NapCatResp<GetFriendListResp[]>>;
|
|
39
|
+
/**
|
|
40
|
+
* 获取群列表
|
|
41
|
+
*/
|
|
42
|
+
export declare function getGroupList(): Promise<NapCatResp<GetGroupListResp[]>>;
|
package/dist/src/core/request.js
CHANGED
|
@@ -136,3 +136,36 @@ export async function getStatus() {
|
|
|
136
136
|
}
|
|
137
137
|
return connection.sendRequest("get_status");
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* 获取登录信息
|
|
141
|
+
*/
|
|
142
|
+
export async function getLoginInfo() {
|
|
143
|
+
const connection = getConnection();
|
|
144
|
+
if (!connection) {
|
|
145
|
+
log.warn("request", `No connection available`);
|
|
146
|
+
return failResp();
|
|
147
|
+
}
|
|
148
|
+
return connection.sendRequest("get_login_info");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 获取好友列表
|
|
152
|
+
*/
|
|
153
|
+
export async function getFriendList() {
|
|
154
|
+
const connection = getConnection();
|
|
155
|
+
if (!connection) {
|
|
156
|
+
log.warn("request", `No connection available`);
|
|
157
|
+
return failResp();
|
|
158
|
+
}
|
|
159
|
+
return connection.sendRequest("get_friend_list");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 获取群列表
|
|
163
|
+
*/
|
|
164
|
+
export async function getGroupList() {
|
|
165
|
+
const connection = getConnection();
|
|
166
|
+
if (!connection) {
|
|
167
|
+
log.warn("request", `No connection available`);
|
|
168
|
+
return failResp();
|
|
169
|
+
}
|
|
170
|
+
return connection.sendRequest("get_group_list");
|
|
171
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Plugin Runtime Storage
|
|
3
3
|
* Stores the PluginRuntime for access in gateway handlers
|
|
4
4
|
*/
|
|
5
|
-
import type { ChannelAccountSnapshot, ChannelGatewayContext, PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
|
-
import
|
|
5
|
+
import type { ChannelAccountSnapshot, ChannelGatewayContext, HistoryEntry, PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
|
+
import { QQConfig, QQLoginInfo, QQSession } from "../types";
|
|
7
7
|
import { ConnectionManager } from "./connection.js";
|
|
8
8
|
export declare function setRuntime(next: PluginRuntime): void;
|
|
9
9
|
export declare function getRuntime(): PluginRuntime | null;
|
|
@@ -14,3 +14,9 @@ export declare function setContextStatus(next: Omit<ChannelAccountSnapshot, 'acc
|
|
|
14
14
|
export declare function setConnection(next: ConnectionManager): void;
|
|
15
15
|
export declare function getConnection(): ConnectionManager | null;
|
|
16
16
|
export declare function clearConnection(): void;
|
|
17
|
+
export declare function getSession(sessionKey: string): QQSession;
|
|
18
|
+
export declare function updateSession(sessionKey: string, session: QQSession): void;
|
|
19
|
+
export declare function clearSession(sessionKey: string): void;
|
|
20
|
+
export declare function setLoginInfo(next: QQLoginInfo): void;
|
|
21
|
+
export declare function getLoginInfo(): QQLoginInfo;
|
|
22
|
+
export declare const historyCache: Map<string, HistoryEntry[]>;
|
package/dist/src/core/runtime.js
CHANGED
|
@@ -46,3 +46,39 @@ export function getConnection() {
|
|
|
46
46
|
export function clearConnection() {
|
|
47
47
|
connection = null;
|
|
48
48
|
}
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Session
|
|
51
|
+
// =============================================================================
|
|
52
|
+
const sessionMap = new Map();
|
|
53
|
+
export function getSession(sessionKey) {
|
|
54
|
+
let session = sessionMap.get(sessionKey);
|
|
55
|
+
if (session) {
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
session = {};
|
|
59
|
+
sessionMap.set(sessionKey, session);
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
62
|
+
export function updateSession(sessionKey, session) {
|
|
63
|
+
sessionMap.set(sessionKey, session);
|
|
64
|
+
}
|
|
65
|
+
export function clearSession(sessionKey) {
|
|
66
|
+
sessionMap.delete(sessionKey);
|
|
67
|
+
}
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// LoginInfo
|
|
70
|
+
// =============================================================================
|
|
71
|
+
const loginInfo = {
|
|
72
|
+
userId: '',
|
|
73
|
+
nickname: '',
|
|
74
|
+
};
|
|
75
|
+
export function setLoginInfo(next) {
|
|
76
|
+
Object.assign(loginInfo, next);
|
|
77
|
+
}
|
|
78
|
+
export function getLoginInfo() {
|
|
79
|
+
return loginInfo;
|
|
80
|
+
}
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// History
|
|
83
|
+
// =============================================================================
|
|
84
|
+
export const historyCache = new Map();
|
|
@@ -14,7 +14,7 @@ export interface NapCatResp<T = unknown> {
|
|
|
14
14
|
data?: T;
|
|
15
15
|
echo?: string;
|
|
16
16
|
}
|
|
17
|
-
export type NapCatAction = 'send_msg' | 'get_msg' | 'get_status' | 'get_file' | 'set_input_status';
|
|
17
|
+
export type NapCatAction = 'send_msg' | 'get_msg' | 'get_status' | 'get_file' | 'get_login_info' | 'get_friend_list' | 'get_group_list' | 'set_input_status';
|
|
18
18
|
export interface NapCatEvent {
|
|
19
19
|
time: number;
|
|
20
20
|
self_id: number;
|
|
@@ -26,7 +26,7 @@ export interface NapCatMetaEvent extends NapCatEvent {
|
|
|
26
26
|
meta_event_type: 'lifecycle' | 'heartbeat';
|
|
27
27
|
sub_type?: 'connect' | 'disconnect' | 'enable' | 'disable';
|
|
28
28
|
}
|
|
29
|
-
export type NapCatMessage = NapCatTextSegment | NapCatAtSegment | NapCatImageSegment | NapCatReplySegment | NapCatFaceSegment | NapCatRecordSegment | NapCatFileSegment | NapCatJsonSegment | NapCatUnknownSegment;
|
|
29
|
+
export type NapCatMessage = NapCatTextSegment | NapCatAtSegment | NapCatImageSegment | NapCatReplySegment | NapCatFaceSegment | NapCatRecordSegment | NapCatFileSegment | NapCatJsonSegment | NapCatUnknownSegment | NapCatVideoSegment;
|
|
30
30
|
export interface NapCatTextSegment {
|
|
31
31
|
type: 'text';
|
|
32
32
|
data: {
|
|
@@ -85,15 +85,17 @@ export interface NapCatJsonSegment {
|
|
|
85
85
|
data: string;
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
export interface NapCatVideoSegment {
|
|
89
|
+
type: 'video';
|
|
90
|
+
data: {
|
|
91
|
+
url: string;
|
|
92
|
+
file_size?: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
88
95
|
export interface NapCatUnknownSegment {
|
|
89
96
|
type: string;
|
|
90
97
|
data: Record<string, unknown>;
|
|
91
98
|
}
|
|
92
|
-
export interface QQConfig {
|
|
93
|
-
wsUrl: string;
|
|
94
|
-
accessToken?: string;
|
|
95
|
-
enabled: boolean;
|
|
96
|
-
}
|
|
97
99
|
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'failed';
|
|
98
100
|
export interface ConnectionStatus {
|
|
99
101
|
state: ConnectionState;
|
|
@@ -102,6 +104,7 @@ export interface ConnectionStatus {
|
|
|
102
104
|
error?: string;
|
|
103
105
|
reconnectAttempts?: number;
|
|
104
106
|
}
|
|
107
|
+
export type OpenClawMessage = OpenClawTextContent | OpenClawAtContent | OpenClawImageContent | OpenClawReplyContent | OpenClawAudioContent | OpenClawJsonContent | OpenClawFileContent | OpenClawVideoContent;
|
|
105
108
|
export interface OpenClawTextContent {
|
|
106
109
|
type: 'text';
|
|
107
110
|
text: string;
|
|
@@ -114,7 +117,6 @@ export interface OpenClawAtContent {
|
|
|
114
117
|
export interface OpenClawImageContent {
|
|
115
118
|
type: 'image';
|
|
116
119
|
url: string;
|
|
117
|
-
/** Optional summary/description (e.g., "[动画表情]" for animated stickers) */
|
|
118
120
|
summary?: string;
|
|
119
121
|
}
|
|
120
122
|
export interface OpenClawReplyContent {
|
|
@@ -126,20 +128,14 @@ export interface OpenClawReplyContent {
|
|
|
126
128
|
}
|
|
127
129
|
export interface OpenClawAudioContent {
|
|
128
130
|
type: 'audio';
|
|
129
|
-
/** Local file path to the audio file */
|
|
130
131
|
path: string;
|
|
131
|
-
/** Optional URL for downloading the audio */
|
|
132
132
|
url?: string;
|
|
133
|
-
/** File name */
|
|
134
133
|
file: string;
|
|
135
|
-
/** File size in bytes */
|
|
136
134
|
fileSize?: number;
|
|
137
135
|
}
|
|
138
136
|
export interface OpenClawJsonContent {
|
|
139
137
|
type: 'json';
|
|
140
|
-
/** Raw JSON data string */
|
|
141
138
|
data: string;
|
|
142
|
-
/** Optional display text/prompt from the JSON */
|
|
143
139
|
prompt?: string;
|
|
144
140
|
}
|
|
145
141
|
export interface OpenClawFileContent {
|
|
@@ -149,7 +145,11 @@ export interface OpenClawFileContent {
|
|
|
149
145
|
url?: string;
|
|
150
146
|
fileSize?: number;
|
|
151
147
|
}
|
|
152
|
-
export
|
|
148
|
+
export interface OpenClawVideoContent {
|
|
149
|
+
type: 'video';
|
|
150
|
+
url?: string;
|
|
151
|
+
fileSize?: number;
|
|
152
|
+
}
|
|
153
153
|
export interface PendingRequest {
|
|
154
154
|
resolve: (response: NapCatResp) => void;
|
|
155
155
|
reject: (error: Error) => void;
|
|
@@ -248,6 +248,18 @@ export interface GetStatusResp {
|
|
|
248
248
|
good: boolean;
|
|
249
249
|
stat: Record<any, any>;
|
|
250
250
|
}
|
|
251
|
+
export interface GetLoginInfoResp {
|
|
252
|
+
user_id: number;
|
|
253
|
+
nickname: string;
|
|
254
|
+
}
|
|
255
|
+
export interface GetFriendListResp {
|
|
256
|
+
user_id: number;
|
|
257
|
+
nickname: string;
|
|
258
|
+
}
|
|
259
|
+
export interface GetGroupListResp {
|
|
260
|
+
group_id: number;
|
|
261
|
+
group_name: string;
|
|
262
|
+
}
|
|
251
263
|
export interface DispatchMessageMedia {
|
|
252
264
|
type?: string;
|
|
253
265
|
path?: string;
|
|
@@ -262,9 +274,38 @@ export interface DispatchMessageParams {
|
|
|
262
274
|
content: string;
|
|
263
275
|
media?: DispatchMessageMedia;
|
|
264
276
|
timestamp: number;
|
|
277
|
+
targetId?: string;
|
|
278
|
+
}
|
|
279
|
+
export interface QQConfig {
|
|
280
|
+
wsUrl: string;
|
|
281
|
+
accessToken?: string;
|
|
282
|
+
enabled: boolean;
|
|
283
|
+
markdownFormat: boolean;
|
|
284
|
+
messageDirect: QQAllowConfig;
|
|
285
|
+
messageGroup: QQGroupConfig;
|
|
286
|
+
messageGroupsCustom: Record<string, QQGroupConfig>;
|
|
287
|
+
}
|
|
288
|
+
export interface QQGroupConfig extends QQAllowConfig {
|
|
289
|
+
requireMention: boolean;
|
|
290
|
+
requirePoke: boolean;
|
|
291
|
+
historyLimit: number;
|
|
292
|
+
wakeWord?: string;
|
|
293
|
+
}
|
|
294
|
+
export interface QQAllowConfig {
|
|
295
|
+
policy: "allow" | "deny" | "allowlist";
|
|
296
|
+
allowFrom: string[];
|
|
297
|
+
denyFrom: string[];
|
|
265
298
|
}
|
|
266
299
|
export type QQProbe = {
|
|
267
300
|
ok: boolean;
|
|
268
301
|
status?: number | null;
|
|
269
302
|
error?: string | null;
|
|
270
303
|
};
|
|
304
|
+
export type QQSession = {
|
|
305
|
+
abortController?: AbortController;
|
|
306
|
+
aborted?: boolean;
|
|
307
|
+
};
|
|
308
|
+
export type QQLoginInfo = {
|
|
309
|
+
userId: string;
|
|
310
|
+
nickname: string;
|
|
311
|
+
};
|
|
@@ -48,9 +48,8 @@ export class MarkdownToText {
|
|
|
48
48
|
// 逻辑:匹配 < 后紧跟字母的模式,保留 "a < b" 或 "1 < 5" 这种数学公式
|
|
49
49
|
text = text.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, '');
|
|
50
50
|
// 3.3 标题 (Headers) -> 视觉醒目文本
|
|
51
|
-
text = text.replace(/^#\s+(.*)$/gm, '\n$1\n
|
|
52
|
-
text = text.replace(
|
|
53
|
-
text = text.replace(/^(#{3,6})\s+(.*)$/gm, '\n【 $2 】\n');
|
|
51
|
+
text = text.replace(/^#\s+(.*)$/gm, '\n$1\n\n\n');
|
|
52
|
+
text = text.replace(/^(#{2,6})\s+(.*)$/gm, '\n$2\n\n');
|
|
54
53
|
// 3.4 Markdown 分割线 (---, ***)
|
|
55
54
|
text = text.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '──────────');
|
|
56
55
|
// 3.5 引用 (Blockquotes)
|
|
@@ -94,7 +93,7 @@ export class MarkdownToText {
|
|
|
94
93
|
return text.replace(codeBlockRegex, (_match, _fence, lang, code) => {
|
|
95
94
|
const key = `${this.maskPrefix}BLOCK-${this.maskCounter++}`;
|
|
96
95
|
const langTag = lang ? ` [${lang}]` : '';
|
|
97
|
-
const formatted = `\n
|
|
96
|
+
const formatted = `\n──────────${langTag}\n${code.replace(/^\n+|\n+$/g, '')}\n──────────\n`;
|
|
98
97
|
this.codeBlockStore.set(key, formatted);
|
|
99
98
|
return key;
|
|
100
99
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izhimu/qq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A QQ channel plugin for OpenClaw using NapCat WebSocket",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"openclaw": "^2026.
|
|
44
|
+
"openclaw": "^2026.3.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"openclaw": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"@types/ws": "^8.18.0",
|
|
59
|
-
"openclaw": "^2026.
|
|
59
|
+
"openclaw": "^2026.3.1",
|
|
60
60
|
"typescript": "^5.0.0"
|
|
61
61
|
}
|
|
62
62
|
}
|