241 lines
7.5 KiB
Python
241 lines
7.5 KiB
Python
"""镜树 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)
|