落地海运协同群:测服靠会话存档听群话,报价单能带上协同字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-15 18:12:45 +08:00
co-authored by Cursor
parent 235ecd8c83
commit 12219cacb1
39 changed files with 4436 additions and 256 deletions
+702
View File
@@ -0,0 +1,702 @@
"""
海运协同群业务:填表进群、补字段重发、产品报价、附件、解散、成交跟进。
本文件职责:群里副作用(主账+企微群)。私聊按钮仍走 sea_text_flow.on_button。
禁止:改询价七项;终态默默忽略;把销售附件当报价。
"""
from __future__ import annotations
import logging
from typing import Any, Optional
from agent.llm.extract_text import extract_collab_fields, extract_product_quote
from agent.policy.quote_overlay import overlay_labeled_facts
from agent.policy import inquiry_copy as copy
from agent.schema.field_validate import SEA_COLLAB_DISPLAY, SEA_COLLAB_FIELDS
# 主账填表按英文码取格子;中文键必须同时带上,否则货好时间/条款写不进表。
_COLLAB_FILL_ALIASES = {
"贸易条款": ("贸易条款", "trade_terms"),
"货好时间": ("货好时间", "cargo_ready_date"),
"HS编码": ("HS编码", "商品海关编码", "海关编码", "hs_code"),
"是否含油": ("是否含油", "has_oil"),
"是否含电": ("是否含电", "has_battery"),
"是否含磁": ("是否含磁", "has_magnet"),
}
logger = logging.getLogger(__name__)
TERMINAL = {"已成交", "未成交", "已关闭"}
OPEN_EDIT = {"询价中", "已报价", "协商中"}
EXCEL_PDF = {".xlsx", ".xls", ".xlsm", ".pdf"}
def ticket_view(ticket: object) -> dict[str, Any]:
"""主账工单或内存票统一成 dict。"""
if ticket is None:
return {}
if isinstance(ticket, dict):
view = dict(ticket)
view.setdefault("work_order_no", view.get("workOrderNo") or "")
view.setdefault("sales_wecom_id", view.get("salesWecomId") or view.get("sales_wecom_id") or "")
view.setdefault("salesWecomId", view.get("sales_wecom_id") or view.get("salesWecomId") or "")
view.setdefault(
"collab_member_ids",
view.get("memberIds") or view.get("collabMemberIds") or view.get("collab_member_ids") or [],
)
view.setdefault(
"product_ids",
view.get("productIds") or view.get("product_ids") or [],
)
view.setdefault(
"product_names",
view.get("productNames") or view.get("product_names") or [],
)
view.setdefault("sales_name", view.get("salesName") or view.get("sales_name") or "")
facts = dict(view.get("collab_facts") or view.get("collabFacts") or {})
view.setdefault("collab_facts", facts)
flag = str(facts.get("__ended") or view.get("collab_ended") or view.get("collabEnded") or "").strip().lower()
if flag in {"1", "true", "yes"}:
view["collab_ended"] = True
return view
facts = dict(getattr(ticket, "collab_facts", {}) or {})
ended = bool(getattr(ticket, "collab_ended", False)) or str(
facts.get("__ended") or ""
).strip().lower() in {"1", "true", "yes"}
return {
"work_order_no": getattr(ticket, "work_order_no", "") or "",
"status": getattr(ticket, "status", "") or "",
"sales_wecom_id": getattr(ticket, "sales_wecom_id", "") or "",
"salesWecomId": getattr(ticket, "sales_wecom_id", "") or "",
"business_line": getattr(ticket, "business_line", "") or "SEA",
"facts": dict(getattr(ticket, "facts", {}) or {}),
"quote": dict(getattr(ticket, "quote", {}) or {}),
"collab_facts": dict(getattr(ticket, "collab_facts", {}) or {}),
"collab_chat_id": getattr(ticket, "collab_chat_id", "") or "",
"collab_member_ids": list(getattr(ticket, "collab_member_ids", []) or []),
"collab_ended": ended,
"product_ids": list(getattr(ticket, "product_ids", []) or []),
"product_names": list(getattr(ticket, "product_names", []) or []),
"sales_name": str(getattr(ticket, "sales_name", "") or ""),
}
def ticket_status(ticket: object) -> str:
return str(ticket_view(ticket).get("status") or "").strip()
def ticket_no(ticket: object) -> str:
return str(ticket_view(ticket).get("work_order_no") or "").strip()
def is_terminal(ticket: object) -> bool:
return ticket_status(ticket) in TERMINAL
def is_sales(ticket: object, sender_id: str) -> bool:
uid = (sender_id or "").strip()
view = ticket_view(ticket)
sales = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
return bool(uid) and uid == sales
def is_product(ticket: object, sender_id: str) -> bool:
"""产品岗:成员里且不是销售。"""
uid = (sender_id or "").strip()
if not uid or is_sales(ticket, uid):
return False
view = ticket_view(ticket)
members = {str(x) for x in (view.get("collab_member_ids") or view.get("memberIds") or [])}
products = {str(x) for x in (view.get("product_ids") or [])}
if products:
return uid in products
return uid in members
def filename_is_excel_pdf(name: str) -> bool:
raw = (name or "").strip().lower()
return any(raw.endswith(ext) for ext in EXCEL_PDF)
def group_client_of(flow: Any) -> Any:
"""正式路径没有注入客户端时,走流程自己的默认企微客户端。"""
getter = getattr(flow, "group_client", None)
if callable(getter):
return getter()
return getattr(flow, "_group_client", None)
def _archive_seat_ids(flow: Any) -> set[str]:
"""会话存档账号(测服 AI001 / 询价机器人)不能出现在群 @ 里。"""
seats: set[str] = {"AI001", "询价机器人"}
getter = getattr(flow, "_archive_seat_userids", None)
if callable(getter):
try:
seats.update(str(x).strip() for x in (getter() or []) if str(x).strip())
except Exception:
logger.warning("读会话存档账号失败,按默认 AI001 排除")
return {x for x in seats if x}
def _staff_display_name(flow: Any, wecom_id: str) -> str:
"""按企微 userid 取后台员工姓名;查不到就退回 userid,仍是具体人。"""
uid = (wecom_id or "").strip()
if not uid:
return ""
book = getattr(flow, "ledger", None) or getattr(flow, "_ledger", None)
finder = getattr(book, "get_staff_by_wecom_id", None) if book is not None else None
if callable(finder):
try:
row = finder(wecom_id=uid) or {}
name = str(row.get("name") or "").strip()
if name:
return name
except Exception:
logger.warning("查群成员姓名失败 uid=%s", uid)
return uid
def resolve_group_mentions(
flow: Any,
ticket: object,
sess: Any = None,
) -> tuple[str, list[str], list[str]]:
"""
群摘要必须 @ 到具体销售和每位产品,禁止只写岗位词「销售」。
进程重启后内存会话没了,主账 byCollabChat 也可能不带姓名:
用销售 userid + 群成员(去掉存档账号)反查员工管理。
返回:(销售展示名, 产品展示名列表, 要点到的 userid)。
"""
view = ticket_view(ticket)
sales_id = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
if sess is not None:
sales_id = sales_id or str(getattr(sess, "sales_wecom_id", "") or getattr(sess, "sender_id", "") or "").strip()
members = [
str(x).strip()
for x in (view.get("collab_member_ids") or view.get("memberIds") or [])
if str(x).strip()
]
if sess is not None:
for uid in getattr(sess, "invited_userids", ()) or ():
raw = str(uid).strip()
if raw and raw not in members:
members.append(raw)
seats = _archive_seat_ids(flow)
product_ids = [
str(x).strip()
for x in (view.get("product_ids") or view.get("productIds") or [])
if str(x).strip() and str(x).strip() not in seats
]
if not product_ids:
product_ids = [m for m in members if m and m != sales_id and m not in seats]
sales_name = ""
if sess is not None:
sales_name = str(getattr(sess, "invited_sales_name", "") or "").strip()
sales_name = sales_name or str(view.get("sales_name") or view.get("salesName") or "").strip()
if not sales_name or sales_name == "销售":
looked = _staff_display_name(flow, sales_id) if sales_id else ""
if looked:
sales_name = looked
if not sales_name:
sales_name = sales_id or "销售"
products: list[str] = []
if sess is not None:
products = [
str(n).strip()
for n in (getattr(sess, "invited_product_names", None) or [])
if str(n or "").strip()
]
if not products:
products = [
str(n).strip()
for n in (view.get("product_names") or view.get("productNames") or [])
if str(n or "").strip()
]
if not products:
products = [name for name in (_staff_display_name(flow, pid) for pid in product_ids) if name]
mention_ids: list[str] = []
if sales_id and sales_id not in seats:
mention_ids.append(sales_id)
for pid in product_ids:
if pid and pid not in mention_ids and pid not in seats:
mention_ids.append(pid)
return sales_name, products, mention_ids
def send_group_text(
client: Any,
chat_id: str,
content: str,
mention_userids: Optional[list[str]] = None,
) -> None:
"""群文本可带企微 userid,方便客户端真正点到人。旧客户端无此参数就只发正文。"""
if not client or not chat_id or not content:
return
sender = getattr(client, "send_group", None)
if not callable(sender):
return
mentions = [str(x).strip() for x in (mention_userids or []) if str(x).strip()]
try:
sender(chat_id=chat_id, content=content, mention_userids=mentions)
except TypeError:
sender(chat_id=chat_id, content=content)
def hydrate_ticket(flow: Any, ticket: object) -> object:
"""
按群找回的工单若缺询价七项,用工单号+销售再读主账补全。
测服 byCollabChat 可能只回协同字段;续办 get 空 sender 也不回 facts。
禁止用空 facts 把已确认港口冲成「-」。
"""
view = ticket_view(ticket)
facts = dict(view.get("facts") or {})
if any(str(facts.get(k) or "").strip() for k in ("起运港", "目的港", "品名", "货量")):
return ticket
no = ticket_no(ticket)
sales = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
book = getattr(flow, "ledger", None)
if book is None or not no:
return ticket
full: Any = None
getter = getattr(book, "get_for_agent", None)
if callable(getter) and sales:
full = getter(work_order_no=no, sender_id=sales)
if not isinstance(full, dict) or not dict(full.get("facts") or {}):
getter = getattr(book, "get_ticket", None)
if callable(getter):
full = getter(work_order_no=no)
if full is None:
return ticket
merged = dict(view)
full_view = ticket_view(full)
if full_view.get("facts"):
merged["facts"] = dict(full_view.get("facts") or {})
if full_view.get("quote") and not merged.get("quote"):
merged["quote"] = dict(full_view.get("quote") or {})
if full_view.get("status"):
merged.setdefault("status", full_view.get("status"))
collab = dict(full_view.get("collab_facts") or {})
collab.update(dict(view.get("collab_facts") or {}))
merged["collab_facts"] = collab
if view.get("collab_chat_id"):
merged["collab_chat_id"] = view.get("collab_chat_id")
return merged
def send_brief_and_sheet(
*,
flow: Any,
ticket: object,
sess: Any = None,
quote_override: Optional[dict[str, Any]] = None,
) -> str:
"""
群里再发一版摘要 + 填好的海运模板。旧消息不撤。
无价也发表,询价和协同能填的先填。
"""
ticket = hydrate_ticket(flow, ticket)
view = ticket_view(ticket)
no = ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "").strip()
client = group_client_of(flow)
facts = dict(view.get("facts") or {})
if sess is not None and getattr(sess, "facts", None):
facts.update(dict(sess.facts or {}))
collab = dict(view.get("collab_facts") or {})
if sess is not None and getattr(sess, "collab_facts", None):
collab.update(dict(sess.collab_facts or {}))
quote = dict(quote_override or (sess.quote if sess else None) or view.get("quote") or {})
sales_name, products, mention_ids = resolve_group_mentions(flow, ticket, sess)
has_price = bool(quote.get("total") or quote.get("fee_rows"))
brief = copy.sea_group_brief(
work_order_no=no,
facts=facts,
quote=quote,
collab_facts=collab,
sales_name=sales_name,
product_names=products,
has_price=has_price,
)
send_group_text(client, chat_id, brief, mention_userids=mention_ids)
send_filled_sheet(flow=flow, ticket=ticket, sess=sess, quote_override=quote_override)
return "brief_and_sheet"
def send_filled_sheet(
*,
flow: Any,
ticket: object,
sess: Any = None,
quote_override: Optional[dict[str, Any]] = None,
) -> None:
"""只发填好的模板文件,不再发文字摘要。"""
ticket = hydrate_ticket(flow, ticket)
view = ticket_view(ticket) if ticket is not None else {}
no = (sess.work_order_no if sess else "") or ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "").strip()
facts = dict(view.get("facts") or {})
if sess is not None and getattr(sess, "facts", None):
facts.update(dict(sess.facts or {}))
collab = dict(view.get("collab_facts") or {})
if sess is not None and getattr(sess, "collab_facts", None):
collab.update(dict(sess.collab_facts or {}))
quote = dict(quote_override or (sess.quote if sess else None) or view.get("quote") or {})
_render_and_send_sheet(
flow=flow,
chat_id=chat_id,
work_order_no=no,
facts=_facts_for_fill(facts, collab),
quote=quote,
sales_id=str(
(sess.sender_id if sess else "")
or view.get("sales_wecom_id")
or view.get("salesWecomId")
or ""
),
)
def _collab_value(collab: dict[str, str], key: str) -> str:
"""协同六项:内部键、对外展示名都能取到。"""
src = dict(collab or {})
val = str(src.get(key) or "").strip()
if val:
return val
label = SEA_COLLAB_DISPLAY.get(key, "")
if label:
val = str(src.get(label) or "").strip()
return val
def _oil_elec_mag_text(collab: dict[str, str]) -> str:
"""
海运模板是一格「是否含油,电,磁」,三句话拼成一句再写入。
没说的写成 -,不编造。
"""
oil = _collab_value(collab, "是否含油")
elec = _collab_value(collab, "是否含电")
mag = _collab_value(collab, "是否含磁")
if not (oil or elec or mag):
return ""
return f"油:{oil or '-'};电:{elec or '-'};磁:{mag or '-'}"
def _facts_for_fill(facts: dict[str, str], collab: dict[str, str]) -> dict[str, str]:
"""
询价七项 + 协同六项。中文键和填表英文码一起带上。
主账 writeFactsByLabel 只认 cargo_ready_date / trade_terms,只传「货好时间」会空着。
"""
merged = dict(facts or {})
src = dict(collab or {})
for key in SEA_COLLAB_FIELDS:
val = _collab_value(src, key)
if not val:
continue
for alias in _COLLAB_FILL_ALIASES.get(key, (key,)):
merged[alias] = val
oem = _oil_elec_mag_text(src)
if oem:
merged["oil_elec_mag"] = oem
merged["是否含油,电,磁"] = oem
return merged
def _render_and_send_sheet(
*,
flow: Any,
chat_id: str,
work_order_no: str,
facts: dict[str, Any],
quote: dict[str, Any],
sales_id: str,
) -> None:
renderer = getattr(flow.ledger, "render_quote", None)
client = group_client_of(flow)
if not callable(renderer) or not chat_id:
return
result = renderer(
work_order_no=work_order_no,
business_line="SEA",
facts=facts,
quote=quote,
format="xlsx",
sales_wecom_id=sales_id,
) or {}
if not result.get("ok"):
logger.warning("群填表失败 wo=%s err=%s", work_order_no, result.get("error"))
return
filename = str(result.get("fileName") or f"{work_order_no}_quote.xlsx")
sender = getattr(client, "send_group_file", None)
if not callable(sender):
logger.warning("群发文件无客户端 wo=%s chat=%s", work_order_no, chat_id)
return
painted = overlay_labeled_facts(
filepath=str(result.get("filePath") or ""),
file_b64=str(result.get("fileBase64") or ""),
facts=facts,
)
sent = sender(
chat_id=chat_id,
filename=filename,
filepath=str(painted.get("filepath") or ""),
file_b64=str(painted.get("file_b64") or ""),
) or {}
if sent.get("ok") is False:
logger.warning("群发文件失败 wo=%s err=%s", work_order_no, sent.get("error"))
send_group_text(client, chat_id, copy.group_sheet_send_fail())
return
logger.info("群已发模板 wo=%s file=%s", work_order_no, filename)
def apply_collab_update(
*,
flow: Any,
ticket: object,
text: str,
sender_id: str,
sess: Any = None,
) -> str:
"""补协同六项。终态要说明原因。"""
no = ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or ticket_view(ticket).get("collab_chat_id") or "")
client = group_client_of(flow)
if is_terminal(ticket):
send_group_text(client, chat_id, copy.ticket_locked(work_order_no=no, status=ticket_status(ticket)))
return "ticket_locked"
fields = extract_collab_fields(text, allow_b=True)
if not fields:
send_group_text(client, chat_id, copy.group_fields_not_recognized())
return "collab_not_recognized"
flow.apply_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
if sess is not None:
merged = dict(sess.collab_facts)
merged.update(fields)
sess.collab_facts = merged
flow._save(sess)
ticket2 = _reload(flow, chat_id, no) or ticket
view = ticket_view(ticket2)
collab = dict(view.get("collab_facts") or {})
collab.update(fields)
view["collab_facts"] = collab
if chat_id and not view.get("collab_chat_id"):
view["collab_chat_id"] = chat_id
send_brief_and_sheet(flow=flow, ticket=view, sess=sess)
return "collab_field_saved"
def apply_product_quote(
*,
flow: Any,
ticket: object,
text: str,
sender_id: str,
sess: Any = None,
injected_quote: Optional[dict[str, Any]] = None,
) -> str:
"""
产品文字报价:盖 TMS 价、填表发出、群里成交跟进。
一句话里的协同字段一并写入。非产品或终态要回话。
"""
no = ticket_no(ticket)
view = ticket_view(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")
client = group_client_of(flow)
if is_terminal(ticket):
send_group_text(client, chat_id, copy.ticket_locked(work_order_no=no, status=ticket_status(ticket)))
return "ticket_locked"
if not is_product(ticket, sender_id):
return "group_ignore"
fields = extract_collab_fields(text)
if fields:
flow.apply_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
if sess is not None:
merged = dict(sess.collab_facts)
merged.update(fields)
sess.collab_facts = merged
quote = extract_product_quote(text, injected=injected_quote)
if not quote:
send_group_text(client, chat_id, copy.group_quote_not_recognized())
return "quote_not_recognized"
_save_product_quote(flow, no, quote, sess)
ticket2 = _reload(flow, chat_id, no) or ticket
send_brief_and_sheet(flow=flow, ticket=ticket2, sess=sess, quote_override=quote)
emit_group_deal(flow=flow, ticket=ticket2, sess=sess, quote=quote)
return "product_quote_sent"
def handle_group_file(
*,
flow: Any,
ticket: object,
sender_id: str,
filename: str,
media: dict[str, Any],
sess: Any = None,
) -> str:
"""
群附件:只收 Excel/PDF。终态不存要提醒。
产品 Excel/PDF 当报价并出成交跟进;别人的只存档。
"""
no = ticket_no(ticket)
view = ticket_view(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")
client = group_client_of(flow)
if is_terminal(ticket):
send_group_text(client, chat_id, copy.ticket_locked_file(work_order_no=no, status=ticket_status(ticket)))
return "ticket_locked_file"
if not filename_is_excel_pdf(filename):
send_group_text(client, chat_id, copy.group_only_excel_pdf())
return "file_type_rejected"
_archive_file(flow, no, filename, media, sender_id)
if is_product(ticket, sender_id):
quote = {
"source_label": "产品附件报价",
"file_name": filename,
"total": str((view.get("quote") or {}).get("total") or "-"),
}
_save_product_quote(flow, no, quote, sess)
emit_group_deal(flow=flow, ticket=ticket, sess=sess, quote=quote)
return "product_file_quote"
return "file_archived"
def dissolve_group(*, flow: Any, ticket: object, sess: Any = None) -> str:
"""解散应用群,这单协同结束,不改六态。"""
view = ticket_view(ticket)
no = ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")
client = group_client_of(flow)
members = list(view.get("collab_member_ids") or view.get("memberIds") or [])
if sess is not None:
members = list(sess.invited_userids or members)
dismiss = getattr(client, "dismiss_group", None)
if callable(dismiss) and chat_id:
dismiss(chat_id=chat_id, userids=members)
if chat_id:
try:
from agent.channel.wshoto.rooms import unwatch_collab_room
unwatch_collab_room(chat_id)
except Exception:
logger.exception("取消存档监听失败 chat=%s", chat_id)
ender = getattr(flow.ledger, "end_collab_group", None)
if callable(ender):
ender(work_order_no=no)
if sess is not None:
sess.collab_ended = True
flow._save(sess)
send_group_text(client, chat_id, copy.group_dissolved(work_order_no=no))
return "group_dissolved"
def emit_group_deal(
*,
flow: Any,
ticket: object,
sess: Any = None,
quote: Optional[dict[str, Any]] = None,
) -> None:
"""群里只发一张成交跟进。版本 +1,旧卡点了要提示。"""
view = ticket_view(ticket)
no = ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")
client = group_client_of(flow)
q = dict(quote or (sess.quote if sess else None) or view.get("quote") or {})
facts = dict((sess.facts if sess else None) or view.get("facts") or {})
ver = 1
if sess is not None:
sess.deal_version = int(getattr(sess, "deal_version", 0) or 0) + 1
ver = sess.deal_version
sess.phase = "wait_deal"
sess.allowed = (copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE)
sess.status = "已报价"
sess.quote = q
sess.wait_version += 1
flow._save(sess)
sales_name, _products, mention_ids = resolve_group_mentions(flow, ticket, sess)
text = f"@{sales_name}\n" + copy.deal_card(work_order_no=no, quote=q, facts=facts)
# 成交跟进只点销售:产品不能点这张卡。
sales_mention = mention_ids[:1] if mention_ids else []
send_group_text(client, chat_id, text, mention_userids=sales_mention)
carder = getattr(client, "send_group_card", None)
payload = copy.deal_wecom_payload(work_order_no=no, quote=q, facts=facts, card_version=ver)
if callable(carder):
carder(chat_id=chat_id, template_card=payload.get("template_card") or {})
def explain_deal_click(
*,
flow: Any,
sender_id: str,
action: str,
card_meta: Optional[dict[str, Any]],
sess: Any,
) -> str:
"""
成交卡点错人 / 旧版 / 终态:群里说清楚,不当没反应。
返回空表示可以继续走成交。
"""
if action not in {copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE}:
return ""
if sess is None:
return ""
chat_id = (sess.collab_chat_id or "").strip()
client = group_client_of(flow)
no = sess.work_order_no
if sess.status in TERMINAL:
send_group_text(client, chat_id, copy.ticket_locked(work_order_no=no, status=sess.status))
return "ticket_locked"
if sender_id != sess.sender_id:
send_group_text(client, chat_id, copy.group_deal_sales_only())
return "not_sales"
clicked = copy.deal_card_version(card_meta)
current = int(getattr(sess, "deal_version", 0) or 0)
if current and clicked and clicked != current:
send_group_text(client, chat_id, copy.group_deal_stale(work_order_no=no))
return "stale_deal"
return ""
def _save_product_quote(flow: Any, no: str, quote: dict[str, Any], sess: Any) -> None:
writer = getattr(flow.ledger, "upsert_product_quote", None) or getattr(
flow.ledger, "upsert_quote", None
)
if callable(writer):
writer(work_order_no=no, quote=quote, to_status="已报价")
if sess is not None:
sess.quote = dict(quote)
sess.status = "已报价"
flow._save(sess)
def _archive_file(flow: Any, no: str, filename: str, media: dict[str, Any], sender_id: str) -> None:
saver = getattr(flow.ledger, "attach_file", None)
if callable(saver):
saver(
work_order_no=no,
file_name=filename,
object_key=str(media.get("media_id") or media.get("MediaId") or filename),
content_type=str(media.get("content_type") or ""),
sender_id=sender_id,
)
def _reload(flow: Any, chat_id: str, no: str) -> object:
finder = getattr(flow.ledger, "find_by_collab_chat", None)
if callable(finder) and chat_id:
hit = finder(chat_id=chat_id)
if hit is not None:
return hit
getter = getattr(flow.ledger, "get_ticket", None)
if callable(getter) and no:
return getter(work_order_no=no)
return None