mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
387 lines
15 KiB
Python
387 lines
15 KiB
Python
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))
|