diff --git a/qingjian_cs/app/auto_draw.py b/qingjian_cs/app/auto_draw.py new file mode 100644 index 0000000..8199481 --- /dev/null +++ b/qingjian_cs/app/auto_draw.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import asyncio +import os +import sys +from pathlib import Path +from typing import Any + +import requests + +from .config import AUTO_DRAW_ENDPOINT, AUTO_DRAW_TIMEOUT_SECONDS + + +def _add_legacy_tw_path() -> None: + root = os.getenv("LEGACY_TW_ROOT", r"D:\main\sandbox\tw_terminator").strip() + if not root: + return + p = Path(root) + if p.exists() and str(p) not in sys.path: + sys.path.insert(0, str(p)) + + +async def _draw_via_legacy_tw( + image_url: str, + customer_id: str, + requirement: str = "", +) -> dict[str, Any]: + _add_legacy_tw_path() + from image.image_processor import image_processor # type: ignore + from services.service_tuhui_upload import upload_to_tuhui # type: ignore + + prompt = requirement.strip() or "按原图做高清处理,保留主体细节,输出清晰可用版本" + process_res = await image_processor.process_image( + image_url=image_url, + operation="enhance", + requirements="complexity:normal", + gemini_prompt=prompt, + aspect_ratio="1:1", + perspective="no", + proc_type="", + subject="", + quality="", + ) + if not process_res.get("success"): + return {"ok": False, "error": str(process_res.get("message", "process_failed"))} + + ok, link, _ = await upload_to_tuhui( + process_res["result_path"], + title=f"客户{customer_id[-4:]}-预览图" if customer_id else "预览图", + description="AI自动作图预览", + price=1, + ) + if not ok: + return {"ok": False, "error": str(link)} + return {"ok": True, "url": str(link)} + + +def _draw_via_http_endpoint(image_url: str, customer_id: str, requirement: str = "") -> dict[str, Any]: + if not AUTO_DRAW_ENDPOINT: + return {"ok": False, "error": "AUTO_DRAW_ENDPOINT not configured"} + payload = { + "image_url": image_url, + "customer_id": customer_id, + "requirement": requirement, + } + resp = requests.post(AUTO_DRAW_ENDPOINT, json=payload, timeout=AUTO_DRAW_TIMEOUT_SECONDS) + if resp.status_code != 200: + return {"ok": False, "error": f"http_{resp.status_code}:{resp.text[:200]}"} + data = resp.json() if resp.text else {} + url = str(data.get("url", "") or data.get("preview_url", "") or "") + if not url: + return {"ok": False, "error": "missing_preview_url"} + return {"ok": True, "url": url} + + +async def auto_draw_preview( + image_url: str, + customer_id: str, + requirement: str = "", +) -> dict[str, Any]: + """ + 统一自动作图入口: + 1) 优先走 tw_terminator 的 Gemini 作图链路 + 2) 失败时回退 AUTO_DRAW_ENDPOINT + """ + try: + return await _draw_via_legacy_tw(image_url=image_url, customer_id=customer_id, requirement=requirement) + except Exception as e: + legacy_error = str(e) + + try: + data = await asyncio.to_thread( + _draw_via_http_endpoint, + image_url, + customer_id, + requirement, + ) + if data.get("ok"): + return data + return {"ok": False, "error": f"legacy:{legacy_error}; endpoint:{data.get('error','unknown')}"} + except Exception as e: + return {"ok": False, "error": f"legacy:{legacy_error}; endpoint:{e}"} + diff --git a/qingjian_cs/app/client.py b/qingjian_cs/app/client.py index 9a74066..180457c 100644 --- a/qingjian_cs/app/client.py +++ b/qingjian_cs/app/client.py @@ -7,7 +7,9 @@ from collections import defaultdict import websockets from .callbacks import post_tianwang_callback +from .auto_draw import auto_draw_preview from .config import ( + AUTO_DRAW_ENABLED, AUTO_QUOTE_WAIT_SECONDS, MESSAGE_DEBOUNCE_SECONDS, QINGJIAN_WS_URI, @@ -77,6 +79,28 @@ class QingjianClient: self.recent_outbound = self.recent_outbound[-200:] activity_event(self.logger, "send_reply_success", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=text) + async def send_image(self, data: dict, image_url: str, trace_id: str = "-") -> None: + image_url = str(image_url or "").strip() + if not image_url: + return + msg = { + "msg_id": "", + "acc_id": data.get("acc_id", ""), + "msg": image_url, + "from_id": data.get("from_id", ""), + "from_name": data.get("from_name", data.get("from_id", "")), + "cy_id": data.get("from_id", ""), + "acc_type": data.get("acc_type", "AliWorkbench"), + "msg_type": 1, + "cy_name": data.get("from_name", data.get("from_id", "")), + } + activity_event(self.logger, "send_image_attempt", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=image_url) + await self.send_message(msg) + self.recent_outbound.append((str(data.get("acc_id", "")), str(data.get("from_id", "")), image_url, time.monotonic())) + if len(self.recent_outbound) > 200: + self.recent_outbound = self.recent_outbound[-200:] + activity_event(self.logger, "send_image_success", trace_id=trace_id, customer_id=data.get("from_id", "-"), msg=image_url) + @staticmethod def _clean_text(text: str) -> str: t = str(text or "").strip() @@ -185,6 +209,49 @@ class QingjianClient: return if decision.action == "quote": + if AUTO_DRAW_ENABLED and self.pending_images.get(key): + latest_image = self.pending_images[key][-1] + activity_event( + self.logger, + "auto_draw_start", + trace_id=trace_id, + customer_id=context["customer_id"], + image_url=latest_image, + ) + draw_res = await auto_draw_preview( + image_url=latest_image, + customer_id=context["customer_id"], + requirement=merged_msg, + ) + if draw_res.get("ok"): + preview_url = str(draw_res.get("url", "") or "") + await self.send_reply(data, "先给你做了预览图。", trace_id=trace_id) + await self.send_image(data, preview_url, trace_id=trace_id) + final_text = "看下预览,满意再拍下付款。" + await self.send_reply(data, final_text, trace_id=trace_id) + self.last_reply_key[key] = final_text + # 预览完成后清掉当前批次,避免同一图重复触发 + self.pending_images[key].clear() + activity_event( + self.logger, + "auto_draw_success", + trace_id=trace_id, + customer_id=context["customer_id"], + preview_url=preview_url, + ) + await post_tianwang_callback( + "message_processed", + data, + extra={"trace_id": trace_id, "route": route, "action": "quote", "reply": final_text, "auto_draw": True}, + ) + return + activity_event( + self.logger, + "auto_draw_fail", + trace_id=trace_id, + customer_id=context["customer_id"], + error=str(draw_res.get("error", "unknown")), + ) text = (decision.reply or "").strip() if self._is_invalid_ai_reply(text): text = self._fallback_reply("quote") diff --git a/qingjian_cs/app/config.py b/qingjian_cs/app/config.py index e0f4fcf..fbfd4c3 100644 --- a/qingjian_cs/app/config.py +++ b/qingjian_cs/app/config.py @@ -31,3 +31,7 @@ MYSQL_TABLE_PREFIX = os.getenv("MYSQL_TABLE_PREFIX", "qjcs_").strip() HTTP_HOST = os.getenv("HTTP_HOST", "127.0.0.1").strip() HTTP_PORT = int(os.getenv("HTTP_PORT", "6060")) TIANWANG_CALLBACK_URL = os.getenv("TIANWANG_CALLBACK_URL", "http://139.199.3.75:18789/api/callback").strip() + +AUTO_DRAW_ENABLED = os.getenv("AUTO_DRAW_ENABLED", "1").strip() in {"1", "true", "True", "yes", "on"} +AUTO_DRAW_ENDPOINT = os.getenv("AUTO_DRAW_ENDPOINT", "").strip() +AUTO_DRAW_TIMEOUT_SECONDS = int(os.getenv("AUTO_DRAW_TIMEOUT_SECONDS", "25"))