Download src/services/workflows/directory-workflow.ts from Leon4gr45/Velo: direct link, hf CLI and curl.
- Browser
- Download file 7.7 kB
-
https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/services/workflows/directory-workflow.ts
- Command line
-
hf download hf://spaces/Leon4gr45/Velo/src/services/workflows/directory-workflow.ts
-
curl -L -o directory-workflow.ts https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/services/workflows/directory-workflow.ts
7.7 kB
| import type { WorkflowDefinition } from '../../stores/workflow-store' | |
| import { useProjectStore } from '../../stores/project-store' | |
| import { ipc } from '../ipc-client' | |
| import type { BlueprintData } from '../../../electron/repositories/blueprint-repository' | |
| import { stripThinkingTags } from './workflow-utils' | |
| // ========================================== | |
| // 1. 结构与类型导出 (保留对外的向后兼容) | |
| // ========================================== | |
| export type ChapterBlueprint = BlueprintData | |
| const EMPTY_BLUEPRINT: ChapterBlueprint = { | |
| chapterNumber: 0, | |
| title: '', | |
| role: '发展', | |
| purpose: '', | |
| keyEvents: '', | |
| characters: [], | |
| suspenseHook: '', | |
| userGuidance: '', | |
| notes: '', | |
| notesUpdatedAt: '', | |
| } | |
| export interface DirectoryWorkflowParams { | |
| mode: 'full' | 'append' | |
| startChapter?: number | |
| count?: number | |
| /** 节奏/风格指导(可选) */ | |
| pacingGuidance?: string | |
| } | |
| // ========================================== | |
| // 2. 蓝图文件访问与工具函数 | |
| // ========================================== | |
| export function parseTextBlueprints(content: string, startNum: number, endNum: number): ChapterBlueprint[] { | |
| let result: ChapterBlueprint[] = [] | |
| try { | |
| const cleanContent = stripThinkingTags(content) | |
| const jsonStr = cleanContent.replace(/```json?\n?/g, '').replace(/```\n?/g, '').trim() | |
| const startIndex = jsonStr.indexOf('{') | |
| const endIndex = jsonStr.lastIndexOf('}') | |
| if (startIndex !== -1 && endIndex !== -1) { | |
| const arrayStr = jsonStr.substring(startIndex, endIndex + 1) | |
| let parsed = JSON.parse(arrayStr) | |
| if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.blueprints) { | |
| parsed = parsed.blueprints | |
| } | |
| if (Array.isArray(parsed)) { | |
| result = parsed | |
| .filter((p: Record<string, unknown>) => { | |
| const n = Number(p.chapterNumber || p.chapter_number) | |
| return n >= startNum && n <= endNum | |
| }) | |
| .map((p: Record<string, unknown>) => ({ | |
| ...EMPTY_BLUEPRINT, | |
| chapterNumber: Number(p.chapterNumber || p.chapter_number || 0), | |
| title: String(p.title || `第${p.chapterNumber}章`), | |
| role: String(p.role || '发展'), | |
| purpose: String(p.purpose || ''), | |
| keyEvents: String(p.keyEvents || p.key_events || ''), | |
| characters: Array.isArray(p.characters) ? p.characters : [], | |
| suspenseHook: String(p.suspenseHook || p.suspense_hook || ''), | |
| userGuidance: '', | |
| })) | |
| } | |
| } | |
| } catch { | |
| console.error('Failed to parse blueprint JSON', content) | |
| } | |
| const distinctMap = new Map<number, ChapterBlueprint>() | |
| for (const item of result) { | |
| if (!distinctMap.has(item.chapterNumber)) distinctMap.set(item.chapterNumber, item) | |
| } | |
| return Array.from(distinctMap.values()).sort((a, b) => a.chapterNumber - b.chapterNumber) | |
| } | |
| export async function loadDirectoryBlueprints(): Promise<ChapterBlueprint[]> { | |
| try { | |
| const blueprints = await ipc.invoke('db:blueprint-get-all') | |
| return blueprints.sort((a, b) => a.chapterNumber - b.chapterNumber) | |
| } catch { | |
| return [] | |
| } | |
| } | |
| export async function saveChapterBlueprint(blueprint: ChapterBlueprint): Promise<void> { | |
| await ipc.invoke('db:blueprint-upsert', blueprint) | |
| } | |
| export async function saveAllBlueprints(blueprints: ChapterBlueprint[]): Promise<void> { | |
| await ipc.invoke('db:blueprint-upsert-many', blueprints) | |
| } | |
| export async function getBlueprintCount(): Promise<number> { | |
| try { | |
| const blueprints = await ipc.invoke('db:blueprint-get-all') | |
| return blueprints.length | |
| } catch { | |
| return 0 | |
| } | |
| } | |
| // ========================================== | |
| // 3. 工作流定义映射工厂 (Command 调度层) | |
| // ========================================== | |
| export function createDirectoryWorkflow(params: DirectoryWorkflowParams = { mode: 'full' }): WorkflowDefinition { | |
| return { | |
| type: 'directory', | |
| title: params.mode === 'append' ? `📋 续写章节蓝图${params.startChapter ? `(从第 ${params.startChapter} 章)` : ''}` : '📋 生成章节蓝图(全量)', | |
| steps: [ | |
| { | |
| name: '读取架构', | |
| description: `从 SQLite 加载项目架构信息`, | |
| executor: async (_step, context, callbacks) => { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) throw new Error('未打开项目') | |
| callbacks.log('读取项目架构信息...') | |
| const core = await ipc.invoke('db:project-core-get') | |
| if (!core) throw new Error('项目核心数据未初始化') | |
| const parts: string[] = [] | |
| if (core.premise && core.premise.length > 50) parts.push(core.premise) | |
| if (core.charactersArch && core.charactersArch.length > 50) parts.push(core.charactersArch) | |
| if (core.worldbuilding && core.worldbuilding.length > 50) parts.push(core.worldbuilding) | |
| if (core.synopsis && core.synopsis.length > 50) parts.push(core.synopsis) | |
| if (parts.length === 0) throw new Error('项目主要架构均未生成') | |
| context.data.architecture = parts.join('\n\n---\n\n') | |
| // 注入节奏指导到 context,供 Command 读取 | |
| if (params.pacingGuidance) context.data.pacingGuidance = params.pacingGuidance | |
| if (params.mode === 'append') { | |
| const existing = await loadDirectoryBlueprints() | |
| context.data.existingBlueprints = existing | |
| callbacks.log(`已加载 ${existing.length} 章已有蓝图`) | |
| } | |
| return `架构加载完成(${parts.length} 段)` | |
| }, | |
| }, | |
| { | |
| name: '生成蓝图', | |
| description: '基于架构文件生成全书章节蓝图', | |
| executor: async (_step, context, callbacks) => { | |
| const { GenerateDirectoryCommand } = await import('./commands/directory.command') | |
| const cmd = new GenerateDirectoryCommand(params) | |
| const blueprints = await cmd.execute({ step: _step, context, callbacks }) | |
| // 返回可读摘要字符串(step.result 必须是 string,否则 AIOutputPanel 渲染会崩溃) | |
| return `已生成 ${blueprints.length} 章蓝图` | |
| }, | |
| }, | |
| { | |
| name: '保存蓝图', | |
| description: `将章节蓝图批量写入 SQLite 数据库`, | |
| executor: async (_step, context, callbacks) => { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) throw new Error('未打开项目') | |
| const newBlueprints = context.data.newBlueprints as ChapterBlueprint[] | |
| const existingBlueprints = context.data.existingBlueprints as ChapterBlueprint[] | |
| callbacks.log('保存蓝图到数据库...') | |
| let merged: ChapterBlueprint[] | |
| if (params.mode === 'full') { | |
| merged = newBlueprints | |
| // TODO: 若需要清理冗余蓝图,可考虑添加 db:blueprint-delete-all 以严格符合全量替换的意图。 | |
| // 在当前 upsert-many 中,仅覆盖更新 | |
| } else { | |
| const existingMap = new Map(existingBlueprints.map(b => [b.chapterNumber, b])) | |
| for (const nb of newBlueprints) existingMap.set(nb.chapterNumber, nb) | |
| merged = Array.from(existingMap.values()).sort((a, b) => a.chapterNumber - b.chapterNumber) | |
| } | |
| await saveAllBlueprints(merged) | |
| useProjectStore.getState().refreshFileTree() | |
| return '已保存蓝图' | |
| }, | |
| }, | |
| ], | |
| onComplete: { | |
| mode: 'silent', | |
| message: params.mode === 'append' ? '✅ 续写蓝图生成完成' : '✅ 全书章节蓝图已生成完成!', | |
| }, | |
| } | |
| } | |