Download src/services/workflow-guards.ts from Leon4gr45/Velo: direct link, hf CLI and curl.
- Browser
- Download file 7.37 kB
-
https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/services/workflow-guards.ts
- Command line
-
hf download hf://spaces/Leon4gr45/Velo/src/services/workflow-guards.ts
-
curl -L -o workflow-guards.ts https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/services/workflow-guards.ts
7.37 kB
| /** | |
| * 工作流前置校验工具 | |
| * | |
| * 每个生成步骤启动前需要校验上游数据是否就绪, | |
| * 以防用户在配置不完整的状态下浪费 AI Token。 | |
| * | |
| * 校验失败时返回 GuardResult { ok: false, message }, | |
| * 由调用方选择展示方式(弹窗提示 / 日志输出)。 | |
| */ | |
| import { useProjectStore } from '../stores/project-store' | |
| import { ipc } from './ipc-client' | |
| import i18n from '../i18n' | |
| import { readPostProcessStatus, getChapterFinalizeScope, getFailedStepLabels } from './workflows/workflow-utils' | |
| const t = (key: string, opts?: Record<string, unknown>) => i18n.t(key, { ns: 'stores', ...opts }) | |
| export interface GuardResult { | |
| ok: boolean | |
| /** 失败时的提示语(给用户看) */ | |
| message?: string | |
| /** 用于跳转引导的目标动作标识 */ | |
| action?: 'open-config' | 'open-world-building' | 'open-blueprint' | |
| } | |
| // ===== Guard 1:生成故事架构前 → 校验小说配置 ===== | |
| /** | |
| * 生成故事架构(架构文件)前的前置校验: | |
| * 要求「核心大纲」或「主角人设」至少填写其中之一。 | |
| */ | |
| export function guardArchitectureGeneration(): GuardResult { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) { | |
| return { ok: false, message: t('guard.openProjectFirst') } | |
| } | |
| const { coreOutline, protagonistProfile, worldSetting, genre } = project.novelConfig | |
| // 校验:小说配置是否有实质内容 | |
| const hasConfig = !!( | |
| coreOutline?.trim() || | |
| protagonistProfile?.trim() || | |
| worldSetting?.trim() | |
| ) | |
| if (!hasConfig) { | |
| return { | |
| ok: false, | |
| message: t('guard.fillNovelConfig', { genre: genre || 'Not set' }), | |
| action: 'open-config', | |
| } | |
| } | |
| return { ok: true } | |
| } | |
| // ===== Guard 2:生成章节蓝图前 → 校验故事架构完成度 ===== | |
| /** | |
| * 生成章节蓝图前的前置校验: | |
| * 要求至少有 1 个架构信息(故事前提)已生成, | |
| * 建议 4 个都完成,但允许继续(仅提示警告)。 | |
| */ | |
| export async function guardDirectoryGeneration(): Promise<GuardResult> { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) { | |
| return { ok: false, message: t('guard.openProjectFirst') } | |
| } | |
| const core = await ipc.invoke('db:project-core-get') | |
| const missing: string[] = [] | |
| const checkHasContent = (text: string | null | undefined) => text && text.length > 50 && !text.includes('> 待生成') | |
| if (!core || !checkHasContent(core.premise)) missing.push(t('sidebar.premise', { ns: 'panels' })) | |
| if (!core || !checkHasContent(core.charactersArch)) missing.push(t('sidebar.characterMap', { ns: 'panels' })) | |
| if (!core || !checkHasContent(core.worldbuilding)) missing.push(t('sidebar.worldbuilding', { ns: 'panels' })) | |
| if (!core || !checkHasContent(core.synopsis)) missing.push(t('sidebar.synopsis', { ns: 'panels' })) | |
| // 故事前提是必须的(第一个大块) | |
| if (missing.includes(t('sidebar.premise', { ns: 'panels' }))) { | |
| return { | |
| ok: false, | |
| message: t('guard.premiseNotGenerated'), | |
| action: 'open-world-building', | |
| } | |
| } | |
| // 其他部分缺失:警告但允许继续 | |
| if (missing.length > 0) { | |
| return { | |
| ok: true, // 允许继续,但携带警告信息 | |
| message: t('guard.missingArchitecture', { items: missing.map(m => `• ${m}`).join('\n') }), | |
| } | |
| } | |
| // 检查角色卡是否存在(不变量 1 的反向约束) | |
| const chars = await ipc.invoke('db:character-get-all') | |
| if (chars.length === 0) { | |
| return { | |
| ok: false, | |
| message: t('guard.noCharacterCards'), | |
| } | |
| } | |
| return { ok: true } | |
| } | |
| // ===== Guard 3:章节写稿前 → 校验章节蓝图存在 ===== | |
| /** | |
| * 章节写稿前的前置校验: | |
| * 1. 要求 blueprints/ 目录中存在至少一个章节蓝图 JSON 文件。 | |
| * 2. 如果指定了章节号且 > 1,要求前一章已定稿(存在于 manuscript/ 中),否则上下文会断裂。 | |
| */ | |
| export async function guardChapterWriting(targetChapterNumber?: number): Promise<GuardResult> { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) { | |
| return { ok: false, message: t('guard.openProjectFirst') } | |
| } | |
| const blueprints = await ipc.invoke('db:blueprint-get-all') | |
| if (blueprints.length === 0) { | |
| return { | |
| ok: false, | |
| message: t('guard.noBlueprints'), | |
| action: 'open-blueprint', | |
| } | |
| } | |
| // 检查角色卡是否存在(不变量 1 的反向约束:无角色卡则阻断写稿) | |
| const chars = await ipc.invoke('db:character-get-all') | |
| if (chars.length === 0) { | |
| return { | |
| ok: false, | |
| message: t('guard.characterCardsMissingForWrite'), | |
| } | |
| } | |
| // 如果指定了且不是第一章,则必须保证前一章已经存在正文库(定稿状态) | |
| if (targetChapterNumber && targetChapterNumber > 1) { | |
| const prevChapter = targetChapterNumber - 1 | |
| const prevDraftMeta = await ipc.invoke('db:draft-get-finalized', prevChapter) | |
| if (!prevDraftMeta) { | |
| return { | |
| ok: false, | |
| message: t('guard.prevChapterNotFinalized', { chapter: prevChapter }), | |
| } | |
| } | |
| // 不变量 2 的反向约束:前一章定稿后处理必须全部通过 | |
| const prevScope = getChapterFinalizeScope(prevChapter) | |
| const prevStatus = await readPostProcessStatus(project.path, prevScope) | |
| if (prevStatus && !prevStatus.allCriticalPassed) { | |
| const failedLabels = getFailedStepLabels(prevStatus) | |
| return { | |
| ok: false, | |
| message: t('guard.prevPostProcessIncomplete', { chapter: prevChapter, steps: failedLabels.map(f => `• ${f}`).join('\n') }), | |
| } | |
| } | |
| // 如果状态文件不存在(旧版定稿)→ 兼容放行 | |
| } | |
| return { ok: true } | |
| } | |
| // ===== Guard 4:角色卡重新生成前 → 检查蓝图是否为空 ===== | |
| /** | |
| * 角色卡重新生成/补齐的前置校验: | |
| * 不变量 1:角色卡仅允许在蓝图目录为空时重新生成, | |
| * 否则会破坏已有蓝图/章节的角色状态链。 | |
| */ | |
| export async function guardCharacterRegeneration(): Promise<GuardResult> { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) { | |
| return { ok: false, message: t('guard.openProjectFirst') } | |
| } | |
| const blueprints = await ipc.invoke('db:blueprint-get-all') | |
| if (blueprints.length > 0) { | |
| return { | |
| ok: false, | |
| message: t('guard.characterCannotRegenerate'), | |
| } | |
| } | |
| return { ok: true } | |
| } | |
| // ===== Guard 5:修复定稿后处理前 → 检查是否为最新定稿章节 ===== | |
| /** | |
| * 修复定稿后处理的前置校验: | |
| * 不变量 2:后处理只允许在最新定稿章节上执行, | |
| * 回溯重跑历史章节会覆盖后续章节的角色状态。 | |
| */ | |
| export async function guardRepairPostProcess(chapterNumber: number): Promise<GuardResult> { | |
| const project = useProjectStore.getState().currentProject | |
| if (!project) { | |
| return { ok: false, message: t('guard.openProjectFirst') } | |
| } | |
| // 从数据库获取最大的定稿章节号 | |
| const maxFinalized = await ipc.invoke('db:draft-get-max-finalized-chapter') | |
| if (maxFinalized === 0) { | |
| return { ok: false, message: t('guard.noFinalizedChapters') } | |
| } | |
| if (chapterNumber !== maxFinalized) { | |
| return { | |
| ok: false, | |
| message: t('guard.canOnlyRepairLatest', { chapter: maxFinalized, target: chapterNumber }), | |
| } | |
| } | |
| return { ok: true } | |
| } | |