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:
parent
baf7d431a4
commit
63ead639f4
16
.agent/skills/操作/写下一章/SKILL.md
Normal file
16
.agent/skills/操作/写下一章/SKILL.md
Normal 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>` 续接读取。必需检查未通过的候选不能采纳(服务端阻断),报告中的发现与证据逐条对应来源版本;模型不参与检查判定,也不自填候选哈希或检查结论。
|
||||
3
.agent/skills/操作/写下一章/目录.md
Normal file
3
.agent/skills/操作/写下一章/目录.md
Normal file
@ -0,0 +1,3 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 写下一章 | [SKILL.md](SKILL.md) | 对目标章发起两阶段生成:受限探索、冻结回放、无工具写作与独立检查,产出模型候选供作者审阅。 | 需要该项操作时 | 只使用已实现的具名入口 |
|
||||
16
.agent/skills/操作/扩写场景/SKILL.md
Normal file
16
.agent/skills/操作/扩写场景/SKILL.md
Normal 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>` 续接读取。必需检查未通过的候选不能采纳(服务端阻断),报告中的发现与证据逐条对应来源版本;模型不参与检查判定,也不自填候选哈希或检查结论。
|
||||
3
.agent/skills/操作/扩写场景/目录.md
Normal file
3
.agent/skills/操作/扩写场景/目录.md
Normal file
@ -0,0 +1,3 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 扩写场景 | [SKILL.md](SKILL.md) | 对既有章节按扩写粒度发起生成,材料与检查与写下一章同一受控链路。 | 需要该项操作时 | 只使用已实现的具名入口 |
|
||||
16
.agent/skills/操作/核对内容一致性/SKILL.md
Normal file
16
.agent/skills/操作/核对内容一致性/SKILL.md
Normal 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>` 续接读取。必需检查未通过的候选不能采纳(服务端阻断),报告中的发现与证据逐条对应来源版本;模型不参与检查判定,也不自填候选哈希或检查结论。
|
||||
3
.agent/skills/操作/核对内容一致性/目录.md
Normal file
3
.agent/skills/操作/核对内容一致性/目录.md
Normal file
@ -0,0 +1,3 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 核对内容一致性 | [SKILL.md](SKILL.md) | 读取候选的独立检查报告:基础有效性与连续性发现(出场核对、复读拦截),证据逐条可回读。 | 需要该项操作时 | 只使用已实现的具名入口 |
|
||||
@ -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) | 读取候选的独立检查报告:基础有效性与连续性发现,证据逐条可回读。 | 需要该项操作时 | 只使用已实现的具名入口 |
|
||||
|
||||
@ -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
@ -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 后端;视觉判定独立留证。
|
||||
- 目标:通过任务与失败候选的检查呈现及服务端阻断可复核,页面无预期外控制台错误。
|
||||
|
||||
@ -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。
|
||||
|
||||
@ -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)。
|
||||
|
||||
@ -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)。
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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 和测试环境
|
||||
|
||||
@ -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)。
|
||||
|
||||
|
||||
@ -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)。
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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 模型协议错误("费用规则与任务固定配置版本不一致")
|
||||
|
||||
@ -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 必须显式配置")
|
||||
|
||||
@ -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 {
|
||||
|
||||
1
src/muse/审校修订/__init__.py
Normal file
1
src/muse/审校修订/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""审校修订模块;跨模块只经 接口.py 导入。"""
|
||||
76
src/muse/审校修订/基础检查.py
Normal file
76
src/muse/审校修订/基础检查.py
Normal 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__ = ["运行基础检查", "判定必需通过", "审校错误"]
|
||||
107
src/muse/审校修订/存储.py
Normal file
107
src/muse/审校修订/存储.py
Normal 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 行集]
|
||||
101
src/muse/审校修订/接口.py
Normal file
101
src/muse/审校修订/接口.py
Normal 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__ = [
|
||||
"审校服务",
|
||||
"审校报告",
|
||||
"审校错误",
|
||||
"基础检查报告",
|
||||
"连续性报告",
|
||||
"组装审校报告",
|
||||
"补齐报告身份",
|
||||
"运行基础检查",
|
||||
"运行连续性审校",
|
||||
]
|
||||
27
src/muse/审校修订/提示词/连续性审阅.md
Normal file
27
src/muse/审校修订/提示词/连续性审阅.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 连续性审阅(审校修订 · 语义阶段模板)
|
||||
|
||||
> 资源身份随发布包加载并记录哈希;本模板供后续语义审阅阶段使用,
|
||||
> W15 的确定性连续性检查(出场核对与复读拦截)不调用本模板。
|
||||
|
||||
你是独立审校员,与写手互不相识。你收到的输入只有:
|
||||
|
||||
- 候选正文全文(已通过基础有效性检查);
|
||||
- 冻结上下文快照中的历史正文投影、时点事实与目标章细纲投影;
|
||||
- 各材料的身份与版本(用于回读证据)。
|
||||
|
||||
## 任务
|
||||
|
||||
逐段核对候选正文与上述证据,只报告以下四类问题:
|
||||
|
||||
1. **事实冲突**:候选断言与时点事实或历史正文矛盾(给出证据位置与原文摘句)。
|
||||
2. **视角问题**:叙事人称或聚焦对象与既定视角不一致(注明依据段落)。
|
||||
3. **因果断裂**:事件先后、动机与结果在候选内部或与历史衔接处不成立。
|
||||
4. **引文证据**:候选中声称引用历史原文的语句必须可在历史投影逐字回读;
|
||||
无法回读的引用标注为「待证」,不得自行认定为伪造或合法。
|
||||
|
||||
## 纪律
|
||||
|
||||
- 只输出问题与证据,不修改候选,不提出改写文本。
|
||||
- 每条发现注明证据来源(材料身份与版本)与候选中的位置。
|
||||
- 缺证据的判断一律标「待证」,不得升级为冲突。
|
||||
- 未覆盖的段落明确列出;禁止以部分覆盖宣称整章通过。
|
||||
92
src/muse/审校修订/模型.py
Normal file
92
src/muse/审校修订/模型.py
Normal 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 审校错误("总判定必须与基础检查及冲突级发现一致")
|
||||
115
src/muse/审校修订/连续性审校.py
Normal file
115
src/muse/审校修订/连续性审校.py
Normal 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__ = ["运行连续性审校", "规则版本"]
|
||||
@ -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():
|
||||
|
||||
67
src/muse/接入/cli/创作命令.py
Normal file
67
src/muse/接入/cli/创作命令.py
Normal 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__ = ["运行创作命令", "生成发起请求"]
|
||||
@ -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(事件路由)
|
||||
|
||||
119
src/muse/接入/http/路由/创作生成.py
Normal file
119
src/muse/接入/http/路由/创作生成.py
Normal 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"],
|
||||
)
|
||||
@ -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"],
|
||||
|
||||
139
src/muse/正文写作/写作任务.py
Normal file
139
src/muse/正文写作/写作任务.py
Normal 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__ = [
|
||||
"写作任务合同",
|
||||
"写作任务错误",
|
||||
"写作粒度集",
|
||||
"字符选区",
|
||||
"创建写作任务",
|
||||
"正文错误",
|
||||
]
|
||||
24
src/muse/正文写作/提示词/生成正文.md
Normal file
24
src/muse/正文写作/提示词/生成正文.md
Normal file
@ -0,0 +1,24 @@
|
||||
# 生成正文(正文写作 · 无工具写作模板)
|
||||
|
||||
> 资源身份随发布包加载并记录哈希;角色身份、字段结构、权限和预算从各自权威装配,
|
||||
> 本模板不含固定作品内容或测试答案。
|
||||
|
||||
你是本作品的写手。你收到的是一次冻结装配的写作材料:
|
||||
|
||||
- 目标章细纲投影(目标、硬约束、关键事件、必出场对象、章末钩子);
|
||||
- 截止时点的历史正文投影与事实投影(如装配入选);
|
||||
- 任务粒度与用途说明(新章 / 扩写 / 选段修改)。
|
||||
|
||||
## 写作要求
|
||||
|
||||
1. 只使用装配材料中的设定与事实;不得虚构与材料冲突的事实。
|
||||
2. 严格覆盖细纲关键事件的顺序与结果方向;必出场对象必须实际出场。
|
||||
3. 用戏剧化叙述展开,禁止把细纲措辞原样搬进正文。
|
||||
4. 输出只包含正文段落,不含大纲、说明、注释或元信息。
|
||||
|
||||
## 输出
|
||||
|
||||
输出一个 JSON 对象:`{"paragraphs": [{"text": "段落文本"}, ...]}`。
|
||||
|
||||
- 每段是完整段落文本,不含换行符;段内换行由系统处理。
|
||||
- 段落顺序即正文顺序;不输出段落标题。
|
||||
@ -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", "必需检查未通过,不能采纳该候选")
|
||||
|
||||
663
src/muse/编排/生成正文.py
Normal file
663
src/muse/编排/生成正文.py
Normal 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__ = [
|
||||
"登记生成正文",
|
||||
"发起生成正文",
|
||||
"流程定义_生成正文",
|
||||
"生成正文步骤",
|
||||
"生成正文模板",
|
||||
"写入模型候选",
|
||||
"流程身份",
|
||||
"流程版本",
|
||||
"探索工具集",
|
||||
"生成正文错误",
|
||||
"处理器版本集",
|
||||
]
|
||||
62
src/muse/编排/载入模板.py
Normal file
62
src/muse/编排/载入模板.py
Normal 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__ = ["载入流程定义"]
|
||||
105
tests/契约/test_连续性审校合同.py
Normal file
105
tests/契约/test_连续性审校合同.py
Normal 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",
|
||||
)
|
||||
642
tests/集成/test_两阶段写手.py
Normal file
642
tests/集成/test_两阶段写手.py
Normal 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 终态.步骤)
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
3
web/src/功能/创作对话/index.ts
Normal file
3
web/src/功能/创作对话/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Ui发起生成 } from "./会话面板";
|
||||
export { Ui消息流 } from "./消息流";
|
||||
export { Ui作者动作 } from "./作者动作";
|
||||
107
web/src/功能/创作对话/会话面板.tsx
Normal file
107
web/src/功能/创作对话/会话面板.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
108
web/src/功能/创作对话/作者动作.tsx
Normal file
108
web/src/功能/创作对话/作者动作.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
89
web/src/功能/创作对话/数据.ts
Normal file
89
web/src/功能/创作对话/数据.ts
Normal 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: "待证",
|
||||
};
|
||||
49
web/src/功能/创作对话/消息流.tsx
Normal file
49
web/src/功能/创作对话/消息流.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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?: {
|
||||
|
||||
82
web/tests/交互/创作对话.test.tsx
Normal file
82
web/tests/交互/创作对话.test.tsx
Normal 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();
|
||||
});
|
||||
65
web/tests/旅程/生成对话.spec.ts
Normal file
65
web/tests/旅程/生成对话.spec.ts
Normal 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([]);
|
||||
}
|
||||
);
|
||||
59
数据库/迁移/V0021__模型候选与审校报告.sql
Normal file
59
数据库/迁移/V0021__模型候选与审校报告.sql
Normal 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$;
|
||||
29
配置/流程模板/生成正文.yaml
Normal file
29
配置/流程模板/生成正文.yaml
Normal 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: [无工具写作]
|
||||
Loading…
x
Reference in New Issue
Block a user