""" 主账 HTTP 账本:建单 / 查价 / 报价 / 六态只打 8180。 本文件职责:把 MemoryLedger 同名方法接到 /inquiry/agent/**;查价走真 TMS(主账出站)。 禁止:直连 TMS / MySQL;把 Jeecg 包装失败当成有价。 """ from __future__ import annotations import logging from typing import Any, Optional from agent.ledger import LedgerClient logger = logging.getLogger(__name__) def _unwrap(data: Any) -> dict[str, Any]: """解开 Jeecg Result:{success, result} → result。""" if not isinstance(data, dict): return {} if "result" in data and isinstance(data.get("result"), dict): inner = data["result"] if data.get("success") is False: return {"ok": False, "error": str(data.get("message") or "ledger_error"), **inner} return inner return data class HttpLedger: """ 生产主账客户端。 超时随 LedgerClient;查价在主账线程打 TMS,本类只转发分类。 """ def __init__(self, client: LedgerClient | None = None) -> None: self._client = client or LedgerClient.from_settings() def _post(self, path: str, body: dict[str, Any], *, idem: str = "") -> dict[str, Any]: resp = self._client.request_json("POST", path, json_body=body, idempotency_key=idem) if not resp.ok: logger.warning("http_ledger fail path=%s status=%s err=%s", path, resp.status_code, resp.error) return {"ok": False, "error": resp.error or f"http_{resp.status_code}"} return _unwrap(resp.data) def find_similar_open( self, *, facts: dict[str, str], business_line: str, exclude_work_order_no: str = "", ) -> dict[str, Any]: """全库相同需求检索;exclude_work_order_no 排除刚建的本单。""" out = self._post( "/inquiry/agent/ticket/findSimilar", { "salesWecomId": "", "businessLine": business_line, "facts": facts, "excludeWorkOrderNo": exclude_work_order_no, }, ) if "found" not in out: return {"found": False, "first_or_same": "first", "work_order_no": "", "status": ""} return out def create_ticket( self, *, sender_id: str, business_line: str, facts: dict[str, str], idempotency_key: str = "", ) -> dict[str, Any]: out = self._post( "/inquiry/agent/ticket/create", { "salesWecomId": sender_id, "businessLine": business_line, "facts": facts, "idempotencyKey": idempotency_key, }, idem=idempotency_key, ) if not out.get("ok") and not out.get("work_order_no"): return {"ok": False, "created": False, "work_order_no": "", "status": "", "first_or_same": "first"} out.setdefault("ok", True) if out.get("similar_work_order_no"): out.setdefault("first_or_same", "same") else: out.setdefault("first_or_same", "first") return out def get_for_agent(self, *, work_order_no: str, sender_id: str) -> dict[str, Any]: """读主账工单快照,供续办回到当时节点。""" out = self._post( "/inquiry/agent/ticket/get", {"work_order_no": work_order_no, "salesWecomId": sender_id}, ) if not out.get("found") and not out.get("ok"): return {"ok": False, "found": False, "owned": False, "work_order_no": work_order_no} return out def query_tms(self, *, work_order_no: str, facts: dict[str, str]) -> dict[str, Any]: out = self._post( "/inquiry/agent/tms/query", {"work_order_no": work_order_no, "facts": facts}, idem=f"tms:{work_order_no}", ) if "classification" not in out and not out.get("ok"): return { "ok": False, "classification": "TMS_TECH_FAILURE", "has_price": False, "quote": {}, } out.setdefault("quote", {}) return out def upsert_quote( self, *, work_order_no: str, quote: dict[str, Any], to_status: str = "", ) -> dict[str, Any]: # 有价时主账 query 已写快照并迁「已报价」;此处只做幂等确认,避免二次假写入。 _ = (work_order_no, quote, to_status) return {"ok": True, "quote_version": 1, "status": to_status or "已报价"} def render_quote( self, *, work_order_no: str, business_line: str, facts: dict[str, Any], quote: Optional[dict[str, Any]] = None, format: str = "xlsx", sales_wecom_id: str = "", ) -> dict[str, Any]: """ 打主账按后台报价模板填 Excel。 失败(无关键词且无默认)原样带回销售话术。Java 不转 PDF。 """ from agent.quote_templates.match import FAIL_MESSAGE # 填表+回文件可能超过默认 10s,单独放宽,仍禁止无限等。 prev_timeout = self._client.timeout_seconds self._client.timeout_seconds = max(float(prev_timeout or 10), 45.0) self._client.close() try: out = self._post( "/inquiry/agent/quote/render", { "workOrderNo": work_order_no, "businessLine": business_line, "facts": facts, "quote": quote or {}, "salesWecomId": sales_wecom_id, }, idem=f"quote-render:{work_order_no}:{format}", ) finally: self._client.timeout_seconds = prev_timeout self._client.close() if not out.get("ok"): return { "ok": False, "error": out.get("error") or "NO_TEMPLATE", "message": out.get("message") or FAIL_MESSAGE, } out.setdefault("needsPdf", str(format).lower() == "pdf") return out def transition(self, *, work_order_no: str, to_status: str) -> dict[str, Any]: out = self._post( "/inquiry/agent/ticket/transition", {"workOrderNo": work_order_no, "toStatus": to_status}, ) if not out.get("ok") and out.get("status"): out["ok"] = True return out if out.get("ok") else {"ok": False, "error": out.get("error") or "transition_failed"}