@openclaw/feishu 2026.5.2 → 2026.5.3-beta.2

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