mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 07:20:59 +00:00
229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
|
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
import {
|
|
jsonToolResult,
|
|
toolExecutionErrorResult,
|
|
unknownToolActionResult,
|
|
} from "./tool-result.js";
|
|
|
|
// ============ Actions ============
|
|
|
|
async function getRootFolderToken(client: Lark.Client): Promise<string> {
|
|
// Use generic HTTP client to call the root folder meta API
|
|
// as it's not directly exposed in the SDK
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accessing internal SDK property
|
|
const domain = (client as any).domain ?? "https://open.feishu.cn";
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accessing internal SDK property
|
|
const res = (await (client as any).httpInstance.get(
|
|
`${domain}/open-apis/drive/explorer/v2/root_folder/meta`,
|
|
)) as { code: number; msg?: string; data?: { token?: string } };
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg ?? "Failed to get root folder");
|
|
}
|
|
const token = res.data?.token;
|
|
if (!token) {
|
|
throw new Error("Root folder token not found");
|
|
}
|
|
return token;
|
|
}
|
|
|
|
async function listFolder(client: Lark.Client, folderToken?: string) {
|
|
// Filter out invalid folder_token values (empty, "0", etc.)
|
|
const validFolderToken = folderToken && folderToken !== "0" ? folderToken : undefined;
|
|
const res = await client.drive.file.list({
|
|
params: validFolderToken ? { folder_token: validFolderToken } : {},
|
|
});
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg);
|
|
}
|
|
|
|
return {
|
|
files:
|
|
res.data?.files?.map((f) => ({
|
|
token: f.token,
|
|
name: f.name,
|
|
type: f.type,
|
|
url: f.url,
|
|
created_time: f.created_time,
|
|
modified_time: f.modified_time,
|
|
owner_id: f.owner_id,
|
|
})) ?? [],
|
|
next_page_token: res.data?.next_page_token,
|
|
};
|
|
}
|
|
|
|
async function getFileInfo(client: Lark.Client, fileToken: string, folderToken?: string) {
|
|
// Use list with folder_token to find file info
|
|
const res = await client.drive.file.list({
|
|
params: folderToken ? { folder_token: folderToken } : {},
|
|
});
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg);
|
|
}
|
|
|
|
const file = res.data?.files?.find((f) => f.token === fileToken);
|
|
if (!file) {
|
|
throw new Error(`File not found: ${fileToken}`);
|
|
}
|
|
|
|
return {
|
|
token: file.token,
|
|
name: file.name,
|
|
type: file.type,
|
|
url: file.url,
|
|
created_time: file.created_time,
|
|
modified_time: file.modified_time,
|
|
owner_id: file.owner_id,
|
|
};
|
|
}
|
|
|
|
async function createFolder(client: Lark.Client, name: string, folderToken?: string) {
|
|
// Feishu supports using folder_token="0" as the root folder.
|
|
// We *try* to resolve the real root token (explorer API), but fall back to "0"
|
|
// because some tenants/apps return 400 for that explorer endpoint.
|
|
let effectiveToken = folderToken && folderToken !== "0" ? folderToken : "0";
|
|
if (effectiveToken === "0") {
|
|
try {
|
|
effectiveToken = await getRootFolderToken(client);
|
|
} catch {
|
|
// ignore and keep "0"
|
|
}
|
|
}
|
|
|
|
const res = await client.drive.file.createFolder({
|
|
data: {
|
|
name,
|
|
folder_token: effectiveToken,
|
|
},
|
|
});
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg);
|
|
}
|
|
|
|
return {
|
|
token: res.data?.token,
|
|
url: res.data?.url,
|
|
};
|
|
}
|
|
|
|
async function moveFile(client: Lark.Client, fileToken: string, type: string, folderToken: string) {
|
|
const res = await client.drive.file.move({
|
|
path: { file_token: fileToken },
|
|
data: {
|
|
type: type as
|
|
| "doc"
|
|
| "docx"
|
|
| "sheet"
|
|
| "bitable"
|
|
| "folder"
|
|
| "file"
|
|
| "mindnote"
|
|
| "slides",
|
|
folder_token: folderToken,
|
|
},
|
|
});
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
task_id: res.data?.task_id,
|
|
};
|
|
}
|
|
|
|
async function deleteFile(client: Lark.Client, fileToken: string, type: string) {
|
|
const res = await client.drive.file.delete({
|
|
path: { file_token: fileToken },
|
|
params: {
|
|
type: type as
|
|
| "doc"
|
|
| "docx"
|
|
| "sheet"
|
|
| "bitable"
|
|
| "folder"
|
|
| "file"
|
|
| "mindnote"
|
|
| "slides"
|
|
| "shortcut",
|
|
},
|
|
});
|
|
if (res.code !== 0) {
|
|
throw new Error(res.msg);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
task_id: res.data?.task_id,
|
|
};
|
|
}
|
|
|
|
// ============ Tool Registration ============
|
|
|
|
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
if (!api.config) {
|
|
api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
|
|
return;
|
|
}
|
|
|
|
const accounts = listEnabledFeishuAccounts(api.config);
|
|
if (accounts.length === 0) {
|
|
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
|
return;
|
|
}
|
|
|
|
const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts);
|
|
if (!toolsCfg.drive) {
|
|
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
|
return;
|
|
}
|
|
|
|
type FeishuDriveExecuteParams = FeishuDriveParams & { accountId?: string };
|
|
|
|
api.registerTool(
|
|
(ctx) => {
|
|
const defaultAccountId = ctx.agentAccountId;
|
|
return {
|
|
name: "feishu_drive",
|
|
label: "Feishu Drive",
|
|
description:
|
|
"Feishu cloud storage operations. Actions: list, info, create_folder, move, delete",
|
|
parameters: FeishuDriveSchema,
|
|
async execute(_toolCallId, params) {
|
|
const p = params as FeishuDriveExecuteParams;
|
|
try {
|
|
const client = createFeishuToolClient({
|
|
api,
|
|
executeParams: p,
|
|
defaultAccountId,
|
|
});
|
|
switch (p.action) {
|
|
case "list":
|
|
return jsonToolResult(await listFolder(client, p.folder_token));
|
|
case "info":
|
|
return jsonToolResult(await getFileInfo(client, p.file_token));
|
|
case "create_folder":
|
|
return jsonToolResult(await createFolder(client, p.name, p.folder_token));
|
|
case "move":
|
|
return jsonToolResult(await moveFile(client, p.file_token, p.type, p.folder_token));
|
|
case "delete":
|
|
return jsonToolResult(await deleteFile(client, p.file_token, p.type));
|
|
default:
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
|
return unknownToolActionResult((p as { action?: unknown }).action);
|
|
}
|
|
} catch (err) {
|
|
return toolExecutionErrorResult(err);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
{ name: "feishu_drive" },
|
|
);
|
|
|
|
api.logger.info?.(`feishu_drive: Registered feishu_drive tool`);
|
|
}
|