@openclaw/feishu 2026.3.13 → 2026.5.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/api.ts +31 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/index.ts +70 -53
  6. package/openclaw.plugin.json +1653 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1252 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +84 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +365 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +32 -94
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +119 -20
  72. package/src/directory.ts +61 -91
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +375 -26
  91. package/src/media.ts +434 -88
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
  114. package/src/monitor.startup.test.ts +11 -9
  115. package/src/monitor.startup.ts +26 -16
  116. package/src/monitor.state.ts +20 -5
  117. package/src/monitor.synthetic-error.ts +18 -0
  118. package/src/monitor.test-mocks.ts +2 -2
  119. package/src/monitor.transport.ts +220 -60
  120. package/src/monitor.ts +15 -10
  121. package/src/monitor.webhook-e2e.test.ts +65 -7
  122. package/src/monitor.webhook-security.test.ts +122 -0
  123. package/src/monitor.webhook.test-helpers.ts +44 -26
  124. package/src/outbound-runtime-api.ts +1 -0
  125. package/src/outbound.test.ts +616 -37
  126. package/src/outbound.ts +623 -81
  127. package/src/perm-schema.ts +1 -1
  128. package/src/perm.ts +1 -7
  129. package/src/pins.ts +108 -0
  130. package/src/policy.test.ts +297 -117
  131. package/src/policy.ts +142 -29
  132. package/src/post.ts +7 -6
  133. package/src/probe.test.ts +14 -9
  134. package/src/probe.ts +26 -16
  135. package/src/processing-claims.ts +59 -0
  136. package/src/qr-terminal.ts +1 -0
  137. package/src/reactions.ts +4 -34
  138. package/src/reasoning-preview.test.ts +59 -0
  139. package/src/reasoning-preview.ts +20 -0
  140. package/src/reply-dispatcher-runtime-api.ts +7 -0
  141. package/src/reply-dispatcher.test.ts +660 -29
  142. package/src/reply-dispatcher.ts +407 -154
  143. package/src/runtime.ts +6 -3
  144. package/src/secret-contract.ts +145 -0
  145. package/src/secret-input.ts +1 -13
  146. package/src/security-audit-shared.ts +69 -0
  147. package/src/security-audit.test.ts +61 -0
  148. package/src/security-audit.ts +1 -0
  149. package/src/send-result.ts +1 -1
  150. package/src/send-target.test.ts +9 -3
  151. package/src/send-target.ts +10 -4
  152. package/src/send.reply-fallback.test.ts +77 -2
  153. package/src/send.test.ts +386 -4
  154. package/src/send.ts +399 -86
  155. package/src/sequential-key.test.ts +72 -0
  156. package/src/sequential-key.ts +28 -0
  157. package/src/sequential-queue.test.ts +92 -0
  158. package/src/sequential-queue.ts +16 -0
  159. package/src/session-conversation.ts +42 -0
  160. package/src/session-route.ts +48 -0
  161. package/src/setup-core.ts +51 -0
  162. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  163. package/src/setup-surface.ts +581 -0
  164. package/src/streaming-card.test.ts +138 -2
  165. package/src/streaming-card.ts +134 -18
  166. package/src/subagent-hooks.test.ts +603 -0
  167. package/src/subagent-hooks.ts +397 -0
  168. package/src/targets.ts +3 -13
  169. package/src/test-support/lifecycle-test-support.ts +479 -0
  170. package/src/thread-bindings.test.ts +143 -0
  171. package/src/thread-bindings.ts +330 -0
  172. package/src/tool-account-routing.test.ts +66 -8
  173. package/src/tool-account.test.ts +44 -0
  174. package/src/tool-account.ts +40 -17
  175. package/src/tool-factory-test-harness.ts +11 -8
  176. package/src/tool-result.ts +3 -1
  177. package/src/tools-config.ts +1 -1
  178. package/src/types.ts +16 -15
  179. package/src/typing.ts +10 -6
  180. package/src/wiki-schema.ts +1 -1
  181. package/src/wiki.ts +1 -7
  182. package/subagent-hooks-api.ts +31 -0
  183. package/tsconfig.json +16 -0
  184. package/src/feishu-command-handler.ts +0 -59
  185. package/src/onboarding.status.test.ts +0 -25
  186. package/src/onboarding.ts +0 -489
  187. package/src/send-message.ts +0 -71
  188. package/src/targets.test.ts +0 -70
package/src/channel.ts CHANGED
@@ -1,35 +1,119 @@
1
+ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
2
+ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
1
3
  import {
2
- collectAllowlistProviderRestrictSendersWarnings,
3
- formatAllowFromLowercase,
4
- mapAllowFromEntries,
5
- } from "openclaw/plugin-sdk/compat";
6
- import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
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";
7
13
  import {
8
- buildProbeChannelStatusSummary,
9
- buildRuntimeAccountStatusSnapshot,
10
- createDefaultChannelRuntimeState,
11
- DEFAULT_ACCOUNT_ID,
12
- PAIRING_APPROVED_MESSAGE,
13
- } from "openclaw/plugin-sdk/feishu";
14
+ createAllowlistProviderGroupPolicyWarningCollector,
15
+ projectConfigAccountIdWarningCollector,
16
+ } from "openclaw/plugin-sdk/channel-policy";
17
+ import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
14
18
  import {
15
- resolveFeishuAccount,
16
- resolveFeishuCredentials,
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,
17
33
  listFeishuAccountIds,
18
34
  resolveDefaultFeishuAccountId,
35
+ resolveFeishuAccount,
36
+ resolveFeishuRuntimeAccount,
19
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";
20
57
  import {
21
- listFeishuDirectoryPeers,
22
- listFeishuDirectoryGroups,
23
- listFeishuDirectoryPeersLive,
24
- listFeishuDirectoryGroupsLive,
25
- } from "./directory.js";
26
- import { feishuOnboardingAdapter } from "./onboarding.js";
27
- import { feishuOutbound } from "./outbound.js";
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";
28
66
  import { resolveFeishuGroupToolPolicy } from "./policy.js";
29
- import { probeFeishu } from "./probe.js";
30
- import { sendMessageFeishu } from "./send.js";
31
- import { normalizeFeishuTarget, looksLikeFeishuId, formatFeishuTarget } from "./targets.js";
32
- import type { ResolvedFeishuAccount, FeishuConfig } from "./types.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
+ }
33
117
 
34
118
  const meta: ChannelMeta = {
35
119
  id: "feishu",
@@ -42,21 +126,116 @@ const meta: ChannelMeta = {
42
126
  order: 70,
43
127
  };
44
128
 
45
- const secretInputJsonSchema = {
46
- oneOf: [
47
- { type: "string" },
48
- {
49
- type: "object",
50
- additionalProperties: false,
51
- required: ["source", "provider", "id"],
52
- properties: {
53
- source: { type: "string", enum: ["env", "file", "exec"] },
54
- provider: { type: "string", minLength: 1 },
55
- id: { type: "string", minLength: 1 },
56
- },
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",
57
146
  },
58
- ],
59
- } as const;
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
+ }
60
239
 
61
240
  function setFeishuNamedAccountEnabled(
62
241
  cfg: ClawdbotConfig,
@@ -82,288 +261,1052 @@ function setFeishuNamedAccountEnabled(
82
261
  };
83
262
  }
84
263
 
85
- export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
86
- id: "feishu",
87
- meta: {
88
- ...meta,
89
- },
90
- pairing: {
91
- idLabel: "feishuUserId",
92
- normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
93
- notifyApproval: async ({ cfg, id }) => {
94
- await sendMessageFeishu({
95
- cfg,
96
- to: id,
97
- text: PAIRING_APPROVED_MESSAGE,
98
- });
99
- },
100
- },
101
- capabilities: {
102
- chatTypes: ["direct", "channel"],
103
- polls: false,
104
- threads: true,
105
- media: true,
106
- reactions: true,
107
- edit: true,
108
- reply: true,
109
- },
110
- agentPrompt: {
111
- messageToolHints: () => [
112
- "- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
113
- "- Feishu supports interactive cards for rich messages.",
114
- ],
115
- },
116
- groups: {
117
- resolveToolPolicy: resolveFeishuGroupToolPolicy,
118
- },
119
- mentions: {
120
- stripPatterns: () => ['<at user_id="[^"]*">[^<]*</at>'],
121
- },
122
- reload: { configPrefixes: ["channels.feishu"] },
123
- configSchema: {
124
- schema: {
125
- type: "object",
126
- additionalProperties: false,
127
- properties: {
128
- enabled: { type: "boolean" },
129
- defaultAccount: { type: "string" },
130
- appId: { type: "string" },
131
- appSecret: secretInputJsonSchema,
132
- encryptKey: secretInputJsonSchema,
133
- verificationToken: secretInputJsonSchema,
134
- domain: {
135
- oneOf: [
136
- { type: "string", enum: ["feishu", "lark"] },
137
- { type: "string", format: "uri", pattern: "^https://" },
138
- ],
139
- },
140
- connectionMode: { type: "string", enum: ["websocket", "webhook"] },
141
- webhookPath: { type: "string" },
142
- webhookHost: { type: "string" },
143
- webhookPort: { type: "integer", minimum: 1 },
144
- dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
145
- allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
146
- groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
147
- groupAllowFrom: {
148
- type: "array",
149
- items: { oneOf: [{ type: "string" }, { type: "number" }] },
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
+ },
150
596
  },
151
- requireMention: { type: "boolean" },
152
- groupSessionScope: {
153
- type: "string",
154
- enum: ["group", "group_sender", "group_topic", "group_topic_sender"],
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);
155
638
  },
156
- topicSessionMode: { type: "string", enum: ["disabled", "enabled"] },
157
- replyInThread: { type: "string", enum: ["disabled", "enabled"] },
158
- historyLimit: { type: "integer", minimum: 0 },
159
- dmHistoryLimit: { type: "integer", minimum: 0 },
160
- textChunkLimit: { type: "integer", minimum: 1 },
161
- chunkMode: { type: "string", enum: ["length", "newline"] },
162
- mediaMaxMb: { type: "number", minimum: 0 },
163
- renderMode: { type: "string", enum: ["auto", "raw", "card"] },
164
- accounts: {
165
- type: "object",
166
- additionalProperties: {
167
- type: "object",
168
- properties: {
169
- enabled: { type: "boolean" },
170
- name: { type: "string" },
171
- appId: { type: "string" },
172
- appSecret: secretInputJsonSchema,
173
- encryptKey: secretInputJsonSchema,
174
- verificationToken: secretInputJsonSchema,
175
- domain: { type: "string", enum: ["feishu", "lark"] },
176
- connectionMode: { type: "string", enum: ["websocket", "webhook"] },
177
- webhookHost: { type: "string" },
178
- webhookPath: { type: "string" },
179
- webhookPort: { type: "integer", minimum: 1 },
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
+ },
180
668
  },
181
- },
669
+ };
182
670
  },
183
- },
184
- },
185
- },
186
- config: {
187
- listAccountIds: (cfg) => listFeishuAccountIds(cfg),
188
- resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
189
- defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
190
- setAccountEnabled: ({ cfg, accountId, enabled }) => {
191
- const account = resolveFeishuAccount({ cfg, accountId });
192
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
193
-
194
- if (isDefault) {
195
- // For default account, set top-level enabled
196
- return {
197
- ...cfg,
198
- channels: {
199
- ...cfg.channels,
200
- feishu: {
201
- ...cfg.channels?.feishu,
202
- enabled,
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,
203
679
  },
204
- },
205
- };
206
- }
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
+ }
207
773
 
208
- // For named accounts, set enabled in accounts[accountId]
209
- return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
210
- },
211
- deleteAccount: ({ cfg, accountId }) => {
212
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
213
-
214
- if (isDefault) {
215
- // Delete entire feishu config
216
- const next = { ...cfg } as ClawdbotConfig;
217
- const nextChannels = { ...cfg.channels };
218
- delete (nextChannels as Record<string, unknown>).feishu;
219
- if (Object.keys(nextChannels).length > 0) {
220
- next.channels = nextChannels;
221
- } else {
222
- delete next.channels;
223
- }
224
- return next;
225
- }
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
+ }
226
953
 
227
- // Delete specific account from accounts
228
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
229
- const accounts = { ...feishuCfg?.accounts };
230
- delete accounts[accountId];
231
-
232
- return {
233
- ...cfg,
234
- channels: {
235
- ...cfg.channels,
236
- feishu: {
237
- ...feishuCfg,
238
- accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
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
+ normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
1158
+ resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
1159
+ const directId = parseFeishuDirectConversationId(conversationId);
1160
+ if (directId) {
1161
+ return { to: `user:${directId}` };
1162
+ }
1163
+ const parsed = parseFeishuConversationId({ conversationId, parentConversationId });
1164
+ if (parsed?.topicId) {
1165
+ return {
1166
+ to: `chat:${parentConversationId?.trim() || parsed.chatId}`,
1167
+ threadId: parsed.topicId,
1168
+ };
1169
+ }
1170
+ return { to: `chat:${parsed?.chatId ?? conversationId.trim()}` };
1171
+ },
1172
+ resolveSessionConversation: ({ kind, rawId }) =>
1173
+ resolveFeishuSessionConversation({ kind, rawId }),
1174
+ resolveOutboundSessionRoute: (params) => resolveFeishuOutboundSessionRoute(params),
1175
+ targetResolver: {
1176
+ looksLikeId: looksLikeFeishuId,
1177
+ hint: "<chatId|user:openId|chat:chatId>",
1178
+ },
1179
+ },
1180
+ directory: createChannelDirectoryAdapter({
1181
+ listPeers: async ({ cfg, query, limit, accountId }) =>
1182
+ listFeishuDirectoryPeers({
1183
+ cfg,
1184
+ query: query ?? undefined,
1185
+ limit: limit ?? undefined,
1186
+ accountId: accountId ?? undefined,
1187
+ }),
1188
+ listGroups: async ({ cfg, query, limit, accountId }) =>
1189
+ listFeishuDirectoryGroups({
1190
+ cfg,
1191
+ query: query ?? undefined,
1192
+ limit: limit ?? undefined,
1193
+ accountId: accountId ?? undefined,
1194
+ }),
1195
+ ...createRuntimeDirectoryLiveAdapter({
1196
+ getRuntime: loadFeishuChannelRuntime,
1197
+ listPeersLive:
1198
+ (runtime) =>
1199
+ async ({ cfg, query, limit, accountId }) =>
1200
+ await runtime.listFeishuDirectoryPeersLive({
1201
+ cfg,
1202
+ query: query ?? undefined,
1203
+ limit: limit ?? undefined,
1204
+ accountId: accountId ?? undefined,
1205
+ }),
1206
+ listGroupsLive:
1207
+ (runtime) =>
1208
+ async ({ cfg, query, limit, accountId }) =>
1209
+ await runtime.listFeishuDirectoryGroupsLive({
1210
+ cfg,
1211
+ query: query ?? undefined,
1212
+ limit: limit ?? undefined,
1213
+ accountId: accountId ?? undefined,
1214
+ }),
1215
+ }),
1216
+ }),
1217
+ status: createComputedAccountStatusAdapter<ResolvedFeishuAccount, FeishuProbeResult>({
1218
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
1219
+ buildChannelSummary: ({ snapshot }) =>
1220
+ buildProbeChannelStatusSummary(snapshot, {
1221
+ port: snapshot.port ?? null,
1222
+ }),
1223
+ probeAccount: async ({ account }) =>
1224
+ await (await loadFeishuChannelRuntime()).probeFeishu(account),
1225
+ resolveAccountSnapshot: ({ account, runtime }) => ({
1226
+ accountId: account.accountId,
1227
+ enabled: account.enabled,
1228
+ configured: account.configured,
1229
+ name: account.name,
1230
+ extra: {
1231
+ appId: account.appId,
1232
+ domain: account.domain,
1233
+ port: runtime?.port ?? null,
239
1234
  },
1235
+ }),
1236
+ }),
1237
+ gateway: {
1238
+ startAccount: async (ctx) => {
1239
+ const { monitorFeishuProvider } = await import("./monitor.js");
1240
+ const account = resolveFeishuRuntimeAccount(
1241
+ { cfg: ctx.cfg, accountId: ctx.accountId },
1242
+ { requireEventSecrets: true },
1243
+ );
1244
+ const port = account.config?.webhookPort ?? null;
1245
+ ctx.setStatus({ accountId: ctx.accountId, port });
1246
+ ctx.log?.info(
1247
+ `starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
1248
+ );
1249
+ return monitorFeishuProvider({
1250
+ config: ctx.cfg,
1251
+ runtime: ctx.runtime,
1252
+ abortSignal: ctx.abortSignal,
1253
+ accountId: ctx.accountId,
1254
+ });
240
1255
  },
241
- };
242
- },
243
- isConfigured: (account) => account.configured,
244
- describeAccount: (account) => ({
245
- accountId: account.accountId,
246
- enabled: account.enabled,
247
- configured: account.configured,
248
- name: account.name,
249
- appId: account.appId,
250
- domain: account.domain,
251
- }),
252
- resolveAllowFrom: ({ cfg, accountId }) => {
253
- const account = resolveFeishuAccount({ cfg, accountId });
254
- return mapAllowFromEntries(account.config?.allowFrom);
255
- },
256
- formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }),
257
- },
258
- security: {
259
- collectWarnings: ({ cfg, accountId }) => {
260
- const account = resolveFeishuAccount({ cfg, accountId });
261
- const feishuCfg = account.config;
262
- return collectAllowlistProviderRestrictSendersWarnings({
263
- cfg,
264
- providerConfigPresent: cfg.channels?.feishu !== undefined,
265
- configuredGroupPolicy: feishuCfg?.groupPolicy,
266
- surface: `Feishu[${account.accountId}] groups`,
267
- openScope: "any member",
268
- groupPolicyPath: "channels.feishu.groupPolicy",
269
- groupAllowFromPath: "channels.feishu.groupAllowFrom",
270
- });
1256
+ },
271
1257
  },
272
- },
273
- setup: {
274
- resolveAccountId: () => DEFAULT_ACCOUNT_ID,
275
- applyAccountConfig: ({ cfg, accountId }) => {
276
- const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
277
-
278
- if (isDefault) {
279
- return {
280
- ...cfg,
281
- channels: {
282
- ...cfg.channels,
283
- feishu: {
284
- ...cfg.channels?.feishu,
285
- enabled: true,
286
- },
287
- },
288
- };
289
- }
290
-
291
- return setFeishuNamedAccountEnabled(cfg, accountId, true);
1258
+ security: {
1259
+ collectWarnings: projectConfigAccountIdWarningCollector<{
1260
+ cfg: ClawdbotConfig;
1261
+ accountId?: string | null;
1262
+ }>(collectFeishuSecurityWarnings),
1263
+ collectAuditFindings: ({ cfg }) => collectFeishuSecurityAuditFindings({ cfg }),
292
1264
  },
293
- },
294
- onboarding: feishuOnboardingAdapter,
295
- messaging: {
296
- normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
297
- targetResolver: {
298
- looksLikeId: looksLikeFeishuId,
299
- hint: "<chatId|user:openId|chat:chatId>",
1265
+ pairing: {
1266
+ text: {
1267
+ idLabel: "feishuUserId",
1268
+ message: PAIRING_APPROVED_MESSAGE,
1269
+ normalizeAllowEntry: createPairingPrefixStripper(/^(feishu|user|open_id):/i),
1270
+ notify: async ({ cfg, id, message, accountId }) => {
1271
+ const { sendMessageFeishu } = await loadFeishuChannelRuntime();
1272
+ await sendMessageFeishu({
1273
+ cfg,
1274
+ to: id,
1275
+ text: message,
1276
+ accountId,
1277
+ });
1278
+ },
1279
+ },
300
1280
  },
301
- },
302
- directory: {
303
- self: async () => null,
304
- listPeers: async ({ cfg, query, limit, accountId }) =>
305
- listFeishuDirectoryPeers({
306
- cfg,
307
- query: query ?? undefined,
308
- limit: limit ?? undefined,
309
- accountId: accountId ?? undefined,
310
- }),
311
- listGroups: async ({ cfg, query, limit, accountId }) =>
312
- listFeishuDirectoryGroups({
313
- cfg,
314
- query: query ?? undefined,
315
- limit: limit ?? undefined,
316
- accountId: accountId ?? undefined,
317
- }),
318
- listPeersLive: async ({ cfg, query, limit, accountId }) =>
319
- listFeishuDirectoryPeersLive({
320
- cfg,
321
- query: query ?? undefined,
322
- limit: limit ?? undefined,
323
- accountId: accountId ?? undefined,
324
- }),
325
- listGroupsLive: async ({ cfg, query, limit, accountId }) =>
326
- listFeishuDirectoryGroupsLive({
327
- cfg,
328
- query: query ?? undefined,
329
- limit: limit ?? undefined,
330
- accountId: accountId ?? undefined,
331
- }),
332
- },
333
- outbound: feishuOutbound,
334
- status: {
335
- defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
336
- buildChannelSummary: ({ snapshot }) =>
337
- buildProbeChannelStatusSummary(snapshot, {
338
- port: snapshot.port ?? null,
1281
+ outbound: {
1282
+ deliveryMode: "direct",
1283
+ chunker: chunkTextForOutbound,
1284
+ chunkerMode: "markdown",
1285
+ textChunkLimit: 4000,
1286
+ presentationCapabilities: {
1287
+ supported: true,
1288
+ buttons: true,
1289
+ selects: false,
1290
+ context: true,
1291
+ divider: true,
1292
+ },
1293
+ renderPresentation: async (ctx) => {
1294
+ const runtime = await loadFeishuChannelRuntime();
1295
+ const renderPresentation = runtime.feishuOutbound.renderPresentation;
1296
+ return renderPresentation ? await renderPresentation(ctx) : null;
1297
+ },
1298
+ sendPayload: async (ctx) => {
1299
+ const runtime = await loadFeishuChannelRuntime();
1300
+ const sendPayload = runtime.feishuOutbound.sendPayload;
1301
+ if (!sendPayload) {
1302
+ throw new Error("Feishu payload sending is not available.");
1303
+ }
1304
+ return await sendPayload(ctx);
1305
+ },
1306
+ ...createRuntimeOutboundDelegates({
1307
+ getRuntime: loadFeishuChannelRuntime,
1308
+ sendText: { resolve: (runtime) => runtime.feishuOutbound.sendText },
1309
+ sendMedia: { resolve: (runtime) => runtime.feishuOutbound.sendMedia },
339
1310
  }),
340
- probeAccount: async ({ account }) => await probeFeishu(account),
341
- buildAccountSnapshot: ({ account, runtime, probe }) => ({
342
- accountId: account.accountId,
343
- enabled: account.enabled,
344
- configured: account.configured,
345
- name: account.name,
346
- appId: account.appId,
347
- domain: account.domain,
348
- ...buildRuntimeAccountStatusSnapshot({ runtime, probe }),
349
- port: runtime?.port ?? null,
350
- }),
351
- },
352
- gateway: {
353
- startAccount: async (ctx) => {
354
- const { monitorFeishuProvider } = await import("./monitor.js");
355
- const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
356
- const port = account.config?.webhookPort ?? null;
357
- ctx.setStatus({ accountId: ctx.accountId, port });
358
- ctx.log?.info(
359
- `starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
360
- );
361
- return monitorFeishuProvider({
362
- config: ctx.cfg,
363
- runtime: ctx.runtime,
364
- abortSignal: ctx.abortSignal,
365
- accountId: ctx.accountId,
366
- });
367
1311
  },
368
- },
369
- };
1312
+ });