mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(feishu): route doc tools by agent account
Previously feishu_doc always used accounts[0], so multi-account setups created docs under the first bot regardless of the calling agent. This change resolves accountId via a before_tool_call hook (defaulting from agentAccountId) and selects the Feishu client per call. Fixes #27321
This commit is contained in:
@@ -6,6 +6,7 @@ import { registerFeishuDocTools } from "./src/docx.js";
|
||||
import { registerFeishuDriveTools } from "./src/drive.js";
|
||||
import { registerFeishuPermTools } from "./src/perm.js";
|
||||
import { setFeishuRuntime } from "./src/runtime.js";
|
||||
import { resolveFeishuAccountForToolContext } from "./src/tool-context.js";
|
||||
import { registerFeishuWikiTools } from "./src/wiki.js";
|
||||
|
||||
export { monitorFeishuProvider } from "./src/monitor.js";
|
||||
@@ -52,6 +53,13 @@ const plugin = {
|
||||
register(api: OpenClawPluginApi) {
|
||||
setFeishuRuntime(api.runtime);
|
||||
api.registerChannel({ plugin: feishuPlugin });
|
||||
|
||||
// Ensure Feishu tool registration uses the calling agent's account / outbound identity.
|
||||
api.registerHook(["before_tool_call"], resolveFeishuAccountForToolContext, {
|
||||
name: "feishu:resolve-account",
|
||||
description: "Resolve Feishu accountId for Feishu tools based on the calling agent",
|
||||
});
|
||||
|
||||
registerFeishuDocTools(api);
|
||||
registerFeishuWikiTools(api);
|
||||
registerFeishuDriveTools(api);
|
||||
|
||||
91
extensions/feishu/src/docx.account-selection.test.ts
Normal file
91
extensions/feishu/src/docx.account-selection.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
const createFeishuClientMock = vi.fn((creds: any) => ({ __appId: creds?.appId }));
|
||||
|
||||
vi.mock("./client.js", () => {
|
||||
return {
|
||||
createFeishuClient: (creds: any) => createFeishuClientMock(creds),
|
||||
};
|
||||
});
|
||||
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { registerFeishuDocTools } from "./docx.js";
|
||||
|
||||
// Patch the specific API calls we need so tool execution doesn't hit network.
|
||||
vi.mock("@larksuiteoapi/node-sdk", () => {
|
||||
return {
|
||||
default: {},
|
||||
};
|
||||
});
|
||||
|
||||
function fakeApi(cfg: any) {
|
||||
const tools: Array<{ name: string; execute: (id: string, params: any) => any }> = [];
|
||||
const api: Partial<OpenClawPluginApi> = {
|
||||
config: cfg,
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
},
|
||||
registerTool: (tool: any) => {
|
||||
tools.push({ name: tool.name, execute: tool.execute });
|
||||
return undefined as any;
|
||||
},
|
||||
};
|
||||
return { api: api as OpenClawPluginApi, tools };
|
||||
}
|
||||
|
||||
describe("feishu_doc account selection", () => {
|
||||
test("uses accountId param to pick correct account when multiple accounts configured", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
a: { appId: "app-a", appSecret: "sec-a", tools: { doc: true } },
|
||||
b: { appId: "app-b", appSecret: "sec-b", tools: { doc: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { api, tools } = fakeApi(cfg);
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const tool = tools.find((t) => t.name === "feishu_doc");
|
||||
expect(tool).toBeTruthy();
|
||||
|
||||
// Trigger a lightweight action (list_blocks) that will immediately attempt client creation.
|
||||
// It will still fail later due to missing SDK mocks, but we only care which account's creds were used.
|
||||
await tool!.execute("call-a", { action: "list_blocks", doc_token: "d", accountId: "a" });
|
||||
await tool!.execute("call-b", { action: "list_blocks", doc_token: "d", accountId: "b" });
|
||||
|
||||
expect(createFeishuClientMock).toHaveBeenCalledTimes(2);
|
||||
expect(createFeishuClientMock.mock.calls[0]?.[0]?.appId).toBe("app-a");
|
||||
expect(createFeishuClientMock.mock.calls[1]?.[0]?.appId).toBe("app-b");
|
||||
});
|
||||
|
||||
test("single-account setup still registers tool and uses that account", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
default: { appId: "app-d", appSecret: "sec-d", tools: { doc: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { api, tools } = fakeApi(cfg);
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const tool = tools.find((t) => t.name === "feishu_doc");
|
||||
expect(tool).toBeTruthy();
|
||||
|
||||
await tool!.execute("call-d", { action: "list_blocks", doc_token: "d" });
|
||||
expect(createFeishuClientMock).toHaveBeenCalled();
|
||||
expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-d");
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
@@ -454,15 +455,20 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use first account's config for tools configuration
|
||||
// Use first account's config for tools configuration (registration-time defaults only)
|
||||
const firstAccount = accounts[0];
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
const mediaMaxBytes = (firstAccount.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
||||
|
||||
// Helper to get client for the default account
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
const registered: string[] = [];
|
||||
|
||||
const resolveAccount = (params: FeishuDocParams) =>
|
||||
resolveFeishuAccount({ cfg: api.config!, accountId: (params as any).accountId });
|
||||
|
||||
const getClient = (params: FeishuDocParams) => createFeishuClient(resolveAccount(params));
|
||||
|
||||
const getMediaMaxBytes = (params: FeishuDocParams) =>
|
||||
(resolveAccount(params).config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
||||
|
||||
// Main document tool with action-based dispatch
|
||||
if (toolsCfg.doc) {
|
||||
api.registerTool(
|
||||
@@ -475,14 +481,14 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
async execute(_toolCallId, params) {
|
||||
const p = params as FeishuDocParams;
|
||||
try {
|
||||
const client = getClient();
|
||||
const client = getClient(p);
|
||||
switch (p.action) {
|
||||
case "read":
|
||||
return json(await readDoc(client, p.doc_token));
|
||||
case "write":
|
||||
return json(await writeDoc(client, p.doc_token, p.content, mediaMaxBytes));
|
||||
return json(await writeDoc(client, p.doc_token, p.content, getMediaMaxBytes(p)));
|
||||
case "append":
|
||||
return json(await appendDoc(client, p.doc_token, p.content, mediaMaxBytes));
|
||||
return json(await appendDoc(client, p.doc_token, p.content, getMediaMaxBytes(p)));
|
||||
case "create":
|
||||
return json(await createDoc(client, p.title, p.folder_token));
|
||||
case "list_blocks":
|
||||
|
||||
38
extensions/feishu/src/tool-context.ts
Normal file
38
extensions/feishu/src/tool-context.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { BeforeToolCallHook } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
|
||||
/**
|
||||
* If the calling agent has an account id, copy it into tool params as accountId
|
||||
* (unless the caller already provided one).
|
||||
*
|
||||
* This allows Feishu tools that are registered at startup (and therefore can't
|
||||
* capture a per-agent client) to select the right Feishu account at execution
|
||||
* time.
|
||||
*/
|
||||
export const resolveFeishuAccountForToolContext: BeforeToolCallHook = async (ctx) => {
|
||||
const toolName = ctx.toolName;
|
||||
if (typeof toolName !== "string" || !toolName.startsWith("feishu_")) {
|
||||
return { blocked: false, params: ctx.params };
|
||||
}
|
||||
|
||||
// If caller already specified an accountId, keep it.
|
||||
const existing = (ctx.params as Record<string, unknown> | undefined)?.accountId;
|
||||
if (typeof existing === "string" && existing.trim()) {
|
||||
return { blocked: false, params: ctx.params };
|
||||
}
|
||||
|
||||
const agentAccountId = ctx.agentAccountId;
|
||||
if (!agentAccountId) {
|
||||
// Backward-compatible: no agent account context => default account.
|
||||
return {
|
||||
blocked: false,
|
||||
params: { ...(ctx.params ?? {}), accountId: DEFAULT_ACCOUNT_ID },
|
||||
};
|
||||
}
|
||||
|
||||
const accountId = normalizeAccountId(agentAccountId);
|
||||
return {
|
||||
blocked: false,
|
||||
params: { ...(ctx.params ?? {}), accountId },
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user