mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: route feishu doc tools by agent account context (#27338) (thanks @AaronL725)
This commit is contained in:
@@ -52,7 +52,6 @@ const plugin = {
|
||||
register(api: OpenClawPluginApi) {
|
||||
setFeishuRuntime(api.runtime);
|
||||
api.registerChannel({ plugin: feishuPlugin });
|
||||
|
||||
registerFeishuDocTools(api);
|
||||
registerFeishuWikiTools(api);
|
||||
registerFeishuDriveTools(api);
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { registerFeishuDocTools } from "./docx.js";
|
||||
|
||||
const createFeishuClientMock = vi.fn((creds: any) => ({ __appId: creds?.appId }));
|
||||
const createFeishuClientMock = vi.fn((creds: { appId?: string } | undefined) => ({
|
||||
__appId: creds?.appId,
|
||||
}));
|
||||
|
||||
vi.mock("./client.js", () => {
|
||||
return {
|
||||
createFeishuClient: (creds: any) => createFeishuClientMock(creds),
|
||||
createFeishuClient: (creds: { appId?: string } | undefined) => 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.
|
||||
// Patch SDK import so tool execution can run without network concerns.
|
||||
vi.mock("@larksuiteoapi/node-sdk", () => {
|
||||
return {
|
||||
default: {},
|
||||
};
|
||||
});
|
||||
|
||||
function fakeApi(cfg: any) {
|
||||
const tools: Array<{ name: string; execute: (id: string, params: any) => any }> = [];
|
||||
type ToolLike = {
|
||||
name: string;
|
||||
execute: (toolCallId: string, params: unknown) => Promise<unknown>;
|
||||
};
|
||||
|
||||
type ToolContextLike = {
|
||||
agentAccountId?: string;
|
||||
};
|
||||
|
||||
type ToolFactoryLike = (ctx: ToolContextLike) => ToolLike | ToolLike[] | null | undefined;
|
||||
|
||||
function createApi(cfg: OpenClawPluginApi["config"]) {
|
||||
const registered: Array<{
|
||||
tool: ToolLike | ToolFactoryLike;
|
||||
opts?: { name?: string };
|
||||
}> = [];
|
||||
|
||||
const api: Partial<OpenClawPluginApi> = {
|
||||
config: cfg,
|
||||
logger: {
|
||||
@@ -28,16 +44,31 @@ function fakeApi(cfg: any) {
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
},
|
||||
registerTool: (tool: any) => {
|
||||
tools.push({ name: tool.name, execute: tool.execute });
|
||||
return undefined as any;
|
||||
registerTool: (tool, opts) => {
|
||||
registered.push({ tool, opts });
|
||||
},
|
||||
};
|
||||
return { api: api as OpenClawPluginApi, tools };
|
||||
|
||||
const resolveTool = (name: string, ctx: ToolContextLike): ToolLike => {
|
||||
const entry = registered.find((item) => item.opts?.name === name);
|
||||
if (!entry) {
|
||||
throw new Error(`Tool not registered: ${name}`);
|
||||
}
|
||||
if (typeof entry.tool === "function") {
|
||||
const built = entry.tool(ctx);
|
||||
if (!built || Array.isArray(built)) {
|
||||
throw new Error(`Unexpected tool factory output for ${name}`);
|
||||
}
|
||||
return built as ToolLike;
|
||||
}
|
||||
return entry.tool as ToolLike;
|
||||
};
|
||||
|
||||
return { api: api as OpenClawPluginApi, resolveTool };
|
||||
}
|
||||
|
||||
describe("feishu_doc account selection", () => {
|
||||
test("uses accountId param to pick correct account when multiple accounts configured", async () => {
|
||||
test("uses agentAccountId context when params omit accountId", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
feishu: {
|
||||
@@ -48,44 +79,45 @@ describe("feishu_doc account selection", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} as OpenClawPluginApi["config"];
|
||||
|
||||
const { api, tools } = fakeApi(cfg);
|
||||
const { api, resolveTool } = createApi(cfg);
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const tool = tools.find((t) => t.name === "feishu_doc");
|
||||
expect(tool).toBeTruthy();
|
||||
const docToolA = resolveTool("feishu_doc", { agentAccountId: "a" });
|
||||
const docToolB = resolveTool("feishu_doc", { agentAccountId: "b" });
|
||||
|
||||
// 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" });
|
||||
await docToolA.execute("call-a", { action: "list_blocks", doc_token: "d" });
|
||||
await docToolB.execute("call-b", { action: "list_blocks", doc_token: "d" });
|
||||
|
||||
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 () => {
|
||||
test("explicit accountId param overrides agentAccountId context", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
default: { appId: "app-d", appSecret: "sec-d", tools: { doc: true } },
|
||||
a: { appId: "app-a", appSecret: "sec-a", tools: { doc: true } },
|
||||
b: { appId: "app-b", appSecret: "sec-b", tools: { doc: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} as OpenClawPluginApi["config"];
|
||||
|
||||
const { api, tools } = fakeApi(cfg);
|
||||
const { api, resolveTool } = createApi(cfg);
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const tool = tools.find((t) => t.name === "feishu_doc");
|
||||
expect(tool).toBeTruthy();
|
||||
const docTool = resolveTool("feishu_doc", { agentAccountId: "b" });
|
||||
await docTool.execute("call-override", {
|
||||
action: "list_blocks",
|
||||
doc_token: "d",
|
||||
accountId: "a",
|
||||
});
|
||||
|
||||
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");
|
||||
expect(createFeishuClientMock.mock.calls.at(-1)?.[0]?.appId).toBe("app-a");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,7 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
|
||||
const feishuDocTool = registerTool.mock.calls
|
||||
.map((call) => call[0])
|
||||
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
||||
.find((tool) => tool.name === "feishu_doc");
|
||||
expect(feishuDocTool).toBeDefined();
|
||||
|
||||
|
||||
@@ -460,53 +460,83 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
|
||||
const registered: string[] = [];
|
||||
type FeishuDocExecuteParams = FeishuDocParams & { accountId?: string };
|
||||
|
||||
const resolveAccount = (params: FeishuDocParams) =>
|
||||
resolveFeishuAccount({ cfg: api.config!, accountId: (params as any).accountId });
|
||||
const resolveAccount = (
|
||||
params: { accountId?: string } | undefined,
|
||||
defaultAccountId: string | undefined,
|
||||
) => {
|
||||
const accountId =
|
||||
typeof params?.accountId === "string" && params.accountId.trim().length > 0
|
||||
? params.accountId.trim()
|
||||
: defaultAccountId;
|
||||
return resolveFeishuAccount({ cfg: api.config!, accountId });
|
||||
};
|
||||
|
||||
const getClient = (params: FeishuDocParams) => createFeishuClient(resolveAccount(params));
|
||||
const getClient = (params: { accountId?: string } | undefined, defaultAccountId?: string) =>
|
||||
createFeishuClient(resolveAccount(params, defaultAccountId));
|
||||
|
||||
const getMediaMaxBytes = (params: FeishuDocParams) =>
|
||||
(resolveAccount(params).config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
||||
const getMediaMaxBytes = (
|
||||
params: { accountId?: string } | undefined,
|
||||
defaultAccountId?: string,
|
||||
) => (resolveAccount(params, defaultAccountId).config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
||||
|
||||
// Main document tool with action-based dispatch
|
||||
if (toolsCfg.doc) {
|
||||
api.registerTool(
|
||||
{
|
||||
name: "feishu_doc",
|
||||
label: "Feishu Doc",
|
||||
description:
|
||||
"Feishu document operations. Actions: read, write, append, create, list_blocks, get_block, update_block, delete_block",
|
||||
parameters: FeishuDocSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const p = params as FeishuDocParams;
|
||||
try {
|
||||
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, getMediaMaxBytes(p)));
|
||||
case "append":
|
||||
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":
|
||||
return json(await listBlocks(client, p.doc_token));
|
||||
case "get_block":
|
||||
return json(await getBlock(client, p.doc_token, p.block_id));
|
||||
case "update_block":
|
||||
return json(await updateBlock(client, p.doc_token, p.block_id, p.content));
|
||||
case "delete_block":
|
||||
return json(await deleteBlock(client, p.doc_token, p.block_id));
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
||||
return json({ error: `Unknown action: ${(p as any).action}` });
|
||||
(ctx) => {
|
||||
const defaultAccountId = ctx.agentAccountId;
|
||||
return {
|
||||
name: "feishu_doc",
|
||||
label: "Feishu Doc",
|
||||
description:
|
||||
"Feishu document operations. Actions: read, write, append, create, list_blocks, get_block, update_block, delete_block",
|
||||
parameters: FeishuDocSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const p = params as FeishuDocExecuteParams;
|
||||
try {
|
||||
const client = getClient(p, defaultAccountId);
|
||||
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,
|
||||
getMediaMaxBytes(p, defaultAccountId),
|
||||
),
|
||||
);
|
||||
case "append":
|
||||
return json(
|
||||
await appendDoc(
|
||||
client,
|
||||
p.doc_token,
|
||||
p.content,
|
||||
getMediaMaxBytes(p, defaultAccountId),
|
||||
),
|
||||
);
|
||||
case "create":
|
||||
return json(await createDoc(client, p.title, p.folder_token));
|
||||
case "list_blocks":
|
||||
return json(await listBlocks(client, p.doc_token));
|
||||
case "get_block":
|
||||
return json(await getBlock(client, p.doc_token, p.block_id));
|
||||
case "update_block":
|
||||
return json(await updateBlock(client, p.doc_token, p.block_id, p.content));
|
||||
case "delete_block":
|
||||
return json(await deleteBlock(client, p.doc_token, p.block_id));
|
||||
default: {
|
||||
const exhaustiveCheck: never = p;
|
||||
return json({ error: `Unknown action: ${String(exhaustiveCheck)}` });
|
||||
}
|
||||
}
|
||||
} 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_doc" },
|
||||
);
|
||||
@@ -516,7 +546,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
// Keep feishu_app_scopes as independent tool
|
||||
if (toolsCfg.scopes) {
|
||||
api.registerTool(
|
||||
{
|
||||
(ctx) => ({
|
||||
name: "feishu_app_scopes",
|
||||
label: "Feishu App Scopes",
|
||||
description:
|
||||
@@ -524,13 +554,13 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
parameters: Type.Object({}),
|
||||
async execute() {
|
||||
try {
|
||||
const result = await listAppScopes(getClient({ action: "create", title: "" } as any));
|
||||
const result = await listAppScopes(getClient(undefined, ctx.agentAccountId));
|
||||
return json(result);
|
||||
} catch (err) {
|
||||
return json({ error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: "feishu_app_scopes" },
|
||||
);
|
||||
registered.push("feishu_app_scopes");
|
||||
|
||||
Reference in New Issue
Block a user