Compare commits
2 Commits
006b035de4
...
afb2b78c15
| Author | SHA1 | Date | |
|---|---|---|---|
| afb2b78c15 | |||
| 4ba636e98c |
@@ -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
|
||||
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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]}")
|
||||
content = (msg.content or "")
|
||||
logger.info(f"[Engine] 收到来自 {msg.platform} 的消息: {msg.user_id} -> {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}
|
||||
)
|
||||
|
||||
# 兜底回复
|
||||
return StandardResponse(
|
||||
reply_content="你好,我是AI助手,有什么可以帮你的?",
|
||||
reply_content="稍等哈,设计师马上来。",
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'<think[^>]*>.*', '', reply_text, flags=re.DOTALL) # 清理 <think_xxx>内部思考泄漏
|
||||
reply_text = re.sub(r'</?think[^>]*>', '', 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'</?tool[_\-]?[^>]*>', '', reply_text, flags=re.IGNORECASE)
|
||||
reply_text = re.sub(r'<think[^>]*>.*?</think[^>]*>', '', reply_text, flags=re.DOTALL)
|
||||
reply_text = re.sub(r'<think[^>]*>.*', '', reply_text, flags=re.DOTALL)
|
||||
reply_text = re.sub(r'</?think[^>]*>', '', 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()
|
||||
|
||||
# 过滤"在呢铁子"
|
||||
|
||||
@@ -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) # 额外元数据(如埋点、调试信息)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 决定不回复此消息
|
||||
@@ -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 回复: 没事,想要了直接拍下就行,不满意包退哈。
|
||||
@@ -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号码发给我,我加你哈。
|
||||
2
run.py
2
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("收到退出信号,正在停止多进程协调器...")
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Binary file not shown.
Reference in New Issue
Block a user