Files
hunjing/backend/app/api/v1/endpoints/mirror_trees.py
T

241 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""镜树 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)