Files
inquiry_robot/inquiry-agent/agent/ledger/http_ledger.py
T
jillion886andCursor 99da1faf30 落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 20:25:35 +08:00

185 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
主账 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"}