@izhimu/qq 0.5.1 → 0.6.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.
Files changed (38) hide show
  1. package/README.md +12 -16
  2. package/dist/index.d.ts +5 -11
  3. package/dist/index.js +9 -18
  4. package/dist/src/adapters/message.d.ts +15 -4
  5. package/dist/src/adapters/message.js +179 -124
  6. package/dist/src/channel.d.ts +2 -7
  7. package/dist/src/channel.js +231 -312
  8. package/dist/src/core/auth.d.ts +67 -0
  9. package/dist/src/core/auth.js +154 -0
  10. package/dist/src/core/config.d.ts +5 -7
  11. package/dist/src/core/config.js +6 -8
  12. package/dist/src/core/connection.d.ts +6 -5
  13. package/dist/src/core/connection.js +17 -70
  14. package/dist/src/core/dispatch.d.ts +7 -54
  15. package/dist/src/core/dispatch.js +210 -398
  16. package/dist/src/core/event-handler.d.ts +42 -0
  17. package/dist/src/core/event-handler.js +171 -0
  18. package/dist/src/core/request.d.ts +3 -8
  19. package/dist/src/core/request.js +13 -126
  20. package/dist/src/core/runtime.d.ts +2 -11
  21. package/dist/src/core/runtime.js +0 -47
  22. package/dist/src/runtime.d.ts +3 -0
  23. package/dist/src/runtime.js +3 -0
  24. package/dist/src/setup-surface.d.ts +2 -0
  25. package/dist/src/setup-surface.js +59 -0
  26. package/dist/src/types/index.d.ts +69 -25
  27. package/dist/src/types/index.js +3 -4
  28. package/dist/src/utils/cqcode.d.ts +0 -9
  29. package/dist/src/utils/cqcode.js +0 -17
  30. package/dist/src/utils/index.d.ts +0 -17
  31. package/dist/src/utils/index.js +17 -154
  32. package/dist/src/utils/log.js +2 -2
  33. package/dist/src/utils/markdown.d.ts +5 -0
  34. package/dist/src/utils/markdown.js +57 -5
  35. package/openclaw.plugin.json +3 -2
  36. package/package.json +9 -11
  37. package/dist/src/onboarding.d.ts +0 -10
  38. package/dist/src/onboarding.js +0 -98
package/README.md CHANGED
@@ -181,27 +181,11 @@ openclaw gateway restart
181
181
 
182
182
  ## 使用方法
183
183
 
184
- ### 发送消息
185
-
186
- ```bash
187
- # 发送私聊消息
188
- openclaw message send "你好!" --to qq:private:123456789
189
-
190
- # 发送群消息
191
- openclaw message send "大家好!" --to qq:group:123456
192
-
193
- # 带回复的消息
194
- openclaw message send "回复你的消息" --to qq:private:123456789 --reply-to <message-id>
195
- ```
196
-
197
184
  ### 检查状态
198
185
 
199
186
  ```bash
200
187
  # 查看频道状态
201
188
  openclaw channels
202
-
203
- # 查看日志
204
- openclaw logs --channel qq
205
189
  ```
206
190
 
207
191
  ### 消息目标格式
@@ -393,6 +377,18 @@ npm run build
393
377
 
394
378
  ## 更新日志
395
379
 
380
+ ### [0.6.0] - 2026-03-25
381
+
382
+ #### ⚠️ 重大变更
383
+ - **OpenClaw SDK 升级**:适配 OpenClaw SDK `2026.3.22` 版本,低于此版本的 OpenClaw 将无法使用本插件。
384
+
385
+ #### 重构
386
+ - **连接管理**:重构连接管理逻辑,优化 WebSocket 连接生命周期。
387
+ - **消息处理**:重构消息处理逻辑,优化适配器结构。
388
+ - **配置管理**:统一配置管理和健康状态处理。
389
+ - **频道模块**:重构频道模块代码结构。
390
+ - **授权系统**:重构 QQ 频道授权和配置系统。
391
+
396
392
  ### [0.5.1] - 2026-03-12
397
393
 
398
394
  #### 修复
package/dist/index.d.ts CHANGED
@@ -1,14 +1,8 @@
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: {
1
+ declare const _default: {
8
2
  id: string;
9
3
  name: string;
10
4
  description: string;
11
- configSchema: typeof emptyPluginConfigSchema;
12
- register(api: OpenClawPluginApi): void;
13
- };
14
- export default plugin;
5
+ configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
6
+ register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
7
+ } & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "kind">;
8
+ export default _default;
package/dist/index.js CHANGED
@@ -1,19 +1,10 @@
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,
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { qqPlugin } from "./src/channel";
3
+ import { setQQRuntime } from "./src/runtime";
4
+ export default defineChannelPluginEntry({
5
+ id: "qq",
11
6
  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;
7
+ description: "QQ Chat channel plugin",
8
+ plugin: qqPlugin,
9
+ setRuntime: setQQRuntime,
10
+ });
@@ -1,8 +1,19 @@
1
1
  /**
2
2
  * Message Type Adapters for NapCat <-> OpenClaw conversion
3
3
  *
4
- * Optimized for maintainability with clear structure and minimal duplication.
4
+ * 使用映射表驱动的统一消息转换系统
5
5
  */
6
- import type { NapCatMessage, OpenClawMessage } from '../types';
7
- export declare function openClawToNapCatMessage(content: OpenClawMessage[]): NapCatMessage[];
8
- export declare function napCatToOpenClawMessage(segments: NapCatMessage[] | string): Promise<OpenClawMessage[]>;
6
+ import type { NapCatMessage, OpenClawMessage, QQAccount } from '../types';
7
+ export declare function outboundMessageAdapter(content: OpenClawMessage[], account: QQAccount): Promise<NapCatMessage[]>;
8
+ export declare function inboundMessageAdapter(segments: NapCatMessage[] | string): Promise<OpenClawMessage[]>;
9
+ export type TextFormatter = (content: OpenClawMessage) => string | null;
10
+ /** 将 OpenClaw 消息内容转换为纯文本 */
11
+ export declare function formatContentToText(content: OpenClawMessage[]): string;
12
+ /** 检查是否包含媒体 */
13
+ export declare function hasMediaContent(content: OpenClawMessage[]): boolean;
14
+ /** 从消息内容提取媒体信息 */
15
+ export declare function extractMedia(content: OpenClawMessage[]): {
16
+ type: string;
17
+ path?: string;
18
+ url?: string;
19
+ } | undefined;
@@ -1,171 +1,226 @@
1
1
  /**
2
2
  * Message Type Adapters for NapCat <-> OpenClaw conversion
3
3
  *
4
- * Optimized for maintainability with clear structure and minimal duplication.
4
+ * 使用映射表驱动的统一消息转换系统
5
5
  */
6
- import { Logger as log, extractImageUrl, getEmojiForFaceId } from '../utils/index.js';
6
+ import { Logger as log, extractImageUrl, getEmojiForFaceId, markdownToText } from '../utils/index.js';
7
7
  import { CQCodeUtils } from '../utils';
8
8
  import { getMsg } from "../core/request.js";
9
9
  // =============================================================================
10
+ // 工具函数
11
+ // =============================================================================
12
+ /** 安全提取字符串字段 */
13
+ const str = (data, key) => String(data[key] ?? '');
14
+ /** 安全提取数字字段 */
15
+ const num = (data, key) => {
16
+ const v = data[key];
17
+ return v != null ? parseInt(String(v), 10) : undefined;
18
+ };
19
+ // =============================================================================
10
20
  // CQ Code Parsing
11
21
  // =============================================================================
12
- /**
13
- * Convert CQNode to NapCatMessageSegment
14
- */
22
+ /** 将 CQNode 转换为 NapCatMessageSegment */
15
23
  function cqNodeToNapCat(node) {
16
- return {
17
- type: node.type,
18
- data: node.data,
19
- };
24
+ return { type: node.type, data: node.data };
20
25
  }
21
- /**
22
- * Parse CQ codes using CQCodeUtils and convert to NapCatMessageSegment[]
23
- */
26
+ /** 解析 CQ 码 */
24
27
  function parseCQCode(text) {
25
- const nodes = CQCodeUtils.parse(text);
26
- return nodes.map(cqNodeToNapCat);
28
+ return CQCodeUtils.parse(text).map(cqNodeToNapCat);
27
29
  }
28
- /**
29
- * Normalize message to segments array (handles string or array format)
30
- */
30
+ /** 标准化消息格式 */
31
31
  function normalizeMessage(message) {
32
- if (typeof message === 'string') {
32
+ if (typeof message === 'string')
33
33
  return parseCQCode(message);
34
- }
35
34
  if (!Array.isArray(message)) {
36
35
  log.warn('adapters', `Invalid message format: ${typeof message}`);
37
36
  return [{ type: 'text', data: { text: String(message) } }];
38
37
  }
39
38
  return message;
40
39
  }
40
+ // =============================================================================
41
+ // JSON 消息解析
42
+ // =============================================================================
41
43
  function parseJsonSegment(segment) {
44
+ const rawData = segment.data.data.trim();
45
+ let prompt;
42
46
  try {
43
- const rawData = segment.data.data.trim();
44
- let jsonData;
45
- try {
46
- jsonData = JSON.parse(rawData);
47
- }
48
- catch (error) {
49
- log.warn('adapters', `Failed to parse JSON message: ${error}`);
50
- }
51
- const result = {
52
- type: 'json',
53
- data: rawData,
54
- };
55
- if (jsonData?.prompt && jsonData.prompt.trim() !== '') {
56
- result.prompt = jsonData.prompt;
47
+ const jsonData = JSON.parse(rawData);
48
+ if (jsonData?.prompt?.trim()) {
49
+ prompt = jsonData.prompt;
57
50
  }
58
- return result;
59
51
  }
60
- catch (error) {
61
- log.warn('adapters', `Failed to parse JSON message: ${error}`);
62
- return null;
52
+ catch {
53
+ log.warn('adapters', 'Failed to parse JSON message');
63
54
  }
55
+ return { type: 'json', data: rawData, ...(prompt && { prompt }) };
64
56
  }
65
- // =============================================================================
66
- // NapCat -> OpenClaw Adapters (Inbound)
67
- // =============================================================================
57
+ const inboundConverters = {
58
+ text: (data) => ({ type: 'text', text: str(data, 'text') }),
59
+ at: (data) => ({
60
+ type: 'at',
61
+ userId: str(data, 'qq'),
62
+ isAll: data.qq === 'all',
63
+ }),
64
+ image: (data) => {
65
+ const url = extractImageUrl(data);
66
+ return url ? { type: 'image', url, summary: data.summary } : null;
67
+ },
68
+ reply: async (data) => {
69
+ const response = await getMsg({ message_id: Number(data.id) });
70
+ if (!response.data?.message)
71
+ return null;
72
+ return {
73
+ type: 'reply',
74
+ messageId: str(data, 'id'),
75
+ message: response.data.raw_message,
76
+ senderId: String(response.data.sender.user_id),
77
+ sender: response.data.sender.nickname,
78
+ };
79
+ },
80
+ video: (data) => ({
81
+ type: 'video',
82
+ url: str(data, 'url'),
83
+ fileSize: num(data, 'file_size'),
84
+ }),
85
+ face: (data) => ({ type: 'text', text: getEmojiForFaceId(str(data, 'id')) }),
86
+ record: (data) => data.path ? {
87
+ type: 'audio',
88
+ path: str(data, 'path'),
89
+ file: str(data, 'file'),
90
+ url: data.url,
91
+ fileSize: num(data, 'file_size'),
92
+ } : null,
93
+ file: (data) => ({
94
+ type: 'file',
95
+ fileId: str(data, 'file'),
96
+ fileSize: num(data, 'file_size'),
97
+ }),
98
+ json: (data) => parseJsonSegment({ type: 'json', data: { data: str(data, 'data') } }),
99
+ };
68
100
  async function napCatToOpenClaw(segment) {
69
101
  const data = segment.data;
70
- switch (segment.type) {
71
- case 'text':
72
- return { type: 'text', text: String(data.text || '') };
73
- case 'at':
74
- return {
75
- type: 'at',
76
- userId: String(data.qq || ''),
77
- isAll: data.qq === 'all',
78
- };
79
- case 'image': {
80
- const url = extractImageUrl(data);
81
- return url ? { type: 'image', url, summary: data.summary } : null;
82
- }
83
- case 'reply':
84
- const response = await getMsg({
85
- message_id: Number(data.id),
86
- });
87
- if (response.data?.message == undefined) {
88
- return null;
89
- }
90
- return {
91
- type: 'reply',
92
- messageId: String(data.id),
93
- message: response.data.raw_message,
94
- senderId: String(response.data.sender.user_id),
95
- sender: response.data.sender.nickname
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
- };
103
- case 'face':
104
- return { type: 'text', text: getEmojiForFaceId(String(data.id || '')) };
105
- case 'record':
106
- return data.path ? {
107
- type: 'audio',
108
- path: String(data.path),
109
- file: String(data.file || ''),
110
- url: data.url,
111
- fileSize: data.file_size ? parseInt(String(data.file_size), 10) : undefined,
112
- } : null;
113
- case 'file':
114
- return {
115
- type: 'file',
116
- fileId: String(data.file || ''),
117
- fileSize: data.file_size ? parseInt(String(data.file_size), 10) : undefined
118
- };
119
- case 'json':
120
- return parseJsonSegment(segment);
121
- default:
122
- log.warn('adapters', `Unknown message type (inbound): ${segment.type}`);
123
- return null;
102
+ const converter = inboundConverters[segment.type];
103
+ if (!converter) {
104
+ log.warn('adapters', `Unknown message type (inbound): ${segment.type}`);
105
+ return null;
124
106
  }
107
+ return converter(data);
125
108
  }
126
- // =============================================================================
127
- // OpenClaw -> NapCat Adapters (Outbound)
128
- // =============================================================================
129
- function openClawSegmentToNapCat(content) {
130
- switch (content.type) {
131
- case 'text':
132
- return { type: 'text', data: { text: content.text } };
133
- case 'at':
134
- return { type: 'at', data: { qq: content.isAll ? 'all' : content.userId } };
135
- case 'image':
136
- return { type: 'image', data: { file: content.url, url: content.url } };
137
- case 'reply':
138
- return { type: 'reply', data: { id: content.messageId } };
139
- case 'file':
140
- return { type: 'file', data: { file: content.file, url: content.url, file_size: content.fileSize } };
141
- case 'audio':
142
- return {
143
- type: 'record',
144
- data: { file: content.file, path: content.path, url: content.url, file_size: content.fileSize }
145
- };
146
- default:
147
- log.warn('adapters', `Unknown content type (outbound): ${content.type}`);
148
- return null;
109
+ const outboundConverters = {
110
+ text: (content, account) => ({
111
+ type: 'text',
112
+ data: { text: account.markdownFormat ? markdownToText(content.text) : content.text },
113
+ }),
114
+ at: (content) => {
115
+ const { isAll, userId } = content;
116
+ return { type: 'at', data: { qq: isAll ? 'all' : userId } };
117
+ },
118
+ image: (content) => {
119
+ const { url } = content;
120
+ return { type: 'image', data: { file: url, url } };
121
+ },
122
+ reply: (content) => ({
123
+ type: 'reply',
124
+ data: { id: content.messageId },
125
+ }),
126
+ file: (content) => {
127
+ const c = content;
128
+ return { type: 'file', data: { file: c.file, url: c.url, file_size: c.fileSize } };
129
+ },
130
+ audio: (content) => {
131
+ const c = content;
132
+ return { type: 'record', data: { file: c.file, path: c.path, url: c.url, file_size: c.fileSize } };
133
+ },
134
+ };
135
+ function openClawToNapCat(content, account) {
136
+ const converter = outboundConverters[content.type];
137
+ if (!converter) {
138
+ log.warn('adapters', `Unknown content type (outbound): ${content.type}`);
139
+ return null;
149
140
  }
141
+ return converter(content, account);
150
142
  }
151
- export function openClawToNapCatMessage(content) {
143
+ // =============================================================================
144
+ // 导出 API
145
+ // =============================================================================
146
+ export async function outboundMessageAdapter(content, account) {
152
147
  const segments = [];
153
148
  for (const item of content) {
154
- const segment = openClawSegmentToNapCat(item);
155
- if (segment) {
149
+ const segment = openClawToNapCat(item, account);
150
+ if (segment)
156
151
  segments.push(segment);
157
- }
158
152
  }
159
153
  return segments;
160
154
  }
161
- export async function napCatToOpenClawMessage(segments) {
155
+ export async function inboundMessageAdapter(segments) {
162
156
  const normalized = normalizeMessage(segments);
163
157
  const content = [];
164
158
  for (const segment of normalized) {
165
159
  const result = await napCatToOpenClaw(segment);
166
- if (result) {
160
+ if (result)
167
161
  content.push(result);
168
- }
169
162
  }
170
163
  return content;
171
164
  }
165
+ const textFormatters = {
166
+ text: (c) => c.text,
167
+ at: (c) => {
168
+ const { isAll, userId } = c;
169
+ return `[提及]${isAll ? '@全体成员' : `@${userId || 'unknown'}`}`;
170
+ },
171
+ image: (c) => `[图片]${c.url || ''}`,
172
+ audio: (c) => `[音频]${c.path || ''}`,
173
+ video: (c) => `[视频]${c.url || ''}`,
174
+ file: (c) => `[文件]${c.fileId || ''}`,
175
+ json: (c) => `[JSON]\n\n\`\`\`json\n${c.data || ''}\n\`\`\``,
176
+ reply: (c) => {
177
+ const { sender, senderId, message } = c;
178
+ const senderInfo = sender && senderId ? `${sender}(${senderId})` : '(未知用户)';
179
+ const replyMsg = message ?? '(无法获取原消息)';
180
+ const quotedContent = `${senderInfo}:\n${replyMsg}`.replace(/^/gm, '> ');
181
+ return `[回复]\n\n${quotedContent}`;
182
+ },
183
+ };
184
+ /** 将 OpenClaw 消息内容转换为纯文本 */
185
+ export function formatContentToText(content) {
186
+ return content
187
+ .map(c => textFormatters[c.type]?.(c) ?? null)
188
+ .filter((v) => v !== null)
189
+ .join('\n');
190
+ }
191
+ const mediaExtractors = [
192
+ {
193
+ check: (c) => c.some(x => x.type === 'image'),
194
+ extract: (c) => {
195
+ const img = c.find(x => x.type === 'image');
196
+ return img ? { type: 'image/jpeg', path: img.url, url: img.url } : undefined;
197
+ },
198
+ },
199
+ {
200
+ check: (c) => c.some(x => x.type === 'audio'),
201
+ extract: (c) => {
202
+ const audio = c.find(x => x.type === 'audio');
203
+ return audio ? { type: 'audio/amr', path: audio.path, url: audio.url } : undefined;
204
+ },
205
+ },
206
+ {
207
+ check: (c) => c.some(x => x.type === 'file'),
208
+ extract: (c) => {
209
+ const file = c.find(x => x.type === 'file');
210
+ return file ? { type: 'application/octet-stream', path: file.file, url: file.url } : undefined;
211
+ },
212
+ },
213
+ ];
214
+ /** 检查是否包含媒体 */
215
+ export function hasMediaContent(content) {
216
+ return content.some(c => c.type === 'image' || c.type === 'audio' || c.type === 'file');
217
+ }
218
+ /** 从消息内容提取媒体信息 */
219
+ export function extractMedia(content) {
220
+ for (const extractor of mediaExtractors) {
221
+ if (extractor.check(content)) {
222
+ return extractor.extract(content);
223
+ }
224
+ }
225
+ return undefined;
226
+ }
@@ -1,7 +1,2 @@
1
- /**
2
- * QQ NapCat Plugin for OpenClaw
3
- * Main plugin entry point
4
- */
5
- import type { ChannelPlugin } from "openclaw/plugin-sdk";
6
- import type { QQConfig } from "./types";
7
- export declare const qqPlugin: ChannelPlugin<QQConfig>;
1
+ import type { QQAccount } from "./types";
2
+ export declare const qqPlugin: import("openclaw/plugin-sdk").ChannelPlugin<QQAccount, unknown, unknown>;