@qihoo/tuitui-openclaw-channel 1.0.6 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
package/src/channel.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  deleteAccountFromConfigSection,
13
13
  buildChannelConfigSchema,
14
14
  } from 'openclaw/plugin-sdk';
15
- import type { TuiTuiInboundMessage } from './types';
15
+ import type { TuiTuiInboundMessage, TuiTuiOutboundDeliverOptions } from './types';
16
16
  import {
17
17
  CHANNEL_ID,
18
18
  CHANNEL_NAME,
@@ -48,7 +48,58 @@ const isConfigured = (account: any)=> Boolean(
48
48
  );
49
49
 
50
50
  const isToGroup = (chatId: string) => /^\d+$/.test(chatId);
51
-
51
+ const arrLowerCaseTrim = (arr: any[]) => arr.filter((v: any) => !!v).map((v: any) => String(v).toLowerCase().trim())
52
+ const configSchema = buildChannelConfigSchema(
53
+ z
54
+ .object({
55
+ enabled: z.boolean().optional().describe('开启或关闭'),
56
+ appId: z.string().min(1).describe('推推机器人身份 AppId(你可以推推搜索【推推机器人助手】,和它聊天自助申请推推机器人)'),
57
+ appSecret: z.string().min(1).describe('推推机器人密钥 Secret'),
58
+ // 私聊策略(默认 pairing)
59
+ dmPolicy: z
60
+ .enum(['pairing', 'allowlist', 'open', 'disabled'])
61
+ .optional()
62
+ .default('pairing')
63
+ .describe('私聊策略:pairing=配对(默认);allowlist=白名单;open=允许所有(不安全);disabled=禁用私聊',),
64
+ // 私聊允许列表(当 dmPolicy=allowlist 生效;pairing 下也可显式允许)
65
+ allowFrom: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe('私聊白名单(dmPolicy=allowlist 时生效;pairing 下可用于显式放行用户)'),
69
+
70
+ // 群组策略
71
+ groupPolicy: z
72
+ .enum(['allowlist', 'disabled'])
73
+ .default('allowlist')
74
+ .describe('群聊策略:allowlist=白名单;disabled=禁用群聊',),
75
+ // 仅在 allowlist 生效的群组 ID 列表
76
+ groupAllowFrom: z
77
+ .array(z.string())
78
+ .optional()
79
+ .describe('群组白名单(仅在 groupPolicy=allowlist 生效)'),
80
+ // 每个群组的覆盖配置
81
+ /*
82
+ groups: z
83
+ .record(
84
+ z.string(),
85
+ z
86
+ .object({
87
+ requireMention: z
88
+ .boolean()
89
+ .optional()
90
+ .describe('该群组是否需要 @ 机器人才触发 Agent(默认 true)'),
91
+ shouldReply: z
92
+ .boolean()
93
+ .optional()
94
+ .describe('该群组是否允许 Agent 回复(默认 true)'),
95
+ })
96
+ .passthrough(),
97
+ )
98
+ .optional()
99
+ .describe('群组级覆盖配置'),
100
+ */
101
+ }) as any,
102
+ );
52
103
  export function createTuiTuiChannelPlugin(apiRuntime: any) {
53
104
  return {
54
105
  id: CHANNEL_ID,
@@ -77,57 +128,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
77
128
 
78
129
  reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
79
130
 
80
- configSchema: buildChannelConfigSchema(
81
- z
82
- .object({
83
- enabled: z.boolean().optional().describe('开启或关闭'),
84
- appId: z.string().min(1).describe('推推机器人身份 AppId(你可以推推搜索【推推机器人助手】,和它聊天自助申请推推机器人)'),
85
- appSecret: z.string().min(1).describe('推推机器人密钥 Secret'),
86
- // 私聊策略(默认 pairing)
87
- dmPolicy: z
88
- .enum(['pairing', 'allowlist', 'open', 'disabled'])
89
- .optional()
90
- .default('pairing')
91
- .describe('私聊策略:pairing=配对(默认);allowlist=白名单;open=允许所有(不安全);disabled=禁用私聊',),
92
- // 私聊允许列表(当 dmPolicy=allowlist 生效;pairing 下也可显式允许)
93
- allowFrom: z
94
- .array(z.string())
95
- .optional()
96
- .describe('私聊白名单(dmPolicy=allowlist 时生效;pairing 下可用于显式放行用户)'),
97
-
98
- // 群组策略
99
- groupPolicy: z
100
- .enum(['allowlist', 'disabled'])
101
- .default('allowlist')
102
- .describe('群聊策略:allowlist=白名单;disabled=禁用群聊',),
103
- // 仅在 allowlist 生效的群组 ID 列表
104
- groupAllowFrom: z
105
- .array(z.string())
106
- .optional()
107
- .describe('群组白名单(仅在 groupPolicy=allowlist 生效)'),
108
- // 每个群组的覆盖配置
109
- /*
110
- groups: z
111
- .record(
112
- z.string(),
113
- z
114
- .object({
115
- requireMention: z
116
- .boolean()
117
- .optional()
118
- .describe('该群组是否需要 @ 机器人才触发 Agent(默认 true)'),
119
- shouldReply: z
120
- .boolean()
121
- .optional()
122
- .describe('该群组是否允许 Agent 回复(默认 true)'),
123
- })
124
- .passthrough(),
125
- )
126
- .optional()
127
- .describe('群组级覆盖配置'),
128
- */
129
- }) as any,
130
- ),
131
+ configSchema,
131
132
 
132
133
  config: {
133
134
  listAccountIds: (cfg: any) => {
@@ -273,7 +274,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
273
274
  return { channel: CHANNEL_ID, messageId: `tuitui-text-${Date.now()}`, chatId };
274
275
  },
275
276
 
276
- sendCustom: async ({ cfg, to, payload, accountId, account, chatType, groupId, log }: any) => {
277
+ sendCustom: async ({ cfg, to, payload, accountId, account, chatType, groupId }: any) => {
277
278
  account = account || resolveAccount(cfg, accountId);
278
279
  checkAccount(account, 'send custom message');
279
280
 
@@ -292,13 +293,13 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
292
293
  return { channel: CHANNEL_ID, messageId: `tuitui-page-${Date.now()}`, chatId };
293
294
  },
294
295
 
295
- sendMedia: async ({ cfg, to, mediaUrl, accountId, account, log }: any) => {
296
+ sendMedia: async ({ cfg, to, mediaUrl, accountId, account }: any) => {
296
297
  account = account || resolveAccount(cfg, accountId);
297
298
  checkAccount(account, 'send media');
298
299
 
299
300
  const chatId = String(to || '').trim();
300
301
  // Determine if this is a group message based on 'to' being all digits (group) or not (direct)
301
- await sendMediaMsg(account, chatId, isToGroup(chatId), mediaUrl);
302
+ await sendMediaMsg(account, chatId, isToGroup(chatId), mediaUrl, 'tuitui.send.media');
302
303
 
303
304
  return { channel: CHANNEL_ID, messageId: `tuitui-media-${Date.now()}`, chatId };
304
305
  },
@@ -386,8 +387,8 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
386
387
  const msg = json.body as TuiTuiInboundMessage;
387
388
  const msgData = msg.data;
388
389
  let userAccount: string | undefined = msg.user_account;
389
- let userUid: string | undefined = msg.uid;
390
- let userName: string | undefined = msg.user_name;
390
+ let msgUid: string | undefined = msg.uid;
391
+ let msgUname: string | undefined = msg.user_name;
391
392
  let chatType: 'direct' | 'group';
392
393
  const chatTypeIsDirect = wsEvent === 'single_chat';
393
394
  const chatTypeIsGroup = wsEvent === 'group_chat';
@@ -404,9 +405,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
404
405
  chatType = 'direct';
405
406
  text = buildMessageBody(msgData);
406
407
 
407
- log?.debug?.(
408
- `[${CHANNEL_ID}] inbound single_chat user_account=${String(userAccount)} uid=${String(userUid)} user_name=${String(msg.user_name)}`,
409
- );
408
+ log?.debug?.(`[${CHANNEL_ID}] inbound single_chat user_account=${userAccount} uid=${msgUid} user_name=${msgUname}`);
410
409
 
411
410
  const msgType = msgData.msg_type;
412
411
  // Extract media URLs for image/voice/file messages
@@ -423,7 +422,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
423
422
  replyToId = msgData.ref.msgid;
424
423
  }
425
424
 
426
- if (!userAccount && !userUid) {
425
+ if (!userAccount && !msgUid) {
427
426
  log?.info?.(`[${CHANNEL_ID}] Missing user_account or uid in single_chat event`);
428
427
  return;
429
428
  }
@@ -433,41 +432,24 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
433
432
  groupId = msgData.group_id;
434
433
  groupName = msgData.group_name;
435
434
 
436
- log?.debug?.(
437
- `[${CHANNEL_ID}] inbound group_chat user_account=${String(userAccount)} uid=${String(userUid)} user_name=${String(msg.user_name)} group_id=${String(groupId)}`,
438
- );
435
+ log?.debug?.(`[${CHANNEL_ID}] inbound group_chat user_account=${userAccount} uid=${msgUid} user_name=${msgUname} group_id=${groupId}`);
439
436
 
440
437
  // Group policy gating and @mention requirements
441
438
  const groupPolicy = String(account.groupPolicy ?? "allowlist").toLowerCase();
442
439
  const groupAllowFromRaw = Array.isArray(account.groupAllowFrom) ? account.groupAllowFrom : [];
443
- const normalizedGroupAllowFrom = groupAllowFromRaw
444
- .filter((v: unknown) => v != null)
445
- .map((v: unknown) => String(v).trim())
446
- .filter(Boolean);
447
- log?.debug?.(
448
- `[${CHANNEL_ID}] groupPolicy=${groupPolicy} groupId=${String(groupId)} groupAllowFrom=${JSON.stringify(normalizedGroupAllowFrom)} at_me=${JSON.stringify(msgData.at_me)} at=${JSON.stringify(msgData.at)}`,
449
- );
450
-
451
- if (groupPolicy === 'disabled') {
452
- log?.info?.('Groups disabled');
453
- return;
454
- }
455
-
440
+ const normalizedGroupAllowFrom = arrLowerCaseTrim(groupAllowFromRaw);
441
+ log?.debug?.(`[${CHANNEL_ID}] groupPolicy=${groupPolicy} groupId=${groupId} groupAllowFrom=${JSON.stringify(normalizedGroupAllowFrom)} at_me=${JSON.stringify(msgData.at_me)} at=${JSON.stringify(msgData.at)}`);
456
442
 
443
+ if (groupPolicy === 'disabled') return log?.info?.('Groups disabled');
444
+
457
445
  // 群消息处理策略
458
446
  const groupCfg = account.groups?.[String(groupId)];
459
447
 
460
448
  // @机器人触发策略
461
- let requireMention = true;
462
- if (groupCfg && typeof groupCfg.requireMention === 'boolean') {
463
- requireMention = groupCfg.requireMention;
464
- }
449
+ const requireMention = typeof groupCfg?.requireMention === 'boolean' ? groupCfg.requireMention : true;
465
450
 
466
451
  // 是否需要回复
467
- let shouldReply = true;
468
- if (groupCfg !== undefined && typeof groupCfg.shouldReply === 'boolean') {
469
- shouldReply = groupCfg.shouldReply;
470
- }
452
+ let shouldReply = typeof groupCfg?.shouldReply === 'boolean' ? groupCfg.shouldReply : true;
471
453
 
472
454
  if (!userAccount || !groupId) {
473
455
  log?.info?.(`[${CHANNEL_ID}] Missing user_account or group_id in group_chat event`);
@@ -515,14 +497,10 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
515
497
  // DM access gating (pairing/allowlist/open/disabled)
516
498
  const dmPolicy = String(account.dmPolicy ?? 'pairing').toLowerCase();
517
499
  const configuredAllowFrom = Array.isArray(account.allowFrom) ? account.allowFrom : [];
518
- const normalizedAllowFrom = configuredAllowFrom
519
- .filter((v: unknown) => !!v)
520
- .map((v: unknown) => String(v).toLowerCase().trim());
500
+ const normalizedAllowFrom = arrLowerCaseTrim(configuredAllowFrom);
521
501
  // 只使用 userAccount 作为匹配依据,因为用户希望 allowFrom 匹配 user_account
522
502
  const senderForPolicy = userAccount ? String(userAccount).toLowerCase().trim() : '';
523
- log?.debug?.(
524
- `[${CHANNEL_ID}] dmPolicy=${dmPolicy} userAccount=${String(userAccount)} userUid=${String(userUid)} senderForPolicy=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`,
525
- );
503
+ log?.debug?.(`[${CHANNEL_ID}] dmPolicy=${dmPolicy} userAccount=${userAccount} msgUid=${msgUid} senderForPolicy=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`);
526
504
 
527
505
  if (chatTypeIsDirect) {
528
506
  if (dmPolicy === 'disabled') {
@@ -533,20 +511,14 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
533
511
  if (dmPolicy !== 'open') {
534
512
  // Merge pairing-store entries unless policy is allowlist-only
535
513
  let storeAllowFrom: string[] = [];
536
- try {
537
- if (dmPolicy !== 'allowlist') {
538
- const entries = await apiRuntime?.channel?.pairing?.readAllowFromStore?.({
514
+ if (dmPolicy !== 'allowlist') {
515
+ try {
516
+ const res = await apiRuntime?.channel?.pairing?.readAllowFromStore?.({
539
517
  channel: CHANNEL_ID,
540
518
  accountId: account.accountId,
541
519
  });
542
- if (Array.isArray(entries)) {
543
- storeAllowFrom = entries
544
- .filter((v: unknown) => !!v)
545
- .map((v: unknown) => String(v).toLowerCase().trim());
546
- }
547
- }
548
- } catch {
549
- storeAllowFrom = [];
520
+ if (Array.isArray(res)) storeAllowFrom = arrLowerCaseTrim(res);
521
+ } catch {}
550
522
  }
551
523
 
552
524
  // 只检查 userAccount 是否在 allowFrom 或 storeAllowFrom 中
@@ -556,9 +528,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
556
528
  if (!isAllowed) {
557
529
  if (dmPolicy === 'pairing') {
558
530
  try {
559
- log?.debug?.(
560
- `[${CHANNEL_ID}] pairing flow: checking if pairing request exists for sender=${senderForPolicy}`,
561
- );
531
+ log?.debug?.(`[${CHANNEL_ID}] pairing flow: checking if pairing request exists for sender=${senderForPolicy}`);
562
532
  const req = await apiRuntime?.channel?.pairing?.upsertPairingRequest?.({
563
533
  channel: CHANNEL_ID,
564
534
  accountId: account.accountId,
@@ -591,9 +561,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
591
561
  }
592
562
 
593
563
  // dmPolicy=allowlist and sender not allowed
594
- log?.warn?.(
595
- `[${CHANNEL_ID}] Blocked unauthorized sender (allowlist): sender=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`,
596
- );
564
+ log?.warn?.(`[${CHANNEL_ID}] Blocked unauthorized sender (allowlist): sender=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`);
597
565
  return;
598
566
  }
599
567
  }
@@ -603,22 +571,13 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
603
571
  await tuituiEmojiReaction(account, chatTypeIsGroup ? groupId : userAccount, chatTypeIsGroup, msgData.msgid, '收到');
604
572
 
605
573
  // Build MsgContext
606
- // 优先使用 userAccount,如果为空则降级使用 userUid
607
- const senderId = userAccount || userUid || 'unknown';
574
+ // 优先使用 userAccount,如果为空则降级使用 msgUid
575
+ const senderId = userAccount || msgUid || 'unknown';
608
576
  // 确保 accountId 有有效值,优先使用 account.accountId,其次使用 ctx 中的 accountId,最后使用 DEFAULT_ACCOUNT_ID
609
577
 
610
578
  const effectiveAccountId = String(account.accountId || accountId || DEFAULT_ACCOUNT_ID || 'default');
611
- const sessionKey = chatTypeIsGroup
612
- ? `${CHANNEL_ID}:${effectiveAccountId}:${groupId}`
613
- : `${CHANNEL_ID}:${effectiveAccountId}:${senderId}`;
614
- console.log(`[${CHANNEL_ID}] effectiveAccountId infos:
615
- account.accountId : ${account.accountId},
616
- accountId: ${accountId},
617
- DEFAULT_ACCOUNT_ID: ${DEFAULT_ACCOUNT_ID},
618
- chatType: ${chatType},
619
- sessionKey: ${sessionKey},
620
- effectiveAccountId: ${effectiveAccountId},
621
- `);
579
+ const sessionKey = `${CHANNEL_ID}:${effectiveAccountId}:${chatTypeIsGroup ? groupId : userAccount}`;
580
+ console.log(`[${CHANNEL_ID}] effectiveAccountId infos: account.accountId : ${account.accountId}, accountId: ${accountId}, DEFAULT_ACCOUNT_ID: ${DEFAULT_ACCOUNT_ID}, chatType: ${chatType}, sessionKey: ${sessionKey}, effectiveAccountId: ${effectiveAccountId}`);
622
581
 
623
582
  const msgCtx: any = {
624
583
  Body: text || ' ',
@@ -632,7 +591,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
632
591
  ChatType: chatType,
633
592
  Surface: CHANNEL_ID,
634
593
  Provider: CHANNEL_ID,
635
- SenderName: msg.user_name || String(senderId),
594
+ SenderName: msgUname || String(senderId),
636
595
  };
637
596
 
638
597
  // Add group-specific fields
@@ -652,23 +611,11 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
652
611
  log?.error?.(`[${CHANNEL_ID}] TuiTuiRuntime error,未能发送回复。`);
653
612
  return;
654
613
  }
655
- const currentCfg = await apiRuntime.config.loadConfig();
656
614
  await apiRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
657
615
  ctx: msgCtx,
658
- cfg: currentCfg,
616
+ cfg: await apiRuntime.config.loadConfig(),
659
617
  dispatcherOptions: {
660
- deliver: async (payload: {
661
- text?: string;
662
- body?: string;
663
- mediaUrl?: string;
664
- mediaUrls?: string[];
665
- custom?: {
666
- msgtype: string;
667
- tousers?: string[];
668
- togroups?: string[];
669
- [key: string]: any;
670
- };
671
- }) => {
618
+ deliver: async (payload: TuiTuiOutboundDeliverOptions) => {
672
619
 
673
620
  // 如果设置了 suppressReply 标志,则不发送回复
674
621
  if (suppressReply) {
@@ -692,7 +639,8 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
692
639
  // Handle media messages
693
640
  const mediaUrl = payload.mediaUrl || payload.mediaUrls?.[0];
694
641
  if (mediaUrl) {
695
- await sendMediaMsg(account, chatTarget, chatTypeIsGroup, mediaUrl, 'tuitui.deliver.media');
642
+ const at = chatTypeIsGroup && userAccount ? [userAccount] : [];
643
+ await sendMediaMsg(account, chatTarget, chatTypeIsGroup, mediaUrl, 'tuitui.deliver.media', at);
696
644
  }
697
645
  return;
698
646
  }
@@ -700,19 +648,20 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
700
648
  const replyText = payload?.text || payload?.body;
701
649
  if (replyText) {
702
650
  // at 使用 userAccount(群聊 @ 用户)
703
- const atList = chatTypeIsGroup && userAccount ? [userAccount] : [];
651
+ const at = chatTypeIsGroup && userAccount ? [userAccount] : [];
704
652
  // 群消息回复时不再使用 reference_msgid 避免并发时引用错乱,依靠 @ 来提醒用户
705
- await sendTextMsg(account, chatTarget, chatTypeIsGroup, replyText, 'tuitui.deliver.text', atList);
653
+ await sendTextMsg(account, chatTarget, chatTypeIsGroup, replyText, 'tuitui.deliver.text', at);
706
654
  }
707
655
  },
708
656
  onReplyStart: () => {
709
- log?.info?.(`[${CHANNEL_ID}] Agent reply started for ${userAccount ?? userUid}`);
657
+ log?.info?.(`[${CHANNEL_ID}] Agent reply started for ${userAccount ?? msgUid}`);
710
658
  },
711
659
  },
712
660
  });
713
661
  });
714
662
 
715
663
  const onErrOrClose = () => {
664
+ if (ws && ws.readyState !== WebSocket.CLOSED) ws.close(); // 关闭超时链接,防止产生多个
716
665
  _setStatus({ running: false, connected: false });
717
666
  _sendMsg = defSendMsg;
718
667
  if (!abortSignal?.aborted) {
package/src/types.ts CHANGED
@@ -115,3 +115,16 @@ export interface TuiTuiGroupEmojiReactionTarget {
115
115
  group?: string;
116
116
  msgid?: string;
117
117
  }
118
+
119
+ export interface TuiTuiOutboundDeliverOptions {
120
+ text?: string;
121
+ body?: string;
122
+ mediaUrl?: string;
123
+ mediaUrls?: string[];
124
+ custom?: {
125
+ msgtype: string;
126
+ tousers?: string[];
127
+ togroups?: string[];
128
+ [key: string]: any;
129
+ };
130
+ }
package/src/utils.ts CHANGED
@@ -225,10 +225,8 @@ export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'i
225
225
  export function buildMessageBody(data: TuiTuiMessageData): string {
226
226
  const parts: string[] = [];
227
227
 
228
- const txt = data.text?.trim();
229
- const pushTxt = () => txt && parts.push(txt);
230
- const pushImgs = () => {
231
- const imgs = data.images;
228
+ const pushTxt = () => parts.push(data.text || '');
229
+ const pushImgs = (imgs: string[] | undefined, parts: string[]) => {
232
230
  if (!imgs) return;
233
231
  const imgLen = imgs?.length || 0;
234
232
  if (imgLen === 0) return;
@@ -246,10 +244,10 @@ export function buildMessageBody(data: TuiTuiMessageData): string {
246
244
  break;
247
245
  case 'mixed':
248
246
  pushTxt();
249
- pushImgs();
247
+ pushImgs(data.images, parts);
250
248
  break;
251
249
  case 'image':
252
- pushImgs();
250
+ pushImgs(data.images, parts);
253
251
  break;
254
252
  case 'voice':
255
253
  if (data.voice) parts.push(`[语音] ${data.voice}`);
@@ -262,14 +260,16 @@ export function buildMessageBody(data: TuiTuiMessageData): string {
262
260
  // Handle reference/reply
263
261
  const { ref } = data;
264
262
  if (ref && ref.is_me) {
265
- const { msg_type, text, images } = ref;
263
+ const { msg_type } = ref;
266
264
  let refContent = `[${msg_type}]`;
267
265
  switch (msg_type) {
268
266
  case 'text':
269
- refContent = text || '';
267
+ refContent = ref.text || '';
270
268
  break;
271
269
  case 'image':
272
- refContent = images?.length ? `[图片]` : '[图片]';
270
+ const refParts: string[] = [];
271
+ pushImgs(ref.images, refParts);
272
+ refContent = refParts.join("\n") || '[图片]';
273
273
  break;
274
274
  }
275
275
  parts.unshift(`[引用机器人消息]\n> ${refContent}`);
@@ -367,6 +367,7 @@ export async function sendMediaMsg(
367
367
  isGroup: boolean,
368
368
  mediaUrl: string,
369
369
  auditCtx: string = 'tuitui.send.media',
370
+ at?: string[],
370
371
  ): Promise<void> {
371
372
  if (!target) return console.error(`[${CHANNEL_ID}] sendMediaMsg Error ${auditCtx}: Missing "target"`);
372
373
  // Check if mediaUrl looks like an image
@@ -384,7 +385,7 @@ export async function sendMediaMsg(
384
385
  isImage
385
386
  ? { ...targets, msgtype: 'image', image: { media_id } }
386
387
  : { ...targets, msgtype: 'attachment', attachment: { media_id } };
387
-
388
+ if (at && at.length > 0) msg.at = at;
388
389
  console.log(`[${CHANNEL_ID}] sendMediaMsg ${auditCtx} - `, msg);
389
390
 
390
391
  await postTuituiMsg(account, msg, auditCtx);