feat: add Docker support and update backend dependencies

- Introduced `requirements.docker.txt` for Docker-specific dependencies.
- Updated `requirements.txt` to include `psycopg[binary]` and `python-multipart`.
- Enhanced test suite in `test_main_endpoints.py` to cover document CRUD operations.
- Modified `docker-compose.yml` to include PostgreSQL and frontend services.
- Added Nginx configuration for reverse proxying API requests.
- Refactored file handling in Vue components to support new document storage backend.
- Created new utility functions in `docsApi.js` for document management.
- Updated configuration to support new API endpoints for document operations.
- Adjusted Vite configuration to proxy API requests to the local backend.
This commit is contained in:
“ydy0615”
2026-06-06 17:18:15 +08:00
parent 81f711ef0b
commit b55af1eff0
20 changed files with 1413 additions and 407 deletions
+147 -1
View File
@@ -1,3 +1,4 @@
import asyncio
import base64
import json
import logging
@@ -5,12 +6,13 @@ import os
import uuid
from typing import Optional
from fastapi import FastAPI, HTTPException, Request, Security
from fastapi import FastAPI, File, Form, HTTPException, Request, Response, Security, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.security import APIKeyHeader
from pydantic import BaseModel
from docs_store import get_document_store
from geoip import get_ip_location_text
from job_handlers import (
_sanitize_converted_markdown,
@@ -110,6 +112,23 @@ class ASRJobRequest(BaseModel):
language: Optional[str] = "zh-CN"
class CreateFolderRequest(BaseModel):
name: str
parentId: Optional[str] = None
class CreateTextFileRequest(BaseModel):
name: str
parentId: Optional[str] = None
content: str = ""
class UpdateNodeRequest(BaseModel):
name: Optional[str] = None
parentId: Optional[str] = None
content: Optional[str] = None
def _preview(text: str, limit: int = 80) -> str:
value = (text or "").replace("\n", "\\n")
if len(value) <= limit:
@@ -219,6 +238,12 @@ async def _queue_load_snapshot() -> dict:
return {}
async def _docs_store_call(method_name: str, *args, **kwargs):
store = get_document_store()
method = getattr(store, method_name)
return await asyncio.to_thread(method, *args, **kwargs)
@app.post("/v1/completions")
async def create_completion(
request: Request,
@@ -452,6 +477,127 @@ async def get_job_load(api_key: str = Security(get_api_key)):
return {"queues": await _queue_load_snapshot()}
@app.get("/v1/docs/nodes")
async def list_docs_nodes(api_key: str = Security(get_api_key)):
del api_key
try:
return {"nodes": await _docs_store_call("list_nodes")}
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
@app.post("/v1/docs/folders")
async def create_docs_folder(req: CreateFolderRequest, api_key: str = Security(get_api_key)):
del api_key
if not (req.name or "").strip():
raise HTTPException(status_code=400, detail="文件夹名称不能为空")
try:
node = await _docs_store_call("create_folder", req.name.strip(), req.parentId)
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
return {"node": node}
@app.post("/v1/docs/files/text")
async def create_docs_text_file(req: CreateTextFileRequest, api_key: str = Security(get_api_key)):
del api_key
if not (req.name or "").strip():
raise HTTPException(status_code=400, detail="文件名称不能为空")
try:
node = await _docs_store_call("create_text_file", req.name.strip(), req.parentId, req.content or "")
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
return {"node": node}
@app.post("/v1/docs/files/upload")
async def upload_docs_file(
file: UploadFile = File(...),
parent_id: Optional[str] = Form(default=None),
api_key: str = Security(get_api_key),
):
del api_key
filename = (file.filename or "").strip()
if not filename:
raise HTTPException(status_code=400, detail="文件名称不能为空")
raw_bytes = await file.read()
try:
node = await _docs_store_call("upload_file", filename, parent_id, raw_bytes, file.content_type or "")
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
return {"node": node}
@app.patch("/v1/docs/nodes/{node_id}")
async def update_docs_node(node_id: str, req: UpdateNodeRequest, api_key: str = Security(get_api_key)):
del api_key
fields_set = req.model_fields_set if hasattr(req, "model_fields_set") else getattr(req, "__fields_set__", set())
if not fields_set:
raise HTTPException(status_code=400, detail="缺少更新内容")
update_kwargs = {}
if "name" in fields_set:
next_name = req.name.strip() if isinstance(req.name, str) else ""
if not next_name:
raise HTTPException(status_code=400, detail="名称不能为空")
update_kwargs["name"] = next_name
if "parentId" in fields_set:
update_kwargs["parent_id"] = req.parentId
if "content" in fields_set:
update_kwargs["content"] = req.content or ""
try:
node = await _docs_store_call("update_node", node_id, **update_kwargs)
except KeyError as exc:
raise HTTPException(status_code=404, detail="节点不存在") from exc
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
return {"node": node}
@app.put("/v1/docs/files/{node_id}/blob")
async def replace_docs_blob(
node_id: str,
file: UploadFile = File(...),
api_key: str = Security(get_api_key),
):
del api_key
filename = (file.filename or "").strip()
if not filename:
raise HTTPException(status_code=400, detail="文件名称不能为空")
raw_bytes = await file.read()
try:
node = await _docs_store_call("replace_blob", node_id, filename, raw_bytes, file.content_type or "")
except KeyError as exc:
raise HTTPException(status_code=404, detail="节点不存在") from exc
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
return {"node": node}
@app.delete("/v1/docs/nodes/{node_id}")
async def delete_docs_node(node_id: str, api_key: str = Security(get_api_key)):
del api_key
try:
await _docs_store_call("delete_node", node_id)
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
return {"ok": True}
@app.get("/v1/docs/files/{node_id}/blob")
async def download_docs_blob(node_id: str, api_key: str = Security(get_api_key)):
del api_key
try:
payload = await _docs_store_call("get_blob", node_id)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="文件不存在") from exc
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
headers = {
"Content-Disposition": f'inline; filename="{payload.filename}"',
}
return Response(content=payload.content, media_type=payload.mime_type, headers=headers)
def _register_tts_asr_routes():
try:
from tts_asr import register_tts_asr_routes