@openclaw/feishu 2026.3.13 → 2026.5.1-beta.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 (188) hide show
  1. package/api.ts +31 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/index.ts +70 -53
  6. package/openclaw.plugin.json +1653 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1252 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +84 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +365 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +32 -94
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +119 -20
  72. package/src/directory.ts +61 -91
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +375 -26
  91. package/src/media.ts +434 -88
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
  114. package/src/monitor.startup.test.ts +11 -9
  115. package/src/monitor.startup.ts +26 -16
  116. package/src/monitor.state.ts +20 -5
  117. package/src/monitor.synthetic-error.ts +18 -0
  118. package/src/monitor.test-mocks.ts +2 -2
  119. package/src/monitor.transport.ts +220 -60
  120. package/src/monitor.ts +15 -10
  121. package/src/monitor.webhook-e2e.test.ts +65 -7
  122. package/src/monitor.webhook-security.test.ts +122 -0
  123. package/src/monitor.webhook.test-helpers.ts +44 -26
  124. package/src/outbound-runtime-api.ts +1 -0
  125. package/src/outbound.test.ts +616 -37
  126. package/src/outbound.ts +623 -81
  127. package/src/perm-schema.ts +1 -1
  128. package/src/perm.ts +1 -7
  129. package/src/pins.ts +108 -0
  130. package/src/policy.test.ts +297 -117
  131. package/src/policy.ts +142 -29
  132. package/src/post.ts +7 -6
  133. package/src/probe.test.ts +14 -9
  134. package/src/probe.ts +26 -16
  135. package/src/processing-claims.ts +59 -0
  136. package/src/qr-terminal.ts +1 -0
  137. package/src/reactions.ts +4 -34
  138. package/src/reasoning-preview.test.ts +59 -0
  139. package/src/reasoning-preview.ts +20 -0
  140. package/src/reply-dispatcher-runtime-api.ts +7 -0
  141. package/src/reply-dispatcher.test.ts +660 -29
  142. package/src/reply-dispatcher.ts +407 -154
  143. package/src/runtime.ts +6 -3
  144. package/src/secret-contract.ts +145 -0
  145. package/src/secret-input.ts +1 -13
  146. package/src/security-audit-shared.ts +69 -0
  147. package/src/security-audit.test.ts +61 -0
  148. package/src/security-audit.ts +1 -0
  149. package/src/send-result.ts +1 -1
  150. package/src/send-target.test.ts +9 -3
  151. package/src/send-target.ts +10 -4
  152. package/src/send.reply-fallback.test.ts +77 -2
  153. package/src/send.test.ts +386 -4
  154. package/src/send.ts +399 -86
  155. package/src/sequential-key.test.ts +72 -0
  156. package/src/sequential-key.ts +28 -0
  157. package/src/sequential-queue.test.ts +92 -0
  158. package/src/sequential-queue.ts +16 -0
  159. package/src/session-conversation.ts +42 -0
  160. package/src/session-route.ts +48 -0
  161. package/src/setup-core.ts +51 -0
  162. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  163. package/src/setup-surface.ts +581 -0
  164. package/src/streaming-card.test.ts +138 -2
  165. package/src/streaming-card.ts +134 -18
  166. package/src/subagent-hooks.test.ts +603 -0
  167. package/src/subagent-hooks.ts +397 -0
  168. package/src/targets.ts +3 -13
  169. package/src/test-support/lifecycle-test-support.ts +479 -0
  170. package/src/thread-bindings.test.ts +143 -0
  171. package/src/thread-bindings.ts +330 -0
  172. package/src/tool-account-routing.test.ts +66 -8
  173. package/src/tool-account.test.ts +44 -0
  174. package/src/tool-account.ts +40 -17
  175. package/src/tool-factory-test-harness.ts +11 -8
  176. package/src/tool-result.ts +3 -1
  177. package/src/tools-config.ts +1 -1
  178. package/src/types.ts +16 -15
  179. package/src/typing.ts +10 -6
  180. package/src/wiki-schema.ts +1 -1
  181. package/src/wiki.ts +1 -7
  182. package/subagent-hooks-api.ts +31 -0
  183. package/tsconfig.json +16 -0
  184. package/src/feishu-command-handler.ts +0 -59
  185. package/src/onboarding.status.test.ts +0 -25
  186. package/src/onboarding.ts +0 -489
  187. package/src/send-message.ts +0 -71
  188. package/src/targets.test.ts +0 -70
@@ -0,0 +1,259 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
2
+ import { resolveFeishuRuntimeAccount } from "./accounts.js";
3
+ import { createFeishuClient } from "./client.js";
4
+ import { encodeQuery, formatFeishuApiError } from "./comment-shared.js";
5
+ import { parseFeishuCommentTarget, type CommentFileType } from "./comment-target.js";
6
+
7
+ const COMMENT_TYPING_REACTION_TYPE = "Typing";
8
+ const COMMENT_REACTION_TIMEOUT_MS = 30_000;
9
+ const commentTypingReactionState = new Map<
10
+ string,
11
+ {
12
+ active: boolean;
13
+ cleaned: boolean;
14
+ cleanupPromise?: Promise<boolean>;
15
+ }
16
+ >();
17
+
18
+ type FeishuCommentReactionClient = ReturnType<typeof createFeishuClient> & {
19
+ request(params: {
20
+ method: "POST";
21
+ url: string;
22
+ data: unknown;
23
+ timeout: number;
24
+ }): Promise<unknown>;
25
+ };
26
+
27
+ function buildCommentTypingReactionKey(params: {
28
+ fileToken: string;
29
+ fileType: CommentFileType;
30
+ replyId: string;
31
+ }): string {
32
+ return `${params.fileType}:${params.fileToken}:${params.replyId}`;
33
+ }
34
+
35
+ function ensureCommentTypingReactionState(key: string) {
36
+ const existing = commentTypingReactionState.get(key);
37
+ if (existing) {
38
+ return existing;
39
+ }
40
+ const created = {
41
+ active: false,
42
+ cleaned: false,
43
+ cleanupPromise: undefined,
44
+ };
45
+ commentTypingReactionState.set(key, created);
46
+ return created;
47
+ }
48
+
49
+ async function requestCommentTypingReactionWithClient(params: {
50
+ client: FeishuCommentReactionClient;
51
+ fileToken: string;
52
+ fileType: CommentFileType;
53
+ replyId: string;
54
+ action: "add" | "delete";
55
+ runtime?: RuntimeEnv;
56
+ logPrefix?: string;
57
+ }): Promise<boolean> {
58
+ try {
59
+ const response = (await params.client.request({
60
+ method: "POST",
61
+ url:
62
+ `/open-apis/drive/v2/files/${encodeURIComponent(params.fileToken)}/comments/reaction` +
63
+ encodeQuery({
64
+ file_type: params.fileType,
65
+ }),
66
+ data: {
67
+ action: params.action,
68
+ reply_id: params.replyId,
69
+ reaction_type: COMMENT_TYPING_REACTION_TYPE,
70
+ },
71
+ timeout: COMMENT_REACTION_TIMEOUT_MS,
72
+ })) as {
73
+ code?: number;
74
+ msg?: string;
75
+ log_id?: string;
76
+ error?: { log_id?: string };
77
+ };
78
+ if (response.code === 0) {
79
+ return true;
80
+ }
81
+ params.runtime?.log?.(
82
+ `${params.logPrefix ?? "[feishu]"}: comment typing reaction ${params.action} failed ` +
83
+ `reply=${params.replyId} file=${params.fileType}:${params.fileToken} ` +
84
+ `code=${response.code ?? "unknown"} msg=${response.msg ?? "unknown"} ` +
85
+ `log_id=${response.log_id ?? response.error?.log_id ?? "unknown"}`,
86
+ );
87
+ } catch (error) {
88
+ params.runtime?.log?.(
89
+ `${params.logPrefix ?? "[feishu]"}: comment typing reaction ${params.action} threw ` +
90
+ `reply=${params.replyId} file=${params.fileType}:${params.fileToken} ` +
91
+ `error=${formatCommentReactionFailure(error)}`,
92
+ );
93
+ }
94
+ return false;
95
+ }
96
+
97
+ function formatCommentReactionFailure(error: unknown): string {
98
+ return formatFeishuApiError(error, { includeNestedErrorLogId: true });
99
+ }
100
+
101
+ async function requestCommentTypingReaction(params: {
102
+ cfg: ClawdbotConfig;
103
+ fileToken: string;
104
+ fileType: CommentFileType;
105
+ replyId: string;
106
+ action: "add" | "delete";
107
+ accountId?: string;
108
+ runtime?: RuntimeEnv;
109
+ }): Promise<boolean> {
110
+ const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
111
+ if (!account.configured || !(account.config.typingIndicator ?? true)) {
112
+ return false;
113
+ }
114
+ const client = createFeishuClient(account) as FeishuCommentReactionClient;
115
+ return requestCommentTypingReactionWithClient({
116
+ client,
117
+ fileToken: params.fileToken,
118
+ fileType: params.fileType,
119
+ replyId: params.replyId,
120
+ action: params.action,
121
+ runtime: params.runtime,
122
+ logPrefix: `feishu[${account.accountId}]`,
123
+ });
124
+ }
125
+
126
+ async function cleanupCommentTypingReactionByKey(params: {
127
+ key: string;
128
+ performDelete: () => Promise<boolean>;
129
+ }): Promise<boolean> {
130
+ const state = ensureCommentTypingReactionState(params.key);
131
+ if (state.cleaned) {
132
+ return false;
133
+ }
134
+ if (state.cleanupPromise) {
135
+ return await state.cleanupPromise;
136
+ }
137
+ const cleanupPromise = (async (): Promise<boolean> => {
138
+ if (!state.active) {
139
+ state.cleaned = true;
140
+ return false;
141
+ }
142
+ const deleted = await params.performDelete();
143
+ if (deleted) {
144
+ state.cleaned = true;
145
+ state.active = false;
146
+ }
147
+ return deleted;
148
+ })();
149
+ state.cleanupPromise = cleanupPromise;
150
+ try {
151
+ return await cleanupPromise;
152
+ } finally {
153
+ state.cleanupPromise = undefined;
154
+ if (state.cleaned) {
155
+ state.active = false;
156
+ commentTypingReactionState.delete(params.key);
157
+ }
158
+ }
159
+ }
160
+
161
+ export async function cleanupAmbientCommentTypingReaction(params: {
162
+ client: FeishuCommentReactionClient;
163
+ deliveryContext?: {
164
+ channel?: string;
165
+ to?: string;
166
+ threadId?: string | number;
167
+ };
168
+ runtime?: RuntimeEnv;
169
+ }): Promise<boolean> {
170
+ const deliveryContext = params.deliveryContext;
171
+ if (
172
+ deliveryContext?.channel &&
173
+ deliveryContext.channel !== "feishu" &&
174
+ deliveryContext.channel !== "feishu-comment"
175
+ ) {
176
+ return false;
177
+ }
178
+ const target = parseFeishuCommentTarget(deliveryContext?.to);
179
+ const replyId =
180
+ typeof deliveryContext?.threadId === "string" || typeof deliveryContext?.threadId === "number"
181
+ ? String(deliveryContext.threadId).trim()
182
+ : "";
183
+ if (!target || !replyId) {
184
+ return false;
185
+ }
186
+ const key = buildCommentTypingReactionKey({
187
+ fileToken: target.fileToken,
188
+ fileType: target.fileType,
189
+ replyId,
190
+ });
191
+ return cleanupCommentTypingReactionByKey({
192
+ key,
193
+ performDelete: () =>
194
+ requestCommentTypingReactionWithClient({
195
+ client: params.client,
196
+ fileToken: target.fileToken,
197
+ fileType: target.fileType,
198
+ replyId,
199
+ action: "delete",
200
+ runtime: params.runtime,
201
+ logPrefix: "[feishu]",
202
+ }),
203
+ });
204
+ }
205
+
206
+ export function createCommentTypingReactionLifecycle(params: {
207
+ cfg: ClawdbotConfig;
208
+ fileToken: string;
209
+ fileType: CommentFileType;
210
+ replyId?: string;
211
+ accountId?: string;
212
+ runtime?: RuntimeEnv;
213
+ }) {
214
+ const key = params.replyId?.trim()
215
+ ? buildCommentTypingReactionKey({
216
+ fileToken: params.fileToken,
217
+ fileType: params.fileType,
218
+ replyId: params.replyId.trim(),
219
+ })
220
+ : undefined;
221
+ const state = key ? ensureCommentTypingReactionState(key) : undefined;
222
+
223
+ return {
224
+ start: async (): Promise<void> => {
225
+ const replyId = params.replyId?.trim();
226
+ if (!state || state.cleaned || state.active || !replyId) {
227
+ return;
228
+ }
229
+ state.active = await requestCommentTypingReaction({
230
+ cfg: params.cfg,
231
+ fileToken: params.fileToken,
232
+ fileType: params.fileType,
233
+ replyId,
234
+ action: "add",
235
+ accountId: params.accountId,
236
+ runtime: params.runtime,
237
+ });
238
+ },
239
+ cleanup: async (): Promise<void> => {
240
+ const replyId = params.replyId?.trim();
241
+ if (!key || !replyId) {
242
+ return;
243
+ }
244
+ await cleanupCommentTypingReactionByKey({
245
+ key,
246
+ performDelete: () =>
247
+ requestCommentTypingReaction({
248
+ cfg: params.cfg,
249
+ fileToken: params.fileToken,
250
+ fileType: params.fileType,
251
+ replyId,
252
+ action: "delete",
253
+ accountId: params.accountId,
254
+ runtime: params.runtime,
255
+ }),
256
+ });
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,182 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ parseCommentContentElements,
4
+ resolveCommentLinkedDocumentFromUrl,
5
+ } from "./comment-shared.js";
6
+
7
+ const VALID_TOKEN_22 = "ABCDEFGHIJKLMNOPQRSTUV";
8
+ const VALID_TOKEN_27 = "ZsJfdxrBFo0RwuxteOLc1Ekvneb";
9
+
10
+ describe("resolveCommentLinkedDocumentFromUrl", () => {
11
+ it.each([
12
+ {
13
+ label: "doc",
14
+ url: `https://example.test/doc/${VALID_TOKEN_22}`,
15
+ expectedKind: "doc",
16
+ expectedResolvedType: "doc",
17
+ expectedToken: VALID_TOKEN_22,
18
+ },
19
+ {
20
+ label: "docs",
21
+ url: `https://example.test/docs/${VALID_TOKEN_22}`,
22
+ expectedKind: "doc",
23
+ expectedResolvedType: "doc",
24
+ expectedToken: VALID_TOKEN_22,
25
+ },
26
+ {
27
+ label: "space/doc",
28
+ url: `https://example.test/space/doc/${VALID_TOKEN_22}`,
29
+ expectedKind: "doc",
30
+ expectedResolvedType: "doc",
31
+ expectedToken: VALID_TOKEN_22,
32
+ },
33
+ {
34
+ label: "sheet",
35
+ url: `https://example.test/sheet/${VALID_TOKEN_22}`,
36
+ expectedKind: "sheet",
37
+ expectedResolvedType: "sheet",
38
+ expectedToken: VALID_TOKEN_22,
39
+ },
40
+ {
41
+ label: "sheets",
42
+ url: `https://example.test/sheets/${VALID_TOKEN_22}`,
43
+ expectedKind: "sheet",
44
+ expectedResolvedType: "sheet",
45
+ expectedToken: VALID_TOKEN_22,
46
+ },
47
+ {
48
+ label: "space/sheet",
49
+ url: `https://example.test/space/sheet/${VALID_TOKEN_22}`,
50
+ expectedKind: "sheet",
51
+ expectedResolvedType: "sheet",
52
+ expectedToken: VALID_TOKEN_22,
53
+ },
54
+ {
55
+ label: "docx with hash",
56
+ url: `https://bytedance.larkoffice.com/docx/${VALID_TOKEN_27}#share-Huggdiqveo5N7NxyA01ck4gLnHh`,
57
+ expectedKind: "docx",
58
+ expectedResolvedType: "docx",
59
+ expectedToken: VALID_TOKEN_27,
60
+ },
61
+ {
62
+ label: "mindnote",
63
+ url: `https://example.test/mindnote/${VALID_TOKEN_22}`,
64
+ expectedKind: "mindnote",
65
+ expectedResolvedType: "mindnote",
66
+ expectedToken: VALID_TOKEN_22,
67
+ },
68
+ {
69
+ label: "mindnotes",
70
+ url: `https://example.test/mindnotes/${VALID_TOKEN_22}`,
71
+ expectedKind: "mindnote",
72
+ expectedResolvedType: "mindnote",
73
+ expectedToken: VALID_TOKEN_22,
74
+ },
75
+ {
76
+ label: "space/mindnote",
77
+ url: `https://example.test/space/mindnote/${VALID_TOKEN_22}`,
78
+ expectedKind: "mindnote",
79
+ expectedResolvedType: "mindnote",
80
+ expectedToken: VALID_TOKEN_22,
81
+ },
82
+ {
83
+ label: "bitable",
84
+ url: `https://example.test/bitable/${VALID_TOKEN_22}?table=tbl_123`,
85
+ expectedKind: "bitable",
86
+ expectedResolvedType: "bitable",
87
+ expectedToken: VALID_TOKEN_22,
88
+ },
89
+ {
90
+ label: "base",
91
+ url: `https://example.test/base/${VALID_TOKEN_22}`,
92
+ expectedKind: "base",
93
+ expectedResolvedType: "base",
94
+ expectedToken: VALID_TOKEN_22,
95
+ },
96
+ {
97
+ label: "space/bitable",
98
+ url: `https://example.test/space/bitable/${VALID_TOKEN_22}`,
99
+ expectedKind: "bitable",
100
+ expectedResolvedType: "bitable",
101
+ expectedToken: VALID_TOKEN_22,
102
+ },
103
+ {
104
+ label: "file",
105
+ url: `https://example.test/file/${VALID_TOKEN_22}`,
106
+ expectedKind: "file",
107
+ expectedResolvedType: "file",
108
+ expectedToken: VALID_TOKEN_22,
109
+ },
110
+ {
111
+ label: "space/file",
112
+ url: `https://example.test/space/file/${VALID_TOKEN_22}`,
113
+ expectedKind: "file",
114
+ expectedResolvedType: "file",
115
+ expectedToken: VALID_TOKEN_22,
116
+ },
117
+ {
118
+ label: "wiki",
119
+ url: `https://example.test/wiki/${VALID_TOKEN_22}`,
120
+ expectedKind: "wiki",
121
+ expectedResolvedType: undefined,
122
+ expectedToken: VALID_TOKEN_22,
123
+ },
124
+ {
125
+ label: "space/wiki",
126
+ url: `https://example.test/space/wiki/${VALID_TOKEN_22}`,
127
+ expectedKind: "wiki",
128
+ expectedResolvedType: undefined,
129
+ expectedToken: VALID_TOKEN_22,
130
+ },
131
+ ])("$label", ({ url, expectedKind, expectedResolvedType, expectedToken }) => {
132
+ const linked = resolveCommentLinkedDocumentFromUrl({ rawUrl: url });
133
+
134
+ expect(linked.urlKind).toBe(expectedKind);
135
+ expect(linked.resolvedObjType).toBe(expectedResolvedType);
136
+ expect(linked.resolvedObjToken ?? linked.wikiNodeToken).toBe(expectedToken);
137
+ });
138
+
139
+ it("does not resolve doc-like paths with short tokens", () => {
140
+ expect(
141
+ resolveCommentLinkedDocumentFromUrl({
142
+ rawUrl: "https://www.baidu.com/docx/guide",
143
+ }),
144
+ ).toEqual({
145
+ rawUrl: "https://www.baidu.com/docx/guide",
146
+ urlKind: "unknown",
147
+ });
148
+ });
149
+ });
150
+
151
+ describe("parseCommentContentElements", () => {
152
+ it("keeps raw external urls in text but excludes unresolved links from structured references", () => {
153
+ const parsed = parseCommentContentElements({
154
+ elements: [
155
+ {
156
+ type: "docs_link",
157
+ docs_link: { url: `https://bytedance.larkoffice.com/docx/${VALID_TOKEN_27}` },
158
+ },
159
+ {
160
+ type: "text_run",
161
+ text_run: { text: " 和 " },
162
+ },
163
+ {
164
+ type: "docs_link",
165
+ docs_link: { url: "https://www.baidu.com/docx/guide" },
166
+ },
167
+ ],
168
+ });
169
+
170
+ expect(parsed.plainText).toBe(
171
+ `https://bytedance.larkoffice.com/docx/${VALID_TOKEN_27} 和 https://www.baidu.com/docx/guide`,
172
+ );
173
+ expect(parsed.linkedDocuments).toEqual([
174
+ expect.objectContaining({
175
+ rawUrl: `https://bytedance.larkoffice.com/docx/${VALID_TOKEN_27}`,
176
+ urlKind: "docx",
177
+ resolvedObjType: "docx",
178
+ resolvedObjToken: VALID_TOKEN_27,
179
+ }),
180
+ ]);
181
+ });
182
+ });