mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
213 lines
6.0 KiB
TypeScript
213 lines
6.0 KiB
TypeScript
|
|
import { app, BrowserWindow, dialog, ipcMain } from "electron"
|
||
|
|
import {
|
||
|
|
applyPresetToEnv,
|
||
|
|
type ConfigPreset,
|
||
|
|
createPreset,
|
||
|
|
deletePreset,
|
||
|
|
getAllPresets,
|
||
|
|
getCurrentPreset,
|
||
|
|
getCurrentPresetId,
|
||
|
|
setCurrentPreset,
|
||
|
|
updatePreset,
|
||
|
|
} from "./config-manager"
|
||
|
|
import { restartNextServer } from "./next-server"
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Allowed configuration keys for presets
|
||
|
|
* This whitelist prevents arbitrary environment variable injection
|
||
|
|
*/
|
||
|
|
const ALLOWED_CONFIG_KEYS = new Set([
|
||
|
|
"AI_PROVIDER",
|
||
|
|
"AI_MODEL",
|
||
|
|
"AI_API_KEY",
|
||
|
|
"AI_BASE_URL",
|
||
|
|
"TEMPERATURE",
|
||
|
|
])
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sanitize preset config to only include allowed keys
|
||
|
|
*/
|
||
|
|
function sanitizePresetConfig(
|
||
|
|
config: Record<string, string | undefined>,
|
||
|
|
): Record<string, string | undefined> {
|
||
|
|
const sanitized: Record<string, string | undefined> = {}
|
||
|
|
for (const key of ALLOWED_CONFIG_KEYS) {
|
||
|
|
if (key in config && typeof config[key] === "string") {
|
||
|
|
sanitized[key] = config[key]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return sanitized
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register all IPC handlers
|
||
|
|
*/
|
||
|
|
export function registerIpcHandlers(): void {
|
||
|
|
// ==================== App Info ====================
|
||
|
|
|
||
|
|
ipcMain.handle("get-version", () => {
|
||
|
|
return app.getVersion()
|
||
|
|
})
|
||
|
|
|
||
|
|
// ==================== Window Controls ====================
|
||
|
|
|
||
|
|
ipcMain.on("window-minimize", (event) => {
|
||
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||
|
|
win?.minimize()
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.on("window-maximize", (event) => {
|
||
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||
|
|
if (win?.isMaximized()) {
|
||
|
|
win.unmaximize()
|
||
|
|
} else {
|
||
|
|
win?.maximize()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.on("window-close", (event) => {
|
||
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||
|
|
win?.close()
|
||
|
|
})
|
||
|
|
|
||
|
|
// ==================== File Dialogs ====================
|
||
|
|
|
||
|
|
ipcMain.handle("dialog-open-file", async (event) => {
|
||
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||
|
|
if (!win) return null
|
||
|
|
|
||
|
|
const result = await dialog.showOpenDialog(win, {
|
||
|
|
properties: ["openFile"],
|
||
|
|
filters: [
|
||
|
|
{ name: "Draw.io Files", extensions: ["drawio", "xml"] },
|
||
|
|
{ name: "All Files", extensions: ["*"] },
|
||
|
|
],
|
||
|
|
})
|
||
|
|
|
||
|
|
if (result.canceled || result.filePaths.length === 0) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
// Read the file content
|
||
|
|
const fs = await import("node:fs/promises")
|
||
|
|
try {
|
||
|
|
const content = await fs.readFile(result.filePaths[0], "utf-8")
|
||
|
|
return content
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to read file:", error)
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.handle("dialog-save-file", async (event, data: string) => {
|
||
|
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||
|
|
if (!win) return false
|
||
|
|
|
||
|
|
const result = await dialog.showSaveDialog(win, {
|
||
|
|
filters: [
|
||
|
|
{ name: "Draw.io Files", extensions: ["drawio"] },
|
||
|
|
{ name: "XML Files", extensions: ["xml"] },
|
||
|
|
],
|
||
|
|
})
|
||
|
|
|
||
|
|
if (result.canceled || !result.filePath) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
const fs = await import("node:fs/promises")
|
||
|
|
try {
|
||
|
|
await fs.writeFile(result.filePath, data, "utf-8")
|
||
|
|
return true
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to save file:", error)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// ==================== Config Presets ====================
|
||
|
|
|
||
|
|
ipcMain.handle("config-presets:get-all", () => {
|
||
|
|
return getAllPresets()
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.handle("config-presets:get-current", () => {
|
||
|
|
return getCurrentPreset()
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.handle("config-presets:get-current-id", () => {
|
||
|
|
return getCurrentPresetId()
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.handle(
|
||
|
|
"config-presets:save",
|
||
|
|
(
|
||
|
|
_event,
|
||
|
|
preset: Omit<ConfigPreset, "id" | "createdAt" | "updatedAt"> & {
|
||
|
|
id?: string
|
||
|
|
},
|
||
|
|
) => {
|
||
|
|
// Validate preset name
|
||
|
|
if (typeof preset.name !== "string" || !preset.name.trim()) {
|
||
|
|
throw new Error("Invalid preset name")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sanitize config to only allow whitelisted keys
|
||
|
|
const sanitizedConfig = sanitizePresetConfig(preset.config ?? {})
|
||
|
|
|
||
|
|
if (preset.id) {
|
||
|
|
// Update existing preset
|
||
|
|
return updatePreset(preset.id, {
|
||
|
|
name: preset.name.trim(),
|
||
|
|
config: sanitizedConfig,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
// Create new preset
|
||
|
|
return createPreset({
|
||
|
|
name: preset.name.trim(),
|
||
|
|
config: sanitizedConfig,
|
||
|
|
})
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
ipcMain.handle("config-presets:delete", (_event, id: string) => {
|
||
|
|
return deletePreset(id)
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.handle("config-presets:apply", async (_event, id: string) => {
|
||
|
|
const env = applyPresetToEnv(id)
|
||
|
|
if (!env) {
|
||
|
|
return { success: false, error: "Preset not found" }
|
||
|
|
}
|
||
|
|
|
||
|
|
const isDev = process.env.NODE_ENV === "development"
|
||
|
|
|
||
|
|
if (isDev) {
|
||
|
|
// In development mode, the config file change will trigger
|
||
|
|
// the file watcher in electron-dev.mjs to restart Next.js
|
||
|
|
// We just need to save the preset (already done in applyPresetToEnv)
|
||
|
|
return { success: true, env, devMode: true }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Production mode: restart the Next.js server to apply new environment variables
|
||
|
|
try {
|
||
|
|
await restartNextServer()
|
||
|
|
return { success: true, env }
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error:
|
||
|
|
error instanceof Error
|
||
|
|
? error.message
|
||
|
|
: "Failed to restart server",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
ipcMain.handle(
|
||
|
|
"config-presets:set-current",
|
||
|
|
(_event, id: string | null) => {
|
||
|
|
return setCurrentPreset(id)
|
||
|
|
},
|
||
|
|
)
|
||
|
|
}
|