@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 +1 -1
- package/src/channel.ts +90 -141
- package/src/types.ts +13 -0
- package/src/utils.ts +11 -10
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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
|
|
390
|
-
let
|
|
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 && !
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
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(
|
|
543
|
-
|
|
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,如果为空则降级使用
|
|
607
|
-
const senderId = userAccount ||
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
651
|
+
const at = chatTypeIsGroup && userAccount ? [userAccount] : [];
|
|
704
652
|
// 群消息回复时不再使用 reference_msgid 避免并发时引用错乱,依靠 @ 来提醒用户
|
|
705
|
-
await sendTextMsg(account, chatTarget, chatTypeIsGroup, replyText, 'tuitui.deliver.text',
|
|
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 ??
|
|
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
|
|
229
|
-
const
|
|
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
|
|
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
|
-
|
|
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);
|