fix(feishu): route chat tool through merged account config

This commit is contained in:
gaohongxiang 2026-03-18 06:38:27 +00:00
parent 6a418ca784
commit 466b8ad0e6
3 changed files with 101 additions and 63 deletions

View File

@ -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");
});
});

View File

@ -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" },
);

View File

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