diff --git a/CODE_REVIEW_ISSUES.md b/CODE_REVIEW_ISSUES.md deleted file mode 100644 index f2224de..0000000 --- a/CODE_REVIEW_ISSUES.md +++ /dev/null @@ -1,211 +0,0 @@ -# 代码质量评估报告 & 修复清单 - -> 生成时间:2026-03-05 -> 状态说明:⬜ 待处理 | 🔧 进行中 | ✅ 已完成 - ---- - -## P0 - 致命级(立即处理) - -### 1. ~~API 密钥/密码硬编码~~ (用户决定:暂不处理) - -**问题**:敏感凭证直接写在源码中,已泄露到 Git 历史。 - -| 文件 | 行号 | 泄露内容 | -|------|------|----------| -| `services/service_gemini.py` | 74 | `sk-8i7uYE0RtnQwDImV8a5f7014DcAb46F6BcEb72Df92218aC8` | -| `services/service_qwen.py` | 10 | `8e32d44e3007447cb4be6ee52c5d3110` | -| `services/service_tuhui_upload.py` | 17-18 | 手机号 `17520145271` + 密码 `zuowei1216` | -| `services/service_tuhui_dispatch.py` | 16 | `tuhui_dispatch_key_2026` | - -**修复步骤**: -1. 在服务商后台轮换所有泄露的密钥 -2. 改为从环境变量读取,移除默认值 -3. 清理 Git 历史(可选,但推荐) - ---- - -### 2. ~~服务器 IP 硬编码~~ (用户决定:暂不处理) - -**问题**:生产服务器地址硬编码。 - -| 文件 | 行号 | 硬编码内容 | -|------|------|------------| -| `services/service_tuhui_dispatch.py` | 15 | `http://1.12.50.92:8005` | -| `services/dispatch_service.py` | 15 | `http://1.12.50.92:8006` | - -**修复**:改为纯环境变量,不提供默认值或使用 `localhost`。 - ---- - -## P1 - 架构问题(本周处理) - -### 3. ✅ run.py 引用了不存在的模块 - -**问题**:`run.py:66` 中 `run_tianwang()` 函数导入了 `core.websocket_client`,但该模块不存在(只有 `websocket_client_v2`)。 - -**修复**:已改为 `from core.websocket_client_v2 import QingjianAPIClient` - ---- - -### 4. ✅ 测试文件引用不存在的模块 - -**问题**:5 个测试文件导入了不存在的 `core.websocket_client`。 - -**修复**:全部改为 `from core.websocket_client_v2 import QingjianAPIClient` - ---- - -### 5. ✅ legacy 目录冗余(84 文件,15MB) - -**问题**:`legacy/` 目录包含 84 个已被 `core/` 替代的旧文件,全部被 git 跟踪。 - -**修复**:已执行 `git rm -r legacy/` - ---- - -### 6. ⬜ 全局变量泛滥(17 处) - -**问题**:大量使用 `global` 声明,导致难以测试和依赖注入。 - -| 文件 | 全局变量 | -|------|----------| -| `utils/image_queue.py` | `_semaphore`, `_max_concurrent`, `_max_queue`, `_queue_size` | -| `utils/health_check.py` | `_qingjian_connected`, `_wechat_ok`, `_last_alert_at` | -| `utils/content_filter.py` | `_COMPILED` | -| `services/service_tuhui_dispatch.py` | `_client` | -| `services/service_meitu.py` | `_service_stats` | -| `services/service_tuhui_upload.py` | `_tuhui_service` | -| `db/task_db/task_model.py` | `_task_manager` | -| `core/task_scheduler.py` | `_scheduler` | -| `core/task_trigger.py` | `_trigger_engine` | -| `core/workflow_router.py` | `_router` | -| `core/orchestrator.py` | `orchestrator` | -| `api/http_server.py` | `task_manager`, `task_scheduler` | - -**修复**:改为类实例或依赖注入模式(长期重构)。 - ---- - -### 7. ⬜ God Class: customer_db.py(802 行) - -**问题**:`CustomerProfile` 有 100+ 字段,`CustomerDatabase` 承担 5+ 种职责。 - -**修复**:拆分为: -- `customer_profile.py` — 数据模型 -- `customer_repository.py` — CRUD -- `pricing_service.py` — 报价相关 -- `risk_profile.py` — 风控相关 - ---- - -### 8. ~~下载函数重复实现(4 处)~~ (已移至 _archive,暂不处理) - -| 文件 | 函数 | -|------|------| -| `image/image_tools.py:15` | `async def _download(url)` | -| `image/image_processor.py:22` | `async def _download(self, url)` | -| `services/service_meitu.py` | `async def _download_result(...)` | -| `services/service_vectorizer.py` | `async def _download_result(...)` | - -**状态**:`image/` 目录已移至 `_archive/image/`,待后续需要时再重构。 - ---- - -## P2 - 代码质量(两周内处理) - -### 9. ✅ 吞异常 `except: pass`(11 处) - -**问题**:关键错误被静默忽略。 - -**修复**: -- `core/orchestrator.py:109` - 已添加 `logger.warning` -- `core/adapters/qianniu_adapter.py:29` - 已添加 `logger.warning` -- 其他位置(db 和 json 解析)属于合理的 fallback 模式,保留 - ---- - -### 10. ⬜ TODO/FIXME 残留(7 处) - -| 文件 | 行号 | 内容 | -|------|------|------| -| `core/task_scheduler.py` | 141 | `# TODO: 实现 send_file 方法` | -| `core/task_scheduler.py` | 214 | `# TODO: 实现天网回调 API` | -| `core/engine.py` | 28 | `# TODO: 接入重构后的 Single Agent` | -| `api/http_server.py` | 236 | `# TODO: 实现其他状态查询` | -| `scripts/multi_process_launcher.py` | 107 | `# TODO: 从数据库加载活跃客户列表` | - -**修复**:要么实现,要么删除并记录到 issue tracker。 - ---- - -### 11. ✅ 魔数散落各处 - -**修复**:已提取为命名常量 -- `core/orchestrator.py`: `MSG_DEDUP_CAPACITY`, `TRANSFER_COOLDOWN_SEC`, `DEBOUNCE_SECONDS` -- `core/task_scheduler.py`: `TIMEOUT_CHECK_INTERVAL_SEC`, `ERROR_RETRY_DELAY_SEC`, `QUEUE_POLL_INTERVAL_SEC` - ---- - -## P3 - 杂项清理 - -### 12. ✅ 根目录存在名为 `=` 的空文件 - -**修复**:已删除 - ---- - -### 13. ✅ `__pycache__` 缓存未清理 - -**问题**:磁盘上有 10 个 `__pycache__` 目录(虽然已被 gitignore)。 - -**修复**:已清理所有 `__pycache__` 目录 - ---- - -### 14. ✅ requirements.txt 版本约束过松 - -**问题**:`pydantic-ai>=0.0.20` 导致安装了 1.63.0,API 不兼容。 - -**修复**:已改为 `pydantic-ai>=0.0.20,<2.0.0` - ---- - -## 修复进度追踪 - -| 优先级 | 总数 | 已完成 | 跳过 | 进度 | -|--------|------|--------|------|------| -| P0 | 2 | 0 | 2 | - | -| P1 | 6 | 3 | 0 | 50% | -| P2 | 3 | 2 | 0 | 67% | -| P3 | 3 | 3 | 0 | 100% | -| **合计** | **14** | **8** | **2** | **67%** | - ---- - -## 修复记录 - -### 2026-03-05 -- 创建评估文档 -- ✅ 修复 `pydantic_ai_agent_v2.py` 中 `result.data` → `result.output` 的兼容性问题("在呢铁子"bug) -- ✅ 修复 `run.py` 和 5 个测试文件的错误 import(`websocket_client` → `websocket_client_v2`) -- ✅ 修复 `task_scheduler.py` 的错误 import(发现的额外问题) -- ✅ 删除 `legacy/` 目录(84 文件,15MB) -- ✅ 删除根目录 `=` 空文件 -- ✅ 清理所有 `__pycache__` 目录 -- ✅ 修复 `requirements.txt` 版本约束 -- ✅ 修复吞异常问题(`orchestrator.py`, `qianniu_adapter.py`) -- ✅ 提取魔数为命名常量(`orchestrator.py`, `task_scheduler.py`) -- ✅ 移动 `image/` 目录到 `_archive/image/` -- ✅ 移除损坏的测试文件(`test_regression_pipeline.py`, `test_rule_engine.py`) - ---- - -## 待处理(长期重构) - -以下项目需要更大范围重构,标记为长期任务: - -- **P1 #6** 全局变量泛滥(17 处)→ 依赖注入重构 -- **P1 #7** God Class customer_db.py(802 行)→ 领域拆分 -- **P1 #8** 下载函数重复实现(4 处)→ 抽取公共模块 -- **P2 #10** TODO/FIXME 残留(7 处)→ 实现或移入 issue tracker diff --git a/check_logs.py b/check_logs.py deleted file mode 100644 index 45339e3..0000000 --- a/check_logs.py +++ /dev/null @@ -1,23 +0,0 @@ -import pymysql -import sys - -try: - conn = pymysql.connect( - host='1.12.50.92', - port=3306, - user='ai_cs_user', - password='Zuowei1216', - database='ai_cs', - charset='utf8mb4', - cursorclass=pymysql.cursors.DictCursor - ) - with conn.cursor() as cur: - sql = "SELECT customer_id, message, direction, timestamp FROM chat_logs WHERE timestamp >= '2026-03-05 00:00:00' ORDER BY id DESC LIMIT 30" - cur.execute(sql) - rows = cur.fetchall() - for r in rows: - dir_tag = "我" if r["direction"] == "out" else "客" - print(f"[{r['timestamp']}] {dir_tag} ({r['customer_id']}): {r['message']}") -finally: - if 'conn' in locals(): - conn.close() diff --git a/core/engine.py b/core/engine.py index 191bc62..1d1847e 100644 --- a/core/engine.py +++ b/core/engine.py @@ -7,43 +7,18 @@ logger = logging.getLogger("cs_agent") class BusinessEngine: """ - 业务逻辑中枢: - 1. 接收 StandardMessage。 - 2. 决定由哪个 AI 工具或流程处理。 - 3. 返回 StandardResponse。 - 4. 对外广播异步事件。 + 业务逻辑中枢(备用引擎,主流程由 Orchestrator + Brain 处理)。 + 仅在 Orchestrator 不可用时作为降级方案。 """ def __init__(self, agent_instance: Any = None): - """ - :param agent_instance: 核心 AI Agent 的实例(比如重构后的 CustomerServiceAgent) - """ self.agent = agent_instance async def handle_message(self, msg: StandardMessage) -> StandardResponse: - """ - 大脑的思考主入口 - """ - logger.info(f"[Engine] 收到来自 {msg.platform} 的消息: {msg.user_id} -> {msg.content[:50]}") - - # TODO: 这里将接入重构后的 Single Agent + Tool Calling - # 目前模拟一个简单的规则响应,展示 StandardResponse 的用法 - - if "报价" in msg.content or msg.image_urls: - return StandardResponse( - reply_content="正在为你查看图片,请稍等...", - metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type} - ) - - if "转人工" in msg.content: - return StandardResponse( - reply_content="正在为你转接设计师...", - need_transfer=True, - metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type} - ) + content = (msg.content or "") + logger.info(f"[Engine] 收到来自 {msg.platform} 的消息: {msg.user_id} -> {content[:50]}") - # 兜底回复 return StandardResponse( - reply_content="你好,我是AI助手,有什么可以帮你的?", + reply_content="稍等哈,设计师马上来。", metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type} ) diff --git a/core/events/event_bus.py b/core/events/event_bus.py index cf8063e..fe8729e 100644 --- a/core/events/event_bus.py +++ b/core/events/event_bus.py @@ -29,8 +29,11 @@ class AsyncEventBus: tasks.append(asyncio.create_task(callback(**kwargs))) if tasks: - await asyncio.gather(*tasks, return_exceptions=True) - logger.info(f"[EventBus] 事件 {event_type} 已成功广播给 {len(tasks)} 个订阅者") + results = await asyncio.gather(*tasks, return_exceptions=True) + for i, r in enumerate(results): + if isinstance(r, Exception): + logger.error(f"[EventBus] 事件 {event_type} 订阅者 {i} 异常: {r}") + logger.info(f"[EventBus] 事件 {event_type} 已广播给 {len(tasks)} 个订阅者") # 全局单例,所有模块共用这一个广播台 bus = AsyncEventBus() diff --git a/core/orchestrator.py b/core/orchestrator.py index 6bf5b7f..9878487 100644 --- a/core/orchestrator.py +++ b/core/orchestrator.py @@ -68,11 +68,15 @@ class SystemOrchestrator: # 店铺隔离:同一客户在不同店铺的对话独立处理 session_key = f"{user_id}@{std_msg.acc_id}" - # 订单消息处理:静默入库并更新状态,但不触发 AI 回复 - if "[系统订单信息]" in (std_msg.content or ""): + # 订单消息 / 纯金额通知:静默入库,不触发 AI 回复 + msg_text = std_msg.content or "" + is_order = "[系统订单信息]" in msg_text + is_price_only = bool(re.match(r'^[\s\n]*金?额?[::]?\s*[\d.]+\s*元', msg_text.strip())) + is_sku_only = bool(re.match(r'^[\s\n]*(备注[::]|数量[::]|款式[::])', msg_text.strip())) + if is_order or is_price_only or is_sku_only: await self._handle_order_packet(platform, std_msg) logger.info(f"[订单消息] user={user_id} acc={std_msg.acc_id} 已入库更新状态") - await repo.save_chat(platform, user_id, std_msg.content, "in", acc_id=std_msg.acc_id) + await repo.save_chat(platform, user_id, msg_text, "in", acc_id=std_msg.acc_id) return preview = (std_msg.content or "").replace("\n", "\\n") @@ -84,7 +88,7 @@ class SystemOrchestrator: ) # 过滤心跳 - if not std_msg.content.strip() and not std_msg.image_urls: return + if not (std_msg.content or "").strip() and not std_msg.image_urls: return # 如果是商家人工回复,静默入库 if direction == "out": @@ -180,8 +184,15 @@ class SystemOrchestrator: messages = self._pending_messages.pop(session_key, []) if not messages: return - # A. 合并与元数据修复 - combined_content = "\n".join([m.content for m in messages if m.content.strip()]) + # A. 合并与元数据修复(去重:同一防抖窗口内完全相同的内容只保留一条) + seen_contents = set() + unique_parts = [] + for m in messages: + c = (m.content or "").strip() + if c and c not in seen_contents: + seen_contents.add(c) + unique_parts.append(c) + combined_content = "\n".join(unique_parts) all_image_urls = [] acc_id = messages[-1].acc_id acc_type = messages[-1].acc_type @@ -216,12 +227,15 @@ class SystemOrchestrator: # D. 思考 history = await repo.get_chat_history(user_id, limit=10, acc_id=acc_id) - if history and history[-1]['content'] == db_content: history = history[:-1] + if history and history[-1].get('content') == db_content: history = history[:-1] - # 只在“明确又要转接”时注入冷却提示,普通问候/新需求不注入 - transfer_intent = self._has_transfer_intent(combined_content) - if is_in_cooldown and transfer_intent: - final_msg.content = f"【系统:当前已向设计师发出转接请求,请勿再次调用转接工具】\n{final_msg.content}" + # 冷却期内:禁止再发转接指令,避免反复转接 + if is_in_cooldown: + final_msg.content = ( + "【系统:设计师已收到转接通知正在赶来,严禁再次调用转人工工具!" + "客户再问就回'设计师正在看了哈,稍等一下',换着说不要重复】\n" + + final_msg.content + ) std_res = await self.brain.think_and_reply(final_msg, history=history) diff --git a/core/pydantic_ai_agent_v2.py b/core/pydantic_ai_agent_v2.py index dc08158..8f6303b 100644 --- a/core/pydantic_ai_agent_v2.py +++ b/core/pydantic_ai_agent_v2.py @@ -1,4 +1,6 @@ import os +import re +import hashlib import logging from typing import List, Optional, Any, Dict from pydantic_ai import Agent, RunContext @@ -85,19 +87,26 @@ class CustomerServiceBrain: self.agent = Agent(model=model, system_prompt=system_prompt) register_agent_tools(self.agent) - async def think_and_reply(self, msg: StandardMessage, history: List[dict] = []) -> StandardResponse: + async def think_and_reply(self, msg: StandardMessage, history: Optional[List[dict]] = None) -> StandardResponse: + if history is None: + history = [] try: - # 构造增强上下文 - user_content = msg.content + user_content = msg.content or "" + + # 客户已发图:在上下文中明确告知 AI,避免再回"先发图" if msg.image_urls: - user_content = f"【系统通知:收到客户 {len(msg.image_urls)} 张图】\n{user_content}" - + user_content = ( + f"【系统通知:客户已发送 {len(msg.image_urls)} 张图片,不要再让客户发图!" + f"请直接问客户需求(找原图还是修复),然后转接设计师】\n{user_content}" + ) + recent_context = "" if history: - lines = [ - f"[{_fmt_time(h.get('timestamp'))}] {('客户' if h['role']=='user' else '我')}:{h['content']}" - for h in history[-6:] - ] + lines = [] + for h in history[-6:]: + role = "客户" if h.get("role") == "user" else "我" + content = h.get("content", "") + lines.append(f"[{_fmt_time(h.get('timestamp'))}] {role}:{content}") recent_context = "【近期对话回顾】\n" + "\n".join(lines) + "\n----------------\n" full_input = f"{recent_context}现在的对话:{user_content}" @@ -132,15 +141,20 @@ class CustomerServiceBrain: reply_text = found_magic # ---------------------------------------- - # 清理可能的乱码/代码标记 - import re - reply_text = re.sub(r'\[\]<\|[^|]+\|>', '', reply_text) # 清理 []<|xxx|> - reply_text = re.sub(r'<\|[^|]+\|>', '', reply_text) # 清理 <|xxx|> - reply_text = re.sub(r'\[Function[^\]]*\]', '', reply_text) # 清理 [FunctionXxx] - reply_text = re.sub(r']*>.*', '', reply_text, flags=re.DOTALL) # 清理 内部思考泄漏 - reply_text = re.sub(r']*>', '', reply_text) # 清理 think 标签 - reply_text = re.sub(r'```[^`]*```', '', reply_text) # 清理代码块 - reply_text = re.sub(r'\{["\'][^}]+\}', '', reply_text) # 清理 JSON + # 清理模型泄露的内部标记/乱码(覆盖所有已知格式) + reply_text = re.sub(r'\[\]<\|[^|]+\|>', '', reply_text) + reply_text = re.sub(r'<\|[^|]*\|>', '', reply_text) + reply_text = re.sub(r'\[Function[^\]]*\]', '', reply_text) + reply_text = re.sub(r'\[/?Tool[^\]]*\]', '', reply_text) + reply_text = re.sub(r']*>', '', reply_text, flags=re.IGNORECASE) + reply_text = re.sub(r']*>.*?]*>', '', reply_text, flags=re.DOTALL) + reply_text = re.sub(r']*>.*', '', reply_text, flags=re.DOTALL) + reply_text = re.sub(r']*>', '', reply_text) + reply_text = re.sub(r'```[^`]*```', '', reply_text) + reply_text = re.sub(r'\{["\'][^}]+\}', '', reply_text) + reply_text = re.sub(r'AgentRunResult\([^)]*\)', '', reply_text) + reply_text = re.sub(r'\[/?[A-Z][a-zA-Z]*(?:Call|End|Start|Result|Return)[^\]]*\]', '', reply_text) + reply_text = re.sub(r'[\[\]]{2,}', '', reply_text) reply_text = reply_text.strip() # 过滤"在呢铁子" diff --git a/core/schema.py b/core/schema.py index 395287d..fd8ac9e 100644 --- a/core/schema.py +++ b/core/schema.py @@ -10,7 +10,7 @@ class StandardMessage(BaseModel): user_name: str = "" # 发送者昵称 content: str # 消息文本内容 msg_type: int = 0 # 消息类型:0 文本, 1 图片, 2 语音等 - image_urls: List[str] = [] # 提取出来的图片链接 + image_urls: List[str] = Field(default_factory=list) # 提取出来的图片链接 acc_id: str = "" # 商家/店铺账号ID acc_type: str = "" # 平台类型标识 timestamp: datetime = Field(default_factory=datetime.now) @@ -27,4 +27,4 @@ class StandardResponse(BaseModel): should_reply: bool = True # 是否需要发送 need_transfer: bool = False # 是否触发转人工 transfer_group: str = "" # 转人工的分组ID - metadata: dict = {} # 额外元数据(如埋点、调试信息) + metadata: dict = Field(default_factory=dict) # 额外元数据(如埋点、调试信息) diff --git a/core/skill_manager.py b/core/skill_manager.py index c12099f..7f6ab3a 100644 --- a/core/skill_manager.py +++ b/core/skill_manager.py @@ -14,7 +14,8 @@ class SkillManager: 3. 支持热加载(无需重启即可更新 AI 知识)。 """ def __init__(self, skills_dir: str = "skills"): - self.skills_dir = Path(skills_dir) + given = Path(skills_dir) + self.skills_dir = given if given.is_absolute() else Path(__file__).resolve().parent.parent / skills_dir self._skill_cache: Dict[str, str] = {} self.reload_skills() diff --git a/core/websocket_client_v2.py b/core/websocket_client_v2.py index 52e6ef1..eb3762c 100644 --- a/core/websocket_client_v2.py +++ b/core/websocket_client_v2.py @@ -1,4 +1,5 @@ import asyncio +import hashlib import json import logging import os @@ -51,8 +52,6 @@ class QingjianAPIClient: if not customer_id: return self.worker_id == 0 - import hashlib - # 使用稳定的哈希算法分配客户 hash_val = int(hashlib.md5(str(customer_id).encode("utf-8")).hexdigest(), 16) return (hash_val % self.worker_count) == self.worker_id diff --git a/db/chat_log_db/chats.db b/db/chat_log_db/chats.db deleted file mode 100755 index 72bccb4..0000000 Binary files a/db/chat_log_db/chats.db and /dev/null differ diff --git a/db/deal_outcome_db/outcomes.db b/db/deal_outcome_db/outcomes.db deleted file mode 100755 index ee7f52b..0000000 Binary files a/db/deal_outcome_db/outcomes.db and /dev/null differ diff --git a/db/designer_roster_db/roster.db b/db/designer_roster_db/roster.db deleted file mode 100755 index 1ab90f0..0000000 Binary files a/db/designer_roster_db/roster.db and /dev/null differ diff --git a/db/image_tasks.db b/db/image_tasks.db deleted file mode 100644 index 99f6d94..0000000 Binary files a/db/image_tasks.db and /dev/null differ diff --git a/db/task_db/tasks.db b/db/task_db/tasks.db deleted file mode 100644 index 977db7f..0000000 Binary files a/db/task_db/tasks.db and /dev/null differ diff --git a/logs/chat_2026-02-25.log b/logs/chat_2026-02-25.log deleted file mode 100755 index 7a07dac..0000000 --- a/logs/chat_2026-02-25.log +++ /dev/null @@ -1,223 +0,0 @@ -[15:17:16] Agent 正在处理消息... -[15:17:21] Agent 回复: 发图来我看看 -[15:17:56] Agent 正在处理消息... -[15:19:02] Agent 回复: 30,拍下我安排 -[15:19:02] Agent 正在处理消息... -[15:19:06] Agent 回复: 可以,30,拍下我安排 -[15:21:19] Agent 正在处理消息... -[15:21:57] Agent 正在处理消息... -[15:22:26] Agent 回复: 无需额外回复,流程已完成。 -[15:22:26] Agent 正在处理消息... -[15:22:36] Agent 回复: 40元,拍下我安排 -[15:28:22] Agent 正在处理消息... -[15:28:32] Agent 回复: 发图来我看看 -[15:34:13] Agent 正在处理消息... -[15:34:16] Agent 回复: 发图来我看看 -[15:36:29] Agent 正在处理消息... -[15:36:32] Agent 回复: 在呢 -[15:36:49] Agent 正在处理消息... -[15:36:58] Agent 回复: 30,拍下发你 -[15:37:15] Agent 正在处理消息... -[15:37:21] Agent 回复: 拍下发你,不好退 -[15:38:22] Agent 正在处理消息... -[15:38:24] Agent 回复: 30,拍下发你 -[15:39:14] Agent 正在处理消息... -[15:39:16] Agent 回复: 拍下发你,不好退 -[17:20:40] Agent 正在处理消息... -[17:20:42] Agent 回复: 在呢 -[17:21:04] Agent 正在处理消息... -[17:21:23] Agent 回复: 已经完成,不需要额外回复了。 -[17:21:23] Agent 正在处理消息... -[17:21:26] Agent 回复: 拍下发你,不满意退款 -[17:21:42] Agent 正在处理消息... -[17:21:44] Agent 回复: 30,拍下吧 -[17:26:08] Agent 正在处理消息... -[17:26:12] Agent 回复: 在呢,啥事 -[17:26:17] Agent 正在处理消息... -[17:26:23] Agent 回复: 发图来看看 -[17:26:41] Agent 正在处理消息... -[17:26:58] Agent 回复: 25,拍下发你 -[17:38:48] Agent 正在处理消息... -[17:38:53] Agent 回复: 在呢,发图我看看。 -[17:38:53] Agent 正在处理消息... -[17:38:55] Agent 回复: 已收到您的订单,付款后我马上安排哈 -[17:38:55] Agent 正在处理消息... -[17:38:58] Agent 回复: 图发过来我看看。 -[17:38:58] Agent 正在处理消息... -[17:39:01] Agent 回复: 已经收到付款啦,把需要的图发我吧。 -[17:57:48] Agent 正在处理消息... -[17:57:52] Agent 处理失败: name 'asyncio' is not defined -[17:59:06] Agent 正在处理消息... -[17:59:09] Agent 回复: 在呢,啥事 -[17:59:23] Agent 正在处理消息... -[17:59:31] Agent 回复: 发图来看看 -[17:59:31] Agent 正在处理消息... -[17:59:35] Agent 回复: 发图来看看 -[18:01:52] Agent 正在处理消息... -[18:02:05] Agent 回复含无效内容,已拦截: 已经完成回复,不需要额外操作。 -[18:05:02] Agent 正在处理消息... -[18:05:05] Agent 回复: 在呢 -[18:05:20] Agent 正在处理消息... -[18:05:37] Agent 回复含无效内容,已拦截: 不需要额外回复,已完成报价和记录。 -[18:08:56] Agent 正在处理消息... -[18:09:02] Agent 回复: 发图来我看看 -[18:09:02] Agent 正在处理消息... -[18:09:05] Agent 回复: 已收到您的订单,付款后我马上安排哈 -[18:09:52] Agent 正在处理消息... -[18:09:57] Agent 回复: 发图来看看 -[18:09:57] Agent 正在处理消息... -[18:10:02] Agent 回复: 已收到,付款后我马上安排哈 -[18:10:47] Agent 正在处理消息... -[18:10:49] Agent 回复: 发图过来我看看 -[18:10:50] Agent 正在处理消息... -[18:10:53] Agent 回复: 已收到您的订单,付款后我马上安排哈 -[18:10:55] Agent 正在处理消息... -[18:10:58] Agent 回复: 把图发我看看哦 -[18:10:58] Agent 正在处理消息... -[18:11:01] Agent 回复: 收到付款,我马上安排。 -[18:18:20] Agent 正在处理消息... -[18:18:23] Agent 回复: 在呢 -[18:20:11] Agent 正在处理消息... -[18:20:14] Agent 回复: 在呢 -[18:20:48] Agent 正在处理消息... -[18:21:17] Agent 回复: 30,拍下我安排 -[18:21:17] Agent 正在处理消息... -[18:21:20] Agent 回复: 拍下发你,不满意退款 -[18:22:12] Agent 正在处理消息... -[18:22:14] Agent 回复: 发图来我看看 -[18:22:15] Agent 正在处理消息... -[18:22:15] Agent 决定不回复此消息 -[18:22:30] Agent 正在处理消息... -[18:22:33] Agent 回复: 发图来我看看 -[18:22:33] Agent 正在处理消息... -[18:22:35] Agent 回复: 收到款了,马上安排。 -[18:22:46] Agent 正在处理消息... -[18:22:49] Agent 回复: 发图来看看 -[18:22:54] Agent 正在处理消息... -[18:22:56] Agent 回复: 收到了,马上弄 -[18:30:27] Agent 正在处理消息... -[18:30:29] Agent 回复: 在呢 -[18:31:40] Agent 正在处理消息... -[18:31:57] Agent 回复: 30,拍下发你 -[18:31:57] Agent 正在处理消息... -[18:32:00] Agent 回复: 拍下我安排 -[18:32:16] Agent 正在处理消息... -[18:32:18] Agent 回复: 发图来我看看 -[18:32:19] Agent 正在处理消息... -[18:32:19] Agent 决定不回复此消息 -[18:32:20] Agent 正在处理消息... -[18:32:23] Agent 回复: 图发我看看 -[18:32:24] Agent 正在处理消息... -[18:32:26] Agent 回复: 收到了,马上安排 -[18:35:54] Agent 正在处理消息... -[18:35:56] Agent 回复: 在呢 -[18:39:30] Agent 正在处理消息... -[18:39:33] Agent 回复: 在呢 -[18:39:45] Agent 正在处理消息... -[18:40:00] Agent 回复含无效内容,已拦截: 已经完成,无需额外回复。 ← 命中pattern: ['无需', '无需额外', '已经完成'] -[18:40:18] Agent 正在处理消息... -[18:40:24] Agent 回复: 发图来看看 -[18:40:24] Agent 正在处理消息... -[18:40:24] Agent 决定不回复此消息 -[18:40:24] Agent 正在处理消息... -[18:40:26] Agent 回复: 发图来看看 -[18:40:26] Agent 正在处理消息... -[18:40:29] Agent 回复: 收到了,马上发你 -[18:45:45] Agent 正在处理消息... -[18:45:47] Agent 回复: 发图来我看看 -[18:46:02] Agent 正在处理消息... -[18:46:04] Agent 回复: 图发我看看 -[18:46:14] Agent 正在处理消息... -[18:46:14] Agent 决定不回复此消息 -[18:46:48] Agent 正在处理消息... -[18:47:00] Agent 回复: 25,拍下发你 -[19:02:22] Agent 正在处理消息... -[19:02:25] Agent 回复: 在呢 -[19:02:38] Agent 正在处理消息... -[19:02:54] Agent 回复: 30,拍下发你 -[19:02:54] Agent 正在处理消息... -[19:03:02] Agent 回复: 拍下我马上安排 -[19:03:02] Agent 正在处理消息... -[19:03:06] Agent 回复: 拍下我安排,好了发你。 -[19:03:31] Agent 正在处理消息... -[19:03:31] Agent 决定不回复此消息 -[19:03:37] Agent 正在处理消息... -[19:03:39] Agent 回复: 收到,马上给你安排。 -[19:06:46] Agent 正在处理消息... -[19:06:46] Agent 决定不回复此消息 -[19:07:13] Agent 正在处理消息... -[19:07:13] Agent 决定不回复此消息 -[19:15:06] Agent 正在处理消息... -[19:15:11] Agent 回复: 没什么,拍下我尽快发你哈。 -[19:15:26] Agent 正在处理消息... -[19:15:26] Agent 决定不回复此消息 -[19:17:17] Agent 正在处理消息... -[19:17:24] Agent 回复: 在呢 -[19:17:41] Agent 正在处理消息... -[19:18:00] Agent 回复: 30,拍下我安排 -[19:18:00] Agent 正在处理消息... -[19:18:10] Agent 回复: 拍下我马上发你 -[19:18:51] Agent 正在处理消息... -[19:18:51] Agent 决定不回复此消息 -[19:19:41] Agent 正在处理消息... -[19:19:41] Agent 决定不回复此消息 -[19:19:59] Agent 正在处理消息... -[19:19:59] Agent 决定不回复此消息 -[19:21:44] Agent 正在处理消息... -[19:21:48] Agent 回复: 在做了,快了 -[19:22:08] Agent 正在处理消息... -[19:22:22] Agent 回复: 满意,拍下我尽快发你。 -[19:22:50] Agent 正在处理消息... -[19:22:55] Agent 回复: 拍下吧,我马上安排。 -[19:29:37] Agent 正在处理消息... -[19:29:44] Agent 回复: 在呢,发图来看看 -[19:30:30] Agent 正在处理消息... -[19:30:30] Agent 决定不回复此消息 -[19:30:41] Agent 正在处理消息... -[19:30:41] Agent 决定不回复此消息 -[19:31:37] Agent 正在处理消息... -[19:31:37] Agent 决定不回复此消息 -[19:31:47] Agent 正在处理消息... -[19:31:47] Agent 决定不回复此消息 -[19:32:07] Agent 正在处理消息... -[19:32:25] Agent 回复: 25,拍下我安排 -[19:33:03] Agent 正在处理消息... -[19:33:03] Agent 决定不回复此消息 -[19:33:07] Agent 正在处理消息... -[19:33:10] Agent 回复: 收到了,马上安排 -[19:33:32] Agent 正在处理消息... -[19:33:34] Agent 回复: 发图来我看看 -[19:33:40] Agent 正在处理消息... -[19:33:40] [Workflow] AI 通知已发送: 图发你了,先看下效果,没问题把邮箱发我我给你发过来。 -[19:33:42] Agent 回复: 把你现在的图发我看看 -[19:33:48] Agent 正在处理消息... -[22:02:37] Agent 正在处理消息... -[22:02:55] Agent 回复: 17,拍下我安排 -[22:03:12] Agent 正在处理消息... -[22:03:49] Agent 回复: 两张50,拍下 -[22:03:53] Agent 正在处理消息... -[22:03:55] Agent 回复: 拍下后发你确认,不满意退款。 -[22:09:51] Agent 正在处理消息... -[22:44:15] Agent 正在处理消息... -[22:44:18] Agent 回复: 在呢 -[23:17:53] Agent 正在处理消息... -[23:17:56] Agent 回复: 发图来我看看 -[23:18:21] Agent 正在处理消息... -[23:19:00] Agent 回复含无效内容,已拦截: 不需要额外操作,等待客户回复即可。 ← 命中pattern: ['不需要额外'] -[23:19:18] Agent 正在处理消息... -[23:19:53] Agent 回复: 20,拍下我安排 -[23:20:30] Agent 正在处理消息... -[23:20:40] Agent 正在处理消息... -[23:20:44] Agent 正在处理消息... -[23:20:47] Agent 回复: 什么问题,你说清楚点 -[23:21:03] Agent 正在处理消息... -[23:21:08] Agent 正在处理消息... -[23:21:12] Agent 回复: 20,拍下我发你 -[23:21:43] Agent 正在处理消息... -[23:21:49] Agent 回复: 15,真的最低了,拍下吧 -[00:16:18] Agent 正在处理消息... -[00:16:43] Agent 回复含无效内容,已拦截: 已经完成报价和记录,无需额外回复。 ← 命中pattern: ['无需', '无需额外', '已经完成'] -[00:16:47] Agent 正在处理消息... -[00:16:47] Agent 决定不回复此消息 -[00:18:45] Agent 正在处理消息... -[00:18:45] Agent 决定不回复此消息 diff --git a/logs/chat_2026-02-26.log b/logs/chat_2026-02-26.log deleted file mode 100755 index 5264dcc..0000000 --- a/logs/chat_2026-02-26.log +++ /dev/null @@ -1,131 +0,0 @@ -[09:51:23] Agent 正在处理消息... -[09:51:29] Agent 回复: 基本都可以,你发图来看看。 -[09:51:38] Agent 正在处理消息... -[09:51:46] Agent 回复: 你发图过来先看看。 -[09:52:01] Agent 正在处理消息... -[10:41:14] Agent 正在处理消息... -[10:41:20] Agent 回复: 在呢,发图吧,做多张有优惠。 -[10:44:58] Agent 正在处理消息... -[10:45:02] Agent 回复: 店家呀,你要找图直接发图就好啦。 -[10:45:25] Agent 正在处理消息... -[10:45:29] Agent 回复: 稍等,我查一下给你重发哈。 -[10:51:25] Agent 正在处理消息... -[10:51:28] Agent 回复: 发图来我看看哈 -[10:51:34] Agent 正在处理消息... -[10:52:11] Agent 回复: 25,拍下发你,人脸可能会有轻微变化,不满意可退。 -[10:53:49] Agent 正在处理消息... -[10:53:56] Agent 回复: 收到啦,麻烦把要处理的图发我一下哈。 -[10:54:39] Agent 正在处理消息... -[10:54:39] Agent 决定不回复此消息 -[10:54:47] Agent 正在处理消息... -[10:54:50] Agent 回复: 是高清优化难免的,整体效果会好很多,拍下给你安排。 -[10:55:37] Agent 正在处理消息... -[10:55:39] Agent 回复: 半小时内就能好,拍下安排哈。 -[10:55:48] Agent 正在处理消息... -[10:55:55] Agent 回复: 给你便宜5块,20,拍下吧。 -[10:56:13] [Workflow] AI 通知已发送: 图发你预览了,看下效果,没问题把邮箱发我我给你发原图。 -[10:57:13] Agent 正在处理消息... -[10:57:13] Agent 决定不回复此消息 -[10:58:06] Agent 正在处理消息... -[10:58:10] Agent 回复: 这已经是最低啦,要做我就给你安排了。 -[11:00:20] Agent 正在处理消息... -[11:00:50] Agent 回复: 25,拍下发你。 -[11:00:54] Agent 正在处理消息... -[11:01:01] Agent 回复: 已安排,好了发你。 -[11:01:55] Agent 正在处理消息... -[11:01:59] Agent 回复: 收到了,马上安排,好了发你。 -[11:02:11] [Workflow] AI 通知已发送: 图发好了,你看下效果,没问题把邮箱发我给你发过去。 -[11:52:54] Agent 正在处理消息... -[11:53:00] Agent 回复: 得重新续费pro套餐了,直接拍店里对应的续费套餐就行。 -[13:06:48] Agent 正在处理消息... -[13:06:52] Agent 回复: 快了,好了发你。 -[13:07:30] Agent 正在处理消息... -[13:07:35] Agent 回复: 可以找,你发相关信息过来看看。 -[13:07:50] Agent 正在处理消息... -[13:08:19] Agent 回复: 这张25,拍下发你,人脸会有轻微变化,介意的话可以考虑下哦。 -[13:08:19] Agent 正在处理消息... -[13:08:23] Agent 回复: 都发过来吧,我一起看看给你算打包价。 -[13:08:52] Agent 正在处理消息... -[13:09:18] Agent 回复: 这类不做哦。 -[13:09:40] Agent 正在处理消息... -[13:09:44] Agent 回复: 放心拍,不满意包退。 -[13:10:05] Agent 正在处理消息... -[13:10:08] Agent 回复: 那拍第一张就行,25,拍下发你,不满意退。 -[14:43:25] Agent 正在处理消息... -[14:43:32] Agent 回复: 看到订单了,请问具体是遇到什么问题了呢? -[14:44:00] Agent 正在处理消息... -[14:44:33] Agent 回复: 15,拍下发你。 -[14:46:07] Agent 正在处理消息... -[14:46:11] Agent 回复: pro权限到期了需要重新续费哦,当前pro月套餐是50元,直接拍下续费就可以继续使用了。 -[14:56:57] Agent 正在处理消息... -[14:57:39] Agent 回复: 25,拍下发你。 -[14:57:39] Agent 正在处理消息... -[14:57:51] Agent 回复: 放心拍,拍下就发,不满意退。 -[15:34:46] Agent 正在处理消息... -[15:35:07] Agent 回复: 25,拍下发你。 -[17:28:45] Agent 正在处理消息... -[17:28:51] Agent 回复: 1块做不了哦,最低10块,要做发图看看。 -[18:02:42] Agent 正在处理消息... -[18:02:42] Agent 决定不回复此消息 -[18:02:48] Agent 正在处理消息... -[18:02:53] Agent 回复: 麻烦把需要处理的图片发我一下哈。 -[18:03:32] Agent 正在处理消息... -[18:03:39] Agent 正在处理消息... -[18:03:42] Agent 回复: 快了,马上发你哈。 -[18:39:55] Agent 正在处理消息... -[18:39:55] Agent 决定不回复此消息 -[18:41:08] Agent 正在处理消息... -[18:41:15] Agent 回复: 你把需要的图片发我一下哈。 -[18:41:24] Agent 正在处理消息... -[18:41:29] Agent 回复: 稍等我看看哈。 -[18:49:34] Agent 正在处理消息... -[18:49:41] Agent 回复: 没事,客人要了直接拍就行,还是这个价。 -[18:49:46] Agent 正在处理消息... -[18:50:12] Agent 回复: 这张25,拍下发你。 -[18:50:12] Agent 正在处理消息... -[18:50:21] Agent 回复: 两张一共45,拍下一起发。 -[19:00:10] Agent 正在处理消息... -[19:00:31] Agent 回复: 15,拍下吧 -[19:00:47] Agent 正在处理消息... -[19:01:16] Agent 回复: 这张25,两张一共40,一起拍了吧 -[19:01:33] Agent 正在处理消息... -[19:01:39] Agent 回复: 35,两块一起拍了吧 -[19:01:49] Agent 正在处理消息... -[19:01:52] Agent 回复: 你拍下我改价,改好了付款就行。 -[19:02:04] Agent 正在处理消息... -[19:02:04] Agent 决定不回复此消息 -[19:02:19] Agent 正在处理消息... -[19:02:28] Agent 回复: 收到了,马上弄好发你。 -[19:03:50] [Workflow] AI 通知已发送: 图发你了,看下效果没问题把邮箱发我,我发你文件。 -[19:05:22] Agent 正在处理消息... -[19:05:25] Agent 回复: 把图发过来吧,我这边好马上开始弄。 -[19:11:19] Agent 正在处理消息... -[19:11:22] Agent 回复: 放心拍,弄好都给你处理干净,不满意退。 -[19:14:54] Agent 正在处理消息... -[19:15:00] Workflow 回复: 好,你说一下哪里要改,或者发图告诉我 -[19:15:15] Agent 正在处理消息... -[19:15:18] Agent 回复: 发图来我看看。 -[19:15:19] Agent 正在处理消息... -[19:15:24] Agent 回复: 已经处理好了,预览图发你了,把邮箱给我,我发你原图文件。 -[19:15:31] Agent 正在处理消息... -[19:15:34] Agent 回复: 你把邮箱地址发我就行,我发你原图文件哈。 -[19:15:50] Agent 正在处理消息... -[19:16:20] Agent 回复: 这张25,拍下就行。 -[19:16:47] Agent 正在处理消息... -[19:16:54] Agent 回复: 好的,尺寸改好了,拍下一起发。 -[19:17:03] Agent 正在处理消息... -[19:17:07] Agent 回复: 知道了,会按这个要求帮你弄好,放心拍就行。 -[19:18:16] Agent 正在处理消息... -[19:18:25] Agent 正在处理消息... -[19:18:30] Agent 回复: 给你算40,这已经很优惠了,拍下吧。 -[19:19:31] Agent 正在处理消息... -[19:19:31] Agent 决定不回复此消息 -[19:19:37] Agent 正在处理消息... -[19:19:44] Agent 回复: 已经收到款了,我马上安排,弄好发给你。 -[19:19:53] Agent 正在处理消息... -[19:19:56] Agent 回复: 放心哈,我正在处理,很快就能弄好发你。 -[19:21:56] [Workflow] AI 通知已发送: 已经处理好啦,你先看看效果,没问题把邮箱发我,我发你原图哦。 -[19:22:59] Agent 正在处理消息... -[19:22:59] Agent 决定不回复此消息 -[20:19:01] Agent 正在处理消息... -[20:19:06] Agent 回复: 没事,想要了直接拍下就行,不满意包退哈。 diff --git a/logs/chat_2026-02-27.log b/logs/chat_2026-02-27.log deleted file mode 100755 index d06646a..0000000 --- a/logs/chat_2026-02-27.log +++ /dev/null @@ -1,136 +0,0 @@ -[10:22:10] Agent 正在处理消息... -[10:22:36] Agent 回复: 25,拍下吧。 -[10:25:57] Agent 正在处理消息... -[10:26:27] Agent 回复含无效内容,已拦截: 不需要额外回复,等待客户下一步即可。 ← 命中pattern: ['不需要额外'] -[10:26:27] Agent 正在处理消息... -[10:26:54] Agent 回复含无效内容,已拦截: 不需要额外操作,等待客户回应即可。 ← 命中pattern: ['不需要额外'] -[10:30:21] Agent 正在处理消息... -[10:30:25] Agent 回复: 放心拍,不满意退。 -[10:32:59] Agent 正在处理消息... -[10:33:28] Agent 回复: 15,拍下发你。 -[10:33:28] Agent 正在处理消息... -[10:33:32] Agent 回复: 15拍下,马上安排。 -[10:37:29] Agent 正在处理消息... -[10:37:29] Agent 决定不回复此消息 -[10:37:34] Agent 正在处理消息... -[10:37:39] Agent 回复: 收到啦,马上帮你弄,好了发你。 -[10:37:54] [Workflow] AI 通知已发送: 图已经发你啦,你看下效果,没问题把邮箱发我我发你原文件。 -[11:10:58] Agent 正在处理消息... -[11:11:00] Agent 正在处理消息... -[11:11:02] Agent 回复: 在呢 -[11:11:20] Agent 正在处理消息... -[11:11:27] Agent 回复: 这个不接哦,我们只做图片相关的哈 -[11:11:40] Agent 回复: 15,拍下发你哈。 -[11:11:40] Agent 正在处理消息... -[11:11:46] Agent 正在处理消息... -[11:11:47] Agent 回复: 总共20,拍下弄好发你。 -[11:11:49] Agent 回复: 发图来看看 -[11:11:58] Agent 正在处理消息... -[11:12:01] Agent 回复: 知道了,20搞定,拍下吧。 -[11:12:23] Agent 正在处理消息... -[11:12:26] Agent 回复: 没问题,拍下安排。 -[11:12:47] Agent 正在处理消息... -[11:12:55] Agent 回复: 行吧,10就10,拍下吧。 -[11:13:19] Agent 正在处理消息... -[11:13:22] Agent 回复: 直接拍这个宝贝就行,拍下备注好要求。 -[11:13:50] Agent 正在处理消息... -[11:14:19] Agent 回复: 已经处理好了,等待客户回应就行。 -[11:14:19] Agent 正在处理消息... -[11:14:24] Agent 回复: 25,拍下就能安排了 -[11:14:37] Agent 正在处理消息... -[11:14:42] Agent 回复: 出版信息我们找不到哦,我们只做图片高清处理,要做直接拍下。 -[11:14:49] Agent 正在处理消息... -[11:14:53] Agent 回复: 高清图我们可以安排,拍下试试,不满意退。 -[11:15:04] Agent 正在处理消息... -[11:15:13] Agent 正在处理消息... -[11:15:13] Agent 决定不回复此消息 -[11:22:03] Agent 正在处理消息... -[11:22:08] Agent 回复: 直接在这里下单就行,拍下我就弄。 -[11:23:01] Agent 正在处理消息... -[11:23:01] Agent 决定不回复此消息 -[11:25:09] Agent 正在处理消息... -[11:25:15] Agent 回复: 看图片情况呢,你发图我给你说具体价格。 -[11:26:05] Agent 正在处理消息... -[12:32:22] Agent 正在处理消息... -[12:32:25] Agent 回复: 发图来我看看 -[12:39:48] Agent 正在处理消息... -[12:40:28] Agent 回复含无效内容,已拦截: 已经回复客户报价了,不需要额外操作。 ← 命中pattern: ['不需要额外'] -[12:40:28] Agent 正在处理消息... -[12:41:01] Agent 回复含无效内容,已拦截: 已经处理完成,不需要额外操作。 ← 命中pattern: ['不需要额外'] -[12:41:01] Agent 正在处理消息... -[12:41:07] Agent 回复: 拍下发你,不满意可退,还有其他图一起发的话有打包优惠。 -[12:41:20] Agent 正在处理消息... -[12:41:23] Agent 回复: 发图来我看看。 -[12:44:10] Agent 正在处理消息... -[12:44:49] Agent 回复含无效内容,已拦截: 已经给客户回复了报价,记录完成,不需要额外操作。 ← 命中pattern: ['不需要额外', '记录完成'] -[12:44:49] Agent 正在处理消息... -[12:44:53] Agent 回复: 可以,拍下发你,不满意退。 -[12:46:55] Agent 正在处理消息... -[12:46:59] Agent 回复: 放心拍,没问题,不满意退哈。 -[12:49:20] Agent 正在处理消息... -[12:49:22] Agent 回复: 快了,好了发你哈 -[12:55:59] Agent 正在处理消息... -[12:56:02] Agent 回复: 发图过来我先看看哈。 -[12:56:12] Agent 正在处理消息... -[12:56:17] Agent 回复: 图发我看看哦,做好给你改到这个尺寸。 -[12:56:20] Agent 正在处理消息... -[12:56:49] Agent 回复含无效内容,已拦截: 不需要额外回复,之前已经发送了报价给客户。 ← 命中pattern: ['不需要额外'] -[12:57:22] Agent 正在处理消息... -[12:57:43] Agent 回复含无效内容,已拦截: 不需要额外回复,已经给出了报价和打包优惠的信息。 ← 命中pattern: ['不需要额外'] -[12:57:49] Agent 正在处理消息... -[12:57:54] Agent 回复: 知道啦,拍下我就开始安排了。 -[12:58:22] Agent 正在处理消息... -[12:58:26] Agent 回复: 直接拍下这个商品就行,今天就能弄好,你上来我发你。 -[13:01:02] Agent 正在处理消息... -[13:01:05] Agent 回复: 稍等哈 -[13:44:03] Agent 正在处理消息... -[13:44:29] Agent 正在处理消息... -[13:44:31] Agent 回复: 在呢 -[13:44:39] Agent 回复: 已经帮你记录好报价啦,等待客户进一步回复即可。 -[13:44:39] Agent 正在处理消息... -[13:44:43] Agent 回复: 还是这张对吧,25拍下就行。 -[13:47:49] Agent 正在处理消息... -[13:47:54] Agent 回复: 稍等,我找一下马上发你 -[13:49:10] Agent 正在处理消息... -[13:49:13] Agent 回复: 马上好 -[13:50:23] Agent 正在处理消息... -[13:50:39] Agent 回复: 25,拍下发你。 -[13:50:39] Agent 正在处理消息... -[13:50:59] Agent 回复: 两张打包50,拍下吧。 -[14:36:40] Agent 正在处理消息... -[14:37:11] Agent 回复含无效内容,已拦截: 已经处理完成,无需额外回复。 ← 命中pattern: ['无需', '无需额外'] -[14:37:45] Agent 正在处理消息... -[14:37:54] Agent 正在处理消息... -[14:37:57] Agent 回复: 没问题,拍下安排。 -[15:19:46] Agent 正在处理消息... -[15:20:14] Agent 回复含无效内容,已拦截: 已经完成回复,无需额外内容。 ← 命中pattern: ['无需', '无需额外', '已经完成'] -[15:20:15] Agent 正在处理消息... -[15:20:17] Agent 回复: 好的,拍下安排哈。 -[15:27:19] Agent 正在处理消息... -[15:27:46] Agent 回复: 25,拍下发你。 -[15:31:52] Agent 正在处理消息... -[15:31:57] Agent 回复: 在呢,有需要发图看看哈。 -[15:32:07] Agent 正在处理消息... -[15:32:39] Agent 回复: 这张25,拍下哈。 -[15:32:46] Agent 正在处理消息... -[15:32:56] Agent 回复: 没问题,可以给到这个要求,拍下安排吧。 -[15:33:07] Agent 正在处理消息... -[15:33:10] Agent 回复: 放心拍,发你高清版,不满意退哈。 -[15:33:36] Agent 正在处理消息... -[15:33:40] Agent 回复: 都是拍下发哦,不满意直接退,放心哈。 -[15:34:24] Agent 正在处理消息... -[15:34:28] Agent 回复: 拍下后不满意直接退哈,没风险的。 -[15:35:04] Agent 正在处理消息... -[15:35:09] Agent 回复: 我们这边都是先拍下再安排哦,不满意直接退款,没损失的。 -[15:35:51] Agent 正在处理消息... -[15:35:55] Agent 回复: 放心哈,达不到你要的效果直接退,绝对不扯皮。 -[15:36:47] Agent 正在处理消息... -[15:36:51] Agent 回复: 那给你算20,拍下吧,不满意直接退。 -[15:44:23] Agent 正在处理消息... -[15:44:26] Agent 回复: 好的,等你消息。 -[15:48:56] Agent 正在处理消息... -[15:49:32] Agent 回复: 这张20,两张一起算35,拍下吧。 -[15:49:33] Agent 正在处理消息... -[15:49:37] Agent 回复: 那再给你优惠5块,两张一共30就好,这个价格已经很划算了哦。 -[15:55:18] Agent 正在处理消息... -[15:55:22] Agent 回复: 好的,你把QQ号码发给我,我加你哈。 diff --git a/run.py b/run.py index 7e849fe..ed5f4b7 100755 --- a/run.py +++ b/run.py @@ -159,7 +159,7 @@ def run_tianwang_multi(num_workers: int, enable_agent: bool, host: str, port: in logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}") logger.info("=" * 60) - coordinator = Coordinator(num_workers=num_workers or 0, enable_agent=enable_agent) + coordinator = Coordinator(num_workers=num_workers or 1, enable_agent=enable_agent) def _signal_handler(signum, frame): logger.info("收到退出信号,正在停止多进程协调器...") diff --git a/services/service_image_analyzer.py b/services/service_image_analyzer.py index 73f5e34..f9ff0bd 100644 --- a/services/service_image_analyzer.py +++ b/services/service_image_analyzer.py @@ -164,7 +164,9 @@ class ImageAnalyzerService: timeout=30 ) - content = response.choices[0].message.content + if not response.choices: + return self._fallback(image_url, "API 返回空 choices") + content = response.choices[0].message.content or "" elapsed = time.monotonic() - start result = self._parse_result(image_url, content) diff --git a/tests/replay/test_golden_replay.py b/tests/replay/test_golden_replay.py deleted file mode 100644 index ee55779..0000000 --- a/tests/replay/test_golden_replay.py +++ /dev/null @@ -1,42 +0,0 @@ -import unittest - -from core.quote_state_machine import QuoteStateMachine - - -class _State: - def __init__(self): - self.pending_image_urls = [] - self.pending_requirements = [] - self.quote_phase = "idle" - self.quote_ready_turns = 0 - - -class GoldenReplayTests(unittest.TestCase): - def test_replay_collect_then_ready_then_quote(self): - sm = QuoteStateMachine(delay_turns=1) - st = _State() - - replay = [ - {"event": "image", "url": "a.jpg", "want_phase": "collecting"}, - {"event": "image", "url": "b.jpg", "want_phase": "collecting"}, - {"event": "finish", "want_phase": "ready_to_quote", "want_defer": True}, - {"event": "progress", "want_phase": "ready_to_quote", "want_defer": False}, - ] - - for step in replay: - if step["event"] == "image": - st.pending_image_urls.append(step["url"]) - sm.refresh(st) - self.assertEqual(st.quote_phase, step["want_phase"]) - elif step["event"] == "finish": - deferred = sm.should_defer_batch_quote(st, mark_ready=True) - self.assertEqual(st.quote_phase, step["want_phase"]) - self.assertEqual(deferred, step["want_defer"]) - elif step["event"] == "progress": - deferred = sm.should_defer_batch_quote(st, mark_ready=False) - self.assertEqual(st.quote_phase, step["want_phase"]) - self.assertEqual(deferred, step["want_defer"]) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_ai_chat.py b/tests/test_ai_chat.py deleted file mode 100644 index fac78bb..0000000 --- a/tests/test_ai_chat.py +++ /dev/null @@ -1,330 +0,0 @@ -""" -AI Agent 对话测试脚本 -从数据库加载聊天记录,测试 AI 回复效果 -""" -import sqlite3 -import asyncio -import sys -from pathlib import Path -from datetime import datetime - -# 颜色代码 -COLORS = { - 'header': '\033[95m\033[1m', - 'customer': '\033[94m', - 'agent': '\033[92m', - 'system': '\033[90m', - 'price': '\033[93m', - 'error': '\033[91m', - 'cyan': '\033[96m', - 'reset': '\033[0m', -} - -# Windows PowerShell defaults to GBK in some environments. -# Make stdout/stderr robust for Unicode logs used by this test script. -for stream_name in ("stdout", "stderr"): - stream = getattr(sys, stream_name, None) - if stream and hasattr(stream, "reconfigure"): - try: - stream.reconfigure(encoding="utf-8", errors="replace") - except Exception: - pass - -# Ensure project root is importable when running as `uv run tests/test_ai_chat.py`. -PROJECT_ROOT = str(Path(__file__).resolve().parent.parent) -if PROJECT_ROOT not in sys.path: - sys.path.insert(0, PROJECT_ROOT) -DB_PATH = Path(PROJECT_ROOT) / "db" / "chat_log_db" / "chats.db" - -def cprint(text, color='reset'): - print(f"{COLORS.get(color, '')}{text}{COLORS['reset']}") - -def check_database(): - """检查数据库内容""" - try: - conn = sqlite3.connect(DB_PATH) - cursor = conn.execute("SELECT COUNT(*) FROM chat_logs") - count = cursor.fetchone()[0] - - if count == 0: - cprint(f"\n✗ 数据库为空,没有聊天记录", 'error') - cprint("提示:需要先有一些聊天记录才能测试", 'system') - conn.close() - return None - - cprint(f"\n✓ 数据库连接成功!共 {count} 条聊天记录", 'system') - - # 获取客户列表 - cursor = conn.execute(""" - SELECT customer_id, customer_name, COUNT(*) as cnt, MAX(timestamp) as last - FROM chat_logs - GROUP BY customer_id - ORDER BY cnt DESC - LIMIT 20 - """) - customers = cursor.fetchall() - - cprint(f"\n找到 {len(customers)} 个客户:", 'cyan') - for i, (cid, name, cnt, last) in enumerate(customers, 1): - cprint(f" {i:2d}. {name or cid:30s} | {cnt:4d}条 | 最后:{last}", 'customer') - - conn.close() - return customers - - except Exception as e: - cprint(f"\n✗ 数据库检查失败:{e}", 'error') - return None - -async def test_customer_conversation(customer_id, customer_name, limit=5): - """测试某个客户的对话""" - cprint(f"\n{'='*70}", 'cyan') - cprint(f"测试客户:{customer_name or customer_id}", 'header') - cprint(f"{'='*70}\n", 'cyan') - - # 获取对话记录 - conn = sqlite3.connect(DB_PATH) - cursor = conn.execute(""" - SELECT direction, message, timestamp - FROM chat_logs - WHERE customer_id = ? - ORDER BY timestamp ASC - LIMIT ? - """, (customer_id, limit)) - conversations = cursor.fetchall() - conn.close() - - if not conversations: - cprint(" 该客户没有对话记录", 'system') - return - - # 初始化 AI Agent - try: - from core.pydantic_ai_agent import CustomerServiceAgent, CustomerMessage - agent = CustomerServiceAgent(skills_dir="skills") - cprint("✓ AI Agent 已加载", 'system') - except Exception as e: - cprint(f"✗ AI Agent 加载失败:{e}", 'error') - return - - # 模拟对话 - for i, (direction, message, timestamp) in enumerate(conversations, 1): - if direction == 'in': - # 客户消息 - cprint(f"\n【消息 {i}/{len(conversations)}】{timestamp}", 'system') - cprint(f"客户:{message}", 'customer') - - # 创建测试消息 - test_msg = CustomerMessage( - msg_id=f"test_{i}", - acc_id="test_shop", - msg=message, - from_id=customer_id, - from_name=customer_name or "测试", - cy_id=customer_id, - acc_type="AliWorkbench", - msg_type=0, - cy_name=customer_name or "测试", - goods_name="专业找图", - goods_order="" - ) - - # 获取 AI 回复 - start = datetime.now() - try: - response = await agent.process_message(test_msg) - elapsed = (datetime.now() - start).total_seconds() * 1000 - - if response.should_reply: - cprint(f"AI [{elapsed:.0f}ms]: {response.reply}", 'agent') - - # 检测特殊内容 - if any(kw in response.reply for kw in ['元', '块', '价格']): - cprint(" ↳ [价格信息]", 'price') - if response.need_transfer: - cprint(" ↳ [转人工]", 'error') - else: - cprint("[AI 静默]", 'system') - - except Exception as e: - cprint(f"✗ AI 回复失败:{e}", 'error') - - elif direction == 'out': - cprint(f"\n[历史回复] {timestamp}", 'system') - cprint(f"客服:{message}", 'system') - - cprint(f"\n{'='*70}", 'cyan') - -async def test_all_customers(customers, limit_per_customer=5): - """批量测试所有客户""" - cprint(f"\n{'='*70}", 'header') - cprint(f" 开始批量测试 {len(customers)} 个客户", 'header') - cprint(f" 每个客户测试前 {limit_per_customer} 条消息", 'header') - cprint(f"{'='*70}\n", 'header') - - total_msgs = 0 - total_replies = 0 - - for i, (cid, name, cnt, _) in enumerate(customers, 1): - cprint(f"\n\n{'='*70}", 'cyan') - cprint(f"进度:{i}/{len(customers)} - {name or cid} ({cnt}条消息)", 'cyan') - cprint(f"{'='*70}", 'cyan') - - if cnt == 0: - cprint(" 跳过(无消息记录)", 'system') - continue - - # 获取对话记录 - conn = sqlite3.connect(DB_PATH) - cursor = conn.execute(""" - SELECT direction, message, timestamp - FROM chat_logs - WHERE customer_id = ? - ORDER BY timestamp ASC - LIMIT ? - """, (cid, limit_per_customer)) - conversations = cursor.fetchall() - conn.close() - - # 初始化 AI Agent(只初始化一次) - try: - from core.pydantic_ai_agent import CustomerServiceAgent, CustomerMessage - if i == 1: # 第一个客户时初始化 - agent = CustomerServiceAgent(skills_dir="skills") - cprint("✓ AI Agent 已加载", 'system') - except Exception as e: - cprint(f"✗ AI Agent 加载失败:{e}", 'error') - return - - # 模拟对话 - for j, (direction, message, timestamp) in enumerate(conversations, 1): - if direction == 'in': - total_msgs += 1 - - # 创建测试消息 - test_msg = CustomerMessage( - msg_id=f"test_{i}_{j}", - acc_id="test_shop", - msg=message, - from_id=cid, - from_name=name or "测试", - cy_id=cid, - acc_type="AliWorkbench", - msg_type=0, - cy_name=name or "测试", - goods_name="专业找图", - goods_order="" - ) - - # 获取 AI 回复 - start = datetime.now() - try: - response = await agent.process_message(test_msg) - elapsed = (datetime.now() - start).total_seconds() * 1000 - - if response.should_reply: - total_replies += 1 - cprint(f"\n[{i}/{len(customers)}] {name or cid} - 消息 {j}", 'system') - cprint(f"客户:{message}", 'customer') - cprint(f"AI [{elapsed:.0f}ms]: {response.reply}", 'agent') - - # 检测特殊内容 - if any(kw in response.reply for kw in ['元', '块', '价格']): - cprint(" ↳ [价格信息]", 'price') - if response.need_transfer: - cprint(" ↳ [转人工]", 'error') - else: - cprint(f"\n[{i}/{len(customers)}] [AI 静默]", 'system') - - except Exception as e: - cprint(f"✗ AI 回复失败:{e}", 'error') - - # 每个客户之间休息一下 - await asyncio.sleep(0.5) - - # 统计结果 - cprint(f"\n\n{'='*70}", 'header') - cprint(f" 批量测试完成!", 'header') - cprint(f"{'='*70}", 'header') - cprint(f"\n统计:", 'system') - cprint(f" 测试客户数:{len(customers)}", 'cyan') - cprint(f" 处理消息数:{total_msgs}", 'cyan') - cprint(f" AI 回复数:{total_replies}", 'cyan') - if total_msgs > 0: - reply_rate = (total_replies / total_msgs) * 100 - cprint(f" 回复率:{reply_rate:.1f}%", 'cyan') - -async def main(): - cprint("="*70, 'header') - cprint(" AI Agent 对话测试", 'header') - cprint(" 从数据库加载聊天记录,测试 AI 回复效果", 'header') - cprint("="*70, 'header') - - # 检查数据库 - customers = check_database() - if not customers: - return - - # 选择测试模式 - cprint(f"\n请选择测试模式:", 'cyan') - cprint(f" 1. 交互式测试 (手动选择客户)", 'customer') - cprint(f" 2. 批量测试所有客户 (自动)", 'agent') - cprint(f" 3. 快速测试前 5 个客户", 'price') - cprint(f" q. 退出", 'system') - - mode = input("\n选择:").strip().lower() - - if mode == 'q': - cprint("\n测试结束!", 'system') - return - - try: - if mode == '1': - # 交互式测试 - cprint(f"\n请输入客户编号 (1-{len(customers)}) 进行测试:", 'cyan') - - while True: - try: - choice = input("\n选择:").strip() - - if choice.lower() == 'q': - cprint("\n测试结束!", 'system') - return - - choice_num = int(choice) - if 1 <= choice_num <= len(customers): - cid, name, cnt, _ = customers[choice_num - 1] - await test_customer_conversation(cid, name or cid, limit=min(cnt, 10)) - else: - cprint(f"请输入 1-{len(customers)} 之间的数字", 'error') - - except ValueError: - cprint("请输入有效数字或 q 退出", 'error') - except KeyboardInterrupt: - cprint("\n\n测试中断", 'error') - return - except Exception as e: - cprint(f"错误:{e}", 'error') - - elif mode == '2': - # 批量测试所有客户 - await test_all_customers(customers, limit_per_customer=5) - - elif mode == '3': - # 快速测试前 5 个客户 - top_5 = customers[:5] - cprint(f"\n快速测试前 5 个客户...", 'cyan') - await test_all_customers(top_5, limit_per_customer=5) - - else: - cprint("无效的选择", 'error') - - except KeyboardInterrupt: - cprint("\n\n测试中断", 'error') - except Exception as e: - cprint(f"错误:{e}", 'error') - -if __name__ == "__main__": - try: - asyncio.run(main()) - except Exception as e: - cprint(f"\n程序异常:{e}", 'error') diff --git a/tests/test_batch_quote_reply_format.py b/tests/test_batch_quote_reply_format.py deleted file mode 100644 index d5cccf2..0000000 --- a/tests/test_batch_quote_reply_format.py +++ /dev/null @@ -1,89 +0,0 @@ -import unittest -from unittest.mock import AsyncMock, patch - -from core.pydantic_ai_agent import CustomerMessage, CustomerServiceAgent - - -class BatchQuoteReplyFormatTest(unittest.IsolatedAsyncioTestCase): - async def test_batch_reply_contains_per_image_and_options(self): - agent = CustomerServiceAgent() - cid = "__batch_quote_case__" - st = agent._get_conversation_state(cid) - st.pending_image_urls = ["https://img.alicdn.com/a.jpg", "https://img.alicdn.com/b.jpg"] - st.pending_requirements = ["去背景", "加急"] - - msg = CustomerMessage( - msg_id="m-batch-1", - acc_id="test_shop", - msg="发完了,统一报价", - from_id=cid, - from_name="t", - cy_id=cid, - acc_type="AliWorkbench", - msg_type=0, - cy_name="t", - goods_name="专业找图", - goods_order="", - ) - - fake_r1 = { - "complexity": "normal", - "reason": "常规处理", - "price_min": 15, - "price_max": 25, - "price_suggest": 20, - "feasibility": "yes", - "risk": "low", - "aspect_ratio": "1:1", - "perspective": "no", - } - fake_r2 = { - "complexity": "complex", - "reason": "细节较多", - "price_min": 20, - "price_max": 30, - "price_suggest": 25, - "feasibility": "yes", - "risk": "low", - "aspect_ratio": "1:1", - "perspective": "no", - } - - with patch("image.image_analyzer.image_analyzer.analyze", new=AsyncMock(side_effect=[fake_r1, fake_r2])): - with patch("core.workflow.workflow.image_analysis_result", new=AsyncMock(return_value=None)): - res = await agent._quote_pending_images(st, msg) - - self.assertFalse(res.get("need_transfer", False)) - reply = res.get("reply", "") - self.assertIn("图1", reply) - self.assertIn("图2", reply) - self.assertIn("可选", reply) - self.assertIn("打包", reply) - self.assertIn("共", reply) - - async def test_single_image_reply_avoids_batch_wording(self): - agent = CustomerServiceAgent() - results = [ - ( - "https://img.alicdn.com/a.jpg", - { - "complexity": "normal", - "reason": "常规处理", - "price_suggest": 20, - }, - ) - ] - reply = agent._build_batch_quote_reply( - results=results, - total_suggest=20, - bundle_price=20, - req_fee={"extra": 0, "hits": []}, - ) - self.assertIn("这张", reply) - self.assertNotIn("这批", reply) - self.assertNotIn("先给你分图报下", reply) - self.assertNotIn("可选:A", reply) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_evolution_mvp.py b/tests/test_evolution_mvp.py deleted file mode 100644 index c41f4b9..0000000 --- a/tests/test_evolution_mvp.py +++ /dev/null @@ -1,54 +0,0 @@ -import unittest -from unittest.mock import patch - -from evolution.mvp import Finding, Sample, can_publish_candidate, evaluate_samples - - -class EvolutionMvpTest(unittest.TestCase): - def test_evaluate_detects_risk_without_transfer(self): - samples = [ - Sample( - customer_id="c1", - acc_id="shop", - in_ts="2026-02-28 10:00:00", - in_text="我要投诉并退款,你们骗人", - out_ts="2026-02-28 10:00:10", - out_text="这个我不清楚,稍后再说", - latency_sec=10, - ) - ] - findings = evaluate_samples(samples) - kinds = {f.kind for f in findings} - self.assertIn("risk_not_transferred", kinds) - self.assertIn("weak_reply", kinds) - - def test_publish_gate(self): - samples = [ - Sample( - customer_id=f"c{i}", - acc_id="shop", - in_ts="2026-02-28 10:00:00", - in_text="你好", - out_ts="2026-02-28 10:00:05", - out_text="您好", - latency_sec=5, - ) - for i in range(35) - ] - findings: list[Finding] = [] - policy = { - "publish_gate": { - "min_sample_count": 30, - "max_high_findings_rate": 0.1, - "max_ai_fail_rate": 5.0, - "max_transfer_rate": 45.0, - } - } - with patch("utils.metrics_tracker.get_runtime_summary", return_value={"rates": {"ai_fail_rate": 1.0, "transfer_rate": 10.0}}): - ok, report = can_publish_candidate(samples, findings, runtime_hours=24, policy=policy) - self.assertTrue(ok) - self.assertEqual(report["sample_count"], 35) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_intent_analyzer.py b/tests/test_intent_analyzer.py deleted file mode 100644 index 582fe70..0000000 --- a/tests/test_intent_analyzer.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from utils.intent_analyzer import detect_intent - - -class IntentAnalyzerTests(unittest.TestCase): - def test_keyword_fallback_for_price(self): - d = detect_intent("这个怎么收费") - self.assertEqual(d.intent, "询价") - self.assertEqual(d.source, "keyword") - - def test_keyword_fallback_for_greeting(self): - d = detect_intent("你好 在吗") - self.assertEqual(d.intent, "打招呼") - self.assertEqual(d.source, "keyword") - - def test_unknown_intent(self): - d = detect_intent("abc123") - self.assertEqual(d.intent, "") - self.assertIn(d.source, ("none", "")) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_multi_worker_routing.py b/tests/test_multi_worker_routing.py deleted file mode 100644 index 3f3c6c8..0000000 --- a/tests/test_multi_worker_routing.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -import unittest - -from core.websocket_client_v2 import QingjianAPIClient - - -class MultiWorkerRoutingTest(unittest.TestCase): - def test_only_one_worker_owns_customer_when_no_explicit_shards(self): - os.environ["AI_CS_WORKER_COUNT"] = "4" - key = "shop_x:tb123456" - owners = 0 - for wid in range(4): - os.environ["AI_CS_WORKER_ID"] = str(wid) - c = QingjianAPIClient(enable_agent=False) - c.shard_keys = set() # 模拟当前无分片表 - if c._is_owned_by_this_worker(key): - owners += 1 - self.assertEqual(owners, 1) - - def tearDown(self): - os.environ.pop("AI_CS_WORKER_COUNT", None) - os.environ.pop("AI_CS_WORKER_ID", None) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_outbound_cooldown.py b/tests/test_outbound_cooldown.py deleted file mode 100644 index 69a2cba..0000000 --- a/tests/test_outbound_cooldown.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import unittest - -from websockets.protocol import State - -from core.websocket_client_v2 import QingjianAPIClient - - -class _DummyWS: - def __init__(self): - self.state = State.OPEN - self.sent = [] - - async def send(self, msg_json: str): - self.sent.append(msg_json) - - -class OutboundCooldownTest(unittest.IsolatedAsyncioTestCase): - def setUp(self): - os.environ["OUTBOUND_PER_CUSTOMER_COOLDOWN_SECONDS"] = "5" - - async def test_skip_second_reply_within_cooldown(self): - c = QingjianAPIClient(enable_agent=False) - c.websocket = _DummyWS() - msg = { - "acc_id": "shop_a", - "from_id": "u001", - "from_name": "u001", - "acc_type": "AliWorkbench", - } - await c.send_reply(msg, "第一条") - await c.send_reply(msg, "第二条") - self.assertEqual(len(c.websocket.sent), 1) - - def tearDown(self): - os.environ.pop("OUTBOUND_PER_CUSTOMER_COOLDOWN_SECONDS", None) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_oversize_guard.py b/tests/test_oversize_guard.py deleted file mode 100644 index 04c0f1b..0000000 --- a/tests/test_oversize_guard.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import unittest - -from core.websocket_client_v2 import QingjianAPIClient - - -class OversizeGuardTest(unittest.TestCase): - def setUp(self): - os.environ["MAX_SERVICE_SIZE_LONGEST_METERS"] = "10" - os.environ["MAX_SERVICE_SIZE_AREA_SQM"] = "20" - - def test_extract_size_pairs(self): - c = QingjianAPIClient(enable_agent=False) - pairs = c._extract_size_pairs_m("15*6.4米 高度") - self.assertTrue(len(pairs) >= 1) - self.assertEqual(pairs[0], (15.0, 6.4)) - - def test_oversize_hits(self): - c = QingjianAPIClient(enable_agent=False) - r = c._oversize_reply_if_needed("15*6.4米") - self.assertIn("做不了", r) - - def test_normal_size_not_hit(self): - c = QingjianAPIClient(enable_agent=False) - r = c._oversize_reply_if_needed("2.4*1.2米") - self.assertEqual(r, "") - - def tearDown(self): - os.environ.pop("MAX_SERVICE_SIZE_LONGEST_METERS", None) - os.environ.pop("MAX_SERVICE_SIZE_AREA_SQM", None) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_system_inquiry_rules.py b/tests/test_system_inquiry_rules.py deleted file mode 100644 index fee4c4e..0000000 --- a/tests/test_system_inquiry_rules.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import unittest -from unittest.mock import AsyncMock - -from core.websocket_client_v2 import QingjianAPIClient - - -class SystemInquiryRulesTest(unittest.IsolatedAsyncioTestCase): - def setUp(self): - self.rules = { - "enabled": True, - "default_action": "silent", - "default_reply": "已收到", - "sender_keywords": ["系统客服", "官方客服"], - "message_keywords": ["系统询单", "代客咨询"], - "shops": { - "shop_reply": { - "enabled": True, - "action": "reply", - "reply": "店铺回复模板", - "sender_keywords": ["机器人客服"], - "message_keywords": ["询单"], - } - }, - } - os.environ["SYSTEM_INQUIRY_ENABLED"] = "true" - os.environ["SYSTEM_INQUIRY_SHOPS"] = "" - - async def test_detect_by_sender_keyword(self): - client = QingjianAPIClient(enable_agent=False) - client._system_inquiry_rules = self.rules - policy = client._resolve_system_inquiry_policy("shop_a") - data = {"acc_id": "shop_a", "from_name": "平台系统客服", "from_id": "kefu001", "msg": "你好"} - self.assertTrue(client._match_system_inquiry(data, policy)) - - async def test_shop_rule_reply_action(self): - client = QingjianAPIClient(enable_agent=False) - client._system_inquiry_rules = self.rules - client.send_reply = AsyncMock() - client.transfer_to_human = AsyncMock() - - data = { - "acc_id": "shop_reply", - "from_name": "机器人客服A", - "from_id": "robot_01", - "msg": "有个询单请处理", - "acc_type": "AliWorkbench", - } - handled = await client._handle_system_inquiry(data) - self.assertTrue(handled) - client.send_reply.assert_awaited_once() - client.transfer_to_human.assert_not_awaited() - - async def test_shop_whitelist_blocks_other_shops(self): - os.environ["SYSTEM_INQUIRY_SHOPS"] = "shop_only" - client = QingjianAPIClient(enable_agent=False) - client._system_inquiry_rules = self.rules - client.send_reply = AsyncMock() - data = {"acc_id": "shop_other", "from_name": "系统客服", "from_id": "sys_1", "msg": "系统询单"} - handled = await client._handle_system_inquiry(data) - self.assertFalse(handled) - client.send_reply.assert_not_awaited() - - def tearDown(self): - for k in ("SYSTEM_INQUIRY_ENABLED", "SYSTEM_INQUIRY_SHOPS"): - os.environ.pop(k, None) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_transfer_greeting_context.py b/tests/test_transfer_greeting_context.py deleted file mode 100644 index d1f8730..0000000 --- a/tests/test_transfer_greeting_context.py +++ /dev/null @@ -1,20 +0,0 @@ -import unittest - -from core.websocket_client_v2 import QingjianAPIClient - - -class TransferGreetingContextTest(unittest.TestCase): - def test_transfer_greeting_is_non_empty(self): - c = QingjianAPIClient(enable_agent=False) - text = c._pick_transfer_greeting() - self.assertTrue(isinstance(text, str) and len(text) > 0) - - def test_transfer_greeting_contains_presence_phrase(self): - c = QingjianAPIClient(enable_agent=False) - for _ in range(10): - text = c._pick_transfer_greeting() - self.assertTrue(("在" in text) or ("我在" in text)) - - -if __name__ == "__main__": - unittest.main(verbosity=2)