- Sync latest changes from clawdbot-feishu including multi-account support - Add eslint-disable comments for SDK-related any types - Remove unused imports - Fix no-floating-promises in monitor.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
228 lines
6.6 KiB
TypeScript
228 lines
6.6 KiB
TypeScript
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<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 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`);
|
|
}
|