evolclaw 2.4.0 → 2.5.0
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/README.md +33 -14
- package/dist/agents/claude-runner.js +224 -23
- package/dist/agents/codex-runner.js +2 -8
- package/dist/agents/gemini-runner.js +1 -8
- package/dist/channels/aun.js +438 -53
- package/dist/channels/dingtalk.js +506 -0
- package/dist/channels/feishu.js +31 -231
- package/dist/channels/qqbot.js +391 -0
- package/dist/channels/wechat.js +36 -38
- package/dist/channels/wecom.js +549 -0
- package/dist/cli.js +69 -9
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +462 -112
- package/dist/core/message/message-bridge.js +8 -5
- package/dist/core/message/message-processor.js +146 -54
- package/dist/core/message/message-queue.js +48 -0
- package/dist/core/message/stream-flusher.js +2 -2
- package/dist/core/session/session-manager.js +21 -3
- package/dist/index.js +48 -13
- package/dist/ipc.js +14 -4
- package/dist/templates/skills.md +64 -0
- package/dist/utils/error-dict.js +63 -0
- package/dist/utils/error-utils.js +156 -56
- package/dist/utils/format.js +32 -0
- package/dist/utils/init-channel.js +734 -8
- package/dist/utils/init.js +33 -2
- package/dist/utils/media-cache.js +2 -0
- package/dist/utils/stats-collector.js +0 -8
- package/package.json +9 -3
package/dist/channels/feishu.js
CHANGED
|
@@ -298,11 +298,15 @@ export class FeishuChannel {
|
|
|
298
298
|
delete response.values._request_id;
|
|
299
299
|
delete response.values._action;
|
|
300
300
|
delete response.values._card_title;
|
|
301
|
+
const cardBody = value._card_body || '';
|
|
302
|
+
delete response.values._card_body;
|
|
303
|
+
const btnLabel = value._btn_label || '';
|
|
304
|
+
delete response.values._btn_label;
|
|
301
305
|
logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
|
|
302
306
|
this.interactionCallback?.(response);
|
|
303
307
|
// Return updated card (buttons disabled + result shown)
|
|
304
308
|
const cardTitle = value._card_title || '操作';
|
|
305
|
-
return this.buildResolvedCard(cardTitle, response);
|
|
309
|
+
return this.buildResolvedCard(cardTitle, response, cardBody, btnLabel);
|
|
306
310
|
}
|
|
307
311
|
catch (err) {
|
|
308
312
|
logger.error('[Feishu] Failed to handle card action:', err);
|
|
@@ -769,8 +773,16 @@ export class FeishuChannel {
|
|
|
769
773
|
return messageId || false;
|
|
770
774
|
}
|
|
771
775
|
catch (error) {
|
|
772
|
-
|
|
773
|
-
|
|
776
|
+
// 飞书 SDK 错误可能在 response.data、message 或 error 本身
|
|
777
|
+
const respData = error?.response?.data;
|
|
778
|
+
const detail = respData
|
|
779
|
+
? JSON.stringify(respData)
|
|
780
|
+
: error?.message && error.message !== String(error)
|
|
781
|
+
? error.message
|
|
782
|
+
: JSON.stringify(error, Object.getOwnPropertyNames(error));
|
|
783
|
+
logger.error(`[Feishu] Failed to send interaction card (id=${interaction.id}, replyTo=${options?.replyToMessageId || 'none'}): ${detail}`);
|
|
784
|
+
// 同时记录卡片内容以便调试
|
|
785
|
+
logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(buildInteractionCard(interaction))}`);
|
|
774
786
|
return false;
|
|
775
787
|
}
|
|
776
788
|
}
|
|
@@ -787,30 +799,20 @@ export class FeishuChannel {
|
|
|
787
799
|
logger.warn(`[Feishu] Failed to patch card ${messageId}:`, error?.response?.data || error?.message);
|
|
788
800
|
}
|
|
789
801
|
}
|
|
790
|
-
buildResolvedCard(cardTitle, response) {
|
|
802
|
+
buildResolvedCard(cardTitle, response, cardBody, btnLabel) {
|
|
791
803
|
const action = response.action;
|
|
792
804
|
const labelMap = {
|
|
793
805
|
'allow': '✅ 已允许',
|
|
794
806
|
'always': '🔓 已设为始终允许',
|
|
795
807
|
'deny': '❌ 已拒绝',
|
|
796
808
|
'cancel': '取消',
|
|
797
|
-
'submit': '✅ 已提交',
|
|
798
809
|
};
|
|
799
|
-
const statusText = labelMap[action] || `✅ ${action}
|
|
800
|
-
|
|
801
|
-
// Build summary of selected values
|
|
810
|
+
const statusText = labelMap[action] || (btnLabel ? `✅ ${btnLabel}` : `✅ ${action}`);
|
|
811
|
+
// Build elements: original body only
|
|
802
812
|
const elements = [];
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
if (entries.length > 0) {
|
|
806
|
-
const lines = entries.map(([k, v]) => {
|
|
807
|
-
const display = Array.isArray(v) ? v.join(', ') : String(v);
|
|
808
|
-
return `**${k}**: ${display}`;
|
|
809
|
-
});
|
|
810
|
-
elements.push({ tag: 'markdown', content: lines.join('\n') });
|
|
811
|
-
}
|
|
813
|
+
if (cardBody) {
|
|
814
|
+
elements.push({ tag: 'markdown', content: cardBody });
|
|
812
815
|
}
|
|
813
|
-
elements.push({ tag: 'markdown', content: `操作时间:${now}` });
|
|
814
816
|
return {
|
|
815
817
|
toast: {
|
|
816
818
|
type: 'success',
|
|
@@ -846,10 +848,6 @@ export function buildInteractionCard(interaction) {
|
|
|
846
848
|
if (kind.kind === 'action') {
|
|
847
849
|
return buildActionCard(interaction.id, kind);
|
|
848
850
|
}
|
|
849
|
-
if (kind.kind === 'form') {
|
|
850
|
-
return buildFormCard(interaction.id, kind);
|
|
851
|
-
}
|
|
852
|
-
// menu kind: not rendered as card (handled via menu.response JSON)
|
|
853
851
|
return null;
|
|
854
852
|
}
|
|
855
853
|
export function buildActionCard(requestId, action) {
|
|
@@ -858,6 +856,9 @@ export function buildActionCard(requestId, action) {
|
|
|
858
856
|
if (action.body) {
|
|
859
857
|
elements.push({ tag: 'markdown', content: action.body });
|
|
860
858
|
}
|
|
859
|
+
// Build full card body for resolved state: original body + button labels
|
|
860
|
+
const btnLabels = action.buttons.map(btn => btn.label).join(' · ');
|
|
861
|
+
const fullCardBody = [action.body, btnLabels].filter(Boolean).join('\n\n');
|
|
861
862
|
// Buttons row
|
|
862
863
|
const buttons = action.buttons.map(btn => {
|
|
863
864
|
const buttonEl = {
|
|
@@ -868,6 +869,8 @@ export function buildActionCard(requestId, action) {
|
|
|
868
869
|
_request_id: requestId,
|
|
869
870
|
_action: btn.key,
|
|
870
871
|
_card_title: action.title,
|
|
872
|
+
_card_body: fullCardBody,
|
|
873
|
+
_btn_label: btn.label,
|
|
871
874
|
},
|
|
872
875
|
};
|
|
873
876
|
if (btn.confirm) {
|
|
@@ -891,209 +894,6 @@ export function buildActionCard(requestId, action) {
|
|
|
891
894
|
elements,
|
|
892
895
|
};
|
|
893
896
|
}
|
|
894
|
-
export function buildFormCard(requestId, form) {
|
|
895
|
-
// Use Feishu card v2 form container: all fields wrapped in a `form` tag.
|
|
896
|
-
// On submit, callback receives `action.form_value` with all field values keyed by `name`.
|
|
897
|
-
// This eliminates per-field callbacks when selecting dropdown options.
|
|
898
|
-
const formElements = [];
|
|
899
|
-
// Body text
|
|
900
|
-
if (form.body) {
|
|
901
|
-
formElements.push({ tag: 'markdown', content: form.body });
|
|
902
|
-
}
|
|
903
|
-
// Fields — inside form, components use `name` (not `value`) for identification
|
|
904
|
-
for (const field of form.fields) {
|
|
905
|
-
formElements.push(buildFormFieldElement(field));
|
|
906
|
-
if (field.hint) {
|
|
907
|
-
formElements.push({
|
|
908
|
-
tag: 'note',
|
|
909
|
-
elements: [{ tag: 'plain_text', content: field.hint }],
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
// Submit button (inside form, uses form_action_type)
|
|
914
|
-
const submitBtn = {
|
|
915
|
-
tag: 'button',
|
|
916
|
-
text: { tag: 'plain_text', content: form.submitLabel || '确认' },
|
|
917
|
-
type: form.submitStyle === 'danger' ? 'danger' : 'primary',
|
|
918
|
-
form_action_type: 'submit',
|
|
919
|
-
name: 'submit',
|
|
920
|
-
value: {
|
|
921
|
-
_request_id: requestId,
|
|
922
|
-
_action: 'submit',
|
|
923
|
-
_card_title: form.title,
|
|
924
|
-
},
|
|
925
|
-
};
|
|
926
|
-
if (form.submitConfirm) {
|
|
927
|
-
submitBtn.confirm = {
|
|
928
|
-
title: { tag: 'plain_text', content: form.submitConfirm.title },
|
|
929
|
-
text: { tag: 'plain_text', content: form.submitConfirm.body },
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
const actions = [submitBtn];
|
|
933
|
-
if (form.cancelable !== false) {
|
|
934
|
-
actions.push({
|
|
935
|
-
tag: 'button',
|
|
936
|
-
text: { tag: 'plain_text', content: '取消' },
|
|
937
|
-
type: 'default',
|
|
938
|
-
// Cancel is NOT form_action_type — it's a regular button that triggers callback directly
|
|
939
|
-
value: {
|
|
940
|
-
_request_id: requestId,
|
|
941
|
-
_action: 'cancel',
|
|
942
|
-
_card_title: form.title,
|
|
943
|
-
},
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
formElements.push({ tag: 'action', actions });
|
|
947
|
-
return {
|
|
948
|
-
schema: '2.0',
|
|
949
|
-
config: { update_multi: true },
|
|
950
|
-
header: {
|
|
951
|
-
template: 'blue',
|
|
952
|
-
title: { tag: 'plain_text', content: form.title },
|
|
953
|
-
},
|
|
954
|
-
body: {
|
|
955
|
-
elements: [
|
|
956
|
-
{
|
|
957
|
-
tag: 'form',
|
|
958
|
-
name: requestId,
|
|
959
|
-
elements: formElements,
|
|
960
|
-
},
|
|
961
|
-
],
|
|
962
|
-
},
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
/** Build a field element for use inside a form container (uses `name` for identification) */
|
|
966
|
-
export function buildFormFieldElement(field) {
|
|
967
|
-
switch (field.type) {
|
|
968
|
-
case 'select': {
|
|
969
|
-
const options = field.options.map(opt => ({
|
|
970
|
-
text: { tag: 'plain_text', content: opt.label },
|
|
971
|
-
value: opt.value,
|
|
972
|
-
}));
|
|
973
|
-
const selectedOpt = field.options.find(opt => opt.selected);
|
|
974
|
-
return {
|
|
975
|
-
tag: 'select_static',
|
|
976
|
-
name: field.key,
|
|
977
|
-
placeholder: { tag: 'plain_text', content: field.placeholder || `选择${field.label}` },
|
|
978
|
-
options,
|
|
979
|
-
...(selectedOpt ? { initial_option: selectedOpt.value } : {}),
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
case 'text': {
|
|
983
|
-
return {
|
|
984
|
-
tag: 'input',
|
|
985
|
-
name: field.key,
|
|
986
|
-
placeholder: { tag: 'plain_text', content: field.placeholder || `输入${field.label}` },
|
|
987
|
-
...(field.defaultValue != null ? { default_value: String(field.defaultValue) } : {}),
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
case 'toggle': {
|
|
991
|
-
const checked = field.defaultValue ?? false;
|
|
992
|
-
return {
|
|
993
|
-
tag: 'select_static',
|
|
994
|
-
name: field.key,
|
|
995
|
-
placeholder: { tag: 'plain_text', content: field.label },
|
|
996
|
-
options: [
|
|
997
|
-
{ text: { tag: 'plain_text', content: '开启' }, value: 'true' },
|
|
998
|
-
{ text: { tag: 'plain_text', content: '关闭' }, value: 'false' },
|
|
999
|
-
],
|
|
1000
|
-
initial_option: checked ? 'true' : 'false',
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
case 'multi-select': {
|
|
1004
|
-
const options = field.options.map(opt => ({
|
|
1005
|
-
text: { tag: 'plain_text', content: opt.label },
|
|
1006
|
-
value: opt.value,
|
|
1007
|
-
}));
|
|
1008
|
-
const selectedValues = field.options.filter(opt => opt.selected).map(opt => opt.value);
|
|
1009
|
-
return {
|
|
1010
|
-
tag: 'multi_select_static',
|
|
1011
|
-
name: field.key,
|
|
1012
|
-
placeholder: { tag: 'plain_text', content: `选择${field.label}` },
|
|
1013
|
-
options,
|
|
1014
|
-
...(selectedValues.length > 0 ? { initial_options: selectedValues } : {}),
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
default:
|
|
1018
|
-
return { tag: 'markdown', content: `[不支持的字段类型: ${field.type}]` };
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
export function buildFieldElement(requestId, field) {
|
|
1022
|
-
switch (field.type) {
|
|
1023
|
-
case 'select': {
|
|
1024
|
-
const options = field.options.map(opt => ({
|
|
1025
|
-
text: { tag: 'plain_text', content: opt.label },
|
|
1026
|
-
value: opt.value,
|
|
1027
|
-
}));
|
|
1028
|
-
const selectedOpt = field.options.find(opt => opt.selected);
|
|
1029
|
-
return {
|
|
1030
|
-
tag: 'action',
|
|
1031
|
-
actions: [{
|
|
1032
|
-
tag: 'select_static',
|
|
1033
|
-
placeholder: { tag: 'plain_text', content: field.placeholder || `选择${field.label}` },
|
|
1034
|
-
options,
|
|
1035
|
-
...(selectedOpt ? { initial_option: selectedOpt.value } : {}),
|
|
1036
|
-
value: {
|
|
1037
|
-
_request_id: requestId,
|
|
1038
|
-
_field_key: field.key,
|
|
1039
|
-
},
|
|
1040
|
-
}],
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
case 'text': {
|
|
1044
|
-
// Feishu cards don't have a native text input component.
|
|
1045
|
-
// Use a note element as placeholder label; actual input via form submit.
|
|
1046
|
-
return {
|
|
1047
|
-
tag: 'note',
|
|
1048
|
-
elements: [
|
|
1049
|
-
{ tag: 'plain_text', content: `${field.label}: ${field.placeholder || '(请在提交时输入)'}` },
|
|
1050
|
-
],
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
case 'toggle': {
|
|
1054
|
-
const checked = field.defaultValue ?? false;
|
|
1055
|
-
return {
|
|
1056
|
-
tag: 'action',
|
|
1057
|
-
actions: [{
|
|
1058
|
-
tag: 'select_static',
|
|
1059
|
-
placeholder: { tag: 'plain_text', content: field.label },
|
|
1060
|
-
options: [
|
|
1061
|
-
{ text: { tag: 'plain_text', content: '开启' }, value: 'true' },
|
|
1062
|
-
{ text: { tag: 'plain_text', content: '关闭' }, value: 'false' },
|
|
1063
|
-
],
|
|
1064
|
-
initial_option: checked ? 'true' : 'false',
|
|
1065
|
-
value: {
|
|
1066
|
-
_request_id: requestId,
|
|
1067
|
-
_field_key: field.key,
|
|
1068
|
-
},
|
|
1069
|
-
}],
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
case 'multi-select': {
|
|
1073
|
-
// Feishu cards: use multi_select_static (checker)
|
|
1074
|
-
const options = field.options.map(opt => ({
|
|
1075
|
-
text: { tag: 'plain_text', content: opt.label },
|
|
1076
|
-
value: opt.value,
|
|
1077
|
-
}));
|
|
1078
|
-
const selectedValues = field.options.filter(opt => opt.selected).map(opt => opt.value);
|
|
1079
|
-
return {
|
|
1080
|
-
tag: 'action',
|
|
1081
|
-
actions: [{
|
|
1082
|
-
tag: 'multi_select_static',
|
|
1083
|
-
placeholder: { tag: 'plain_text', content: `选择${field.label}` },
|
|
1084
|
-
options,
|
|
1085
|
-
...(selectedValues.length > 0 ? { initial_options: selectedValues } : {}),
|
|
1086
|
-
value: {
|
|
1087
|
-
_request_id: requestId,
|
|
1088
|
-
_field_key: field.key,
|
|
1089
|
-
},
|
|
1090
|
-
}],
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
default:
|
|
1094
|
-
return { tag: 'markdown', content: `[不支持的字段类型: ${field.type}]` };
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
897
|
function displayWidth(str) {
|
|
1098
898
|
let width = 0;
|
|
1099
899
|
for (const ch of str) {
|
|
@@ -1195,7 +995,7 @@ export function hasMarkdownSyntax(text) {
|
|
|
1195
995
|
];
|
|
1196
996
|
return markdownPatterns.some(pattern => pattern.test(text));
|
|
1197
997
|
}
|
|
1198
|
-
import { normalizeChannelInstances } from '../config.js';
|
|
998
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
|
|
1199
999
|
export class FeishuChannelPlugin {
|
|
1200
1000
|
name = 'feishu';
|
|
1201
1001
|
isEnabled(config) {
|
|
@@ -1231,14 +1031,14 @@ export class FeishuChannelPlugin {
|
|
|
1231
1031
|
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1232
1032
|
};
|
|
1233
1033
|
const policy = {
|
|
1234
|
-
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
1235
|
-
canListProjects: (chatType, identity) => identity === 'owner',
|
|
1034
|
+
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
1035
|
+
canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
1236
1036
|
canCreateSession: (chatType, identity) => true,
|
|
1237
1037
|
canDeleteSession: (chatType, identity) => true,
|
|
1238
|
-
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
1038
|
+
canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
1239
1039
|
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
1240
1040
|
showMiddleResult: (chatType, identity) => {
|
|
1241
|
-
const mode = inst.
|
|
1041
|
+
const mode = getChannelShowActivities(config, inst.name);
|
|
1242
1042
|
if (mode === 'none')
|
|
1243
1043
|
return false;
|
|
1244
1044
|
if (mode === 'dm-only')
|
|
@@ -1248,7 +1048,7 @@ export class FeishuChannelPlugin {
|
|
|
1248
1048
|
return true;
|
|
1249
1049
|
},
|
|
1250
1050
|
showIdleMonitor: (chatType, identity) => {
|
|
1251
|
-
const mode = inst.
|
|
1051
|
+
const mode = getChannelShowActivities(config, inst.name);
|
|
1252
1052
|
if (mode === 'none')
|
|
1253
1053
|
return false;
|
|
1254
1054
|
if (mode === 'dm-only')
|