@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.
- package/index.js +25 -6
- package/package.json +1 -1
- package/src/card/builder.d.ts +29 -7
- package/src/card/builder.js +241 -29
- package/src/card/reasoning-utils.d.ts +14 -0
- package/src/card/reasoning-utils.js +64 -0
- package/src/card/reply-dispatcher-types.d.ts +12 -15
- package/src/card/reply-dispatcher-types.js +1 -0
- package/src/card/reply-dispatcher.js +63 -11
- package/src/card/streaming-card-controller.d.ts +15 -0
- package/src/card/streaming-card-controller.js +188 -65
- package/src/card/tool-use-config.d.ts +26 -0
- package/src/card/tool-use-config.js +76 -0
- package/src/card/tool-use-display.d.ts +29 -0
- package/src/card/tool-use-display.js +438 -0
- package/src/card/tool-use-trace-store.d.ts +51 -0
- package/src/card/tool-use-trace-store.js +271 -0
- package/src/channel/event-handlers.d.ts +1 -0
- package/src/channel/event-handlers.js +51 -0
- package/src/channel/monitor.js +2 -0
- package/src/core/comment-target.d.ts +65 -0
- package/src/core/comment-target.js +100 -0
- package/src/core/config-schema.d.ts +9 -0
- package/src/core/config-schema.js +5 -0
- package/src/core/tool-scopes.d.ts +1 -1
- package/src/core/tool-scopes.js +7 -0
- package/src/messaging/inbound/comment-context.d.ts +82 -0
- package/src/messaging/inbound/comment-context.js +353 -0
- package/src/messaging/inbound/comment-handler.d.ts +30 -0
- package/src/messaging/inbound/comment-handler.js +269 -0
- package/src/messaging/inbound/dispatch-commands.js +23 -2
- package/src/messaging/inbound/dispatch-context.js +19 -7
- package/src/messaging/inbound/dispatch.js +87 -8
- package/src/messaging/outbound/deliver.d.ts +29 -0
- package/src/messaging/outbound/deliver.js +94 -0
- package/src/messaging/outbound/outbound.js +19 -0
- package/src/messaging/types.d.ts +63 -0
- package/src/tools/oapi/drive/doc-comments.js +93 -24
- package/src/tools/oapi/index.js +1 -0
- package/src/tools/oapi/task/index.d.ts +1 -0
- package/src/tools/oapi/task/index.js +3 -1
- package/src/tools/oapi/task/section.d.ts +17 -0
- 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>;
|