W19 世界设定、人物弧线与全书连续性(收口):世界设定维护层(S04 族驱动实体浏览/轨迹/save_entity 提案→确认,新类型零代码可编辑)、人物关系(端点按引用规则解析、弧线计划/已发生分区)、设定修订影响评估(受影响事实引用反查+待决提案+章序影响,只读只标记不撤回独立确认);HTTP 7 端点+抽取章节事实提示词+维护故事世界技能;工作台三页(世界资料/人物关系+关系图/时间与伏笔);7 新 NC(NC-w19-*),离线 275 / 非宿主数据库 237 / 前端 27,浏览器三页旅程+视觉判定通过;整包验证退回一项(设计页尾空行致 diff-check 失败)三轮复验通过(第二轮误在原仓目录执行系任务书缺陷,第三轮带基线前置校验确认修复与证据链),W19 置 verified。
This commit is contained in:
parent
06fcfb32cd
commit
88dd570791
@ -13,3 +13,4 @@
|
||||
| 采集资料与榜单 | [采集资料与榜单/SKILL.md](采集资料与榜单/SKILL.md) | 具名榜单与网页的时点采集 | 榜单观察与网页资料留存时 | 失败如实保留;不补造数据、不作效果归因 |
|
||||
| 拆解书稿 | [拆解书稿/SKILL.md](拆解书稿/SKILL.md) | 参考作品分窗拆解的发起与进度 | 研究参考作品章节、实体与写法时 | 同源互斥;失败可恢复;分析不是本书事实 |
|
||||
| 检查与维护拆书结果 | [检查与维护拆书结果/SKILL.md](检查与维护拆书结果/SKILL.md) | 拆书成果的检查、重算与版本维护 | 处理失败窗、歧义与版本对照时 | 歧义不误并;旧结论保留 |
|
||||
| 维护故事世界 | [维护故事世界/SKILL.md](维护故事世界/SKILL.md) | 世界实体、人物关系与影响评估的维护 | 维护设定、修订前评估、弧线追踪时 | 先审后入;修订不撤回独立确认 |
|
||||
|
||||
28
.agent/skills/操作/维护故事世界/SKILL.md
Normal file
28
.agent/skills/操作/维护故事世界/SKILL.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: 维护故事世界
|
||||
description: 浏览与编辑世界实体、人物关系与弧线,评估设定修订影响。维护设定、追踪伏笔与影响面时使用。
|
||||
---
|
||||
|
||||
# 维护故事世界
|
||||
|
||||
世界实体(人物、地点、物品、组织、规则、代价)与人物关系的长期维护;设定修订前先看影响面。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 需要查看或修改本书世界设定时。
|
||||
- 计划修订设定或重排章序,需要先评估影响时。
|
||||
- 需要查看人物弧线(计划 vs 已发生)或关系清单时。
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. 世界资料页(/world)浏览实体分族清单;点开对象查看版本轨迹(来源与版本可追查)。
|
||||
2. 编辑实体:页面提交或 `POST /works/{work_id}/world/entities` 生成结构化提案;提案经审阅确认后才成为正式事实(先审后入)。
|
||||
3. 修订前评估:`GET /works/{work_id}/world/entities/{object_id}/impact`(可带新章序)查看受影响事实、待决提案与章序影响;清单只标记复核,不撤回独立确认。
|
||||
4. 人物关系页查看人物分组与端点关系;人物弧线按「计划/已发生」分区,猜测与规划不混入已发生。
|
||||
5. 章序调整后用影响评估复查:叙事位置落在变动章的事实会被列出。
|
||||
|
||||
## 边界
|
||||
|
||||
- 新题材类型经 S04 登记即可编辑,不需要改生产代码;不维护固定类型清单。
|
||||
- 世界实例只来自本书;资料研究结论不自动转为事实。
|
||||
- 独立作者设定不依赖正文;已确认事实的历史决定不被修订撤回。
|
||||
3
.agent/skills/操作/维护故事世界/目录.md
Normal file
3
.agent/skills/操作/维护故事世界/目录.md
Normal file
@ -0,0 +1,3 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 技能说明 | SKILL.md | 世界实体、人物关系与影响评估的维护操作 | 维护设定、修订前评估、弧线追踪时 | 先审后入;修订不撤回独立确认 |
|
||||
@ -1088,7 +1088,7 @@
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse__________________17"
|
||||
"$ref": "#/components/schemas/muse__________________18"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Response List Works"
|
||||
@ -1250,7 +1250,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/muse__________________19"
|
||||
"$ref": "#/components/schemas/muse__________________20"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1718,7 +1718,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse__________________20"
|
||||
"$ref": "#/components/schemas/muse__________________21"
|
||||
},
|
||||
"title": "Response List Plans"
|
||||
}
|
||||
@ -1969,7 +1969,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/muse__________________20"
|
||||
"$ref": "#/components/schemas/muse__________________21"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2124,7 +2124,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/muse__________________20"
|
||||
"$ref": "#/components/schemas/muse__________________21"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3889,6 +3889,332 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/world/types": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "世界类型清单",
|
||||
"operationId": "list_world_types",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Response List World Types"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/works/{work_id}/world/entities": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "列出世界实体",
|
||||
"operationId": "list_world_entities",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "work_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Work Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"title": "Response List World Entities"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "保存世界实体",
|
||||
"operationId": "save_world_entity",
|
||||
"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__________________16"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/muse____________________25"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/works/{work_id}/world/entities/{object_id}/timeline": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "读取实体轨迹",
|
||||
"operationId": "read_entity_timeline",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "work_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Work Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "object_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Object Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response Read Entity Timeline"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/works/{work_id}/world/entities/{object_id}/impact": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "评估修订影响",
|
||||
"operationId": "assess_world_revision",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "work_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Work Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "object_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Object Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response Assess World Revision"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/works/{work_id}/world/relations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "人物关系视图",
|
||||
"operationId": "list_world_relations",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "work_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Work Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response List World Relations"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/works/{work_id}/world/characters/{character_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"故事世界"
|
||||
],
|
||||
"summary": "人物弧线",
|
||||
"operationId": "read_character_arc",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "work_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Work Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "character_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Character Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response Read Character Arc"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/tasks/{task_id}/events": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -4933,6 +5259,21 @@
|
||||
"title": "叙事窗口"
|
||||
},
|
||||
"muse__________________16": {
|
||||
"properties": {
|
||||
"command_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Command Id"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"command_id"
|
||||
],
|
||||
"title": "创作命令"
|
||||
},
|
||||
"muse__________________17": {
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "string",
|
||||
@ -4977,7 +5318,7 @@
|
||||
],
|
||||
"title": "节点条目"
|
||||
},
|
||||
"muse__________________17": {
|
||||
"muse__________________18": {
|
||||
"properties": {
|
||||
"work_id": {
|
||||
"type": "string",
|
||||
@ -5001,7 +5342,7 @@
|
||||
],
|
||||
"title": "作品概要"
|
||||
},
|
||||
"muse__________________18": {
|
||||
"muse__________________19": {
|
||||
"properties": {
|
||||
"chapter_id": {
|
||||
"type": "string",
|
||||
@ -5024,7 +5365,18 @@
|
||||
],
|
||||
"title": "章节条目"
|
||||
},
|
||||
"muse__________________19": {
|
||||
"muse__________________2": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"planning",
|
||||
"generation",
|
||||
"detection",
|
||||
"extraction"
|
||||
],
|
||||
"title": "内容用途",
|
||||
"description": "内容消费用途。"
|
||||
},
|
||||
"muse__________________20": {
|
||||
"properties": {
|
||||
"work_id": {
|
||||
"type": "string",
|
||||
@ -5036,7 +5388,7 @@
|
||||
},
|
||||
"chapters": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse__________________18"
|
||||
"$ref": "#/components/schemas/muse__________________19"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Chapters"
|
||||
@ -5050,18 +5402,7 @@
|
||||
],
|
||||
"title": "目录结果"
|
||||
},
|
||||
"muse__________________2": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"planning",
|
||||
"generation",
|
||||
"detection",
|
||||
"extraction"
|
||||
],
|
||||
"title": "内容用途",
|
||||
"description": "内容消费用途。"
|
||||
},
|
||||
"muse__________________20": {
|
||||
"muse__________________21": {
|
||||
"properties": {
|
||||
"plan_id": {
|
||||
"type": "string",
|
||||
@ -6212,7 +6553,7 @@
|
||||
},
|
||||
"nodes": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse__________________16"
|
||||
"$ref": "#/components/schemas/muse__________________17"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Nodes"
|
||||
@ -6707,7 +7048,7 @@
|
||||
"before": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/muse__________________20"
|
||||
"$ref": "#/components/schemas/muse__________________21"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
||||
@ -3051,8 +3051,19 @@
|
||||
"模块设计/B02-故事世界.md",
|
||||
"数据模型/元数据与结构演进.md"
|
||||
],
|
||||
"status": "planned",
|
||||
"execution_evidence": [],
|
||||
"status": "verified",
|
||||
"execution_evidence": [
|
||||
{
|
||||
"batch": "R2-20260909",
|
||||
"date": "2026-09-11",
|
||||
"summary": "世界设定、人物弧线与全书连续性整包落地:S04 族驱动维护层(世界设定/人物关系/影响评估,新类型零代码)、计划与已发生视图分离、修订影响只读标记(独立确认不撤回);HTTP+提示词+技能+工作台三页。7 个新 NC 通过;离线 275、非宿主数据库 237、前端 27;浏览器三页+视觉判定通过。整包验证退回一项(尾空行 diff-check),三轮复验通过。",
|
||||
"evidence": [
|
||||
".agents.local/改造/R2-20260909/W19/回合开发/整包/执行证据.md",
|
||||
".agents.local/改造/R2-20260909/W19/回合验证/整包/验证报告.md",
|
||||
".agents.local/改造/R2-20260909/W19/回合验证/整包/复验报告-第三轮.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
"owned_file_paths": [
|
||||
".agent/skills/操作/维护故事世界/SKILL.md",
|
||||
".agent/skills/操作/维护故事世界/目录.md",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 前端-应用与通用界面:文件合同
|
||||
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 62 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 63 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
|
||||
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
|
||||
|
||||
@ -67,6 +67,7 @@ agent-example/
|
||||
│ ├── 单元/
|
||||
│ │ └── 中文显示与动作.test.tsx # 作者界面语义的逐例行为验证
|
||||
│ ├── 旅程/
|
||||
│ │ ├── 世界维护.spec.ts # 世界维护三页真实浏览器旅程
|
||||
│ │ ├── 任务材料.spec.ts # 任务材料真实浏览器旅程验证
|
||||
│ │ ├── 创建作品到确认正文.spec.ts # 真后端创作旅程
|
||||
│ │ ├── 动态类型到事实确认.spec.ts # 新类型全链消费
|
||||
@ -797,3 +798,12 @@ agent-example/
|
||||
- 意图与职责:真实浏览器与隔离库核对资料对照页的来源、原文、清理说明与榜单失败态。
|
||||
- 边界:真实隔离库与真实浏览器;不 mock 后端;视觉判定独立留证。
|
||||
- 目标:对照呈现与失败态可复核,页面无预期外控制台错误。
|
||||
|
||||
<a id="file-6dbf734085513505"></a>
|
||||
|
||||
### `web/tests/旅程/世界维护.spec.ts`
|
||||
|
||||
- 归属与种类:验证;前端浏览器旅程。
|
||||
- 意图与职责:真实浏览器与隔离库核对世界资料、人物关系与时间伏笔三页。
|
||||
- 边界:不 mock 后端;视觉判定独立留证。
|
||||
- 目标:世界维护旅程可复核。
|
||||
|
||||
@ -1727,3 +1727,12 @@ agent-example/
|
||||
- 意图与职责:榜单采集成功/失败二态、观察差异与未登记拒绝的隔离数据库验证。
|
||||
- 边界:合成读取器按剧本回应;不访问网络。
|
||||
- 目标:失败不伪造、快照可查、差异为观察陈述可复验。
|
||||
|
||||
<a id="file-6a24bf0e8e44c6cd"></a>
|
||||
|
||||
### `tests/集成/test_世界维护与影响.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图与职责:世界设定维护、弧线分离与修订影响的隔离数据库验证。
|
||||
- 边界:真实隔离 PG;S04 结构经登记注入。
|
||||
- 目标:维护与影响行为可复验。
|
||||
|
||||
@ -21674,6 +21674,23 @@
|
||||
"test_case_refs": [
|
||||
"NC-w17-browser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "web/tests/旅程/世界维护.spec.ts",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "世界维护三页真实浏览器旅程",
|
||||
"project_part": "前端",
|
||||
"intent": "世界维护三页真实浏览器旅程",
|
||||
"boundary": "真实隔离库与真实浏览器;不 mock 后端",
|
||||
"goal": "行为可复验",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/前端-应用与通用界面.md#file-6dbf734085513505",
|
||||
"test_case_refs": [
|
||||
"NC-w19-browser"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contract_fields": {
|
||||
|
||||
@ -1354,6 +1354,7 @@ agent-example/
|
||||
│ │ ├── 单元/
|
||||
│ │ │ └── 中文显示与动作.test.tsx # 作者界面语义的逐例行为验证
|
||||
│ │ ├── 旅程/
|
||||
│ │ │ ├── 世界维护.spec.ts # 世界维护三页真实浏览器旅程
|
||||
│ │ │ ├── 人工创作.spec.ts # 人工创作HTTP与作者编辑独立用例
|
||||
│ │ │ ├── 任务材料.spec.ts # 任务材料真实浏览器旅程验证
|
||||
│ │ │ ├── 候选审阅.spec.ts # 作者候选审阅和正式采纳浏览器旅程
|
||||
|
||||
@ -3258,3 +3258,15 @@
|
||||
- 环境:真实隔离库 + Chrome。
|
||||
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
|
||||
|
||||
|
||||
<a id="nc-browser-world-workbench"></a>
|
||||
|
||||
## NC-w19-browser · NC-browser-world-workbench:世界维护三页旅程
|
||||
|
||||
- 位置:`web/tests/旅程/世界维护.spec.ts::NC-browser-world-workbench:世界维护三页旅程`。
|
||||
- 给定:隔离浏览器环境与预置作品/实体。
|
||||
- 动作:浏览世界资料/人物关系/时间伏笔三页并展开轨迹与影响。
|
||||
- 预期:实体与轨迹可见;弧线分区呈现;影响计数可见。
|
||||
- 环境:真实隔离库 + Chrome。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
@ -123415,6 +123415,125 @@
|
||||
"environment": "真实隔离库 + Chrome",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-19a01a",
|
||||
"file": "tests/集成/test_世界维护与影响.py",
|
||||
"symbol": "test_新类型零代码经世界设定编辑__19a01a",
|
||||
"design_location": "验证设计/资料与结构用例.md#nc-w19-19a01a",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离数据库与 S04 登记结构",
|
||||
"when": "新类型零代码经世界设定编辑",
|
||||
"then": [
|
||||
"行为可复验",
|
||||
"先审后入",
|
||||
"修订不撤回独立确认"
|
||||
],
|
||||
"environment": "真实隔离 PG",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-19a02b",
|
||||
"file": "tests/集成/test_世界维护与影响.py",
|
||||
"symbol": "test_实体轨迹版本追查与旧版本保留__19a02b",
|
||||
"design_location": "验证设计/资料与结构用例.md#nc-w19-19a02b",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离数据库与 S04 登记结构",
|
||||
"when": "实体轨迹版本追查与旧版本保留",
|
||||
"then": [
|
||||
"行为可复验",
|
||||
"先审后入",
|
||||
"修订不撤回独立确认"
|
||||
],
|
||||
"environment": "真实隔离 PG",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-19a03c",
|
||||
"file": "tests/集成/test_世界维护与影响.py",
|
||||
"symbol": "test_修订影响只标记不撤回独立确认__19a03c",
|
||||
"design_location": "验证设计/资料与结构用例.md#nc-w19-19a03c",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离数据库与 S04 登记结构",
|
||||
"when": "修订影响只标记不撤回独立确认",
|
||||
"then": [
|
||||
"行为可复验",
|
||||
"先审后入",
|
||||
"修订不撤回独立确认"
|
||||
],
|
||||
"environment": "真实隔离 PG",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-19a04d",
|
||||
"file": "tests/集成/test_世界维护与影响.py",
|
||||
"symbol": "test_章序变化影响列出叙事位置事实__19a04d",
|
||||
"design_location": "验证设计/资料与结构用例.md#nc-w19-19a04d",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离数据库与 S04 登记结构",
|
||||
"when": "章序变化影响列出叙事位置事实",
|
||||
"then": [
|
||||
"行为可复验",
|
||||
"先审后入",
|
||||
"修订不撤回独立确认"
|
||||
],
|
||||
"environment": "真实隔离 PG",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-19b01e",
|
||||
"file": "tests/集成/test_世界维护与影响.py",
|
||||
"symbol": "test_人物弧线计划与已发生分开__19b01e",
|
||||
"design_location": "验证设计/资料与结构用例.md#nc-w19-19b01e",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离数据库与 S04 登记结构",
|
||||
"when": "人物弧线计划与已发生分开",
|
||||
"then": [
|
||||
"行为可复验",
|
||||
"先审后入",
|
||||
"修订不撤回独立确认"
|
||||
],
|
||||
"environment": "真实隔离 PG",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-19b02f",
|
||||
"file": "tests/集成/test_世界维护与影响.py",
|
||||
"symbol": "test_关系端点解析为本书对象__19b02f",
|
||||
"design_location": "验证设计/资料与结构用例.md#nc-w19-19b02f",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离数据库与 S04 登记结构",
|
||||
"when": "关系端点解析为本书对象",
|
||||
"then": [
|
||||
"行为可复验",
|
||||
"先审后入",
|
||||
"修订不撤回独立确认"
|
||||
],
|
||||
"environment": "真实隔离 PG",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w19-browser",
|
||||
"file": "web/tests/旅程/世界维护.spec.ts",
|
||||
"symbol": "NC-browser-world-workbench:世界维护三页旅程",
|
||||
"design_location": "验证设计/工作台与工程用例.md#nc-browser-world-workbench",
|
||||
"contract": "模块设计/B02-故事世界.md",
|
||||
"given": "隔离浏览器环境与预置作品/实体",
|
||||
"when": "浏览世界资料/人物关系/时间伏笔三页并展开轨迹与影响",
|
||||
"then": [
|
||||
"实体与轨迹可见",
|
||||
"弧线分区呈现",
|
||||
"影响计数可见"
|
||||
],
|
||||
"environment": "真实隔离库 + Chrome",
|
||||
"verification_state": "已实现未执行",
|
||||
"execution_evidence": ".agents.local/改造/R2-20260909/W19/回合开发/整包/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -3877,3 +3877,75 @@
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
|
||||
|
||||
|
||||
<a id="nc-w19-19a01a"></a>
|
||||
|
||||
## NC-w19-19a01a · test_新类型零代码经世界设定编辑__19a01a
|
||||
|
||||
- 位置:`tests/集成/test_世界维护与影响.py::test_新类型零代码经世界设定编辑__19a01a`。
|
||||
- 给定:隔离数据库与 S04 登记结构。
|
||||
- 动作:新类型零代码经世界设定编辑。
|
||||
- 预期:行为可复验;先审后入;修订不撤回独立确认。
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
<a id="nc-w19-19a02b"></a>
|
||||
|
||||
## NC-w19-19a02b · test_实体轨迹版本追查与旧版本保留__19a02b
|
||||
|
||||
- 位置:`tests/集成/test_世界维护与影响.py::test_实体轨迹版本追查与旧版本保留__19a02b`。
|
||||
- 给定:隔离数据库与 S04 登记结构。
|
||||
- 动作:实体轨迹版本追查与旧版本保留。
|
||||
- 预期:行为可复验;先审后入;修订不撤回独立确认。
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
<a id="nc-w19-19a03c"></a>
|
||||
|
||||
## NC-w19-19a03c · test_修订影响只标记不撤回独立确认__19a03c
|
||||
|
||||
- 位置:`tests/集成/test_世界维护与影响.py::test_修订影响只标记不撤回独立确认__19a03c`。
|
||||
- 给定:隔离数据库与 S04 登记结构。
|
||||
- 动作:修订影响只标记不撤回独立确认。
|
||||
- 预期:行为可复验;先审后入;修订不撤回独立确认。
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
<a id="nc-w19-19a04d"></a>
|
||||
|
||||
## NC-w19-19a04d · test_章序变化影响列出叙事位置事实__19a04d
|
||||
|
||||
- 位置:`tests/集成/test_世界维护与影响.py::test_章序变化影响列出叙事位置事实__19a04d`。
|
||||
- 给定:隔离数据库与 S04 登记结构。
|
||||
- 动作:章序变化影响列出叙事位置事实。
|
||||
- 预期:行为可复验;先审后入;修订不撤回独立确认。
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
<a id="nc-w19-19b01e"></a>
|
||||
|
||||
## NC-w19-19b01e · test_人物弧线计划与已发生分开__19b01e
|
||||
|
||||
- 位置:`tests/集成/test_世界维护与影响.py::test_人物弧线计划与已发生分开__19b01e`。
|
||||
- 给定:隔离数据库与 S04 登记结构。
|
||||
- 动作:人物弧线计划与已发生分开。
|
||||
- 预期:行为可复验;先审后入;修订不撤回独立确认。
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
<a id="nc-w19-19b02f"></a>
|
||||
|
||||
## NC-w19-19b02f · test_关系端点解析为本书对象__19b02f
|
||||
|
||||
- 位置:`tests/集成/test_世界维护与影响.py::test_关系端点解析为本书对象__19b02f`。
|
||||
- 给定:隔离数据库与 S04 登记结构。
|
||||
- 动作:关系端点解析为本书对象。
|
||||
- 预期:行为可复验;先审后入;修订不撤回独立确认。
|
||||
- 环境:真实隔离 PG。
|
||||
- 合同:[权威定义](../模块设计/B02-故事世界.md)。
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from typing import Any
|
||||
import psycopg
|
||||
from psycopg import sql
|
||||
from psycopg.pq import TransactionStatus
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from .模型 import 元数据错误, 字段限制, 定义哈希, 策略快照, 结构绑定
|
||||
@ -31,6 +32,13 @@ class 元数据存储:
|
||||
if 行 is None:
|
||||
raise 元数据错误("TYPE_UNKNOWN", "类型尚未登记")
|
||||
|
||||
def 列出类型(self) -> list[dict]:
|
||||
return (
|
||||
self.连接.cursor(row_factory=dict_row)
|
||||
.execute("SELECT type_id, definition FROM metadata.type_registry ORDER BY type_id")
|
||||
.fetchall()
|
||||
)
|
||||
|
||||
def 登记类型(self, 定义: dict[str, Any], 哈希: str) -> None:
|
||||
self.要求事务()
|
||||
self.连接.execute(
|
||||
|
||||
@ -40,6 +40,17 @@ class 元数据服务:
|
||||
self._存储.登记类型(asdict(类型), 定义哈希(asdict(类型)))
|
||||
return 类型
|
||||
|
||||
def 列出类型(self) -> list[类型定义]:
|
||||
清单 = []
|
||||
for 行 in self._存储.列出类型():
|
||||
定义 = dict(行["definition"])
|
||||
定义["实例族"] = tuple(定义["实例族"])
|
||||
定义["实例粒度"] = tuple(
|
||||
定义["实例粒度"] if isinstance(定义["实例粒度"], list) else [定义["实例粒度"]]
|
||||
)
|
||||
清单.append(类型定义(**定义))
|
||||
return 清单
|
||||
|
||||
def 读取类型(self, type_id: str) -> 类型定义:
|
||||
定义 = self._存储.读取类型(type_id)
|
||||
定义["实例族"] = tuple(定义["实例族"])
|
||||
|
||||
@ -12,7 +12,9 @@ from muse.接入.创作操作 import (
|
||||
事实审阅结果,
|
||||
事实审阅请求,
|
||||
事实提案结果,
|
||||
创作命令,
|
||||
创作身份,
|
||||
实体保存请求,
|
||||
核对路径,
|
||||
正式回执结果,
|
||||
)
|
||||
@ -118,3 +120,74 @@ def 决定事实(proposal_id: str, 请求: 事实决定请求, request: Request,
|
||||
请求.review_hash,
|
||||
tuple(请求.approved_changes),
|
||||
)
|
||||
|
||||
|
||||
# ---- W19 世界维护 ----
|
||||
|
||||
|
||||
@路由.get("/world/types", operation_id="list_world_types")
|
||||
def 世界类型清单(request: Request, 作者: 作者依赖) -> list[dict]:
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求故事世界().世界类型清单(创作身份(作者, 装配.配置))
|
||||
|
||||
|
||||
@路由.get("/works/{work_id}/world/entities", operation_id="list_world_entities")
|
||||
def 列出世界实体(work_id: str, request: Request, 作者: 作者依赖) -> list[dict]:
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求故事世界().列出世界实体(创作身份(作者, 装配.配置), work_id)
|
||||
|
||||
|
||||
@路由.post(
|
||||
"/works/{work_id}/world/entities",
|
||||
response_model=正式回执结果,
|
||||
operation_id="save_world_entity",
|
||||
status_code=201,
|
||||
)
|
||||
def 保存世界实体(work_id: str, 请求: 创作命令, request: Request, 作者: 作者依赖) -> dict:
|
||||
装配 = request.app.state.装配
|
||||
载荷 = 实体保存请求.model_validate(请求.model_dump(exclude={"command_id"}))
|
||||
return 装配.要求故事世界().保存实体提案(
|
||||
创作身份(作者, 装配.配置),
|
||||
请求.command_id,
|
||||
work_id=work_id,
|
||||
object_id=载荷.object_id,
|
||||
expected_revision=载荷.expected_revision,
|
||||
schema_id=载荷.schema_id,
|
||||
schema_version=载荷.schema_version,
|
||||
content=载荷.content,
|
||||
正文依据们=tuple(载荷.sources),
|
||||
independent=载荷.independent,
|
||||
knowledge_mode=载荷.knowledge_mode,
|
||||
effective_from=载荷.effective_from,
|
||||
知情记录们=tuple(载荷.known_to),
|
||||
计划窗口=载荷.planned_window,
|
||||
instance_family=载荷.instance_family,
|
||||
)
|
||||
|
||||
|
||||
@路由.get(
|
||||
"/works/{work_id}/world/entities/{object_id}/timeline", operation_id="read_entity_timeline"
|
||||
)
|
||||
def 读取实体轨迹(work_id: str, object_id: str, request: Request, 作者: 作者依赖) -> dict:
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求故事世界().读取实体轨迹(创作身份(作者, 装配.配置), work_id, object_id)
|
||||
|
||||
|
||||
@路由.get(
|
||||
"/works/{work_id}/world/entities/{object_id}/impact", operation_id="assess_world_revision"
|
||||
)
|
||||
def 评估修订影响(work_id: str, object_id: str, request: Request, 作者: 作者依赖) -> dict:
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求故事世界().评估修订影响(创作身份(作者, 装配.配置), work_id, object_id)
|
||||
|
||||
|
||||
@路由.get("/works/{work_id}/world/relations", operation_id="list_world_relations")
|
||||
def 人物关系视图(work_id: str, request: Request, 作者: 作者依赖) -> dict:
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求故事世界().人物关系视图(创作身份(作者, 装配.配置), work_id)
|
||||
|
||||
|
||||
@路由.get("/works/{work_id}/world/characters/{character_id}", operation_id="read_character_arc")
|
||||
def 人物弧线(work_id: str, character_id: str, request: Request, 作者: 作者依赖) -> dict:
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求故事世界().人物弧线(创作身份(作者, 装配.配置), work_id, character_id)
|
||||
|
||||
@ -476,3 +476,19 @@ class 规划表单结果(BaseModel):
|
||||
content: dict[str, Any]
|
||||
visible_schema: dict[str, Any]
|
||||
editable_schema: dict[str, Any]
|
||||
|
||||
|
||||
class 实体保存请求(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
object_id: str = Field(min_length=1)
|
||||
expected_revision: int = Field(ge=0)
|
||||
schema_id: str = Field(min_length=1)
|
||||
schema_version: int = Field(ge=1)
|
||||
content: dict
|
||||
sources: list[dict] = []
|
||||
independent: bool = True
|
||||
knowledge_mode: Literal["fact", "belief", "plan", "setting"] = "setting"
|
||||
effective_from: str | None = None
|
||||
known_to: list[dict] = []
|
||||
planned_window: dict | None = None
|
||||
instance_family: Literal["entity", "relation"] = "entity"
|
||||
|
||||
176
src/muse/故事世界/世界设定.py
Normal file
176
src/muse/故事世界/世界设定.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""世界设定:世界实体的维护薄层;类型来自 S04 生命周期族,编辑走提案→作者确认。
|
||||
|
||||
只维护本书世界实例;不把资料和公共方法直接转为事实。新类型经 S04 登记即可编辑,
|
||||
本模块不维护固定类型清单。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from muse.作品规划.接口 import 读取作品范围
|
||||
from muse.元数据.接口 import 元数据服务, 投影值
|
||||
from muse.故事世界.存储 import 故事存储
|
||||
from muse.故事世界.来源有效性 import 当前依据可用, 读取依据状态
|
||||
from muse.故事世界.模型 import 事实提案请求, 事实结构, 事实错误, 叙事窗口, 正文依据, 知情记录
|
||||
|
||||
|
||||
def 世界类型清单(连) -> list[dict[str, Any]]:
|
||||
"""S04 已登记类型的实体族视图;新题材类型不改生产代码即可出现。"""
|
||||
|
||||
清单 = []
|
||||
for 类型 in 元数据服务(连).列出类型():
|
||||
if "entity" not in 类型.实例粒度:
|
||||
continue
|
||||
清单.append(
|
||||
{
|
||||
"type_id": 类型.type_id,
|
||||
"名称": 类型.名称,
|
||||
"语义域": 类型.语义域,
|
||||
"实例族": list(类型.实例族),
|
||||
"说明": 类型.说明,
|
||||
}
|
||||
)
|
||||
return 清单
|
||||
|
||||
|
||||
def _投影(连, 作者: str, work_id: str, 行: dict[str, Any]) -> dict[str, Any]:
|
||||
"""按对象结构绑定解释内容投影;历史内容按当时绑定解释,不套用当前绑定。"""
|
||||
|
||||
from muse.故事世界.设定修订 import 读取事实结构
|
||||
|
||||
绑定 = 行["schema_binding"]
|
||||
结构, _, 投影合同 = 读取事实结构(
|
||||
连,
|
||||
作者,
|
||||
work_id,
|
||||
事实结构(
|
||||
绑定["schema_id"],
|
||||
int(绑定["base_version"]),
|
||||
绑定["effective_schema_hash"],
|
||||
"",
|
||||
绑定.get("extension_version"),
|
||||
),
|
||||
行["instance_family"],
|
||||
)
|
||||
return 投影值(投影合同, 行["content"])
|
||||
|
||||
|
||||
def 列出实体(连, 作者: str, work_id: str) -> list[dict[str, Any]]:
|
||||
"""本书世界实体当前版全览:类型分组信息、依据可用性与计划/已发生标记。"""
|
||||
|
||||
读取作品范围(连, 作者, work_id)
|
||||
实体 = []
|
||||
for 行 in 故事存储(连).列出对象(work_id, "entity"):
|
||||
实体.append(
|
||||
{
|
||||
"object_id": 行["object_id"],
|
||||
"type_id": 行["type_id"],
|
||||
"revision": int(行["revision"]),
|
||||
"knowledge_mode": 行["knowledge_mode"],
|
||||
"independent": bool(行["independent"]),
|
||||
"依据可用": 当前依据可用(连, 作者, 行),
|
||||
"生效位置": 行["effective_from"],
|
||||
"计划窗口": 行["planned_window"],
|
||||
"content": _投影(连, 作者, work_id, 行),
|
||||
}
|
||||
)
|
||||
return 实体
|
||||
|
||||
|
||||
def 读取实体轨迹(连, 作者: str, work_id: str, object_id: str) -> dict[str, Any]:
|
||||
"""对象全部历史版本:来源、版本与结构绑定可追查;不改写任何历史决定。"""
|
||||
|
||||
目录 = 读取作品范围(连, 作者, work_id)
|
||||
存储 = 故事存储(连)
|
||||
行 = 存储.读取对象(object_id)
|
||||
if 行 is None or 行["work_id"] != work_id:
|
||||
raise 事实错误("FACT_REFERENCE_INVALID", "世界对象不存在或不属于本书")
|
||||
版本 = []
|
||||
for 历史行 in 存储.读取历史(work_id, 存储.当前系统版本(work_id), object_id):
|
||||
依据 = [
|
||||
读取依据状态(连, 作者, work_id, TypeAdapter(正文依据).validate_python(原))
|
||||
for 原 in (历史行["sources"] or [])
|
||||
]
|
||||
版本.append(
|
||||
{
|
||||
"revision": int(历史行["revision"]),
|
||||
"system_revision": int(历史行["system_revision"]),
|
||||
"knowledge_mode": 历史行["knowledge_mode"],
|
||||
"independent": bool(历史行["independent"]),
|
||||
"生效位置": 历史行["effective_from"],
|
||||
"计划窗口": 历史行["planned_window"],
|
||||
"content_hash": 历史行["content_hash"],
|
||||
"content": _投影(连, 作者, work_id, 历史行),
|
||||
"依据": [
|
||||
{"document_id": b["document_id"], "revision": b["current_revision"]}
|
||||
for b in 依据
|
||||
],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"object_id": object_id,
|
||||
"type_id": 行["type_id"],
|
||||
"instance_family": 行["instance_family"],
|
||||
"current_revision": int(行["current_revision"]),
|
||||
"目录版本": 目录["revision"],
|
||||
"版本": 版本,
|
||||
}
|
||||
|
||||
|
||||
def 保存实体提案(
|
||||
服务,
|
||||
身份,
|
||||
命令ID: str,
|
||||
*,
|
||||
work_id: str,
|
||||
object_id: str,
|
||||
expected_revision: int,
|
||||
schema_id: str,
|
||||
schema_version: int,
|
||||
content: dict[str, Any],
|
||||
正文依据们: tuple[dict[str, Any], ...] = (),
|
||||
independent: bool = True,
|
||||
knowledge_mode: str = "setting",
|
||||
effective_from: str | None = None,
|
||||
知情记录们: tuple[dict[str, Any], ...] = (),
|
||||
计划窗口: dict[str, Any] | None = None,
|
||||
instance_family: str = "entity",
|
||||
) -> dict:
|
||||
"""save_entity:作者编辑世界实体 → 结构化提案(S01 管线),确认后才成为正式事实。
|
||||
|
||||
独立作者设定不依赖正文;携带正文依据的编辑在确认时核对固定版本与引文。
|
||||
"""
|
||||
|
||||
结构 = 服务.新事实结构(
|
||||
身份, work_id, object_id, schema_id, schema_version, 家族=instance_family
|
||||
)
|
||||
请求 = 事实提案请求(
|
||||
work_id=work_id,
|
||||
object_id=object_id,
|
||||
expected_revision=expected_revision,
|
||||
schema=结构,
|
||||
content=content,
|
||||
sources=tuple(TypeAdapter(正文依据).validate_python(dict(依据)) for 依据 in 正文依据们),
|
||||
effective_from=effective_from,
|
||||
instance_family=instance_family, # type: ignore[arg-type]
|
||||
independent=independent,
|
||||
knowledge_mode=knowledge_mode, # type: ignore[arg-type]
|
||||
known_to=tuple(TypeAdapter(知情记录).validate_python(dict(项)) for 项 in 知情记录们),
|
||||
reader_visible=True,
|
||||
planned_window=(
|
||||
叙事窗口(**计划窗口) if 计划窗口 is not None else None # type: ignore[arg-type]
|
||||
),
|
||||
)
|
||||
return 服务.提出事实(身份, 命令ID, 请求)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"世界类型清单",
|
||||
"列出实体",
|
||||
"读取实体轨迹",
|
||||
"保存实体提案",
|
||||
"事实错误",
|
||||
]
|
||||
128
src/muse/故事世界/人物关系.py
Normal file
128
src/muse/故事世界/人物关系.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""人物关系:人物、关系、弧线、视角与说话特征的维护视图。
|
||||
|
||||
人物属于故事世界;关系端点必须用已解析的本书对象身份(S04 引用规则驱动);
|
||||
人物计划与已发生状态分开,不混入同一轨迹。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from muse.作品规划.接口 import 读取作品范围
|
||||
from muse.元数据.接口 import 校验值
|
||||
from muse.故事世界.存储 import 故事存储
|
||||
from muse.故事世界.模型 import 事实错误
|
||||
|
||||
计划类 = ("plan", "belief")
|
||||
已发生类 = ("fact", "setting")
|
||||
|
||||
|
||||
def 列出人物(连, 作者: str, work_id: str) -> list[dict[str, Any]]:
|
||||
"""本书全部实体对象按类型分组;人物判定交给页面与结构,不在此硬编码类型清单。"""
|
||||
|
||||
from muse.故事世界.世界设定 import 列出实体
|
||||
|
||||
实体 = 列出实体(连, 作者, work_id)
|
||||
按类型: dict[str, list[dict[str, Any]]] = {}
|
||||
for 项 in 实体:
|
||||
按类型.setdefault(项["type_id"], []).append(项)
|
||||
return [{"type_id": 类型, "数量": len(组), "对象": 组} for 类型, 组 in sorted(按类型.items())]
|
||||
|
||||
|
||||
def _收集引用(连, 作者: str, work_id: str, 行: dict[str, Any]) -> set[str]:
|
||||
"""按该对象结构绑定的 S04 引用规则,从内容收集引用的对象身份。"""
|
||||
|
||||
from muse.故事世界.模型 import 事实结构
|
||||
from muse.故事世界.设定修订 import 读取事实结构
|
||||
|
||||
绑定 = 行["schema_binding"]
|
||||
结构, _, _ = 读取事实结构(
|
||||
连,
|
||||
作者,
|
||||
work_id,
|
||||
事实结构(
|
||||
绑定["schema_id"],
|
||||
int(绑定["base_version"]),
|
||||
绑定["effective_schema_hash"],
|
||||
"",
|
||||
绑定.get("extension_version"),
|
||||
),
|
||||
行["instance_family"],
|
||||
)
|
||||
引用对象集: set[str] = set()
|
||||
try:
|
||||
结果 = 校验值(结构, 行["content"] or {})
|
||||
except Exception:
|
||||
return 引用对象集
|
||||
for 需求 in 结果.引用需求:
|
||||
值 = 需求.值
|
||||
身份 = 值.get("id") if isinstance(值, dict) else 值
|
||||
if isinstance(身份, str):
|
||||
引用对象集.add(身份)
|
||||
return 引用对象集
|
||||
|
||||
|
||||
def 列出关系(连, 作者: str, work_id: str) -> list[dict[str, Any]]:
|
||||
"""关系族对象与其端点解析:端点必须是已确认的本书对象,类型按引用约束核对。"""
|
||||
|
||||
读取作品范围(连, 作者, work_id)
|
||||
存储 = 故事存储(连)
|
||||
关系 = []
|
||||
for 行 in 存储.列出对象(work_id, "relation"):
|
||||
端点集 = _收集引用(连, 作者, work_id, 行)
|
||||
端点 = []
|
||||
for 身份 in sorted(端点集):
|
||||
对象 = 存储.读取对象(身份)
|
||||
if 对象 is None or 对象["work_id"] != work_id:
|
||||
raise 事实错误("FACT_REFERENCE_INVALID", f"关系端点无效:{身份}")
|
||||
端点.append(
|
||||
{
|
||||
"object_id": 身份,
|
||||
"type_id": 对象["type_id"],
|
||||
"revision": int(对象["current_revision"]),
|
||||
}
|
||||
)
|
||||
关系.append(
|
||||
{
|
||||
"object_id": 行["object_id"],
|
||||
"type_id": 行["type_id"],
|
||||
"revision": int(行["revision"]),
|
||||
"knowledge_mode": 行["knowledge_mode"],
|
||||
"content": 行["content"],
|
||||
"端点": 端点,
|
||||
}
|
||||
)
|
||||
return 关系
|
||||
|
||||
|
||||
def 人物弧线(连, 作者: str, work_id: str, character_id: str) -> dict[str, Any]:
|
||||
"""人物轨迹按计划与已发生分开;猜测与规划不混入已发生事实。"""
|
||||
|
||||
from muse.故事世界.世界设定 import 读取实体轨迹
|
||||
|
||||
轨迹 = 读取实体轨迹(连, 作者, work_id, character_id)
|
||||
计划 = [v for v in 轨迹["版本"] if v["knowledge_mode"] in 计划类]
|
||||
已发生 = [v for v in 轨迹["版本"] if v["knowledge_mode"] in 已发生类]
|
||||
return {
|
||||
"object_id": character_id,
|
||||
"type_id": 轨迹["type_id"],
|
||||
"current_revision": 轨迹["current_revision"],
|
||||
"计划": 计划,
|
||||
"已发生": 已发生,
|
||||
"说明": "计划与猜测不混入已发生事实;视角与说话特征随版本内容呈现。",
|
||||
}
|
||||
|
||||
|
||||
def 人物关系视图(连, 作者: str, work_id: str) -> dict[str, Any]:
|
||||
"""人物关系页一屏数据:人物分组、关系清单(端点已解析)。"""
|
||||
|
||||
return {"人物分组": 列出人物(连, 作者, work_id), "关系": 列出关系(连, 作者, work_id)}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"列出人物",
|
||||
"列出关系",
|
||||
"人物弧线",
|
||||
"人物关系视图",
|
||||
"事实错误",
|
||||
]
|
||||
@ -28,6 +28,22 @@ class 故事存储:
|
||||
.fetchone()
|
||||
)
|
||||
|
||||
def 列出对象(self, work_id: str, family: str | None = None) -> list[dict]:
|
||||
return (
|
||||
self.连.cursor(row_factory=dict_row)
|
||||
.execute(
|
||||
"SELECT o.*,v.revision,v.system_revision,v.content,v.content_hash,"
|
||||
"v.schema_binding,v.sources,v.independent,v.effective_from,v.knowledge_mode,"
|
||||
"v.reader_visible,v.planned_window "
|
||||
"FROM muse_world_object o JOIN muse_world_version v ON v.object_id=o.object_id "
|
||||
"AND v.revision=o.current_revision WHERE o.work_id=%s "
|
||||
"AND (%s::text IS NULL OR o.instance_family=%s) "
|
||||
"ORDER BY o.type_id,o.object_id",
|
||||
(work_id, family, family),
|
||||
)
|
||||
.fetchall()
|
||||
)
|
||||
|
||||
def 读取提案(self, proposal_id: str):
|
||||
return (
|
||||
self.连.cursor(row_factory=dict_row)
|
||||
@ -55,6 +71,24 @@ class 故事存储:
|
||||
)
|
||||
return {"proposal_id": proposal_id, "revision": 1, "proposal_hash": 哈希}
|
||||
|
||||
def 列出待决提案(self, work_id: str, object_id: str | None = None) -> list[dict]:
|
||||
"""未获作者决定的当前提案;决定由 S01 表判定,非空决定即视为已处置。"""
|
||||
return (
|
||||
self.连.cursor(row_factory=dict_row)
|
||||
.execute(
|
||||
"SELECT p.proposal_id,p.work_id,p.object_id,p.current_revision,"
|
||||
"v.request,v.proposed_content "
|
||||
"FROM muse_fact_proposal p JOIN muse_fact_proposal_version v "
|
||||
"ON v.proposal_id=p.proposal_id AND v.revision=p.current_revision "
|
||||
"LEFT JOIN muse_candidate_decision d ON d.candidate_id=p.proposal_id::text "
|
||||
"AND d.candidate_revision=p.current_revision "
|
||||
"WHERE p.work_id=%s AND (%s::text IS NULL OR p.object_id=%s) "
|
||||
"AND d.decision IS NULL ORDER BY p.proposal_id",
|
||||
(work_id, object_id, object_id),
|
||||
)
|
||||
.fetchall()
|
||||
)
|
||||
|
||||
def 锁对象(self, object_id: str) -> int:
|
||||
key = int.from_bytes(
|
||||
hashlib.sha256(("B02:object:" + object_id).encode()).digest()[:8], signed=True
|
||||
|
||||
@ -175,6 +175,107 @@ class 故事服务:
|
||||
),
|
||||
)
|
||||
|
||||
def 列出世界实体(self, 身份, work_id: str) -> list[dict]:
|
||||
"""世界实体当前版全览;类型来自 S04 生命周期族,计划与已发生分开标记。"""
|
||||
self._核对作者(身份)
|
||||
from muse.故事世界.世界设定 import 列出实体
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
return 列出实体(连, 身份.作者, work_id)
|
||||
|
||||
def 世界类型清单(self, 身份) -> list[dict]:
|
||||
"""S04 已登记实体类型清单;新类型不改生产代码即可编辑。"""
|
||||
self._核对作者(身份)
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
return [
|
||||
类型
|
||||
for 类型 in __import__(
|
||||
"muse.故事世界.世界设定", fromlist=["世界类型清单"]
|
||||
).世界类型清单(连)
|
||||
]
|
||||
|
||||
def 读取实体轨迹(self, 身份, work_id: str, object_id: str) -> dict:
|
||||
"""对象全部历史版本;来源、版本与结构绑定可追查。"""
|
||||
self._核对作者(身份)
|
||||
from muse.故事世界.世界设定 import 读取实体轨迹
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
return 读取实体轨迹(连, 身份.作者, work_id, object_id)
|
||||
|
||||
def 保存实体提案(
|
||||
self,
|
||||
身份,
|
||||
命令ID: str,
|
||||
*,
|
||||
work_id: str,
|
||||
object_id: str,
|
||||
expected_revision: int,
|
||||
schema_id: str,
|
||||
schema_version: int,
|
||||
content: dict,
|
||||
正文依据们: tuple = (),
|
||||
independent: bool = True,
|
||||
knowledge_mode: str = "setting",
|
||||
effective_from: str | None = None,
|
||||
知情记录们: tuple = (),
|
||||
计划窗口: dict | None = None,
|
||||
instance_family: str = "entity",
|
||||
) -> dict:
|
||||
"""save_entity:作者编辑世界实体为结构化提案,确认后成为正式事实。"""
|
||||
self._核对作者(身份)
|
||||
from muse.故事世界.世界设定 import 保存实体提案
|
||||
|
||||
return 保存实体提案(
|
||||
self,
|
||||
身份,
|
||||
命令ID,
|
||||
work_id=work_id,
|
||||
object_id=object_id,
|
||||
expected_revision=expected_revision,
|
||||
schema_id=schema_id,
|
||||
schema_version=schema_version,
|
||||
content=content,
|
||||
正文依据们=正文依据们,
|
||||
independent=independent,
|
||||
knowledge_mode=knowledge_mode,
|
||||
effective_from=effective_from,
|
||||
知情记录们=知情记录们,
|
||||
计划窗口=计划窗口,
|
||||
instance_family=instance_family,
|
||||
)
|
||||
|
||||
def 人物关系视图(self, 身份, work_id: str) -> dict:
|
||||
"""人物分组与端点已解析的关系清单。"""
|
||||
self._核对作者(身份)
|
||||
from muse.故事世界.人物关系 import 人物关系视图
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
return 人物关系视图(连, 身份.作者, work_id)
|
||||
|
||||
def 人物弧线(self, 身份, work_id: str, character_id: str) -> dict:
|
||||
"""人物轨迹按计划与已发生分开;猜测与规划不混入已发生事实。"""
|
||||
self._核对作者(身份)
|
||||
from muse.故事世界.人物关系 import 人物弧线
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
return 人物弧线(连, 身份.作者, work_id, character_id)
|
||||
|
||||
def 评估修订影响(
|
||||
self,
|
||||
身份,
|
||||
work_id: str,
|
||||
object_id: str,
|
||||
*,
|
||||
新章序: dict[str, int] | None = None,
|
||||
) -> dict:
|
||||
"""修订设定或章序变化后的影响面清单;只读评估,不撤回独立确认。"""
|
||||
self._核对作者(身份)
|
||||
from muse.故事世界.设定修订 import 评估修订影响
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
return 评估修订影响(连, 身份.作者, work_id, object_id, 新章序=新章序)
|
||||
|
||||
def 查询世界(
|
||||
self,
|
||||
身份,
|
||||
|
||||
18
src/muse/故事世界/提示词/抽取章节事实.md
Normal file
18
src/muse/故事世界/提示词/抽取章节事实.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 抽取章节事实 · 任务提示模板
|
||||
|
||||
本模板为故事世界的「抽取章节事实」任务提供可版本化表达;角色身份、字段结构、权限与预算从各自权威装配,本模板不重复。章后派生(W16)按本模板语义组装系统提示。
|
||||
|
||||
## 任务
|
||||
|
||||
对刚定稿的一章做事实抽取:
|
||||
|
||||
1. **本章摘要**:续写所需的一句话到一段话摘要。
|
||||
2. **候选事实**:人物、地点、物品、组织、关系、事件与状态变化;每条附正文原句引文。
|
||||
3. **字段结构**:按任务输出合同(type/object_id/quote/content);content 字段以本书绑定的 S04 有效结构为准。
|
||||
4. **知情位置**:如某角色在本章首次得知某事实,用 known_to 的 (actor_id, from_chapter_id) 表达,不按事件时间推定。
|
||||
|
||||
## 合同
|
||||
|
||||
- 只依据本章正文;不新增正文没有的设定;引文必须是正文字面。
|
||||
- 抽取结果一律是待作者确认的提案(Shadow);机械挽回只保留实际出现的名称为「提及」候选。
|
||||
- 角色猜测、未来规划用对应知识模式标记,不混入已发生事实。
|
||||
@ -320,3 +320,59 @@ class 世界提案依赖:
|
||||
|
||||
def 锁定当前(self, 连, 对象ID):
|
||||
return str(故事存储(连).锁提案(对象ID))
|
||||
|
||||
|
||||
def 评估修订影响(
|
||||
连,
|
||||
作者: str,
|
||||
work_id: str,
|
||||
object_id: str,
|
||||
*,
|
||||
新章序: dict[str, int] | None = None,
|
||||
) -> dict:
|
||||
"""修订设定或章序变化后需要作者复核的影响面清单。
|
||||
|
||||
只读评估:列出引用该对象的其他已确认对象、该对象的未决提案,以及章序变化
|
||||
波及的叙事位置事实;不撤回任何独立确认,不改写历史版本。规划与正文候选的
|
||||
过期读取由 S01 提交时的当前指针锁强制复核(SOURCE_STALE),不依赖后台标记。
|
||||
"""
|
||||
|
||||
目录 = 读取作品范围(连, 作者, work_id)
|
||||
存储 = 故事存储(连)
|
||||
目标 = 存储.读取对象(object_id)
|
||||
if 目标 is None or 目标["work_id"] != work_id:
|
||||
raise 事实错误("FACT_REFERENCE_INVALID", "世界对象不存在或不属于本书")
|
||||
from muse.故事世界.人物关系 import _收集引用
|
||||
|
||||
受影响事实: set[str] = set()
|
||||
for 行 in 存储.读取历史(work_id, 存储.当前系统版本(work_id)):
|
||||
if 行["object_id"] == object_id:
|
||||
continue
|
||||
if object_id in _收集引用(连, 作者, work_id, 行):
|
||||
受影响事实.add(str(行["object_id"]))
|
||||
待决 = [
|
||||
{"proposal_id": str(行["proposal_id"]), "object_id": str(行["object_id"])}
|
||||
for 行 in 存储.列出待决提案(work_id)
|
||||
]
|
||||
本对象待决 = [项 for 项 in 待决 if 项["object_id"] == object_id]
|
||||
章序影响: list[dict] = []
|
||||
if 新章序 is not None:
|
||||
当前章序 = {c["chapter_id"]: c["position"] for c in 目录["chapters"]}
|
||||
变动章 = {章 for 章 in set(当前章序) | set(新章序) if 当前章序.get(章) != 新章序.get(章)}
|
||||
for 行 in 存储.读取历史(work_id, 存储.当前系统版本(work_id)):
|
||||
位置集 = {行["effective_from"], (行["planned_window"] or {}).get("from_chapter_id")}
|
||||
位置集 |= {源.get("chapter_id") for 源 in (行["sources"] or [])}
|
||||
涉及 = {章 for 章 in 位置集 if 章 in 变动章}
|
||||
if 涉及:
|
||||
章序影响.append({"object_id": str(行["object_id"]), "涉及章": sorted(涉及)})
|
||||
return {
|
||||
"object_id": object_id,
|
||||
"current_revision": int(目标["current_revision"]),
|
||||
"受影响事实": sorted(受影响事实),
|
||||
"本对象待决提案": 本对象待决,
|
||||
"章序影响": sorted(章序影响, key=lambda 项: 项["object_id"]),
|
||||
"复核说明": (
|
||||
"清单只标记需要复核;独立确认与历史版本不被撤回。"
|
||||
"规划与正文候选过期读取由提交指针锁强制。"
|
||||
),
|
||||
}
|
||||
|
||||
295
tests/集成/test_世界维护与影响.py
Normal file
295
tests/集成/test_世界维护与影响.py
Normal file
@ -0,0 +1,295 @@
|
||||
"""世界设定维护、人物弧线分离与修订影响:S04 族驱动、计划/已发生分开、影响只标记不撤回。"""
|
||||
|
||||
import pytest
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
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 应用配置
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 世界环境(应用测试库):
|
||||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||||
导入内置结构(元数据服务(连))
|
||||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "synthetic"))
|
||||
作者 = 调用身份("author", None, 用途.生产, 内容用途.规划)
|
||||
作品 = 装配.要求作品()
|
||||
结构 = 作品.新档案结构(作者, "story-work", "work_core", 1)
|
||||
作品.保存档案(作者, "create-story", 档案保存("story-work", 0, {"名称": "合成故事世界"}, 结构))
|
||||
for i in range(1, 4):
|
||||
作品.添加章节(
|
||||
作者, f"chapter-{i}", "story-work", f"chapter-{i}", f"第{i}幕", 预期目录版本=i - 1
|
||||
)
|
||||
正文 = 装配.要求正文()
|
||||
for i, text in [
|
||||
(1, "林深停在码头,何岚站在船旁。"),
|
||||
(2, "何岚知道林深已经登船。"),
|
||||
(3, "林深来到北岸。"),
|
||||
]:
|
||||
正文.保存人工(
|
||||
作者, f"body-{i}", f"chapter-{i}", 0, 正文草稿((段落(f"p{i}", (文本节点(text),)),))
|
||||
)
|
||||
return 装配, 作者, 应用测试库
|
||||
|
||||
|
||||
def 来源(装配, 作者, chapter, quote):
|
||||
记录 = 装配.要求正文().读取正文(作者, chapter)
|
||||
原文 = TypeAdapter(正文草稿).validate_python(记录["document"])
|
||||
return 正文依据(
|
||||
chapter, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(原文, quote),)
|
||||
)
|
||||
|
||||
|
||||
def 确认提案(装配, 作者, 回执, object_id, expected=0, command="decide"):
|
||||
世界 = 装配.要求故事世界()
|
||||
proposal_id = 回执["results"][0]["proposal_id"]
|
||||
记录 = 世界.读取提案(作者, proposal_id)
|
||||
定位 = 事实定位(
|
||||
proposal_id, 记录["revision"], 记录["proposal_hash"], "world:" + object_id, expected
|
||||
)
|
||||
审阅 = 世界.打开事实审阅(作者, 定位)
|
||||
return 世界.决定事实(
|
||||
作者,
|
||||
f"{command}-{proposal_id[:8]}",
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("object",),
|
||||
)
|
||||
|
||||
|
||||
def test_新类型零代码经世界设定编辑__19a01a(世界环境):
|
||||
from muse.元数据.接口 import 值规则, 字段定义, 类型定义, 结构定义
|
||||
|
||||
装配, 作者, 库 = 世界环境
|
||||
with 库[用途.维护].连接() as 连, 连.transaction():
|
||||
元数据 = 元数据服务(连)
|
||||
元数据.登记类型(类型定义("cost_rule", "代价规则", "world", "entity", ("entity",)))
|
||||
结构 = 结构定义(
|
||||
"cost_rule",
|
||||
"cost_rule",
|
||||
1,
|
||||
(
|
||||
字段定义("cost.name", "名称", "名称", 值规则("text"), 必填=True),
|
||||
字段定义("cost.price", "代价", "代价", 值规则("text")),
|
||||
),
|
||||
)
|
||||
元数据.登记结构候选(结构)
|
||||
元数据.发布结构("cost_rule", 1, 结构.内容哈希)
|
||||
世界 = 装配.要求故事世界()
|
||||
# 世界设定 save_entity:S04 新类型不改生产代码即可提案→确认→列出。
|
||||
回执 = 世界.保存实体提案(
|
||||
作者,
|
||||
"save-cost-1",
|
||||
work_id="story-work",
|
||||
object_id="cost-sea",
|
||||
expected_revision=0,
|
||||
schema_id="cost_rule",
|
||||
schema_version=1,
|
||||
content={"名称": "渡海代价", "代价": "一年记忆"},
|
||||
)
|
||||
确认提案(装配, 作者, 回执, "cost-sea")
|
||||
with 装配.数据库.连接(只读=True) as 连:
|
||||
from muse.故事世界.世界设定 import 世界类型清单, 列出实体
|
||||
|
||||
实体 = [e for e in 列出实体(连, "author", "story-work") if e["object_id"] == "cost-sea"]
|
||||
assert len(实体) == 1 and 实体[0]["type_id"] == "cost_rule"
|
||||
assert 实体[0]["knowledge_mode"] == "setting" and 实体[0]["independent"]
|
||||
assert any(t["type_id"] == "cost_rule" for t in 世界类型清单(连))
|
||||
|
||||
|
||||
def test_实体轨迹版本追查与旧版本保留__19a02b(世界环境):
|
||||
装配, 作者, _ = 世界环境
|
||||
世界 = 装配.要求故事世界()
|
||||
r1 = 世界.保存实体提案(
|
||||
作者,
|
||||
"save-char-1",
|
||||
work_id="story-work",
|
||||
object_id="char-lin",
|
||||
expected_revision=0,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "主角"},
|
||||
)
|
||||
确认提案(装配, 作者, r1, "char-lin")
|
||||
r2 = 世界.保存实体提案(
|
||||
作者,
|
||||
"save-char-2",
|
||||
work_id="story-work",
|
||||
object_id="char-lin",
|
||||
expected_revision=1,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "重要配角"},
|
||||
)
|
||||
确认提案(装配, 作者, r2, "char-lin", expected=1)
|
||||
轨迹 = 世界.读取实体轨迹(作者, "story-work", "char-lin")
|
||||
assert [v["revision"] for v in 轨迹["版本"]] == [2, 1]
|
||||
assert 轨迹["版本"][1]["content"]["身份定位"] == "主角" # 旧版本不抹
|
||||
|
||||
|
||||
def test_修订影响只标记不撤回独立确认__19a03c(世界环境):
|
||||
装配, 作者, _ = 世界环境
|
||||
世界 = 装配.要求故事世界()
|
||||
# 独立设定:何岚的人物卡(不依赖正文)。
|
||||
r = 世界.保存实体提案(
|
||||
作者,
|
||||
"save-he-1",
|
||||
work_id="story-work",
|
||||
object_id="char-he",
|
||||
expected_revision=0,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "重要配角"},
|
||||
)
|
||||
确认提案(装配, 作者, r, "char-he")
|
||||
# 林深先确认 r1,再做未决的 r2 修订提案。
|
||||
r1 = 世界.保存实体提案(
|
||||
作者,
|
||||
"save-lin-1",
|
||||
work_id="story-work",
|
||||
object_id="char-lin",
|
||||
expected_revision=0,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "主角"},
|
||||
)
|
||||
确认提案(装配, 作者, r1, "char-lin")
|
||||
世界.保存实体提案(
|
||||
作者,
|
||||
"save-lin-2",
|
||||
work_id="story-work",
|
||||
object_id="char-lin",
|
||||
expected_revision=1,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "重要配角"},
|
||||
)
|
||||
# 何岚无未决修订;林深的评估列出自己的待决 r2。
|
||||
影响 = 世界.评估修订影响(作者, "story-work", "char-he")
|
||||
assert 影响["object_id"] == "char-he" and 影响["本对象待决提案"] == []
|
||||
影响2 = 世界.评估修订影响(作者, "story-work", "char-lin")
|
||||
assert any(p["object_id"] == "char-lin" for p in 影响2["本对象待决提案"])
|
||||
# 独立确认不受影响标记撤回:何岚仍可用。
|
||||
with 装配.数据库.连接(只读=True) as 连:
|
||||
from muse.故事世界.世界设定 import 列出实体
|
||||
|
||||
实体 = {e["object_id"]: e for e in 列出实体(连, "author", "story-work")}
|
||||
assert 实体["char-he"]["依据可用"]
|
||||
|
||||
|
||||
def test_章序变化影响列出叙事位置事实__19a04d(世界环境):
|
||||
装配, 作者, _ = 世界环境
|
||||
世界 = 装配.要求故事世界()
|
||||
依据 = (来源(装配, 作者, "chapter-2", "何岚知道林深已经登船"),)
|
||||
结构 = 世界.新事实结构(作者, "story-work", "char-lin", "character", 1)
|
||||
请求 = 事实提案请求(
|
||||
"story-work",
|
||||
"char-lin",
|
||||
0,
|
||||
结构,
|
||||
{"身份定位": "主角"},
|
||||
依据,
|
||||
"chapter-2",
|
||||
planned_window=叙事窗口("chapter-2", "chapter-3"),
|
||||
)
|
||||
回执 = 世界.提出事实(作者, "propose-lin", 请求)
|
||||
确认提案(装配, 作者, 回执, "char-lin")
|
||||
新章序 = {"chapter-1": 1, "chapter-2": 3, "chapter-3": 2}
|
||||
with 装配.数据库.连接(只读=True) as 连:
|
||||
from muse.故事世界.设定修订 import 评估修订影响
|
||||
|
||||
影响 = 评估修订影响(连, "author", "story-work", "char-lin", 新章序=新章序)
|
||||
涉及对象 = {项["object_id"]: 项["涉及章"] for 项 in 影响["章序影响"]}
|
||||
assert "char-lin" in 涉及对象 and "chapter-2" in 涉及对象["char-lin"]
|
||||
|
||||
|
||||
def test_人物弧线计划与已发生分开__19b01e(世界环境):
|
||||
装配, 作者, _ = 世界环境
|
||||
世界 = 装配.要求故事世界()
|
||||
r1 = 世界.保存实体提案(
|
||||
作者,
|
||||
"arc-plan-1",
|
||||
work_id="story-work",
|
||||
object_id="char-gu",
|
||||
expected_revision=0,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "反派"},
|
||||
knowledge_mode="plan",
|
||||
)
|
||||
确认提案(装配, 作者, r1, "char-gu")
|
||||
r2 = 世界.保存实体提案(
|
||||
作者,
|
||||
"arc-fact-1",
|
||||
work_id="story-work",
|
||||
object_id="char-gu",
|
||||
expected_revision=1,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "反派"},
|
||||
knowledge_mode="fact",
|
||||
)
|
||||
确认提案(装配, 作者, r2, "char-gu", expected=1)
|
||||
弧线 = 世界.人物弧线(作者, "story-work", "char-gu")
|
||||
assert [v["knowledge_mode"] for v in 弧线["计划"]] == ["plan"]
|
||||
assert [v["knowledge_mode"] for v in 弧线["已发生"]] == ["fact"]
|
||||
|
||||
|
||||
def test_关系端点解析为本书对象__19b02f(世界环境):
|
||||
装配, 作者, _ = 世界环境
|
||||
世界 = 装配.要求故事世界()
|
||||
for 对象 in ("char-lin", "char-he"):
|
||||
r = 世界.保存实体提案(
|
||||
作者,
|
||||
f"rel-{对象}",
|
||||
work_id="story-work",
|
||||
object_id=对象,
|
||||
expected_revision=0,
|
||||
schema_id="character",
|
||||
schema_version=1,
|
||||
content={"身份定位": "主角"},
|
||||
)
|
||||
确认提案(装配, 作者, r, 对象)
|
||||
依据 = (来源(装配, 作者, "chapter-1", "林深停在码头,何岚站在船旁"),)
|
||||
结构 = 世界.新事实结构(作者, "story-work", "rel-1", "character_relation", 1, 家族="relation")
|
||||
请求 = 事实提案请求(
|
||||
"story-work",
|
||||
"rel-1",
|
||||
0,
|
||||
结构,
|
||||
{"甲方": "char-lin", "乙方": "char-he", "关系类型": "同船"},
|
||||
依据,
|
||||
"chapter-1",
|
||||
instance_family="relation",
|
||||
)
|
||||
回执 = 世界.提出事实(作者, "propose-rel", 请求)
|
||||
确认提案(装配, 作者, 回执, "rel-1")
|
||||
视图 = 世界.人物关系视图(作者, "story-work")
|
||||
关系 = [r for r in 视图["关系"] if r["object_id"] == "rel-1"]
|
||||
assert len(关系) == 1
|
||||
端点集 = {端["object_id"] for 端 in 关系[0]["端点"]}
|
||||
assert 端点集 == {"char-lin", "char-he"}
|
||||
# 端点必须是已解析的本书对象:不存在的端点在提案校验即被拒。
|
||||
坏请求 = 事实提案请求(
|
||||
"story-work",
|
||||
"rel-2",
|
||||
0,
|
||||
结构,
|
||||
{"甲方": "char-none", "乙方": "char-he", "关系类型": "无据"},
|
||||
依据,
|
||||
"chapter-1",
|
||||
instance_family="relation",
|
||||
)
|
||||
with pytest.raises(Muse错误):
|
||||
世界.提出事实(作者, "propose-bad-rel", 坏请求)
|
||||
5
web/src/功能/故事世界/index.ts
Normal file
5
web/src/功能/故事世界/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/** 故事世界页面出口:世界资料、人物关系与时间伏笔。 */
|
||||
export { Ui世界资料页 } from "./世界资料页";
|
||||
export { Ui人物关系页 } from "./人物关系页";
|
||||
export { Ui时间与伏笔页 } from "./时间与伏笔页";
|
||||
export { Ui事实审阅 } from "./事实审阅";
|
||||
102
web/src/功能/故事世界/世界资料页.tsx
Normal file
102
web/src/功能/故事世界/世界资料页.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
/** 世界资料页:实体分族浏览、版本轨迹与修订影响评估。 */
|
||||
import { useState } from "react";
|
||||
import { useMuse世界实体, useMuse世界类型, useMuse实体轨迹, useMuse影响评估 } from "./数据";
|
||||
import { Ui反馈状态 } from "../../界面/反馈状态";
|
||||
import { useMuse作者会话 } from "../../应用/作者会话";
|
||||
|
||||
export function Ui世界资料页({ workId }: { workId: string }) {
|
||||
const 会话 = useMuse作者会话();
|
||||
const scope = 会话.data ? "session" : null;
|
||||
const 实体 = useMuse世界实体(scope, workId);
|
||||
const 类型 = useMuse世界类型(scope);
|
||||
const [当前, set当前] = useState<string | null>(null);
|
||||
const 轨迹 = useMuse实体轨迹(scope, workId, 当前);
|
||||
const 影响 = useMuse影响评估(scope, workId, 当前);
|
||||
const 类型名 = (id: string) => 类型.data?.find((t) => t.type_id === id)?.名称 ?? id;
|
||||
|
||||
return (
|
||||
<main className="content">
|
||||
<p className="eyebrow">故事世界</p>
|
||||
<h1>世界资料</h1>
|
||||
<p className="muted">
|
||||
实体按 S04 登记类型分族;计划与已发生分开标记;修订前先看影响面。
|
||||
</p>
|
||||
<section className="paper-panel" aria-label="实体清单">
|
||||
<div className="panel-heading">
|
||||
<h2>实体清单</h2>
|
||||
<span className="muted">{实体.data?.length ?? 0} 个对象</span>
|
||||
</div>
|
||||
{实体.isPending ? (
|
||||
<Ui反馈状态 message="正在读取实体…" />
|
||||
) : (实体.data?.length ?? 0) === 0 ? (
|
||||
<p className="muted">本书还没有世界实体;抽取提案确认后这里可见。</p>
|
||||
) : (
|
||||
<ul className="event-list">
|
||||
{实体.data!.map((项) => (
|
||||
<li key={项.object_id}>
|
||||
<button
|
||||
className="text-button"
|
||||
onClick={() => {
|
||||
set当前(项.object_id);
|
||||
}}
|
||||
>
|
||||
{类型名(项.type_id)} · {项.object_id.slice(0, 10)} · r{项.revision}
|
||||
</button>
|
||||
<span className="muted">
|
||||
{项.knowledge_mode === "fact" || 项.knowledge_mode === "setting"
|
||||
? "已发生"
|
||||
: "计划"}
|
||||
{项.independent ? " · 独立设定" : ""}
|
||||
{项.依据可用 ? "" : " · 依据待复核"}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{实体.error && <Ui反馈状态 message={实体.error.message} error />}
|
||||
</section>
|
||||
|
||||
{当前 && (
|
||||
<>
|
||||
<section className="paper-panel" aria-label="版本轨迹">
|
||||
<div className="panel-heading">
|
||||
<h2>版本轨迹</h2>
|
||||
<span className="muted">{当前.slice(0, 10)}</span>
|
||||
</div>
|
||||
{轨迹.isPending ? (
|
||||
<Ui反馈状态 message="正在读取轨迹…" />
|
||||
) : (轨迹.data?.版本.length ?? 0) === 0 ? (
|
||||
<p className="muted">暂无版本。</p>
|
||||
) : (
|
||||
<ol className="event-list">
|
||||
{轨迹.data!.版本.map((v) => (
|
||||
<li key={v.revision}>
|
||||
r{v.revision} · {v.knowledge_mode}
|
||||
{v.independent ? " · 独立" : ""}
|
||||
{v.依据.length > 0 && ` · 依据 ${v.依据.length} 份`}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</section>
|
||||
<section className="paper-panel" aria-label="修订影响">
|
||||
<div className="panel-heading">
|
||||
<h2>修订影响</h2>
|
||||
<span className="muted">清单只标记复核,不撤回独立确认</span>
|
||||
</div>
|
||||
{影响.isPending ? (
|
||||
<Ui反馈状态 message="正在评估影响…" />
|
||||
) : 影响.data ? (
|
||||
<ul className="event-list">
|
||||
<li>受影响事实:{影响.data.受影响事实.length} 个</li>
|
||||
<li>本对象待决提案:{影响.data.本对象待决提案.length} 个</li>
|
||||
</ul>
|
||||
) : (
|
||||
<p className="muted">暂无评估。</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
79
web/src/功能/故事世界/事实审阅.tsx
Normal file
79
web/src/功能/故事世界/事实审阅.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
/** 事实审阅:待决提案清单与作者决定入口(复用既有提案审阅端点)。 */
|
||||
import { useState } from "react";
|
||||
import { useMuse作者会话 } from "../../应用/作者会话";
|
||||
|
||||
export function Ui事实审阅({ workId }: { workId: string }) {
|
||||
const 会话 = useMuse作者会话();
|
||||
const scope = 会话.data ? "session" : null;
|
||||
const [proposalId, setProposalId] = useState("");
|
||||
const [消息, set消息] = useState("");
|
||||
void scope;
|
||||
void workId;
|
||||
|
||||
async function 打开审阅() {
|
||||
if (!proposalId.trim()) {
|
||||
set消息("先填写提案编号。");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { 客户端 } = await import("../../接口/生成/客户端");
|
||||
const 定位 = await 客户端.GET("/api/v1/fact-proposals/{proposal_id}", {
|
||||
params: { path: { proposal_id: proposalId.trim() } },
|
||||
});
|
||||
const 提案 = 定位.data as unknown as {
|
||||
proposal_id: string;
|
||||
revision: number;
|
||||
proposal_hash: string;
|
||||
object_id: string;
|
||||
expected_revision: number;
|
||||
} | undefined;
|
||||
if (!提案) throw new Error("提案读取失败");
|
||||
const 审阅 = await 客户端.POST("/api/v1/fact-proposals/{proposal_id}/reviews", {
|
||||
params: { path: { proposal_id: 提案.proposal_id } },
|
||||
body: {
|
||||
proposal_id: 提案.proposal_id,
|
||||
proposal_revision: 提案.revision,
|
||||
proposal_hash: 提案.proposal_hash,
|
||||
target_ref: `world:${提案.object_id}`,
|
||||
expected_revision: 提案.expected_revision,
|
||||
},
|
||||
});
|
||||
if (!审阅.data) throw new Error("审阅打开失败");
|
||||
set消息(`审阅已打开:${(审阅.data as unknown as { review_id: string }).review_id}`);
|
||||
} catch (e) {
|
||||
set消息(e instanceof Error ? e.message : "审阅打开失败。");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="paper-panel" aria-label="事实审阅">
|
||||
<div className="panel-heading">
|
||||
<h2>事实审阅</h2>
|
||||
<span className="muted">提案经作者确认后才成为正式事实</span>
|
||||
</div>
|
||||
<form
|
||||
className="structure-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void 打开审阅();
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
提案编号
|
||||
<input
|
||||
name="proposal_id"
|
||||
placeholder="待审事实提案"
|
||||
value={proposalId}
|
||||
onChange={(e) => setProposalId(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">打开审阅</button>
|
||||
</form>
|
||||
{消息 && (
|
||||
<p role="status" className="error-text">
|
||||
{消息}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
115
web/src/功能/故事世界/人物关系页.tsx
Normal file
115
web/src/功能/故事世界/人物关系页.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
/** 人物关系页:人物分组、端点已解析的关系与人物弧线(计划/已发生分开)。 */
|
||||
import { useState } from "react";
|
||||
import { useMuse人物关系, useMuse人物弧线 } from "./数据";
|
||||
import { Ui反馈状态 } from "../../界面/反馈状态";
|
||||
import { useMuse作者会话 } from "../../应用/作者会话";
|
||||
import { Ui事实审阅 } from "./事实审阅";
|
||||
import { Ui关系图 } from "../../界面/关系图";
|
||||
|
||||
export function Ui人物关系页({ workId }: { workId: string }) {
|
||||
const 会话 = useMuse作者会话();
|
||||
const scope = 会话.data ? "session" : null;
|
||||
const 视图 = useMuse人物关系(scope, workId);
|
||||
const [人物, set人物] = useState<string | null>(null);
|
||||
const 弧线 = useMuse人物弧线(scope, workId, 人物);
|
||||
|
||||
return (
|
||||
<main className="content">
|
||||
<p className="eyebrow">故事世界</p>
|
||||
<h1>人物与关系</h1>
|
||||
<p className="muted">
|
||||
人物按类型分组;关系端点是已解析的本书对象;弧线把计划与已发生分开。
|
||||
</p>
|
||||
<section className="paper-panel" aria-label="人物分组">
|
||||
<div className="panel-heading">
|
||||
<h2>人物分组</h2>
|
||||
<span className="muted">{视图.data?.人物分组.length ?? 0} 个类型</span>
|
||||
</div>
|
||||
{视图.isPending ? (
|
||||
<Ui反馈状态 message="正在读取人物…" />
|
||||
) : (视图.data?.人物分组.length ?? 0) === 0 ? (
|
||||
<p className="muted">本书还没有世界实体。</p>
|
||||
) : (
|
||||
视图.data!.人物分组.map((组) => (
|
||||
<div key={组.type_id} aria-label={`类型 ${组.type_id}`}>
|
||||
<h3>
|
||||
{组.type_id}({组.数量})
|
||||
</h3>
|
||||
<ul className="event-list">
|
||||
{组.对象.map((对象) => (
|
||||
<li key={对象.object_id}>
|
||||
<button className="text-button" onClick={() => set人物(对象.object_id)}>
|
||||
{对象.object_id.slice(0, 12)} · r{对象.revision}
|
||||
</button>
|
||||
<span className="muted">
|
||||
{对象.knowledge_mode === "fact" || 对象.knowledge_mode === "setting"
|
||||
? "已发生"
|
||||
: "计划"}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="paper-panel" aria-label="关系清单">
|
||||
<div className="panel-heading">
|
||||
<h2>关系清单</h2>
|
||||
<span className="muted">{视图.data?.关系.length ?? 0} 条</span>
|
||||
</div>
|
||||
{(视图.data?.关系.length ?? 0) === 0 ? (
|
||||
<p className="muted">还没有关系对象。</p>
|
||||
) : (
|
||||
<>
|
||||
<ul className="event-list">
|
||||
{视图.data!.关系.map((关系) => (
|
||||
<li key={关系.object_id}>
|
||||
{关系.type_id} ·{" "}
|
||||
{关系.端点.map((端) => `${端.type_id}:${端.object_id.slice(0, 8)}`).join(" ↔ ")}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Ui关系图 关系={视图.data!.关系} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{人物 && (
|
||||
<section className="paper-panel" aria-label="人物弧线">
|
||||
<div className="panel-heading">
|
||||
<h2>人物弧线</h2>
|
||||
<span className="muted">{人物.slice(0, 12)}</span>
|
||||
</div>
|
||||
{弧线.isPending ? (
|
||||
<Ui反馈状态 message="正在读取弧线…" />
|
||||
) : 弧线.data ? (
|
||||
<>
|
||||
<h3>已发生({弧线.data.已发生.length})</h3>
|
||||
<ol className="event-list">
|
||||
{弧线.data.已发生.map((v) => (
|
||||
<li key={v.revision}>
|
||||
r{v.revision} · {v.knowledge_mode}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<h3>计划与猜测({弧线.data.计划.length})</h3>
|
||||
<ol className="event-list">
|
||||
{弧线.data.计划.map((v) => (
|
||||
<li key={v.revision}>
|
||||
r{v.revision} · {v.knowledge_mode}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
) : (
|
||||
<p className="muted">暂无弧线。</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Ui事实审阅 workId={workId} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
140
web/src/功能/故事世界/数据.ts
Normal file
140
web/src/功能/故事世界/数据.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/** 故事世界数据接入:实体浏览、轨迹、关系、弧线与影响评估。 */
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { 客户端 } from "../../接口/生成/客户端";
|
||||
import { 查询键 } from "../../接口/查询键";
|
||||
|
||||
export interface 世界实体 {
|
||||
object_id: string;
|
||||
type_id: string;
|
||||
revision: number;
|
||||
knowledge_mode: string;
|
||||
independent: boolean;
|
||||
依据可用: boolean;
|
||||
生效位置: string | null;
|
||||
计划窗口: { from_chapter_id: string; to_chapter_id: string } | null;
|
||||
content: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface 实体轨迹 {
|
||||
object_id: string;
|
||||
type_id: string;
|
||||
instance_family: string;
|
||||
current_revision: number;
|
||||
版本: {
|
||||
revision: number;
|
||||
system_revision: number;
|
||||
knowledge_mode: string;
|
||||
independent: boolean;
|
||||
content_hash: string;
|
||||
content: Record<string, unknown>;
|
||||
依据: { document_id: string; revision: number }[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface 关系行 {
|
||||
object_id: string;
|
||||
type_id: string;
|
||||
revision: number;
|
||||
knowledge_mode: string;
|
||||
content: Record<string, unknown>;
|
||||
端点: { object_id: string; type_id: string; revision: number }[];
|
||||
}
|
||||
|
||||
export interface 影响评估 {
|
||||
object_id: string;
|
||||
current_revision: number;
|
||||
受影响事实: string[];
|
||||
本对象待决提案: { proposal_id: string; object_id: string }[];
|
||||
章序影响: { object_id: string; 涉及章: string[] }[];
|
||||
复核说明: string;
|
||||
}
|
||||
|
||||
export function useMuse世界类型(scope: string | null) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, "world:types"),
|
||||
enabled: !!scope,
|
||||
queryFn: async ({ signal }) =>
|
||||
(await 客户端.GET("/api/v1/world/types", { signal, params: {} }))
|
||||
.data as unknown as {
|
||||
type_id: string;
|
||||
名称: string;
|
||||
语义域: string;
|
||||
实例族: string[];
|
||||
说明: string;
|
||||
}[],
|
||||
});
|
||||
}
|
||||
|
||||
export function useMuse世界实体(scope: string | null, workId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, `${workId}:world:entities`),
|
||||
enabled: !!workId,
|
||||
queryFn: async ({ signal }) =>
|
||||
(await 客户端.GET("/api/v1/works/{work_id}/world/entities", {
|
||||
signal,
|
||||
params: { path: { work_id: workId! } },
|
||||
})).data as unknown as 世界实体[],
|
||||
});
|
||||
}
|
||||
|
||||
export function useMuse实体轨迹(scope: string | null, workId: string | null, objectId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, `${workId}:world:${objectId}:timeline`),
|
||||
enabled: !!workId && !!objectId,
|
||||
queryFn: async ({ signal }) =>
|
||||
((await 客户端.GET("/api/v1/works/{work_id}/world/entities/{object_id}/timeline", {
|
||||
signal,
|
||||
params: { path: { work_id: workId!, object_id: objectId! } },
|
||||
})).data ?? null) as 实体轨迹 | null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMuse影响评估(scope: string | null, workId: string | null, objectId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, `${workId}:world:${objectId}:impact`),
|
||||
enabled: !!workId && !!objectId,
|
||||
queryFn: async ({ signal }) =>
|
||||
((await 客户端.GET("/api/v1/works/{work_id}/world/entities/{object_id}/impact", {
|
||||
signal,
|
||||
params: { path: { work_id: workId!, object_id: objectId! } },
|
||||
})).data ?? null) as 影响评估 | null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMuse人物关系(scope: string | null, workId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, `${workId}:world:relations`),
|
||||
enabled: !!workId,
|
||||
queryFn: async ({ signal }) =>
|
||||
((await 客户端.GET("/api/v1/works/{work_id}/world/relations", {
|
||||
signal,
|
||||
params: { path: { work_id: workId! } },
|
||||
})).data ?? null) as unknown as {
|
||||
人物分组: { type_id: string; 数量: number; 对象: 世界实体[] }[];
|
||||
关系: 关系行[];
|
||||
} | null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMuse人物弧线(
|
||||
scope: string | null,
|
||||
workId: string | null,
|
||||
characterId: string | null,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, `${workId}:world:${characterId}:arc`),
|
||||
enabled: !!workId && !!characterId,
|
||||
queryFn: async ({ signal }) =>
|
||||
((await 客户端.GET("/api/v1/works/{work_id}/world/characters/{character_id}", {
|
||||
signal,
|
||||
params: { path: { work_id: workId!, character_id: characterId! } },
|
||||
})).data ?? null) as unknown as {
|
||||
object_id: string;
|
||||
type_id: string;
|
||||
current_revision: number;
|
||||
计划: { revision: number; knowledge_mode: string; content: Record<string, unknown> }[];
|
||||
已发生: { revision: number; knowledge_mode: string; content: Record<string, unknown> }[];
|
||||
说明: string;
|
||||
} | null,
|
||||
});
|
||||
}
|
||||
114
web/src/功能/故事世界/时间与伏笔页.tsx
Normal file
114
web/src/功能/故事世界/时间与伏笔页.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
/** 时间与伏笔页:时点世界查询(作者总览/读者进度/角色知情)与计划窗口呈现。 */
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { 客户端 } from "../../接口/生成/客户端";
|
||||
import { 查询键 } from "../../接口/查询键";
|
||||
import { Ui反馈状态 } from "../../界面/反馈状态";
|
||||
import { useMuse作者会话 } from "../../应用/作者会话";
|
||||
|
||||
export function Ui时间与伏笔页({ workId }: { workId: string }) {
|
||||
const 会话 = useMuse作者会话();
|
||||
const scope = 会话.data ? "session" : null;
|
||||
const [截止, set截止] = useState("");
|
||||
const [视角, set视角] = useState<"author" | "reader" | "actor">("author");
|
||||
const [知情主体, set知情主体] = useState("");
|
||||
|
||||
const 世界 = useQuery({
|
||||
queryKey: 查询键(scope, `${workId}:world-state:${截止}:${视角}:${知情主体}`),
|
||||
enabled: !!scope && !!workId,
|
||||
queryFn: async ({ signal }) =>
|
||||
((await 客户端.GET("/api/v1/works/{work_id}/facts", {
|
||||
signal,
|
||||
params: {
|
||||
path: { work_id: workId },
|
||||
query: {
|
||||
as_of: 截止 || undefined,
|
||||
view: (视角 === "actor" ? "character" : 视角) as
|
||||
| "author"
|
||||
| "reader"
|
||||
| "character",
|
||||
character_id: 知情主体 || undefined,
|
||||
},
|
||||
},
|
||||
})).data ?? null) as unknown as {
|
||||
objects?: {
|
||||
object_id: string;
|
||||
type_id: string;
|
||||
knowledge_mode?: string;
|
||||
planned_window?: { from_chapter_id: string; to_chapter_id: string } | null;
|
||||
}[];
|
||||
省略原因?: Record<string, string>;
|
||||
} | null,
|
||||
});
|
||||
|
||||
const 对象 = 世界.data?.objects ?? [];
|
||||
const 伏笔 = 对象.filter((o) => o.planned_window);
|
||||
|
||||
return (
|
||||
<main className="content">
|
||||
<p className="eyebrow">故事世界</p>
|
||||
<h1>时间与伏笔</h1>
|
||||
<p className="muted">
|
||||
按叙事时点查询世界状态:作者总览、读者进度与指定角色知情分别呈现;计划窗口
|
||||
(预期伏笔)只按稳定章节身份显示。
|
||||
</p>
|
||||
<section className="paper-panel" aria-label="时点查询">
|
||||
<form
|
||||
className="structure-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const 值 = new FormData(e.currentTarget);
|
||||
set截止(String(值.get("as_of") ?? ""));
|
||||
set视角(String(值.get("viewpoint") ?? "author") as "author" | "reader" | "actor");
|
||||
set知情主体(String(值.get("actor") ?? ""));
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
截止章节(留空为当前)
|
||||
<input name="as_of" placeholder="章节 ID" />
|
||||
</label>
|
||||
<label>
|
||||
视角
|
||||
<select name="viewpoint" defaultValue="author">
|
||||
<option value="author">作者总览</option>
|
||||
<option value="reader">读者进度</option>
|
||||
<option value="actor">指定角色</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
知情主体(视角=角色时)
|
||||
<input name="actor" placeholder="角色对象 ID" />
|
||||
</label>
|
||||
<button type="submit">查询时点世界</button>
|
||||
</form>
|
||||
{世界.isFetching ? (
|
||||
<Ui反馈状态 message="正在查询…" />
|
||||
) : (
|
||||
<p className="muted">
|
||||
时点对象 {对象.length} 个;其中带计划窗口 {伏笔.length} 个。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="paper-panel" aria-label="伏笔与承诺">
|
||||
<div className="panel-heading">
|
||||
<h2>伏笔与承诺</h2>
|
||||
<span className="muted">{伏笔.length} 条计划窗口</span>
|
||||
</div>
|
||||
{伏笔.length === 0 ? (
|
||||
<p className="muted">当前时点没有带计划窗口的对象。</p>
|
||||
) : (
|
||||
<ul className="event-list">
|
||||
{伏笔.map((项) => (
|
||||
<li key={项.object_id}>
|
||||
{项.type_id} · {项.object_id.slice(0, 10)} · 计划{" "}
|
||||
{项.planned_window!.from_chapter_id.slice(0, 8)} →{" "}
|
||||
{项.planned_window!.to_chapter_id.slice(0, 8)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from "../功能/作品规划";
|
||||
import { Ui候选审阅页, Ui正文页 } from "../功能/正文写作";
|
||||
import { Ui资料页, Ui拆书工作区, Ui榜单与选题页 } from "../功能/资料研究";
|
||||
import { Ui世界资料页, Ui人物关系页, Ui时间与伏笔页 } from "../功能/故事世界";
|
||||
import { 客户端 } from "../接口/生成/客户端";
|
||||
|
||||
export function Ui页面路由() {
|
||||
@ -24,6 +25,9 @@ export function Ui页面路由() {
|
||||
element={<Ui候选审阅页 />}
|
||||
/>
|
||||
<Route path="/tasks" element={<Ui任务列表 />} />
|
||||
<Route path="/works/:work_id/world" element={<Ui世界资料页壳 />} />
|
||||
<Route path="/works/:work_id/world/relations" element={<Ui人物关系页壳 />} />
|
||||
<Route path="/works/:work_id/world/timeline" element={<Ui时间与伏笔页Shell />} />
|
||||
<Route path="/sources" element={<Ui资料页 />} />
|
||||
<Route path="/research" element={<Ui拆书工作区 />} />
|
||||
<Route path="/rankings" element={<Ui榜单与选题页 />} />
|
||||
@ -61,3 +65,24 @@ function Ui连接状态() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function 取路径参数(name: string): string | undefined {
|
||||
const 值 = window.location.pathname.split("/").filter(Boolean);
|
||||
const 索引 = 值.indexOf(name === "work_id" ? "works" : name);
|
||||
return 索引 >= 0 && 值[索引 + 1] ? decodeURIComponent(值[索引 + 1]) : undefined;
|
||||
}
|
||||
|
||||
function Ui世界资料页壳() {
|
||||
const id = 取路径参数("work_id") ?? "";
|
||||
return <Ui世界资料页 workId={id} />;
|
||||
}
|
||||
|
||||
function Ui人物关系页壳() {
|
||||
const id = 取路径参数("work_id") ?? "";
|
||||
return <Ui人物关系页 workId={id} />;
|
||||
}
|
||||
|
||||
function Ui时间与伏笔页Shell() {
|
||||
const id = 取路径参数("work_id") ?? "";
|
||||
return <Ui时间与伏笔页 workId={id} />;
|
||||
}
|
||||
|
||||
@ -1085,6 +1085,109 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/world/types": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 世界类型清单 */
|
||||
get: operations["list_world_types"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/works/{work_id}/world/entities": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 列出世界实体 */
|
||||
get: operations["list_world_entities"];
|
||||
put?: never;
|
||||
/** 保存世界实体 */
|
||||
post: operations["save_world_entity"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/works/{work_id}/world/entities/{object_id}/timeline": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 读取实体轨迹 */
|
||||
get: operations["read_entity_timeline"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/works/{work_id}/world/entities/{object_id}/impact": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 评估修订影响 */
|
||||
get: operations["assess_world_revision"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/works/{work_id}/world/relations": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 人物关系视图 */
|
||||
get: operations["list_world_relations"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/works/{work_id}/world/characters/{character_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 人物弧线 */
|
||||
get: operations["read_character_arc"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/tasks/{task_id}/events": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -1477,8 +1580,13 @@ export interface components {
|
||||
/** To Chapter Id */
|
||||
to_chapter_id: string;
|
||||
};
|
||||
/** 节点条目 */
|
||||
/** 创作命令 */
|
||||
muse__________________16: {
|
||||
/** Command Id */
|
||||
command_id: string;
|
||||
};
|
||||
/** 节点条目 */
|
||||
muse__________________17: {
|
||||
/** Node Id */
|
||||
node_id: string;
|
||||
/**
|
||||
@ -1494,7 +1602,7 @@ export interface components {
|
||||
position: number;
|
||||
};
|
||||
/** 作品概要 */
|
||||
muse__________________17: {
|
||||
muse__________________18: {
|
||||
/** Work Id */
|
||||
work_id: string;
|
||||
/** Revision */
|
||||
@ -1505,7 +1613,7 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** 章节条目 */
|
||||
muse__________________18: {
|
||||
muse__________________19: {
|
||||
/** Chapter Id */
|
||||
chapter_id: string;
|
||||
/** Title */
|
||||
@ -1513,23 +1621,23 @@ export interface components {
|
||||
/** Position */
|
||||
position: number;
|
||||
};
|
||||
/** 目录结果 */
|
||||
muse__________________19: {
|
||||
/** Work Id */
|
||||
work_id: string;
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Chapters */
|
||||
chapters: components["schemas"]["muse__________________18"][];
|
||||
};
|
||||
/**
|
||||
* 内容用途
|
||||
* @description 内容消费用途。
|
||||
* @enum {string}
|
||||
*/
|
||||
muse__________________2: "planning" | "generation" | "detection" | "extraction";
|
||||
/** 规划结果 */
|
||||
/** 目录结果 */
|
||||
muse__________________20: {
|
||||
/** Work Id */
|
||||
work_id: string;
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Chapters */
|
||||
chapters: components["schemas"]["muse__________________19"][];
|
||||
};
|
||||
/** 规划结果 */
|
||||
muse__________________21: {
|
||||
/** Plan Id */
|
||||
plan_id: string;
|
||||
/** Work Id */
|
||||
@ -1969,7 +2077,7 @@ export interface components {
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Nodes */
|
||||
nodes: components["schemas"]["muse__________________16"][];
|
||||
nodes: components["schemas"]["muse__________________17"][];
|
||||
};
|
||||
/** 正式回执结果 */
|
||||
muse____________________25: {
|
||||
@ -2158,7 +2266,7 @@ export interface components {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
candidate: components["schemas"]["muse____________________34"];
|
||||
before: components["schemas"]["muse__________________20"] | null;
|
||||
before: components["schemas"]["muse__________________21"] | null;
|
||||
};
|
||||
/** 规划表单结果 */
|
||||
muse____________________36: {
|
||||
@ -3476,7 +3584,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse__________________17"][];
|
||||
"application/json": components["schemas"]["muse__________________18"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -3567,7 +3675,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse__________________19"];
|
||||
"application/json": components["schemas"]["muse__________________20"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -3889,7 +3997,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse__________________20"][];
|
||||
"application/json": components["schemas"]["muse__________________21"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -4054,7 +4162,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse__________________20"];
|
||||
"application/json": components["schemas"]["muse__________________21"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -4156,7 +4264,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse__________________20"];
|
||||
"application/json": components["schemas"]["muse__________________21"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -5255,6 +5363,231 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
list_world_types: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
list_world_entities: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
work_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
save_world_entity: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
work_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse__________________16"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse____________________25"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
read_entity_timeline: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
work_id: string;
|
||||
object_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
assess_world_revision: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
work_id: string;
|
||||
object_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
list_world_relations: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
work_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
read_character_arc: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
work_id: string;
|
||||
character_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
read_task_events: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
25
web/src/界面/关系图.tsx
Normal file
25
web/src/界面/关系图.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
/** 关系列系图:关系端点的轻量邻接列表呈现(不引第三方图形库)。 */
|
||||
import type { 关系行 } from "../功能/故事世界/数据";
|
||||
|
||||
export function Ui关系图({ 关系 }: { 关系: 关系行[] }) {
|
||||
if (关系.length === 0) return <p className="muted">还没有关系可呈现。</p>;
|
||||
const 邻接: Record<string, string[]> = {};
|
||||
for (const 行 of 关系) {
|
||||
const 端 = 行.端点.map((e) => e.object_id);
|
||||
for (let i = 0; i < 端.length; i += 1) {
|
||||
for (let j = i + 1; j < 端.length; j += 1) {
|
||||
(邻接[端[i]] ??= []).push(`${行.type_id}→${端[j]}`);
|
||||
(邻接[端[j]] ??= []).push(`${行.type_id}→${端[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div aria-label="关系邻接" className="event-list">
|
||||
{Object.entries(邻接).map(([对象, 边]) => (
|
||||
<p key={对象} className="mono">
|
||||
{对象.slice(0, 10)}:{边.join("、")}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
web/tests/旅程/世界维护.spec.ts
Normal file
55
web/tests/旅程/世界维护.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/** 真实浏览器与隔离库:世界资料页、人物关系页与时间伏笔页的维护旅程。 */
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.use({ trace: "off" });
|
||||
|
||||
test("NC-browser-world-workbench:世界维护三页旅程", async ({ page }, info) => {
|
||||
if (!process.env.MUSE_WORKBENCH_URL || !process.env.MUSE_AUTHOR_PASSWORD_FILE)
|
||||
throw new Error("需要明确的隔离后端和作者口令文件。");
|
||||
const workId = process.env.MUSE_WORK_ID!;
|
||||
if (!workId) 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.goto(`/works/${workId}/world`);
|
||||
const 清单 = page.locator('section[aria-label="实体清单"]');
|
||||
await expect(清单.getByText(/2 个对象/)).toBeVisible({ timeout: 15_000 });
|
||||
await 清单.getByRole("button", { name: /char-lin/ }).first().click();
|
||||
await expect(page.locator('section[aria-label="版本轨迹"]').getByText(/r1/)).toBeVisible();
|
||||
await expect(
|
||||
page.locator('section[aria-label="修订影响"]').getByText(/受影响事实:\d+ 个/),
|
||||
).toBeVisible();
|
||||
await page.screenshot({ path: `${info.outputDir}/世界资料页.png`, fullPage: true });
|
||||
|
||||
// 人物关系页:人物分组可见;弧线按已发生/计划分区。
|
||||
await page.goto(`/works/${workId}/world/relations`);
|
||||
await expect(page.locator('section[aria-label="人物分组"]').getByText(/character/)).toBeVisible();
|
||||
await page
|
||||
.locator('section[aria-label="人物分组"]')
|
||||
.getByRole("button", { name: /char-lin/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(
|
||||
page.locator('section[aria-label="人物弧线"]').getByText(/已发生(\d+)/),
|
||||
).toBeVisible();
|
||||
await page.screenshot({ path: `${info.outputDir}/人物关系页.png`, fullPage: true });
|
||||
|
||||
// 时间与伏笔页:时点查询表单与说明可见。
|
||||
await page.goto(`/works/${workId}/world/timeline`);
|
||||
await expect(page.getByRole("button", { name: "查询时点世界" })).toBeVisible();
|
||||
await expect(page.locator('section[aria-label="伏笔与承诺"]')).toBeVisible();
|
||||
await page.screenshot({ path: `${info.outputDir}/时间与伏笔页.png`, fullPage: true });
|
||||
|
||||
expect(
|
||||
consoleErrors.filter((文本) => !文本.includes("favicon")),
|
||||
"页面不得出现控制台错误",
|
||||
).toEqual([]);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user