@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
@@ -22,6 +22,7 @@ const lark_logger_1 = require("../core/lark-logger.js");
22
22
  const deliver_1 = require("../messaging/outbound/deliver.js");
23
23
  const send_1 = require("../messaging/outbound/send.js");
24
24
  const typing_1 = require("../messaging/outbound/typing.js");
25
+ const builder_1 = require("./builder.js");
25
26
  const card_error_1 = require("./card-error.js");
26
27
  const reply_mode_1 = require("./reply-mode.js");
27
28
  const streaming_card_controller_1 = require("./streaming-card-controller.js");
@@ -32,7 +33,7 @@ const log = (0, lark_logger_1.larkLogger)('card/reply-dispatcher');
32
33
  // ---------------------------------------------------------------------------
33
34
  function createFeishuReplyDispatcher(params) {
34
35
  const core = lark_client_1.LarkClient.runtime;
35
- const { cfg, agentId, sessionKey, chatId, replyToMessageId, accountId, replyInThread } = params;
36
+ const { cfg, agentId, chatId, sessionKey, replyToMessageId, accountId, replyInThread } = params;
36
37
  // Resolve account so we can read per-account config (e.g. replyMode)
37
38
  const account = (0, accounts_1.getLarkAccount)(cfg, accountId);
38
39
  const feishuCfg = account.config;
@@ -50,6 +51,7 @@ function createFeishuReplyDispatcher(params) {
50
51
  const useStreamingCards = replyMode === 'streaming';
51
52
  // ---- Block streaming for static mode ----
52
53
  const enableBlockStreaming = feishuCfg?.blockStreaming === true && !useStreamingCards;
54
+ const { toolUseDisplay } = params;
53
55
  const resolvedFooter = (0, footer_config_1.resolveFooterConfig)(feishuCfg?.footer);
54
56
  log.info('reply mode resolved', {
55
57
  effectiveReplyMode,
@@ -81,6 +83,7 @@ function createFeishuReplyDispatcher(params) {
81
83
  chatId,
82
84
  replyToMessageId,
83
85
  replyInThread,
86
+ toolUseDisplay,
84
87
  resolvedFooter,
85
88
  })
86
89
  : null;
@@ -171,9 +174,10 @@ function createFeishuReplyDispatcher(params) {
171
174
  return;
172
175
  await typingCallbacks.onReplyStart?.();
173
176
  },
174
- deliver: async (payload) => {
177
+ deliver: async (payload, meta) => {
175
178
  log.debug('deliver called', {
176
179
  textPreview: payload.text?.slice(0, 100),
180
+ kind: meta?.kind,
177
181
  });
178
182
  if (shouldSkip('deliver.entry'))
179
183
  return;
@@ -190,7 +194,7 @@ function createFeishuReplyDispatcher(params) {
190
194
  return;
191
195
  }
192
196
  // 提取文本和媒体 URL
193
- const text = payload.text ?? '';
197
+ const text = getVisiblePayloadText(payload);
194
198
  const payloadMediaUrls = payload.mediaUrls?.length
195
199
  ? payload.mediaUrls
196
200
  : payload.mediaUrl
@@ -202,15 +206,21 @@ function createFeishuReplyDispatcher(params) {
202
206
  }
203
207
  // ---- Streaming card mode ----
204
208
  if (controller) {
205
- await controller.ensureCardCreated();
206
- if (controller.isTerminated)
207
- return;
208
- if (controller.cardMessageId) {
209
- await controller.onDeliver(payload);
209
+ if (meta?.kind === 'tool' && shouldRouteToolPayloadToCard(payload, toolUseDisplay.showToolUse)) {
210
+ await controller.onToolPayload(payload);
210
211
  return;
211
212
  }
212
- // Card creation failed — fall through to static delivery
213
- log.warn('deliver: card creation failed, falling back to static delivery');
213
+ if (text.trim()) {
214
+ await controller.ensureCardCreated();
215
+ if (controller.isTerminated)
216
+ return;
217
+ if (controller.cardMessageId) {
218
+ await controller.onDeliver({ ...payload, text });
219
+ return;
220
+ }
221
+ // Card creation failed — fall through to static delivery
222
+ log.warn('deliver: card creation failed, falling back to static delivery');
223
+ }
214
224
  }
215
225
  // ---- Static text delivery ----
216
226
  if (text.trim()) {
@@ -374,12 +384,21 @@ function createFeishuReplyDispatcher(params) {
374
384
  dispatcher,
375
385
  replyOptions: {
376
386
  ...replyOptions,
377
- onModelSelected: prefixContext.onModelSelected,
387
+ ...(controller
388
+ ? {
389
+ shouldEmitToolResult: () => false,
390
+ shouldEmitToolOutput: () => false,
391
+ }
392
+ : {}),
393
+ onModelSelected: (ctx) => {
394
+ prefixContext.onModelSelected(ctx);
395
+ },
378
396
  disableBlockStreaming: !enableBlockStreaming,
379
397
  ...(controller
380
398
  ? {
381
399
  onReasoningStream: (payload) => controller.onReasoningStream(payload),
382
400
  onPartialReply: (payload) => controller.onPartialReply(payload),
401
+ onToolStart: (payload) => controller.onToolStart(payload),
383
402
  }
384
403
  : {}),
385
404
  },
@@ -391,3 +410,36 @@ function createFeishuReplyDispatcher(params) {
391
410
  abortCard,
392
411
  };
393
412
  }
413
+ function getVisiblePayloadText(payload) {
414
+ if (payload.isReasoning === true)
415
+ return '';
416
+ const rawText = payload.text ?? '';
417
+ if (!rawText)
418
+ return '';
419
+ const split = (0, builder_1.splitReasoningText)(rawText);
420
+ if (split.answerText != null) {
421
+ return split.answerText;
422
+ }
423
+ return (0, builder_1.stripReasoningTags)(rawText);
424
+ }
425
+ function shouldRouteToolPayloadToCard(payload, showToolUse) {
426
+ if (!showToolUse)
427
+ return false;
428
+ if (!getVisiblePayloadText(payload).trim())
429
+ return false;
430
+ if (payload.interactive)
431
+ return false;
432
+ if (payload.btw)
433
+ return false;
434
+ if (payload.audioAsVoice)
435
+ return false;
436
+ if (payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0)
437
+ return false;
438
+ const execApproval = payload.channelData && typeof payload.channelData === 'object' && !Array.isArray(payload.channelData)
439
+ ? payload.channelData.execApproval
440
+ : undefined;
441
+ if (execApproval && typeof execApproval === 'object' && !Array.isArray(execApproval)) {
442
+ return false;
443
+ }
444
+ return true;
445
+ }
@@ -24,6 +24,7 @@ export declare class StreamingCardController {
24
24
  private cardKit;
25
25
  private text;
26
26
  private reasoning;
27
+ private toolUse;
27
28
  private readonly flush;
28
29
  private readonly guard;
29
30
  private readonly imageResolver;
@@ -57,6 +58,10 @@ export declare class StreamingCardController {
57
58
  get terminalReason(): TerminalReason | null;
58
59
  /** @internal — exposed for test assertions only. */
59
60
  get currentPhase(): CardPhase;
61
+ private get shouldDisplayToolUse();
62
+ private computeToolUseDisplay;
63
+ private get visibleToolUseElapsedMs();
64
+ private computeToolUseTitleSuffix;
60
65
  /**
61
66
  * Unified callback guard — returns true if the pipeline is active
62
67
  * and the callback should proceed.
@@ -70,6 +75,8 @@ export declare class StreamingCardController {
70
75
  private isStaleCreate;
71
76
  private transition;
72
77
  private onEnterTerminalPhase;
78
+ private markToolUseActivity;
79
+ private captureToolUseElapsed;
73
80
  /**
74
81
  * Handle a deliver() call in streaming card mode.
75
82
  *
@@ -78,6 +85,11 @@ export declare class StreamingCardController {
78
85
  */
79
86
  onDeliver(payload: ReplyPayload): Promise<void>;
80
87
  onReasoningStream(payload: ReplyPayload): Promise<void>;
88
+ onToolStart(payload: {
89
+ name?: string;
90
+ phase?: string;
91
+ }): Promise<void>;
92
+ onToolPayload(_payload: ReplyPayload): Promise<void>;
81
93
  onPartialReply(payload: ReplyPayload): Promise<void>;
82
94
  onError(err: unknown, info: {
83
95
  kind: string;
@@ -89,6 +101,9 @@ export declare class StreamingCardController {
89
101
  private performFlush;
90
102
  private buildDisplayText;
91
103
  private throttledCardUpdate;
104
+ private lastToolUseStatusUpdateTime;
105
+ private throttledToolUseStatusUpdate;
106
+ private updateToolUseStatus;
92
107
  private finalizeCard;
93
108
  /**
94
109
  * Close streaming mode then update card content (shared by onError and abortCard).
@@ -15,8 +15,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.StreamingCardController = void 0;
16
16
  exports.prepareTerminalCardContent = prepareTerminalCardContent;
17
17
  const promises_1 = require("node:fs/promises");
18
- const reply_runtime_1 = require("openclaw/plugin-sdk/reply-runtime");
19
18
  const agent_runtime_1 = require("openclaw/plugin-sdk/agent-runtime");
19
+ const reply_runtime_1 = require("openclaw/plugin-sdk/reply-runtime");
20
20
  const api_error_1 = require("../core/api-error.js");
21
21
  const lark_logger_1 = require("../core/lark-logger.js");
22
22
  const lark_client_1 = require("../core/lark-client.js");
@@ -28,47 +28,12 @@ const cardkit_1 = require("./cardkit.js");
28
28
  const flush_controller_1 = require("./flush-controller.js");
29
29
  const image_resolver_1 = require("./image-resolver.js");
30
30
  const markdown_style_1 = require("./markdown-style.js");
31
+ const tool_use_display_1 = require("./tool-use-display.js");
32
+ const tool_use_trace_store_1 = require("./tool-use-trace-store.js");
31
33
  const reply_dispatcher_types_1 = require("./reply-dispatcher-types.js");
32
34
  const unavailable_guard_1 = require("./unavailable-guard.js");
33
35
  const log = (0, lark_logger_1.larkLogger)('card/streaming');
34
36
  // ---------------------------------------------------------------------------
35
- // CardKit 2.0 initial streaming payload
36
- // ---------------------------------------------------------------------------
37
- const STREAMING_THINKING_CARD = {
38
- schema: '2.0',
39
- config: {
40
- streaming_mode: true,
41
- // locales 用于支持多语言摘要展示
42
- locales: ['zh_cn', 'en_us'],
43
- summary: {
44
- content: 'Thinking...',
45
- i18n_content: { zh_cn: '思考中...', en_us: 'Thinking...' },
46
- },
47
- },
48
- body: {
49
- elements: [
50
- {
51
- tag: 'markdown',
52
- content: '',
53
- text_align: 'left',
54
- text_size: 'normal_v2',
55
- margin: '0px 0px 0px 0px',
56
- element_id: builder_1.STREAMING_ELEMENT_ID,
57
- },
58
- {
59
- tag: 'markdown',
60
- content: ' ',
61
- icon: {
62
- tag: 'custom_icon',
63
- img_key: 'img_v3_02vb_496bec09-4b43-4773-ad6b-0cdd103cd2bg',
64
- size: '16px 16px',
65
- },
66
- element_id: 'loading_icon',
67
- },
68
- ],
69
- },
70
- };
71
- // ---------------------------------------------------------------------------
72
37
  // StreamingCardController
73
38
  // ---------------------------------------------------------------------------
74
39
  class StreamingCardController {
@@ -86,6 +51,7 @@ class StreamingCardController {
86
51
  completedText: '',
87
52
  streamingPrefix: '',
88
53
  lastPartialText: '',
54
+ lastFlushedText: '',
89
55
  };
90
56
  reasoning = {
91
57
  accumulatedReasoningText: '',
@@ -93,6 +59,11 @@ class StreamingCardController {
93
59
  reasoningElapsedMs: 0,
94
60
  isReasoningPhase: false,
95
61
  };
62
+ toolUse = {
63
+ startedAt: null,
64
+ elapsedMs: 0,
65
+ isActive: false,
66
+ };
96
67
  // ---- Sub-controllers ----
97
68
  flush;
98
69
  guard;
@@ -285,6 +256,31 @@ class StreamingCardController {
285
256
  get currentPhase() {
286
257
  return this.phase;
287
258
  }
259
+ get shouldDisplayToolUse() {
260
+ return this.deps.toolUseDisplay.showToolUse;
261
+ }
262
+ computeToolUseDisplay() {
263
+ if (!this.shouldDisplayToolUse)
264
+ return null;
265
+ const traceSteps = (0, tool_use_trace_store_1.getToolUseTraceSteps)(this.deps.sessionKey);
266
+ return (0, tool_use_display_1.normalizeToolUseDisplay)({
267
+ traceSteps,
268
+ showFullPaths: this.deps.toolUseDisplay.showFullPaths,
269
+ showResultDetails: this.deps.toolUseDisplay.showToolResultDetails,
270
+ });
271
+ }
272
+ get visibleToolUseElapsedMs() {
273
+ if (!this.shouldDisplayToolUse || !this.toolUse.startedAt) {
274
+ return undefined;
275
+ }
276
+ return this.toolUse.elapsedMs || Date.now() - this.toolUse.startedAt;
277
+ }
278
+ computeToolUseTitleSuffix(display) {
279
+ if (!this.shouldDisplayToolUse)
280
+ return undefined;
281
+ const stepCount = display?.stepCount ?? 0;
282
+ return stepCount > 0 ? (0, tool_use_display_1.buildToolUseTitleSuffix)({ stepCount }) : undefined;
283
+ }
288
284
  // ------------------------------------------------------------------
289
285
  // Unified callback guard
290
286
  // ------------------------------------------------------------------
@@ -330,6 +326,22 @@ class StreamingCardController {
330
326
  this.flush.complete();
331
327
  this.disposeShutdownHook?.();
332
328
  this.disposeShutdownHook = null;
329
+ if (this.phase === 'terminated' || this.phase === 'creation_failed') {
330
+ (0, tool_use_trace_store_1.clearToolUseTraceRun)(this.deps.sessionKey);
331
+ }
332
+ }
333
+ markToolUseActivity() {
334
+ if (!this.toolUse.startedAt) {
335
+ this.toolUse.startedAt = Date.now();
336
+ }
337
+ this.toolUse.elapsedMs = Date.now() - this.toolUse.startedAt;
338
+ this.toolUse.isActive = true;
339
+ }
340
+ captureToolUseElapsed() {
341
+ if (!this.toolUse.startedAt)
342
+ return;
343
+ this.toolUse.elapsedMs = Date.now() - this.toolUse.startedAt;
344
+ this.toolUse.isActive = false;
333
345
  }
334
346
  // ------------------------------------------------------------------
335
347
  // SDK callback bindings
@@ -351,6 +363,7 @@ class StreamingCardController {
351
363
  return;
352
364
  if (!this.cardKit.cardMessageId)
353
365
  return;
366
+ this.captureToolUseElapsed();
354
367
  const split = (0, builder_1.splitReasoningText)(text);
355
368
  if (split.reasoningText && !split.answerText) {
356
369
  // Pure reasoning payload
@@ -396,6 +409,42 @@ class StreamingCardController {
396
409
  this.reasoning.accumulatedReasoningText = split.reasoningText ?? rawText;
397
410
  await this.throttledCardUpdate();
398
411
  }
412
+ async onToolStart(payload) {
413
+ if (!this.shouldProceed('onToolStart'))
414
+ return;
415
+ if (!this.shouldDisplayToolUse)
416
+ return;
417
+ if (payload.phase && payload.phase !== 'start')
418
+ return;
419
+ this.markToolUseActivity();
420
+ await this.ensureCardCreated();
421
+ if (!this.shouldProceed('onToolStart.postCreate'))
422
+ return;
423
+ if (!this.cardKit.cardMessageId)
424
+ return;
425
+ if (!this.text.accumulatedText && this.cardKit.cardKitCardId) {
426
+ await this.throttledToolUseStatusUpdate();
427
+ return;
428
+ }
429
+ await this.throttledCardUpdate();
430
+ }
431
+ async onToolPayload(_payload) {
432
+ if (!this.shouldProceed('onToolPayload'))
433
+ return;
434
+ if (!this.shouldDisplayToolUse)
435
+ return;
436
+ this.markToolUseActivity();
437
+ await this.ensureCardCreated();
438
+ if (!this.shouldProceed('onToolPayload.postCreate'))
439
+ return;
440
+ if (!this.cardKit.cardMessageId)
441
+ return;
442
+ if (!this.text.accumulatedText && this.cardKit.cardKitCardId) {
443
+ await this.throttledToolUseStatusUpdate();
444
+ return;
445
+ }
446
+ await this.throttledCardUpdate();
447
+ }
399
448
  async onPartialReply(payload) {
400
449
  if (!this.shouldProceed('onPartialReply'))
401
450
  return;
@@ -416,6 +465,7 @@ class StreamingCardController {
416
465
  log.debug('onPartialReply', { len: text.length });
417
466
  if (!text)
418
467
  return;
468
+ this.captureToolUseElapsed();
419
469
  if (!this.reasoning.reasoningStartTime) {
420
470
  this.reasoning.reasoningStartTime = Date.now();
421
471
  }
@@ -447,14 +497,16 @@ class StreamingCardController {
447
497
  if (this.guard.terminate('onError', err))
448
498
  return;
449
499
  log.error(`${info.kind} reply failed`, { error: String(err) });
500
+ this.captureToolUseElapsed();
450
501
  this.finalizeCard('onError', 'error');
451
502
  await this.flush.waitForFlush();
452
503
  if (this.cardCreationPromise)
453
504
  await this.cardCreationPromise;
454
505
  const errorEffectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
455
506
  const footerMetrics = this.needsFooterMetrics() ? await this.getFooterSessionMetrics() : undefined;
456
- if (this.cardKit.cardMessageId) {
457
- try {
507
+ const toolUseDisplay = this.computeToolUseDisplay();
508
+ try {
509
+ if (this.cardKit.cardMessageId) {
458
510
  const rawErrorText = this.text.accumulatedText
459
511
  ? `${this.text.accumulatedText}\n\n---\n**Error**: An error occurred while generating the response.`
460
512
  : '**Error**: An error occurred while generating the response.';
@@ -466,6 +518,10 @@ class StreamingCardController {
466
518
  text: terminalContent.text,
467
519
  reasoningText: terminalContent.reasoningText,
468
520
  reasoningElapsedMs: this.reasoning.reasoningElapsedMs || undefined,
521
+ toolUseSteps: toolUseDisplay?.steps,
522
+ toolUseTitleSuffix: this.computeToolUseTitleSuffix(toolUseDisplay),
523
+ toolUseElapsedMs: this.visibleToolUseElapsedMs,
524
+ showToolUse: this.deps.toolUseDisplay.showToolUse,
469
525
  elapsedMs: this.elapsed(),
470
526
  isError: true,
471
527
  footer: this.deps.resolvedFooter,
@@ -483,9 +539,12 @@ class StreamingCardController {
483
539
  });
484
540
  }
485
541
  }
486
- catch {
487
- // Ignore update failures during error handling
488
- }
542
+ }
543
+ catch {
544
+ // Ignore update failures during error handling
545
+ }
546
+ finally {
547
+ (0, tool_use_trace_store_1.clearToolUseTraceRun)(this.deps.sessionKey);
489
548
  }
490
549
  }
491
550
  async onIdle() {
@@ -495,6 +554,7 @@ class StreamingCardController {
495
554
  return;
496
555
  if (this.isTerminalPhase)
497
556
  return;
557
+ this.captureToolUseElapsed();
498
558
  this.finalizeCard('onIdle', 'normal');
499
559
  await this.flush.waitForFlush();
500
560
  if (this.cardCreationPromise) {
@@ -503,8 +563,8 @@ class StreamingCardController {
503
563
  await this.flush.waitForFlush();
504
564
  }
505
565
  const idleEffectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
506
- if (this.cardKit.cardMessageId) {
507
- try {
566
+ try {
567
+ if (this.cardKit.cardMessageId) {
508
568
  if (idleEffectiveCardId) {
509
569
  const seqBeforeClose = this.cardKit.cardKitSequence;
510
570
  this.cardKit.cardKitSequence += 1;
@@ -527,6 +587,7 @@ class StreamingCardController {
527
587
  }
528
588
  // 等待图片异步解析(最多 15s),避免终态卡片留占位符
529
589
  const resolvedDisplayText = await this.imageResolver.resolveImagesAwait(displayText, 15_000);
590
+ const idleToolUseDisplay = this.computeToolUseDisplay();
530
591
  const terminalContent = prepareTerminalCardContent({
531
592
  text: resolvedDisplayText,
532
593
  reasoningText: this.reasoning.accumulatedReasoningText || undefined,
@@ -536,6 +597,10 @@ class StreamingCardController {
536
597
  text: terminalContent.text,
537
598
  reasoningText: terminalContent.reasoningText,
538
599
  reasoningElapsedMs: this.reasoning.reasoningElapsedMs || undefined,
600
+ toolUseSteps: idleToolUseDisplay?.steps,
601
+ toolUseTitleSuffix: this.computeToolUseTitleSuffix(idleToolUseDisplay),
602
+ toolUseElapsedMs: this.visibleToolUseElapsedMs,
603
+ showToolUse: this.deps.toolUseDisplay.showToolUse,
539
604
  elapsedMs: this.elapsed(),
540
605
  footer: this.deps.resolvedFooter,
541
606
  footerMetrics,
@@ -568,9 +633,12 @@ class StreamingCardController {
568
633
  isCardKit: !!idleEffectiveCardId,
569
634
  });
570
635
  }
571
- catch (err) {
572
- log.warn('final card update failed', { error: String(err) });
573
- }
636
+ }
637
+ catch (err) {
638
+ log.warn('final card update failed', { error: String(err) });
639
+ }
640
+ finally {
641
+ (0, tool_use_trace_store_1.clearToolUseTraceRun)(this.deps.sessionKey);
574
642
  }
575
643
  }
576
644
  // ------------------------------------------------------------------
@@ -585,6 +653,7 @@ class StreamingCardController {
585
653
  }
586
654
  async abortCard() {
587
655
  try {
656
+ this.captureToolUseElapsed();
588
657
  if (!this.transition('aborted', 'abortCard', 'abort'))
589
658
  return;
590
659
  // transition() already executed onEnterTerminalPhase (cancel + complete + dispose hook)
@@ -594,6 +663,7 @@ class StreamingCardController {
594
663
  await this.cardCreationPromise;
595
664
  const effectiveCardId = this.cardKit.cardKitCardId ?? this.cardKit.originalCardKitCardId;
596
665
  const elapsedMs = Date.now() - this.dispatchStartTime;
666
+ const abortToolUseDisplay = this.computeToolUseDisplay();
597
667
  const terminalContent = prepareTerminalCardContent({
598
668
  text: this.text.accumulatedText || 'Aborted.',
599
669
  reasoningText: this.reasoning.accumulatedReasoningText || undefined,
@@ -604,6 +674,10 @@ class StreamingCardController {
604
674
  text: terminalContent.text,
605
675
  reasoningText: terminalContent.reasoningText,
606
676
  reasoningElapsedMs: this.reasoning.reasoningElapsedMs || undefined,
677
+ toolUseSteps: abortToolUseDisplay?.steps,
678
+ toolUseTitleSuffix: this.computeToolUseTitleSuffix(abortToolUseDisplay),
679
+ toolUseElapsedMs: this.visibleToolUseElapsedMs,
680
+ showToolUse: this.deps.toolUseDisplay.showToolUse,
607
681
  elapsedMs,
608
682
  isAborted: true,
609
683
  footer: this.deps.resolvedFooter,
@@ -618,6 +692,10 @@ class StreamingCardController {
618
692
  text: terminalContent.text,
619
693
  reasoningText: terminalContent.reasoningText,
620
694
  reasoningElapsedMs: this.reasoning.reasoningElapsedMs || undefined,
695
+ toolUseSteps: abortToolUseDisplay?.steps,
696
+ toolUseTitleSuffix: this.computeToolUseTitleSuffix(abortToolUseDisplay),
697
+ toolUseElapsedMs: this.visibleToolUseElapsedMs,
698
+ showToolUse: this.deps.toolUseDisplay.showToolUse,
621
699
  elapsedMs,
622
700
  isAborted: true,
623
701
  footer: this.deps.resolvedFooter,
@@ -637,6 +715,9 @@ class StreamingCardController {
637
715
  catch (err) {
638
716
  log.warn('abortCard failed', { error: String(err) });
639
717
  }
718
+ finally {
719
+ (0, tool_use_trace_store_1.clearToolUseTraceRun)(this.deps.sessionKey);
720
+ }
640
721
  }
641
722
  // ------------------------------------------------------------------
642
723
  // Internal: card creation
@@ -661,7 +742,7 @@ class StreamingCardController {
661
742
  // Step 1: Create card entity
662
743
  const cId = await (0, cardkit_1.createCardEntity)({
663
744
  cfg: this.deps.cfg,
664
- card: STREAMING_THINKING_CARD,
745
+ card: (0, builder_1.buildStreamingThinkingCard)(this.deps.toolUseDisplay.showToolUse),
665
746
  accountId: this.deps.accountId,
666
747
  });
667
748
  if (this.isStaleCreate(epoch)) {
@@ -722,7 +803,9 @@ class StreamingCardController {
722
803
  log.warn('CardKit flow failed, falling back to IM', { apiDetail });
723
804
  this.cardKit.cardKitCardId = null;
724
805
  this.cardKit.originalCardKitCardId = null;
725
- const fallbackCard = (0, builder_1.buildCardContent)('thinking');
806
+ const fallbackCard = (0, builder_1.buildCardContent)('streaming', {
807
+ showToolUse: this.deps.toolUseDisplay.showToolUse,
808
+ });
726
809
  const result = await (0, send_1.sendCardFeishu)({
727
810
  cfg: this.deps.cfg,
728
811
  to: this.deps.chatId,
@@ -781,27 +864,33 @@ class StreamingCardController {
781
864
  // 流式中间帧使用同步 resolveImages(不等待异步上传)
782
865
  const resolvedText = this.imageResolver.resolveImages(displayText);
783
866
  if (this.cardKit.cardKitCardId) {
784
- // CardKit streaming typewriter effect
785
- const prevSeq = this.cardKit.cardKitSequence;
786
- this.cardKit.cardKitSequence += 1;
787
- log.debug('flushCardUpdate: seq bump', {
788
- seqBefore: prevSeq,
789
- seqAfter: this.cardKit.cardKitSequence,
790
- });
791
- await (0, cardkit_1.streamCardContent)({
792
- cfg: this.deps.cfg,
793
- cardId: this.cardKit.cardKitCardId,
794
- elementId: builder_1.STREAMING_ELEMENT_ID,
795
- content: (0, markdown_style_1.optimizeMarkdownStyle)(resolvedText),
796
- sequence: this.cardKit.cardKitSequence,
797
- accountId: this.deps.accountId,
798
- });
867
+ if (resolvedText !== this.text.lastFlushedText) {
868
+ const prevSeq = this.cardKit.cardKitSequence;
869
+ this.cardKit.cardKitSequence += 1;
870
+ log.debug('flushCardUpdate: answer seq bump', {
871
+ seqBefore: prevSeq,
872
+ seqAfter: this.cardKit.cardKitSequence,
873
+ });
874
+ await (0, cardkit_1.streamCardContent)({
875
+ cfg: this.deps.cfg,
876
+ cardId: this.cardKit.cardKitCardId,
877
+ elementId: builder_1.STREAMING_ELEMENT_ID,
878
+ content: (0, markdown_style_1.optimizeMarkdownStyle)(resolvedText),
879
+ sequence: this.cardKit.cardKitSequence,
880
+ accountId: this.deps.accountId,
881
+ });
882
+ this.text.lastFlushedText = resolvedText;
883
+ }
799
884
  }
800
885
  else {
801
886
  log.debug('flushCardUpdate: IM patch fallback');
887
+ const flushDisplay = this.computeToolUseDisplay();
802
888
  const card = (0, builder_1.buildCardContent)('streaming', {
803
889
  text: this.reasoning.isReasoningPhase ? '' : resolvedText,
804
890
  reasoningText: this.reasoning.isReasoningPhase ? this.reasoning.accumulatedReasoningText : undefined,
891
+ toolUseSteps: flushDisplay?.steps,
892
+ toolUseTitleSuffix: this.computeToolUseTitleSuffix(flushDisplay),
893
+ showToolUse: this.deps.toolUseDisplay.showToolUse,
805
894
  });
806
895
  await (0, send_1.updateCardFeishu)({
807
896
  cfg: this.deps.cfg,
@@ -856,6 +945,40 @@ class StreamingCardController {
856
945
  const throttleMs = this.cardKit.cardKitCardId ? reply_dispatcher_types_1.THROTTLE_CONSTANTS.CARDKIT_MS : reply_dispatcher_types_1.THROTTLE_CONSTANTS.PATCH_MS;
857
946
  await this.flush.throttledUpdate(throttleMs);
858
947
  }
948
+ // ---- Tool-use status streaming (pre-answer phase) ----
949
+ lastToolUseStatusUpdateTime = 0;
950
+ async throttledToolUseStatusUpdate() {
951
+ if (!this.cardKit.cardKitCardId)
952
+ return;
953
+ const now = Date.now();
954
+ if (now - this.lastToolUseStatusUpdateTime < reply_dispatcher_types_1.THROTTLE_CONSTANTS.REASONING_STATUS_MS)
955
+ return;
956
+ this.lastToolUseStatusUpdateTime = now;
957
+ await this.updateToolUseStatus();
958
+ }
959
+ async updateToolUseStatus() {
960
+ if (!this.cardKit.cardKitCardId || this.isTerminalPhase)
961
+ return;
962
+ try {
963
+ const display = this.computeToolUseDisplay();
964
+ const card = (0, builder_1.buildStreamingPreAnswerCard)({
965
+ steps: display?.steps,
966
+ elapsedMs: this.visibleToolUseElapsedMs,
967
+ showToolUse: this.shouldDisplayToolUse,
968
+ });
969
+ this.cardKit.cardKitSequence += 1;
970
+ await (0, cardkit_1.updateCardKitCard)({
971
+ cfg: this.deps.cfg,
972
+ cardId: this.cardKit.cardKitCardId,
973
+ card,
974
+ sequence: this.cardKit.cardKitSequence,
975
+ accountId: this.deps.accountId,
976
+ });
977
+ }
978
+ catch (err) {
979
+ log.debug('updateToolUseStatus failed', { error: String(err) });
980
+ }
981
+ }
859
982
  // ------------------------------------------------------------------
860
983
  // Internal: lifecycle helpers
861
984
  // ------------------------------------------------------------------
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Resolution logic for Feishu tool-use display.
6
+ *
7
+ * The source of truth is OpenClaw's effective verbose state:
8
+ * inline `/verbose` override > session store override > config default.
9
+ * Feishu channel config only retains UI-level detail (`showFullPaths`).
10
+ */
11
+ import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
12
+ import type { FeishuConfig } from '../core/types';
13
+ export type ToolUseMode = 'off' | 'on' | 'full';
14
+ export interface ToolUseDisplayConfig {
15
+ mode: ToolUseMode;
16
+ showToolUse: boolean;
17
+ showToolResultDetails: boolean;
18
+ showFullPaths: boolean;
19
+ }
20
+ export declare function resolveToolUseDisplayConfig(params: {
21
+ cfg: ClawdbotConfig;
22
+ feishuCfg: FeishuConfig | undefined;
23
+ agentId: string;
24
+ sessionKey: string;
25
+ body?: string;
26
+ }): ToolUseDisplayConfig;