Makelore 2.0 initial clean snapshot
This commit is contained in:
80
electron/main/ipc/host-api-proxy.ts
Normal file
80
electron/main/ipc/host-api-proxy.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { getPort } from '../../utils/config';
|
||||
import { getHostApiToken } from '../../api/server';
|
||||
|
||||
type HostApiFetchRequest = {
|
||||
path: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
export function registerHostApiProxyHandlers(): void {
|
||||
const hostApiPort = getPort('NIANCODE_HOST_API');
|
||||
|
||||
// Expose the per-session auth token to the renderer so the browser-fallback
|
||||
// path in host-api.ts can authenticate against the Host API server.
|
||||
ipcMain.handle('hostapi:token', () => getHostApiToken());
|
||||
ipcMain.handle('hostapi:base-url', () => `http://127.0.0.1:${hostApiPort}`);
|
||||
|
||||
ipcMain.handle('hostapi:fetch', async (_, request: HostApiFetchRequest) => {
|
||||
try {
|
||||
const path = typeof request?.path === 'string' ? request.path : '';
|
||||
if (!path || !path.startsWith('/')) {
|
||||
throw new Error(`Invalid host API path: ${String(request?.path)}`);
|
||||
}
|
||||
|
||||
const method = (request.method || 'GET').toUpperCase();
|
||||
const headers: Record<string, string> = { ...(request.headers || {}) };
|
||||
// Inject the per-session auth token so the Host API server accepts this request.
|
||||
headers['Authorization'] = `Bearer ${getHostApiToken()}`;
|
||||
let body: string | undefined;
|
||||
|
||||
if (request.body !== undefined && request.body !== null) {
|
||||
if (typeof request.body === 'string') {
|
||||
body = request.body;
|
||||
} else {
|
||||
body = JSON.stringify(request.body);
|
||||
}
|
||||
// Ensure Content-Type is set for requests with a body so the
|
||||
// server's anti-CSRF Content-Type gate does not reject them.
|
||||
if (!headers['Content-Type'] && !headers['content-type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
// The Host API is an in-process loopback server. Using Electron's
|
||||
// proxy-aware network stack here can incorrectly send 127.0.0.1 through
|
||||
// a configured system proxy and surface a generic `fetch failed` before
|
||||
// the request ever reaches the local route.
|
||||
const response = await fetch(`http://127.0.0.1:${hostApiPort}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const data: { status: number; ok: boolean; json?: unknown; text?: string } = {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
};
|
||||
|
||||
if (response.status !== 204) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
data.json = await response.json().catch(() => undefined);
|
||||
} else {
|
||||
data.text = await response.text().catch(() => '');
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, data };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
45
electron/main/ipc/transcript-export.ts
Normal file
45
electron/main/ipc/transcript-export.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { dialog, ipcMain, type BrowserWindow } from 'electron';
|
||||
|
||||
interface TranscriptSaveRequest {
|
||||
defaultPath?: unknown;
|
||||
markdown?: unknown;
|
||||
}
|
||||
|
||||
const MAX_TRANSCRIPT_MARKDOWN_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
function safeDefaultPath(value: unknown): string {
|
||||
if (typeof value !== 'string') return 'chat-transcript.md';
|
||||
const fileName = value.split(/[\\/]/).filter(Boolean).at(-1)?.trim();
|
||||
if (!fileName) return 'chat-transcript.md';
|
||||
return fileName.toLowerCase().endsWith('.md') ? fileName : `${fileName}.md`;
|
||||
}
|
||||
|
||||
export function registerTranscriptExportHandler(mainWindow: BrowserWindow): void {
|
||||
ipcMain.handle('transcript:save', async (_event, request: TranscriptSaveRequest) => {
|
||||
if (typeof request?.markdown !== 'string') {
|
||||
throw new Error('Invalid transcript markdown');
|
||||
}
|
||||
if (Buffer.byteLength(request.markdown, 'utf8') > MAX_TRANSCRIPT_MARKDOWN_BYTES) {
|
||||
throw new Error('Transcript is too large');
|
||||
}
|
||||
|
||||
let selected: Electron.SaveDialogReturnValue;
|
||||
try {
|
||||
selected = await dialog.showSaveDialog(mainWindow, {
|
||||
defaultPath: safeDefaultPath(request.defaultPath),
|
||||
filters: [{ name: 'Markdown', extensions: ['md'] }],
|
||||
});
|
||||
} catch {
|
||||
throw new Error('Failed to save transcript');
|
||||
}
|
||||
if (selected.canceled || !selected.filePath) return { status: 'cancelled' as const };
|
||||
|
||||
try {
|
||||
await writeFile(selected.filePath, request.markdown, 'utf8');
|
||||
} catch {
|
||||
throw new Error('Failed to save transcript');
|
||||
}
|
||||
return { status: 'saved' as const };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user