@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
@@ -1,445 +1,257 @@
1
- /**
2
- * Message Dispatch Module
3
- * Handles routing and dispatching incoming messages to the AI
4
- */
5
- import { buildPendingHistoryContextFromMap, clearHistoryEntries, recordPendingHistoryEntry, resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk";
6
- import { getRuntime, getContext, getSession, clearSession, updateSession, getLoginInfo, historyCache } from './runtime.js';
7
- import { getFile, sendMsg, setInputStatus } from './request.js';
8
- import { napCatToOpenClawMessage, openClawToNapCatMessage } from '../adapters/message.js';
9
- import { Logger as log, markdownToText, buildMediaMessage } from '../utils/index.js';
10
- import { CHANNEL_ID } from "./config.js";
11
- /**
12
- * Convert OpenClaw message content array to plain text
13
- * For images, includes the URL so AI models can access them
14
- * For replies, includes quoted message content if available
15
- */
16
- async function contentToPlainText(content) {
17
- const results = await Promise.all(content.map(async (c) => {
18
- switch (c.type) {
19
- case 'text':
20
- return c.text;
21
- case 'at':
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
- }
36
- case 'json':
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
- }
44
- default:
45
- return null;
46
- }
47
- }));
48
- return results.filter((v) => v !== null).join('\n');
49
- }
50
- async function contextToMedia(content) {
51
- const hasMedia = content.some(c => c.type === 'image' || c.type === 'audio' || c.type === 'file');
52
- if (!hasMedia) {
53
- return;
54
- }
55
- const image = content.find(c => c.type === 'image');
56
- if (image) {
57
- return {
58
- type: 'image/jpeg',
59
- path: image.url,
60
- url: image.url,
61
- };
62
- }
63
- const audio = content.find(c => c.type === 'audio');
64
- if (audio) {
65
- return {
66
- type: 'audio/amr',
67
- path: audio.path,
68
- url: audio.url,
69
- };
70
- }
71
- const file = content.find(c => c.type === 'file');
72
- if (file) {
73
- const fileInfo = await getFile({ file_id: file.fileId });
74
- if (fileInfo.data?.file == undefined) {
75
- return;
76
- }
77
- return {
78
- type: 'application/octet-stream',
79
- path: fileInfo.data?.file,
80
- url: fileInfo.data?.url,
81
- };
82
- }
83
- return;
84
- }
85
- async function sendText(isGroup, chatId, text) {
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
- }];
1
+ import { createChannelInboundDebouncer, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
2
+ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
3
+ import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
4
+ import { recordPendingHistoryEntry } from "openclaw/plugin-sdk/reply-history";
5
+ import { getLoginInfo, historyCache } from './runtime.js';
6
+ import { sendMsg, setInputStatus } from './request.js';
7
+ import { outboundMessageAdapter } from '../adapters/message.js';
8
+ import { Logger as log, buildMediaMessage } from '../utils/index.js';
9
+ import { QQ_CHANNEL } from "./config.js";
10
+ import { getQQConfigByChatType } from "./auth.js";
11
+ async function send(account, isGroup, to, messageSegments) {
96
12
  try {
97
13
  await sendMsg({
98
14
  message_type: isGroup ? 'group' : 'private',
99
- group_id: isGroup ? chatId : undefined,
100
- user_id: !isGroup ? chatId : undefined,
101
- message: messageSegments,
15
+ group_id: isGroup ? to : undefined,
16
+ user_id: !isGroup ? to : undefined,
17
+ message: await outboundMessageAdapter(messageSegments, account),
102
18
  });
103
- log.info('dispatch', `Sent reply: ${text.slice(0, 100)}`);
19
+ log.debug('dispatch', `Sent reply success`);
104
20
  }
105
21
  catch (error) {
106
22
  log.error('dispatch', `Send failed: ${error}`);
107
23
  }
108
24
  }
109
- async function sendMedia(isGroup, chatId, mediaUrl) {
110
- const content = [buildMediaMessage(mediaUrl)];
111
- try {
112
- await sendMsg({
113
- message_type: isGroup ? 'group' : 'private',
114
- group_id: isGroup ? chatId : undefined,
115
- user_id: !isGroup ? chatId : undefined,
116
- message: openClawToNapCatMessage(content),
117
- });
118
- log.info('dispatch', `Sent reply: ${mediaUrl.slice(0, 100)}`);
119
- }
120
- catch (error) {
121
- log.error('dispatch', `Send failed: ${error}`);
25
+ function mention(account, content, groupId, targetId, loginInfo) {
26
+ const config = getQQConfigByChatType(true, groupId, account);
27
+ const isMentionEnabled = !!config?.requireMention;
28
+ const isPokeEnabled = !!config?.requirePoke;
29
+ const isWakeEnabled = !!config?.wakeWord?.trim();
30
+ if (!isMentionEnabled && !isPokeEnabled && !isWakeEnabled) {
31
+ log.debug('dispatch', 'All requires are disabled, returning true by default.');
32
+ return true;
122
33
  }
34
+ const requireMention = isMentionEnabled &&
35
+ (content.includes('[提及]@全体成员') ||
36
+ (!!loginInfo?.userId && content.includes(`[提及]@${loginInfo.userId}`)));
37
+ const requirePoke = isPokeEnabled &&
38
+ (content.includes('[动作]') && targetId === loginInfo?.userId);
39
+ const requireWake = isWakeEnabled &&
40
+ content.includes(config.wakeWord ?? "");
41
+ log.debug('dispatch', `Require mention: ${requireMention}, require poke: ${requirePoke}, require wake: ${requireWake}`);
42
+ return requireMention || requirePoke || requireWake;
123
43
  }
124
44
  /**
125
- * Dispatch an incoming message to the AI for processing
45
+ * 防抖器
46
+ * @param params
126
47
  */
127
- export async function dispatchMessage(params) {
128
- let { chatType, chatId, senderId, senderName, messageId, content, media, timestamp, targetId } = params;
129
- const runtime = getRuntime();
130
- if (!runtime) {
131
- log.warn('dispatch', `Plugin runtime not available`);
132
- return;
133
- }
134
- const context = getContext();
135
- if (!context) {
136
- log.warn('dispatch', `No gateway context`);
137
- return;
138
- }
139
- const isGroup = chatType === 'group';
140
- const config = context.account;
141
- // At 模式处理
48
+ function createDebouncer(params) {
49
+ return createChannelInboundDebouncer({
50
+ cfg: params.cfg,
51
+ channel: QQ_CHANNEL,
52
+ buildKey: (item) => {
53
+ const peerId = item.msg.isGroup
54
+ ? (item.msg.groupId ?? item.msg.senderId)
55
+ : item.msg.senderId;
56
+ return `qq:${item.account.accountId}:${peerId}`;
57
+ },
58
+ shouldDebounce: (item) => shouldDebounceTextInbound({
59
+ text: item.msg.text,
60
+ cfg: item.cfg,
61
+ hasMedia: item.msg.hasMedia,
62
+ }),
63
+ onFlush: async (items) => {
64
+ if (items.length === 0)
65
+ return;
66
+ const mergedText = items
67
+ .map((item) => item.msg.text)
68
+ .filter(Boolean)
69
+ .join("\n");
70
+ const first = items[0];
71
+ const mergedMsg = {
72
+ ...first.msg,
73
+ text: mergedText,
74
+ };
75
+ await processInboundMessage({
76
+ ...first,
77
+ msg: mergedMsg,
78
+ });
79
+ },
80
+ onError: (err, items) => {
81
+ log.error('dispatch', `debounce flush failed for ${items.length} items:`, err);
82
+ },
83
+ });
84
+ }
85
+ /**
86
+ * 入站消息解析
87
+ * @param params
88
+ */
89
+ async function processInboundMessage(params) {
90
+ const { cfg, account, runtime, msg } = params;
91
+ const isGroup = msg.isGroup;
92
+ const loginInfo = getLoginInfo();
93
+ const peerId = isGroup ? (msg.groupId ?? msg.senderId) : msg.senderId;
94
+ // For group messages, check @mention requirement
142
95
  if (isGroup) {
143
- const isMention = mention(content, chatId, targetId);
96
+ const isMention = mention(account, msg.text, peerId, msg.targetId, loginInfo);
144
97
  if (!isMention) {
145
98
  log.debug('dispatch', `Skipping group message (not mentioned)`);
146
- const groupConfig = getGroupConfig(chatId, config);
99
+ const groupConfig = getQQConfigByChatType(true, peerId, account);
147
100
  recordPendingHistoryEntry({
148
101
  historyMap: historyCache,
149
- historyKey: chatId,
102
+ historyKey: peerId,
150
103
  limit: groupConfig.historyLimit ?? 20,
151
104
  entry: {
152
- sender: `${senderName}(${senderId})`,
153
- body: content,
154
- timestamp: timestamp,
155
- messageId: messageId,
105
+ sender: `${msg.senderName}(${msg.senderId})`,
106
+ body: msg.text,
107
+ timestamp: msg.timestamp,
108
+ messageId: msg.messageId,
156
109
  },
157
110
  });
158
111
  return;
159
112
  }
160
113
  }
161
- const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
162
- cfg: context.cfg,
163
- channel: CHANNEL_ID,
164
- accountId: context.accountId,
114
+ const { channel } = runtime;
115
+ // 1.解析路由
116
+ const route = channel.routing.resolveAgentRoute({
117
+ cfg,
118
+ channel: QQ_CHANNEL,
119
+ accountId: account.accountId,
165
120
  peer: {
166
121
  kind: isGroup ? "group" : "direct",
167
- id: chatId,
122
+ id: peerId,
168
123
  },
169
- runtime: runtime.channel,
170
- sessionStore: context.cfg.session?.store
171
124
  });
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({
191
- channel: CHANNEL_ID,
125
+ // 2.构建 Envelope
126
+ const storePath = channel.session.resolveStorePath(cfg.session?.store, {
127
+ agentId: route.agentId
128
+ });
129
+ const previousTimestamp = channel.session.readSessionUpdatedAt({
130
+ storePath,
131
+ sessionKey: route.sessionKey,
132
+ });
133
+ const envelopeOptions = channel.reply.resolveEnvelopeFormatOptions(cfg);
134
+ const fromLabel = isGroup ? `group:${peerId}` : (msg.senderName ?? `user:${msg.senderId}`);
135
+ const body = channel.reply.formatAgentEnvelope({
136
+ channel: QQ_CHANNEL,
192
137
  from: fromLabel,
193
- body: content,
194
- timestamp,
138
+ timestamp: msg.timestamp,
139
+ previousTimestamp,
140
+ envelope: envelopeOptions,
141
+ body: msg.text,
195
142
  });
196
- log.debug('dispatch', `Inbound envelope: ${body}`);
197
- const fromAddress = `qq:${fromLabel}`;
198
- const toAddress = `qq:${chatId}`;
199
- const ctxPayload = runtime.channel.reply.finalizeInboundContext({
143
+ // 3.构建消息上下文
144
+ const ctxPayload = channel.reply.finalizeInboundContext({
200
145
  Body: body,
201
- RawBody: content,
202
- CommandBody: content,
203
- From: fromAddress,
204
- To: toAddress,
146
+ BodyForAgent: msg.text,
147
+ RawBody: msg.text,
148
+ CommandBody: msg.text,
149
+ From: `${QQ_CHANNEL}:${msg.senderId}`,
150
+ To: `${QQ_CHANNEL}:${peerId}`,
205
151
  SessionKey: route.sessionKey,
206
152
  AccountId: route.accountId,
207
- ChatType: isGroup ? 'group' : 'direct',
153
+ ChatType: isGroup ? "channel" : "direct",
208
154
  ConversationLabel: fromLabel,
209
- SenderId: senderId,
210
- SenderName: senderName,
211
- Provider: CHANNEL_ID,
212
- Surface: CHANNEL_ID,
213
- MessageSid: messageId,
214
- Timestamp: timestamp,
215
- MediaType: media?.type,
216
- MediaPath: media?.path,
217
- MediaUrl: media?.url,
218
- OriginatingChannel: CHANNEL_ID,
219
- OriginatingTo: toAddress,
155
+ SenderId: msg.senderId,
156
+ SenderName: msg.senderName,
157
+ WasMentioned: isGroup ? (msg.wasMentioned ?? false) : undefined,
158
+ Provider: QQ_CHANNEL,
159
+ Surface: QQ_CHANNEL,
160
+ MessageSid: msg.messageId,
161
+ MessageSidFull: msg.messageId,
162
+ Timestamp: msg.timestamp,
163
+ MediaType: msg.media?.type,
164
+ MediaPath: msg.media?.path,
165
+ MediaUrl: msg.media?.url,
166
+ ReplyToId: msg.replyToId,
167
+ OriginatingChannel: QQ_CHANNEL,
168
+ OriginatingTo: `${QQ_CHANNEL}:${peerId}`,
220
169
  });
221
- log.info('dispatch', `Dispatching to agent ${route.agentId}, session: ${route.sessionKey}`);
222
- await runtime.channel.session.recordInboundSession({
170
+ // 4.记录 Session
171
+ channel.session.recordSessionMetaFromInbound({
223
172
  storePath,
224
- sessionKey: route.sessionKey,
173
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
225
174
  ctx: ctxPayload,
226
- onRecordError(err) {
227
- log.error('dispatch', `Failed to record inbound session: ${err}`);
228
- },
175
+ }).catch((err) => {
176
+ log.error('dispatch', `session record failed: ${err}`);
229
177
  });
230
- const messagesConfig = runtime.channel.reply.resolveEffectiveMessagesConfig(context.cfg, route.agentId);
231
- try {
232
- session.abortController = new AbortController();
233
- updateSession(route.sessionKey, session);
234
- await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
235
- ctx: ctxPayload,
236
- cfg: context.cfg,
237
- dispatcherOptions: {
238
- humanDelay: {
239
- mode: "off"
240
- },
241
- responsePrefix: messagesConfig.responsePrefix,
242
- onReplyStart: async () => {
243
- if (!isGroup) {
244
- // 输入状态
245
- await setInputStatus({
246
- user_id: senderId,
247
- event_type: 1
248
- });
249
- }
250
- },
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
- }
260
- log.info('dispatch', `deliver(${info.kind}): ${JSON.stringify(payload)}`);
261
- if (payload.text && !payload.text.startsWith('MEDIA:')) {
262
- await sendText(isGroup, chatId, payload.text);
263
- }
264
- if (payload.text && payload.text.startsWith('MEDIA:')) {
265
- await sendMedia(isGroup, chatId, payload.text.replace('MEDIA:', ''));
266
- }
267
- if (payload.mediaUrl) {
268
- await sendMedia(isGroup, chatId, payload.mediaUrl);
269
- }
270
- if (payload.mediaUrls && payload.mediaUrls.length > 0) {
271
- for (const mediaUrl of payload.mediaUrls) {
272
- await sendMedia(isGroup, chatId, mediaUrl);
273
- }
178
+ // 5.创建 Reply Pipeline
179
+ const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
180
+ cfg,
181
+ agentId: route.agentId,
182
+ channel: QQ_CHANNEL,
183
+ accountId: route.accountId,
184
+ });
185
+ // 6.分发 Reply
186
+ await channel.reply.dispatchReplyWithBufferedBlockDispatcher({
187
+ ctx: ctxPayload,
188
+ cfg,
189
+ dispatcherOptions: {
190
+ ...replyPipeline,
191
+ onReplyStart: async () => {
192
+ if (!isGroup) {
193
+ // 输入状态
194
+ await setInputStatus({
195
+ user_id: msg.senderId,
196
+ event_type: 1
197
+ });
198
+ }
199
+ },
200
+ deliver: async (payload) => {
201
+ const reply = resolveSendableOutboundReplyParts(payload);
202
+ if (!reply.hasContent)
203
+ return;
204
+ const chunkLimit = 4000;
205
+ const chunks = reply.trimmedText
206
+ ? channel.text.chunkMarkdownText(reply.trimmedText, chunkLimit)
207
+ : [];
208
+ const to = msg.replyToId ?? peerId;
209
+ const messageSegments = [];
210
+ for (const chunk of chunks) {
211
+ messageSegments.push({ type: "text", text: chunk });
212
+ }
213
+ if (reply.hasMedia) {
214
+ for (const mediaUrl of reply.mediaUrls) {
215
+ messageSegments.push(buildMediaMessage(mediaUrl));
274
216
  }
275
- },
276
- onError: async (err) => {
277
- log.error('dispatch', `Dispatch error: ${err}`);
278
- await sendText(isGroup, chatId, `[错误]\n${String(err)}`);
279
- },
217
+ }
218
+ await send(account, isGroup, to, messageSegments);
280
219
  },
281
- replyOptions: {
282
- abortSignal: session.abortController?.signal,
220
+ onError: (err, info) => {
221
+ log.error('dispatch', `${info.kind} reply failed: ${err}`);
283
222
  },
223
+ },
224
+ replyOptions: {
225
+ onModelSelected,
226
+ },
227
+ });
228
+ // 7.结束输入状态
229
+ if (!isGroup) {
230
+ await setInputStatus({
231
+ user_id: msg.senderId,
232
+ event_type: 2
284
233
  });
285
- log.info('dispatch', `Dispatch completed`);
286
- }
287
- catch (error) {
288
- log.error('dispatch', `Message processing failed: ${error}`);
289
234
  }
290
- finally {
291
- if (!isGroup) {
292
- // 输入状态
293
- await setInputStatus({
294
- user_id: senderId,
295
- event_type: 2
235
+ }
236
+ export function createInboundHandler(params) {
237
+ const debouncer = createDebouncer(params);
238
+ return async (msg) => {
239
+ const canDebounce = shouldDebounceTextInbound({
240
+ text: msg.text,
241
+ cfg: params.cfg,
242
+ hasMedia: msg.hasMedia,
243
+ });
244
+ if (canDebounce && debouncer.debounceMs > 0) {
245
+ await debouncer.debouncer.enqueue({
246
+ ...params,
247
+ msg,
296
248
  });
297
249
  }
298
- clearSession(route.sessionKey);
299
- }
300
- }
301
- /**
302
- * Handle group message event
303
- */
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
- }
309
- const content = await napCatToOpenClawMessage(event.message);
310
- const plainText = await contentToPlainText(content);
311
- const media = await contextToMedia(content);
312
- log.info('dispatch', `Group message from ${event.sender?.nickname || event.sender?.card || event.user_id}: ${plainText}, media: ${media != undefined}`);
313
- await dispatchMessage({
314
- chatType: 'group',
315
- chatId: String(event.group_id),
316
- senderId: String(event.user_id),
317
- senderName: event.sender?.nickname || event.sender?.card,
318
- messageId: String(event.message_id),
319
- content: plainText,
320
- media,
321
- timestamp: event.time * 1000,
322
- });
323
- }
324
- /**
325
- * Handle private message event
326
- */
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
- }
332
- const content = await napCatToOpenClawMessage(event.message);
333
- const plainText = await contentToPlainText(content);
334
- const media = await contextToMedia(content);
335
- log.info('dispatch', `Private message from ${event.sender?.nickname || event.user_id}: ${plainText}, media: ${media != undefined}`);
336
- await dispatchMessage({
337
- chatType: 'direct',
338
- chatId: String(event.user_id),
339
- senderId: String(event.user_id),
340
- senderName: event.sender?.nickname,
341
- messageId: String(event.message_id),
342
- content: plainText,
343
- media,
344
- timestamp: event.time * 1000,
345
- });
346
- }
347
- /**
348
- * Handle poke event
349
- */
350
- function extractPokeActionText(rawInfo) {
351
- if (!rawInfo)
352
- return '戳了戳';
353
- const actionItem = rawInfo.find(item => item.type === 'nor' && item.txt);
354
- return actionItem?.txt || '戳了戳';
355
- }
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
- }
361
- const actionText = extractPokeActionText(event.raw_info);
362
- log.info('dispatch', `Poke from ${event.user_id}: ${actionText}`);
363
- const pokeMessage = actionText || '戳了戳';
364
- const chatType = event.group_id ? 'group' : 'direct';
365
- const chatId = String(event.group_id || event.user_id);
366
- await dispatchMessage({
367
- chatType,
368
- chatId,
369
- senderId: String(event.user_id),
370
- senderName: String(event.user_id),
371
- messageId: `poke_${event.user_id}_${Date.now()}`,
372
- content: `[动作]${pokeMessage}`,
373
- timestamp: Date.now(),
374
- targetId: String(event.target_id),
375
- });
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;
250
+ else {
251
+ await processInboundMessage({
252
+ ...params,
253
+ msg,
254
+ });
255
+ }
256
+ };
445
257
  }