evolclaw 3.1.4 → 3.1.6
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/CHANGELOG.md +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
package/dist/channels/feishu.js
CHANGED
|
@@ -21,6 +21,7 @@ export class FeishuChannel {
|
|
|
21
21
|
enableRichContent;
|
|
22
22
|
// chatId → 该会话内仍 pending 的交互卡片 messageId 集合,用于作废
|
|
23
23
|
pendingCardsByChat = new Map();
|
|
24
|
+
cardMetaStore = new CardMetaStore();
|
|
24
25
|
constructor(config) {
|
|
25
26
|
this.config = config;
|
|
26
27
|
this.enableRichContent = config.enableRichContent ?? false; // 默认关闭
|
|
@@ -282,7 +283,8 @@ export class FeishuChannel {
|
|
|
282
283
|
else if (msg.message_type === 'merge_forward') {
|
|
283
284
|
const { text: mergedText, images: mergedImages } = await this.extractMergeForwardContent(msg.message_id, msg.chat_id);
|
|
284
285
|
if (mergedText) {
|
|
285
|
-
|
|
286
|
+
// 直接发送合并转发时,parent_id 指向自己,引用解析会把相同内容填入 quotedText 导致重复,丢弃
|
|
287
|
+
const finalContent = mergedText;
|
|
286
288
|
const allImages = [...quotedImages, ...mergedImages];
|
|
287
289
|
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
288
290
|
}
|
|
@@ -320,76 +322,46 @@ export class FeishuChannel {
|
|
|
320
322
|
const operatorId = data.operator?.open_id;
|
|
321
323
|
const chatId = data.context?.open_chat_id || data.open_chat_id;
|
|
322
324
|
const cardMessageId = data.open_message_id || data.context?.open_message_id;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
const formValues = action.form_value || {};
|
|
326
|
+
const decision = routeCardAction({ value, formValues, operatorId }, this.cardMetaStore);
|
|
327
|
+
switch (decision.kind) {
|
|
328
|
+
case 'ignore':
|
|
329
|
+
return;
|
|
330
|
+
case 'reject':
|
|
331
|
+
return { toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' } };
|
|
332
|
+
case 'expired':
|
|
333
|
+
return { toast: { type: 'warning', content: '⚠️ 卡片已失效,请重新发起' } };
|
|
334
|
+
case 'show-input':
|
|
335
|
+
return decision.card;
|
|
336
|
+
case 'command': {
|
|
337
|
+
logger.info(`[Feishu] CommandCard trigger: command=${decision.command}, operator=${operatorId}`);
|
|
338
|
+
if (this.messageHandler) {
|
|
339
|
+
// 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
|
|
340
|
+
// 由 ensureSession 从已有 session 中继承正确的 chatType
|
|
341
|
+
await this.messageHandler({
|
|
342
|
+
channelId: chatId,
|
|
343
|
+
content: decision.command,
|
|
344
|
+
peerId: operatorId,
|
|
345
|
+
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
346
|
+
source: 'card-trigger',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (chatId && cardMessageId)
|
|
350
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
351
|
+
this.cardMetaStore.markResolved(value._id);
|
|
352
|
+
this.cardMetaStore.cleanup(value._id);
|
|
353
|
+
return decision.card;
|
|
329
354
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
339
|
-
source: 'card-trigger',
|
|
340
|
-
});
|
|
355
|
+
case 'respond': {
|
|
356
|
+
logger.info(`[Feishu] Card action: id=${value._id}, action=${decision.response.action}`);
|
|
357
|
+
this.interactionCallback?.(decision.response);
|
|
358
|
+
if (chatId && cardMessageId)
|
|
359
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
360
|
+
this.cardMetaStore.markResolved(value._id);
|
|
361
|
+
this.cardMetaStore.cleanup(value._id);
|
|
362
|
+
return decision.card;
|
|
341
363
|
}
|
|
342
|
-
// 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
|
|
343
|
-
if (chatId && cardMessageId)
|
|
344
|
-
this.untrackPendingCard(chatId, cardMessageId);
|
|
345
|
-
const cardTitle = value._card_title || '操作';
|
|
346
|
-
const btnLabel = value._btn_label || value._command;
|
|
347
|
-
const cardBody = value._card_body || '';
|
|
348
|
-
return this.buildResolvedCard(cardTitle, { type: 'interaction.response', id: '', action: value._command, operatorId }, cardBody, btnLabel);
|
|
349
|
-
}
|
|
350
|
-
// ── ActionInteraction 分支 ──
|
|
351
|
-
const requestId = value._request_id;
|
|
352
|
-
if (!requestId) {
|
|
353
|
-
logger.debug('[Feishu] Card action without _request_id or _command, ignoring');
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
// initiator 校验
|
|
357
|
-
if (value._initiator && operatorId && operatorId !== value._initiator) {
|
|
358
|
-
return {
|
|
359
|
-
toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
|
|
360
|
-
};
|
|
361
364
|
}
|
|
362
|
-
// Legacy field change (non-form select_static with _field_key): ignore silently
|
|
363
|
-
if (value._field_key) {
|
|
364
|
-
logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
// Form submit: `action.form_value` contains all field values from form container
|
|
368
|
-
const formValues = action.form_value || {};
|
|
369
|
-
const response = {
|
|
370
|
-
type: 'interaction.response',
|
|
371
|
-
id: requestId,
|
|
372
|
-
action: value._action || 'submit',
|
|
373
|
-
values: { ...formValues, ...value },
|
|
374
|
-
operatorId,
|
|
375
|
-
};
|
|
376
|
-
// Remove internal fields from values
|
|
377
|
-
delete response.values._request_id;
|
|
378
|
-
delete response.values._action;
|
|
379
|
-
delete response.values._initiator;
|
|
380
|
-
delete response.values._card_title;
|
|
381
|
-
const cardBody = value._card_body || '';
|
|
382
|
-
delete response.values._card_body;
|
|
383
|
-
const btnLabel = value._btn_label || '';
|
|
384
|
-
delete response.values._btn_label;
|
|
385
|
-
logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
|
|
386
|
-
this.interactionCallback?.(response);
|
|
387
|
-
// 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
|
|
388
|
-
if (chatId && cardMessageId)
|
|
389
|
-
this.untrackPendingCard(chatId, cardMessageId);
|
|
390
|
-
// Return updated card (buttons disabled + result shown)
|
|
391
|
-
const cardTitle = value._card_title || '操作';
|
|
392
|
-
return this.buildResolvedCard(cardTitle, response, cardBody, btnLabel);
|
|
393
365
|
}
|
|
394
366
|
catch (err) {
|
|
395
367
|
logger.error('[Feishu] Failed to handle card action:', err);
|
|
@@ -542,10 +514,15 @@ export class FeishuChannel {
|
|
|
542
514
|
if (options.replyInThread) {
|
|
543
515
|
replyData.reply_in_thread = true;
|
|
544
516
|
}
|
|
545
|
-
await this.client.im.message.reply({
|
|
517
|
+
const replyRes = await this.client.im.message.reply({
|
|
546
518
|
path: { message_id: options.replyToMessageId },
|
|
547
519
|
data: replyData
|
|
548
520
|
});
|
|
521
|
+
if (options.replyInThread && options.onThreadCreated) {
|
|
522
|
+
const newThreadId = replyRes?.data?.thread_id;
|
|
523
|
+
if (newThreadId)
|
|
524
|
+
options.onThreadCreated(newThreadId);
|
|
525
|
+
}
|
|
549
526
|
}
|
|
550
527
|
else {
|
|
551
528
|
await this.client.im.message.create({
|
|
@@ -562,21 +539,22 @@ export class FeishuChannel {
|
|
|
562
539
|
}
|
|
563
540
|
catch (error) {
|
|
564
541
|
// 230011: 消息已被撤回,降级为普通消息重试
|
|
565
|
-
|
|
566
|
-
|
|
542
|
+
// 99992354: message_id 不存在(合成 ID 或已过期),降级为普通消息
|
|
543
|
+
const errCode = error.response?.data?.code;
|
|
544
|
+
if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
|
|
545
|
+
logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying without reply`);
|
|
567
546
|
return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
|
|
568
547
|
}
|
|
569
548
|
// 230025: 消息内容超长,截断后重试
|
|
570
|
-
if (
|
|
549
|
+
if (errCode === 230025) {
|
|
571
550
|
logger.warn(`[Feishu] Message too long (230025, ${content.length} chars), truncating`);
|
|
572
551
|
const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
|
|
573
552
|
return this.sendMessage(chatId, truncated, options);
|
|
574
553
|
}
|
|
575
554
|
const respData = error?.response?.data;
|
|
576
|
-
const code = respData?.code;
|
|
577
555
|
logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
|
|
578
556
|
// post 格式相关错误(400/230001):降级为纯文本重试
|
|
579
|
-
if (!options?.forceText && (error?.response?.status === 400 ||
|
|
557
|
+
if (!options?.forceText && (error?.response?.status === 400 || errCode === 230001)) {
|
|
580
558
|
logger.warn('[Feishu] Retrying as plain text (forceText)');
|
|
581
559
|
return this.sendMessage(chatId, content, { ...options, forceText: true });
|
|
582
560
|
}
|
|
@@ -639,9 +617,10 @@ export class FeishuChannel {
|
|
|
639
617
|
logger.info('[Feishu] File message sent successfully');
|
|
640
618
|
}
|
|
641
619
|
catch (error) {
|
|
642
|
-
// 230011:
|
|
643
|
-
|
|
644
|
-
|
|
620
|
+
// 230011/99992354: reply target invalid, retry without reply
|
|
621
|
+
const errCode = error.response?.data?.code;
|
|
622
|
+
if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
|
|
623
|
+
logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying file send without reply`);
|
|
645
624
|
return this.sendFile(chatId, filePath);
|
|
646
625
|
}
|
|
647
626
|
logger.error('[Feishu] Failed to send file:', error);
|
|
@@ -683,6 +662,7 @@ export class FeishuChannel {
|
|
|
683
662
|
logger.debug('[Feishu] Image message sent successfully');
|
|
684
663
|
}
|
|
685
664
|
catch (error) {
|
|
665
|
+
// 99992354: reply target invalid — image cannot easily retry, just log
|
|
686
666
|
logger.error('[Feishu] Failed to send image:', error);
|
|
687
667
|
throw error;
|
|
688
668
|
}
|
|
@@ -736,13 +716,18 @@ export class FeishuChannel {
|
|
|
736
716
|
// seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
|
|
737
717
|
if (this.seenThreads.size > 1000)
|
|
738
718
|
this.seenThreads.clear();
|
|
739
|
-
//
|
|
740
|
-
if (
|
|
719
|
+
// 重写文件,去掉过期条目(仅在有记录被清理时才写)
|
|
720
|
+
if (cleaned > 0 && this.config.seenMsgFile) {
|
|
741
721
|
try {
|
|
742
|
-
|
|
743
|
-
.
|
|
744
|
-
|
|
745
|
-
|
|
722
|
+
if (this.seenMessages.size === 0) {
|
|
723
|
+
fs.unlinkSync(this.config.seenMsgFile);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
const lines = [...this.seenMessages.entries()]
|
|
727
|
+
.map(([id, ts]) => JSON.stringify({ id, ts }))
|
|
728
|
+
.join('\n') + '\n';
|
|
729
|
+
fs.writeFileSync(this.config.seenMsgFile, lines);
|
|
730
|
+
}
|
|
746
731
|
}
|
|
747
732
|
catch { }
|
|
748
733
|
}
|
|
@@ -981,12 +966,13 @@ export class FeishuChannel {
|
|
|
981
966
|
if (!set || set.size === 0)
|
|
982
967
|
return;
|
|
983
968
|
const expiredCard = {
|
|
984
|
-
|
|
969
|
+
schema: '2.0',
|
|
970
|
+
config: { update_multi: true },
|
|
985
971
|
header: {
|
|
986
972
|
template: 'grey',
|
|
987
973
|
title: { tag: 'plain_text', content: '已过期' },
|
|
988
974
|
},
|
|
989
|
-
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
975
|
+
body: { elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }] },
|
|
990
976
|
};
|
|
991
977
|
const ids = Array.from(set);
|
|
992
978
|
this.pendingCardsByChat.delete(chatId);
|
|
@@ -1006,10 +992,8 @@ export class FeishuChannel {
|
|
|
1006
992
|
async sendInteraction(chatId, interaction, options) {
|
|
1007
993
|
if (!this.client)
|
|
1008
994
|
return false;
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return false;
|
|
1012
|
-
// 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
|
|
995
|
+
// 统一路径:schema 2.0 内联发送(im.message),不走 cardkit 实体
|
|
996
|
+
const card = buildCardV2(interaction);
|
|
1013
997
|
await this.invalidatePendingCards(chatId);
|
|
1014
998
|
try {
|
|
1015
999
|
let messageId;
|
|
@@ -1038,12 +1022,15 @@ export class FeishuChannel {
|
|
|
1038
1022
|
messageId = res?.data?.message_id;
|
|
1039
1023
|
}
|
|
1040
1024
|
logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
|
|
1041
|
-
if (messageId)
|
|
1025
|
+
if (messageId) {
|
|
1042
1026
|
this.trackPendingCard(chatId, messageId);
|
|
1027
|
+
this.cardMetaStore.set(interaction.id, {
|
|
1028
|
+
interaction, chatId, messageId, resolved: false,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1043
1031
|
return messageId || false;
|
|
1044
1032
|
}
|
|
1045
1033
|
catch (error) {
|
|
1046
|
-
// 飞书 SDK 错误可能在 response.data、message 或 error 本身
|
|
1047
1034
|
const respData = error?.response?.data;
|
|
1048
1035
|
const detail = respData
|
|
1049
1036
|
? JSON.stringify(respData)
|
|
@@ -1051,42 +1038,10 @@ export class FeishuChannel {
|
|
|
1051
1038
|
? error.message
|
|
1052
1039
|
: JSON.stringify(error, Object.getOwnPropertyNames(error));
|
|
1053
1040
|
logger.error(`[Feishu] Failed to send interaction card (id=${interaction.id}, replyTo=${options?.replyToMessageId || 'none'}): ${detail}`);
|
|
1054
|
-
|
|
1055
|
-
logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(buildInteractionCard(interaction))}`);
|
|
1041
|
+
logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(card)}`);
|
|
1056
1042
|
return false;
|
|
1057
1043
|
}
|
|
1058
1044
|
}
|
|
1059
|
-
buildResolvedCard(cardTitle, response, cardBody, btnLabel) {
|
|
1060
|
-
const action = response.action;
|
|
1061
|
-
const labelMap = {
|
|
1062
|
-
'allow': '✅ 已允许',
|
|
1063
|
-
'always': '🔓 已设为始终允许',
|
|
1064
|
-
'deny': '❌ 已拒绝',
|
|
1065
|
-
'cancel': '取消',
|
|
1066
|
-
};
|
|
1067
|
-
const statusText = labelMap[action] || (btnLabel ? `✅ ${btnLabel}` : `✅ ${action}`);
|
|
1068
|
-
const elements = [];
|
|
1069
|
-
if (cardBody) {
|
|
1070
|
-
elements.push({ tag: 'markdown', content: cardBody });
|
|
1071
|
-
}
|
|
1072
|
-
return {
|
|
1073
|
-
toast: {
|
|
1074
|
-
type: 'success',
|
|
1075
|
-
content: statusText,
|
|
1076
|
-
},
|
|
1077
|
-
card: {
|
|
1078
|
-
type: 'raw',
|
|
1079
|
-
data: {
|
|
1080
|
-
config: { wide_screen_mode: true, update_multi: true },
|
|
1081
|
-
header: {
|
|
1082
|
-
template: action === 'deny' ? 'red' : 'green',
|
|
1083
|
-
title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
|
|
1084
|
-
},
|
|
1085
|
-
elements,
|
|
1086
|
-
},
|
|
1087
|
-
},
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
1045
|
addAckReaction(messageId) {
|
|
1091
1046
|
if (!this.client)
|
|
1092
1047
|
return;
|
|
@@ -1098,104 +1053,245 @@ export class FeishuChannel {
|
|
|
1098
1053
|
}).catch(() => { });
|
|
1099
1054
|
}
|
|
1100
1055
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1056
|
+
export class CardMetaStore {
|
|
1057
|
+
map = new Map();
|
|
1058
|
+
set(id, entry) {
|
|
1059
|
+
this.map.set(id, entry);
|
|
1060
|
+
}
|
|
1061
|
+
get(id) {
|
|
1062
|
+
return this.map.get(id);
|
|
1063
|
+
}
|
|
1064
|
+
markResolved(id) {
|
|
1065
|
+
const entry = this.map.get(id);
|
|
1066
|
+
if (entry)
|
|
1067
|
+
entry.resolved = true;
|
|
1068
|
+
}
|
|
1069
|
+
markInputShown(id) {
|
|
1070
|
+
const entry = this.map.get(id);
|
|
1071
|
+
if (entry)
|
|
1072
|
+
entry.inputShown = true;
|
|
1106
1073
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1074
|
+
cleanup(id) {
|
|
1075
|
+
this.map.delete(id);
|
|
1109
1076
|
}
|
|
1110
|
-
return null;
|
|
1111
1077
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1078
|
+
// ── 统一卡片构建器(schema 2.0 内联)──
|
|
1079
|
+
/**
|
|
1080
|
+
* 唯一卡片构建器。输入协议层 InteractionRequest,输出 schema 2.0 内联卡片 JSON。
|
|
1081
|
+
* - command-card: 按钮 value 带 { _id, _command, _initiator }
|
|
1082
|
+
* - action: 按钮 value 带 { _id, _action, _initiator };checkers → form+checker;
|
|
1083
|
+
* allowCustomInput → 「手动输入」按钮(form 容器外)
|
|
1084
|
+
* @param opts.showInput 展开自定义输入框(用于 _show_input 回调返回整卡)
|
|
1085
|
+
*/
|
|
1086
|
+
export function buildCardV2(interaction, opts) {
|
|
1087
|
+
const { kind } = interaction;
|
|
1088
|
+
const id = interaction.id;
|
|
1089
|
+
const initiatorId = interaction.initiatorId;
|
|
1090
|
+
const title = kind.title;
|
|
1091
|
+
const body = kind.body;
|
|
1092
|
+
const formElements = [];
|
|
1093
|
+
// Body markdown
|
|
1094
|
+
if (body) {
|
|
1095
|
+
formElements.push({ tag: 'markdown', content: body, element_id: 'body_md' });
|
|
1096
|
+
}
|
|
1097
|
+
// Checkers (action only)
|
|
1098
|
+
if (kind.kind === 'action' && kind.checkers?.length) {
|
|
1099
|
+
kind.checkers.forEach((chk, idx) => {
|
|
1100
|
+
const text = chk.description ? `${chk.label} — ${chk.description}` : chk.label;
|
|
1101
|
+
formElements.push({
|
|
1102
|
+
tag: 'checker',
|
|
1103
|
+
name: `opt_${idx}`,
|
|
1104
|
+
checked: false,
|
|
1105
|
+
text: { tag: 'plain_text', content: text },
|
|
1106
|
+
element_id: `chk_${idx}`,
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
formElements.push({ tag: 'hr', element_id: 'hr_btns' });
|
|
1116
1110
|
}
|
|
1117
|
-
//
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
const buttons = card.buttons.map(btn => {
|
|
1111
|
+
// Buttons
|
|
1112
|
+
const buttons = kind.buttons;
|
|
1113
|
+
buttons.forEach((btn, idx) => {
|
|
1121
1114
|
const buttonEl = {
|
|
1122
1115
|
tag: 'button',
|
|
1123
1116
|
text: { tag: 'plain_text', content: btn.label },
|
|
1124
1117
|
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
},
|
|
1118
|
+
action_type: 'form_submit',
|
|
1119
|
+
name: `btn_${idx}`,
|
|
1120
|
+
element_id: `btn_${idx}`,
|
|
1121
|
+
value: kind.kind === 'command-card'
|
|
1122
|
+
? { _id: id, _command: btn.command, _initiator: initiatorId }
|
|
1123
|
+
: { _id: id, _action: btn.key, _initiator: initiatorId },
|
|
1132
1124
|
};
|
|
1133
|
-
if (btn.disabled)
|
|
1125
|
+
if (btn.disabled)
|
|
1134
1126
|
buttonEl.disabled = true;
|
|
1135
|
-
}
|
|
1136
1127
|
if (btn.confirm) {
|
|
1137
1128
|
buttonEl.confirm = {
|
|
1138
1129
|
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
1139
1130
|
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
1140
1131
|
};
|
|
1141
1132
|
}
|
|
1142
|
-
|
|
1133
|
+
formElements.push(buttonEl);
|
|
1143
1134
|
});
|
|
1144
|
-
|
|
1135
|
+
// Custom input (action only)
|
|
1136
|
+
const allowCustomInput = kind.kind === 'action' && kind.allowCustomInput;
|
|
1137
|
+
const outerElements = [];
|
|
1138
|
+
if (allowCustomInput && opts?.showInput) {
|
|
1139
|
+
// 展开态:输入框 + 提交按钮内联进 form(整卡作为回调返回值替换,规避 200810)
|
|
1140
|
+
formElements.push({ tag: 'hr', element_id: 'hr_input' }, {
|
|
1141
|
+
tag: 'input',
|
|
1142
|
+
name: 'custom_text',
|
|
1143
|
+
element_id: 'input_custom',
|
|
1144
|
+
placeholder: { tag: 'plain_text', content: '输入自定义回复...' },
|
|
1145
|
+
}, {
|
|
1146
|
+
tag: 'button',
|
|
1147
|
+
text: { tag: 'plain_text', content: '✅ 提交输入' },
|
|
1148
|
+
type: 'primary',
|
|
1149
|
+
action_type: 'form_submit',
|
|
1150
|
+
name: 'btn_submit_custom',
|
|
1151
|
+
element_id: 'btn_submit_custom',
|
|
1152
|
+
value: { _id: id, _action: '_custom_input', _initiator: initiatorId },
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
else if (allowCustomInput) {
|
|
1156
|
+
// 初始态:「手动输入」按钮放在 form 容器外(form 内按钮须 form_submit,11310)
|
|
1157
|
+
outerElements.push({
|
|
1158
|
+
tag: 'button',
|
|
1159
|
+
text: { tag: 'plain_text', content: '✏️ 手动输入' },
|
|
1160
|
+
type: 'default',
|
|
1161
|
+
element_id: 'btn_show_input',
|
|
1162
|
+
value: { _id: id, _action: '_show_input', _initiator: initiatorId },
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1145
1165
|
return {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1166
|
+
schema: '2.0',
|
|
1167
|
+
config: { update_multi: true, streaming_mode: false },
|
|
1168
|
+
header: { title: { tag: 'plain_text', content: title }, template: 'blue' },
|
|
1169
|
+
body: {
|
|
1170
|
+
elements: [
|
|
1171
|
+
{
|
|
1172
|
+
tag: 'form',
|
|
1173
|
+
name: 'action_form',
|
|
1174
|
+
element_id: 'action_form',
|
|
1175
|
+
elements: formElements,
|
|
1176
|
+
},
|
|
1177
|
+
...outerElements,
|
|
1178
|
+
],
|
|
1150
1179
|
},
|
|
1151
|
-
elements,
|
|
1152
1180
|
};
|
|
1153
1181
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1182
|
+
/**
|
|
1183
|
+
* 唯一 resolved 终态构建器(按钮禁用 + 结果展示 + checker 勾选汇总)。
|
|
1184
|
+
* 作为飞书卡片回调的返回值下发,替换原卡片内容。
|
|
1185
|
+
*/
|
|
1186
|
+
export function buildResolvedV2(interaction, response) {
|
|
1187
|
+
const action = response.action;
|
|
1188
|
+
const kind = interaction.kind;
|
|
1189
|
+
const labelMap = {
|
|
1190
|
+
'allow': '✅ 已允许',
|
|
1191
|
+
'always': '🔓 已设为始终允许',
|
|
1192
|
+
'deny': '❌ 已拒绝',
|
|
1193
|
+
'cancel': '取消',
|
|
1194
|
+
};
|
|
1195
|
+
const statusText = labelMap[action] || (/^\p{Emoji}/u.test(action) ? action : `✅ ${action}`);
|
|
1196
|
+
const headerTemplate = action === 'deny' ? 'red' : 'green';
|
|
1197
|
+
const headerTitle = `${kind.title} — ${statusText}`;
|
|
1198
|
+
const bodyElements = [];
|
|
1199
|
+
if (kind.body) {
|
|
1200
|
+
bodyElements.push({ tag: 'markdown', content: kind.body });
|
|
1201
|
+
}
|
|
1202
|
+
// Checker summary from interaction.kind.checkers + response.values
|
|
1203
|
+
if (kind.kind === 'action' && kind.checkers?.length && response.values) {
|
|
1204
|
+
const lines = kind.checkers.map((chk, idx) => {
|
|
1205
|
+
const checked = !!response.values[`opt_${idx}`];
|
|
1206
|
+
return `${checked ? '☑' : '☐'} ${chk.label}`;
|
|
1207
|
+
});
|
|
1208
|
+
bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
|
|
1209
|
+
}
|
|
1210
|
+
// CommandCard: 显示原有按钮列表(保留上下文)
|
|
1211
|
+
if (kind.kind === 'command-card' && kind.buttons?.length) {
|
|
1212
|
+
const lines = kind.buttons.map(btn => {
|
|
1213
|
+
const prefix = btn.command === action ? '✓' : '•';
|
|
1214
|
+
const cleanLabel = btn.label.replace(/^✓\s*/, '');
|
|
1215
|
+
return `${prefix} ${cleanLabel}`;
|
|
1216
|
+
});
|
|
1217
|
+
bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
|
|
1159
1218
|
}
|
|
1160
|
-
// Build full card body for resolved state: original body + button labels
|
|
1161
|
-
const btnLabels = action.buttons.map(btn => btn.label).join(' · ');
|
|
1162
|
-
const fullCardBody = [action.body, btnLabels].filter(Boolean).join('\n\n');
|
|
1163
|
-
// Buttons row
|
|
1164
|
-
const buttons = action.buttons.map(btn => {
|
|
1165
|
-
const buttonEl = {
|
|
1166
|
-
tag: 'button',
|
|
1167
|
-
text: { tag: 'plain_text', content: btn.label },
|
|
1168
|
-
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
1169
|
-
value: {
|
|
1170
|
-
_request_id: requestId,
|
|
1171
|
-
_action: btn.key,
|
|
1172
|
-
_initiator: initiatorId,
|
|
1173
|
-
_card_title: action.title,
|
|
1174
|
-
_card_body: fullCardBody,
|
|
1175
|
-
_btn_label: btn.label,
|
|
1176
|
-
},
|
|
1177
|
-
};
|
|
1178
|
-
if (btn.confirm) {
|
|
1179
|
-
buttonEl.confirm = {
|
|
1180
|
-
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
1181
|
-
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
return buttonEl;
|
|
1185
|
-
});
|
|
1186
|
-
elements.push({
|
|
1187
|
-
tag: 'action',
|
|
1188
|
-
actions: buttons,
|
|
1189
|
-
});
|
|
1190
1219
|
return {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1220
|
+
toast: { type: 'success', content: statusText },
|
|
1221
|
+
card: {
|
|
1222
|
+
type: 'raw',
|
|
1223
|
+
data: {
|
|
1224
|
+
schema: '2.0',
|
|
1225
|
+
config: { update_multi: true, streaming_mode: false },
|
|
1226
|
+
header: {
|
|
1227
|
+
template: headerTemplate,
|
|
1228
|
+
title: { tag: 'plain_text', content: headerTitle },
|
|
1229
|
+
},
|
|
1230
|
+
body: { elements: bodyElements },
|
|
1231
|
+
},
|
|
1195
1232
|
},
|
|
1196
|
-
elements,
|
|
1197
1233
|
};
|
|
1198
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* 卡片回调的纯路由决策。不产生副作用(除 store.markInputShown),返回决策对象供
|
|
1237
|
+
* WS 回调执行器消费。元数据从 CardMetaStore 反查,value 只读 _id / _action / _command。
|
|
1238
|
+
*/
|
|
1239
|
+
export function routeCardAction(input, store) {
|
|
1240
|
+
const { value, formValues, operatorId } = input;
|
|
1241
|
+
const id = value._id;
|
|
1242
|
+
if (!id)
|
|
1243
|
+
return { kind: 'ignore' };
|
|
1244
|
+
const entry = store.get(id);
|
|
1245
|
+
const interaction = entry?.interaction;
|
|
1246
|
+
const initiatorId = interaction?.initiatorId ?? value._initiator;
|
|
1247
|
+
// initiator 校验
|
|
1248
|
+
if (initiatorId && operatorId && operatorId !== initiatorId) {
|
|
1249
|
+
return { kind: 'reject' };
|
|
1250
|
+
}
|
|
1251
|
+
// command-card:按钮直接触发命令
|
|
1252
|
+
if (value._command) {
|
|
1253
|
+
const synthetic = {
|
|
1254
|
+
type: 'interaction.response', id, action: value._command, operatorId,
|
|
1255
|
+
};
|
|
1256
|
+
const card = interaction
|
|
1257
|
+
? buildResolvedV2(interaction, synthetic)
|
|
1258
|
+
: buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
|
|
1259
|
+
kind: { kind: 'command-card', title: '操作', buttons: [] } }, synthetic);
|
|
1260
|
+
return { kind: 'command', command: value._command, card };
|
|
1261
|
+
}
|
|
1262
|
+
// _show_input:点击「手动输入」→ 整卡替换为带输入框的版本(规避 200810)
|
|
1263
|
+
if (value._action === '_show_input') {
|
|
1264
|
+
if (!interaction)
|
|
1265
|
+
return { kind: 'expired' };
|
|
1266
|
+
store.markInputShown(id);
|
|
1267
|
+
return {
|
|
1268
|
+
kind: 'show-input',
|
|
1269
|
+
card: { toast: { type: 'info', content: '请在下方输入' },
|
|
1270
|
+
card: { type: 'raw', data: buildCardV2(interaction, { showInput: true }) } },
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
// 普通提交 / 自定义输入提交
|
|
1274
|
+
const values = { ...formValues, ...value };
|
|
1275
|
+
for (const k of Object.keys(values)) {
|
|
1276
|
+
if (k.startsWith('_'))
|
|
1277
|
+
delete values[k];
|
|
1278
|
+
}
|
|
1279
|
+
const response = {
|
|
1280
|
+
type: 'interaction.response', id, action: value._action || 'submit', values, operatorId,
|
|
1281
|
+
};
|
|
1282
|
+
// _custom_input:把用户输入追加到 resolved 卡片正文
|
|
1283
|
+
let resolvedInteraction = interaction;
|
|
1284
|
+
if (interaction && response.action === '_custom_input' && formValues.custom_text) {
|
|
1285
|
+
const newBody = [interaction.kind.body, `**输入内容:** ${formValues.custom_text}`]
|
|
1286
|
+
.filter(Boolean).join('\n\n');
|
|
1287
|
+
resolvedInteraction = { ...interaction, kind: { ...interaction.kind, body: newBody } };
|
|
1288
|
+
}
|
|
1289
|
+
const card = resolvedInteraction
|
|
1290
|
+
? buildResolvedV2(resolvedInteraction, response)
|
|
1291
|
+
: buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
|
|
1292
|
+
kind: { kind: 'action', title: '操作', buttons: [] } }, response);
|
|
1293
|
+
return { kind: 'respond', response, card };
|
|
1294
|
+
}
|
|
1199
1295
|
function displayWidth(str) {
|
|
1200
1296
|
let width = 0;
|
|
1201
1297
|
for (const ch of str) {
|
|
@@ -1327,7 +1423,7 @@ export class FeishuChannelPlugin {
|
|
|
1327
1423
|
const adapter = {
|
|
1328
1424
|
channelName: inst.name,
|
|
1329
1425
|
channelKey: inst.name,
|
|
1330
|
-
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
|
|
1426
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true, thread: true },
|
|
1331
1427
|
send: async (envelope, payload) => {
|
|
1332
1428
|
const ctx = envelope.replyContext;
|
|
1333
1429
|
const channelId = envelope.channelId;
|
|
@@ -1341,6 +1437,8 @@ export class FeishuChannelPlugin {
|
|
|
1341
1437
|
const sendCtx = { ...(ctx ?? {}) };
|
|
1342
1438
|
if (payload.kind === 'result.text' && payload.isFinal)
|
|
1343
1439
|
sendCtx.title = '✅ 最终回复:';
|
|
1440
|
+
if (ctx?.metadata?.onThreadCreated)
|
|
1441
|
+
sendCtx.onThreadCreated = ctx.metadata.onThreadCreated;
|
|
1344
1442
|
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1345
1443
|
return;
|
|
1346
1444
|
}
|
|
@@ -1367,9 +1465,12 @@ export class FeishuChannelPlugin {
|
|
|
1367
1465
|
case 'status.progress':
|
|
1368
1466
|
// Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
|
|
1369
1467
|
return;
|
|
1370
|
-
case 'interaction':
|
|
1371
|
-
await channel.sendInteraction(channelId, payload.interaction, ctx);
|
|
1468
|
+
case 'interaction': {
|
|
1469
|
+
const sent = await channel.sendInteraction(channelId, payload.interaction, ctx);
|
|
1470
|
+
if (!sent)
|
|
1471
|
+
throw new Error('sendInteraction returned false');
|
|
1372
1472
|
return;
|
|
1473
|
+
}
|
|
1373
1474
|
case 'custom':
|
|
1374
1475
|
// Feishu 不支持自定义 payload
|
|
1375
1476
|
return;
|
|
@@ -1425,6 +1526,7 @@ export class FeishuChannelPlugin {
|
|
|
1425
1526
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
|
|
1426
1527
|
await handler({
|
|
1427
1528
|
channel: adapter.channelName, channelType, channelId: chatId, content, images,
|
|
1529
|
+
selfAID: inst.agentName,
|
|
1428
1530
|
chatType: chatType || 'private',
|
|
1429
1531
|
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
1430
1532
|
replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|