diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index 11d0707..32abd7f 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -2400,27 +2400,63 @@ class OrderService: def parse_logistics_table_images(self, image_urls, session): - """识别物流表格截图,用 LLM 从 OCR 文本中提取快递单号和收件人。""" + """识别物流表格截图,用 LLM 视觉能力直接从图片中提取快递单号和收件人。""" + import base64 as _b64 + import json as _json import logging as _log - _logger = _log.getLogger(__name__) - from backend.app.services.ai_service import ai_service + from urllib import request as urllib_request + from backend.app.services.ai_service import read_image_bytes from backend.app.core.config import get_settings + + _logger = _log.getLogger(__name__) settings = get_settings() + api_key = settings.llm_parse_api_key or settings.aliyun_ai_access_key_id + + system_prompt = ( + "你是物流信息提取助手。从物流表格截图中提取每一行的快递单号和收件人姓名。\n" + "表格通常包含:运单号、运单状态、件数、体积、计费重量、付款方式、运费、" + "保价费、包装服务费、信息费、代收货款、签收费、声明价值、产品类型、" + "增值服务、服务方式、业务属性、托寄物、寄件人、收件人、目的网点 等列。\n\n" + "严格按以下 JSON 数组格式输出,不要添加任何其他内容:\n" + '[{"tracking_number":"运单号","express_company":"快递公司名或null","recipient_name":"收件人姓名"}]' + ) + results = [] seen_tracking = set() for url in image_urls: try: _logger.info("[parse_logistics_table_images] 处理图片: %s", url) - ctx = ai_service._ocr_and_parse_image(url) - raw_text = ctx.get("raw_text", "") - _logger.info("[parse_logistics_table_images] OCR原始文本长度: %d, 前500字: %s", - len(raw_text), raw_text[:500]) - if not raw_text.strip(): - _logger.warning("[parse_logistics_table_images] OCR未识别到文字") - continue - # 用 LLM 从表格文本中提取快递单号和收件人 - llm_items = self._llm_extract_logistics(raw_text, settings) - _logger.info("[parse_logistics_table_images] LLM提取结果: %d条", len(llm_items)) + image_bytes = read_image_bytes(url) + image_b64 = _b64.b64encode(image_bytes).decode("utf-8") + suffix = url.rsplit(".", 1)[-1].lower() if "." in url else "jpeg" + mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "bmp": "image/bmp"}.get(suffix, "image/jpeg") + + payload = _json.dumps({ + "model": settings.llm_parse_model, + "messages": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": [ + {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{image_b64}"}}, + {"type": "text", "text": "请从这个物流表格截图中提取每一行的快递单号和收件人姓名:"}, + ]}, + ], + "temperature": 0.1, + "max_tokens": 4096, + }).encode("utf-8") + + req = urllib_request.Request( + url=settings.llm_parse_api_url, data=payload, + headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, + method="POST", + ) + with urllib_request.urlopen(req, timeout=60) as resp: + result = _json.loads(resp.read().decode("utf-8")) + + content = result["choices"][0]["message"]["content"] + _logger.info("[parse_logistics_table_images] LLM响应: %s", content[:500]) + llm_items = self._extract_json_array(content) + _logger.info("[parse_logistics_table_images] 提取结果: %d条", len(llm_items)) + for item in llm_items: tn = (item.get("tracking_number") or "").strip() if not tn or len(tn) < 10 or tn in seen_tracking: