Files
zn-ai/electron/api/routes/files.ts
duanshuwen ee72cf7261 feat: refactor HomePage to integrate agents store and update related components
feat: add runtime event handling for providers in ProvidersSection

feat: update routing to include Channels and Agents pages

feat: extend route types and navigation items for Channels and Agents

feat: implement agents store for managing agent data and interactions

fix: update chat store to utilize agents store for agent-related functionality

chore: export agents store from index

fix: enhance runtime types for better event handling

fix: update Vite config to handle dev server URL correctly
2026-04-18 14:56:32 +08:00

117 lines
3.5 KiB
TypeScript

import crypto from 'node:crypto';
import { nativeImage } from 'electron';
import { extname, join } from 'node:path';
import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils';
import { ok, parseJsonBody } from '../route-utils';
import { getUserDataDir } from '@electron/utils/paths';
const EXT_MIME_MAP: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.bmp': 'image/bmp',
'.txt': 'text/plain',
'.json': 'application/json',
'.pdf': 'application/pdf',
};
function getMimeType(ext: string): string {
return EXT_MIME_MAP[ext.toLowerCase()] || 'application/octet-stream';
}
function mimeToExt(mimeType: string): string {
for (const [ext, mime] of Object.entries(EXT_MIME_MAP)) {
if (mime === mimeType) return ext;
}
return '';
}
const OUTBOUND_DIR = join(getUserDataDir(), 'openclaw-media', 'outbound');
async function generateImagePreview(filePath: string, mimeType: string): Promise<string | null> {
try {
const image = nativeImage.createFromPath(filePath);
if (image.isEmpty()) return null;
const size = image.getSize();
const maxDim = 512;
const normalized = size.width > maxDim || size.height > maxDim
? (size.width >= size.height ? image.resize({ width: maxDim }) : image.resize({ height: maxDim }))
: image;
return `data:${mimeType};base64,${normalized.toPNG().toString('base64')}`;
} catch {
return null;
}
}
export async function handleFileRoutes(
request: NormalizedHostApiRequest,
_ctx: HostApiContext,
) {
const { pathname, method } = request;
if (pathname === '/api/files/stage-buffer' && method === 'POST') {
const body = parseJsonBody<{ base64: string; fileName: string; mimeType: string }>(request.body);
const fsP = await import('node:fs/promises');
await fsP.mkdir(OUTBOUND_DIR, { recursive: true });
const id = crypto.randomUUID();
const ext = extname(body.fileName) || mimeToExt(body.mimeType);
const stagedPath = join(OUTBOUND_DIR, `${id}${ext}`);
const buffer = Buffer.from(body.base64 || '', 'base64');
await fsP.writeFile(stagedPath, buffer);
const mimeType = body.mimeType || getMimeType(ext);
const preview = mimeType.startsWith('image/')
? await generateImagePreview(stagedPath, mimeType)
: null;
return ok({
id,
fileName: body.fileName,
mimeType,
fileSize: buffer.length,
stagedPath,
preview,
});
}
if (pathname === '/api/files/stage-paths' && method === 'POST') {
const body = parseJsonBody<{ filePaths: string[] }>(request.body);
const fsP = await import('node:fs/promises');
await fsP.mkdir(OUTBOUND_DIR, { recursive: true });
const results = [];
for (const filePath of body.filePaths || []) {
const id = crypto.randomUUID();
const ext = extname(filePath);
const stagedPath = join(OUTBOUND_DIR, `${id}${ext}`);
await fsP.copyFile(filePath, stagedPath);
const stats = await fsP.stat(stagedPath);
const mimeType = getMimeType(ext);
const fileName = filePath.split(/[\\/]/).pop() || 'file';
const preview = mimeType.startsWith('image/')
? await generateImagePreview(stagedPath, mimeType)
: null;
results.push({
id,
fileName,
mimeType,
fileSize: stats.size,
stagedPath,
preview,
});
}
return ok(results);
}
return null;
}