Files
AI_agent_detect/main.py

322 lines
10 KiB
Python
Raw Normal View History

2025-12-02 17:16:26 +08:00
import io
import os
import time
2025-12-02 17:16:26 +08:00
import tempfile
import shutil
2025-12-02 17:16:26 +08:00
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional
2025-12-02 17:16:26 +08:00
import requests
import uvicorn
from PIL import Image
from fastapi import FastAPI, HTTPException, UploadFile, File
2025-12-02 17:16:26 +08:00
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
2025-12-02 17:16:26 +08:00
from pydantic import BaseModel, HttpUrl
2025-12-03 11:22:41 +08:00
# 只导入AI_Agent的统一入口方法不再导入多个内部方法
from AI_Agent import generate_tender_from_input
2025-12-02 17:16:26 +08:00
from config import DetectionResponse
from process import detect_large_image_from_url
2025-12-03 11:22:41 +08:00
# 配置文件存储路径(使用绝对路径确保一致性)
BASE_DIR = Path(__file__).parent.resolve() # 项目根目录绝对路径
UPLOAD_DIR = BASE_DIR / "uploaded_files"
OUTPUT_DIR = BASE_DIR / "generated_tenders"
UPLOAD_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
2025-12-02 17:16:26 +08:00
# 全局检测管理器
detector_manager = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global detector_manager
try:
from manager import UnifiedDetectionManager
detector_manager = UnifiedDetectionManager()
print("检测管理器初始化成功")
except Exception as e:
print(f"初始化失败:{str(e)}")
raise
yield
# 程序关闭时清理临时文件(可选)
print("清理临时文件...")
for file in UPLOAD_DIR.glob("*"):
try:
if file.is_file():
file.unlink()
except Exception as e:
print(f"清理文件 {file} 失败:{e}")
2025-12-02 17:16:26 +08:00
app = FastAPI(lifespan=lifespan, title="目标检测与投标文件生成API", version="1.0.0")
2025-12-02 17:16:26 +08:00
# 配置跨域请求
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class DetectionRequest(BaseModel):
type: str
url: HttpUrl
class DetectionProcessRequest(BaseModel):
url: HttpUrl
class TenderGenerateResponse(BaseModel):
"""投标文件生成响应模型"""
status: str
message: str
relative_path: str
file_name: str
file_size: Optional[int] = None
2025-12-02 17:16:26 +08:00
@app.post("/detect_image", response_model=DetectionResponse)
async def run_detection_image(request: DetectionRequest):
# 解析检测类型
requested_types = {t.strip().lower() for t in request.type.split(',') if t.strip()}
print(f"请求的检测类型: {requested_types}")
if not requested_types:
raise HTTPException(status_code=400, detail="未指定检测类型")
# 下载图片
try:
response = requests.get(str(request.url), timeout=15)
response.raise_for_status()
# 获取图片尺寸
with Image.open(io.BytesIO(response.content)) as img:
img_size = [img.width, img.height]
# 创建临时文件
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file:
temp_file.write(response.content)
temp_path = temp_file.name
except Exception as e:
raise HTTPException(status_code=400, detail=f"图片处理失败:{str(e)}")
# 执行检测
results = []
errors = []
try:
detection_results = detector_manager.detect(temp_path, ",".join(requested_types))
if detection_results:
results = detection_results
except Exception as e:
errors.append(f"检测失败:{str(e)}")
finally:
# 清理临时文件
if os.path.exists(temp_path):
os.remove(temp_path)
return {
"hasTarget": 1 if results else 0,
"originalImgSize": img_size,
"targets": results,
"processing_errors": errors
}
@app.post("/detect_process", response_model=DetectionResponse)
async def run_detection_process(request: DetectionProcessRequest):
return detect_large_image_from_url(str(request.url))
@app.get("/supported_types")
async def get_supported_types():
if detector_manager:
info = detector_manager.get_available_info()
return {
"supported_types": info["supported_types"],
}
return {"supported_types": []}
@app.post("/generate_tender", response_model=TenderGenerateResponse)
async def generate_tender_file(file: UploadFile = File(...)):
"""
上传招标文件Word格式生成投标文件
2025-12-03 11:22:41 +08:00
支持的文件格式.docx
返回生成文件的相对路径用于下载接口
"""
2025-12-03 11:22:41 +08:00
# 初始化upload_path变量避免UnboundLocalError
upload_path = None
# 验证文件类型
if not file.filename.endswith((".docx", ".doc")):
raise HTTPException(
status_code=400,
detail="仅支持Word文件.docx或.doc格式"
)
try:
# 保存上传的文件
timestamp = int(time.time())
file_ext = Path(file.filename).suffix
upload_filename = f"tender_{timestamp}{file_ext}"
upload_path = UPLOAD_DIR / upload_filename
# 保存文件内容
with open(upload_path, "wb") as f:
shutil.copyfileobj(file.file, f)
print(f"已接收上传文件:{upload_path}")
2025-12-03 11:22:41 +08:00
# 生成输出文件名和路径(使用绝对路径)
output_filename = f"投标文件_生成版_{timestamp}.docx"
output_path = OUTPUT_DIR / output_filename
2025-12-03 11:22:41 +08:00
output_path_abs = str(output_path.resolve()) # 绝对路径
# 调用AI_Agent的统一入口方法仅这一个调用
print("开始生成投标文件...")
generate_success = generate_tender_from_input(
2025-12-03 11:22:41 +08:00
input_word_path=str(upload_path.resolve()), # 传入绝对路径
output_word_path=output_path_abs
)
if not generate_success:
raise Exception("投标文件生成核心流程执行失败")
# 验证生成的文件是否存在
if not output_path.exists() or not output_path.is_file():
raise Exception("投标文件生成后未找到目标文件")
# 计算文件大小
file_size = output_path.stat().st_size
2025-12-03 11:22:41 +08:00
# 修复使用os.path.relpath计算相对路径更灵活
try:
# 计算相对于项目根目录的相对路径
relative_path = os.path.relpath(output_path_abs, str(BASE_DIR))
# 统一路径分隔符为 '/'避免Windows和Linux差异
relative_path = relative_path.replace(os.sep, '/')
except Exception as e:
# 异常情况下直接返回文件名(降级方案)
relative_path = output_filename
print(f"计算相对路径失败,使用降级方案:{e}")
2025-12-03 11:22:41 +08:00
print(f"投标文件生成成功:{output_path_abs}")
print(f"相对路径:{relative_path}")
return {
"status": "success",
"message": "投标文件生成完成",
"relative_path": relative_path,
"file_name": output_filename,
"file_size": file_size
}
except Exception as e:
error_msg = f"生成投标文件失败:{str(e)}"
print(error_msg)
raise HTTPException(
status_code=500,
detail=error_msg
)
finally:
# 关闭上传文件流
await file.close()
# 清理上传的原始文件,节省空间
if upload_path and upload_path.exists():
try:
upload_path.unlink()
print(f"已清理上传文件:{upload_path}")
except Exception as e:
print(f"清理上传文件失败:{e}")
@app.get("/download_file")
async def download_generated_file(relative_path: str):
"""
根据相对路径下载生成的投标文件
2025-12-03 11:22:41 +08:00
Args:
relative_path: 生成文件的相对路径/generate_tender接口获取
"""
try:
2025-12-03 11:22:41 +08:00
# 修复:将相对路径转换为绝对路径
# 统一路径分隔符
relative_path = relative_path.replace('/', os.sep)
# 拼接项目根目录得到绝对路径
abs_path = BASE_DIR / relative_path
abs_path = abs_path.resolve() # 解析完整路径
# 验证路径是否在允许的输出目录内
if not abs_path.is_relative_to(OUTPUT_DIR.resolve()):
raise HTTPException(
status_code=403,
detail="访问禁止:文件路径不在允许范围内"
)
if not abs_path.exists() or not abs_path.is_file():
raise HTTPException(
status_code=404,
detail="文件不存在或已被删除"
)
# 返回文件下载响应
return FileResponse(
path=abs_path,
filename=abs_path.name,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
except HTTPException:
raise
except Exception as e:
print(f"文件下载失败:{str(e)}")
raise HTTPException(
status_code=500,
detail=f"文件下载失败:{str(e)}"
)
@app.get("/list_generated_files")
async def list_generated_files():
"""列出所有生成的投标文件(可选接口)"""
files = []
for file in OUTPUT_DIR.glob("*.docx"):
2025-12-03 11:22:41 +08:00
# 计算相对路径
try:
relative_path = os.path.relpath(str(file.resolve()), str(BASE_DIR))
relative_path = relative_path.replace(os.sep, '/')
except:
relative_path = file.name
files.append({
"file_name": file.name,
2025-12-03 11:22:41 +08:00
"relative_path": relative_path,
"file_size": file.stat().st_size,
"created_time": file.stat().st_ctime
})
return {
"total": len(files),
"files": files
}
2025-12-02 17:16:26 +08:00
if __name__ == "__main__":
import argparse
# 补充必要的导入(在主函数中导入,避免启动时依赖冲突)
2025-12-02 17:16:26 +08:00
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--reload", action="store_true")
args = parser.parse_args()
uvicorn.run("main:app", host=args.host, port=args.port, reload=args.reload)