@alemonjs/qq-bot 2.1.30 → 2.1.32

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/dist/index.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en" id="__gui">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
package/lib/config.d.ts CHANGED
@@ -6,7 +6,15 @@ export type Options = {
6
6
  master_id?: string[];
7
7
  markdownToText?: boolean;
8
8
  hideUnsupported?: boolean | number;
9
+ bots?: Record<string, Omit<sdkOptions, 'app_id'> & {
10
+ app_id?: string;
11
+ }>;
12
+ default_bot?: string;
9
13
  } & sdkOptions;
10
14
  export declare const getQQBotConfig: () => Options;
15
+ export declare const getQQBotBots: () => {
16
+ bots: Map<string, sdkOptions>;
17
+ defaultBot?: string;
18
+ };
11
19
  export declare const getIdentity: (UserId: string) => readonly [boolean, string];
12
20
  export declare const getMaster: (UserId: string) => readonly [boolean, string];
package/lib/config.js CHANGED
@@ -8,6 +8,28 @@ const getQQBotConfig = () => {
8
8
  const bagValue = value[platformFullName] || {};
9
9
  return { ...commonValue, ...bagValue };
10
10
  };
11
+ const getQQBotBots = () => {
12
+ const config = getQQBotConfig();
13
+ const { bots: configuredBots, default_bot, master_id: _masterId, master_key: _masterKey, markdownToText: _markdown, hideUnsupported: _hide, ...common } = config;
14
+ const bots = new Map();
15
+ if (configuredBots && Object.keys(configuredBots).length) {
16
+ for (const [botId, value] of Object.entries(configuredBots)) {
17
+ if (value.app_id && value.app_id !== botId)
18
+ throw new Error(`qq-bot bots.${botId}.app_id must match its map key`);
19
+ if (!value.secret)
20
+ throw new Error(`qq-bot bots.${botId}.secret is required`);
21
+ bots.set(botId, { ...common, ...value, app_id: botId, secret: value.secret });
22
+ }
23
+ }
24
+ else if (config.app_id && config.secret) {
25
+ bots.set(config.app_id, common);
26
+ }
27
+ if (bots.size > 1 && !default_bot)
28
+ throw new Error('qq-bot.default_bot is required when multiple bots are configured');
29
+ if (default_bot && !bots.has(default_bot))
30
+ throw new Error(`qq-bot.default_bot ${default_bot} is not configured`);
31
+ return { bots, defaultBot: default_bot || (bots.size === 1 ? bots.keys().next().value : undefined) };
32
+ };
11
33
  const getIdentity = (UserId) => {
12
34
  const isMasterUser = UserId ? isMaster(UserId, platform) : false;
13
35
  const UserKey = createUserHashKey({
@@ -20,4 +42,4 @@ const getMaster = (UserId) => {
20
42
  return getIdentity(UserId);
21
43
  };
22
44
 
23
- export { getIdentity, getMaster, getQQBotConfig, platform, platformFullName };
45
+ export { getIdentity, getMaster, getQQBotBots, getQQBotConfig, platform, platformFullName };
package/lib/desktop.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getConfig, getConfigValue } from 'alemonjs';
5
+ import { getQQBotRegistry } from './index.websoket.js';
5
6
 
6
7
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
7
8
  const activate = context => {
@@ -22,7 +23,7 @@ const activate = context => {
22
23
  webView.onMessage(data => {
23
24
  try {
24
25
  if (data.type === 'qq-bot.form.save') {
25
- const db = data.data;
26
+ const { bots_json: _botsJson, ...db } = data.data;
26
27
  const config = getConfig();
27
28
  const value = config.value ?? {};
28
29
  value['qq-bot'] = {
@@ -43,6 +44,12 @@ const activate = context => {
43
44
  data: config['qq-bot'] ?? {}
44
45
  });
45
46
  }
47
+ else if (data.type === 'qq-bot.status') {
48
+ webView.postMessage({
49
+ type: 'qq-bot.status',
50
+ data: getQQBotRegistry()?.getConnectionStatus() ?? { Platform: 'qq-bot', state: 'stopped', bots: [] }
51
+ });
52
+ }
46
53
  }
47
54
  catch (e) {
48
55
  console.error(e);
package/lib/index.js CHANGED
@@ -1,16 +1,13 @@
1
- import { definePlatform, getConfigValue } from 'alemonjs';
1
+ import { definePlatform } from 'alemonjs';
2
2
  import { start } from './index.webhook.js';
3
3
  import { start as start$1 } from './index.websoket.js';
4
- import { platform } from './config.js';
4
+ import { getQQBotConfig } from './config.js';
5
+ export { platform } from './config.js';
5
6
  export { useClient, useMode, useValue } from './hook.js';
6
7
  export { QQBotAPI as API } from './sdk/api.js';
7
8
 
8
9
  const main = () => {
9
- let value = getConfigValue();
10
- if (!value) {
11
- value = {};
12
- }
13
- const config = value[platform];
10
+ const config = getQQBotConfig();
14
11
  if (config?.route || config?.port || config?.ws) {
15
12
  start();
16
13
  }
@@ -20,4 +17,4 @@ const main = () => {
20
17
  };
21
18
  var index = definePlatform({ main });
22
19
 
23
- export { index as default, platform };
20
+ export { index as default };
@@ -4,6 +4,9 @@ import { getQQBotConfig } from './config.js';
4
4
 
5
5
  const start = () => {
6
6
  const config = getQQBotConfig();
7
+ if (config.bots && Object.keys(config.bots).length > 1) {
8
+ throw new Error('qq-bot.bots is only supported by WebSocket mode; remove route/port/ws to use the QQ WebSocket gateway');
9
+ }
7
10
  const { master_id: _, master_key: __, ...cfgConfig } = config;
8
11
  const client = new QQBotClient({
9
12
  ...cfgConfig
@@ -1 +1,3 @@
1
+ import { QQBotRegistry } from './sdk/registry';
2
+ export declare const getQQBotRegistry: () => QQBotRegistry;
1
3
  export declare const start: () => void;
@@ -1,9 +1,13 @@
1
- import { QQBotClients } from './sdk/client.websoket.js';
2
- import { register } from './register.js';
3
- import { getQQBotConfig } from './config.js';
1
+ import { getQQBotConfig, getQQBotBots } from './config.js';
2
+ import { QQBotRegistry } from './sdk/registry.js';
4
3
 
4
+ let activeRegistry;
5
+ const getQQBotRegistry = () => activeRegistry;
5
6
  const start = () => {
6
7
  const config = getQQBotConfig();
8
+ const { bots, defaultBot } = getQQBotBots();
9
+ const registry = new QQBotRegistry(defaultBot);
10
+ activeRegistry = registry;
7
11
  const notPrivateIntents = [
8
12
  'GUILDS',
9
13
  'GUILD_MEMBERS',
@@ -28,18 +32,16 @@ const start = () => {
28
32
  else {
29
33
  intents.push(...notPrivateIntents, ...isGroupIntents, ...pubIntents);
30
34
  }
31
- const client = new QQBotClients({
32
- ...config,
33
- intents: config?.intents || intents,
34
- is_private: config?.is_private ?? false,
35
- sandbox: config?.sandbox ?? false,
36
- shard: config?.shard ?? [0, 1],
37
- gatewayURL: config?.gatewayURL,
38
- base_url_gateway: config?.base_url_gateway,
39
- base_url_app_access_token: config?.base_url_app_access_token
40
- });
41
- void client.connect(config?.gatewayURL);
42
- register(client);
35
+ for (const [botId, botConfig] of bots) {
36
+ const client = registry.add(botId, {
37
+ ...botConfig,
38
+ intents: botConfig.intents || intents,
39
+ is_private: botConfig.is_private ?? false,
40
+ sandbox: botConfig.sandbox ?? false,
41
+ shard: botConfig.shard ?? [0, 1]
42
+ });
43
+ void client.connect(botConfig.gatewayURL);
44
+ }
43
45
  };
44
46
 
45
- export { start };
47
+ export { getQQBotRegistry, start };
@@ -1,4 +1,11 @@
1
1
  export type C2C_MESSAGE_CREATE_TYPE = {
2
+ attachments?: {
3
+ id?: string;
4
+ url?: string;
5
+ content_type?: string;
6
+ filename?: string;
7
+ size?: number;
8
+ }[];
2
9
  author: {
3
10
  id: string;
4
11
  user_openid: string;
@@ -1,4 +1,11 @@
1
1
  export interface GROUP_AT_MESSAGE_CREATE_TYPE {
2
+ attachments?: {
3
+ id?: string;
4
+ url?: string;
5
+ content_type?: string;
6
+ filename?: string;
7
+ size?: number;
8
+ }[];
2
9
  author: {
3
10
  bot?: boolean;
4
11
  id: string;
@@ -1,4 +1,11 @@
1
1
  export interface GROUP_MESSAGE_CREATE_TYPE {
2
+ attachments?: {
3
+ id?: string;
4
+ url?: string;
5
+ content_type?: string;
6
+ filename?: string;
7
+ size?: number;
8
+ }[];
2
9
  author: {
3
10
  bot?: boolean;
4
11
  id: string;
package/lib/register.d.ts CHANGED
@@ -1,2 +1,11 @@
1
+ import { cbpPlatform } from 'alemonjs';
1
2
  import { QQBotClients } from './sdk/client.websoket';
2
- export declare const register: (client: QQBotClients) => void;
3
+ export type QQBotRegistration = {
4
+ onAction: (data: any, consume: (result: any[]) => void) => Promise<void>;
5
+ onApi: (data: any, consume: (result: any[]) => void) => Promise<void>;
6
+ };
7
+ export declare const register: (client: QQBotClients, options?: {
8
+ botId?: string;
9
+ cbp?: ReturnType<typeof cbpPlatform>;
10
+ bindActions?: boolean;
11
+ }) => QQBotRegistration;
package/lib/register.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import { cbpPlatform, FormatEvent, ResultCode, createResult } from 'alemonjs';
2
+ import { createHash } from 'node:crypto';
3
+ import { stat, readFile } from 'node:fs/promises';
4
+ import { basename } from 'node:path';
2
5
  import { GROUP_AT_MESSAGE_CREATE, C2C_MESSAGE_CREATE, DIRECT_MESSAGE_CREATE, AT_MESSAGE_CREATE, MESSAGE_CREATE } from './sends.js';
3
6
  import { getQQBotConfig, platform, getMaster, getIdentity } from './config.js';
4
7
 
5
- const register = (client) => {
8
+ const register = (client, options) => {
9
+ const MAX_MEDIA_SIZE = 100 * 1024 * 1024;
6
10
  const config = getQQBotConfig();
7
- const botId = String(config?.app_id ?? '');
11
+ const botId = String(options?.botId ?? config?.app_id ?? '');
8
12
  const port = process.env?.port || config?.port || 17117;
9
13
  const url = `ws://127.0.0.1:${port}`;
10
- const cbp = cbpPlatform(url);
14
+ const cbp = options?.cbp ?? cbpPlatform(url);
11
15
  const createUserAvatarURL = (authorId) => {
12
- return `https://q.qlogo.cn/qqapp/${config.app_id}/${authorId}/640`;
16
+ return `https://q.qlogo.cn/qqapp/${botId}/${authorId}/640`;
13
17
  };
14
18
  const getGroupMessageMeta = (event) => {
15
19
  const author = event?.author;
@@ -39,6 +43,122 @@ const register = (client) => {
39
43
  auditTime
40
44
  };
41
45
  };
46
+ const getMediaItems = (attachments) => {
47
+ return (attachments || []).flatMap(attachment => {
48
+ if (!attachment?.url) {
49
+ return [];
50
+ }
51
+ const mimeType = attachment.content_type || '';
52
+ const Type = mimeType.startsWith('image/')
53
+ ? 'image'
54
+ : mimeType.startsWith('audio/')
55
+ ? 'audio'
56
+ : mimeType.startsWith('video/')
57
+ ? 'video'
58
+ : 'file';
59
+ return [
60
+ {
61
+ Type,
62
+ Url: attachment.url,
63
+ ...(attachment.filename && { FileName: attachment.filename }),
64
+ ...(typeof attachment.size === 'number' && { FileSize: attachment.size }),
65
+ ...(mimeType && { MimeType: mimeType })
66
+ }
67
+ ];
68
+ });
69
+ };
70
+ const normalizeTarget = (target) => {
71
+ if (!target?.targetId || !target.scope) {
72
+ return undefined;
73
+ }
74
+ return target;
75
+ };
76
+ const validateTargetBot = (target) => {
77
+ return !target.BotId || target.BotId === botId;
78
+ };
79
+ const mediaCache = new Map();
80
+ const mediaType = (type) => (type === 'image' ? 1 : type === 'video' ? 2 : type === 'audio' ? 3 : 4);
81
+ const prepareMedia = async (target, params) => {
82
+ const sources = [params?.url, params?.data, params?.filePath, params?.fileId].filter(value => value !== undefined && value !== '').length;
83
+ if (sources !== 1) {
84
+ throw new Error('Provide exactly one media source: url, data, filePath, or fileId');
85
+ }
86
+ if (params.fileId) {
87
+ return { fileId: String(params.fileId), reused: true };
88
+ }
89
+ let data = params.data;
90
+ let buffer;
91
+ let filePath;
92
+ let fileSize;
93
+ let hashes;
94
+ let name = params.name;
95
+ let hashSource = String(params.url || '');
96
+ if (params.filePath) {
97
+ const metadata = await stat(String(params.filePath));
98
+ if (!metadata.isFile()) {
99
+ throw new Error('media filePath must point to a regular file');
100
+ }
101
+ if (metadata.size > MAX_MEDIA_SIZE) {
102
+ throw new Error('QQ media files must not exceed 100 MiB');
103
+ }
104
+ filePath = String(params.filePath);
105
+ fileSize = metadata.size;
106
+ hashes = await client.getMediaFileHashes(filePath);
107
+ hashSource = hashes.sha256;
108
+ name ||= basename(filePath);
109
+ if (fileSize < 5 * 1024 * 1024) {
110
+ buffer = await readFile(filePath);
111
+ data = `base64://${buffer.toString('base64')}`;
112
+ }
113
+ }
114
+ else if (data) {
115
+ hashSource = data;
116
+ const value = data.replace(/^base64:\/\//, '');
117
+ buffer = Buffer.from(value, 'base64');
118
+ if (buffer.length > MAX_MEDIA_SIZE) {
119
+ throw new Error('QQ media files must not exceed 100 MiB');
120
+ }
121
+ }
122
+ const key = [botId, target.scope, target.targetId, params.type, createHash('sha256').update(hashSource).digest('hex')].join(':');
123
+ const cached = mediaCache.get(key);
124
+ if (cached && (!cached.expiresAt || cached.expiresAt > Date.now())) {
125
+ return { fileId: cached.fileId, expiresAt: cached.expiresAt, reused: true };
126
+ }
127
+ return { key, url: params.url, data, buffer, filePath, fileSize, hashes, name, reused: false };
128
+ };
129
+ const uploadMedia = async (target, params) => {
130
+ if (!validateTargetBot(target)) {
131
+ throw new Error(`BotId ${target.BotId} is not active`);
132
+ }
133
+ if (target.scope !== 'group' && target.scope !== 'c2c') {
134
+ throw new Error('QQ media upload only supports group and c2c targets');
135
+ }
136
+ const prepared = await prepareMedia(target, params);
137
+ if (prepared.fileId) {
138
+ return prepared;
139
+ }
140
+ const fileType = mediaType(params.type);
141
+ const value = prepared.filePath && prepared.fileSize && prepared.fileSize >= 5 * 1024 * 1024
142
+ ? await client.postChunkedRichMedia({
143
+ scope: target.scope,
144
+ targetId: target.targetId,
145
+ fileType,
146
+ filePath: prepared.filePath,
147
+ size: prepared.fileSize,
148
+ hashes: prepared.hashes,
149
+ name: prepared.name
150
+ })
151
+ : prepared.buffer && prepared.buffer.length >= 5 * 1024 * 1024
152
+ ? await client.postChunkedRichMedia({ scope: target.scope, targetId: target.targetId, fileType, data: prepared.buffer, name: prepared.name })
153
+ : target.scope === 'group'
154
+ ? await client.postRichMediaByGroup(target.targetId, { file_type: fileType, url: prepared.url, file_data: prepared.data, srv_send_msg: false })
155
+ : await client.postRichMediaByUser(target.targetId, { file_type: fileType, url: prepared.url, file_data: prepared.data, srv_send_msg: false });
156
+ const expiresAt = value.ttl ? Date.now() + value.ttl * 1000 : undefined;
157
+ if (prepared.key) {
158
+ mediaCache.set(prepared.key, { fileId: value.file_info, expiresAt });
159
+ }
160
+ return { fileId: value.file_info, expiresAt, reused: false };
161
+ };
42
162
  const createUserMeta = (UserId, extra = {}) => {
43
163
  const [IsMaster, UserKey] = getIdentity(UserId);
44
164
  return {
@@ -85,6 +205,7 @@ const register = (client) => {
85
205
  })
86
206
  .addMessage({ MessageId: meta.messageId })
87
207
  .addText({ MessageText: msg?.trim() })
208
+ .addMedia({ MessageMedia: getMediaItems(event.attachments) })
88
209
  .addOpen({ OpenId: meta.openId })
89
210
  .add({ tag: 'GROUP_MESSAGE_CREATE' }).value);
90
211
  });
@@ -128,6 +249,7 @@ const register = (client) => {
128
249
  })
129
250
  .addMessage({ MessageId: meta.messageId })
130
251
  .addText({ MessageText: msg?.trim() })
252
+ .addMedia({ MessageMedia: getMediaItems(event.attachments) })
131
253
  .addOpen({ OpenId: meta.openId })
132
254
  .add({ tag: 'GROUP_AT_MESSAGE_CREATE' }).value);
133
255
  });
@@ -147,6 +269,7 @@ const register = (client) => {
147
269
  })
148
270
  .addMessage({ MessageId: event.id })
149
271
  .addText({ MessageText: event.content?.trim() })
272
+ .addMedia({ MessageMedia: getMediaItems(event.attachments) })
150
273
  .addOpen({ OpenId: `C2C:${event.author.user_openid}` })
151
274
  .add({ tag: 'C2C_MESSAGE_CREATE' }).value);
152
275
  });
@@ -170,6 +293,7 @@ const register = (client) => {
170
293
  })
171
294
  .addMessage({ MessageId: event.id })
172
295
  .addText({ MessageText: msg?.trim() })
296
+ .addMedia({ MessageMedia: getMediaItems(event.attachments) })
173
297
  .addOpen({ OpenId: `DIRECT:${event.guild_id}` })
174
298
  .add({ tag: 'DIRECT_MESSAGE_CREATE' }).value);
175
299
  });
@@ -195,6 +319,7 @@ const register = (client) => {
195
319
  })
196
320
  .addMessage({ MessageId: event.id })
197
321
  .addText({ MessageText: msg?.trim() })
322
+ .addMedia({ MessageMedia: getMediaItems(event.attachments) })
198
323
  .addOpen({ OpenId: `DIRECT:${event.guild_id}` })
199
324
  .add({ tag: 'AT_MESSAGE_CREATE' }).value);
200
325
  });
@@ -238,6 +363,7 @@ const register = (client) => {
238
363
  })
239
364
  .addMessage({ MessageId: event.id })
240
365
  .addText({ MessageText: msg?.trim() })
366
+ .addMedia({ MessageMedia: getMediaItems(event.attachments) })
241
367
  .addOpen({ OpenId: `DIRECT:${event.guild_id}` })
242
368
  .add({ tag: 'MESSAGE_CREATE' }).value);
243
369
  });
@@ -260,6 +386,11 @@ const register = (client) => {
260
386
  })
261
387
  .addMessage({ MessageId: `INTERACTION_CREATE:${event.id}` })
262
388
  .addText({ MessageText: MessageText })
389
+ .addInteraction({
390
+ InteractionId: event.id,
391
+ InteractionData: JSON.stringify(event.data.resolved),
392
+ Target: { scope: 'group', targetId: event.group_openid, BotId: botId }
393
+ })
263
394
  .addOpen({ OpenId: `C2C:${event.group_member_openid}` })
264
395
  .add({ tag: 'INTERACTION_CREATE_GROUP' }).value;
265
396
  cbp.send(e);
@@ -280,6 +411,11 @@ const register = (client) => {
280
411
  })
281
412
  .addMessage({ MessageId: event.id })
282
413
  .addText({ MessageText: MessageText })
414
+ .addInteraction({
415
+ InteractionId: event.id,
416
+ InteractionData: JSON.stringify(event.data.resolved),
417
+ Target: { scope: 'c2c', targetId: event.user_openid, BotId: botId }
418
+ })
283
419
  .addOpen({ OpenId: `C2C:${event.user_openid}` })
284
420
  .add({ tag: 'INTERACTION_CREATE_C2C' }).value;
285
421
  cbp.send(e);
@@ -296,6 +432,11 @@ const register = (client) => {
296
432
  .addUser({ UserId: event.data.resolved.user_id, UserKey, UserAvatar: UserAvatar, IsMaster: isMaster, IsBot: false })
297
433
  .addMessage({ MessageId: event.data.resolved.message_id })
298
434
  .addText({ MessageText: MessageText })
435
+ .addInteraction({
436
+ InteractionId: event.id,
437
+ InteractionData: JSON.stringify(event.data.resolved),
438
+ Target: { scope: 'channel', targetId: event.channel_id, BotId: botId }
439
+ })
299
440
  .addOpen({ OpenId: `DIRECT:${event.guild_id}` })
300
441
  .add({ tag: 'INTERACTION_CREATE_GUILD' }).value;
301
442
  cbp.send(e);
@@ -483,6 +624,24 @@ const register = (client) => {
483
624
  const api = {
484
625
  active: {
485
626
  send: {
627
+ target: async (target, val, replyId) => {
628
+ if (!validateTargetBot(target)) {
629
+ return [createResult(ResultCode.FailParams, `BotId ${target.BotId} is not active`, null)];
630
+ }
631
+ if (target.scope === 'group') {
632
+ return GROUP_AT_MESSAGE_CREATE(client, { ChannelId: target.targetId, MessageId: replyId }, val);
633
+ }
634
+ if (target.scope === 'c2c') {
635
+ return C2C_MESSAGE_CREATE(client, { UserId: target.targetId, MessageId: replyId }, val);
636
+ }
637
+ if (target.scope === 'channel') {
638
+ return AT_MESSAGE_CREATE(client, { ChannelId: target.targetId, MessageId: replyId }, val);
639
+ }
640
+ if (target.scope === 'direct') {
641
+ return DIRECT_MESSAGE_CREATE(client, { UserId: target.targetId, MessageId: replyId }, val);
642
+ }
643
+ return [createResult(ResultCode.FailParams, `Unsupported target scope: ${target.scope}`, null)];
644
+ },
486
645
  channel: async (SpaceId, val) => {
487
646
  if (/^GUILD:/.test(SpaceId)) {
488
647
  const id = SpaceId.replace('GUILD:', '');
@@ -619,11 +778,44 @@ const register = (client) => {
619
778
  const res = await api.active.send.user(userId, paramFormat);
620
779
  consume(res);
621
780
  }
781
+ else if (data.action === 'message.send.target') {
782
+ const target = normalizeTarget(data.payload.target);
783
+ if (!target) {
784
+ consume([createResult(ResultCode.FailParams, 'message.send.target 缺少 target', null)]);
785
+ return;
786
+ }
787
+ const res = await api.active.send.target(target, data.payload.params?.format || [], data.payload.params?.replyId);
788
+ consume(res);
789
+ }
622
790
  else if (data.action === 'message.delete') {
623
- const channelId = data.payload.ChannelId;
624
791
  const messageId = data.payload.MessageId;
792
+ const params = data.payload.params ?? {};
793
+ const target = normalizeTarget(data.payload.target);
794
+ if (target && !validateTargetBot(target)) {
795
+ consume([createResult(ResultCode.FailParams, `BotId ${target.BotId} is not active`, null)]);
796
+ return;
797
+ }
798
+ const scope = target?.scope ?? params.scope ?? params.messageType;
799
+ const request = scope === 'group'
800
+ ? client.grouMessageDelte(String(target?.targetId ?? params.groupId ?? data.payload.GroupId ?? data.payload.ChannelId), messageId)
801
+ : scope === 'user' || scope === 'c2c'
802
+ ? client.userMessageDelete(String(target?.targetId ?? params.userId ?? data.payload.UserId), messageId)
803
+ : scope === 'direct'
804
+ ? client.dmsMessageDelete(String(target?.targetId ?? params.guildId ?? data.payload.GuildId ?? data.payload.ChannelId), messageId)
805
+ : client.channelsMessagesDelete(target?.targetId ?? data.payload.ChannelId, messageId);
806
+ const res = await request.then(r => createResult(ResultCode.Ok, data.action, r)).catch(err => createResult(ResultCode.Fail, data.action, err));
807
+ consume([res]);
808
+ }
809
+ else if (data.action === 'interaction.ack') {
810
+ const params = data.payload.params ?? {};
811
+ const interactionId = String(params.interactionId ?? data.payload.InteractionId ?? '');
812
+ const mode = data.payload.target?.scope === 'channel' || data.payload.target?.scope === 'direct' || params.mode === 'guild' ? 'guild' : 'group';
813
+ if (!interactionId) {
814
+ consume([createResult(ResultCode.Fail, 'interaction.ack 缺少 InteractionId', null)]);
815
+ return;
816
+ }
625
817
  const res = await client
626
- .channelsMessagesDelete(channelId, messageId)
818
+ .interactionResponse(mode, interactionId, params.code)
627
819
  .then(r => createResult(ResultCode.Ok, data.action, r))
628
820
  .catch(err => createResult(ResultCode.Fail, data.action, err));
629
821
  consume([res]);
@@ -870,7 +1062,63 @@ const register = (client) => {
870
1062
  consume([res]);
871
1063
  }
872
1064
  else if (data.action === 'media.upload') {
873
- consume([createResult(ResultCode.Warn, 'media.upload not supported, use media.send.user or media.send.channel', null)]);
1065
+ const target = normalizeTarget(data.payload.target);
1066
+ if (!target) {
1067
+ consume([createResult(ResultCode.Warn, 'QQ media.upload requires target', null)]);
1068
+ return;
1069
+ }
1070
+ const res = await uploadMedia(target, data.payload.params || {})
1071
+ .then(value => createResult(ResultCode.Ok, data.action, value))
1072
+ .catch(err => createResult(ResultCode.Fail, data.action, err));
1073
+ consume([res]);
1074
+ }
1075
+ else if (data.action === 'media.send') {
1076
+ const target = normalizeTarget(data.payload.target);
1077
+ if (!target) {
1078
+ consume([createResult(ResultCode.FailParams, 'media.send 缺少 target', null)]);
1079
+ return;
1080
+ }
1081
+ if (!validateTargetBot(target)) {
1082
+ consume([createResult(ResultCode.FailParams, `BotId ${target.BotId} is not active`, null)]);
1083
+ return;
1084
+ }
1085
+ const params = data.payload.params || {};
1086
+ if (params.fileId) {
1087
+ const send = target.scope === 'group'
1088
+ ? client.groupOpenMessages(target.targetId, { msg_type: 7, content: params.content || '', media: { file_info: params.fileId } })
1089
+ : target.scope === 'c2c'
1090
+ ? client.usersOpenMessages(target.targetId, { msg_type: 7, content: params.content || '', media: { file_info: params.fileId } })
1091
+ : null;
1092
+ if (!send) {
1093
+ consume([createResult(ResultCode.Warn, 'QQ media.send only supports group and c2c targets', null)]);
1094
+ return;
1095
+ }
1096
+ const res = await send
1097
+ .then(value => createResult(ResultCode.Ok, data.action, { id: value.id }))
1098
+ .catch(err => createResult(ResultCode.Fail, data.action, err));
1099
+ consume([res]);
1100
+ return;
1101
+ }
1102
+ const upload = await uploadMedia(target, params)
1103
+ .then(value => value.fileId)
1104
+ .catch(error => {
1105
+ consume([createResult(ResultCode.Fail, data.action, error)]);
1106
+ return null;
1107
+ });
1108
+ if (!upload) {
1109
+ return;
1110
+ }
1111
+ await onactions({ action: 'media.send', payload: { target, params: { ...params, fileId: upload, url: undefined, data: undefined, filePath: undefined } } }, consume);
1112
+ }
1113
+ else if (data.action === 'connection.status') {
1114
+ const status = client.getConnectionStatus();
1115
+ consume([
1116
+ createResult(ResultCode.Ok, data.action, {
1117
+ Platform: platform,
1118
+ state: status.state,
1119
+ bots: [{ BotId: botId, ...status }]
1120
+ })
1121
+ ]);
874
1122
  }
875
1123
  else if (data.action === 'permission.get') {
876
1124
  const res = await client
@@ -920,7 +1168,6 @@ const register = (client) => {
920
1168
  consume([createResult(ResultCode.Fail, '请求失败', error)]);
921
1169
  }
922
1170
  };
923
- cbp.onactions((data, consume) => void onactions(data, consume));
924
1171
  const onapis = async (data, consume) => {
925
1172
  const key = data.payload?.key;
926
1173
  const params = data.payload?.params;
@@ -947,7 +1194,11 @@ const register = (client) => {
947
1194
  consume([createResult(ResultCode.Fail, '请求失败', error)]);
948
1195
  }
949
1196
  };
950
- cbp.onapis((data, consume) => void onapis(data, consume));
1197
+ if (options?.bindActions !== false) {
1198
+ cbp.onactions((data, consume) => void onactions(data, consume));
1199
+ cbp.onapis((data, consume) => void onapis(data, consume));
1200
+ }
1201
+ return { onAction: onactions, onApi: onapis };
951
1202
  };
952
1203
 
953
1204
  export { register };