forked from EduCraft/curriculum-project-hub
fix: confine Agent and MCP data access
This commit is contained in:
+66
-16
@@ -6,8 +6,12 @@
|
||||
* reactions, file download/upload, and the card action callback.
|
||||
*/
|
||||
import * as lark from "@larksuiteoapi/node-sdk";
|
||||
import { readFile, mkdir } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type { Readable } from "node:stream";
|
||||
import {
|
||||
WorkspaceFileBoundaryError,
|
||||
writeNewWorkspaceFileNoFollow,
|
||||
} from "../security/workspaceFiles.js";
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import { DEFAULT_MAX_MESSAGE_LENGTH, splitAtBoundary } from "./textStream.js";
|
||||
import type { SenderNameCache } from "./senderCache.js";
|
||||
@@ -486,20 +490,38 @@ export async function downloadMessageFile(
|
||||
rt: FeishuRuntime,
|
||||
messageId: string,
|
||||
fileKey: string,
|
||||
savePath: string,
|
||||
workspaceRoot: string,
|
||||
workspaceDir: string,
|
||||
workspaceRelativePath: string,
|
||||
resourceType: "image" | "file",
|
||||
): Promise<void> {
|
||||
await mkdir(dirname(savePath), { recursive: true });
|
||||
): Promise<string> {
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
return await withRetry(async () => {
|
||||
const resource = await rt.client.im.v1.messageResource.get({
|
||||
params: { type: resourceType },
|
||||
path: { message_id: messageId, file_key: fileKey },
|
||||
});
|
||||
await resource.writeFile(savePath);
|
||||
const readableResource = resource as unknown as { getReadableStream: () => Readable };
|
||||
const source = readableResource.getReadableStream();
|
||||
try {
|
||||
return await writeNewWorkspaceFileNoFollow(
|
||||
workspaceRoot,
|
||||
workspaceDir,
|
||||
workspaceRelativePath,
|
||||
source,
|
||||
);
|
||||
} catch (error) {
|
||||
source.destroy();
|
||||
throw error;
|
||||
}
|
||||
}, {
|
||||
shouldRetry: (error) => !(error instanceof WorkspaceFileBoundaryError),
|
||||
});
|
||||
} catch (e) {
|
||||
rt.logger.error({ err: e, messageId, fileKey, resourceType, savePath }, "downloadMessageFile failed");
|
||||
rt.logger.error(
|
||||
{ err: e, messageId, fileKey, resourceType, workspaceRoot, workspaceDir, workspaceRelativePath },
|
||||
"downloadMessageFile failed",
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -512,33 +534,61 @@ export async function sendFile(
|
||||
fileName: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
return await sendFileData(rt, chatId, await readFile(filePath), fileName, options);
|
||||
} catch (error) {
|
||||
// Legacy callers model delivery as nullable. Keep that API while retaining
|
||||
// the complete failure object logged by sendFileData (or log read failure).
|
||||
if (!(error instanceof FeishuFileDeliveryError)) {
|
||||
rt.logger.error({ chatId, filePath, fileName, err: error }, "sendFile failed before upload");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeishuFileDeliveryError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = "FeishuFileDeliveryError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Upload and send an already-opened, no-follow file snapshot to a chat. */
|
||||
export async function sendFileData(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
fileData: Buffer,
|
||||
fileName: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string> {
|
||||
const client = rt.client as unknown as {
|
||||
im: { v1: {
|
||||
file: { create: (p: unknown) => Promise<FileCreateResponse> }
|
||||
} };
|
||||
};
|
||||
try {
|
||||
const buf = await readFile(filePath);
|
||||
const ext = fileName.split(".").pop() ?? "";
|
||||
const fileType = ext === "pdf" ? "pdf" : ext === "doc" ? "doc" : ext === "xls" ? "xls" : ext === "ppt" ? "ppt" : "stream";
|
||||
const uploadRes = await withRetry(async () =>
|
||||
client.im.v1.file.create({ data: { file_type: fileType, file_name: fileName, file: buf } }),
|
||||
client.im.v1.file.create({ data: { file_type: fileType, file_name: fileName, file: fileData } }),
|
||||
);
|
||||
const fileKey = fileKeyFromResponse(uploadRes);
|
||||
if (fileKey === undefined) {
|
||||
rt.logger.warn({ chatId, filePath }, "sendFile failed: upload response missing file_key");
|
||||
return null;
|
||||
throw new FeishuFileDeliveryError("Feishu file upload response is missing file_key");
|
||||
}
|
||||
const messageId = await withRetry(async () =>
|
||||
sendMessagePayload(rt, chatId, { msgType: "file", content: JSON.stringify({ file_key: fileKey }) }, options),
|
||||
);
|
||||
if (messageId === null) {
|
||||
rt.logger.warn({ chatId, filePath }, "sendFile failed: message response missing message_id");
|
||||
throw new FeishuFileDeliveryError("Feishu file message response is missing message_id");
|
||||
}
|
||||
return messageId;
|
||||
} catch (e) {
|
||||
rt.logger.warn({ chatId, filePath, err: e instanceof Error ? e.message : String(e) }, "sendFile failed");
|
||||
return null;
|
||||
} catch (error) {
|
||||
const failure = error instanceof FeishuFileDeliveryError
|
||||
? error
|
||||
: new FeishuFileDeliveryError("Feishu file delivery failed", { cause: error });
|
||||
rt.logger.error({ chatId, fileName, err: failure }, "sendFileData failed");
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user