diff --git a/core/websocket_quote_flow.py b/core/websocket_quote_flow.py new file mode 100644 index 0000000..e510035 --- /dev/null +++ b/core/websocket_quote_flow.py @@ -0,0 +1,128 @@ +import asyncio +import logging + +logger = logging.getLogger("cs_agent") + + +async def handle_single_image_quote(client, data: dict, url: str): + try: + from image.image_analyzer import image_analyzer + + result = await image_analyzer.analyze(url) + if isinstance(result, dict) and result.get("success", False): + if result.get("feasibility") == "no" or result.get("risk") == "high": + note = str(result.get("note", "") or "") + if "文字内容过于密集" in note or "密集文字" in note: + reply = "这类文字太密的图我们这边不接单,抱歉哈。你要是简化后再发我可以继续看。" + else: + reply = "这张处理风险比较高,我这边先不直接接,建议转人工评估更稳。" + await client.send_reply(data, reply) + return + + from config.config import MIN_PRICE_FLOOR + price = result.get("price_suggest", 20) + floor_dyn = result.get("price_min", MIN_PRICE_FLOOR) + floor = max(MIN_PRICE_FLOOR, int(floor_dyn) if isinstance(floor_dyn, (int, float)) else MIN_PRICE_FLOOR) + price = max(floor, round(price / 5) * 5) + try: + from db.customer_db import db as _db + _db.update_last_min_price(data.get('from_id', ''), floor) + except Exception: + logger.debug("更新单图最低价失败", exc_info=True) + reply = f"这张按{price}元,满意再拍" + else: + # 识别失败时不做兜底报价,避免把未识别图片误判为可做 + reply = "这张我这边暂时识别不稳定,先不乱报价。你可以换一张更清晰的,我再给你准报价。" + await client.send_reply(data, reply) + except Exception: + logger.exception("单图分析流程失败") + + +async def handle_multi_image_quote(client, data: dict, urls: list): + try: + from image.image_analyzer import image_analyzer + + def _detect_composite_request() -> bool: + try: + from db.chat_log_db import get_recent_conversation + recent = get_recent_conversation( + customer_id=data.get('from_id', ''), + acc_id=data.get('acc_id', ''), + limit=8, + ) + keywords = ("抓到", "放到", "合成", "融合", "嵌到", "换到", "替换", "P到", "抠出来放到") + for item in recent: + msg = (item.get("message") or "") + if any(k in msg for k in keywords): + return True + except Exception: + logger.debug("检测合成需求失败,按非合成处理", exc_info=True) + return False + + tasks = [image_analyzer.analyze(u) for u in urls] + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 先做风险分流:多图中只要出现不可做/高风险,不进入报价 + unsafe = [] + dense_text_reject = [] + for i, result in enumerate(results, 1): + if isinstance(result, dict) and result.get("success", False): + if result.get("feasibility") == "no" or result.get("risk") == "high": + unsafe.append(f"图{i}") + note = str(result.get("note", "") or "") + if "文字内容过于密集" in note or "密集文字" in note: + dense_text_reject.append(f"图{i}") + + if unsafe: + if dense_text_reject and len(dense_text_reject) == len(unsafe): + reply = "这类文字太密的图我们这边不接单,抱歉哈。你要是简化后再发我可以继续看。" + else: + reply = f"这批里{'、'.join(unsafe)}处理风险较高,我这边先不直接接,建议转人工评估更稳。" + await client.send_reply(data, reply) + return + + pairs = [] + for u, result in zip(urls, results): + if isinstance(result, dict) and result.get("success", False): + from config.config import MIN_PRICE_FLOOR + floor_dyn = result.get("price_min", MIN_PRICE_FLOOR) + floor = max(MIN_PRICE_FLOOR, int(floor_dyn) if isinstance(floor_dyn, (int, float)) else MIN_PRICE_FLOOR) + price = max(floor, round(result.get("price_suggest", 20) / 5) * 5) + pairs.append((u, price, result.get("category", ""), result.get("megapixels", 0.0))) + try: + if pairs: + floors = [] + for _u, result in zip(urls, results): + if isinstance(result, dict) and result.get("success", False): + from config.config import MIN_PRICE_FLOOR + floor_dyn = result.get("price_min", MIN_PRICE_FLOOR) + floor = max(MIN_PRICE_FLOOR, int(floor_dyn) if isinstance(floor_dyn, (int, float)) else MIN_PRICE_FLOOR) + floors.append(floor) + if floors: + from db.customer_db import db as _db + _db.update_last_min_price(data.get('from_id', ''), min(floors)) + except Exception: + logger.debug("更新多图最低价失败", exc_info=True) + + if not pairs: + await client.send_reply(data, "这组图我这边暂时识别不稳定,先不乱报价。你可以换清晰图再发我。") + return + + composite = _detect_composite_request() + composite_fee = 5 if composite else 0 + avg_raw = sum(p for _, p, _, _ in pairs) / len(pairs) + from config.config import MIN_PRICE_FLOOR + avg_price = max(MIN_PRICE_FLOOR, round((avg_raw + composite_fee) / 5) * 5) + top_price = max(MIN_PRICE_FLOOR, max(pairs, key=lambda x: x[1])[1] + composite_fee) + count = len(pairs) + if composite: + reply = f"这组{count}张我看了,按{avg_price}元一张;合成那张{top_price}元,满意再拍" + else: + reply = f"这组{count}张我看了,按{avg_price}元一张;复杂那张{top_price}元,满意再拍" + await client.send_reply(data, reply) + except Exception as e: + logger.error("多图分析失败: %s", e) + try: + await client.send_reply(data, "这组图我这边暂时识别异常,先不乱报价。你可以稍后再发我。") + except Exception: + logger.debug("多图分析失败后的兜底回复发送失败", exc_info=True)