From eb1486692de7b9d7aa8cb665cb4cae77c2bd4e77 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:45:37 +0000 Subject: [PATCH 1/9] fix(feishu): avoid resolving SecretRefs during register --- extensions/feishu/index.secretref.test.ts | 37 ++++++++ extensions/feishu/src/accounts.test.ts | 33 +++++++ extensions/feishu/src/accounts.ts | 110 +++++++++++++++------- extensions/feishu/src/bitable.ts | 4 +- extensions/feishu/src/chat.ts | 12 ++- extensions/feishu/src/docx.ts | 4 +- extensions/feishu/src/drive.ts | 4 +- extensions/feishu/src/perm.ts | 4 +- extensions/feishu/src/tool-account.ts | 4 +- extensions/feishu/src/wiki.ts | 4 +- 10 files changed, 166 insertions(+), 50 deletions(-) create mode 100644 extensions/feishu/index.secretref.test.ts diff --git a/extensions/feishu/index.secretref.test.ts b/extensions/feishu/index.secretref.test.ts new file mode 100644 index 00000000000..32c356bad35 --- /dev/null +++ b/extensions/feishu/index.secretref.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { createTestPluginApi } from "../test-utils/plugin-api.js"; +import { createPluginRuntimeMock } from "../test-utils/plugin-runtime-mock.js"; +import plugin from "./index.js"; + +describe("feishu plugin register SecretRef regression", () => { + it("does not resolve SecretRefs while registering tools", () => { + const api = createTestPluginApi({ + id: plugin.id, + name: plugin.name, + source: "extensions/feishu/index.ts", + config: { + channels: { + feishu: { + enabled: true, + accounts: { + main: { + appId: { source: "file", provider: "default", id: "path/to/app-id" }, + appSecret: { source: "file", provider: "default", id: "path/to/app-secret" }, + tools: { + chat: true, + doc: true, + drive: true, + perm: true, + wiki: true, + }, + }, + }, + }, + }, + } as never, + runtime: createPluginRuntimeMock(), + }); + + expect(() => plugin.register(api)).not.toThrow(); + }); +}); diff --git a/extensions/feishu/src/accounts.test.ts b/extensions/feishu/src/accounts.test.ts index cfe8d0abcdc..f16f2ad4ccf 100644 --- a/extensions/feishu/src/accounts.test.ts +++ b/extensions/feishu/src/accounts.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + listEnabledFeishuAccountConfigs, resolveDefaultFeishuAccountId, resolveDefaultFeishuAccountSelection, resolveFeishuAccount, @@ -369,3 +370,35 @@ describe("resolveFeishuAccount", () => { ).not.toThrow(); }); }); + +describe("listEnabledFeishuAccountConfigs", () => { + it("treats SecretRef-backed accounts as configured without resolving them", () => { + const accounts = listEnabledFeishuAccountConfigs({ + channels: { + feishu: { + enabled: true, + appId: { source: "file", provider: "default", id: "path/to/app-id" }, + appSecret: { source: "file", provider: "default", id: "path/to/app-secret" }, + accounts: { + main: { + enabled: true, + tools: { doc: true }, + }, + }, + }, + }, + } as never); + + expect(accounts).toHaveLength(1); + expect(accounts[0]).toEqual( + expect.objectContaining({ + accountId: "main", + enabled: true, + configured: true, + config: expect.objectContaining({ + tools: { doc: true }, + }), + }), + ); + }); +}); diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index ede2be08635..b9d9a0137f6 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -1,9 +1,14 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import type { ClawdbotConfig } from "../runtime-api.js"; -import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js"; +import { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "./secret-input.js"; import type { FeishuConfig, FeishuAccountConfig, + FeishuAccountSelectionSource, FeishuDefaultAccountSelectionSource, FeishuDomain, ResolvedFeishuAccount, @@ -99,6 +104,51 @@ function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): Feish return { ...base, ...account } as FeishuConfig; } +function resolveFeishuAccountConfigState(params: { + cfg: ClawdbotConfig; + accountId?: string | null; +}): { + accountId: string; + selectionSource: FeishuAccountSelectionSource; + enabled: boolean; + configured: boolean; + name?: string; + domain: FeishuDomain; + config: FeishuConfig; +} { + const hasExplicitAccountId = + typeof params.accountId === "string" && params.accountId.trim() !== ""; + const defaultSelection = hasExplicitAccountId + ? null + : resolveDefaultFeishuAccountSelection(params.cfg); + const accountId = hasExplicitAccountId + ? normalizeAccountId(params.accountId) + : (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID); + const selectionSource: FeishuAccountSelectionSource = hasExplicitAccountId + ? "explicit" + : (defaultSelection?.source ?? "fallback"); + const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined; + + const baseEnabled = feishuCfg?.enabled !== false; + const merged = mergeFeishuAccountConfig(params.cfg, accountId); + const accountEnabled = merged.enabled !== false; + const enabled = baseEnabled && accountEnabled; + const configured = Boolean( + hasConfiguredSecretInput(merged.appId) && hasConfiguredSecretInput(merged.appSecret), + ); + const accountName = (merged as FeishuAccountConfig).name; + + return { + accountId, + selectionSource, + enabled, + configured, + name: typeof accountName === "string" ? accountName.trim() || undefined : undefined, + domain: merged.domain ?? "feishu", + config: merged, + }; +} + /** * Resolve Feishu credentials from a config. */ @@ -192,48 +242,42 @@ export function resolveFeishuAccount(params: { cfg: ClawdbotConfig; accountId?: string | null; }): ResolvedFeishuAccount { - const hasExplicitAccountId = - typeof params.accountId === "string" && params.accountId.trim() !== ""; - const defaultSelection = hasExplicitAccountId - ? null - : resolveDefaultFeishuAccountSelection(params.cfg); - const accountId = hasExplicitAccountId - ? normalizeAccountId(params.accountId) - : (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID); - const selectionSource = hasExplicitAccountId - ? "explicit" - : (defaultSelection?.source ?? "fallback"); - const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined; - - // Base enabled state (top-level) - const baseEnabled = feishuCfg?.enabled !== false; - - // Merge configs - const merged = mergeFeishuAccountConfig(params.cfg, accountId); - - // Account-level enabled state - const accountEnabled = merged.enabled !== false; - const enabled = baseEnabled && accountEnabled; - - // Resolve credentials from merged config - const creds = resolveFeishuCredentials(merged); - const accountName = (merged as FeishuAccountConfig).name; + const state = resolveFeishuAccountConfigState(params); + const creds = resolveFeishuCredentials(state.config); return { - accountId, - selectionSource, - enabled, + accountId: state.accountId, + selectionSource: state.selectionSource, + enabled: state.enabled, configured: Boolean(creds), - name: typeof accountName === "string" ? accountName.trim() || undefined : undefined, + name: state.name, appId: creds?.appId, appSecret: creds?.appSecret, encryptKey: creds?.encryptKey, verificationToken: creds?.verificationToken, - domain: creds?.domain ?? "feishu", - config: merged, + domain: creds?.domain ?? state.domain, + config: state.config, }; } +/** + * List all enabled accounts that appear configured from raw config input alone. + * This preflight path intentionally does not resolve SecretRefs. + */ +export function listEnabledFeishuAccountConfigs(cfg: ClawdbotConfig): Array<{ + accountId: string; + selectionSource: FeishuAccountSelectionSource; + enabled: boolean; + configured: boolean; + name?: string; + domain: FeishuDomain; + config: FeishuConfig; +}> { + return listFeishuAccountIds(cfg) + .map((accountId) => resolveFeishuAccountConfigState({ cfg, accountId })) + .filter((account) => account.enabled && account.configured); +} + /** * List all enabled and configured accounts. */ diff --git a/extensions/feishu/src/bitable.ts b/extensions/feishu/src/bitable.ts index 451839edb1f..3518d96d97e 100644 --- a/extensions/feishu/src/bitable.ts +++ b/extensions/feishu/src/bitable.ts @@ -1,7 +1,7 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import { Type } from "@sinclair/typebox"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccounts } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { createFeishuToolClient } from "./tool-account.js"; // ============ Helpers ============ @@ -538,7 +538,7 @@ export function registerFeishuBitableTools(api: OpenClawPluginApi) { return; } - const accounts = listEnabledFeishuAccounts(api.config); + const accounts = listEnabledFeishuAccountConfigs(api.config); if (accounts.length === 0) { api.logger.debug?.("feishu_bitable: No Feishu accounts configured, skipping bitable tools"); return; diff --git a/extensions/feishu/src/chat.ts b/extensions/feishu/src/chat.ts index b32dfb41230..849b9883260 100644 --- a/extensions/feishu/src/chat.ts +++ b/extensions/feishu/src/chat.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccounts } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs, resolveFeishuAccount } from "./accounts.js"; import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; @@ -126,20 +126,20 @@ export function registerFeishuChatTools(api: OpenClawPluginApi) { return; } - const accounts = listEnabledFeishuAccounts(api.config); + const accounts = listEnabledFeishuAccountConfigs(api.config); if (accounts.length === 0) { api.logger.debug?.("feishu_chat: No Feishu accounts configured, skipping chat tools"); return; } - const firstAccount = accounts[0]; + const firstAccount = accounts[0]!; const toolsCfg = resolveToolsConfig(firstAccount.config.tools); if (!toolsCfg.chat) { api.logger.debug?.("feishu_chat: chat tool disabled in config"); return; } - const getClient = () => createFeishuClient(firstAccount); + const defaultAccountId = firstAccount.accountId; api.registerTool( { @@ -150,7 +150,9 @@ export function registerFeishuChatTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuChatParams; try { - const client = getClient(); + const client = createFeishuClient( + resolveFeishuAccount({ cfg: api.config, accountId: defaultAccountId }), + ); switch (p.action) { case "members": if (!p.chat_id) { diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index 7debd446a14..74c8e0d3d8a 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -5,7 +5,7 @@ import { basename } from "node:path"; import type * as Lark from "@larksuiteoapi/node-sdk"; import { Type } from "@sinclair/typebox"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccounts } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js"; import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js"; import { updateColorText } from "./docx-color-text.js"; @@ -1233,7 +1233,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) { } // Check if any account is configured - const accounts = listEnabledFeishuAccounts(api.config); + const accounts = listEnabledFeishuAccountConfigs(api.config); if (accounts.length === 0) { api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools"); return; diff --git a/extensions/feishu/src/drive.ts b/extensions/feishu/src/drive.ts index 495b6aaaef9..2d9cd7004f6 100644 --- a/extensions/feishu/src/drive.ts +++ b/extensions/feishu/src/drive.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccounts } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; import { @@ -169,7 +169,7 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) { return; } - const accounts = listEnabledFeishuAccounts(api.config); + const accounts = listEnabledFeishuAccountConfigs(api.config); if (accounts.length === 0) { api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools"); return; diff --git a/extensions/feishu/src/perm.ts b/extensions/feishu/src/perm.ts index a9d2e062eec..553796009e3 100644 --- a/extensions/feishu/src/perm.ts +++ b/extensions/feishu/src/perm.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccounts } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; import { @@ -118,7 +118,7 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) { return; } - const accounts = listEnabledFeishuAccounts(api.config); + const accounts = listEnabledFeishuAccountConfigs(api.config); if (accounts.length === 0) { api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools"); return; diff --git a/extensions/feishu/src/tool-account.ts b/extensions/feishu/src/tool-account.ts index dff71b424dc..182ec6847a1 100644 --- a/extensions/feishu/src/tool-account.ts +++ b/extensions/feishu/src/tool-account.ts @@ -3,7 +3,7 @@ import type { OpenClawPluginApi } from "../runtime-api.js"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; -import type { FeishuToolsConfig, ResolvedFeishuAccount } from "./types.js"; +import type { FeishuToolsConfig } from "./types.js"; type AccountAwareParams = { accountId?: string }; @@ -47,7 +47,7 @@ export function createFeishuToolClient(params: { } export function resolveAnyEnabledFeishuToolsConfig( - accounts: ResolvedFeishuAccount[], + accounts: Array<{ config: { tools?: FeishuToolsConfig } }>, ): Required { const merged: Required = { doc: false, diff --git a/extensions/feishu/src/wiki.ts b/extensions/feishu/src/wiki.ts index a2df89ff0fe..09c6ddea498 100644 --- a/extensions/feishu/src/wiki.ts +++ b/extensions/feishu/src/wiki.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccounts } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; import { jsonToolResult, @@ -157,7 +157,7 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) { return; } - const accounts = listEnabledFeishuAccounts(api.config); + const accounts = listEnabledFeishuAccountConfigs(api.config); if (accounts.length === 0) { api.logger.debug?.("feishu_wiki: No Feishu accounts configured, skipping wiki tools"); return; From 4323fc7c45358af29501562a009c6c4a8fd0f37a Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:59:45 +0000 Subject: [PATCH 2/9] fix(feishu): restore tool account type import --- extensions/feishu/src/tool-account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/feishu/src/tool-account.ts b/extensions/feishu/src/tool-account.ts index 182ec6847a1..41b8e158dd1 100644 --- a/extensions/feishu/src/tool-account.ts +++ b/extensions/feishu/src/tool-account.ts @@ -3,7 +3,7 @@ import type { OpenClawPluginApi } from "../runtime-api.js"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; -import type { FeishuToolsConfig } from "./types.js"; +import type { FeishuToolsConfig, ResolvedFeishuAccount } from "./types.js"; type AccountAwareParams = { accountId?: string }; From 6a418ca78412b4edf455b7f3f6937c3fae867878 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:23:49 +0000 Subject: [PATCH 3/9] test(channels): align routing auth expectations with current behavior --- extensions/discord/src/monitor/exec-approvals.test.ts | 8 +++++++- .../src/bot-message-context.topic-agentid.test.ts | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/extensions/discord/src/monitor/exec-approvals.test.ts b/extensions/discord/src/monitor/exec-approvals.test.ts index be3ead1d400..135a418822c 100644 --- a/extensions/discord/src/monitor/exec-approvals.test.ts +++ b/extensions/discord/src/monitor/exec-approvals.test.ts @@ -714,6 +714,8 @@ describe("DiscordExecApprovalHandler target config", () => { describe("DiscordExecApprovalHandler gateway auth", () => { it("passes the shared gateway token from config into GatewayClient", async () => { + vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", undefined); + vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", undefined); const handler = new DiscordExecApprovalHandler({ token: "discord-bot-token", accountId: "default", @@ -727,7 +729,11 @@ describe("DiscordExecApprovalHandler gateway auth", () => { }, }); - await handler.start(); + try { + await handler.start(); + } finally { + vi.unstubAllEnvs(); + } expect(gatewayClientStarts).toHaveBeenCalledTimes(1); expect(gatewayClientParams[0]).toMatchObject({ diff --git a/extensions/telegram/src/bot-message-context.topic-agentid.test.ts b/extensions/telegram/src/bot-message-context.topic-agentid.test.ts index 57c0c8209a0..f8797de29c3 100644 --- a/extensions/telegram/src/bot-message-context.topic-agentid.test.ts +++ b/extensions/telegram/src/bot-message-context.topic-agentid.test.ts @@ -100,7 +100,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => { expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:"); }); - it("preserves an unknown topic agentId in the session key", async () => { + it("falls back to default agent for unknown topic agentId", async () => { vi.mocked(loadConfig).mockReturnValue({ agents: { list: [{ id: "main", default: true }, { id: "zu" }], @@ -112,7 +112,9 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => { const ctx = await buildForumContext({ topicConfig: { agentId: "ghost" } }); expect(ctx).not.toBeNull(); - expect(ctx?.ctxPayload?.SessionKey).toContain("agent:ghost:"); + // pickFirstExistingAgentId falls back to the default agent when the + // configured agentId does not match any entry in agents.list. + expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:"); }); it("routes DM topic to specific agent when agentId is set", async () => { From 466b8ad0e6e3234a45ae4a53c286c5c0e7774d0d Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:38:27 +0000 Subject: [PATCH 4/9] fix(feishu): route chat tool through merged account config --- extensions/feishu/src/chat.test.ts | 23 ++-- extensions/feishu/src/chat.ts | 101 +++++++++--------- .../feishu/src/tool-account-routing.test.ts | 40 +++++++ 3 files changed, 101 insertions(+), 63 deletions(-) diff --git a/extensions/feishu/src/chat.test.ts b/extensions/feishu/src/chat.test.ts index d06442b12f8..1fb05c6c3e9 100644 --- a/extensions/feishu/src/chat.test.ts +++ b/extensions/feishu/src/chat.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerFeishuChatTools } from "./chat.js"; +import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); const chatGetMock = vi.hoisted(() => vi.fn()); @@ -25,8 +26,7 @@ describe("registerFeishuChatTools", () => { }); it("registers feishu_chat and handles info/members actions", async () => { - const registerTool = vi.fn(); - registerFeishuChatTools({ + const { api, resolveTool } = createToolFactoryHarness({ config: { channels: { feishu: { @@ -37,13 +37,10 @@ describe("registerFeishuChatTools", () => { }, }, } as any, - logger: { debug: vi.fn(), info: vi.fn() } as any, - registerTool, - } as any); + }); + registerFeishuChatTools(api); - expect(registerTool).toHaveBeenCalledTimes(1); - const tool = registerTool.mock.calls[0]?.[0]; - expect(tool?.name).toBe("feishu_chat"); + const tool = resolveTool("feishu_chat"); chatGetMock.mockResolvedValueOnce({ code: 0, @@ -97,8 +94,7 @@ describe("registerFeishuChatTools", () => { }); it("skips registration when chat tool is disabled", () => { - const registerTool = vi.fn(); - registerFeishuChatTools({ + const { api, resolveTool } = createToolFactoryHarness({ config: { channels: { feishu: { @@ -109,9 +105,8 @@ describe("registerFeishuChatTools", () => { }, }, } as any, - logger: { debug: vi.fn(), info: vi.fn() } as any, - registerTool, - } as any); - expect(registerTool).not.toHaveBeenCalled(); + }); + registerFeishuChatTools(api); + expect(() => resolveTool("feishu_chat")).toThrow("Tool not registered: feishu_chat"); }); }); diff --git a/extensions/feishu/src/chat.ts b/extensions/feishu/src/chat.ts index 849b9883260..dc6aeffe9b3 100644 --- a/extensions/feishu/src/chat.ts +++ b/extensions/feishu/src/chat.ts @@ -1,9 +1,8 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccountConfigs, resolveFeishuAccount } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js"; -import { createFeishuClient } from "./client.js"; -import { resolveToolsConfig } from "./tools-config.js"; +import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; function json(data: unknown) { return { @@ -132,60 +131,64 @@ export function registerFeishuChatTools(api: OpenClawPluginApi) { return; } - const firstAccount = accounts[0]!; - const toolsCfg = resolveToolsConfig(firstAccount.config.tools); + const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts); if (!toolsCfg.chat) { api.logger.debug?.("feishu_chat: chat tool disabled in config"); return; } - const defaultAccountId = firstAccount.accountId; + type FeishuChatExecuteParams = FeishuChatParams & { accountId?: string }; api.registerTool( - { - name: "feishu_chat", - label: "Feishu Chat", - description: "Feishu chat operations. Actions: members, info, member_info", - parameters: FeishuChatSchema, - async execute(_toolCallId, params) { - const p = params as FeishuChatParams; - try { - const client = createFeishuClient( - resolveFeishuAccount({ cfg: api.config, accountId: defaultAccountId }), - ); - switch (p.action) { - case "members": - if (!p.chat_id) { - return json({ error: "chat_id is required for action members" }); - } - return json( - await getChatMembers( - client, - p.chat_id, - p.page_size, - p.page_token, - p.member_id_type, - ), - ); - case "info": - if (!p.chat_id) { - return json({ error: "chat_id is required for action info" }); - } - return json(await getChatInfo(client, p.chat_id)); - case "member_info": - if (!p.member_id) { - return json({ error: "member_id is required for action member_info" }); - } - return json( - await getFeishuMemberInfo(client, p.member_id, p.member_id_type ?? "open_id"), - ); - default: - return json({ error: `Unknown action: ${String(p.action)}` }); + (ctx) => { + const defaultAccountId = ctx.agentAccountId; + return { + name: "feishu_chat", + label: "Feishu Chat", + description: "Feishu chat operations. Actions: members, info, member_info", + parameters: FeishuChatSchema, + async execute(_toolCallId, params) { + const p = params as FeishuChatExecuteParams; + try { + const client = createFeishuToolClient({ + api, + executeParams: p, + defaultAccountId, + }); + switch (p.action) { + case "members": + if (!p.chat_id) { + return json({ error: "chat_id is required for action members" }); + } + return json( + await getChatMembers( + client, + p.chat_id, + p.page_size, + p.page_token, + p.member_id_type, + ), + ); + case "info": + if (!p.chat_id) { + return json({ error: "chat_id is required for action info" }); + } + return json(await getChatInfo(client, p.chat_id)); + case "member_info": + if (!p.member_id) { + return json({ error: "member_id is required for action member_info" }); + } + return json( + await getFeishuMemberInfo(client, p.member_id, p.member_id_type ?? "open_id"), + ); + default: + return json({ error: `Unknown action: ${String(p.action)}` }); + } + } catch (err) { + return json({ error: err instanceof Error ? err.message : String(err) }); } - } catch (err) { - return json({ error: err instanceof Error ? err.message : String(err) }); - } - }, + }, + }; }, { name: "feishu_chat" }, ); diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index b5697676493..10623ab8f51 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -1,13 +1,18 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { registerFeishuBitableTools } from "./bitable.js"; +import { registerFeishuChatTools } from "./chat.js"; import { registerFeishuDriveTools } from "./drive.js"; import { registerFeishuPermTools } from "./perm.js"; import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; import { registerFeishuWikiTools } from "./wiki.js"; +const chatGetMock = vi.fn(); const createFeishuClientMock = vi.fn((account: { appId?: string } | undefined) => ({ __appId: account?.appId, + im: { + chat: { get: chatGetMock }, + }, })); vi.mock("./client.js", () => ({ @@ -16,11 +21,13 @@ vi.mock("./client.js", () => ({ function createConfig(params: { toolsA?: { + chat?: boolean; wiki?: boolean; drive?: boolean; perm?: boolean; }; toolsB?: { + chat?: boolean; wiki?: boolean; drive?: boolean; perm?: boolean; @@ -100,6 +107,39 @@ describe("feishu tool account routing", () => { expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); }); + test("chat tool registers when first account disables it and routes to agentAccountId", async () => { + chatGetMock.mockResolvedValue({ code: 0, data: { name: "chat", user_count: 1 } }); + const { api, resolveTool } = createToolFactoryHarness( + createConfig({ + toolsA: { chat: false }, + toolsB: { chat: true }, + }), + ); + registerFeishuChatTools(api); + + const tool = resolveTool("feishu_chat", { agentAccountId: "b" }); + await tool.execute("call", { action: "info", chat_id: "oc_b" }); + + expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); + }); + + test("chat tool prefers configured defaultAccount over inherited default account context", async () => { + chatGetMock.mockResolvedValue({ code: 0, data: { name: "chat", user_count: 1 } }); + const { api, resolveTool } = createToolFactoryHarness( + createConfig({ + defaultAccount: "b", + toolsA: { chat: true }, + toolsB: { chat: true }, + }), + ); + registerFeishuChatTools(api); + + const tool = resolveTool("feishu_chat", { agentAccountId: "a" }); + await tool.execute("call", { action: "info", chat_id: "oc_b" }); + + expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); + }); + test("perm tool registers when only second account enables it and routes to agentAccountId", async () => { const { api, resolveTool } = createToolFactoryHarness( createConfig({ From b75a55da5de010c2395dad6ef1db57eb31736ac0 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:05:01 +0000 Subject: [PATCH 5/9] fix(feishu): preserve per-account chat disable semantics --- extensions/feishu/src/accounts.test.ts | 27 +++++++++++++++++++ extensions/feishu/src/accounts.ts | 15 ++++++++--- extensions/feishu/src/chat.ts | 12 ++++++++- .../feishu/src/tool-account-routing.test.ts | 22 +++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/extensions/feishu/src/accounts.test.ts b/extensions/feishu/src/accounts.test.ts index f16f2ad4ccf..ad8ea3426d1 100644 --- a/extensions/feishu/src/accounts.test.ts +++ b/extensions/feishu/src/accounts.test.ts @@ -401,4 +401,31 @@ describe("listEnabledFeishuAccountConfigs", () => { }), ); }); + + it("preserves inherited tools flags when account tools only override a subset", () => { + const accounts = listEnabledFeishuAccountConfigs({ + channels: { + feishu: { + enabled: true, + appId: "app", + appSecret: "secret", // pragma: allowlist secret + tools: { + chat: false, + }, + accounts: { + main: { + enabled: true, + tools: { doc: true }, + }, + }, + }, + }, + } as never); + + expect(accounts).toHaveLength(1); + expect(accounts[0]?.config.tools).toEqual({ + chat: false, + doc: true, + }); + }); }); diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index b9d9a0137f6..72db0a8687f 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -100,11 +100,20 @@ function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): Feish // Get account-specific overrides const account = resolveAccountConfig(cfg, accountId) ?? {}; - // Merge: account config overrides base config - return { ...base, ...account } as FeishuConfig; + // Merge account overrides over base config while preserving nested tools defaults. + const merged = { ...base, ...account } as FeishuConfig; + const baseTools = base.tools; + const accountTools = account.tools; + if (baseTools || accountTools) { + merged.tools = { + ...(baseTools ?? {}), + ...(accountTools ?? {}), + }; + } + return merged; } -function resolveFeishuAccountConfigState(params: { +export function resolveFeishuAccountConfigState(params: { cfg: ClawdbotConfig; accountId?: string | null; }): { diff --git a/extensions/feishu/src/chat.ts b/extensions/feishu/src/chat.ts index dc6aeffe9b3..7e2f9e87dcc 100644 --- a/extensions/feishu/src/chat.ts +++ b/extensions/feishu/src/chat.ts @@ -1,8 +1,9 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccountConfigs } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs, resolveFeishuAccountConfigState } from "./accounts.js"; import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; +import { resolveToolsConfig } from "./tools-config.js"; function json(data: unknown) { return { @@ -150,6 +151,15 @@ export function registerFeishuChatTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuChatExecuteParams; try { + const account = resolveFeishuAccountConfigState({ + cfg: api.config, + accountId: p.accountId ?? defaultAccountId ?? undefined, + }); + if (!resolveToolsConfig(account.config.tools).chat) { + return json({ + error: `Feishu chat is disabled for account "${account.accountId}".`, + }); + } const client = createFeishuToolClient({ api, executeParams: p, diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index 10623ab8f51..1c49393c88b 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -140,6 +140,28 @@ describe("feishu tool account routing", () => { expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); }); + test("chat tool blocks execution when the routed account disables chat", async () => { + const { api, resolveTool } = createToolFactoryHarness( + createConfig({ + toolsA: { chat: true }, + toolsB: { chat: false }, + }), + ); + registerFeishuChatTools(api); + + const tool = resolveTool("feishu_chat", { agentAccountId: "b" }); + const result = await tool.execute("call", { action: "info", chat_id: "oc_b" }); + + expect(result).toEqual( + expect.objectContaining({ + details: expect.objectContaining({ + error: 'Feishu chat is disabled for account "b".', + }), + }), + ); + expect(createFeishuClientMock).not.toHaveBeenCalled(); + }); + test("perm tool registers when only second account enables it and routes to agentAccountId", async () => { const { api, resolveTool } = createToolFactoryHarness( createConfig({ From 5339a790d51474a9ea3b668318f2dfbfce376556 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:15:43 +0000 Subject: [PATCH 6/9] fix(feishu): guard routed tool disables --- extensions/feishu/src/chat.ts | 26 ++++++++++---- .../feishu/src/docx.account-selection.test.ts | 33 ++++++++++++++++++ extensions/feishu/src/docx.ts | 34 +++++++++++++++++++ extensions/feishu/src/drive.ts | 24 ++++++++++++- extensions/feishu/src/perm.ts | 24 ++++++++++++- .../feishu/src/tool-account-routing.test.ts | 23 +++++++++++++ extensions/feishu/src/tool-account.ts | 29 +++++++++++++++- extensions/feishu/src/wiki.ts | 24 ++++++++++++- 8 files changed, 206 insertions(+), 11 deletions(-) diff --git a/extensions/feishu/src/chat.ts b/extensions/feishu/src/chat.ts index 7e2f9e87dcc..2706ed9d08b 100644 --- a/extensions/feishu/src/chat.ts +++ b/extensions/feishu/src/chat.ts @@ -1,9 +1,13 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { listEnabledFeishuAccountConfigs, resolveFeishuAccountConfigState } from "./accounts.js"; +import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js"; -import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; -import { resolveToolsConfig } from "./tools-config.js"; +import { + createFeishuToolClient, + isFeishuToolEnabledForRoutedAccount, + resolveAnyEnabledFeishuToolsConfig, + resolveFeishuToolAccountConfigState, +} from "./tool-account.js"; function json(data: unknown) { return { @@ -151,11 +155,19 @@ export function registerFeishuChatTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuChatExecuteParams; try { - const account = resolveFeishuAccountConfigState({ - cfg: api.config, - accountId: p.accountId ?? defaultAccountId ?? undefined, + const account = resolveFeishuToolAccountConfigState({ + api, + executeParams: p, + defaultAccountId, }); - if (!resolveToolsConfig(account.config.tools).chat) { + if ( + !isFeishuToolEnabledForRoutedAccount({ + api, + executeParams: p, + defaultAccountId, + tool: "chat", + }) + ) { return json({ error: `Feishu chat is disabled for account "${account.accountId}".`, }); diff --git a/extensions/feishu/src/docx.account-selection.test.ts b/extensions/feishu/src/docx.account-selection.test.ts index 1f11e290815..118b194f8f0 100644 --- a/extensions/feishu/src/docx.account-selection.test.ts +++ b/extensions/feishu/src/docx.account-selection.test.ts @@ -67,4 +67,37 @@ describe("feishu_doc account selection", () => { expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-a"); }); + + test("blocks execution when configured defaultAccount disables doc", async () => { + const cfg = { + channels: { + feishu: { + enabled: true, + defaultAccount: "b", + accounts: { + a: { appId: "app-a", appSecret: "sec-a", tools: { doc: true } }, // pragma: allowlist secret + b: { appId: "app-b", appSecret: "sec-b", tools: { doc: false } }, // pragma: allowlist secret + }, + }, + }, + } as OpenClawPluginApi["config"]; + + const { api, resolveTool } = createToolFactoryHarness(cfg); + registerFeishuDocTools(api); + + const docTool = resolveTool("feishu_doc", { agentAccountId: "a" }); + const result = await docTool.execute("call-disabled", { + action: "list_blocks", + doc_token: "d", + }); + + expect(result).toEqual( + expect.objectContaining({ + details: expect.objectContaining({ + error: 'Feishu doc is disabled for account "b".', + }), + }), + ); + expect(createFeishuClientMock).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index 74c8e0d3d8a..aaa0be53952 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -20,7 +20,9 @@ import { import { getFeishuRuntime } from "./runtime.js"; import { createFeishuToolClient, + isFeishuToolEnabledForRoutedAccount, resolveAnyEnabledFeishuToolsConfig, + resolveFeishuToolAccountConfigState, resolveFeishuToolAccount, } from "./tool-account.js"; @@ -1273,6 +1275,23 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuDocExecuteParams; try { + const account = resolveFeishuToolAccountConfigState({ + api, + executeParams: p, + defaultAccountId, + }); + if ( + !isFeishuToolEnabledForRoutedAccount({ + api, + executeParams: p, + defaultAccountId, + tool: "doc", + }) + ) { + return json({ + error: `Feishu doc is disabled for account "${account.accountId}".`, + }); + } const client = getClient(p, defaultAccountId); switch (p.action) { case "read": @@ -1442,6 +1461,21 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) { parameters: Type.Object({}), async execute() { try { + const account = resolveFeishuToolAccountConfigState({ + api, + defaultAccountId: ctx.agentAccountId, + }); + if ( + !isFeishuToolEnabledForRoutedAccount({ + api, + defaultAccountId: ctx.agentAccountId, + tool: "scopes", + }) + ) { + return json({ + error: `Feishu scopes are disabled for account "${account.accountId}".`, + }); + } const result = await listAppScopes(getClient(undefined, ctx.agentAccountId)); return json(result); } catch (err) { diff --git a/extensions/feishu/src/drive.ts b/extensions/feishu/src/drive.ts index 2d9cd7004f6..842015fec8f 100644 --- a/extensions/feishu/src/drive.ts +++ b/extensions/feishu/src/drive.ts @@ -2,7 +2,12 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js"; -import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; +import { + createFeishuToolClient, + isFeishuToolEnabledForRoutedAccount, + resolveAnyEnabledFeishuToolsConfig, + resolveFeishuToolAccountConfigState, +} from "./tool-account.js"; import { jsonToolResult, toolExecutionErrorResult, @@ -195,6 +200,23 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuDriveExecuteParams; try { + const account = resolveFeishuToolAccountConfigState({ + api, + executeParams: p, + defaultAccountId, + }); + if ( + !isFeishuToolEnabledForRoutedAccount({ + api, + executeParams: p, + defaultAccountId, + tool: "drive", + }) + ) { + return jsonToolResult({ + error: `Feishu drive is disabled for account "${account.accountId}".`, + }); + } const client = createFeishuToolClient({ api, executeParams: p, diff --git a/extensions/feishu/src/perm.ts b/extensions/feishu/src/perm.ts index 553796009e3..edee640bb9f 100644 --- a/extensions/feishu/src/perm.ts +++ b/extensions/feishu/src/perm.ts @@ -2,7 +2,12 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { listEnabledFeishuAccountConfigs } from "./accounts.js"; import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js"; -import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; +import { + createFeishuToolClient, + isFeishuToolEnabledForRoutedAccount, + resolveAnyEnabledFeishuToolsConfig, + resolveFeishuToolAccountConfigState, +} from "./tool-account.js"; import { jsonToolResult, toolExecutionErrorResult, @@ -143,6 +148,23 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuPermExecuteParams; try { + const account = resolveFeishuToolAccountConfigState({ + api, + executeParams: p, + defaultAccountId, + }); + if ( + !isFeishuToolEnabledForRoutedAccount({ + api, + executeParams: p, + defaultAccountId, + tool: "perm", + }) + ) { + return jsonToolResult({ + error: `Feishu perm is disabled for account "${account.accountId}".`, + }); + } const client = createFeishuToolClient({ api, executeParams: p, diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index 1c49393c88b..4b20d422037 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -162,6 +162,29 @@ describe("feishu tool account routing", () => { expect(createFeishuClientMock).not.toHaveBeenCalled(); }); + test("chat tool checks the same routed account precedence as execution", async () => { + const { api, resolveTool } = createToolFactoryHarness( + createConfig({ + defaultAccount: "b", + toolsA: { chat: true }, + toolsB: { chat: false }, + }), + ); + registerFeishuChatTools(api); + + const tool = resolveTool("feishu_chat", { agentAccountId: "a" }); + const result = await tool.execute("call", { action: "info", chat_id: "oc_b" }); + + expect(result).toEqual( + expect.objectContaining({ + details: expect.objectContaining({ + error: 'Feishu chat is disabled for account "b".', + }), + }), + ); + expect(createFeishuClientMock).not.toHaveBeenCalled(); + }); + test("perm tool registers when only second account enables it and routes to agentAccountId", async () => { const { api, resolveTool } = createToolFactoryHarness( createConfig({ diff --git a/extensions/feishu/src/tool-account.ts b/extensions/feishu/src/tool-account.ts index 41b8e158dd1..a6d3b84d15d 100644 --- a/extensions/feishu/src/tool-account.ts +++ b/extensions/feishu/src/tool-account.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuAccount, resolveFeishuAccountConfigState } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; import type { FeishuToolsConfig, ResolvedFeishuAccount } from "./types.js"; @@ -38,6 +38,33 @@ export function resolveFeishuToolAccount(params: { }); } +export function resolveFeishuToolAccountConfigState(params: { + api: Pick; + executeParams?: AccountAwareParams; + defaultAccountId?: string; +}) { + if (!params.api.config) { + throw new Error("Feishu config unavailable"); + } + return resolveFeishuAccountConfigState({ + cfg: params.api.config, + accountId: + normalizeOptionalAccountId(params.executeParams?.accountId) ?? + readConfiguredDefaultAccountId(params.api.config) ?? + normalizeOptionalAccountId(params.defaultAccountId), + }); +} + +export function isFeishuToolEnabledForRoutedAccount(params: { + api: Pick; + executeParams?: AccountAwareParams; + defaultAccountId?: string; + tool: keyof Required; +}): boolean { + const account = resolveFeishuToolAccountConfigState(params); + return resolveToolsConfig(account.config.tools)[params.tool]; +} + export function createFeishuToolClient(params: { api: Pick; executeParams?: AccountAwareParams; diff --git a/extensions/feishu/src/wiki.ts b/extensions/feishu/src/wiki.ts index 09c6ddea498..1ec4187987c 100644 --- a/extensions/feishu/src/wiki.ts +++ b/extensions/feishu/src/wiki.ts @@ -1,7 +1,12 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { listEnabledFeishuAccountConfigs } from "./accounts.js"; -import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; +import { + createFeishuToolClient, + isFeishuToolEnabledForRoutedAccount, + resolveAnyEnabledFeishuToolsConfig, + resolveFeishuToolAccountConfigState, +} from "./tool-account.js"; import { jsonToolResult, toolExecutionErrorResult, @@ -183,6 +188,23 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) { async execute(_toolCallId, params) { const p = params as FeishuWikiExecuteParams; try { + const account = resolveFeishuToolAccountConfigState({ + api, + executeParams: p, + defaultAccountId, + }); + if ( + !isFeishuToolEnabledForRoutedAccount({ + api, + executeParams: p, + defaultAccountId, + tool: "wiki", + }) + ) { + return jsonToolResult({ + error: `Feishu wiki is disabled for account "${account.accountId}".`, + }); + } const client = createFeishuToolClient({ api, executeParams: p, From 77edd2dad15336bad3d61176c4ff013ee9e87c81 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:38:38 +0000 Subject: [PATCH 7/9] fix(feishu): align preflight with supported fields --- extensions/feishu/index.secretref.test.ts | 2 +- extensions/feishu/src/accounts.test.ts | 22 +++++++++- extensions/feishu/src/accounts.ts | 2 +- .../feishu/src/docx.account-selection.test.ts | 41 +++++++++++++++++++ extensions/feishu/src/docx.ts | 11 +++-- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/extensions/feishu/index.secretref.test.ts b/extensions/feishu/index.secretref.test.ts index 32c356bad35..36260b20e29 100644 --- a/extensions/feishu/index.secretref.test.ts +++ b/extensions/feishu/index.secretref.test.ts @@ -15,7 +15,7 @@ describe("feishu plugin register SecretRef regression", () => { enabled: true, accounts: { main: { - appId: { source: "file", provider: "default", id: "path/to/app-id" }, + appId: "app-id", appSecret: { source: "file", provider: "default", id: "path/to/app-secret" }, tools: { chat: true, diff --git a/extensions/feishu/src/accounts.test.ts b/extensions/feishu/src/accounts.test.ts index ad8ea3426d1..fcaee6b740c 100644 --- a/extensions/feishu/src/accounts.test.ts +++ b/extensions/feishu/src/accounts.test.ts @@ -377,7 +377,7 @@ describe("listEnabledFeishuAccountConfigs", () => { channels: { feishu: { enabled: true, - appId: { source: "file", provider: "default", id: "path/to/app-id" }, + appId: "app-id", appSecret: { source: "file", provider: "default", id: "path/to/app-secret" }, accounts: { main: { @@ -402,6 +402,26 @@ describe("listEnabledFeishuAccountConfigs", () => { ); }); + it("does not treat SecretRef-style appId objects as configured in config-only preflight", () => { + const accounts = listEnabledFeishuAccountConfigs({ + channels: { + feishu: { + enabled: true, + appId: { source: "file", provider: "default", id: "path/to/app-id" } as never, + appSecret: { source: "file", provider: "default", id: "path/to/app-secret" }, + accounts: { + main: { + enabled: true, + tools: { doc: true }, + }, + }, + }, + }, + } as never); + + expect(accounts).toHaveLength(0); + }); + it("preserves inherited tools flags when account tools only override a subset", () => { const accounts = listEnabledFeishuAccountConfigs({ channels: { diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index 72db0a8687f..6886ab39890 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -143,7 +143,7 @@ export function resolveFeishuAccountConfigState(params: { const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const configured = Boolean( - hasConfiguredSecretInput(merged.appId) && hasConfiguredSecretInput(merged.appSecret), + normalizeSecretInputString(merged.appId) && hasConfiguredSecretInput(merged.appSecret), ); const accountName = (merged as FeishuAccountConfig).name; diff --git a/extensions/feishu/src/docx.account-selection.test.ts b/extensions/feishu/src/docx.account-selection.test.ts index 118b194f8f0..27cbd755b3d 100644 --- a/extensions/feishu/src/docx.account-selection.test.ts +++ b/extensions/feishu/src/docx.account-selection.test.ts @@ -5,6 +5,16 @@ import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; const createFeishuClientMock = vi.fn((creds: { appId?: string } | undefined) => ({ __appId: creds?.appId, + application: { + scope: { + list: vi.fn().mockResolvedValue({ + code: 0, + data: { + scopes: [], + }, + }), + }, + }, })); vi.mock("./client.js", () => { @@ -100,4 +110,35 @@ describe("feishu_doc account selection", () => { ); expect(createFeishuClientMock).not.toHaveBeenCalled(); }); + + test("feishu_app_scopes allows explicit accountId override when defaultAccount disables scopes", async () => { + const cfg = { + channels: { + feishu: { + enabled: true, + defaultAccount: "b", + accounts: { + a: { appId: "app-a", appSecret: "sec-a", tools: { scopes: true } }, // pragma: allowlist secret + b: { appId: "app-b", appSecret: "sec-b", tools: { scopes: false } }, // pragma: allowlist secret + }, + }, + }, + } as OpenClawPluginApi["config"]; + + const { api, resolveTool } = createToolFactoryHarness(cfg); + registerFeishuDocTools(api); + + const scopesTool = resolveTool("feishu_app_scopes", { agentAccountId: "a" }); + await scopesTool.execute("call-enabled", { accountId: "a" }); + const blocked = await scopesTool.execute("call-blocked", {}); + + expect(createFeishuClientMock.mock.calls[0]?.[0]?.appId).toBe("app-a"); + expect(blocked).toEqual( + expect.objectContaining({ + details: expect.objectContaining({ + error: 'Feishu scopes are disabled for account "b".', + }), + }), + ); + }); }); diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index aaa0be53952..f1fea67d30c 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -1458,16 +1458,21 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) { label: "Feishu App Scopes", description: "List current app permissions (scopes). Use to debug permission issues or check available capabilities.", - parameters: Type.Object({}), - async execute() { + parameters: Type.Object({ + accountId: Type.Optional(Type.String()), + }), + async execute(_toolCallId, params) { + const p = params as { accountId?: string }; try { const account = resolveFeishuToolAccountConfigState({ api, + executeParams: p, defaultAccountId: ctx.agentAccountId, }); if ( !isFeishuToolEnabledForRoutedAccount({ api, + executeParams: p, defaultAccountId: ctx.agentAccountId, tool: "scopes", }) @@ -1476,7 +1481,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) { error: `Feishu scopes are disabled for account "${account.accountId}".`, }); } - const result = await listAppScopes(getClient(undefined, ctx.agentAccountId)); + const result = await listAppScopes(getClient(p, ctx.agentAccountId)); return json(result); } catch (err) { return json({ error: err instanceof Error ? err.message : String(err) }); From af2c7fc17127475a6e1a85e143c08505f9e2a5b5 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:39:05 +0000 Subject: [PATCH 8/9] test: drop unrelated branch carryover --- extensions/discord/src/monitor/exec-approvals.test.ts | 8 +------- .../src/bot-message-context.topic-agentid.test.ts | 6 ++---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/extensions/discord/src/monitor/exec-approvals.test.ts b/extensions/discord/src/monitor/exec-approvals.test.ts index 135a418822c..be3ead1d400 100644 --- a/extensions/discord/src/monitor/exec-approvals.test.ts +++ b/extensions/discord/src/monitor/exec-approvals.test.ts @@ -714,8 +714,6 @@ describe("DiscordExecApprovalHandler target config", () => { describe("DiscordExecApprovalHandler gateway auth", () => { it("passes the shared gateway token from config into GatewayClient", async () => { - vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", undefined); - vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", undefined); const handler = new DiscordExecApprovalHandler({ token: "discord-bot-token", accountId: "default", @@ -729,11 +727,7 @@ describe("DiscordExecApprovalHandler gateway auth", () => { }, }); - try { - await handler.start(); - } finally { - vi.unstubAllEnvs(); - } + await handler.start(); expect(gatewayClientStarts).toHaveBeenCalledTimes(1); expect(gatewayClientParams[0]).toMatchObject({ diff --git a/extensions/telegram/src/bot-message-context.topic-agentid.test.ts b/extensions/telegram/src/bot-message-context.topic-agentid.test.ts index f8797de29c3..57c0c8209a0 100644 --- a/extensions/telegram/src/bot-message-context.topic-agentid.test.ts +++ b/extensions/telegram/src/bot-message-context.topic-agentid.test.ts @@ -100,7 +100,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => { expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:"); }); - it("falls back to default agent for unknown topic agentId", async () => { + it("preserves an unknown topic agentId in the session key", async () => { vi.mocked(loadConfig).mockReturnValue({ agents: { list: [{ id: "main", default: true }, { id: "zu" }], @@ -112,9 +112,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => { const ctx = await buildForumContext({ topicConfig: { agentId: "ghost" } }); expect(ctx).not.toBeNull(); - // pickFirstExistingAgentId falls back to the default agent when the - // configured agentId does not match any entry in agents.list. - expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:"); + expect(ctx?.ctxPayload?.SessionKey).toContain("agent:ghost:"); }); it("routes DM topic to specific agent when agentId is set", async () => { From 7b9351f5adc1e22c88435cf71773dc85eabc20f2 Mon Sep 17 00:00:00 2001 From: gaohongxiang <33713367+gaohongxiang@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:52:33 +0000 Subject: [PATCH 9/9] fix(feishu): expose chat account override --- extensions/feishu/src/chat-schema.ts | 5 +++++ .../feishu/src/tool-account-routing.test.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/extensions/feishu/src/chat-schema.ts b/extensions/feishu/src/chat-schema.ts index 5460f11dcc9..702773d4b73 100644 --- a/extensions/feishu/src/chat-schema.ts +++ b/extensions/feishu/src/chat-schema.ts @@ -9,6 +9,11 @@ export const FeishuChatSchema = Type.Object({ enum: [...CHAT_ACTION_VALUES], description: "Action to run: members | info | member_info", }), + accountId: Type.Optional( + Type.String({ + description: "Optional Feishu account override for multi-account setups", + }), + ), chat_id: Type.Optional(Type.String({ description: "Chat ID (from URL or event payload)" })), member_id: Type.Optional(Type.String({ description: "Member ID for member_info lookups" })), page_size: Type.Optional(Type.Number({ description: "Page size (1-100, default 50)" })), diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index 4b20d422037..af357838c7d 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -140,6 +140,23 @@ describe("feishu tool account routing", () => { expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-b"); }); + test("chat tool allows explicit accountId override when defaultAccount disables chat", async () => { + chatGetMock.mockResolvedValue({ code: 0, data: { name: "chat", user_count: 1 } }); + const { api, resolveTool } = createToolFactoryHarness( + createConfig({ + defaultAccount: "b", + toolsA: { chat: true }, + toolsB: { chat: false }, + }), + ); + registerFeishuChatTools(api); + + const tool = resolveTool("feishu_chat", { agentAccountId: "a" }); + await tool.execute("call", { action: "info", chat_id: "oc_a", accountId: "a" }); + + expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-a"); + }); + test("chat tool blocks execution when the routed account disables chat", async () => { const { api, resolveTool } = createToolFactoryHarness( createConfig({