让成交卡等报价文件真正发出后再出现,并收紧 PDF 顶部留白,避免抬头被裁或空一截。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-15 10:36:55 +08:00
co-authored by Cursor
parent cb3bf5238b
commit a7a8487eb3
22 changed files with 1630 additions and 126 deletions
+177 -45
View File
@@ -47,6 +47,12 @@ BTN_REJECT_KEY = "reject"
BTN_DEAL = "成交"
BTN_LOST = "未成交"
BTN_NEGOTIATE = "协商中"
BTN_DEAL_KEY = "deal"
BTN_LOST_KEY = "lost"
BTN_NEGOTIATE_KEY = "negotiate"
# 旧卡 EventKey 兼容;新暂无报价卡按现网图:无按钮。
BTN_COPY_TICKET = "复制工单号"
BTN_COPY_TICKET_KEY = "copy_ticket"
_WO_NO = re.compile(r"WO\d{12}", re.IGNORECASE)
# 仅给无网 stub / 工单号回填用,正式续办意图由 DeepSeek A 判断,不要求销售说这些词。
@@ -245,6 +251,18 @@ def canonical_button(raw: str) -> str:
return BTN_ADOPT_PDF
if text in {BTN_REJECT_KEY} or text.startswith(f"{BTN_REJECT_KEY}:"):
return BTN_REJECT
if text in {BTN_COPY_TICKET, BTN_COPY_TICKET_KEY} or text.startswith(
f"{BTN_COPY_TICKET_KEY}:"
):
return BTN_COPY_TICKET
if text in {BTN_DEAL, BTN_DEAL_KEY} or text.startswith(f"{BTN_DEAL_KEY}:"):
return BTN_DEAL
if text in {BTN_LOST, BTN_LOST_KEY} or text.startswith(f"{BTN_LOST_KEY}:"):
return BTN_LOST
if text in {BTN_NEGOTIATE, BTN_NEGOTIATE_KEY} or text.startswith(
f"{BTN_NEGOTIATE_KEY}:"
):
return BTN_NEGOTIATE
if text == BTN_SKIP_COLLAB_DONE:
return ""
for btn in (BTN_ADOPT_EXCEL, BTN_ADOPT_PDF, BTN_REJECT, BTN_DEAL, BTN_LOST, BTN_NEGOTIATE):
@@ -313,6 +331,10 @@ def _work_order_in_token(raw: str) -> str:
f"{BTN_ADOPT_EXCEL_KEY}:",
f"{BTN_ADOPT_PDF_KEY}:",
f"{BTN_REJECT_KEY}:",
f"{BTN_COPY_TICKET_KEY}:",
f"{BTN_DEAL_KEY}:",
f"{BTN_LOST_KEY}:",
f"{BTN_NEGOTIATE_KEY}:",
):
if text.startswith(prefix):
rest = text[len(prefix) :].strip()
@@ -439,7 +461,7 @@ def tms_valid_until_display(quote: dict[str, Any] | None) -> str:
def similar_inquiry_line(*, first_or_same: str, history_work_order_no: str = "") -> str:
"""
相同询价内容检索:首次询价,或引用全库最新一条一模一样的历史工单号。
相同询价内容检索文案(主账仍会算,销售侧价格卡已隐藏,不再调用)。
"""
no = (history_work_order_no or "").strip()
if first_or_same == "same" and no:
@@ -455,11 +477,12 @@ def quote_detail_lines(
status: str = "已报价",
) -> list[str]:
"""
价格明细正文:TMS 费用、合计、时效、有效期、相同检索。
价格明细正文:TMS 费用、合计、时效、有效期。
给文本兜底和企微引用条共用,禁止在这里编时效/有效期。价格卡不再展示工单状态。
给文本兜底和企微引用条共用,禁止在这里编时效/有效期。
价格卡不展示工单状态,也不展示相同询价内容检索。
"""
_ = status
_ = status, first_or_same, history_work_order_no
src = dict(quote or {})
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
@@ -467,11 +490,6 @@ def quote_detail_lines(
lines.append(f"总销售价合计:{total}")
lines.append(f"时效:{tms_eta_display(src)}")
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
lines.append(
similar_inquiry_line(
first_or_same=first_or_same, history_work_order_no=history_work_order_no
)
)
return lines
@@ -518,22 +536,21 @@ def tms_hit_wecom_payload(
价格明细
┌ 引用条:空运费 / 其它费用 / 总销售价合计
时效、报价有效期、相同询价内容检索
时效、报价有效期
协同说明(普通文字,贴按钮上方)
按钮
相同询价内容检索对销售隐藏;first_or_same 只兼容调用方,不展示。
企微 quote_area.title 只能很短,超长会被整段藏掉。报价必须放 quote_text。
不用 horizontal:它会排到副标题后面,把费用挤到按钮前。
"""
_ = first_or_same, history_work_order_no
src = dict(quote or {})
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
meta = "\n".join(
[
f"时效:{tms_eta_display(src)}",
f"报价有效期:{tms_valid_until_display(src)}",
similar_inquiry_line(
first_or_same=first_or_same, history_work_order_no=history_work_order_no
),
"",
collab_hint(work_order_no),
]
@@ -562,58 +579,86 @@ def tms_hit_wecom_payload(
}
def skip_collab_card_update(*, response_code: str) -> dict[str, Any]:
def skip_collab_card_update(*, response_code: str, replace_name: str = "") -> dict[str, Any]:
"""
点过「生成报价单」后更新原卡:按钮改成已提交并保持置灰。
点过模板卡后更新原卡:被点按钮改成已提交,整卡置灰不可再点。
必须带企微回调 ResponseCode;无码则不要入出站。
成交卡三个按钮共用这一条,replace_name 默认「已提交」。
"""
return {
"msgtype": "update_template_card",
"response_code": response_code,
"button": {"replace_name": BTN_SKIP_COLLAB_DONE},
"button": {"replace_name": (replace_name or BTN_SKIP_COLLAB_DONE).strip() or BTN_SKIP_COLLAB_DONE},
}
def tms_miss_quote_text(*, work_order_no: str) -> str:
"""引用条正文:只放「请务必在群聊发送工单编号」。"""
return f"请务必在群聊发送工单编号{work_order_no}"
"""文字兜底里的拉群句,与现网图一致。"""
return f"请手动拉相关人员和智能体到原有群,并在群聊发送工单编号{work_order_no}"
def tms_miss_hint() -> str:
"""卡片正文:无价说明 + 拉群提示,不单独再写标题下灰字。"""
return "TMS标准价库暂无匹配报价,请手动拉新人与智能体到原有群"
"""卡片标题下灰字:与现网图一致。"""
return "TMS 标准价库暂无匹配报价"
def tms_miss_body() -> str:
"""卡片中间说明:需求已建单。"""
return "需求已解析并创建工单。"
def tms_miss(*, work_order_no: str) -> str:
"""
TMS 已经查过、确认没标准价时的文字兜底。
卡片发送失败时的短兜底。拉群句另发,不写在这里。
禁止在没打 TMS 前用本句冒充无价。
"""
return (
f"TMS暂无报价。{tms_miss_hint()},"
f"{tms_miss_quote_text(work_order_no=work_order_no)}"
)
no = (work_order_no or "").strip()
return f"询价工单{no}已建立,TMS标准价库暂无匹配报价。"
def tms_miss_wecom_payload(*, work_order_no: str, action_url: str = "") -> dict[str, Any]:
def tms_miss_followup(*, work_order_no: str) -> str:
"""出卡后再发的独立一句:拉人进群并带上工单号。"""
return tms_miss_quote_text(work_order_no=work_order_no)
def copy_ticket_text(*, work_order_no: str) -> str:
"""
TMS 已查过、确认没价:一张 text_notice 卡。
点「复制工单号」后回给销售的正文:只含工单号。
顺序:来源 → 标题(仅工单号+暂无报价)→ 引用条(工单编号)→ 拉群说明。
禁止:标题下灰字、工单状态、查价结果/来源行、按钮。
企微 text_notice 必须带可信域名 card_action,否则整卡发失败只剩文字。
企微模板卡按钮不能写系统剪贴板。整条消息只有工单号,销售长按「复制」不会夹杂其它字。
"""
return (work_order_no or "").strip()
def tms_miss_wecom_payload(
*,
work_order_no: str,
action_url: str = "",
copy_url: str = "",
) -> dict[str, Any]:
"""
TMS 已查过、确认没价:一张文本通知卡(对齐现网图)。
询价工单号 / 灰字无价说明 / 建单说明 / 工单状态与查价结果。
不要「来源」行、不要按钮。企微 text_notice 必须带可信域名 card_action。
copy_url 忽略:不再跳复制页。
"""
_ = copy_url
no = (work_order_no or "").strip()
card: dict[str, Any] = {
"card_type": "text_notice",
"source": {"desc": "TMS查价结果", "desc_color": 0},
"main_title": {"title": f"{work_order_no}暂无报价"},
"quote_area": {
"type": 0,
"quote_text": tms_miss_quote_text(work_order_no=work_order_no),
"main_title": {
"title": f"询价工单 {no}",
"desc": tms_miss_hint(),
},
"sub_title_text": tms_miss_hint(),
"task_id": card_task_id("miss", work_order_no),
"sub_title_text": tms_miss_body(),
"horizontal_content_list": [
{"keyname": "工单状态", "value": "询价中"},
{"keyname": "查价结果", "value": "暂无匹配报价"},
],
"task_id": card_task_id("miss", no),
}
url = (action_url or "").strip()
if url:
@@ -778,15 +823,102 @@ def file_ready(*, kind: str, work_order_no: str, filename: str) -> str:
return f"已生成{kind}最终报价单:{filename}(工单 {work_order_no})"
def deal_card(*, work_order_no: str, status: str, summary: str) -> str:
return (
"【咨询成交状态】\n"
f"工单编号:{work_order_no}\n"
f"工单状态:{status}\n"
f"询价内容:{summary}\n"
"请立即确认当前与客户沟通的成交状态:\n"
f"【{BTN_DEAL}】【{BTN_LOST}】【{BTN_NEGOTIATE}】"
)
DEAL_HINT = "咨询成交状态:请选择成交 / 未成交 / 协商中(未成交需补充原因)。"
DEAL_SUBTITLE = "报价单已发送,请跟进成交状态"
def deal_horizontal_rows(
*,
quote: dict[str, Any] | None,
facts: dict[str, str] | None = None,
) -> list[dict[str, str]]:
"""
成交跟进卡字段:总销售价合计、时效、报价有效期。
不要历史报价 / 工单状态 / 来源。来源只留卡片顶部红字。
"""
src = dict(quote or {})
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
return [
{"keyname": "总销售价合计", "value": _clip(total, 26)},
{"keyname": "时效", "value": _clip(tms_eta_display(src), 26)},
{"keyname": "报价有效期", "value": _clip(tms_valid_until_display(src), 26)},
]
def deal_card(
*,
work_order_no: str,
status: str = "",
summary: str = "",
quote: dict[str, Any] | None = None,
facts: dict[str, str] | None = None,
) -> str:
"""
成交跟进文字兜底(模板卡发失败时仍给销售看)。
与卡片同一套字段;status / summary 只兼容旧调用,不再展示。
"""
_ = status, summary
no = (work_order_no or "").strip()
rows = deal_horizontal_rows(quote=quote, facts=facts)
lines = [
f"询价工单 {no}",
DEAL_SUBTITLE,
DEAL_HINT,
]
lines.extend(f"{row['keyname']}:{row['value']}" for row in rows)
lines.append(f"【{BTN_DEAL}】【{BTN_LOST}】【{BTN_NEGOTIATE}】")
return "\n".join(lines)
def deal_wecom_payload(
*,
work_order_no: str,
quote: dict[str, Any] | None = None,
facts: dict[str, str] | None = None,
) -> dict[str, Any]:
"""
成交跟进卡:对齐现网 button_interaction。
顶部来源红字;标题询价工单号;副标题跟进成交;三个按钮。
字段只要总销售价、时效、有效期。
"""
src = dict(quote or {})
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
no = (work_order_no or "").strip()
return {
"msgtype": "template_card",
"template_card": {
"card_type": "button_interaction",
"source": {"desc": source, "desc_color": 0},
"main_title": {
"title": f"询价工单 {no}",
"desc": DEAL_SUBTITLE,
},
"sub_title_text": DEAL_HINT,
"horizontal_content_list": deal_horizontal_rows(quote=src, facts=facts),
"task_id": card_task_id("deal", no),
"button_list": [
{
"text": BTN_DEAL,
"style": 1,
"key": f"{BTN_DEAL_KEY}:{no}",
},
{
"text": BTN_LOST,
"style": 1,
"key": f"{BTN_LOST_KEY}:{no}",
},
{
"text": BTN_NEGOTIATE,
"style": 1,
"key": f"{BTN_NEGOTIATE_KEY}:{no}",
},
],
},
}
def deal_ack(outcome: str, work_order_no: str) -> str: