import { ipcMain, dialog } from 'electron' import { randomUUID } from 'node:crypto' import fs from 'node:fs' import path from 'node:path' import { readJsonFile, writeJsonFile, RECENT_PROJECTS_PATH } from '../utils/config-utils' import { ProjectData } from '../../src/shared/ipc-channels' import { DIR_VELA_INTERNAL, DIR_PROMPTS } from '../../src/shared/project-paths' import { initProjectDatabase } from '../database' import { ProjectCoreRepository } from '../repositories/project-core-repository' interface RecentProject { name: string path: string updatedAt: string } function loadRecentProjects(): RecentProject[] { return readJsonFile(RECENT_PROJECTS_PATH, []) } function addRecentProject(project: RecentProject) { const list = loadRecentProjects() const filtered = list.filter((p) => p.path !== project.path) filtered.unshift(project) const trimmed = filtered.slice(0, 20) writeJsonFile(RECENT_PROJECTS_PATH, trimmed) } export function registerProjectController() { // 创建新项目 ipcMain.handle('project:create', async (_event, config: { name: string; path: string; genre: string; targetAudience: string }) => { try { const projectId = randomUUID() const projectDir = path.join(config.path, config.name) // 仅创建必要的系统目录 fs.mkdirSync(path.join(projectDir, DIR_VELA_INTERNAL), { recursive: true }) fs.mkdirSync(path.join(projectDir, DIR_PROMPTS), { recursive: true }) // 初始化 DB 底座 initProjectDatabase(projectDir) // 初始化 project_core 记录 ProjectCoreRepository.init(config.name) ProjectCoreRepository.update({ genre: config.genre, targetAudience: config.targetAudience, }) // 补充缺失在 DB 初始化时生成所需的数据 const projectData: ProjectData = { id: projectId, name: config.name, path: projectDir, novelConfig: { genre: config.genre, subGenre: '', targetAudience: config.targetAudience, totalChapters: 100, wordsPerChapter: 3000, plotStructure: 'three_act', narrativePOV: 'third_limited', coreOutline: '', worldSetting: '', goldenFinger: '', protagonistProfile: '', globalGuidance: '', }, characterStates: '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), } // 添加到最近项目列表 addRecentProject({ name: config.name, path: projectDir, updatedAt: projectData.updatedAt }) return { success: true, projectId, projectPath: projectDir } } catch (error) { return { success: false, projectId: '', error: String(error) } } }) // 打开现有项目 ipcMain.handle('project:open', async (_event, projectPath: string) => { try { if (!fs.existsSync(projectPath)) { return { success: false, project: null, error: '目录不存在' } } // TODO: 这里可以加入一个检测旧版项目的逻辑(如果有 旧的 01_novel_config.json 等),提示不支持旧格式。 // 因为新架构不兼容旧项目,这里我们只要初始化 DB 即可 initProjectDatabase(projectPath) // 从 DB 读取配置 const coreData = ProjectCoreRepository.get() if (!coreData) { // 如果是从空目录新建并打开,尝试初始化 const folderName = path.basename(projectPath) ProjectCoreRepository.init(folderName) } // 组装返回给前端的数据结构 const updatedCoreData = ProjectCoreRepository.get()! const projectData: ProjectData = { id: 'main', name: updatedCoreData.projectName, path: projectPath, novelConfig: { genre: updatedCoreData.genre, subGenre: updatedCoreData.subGenre, targetAudience: updatedCoreData.targetAudience, totalChapters: updatedCoreData.totalChapters, wordsPerChapter: updatedCoreData.wordsPerChapter, plotStructure: updatedCoreData.plotStructure as 'three_act' | 'heros_journey' | 'save_the_cat' | 'kishotenketsu' | 'multi_thread' | 'freeform', narrativePOV: updatedCoreData.narrativePov as 'third_limited' | 'first_person' | 'third_omniscient' | 'multi_pov', coreOutline: updatedCoreData.synopsis, // 旧字段映射 worldSetting: updatedCoreData.worldbuilding, // 旧字段映射 goldenFinger: updatedCoreData.goldenFinger, protagonistProfile: updatedCoreData.charactersArch, // 旧字段映射 globalGuidance: updatedCoreData.globalGuidance, writingStyle: updatedCoreData.writingStyle, referenceWorks: updatedCoreData.referenceWorks, }, characterStates: updatedCoreData.characterStates, createdAt: new Date().toISOString(), // db 中实际上有,但这里先 mock 一下时间避免前端报错 updatedAt: new Date().toISOString(), } addRecentProject({ name: projectData.name, path: projectPath, updatedAt: projectData.updatedAt }) return { success: true, project: projectData } } catch (error) { return { success: false, project: null, error: String(error) } } }) // 保存/更新项目配置 // 注意:这个接口前端可能还传了很多 novelConfig 中的字段,我们需要 mapping 给 DB。 ipcMain.handle('project:save', async (_event, _projectId: string, data: Partial) => { try { if (!data.path) return { success: false, error: '缺少项目路径' } if (data.novelConfig) { ProjectCoreRepository.update({ genre: data.novelConfig.genre, subGenre: data.novelConfig.subGenre, targetAudience: data.novelConfig.targetAudience, totalChapters: data.novelConfig.totalChapters, wordsPerChapter: data.novelConfig.wordsPerChapter, plotStructure: data.novelConfig.plotStructure, narrativePov: data.novelConfig.narrativePOV, goldenFinger: data.novelConfig.goldenFinger, globalGuidance: data.novelConfig.globalGuidance, writingStyle: data.novelConfig.writingStyle ?? '', referenceWorks: data.novelConfig.referenceWorks ?? '', }) } if (data.name) { ProjectCoreRepository.update({ projectName: data.name }) } if (data.characterStates !== undefined) { ProjectCoreRepository.update({ characterStates: data.characterStates }) } addRecentProject({ name: data.name ?? 'Unknown', path: data.path, updatedAt: new Date().toISOString(), }) return { success: true } } catch (error) { return { success: false, error: String(error) } } }) // project:update-config 同理 ipcMain.handle('project:update-config', async (_event, _projectId: string, data: Partial) => { try { if (data.novelConfig) { ProjectCoreRepository.update({ genre: data.novelConfig.genre, subGenre: data.novelConfig.subGenre, targetAudience: data.novelConfig.targetAudience, totalChapters: data.novelConfig.totalChapters, wordsPerChapter: data.novelConfig.wordsPerChapter, plotStructure: data.novelConfig.plotStructure, narrativePov: data.novelConfig.narrativePOV, goldenFinger: data.novelConfig.goldenFinger, globalGuidance: data.novelConfig.globalGuidance, writingStyle: data.novelConfig.writingStyle ?? '', referenceWorks: data.novelConfig.referenceWorks ?? '', }) } return { success: true } } catch (error) { return { success: false, error: String(error) } } }) ipcMain.handle('project:recent-list', async () => { return loadRecentProjects() }) ipcMain.handle('dialog:select-folder', async () => { const result = await dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'], title: '选择项目保存位置', }) if (result.canceled || result.filePaths.length === 0) return null return result.filePaths[0] }) }