Files
mai-bot/dashboard/electron/main/protocol.ts
DrSmoothl b5cc361ce9 feat(electron): add main process, preload, store, protocol, and type definitions
- Add ElectronAPI type definitions and runtime contract (isElectron guard)
- Add electron-store with backend connection data model
- Add centralized API base URL utility (api-base.ts)
- Implement app:// custom protocol with API proxy
- Implement preload script with full contextBridge API
- Complete main process: BrowserWindow config, IPC handlers, window controls
- Register app:// scheme as privileged for secure renderer access
2026-03-03 00:57:50 +08:00

90 lines
2.6 KiB
TypeScript

import { net, protocol } from 'electron'
import { readFile } from 'fs/promises'
import { dirname, extname, join } from 'path'
import { fileURLToPath } from 'url'
import { getActiveBackend } from './store'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const MIME_TYPES: Record<string, string> = {
'.html': 'text/html',
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.cjs': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.txt': 'text/plain',
'.webp': 'image/webp',
}
export function registerAppProtocol(): void {
protocol.handle('app', async (request) => {
const url = new URL(request.url)
const pathname = url.pathname
if (pathname.startsWith('/api/')) {
const backend = getActiveBackend()
const targetUrl = backend
? `${backend.url.replace(/\/$/, '')}${pathname}${url.search}`
: null
if (!targetUrl) {
return new Response(JSON.stringify({ error: 'No backend configured' }), {
status: 503,
headers: { 'Content-Type': 'application/json' },
})
}
const headers = new Headers(request.headers)
headers.delete('host')
return net.fetch(targetUrl, {
method: request.method,
headers,
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
duplex: 'half',
})
}
// Dev mode: renderer is served by vite dev server, not app:// protocol
if (process.env.ELECTRON_RENDERER_URL) {
return new Response(null, { status: 204 })
}
const rendererDir = join(__dirname, '../renderer')
const safePath = decodeURIComponent(pathname)
.replace(/\.\./g, '')
.replace(/^\/+/, '')
const resolvedPath = safePath === '' ? 'index.html' : safePath
const filePath = resolvedPath.endsWith('/')
? join(rendererDir, resolvedPath, 'index.html')
: join(rendererDir, resolvedPath)
const tryReadFile = async (path: string) => {
const ext = extname(path)
const mimeType = MIME_TYPES[ext] ?? 'application/octet-stream'
const data = await readFile(path)
return new Response(data, { headers: { 'Content-Type': mimeType } })
}
try {
return await tryReadFile(filePath)
} catch {
const indexPath = join(rendererDir, 'index.html')
return tryReadFile(indexPath)
}
})
}