@larksuite/openclaw-lark 2026.4.7-beta.0 → 2026.4.8-beta.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/package.json +1 -1
- package/src/card/builder.js +101 -17
- package/src/card/markdown-style.js +3 -2
- package/src/card/tool-use-display.d.ts +8 -0
- package/src/card/tool-use-display.js +80 -42
- package/src/channel/event-handlers.js +10 -8
- package/src/channel/interactive-dispatch.d.ts +59 -0
- package/src/channel/interactive-dispatch.js +187 -0
- package/src/messaging/outbound/deliver.js +1 -1
- package/src/tools/oapi/drive/doc-comments.js +1 -1
package/package.json
CHANGED
package/src/card/builder.js
CHANGED
|
@@ -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: '
|
|
593
|
+
vertical_spacing: '4px',
|
|
593
594
|
padding: '8px 8px 8px 8px',
|
|
594
|
-
elements: steps.
|
|
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: '
|
|
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
|
|
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: '
|
|
676
|
+
vertical_spacing: '4px',
|
|
674
677
|
padding: '8px 8px 8px 8px',
|
|
675
678
|
elements: stepElements,
|
|
676
679
|
};
|
|
677
680
|
}
|
|
678
|
-
function
|
|
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: '
|
|
688
|
-
content: step
|
|
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
|
|
695
|
-
const
|
|
696
|
-
|
|
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:
|
|
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(
|
|
34
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
@@ -237,7 +238,7 @@ async function handleCommentEvent(ctx, data) {
|
|
|
237
238
|
const commentId = parsed.comment_id ?? '';
|
|
238
239
|
const replyId = parsed.reply_id ?? '';
|
|
239
240
|
// Parser has normalized notice_meta fields into canonical top-level fields
|
|
240
|
-
const
|
|
241
|
+
const _senderOpenId = parsed.user_id?.open_id ?? '';
|
|
241
242
|
const isMentioned = parsed.is_mention ?? false;
|
|
242
243
|
const eventTimestamp = parsed.action_time;
|
|
243
244
|
log(`feishu[${accountId}]: drive comment event: ` +
|
|
@@ -245,9 +246,7 @@ async function handleCommentEvent(ctx, data) {
|
|
|
245
246
|
`${replyId ? `, reply=${replyId}` : ''}` +
|
|
246
247
|
`${isMentioned ? ', @bot' : ''}`);
|
|
247
248
|
// Dedup: build a deterministic key from the comment/reply IDs
|
|
248
|
-
const dedupKey = replyId
|
|
249
|
-
? `comment:${commentId}:reply:${replyId}`
|
|
250
|
-
: `comment:${commentId}`;
|
|
249
|
+
const dedupKey = replyId ? `comment:${commentId}:reply:${replyId}` : `comment:${commentId}`;
|
|
251
250
|
if (!ctx.messageDedup.tryRecord(dedupKey, accountId)) {
|
|
252
251
|
log(`feishu[${accountId}]: duplicate comment event ${dedupKey}, skipping`);
|
|
253
252
|
return;
|
|
@@ -276,13 +275,16 @@ async function handleCommentEvent(ctx, data) {
|
|
|
276
275
|
// ---------------------------------------------------------------------------
|
|
277
276
|
async function handleCardActionEvent(ctx, data) {
|
|
278
277
|
try {
|
|
279
|
-
// AskUserQuestion
|
|
280
|
-
// carrying user answers for the AI to receive in a new turn.
|
|
278
|
+
// AskUserQuestion:表单卡片交互(宿主内建能力优先)
|
|
281
279
|
const askResult = (0, ask_user_question_1.handleAskUserAction)(data, ctx.cfg, ctx.accountId);
|
|
282
280
|
if (askResult !== undefined)
|
|
283
281
|
return askResult;
|
|
284
|
-
//
|
|
285
|
-
|
|
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 });
|
|
286
288
|
}
|
|
287
289
|
catch (err) {
|
|
288
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
|
+
}
|
|
@@ -397,7 +397,7 @@ async function sendCommentReplyLark(params) {
|
|
|
397
397
|
catch (secondErr) {
|
|
398
398
|
const detail = (0, api_error_1.formatLarkError)(firstErr);
|
|
399
399
|
log.error(`sendCommentReplyLark failed: ${detail}`);
|
|
400
|
-
throw new Error(`Comment reply failed: ${detail}`, { cause:
|
|
400
|
+
throw new Error(`Comment reply failed: ${detail}`, { cause: secondErr });
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
}
|
|
@@ -296,7 +296,7 @@ function registerDocCommentsTool(api) {
|
|
|
296
296
|
data: { content: { elements: sdkElements } },
|
|
297
297
|
}), { as: 'tenant' });
|
|
298
298
|
}
|
|
299
|
-
catch (
|
|
299
|
+
catch (_firstErr) {
|
|
300
300
|
// Fallback: 部分 API 版本使用 reply_elements 格式
|
|
301
301
|
log.info(`doc_comments.reply: first attempt failed, trying reply_elements format`);
|
|
302
302
|
res = await client.invoke('feishu_doc_comments.reply', (sdk) => sdk.request({
|