From afb2b78c15f6377c7dcdff62435433c7589f2294 Mon Sep 17 00:00:00 2001 From: jimi <1847930177@qq.com> Date: Fri, 6 Mar 2026 13:23:32 +0800 Subject: [PATCH] newtw2 --- CODE_REVIEW_ISSUES.md | 211 --------------- check_logs.py | 23 -- core/engine.py | 35 +-- core/events/event_bus.py | 7 +- core/orchestrator.py | 36 ++- core/pydantic_ai_agent_v2.py | 50 ++-- core/schema.py | 4 +- core/skill_manager.py | 3 +- core/websocket_client_v2.py | 3 +- db/chat_log_db/chats.db | Bin 102400 -> 0 bytes db/deal_outcome_db/outcomes.db | Bin 28672 -> 0 bytes db/designer_roster_db/roster.db | Bin 40960 -> 0 bytes db/image_tasks.db | Bin 20480 -> 0 bytes db/task_db/tasks.db | Bin 24576 -> 0 bytes logs/chat_2026-02-25.log | 223 ---------------- logs/chat_2026-02-26.log | 131 ---------- logs/chat_2026-02-27.log | 136 ---------- run.py | 2 +- services/service_image_analyzer.py | 4 +- tests/replay/test_golden_replay.py | 42 --- tests/test_ai_chat.py | 330 ------------------------ tests/test_batch_quote_reply_format.py | 89 ------- tests/test_evolution_mvp.py | 54 ---- tests/test_intent_analyzer.py | 24 -- tests/test_multi_worker_routing.py | 26 -- tests/test_outbound_cooldown.py | 40 --- tests/test_oversize_guard.py | 34 --- tests/test_system_inquiry_rules.py | 70 ----- tests/test_transfer_greeting_context.py | 20 -- 29 files changed, 76 insertions(+), 1521 deletions(-) delete mode 100644 CODE_REVIEW_ISSUES.md delete mode 100644 check_logs.py delete mode 100755 db/chat_log_db/chats.db delete mode 100755 db/deal_outcome_db/outcomes.db delete mode 100755 db/designer_roster_db/roster.db delete mode 100644 db/image_tasks.db delete mode 100644 db/task_db/tasks.db delete mode 100755 logs/chat_2026-02-25.log delete mode 100755 logs/chat_2026-02-26.log delete mode 100755 logs/chat_2026-02-27.log delete mode 100644 tests/replay/test_golden_replay.py delete mode 100644 tests/test_ai_chat.py delete mode 100644 tests/test_batch_quote_reply_format.py delete mode 100644 tests/test_evolution_mvp.py delete mode 100644 tests/test_intent_analyzer.py delete mode 100644 tests/test_multi_worker_routing.py delete mode 100644 tests/test_outbound_cooldown.py delete mode 100644 tests/test_oversize_guard.py delete mode 100644 tests/test_system_inquiry_rules.py delete mode 100644 tests/test_transfer_greeting_context.py 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 72bccb486a2c0bdf998dccc5408c24fe2c65b4c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102400 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 Qu8CYtbTx4uO}2;s0Q@n@0L zXgjXe4%|3!*lC9yHtjH>9T5i(NJ#tvh!Y1k5s4RR69>?ad+ao>ous8FAn<+E#D4tv zar}IKPptZJ?Jmsc$~BW;s;*u&YCJ^-38my~&IutA@d}GqD=A{eezg*9&1^G`kVDHf z!u}(n{y{SEFMBZXb7b$p#r`*6JnkRtzqd_I6hQz25P$##AOHafK;S(Htc~{tqa!19 zJytW8R?O9Ey;9Owt4rmIXEt=Ipk_3cYnhWdm3s>KSeUmZ>q_;SUM_J>{Z`}W3)yp- z!XiJbE>3i;xnfu~y<91o-}7u`txvwUSXYOS(8~v1$D-CxU_K{P-Vu^X}K#qxn13LoU$%BZe=0U9^}q8`ua|*FLbVUS;Tg;k8Qo4r}yzO z&z?;1jSq@%PdkLA4~` zcHD+_38g23Uf4`go6F|KR?ew;t<%%qljgBt>o|4$1{L$!`J(C`5d6pqDG*hM--T># z`dYF$l=j(&Q~M-eVvh)WDc+DE009U<00Izz00bZa0SG_<0ubmKfjyE!4h%iNeee10 zUmACAoOFibmiAEJ(u}jzurKgS>=j{e#Tya?AOHafKmY;|fB*y_009U<00O-(5SE8% zb0|Ox$wQ&$`M-VrpR(6P{2)O90uX=z1Rwwb2tWV=5P$##An>UOM5sJW-TN9O`}|*K ze-QRJdnhJI5P$##AOHafKmY;|fB*y_009VmV1fOzLPuj#7ybY z5ow>Kgbv0UH-CF}=ZD7KpI+R$n@FanXVQm{d_|Se=!maUaw<8KJeo*PrKat_|CiZA z!k(~y!~_Wf5P$##AOHafKmY;|fB*y_0D+HKV7C|=p#Gr&`Aab*K)pi(l2|et@(l$v zpZ|BCu)o;-kGOf*H3&ce0uX=z1Rwwb2tWV=5P-mEE)Y|cp}th}#(r^OKRr2>oJ^*9 zB7JN+eJpi2CcDK(m*TK^?4QJ*684n6`OG2Fd3b+sb zGwgr)7j6y{xC@sQ`}#i_?AhjF#~}a#2tWV=5P$##AOHafKmY;|*nxn3|3A+EcL0S| ZAOHafKmY;|fB*y_009U<00KQM@IQN<^+^B# diff --git a/db/image_tasks.db b/db/image_tasks.db deleted file mode 100644 index 99f6d9440188e88874badcea437f743401d2ab92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI#!D`zu7zc2xP1+GK?PcUx4Ik1#C>>+>WvvHcUb1B_v?rt4S|*mrjx9s4gR$+k z_7r=EJ;5%8S$1aBxH;}({)X7H{YjR7`fQ^2?>wP6{TQb!rsy;4*doMwL8)a~-OaB! zl$$>9Y|8pTjPkBx*E$P{ZU3@5y*sOScl6W#W}oZ|#&rll00Izz00bZa0SNq8fv+dz zullQ-k} z4Snt2j2e+)o~d}n(>i)O^Xc@;^O~W_k}3WFylVB*eKo%Fd^%VQCZ)&+%?fcU7DBRR zP4*SSdfic6K1m6~116jOP1lh!>>xQv8cJlJBE zFO|{XW{Y9Unc`7UtWvaYGayeQGvJ>1h{m}J3-7Hl);*$0Dnd?+nfB`bo}Zl$dFQY{ zaL5;1$cW#H=r+(37w9Q5GNHRkk;(MZy{7k~=xl1{%&9T&Bl>SXQ~c zHPUIn_^v+E-&R}ij`a>31Rwwb2tWV=5P$##AOHafKmY=573k@KVg29M>BYDp009U< z00Izz00bZa0SG_<0;K@f|40M~KmY;|fB*y_009U<00IzzK>G!-{%`*rV}uZZ00bZa Q0SG_<0uX=z1Rwx`zbMLTD*ylh diff --git a/db/task_db/tasks.db b/db/task_db/tasks.db deleted file mode 100644 index 977db7f13531ca08b96e16367658b4425a79958b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI)Z)?*)90%}=U8`M@>I2Ef7@SC{1vk2GZZZ(8F3Pl4?K0Sq5n_9*acxsCS6mxF`O36@srpu=va;3i`}zlBRB1s}H4qk7iAJzxyTk*(Gqty0v=+OE9`^ z^SUYMJUK-ag>6dxOu;I`batHBwUUqI6 ztnXgeSSXiQ+0W!1XP(|uj;pGwyikPspvK(B?vG=B20eeZpm|-P=f~-x(hZf@LUnCJ zr*})MxAUbxDu%V{pzP zd4d?fKjAEbTh%J#MsUW#Av>)7#O5B%AEO+vXOzy&(3@*=kSW6$#C9BTtmM|XLE$+zs zLDz0*rJ!AIA>6_rk?zn5@;6zdVl}EqoPaaI+eX=LHAExPOlh zAnOG@@I1m9{GlO-x12wByA%#7+x4cr!-j=0&TVcC|HW|Tv})ctf&p`2sAMw7V(Mj8 zA?}00d0dk-d&6uA+dVeiyW5y^-y-s1d!qZmJt;ROd6UQ&3Irek0SG_<0uX=z1Rwwb z2tWV=e-zl8S0<*@sWYi`X7Y53q$ZWA6H0!v+fLWkbR(P11q)4rIimCS`Ir5dpI^V~ zJbQom_&{KAz*)1__L|+_a^!kRZ^6L(tn=8DXdWdd700Izz00bZa0SG_<0uX=z Q1pZ5b=}104_`d-B0_MxVBme*a 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)