@larksuite/openclaw-lark 2026.4.1 → 2026.4.7

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.
Files changed (43) hide show
  1. package/index.js +25 -6
  2. package/package.json +1 -1
  3. package/src/card/builder.d.ts +29 -7
  4. package/src/card/builder.js +241 -29
  5. package/src/card/reasoning-utils.d.ts +14 -0
  6. package/src/card/reasoning-utils.js +64 -0
  7. package/src/card/reply-dispatcher-types.d.ts +12 -15
  8. package/src/card/reply-dispatcher-types.js +1 -0
  9. package/src/card/reply-dispatcher.js +63 -11
  10. package/src/card/streaming-card-controller.d.ts +15 -0
  11. package/src/card/streaming-card-controller.js +188 -65
  12. package/src/card/tool-use-config.d.ts +26 -0
  13. package/src/card/tool-use-config.js +76 -0
  14. package/src/card/tool-use-display.d.ts +29 -0
  15. package/src/card/tool-use-display.js +438 -0
  16. package/src/card/tool-use-trace-store.d.ts +51 -0
  17. package/src/card/tool-use-trace-store.js +271 -0
  18. package/src/channel/event-handlers.d.ts +1 -0
  19. package/src/channel/event-handlers.js +51 -0
  20. package/src/channel/monitor.js +2 -0
  21. package/src/core/comment-target.d.ts +65 -0
  22. package/src/core/comment-target.js +100 -0
  23. package/src/core/config-schema.d.ts +9 -0
  24. package/src/core/config-schema.js +5 -0
  25. package/src/core/tool-scopes.d.ts +1 -1
  26. package/src/core/tool-scopes.js +7 -0
  27. package/src/messaging/inbound/comment-context.d.ts +82 -0
  28. package/src/messaging/inbound/comment-context.js +353 -0
  29. package/src/messaging/inbound/comment-handler.d.ts +30 -0
  30. package/src/messaging/inbound/comment-handler.js +269 -0
  31. package/src/messaging/inbound/dispatch-commands.js +23 -2
  32. package/src/messaging/inbound/dispatch-context.js +19 -7
  33. package/src/messaging/inbound/dispatch.js +87 -8
  34. package/src/messaging/outbound/deliver.d.ts +29 -0
  35. package/src/messaging/outbound/deliver.js +94 -0
  36. package/src/messaging/outbound/outbound.js +19 -0
  37. package/src/messaging/types.d.ts +63 -0
  38. package/src/tools/oapi/drive/doc-comments.js +93 -24
  39. package/src/tools/oapi/index.js +1 -0
  40. package/src/tools/oapi/task/index.d.ts +1 -0
  41. package/src/tools/oapi/task/index.js +3 -1
  42. package/src/tools/oapi/task/section.d.ts +17 -0
  43. package/src/tools/oapi/task/section.js +285 -0
package/index.js CHANGED
@@ -22,6 +22,8 @@ const diagnose_1 = require("./src/commands/diagnose.js");
22
22
  const index_3 = require("./src/commands/index.js");
23
23
  const lark_logger_1 = require("./src/core/lark-logger.js");
24
24
  const security_check_1 = require("./src/core/security-check.js");
25
+ const tool_use_trace_store_1 = require("./src/card/tool-use-trace-store.js");
26
+ const reasoning_utils_1 = require("./src/card/reasoning-utils.js");
25
27
  const log = (0, lark_logger_1.larkLogger)('plugin');
26
28
  // ---------------------------------------------------------------------------
27
29
  // Re-exports for external consumers
@@ -105,20 +107,37 @@ const plugin = {
105
107
  (0, oauth_batch_auth_1.registerFeishuOAuthBatchAuthTool)(api);
106
108
  // Register AskUserQuestion tool (interactive card-based user prompting)
107
109
  (0, ask_user_question_1.registerAskUserQuestionTool)(api);
108
- // ---- Tool call hooks (trace Feishu-owned tool invocations only) ----
109
- api.on('before_tool_call', (event) => {
110
+ api.on('before_tool_call', (event, ctx) => {
111
+ (0, tool_use_trace_store_1.recordToolUseStart)({
112
+ sessionKey: ctx.sessionKey,
113
+ toolName: event.toolName,
114
+ toolParams: event.params,
115
+ toolCallId: event.toolCallId ?? ctx.toolCallId,
116
+ runId: event.runId ?? ctx.runId,
117
+ });
110
118
  if (!event.toolName.startsWith('feishu_'))
111
119
  return;
112
- log.info(`tool call: ${event.toolName} params=${JSON.stringify(event.params)}`);
120
+ const paramsPreview = (0, reasoning_utils_1.sanitizeParamsForLog)(event.params);
121
+ log.info(`tool call: ${event.toolName} session=${ctx.sessionKey ?? '-'} params=${paramsPreview}`);
113
122
  });
114
- api.on('after_tool_call', (event) => {
123
+ api.on('after_tool_call', (event, ctx) => {
124
+ (0, tool_use_trace_store_1.recordToolUseEnd)({
125
+ sessionKey: ctx.sessionKey,
126
+ toolName: event.toolName,
127
+ toolParams: event.params,
128
+ toolCallId: event.toolCallId ?? ctx.toolCallId,
129
+ runId: event.runId ?? ctx.runId,
130
+ result: event.result,
131
+ error: event.error,
132
+ durationMs: event.durationMs,
133
+ });
115
134
  if (!event.toolName.startsWith('feishu_'))
116
135
  return;
117
136
  if (event.error) {
118
- log.error(`tool fail: ${event.toolName} ${event.error} (${event.durationMs ?? 0}ms)`);
137
+ log.error(`tool fail: ${event.toolName} session=${ctx.sessionKey ?? '-'} ${event.error} (${event.durationMs ?? 0}ms)`);
119
138
  }
120
139
  else {
121
- log.info(`tool done: ${event.toolName} ok (${event.durationMs ?? 0}ms)`);
140
+ log.info(`tool done: ${event.toolName} session=${ctx.sessionKey ?? '-'} ok (${event.durationMs ?? 0}ms)`);
122
141
  }
123
142
  });
124
143
  // ---- Diagnostic commands ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larksuite/openclaw-lark",
3
- "version": "2026.4.1",
3
+ "version": "2026.4.7",
4
4
  "description": "OpenClaw Lark/Feishu channel plugin",
5
5
  "exports": {
6
6
  ".": {
@@ -8,6 +8,7 @@
8
8
  * different agent response states (thinking, streaming, complete, confirm).
9
9
  */
10
10
  import type { FooterSessionMetrics } from './reply-dispatcher-types';
11
+ import { type ToolUseDisplayStep } from './tool-use-display';
11
12
  /**
12
13
  * Element ID used for the streaming text area in cards. The CardKit
13
14
  * `cardElement.content()` API targets this element for typewriter-effect
@@ -15,12 +16,6 @@ import type { FooterSessionMetrics } from './reply-dispatcher-types';
15
16
  */
16
17
  export declare const STREAMING_ELEMENT_ID = "streaming_content";
17
18
  export declare const REASONING_ELEMENT_ID = "reasoning_content";
18
- export interface ToolCallInfo {
19
- name: string;
20
- status: 'running' | 'complete' | 'error';
21
- args?: Record<string, unknown>;
22
- result?: string;
23
- }
24
19
  export interface CardElement {
25
20
  tag: string;
26
21
  [key: string]: unknown;
@@ -76,6 +71,13 @@ export declare function formatReasoningDuration(ms: number): {
76
71
  zh: string;
77
72
  en: string;
78
73
  };
74
+ /**
75
+ * Format tool-use duration into a human-readable i18n pair.
76
+ */
77
+ export declare function formatToolUseDuration(ms: number): {
78
+ zh: string;
79
+ en: string;
80
+ };
79
81
  /**
80
82
  * Format milliseconds into a human-readable duration string.
81
83
  */
@@ -108,7 +110,13 @@ export declare function buildCardContent(state: CardState, data?: {
108
110
  text?: string;
109
111
  reasoningText?: string;
110
112
  reasoningElapsedMs?: number;
111
- toolCalls?: ToolCallInfo[];
113
+ toolUseSteps?: ToolUseDisplayStep[];
114
+ toolUseTitleSuffix?: {
115
+ zh: string;
116
+ en: string;
117
+ };
118
+ toolUseElapsedMs?: number;
119
+ showToolUse?: boolean;
112
120
  confirmData?: ConfirmData;
113
121
  elapsedMs?: number;
114
122
  isError?: boolean;
@@ -127,4 +135,18 @@ export declare function buildCardContent(state: CardState, data?: {
127
135
  * Convert an old-format FeishuCard to CardKit JSON 2.0 format.
128
136
  * JSON 2.0 uses `body.elements` instead of top-level `elements`.
129
137
  */
138
+ /**
139
+ * Build the initial CardKit 2.0 streaming card with a loading icon.
140
+ * Optionally includes a tool-use pending panel above the streaming area.
141
+ */
142
+ export declare function buildStreamingThinkingCard(showToolUse?: boolean): Record<string, unknown>;
143
+ /**
144
+ * Build a CardKit 2.0 card for the pre-answer streaming phase.
145
+ * Used both for the initial card and for live updates during tool calls.
146
+ */
147
+ export declare function buildStreamingPreAnswerCard(params: {
148
+ steps?: ToolUseDisplayStep[];
149
+ elapsedMs?: number;
150
+ showToolUse?: boolean;
151
+ }): Record<string, unknown>;
130
152
  export declare function toCardKit2(card: FeishuCard): Record<string, unknown>;
@@ -13,12 +13,16 @@ exports.REASONING_ELEMENT_ID = exports.STREAMING_ELEMENT_ID = void 0;
13
13
  exports.splitReasoningText = splitReasoningText;
14
14
  exports.stripReasoningTags = stripReasoningTags;
15
15
  exports.formatReasoningDuration = formatReasoningDuration;
16
+ exports.formatToolUseDuration = formatToolUseDuration;
16
17
  exports.formatElapsed = formatElapsed;
17
18
  exports.compactNumber = compactNumber;
18
19
  exports.formatFooterRuntimeSegments = formatFooterRuntimeSegments;
19
20
  exports.buildCardContent = buildCardContent;
21
+ exports.buildStreamingThinkingCard = buildStreamingThinkingCard;
22
+ exports.buildStreamingPreAnswerCard = buildStreamingPreAnswerCard;
20
23
  exports.toCardKit2 = toCardKit2;
21
24
  const markdown_style_1 = require("./markdown-style.js");
25
+ const tool_use_display_1 = require("./tool-use-display.js");
22
26
  // ---------------------------------------------------------------------------
23
27
  // Constants
24
28
  // ---------------------------------------------------------------------------
@@ -124,6 +128,13 @@ function formatReasoningDuration(ms) {
124
128
  const d = formatElapsed(ms);
125
129
  return { zh: `思考了 ${d}`, en: `Thought for ${d}` };
126
130
  }
131
+ /**
132
+ * Format tool-use duration into a human-readable i18n pair.
133
+ */
134
+ function formatToolUseDuration(ms) {
135
+ const d = formatElapsed(ms);
136
+ return { zh: `执行耗时 ${d}`, en: `Tool use for ${d}` };
137
+ }
127
138
  /**
128
139
  * Format milliseconds into a human-readable duration string.
129
140
  */
@@ -243,15 +254,23 @@ function buildCardContent(state, data = {}) {
243
254
  case 'thinking':
244
255
  return buildThinkingCard();
245
256
  case 'streaming':
246
- return buildStreamingCard(data.text ?? '', data.toolCalls ?? [], data.reasoningText);
257
+ return buildStreamingCard(data.text ?? '', {
258
+ reasoningText: data.reasoningText,
259
+ showToolUse: data.showToolUse,
260
+ toolUseSteps: data.toolUseSteps,
261
+ toolUseTitleSuffix: data.toolUseTitleSuffix,
262
+ });
247
263
  case 'complete':
248
264
  return buildCompleteCard({
249
265
  text: data.text ?? '',
250
- toolCalls: data.toolCalls ?? [],
251
266
  elapsedMs: data.elapsedMs,
252
267
  isError: data.isError,
253
268
  reasoningText: data.reasoningText,
254
269
  reasoningElapsedMs: data.reasoningElapsedMs,
270
+ toolUseSteps: data.toolUseSteps,
271
+ toolUseTitleSuffix: data.toolUseTitleSuffix,
272
+ toolUseElapsedMs: data.toolUseElapsedMs,
273
+ showToolUse: data.showToolUse,
255
274
  isAborted: data.isAborted,
256
275
  footer: data.footer,
257
276
  footerMetrics: data.footerMetrics,
@@ -277,8 +296,18 @@ function buildThinkingCard() {
277
296
  ],
278
297
  };
279
298
  }
280
- function buildStreamingCard(partialText, toolCalls, reasoningText) {
299
+ function buildStreamingCard(partialText, params = {}) {
300
+ const { showToolUse = true, toolUseSteps, toolUseTitleSuffix, reasoningText } = params;
281
301
  const elements = [];
302
+ const hasToolUse = Boolean(toolUseSteps?.length);
303
+ if (showToolUse) {
304
+ elements.push(hasToolUse
305
+ ? buildToolUsePanel({
306
+ toolUseSteps,
307
+ titleSuffix: toolUseTitleSuffix,
308
+ })
309
+ : buildStreamingToolUsePendingPanel());
310
+ }
282
311
  if (!partialText && reasoningText) {
283
312
  // Reasoning phase: show reasoning content in notation style
284
313
  elements.push({
@@ -298,26 +327,21 @@ function buildStreamingCard(partialText, toolCalls, reasoningText) {
298
327
  content: (0, markdown_style_1.optimizeMarkdownStyle)(partialText),
299
328
  });
300
329
  }
301
- // Tool calls in progress
302
- if (toolCalls.length > 0) {
303
- const toolLines = toolCalls.map((tc) => {
304
- const statusIcon = tc.status === 'running' ? '\ud83d\udd04' : tc.status === 'complete' ? '\u2705' : '\u274c';
305
- return `${statusIcon} ${tc.name} - ${tc.status}`;
306
- });
307
- elements.push({
308
- tag: 'markdown',
309
- content: toolLines.join('\n'),
310
- text_size: 'notation',
311
- });
312
- }
313
330
  return {
314
331
  config: { wide_screen_mode: true, update_multi: true, locales: ['zh_cn', 'en_us'] },
315
332
  elements,
316
333
  };
317
334
  }
318
335
  function buildCompleteCard(params) {
319
- const { text, toolCalls, elapsedMs, isError, reasoningText, reasoningElapsedMs, isAborted, footer, footerMetrics } = params;
336
+ const { text, elapsedMs, isError, reasoningText, reasoningElapsedMs, toolUseSteps, toolUseTitleSuffix, toolUseElapsedMs, showToolUse = true, isAborted, footer, footerMetrics, } = params;
320
337
  const elements = [];
338
+ if (showToolUse) {
339
+ elements.push(buildToolUsePanel({
340
+ toolUseSteps,
341
+ toolUseElapsedMs,
342
+ titleSuffix: toolUseTitleSuffix,
343
+ }));
344
+ }
321
345
  // Collapsible reasoning panel (before main content)
322
346
  if (reasoningText) {
323
347
  const dur = reasoningElapsedMs ? formatReasoningDuration(reasoningElapsedMs) : null;
@@ -361,18 +385,6 @@ function buildCompleteCard(params) {
361
385
  tag: 'markdown',
362
386
  content: (0, markdown_style_1.optimizeMarkdownStyle)(text),
363
387
  });
364
- // Tool calls summary
365
- if (toolCalls.length > 0) {
366
- const toolSummaryLines = toolCalls.map((tc) => {
367
- const statusIcon = tc.status === 'complete' ? '\u2705' : '\u274c';
368
- return `${statusIcon} **${tc.name}** - ${tc.status}`;
369
- });
370
- elements.push({
371
- tag: 'markdown',
372
- content: toolSummaryLines.join('\n'),
373
- text_size: 'notation',
374
- });
375
- }
376
388
  // Footer meta-info: split into two lines for readability.
377
389
  // Line 1 (primary): status · elapsed · model
378
390
  // Line 2 (detail): tokens · cache · context
@@ -396,7 +408,7 @@ function buildCompleteCard(params) {
396
408
  if (footerZhLines.length > 0) {
397
409
  elements.push(...buildFooter(footerZhLines.join('\n'), footerEnLines.join('\n'), isError));
398
410
  }
399
- // Use the answer text (not reasoning) as the feed preview summary.
411
+ // Use the answer text as the feed preview summary.
400
412
  // Strip markdown syntax so the preview reads as plain text.
401
413
  const summaryText = text.replace(/[*_`#>[\]()~]/g, '').trim();
402
414
  const summary = summaryText ? { content: summaryText.slice(0, 120) } : undefined;
@@ -486,6 +498,102 @@ function buildConfirmCard(confirmData) {
486
498
  * Convert an old-format FeishuCard to CardKit JSON 2.0 format.
487
499
  * JSON 2.0 uses `body.elements` instead of top-level `elements`.
488
500
  */
501
+ /**
502
+ * Build the initial CardKit 2.0 streaming card with a loading icon.
503
+ * Optionally includes a tool-use pending panel above the streaming area.
504
+ */
505
+ function buildStreamingThinkingCard(showToolUse = true) {
506
+ return buildStreamingPreAnswerCard({ showToolUse });
507
+ }
508
+ /**
509
+ * Build a CardKit 2.0 card for the pre-answer streaming phase.
510
+ * Used both for the initial card and for live updates during tool calls.
511
+ */
512
+ function buildStreamingPreAnswerCard(params) {
513
+ const { steps, elapsedMs, showToolUse = true } = params;
514
+ const hasSteps = Boolean(steps?.length);
515
+ const elements = [];
516
+ if (showToolUse) {
517
+ elements.push(hasSteps ? buildStreamingToolUseActivePanel({ steps: steps, elapsedMs }) : buildStreamingToolUsePendingPanel());
518
+ }
519
+ elements.push({
520
+ tag: 'markdown',
521
+ content: '',
522
+ text_align: 'left',
523
+ text_size: 'normal_v2',
524
+ margin: '0px 0px 0px 0px',
525
+ element_id: exports.STREAMING_ELEMENT_ID,
526
+ });
527
+ elements.push({
528
+ tag: 'markdown',
529
+ content: ' ',
530
+ icon: {
531
+ tag: 'custom_icon',
532
+ img_key: 'img_v3_02vb_496bec09-4b43-4773-ad6b-0cdd103cd2bg',
533
+ size: '16px 16px',
534
+ },
535
+ element_id: 'loading_icon',
536
+ });
537
+ return {
538
+ schema: '2.0',
539
+ config: {
540
+ streaming_mode: true,
541
+ locales: ['zh_cn', 'en_us'],
542
+ summary: {
543
+ content: 'Processing...',
544
+ i18n_content: { zh_cn: '处理中...', en_us: 'Processing...' },
545
+ },
546
+ },
547
+ body: { elements },
548
+ };
549
+ }
550
+ /**
551
+ * Build the collapsible panel for the active pre-answer phase.
552
+ * Used by buildStreamingPreAnswerCard when at least one step exists.
553
+ */
554
+ function buildStreamingToolUseActivePanel(params) {
555
+ const { steps, elapsedMs } = params;
556
+ const enParts = ['Tool use'];
557
+ const zhParts = ['工具执行'];
558
+ if (steps.length > 0) {
559
+ enParts.push(`${steps.length} step${steps.length === 1 ? '' : 's'}`);
560
+ zhParts.push(`${steps.length} 步`);
561
+ }
562
+ if (elapsedMs != null && elapsedMs > 0) {
563
+ const d = formatElapsed(elapsedMs);
564
+ enParts.push(`(${d})`);
565
+ zhParts.push(`(${d})`);
566
+ }
567
+ return {
568
+ tag: 'collapsible_panel',
569
+ expanded: true,
570
+ header: {
571
+ title: {
572
+ tag: 'plain_text',
573
+ content: `🛠️ ${enParts.join(' · ')}`,
574
+ i18n_content: {
575
+ zh_cn: `🛠️ ${zhParts.join(' · ')}`,
576
+ en_us: `🛠️ ${enParts.join(' · ')}`,
577
+ },
578
+ text_color: 'grey',
579
+ text_size: 'notation',
580
+ },
581
+ vertical_align: 'center',
582
+ icon: {
583
+ tag: 'standard_icon',
584
+ token: 'down-small-ccm_outlined',
585
+ color: 'grey',
586
+ size: '16px 16px',
587
+ },
588
+ icon_position: 'right',
589
+ icon_expanded_angle: -180,
590
+ },
591
+ border: { color: 'grey', corner_radius: '5px' },
592
+ vertical_spacing: '8px',
593
+ padding: '8px 8px 8px 8px',
594
+ elements: steps.map(buildToolUseStepElement),
595
+ };
596
+ }
489
597
  function toCardKit2(card) {
490
598
  const result = {
491
599
  schema: '2.0',
@@ -496,3 +604,107 @@ function toCardKit2(card) {
496
604
  result.header = card.header;
497
605
  return result;
498
606
  }
607
+ function buildStreamingToolUsePendingPanel() {
608
+ return {
609
+ tag: 'collapsible_panel',
610
+ expanded: false,
611
+ header: {
612
+ title: {
613
+ tag: 'plain_text',
614
+ content: '🛠️ Tool use pending',
615
+ i18n_content: {
616
+ zh_cn: '🛠️ 等待工具执行',
617
+ en_us: '🛠️ Tool use pending',
618
+ },
619
+ text_color: 'grey',
620
+ text_size: 'notation',
621
+ },
622
+ vertical_align: 'center',
623
+ icon: {
624
+ tag: 'standard_icon',
625
+ token: 'down-small-ccm_outlined',
626
+ color: 'grey',
627
+ size: '16px 16px',
628
+ },
629
+ icon_position: 'right',
630
+ icon_expanded_angle: -180,
631
+ },
632
+ border: { color: 'grey', corner_radius: '5px' },
633
+ vertical_spacing: '8px',
634
+ padding: '8px 8px 8px 8px',
635
+ elements: [],
636
+ };
637
+ }
638
+ function buildToolUsePanel(params) {
639
+ const { toolUseSteps = [], toolUseElapsedMs, titleSuffix } = params;
640
+ const duration = toolUseElapsedMs ? formatToolUseDuration(toolUseElapsedMs) : null;
641
+ const zhTitleParts = [duration?.zh ?? '工具执行'];
642
+ const enTitleParts = [duration?.en ?? 'Tool use'];
643
+ if (titleSuffix) {
644
+ zhTitleParts.push(titleSuffix.zh);
645
+ enTitleParts.push(titleSuffix.en);
646
+ }
647
+ const stepElements = toolUseSteps.length > 0 ? toolUseSteps.map((step) => buildToolUseStepElement(step)) : [buildToolUsePlaceholder()];
648
+ return {
649
+ tag: 'collapsible_panel',
650
+ expanded: false,
651
+ header: {
652
+ title: {
653
+ tag: 'plain_text',
654
+ content: `🛠️ ${enTitleParts.join(' · ')}`,
655
+ i18n_content: {
656
+ zh_cn: `🛠️ ${zhTitleParts.join(' · ')}`,
657
+ en_us: `🛠️ ${enTitleParts.join(' · ')}`,
658
+ },
659
+ text_color: 'grey',
660
+ text_size: 'notation',
661
+ },
662
+ vertical_align: 'center',
663
+ icon: {
664
+ tag: 'standard_icon',
665
+ token: 'down-small-ccm_outlined',
666
+ color: 'grey',
667
+ size: '16px 16px',
668
+ },
669
+ icon_position: 'right',
670
+ icon_expanded_angle: -180,
671
+ },
672
+ border: { color: 'grey', corner_radius: '5px' },
673
+ vertical_spacing: '8px',
674
+ padding: '8px 8px 8px 8px',
675
+ elements: stepElements,
676
+ };
677
+ }
678
+ function buildToolUseStepElement(step) {
679
+ return {
680
+ tag: 'div',
681
+ icon: {
682
+ tag: 'standard_icon',
683
+ token: step.iconToken,
684
+ color: 'grey',
685
+ },
686
+ text: {
687
+ tag: 'plain_text',
688
+ content: step.detail ? `${step.title}\n${step.detail}` : step.title,
689
+ text_color: 'grey',
690
+ text_size: 'notation',
691
+ },
692
+ };
693
+ }
694
+ function buildToolUsePlaceholder(labels) {
695
+ const zh = labels?.zh ?? '暂无工具步骤';
696
+ const en = labels?.en ?? tool_use_display_1.EMPTY_TOOL_USE_PLACEHOLDER;
697
+ return {
698
+ tag: 'div',
699
+ text: {
700
+ tag: 'plain_text',
701
+ content: en,
702
+ i18n_content: {
703
+ zh_cn: zh,
704
+ en_us: en,
705
+ },
706
+ text_color: 'grey',
707
+ text_size: 'notation',
708
+ },
709
+ };
710
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Shared utilities for the reasoning display subsystem.
6
+ */
7
+ export declare function normalizeToolName(name?: string): string;
8
+ export declare function truncateText(value: string, maxLength: number): string;
9
+ export declare function redactInlineSecrets(value: string): string;
10
+ /**
11
+ * Sanitize tool params for safe logging.
12
+ * Logs only param key names (no values) to avoid leaking sensitive data.
13
+ */
14
+ export declare function sanitizeParamsForLog(params?: Record<string, unknown>): string;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Shared utilities for the reasoning display subsystem.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.normalizeToolName = normalizeToolName;
10
+ exports.truncateText = truncateText;
11
+ exports.redactInlineSecrets = redactInlineSecrets;
12
+ exports.sanitizeParamsForLog = sanitizeParamsForLog;
13
+ function normalizeToolName(name) {
14
+ return name?.trim().toLowerCase() ?? '';
15
+ }
16
+ function truncateText(value, maxLength) {
17
+ if (value.length <= maxLength)
18
+ return value;
19
+ return `${value.slice(0, maxLength - 3)}...`;
20
+ }
21
+ const INLINE_ASSIGNMENT_RE = /(^|[\s"'`])([A-Za-z_][A-Za-z0-9_]*)(=(?:"[^"]*"|'[^']*'|[^\s"'`]+))/g;
22
+ const AUTH_HEADER_SECRET_RE = /(Authorization\s*:\s*(?:Bearer|Basic|Token)\s+)([^'"\s]+)/gi;
23
+ const QUOTED_HEADER_ARG_RE = /((?:^|[\s"'`])(?:-H|--header)\s+)(['"])([A-Za-z0-9_-]+)(\s*:\s*)([^'"]*)(\2)/gi;
24
+ const UNQUOTED_HEADER_ARG_RE = /((?:^|[\s"'`])(?:-H|--header)\s+)([A-Za-z0-9_-]+)(\s*:\s*)([^\s"'`]+)/gi;
25
+ const SECRET_FLAG_RE = /((?:^|[\s"'`]))(--?[A-Za-z0-9][A-Za-z0-9-]*)(=|\s+)(?:"([^"]*)"|'([^']*)'|([^\s"'`]+))/g;
26
+ const SENSITIVE_NAME_RE = /token|secret|password|api[_-]?key|authorization|cookie|credential|bearer|session[_-]?id|client[_-]?secret|access[_-]?key/i;
27
+ function redactInlineSecrets(value) {
28
+ return value
29
+ .replace(INLINE_ASSIGNMENT_RE, (match, prefix, key) => isSensitiveName(key) ? `${prefix}${key}=[redacted]` : match)
30
+ .replace(AUTH_HEADER_SECRET_RE, '$1[redacted]')
31
+ .replace(QUOTED_HEADER_ARG_RE, (match, prefix, quote, name, separator) => shouldRedactHeaderValue(name) ? `${prefix}${quote}${name}${separator}[redacted]${quote}` : match)
32
+ .replace(UNQUOTED_HEADER_ARG_RE, (match, prefix, name, separator) => shouldRedactHeaderValue(name) ? `${prefix}${name}${separator}[redacted]` : match)
33
+ .replace(SECRET_FLAG_RE, (match, prefix, flag, separator, doubleQuoted, singleQuoted, bare) => {
34
+ const normalizedFlag = flag.replace(/^-+/, '');
35
+ if (!isSensitiveName(normalizedFlag))
36
+ return match;
37
+ const redactedValue = doubleQuoted !== undefined
38
+ ? '"[redacted]"'
39
+ : singleQuoted !== undefined
40
+ ? "'[redacted]'"
41
+ : bare !== undefined
42
+ ? '[redacted]'
43
+ : '[redacted]';
44
+ return `${prefix}${flag}${separator}${redactedValue}`;
45
+ });
46
+ }
47
+ function isSensitiveName(value) {
48
+ return SENSITIVE_NAME_RE.test(value);
49
+ }
50
+ function shouldRedactHeaderValue(name) {
51
+ return !/^authorization$/i.test(name) && isSensitiveName(name);
52
+ }
53
+ /**
54
+ * Sanitize tool params for safe logging.
55
+ * Logs only param key names (no values) to avoid leaking sensitive data.
56
+ */
57
+ function sanitizeParamsForLog(params) {
58
+ if (!params || typeof params !== 'object')
59
+ return '';
60
+ const keys = Object.keys(params);
61
+ if (keys.length === 0)
62
+ return '{}';
63
+ return `{${keys.join(',')}}`;
64
+ }
@@ -8,8 +8,10 @@
8
8
  * reply-dispatcher.ts, streaming-card-controller.ts, flush-controller.ts,
9
9
  * and unavailable-guard.ts.
10
10
  */
11
- import type { ClawdbotConfig, ReplyPayload } from 'openclaw/plugin-sdk';
11
+ import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
12
+ import type { ReplyDispatcher } from 'openclaw/plugin-sdk/reply-runtime';
12
13
  import type { FeishuFooterConfig } from '../core/types';
14
+ import type { ToolUseDisplayConfig } from './tool-use-config';
13
15
  export declare const CARD_PHASES: {
14
16
  readonly idle: "idle";
15
17
  readonly creating: "creating";
@@ -38,11 +40,17 @@ export interface ReasoningState {
38
40
  reasoningElapsedMs: number;
39
41
  isReasoningPhase: boolean;
40
42
  }
43
+ export interface ToolUseState {
44
+ startedAt: number | null;
45
+ elapsedMs: number;
46
+ isActive: boolean;
47
+ }
41
48
  export interface StreamingTextState {
42
49
  accumulatedText: string;
43
50
  completedText: string;
44
51
  streamingPrefix: string;
45
52
  lastPartialText: string;
53
+ lastFlushedText: string;
46
54
  }
47
55
  export interface CardKitState {
48
56
  cardKitCardId: string | null;
@@ -65,6 +73,7 @@ export declare const THROTTLE_CONSTANTS: {
65
73
  readonly PATCH_MS: 1500;
66
74
  readonly LONG_GAP_THRESHOLD_MS: 2000;
67
75
  readonly BATCH_AFTER_GAP_MS: 300;
76
+ readonly REASONING_STATUS_MS: 1500;
68
77
  };
69
78
  export declare const EMPTY_REPLY_FALLBACK_TEXT = "Done.";
70
79
  export interface CreateFeishuReplyDispatcherParams {
@@ -81,20 +90,7 @@ export interface CreateFeishuReplyDispatcherParams {
81
90
  skipTyping?: boolean;
82
91
  /** When true, replies are sent into the thread instead of main chat. */
83
92
  replyInThread?: boolean;
84
- }
85
- /**
86
- * Manual mirror of the SDK-internal ReplyDispatcher type
87
- * (from openclaw/plugin-sdk auto-reply/reply/reply-dispatcher.d.ts).
88
- *
89
- * Must be kept in sync when the SDK updates the dispatcher signature.
90
- */
91
- export interface ReplyDispatcher {
92
- sendToolResult: (payload: ReplyPayload) => boolean;
93
- sendBlockReply: (payload: ReplyPayload) => boolean;
94
- sendFinalReply: (payload: ReplyPayload) => boolean;
95
- waitForIdle: () => Promise<void>;
96
- getQueuedCounts: () => Record<string, number>;
97
- markComplete: () => void;
93
+ toolUseDisplay: ToolUseDisplayConfig;
98
94
  }
99
95
  /**
100
96
  * The structured return type of createFeishuReplyDispatcher.
@@ -128,5 +124,6 @@ export interface StreamingCardDeps {
128
124
  chatId: string;
129
125
  replyToMessageId: string | undefined;
130
126
  replyInThread: boolean | undefined;
127
+ toolUseDisplay: ToolUseDisplayConfig;
131
128
  resolvedFooter: Required<FeishuFooterConfig>;
132
129
  }
@@ -56,5 +56,6 @@ exports.THROTTLE_CONSTANTS = {
56
56
  PATCH_MS: 1500,
57
57
  LONG_GAP_THRESHOLD_MS: 2000,
58
58
  BATCH_AFTER_GAP_MS: 300,
59
+ REASONING_STATUS_MS: 1500,
59
60
  };
60
61
  exports.EMPTY_REPLY_FALLBACK_TEXT = 'Done.';