W15 通用生成、基本审校与候选(收口):多粒度写作任务与两阶段写手(预组装→受限探索→冻结回放→无工具写作→检查入候选)、V0021 模型候选证据绑定与版本化审校报告、基础+连续性确定性检查、采纳门 CHECK_FAILED/CHECK_MISSING 禁未检候选、模型候选服务端写哈希与证据(作者决定仍走 S01)、HTTP/CLI/主会话技能/工作台创作对话接入;15 新 NC(NC-w15-* 11 + UI 3 + browser 1),离线 269 / 非宿主数据库 209 / 前端 27,真实浏览器旅程与视觉判定通过;独立整包验证退回一次后按 NC 大写前缀约定修正并复核登记门禁;W15 置 verified。

This commit is contained in:
zizi 2026-09-11 01:44:13 +08:00
parent baf7d431a4
commit 63ead639f4
52 changed files with 48602 additions and 44313 deletions

View File

@ -0,0 +1,16 @@
---
id: write-next-chapter
name: 写下一章
category: operation
contract_version: 1
commands: [start_writing_task,read_task,read_task_events]
description: 对目标章发起两阶段生成:受限探索、冻结回放、无工具写作与独立检查,产出模型候选供作者审阅。
---
# 写下一章
对目标章发起两阶段生成:受限探索、冻结回放、无工具写作与独立检查,产出模型候选供作者审阅。
发起时写请求 JSON(command_id、work_id、chapter_id、granularity=`new_chapter`、branch_id、purpose、config_id),用 `.venv/bin/python -m muse 创作 <私人作者配置> 发起 <请求文件>` 执行;重试同一请求保留 command_id,参数改变必须用新命令身份。
任务进度用 `任务 <配置> 事件 <任务ID>` 续接读取。必需检查未通过的候选不能采纳(服务端阻断),报告中的发现与证据逐条对应来源版本;模型不参与检查判定,也不自填候选哈希或检查结论。

View File

@ -0,0 +1,3 @@
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|------|----------|----------|----------|----------|
| 写下一章 | [SKILL.md](SKILL.md) | 对目标章发起两阶段生成:受限探索、冻结回放、无工具写作与独立检查,产出模型候选供作者审阅。 | 需要该项操作时 | 只使用已实现的具名入口 |

View File

@ -0,0 +1,16 @@
---
id: expand-scene
name: 扩写场景
category: operation
contract_version: 1
commands: [start_writing_task,read_task,read_task_events]
description: 对既有章节按扩写粒度发起生成,材料与检查与写下一章同一受控链路。
---
# 扩写场景
对既有章节按扩写粒度发起生成,材料与检查与写下一章同一受控链路。
发起时写请求 JSON(command_id、work_id、chapter_id、granularity=`expand_scene`、branch_id、purpose、config_id),用 `.venv/bin/python -m muse 创作 <私人作者配置> 发起 <请求文件>` 执行;重试同一请求保留 command_id,参数改变必须用新命令身份。
任务进度用 `任务 <配置> 事件 <任务ID>` 续接读取。必需检查未通过的候选不能采纳(服务端阻断),报告中的发现与证据逐条对应来源版本;模型不参与检查判定,也不自填候选哈希或检查结论。

View File

@ -0,0 +1,3 @@
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|------|----------|----------|----------|----------|
| 扩写场景 | [SKILL.md](SKILL.md) | 对既有章节按扩写粒度发起生成,材料与检查与写下一章同一受控链路。 | 需要该项操作时 | 只使用已实现的具名入口 |

View File

@ -0,0 +1,16 @@
---
id: check-consistency
name: 核对内容一致性
category: operation
contract_version: 1
commands: [read_candidate_review_report,read_task_events]
description: 读取候选的独立检查报告:基础有效性与连续性发现(出场核对、复读拦截),证据逐条可回读。
---
# 核对内容一致性
读取候选的独立检查报告:基础有效性与连续性发现(出场核对、复读拦截),证据逐条可回读。
候选检查报告用 `创作 <私人作者配置> 报告 <候选ID>` 查看,也可在任务事件中读取检查完成事件。
任务进度用 `任务 <配置> 事件 <任务ID>` 续接读取。必需检查未通过的候选不能采纳(服务端阻断),报告中的发现与证据逐条对应来源版本;模型不参与检查判定,也不自填候选哈希或检查结论。

View File

@ -0,0 +1,3 @@
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|------|----------|----------|----------|----------|
| 核对内容一致性 | [SKILL.md](SKILL.md) | 读取候选的独立检查报告:基础有效性与连续性发现(出场核对、复读拦截),证据逐条可回读。 | 需要该项操作时 | 只使用已实现的具名入口 |

View File

@ -5,3 +5,6 @@
| 决定正文候选去留 | [SKILL.md](决定正文候选去留/SKILL.md) | 比较、修改候选并执行作者明确决定 | 使用新版候选入口时 | 使用被审版本,不自动替作者采纳 |
| 完善故事基础设定 | [SKILL.md](完善故事基础设定/SKILL.md) | 私人探索保存与最终格式检查 | 使用新版探索入口 | 不自动进入正式规划 |
| 确认规划候选 | [SKILL.md](确认规划候选/SKILL.md) | 读取规划候选并执行作者明确决定 | 使用新版规划入口 | 使用被审版本,回查正式回执 |
| 写下一章 | [SKILL.md](写下一章/SKILL.md) | 对目标章发起两阶段生成:受限探索、冻结回放、无工具写作与独立检查,产出模型候选供作者审阅。 | 需要该项操作时 | 只使用已实现的具名入口 |
| 扩写场景 | [SKILL.md](扩写场景/SKILL.md) | 对既有章节按扩写粒度发起生成,材料与检查与写下一章同一受控链路。 | 需要该项操作时 | 只使用已实现的具名入口 |
| 核对内容一致性 | [SKILL.md](核对内容一致性/SKILL.md) | 读取候选的独立检查报告:基础有效性与连续性发现,证据逐条可回读。 | 需要该项操作时 | 只使用已实现的具名入口 |

View File

@ -2462,6 +2462,117 @@
}
}
},
"/api/v1/works/{work_id}/writing-tasks": {
"post": {
"tags": [
"创作生成"
],
"summary": "发起生成",
"operationId": "start_writing_task",
"parameters": [
{
"name": "work_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Work Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/muse______http____________________5"
}
}
}
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/muse______http____________________6"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/candidates/{candidate_id}/review-report": {
"get": {
"tags": [
"创作生成"
],
"summary": "读取审校报告",
"operationId": "read_candidate_review_report",
"parameters": [
{
"name": "candidate_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Candidate Id"
}
},
{
"name": "revision",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "null"
}
],
"title": "Revision"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/muse______http____________________7"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/chapters/{chapter_id}/writing-candidates": {
"get": {
"tags": [
@ -7100,6 +7211,61 @@
],
"title": "任务响应"
},
"muse______http__________________3": {
"properties": {
"checks": {
"items": {
"$ref": "#/components/schemas/muse______http___________________4"
},
"type": "array",
"title": "Checks"
},
"verdict": {
"type": "string",
"title": "Verdict"
}
},
"type": "object",
"required": [
"checks",
"verdict"
],
"title": "基础呈现"
},
"muse______http__________________4": {
"properties": {
"类别": {
"type": "string",
"title": "类别"
},
"判定": {
"type": "string",
"title": "判定"
},
"说明": {
"type": "string",
"title": "说明"
},
"证据": {
"additionalProperties": true,
"type": "object",
"title": "证据"
},
"位置": {
"type": "string",
"title": "位置",
"default": ""
}
},
"type": "object",
"required": [
"类别",
"判定",
"说明",
"证据"
],
"title": "发现呈现"
},
"muse______http___________________1": {
"properties": {
"source": {
@ -7185,6 +7351,51 @@
],
"title": "来源证据响应"
},
"muse______http___________________4": {
"properties": {
"名称": {
"type": "string",
"title": "名称"
},
"判定": {
"type": "string",
"title": "判定"
},
"证据": {
"type": "string",
"title": "证据"
}
},
"type": "object",
"required": [
"名称",
"判定",
"证据"
],
"title": "检查项呈现"
},
"muse______http___________________5": {
"properties": {
"findings": {
"items": {
"$ref": "#/components/schemas/muse______http__________________4"
},
"type": "array",
"title": "Findings"
},
"coverage": {
"additionalProperties": true,
"type": "object",
"title": "Coverage"
}
},
"type": "object",
"required": [
"findings",
"coverage"
],
"title": "连续性呈现"
},
"muse______http____________________1": {
"properties": {
"command_id": {
@ -7353,6 +7564,126 @@
"authorization_id"
],
"title": "原文归档请求"
},
"muse______http____________________5": {
"properties": {
"command_id": {
"type": "string",
"minLength": 1,
"title": "Command Id"
},
"chapter_id": {
"type": "string",
"minLength": 1,
"title": "Chapter Id"
},
"granularity": {
"type": "string",
"enum": [
"new_chapter",
"expand_scene",
"edit_selection"
],
"title": "Granularity"
},
"branch_id": {
"type": "string",
"title": "Branch Id",
"default": "main"
},
"purpose": {
"type": "string",
"title": "Purpose",
"default": ""
},
"config_id": {
"type": "string",
"minLength": 1,
"title": "Config Id"
},
"budget": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"title": "Budget"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"command_id",
"chapter_id",
"granularity",
"config_id"
],
"title": "发起生成请求"
},
"muse______http____________________6": {
"properties": {
"task_id": {
"type": "string",
"title": "Task Id"
},
"writing_task_id": {
"type": "string",
"title": "Writing Task Id"
}
},
"type": "object",
"required": [
"task_id",
"writing_task_id"
],
"title": "发起生成回执"
},
"muse______http____________________7": {
"properties": {
"report_id": {
"type": "string",
"title": "Report Id"
},
"candidate_id": {
"type": "string",
"title": "Candidate Id"
},
"candidate_revision": {
"type": "integer",
"title": "Candidate Revision"
},
"snapshot_id": {
"type": "string",
"title": "Snapshot Id"
},
"basic": {
"$ref": "#/components/schemas/muse______http__________________3"
},
"continuity": {
"$ref": "#/components/schemas/muse______http___________________5"
},
"verdict": {
"type": "string",
"title": "Verdict"
}
},
"type": "object",
"required": [
"report_id",
"candidate_id",
"candidate_revision",
"snapshot_id",
"basic",
"continuity",
"verdict"
],
"title": "审校报告呈现"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# 前端-应用与通用界面:文件合同
本页从[目标文件清单](../目标文件清单.json)投影,共 58 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
本页从[目标文件清单](../目标文件清单.json)投影,共 60 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
@ -57,6 +57,7 @@ agent-example/
│ │ ├── 任务重连.test.tsx # 持久化游标续接
│ │ ├── 候选审阅.test.tsx # 被审内容与批准范围
│ │ ├── 切作品隔离.test.tsx # 迟到响应不写错作品
│ │ ├── 创作对话.test.tsx # 创作对话组件渲染验证
│ │ ├── 动态字段与结构过期.test.tsx # 新字段可编辑,版本变化保留输入并复核
│ │ ├── 报告失效.test.tsx # 旧报告不显示当前通过
│ │ └── 隐藏字段不泄露.test.tsx # 撤回投影后清除缓存和显示
@ -68,6 +69,7 @@ agent-example/
│ │ ├── 动态类型到事实确认.spec.ts # 新类型全链消费
│ │ ├── 定稿导出与反馈.spec.ts # 固定稿件交付
│ │ ├── 拆书到方法绑定.spec.ts # 来源到方法消费
│ │ ├── 生成对话.spec.ts # 创作对话真实浏览器旅程验证
│ │ └── 编辑冲突与恢复.spec.ts # 编辑保存和任务恢复
│ ├── 编辑/
│ │ ├── 中文输入.test.tsx # 输入法组合时不破坏正文或提前保存
@ -724,3 +726,21 @@ agent-example/
- 目标:三层内容可见且无页面错误。
- 合同依据:行为验证。
- 用例合同:[NC-browser-context-workbench](../验证设计/工作台与工程用例.md#nc-browser-context-workbench)。
<a id="file-d377a9ce1938d9ac"></a>
### `web/tests/交互/创作对话.test.tsx`
- 归属与种类:验证;前端组件测试。
- 意图与职责:保护发起面板、消息流折叠与检查决定面板的渲染语义。
- 边界:离线组件,传输替身;真实后端旅程由 Playwright 承担。
- 目标:渲染破坏或状态误呈时用例失败。
<a id="file-d2adeda7317b3571"></a>
### `web/tests/旅程/生成对话.spec.ts`
- 归属与种类:验证;前端浏览器旅程。
- 意图与职责:真实浏览器与隔离库内核对创作对话、独立检查呈现与采纳阻断。
- 边界:真实隔离库与真实浏览器;不 mock 后端;视觉判定独立留证。
- 目标:通过任务与失败候选的检查呈现及服务端阻断可复核,页面无预期外控制台错误。

View File

@ -1,6 +1,6 @@
# 后端-流程编排:文件合同
本页从[目标文件清单](../目标文件清单.json)投影,共 15 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
本页从[目标文件清单](../目标文件清单.json)投影,共 16 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
@ -25,7 +25,8 @@ agent-example/
├── 研究拆书.py # 资料处理、分窗分析、恢复与成果交接
├── 章后处理.py # 摘要、事实提案、索引及影响复核
├── 经验改进.py # 具体变更评测、批准及目标模块启用
└── 规划章节.py # 前置条件、近期细化与规划候选
├── 规划章节.py # 前置条件、近期细化与规划候选
└── 载入模板.py # 流程模板 YAML 的受控载入
```
## 逐文件合同
@ -203,3 +204,14 @@ agent-example/
- 目标:正常安装后可从任意 cwd 导入,导入无运行副作用。
- 合同依据:Python 包身份。
- 非测试旧文件承接:[muse/flow/__init__.py](../旧文件处置/实现替换与合并.md#old-fadbb830631228ae)。
<a id="file-7e9bc3519f5f4119"></a>
### `src/muse/编排/载入模板.py`
- 归属与种类:编排;流程模板载入。
- 意图:让可审 YAML 配置成为流程定义的唯一来源。
- 职责:读取流程模板并映射为流程定义;结构不完整或含未登记字段即拒绝。
- 边界:只允许引用已登记处理器与固定版本;不能删除保护节点或运行任意脚本;载入产物交由流程校验冻结。
- 目标:模板损坏、缺字段或引用未知处理器在发布前失败,不静默降级。
- 合同依据:[流程编排](../模块设计/S02-任务运行.md)、配置/流程模板/生成正文.yaml。

View File

@ -1,6 +1,6 @@
# 后端-装配与接入:文件合同
本页从[目标文件清单](../目标文件清单.json)投影,共 50 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
本页从[目标文件清单](../目标文件清单.json)投影,共 51 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
@ -49,6 +49,7 @@ agent-example/
│ │ │ ├── 作者审阅.py # 作者审阅的类型化查询与动作
│ │ │ ├── 作者经验.py # 作者经验的类型化查询与动作
│ │ │ ├── 元数据.py # 元数据的类型化查询与动作
│ │ │ ├── 创作生成.py # 通用生成的类型化发起与审校报告查询
│ │ │ ├── 审校修订.py # 审校修订的类型化查询与动作
│ │ │ ├── 故事世界.py # 故事世界的类型化查询与动作
│ │ │ ├── 效果评测.py # 效果评测的类型化查询与动作
@ -626,3 +627,14 @@ agent-example/
- 边界:只走已登记的真实owner,不接SQL或作者自报身份。
- 目标:命令、版本、中文内容和错误能实际核对。
- 合同依据:[命令与作者决策](../接口契约/命令与作者决策.md)。
<a id="file-308ff8426766f36d"></a>
### `src/muse/接入/http/路由/创作生成.py`
- 归属与种类:接入;HTTP 路由。
- 意图:作者经工作台发起通用生成并读取候选审校报告。
- 职责:发起生成任务(运行配置由作者明确选择);按候选版本查询独立检查报告。
- 边界:只做协议、身份和序列化;不直写表、不替作者批准候选、不替作者选择配置。
- 目标:发起返回可追踪任务;报告与候选版本一一对应;错误保留错误码与可重试信息。
- 合同依据:[S02 任务运行](../模块设计/S02-任务运行.md)、[B06 审校修订](../模块设计/B06-审校修订.md)。

View File

@ -1,6 +1,6 @@
# 数据库与迁移:文件合同
本页从[目标文件清单](../目标文件清单.json)投影,共 64 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
本页从[目标文件清单](../目标文件清单.json)投影,共 65 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
@ -78,7 +78,8 @@ agent-example/
├── V0017__跨模块引用约束.sql # 跨模块引用约束结构迁移
├── V0018__工作台查询视图.sql # 工作台查询视图结构迁移
├── V0019__用途隔离与数据权限.sql # 用途隔离与数据权限结构迁移
└── V0020__上下文快照与消费.sql # 上下文快照与消费记录结构迁移
├── V0020__上下文快照与消费.sql # 上下文快照与消费记录结构迁移
└── V0021__模型候选与审校报告.sql # 模型候选来源与审校报告结构迁移
```
## 逐文件合同
@ -823,3 +824,14 @@ agent-example/
- 边界:只执行本版本声明的结构变化;不占用其他工作包预留序号,不以启动自动修表或复用旧序号覆盖历史。
- 目标:迁移顺序与校验和可复验,失败不留下假完成版本。
- 合同依据:结构迁移。
<a id="file-143f30dddeddc0bd"></a>
### `数据库/迁移/V0021__模型候选与审校报告.sql`
- 归属与种类:数据演进;迁移。
- 意图:为模型候选来源证据与版本化审校报告提供数据库演进。
- 职责:扩展候选来源约束接纳 `model`;新增模型候选证据绑定表与审校报告表(绑定候选版本与所依据快照)。
- 边界:只执行本版本声明的结构变化;候选来源约束显式核对后扩展;报告与绑定只追加不改写。
- 目标:迁移顺序与校验和可复验,失败不留下假完成版本。
- 合同依据:[存储与对象关系](../数据模型/存储与对象关系.md)。

View File

@ -15572,6 +15572,12 @@
"design_location": "文件设计/测试与夹具.md#file-44561d809cfc797b",
"legacy_sources": [],
"test_case_refs": [
"NC-w15-basic-invalid",
"NC-w15-basic-output",
"NC-w15-basic-truncation",
"NC-w15-continuity-clean",
"NC-w15-continuity-conflict",
"NC-w15-report-verdict",
"TC-0c07123f6d61",
"TC-13172a870204",
"TC-16ef9c430150",
@ -16326,6 +16332,11 @@
"design_location": "文件设计/测试与夹具.md#file-f99a864b9562c655",
"legacy_sources": [],
"test_case_refs": [
"NC-w15-check-gate",
"NC-w15-cross-scope-denied",
"NC-w15-model-self-fill",
"NC-w15-stream-locatable",
"NC-w15-two-phase-ok",
"TC-0265d39884be",
"TC-030539563fd7",
"TC-046e516fb393",
@ -21487,6 +21498,87 @@
"test_case_refs": [
"NC-browser-context-workbench"
]
},
{
"path": "src/muse/接入/http/路由/创作生成.py",
"owner": "接入",
"kind": "源码",
"responsibility": "通用生成的类型化发起与审校报告查询",
"project_part": "后端",
"intent": "把 通用生成与检查 的业务接口映射为稳定 HTTP 动作",
"boundary": "只做协议、身份和序列化;运行配置由作者明确选择,不替作者批准候选",
"goal": "发起返回可追踪任务,报告查询绑定候选版本,错误保留可重试信息",
"contract_basis": "HTTP 路由",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/后端-装配与接入.md#file-308ff8426766f36d",
"test_case_refs": []
},
{
"path": "src/muse/编排/载入模板.py",
"owner": "编排",
"kind": "源码",
"responsibility": "流程模板 YAML 的受控载入",
"project_part": "后端",
"intent": "让可审配置成为流程定义的唯一来源",
"boundary": "只映射已登记处理器与固定版本;不删除保护节点、不执行任意脚本",
"goal": "载入产物经流程校验冻结,模板损坏或越权字段即拒绝",
"contract_basis": "流程编排",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/后端-流程编排.md#file-7e9bc3519f5f4119",
"test_case_refs": []
},
{
"path": "数据库/迁移/V0021__模型候选与审校报告.sql",
"owner": "数据演进",
"kind": "迁移",
"responsibility": "模型候选来源与审校报告结构迁移",
"project_part": "后端",
"intent": "为模型候选证据绑定与版本化审校报告提供结构演进",
"boundary": "只执行本版本声明的结构变化;候选来源约束显式核对后扩展,不占用其他工作包预留序号",
"goal": "迁移顺序与校验和可复验,失败不留下假完成版本",
"contract_basis": "结构迁移",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/数据库与迁移.md#file-143f30dddeddc0bd",
"test_case_refs": []
},
{
"path": "web/tests/交互/创作对话.test.tsx",
"owner": "验证",
"kind": "测试",
"responsibility": "创作对话组件渲染验证",
"project_part": "前端",
"intent": "保护发起面板、消息流与检查面板的渲染语义",
"boundary": "离线组件,传输替身;真实后端旅程由 Playwright 承担",
"goal": "渲染破坏或状态误呈时用例失败",
"contract_basis": "行为验证",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/前端-应用与通用界面.md#file-d377a9ce1938d9ac",
"test_case_refs": [
"NC-ui-dialog-empty",
"NC-ui-dialog-start",
"NC-ui-dialog-stream"
]
},
{
"path": "web/tests/旅程/生成对话.spec.ts",
"owner": "验证",
"kind": "测试",
"responsibility": "创作对话真实浏览器旅程验证",
"project_part": "前端",
"intent": "保护生成对话、独立检查与采纳阻断在真实浏览器可见",
"boundary": "真实隔离库与真实浏览器;不 mock 后端",
"goal": "通过任务与失败候选的检查呈现及服务端阻断可复核",
"contract_basis": "行为验证",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/前端-应用与通用界面.md#file-d2adeda7317b3571",
"test_case_refs": [
"NC-browser-generation-dialog"
]
}
],
"contract_fields": {

View File

@ -916,6 +916,7 @@ agent-example/
│ │ │ │ ├── 作者审阅.py # 作者审阅的类型化查询与动作
│ │ │ │ ├── 作者经验.py # 作者经验的类型化查询与动作
│ │ │ │ ├── 元数据.py # 元数据的类型化查询与动作
│ │ │ │ ├── 创作生成.py # 通用生成的类型化发起与审校报告查询
│ │ │ │ ├── 审校修订.py # 审校修订的类型化查询与动作
│ │ │ │ ├── 故事世界.py # 故事世界的类型化查询与动作
│ │ │ │ ├── 效果评测.py # 效果评测的类型化查询与动作
@ -1013,7 +1014,8 @@ agent-example/
│ │ ├── 研究拆书.py # 资料处理、分窗分析、恢复与成果交接
│ │ ├── 章后处理.py # 摘要、事实提案、索引及影响复核
│ │ ├── 经验改进.py # 具体变更评测、批准及目标模块启用
│ │ └── 规划章节.py # 前置条件、近期细化与规划候选
│ │ ├── 规划章节.py # 前置条件、近期细化与规划候选
│ │ └── 载入模板.py # 流程模板 YAML 的受控载入
│ ├── 资料研究/
│ │ ├── __init__.py # Python 包身份
│ │ ├── 分析版本.py # 分窗成果、修复、重算与版本选用
@ -1342,6 +1344,7 @@ agent-example/
│ │ │ ├── 任务重连.test.tsx # 持久化游标续接
│ │ │ ├── 候选审阅.test.tsx # 被审内容与批准范围
│ │ │ ├── 切作品隔离.test.tsx # 迟到响应不写错作品
│ │ │ ├── 创作对话.test.tsx # 创作对话组件渲染验证
│ │ │ ├── 动态字段与结构过期.test.tsx # 新字段可编辑,版本变化保留输入并复核
│ │ │ ├── 报告失效.test.tsx # 旧报告不显示当前通过
│ │ │ ├── 退出会话.test.tsx # 退出作者会话后的页面和订阅生命周期
@ -1357,6 +1360,7 @@ agent-example/
│ │ │ ├── 定稿导出与反馈.spec.ts # 固定稿件交付
│ │ │ ├── 拆书到方法绑定.spec.ts # 来源到方法消费
│ │ │ ├── 探索草稿.spec.ts # 私人探索的实际作者浏览器旅程
│ │ │ ├── 生成对话.spec.ts # 创作对话真实浏览器旅程验证
│ │ │ └── 编辑冲突与恢复.spec.ts # 编辑保存和任务恢复
│ │ ├── 编辑/
│ │ │ ├── 中文输入.test.tsx # 输入法组合时不破坏正文或提前保存
@ -1446,7 +1450,8 @@ agent-example/
│ ├── V0017__跨模块引用约束.sql # 跨模块引用约束结构迁移
│ ├── V0018__工作台查询视图.sql # 工作台查询视图结构迁移
│ ├── V0019__用途隔离与数据权限.sql # 用途隔离与数据权限结构迁移
│ └── V0020__上下文快照与消费.sql # 上下文快照与消费记录结构迁移
│ ├── V0020__上下文快照与消费.sql # 上下文快照与消费记录结构迁移
│ └── V0021__模型候选与审校报告.sql # 模型候选来源与审校报告结构迁移
├── 部署/
│ ├── Dockerfile # 前端构建和后端发布镜像
│ ├── compose.test.yaml # 隔离 PostgreSQL 和测试环境

View File

@ -4966,3 +4966,135 @@
- 环境:真实隔离PostgreSQL。
- 合同:[权威定义](../模块设计/B09-上下文.md)。
<a id="nc-w15-two-phase-ok"></a>
## NC-w15-two-phase-ok · test_任意合法章位生成模型候选并落独立检查报告__b41e29
- 位置:`tests/集成/test_两阶段写手.py::test_任意合法章位生成模型候选并落独立检查报告__b41e29`。
- 给定:两作品两章位各有已确认细纲与事实,配置已启用。
- 动作:走完五步生成正文流程(合成宿主)。
- 预期:写作回合禁工具;候选 origin=model;检查报告 passed;事件含正文/候选/检查;两章位候选不同。
- 环境:真实隔离PostgreSQL。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-cross-scope-denied"></a>
## NC-w15-cross-scope-denied · test_跨作品与无细纲章位拒绝发起__c73d0f
- 位置:`tests/集成/test_两阶段写手.py::test_跨作品与无细纲章位拒绝发起__c73d0f`。
- 给定:无细纲章位与跨作品章位。
- 动作:发起生成正文。
- 预期:无细纲授权拒绝;章位归属不一致拒绝。
- 环境:真实隔离PostgreSQL。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-check-gate"></a>
## NC-w15-check-gate · test_必需检查失败不能采纳且保留失败候选证据__d52a8e
- 位置:`tests/集成/test_两阶段写手.py::test_必需检查失败不能采纳且保留失败候选证据__d52a8e`。
- 给定:候选复读细纲硬约束措辞。
- 动作:独立检查后作者尝试采纳再拒绝。
- 预期:verdict=failed;采纳 CHECK_FAILED 拒绝;拒绝决定可落库。
- 环境:真实隔离PostgreSQL。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-stream-locatable"></a>
## NC-w15-stream-locatable · test_无终态断流可定位且重开后任务与事件仍可找回__e6f317
- 位置:`tests/集成/test_两阶段写手.py::test_无终态断流可定位且重开后任务与事件仍可找回__e6f317`。
- 给定:写作回合宿主 500。
- 动作:任务执行后重读任务与事件。
- 预期:任务未无声消失;写作步骤未完成;事件可回放;无残留候选。
- 环境:真实隔离PostgreSQL。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-model-self-fill"></a>
## NC-w15-model-self-fill · test_模型自填无效输出被输出合同拒绝__93c5b7
- 位置:`tests/集成/test_两阶段写手.py::test_模型自填无效输出被输出合同拒绝__93c5b7`。
- 给定:模型输出夹带 checks_passed 等额外字段。
- 动作:走生成流程。
- 预期:输出合同 additionalProperties=false 拒绝;步骤失败可定位。
- 环境:真实隔离PostgreSQL。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-basic-invalid"></a>
## NC-w15-basic-invalid · test_空内容与空段结构失败且证据明确__a19b42
- 位置:`tests/契约/test_连续性审校合同.py::test_空内容与空段结构失败且证据明确__a19b42`。
- 给定:全空白正文。
- 动作:运行基础检查。
- 预期:空内容失败;结构失败。
- 环境:离线组件纯函数。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-basic-truncation"></a>
## NC-w15-basic-truncation · test_末段无收束判截断合法收束不误报__b28c53
- 位置:`tests/契约/test_连续性审校合同.py::test_末段无收束判截断合法收束不误报__b28c53`。
- 给定:末段无收束标点或有收束。
- 动作:运行基础检查。
- 预期:无收束判截断;有收束通过。
- 环境:离线组件纯函数。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-basic-output"></a>
## NC-w15-basic-output · test_原始交付损坏判输出有效性失败__c39d64
- 位置:`tests/契约/test_连续性审校合同.py::test_原始交付损坏判输出有效性失败__c39d64`。
- 给定:原始交付非合法 JSON。
- 动作:运行基础检查。
- 预期:输出有效性失败。
- 环境:离线组件纯函数。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-continuity-conflict"></a>
## NC-w15-continuity-conflict · test_必出场缺失与复读细纲措辞均为冲突级发现__d4ae75
- 位置:`tests/契约/test_连续性审校合同.py::test_必出场缺失与复读细纲措辞均为冲突级发现__d4ae75`。
- 给定:必出场缺失且复读硬约束短句。
- 动作:运行连续性审校。
- 预期:conflict 级发现含泄漏短语。
- 环境:离线组件纯函数。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-continuity-clean"></a>
## NC-w15-continuity-clean · test_出场齐全且不复读时无发现并记录覆盖__e5bf86
- 位置:`tests/契约/test_连续性审校合同.py::test_出场齐全且不复读时无发现并记录覆盖__e5bf86`。
- 给定:出场齐全且不复读。
- 动作:运行连续性审校。
- 预期:无发现;覆盖记录含快照与规则版本。
- 环境:离线组件纯函数。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。
<a id="nc-w15-report-verdict"></a>
## NC-w15-report-verdict · test_报告总判定与基础及冲突发现一致且绑定版本__f6c097
- 位置:`tests/契约/test_连续性审校合同.py::test_报告总判定与基础及冲突发现一致且绑定版本__f6c097`。
- 给定:通过与失败两组输入。
- 动作:组装审校报告。
- 预期:判定一致;篡改总判定被拒。
- 环境:离线组件纯函数。
- 合同:[权威定义](../模块设计/B05-正文写作.md)。

View File

@ -3186,3 +3186,51 @@
- 环境:真实浏览器。
- 合同:[权威定义](../接口契约/任务工具与事件.md)。
<a id="nc-ui-dialog-stream"></a>
## NC-ui-dialog-stream · NC-ui-dialog-stream:正文段落按序累积、动作事件标注
- 位置:`web/tests/交互/创作对话.test.tsx::NC-ui-dialog-stream:正文段落按序累积、动作事件标注`。
- 给定:创作对话组件传输替身。
- 动作:渲染与提交。
- 预期:按合同呈现。
- 环境:离线组件传输替身。
- 合同:[权威定义](../接口契约/任务工具与事件.md)。
<a id="nc-ui-dialog-empty"></a>
## NC-ui-dialog-empty · NC-ui-dialog-empty:无事件时给出找回提示不空白
- 位置:`web/tests/交互/创作对话.test.tsx::NC-ui-dialog-empty:无事件时给出找回提示不空白`。
- 给定:创作对话组件传输替身。
- 动作:渲染与提交。
- 预期:按合同呈现。
- 环境:离线组件传输替身。
- 合同:[权威定义](../接口契约/任务工具与事件.md)。
<a id="nc-ui-dialog-start"></a>
## NC-ui-dialog-start · NC-ui-dialog-start:发起成功展示任务回执
- 位置:`web/tests/交互/创作对话.test.tsx::NC-ui-dialog-start:发起成功展示任务回执`。
- 给定:创作对话组件传输替身。
- 动作:渲染与提交。
- 预期:按合同呈现。
- 环境:离线组件传输替身。
- 合同:[权威定义](../接口契约/任务工具与事件.md)。
<a id="nc-browser-generation-dialog"></a>
## NC-browser-generation-dialog · NC-browser-generation-dialog:创作对话、独立检查与采纳阻断
- 位置:`web/tests/旅程/生成对话.spec.ts::NC-browser-generation-dialog:创作对话、独立检查与采纳阻断`。
- 给定:隔离后端已有跑完的通过与失败生成任务。
- 动作:真实浏览器打开任务。
- 预期:创作对话与正文可见;检查报告可见;失败候选采纳被阻断。
- 环境:真实浏览器隔离库。
- 合同:[权威定义](../接口契约/任务工具与事件.md)。

View File

@ -122641,6 +122641,297 @@
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-conflict-row",
"reason": "W14 收尾补测:回合3/回合2 记录的覆盖缺口按功能价值补齐,不承接已退役旧用例身份。"
},
{
"case_id": "NC-w15-two-phase-ok",
"file": "tests/集成/test_两阶段写手.py",
"symbol": "test_任意合法章位生成模型候选并落独立检查报告__b41e29",
"given": "两作品两章位各有已确认细纲与事实,配置已启用",
"when": "走完五步生成正文流程(合成宿主)",
"then": [
"写作回合禁工具",
"候选 origin=model",
"检查报告 passed",
"事件含正文/候选/检查",
"两章位候选不同"
],
"environment": "真实隔离PostgreSQL",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15数据库全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-two-phase-ok",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-cross-scope-denied",
"file": "tests/集成/test_两阶段写手.py",
"symbol": "test_跨作品与无细纲章位拒绝发起__c73d0f",
"given": "无细纲章位与跨作品章位",
"when": "发起生成正文",
"then": [
"无细纲授权拒绝",
"章位归属不一致拒绝"
],
"environment": "真实隔离PostgreSQL",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15数据库全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-cross-scope-denied",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-check-gate",
"file": "tests/集成/test_两阶段写手.py",
"symbol": "test_必需检查失败不能采纳且保留失败候选证据__d52a8e",
"given": "候选复读细纲硬约束措辞",
"when": "独立检查后作者尝试采纳再拒绝",
"then": [
"verdict=failed",
"采纳 CHECK_FAILED 拒绝",
"拒绝决定可落库"
],
"environment": "真实隔离PostgreSQL",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15数据库全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-check-gate",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-stream-locatable",
"file": "tests/集成/test_两阶段写手.py",
"symbol": "test_无终态断流可定位且重开后任务与事件仍可找回__e6f317",
"given": "写作回合宿主 500",
"when": "任务执行后重读任务与事件",
"then": [
"任务未无声消失",
"写作步骤未完成",
"事件可回放",
"无残留候选"
],
"environment": "真实隔离PostgreSQL",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15数据库全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-stream-locatable",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-model-self-fill",
"file": "tests/集成/test_两阶段写手.py",
"symbol": "test_模型自填无效输出被输出合同拒绝__93c5b7",
"given": "模型输出夹带 checks_passed 等额外字段",
"when": "走生成流程",
"then": [
"输出合同 additionalProperties=false 拒绝",
"步骤失败可定位"
],
"environment": "真实隔离PostgreSQL",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15数据库全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-model-self-fill",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-basic-invalid",
"file": "tests/契约/test_连续性审校合同.py",
"symbol": "test_空内容与空段结构失败且证据明确__a19b42",
"given": "全空白正文",
"when": "运行基础检查",
"then": [
"空内容失败",
"结构失败"
],
"environment": "离线组件纯函数",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15离线全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-basic-invalid",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-basic-truncation",
"file": "tests/契约/test_连续性审校合同.py",
"symbol": "test_末段无收束判截断合法收束不误报__b28c53",
"given": "末段无收束标点或有收束",
"when": "运行基础检查",
"then": [
"无收束判截断",
"有收束通过"
],
"environment": "离线组件纯函数",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15离线全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-basic-truncation",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-basic-output",
"file": "tests/契约/test_连续性审校合同.py",
"symbol": "test_原始交付损坏判输出有效性失败__c39d64",
"given": "原始交付非合法 JSON",
"when": "运行基础检查",
"then": [
"输出有效性失败"
],
"environment": "离线组件纯函数",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15离线全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-basic-output",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-continuity-conflict",
"file": "tests/契约/test_连续性审校合同.py",
"symbol": "test_必出场缺失与复读细纲措辞均为冲突级发现__d4ae75",
"given": "必出场缺失且复读硬约束短句",
"when": "运行连续性审校",
"then": [
"conflict 级发现含泄漏短语"
],
"environment": "离线组件纯函数",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15离线全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-continuity-conflict",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-continuity-clean",
"file": "tests/契约/test_连续性审校合同.py",
"symbol": "test_出场齐全且不复读时无发现并记录覆盖__e5bf86",
"given": "出场齐全且不复读",
"when": "运行连续性审校",
"then": [
"无发现",
"覆盖记录含快照与规则版本"
],
"environment": "离线组件纯函数",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15离线全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-continuity-clean",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-w15-report-verdict",
"file": "tests/契约/test_连续性审校合同.py",
"symbol": "test_报告总判定与基础及冲突发现一致且绑定版本__f6c097",
"given": "通过与失败两组输入",
"when": "组装审校报告",
"then": [
"判定一致",
"篡改总判定被拒"
],
"environment": "离线组件纯函数",
"contract": "模块设计/B05-正文写作.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15离线全量.xml"
],
"verification_state": "通过",
"design_location": "验证设计/创作与正式变更用例.md#nc-w15-report-verdict",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。"
},
{
"case_id": "NC-ui-dialog-stream",
"file": "web/tests/交互/创作对话.test.tsx",
"symbol": "NC-ui-dialog-stream:正文段落按序累积、动作事件标注",
"given": "创作对话组件传输替身",
"when": "渲染与提交",
"then": [
"按合同呈现"
],
"environment": "离线组件传输替身",
"contract": "接口契约/任务工具与事件.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15前端测试.log"
],
"verification_state": "通过",
"design_location": "验证设计/工作台与工程用例.md#nc-ui-dialog-stream",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。",
"code_sha256": "ec4914850ad3696663ff13d288b3bf66cc7f2f2c205c202551d038840f7a5724"
},
{
"case_id": "NC-ui-dialog-empty",
"file": "web/tests/交互/创作对话.test.tsx",
"symbol": "NC-ui-dialog-empty:无事件时给出找回提示不空白",
"given": "创作对话组件传输替身",
"when": "渲染与提交",
"then": [
"按合同呈现"
],
"environment": "离线组件传输替身",
"contract": "接口契约/任务工具与事件.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15前端测试.log"
],
"verification_state": "通过",
"design_location": "验证设计/工作台与工程用例.md#nc-ui-dialog-empty",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。",
"code_sha256": "ec4914850ad3696663ff13d288b3bf66cc7f2f2c205c202551d038840f7a5724"
},
{
"case_id": "NC-ui-dialog-start",
"file": "web/tests/交互/创作对话.test.tsx",
"symbol": "NC-ui-dialog-start:发起成功展示任务回执",
"given": "创作对话组件传输替身",
"when": "渲染与提交",
"then": [
"按合同呈现"
],
"environment": "离线组件传输替身",
"contract": "接口契约/任务工具与事件.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/W15前端测试.log"
],
"verification_state": "通过",
"design_location": "验证设计/工作台与工程用例.md#nc-ui-dialog-start",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。",
"code_sha256": "ec4914850ad3696663ff13d288b3bf66cc7f2f2c205c202551d038840f7a5724"
},
{
"case_id": "NC-browser-generation-dialog",
"file": "web/tests/旅程/生成对话.spec.ts",
"symbol": "NC-browser-generation-dialog:创作对话、独立检查与采纳阻断",
"given": "隔离后端已有跑完的通过与失败生成任务",
"when": "真实浏览器打开任务",
"then": [
"创作对话与正文可见",
"检查报告可见",
"失败候选采纳被阻断"
],
"environment": "真实浏览器隔离库",
"contract": "接口契约/任务工具与事件.md",
"execution_evidence": [
".agents.local/改造/R2-20260909/W15/回合开发/整包/生成对话浏览器.json"
],
"verification_state": "通过",
"design_location": "验证设计/工作台与工程用例.md#nc-browser-generation-dialog",
"reason": "W15 通用生成、基本审校与候选:两阶段写手全链与独立检查,按功能价值新写。",
"code_sha256": "971edc8da5e0945d4d8f39f56dde83bbb4c103459f8a6c6148d473e195a1f6a1"
}
]
}

View File

@ -86,6 +86,25 @@ class 调用计价(Protocol):
def 金额(self, 结果: 模型结果) -> Decimal | None: ...
def _阶段工具(设定: dict, 阶段: str) -> tuple[str, ...]:
"""固定配置的工具集优先按阶段取(两阶段写手探索带工具、生成禁工具)。"""
分阶段 = 设定.get("stage_tools")
if isinstance(分阶段, dict) and 阶段 in 分阶段:
return tuple(分阶段[阶段])
return tuple(设定.get("tools", []))
class 未知计价:
"""尚未接入可靠费用规则时的诚实计价:返回未知,不推定免费。"""
def __init__(self, 版本: str) -> None:
self.版本 = 版本
def 金额(self, 结果: 模型结果) -> Decimal | None:
return None
@dataclass(frozen=True)
class 模型交付:
内容: Any
@ -174,7 +193,13 @@ class 模型执行器:
请求.model,
请求.thinking,
tuple(t["name"] for t in 请求.工具),
) != (设定["provider"], 设定["model"], 设定["thinking"], tuple(设定.get("tools", []))):
) != (
设定["provider"],
设定["model"],
设定["thinking"],
_阶段工具(设定, 阶段),
):
raise 模型协议错误("角色调用与任务固定的运行配置不一致")
raise 模型协议错误("角色调用与任务固定的运行配置不一致")
if self.计价.版本 != self.固定配置.内容.计价版本:
raise 模型协议错误("费用规则与任务固定配置版本不一致")

View File

@ -96,10 +96,16 @@ class 运行配置内容:
"model",
"thinking",
"tools",
"stage_tools",
"input_schema",
"output_schema",
}:
raise 配置版本错误("角色配置包含不支持的字段;凭据只能放在引用表")
if "stage_tools" in 策略 and (
not isinstance(策略["stage_tools"], dict)
or not all(isinstance(v, list) for v in 策略["stage_tools"].values())
):
raise 配置版本错误("stage_tools 必须是阶段到工具名单的映射")
for 字段 in ("provider", "model", "thinking"):
if not isinstance(策略.get(字段), str) or not 策略[字段].strip():
raise 配置版本错误("角色 provider、model 与 thinking 必须显式配置")

View File

@ -74,6 +74,7 @@ class 应用装配:
故事世界: 故事服务 | None = None
规划: 规划服务 | None = None
上下文: 上下文服务 | None = None
流程登记: 流程登记 | None = None
def 要求上下文(self) -> 上下文服务:
if self.上下文 is None:
@ -185,12 +186,14 @@ def 构建(配置: 应用配置, *, 时钟: 时钟 | None = None, 流程: 流程
正式 = 正式变更服务(数据库, 目录)
作品 = 作品服务(数据库, 正式)
正文 = 正文服务(数据库, 正式)
任务 = 任务服务(数据库, 流程 or 流程登记())
登记 = 流程 or 流程登记()
任务 = 任务服务(数据库, 登记)
return 应用装配(
配置=配置,
时钟=时钟 or 真实时钟(),
数据库=数据库,
任务运行=任务,
流程登记=登记,
作品=作品,
正文=正文,
作品进度=创作进度服务(作品, 正文, 任务),
@ -200,6 +203,21 @@ def 构建(配置: 应用配置, *, 时钟: 时钟 | None = None, 流程: 流程
)
def 装配生成正文(装配: 应用装配, 计价, 模板路径: Path | None = None) -> None:
"""登记并发布生成正文流程;计价由调用方注入,模板默认取仓内可审配置。"""
from muse.编排.接口 import 流程服务
from muse.编排.生成正文 import 登记生成正文
from muse.编排.载入模板 import 载入流程定义
登记 = 装配.流程登记
if 登记 is None or 装配.任务运行 is None:
raise 装配未完成("任务运行与流程登记未注入")
登记生成正文(登记, 装配, 计价)
定义 = 载入流程定义(模板路径 or Path("配置/流程模板/生成正文.yaml"))
流程服务(装配.任务运行, 登记).发布(定义)
def 自检(装配: 应用装配) -> dict[str, Any]:
"""进程内自检:报告已注入实现与缺口;不连接任何外部服务。"""
return {

View File

@ -0,0 +1 @@
"""审校修订模块;跨模块只经 接口.py 导入。"""

View File

@ -0,0 +1,76 @@
"""基础有效性检查:空内容、截断、结构与输出有效性;全部确定性,不调模型。"""
from __future__ import annotations
import json
from muse.审校修订.模型 import 基础检查报告, 审校错误, 检查项
from muse.正文写作.模型 import 文本节点, 正文草稿
from muse.正文写作.正文格式 import 可见文本
# 末段收束标点:句末、问叹、省略、引号/括号/书名号闭合与破折号收尾均视为完整收束;
# 逗号、分号、冒号、顿号结尾视为疑似截断。
_收束尾字符 = "。!?…」』)】》”’—―"
def _末段收束(文本: str) -> bool:
尾 = 文本.rstrip()
return bool(尾) and 尾[-1] in _收束尾字符
def _段间空隙(草稿: 正文草稿) -> int:
空段 = 0
for 段 in 草稿.paragraphs:
内容 = "".join(n.text if isinstance(n, 文本节点) else n.newline for n in 段.content)
if not 内容.strip():
空段 += 1
return 空段
def 运行基础检查(草稿: 正文草稿, *, 原始交付: str | None = None) -> 基础检查报告:
"""对候选草稿执行确定性有效性检查。
原始交付是模型输出的原始文本(若可得):用于识别结构化输出损坏;
不提供时输出有效性按草稿自身结构判定。
"""
项: list[检查项] = []
文本 = 可见文本(草稿)
if not 文本.strip():
项.append(检查项("空内容", "failed", "全文可见文本为空"))
else:
项.append(检查项("空内容", "passed", f"可见文本 {len(文本)} 字符"))
空段 = _段间空隙(草稿)
if 空段:
项.append(检查项("结构", "failed", f"存在 {空段} 个无可见内容段落"))
else:
项.append(检查项("结构", "passed", f"{len(草稿.paragraphs)} 段均含可见内容"))
段数 = len(草稿.paragraphs)
末段 = 可见文本(草稿).splitlines()[-1] if 段数 else ""
if 段数 and not _末段收束(末段):
项.append(检查项("截断", "failed", "末段未以收束标点结束,疑似截断"))
else:
项.append(检查项("截断", "passed", "末段收束完整"))
if 原始交付 is not None:
try:
json.loads(原始交付)
except (json.JSONDecodeError, TypeError):
项.append(检查项("输出有效性", "failed", "模型原始交付不是合法结构化输出"))
else:
项.append(检查项("输出有效性", "passed", "模型原始交付可解析"))
else:
项.append(检查项("输出有效性", "passed", "无原始交付,按草稿结构判定"))
判定: str = "failed" if any(i.判定 == "failed" for i in 项) else "passed"
return 基础检查报告(tuple(项), 判定) # type: ignore[arg-type]
def 判定必需通过(报告: 基础检查报告) -> bool:
"""必需检查是否通过;供采纳门使用,语义独立于呈现。"""
return 报告.判定 == "passed"
__all__ = ["运行基础检查", "判定必需通过", "审校错误"]

View File

@ -0,0 +1,107 @@
"""审校修订记录读写:报告与候选版本一一绑定,使用调用方事务,不改写历史。"""
from __future__ import annotations
from typing import Any
from uuid import NAMESPACE_URL, uuid5
from psycopg.types.json import Jsonb
from muse.审校修订.模型 import 审校报告, 审校错误
from muse.正式变更.接口 import 固定哈希
class 审校存储:
def __init__(self, 连) -> None:
self.连 = 连
def 写入报告(self, 报告: 审校报告) -> dict[str, Any]:
"""同一候选版本重复写入同一报告幂等;不同内容冲突拒绝。"""
身份 = str(
uuid5(
NAMESPACE_URL,
"muse:B06:report:" + 固定哈希([报告.candidate_id, 报告.candidate_revision]),
)
)
if 报告.report_id != 身份:
raise 审校错误("报告身份必须由候选身份与版本确定性派生")
载荷 = (
Jsonb(
{
"checks": [
{"名称": c.名称, "判定": c.判定, "证据": c.证据} for c in 报告.basic.检查
],
"verdict": 报告.basic.判定,
}
),
Jsonb(
{
"findings": [
{
"类别": f.类别,
"判定": f.判定,
"说明": f.说明,
"证据": f.证据,
"位置": f.位置,
}
for f in 报告.continuity.findings
],
"coverage": 报告.continuity.coverage,
}
),
)
行 = self.连.execute(
"SELECT report_id, basic, continuity, verdict FROM muse_review_report "
"WHERE candidate_id=%s AND candidate_revision=%s",
(报告.candidate_id, 报告.candidate_revision),
).fetchone()
if 行 is not None:
if (行[3], 行[1], 行[2]) != (报告.verdict, 载荷[0].obj, 载荷[1].obj):
raise 审校错误("该候选版本已有不同报告,不能改写历史")
return {"report_id": 行[0], "verdict": 行[3]}
self.连.execute(
"INSERT INTO muse_review_report"
"(report_id,candidate_id,candidate_revision,snapshot_id,basic,continuity,"
"verdict,coverage) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
(
身份,
报告.candidate_id,
报告.candidate_revision,
报告.snapshot_id,
载荷[0],
载荷[1],
报告.verdict,
Jsonb(报告.continuity.coverage),
),
)
return {"report_id": 身份, "verdict": 报告.verdict}
def 读取报告(self, candidate_id: str, candidate_revision: int) -> dict[str, Any] | None:
行 = self.连.execute(
"SELECT report_id,candidate_id,candidate_revision,snapshot_id,basic,"
"continuity,verdict,coverage,created_at FROM muse_review_report "
"WHERE candidate_id=%s AND candidate_revision=%s",
(candidate_id, candidate_revision),
).fetchone()
if 行 is None:
return None
return {
"report_id": 行[0],
"candidate_id": 行[1],
"candidate_revision": 行[2],
"snapshot_id": 行[3],
"basic": 行[4],
"continuity": 行[5],
"verdict": 行[6],
"coverage": 行[7],
"created_at": 行[8],
}
def 列出报告(self, candidate_id: str) -> list[dict[str, Any]]:
行集 = self.连.execute(
"SELECT candidate_revision,verdict,created_at FROM muse_review_report "
"WHERE candidate_id=%s ORDER BY candidate_revision",
(candidate_id,),
).fetchall()
return [{"candidate_revision": r[0], "verdict": r[1], "created_at": r[2]} for r in 行集]

View File

@ -0,0 +1,101 @@
"""审校修订公开用例;HTTP、CLI 与编排对相同用例得到一致结果。"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.审校修订.基础检查 import 运行基础检查
from muse.审校修订.存储 import 审校存储
from muse.审校修订.模型 import 基础检查报告, 审校报告, 审校错误, 连续性报告
from muse.审校修订.连续性审校 import 运行连续性审校
from muse.正文写作.模型 import 正文草稿
from muse.正文写作.正文格式 import 可见文本
def 组装审校报告(
草稿: 正文草稿,
细纲投影: Mapping[str, Any],
出场名映射: Mapping[str, str],
*,
candidate_id: str,
candidate_revision: int,
snapshot_id: str,
原始交付: str | None = None,
) -> 审校报告:
"""在同一证据口径下执行基础与连续性检查并组装报告;不落库。"""
基础 = 运行基础检查(草稿, 原始交付=原始交付)
连续 = 运行连续性审校(可见文本(草稿), 细纲投影, 出场名映射, 依据快照=snapshot_id)
有冲突 = any(f.判定 == "conflict" for f in 连续.findings)
判定 = "failed" if (基础.判定 == "failed" or 有冲突) else "passed"
return 审校报告("", candidate_id, candidate_revision, snapshot_id, 基础, 连续, 判定)
class 审校服务:
"""独立检查入口:编排处理器传入任务事务连接;工作台只读查询。"""
def __init__(self, 数据库: 数据库工厂) -> None:
self.数据库 = 数据库
def 检查并保存(
self,
连,
草稿: 正文草稿,
细纲投影: Mapping[str, Any],
出场名映射: Mapping[str, str],
*,
candidate_id: str,
candidate_revision: int,
snapshot_id: str,
原始交付: str | None = None,
) -> dict[str, Any]:
报告 = 组装审校报告(
草稿,
细纲投影,
出场名映射,
candidate_id=candidate_id,
candidate_revision=candidate_revision,
snapshot_id=snapshot_id,
原始交付=原始交付,
)
return 审校存储(连).写入报告(补齐报告身份(报告))
def 读取报告(self, candidate_id: str, candidate_revision: int) -> dict[str, Any] | None:
with self.数据库.连接(只读=True) as 连:
return 审校存储(连).读取报告(candidate_id, candidate_revision)
def 列出报告(self, candidate_id: str) -> list[dict[str, Any]]:
with self.数据库.连接(只读=True) as 连:
return 审校存储(连).列出报告(candidate_id)
def 补齐报告身份(报告: 审校报告) -> 审校报告:
"""补齐由候选身份与版本派生的报告身份;写入侧强制校验。"""
from dataclasses import replace
from uuid import NAMESPACE_URL, uuid5
from muse.正式变更.接口 import 固定哈希
身份 = str(
uuid5(
NAMESPACE_URL,
"muse:B06:report:" + 固定哈希([报告.candidate_id, 报告.candidate_revision]),
)
)
return replace(报告, report_id=身份)
__all__ = [
"审校服务",
"审校报告",
"审校错误",
"基础检查报告",
"连续性报告",
"组装审校报告",
"补齐报告身份",
"运行基础检查",
"运行连续性审校",
]

View File

@ -0,0 +1,27 @@
# 连续性审阅(审校修订 · 语义阶段模板)
> 资源身份随发布包加载并记录哈希;本模板供后续语义审阅阶段使用,
> W15 的确定性连续性检查(出场核对与复读拦截)不调用本模板。
你是独立审校员,与写手互不相识。你收到的输入只有:
- 候选正文全文(已通过基础有效性检查);
- 冻结上下文快照中的历史正文投影、时点事实与目标章细纲投影;
- 各材料的身份与版本(用于回读证据)。
## 任务
逐段核对候选正文与上述证据,只报告以下四类问题:
1. **事实冲突**:候选断言与时点事实或历史正文矛盾(给出证据位置与原文摘句)。
2. **视角问题**:叙事人称或聚焦对象与既定视角不一致(注明依据段落)。
3. **因果断裂**:事件先后、动机与结果在候选内部或与历史衔接处不成立。
4. **引文证据**:候选中声称引用历史原文的语句必须可在历史投影逐字回读;
无法回读的引用标注为「待证」,不得自行认定为伪造或合法。
## 纪律
- 只输出问题与证据,不修改候选,不提出改写文本。
- 每条发现注明证据来源(材料身份与版本)与候选中的位置。
- 缺证据的判断一律标「待证」,不得升级为冲突。
- 未覆盖的段落明确列出;禁止以部分覆盖宣称整章通过。

View File

@ -0,0 +1,92 @@
"""审校修订稳定对象:问题、发现、报告与检查结论;只描述证据,不携带修改能力。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
from muse.共享.错误 import Muse错误
class 审校错误(Muse错误):
错误码 = "REVIEW_INVALID"
@dataclass(frozen=True, slots=True)
class 检查项:
"""一项机械可判定的检查结果;证据描述必须能独立复核。"""
名称: str
判定: Literal["passed", "failed"]
证据: str
def __post_init__(self) -> None:
if self.判定 not in {"passed", "failed"} or not self.名称 or not self.证据:
raise 审校错误("检查项需要名称、判定与证据")
@dataclass(frozen=True, slots=True)
class 基础检查报告:
"""空内容、截断、结构与输出有效性的确定性结论;不评文学好坏。"""
检查: tuple[检查项, ...]
判定: Literal["passed", "failed"]
def __post_init__(self) -> None:
实际 = {c.判定 for c in self.检查}
if 实际 - {"passed", "failed"}:
raise 审校错误("基础检查判定不合法")
if (self.判定 == "failed") != ("failed" in 实际):
raise 审校错误("基础检查总判定必须与分项一致")
@dataclass(frozen=True, slots=True)
class 连续性发现:
"""一条证据化的连续性问题;缺证据的判断标记为待证,不自动否决候选。"""
类别: Literal["引文", "视角", "因果"]
判定: Literal["conflict", "pending"]
说明: str
证据: dict[str, Any]
位置: str = ""
def __post_init__(self) -> None:
if self.类别 not in {"引文", "视角", "因果"} or self.判定 not in {"conflict", "pending"}:
raise 审校错误("连续性发现的类别或判定不合法")
if not self.说明 or not self.证据:
raise 审校错误("连续性发现必须携带说明与证据")
@dataclass(frozen=True, slots=True)
class 连续性报告:
findings: tuple[连续性发现, ...]
coverage: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not isinstance(self.coverage, dict) or "依据快照" not in self.coverage:
raise 审校错误("连续性报告必须记录依据快照与覆盖")
@dataclass(frozen=True, slots=True)
class 审校报告:
"""绑定候选版本的完整检查证据。
verdict 由基础必需检查与连续性冲突级发现共同决定:任一失败即 failed;
待证(pending)发现只呈现给作者,不阻断采纳。
"""
report_id: str
candidate_id: str
candidate_revision: int
snapshot_id: str
basic: 基础检查报告
continuity: 连续性报告
verdict: Literal["passed", "failed"]
def __post_init__(self) -> None:
if self.candidate_revision <= 0:
raise 审校错误("审校报告必须绑定具体候选版本")
有冲突 = any(f.判定 == "conflict" for f in self.continuity.findings)
应为失败 = self.basic.判定 == "failed" or 有冲突
if (self.verdict == "failed") != 应为失败:
raise 审校错误("总判定必须与基础检查及冲突级发现一致")

View File

@ -0,0 +1,115 @@
"""独立连续性检查:对候选正文与冻结细纲/装配做确定性核对,只输出问题与证据。
与写手完全独立的代码路径执行;不调模型、不修改候选、不自动确认新事实。
视角与因果的语义审读由后续审阅模板(提示词/连续性审阅.md)在语义阶段接入,
本文件不伪造语义判断。
"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from muse.审校修订.模型 import 连续性发现, 连续性报告
规则版本 = "w15-continuity-1"
# 与旧承接一致的复读拦截阈值:连续 4 个非空白字符即视为可识别措辞。
_最短复读长度 = 4
_断句标点 = ",。;、:!?;,:"
def _压缩(文本: str) -> str:
return "".join(ch for ch in 文本 if not ch.isspace())
def _断句(文本: str) -> list[str]:
片段: list[str] = []
缓冲: list[str] = []
for ch in 文本:
if ch in _断句标点:
if 缓冲:
片段.append("".join(缓冲))
缓冲 = []
elif not ch.isspace():
缓冲.append(ch)
if 缓冲:
片段.append("".join(缓冲))
return 片段
def _约束文本(细纲投影: Mapping[str, Any]) -> list[str]:
"""复读拦截的参照文本:硬约束与章末钩子是细纲措辞,不应原样进正文。"""
文本: list[str] = []
for 项 in 细纲投影.get("hardConstraints") or []:
if isinstance(项, str) and 项.strip():
文本.append(项)
钩子 = 细纲投影.get("chapterEndHook")
if isinstance(钩子, str) and 钩子.strip():
文本.append(钩子)
return 文本
def 运行连续性审校(
候选文本: str,
细纲投影: Mapping[str, Any],
出场名映射: Mapping[str, str],
*,
依据快照: str,
) -> 连续性报告:
"""对候选可见文本执行确定性连续性核对。
出场名映射为「实体身份 → 正文应出现的名称」;缺映射时按身份本身核对,
找不到即按待证处理(不能因解析缺失误杀候选)。
"""
发现: list[连续性发现] = []
正文压缩 = _压缩(候选文本)
for 身份, 名称 in 出场名映射.items():
显示 = 名称 or 身份
if 显示 not in 正文压缩:
发现.append(
连续性发现(
"引文",
"conflict",
f"细纲要求出场对象「{显示}」未出现在候选正文",
{"entity": 身份, "expected": 显示, "正文哈希": _正文摘要(候选文本)},
)
)
已见: set[str] = set()
for 参照 in _约束文本(细纲投影):
for 片 in _断句(参照):
if len(片) < _最短复读长度 or 片 in 已见:
continue
if 片 in 正文压缩:
已见.add(片)
发现.append(
连续性发现(
"引文",
"conflict",
"候选正文复读了细纲措辞;细纲只写事件与结果,正文须展开",
{"leaked_phrase": 片, "来源": "fine_outline"},
)
)
return 连续性报告(
tuple(发现),
{
"依据快照": 依据快照,
"规则版本": 规则版本,
"覆盖": "全文章节候选",
"细纲目标章": 细纲投影.get("targetChapter"),
},
)
def _正文摘要(文本: str) -> str:
import hashlib
return hashlib.sha256(文本.encode()).hexdigest()[:16]
__all__ = ["运行连续性审校", "规则版本"]

View File

@ -40,6 +40,11 @@ def _解析参数(argv: list[str] | None) -> argparse.Namespace:
任务解析.add_argument("动作", choices=["列表", "查看", "事件", "控制"])
任务解析.add_argument("目标", nargs="?", help="任务 ID;控制时为具名请求 JSON 文件")
任务解析.add_argument("--游标", type=int, default=0)
创作解析 = 子命令.add_parser("创作", help="发起通用生成并查看候选检查报告")
创作解析.add_argument("配置", help="作者配置 TOML 路径")
创作解析.add_argument("动作", choices=["发起", "报告"])
创作解析.add_argument("目标", nargs="?", help="发起时为请求 JSON 文件;报告时为候选 ID")
创作解析.add_argument("--版本", type=int, default=None, help="报告的目标候选版本")
结构解析 = 子命令.add_parser("结构", help="发布内置种子或查看已发布的固定结构")
结构解析.add_argument("配置", help="应用配置 TOML 路径")
结构解析.add_argument("动作", choices=["发布内置", "查看", "维护"])
@ -196,6 +201,12 @@ def main(argv: list[str] | None = None) -> int:
结果 = 运行任务命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 参数.游标)
print(json.dumps(结果, ensure_ascii=False, default=str))
return _退出码_成功
if 参数.命令 == "创作":
from muse.接入.cli.创作命令 import 运行创作命令
结果 = 运行创作命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 版本=参数.版本)
print(json.dumps(结果, ensure_ascii=False, default=str))
return _退出码_成功
if 参数.命令 == "自检":
装配 = 构建(读取配置(参数.配置))
for 键, 值 in 自检(装配).items():

View File

@ -0,0 +1,67 @@
"""本地作者发起通用生成并查看候选检查;输入文件只提供内容,身份来自受控配置。"""
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from muse.共享.错误 import 配置错误
from muse.启动 import 应用装配
from muse.接入.主会话 import 参数合同错误
from muse.接入.创作操作 import 创作身份
class 生成发起请求(BaseModel):
model_config = ConfigDict(extra="forbid")
command_id: str = Field(min_length=1)
work_id: str = Field(min_length=1)
chapter_id: str = Field(min_length=1)
granularity: str = Field(min_length=1)
branch_id: str = "main"
purpose: str = ""
config_id: str = Field(min_length=1)
def 运行创作命令(
装配: 应用装配, 动作: str, 目标: str | None, *, 版本: int | None = None
) -> dict | list:
if 装配.配置.HTTP is None:
raise 配置错误("创作命令需要本地作者身份配置")
作者 = 创作身份(装配.配置.HTTP.作者ID, 装配.配置)
if 动作 == "发起":
if not 目标:
raise 配置错误("发起命令需要请求 JSON 文件")
try:
请求 = 生成发起请求.model_validate_json(Path(目标).read_text(encoding="utf-8"))
except ValidationError as exc:
raise 参数合同错误(exc.errors(), 前缀=("body",)) from None
from muse.编排.生成正文 import 发起生成正文
return 发起生成正文(
装配,
作者,
请求.command_id,
请求.granularity,
work_id=请求.work_id,
chapter_id=请求.chapter_id,
分支=请求.branch_id,
用途说明=请求.purpose,
配置ID=请求.config_id,
)
if 动作 == "报告":
if not 目标:
raise 配置错误("报告命令需要候选身份")
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.审校修订.接口 import 审校服务
候选 = 装配.要求正文().读取候选(作者, 目标)
当前版本 = 版本 if 版本 is not None else int(候选["revision"])
服务 = 审校服务(数据库工厂(装配.配置.数据库, 装配.配置.运行用途))
报告 = 服务.读取报告(目标, 当前版本)
if 报告 is None:
raise 配置错误(f"候选 {目标} 版本 {当前版本} 没有审校报告")
报告["created_at"] = str(报告["created_at"])
return 报告
raise 配置错误(f"未知创作命令动作:{动作}")
__all__ = ["运行创作命令", "生成发起请求"]

View File

@ -20,6 +20,7 @@ from muse.接入.http.路由.任务运行 import 路由 as 任务路由
from muse.接入.http.路由.作品规划 import 路由 as 作品路由
from muse.接入.http.路由.作者审阅 import 路由 as 审阅路由
from muse.接入.http.路由.元数据 import 路由 as 元数据路由
from muse.接入.http.路由.创作生成 import 路由 as 生成路由
from muse.接入.http.路由.故事世界 import 路由 as 世界路由
from muse.接入.http.路由.正文写作 import 路由 as 正文路由
from muse.接入.http.错误响应 import 协议错误, 安装错误响应
@ -82,6 +83,7 @@ def 注册API(应用: FastAPI) -> None:
应用.include_router(元数据路由)
应用.include_router(作品路由)
应用.include_router(正文路由)
应用.include_router(生成路由)
应用.include_router(审阅路由)
应用.include_router(世界路由)
应用.include_router(事件路由)

View File

@ -0,0 +1,119 @@
"""通用生成的 HTTP 外壳:发起写作任务与读取候选审校报告。"""
from typing import Any, Literal
from fastapi import APIRouter, HTTPException, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from muse.接入.http.作者会话 import 作者依赖
from muse.接入.创作操作 import 创作身份
路由 = APIRouter(prefix="/api/v1", tags=["创作生成"])
class 发起生成请求(BaseModel):
model_config = ConfigDict(extra="forbid")
command_id: str = Field(min_length=1)
chapter_id: str = Field(min_length=1)
granularity: Literal["new_chapter", "expand_scene", "edit_selection"]
branch_id: str = "main"
purpose: str = ""
config_id: str = Field(min_length=1)
budget: dict[str, int] | None = None
class 发起生成回执(BaseModel):
task_id: str
writing_task_id: str
class 检查项呈现(BaseModel):
名称: str
判定: str
证据: str
class 基础呈现(BaseModel):
checks: list[检查项呈现]
verdict: str
class 发现呈现(BaseModel):
类别: str
判定: str
说明: str
证据: dict[str, Any]
位置: str = ""
class 连续性呈现(BaseModel):
findings: list[发现呈现]
coverage: dict[str, Any]
class 审校报告呈现(BaseModel):
report_id: str
candidate_id: str
candidate_revision: int
snapshot_id: str
basic: 基础呈现
continuity: 连续性呈现
verdict: str
@路由.post(
"/works/{work_id}/writing-tasks",
response_model=发起生成回执,
operation_id="start_writing_task",
status_code=201,
)
def 发起生成(work_id: str, 请求: 发起生成请求, request: Request, 作者: 作者依赖) -> 发起生成回执:
装配 = request.app.state.装配
from muse.编排.生成正文 import 发起生成正文
回执 = 发起生成正文(
装配,
创作身份(作者, 装配.配置),
请求.command_id,
请求.granularity,
work_id=work_id,
chapter_id=请求.chapter_id,
分支=请求.branch_id,
用途说明=请求.purpose,
预算=请求.budget,
配置ID=请求.config_id,
)
return 发起生成回执(**回执)
@路由.get(
"/candidates/{candidate_id}/review-report",
response_model=审校报告呈现,
operation_id="read_candidate_review_report",
)
def 读取审校报告(
candidate_id: str,
request: Request,
作者: 作者依赖,
revision: int | None = Query(default=None, ge=1),
) -> 审校报告呈现:
装配 = request.app.state.装配
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.审校修订.接口 import 审校服务
身份 = 创作身份(作者, 装配.配置)
正文 = 装配.要求正文()
当前行 = 正文.读取候选(身份, candidate_id)
版本 = revision if revision is not None else int(当前行["revision"])
报告 = 审校服务(数据库工厂(装配.配置.数据库, 装配.配置.运行用途)).读取报告(candidate_id, 版本)
if 报告 is None:
raise HTTPException(404, "该候选版本没有审校报告")
return 审校报告呈现(
report_id=str(报告["report_id"]),
candidate_id=str(报告["candidate_id"]),
candidate_revision=int(报告["candidate_revision"]),
snapshot_id=str(报告["snapshot_id"]),
basic=基础呈现.model_validate(报告["basic"]),
continuity=连续性呈现.model_validate(报告["continuity"]),
verdict=报告["verdict"],
)

View File

@ -11,6 +11,7 @@ from __future__ import annotations
from uuid import uuid4
from muse.上下文.接口 import 上下文服务, 事实工具, 历史正文工具, 细纲工具
from muse.任务运行.执行合同 import 模型协议错误
from muse.任务运行.接口 import 事件类型, 任务服务, 只读工具集, 工具定义, 工具范围, 领取凭证
@ -31,9 +32,16 @@ def 领取只读工具(上下文: 上下文服务, 运行: 任务服务, 领取:
def 记录(载荷: dict) -> None:
运行.追加事件(领取, 事件类型.工具读取, 载荷, 事件ID=str(uuid4()))
# 声明的工具与冻结范围实际安装的交集才可用:没有历史文稿时读取工具
# 合法缺席,不应让整个探索步骤失败;全部缺席仍视为流程配置错误。
已登记 = {定义.名称 for 定义 in 登记}
可用 = tuple(名 for 名 in 步骤.工具 if 名 in 已登记)
if 步骤.工具 and not 可用:
raise 模型协议错误("工具未登记、重复或所需工具加载失败")
return 只读工具集(
登记,
步骤.工具,
可用,
工具范围(
任务.任务ID,
冻结["work_id"],

View File

@ -0,0 +1,139 @@
"""多粒度写作任务合同:形成目标、范围、前置和预算;不调用模型、不采纳候选。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from uuid import NAMESPACE_URL, uuid5
from muse.作品规划.接口 import 读取章节归属
from muse.共享.错误 import Muse错误
from muse.正文写作.存储 import 正文存储
from muse.正文写作.模型 import 正文错误
from muse.正文写作.正文格式 import 可见文本, 文稿身份
class 写作任务错误(Muse错误):
错误码 = "WRITING_TASK_INVALID"
def 写作粒度集() -> tuple[str, str, str]:
"""多粒度写作的稳定取值:新章、扩写、选段修改。"""
return ("new_chapter", "expand_scene", "edit_selection")
@dataclass(frozen=True, slots=True)
class 字符选区:
"""候选可见文本上的字符区间;半开区间 [起点, 终点)。"""
起点: int
终点: int
def __post_init__(self) -> None:
if (
isinstance(self.起点, bool)
or isinstance(self.终点, bool)
or not (0 <= self.起点 < self.终点)
):
raise 写作任务错误("字符选区必须是 0 ≤ 起点 < 终点的整数区间")
@dataclass(frozen=True, slots=True)
class 写作任务合同:
"""一次多粒度写作的业务目标;身份由作者与命令确定性派生,重放同身份。"""
任务ID: str
命令ID: str
作者: str
粒度: str
用途说明: str
work_id: str
chapter_id: str
branch_id: str
基线版本: int
选区: tuple[字符选区, ...] = ()
保护区: tuple[字符选区, ...] = ()
预算: dict[str, Any] = field(default_factory=dict)
停止条件: tuple[str, ...] = ()
def __post_init__(self) -> None:
if self.粒度 not in 写作粒度集():
raise 写作任务错误(f"未知写作粒度:{self.粒度}")
if self.粒度 == "edit_selection" and not self.选区:
raise 写作任务错误("选段修改必须携带明确选区")
if self.基线版本 < 0 or not self.任务ID or not self.命令ID or not self.作者:
raise 写作任务错误("写作任务缺少身份或基线版本不合法")
for 甲 in self.选区:
for 乙 in self.保护区:
if 甲.起点 < 乙.终点 and 乙.起点 < 甲.终点:
raise 写作任务错误("选区与保护区不能重叠")
def 创建写作任务(
连,
身份,
命令ID: str,
粒度: str,
*,
work_id: str,
chapter_id: str,
分支: str = "main",
用途说明: str = "",
选区: tuple[字符选区, ...] = (),
保护区: tuple[字符选区, ...] = (),
预算: dict[str, Any] | None = None,
停止条件: tuple[str, ...] = (),
) -> 写作任务合同:
"""按当前章位冻结写作任务;章位归属与选区边界在创建时机械校验。"""
if not 命令ID:
raise 写作任务错误("写作任务需要明确命令身份")
章节 = 读取章节归属(连, 身份.作者, chapter_id)
if 章节["work_id"] != work_id:
raise 写作任务错误("章节不属于目标作品")
存储 = 正文存储(连)
文档ID = 文稿身份(chapter_id, 分支)
正文 = 存储.读取(文档ID)
基线版本 = 正文["revision"] if 正文 else 0
任务ID = str(uuid5(NAMESPACE_URL, "muse:B05:writing_task:" + _身份哈希(身份.作者, 命令ID)))
任务 = 写作任务合同(
任务ID,
命令ID,
身份.作者,
粒度,
用途说明,
work_id,
chapter_id,
分支,
基线版本,
选区,
保护区,
预算 or {},
停止条件,
)
if 基线版本 == 0:
if 任务.粒度 != "new_chapter":
raise 写作任务错误("没有既有正文,只允许新章粒度")
return 任务
可见长度 = len(可见文本(存储.读取草稿(文档ID, 基线版本)))
for 区 in 任务.选区 + 任务.保护区:
if 区.终点 > 可见长度:
raise 写作任务错误(f"选区越界:终点 {区.终点} 超出当前正文长度 {可见长度}")
return 任务
def _身份哈希(作者: str, 命令ID: str) -> str:
from muse.正式变更.接口 import 固定哈希
return 固定哈希([作者, 命令ID])
__all__ = [
"写作任务合同",
"写作任务错误",
"写作粒度集",
"字符选区",
"创建写作任务",
"正文错误",
]

View File

@ -0,0 +1,24 @@
# 生成正文(正文写作 · 无工具写作模板)
> 资源身份随发布包加载并记录哈希;角色身份、字段结构、权限和预算从各自权威装配,
> 本模板不含固定作品内容或测试答案。
你是本作品的写手。你收到的是一次冻结装配的写作材料:
- 目标章细纲投影(目标、硬约束、关键事件、必出场对象、章末钩子);
- 截止时点的历史正文投影与事实投影(如装配入选);
- 任务粒度与用途说明(新章 / 扩写 / 选段修改)。
## 写作要求
1. 只使用装配材料中的设定与事实;不得虚构与材料冲突的事实。
2. 严格覆盖细纲关键事件的顺序与结果方向;必出场对象必须实际出场。
3. 用戏剧化叙述展开,禁止把细纲措辞原样搬进正文。
4. 输出只包含正文段落,不含大纲、说明、注释或元信息。
## 输出
输出一个 JSON 对象:`{"paragraphs": [{"text": "段落文本"}, ...]}`。
- 每段是完整段落文本,不含换行符;段内换行由系统处理。
- 段落顺序即正文顺序;不输出段落标题。

View File

@ -62,6 +62,7 @@ class 正文候选决定入口:
if 命令.动作 is not 作者动作.采纳:
raise 变更错误("INVALID_REQUEST", "候选入口只接受明确作者决定")
行 = 核对候选归属(连, 身份, 命令.业务请求.candidate_id)
_核对必需检查(连, 行)
基线 = 正文存储(连).读取草稿(当前.目标, 当前.数据版本)
if 行["base_revision"] != 当前.数据版本 or 行["base_hash"] != 正文结构哈希(基线):
raise 变更错误("SOURCE_STALE", "候选依据的正文已改变,不能直接覆盖作者新稿")
@ -71,3 +72,22 @@ class 正文候选决定入口:
(参与操作("B05.write_document", 正文写入(身份.作者, 请求, 草稿)),),
(修改影响(当前.目标, "document_changed", str(当前.数据版本), str(当前.数据版本 + 1)),),
)
def _核对必需检查(连, 行) -> None:
"""模型候选必需检查未通过或缺证据时阻止采纳;人工改稿不借用模型检查门。"""
来源 = 连.execute(
"SELECT origin FROM muse_writing_candidate WHERE candidate_id=%s",
(行["candidate_id"],),
).fetchone()
if 来源 is None or 来源[0] != "model":
return
报告 = 连.execute(
"SELECT verdict FROM muse_review_report WHERE candidate_id=%s AND candidate_revision=%s",
(行["candidate_id"], 行["revision"]),
).fetchone()
if 报告 is None:
raise 变更错误("CHECK_MISSING", "模型候选缺少必需检查报告,不能采纳")
if 报告[0] != "passed":
raise 变更错误("CHECK_FAILED", "必需检查未通过,不能采纳该候选")

View File

@ -0,0 +1,663 @@
"""生成正文旅程:预组装、受限探索、冻结回放、无工具写作与检查入候选。
只调用各模块公开用例并按授权安排步骤;处理器在此登记为实际 Python 实现,
配置(配置/流程模板/生成正文.yaml)只选择稳定身份与版本。
"""
from __future__ import annotations
import asyncio
import hashlib
import json
from dataclasses import asdict
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
from uuid import NAMESPACE_URL, uuid4, uuid5
from psycopg.types.json import Jsonb
from muse.上下文.接口 import (
依赖重验检查,
回放结果,
探索条目,
来源绑定,
装配材料,
记录探索,
)
from muse.任务运行.接口 import (
事件类型,
任务请求,
回合记录,
工具回传,
执行上下文,
模型请求,
步骤处理器,
步骤结果,
步骤计划,
)
from muse.任务运行.模型调用 import 请求字节
from muse.任务运行.角色会话 import 组装角色请求
from muse.任务运行.角色策略 import 角色策略目录
from muse.任务运行.配置版本 import 配置版本管理
from muse.共享.错误 import Muse错误
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.审校修订.存储 import 审校存储
from muse.审校修订.接口 import 组装审校报告, 补齐报告身份
from muse.正文写作.写作任务 import 创建写作任务
from muse.正文写作.存储 import 正文存储
from muse.正文写作.模型 import 文本节点, 正文草稿, 段落
from muse.正文写作.正文格式 import 可见文本哈希, 文稿身份, 正文结构哈希
from muse.编排.接口 import 流程登记
from muse.编排.流程版本 import 流程定义
流程身份 = "生成正文"
流程版本 = "1.0.0"
# 探索阶段允许的具名只读工具;与冻结范围安装的工具一致,写作阶段为空(禁工具)。
探索工具集 = ("query_work_plan", "query_facts_as_of", "read_history_document")
处理器版本集 = {
"ctx.prep": "1",
"ctx.explore": "1",
"ctx.freeze": "1",
"ctx.write": "1",
"ctx.check": "1",
}
_写作输出合同 = {
"type": "object",
"properties": {
"paragraphs": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {"text": {"type": "string", "minLength": 1}},
"required": ["text"],
"additionalProperties": False,
},
}
},
"required": ["paragraphs"],
"additionalProperties": False,
}
_探索输出合同 = {"type": "object"}
class 生成正文错误(Muse错误):
"""旅程编排内的明确失败;错误说明面向作者呈现。"""
错误码 = "WRITING_FLOW_INVALID"
def 生成正文模板() -> tuple[str, str]:
"""加载发布包内的无工具写作模板;返回(模板文本, 模板哈希)。"""
路径 = Path(__file__).resolve().parents[1] / "正文写作" / "提示词" / "生成正文.md"
文本 = 路径.read_text(encoding="utf-8")
return 文本, hashlib.sha256(文本.encode()).hexdigest()
def _工厂(装配) -> 数据库工厂:
return 数据库工厂(装配.配置.数据库, 装配.配置.运行用途)
def 前步检查点(上下文: 执行上下文, 步骤ID: str) -> dict[str, Any]:
"""读取已提交前置步骤的检查点;缺失即旅程不完整,明确失败。"""
for 项 in 上下文.任务.步骤:
if 项.get("step_id") == 步骤ID:
检查点 = 项.get("checkpoint") or {}
if not 检查点:
raise 生成正文错误(f"前置步骤 {步骤ID} 没有可用检查点")
return 检查点
raise 生成正文错误(f"前置步骤 {步骤ID} 尚未执行")
def _探索清单从检查点(数据: list[dict[str, Any]]) -> tuple[探索条目, ...]:
return tuple(
探索条目(
调用ID=项["调用ID"],
工具名=项["工具名"],
顺序=项["顺序"],
参数=项["参数"],
来源引用=tuple(来源绑定(**绑) for 绑 in 项["来源引用"]),
消费哈希=项["消费哈希"],
)
for 项 in 数据
)
def _原文授权(装配, 任务, 请求: 模型请求, *, 方式: str) -> str:
"""为一次模型调用取得作者批准的原文授权。
探索回合走持久证据(无租约);生成回合走临时租约,字节只进受控暂存。
"""
原文 = 装配.要求原文()
哈希 = hashlib.sha256(请求字节(请求)).hexdigest()
return 原文.批准保留(
任务.任务ID,
任务.作者,
"raw-" + 请求.调用ID,
来源版本="生成正文任务",
哈希=(哈希,),
内容用途="generation",
方式=方式,
有效期=datetime.now(UTC) + timedelta(minutes=20),
调用ID=请求.调用ID,
调用请求哈希=哈希,
)
def _写手请求(
装配,
上下文: 执行上下文,
*,
阶段: str,
系统提示: str,
用户输入: str,
工具: tuple[dict[str, Any], ...] = (),
历史: tuple[回合记录, ...] = (),
) -> 模型请求:
任务 = 上下文.任务
版本 = 配置版本管理(_工厂(装配)).读取任务绑定(任务.任务ID)
声明 = 版本.内容.角色配置["writer"]
请求 = 模型请求(
调用ID=str(uuid4()),
provider=声明["provider"],
model=声明["model"],
thinking=声明.get("thinking"),
系统提示=系统提示,
用户输入=用户输入,
输出合同=_写作输出合同 if 阶段 == "生成" else _探索输出合同,
最大输出token=int(任务.冻结输入["冻结上下文"]["budget"].get("最大输出token", 8192)),
总期限秒=600.0,
工具=工具,
历史=历史,
)
return 组装角色请求(角色策略目录.从发布包(), "writer", 请求)
def 登记生成正文(登记: 流程登记, 装配, 计价) -> None:
"""登记生成正文流程的处理器、类型与恢复检查;装配提供全部实现。"""
服务 = 装配.要求上下文()
登记.登记类型(流程身份, 必需保护=("冻结上下文",))
登记.登记恢复检查(流程身份, 流程版本, 依赖重验检查(服务))
def 预组装(上下文: 执行上下文) -> 步骤结果:
范围 = 服务.恢复冻结范围(上下文.任务)
写作 = 上下文.任务.冻结输入["输入"]["写作任务"]
if 写作.get("chapter_id") != (范围.get("target_plan") or {}).get("chapter_id"):
raise 生成正文错误(f"写作任务章位 {写作.get('chapter_id')} 与冻结细纲章位不一致")
if "budget" not in 上下文.任务.冻结输入["冻结上下文"]:
raise 生成正文错误("任务缺少预算冻结输入")
return 步骤结果(
输出={"work_id": 范围["work_id"], "chapter_id": 写作["chapter_id"]},
检查点={"预组装": {"as_of": 范围["as_of"], "来源数": len(范围["source_ids"])}},
)
def 受限探索(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
工具 = 装配.要求只读工具(上下文.领取)
清单: tuple[探索条目, ...] = ()
绑定集: dict[str, 来源绑定] = {}
历史: list[回合记录] = []
轮次上限 = int(
任务.冻结输入["冻结上下文"].get("stop_conditions", {}).get("探索轮数上限", 6)
)
工具描述 = tuple(
{"name": d.名称, "description": d.说明, "parameters": d.参数合同}
for d in 工具.工具.values()
)
探索提示 = (
"你是本作品的写手,当前处于受限探索阶段。"
"使用提供的只读工具了解目标章细纲、截止时点事实与历史正文,"
"为写作收集必要材料;不要输出正文,探索完成后输出一个空 JSON 对象。"
)
for _ in range(轮次上限):
请求 = _写手请求(
装配,
上下文,
阶段="探索",
系统提示=探索提示,
用户输入=json.dumps(
{
"粒度": 任务.冻结输入["输入"]["写作任务"]["粒度"],
"用途说明": 任务.冻结输入["输入"]["写作任务"].get("用途说明", ""),
"材料需求": ["目标章细纲", "时点事实", "历史正文"],
},
ensure_ascii=False,
),
工具=工具描述,
历史=tuple(历史),
)
执行器 = 装配.要求模型执行器(上下文, 计价)
授权 = _原文授权(装配, 任务, 请求, 方式="persistent")
回合 = asyncio.run(执行器.执行回合(上下文, 请求, 阶段="探索", 原文授权ID=授权))
if 回合.结果.状态 == "tool_calls":
回传: list[工具回传] = []
for 调用 in 回合.结果.工具调用:
结果 = 工具.调用(调用)
清单 = 记录探索(清单, 调用.调用ID, 调用.名称, 调用.参数, 结果)
for 源 in 结果.来源:
绑定集.setdefault(
源.来源ID,
来源绑定(源.来源ID, 源.数据版本, 源.结构哈希, 源.投影版本),
)
回传.append(工具回传(调用, json.dumps(结果.内容, ensure_ascii=False)))
历史.append(回合记录(回合.结果, tuple(回传)))
continue
if 回合.结果.状态 != "completed":
raise 生成正文错误(f"探索回合未完成:{回合.结果.状态}")
break
else:
raise 生成正文错误("探索轮数超出停止条件上限")
if not 清单:
raise 生成正文错误("写手没有进行任何受限探索读取")
return 步骤结果(
输出={"清单长度": len(清单)},
检查点={
"清单": [asdict(项) for 项 in 清单],
"绑定": [asdict(绑) for 绑 in 绑定集.values()],
},
)
def 冻结回放(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
前置 = 前步检查点(上下文, "受限探索")
清单 = _探索清单从检查点(前置["清单"])
绑定集 = {绑["来源ID"]: 来源绑定(**绑) for 绑 in 前置["绑定"]}
回放: 回放结果 = 服务.回放探索(任务.任务ID, 清单)
材料: dict[str, str] = {}
细纲投影: dict[str, Any] | None = None
事实: list[dict[str, Any]] = []
for 项 in 回放.逐项:
if 项.工具名 == "query_work_plan":
细纲投影 = 项.内容["content"]
材料["目标章细纲"] = json.dumps(项.内容["content"], ensure_ascii=False)
elif 项.工具名 == "query_facts_as_of":
事实 = list(项.内容.get("facts", []))
for 实体 in 事实:
名称 = (实体.get("content") or {}).get("名称")
if 名称:
材料["事实:" + 实体["object_id"]] = str(名称)
elif 项.工具名 == "read_history_document":
材料["历史:" + 项.调用ID] = 项.内容.get("text", "")
if 细纲投影 is None:
raise 生成正文错误("回放清单中没有目标章细纲,不能进入写作")
装配结果 = 装配材料(
材料,
("目标章细纲",),
字节上限=int(任务.冻结输入["冻结上下文"]["budget"]["context_bytes"]),
)
回执 = 服务.冻结上下文(任务.任务ID, 装配结果, tuple(绑定集.values()))
return 步骤结果(
输出={"snapshot_id": 回执.快照ID, "字节数": 装配结果.字节数},
检查点={
"快照ID": 回执.快照ID,
"细纲投影": 细纲投影,
"事实": 事实,
"入选": list(装配结果.入选来源),
},
)
def 无工具写作(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
前置 = 前步检查点(上下文, "冻结回放")
快照 = 服务.读取快照(前置["快照ID"])
if 快照.失效说明:
raise 生成正文错误(f"上下文快照已失效:{快照.失效说明}")
模板, 模板哈希 = 生成正文模板()
写作 = 任务.冻结输入["输入"]["写作任务"]
请求 = _写手请求(
装配,
上下文,
阶段="生成",
系统提示=模板,
用户输入=json.dumps(
{
"粒度": 写作["粒度"],
"用途说明": 写作.get("用途说明", ""),
"装配材料": 快照.装配["文本"],
"选区": 写作.get("选区", []),
"保护区": 写作.get("保护区", []),
},
ensure_ascii=False,
),
)
执行器 = 装配.要求模型执行器(上下文, 计价)
授权 = _原文授权(装配, 任务, 请求, 方式="temporary")
交付 = asyncio.run(
执行器.执行(
上下文,
请求,
阶段="生成",
原文授权ID=授权,
临时租约ID=_租约(装配, 任务, 授权, 请求.调用ID),
)
)
段落集 = 交付.内容["paragraphs"]
for 段 in 段落集:
装配.任务运行.追加事件(
上下文.领取,
事件类型.文本片段,
{"文本": 段["text"], "模板哈希": 模板哈希},
事件ID=str(uuid4()),
)
return 步骤结果(
输出={"段落数": len(段落集)},
检查点={
"段落": [段["text"] for 段 in 段落集],
"调用ID": 交付.调用ID,
"证据回执": 交付.证据回执,
"模板哈希": 模板哈希,
},
)
def 检查与候选(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
冻结点 = 前步检查点(上下文, "冻结回放")
写作点 = 前步检查点(上下文, "无工具写作")
写作 = 任务.冻结输入["输入"]["写作任务"]
草稿 = 正文草稿(
tuple(
段落(f"gen-{序}", (文本节点(文本),))
for 序, 文本 in enumerate(写作点["段落"], start=1)
)
)
出场名映射: dict[str, str] = {}
for 实体 in 冻结点.get("事实", []):
名称 = (实体.get("content") or {}).get("名称")
if 名称:
出场名映射.setdefault(实体["object_id"], str(名称))
for 身份 in 冻结点["细纲投影"].get("mustAppearEntities") or []:
出场名映射.setdefault(str(身份), str(身份))
with _工厂(装配).连接() as 连, 连.transaction():
候选 = 写入模型候选(
连,
任务.作者,
任务.任务ID,
冻结点["快照ID"],
写作["chapter_id"],
写作.get("branch_id", "main"),
int(写作["基线版本"]),
草稿,
流程版本=流程版本,
模型证据=[写作点["调用ID"], 写作点["证据回执"]],
提示词哈希={"生成正文": 写作点["模板哈希"]},
)
报告 = 组装审校报告(
草稿,
冻结点["细纲投影"],
出场名映射,
candidate_id=候选["candidate_id"],
candidate_revision=int(候选["revision"]),
snapshot_id=冻结点["快照ID"],
原始交付=json.dumps(
{"paragraphs": [{"text": t} for t in 写作点["段落"]]},
ensure_ascii=False,
),
)
审校存储(连).写入报告(补齐报告身份(报告))
for 类型, 载荷 in (
(
事件类型.候选保存,
{"candidate_id": 候选["candidate_id"], "revision": 候选["revision"]},
),
(事件类型.检查完成, {"verdict": 报告.verdict, "report_id": 报告.report_id}),
):
装配.任务运行.追加事件(上下文.领取, 类型, 载荷, 事件ID=str(uuid4()))
return 步骤结果(
输出={
"candidate_id": 候选["candidate_id"],
"revision": 候选["revision"],
"candidate_hash": 候选["candidate_hash"],
"verdict": 报告.verdict,
},
检查点={"候选": 候选, "报告": 报告.report_id, "verdict": 报告.verdict},
)
登记.登记处理器(步骤处理器("ctx.prep", "1", 预组装, "writing-task", "frozen-scope"))
登记.登记处理器(
步骤处理器(
"ctx.explore",
"1",
受限探索,
"frozen-scope",
"exploration-manifest",
角色="writer",
允许工具=探索工具集,
)
)
登记.登记处理器(
步骤处理器(
"ctx.freeze",
"1",
冻结回放,
"exploration-manifest",
"context-snapshot",
保护职责="冻结上下文",
)
)
登记.登记处理器(
步骤处理器(
"ctx.write",
"1",
无工具写作,
"context-snapshot",
"generated-draft",
角色="writer",
)
)
登记.登记处理器(步骤处理器("ctx.check", "1", 检查与候选, "generated-draft", "model-candidate"))
def _租约(装配, 任务, 授权ID: str, 调用ID: str) -> str:
原文 = 装配.要求原文()
return 原文.创建租约(
任务.任务ID,
授权ID,
"lease-" + 调用ID,
datetime.now(UTC) + timedelta(minutes=19),
最少剩余秒=0,
)
def 生成正文步骤() -> tuple[步骤计划, ...]:
return (
步骤计划("预组装", "ctx.prep", "1"),
步骤计划("受限探索", "ctx.explore", "1", 依赖=("预组装",), 角色="writer", 工具=探索工具集),
步骤计划("冻结回放", "ctx.freeze", "1", 依赖=("受限探索",)),
步骤计划("无工具写作", "ctx.write", "1", 依赖=("冻结回放",), 角色="writer"),
步骤计划("检查与候选", "ctx.check", "1", 依赖=("无工具写作",)),
)
def 流程定义_生成正文() -> 流程定义:
return 流程定义(流程身份, 流程版本, 生成正文步骤())
def 发起生成正文(
装配,
身份,
命令ID: str,
粒度: str,
*,
work_id: str,
chapter_id: str,
分支: str = "main",
用途说明: str = "",
预算: dict[str, Any] | None = None,
配置ID: str,
) -> dict[str, str]:
"""冻结写作任务与来源范围后创建生成正文任务;运行配置由作者明确选择。"""
from muse.作品规划.接口 import 读取作品范围, 读取章节归属
with _工厂(装配).连接(只读=True) as 连:
章节 = 读取章节归属(连, 身份.作者, chapter_id)
目录 = 读取作品范围(连, 身份.作者, work_id)
前文 = []
for 行 in 目录["chapters"]:
if 行["position"] >= 章节["position"]:
continue
文稿 = 正文存储(连).读取(文稿身份(行["chapter_id"], 分支))
if 文稿:
前文.append(
{
"chapter_id": 行["chapter_id"],
"revision": 文稿["revision"],
"branch_id": 分支,
}
)
任务合同 = 创建写作任务(
连,
身份,
命令ID,
粒度,
work_id=work_id,
chapter_id=chapter_id,
分支=分支,
用途说明=用途说明,
预算=预算,
)
范围 = 装配.要求上下文().授权任务范围(
身份,
work_id,
int(章节["position"]) - 1,
tuple(前文),
目标章ID=chapter_id,
含事实=True,
)
冻结上下文 = {
"source_scope": 范围,
"schema_versions": {"context": "w14", "review": "w15"},
"authorization": {
"authorized_by": 范围["authorized_by"],
"run_purpose": 范围["run_purpose"],
"content_purpose": 范围["content_purpose"],
},
"budget": 预算 or {"context_bytes": 65536, "最大输出token": 8192},
"stop_conditions": {"探索轮数上限": 6},
}
策略 = 角色策略目录.从发布包()
请求 = 任务请求(
任务类型=流程身份,
命令ID=命令ID,
作者=身份.作者,
执行用途=装配.配置.运行用途,
内容用途=身份.内容用途,
输入={"写作任务": asdict(任务合同)},
角色策略版本=策略.定义["version"],
资源发布身份=装配.配置.资源发布身份,
冻结上下文=冻结上下文,
作品ID=work_id,
)
任务ID = 装配.任务运行.创建任务(请求, 流程身份, 流程版本)
配置版本管理(_工厂(装配)).冻结到任务(任务ID, 配置ID)
return {"task_id": 任务ID, "writing_task_id": 任务合同.任务ID}
def 写入模型候选(
连,
作者: str,
任务ID: str,
快照ID: str,
chapter_id: str,
branch_id: str,
基线版本: int,
草稿: 正文草稿,
*,
流程版本: str,
模型证据: list[Any],
提示词哈希: dict[str, str],
) -> dict[str, str]:
"""在任务事务内写入模型候选及来源证据;哈希与版本由服务端计算。"""
from muse.正式变更.接口 import 固定哈希
基线哈希 = (
正文结构哈希(正文存储(连).读取草稿(文稿身份(chapter_id, branch_id), 基线版本))
if 基线版本 > 0
else "empty"
)
candidate_id = str(uuid5(NAMESPACE_URL, "muse:B05:model_candidate:" + 固定哈希([任务ID])))
版 = 1
行 = 连.execute(
"SELECT current_revision FROM muse_writing_candidate WHERE candidate_id=%s",
(candidate_id,),
).fetchone()
if 行 is not None:
版 = int(行[0]) + 1
更新 = 连.execute(
"UPDATE muse_writing_candidate SET current_revision=%s "
"WHERE candidate_id=%s AND current_revision=%s RETURNING candidate_id",
(版, candidate_id, 版 - 1),
).fetchone()
if 更新 is None:
raise 生成正文错误("模型候选已被其他尝试修改")
else:
连.execute(
"INSERT INTO muse_writing_candidate"
"(candidate_id,chapter_id,branch_id,origin,current_revision) "
"VALUES (%s,%s,%s,'model',%s)",
(candidate_id, chapter_id, branch_id, 版),
)
文稿哈希 = 正文结构哈希(草稿)
哈希 = 固定哈希(
[candidate_id, 版, chapter_id, branch_id, 基线版本, 基线哈希, 文稿哈希, "model"]
)
连.execute(
"INSERT INTO muse_writing_candidate_version"
"(candidate_id,revision,base_revision,base_hash,document,document_hash,"
"visible_text_hash,candidate_hash,checks,author_id) VALUES "
"(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
(
candidate_id,
版,
基线版本,
基线哈希,
Jsonb(asdict(草稿)),
文稿哈希,
可见文本哈希(草稿),
哈希,
Jsonb({"format": "passed", "format_version": 1, "quality": "model_pending"}),
作者,
),
)
连.execute(
"INSERT INTO muse_model_candidate"
"(candidate_id,task_id,snapshot_id,flow_version,processor_versions,"
"model_evidence,prompt_hashes) VALUES (%s,%s,%s,%s,%s,%s,%s) "
"ON CONFLICT (candidate_id) DO NOTHING",
(
candidate_id,
任务ID,
快照ID,
流程版本,
Jsonb(处理器版本集),
Jsonb(模型证据),
Jsonb(提示词哈希),
),
)
return {"candidate_id": candidate_id, "revision": str(版), "candidate_hash": 哈希}
__all__ = [
"登记生成正文",
"发起生成正文",
"流程定义_生成正文",
"生成正文步骤",
"生成正文模板",
"写入模型候选",
"流程身份",
"流程版本",
"探索工具集",
"生成正文错误",
"处理器版本集",
]

View File

@ -0,0 +1,62 @@
"""从可审 YAML 配置载入流程定义;只允许引用已登记处理器与固定版本。
配置不能删除保护节点或运行任意脚本:载入产物交由 流程登记.冻结 校验
处理器存在性、槽位角色与工具许可、依赖闭包与必需保护。
"""
from __future__ import annotations
from pathlib import Path
import yaml
from muse.任务运行.模型 import 步骤计划
from muse.编排.槽位约束 import 流程校验错误
from muse.编排.流程版本 import 流程定义
def 载入流程定义(路径: Path) -> 流程定义:
"""读取流程模板并映射为流程定义;结构不完整或含未登记字段即拒绝。"""
if not 路径.is_file():
raise 流程校验错误(f"流程模板不存在:{路径}")
try:
数据 = yaml.safe_load(路径.read_text(encoding="utf-8"))
except yaml.YAMLError as 错误:
raise 流程校验错误(f"流程模板不是合法 YAML:{错误}") from None
if not isinstance(数据, dict):
raise 流程校验错误("流程模板必须是映射结构")
流程ID = 数据.get("flow_id")
版本 = 数据.get("version")
步骤集 = 数据.get("steps")
if not 流程ID or not 版本 or not isinstance(步骤集, list) or not 步骤集:
raise 流程校验错误("流程模板缺少 flow_id、version 或 steps")
计划: list[步骤计划] = []
for 项 in 步骤集:
if not isinstance(项, dict):
raise 流程校验错误("流程步骤必须是映射结构")
步骤ID = 项.get("step_id")
处理器ID = 项.get("processor")
处理器版本 = str(项.get("processor_version", ""))
if not 步骤ID or not 处理器ID or not 处理器版本:
raise 流程校验错误("流程步骤缺少 step_id、processor 或 processor_version")
依赖 = 项.get("depends_on") or []
工具 = 项.get("tools") or []
if not isinstance(依赖, list) or not all(isinstance(d, str) for d in 依赖):
raise 流程校验错误(f"步骤 {步骤ID} 的 depends_on 必须是字符串列表")
if not isinstance(工具, list) or not all(isinstance(t, str) for t in 工具):
raise 流程校验错误(f"步骤 {步骤ID} 的 tools 必须是字符串列表")
计划.append(
步骤计划(
步骤ID,
处理器ID,
处理器版本,
依赖=tuple(依赖),
角色=项.get("role"),
工具=tuple(工具),
)
)
return 流程定义(流程ID, str(版本), tuple(计划))
__all__ = ["载入流程定义"]

View File

@ -0,0 +1,105 @@
"""审校修订公开合同:基础检查、连续性发现与报告判定的确定性口径;不连数据库。"""
import pytest
from muse.审校修订.基础检查 import 判定必需通过, 运行基础检查
from muse.审校修订.接口 import 组装审校报告, 补齐报告身份
from muse.审校修订.模型 import 审校报告, 审校错误
from muse.审校修订.连续性审校 import 运行连续性审校
from muse.正文写作.模型 import 文本节点, 正文草稿, 段落
def 草稿(*文本: str) -> 正文草稿:
return 正文草稿(tuple(段落(f"p-{序}", (文本节点(段),)) for 序, 段 in enumerate(文本, start=1)))
细纲 = {
"targetChapter": 3,
"chapterGoal": "邮差在旧码头确认收信人。",
"hardConstraints": ["上一章结尾雨还没停。"],
"mustAppearEntities": ["lin-shen"],
"chapterEndHook": "仓库的门后有人应声。",
}
def test_空内容与空段结构失败且证据明确__a19b42():
报告 = 运行基础检查(草稿(" "))
项 = {c.名称: c for c in 报告.检查}
assert 报告.判定 == "failed"
assert 项["空内容"].判定 == "failed"
assert 项["结构"].判定 == "failed"
def test_末段无收束判截断合法收束不误报__b28c53():
截断 = 运行基础检查(草稿("林深走到跳板尽头,忽然停住"))
assert {c.名称: c for c in 截断.检查}["截断"].判定 == "failed"
收束 = 运行基础检查(草稿("林深把信递了过去。"))
assert {c.名称: c for c in 收束.检查}["截断"].判定 == "passed"
def test_原始交付损坏判输出有效性失败__c39d64():
好 = 运行基础检查(草稿("他收好信。"), 原始交付='{"paragraphs":[]}')
坏 = 运行基础检查(草稿("他收好信。"), 原始交付="{not-json")
assert {c.名称: c for c in 好.检查}["输出有效性"].判定 == "passed"
assert {c.名称: c for c in 坏.检查}["输出有效性"].判定 == "failed"
assert not 判定必需通过(坏)
def test_必出场缺失与复读细纲措辞均为冲突级发现__d4ae75():
发现 = 运行连续性审校(
"林深把信压在掌心。上一章结尾雨还没停,他没有回头。",
细纲,
{"lin-shen": "林深"},
依据快照="snap-1",
).findings
类别 = [(f.类别, f.判定) for f in 发现]
assert ("引文", "conflict") in 类别
泄漏 = [f for f in 发现 if "复读" in f.说明]
assert 泄漏 and 泄漏[0].证据["leaked_phrase"]
def test_出场齐全且不复读时无发现并记录覆盖__e5bf86():
报告 = 运行连续性审校(
"雨点敲着铁皮屋顶,林深把退回的信压在掌心,沿跳板走向仓库。",
细纲,
{"lin-shen": "林深"},
依据快照="snap-1",
)
assert 报告.findings == ()
assert 报告.coverage["依据快照"] == "snap-1"
assert 报告.coverage["规则版本"].startswith("w15")
def test_报告总判定与基础及冲突发现一致且绑定版本__f6c097():
好报告 = 组装审校报告(
草稿("林深沿跳板走向仓库,把信递了过去。"),
细纲,
{"lin-shen": "林深"},
candidate_id="c-1",
candidate_revision=1,
snapshot_id="snap-1",
)
assert 好报告.verdict == "passed"
带身份 = 补齐报告身份(好报告)
assert 带身份.report_id
坏报告 = 组装审校报告(
草稿("上一章结尾雨还没停,他停住"),
细纲,
{"lin-shen": "林深"},
candidate_id="c-2",
candidate_revision=1,
snapshot_id="snap-1",
)
assert 坏报告.verdict == "failed"
with pytest.raises(审校错误):
审校报告(
"r-x",
"c-3",
1,
"snap-1",
好报告.basic,
坏报告.continuity,
"passed",
)

View File

@ -0,0 +1,642 @@
"""两阶段写手全链路:真实任务、PG 账本与合成 HTTP 流贯通;不据此宣称真实模型或文学效果。"""
import json
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from uuid import uuid4
import pytest
from pydantic import TypeAdapter
from muse.上下文.接口 import 上下文错误
from muse.任务运行.接口 import 任务状态
from muse.任务运行.模型 import 内容哈希
from muse.任务运行.配置版本 import (
凭据引用,
提供方配置,
运行配置内容,
配置版本管理,
配置验证证据,
)
from muse.任务运行.预算管理 import (
任务预算计划,
角色预算,
预算管理,
额度策略,
)
from muse.作品规划.接口 import 档案保存, 规划保存, 规划定位
from muse.元数据.接口 import 元数据服务, 导入内置结构
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
from muse.共享.错误 import Muse错误
from muse.启动 import 构建, 装配生成正文
from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据
from muse.正式变更.接口 import 作者动作, 变更错误
from muse.正文写作.写作任务 import 写作任务错误
from muse.正文写作.接口 import 候选定位, 文本节点, 正文草稿, 段落
from muse.正文写作.正文格式 import 可见文本
from muse.编排.生成正文 import 发起生成正文, 处理器版本集, 探索工具集, 流程版本
from muse.配置 import 应用配置
处理器能力 = list(处理器版本集)
pytestmark = pytest.mark.数据库
class 合成计价:
版本 = "synthetic-price-1"
def 金额(self, 结果):
return Decimal("0.125") if 结果.用量 else None
def 细纲内容(parent_id: str, 目标章: int = 3, 钩子: str = "仓库的门后有人应声。") -> dict:
return {
"targetChapter": 目标章,
"sourceRef": {"sourceId": parent_id, "sourceVersion": 1},
"chapterGoal": "邮差在旧码头确认收信人。",
"hardConstraints": ["上一章结尾雨还没停。"],
"keyEvents": [
{
"id": "arrival",
"order": 1,
"event": "邮差到达旧码头",
"participants": ["lin-shen"],
"trigger": "收到退回的信",
"resultDirection": "确认码头还在使用",
}
],
"adjustableBeats": [],
"declaredNewFacts": [],
"mustAppearEntities": ["lin-shen"],
"chapterEndHook": 钩子,
}
@pytest.fixture
def 生成环境(应用测试库, tmp_path):
"""作品、细纲、事实、配置、流程与合成宿主一次装配;剧本由用例注入。"""
库 = 应用测试库[用途.生产]
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
导入内置结构(元数据服务(连))
from muse.任务运行.角色策略 import 角色策略目录
策略 = 角色策略目录.从发布包()
from muse.编排.接口 import 流程登记
登记 = 流程登记()
装配 = 构建(
应用配置(
库.引用,
策略.资源发布身份,
运行用途=库.用途,
原文暂存=str(tmp_path / "raw"),
),
流程=登记,
)
from pathlib import Path
装配生成正文(
装配,
合成计价(),
Path(__file__).parents[2] / "配置" / "流程模板" / "生成正文.yaml",
)
作者 = 调用身份("gen-author", None, 库.用途, 内容用途.生成)
作品, 正文 = 装配.要求作品(), 装配.要求正文()
结构选择 = 作品.新档案结构(作者, "gen-work", "work_core", 1)
作品.保存档案(
作者,
"create-gen-work",
档案保存(
"gen-work", 0, {"名称": "雨码头", "题材定位": "现实", "主题立意": "守约"}, 结构选择
),
)
for i in range(1, 5):
作品.添加章节(作者, f"add-ch-{i}", "gen-work", f"ch-{i}", f"第{i}章", 预期目录版本=i - 1)
正文.保存人工(
作者,
"body-ch-1",
"ch-1",
0,
正文草稿((段落("p-1", (文本节点("林深停在码头,雨还没停。"),)),)),
)
外结构 = 作品.新档案结构(作者, "other-book", "work_core", 1)
作品.保存档案(
作者,
"create-other-book",
档案保存("other-book", 0, {"名称": "别的书", "题材定位": "现实", "主题立意": "无"}, 外结构),
)
作品.添加章节(作者, "add-foreign", "other-book", "foreign", "外章", 预期目录版本=0)
规划 = 装配.要求规划()
def 保存并确认(请求, 命令: str, **关键字) -> dict:
回执 = 规划.保存人工候选(作者, 命令 + "-draft", 请求)
候选 = 规划.读取候选(作者, 回执["results"][0]["candidate_id"])
定位 = 规划定位(
候选["candidate_id"],
候选["revision"],
候选["candidate_hash"],
候选["plan_id"],
请求.expected_revision,
)
审阅 = 规划.打开审阅(作者, 定位)
规划.决定(
作者,
命令 + "-accept",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("plan",),
)
return 规划.读取规划(作者, 候选["plan_id"])
世界 = 装配.要求故事世界()
def 正文源(章: str, 引文: str) -> 正文依据:
记录 = 正文.读取正文(作者, 章)
草稿 = TypeAdapter(正文草稿).validate_python(记录["document"])
return 正文依据(
章, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(草稿, 引文),)
)
def 确认事实(对象, 类型, 内容, 来源=()) -> None:
选择 = 世界.新事实结构(作者, "gen-work", 对象, 类型, 1)
回执 = 世界.提出事实(
作者,
"propose-" + 对象,
事实提案请求("gen-work", 对象, 0, 选择, 内容, tuple(来源), "ch-1"),
)
提案 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
定位 = 事实定位(
提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + 对象, 0
)
审阅 = 世界.打开事实审阅(作者, 定位)
世界.决定事实(
作者,
"confirm-" + 对象,
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("object",),
)
确认事实("lin-shen", "character", {"名称": "林深"}, (正文源("ch-1", "林深停在码头"),))
父 = 保存并确认(
规划保存(
"gen-work", 0, 规划.新规划结构(作者, "gen-work", "outline", 1), {"名称": "雨码头"}
),
"outline",
)
保存并确认(
规划保存(
"gen-work",
0,
规划.新规划结构(作者, "gen-work", "fine_outline", 1),
细纲内容(父["plan_id"], 目标章=3),
chapter_id="ch-3",
),
"fine-outline-ch3",
)
保存并确认(
规划保存(
"gen-work",
0,
规划.新规划结构(作者, "gen-work", "fine_outline", 1),
细纲内容(父["plan_id"], 目标章=4, 钩子="天亮前灯还亮着。"),
chapter_id="ch-4",
),
"fine-outline-ch4",
)
收到: list[dict] = []
剧本: list = []
def 回应(数据):
收到.append(数据)
动作 = 剧本.pop(0) if 剧本 else {"类型": "空对象"}
if 动作.get("类型") == "失败":
raise RuntimeError("合成宿主按剧本失败")
if 动作.get("类型") == "工具调用":
输出 = [
{
"type": "function_call",
"id": f"fc-{len(收到)}",
"call_id": 动作["调用ID"],
"name": 动作["名称"],
"arguments": json.dumps(动作.get("参数", {}), ensure_ascii=False),
}
]
else:
输出 = [
{
"type": "message",
"content": [
{
"type": "output_text",
"text": json.dumps(动作.get("文本", {}), ensure_ascii=False),
}
],
}
]
载荷 = {
"type": "response.completed",
"response": {
"id": f"gen-response-{len(收到)}",
"model": "claude-opus-4-8[1M]",
"status": "completed",
"output": 输出,
"usage": {"input_tokens": 5, "output_tokens": 7},
},
}
return ("data: " + json.dumps(载荷, ensure_ascii=False) + "\n\n").encode()
服务: list = []
def 启动(处理):
class 请求处理(BaseHTTPRequestHandler):
def do_POST(self):
try:
输入 = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
输出 = 处理(输入)
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(输出)))
self.end_headers()
self.wfile.write(输出)
except Exception:
self.send_error(500)
服务.append("失败")
def log_message(self, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), 请求处理)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
服务.append((server, thread))
return f"http://127.0.0.1:{server.server_port}/v1/responses"
地址 = 启动(回应)
凭据 = tmp_path / "gen-key"
凭据.write_text("synthetic-generation-credential")
原配置 = 运行配置内容(
"direct",
"1",
策略.定义["version"],
策略.资源发布身份,
"synthetic",
{
"writer": {
"provider": "synthetic",
"model": "claude-opus-4-8[1M]",
"thinking": "high",
"stage_tools": {"探索": list(探索工具集), "生成": []},
}
},
(凭据引用("provider-key", "受控存储", str(凭据)),),
(提供方配置("synthetic", "responses", 地址, "provider-key"),),
合成计价().版本,
)
class 离线配置检查:
身份 = "synthetic-generation-validation"
def 验证(self, 内容, 执行用途):
return 配置验证证据(
内容哈希(内容.冻结()),
内容.角色策略版本,
内容.资源发布身份,
执行用途,
("synthetic-generation-contract",),
"runtime",
)
管理 = 配置版本管理(库, 离线配置检查())
管理.保存草案("gen-config", "1", 原配置)
回执 = 管理.验证版本("gen-config", "1")
管理.启用("gen-config", "1", 验证回执=回执, 批准引用="synthetic-approval", 预期代次=0)
环境 = {
"库": 库,
"装配": 装配,
"登记": 登记,
"作者": 作者,
"作品": 作品,
"正文": 正文,
"规划": 规划,
"剧本": 剧本,
"收到": 收到,
"管理": 管理,
"服务": 服务,
"raw": str(tmp_path / "raw"),
}
yield 环境
for 项 in 服务:
if isinstance(项, tuple):
项[0].shutdown()
项[0].server_close()
def 发起并跑完(环境, *, 章位="ch-3", 命令=None, 剧本=None):
库, 装配 = 环境["库"], 环境["装配"]
if 剧本:
环境["剧本"].extend(剧本)
回执 = 发起生成正文(
装配,
环境["作者"],
命令 or f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id=章位,
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(库, "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
运行 = 装配.任务运行
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None, "任务未完成却没有可领取步骤"
运行.执行一步(领取)
return 任务ID, 运行.读取任务(任务ID)
def 正常剧本(正文段):
return [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "工具调用", "调用ID": "exp-facts", "名称": "query_facts_as_of", "参数": {}},
{
"类型": "工具调用",
"调用ID": "exp-hist",
"名称": "read_history_document",
"参数": {"chapter_id": "ch-1", "revision": 1},
},
{"类型": "空对象"},
{"类型": "文本", "文本": {"paragraphs": [{"text": 段} for 段 in 正文段]}},
]
def test_任意合法章位生成模型候选并落独立检查报告__b41e29(生成环境):
正文段 = [
"雨点敲着铁皮屋顶,林深把退回的信压在掌心。",
"他沿跳板走向仓库,门后传来一声应答。",
]
任务ID, 快照 = 发起并跑完(生成环境, 章位="ch-3", 剧本=正常剧本(正文段))
assert 快照.状态 == 任务状态.已完成, [s for s in 快照.步骤]
环境 = 生成环境
库, 作者 = 环境["库"], 环境["作者"]
# 写作回合禁工具:合成宿主收到的最后一次请求 tools 为空。
写作请求 = 环境["收到"][-1]
assert not 写作请求.get("tools"), "写作回合必须禁工具"
assert "无工具写作" in 写作请求["instructions"]
检查步 = next(s for s in 快照.步骤 if s["step_id"] == "检查与候选")
候选ID = 检查步["result"]["candidate_id"]
assert 检查步["result"]["verdict"] == "passed"
with 库.连接(只读=True) as 连:
行 = 连.execute(
"SELECT origin,current_revision FROM muse_writing_candidate WHERE candidate_id=%s",
(候选ID,),
).fetchone()
assert 行[0] == "model" and 行[1] == 1
报告 = 连.execute(
"SELECT verdict,basic,continuity FROM muse_review_report "
"WHERE candidate_id=%s AND candidate_revision=1",
(候选ID,),
).fetchone()
assert 报告[0] == "passed"
assert all(项["判定"] == "passed" for 项 in 报告[1]["checks"])
来源 = 连.execute(
"SELECT task_id,flow_version FROM muse_model_candidate WHERE candidate_id=%s",
(候选ID,),
).fetchone()
assert str(来源[0]) == 任务ID and 来源[1] == 流程版本
事件 = 环境["装配"].任务运行.续接事件(任务ID, 0, 数量=100)
类型序 = [e.类型.value for e in 事件.事件]
assert "text.delta" in 类型序 and "candidate.saved" in 类型序
assert "check.completed" in 类型序
候选 = 环境["正文"].读取候选(作者, 候选ID)
assert 可见文本(TypeAdapter(正文草稿).validate_python(候选["document"])) == "\n".join(正文段)
# 第二个合法章位(ch-4,另一细纲)同样可生成。
任务2, 快照2 = 发起并跑完(
生成环境,
章位="ch-4",
剧本=正常剧本(["天快亮了,林深数着灯。", "最后一盏还亮着。"]),
)
assert 快照2.状态 == 任务状态.已完成
检查2 = next(s for s in 快照2.步骤 if s["step_id"] == "检查与候选")
assert 检查2["result"]["candidate_id"] != 候选ID
def test_跨作品与无细纲章位拒绝发起__c73d0f(生成环境):
环境 = 生成环境
# 无细纲章位:授权细纲失败(作品内合法章位但无已确认细纲)。
with pytest.raises(上下文错误):
发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-2",
配置ID="gen-config",
)
# 跨作品章位:章位不属于目标作品。
with pytest.raises(写作任务错误, match="不属于目标作品"):
发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="foreign",
配置ID="gen-config",
)
def test_必需检查失败不能采纳且保留失败候选证据__d52a8e(生成环境):
# 复读细纲硬约束措辞 → 连续性冲突 → verdict failed。
剧本 = 正常剧本(
[
"上一章结尾雨还没停,林深攥着信封走进雨幕。",
"仓库门后有人应声,他把信递了过去。",
]
)
任务ID, 快照 = 发起并跑完(生成环境, 章位="ch-3", 剧本=剧本)
assert 快照.状态 == 任务状态.已完成
环境 = 生成环境
检查步 = next(s for s in 快照.步骤 if s["step_id"] == "检查与候选")
候选ID = 检查步["result"]["candidate_id"]
assert 检查步["result"]["verdict"] == "failed"
正文服务 = 环境["正文"]
作者 = 环境["作者"]
候选 = 正文服务.读取候选(作者, 候选ID)
定位 = 候选定位(
候选ID,
候选["revision"],
候选["candidate_hash"],
候选["document_id"],
候选["current_document_revision"],
)
审阅 = 正文服务.打开候选审阅(作者, 定位)
with pytest.raises(变更错误) as 异常:
正文服务.决定候选(
作者,
f"adopt-{uuid4()}",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("document",),
)
assert 异常.value.错误码 == "CHECK_FAILED"
# 失败候选保留为证据:仍可读取与拒绝。
正文服务.决定候选(
作者,
f"reject-{uuid4()}",
定位,
作者动作.拒绝,
审阅["review_id"],
审阅["review_hash"],
(),
)
assert 正文服务.读取候选(作者, 候选ID)["decision"] == "reject"
def test_无终态断流可定位且重开后任务与事件仍可找回__e6f317(生成环境):
环境 = 生成环境
# 探索后写作回合宿主失败:步骤失败、任务可定位、事件保留、无候选。
剧本 = [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "空对象"},
{"类型": "失败"},
]
回执 = 发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(环境["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
环境["剧本"].extend(剧本)
运行 = 环境["装配"].任务运行
with pytest.raises((Muse错误, RuntimeError, OSError)):
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None
运行.执行一步(领取)
终态 = 运行.读取任务(任务ID)
# 断流定位:任务未无声消失,写作步骤未完成,后续领取把发出未回的调用标记待对账。
assert 终态.状态 != 任务状态.已完成
写作步 = next(s for s in 终态.步骤 if s["step_id"] == "无工具写作")
assert 写作步["state"] != "completed", 终态.步骤
运行.领取步骤("worker", 处理器能力)
对账态 = 运行.读取任务(任务ID)
assert 对账态.状态 in {
任务状态.待对账,
任务状态.已失败,
任务状态.运行中,
}, 对账态.状态
事件 = 运行.续接事件(任务ID, 0, 数量=100)
assert 事件.事件, "断流后事件必须可回放定位"
with 环境["库"].连接(只读=True) as 连:
数 = 连.execute(
"SELECT count(*) FROM muse_writing_candidate c "
"JOIN muse_model_candidate m USING(candidate_id) WHERE m.task_id=%s",
(任务ID,),
).fetchone()[0]
assert 数 == 0
def test_模型自填无效输出被输出合同拒绝__93c5b7(生成环境):
# 模型试图自填检查结论与额外字段:输出合同 additionalProperties=false 直接拒绝。
剧本 = [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "空对象"},
{
"类型": "文本",
"文本": {
"paragraphs": [{"text": "林深收好信。"}],
"checks_passed": True,
"author_confirmed": True,
},
},
]
环境 = 生成环境
回执 = 发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(环境["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
环境["剧本"].extend(剧本)
运行 = 环境["装配"].任务运行
with pytest.raises((Muse错误, RuntimeError, OSError)):
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None
运行.执行一步(领取)
终态 = 运行.读取任务(任务ID)
assert any(s["state"] == "failed" for s in 终态.步骤)

View File

@ -6,6 +6,7 @@ import { useMuse作品会话 } from "../../应用/作品会话";
import { 查询键 } from "../../接口/查询键";
import { 状态 } from "./数据";
import { Ui任务详情 } from "./任务详情";
import { Ui发起生成 } from "../创作对话/会话面板";
export function Ui任务列表() {
const work = useMuse作品会话();
@ -93,6 +94,9 @@ export function Ui任务列表() {
<button className="primary">查看任务</button>
</div>
</form>
{!taskId ? (
<Ui发起生成 scope={work.workId} 默认作品={work.workId ?? ""} />
) : null}
{!taskId ? (
<section className="recent-tasks">
<div className="panel-heading">

View File

@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { Ui任务上下文 } from "../上下文";
import { Ui消息流 } from "../创作对话/消息流";
import { Ui作者动作 } from "../创作对话/作者动作";
import { useMuse作者会话 } from "../../应用/作者会话";
import { Ui恢复控制 } from "./恢复控制";
import { 订阅任务事件 } from "../../接口/事件连接";
import { 查询键 } from "../../接口/查询键";
@ -17,6 +20,14 @@ export function Ui任务详情({
const cache = useQueryClient();
const [events, setEvents] = useState<任务事件[]>([]);
const [notice, setNotice] = useState("");
const 生成中 = task.task_type === "生成正文";
const 候选事件 = [...events]
.reverse()
.find((条) => 条.event_type === "candidate.saved");
const 候选ID = 生成中
? ((候选事件?.payload as { candidate_id?: string } | undefined)?.candidate_id ??
null)
: null;
useEffect(
() =>
订阅任务事件(task.task_id, {
@ -76,6 +87,39 @@ export function Ui任务详情({
</ol>
</section>
<Ui任务上下文 taskId={task.task_id} scope={scope} />
{生成中 && (
<section
className="paper-panel"
style={{ gridColumn: "1 / -1" }}
aria-label="创作对话"
>
<div className="panel-heading">
<h2>创作对话</h2>
<span className="muted">生成过程与候选检查</span>
</div>
<Ui消息流 事件={events} />
{候选ID && <Ui生成候选动作 scope={scope} candidateId={候选ID} />}
</section>
)}
</div>
);
}
/** 生成候选的检查与决定面板;作者会话由此处自取,任务列表无需传递。 */
function Ui生成候选动作({
scope,
candidateId,
}: {
scope: string | null;
candidateId: string;
}) {
const 会话 = useMuse作者会话();
if (!会话.data) return <Ui反馈状态 message="正在读取作者会话…" />;
return (
<Ui作者动作
scope={scope}
author={会话.data.author_id}
candidateId={candidateId}
/>
);
}

View File

@ -0,0 +1,3 @@
export { Ui发起生成 } from "./会话面板";
export { Ui消息流 } from "./消息流";
export { Ui作者动作 } from "./作者动作";

View File

@ -0,0 +1,107 @@
/** 发起生成的会话面板:粒度、章位与运行配置由作者明确选择。 */
import { useState } from "react";
import { useMuse发起生成, 粒度标签, type 写作粒度 } from "./数据";
import { Ui反馈状态 } from "../../界面/反馈状态";
function 新命令身份(): string {
return `gen-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
export function Ui发起生成({
scope,
默认作品 = "",
}: {
scope: string | null;
默认作品?: string;
}) {
const 发起 = useMuse发起生成(scope);
const [作品, set作品] = useState(默认作品);
const [章位, set章位] = useState("");
const [粒度, set粒度] = useState<写作粒度>("new_chapter");
const [配置, set配置] = useState("");
const [用途, set用途] = useState("");
return (
<section className="paper-panel" aria-label="发起生成">
<div className="panel-heading">
<h2>发起生成</h2>
<span className="muted">两阶段写作 · 独立检查</span>
</div>
<form
className="structure-form"
onSubmit={(event) => {
event.preventDefault();
if (!作品 || !章位 || !配置) return;
发起.mutate({
work_id: 作品,
chapter_id: 章位,
granularity: 粒度,
branch_id: "main",
purpose: 用途,
config_id: 配置,
command_id: 新命令身份(),
});
}}
>
<label>
作品 ID
<input
value={作品}
onChange={(e) => set作品(e.target.value.trim())}
placeholder="例如 work-3f2a"
required
/>
</label>
<label>
目标章 ID
<input
value={章位}
onChange={(e) => set章位(e.target.value.trim())}
placeholder="已确认细纲的章节"
required
/>
</label>
<label>
写作粒度
<select
value={粒度}
onChange={(e) => set粒度(e.target.value as 写作粒度)}
>
{Object.entries(粒度标签).map(([值, 名]) => (
<option key={值} value={值}>
{名}
</option>
))}
</select>
</label>
<label>
运行配置 ID
<input
value={配置}
onChange={(e) => set配置(e.target.value.trim())}
placeholder="已验证启用的配置版本"
required
/>
</label>
<label>
用途说明
<input
value={用途}
onChange={(e) => set用途(e.target.value)}
placeholder="可选:本次生成的目标或要求"
/>
</label>
<button type="submit" disabled={发起.isPending}>
{发起.isPending ? "正在发起…" : "发起生成"}
</button>
</form>
{发起.error ? (
<Ui反馈状态 message={发起.error.message} error />
) : 发起.data ? (
<p className="mono muted">
已创建任务 {发起.data.task_id};在任务列表中查看进度、正文与候选。
</p>
) : null}
</section>
);
}

View File

@ -0,0 +1,108 @@
/** 生成候选的作者动作:检查报告呈现 + 复用候选审阅决定流。 */
import { useCallback, useEffect, useState } from "react";
import { 判定文案, useMuse审校报告 } from "./数据";
import { 打开审阅, Ui决策面板, type 候选审阅 } from "../正式变更";
import { 客户端 } from "../../接口/生成/客户端";
import { Ui反馈状态 } from "../../界面/反馈状态";
import type { operations } from "../../接口/生成/类型";
type 候选记录 =
operations["read_writing_candidate"]["responses"][200]["content"]["application/json"];
/** 生成候选的作者动作面板:独立检查报告与成熟决定流复用同一服务端核对。 */
export function Ui作者动作({
scope,
author,
candidateId,
}: {
scope: string | null;
author: string;
candidateId: string;
}) {
const [候选, set候选] = useState<候选记录 | null>(null);
const [审阅, set审阅] = useState<候选审阅 | null>(null);
const [失败, set失败] = useState("");
const 报告 = useMuse审校报告(scope, candidateId, 候选?.revision ?? null);
const 读取并审阅 = useCallback(async () => {
set失败("");
try {
const { data } = await 客户端.GET(
"/api/v1/writing-candidates/{candidate_id}",
{ params: { path: { candidate_id: candidateId } } },
);
if (!data) throw new Error("候选读取为空");
set候选(data);
set审阅(
await 打开审阅({
candidate_id: candidateId,
candidate_revision: data.revision,
candidate_hash: data.candidate_hash,
target_ref: data.document_id,
expected_revision: data.current_document_revision,
}),
);
} catch (错误) {
set失败(错误 instanceof Error ? 错误.message : "候选读取失败");
}
}, [candidateId]);
useEffect(() => {
void 读取并审阅();
}, [读取并审阅]);
return (
<section className="paper-panel" aria-label="候选检查与决定">
<div className="panel-heading">
<h2>候选检查与决定</h2>
<span className="state-pill">
{报告.data ? 判定文案[报告.data.verdict] : "读取中"}
</span>
</div>
{报告.isPending ? (
<Ui反馈状态 message="正在读取独立检查报告…" />
) : 报告.error ? (
<Ui反馈状态 message={报告.error.message} error />
) : 报告.data ? (
<>
<ol className="step-list">
{报告.data.basic.checks.map((项) => (
<li key={项["名称"]}>
<span className="step-number">
{项["判定"] === "passed" ? "过" : "败"}
</span>
<div>
<strong>{项["名称"]}</strong>
<span>
{项["证据"]}({判定文案[项["判定"]] ?? 项["判定"]})
</span>
</div>
</li>
))}
</ol>
{报告.data.continuity.findings.length > 0 ? (
<ul className="event-list">
{报告.data.continuity.findings.map((发现, 序) => (
<li key={序}>
{发现["类别"]}·{判定文案[发现["判定"]] ?? 发现["判定"]}:
{发现["说明"]}
</li>
))}
</ul>
) : (
<p className="muted">连续性检查没有发现问题。</p>
)}
</>
) : (
<Ui反馈状态 message="该候选版本没有独立检查报告。" />
)}
{失败 && <Ui反馈状态 message={失败} error />}
{审阅 ? (
<Ui决策面板 review={审阅} author={author} onReopen={() => void 读取并审阅()} />
) : (
!失败 && <Ui反馈状态 message="正在打开候选审阅…" />
)}
<p className="muted">必需检查未通过的候选不能采纳;服务端按候选版本核对检查结论。</p>
</section>
);
}

View File

@ -0,0 +1,89 @@
/** 创作对话的数据接入:发起生成、事件解读与候选检查报告查询。 */
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { 客户端 } from "../../接口/生成/客户端";
import { 查询键 } from "../../接口/查询键";
import { 接口错误 } from "../../接口/传输";
import type { operations } from "../../接口/生成/类型";
export type 发起回执 =
operations["start_writing_task"]["responses"][201]["content"]["application/json"];
export type 写作粒度 = "new_chapter" | "expand_scene" | "edit_selection";
export type 审校报告 =
operations["read_candidate_review_report"]["responses"][200]["content"]["application/json"];
export const 粒度标签: Record<string, string> = {
new_chapter: "新章",
expand_scene: "扩写",
edit_selection: "选段修改",
};
export const 事件名补充: Record<string, string> = {
"text.delta": "写出段落",
"candidate.saved": "候选已保存",
"check.completed": "检查完成",
"tool.read": "读取材料",
};
/** 发起生成正文:命令身份由调用方生成并保持幂等。 */
export function useMuse发起生成(scope: string | null) {
const cache = useQueryClient();
return useMutation({
mutationFn: async (输入: {
work_id: string;
chapter_id: string;
granularity: 写作粒度;
branch_id: string;
purpose: string;
config_id: string;
command_id: string;
}) => {
const { work_id, ...body } = 输入;
const { data, error, response } = await 客户端.POST(
"/api/v1/works/{work_id}/writing-tasks",
{ params: { path: { work_id } }, body },
);
if (data) return data;
throw new 接口错误(
(error as { code?: string })?.code ?? "HTTP_ERROR",
(error as { message?: string })?.message ?? "发起生成未完成,请重试。",
response.status,
);
},
onSuccess: () => {
void cache.invalidateQueries({ queryKey: 查询键(scope, "tasks") });
},
});
}
/** 读取候选当前版本的独立检查报告。 */
export function useMuse审校报告(
scope: string | null,
candidateId: string | null,
revision: number | null,
) {
return useQuery({
queryKey: 查询键(scope, candidateId ?? "", "review-report", String(revision ?? 0)),
enabled: !!candidateId,
queryFn: async ({ signal }) => {
const 身份 = candidateId as string;
return (
await 客户端.GET("/api/v1/candidates/{candidate_id}/review-report", {
params: {
path: { candidate_id: 身份 },
query: revision ? { revision } : undefined,
signal,
},
})
).data!;
},
});
}
export const 判定文案: Record<string, string> = {
passed: "通过",
failed: "未通过",
conflict: "冲突",
pending: "待证",
};

View File

@ -0,0 +1,49 @@
/** 生成过程的消息流:正文段落、材料读取与检查事件按序呈现。 */
import type { 任务事件 } from "../任务运行/数据";
import { 事件名 } from "../任务运行/数据";
import { 事件名补充 } from "./数据";
/** 把任务事件流折叠为可读消息;正文段落按写作顺序累积成稿。 */
export function 折叠消息(事件: 任务事件[]): {
正文段落: string[];
动作: { key: string; 文案: string }[];
} {
const 正文段落: string[] = [];
const 动作: { key: string; 文案: string }[] = [];
for (const 条 of 事件) {
const 载荷 = 条.payload as Record<string, unknown>;
if (条.event_type === "text.delta" && typeof 载荷?.文本 === "string") {
正文段落.push(载荷.文本);
continue;
}
const 名 = 事件名补充[条.event_type] ?? 事件名[条.event_type];
if (名) 动作.push({ key: 条.event_id, 文案: 名 });
}
return { 正文段落, 动作 };
}
export function Ui消息流({ 事件 }: { 事件: 任务事件[] }) {
const { 正文段落, 动作 } = 折叠消息(事件);
if (!正文段落.length && !动作.length)
return (
<p className="muted">生成过程的事件会在这里按顺序出现;页面重开后仍可找回。</p>
);
return (
<div className="event-panel" aria-live="polite">
{动作.length > 0 && (
<ol className="event-list">
{动作.map((条) => (
<li key={条.key}>{条.文案}</li>
))}
</ol>
)}
{正文段落.length > 0 && (
<article className="draft-stream">
{正文段落.map((段, 序) => (
<p key={序}>{段}</p>
))}
</article>
)}
</div>
);
}

View File

@ -690,6 +690,40 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/works/{work_id}/writing-tasks": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** 发起生成 */
post: operations["start_writing_task"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/candidates/{candidate_id}/review-report": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** 读取审校报告 */
get: operations["read_candidate_review_report"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/chapters/{chapter_id}/writing-candidates": {
parameters: {
query?: never;
@ -2351,6 +2385,31 @@ export interface components {
/** Steps */
steps: components["schemas"]["muse______http__________________1"][];
};
/** 基础呈现 */
muse______http__________________3: {
/** Checks */
checks: components["schemas"]["muse______http___________________4"][];
/** Verdict */
verdict: string;
};
/** 发现呈现 */
muse______http__________________4: {
/** 类别 */
"\u7C7B\u522B": string;
/** 判定 */
"\u5224\u5B9A": string;
/** 说明 */
"\u8BF4\u660E": string;
/** 证据 */
"\u8BC1\u636E": {
[key: string]: unknown;
};
/**
* 位置
* @default
*/
"\u4F4D\u7F6E": string;
};
/** 省略条目响应 */
muse______http___________________1: {
/** Source */
@ -2382,6 +2441,24 @@ export interface components {
/** Current State */
current_state: string;
};
/** 检查项呈现 */
muse______http___________________4: {
/** 名称 */
"\u540D\u79F0": string;
/** 判定 */
"\u5224\u5B9A": string;
/** 证据 */
"\u8BC1\u636E": string;
};
/** 连续性呈现 */
muse______http___________________5: {
/** Findings */
findings: components["schemas"]["muse______http__________________4"][];
/** Coverage */
coverage: {
[key: string]: unknown;
};
};
/** 会话授权请求 */
muse______http____________________1: {
/** Command Id */
@ -2445,6 +2522,56 @@ export interface components {
/** Authorization Id */
authorization_id: string;
};
/** 发起生成请求 */
muse______http____________________5: {
/** Command Id */
command_id: string;
/** Chapter Id */
chapter_id: string;
/**
* Granularity
* @enum {string}
*/
granularity: "new_chapter" | "expand_scene" | "edit_selection";
/**
* Branch Id
* @default main
*/
branch_id: string;
/**
* Purpose
* @default
*/
purpose: string;
/** Config Id */
config_id: string;
/** Budget */
budget?: {
[key: string]: number;
} | null;
};
/** 发起生成回执 */
muse______http____________________6: {
/** Task Id */
task_id: string;
/** Writing Task Id */
writing_task_id: string;
};
/** 审校报告呈现 */
muse______http____________________7: {
/** Report Id */
report_id: string;
/** Candidate Id */
candidate_id: string;
/** Candidate Revision */
candidate_revision: number;
/** Snapshot Id */
snapshot_id: string;
basic: components["schemas"]["muse______http__________________3"];
continuity: components["schemas"]["muse______http___________________5"];
/** Verdict */
verdict: string;
};
};
responses: never;
parameters: never;
@ -3997,6 +4124,74 @@ export interface operations {
};
};
};
start_writing_task: {
parameters: {
query?: never;
header?: never;
path: {
work_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["muse______http____________________5"];
};
};
responses: {
/** @description Successful Response */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["muse______http____________________6"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
read_candidate_review_report: {
parameters: {
query?: {
revision?: number | null;
};
header?: never;
path: {
candidate_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["muse______http____________________7"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_writing_candidates: {
parameters: {
query?: {

View File

@ -0,0 +1,82 @@
/** 创作对话组件传输替身:消息流折叠、发起回执与检查报告渲染;真实旅程由 Playwright 承担。 */
import { afterEach, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { Ui依赖装配 } from "../../src/应用/依赖装配";
import { Ui消息流 } from "../../src/功能/创作对话/消息流";
import { Ui发起生成 } from "../../src/功能/创作对话/会话面板";
import type { 任务事件 } from "../../src/功能/任务运行/数据";
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
function 事件(类型: string, 载荷: Record<string, unknown>, 序 = 1): 任务事件 {
return {
event_id: `e-${序}`,
sequence: 序,
event_type: 类型,
occurred_at: "2026-09-11T10:00:00Z",
payload: 载荷,
} as 任务事件;
}
it("NC-ui-dialog-stream:正文段落按序累积、动作事件标注", () => {
render(
<Ui依赖装配>
<Ui消息流
事件={[
事件("step.started", {}, 1),
事件("text.delta", { 文本: "雨点敲着铁皮屋顶。" }, 2),
事件("text.delta", { 文本: "林深把信压在掌心。" }, 3),
事件("candidate.saved", { candidate_id: "c-1" }, 4),
]}
/>
</Ui依赖装配>,
);
expect(screen.getByText("雨点敲着铁皮屋顶。")).toBeTruthy();
expect(screen.getByText("林深把信压在掌心。")).toBeTruthy();
expect(screen.getByText("开始处理")).toBeTruthy();
expect(screen.getByText("候选已保存")).toBeTruthy();
});
it("NC-ui-dialog-empty:无事件时给出找回提示不空白", () => {
render(
<Ui依赖装配>
<Ui消息流 事件={[]} />
</Ui依赖装配>,
);
expect(screen.getByText(/页面重开后仍可找回/)).toBeTruthy();
});
it("NC-ui-dialog-start:发起成功展示任务回执", async () => {
const 请求: string[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (request: Request) => {
请求.push(request.url);
if (new URL(request.url).pathname === "/api/v1/works/w-9/writing-tasks") {
const 体 = await request.clone().json();
expect(体.granularity).toBe("new_chapter");
return new Response(
JSON.stringify({ task_id: "task-9", writing_task_id: "wt-9" }),
{ status: 201, headers: { "Content-Type": "application/json" } },
);
}
return new Response(JSON.stringify({ author_id: "author" }), {
headers: { "Content-Type": "application/json" },
});
}),
);
render(
<Ui依赖装配>
<Ui发起生成 scope="w-9" 默认作品="w-9" />
</Ui依赖装配>,
);
const 输入框 = Array.from(document.querySelectorAll("input")) as HTMLInputElement[];
expect(输入框[0].value).toBe("w-9");
fireEvent.input(输入框[1], { target: { value: "ch-9" } });
fireEvent.input(输入框[2], { target: { value: "cfg-1" } });
fireEvent.click(screen.getByRole("button", { name: "发起生成" }));
expect(await screen.findByText(/task-9/)).toBeTruthy();
});

View File

@ -0,0 +1,65 @@
/** 真实浏览器和隔离库:生成正文任务的创作对话、候选检查与失败候选不能采纳。 */
import { readFileSync } from "node:fs";
import { expect, test } from "@playwright/test";
test.use({ trace: "off" });
test("NC-browser-generation-dialog:创作对话、独立检查与采纳阻断", async ({
page,
}, info) => {
if (!process.env.MUSE_WORKBENCH_URL || !process.env.MUSE_AUTHOR_PASSWORD_FILE)
throw new Error("需要明确的隔离后端和作者口令文件。");
const goodTaskId = process.env.MUSE_GEN_TASK_ID;
const failedTaskId = process.env.MUSE_GEN_FAILED_TASK_ID;
if (!goodTaskId || !failedTaskId)
throw new Error("需要已跑完的通过任务与失败候选任务编号。");
const consoleErrors: string[] = [];
page.on("pageerror", (error) => consoleErrors.push(String(error)));
await page.goto("/");
await page
.getByLabel("访问口令")
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE, "utf8").trim());
await page.getByRole("button", { name: "进入工作台" }).click();
await page.getByRole("link", { name: /任务记录/ }).waitFor();
page.on("console", (message) => {
if (message.type() === "error" && !/favicon/.test(message.text()))
consoleErrors.push(message.text());
});
await page.goto("/tasks");
await page.getByLabel("任务编号").fill(goodTaskId);
await page.getByRole("button", { name: "查看任务" }).click();
const 对话 = page.locator('section[aria-label="创作对话"]');
await expect(对话).toBeVisible();
// 流式正文与过程事件可见。
await expect(对话.getByText("雨点敲着铁皮屋顶,林深把信压在掌心。")).toBeVisible();
await expect(对话.getByText("候选已保存")).toBeVisible();
// 独立检查报告:基础项全过、连续性无发现;采纳入口开放。
const 动作 = page.locator('section[aria-label="候选检查与决定"]');
await expect(动作.getByText("通过", { exact: true }).first()).toBeVisible();
await expect(动作.getByText("连续性检查没有发现问题。")).toBeVisible();
await expect(
动作.getByRole("button", { name: "采纳为正文新版本" }),
).toBeEnabled();
await page.screenshot({ path: `${info.outputDir}/生成任务与候选检查.png`, fullPage: true });
// 失败候选:复读细纲措辞 → 冲突呈现且采纳被服务端阻断(错误经作者决定面板呈现)。
await page.goto("/tasks");
await page.getByLabel("任务编号").fill(failedTaskId);
await page.getByRole("button", { name: "查看任务" }).click();
const 失败动作 = page.locator('section[aria-label="候选检查与决定"]');
await expect(失败动作).toBeVisible();
await expect(失败动作.getByText(/复读了细纲措辞/)).toBeVisible();
await expect(失败动作.getByText("未通过").first()).toBeVisible();
await 失败动作.getByRole("button", { name: "采纳为正文新版本" }).click();
await expect(失败动作.locator("p[role=\"alert\"]")).toContainText(/必需检查未通过/);
// 上一次点击的服务端拒绝(CHECK_FAILED)会以 422 响应返回,浏览器控制台把它记为资源错误;
// 拒绝本身正是上一行断言钉住的行为,故按 W14 同样的噪声排除纪律只排除这一条。
expect(
consoleErrors.filter((文本) => !文本.includes("status of 422")),
"页面不得出现预期外的控制台错误",
).toEqual([]);
}
);

View File

@ -0,0 +1,59 @@
-- W15(B05/B06):模型候选来源证据与审校报告;候选来源在作者改稿之外扩展模型生成。
-- 模型候选不走 S01 作者决定命令,由写作任务在任务运行事务内写入;作者决定仍归 S01。
-- B05/B06 正式内容表只在 public 命名空间(与 V0009 一致)。
DO $migration$
DECLARE
origin_constraint text;
BEGIN
SELECT conname INTO origin_constraint
FROM pg_constraint
WHERE conrelid = 'public.muse_writing_candidate'::regclass
AND contype = 'c'
AND pg_get_constraintdef(oid) ILIKE '%origin%';
IF origin_constraint IS NULL THEN
RAISE EXCEPTION '候选来源约束缺失,不能盲目扩展';
END IF;
EXECUTE format($ddl$
ALTER TABLE public.muse_writing_candidate DROP CONSTRAINT %I;
ALTER TABLE public.muse_writing_candidate
ADD CONSTRAINT writing_candidate_origin_check
CHECK (origin IN ('author', 'model'));
-- 模型候选的证据绑定:来源任务、冻结快照与处理器/模型回执哈希;一行绑定一个候选,不可改写。
CREATE TABLE public.muse_model_candidate (
candidate_id uuid PRIMARY KEY
REFERENCES public.muse_writing_candidate(candidate_id),
task_id uuid NOT NULL REFERENCES public.muse_task(task_id),
snapshot_id uuid NOT NULL REFERENCES public.muse_context_snapshot(snapshot_id),
flow_version text NOT NULL,
processor_versions jsonb NOT NULL,
model_evidence jsonb NOT NULL,
prompt_hashes jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CHECK (jsonb_typeof(processor_versions) = 'object'),
CHECK (jsonb_typeof(model_evidence) = 'array'),
CHECK (jsonb_typeof(prompt_hashes) = 'object')
);
-- 审校报告:绑定候选版本与所依据快照;问题与覆盖只追加,不修改候选本身。
CREATE TABLE public.muse_review_report (
report_id uuid PRIMARY KEY,
candidate_id uuid NOT NULL
REFERENCES public.muse_writing_candidate(candidate_id),
candidate_revision bigint NOT NULL CHECK (candidate_revision > 0),
snapshot_id uuid NOT NULL
REFERENCES public.muse_context_snapshot(snapshot_id),
basic jsonb NOT NULL,
continuity jsonb NOT NULL,
verdict text NOT NULL CHECK (verdict IN ('passed', 'failed')),
coverage jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (candidate_id, candidate_revision),
CHECK (basic ? ALL (ARRAY['checks', 'verdict'])),
CHECK (continuity ? ALL (ARRAY['findings', 'coverage'])),
CHECK (jsonb_typeof(coverage) = 'object')
);
CREATE INDEX ON public.muse_review_report (candidate_id, created_at);
$ddl$, origin_constraint);
END
$migration$;

View File

@ -0,0 +1,29 @@
# 生成正文流程模板(编排):只引用已登记处理器与槽位;保护节点不可删除。
# 本文件是可审配置:发布版本固定全部合法步骤的处理器身份、版本、依赖与槽位。
flow_id: 生成正文
version: 1.0.0
steps:
- step_id: 预组装
processor: ctx.prep
processor_version: "1"
- step_id: 受限探索
processor: ctx.explore
processor_version: "1"
depends_on: [预组装]
role: writer
tools: [query_work_plan, query_facts_as_of, read_history_document]
- step_id: 冻结回放
processor: ctx.freeze
processor_version: "1"
depends_on: [受限探索]
protection: 冻结上下文
- step_id: 无工具写作
processor: ctx.write
processor_version: "1"
depends_on: [冻结回放]
role: writer
tools: []
- step_id: 检查与候选
processor: ctx.check
processor_version: "1"
depends_on: [无工具写作]