Root cause
Validation & fail-open logic
feat(feishu): sync upstream changes from clawdbot-feishu
AI-assisted change: Claude
How AI contributed: New attack surfacePublished Mar 13, 2026 in
openclaw/openclawAll 35 findings in this repository
Summary Feishu webhook mode allowed deployments that configured only verificationToken without encryptKey. In that state, forged inbound events could be accepted because the weaker configuration did not provide the required cryptographic verification boundary. Impact An unauthenticated network attacker who could reach the webhook endpoint could inject forged Feishu events, impersonate senders, and potentially trigger downstream tool execution subject to the local agent policy. Affected versions openclaw <= 2026.3.11 Patch Fixed in openclaw 2026.3.12. Feishu webhook mode now fails closed unless encryptKey is configured, and the webhook transport rejects missing or invalid signatures before dispatch. Update to 2026.3.12 or later and configure encryptKey for webhook deployments.
Claude-assisted sync added extensions/feishu/src/monitor.ts with a webhook HTTP server, but neither the monitor nor config-schema.ts required an encrypt key, so deployments that configured only verificationToken accepted forged inbound events with no cryptographic verification boundary. The fix makes encryptKey mandatory for webhook connection mode, enforces it at startup and rejects deliveries that lack a valid signature.
Root cause
feat(feishu): sync upstream changes from clawdbot-feishu
AI-assisted change: Claude
Fix
Security: require Feishu webhook encrypt key (#44087)
Fix by Vincent Koc · no AI marker found
Case facts
--- a/extensions/feishu/src/channel.ts+++ b/extensions/feishu/src/channel.ts@@ -3,6 +3,7 @@ import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sd import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js"; import { resolveFeishuAccount,+ resolveFeishuCredentials, listFeishuAccountIds, resolveDefaultFeishuAccountId, } from "./accounts.js";@@ -17,7 +18,7 @@ import { feishuOutbound } from "./outbound.js"; import { resolveFeishuGroupToolPolicy } from "./policy.js"; import { probeFeishu } from "./probe.js"; import { sendMessageFeishu } from "./send.js";-import { normalizeFeishuTarget, looksLikeFeishuId } from "./targets.js";+import { normalizeFeishuTarget, looksLikeFeishuId, formatFeishuTarget } from "./targets.js"; const meta = { id: "feishu",@@ -48,13 +49,13 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { }, }, capabilities: {- chatTypes: ["direct", "group"],+ chatTypes: ["direct", "channel"],+ polls: false,+ threads: true, media: true, reactions: true,- threads: false,- polls: false,- nativeCommands: true,- blockStreaming: true,+ edit: true,+ reply: true, }, agentPrompt: { messageToolHints: () => [@@ -93,6 +94,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { items: { oneOf: [{ type: "string" }, { type: "number" }] }, }, requireMention: { type: "boolean" },+ topicSessionMode: { type: "string", enum: ["disabled", "enabled"] }, historyLimit: { type: "integer", minimum: 0 }, dmHistoryLimit: { type: "integer", minimum: 0 }, textChunkLimit: { type: "integer", minimum: 1 },@@ -123,7 +125,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }), defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => {- const _account = resolveFeishuAccount({ cfg, accountId });+ const account = resolveFeishuAccount({ cfg, accountId }); const isDefault = accountId === DEFAULT_ACCOUNT_ID; if (isDefault) {@@ -218,9 +220,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { cfg.channels as Record<string, { groupPolicy?: string }> | undefined )?.defaults?.groupPolicy; const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";- if (groupPolicy !== "open") {- return [];- }+ if (groupPolicy !== "open") return []; return [ `- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`, ];--- a/extensions/feishu/src/config-schema.ts+++ b/extensions/feishu/src/config-schema.ts@@ -53,6 +53,20 @@ const ChannelHeartbeatVisibilitySchema = z .strict() .optional(); +/**+ * Dynamic agent creation configuration.+ * When enabled, a new agent is created for each unique DM user.+ */+const DynamicAgentCreationSchema = z+ .object({+ enabled: z.boolean().optional(),+ workspaceTemplate: z.string().optional(),+ agentDirTemplate: z.string().optional(),+ maxAgents: z.number().int().positive().optional(),+ })+ .strict()+ .optional();+ /** * Feishu tools configuration. * Controls which tool categories are enabled.@@ -72,6 +86,16 @@ const FeishuToolsConfigSchema = z .strict() .optional(); +/**+ * Topic session isolation mode for group chats.+ * - "disabled" (default): All messages in a group share one session+ * - "enabled": Messages in different topics get separate sessions+ *+ * When enabled, the session key becomes `chat:{chatId}:topic:{rootId}`+ * for messages within a topic thread, allowing isolated conversations.+ */+const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();+ export const FeishuGroupSchema = z .object({ requireMention: z.boolean().optional(),@@ -80,6 +104,7 @@ export const FeishuGroupSchema = z enabled: z.boolean().optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), systemPrompt: z.string().optional(),+ topicSessionMode: TopicSessionModeSchema, }) .strict(); @@ -142,6 +167,7 @@ export const FeishuConfigSchema = z groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), requireMention: z.boolean().optional().default(true), groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),+ topicSessionMode: TopicSessionModeSchema, historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema).optional(),@@ -152,6 +178,8 @@ export const FeishuConfigSchema = z heartbeat: ChannelHeartbeatVisibilitySchema, renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown tools: FeishuToolsConfigSchema,+ // Dynamic agent creation for DM users+ dynamicAgentCreation: DynamicAgentCreationSchema, // Multi-account configuration accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(), })diff --git a/extensions/feishu/src/config-schema.ts b/extensions/feishu/src/config-schema.tsindex 4060e6e2cbb8f..b78404de6f821 100644--- a/extensions/feishu/src/config-schema.ts+++ b/extensions/feishu/src/config-schema.ts@@ -186,7 +186,7 @@ export const FeishuAccountConfigSchema = z name: z.string().optional(), // Display name for this account appId: z.string().optional(), appSecret: buildSecretInputSchema().optional(),- encryptKey: z.string().optional(),+ encryptKey: buildSecretInputSchema().optional(), verificationToken: buildSecretInputSchema().optional(), domain: FeishuDomainSchema.optional(), connectionMode: FeishuConnectionModeSchema.optional(),@@ -204,7 +204,7 @@ export const FeishuConfigSchema = z // Top-level credentials (backward compatible for single-account mode) appId: z.string().optional(), appSecret: buildSecretInputSchema().optional(),- encryptKey: z.string().optional(),+ encryptKey: buildSecretInputSchema().optional(), verificationToken: buildSecretInputSchema().optional(), domain: FeishuDomainSchema.optional().default("feishu"), connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),@@ -240,13 +240,23 @@ export const FeishuConfigSchema = z const defaultConnectionMode = value.connectionMode ?? "websocket"; const defaultVerificationTokenConfigured = hasConfiguredSecretInput(value.verificationToken);- if (defaultConnectionMode === "webhook" && !defaultVerificationTokenConfigured) {- ctx.addIssue({- code: z.ZodIssueCode.custom,- path: ["verificationToken"],- message:- 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken',- });+ const defaultEncryptKeyConfigured = hasConfiguredSecretInput(value.encryptKey);+ if (defaultConnectionMode === "webhook") {+ if (!defaultVerificationTokenConfigured) {+ ctx.addIssue({+ code: z.ZodIssueCode.custom,+ path: ["verificationToken"],+ message:+ 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken',+ });+ }+ if (!defaultEncryptKeyConfigured) {+ ctx.addIssue({+ code: z.ZodIssueCode.custom,+ path: ["encryptKey"],+ message: 'channels.feishu.connectionMode="webhook" requires channels.feishu.encryptKey',+ });+ } } for (const [accountId, account] of Object.entries(value.accounts ?? {})) {@@ -259,6 +269,8 @@ export const FeishuConfigSchema = z } const accountVerificationTokenConfigured = hasConfiguredSecretInput(account.verificationToken) || defaultVerificationTokenConfigured;+ const accountEncryptKeyConfigured =+ hasConfiguredSecretInput(account.encryptKey) || defaultEncryptKeyConfigured; if (!accountVerificationTokenConfigured) { ctx.addIssue({ code: z.ZodIssueCode.custom,@@ -268,6 +280,15 @@ export const FeishuConfigSchema = z "a verificationToken (account-level or top-level)", }); }+ if (!accountEncryptKeyConfigured) {+ ctx.addIssue({+ code: z.ZodIssueCode.custom,+ path: ["accounts", accountId, "encryptKey"],+ message:+ `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires ` ++ "an encryptKey (account-level or top-level)",+ });+ } } if (value.dmPolicy === "open") {diff --git a/extensions/feishu/src/config-schema.test.ts b/extensions/feishu/src/config-schema.test.tsindex cdd4724d3fb14..0e0881c849fae 100644--- a/extensions/feishu/src/config-schema.test.ts+++ b/extensions/feishu/src/config-schema.test.ts@@ -47,7 +47,7 @@ describe("FeishuConfigSchema webhook validation", => { } }); - it("accepts top-level webhook mode with verificationToken", () => {+ it("rejects top-level webhook mode without encryptKey", () => { const result = FeishuConfigSchema.safeParse({ connectionMode: "webhook", verificationToken: "token_top",@@ -55,6 +55,21 @@ describe("FeishuConfigSchema webhook validation", => { appSecret: "secret_top", // pragma: allowlist secret }); + expect(result.success).toBe(false);+ if (!result.success) {+ expect(result.error.issues.some((issue) => issue.path.join(".") === "encryptKey")).toBe(true);+ }+ });++ it("accepts top-level webhook mode with verificationToken and encryptKey", () => {+ const result = FeishuConfigSchema.safeParse({+ connectionMode: "webhook",+ verificationToken: "token_top",+ encryptKey: "encrypt_top",+ appId: "cli_top",+ appSecret: "secret_top", // pragma: allowlist secret+ });+ expect(result.success).toBe(true); }); @@ -79,9 +94,30 @@ describe("FeishuConfigSchema webhook validation", => { } }); - it("accepts account webhook mode inheriting top-level verificationToken", () => {+ it("rejects account webhook mode without encryptKey", () => {+ const result = FeishuConfigSchema.safeParse({+ accounts: {+ main: {+ connectionMode: "webhook",+ verificationToken: "token_main",+ appId: "cli_main",+ appSecret: "secret_main", // pragma: allowlist secret+ },+ },+ });++ expect(result.success).toBe(false);+ if (!result.success) {+ expect(+ result.error.issues.some((issue) => issue.path.join(".") === "accounts.main.encryptKey"),+ ).toBe(true);+ }+ });++ it("accepts account webhook mode inheriting top-level verificationToken and encryptKey", () => { const result = FeishuConfigSchema.safeParse({ verificationToken: "token_top",+ encryptKey: "encrypt_top", accounts: { main: { connectionMode: "webhook",@@ -102,6 +138,31 @@ describe("FeishuConfigSchema webhook validation", => { provider: "default", id: "FEISHU_VERIFICATION_TOKEN", },+ encryptKey: "encrypt_top",+ appId: "cli_top",+ appSecret: {+ source: "env",+ provider: "default",+ id: "FEISHU_APP_SECRET",+ },+ });++ expect(result.success).toBe(true);+ });++ it("accepts SecretRef encryptKey in webhook mode", () => {+ const result = FeishuConfigSchema.safeParse({+ connectionMode: "webhook",+ verificationToken: {+ source: "env",+ provider: "default",+ id: "FEISHU_VERIFICATION_TOKEN",+ },+ encryptKey: {+ source: "env",+ provider: "default",+ id: "FEISHU_ENCRYPT_KEY",+ }, appId: "cli_top", appSecret: { source: "env",diff --git a/extensions/feishu/src/monitor.webhook-security.test.ts b/extensions/feishu/src/monitor.webhook-security.test.tsindex 466b9a4201aea..e9bfa8bf008d8 100644--- a/extensions/feishu/src/monitor.webhook-security.test.ts+++ b/extensions/feishu/src/monitor.webhook-security.test.ts@@ -64,6 +64,7 @@ function buildConfig(params: { path: string; port: number; verificationToken?: string;+ encryptKey?: string; }): ClawdbotConfig { return { channels: {@@ -78,6 +79,7 @@ function buildConfig(params: { webhookHost: "127.0.0.1", webhookPort: params.port, webhookPath: params.path,+ encryptKey: params.encryptKey, verificationToken: params.verificationToken, }, },@@ -91,6 +93,7 @@ async function withRunningWebhookMonitor( accountId: string; path: string; verificationToken: string;+ encryptKey: string; }, run: (url: string) => Promise<void>, ) {@@ -99,6 +102,7 @@ async function withRunningWebhookMonitor( accountId: params.accountId, path: params.path, port,+ encryptKey: params.encryptKey, verificationToken: params.verificationToken, }); @@ -141,6 +145,19 @@ describe("Feishu webhook security hardening", => { ); }); + it("rejects webhook mode without encryptKey", async () => {+ probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });++ const cfg = buildConfig({+ accountId: "missing-encrypt-key",+ path: "/hook-missing-encrypt",+ port: await getFreePort(),+ verificationToken: "verify_token",+ });++ await expect(monitorFeishuProvider({ config: cfg })).rejects.toThrow(/requires encryptKey/i);+ });+ it("returns 415 for POST requests without json content type", async () => { probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); await withRunningWebhookMonitor(@@ -148,6 +165,7 @@ describe("Feishu webhook security hardening", => { accountId: "content-type", path: "/hook-content-type", verificationToken: "verify_token",+ encryptKey: "encrypt_key", }, async (url) => { const response = await fetch(url, {@@ -169,6 +187,7 @@ describe("Feishu webhook security hardening", => { accountId: "rate-limit", path: "/hook-rate-limit", verificationToken: "verify_token",+ encryptKey: "encrypt_key", }, async (url) => { let saw429 = false;diff --git a/src/secrets/runtime-config-collectors-channels.ts b/src/secrets/runtime-config-collectors-channels.tsindex 91460e39aea47..9fcf71394cb57 100644--- a/src/secrets/runtime-config-collectors-channels.ts+++ b/src/secrets/runtime-config-collectors-channels.ts@@ -801,6 +801,31 @@ function collectFeishuAssignments(params: { : baseConnectionMode; return accountMode === "webhook"; });+ const topLevelEncryptKeyActive = !surface.channelEnabled+ ? false+ : !surface.hasExplicitAccounts+ ? baseConnectionMode === "webhook"+ : surface.accounts.some(({ account, enabled }) => {+ if (!enabled || hasOwnProperty(account, "encryptKey")) {+ return false;+ }+ const accountMode = hasOwnProperty(account, "connectionMode")+ ? normalizeSecretStringValue(account.connectionMode)+ : baseConnectionMode;+ return accountMode === "webhook";+ });+ collectSecretInputAssignment({+ value: feishu.encryptKey,+ path: "channels.feishu.encryptKey",+ expected: "string",+ defaults: params.defaults,+ context: params.context,+ active: topLevelEncryptKeyActive,+ inactiveReason: "no enabled Feishu webhook-mode surface inherits this top-level encryptKey.",+ apply: (value) => {+ feishu.encryptKey = value;+ },+ }); collectSecretInputAssignment({ value: feishu.verificationToken, path: "channels.feishu.verificationToken",@@ -818,6 +843,23 @@ function collectFeishuAssignments(params: { return; } for (const { accountId, account, enabled } of surface.accounts) {+ if (hasOwnProperty(account, "encryptKey")) {+ const accountMode = hasOwnProperty(account, "connectionMode")+ ? normalizeSecretStringValue(account.connectionMode)+ : baseConnectionMode;+ collectSecretInputAssignment({+ value: account.encryptKey,+ path: `channels.feishu.accounts.${accountId}.encryptKey`,+ expected: "string",+ defaults: params.defaults,+ context: params.context,+ active: enabled && accountMode === "webhook",+ inactiveReason: "Feishu account is disabled or not running in webhook mode.",+ apply: (value) => {+ account.encryptKey = value;+ },+ });+ } if (!hasOwnProperty(account, "verificationToken")) { continue; }diff --git a/extensions/feishu/src/onboarding.ts b/extensions/feishu/src/onboarding.tsindex 46ad40d7681eb..24d3bbcc413a1 100644--- a/extensions/feishu/src/onboarding.ts+++ b/extensions/feishu/src/onboarding.ts@@ -370,6 +370,37 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = { }, }; }+ const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey;+ const encryptKeyPromptState = buildSingleChannelSecretPromptState({+ accountConfigured: hasConfiguredSecretInput(currentEncryptKey),+ hasConfigToken: hasConfiguredSecretInput(currentEncryptKey),+ allowEnv: false,+ });+ const encryptKeyResult = await promptSingleChannelSecretInput({+ cfg: next,+ prompter,+ providerHint: "feishu-webhook",+ credentialLabel: "encrypt key",+ accountConfigured: encryptKeyPromptState.accountConfigured,+ canUseEnv: encryptKeyPromptState.canUseEnv,+ hasConfigToken: encryptKeyPromptState.hasConfigToken,+ envPrompt: "",+ keepPrompt: "Feishu encrypt key already configured. Keep it?",+ inputPrompt: "Enter Feishu encrypt key",+ preferredEnvVar: "FEISHU_ENCRYPT_KEY",+ });+ if (encryptKeyResult.action === "set") {+ next = {+ ...next,+ channels: {+ ...next.channels,+ feishu: {+ ...next.channels?.feishu,+ encryptKey: encryptKeyResult.value,+ },+ },+ };+ } const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath; const webhookPath = String( await prompter.text({diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.tsindex f085c9981ab5c..67f622a56fac6 100644--- a/src/secrets/target-registry-data.ts+++ b/src/secrets/target-registry-data.ts@@ -173,6 +173,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, },+ {+ id: "channels.feishu.accounts.*.encryptKey",+ targetType: "channels.feishu.accounts.*.encryptKey",+ configFile: "openclaw.json",+ pathPattern: "channels.feishu.accounts.*.encryptKey",+ secretShape: SECRET_INPUT_SHAPE,+ expectedResolvedValue: "string",+ includeInPlan: true,+ includeInConfigure: true,+ includeInAudit: true,+ }, { id: "channels.feishu.accounts.*.verificationToken", targetType: "channels.feishu.accounts.*.verificationToken",@@ -195,6 +206,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, },+ {+ id: "channels.feishu.encryptKey",+ targetType: "channels.feishu.encryptKey",+ configFile: "openclaw.json",+ pathPattern: "channels.feishu.encryptKey",+ secretShape: SECRET_INPUT_SHAPE,+ expectedResolvedValue: "string",+ includeInPlan: true,+ includeInConfigure: true,+ includeInAudit: true,+ }, { id: "channels.feishu.verificationToken", targetType: "channels.feishu.verificationToken",diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.tsindex 35d265a612d59..a5229c054f293 100644--- a/src/secrets/runtime.coverage.test.ts+++ b/src/secrets/runtime.coverage.test.ts@@ -71,6 +71,9 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string) if (entry.id === "channels.feishu.verificationToken") { setPathCreateStrict(config, ["channels", "feishu", "connectionMode"], "webhook"); }+ if (entry.id === "channels.feishu.encryptKey") {+ setPathCreateStrict(config, ["channels", "feishu", "connectionMode"], "webhook");+ } if (entry.id === "channels.feishu.accounts.*.verificationToken") { setPathCreateStrict( config,@@ -78,6 +81,13 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string) "webhook", ); }+ if (entry.id === "channels.feishu.accounts.*.encryptKey") {+ setPathCreateStrict(+ config,+ ["channels", "feishu", "accounts", "sample", "connectionMode"],+ "webhook",+ );+ } if (entry.id === "tools.web.search.gemini.apiKey") { setPathCreateStrict(config, ["tools", "web", "search", "provider"], "gemini"); }diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.tsindex 7c90136e70f41..856941c4b21a7 100644--- a/extensions/feishu/src/channel.ts+++ b/extensions/feishu/src/channel.ts@@ -129,7 +129,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { defaultAccount: { type: "string" }, appId: { type: "string" }, appSecret: secretInputJsonSchema,- encryptKey: { type: "string" },+ encryptKey: secretInputJsonSchema, verificationToken: secretInputJsonSchema, domain: { oneOf: [@@ -170,7 +170,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = { name: { type: "string" }, appId: { type: "string" }, appSecret: secretInputJsonSchema,- encryptKey: { type: "string" },+ encryptKey: secretInputJsonSchema, verificationToken: secretInputJsonSchema, domain: { type: "string", enum: ["feishu", "lark"] }, connectionMode: { type: "string", enum: ["websocket", "webhook"] },diff --git a/extensions/feishu/src/accounts.test.ts b/extensions/feishu/src/accounts.test.tsindex 979f2fa3791cb..56783bbd29dd9 100644--- a/extensions/feishu/src/accounts.test.ts+++ b/extensions/feishu/src/accounts.test.ts@@ -241,6 +241,25 @@ describe("resolveFeishuCredentials", => { domain: "feishu", }); });++ it("does not resolve encryptKey SecretRefs outside webhook mode", () => {+ const creds = resolveFeishuCredentials(+ asConfig({+ connectionMode: "websocket",+ appId: "cli_123",+ appSecret: "secret_456",+ encryptKey: { source: "file", provider: "default", id: "path/to/secret" } as never,+ }),+ );++ expect(creds).toEqual({+ appId: "cli_123",+ appSecret: "secret_456", // pragma: allowlist secret+ encryptKey: undefined,+ verificationToken: undefined,+ domain: "feishu",+ });+ }); }); describe("resolveFeishuAccount", () => {diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.tsindex 016bc99745822..b528f6ae0e542 100644--- a/extensions/feishu/src/accounts.ts+++ b/extensions/feishu/src/accounts.ts@@ -169,10 +169,14 @@ export function resolveFeishuCredentials( if (!appId || !appSecret) { return null; }+ const connectionMode = cfg?.connectionMode ?? "websocket"; return { appId, appSecret,- encryptKey: normalizeString(cfg?.encryptKey),+ encryptKey:+ connectionMode === "webhook"+ ? resolveSecretLike(cfg?.encryptKey, "channels.feishu.encryptKey")+ : normalizeString(cfg?.encryptKey), verificationToken: resolveSecretLike( cfg?.verificationToken, "channels.feishu.verificationToken",diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.tsindex 09f18cbcd4507..f7d40d8e280b8 100644--- a/extensions/feishu/src/monitor.account.ts+++ b/extensions/feishu/src/monitor.account.ts@@ -534,6 +534,9 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams): if (connectionMode === "webhook" && !account.verificationToken?.trim()) { throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`); }+ if (connectionMode === "webhook" && !account.encryptKey?.trim()) {+ throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`);+ } const warmupCount = await warmupDedupFromDisk(accountId, log); if (warmupCount > 0) {AI-assisted change 66265d05bf192b132edbc3b5b75502e85ff566a074a6c502776769615596d062 · Fix e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855