Merge 7b9351f5adc1e22c88435cf71773dc85eabc20f2 into 9fb78453e088cd7b553d7779faa0de5c83708e70
This commit is contained in:
commit
507c5eb9f9
37
extensions/feishu/index.secretref.test.ts
Normal file
37
extensions/feishu/index.secretref.test.ts
Normal file
@ -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: "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();
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
listEnabledFeishuAccountConfigs,
|
||||
resolveDefaultFeishuAccountId,
|
||||
resolveDefaultFeishuAccountSelection,
|
||||
resolveFeishuAccount,
|
||||
@ -369,3 +370,82 @@ describe("resolveFeishuAccount", () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("listEnabledFeishuAccountConfigs", () => {
|
||||
it("treats SecretRef-backed accounts as configured without resolving them", () => {
|
||||
const accounts = listEnabledFeishuAccountConfigs({
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
appId: "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 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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: {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
@ -95,8 +100,62 @@ 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;
|
||||
}
|
||||
|
||||
export 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(
|
||||
normalizeSecretInputString(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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,48 +251,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.
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)" })),
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
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 { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
import {
|
||||
createFeishuToolClient,
|
||||
isFeishuToolEnabledForRoutedAccount,
|
||||
resolveAnyEnabledFeishuToolsConfig,
|
||||
resolveFeishuToolAccountConfigState,
|
||||
} from "./tool-account.js";
|
||||
|
||||
function json(data: unknown) {
|
||||
return {
|
||||
@ -126,64 +130,87 @@ 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 toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts);
|
||||
if (!toolsCfg.chat) {
|
||||
api.logger.debug?.("feishu_chat: chat tool disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
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 = getClient();
|
||||
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 account = resolveFeishuToolAccountConfigState({
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId,
|
||||
});
|
||||
if (
|
||||
!isFeishuToolEnabledForRoutedAccount({
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId,
|
||||
tool: "chat",
|
||||
})
|
||||
) {
|
||||
return json({
|
||||
error: `Feishu chat is disabled for account "${account.accountId}".`,
|
||||
});
|
||||
}
|
||||
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" },
|
||||
);
|
||||
|
||||
@ -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", () => {
|
||||
@ -67,4 +77,68 @@ 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();
|
||||
});
|
||||
|
||||
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".',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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";
|
||||
@ -20,7 +20,9 @@ import {
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import {
|
||||
createFeishuToolClient,
|
||||
isFeishuToolEnabledForRoutedAccount,
|
||||
resolveAnyEnabledFeishuToolsConfig,
|
||||
resolveFeishuToolAccountConfigState,
|
||||
resolveFeishuToolAccount,
|
||||
} from "./tool-account.js";
|
||||
|
||||
@ -1233,7 +1235,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;
|
||||
@ -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":
|
||||
@ -1439,10 +1458,30 @@ 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 result = await listAppScopes(getClient(undefined, ctx.agentAccountId));
|
||||
const account = resolveFeishuToolAccountConfigState({
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId: ctx.agentAccountId,
|
||||
});
|
||||
if (
|
||||
!isFeishuToolEnabledForRoutedAccount({
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId: ctx.agentAccountId,
|
||||
tool: "scopes",
|
||||
})
|
||||
) {
|
||||
return json({
|
||||
error: `Feishu scopes are disabled for account "${account.accountId}".`,
|
||||
});
|
||||
}
|
||||
const result = await listAppScopes(getClient(p, ctx.agentAccountId));
|
||||
return json(result);
|
||||
} catch (err) {
|
||||
return json({ error: err instanceof Error ? err.message : String(err) });
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
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 {
|
||||
createFeishuToolClient,
|
||||
isFeishuToolEnabledForRoutedAccount,
|
||||
resolveAnyEnabledFeishuToolsConfig,
|
||||
resolveFeishuToolAccountConfigState,
|
||||
} from "./tool-account.js";
|
||||
import {
|
||||
jsonToolResult,
|
||||
toolExecutionErrorResult,
|
||||
@ -169,7 +174,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;
|
||||
@ -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,
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
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 {
|
||||
createFeishuToolClient,
|
||||
isFeishuToolEnabledForRoutedAccount,
|
||||
resolveAnyEnabledFeishuToolsConfig,
|
||||
resolveFeishuToolAccountConfigState,
|
||||
} from "./tool-account.js";
|
||||
import {
|
||||
jsonToolResult,
|
||||
toolExecutionErrorResult,
|
||||
@ -118,7 +123,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;
|
||||
@ -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,
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import type { OpenClawPluginApi } from "../runtime-api.js";
|
||||
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,101 @@ 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("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({
|
||||
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("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({
|
||||
|
||||
@ -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<OpenClawPluginApi, "config">;
|
||||
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<OpenClawPluginApi, "config">;
|
||||
executeParams?: AccountAwareParams;
|
||||
defaultAccountId?: string;
|
||||
tool: keyof Required<FeishuToolsConfig>;
|
||||
}): boolean {
|
||||
const account = resolveFeishuToolAccountConfigState(params);
|
||||
return resolveToolsConfig(account.config.tools)[params.tool];
|
||||
}
|
||||
|
||||
export function createFeishuToolClient(params: {
|
||||
api: Pick<OpenClawPluginApi, "config">;
|
||||
executeParams?: AccountAwareParams;
|
||||
@ -47,7 +74,7 @@ export function createFeishuToolClient(params: {
|
||||
}
|
||||
|
||||
export function resolveAnyEnabledFeishuToolsConfig(
|
||||
accounts: ResolvedFeishuAccount[],
|
||||
accounts: Array<{ config: { tools?: FeishuToolsConfig } }>,
|
||||
): Required<FeishuToolsConfig> {
|
||||
const merged: Required<FeishuToolsConfig> = {
|
||||
doc: false,
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "../runtime-api.js";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
||||
import { listEnabledFeishuAccountConfigs } from "./accounts.js";
|
||||
import {
|
||||
createFeishuToolClient,
|
||||
isFeishuToolEnabledForRoutedAccount,
|
||||
resolveAnyEnabledFeishuToolsConfig,
|
||||
resolveFeishuToolAccountConfigState,
|
||||
} from "./tool-account.js";
|
||||
import {
|
||||
jsonToolResult,
|
||||
toolExecutionErrorResult,
|
||||
@ -157,7 +162,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;
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user