图片上传识别功能
This commit is contained in:
@@ -76,6 +76,7 @@ class PreviewChatTurnResponse(BaseModel):
|
||||
created_at: datetime
|
||||
user_text: str
|
||||
agent_text: str
|
||||
attachments: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -6,15 +6,17 @@ from __future__ import annotations
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.auth import get_current_user
|
||||
from app.core.config import settings
|
||||
from app.models.user import User
|
||||
from app.services.builtin_tools import _local_file_workspace_root
|
||||
from app.services.builtin_tools import _local_file_workspace_root, _resolve_path_under_workspace
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -110,3 +112,32 @@ async def upload_preview_file(
|
||||
size=total,
|
||||
content_type=file.content_type,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/preview/file")
|
||||
async def get_preview_file(
|
||||
file_path: str = Query(..., description="上传后返回的 relative_path"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
读取当前用户预览附件(用于前端历史回显图片缩略图)。
|
||||
仅允许访问 uploads/preview/<current_user.id>/ 下文件。
|
||||
"""
|
||||
path, err = _resolve_path_under_workspace(file_path)
|
||||
if err or path is None:
|
||||
raise HTTPException(status_code=400, detail=f"无效文件路径: {err or file_path}")
|
||||
if not path.is_file():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
|
||||
root = _local_file_workspace_root()
|
||||
try:
|
||||
rel = path.relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=403, detail="不允许访问该文件")
|
||||
|
||||
prefix = f"uploads/preview/{current_user.id}/"
|
||||
if not rel.startswith(prefix):
|
||||
raise HTTPException(status_code=403, detail="无权访问该文件")
|
||||
|
||||
media_type, _ = mimetypes.guess_type(str(path))
|
||||
return FileResponse(path=str(path), media_type=media_type or "application/octet-stream")
|
||||
|
||||
@@ -97,6 +97,31 @@ def _user_id_from_input(input_data: Optional[Dict[str, Any]]) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_attachments(input_data: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
if not input_data:
|
||||
return []
|
||||
raw = input_data.get("attachments")
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
out: List[Dict[str, Any]] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
rel = str(item.get("relative_path") or "").strip()
|
||||
name = str(item.get("filename") or "").strip()
|
||||
if not rel:
|
||||
continue
|
||||
content_type = str(item.get("content_type") or "").strip() or None
|
||||
out.append(
|
||||
{
|
||||
"relative_path": rel,
|
||||
"filename": name or rel.rsplit("/", 1)[-1],
|
||||
"content_type": content_type,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def fetch_agent_preview_chat_turns(
|
||||
db: Session,
|
||||
agent_id: str,
|
||||
@@ -149,6 +174,7 @@ def fetch_agent_preview_chat_turns(
|
||||
"created_at": ex.created_at,
|
||||
"user_text": user_text or "(无文本)",
|
||||
"agent_text": agent_text or "(无输出)",
|
||||
"attachments": _extract_attachments(inp),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
58
backend/scripts/check_ocr_env.py
Normal file
58
backend/scripts/check_ocr_env.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""检查图片 OCR 环境:Pillow、pytesseract、Tesseract 可执行文件、chi_sim 语言包。
|
||||
|
||||
在 backend 目录执行:
|
||||
.\\venv\\Scripts\\python scripts\\check_ocr_env.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 保证能加载 app 配置
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.core.config import settings # noqa: E402
|
||||
from app.services.builtin_tools import _local_file_workspace_root, _tessdata_dir_for_ocr # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("TESSERACT_CMD (settings):", settings.TESSERACT_CMD or "(空,将尝试 PATH)")
|
||||
print("TESSERACT_TESSDATA_DIR (settings):", settings.TESSERACT_TESSDATA_DIR or "(空,将尝试仓库 tessdata/)")
|
||||
try:
|
||||
import PIL # noqa: F401
|
||||
|
||||
print("Pillow: OK")
|
||||
except ImportError as e:
|
||||
print("Pillow: 缺失 —", e)
|
||||
print(" 请执行: pip install Pillow")
|
||||
return 2
|
||||
try:
|
||||
import pytesseract as pt
|
||||
|
||||
print("pytesseract: OK")
|
||||
except ImportError as e:
|
||||
print("pytesseract: 缺失 —", e)
|
||||
print(" 请执行: pip install pytesseract")
|
||||
return 3
|
||||
cmd = (settings.TESSERACT_CMD or "").strip()
|
||||
if cmd:
|
||||
pt.pytesseract.tesseract_cmd = cmd
|
||||
try:
|
||||
ver = pt.get_tesseract_version()
|
||||
print("Tesseract 版本:", ver)
|
||||
except Exception as e:
|
||||
print("Tesseract 可执行文件: 不可用 —", e)
|
||||
print(" Windows 请安装 Tesseract,并在 .env 设置 TESSERACT_CMD=.../tesseract.exe")
|
||||
return 4
|
||||
td = _tessdata_dir_for_ocr()
|
||||
print("解析到的 tessdata 目录:", td or "(未找到)")
|
||||
root = _local_file_workspace_root()
|
||||
loc = root / "tessdata"
|
||||
if loc.is_dir():
|
||||
has_chi = any(loc.glob("chi_sim.traineddata"))
|
||||
print("仓库 tessdata/chi_sim.traineddata:", "存在" if has_chi else "缺失(中文识别差)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user