@alemonjs/qq-bot 2.1.31 → 2.1.33

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.
@@ -0,0 +1,25 @@
1
+ import { cbpPlatform } from 'alemonjs';
2
+ import { QQBotClients } from './client.websoket.js';
3
+ import type { Options } from './typing.js';
4
+ export declare class QQBotRegistry {
5
+ #private;
6
+ constructor(defaultBot?: string, cbp?: ReturnType<typeof cbpPlatform>);
7
+ add(botId: string, options: Options): QQBotClients;
8
+ get(botId?: string): QQBotClients;
9
+ getConnectionStatus(botId?: string): {
10
+ Platform: string;
11
+ state: import("./api.js").QQBotConnectionState;
12
+ bots: {
13
+ state: import("./api.js").QQBotConnectionState;
14
+ transport: "websocket" | "webhook" | "proxy" | null;
15
+ reconnectAttempts: number;
16
+ heartbeatAcknowledged: boolean;
17
+ sessionId?: string;
18
+ sequence?: number;
19
+ resumed: boolean;
20
+ lastError?: string;
21
+ BotId: string;
22
+ }[];
23
+ };
24
+ disconnect(): void;
25
+ }
@@ -0,0 +1,116 @@
1
+ import { cbpPlatform, createResult, ResultCode } from 'alemonjs';
2
+ import { getQQBotConfig } from '../config.js';
3
+ import { register } from '../register.js';
4
+ import { QQBotClients } from './client.websoket.js';
5
+
6
+ class QQBotRegistry {
7
+ #bots = new Map();
8
+ #streams = new Map();
9
+ #defaultBot;
10
+ #cbp;
11
+ constructor(defaultBot, cbp) {
12
+ this.#defaultBot = defaultBot;
13
+ if (cbp) {
14
+ this.#cbp = cbp;
15
+ }
16
+ else {
17
+ const config = getQQBotConfig();
18
+ const port = process.env.port || config.port || 17117;
19
+ this.#cbp = cbpPlatform(`ws://127.0.0.1:${port}`);
20
+ }
21
+ this.#cbp.onactions((data, consume) => void this.#onAction(data, consume));
22
+ this.#cbp.onapis((data, consume) => void this.#onApi(data, consume));
23
+ }
24
+ add(botId, options) {
25
+ if (!botId)
26
+ throw new Error('QQ BotId is required');
27
+ if (this.#bots.has(botId))
28
+ throw new Error(`Duplicate QQ BotId: ${botId}`);
29
+ const client = new QQBotClients({ ...options, app_id: botId });
30
+ client.setStreamLifecycleListener(streamId => this.#streams.delete(streamId));
31
+ const adapter = register(client, { botId, cbp: this.#cbp, bindActions: false });
32
+ this.#bots.set(botId, { client, adapter });
33
+ return client;
34
+ }
35
+ get(botId) {
36
+ return botId ? this.#bots.get(botId)?.client : undefined;
37
+ }
38
+ getConnectionStatus(botId) {
39
+ const bots = [...this.#bots].flatMap(([BotId, entry]) => {
40
+ if (botId && botId !== BotId)
41
+ return [];
42
+ return [{ BotId, ...entry.client.getConnectionStatus() }];
43
+ });
44
+ const state = bots.some(item => item.state === 'ready')
45
+ ? 'ready'
46
+ : bots.some(item => item.state === 'connecting' || item.state === 'reconnecting')
47
+ ? 'connecting'
48
+ : bots[0]?.state || 'idle';
49
+ return { Platform: 'qq-bot', state, bots };
50
+ }
51
+ disconnect() {
52
+ for (const { client } of this.#bots.values())
53
+ client.disconnect();
54
+ this.#streams.clear();
55
+ }
56
+ #requestedBotId(data) {
57
+ const payload = data?.payload || {};
58
+ return payload.target?.BotId || payload.BotId || payload.event?.BotId || payload.event?.Platform?.BotId;
59
+ }
60
+ #select(data) {
61
+ const requested = this.#requestedBotId(data);
62
+ if (requested) {
63
+ const entry = this.#bots.get(String(requested));
64
+ return entry ? { entry } : { error: `BotId ${requested} is not configured (available: ${[...this.#bots.keys()].join(', ') || 'none'})` };
65
+ }
66
+ if (this.#bots.size === 1)
67
+ return { entry: this.#bots.values().next().value };
68
+ if (this.#defaultBot)
69
+ return { entry: this.#bots.get(this.#defaultBot) };
70
+ return { error: 'BotId is required because multiple QQ bots are active and no default_bot is configured' };
71
+ }
72
+ async #onAction(data, consume) {
73
+ if (data?.action === 'connection.status') {
74
+ const botId = data?.payload?.BotId;
75
+ const status = this.getConnectionStatus(botId);
76
+ if (botId && !status.bots.length) {
77
+ consume([createResult(ResultCode.FailParams, `BotId ${botId} is not configured`, null)]);
78
+ }
79
+ else {
80
+ consume([createResult(ResultCode.Ok, data.action, status)]);
81
+ }
82
+ return;
83
+ }
84
+ const { entry, error } = this.#select(data);
85
+ if (!entry) {
86
+ consume([createResult(ResultCode.FailParams, error || 'QQ Bot is unavailable', null)]);
87
+ return;
88
+ }
89
+ await entry.adapter.onAction(data, consume);
90
+ }
91
+ async #onApi(data, consume) {
92
+ const params = data?.payload?.params;
93
+ const key = String(data?.payload?.key || '');
94
+ const method = key.split('.').at(-1);
95
+ const streamId = method === 'streamUpdate' || method === 'streamComplete' || method === 'streamCancel' ? params?.[0] : undefined;
96
+ const requested = (Array.isArray(params) ? params[0]?.BotId : undefined) || (streamId ? this.#streams.get(String(streamId)) : undefined);
97
+ const { entry, error } = this.#select({ payload: { BotId: requested, event: data?.payload?.event } });
98
+ if (!entry) {
99
+ consume([createResult(ResultCode.FailParams, error || 'QQ Bot is unavailable', null)]);
100
+ return;
101
+ }
102
+ await entry.adapter.onApi(data, results => {
103
+ const result = results.find((item) => item.code === ResultCode.Ok)?.data;
104
+ if (method === 'streamOpen' && result?.streamId)
105
+ this.#streams.set(String(result.streamId), requested || this.#botIdFor(entry));
106
+ if (streamId && (method === 'streamComplete' || method === 'streamCancel'))
107
+ this.#streams.delete(String(streamId));
108
+ consume(results);
109
+ });
110
+ }
111
+ #botIdFor(entry) {
112
+ return [...this.#bots.entries()].find(([, value]) => value === entry)?.[0] || '';
113
+ }
114
+ }
115
+
116
+ export { QQBotRegistry };
@@ -0,0 +1,18 @@
1
+ export type PersistedSession = {
2
+ botId: string;
3
+ sessionId: string;
4
+ sequence: number;
5
+ };
6
+ export interface SessionStore {
7
+ load(botId: string): Promise<PersistedSession | null>;
8
+ save(session: PersistedSession): Promise<void>;
9
+ clear(botId: string): Promise<void>;
10
+ }
11
+ export declare class FileSessionStore implements SessionStore {
12
+ #private;
13
+ private readonly root;
14
+ constructor(root?: string);
15
+ load(botId: string): Promise<PersistedSession | null>;
16
+ save(session: PersistedSession): Promise<void>;
17
+ clear(botId: string): Promise<void>;
18
+ }
@@ -0,0 +1,56 @@
1
+ import { readFile, mkdir, writeFile, rename, unlink } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join, dirname } from 'node:path';
4
+
5
+ class FileSessionStore {
6
+ root;
7
+ #queues = new Map();
8
+ #generations = new Map();
9
+ constructor(root = join(process.cwd(), '.data', 'qq-bot', 'sessions')) {
10
+ this.root = root;
11
+ }
12
+ #path(botId) {
13
+ return join(this.root, `${encodeURIComponent(botId)}.json`);
14
+ }
15
+ async load(botId) {
16
+ try {
17
+ const value = JSON.parse(await readFile(this.#path(botId), 'utf8'));
18
+ return value?.botId === botId && typeof value.sessionId === 'string' && Number.isFinite(value.sequence) ? value : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ async save(session) {
25
+ const generation = this.#generations.get(session.botId) || 0;
26
+ await this.#enqueue(session.botId, async () => {
27
+ if (generation !== (this.#generations.get(session.botId) || 0))
28
+ return;
29
+ const path = this.#path(session.botId);
30
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
31
+ await mkdir(dirname(path), { recursive: true });
32
+ await writeFile(temp, JSON.stringify(session), 'utf8');
33
+ await rename(temp, path);
34
+ });
35
+ }
36
+ async clear(botId) {
37
+ this.#generations.set(botId, (this.#generations.get(botId) || 0) + 1);
38
+ await this.#enqueue(botId, async () => {
39
+ await unlink(this.#path(botId)).catch(() => undefined);
40
+ });
41
+ }
42
+ #enqueue(botId, task) {
43
+ const previous = this.#queues.get(botId) || Promise.resolve();
44
+ const next = previous.catch(() => undefined).then(task);
45
+ this.#queues.set(botId, next);
46
+ void next
47
+ .finally(() => {
48
+ if (this.#queues.get(botId) === next)
49
+ this.#queues.delete(botId);
50
+ })
51
+ .catch(() => undefined);
52
+ return next;
53
+ }
54
+ }
55
+
56
+ export { FileSessionStore };
@@ -1,5 +1,6 @@
1
1
  import { IntentsEnum } from './intents';
2
- export type MessageType = 0 | 1 | 2 | 3 | 4 | 7;
2
+ import type { SessionStore } from './session.js';
3
+ export type MessageType = 0 | 1 | 2 | 3 | 4 | 6 | 7;
3
4
  export type FileType = 1 | 2 | 3 | 4;
4
5
  export interface ButtonType {
5
6
  id: string;
@@ -54,6 +55,128 @@ export interface ApiRequestData {
54
55
  event_id?: any;
55
56
  msg_id?: string;
56
57
  msg_seq?: number;
58
+ is_wakeup?: boolean;
59
+ force_verify_image_resource?: boolean;
60
+ input_notify?: {
61
+ input_type: number;
62
+ input_second: number;
63
+ };
64
+ }
65
+ export interface VerifyInfo {
66
+ method: string;
67
+ verify_message?: string;
68
+ review_qa_list?: {
69
+ question: string;
70
+ answer: string;
71
+ }[];
72
+ }
73
+ export interface JoinRequest {
74
+ join_request_id: string;
75
+ risk_tips?: string;
76
+ union_openid?: string;
77
+ member_openid: string;
78
+ username: string;
79
+ apply_at: string;
80
+ apply_source: string;
81
+ invited_by?: string;
82
+ bot: boolean;
83
+ verify_info?: VerifyInfo;
84
+ auto_approved?: {
85
+ strategy_id: string;
86
+ };
87
+ }
88
+ export interface GlobalMuteRule {
89
+ mode: 'none' | 'always' | 'schedule';
90
+ schedule_rules?: {
91
+ task_id: string;
92
+ start_at: string;
93
+ end_at: string;
94
+ enabled: boolean;
95
+ }[];
96
+ recurring_rules?: {
97
+ task_id: string;
98
+ weekdays: number[];
99
+ start_time: string;
100
+ end_time: string;
101
+ enabled: boolean;
102
+ }[];
103
+ }
104
+ export interface MemberMuteState {
105
+ member_openid: string;
106
+ mute_expire_at: string;
107
+ username: string;
108
+ union_openid: string;
109
+ }
110
+ export interface SetMemberMuteState {
111
+ op: 'add' | 'update' | 'del';
112
+ member_openid: string;
113
+ mute_expire_at?: string;
114
+ }
115
+ export interface JoinApprovalStrategy {
116
+ strategy_id: string;
117
+ group_openids: string[];
118
+ group_ids: string[];
119
+ whitelist_user_count: number;
120
+ is_enable: 'on' | 'off';
121
+ expire_at: string;
122
+ created_at: string;
123
+ updated_at: string;
124
+ remark: string;
125
+ }
126
+ export interface GroupAction {
127
+ op: 'add' | 'del';
128
+ group_openids?: string[];
129
+ group_ids?: string[];
130
+ }
131
+ export interface UploadPrepareData {
132
+ file_type: FileType;
133
+ file_name: string;
134
+ file_size: string;
135
+ md5: string;
136
+ sha1: string;
137
+ md5_10m: string;
138
+ }
139
+ export interface UploadPart {
140
+ index: number;
141
+ presigned_url: string;
142
+ block_size: string;
143
+ }
144
+ export interface UploadConfig {
145
+ concurrency: number;
146
+ retry_timeout: number;
147
+ retry_delay: number;
148
+ }
149
+ export interface UploadPrepareResult {
150
+ upload_id: string;
151
+ block_size: string;
152
+ parts: UploadPart[];
153
+ upload_config: UploadConfig;
154
+ }
155
+ export interface UploadPartFinishData {
156
+ upload_id: string;
157
+ part_index: number;
158
+ block_size: string;
159
+ md5: string;
160
+ }
161
+ export interface StreamMessageData {
162
+ input_mode?: 'append' | 'replace';
163
+ input_state?: 1 | 10;
164
+ content_type?: 'text' | 'markdown';
165
+ content_raw?: string;
166
+ event_id?: string;
167
+ msg_id?: string;
168
+ msg_seq?: number;
169
+ index?: number;
170
+ stream_msg_id?: string;
171
+ is_wakeup?: boolean;
172
+ }
173
+ export interface StreamMessageResult {
174
+ id: string;
175
+ timestamp: string;
176
+ ext_info?: {
177
+ ref_idx?: string;
178
+ };
179
+ remain_msg_len?: number;
57
180
  }
58
181
  export interface Options {
59
182
  app_id: string;
@@ -68,4 +191,5 @@ export interface Options {
68
191
  gatewayURL?: string;
69
192
  base_url_gateway?: string;
70
193
  base_url_app_access_token?: string;
194
+ sessionStore?: SessionStore;
71
195
  }
package/lib/sends.d.ts CHANGED
@@ -5,12 +5,16 @@ export declare const GROUP_AT_MESSAGE_CREATE: (client: Client, event: {
5
5
  ChannelId: string;
6
6
  MessageId?: string;
7
7
  _tag?: string;
8
- }, val: DataEnums[]) => Promise<ClientAPIMessageResult[]>;
8
+ }, val: DataEnums[], options?: {
9
+ forceVerifyImageResource?: boolean;
10
+ }) => Promise<ClientAPIMessageResult[]>;
9
11
  export declare const C2C_MESSAGE_CREATE: (client: Client, event: {
10
12
  UserId: string;
11
13
  MessageId?: string;
12
14
  _tag?: string;
13
- }, val: DataEnums[]) => Promise<ClientAPIMessageResult[]>;
15
+ }, val: DataEnums[], options?: {
16
+ forceVerifyImageResource?: boolean;
17
+ }) => Promise<ClientAPIMessageResult[]>;
14
18
  export declare const DIRECT_MESSAGE_CREATE: (client: Client, event: {
15
19
  UserId: string;
16
20
  MessageId?: string;
package/lib/sends.js CHANGED
@@ -1,11 +1,31 @@
1
1
  import { readFileSync } from 'fs';
2
- import { createResult, ResultCode } from 'alemonjs';
2
+ import { createResult, ResultCode, logger } from 'alemonjs';
3
3
  import axios from 'axios';
4
4
  import { dataEnumToText, markdownToText, buttonsToText } from './format.js';
5
5
  import { getQQBotConfig } from './config.js';
6
6
 
7
7
  const MAX_BUTTON_ROWS = 5;
8
8
  const MAX_BUTTONS_PER_ROW = 5;
9
+ const BUTTON_STYLE_MAP = {
10
+ gray: 0,
11
+ blue: 1,
12
+ red: 3,
13
+ 'blue-fill': 4
14
+ };
15
+ const resolveButtonStyle = (style) => {
16
+ if (typeof style === 'number') {
17
+ return style;
18
+ }
19
+ if (typeof style === 'string') {
20
+ if (BUTTON_STYLE_MAP[style] !== undefined) {
21
+ return BUTTON_STYLE_MAP[style];
22
+ }
23
+ if (style.trim() !== '' && !Number.isNaN(Number(style))) {
24
+ return Number(style);
25
+ }
26
+ }
27
+ return 0;
28
+ };
9
29
  const createButtonsData = (rows, startId = 0) => {
10
30
  let id = startId;
11
31
  const clippedRows = rows.slice(0, MAX_BUTTON_ROWS);
@@ -18,25 +38,45 @@ const createButtonsData = (rows, startId = 0) => {
18
38
  const typing = options?.type ?? 'command';
19
39
  const typeMap = { command: 2, link: 0, call: 1 };
20
40
  const rowData = options?.rawData ?? {};
41
+ const data = options?.data;
42
+ const modal = options?.modal ??
43
+ (typeof data === 'object' && data
44
+ ? {
45
+ content: data.click,
46
+ confirmText: data.confirm,
47
+ cancelText: data.cancel
48
+ }
49
+ : undefined);
50
+ const action = {
51
+ type: typeMap[typing],
52
+ permission: {
53
+ type: typeof options.permission?.type === 'undefined' ? 2 : options?.permission?.type,
54
+ specify_user_ids: options?.permission?.userIds,
55
+ specify_role_ids: options?.permission?.roleIds
56
+ },
57
+ unsupport_tips: options?.toolTip ?? '',
58
+ data: typeof data === 'string' ? data : '',
59
+ at_bot_show_channel_list: options?.atBotShowChannelList ?? false,
60
+ enter: options?.autoEnter ?? false,
61
+ reply: options?.reply ?? false,
62
+ anchor: options?.anchor ?? 0,
63
+ click_limit: options?.clickLimit
64
+ };
65
+ if (modal) {
66
+ action.modal = {
67
+ content: modal.content ?? '是否确认操作?',
68
+ confirm_text: modal.confirmText ?? '是',
69
+ cancel_text: modal.cancelText ?? '否'
70
+ };
71
+ }
21
72
  return {
22
73
  id: String(id),
23
74
  render_data: {
24
75
  label: value,
25
76
  visited_label: value,
26
- style: options.style !== 'blue' ? 0 : 1
27
- },
28
- action: {
29
- type: typeMap[typing],
30
- permission: {
31
- type: typeof options.permission?.type === 'undefined' ? 2 : options?.permission?.type,
32
- specify_user_ids: options?.permission?.userIds,
33
- specify_role_ids: options?.permission?.roleIds
34
- },
35
- unsupport_tips: options?.toolTip ?? '',
36
- data: options?.data ?? '',
37
- at_bot_show_channel_list: false,
38
- enter: options?.autoEnter ?? false
77
+ style: resolveButtonStyle(options?.style)
39
78
  },
79
+ action,
40
80
  ...rowData
41
81
  };
42
82
  })
@@ -188,6 +228,9 @@ const extractContent = (val, mode) => {
188
228
  'Image',
189
229
  'ImageFile',
190
230
  'ImageURL',
231
+ 'Audio',
232
+ 'Video',
233
+ 'Attachment',
191
234
  'Markdown',
192
235
  'MarkdownOriginal',
193
236
  'BT.group',
@@ -232,6 +275,7 @@ const buildMdAndButtonsParams = (val) => {
232
275
  return null;
233
276
  }
234
277
  const params = {};
278
+ let keyboardExtra;
235
279
  for (const item of items) {
236
280
  if (item.type === 'ButtonTemplate') {
237
281
  if (item?.value) {
@@ -239,6 +283,13 @@ const buildMdAndButtonsParams = (val) => {
239
283
  }
240
284
  }
241
285
  else if (item.type === 'BT.group' && typeof item?.value !== 'string') {
286
+ const groupOptions = item?.options ?? {};
287
+ if (groupOptions.smallButton || groupOptions.rawData) {
288
+ keyboardExtra = {
289
+ smallButton: keyboardExtra?.smallButton || groupOptions.smallButton,
290
+ rawData: groupOptions.rawData
291
+ };
292
+ }
242
293
  if (params['keyboard']?.content?.rows) {
243
294
  const existingRows = params['keyboard'].content.rows;
244
295
  const currentId = params['keyboard'].content.nextId ?? existingRows.length;
@@ -274,6 +325,14 @@ const buildMdAndButtonsParams = (val) => {
274
325
  }
275
326
  }
276
327
  }
328
+ if (keyboardExtra && params['keyboard']?.content) {
329
+ if (keyboardExtra.smallButton) {
330
+ params['keyboard'].content.style = { font_size: 'small' };
331
+ }
332
+ if (keyboardExtra.rawData) {
333
+ Object.assign(params['keyboard'].content, keyboardExtra.rawData);
334
+ }
335
+ }
277
336
  if (params['keyboard']?.content?.nextId !== undefined) {
278
337
  delete params['keyboard'].content.nextId;
279
338
  }
@@ -298,46 +357,66 @@ const buildArkParams = (val) => {
298
357
  }
299
358
  return params;
300
359
  };
360
+ const MEDIA_FILE_TYPE = {
361
+ Image: 1,
362
+ ImageFile: 1,
363
+ ImageURL: 1,
364
+ Video: 2,
365
+ Audio: 3,
366
+ Attachment: 4
367
+ };
368
+ const filterMedia = (val) => {
369
+ return val.filter(item => MEDIA_FILE_TYPE[item.type] !== undefined);
370
+ };
301
371
  const filterImages = (val) => {
302
372
  return val.filter(item => item.type === 'Image' || item.type === 'ImageFile' || item.type === 'ImageURL');
303
373
  };
304
- const resolveRichMediaUrl = async (images, uploadMedia) => {
305
- for (const item of images) {
306
- let fileData;
307
- let fileInfo;
308
- if (item.type === 'ImageURL') {
309
- fileData = await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
374
+ const resolveFileData = async (item) => {
375
+ if (item.type === 'ImageURL') {
376
+ return await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
377
+ }
378
+ if (item.type === 'ImageFile') {
379
+ return readFileSync(item.value, 'base64');
380
+ }
381
+ const value = item.value;
382
+ if (typeof value === 'string') {
383
+ if (value.startsWith('https://') || value.startsWith('http://')) {
384
+ return await axios.get(value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
310
385
  }
311
- else if (item.type === 'Image') {
312
- if (typeof item.value === 'string' && (item.value.startsWith('https://') || item.value.startsWith('http://'))) {
313
- fileData = await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
314
- }
315
- else if (typeof item.value === 'string' && item.value.startsWith('file://')) {
316
- const localFilePath = item.value.replace('file://', '');
317
- fileData = readFileSync(localFilePath, 'base64');
318
- }
319
- else if (typeof item.value === 'string' && item.value.startsWith('base64://')) {
320
- fileData = item.value.replace('base64://', '');
321
- }
322
- else if (Buffer.isBuffer(item.value)) {
323
- fileData = item.value.toString('base64');
324
- }
325
- else if (typeof item.value === 'string') {
326
- fileData = item.value;
327
- }
386
+ if (value.startsWith('buffer://')) {
387
+ return value.replace('buffer://', '');
328
388
  }
329
- else if (item.type === 'ImageFile') {
330
- fileData = readFileSync(item.value, 'base64');
389
+ if (value.startsWith('file://')) {
390
+ return readFileSync(value.replace('file://', ''), 'base64');
331
391
  }
332
- if (fileData) {
333
- fileInfo = await uploadMedia({ file_type: 1, file_data: fileData }).then(res => res?.file_info);
392
+ if (value.startsWith('base64://')) {
393
+ return value.replace('base64://', '');
334
394
  }
335
- if (fileInfo) {
336
- return fileInfo;
395
+ return value;
396
+ }
397
+ if (Buffer.isBuffer(value)) {
398
+ return value.toString('base64');
399
+ }
400
+ return undefined;
401
+ };
402
+ const resolveMediaUrl = async (media, uploadMedia) => {
403
+ for (const item of media) {
404
+ const fileData = await resolveFileData(item);
405
+ if (fileData) {
406
+ const fileInfo = await uploadMedia({ file_type: MEDIA_FILE_TYPE[item.type] ?? 1, file_data: fileData }).then(res => res?.file_info);
407
+ if (fileInfo) {
408
+ return fileInfo;
409
+ }
337
410
  }
338
411
  }
339
412
  return undefined;
340
413
  };
414
+ const stripMediaPlaceholders = (text) => text
415
+ .replace(/\[附件[^\]]*\]/g, '')
416
+ .replace(/\[音频\]/g, '')
417
+ .replace(/\[视频\]/g, '')
418
+ .replace(/\n{2,}/g, '\n')
419
+ .trim();
341
420
  const flattenMdToText = (content, val) => {
342
421
  const mdItems = val.filter(item => item.type === 'Markdown');
343
422
  const btnItems = val.filter(item => item.type === 'BT.group');
@@ -357,19 +436,19 @@ const flattenMdToText = (content, val) => {
357
436
  .join('\n')
358
437
  .replace(/^[^\S\n\r]+|[^\S\n\r]+$/g, '');
359
438
  };
360
- const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMessage, label) => {
439
+ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMessage, label, options) => {
361
440
  const config = getQQBotConfig();
362
441
  const mdToText = config.markdownToText === true;
363
- const images = filterImages(val);
364
- if (images.length > 0) {
365
- const url = await resolveRichMediaUrl(images, uploadMedia);
366
- if (!url) {
367
- return [createResult(ResultCode.Fail, '图片上传失败', null)];
442
+ const media = filterMedia(val);
443
+ if (media.length > 0) {
444
+ const fileInfo = await resolveMediaUrl(media, uploadMedia);
445
+ if (!fileInfo) {
446
+ return [createResult(ResultCode.Fail, '媒体上传失败', null)];
368
447
  }
369
- const imgContent = flattenMdToText(content, val);
448
+ const mediaContent = stripMediaPlaceholders(flattenMdToText(content, val));
370
449
  const res = await sendMessage({
371
- content: imgContent,
372
- media: { file_info: url },
450
+ content: mediaContent,
451
+ media: { file_info: fileInfo },
373
452
  msg_type: 7,
374
453
  ...baseParams
375
454
  });
@@ -392,7 +471,7 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
392
471
  if (mdParams.markdown?.content && content) {
393
472
  mdParams.markdown.content = content + '\n' + mdParams.markdown.content;
394
473
  }
395
- const res = await sendMessage({ content, msg_type: 2, ...mdParams, ...baseParams });
474
+ const res = await sendMessage({ content, msg_type: 2, ...mdParams, ...baseParams, force_verify_image_resource: options?.forceVerifyImageResource });
396
475
  return [createResult(ResultCode.Ok, label, { id: res.id })];
397
476
  }
398
477
  const arkParams = buildArkParams(val);
@@ -473,21 +552,21 @@ const sendGuildMessage = async (content, val, baseParams, sendMessage, label) =>
473
552
  }
474
553
  return [];
475
554
  };
476
- const GROUP_AT_MESSAGE_CREATE = async (client, event, val) => {
555
+ const GROUP_AT_MESSAGE_CREATE = async (client, event, val, options) => {
477
556
  const baseParams = buildBaseParams(event._tag, event.MessageId, 'INTERACTION_CREATE_GROUP');
478
557
  const content = extractContent(val, 'group');
479
558
  try {
480
- return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByGroup(event.ChannelId, data), data => client.groupOpenMessages(event.ChannelId, data), 'client.groupOpenMessages');
559
+ return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByGroup(event.ChannelId, data), data => client.groupOpenMessages(event.ChannelId, data), 'client.groupOpenMessages', options);
481
560
  }
482
561
  catch (err) {
483
562
  return [createResult(ResultCode.Fail, err?.response?.data ?? err?.message ?? err, null)];
484
563
  }
485
564
  };
486
- const C2C_MESSAGE_CREATE = async (client, event, val) => {
565
+ const C2C_MESSAGE_CREATE = async (client, event, val, options) => {
487
566
  const baseParams = buildBaseParams(event._tag, event.MessageId, 'INTERACTION_CREATE_C2C');
488
567
  const content = extractContent(val, 'group');
489
568
  try {
490
- return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByUser(event.UserId, data), data => client.usersOpenMessages(event.UserId, data), 'client.usersOpenMessages');
569
+ return await sendOpenApiMessage(content, val, baseParams, data => client.postRichMediaByUser(event.UserId, data), data => client.usersOpenMessages(event.UserId, data), 'client.usersOpenMessages', options);
491
570
  }
492
571
  catch (err) {
493
572
  return [createResult(ResultCode.Fail, err?.response?.data ?? err?.message ?? err, null)];