""" 空运文字询价出站话术(销售可见)。 本文件职责:集中文案;按钮字面量必须与 handler 精确匹配。 禁止:在本文件调主账/TMS;禁止把密钥写进话术。 """ from __future__ import annotations import json import re import time import uuid from typing import Any, Iterable from agent.schema.field_validate import AIR_REQUIRED_FIELDS # 空运补问:与合同 8 项一致。报价日期默认当天,不进待补充。 _AIR_EXAMPLES = { "起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶", "件数": "10件", "毛重": "100KGS", "体积": "2CBM", "包装方式": "托盘", "报价日期": "2026-09-15", } _AIR_CLARIFY = tuple( (key, label, _AIR_EXAMPLES[key]) for key, label in AIR_REQUIRED_FIELDS ) # 文字按钮(销售点卡或原样回复均可) BTN_SKIP_COLLAB = "不需要协同,直接生成报价单" BTN_SKIP_COLLAB_OLD = "不需要协同,直接生成基础报价单" BTN_SKIP_COLLAB_KEY = "skip_collab" BTN_SKIP_COLLAB_DONE = "已提交" BTN_ADOPT_EXCEL = "采用,生成Excel报价单" BTN_ADOPT_PDF = "采用,生成PDF报价单" BTN_REJECT = "不采用" # 企微一张卡三个按钮只能横排,长文案会被裁。卡片用短文案才能看全。 BTN_ADOPT_EXCEL_CARD = "生成Excel" BTN_ADOPT_PDF_CARD = "生成PDF" BTN_ADOPT_EXCEL_KEY = "adopt_excel" BTN_ADOPT_PDF_KEY = "adopt_pdf" BTN_REJECT_KEY = "reject" BTN_DEAL = "成交" BTN_LOST = "未成交" BTN_NEGOTIATE = "协商中" _WO_NO = re.compile(r"WO\d{12}", re.IGNORECASE) # 仅给无网 stub / 工单号回填用,正式续办意图由 DeepSeek A 判断,不要求销售说这些词。 _CONTINUE_HINT = ( "继续", "接着", "回到", "再看", "再出", "找回", "重开", "那单", "这单", "历史工单", "咨询工单", "刚才那", "上次那", ) ASK_TRANSPORT = "请补充运输方式:海运 / 空运 / 陆运。" ASK_LAND_SUBTYPE = "请补充陆运类型:国内运输拼车 / 国内运输整车 / 跨境集拼 / 跨境整车 / 中港。" FIRST_CHAT = "你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。" ROUTE_MODEL_FAILED = "刚才没听清,请再说一次:是新询价,还是要接着办哪张工单?" def _air_known_value(facts: dict[str, str], key: str) -> str: """已识别字段的销售侧展示值;没有则空串。""" src = facts or {} if key == "起运港": return (src.get("起运港") or "").strip() if key == "目的港": return (src.get("目的港") or "").strip() if key == "品名": return (src.get("品名") or "").strip() if key == "件数": raw = (src.get("件数") or "").strip() return _keep_or_add_unit(raw, ("件", "PCS"), "件") if raw else "" if key == "毛重": raw = (src.get("毛重") or "").strip() return _keep_or_add_unit(raw, ("KGS", "KG", "公斤", "千克"), "") if raw else "" if key == "体积": raw = (src.get("体积") or "").strip() return _keep_or_add_unit(raw, ("CBM", "M3", "M³", "立方米", "立方", "方"), "") if raw else "" if key == "包装方式": return (src.get("包装方式") or src.get("包装类型") or "").strip() if key == "报价日期": return (src.get("报价日期") or "").strip() return (src.get(key) or "").strip() def ask_clarify( *, facts: dict[str, str] | None = None, missing_keys: Iterable[str] = (), transport_mode: str = "空运", ) -> str: """ 缺项补问:先回显已识别,再列待补充并给示例。 报价日期写进已识别(销售说了用原话,没说用当天),不出现在待补充。 """ src = dict(facts or {}) skip = {"报价日期"} missing = [k for k in missing_keys if k and k not in skip] missing_set = set(missing) lines = ["请确认并补充以下信息:"] if transport_mode: lines.append(f"运输方式:{transport_mode}") for key, label, _example in _AIR_CLARIFY: val = _air_known_value(src, key) if val: lines.append(f"{label}:{val}") lines.append("") lines.append("待补充:") any_missing = False if "运输方式" in missing_set: any_missing = True lines.append("运输方式:(如:空运)") for key, label, example in _AIR_CLARIFY: if key in missing_set: any_missing = True lines.append(f"{label}:(如:{example})") if not any_missing: return "" return "\n".join(lines) def ask_missing(display_names: Iterable[str]) -> str: """兼容旧调用:仅有展示名时仍走补问,但没有已识别回显。""" alias = { "起运地": "起运港", "目的地": "目的港", "起运地(机场名或三字码)": "起运港", "目的地(机场名或三字码)": "目的港", "毛重": "毛重", "重量": "毛重", "重量(KG)": "毛重", "体积": "体积", "体积(CBM)": "体积", "包装类型": "包装方式", "包装方式": "包装方式", } keys: list[str] = [] for name in display_names: key = alias.get(name, name) if key and key not in keys and key != "报价日期": keys.append(key) return ask_clarify(facts={}, missing_keys=keys, transport_mode="空运") def _keep_or_add_unit(raw: str, known: tuple[str, ...], default: str) -> str: """ 销售侧单位原样回显。 写了 1.5KG 就回 1.5KG,禁止改成 1.5KGS。没写单位才补 default(可空)。 """ text = (raw or "").strip() if not text: return "-" upper = text.upper() for unit in known: if upper.endswith(unit.upper()): return text if default and not any(ch.isalpha() or ch in "立方件" for ch in text): return f"{text}{default}" return text def _with_unit(raw: str, suffix: str) -> str: """兼容旧调用:已带单位则原样。重量不要走这里补 KGS。""" if suffix.upper() == "KGS": return _keep_or_add_unit(raw, ("KGS", "KG", "公斤", "千克"), "") if suffix.upper() == "CBM": return _keep_or_add_unit(raw, ("CBM", "M3", "M³", "立方米", "立方", "方"), "") return _keep_or_add_unit(raw, (suffix,), suffix) def inquiry_field_lines( *, work_order_no: str, facts: dict[str, str], transport_mode: str = "空运", ) -> list[str]: """ 询价确认字段行(销售可见)。 起运/目的展示销售原文(南京/北京),三字码只给出站 TMS,不对销售展示。 """ origin = (facts.get("起运港") or "-").strip() dest = (facts.get("目的港") or "-").strip() cargo = (facts.get("品名") or "-").strip() or "-" pack = (facts.get("包装方式") or facts.get("包装类型") or "-").strip() or "-" quote_date = (facts.get("报价日期") or "-").strip() or "-" return [ f"工单号:{work_order_no}", f"运输方式:{transport_mode}", f"起运地:{origin}", f"目的地:{dest}", f"品名:{cargo}", f"件数:{_with_unit(str(facts.get('件数') or ''), '件')}", f"重量:{_with_unit(str(facts.get('毛重') or ''), 'KGS')}", f"体积:{_with_unit(str(facts.get('体积') or ''), 'CBM')}", f"包装类型:{pack}", f"报价日期:{quote_date}", ] def inquiry_card(*, work_order_no: str, facts: dict[str, str], first_or_same: str = "first") -> str: """ 建单后的询价确认。 无标题、无 [test]。first_or_same 保留兼容,不再对销售展示。 """ _ = first_or_same lines = inquiry_field_lines(work_order_no=work_order_no, facts=facts) lines.append("工单已创建,正在查询TMS报价。") return "\n".join(lines) def canonical_button(raw: str) -> str: """把销售点卡 / 原文 / 企微 EventKey 收成标准按钮文案;对不上返回空。""" text = (raw or "").strip().strip("【】") if text in {BTN_SKIP_COLLAB, BTN_SKIP_COLLAB_OLD, BTN_SKIP_COLLAB_KEY}: return BTN_SKIP_COLLAB # 新卡 EventKey 带工单号:skip_collab:WO…,避免点旧卡落到最新单 if text.startswith(f"{BTN_SKIP_COLLAB_KEY}:"): return BTN_SKIP_COLLAB if text in {BTN_ADOPT_EXCEL, BTN_ADOPT_EXCEL_CARD, BTN_ADOPT_EXCEL_KEY} or text.startswith( f"{BTN_ADOPT_EXCEL_KEY}:" ): return BTN_ADOPT_EXCEL if text in {BTN_ADOPT_PDF, BTN_ADOPT_PDF_CARD, BTN_ADOPT_PDF_KEY} or text.startswith( f"{BTN_ADOPT_PDF_KEY}:" ): return BTN_ADOPT_PDF if text in {BTN_REJECT_KEY} or text.startswith(f"{BTN_REJECT_KEY}:"): return BTN_REJECT if text == BTN_SKIP_COLLAB_DONE: return "" for btn in (BTN_ADOPT_EXCEL, BTN_ADOPT_PDF, BTN_REJECT, BTN_DEAL, BTN_LOST, BTN_NEGOTIATE): if text == btn: return btn return "" def extract_work_order_no(text: str) -> str: """从任意原话里抽出 WO + 12 位数字;没有则空。不判断意图。""" hit = _WO_NO.search(text or "") return hit.group(0).upper() if hit else "" def card_task_id(kind: str, work_order_no: str) -> str: """ 每次出卡一个新 task_id。企微 42014:同一 task_id 不能再发,会整卡失败只剩文字。 工单号仍嵌在前缀里,点卡回调靠 work_order_from_card_meta 找回。 """ stamp = f"{int(time.time() * 1000):x}{uuid.uuid4().hex[:4]}" return f"{kind}-{work_order_no}-{stamp}" def looks_like_continue_ticket(text: str) -> bool: """ 无网夹具用的弱提示。正式环境必须由大模型判续办意图。 禁止把本函数当成生产唯一路由。 """ raw = (text or "").strip() if not raw: return False return any(token in raw for token in _CONTINUE_HINT) def work_order_from_card_meta( card_meta: dict[str, Any] | None = None, *, text: str = "", ) -> str: """ 从企微点卡回调用找回「点的是哪一张卡当时的工单」。 优先 TaskId / EventKey 里的工单号(quote-WO…、scheme-adopt_excel-WO…)。 对不上返回空:调用方按该销售当前最新书签处理。 """ src = card_meta or {} for raw in ( str(src.get("task_id") or "").strip(), str(src.get("event_key") or "").strip(), (text or "").strip(), ): no = _work_order_in_token(raw) if no: return no return "" def _work_order_in_token(raw: str) -> str: """从 TaskId / EventKey 里抽出 WO 工单号;没有则空。""" text = (raw or "").strip() if not text: return "" for prefix in ( f"{BTN_SKIP_COLLAB_KEY}:", f"{BTN_ADOPT_EXCEL_KEY}:", f"{BTN_ADOPT_PDF_KEY}:", f"{BTN_REJECT_KEY}:", ): if text.startswith(prefix): rest = text[len(prefix) :].strip() if rest.upper().startswith("WO"): return extract_work_order_no(rest) or rest.split("-", 1)[0] # 卡片 task_id 会带时间后缀(scheme-WO…-abc),只取 WO+12 位。 return extract_work_order_no(text) _CURRENCY_PREFIX = re.compile(r"^[A-Za-z]{3}\b") def money_text(amount: Any, currency: str = "") -> str: """ 销售可见金额:TMS 有币种就写成「CNY 14669.60」。 金额已带三字码前缀则原样。没有币种不编造 CNY/USD。 """ raw = str(amount if amount not in (None, "") else "").strip() if not raw or raw == "-": return raw or "-" if _CURRENCY_PREFIX.match(raw): return raw code = str(currency or "").strip().upper() if code == "RMB": code = "CNY" if not code: return raw return f"{code} {raw}" def quote_fee_lines(quote: dict[str, Any] | None) -> list[tuple[str, str]]: """ 价格明细行:优先 TMS fee_lines,否则用运费/其它/合计兜底。 返回 (费用名, 金额) ,空金额不展示。金额带 TMS 币种,不编造。 """ src = dict(quote or {}) header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip() raw = src.get("fee_lines") parsed: Any = raw if isinstance(raw, str) and raw.strip(): try: parsed = json.loads(raw) except json.JSONDecodeError: parsed = None lines: list[tuple[str, str]] = [] if isinstance(parsed, list): for item in parsed: if not isinstance(item, dict): continue name = str(item.get("name") or item.get("label") or "").strip() amount = str(item.get("amount") or item.get("value") or "").strip() line_ccy = str(item.get("currency") or item.get("currencyCode") or header_ccy).strip() if name and amount and amount != "-": lines.append((name, money_text(amount, line_ccy))) if lines: return lines freight = str(src.get("freight") or "").strip() other = str(src.get("other_fee") or "").strip() if freight and freight != "-": lines.append(("空运费", money_text(freight, header_ccy))) if other and other != "-": lines.append(("其他附加费", money_text(other, header_ccy))) return lines def quote_area_fee_text(quote: dict[str, Any] | None) -> str: """ 卡片引用条正文:费用行 + 总销售价合计。 必须走 quote_text。塞进 quote_area.title 会被企微整段藏掉。 """ 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) fee_lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)] fee_lines.append(f"总销售价合计:{total}") return "\n".join(fee_lines) def collab_hint(work_order_no: str) -> str: """价格卡协同说明:无「内部协同」前缀,紧挨生成报价单按钮上方。""" return ( f"若需要咨询内部航线人员,请手动拉新人与智能体到原有群," f"并在群聊发送工单编号{work_order_no}。" ) def _strip_leading_dash(raw: str) -> str: """去掉时效前导 '-' / '—',避免销售看到「-3-5天」或「-;3-5天」。""" text = (raw or "").strip() while text.startswith(("-", "—", "–")): rest = text[1:].lstrip(" \t;;,,") if not rest: return "" text = rest return text def tms_eta_display(quote: dict[str, Any] | None) -> str: """时效只取 TMS 回的 eta / 预计送达;没有就「-」。禁止本地编。前导横杠会剥掉。""" src = dict(quote or {}) eta = _strip_leading_dash(str(src.get("eta") or "")) if eta: return eta dated = _strip_leading_dash( str(src.get("eta_date") or src.get("estimated_delivery") or "") ) return dated or "-" def tms_valid_until_display(quote: dict[str, Any] | None) -> str: """报价有效期只取 TMS 原文,没有就 "-"。""" src = dict(quote or {}) raw = str( src.get("valid_until") or src.get("validUntil") or src.get("quoteValidUntil") or "" ).strip() return raw if raw else "-" 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: return f"相同询价内容检索:引用历史工单号{no}" return "相同询价内容检索:首次询价" def quote_detail_lines( quote: dict[str, Any] | None, *, first_or_same: str = "first", history_work_order_no: str = "", status: str = "已报价", ) -> list[str]: """ 价格明细正文:TMS 费用、合计、时效、有效期、相同检索。 给文本兜底和企微引用条共用,禁止在这里编时效/有效期。价格卡不再展示工单状态。 """ _ = status 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) lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)] 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 def tms_hit_card( *, work_order_no: str, quote: dict[str, Any], first_or_same: str = "first", history_work_order_no: str = "", status: str = "已报价", ) -> str: """ 有价后的价格明细(文本兜底,模板卡发失败时仍给销售看)。 顺序:来源 → 价格明细 → 报价正文 → 协同说明 → 按钮。 """ src = dict(quote or {}) source = str(src.get("source_label") or "TMS标准报价").replace(" ", "") lines = [source, f"{work_order_no}价格明细"] lines.extend( quote_detail_lines( src, first_or_same=first_or_same, history_work_order_no=history_work_order_no, status=status, ) ) lines.append("") lines.append(collab_hint(work_order_no)) lines.append(BTN_SKIP_COLLAB) return "\n".join(lines) def tms_hit_wecom_payload( *, work_order_no: str, quote: dict[str, Any], first_or_same: str = "first", history_work_order_no: str = "", status: str = "已报价", ) -> dict[str, Any]: """ 一张价格卡,销售侧顺序: 价格明细 ┌ 引用条:空运费 / 其它费用 / 总销售价合计 时效、报价有效期、相同询价内容检索 协同说明(普通文字,贴按钮上方) 按钮 企微 quote_area.title 只能很短,超长会被整段藏掉。报价必须放 quote_text。 不用 horizontal:它会排到副标题后面,把费用挤到按钮前。 """ 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), ] ) return { "msgtype": "template_card", "template_card": { "card_type": "button_interaction", "source": {"desc": source, "desc_color": 0}, "main_title": {"title": f"{work_order_no}价格明细"}, "quote_area": { "type": 0, "quote_text": quote_area_fee_text(src), }, "sub_title_text": meta, "task_id": card_task_id("quote", work_order_no), "button_list": [ { "text": BTN_SKIP_COLLAB, "style": 1, # EventKey 也带工单号:TaskId 偶发缺失时仍能找回当时那一单 "key": f"{BTN_SKIP_COLLAB_KEY}:{work_order_no}", } ], }, } def skip_collab_card_update(*, response_code: str) -> dict[str, Any]: """ 点过「生成报价单」后更新原卡:按钮改成已提交并保持置灰。 必须带企微回调 ResponseCode;无码则不要入出站。 """ return { "msgtype": "update_template_card", "response_code": response_code, "button": {"replace_name": BTN_SKIP_COLLAB_DONE}, } def tms_miss_quote_text(*, work_order_no: str) -> str: """引用条正文:只放「请务必在群聊发送工单编号」。""" return f"请务必在群聊发送工单编号{work_order_no}" def tms_miss_hint() -> str: """卡片正文:无价说明 + 拉群提示,不单独再写标题下灰字。""" return "TMS标准价库暂无匹配报价,请手动拉新人与智能体到原有群" 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)}" ) def tms_miss_wecom_payload(*, work_order_no: str, action_url: str = "") -> dict[str, Any]: """ TMS 已查过、确认没价:一张 text_notice 卡。 顺序:来源 → 标题(仅工单号+暂无报价)→ 引用条(工单编号)→ 拉群说明。 禁止:标题下灰字、工单状态、查价结果/来源行、按钮。 企微 text_notice 必须带可信域名 card_action,否则整卡发失败只剩文字。 """ 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), }, "sub_title_text": tms_miss_hint(), "task_id": card_task_id("miss", work_order_no), } url = (action_url or "").strip() if url: card["card_action"] = {"type": 1, "url": url} return {"msgtype": "template_card", "template_card": card} def tms_contract_gap(*, work_order_no: str) -> str: """已废弃:不再因字典对不上拦查价;保留以免旧单测/引用报错。""" return tms_miss(work_order_no=work_order_no) def tms_need_more(*, work_order_no: str, hint: str = "") -> str: """已废弃:TMS 回 1003 按合同收成暂无报价。保留以免旧引用报错。""" _ = hint return tms_miss(work_order_no=work_order_no) def skip_collab_ack() -> str: return "好的,已跳过内部协同。正在按 TMS 标准报价生成报价单草稿…" def scheme_card(*, work_order_no: str, status: str, facts: dict[str, str], quote: dict[str, Any]) -> str: """ 报价方案文本兜底(模板卡发失败时仍给销售看)。 与卡片同一套字段,禁止漏项。不再展示工单状态。 """ _ = status src = dict(quote or {}) # 方案卡不展示询价「报价日期」,改展示 TMS 报价有效期 body = "\n".join( line for line in inquiry_field_lines(work_order_no=work_order_no, facts=facts) if not line.startswith("报价日期:") ) eta = tms_eta_display(src) valid = tms_valid_until_display(src) lines = [ f"【{work_order_no}报价方案】", body, "价格明细", quote_area_fee_text(src), f"时效:{eta or '-'}", f"报价有效期:{valid}", "请确认是否采用该报价方案:", f"【{BTN_ADOPT_EXCEL_CARD}】", f"【{BTN_ADOPT_PDF_CARD}】", f"【{BTN_REJECT}】", ] return "\n".join(lines) def _clip(raw: str, limit: int) -> str: """企微列表格有字数建议;超长才裁,避免空白。""" text = (raw or "").strip() or "-" return text if len(text) <= limit else text[: limit - 1] + "…" def scheme_horizontal_rows( *, work_order_no: str, facts: dict[str, str], quote: dict[str, Any], ) -> list[dict[str, str]]: """ 卡片字段:一行一项,每格独立字数,不会像副标题整段截成 8CB…。 企微 horizontal 最多 6 条。运输方式写进航线行,避免丢字段。 不用 sub_title 堆字段(它会排到列表前面,且超长必截断)。 """ src = facts or {} q = dict(quote or {}) origin = (src.get("起运港") or "-").strip() or "-" dest = (src.get("目的港") or "-").strip() or "-" cargo = (src.get("品名") or "-").strip() or "-" pieces = _with_unit(str(src.get("件数") or ""), "件") weight = _with_unit(str(src.get("毛重") or ""), "KGS") volume = _with_unit(str(src.get("体积") or ""), "CBM") pack = (src.get("包装方式") or src.get("包装类型") or "-").strip() or "-" return [ {"keyname": "工单号", "value": _clip(work_order_no, 26)}, {"keyname": "航线", "value": _clip(f"空运 {origin}→{dest}", 26)}, {"keyname": "货物", "value": _clip(f"{cargo};{pieces};{weight};{volume}", 26)}, {"keyname": "包装", "value": _clip(pack, 26)}, {"keyname": "时效", "value": _clip(tms_eta_display(q), 26)}, {"keyname": "报价有效期", "value": _clip(tms_valid_until_display(q), 26)}, ] def scheme_wecom_payload( *, work_order_no: str, facts: dict[str, str], quote: dict[str, Any], status: str = "已报价", ) -> dict[str, Any]: """ 一张报价方案卡:标题带工单号、引用条、字段、三个短按钮。 按钮文案必须短:生成Excel / 生成PDF / 不采用。长文案横排会被裁。 """ _ = status src = dict(quote or {}) source = str(src.get("source_label") or "TMS标准报价").replace(" ", "") return { "msgtype": "template_card", "template_card": { "card_type": "button_interaction", "source": {"desc": source, "desc_color": 0}, "main_title": {"title": f"{work_order_no}报价方案"}, "quote_area": { "type": 0, "quote_text": quote_area_fee_text(src), }, "horizontal_content_list": scheme_horizontal_rows( work_order_no=work_order_no, facts=facts, quote=src ), "task_id": card_task_id("scheme", work_order_no), "button_list": [ { "text": BTN_ADOPT_EXCEL_CARD, "style": 1, "key": f"{BTN_ADOPT_EXCEL_KEY}:{work_order_no}", }, { "text": BTN_ADOPT_PDF_CARD, "style": 1, "key": f"{BTN_ADOPT_PDF_KEY}:{work_order_no}", }, { "text": BTN_REJECT, "style": 2, "key": f"{BTN_REJECT_KEY}:{work_order_no}", }, ], }, } def reject_ack() -> str: return "请问如何调整报价单,以满足您的需求?您可使用「报价加 10%」等调价指令。" def adopt_excel_ack() -> str: return "已确认采用该方案,正在生成 Excel 最终报价单…" def adopt_pdf_ack() -> str: return "已确认采用该方案,正在生成PDF最终报价单…" QUOTE_TEMPLATE_MISSING = "生成报价单失败,请联系IT运维维护后台报价模板。" def quote_template_missing() -> str: """关键词与默认模板都未维护时的销售提示(产品口径,勿改字)。""" return QUOTE_TEMPLATE_MISSING 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}】" ) def deal_ack(outcome: str, work_order_no: str) -> str: if outcome == "已成交": return f"已记录为「已成交」。工单 {work_order_no} 已关闭智能体跟进。" if outcome == "未成交": return f"已记录为「未成交」。请补充未成交原因,工单 {work_order_no} 将关闭智能体跟进。" return f"已记录为「协商中」。工单 {work_order_no} 跟进周期已顺延。"