@larksuite/openclaw-lark 2026.4.7 → 2026.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larksuite/openclaw-lark",
3
- "version": "2026.4.7",
3
+ "version": "2026.4.8",
4
4
  "description": "OpenClaw Lark/Feishu channel plugin",
5
5
  "exports": {
6
6
  ".": {
@@ -33,6 +33,7 @@ const tool_use_display_1 = require("./tool-use-display.js");
33
33
  */
34
34
  exports.STREAMING_ELEMENT_ID = 'streaming_content';
35
35
  exports.REASONING_ELEMENT_ID = 'reasoning_content';
36
+ const TOOL_USE_STEP_CONTENT_INDENT = '0px 0px 0px 22px';
36
37
  // ---------------------------------------------------------------------------
37
38
  // Helpers
38
39
  // ---------------------------------------------------------------------------
@@ -589,9 +590,9 @@ function buildStreamingToolUseActivePanel(params) {
589
590
  icon_expanded_angle: -180,
590
591
  },
591
592
  border: { color: 'grey', corner_radius: '5px' },
592
- vertical_spacing: '8px',
593
+ vertical_spacing: '4px',
593
594
  padding: '8px 8px 8px 8px',
594
- elements: steps.map(buildToolUseStepElement),
595
+ elements: steps.flatMap((step) => buildToolUseStepElements(step)),
595
596
  };
596
597
  }
597
598
  function toCardKit2(card) {
@@ -630,7 +631,7 @@ function buildStreamingToolUsePendingPanel() {
630
631
  icon_expanded_angle: -180,
631
632
  },
632
633
  border: { color: 'grey', corner_radius: '5px' },
633
- vertical_spacing: '8px',
634
+ vertical_spacing: '4px',
634
635
  padding: '8px 8px 8px 8px',
635
636
  elements: [],
636
637
  };
@@ -644,7 +645,9 @@ function buildToolUsePanel(params) {
644
645
  zhTitleParts.push(titleSuffix.zh);
645
646
  enTitleParts.push(titleSuffix.en);
646
647
  }
647
- const stepElements = toolUseSteps.length > 0 ? toolUseSteps.map((step) => buildToolUseStepElement(step)) : [buildToolUsePlaceholder()];
648
+ const stepElements = toolUseSteps.length > 0
649
+ ? toolUseSteps.flatMap((step) => buildToolUseStepElements(step))
650
+ : [buildToolUsePlaceholder()];
648
651
  return {
649
652
  tag: 'collapsible_panel',
650
653
  expanded: false,
@@ -670,12 +673,41 @@ function buildToolUsePanel(params) {
670
673
  icon_expanded_angle: -180,
671
674
  },
672
675
  border: { color: 'grey', corner_radius: '5px' },
673
- vertical_spacing: '8px',
676
+ vertical_spacing: '4px',
674
677
  padding: '8px 8px 8px 8px',
675
678
  elements: stepElements,
676
679
  };
677
680
  }
678
- function buildToolUseStepElement(step) {
681
+ function buildToolUseStepElements(step) {
682
+ const elements = [buildToolUseStepTitleElement(step)];
683
+ const detailElement = buildToolUseStepDetailElement(step);
684
+ if (detailElement) {
685
+ elements.push(detailElement);
686
+ }
687
+ const outputElement = buildToolUseStepOutputElement(step);
688
+ if (outputElement) {
689
+ elements.push(outputElement);
690
+ }
691
+ return elements;
692
+ }
693
+ function buildToolUsePlaceholder(labels) {
694
+ const zh = labels?.zh ?? '暂无工具步骤';
695
+ const en = labels?.en ?? tool_use_display_1.EMPTY_TOOL_USE_PLACEHOLDER;
696
+ return {
697
+ tag: 'div',
698
+ text: {
699
+ tag: 'plain_text',
700
+ content: en,
701
+ i18n_content: {
702
+ zh_cn: zh,
703
+ en_us: en,
704
+ },
705
+ text_color: 'grey',
706
+ text_size: 'notation',
707
+ },
708
+ };
709
+ }
710
+ function buildToolUseStepTitleElement(step) {
679
711
  return {
680
712
  tag: 'div',
681
713
  icon: {
@@ -684,27 +716,79 @@ function buildToolUseStepElement(step) {
684
716
  color: 'grey',
685
717
  },
686
718
  text: {
687
- tag: 'plain_text',
688
- content: step.detail ? `${step.title}\n${step.detail}` : step.title,
689
- text_color: 'grey',
719
+ tag: 'lark_md',
720
+ content: buildToolUseStepTitleMarkdown(step),
690
721
  text_size: 'notation',
691
722
  },
692
723
  };
693
724
  }
694
- function buildToolUsePlaceholder(labels) {
695
- const zh = labels?.zh ?? '暂无工具步骤';
696
- const en = labels?.en ?? tool_use_display_1.EMPTY_TOOL_USE_PLACEHOLDER;
725
+ function buildToolUseStepTitleMarkdown(step) {
726
+ const status = formatToolUseStepStatus(step.status);
727
+ return (0, markdown_style_1.optimizeMarkdownStyle)(`**${escapeToolUseMarkdownText(step.title)}** · <font color='${status.color}'>${status.label}</font>`, 1);
728
+ }
729
+ function buildToolUseStepDetailElement(step) {
730
+ const detail = step.detail?.trim();
731
+ if (!detail)
732
+ return undefined;
697
733
  return {
698
734
  tag: 'div',
735
+ margin: TOOL_USE_STEP_CONTENT_INDENT,
699
736
  text: {
700
737
  tag: 'plain_text',
701
- content: en,
702
- i18n_content: {
703
- zh_cn: zh,
704
- en_us: en,
705
- },
738
+ content: detail,
706
739
  text_color: 'grey',
707
740
  text_size: 'notation',
708
741
  },
709
742
  };
710
743
  }
744
+ function buildToolUseStepOutputElement(step) {
745
+ const content = buildToolUseStepOutputMarkdown(step);
746
+ if (!content)
747
+ return undefined;
748
+ return {
749
+ tag: 'div',
750
+ margin: TOOL_USE_STEP_CONTENT_INDENT,
751
+ text: {
752
+ tag: 'lark_md',
753
+ content,
754
+ text_size: 'notation',
755
+ },
756
+ };
757
+ }
758
+ function buildToolUseStepOutputMarkdown(step) {
759
+ const lines = [];
760
+ if (step.errorBlock) {
761
+ lines.push('**Error**');
762
+ lines.push(formatToolUseCodeBlock(step.errorBlock.content, step.errorBlock.language));
763
+ }
764
+ else if (step.resultBlock) {
765
+ lines.push('**Result**');
766
+ lines.push(formatToolUseCodeBlock(step.resultBlock.content, step.resultBlock.language));
767
+ }
768
+ if (lines.length === 0)
769
+ return undefined;
770
+ return (0, markdown_style_1.optimizeMarkdownStyle)(lines.join('\n'), 1);
771
+ }
772
+ function formatToolUseStepStatus(status) {
773
+ switch (status) {
774
+ case 'running':
775
+ return { label: 'Running', color: 'turquoise' };
776
+ case 'error':
777
+ return { label: 'Failed', color: 'red' };
778
+ case 'success':
779
+ default:
780
+ return { label: 'Succeeded', color: 'green' };
781
+ }
782
+ }
783
+ function formatToolUseCodeBlock(content, language) {
784
+ const normalized = content.replace(/\r\n/g, '\n').trim();
785
+ const fence = '`'.repeat(Math.max(3, longestBacktickRun(normalized) + 1));
786
+ return `${fence}${language}\n${normalized}\n${fence}`;
787
+ }
788
+ function longestBacktickRun(value) {
789
+ const matches = value.match(/`+/g) ?? [];
790
+ return matches.reduce((max, run) => Math.max(max, run.length), 0);
791
+ }
792
+ function escapeToolUseMarkdownText(value) {
793
+ return value.replace(/\\/g, '\\\\').replace(/([`*_{}[\]<>])/g, '\\$1');
794
+ }
@@ -30,8 +30,9 @@ function _optimizeMarkdownStyle(text, cardVersion = 2) {
30
30
  // ── 1. 提取代码块,用占位符保护,处理后再还原 ─────────────────────
31
31
  const MARK = '___CB_';
32
32
  const codeBlocks = [];
33
- let r = text.replace(/```[\s\S]*?```/g, (m) => {
34
- return `${MARK}${codeBlocks.push(m) - 1}___`;
33
+ let r = text.replace(/(^|\n)(`{3,})([^\n]*)\n[\s\S]*?\n\2(?=\n|$)/g, (m, prefix = '') => {
34
+ const block = m.slice(String(prefix).length);
35
+ return `${prefix}${MARK}${codeBlocks.push(block) - 1}___`;
35
36
  });
36
37
  // ── 2. 标题降级 ────────────────────────────────────────────────────
37
38
  // 只有当原文档包含 h1~h3 标题时才执行降级
@@ -5,10 +5,18 @@
5
5
  * Structured tool-use display for Lark/Feishu cards.
6
6
  */
7
7
  import type { ToolUseTraceStep } from './tool-use-trace-store';
8
+ export type ToolUseStepStatus = ToolUseTraceStep['status'];
9
+ export interface ToolUseDisplayBlock {
10
+ language: 'json' | 'text';
11
+ content: string;
12
+ }
8
13
  export interface ToolUseDisplayStep {
9
14
  title: string;
10
15
  detail?: string;
11
16
  iconToken: string;
17
+ status: ToolUseStepStatus;
18
+ resultBlock?: ToolUseDisplayBlock;
19
+ errorBlock?: ToolUseDisplayBlock;
12
20
  }
13
21
  export interface ToolUseDisplayResult {
14
22
  content: string;
@@ -142,6 +142,7 @@ function toTraceSource(step) {
142
142
  result: step.result,
143
143
  error: step.error,
144
144
  durationMs: step.durationMs,
145
+ status: step.status,
145
146
  };
146
147
  }
147
148
  function formatToolStep(source, options) {
@@ -151,11 +152,16 @@ function formatToolStep(source, options) {
151
152
  undefined;
152
153
  const detail = rawDetail ? sanitizeToolDetail(descriptor?.sanitizer ?? 'generic', rawDetail, options) : undefined;
153
154
  const title = buildToolTitle(source, descriptor, rawDetail);
154
- const meta = buildStepMeta(source, descriptor, options);
155
+ const status = resolveStepStatus(source);
156
+ const errorBlock = source.error ? buildErrorBlock(source.error, descriptor) : undefined;
157
+ const resultBlock = !errorBlock && options.showResultDetails ? buildResultBlock(source, descriptor) : undefined;
155
158
  return {
156
159
  title,
157
- detail: joinDetailParts(detail, meta),
160
+ detail,
158
161
  iconToken: descriptor?.iconToken ?? 'setting-inter_outlined',
162
+ status,
163
+ resultBlock,
164
+ errorBlock,
159
165
  };
160
166
  }
161
167
  function buildToolTitle(source, descriptor, rawDetail) {
@@ -218,35 +224,22 @@ function pickSummaryDetail(signals, preference) {
218
224
  }
219
225
  return undefined;
220
226
  }
221
- function buildStepMeta(source, descriptor, options) {
222
- const parts = [];
223
- if (source.error) {
224
- parts.push(`Failed: ${source.error}`);
225
- }
226
- else if (options.showResultDetails) {
227
- const resultDetail = buildResultDetail(source, descriptor, options);
228
- if (resultDetail) {
229
- parts.push(`Result: ${resultDetail}`);
230
- }
231
- }
232
- return parts.length > 0 ? parts.join(' · ') : undefined;
233
- }
234
- function joinDetailParts(detail, meta) {
235
- if (detail && meta)
236
- return `${detail} · ${meta}`;
237
- return detail ?? meta;
238
- }
239
- function buildResultDetail(source, descriptor, options) {
227
+ function buildResultBlock(source, descriptor) {
240
228
  if (source.result == null)
241
229
  return undefined;
242
230
  if (descriptor && ['Read', 'Edit', 'Fetch web page', 'Browser'].includes(descriptor.title)) {
243
231
  return undefined;
244
232
  }
245
- const raw = asDisplayText(source.result);
246
- const cleaned = descriptor
247
- ? sanitizeToolDetail(descriptor.sanitizer, raw, options)
248
- : sanitizeToolDetail('generic', raw, options);
249
- return cleaned || undefined;
233
+ return buildDisplayBlock(sanitizeDisplayBlockValue(source.result, descriptor));
234
+ }
235
+ function buildErrorBlock(error, descriptor) {
236
+ return buildDisplayBlock(sanitizeDisplayBlockValue(error, descriptor), 'text');
237
+ }
238
+ function sanitizeDisplayBlockValue(value, descriptor) {
239
+ if (descriptor?.sanitizer === 'command' && typeof value === 'string') {
240
+ return (0, reasoning_utils_1.redactInlineSecrets)(value);
241
+ }
242
+ return value;
250
243
  }
251
244
  function buildPatternDetail(params, options) {
252
245
  const pattern = extractScalarText(params.pattern);
@@ -264,6 +257,12 @@ function extractScalarText(value) {
264
257
  return undefined;
265
258
  }
266
259
  function sanitizeToolDetail(kind, value, options) {
260
+ if (kind === 'command') {
261
+ const cleaned = normalizeInlineDisplayText(value);
262
+ if (!cleaned)
263
+ return undefined;
264
+ return sanitizeCommandLike(cleaned, options);
265
+ }
267
266
  const cleaned = sanitizeGenericText(value);
268
267
  if (!cleaned)
269
268
  return undefined;
@@ -279,13 +278,14 @@ function sanitizeToolDetail(kind, value, options) {
279
278
  return stripQuotes(cleaned);
280
279
  case 'url':
281
280
  return stripQuotes(cleaned).replace(/^from\s+/i, '');
282
- case 'command':
283
- return sanitizeCommandLike(cleaned, options);
284
281
  case 'generic':
285
282
  default:
286
283
  return cleaned;
287
284
  }
288
285
  }
286
+ function normalizeInlineDisplayText(value) {
287
+ return value.replace(/\s+/g, ' ').trim();
288
+ }
289
289
  function sanitizePathLike(value, options) {
290
290
  const cleaned = sanitizeGenericText(value)
291
291
  .replace(/^(?:from|file|path)\s+/i, '')
@@ -309,6 +309,58 @@ function sanitizeCommandLike(value, options) {
309
309
  const redacted = (0, reasoning_utils_1.redactInlineSecrets)(cleaned);
310
310
  return options.showFullPaths ? redacted : redactCommandPaths(redacted);
311
311
  }
312
+ function resolveStepStatus(source) {
313
+ if (source.error)
314
+ return 'error';
315
+ if (source.status)
316
+ return source.status;
317
+ return 'success';
318
+ }
319
+ function buildDisplayBlock(value, fallbackLanguage = 'json') {
320
+ if (value == null)
321
+ return undefined;
322
+ if (typeof value === 'string') {
323
+ const normalized = value.replace(/\r\n/g, '\n').trim();
324
+ if (!normalized)
325
+ return undefined;
326
+ const parsed = tryParseJson(normalized);
327
+ if (parsed && typeof parsed === 'object') {
328
+ const prettyJson = stringifyJson(parsed);
329
+ if (prettyJson) {
330
+ return { language: 'json', content: prettyJson };
331
+ }
332
+ }
333
+ return { language: fallbackLanguage === 'json' ? 'text' : fallbackLanguage, content: normalized };
334
+ }
335
+ if (typeof value === 'object') {
336
+ const prettyJson = stringifyJson(value);
337
+ if (prettyJson) {
338
+ return { language: 'json', content: prettyJson };
339
+ }
340
+ }
341
+ const normalized = String(value).trim();
342
+ return normalized ? { language: 'text', content: normalized } : undefined;
343
+ }
344
+ function stringifyJson(value) {
345
+ try {
346
+ return JSON.stringify(value, null, 2);
347
+ }
348
+ catch {
349
+ return undefined;
350
+ }
351
+ }
352
+ function tryParseJson(value) {
353
+ const trimmed = value.trim();
354
+ if (!trimmed || !/^(?:\{|\[)/.test(trimmed)) {
355
+ return undefined;
356
+ }
357
+ try {
358
+ return JSON.parse(trimmed);
359
+ }
360
+ catch {
361
+ return undefined;
362
+ }
363
+ }
312
364
  function redactCommandPaths(command) {
313
365
  return command
314
366
  .split(/(\s+)/)
@@ -407,20 +459,6 @@ function humanizeToolName(name) {
407
459
  function formatDurationLabel(durationMs) {
408
460
  return durationMs < 1000 ? `${durationMs} ms` : `${(durationMs / 1000).toFixed(1)} s`;
409
461
  }
410
- function asDisplayText(value) {
411
- if (typeof value === 'string')
412
- return value;
413
- if (value == null)
414
- return '';
415
- if (typeof value !== 'object')
416
- return String(value);
417
- try {
418
- return JSON.stringify(value);
419
- }
420
- catch {
421
- return '';
422
- }
423
- }
424
462
  function stripQuotes(value) {
425
463
  return value.replace(/^[`'"]+|[`'"]+$/g, '').trim();
426
464
  }
@@ -26,6 +26,7 @@ const auto_auth_1 = require("../tools/auto-auth.js");
26
26
  const ask_user_question_1 = require("../tools/ask-user-question.js");
27
27
  const chat_queue_1 = require("./chat-queue.js");
28
28
  const abort_detect_1 = require("./abort-detect.js");
29
+ const interactive_dispatch_1 = require("./interactive-dispatch.js");
29
30
  const elog = (0, lark_logger_1.larkLogger)('channel/event-handlers');
30
31
  // ---------------------------------------------------------------------------
31
32
  // Event ownership validation
@@ -274,13 +275,16 @@ async function handleCommentEvent(ctx, data) {
274
275
  // ---------------------------------------------------------------------------
275
276
  async function handleCardActionEvent(ctx, data) {
276
277
  try {
277
- // AskUserQuestion card interactions — injects synthetic message
278
- // carrying user answers for the AI to receive in a new turn.
278
+ // AskUserQuestion:表单卡片交互(宿主内建能力优先)
279
279
  const askResult = (0, ask_user_question_1.handleAskUserAction)(data, ctx.cfg, ctx.accountId);
280
280
  if (askResult !== undefined)
281
281
  return askResult;
282
- // Auto-auth card actions (OAuth device flow, app scope confirmation)
283
- return await (0, auto_auth_1.handleCardAction)(data, ctx.cfg, ctx.accountId);
282
+ // auto-auth:授权/权限引导相关卡片交互(宿主内建能力优先)
283
+ const authResult = await (0, auto_auth_1.handleCardAction)(data, ctx.cfg, ctx.accountId);
284
+ if (authResult !== undefined)
285
+ return authResult;
286
+ // 业务自定义卡片交互:使用 SDK 标准 interactive dispatch 管道转发给业务插件。
287
+ return await (0, interactive_dispatch_1.dispatchFeishuPluginInteractiveHandler)({ cfg: ctx.cfg, accountId: ctx.accountId, data });
284
288
  }
285
289
  catch (err) {
286
290
  elog.warn(`card.action.trigger handler error: ${err}`);
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Feishu interactive dispatch wrapper.
6
+ *
7
+ * This module adapts Feishu `card.action.trigger` events into OpenClaw's
8
+ * standard interactive dispatch pipeline:
9
+ * - Plugins register via `api.registerInteractiveHandler({ channel, namespace, handler })`
10
+ * - Channel forwards via `dispatchPluginInteractiveHandler()`
11
+ *
12
+ * We intentionally do NOT maintain any channel-local global registry here.
13
+ */
14
+ import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
15
+ export type FeishuInteractiveHandlerResponse = unknown;
16
+ export interface FeishuInteractiveHandlerContext {
17
+ channel: 'feishu';
18
+ accountId: string;
19
+ senderId?: string;
20
+ conversationId?: string;
21
+ messageId?: string;
22
+ namespace: string;
23
+ payload: string;
24
+ action: string;
25
+ rawEvent: unknown;
26
+ respond: {
27
+ reply: (args: {
28
+ text: string;
29
+ }) => Promise<void>;
30
+ followUp: (args: {
31
+ text: string;
32
+ }) => Promise<void>;
33
+ /**
34
+ * Best-effort "edit current message" mapping.
35
+ * In Feishu, we prefer updating the original interactive card when possible.
36
+ */
37
+ editMessage: (args: {
38
+ text?: string;
39
+ blocks?: unknown[];
40
+ }) => Promise<void>;
41
+ };
42
+ }
43
+ /**
44
+ * Dispatch a Feishu interactive card action to business plugins through
45
+ * the OpenClaw SDK's standard interactive dispatch pipeline.
46
+ *
47
+ * Returns `undefined` when:
48
+ * - the event does not look like an interactive action we can route, or
49
+ * - no plugin handler is registered for the derived namespace.
50
+ *
51
+ * @param params.cfg - OpenClaw config snapshot.
52
+ * @param params.accountId - Current Feishu account id.
53
+ * @param params.data - Raw `card.action.trigger` event payload.
54
+ */
55
+ export declare function dispatchFeishuPluginInteractiveHandler(params: {
56
+ cfg: ClawdbotConfig;
57
+ accountId: string;
58
+ data: unknown;
59
+ }): Promise<unknown | undefined>;
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Feishu interactive dispatch wrapper.
7
+ *
8
+ * This module adapts Feishu `card.action.trigger` events into OpenClaw's
9
+ * standard interactive dispatch pipeline:
10
+ * - Plugins register via `api.registerInteractiveHandler({ channel, namespace, handler })`
11
+ * - Channel forwards via `dispatchPluginInteractiveHandler()`
12
+ *
13
+ * We intentionally do NOT maintain any channel-local global registry here.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.dispatchFeishuPluginInteractiveHandler = dispatchFeishuPluginInteractiveHandler;
17
+ // NOTE: This is the SDK-standard interactive pipeline.
18
+ const plugin_runtime_1 = require("openclaw/plugin-sdk/plugin-runtime");
19
+ const lark_logger_1 = require("../core/lark-logger.js");
20
+ const send_1 = require("../messaging/outbound/send.js");
21
+ const log = (0, lark_logger_1.larkLogger)('channel/interactive-dispatch');
22
+ function extractBasics(data) {
23
+ try {
24
+ const ev = data;
25
+ const action = ev.action?.value?.action;
26
+ if (!action || typeof action !== 'string')
27
+ return null;
28
+ const openChatId = ev.open_chat_id ?? ev.context?.open_chat_id;
29
+ const openMessageId = ev.open_message_id ?? ev.context?.open_message_id;
30
+ return {
31
+ action: action.trim(),
32
+ senderOpenId: ev.operator?.open_id,
33
+ openChatId,
34
+ openMessageId,
35
+ };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function buildMarkdownCard(text) {
42
+ return {
43
+ schema: '2.0',
44
+ body: {
45
+ elements: [
46
+ {
47
+ tag: 'markdown',
48
+ content: text,
49
+ },
50
+ ],
51
+ },
52
+ };
53
+ }
54
+ /**
55
+ * Dispatch a Feishu interactive card action to business plugins through
56
+ * the OpenClaw SDK's standard interactive dispatch pipeline.
57
+ *
58
+ * Returns `undefined` when:
59
+ * - the event does not look like an interactive action we can route, or
60
+ * - no plugin handler is registered for the derived namespace.
61
+ *
62
+ * @param params.cfg - OpenClaw config snapshot.
63
+ * @param params.accountId - Current Feishu account id.
64
+ * @param params.data - Raw `card.action.trigger` event payload.
65
+ */
66
+ async function dispatchFeishuPluginInteractiveHandler(params) {
67
+ const basics = extractBasics(params.data);
68
+ if (!basics)
69
+ return undefined;
70
+ if (!basics.action)
71
+ return undefined;
72
+ const respond = {
73
+ reply: async (args) => {
74
+ if (!basics.openChatId || !String(args?.text || '').trim())
75
+ return;
76
+ await (0, send_1.sendMessageFeishu)({
77
+ cfg: params.cfg,
78
+ to: basics.openChatId,
79
+ text: String(args?.text || ''),
80
+ replyToMessageId: basics.openMessageId,
81
+ accountId: params.accountId,
82
+ replyInThread: false,
83
+ });
84
+ },
85
+ followUp: async (args) => {
86
+ if (!basics.openChatId || !String(args?.text || '').trim())
87
+ return;
88
+ await (0, send_1.sendMessageFeishu)({
89
+ cfg: params.cfg,
90
+ to: basics.openChatId,
91
+ text: String(args?.text || ''),
92
+ replyToMessageId: basics.openMessageId,
93
+ accountId: params.accountId,
94
+ replyInThread: false,
95
+ });
96
+ },
97
+ editMessage: async (args) => {
98
+ if (!basics.openMessageId) {
99
+ if (Array.isArray(args?.blocks) && args.blocks.length && basics.openChatId) {
100
+ await (0, send_1.sendCardFeishu)({
101
+ cfg: params.cfg,
102
+ to: basics.openChatId,
103
+ card: { schema: '2.0', body: { elements: args.blocks } },
104
+ replyToMessageId: basics.openMessageId,
105
+ accountId: params.accountId,
106
+ replyInThread: false,
107
+ });
108
+ return;
109
+ }
110
+ if (typeof args?.text === 'string' && args.text.trim() && basics.openChatId) {
111
+ await (0, send_1.sendMessageFeishu)({
112
+ cfg: params.cfg,
113
+ to: basics.openChatId,
114
+ text: args.text,
115
+ replyToMessageId: basics.openMessageId,
116
+ accountId: params.accountId,
117
+ replyInThread: false,
118
+ });
119
+ }
120
+ return;
121
+ }
122
+ if (Array.isArray(args?.blocks) && args.blocks.length) {
123
+ await (0, send_1.updateCardFeishu)({
124
+ cfg: params.cfg,
125
+ messageId: basics.openMessageId,
126
+ card: { schema: '2.0', body: { elements: args.blocks } },
127
+ accountId: params.accountId,
128
+ });
129
+ return;
130
+ }
131
+ if (typeof args?.text === 'string' && args.text.trim()) {
132
+ await (0, send_1.updateCardFeishu)({
133
+ cfg: params.cfg,
134
+ messageId: basics.openMessageId,
135
+ card: buildMarkdownCard(args.text),
136
+ accountId: params.accountId,
137
+ });
138
+ return;
139
+ }
140
+ await (0, send_1.updateCardFeishu)({
141
+ cfg: params.cfg,
142
+ messageId: basics.openMessageId,
143
+ card: { schema: '2.0', body: { elements: [] } },
144
+ accountId: params.accountId,
145
+ });
146
+ },
147
+ };
148
+ try {
149
+ const dedupeId = `feishu:${params.accountId}:${basics.openChatId ?? '-'}:${basics.openMessageId ?? '-'}:${basics.senderOpenId ?? '-'}:${basics.action}`;
150
+ let cardResponse;
151
+ const result = await (0, plugin_runtime_1.dispatchPluginInteractiveHandler)({
152
+ channel: 'feishu',
153
+ data: basics.action,
154
+ dedupeId,
155
+ invoke: async (match) => {
156
+ const { registration, namespace, payload } = match;
157
+ const handlerCtx = {
158
+ channel: 'feishu',
159
+ accountId: params.accountId,
160
+ senderId: basics.senderOpenId,
161
+ conversationId: basics.openChatId,
162
+ messageId: basics.openMessageId,
163
+ namespace,
164
+ payload,
165
+ action: basics.action,
166
+ rawEvent: params.data,
167
+ respond,
168
+ };
169
+ cardResponse = await registration.handler(handlerCtx);
170
+ // If the handler returns a card response, treat it as handled.
171
+ return { handled: cardResponse !== undefined };
172
+ },
173
+ });
174
+ if (!result.matched)
175
+ return undefined;
176
+ return cardResponse;
177
+ }
178
+ catch (err) {
179
+ log.warn(`interactive dispatch failed: ${String(err)}`);
180
+ return {
181
+ toast: {
182
+ type: 'error',
183
+ content: '交互处理失败,请稍后重试',
184
+ },
185
+ };
186
+ }
187
+ }