import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js"; import { resolveToolsConfig } from "./tools-config.js"; // ============ Helpers ============ function json(data: unknown) { return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }], details: data, }; } // ============ Actions ============ async function getRootFolderToken(client: Lark.Client): Promise { // 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 firstAccount = accounts[0]; const toolsCfg = resolveToolsConfig(firstAccount.config.tools); if (!toolsCfg.drive) { api.logger.debug?.("feishu_drive: drive tool disabled in config"); return; } const getClient = () => createFeishuClient(firstAccount); api.registerTool( { 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 FeishuDriveParams; try { const client = getClient(); switch (p.action) { case "list": return json(await listFolder(client, p.folder_token)); case "info": return json(await getFileInfo(client, p.file_token)); case "create_folder": return json(await createFolder(client, p.name, p.folder_token)); case "move": return json(await moveFile(client, p.file_token, p.type, p.folder_token)); case "delete": return json(await deleteFile(client, p.file_token, p.type)); default: // eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback return json({ error: `Unknown action: ${(p as any).action}` }); } } catch (err) { return json({ error: err instanceof Error ? err.message : String(err) }); } }, }, { name: "feishu_drive" }, ); api.logger.info?.(`feishu_drive: Registered feishu_drive tool`); }