@qihoo/tuitui-openclaw-channel 1.0.6 → 1.0.7

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/channel.ts +82 -116
  3. package/src/utils.ts +11 -10
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.7",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
package/src/channel.ts CHANGED
@@ -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, payload, 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,27 +432,16 @@ 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
 
@@ -515,14 +503,10 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
515
503
  // DM access gating (pairing/allowlist/open/disabled)
516
504
  const dmPolicy = String(account.dmPolicy ?? 'pairing').toLowerCase();
517
505
  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());
506
+ const normalizedAllowFrom = arrLowerCaseTrim(configuredAllowFrom);
521
507
  // 只使用 userAccount 作为匹配依据,因为用户希望 allowFrom 匹配 user_account
522
508
  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
- );
509
+ log?.debug?.(`[${CHANNEL_ID}] dmPolicy=${dmPolicy} userAccount=${userAccount} msgUid=${msgUid} senderForPolicy=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`);
526
510
 
527
511
  if (chatTypeIsDirect) {
528
512
  if (dmPolicy === 'disabled') {
@@ -533,20 +517,14 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
533
517
  if (dmPolicy !== 'open') {
534
518
  // Merge pairing-store entries unless policy is allowlist-only
535
519
  let storeAllowFrom: string[] = [];
536
- try {
537
- if (dmPolicy !== 'allowlist') {
538
- const entries = await apiRuntime?.channel?.pairing?.readAllowFromStore?.({
520
+ if (dmPolicy !== 'allowlist') {
521
+ try {
522
+ const res = await apiRuntime?.channel?.pairing?.readAllowFromStore?.({
539
523
  channel: CHANNEL_ID,
540
524
  accountId: account.accountId,
541
525
  });
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 = [];
526
+ if (Array.isArray(res)) storeAllowFrom = arrLowerCaseTrim(res);
527
+ } catch {}
550
528
  }
551
529
 
552
530
  // 只检查 userAccount 是否在 allowFrom 或 storeAllowFrom 中
@@ -556,9 +534,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
556
534
  if (!isAllowed) {
557
535
  if (dmPolicy === 'pairing') {
558
536
  try {
559
- log?.debug?.(
560
- `[${CHANNEL_ID}] pairing flow: checking if pairing request exists for sender=${senderForPolicy}`,
561
- );
537
+ log?.debug?.(`[${CHANNEL_ID}] pairing flow: checking if pairing request exists for sender=${senderForPolicy}`);
562
538
  const req = await apiRuntime?.channel?.pairing?.upsertPairingRequest?.({
563
539
  channel: CHANNEL_ID,
564
540
  accountId: account.accountId,
@@ -591,9 +567,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
591
567
  }
592
568
 
593
569
  // dmPolicy=allowlist and sender not allowed
594
- log?.warn?.(
595
- `[${CHANNEL_ID}] Blocked unauthorized sender (allowlist): sender=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`,
596
- );
570
+ log?.warn?.(`[${CHANNEL_ID}] Blocked unauthorized sender (allowlist): sender=${senderForPolicy} allowFrom=${JSON.stringify(normalizedAllowFrom)}`);
597
571
  return;
598
572
  }
599
573
  }
@@ -603,22 +577,13 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
603
577
  await tuituiEmojiReaction(account, chatTypeIsGroup ? groupId : userAccount, chatTypeIsGroup, msgData.msgid, '收到');
604
578
 
605
579
  // Build MsgContext
606
- // 优先使用 userAccount,如果为空则降级使用 userUid
607
- const senderId = userAccount || userUid || 'unknown';
580
+ // 优先使用 userAccount,如果为空则降级使用 msgUid
581
+ const senderId = userAccount || msgUid || 'unknown';
608
582
  // 确保 accountId 有有效值,优先使用 account.accountId,其次使用 ctx 中的 accountId,最后使用 DEFAULT_ACCOUNT_ID
609
583
 
610
584
  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
- `);
585
+ const sessionKey = `${CHANNEL_ID}:${effectiveAccountId}:${chatTypeIsGroup ? groupId : userAccount}`;
586
+ 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
587
 
623
588
  const msgCtx: any = {
624
589
  Body: text || ' ',
@@ -632,7 +597,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
632
597
  ChatType: chatType,
633
598
  Surface: CHANNEL_ID,
634
599
  Provider: CHANNEL_ID,
635
- SenderName: msg.user_name || String(senderId),
600
+ SenderName: msgUname || String(senderId),
636
601
  };
637
602
 
638
603
  // Add group-specific fields
@@ -692,7 +657,8 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
692
657
  // Handle media messages
693
658
  const mediaUrl = payload.mediaUrl || payload.mediaUrls?.[0];
694
659
  if (mediaUrl) {
695
- await sendMediaMsg(account, chatTarget, chatTypeIsGroup, mediaUrl, 'tuitui.deliver.media');
660
+ const at = chatTypeIsGroup ? [msgUname] : [];
661
+ await sendMediaMsg(account, chatTarget, chatTypeIsGroup, mediaUrl, 'tuitui.deliver.media', at);
696
662
  }
697
663
  return;
698
664
  }
@@ -706,7 +672,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
706
672
  }
707
673
  },
708
674
  onReplyStart: () => {
709
- log?.info?.(`[${CHANNEL_ID}] Agent reply started for ${userAccount ?? userUid}`);
675
+ log?.info?.(`[${CHANNEL_ID}] Agent reply started for ${userAccount ?? msgUid}`);
710
676
  },
711
677
  },
712
678
  });
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);