1 Commits

Author SHA1 Message Date
c3fbf7a30b feat: tighten visual novel one-line generation flow 2026-05-13 12:26:39 +08:00
4 changed files with 533 additions and 18 deletions

View File

@@ -0,0 +1,343 @@
# Genarrative 视觉小说“一句话生成”最小闭环落地计划
生成时间2026-05-13 11:22
工作区:`C:/proj/Genarrative/.worktrees/hermes-visual-novel`
参考文档:`C:/Users/DSK/Documents/Interactive-fiction/一句话生成视觉小说整体流程总结.md`
## 1. 目标
把 Interactive-fiction 总结文档中的“一句话生成视觉小说”流程,映射并落地到 Genarrative 现有视觉小说能力中,优先做成一个可端到端验证的最小闭环:
1. 用户在视觉小说入口输入一句话并选择画风。
2. 前端进入生成过程页,展示分阶段进度。
3. 后端创建视觉小说创作会话,并基于 seedText 生成 `VisualNovelResultDraft`
4. 生成完成后进入草稿结果页,可看到世界观、角色、场景、剧情阶段、开场选择。
5. 草稿可编译/保存为作品 profile并进入视觉小说运行态测试/正式游玩。
本计划只覆盖 Genarrative 内部最小闭环,不引入 Interactive-fiction 原项目的独立 TXT 播放记录、分享播放包、外部活动运营、独立账号/交易/资产系统。
## 2. 当前上下文与已发现实现
### 2.1 Interactive-fiction 总结文档提炼
参考文档将整体流程分为:
- 输入侧:一句话创意、主题/风格、可选文档或素材。
- 生成侧:理解意图、扩展世界观、角色、场景、剧情阶段、开场与选择。
- 编辑侧:草稿页可查看和调整生成结果。
- 运行侧:从草稿进入视觉小说游玩,支持剧情推进、玩家选择、历史与状态。
- 资产侧:角色立绘、背景、音乐/音效可作为后续增强,最小闭环可先使用文字描述与空资产占位。
### 2.2 Genarrative 已有实现基础
已确认项目中视觉小说相关能力并非从零开始:
- 前端入口表单:
- `src/components/visual-novel-creation/VisualNovelAgentWorkspace.tsx`
- 已有“一句话创作” textarea、6 个视觉画风选项、提交按钮“生成视觉小说草稿”。
- 前端入口 payload/progress
- `src/components/visual-novel-creation/visualNovelEntryGeneration.ts`
- 已有 `VisualNovelEntryFormPayload`、锚点展示、一句话/画风生成进度步骤。
- 前端平台主流程:
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
- 已接入 `createVisualNovelDraftFromForm`,会创建 session、stream message、进入 `visual-novel-generating`,完成后进入 `visual-novel-result`
- 前端 API client
- `src/services/visual-novel-creation/visualNovelCreationClient.ts`
- 已封装 session/message/action/compile 接口。
- 共享契约:
- `packages/shared/src/contracts/visualNovel.ts`
- 已定义 `VisualNovelResultDraft`、world/characters/scenes/storyPhases/opening/runtimeConfig/work/run/history 等结构。
- 后端 API
- `server-rs/crates/api-server/src/visual_novel.rs`
- 已有创建 session、发消息、流式消息、执行 action、compile、work、runtime run 等接口。
- 后端 prompt
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
- 已有 `VISUAL_NOVEL_CREATION_SYSTEM_PROMPT`、结构化输出契约、runtime GM prompt、repair prompt。
- SpacetimeDB 模块:
- `server-rs/crates/spacetime-module/src/visual_novel.rs`
- 已有 session/message/work/run/history/event 表与 procedure。
- 文档参考:
- `docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`
- `docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`
- `docs/technical/VISUAL_NOVEL_PROMPT_AND_LLM_TOOLS_VN03_2026-05-05.md`
### 2.3 关键实现判断
当前项目已经实现了视觉小说的主要骨架,本次不应大规模重写。更合理的落地方式是补齐“一句话生成”闭环中最容易断裂的点:
- 入口输入与画风信息是否被稳定传给后端 prompt。
- 后端生成 draft 后是否自动保存/关联可编辑 work profile。
- 生成过程页是否能清晰展示 Interactive-fiction 文档中提到的阶段。
- 结果页是否有足够的字段展示与继续游玩入口。
- 运行态是否能基于 opening/choices 正常启动,而不依赖尚未生成的图片/音乐资产。
## 3. 拟采用方案
### 3.1 最小闭环范围
本次优先实现:
1. “一句话 + 视觉画风”作为 `sourceMode: 'idea'` 的 seedText。
2. 后端生成完整 `VisualNovelResultDraft`,包括:
- world
- 3-6 个角色
- 3-8 个场景
- 3-6 个剧情阶段
- opening narration/firstDialogue/2-4 个 choices
- runtimeConfig
3. 若 LLM 输出失败,使用 repair 或确定性 fallback保证可回到草稿页并显示错误/警告。
4. 结果页支持保存/编译为 work profile。
5. work profile 支持启动 runtime runopening 能展示初始场景、旁白、对话和选择。
暂不做或仅预留:
- 真实图片/音乐生成队列。
- 多文档解析导入的完整链路。
- 复杂分镜/节点图编辑器。
- 外部 Interactive-fiction 项目的播放器、TXT 记录包、分享活动、独立账号系统。
### 3.2 与 Genarrative 架构的映射
| Interactive-fiction 概念 | Genarrative 落点 |
| --- | --- |
| 一句话创意 | `VisualNovelEntryFormPayload.ideaText` / `seedText` |
| 画风/主题 | `seedText` 中的“视觉画风/画风要求”,后续可结构化为 metadata |
| 世界观设定 | `VisualNovelResultDraft.world` |
| 角色设定 | `VisualNovelResultDraft.characters` |
| 场景设定 | `VisualNovelResultDraft.scenes` |
| 剧情阶段/章节 | `VisualNovelResultDraft.storyPhases` |
| 开场文本与选项 | `VisualNovelResultDraft.opening` |
| 运行时剧情推进 | `VisualNovelRuntimeStep[]` + run snapshot/history |
| 发布/作品库 | `VisualNovelWorkProfileRecord` / works API |
## 4. 分步计划
### Step 1补齐入口 payload 与生成过程语义
涉及文件:
- `src/components/visual-novel-creation/VisualNovelAgentWorkspace.tsx`
- `src/components/visual-novel-creation/visualNovelEntryGeneration.ts`
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
任务:
1. 保持现有 6 个画风选项,但确认每个 option 的 prompt 会进入 `seedText`
2. 将生成过程阶段从当前 3 步细化为更贴合参考文档的 4-5 步,例如:
- 理解一句话创意
- 扩展世界观与玩家身份
- 设计角色/场景/剧情阶段
- 生成开场与选择
- 准备可编辑草稿
3. 生成过程页的 anchor 保留“一句话”和“视觉画风”,必要时增加“生成目标:视觉小说草稿”。
4. 确认 `createVisualNovelDraftFromForm` 对失败状态会保留返回入口/重试能力。
验收点:提交一句话后能进入 `visual-novel-generating`,看到阶段进度;完成后进入 `visual-novel-result`
### Step 2增强后端 creation prompt 与 fallback 约束
涉及文件:
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
- `server-rs/crates/api-server/src/visual_novel.rs`
- 如已有 domain crate`server-rs/crates/module-visual-novel/**` 或相关 normalize/validate 文件
任务:
1. 在 creation prompt 中显式吸收 Interactive-fiction 的“一句话生成”目标:
- 从 seedText 提取核心创意、视觉风格、故事类型。
- 生成可直接运行的 opening 和 choices。
- 图片/音乐资产先置 null但必须有可生成图像的描述。
2. 强化输出约束:
- `opening.sceneId` 必须指向存在且 availability 为 `opening` 的 scene。
- `opening.initialChoices` 必须 2-4 个。
- `storyPhases[0]` 必须包含 opening scene 和主要角色。
- `publishReady` 的判定与 validationIssues 一致。
3. 检查 `submit_visual_novel_message_turn` / `resolve_action_draft` / compile 相关代码:
- 如果 LLM 失败,是否已有 fallback没有则补确定性 fallback draft。
- 如果 draft 不完整,是否会 normalize/repair 并写入 session。
4. 保留现有“不要输出旧 TXT 播放记录、分享播放包、外部商业字段”的约束,避免把参考项目的外部概念误并入 Genarrative。
验收点:后端给定 seedText 时,返回 session.draft 不为空且满足共享契约。
### Step 3确认草稿结果页、保存/编译与作品库链路
涉及文件:
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
- `src/components/visual-novel-creation/**`
- `src/services/visual-novel-works*` 或相关 visual novel works client
- `server-rs/crates/api-server/src/visual_novel.rs`
- `packages/shared/src/contracts/visualNovel.ts`
任务:
1. 查找并确认 `visual-novel-result` 页面组件:
- 是否显示 workTitle/workDescription/world/characters/scenes/storyPhases/opening。
- 是否有保存/发布/开始试玩按钮。
2. 确认 `compileVisualNovelWorkProfile``executeVisualNovelAction({kind:'compile_work_profile'})` 会生成/更新 work profile。
3. 确认作品架上使用 `profileId` 而不是 sessionId 作为稳定作品 ID。
4. 如果结果页缺少“一句话来源/画风”的可视化提示,可在结果页或 summary 中补轻量展示,避免用户以为画风丢失。
验收点:生成完成后能保存为作品;作品出现在“我的作品/创作架”;再次打开能读取同一 draft。
### Step 4确认运行态 opening 闭环
涉及文件:
- `src/components/visual-novel-runtime/**`
- `src/services/visual-novel-runtime*`
- `server-rs/crates/api-server/src/visual_novel.rs`
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
- `packages/shared/src/contracts/visualNovel.ts`
任务:
1. 启动 visual novel work run 时,优先使用 `draft.opening` 生成第一轮 runtime snapshot/history。
2. 如果没有图片/音乐,前端 runtime shell 必须可用文字 fallback不应白屏或阻断游玩。
3. 玩家选择 `choice` 后,后端 runtime GM prompt 生成下一轮 `VisualNovelRuntimeStep[]`
4. 确认正式游玩入口调用 `work_play_start`,并满足已有埋点约定:
- `scope_kind=work`
- `scope_id=稳定作品 ID`
- metadata 包含 `playType/workId/sourceRoute/userId` 等。
验收点:从生成出的作品进入运行态,能看到 opening 并点击至少一个选择推进一轮。
### Step 5补测试与文档
涉及文件:
- 前端测试:按仓库现有测试布局查找 `*.test.ts` / `*.test.tsx`
- Rust 测试:`server-rs/crates/api-server/src/**` 或 domain crate tests
- 文档:可追加到 `docs/technical/``.hermes/shared-memory/decision-log.md`(如团队约定需要)
建议测试:
1. TypeScript 单元测试:
- `buildVisualNovelEntryGenerationProgress` 阶段输出。
- `buildVisualNovelEntryGenerationAnchorEntries` 能展示一句话和画风。
2. Rust 单元测试:
- creation prompt 包含 seedText、sourceMode、输出契约。
- draft normalize/fallback 能生成合法 opening/choices。
- runtime opening 或 first-step 构造不依赖图片/音乐。
3. 集成/手工测试文档:
- 访问平台视觉小说入口。
- 输入一句话。
- 选择画风。
- 点击生成。
- 查看结果页。
- 保存作品。
- 启动试玩并点击选择。
## 5. 可能改动文件清单
高概率改动:
- `src/components/visual-novel-creation/VisualNovelAgentWorkspace.tsx`
- `src/components/visual-novel-creation/visualNovelEntryGeneration.ts`
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
- `server-rs/crates/api-server/src/prompt/visual_novel.rs`
- `server-rs/crates/api-server/src/visual_novel.rs`
- `packages/shared/src/contracts/visualNovel.ts`
中概率改动:
- `src/components/visual-novel-runtime/**`
- `src/services/visual-novel-creation/**`
- `src/services/visual-novel-runtime/**`
- `src/services/visual-novel-works/**`
- `server-rs/crates/spacetime-module/src/visual_novel.rs`
- `server-rs/crates/spacetime-client/**` 生成/绑定文件,若 SpacetimeDB contract 需要更新
低概率/仅文档:
- `docs/technical/VISUAL_NOVEL_IMPLEMENTATION_HANDOFF_2026-05-07.md`
- `docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`
- `.hermes/shared-memory/decision-log.md`
## 6. 验证计划
### 6.1 静态检查
在 worktree 根目录执行:
```bash
npm run typecheck
```
如仓库无统一 typecheck则按 package scripts 选择最接近的前端类型检查命令。
### 6.2 前端定向测试
优先运行与 visual novel / platform entry 相关测试,如存在:
```bash
npm test -- visual-novel
npm test -- platform-entry
```
若仓库使用 vitest
```bash
npm run test -- visual-novel
```
### 6.3 Rust 定向测试
`server-rs` 下运行 visual novel 相关测试:
```bash
cargo test -p api-server visual_novel
cargo test -p shared-contracts visual_novel
```
如改动 SpacetimeDB module
```bash
cargo test -p spacetime-module visual_novel
```
### 6.4 人工验收步骤
1. 启动本地 dev 栈。
2. 访问 Genarrative 主站。
3. 进入创作/视觉小说入口。
4. 输入:`一个雨夜,失忆的高中生在旧图书馆发现一本会回应她心声的日记。`
5. 选择任一画风,例如“映画动画”。
6. 点击“生成视觉小说草稿”。
7. 预期:进入生成过程页,能看到分阶段进度。
8. 预期:完成后进入草稿结果页,包含标题、简介、世界观、角色、场景、剧情阶段和 opening choices。
9. 点击保存/编译作品。
10. 从作品入口进入试玩。
11. 预期opening 文本出现,至少 2 个选择可点击;点击后剧情继续推进一轮。
## 7. 风险、权衡与开放问题
### 7.1 风险
- 现有视觉小说代码已较完整,贸然新增一套 parallel pipeline 会制造重复逻辑;应复用当前 `VisualNovelResultDraft` 与 creation agent flow。
- LLM 输出不稳定可能导致草稿结构不完整;需要 normalize/repair/fallback 确保最小闭环。
- 视觉/音乐资产生成未接入时UI 必须接受 null asset否则运行态可能白屏。
- `PlatformEntryFlowShellImpl.tsx` 文件很大,改动需局部、谨慎,避免影响其他玩法入口。
- 若改动 SpacetimeDB 表结构,可能牵涉 publish、client binding、清库/迁移;最小闭环阶段应尽量避免 schema 变更。
### 7.2 权衡
- 先让文字版视觉小说完整跑通,再补角色立绘/背景图生成。
- 先用 `seedText` 承载画风,再考虑把 `visualStyleId/Label/Prompt` 结构化进 draft metadata。
- 先用现有 result/work/runtime 页面闭环,不引入新编辑器。
### 7.3 开放问题
1. 用户是否要求把 Interactive-fiction 原项目中的具体 UI 样式/页面布局迁移到 Genarrative当前计划只迁移流程语义不迁移独立 UI。
2. 画风是否需要成为作品可编辑字段?当前以 seedText/prompt 影响生成内容,后续可在 draft 中增加 metadata。
3. 文档导入模式是否本期要做当前计划聚焦一句话模式document 模式只保留契约能力。
4. 是否需要真实图片/音乐生成?当前计划作为后续增强,不纳入最小闭环。
## 8. 建议实施顺序
1. 先做只改 prompt/progress/少量前端展示的轻量闭环修补。
2. 运行前后端定向测试,确认现有能力是否已足够。
3. 如果后端没有 fallback 或 normalize再补 Rust 层确定性兜底。
4. 手工跑通“一句话 -> 生成 -> 结果页 -> 保存 -> 试玩”。
5. 最后再考虑是否需要资产生成、文档导入、结构化画风 metadata。

View File

@@ -224,11 +224,23 @@ pub(crate) fn build_visual_novel_creation_user_prompt(
"currentDraft": params.current_draft,
"recentMessages": params.recent_messages,
"nowIso": params.now_iso,
"oneLineGenerationFlow": [
"提取一句话核心创意、故事类型、玩家身份和视觉画风",
"扩展世界观、故事前提、文学风格和默认叙事语气",
"设计 3 到 6 个角色,并为每个角色写出可生成立绘的 appearance",
"设计 3 到 8 个场景,并为 opening 场景写出可生成背景图的 description",
"组织 3 到 6 个剧情阶段,第一阶段必须能从 opening 进入",
"生成 opening.narration、可选 firstDialogue 和 2 到 4 个 initialChoices",
"图片、音乐可先为 null但文字草稿必须可进入结果页编辑、保存并试玩"
],
"draftRequirements": {
"mainCharacters": "3 到 6 个,至少 1 个非玩家主要角色",
"scenes": "3 到 8 个,至少 1 个 opening 场景",
"storyPhases": "3 到 6 个,第一阶段可从 opening 进入",
"initialChoices": "2 到 4 个",
"initialChoices": "2 到 4 个 initialChoices",
"openingScene": "opening.sceneId 必须指向存在且 availability 为 opening 的 scene",
"firstPhase": "storyPhases[0] 必须包含 opening scene 和主要角色",
"assetFallback": "图片、音乐可先为 null但 appearance 和 scene description 必须足够后续生成资产",
"runtimeConfigDefaults": "沿用契约默认值attributePanelMode 默认为 off"
},
"outputContract": VISUAL_NOVEL_CREATION_OUTPUT_CONTRACT
@@ -616,6 +628,29 @@ mod tests {
assert!(repair_prompt.contains("scene_change"));
}
#[test]
fn creation_prompt_guides_one_line_flow_into_playable_draft() {
let asset_ids = source_asset_ids();
let prompt = build_visual_novel_creation_user_prompt(VisualNovelCreationPromptParams {
source_mode: "idea",
seed_text: Some(
"雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。\n视觉画风:映画动画\n画风要求:电影感动画视觉小说画风。",
),
source_asset_ids: asset_ids.as_slice(),
document_summary: None,
current_draft: None,
recent_messages: &[],
now_iso: "2026-05-13T12:00:00Z",
});
assert!(prompt.contains("oneLineGenerationFlow"));
assert!(prompt.contains("提取一句话核心创意"));
assert!(prompt.contains("视觉画风"));
assert!(prompt.contains("opening.sceneId"));
assert!(prompt.contains("2 到 4 个 initialChoices"));
assert!(prompt.contains("图片、音乐可先为 null"));
}
#[test]
fn llm_requests_use_responses_template_model() {
let asset_ids = source_asset_ids();

View File

@@ -0,0 +1,100 @@
import { describe, expect, test } from 'vitest';
import {
buildVisualNovelEntryGenerationAnchorEntries,
buildVisualNovelEntryGenerationProgress,
type VisualNovelEntryFormPayload,
} from './visualNovelEntryGeneration';
function createVisualNovelPayload(
overrides: Partial<VisualNovelEntryFormPayload> = {},
): VisualNovelEntryFormPayload {
return {
sourceMode: 'idea',
seedText:
'雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。\n视觉画风映画动画\n画风要求电影感动画视觉小说画风。',
sourceAssetIds: [],
ideaText: '雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。',
visualStyleId: 'cinematic-anime',
visualStyleLabel: '映画动画',
visualStylePrompt: '电影感动画视觉小说画风。',
...overrides,
};
}
describe('visualNovelEntryGeneration', () => {
test('one-line visual novel generation exposes reference-flow stages', () => {
const progress = buildVisualNovelEntryGenerationProgress(
1_000,
'generating',
1_500,
);
expect(progress.steps.map((step) => step.id)).toEqual([
'visual-novel-intent',
'visual-novel-world',
'visual-novel-cast-scenes',
'visual-novel-opening',
'visual-novel-ready',
]);
expect(progress.phaseLabel).toBe('理解一句话创意');
expect(progress.steps[0]?.detail).toBe(
'提取核心题材、视觉画风、玩家身份和互动叙事目标。',
);
expect(progress.estimatedRemainingMs).toBe(44_500);
expect(progress.overallProgress).toBeGreaterThan(0);
});
test('one-line visual novel generation advances to opening choices before ready', () => {
const progress = buildVisualNovelEntryGenerationProgress(
1_000,
'generating',
35_000,
);
expect(progress.phaseId).toBe('visual-novel-opening');
expect(progress.phaseLabel).toBe('生成开场与选择');
expect(progress.steps[2]?.status).toBe('completed');
expect(progress.steps[3]?.status).toBe('active');
expect(progress.overallProgress).toBeLessThan(99);
});
test('one-line visual novel generation ready copy points to editable draft', () => {
const progress = buildVisualNovelEntryGenerationProgress(
1_000,
'ready',
46_000,
);
expect(progress.phaseId).toBe('ready');
expect(progress.phaseLabel).toBe('生成完成');
expect(progress.phaseDetail).toBe(
'视觉小说草稿已准备完成,可进入结果页编辑、保存并试玩。',
);
expect(progress.overallProgress).toBe(100);
});
test('one-line visual novel generation anchors include source, style and target', () => {
const entries = buildVisualNovelEntryGenerationAnchorEntries(
createVisualNovelPayload(),
);
expect(entries).toEqual([
{
id: 'visual-novel-idea',
label: '一句话',
value: '雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。',
},
{
id: 'visual-novel-style',
label: '视觉画风',
value: '映画动画',
},
{
id: 'visual-novel-target',
label: '生成目标',
value: '可编辑并可试玩的视觉小说草稿',
},
]);
});
});

View File

@@ -41,6 +41,11 @@ export function buildVisualNovelEntryGenerationAnchorEntries(
label: '视觉画风',
value: payload.visualStyleLabel,
},
{
id: 'visual-novel-target',
label: '生成目标',
value: '可编辑并可试玩的视觉小说草稿',
},
].filter((entry) => entry.value.trim());
}
@@ -72,27 +77,55 @@ export function buildVisualNovelEntryGenerationProgress(
weight: number;
durationMs: number;
},
] = [
{
id: 'visual-novel-session',
label: '创建创作会话',
detail: '写入一句话与视觉画风,准备生成视觉小说底稿。',
weight: 24,
durationMs: 5_000,
id: string;
label: string;
detail: string;
weight: number;
durationMs: number;
},
{
id: 'visual-novel-draft',
label: '生成故事底稿',
detail: '整理世界观、角色、场景和剧情阶段。',
weight: 56,
durationMs: 22_000,
id: string;
label: string;
detail: string;
weight: number;
durationMs: number;
},
] = [
{
id: 'visual-novel-intent',
label: '理解一句话创意',
detail: '提取核心题材、视觉画风、玩家身份和互动叙事目标。',
weight: 16,
durationMs: 6_000,
},
{
id: 'visual-novel-world',
label: '扩展世界观',
detail: '生成世界背景、故事前提、文学风格和玩家角色。',
weight: 22,
durationMs: 10_000,
},
{
id: 'visual-novel-cast-scenes',
label: '设计角色与场景',
detail: '补齐主要角色、可生成立绘的外观描述和 opening 场景。',
weight: 28,
durationMs: 16_000,
},
{
id: 'visual-novel-opening',
label: '生成开场与选择',
detail: '写入开场旁白、首句对白、剧情阶段和 2 到 4 个初始选择。',
weight: 24,
durationMs: 10_000,
},
{
id: 'visual-novel-ready',
label: '准备草稿页',
detail: '校验可编辑字段并进入草稿页。',
weight: 20,
durationMs: 4_000,
detail: '校验可编辑字段并进入结果页,后续可保存作品和试玩。',
weight: 10,
durationMs: 3_000,
},
];
let elapsedBeforeStep = 0;
@@ -130,9 +163,13 @@ export function buildVisualNovelEntryGenerationProgress(
: phase === 'failed'
? Math.max(1, completedWeight)
: Math.min(98, completedWeight + activeStep.weight * activeRatio);
const estimatedTotalMs = timeline.reduce(
(sum, step) => sum + step.durationMs,
0,
);
return {
phaseId: phase,
phaseId: phase === 'generating' ? activeStep.id : phase,
phaseLabel:
phase === 'ready'
? '生成完成'
@@ -141,7 +178,7 @@ export function buildVisualNovelEntryGenerationProgress(
: activeStep.label,
phaseDetail:
phase === 'ready'
? '视觉小说草稿已准备完成。'
? '视觉小说草稿已准备完成,可进入结果页编辑、保存并试玩。'
: phase === 'failed'
? '草稿生成失败,请返回入口页调整后重试。'
: activeStep.detail,
@@ -151,7 +188,7 @@ export function buildVisualNovelEntryGenerationProgress(
totalWeight: 100,
elapsedMs,
estimatedRemainingMs:
phase === 'ready' ? 0 : Math.max(0, 31_000 - elapsedMs),
phase === 'ready' ? 0 : Math.max(0, estimatedTotalMs - elapsedMs),
activeStepIndex: normalizedActiveStepIndex,
steps: timeline.map((step, index) => {
const isCompleted =