@kodelyth/feishu 2026.5.42 → 2026.6.1

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 (192) hide show
  1. package/klaw.plugin.json +1712 -47
  2. package/package.json +17 -4
  3. package/api.ts +0 -32
  4. package/channel-entry.ts +0 -20
  5. package/channel-plugin-api.ts +0 -1
  6. package/contract-api.ts +0 -16
  7. package/index.ts +0 -82
  8. package/runtime-api.ts +0 -52
  9. package/secret-contract-api.ts +0 -5
  10. package/security-contract-api.ts +0 -1
  11. package/session-key-api.ts +0 -1
  12. package/setup-api.ts +0 -3
  13. package/setup-entry.test.ts +0 -19
  14. package/setup-entry.ts +0 -13
  15. package/src/accounts.test.ts +0 -480
  16. package/src/accounts.ts +0 -333
  17. package/src/agent-config.ts +0 -21
  18. package/src/app-registration.ts +0 -331
  19. package/src/approval-auth.test.ts +0 -24
  20. package/src/approval-auth.ts +0 -25
  21. package/src/async.test.ts +0 -35
  22. package/src/async.ts +0 -104
  23. package/src/audio-preflight.runtime.ts +0 -9
  24. package/src/bitable.test.ts +0 -136
  25. package/src/bitable.ts +0 -762
  26. package/src/bot-content.ts +0 -485
  27. package/src/bot-group-name.test.ts +0 -116
  28. package/src/bot-runtime-api.ts +0 -12
  29. package/src/bot-sender-name.ts +0 -125
  30. package/src/bot.broadcast.test.ts +0 -523
  31. package/src/bot.card-action.test.ts +0 -552
  32. package/src/bot.checkBotMentioned.test.ts +0 -265
  33. package/src/bot.helpers.test.ts +0 -135
  34. package/src/bot.stripBotMention.test.ts +0 -126
  35. package/src/bot.test.ts +0 -3671
  36. package/src/bot.ts +0 -1703
  37. package/src/card-action.ts +0 -447
  38. package/src/card-interaction.test.ts +0 -131
  39. package/src/card-interaction.ts +0 -159
  40. package/src/card-test-helpers.ts +0 -54
  41. package/src/card-ux-approval.ts +0 -65
  42. package/src/card-ux-launcher.test.ts +0 -106
  43. package/src/card-ux-launcher.ts +0 -121
  44. package/src/card-ux-shared.ts +0 -33
  45. package/src/channel-runtime-api.ts +0 -16
  46. package/src/channel.runtime.ts +0 -47
  47. package/src/channel.test.ts +0 -1151
  48. package/src/channel.ts +0 -1423
  49. package/src/chat-schema.ts +0 -25
  50. package/src/chat.test.ts +0 -240
  51. package/src/chat.ts +0 -188
  52. package/src/client-timeout.ts +0 -42
  53. package/src/client.test.ts +0 -447
  54. package/src/client.ts +0 -262
  55. package/src/comment-dispatcher-runtime-api.ts +0 -6
  56. package/src/comment-dispatcher.test.ts +0 -185
  57. package/src/comment-dispatcher.ts +0 -107
  58. package/src/comment-handler-runtime-api.ts +0 -3
  59. package/src/comment-handler.test.ts +0 -592
  60. package/src/comment-handler.ts +0 -303
  61. package/src/comment-reaction.test.ts +0 -138
  62. package/src/comment-reaction.ts +0 -259
  63. package/src/comment-shared.test.ts +0 -183
  64. package/src/comment-shared.ts +0 -406
  65. package/src/comment-target.ts +0 -44
  66. package/src/config-schema.test.ts +0 -326
  67. package/src/config-schema.ts +0 -335
  68. package/src/conversation-id.test.ts +0 -18
  69. package/src/conversation-id.ts +0 -199
  70. package/src/dedup-runtime-api.ts +0 -1
  71. package/src/dedup.ts +0 -141
  72. package/src/dedupe-key.ts +0 -72
  73. package/src/directory.static.ts +0 -61
  74. package/src/directory.test.ts +0 -141
  75. package/src/directory.ts +0 -124
  76. package/src/doc-schema.ts +0 -182
  77. package/src/docx-batch-insert.test.ts +0 -116
  78. package/src/docx-batch-insert.ts +0 -223
  79. package/src/docx-color-text.ts +0 -154
  80. package/src/docx-table-ops.test.ts +0 -53
  81. package/src/docx-table-ops.ts +0 -316
  82. package/src/docx-types.ts +0 -38
  83. package/src/docx.account-selection.test.ts +0 -95
  84. package/src/docx.test.ts +0 -701
  85. package/src/docx.ts +0 -1596
  86. package/src/drive-schema.ts +0 -92
  87. package/src/drive.test.ts +0 -1237
  88. package/src/drive.ts +0 -829
  89. package/src/dynamic-agent.test.ts +0 -155
  90. package/src/dynamic-agent.ts +0 -143
  91. package/src/event-types.ts +0 -45
  92. package/src/external-keys.test.ts +0 -20
  93. package/src/external-keys.ts +0 -19
  94. package/src/lifecycle.test-support.ts +0 -220
  95. package/src/media.test.ts +0 -955
  96. package/src/media.ts +0 -1105
  97. package/src/mention-target.types.ts +0 -5
  98. package/src/mention.ts +0 -114
  99. package/src/message-action-contract.ts +0 -13
  100. package/src/monitor-state-runtime-api.ts +0 -7
  101. package/src/monitor-transport-runtime-api.ts +0 -10
  102. package/src/monitor.account.ts +0 -492
  103. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
  104. package/src/monitor.bot-identity.ts +0 -86
  105. package/src/monitor.bot-menu-handler.ts +0 -165
  106. package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
  107. package/src/monitor.bot-menu.test.ts +0 -188
  108. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
  109. package/src/monitor.card-action.lifecycle.test-support.ts +0 -421
  110. package/src/monitor.cleanup.test.ts +0 -383
  111. package/src/monitor.comment-notice-handler.ts +0 -105
  112. package/src/monitor.comment.test.ts +0 -967
  113. package/src/monitor.comment.ts +0 -1386
  114. package/src/monitor.lifecycle.test.ts +0 -4
  115. package/src/monitor.message-handler.ts +0 -350
  116. package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
  117. package/src/monitor.reaction.test.ts +0 -739
  118. package/src/monitor.startup.test.ts +0 -213
  119. package/src/monitor.startup.ts +0 -74
  120. package/src/monitor.state.defaults.test.ts +0 -46
  121. package/src/monitor.state.ts +0 -170
  122. package/src/monitor.synthetic-error.ts +0 -18
  123. package/src/monitor.test-mocks.ts +0 -46
  124. package/src/monitor.transport.ts +0 -451
  125. package/src/monitor.ts +0 -100
  126. package/src/monitor.webhook-e2e.test.ts +0 -279
  127. package/src/monitor.webhook-security.test.ts +0 -389
  128. package/src/monitor.webhook.test-helpers.ts +0 -116
  129. package/src/outbound-runtime-api.ts +0 -1
  130. package/src/outbound.test.ts +0 -1118
  131. package/src/outbound.ts +0 -785
  132. package/src/perm-schema.ts +0 -52
  133. package/src/perm.ts +0 -170
  134. package/src/pins.ts +0 -108
  135. package/src/policy.test.ts +0 -223
  136. package/src/policy.ts +0 -318
  137. package/src/post.test.ts +0 -105
  138. package/src/post.ts +0 -275
  139. package/src/probe.test.ts +0 -283
  140. package/src/probe.ts +0 -166
  141. package/src/processing-claims.ts +0 -59
  142. package/src/qr-terminal.ts +0 -1
  143. package/src/reactions.ts +0 -123
  144. package/src/reasoning-preview.test.ts +0 -113
  145. package/src/reasoning-preview.ts +0 -28
  146. package/src/reply-dispatcher-runtime-api.ts +0 -7
  147. package/src/reply-dispatcher.test.ts +0 -1513
  148. package/src/reply-dispatcher.ts +0 -748
  149. package/src/runtime.ts +0 -9
  150. package/src/secret-contract.ts +0 -145
  151. package/src/secret-input.ts +0 -1
  152. package/src/security-audit-shared.ts +0 -69
  153. package/src/security-audit.test.ts +0 -59
  154. package/src/security-audit.ts +0 -1
  155. package/src/send-result.ts +0 -80
  156. package/src/send-target.test.ts +0 -86
  157. package/src/send-target.ts +0 -35
  158. package/src/send.reply-fallback.test.ts +0 -417
  159. package/src/send.test.ts +0 -621
  160. package/src/send.ts +0 -861
  161. package/src/sequential-key.test.ts +0 -72
  162. package/src/sequential-key.ts +0 -25
  163. package/src/sequential-queue.test.ts +0 -165
  164. package/src/sequential-queue.ts +0 -86
  165. package/src/session-conversation.ts +0 -42
  166. package/src/session-route.ts +0 -48
  167. package/src/setup-core.ts +0 -51
  168. package/src/setup-surface.test.ts +0 -484
  169. package/src/setup-surface.ts +0 -618
  170. package/src/streaming-card.test.ts +0 -397
  171. package/src/streaming-card.ts +0 -571
  172. package/src/subagent-hooks.test.ts +0 -627
  173. package/src/subagent-hooks.ts +0 -413
  174. package/src/targets.ts +0 -97
  175. package/src/test-support/lifecycle-test-support.ts +0 -454
  176. package/src/thread-bindings.test.ts +0 -180
  177. package/src/thread-bindings.ts +0 -331
  178. package/src/tool-account-routing.test.ts +0 -250
  179. package/src/tool-account.test.ts +0 -44
  180. package/src/tool-account.ts +0 -93
  181. package/src/tool-factory-test-harness.ts +0 -79
  182. package/src/tool-result.test.ts +0 -32
  183. package/src/tool-result.ts +0 -16
  184. package/src/tools-config.test.ts +0 -21
  185. package/src/tools-config.ts +0 -22
  186. package/src/types.ts +0 -106
  187. package/src/typing.test.ts +0 -144
  188. package/src/typing.ts +0 -214
  189. package/src/wiki-schema.ts +0 -69
  190. package/src/wiki.ts +0 -270
  191. package/subagent-hooks-api.ts +0 -31
  192. package/tsconfig.json +0 -16
@@ -1,1386 +0,0 @@
1
- import { formatErrorMessage } from "klaw/plugin-sdk/error-runtime";
2
- import type { ClawdbotConfig } from "../runtime-api.js";
3
- import { raceWithTimeoutAndAbort } from "./async.js";
4
- import { createFeishuClient } from "./client.js";
5
- import {
6
- encodeQuery,
7
- extractReplyText,
8
- isRecord,
9
- normalizeString,
10
- parseCommentContentElements,
11
- type ParsedCommentContent,
12
- type ParsedCommentLinkedDocument,
13
- readString,
14
- } from "./comment-shared.js";
15
- import { normalizeCommentFileType, type CommentFileType } from "./comment-target.js";
16
- import type { ResolvedFeishuAccount } from "./types.js";
17
-
18
- const FEISHU_COMMENT_VERIFY_TIMEOUT_MS = 3_000;
19
- const FEISHU_COMMENT_LIST_PAGE_SIZE = 100;
20
- const FEISHU_COMMENT_LIST_PAGE_LIMIT = 5;
21
- const FEISHU_COMMENT_REPLY_PAGE_SIZE = 100;
22
- const FEISHU_COMMENT_REPLY_PAGE_LIMIT = 5;
23
- const FEISHU_COMMENT_REPLY_MISS_RETRY_DELAY_MS = 1_000;
24
- const FEISHU_COMMENT_REPLY_MISS_RETRY_LIMIT = 6;
25
- const FEISHU_COMMENT_THREAD_PROMPT_LIMIT = 20;
26
- const FEISHU_WHOLE_COMMENT_PROMPT_LIMIT = 12;
27
- const FEISHU_PROMPT_TEXT_LIMIT = 220;
28
-
29
- type FeishuDriveCommentUserId = {
30
- open_id?: string;
31
- user_id?: string;
32
- union_id?: string;
33
- };
34
-
35
- export type FeishuDriveCommentNoticeEvent = {
36
- comment_id?: string;
37
- event_id?: string;
38
- is_mentioned?: boolean;
39
- notice_meta?: {
40
- file_token?: string;
41
- file_type?: string;
42
- from_user_id?: FeishuDriveCommentUserId;
43
- notice_type?: string;
44
- to_user_id?: FeishuDriveCommentUserId;
45
- };
46
- reply_id?: string;
47
- timestamp?: string;
48
- type?: string;
49
- };
50
-
51
- type ResolveDriveCommentEventParams = {
52
- cfg: ClawdbotConfig;
53
- accountId: string;
54
- event: FeishuDriveCommentNoticeEvent;
55
- account?: ResolvedFeishuAccount;
56
- botOpenId?: string;
57
- createClient?: (account: ResolvedFeishuAccount) => FeishuRequestClient;
58
- verificationTimeoutMs?: number;
59
- logger?: (message: string) => void;
60
- waitMs?: (ms: number) => Promise<void>;
61
- };
62
-
63
- type ResolvedDriveCommentEventTurn = {
64
- eventId: string;
65
- messageId: string;
66
- commentId: string;
67
- replyId?: string;
68
- noticeType: "add_comment" | "add_reply";
69
- fileToken: string;
70
- fileType: CommentFileType;
71
- isWholeComment?: boolean;
72
- senderId: string;
73
- senderUserId?: string;
74
- timestamp?: string;
75
- isMentioned?: boolean;
76
- documentTitle?: string;
77
- documentUrl?: string;
78
- quoteText?: string;
79
- rootCommentText?: string;
80
- targetReplyText?: string;
81
- prompt: string;
82
- preview: string;
83
- };
84
-
85
- type FeishuRequestClient = ReturnType<typeof createFeishuClient> & {
86
- request(params: {
87
- method: "GET" | "POST";
88
- url: string;
89
- data: unknown;
90
- timeout: number;
91
- }): Promise<unknown>;
92
- };
93
-
94
- type FeishuOpenApiResponse<T> = {
95
- code?: number;
96
- log_id?: string;
97
- msg?: string;
98
- data?: T;
99
- };
100
-
101
- type FeishuDriveMetaBatchQueryResponse = FeishuOpenApiResponse<{
102
- metas?: Array<{
103
- doc_token?: string;
104
- title?: string;
105
- url?: string;
106
- }>;
107
- }>;
108
-
109
- type FeishuDriveCommentReply = {
110
- reply_id?: string;
111
- user_id?: string;
112
- create_time?: number;
113
- update_time?: number;
114
- content?: {
115
- elements?: unknown[];
116
- };
117
- };
118
-
119
- type FeishuDriveCommentCard = {
120
- comment_id?: string;
121
- user_id?: string;
122
- create_time?: number;
123
- update_time?: number;
124
- is_whole?: boolean;
125
- has_more?: boolean;
126
- page_token?: string;
127
- quote?: string;
128
- reply_list?: {
129
- replies?: FeishuDriveCommentReply[];
130
- };
131
- };
132
-
133
- type FeishuDriveCommentBatchQueryResponse = FeishuOpenApiResponse<{
134
- items?: FeishuDriveCommentCard[];
135
- }>;
136
-
137
- type FeishuDriveCommentListResponse = FeishuOpenApiResponse<{
138
- has_more?: boolean;
139
- items?: FeishuDriveCommentCard[];
140
- page_token?: string;
141
- }>;
142
-
143
- type FeishuDriveCommentRepliesListResponse = FeishuOpenApiResponse<{
144
- has_more?: boolean;
145
- items?: FeishuDriveCommentReply[];
146
- page_token?: string;
147
- }>;
148
-
149
- type ResolvedCommentReplyContext = {
150
- replyId?: string;
151
- userId?: string;
152
- createTime?: number;
153
- isBotAuthored: boolean;
154
- content: ParsedCommentContent;
155
- };
156
-
157
- type ResolvedWholeCommentTimelineEntry = {
158
- commentId: string;
159
- userId?: string;
160
- createTime?: number;
161
- isCurrentComment: boolean;
162
- isBotAuthored: boolean;
163
- content: ParsedCommentContent;
164
- };
165
-
166
- function readBoolean(value: unknown): boolean | undefined {
167
- return typeof value === "boolean" ? value : undefined;
168
- }
169
-
170
- function safeJsonStringify(value: unknown): string {
171
- try {
172
- return JSON.stringify(value);
173
- } catch (error) {
174
- return JSON.stringify({
175
- error: formatErrorMessage(error),
176
- });
177
- }
178
- }
179
-
180
- function truncatePromptText(
181
- text: string | undefined,
182
- maxLength = FEISHU_PROMPT_TEXT_LIMIT,
183
- ): string {
184
- const normalized = normalizeString(text);
185
- if (!normalized) {
186
- return "";
187
- }
188
- return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}…` : normalized;
189
- }
190
-
191
- function formatPromptTextValue(text: string | undefined): string {
192
- return safeJsonStringify(truncatePromptText(text) || "");
193
- }
194
-
195
- function formatPromptBoolean(value: boolean | undefined): string {
196
- return value === true ? "yes" : "no";
197
- }
198
-
199
- function buildDriveCommentsListUrl(params: {
200
- fileToken: string;
201
- fileType: CommentFileType;
202
- pageToken?: string;
203
- isWholeOnly?: boolean;
204
- }): string {
205
- return (
206
- `/open-apis/drive/v1/files/${encodeURIComponent(params.fileToken)}/comments` +
207
- encodeQuery({
208
- file_type: params.fileType,
209
- is_whole: params.isWholeOnly === true ? "true" : undefined,
210
- page_size: String(FEISHU_COMMENT_LIST_PAGE_SIZE),
211
- page_token: params.pageToken,
212
- user_id_type: "open_id",
213
- })
214
- );
215
- }
216
-
217
- function compareCommentTimelineEntries(
218
- left: { createTime?: number; stableId?: string },
219
- right: { createTime?: number; stableId?: string },
220
- ): number {
221
- const leftTime = left.createTime ?? Number.MAX_SAFE_INTEGER;
222
- const rightTime = right.createTime ?? Number.MAX_SAFE_INTEGER;
223
- if (leftTime !== rightTime) {
224
- return leftTime - rightTime;
225
- }
226
- return (left.stableId ?? "").localeCompare(right.stableId ?? "");
227
- }
228
-
229
- function formatLinkedDocumentInline(link: ParsedCommentLinkedDocument): string {
230
- const parts = [
231
- `raw_url=${link.rawUrl}`,
232
- `url_kind=${link.urlKind}`,
233
- link.wikiNodeToken ? `wiki_node_token=${link.wikiNodeToken}` : null,
234
- `resolved_type=${link.resolvedObjType ?? "UNKNOWN"}`,
235
- `resolved_token=${link.resolvedObjToken ?? "UNKNOWN"}`,
236
- `same_as_current_document=${formatPromptBoolean(link.isCurrentDocument)}`,
237
- ].filter((part): part is string => Boolean(part));
238
- return parts.join(" ");
239
- }
240
-
241
- function formatLinkedDocumentsPromptLines(params: {
242
- title: string;
243
- linkedDocuments: ParsedCommentLinkedDocument[];
244
- }): string[] {
245
- if (params.linkedDocuments.length === 0) {
246
- return [];
247
- }
248
- return [
249
- params.title,
250
- ...params.linkedDocuments.map(
251
- (link, index) => `- [${index + 1}] ${formatLinkedDocumentInline(link)}`,
252
- ),
253
- ];
254
- }
255
-
256
- function formatLinkedDocumentsInlineSummary(
257
- linkedDocuments: ParsedCommentLinkedDocument[],
258
- ): string {
259
- if (linkedDocuments.length === 0) {
260
- return "none";
261
- }
262
- return linkedDocuments
263
- .map(
264
- (link) =>
265
- `${link.resolvedObjType ?? link.urlKind}:${link.resolvedObjToken ?? link.wikiNodeToken ?? "UNKNOWN"}`,
266
- )
267
- .join(",");
268
- }
269
-
270
- function summarizeCommentRepliesForLog(replies: FeishuDriveCommentReply[]): string {
271
- return safeJsonStringify(
272
- replies.map((reply) => ({
273
- reply_id: reply.reply_id,
274
- text_len: extractReplyText(reply)?.length ?? 0,
275
- })),
276
- );
277
- }
278
-
279
- async function resolveParsedCommentContent(params: {
280
- elements?: unknown[];
281
- botOpenIds?: Iterable<string | undefined>;
282
- currentDocument: {
283
- fileType: CommentFileType;
284
- fileToken: string;
285
- };
286
- client: FeishuRequestClient;
287
- wikiCache: Map<
288
- string,
289
- Promise<{
290
- resolvedObjType?: CommentFileType;
291
- resolvedObjToken?: string;
292
- } | null>
293
- >;
294
- logger?: (message: string) => void;
295
- accountId: string;
296
- }): Promise<ParsedCommentContent> {
297
- const parsed = parseCommentContentElements({
298
- elements: params.elements,
299
- botOpenIds: params.botOpenIds,
300
- currentDocument: params.currentDocument,
301
- });
302
- if (!parsed.linkedDocuments.some((link) => link.urlKind === "wiki" && link.wikiNodeToken)) {
303
- return parsed;
304
- }
305
-
306
- const resolvedLinkedDocuments = await Promise.all(
307
- parsed.linkedDocuments.map(async (link) => {
308
- if (link.urlKind !== "wiki" || !link.wikiNodeToken) {
309
- return link;
310
- }
311
- let pending = params.wikiCache.get(link.wikiNodeToken);
312
- if (!pending) {
313
- pending = params.client.wiki.space
314
- .getNode({
315
- params: {
316
- token: link.wikiNodeToken,
317
- },
318
- })
319
- .then((response) => {
320
- if (response.code !== 0) {
321
- params.logger?.(
322
- `feishu[${params.accountId}]: wiki link resolution failed token=${link.wikiNodeToken} ` +
323
- `code=${response.code ?? "unknown"} msg=${response.msg ?? "unknown"}`,
324
- );
325
- return null;
326
- }
327
- const objType = normalizeCommentFileType(response.data?.node?.obj_type);
328
- const objToken = normalizeString(response.data?.node?.obj_token);
329
- if (!objType || !objToken) {
330
- return null;
331
- }
332
- return {
333
- resolvedObjType: objType,
334
- resolvedObjToken: objToken,
335
- };
336
- })
337
- .catch((error) => {
338
- params.logger?.(
339
- `feishu[${params.accountId}]: wiki link resolution threw token=${link.wikiNodeToken} error=${formatErrorMessage(error)}`,
340
- );
341
- return null;
342
- });
343
- params.wikiCache.set(link.wikiNodeToken, pending);
344
- }
345
- const resolved = await pending;
346
- if (!resolved) {
347
- return link;
348
- }
349
- return {
350
- ...link,
351
- resolvedObjType: resolved.resolvedObjType,
352
- resolvedObjToken: resolved.resolvedObjToken,
353
- isCurrentDocument:
354
- resolved.resolvedObjType === params.currentDocument.fileType &&
355
- resolved.resolvedObjToken === params.currentDocument.fileToken,
356
- };
357
- }),
358
- );
359
-
360
- return {
361
- ...parsed,
362
- linkedDocuments: resolvedLinkedDocuments,
363
- };
364
- }
365
-
366
- async function delayMs(ms: number): Promise<void> {
367
- await new Promise((resolve) => setTimeout(resolve, ms));
368
- }
369
-
370
- function buildDriveCommentTargetUrl(params: {
371
- fileToken: string;
372
- fileType: CommentFileType;
373
- }): string {
374
- return (
375
- `/open-apis/drive/v1/files/${encodeURIComponent(params.fileToken)}/comments/batch_query` +
376
- encodeQuery({
377
- file_type: params.fileType,
378
- user_id_type: "open_id",
379
- })
380
- );
381
- }
382
-
383
- function buildDriveCommentRepliesUrl(params: {
384
- fileToken: string;
385
- commentId: string;
386
- fileType: CommentFileType;
387
- pageToken?: string;
388
- }): string {
389
- return (
390
- `/open-apis/drive/v1/files/${encodeURIComponent(params.fileToken)}/comments/${encodeURIComponent(
391
- params.commentId,
392
- )}/replies` +
393
- encodeQuery({
394
- file_type: params.fileType,
395
- page_token: params.pageToken,
396
- page_size: String(FEISHU_COMMENT_REPLY_PAGE_SIZE),
397
- user_id_type: "open_id",
398
- })
399
- );
400
- }
401
-
402
- async function fetchDriveComments(params: {
403
- client: FeishuRequestClient;
404
- fileToken: string;
405
- fileType: CommentFileType;
406
- isWholeOnly?: boolean;
407
- timeoutMs: number;
408
- logger?: (message: string) => void;
409
- accountId: string;
410
- }): Promise<FeishuDriveCommentCard[]> {
411
- const comments: FeishuDriveCommentCard[] = [];
412
- let pageToken: string | undefined;
413
- for (let page = 0; page < FEISHU_COMMENT_LIST_PAGE_LIMIT; page += 1) {
414
- const response = await requestFeishuOpenApi<FeishuDriveCommentListResponse>({
415
- client: params.client,
416
- method: "GET",
417
- url: buildDriveCommentsListUrl({
418
- fileToken: params.fileToken,
419
- fileType: params.fileType,
420
- isWholeOnly: params.isWholeOnly,
421
- pageToken,
422
- }),
423
- timeoutMs: params.timeoutMs,
424
- logger: params.logger,
425
- errorLabel: `feishu[${params.accountId}]: failed to list drive comments for ${params.fileToken}`,
426
- });
427
- if (response?.code !== 0) {
428
- if (response) {
429
- params.logger?.(
430
- `feishu[${params.accountId}]: failed to list drive comments for ${params.fileToken}: ` +
431
- `${response.msg ?? "unknown error"} log_id=${response.log_id?.trim() || "unknown"}`,
432
- );
433
- }
434
- break;
435
- }
436
- comments.push(...(response.data?.items ?? []));
437
- if (response.data?.has_more !== true || !response.data.page_token?.trim()) {
438
- break;
439
- }
440
- pageToken = response.data.page_token.trim();
441
- }
442
- return comments;
443
- }
444
-
445
- async function requestFeishuOpenApi<T>(params: {
446
- client: FeishuRequestClient;
447
- method: "GET" | "POST";
448
- url: string;
449
- data?: unknown;
450
- timeoutMs: number;
451
- logger?: (message: string) => void;
452
- errorLabel: string;
453
- }): Promise<T | null> {
454
- const formatErrorDetails = (error: unknown): string => {
455
- if (!isRecord(error)) {
456
- return typeof error === "string" ? error : JSON.stringify(error);
457
- }
458
- const response = isRecord(error.response) ? error.response : undefined;
459
- const responseData = isRecord(response?.data) ? response?.data : undefined;
460
- const details = {
461
- message:
462
- typeof error.message === "string"
463
- ? error.message
464
- : typeof error === "string"
465
- ? error
466
- : JSON.stringify(error),
467
- code: readString(error.code),
468
- method: readString(isRecord(error.config) ? error.config.method : undefined),
469
- url: readString(isRecord(error.config) ? error.config.url : undefined),
470
- http_status: typeof response?.status === "number" ? response.status : undefined,
471
- feishu_code:
472
- typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
473
- feishu_msg: readString(responseData?.msg),
474
- feishu_log_id: readString(responseData?.log_id),
475
- };
476
- return safeJsonStringify(details);
477
- };
478
-
479
- const result = await raceWithTimeoutAndAbort(
480
- params.client.request({
481
- method: params.method,
482
- url: params.url,
483
- data: params.data ?? {},
484
- timeout: params.timeoutMs,
485
- }),
486
- { timeoutMs: params.timeoutMs },
487
- )
488
- .then((resolved) => (resolved.status === "resolved" ? resolved.value : null))
489
- .catch((error) => {
490
- params.logger?.(`${params.errorLabel}: ${formatErrorDetails(error)}`);
491
- return null;
492
- });
493
- if (!result) {
494
- params.logger?.(`${params.errorLabel}: request timed out or returned no data`);
495
- }
496
- return result;
497
- }
498
-
499
- async function fetchDriveCommentReplies(params: {
500
- client: FeishuRequestClient;
501
- fileToken: string;
502
- fileType: CommentFileType;
503
- commentId: string;
504
- timeoutMs: number;
505
- logger?: (message: string) => void;
506
- accountId: string;
507
- }): Promise<{ replies: FeishuDriveCommentReply[]; logIds: string[] }> {
508
- const replies: FeishuDriveCommentReply[] = [];
509
- const logIds: string[] = [];
510
- let pageToken: string | undefined;
511
- for (let page = 0; page < FEISHU_COMMENT_REPLY_PAGE_LIMIT; page += 1) {
512
- const response = await requestFeishuOpenApi<FeishuDriveCommentRepliesListResponse>({
513
- client: params.client,
514
- method: "GET",
515
- url: buildDriveCommentRepliesUrl({
516
- fileToken: params.fileToken,
517
- commentId: params.commentId,
518
- fileType: params.fileType,
519
- pageToken,
520
- }),
521
- timeoutMs: params.timeoutMs,
522
- logger: params.logger,
523
- errorLabel: `feishu[${params.accountId}]: failed to fetch comment replies for ${params.commentId}`,
524
- });
525
- if (response?.log_id?.trim()) {
526
- logIds.push(response.log_id.trim());
527
- }
528
- if (response?.code !== 0) {
529
- if (response) {
530
- params.logger?.(
531
- `feishu[${params.accountId}]: failed to fetch comment replies for ${params.commentId}: ` +
532
- `${response.msg ?? "unknown error"} ` +
533
- `log_id=${response.log_id?.trim() || "unknown"}`,
534
- );
535
- }
536
- break;
537
- }
538
- replies.push(...(response.data?.items ?? []));
539
- if (response.data?.has_more !== true || !response.data.page_token?.trim()) {
540
- break;
541
- }
542
- pageToken = response.data.page_token.trim();
543
- }
544
- return { replies, logIds };
545
- }
546
-
547
- async function resolveCommentReplyContext(params: {
548
- reply: FeishuDriveCommentReply;
549
- botOpenIds?: Iterable<string | undefined>;
550
- currentDocument: {
551
- fileType: CommentFileType;
552
- fileToken: string;
553
- };
554
- client: FeishuRequestClient;
555
- wikiCache: Map<
556
- string,
557
- Promise<{
558
- resolvedObjType?: CommentFileType;
559
- resolvedObjToken?: string;
560
- } | null>
561
- >;
562
- logger?: (message: string) => void;
563
- accountId: string;
564
- }): Promise<ResolvedCommentReplyContext> {
565
- const userId = normalizeString(params.reply.user_id);
566
- const normalizedBotOpenIds = new Set(
567
- Array.from(params.botOpenIds ?? [])
568
- .map((botId) => normalizeString(botId))
569
- .filter((botId): botId is string => Boolean(botId)),
570
- );
571
- return {
572
- replyId: normalizeString(params.reply.reply_id),
573
- userId,
574
- createTime: typeof params.reply.create_time === "number" ? params.reply.create_time : undefined,
575
- isBotAuthored: typeof userId === "string" && normalizedBotOpenIds.has(userId),
576
- content: await resolveParsedCommentContent({
577
- elements: isRecord(params.reply.content) ? params.reply.content.elements : undefined,
578
- botOpenIds: params.botOpenIds,
579
- currentDocument: params.currentDocument,
580
- client: params.client,
581
- wikiCache: params.wikiCache,
582
- logger: params.logger,
583
- accountId: params.accountId,
584
- }),
585
- };
586
- }
587
-
588
- function selectCommentThreadPromptReplies(
589
- replies: ResolvedCommentReplyContext[],
590
- targetReplyId?: string,
591
- ): ResolvedCommentReplyContext[] {
592
- if (replies.length <= FEISHU_COMMENT_THREAD_PROMPT_LIMIT) {
593
- return replies;
594
- }
595
- const targetIndex = replies.findIndex((reply) => reply.replyId === targetReplyId);
596
- const currentIndex = targetIndex >= 0 ? targetIndex : replies.length - 1;
597
- const selected = new Set<number>([0, currentIndex, replies.length - 1]);
598
- for (let radius = 1; selected.size < FEISHU_COMMENT_THREAD_PROMPT_LIMIT; radius += 1) {
599
- const before = currentIndex - radius;
600
- const after = currentIndex + radius;
601
- if (before >= 0) {
602
- selected.add(before);
603
- }
604
- if (selected.size >= FEISHU_COMMENT_THREAD_PROMPT_LIMIT) {
605
- break;
606
- }
607
- if (after < replies.length) {
608
- selected.add(after);
609
- }
610
- if (before < 0 && after >= replies.length) {
611
- break;
612
- }
613
- }
614
- return [...selected]
615
- .toSorted((left, right) => left - right)
616
- .map((index) => replies[index])
617
- .filter((reply): reply is ResolvedCommentReplyContext => Boolean(reply));
618
- }
619
-
620
- function formatCommentThreadPromptLines(params: {
621
- replies: ResolvedCommentReplyContext[];
622
- targetReplyId?: string;
623
- }): string[] {
624
- const promptReplies = selectCommentThreadPromptReplies(params.replies, params.targetReplyId);
625
- return promptReplies.map((reply, index) => {
626
- const text = reply.content.semanticText ?? reply.content.plainText;
627
- return (
628
- `- [${index + 1}] author=${reply.isBotAuthored ? "assistant" : "user"} ` +
629
- `user_id=${reply.userId ?? "UNKNOWN"} ` +
630
- `reply_id=${reply.replyId ?? "UNKNOWN"} ` +
631
- `current_event=${reply.replyId === params.targetReplyId ? "yes" : "no"} ` +
632
- `text=${formatPromptTextValue(text)} ` +
633
- `referenced_docs=${formatLinkedDocumentsInlineSummary(reply.content.linkedDocuments)}`
634
- );
635
- });
636
- }
637
-
638
- function findNearestBotTimelineEntry(params: {
639
- entries: ResolvedWholeCommentTimelineEntry[];
640
- currentIndex: number;
641
- direction: "before" | "after";
642
- }): ResolvedWholeCommentTimelineEntry | undefined {
643
- const step = params.direction === "after" ? 1 : -1;
644
- for (
645
- let index = params.currentIndex + step;
646
- index >= 0 && index < params.entries.length;
647
- index += step
648
- ) {
649
- const candidate = params.entries[index];
650
- if (candidate?.isBotAuthored) {
651
- return candidate;
652
- }
653
- }
654
- return undefined;
655
- }
656
-
657
- function selectWholeCommentTimelineEntries(params: {
658
- entries: ResolvedWholeCommentTimelineEntry[];
659
- currentCommentId: string;
660
- }): ResolvedWholeCommentTimelineEntry[] {
661
- if (params.entries.length <= FEISHU_WHOLE_COMMENT_PROMPT_LIMIT) {
662
- return params.entries;
663
- }
664
- const currentIndex = params.entries.findIndex(
665
- (entry) => entry.commentId === params.currentCommentId,
666
- );
667
- if (currentIndex < 0) {
668
- return params.entries.slice(-FEISHU_WHOLE_COMMENT_PROMPT_LIMIT);
669
- }
670
- const selected = new Set<number>([currentIndex]);
671
- const nearestBotAfter = params.entries.findIndex(
672
- (entry, index) => index > currentIndex && entry.isBotAuthored,
673
- );
674
- if (nearestBotAfter >= 0) {
675
- selected.add(nearestBotAfter);
676
- }
677
- for (let index = currentIndex - 1; index >= 0; index -= 1) {
678
- if (params.entries[index]?.isBotAuthored) {
679
- selected.add(index);
680
- break;
681
- }
682
- }
683
- for (let radius = 1; selected.size < FEISHU_WHOLE_COMMENT_PROMPT_LIMIT; radius += 1) {
684
- const before = currentIndex - radius;
685
- const after = currentIndex + radius;
686
- if (before >= 0) {
687
- selected.add(before);
688
- }
689
- if (selected.size >= FEISHU_WHOLE_COMMENT_PROMPT_LIMIT) {
690
- break;
691
- }
692
- if (after < params.entries.length) {
693
- selected.add(after);
694
- }
695
- if (before < 0 && after >= params.entries.length) {
696
- break;
697
- }
698
- }
699
- return [...selected]
700
- .toSorted((left, right) => left - right)
701
- .map((index) => params.entries[index])
702
- .filter((entry): entry is ResolvedWholeCommentTimelineEntry => Boolean(entry));
703
- }
704
-
705
- function formatWholeCommentTimelinePromptLines(params: {
706
- entries: ResolvedWholeCommentTimelineEntry[];
707
- currentCommentId: string;
708
- }): string[] {
709
- return selectWholeCommentTimelineEntries(params).map((entry, index) => {
710
- const text = entry.content.semanticText ?? entry.content.plainText;
711
- return (
712
- `- [${index + 1}] create_time=${entry.createTime ?? "UNKNOWN"} ` +
713
- `comment_id=${entry.commentId} ` +
714
- `author=${entry.isBotAuthored ? "assistant" : "user"} ` +
715
- `user_id=${entry.userId ?? "UNKNOWN"} ` +
716
- `current_comment=${entry.commentId === params.currentCommentId ? "yes" : "no"} ` +
717
- `text=${formatPromptTextValue(text)} ` +
718
- `referenced_docs=${formatLinkedDocumentsInlineSummary(entry.content.linkedDocuments)}`
719
- );
720
- });
721
- }
722
-
723
- async function fetchDriveCommentContext(params: {
724
- client: FeishuRequestClient;
725
- fileToken: string;
726
- fileType: CommentFileType;
727
- commentId: string;
728
- replyId?: string;
729
- botOpenIds?: Iterable<string | undefined>;
730
- timeoutMs: number;
731
- logger?: (message: string) => void;
732
- accountId: string;
733
- waitMs: (ms: number) => Promise<void>;
734
- }): Promise<{
735
- documentTitle?: string;
736
- documentUrl?: string;
737
- isWholeComment?: boolean;
738
- quoteText?: string;
739
- rootCommentText?: string;
740
- targetReplyText?: string;
741
- rootCommentContent?: ParsedCommentContent;
742
- targetReplyContent?: ParsedCommentContent;
743
- currentCommentThreadReplies: ResolvedCommentReplyContext[];
744
- wholeCommentTimeline: ResolvedWholeCommentTimelineEntry[];
745
- nearestBotWholeCommentAfter?: ResolvedWholeCommentTimelineEntry;
746
- nearestBotWholeCommentBefore?: ResolvedWholeCommentTimelineEntry;
747
- }> {
748
- const [metaResponse, commentResponse] = await Promise.all([
749
- requestFeishuOpenApi<FeishuDriveMetaBatchQueryResponse>({
750
- client: params.client,
751
- method: "POST",
752
- url: "/open-apis/drive/v1/metas/batch_query",
753
- data: {
754
- request_docs: [{ doc_token: params.fileToken, doc_type: params.fileType }],
755
- with_url: true,
756
- },
757
- timeoutMs: params.timeoutMs,
758
- logger: params.logger,
759
- errorLabel: `feishu[${params.accountId}]: failed to fetch drive metadata for ${params.fileToken}`,
760
- }),
761
- requestFeishuOpenApi<FeishuDriveCommentBatchQueryResponse>({
762
- client: params.client,
763
- method: "POST",
764
- url: buildDriveCommentTargetUrl({
765
- fileToken: params.fileToken,
766
- fileType: params.fileType,
767
- }),
768
- data: {
769
- comment_ids: [params.commentId],
770
- },
771
- timeoutMs: params.timeoutMs,
772
- logger: params.logger,
773
- errorLabel: `feishu[${params.accountId}]: failed to fetch drive comment ${params.commentId}`,
774
- }),
775
- ]);
776
- const wikiCache = new Map<
777
- string,
778
- Promise<{
779
- resolvedObjType?: CommentFileType;
780
- resolvedObjToken?: string;
781
- } | null>
782
- >();
783
-
784
- const commentCard =
785
- commentResponse?.code === 0
786
- ? (commentResponse.data?.items ?? []).find(
787
- (item) => item.comment_id?.trim() === params.commentId,
788
- )
789
- : undefined;
790
- const embeddedReplies = commentCard?.reply_list?.replies ?? [];
791
- params.logger?.(
792
- `feishu[${params.accountId}]: embedded comment replies comment=${params.commentId} ` +
793
- `count=${embeddedReplies.length} summary=${summarizeCommentRepliesForLog(embeddedReplies)}`,
794
- );
795
- const embeddedTargetReply = params.replyId
796
- ? embeddedReplies.find((reply) => reply.reply_id?.trim() === params.replyId?.trim())
797
- : embeddedReplies.at(-1);
798
-
799
- let replies = embeddedReplies;
800
- let fetchedMatchedReply = params.replyId
801
- ? replies.find((reply) => reply.reply_id?.trim() === params.replyId?.trim())
802
- : undefined;
803
- const needsExtraReplies =
804
- !embeddedTargetReply || replies.length === 0 || commentCard?.has_more === true;
805
- if (needsExtraReplies) {
806
- params.logger?.(
807
- `feishu[${params.accountId}]: fetching extra comment replies comment=${params.commentId} ` +
808
- `requested_reply=${params.replyId ?? "none"} ` +
809
- `embedded_count=${embeddedReplies.length} ` +
810
- `embedded_hit=${embeddedTargetReply ? "yes" : "no"} ` +
811
- `embedded_has_more=${commentCard?.has_more === true ? "yes" : "no"}`,
812
- );
813
- const fetched = await fetchDriveCommentReplies(params);
814
- if (fetched.replies.length > 0) {
815
- params.logger?.(
816
- `feishu[${params.accountId}]: fetched extra comment replies comment=${params.commentId} ` +
817
- `count=${fetched.replies.length} ` +
818
- `log_ids=${safeJsonStringify(fetched.logIds)} ` +
819
- `summary=${summarizeCommentRepliesForLog(fetched.replies)}`,
820
- );
821
- replies = fetched.replies;
822
- fetchedMatchedReply = params.replyId
823
- ? replies.find((reply) => reply.reply_id?.trim() === params.replyId?.trim())
824
- : undefined;
825
- }
826
- if (params.replyId && !embeddedTargetReply && !fetchedMatchedReply) {
827
- for (let attempt = 1; attempt <= FEISHU_COMMENT_REPLY_MISS_RETRY_LIMIT; attempt += 1) {
828
- params.logger?.(
829
- `feishu[${params.accountId}]: retrying comment reply lookup comment=${params.commentId} ` +
830
- `requested_reply=${params.replyId} attempt=${attempt}/${FEISHU_COMMENT_REPLY_MISS_RETRY_LIMIT} ` +
831
- `delay_ms=${FEISHU_COMMENT_REPLY_MISS_RETRY_DELAY_MS}`,
832
- );
833
- await params.waitMs(FEISHU_COMMENT_REPLY_MISS_RETRY_DELAY_MS);
834
- const retried = await fetchDriveCommentReplies(params);
835
- if (retried.replies.length > 0) {
836
- params.logger?.(
837
- `feishu[${params.accountId}]: fetched retried comment replies comment=${params.commentId} ` +
838
- `attempt=${attempt} count=${retried.replies.length} ` +
839
- `log_ids=${safeJsonStringify(retried.logIds)} ` +
840
- `summary=${summarizeCommentRepliesForLog(retried.replies)}`,
841
- );
842
- replies = retried.replies;
843
- }
844
- fetchedMatchedReply = replies.find((reply) => reply.reply_id?.trim() === params.replyId);
845
- if (fetchedMatchedReply) {
846
- break;
847
- }
848
- }
849
- }
850
- }
851
-
852
- const rootReply = replies[0] ?? embeddedReplies[0];
853
- const targetReply = params.replyId
854
- ? (embeddedTargetReply ?? fetchedMatchedReply ?? undefined)
855
- : (replies.at(-1) ?? embeddedTargetReply ?? rootReply);
856
- const matchSource = params.replyId
857
- ? embeddedTargetReply
858
- ? "embedded"
859
- : fetchedMatchedReply
860
- ? "fetched"
861
- : "miss"
862
- : targetReply === rootReply
863
- ? "fallback_root"
864
- : targetReply === embeddedTargetReply
865
- ? "embedded_latest"
866
- : "fetched_latest";
867
- params.logger?.(
868
- `feishu[${params.accountId}]: comment reply resolution comment=${params.commentId} ` +
869
- `requested_reply=${params.replyId ?? "none"} match_source=${matchSource} ` +
870
- `root=${safeJsonStringify({ reply_id: rootReply?.reply_id, text_len: extractReplyText(rootReply)?.length ?? 0 })} ` +
871
- `target=${safeJsonStringify({ reply_id: targetReply?.reply_id, text_len: extractReplyText(targetReply)?.length ?? 0 })}`,
872
- );
873
- const meta = metaResponse?.code === 0 ? metaResponse.data?.metas?.[0] : undefined;
874
- const currentDocument = {
875
- fileType: params.fileType,
876
- fileToken: params.fileToken,
877
- };
878
- const resolvedReplies = await Promise.all(
879
- replies.map((reply) =>
880
- resolveCommentReplyContext({
881
- reply,
882
- botOpenIds: params.botOpenIds,
883
- currentDocument,
884
- client: params.client,
885
- wikiCache,
886
- logger: params.logger,
887
- accountId: params.accountId,
888
- }),
889
- ),
890
- );
891
- resolvedReplies.sort((left, right) =>
892
- compareCommentTimelineEntries(
893
- {
894
- createTime: left.createTime,
895
- stableId: left.replyId,
896
- },
897
- {
898
- createTime: right.createTime,
899
- stableId: right.replyId,
900
- },
901
- ),
902
- );
903
- const rootReplyContext =
904
- resolvedReplies.find((reply) => reply.replyId === normalizeString(rootReply?.reply_id)) ??
905
- resolvedReplies[0];
906
- const targetReplyContext =
907
- resolvedReplies.find((reply) => reply.replyId === normalizeString(targetReply?.reply_id)) ??
908
- (params.replyId ? undefined : (resolvedReplies.at(-1) ?? rootReplyContext));
909
-
910
- let wholeCommentTimeline: ResolvedWholeCommentTimelineEntry[] = [];
911
- if (commentCard?.is_whole === true) {
912
- const allComments = await fetchDriveComments({
913
- client: params.client,
914
- fileToken: params.fileToken,
915
- fileType: params.fileType,
916
- isWholeOnly: true,
917
- timeoutMs: params.timeoutMs,
918
- logger: params.logger,
919
- accountId: params.accountId,
920
- });
921
- const wholeComments = allComments.filter((comment) => comment.is_whole === true);
922
- wholeCommentTimeline = await Promise.all(
923
- wholeComments.map(async (comment) => {
924
- const rootWholeReply = comment.reply_list?.replies?.[0];
925
- const normalizedBotOpenIds = new Set(
926
- Array.from(params.botOpenIds ?? [])
927
- .map((botId) => normalizeString(botId))
928
- .filter((botId): botId is string => Boolean(botId)),
929
- );
930
- const content = await resolveParsedCommentContent({
931
- elements: isRecord(rootWholeReply?.content) ? rootWholeReply.content.elements : undefined,
932
- botOpenIds: params.botOpenIds,
933
- currentDocument,
934
- client: params.client,
935
- wikiCache,
936
- logger: params.logger,
937
- accountId: params.accountId,
938
- });
939
- const commentUserId =
940
- normalizeString(rootWholeReply?.user_id) || normalizeString(comment.user_id);
941
- return {
942
- commentId: normalizeString(comment.comment_id) ?? "",
943
- userId: commentUserId,
944
- createTime:
945
- typeof comment.create_time === "number"
946
- ? comment.create_time
947
- : typeof rootWholeReply?.create_time === "number"
948
- ? rootWholeReply.create_time
949
- : undefined,
950
- isCurrentComment: normalizeString(comment.comment_id) === params.commentId,
951
- isBotAuthored:
952
- typeof commentUserId === "string" && normalizedBotOpenIds.has(commentUserId),
953
- content,
954
- };
955
- }),
956
- );
957
- wholeCommentTimeline = wholeCommentTimeline
958
- .filter((entry) => Boolean(entry.commentId))
959
- .toSorted((left, right) =>
960
- compareCommentTimelineEntries(
961
- {
962
- createTime: left.createTime,
963
- stableId: left.commentId,
964
- },
965
- {
966
- createTime: right.createTime,
967
- stableId: right.commentId,
968
- },
969
- ),
970
- );
971
- }
972
-
973
- const currentWholeCommentIndex = wholeCommentTimeline.findIndex(
974
- (entry) => entry.commentId === params.commentId,
975
- );
976
-
977
- return {
978
- documentTitle: normalizeString(meta?.title),
979
- documentUrl: normalizeString(meta?.url),
980
- isWholeComment: commentCard?.is_whole,
981
- quoteText: normalizeString(commentCard?.quote),
982
- rootCommentText: rootReplyContext?.content.semanticText ?? rootReplyContext?.content.plainText,
983
- targetReplyText:
984
- targetReplyContext?.content.semanticText ?? targetReplyContext?.content.plainText,
985
- rootCommentContent: rootReplyContext?.content,
986
- targetReplyContent: targetReplyContext?.content,
987
- currentCommentThreadReplies: resolvedReplies,
988
- wholeCommentTimeline,
989
- nearestBotWholeCommentAfter:
990
- currentWholeCommentIndex >= 0
991
- ? findNearestBotTimelineEntry({
992
- entries: wholeCommentTimeline,
993
- currentIndex: currentWholeCommentIndex,
994
- direction: "after",
995
- })
996
- : undefined,
997
- nearestBotWholeCommentBefore:
998
- currentWholeCommentIndex >= 0
999
- ? findNearestBotTimelineEntry({
1000
- entries: wholeCommentTimeline,
1001
- currentIndex: currentWholeCommentIndex,
1002
- direction: "before",
1003
- })
1004
- : undefined,
1005
- };
1006
- }
1007
-
1008
- function buildDriveCommentSurfacePrompt(params: {
1009
- noticeType: "add_comment" | "add_reply";
1010
- fileType: CommentFileType;
1011
- fileToken: string;
1012
- commentId: string;
1013
- replyId?: string;
1014
- isWholeComment?: boolean;
1015
- isMentioned?: boolean;
1016
- documentTitle?: string;
1017
- documentUrl?: string;
1018
- quoteText?: string;
1019
- rootCommentText?: string;
1020
- targetReplyText?: string;
1021
- rootCommentContent?: ParsedCommentContent;
1022
- targetReplyContent?: ParsedCommentContent;
1023
- currentCommentThreadReplies: ResolvedCommentReplyContext[];
1024
- wholeCommentTimeline: ResolvedWholeCommentTimelineEntry[];
1025
- nearestBotWholeCommentAfter?: ResolvedWholeCommentTimelineEntry;
1026
- nearestBotWholeCommentBefore?: ResolvedWholeCommentTimelineEntry;
1027
- }): string {
1028
- const documentLabel = params.documentTitle
1029
- ? `"${params.documentTitle}"`
1030
- : `${params.fileType} document ${params.fileToken}`;
1031
- const actionLabel = params.noticeType === "add_reply" ? "reply" : "comment";
1032
- const firstLine = `The user added a ${actionLabel} in ${documentLabel}.`;
1033
- const lines = [firstLine];
1034
- if (params.targetReplyText) {
1035
- lines.push(`Current user comment text: ${formatPromptTextValue(params.targetReplyText)}`);
1036
- }
1037
- if (
1038
- params.noticeType === "add_reply" &&
1039
- params.rootCommentText &&
1040
- params.rootCommentText !== params.targetReplyText
1041
- ) {
1042
- lines.push(`Original comment text: ${formatPromptTextValue(params.rootCommentText)}`);
1043
- }
1044
- if (params.quoteText) {
1045
- lines.push(`Quoted content: ${formatPromptTextValue(params.quoteText)}`);
1046
- }
1047
- if (params.isMentioned === true) {
1048
- lines.push("This comment mentioned you.");
1049
- }
1050
- if (params.documentUrl) {
1051
- lines.push(`Document link: ${params.documentUrl}`);
1052
- }
1053
- lines.push(
1054
- "Current commented document:",
1055
- `- file_type=${params.fileType}`,
1056
- `- file_token=${params.fileToken}`,
1057
- );
1058
- if (params.documentTitle) {
1059
- lines.push(`- title=${params.documentTitle}`);
1060
- }
1061
- if (params.documentUrl) {
1062
- lines.push(`- url=${params.documentUrl}`);
1063
- }
1064
- lines.push(
1065
- `Event type: ${params.noticeType}`,
1066
- `file_token: ${params.fileToken}`,
1067
- `file_type: ${params.fileType}`,
1068
- `comment_id: ${params.commentId}`,
1069
- );
1070
- if (params.isWholeComment === true) {
1071
- lines.push("This is a whole-document comment.");
1072
- }
1073
- if (params.replyId?.trim()) {
1074
- lines.push(`reply_id: ${params.replyId.trim()}`);
1075
- }
1076
- if (params.targetReplyContent?.semanticText) {
1077
- lines.push(
1078
- `Current user comment semantic text: ${formatPromptTextValue(
1079
- params.targetReplyContent.semanticText,
1080
- )}`,
1081
- );
1082
- }
1083
- if (params.targetReplyContent?.botMentioned) {
1084
- lines.push(
1085
- "Bot routing mention detected in the current user comment. Treat that mention as routing only, not task content.",
1086
- );
1087
- }
1088
- const nonBotMentions = (params.targetReplyContent?.mentions ?? [])
1089
- .filter((mention) => !mention.isBotMention)
1090
- .map((mention) => mention.displayText);
1091
- if (nonBotMentions.length > 0) {
1092
- lines.push(`Other mentioned users in current comment: ${nonBotMentions.join(", ")}`);
1093
- }
1094
- lines.push(
1095
- ...formatLinkedDocumentsPromptLines({
1096
- title: "Referenced documents from current user comment:",
1097
- linkedDocuments: params.targetReplyContent?.linkedDocuments ?? [],
1098
- }),
1099
- );
1100
- if (!params.isWholeComment && params.currentCommentThreadReplies.length > 0) {
1101
- lines.push(
1102
- "Current comment card timeline (primary context for follow-ups on this comment card):",
1103
- ...formatCommentThreadPromptLines({
1104
- replies: params.currentCommentThreadReplies,
1105
- targetReplyId: params.replyId,
1106
- }),
1107
- "For this non-whole comment, use the current comment card timeline above as the primary source for phrases like 'above', 'previous result', 'that summary', or 'insert it'.",
1108
- "Document-level session history is auxiliary background only. Do not use another comment card's recent output as the primary referent.",
1109
- );
1110
- }
1111
- if (params.isWholeComment && params.wholeCommentTimeline.length > 0) {
1112
- lines.push(
1113
- "Whole-document comment timeline (primary context for whole-comment follow-ups):",
1114
- ...formatWholeCommentTimelinePromptLines({
1115
- entries: params.wholeCommentTimeline,
1116
- currentCommentId: params.commentId,
1117
- }),
1118
- );
1119
- if (params.nearestBotWholeCommentAfter) {
1120
- lines.push(
1121
- `Nearest bot-authored whole-comment after the current comment: comment_id=${params.nearestBotWholeCommentAfter.commentId} text=${formatPromptTextValue(
1122
- params.nearestBotWholeCommentAfter.content.semanticText ??
1123
- params.nearestBotWholeCommentAfter.content.plainText,
1124
- )}`,
1125
- );
1126
- }
1127
- if (params.nearestBotWholeCommentBefore) {
1128
- lines.push(
1129
- `Nearest bot-authored whole-comment before the current comment: comment_id=${params.nearestBotWholeCommentBefore.commentId} text=${formatPromptTextValue(
1130
- params.nearestBotWholeCommentBefore.content.semanticText ??
1131
- params.nearestBotWholeCommentBefore.content.plainText,
1132
- )}`,
1133
- );
1134
- }
1135
- lines.push(
1136
- "For this whole-document comment, use the whole-comment timeline above as the primary source for phrases like 'just now', 'previous result', 'that summary', or 'write it back'.",
1137
- "Document-level session history is auxiliary background only. Do not resolve whole-comment follow-ups by blindly using the most recent document-session output.",
1138
- );
1139
- }
1140
- lines.push(
1141
- "This is a Feishu document comment thread.",
1142
- "It is not a Feishu IM chat.",
1143
- "Your final text reply will be posted to the current comment thread automatically.",
1144
- "Use the thread timeline above as the main context for follow-up requests.",
1145
- "Do not use another comment card or document-session output as the main reference.",
1146
- "If you need comment thread context, use feishu_drive.list_comments or feishu_drive.list_comment_replies.",
1147
- "If you modify the document, post a user-visible follow-up in the comment thread.",
1148
- "Use feishu_drive.reply_comment or feishu_drive.add_comment for that follow-up.",
1149
- "Whole-document comments do not support direct replies.",
1150
- "For whole-document comments, use feishu_drive.add_comment.",
1151
- 'Only treat URLs listed under "Referenced documents from current user comment" as structured Feishu document references.',
1152
- "URLs that appear only in comment text are plain links unless you verify them.",
1153
- "If the user asks about a linked Feishu document or wiki page, treat that linked document as the read target.",
1154
- "If the user asks you to use a linked document as guidance, treat the linked document as the reference source and the current commented document as the edit target.",
1155
- "If a referenced document resolves to the same file_token and file_type as the current commented document, treat it as the current document.",
1156
- "If the user asks you to modify document content, you must use feishu_doc to make the change.",
1157
- 'Do not reply with only "done", "I\'ll handle it", or a restated plan without calling tools.',
1158
- "If the comment quotes document content, treat the quoted content as the main anchor.",
1159
- 'For requests like "insert xxx below this content", locate the quoted content first, then edit the document.',
1160
- 'For requests like "summarize the content below", "explain this section", or "continue writing from here", use the quoted content as the main target.',
1161
- "If the quote is not enough, use feishu_doc.read or feishu_doc.list_blocks to read nearby context.",
1162
- "Do not guess document content from the comment alone.",
1163
- "Do not give a vague answer before reading enough context.",
1164
- "Unless the user asks for the whole document, handle only the local content around the quoted anchor.",
1165
- "If document edits are involved, read the anchor first, then edit.",
1166
- "If the edit fails or the anchor cannot be found, say so clearly.",
1167
- "If this is a reading task, such as summarization, explanation, or extraction, you may output the final answer directly after confirming the context.",
1168
- "Use the same language as the user's comment or reply, unless the user asks for another language.",
1169
- "Use plain text only.",
1170
- "Do not use Markdown.",
1171
- "Do not use headings.",
1172
- "Do not use bullet lists.",
1173
- "Do not use numbered lists.",
1174
- "Do not use tables.",
1175
- "Do not use blockquotes.",
1176
- "Do not use code blocks.",
1177
- "Do not show reasoning.",
1178
- "Do not show analysis.",
1179
- "Do not show chain-of-thought.",
1180
- "Do not show scratch work.",
1181
- "Do not describe your plan.",
1182
- "Do not describe your steps.",
1183
- "Do not describe tool use.",
1184
- 'Do not start with phrases like "I will", "I’ll first", "I need to", "The user wants", or "I have updated".',
1185
- "Output only the final user-facing reply.",
1186
- "If you already sent the user-visible reply with feishu_drive.reply_comment or feishu_drive.add_comment, output exactly NO_REPLY.",
1187
- "If no user-visible reply is needed, output exactly NO_REPLY.",
1188
- "Be concise.",
1189
- "Do not omit requested content.",
1190
- );
1191
- lines.push(
1192
- "Choose one outcome: output the final plain-text reply, edit the document and then post a user-visible follow-up in the comment thread, or output exactly NO_REPLY.",
1193
- );
1194
- return lines.join("\n");
1195
- }
1196
-
1197
- async function resolveDriveCommentEventCore(params: ResolveDriveCommentEventParams): Promise<{
1198
- eventId: string;
1199
- commentId: string;
1200
- replyId?: string;
1201
- noticeType: "add_comment" | "add_reply";
1202
- fileToken: string;
1203
- fileType: CommentFileType;
1204
- isWholeComment?: boolean;
1205
- senderId: string;
1206
- senderUserId?: string;
1207
- timestamp?: string;
1208
- isMentioned?: boolean;
1209
- context: {
1210
- documentTitle?: string;
1211
- documentUrl?: string;
1212
- quoteText?: string;
1213
- rootCommentText?: string;
1214
- targetReplyText?: string;
1215
- rootCommentContent?: ParsedCommentContent;
1216
- targetReplyContent?: ParsedCommentContent;
1217
- currentCommentThreadReplies: ResolvedCommentReplyContext[];
1218
- wholeCommentTimeline: ResolvedWholeCommentTimelineEntry[];
1219
- nearestBotWholeCommentAfter?: ResolvedWholeCommentTimelineEntry;
1220
- nearestBotWholeCommentBefore?: ResolvedWholeCommentTimelineEntry;
1221
- };
1222
- } | null> {
1223
- const {
1224
- cfg,
1225
- accountId,
1226
- event,
1227
- account,
1228
- botOpenId,
1229
- createClient,
1230
- verificationTimeoutMs = FEISHU_COMMENT_VERIFY_TIMEOUT_MS,
1231
- logger,
1232
- waitMs = delayMs,
1233
- } = params;
1234
- const eventId = event.event_id?.trim();
1235
- const commentId = event.comment_id?.trim();
1236
- const replyId = event.reply_id?.trim();
1237
- const noticeType = event.notice_meta?.notice_type?.trim();
1238
- const fileToken = event.notice_meta?.file_token?.trim();
1239
- const fileType = normalizeCommentFileType(event.notice_meta?.file_type);
1240
- const senderId = event.notice_meta?.from_user_id?.open_id?.trim();
1241
- const senderUserId = normalizeString(event.notice_meta?.from_user_id?.user_id);
1242
- if (!eventId || !commentId || !noticeType || !fileToken || !fileType || !senderId) {
1243
- logger?.(
1244
- `feishu[${accountId}]: drive comment notice missing required fields event=${eventId ?? "unknown"} comment=${commentId ?? "unknown"}`,
1245
- );
1246
- return null;
1247
- }
1248
- if (noticeType !== "add_comment" && noticeType !== "add_reply") {
1249
- logger?.(`feishu[${accountId}]: unsupported drive comment notice type ${noticeType}`);
1250
- return null;
1251
- }
1252
- if (!botOpenId) {
1253
- logger?.(
1254
- `feishu[${accountId}]: skipping drive comment notice because bot open_id is unavailable ` +
1255
- `event=${eventId}`,
1256
- );
1257
- return null;
1258
- }
1259
- if (senderId === botOpenId) {
1260
- logger?.(
1261
- `feishu[${accountId}]: ignoring self-authored drive comment notice event=${eventId} sender=${senderId}`,
1262
- );
1263
- return null;
1264
- }
1265
-
1266
- const client = createClient
1267
- ? createClient(account ?? ({ accountId } as ResolvedFeishuAccount))
1268
- : (createFeishuClient(
1269
- (await import("./accounts.js")).resolveFeishuAccount({ cfg, accountId }),
1270
- ) as FeishuRequestClient);
1271
- const context = await fetchDriveCommentContext({
1272
- client,
1273
- fileToken,
1274
- fileType,
1275
- commentId,
1276
- replyId,
1277
- botOpenIds: [botOpenId, event.notice_meta?.to_user_id?.open_id],
1278
- timeoutMs: verificationTimeoutMs,
1279
- logger,
1280
- accountId,
1281
- waitMs,
1282
- });
1283
- return {
1284
- eventId,
1285
- commentId,
1286
- replyId,
1287
- noticeType,
1288
- fileToken,
1289
- fileType,
1290
- isWholeComment: context.isWholeComment,
1291
- senderId,
1292
- senderUserId,
1293
- timestamp: event.timestamp,
1294
- isMentioned: event.is_mentioned,
1295
- context,
1296
- };
1297
- }
1298
-
1299
- export function parseFeishuDriveCommentNoticeEventPayload(
1300
- value: unknown,
1301
- ): FeishuDriveCommentNoticeEvent | null {
1302
- if (!isRecord(value) || !isRecord(value.notice_meta)) {
1303
- return null;
1304
- }
1305
- const noticeMeta = value.notice_meta;
1306
- const fromUserId = isRecord(noticeMeta.from_user_id) ? noticeMeta.from_user_id : undefined;
1307
- const toUserId = isRecord(noticeMeta.to_user_id) ? noticeMeta.to_user_id : undefined;
1308
- return {
1309
- comment_id: readString(value.comment_id),
1310
- event_id: readString(value.event_id),
1311
- is_mentioned: readBoolean(value.is_mentioned),
1312
- notice_meta: {
1313
- file_token: readString(noticeMeta.file_token),
1314
- file_type: readString(noticeMeta.file_type),
1315
- from_user_id: fromUserId
1316
- ? {
1317
- open_id: readString(fromUserId.open_id),
1318
- user_id: readString(fromUserId.user_id),
1319
- union_id: readString(fromUserId.union_id),
1320
- }
1321
- : undefined,
1322
- notice_type: readString(noticeMeta.notice_type),
1323
- to_user_id: toUserId
1324
- ? {
1325
- open_id: readString(toUserId.open_id),
1326
- user_id: readString(toUserId.user_id),
1327
- union_id: readString(toUserId.union_id),
1328
- }
1329
- : undefined,
1330
- },
1331
- reply_id: readString(value.reply_id),
1332
- timestamp: readString(value.timestamp),
1333
- type: readString(value.type),
1334
- };
1335
- }
1336
-
1337
- export async function resolveDriveCommentEventTurn(
1338
- params: ResolveDriveCommentEventParams,
1339
- ): Promise<ResolvedDriveCommentEventTurn | null> {
1340
- const resolved = await resolveDriveCommentEventCore(params);
1341
- if (!resolved) {
1342
- return null;
1343
- }
1344
- const prompt = buildDriveCommentSurfacePrompt({
1345
- noticeType: resolved.noticeType,
1346
- fileType: resolved.fileType,
1347
- fileToken: resolved.fileToken,
1348
- commentId: resolved.commentId,
1349
- replyId: resolved.replyId,
1350
- isWholeComment: resolved.isWholeComment,
1351
- isMentioned: resolved.isMentioned,
1352
- documentTitle: resolved.context.documentTitle,
1353
- documentUrl: resolved.context.documentUrl,
1354
- quoteText: resolved.context.quoteText,
1355
- rootCommentText: resolved.context.rootCommentText,
1356
- targetReplyText: resolved.context.targetReplyText,
1357
- rootCommentContent: resolved.context.rootCommentContent,
1358
- targetReplyContent: resolved.context.targetReplyContent,
1359
- currentCommentThreadReplies: resolved.context.currentCommentThreadReplies,
1360
- wholeCommentTimeline: resolved.context.wholeCommentTimeline,
1361
- nearestBotWholeCommentAfter: resolved.context.nearestBotWholeCommentAfter,
1362
- nearestBotWholeCommentBefore: resolved.context.nearestBotWholeCommentBefore,
1363
- });
1364
- const preview = prompt.replace(/\s+/g, " ").slice(0, 160);
1365
- return {
1366
- eventId: resolved.eventId,
1367
- messageId: `drive-comment:${resolved.eventId}`,
1368
- commentId: resolved.commentId,
1369
- replyId: resolved.replyId,
1370
- noticeType: resolved.noticeType,
1371
- fileToken: resolved.fileToken,
1372
- fileType: resolved.fileType,
1373
- isWholeComment: resolved.isWholeComment,
1374
- senderId: resolved.senderId,
1375
- senderUserId: resolved.senderUserId,
1376
- timestamp: resolved.timestamp,
1377
- isMentioned: resolved.isMentioned,
1378
- documentTitle: resolved.context.documentTitle,
1379
- documentUrl: resolved.context.documentUrl,
1380
- quoteText: resolved.context.quoteText,
1381
- rootCommentText: resolved.context.rootCommentText,
1382
- targetReplyText: resolved.context.targetReplyText,
1383
- prompt,
1384
- preview,
1385
- };
1386
- }