M2a:镜树域后端核心——树/节点/引文/归真/移栽全量 API 与 Kernel 完成回调(芽体固化/残答封存),8 用例钉死 PRD 场景 1/2/3/5/6/7/8
This commit is contained in:
1 parent
1cd0069d65
commit
e6a3b7121f
16 files changed
+2002
-74
No files matched your search
@@ -21,6 +21,7 @@ from app.api.v1.endpoints import (
|
||||
jingsheng_characters,
|
||||
kernel_runs,
|
||||
lorebooks,
|
||||
mirror_trees,
|
||||
mcp,
|
||||
memory,
|
||||
personality,
|
||||
@@ -44,6 +45,8 @@ api_router.include_router(runs.router)
|
||||
api_router.include_router(kernel_runs.router)
|
||||
api_router.include_router(embryo_drafts.router)
|
||||
api_router.include_router(jingsheng_characters.router)
|
||||
api_router.include_router(mirror_trees.router)
|
||||
api_router.include_router(mirror_trees.node_router)
|
||||
api_router.include_router(memory.router)
|
||||
api_router.include_router(lorebooks.router)
|
||||
api_router.include_router(personality.router)
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
"""镜树 API(api-contracts.md §3.3)。树级操作挂 /trees,节点级挂 /nodes 与 /citations。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.v1.deps import get_current_user
|
||||
from app.core.errors import AppError
|
||||
from app.domains import mirror_tree
|
||||
from app.kernel import Kernel
|
||||
from app.schemas.auth import CurrentUser
|
||||
|
||||
router = APIRouter(prefix="/trees", tags=["mirror-tree"])
|
||||
node_router = APIRouter(prefix="/nodes", tags=["mirror-tree"])
|
||||
|
||||
|
||||
class TreeCreate(BaseModel):
|
||||
character_id: str
|
||||
title: str = Field(min_length=1, max_length=300)
|
||||
default_realm: str = "reality"
|
||||
|
||||
|
||||
class TreePatch(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=300)
|
||||
status: str | None = Field(default=None, description="active / archived")
|
||||
|
||||
|
||||
class QuestionCreate(BaseModel):
|
||||
content: str = Field(min_length=1, max_length=20_000)
|
||||
realm: str | None = None
|
||||
parent_answer_id: str | None = None
|
||||
citations: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AnswerRequest(BaseModel):
|
||||
instructions: str | None = Field(default=None, max_length=8000)
|
||||
|
||||
|
||||
class AdoptRequest(BaseModel):
|
||||
node_id: str
|
||||
|
||||
|
||||
class CanonizeRequest(BaseModel):
|
||||
endpoint_node_id: str
|
||||
canonical_scope: str
|
||||
world_id: str | None = None
|
||||
|
||||
|
||||
class EditForkRequest(BaseModel):
|
||||
new_content: str = Field(min_length=1, max_length=20_000)
|
||||
|
||||
|
||||
class TransplantRequest(BaseModel):
|
||||
node_id: str
|
||||
new_title: str = Field(min_length=1, max_length=300)
|
||||
|
||||
|
||||
class CitationCreate(BaseModel):
|
||||
target_type: str = "text_range"
|
||||
locator: dict = Field(default_factory=dict)
|
||||
snapshot: str = ""
|
||||
source_id: str = Field(min_length=1, max_length=200)
|
||||
source_version: int = 1
|
||||
|
||||
|
||||
def _get_kernel(request: Request) -> Kernel:
|
||||
kernel: Kernel | None = getattr(request.app.state, "kernel", None)
|
||||
if kernel is None:
|
||||
raise AppError("service_unavailable", "Kernel 运行时未就绪。", status_code=503)
|
||||
return kernel
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_trees_api(
|
||||
character_id: str | None = Query(default=None),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
return [mirror_tree.tree_view(t) for t in await mirror_tree.list_trees(user.owner_scope, character_id)]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_tree_api(
|
||||
payload: TreeCreate, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
tree = await mirror_tree.create_tree(
|
||||
character_id=payload.character_id, owner_scope=user.owner_scope, title=payload.title,
|
||||
default_realm=payload.default_realm,
|
||||
)
|
||||
return mirror_tree.tree_view(tree)
|
||||
|
||||
|
||||
@router.get("/{tree_id}")
|
||||
async def get_tree_api(tree_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||||
return await mirror_tree.get_tree_view(tree_id, user.owner_scope)
|
||||
|
||||
|
||||
@router.patch("/{tree_id}")
|
||||
async def patch_tree_api(
|
||||
tree_id: str, payload: TreePatch, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
if payload.status == "archived":
|
||||
return await mirror_tree.archive_tree(tree_id, user.owner_scope)
|
||||
if payload.title is not None:
|
||||
return await mirror_tree.rename_tree(tree_id, user.owner_scope, payload.title)
|
||||
return await mirror_tree.get_tree_view(tree_id, user.owner_scope)
|
||||
|
||||
|
||||
@router.get("/{tree_id}/path")
|
||||
async def tree_path_api(
|
||||
tree_id: str,
|
||||
entry: str | None = Query(default=None),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
return await mirror_tree.path_view(tree_id, user.owner_scope, entry)
|
||||
|
||||
|
||||
@router.get("/{tree_id}/local-graph")
|
||||
async def local_graph_api(
|
||||
tree_id: str,
|
||||
node: str | None = Query(default=None),
|
||||
depth: int = Query(default=2, ge=0, le=6),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
return await mirror_tree.local_graph(tree_id, user.owner_scope, node, depth)
|
||||
|
||||
|
||||
@router.post("/{tree_id}/nodes", status_code=201)
|
||||
async def create_question_api(
|
||||
tree_id: str, payload: QuestionCreate, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
node = await mirror_tree.create_question(
|
||||
tree_id=tree_id,
|
||||
owner_scope=user.owner_scope,
|
||||
content=payload.content,
|
||||
realm=payload.realm,
|
||||
parent_answer_id=payload.parent_answer_id,
|
||||
citations=payload.citations,
|
||||
)
|
||||
return mirror_tree.node_view(node)
|
||||
|
||||
|
||||
@router.post("/{tree_id}/adopt")
|
||||
async def adopt_api(
|
||||
tree_id: str, payload: AdoptRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await mirror_tree.adopt(tree_id=tree_id, owner_scope=user.owner_scope, node_id=payload.node_id)
|
||||
|
||||
|
||||
@router.post("/{tree_id}/canonize")
|
||||
async def canonize_api(
|
||||
tree_id: str, payload: CanonizeRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await mirror_tree.canonize(
|
||||
tree_id=tree_id,
|
||||
owner_scope=user.owner_scope,
|
||||
endpoint_node_id=payload.endpoint_node_id,
|
||||
scope=payload.canonical_scope,
|
||||
world_id=payload.world_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{tree_id}/transplant", status_code=201)
|
||||
async def transplant_api(
|
||||
tree_id: str, payload: TransplantRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await mirror_tree.transplant(
|
||||
tree_id=tree_id, owner_scope=user.owner_scope, node_id=payload.node_id,
|
||||
new_title=payload.new_title,
|
||||
)
|
||||
|
||||
|
||||
# ── 节点级 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@node_router.post("/{node_id}/answers", status_code=202)
|
||||
async def spawn_answer_api(
|
||||
node_id: str,
|
||||
payload: AnswerRequest,
|
||||
request: Request,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = await mirror_tree.spawn_answer(
|
||||
question_id=node_id,
|
||||
owner_scope=user.owner_scope,
|
||||
kernel=_get_kernel(request),
|
||||
instructions=payload.instructions,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@node_router.post("/{node_id}/continue", status_code=202)
|
||||
async def continue_api(
|
||||
node_id: str,
|
||||
payload: AnswerRequest,
|
||||
request: Request,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
return await mirror_tree.continue_node(
|
||||
node_id=node_id, owner_scope=user.owner_scope, kernel=_get_kernel(request),
|
||||
instructions=payload.instructions,
|
||||
)
|
||||
|
||||
|
||||
@node_router.post("/{node_id}/resume", status_code=202)
|
||||
async def resume_api(
|
||||
node_id: str,
|
||||
payload: AnswerRequest,
|
||||
request: Request,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
return await mirror_tree.resume_node(
|
||||
node_id=node_id, owner_scope=user.owner_scope, kernel=_get_kernel(request),
|
||||
instructions=payload.instructions,
|
||||
)
|
||||
|
||||
|
||||
@node_router.post("/{node_id}/edit-fork", status_code=201)
|
||||
async def edit_fork_api(
|
||||
node_id: str, payload: EditForkRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await mirror_tree.edit_fork(
|
||||
node_id=node_id, owner_scope=user.owner_scope, new_content=payload.new_content
|
||||
)
|
||||
|
||||
|
||||
@node_router.post("/{node_id}/citations", status_code=201)
|
||||
async def create_citation_api(
|
||||
node_id: str, payload: CitationCreate, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await mirror_tree.create_citation(
|
||||
node_id=node_id, owner_scope=user.owner_scope, data=payload.model_dump()
|
||||
)
|
||||
|
||||
|
||||
@node_router.get("/{node_id}/citations")
|
||||
async def list_citations_api(
|
||||
node_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> list[dict]:
|
||||
return await mirror_tree.list_citations(node_id, user.owner_scope)
|
||||
Reference in new issue
Block a user