* refactor: migrate chat history virtualization to virtua * refactor: render loaded chat history directly * refactor: finish virtua migration * perf: defer tool body rendering * perf: queue deferred tool body mounts * perf: quiet and defer markdown file probes * perf: defer markdown code highlighting * perf: stabilize markdown plugin lists * perf: defer mermaid markdown rendering * perf: delay markdown file reference annotation * perf: attach markdown table listeners on demand * perf: trim markdown render overhead
148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
import * as fs from 'fs';
|
|
import { getFsMimeType, normalizeFsPath, resolveFileReadPath, type FsReadPathResolution } from './bridge-fs-helpers-runtime';
|
|
|
|
type ApiProxyResponsePayload = {
|
|
status: number;
|
|
headers: Record<string, string>;
|
|
bodyBase64: string;
|
|
};
|
|
|
|
export const base64EncodeUtf8 = (text: string) => Buffer.from(text, 'utf8').toString('base64');
|
|
|
|
export const collectHeaders = (headers: Headers): Record<string, string> => {
|
|
const result: Record<string, string> = {};
|
|
headers.forEach((value, key) => {
|
|
result[key] = value;
|
|
});
|
|
return result;
|
|
};
|
|
|
|
export const buildUnavailableApiResponse = (): ApiProxyResponsePayload => {
|
|
const body = JSON.stringify({ error: 'OpenCode API unavailable' });
|
|
return {
|
|
status: 503,
|
|
headers: { 'content-type': 'application/json' },
|
|
bodyBase64: base64EncodeUtf8(body),
|
|
};
|
|
};
|
|
|
|
export const sanitizeForwardHeaders = (input: Record<string, string> | undefined): Record<string, string> => {
|
|
const headers: Record<string, string> = { ...(input || {}) };
|
|
delete headers['content-length'];
|
|
delete headers['host'];
|
|
delete headers['connection'];
|
|
return headers;
|
|
};
|
|
|
|
const buildProxyJsonError = (status: number, error: string): ApiProxyResponsePayload => ({
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
bodyBase64: base64EncodeUtf8(JSON.stringify({ error })),
|
|
});
|
|
|
|
const normalizeFsProxyPath = (pathname: string): '/api/fs/stat' | '/api/fs/read' | '/api/fs/raw' | null => {
|
|
if (pathname === '/api/fs/stat' || pathname === '/fs/stat') return '/api/fs/stat';
|
|
if (pathname === '/api/fs/read' || pathname === '/fs/read') return '/api/fs/read';
|
|
if (pathname === '/api/fs/raw' || pathname === '/fs/raw') return '/api/fs/raw';
|
|
return null;
|
|
};
|
|
|
|
export const tryHandleLocalFsProxy = async (method: string, requestPath: string): Promise<ApiProxyResponsePayload | null> => {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(requestPath, 'https://openchamber.local');
|
|
} catch {
|
|
return buildProxyJsonError(400, 'Invalid request path');
|
|
}
|
|
|
|
const fsProxyPath = normalizeFsProxyPath(parsed.pathname);
|
|
if (!fsProxyPath) {
|
|
return null;
|
|
}
|
|
|
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
return buildProxyJsonError(405, 'Method not allowed');
|
|
}
|
|
|
|
const targetPath = parsed.searchParams.get('path') || '';
|
|
const optional = parsed.searchParams.get('optional') === 'true';
|
|
const resolution: FsReadPathResolution = await resolveFileReadPath(targetPath);
|
|
if (!resolution.ok) {
|
|
if (fsProxyPath === '/api/fs/stat' && optional && resolution.status === 404) {
|
|
return {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'cache-control': 'no-store',
|
|
},
|
|
bodyBase64: base64EncodeUtf8(JSON.stringify({ path: targetPath, exists: false })),
|
|
};
|
|
}
|
|
return buildProxyJsonError(resolution.status, resolution.error);
|
|
}
|
|
|
|
try {
|
|
const stats = await fs.promises.stat(resolution.resolvedPath);
|
|
if (!stats.isFile()) {
|
|
return buildProxyJsonError(400, 'Specified path is not a file');
|
|
}
|
|
|
|
if (fsProxyPath === '/api/fs/stat') {
|
|
return {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'cache-control': 'no-store',
|
|
},
|
|
bodyBase64: base64EncodeUtf8(JSON.stringify({
|
|
path: normalizeFsPath(resolution.resolvedPath),
|
|
isFile: true,
|
|
size: stats.size,
|
|
mtimeMs: stats.mtimeMs,
|
|
})),
|
|
};
|
|
}
|
|
|
|
if (fsProxyPath === '/api/fs/read') {
|
|
const content = await fs.promises.readFile(resolution.resolvedPath, 'utf8');
|
|
return {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'cache-control': 'no-store',
|
|
},
|
|
bodyBase64: base64EncodeUtf8(content),
|
|
};
|
|
}
|
|
|
|
const raw = await fs.promises.readFile(resolution.resolvedPath);
|
|
return {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': getFsMimeType(resolution.resolvedPath),
|
|
'cache-control': 'no-store',
|
|
},
|
|
bodyBase64: Buffer.from(raw).toString('base64'),
|
|
};
|
|
} catch (error) {
|
|
const err = error as NodeJS.ErrnoException;
|
|
if (err?.code === 'ENOENT') {
|
|
if (fsProxyPath === '/api/fs/stat' && optional) {
|
|
return {
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'cache-control': 'no-store',
|
|
},
|
|
bodyBase64: base64EncodeUtf8(JSON.stringify({ path: targetPath, exists: false })),
|
|
};
|
|
}
|
|
return buildProxyJsonError(404, 'File not found');
|
|
}
|
|
if (fsProxyPath === '/api/fs/stat') {
|
|
return buildProxyJsonError(500, 'Unable to stat file');
|
|
}
|
|
return buildProxyJsonError(500, 'Unable to read file');
|
|
}
|
|
};
|