@ssfxx44533/onebot-qq 0.1.2
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 +119 -0
- package/index.ts +3 -0
- package/openclaw.plugin.json +192 -0
- package/package.json +47 -0
- package/setup-entry.ts +3 -0
- package/src/channel.ts +549 -0
- package/src/config.js +380 -0
- package/src/inbound.ts +587 -0
- package/src/message.js +235 -0
- package/src/onebot-client.js +226 -0
- package/src/plugin.ts +16 -0
- package/src/runtime.js +85 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAccountScopedDmSecurityPolicy,
|
|
3
|
+
} from "openclaw/plugin-sdk/channel-policy";
|
|
4
|
+
import {
|
|
5
|
+
PAIRING_APPROVED_MESSAGE,
|
|
6
|
+
} from "openclaw/plugin-sdk/channel-status";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ACCOUNT_ID,
|
|
10
|
+
DEFAULT_MAX_CHUNK_LENGTH,
|
|
11
|
+
ONEBOT_QQ_CHANNEL_SCHEMA,
|
|
12
|
+
normalizeOneBotGroupId,
|
|
13
|
+
normalizeOneBotSenderEntry,
|
|
14
|
+
normalizeOneBotUserId,
|
|
15
|
+
parseOutboundTarget,
|
|
16
|
+
resolveLocalFilePath,
|
|
17
|
+
resolveMediaKind,
|
|
18
|
+
resolveOneBotAccount,
|
|
19
|
+
resolveUploadName,
|
|
20
|
+
isRemoteUrl,
|
|
21
|
+
listOneBotAccountIds,
|
|
22
|
+
} from "./config.js";
|
|
23
|
+
import {
|
|
24
|
+
splitTextChunks,
|
|
25
|
+
} from "./message.js";
|
|
26
|
+
import {
|
|
27
|
+
getLiveAccount,
|
|
28
|
+
registerLiveAccount,
|
|
29
|
+
unregisterLiveAccount,
|
|
30
|
+
updateLiveAccountSelfId,
|
|
31
|
+
} from "./runtime.js";
|
|
32
|
+
import {
|
|
33
|
+
startOneBotMonitor,
|
|
34
|
+
withOneBotSocket,
|
|
35
|
+
} from "./onebot-client.js";
|
|
36
|
+
import {
|
|
37
|
+
handleOneBotInboundMessage,
|
|
38
|
+
} from "./inbound.ts";
|
|
39
|
+
|
|
40
|
+
function buildTextSegments(text, replyToId) {
|
|
41
|
+
const segments = [];
|
|
42
|
+
const normalizedReplyToId = String(replyToId ?? "").trim();
|
|
43
|
+
if (normalizedReplyToId) {
|
|
44
|
+
segments.push({
|
|
45
|
+
type: "reply",
|
|
46
|
+
data: { id: normalizedReplyToId },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (text) {
|
|
50
|
+
segments.push({
|
|
51
|
+
type: "text",
|
|
52
|
+
data: { text },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return segments;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function withPreferredSocket(account, work) {
|
|
59
|
+
const live = getLiveAccount(account.accountId);
|
|
60
|
+
if (live?.socket && live.socket.closed !== true) {
|
|
61
|
+
try {
|
|
62
|
+
return await work(live.socket);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (live.socket.closed !== true) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return await withOneBotSocket(
|
|
71
|
+
{
|
|
72
|
+
wsUrl: account.wsUrl,
|
|
73
|
+
callTimeoutMs: account.callTimeoutMs,
|
|
74
|
+
},
|
|
75
|
+
work,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveActionForTarget(target, baseAction) {
|
|
80
|
+
if (baseAction === "send_msg") {
|
|
81
|
+
return target.chatType === "group" ? "send_group_msg" : "send_private_msg";
|
|
82
|
+
}
|
|
83
|
+
if (baseAction === "upload_file") {
|
|
84
|
+
return target.chatType === "group" ? "upload_group_file" : "upload_private_file";
|
|
85
|
+
}
|
|
86
|
+
return baseAction;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveTargetParams(target) {
|
|
90
|
+
if (target.chatType === "group") {
|
|
91
|
+
return { group_id: String(target.targetId) };
|
|
92
|
+
}
|
|
93
|
+
return { user_id: String(target.targetId) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function sendTextToTarget(account, target, text, replyToId = null) {
|
|
97
|
+
const chunks = splitTextChunks(text, account.maxChunkLength);
|
|
98
|
+
if (chunks.length === 0) {
|
|
99
|
+
return { channel: "onebot-qq", messageId: "" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let lastMessageId = "";
|
|
103
|
+
await withPreferredSocket(account, async (socket) => {
|
|
104
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
105
|
+
const response = await socket.call(resolveActionForTarget(target, "send_msg"), {
|
|
106
|
+
...resolveTargetParams(target),
|
|
107
|
+
message: buildTextSegments(chunk, index === 0 ? replyToId : null),
|
|
108
|
+
});
|
|
109
|
+
lastMessageId = String(response?.data?.message_id ?? lastMessageId ?? "");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { channel: "onebot-qq", messageId: lastMessageId };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function sendSegmentMediaToTarget(account, target, mediaKind, mediaUrl, text = "", replyToId = null) {
|
|
117
|
+
const normalizedMediaUrl = isRemoteUrl(mediaUrl) ? mediaUrl : resolveLocalFilePath(mediaUrl);
|
|
118
|
+
const segments = buildTextSegments(text, replyToId);
|
|
119
|
+
segments.push({
|
|
120
|
+
type:
|
|
121
|
+
mediaKind === "audio"
|
|
122
|
+
? "record"
|
|
123
|
+
: mediaKind === "video"
|
|
124
|
+
? "video"
|
|
125
|
+
: "image",
|
|
126
|
+
data: { file: normalizedMediaUrl },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let messageId = "";
|
|
130
|
+
await withPreferredSocket(account, async (socket) => {
|
|
131
|
+
const response = await socket.call(resolveActionForTarget(target, "send_msg"), {
|
|
132
|
+
...resolveTargetParams(target),
|
|
133
|
+
message: segments,
|
|
134
|
+
});
|
|
135
|
+
messageId = String(response?.data?.message_id ?? "");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { channel: "onebot-qq", messageId };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function uploadFileToTarget(account, target, mediaUrl) {
|
|
142
|
+
const file = isRemoteUrl(mediaUrl) ? mediaUrl : resolveLocalFilePath(mediaUrl);
|
|
143
|
+
await withPreferredSocket(account, async (socket) => {
|
|
144
|
+
await socket.call(resolveActionForTarget(target, "upload_file"), {
|
|
145
|
+
...resolveTargetParams(target),
|
|
146
|
+
file,
|
|
147
|
+
name: resolveUploadName(mediaUrl),
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return { channel: "onebot-qq", messageId: "" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolvePayloadMediaUrls(payload) {
|
|
155
|
+
if (Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0) {
|
|
156
|
+
return payload.mediaUrls
|
|
157
|
+
.map((entry) => String(entry ?? "").trim())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
}
|
|
160
|
+
const single = String(payload?.mediaUrl ?? "").trim();
|
|
161
|
+
return single ? [single] : [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function deliverPayloadToChat({ account, target, payload }) {
|
|
165
|
+
const text = String(payload?.text ?? "");
|
|
166
|
+
const mediaUrls = resolvePayloadMediaUrls(payload);
|
|
167
|
+
const replyToId = String(payload?.replyToId ?? target.replyToId ?? "").trim() || null;
|
|
168
|
+
|
|
169
|
+
if (mediaUrls.length === 0) {
|
|
170
|
+
if (!text.trim()) {
|
|
171
|
+
return { channel: "onebot-qq", messageId: "" };
|
|
172
|
+
}
|
|
173
|
+
return await sendTextToTarget(account, target, text, replyToId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let first = true;
|
|
177
|
+
let lastResult = { channel: "onebot-qq", messageId: "" };
|
|
178
|
+
for (const mediaUrl of mediaUrls) {
|
|
179
|
+
const mediaKind = resolveMediaKind(mediaUrl);
|
|
180
|
+
if (mediaKind === "image" || mediaKind === "audio" || mediaKind === "video") {
|
|
181
|
+
lastResult = await sendSegmentMediaToTarget(
|
|
182
|
+
account,
|
|
183
|
+
target,
|
|
184
|
+
mediaKind,
|
|
185
|
+
mediaUrl,
|
|
186
|
+
first ? text : "",
|
|
187
|
+
first ? replyToId : null,
|
|
188
|
+
);
|
|
189
|
+
} else {
|
|
190
|
+
if (first && text.trim()) {
|
|
191
|
+
await sendTextToTarget(account, target, text, replyToId);
|
|
192
|
+
}
|
|
193
|
+
lastResult = await uploadFileToTarget(account, target, mediaUrl);
|
|
194
|
+
}
|
|
195
|
+
first = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return lastResult;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseTargetOrThrow(account, rawTarget) {
|
|
202
|
+
const parsed = parseOutboundTarget(rawTarget, account.defaultTo ? parseOutboundTarget(account.defaultTo) : null);
|
|
203
|
+
if (!parsed) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"Invalid OneBot QQ target. Use a QQ number for private chat, or group:<groupId> for groups.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createStatusSnapshot(account, runtime) {
|
|
212
|
+
return {
|
|
213
|
+
accountId: account.accountId,
|
|
214
|
+
name: account.name,
|
|
215
|
+
enabled: account.enabled,
|
|
216
|
+
configured: true,
|
|
217
|
+
running: runtime?.running ?? false,
|
|
218
|
+
selfId: runtime?.selfId ?? account.selfId ?? null,
|
|
219
|
+
dmPolicy: account.dmPolicy,
|
|
220
|
+
allowFrom: account.allowFrom,
|
|
221
|
+
lastError: runtime?.lastError ?? null,
|
|
222
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
223
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function uniqueEntries(values) {
|
|
228
|
+
return [...new Set(values.filter(Boolean))];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function sliceDirectoryEntries(entries, query, limit) {
|
|
232
|
+
const normalizedQuery = String(query ?? "").trim().toLowerCase();
|
|
233
|
+
const filtered = normalizedQuery
|
|
234
|
+
? entries.filter((entry) => {
|
|
235
|
+
const haystack = `${entry.id} ${entry.name ?? ""}`.toLowerCase();
|
|
236
|
+
return haystack.includes(normalizedQuery);
|
|
237
|
+
})
|
|
238
|
+
: entries;
|
|
239
|
+
const max = Number.isFinite(limit) && Number(limit) > 0 ? Number(limit) : filtered.length;
|
|
240
|
+
return filtered.slice(0, max);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function listConfiguredPeerIds(account) {
|
|
244
|
+
if (account.allowFrom.includes("*")) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return uniqueEntries(account.allowFrom.map((entry) => normalizeOneBotSenderEntry(entry)));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function listConfiguredGroupIds(account) {
|
|
251
|
+
return uniqueEntries(
|
|
252
|
+
Object.keys(account.groups ?? {})
|
|
253
|
+
.map((entry) => normalizeOneBotGroupId(entry))
|
|
254
|
+
.filter(Boolean),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const onebotQqPlugin = {
|
|
259
|
+
id: "onebot-qq",
|
|
260
|
+
meta: {
|
|
261
|
+
id: "onebot-qq",
|
|
262
|
+
label: "OneBot QQ",
|
|
263
|
+
selectionLabel: "OneBot QQ (NapCat)",
|
|
264
|
+
docsPath: "/channels/onebot-qq",
|
|
265
|
+
docsLabel: "OneBot QQ",
|
|
266
|
+
blurb: "NapCat / OneBot QQ channel for OpenClaw.",
|
|
267
|
+
order: 76,
|
|
268
|
+
quickstartAllowFrom: true,
|
|
269
|
+
},
|
|
270
|
+
configSchema: {
|
|
271
|
+
schema: ONEBOT_QQ_CHANNEL_SCHEMA,
|
|
272
|
+
},
|
|
273
|
+
pairing: {
|
|
274
|
+
idLabel: "qqUserId",
|
|
275
|
+
normalizeAllowEntry: (entry) => normalizeOneBotSenderEntry(entry),
|
|
276
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
277
|
+
const targetId = normalizeOneBotUserId(id);
|
|
278
|
+
if (!targetId) {
|
|
279
|
+
throw new Error(`Invalid QQ user id for pairing approval: ${id}`);
|
|
280
|
+
}
|
|
281
|
+
const account = resolveOneBotAccount(cfg, DEFAULT_ACCOUNT_ID);
|
|
282
|
+
await sendTextToTarget(
|
|
283
|
+
account,
|
|
284
|
+
{
|
|
285
|
+
chatType: "private",
|
|
286
|
+
targetId,
|
|
287
|
+
groupId: null,
|
|
288
|
+
userId: targetId,
|
|
289
|
+
},
|
|
290
|
+
PAIRING_APPROVED_MESSAGE,
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
capabilities: {
|
|
295
|
+
chatTypes: ["direct", "group"],
|
|
296
|
+
media: true,
|
|
297
|
+
},
|
|
298
|
+
messaging: {
|
|
299
|
+
normalizeTarget: (target) => String(target ?? "").trim(),
|
|
300
|
+
targetResolver: {
|
|
301
|
+
looksLikeId: (value) =>
|
|
302
|
+
/^(?:\d+|(?:onebot-qq:)?(?:group|qq-group|g|private|user|qq|qq-private|u):\d+)$/i.test(
|
|
303
|
+
String(value ?? "").trim(),
|
|
304
|
+
),
|
|
305
|
+
hint: "<qq>|group:<qq-group-id>",
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
reload: { configPrefixes: ["channels.onebot-qq"] },
|
|
309
|
+
config: {
|
|
310
|
+
listAccountIds: (cfg) => listOneBotAccountIds(cfg),
|
|
311
|
+
resolveAccount: (cfg, accountId) => resolveOneBotAccount(cfg, accountId),
|
|
312
|
+
isConfigured: () => true,
|
|
313
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
314
|
+
resolveOneBotAccount(cfg, accountId ?? DEFAULT_ACCOUNT_ID).allowFrom,
|
|
315
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
316
|
+
uniqueEntries(allowFrom.map((entry) => normalizeOneBotSenderEntry(entry))),
|
|
317
|
+
resolveDefaultTo: ({ cfg, accountId }) =>
|
|
318
|
+
resolveOneBotAccount(cfg, accountId ?? DEFAULT_ACCOUNT_ID).defaultTo ?? undefined,
|
|
319
|
+
describeAccount: (account) => ({
|
|
320
|
+
accountId: account.accountId,
|
|
321
|
+
name: account.name,
|
|
322
|
+
enabled: account.enabled,
|
|
323
|
+
configured: true,
|
|
324
|
+
selfId: account.selfId,
|
|
325
|
+
wsUrl: account.wsUrl,
|
|
326
|
+
}),
|
|
327
|
+
},
|
|
328
|
+
security: {
|
|
329
|
+
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
|
330
|
+
buildAccountScopedDmSecurityPolicy({
|
|
331
|
+
cfg,
|
|
332
|
+
channelKey: "onebot-qq",
|
|
333
|
+
accountId,
|
|
334
|
+
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
335
|
+
policy: account.dmPolicy,
|
|
336
|
+
allowFrom: account.allowFrom ?? [],
|
|
337
|
+
policyPathSuffix: "dmPolicy",
|
|
338
|
+
normalizeEntry: (raw) => normalizeOneBotSenderEntry(raw),
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
status: {
|
|
342
|
+
defaultRuntime: {
|
|
343
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
344
|
+
running: false,
|
|
345
|
+
selfId: null,
|
|
346
|
+
lastError: null,
|
|
347
|
+
lastInboundAt: null,
|
|
348
|
+
lastOutboundAt: null,
|
|
349
|
+
},
|
|
350
|
+
buildChannelSummary: ({ account, snapshot }) =>
|
|
351
|
+
createStatusSnapshot(account, snapshot),
|
|
352
|
+
buildAccountSnapshot: ({ account, runtime }) =>
|
|
353
|
+
createStatusSnapshot(account, runtime),
|
|
354
|
+
},
|
|
355
|
+
outbound: {
|
|
356
|
+
deliveryMode: "direct",
|
|
357
|
+
textChunkLimit: DEFAULT_MAX_CHUNK_LENGTH,
|
|
358
|
+
sendText: async (ctx) => {
|
|
359
|
+
const account = resolveOneBotAccount(ctx.cfg, ctx.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
360
|
+
const target = parseTargetOrThrow(account, ctx.to);
|
|
361
|
+
return await sendTextToTarget(account, target, ctx.text, ctx.replyToId ?? null);
|
|
362
|
+
},
|
|
363
|
+
sendMedia: async (ctx) => {
|
|
364
|
+
const account = resolveOneBotAccount(ctx.cfg, ctx.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
365
|
+
const target = parseTargetOrThrow(account, ctx.to);
|
|
366
|
+
return await deliverPayloadToChat({
|
|
367
|
+
account,
|
|
368
|
+
target,
|
|
369
|
+
payload: {
|
|
370
|
+
text: ctx.text,
|
|
371
|
+
mediaUrl: ctx.mediaUrl,
|
|
372
|
+
replyToId: ctx.replyToId,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
sendPayload: async (ctx) => {
|
|
377
|
+
const account = resolveOneBotAccount(ctx.cfg, ctx.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
378
|
+
const target = parseTargetOrThrow(account, ctx.to);
|
|
379
|
+
return await deliverPayloadToChat({
|
|
380
|
+
account,
|
|
381
|
+
target,
|
|
382
|
+
payload: ctx.payload,
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
directory: {
|
|
387
|
+
self: async ({ cfg, accountId }) => {
|
|
388
|
+
const account = resolveOneBotAccount(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
389
|
+
const live = getLiveAccount(account.accountId);
|
|
390
|
+
const selfId = live?.selfId ?? account.selfId;
|
|
391
|
+
if (!selfId) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
kind: "user",
|
|
396
|
+
id: selfId,
|
|
397
|
+
name: account.name,
|
|
398
|
+
raw: {
|
|
399
|
+
accountId: account.accountId,
|
|
400
|
+
selfId,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
405
|
+
const account = resolveOneBotAccount(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
406
|
+
return sliceDirectoryEntries(
|
|
407
|
+
listConfiguredPeerIds(account).map((id) => ({
|
|
408
|
+
kind: "user",
|
|
409
|
+
id,
|
|
410
|
+
name: `QQ user ${id}`,
|
|
411
|
+
})),
|
|
412
|
+
query,
|
|
413
|
+
limit,
|
|
414
|
+
);
|
|
415
|
+
},
|
|
416
|
+
listGroups: async ({ cfg, accountId, query, limit }) => {
|
|
417
|
+
const account = resolveOneBotAccount(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
418
|
+
return sliceDirectoryEntries(
|
|
419
|
+
listConfiguredGroupIds(account).map((id) => ({
|
|
420
|
+
kind: "group",
|
|
421
|
+
id,
|
|
422
|
+
name: `QQ Group ${id}`,
|
|
423
|
+
})),
|
|
424
|
+
query,
|
|
425
|
+
limit,
|
|
426
|
+
);
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
resolver: {
|
|
430
|
+
resolveTargets: async ({ inputs, kind }) =>
|
|
431
|
+
inputs.map((input) => {
|
|
432
|
+
const trimmed = String(input ?? "").trim();
|
|
433
|
+
const parsed = parseOutboundTarget(trimmed);
|
|
434
|
+
if (!parsed) {
|
|
435
|
+
return {
|
|
436
|
+
input,
|
|
437
|
+
resolved: false,
|
|
438
|
+
note: "invalid QQ target",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (kind === "group" && parsed.chatType !== "group") {
|
|
442
|
+
return {
|
|
443
|
+
input,
|
|
444
|
+
resolved: false,
|
|
445
|
+
note: "expected QQ group target",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (kind === "user" && parsed.chatType !== "private") {
|
|
449
|
+
return {
|
|
450
|
+
input,
|
|
451
|
+
resolved: false,
|
|
452
|
+
note: "expected QQ user target",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
input,
|
|
457
|
+
resolved: true,
|
|
458
|
+
id: parsed.targetId,
|
|
459
|
+
name: parsed.chatType === "group" ? `QQ Group ${parsed.targetId}` : `QQ user ${parsed.targetId}`,
|
|
460
|
+
};
|
|
461
|
+
}),
|
|
462
|
+
},
|
|
463
|
+
agentPrompt: {
|
|
464
|
+
messageToolHints: () => [
|
|
465
|
+
"For OneBot QQ private chats, use the recipient QQ number as the target.",
|
|
466
|
+
"For QQ groups, use group:<groupId> as the target.",
|
|
467
|
+
"When sending generated images back to QQ, keep them as image attachments instead of converting them to plain links.",
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
auth: {
|
|
471
|
+
login: async () => {
|
|
472
|
+
throw new Error(
|
|
473
|
+
"OneBot QQ login is managed by NapCat. Configure channels.onebot-qq.wsUrl and start NapCat separately.",
|
|
474
|
+
);
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
gateway: {
|
|
478
|
+
startAccount: async (ctx) => {
|
|
479
|
+
const account = resolveOneBotAccount(ctx.cfg, ctx.accountId ?? ctx.account?.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
480
|
+
ctx.setStatus?.({
|
|
481
|
+
accountId: account.accountId,
|
|
482
|
+
running: true,
|
|
483
|
+
lastError: null,
|
|
484
|
+
selfId: account.selfId,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const monitor = startOneBotMonitor({
|
|
488
|
+
account,
|
|
489
|
+
log: ctx.log,
|
|
490
|
+
onStatus: (patch) => {
|
|
491
|
+
ctx.setStatus?.({
|
|
492
|
+
accountId: account.accountId,
|
|
493
|
+
...patch,
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
onConnected: ({ socket, selfId }) => {
|
|
497
|
+
registerLiveAccount(account.accountId, { socket, selfId });
|
|
498
|
+
if (selfId) {
|
|
499
|
+
updateLiveAccountSelfId(account.accountId, selfId);
|
|
500
|
+
ctx.setStatus?.({
|
|
501
|
+
accountId: account.accountId,
|
|
502
|
+
selfId,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
ctx.log?.info?.(
|
|
506
|
+
`[${account.accountId}] connected to NapCat at ${account.wsUrl}${selfId ? ` as ${selfId}` : ""}`,
|
|
507
|
+
);
|
|
508
|
+
},
|
|
509
|
+
onDisconnected: (socket) => {
|
|
510
|
+
unregisterLiveAccount(account.accountId, socket);
|
|
511
|
+
},
|
|
512
|
+
onEvent: async (event) => {
|
|
513
|
+
const live = getLiveAccount(account.accountId);
|
|
514
|
+
await handleOneBotInboundMessage({
|
|
515
|
+
account,
|
|
516
|
+
cfg: ctx.cfg,
|
|
517
|
+
runtime: ctx.channelRuntime,
|
|
518
|
+
event,
|
|
519
|
+
selfId: live?.selfId ?? account.selfId,
|
|
520
|
+
log: ctx.log,
|
|
521
|
+
onStatus: (patch) => {
|
|
522
|
+
ctx.setStatus?.({
|
|
523
|
+
accountId: account.accountId,
|
|
524
|
+
...patch,
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
deliver: deliverPayloadToChat,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
if (ctx.abortSignal.aborted) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
await new Promise((resolve) => {
|
|
537
|
+
ctx.abortSignal.addEventListener("abort", resolve, { once: true });
|
|
538
|
+
});
|
|
539
|
+
} finally {
|
|
540
|
+
await monitor.stop();
|
|
541
|
+
unregisterLiveAccount(account.accountId);
|
|
542
|
+
ctx.setStatus?.({
|
|
543
|
+
accountId: account.accountId,
|
|
544
|
+
running: false,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
};
|