Files
tw2/qingjian_cs/app/rules.py
jimi a842f1861f
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
chore: initial import of standalone agentscope project
2026-03-02 18:21:40 +08:00

133 lines
5.0 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.
import re
from dataclasses import dataclass
IMAGE_URL_RE = re.compile(r"https?://[^\s]+(?:\.jpg|\.jpeg|\.png|\.webp|\.bmp|\.gif)(?:\?[^\s]*)?", re.I)
SIZE_RE = re.compile(r"(\d+(?:\.\d+)?)\s*(米|m|M)\s*[xX*乘]\s*(\d+(?:\.\d+)?)\s*(米|m|M)")
MAP_POLITICAL_KWS = ["地图", "国界", "边界", "南海", "台湾", "香港", "澳门", "西藏", "新疆", "政治"]
PORN_RISK_KWS = ["", "成人视频", "成人视频", "性爱", "激情", "成人视频"]
EXTERNAL_CONTACT_KWS = ["微信", "vx", "vx", "qq", "手机号", "电话", "加我", "私下"]
PRICE_KWS = ["多少钱", "怎么收费", "报价", "价格", "多少米", "多少"]
GREETING_KWS = ["你好", "您好", "在吗", "在不在", "hello", "hi"]
FINISH_KWS = ["发完了", "没了", "就这些", "报价吧", "可以报价", "先这样"]
NONSENSE_KWS = ["", "", "好的", "ok", "1", "收到"]
@dataclass
class RuleResult:
ignore: bool = False
normalized_msg: str = ""
reason: str = ""
def extract_customer_text_from_shop_card(msg: str) -> str:
if "[进店卡片]" not in (msg or ""):
return ""
prefix = msg.split("#*#[进店卡片]", 1)[0].strip()
if prefix and prefix not in {"你好", "您好", "在吗"}:
return prefix
return prefix
def detect_order_status(order_text: str) -> str:
t = (order_text or "")
if "买家已付款" in t:
return "paid"
if "等待买家付款" in t or "待付款" in t:
return "pending_payment"
if "已退款" in t or "退款" in t:
return "refund"
return "unknown"
def extract_size_pairs_m(msg: str) -> list[tuple[float, float]]:
out: list[tuple[float, float]] = []
for m in SIZE_RE.finditer(msg or ""):
w = float(m.group(1))
h = float(m.group(3))
out.append((w, h))
return out
def has_map_or_political_risk(msg: str, goods_name: str = "") -> bool:
t = f"{msg or ''} {goods_name or ''}".lower()
return any(k.lower() in t for k in MAP_POLITICAL_KWS)
def has_porn_risk(msg: str) -> bool:
t = (msg or "").lower()
return any(k.lower() in t for k in PORN_RISK_KWS)
def requests_external_contact(msg: str) -> bool:
t = (msg or "").lower()
return any(k.lower() in t for k in EXTERNAL_CONTACT_KWS)
def is_meaningless_short(msg: str) -> bool:
t = (msg or "").strip().lower()
if len(t) <= 2:
return True
return t in NONSENSE_KWS
def prefilter_message(msg: str, msg_type: int) -> RuleResult:
m = (msg or "").strip()
if not m:
return RuleResult(ignore=True, reason="empty")
if msg_type not in (0, 1):
return RuleResult(ignore=True, reason="unsupported_msg_type")
if "" in m and " 转交给 " in m:
return RuleResult(ignore=True, reason="transfer_notice")
if "Gemini 店铺消息,跳过" in m:
return RuleResult(ignore=True, reason="system_echo")
if "[进店卡片]" in m:
t = extract_customer_text_from_shop_card(m)
if t:
return RuleResult(ignore=False, normalized_msg=t, reason="shop_card_with_text")
return RuleResult(ignore=True, reason="pure_shop_card")
return RuleResult(ignore=False, normalized_msg=m, reason="normal")
def detect_intent(msg: str) -> str:
m = (msg or "").lower()
if IMAGE_URL_RE.search(m):
return "image"
if any(k in m for k in FINISH_KWS):
return "finish_or_quote_trigger"
if any(k in m for k in PRICE_KWS):
return "pricing"
if any(k in m for k in GREETING_KWS):
return "greeting"
if requests_external_contact(m):
return "external_contact"
if is_meaningless_short(m):
return "nonsense"
return "unknown"
def extract_image_urls(msg: str) -> list[str]:
return IMAGE_URL_RE.findall(msg or "")
def rules_prompt() -> str:
return (
"你是淘宝客服主决策。只输出JSON动作不要解释。\n"
"动作 action 只能是: reply / quote / transfer / noop。\n"
"规则提炼(严格执行):\n"
"1) 客户发图: 先承接, 允许继续收图。\n"
"2) 询价且有图(当前/待处理): 优先 quote。\n"
"3) 无图询价: reply 承接并引导发图。\n"
"4) 客户说发完了/报价吧/没图: 若有待处理图则 quote。\n"
"5) 外部联系方式请求: reply 站内引导, 不给微信QQ手机号。\n"
"6) 地图/政治/黄暴风险: transfer 或拒绝性 reply。\n"
"7) 仅无意义短句(嗯/哦/ok): 给简短自然承接, 不要长回复。\n"
"8) 避免重复同一句; 若上句语义相同则换表达。\n"
"9) 订单已付款: 可回复已安排; 待付款: 提示先付款。\n"
"10) 尺寸明显超大(如>=2m*2m): 提示需补图/重做边缘, 不要直接承诺一模一样。\n"
"11) 店铺差异化: 按 acc_id/persona 口吻回复, 保持真人聊天。\n"
"12) 最终输出只允许一个动作, 不能混合。\n"
"输出格式:\n"
'{"action":"reply|quote|transfer|noop","reply":"","transfer_msg":"","quote_mode":"flush_pending|analyze_current_or_recent|collect_only","reason":""}'
)