@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,82 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Drive comment event context resolution.
6
+ *
7
+ * Resolves the full context for a `drive.notice.comment_add_v1` event:
8
+ * document title, comment quoted text, and reply chain.
9
+ */
10
+ import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
11
+ import type { FeishuDriveCommentEvent } from '../types';
12
+ /** Resolved context for a Drive comment event. */
13
+ export interface CommentEventTurn {
14
+ /** Document title (best-effort, may be undefined). */
15
+ docTitle?: string;
16
+ /** File type of the document. */
17
+ fileType: string;
18
+ /** File token of the document. */
19
+ fileToken: string;
20
+ /** Root comment ID. */
21
+ commentId: string;
22
+ /** Reply ID (if this event is a reply). */
23
+ replyId?: string;
24
+ /** Quoted text from the comment (the content being commented on). */
25
+ quotedText?: string;
26
+ /** The text of the triggering comment/reply. */
27
+ commentText?: string;
28
+ /** Reply chain context (previous replies in the thread). */
29
+ replyChainContext?: string;
30
+ /** Whether the bot was @-mentioned. */
31
+ isMentioned?: boolean;
32
+ /** Whether the source comment is a whole-document comment. */
33
+ isWholeComment?: boolean;
34
+ /** Surface prompt for the agent (context + instructions). */
35
+ prompt: string;
36
+ /** Short preview of the prompt. */
37
+ preview: string;
38
+ }
39
+ /**
40
+ * Infer whether a Drive comment thread is a whole-document comment.
41
+ *
42
+ * The explicit `is_whole` flag is authoritative when present. When the API
43
+ * omits it, the root comment's quoted anchor is the best fallback signal:
44
+ * whole-document comments have no quote, while anchored comments do.
45
+ *
46
+ * Note that this inference is about the root thread, so it must behave the
47
+ * same for both root-comment events and reply events.
48
+ */
49
+ export declare function inferIsWholeComment(params: {
50
+ explicitIsWhole?: boolean;
51
+ quotedText?: string;
52
+ }): boolean;
53
+ /**
54
+ * Resolve the full context for a Drive comment event.
55
+ *
56
+ * Fetches document metadata, comment content, and reply chain.
57
+ */
58
+ export declare function resolveDriveCommentEventTurn(params: {
59
+ cfg: ClawdbotConfig;
60
+ event: FeishuDriveCommentEvent;
61
+ accountId?: string;
62
+ }): Promise<CommentEventTurn | null>;
63
+ /**
64
+ * Parse the raw webhook payload for a `drive.notice.comment_add_v1` event.
65
+ *
66
+ * The SDK flattens the v2 envelope, so the event data may be at the
67
+ * top level or nested under `event`. User info and timestamp live inside
68
+ * `notice_meta` in the real event structure, with fallback to top-level
69
+ * fields for compatibility with different SDK flattening styles.
70
+ */
71
+ /**
72
+ * Normalize a Drive comment event into a canonical shape.
73
+ *
74
+ * Real event structures vary:
75
+ * - **notice_meta style**: file_token, file_type, from_user_id, timestamp
76
+ * all live inside `notice_meta`; top-level only has comment_id/reply_id.
77
+ * - **SDK-flattened style**: fields may be hoisted to top level.
78
+ *
79
+ * This parser checks `notice_meta.*` first, then falls back to top-level
80
+ * fields, so the handler can consume a single canonical shape.
81
+ */
82
+ export declare function parseFeishuDriveCommentNoticeEventPayload(data: unknown): FeishuDriveCommentEvent | null;
@@ -0,0 +1,353 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Drive comment event context resolution.
7
+ *
8
+ * Resolves the full context for a `drive.notice.comment_add_v1` event:
9
+ * document title, comment quoted text, and reply chain.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.inferIsWholeComment = inferIsWholeComment;
13
+ exports.resolveDriveCommentEventTurn = resolveDriveCommentEventTurn;
14
+ exports.parseFeishuDriveCommentNoticeEventPayload = parseFeishuDriveCommentNoticeEventPayload;
15
+ const lark_client_1 = require("../../core/lark-client.js");
16
+ const lark_logger_1 = require("../../core/lark-logger.js");
17
+ const logger = (0, lark_logger_1.larkLogger)('inbound/comment-context');
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Extract plain text from comment element arrays.
23
+ */
24
+ function extractElementText(elements) {
25
+ if (!Array.isArray(elements))
26
+ return '';
27
+ return elements
28
+ .map((el) => {
29
+ if (el.type === 'text_run' && el.text_run?.text)
30
+ return el.text_run.text;
31
+ if (el.type === 'person' && el.person?.user_id)
32
+ return `@${el.person.user_id}`;
33
+ if (el.type === 'docs_link' && el.docs_link?.url)
34
+ return el.docs_link.url;
35
+ // Fallback for simplified element formats
36
+ if (el.text)
37
+ return el.text;
38
+ return '';
39
+ })
40
+ .join('');
41
+ }
42
+ /**
43
+ * Extract text from a comment reply object.
44
+ */
45
+ function extractReplyText(reply) {
46
+ if (!reply?.content?.elements)
47
+ return '';
48
+ return extractElementText(reply.content.elements);
49
+ }
50
+ /**
51
+ * Infer whether a Drive comment thread is a whole-document comment.
52
+ *
53
+ * The explicit `is_whole` flag is authoritative when present. When the API
54
+ * omits it, the root comment's quoted anchor is the best fallback signal:
55
+ * whole-document comments have no quote, while anchored comments do.
56
+ *
57
+ * Note that this inference is about the root thread, so it must behave the
58
+ * same for both root-comment events and reply events.
59
+ */
60
+ function inferIsWholeComment(params) {
61
+ if (typeof params.explicitIsWhole === 'boolean') {
62
+ return params.explicitIsWhole;
63
+ }
64
+ return !params.quotedText?.trim();
65
+ }
66
+ /**
67
+ * Fetch document title via Drive file meta API.
68
+ */
69
+ async function fetchDocTitle(params) {
70
+ const { cfg, fileToken, fileType, accountId } = params;
71
+ try {
72
+ const client = lark_client_1.LarkClient.fromCfg(cfg, accountId);
73
+ const res = await client.sdk.drive.v1.fileMeta.batchQuery({
74
+ data: {
75
+ request_docs: [{ doc_token: fileToken, doc_type: fileType }],
76
+ with_url: false,
77
+ },
78
+ });
79
+ const meta = res?.data?.metas?.[0];
80
+ return meta?.title || undefined;
81
+ }
82
+ catch (err) {
83
+ logger.warn(`failed to fetch doc title: ${err}`);
84
+ return undefined;
85
+ }
86
+ }
87
+ /**
88
+ * Fetch a single comment with its replies.
89
+ */
90
+ async function fetchComment(params) {
91
+ const { cfg, fileToken, fileType, commentId, accountId } = params;
92
+ try {
93
+ const client = lark_client_1.LarkClient.fromCfg(cfg, accountId);
94
+ // Paginate through comments to find the target comment.
95
+ // Cap at MAX_COMMENT_PAGES to avoid excessive OAPI calls on
96
+ // documents with thousands of comments.
97
+ const MAX_COMMENT_PAGES = 5; // 5 × 100 = 500 comments max scan
98
+ let comment = undefined;
99
+ let commentPageToken;
100
+ let commentHasMore = true;
101
+ let commentPages = 0;
102
+ while (commentHasMore && !comment && commentPages < MAX_COMMENT_PAGES) {
103
+ commentPages++;
104
+ const res = await client.sdk.drive.v1.fileComment.list({
105
+ path: { file_token: fileToken },
106
+ params: {
107
+ file_type: fileType,
108
+ page_size: 100,
109
+ page_token: commentPageToken,
110
+ user_id_type: 'open_id',
111
+ },
112
+ });
113
+ const data = res?.data;
114
+ const items = data?.items ?? [];
115
+ comment = items.find((c) => c.comment_id === commentId);
116
+ commentHasMore = data?.has_more ?? false;
117
+ commentPageToken = data?.page_token;
118
+ }
119
+ if (!comment)
120
+ return undefined;
121
+ // Fetch complete replies
122
+ const replies = [];
123
+ let pageToken;
124
+ let hasMore = true;
125
+ while (hasMore) {
126
+ const replyRes = await client.sdk.drive.v1.fileCommentReply.list({
127
+ path: { file_token: fileToken, comment_id: commentId },
128
+ params: {
129
+ file_type: fileType,
130
+ page_token: pageToken,
131
+ page_size: 50,
132
+ user_id_type: 'open_id',
133
+ },
134
+ });
135
+ const replyData = replyRes?.data;
136
+ if (replyData?.items) {
137
+ replies.push(...replyData.items);
138
+ hasMore = replyData.has_more ?? false;
139
+ pageToken = replyData.page_token;
140
+ }
141
+ else {
142
+ break;
143
+ }
144
+ }
145
+ return { comment, replies };
146
+ }
147
+ catch (err) {
148
+ logger.warn(`failed to fetch comment: ${err}`);
149
+ return undefined;
150
+ }
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // Surface prompt
154
+ // ---------------------------------------------------------------------------
155
+ /**
156
+ * Build the surface prompt sent to the agent for a Drive comment event.
157
+ *
158
+ * Includes document context (title, quoted text, comment text) and
159
+ * behavioural instructions aligned with the upstream PR.
160
+ */
161
+ function buildDriveCommentSurfacePrompt(params) {
162
+ const documentLabel = params.documentTitle
163
+ ? `"${params.documentTitle}"`
164
+ : `${params.fileType} document ${params.fileToken}`;
165
+ const actionLabel = params.noticeType === 'add_reply' ? 'reply' : 'comment';
166
+ const firstLine = params.targetReplyText
167
+ ? `The user added a ${actionLabel} in ${documentLabel}: ${params.targetReplyText}`
168
+ : `The user added a ${actionLabel} in ${documentLabel}.`;
169
+ const lines = [firstLine];
170
+ if (params.noticeType === 'add_reply' &&
171
+ params.rootCommentText &&
172
+ params.rootCommentText !== params.targetReplyText) {
173
+ lines.push(`Original comment: ${params.rootCommentText}`);
174
+ }
175
+ if (params.quoteText) {
176
+ lines.push(`Quoted content: ${params.quoteText}`);
177
+ }
178
+ if (params.isMentioned === true) {
179
+ lines.push('This comment mentioned you.');
180
+ }
181
+ lines.push(`Event type: ${params.noticeType}`, `file_token: ${params.fileToken}`, `file_type: ${params.fileType}`, `comment_id: ${params.commentId}`);
182
+ if (params.replyId?.trim()) {
183
+ lines.push(`reply_id: ${params.replyId.trim()}`);
184
+ }
185
+ lines.push('This is a Feishu document comment-thread event, not a Feishu IM conversation. Your final text reply will be posted automatically to the current comment thread and will not be sent as an instant message.', 'If you need to inspect or handle the comment thread, prefer the feishu_drive tools: use list_comments / list_comment_replies to inspect comments, and use reply_comment/add_comment to notify the user after modifying the document.', 'If the comment asks you to modify document content, such as adding, inserting, replacing, or deleting text, tables, or headings, you must first use feishu_doc to actually modify the document. Do not reply with only "done", "I\'ll handle it", or a restated plan without calling tools.', 'If the comment quotes document content, that quoted text is usually the edit anchor. For requests like "insert xxx below this content", first locate the position around the quoted content, then use feishu_doc to make the change.', 'If the comment asks you to summarize, explain, rewrite, translate, refine, continue, or review the document content "below", "above", "this paragraph", "this section", or the quoted content, you must also treat the quoted content as the primary target anchor instead of defaulting to the whole document.', 'For requests like "summarize the content below", "explain this section", or "continue writing from here", first locate the relevant document fragment based on the comment\'s quoted content. If the quote is not sufficient to support the answer, then use feishu_doc.read or feishu_doc.list_blocks to read nearby context.', 'Do not guess document content based only on the comment text, and do not output a vague summary before reading enough context. Unless the user explicitly asks to summarize the entire document, default to handling only the local scope related to the quoted content.', 'When document edits are involved, first use feishu_doc.read or feishu_doc.list_blocks to confirm the context, then use feishu_doc writing or updating capabilities to complete the change. After the edit succeeds, notify the user through feishu_drive.reply_comment.', 'If the document edit fails or you cannot locate the anchor, do not pretend it succeeded. Reply clearly in the comment thread with the reason for failure or the missing information.', 'If this is a reading-comprehension task, such as summarization, explanation, or extraction, you may directly output the final answer text after confirming the context. The system will automatically reply with that answer in the current comment thread.', 'When you produce a user-visible reply, keep it in the same language as the user\'s original comment or reply unless they explicitly ask for another language.', 'If you have already completed the user-visible action through feishu_drive.reply_comment or feishu_drive.add_comment, output NO_REPLY at the end to avoid duplicate sending.', 'If the user directly asks a question in the comment and a plain text answer is sufficient, output the answer text directly. The system will automatically reply with your final answer in the current comment thread.', 'If you determine that the current comment does not require any user-visible action, output NO_REPLY at the end.');
186
+ lines.push(`Decide what to do next based on this document ${actionLabel} event.`);
187
+ return lines.join('\n');
188
+ }
189
+ // ---------------------------------------------------------------------------
190
+ // Main resolution
191
+ // ---------------------------------------------------------------------------
192
+ /**
193
+ * Resolve the full context for a Drive comment event.
194
+ *
195
+ * Fetches document metadata, comment content, and reply chain.
196
+ */
197
+ async function resolveDriveCommentEventTurn(params) {
198
+ const { cfg, event, accountId } = params;
199
+ const fileToken = event.file_token;
200
+ const fileType = event.file_type ?? 'docx';
201
+ const commentId = event.comment_id;
202
+ if (!fileToken || !commentId) {
203
+ logger.warn('missing file_token or comment_id in comment event');
204
+ return null;
205
+ }
206
+ // Fetch document title and comment context in parallel
207
+ const [docTitle, commentData] = await Promise.all([
208
+ fetchDocTitle({ cfg, fileToken, fileType, accountId }),
209
+ fetchComment({ cfg, fileToken, fileType, commentId, accountId }),
210
+ ]);
211
+ let quotedText;
212
+ let commentText;
213
+ let replyChainContext;
214
+ let isWholeComment = false;
215
+ if (commentData) {
216
+ // Extract quoted text from the root comment
217
+ if (commentData.comment?.quote) {
218
+ quotedText = String(commentData.comment.quote);
219
+ }
220
+ isWholeComment = inferIsWholeComment({
221
+ explicitIsWhole: commentData.comment?.is_whole,
222
+ quotedText,
223
+ });
224
+ // Determine the triggering text
225
+ if (event.reply_id && commentData.replies.length > 0) {
226
+ // This is a reply event — find the specific reply
227
+ const targetReply = commentData.replies.find((r) => r.reply_id === event.reply_id);
228
+ commentText = targetReply ? extractReplyText(targetReply) : undefined;
229
+ // Build reply chain context (all replies before the target)
230
+ const chainReplies = commentData.replies.filter((r) => r.reply_id !== event.reply_id);
231
+ if (chainReplies.length > 0) {
232
+ replyChainContext = chainReplies
233
+ .map((r) => {
234
+ const sender = r.user_id?.open_id ?? 'unknown';
235
+ const text = extractReplyText(r);
236
+ return `[${sender}]: ${text}`;
237
+ })
238
+ .join('\n');
239
+ }
240
+ }
241
+ else {
242
+ // This is a root comment event
243
+ const rootReply = commentData.comment?.reply_list?.replies?.[0];
244
+ commentText = rootReply ? extractReplyText(rootReply) : undefined;
245
+ }
246
+ }
247
+ // Determine notice type and root comment text for prompt building
248
+ const noticeType = event.reply_id ? 'add_reply' : 'add_comment';
249
+ let rootCommentText;
250
+ if (commentData) {
251
+ const rootReply = commentData.comment?.reply_list?.replies?.[0];
252
+ rootCommentText = rootReply ? extractReplyText(rootReply) : undefined;
253
+ }
254
+ const isMentioned = event.notice_meta?.is_mentioned ?? event.is_mention;
255
+ const prompt = buildDriveCommentSurfacePrompt({
256
+ noticeType,
257
+ fileType,
258
+ fileToken,
259
+ commentId,
260
+ replyId: event.reply_id,
261
+ isMentioned,
262
+ documentTitle: docTitle,
263
+ quoteText: quotedText,
264
+ rootCommentText,
265
+ targetReplyText: commentText,
266
+ });
267
+ const preview = prompt.replace(/\s+/g, ' ').slice(0, 160);
268
+ return {
269
+ docTitle,
270
+ fileType,
271
+ fileToken,
272
+ commentId,
273
+ replyId: event.reply_id,
274
+ quotedText,
275
+ commentText,
276
+ replyChainContext,
277
+ isMentioned,
278
+ isWholeComment,
279
+ prompt,
280
+ preview,
281
+ };
282
+ }
283
+ // ---------------------------------------------------------------------------
284
+ // Event payload parsing
285
+ // ---------------------------------------------------------------------------
286
+ /**
287
+ * Parse the raw webhook payload for a `drive.notice.comment_add_v1` event.
288
+ *
289
+ * The SDK flattens the v2 envelope, so the event data may be at the
290
+ * top level or nested under `event`. User info and timestamp live inside
291
+ * `notice_meta` in the real event structure, with fallback to top-level
292
+ * fields for compatibility with different SDK flattening styles.
293
+ */
294
+ /**
295
+ * Normalize a Drive comment event into a canonical shape.
296
+ *
297
+ * Real event structures vary:
298
+ * - **notice_meta style**: file_token, file_type, from_user_id, timestamp
299
+ * all live inside `notice_meta`; top-level only has comment_id/reply_id.
300
+ * - **SDK-flattened style**: fields may be hoisted to top level.
301
+ *
302
+ * This parser checks `notice_meta.*` first, then falls back to top-level
303
+ * fields, so the handler can consume a single canonical shape.
304
+ */
305
+ function parseFeishuDriveCommentNoticeEventPayload(data) {
306
+ if (!data || typeof data !== 'object')
307
+ return null;
308
+ const raw = data;
309
+ // Handle both flattened and nested event formats
310
+ const event = (raw.event ?? raw);
311
+ // notice_meta is the primary source for most fields in real events
312
+ const noticeMeta = (event.notice_meta ?? raw.notice_meta);
313
+ // file_token: notice_meta > top-level event > top-level raw
314
+ const fileToken = (noticeMeta?.file_token ?? event.file_token ?? raw.file_token);
315
+ // file_type: notice_meta > top-level
316
+ const fileType = (noticeMeta?.file_type ?? event.file_type ?? raw.file_type);
317
+ // comment_id / reply_id are typically at event top-level
318
+ const commentId = (event.comment_id ?? raw.comment_id);
319
+ const replyId = (event.reply_id ?? raw.reply_id);
320
+ if (!fileToken || !commentId)
321
+ return null;
322
+ // User info: notice_meta.from_user_id > top-level user_id
323
+ const metaUserId = noticeMeta?.from_user_id;
324
+ const fallbackUserId = (event.user_id ?? raw.user_id);
325
+ const userId = metaUserId ?? fallbackUserId;
326
+ // Timestamp: notice_meta.timestamp > top-level action_time
327
+ const timestamp = (noticeMeta?.timestamp ?? event.action_time ?? raw.action_time);
328
+ // Mention flag: notice_meta.is_mentioned > top-level is_mention
329
+ const isMentioned = (noticeMeta?.is_mentioned ?? event.is_mention ?? raw.is_mention);
330
+ // Build the canonical, normalized event
331
+ return {
332
+ app_id: (raw.app_id ?? event.app_id),
333
+ // Canonical fields — always populated from the best source
334
+ file_token: fileToken,
335
+ file_type: fileType,
336
+ comment_id: commentId,
337
+ reply_id: replyId,
338
+ // notice_meta preserved for debugging
339
+ notice_meta: noticeMeta
340
+ ? {
341
+ from_user_id: userId,
342
+ file_token: fileToken,
343
+ file_type: fileType,
344
+ timestamp,
345
+ is_mentioned: isMentioned,
346
+ }
347
+ : undefined,
348
+ // Normalized top-level convenience fields (canonical)
349
+ is_mention: isMentioned,
350
+ user_id: userId,
351
+ action_time: timestamp,
352
+ };
353
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Drive comment event handler for the Lark/Feishu channel plugin.
6
+ *
7
+ * Handles `drive.notice.comment_add_v1` events by resolving comment
8
+ * context, enforcing access policies, building a synthetic
9
+ * {@link MessageContext}, and dispatching to the agent.
10
+ *
11
+ * Modeled after the reaction handler pattern (reaction-handler.ts):
12
+ * bypasses the 7-stage message pipeline and dispatches directly.
13
+ */
14
+ import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk';
15
+ import type { HistoryEntry } from 'openclaw/plugin-sdk/reply-history';
16
+ import type { FeishuDriveCommentEvent } from '../types';
17
+ /**
18
+ * Handle a Drive comment event.
19
+ *
20
+ * Resolves the comment context, checks access policies, builds a
21
+ * synthetic MessageContext, and dispatches to the agent.
22
+ */
23
+ export declare function handleFeishuCommentEvent(params: {
24
+ cfg: ClawdbotConfig;
25
+ event: FeishuDriveCommentEvent;
26
+ botOpenId?: string;
27
+ runtime?: RuntimeEnv;
28
+ chatHistories?: Map<string, HistoryEntry[]>;
29
+ accountId?: string;
30
+ }): Promise<void>;