routes.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import base64
  2. from datetime import datetime
  3. from typing import List
  4. from fastapi import APIRouter, HTTPException
  5. from utils.device_id import get_device_id
  6. from .models import (
  7. ChatRequest,
  8. ChatResponse,
  9. OCRRequest,
  10. MessageCreateBill,
  11. ImageVectorRequest,
  12. ImageVectorResponse,
  13. BuildIndexRequest,
  14. BuildIndexResponse,
  15. SearchRequest,
  16. SearchResponse,
  17. SearchResultItem,
  18. )
  19. from core.chat_service import chat_service
  20. from core.agent_manager import agent_manager
  21. from core.image_search_service import image_search_service
  22. from utils.logger import chat_logger
  23. from tools.tool_factory import get_all_tools
  24. import time
  25. from utils.registration_manager import registration_manager
  26. from core.ocr_service import PaddleOCRService
  27. from core.document_processor.document_service import DocumentProcessingService
  28. from core.async_chat_service import async_chat_service
  29. from core.chat_result_manager import chat_result_manager
  30. from typing import Dict, Any
  31. from config.settings import settings
  32. # 初始化服务
  33. ocr_service = PaddleOCRService(
  34. # api_url="https://a8l0g1qda8zd48nb.aistudio-app.com/ocr",
  35. # token="f97d214abf87d5ea3c156e21257732a3b19661cb",
  36. api_url=settings.PADDLE_OCR_API_URL,
  37. token=settings.PADDLE_OCR_TOKEN,
  38. )
  39. doc_service = DocumentProcessingService(ocr_service=ocr_service)
  40. router = APIRouter()
  41. @router.post("/chat/async", response_model=Dict[str, Any])
  42. async def async_chat_endpoint(request: ChatRequest):
  43. """异步聊天接口 - 立即返回任务ID"""
  44. try:
  45. task_id = await async_chat_service.submit_chat_task(request.model_dump())
  46. return {
  47. "success": True,
  48. "task_id": task_id,
  49. "message": "任务已提交,请使用任务ID轮询结果",
  50. "polling_endpoint": f"/chat/result/{task_id}",
  51. }
  52. except Exception as e:
  53. chat_logger.error(f"异步聊天任务提交失败: {str(e)}")
  54. raise HTTPException(status_code=500, detail=f"任务提交失败: {str(e)}")
  55. @router.get("/chat/result/{task_id}", response_model=Dict[str, Any])
  56. async def get_chat_result(task_id: str):
  57. """获取聊天结果"""
  58. try:
  59. result = await async_chat_service.get_task_result(task_id)
  60. return result
  61. except Exception as e:
  62. chat_logger.error(f"获取聊天结果失败: {str(e)}")
  63. raise HTTPException(status_code=500, detail=f"获取结果失败: {str(e)}")
  64. @router.get("/chat/tasks/cleanup")
  65. async def cleanup_old_tasks():
  66. """清理旧任务数据(管理员接口)"""
  67. try:
  68. chat_result_manager.cleanup_old_tasks(max_days=7)
  69. return {"success": True, "message": "旧任务数据清理完成"}
  70. except Exception as e:
  71. chat_logger.error(f"清理旧任务失败: {str(e)}")
  72. raise HTTPException(status_code=500, detail=f"清理失败: {str(e)}")
  73. @router.post("/chat", response_model=ChatResponse)
  74. async def chat_endpoint(request: ChatRequest):
  75. """聊天接口"""
  76. try:
  77. result = await chat_service.process_chat_request(request.model_dump())
  78. return ChatResponse(**result)
  79. except Exception as e:
  80. chat_logger.error(f"API处理失败: {str(e)}")
  81. raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
  82. @router.post("/ocr")
  83. async def orc(request: OCRRequest):
  84. """OCR接口"""
  85. try:
  86. chat_logger.info(f"开始进行图片识别")
  87. # 1. 解码Base64图片
  88. try:
  89. if "," in request.image:
  90. # 去掉 data:image/xxx;base64, 前缀
  91. base64_str = request.image.split(",", 1)[1]
  92. else:
  93. base64_str = request.image
  94. image_bytes = base64.b64decode(base64_str)
  95. except Exception as e:
  96. chat_logger.error(f"图片解码失败: {e}")
  97. raise HTTPException(400, f"图片格式错误: {str(e)}")
  98. # 2. OCR识别
  99. result = await doc_service.pure_ocr(
  100. image_bytes=image_bytes, file_type=request.file_type
  101. )
  102. # 3. 返回结果
  103. return result
  104. except HTTPException:
  105. raise
  106. except Exception as e:
  107. chat_logger.error(f"处理失败: {e}")
  108. raise HTTPException(500, f"处理失败: {str(e)}")
  109. @router.post("/ocr_create_bill")
  110. async def ocr_create_bill_endpoint(request: OCRRequest):
  111. """
  112. 处理单张图片
  113. 请求格式:
  114. {
  115. "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
  116. "type": "invoice",
  117. }
  118. """
  119. try:
  120. chat_logger.info(f"开始处理 {request.type} 单据")
  121. # 1. 解码Base64图片
  122. try:
  123. if "," in request.image:
  124. # 去掉 data:image/xxx;base64, 前缀
  125. base64_str = request.image.split(",", 1)[1]
  126. else:
  127. base64_str = request.image
  128. image_bytes = base64.b64decode(base64_str)
  129. except Exception as e:
  130. chat_logger.error(f"图片解码失败: {e}")
  131. raise HTTPException(400, f"图片格式错误: {str(e)}")
  132. # 2. 处理单据
  133. result = await doc_service.ocr_create_bill(
  134. image_bytes=image_bytes, document_type=request.type
  135. )
  136. # 3. 返回结果
  137. return {
  138. "success": True,
  139. "type": request.type,
  140. "text": result.get("ocr_text", ""), # 识别出的文本
  141. "data": result.get("parsed_data", {}), # 结构化数据
  142. "timestamp": datetime.now().isoformat(),
  143. }
  144. except HTTPException:
  145. raise
  146. except Exception as e:
  147. chat_logger.error(f"处理失败: {e}")
  148. raise HTTPException(500, f"处理失败: {str(e)}")
  149. @router.post("/message_create_bill")
  150. async def message_create_bill_endpoint(request: MessageCreateBill):
  151. """
  152. 通过文本消息辅助建立单据
  153. 请求格式:
  154. {
  155. "message": "这是一条单据描述",
  156. "document_type": "invoice",
  157. }
  158. """
  159. try:
  160. result = await doc_service.message_create_bill(
  161. message=request.message, document_type=request.document_type
  162. )
  163. return result
  164. except Exception as e:
  165. chat_logger.error(f"处理失败: {e}")
  166. raise HTTPException(500, f"处理失败: {str(e)}")
  167. @router.get("/cache/status")
  168. async def cache_status():
  169. """查看缓存状态 - 修复版本"""
  170. try:
  171. cache_status = await agent_manager.get_cache_status()
  172. # ✅ 确保返回完整信息
  173. return {
  174. "success": True,
  175. "cache_size": cache_status.get("cache_size", 0),
  176. "cache_expiry_seconds": cache_status.get("cache_expiry_seconds", 3600),
  177. "cache_entries_count": len(cache_status.get("cache_entries", [])),
  178. "cache_entries": cache_status.get("cache_entries", []),
  179. "timestamp": time.time(),
  180. }
  181. except Exception as e:
  182. chat_logger.error(f"获取缓存状态失败: {str(e)}")
  183. return {
  184. "success": False,
  185. "error": str(e),
  186. "cache_size": 0,
  187. "cache_entries_count": 0,
  188. "cache_entries": [],
  189. }
  190. @router.delete("/cache/clear")
  191. async def clear_cache():
  192. """清空缓存"""
  193. try:
  194. # ✅ 使用异步版本
  195. cleared = await agent_manager.clear_cache()
  196. chat_logger.info(f"清空agent缓存, 清理数量={cleared}")
  197. return {
  198. "cleared_entries": cleared,
  199. "message": "缓存已清空",
  200. "status": "success",
  201. }
  202. except Exception as e:
  203. chat_logger.error(f"清空缓存失败: {str(e)}")
  204. raise HTTPException(status_code=500, detail=f"清空缓存失败: {str(e)}")
  205. @router.get("/health")
  206. async def health_check():
  207. """健康检查"""
  208. try:
  209. # ✅ 使用异步版本
  210. registration_status = await registration_manager.check_registration()
  211. return {
  212. "status": "healthy",
  213. "service": "龙嘉软件AI助手API",
  214. "registration_status": (
  215. "已注册" if registration_status else "未注册或注册过期"
  216. ),
  217. "device_id": get_device_id(),
  218. "timestamp": time.time(),
  219. }
  220. except Exception as e:
  221. return {
  222. "status": "unhealthy",
  223. "service": "龙嘉软件AI助手API",
  224. "error": str(e),
  225. "timestamp": time.time(),
  226. }
  227. @router.get("/tools/status")
  228. async def tools_status():
  229. """查看工具状态"""
  230. try:
  231. tools = get_all_tools()
  232. tool_info = []
  233. for i, tool in enumerate(tools):
  234. tool_info.append(
  235. {
  236. "index": i + 1,
  237. "name": getattr(tool, "name", "unknown"),
  238. "description": getattr(tool, "description", "unknown")[:100]
  239. + "...",
  240. }
  241. )
  242. return {"total_tools": len(tools), "tools": tool_info, "status": "success"}
  243. except Exception as e:
  244. chat_logger.error(f"获取工具状态失败: {str(e)}")
  245. raise HTTPException(status_code=500, detail=f"获取工具状态失败: {str(e)}")
  246. @router.post("/cache/initialize")
  247. async def initialize_cache():
  248. """初始化缓存系统"""
  249. try:
  250. # ✅ 使用异步版本
  251. await agent_manager.initialize()
  252. chat_logger.info("缓存系统初始化完成")
  253. return {"status": "success", "message": "缓存系统初始化完成"}
  254. except Exception as e:
  255. chat_logger.error(f"初始化缓存失败: {str(e)}")
  256. raise HTTPException(status_code=500, detail=f"初始化缓存失败: {str(e)}")
  257. @router.post("/cache/shutdown")
  258. async def shutdown_cache():
  259. """关闭缓存系统"""
  260. try:
  261. # ✅ 使用异步版本
  262. cleared = await agent_manager.shutdown()
  263. chat_logger.info(f"缓存系统已关闭,清理了 {cleared} 个实例")
  264. return {
  265. "cleared_entries": cleared,
  266. "message": "缓存系统已关闭",
  267. "status": "success",
  268. }
  269. except Exception as e:
  270. chat_logger.error(f"关闭缓存失败: {str(e)}")
  271. raise HTTPException(status_code=500, detail=f"关闭缓存失败: {str(e)}")
  272. @router.post("/admin/refresh-registration")
  273. async def refresh_registration():
  274. """手动刷新注册状态(管理员用)"""
  275. registration_manager.force_refresh()
  276. return {"status": "success", "message": "注册状态已刷新"}
  277. @router.get("/admin/registration-status")
  278. async def get_registration_status():
  279. """获取注册状态(管理员用)"""
  280. status = await registration_manager.check_registration()
  281. status_info = registration_manager.get_status()
  282. return {"is_registered": status, "status_info": status_info}
  283. @router.get("/")
  284. async def root():
  285. registration_status = await registration_manager.check_registration()
  286. base_info = {
  287. "service": "龙嘉软件AI助手API",
  288. "version": "1.0.0",
  289. "registration_status": "active" if registration_status else "expired",
  290. "device_id": get_device_id(),
  291. }
  292. if registration_status:
  293. base_info.update(
  294. {
  295. "endpoints": {
  296. "POST /chat": "聊天",
  297. "GET /health": "健康检查",
  298. "GET /cache/status": "查看agent缓存状态",
  299. "DELETE /cache/clear": "清空agent缓存",
  300. "GET /": "API信息",
  301. }
  302. }
  303. )
  304. else:
  305. base_info.update(
  306. {
  307. "message": "⚠️ 服务注册已过期,部分功能受限",
  308. "available_endpoints": {
  309. "GET /health": "健康检查",
  310. "GET /registration/status": "查看注册状态",
  311. "GET /service/info": "服务完整信息",
  312. "GET /": "API信息",
  313. },
  314. "restricted_endpoints": {"POST /chat": "AI聊天功能(需续费)"},
  315. "support_contact": "请联系管理员续费服务",
  316. }
  317. )
  318. return base_info
  319. @router.post("/image/vector/batch", response_model=List[ImageVectorResponse])
  320. async def batch_calculate_vectors_endpoint(requests: List[ImageVectorRequest]):
  321. """批量计算图片特征向量"""
  322. try:
  323. # 构建请求数据
  324. image_items = []
  325. for req in requests:
  326. image_items.append({"image": req.image, "image_id": req.image_id})
  327. # 调用服务
  328. results = await image_search_service.batch_calculate_vectors(image_items)
  329. # 构建响应
  330. responses = []
  331. for result in results:
  332. response = ImageVectorResponse(
  333. success=result.get("success", False),
  334. image_id=result.get("image_id"),
  335. vector=result.get("vector"),
  336. error=result.get("error"),
  337. )
  338. responses.append(response)
  339. return responses
  340. except Exception as e:
  341. chat_logger.error(f"批量计算图片特征向量失败: {str(e)}")
  342. raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
  343. @router.post("/image/index/build", response_model=BuildIndexResponse)
  344. async def build_index_endpoint(request: BuildIndexRequest):
  345. """构建索引及映射关系"""
  346. try:
  347. # 构建请求数据
  348. image_vectors = []
  349. for item in request.image_vectors:
  350. image_vectors.append(
  351. {
  352. "image_id": item.image_id,
  353. "vector": item.vector,
  354. "image_name": item.image_name,
  355. "image_path": item.image_path,
  356. }
  357. )
  358. # 调用服务
  359. indexed_count = await image_search_service.build_index(image_vectors)
  360. # 构建响应
  361. response = BuildIndexResponse(success=True, indexed_count=indexed_count)
  362. return response
  363. except Exception as e:
  364. chat_logger.error(f"构建索引失败: {str(e)}")
  365. raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
  366. @router.post("/image/search", response_model=SearchResponse)
  367. async def search_endpoint(request: SearchRequest):
  368. """搜索相似图片(支持以图搜图和以文搜图)"""
  369. try:
  370. import time
  371. start_time = time.time()
  372. # 处理图片数据
  373. image_bytes = None
  374. if request.image:
  375. try:
  376. if "," in request.image:
  377. base64_str = request.image.split(",", 1)[1]
  378. else:
  379. base64_str = request.image
  380. image_bytes = base64.b64decode(base64_str)
  381. except Exception as e:
  382. chat_logger.error(f"图片解码失败: {e}")
  383. raise HTTPException(400, f"图片格式错误: {str(e)}")
  384. # 调用服务
  385. results = await image_search_service.search(
  386. image_bytes=image_bytes, text=request.text, top_k=request.top_k
  387. )
  388. # 计算处理时间
  389. processing_time = time.time() - start_time
  390. # 构建响应
  391. search_results = []
  392. for result in results:
  393. item = SearchResultItem(
  394. image_id=result.get("image_id"),
  395. similarity=result.get("similarity"),
  396. image_name=result.get("image_name"),
  397. image_path=result.get("image_path"),
  398. )
  399. search_results.append(item)
  400. response = SearchResponse(
  401. success=True,
  402. results=search_results,
  403. total_count=len(search_results),
  404. processing_time=round(processing_time, 4),
  405. )
  406. return response
  407. except HTTPException:
  408. raise
  409. except Exception as e:
  410. chat_logger.error(f"搜索失败: {str(e)}")
  411. raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
  412. @router.get("/image/index/status")
  413. async def get_index_status_endpoint():
  414. """获取索引状态"""
  415. try:
  416. status = await image_search_service.get_index_status()
  417. return {"success": True, "status": status}
  418. except Exception as e:
  419. chat_logger.error(f"获取索引状态失败: {str(e)}")
  420. raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")
  421. @router.post("/image/index/clear")
  422. async def clear_index_endpoint():
  423. """清空索引"""
  424. try:
  425. await image_search_service.clear_index()
  426. return {"success": True, "message": "索引已清空"}
  427. except Exception as e:
  428. chat_logger.error(f"清空索引失败: {str(e)}")
  429. raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")