@openclaw/feishu 2026.5.2 → 2026.5.3-beta.2

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 (224) hide show
  1. package/dist/accounts-Ba3-WP1z.js +423 -0
  2. package/dist/api.js +2280 -0
  3. package/dist/app-registration-B8qc1MCM.js +184 -0
  4. package/dist/audio-preflight.runtime-BPlzkO3l.js +7 -0
  5. package/dist/card-interaction-BfRLgvw_.js +96 -0
  6. package/dist/channel-CSD_Jt8I.js +1668 -0
  7. package/dist/channel-entry.js +22 -0
  8. package/dist/channel-plugin-api.js +2 -0
  9. package/dist/channel.runtime-DYsXcD36.js +700 -0
  10. package/dist/client-DBVoQL5w.js +157 -0
  11. package/dist/contract-api.js +9 -0
  12. package/dist/conversation-id-DWS3Ep2A.js +139 -0
  13. package/dist/directory.static-f3EeoRJd.js +44 -0
  14. package/dist/drive-C5eJLJr7.js +883 -0
  15. package/dist/index.js +68 -0
  16. package/dist/monitor-CT189QfR.js +60 -0
  17. package/dist/monitor.account-dJV2jO8C.js +4990 -0
  18. package/dist/monitor.state-DYM02ipp.js +100 -0
  19. package/dist/policy-D6c-wMPl.js +118 -0
  20. package/dist/probe-BNzzU_uR.js +149 -0
  21. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  22. package/dist/runtime-CG0DuRCy.js +8 -0
  23. package/dist/runtime-api.js +14 -0
  24. package/dist/secret-contract-Dm4Z_zQN.js +119 -0
  25. package/dist/secret-contract-api.js +2 -0
  26. package/dist/security-audit-DqJdocrN.js +11 -0
  27. package/dist/security-audit-shared-ByuMx9cJ.js +38 -0
  28. package/dist/security-contract-api.js +2 -0
  29. package/dist/send-DowxxbpH.js +1218 -0
  30. package/dist/session-conversation-B4nrW-vo.js +27 -0
  31. package/dist/session-key-api.js +2 -0
  32. package/dist/setup-api.js +2 -0
  33. package/dist/setup-entry.js +15 -0
  34. package/dist/subagent-hooks-C3UhPVLV.js +227 -0
  35. package/dist/subagent-hooks-api.js +23 -0
  36. package/dist/targets-JMFJRKSe.js +48 -0
  37. package/dist/thread-bindings-BmS6TLes.js +222 -0
  38. package/package.json +15 -6
  39. package/api.ts +0 -31
  40. package/channel-entry.ts +0 -20
  41. package/channel-plugin-api.ts +0 -1
  42. package/contract-api.ts +0 -16
  43. package/index.ts +0 -82
  44. package/runtime-api.ts +0 -55
  45. package/secret-contract-api.ts +0 -5
  46. package/security-contract-api.ts +0 -1
  47. package/session-key-api.ts +0 -1
  48. package/setup-api.ts +0 -3
  49. package/setup-entry.test.ts +0 -14
  50. package/setup-entry.ts +0 -13
  51. package/src/accounts.test.ts +0 -459
  52. package/src/accounts.ts +0 -326
  53. package/src/app-registration.ts +0 -331
  54. package/src/approval-auth.test.ts +0 -24
  55. package/src/approval-auth.ts +0 -25
  56. package/src/async.test.ts +0 -35
  57. package/src/async.ts +0 -104
  58. package/src/audio-preflight.runtime.ts +0 -9
  59. package/src/bitable.test.ts +0 -131
  60. package/src/bitable.ts +0 -762
  61. package/src/bot-content.ts +0 -474
  62. package/src/bot-group-name.test.ts +0 -108
  63. package/src/bot-runtime-api.ts +0 -12
  64. package/src/bot-sender-name.ts +0 -125
  65. package/src/bot.broadcast.test.ts +0 -463
  66. package/src/bot.card-action.test.ts +0 -577
  67. package/src/bot.checkBotMentioned.test.ts +0 -265
  68. package/src/bot.helpers.test.ts +0 -118
  69. package/src/bot.stripBotMention.test.ts +0 -126
  70. package/src/bot.test.ts +0 -3040
  71. package/src/bot.ts +0 -1559
  72. package/src/card-action.ts +0 -447
  73. package/src/card-interaction.test.ts +0 -129
  74. package/src/card-interaction.ts +0 -159
  75. package/src/card-test-helpers.ts +0 -47
  76. package/src/card-ux-approval.ts +0 -65
  77. package/src/card-ux-launcher.test.ts +0 -99
  78. package/src/card-ux-launcher.ts +0 -121
  79. package/src/card-ux-shared.ts +0 -33
  80. package/src/channel-runtime-api.ts +0 -16
  81. package/src/channel.runtime.ts +0 -47
  82. package/src/channel.test.ts +0 -959
  83. package/src/channel.ts +0 -1313
  84. package/src/chat-schema.ts +0 -25
  85. package/src/chat.test.ts +0 -196
  86. package/src/chat.ts +0 -188
  87. package/src/client.test.ts +0 -433
  88. package/src/client.ts +0 -290
  89. package/src/comment-dispatcher-runtime-api.ts +0 -6
  90. package/src/comment-dispatcher.test.ts +0 -169
  91. package/src/comment-dispatcher.ts +0 -107
  92. package/src/comment-handler-runtime-api.ts +0 -3
  93. package/src/comment-handler.test.ts +0 -486
  94. package/src/comment-handler.ts +0 -309
  95. package/src/comment-reaction.test.ts +0 -166
  96. package/src/comment-reaction.ts +0 -259
  97. package/src/comment-shared.test.ts +0 -182
  98. package/src/comment-shared.ts +0 -406
  99. package/src/comment-target.ts +0 -44
  100. package/src/config-schema.test.ts +0 -309
  101. package/src/config-schema.ts +0 -333
  102. package/src/conversation-id.test.ts +0 -18
  103. package/src/conversation-id.ts +0 -199
  104. package/src/dedup-runtime-api.ts +0 -1
  105. package/src/dedup.ts +0 -141
  106. package/src/directory.static.ts +0 -61
  107. package/src/directory.test.ts +0 -136
  108. package/src/directory.ts +0 -124
  109. package/src/doc-schema.ts +0 -182
  110. package/src/docx-batch-insert.test.ts +0 -91
  111. package/src/docx-batch-insert.ts +0 -223
  112. package/src/docx-color-text.ts +0 -154
  113. package/src/docx-table-ops.test.ts +0 -53
  114. package/src/docx-table-ops.ts +0 -316
  115. package/src/docx-types.ts +0 -38
  116. package/src/docx.account-selection.test.ts +0 -79
  117. package/src/docx.test.ts +0 -685
  118. package/src/docx.ts +0 -1616
  119. package/src/drive-schema.ts +0 -92
  120. package/src/drive.test.ts +0 -1219
  121. package/src/drive.ts +0 -829
  122. package/src/dynamic-agent.ts +0 -137
  123. package/src/event-types.ts +0 -45
  124. package/src/external-keys.test.ts +0 -20
  125. package/src/external-keys.ts +0 -19
  126. package/src/lifecycle.test-support.ts +0 -220
  127. package/src/media.test.ts +0 -900
  128. package/src/media.ts +0 -861
  129. package/src/mention-target.types.ts +0 -5
  130. package/src/mention.ts +0 -114
  131. package/src/message-action-contract.ts +0 -13
  132. package/src/monitor-state-runtime-api.ts +0 -7
  133. package/src/monitor-transport-runtime-api.ts +0 -7
  134. package/src/monitor.account.ts +0 -468
  135. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
  136. package/src/monitor.bot-identity.ts +0 -86
  137. package/src/monitor.bot-menu-handler.ts +0 -165
  138. package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
  139. package/src/monitor.bot-menu.test.ts +0 -178
  140. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
  141. package/src/monitor.card-action.lifecycle.test-support.ts +0 -373
  142. package/src/monitor.cleanup.test.ts +0 -376
  143. package/src/monitor.comment-notice-handler.ts +0 -105
  144. package/src/monitor.comment.test.ts +0 -937
  145. package/src/monitor.comment.ts +0 -1386
  146. package/src/monitor.lifecycle.test.ts +0 -4
  147. package/src/monitor.message-handler.ts +0 -339
  148. package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
  149. package/src/monitor.reaction.test.ts +0 -713
  150. package/src/monitor.startup.test.ts +0 -192
  151. package/src/monitor.startup.ts +0 -74
  152. package/src/monitor.state.defaults.test.ts +0 -46
  153. package/src/monitor.state.ts +0 -170
  154. package/src/monitor.synthetic-error.ts +0 -18
  155. package/src/monitor.test-mocks.ts +0 -45
  156. package/src/monitor.transport.ts +0 -424
  157. package/src/monitor.ts +0 -100
  158. package/src/monitor.webhook-e2e.test.ts +0 -272
  159. package/src/monitor.webhook-security.test.ts +0 -264
  160. package/src/monitor.webhook.test-helpers.ts +0 -116
  161. package/src/outbound-runtime-api.ts +0 -1
  162. package/src/outbound.test.ts +0 -935
  163. package/src/outbound.ts +0 -718
  164. package/src/perm-schema.ts +0 -52
  165. package/src/perm.ts +0 -170
  166. package/src/pins.ts +0 -108
  167. package/src/policy.test.ts +0 -334
  168. package/src/policy.ts +0 -236
  169. package/src/post.test.ts +0 -105
  170. package/src/post.ts +0 -275
  171. package/src/probe.test.ts +0 -275
  172. package/src/probe.ts +0 -166
  173. package/src/processing-claims.ts +0 -59
  174. package/src/qr-terminal.ts +0 -1
  175. package/src/reactions.ts +0 -123
  176. package/src/reasoning-preview.test.ts +0 -59
  177. package/src/reasoning-preview.ts +0 -20
  178. package/src/reply-dispatcher-runtime-api.ts +0 -7
  179. package/src/reply-dispatcher.test.ts +0 -1144
  180. package/src/reply-dispatcher.ts +0 -650
  181. package/src/runtime.ts +0 -9
  182. package/src/secret-contract.ts +0 -145
  183. package/src/secret-input.ts +0 -1
  184. package/src/security-audit-shared.ts +0 -69
  185. package/src/security-audit.test.ts +0 -61
  186. package/src/security-audit.ts +0 -1
  187. package/src/send-result.ts +0 -29
  188. package/src/send-target.test.ts +0 -80
  189. package/src/send-target.ts +0 -35
  190. package/src/send.reply-fallback.test.ts +0 -292
  191. package/src/send.test.ts +0 -550
  192. package/src/send.ts +0 -800
  193. package/src/sequential-key.test.ts +0 -72
  194. package/src/sequential-key.ts +0 -28
  195. package/src/sequential-queue.test.ts +0 -92
  196. package/src/sequential-queue.ts +0 -16
  197. package/src/session-conversation.ts +0 -42
  198. package/src/session-route.ts +0 -48
  199. package/src/setup-core.ts +0 -51
  200. package/src/setup-surface.test.ts +0 -174
  201. package/src/setup-surface.ts +0 -581
  202. package/src/streaming-card.test.ts +0 -190
  203. package/src/streaming-card.ts +0 -490
  204. package/src/subagent-hooks.test.ts +0 -603
  205. package/src/subagent-hooks.ts +0 -397
  206. package/src/targets.ts +0 -97
  207. package/src/test-support/lifecycle-test-support.ts +0 -453
  208. package/src/thread-bindings.test.ts +0 -143
  209. package/src/thread-bindings.ts +0 -330
  210. package/src/tool-account-routing.test.ts +0 -187
  211. package/src/tool-account.test.ts +0 -44
  212. package/src/tool-account.ts +0 -93
  213. package/src/tool-factory-test-harness.ts +0 -79
  214. package/src/tool-result.test.ts +0 -32
  215. package/src/tool-result.ts +0 -16
  216. package/src/tools-config.test.ts +0 -21
  217. package/src/tools-config.ts +0 -22
  218. package/src/types.ts +0 -104
  219. package/src/typing.test.ts +0 -144
  220. package/src/typing.ts +0 -214
  221. package/src/wiki-schema.ts +0 -55
  222. package/src/wiki.ts +0 -227
  223. package/subagent-hooks-api.ts +0 -31
  224. package/tsconfig.json +0 -16
@@ -1,1386 +0,0 @@
1
- import { formatErrorMessage } from "openclaw/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
- }