evolclaw 2.3.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.
@@ -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
- const detail = error?.response?.data || error?.message || error;
773
- logger.error(`[Feishu] Failed to send interaction card (id=${interaction.id}, replyTo=${options?.replyToMessageId || 'none'}):`, detail);
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
- const now = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
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 (response.values && action === 'submit') {
804
- const entries = Object.entries(response.values).filter(([k]) => !k.startsWith('_'));
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.showActivities ?? config.showActivities ?? 'all';
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.showActivities ?? config.showActivities ?? 'all';
1051
+ const mode = getChannelShowActivities(config, inst.name);
1252
1052
  if (mode === 'none')
1253
1053
  return false;
1254
1054
  if (mode === 'dm-only')