mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 00:23:53 +08:00
feat: add FastAPI manager API compatibility baseline
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import FastAPI, File, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
app = FastAPI(title="manager-api repeatable external-service mock")
|
||||
|
||||
_requests: list[dict[str, Any]] = []
|
||||
_datasets: dict[str, dict[str, Any]] = {}
|
||||
_documents: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
async def _capture(request: Request, body: Any = None) -> None:
|
||||
_requests.append(
|
||||
{
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"query": dict(request.query_params),
|
||||
"headers": {
|
||||
key.lower(): value
|
||||
for key, value in request.headers.items()
|
||||
if key.lower() in {"authorization", "content-type"}
|
||||
},
|
||||
"body": body,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "UP"}
|
||||
|
||||
|
||||
@app.get("/__requests")
|
||||
async def requests() -> dict[str, Any]:
|
||||
return {"requests": list(_requests)}
|
||||
|
||||
|
||||
@app.delete("/__requests")
|
||||
async def reset_requests() -> dict[str, bool]:
|
||||
_requests.clear()
|
||||
return {"reset": True}
|
||||
|
||||
|
||||
@app.post("/api/commands/{client_id}")
|
||||
async def mqtt_command(client_id: str, request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
method = body.get("payload", {}).get("method") if isinstance(body, Mapping) else None
|
||||
if method == "tools/list":
|
||||
response = {
|
||||
"success": True,
|
||||
"data": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "contract.echo",
|
||||
"description": "repeatable contract fixture",
|
||||
"inputSchema": {"type": "object"},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
elif method == "tools/call":
|
||||
response = {"success": True, "data": {"content": [{"type": "text", "text": '{"echo":true}'}]}}
|
||||
else:
|
||||
response = {"success": True, "data": {"clientId": client_id}}
|
||||
return JSONResponse(response)
|
||||
|
||||
|
||||
@app.post("/api/devices/status")
|
||||
async def mqtt_status(request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
client_ids = body.get("clientIds", []) if isinstance(body, Mapping) else []
|
||||
return JSONResponse({"success": True, "data": {str(value): True for value in client_ids}})
|
||||
|
||||
|
||||
@app.post("/api/call/{operation}")
|
||||
async def mqtt_call(operation: str, request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
return JSONResponse({"status": "success", "message": operation})
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completions(request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
return JSONResponse(
|
||||
{
|
||||
"id": "chatcmpl-contract",
|
||||
"choices": [{"index": 0, "message": {"role": "assistant", "content": "contract summary"}}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/datasets")
|
||||
async def list_datasets(request: Request) -> JSONResponse:
|
||||
await _capture(request)
|
||||
dataset_id = request.query_params.get("id")
|
||||
values = list(_datasets.values())
|
||||
if dataset_id:
|
||||
values = [value for value in values if value["id"] == dataset_id]
|
||||
return JSONResponse({"code": 0, "data": values})
|
||||
|
||||
|
||||
@app.post("/api/v1/datasets")
|
||||
async def create_dataset(request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
identifier = "rag-contract-" + hashlib.sha256(
|
||||
json.dumps(body, ensure_ascii=False, sort_keys=True).encode()
|
||||
).hexdigest()[:12]
|
||||
value = {
|
||||
"id": identifier,
|
||||
"name": body.get("name"),
|
||||
"description": body.get("description"),
|
||||
"tenant_id": "tenant-contract",
|
||||
"avatar": body.get("avatar"),
|
||||
"document_count": 0,
|
||||
"chunk_count": 0,
|
||||
"token_num": 0,
|
||||
}
|
||||
_datasets[identifier] = value
|
||||
return JSONResponse({"code": 0, "data": value})
|
||||
|
||||
|
||||
@app.put("/api/v1/datasets/{dataset_id}")
|
||||
async def update_dataset(dataset_id: str, request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
value = _datasets.setdefault(dataset_id, {"id": dataset_id})
|
||||
value.update(body)
|
||||
return JSONResponse({"code": 0, "data": value})
|
||||
|
||||
|
||||
@app.delete("/api/v1/datasets/{dataset_id}")
|
||||
async def delete_dataset(dataset_id: str, request: Request) -> JSONResponse:
|
||||
await _capture(request)
|
||||
_datasets.pop(dataset_id, None)
|
||||
return JSONResponse({"code": 0, "data": True})
|
||||
|
||||
|
||||
@app.post("/api/v1/datasets/{dataset_id}/documents")
|
||||
async def upload_document(
|
||||
dataset_id: str,
|
||||
request: Request,
|
||||
file: Annotated[UploadFile, File()],
|
||||
) -> JSONResponse:
|
||||
content = await file.read()
|
||||
body: dict[str, Any] = {
|
||||
"filename": file.filename,
|
||||
"size": len(content),
|
||||
"sha256": hashlib.sha256(content).hexdigest(),
|
||||
}
|
||||
await _capture(request, body)
|
||||
identifier = "doc-contract-" + str(body["sha256"])[:12]
|
||||
value = {
|
||||
"id": identifier,
|
||||
"dataset_id": dataset_id,
|
||||
"name": file.filename,
|
||||
"size": len(content),
|
||||
"run": "UNSTART",
|
||||
"progress": 0.0,
|
||||
"chunk_count": 0,
|
||||
"token_count": 0,
|
||||
}
|
||||
_documents[identifier] = value
|
||||
return JSONResponse({"code": 0, "data": [value]})
|
||||
|
||||
|
||||
@app.get("/api/v1/datasets/{dataset_id}/documents")
|
||||
async def list_documents(dataset_id: str, request: Request) -> JSONResponse:
|
||||
await _capture(request)
|
||||
document_id = request.query_params.get("id")
|
||||
values = [value for value in _documents.values() if value["dataset_id"] == dataset_id]
|
||||
if document_id:
|
||||
values = [value for value in values if value["id"] == document_id]
|
||||
return JSONResponse({"code": 0, "data": {"docs": values, "total": len(values)}})
|
||||
|
||||
|
||||
@app.post("/api/v1/datasets/{dataset_id}/chunks")
|
||||
async def parse_documents(dataset_id: str, request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
for document_id in body.get("document_ids", []):
|
||||
if document_id in _documents:
|
||||
_documents[document_id].update(run="DONE", progress=1.0, chunk_count=2, token_count=17)
|
||||
return JSONResponse({"code": 0, "data": True})
|
||||
|
||||
|
||||
@app.delete("/api/v1/datasets/{dataset_id}/documents")
|
||||
async def delete_documents(dataset_id: str, request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
await _capture(request, body)
|
||||
for document_id in body.get("ids", body.get("document_ids", [])):
|
||||
_documents.pop(document_id, None)
|
||||
return JSONResponse({"code": 0, "data": True})
|
||||
|
||||
|
||||
@app.post("/voice-clone")
|
||||
async def clone_voice(request: Request) -> JSONResponse:
|
||||
content = await request.body()
|
||||
await _capture(request, {"size": len(content), "sha256": hashlib.sha256(content).hexdigest()})
|
||||
return JSONResponse({"voice_id": "voice-contract", "status": "success"})
|
||||
|
||||
|
||||
@app.post("/transient/{failures}")
|
||||
async def transient(failures: int, request: Request) -> JSONResponse:
|
||||
await _capture(request)
|
||||
count = sum(1 for item in _requests if item["path"] == request.url.path)
|
||||
if count <= failures:
|
||||
await asyncio.sleep(0)
|
||||
return JSONResponse({"error": "retryable"}, status_code=503)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.get("/binary")
|
||||
async def binary(request: Request) -> JSONResponse:
|
||||
await _capture(request)
|
||||
content = b"contract-binary\x00\xff"
|
||||
return JSONResponse({"base64": base64.b64encode(content).decode(), "size": len(content)})
|
||||
Reference in New Issue
Block a user