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] 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({