mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 08:33:53 +08:00
feat: add FastAPI manager API compatibility baseline
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""HTTP routers grouped by the Java business domains."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
def application_routers() -> list[APIRouter]:
|
||||
"""Return every migrated business router; imports stay explicit for coverage auditing."""
|
||||
from app.routers.agent import router as agent_router
|
||||
from app.routers.config import config_router
|
||||
from app.routers.correctword import correctword_router
|
||||
from app.routers.device import device_router
|
||||
from app.routers.knowledge import knowledge_router
|
||||
from app.routers.model import model_router
|
||||
from app.routers.security import security_router
|
||||
from app.routers.sys import sys_router
|
||||
from app.routers.timbre import timbre_router
|
||||
from app.routers.voiceclone import voiceclone_router
|
||||
|
||||
return [
|
||||
security_router,
|
||||
sys_router,
|
||||
config_router,
|
||||
agent_router,
|
||||
device_router,
|
||||
voiceclone_router,
|
||||
model_router,
|
||||
timbre_router,
|
||||
correctword_router,
|
||||
knowledge_router,
|
||||
]
|
||||
@@ -0,0 +1,386 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.errors import ErrorCode
|
||||
from app.core.responses import JavaJSONResponse, error_response, ok
|
||||
from app.core.security import AuthUser, require_normal, require_super_admin
|
||||
from app.schemas.agent import (
|
||||
AgentChatHistoryReport,
|
||||
AgentCreate,
|
||||
AgentMemory,
|
||||
AgentSnapshotPage,
|
||||
AgentSnapshotRestore,
|
||||
AgentTagAssignment,
|
||||
AgentTemplate,
|
||||
AgentUpdate,
|
||||
AgentVoicePrintSave,
|
||||
AgentVoicePrintUpdate,
|
||||
)
|
||||
from app.services.agent import AgentService, run_chat_summary_task
|
||||
|
||||
router = APIRouter(tags=["agent"])
|
||||
DbSession = Annotated[AsyncSession, Depends(get_db)]
|
||||
NormalUser = Annotated[AuthUser, Depends(require_normal)]
|
||||
SuperUser = Annotated[AuthUser, Depends(require_super_admin)]
|
||||
|
||||
|
||||
def _service(session: AsyncSession, user: AuthUser | None, request: Request) -> AgentService:
|
||||
return AgentService(session, user, language=request.headers.get("Accept-Language"))
|
||||
|
||||
|
||||
@router.post("/agent/chat-history/report")
|
||||
async def report_chat_history(report: AgentChatHistoryReport, request: Request, session: DbSession) -> JavaJSONResponse:
|
||||
return ok(await _service(session, None, request).report_chat(report))
|
||||
|
||||
|
||||
@router.post("/agent/chat-history/getDownloadUrl/{agentId}/{sessionId}")
|
||||
async def issue_chat_history_download(
|
||||
agentId: str, sessionId: str, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
service = _service(session, user, request)
|
||||
if not await service.has_agent_permission(agentId):
|
||||
return error_response(request, 10132)
|
||||
return ok(await service.issue_history_token(agentId, sessionId))
|
||||
|
||||
|
||||
@router.get("/agent/chat-history/download/{uuid}/current")
|
||||
async def download_current_chat_history(uuid: str, request: Request, session: DbSession) -> Response:
|
||||
content = await _service(session, None, request).consume_history_download(uuid, previous=False)
|
||||
return Response(
|
||||
content.encode("utf-8"),
|
||||
media_type="text/plain;charset=UTF-8",
|
||||
headers={"Content-Disposition": "attachment;filename=history.txt"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agent/chat-history/download/{uuid}/previous")
|
||||
async def download_previous_chat_history(uuid: str, request: Request, session: DbSession) -> Response:
|
||||
content = await _service(session, None, request).consume_history_download(uuid, previous=True)
|
||||
return Response(
|
||||
content.encode("utf-8"),
|
||||
media_type="text/plain;charset=UTF-8",
|
||||
headers={"Content-Disposition": "attachment;filename=history.txt"},
|
||||
)
|
||||
|
||||
|
||||
# Static paths are deliberately registered before /agent/{id}; Starlette resolves in declaration order.
|
||||
@router.get("/agent/template/page")
|
||||
async def template_page(
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: SuperUser,
|
||||
page: int = Query(default=1),
|
||||
limit: int = Query(default=10),
|
||||
agentName: str | None = Query(default=None),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).template_page(page, limit, agentName))
|
||||
|
||||
|
||||
@router.post("/agent/template/batch-remove")
|
||||
async def batch_delete_templates(
|
||||
ids: list[str], request: Request, session: DbSession, user: SuperUser
|
||||
) -> JavaJSONResponse:
|
||||
deleted = await _service(session, user, request).batch_delete_templates(ids)
|
||||
return (
|
||||
ok("批量删除成功") if deleted else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "批量删除模板失败")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agent/template/{id}")
|
||||
async def template_detail(id: str, request: Request, session: DbSession, user: SuperUser) -> JavaJSONResponse:
|
||||
result = await _service(session, user, request).template_detail(id)
|
||||
return ok(result) if result is not None else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "模板不存在")
|
||||
|
||||
|
||||
@router.post("/agent/template")
|
||||
async def create_template(
|
||||
template: AgentTemplate, request: Request, session: DbSession, user: SuperUser
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).create_template(template))
|
||||
|
||||
|
||||
@router.put("/agent/template")
|
||||
async def update_template(
|
||||
template: AgentTemplate, request: Request, session: DbSession, user: SuperUser
|
||||
) -> JavaJSONResponse:
|
||||
# MyBatis-Plus raises before returning a boolean when updateById receives
|
||||
# an entity without its @TableId. Keep Java's generic error envelope for
|
||||
# that exact input; an unknown but non-empty id still returns the controller's
|
||||
# explicit "更新模板失败" message below.
|
||||
if template.id is None:
|
||||
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR)
|
||||
updated = await _service(session, user, request).update_template(template)
|
||||
return ok(template) if updated else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "更新模板失败")
|
||||
|
||||
|
||||
@router.delete("/agent/template/{id}")
|
||||
async def delete_template(id: str, request: Request, session: DbSession, user: SuperUser) -> JavaJSONResponse:
|
||||
service = _service(session, user, request)
|
||||
if await service.template_detail(id) is None:
|
||||
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "模板不存在")
|
||||
return (
|
||||
ok("删除模板成功")
|
||||
if await service.delete_template(id)
|
||||
else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "删除模板失败")
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agent/voice-print")
|
||||
async def create_voiceprint(
|
||||
dto: AgentVoicePrintSave, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
created = await _service(session, user, request).create_voiceprint(dto)
|
||||
return ok() if created else error_response(request, 10057)
|
||||
|
||||
|
||||
@router.put("/agent/voice-print")
|
||||
async def update_voiceprint(
|
||||
dto: AgentVoicePrintUpdate, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
updated = await _service(session, user, request).update_voiceprint(dto)
|
||||
return ok() if updated else error_response(request, 10058)
|
||||
|
||||
|
||||
@router.delete("/agent/voice-print/{id}")
|
||||
async def delete_voiceprint(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
deleted = await _service(session, user, request).delete_voiceprint(id)
|
||||
return ok() if deleted else error_response(request, 10059)
|
||||
|
||||
|
||||
@router.get("/agent/voice-print/list/{id}")
|
||||
async def list_voiceprints(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).voiceprint_list(id))
|
||||
|
||||
|
||||
@router.get("/agent/mcp/address/{agentId}")
|
||||
async def mcp_address(agentId: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
service = _service(session, user, request)
|
||||
if not await service.has_agent_permission(agentId):
|
||||
return error_response(request, 10200)
|
||||
address = await service.mcp_address(agentId)
|
||||
return ok(address) if address is not None else error_response(request, 10201)
|
||||
|
||||
|
||||
@router.get("/agent/mcp/tools/{agentId}")
|
||||
async def mcp_tools(agentId: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
service = _service(session, user, request)
|
||||
if not await service.has_agent_permission(agentId):
|
||||
return error_response(request, 10202)
|
||||
return ok(await service.mcp_tools(agentId))
|
||||
|
||||
|
||||
@router.get("/agent/tag/list")
|
||||
async def all_tags(request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).all_tags())
|
||||
|
||||
|
||||
@router.post("/agent/tag")
|
||||
async def create_tag(
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: NormalUser,
|
||||
params: dict[str, str] = Body(...),
|
||||
) -> JavaJSONResponse:
|
||||
tag_name = params.get("tagName")
|
||||
if tag_name is None or not tag_name.strip():
|
||||
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "标签名称不能为空")
|
||||
return ok(await _service(session, user, request).save_tag(tag_name))
|
||||
|
||||
|
||||
@router.delete("/agent/tag/{id}")
|
||||
async def delete_tag(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
await _service(session, user, request).delete_tag(id)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/agent/audio/{audioId}")
|
||||
async def issue_audio_token(audioId: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
token = await _service(session, user, request).issue_audio_token(audioId)
|
||||
return ok(token) if token is not None else error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "音频不存在")
|
||||
|
||||
|
||||
@router.get("/agent/play/{uuid}")
|
||||
async def play_agent_audio(uuid: str, request: Request, session: DbSession) -> Response:
|
||||
audio = await _service(session, None, request).consume_audio_token(uuid)
|
||||
if audio is None:
|
||||
return Response(status_code=404)
|
||||
return Response(
|
||||
audio,
|
||||
media_type="application/octet-stream",
|
||||
headers={"Content-Disposition": 'attachment; filename="play.wav"'},
|
||||
)
|
||||
|
||||
|
||||
@router.put("/agent/saveMemory/{macAddress}")
|
||||
async def update_memory(
|
||||
macAddress: str, dto: AgentMemory, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session, user, request).update_memory_by_mac(macAddress, dto)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/agent/chat-summary/{sessionId}/save")
|
||||
async def save_chat_summary(
|
||||
sessionId: str, background_tasks: BackgroundTasks, request: Request, session: DbSession
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session, None, request).session_agent(sessionId)
|
||||
background_tasks.add_task(run_chat_summary_task, sessionId)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/agent/chat-title/{sessionId}/generate")
|
||||
async def generate_chat_title(sessionId: str, request: Request, session: DbSession) -> JavaJSONResponse:
|
||||
service = _service(session, None, request)
|
||||
await service.session_agent(sessionId)
|
||||
await service.generate_chat_title(sessionId)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.get("/agent/all")
|
||||
async def admin_agent_list(
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: SuperUser,
|
||||
page: int = Query(default=1),
|
||||
limit: int = Query(default=10),
|
||||
orderField: str | None = Query(default=None),
|
||||
order: str | None = Query(default=None),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).admin_agents(page, limit, orderField, order))
|
||||
|
||||
|
||||
@router.get("/agent/list")
|
||||
async def user_agent_list(
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: NormalUser,
|
||||
keyword: str | None = Query(default=None),
|
||||
searchType: str = Query(default="name"),
|
||||
) -> JavaJSONResponse:
|
||||
del searchType # Java accepts the parameter but the consolidated implementation ignores it.
|
||||
return ok(await _service(session, user, request).user_agents(keyword))
|
||||
|
||||
|
||||
@router.get("/agent/template")
|
||||
async def template_list(request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).templates())
|
||||
|
||||
|
||||
@router.get("/agent/{agentId}/snapshots")
|
||||
async def snapshot_page(
|
||||
agentId: str,
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: NormalUser,
|
||||
page: int | None = Query(default=1),
|
||||
limit: int | None = Query(default=10),
|
||||
maxVersionNo: int | None = Query(default=None),
|
||||
) -> JavaJSONResponse:
|
||||
params = AgentSnapshotPage(page=page, limit=limit, max_version_no=maxVersionNo)
|
||||
return ok(await _service(session, user, request).snapshot_page(agentId, params))
|
||||
|
||||
|
||||
@router.get("/agent/{agentId}/snapshots/{snapshotId}")
|
||||
async def snapshot_detail(
|
||||
agentId: str, snapshotId: str, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).snapshot_detail(agentId, snapshotId))
|
||||
|
||||
|
||||
@router.post("/agent/{agentId}/snapshots/{snapshotId}/restore")
|
||||
async def restore_snapshot(
|
||||
agentId: str,
|
||||
snapshotId: str,
|
||||
dto: AgentSnapshotRestore,
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: NormalUser,
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session, user, request).restore_snapshot(agentId, snapshotId, dto.current_state_token)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.delete("/agent/{agentId}/snapshots/{snapshotId}")
|
||||
async def delete_snapshot(
|
||||
agentId: str, snapshotId: str, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session, user, request).delete_snapshot(agentId, snapshotId)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.get("/agent/{id}/sessions")
|
||||
async def agent_sessions(
|
||||
id: str,
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
user: NormalUser,
|
||||
page: str | None = Query(default=None),
|
||||
limit: str | None = Query(default=None),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).sessions(id, page, limit))
|
||||
|
||||
|
||||
@router.get("/agent/{id}/chat-history/user")
|
||||
async def recent_agent_history(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
service = _service(session, user, request)
|
||||
if not await service.has_agent_permission(id):
|
||||
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "没有权限查看该智能体的聊天记录")
|
||||
return ok(await service.recent_user_history(id))
|
||||
|
||||
|
||||
@router.get("/agent/{id}/chat-history/audio")
|
||||
async def agent_audio_content(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).audio_content(id))
|
||||
|
||||
|
||||
@router.get("/agent/{id}/chat-history/{sessionId}")
|
||||
async def agent_history(
|
||||
id: str, sessionId: str, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
service = _service(session, user, request)
|
||||
if not await service.has_agent_permission(id):
|
||||
return error_response(request, ErrorCode.INTERNAL_SERVER_ERROR, "没有权限查看该智能体的聊天记录")
|
||||
return ok(await service.history(id, sessionId))
|
||||
|
||||
|
||||
@router.get("/agent/{id}/tags")
|
||||
async def agent_tags(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).agent_tags(id))
|
||||
|
||||
|
||||
@router.put("/agent/{id}/tags")
|
||||
async def save_agent_tags(
|
||||
id: str, dto: AgentTagAssignment, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session, user, request).save_agent_tags(id, dto.tag_ids, dto.tag_names)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.post("/agent")
|
||||
async def create_agent(dto: AgentCreate, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).create_agent(dto))
|
||||
|
||||
|
||||
@router.put("/agent/{id}")
|
||||
async def update_agent(
|
||||
id: str, dto: AgentUpdate, request: Request, session: DbSession, user: NormalUser
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session, user, request).update_agent(id, dto)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.delete("/agent/{id}")
|
||||
async def delete_agent(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
await _service(session, user, request).delete_agent(id)
|
||||
return ok()
|
||||
|
||||
|
||||
@router.get("/agent/{id}")
|
||||
async def agent_detail(id: str, request: Request, session: DbSession, user: NormalUser) -> JavaJSONResponse:
|
||||
return ok(await _service(session, user, request).agent_detail(id))
|
||||
@@ -0,0 +1,34 @@
|
||||
# ruff: noqa: B008
|
||||
# FastAPI evaluates dependency marker defaults intentionally when registering routes.
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.responses import JavaJSONResponse, ok
|
||||
from app.core.serialization import preserve_java_map_keys
|
||||
from app.repositories.config import ConfigRepository
|
||||
from app.schemas.config import AgentModelsRequest, CorrectWordsRequest
|
||||
from app.services.config import ConfigService
|
||||
|
||||
config_router = APIRouter()
|
||||
|
||||
|
||||
def _service(session: AsyncSession) -> ConfigService:
|
||||
return ConfigService(ConfigRepository(session))
|
||||
|
||||
|
||||
@config_router.post("/config/server-base")
|
||||
async def server_base(session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
return ok(preserve_java_map_keys(await _service(session).get_config(use_cache=True)))
|
||||
|
||||
|
||||
@config_router.post("/config/agent-models")
|
||||
async def agent_models(dto: AgentModelsRequest, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
return ok(preserve_java_map_keys(await _service(session).get_agent_models(dto.mac_address, dto.selected_module)))
|
||||
|
||||
|
||||
@config_router.post("/config/correct-words")
|
||||
async def correct_words(dto: CorrectWordsRequest, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
return ok(await _service(session).get_correct_words(dto.mac_address))
|
||||
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.responses import JavaJSONResponse, ok
|
||||
from app.core.security import require_normal
|
||||
from app.repositories.correctword import CorrectWordRepository
|
||||
from app.schemas.correctword import CorrectWordFileBody
|
||||
from app.services.correctword import CorrectWordService
|
||||
|
||||
correctword_router = APIRouter()
|
||||
|
||||
|
||||
def _java_urlencode(value: str) -> str:
|
||||
# java.net.URLEncoder leaves alphanumerics plus .-*_ unescaped, encodes
|
||||
# spaces as '+', and encodes '~'. The controller then replaces '+' with
|
||||
# '%20'. urllib always leaves '~', so handle that final difference here.
|
||||
return quote(value, safe="*.-_").replace("~", "%7E")
|
||||
|
||||
|
||||
def _service(session: AsyncSession) -> CorrectWordService:
|
||||
return CorrectWordService(CorrectWordRepository(session))
|
||||
|
||||
|
||||
@correctword_router.post("/correct-word/file")
|
||||
async def create_file(
|
||||
body: CorrectWordFileBody, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session).create(body, require_normal(request)))
|
||||
|
||||
|
||||
@correctword_router.put("/correct-word/file/{file_id}")
|
||||
async def update_file(
|
||||
file_id: str,
|
||||
body: CorrectWordFileBody,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session).update(file_id, body, require_normal(request))
|
||||
return ok()
|
||||
|
||||
|
||||
@correctword_router.get("/correct-word/file/list")
|
||||
async def list_files(
|
||||
request: Request,
|
||||
page: str | None = None,
|
||||
limit: str | None = None,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session).page(require_normal(request), page, limit))
|
||||
|
||||
|
||||
@correctword_router.get("/correct-word/file/select")
|
||||
async def select_files(request: Request, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
return ok(await _service(session).all(require_normal(request)))
|
||||
|
||||
|
||||
@correctword_router.get("/correct-word/file/download/{file_id}")
|
||||
async def download_file(
|
||||
file_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> Response:
|
||||
require_normal(request)
|
||||
item = await _service(session).get(file_id)
|
||||
if item is None or not item["content"]:
|
||||
return Response(status_code=404)
|
||||
body = "\n".join(item["content"]).encode("utf-8")
|
||||
file_name = str(item["fileName"])
|
||||
ascii_name = "".join(character if ord(character) < 128 else "_" for character in file_name)
|
||||
disposition = f"attachment; filename=\"{ascii_name}\"; filename*=UTF-8''{_java_urlencode(file_name)}"
|
||||
return Response(
|
||||
body,
|
||||
media_type="application/octet-stream",
|
||||
headers={"Content-Disposition": disposition, "Content-Length": str(len(body))},
|
||||
)
|
||||
|
||||
|
||||
@correctword_router.delete("/correct-word/file/{file_id}")
|
||||
async def delete_file(
|
||||
file_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
await _service(session).delete([file_id])
|
||||
return ok()
|
||||
|
||||
|
||||
@correctword_router.post("/correct-word/file/batch-delete")
|
||||
async def batch_delete_files(
|
||||
file_ids: list[str], request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
await _service(session).delete(file_ids)
|
||||
return ok()
|
||||
@@ -0,0 +1,505 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, Header, Query, Request, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.i18n import resolve_language
|
||||
from app.core.responses import JavaJSONResponse, envelope, error_response, ok
|
||||
from app.core.security import require_normal, require_super_admin
|
||||
from app.schemas.device import (
|
||||
DeviceAddressBookAliasRequest,
|
||||
DeviceAddressBookPermissionRequest,
|
||||
DeviceManualAddRequest,
|
||||
DeviceRegisterRequest,
|
||||
DeviceReportRequest,
|
||||
DeviceToolCallRequest,
|
||||
DeviceUnbindRequest,
|
||||
DeviceUpdateRequest,
|
||||
OtaRecord,
|
||||
)
|
||||
from app.services.device import MAC_PATTERN, DeviceService, is_blank
|
||||
|
||||
device_router = APIRouter()
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_db)]
|
||||
FirmwareUpload = Annotated[UploadFile, File()]
|
||||
CallerMacQuery = Annotated[str, Query(alias="callerMac")]
|
||||
DeviceIdHeader = Annotated[str | None, Header(alias="Device-Id")]
|
||||
ClientIdHeader = Annotated[str | None, Header(alias="Client-Id")]
|
||||
|
||||
|
||||
def _query_map(request: Request) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in request.query_params.multi_items():
|
||||
if key in result:
|
||||
previous = result[key]
|
||||
result[key] = [*previous, value] if isinstance(previous, list) else [previous, value]
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _raw_ota(payload: dict[str, Any]) -> Response:
|
||||
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
return Response(
|
||||
body,
|
||||
status_code=200,
|
||||
media_type="application/json",
|
||||
headers={"Content-Length": str(len(body))},
|
||||
)
|
||||
|
||||
|
||||
@device_router.post("/device/bind/{agent_id}/{device_code}")
|
||||
async def bind_device(
|
||||
agent_id: str,
|
||||
device_code: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
await DeviceService(session).activate_bound_device(agent_id=agent_id, activation_code=device_code, user=user)
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.post("/device/register")
|
||||
async def register_device(
|
||||
body: DeviceRegisterRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
if is_blank(body.mac_address):
|
||||
return error_response(request, 10175)
|
||||
return ok(await DeviceService(session).register_device(body.mac_address or ""))
|
||||
|
||||
|
||||
@device_router.get("/device/bind/{agent_id}")
|
||||
async def get_bound_devices(
|
||||
agent_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
return ok(await DeviceService(session).list_user_devices(user.id, agent_id))
|
||||
|
||||
|
||||
@device_router.post("/device/bind/{agent_id}")
|
||||
async def device_online(
|
||||
agent_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
await request.body()
|
||||
try:
|
||||
return ok(await DeviceService(session).get_online_data(agent_id, user))
|
||||
except Exception as exc:
|
||||
return error_response(request, 500, f"转发请求失败: {exc}")
|
||||
|
||||
|
||||
@device_router.post("/device/unbind")
|
||||
async def unbind_device(
|
||||
body: DeviceUnbindRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
# DeviceController does not apply @Valid to DeviceUnBindDTO. An empty
|
||||
# object reaches the service with a null id and is a successful no-op.
|
||||
await DeviceService(session).unbind(user_id=user.id, device_id=body.device_id or "")
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.put("/device/update/{device_id}")
|
||||
async def update_device(
|
||||
device_id: str,
|
||||
body: DeviceUpdateRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
validation = _validate_device_update(body, request.headers.get("Accept-Language"))
|
||||
if validation is not None:
|
||||
return error_response(request, 10034, validation)
|
||||
if not await DeviceService(session).update_device(device_id=device_id, request=body, user=user):
|
||||
return error_response(request, 500, "设备不存在")
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.put("/user/configDevice/{device_id}")
|
||||
async def configure_device(
|
||||
device_id: str,
|
||||
body: DeviceUpdateRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
validation = _validate_device_update(body, request.headers.get("Accept-Language"))
|
||||
if validation is not None:
|
||||
return error_response(request, 10034, validation)
|
||||
if not await DeviceService(session).update_device(device_id=device_id, request=body, user=user):
|
||||
return error_response(request, 500, "设备不存在")
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.post("/device/manual-add")
|
||||
async def manual_add_device(
|
||||
body: DeviceManualAddRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
await DeviceService(session).manual_add(request=body, user=user)
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.post("/device/tools/list/{device_id}")
|
||||
async def list_device_tools(
|
||||
device_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
tools = await DeviceService(session).get_tools(device_id=device_id, user=user)
|
||||
if tools is None:
|
||||
return error_response(request, 10194)
|
||||
return ok(tools)
|
||||
|
||||
|
||||
@device_router.post("/device/tools/call/{device_id}")
|
||||
async def call_device_tool(
|
||||
device_id: str,
|
||||
body: DeviceToolCallRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
if is_blank(body.name):
|
||||
return error_response(request, 10034, "工具名称不能为空")
|
||||
result = await DeviceService(session).call_tool(
|
||||
device_id=device_id,
|
||||
tool_name=body.name or "",
|
||||
arguments=body.arguments,
|
||||
user=user,
|
||||
)
|
||||
if result is None:
|
||||
return error_response(request, 10194)
|
||||
return JavaJSONResponse(envelope(result, msg="Tools called successfully"))
|
||||
|
||||
|
||||
# Static address-book paths deliberately precede /address-book/{mac_address}.
|
||||
@device_router.get("/device/address-book/call")
|
||||
async def call_address_book(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
caller_mac: CallerMacQuery,
|
||||
nickname: str,
|
||||
answer: bool = False,
|
||||
) -> JavaJSONResponse:
|
||||
result = await DeviceService(session).call_by_nickname(
|
||||
caller_mac=caller_mac,
|
||||
nickname=nickname,
|
||||
answer=answer,
|
||||
)
|
||||
return ok(result)
|
||||
|
||||
|
||||
@device_router.get("/device/address-book/lookup")
|
||||
async def lookup_address_book(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
caller_mac: CallerMacQuery,
|
||||
nickname: str,
|
||||
) -> JavaJSONResponse:
|
||||
result = await DeviceService(session).lookup_address_book(caller_mac=caller_mac, nickname=nickname)
|
||||
if result is None:
|
||||
return error_response(request, 500, "未找到对应设备")
|
||||
return ok(result)
|
||||
|
||||
|
||||
@device_router.put("/device/address-book/alias")
|
||||
async def update_address_alias(
|
||||
body: DeviceAddressBookAliasRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
if is_blank(body.target_mac):
|
||||
return error_response(request, 10034, "目标MAC地址不能为空")
|
||||
if is_blank(body.mac_address):
|
||||
return error_response(request, 10034, "MAC地址不能为空")
|
||||
service = DeviceService(session)
|
||||
caller = await service.repository.get_device_by_mac(body.mac_address or "")
|
||||
if caller is None or int(caller.get("user_id") or -1) != user.id:
|
||||
return error_response(request, 500, "无权限操作该设备")
|
||||
await service.save_address_book(
|
||||
mac_address=body.mac_address or "",
|
||||
target_mac=body.target_mac or "",
|
||||
alias=body.alias,
|
||||
has_permission=None,
|
||||
actor=user.id,
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.put("/device/address-book/permission")
|
||||
async def update_address_permission(
|
||||
body: DeviceAddressBookPermissionRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
if is_blank(body.mac_address):
|
||||
return error_response(request, 10034, "MAC地址不能为空")
|
||||
if is_blank(body.target_mac):
|
||||
return error_response(request, 10034, "目标MAC地址不能为空")
|
||||
service = DeviceService(session)
|
||||
caller = await service.repository.get_device_by_mac(body.mac_address or "")
|
||||
if caller is None or int(caller.get("user_id") or -1) != user.id:
|
||||
return error_response(request, 500, "无权限操作该设备")
|
||||
await service.save_address_book(
|
||||
mac_address=body.mac_address or "",
|
||||
target_mac=body.target_mac or "",
|
||||
alias=None,
|
||||
has_permission=body.has_permission,
|
||||
actor=user.id,
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.get("/device/address-book/{mac_address}")
|
||||
async def get_address_book(
|
||||
mac_address: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
return ok(await DeviceService(session).address_book(mac_address))
|
||||
|
||||
|
||||
@device_router.post("/ota/")
|
||||
async def check_ota_version(
|
||||
report: DeviceReportRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
background_tasks: BackgroundTasks,
|
||||
device_id: DeviceIdHeader = None,
|
||||
client_id: ClientIdHeader = None,
|
||||
) -> Response:
|
||||
if is_blank(device_id):
|
||||
# Java's required @RequestHeader fails before the controller's blank
|
||||
# guard and is translated by its global handler into this envelope.
|
||||
return error_response(request, 500)
|
||||
if MAC_PATTERN.fullmatch(device_id or "") is None:
|
||||
return _raw_ota({"error": "Invalid device ID"})
|
||||
selected_client = device_id if is_blank(client_id) else client_id
|
||||
client_ip = request.client.host if request.client is not None else "unknown"
|
||||
service = DeviceService(session)
|
||||
|
||||
def defer_connection_update(device: str, agent: str | None, version: str | None) -> None:
|
||||
background_tasks.add_task(
|
||||
DeviceService.persist_connection_update_background,
|
||||
device,
|
||||
agent,
|
||||
version,
|
||||
)
|
||||
|
||||
payload = await service.check_ota(
|
||||
device_id=device_id or "",
|
||||
client_id=selected_client or device_id or "",
|
||||
report=report,
|
||||
request_url=str(request.url),
|
||||
client_ip=client_ip,
|
||||
defer_connection_update=defer_connection_update,
|
||||
)
|
||||
return _raw_ota(payload)
|
||||
|
||||
|
||||
@device_router.post("/ota/activate")
|
||||
async def activate_ota_device(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
device_id: DeviceIdHeader = None,
|
||||
client_id: ClientIdHeader = None,
|
||||
) -> Response:
|
||||
del client_id
|
||||
if is_blank(device_id):
|
||||
return error_response(request, 500)
|
||||
if await DeviceService(session).repository.get_device_by_mac(device_id or "") is None:
|
||||
return Response(status_code=202)
|
||||
return Response("success", media_type="text/plain;charset=UTF-8")
|
||||
|
||||
|
||||
@device_router.get("/ota/")
|
||||
async def ota_health(session: SessionDep) -> Response:
|
||||
return Response(
|
||||
await DeviceService(session).ota_health_text(),
|
||||
media_type="text/plain;charset=UTF-8",
|
||||
)
|
||||
|
||||
|
||||
# Static otaMag paths deliberately precede /otaMag/{id}.
|
||||
@device_router.get("/otaMag/getDownloadUrl/{ota_id}")
|
||||
async def get_ota_download_url(
|
||||
ota_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await DeviceService(session).create_ota_download_id(ota_id))
|
||||
|
||||
|
||||
@device_router.get("/otaMag/download/{download_id}")
|
||||
async def download_ota(download_id: str, session: SessionDep) -> Response:
|
||||
resolved = await DeviceService(session).resolve_ota_download(download_id)
|
||||
if resolved is None:
|
||||
return Response(status_code=404)
|
||||
path, filename = resolved
|
||||
try:
|
||||
content = path.read_bytes()
|
||||
except OSError:
|
||||
return Response(status_code=500)
|
||||
return Response(
|
||||
content,
|
||||
media_type="application/octet-stream",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@device_router.post("/otaMag/upload")
|
||||
async def upload_firmware(
|
||||
request: Request,
|
||||
file: FirmwareUpload,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
service = DeviceService(session)
|
||||
try:
|
||||
content = await file.read()
|
||||
return ok(await service.save_firmware_file(filename=file.filename, content=content))
|
||||
except ValueError as exc:
|
||||
return error_response(request, 500, str(exc))
|
||||
except OSError as exc:
|
||||
return error_response(request, 500, f"文件上传失败:{exc}")
|
||||
|
||||
|
||||
@device_router.post("/otaMag/uploadAssetsBin")
|
||||
async def upload_assets_firmware(
|
||||
request: Request,
|
||||
file: FirmwareUpload,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
service = DeviceService(session)
|
||||
try:
|
||||
content = await file.read()
|
||||
return ok(await service.save_assets_file(filename=file.filename, content=content, user=user))
|
||||
except ValueError as exc:
|
||||
return error_response(request, 500, str(exc))
|
||||
except OSError as exc:
|
||||
return error_response(request, 500, f"文件上传失败:{exc}")
|
||||
|
||||
|
||||
@device_router.get("/otaMag")
|
||||
async def page_ota(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await DeviceService(session).ota_page(_query_map(request)))
|
||||
|
||||
|
||||
@device_router.get("/otaMag/{ota_id}")
|
||||
async def get_ota(
|
||||
ota_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await DeviceService(session).get_ota_record(ota_id))
|
||||
|
||||
|
||||
@device_router.post("/otaMag")
|
||||
async def save_ota(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
record: OtaRecord | None = None,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_super_admin(request)
|
||||
if record is None:
|
||||
return error_response(request, 500, "固件信息不能为空")
|
||||
if is_blank(record.firmware_name):
|
||||
return error_response(request, 500, "固件名称不能为空")
|
||||
if is_blank(record.type):
|
||||
return error_response(request, 500, "固件类型不能为空")
|
||||
if is_blank(record.version):
|
||||
return error_response(request, 500, "版本号不能为空")
|
||||
try:
|
||||
await DeviceService(session).save_ota(record, user)
|
||||
return ok()
|
||||
except RuntimeError as exc:
|
||||
return error_response(request, 500, str(exc))
|
||||
|
||||
|
||||
@device_router.delete("/otaMag/{ota_id}")
|
||||
async def delete_ota(
|
||||
ota_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
ids = ota_id.split(",") if ota_id else []
|
||||
if not ids:
|
||||
return error_response(request, 500, "删除的固件ID不能为空")
|
||||
await DeviceService(session).delete_ota(ids)
|
||||
return ok()
|
||||
|
||||
|
||||
@device_router.put("/otaMag/{ota_id}")
|
||||
async def update_ota(
|
||||
ota_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
record: OtaRecord | None = None,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_super_admin(request)
|
||||
if record is None:
|
||||
return error_response(request, 500, "固件信息不能为空")
|
||||
try:
|
||||
await DeviceService(session).update_ota(ota_id, record, user)
|
||||
return ok()
|
||||
except RuntimeError as exc:
|
||||
return error_response(request, 500, str(exc))
|
||||
|
||||
|
||||
def _validate_device_update(body: DeviceUpdateRequest, accept_language: str | None) -> str | None:
|
||||
language = resolve_language(accept_language)
|
||||
if body.auto_update is not None and body.auto_update < 0:
|
||||
return {
|
||||
"zh-CN": "最小不能小于0",
|
||||
"zh-TW": "必須大於或等於 0",
|
||||
"de-DE": "muss größer-gleich 0 sein",
|
||||
"pt-BR": "deve ser maior que ou igual à 0",
|
||||
}.get(language, "must be greater than or equal to 0")
|
||||
if body.auto_update is not None and body.auto_update > 1:
|
||||
return {
|
||||
"zh-CN": "最大不能超过1",
|
||||
"zh-TW": "必須小於或等於 1",
|
||||
"de-DE": "muss kleiner-gleich 1 sein",
|
||||
"pt-BR": "deve ser menor que ou igual à 1",
|
||||
}.get(language, "must be less than or equal to 1")
|
||||
if body.alias is not None and len(body.alias.encode("utf-16-le")) // 2 > 64:
|
||||
return {
|
||||
"zh-CN": "个数必须在0和64之间",
|
||||
"zh-TW": "大小必須在 0 和 64 之間",
|
||||
"de-DE": "Größe muss zwischen 0 und 64 sein",
|
||||
"pt-BR": "tamanho deve ser entre 0 e 64",
|
||||
}.get(language, "size must be between 0 and 64")
|
||||
return None
|
||||
@@ -0,0 +1,267 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.errors import AppError
|
||||
from app.core.responses import JavaJSONResponse, envelope, ok
|
||||
from app.core.security import require_normal
|
||||
from app.repositories.knowledge import KnowledgeRepository
|
||||
from app.schemas.knowledge import DocumentBatchBody, KnowledgeBaseBody, RetrievalBody
|
||||
from app.services.knowledge import KnowledgeBaseService, KnowledgeDocumentService, dataset_dto
|
||||
|
||||
knowledge_router = APIRouter()
|
||||
|
||||
|
||||
def _base(session: AsyncSession) -> KnowledgeBaseService:
|
||||
return KnowledgeBaseService(KnowledgeRepository(session))
|
||||
|
||||
|
||||
def _documents(session: AsyncSession) -> KnowledgeDocumentService:
|
||||
return KnowledgeDocumentService(KnowledgeRepository(session))
|
||||
|
||||
|
||||
@knowledge_router.get("/datasets/rag-models")
|
||||
async def rag_models(request: Request, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
return ok(await _base(session).rag_models())
|
||||
|
||||
|
||||
@knowledge_router.delete("/datasets/batch")
|
||||
async def delete_datasets_batch(
|
||||
request: Request, ids: str = Query(), session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
if not ids.strip():
|
||||
raise AppError(10003)
|
||||
await _base(session).batch_delete(
|
||||
ids.split(","), user, request.headers.get("Accept-Language")
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@knowledge_router.get("/datasets")
|
||||
async def datasets_page(
|
||||
request: Request,
|
||||
name: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(
|
||||
await _base(session).page(
|
||||
require_normal(request),
|
||||
name,
|
||||
page,
|
||||
page_size,
|
||||
request.headers.get("Accept-Language"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@knowledge_router.post("/datasets")
|
||||
async def create_dataset(
|
||||
body: KnowledgeBaseBody, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _base(session).create(body, require_normal(request)))
|
||||
|
||||
|
||||
@knowledge_router.get("/datasets/{dataset_id}")
|
||||
async def get_dataset(
|
||||
dataset_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
return ok(dataset_dto(await _base(session).get_owned(dataset_id, require_normal(request))))
|
||||
|
||||
|
||||
@knowledge_router.put("/datasets/{dataset_id}")
|
||||
async def update_dataset(
|
||||
dataset_id: str,
|
||||
body: KnowledgeBaseBody,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _base(session).update(dataset_id, body, require_normal(request)))
|
||||
|
||||
|
||||
@knowledge_router.delete("/datasets/{dataset_id}")
|
||||
async def delete_dataset(
|
||||
dataset_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
await _base(session).delete(
|
||||
dataset_id, require_normal(request), request.headers.get("Accept-Language")
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@knowledge_router.get("/datasets/{dataset_id}/documents/status/{status}")
|
||||
async def documents_by_status(
|
||||
dataset_id: str,
|
||||
status: str,
|
||||
request: Request,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(
|
||||
await _documents(session).page(
|
||||
dataset_id,
|
||||
require_normal(request),
|
||||
name=None,
|
||||
status=status,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@knowledge_router.get("/datasets/{dataset_id}/documents")
|
||||
async def documents_page(
|
||||
dataset_id: str,
|
||||
request: Request,
|
||||
name: str | None = None,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(
|
||||
await _documents(session).page(
|
||||
dataset_id,
|
||||
require_normal(request),
|
||||
name=name,
|
||||
status=status,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@knowledge_router.post("/datasets/{dataset_id}/documents")
|
||||
async def upload_document(
|
||||
dataset_id: str,
|
||||
request: Request,
|
||||
file: Annotated[UploadFile, File()],
|
||||
name: Annotated[str | None, Form()] = None,
|
||||
chunk_method: Annotated[str | None, Form(alias="chunkMethod")] = None,
|
||||
meta_fields: Annotated[str | None, Form(alias="metaFields")] = None,
|
||||
parser_config: Annotated[str | None, Form(alias="parserConfig")] = None,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(
|
||||
await _documents(session).upload(
|
||||
dataset_id,
|
||||
require_normal(request),
|
||||
file,
|
||||
name=name,
|
||||
meta_fields=_parse_form_json(meta_fields),
|
||||
chunk_method=chunk_method,
|
||||
parser_config=_parse_form_json(parser_config),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@knowledge_router.delete("/datasets/{dataset_id}/documents")
|
||||
async def delete_documents(
|
||||
dataset_id: str,
|
||||
body: DocumentBatchBody,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _documents(session).delete(
|
||||
dataset_id,
|
||||
body.ids,
|
||||
require_normal(request),
|
||||
request.headers.get("Accept-Language"),
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@knowledge_router.delete("/datasets/{dataset_id}/documents/{document_id}")
|
||||
async def delete_document(
|
||||
dataset_id: str,
|
||||
document_id: str,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _documents(session).delete(
|
||||
dataset_id,
|
||||
[document_id],
|
||||
require_normal(request),
|
||||
request.headers.get("Accept-Language"),
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@knowledge_router.post("/datasets/{dataset_id}/chunks")
|
||||
async def parse_documents(
|
||||
dataset_id: str,
|
||||
body: dict[str, Any],
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
# Java validates dataset existence/ownership before it reads document_ids.
|
||||
# A missing dataset must therefore win over the controller's empty-body
|
||||
# business error.
|
||||
await _base(session).get_owned(dataset_id, user)
|
||||
document_ids = body.get("document_ids")
|
||||
if document_ids is not None and not isinstance(document_ids, list):
|
||||
# Spring fails Map<String,List<String>> deserialization before entering
|
||||
# the controller, which is handled as the generic code-500 envelope.
|
||||
raise RuntimeError("document_ids must be an array")
|
||||
if not document_ids:
|
||||
return JavaJSONResponse(envelope(None, code=500, msg="document_ids参数不能为空"))
|
||||
success = await _documents(session).parse(dataset_id, document_ids, user)
|
||||
return ok() if success else JavaJSONResponse(
|
||||
envelope(None, code=500, msg="文档解析失败,文档可能正在处理中")
|
||||
)
|
||||
|
||||
|
||||
@knowledge_router.get("/datasets/{dataset_id}/documents/{document_id}/chunks")
|
||||
async def list_chunks(
|
||||
dataset_id: str,
|
||||
document_id: str,
|
||||
request: Request,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
keywords: str | None = None,
|
||||
id: str | None = None, # noqa: A002 - exact Java query parameter
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(
|
||||
await _documents(session).chunks(
|
||||
dataset_id,
|
||||
document_id,
|
||||
require_normal(request),
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
keywords=keywords,
|
||||
chunk_id=id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@knowledge_router.post("/datasets/{dataset_id}/retrieval-test")
|
||||
async def retrieval_test(
|
||||
dataset_id: str,
|
||||
body: RetrievalBody,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _documents(session).retrieval(dataset_id, body, require_normal(request)))
|
||||
|
||||
|
||||
def _parse_form_json(value: str | None) -> dict[str, Any] | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
result = json.loads(value)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"解析JSON字符串失败: {value}") from exc
|
||||
if not isinstance(result, dict):
|
||||
raise RuntimeError(f"解析JSON字符串失败: {value}")
|
||||
return dict(result)
|
||||
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.responses import JavaJSONResponse, envelope, ok
|
||||
from app.core.security import require_normal, require_super_admin
|
||||
from app.repositories.config import ConfigRepository
|
||||
from app.repositories.model import ModelRepository
|
||||
from app.schemas.model import ModelConfigBody, ModelProviderBody
|
||||
from app.services.config import ConfigService
|
||||
from app.services.model import ModelProviderService, ModelService
|
||||
|
||||
model_router = APIRouter()
|
||||
|
||||
|
||||
def _models(session: AsyncSession) -> ModelService:
|
||||
return ModelService(ModelRepository(session))
|
||||
|
||||
|
||||
def _providers(session: AsyncSession) -> ModelProviderService:
|
||||
return ModelProviderService(ModelRepository(session))
|
||||
|
||||
|
||||
async def _refresh_server_config(session: AsyncSession) -> None:
|
||||
await ConfigService(ConfigRepository(session)).get_config(use_cache=False)
|
||||
|
||||
|
||||
@model_router.get("/models/names")
|
||||
async def model_names(
|
||||
request: Request,
|
||||
model_type: str = Query(alias="modelType"),
|
||||
model_name: str | None = Query(default=None, alias="modelName"),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
return ok(await _models(session).names(model_type, model_name))
|
||||
|
||||
|
||||
@model_router.get("/models/llm/names")
|
||||
async def llm_names(
|
||||
request: Request,
|
||||
model_name: str | None = Query(default=None, alias="modelName"),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
return ok(await _models(session).llm_names(model_name))
|
||||
|
||||
|
||||
@model_router.get("/models/list")
|
||||
async def model_list(
|
||||
request: Request,
|
||||
model_type: str = Query(alias="modelType"),
|
||||
model_name: str | None = Query(default=None, alias="modelName"),
|
||||
page: str = "0",
|
||||
limit: str = "10",
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _models(session).model_page(model_type, model_name, page, limit))
|
||||
|
||||
|
||||
@model_router.get("/models/provider/plugin/names")
|
||||
async def plugin_names(request: Request, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
return ok(await ModelRepository(session).list_plugins_for_user(user.id))
|
||||
|
||||
|
||||
@model_router.get("/models/provider")
|
||||
async def provider_list(
|
||||
request: Request,
|
||||
model_type: str | None = Query(default=None, alias="modelType"),
|
||||
name: str | None = None,
|
||||
page: str = "0",
|
||||
limit: str = "10",
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _providers(session).page(model_type, name, page, limit))
|
||||
|
||||
|
||||
@model_router.post("/models/provider")
|
||||
async def provider_add(
|
||||
body: ModelProviderBody, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _providers(session).add(body, require_super_admin(request)))
|
||||
|
||||
|
||||
@model_router.put("/models/provider")
|
||||
async def provider_edit(
|
||||
body: ModelProviderBody, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _providers(session).edit(body, require_super_admin(request)))
|
||||
|
||||
|
||||
@model_router.post("/models/provider/delete")
|
||||
async def provider_delete(
|
||||
ids: list[str], request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _providers(session).delete(ids)
|
||||
return ok()
|
||||
|
||||
|
||||
@model_router.get("/models/{model_type}/provideTypes")
|
||||
async def provider_types(
|
||||
model_type: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await ModelRepository(session).list_providers_by_type(model_type))
|
||||
|
||||
|
||||
@model_router.post("/models/{model_type}/{provide_code}")
|
||||
async def model_add(
|
||||
model_type: str,
|
||||
provide_code: str,
|
||||
body: ModelConfigBody,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
result = await _models(session).add(model_type, provide_code, body)
|
||||
await _refresh_server_config(session)
|
||||
return ok(result)
|
||||
|
||||
|
||||
@model_router.put("/models/enable/{model_id}/{status}")
|
||||
async def model_enable(
|
||||
model_id: str, status: int, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
message = await _models(session).enable(model_id, status)
|
||||
return JavaJSONResponse(envelope(None, code=500, msg=message)) if message else ok()
|
||||
|
||||
|
||||
@model_router.put("/models/{model_type}/{provide_code}/{model_id}")
|
||||
async def model_edit(
|
||||
model_type: str,
|
||||
provide_code: str,
|
||||
model_id: str,
|
||||
body: ModelConfigBody,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
result = await _models(session).edit(model_type, provide_code, model_id, body)
|
||||
await _refresh_server_config(session)
|
||||
return ok(result)
|
||||
|
||||
|
||||
@model_router.put("/models/default/{model_id}")
|
||||
async def model_default(
|
||||
model_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
message = await _models(session).set_default(model_id)
|
||||
if message:
|
||||
return JavaJSONResponse(envelope(None, code=500, msg=message))
|
||||
await _refresh_server_config(session)
|
||||
return ok()
|
||||
|
||||
|
||||
@model_router.get("/models/{model_id}")
|
||||
async def model_get(
|
||||
model_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _models(session).get_model(model_id))
|
||||
|
||||
|
||||
@model_router.delete("/models/{model_id}")
|
||||
async def model_delete(
|
||||
model_id: str, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _models(session).delete(model_id)
|
||||
return ok()
|
||||
@@ -0,0 +1,111 @@
|
||||
# ruff: noqa: B008
|
||||
# FastAPI evaluates dependency marker defaults intentionally when registering routes.
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.errors import AppError
|
||||
from app.core.responses import JavaJSONResponse, ok
|
||||
from app.core.security import require_normal
|
||||
from app.repositories.security import SecurityRepository
|
||||
from app.schemas.security import (
|
||||
LoginRequest,
|
||||
PasswordChangeRequest,
|
||||
RetrievePasswordRequest,
|
||||
SmsVerificationRequest,
|
||||
)
|
||||
from app.services.security import CaptchaService, SecurityService
|
||||
|
||||
security_router = APIRouter()
|
||||
|
||||
|
||||
def _service(session: AsyncSession) -> SecurityService:
|
||||
return SecurityService(SecurityRepository(session))
|
||||
|
||||
|
||||
@security_router.get("/user/captcha")
|
||||
async def captcha(uuid: str | None = Query(default=None)) -> Response:
|
||||
if uuid is None or not uuid.strip():
|
||||
raise AppError(10006)
|
||||
content = await CaptchaService().create(uuid)
|
||||
return Response(
|
||||
content,
|
||||
media_type="image/gif",
|
||||
headers={
|
||||
"Pragma": "No-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
"Expires": "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@security_router.post("/user/smsVerification")
|
||||
async def sms_verification(dto: SmsVerificationRequest, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
await _service(session).send_sms_verification(dto)
|
||||
return ok()
|
||||
|
||||
|
||||
@security_router.post("/user/login")
|
||||
async def login(
|
||||
dto: LoginRequest,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
return ok(await _service(session).login(dto, request))
|
||||
|
||||
|
||||
@security_router.post("/user/register")
|
||||
async def register(dto: LoginRequest, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
await _service(session).register(dto)
|
||||
return ok()
|
||||
|
||||
|
||||
@security_router.get("/user/info")
|
||||
async def info(request: Request) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
return ok(
|
||||
{
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"superAdmin": user.super_admin,
|
||||
"token": user.token,
|
||||
"status": user.status,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@security_router.put("/user/change-password")
|
||||
async def change_password(
|
||||
dto: PasswordChangeRequest,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session).change_password(
|
||||
require_normal(request),
|
||||
dto,
|
||||
request.headers.get("Accept-Language"),
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@security_router.put("/user/retrieve-password")
|
||||
async def retrieve_password(
|
||||
dto: RetrievePasswordRequest,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session).retrieve_password(dto, request.headers.get("Accept-Language"))
|
||||
return ok()
|
||||
|
||||
|
||||
@security_router.get("/user/pub-config")
|
||||
async def public_config(session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
return ok(await _service(session).public_config())
|
||||
|
||||
|
||||
@security_router.get("/api/ping")
|
||||
async def api_ping() -> JavaJSONResponse:
|
||||
return ok("pong")
|
||||
@@ -0,0 +1,334 @@
|
||||
# ruff: noqa: B008
|
||||
# FastAPI evaluates dependency and body marker defaults intentionally when registering routes.
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.errors import AppError
|
||||
from app.core.responses import JavaJSONResponse, envelope, ok
|
||||
from app.core.security import require_normal, require_super_admin
|
||||
from app.repositories.sys import SysRepository
|
||||
from app.schemas.sys import DictDataPayload, DictTypePayload, EmitServerActionRequest, SysParamPayload
|
||||
from app.services.sys import AdminService, DictService, ServerActionService, SysParamService
|
||||
|
||||
sys_router = APIRouter()
|
||||
|
||||
|
||||
def _repository(session: AsyncSession) -> SysRepository:
|
||||
return SysRepository(session)
|
||||
|
||||
|
||||
def _admin(session: AsyncSession) -> AdminService:
|
||||
return AdminService(_repository(session))
|
||||
|
||||
|
||||
def _params(session: AsyncSession) -> SysParamService:
|
||||
return SysParamService(_repository(session))
|
||||
|
||||
|
||||
def _dict(session: AsyncSession) -> DictService:
|
||||
return DictService(_repository(session))
|
||||
|
||||
|
||||
async def _refresh_server_config(session: AsyncSession) -> None:
|
||||
from app.repositories.config import ConfigRepository
|
||||
from app.services.config import ConfigService
|
||||
|
||||
await ConfigService(ConfigRepository(session)).get_config(use_cache=False)
|
||||
|
||||
|
||||
@sys_router.get("/admin/users")
|
||||
async def page_users(
|
||||
request: Request,
|
||||
mobile: str | None = None,
|
||||
page: str = Query(default="1"),
|
||||
limit: str = Query(default="10"),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
try:
|
||||
current, size = int(page), int(limit)
|
||||
except ValueError as exc:
|
||||
# Java parses these Map-backed values inside the service; malformed
|
||||
# numbers therefore reach its generic code=500 handler rather than
|
||||
# Bean Validation.
|
||||
raise AppError(500, "排序值不能小于0") from exc
|
||||
return ok(await _admin(session).page_users(mobile=mobile, page=current, limit=size))
|
||||
|
||||
|
||||
@sys_router.put("/admin/users/{user_id}")
|
||||
async def reset_user_password(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
user = require_super_admin(request)
|
||||
return ok(await _admin(session).reset_password(user_id, user))
|
||||
|
||||
|
||||
@sys_router.delete("/admin/users/{user_id}")
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _admin(session).delete_user(user_id)
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.put("/admin/users/changeStatus/{status}")
|
||||
async def change_user_status(
|
||||
status: int,
|
||||
request: Request,
|
||||
user_ids: list[str] = Body(),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
user = require_super_admin(request)
|
||||
await _admin(session).change_status(status, user_ids, user)
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.get("/admin/device/all")
|
||||
async def page_all_devices(
|
||||
request: Request,
|
||||
keywords: str | None = None,
|
||||
page: int = Query(default=1, ge=0),
|
||||
limit: int = Query(default=10, ge=0),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _admin(session).page_devices(keywords=keywords, page=page, limit=limit))
|
||||
|
||||
|
||||
@sys_router.get("/admin/server/server-list")
|
||||
async def websocket_server_list(request: Request, session: AsyncSession = Depends(get_db)) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
params = _params(session)
|
||||
return ok(await ServerActionService(params).server_list())
|
||||
|
||||
|
||||
@sys_router.post("/admin/server/emit-action")
|
||||
async def emit_server_action(
|
||||
dto: EmitServerActionRequest,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await ServerActionService(_params(session)).emit(dto))
|
||||
|
||||
|
||||
@sys_router.get("/admin/params/page")
|
||||
async def page_params(
|
||||
request: Request,
|
||||
page: int = Query(default=1, ge=0),
|
||||
limit: int = Query(default=10, ge=0),
|
||||
order_field: str | None = Query(default=None, alias="orderField"),
|
||||
order: str | None = None,
|
||||
param_code: str | None = Query(default=None, alias="paramCode"),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(
|
||||
await _params(session).page(
|
||||
param_code=param_code,
|
||||
page=page,
|
||||
limit=limit,
|
||||
order_field=order_field,
|
||||
order=order,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@sys_router.get("/admin/params/{param_id}")
|
||||
async def get_param(
|
||||
param_id: int,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _params(session).get(param_id))
|
||||
|
||||
|
||||
@sys_router.post("/admin/params")
|
||||
async def save_param(
|
||||
dto: SysParamPayload,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _params(session).save(
|
||||
dto,
|
||||
require_super_admin(request),
|
||||
request.headers.get("Accept-Language"),
|
||||
)
|
||||
await _refresh_server_config(session)
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.put("/admin/params")
|
||||
async def update_param(
|
||||
dto: SysParamPayload,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _params(session).update(
|
||||
dto,
|
||||
require_super_admin(request),
|
||||
request.headers.get("Accept-Language"),
|
||||
)
|
||||
await _refresh_server_config(session)
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.post("/admin/params/delete")
|
||||
async def delete_params(
|
||||
request: Request,
|
||||
ids: list[str] = Body(),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _params(session).delete(ids)
|
||||
await _refresh_server_config(session)
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.get("/admin/dict/type/page")
|
||||
async def page_dict_types(
|
||||
request: Request,
|
||||
dict_type: str | None = Query(default=None, alias="dictType"),
|
||||
dict_name: str | None = Query(default=None, alias="dictName"),
|
||||
page: int = Query(default=1, ge=0),
|
||||
limit: int = Query(default=10, ge=0),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(
|
||||
await _dict(session).page_types(
|
||||
dict_type=dict_type,
|
||||
dict_name=dict_name,
|
||||
page=page,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@sys_router.get("/admin/dict/type/{type_id}")
|
||||
async def get_dict_type(
|
||||
type_id: int,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _dict(session).get_type(type_id))
|
||||
|
||||
|
||||
@sys_router.post("/admin/dict/type/save")
|
||||
async def save_dict_type(
|
||||
dto: DictTypePayload,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _dict(session).save_type(dto, require_super_admin(request))
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.put("/admin/dict/type/update")
|
||||
async def update_dict_type(
|
||||
dto: DictTypePayload,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _dict(session).update_type(dto, require_super_admin(request))
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.post("/admin/dict/type/delete")
|
||||
async def delete_dict_types(
|
||||
request: Request,
|
||||
ids: list[int] = Body(),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _dict(session).delete_types(ids)
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.get("/admin/dict/data/page")
|
||||
async def page_dict_data(
|
||||
request: Request,
|
||||
dict_type_id: str | None = Query(default=None, alias="dictTypeId"),
|
||||
dict_label: str | None = Query(default=None, alias="dictLabel"),
|
||||
dict_value: str | None = Query(default=None, alias="dictValue"),
|
||||
page: int = Query(default=1, ge=0),
|
||||
limit: int = Query(default=10, ge=0),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
if dict_type_id is None or not dict_type_id:
|
||||
return JavaJSONResponse(envelope(None, code=500, msg="dictTypeId不能为空"))
|
||||
try:
|
||||
parsed_type_id = int(dict_type_id)
|
||||
except ValueError as exc:
|
||||
raise AppError(500) from exc
|
||||
return ok(
|
||||
await _dict(session).page_data(
|
||||
dict_type_id=parsed_type_id,
|
||||
dict_label=dict_label,
|
||||
dict_value=dict_value,
|
||||
page=page,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@sys_router.get("/admin/dict/data/type/{dict_type}")
|
||||
async def dict_items(
|
||||
dict_type: str,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
return ok(await _dict(session).items(dict_type))
|
||||
|
||||
|
||||
@sys_router.get("/admin/dict/data/{data_id}")
|
||||
async def get_dict_data(
|
||||
data_id: int,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await _dict(session).get_data(data_id))
|
||||
|
||||
|
||||
@sys_router.post("/admin/dict/data/save")
|
||||
async def save_dict_data(
|
||||
dto: DictDataPayload,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _dict(session).save_data(dto, require_super_admin(request))
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.put("/admin/dict/data/update")
|
||||
async def update_dict_data(
|
||||
dto: DictDataPayload,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
await _dict(session).update_data(dto, require_super_admin(request))
|
||||
return ok()
|
||||
|
||||
|
||||
@sys_router.post("/admin/dict/data/delete")
|
||||
async def delete_dict_data(
|
||||
request: Request,
|
||||
ids: list[int] = Body(),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _dict(session).delete_data(ids)
|
||||
return ok()
|
||||
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.responses import JavaJSONResponse, ok
|
||||
from app.core.security import require_normal, require_super_admin
|
||||
from app.repositories.timbre import TimbreRepository
|
||||
from app.schemas.timbre import TimbreBody
|
||||
from app.services.timbre import TimbreService
|
||||
|
||||
timbre_router = APIRouter()
|
||||
|
||||
|
||||
def _service(session: AsyncSession) -> TimbreService:
|
||||
return TimbreService(TimbreRepository(session))
|
||||
|
||||
|
||||
@timbre_router.get("/ttsVoice")
|
||||
async def timbre_page(
|
||||
request: Request,
|
||||
tts_model_id: str | None = Query(default=None, alias="ttsModelId"),
|
||||
name: str | None = None,
|
||||
page: str | None = None,
|
||||
limit: str | None = None,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(
|
||||
await _service(session).page(
|
||||
tts_model_id, name, page, limit, request.headers.get("Accept-Language")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@timbre_router.post("/ttsVoice")
|
||||
async def timbre_save(
|
||||
body: TimbreBody, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session).save(
|
||||
body, require_super_admin(request), request.headers.get("Accept-Language")
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@timbre_router.put("/ttsVoice/{timbre_id}")
|
||||
async def timbre_update(
|
||||
timbre_id: str, body: TimbreBody, request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
await _service(session).update(
|
||||
timbre_id, body, require_super_admin(request), request.headers.get("Accept-Language")
|
||||
)
|
||||
return ok()
|
||||
|
||||
|
||||
@timbre_router.post("/ttsVoice/delete")
|
||||
async def timbre_delete(
|
||||
ids: list[str], request: Request, session: AsyncSession = Depends(get_db)
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
await _service(session).delete(ids)
|
||||
return ok()
|
||||
|
||||
|
||||
@timbre_router.get("/models/{model_id}/voices")
|
||||
async def model_voices(
|
||||
model_id: str,
|
||||
request: Request,
|
||||
voice_name: str | None = Query(default=None, alias="voiceName"),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
return ok(await _service(session).voices(model_id, voice_name, user, request.headers.get("Accept-Language")))
|
||||
@@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.errors import AppError
|
||||
from app.core.i18n import message_for
|
||||
from app.core.responses import JavaJSONResponse, error_response, ok
|
||||
from app.core.security import require_normal, require_super_admin
|
||||
from app.schemas.voiceclone import VoiceCloneRenameRequest, VoiceCloneTrainRequest, VoiceResourceCreateRequest
|
||||
from app.services.voiceclone import VoiceCloneService
|
||||
|
||||
voiceclone_router = APIRouter()
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_db)]
|
||||
VoiceFile = Annotated[UploadFile, File(alias="voiceFile")]
|
||||
VoiceIdForm = Annotated[str, Form(alias="id")]
|
||||
|
||||
|
||||
def _query_map(request: Request) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in request.query_params.multi_items():
|
||||
if key in result:
|
||||
previous = result[key]
|
||||
result[key] = [*previous, value] if isinstance(previous, list) else [previous, value]
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
# Static voiceResource paths deliberately precede /voiceResource/{id}.
|
||||
@voiceclone_router.get("/voiceResource/ttsPlatforms")
|
||||
async def tts_platforms(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await VoiceCloneService(session).tts_platforms())
|
||||
|
||||
|
||||
@voiceclone_router.get("/voiceResource/user/{user_id}")
|
||||
async def voice_resources_by_user(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_normal(request)
|
||||
return ok(await VoiceCloneService(session).get_by_user(user_id))
|
||||
|
||||
|
||||
@voiceclone_router.get("/voiceResource")
|
||||
async def page_voice_resources(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await VoiceCloneService(session).page(_query_map(request)))
|
||||
|
||||
|
||||
@voiceclone_router.get("/voiceResource/{voice_id}")
|
||||
async def get_voice_resource(
|
||||
voice_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
return ok(await VoiceCloneService(session).get_detail(voice_id))
|
||||
|
||||
|
||||
@voiceclone_router.post("/voiceResource")
|
||||
async def create_voice_resource(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
body: VoiceResourceCreateRequest | None = None,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_super_admin(request)
|
||||
if body is None:
|
||||
return error_response(request, 10145)
|
||||
if body.model_id is None or body.model_id == "":
|
||||
return error_response(request, 10146)
|
||||
if not body.voice_ids:
|
||||
return error_response(request, 10147)
|
||||
if body.user_id is None:
|
||||
return error_response(request, 10148)
|
||||
try:
|
||||
await VoiceCloneService(session).create_resources(body, actor=user)
|
||||
return ok()
|
||||
except AppError:
|
||||
raise
|
||||
except RuntimeError as exc:
|
||||
return error_response(request, 10065, str(exc))
|
||||
|
||||
|
||||
@voiceclone_router.delete("/voiceResource/{voice_id}")
|
||||
async def delete_voice_resource(
|
||||
voice_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
require_super_admin(request)
|
||||
ids = voice_id.split(",") if voice_id else []
|
||||
if not ids:
|
||||
return error_response(request, 10149)
|
||||
await VoiceCloneService(session).delete(ids)
|
||||
return ok()
|
||||
|
||||
|
||||
@voiceclone_router.get("/voiceClone")
|
||||
async def page_voice_clones(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
return ok(await VoiceCloneService(session).page(_query_map(request), user_id=user.id))
|
||||
|
||||
|
||||
@voiceclone_router.post("/voiceClone/upload")
|
||||
async def upload_voice_clone(
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
voice_file: VoiceFile,
|
||||
voice_id: VoiceIdForm = "",
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
service = VoiceCloneService(session)
|
||||
try:
|
||||
content = await voice_file.read()
|
||||
if not content:
|
||||
return error_response(request, 10140)
|
||||
content_type = voice_file.content_type
|
||||
if content_type is None or not content_type.startswith("audio/"):
|
||||
return error_response(request, 10141)
|
||||
filename = voice_file.filename
|
||||
if filename is None or "." not in filename:
|
||||
raise RuntimeError("文件名缺少扩展名")
|
||||
extension = filename[filename.rfind(".") :].lower()
|
||||
if extension not in {".mp3", ".wav"}:
|
||||
return error_response(request, 500, "只允许上传.mp3和.wav格式的文件")
|
||||
if len(content) > 10 * 1024 * 1024:
|
||||
return error_response(request, 10142)
|
||||
await service.check_permission(voice_id, user)
|
||||
await service.upload_voice(voice_id, content)
|
||||
return ok()
|
||||
except Exception as exc:
|
||||
if isinstance(exc, AppError):
|
||||
message = exc.message or message_for(exc.code, request.headers.get("Accept-Language"))
|
||||
else:
|
||||
message = str(exc)
|
||||
return error_response(request, 10143, message)
|
||||
|
||||
|
||||
@voiceclone_router.post("/voiceClone/updateName")
|
||||
async def update_voice_clone_name(
|
||||
body: VoiceCloneRenameRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
if body.id is None or body.id == "":
|
||||
return error_response(request, 10006)
|
||||
if body.name is None or body.name == "":
|
||||
return error_response(request, 10181)
|
||||
service = VoiceCloneService(session)
|
||||
try:
|
||||
await service.check_permission(body.id, user)
|
||||
await service.rename(body.id or "", body.name or "")
|
||||
return ok()
|
||||
except Exception as exc:
|
||||
if isinstance(exc, AppError):
|
||||
message = exc.message or message_for(exc.code, request.headers.get("Accept-Language"))
|
||||
else:
|
||||
message = str(exc)
|
||||
return error_response(request, 10066, message)
|
||||
|
||||
|
||||
@voiceclone_router.post("/voiceClone/audio/{voice_id}")
|
||||
async def get_voice_clone_audio_id(
|
||||
voice_id: str,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
service = VoiceCloneService(session)
|
||||
await service.check_permission(voice_id, user)
|
||||
return ok(await service.create_audio_id(voice_id))
|
||||
|
||||
|
||||
@voiceclone_router.get("/voiceClone/play/{download_id}")
|
||||
async def play_voice_clone(download_id: str, session: SessionDep) -> Response:
|
||||
try:
|
||||
content = await VoiceCloneService(session).consume_audio(download_id)
|
||||
if content is None:
|
||||
return Response(status_code=404)
|
||||
return Response(
|
||||
content,
|
||||
media_type="audio/wav",
|
||||
headers={
|
||||
"Content-Length": str(len(content)),
|
||||
"Content-Disposition": "inline; filename=voice.wav",
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
return Response(status_code=500)
|
||||
|
||||
|
||||
@voiceclone_router.post("/voiceClone/cloneAudio")
|
||||
async def train_voice_clone(
|
||||
body: VoiceCloneTrainRequest,
|
||||
request: Request,
|
||||
session: SessionDep,
|
||||
) -> JavaJSONResponse:
|
||||
user = require_normal(request)
|
||||
service = VoiceCloneService(session)
|
||||
await service.check_permission(body.clone_id, user)
|
||||
await service.clone_audio(
|
||||
body.clone_id or "",
|
||||
accept_language=request.headers.get("Accept-Language"),
|
||||
)
|
||||
return ok()
|
||||
Reference in New Issue
Block a user