From 06117377380c9d8bd45d924e28fc51982362a150 Mon Sep 17 00:00:00 2001 From: Marcus <1922576605@qq.com> Date: Sun, 18 Oct 2026 05:24:29 -0700 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E4=BC=98=E5=8C=96=E6=BC=94?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/public.py | 113 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/app/api/public.py b/app/api/public.py index a0272ba..70be7c6 100644 --- a/app/api/public.py +++ b/app/api/public.py @@ -1,14 +1,24 @@ """匿名接口,统一放在 /api/public 下,一眼能看出哪些不鉴权""" -from typing import Annotated, Any +from typing import Annotated, Any, Literal -from fastapi import APIRouter, Query, Response +from fastapi import APIRouter, Depends, Query, Request, Response from fastapi.responses import FileResponse +from app.api.files import MAX_OFFSET, MAX_PAGE_ROWS, _detail, _public, _service from app.api.screens import Service, thumbnail_url from app.common.response import ApiResponse, success_response from app.config import settings from app.core.exceptions import BizException +from app.schemas.conversation import ConversationDetail, ConversationList +from app.schemas.data_file import ( + AggregateRequest, + AggregateResult, + DataFileDetail, + DataFileInfo, + DataFileList, + DataTableRows, +) from app.schemas.saved_screen import ( DemoScreen, DemoScreenList, @@ -56,6 +66,105 @@ async def list_demo_screens( ) +def demo_owner() -> int: + """演示账号的用户 ID;没配置时没有演示数据,按不存在处理""" + owner_id = settings.demo_owner_id + if owner_id is None: + raise BizException(404, "没有演示数据") + return owner_id + + +DemoOwner = Annotated[int, Depends(demo_owner)] + + +# 以下是演示账号的对话和数据文件,只读:形状与登录后的 /conversations、/files 一致, +# 前端未登录时换个前缀就能复用;归属一律按演示账号校验,别人的会话和文件与不存在同样是 404 +@router.get("/demo/conversations") +async def list_demo_conversations( + request: Request, + screen_id: Annotated[str, Query(min_length=1)], + limit: Annotated[int, Query(ge=1, le=20)] = 1, +) -> ApiResponse[ConversationList]: + """演示大屏下的会话,最近更新的在前;没配演示账号时为空""" + owner_id = settings.demo_owner_id + if owner_id is None: + return success_response(ConversationList(conversations=[], total=0)) + store = request.app.state.conversation_store + return success_response( + ConversationList( + conversations=await store.list_by_user(owner_id, limit, 0, screen_id), + total=await store.count_by_user(owner_id, screen_id), + ) + ) + + +@router.get("/demo/conversations/{conversation_id}") +async def get_demo_conversation( + conversation_id: str, request: Request, owner_id: DemoOwner +) -> ApiResponse[ConversationDetail]: + store = request.app.state.conversation_store + if not await store.exists(conversation_id, owner_id): + raise BizException(404, "会话不存在或已过期") + turns = await store.load_turns(conversation_id, None) + return success_response( + ConversationDetail(conversation_id=conversation_id, turns=turns) + ) + + +@router.get("/demo/files") +async def list_demo_files( + request: Request, + conversation_id: Annotated[str, Query(min_length=1)], + owner_id: DemoOwner, +) -> ApiResponse[DataFileList]: + # 不带上传任务:那是演示账号自己正在进行的,访客不该看到 + records = await _service(request).list_files(owner_id, conversation_id) + return success_response( + DataFileList( + files=[DataFileInfo.model_validate(_public(record)) for record in records] + ) + ) + + +@router.get("/demo/files/{file_id}") +async def get_demo_file( + file_id: str, request: Request, owner_id: DemoOwner +) -> ApiResponse[DataFileDetail]: + return success_response(_detail(await _service(request).get_file(owner_id, file_id))) + + +@router.get("/demo/files/{file_id}/tables/{table}/rows") +async def get_demo_table_rows( + file_id: str, + table: str, + request: Request, + owner_id: DemoOwner, + offset: Annotated[int, Query(ge=0, le=MAX_OFFSET)] = 0, + limit: Annotated[int, Query(ge=1, le=MAX_PAGE_ROWS)] = 100, + sort_by: Annotated[str | None, Query(min_length=1)] = None, + order: Annotated[Literal["asc", "desc"], Query()] = "asc", +) -> ApiResponse[DataTableRows]: + return success_response( + await _service(request).get_rows( + owner_id, file_id, table, offset, limit, sort_by, order == "desc" + ) + ) + + +@router.post("/demo/files/{file_id}/tables/{table}/aggregate") +async def aggregate_demo_table( + file_id: str, + table: str, + body: AggregateRequest, + request: Request, + owner_id: DemoOwner, +) -> ApiResponse[AggregateResult]: + """POST 但只读:汇总结果算好就返回,不改任何数据""" + return success_response( + await _service(request).aggregate(owner_id, file_id, table, body) + ) + + @router.get("/screens/{share_id}") async def get_public_screen( share_id: str, service: Service