@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
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Drive comment event handler for the Lark/Feishu channel plugin.
7
+ *
8
+ * Handles `drive.notice.comment_add_v1` events by resolving comment
9
+ * context, enforcing access policies, building a synthetic
10
+ * {@link MessageContext}, and dispatching to the agent.
11
+ *
12
+ * Modeled after the reaction handler pattern (reaction-handler.ts):
13
+ * bypasses the 7-stage message pipeline and dispatches directly.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.handleFeishuCommentEvent = handleFeishuCommentEvent;
50
+ const crypto = __importStar(require("node:crypto"));
51
+ const reply_history_1 = require("openclaw/plugin-sdk/reply-history");
52
+ const accounts_1 = require("../../core/accounts.js");
53
+ const comment_target_1 = require("../../core/comment-target.js");
54
+ const lark_client_1 = require("../../core/lark-client.js");
55
+ const lark_logger_1 = require("../../core/lark-logger.js");
56
+ const user_name_cache_1 = require("./user-name-cache.js");
57
+ const dispatch_1 = require("./dispatch.js");
58
+ const gate_1 = require("./gate.js");
59
+ const policy_1 = require("./policy.js");
60
+ const gate_effects_1 = require("./gate-effects.js");
61
+ const comment_context_1 = require("./comment-context.js");
62
+ const logger = (0, lark_logger_1.larkLogger)('inbound/comment-handler');
63
+ // ---------------------------------------------------------------------------
64
+ // Public API
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Handle a Drive comment event.
68
+ *
69
+ * Resolves the comment context, checks access policies, builds a
70
+ * synthetic MessageContext, and dispatches to the agent.
71
+ */
72
+ async function handleFeishuCommentEvent(params) {
73
+ const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
74
+ const log = runtime?.log ?? ((...args) => logger.info(args.map(String).join(' ')));
75
+ const error = runtime?.error ?? ((...args) => logger.error(args.map(String).join(' ')));
76
+ // The parser has already normalized all fields from notice_meta into
77
+ // canonical top-level fields, so we just read the canonical shape.
78
+ const senderOpenId = event.user_id?.open_id ?? '';
79
+ const senderUserId = event.user_id?.user_id;
80
+ const senderUnionId = event.user_id?.union_id;
81
+ const fileToken = event.file_token ?? '';
82
+ const fileType = (event.file_type ?? 'docx');
83
+ const commentId = event.comment_id ?? '';
84
+ if (!senderOpenId || !fileToken || !commentId) {
85
+ log(`feishu[${accountId}]: comment event missing required fields, skipping`);
86
+ return;
87
+ }
88
+ // ---- Self-comment filter ----
89
+ // Ignore comments/replies authored by the bot itself.
90
+ if (senderOpenId === botOpenId) {
91
+ log(`feishu[${accountId}]: ignoring self-authored comment on ${fileToken}`);
92
+ return;
93
+ }
94
+ // ---- Account resolution ----
95
+ const account = (0, accounts_1.getLarkAccount)(cfg, accountId);
96
+ const accountFeishuCfg = account.config;
97
+ const accountScopedCfg = {
98
+ ...cfg,
99
+ channels: { ...cfg.channels, feishu: accountFeishuCfg },
100
+ };
101
+ // ---- Access policy enforcement (DM-style) ----
102
+ // Comment events are user-to-bot interactions outside of IM, so we apply
103
+ // the same dmPolicy (open/allowlist/pairing) as DM messages.
104
+ // This mirrors the logic in gate.ts:checkDmGate.
105
+ const dmPolicy = accountFeishuCfg?.dmPolicy ?? 'pairing';
106
+ if (dmPolicy === 'disabled') {
107
+ log(`feishu[${accountId}]: comment event rejected (dmPolicy=disabled)`);
108
+ return;
109
+ }
110
+ if (dmPolicy !== 'open') {
111
+ // Read both config allowlist and pairing store (matches gate.ts behavior)
112
+ const configAllowFrom = accountFeishuCfg?.allowFrom ?? [];
113
+ const storeAllowFrom = await (0, gate_1.readFeishuAllowFromStore)(account.accountId).catch(() => []);
114
+ const combinedAllowFrom = [...configAllowFrom, ...storeAllowFrom];
115
+ const match = (0, policy_1.resolveFeishuAllowlistMatch)({
116
+ allowFrom: combinedAllowFrom,
117
+ senderId: senderOpenId,
118
+ });
119
+ // Also check user_id if available
120
+ const userIdMatch = senderUserId
121
+ ? (0, policy_1.resolveFeishuAllowlistMatch)({
122
+ allowFrom: combinedAllowFrom,
123
+ senderId: senderUserId,
124
+ })
125
+ : { allowed: false };
126
+ if (!match.allowed && !userIdMatch.allowed) {
127
+ if (dmPolicy === 'pairing') {
128
+ // Create pairing request and send challenge (mirrors gate.ts:334).
129
+ // Prefer replying in the comment thread so the user sees the
130
+ // challenge in context; fall back to DM if comment reply fails.
131
+ log(`feishu[${accountId}]: comment sender not paired, creating pairing request`);
132
+ try {
133
+ const core = lark_client_1.LarkClient.runtime;
134
+ const { code } = await core.channel.pairing.upsertPairingRequest({
135
+ channel: 'feishu',
136
+ id: senderOpenId,
137
+ accountId: account.accountId,
138
+ });
139
+ const pairingText = core.channel.pairing.buildPairingReply({
140
+ channel: 'feishu',
141
+ idLine: senderOpenId,
142
+ code,
143
+ });
144
+ // Try comment thread reply first
145
+ let sentInThread = false;
146
+ try {
147
+ const client = lark_client_1.LarkClient.fromCfg(accountScopedCfg, accountId);
148
+ await client.sdk.request({
149
+ method: 'POST',
150
+ url: `/open-apis/drive/v1/files/${fileToken}/comments/${commentId}/replies`,
151
+ params: { file_type: fileType, user_id_type: 'open_id' },
152
+ data: {
153
+ content: {
154
+ elements: [{ type: 'text_run', text_run: { text: pairingText } }],
155
+ },
156
+ },
157
+ });
158
+ sentInThread = true;
159
+ }
160
+ catch {
161
+ // Comment reply failed — fall through to DM
162
+ }
163
+ // Fallback: send to DM
164
+ if (!sentInThread) {
165
+ await (0, gate_effects_1.sendPairingReply)({
166
+ senderId: senderOpenId,
167
+ chatId: senderOpenId,
168
+ accountId: account.accountId,
169
+ accountScopedCfg,
170
+ });
171
+ }
172
+ }
173
+ catch (pairingErr) {
174
+ log(`feishu[${accountId}]: pairing request failed: ${String(pairingErr)}`);
175
+ }
176
+ }
177
+ else {
178
+ log(`feishu[${accountId}]: comment event rejected (dmPolicy=${dmPolicy}, not in allowlist)`);
179
+ }
180
+ return;
181
+ }
182
+ }
183
+ // ---- Resolve comment context ----
184
+ const turn = await (0, comment_context_1.resolveDriveCommentEventTurn)({ cfg, event, accountId });
185
+ if (!turn) {
186
+ log(`feishu[${accountId}]: failed to resolve comment context, skipping`);
187
+ return;
188
+ }
189
+ // ---- Mention filter ----
190
+ // The event-level is_mentioned flag is not stable for Drive comments.
191
+ // Fall back to the resolved comment text, where @mentions are normalized
192
+ // into "@<open_id>" by extractElementText().
193
+ const eventMentioned = event.notice_meta?.is_mentioned ?? event.is_mention;
194
+ const textMentioned = Boolean(botOpenId) && Boolean(turn.commentText?.includes(`@${botOpenId}`));
195
+ if (eventMentioned !== true && !textMentioned) {
196
+ log(`feishu[${accountId}]: comment event not mentioning bot, skipping` +
197
+ ` (eventFlag=${String(eventMentioned)}, textMention=${textMentioned})`);
198
+ return;
199
+ }
200
+ // ---- Build synthetic MessageContext ----
201
+ const commentTarget = (0, comment_target_1.buildFeishuCommentTarget)({
202
+ // Whole-document comment threads do not support in-thread replies
203
+ // through the replies API. For any event in that thread, fall back
204
+ // to creating a new whole-document comment instead.
205
+ deliveryMode: turn.isWholeComment ? 'create_whole' : 'reply',
206
+ fileType,
207
+ fileToken,
208
+ commentId,
209
+ });
210
+ const syntheticMessageId = `comment:${commentId}:${event.reply_id ?? 'root'}:${crypto.randomUUID()}`;
211
+ const syntheticText = turn.prompt;
212
+ let ctx = {
213
+ chatId: commentTarget, // Use comment target as the "chat" identifier
214
+ messageId: syntheticMessageId,
215
+ senderId: senderOpenId,
216
+ chatType: 'p2p', // Comment events are treated as direct interactions
217
+ content: syntheticText,
218
+ contentType: 'text',
219
+ resources: [],
220
+ mentions: [],
221
+ mentionAll: false,
222
+ rawMessage: {
223
+ message_id: syntheticMessageId,
224
+ chat_id: commentTarget,
225
+ chat_type: 'p2p',
226
+ message_type: 'text',
227
+ content: JSON.stringify({ text: syntheticText }),
228
+ create_time: event.action_time ?? String(Date.now()),
229
+ },
230
+ rawSender: {
231
+ sender_id: {
232
+ open_id: senderOpenId,
233
+ user_id: senderUserId,
234
+ union_id: senderUnionId,
235
+ },
236
+ sender_type: 'user',
237
+ },
238
+ };
239
+ // ---- Sender name resolution ----
240
+ const senderResult = await (0, user_name_cache_1.resolveUserName)({ account, openId: senderOpenId, log });
241
+ if (senderResult.name) {
242
+ ctx = { ...ctx, senderName: senderResult.name };
243
+ }
244
+ log(`feishu[${accountId}]: comment event on ${commentId}` +
245
+ `${event.reply_id ? ` (reply ${event.reply_id})` : ''}, dispatching to agent`);
246
+ logger.info(`comment event on ${commentId}` +
247
+ `${event.reply_id ? ` (reply ${event.reply_id})` : ''}`);
248
+ const historyLimit = Math.max(0, accountFeishuCfg?.historyLimit ?? accountScopedCfg.messages?.groupChat?.historyLimit ?? reply_history_1.DEFAULT_GROUP_HISTORY_LIMIT);
249
+ // ---- Dispatch to agent ----
250
+ try {
251
+ await (0, dispatch_1.dispatchToAgent)({
252
+ ctx,
253
+ permissionError: undefined,
254
+ mediaPayload: {},
255
+ quotedContent: undefined,
256
+ account,
257
+ accountScopedCfg,
258
+ runtime,
259
+ chatHistories,
260
+ historyLimit,
261
+ replyToMessageId: undefined, // No IM message to reply to
262
+ commandAuthorized: false,
263
+ skipTyping: true, // No IM typing indicator for comment events
264
+ });
265
+ }
266
+ catch (err) {
267
+ error(`feishu[${accountId}]: error dispatching comment event: ${String(err)}`);
268
+ }
269
+ }
@@ -14,6 +14,7 @@ exports.dispatchSystemCommand = dispatchSystemCommand;
14
14
  const lark_logger_1 = require("../../core/lark-logger.js");
15
15
  const lark_ticket_1 = require("../../core/lark-ticket.js");
16
16
  const reply_dispatcher_1 = require("../../card/reply-dispatcher.js");
17
+ const tool_use_trace_store_1 = require("../../card/tool-use-trace-store.js");
17
18
  const send_1 = require("../outbound/send.js");
18
19
  const dispatch_builders_1 = require("./dispatch-builders.js");
19
20
  const log = (0, lark_logger_1.larkLogger)('inbound/dispatch-commands');
@@ -44,15 +45,22 @@ async function dispatchPermissionNotification(dc, permissionError, replyToMessag
44
45
  messageSid: `${dc.ctx.messageId}:permission-error`,
45
46
  wasMentioned: false,
46
47
  });
48
+ (0, tool_use_trace_store_1.startToolUseTraceRun)(dc.threadSessionKey ?? dc.route.sessionKey);
47
49
  const { dispatcher: permDispatcher, replyOptions: permReplyOptions, markDispatchIdle: markPermIdle, markFullyComplete: markPermComplete, } = (0, reply_dispatcher_1.createFeishuReplyDispatcher)({
48
50
  cfg: dc.accountScopedCfg,
49
51
  agentId: dc.route.agentId,
50
- sessionKey: dc.threadSessionKey ?? dc.route.sessionKey,
51
52
  chatId: dc.ctx.chatId,
53
+ sessionKey: dc.threadSessionKey ?? dc.route.sessionKey,
52
54
  replyToMessageId: replyToMessageId ?? dc.ctx.messageId,
53
55
  accountId: dc.account.accountId,
54
56
  chatType: dc.ctx.chatType,
55
57
  replyInThread: dc.isThread,
58
+ toolUseDisplay: {
59
+ mode: 'off',
60
+ showToolUse: false,
61
+ showToolResultDetails: false,
62
+ showFullPaths: false,
63
+ },
56
64
  });
57
65
  dc.log(`feishu[${dc.account.accountId}]: dispatching permission error notification to agent`);
58
66
  await dc.core.channel.reply.dispatchReplyFromConfig({
@@ -74,13 +82,17 @@ async function dispatchPermissionNotification(dc, permissionError, replyToMessag
74
82
  */
75
83
  async function dispatchSystemCommand(dc, ctxPayload, replyToMessageId) {
76
84
  let delivered = false;
85
+ const suppressToolDetails = isLifecycleSessionCommand(dc.ctx.content);
77
86
  dc.log(`feishu[${dc.account.accountId}]: detected system command, using plain-text dispatch`);
78
87
  log.info('system command detected, plain-text dispatch');
79
88
  await dc.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
80
89
  ctx: ctxPayload,
81
90
  cfg: dc.accountScopedCfg,
82
91
  dispatcherOptions: {
83
- deliver: async (payload) => {
92
+ deliver: async (payload, info) => {
93
+ if (suppressToolDetails && info.kind === 'tool') {
94
+ return;
95
+ }
84
96
  const text = payload.text?.trim() ?? '';
85
97
  if (!text)
86
98
  return;
@@ -108,3 +120,12 @@ async function dispatchSystemCommand(dc, ctxPayload, replyToMessageId) {
108
120
  dc.log(`feishu[${dc.account.accountId}]: system command dispatched (delivered=${delivered})`);
109
121
  log.info(`system command dispatched (delivered=${delivered}, elapsed=${(0, lark_ticket_1.ticketElapsed)()}ms)`);
110
122
  }
123
+ function isLifecycleSessionCommand(text) {
124
+ if (!text)
125
+ return false;
126
+ const match = text.trim().match(/^\/([^\s@]+)/);
127
+ if (!match)
128
+ return false;
129
+ const command = match[1]?.toLowerCase();
130
+ return command === 'new' || command === 'reset';
131
+ }
@@ -17,6 +17,7 @@ const routing_1 = require("openclaw/plugin-sdk/routing");
17
17
  const lark_client_1 = require("../../core/lark-client.js");
18
18
  const lark_logger_1 = require("../../core/lark-logger.js");
19
19
  const chat_info_cache_1 = require("../../core/chat-info-cache.js");
20
+ const comment_target_1 = require("../../core/comment-target.js");
20
21
  const log = (0, lark_logger_1.larkLogger)('inbound/dispatch-context');
21
22
  // ---------------------------------------------------------------------------
22
23
  // RuntimeEnv fallback
@@ -46,26 +47,37 @@ function buildDispatchContext(params) {
46
47
  const runtime = ensureRuntime(params.runtime);
47
48
  const log = runtime.log;
48
49
  const error = runtime.error;
49
- const isGroup = ctx.chatType === 'group';
50
+ const isComment = (0, comment_target_1.isCommentTarget)(ctx.chatId);
51
+ const isGroup = !isComment && ctx.chatType === 'group';
50
52
  const isThread = isGroup && Boolean(ctx.threadId);
51
53
  const core = lark_client_1.LarkClient.runtime;
52
54
  const feishuFrom = `feishu:${ctx.senderId}`;
53
- const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderId}`;
55
+ // Comment targets use the comment target string directly as the "To"
56
+ // so the outbound routing layer can detect it and route through Drive API.
57
+ const feishuTo = isComment
58
+ ? ctx.chatId
59
+ : isGroup
60
+ ? `chat:${ctx.chatId}`
61
+ : `user:${ctx.senderId}`;
54
62
  const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderId}` : ctx.senderId;
55
63
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(accountScopedCfg);
56
64
  // ---- Route resolution ----
65
+ // Comment targets use the comment target as the peer ID so each
66
+ // comment thread gets its own session key.
57
67
  const route = core.channel.routing.resolveAgentRoute({
58
68
  cfg: accountScopedCfg,
59
69
  channel: 'feishu',
60
70
  accountId: account.accountId,
61
- peer: {
62
- kind: isGroup ? 'group' : 'direct',
63
- id: isGroup ? ctx.chatId : ctx.senderId,
64
- },
71
+ peer: isComment
72
+ ? { kind: 'direct', id: ctx.chatId }
73
+ : {
74
+ kind: isGroup ? 'group' : 'direct',
75
+ id: isGroup ? ctx.chatId : ctx.senderId,
76
+ },
65
77
  });
66
78
  // ---- System event ----
67
79
  const sender = ctx.senderName ? `${ctx.senderName} (${ctx.senderId})` : ctx.senderId;
68
- const location = isGroup ? `group ${ctx.chatId}` : 'DM';
80
+ const location = isComment ? `comment ${ctx.chatId}` : isGroup ? `group ${ctx.chatId}` : 'DM';
69
81
  const tags = [];
70
82
  tags.push(`msg:${ctx.messageId}`);
71
83
  if (ctx.parentId)
@@ -21,9 +21,13 @@ const lark_logger_1 = require("../../core/lark-logger.js");
21
21
  const lark_ticket_1 = require("../../core/lark-ticket.js");
22
22
  const reply_dispatcher_1 = require("../../card/reply-dispatcher.js");
23
23
  const chat_queue_1 = require("../../channel/chat-queue.js");
24
+ const tool_use_config_1 = require("../../card/tool-use-config.js");
25
+ const tool_use_trace_store_1 = require("../../card/tool-use-trace-store.js");
24
26
  const abort_detect_1 = require("../../channel/abort-detect.js");
25
27
  const chat_info_cache_1 = require("../../core/chat-info-cache.js");
28
+ const comment_target_1 = require("../../core/comment-target.js");
26
29
  const targets_1 = require("../../core/targets.js");
30
+ const deliver_1 = require("../outbound/deliver.js");
27
31
  const doctor_1 = require("../../commands/doctor.js");
28
32
  const auth_1 = require("../../commands/auth.js");
29
33
  const index_1 = require("../../commands/index.js");
@@ -45,7 +49,58 @@ const log = (0, lark_logger_1.larkLogger)('inbound/dispatch');
45
49
  * system-command path — command handlers don't consume history context,
46
50
  * so the entries should be preserved for the next normal message.
47
51
  */
52
+ /**
53
+ * Dispatch a comment-target message via the buffered block dispatcher.
54
+ *
55
+ * Comment targets cannot use the streaming card flow (IM APIs don't
56
+ * understand comment:... targets). Instead we use the SDK's buffered
57
+ * block dispatcher with a deliver callback that sends via the Drive
58
+ * comment reply API.
59
+ */
60
+ async function dispatchCommentMessage(dc, ctxPayload, skillFilter) {
61
+ const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
62
+ dc.log(`feishu[${dc.account.accountId}]: dispatching comment reply (session=${effectiveSessionKey})`);
63
+ log.info(`dispatching comment reply (session=${effectiveSessionKey})`);
64
+ let delivered = false;
65
+ await dc.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
66
+ ctx: ctxPayload,
67
+ cfg: dc.accountScopedCfg,
68
+ dispatcherOptions: {
69
+ deliver: async (payload) => {
70
+ const text = payload.text?.trim() ?? '';
71
+ if (!text || text === 'NO_REPLY')
72
+ return;
73
+ await (0, deliver_1.sendCommentReplyLark)({
74
+ cfg: dc.accountScopedCfg,
75
+ to: dc.ctx.chatId,
76
+ text,
77
+ accountId: dc.account.accountId,
78
+ });
79
+ delivered = true;
80
+ },
81
+ onSkip: (_payload, info) => {
82
+ if (info.reason !== 'silent') {
83
+ dc.log(`feishu[${dc.account.accountId}]: comment reply skipped (reason=${info.reason})`);
84
+ }
85
+ },
86
+ onError: (err, info) => {
87
+ dc.error(`feishu[${dc.account.accountId}]: comment ${info.kind} reply failed: ${String(err)}`);
88
+ },
89
+ },
90
+ replyOptions: {
91
+ ...(skillFilter ? { skillFilter } : {}),
92
+ },
93
+ });
94
+ dc.log(`feishu[${dc.account.accountId}]: comment dispatch complete (delivered=${delivered})`);
95
+ log.info(`comment dispatch complete (delivered=${delivered}, elapsed=${(0, lark_ticket_1.ticketElapsed)()}ms)`);
96
+ }
48
97
  async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey, historyLimit, replyToMessageId, skillFilter, skipTyping) {
98
+ // Comment targets bypass the streaming card / IM flow entirely —
99
+ // route through the Drive comment reply API.
100
+ if ((0, comment_target_1.isCommentTarget)(dc.ctx.chatId)) {
101
+ await dispatchCommentMessage(dc, ctxPayload, skillFilter);
102
+ return;
103
+ }
49
104
  // Abort messages should never create streaming cards — dispatch via the
50
105
  // plain-text system-command path so the SDK's abort handler can reply
51
106
  // without touching CardKit.
@@ -55,16 +110,31 @@ async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey,
55
110
  await (0, dispatch_commands_1.dispatchSystemCommand)(dc, ctxPayload, replyToMessageId);
56
111
  return;
57
112
  }
113
+ const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
114
+ const toolUseDisplay = (0, tool_use_config_1.resolveToolUseDisplayConfig)({
115
+ cfg: dc.accountScopedCfg,
116
+ feishuCfg: dc.account.config,
117
+ agentId: dc.route.agentId,
118
+ sessionKey: effectiveSessionKey,
119
+ body: dc.ctx.content,
120
+ });
121
+ if (toolUseDisplay.showToolUse) {
122
+ (0, tool_use_trace_store_1.startToolUseTraceRun)(effectiveSessionKey);
123
+ }
124
+ else {
125
+ (0, tool_use_trace_store_1.clearToolUseTraceRun)(effectiveSessionKey);
126
+ }
58
127
  const { dispatcher, replyOptions, markDispatchIdle, markFullyComplete, abortCard } = (0, reply_dispatcher_1.createFeishuReplyDispatcher)({
59
128
  cfg: dc.accountScopedCfg,
60
129
  agentId: dc.route.agentId,
61
- sessionKey: dc.threadSessionKey ?? dc.route.sessionKey,
62
130
  chatId: dc.ctx.chatId,
131
+ sessionKey: effectiveSessionKey,
63
132
  replyToMessageId: replyToMessageId ?? dc.ctx.messageId,
64
133
  accountId: dc.account.accountId,
65
134
  chatType: dc.ctx.chatType,
66
135
  skipTyping,
67
136
  replyInThread: dc.isThread,
137
+ toolUseDisplay,
68
138
  });
69
139
  // Create an AbortController so the abort fast-path can cancel the
70
140
  // underlying LLM request (not just the streaming card UI).
@@ -73,7 +143,6 @@ async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey,
73
143
  // terminate the streaming card before this task completes.
74
144
  const queueKey = (0, chat_queue_1.buildQueueKey)(dc.account.accountId, dc.ctx.chatId, dc.ctx.threadId);
75
145
  (0, chat_queue_1.registerActiveDispatcher)(queueKey, { abortCard, abortController });
76
- const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
77
146
  dc.log(`feishu[${dc.account.accountId}]: dispatching to agent (session=${effectiveSessionKey})`);
78
147
  log.info(`dispatching to agent (session=${effectiveSessionKey})`);
79
148
  try {
@@ -148,7 +217,10 @@ async function dispatchToAgent(params) {
148
217
  const messageBody = (0, dispatch_builders_1.buildMessageBody)(params.ctx, params.quotedContent);
149
218
  // 3. Permission-error notification (optional side-effect).
150
219
  // Isolated so a failure here does not block the main message dispatch.
151
- if (params.permissionError) {
220
+ // Skipped for comment targets: the streaming card dispatcher inside
221
+ // dispatchPermissionNotification sends via IM APIs which don't
222
+ // understand comment:... targets.
223
+ if (params.permissionError && !(0, comment_target_1.isCommentTarget)(dc.ctx.chatId)) {
152
224
  try {
153
225
  await (0, dispatch_commands_1.dispatchPermissionNotification)(dc, params.permissionError, params.replyToMessageId);
154
226
  }
@@ -213,11 +285,15 @@ async function dispatchToAgent(params) {
213
285
  // Must run BEFORE the SDK command check — the SDK does not recognise
214
286
  // plugin-registered commands via isControlCommandMessage, so
215
287
  // /feishu_* falls through to the AI agent otherwise.
288
+ // Skipped for comment targets: comment text won't match /feishu_*
289
+ // patterns in practice, and sendCardFeishu/sendMessageFeishu can't
290
+ // deliver to comment:... targets.
216
291
  const contentTrimmed = (params.ctx.content ?? '').trim();
217
- const isDoctorCommand = /^\/feishu[_ ]doctor\s*$/i.test(contentTrimmed);
218
- const isAuthCommand = /^\/feishu[_ ](?:auth|onboarding)\s*$/i.test(contentTrimmed);
219
- const isStartCommand = /^\/feishu[_ ]start\s*$/i.test(contentTrimmed);
220
- const isHelpCommand = /^\/feishu(?:[_ ]help)?\s*$/i.test(contentTrimmed);
292
+ const isCommentFlow = (0, comment_target_1.isCommentTarget)(dc.ctx.chatId);
293
+ const isDoctorCommand = !isCommentFlow && /^\/feishu[_ ]doctor\s*$/i.test(contentTrimmed);
294
+ const isAuthCommand = !isCommentFlow && /^\/feishu[_ ](?:auth|onboarding)\s*$/i.test(contentTrimmed);
295
+ const isStartCommand = !isCommentFlow && /^\/feishu[_ ]start\s*$/i.test(contentTrimmed);
296
+ const isHelpCommand = !isCommentFlow && /^\/feishu(?:[_ ]help)?\s*$/i.test(contentTrimmed);
221
297
  const i18nCommandName = isDoctorCommand
222
298
  ? 'doctor'
223
299
  : isAuthCommand
@@ -269,7 +345,10 @@ async function dispatchToAgent(params) {
269
345
  return;
270
346
  }
271
347
  // 8. Dispatch: system command vs. normal message
272
- const isCommand = dc.core.channel.commands.isControlCommandMessage(params.ctx.content, params.accountScopedCfg);
348
+ // Comment targets always go to normal dispatch — system command
349
+ // delivery uses sendMessageFeishu which can't reach comment threads.
350
+ const isCommand = !isCommentFlow &&
351
+ dc.core.channel.commands.isControlCommandMessage(params.ctx.content, params.accountScopedCfg);
273
352
  // Resolve per-group skill filter (per-group > default "*")
274
353
  const skillFilter = dc.isGroup ? (params.groupConfig?.skills ?? params.defaultGroupConfig?.skills) : undefined;
275
354
  if (isCommand) {
@@ -153,3 +153,32 @@ export interface SendMediaLarkParams {
153
153
  * ```
154
154
  */
155
155
  export declare function sendMediaLark(params: SendMediaLarkParams): Promise<FeishuSendResult>;
156
+ /**
157
+ * Parameters for sending a reply to a Drive comment thread.
158
+ */
159
+ export interface SendCommentReplyLarkParams {
160
+ /** Plugin configuration. */
161
+ cfg: ClawdbotConfig;
162
+ /**
163
+ * Target in comment format: `comment:<fileType>:<fileToken>:<commentId>`.
164
+ * Parsed via `parseFeishuCommentTarget`.
165
+ */
166
+ to: string;
167
+ /** Reply text content. */
168
+ text: string;
169
+ /** Optional account identifier for multi-account setups. */
170
+ accountId?: string;
171
+ }
172
+ /**
173
+ * Send a text message to a Feishu Drive comment surface.
174
+ *
175
+ * Parses the comment target from `to`, then chooses one of two delivery
176
+ * strategies:
177
+ * - `reply` → reply in the existing comment thread
178
+ * - `create_whole` → create a new whole-document comment
179
+ *
180
+ * Returns a synthetic FeishuSendResult (no IM messageId).
181
+ *
182
+ * @throws {Error} When the target is not a valid comment target or API fails.
183
+ */
184
+ export declare function sendCommentReplyLark(params: SendCommentReplyLarkParams): Promise<FeishuSendResult>;