# 技术方案 Step04 最低字数控制设计

## 目标

在技术方案 Step04 正文生成环节增加“最低字数”控制。用户可以设置本次生成的最低正文字数；当 AI 默认生成内容低于目标时，系统自动通过补目录和扩写正文补足字数。

该能力只控制“最低字数”，不控制最大字数。不同 AI 模型默认输出长度不同，系统只在实际生成字数低于预设值时介入扩充。

## 配置项

### 正文生成配置

- 删除正文生成配置顶部说明“开始生成前确认是否配图，以及本次最多生成多少张 AI 图片。”
- 在正文生成配置中新增“最低字数”输入项。
- 默认值为 `0`，表示不限制最低字数，按现有正文生成逻辑生成多少就是多少，不进入补目录或扩写环节。
- 当最低字数大于 `0` 时，正文生成完成后按最新字数统计结果判断是否需要自动补足。

### 配置说明文案

建议说明：

```text
不同 AI 模型默认生成字数不同。系统不限制最大字数；当生成字数低于最低字数时，会自动补充目录或扩写正文。
```

## 字数统计口径

最低字数判断和页面展示必须使用同一套统计口径，避免 Main 侧任务判断和 Renderer 侧总字数显示不一致。

统计规则：

- 排除图片 Markdown，例如 `![alt](url)`。
- 排除 fenced code block，包括 Mermaid 代码块。
- 排除 Mermaid、PlantUML、Graphviz 等图表代码内容。
- Markdown 链接只保留链接文本，不统计 URL。
- 排除裸 URL。
- 排除 Markdown 标记、表格分隔符、标题符号、列表符号等非正文符号。
- 中文按字符数统计。
- 英文、数字按连续词段统计。
- 表格中的正文内容仍应统计，但表格结构符号不统计。

当前页面里的简单 `replace(/\s+/g, '')` 统计方式需要替换为新的统一工具函数。

## 总体生成流程

1. 用户点击“开始生成”。
2. 系统按现有逻辑执行正文编排和正文生成。
3. 正文生成阶段结束后，先不进入图片阶段。
4. 系统按统一字数统计口径计算当前总字数和每个叶子节点字数。
5. 如果“最低字数”为 `0`，直接进入图片阶段。
6. 如果当前总字数大于等于最低字数，直接进入图片阶段。
7. 如果当前总字数小于最低字数，且小于最低字数的 80%，先进入“补目录子任务”。
8. 如果当前总字数小于最低字数，但大于等于最低字数的 80%，进入“补字数子任务”。
9. 补目录子任务完成并生成新增叶子正文后，再重新计算总字数。
10. 如果仍未达到最低字数，进入补字数子任务。
11. 补字数子任务持续循环执行，直到达到最低字数。
12. 达到最低字数后，汇总本次任务生成、补目录生成或扩写过，且仍然有效、正文成功、尚未配图的叶子节点，统一进入图片阶段。
13. 图片阶段完成后，Step04 正文生成任务结束。

## 新增任务阶段

`contentGenerationTask.stats.content.phase` 需要新增扩写相关阶段。

建议阶段：

- `planning`：正文编排。
- `generating`：正文生成。
- `outline-expanding`：补目录。
- `expanding`：补字数。
- `illustrating`：配图。
- `done`：完成。

扩写阶段进度条目标为最低字数，使用 `当前总字数 / 最低字数` 计算百分比。页面右上角总字数应继续根据 workspace 中最新正文实时更新，因此扩写进度条只显示百分比，不显示具体字数。

## 补目录子任务

### 触发条件

当最低字数大于 `0`，且当前总字数小于最低字数的 80% 时触发。

### 输入给 AI 的内容

- 项目概述。
- 技术评分要求。
- 当前完整目录。
- 当前总字数。
- 预期最低字数。
- 当前叶子节点字数中位数。
- 已有补目录轮次。

### AI 输出要求

AI 只输出“新增目录”，不输出完整目录。

允许新增：

- 二级目录。
- 三级目录。
- 四级目录。

禁止新增：

- 一级目录。
- 完整目录。
- 正文内容。
- 图片、表格、编排计划等非目录数据。

新增目录可以包含通用技术方案内容，但不得与项目概述、评分要求和已有目录相违背。允许的通用方向包括组织管理、质量控制、安全管理、进度保障、验收交付、运维服务、培训计划、资料管理、风险控制、应急响应等。

### 合并规则

由程序负责将 AI 返回的新增目录合并到当前目录。

合并规则：

- 不允许修改、删除、重命名一级目录。
- 允许在一级目录下新增二级目录。
- 允许在二级目录下新增三级目录。
- 允许在三级目录下新增四级目录。
- 原则上保留所有既有节点 ID，避免 `contentGenerationSections` 和 `contentGenerationPlans` 错挂。
- 新增节点由程序分配 ID。
- 如果某个既有叶子节点被扩展为非叶子节点，该节点原有正文必须清空。
- 被清空节点对应的 `contentGenerationSections`、`contentGenerationPlans` 和 `outlineData.outline[*].content` 都必须同步删除。
- 被清空节点此前占用的表格、AI 图片、Mermaid 图片等编排额度必须返还。
- 合并完成后必须校验完整目录结构。
- 目录编号整理以“不改变既有节点 ID”为优先原则；只对新增节点分配稳定、合法、不冲突的编号。

完整目录校验规则：

- 一级目录数量、顺序和标题必须保持不变。
- 所有节点必须包含非空 `id`、`title`、`description`。
- 所有节点 ID 必须唯一。
- 目录层级不能超过四级。
- 非叶子节点不得保留 `content`。
- 二级目录 ID 必须挂在一级目录 ID 下，三级目录 ID 必须挂在二级目录 ID 下，四级目录 ID 必须挂在三级目录 ID 下。
- 新增节点只能出现在二级、三级、四级目录。

### JSON 修复与校验

- AI 返回 JSON 使用现有 `collectJsonResponse()` 和 JSON 自修复链路。
- 修复后仍必须经过新增目录结构校验。
- 合并后的完整目录必须再次校验。
- 如果 AI 返回完整目录、正文内容、图片、表格、编排计划、非法层级或其他非新增目录字段，必须判定本轮返回格式无效，并优先通过自修复链路修正为“新增目录”格式。
- 如果 AI 同时返回合法 `additions` 和非法 `outline`、正文、图片、表格、编排计划等字段，也必须进入自修复链路，不能静默忽略非法字段后直接应用。
- 无法修复时本轮补目录失败并进入下一轮或后续正文扩写。

### 轮次与终止

- 补目录最多执行 3 轮。
- 每轮补目录后，根据叶子节点字数中位数预估新增叶子可生成字数。
- 如果 `当前总字数 + 预估新增字数` 大于等于最低字数的 80%，停止补目录并生成新增叶子正文。
- 如果 3 轮后仍未达到 80% 预估，也停止补目录，直接生成已经新增的叶子正文。
- 补目录只负责扩大可生成正文的目录空间，不负责最终确保达标；最终达标由补字数子任务负责。

### 新增叶子正文生成

补目录完成后，系统自动对新增叶子执行正文编排和正文生成，使新增目录追平到“正文已生成、配图待执行”的进度。

要求：

- 继续使用用户原始正文生成配置。
- 表格、AI 图片、Mermaid 图片等编排额度使用剩余额度。
- 如果某些旧叶子变成非叶子并清空了正文，其已占用的编排额度必须先返还，再计算剩余额度。
- 新增叶子的正文生成完成后重新计算总字数。
- 此阶段只执行编排和正文生成，不执行任何图片生成、Mermaid 渲染校验或图片插入。
- 图片阶段必须等最低字数达标后统一执行，且只覆盖本次任务实际影响过、仍然有效、正文成功、尚未配图的叶子节点。

## 补字数子任务

### 触发条件

满足以下任一条件时触发：

- 当前总字数小于最低字数，但大于等于最低字数的 80%。
- 补目录和新增叶子正文生成完成后，当前总字数仍小于最低字数。
- 补目录达到 3 轮上限后，当前总字数仍小于最低字数。

### 节点选择策略

1. 收集所有已成功生成正文的叶子节点。
2. 使用统一字数统计口径计算每个叶子节点字数。
3. 按字数从小到大排序。
4. 取中位数所在叶子节点作为起点。
5. 首轮先扩写中位数节点。
6. 后续从中位数向两端扩散选择节点，每批最多选择两个节点并发扩写。

扩散顺序示例：

```text
[中位数节点]
[中位数 - 1, 中位数 + 1]
[中位数 - 2, 中位数 + 2]
```

选择时跳过：

- 没有正文的节点。
- 正文生成失败的节点。
- 已经变成非叶子节点的旧节点。
- 当前正在扩写的节点。

### AI 输入内容

每个扩写请求提交：

- 完整目录。
- 当前要扩写的章节路径。
- 当前章节标题和描述。
- 当前章节原正文。
- 当前章节已统计字数。
- 当前章节预期扩写后字数。
- 避免与其他章节重复的明确约束。

当前章节预期扩写后字数可以按 `当前章节字数 * 2` 计算，但不得小于一个合理下限，避免短章节扩写目标过小。

### AI 输出策略

不要求 AI 返回扩写后的完整正文。AI 应根据正文结构自行选择一种扩写操作。

允许操作：

- 新增一个段落，并返回建议插入位置。
- 重写并扩写某个段落，并返回要替换的原段落定位。

禁止操作：

- 返回完整全文。
- 修改章节标题。
- 引入其他章节应承载的内容。
- 输出图片 Markdown、Mermaid、代码块。
- 输出与项目或当前章节相冲突的内容。

### 程序合并规则

由程序根据 AI 返回的操作合并正文。

合并规则：

- 新增段落时，按 AI 返回的插入位置插入；插入位置无效时追加到正文末尾。
- 替换段落时，只替换明确匹配到的原段落；无法匹配时降级为追加新增段落。
- 合并后重新执行正文规范化。
- 合并后同步更新 `contentGenerationSections` 和 `outlineData.outline[*].content`。
- 每次节点扩写完成后立即落盘 workspace，并推送任务事件。
- 每次节点扩写完成后重新计算总字数。
- 扩写阶段不执行任何图片生成、Mermaid 渲染校验或图片插入。
- 扩写阶段不重新分配配图目标；已有编排计划继续保留，最终配图目标在统一图片阶段按本次任务实际影响过的小节确定。

### 循环规则

补字数子任务不设置硬上限。

只要当前总字数仍低于最低字数，就持续执行补字数子任务，直到达到最低字数。

执行要求：

- 每批最多并发 2 个 AI 扩写请求。
- 第一批只扩写中位数节点；从第二批开始按中位数左右两端成对扩写。
- 每批结束后重新计算总字数。
- 达到最低字数后立即停止扩写，不再启动新的扩写请求。
- 已经启动的同批扩写请求可以自然完成，完成后再进入图片阶段。
- 如果某轮可选节点为空，应重新从全部成功叶子节点中选择，允许重复扩写已扩写过的节点，直到达到最低字数。
- 如果 AI 扩写失败，记录日志并继续选择其他节点扩写；不能因为单个节点失败中断整个最低字数任务。
- 如果完整覆盖一轮可选成功叶子节点后，总字数没有增长，应结束扩写并将任务标记为失败，避免后台任务无限 running 并持续消耗 AI 请求；这里不设置固定轮次上限，只以整轮是否增长作为退出保护。

## 配图阶段

配图阶段必须在最低字数达标后统一执行。

原因：

- 字数统计需要排除图片 Markdown 和 Mermaid 代码块。
- 扩写正文时不应把图片 Markdown 或 Mermaid 作为正文上下文。
- 补目录可能清空旧叶子正文并返还图片、表格等编排额度。
- 补目录新增叶子和补字数扩写都会改变最终正文集合，提前配图会导致图片插入、额度占用和后续扩写上下文互相干扰。

额度规则：

- 表格、AI 图片、Mermaid 图片额度以当前有效叶子节点的编排计划为准。
- 被清空或不再是叶子节点的旧节点，其编排计划不再有效，已占用额度必须返还。
- 新增叶子编排时只消耗剩余额度。
- 最终配图只对本次任务实际影响过、仍然有效且正文生成成功的叶子节点执行。
- 最终配图目标在进入图片阶段时统一汇总，但不能把本次未生成、未补目录生成、未扩写的历史成功节点重新纳入配图。
- 如果目标小节正文中已经存在图片或 Mermaid 图，必须跳过，避免重复追加配图。

## 页面展示

### 生成配置弹窗

- 新增“最低字数”输入项。
- 默认 `0`。
- 输入为非负整数。
- 文案说明“0 表示不限制”。

### 统计区

- 右上角总字数使用统一字数统计口径。
- 扩写过程中总字数根据 workspace 最新内容实时更新。

### 左侧进度卡片

- `planning` 显示正文编排进度。
- `generating` 显示正文生成进度。
- `outline-expanding` 显示补目录进度。
- `expanding` 显示最低字数达成百分比。
- `illustrating` 显示配图进度。

扩写阶段显示示例：

```text
正在扩写正文，最低字数达成 76%。
```

## 数据落盘

所有最低字数相关状态必须由 Main 侧后台任务持续写入 workspace。

需要保存：

- `contentGenerationOptions.minimumWords`。
- 当前任务阶段。
- 当前总字数。
- 最低字数。
- 补目录轮次。
- 扩写进度。
- 每个叶子节点最新正文。
- 最新 `outlineData`。
- 最新 `contentGenerationSections`。
- 最新 `contentGenerationPlans`。

Renderer 只负责展示和启动任务，不承载扩写任务生命周期。

## 实现注意事项

- 不新增单独 IPC，继续复用 `tasks:start-content-generation` 和 `tasks:event`。
- 最低字数逻辑应放在 `electron/services/contentGenerationTask.cjs` 的正文生成后台任务中。
- 目录补充、正文扩写的 JSON 调用复用现有 JSON 修复链路。
- 页面手动编辑正文后也应使用统一字数统计口径更新总字数。
- Word 导出仍以 `outlineData.outline[*].content` 为权威来源。
- 补目录导致目录结构变化时，必须保证旧正文不会残留在非叶子节点上。
- 扩写和补目录期间不得进入图片阶段。
- 最低字数为 `0` 时保持现有生成行为，避免无配置用户被额外消耗 AI 请求。
