import { randomUUID } from "node:crypto"; import { join } from "node:path"; import type { ToolContext } from "../agent/tools.js"; import { downloadMessageFile, type FeishuRuntime } from "./client.js"; export interface FeishuMessageResourceArgs { readonly messageId: string; readonly fileKey: string; readonly resourceType: "image" | "file"; } export interface DownloadedFeishuMessageResource extends FeishuMessageResourceArgs { readonly path: string; } type DownloadContext = Pick & { readonly workspaceRoot: string }; interface MessageLookupResult { readonly data?: { readonly message?: { readonly chat_id?: string | undefined } | undefined; readonly items?: ReadonlyArray<{ readonly chat_id?: string | undefined }> | undefined; } | undefined; } /** Download one resource after proving its message belongs to the run's bound chat. */ export async function downloadFeishuMessageResource( args: FeishuMessageResourceArgs, context: DownloadContext, rt: FeishuRuntime, ): Promise { const response = await rt.client.im.v1.message.get({ path: { message_id: args.messageId }, }) as MessageLookupResult; const message = response.data?.message ?? response.data?.items?.[0]; if (message === undefined) { throw new Error(`Feishu message not found: ${args.messageId}`); } if (message.chat_id !== context.boundChatId) { rt.logger.warn( { messageId: args.messageId, boundChatId: context.boundChatId, returnedChatId: message.chat_id }, "Feishu resource download refused outside bound chat", ); throw new Error( `refused Feishu resource download: message ${args.messageId} is not in the current project's bound chat`, ); } const extension = args.resourceType === "image" ? ".png" : ".bin"; const workspaceRelativePath = join( ".cph", "inbox", `feishu-${args.resourceType}-${randomUUID()}${extension}`, ); const savePath = await downloadMessageFile( rt, args.messageId, args.fileKey, context.workspaceRoot, context.workspaceDir, workspaceRelativePath, args.resourceType, ); return { ...args, path: savePath }; }