Browse Source

fix: WorldEditor save-before-switch, Ollama markdown preservation, JSON parse safety, update CLAUDE.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce 6 months ago
parent
commit
0bb6a627fb

+ 180 - 80
CLAUDE.md

@@ -16,87 +16,201 @@ yarn electron:build   # Electron 打包
 打包.bat              # 一键打包脚本
 ```
 
+## 架构总览:四阶段创作流程
+
+创作流程分为四个阶段(Phase 0-3),每个阶段对应独立的 UI 和 API。
+
+### Phase 0 — 项目初始化 (InitWizard)
+
+用户通过 AI 引导式面试完成项目初始化。InitWizard 提供分步表单或 AI 对话模式:
+1. **Genre 选择** — 修仙/种田/生存/建造/都市/奇幻/自定义
+2. **角色设定** — 主角身份、核心性格、核心阵容
+3. **情节方向** — 主线冲突、结局方向
+4. **世界观描述** — 自由文本描述世界背景
+5. **文风设定** — 文风预设 + 7 种风格模块选择 + 强度/段落模式/章节标题开关
+
+初始化完成后自动写入标准文档集:`00-project-overview.md`、`01-theme-and-proposition.md`、`03-cast-bible.md`、`09-style-guide.md`、`iron-rules.md`、`style-anchor.md`,以及 `world_state/` 和 `event-graph/` 初始文件。
+
+### Phase 1 — 世界构建 (WorldEditor + EventGraphEditor)
+
+创作者在写作前构建完整的世界框架:
+
+- **WorldEditor** — 侧边栏文件树 + 文本编辑器 + AI 扩展功能。编辑 `world_state/`(7 个标准世界状态文件)和 `world_state/lore/`(世界观设定)。支持自动保存(1.5s 防抖),切换文件时强制保存当前编辑内容。
+- **EventGraphEditor** — 事件图谱编辑器。管理事件节点(类型:major/arc/milestone/foreshadow/flashback)和边(cause/foreshadow/resolve/parallel)。支持 AI 补全事件、批量操作(展开/折叠/清除)。
+- **PhaseNavigation** — 阶段切换组件,在 Phase 1-3 之间导航。
+
+### Phase 2 — 章节创作 (8 步管线 + 质量层)
+
+**8 步管线**(状态自动保存到 sessionStorage,防刷新丢失):
+
+| 步骤 | 组件 | 功能 |
+|------|------|------|
+| 0 | Step0ControlCard | AI 生成/手动填写控制卡:本章改变什么、推进哪条线、回归什么债务、核心冲突、结尾余波、候选标题 |
+| 1 | Step1PlotSelection | AI 生成 5 个剧情选项,或手动输入 |
+| 2 | Step2EmbedHooks | AI 生成叙事钩子 + 手动添加 |
+| 3 | Step3ResolveHooks | 选择要回收的历史钩子 |
+| 4 | Step4ContentEditor | 流式 AI 生成 + 编辑 + 润色 + 去AI味(三步修订:去模板化、去专业腔、恢复真实语感)。自动注入 `writing_style.md` 和 `09-style-guide.md` |
+| 5 | Step5ReviewSubmit | 审查 AI 更新的世界状态文件 + 章节标题确认 + 保存 |
+| 6 | Step6ContinuityCheck | 8 项连续性审计(设定漂移/人物失真/关系断档/伏笔遗失/支线冷落/情绪债务/AI模式化/段落漂移) |
+| 7 | Step7ReaderFeedback | 模拟读者反馈评分(每 5 章自动触发,可覆盖) |
+
+**三层质量保障体系:**
+1. **每章检查** — Step6ContinuityCheck + Step7ReaderFeedback
+2. **每 5 章读者反馈** — API `reader-feedback/route.ts`,评分维度:节奏感、爽点密度、情感共鸣、阅读困惑
+3. **每 20 章里程碑审计** — API `audit/route.ts`,检查维度:角色漂移、设定漂移、风格漂移、伏笔缺口
+
+**滚动摘要系统** — 每章完成后更新 `world_state/rolling-summaries/`,为后续章节提供上下文压缩,解决长篇小说上下文窗口限制。
+
+**反漂移系统:**
+- **iron-rules.md** — 项目铁律(不可违背的设定),在审计/连续性检查中注入
+- **style-anchor.md** — 风格锚定摘要,注入里程碑审计
+
+### Phase 3 — 维护面板 (MaintenancePanel)
+
+长篇创作进入稳态后的维护界面,提供:
+- 事件图谱章节分配查看
+- 级联影响分析(重写某章时自动检测后续受影响章节)
+- 世界设定变更冲突检测
+- 全局批量维护操作
+
+**级联检查系统** (cascade-check/route.ts):
+- `action: "rewrite"` — 检测重写章节影响到的后续章节(通过事件图谱的父子依赖)
+- `action: "setting_change"` — 检测世界设定变更与已有章节的冲突
+
+### 马拉松模式
+
+勾选后每章保存完成自动延迟 1 秒打开下一章管线,可随时取消。
+
 ## 项目结构
 
 ```
 src/app/
-├── page.tsx                    # 首页(通用写作助手 WritingAssistant)
-├── layout.tsx                  # 根布局
-├── globals.css                 # 全局样式
-├── components/
-│   ├── WritingAssistant.tsx    # 通用 AI 编辑器 (NovelEditor + SettingsDrawer)
-│   ├── Navigation.tsx          # 导航栏 [写作助手, 小说创作]
-│   ├── FeatureLayout.tsx       # 功能页布局壳
-│   └── ApiSettings.tsx         # API 配置(Provider/URL/Key/Model)
+├── page.tsx                         # 首页(通用写作助手 WritingAssistant)
+├── layout.tsx                       # 根布局
+├── globals.css                      # 全局样式
+├── error.tsx / global-error.tsx     # 错误边界
+├── not-found.tsx                    # 404
+├── components/                      # 通用组件
+│   ├── WritingAssistant.tsx         # 通用 AI 编辑器
+│   ├── Navigation.tsx               # 导航栏 [写作助手, 小说创作]
+│   ├── FeatureLayout.tsx            # 功能页布局壳
+│   ├── ApiSettings.tsx              # API 配置(Provider/URL/Key/Model)
+│   ├── AiGuideChat.tsx              # AI 引导对话
+│   ├── PromptForm.tsx               # 提示词表单
+│   ├── StreamingContent.tsx         # 流式内容展示
+│   ├── ResumePreview.tsx            # 简历预览
+│   ├── Footer.tsx                   # 页脚
+│   ├── editor/
+│   │   ├── NovelEditor.tsx          # 通用 AI 编辑器
+│   │   └── utils/streamToEditor.ts  # SSE 流接入编辑器
+│   ├── sidebar/
+│   │   └── SettingsDrawer.tsx       # 设置抽屉
+│   └── decorations/                 # 装饰组件
 ├── features/
-│   └── novel-writer/           # 【核心】长篇创作系统(Electron 默认入口)
-│       ├── page.tsx            # 三阶段:项目选择→初始化向导→创作界面
+│   └── novel-writer/                # 【核心】长篇创作系统(Electron 默认入口)
+│       ├── page.tsx                 # 四阶段入口:项目选择→初始化→创作→维护
 │       └── components/
 │           ├── ProjectManager.tsx           # 项目 CRUD(Electron 文件夹选择器)
-│           ├── InitWizard.tsx               # 5步初始化向导
-│           ├── NovelContextManager.tsx      # 主创作界面 + 马拉松模式
-│           ├── WorldStateSidebar.tsx        # 世界状态文件侧边栏
-│           ├── ChapterDraftPipeline.tsx     # 7步章节管线
-│           ├── ChapterHistoryList.tsx       # 章节历史
-│           ├── WritingConstraintManager.tsx # 写作约束
-│           ├── AntiHomogenizationPanel.tsx  # 反同质化策略
+│           ├── InitWizard.tsx               # Phase 0: 5步/对话式初始化向导
+│           ├── NovelContextManager.tsx      # Phase 2: 主创作界面+管线编排
+│           ├── PhaseNavigation.tsx          # Phase 1-3 阶段切换
+│           ├── WorldEditor.tsx              # Phase 1: 世界状态文件编辑器
+│           ├── EventGraphEditor.tsx         # Phase 1: 事件图谱编辑器
+│           ├── WorldStateSidebar.tsx        # 世界状态侧边栏
+│           ├── ChapterDraftPipeline.tsx     # 8步章节管线编排
+│           ├── ChapterHistoryList.tsx       # 章节历史列表
+│           ├── WritingConstraintManager.tsx # 写作约束管理
+│           ├── AntiHomogenizationPanel.tsx  # 反同质化策略面板
+│           ├── MaintenancePanel.tsx         # Phase 3: 维护面板
 │           └── pipeline/
-│               ├── Step0ControlCard.tsx      # 控制卡(AI/手动)
-│               ├── Step1PlotSelection.tsx    # 剧情选择(AI 生成5选项)
-│               ├── Step2EmbedHooks.tsx       # 埋入叙事钩子
-│               ├── Step3ResolveHooks.tsx     # 解决历史钩子
-│               ├── Step4ContentEditor.tsx    # 内容生成 + 润色 + 去AI味
-│               ├── Step5ReviewSubmit.tsx     # 审核世界状态更新 + 保存
-│               └── Step6ContinuityCheck.tsx  # 连续性检查(8项审计)
+│               ├── Step0ControlCard.tsx     # 控制卡
+│               ├── Step1PlotSelection.tsx   # 剧情选择
+│               ├── Step2EmbedHooks.tsx      # 埋入叙事钩子
+│               ├── Step3ResolveHooks.tsx    # 解决历史钩子
+│               ├── Step4ContentEditor.tsx   # 内容生成+润色+去AI味
+│               ├── Step5ReviewSubmit.tsx    # 审核+保存
+│               ├── Step6ContinuityCheck.tsx # 连续性检查
+│               └── Step7ReaderFeedback.tsx  # 读者反馈
 ├── hooks/
-│   ├── useNovelProject.ts     # 项目生命周期
-│   ├── useWorldState.ts       # 世界状态文件管理
-│   ├── useChapterHistory.ts   # 章节历史
-│   ├── useStateUpdater.ts     # AI 更新世界状态
-│   └── useEditorAI.ts         # 通用编辑器 AI
+│   ├── useNovelProject.ts          # 项目生命周期管理
+│   ├── useWorldState.ts            # 世界状态文件读写
+│   ├── useChapterHistory.ts        # 章节历史管理
+│   ├── useStateUpdater.ts          # AI 更新世界状态
+│   ├── useEditorAI.ts              # 通用编辑器 AI
+│   └── useContextInjector.ts       # 上下文注入(iron-rules + style-anchor)
 ├── lib/
-│   ├── novel-types.ts         # 核心类型定义
-│   ├── novel-utils.ts         # 文件名/模板/钩子工具
-│   ├── novel-server-utils.ts  # 路径安全/isValidProject
-│   ├── style-modules.ts       # 7种风格模块 Prompt(幽默/悬念/推理/爱情/恐怖/奇幻/文学)
-│   ├── chapter-prompts.ts     # 控制卡/连续性检查/去AI味 Prompt 模板
+│   ├── novel-types.ts              # 核心类型定义
+│   ├── novel-utils.ts              # 文件名/模板/钩子工具
+│   ├── novel-server-utils.ts       # 服务端工具(isPathSafe/isValidProject)
+│   ├── style-modules.ts            # 7种风格模块 Prompt
+│   ├── chapter-prompts.ts          # 控制卡/连续性检查/去AI味/读者反馈 Prompt 模板
+│   ├── event-graph-utils.ts        # 事件图谱数据处理工具
 │   ├── api.ts / types.ts / editor-types.ts / secureApiKey.ts
+│   ├── resumeAiGuide.ts / resumeApi.ts / resumeTypes.ts / resumeTemplates.ts / resumePdfExport.ts
+│   └── __tests__/                  # 单元测试
+│       ├── novel-utils.test.ts / novel-utils-isvalidproject.test.ts
+│       ├── context-injector.test.ts / event-graph-utils.test.ts
+│       ├── iron-rules.test.ts / resolve-hook.test.ts / state-updater.test.ts
 └── api/
-    ├── proxy/route.ts           # 非流式 LLM 代理
-    ├── stream-proxy/route.ts    # 流式 SSE 代理
+    ├── proxy/route.ts                 # 非流式 LLM 代理(支持 OpenAI 格式 + Ollama)
+    ├── proxy/ollama-models/route.ts   # Ollama 模型列表
+    ├── proxy/cherry-models/route.ts   # Cherry Studio 模型列表
+    ├── stream-proxy/route.ts          # 流式 SSE 代理
     └── novel/
-        ├── project/route.ts     # 项目 CRUD
-        ├── files/route.ts       # 文件读写(read/write/list/delete/open)
-        ├── init/route.ts        # 项目初始化向导
-        └── select-folder/route.ts
+        ├── project/route.ts           # 项目 CRUD
+        ├── files/route.ts             # 文件读写(read/write/list/delete/open)
+        ├── init/route.ts              # 项目初始化(写入标准文档)
+        ├── select-folder/route.ts     # Electron 文件夹选择器
+        ├── iron-rules/route.ts        # iron-rules.md 读写
+        ├── style-anchor/route.ts      # style-anchor.md 读写
+        ├── summarize/route.ts         # 滚动摘要生成
+        ├── audit/route.ts             # 里程碑审计(每20章)
+        ├── reader-feedback/route.ts   # 模拟读者反馈(每5章)
+        ├── cascade-check/route.ts     # 级联影响分析(重写/设定变更)
+        ├── event-graph/route.ts       # 事件图谱 CRUD
+        │   ├── allocate/route.ts      # AI 事件-章节分配
+        │   └── validate/route.ts      # 事件图谱验证
+        └── __tests__/
+            ├── files.test.ts
+            └── init.test.ts
+
 electron/
 ├── main.ts                    # Electron 主进程(加载 /features/novel-writer)
 └── preload.ts                 # 暴露 electronAPI(selectFolder/getAppConfig/saveAppConfig)
 ```
 
-## 核心创作流程
-
-### 项目初始化(InitWizard 5步)
-1. **选择类型** — 修仙/种田/生存/建造/都市/奇幻/自定义
-2. **角色设定** — 主角身份、核心性格、核心阵容
-3. **情节方向** — 主线冲突、结局方向
-4. **世界观描述** — 自由文本描述世界背景
-5. **文风设定** — 文风预设 + 7种风格模块选择 + 强度/段落模式/章节标题开关
-   - 初始化完成时自动写入 00-project-overview / 01-theme / 03-cast-bible / 09-style-guide 等标准文档
-
-### 章节管线(7步,状态自动保存到 sessionStorage)
-0. **控制卡** — AI 生成/手动填写:本章改变什么、推进哪条线、回归什么债务、核心冲突、结尾余波、候选标题
-1. **选择剧情** — AI 生成 5 个剧情选项或手动输入
-2. **埋入钩子** — AI 生成叙事钩子 + 手动添加
-3. **解决钩子** — 选择要回收的历史钩子
-4. **生成内容** — 流式 AI 生成 + 编辑 + 文本润色 + **去AI味**(三步修订:去模板化→去专业腔→恢复真实语感)
-   - 自动注入 writing_style.md 和 09-style-guide.md 到提示词
-5. **审核提交** — 审查 AI 更新的世界状态文件 + 章节标题确认 + 保存
-6. **连续性检查** — 8项审计(设定漂移/人物失真/关系断档/伏笔遗失/支线冷落/情绪债务/AI模式化/段落漂移)
+## 文件系统
 
-### 马拉松模式
-- 勾选后每章保存完成自动延迟 1 秒打开下一章管线
-- 可随时取消
+```
+{项目文件夹}/
+├── novel_project.json              # 项目配置
+├── 00-project-overview.md          # 项目总览
+├── 01-theme-and-proposition.md     # 主题命题
+├── 03-cast-bible.md                # 人物圣经
+├── 09-style-guide.md               # 风格指南(7种模块)
+├── iron-rules.md                   # 项目铁律(反漂移)
+├── style-anchor.md                 # 风格锚定摘要(反漂移)
+├── world_state/                    # 世界状态文件(7个标准文件)
+│   ├── 02-core-world-rules.md
+│   ├── 04-world-physics.md
+│   ├── 05-factions.md
+│   ├── 06-map-and-geography.md
+│   ├── 07-history-and-events.md
+│   ├── 08-magic-and-power-system.md
+│   ├── 10-main-plot-outline.md
+│   ├── lore/                       # 世界观设定
+│   └── rolling-summaries/          # 滚动摘要(每章追加)
+├── chapters/                       # 正文章节
+├── control-cards/                  # 章节控制卡
+├── event-graph/                    # 事件图谱
+│   ├── events.json                 # 事件节点
+│   ├── edges.json                  # 事件间关系
+│   └── chapter-allocation.json     # 事件-章节分配
+├── logs/                           # 审计日志
+│   ├── audit/                      # 里程碑审计记录
+│   └── reader-feedback/            # 读者反馈记录
+└── history/                        # 历史版本
+```
 
 ## 内置风格模块(7种)
 
@@ -112,29 +226,13 @@ electron/
 | 奇幻 | 规则代价、日常厚度、规则一致性 |
 | 文学 | 语言节制、主题通过选择戏剧化、意象系统一致性 |
 
-## 文件系统
-
-```text
-{项目文件夹}/
-├── novel_project.json          # 项目配置
-├── 00-project-overview.md      # 项目总览
-├── 01-theme-and-proposition.md # 主题命题
-├── 03-cast-bible.md            # 人物圣经
-├── 09-style-guide.md           # 风格指南
-├── world_state/                # 世界状态文件(7个)
-│   └── lore/                   # 世界观设定(7个)
-├── chapters/                   # 正文章节
-├── control-cards/              # 章节控制卡
-├── logs/                       # 审计日志
-└── history/                    # 历史版本
-```
-
 ## LLM 提供者
 
 OpenAI / Grok (xAI) / DeepSeek / Ollama (本地) / Cherry Studio / 自定义 API。
 API Key 不经过服务器存储,所有调用通过 Next.js API Route 代理转发。
+Ollama 响应保留原始 Markdown 格式,不做去标记处理。
 
-## 已完成的安全/质量改进
+## 安全/质量保证
 
 - `isPathSafe()` 防路径遍历(`../` `..\\` `%2e%2e` 空字节)
 - 文件打开改用 `spawn()` 防命令注入
@@ -148,3 +246,5 @@ API Key 不经过服务器存储,所有调用通过 Next.js API Route 代理
 - 管线状态保存到 sessionStorage(防刷新丢失)
 - 关闭项目和取消管线需二次确认
 - 项目表单支持 Enter 键提交
+- AI JSON 响应解析均包裹在 try/catch 中,防止格式异常崩溃
+- WorldEditor 文件切换前强制保存当前编辑内容,防止防抖定时器被清除导致数据丢失

+ 6 - 1
src/app/api/novel/audit/route.ts

@@ -63,7 +63,12 @@ ${summaries.slice(0, 5000)}
     const data = await response.json();
     const text = data.choices?.[0]?.message?.content || '';
     const jsonMatch = text.match(/\{[\s\S]*\}/);
-    const audit = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
+    let audit;
+    try {
+      audit = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
+    } catch {
+      return NextResponse.json({ success: false, error: 'AI 返回了无效的 JSON 响应' }, { status: 502 });
+    }
 
     if (audit) {
       const auditDir = path.join(projectPath, 'logs', 'audit');

+ 6 - 1
src/app/api/novel/cascade-check/route.ts

@@ -93,7 +93,12 @@ export async function POST(request: NextRequest) {
         const data = await response.json();
         const text = data.choices?.[0]?.message?.content || '';
         const jsonMatch = text.match(/\{[\s\S]*\}/);
-        const result = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
+        let result;
+        try {
+          result = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
+        } catch {
+          return NextResponse.json({ success: false, error: 'AI 返回了无效的 JSON 响应' }, { status: 502 });
+        }
         return NextResponse.json({
           success: true,
           result: {

+ 6 - 1
src/app/api/novel/event-graph/allocate/route.ts

@@ -61,7 +61,12 @@ ${edgesRaw}
     const data = await response.json();
     const text = data.choices?.[0]?.message?.content || '';
     const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, text];
-    const allocations = JSON.parse(jsonMatch[1].trim());
+    let allocations: unknown;
+    try {
+      allocations = JSON.parse(jsonMatch[1].trim());
+    } catch {
+      return NextResponse.json({ success: false, error: 'AI 返回了无效的 JSON 响应' }, { status: 502 });
+    }
 
     // Save allocation
     await fs.writeFile(

+ 6 - 1
src/app/api/novel/reader-feedback/route.ts

@@ -74,7 +74,12 @@ ${chapterContent.slice(0, 3000)}
     const data = await response.json();
     const text = data.choices?.[0]?.message?.content || '';
     const jsonMatch = text.match(/\{[\s\S]*\}/);
-    const feedback = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
+    let feedback;
+    try {
+      feedback = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
+    } catch {
+      return NextResponse.json({ success: false, error: 'AI 返回了无效的 JSON 响应' }, { status: 502 });
+    }
 
     // Save feedback to file
     const feedbackDir = path.join(projectPath, 'logs', 'reader-feedback');

+ 2 - 3
src/app/api/proxy/route.ts

@@ -164,9 +164,8 @@ export async function POST(request: NextRequest) {
             }
           }
 
-          // 去除内容中的Markdown标记,简化为纯文本
-          content = content.replace(/[#*`_~]/g, '');
-          
+          // 保留原始Markdown格式(不进行去标记处理,以支持小说写作中的标题/强调等格式)
+
           // 确保content是字符串类型
           if (typeof content !== 'string') {
             content = String(content);

+ 7 - 1
src/app/features/novel-writer/components/WorldEditor.tsx

@@ -51,7 +51,13 @@ export default function WorldEditor({ projectPath, apiSettings, initialFileName
     return () => clearTimeout(timer);
   }, [editContent, selectedFile, updateFile]);
 
-  const handleSelectFile = (file: WorldStateFile) => {
+  const handleSelectFile = async (file: WorldStateFile) => {
+    // Force-save current file before switching
+    if (selectedFile && editContent !== selectedFile.content && editContent.trim()) {
+      try {
+        await updateFile(selectedFile.fileName, editContent, selectedFile.isLore);
+      } catch { /* ignore, proceed with switch */ }
+    }
     setSelectedFile(file);
     setEditContent(file.content);
     setSaveStatus('idle');