@kodelyth/feishu 2026.5.42 → 2026.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/klaw.plugin.json +1712 -47
  2. package/package.json +17 -4
  3. package/api.ts +0 -32
  4. package/channel-entry.ts +0 -20
  5. package/channel-plugin-api.ts +0 -1
  6. package/contract-api.ts +0 -16
  7. package/index.ts +0 -82
  8. package/runtime-api.ts +0 -52
  9. package/secret-contract-api.ts +0 -5
  10. package/security-contract-api.ts +0 -1
  11. package/session-key-api.ts +0 -1
  12. package/setup-api.ts +0 -3
  13. package/setup-entry.test.ts +0 -19
  14. package/setup-entry.ts +0 -13
  15. package/src/accounts.test.ts +0 -480
  16. package/src/accounts.ts +0 -333
  17. package/src/agent-config.ts +0 -21
  18. package/src/app-registration.ts +0 -331
  19. package/src/approval-auth.test.ts +0 -24
  20. package/src/approval-auth.ts +0 -25
  21. package/src/async.test.ts +0 -35
  22. package/src/async.ts +0 -104
  23. package/src/audio-preflight.runtime.ts +0 -9
  24. package/src/bitable.test.ts +0 -136
  25. package/src/bitable.ts +0 -762
  26. package/src/bot-content.ts +0 -485
  27. package/src/bot-group-name.test.ts +0 -116
  28. package/src/bot-runtime-api.ts +0 -12
  29. package/src/bot-sender-name.ts +0 -125
  30. package/src/bot.broadcast.test.ts +0 -523
  31. package/src/bot.card-action.test.ts +0 -552
  32. package/src/bot.checkBotMentioned.test.ts +0 -265
  33. package/src/bot.helpers.test.ts +0 -135
  34. package/src/bot.stripBotMention.test.ts +0 -126
  35. package/src/bot.test.ts +0 -3671
  36. package/src/bot.ts +0 -1703
  37. package/src/card-action.ts +0 -447
  38. package/src/card-interaction.test.ts +0 -131
  39. package/src/card-interaction.ts +0 -159
  40. package/src/card-test-helpers.ts +0 -54
  41. package/src/card-ux-approval.ts +0 -65
  42. package/src/card-ux-launcher.test.ts +0 -106
  43. package/src/card-ux-launcher.ts +0 -121
  44. package/src/card-ux-shared.ts +0 -33
  45. package/src/channel-runtime-api.ts +0 -16
  46. package/src/channel.runtime.ts +0 -47
  47. package/src/channel.test.ts +0 -1151
  48. package/src/channel.ts +0 -1423
  49. package/src/chat-schema.ts +0 -25
  50. package/src/chat.test.ts +0 -240
  51. package/src/chat.ts +0 -188
  52. package/src/client-timeout.ts +0 -42
  53. package/src/client.test.ts +0 -447
  54. package/src/client.ts +0 -262
  55. package/src/comment-dispatcher-runtime-api.ts +0 -6
  56. package/src/comment-dispatcher.test.ts +0 -185
  57. package/src/comment-dispatcher.ts +0 -107
  58. package/src/comment-handler-runtime-api.ts +0 -3
  59. package/src/comment-handler.test.ts +0 -592
  60. package/src/comment-handler.ts +0 -303
  61. package/src/comment-reaction.test.ts +0 -138
  62. package/src/comment-reaction.ts +0 -259
  63. package/src/comment-shared.test.ts +0 -183
  64. package/src/comment-shared.ts +0 -406
  65. package/src/comment-target.ts +0 -44
  66. package/src/config-schema.test.ts +0 -326
  67. package/src/config-schema.ts +0 -335
  68. package/src/conversation-id.test.ts +0 -18
  69. package/src/conversation-id.ts +0 -199
  70. package/src/dedup-runtime-api.ts +0 -1
  71. package/src/dedup.ts +0 -141
  72. package/src/dedupe-key.ts +0 -72
  73. package/src/directory.static.ts +0 -61
  74. package/src/directory.test.ts +0 -141
  75. package/src/directory.ts +0 -124
  76. package/src/doc-schema.ts +0 -182
  77. package/src/docx-batch-insert.test.ts +0 -116
  78. package/src/docx-batch-insert.ts +0 -223
  79. package/src/docx-color-text.ts +0 -154
  80. package/src/docx-table-ops.test.ts +0 -53
  81. package/src/docx-table-ops.ts +0 -316
  82. package/src/docx-types.ts +0 -38
  83. package/src/docx.account-selection.test.ts +0 -95
  84. package/src/docx.test.ts +0 -701
  85. package/src/docx.ts +0 -1596
  86. package/src/drive-schema.ts +0 -92
  87. package/src/drive.test.ts +0 -1237
  88. package/src/drive.ts +0 -829
  89. package/src/dynamic-agent.test.ts +0 -155
  90. package/src/dynamic-agent.ts +0 -143
  91. package/src/event-types.ts +0 -45
  92. package/src/external-keys.test.ts +0 -20
  93. package/src/external-keys.ts +0 -19
  94. package/src/lifecycle.test-support.ts +0 -220
  95. package/src/media.test.ts +0 -955
  96. package/src/media.ts +0 -1105
  97. package/src/mention-target.types.ts +0 -5
  98. package/src/mention.ts +0 -114
  99. package/src/message-action-contract.ts +0 -13
  100. package/src/monitor-state-runtime-api.ts +0 -7
  101. package/src/monitor-transport-runtime-api.ts +0 -10
  102. package/src/monitor.account.ts +0 -492
  103. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
  104. package/src/monitor.bot-identity.ts +0 -86
  105. package/src/monitor.bot-menu-handler.ts +0 -165
  106. package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
  107. package/src/monitor.bot-menu.test.ts +0 -188
  108. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
  109. package/src/monitor.card-action.lifecycle.test-support.ts +0 -421
  110. package/src/monitor.cleanup.test.ts +0 -383
  111. package/src/monitor.comment-notice-handler.ts +0 -105
  112. package/src/monitor.comment.test.ts +0 -967
  113. package/src/monitor.comment.ts +0 -1386
  114. package/src/monitor.lifecycle.test.ts +0 -4
  115. package/src/monitor.message-handler.ts +0 -350
  116. package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
  117. package/src/monitor.reaction.test.ts +0 -739
  118. package/src/monitor.startup.test.ts +0 -213
  119. package/src/monitor.startup.ts +0 -74
  120. package/src/monitor.state.defaults.test.ts +0 -46
  121. package/src/monitor.state.ts +0 -170
  122. package/src/monitor.synthetic-error.ts +0 -18
  123. package/src/monitor.test-mocks.ts +0 -46
  124. package/src/monitor.transport.ts +0 -451
  125. package/src/monitor.ts +0 -100
  126. package/src/monitor.webhook-e2e.test.ts +0 -279
  127. package/src/monitor.webhook-security.test.ts +0 -389
  128. package/src/monitor.webhook.test-helpers.ts +0 -116
  129. package/src/outbound-runtime-api.ts +0 -1
  130. package/src/outbound.test.ts +0 -1118
  131. package/src/outbound.ts +0 -785
  132. package/src/perm-schema.ts +0 -52
  133. package/src/perm.ts +0 -170
  134. package/src/pins.ts +0 -108
  135. package/src/policy.test.ts +0 -223
  136. package/src/policy.ts +0 -318
  137. package/src/post.test.ts +0 -105
  138. package/src/post.ts +0 -275
  139. package/src/probe.test.ts +0 -283
  140. package/src/probe.ts +0 -166
  141. package/src/processing-claims.ts +0 -59
  142. package/src/qr-terminal.ts +0 -1
  143. package/src/reactions.ts +0 -123
  144. package/src/reasoning-preview.test.ts +0 -113
  145. package/src/reasoning-preview.ts +0 -28
  146. package/src/reply-dispatcher-runtime-api.ts +0 -7
  147. package/src/reply-dispatcher.test.ts +0 -1513
  148. package/src/reply-dispatcher.ts +0 -748
  149. package/src/runtime.ts +0 -9
  150. package/src/secret-contract.ts +0 -145
  151. package/src/secret-input.ts +0 -1
  152. package/src/security-audit-shared.ts +0 -69
  153. package/src/security-audit.test.ts +0 -59
  154. package/src/security-audit.ts +0 -1
  155. package/src/send-result.ts +0 -80
  156. package/src/send-target.test.ts +0 -86
  157. package/src/send-target.ts +0 -35
  158. package/src/send.reply-fallback.test.ts +0 -417
  159. package/src/send.test.ts +0 -621
  160. package/src/send.ts +0 -861
  161. package/src/sequential-key.test.ts +0 -72
  162. package/src/sequential-key.ts +0 -25
  163. package/src/sequential-queue.test.ts +0 -165
  164. package/src/sequential-queue.ts +0 -86
  165. package/src/session-conversation.ts +0 -42
  166. package/src/session-route.ts +0 -48
  167. package/src/setup-core.ts +0 -51
  168. package/src/setup-surface.test.ts +0 -484
  169. package/src/setup-surface.ts +0 -618
  170. package/src/streaming-card.test.ts +0 -397
  171. package/src/streaming-card.ts +0 -571
  172. package/src/subagent-hooks.test.ts +0 -627
  173. package/src/subagent-hooks.ts +0 -413
  174. package/src/targets.ts +0 -97
  175. package/src/test-support/lifecycle-test-support.ts +0 -454
  176. package/src/thread-bindings.test.ts +0 -180
  177. package/src/thread-bindings.ts +0 -331
  178. package/src/tool-account-routing.test.ts +0 -250
  179. package/src/tool-account.test.ts +0 -44
  180. package/src/tool-account.ts +0 -93
  181. package/src/tool-factory-test-harness.ts +0 -79
  182. package/src/tool-result.test.ts +0 -32
  183. package/src/tool-result.ts +0 -16
  184. package/src/tools-config.test.ts +0 -21
  185. package/src/tools-config.ts +0 -22
  186. package/src/types.ts +0 -106
  187. package/src/typing.test.ts +0 -144
  188. package/src/typing.ts +0 -214
  189. package/src/wiki-schema.ts +0 -69
  190. package/src/wiki.ts +0 -270
  191. package/subagent-hooks-api.ts +0 -31
  192. package/tsconfig.json +0 -16
package/src/channel.ts DELETED
@@ -1,1423 +0,0 @@
1
- import { describeAccountSnapshot } from "klaw/plugin-sdk/account-helpers";
2
- import { formatAllowFromLowercase } from "klaw/plugin-sdk/allow-from";
3
- import {
4
- adaptScopedAccountAccessor,
5
- createHybridChannelConfigAdapter,
6
- } from "klaw/plugin-sdk/channel-config-helpers";
7
- import type {
8
- ChannelMessageActionAdapter,
9
- ChannelMessageActionContext,
10
- ChannelMessageToolDiscovery,
11
- } from "klaw/plugin-sdk/channel-contract";
12
- import { createChatChannelPlugin } from "klaw/plugin-sdk/channel-core";
13
- import {
14
- defineChannelMessageAdapter,
15
- type ChannelMessageSendResult,
16
- type MessageReceiptPartKind,
17
- } from "klaw/plugin-sdk/channel-message";
18
- import { createPairingPrefixStripper } from "klaw/plugin-sdk/channel-pairing";
19
- import {
20
- createAllowlistProviderGroupPolicyWarningCollector,
21
- projectConfigAccountIdWarningCollector,
22
- } from "klaw/plugin-sdk/channel-policy";
23
- import { getSessionBindingService } from "klaw/plugin-sdk/conversation-runtime";
24
- import {
25
- createChannelDirectoryAdapter,
26
- createRuntimeDirectoryLiveAdapter,
27
- } from "klaw/plugin-sdk/directory-runtime";
28
- import {
29
- normalizeMessagePresentation,
30
- renderMessagePresentationFallbackText,
31
- } from "klaw/plugin-sdk/interactive-runtime";
32
- import { createLazyRuntimeNamedExport } from "klaw/plugin-sdk/lazy-runtime";
33
- import { createRuntimeOutboundDelegates } from "klaw/plugin-sdk/outbound-runtime";
34
- import { createComputedAccountStatusAdapter } from "klaw/plugin-sdk/status-helpers";
35
- import { normalizeLowercaseStringOrEmpty } from "klaw/plugin-sdk/string-coerce-runtime";
36
- import {
37
- inspectFeishuCredentials,
38
- listEnabledFeishuAccounts,
39
- listFeishuAccountIds,
40
- resolveDefaultFeishuAccountId,
41
- resolveFeishuAccount,
42
- resolveFeishuRuntimeAccount,
43
- } from "./accounts.js";
44
- import { feishuApprovalAuth } from "./approval-auth.js";
45
- import { FEISHU_CARD_INTERACTION_VERSION } from "./card-interaction.js";
46
- import type {
47
- ChannelMessageActionName,
48
- ChannelMeta,
49
- ChannelPlugin,
50
- ClawdbotConfig,
51
- } from "./channel-runtime-api.js";
52
- import {
53
- buildChannelConfigSchema,
54
- buildProbeChannelStatusSummary,
55
- chunkTextForOutbound,
56
- createActionGate,
57
- createDefaultChannelRuntimeState,
58
- DEFAULT_ACCOUNT_ID,
59
- PAIRING_APPROVED_MESSAGE,
60
- } from "./channel-runtime-api.js";
61
- import { isRecord } from "./comment-shared.js";
62
- import { FeishuConfigSchema } from "./config-schema.js";
63
- import {
64
- buildFeishuConversationId,
65
- buildFeishuModelOverrideParentCandidates,
66
- parseFeishuConversationId,
67
- parseFeishuDirectConversationId,
68
- parseFeishuTargetId,
69
- } from "./conversation-id.js";
70
- import { listFeishuDirectoryGroups, listFeishuDirectoryPeers } from "./directory.static.js";
71
- import { messageActionTargetAliases } from "./message-action-contract.js";
72
- import { resolveFeishuGroupToolPolicy } from "./policy.js";
73
- import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
74
- import { collectFeishuSecurityAuditFindings } from "./security-audit.js";
75
- import { createFeishuSendReceipt } from "./send-result.js";
76
- import { resolveFeishuSessionConversation } from "./session-conversation.js";
77
- import { resolveFeishuOutboundSessionRoute } from "./session-route.js";
78
- import { feishuSetupAdapter } from "./setup-core.js";
79
- import { feishuSetupWizard, runFeishuLogin } from "./setup-surface.js";
80
- import { looksLikeFeishuId, normalizeFeishuTarget } from "./targets.js";
81
- import type { FeishuConfig, FeishuProbeResult, ResolvedFeishuAccount } from "./types.js";
82
-
83
- function readFeishuMediaParam(params: Record<string, unknown>): string | undefined {
84
- const media = params.media;
85
- if (typeof media !== "string") {
86
- return undefined;
87
- }
88
- return media.trim() ? media : undefined;
89
- }
90
-
91
- function readBooleanParam(params: Record<string, unknown>, keys: string[]): boolean | undefined {
92
- for (const key of keys) {
93
- const value = params[key];
94
- if (typeof value === "boolean") {
95
- return value;
96
- }
97
- }
98
- return undefined;
99
- }
100
-
101
- function hasLegacyFeishuCardCommandValue(actionValue: unknown): boolean {
102
- return (
103
- isRecord(actionValue) &&
104
- actionValue.oc !== FEISHU_CARD_INTERACTION_VERSION &&
105
- (Boolean(typeof actionValue.command === "string" && actionValue.command.trim()) ||
106
- Boolean(typeof actionValue.text === "string" && actionValue.text.trim()))
107
- );
108
- }
109
-
110
- function containsLegacyFeishuCardCommandValue(node: unknown): boolean {
111
- if (Array.isArray(node)) {
112
- return node.some((item) => containsLegacyFeishuCardCommandValue(item));
113
- }
114
- if (!isRecord(node)) {
115
- return false;
116
- }
117
-
118
- if (node.tag === "button" && hasLegacyFeishuCardCommandValue(node.value)) {
119
- return true;
120
- }
121
-
122
- return Object.values(node).some((value) => containsLegacyFeishuCardCommandValue(value));
123
- }
124
-
125
- const meta: ChannelMeta = {
126
- id: "feishu",
127
- label: "Feishu",
128
- selectionLabel: "Feishu/Lark (飞书)",
129
- docsPath: "/channels/feishu",
130
- docsLabel: "feishu",
131
- blurb: "飞书/Lark enterprise messaging.",
132
- aliases: ["lark"],
133
- order: 70,
134
- };
135
-
136
- const loadFeishuChannelRuntime = createLazyRuntimeNamedExport(
137
- () => import("./channel.runtime.js"),
138
- "feishuChannelRuntime",
139
- );
140
-
141
- function toFeishuMessageSendResult(
142
- result: { messageId?: string; chatId?: string; receipt?: ChannelMessageSendResult["receipt"] },
143
- kind: MessageReceiptPartKind,
144
- ): ChannelMessageSendResult {
145
- const receipt =
146
- result.receipt ??
147
- createFeishuSendReceipt({
148
- messageId: result.messageId,
149
- chatId: result.chatId ?? "",
150
- kind,
151
- });
152
- return {
153
- messageId: result.messageId || receipt.primaryPlatformMessageId,
154
- receipt,
155
- };
156
- }
157
-
158
- const feishuMessageAdapter = defineChannelMessageAdapter({
159
- id: "feishu",
160
- durableFinal: {
161
- capabilities: {
162
- text: true,
163
- media: true,
164
- },
165
- },
166
- send: {
167
- text: async (ctx) => {
168
- const runtime = await loadFeishuChannelRuntime();
169
- const sendText = runtime.feishuOutbound.sendText;
170
- if (!sendText) {
171
- throw new Error("Feishu text sending is not available.");
172
- }
173
- return toFeishuMessageSendResult(await sendText(ctx), "text");
174
- },
175
- media: async (ctx) => {
176
- const runtime = await loadFeishuChannelRuntime();
177
- const sendMedia = runtime.feishuOutbound.sendMedia;
178
- if (!sendMedia) {
179
- throw new Error("Feishu media sending is not available.");
180
- }
181
- return toFeishuMessageSendResult(await sendMedia(ctx), "media");
182
- },
183
- },
184
- });
185
-
186
- function buildFeishuPresentationCard(params: {
187
- presentation: NonNullable<ReturnType<typeof normalizeMessagePresentation>>;
188
- fallbackText?: string;
189
- }): Record<string, unknown> {
190
- const fallbackPresentation: NonNullable<ReturnType<typeof normalizeMessagePresentation>> = {
191
- ...(params.presentation.tone ? { tone: params.presentation.tone } : {}),
192
- blocks: params.presentation.blocks,
193
- };
194
- return {
195
- schema: "2.0",
196
- config: {
197
- width_mode: "fill",
198
- },
199
- ...(params.presentation.title
200
- ? {
201
- header: {
202
- title: { tag: "plain_text", content: params.presentation.title },
203
- template: "blue",
204
- },
205
- }
206
- : {}),
207
- body: {
208
- elements: [
209
- {
210
- tag: "markdown",
211
- content: renderMessagePresentationFallbackText({
212
- text: params.fallbackText,
213
- presentation: fallbackPresentation,
214
- }),
215
- },
216
- ],
217
- },
218
- };
219
- }
220
-
221
- async function createFeishuActionClient(account: ResolvedFeishuAccount) {
222
- const { createFeishuClient } = await import("./client.js");
223
- return createFeishuClient(account);
224
- }
225
-
226
- const collectFeishuSecurityWarnings = createAllowlistProviderGroupPolicyWarningCollector<{
227
- cfg: ClawdbotConfig;
228
- accountId?: string | null;
229
- }>({
230
- providerConfigPresent: (cfg) => cfg.channels?.feishu !== undefined,
231
- resolveGroupPolicy: ({ cfg, accountId }) =>
232
- resolveFeishuAccount({ cfg, accountId }).config?.groupPolicy,
233
- collect: ({ cfg, accountId, groupPolicy }) => {
234
- if (groupPolicy !== "open") {
235
- return [];
236
- }
237
- const account = resolveFeishuAccount({ cfg, accountId });
238
- return [
239
- `- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
240
- ];
241
- },
242
- });
243
-
244
- function describeFeishuMessageTool({
245
- cfg,
246
- accountId,
247
- }: Parameters<
248
- NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>
249
- >[0]): ChannelMessageToolDiscovery {
250
- const enabledAccounts = accountId
251
- ? [resolveFeishuAccount({ cfg, accountId })].filter(
252
- (account) => account.enabled && account.configured,
253
- )
254
- : listEnabledFeishuAccounts(cfg);
255
- const enabled =
256
- enabledAccounts.length > 0 ||
257
- (!accountId &&
258
- cfg.channels?.feishu?.enabled !== false &&
259
- Boolean(inspectFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)));
260
- if (enabledAccounts.length === 0) {
261
- return {
262
- actions: [],
263
- capabilities: enabled ? ["presentation"] : [],
264
- };
265
- }
266
- const actions = new Set<ChannelMessageActionName>([
267
- "send",
268
- "read",
269
- "edit",
270
- "thread-reply",
271
- "pin",
272
- "list-pins",
273
- "unpin",
274
- "member-info",
275
- "channel-info",
276
- "channel-list",
277
- ]);
278
- if (
279
- accountId
280
- ? enabledAccounts.some((account) => isFeishuReactionsActionEnabled({ cfg, account }))
281
- : areAnyFeishuReactionActionsEnabled(cfg)
282
- ) {
283
- actions.add("react");
284
- actions.add("reactions");
285
- }
286
- return {
287
- actions: Array.from(actions),
288
- capabilities: enabled ? ["presentation"] : [],
289
- };
290
- }
291
-
292
- function setFeishuNamedAccountEnabled(
293
- cfg: ClawdbotConfig,
294
- accountId: string,
295
- enabled: boolean,
296
- ): ClawdbotConfig {
297
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
298
- return {
299
- ...cfg,
300
- channels: {
301
- ...cfg.channels,
302
- feishu: {
303
- ...feishuCfg,
304
- accounts: {
305
- ...feishuCfg?.accounts,
306
- [accountId]: {
307
- ...feishuCfg?.accounts?.[accountId],
308
- enabled,
309
- },
310
- },
311
- },
312
- },
313
- };
314
- }
315
-
316
- const feishuConfigAdapter = createHybridChannelConfigAdapter<
317
- ResolvedFeishuAccount,
318
- ResolvedFeishuAccount
319
- >({
320
- sectionKey: "feishu",
321
- listAccountIds: listFeishuAccountIds,
322
- resolveAccount: adaptScopedAccountAccessor(resolveFeishuAccount),
323
- defaultAccountId: resolveDefaultFeishuAccountId,
324
- clearBaseFields: [],
325
- resolveAllowFrom: (account) => account.config.allowFrom,
326
- formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
327
- });
328
-
329
- function isFeishuReactionsActionEnabled(params: {
330
- cfg: ClawdbotConfig;
331
- account: ResolvedFeishuAccount;
332
- }): boolean {
333
- if (!params.account.enabled || !params.account.configured) {
334
- return false;
335
- }
336
- const gate = createActionGate(
337
- (params.account.config.actions ??
338
- (params.cfg.channels?.feishu as { actions?: unknown } | undefined)?.actions) as Record<
339
- string,
340
- boolean | undefined
341
- >,
342
- );
343
- return gate("reactions");
344
- }
345
-
346
- function areAnyFeishuReactionActionsEnabled(cfg: ClawdbotConfig): boolean {
347
- for (const account of listEnabledFeishuAccounts(cfg)) {
348
- if (isFeishuReactionsActionEnabled({ cfg, account })) {
349
- return true;
350
- }
351
- }
352
- return false;
353
- }
354
-
355
- function isFeishuGroupTopicSessionKey(sessionKey: string | null | undefined): boolean {
356
- if (typeof sessionKey !== "string" || !sessionKey) {
357
- return false;
358
- }
359
- const parsed = parseFeishuConversationId({ conversationId: sessionKey });
360
- return parsed?.scope === "group_topic" || parsed?.scope === "group_topic_sender";
361
- }
362
-
363
- type FeishuActionReplyAnchor = {
364
- replyToMessageId: string | undefined;
365
- replyInThread: boolean;
366
- };
367
-
368
- type FeishuSendActionContext = Pick<
369
- ChannelMessageActionContext,
370
- "action" | "params" | "sessionKey" | "toolContext"
371
- >;
372
-
373
- function resolveFeishuTopicAutoThreadAnchor(ctx: FeishuSendActionContext): string | undefined {
374
- if (ctx.action !== "send") {
375
- return undefined;
376
- }
377
- if (!isFeishuGroupTopicSessionKey(ctx.sessionKey)) {
378
- return undefined;
379
- }
380
- const inbound = ctx.toolContext?.currentMessageId;
381
- return typeof inbound === "string" && inbound.length > 0 ? inbound : undefined;
382
- }
383
-
384
- function buildFeishuSendReplyAnchor(ctx: FeishuSendActionContext): FeishuActionReplyAnchor {
385
- if (ctx.action === "thread-reply") {
386
- return {
387
- replyToMessageId: resolveFeishuMessageId(ctx.params),
388
- replyInThread: true,
389
- };
390
- }
391
- const autoThreadId = resolveFeishuTopicAutoThreadAnchor(ctx);
392
- return {
393
- replyToMessageId: autoThreadId,
394
- replyInThread: autoThreadId !== undefined,
395
- };
396
- }
397
-
398
- function isSupportedFeishuDirectConversationId(conversationId: string): boolean {
399
- const trimmed = conversationId.trim();
400
- if (!trimmed || trimmed.includes(":")) {
401
- return false;
402
- }
403
- if (trimmed.startsWith("oc_") || trimmed.startsWith("on_")) {
404
- return false;
405
- }
406
- return true;
407
- }
408
-
409
- function normalizeFeishuAcpConversationId(conversationId: string) {
410
- const parsed = parseFeishuConversationId({ conversationId });
411
- if (
412
- !parsed ||
413
- (parsed.scope !== "group_topic" &&
414
- parsed.scope !== "group_topic_sender" &&
415
- !isSupportedFeishuDirectConversationId(parsed.canonicalConversationId))
416
- ) {
417
- return null;
418
- }
419
- return {
420
- conversationId: parsed.canonicalConversationId,
421
- parentConversationId:
422
- parsed.scope === "group_topic" || parsed.scope === "group_topic_sender"
423
- ? parsed.chatId
424
- : undefined,
425
- };
426
- }
427
-
428
- function matchFeishuAcpConversation(params: {
429
- bindingConversationId: string;
430
- conversationId: string;
431
- parentConversationId?: string;
432
- }) {
433
- const binding = normalizeFeishuAcpConversationId(params.bindingConversationId);
434
- if (!binding) {
435
- return null;
436
- }
437
- const incoming = parseFeishuConversationId({
438
- conversationId: params.conversationId,
439
- parentConversationId: params.parentConversationId,
440
- });
441
- if (
442
- !incoming ||
443
- (incoming.scope !== "group_topic" &&
444
- incoming.scope !== "group_topic_sender" &&
445
- !isSupportedFeishuDirectConversationId(incoming.canonicalConversationId))
446
- ) {
447
- return null;
448
- }
449
- const matchesCanonicalConversation = binding.conversationId === incoming.canonicalConversationId;
450
- const matchesParentTopicForSenderScopedConversation =
451
- incoming.scope === "group_topic_sender" &&
452
- binding.parentConversationId === incoming.chatId &&
453
- binding.conversationId === `${incoming.chatId}:topic:${incoming.topicId}`;
454
- if (!matchesCanonicalConversation && !matchesParentTopicForSenderScopedConversation) {
455
- return null;
456
- }
457
- return {
458
- conversationId: matchesParentTopicForSenderScopedConversation
459
- ? binding.conversationId
460
- : incoming.canonicalConversationId,
461
- parentConversationId:
462
- incoming.scope === "group_topic" || incoming.scope === "group_topic_sender"
463
- ? incoming.chatId
464
- : undefined,
465
- matchPriority: matchesCanonicalConversation ? 2 : 1,
466
- };
467
- }
468
-
469
- function resolveFeishuSenderScopedCommandConversation(params: {
470
- accountId: string;
471
- parentConversationId?: string;
472
- threadId?: string;
473
- senderId?: string;
474
- sessionKey?: string;
475
- parentSessionKey?: string;
476
- }): string | undefined {
477
- const parentConversationId = params.parentConversationId?.trim();
478
- const threadId = params.threadId?.trim();
479
- const senderId = params.senderId?.trim();
480
- if (!parentConversationId || !threadId || !senderId) {
481
- return undefined;
482
- }
483
- const expectedScopePrefix = `feishu:group:${normalizeLowercaseStringOrEmpty(parentConversationId)}:topic:${normalizeLowercaseStringOrEmpty(threadId)}:sender:`;
484
- const isSenderScopedSession = [params.sessionKey, params.parentSessionKey].some((candidate) => {
485
- const normalized = normalizeLowercaseStringOrEmpty(candidate ?? "");
486
- if (!normalized) {
487
- return false;
488
- }
489
- const scopedRest = normalized.replace(/^agent:[^:]+:/, "");
490
- return scopedRest.startsWith(expectedScopePrefix);
491
- });
492
- const senderScopedConversationId = buildFeishuConversationId({
493
- chatId: parentConversationId,
494
- scope: "group_topic_sender",
495
- topicId: threadId,
496
- senderOpenId: senderId,
497
- });
498
- if (isSenderScopedSession) {
499
- return senderScopedConversationId;
500
- }
501
- if (!params.sessionKey?.trim()) {
502
- return undefined;
503
- }
504
- const boundConversation = getSessionBindingService()
505
- .listBySession(params.sessionKey)
506
- .find((binding) => {
507
- if (
508
- binding.conversation.channel !== "feishu" ||
509
- binding.conversation.accountId !== params.accountId
510
- ) {
511
- return false;
512
- }
513
- return binding.conversation.conversationId === senderScopedConversationId;
514
- });
515
- return boundConversation?.conversation.conversationId;
516
- }
517
-
518
- function resolveFeishuCommandConversation(params: {
519
- accountId: string;
520
- threadId?: string;
521
- senderId?: string;
522
- sessionKey?: string;
523
- parentSessionKey?: string;
524
- originatingTo?: string;
525
- commandTo?: string;
526
- fallbackTo?: string;
527
- }) {
528
- if (params.threadId) {
529
- const parentConversationId =
530
- parseFeishuTargetId(params.originatingTo) ??
531
- parseFeishuTargetId(params.commandTo) ??
532
- parseFeishuTargetId(params.fallbackTo);
533
- if (!parentConversationId) {
534
- return null;
535
- }
536
- const senderScopedConversationId = resolveFeishuSenderScopedCommandConversation({
537
- accountId: params.accountId,
538
- parentConversationId,
539
- threadId: params.threadId,
540
- senderId: params.senderId,
541
- sessionKey: params.sessionKey,
542
- parentSessionKey: params.parentSessionKey,
543
- });
544
- return {
545
- conversationId:
546
- senderScopedConversationId ??
547
- buildFeishuConversationId({
548
- chatId: parentConversationId,
549
- scope: "group_topic",
550
- topicId: params.threadId,
551
- }),
552
- parentConversationId,
553
- };
554
- }
555
- const conversationId =
556
- parseFeishuDirectConversationId(params.originatingTo) ??
557
- parseFeishuDirectConversationId(params.commandTo) ??
558
- parseFeishuDirectConversationId(params.fallbackTo);
559
- return conversationId ? { conversationId } : null;
560
- }
561
-
562
- function jsonActionResult(details: Record<string, unknown>) {
563
- return {
564
- content: [{ type: "text" as const, text: JSON.stringify(details) }],
565
- details,
566
- };
567
- }
568
-
569
- function readFirstString(
570
- params: Record<string, unknown>,
571
- keys: string[],
572
- fallback?: string | null,
573
- ): string | undefined {
574
- for (const key of keys) {
575
- const value = params[key];
576
- if (typeof value === "string" && value.trim()) {
577
- return value.trim();
578
- }
579
- }
580
- if (typeof fallback === "string" && fallback.trim()) {
581
- return fallback.trim();
582
- }
583
- return undefined;
584
- }
585
-
586
- function readOptionalNumber(params: Record<string, unknown>, keys: string[]): number | undefined {
587
- for (const key of keys) {
588
- const value = params[key];
589
- if (typeof value === "number" && Number.isFinite(value)) {
590
- return value;
591
- }
592
- if (typeof value === "string" && value.trim()) {
593
- const parsed = Number(value);
594
- if (Number.isFinite(parsed)) {
595
- return parsed;
596
- }
597
- }
598
- }
599
- return undefined;
600
- }
601
-
602
- function resolveFeishuActionTarget(ctx: {
603
- params: Record<string, unknown>;
604
- toolContext?: { currentChannelId?: string } | null;
605
- }): string | undefined {
606
- return readFirstString(ctx.params, ["to", "target"], ctx.toolContext?.currentChannelId);
607
- }
608
-
609
- function resolveFeishuChatId(ctx: {
610
- params: Record<string, unknown>;
611
- toolContext?: { currentChannelId?: string } | null;
612
- }): string | undefined {
613
- const raw = readFirstString(
614
- ctx.params,
615
- ["chatId", "chat_id", "channelId", "channel_id", "to", "target"],
616
- ctx.toolContext?.currentChannelId,
617
- );
618
- if (!raw) {
619
- return undefined;
620
- }
621
- if (/^(user|dm|open_id):/i.test(raw)) {
622
- return undefined;
623
- }
624
- if (/^(chat|group|channel):/i.test(raw)) {
625
- return normalizeFeishuTarget(raw) ?? undefined;
626
- }
627
- return raw;
628
- }
629
-
630
- function resolveFeishuMessageId(params: Record<string, unknown>): string | undefined {
631
- return readFirstString(params, ["messageId", "message_id", "replyTo", "reply_to"]);
632
- }
633
-
634
- function resolveFeishuMemberId(params: Record<string, unknown>): string | undefined {
635
- return readFirstString(params, [
636
- "memberId",
637
- "member_id",
638
- "userId",
639
- "user_id",
640
- "openId",
641
- "open_id",
642
- "unionId",
643
- "union_id",
644
- ]);
645
- }
646
-
647
- function resolveFeishuMemberIdType(
648
- params: Record<string, unknown>,
649
- ): "open_id" | "user_id" | "union_id" {
650
- const raw = readFirstString(params, [
651
- "memberIdType",
652
- "member_id_type",
653
- "userIdType",
654
- "user_id_type",
655
- ]);
656
- if (raw === "open_id" || raw === "user_id" || raw === "union_id") {
657
- return raw;
658
- }
659
- if (
660
- readFirstString(params, ["userId", "user_id"]) &&
661
- !readFirstString(params, ["openId", "open_id", "unionId", "union_id"])
662
- ) {
663
- return "user_id";
664
- }
665
- if (
666
- readFirstString(params, ["unionId", "union_id"]) &&
667
- !readFirstString(params, ["openId", "open_id"])
668
- ) {
669
- return "union_id";
670
- }
671
- return "open_id";
672
- }
673
-
674
- export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResult> =
675
- createChatChannelPlugin({
676
- base: {
677
- id: "feishu",
678
- meta: {
679
- ...meta,
680
- },
681
- capabilities: {
682
- chatTypes: ["direct", "channel"],
683
- polls: false,
684
- threads: true,
685
- media: true,
686
- tts: {
687
- voice: {
688
- synthesisTarget: "voice-note",
689
- transcodesAudio: true,
690
- },
691
- },
692
- reactions: true,
693
- edit: true,
694
- reply: true,
695
- },
696
- agentPrompt: {
697
- messageToolHints: () => [
698
- "- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
699
- "- Feishu supports interactive cards plus native image, file, audio, and video/media delivery.",
700
- "- Feishu supports `send`, `read`, `edit`, `thread-reply`, pins, and channel/member lookup, plus reactions when enabled.",
701
- ],
702
- },
703
- groups: {
704
- resolveToolPolicy: resolveFeishuGroupToolPolicy,
705
- },
706
- conversationBindings: {
707
- defaultTopLevelPlacement: "current",
708
- buildModelOverrideParentCandidates: ({ parentConversationId }) =>
709
- buildFeishuModelOverrideParentCandidates(parentConversationId),
710
- },
711
- mentions: {
712
- stripPatterns: () => ['<at user_id="[^"]*">[^<]*</at>'],
713
- },
714
- reload: { configPrefixes: ["channels.feishu"] },
715
- configSchema: buildChannelConfigSchema(FeishuConfigSchema),
716
- config: {
717
- ...feishuConfigAdapter,
718
- setAccountEnabled: ({ cfg, accountId, enabled }) => {
719
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
720
- if (isDefault) {
721
- return {
722
- ...cfg,
723
- channels: {
724
- ...cfg.channels,
725
- feishu: {
726
- ...cfg.channels?.feishu,
727
- enabled,
728
- },
729
- },
730
- };
731
- }
732
- return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
733
- },
734
- deleteAccount: ({ cfg, accountId }) => {
735
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
736
-
737
- if (isDefault) {
738
- // Delete entire feishu config
739
- const next = { ...cfg } as ClawdbotConfig;
740
- const nextChannels = { ...cfg.channels };
741
- delete (nextChannels as Record<string, unknown>).feishu;
742
- if (Object.keys(nextChannels).length > 0) {
743
- next.channels = nextChannels;
744
- } else {
745
- delete next.channels;
746
- }
747
- return next;
748
- }
749
-
750
- // Delete specific account from accounts
751
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
752
- const accounts = { ...feishuCfg?.accounts };
753
- delete accounts[accountId];
754
-
755
- return {
756
- ...cfg,
757
- channels: {
758
- ...cfg.channels,
759
- feishu: {
760
- ...feishuCfg,
761
- accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
762
- },
763
- },
764
- };
765
- },
766
- isConfigured: (account) => account.configured,
767
- describeAccount: (account) =>
768
- describeAccountSnapshot({
769
- account,
770
- configured: account.configured,
771
- extra: {
772
- appId: account.appId,
773
- domain: account.domain,
774
- },
775
- }),
776
- },
777
- approvalCapability: feishuApprovalAuth,
778
- secrets: {
779
- secretTargetRegistryEntries,
780
- collectRuntimeConfigAssignments,
781
- },
782
- actions: {
783
- messageActionTargetAliases,
784
- describeMessageTool: describeFeishuMessageTool,
785
- handleAction: async (ctx) => {
786
- const account = resolveFeishuAccount({
787
- cfg: ctx.cfg,
788
- accountId: ctx.accountId ?? undefined,
789
- });
790
- if (
791
- (ctx.action === "react" || ctx.action === "reactions") &&
792
- !isFeishuReactionsActionEnabled({ cfg: ctx.cfg, account })
793
- ) {
794
- throw new Error("Feishu reactions are disabled via actions.reactions.");
795
- }
796
- if (ctx.action === "send" || ctx.action === "thread-reply") {
797
- const to = resolveFeishuActionTarget(ctx);
798
- if (!to) {
799
- throw new Error(`Feishu ${ctx.action} requires a target (to).`);
800
- }
801
- const { replyToMessageId, replyInThread } = buildFeishuSendReplyAnchor(ctx);
802
- if (ctx.action === "thread-reply" && !replyToMessageId) {
803
- throw new Error("Feishu thread-reply requires messageId.");
804
- }
805
- const presentation = normalizeMessagePresentation(ctx.params.presentation);
806
- const text = readFirstString(ctx.params, ["text", "message"]);
807
- const mediaUrl = readFeishuMediaParam(ctx.params);
808
- const audioAsVoice = readBooleanParam(ctx.params, ["asVoice", "audioAsVoice"]);
809
- const card = presentation
810
- ? buildFeishuPresentationCard({ presentation, fallbackText: text })
811
- : undefined;
812
- if (card && mediaUrl) {
813
- throw new Error(`Feishu ${ctx.action} does not support card with media.`);
814
- }
815
- if (!card && !text && !mediaUrl) {
816
- throw new Error(`Feishu ${ctx.action} requires text/message, media, or card.`);
817
- }
818
- const runtime = await loadFeishuChannelRuntime();
819
- const maybeSendMedia = runtime.feishuOutbound.sendMedia;
820
- if (mediaUrl && !maybeSendMedia) {
821
- throw new Error("Feishu media sending is not available.");
822
- }
823
- const sendMedia = maybeSendMedia;
824
- let result;
825
- if (card) {
826
- if (containsLegacyFeishuCardCommandValue(card)) {
827
- throw new Error(
828
- "Feishu card buttons that trigger text or commands must use structured interaction envelopes.",
829
- );
830
- }
831
- result = await runtime.sendCardFeishu({
832
- cfg: ctx.cfg,
833
- to,
834
- card,
835
- accountId: ctx.accountId ?? undefined,
836
- replyToMessageId,
837
- replyInThread,
838
- });
839
- } else if (mediaUrl) {
840
- result = await sendMedia!({
841
- cfg: ctx.cfg,
842
- to,
843
- text: text ?? "",
844
- mediaUrl,
845
- accountId: ctx.accountId ?? undefined,
846
- mediaLocalRoots: ctx.mediaLocalRoots,
847
- ...(replyInThread
848
- ? { threadId: replyToMessageId }
849
- : { replyToId: replyToMessageId }),
850
- ...(audioAsVoice === true ? { audioAsVoice: true } : {}),
851
- });
852
- } else {
853
- result = await runtime.sendMessageFeishu({
854
- cfg: ctx.cfg,
855
- to,
856
- text: text!,
857
- accountId: ctx.accountId ?? undefined,
858
- replyToMessageId,
859
- replyInThread,
860
- });
861
- }
862
- return jsonActionResult({
863
- ok: true,
864
- channel: "feishu",
865
- action: ctx.action,
866
- ...result,
867
- });
868
- }
869
-
870
- if (ctx.action === "read") {
871
- const messageId = resolveFeishuMessageId(ctx.params);
872
- if (!messageId) {
873
- throw new Error("Feishu read requires messageId.");
874
- }
875
- const { getMessageFeishu } = await loadFeishuChannelRuntime();
876
- const message = await getMessageFeishu({
877
- cfg: ctx.cfg,
878
- messageId,
879
- accountId: ctx.accountId ?? undefined,
880
- });
881
- if (!message) {
882
- return {
883
- isError: true,
884
- content: [
885
- {
886
- type: "text" as const,
887
- text: JSON.stringify({
888
- error: `Feishu read failed or message not found: ${messageId}`,
889
- }),
890
- },
891
- ],
892
- details: { error: `Feishu read failed or message not found: ${messageId}` },
893
- };
894
- }
895
- return jsonActionResult({ ok: true, channel: "feishu", action: "read", message });
896
- }
897
-
898
- if (ctx.action === "edit") {
899
- const messageId = resolveFeishuMessageId(ctx.params);
900
- if (!messageId) {
901
- throw new Error("Feishu edit requires messageId.");
902
- }
903
- const text = readFirstString(ctx.params, ["text", "message"]);
904
- const card =
905
- ctx.params.card && typeof ctx.params.card === "object"
906
- ? (ctx.params.card as Record<string, unknown>)
907
- : undefined;
908
- const { editMessageFeishu } = await loadFeishuChannelRuntime();
909
- const result = await editMessageFeishu({
910
- cfg: ctx.cfg,
911
- messageId,
912
- text,
913
- card,
914
- accountId: ctx.accountId ?? undefined,
915
- });
916
- return jsonActionResult({
917
- ok: true,
918
- channel: "feishu",
919
- action: "edit",
920
- ...result,
921
- });
922
- }
923
-
924
- if (ctx.action === "pin") {
925
- const messageId = resolveFeishuMessageId(ctx.params);
926
- if (!messageId) {
927
- throw new Error("Feishu pin requires messageId.");
928
- }
929
- const { createPinFeishu } = await loadFeishuChannelRuntime();
930
- const pin = await createPinFeishu({
931
- cfg: ctx.cfg,
932
- messageId,
933
- accountId: ctx.accountId ?? undefined,
934
- });
935
- return jsonActionResult({ ok: true, channel: "feishu", action: "pin", pin });
936
- }
937
-
938
- if (ctx.action === "unpin") {
939
- const messageId = resolveFeishuMessageId(ctx.params);
940
- if (!messageId) {
941
- throw new Error("Feishu unpin requires messageId.");
942
- }
943
- const { removePinFeishu } = await loadFeishuChannelRuntime();
944
- await removePinFeishu({
945
- cfg: ctx.cfg,
946
- messageId,
947
- accountId: ctx.accountId ?? undefined,
948
- });
949
- return jsonActionResult({
950
- ok: true,
951
- channel: "feishu",
952
- action: "unpin",
953
- messageId,
954
- });
955
- }
956
-
957
- if (ctx.action === "list-pins") {
958
- const chatId = resolveFeishuChatId(ctx);
959
- if (!chatId) {
960
- throw new Error("Feishu list-pins requires chatId or channelId.");
961
- }
962
- const { listPinsFeishu } = await loadFeishuChannelRuntime();
963
- const result = await listPinsFeishu({
964
- cfg: ctx.cfg,
965
- chatId,
966
- startTime: readFirstString(ctx.params, ["startTime", "start_time"]),
967
- endTime: readFirstString(ctx.params, ["endTime", "end_time"]),
968
- pageSize: readOptionalNumber(ctx.params, ["pageSize", "page_size"]),
969
- pageToken: readFirstString(ctx.params, ["pageToken", "page_token"]),
970
- accountId: ctx.accountId ?? undefined,
971
- });
972
- return jsonActionResult({
973
- ok: true,
974
- channel: "feishu",
975
- action: "list-pins",
976
- ...result,
977
- });
978
- }
979
-
980
- if (ctx.action === "channel-info") {
981
- const chatId = resolveFeishuChatId(ctx);
982
- if (!chatId) {
983
- throw new Error("Feishu channel-info requires chatId or channelId.");
984
- }
985
- const runtime = await loadFeishuChannelRuntime();
986
- const client = await createFeishuActionClient(account);
987
- const channel = await runtime.getChatInfo(client, chatId);
988
- const includeMembers =
989
- ctx.params.includeMembers === true || ctx.params.members === true;
990
- if (!includeMembers) {
991
- return jsonActionResult({
992
- ok: true,
993
- provider: "feishu",
994
- action: "channel-info",
995
- channel,
996
- });
997
- }
998
- const members = await runtime.getChatMembers(
999
- client,
1000
- chatId,
1001
- readOptionalNumber(ctx.params, ["pageSize", "page_size"]),
1002
- readFirstString(ctx.params, ["pageToken", "page_token"]),
1003
- resolveFeishuMemberIdType(ctx.params),
1004
- );
1005
- return jsonActionResult({
1006
- ok: true,
1007
- provider: "feishu",
1008
- action: "channel-info",
1009
- channel,
1010
- members,
1011
- });
1012
- }
1013
-
1014
- if (ctx.action === "member-info") {
1015
- const runtime = await loadFeishuChannelRuntime();
1016
- const client = await createFeishuActionClient(account);
1017
- const memberId = resolveFeishuMemberId(ctx.params);
1018
- if (memberId) {
1019
- const member = await runtime.getFeishuMemberInfo(
1020
- client,
1021
- memberId,
1022
- resolveFeishuMemberIdType(ctx.params),
1023
- );
1024
- return jsonActionResult({
1025
- ok: true,
1026
- channel: "feishu",
1027
- action: "member-info",
1028
- member,
1029
- });
1030
- }
1031
- const chatId = resolveFeishuChatId(ctx);
1032
- if (!chatId) {
1033
- throw new Error("Feishu member-info requires memberId or chatId/channelId.");
1034
- }
1035
- const members = await runtime.getChatMembers(
1036
- client,
1037
- chatId,
1038
- readOptionalNumber(ctx.params, ["pageSize", "page_size"]),
1039
- readFirstString(ctx.params, ["pageToken", "page_token"]),
1040
- resolveFeishuMemberIdType(ctx.params),
1041
- );
1042
- return jsonActionResult({
1043
- ok: true,
1044
- channel: "feishu",
1045
- action: "member-info",
1046
- ...members,
1047
- });
1048
- }
1049
-
1050
- if (ctx.action === "channel-list") {
1051
- const runtime = await loadFeishuChannelRuntime();
1052
- const query = readFirstString(ctx.params, ["query"]);
1053
- const limit = readOptionalNumber(ctx.params, ["limit"]);
1054
- const scope = readFirstString(ctx.params, ["scope", "kind"]) ?? "all";
1055
- if (
1056
- scope === "groups" ||
1057
- scope === "group" ||
1058
- scope === "channels" ||
1059
- scope === "channel"
1060
- ) {
1061
- const groups = await runtime.listFeishuDirectoryGroupsLive({
1062
- cfg: ctx.cfg,
1063
- query,
1064
- limit,
1065
- fallbackToStatic: false,
1066
- accountId: ctx.accountId ?? undefined,
1067
- });
1068
- return jsonActionResult({
1069
- ok: true,
1070
- channel: "feishu",
1071
- action: "channel-list",
1072
- groups,
1073
- });
1074
- }
1075
- if (
1076
- scope === "peers" ||
1077
- scope === "peer" ||
1078
- scope === "members" ||
1079
- scope === "member" ||
1080
- scope === "users" ||
1081
- scope === "user"
1082
- ) {
1083
- const peers = await runtime.listFeishuDirectoryPeersLive({
1084
- cfg: ctx.cfg,
1085
- query,
1086
- limit,
1087
- fallbackToStatic: false,
1088
- accountId: ctx.accountId ?? undefined,
1089
- });
1090
- return jsonActionResult({
1091
- ok: true,
1092
- channel: "feishu",
1093
- action: "channel-list",
1094
- peers,
1095
- });
1096
- }
1097
- const [groups, peers] = await Promise.all([
1098
- runtime.listFeishuDirectoryGroupsLive({
1099
- cfg: ctx.cfg,
1100
- query,
1101
- limit,
1102
- fallbackToStatic: false,
1103
- accountId: ctx.accountId ?? undefined,
1104
- }),
1105
- runtime.listFeishuDirectoryPeersLive({
1106
- cfg: ctx.cfg,
1107
- query,
1108
- limit,
1109
- fallbackToStatic: false,
1110
- accountId: ctx.accountId ?? undefined,
1111
- }),
1112
- ]);
1113
- return jsonActionResult({
1114
- ok: true,
1115
- channel: "feishu",
1116
- action: "channel-list",
1117
- groups,
1118
- peers,
1119
- });
1120
- }
1121
-
1122
- if (ctx.action === "react") {
1123
- const messageId = resolveFeishuMessageId(ctx.params);
1124
- if (!messageId) {
1125
- throw new Error("Feishu reaction requires messageId.");
1126
- }
1127
- const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : "";
1128
- const remove = ctx.params.remove === true;
1129
- const clearAll = ctx.params.clearAll === true;
1130
- if (remove) {
1131
- if (!emoji) {
1132
- throw new Error("Emoji is required to remove a Feishu reaction.");
1133
- }
1134
- const { listReactionsFeishu, removeReactionFeishu } =
1135
- await loadFeishuChannelRuntime();
1136
- const matches = await listReactionsFeishu({
1137
- cfg: ctx.cfg,
1138
- messageId,
1139
- emojiType: emoji,
1140
- accountId: ctx.accountId ?? undefined,
1141
- });
1142
- const ownReaction = matches.find((entry) => entry.operatorType === "app");
1143
- if (!ownReaction) {
1144
- return jsonActionResult({ ok: true, removed: null });
1145
- }
1146
- await removeReactionFeishu({
1147
- cfg: ctx.cfg,
1148
- messageId,
1149
- reactionId: ownReaction.reactionId,
1150
- accountId: ctx.accountId ?? undefined,
1151
- });
1152
- return jsonActionResult({ ok: true, removed: emoji });
1153
- }
1154
- if (!emoji) {
1155
- if (!clearAll) {
1156
- throw new Error(
1157
- "Emoji is required to add a Feishu reaction. Set clearAll=true to remove all bot reactions.",
1158
- );
1159
- }
1160
- const { listReactionsFeishu, removeReactionFeishu } =
1161
- await loadFeishuChannelRuntime();
1162
- const reactions = await listReactionsFeishu({
1163
- cfg: ctx.cfg,
1164
- messageId,
1165
- accountId: ctx.accountId ?? undefined,
1166
- });
1167
- let removed = 0;
1168
- for (const reaction of reactions.filter((entry) => entry.operatorType === "app")) {
1169
- await removeReactionFeishu({
1170
- cfg: ctx.cfg,
1171
- messageId,
1172
- reactionId: reaction.reactionId,
1173
- accountId: ctx.accountId ?? undefined,
1174
- });
1175
- removed += 1;
1176
- }
1177
- return jsonActionResult({ ok: true, removed });
1178
- }
1179
- const { addReactionFeishu } = await loadFeishuChannelRuntime();
1180
- await addReactionFeishu({
1181
- cfg: ctx.cfg,
1182
- messageId,
1183
- emojiType: emoji,
1184
- accountId: ctx.accountId ?? undefined,
1185
- });
1186
- return jsonActionResult({ ok: true, added: emoji });
1187
- }
1188
-
1189
- if (ctx.action === "reactions") {
1190
- const messageId = resolveFeishuMessageId(ctx.params);
1191
- if (!messageId) {
1192
- throw new Error("Feishu reactions lookup requires messageId.");
1193
- }
1194
- const { listReactionsFeishu } = await loadFeishuChannelRuntime();
1195
- const reactions = await listReactionsFeishu({
1196
- cfg: ctx.cfg,
1197
- messageId,
1198
- accountId: ctx.accountId ?? undefined,
1199
- });
1200
- return jsonActionResult({ ok: true, reactions });
1201
- }
1202
-
1203
- throw new Error(`Unsupported Feishu action: "${ctx.action}"`);
1204
- },
1205
- },
1206
- bindings: {
1207
- compileConfiguredBinding: ({ conversationId }) =>
1208
- normalizeFeishuAcpConversationId(conversationId),
1209
- matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) =>
1210
- matchFeishuAcpConversation({
1211
- bindingConversationId: compiledBinding.conversationId,
1212
- conversationId,
1213
- parentConversationId,
1214
- }),
1215
- resolveCommandConversation: ({
1216
- accountId,
1217
- threadId,
1218
- senderId,
1219
- sessionKey,
1220
- parentSessionKey,
1221
- originatingTo,
1222
- commandTo,
1223
- fallbackTo,
1224
- }) =>
1225
- resolveFeishuCommandConversation({
1226
- accountId,
1227
- threadId,
1228
- senderId,
1229
- sessionKey,
1230
- parentSessionKey,
1231
- originatingTo,
1232
- commandTo,
1233
- fallbackTo,
1234
- }),
1235
- },
1236
- auth: {
1237
- login: async ({ cfg }) => {
1238
- const { createClackPrompter } = await import("klaw/plugin-sdk/setup-runtime");
1239
- const { replaceConfigFile } = await import("klaw/plugin-sdk/config-mutation");
1240
- const prompter = createClackPrompter();
1241
- const nextCfg = await runFeishuLogin({ cfg, prompter });
1242
- if (nextCfg !== cfg) {
1243
- await replaceConfigFile({
1244
- nextConfig: nextCfg,
1245
- afterWrite: { mode: "auto" },
1246
- });
1247
- }
1248
- },
1249
- },
1250
- setup: feishuSetupAdapter,
1251
- setupWizard: feishuSetupWizard,
1252
- messaging: {
1253
- targetPrefixes: ["feishu", "lark"],
1254
- normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
1255
- resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
1256
- const directId = parseFeishuDirectConversationId(conversationId);
1257
- if (directId) {
1258
- return { to: `user:${directId}` };
1259
- }
1260
- const parsed = parseFeishuConversationId({ conversationId, parentConversationId });
1261
- if (parsed?.topicId) {
1262
- return {
1263
- to: `chat:${parentConversationId?.trim() || parsed.chatId}`,
1264
- threadId: parsed.topicId,
1265
- };
1266
- }
1267
- return { to: `chat:${parsed?.chatId ?? conversationId.trim()}` };
1268
- },
1269
- resolveSessionConversation: ({ kind, rawId }) =>
1270
- resolveFeishuSessionConversation({ kind, rawId }),
1271
- resolveOutboundSessionRoute: (params) => resolveFeishuOutboundSessionRoute(params),
1272
- targetResolver: {
1273
- looksLikeId: looksLikeFeishuId,
1274
- hint: "<chatId|user:openId|chat:chatId>",
1275
- },
1276
- },
1277
- directory: createChannelDirectoryAdapter({
1278
- listPeers: async ({ cfg, query, limit, accountId }) =>
1279
- listFeishuDirectoryPeers({
1280
- cfg,
1281
- query: query ?? undefined,
1282
- limit: limit ?? undefined,
1283
- accountId: accountId ?? undefined,
1284
- }),
1285
- listGroups: async ({ cfg, query, limit, accountId }) =>
1286
- listFeishuDirectoryGroups({
1287
- cfg,
1288
- query: query ?? undefined,
1289
- limit: limit ?? undefined,
1290
- accountId: accountId ?? undefined,
1291
- }),
1292
- ...createRuntimeDirectoryLiveAdapter({
1293
- getRuntime: loadFeishuChannelRuntime,
1294
- listPeersLive:
1295
- (runtime) =>
1296
- async ({ cfg, query, limit, accountId }) =>
1297
- await runtime.listFeishuDirectoryPeersLive({
1298
- cfg,
1299
- query: query ?? undefined,
1300
- limit: limit ?? undefined,
1301
- accountId: accountId ?? undefined,
1302
- }),
1303
- listGroupsLive:
1304
- (runtime) =>
1305
- async ({ cfg, query, limit, accountId }) =>
1306
- await runtime.listFeishuDirectoryGroupsLive({
1307
- cfg,
1308
- query: query ?? undefined,
1309
- limit: limit ?? undefined,
1310
- accountId: accountId ?? undefined,
1311
- }),
1312
- }),
1313
- }),
1314
- status: createComputedAccountStatusAdapter<ResolvedFeishuAccount, FeishuProbeResult>({
1315
- defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
1316
- buildChannelSummary: ({ snapshot }) =>
1317
- buildProbeChannelStatusSummary(snapshot, {
1318
- port: snapshot.port ?? null,
1319
- }),
1320
- probeAccount: async ({ account }) =>
1321
- await (await loadFeishuChannelRuntime()).probeFeishu(account),
1322
- resolveAccountSnapshot: ({ account, runtime }) => ({
1323
- accountId: account.accountId,
1324
- enabled: account.enabled,
1325
- configured: account.configured,
1326
- name: account.name,
1327
- extra: {
1328
- appId: account.appId,
1329
- domain: account.domain,
1330
- port: runtime?.port ?? null,
1331
- },
1332
- }),
1333
- }),
1334
- gateway: {
1335
- startAccount: async (ctx) => {
1336
- const { monitorFeishuProvider } = await import("./monitor.js");
1337
- const account = resolveFeishuRuntimeAccount(
1338
- { cfg: ctx.cfg, accountId: ctx.accountId },
1339
- { requireEventSecrets: true },
1340
- );
1341
- const port = account.config?.webhookPort ?? null;
1342
- ctx.setStatus({ accountId: ctx.accountId, port });
1343
- ctx.log?.info(
1344
- `starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
1345
- );
1346
- return monitorFeishuProvider({
1347
- config: ctx.cfg,
1348
- runtime: ctx.runtime,
1349
- abortSignal: ctx.abortSignal,
1350
- accountId: ctx.accountId,
1351
- });
1352
- },
1353
- },
1354
- message: feishuMessageAdapter,
1355
- },
1356
- security: {
1357
- collectWarnings: projectConfigAccountIdWarningCollector<{
1358
- cfg: ClawdbotConfig;
1359
- accountId?: string | null;
1360
- }>(collectFeishuSecurityWarnings),
1361
- collectAuditFindings: ({ cfg }) => collectFeishuSecurityAuditFindings({ cfg }),
1362
- },
1363
- pairing: {
1364
- text: {
1365
- idLabel: "feishuUserId",
1366
- message: PAIRING_APPROVED_MESSAGE,
1367
- normalizeAllowEntry: createPairingPrefixStripper(/^(feishu|user|open_id):/i),
1368
- notify: async ({ cfg, id, message, accountId }) => {
1369
- const { sendMessageFeishu } = await loadFeishuChannelRuntime();
1370
- await sendMessageFeishu({
1371
- cfg,
1372
- to: id,
1373
- text: message,
1374
- accountId,
1375
- });
1376
- },
1377
- },
1378
- },
1379
- outbound: {
1380
- deliveryMode: "direct",
1381
- chunker: chunkTextForOutbound,
1382
- chunkerMode: "markdown",
1383
- textChunkLimit: 4000,
1384
- presentationCapabilities: {
1385
- supported: true,
1386
- buttons: true,
1387
- selects: false,
1388
- context: true,
1389
- divider: true,
1390
- limits: {
1391
- actions: {
1392
- maxActions: 20,
1393
- maxActionsPerRow: 5,
1394
- maxLabelLength: 40,
1395
- maxValueBytes: 1024,
1396
- },
1397
- text: {
1398
- maxLength: 4000,
1399
- encoding: "characters",
1400
- markdownDialect: "markdown",
1401
- },
1402
- },
1403
- },
1404
- renderPresentation: async (ctx) => {
1405
- const runtime = await loadFeishuChannelRuntime();
1406
- const renderPresentation = runtime.feishuOutbound.renderPresentation;
1407
- return renderPresentation ? await renderPresentation(ctx) : null;
1408
- },
1409
- sendPayload: async (ctx) => {
1410
- const runtime = await loadFeishuChannelRuntime();
1411
- const sendPayload = runtime.feishuOutbound.sendPayload;
1412
- if (!sendPayload) {
1413
- throw new Error("Feishu payload sending is not available.");
1414
- }
1415
- return await sendPayload(ctx);
1416
- },
1417
- ...createRuntimeOutboundDelegates({
1418
- getRuntime: loadFeishuChannelRuntime,
1419
- sendText: { resolve: (runtime) => runtime.feishuOutbound.sendText },
1420
- sendMedia: { resolve: (runtime) => runtime.feishuOutbound.sendMedia },
1421
- }),
1422
- },
1423
- });