feat: wire wechat archive through langbot and n8n

This commit is contained in:
Codex
2026-06-13 04:32:52 +00:00
parent e47eca77ab
commit c818d0cd44
8 changed files with 653 additions and 78 deletions
@@ -1032,6 +1032,38 @@ async function createTransferJob(direction: TransferDirection, body: JsonRecord)
return { ok: true, job: transferFromRow(row) };
}
function safeStagingFilename(value: string, fallback: string): string {
const base = basename(value || fallback || "upload.txt").replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^_+|_+$/gu, "");
return base || fallback || "upload.txt";
}
function contentUploadBuffer(body: JsonRecord): { buffer: Buffer; source: string } {
if (typeof body.dataBase64 === "string" && body.dataBase64.length > 0) return { buffer: Buffer.from(body.dataBase64, "base64"), source: "dataBase64" };
if (typeof body.content === "string") return { buffer: Buffer.from(body.content, "utf8"), source: "content" };
throw new HttpError(400, "upload-content requires content or dataBase64");
}
async function createContentUpload(body: JsonRecord): Promise<JsonRecord> {
const { buffer, source } = contentUploadBuffer(body);
const maxBytes = Math.max(1, Math.min(10 * 1024 * 1024, asNumber(body.maxBytes, 10 * 1024 * 1024)));
if (buffer.byteLength > maxBytes) throw new HttpError(413, "upload-content body is too large", { maxBytes, bytes: buffer.byteLength });
const remotePath = remoteFilePath(String(body.remotePath || ""), safeStagingFilename(String(body.filename || ""), "upload.txt"));
const filename = safeStagingFilename(String(body.filename || pathPosix.basename(remotePath)), pathPosix.basename(remotePath) || "upload.txt");
const relativePath = pathPosix.join("content-uploads", `${Date.now()}-${randomUUID().slice(0, 8)}-${filename}`);
const localPath = resolveStagingPath(relativePath);
mkdirSync(dirname(localPath), { recursive: true });
await writeFile(localPath, buffer);
const sha256 = createHash("sha256").update(buffer).digest("hex");
const created = await createTransferJob("upload", { localPath: relativePath, remotePath });
return {
ok: created.ok === true,
source,
remotePath,
local: { relativePath, bytes: buffer.byteLength, sha256 },
job: asRecord(created.job),
};
}
async function computeMd5Blocks(filePath: string, jobId: string): Promise<{ size: number; fullMd5: string; blocks: string[] }> {
const info = await stat(filePath);
if (!info.isFile()) throw new HttpError(400, "localPath must be a file inside staging directory", { localPath: filePath });
@@ -1363,6 +1395,7 @@ async function route(req: Request): Promise<Response> {
if (path === "/api/folders" && req.method === "POST") return jsonResponse(await createFolder(await readJsonBody(req)));
if (path === "/api/files/manage" && req.method === "POST") return jsonResponse(await manageFiles(await readJsonBody(req)));
if (path === "/api/transfers/upload-from-path" && req.method === "POST") return jsonResponse(await createTransferJob("upload", await readJsonBody(req)));
if (path === "/api/transfers/upload-content" && req.method === "POST") return jsonResponse(await createContentUpload(await readJsonBody(req)));
if (path === "/api/transfers/download-to-path" && req.method === "POST") return jsonResponse(await createTransferJob("download", await readJsonBody(req)));
if (path === "/api/self-test" && req.method === "POST") return jsonResponse(await runSelfTest(await readJsonBody(req)));
if (path === "/api/transfers" && req.method === "GET") return jsonResponse(await listTransfers(url));