机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-19 09:41:59 +08:00
co-authored by Cursor
parent 277cdc1c28
commit 2b528758b4
119 changed files with 6760 additions and 3516 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ class LedgerClient:
"""
主账客户端。
base_url:BIZ_API_BASE_URL(新系统多为 http://127.0.0.1:8180/jeecgboot)。
base_url:BIZ_API_BASE_URL(新系统为 http://127.0.0.1:8080/jeecgboot)。
超时:默认 10s,禁止无限阻塞 Worker 槽。
禁止:在本类拼 TMS HMAC 或写工单 SQL。
"""
+76 -5
View File
@@ -1,5 +1,5 @@
"""
主账 HTTP 账本:建单 / 查价 / 报价 / 六态只打 8180。
主账 HTTP 账本:建单 / 查价 / 报价 / 六态只打 8080。
本文件职责:把 MemoryLedger 同名方法接到 /inquiry/agent/**;查价走真 TMS(主账出站)。
禁止:直连 TMS / MySQL;把 Jeecg 包装失败当成有价。
@@ -236,13 +236,15 @@ class HttpLedger:
quote: Optional[dict[str, Any]] = None,
format: str = "xlsx",
sales_wecom_id: str = "",
persist_attachment: bool = False,
) -> dict[str, Any]:
"""
打主账按后台报价模板填 Excel。
失败(无关键词且无默认)原样带回销售话术。Java 不转 PDF。
persist_attachment:True 才写主账附件表(确认落库);激活发群默认 False。
"""
from agent.quote_templates.match import FAIL_MESSAGE
from agent.quote_templates.match import FAIL_MESSAGE, normalize_biz, pick_quote_template
from agent.schema.quote_adjust import stamp_prices_for_sheet
# 出单前把当前费用写入线路表、海运分段、空运舱位。否则报价单仍是旧价。
@@ -260,8 +262,9 @@ class HttpLedger:
"facts": facts,
"quote": quote or {},
"salesWecomId": sales_wecom_id,
"persistAttachment": bool(persist_attachment),
},
idem=f"quote-render:{work_order_no}:{format}",
idem=f"quote-render:{work_order_no}:{format}:{int(bool(persist_attachment))}",
)
finally:
self._client.timeout_seconds = prev_timeout
@@ -270,6 +273,7 @@ class HttpLedger:
return {
"ok": False,
"error": out.get("error") or "NO_TEMPLATE",
"failStage": out.get("failStage"),
"message": out.get("message") or FAIL_MESSAGE,
}
from agent.quote_templates.filename import filename_from_render
@@ -465,12 +469,16 @@ class HttpLedger:
flight_no: str = "",
) -> dict[str, Any]:
"""锁舱出站。航班号和航司一并带上,主账缺了这两项不能当锁舱失败。"""
from agent.policy.bot_name import strip_bot_mention
# 群入站常带「@智价通AIQ …」;出站备注只留业务原文,禁止把 @机器人 带给 TMS。
clean_instruction = strip_bot_mention(instruction_text or "")
body: dict[str, Any] = {
"workOrderNo": work_order_no,
"optionNo": option_no,
"operatorUserId": sender_id,
"operatorName": operator_name,
"instructionText": instruction_text,
"instructionText": clean_instruction,
}
if (airline or "").strip():
body["airline"] = airline.strip().upper()
@@ -491,6 +499,9 @@ class HttpLedger:
operator_name: str = "",
instruction_text: str = "",
) -> dict[str, Any]:
from agent.policy.bot_name import strip_bot_mention
clean_instruction = strip_bot_mention(instruction_text or "")
return self._post(
"/inquiry/agent/tms/releaseCabin",
{
@@ -498,7 +509,7 @@ class HttpLedger:
"lockId": lock_id,
"operatorUserId": sender_id,
"operatorName": operator_name,
"instructionText": instruction_text,
"instructionText": clean_instruction,
},
idem=f"air-release:{work_order_no}:{lock_id}",
)
@@ -510,6 +521,38 @@ class HttpLedger:
return None
return out
def list_open_collab(self) -> list[dict[str, Any]] | None:
"""
列出仍绑着协同群的工单(含已结束协同)。
Worker 对账监听用。
返回 None = 接口失败/形态不对(调用方禁止据此做孤儿清群);
返回 [] = 确实没有绑群工单。
"""
try:
out = self._post("/inquiry/agent/ticket/listOpenCollab", {})
except Exception:
logger.exception("listOpenCollab 调用失败")
return None
if not isinstance(out, dict):
logger.error("listOpenCollab 返回非对象")
return None
# 主账失败常见 success=false / code!=200,不能当成「零条活群」。
if out.get("success") is False:
logger.error("listOpenCollab 业务失败 code=%s", out.get("code") or out.get("status"))
return None
rows = out.get("tickets")
if rows is None and "result" in out:
result = out.get("result")
if isinstance(result, dict):
rows = result.get("tickets")
elif isinstance(result, list):
rows = result
if not isinstance(rows, list):
logger.error("listOpenCollab 无 tickets 列表字段 keys=%s", sorted(out.keys())[:20])
return None
return [r for r in rows if isinstance(r, dict)]
def end_collab_group(self, *, work_order_no: str) -> dict[str, Any]:
"""协同结束,这单不能再拉群。不改六态。"""
return self._post(
@@ -539,6 +582,34 @@ class HttpLedger:
},
)
def attach_file_bytes(
self,
*,
work_order_no: str,
file_name: str,
file_bytes: bytes,
content_type: str = "",
sender_id: str = "",
) -> dict[str, Any]:
"""
报价附件字节写入主账(上传对象存储 + inq_attachment)。
用于 PDF 转换完成后落库;Excel 出单由主账 render 内登记。
"""
import base64
return self._post(
"/inquiry/agent/ticket/attachFileBytes",
{
"workOrderNo": work_order_no,
"fileName": file_name,
"fileBase64": base64.b64encode(file_bytes or b"").decode("ascii"),
"contentType": content_type or "application/octet-stream",
"senderId": sender_id,
},
idem=f"attach-bytes:{work_order_no}:{file_name}:{len(file_bytes or b'')}",
)
def upsert_product_quote(
self,
*,
+25 -1
View File
@@ -1,5 +1,5 @@
"""
主账内存假账本(单测 / 无 8180 时)。
主账内存假账本(单测 / 无 8080 时)。
本文件职责:实现建单(每次新开)、相似检索、TMS fixture、报价写入、六态流转,语义对齐 Java 主账。
禁止:冒充已打真实 TMS;禁止被生产 Worker 当默认 Owner(生产必须 HTTP 主账)。
@@ -427,9 +427,11 @@ class MemoryLedger:
quote: Optional[dict[str, Any]] = None,
format: str = "xlsx",
sales_wecom_id: str = "",
persist_attachment: bool = False,
) -> dict[str, Any]:
"""
按询价需求匹配内存模板。不填真实 xlsx,只回假字节供流程/单测。
persist_attachment 与主账同语义:True 表示应登记附件(单测可断言 attachmentSaved)。
"""
from agent.quote_templates.match import FAIL_MESSAGE, pick_quote_template
@@ -459,6 +461,8 @@ class MemoryLedger:
"fileBase64": "UEsDBAoAAAAAA",
"needsPdf": False,
"facts": dict(facts or {}),
"attachmentSaved": bool(persist_attachment),
"persistAttachmentRequested": bool(persist_attachment),
}
with self._lock:
ticket = self._tickets.get((work_order_no or "").strip())
@@ -745,6 +749,26 @@ class MemoryLedger:
ticket.attachments.append(row)
return {"ok": True, "file_name": file_name}
def attach_file_bytes(
self,
*,
work_order_no: str,
file_name: str,
file_bytes: bytes,
content_type: str = "",
sender_id: str = "",
) -> dict[str, Any]:
"""单测:字节附件只记元数据,不写真实对象存储。"""
_ = file_bytes
key = f"memory/{work_order_no}/{file_name or 'file.bin'}"
return self.attach_file(
work_order_no=work_order_no,
file_name=file_name,
object_key=key,
content_type=content_type,
sender_id=sender_id,
)
def upsert_product_quote(
self,
*,