@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.
- package/README.md +12 -16
- package/dist/index.d.ts +5 -11
- package/dist/index.js +9 -18
- package/dist/src/adapters/message.d.ts +15 -4
- package/dist/src/adapters/message.js +179 -124
- package/dist/src/channel.d.ts +2 -7
- package/dist/src/channel.js +231 -312
- package/dist/src/core/auth.d.ts +67 -0
- package/dist/src/core/auth.js +154 -0
- package/dist/src/core/config.d.ts +5 -7
- package/dist/src/core/config.js +6 -8
- package/dist/src/core/connection.d.ts +6 -5
- package/dist/src/core/connection.js +17 -70
- package/dist/src/core/dispatch.d.ts +7 -54
- package/dist/src/core/dispatch.js +210 -398
- package/dist/src/core/event-handler.d.ts +42 -0
- package/dist/src/core/event-handler.js +171 -0
- package/dist/src/core/request.d.ts +3 -8
- package/dist/src/core/request.js +13 -126
- package/dist/src/core/runtime.d.ts +2 -11
- package/dist/src/core/runtime.js +0 -47
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/setup-surface.d.ts +2 -0
- package/dist/src/setup-surface.js +59 -0
- package/dist/src/types/index.d.ts +69 -25
- package/dist/src/types/index.js +3 -4
- package/dist/src/utils/cqcode.d.ts +0 -9
- package/dist/src/utils/cqcode.js +0 -17
- package/dist/src/utils/index.d.ts +0 -17
- package/dist/src/utils/index.js +17 -154
- package/dist/src/utils/log.js +2 -2
- package/dist/src/utils/markdown.d.ts +5 -0
- package/dist/src/utils/markdown.js +57 -5
- package/openclaw.plugin.json +3 -2
- package/package.json +9 -11
- package/dist/src/onboarding.d.ts +0 -10
- package/dist/src/onboarding.js +0 -98
|
@@ -1,445 +1,257 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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 ?
|
|
100
|
-
user_id: !isGroup ?
|
|
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.
|
|
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
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
*
|
|
45
|
+
* 防抖器
|
|
46
|
+
* @param params
|
|
126
47
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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 =
|
|
99
|
+
const groupConfig = getQQConfigByChatType(true, peerId, account);
|
|
147
100
|
recordPendingHistoryEntry({
|
|
148
101
|
historyMap: historyCache,
|
|
149
|
-
historyKey:
|
|
102
|
+
historyKey: peerId,
|
|
150
103
|
limit: groupConfig.historyLimit ?? 20,
|
|
151
104
|
entry: {
|
|
152
|
-
sender: `${senderName}(${senderId})`,
|
|
153
|
-
body:
|
|
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 {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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:
|
|
122
|
+
id: peerId,
|
|
168
123
|
},
|
|
169
|
-
runtime: runtime.channel,
|
|
170
|
-
sessionStore: context.cfg.session?.store
|
|
171
124
|
});
|
|
172
|
-
//
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
194
|
-
|
|
138
|
+
timestamp: msg.timestamp,
|
|
139
|
+
previousTimestamp,
|
|
140
|
+
envelope: envelopeOptions,
|
|
141
|
+
body: msg.text,
|
|
195
142
|
});
|
|
196
|
-
|
|
197
|
-
const
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 ?
|
|
153
|
+
ChatType: isGroup ? "channel" : "direct",
|
|
208
154
|
ConversationLabel: fromLabel,
|
|
209
|
-
SenderId: senderId,
|
|
210
|
-
SenderName: senderName,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
},
|
|
175
|
+
}).catch((err) => {
|
|
176
|
+
log.error('dispatch', `session record failed: ${err}`);
|
|
229
177
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
}
|