evolclaw 3.1.3 → 3.1.5
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 +27 -0
- package/assets/.env.template +4 -0
- package/assets/config.json.template +6 -0
- package/assets/wechat-group-qr.jpeg +0 -0
- package/dist/agents/claude-runner.js +348 -156
- package/dist/agents/kit-renderer.js +211 -42
- package/dist/aun/aid/agentmd.js +75 -139
- package/dist/aun/aid/client.js +1 -14
- package/dist/aun/aid/identity.js +381 -54
- package/dist/aun/aid/index.js +3 -2
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -35
- package/dist/channels/aun.js +92 -144
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +270 -190
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +26 -27
- package/dist/cli/bench.js +45 -34
- package/dist/cli/help.js +23 -0
- package/dist/cli/index.js +538 -77
- package/dist/cli/init-channel.js +7 -4
- package/dist/cli/link-rules.js +2 -1
- package/dist/cli/model.js +324 -0
- package/dist/cli/net-check.js +138 -56
- 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/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +87 -93
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -4
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/message-bridge.js +6 -6
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +104 -118
- 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 +78 -44
- 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 +312 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +37 -0
- package/dist/core/trigger/scheduler.js +2 -1
- package/dist/index.js +10 -3
- package/dist/ipc.js +22 -0
- package/dist/paths.js +87 -16
- package/dist/utils/npm-ops.js +18 -11
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +11 -7
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +181 -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 +82 -0
- package/kits/docs/evolclaw/msg.md +86 -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 +11 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +75 -39
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/05-venue.md +2 -2
- package/kits/rules/06-channel.md +30 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +4 -1
- package/kits/templates/system-fragments/identity.md +4 -4
- package/kits/templates/system-fragments/relation.md +8 -5
- package/kits/templates/system-fragments/session.md +27 -0
- package/kits/templates/system-fragments/venue.md +13 -1
- package/package.json +13 -6
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/net-check.js +0 -640
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/dist/watch-msg.js +0 -544
- 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/kits/templates/system-fragments/eckruntime.md +0 -14
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; // 默认关闭
|
|
@@ -320,76 +321,46 @@ export class FeishuChannel {
|
|
|
320
321
|
const operatorId = data.operator?.open_id;
|
|
321
322
|
const chatId = data.context?.open_chat_id || data.open_chat_id;
|
|
322
323
|
const cardMessageId = data.open_message_id || data.context?.open_message_id;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
324
|
+
const formValues = action.form_value || {};
|
|
325
|
+
const decision = routeCardAction({ value, formValues, operatorId }, this.cardMetaStore);
|
|
326
|
+
switch (decision.kind) {
|
|
327
|
+
case 'ignore':
|
|
328
|
+
return;
|
|
329
|
+
case 'reject':
|
|
330
|
+
return { toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' } };
|
|
331
|
+
case 'expired':
|
|
332
|
+
return { toast: { type: 'warning', content: '⚠️ 卡片已失效,请重新发起' } };
|
|
333
|
+
case 'show-input':
|
|
334
|
+
return decision.card;
|
|
335
|
+
case 'command': {
|
|
336
|
+
logger.info(`[Feishu] CommandCard trigger: command=${decision.command}, operator=${operatorId}`);
|
|
337
|
+
if (this.messageHandler) {
|
|
338
|
+
// 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
|
|
339
|
+
// 由 ensureSession 从已有 session 中继承正确的 chatType
|
|
340
|
+
await this.messageHandler({
|
|
341
|
+
channelId: chatId,
|
|
342
|
+
content: decision.command,
|
|
343
|
+
peerId: operatorId,
|
|
344
|
+
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
345
|
+
source: 'card-trigger',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (chatId && cardMessageId)
|
|
349
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
350
|
+
this.cardMetaStore.markResolved(value._id);
|
|
351
|
+
this.cardMetaStore.cleanup(value._id);
|
|
352
|
+
return decision.card;
|
|
329
353
|
}
|
|
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
|
-
});
|
|
354
|
+
case 'respond': {
|
|
355
|
+
logger.info(`[Feishu] Card action: id=${value._id}, action=${decision.response.action}`);
|
|
356
|
+
this.interactionCallback?.(decision.response);
|
|
357
|
+
if (chatId && cardMessageId)
|
|
358
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
359
|
+
this.cardMetaStore.markResolved(value._id);
|
|
360
|
+
this.cardMetaStore.cleanup(value._id);
|
|
361
|
+
return decision.card;
|
|
341
362
|
}
|
|
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
363
|
}
|
|
356
|
-
// initiator 校验
|
|
357
|
-
if (value._initiator && operatorId && operatorId !== value._initiator) {
|
|
358
|
-
return {
|
|
359
|
-
toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
|
|
360
|
-
};
|
|
361
|
-
}
|
|
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
364
|
}
|
|
394
365
|
catch (err) {
|
|
395
366
|
logger.error('[Feishu] Failed to handle card action:', err);
|
|
@@ -562,21 +533,22 @@ export class FeishuChannel {
|
|
|
562
533
|
}
|
|
563
534
|
catch (error) {
|
|
564
535
|
// 230011: 消息已被撤回,降级为普通消息重试
|
|
565
|
-
|
|
566
|
-
|
|
536
|
+
// 99992354: message_id 不存在(合成 ID 或已过期),降级为普通消息
|
|
537
|
+
const errCode = error.response?.data?.code;
|
|
538
|
+
if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
|
|
539
|
+
logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying without reply`);
|
|
567
540
|
return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
|
|
568
541
|
}
|
|
569
542
|
// 230025: 消息内容超长,截断后重试
|
|
570
|
-
if (
|
|
543
|
+
if (errCode === 230025) {
|
|
571
544
|
logger.warn(`[Feishu] Message too long (230025, ${content.length} chars), truncating`);
|
|
572
545
|
const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
|
|
573
546
|
return this.sendMessage(chatId, truncated, options);
|
|
574
547
|
}
|
|
575
548
|
const respData = error?.response?.data;
|
|
576
|
-
const code = respData?.code;
|
|
577
549
|
logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
|
|
578
550
|
// post 格式相关错误(400/230001):降级为纯文本重试
|
|
579
|
-
if (!options?.forceText && (error?.response?.status === 400 ||
|
|
551
|
+
if (!options?.forceText && (error?.response?.status === 400 || errCode === 230001)) {
|
|
580
552
|
logger.warn('[Feishu] Retrying as plain text (forceText)');
|
|
581
553
|
return this.sendMessage(chatId, content, { ...options, forceText: true });
|
|
582
554
|
}
|
|
@@ -639,9 +611,10 @@ export class FeishuChannel {
|
|
|
639
611
|
logger.info('[Feishu] File message sent successfully');
|
|
640
612
|
}
|
|
641
613
|
catch (error) {
|
|
642
|
-
// 230011:
|
|
643
|
-
|
|
644
|
-
|
|
614
|
+
// 230011/99992354: reply target invalid, retry without reply
|
|
615
|
+
const errCode = error.response?.data?.code;
|
|
616
|
+
if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
|
|
617
|
+
logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying file send without reply`);
|
|
645
618
|
return this.sendFile(chatId, filePath);
|
|
646
619
|
}
|
|
647
620
|
logger.error('[Feishu] Failed to send file:', error);
|
|
@@ -683,6 +656,7 @@ export class FeishuChannel {
|
|
|
683
656
|
logger.debug('[Feishu] Image message sent successfully');
|
|
684
657
|
}
|
|
685
658
|
catch (error) {
|
|
659
|
+
// 99992354: reply target invalid — image cannot easily retry, just log
|
|
686
660
|
logger.error('[Feishu] Failed to send image:', error);
|
|
687
661
|
throw error;
|
|
688
662
|
}
|
|
@@ -981,12 +955,13 @@ export class FeishuChannel {
|
|
|
981
955
|
if (!set || set.size === 0)
|
|
982
956
|
return;
|
|
983
957
|
const expiredCard = {
|
|
984
|
-
|
|
958
|
+
schema: '2.0',
|
|
959
|
+
config: { update_multi: true },
|
|
985
960
|
header: {
|
|
986
961
|
template: 'grey',
|
|
987
962
|
title: { tag: 'plain_text', content: '已过期' },
|
|
988
963
|
},
|
|
989
|
-
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
964
|
+
body: { elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }] },
|
|
990
965
|
};
|
|
991
966
|
const ids = Array.from(set);
|
|
992
967
|
this.pendingCardsByChat.delete(chatId);
|
|
@@ -1006,10 +981,8 @@ export class FeishuChannel {
|
|
|
1006
981
|
async sendInteraction(chatId, interaction, options) {
|
|
1007
982
|
if (!this.client)
|
|
1008
983
|
return false;
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return false;
|
|
1012
|
-
// 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
|
|
984
|
+
// 统一路径:schema 2.0 内联发送(im.message),不走 cardkit 实体
|
|
985
|
+
const card = buildCardV2(interaction);
|
|
1013
986
|
await this.invalidatePendingCards(chatId);
|
|
1014
987
|
try {
|
|
1015
988
|
let messageId;
|
|
@@ -1038,12 +1011,15 @@ export class FeishuChannel {
|
|
|
1038
1011
|
messageId = res?.data?.message_id;
|
|
1039
1012
|
}
|
|
1040
1013
|
logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
|
|
1041
|
-
if (messageId)
|
|
1014
|
+
if (messageId) {
|
|
1042
1015
|
this.trackPendingCard(chatId, messageId);
|
|
1016
|
+
this.cardMetaStore.set(interaction.id, {
|
|
1017
|
+
interaction, chatId, messageId, resolved: false,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1043
1020
|
return messageId || false;
|
|
1044
1021
|
}
|
|
1045
1022
|
catch (error) {
|
|
1046
|
-
// 飞书 SDK 错误可能在 response.data、message 或 error 本身
|
|
1047
1023
|
const respData = error?.response?.data;
|
|
1048
1024
|
const detail = respData
|
|
1049
1025
|
? JSON.stringify(respData)
|
|
@@ -1051,42 +1027,10 @@ export class FeishuChannel {
|
|
|
1051
1027
|
? error.message
|
|
1052
1028
|
: JSON.stringify(error, Object.getOwnPropertyNames(error));
|
|
1053
1029
|
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))}`);
|
|
1030
|
+
logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(card)}`);
|
|
1056
1031
|
return false;
|
|
1057
1032
|
}
|
|
1058
1033
|
}
|
|
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
1034
|
addAckReaction(messageId) {
|
|
1091
1035
|
if (!this.client)
|
|
1092
1036
|
return;
|
|
@@ -1098,104 +1042,236 @@ export class FeishuChannel {
|
|
|
1098
1042
|
}).catch(() => { });
|
|
1099
1043
|
}
|
|
1100
1044
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1045
|
+
export class CardMetaStore {
|
|
1046
|
+
map = new Map();
|
|
1047
|
+
set(id, entry) {
|
|
1048
|
+
this.map.set(id, entry);
|
|
1049
|
+
}
|
|
1050
|
+
get(id) {
|
|
1051
|
+
return this.map.get(id);
|
|
1106
1052
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1053
|
+
markResolved(id) {
|
|
1054
|
+
const entry = this.map.get(id);
|
|
1055
|
+
if (entry)
|
|
1056
|
+
entry.resolved = true;
|
|
1057
|
+
}
|
|
1058
|
+
markInputShown(id) {
|
|
1059
|
+
const entry = this.map.get(id);
|
|
1060
|
+
if (entry)
|
|
1061
|
+
entry.inputShown = true;
|
|
1062
|
+
}
|
|
1063
|
+
cleanup(id) {
|
|
1064
|
+
this.map.delete(id);
|
|
1109
1065
|
}
|
|
1110
|
-
return null;
|
|
1111
1066
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1067
|
+
// ── 统一卡片构建器(schema 2.0 内联)──
|
|
1068
|
+
/**
|
|
1069
|
+
* 唯一卡片构建器。输入协议层 InteractionRequest,输出 schema 2.0 内联卡片 JSON。
|
|
1070
|
+
* - command-card: 按钮 value 带 { _id, _command, _initiator }
|
|
1071
|
+
* - action: 按钮 value 带 { _id, _action, _initiator };checkers → form+checker;
|
|
1072
|
+
* allowCustomInput → 「手动输入」按钮(form 容器外)
|
|
1073
|
+
* @param opts.showInput 展开自定义输入框(用于 _show_input 回调返回整卡)
|
|
1074
|
+
*/
|
|
1075
|
+
export function buildCardV2(interaction, opts) {
|
|
1076
|
+
const { kind } = interaction;
|
|
1077
|
+
const id = interaction.id;
|
|
1078
|
+
const initiatorId = interaction.initiatorId;
|
|
1079
|
+
const title = kind.title;
|
|
1080
|
+
const body = kind.body;
|
|
1081
|
+
const formElements = [];
|
|
1082
|
+
// Body markdown
|
|
1083
|
+
if (body) {
|
|
1084
|
+
formElements.push({ tag: 'markdown', content: body, element_id: 'body_md' });
|
|
1116
1085
|
}
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1086
|
+
// Checkers (action only)
|
|
1087
|
+
if (kind.kind === 'action' && kind.checkers?.length) {
|
|
1088
|
+
kind.checkers.forEach((chk, idx) => {
|
|
1089
|
+
const text = chk.description ? `${chk.label} — ${chk.description}` : chk.label;
|
|
1090
|
+
formElements.push({
|
|
1091
|
+
tag: 'checker',
|
|
1092
|
+
name: `opt_${idx}`,
|
|
1093
|
+
checked: false,
|
|
1094
|
+
text: { tag: 'plain_text', content: text },
|
|
1095
|
+
element_id: `chk_${idx}`,
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
formElements.push({ tag: 'hr', element_id: 'hr_btns' });
|
|
1099
|
+
}
|
|
1100
|
+
// Buttons
|
|
1101
|
+
const buttons = kind.buttons;
|
|
1102
|
+
buttons.forEach((btn, idx) => {
|
|
1121
1103
|
const buttonEl = {
|
|
1122
1104
|
tag: 'button',
|
|
1123
1105
|
text: { tag: 'plain_text', content: btn.label },
|
|
1124
1106
|
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
},
|
|
1107
|
+
action_type: 'form_submit',
|
|
1108
|
+
name: `btn_${idx}`,
|
|
1109
|
+
element_id: `btn_${idx}`,
|
|
1110
|
+
value: kind.kind === 'command-card'
|
|
1111
|
+
? { _id: id, _command: btn.command, _initiator: initiatorId }
|
|
1112
|
+
: { _id: id, _action: btn.key, _initiator: initiatorId },
|
|
1132
1113
|
};
|
|
1133
|
-
if (btn.disabled)
|
|
1114
|
+
if (btn.disabled)
|
|
1134
1115
|
buttonEl.disabled = true;
|
|
1135
|
-
}
|
|
1136
1116
|
if (btn.confirm) {
|
|
1137
1117
|
buttonEl.confirm = {
|
|
1138
1118
|
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
1139
1119
|
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
1140
1120
|
};
|
|
1141
1121
|
}
|
|
1142
|
-
|
|
1122
|
+
formElements.push(buttonEl);
|
|
1143
1123
|
});
|
|
1144
|
-
|
|
1124
|
+
// Custom input (action only)
|
|
1125
|
+
const allowCustomInput = kind.kind === 'action' && kind.allowCustomInput;
|
|
1126
|
+
const outerElements = [];
|
|
1127
|
+
if (allowCustomInput && opts?.showInput) {
|
|
1128
|
+
// 展开态:输入框 + 提交按钮内联进 form(整卡作为回调返回值替换,规避 200810)
|
|
1129
|
+
formElements.push({ tag: 'hr', element_id: 'hr_input' }, {
|
|
1130
|
+
tag: 'input',
|
|
1131
|
+
name: 'custom_text',
|
|
1132
|
+
element_id: 'input_custom',
|
|
1133
|
+
placeholder: { tag: 'plain_text', content: '输入自定义回复...' },
|
|
1134
|
+
}, {
|
|
1135
|
+
tag: 'button',
|
|
1136
|
+
text: { tag: 'plain_text', content: '✅ 提交输入' },
|
|
1137
|
+
type: 'primary',
|
|
1138
|
+
action_type: 'form_submit',
|
|
1139
|
+
name: 'btn_submit_custom',
|
|
1140
|
+
element_id: 'btn_submit_custom',
|
|
1141
|
+
value: { _id: id, _action: '_custom_input', _initiator: initiatorId },
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
else if (allowCustomInput) {
|
|
1145
|
+
// 初始态:「手动输入」按钮放在 form 容器外(form 内按钮须 form_submit,11310)
|
|
1146
|
+
outerElements.push({
|
|
1147
|
+
tag: 'button',
|
|
1148
|
+
text: { tag: 'plain_text', content: '✏️ 手动输入' },
|
|
1149
|
+
type: 'default',
|
|
1150
|
+
element_id: 'btn_show_input',
|
|
1151
|
+
value: { _id: id, _action: '_show_input', _initiator: initiatorId },
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1145
1154
|
return {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1155
|
+
schema: '2.0',
|
|
1156
|
+
config: { update_multi: true, streaming_mode: false },
|
|
1157
|
+
header: { title: { tag: 'plain_text', content: title }, template: 'blue' },
|
|
1158
|
+
body: {
|
|
1159
|
+
elements: [
|
|
1160
|
+
{
|
|
1161
|
+
tag: 'form',
|
|
1162
|
+
name: 'action_form',
|
|
1163
|
+
element_id: 'action_form',
|
|
1164
|
+
elements: formElements,
|
|
1165
|
+
},
|
|
1166
|
+
...outerElements,
|
|
1167
|
+
],
|
|
1150
1168
|
},
|
|
1151
|
-
elements,
|
|
1152
1169
|
};
|
|
1153
1170
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1171
|
+
/**
|
|
1172
|
+
* 唯一 resolved 终态构建器(按钮禁用 + 结果展示 + checker 勾选汇总)。
|
|
1173
|
+
* 作为飞书卡片回调的返回值下发,替换原卡片内容。
|
|
1174
|
+
*/
|
|
1175
|
+
export function buildResolvedV2(interaction, response) {
|
|
1176
|
+
const action = response.action;
|
|
1177
|
+
const kind = interaction.kind;
|
|
1178
|
+
const labelMap = {
|
|
1179
|
+
'allow': '✅ 已允许',
|
|
1180
|
+
'always': '🔓 已设为始终允许',
|
|
1181
|
+
'deny': '❌ 已拒绝',
|
|
1182
|
+
'cancel': '取消',
|
|
1183
|
+
};
|
|
1184
|
+
const statusText = labelMap[action] || (/^\p{Emoji}/u.test(action) ? action : `✅ ${action}`);
|
|
1185
|
+
const headerTemplate = action === 'deny' ? 'red' : 'green';
|
|
1186
|
+
const headerTitle = `${kind.title} — ${statusText}`;
|
|
1187
|
+
const bodyElements = [];
|
|
1188
|
+
if (kind.body) {
|
|
1189
|
+
bodyElements.push({ tag: 'markdown', content: kind.body });
|
|
1190
|
+
}
|
|
1191
|
+
// Checker summary from interaction.kind.checkers + response.values
|
|
1192
|
+
if (kind.kind === 'action' && kind.checkers?.length && response.values) {
|
|
1193
|
+
const lines = kind.checkers.map((chk, idx) => {
|
|
1194
|
+
const checked = !!response.values[`opt_${idx}`];
|
|
1195
|
+
return `${checked ? '☑' : '☐'} ${chk.label}`;
|
|
1196
|
+
});
|
|
1197
|
+
bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
|
|
1159
1198
|
}
|
|
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
1199
|
return {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1200
|
+
toast: { type: 'success', content: statusText },
|
|
1201
|
+
card: {
|
|
1202
|
+
type: 'raw',
|
|
1203
|
+
data: {
|
|
1204
|
+
schema: '2.0',
|
|
1205
|
+
config: { update_multi: true, streaming_mode: false },
|
|
1206
|
+
header: {
|
|
1207
|
+
template: headerTemplate,
|
|
1208
|
+
title: { tag: 'plain_text', content: headerTitle },
|
|
1209
|
+
},
|
|
1210
|
+
body: { elements: bodyElements },
|
|
1211
|
+
},
|
|
1195
1212
|
},
|
|
1196
|
-
elements,
|
|
1197
1213
|
};
|
|
1198
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* 卡片回调的纯路由决策。不产生副作用(除 store.markInputShown),返回决策对象供
|
|
1217
|
+
* WS 回调执行器消费。元数据从 CardMetaStore 反查,value 只读 _id / _action / _command。
|
|
1218
|
+
*/
|
|
1219
|
+
export function routeCardAction(input, store) {
|
|
1220
|
+
const { value, formValues, operatorId } = input;
|
|
1221
|
+
const id = value._id;
|
|
1222
|
+
if (!id)
|
|
1223
|
+
return { kind: 'ignore' };
|
|
1224
|
+
const entry = store.get(id);
|
|
1225
|
+
const interaction = entry?.interaction;
|
|
1226
|
+
const initiatorId = interaction?.initiatorId ?? value._initiator;
|
|
1227
|
+
// initiator 校验
|
|
1228
|
+
if (initiatorId && operatorId && operatorId !== initiatorId) {
|
|
1229
|
+
return { kind: 'reject' };
|
|
1230
|
+
}
|
|
1231
|
+
// command-card:按钮直接触发命令
|
|
1232
|
+
if (value._command) {
|
|
1233
|
+
const synthetic = {
|
|
1234
|
+
type: 'interaction.response', id, action: value._command, operatorId,
|
|
1235
|
+
};
|
|
1236
|
+
const card = interaction
|
|
1237
|
+
? buildResolvedV2(interaction, synthetic)
|
|
1238
|
+
: buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
|
|
1239
|
+
kind: { kind: 'command-card', title: '操作', buttons: [] } }, synthetic);
|
|
1240
|
+
return { kind: 'command', command: value._command, card };
|
|
1241
|
+
}
|
|
1242
|
+
// _show_input:点击「手动输入」→ 整卡替换为带输入框的版本(规避 200810)
|
|
1243
|
+
if (value._action === '_show_input') {
|
|
1244
|
+
if (!interaction)
|
|
1245
|
+
return { kind: 'expired' };
|
|
1246
|
+
store.markInputShown(id);
|
|
1247
|
+
return {
|
|
1248
|
+
kind: 'show-input',
|
|
1249
|
+
card: { toast: { type: 'info', content: '请在下方输入' },
|
|
1250
|
+
card: { type: 'raw', data: buildCardV2(interaction, { showInput: true }) } },
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
// 普通提交 / 自定义输入提交
|
|
1254
|
+
const values = { ...formValues, ...value };
|
|
1255
|
+
for (const k of Object.keys(values)) {
|
|
1256
|
+
if (k.startsWith('_'))
|
|
1257
|
+
delete values[k];
|
|
1258
|
+
}
|
|
1259
|
+
const response = {
|
|
1260
|
+
type: 'interaction.response', id, action: value._action || 'submit', values, operatorId,
|
|
1261
|
+
};
|
|
1262
|
+
// _custom_input:把用户输入追加到 resolved 卡片正文
|
|
1263
|
+
let resolvedInteraction = interaction;
|
|
1264
|
+
if (interaction && response.action === '_custom_input' && formValues.custom_text) {
|
|
1265
|
+
const newBody = [interaction.kind.body, `**输入内容:** ${formValues.custom_text}`]
|
|
1266
|
+
.filter(Boolean).join('\n\n');
|
|
1267
|
+
resolvedInteraction = { ...interaction, kind: { ...interaction.kind, body: newBody } };
|
|
1268
|
+
}
|
|
1269
|
+
const card = resolvedInteraction
|
|
1270
|
+
? buildResolvedV2(resolvedInteraction, response)
|
|
1271
|
+
: buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
|
|
1272
|
+
kind: { kind: 'action', title: '操作', buttons: [] } }, response);
|
|
1273
|
+
return { kind: 'respond', response, card };
|
|
1274
|
+
}
|
|
1199
1275
|
function displayWidth(str) {
|
|
1200
1276
|
let width = 0;
|
|
1201
1277
|
for (const ch of str) {
|
|
@@ -1367,9 +1443,12 @@ export class FeishuChannelPlugin {
|
|
|
1367
1443
|
case 'status.progress':
|
|
1368
1444
|
// Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
|
|
1369
1445
|
return;
|
|
1370
|
-
case 'interaction':
|
|
1371
|
-
await channel.sendInteraction(channelId, payload.interaction, ctx);
|
|
1446
|
+
case 'interaction': {
|
|
1447
|
+
const sent = await channel.sendInteraction(channelId, payload.interaction, ctx);
|
|
1448
|
+
if (!sent)
|
|
1449
|
+
throw new Error('sendInteraction returned false');
|
|
1372
1450
|
return;
|
|
1451
|
+
}
|
|
1373
1452
|
case 'custom':
|
|
1374
1453
|
// Feishu 不支持自定义 payload
|
|
1375
1454
|
return;
|
|
@@ -1425,6 +1504,7 @@ export class FeishuChannelPlugin {
|
|
|
1425
1504
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
|
|
1426
1505
|
await handler({
|
|
1427
1506
|
channel: adapter.channelName, channelType, channelId: chatId, content, images,
|
|
1507
|
+
selfAID: inst.agentName,
|
|
1428
1508
|
chatType: chatType || 'private',
|
|
1429
1509
|
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
1430
1510
|
replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|
package/dist/channels/qqbot.js
CHANGED
package/dist/channels/wechat.js
CHANGED
package/dist/channels/wecom.js
CHANGED