2312
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- backend/.dockerignore +53 -0
- backend/Dockerfile.backend +18 -0
- backend/backend_app/__init__.py +0 -0
- backend/backend_app/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/backend_app/__pycache__/__init__.cpython-312.pyc +0 -0
- backend/backend_app/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/backend_app/__pycache__/config.cpython-311.pyc +0 -0
- backend/backend_app/__pycache__/config.cpython-313.pyc +0 -0
- backend/backend_app/__pycache__/constants.cpython-311.pyc +0 -0
- backend/backend_app/__pycache__/constants.cpython-313.pyc +0 -0
- backend/backend_app/__pycache__/consumer.cpython-311.pyc +0 -0
- backend/backend_app/__pycache__/di.cpython-311.pyc +0 -0
- backend/backend_app/__pycache__/di.cpython-313.pyc +0 -0
- backend/backend_app/__pycache__/main.cpython-311.pyc +0 -0
- backend/backend_app/__pycache__/main.cpython-312.pyc +0 -0
- backend/backend_app/__pycache__/main.cpython-313.pyc +0 -0
- backend/backend_app/__pycache__/redis_config.cpython-311.pyc +0 -0
- backend/backend_app/api/__init__.py +0 -0
- backend/backend_app/api/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/backend_app/api/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/backend_app/api/__pycache__/api_router.cpython-311.pyc +0 -0
- backend/backend_app/api/__pycache__/api_router.cpython-313.pyc +0 -0
- backend/backend_app/api/api_router.py +23 -0
- backend/backend_app/api/llm_api/__init__.py +0 -0
- backend/backend_app/api/llm_api/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/__pycache__/llm_model.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/__pycache__/llm_model.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__init__.py +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/ingest.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/ingest_component.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/ingest_component.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/ingest_component_kg_rag.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/ingest_helper.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/__pycache__/ingest_helper.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/ingest/ingest.py +185 -0
- backend/backend_app/api/llm_api/newUpdateFile/__init__.py +0 -0
- backend/backend_app/api/llm_api/newUpdateFile/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/newUpdateFile/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/newUpdateFile/__pycache__/new_updateFile.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/newUpdateFile/__pycache__/new_updateFile.cpython-313.pyc +0 -0
- backend/backend_app/api/llm_api/newUpdateFile/new_updateFile.py +245 -0
- backend/backend_app/api/llm_api/universal/__init__.py +1 -0
- backend/backend_app/api/llm_api/universal/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/universal/__pycache__/universal_router.cpython-311.pyc +0 -0
- backend/backend_app/api/llm_api/universal/universal_router.py +128 -0
- backend/backend_app/api/video/README.md +428 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
backend/cache/models--BAAI--bge-small-zh/blobs/5930ef2631f95185a3b74db3dcff787376bc93cdf693321ef75b2d4ed3b3545d filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
backend/cache/models--BAAI--bge-small-zh/blobs/5cc750eb4f5412313aba1482e53aeccd1ed0b97bc213f2962e0c80e6c8809319 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
backend/temp_gorilla/berkeley-function-call-leaderboard/architecture_diagram.png filter=lfs diff=lfs merge=lfs -text
|
backend/.dockerignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1. Python 编译产物(无需打包进镜像)
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
.mypy_cache/
|
| 6 |
+
.pytest_cache/
|
| 7 |
+
.coverage
|
| 8 |
+
htmlcov/
|
| 9 |
+
|
| 10 |
+
# 2. 虚拟环境(镜像内会重新安装依赖,本地虚拟环境无需打包)
|
| 11 |
+
.venv/
|
| 12 |
+
venv/
|
| 13 |
+
ENV/
|
| 14 |
+
env/
|
| 15 |
+
|
| 16 |
+
# 3. 敏感/配置文件(避免密钥、本地配置打包进镜像)
|
| 17 |
+
.env
|
| 18 |
+
.env.local
|
| 19 |
+
.env.*.local
|
| 20 |
+
*.env
|
| 21 |
+
secrets/
|
| 22 |
+
|
| 23 |
+
# 4. 项目临时/大文件(减少镜像体积)
|
| 24 |
+
temp_docs/ # 文档上传临时目录
|
| 25 |
+
model_cache/ # 本地模型缓存(镜像内可按需下载或挂载)
|
| 26 |
+
~/.cache/huggingface/ # 本地 HuggingFace 模型缓存
|
| 27 |
+
~/.cache/torch/
|
| 28 |
+
*.log
|
| 29 |
+
*.tmp
|
| 30 |
+
.DS_Store
|
| 31 |
+
Thumbs.db
|
| 32 |
+
|
| 33 |
+
# 5. 版本控制/构建产物(无需打包)
|
| 34 |
+
.git/
|
| 35 |
+
.gitignore
|
| 36 |
+
.github/
|
| 37 |
+
dist/
|
| 38 |
+
build/
|
| 39 |
+
*.egg-info/
|
| 40 |
+
|
| 41 |
+
# 6. Docker 相关(避免循环打包)
|
| 42 |
+
.dockerignore
|
| 43 |
+
Dockerfile
|
| 44 |
+
docker-compose.yml
|
| 45 |
+
*.dockerfile
|
| 46 |
+
src/*.py
|
| 47 |
+
electron_app/
|
| 48 |
+
|
| 49 |
+
# 7. 其他无关文件
|
| 50 |
+
README.md
|
| 51 |
+
LICENSE
|
| 52 |
+
*.txt
|
| 53 |
+
*.md
|
backend/Dockerfile.backend
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face 官方标准格式 —— 必须这样写
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# 固定配置
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
USER user
|
| 7 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# 安装依赖
|
| 11 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 12 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
| 13 |
+
|
| 14 |
+
# 复制代码
|
| 15 |
+
COPY --chown=user . /app
|
| 16 |
+
|
| 17 |
+
# 必须端口 7860
|
| 18 |
+
CMD ["uvicorn", "backend.backend_app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
backend/backend_app/__init__.py
ADDED
|
File without changes
|
backend/backend_app/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
backend/backend_app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (167 Bytes). View file
|
|
|
backend/backend_app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (150 Bytes). View file
|
|
|
backend/backend_app/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.9 kB). View file
|
|
|
backend/backend_app/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (1.68 kB). View file
|
|
|
backend/backend_app/__pycache__/constants.cpython-311.pyc
ADDED
|
Binary file (2.64 kB). View file
|
|
|
backend/backend_app/__pycache__/constants.cpython-313.pyc
ADDED
|
Binary file (2.39 kB). View file
|
|
|
backend/backend_app/__pycache__/consumer.cpython-311.pyc
ADDED
|
Binary file (3.14 kB). View file
|
|
|
backend/backend_app/__pycache__/di.cpython-311.pyc
ADDED
|
Binary file (803 Bytes). View file
|
|
|
backend/backend_app/__pycache__/di.cpython-313.pyc
ADDED
|
Binary file (694 Bytes). View file
|
|
|
backend/backend_app/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (9.49 kB). View file
|
|
|
backend/backend_app/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (12.8 kB). View file
|
|
|
backend/backend_app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (3.58 kB). View file
|
|
|
backend/backend_app/__pycache__/redis_config.cpython-311.pyc
ADDED
|
Binary file (740 Bytes). View file
|
|
|
backend/backend_app/api/__init__.py
ADDED
|
File without changes
|
backend/backend_app/api/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (166 Bytes). View file
|
|
|
backend/backend_app/api/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
backend/backend_app/api/__pycache__/api_router.cpython-311.pyc
ADDED
|
Binary file (1.3 kB). View file
|
|
|
backend/backend_app/api/__pycache__/api_router.cpython-313.pyc
ADDED
|
Binary file (971 Bytes). View file
|
|
|
backend/backend_app/api/api_router.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from fastapi import APIRouter
|
| 3 |
+
|
| 4 |
+
from backend_app.api.llm_api.newUpdateFile.new_updateFile import new_updateFile_router
|
| 5 |
+
from backend_app.api.llm_api.universal.universal_router import universal_router
|
| 6 |
+
from backend_app.api.llm_api.ingest.ingest import ingest_router
|
| 7 |
+
from backend_app.api.voice.voice_router import voice_router
|
| 8 |
+
from backend_app.api.video.video_router import video_router
|
| 9 |
+
|
| 10 |
+
api_router = APIRouter()
|
| 11 |
+
|
| 12 |
+
api_router.include_router(new_updateFile_router, prefix="/new_update_file", tags=["new_update_file"])
|
| 13 |
+
|
| 14 |
+
api_router.include_router(ingest_router, prefix="/ingest", tags=["ingest"])
|
| 15 |
+
|
| 16 |
+
# universal enterprise assistant
|
| 17 |
+
api_router.include_router(universal_router, prefix="/universal", tags=["universal"])
|
| 18 |
+
|
| 19 |
+
# voice generation service
|
| 20 |
+
api_router.include_router(voice_router, prefix="/voice", tags=["voice"])
|
| 21 |
+
|
| 22 |
+
# video generation service
|
| 23 |
+
api_router.include_router(video_router, prefix="/video", tags=["video"])
|
backend/backend_app/api/llm_api/__init__.py
ADDED
|
File without changes
|
backend/backend_app/api/llm_api/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (174 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/__pycache__/llm_model.cpython-311.pyc
ADDED
|
Binary file (6.25 kB). View file
|
|
|
backend/backend_app/api/llm_api/__pycache__/llm_model.cpython-313.pyc
ADDED
|
Binary file (5.43 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__init__.py
ADDED
|
File without changes
|
backend/backend_app/api/llm_api/ingest/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (173 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (161 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/ingest.cpython-311.pyc
ADDED
|
Binary file (8.01 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/ingest_component.cpython-311.pyc
ADDED
|
Binary file (9.65 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/ingest_component.cpython-313.pyc
ADDED
|
Binary file (8.59 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/ingest_component_kg_rag.cpython-313.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/ingest_helper.cpython-311.pyc
ADDED
|
Binary file (5.35 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/__pycache__/ingest_helper.cpython-313.pyc
ADDED
|
Binary file (4.94 kB). View file
|
|
|
backend/backend_app/api/llm_api/ingest/ingest.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Dict
|
| 3 |
+
from fastapi import APIRouter, Form, HTTPException
|
| 4 |
+
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
from helloAgents.tools.registry import global_registry
|
| 10 |
+
from helloAgents.tools.builtin.rag_tool import RAGTool
|
| 11 |
+
# 2. 定义请求体
|
| 12 |
+
class FileListRequest(BaseModel):
|
| 13 |
+
user_id: str
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
ingest_router = APIRouter()
|
| 17 |
+
|
| 18 |
+
# ------------------------------
|
| 19 |
+
# ⚙️ 企业级配置(和你原有一致)
|
| 20 |
+
# ------------------------------
|
| 21 |
+
MANIFEST_FILE = ".file_manifest.json"
|
| 22 |
+
CHUNK_SIZE = 8192
|
| 23 |
+
MAX_PARALLEL_FILES = 4
|
| 24 |
+
SAVE_BASE_DIR = "./knowledge_base" # 统一基座目录
|
| 25 |
+
|
| 26 |
+
# ------------------------------
|
| 27 |
+
# 🆕 新增:获取用户上传文档接口
|
| 28 |
+
# ------------------------------
|
| 29 |
+
@ingest_router.post("/list")
|
| 30 |
+
async def get_user_uploaded_files(body: FileListRequest):
|
| 31 |
+
"""
|
| 32 |
+
获取指定命名空间下所有已上传的文件列表
|
| 33 |
+
返回:所有真实文件、文件路径、哈希、去重状态、文件信息
|
| 34 |
+
"""
|
| 35 |
+
user_id = body.user_id
|
| 36 |
+
if not user_id.strip():
|
| 37 |
+
raise HTTPException(status_code=400, detail="用户ID不能为空")
|
| 38 |
+
|
| 39 |
+
# 用户文件目录
|
| 40 |
+
user_dir = os.path.join(SAVE_BASE_DIR, user_id)
|
| 41 |
+
if not os.path.exists(user_dir):
|
| 42 |
+
return {
|
| 43 |
+
"success": True,
|
| 44 |
+
"user_id": user_id,
|
| 45 |
+
"total_files": 0,
|
| 46 |
+
"files": [],
|
| 47 |
+
"msg": "该用户暂无上传文件"
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# 加载 manifest 清单(你原有去重/版本管理核心)
|
| 51 |
+
manifest = load_manifest(user_dir)
|
| 52 |
+
if not manifest:
|
| 53 |
+
return {
|
| 54 |
+
"success": True,
|
| 55 |
+
"user_id": user_id,
|
| 56 |
+
"total_files": 0,
|
| 57 |
+
"files": [],
|
| 58 |
+
"msg": "该用户暂无上传文件(清单为空)"
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# 构建文件列表
|
| 62 |
+
file_list = []
|
| 63 |
+
for filename, file_hash in manifest.items():
|
| 64 |
+
file_path = os.path.join(user_dir, filename)
|
| 65 |
+
|
| 66 |
+
# 获取文件基本信息
|
| 67 |
+
file_stat = os.stat(file_path)
|
| 68 |
+
create_time = datetime.fromtimestamp(file_stat.st_ctime).strftime("%Y-%m-%d %H:%M:%S")
|
| 69 |
+
update_time = datetime.fromtimestamp(file_stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
| 70 |
+
file_size = file_stat.st_size # 字节
|
| 71 |
+
|
| 72 |
+
# 文件格式
|
| 73 |
+
_, ext = os.path.splitext(filename)
|
| 74 |
+
ext = ext.lower().lstrip(".")
|
| 75 |
+
|
| 76 |
+
file_list.append({
|
| 77 |
+
"filename": filename,
|
| 78 |
+
"original_filename": filename.split("_v")[0] + "." + ext if "_v" in filename else filename,
|
| 79 |
+
"file_path": file_path,
|
| 80 |
+
"file_hash": file_hash,
|
| 81 |
+
"file_type": ext,
|
| 82 |
+
"file_size_bytes": file_size,
|
| 83 |
+
"file_size": f"{round(file_size / 1024 / 1024, 2)} MB" if file_size > 1024*1024 else f"{round(file_size / 1024, 2)} KB",
|
| 84 |
+
"create_time": create_time,
|
| 85 |
+
"update_time": update_time,
|
| 86 |
+
"is_versioned": "_v" in filename # 是否是版本文件
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
# 按上传时间倒序排列(最新在前)
|
| 90 |
+
file_list = sorted(file_list, key=lambda x: x["create_time"], reverse=True)
|
| 91 |
+
|
| 92 |
+
return {
|
| 93 |
+
"success": True,
|
| 94 |
+
"user_id": user_id,
|
| 95 |
+
"total_files": len(file_list),
|
| 96 |
+
"files": file_list,
|
| 97 |
+
"msg": f"成功获取 {len(file_list)} 个文件"
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def load_manifest(ns_path: str) -> Dict[str, str]:
|
| 103 |
+
manifest_path = os.path.join(ns_path, MANIFEST_FILE)
|
| 104 |
+
if os.path.exists(manifest_path):
|
| 105 |
+
try:
|
| 106 |
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
| 107 |
+
return json.load(f)
|
| 108 |
+
except Exception:
|
| 109 |
+
logger.warning(f"读取 manifest 失败: {manifest_path}")
|
| 110 |
+
return {}
|
| 111 |
+
return {}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class DeleteFileListRequest(BaseModel):
|
| 117 |
+
user_id: str
|
| 118 |
+
doc_id: str
|
| 119 |
+
# ------------------------------
|
| 120 |
+
# 🗑️ 新增:删除文档接口(通过 file_hash 删除)
|
| 121 |
+
# ------------------------------
|
| 122 |
+
@ingest_router.post("/delete")
|
| 123 |
+
async def delete_file_by_hash(
|
| 124 |
+
body: DeleteFileListRequest
|
| 125 |
+
):
|
| 126 |
+
# 接收前端传来的 user_id 和 doc_id
|
| 127 |
+
user_id = body.user_id
|
| 128 |
+
doc_id = body.doc_id
|
| 129 |
+
if not user_id.strip() or not doc_id.strip():
|
| 130 |
+
raise HTTPException(status_code=400, detail="user_id 和 doc_id 不能为空")
|
| 131 |
+
|
| 132 |
+
user_dir = os.path.join(SAVE_BASE_DIR, user_id)
|
| 133 |
+
manifest = load_manifest(user_dir)
|
| 134 |
+
|
| 135 |
+
# 1. 通过 doc_id 找到对应的文件名
|
| 136 |
+
target_filename = None
|
| 137 |
+
for filename, f_doc_id in manifest.items():
|
| 138 |
+
if f_doc_id == doc_id:
|
| 139 |
+
target_filename = filename
|
| 140 |
+
break
|
| 141 |
+
|
| 142 |
+
if not target_filename:
|
| 143 |
+
raise HTTPException(status_code=404, detail="文件不存在或已删除")
|
| 144 |
+
|
| 145 |
+
# 2. 删除真实文件
|
| 146 |
+
file_path = os.path.join(user_dir, target_filename)
|
| 147 |
+
if os.path.exists(file_path):
|
| 148 |
+
os.remove(file_path)
|
| 149 |
+
|
| 150 |
+
# 3. 从 manifest 中移除
|
| 151 |
+
del manifest[target_filename]
|
| 152 |
+
save_manifest(user_dir, manifest)
|
| 153 |
+
|
| 154 |
+
# 删除向量库
|
| 155 |
+
rag_tool: RAGTool = global_registry.get_tool("rag")
|
| 156 |
+
result = rag_tool.run({
|
| 157 |
+
"action": "delete_document",
|
| 158 |
+
"user_id": user_id,
|
| 159 |
+
"doc_id": doc_id
|
| 160 |
+
})
|
| 161 |
+
|
| 162 |
+
if result:
|
| 163 |
+
return {
|
| 164 |
+
"success": True,
|
| 165 |
+
"msg": "删除成功",
|
| 166 |
+
"filename": target_filename,
|
| 167 |
+
"doc_id": doc_id
|
| 168 |
+
}
|
| 169 |
+
else:
|
| 170 |
+
return {
|
| 171 |
+
"success": False,
|
| 172 |
+
"msg": "删除失败,请手动检查",
|
| 173 |
+
"filename": target_filename,
|
| 174 |
+
"doc_id": doc_id
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def save_manifest(ns_path: str, manifest: Dict[str, str]):
|
| 181 |
+
manifest_path = os.path.join(ns_path, MANIFEST_FILE)
|
| 182 |
+
temp_path = manifest_path + ".tmp"
|
| 183 |
+
with open(temp_path, "w", encoding="utf-8") as f:
|
| 184 |
+
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
| 185 |
+
os.replace(temp_path, manifest_path)
|
backend/backend_app/api/llm_api/newUpdateFile/__init__.py
ADDED
|
File without changes
|
backend/backend_app/api/llm_api/newUpdateFile/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (188 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/newUpdateFile/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (176 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/newUpdateFile/__pycache__/new_updateFile.cpython-311.pyc
ADDED
|
Binary file (13.5 kB). View file
|
|
|
backend/backend_app/api/llm_api/newUpdateFile/__pycache__/new_updateFile.cpython-313.pyc
ADDED
|
Binary file (4.06 kB). View file
|
|
|
backend/backend_app/api/llm_api/newUpdateFile/new_updateFile.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, UploadFile, File, Form, HTTPException
|
| 2 |
+
from typing import List, Dict
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import asyncio
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import tempfile
|
| 10 |
+
import shutil
|
| 11 |
+
|
| 12 |
+
from helloAgents.tools.async_executor import AsyncToolExecutor
|
| 13 |
+
from helloAgents.tools.builtin.rag_tool import RAGTool
|
| 14 |
+
from helloAgents.tools.builtin.memory_tool import MemoryTool
|
| 15 |
+
from helloAgents.tools.registry import global_registry
|
| 16 |
+
|
| 17 |
+
from redis_config import get_redis, QUEUE_RAG_QDRANT, QUEUE_RAG_NEO4J, QUEUE_MEMORY
|
| 18 |
+
redis = get_redis()
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
new_updateFile_router = APIRouter()
|
| 22 |
+
|
| 23 |
+
# ------------------------------
|
| 24 |
+
# ⚙️ 企业级配置
|
| 25 |
+
# ------------------------------
|
| 26 |
+
MANIFEST_FILE = ".file_manifest.json"
|
| 27 |
+
CHUNK_SIZE = 8192
|
| 28 |
+
MAX_PARALLEL_FILES = 4
|
| 29 |
+
|
| 30 |
+
# ------------------------------
|
| 31 |
+
# 🛠️ 核心工具函数
|
| 32 |
+
# ------------------------------
|
| 33 |
+
|
| 34 |
+
def load_manifest(ns_path: str) -> Dict[str, str]:
|
| 35 |
+
manifest_path = os.path.join(ns_path, MANIFEST_FILE)
|
| 36 |
+
if os.path.exists(manifest_path):
|
| 37 |
+
try:
|
| 38 |
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
| 39 |
+
return json.load(f)
|
| 40 |
+
except Exception:
|
| 41 |
+
logger.warning(f"读取 manifest 失败: {manifest_path}")
|
| 42 |
+
return {}
|
| 43 |
+
return {}
|
| 44 |
+
|
| 45 |
+
def save_manifest(ns_path: str, manifest: Dict[str, str]):
|
| 46 |
+
manifest_path = os.path.join(ns_path, MANIFEST_FILE)
|
| 47 |
+
temp_path = manifest_path + ".tmp"
|
| 48 |
+
with open(temp_path, "w", encoding="utf-8") as f:
|
| 49 |
+
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
| 50 |
+
os.replace(temp_path, manifest_path)
|
| 51 |
+
|
| 52 |
+
def calculate_file_hash_stream(file_path: str) -> str:
|
| 53 |
+
sha256 = hashlib.sha256()
|
| 54 |
+
with open(file_path, "rb") as f:
|
| 55 |
+
while chunk := f.read(CHUNK_SIZE):
|
| 56 |
+
sha256.update(chunk)
|
| 57 |
+
return sha256.hexdigest()
|
| 58 |
+
|
| 59 |
+
def calculate_upload_hash_stream(file_obj) -> str:
|
| 60 |
+
sha256 = hashlib.sha256()
|
| 61 |
+
file_obj.file.seek(0)
|
| 62 |
+
while chunk := file_obj.file.read(CHUNK_SIZE):
|
| 63 |
+
sha256.update(chunk)
|
| 64 |
+
file_obj.file.seek(0)
|
| 65 |
+
return sha256.hexdigest()
|
| 66 |
+
|
| 67 |
+
async def save_file_with_version_and_deduplicate(
|
| 68 |
+
upload_file: UploadFile,
|
| 69 |
+
user_id: str,
|
| 70 |
+
save_dir: str = "./knowledge_base"
|
| 71 |
+
) -> dict:
|
| 72 |
+
original_name = upload_file.filename.strip()
|
| 73 |
+
name, ext = os.path.splitext(original_name)
|
| 74 |
+
ns_path = os.path.join(save_dir, user_id)
|
| 75 |
+
|
| 76 |
+
os.makedirs(ns_path, exist_ok=True)
|
| 77 |
+
manifest = load_manifest(ns_path)
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
new_file_hash = calculate_upload_hash_stream(upload_file)
|
| 81 |
+
|
| 82 |
+
for existing_name, existing_hash in manifest.items():
|
| 83 |
+
if existing_hash == new_file_hash:
|
| 84 |
+
return {
|
| 85 |
+
"success": True,
|
| 86 |
+
"is_duplicate": True,
|
| 87 |
+
"filename": original_name,
|
| 88 |
+
"file_path": os.path.join(ns_path, existing_name),
|
| 89 |
+
"message": f"内容已存在(同名:{existing_name}),自动去重"
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
target_name = original_name
|
| 93 |
+
version = 1
|
| 94 |
+
while target_name in manifest:
|
| 95 |
+
version += 1
|
| 96 |
+
target_name = f"{name}_v{version}{ext}"
|
| 97 |
+
|
| 98 |
+
final_path = os.path.join(ns_path, target_name)
|
| 99 |
+
|
| 100 |
+
with tempfile.NamedTemporaryFile(dir=ns_path, delete=False) as tmp_file:
|
| 101 |
+
shutil.copyfileobj(upload_file.file, tmp_file)
|
| 102 |
+
temp_file_path = tmp_file.name
|
| 103 |
+
|
| 104 |
+
os.replace(temp_file_path, final_path)
|
| 105 |
+
|
| 106 |
+
manifest[target_name] = new_file_hash
|
| 107 |
+
save_manifest(ns_path, manifest)
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"success": True,
|
| 111 |
+
"is_duplicate": False,
|
| 112 |
+
"filename": original_name,
|
| 113 |
+
"file_path": final_path,
|
| 114 |
+
"hash": new_file_hash,
|
| 115 |
+
"message": f"已保存(版本 v{version})" if version > 1 else "已保存"
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
logger.error(f"保存失败 {upload_file.filename}: {str(e)}")
|
| 120 |
+
return {
|
| 121 |
+
"success": False,
|
| 122 |
+
"filename": upload_file.filename,
|
| 123 |
+
"error": str(e)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
# ------------------------------
|
| 127 |
+
# 主处理逻辑(已补全记忆)
|
| 128 |
+
# ------------------------------
|
| 129 |
+
async def process_uploaded_files(files: List[UploadFile], user_id: str) -> dict:
|
| 130 |
+
memory_tool: MemoryTool = global_registry.get_tool("memory")
|
| 131 |
+
rag_tool: RAGTool = global_registry.get_tool("rag")
|
| 132 |
+
|
| 133 |
+
if not files:
|
| 134 |
+
raise HTTPException(status_code=400, detail="请上传至少一个文件")
|
| 135 |
+
if not user_id.strip():
|
| 136 |
+
raise HTTPException(status_code=400, detail="命名空间不能为空")
|
| 137 |
+
|
| 138 |
+
semaphore = asyncio.Semaphore(MAX_PARALLEL_FILES)
|
| 139 |
+
|
| 140 |
+
async def save_with_limit(f):
|
| 141 |
+
async with semaphore:
|
| 142 |
+
return await save_file_with_version_and_deduplicate(f, user_id)
|
| 143 |
+
|
| 144 |
+
save_tasks = [save_with_limit(f) for f in files]
|
| 145 |
+
save_results = await asyncio.gather(*save_tasks)
|
| 146 |
+
|
| 147 |
+
saved_files = []
|
| 148 |
+
save_errors = []
|
| 149 |
+
duplicate_files = []
|
| 150 |
+
|
| 151 |
+
for res in save_results:
|
| 152 |
+
if not res["success"]:
|
| 153 |
+
save_errors.append(res)
|
| 154 |
+
elif res.get("is_duplicate"):
|
| 155 |
+
duplicate_files.append(res)
|
| 156 |
+
else:
|
| 157 |
+
saved_files.append(res)
|
| 158 |
+
|
| 159 |
+
# ============================
|
| 160 |
+
# ✅ 企业级精简版:只存 1 条总结记忆(包含所有文件路径+状态)
|
| 161 |
+
# ============================
|
| 162 |
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 163 |
+
total = len(files)
|
| 164 |
+
success_cnt = len(saved_files)
|
| 165 |
+
dup_cnt = len(duplicate_files)
|
| 166 |
+
fail_cnt = len(save_errors)
|
| 167 |
+
|
| 168 |
+
# 构建清晰的文件清单
|
| 169 |
+
success_files = "\n".join([f"- {f['filename']} | 路径:{f['file_path']}" for f in saved_files])
|
| 170 |
+
dup_files_list = "\n".join([f"- {d['filename']}" for d in duplicate_files])
|
| 171 |
+
err_files_list = "\n".join([f"- {e['filename']} | 原因:{e['error']}" for e in save_errors])
|
| 172 |
+
|
| 173 |
+
# ============================
|
| 174 |
+
# ✅ 记忆存储结束
|
| 175 |
+
# ============================
|
| 176 |
+
summary_content = f"""【文件上传总结】{now} | 用户:{user_id}
|
| 177 |
+
总文件数:{total}
|
| 178 |
+
✅ 上传成功:{success_cnt} 个
|
| 179 |
+
{success_files if success_files else '无'}
|
| 180 |
+
|
| 181 |
+
⚠️ 重复文件:{dup_cnt} 个
|
| 182 |
+
{dup_files_list if dup_files_list else '无'}
|
| 183 |
+
|
| 184 |
+
❌ 上传失败:{fail_cnt} 个
|
| 185 |
+
{err_files_list if err_files_list else '无'}
|
| 186 |
+
"""
|
| 187 |
+
summary_tasks = {
|
| 188 |
+
"action": "add",
|
| 189 |
+
"user_id": user_id,
|
| 190 |
+
"memory_type": "episodic",
|
| 191 |
+
"content": summary_content.strip(),
|
| 192 |
+
"importance": 0.8,
|
| 193 |
+
"session_id": None
|
| 194 |
+
}
|
| 195 |
+
redis.rpush(QUEUE_MEMORY, json.dumps(summary_tasks))
|
| 196 |
+
|
| 197 |
+
if not saved_files:
|
| 198 |
+
return {
|
| 199 |
+
"success": len(save_errors) == 0,
|
| 200 |
+
"msg": f"去重 {len(duplicate_files)} 个 | 失败 {len(save_errors)} 个",
|
| 201 |
+
"duplicate_count": len(duplicate_files),
|
| 202 |
+
"results": [],
|
| 203 |
+
"errors": save_errors,
|
| 204 |
+
"duplicates": duplicate_files
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
# RAG qdrant 入库
|
| 208 |
+
task = {
|
| 209 |
+
"action": "add_document",
|
| 210 |
+
"file_path": saved_files,
|
| 211 |
+
"user_id": user_id
|
| 212 |
+
}
|
| 213 |
+
redis.rpush(QUEUE_RAG_QDRANT, json.dumps(task))
|
| 214 |
+
|
| 215 |
+
# RAG neo4j 入库
|
| 216 |
+
task_neo4j = {
|
| 217 |
+
"action": "add_neo4j_document",
|
| 218 |
+
"file_path": saved_files,
|
| 219 |
+
"user_id": user_id
|
| 220 |
+
}
|
| 221 |
+
redis.rpush(QUEUE_RAG_NEO4J, json.dumps(task_neo4j))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
return {
|
| 225 |
+
"success": True,
|
| 226 |
+
"msg": f"成功 {len(saved_files)} | 去重 {len(duplicate_files)}",
|
| 227 |
+
"data": {
|
| 228 |
+
"total": len(files),
|
| 229 |
+
"saved": len(saved_files),
|
| 230 |
+
"success": saved_files,
|
| 231 |
+
"duplicate_files": duplicate_files,
|
| 232 |
+
"namespace": user_id
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
# ------------------------------
|
| 237 |
+
# 路由
|
| 238 |
+
# ------------------------------
|
| 239 |
+
@new_updateFile_router.post("/new_update_file")
|
| 240 |
+
async def new_update_file(
|
| 241 |
+
request: Request,
|
| 242 |
+
files: List[UploadFile] = File(..., description="支持 txt, md, pdf, docx, doc, json"),
|
| 243 |
+
namespace: str = Form(..., description="命名空间")
|
| 244 |
+
) -> dict:
|
| 245 |
+
return await process_uploaded_files(files, namespace)
|
backend/backend_app/api/llm_api/universal/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""全能企业级助手API模块"""
|
backend/backend_app/api/llm_api/universal/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (231 Bytes). View file
|
|
|
backend/backend_app/api/llm_api/universal/__pycache__/universal_router.cpython-311.pyc
ADDED
|
Binary file (7.16 kB). View file
|
|
|
backend/backend_app/api/llm_api/universal/universal_router.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import asyncio
|
| 3 |
+
import nest_asyncio
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from fastapi import APIRouter, Request, HTTPException
|
| 6 |
+
from pydantic import BaseModel, Field, field_validator
|
| 7 |
+
from typing import Optional, Dict, Any
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
from helloAgents.agents.universal_enterprise_agent import KnowledgeBaseAssistant
|
| 11 |
+
from helloAgents.core.llm import HelloAgentsLLM
|
| 12 |
+
from helloAgents.tools.registry import global_registry
|
| 13 |
+
from helloAgents.core.exceptions import (
|
| 14 |
+
HelloAgentsException,
|
| 15 |
+
ToolException
|
| 16 |
+
)
|
| 17 |
+
nest_asyncio.apply()
|
| 18 |
+
|
| 19 |
+
# 评估工具
|
| 20 |
+
from helloAgents.tools.builtin.bfcl_evaluation_tool import BFCLEvaluationTool
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
universal_router = APIRouter()
|
| 24 |
+
|
| 25 |
+
# ==================== 请求/响应模型 ====================
|
| 26 |
+
class UniversalChatRequest(BaseModel):
|
| 27 |
+
text: str = Field(..., description="用户输入的文本", max_length=5000)
|
| 28 |
+
user_id: str = Field(..., description="用户ID(必需)")
|
| 29 |
+
agent_id: Optional[str] = Field("universal_assistant")
|
| 30 |
+
session_id: Optional[str] = Field(None)
|
| 31 |
+
enable_memory: Optional[bool] = Field(True)
|
| 32 |
+
enable_rag: Optional[bool] = Field(True)
|
| 33 |
+
max_context_length: Optional[int] = Field(2000, ge=100, le=8000)
|
| 34 |
+
tool_choice: Optional[str] = Field("auto")
|
| 35 |
+
|
| 36 |
+
@field_validator("user_id")
|
| 37 |
+
def validate_user_id(cls, v):
|
| 38 |
+
if not re.match(r"^[a-zA-Z0-9_-]{3,50}$", v):
|
| 39 |
+
raise ValueError("用户ID格式错误")
|
| 40 |
+
return v
|
| 41 |
+
|
| 42 |
+
class UniversalChatResponse(BaseModel):
|
| 43 |
+
success: bool
|
| 44 |
+
data: str
|
| 45 |
+
session_id: str
|
| 46 |
+
user_id: str
|
| 47 |
+
agent_id: str
|
| 48 |
+
tool_calls: Optional[int]
|
| 49 |
+
timestamp: str
|
| 50 |
+
|
| 51 |
+
class ToolStatisticsResponse(BaseModel):
|
| 52 |
+
success: bool
|
| 53 |
+
statistics: Dict[str, Any]
|
| 54 |
+
timestamp: str
|
| 55 |
+
|
| 56 |
+
# ==================== 【终极正确】异步Agent → 同步适配(FastAPI安全版)====================
|
| 57 |
+
class SyncAgentWrapper:
|
| 58 |
+
def __init__(self, async_agent):
|
| 59 |
+
self.agent = async_agent
|
| 60 |
+
self.name = async_agent.name
|
| 61 |
+
|
| 62 |
+
def run(self, prompt, **kwargs):
|
| 63 |
+
# ✅ 唯一在 FastAPI 中安全运行异步 Agent 的方式
|
| 64 |
+
return asyncio.run(self.agent.run(prompt,** kwargs))
|
| 65 |
+
|
| 66 |
+
# ==================== 正常聊天接口 ====================
|
| 67 |
+
@universal_router.post("/chat", response_model=UniversalChatResponse)
|
| 68 |
+
async def universal_chat(
|
| 69 |
+
request: Request,
|
| 70 |
+
body: UniversalChatRequest
|
| 71 |
+
) -> UniversalChatResponse:
|
| 72 |
+
try:
|
| 73 |
+
agent = KnowledgeBaseAssistant(
|
| 74 |
+
name="通用助手",
|
| 75 |
+
llm=HelloAgentsLLM(),
|
| 76 |
+
tool_registry=global_registry
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
response = await agent.run(
|
| 80 |
+
input_text=body.text,
|
| 81 |
+
user_id=body.user_id,
|
| 82 |
+
agent_id=body.agent_id,
|
| 83 |
+
session_id=body.session_id
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
session_id = body.session_id or f"ses_{int(datetime.now().timestamp())}"
|
| 87 |
+
|
| 88 |
+
return UniversalChatResponse(
|
| 89 |
+
success=True,
|
| 90 |
+
data=response,
|
| 91 |
+
session_id=session_id,
|
| 92 |
+
user_id=body.user_id,
|
| 93 |
+
agent_id=body.agent_id,
|
| 94 |
+
tool_calls=0,
|
| 95 |
+
timestamp=datetime.now().isoformat()
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
except HelloAgentsException as e:
|
| 99 |
+
logger.error(f"业务异常: {e}", exc_info=True)
|
| 100 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 101 |
+
except Exception as e:
|
| 102 |
+
logger.error(f"聊天失败: {e}", exc_info=True)
|
| 103 |
+
raise HTTPException(status_code=500, detail="服务内部错误")
|
| 104 |
+
|
| 105 |
+
# ==================== 【独立】BFCL评估接口(不影响正常业务) ====================
|
| 106 |
+
@universal_router.get("/bfcl-eval")
|
| 107 |
+
def run_bfcl_evaluation(): # 注意:这里必须是 同步函数!
|
| 108 |
+
try:
|
| 109 |
+
# 初始化
|
| 110 |
+
llm = HelloAgentsLLM()
|
| 111 |
+
agent = KnowledgeBaseAssistant(name="BFCL-Eval-Agent", llm=llm)
|
| 112 |
+
sync_agent = SyncAgentWrapper(agent)
|
| 113 |
+
|
| 114 |
+
# 执行评估
|
| 115 |
+
bfcl_tool = BFCLEvaluationTool()
|
| 116 |
+
result = bfcl_tool.run(
|
| 117 |
+
agent=sync_agent,
|
| 118 |
+
category="simple_python",
|
| 119 |
+
max_samples=0,
|
| 120 |
+
run_official_eval=True
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
return {
|
| 124 |
+
"success": True,
|
| 125 |
+
"result": result
|
| 126 |
+
}
|
| 127 |
+
except Exception as e:
|
| 128 |
+
return {"success": False, "error": str(e)}
|
backend/backend_app/api/video/README.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 视频生成API文档
|
| 2 |
+
|
| 3 |
+
## 概述
|
| 4 |
+
视频生成API提供企业级文本到视频生成服务,支持多种视频风格、分辨率、时长等参数控制,可生成MP4、AVI、MOV等多种格式的视频文件,支持背景音乐和语音解说。
|
| 5 |
+
|
| 6 |
+
## API端点
|
| 7 |
+
|
| 8 |
+
### 1. 生成视频
|
| 9 |
+
**POST** `/api/v1/video/generate`
|
| 10 |
+
|
| 11 |
+
**请求体:**
|
| 12 |
+
```json
|
| 13 |
+
{
|
| 14 |
+
"prompt": "一只小鸟从天空飞过,落在树枝上,背景是森林",
|
| 15 |
+
"style": "animation",
|
| 16 |
+
"duration": 5,
|
| 17 |
+
"resolution": "720p",
|
| 18 |
+
"aspect_ratio": "16:9",
|
| 19 |
+
"frame_rate": 30,
|
| 20 |
+
"background_music": false,
|
| 21 |
+
"voice_over": true,
|
| 22 |
+
"voice_over_text": "这是一只美丽的小鸟,它在森林中自由飞翔",
|
| 23 |
+
"user_id": "user_12345"
|
| 24 |
+
}
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
**参数说明:**
|
| 28 |
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
| 29 |
+
|------|------|------|--------|------|
|
| 30 |
+
| prompt | string | 是 | - | 视频描述文本,最长2000字符 |
|
| 31 |
+
| style | string | 否 | animation | 视频风格:animation(动画)、realistic(真人实拍)、3d(3D建模)、cartoon(卡通)、cinematic(电影感) |
|
| 32 |
+
| duration | int | 否 | 5 | 视频时长(秒),范围1-60 |
|
| 33 |
+
| resolution | string | 否 | 720p | 分辨率:480p、720p、1080p、4k |
|
| 34 |
+
| aspect_ratio | string | 否 | 16:9 | 宽高比:16:9、4:3、1:1、9:16 |
|
| 35 |
+
| frame_rate | int | 否 | 30 | 帧率,范围1-60 |
|
| 36 |
+
| background_music | bool | 否 | false | 是否添加背景音乐 |
|
| 37 |
+
| voice_over | bool | 否 | false | 是否添加语音解说 |
|
| 38 |
+
| voice_over_text | string | 否 | null | 语音解说文本 |
|
| 39 |
+
| user_id | string | 否 | null | 用户ID,用于统计和配额管理 |
|
| 40 |
+
|
| 41 |
+
**响应:**
|
| 42 |
+
```json
|
| 43 |
+
{
|
| 44 |
+
"success": true,
|
| 45 |
+
"video_url": "/api/video/files/video_abc123.mp4",
|
| 46 |
+
"video_id": "video_abc123",
|
| 47 |
+
"duration": 5,
|
| 48 |
+
"file_size": 1024000,
|
| 49 |
+
"resolution": "720p",
|
| 50 |
+
"format": "mp4",
|
| 51 |
+
"thumbnail_url": "/api/video/files/thumbnails/thumb_abc123.jpg",
|
| 52 |
+
"timestamp": "2024-01-01T12:00:00",
|
| 53 |
+
"estimated_generation_time": 30
|
| 54 |
+
}
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
### 2. 获取视频状态
|
| 58 |
+
**POST** `/api/v1/video/status`
|
| 59 |
+
|
| 60 |
+
**请求体:**
|
| 61 |
+
```json
|
| 62 |
+
{
|
| 63 |
+
"video_id": "video_abc123"
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
**响应:**
|
| 68 |
+
```json
|
| 69 |
+
{
|
| 70 |
+
"success": true,
|
| 71 |
+
"video_id": "video_abc123",
|
| 72 |
+
"status": "processing",
|
| 73 |
+
"progress": 65.5,
|
| 74 |
+
"estimated_completion_time": "2024-01-01T12:01:30",
|
| 75 |
+
"message": "正在生成视频帧...",
|
| 76 |
+
"timestamp": "2024-01-01T12:00:30"
|
| 77 |
+
}
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**状态说明:**
|
| 81 |
+
- `pending`: 等待处理
|
| 82 |
+
- `processing`: 处理中
|
| 83 |
+
- `completed`: 已完成
|
| 84 |
+
- `failed`: 失败
|
| 85 |
+
|
| 86 |
+
### 3. 获取视频文件
|
| 87 |
+
**GET** `/api/v1/video/files/{video_id}.{format}`
|
| 88 |
+
|
| 89 |
+
**参数:**
|
| 90 |
+
| 参数 | 类型 | 必填 | 说明 |
|
| 91 |
+
|------|------|------|------|
|
| 92 |
+
| video_id | string | 是 | 视频文件ID |
|
| 93 |
+
| format | string | 是 | 文件格式:mp4、avi、mov、webm |
|
| 94 |
+
|
| 95 |
+
**响应:**
|
| 96 |
+
返回视频文件流,Content-Type为对应的视频类型。
|
| 97 |
+
|
| 98 |
+
### 4. 获取视频缩略图
|
| 99 |
+
**GET** `/api/v1/video/files/thumbnails/{thumbnail_filename}`
|
| 100 |
+
|
| 101 |
+
**参数:**
|
| 102 |
+
| 参数 | 类型 | 必填 | 说明 |
|
| 103 |
+
|------|------|------|------|
|
| 104 |
+
| thumbnail_filename | string | 是 | 缩略图文件名,格式:thumb_{video_id}.jpg |
|
| 105 |
+
|
| 106 |
+
**响应:**
|
| 107 |
+
返回缩略图文件流,Content-Type为image/jpeg。
|
| 108 |
+
|
| 109 |
+
### 5. 获取支持的视频风格
|
| 110 |
+
**GET** `/api/v1/video/supported-styles`
|
| 111 |
+
|
| 112 |
+
**响应:**
|
| 113 |
+
```json
|
| 114 |
+
{
|
| 115 |
+
"success": true,
|
| 116 |
+
"styles": {
|
| 117 |
+
"animation": {"name": "动画", "description": "2D/3D动画风格"},
|
| 118 |
+
"realistic": {"name": "真人实拍", "description": "实拍视频效果"},
|
| 119 |
+
"3d": {"name": "3D建模", "description": "三维建模渲染"},
|
| 120 |
+
"cartoon": {"name": "卡通", "description": "卡通漫画风格"},
|
| 121 |
+
"cinematic": {"name": "电影感", "description": "电影级视觉效果"}
|
| 122 |
+
},
|
| 123 |
+
"resolutions": ["480p", "720p", "1080p", "4k"],
|
| 124 |
+
"formats": ["mp4", "avi", "mov", "webm"],
|
| 125 |
+
"timestamp": "2024-01-01T12:00:00"
|
| 126 |
+
}
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### 6. 列出视频文件
|
| 130 |
+
**GET** `/api/v1/video/list?page=1&page_size=20`
|
| 131 |
+
|
| 132 |
+
**查询参数:**
|
| 133 |
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
| 134 |
+
|------|------|------|--------|------|
|
| 135 |
+
| page | int | 否 | 1 | 页码 |
|
| 136 |
+
| page_size | int | 否 | 20 | 每页数量 |
|
| 137 |
+
|
| 138 |
+
**响应:**
|
| 139 |
+
```json
|
| 140 |
+
{
|
| 141 |
+
"success": true,
|
| 142 |
+
"videos": [
|
| 143 |
+
{
|
| 144 |
+
"video_id": "video_abc123",
|
| 145 |
+
"filename": "video_abc123.mp4",
|
| 146 |
+
"size": 1024000,
|
| 147 |
+
"created_at": "2024-01-01T12:00:00",
|
| 148 |
+
"format": "mp4",
|
| 149 |
+
"has_thumbnail": true
|
| 150 |
+
}
|
| 151 |
+
],
|
| 152 |
+
"total": 8,
|
| 153 |
+
"page": 1,
|
| 154 |
+
"page_size": 20,
|
| 155 |
+
"timestamp": "2024-01-01T12:00:00"
|
| 156 |
+
}
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
### 7. 删除视频文件
|
| 160 |
+
**POST** `/api/v1/video/delete`
|
| 161 |
+
|
| 162 |
+
**请求体:**
|
| 163 |
+
```json
|
| 164 |
+
{
|
| 165 |
+
"video_id": "video_abc123",
|
| 166 |
+
"confirm": true
|
| 167 |
+
}
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
**响应:**
|
| 171 |
+
```json
|
| 172 |
+
{
|
| 173 |
+
"success": true,
|
| 174 |
+
"message": "视频文件已成功删除",
|
| 175 |
+
"timestamp": "2024-01-01T12:00:00"
|
| 176 |
+
}
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
### 8. 健康检查
|
| 180 |
+
**GET** `/api/v1/video/health`
|
| 181 |
+
|
| 182 |
+
**响应:**
|
| 183 |
+
```json
|
| 184 |
+
{
|
| 185 |
+
"status": "healthy",
|
| 186 |
+
"service": "video_generation",
|
| 187 |
+
"output_dir": "./video_outputs",
|
| 188 |
+
"video_files_count": 15,
|
| 189 |
+
"thumbnail_files_count": 15,
|
| 190 |
+
"timestamp": "2024-01-01T12:00:00"
|
| 191 |
+
}
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## 错误处理
|
| 195 |
+
|
| 196 |
+
所有API端点都遵循统一的错误响应格式:
|
| 197 |
+
|
| 198 |
+
```json
|
| 199 |
+
{
|
| 200 |
+
"detail": "错误描述信息"
|
| 201 |
+
}
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
**HTTP状态码:**
|
| 205 |
+
- 200: 请求成功
|
| 206 |
+
- 400: 参数错误
|
| 207 |
+
- 404: 资源不存在
|
| 208 |
+
- 500: 服务器内部错误
|
| 209 |
+
|
| 210 |
+
## 企业级功能
|
| 211 |
+
|
| 212 |
+
### 1. 异步处理
|
| 213 |
+
支持长视频生成的异步处理,可通过状态查询接口实时获取生成进度。
|
| 214 |
+
|
| 215 |
+
### 2. 多格式支持
|
| 216 |
+
支持多种视频格式和分辨率,满足不同场景需求。
|
| 217 |
+
|
| 218 |
+
### 3. 缩略图生成
|
| 219 |
+
自动为每个视频生成缩略图,便于预览和展示。
|
| 220 |
+
|
| 221 |
+
### 4. 语音解说集成
|
| 222 |
+
支持为视频添加语音解说,实现文本到语音再到视频的完整流程。
|
| 223 |
+
|
| 224 |
+
### 5. 进度跟踪
|
| 225 |
+
实时进度跟踪和状态更新,提供良好的用户体验。
|
| 226 |
+
|
| 227 |
+
## 部署配置
|
| 228 |
+
|
| 229 |
+
### 环境变量
|
| 230 |
+
```
|
| 231 |
+
# 视频引擎配置
|
| 232 |
+
VIDEO_ENGINE=simulated # simulated, moviepy, svd, runwayml
|
| 233 |
+
VIDEO_API_KEY=your_api_key
|
| 234 |
+
VIDEO_MODEL=svd
|
| 235 |
+
|
| 236 |
+
# 存储配置
|
| 237 |
+
STORAGE_TYPE=local # local, s3, azure_blob, gcs
|
| 238 |
+
STORAGE_BUCKET=video-bucket
|
| 239 |
+
STORAGE_REGION=us-west-1
|
| 240 |
+
|
| 241 |
+
# 异步处理配置
|
| 242 |
+
VIDEO_ASYNC_PROCESSING=true
|
| 243 |
+
MAX_WORKERS=4
|
| 244 |
+
QUEUE_TIMEOUT=300
|
| 245 |
+
|
| 246 |
+
# API配置
|
| 247 |
+
API_RATE_LIMIT=30 # 视频生成较耗资源,限制更低
|
| 248 |
+
API_AUTH_REQUIRED=false
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### 依赖安装
|
| 252 |
+
```bash
|
| 253 |
+
# 基本依赖
|
| 254 |
+
pip install fastapi uvicorn python-multipart
|
| 255 |
+
|
| 256 |
+
# 视频处理依赖(根据选择的引擎)
|
| 257 |
+
pip install moviepy # 视频编辑和处理
|
| 258 |
+
pip install opencv-python # 计算机视觉库
|
| 259 |
+
pip install pillow # 图像处理
|
| 260 |
+
|
| 261 |
+
# AI模型相关(用于高级视频生成)
|
| 262 |
+
pip install diffusers # 扩散模型(用于视频生成)
|
| 263 |
+
pip install transformers # 已包含
|
| 264 |
+
pip install torch # 已包含
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
## 使用示例
|
| 268 |
+
|
| 269 |
+
### Python客户端
|
| 270 |
+
```python
|
| 271 |
+
import requests
|
| 272 |
+
import time
|
| 273 |
+
|
| 274 |
+
BASE_URL = "http://localhost:8000/api/v1"
|
| 275 |
+
|
| 276 |
+
def generate_video(prompt, style="animation"):
|
| 277 |
+
# 提交生成请求
|
| 278 |
+
response = requests.post(f"{BASE_URL}/video/generate", json={
|
| 279 |
+
"prompt": prompt,
|
| 280 |
+
"style": style,
|
| 281 |
+
"duration": 5,
|
| 282 |
+
"resolution": "720p"
|
| 283 |
+
})
|
| 284 |
+
|
| 285 |
+
if response.status_code != 200:
|
| 286 |
+
print(f"提交失败: {response.text}")
|
| 287 |
+
return None
|
| 288 |
+
|
| 289 |
+
data = response.json()
|
| 290 |
+
if not data["success"]:
|
| 291 |
+
print(f"生成失败: {data.get('message')}")
|
| 292 |
+
return None
|
| 293 |
+
|
| 294 |
+
video_id = data["video_id"]
|
| 295 |
+
print(f"视频生成已提交,ID: {video_id}")
|
| 296 |
+
|
| 297 |
+
# 轮询状态
|
| 298 |
+
while True:
|
| 299 |
+
status_response = requests.post(f"{BASE_URL}/video/status", json={
|
| 300 |
+
"video_id": video_id
|
| 301 |
+
})
|
| 302 |
+
|
| 303 |
+
if status_response.status_code == 200:
|
| 304 |
+
status_data = status_response.json()
|
| 305 |
+
if status_data["success"]:
|
| 306 |
+
status = status_data["status"]
|
| 307 |
+
progress = status_data["progress"]
|
| 308 |
+
|
| 309 |
+
print(f"进度: {progress}% - 状态: {status}")
|
| 310 |
+
|
| 311 |
+
if status == "completed":
|
| 312 |
+
print(f"视频生成完成: {data['video_url']}")
|
| 313 |
+
return data
|
| 314 |
+
elif status == "failed":
|
| 315 |
+
print("视频生成失败")
|
| 316 |
+
return None
|
| 317 |
+
|
| 318 |
+
time.sleep(2)
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
### JavaScript/TypeScript客户端
|
| 322 |
+
```typescript
|
| 323 |
+
const API_BASE_URL = 'http://localhost:8000/api/v1';
|
| 324 |
+
|
| 325 |
+
class VideoGenerator {
|
| 326 |
+
async generateVideo(prompt: string): Promise<any> {
|
| 327 |
+
// 提交生成请求
|
| 328 |
+
const response = await fetch(`${API_BASE_URL}/video/generate`, {
|
| 329 |
+
method: 'POST',
|
| 330 |
+
headers: {
|
| 331 |
+
'Content-Type': 'application/json',
|
| 332 |
+
},
|
| 333 |
+
body: JSON.stringify({
|
| 334 |
+
prompt,
|
| 335 |
+
style: 'animation',
|
| 336 |
+
duration: 5,
|
| 337 |
+
resolution: '720p'
|
| 338 |
+
})
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
if (!response.ok) {
|
| 342 |
+
throw new Error(`提交失败: ${response.status}`);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
const data = await response.json();
|
| 346 |
+
if (!data.success) {
|
| 347 |
+
throw new Error(`生成失败: ${data.message}`);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
return data;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
async checkStatus(videoId: string): Promise<any> {
|
| 354 |
+
const response = await fetch(`${API_BASE_URL}/video/status`, {
|
| 355 |
+
method: 'POST',
|
| 356 |
+
headers: {
|
| 357 |
+
'Content-Type': 'application/json',
|
| 358 |
+
},
|
| 359 |
+
body: JSON.stringify({ video_id: videoId })
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
if (!response.ok) {
|
| 363 |
+
throw new Error(`状态查询失败: ${response.status}`);
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
return await response.json();
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
async downloadVideo(videoId: string, format: string = 'mp4'): Promise<void> {
|
| 370 |
+
const url = `${API_BASE_URL}/video/files/${videoId}.${format}`;
|
| 371 |
+
window.open(url, '_blank');
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
```
|
| 375 |
+
|
| 376 |
+
## 性能优化建议
|
| 377 |
+
|
| 378 |
+
1. **异步队列**:使用消息队列处理视频生成任务,避免阻塞主线程
|
| 379 |
+
2. **GPU加速**:视频生成任务尽量使用GPU加速
|
| 380 |
+
3. **分级存储**:热数据使用SSD,冷数据迁移到低成本存储
|
| 381 |
+
4. **CDN分发**:生成的视频文件通过CDN分发,提高访问速度
|
| 382 |
+
5. **预览优化**:先生成低分辨率预览,再生成全分辨率版本
|
| 383 |
+
|
| 384 |
+
## 安全注意事���
|
| 385 |
+
|
| 386 |
+
1. **内容审核**:对用户输入的描述文本进行内容安全审核
|
| 387 |
+
2. **资源限制**:实施严格的资源使用限制,防止滥用
|
| 388 |
+
3. **访问控制**:敏感操作需要身份验证和授权
|
| 389 |
+
4. **数据隔离**:不同用户的数据进行逻辑或物理隔离
|
| 390 |
+
5. **监控告警**:实时监控系统资源使用情况,设置告警阈值
|
| 391 |
+
|
| 392 |
+
## 扩展功能
|
| 393 |
+
|
| 394 |
+
### 1. 批量生成
|
| 395 |
+
支持批量视频生成,提高处理效率。
|
| 396 |
+
|
| 397 |
+
### 2. 自定义模板
|
| 398 |
+
支持用户上传自定义视频模板。
|
| 399 |
+
|
| 400 |
+
### 3. 高级编辑
|
| 401 |
+
支持对生成的视频进行进一步编辑(裁剪、滤镜、字幕等)。
|
| 402 |
+
|
| 403 |
+
### 4. 多语言支持
|
| 404 |
+
支持更多语言的语音解说。
|
| 405 |
+
|
| 406 |
+
### 5. 实时预览
|
| 407 |
+
支持生成过程中的实时预览功能。
|
| 408 |
+
|
| 409 |
+
## 故障排除
|
| 410 |
+
|
| 411 |
+
### 常见问题
|
| 412 |
+
1. **生成速度慢**:检查GPU资源是否充足,考虑升级硬件或优化算法
|
| 413 |
+
2. **内存不足**:调整视频分辨率或时长,减少内存占用
|
| 414 |
+
3. **文件太大**:调整视频编码参数,优化文件大小
|
| 415 |
+
4. **质量不佳**:优化描述文本,调整生成参数
|
| 416 |
+
5. **服务不可用**:检查依赖服务状态,查看错误日志
|
| 417 |
+
|
| 418 |
+
### 日志查看
|
| 419 |
+
```bash
|
| 420 |
+
# 查看应用日志
|
| 421 |
+
tail -f logs/application.log
|
| 422 |
+
|
| 423 |
+
# 查看错误日志
|
| 424 |
+
tail -f logs/error.log
|
| 425 |
+
|
| 426 |
+
# 查看访问日志
|
| 427 |
+
tail -f logs/access.log
|
| 428 |
+
```
|