phone-act-x 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: phone-act-x
3
+ Version: 1.0.0
4
+ Summary: 微信视频号私信自动回复引擎 — 贷款业务场景关键词匹配
5
+ Author: phone-act
6
+ License: MIT
7
+ Requires-Python: >=3.10
@@ -0,0 +1,22 @@
1
+ """
2
+ phone_act — 微信视频号私信自动回复引擎
3
+ =========================================
4
+
5
+ 用法:
6
+ from phone_act import process, PhoneAct
7
+
8
+ # 快捷方式
9
+ result = process("我征信不好")
10
+ if result["matched"]:
11
+ wechat_send(result["reply"])
12
+
13
+ # 完整控制
14
+ bot = PhoneAct()
15
+ bot.add_rule("欢迎", ["你好"], "您好!")
16
+ result = bot.process("你好")
17
+ """
18
+
19
+ from phone_act.engine import PhoneAct, process, get_bot, clean_text, encode_phone, decode_phone
20
+
21
+ __version__ = "1.0.0"
22
+ __all__ = ["PhoneAct", "process", "get_bot", "clean_text", "encode_phone", "decode_phone"]
@@ -0,0 +1,429 @@
1
+ """
2
+ phone_act 引擎 — 关键词规则匹配 + 频控 + 手机号编码
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ import time
10
+ from datetime import datetime, timezone, timedelta
11
+
12
+
13
+ _HERE = os.path.dirname(os.path.abspath(__file__))
14
+
15
+
16
+ def _default_data_dir() -> str:
17
+ """数据文件目录,优先当前目录,fallback 到包目录"""
18
+ cwd = os.getcwd()
19
+ if os.path.isdir(cwd):
20
+ return cwd
21
+ return _HERE
22
+
23
+
24
+ def _now() -> str:
25
+ bj = timezone(timedelta(hours=8))
26
+ return datetime.now(timezone.utc).astimezone(bj).strftime("%Y-%m-%d %H:%M:%S")
27
+
28
+
29
+ # ===================================================================
30
+ # 默认规则(贷款业务)
31
+ # ===================================================================
32
+
33
+ DEFAULT_RULES = [
34
+ {
35
+ "id": 1,
36
+ "name": "联系方式",
37
+ "keywords": ["怎么联系", "联系方式", "电话", "加你", "加您", "加微信",
38
+ "联系你", "联系您", "怎么加", "加个微信", "留个电话"],
39
+ "match_mode": "any",
40
+ "reply": "幺捌伍幺叁零陆陆伍柒贰",
41
+ "enabled": True,
42
+ "priority": 20,
43
+ },
44
+ {
45
+ "id": 2,
46
+ "name": "征信不好→问爱人",
47
+ "keywords": ["征信不好", "征信不太好", "征信不行", "征信差", "征信有问题",
48
+ "征信花了", "征信黑", "征信不过", "征信没通过",
49
+ "逾期了", "有逾期", "当前逾期", "逾期过"],
50
+ "match_mode": "any",
51
+ "reply": "您爱人征信怎么样?",
52
+ "enabled": True,
53
+ "priority": 16,
54
+ },
55
+ {
56
+ "id": 3,
57
+ "name": "征信确认",
58
+ "keywords": ["征信", "逾期", "信用", "黑户", "失信", "黑名单",
59
+ "法院", "执行", "老赖"],
60
+ "match_mode": "any",
61
+ "reply": "您征信怎么样?有当前逾期吗?",
62
+ "enabled": True,
63
+ "priority": 15,
64
+ },
65
+ {
66
+ "id": 4,
67
+ "name": "要号码",
68
+ "keywords": ["然后", "接下来", "怎么操作", "要什么", "资料",
69
+ "提供什么", "需要什么", "怎么办"],
70
+ "match_mode": "any",
71
+ "reply": "您的号码是多少?",
72
+ "enabled": True,
73
+ "priority": 12,
74
+ },
75
+ {
76
+ "id": 5,
77
+ "name": "贷款咨询",
78
+ "keywords": ["贷款", "怎么贷", "怎么申请", "能贷多少", "能贷吗",
79
+ "借款", "借钱", "办贷款", "做贷款", "放款", "下款",
80
+ "利息", "利率", "额度", "怎么弄"],
81
+ "match_mode": "any",
82
+ "reply": "您今年多大了?征信怎么样?方便的话我先了解下您的情况。",
83
+ "enabled": True,
84
+ "priority": 10,
85
+ },
86
+ {
87
+ "id": 6,
88
+ "name": "打招呼/求助",
89
+ "keywords": ["你好", "在吗", "hello", "hi", "在不在", "咨询", "请问",
90
+ "帮助", "帮忙", "帮帮我", "急", "着急", "经理", "老师",
91
+ "能不能", "可以吗", "行吗"],
92
+ "match_mode": "any",
93
+ "reply": "您好!您今年多大了?",
94
+ "enabled": True,
95
+ "priority": 5,
96
+ },
97
+ {
98
+ "id": 99,
99
+ "name": "兜底",
100
+ "keywords": [],
101
+ "match_mode": "fallback",
102
+ "reply": "您好!方便说一下您今年多大了吗?我帮您看看能不能做。",
103
+ "enabled": True,
104
+ "priority": 0,
105
+ },
106
+ ]
107
+
108
+
109
+ # ===================================================================
110
+ # 工具函数
111
+ # ===================================================================
112
+
113
+ def clean_text(text: str) -> str:
114
+ """清洗客户消息:去标点、空格,提高命中率"""
115
+ text = text.strip()
116
+ text = re.sub(r"[,。!?、;:“”‘’()【】《》…—\s]+", "", text)
117
+ text = re.sub(r"[,\.!\?;:\"'\(\)\[\]{}@#$%^&*+=~`|<>/\\-]+", "", text)
118
+ return text.lower()
119
+
120
+
121
+ _DIGIT_MAP = {
122
+ "0": "零", "1": "壹", "2": "贰", "3": "叁", "4": "肆",
123
+ "5": "伍", "6": "陆", "7": "柒", "8": "捌", "9": "玖",
124
+ }
125
+ _REVERSE_MAP = {v: k for k, v in _DIGIT_MAP.items()}
126
+ _REVERSE_MAP["幺"] = "1"
127
+ _REVERSE_MAP["两"] = "2"
128
+
129
+
130
+ def encode_phone(phone: str) -> str:
131
+ """13800138000 → 壹叁捌零零壹叁捌零零零"""
132
+ return "".join(_DIGIT_MAP.get(d, d) for d in phone)
133
+
134
+
135
+ def decode_phone(encoded: str) -> str:
136
+ """幺捌伍幺叁零陆陆伍柒贰 → 18513066572"""
137
+ result = []
138
+ for ch in encoded:
139
+ d = _REVERSE_MAP.get(ch)
140
+ if d is not None:
141
+ result.append(d)
142
+ elif ch.isdigit():
143
+ result.append(ch)
144
+ return "".join(result)
145
+
146
+
147
+ # ===================================================================
148
+ # 引擎
149
+ # ===================================================================
150
+
151
+
152
+ class PhoneAct:
153
+ """关键词自动回复引擎"""
154
+
155
+ def __init__(self, rules_file: str = None, cooldown: int = 60, max_per_minute: int = 30):
156
+ self.rules_file = rules_file or os.path.join(_default_data_dir(), "rules.json")
157
+ self.cooldown = cooldown
158
+ self.max_per_minute = max_per_minute
159
+ self._reply_times: dict[str, float] = {}
160
+ self._minute_count: list[tuple[str, int]] = []
161
+
162
+ # ====== 规则管理 ======
163
+
164
+ def load_rules(self) -> list[dict]:
165
+ if not os.path.exists(self.rules_file):
166
+ self.save_rules(DEFAULT_RULES)
167
+ return [r for r in DEFAULT_RULES if r.get("enabled", True)]
168
+ try:
169
+ with open(self.rules_file, "r", encoding="utf-8") as f:
170
+ rules = json.load(f)
171
+ except (json.JSONDecodeError, IOError):
172
+ rules = DEFAULT_RULES
173
+ return sorted(
174
+ [r for r in rules if r.get("enabled", True)],
175
+ key=lambda r: r.get("priority", 0),
176
+ reverse=True,
177
+ )
178
+
179
+ def save_rules(self, rules: list[dict]) -> None:
180
+ with open(self.rules_file, "w", encoding="utf-8") as f:
181
+ json.dump(rules, f, ensure_ascii=False, indent=2)
182
+
183
+ def get_all_rules(self) -> list[dict]:
184
+ if not os.path.exists(self.rules_file):
185
+ return list(DEFAULT_RULES)
186
+ try:
187
+ with open(self.rules_file, "r", encoding="utf-8") as f:
188
+ return json.load(f)
189
+ except (json.JSONDecodeError, IOError):
190
+ return list(DEFAULT_RULES)
191
+
192
+ def add_rule(self, name: str, keywords: list[str], reply: str,
193
+ match_mode: str = "any", priority: int = 0) -> dict:
194
+ rules = self.get_all_rules()
195
+ new_id = max((r.get("id", 0) for r in rules), default=0) + 1
196
+ rule = {
197
+ "id": new_id, "name": name, "keywords": keywords,
198
+ "match_mode": match_mode, "reply": reply,
199
+ "enabled": True, "priority": priority,
200
+ }
201
+ rules.append(rule)
202
+ self.save_rules(rules)
203
+ return rule
204
+
205
+ def update_rule(self, rule_id: int, **kwargs) -> dict | None:
206
+ rules = self.get_all_rules()
207
+ for r in rules:
208
+ if r.get("id") == rule_id:
209
+ r.update(kwargs)
210
+ self.save_rules(rules)
211
+ return r
212
+ return None
213
+
214
+ def delete_rule(self, rule_id: int) -> bool:
215
+ rules = self.get_all_rules()
216
+ new_rules = [r for r in rules if r.get("id") != rule_id]
217
+ if len(new_rules) < len(rules):
218
+ self.save_rules(new_rules)
219
+ return True
220
+ return False
221
+
222
+ def toggle_rule(self, rule_id: int) -> dict | None:
223
+ rules = self.get_all_rules()
224
+ for r in rules:
225
+ if r.get("id") == rule_id:
226
+ r["enabled"] = not r.get("enabled", True)
227
+ self.save_rules(rules)
228
+ return r
229
+ return None
230
+
231
+ # ====== 消息处理 ======
232
+
233
+ def process(self, content: str, from_user: str = "",
234
+ conversation_id: str = "") -> dict:
235
+ content = content.strip()
236
+ if not content:
237
+ return {"matched": False, "reply": "", "reason": "消息为空"}
238
+
239
+ rules = self.load_rules()
240
+ if not rules:
241
+ return {"matched": False, "reply": "", "reason": "没有启用的规则"}
242
+
243
+ cleaned = clean_text(content)
244
+ cid = conversation_id or from_user or "default"
245
+
246
+ for rule in rules:
247
+ mode = rule.get("match_mode", "any")
248
+ reply_text = rule.get("reply", "").strip()
249
+
250
+ if mode == "fallback":
251
+ if not reply_text:
252
+ continue
253
+ if not self._check_rate(cid):
254
+ return {"matched": True, "reply": "", "reason": "频控冷却中",
255
+ "keyword": "(兜底)", "rule_id": rule["id"]}
256
+ self._reply_times[cid] = time.time()
257
+ self._log(content, reply_text, from_user, cid, "(兜底)", rule["id"])
258
+ return {
259
+ "matched": True, "reply": reply_text,
260
+ "keyword": "(兜底)", "rule_id": rule["id"],
261
+ "rule_name": rule.get("name", ""), "match_mode": "fallback",
262
+ }
263
+
264
+ keywords = rule.get("keywords", [])
265
+ if not keywords or not reply_text:
266
+ continue
267
+
268
+ kw = self._match(keywords, cleaned, mode, content)
269
+ if not kw:
270
+ continue
271
+
272
+ if not self._check_rate(cid):
273
+ return {
274
+ "matched": True, "reply": "",
275
+ "reason": "冷却中", "keyword": kw, "rule_id": rule["id"],
276
+ }
277
+
278
+ self._reply_times[cid] = time.time()
279
+ self._log(content, reply_text, from_user, cid, kw, rule["id"])
280
+ return {
281
+ "matched": True, "reply": reply_text,
282
+ "keyword": kw, "rule_id": rule["id"],
283
+ "rule_name": rule.get("name", ""), "match_mode": mode,
284
+ }
285
+
286
+ self._log(content, "", from_user, cid, "", None)
287
+ return {"matched": False, "reply": "", "reason": "无匹配规则"}
288
+
289
+ def process_batch(self, messages: list[dict]) -> list[dict]:
290
+ return [self.process(**m) if isinstance(m, dict) else
291
+ self.process(str(m)) for m in messages]
292
+
293
+ # ====== 内部 ======
294
+
295
+ @staticmethod
296
+ def _match(keywords: list[str], cleaned: str, mode: str,
297
+ original: str = "") -> str | None:
298
+ if mode == "exact":
299
+ for kw in keywords:
300
+ if original.strip() == kw:
301
+ return kw
302
+ elif mode == "all":
303
+ hits = [kw for kw in keywords if kw in cleaned]
304
+ return ", ".join(hits) if len(hits) == len(keywords) else None
305
+ else:
306
+ for kw in keywords:
307
+ if kw in cleaned:
308
+ return kw
309
+ return None
310
+
311
+ def _check_rate(self, cid: str) -> bool:
312
+ if cid in self._reply_times:
313
+ if time.time() - self._reply_times[cid] < self.cooldown:
314
+ return False
315
+ return self._check_global_rate()
316
+
317
+ def _check_global_rate(self) -> bool:
318
+ now = time.time()
319
+ minute_key = time.strftime("%Y%m%d%H%M", time.localtime(now))
320
+ self._minute_count = [
321
+ (k, c) for k, c in self._minute_count
322
+ if now - self._parse_minute_ts(k) < 120
323
+ ]
324
+ current = sum(c for k, c in self._minute_count if k == minute_key)
325
+ if current >= self.max_per_minute:
326
+ return False
327
+ for i, (k, c) in enumerate(self._minute_count):
328
+ if k == minute_key:
329
+ self._minute_count[i] = (k, c + 1)
330
+ return True
331
+ self._minute_count.append((minute_key, 1))
332
+ return True
333
+
334
+ @staticmethod
335
+ def _parse_minute_ts(key: str) -> float:
336
+ try:
337
+ return time.mktime(time.strptime(key, "%Y%m%d%H%M"))
338
+ except (ValueError, OverflowError):
339
+ return 0
340
+
341
+ def _log(self, content: str, reply: str, from_user: str,
342
+ conversation_id: str, keyword: str, rule_id: int | None) -> None:
343
+ log_file = os.path.join(_default_data_dir(), "message_logs.json")
344
+ entry = {
345
+ "from_user": from_user,
346
+ "conversation_id": conversation_id,
347
+ "content": content[:200],
348
+ "reply": reply[:200],
349
+ "keyword": keyword,
350
+ "rule_id": rule_id,
351
+ "matched": bool(reply),
352
+ "time": _now(),
353
+ }
354
+ try:
355
+ with open(log_file, "a", encoding="utf-8") as f:
356
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
357
+ except IOError:
358
+ pass
359
+
360
+
361
+ # ===================================================================
362
+ # 全局单例
363
+ # ===================================================================
364
+
365
+ _bot: PhoneAct | None = None
366
+
367
+
368
+ def get_bot() -> PhoneAct:
369
+ global _bot
370
+ if _bot is None:
371
+ _bot = PhoneAct()
372
+ return _bot
373
+
374
+
375
+ def process(content: str, from_user: str = "", conversation_id: str = "") -> dict:
376
+ """快捷函数 — workbuddy 里直接调这个"""
377
+ return get_bot().process(content, from_user=from_user, conversation_id=conversation_id)
378
+
379
+
380
+ # ===================================================================
381
+ # CLI
382
+ # ===================================================================
383
+
384
+ def main():
385
+ """CLI 入口: phone-act '消息'"""
386
+ bot = PhoneAct()
387
+
388
+ if len(sys.argv) > 1:
389
+ arg = sys.argv[1]
390
+ if arg in ("-h", "--help"):
391
+ print("phone-act — 微信视频号私信自动回复")
392
+ print("用法:")
393
+ print(" phone-act '消息内容' 匹配消息")
394
+ print(" echo '消息' | phone-act 管道输入")
395
+ print(" phone-act --rules 查看所有规则")
396
+ print(" phone-act --init 生成默认规则文件 rules.json")
397
+ print(" phone-act --encode 13800138000 手机号编码")
398
+ sys.exit(0)
399
+ if arg == "--rules":
400
+ rules = bot.get_all_rules()
401
+ print(json.dumps(rules, ensure_ascii=False, indent=2))
402
+ sys.exit(0)
403
+ if arg == "--init":
404
+ bot.save_rules(DEFAULT_RULES)
405
+ print(f"已生成 rules.json → {bot.rules_file}")
406
+ sys.exit(0)
407
+ if arg == "--encode":
408
+ phone = sys.argv[2] if len(sys.argv) > 2 else ""
409
+ if phone.isdigit() and len(phone) == 11:
410
+ print(f"原始: {phone}")
411
+ print(f"编码: {encode_phone(phone)}")
412
+ else:
413
+ print("请输入11位手机号")
414
+ sys.exit(0)
415
+ if arg == "--decode":
416
+ encoded = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else ""
417
+ if encoded:
418
+ print(f"解码: {decode_phone(encoded)}")
419
+ sys.exit(0)
420
+
421
+ msg = " ".join(sys.argv[1:])
422
+ else:
423
+ msg = sys.stdin.read().strip()
424
+ if not msg:
425
+ print("用法: phone-act '消息内容'")
426
+ sys.exit(0)
427
+
428
+ result = bot.process(msg)
429
+ print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: phone-act-x
3
+ Version: 1.0.0
4
+ Summary: 微信视频号私信自动回复引擎 — 贷款业务场景关键词匹配
5
+ Author: phone-act
6
+ License: MIT
7
+ Requires-Python: >=3.10
@@ -0,0 +1,9 @@
1
+ pyproject.toml
2
+ phone_act/__init__.py
3
+ phone_act/engine.py
4
+ phone_act_x.egg-info/PKG-INFO
5
+ phone_act_x.egg-info/SOURCES.txt
6
+ phone_act_x.egg-info/dependency_links.txt
7
+ phone_act_x.egg-info/entry_points.txt
8
+ phone_act_x.egg-info/top_level.txt
9
+ tests/test_phone_act.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ phone-act = phone_act.engine:main
@@ -0,0 +1 @@
1
+ phone_act
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "phone-act-x"
7
+ version = "1.0.0"
8
+ description = "微信视频号私信自动回复引擎 — 贷款业务场景关键词匹配"
9
+ requires-python = ">=3.10"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "phone-act"}]
12
+
13
+ dependencies = []
14
+
15
+ [project.scripts]
16
+ phone-act = "phone_act.engine:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ include = ["phone_act*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,139 @@
1
+ """
2
+ phone_act 测试
3
+ 运行: cd /Users/yuxia/Desktop/phone-act && python3 -m pytest tests/ -v
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+ import tempfile
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+ from phone_act import PhoneAct, process, clean_text, encode_phone, decode_phone
14
+
15
+ _passed = 0
16
+ _failed = 0
17
+
18
+
19
+ def ok(label: str, result: dict, **expected):
20
+ global _passed, _failed
21
+ fail = False
22
+ for key, val in expected.items():
23
+ if result.get(key) != val:
24
+ print(f" ✗ {label}: {key}={result.get(key)!r}, 期望={val!r}")
25
+ fail = True
26
+ if not fail:
27
+ _passed += 1
28
+ print(f" ✓ {label}")
29
+ else:
30
+ _failed += 1
31
+
32
+
33
+ def eq(label: str, got, expected):
34
+ global _passed, _failed
35
+ if got == expected:
36
+ _passed += 1
37
+ print(f" ✓ {label}")
38
+ else:
39
+ _failed += 1
40
+ print(f" ✗ {label}: got={got!r}, 期望={expected!r}")
41
+
42
+
43
+ # ============================================================
44
+ print("【真实客户消息】")
45
+ bot = PhoneAct()
46
+
47
+ ok("征信没问题 → 征信确认",
48
+ bot.process("征信没问题。贷款不出", conversation_id="t1"), matched=True, rule_id=3)
49
+
50
+ ok("征信不好 → 问爱人",
51
+ bot.process("我征信不好能贷吗", conversation_id="t2"), matched=True, rule_id=2)
52
+
53
+ ok("有逾期 → 问爱人",
54
+ bot.process("我有逾期怎么办", conversation_id="t3"), matched=True, rule_id=2)
55
+
56
+ ok("怎么贷款的 → 贷款咨询",
57
+ bot.process("你们怎么贷款的", conversation_id="t4"), matched=True, rule_id=5)
58
+
59
+ ok("怎么联系 → 联系方式",
60
+ bot.process("怎么联系你", conversation_id="t5"), matched=True, rule_id=1)
61
+
62
+ ok("你好 → 打招呼",
63
+ bot.process("你好", conversation_id="t6"), matched=True, rule_id=6)
64
+
65
+ ok("黑户 → 征信确认",
66
+ bot.process("我黑户能贷吗", conversation_id="t7"), matched=True, rule_id=3)
67
+
68
+ ok("帮帮我 → 打招呼",
69
+ bot.process("帮帮我急用钱", conversation_id="t8"), matched=True, rule_id=6)
70
+
71
+ ok("然后怎么操作 → 要号码",
72
+ bot.process("然后怎么操作", conversation_id="t9"), matched=True, rule_id=4)
73
+
74
+ ok("需要资料 → 要号码",
75
+ bot.process("需要什么资料", conversation_id="t10"), matched=True, rule_id=4)
76
+
77
+ ok("留电话 → 联系方式",
78
+ bot.process("留个电话", conversation_id="t11"), matched=True, rule_id=1)
79
+
80
+ ok("兜底",
81
+ bot.process("今天吃了没", conversation_id="t12"), matched=True, rule_id=99)
82
+
83
+ # ============================================================
84
+ print("\n【频控】")
85
+ bot2 = PhoneAct(cooldown=999)
86
+ r1 = bot2.process("你好", conversation_id="cool1")
87
+ ok("首次回复", r1, matched=True)
88
+ r2 = bot2.process("你好", conversation_id="cool1")
89
+ ok("冷却中", r2, matched=True, reply="")
90
+ r3 = bot2.process("你好", conversation_id="cool2")
91
+ ok("其他用户不受影响", r3, matched=True)
92
+
93
+ # ============================================================
94
+ print("\n【规则管理】")
95
+
96
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
97
+ json.dump([], f)
98
+ rf = f.name
99
+
100
+ bot3 = PhoneAct(rules_file=rf)
101
+ r = bot3.add_rule("测试", ["测试词"], "测试回复", priority=10)
102
+ ok("添加", r, name="测试")
103
+ ok("匹配", bot3.process("测试词来了", conversation_id="m1"), matched=True, reply="测试回复")
104
+
105
+ bot3.toggle_rule(r["id"])
106
+ ok("禁用后不匹配", bot3.process("测试词", conversation_id="m2"), matched=False)
107
+
108
+ bot3.toggle_rule(r["id"])
109
+ ok("重新启用", bot3.process("测试词", conversation_id="m3"), matched=True)
110
+
111
+ bot3.delete_rule(r["id"])
112
+ ok("删除", bot3.process("测试词", conversation_id="m4"), matched=False)
113
+
114
+ r2 = bot3.add_rule("更新测试", ["旧词"], "旧回复", priority=10)
115
+ bot3.update_rule(r2["id"], reply="已更新", keywords=["新关键词"])
116
+ ok("更新后", bot3.process("这个新关键词来了", conversation_id="m5"), matched=True, reply="已更新")
117
+
118
+ os.unlink(rf)
119
+
120
+ # ============================================================
121
+ print("\n【文本清洗】")
122
+ eq("中文标点", clean_text("你好,在吗?"), "你好在吗")
123
+ eq("混合标点", clean_text("征信没问题。 贷款不出!"), "征信没问题贷款不出")
124
+
125
+ # ============================================================
126
+ print("\n【手机号编码】")
127
+ eq("编码", encode_phone("18513066572"), "壹捌伍壹叁零陆陆伍柒贰")
128
+ eq("解码", decode_phone("幺捌伍幺叁零陆陆伍柒贰"), "18513066572")
129
+
130
+ # ============================================================
131
+ print("\n【快捷函数】")
132
+ ok("process()", process("你好", from_user="test", conversation_id="s1"), matched=True)
133
+
134
+ # ============================================================
135
+ print(f"\n{'='*40}")
136
+ print(f" 通过: {_passed} 失败: {_failed}")
137
+ print(f"{'='*40}")
138
+
139
+ sys.exit(0 if _failed == 0 else 1)