@actagent/feishu 2026.6.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 (207) hide show
  1. package/README.md +11 -0
  2. package/actagent.plugin.json +224 -0
  3. package/api.ts +33 -0
  4. package/channel-entry.ts +21 -0
  5. package/channel-plugin-api.ts +2 -0
  6. package/contract-api.ts +17 -0
  7. package/index.ts +83 -0
  8. package/legacy-state-migrations-api.ts +2 -0
  9. package/npm-shrinkwrap.json +539 -0
  10. package/package.json +64 -0
  11. package/runtime-api.ts +58 -0
  12. package/runtime-setter-api.ts +3 -0
  13. package/secret-contract-api.ts +6 -0
  14. package/security-contract-api.ts +2 -0
  15. package/session-key-api.ts +2 -0
  16. package/setup-api.ts +4 -0
  17. package/setup-entry.test.ts +33 -0
  18. package/setup-entry.ts +25 -0
  19. package/skills/feishu-doc/SKILL.md +211 -0
  20. package/skills/feishu-doc/references/block-types.md +103 -0
  21. package/skills/feishu-drive/SKILL.md +97 -0
  22. package/skills/feishu-perm/SKILL.md +119 -0
  23. package/skills/feishu-wiki/SKILL.md +113 -0
  24. package/src/accounts.test.ts +481 -0
  25. package/src/accounts.ts +380 -0
  26. package/src/agent-config.ts +22 -0
  27. package/src/app-registration.test.ts +62 -0
  28. package/src/app-registration.ts +355 -0
  29. package/src/approval-auth.test.ts +25 -0
  30. package/src/approval-auth.ts +26 -0
  31. package/src/async.test.ts +68 -0
  32. package/src/async.ts +109 -0
  33. package/src/audio-preflight.runtime.ts +10 -0
  34. package/src/bitable.test.ts +174 -0
  35. package/src/bitable.ts +781 -0
  36. package/src/bot-content.ts +488 -0
  37. package/src/bot-group-name.test.ts +148 -0
  38. package/src/bot-runtime-api.ts +13 -0
  39. package/src/bot-sender-name.test.ts +68 -0
  40. package/src/bot-sender-name.ts +137 -0
  41. package/src/bot.broadcast.test.ts +643 -0
  42. package/src/bot.card-action.test.ts +647 -0
  43. package/src/bot.checkBotMentioned.test.ts +266 -0
  44. package/src/bot.helpers.test.ts +136 -0
  45. package/src/bot.stripBotMention.test.ts +127 -0
  46. package/src/bot.test.ts +3817 -0
  47. package/src/bot.ts +1788 -0
  48. package/src/card-action.ts +515 -0
  49. package/src/card-interaction.test.ts +132 -0
  50. package/src/card-interaction.ts +160 -0
  51. package/src/card-test-helpers.ts +55 -0
  52. package/src/card-ux-approval.ts +66 -0
  53. package/src/card-ux-launcher.test.ts +126 -0
  54. package/src/card-ux-launcher.ts +136 -0
  55. package/src/card-ux-shared.ts +34 -0
  56. package/src/channel-runtime-api.ts +17 -0
  57. package/src/channel.runtime.ts +48 -0
  58. package/src/channel.test.ts +1337 -0
  59. package/src/channel.ts +1401 -0
  60. package/src/chat-schema.ts +30 -0
  61. package/src/chat.test.ts +295 -0
  62. package/src/chat.ts +198 -0
  63. package/src/client-timeout.ts +44 -0
  64. package/src/client.test.ts +463 -0
  65. package/src/client.ts +263 -0
  66. package/src/comment-dispatcher-runtime-api.ts +7 -0
  67. package/src/comment-dispatcher.test.ts +186 -0
  68. package/src/comment-dispatcher.ts +108 -0
  69. package/src/comment-handler-runtime-api.ts +4 -0
  70. package/src/comment-handler.test.ts +588 -0
  71. package/src/comment-handler.ts +304 -0
  72. package/src/comment-reaction.test.ts +139 -0
  73. package/src/comment-reaction.ts +260 -0
  74. package/src/comment-shared.test.ts +184 -0
  75. package/src/comment-shared.ts +405 -0
  76. package/src/comment-target.ts +45 -0
  77. package/src/config-schema.test.ts +327 -0
  78. package/src/config-schema.ts +338 -0
  79. package/src/conversation-id.test.ts +19 -0
  80. package/src/conversation-id.ts +199 -0
  81. package/src/dedup-migrations.test.ts +90 -0
  82. package/src/dedup-migrations.ts +103 -0
  83. package/src/dedup.test.ts +95 -0
  84. package/src/dedup.ts +304 -0
  85. package/src/dedupe-key.ts +68 -0
  86. package/src/directory.static.ts +62 -0
  87. package/src/directory.test.ts +142 -0
  88. package/src/directory.ts +125 -0
  89. package/src/doc-schema.ts +183 -0
  90. package/src/doctor.test.ts +382 -0
  91. package/src/doctor.ts +876 -0
  92. package/src/docx-batch-insert.test.ts +117 -0
  93. package/src/docx-batch-insert.ts +223 -0
  94. package/src/docx-color-text.ts +154 -0
  95. package/src/docx-table-ops.test.ts +54 -0
  96. package/src/docx-table-ops.ts +316 -0
  97. package/src/docx-types.ts +39 -0
  98. package/src/docx.account-selection.test.ts +96 -0
  99. package/src/docx.test.ts +706 -0
  100. package/src/docx.ts +1598 -0
  101. package/src/drive-schema.ts +93 -0
  102. package/src/drive.test.ts +1240 -0
  103. package/src/drive.ts +830 -0
  104. package/src/dynamic-agent.test.ts +156 -0
  105. package/src/dynamic-agent.ts +144 -0
  106. package/src/event-types.ts +46 -0
  107. package/src/external-keys.test.ts +21 -0
  108. package/src/external-keys.ts +20 -0
  109. package/src/lifecycle.test-support.ts +223 -0
  110. package/src/media.test.ts +956 -0
  111. package/src/media.ts +1106 -0
  112. package/src/mention-target.types.ts +6 -0
  113. package/src/mention.ts +115 -0
  114. package/src/message-action-contract.ts +14 -0
  115. package/src/monitor-state-runtime-api.ts +8 -0
  116. package/src/monitor-transport-runtime-api.ts +11 -0
  117. package/src/monitor.account.ts +501 -0
  118. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +215 -0
  119. package/src/monitor.bot-identity.ts +87 -0
  120. package/src/monitor.bot-menu-handler.ts +164 -0
  121. package/src/monitor.bot-menu.lifecycle.test-support.ts +221 -0
  122. package/src/monitor.bot-menu.test.ts +200 -0
  123. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +265 -0
  124. package/src/monitor.card-action.lifecycle.test-support.ts +418 -0
  125. package/src/monitor.cleanup.test.ts +384 -0
  126. package/src/monitor.comment-notice-handler.ts +106 -0
  127. package/src/monitor.comment.test.ts +968 -0
  128. package/src/monitor.comment.ts +1386 -0
  129. package/src/monitor.lifecycle.test.ts +5 -0
  130. package/src/monitor.message-handler.ts +346 -0
  131. package/src/monitor.reaction.test.ts +770 -0
  132. package/src/monitor.startup.test.ts +232 -0
  133. package/src/monitor.startup.ts +76 -0
  134. package/src/monitor.state.defaults.test.ts +47 -0
  135. package/src/monitor.state.ts +171 -0
  136. package/src/monitor.synthetic-error.ts +19 -0
  137. package/src/monitor.test-mocks.ts +47 -0
  138. package/src/monitor.transport.ts +451 -0
  139. package/src/monitor.ts +104 -0
  140. package/src/monitor.webhook-e2e.test.ts +284 -0
  141. package/src/monitor.webhook-security.test.ts +394 -0
  142. package/src/monitor.webhook.test-helpers.ts +138 -0
  143. package/src/outbound-runtime-api.ts +2 -0
  144. package/src/outbound.test.ts +1255 -0
  145. package/src/outbound.ts +742 -0
  146. package/src/perm-schema.ts +53 -0
  147. package/src/perm.ts +171 -0
  148. package/src/pins.ts +109 -0
  149. package/src/policy.test.ts +224 -0
  150. package/src/policy.ts +322 -0
  151. package/src/post.test.ts +106 -0
  152. package/src/post.ts +276 -0
  153. package/src/presentation-card.ts +204 -0
  154. package/src/probe.test.ts +310 -0
  155. package/src/probe.ts +181 -0
  156. package/src/processing-claims.ts +60 -0
  157. package/src/qr-terminal.ts +2 -0
  158. package/src/reactions.ts +124 -0
  159. package/src/reasoning-preview.test.ts +114 -0
  160. package/src/reasoning-preview.ts +29 -0
  161. package/src/reply-dispatcher-runtime-api.ts +8 -0
  162. package/src/reply-dispatcher.test.ts +2009 -0
  163. package/src/reply-dispatcher.ts +865 -0
  164. package/src/runtime.ts +10 -0
  165. package/src/secret-contract.ts +146 -0
  166. package/src/secret-input.ts +2 -0
  167. package/src/security-audit-shared.ts +70 -0
  168. package/src/security-audit.test.ts +60 -0
  169. package/src/security-audit.ts +2 -0
  170. package/src/send-result.ts +81 -0
  171. package/src/send-target.test.ts +87 -0
  172. package/src/send-target.ts +36 -0
  173. package/src/send.reply-fallback.test.ts +418 -0
  174. package/src/send.test.ts +661 -0
  175. package/src/send.ts +860 -0
  176. package/src/sequential-key.test.ts +73 -0
  177. package/src/sequential-key.ts +29 -0
  178. package/src/sequential-queue.test.ts +184 -0
  179. package/src/sequential-queue.ts +90 -0
  180. package/src/session-conversation.ts +42 -0
  181. package/src/session-route.ts +49 -0
  182. package/src/setup-core.ts +52 -0
  183. package/src/setup-surface.test.ts +485 -0
  184. package/src/setup-surface.ts +620 -0
  185. package/src/streaming-card.test.ts +549 -0
  186. package/src/streaming-card.ts +611 -0
  187. package/src/subagent-hooks.test.ts +632 -0
  188. package/src/subagent-hooks.ts +414 -0
  189. package/src/targets.ts +98 -0
  190. package/src/test-support/lifecycle-test-support.ts +459 -0
  191. package/src/thread-bindings.test.ts +181 -0
  192. package/src/thread-bindings.ts +332 -0
  193. package/src/tool-account-routing.test.ts +419 -0
  194. package/src/tool-account.test.ts +45 -0
  195. package/src/tool-account.ts +98 -0
  196. package/src/tool-factory-test-harness.ts +83 -0
  197. package/src/tool-result.test.ts +33 -0
  198. package/src/tool-result.ts +17 -0
  199. package/src/tools-config.test.ts +52 -0
  200. package/src/tools-config.ts +29 -0
  201. package/src/types.ts +111 -0
  202. package/src/typing.test.ts +145 -0
  203. package/src/typing.ts +215 -0
  204. package/src/wiki-schema.ts +70 -0
  205. package/src/wiki.ts +271 -0
  206. package/subagent-hooks-api.ts +22 -0
  207. package/tsconfig.json +16 -0
@@ -0,0 +1,380 @@
1
+ // Feishu plugin module implements accounts behavior.
2
+ import {
3
+ DEFAULT_ACCOUNT_ID,
4
+ type ACTAgentConfig as ACTAgentBotConfig,
5
+ createAccountListHelpers,
6
+ hasConfiguredAccountValue,
7
+ normalizeAccountId,
8
+ normalizeOptionalAccountId,
9
+ resolveMergedAccountConfig,
10
+ } from "actagent/plugin-sdk/account-resolution";
11
+ import { coerceSecretRef } from "actagent/plugin-sdk/provider-auth";
12
+ import { normalizeString } from "./comment-shared.js";
13
+ import type {
14
+ FeishuConfig,
15
+ FeishuAccountConfig,
16
+ FeishuDefaultAccountSelectionSource,
17
+ FeishuDomain,
18
+ ResolvedFeishuAccount,
19
+ } from "./types.js";
20
+
21
+ const { listAccountIds: listFeishuAccountIds, resolveDefaultAccountId } = createAccountListHelpers(
22
+ "feishu",
23
+ {
24
+ allowUnlistedDefaultAccount: true,
25
+ hasImplicitDefaultAccount: (cfg) => {
26
+ const feishu = cfg.channels?.feishu;
27
+ return (
28
+ hasConfiguredAccountValue(feishu?.appId) && hasConfiguredAccountValue(feishu?.appSecret)
29
+ );
30
+ },
31
+ },
32
+ );
33
+
34
+ export { listFeishuAccountIds };
35
+
36
+ type FeishuCredentialResolutionMode = "inspect" | "strict";
37
+ type FeishuResolvedSecretRef = NonNullable<ReturnType<typeof coerceSecretRef>>;
38
+
39
+ function formatSecretRefLabel(ref: FeishuResolvedSecretRef): string {
40
+ return `${ref.source}:${ref.provider}:${ref.id}`;
41
+ }
42
+
43
+ export class FeishuSecretRefUnavailableError extends Error {
44
+ path: string;
45
+
46
+ constructor(path: string, ref: FeishuResolvedSecretRef) {
47
+ super(
48
+ `${path}: unresolved SecretRef "${formatSecretRefLabel(ref)}". ` +
49
+ "Resolve this command against an active gateway runtime snapshot before reading it.",
50
+ );
51
+ this.name = "FeishuSecretRefUnavailableError";
52
+ this.path = path;
53
+ }
54
+ }
55
+
56
+ export function isFeishuSecretRefUnavailableError(
57
+ error: unknown,
58
+ ): error is FeishuSecretRefUnavailableError {
59
+ return error instanceof FeishuSecretRefUnavailableError;
60
+ }
61
+
62
+ function resolveFeishuSecretLike(params: {
63
+ value: unknown;
64
+ path: string;
65
+ mode: FeishuCredentialResolutionMode;
66
+ allowEnvSecretRefRead?: boolean;
67
+ }): string | undefined {
68
+ const asString = normalizeString(params.value);
69
+ if (asString) {
70
+ return asString;
71
+ }
72
+
73
+ const ref = coerceSecretRef(params.value);
74
+ if (!ref) {
75
+ return undefined;
76
+ }
77
+
78
+ if (params.mode === "inspect") {
79
+ if (params.allowEnvSecretRefRead && ref.source === "env") {
80
+ const envValue = normalizeString(process.env[ref.id]);
81
+ if (envValue) {
82
+ return envValue;
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ throw new FeishuSecretRefUnavailableError(params.path, ref);
89
+ }
90
+
91
+ function resolveFeishuBaseCredentials(
92
+ cfg: FeishuConfig | undefined,
93
+ mode: FeishuCredentialResolutionMode,
94
+ ): {
95
+ appId: string;
96
+ appSecret: string;
97
+ domain: FeishuDomain;
98
+ } | null {
99
+ const appId = resolveFeishuSecretLike({
100
+ value: cfg?.appId,
101
+ path: "channels.feishu.appId",
102
+ mode,
103
+ allowEnvSecretRefRead: true,
104
+ });
105
+ const appSecret = resolveFeishuSecretLike({
106
+ value: cfg?.appSecret,
107
+ path: "channels.feishu.appSecret",
108
+ mode,
109
+ allowEnvSecretRefRead: true,
110
+ });
111
+
112
+ if (!appId || !appSecret) {
113
+ return null;
114
+ }
115
+
116
+ return {
117
+ appId,
118
+ appSecret,
119
+ domain: cfg?.domain ?? "feishu",
120
+ };
121
+ }
122
+
123
+ function resolveFeishuEventSecrets(
124
+ cfg: FeishuConfig | undefined,
125
+ mode: FeishuCredentialResolutionMode,
126
+ ): {
127
+ encryptKey?: string;
128
+ verificationToken?: string;
129
+ } {
130
+ return {
131
+ encryptKey:
132
+ (cfg?.connectionMode ?? "websocket") === "webhook"
133
+ ? resolveFeishuSecretLike({
134
+ value: cfg?.encryptKey,
135
+ path: "channels.feishu.encryptKey",
136
+ mode,
137
+ allowEnvSecretRefRead: true,
138
+ })
139
+ : normalizeString(cfg?.encryptKey),
140
+ verificationToken: resolveFeishuSecretLike({
141
+ value: cfg?.verificationToken,
142
+ path: "channels.feishu.verificationToken",
143
+ mode,
144
+ allowEnvSecretRefRead: true,
145
+ }),
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Resolve the default account selection and its source.
151
+ */
152
+ export function resolveDefaultFeishuAccountSelection(cfg: ACTAgentBotConfig): {
153
+ accountId: string;
154
+ source: FeishuDefaultAccountSelectionSource;
155
+ } {
156
+ const preferred = normalizeOptionalAccountId(
157
+ (cfg.channels?.feishu as FeishuConfig | undefined)?.defaultAccount,
158
+ );
159
+ if (preferred) {
160
+ return {
161
+ accountId: preferred,
162
+ source: "explicit-default",
163
+ };
164
+ }
165
+ const ids = listFeishuAccountIds(cfg);
166
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
167
+ return {
168
+ accountId: DEFAULT_ACCOUNT_ID,
169
+ source: "mapped-default",
170
+ };
171
+ }
172
+ return {
173
+ accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
174
+ source: "fallback",
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Resolve the default account ID.
180
+ */
181
+ export function resolveDefaultFeishuAccountId(cfg: ACTAgentBotConfig): string {
182
+ return resolveDefaultAccountId(cfg);
183
+ }
184
+
185
+ function resolveRawFeishuAccountConfig(
186
+ accounts: Record<string, Partial<FeishuConfig>> | undefined,
187
+ accountId: string,
188
+ ): Partial<FeishuConfig> | undefined {
189
+ if (!accounts || typeof accounts !== "object") {
190
+ return undefined;
191
+ }
192
+ if (Object.hasOwn(accounts, accountId)) {
193
+ return accounts[accountId];
194
+ }
195
+ const normalized = accountId.toLowerCase();
196
+ const matchKey = Object.keys(accounts).find((key) => key.toLowerCase() === normalized);
197
+ return matchKey ? accounts[matchKey] : undefined;
198
+ }
199
+
200
+ /**
201
+ * Merge top-level config with account-specific config.
202
+ * Account-specific fields override top-level fields.
203
+ */
204
+ function mergeFeishuAccountConfig(cfg: ACTAgentBotConfig, accountId: string): FeishuConfig {
205
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
206
+ const accounts = feishuCfg?.accounts as Record<string, Partial<FeishuConfig>> | undefined;
207
+ const accountTools = resolveRawFeishuAccountConfig(accounts, accountId)?.tools;
208
+ const merged = resolveMergedAccountConfig<FeishuConfig>({
209
+ channelConfig: feishuCfg,
210
+ accounts,
211
+ accountId,
212
+ omitKeys: ["defaultAccount"],
213
+ nestedObjectKeys: ["tools"],
214
+ });
215
+ const topTools = feishuCfg?.tools;
216
+ if (merged.tools === undefined && topTools !== undefined) {
217
+ return { ...merged, tools: topTools };
218
+ }
219
+ if (
220
+ topTools?.bitable === false ||
221
+ (topTools?.bitable === undefined && topTools?.base === false)
222
+ ) {
223
+ return {
224
+ ...merged,
225
+ tools: {
226
+ ...merged.tools,
227
+ bitable: false,
228
+ base: false,
229
+ },
230
+ };
231
+ }
232
+ if (accountTools?.bitable === undefined && accountTools?.base !== undefined) {
233
+ return {
234
+ ...merged,
235
+ tools: {
236
+ ...merged.tools,
237
+ bitable: accountTools.base,
238
+ base: accountTools.base,
239
+ },
240
+ };
241
+ }
242
+ return merged;
243
+ }
244
+
245
+ /**
246
+ * Resolve Feishu credentials from a config.
247
+ */
248
+ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
249
+ appId: string;
250
+ appSecret: string;
251
+ encryptKey?: string;
252
+ verificationToken?: string;
253
+ domain: FeishuDomain;
254
+ } | null;
255
+ export function resolveFeishuCredentials(
256
+ cfg: FeishuConfig | undefined,
257
+ options: {
258
+ mode?: FeishuCredentialResolutionMode;
259
+ allowUnresolvedSecretRef?: boolean;
260
+ },
261
+ ): {
262
+ appId: string;
263
+ appSecret: string;
264
+ encryptKey?: string;
265
+ verificationToken?: string;
266
+ domain: FeishuDomain;
267
+ } | null;
268
+ export function resolveFeishuCredentials(
269
+ cfg?: FeishuConfig,
270
+ options?: {
271
+ mode?: FeishuCredentialResolutionMode;
272
+ allowUnresolvedSecretRef?: boolean;
273
+ },
274
+ ): {
275
+ appId: string;
276
+ appSecret: string;
277
+ encryptKey?: string;
278
+ verificationToken?: string;
279
+ domain: FeishuDomain;
280
+ } | null {
281
+ const mode = options?.mode ?? (options?.allowUnresolvedSecretRef ? "inspect" : "strict");
282
+ const base = resolveFeishuBaseCredentials(cfg, mode);
283
+ if (!base) {
284
+ return null;
285
+ }
286
+ const eventSecrets = resolveFeishuEventSecrets(cfg, mode);
287
+
288
+ return {
289
+ ...base,
290
+ ...eventSecrets,
291
+ };
292
+ }
293
+
294
+ export function inspectFeishuCredentials(cfg?: FeishuConfig) {
295
+ return resolveFeishuCredentials(cfg, { mode: "inspect" });
296
+ }
297
+
298
+ function buildResolvedFeishuAccount(params: {
299
+ cfg: ACTAgentBotConfig;
300
+ accountId?: string | null;
301
+ baseMode: FeishuCredentialResolutionMode;
302
+ eventSecretMode: FeishuCredentialResolutionMode;
303
+ }): ResolvedFeishuAccount {
304
+ const hasExplicitAccountId =
305
+ typeof params.accountId === "string" && params.accountId.trim() !== "";
306
+ const defaultSelection = hasExplicitAccountId
307
+ ? null
308
+ : resolveDefaultFeishuAccountSelection(params.cfg);
309
+ const accountId = hasExplicitAccountId
310
+ ? normalizeAccountId(params.accountId)
311
+ : (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID);
312
+ const selectionSource = hasExplicitAccountId
313
+ ? "explicit"
314
+ : (defaultSelection?.source ?? "fallback");
315
+ const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
316
+
317
+ const baseEnabled = feishuCfg?.enabled !== false;
318
+ const merged = mergeFeishuAccountConfig(params.cfg, accountId);
319
+ const accountEnabled = merged.enabled !== false;
320
+ const enabled = baseEnabled && accountEnabled;
321
+ const baseCreds = resolveFeishuBaseCredentials(merged, params.baseMode);
322
+ const eventSecrets = resolveFeishuEventSecrets(merged, params.eventSecretMode);
323
+ const accountName = (merged as FeishuAccountConfig).name;
324
+
325
+ return {
326
+ accountId,
327
+ selectionSource,
328
+ enabled,
329
+ configured: Boolean(baseCreds),
330
+ name: typeof accountName === "string" ? accountName.trim() || undefined : undefined,
331
+ appId: baseCreds?.appId,
332
+ appSecret: baseCreds?.appSecret,
333
+ encryptKey: eventSecrets.encryptKey,
334
+ verificationToken: eventSecrets.verificationToken,
335
+ domain: baseCreds?.domain ?? "feishu",
336
+ config: merged,
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Resolve a read-only Feishu account snapshot for CLI/config surfaces.
342
+ * Unresolved SecretRefs are treated as unavailable instead of throwing.
343
+ */
344
+ export function resolveFeishuAccount(params: {
345
+ cfg: ACTAgentBotConfig;
346
+ accountId?: string | null;
347
+ }): ResolvedFeishuAccount {
348
+ return buildResolvedFeishuAccount({
349
+ ...params,
350
+ baseMode: "inspect",
351
+ eventSecretMode: "inspect",
352
+ });
353
+ }
354
+
355
+ /**
356
+ * Resolve a runtime Feishu account.
357
+ * Required app credentials stay strict; event-only secrets can be required by callers.
358
+ */
359
+ export function resolveFeishuRuntimeAccount(
360
+ params: {
361
+ cfg: ACTAgentBotConfig;
362
+ accountId?: string | null;
363
+ },
364
+ options?: { requireEventSecrets?: boolean },
365
+ ): ResolvedFeishuAccount {
366
+ return buildResolvedFeishuAccount({
367
+ ...params,
368
+ baseMode: "strict",
369
+ eventSecretMode: options?.requireEventSecrets ? "strict" : "inspect",
370
+ });
371
+ }
372
+
373
+ /**
374
+ * List all enabled and configured accounts.
375
+ */
376
+ export function listEnabledFeishuAccounts(cfg: ACTAgentBotConfig): ResolvedFeishuAccount[] {
377
+ return listFeishuAccountIds(cfg)
378
+ .map((accountId) => resolveFeishuAccount({ cfg, accountId }))
379
+ .filter((account) => account.enabled && account.configured);
380
+ }
@@ -0,0 +1,22 @@
1
+ // Feishu helper module supports agent config behavior.
2
+ import type { ACTAgentBotConfig } from "./bot-runtime-api.js";
3
+
4
+ type ReasoningDefault = "on" | "stream" | "off";
5
+
6
+ const DEFAULT_AGENT_ID = "main";
7
+
8
+ function normalizeAgentId(value: string | undefined | null): string {
9
+ const normalized = (value ?? "").trim().toLowerCase();
10
+ return normalized || DEFAULT_AGENT_ID;
11
+ }
12
+
13
+ export function resolveFeishuConfigReasoningDefault(
14
+ cfg: ACTAgentBotConfig,
15
+ agentId: string,
16
+ ): ReasoningDefault {
17
+ const id = normalizeAgentId(agentId);
18
+ const agentDefault = cfg.agents?.list?.find(
19
+ (entry) => normalizeAgentId(entry?.id) === id,
20
+ )?.reasoningDefault;
21
+ return agentDefault ?? cfg.agents?.defaults?.reasoningDefault ?? "off";
22
+ }
@@ -0,0 +1,62 @@
1
+ // Feishu tests cover app registration plugin behavior.
2
+ import { MAX_TIMER_TIMEOUT_MS } from "actagent/plugin-sdk/number-runtime";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { beginAppRegistration, pollAppRegistration } from "./app-registration.js";
5
+
6
+ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
7
+ fetchWithSsrFGuardMock: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("actagent/plugin-sdk/ssrf-runtime", () => ({
11
+ fetchWithSsrFGuard: fetchWithSsrFGuardMock,
12
+ }));
13
+
14
+ function mockFeishuJson(payload: unknown) {
15
+ fetchWithSsrFGuardMock.mockResolvedValueOnce({
16
+ response: new Response(JSON.stringify(payload), { status: 200 }),
17
+ release: async () => {},
18
+ });
19
+ }
20
+
21
+ describe("Feishu app registration", () => {
22
+ afterEach(() => {
23
+ vi.useRealTimers();
24
+ vi.restoreAllMocks();
25
+ fetchWithSsrFGuardMock.mockReset();
26
+ });
27
+
28
+ it("defaults unsafe begin polling lifetimes from provider responses", async () => {
29
+ mockFeishuJson({
30
+ device_code: "device-code",
31
+ verification_uri_complete: "https://accounts.feishu.cn/verify?x=1",
32
+ user_code: "user-code",
33
+ interval: Number.POSITIVE_INFINITY,
34
+ expire_in: Number.POSITIVE_INFINITY,
35
+ });
36
+
37
+ await expect(beginAppRegistration()).resolves.toMatchObject({
38
+ deviceCode: "device-code",
39
+ userCode: "user-code",
40
+ interval: 5,
41
+ expireIn: 600,
42
+ });
43
+ });
44
+
45
+ it("clamps unsafe poll sleeps from provider intervals", async () => {
46
+ vi.useFakeTimers();
47
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
48
+ fetchWithSsrFGuardMock.mockRejectedValueOnce(new Error("transient"));
49
+
50
+ const poll = pollAppRegistration({
51
+ deviceCode: "device-code",
52
+ interval: 10_000_000,
53
+ expireIn: 10_000_000,
54
+ });
55
+ await vi.advanceTimersByTimeAsync(0);
56
+
57
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
58
+
59
+ await vi.runOnlyPendingTimersAsync();
60
+ await expect(poll).resolves.toEqual({ status: "timeout" });
61
+ });
62
+ });