✨ feat: WP-03 三界归真作用域与消息版本谱系(ADR-020 落地)

- 迁移 5:conversation.realm(默认真界,检查约束);message.realm 快照、canonized_at、parent_message_id 自引用与 variant_kind(origin/revision/candidate)
- 界域端点:GET conversation(realm/当前分支)与 POST realm(入境/出境显式切换,留审计——ADR-020 切换必须可见)
- 归真端点 POST /v1/messages/{id}/canonizations:scope 必须与消息界域一致(不一致 422);幂等(重复归真 200 原状态);境界归真产出 world.canonized Outbox 事件(世界知识候选入口,WP-04 消费);幻境归真仅标记试演记录
- 编辑版本 POST /v1/messages/{id}/revisions:新消息 variant=revision 引用原消息(markdown 修订同样经灵台引擎规范化),原消息永不修改
- 测试 44 项全绿(+8):界域快照、归真正/反/幂等、幻境试演、revision 原消息不变、跨域 401/403/404 fail-closed
This commit is contained in:
chenxin committed 2026-09-10 13:28:25 +08:00
1 parent 37c53862c4
commit 603a683c14
6 files changed
+432 -6

No files matched your search

+93
View File
@@ -206,3 +206,96 @@ async def set_current_branch(
db, domain.id, conversation_id, body.get("branch_id", "")
)
return {"id": conversation.id, "current_branch_id": conversation.current_branch_id or ""}
@router.get("/{conversation_id}")
async def get_conversation(
conversation_id: str,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, object]:
domain = await _domain_of(db, user)
conversation = await service.require_conversation(db, domain.id, conversation_id)
from app.conversations.schemas import ConversationOut
return ConversationOut(
id=conversation.id,
title=conversation.title,
current_branch_id=conversation.current_branch_id,
created_at=conversation.created_at.isoformat(),
).model_dump() | {"realm": conversation.realm}
@router.post("/{conversation_id}/realm")
async def set_realm(
conversation_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, str]:
"""入境/出境:切换对话界域(显式、留审计)。"""
domain = await _domain_of(db, user)
conversation = await service.set_realm(
db,
domain.id,
conversation_id,
body.get("realm", ""),
getattr(request.state, "trace_id", None),
)
return {"id": conversation.id, "realm": conversation.realm}
message_router = APIRouter(prefix="/v1/messages", tags=["messages"])
@message_router.post("/{message_id}/canonizations", status_code=201, response_model=None)
async def canonize_message(
message_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> JSONResponse | dict[str, str | None]:
"""归真:携带 canonical_scope(ADR-020);重复归真幂等返回原状态(200)。"""
from fastapi.responses import JSONResponse
domain = await _domain_of(db, user)
message, replayed = await service.canonize_message(
db, domain.id, message_id, body.get("scope", ""), getattr(request.state, "trace_id", None)
)
payload = {
"message_id": message.id,
"canonical_scope": message.realm,
"canonized_at": message.canonized_at.isoformat() if message.canonized_at else None,
}
if replayed:
return JSONResponse(status_code=200, content=payload)
return payload
@message_router.post("/{message_id}/revisions", status_code=201)
async def revise_message(
message_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: MessageIn,
) -> dict[str, object]:
"""编辑版本:新消息引用原消息;原消息永不修改。"""
domain = await _domain_of(db, user)
revision = await service.revise_message(
db, domain.id, message_id, body, getattr(request.state, "trace_id", None)
)
from app.conversations.schemas import MessageOut
return MessageOut(
id=revision.id,
conversation_id=revision.conversation_id,
branch_id=revision.branch_id,
role=revision.role,
content=revision.content,
parser_version=revision.parser_version,
created_at=revision.created_at.isoformat(),
).model_dump() | {"parent_message_id": revision.parent_message_id,
"variant_kind": revision.variant_kind}