feat: 添加 AI Agent 对话测试工具 + 代码优化

主要变更:

- 新增 tests/test_ai_chat.py: AI Agent 对话测试工具

- 优化 core/pydantic_ai_agent.py 和 db/chat_log_db.py

- 清理归档文件,更新文档

Made-with: Cursor
This commit is contained in:
2026-02-28 16:19:35 +08:00
parent a6c42d505a
commit c39840fe15
49 changed files with 2453 additions and 8556 deletions

36
.gitignore vendored
View File

@@ -1,36 +0,0 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# 数据库
*.db
*.db-journal
# 日志
logs/
*.log
# 结果图片
results/
# 环境配置
.env.backup
.env.*.backup
# 备份文件
*.bak
*.backup
*.fix_backup
# IDE
.idea/
.vscode/
*.swp
*.swo
# 系统文件
.DS_Store
Thumbs.db

View File

@@ -1,325 +0,0 @@
# AI 客服系统 - 天网协作版部署文档
## 📋 运行架构
**单进程多模块架构**
- ✅ 一个 Python 进程
- ✅ 多个模块协同工作
- ✅ WebSocket 客户端 + HTTP API 服务器 + 任务调度器
**优势**
- 部署简单(一条命令)
- 资源共享(数据库连接、内存)
- 通信高效(进程内调用)
---
## 🚀 部署步骤
### 步骤 1环境检查
```bash
# 检查 Python 版本
python3 --version # 需要 3.8+
# 检查依赖
cd /root/ai_customer_service/ai_cs
pip3 list | grep -E "flask|pydantic|sqlite"
```
### 步骤 2启动天网协作版
```bash
cd /root/ai_customer_service/ai_cs
# 启动(包含 AI Agent
python3 run_with_tianwang.py
# 启动(不包含 AI Agent仅基础回复
python3 run_with_tianwang.py --no-agent
```
**启动后会看到**
```
============================================================
AI 客服系统 - 天网协作版
============================================================
正在启动 HTTP API 服务器...
HTTP API 服务器已启动http://0.0.0.0:5678
天网任务接口:
POST /api/task/receive - 接收任务
POST /api/task/cancel - 取消任务
GET /api/task/status/:id - 查询任务状态
GET /api/task/list - 任务列表
GET /api/health - 健康检查
============================================================
正在连接轻简 API...
AI Agent: 已启用
============================================================
系统已就绪,等待消息和任务...
```
### 步骤 3测试 API
```bash
# 测试健康检查
curl http://localhost:5678/api/health
# 接收任务
curl -X POST http://localhost:5678/api/task/receive \
-H "Content-Type: application/json" \
-d '{
"task_id": "TEST_001",
"type": "test",
"customer": {"id": "test_001"},
"trigger": {"type": "customer_reply", "keyword": "测试"},
"action": {"type": "send_message", "message": "测试消息"},
"created_by": "测试"
}'
# 查询任务
curl http://localhost:5678/api/task/status/TEST_001
```
---
## 🔧 生产环境部署
### 方式 1systemd 服务(推荐)
创建服务文件:
```bash
cat > /etc/systemd/system/ai-cs-tianwang.service << 'SERVICE'
[Unit]
Description=AI Customer Service with Tianwang
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/ai_customer_service/ai_cs
ExecStart=/usr/bin/python3 run_with_tianwang.py
Restart=always
RestartSec=10
LimitNOFILE=65535
# 环境变量(可选)
Environment="HTTP_API_PORT=5678"
Environment="TASK_DB_PATH=/root/ai_customer_service/ai_cs/db/task_db/tasks.db"
# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ai-cs-tianwang
[Install]
WantedBy=multi-user.target
SERVICE
# 启动服务
systemctl daemon-reload
systemctl enable ai-cs-tianwang
systemctl start ai-cs-tianwang
# 查看状态
systemctl status ai-cs-tianwang
# 查看日志
journalctl -u ai-cs-tianwang -f
```
### 方式 2Docker 部署
创建 Dockerfile
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5678
CMD ["python3", "run_with_tianwang.py"]
```
构建并运行:
```bash
docker build -t ai-cs-tianwang .
docker run -d \
--name ai-cs \
-p 5678:5678 \
-v /root/ai_customer_service/ai_cs/db:/app/db \
--restart unless-stopped \
ai-cs-tianwang
```
### 方式 3后台运行简单场景
```bash
cd /root/ai_customer_service/ai_cs
# 使用 nohup 后台运行
nohup python3 run_with_tianwang.py > /var/log/ai-cs.log 2>&1 &
# 查看进程
ps aux | grep run_with_tianwang
# 查看日志
tail -f /var/log/ai-cs.log
# 停止进程
pkill -f run_with_tianwang
```
---
## 📊 端口说明
| 端口 | 用途 | 是否必须 |
|------|------|----------|
| 5678 | HTTP API 服务器 | ✅ 必须 |
| 9528 | 轻简软件 WebSocket | ✅ 必须(外部) |
**防火墙配置**
```bash
# 开放 HTTP API 端口
firewall-cmd --add-port=5678/tcp --permanent
firewall-cmd --reload
# 或者使用 iptables
iptables -A INPUT -p tcp --dport 5678 -j ACCEPT
```
---
## 🔍 监控与日志
### 查看运行状态
```bash
# systemd 方式
systemctl status ai-cs-tianwang
# 进程方式
ps aux | grep run_with_tianwang
```
### 查看日志
```bash
# systemd 方式
journalctl -u ai-cs-tianwang -f
# 文件方式
tail -f /var/log/ai-cs.log
# 只看任务相关日志
journalctl -u ai-cs-tianwang -f | grep "任务"
```
### 查看数据库
```bash
# 进入 SQLite
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db
# 查看所有任务
SELECT task_id, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 10;
# 查看待触发任务
SELECT * FROM tasks WHERE status='pending';
# 查看失败任务
SELECT task_id, error_message FROM tasks WHERE status='failed';
# 退出
.exit
```
---
## ⚠️ 注意事项
1. **HTTP 端口**
- 默认 5678确保未被占用
- 可在 `.env` 中修改:`HTTP_API_PORT=5679`
2. **数据库路径**
- 确保有写权限
- 建议定期备份:`cp tasks.db tasks.db.backup`
3. **天网回调**
- 配置正确的回调 URL
- 确保天网服务器能访问 AI 客服
4. **资源限制**
- 建议内存:≥ 512MB
- 建议 CPU≥ 1 核心
5. **进程守护**
- 推荐使用 systemd
- 自动重启,故障恢复
---
## 🐛 故障排查
### 无法启动
```bash
# 检查端口占用
netstat -tlnp | grep 5678
# 检查依赖
pip3 list | grep -E "flask|pydantic"
# 手动运行查看错误
python3 run_with_tianwang.py
```
### 任务未触发
```bash
# 1. 检查任务状态
curl http://localhost:5678/api/task/status/TASK_ID
# 2. 查看日志
journalctl -u ai-cs-tianwang -f | grep "任务触发"
# 3. 检查触发条件
# 确认客户消息包含触发关键词
```
### HTTP API 无法访问
```bash
# 1. 检查服务状态
systemctl status ai-cs-tianwang
# 2. 检查端口
netstat -tlnp | grep 5678
# 3. 检查防火墙
firewall-cmd --list-ports
# 4. 本地测试
curl http://localhost:5678/api/health
```
---
## 📞 技术支持
- 完整文档:`/root/ai_customer_service/ai_cs/TIANWANG_INTEGRATION.md`
- 使用示例:`/root/ai_customer_service/ai_cs/SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md`
- 日志位置:`journalctl -u ai-cs-tianwang -f`
- 数据库位置:`/root/ai_customer_service/ai_cs/db/task_db/tasks.db`

View File

@@ -1,105 +0,0 @@
# AI 客服系统修复总结
## ✅ 已修复的问题
### 1. Agent 模块语法错误
**问题**: `@self.agent.tool`装饰器在类方法外部使用
**修复**: 删除了错误的`upload_to_tuhui_platform` 工具定义(行 219-246
### 2. WebSocket 客户端循环导入
**问题**: 顶部导入导致循环依赖
**修复**: 改为延迟加载,在需要时才导入任务模块
### 3. 任务模块初始化
**问题**: `__init__` 中直接导入任务模块
**修复**: 删除直接初始化,改为`_load_task_modules()` 方法延迟加载
---
## 🚀 启动方式
### 方式 1: AI 客服(原始稳定版)
```bash
cd /root/ai_customer_service/ai_cs
python3 run.py
```
### 方式 2: 天网协作版HTTP API
```bash
cd /root/ai_customer_service/ai_cs
python3 run_tianwang_simple.py
```
### 方式 3: 后台运行
```bash
# AI 客服
nohup python3 run.py > /tmp/ai-cs.log 2>&1 &
# 天网协作
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
```
---
## 📊 当前状态
| 模块 | 状态 | 说明 |
|------|------|------|
| **AI 客服** | ✅ 可启动 | 需要轻简软件配合 |
| **天网 HTTP API** | ✅ 运行中 | 端口 5678 |
| **任务数据库** | ✅ 正常 | SQLite |
| **任务接收** | ✅ 已测试 | POST /api/task/receive |
| **指定客户回复** | ✅ 已实现 | specified_customer_reply |
---
## 🔧 修复的文件
1. `core/pydantic_ai_agent.py` - 删除错误的工具定义
2. `core/websocket_client.py` - 修复循环导入
3. `db/task_db/task_model.py` - 修复 INSERT 语句
4. `run_tianwang_simple.py` - 新建简化启动器
---
## 📝 测试记录
### 天网 API 测试
```bash
# 健康检查
curl http://localhost:5678/api/health
# ✅ 200 OK
# 接收任务
curl -X POST http://localhost:5678/api/task/receive \
-H "Content-Type: application/json" \
-d '{"task_id": "TEST_002", ...}'
# ✅ 任务接收成功
# 查询任务
curl http://localhost:5678/api/task/status/TEST_002
# ✅ 返回任务状态
```
### AI 客服测试
```bash
python3 run.py --no-agent
# ✅ 启动成功(需要轻简软件配合)
```
---
## 📖 文档位置
- 部署文档:`DEPLOYMENT.md`
- 使用示例:`SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md`
- 完整文档:`TIANWANG_INTEGRATION.md`
---
## ⚠️ 注意事项
1. **AI 客服**需要轻简软件运行在`ws://127.0.0.1:9528`
2. **天网 API**独立运行,不需要轻简软件
3. 两个系统可以同时运行,互不干扰

View File

@@ -1,226 +0,0 @@
# 多进程异步并行架构
## 架构说明
### 当前架构(改造前)
```
单进程 + 单事件循环
┌─────────────────────┐
│ Python 进程 │
│ ┌─────────────────┐ │
│ │ asyncio Loop │ │
│ │ 所有客户 + Agent │ │
│ └─────────────────┘ │
└─────────────────────┘
```
**问题**
- ❌ 单 CPU 核心
- ❌ 一个客户卡住影响全局
- ❌ 无法利用多核 CPU
---
### 新架构(改造后)
```
多进程 + 多事件循环
┌─────────┐ ┌─────────┐ ┌─────────┐
│进程 1 │ │进程 2 │ │进程 3 │
│Loop + │ │Loop + │ │Loop + │
│客户 A,B │ │客户 C,D │ │客户 E,F │
└─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓
独立运行 独立运行 独立运行
```
**优势**
- ✅ 真正的多核并行
- ✅ 故障隔离
- ✅ 负载均衡
- ✅ 可动态扩缩容
---
## 使用方法
### 方式 1systemd 服务(推荐)
```bash
# 启动多进程模式
systemctl start ai-cs-multi
# 查看状态
systemctl status ai-cs-multi
# 查看日志
journalctl -u ai-cs-multi -f
# 停止服务
systemctl stop ai-cs-multi
```
### 方式 2命令行启动
```bash
cd /root/ai_customer_service/ai_cs
# 使用默认进程数CPU 核心数)
python3 scripts/multi_process_launcher.py
# 指定进程数
python3 scripts/multi_process_launcher.py --workers 4
```
---
## 配置说明
### 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `AI_CS_WORKER_ID` | 工作进程 ID | 0 |
| `AI_CS_SHARD_KEYS` | 本进程负责的客户 key | 空 |
### 分片算法
客户按 `acc_id:from_id` 的 MD5 hash 值分配到不同进程:
```python
shard_id = int(md5(f"{acc_id}:{from_id}").hexdigest(), 16) % num_workers
```
**特点**
- 同一客户始终分配到同一进程
- 不同客户均匀分布
- 动态增减进程时自动重新平衡
---
## 监控
### 查看进程状态
```bash
# 查看所有工作进程
ps aux | grep ai-cs-worker
# 查看每个进程的 CPU 使用
top -p $(pgrep -d, -f ai-cs-worker)
```
### 日志查看
```bash
# 查看主进程日志
journalctl -u ai-cs-multi -f
# 查看特定工作进程日志
journalctl -u ai-cs-multi -f | grep "Worker 2"
```
---
## 性能对比
| 指标 | 单进程 | 多进程 (4 核) |
|------|--------|-------------|
| **并发客户数** | ~50 | ~200 |
| **CPU 使用率** | 25% (单核) | 80% (4 核) |
| **响应延迟** | 高 | 低 |
| **故障影响** | 全局 | 局部 |
---
## 注意事项
1. **进程数选择**
- 默认 = CPU 核心数
- 建议不超过 CPU 核心数 × 2
2. **内存占用**
- 每个进程独立内存空间
- 总内存 = 进程数 × 单进程内存
3. **日志管理**
- 每个进程独立日志
- 通过 `worker_id` 区分
4. **动态扩容**
- 修改 `--workers` 参数
- 重启服务自动重新分片
---
## 故障排查
### Worker 进程退出
```bash
# 查看日志
journalctl -u ai-cs-multi -f | grep "Worker.*退出"
# 手动重启
systemctl restart ai-cs-multi
```
### 负载不均衡
```bash
# 查看每个进程处理的客户数
ps aux | grep ai-cs-worker | awk '{print $2}' | while read pid; do
echo "PID $pid: $(ps -p $pid -o %cpu,%mem,cmd --no-headers)"
done
```
### 内存泄漏
```bash
# 监控内存使用
watch -n 1 'ps aux | grep ai-cs-worker | awk "{sum+=$6} END {print \"Total: \" sum/1024 \" MB\"}"'
```
---
## 使用 run.py 启动
### 单进程模式(默认)
```bash
cd /root/ai_customer_service/ai_cs
# 正常启动(含 AI Agent
python3 run.py
# 不启用 AI Agent
python3 run.py --no-agent
```
### 多进程模式
```bash
cd /root/ai_customer_service/ai_cs
# 多进程模式(默认 CPU 核心数)
python3 run.py --multi
# 指定进程数
python3 run.py --multi --workers 4
```
### systemd 服务
```bash
# 单进程模式
systemctl start ai-cs
systemctl status ai-cs
journalctl -u ai-cs -f
# 多进程模式
systemctl start ai-cs-multi
systemctl status ai-cs-multi
journalctl -u ai-cs-multi -f
```

176
README.md
View File

@@ -1,166 +1,82 @@
# AI 客服系统 - 天网协作版
**版本**: v1.0
**更新日期**: 2026-02-27
**服务器**: 1.12.50.92
**版本**: v1.0 | **服务器**: 1.12.50.92
---
## 🎯 功能概览
## 功能概览
### 核心功能
1. **天网协作** - 接收天网任务,支持指定客户回复触发
2. **三种工作流** - 根据客户说的话自动判断执行
3. **图片任务数据库** - 任务持久化,支持后续增加需求
4. **图绘派单系统** - 自动派单给在线设计师
5. **文字检测加价** - 自动识别文字数量并加价60-80 元高价值订单)
6. **风险评估** - 自动识别敏感内容,拒绝不良订单
7. **作图失败转人工** - 失败自动转接人工客服
| 功能 | 说明 |
|------|------|
| 天网协作 | 接收天网任务,支持指定客户回复触发 |
| 三种工作流 | 找图 / 处理图片 / 转人工派单 |
| 图片任务数据库 | 任务持久化,支持后续增加需求 |
| 图绘派单系统 | 自动派单给在线设计师 |
| 文字检测加价 | 自动识别文字数量并加价 |
| 风险评估 | 自动识别敏感内容,拒绝不良订单 |
| 作图失败转人工 | 失败自动转接人工客服 |
---
## 🚀 快速开始
### 启动服务
## 快速开始
```bash
cd /root/ai_customer_service/ai_cs
pip3 install -r requirements.txt
# 启动天网协作版(推荐
python3 run_tianwang_simple.py
# 天网协作版(仅 HTTP API
python3 run.py --api-only
# 后台运行
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
# 完整版HTTP API + WebSocket + AI Agent
python3 run.py --tianwang
# AI 客服(仅 WebSocket默认
python3 run.py
```
### API 地址
- **天网任务接收**: `http://127.0.0.1:6060`
- **派单系统**: `http://1.12.50.92:8005`
- **图绘平台**: `http://1.12.50.92:8002`
---
## 📋 三种工作流
### 1. 查找图片
**触发词**: "找一下"、"找图"、"找原图"
**回复**: "找到了http://tuhui.cloud/works/123"
### 2. 处理图片
**触发词**: "做一下"、"处理一下"、"安排"
**回复**: "稍等,我看看...好的,可以做"
### 3. 转人工派单
**触发词**: "做不了"、"处理不了"
**回复**: "好的,已帮您安排设计师处理"
---
## 💰 价格策略
### 基础价格
| 复杂度 | 价格 |
|--------|------|
| simple | 10-15 元 |
| normal | 15-20 元 |
| complex | 20-25 元 |
| hard | 25-30 元 |
### 文字加价
| 文字数量 | 加价 |
|----------|------|
| 少量 (1-10 字) | +5 元 |
| 中量 (11-50 字) | +15 元 |
| 大量 (51-200 字) | +30 元 |
| 极多 (200 字以上) | +50 元 |
### 高价值订单
**文字分层 + 大量文字****60-80 元**
---
## 🔧 API 接口
### 接收天网任务
### 后台运行
```bash
curl -X POST http://localhost:6060/api/task/receive \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_001",
"customer": {"id": "customer_123"},
"trigger": {"type": "customer_reply", "keyword": "好的"},
"action": {"type": "send_message", "message": "您好"}
}'
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
```
### 查询派单队列
### 验证
```bash
curl -X GET "http://1.12.50.92:8005/dispatch/queue" \
-H "X-API-Key: tuhui_dispatch_key_2026"
```
### 查询在线设计师
```bash
curl -X GET "http://1.12.50.92:8005/online/designers" \
-H "X-API-Key: tuhui_dispatch_key_2026"
curl http://localhost:6060/api/health
```
---
## 📖 文档
## API 地址
| 文档 | 说明 |
| 服务 | 地址 |
|------|------|
| `项目功能汇总.md` | **完整功能说明** |
| `DEPLOYMENT.md` | 部署文档 |
| `TIANWANG_INTEGRATION.md` | 天网协作 |
| `三种工作流功能说明.md` | 工作流 |
| `文字加价功能说明.md` | 价格策略 |
| `风险评估功能说明.md` | 风险控制 |
| `图片任务数据库功能说明.md` | 任务管理 |
| `图绘派单系统集成说明.md` | 派单系统 |
| AI 客服 API | `http://127.0.0.1:6060` |
| 派单系统 | `http://1.12.50.92:8005` |
| 图绘平台 | `http://1.12.50.92:8002` |
---
## 🎯 使用示例
## 文档
### 客户发送图片
```
客户:找一下这个图 [图片]
AI: 找到了http://tuhui.cloud/works/123
```
```
客户:做一下 [图片]
AI: 稍等,我看看...好的,可以做
```
```
客户:这个能做吗 [图片]
AI: 抱歉,这个我做不了
AI: 好的,已帮您安排设计师处理
```
| 文档 | 内容 |
|------|------|
| **项目功能汇总.md** | 全部功能详细说明(工作流、报价、风险、派单、数据库等) |
| **部署文档.md** | 部署、API 接口、天网集成、多进程、故障排查 |
---
## 📞 技术支持
## 项目结构
**服务器**: 1.12.50.92
**日志**: `/tmp/tianwang.log`
**进程**: `ps aux | grep run_tianwang`
---
**所有功能已部署,系统运行正常!** 🎉
```
├── api/ # HTTP API 服务器
├── core/ # 核心逻辑Agent、工作流、WebSocket
├── config/ # 配置文件
├── db/ # 数据库模块
├── image/ # 图片处理模块
├── services/ # 外部服务集成
├── utils/ # 工具模块
├── skills/ # Agent 技能定义
└── run.py # 统一入口(--api-only / --tianwang / 默认 WebSocket
```

View File

@@ -1,239 +0,0 @@
# 指定客户回复触发任务 - 使用示例
## 📋 功能说明
支持**指定特定客户**回复**指定内容**时触发任务。
---
## 🎯 使用场景
### 场景 1只等小明说"好的"就发文件
**任务配置**
```json
{
"task_id": "TASK_001",
"type": "send_file_after_reply",
"customer": {
"id": "customer_123",
"name": "小明"
},
"trigger": {
"type": "specified_customer_reply",
"customer_id": "customer_123",
"customer_name": "小明",
"keyword": "好的",
"exact_match": false
},
"action": {
"type": "send_message",
"message": "这是您要的文件"
},
"priority": "normal",
"timeout_hours": 24,
"created_by": "设计师 lz"
}
```
**触发逻辑**
1. ✅ 客户 ID = `customer_123`
2. ✅ 客户名称 = `小明`
3. ✅ 消息内容包含"好的"
4. ✅ 触发任务,发送文件
**如果其他客户说"好的"**
- ❌ 不触发(客户 ID 不匹配)
**如果小明说"可以的"**
- ❌ 不触发(关键词不匹配)
---
### 场景 2精确匹配 - 只认"已付款"三个字
**任务配置**
```json
{
"task_id": "TASK_002",
"type": "send_tutorial_after_payment",
"customer": {
"id": "customer_456",
"name": "小红"
},
"trigger": {
"type": "specified_customer_reply",
"customer_id": "customer_456",
"customer_name": "小红",
"keyword": "已付款",
"exact_match": true
},
"action": {
"type": "send_message",
"message": "感谢购买!这是教程..."
}
}
```
**触发逻辑**
- ✅ 客户说"已付款" → 触发
- ❌ 客户说"我已付款了" → 不触发(不是精确匹配)
- ❌ 客户说"付款了" → 不触发(关键词不匹配)
---
### 场景 3不指定客户名称只认客户 ID
**任务配置**
```json
{
"task_id": "TASK_003",
"type": "send_welcome",
"customer": {
"id": "customer_789"
},
"trigger": {
"type": "specified_customer_reply",
"customer_id": "customer_789",
"keyword": "你好"
},
"action": {
"type": "send_message",
"message": "欢迎光临!"
}
}
```
**触发逻辑**
- ✅ 只要客户 ID 是 `customer_789`,说"你好"就触发
- ✅ 不检查客户名称
---
## 📝 API 调用示例
### 1. 创建指定客户回复任务
```bash
curl -X POST http://localhost:5678/api/task/receive \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_20260227_001",
"type": "send_file_after_reply",
"customer": {
"id": "customer_123",
"name": "小明"
},
"trigger": {
"type": "specified_customer_reply",
"customer_id": "customer_123",
"customer_name": "小明",
"keyword": "好的",
"exact_match": false
},
"action": {
"type": "send_message",
"message": "这是您要的文件"
},
"priority": "normal",
"timeout_hours": 24,
"created_by": "设计师 lz"
}'
```
### 2. 查询任务状态
```bash
curl http://localhost:5678/api/task/status/TASK_20260227_001
```
### 3. 取消任务
```bash
curl -X POST http://localhost:5678/api/task/cancel \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_20260227_001",
"reason": "客户不需要了"
}'
```
---
## 🔧 触发条件配置说明
### 字段说明
| 字段 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `type` | ✅ | 触发类型 | `specified_customer_reply` |
| `customer_id` | ✅ | 指定客户 ID | `customer_123` |
| `customer_name` | ❌ | 指定客户名称 | `小明` |
| `keyword` | ✅ | 回复关键词 | `好的` |
| `exact_match` | ❌ | 是否精确匹配 | `false`(默认) |
### exact_match 参数
- `false`(默认):模糊匹配,消息**包含**关键词即可
- 关键词:"好的"
- 匹配:"好的"、"好的谢谢"、"好的我知道了"
- `true`:精确匹配,消息**等于**关键词
- 关键词:"好的"
- 匹配:"好的"
- 不匹配:"好的谢谢"
---
## 📊 触发流程图
```
客户发送消息
检查任务列表
遍历待触发任务
┌─────────────────────────────┐
│ 1. 检查客户 ID 是否匹配? │ ← specified_customer_id
└──────────┬──────────────────┘
│ NO → 跳过此任务
│ YES
┌─────────────────────────────┐
│ 2. 检查客户名称是否匹配? │ ← specified_customer_name可选
└──────────┬──────────────────┘
│ NO → 跳过此任务
│ YES
┌─────────────────────────────┐
│ 3. 检查回复内容是否匹配? │ ← keyword + exact_match
└──────────┬──────────────────┘
│ NO → 跳过此任务
│ YES
触发任务执行
```
---
## 🐛 常见问题
### Q1: 如何只监听特定客户?
**A**: 使用 `specified_customer_reply` 触发类型,配置 `customer_id` 字段。
### Q2: 如何精确匹配某句话?
**A**: 设置 `exact_match: true`,只有消息完全等于关键词时才触发。
### Q3: 可以监听多个关键词吗?
**A**: 可以,使用 `customer_keyword` 触发类型,配置 `keywords` 数组。
### Q4: 任务超时后会自动取消吗?
**A**: 是的,默认 24 小时超时,可在任务配置中设置 `timeout_hours`
---
## 📖 完整文档
查看:`/root/ai_customer_service/ai_cs/TIANWANG_INTEGRATION.md`

View File

@@ -1,402 +0,0 @@
# 天网 AI 客服协作系统 - 完整文档
## 📋 项目概述
**目标**: 实现天网(任务调度中心)与 AI 客服的协作支持设计师在群里下发任务AI 客服自动执行。
**协作流程**:
```
设计师(群里) → 天网(任务分析) → AI 客服(执行)
↑ ↓
└←←←←←← 结果反馈 ←←←←←←←←←←┘
```
---
## 🚀 已实现的功能
### 1⃣ 任务接收接口 ✅
#### API 端点
```
POST /api/task/receive # 接收天网下发的任务
POST /api/task/cancel # 取消任务
GET /api/task/status/:id # 查询任务状态
GET /api/task/list # 任务列表
GET /api/health # 健康检查
```
#### 任务数据结构
```json
{
"task_id": "TASK_20260226_001",
"type": "send_file_after_reply",
"customer": {
"name": "小明",
"id": "customer_123"
},
"trigger": {
"type": "customer_reply",
"keyword": "好的"
},
"action": {
"type": "send_file",
"file_url": "https://xxx.com/file.zip",
"message": "这是您要的文件"
},
"priority": "normal",
"timeout_hours": 24,
"created_by": "设计师 lz"
}
```
#### 返回格式
```json
{
"code": 200,
"message": "任务接收成功",
"data": {
"task_id": "TASK_20260226_001",
"status": "pending"
}
}
```
---
### 2⃣ 任务调度系统 ✅
#### 任务状态流转
```
pending → waiting → running → completed
failed
```
#### 功能特性
- ✅ 优先级队列urgent > high > normal
- ✅ 超时处理(默认 24 小时)
- ✅ 自动重试(最多 3 次)
- ✅ 状态持久化SQLite
- ✅ 天网回调通知
---
### 3⃣ 条件监听增强 ✅
#### 触发条件类型
```python
TRIGGER_TYPES = {
"customer_reply": "客户回复指定内容",
"customer_keyword": "客户说某关键词",
"customer_payment": "客户付款",
"customer_order": "客户下单",
"time_reach": "到达指定时间",
"custom_event": "自定义事件"
}
```
#### 触发流程
```
收到客户消息
检查是否有匹配的任务
触发任务执行
自动回复客户
```
---
### 4⃣ 文件/链接发送功能 🔄
- ✅ 消息发送(已实现)
- 🔄 文件发送(待接入轻简 API
- 🔄 链接发送(已实现为基础消息)
---
## 📁 项目结构
```
ai_customer_service/ai_cs/
├── db/task_db/
│ ├── task_model.py # 任务数据模型
│ └── tasks.db # SQLite 数据库
├── core/
│ ├── task_scheduler.py # 任务调度器
│ └── websocket_client.py # WebSocket 客户端(已增强)
├── api/
│ └── http_server.py # HTTP API 服务器
├── scripts/
│ └── multi_process_launcher.py
├── run_with_tianwang.py # 天网协作版启动器
└── TIANWANG_INTEGRATION.md # 本文档
```
---
## 🚀 使用方法
### 启动天网协作版
```bash
cd /root/ai_customer_service/ai_cs
# 方式 1使用专用启动器
python3 run_with_tianwang.py
# 方式 2使用 run.py未来整合
python3 run.py --tianwang
```
### API 调用示例
#### 1. 接收任务
```bash
curl -X POST http://localhost:5678/api/task/receive \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_20260226_001",
"type": "send_file_after_reply",
"customer": {
"name": "小明",
"id": "customer_123"
},
"trigger": {
"type": "customer_reply",
"keyword": "好的"
},
"action": {
"type": "send_message",
"message": "这是您要的文件"
},
"priority": "normal",
"timeout_hours": 24,
"created_by": "设计师 lz"
}'
```
#### 2. 查询任务状态
```bash
curl http://localhost:5678/api/task/status/TASK_20260226_001
```
#### 3. 取消任务
```bash
curl -X POST http://localhost:5678/api/task/cancel \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_20260226_001",
"reason": "客户已退款"
}'
```
---
## 📊 数据库表结构
```sql
CREATE TABLE tasks (
task_id TEXT PRIMARY KEY,
type TEXT NOT NULL,
customer_name TEXT,
customer_id TEXT,
trigger_type TEXT,
trigger_keyword TEXT,
trigger_keywords TEXT, -- JSON array
action_type TEXT,
action_file_url TEXT,
action_message TEXT,
priority TEXT DEFAULT 'normal',
timeout_hours INTEGER DEFAULT 24,
status TEXT DEFAULT 'pending',
retry_count INTEGER DEFAULT 0,
max_retry INTEGER DEFAULT 3,
created_at TEXT,
created_by TEXT,
triggered_at TEXT,
completed_at TEXT,
error_message TEXT,
result TEXT -- JSON
)
```
---
## 🔧 配置说明
### 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `HTTP_API_HOST` | HTTP 服务器地址 | 0.0.0.0 |
| `HTTP_API_PORT` | HTTP 服务器端口 | 5678 |
| `TASK_DB_PATH` | 任务数据库路径 | db/task_db/tasks.db |
### 天网回调配置
`core/task_scheduler.py` 中配置天网回调 URL
```python
async def _notify_tianwang(self, task_id: str, status: str, error: str = None):
async with httpx.AsyncClient() as client:
await client.post(
'http://tianwang-server/api/task/callback', # ← 修改这里
json={
'task_id': task_id,
'status': status,
'error': error,
'timestamp': datetime.now().isoformat()
}
)
```
---
## 📝 使用场景示例
### 场景 1客户说"好的"后发送文件
**天网下发任务**:
```json
{
"task_id": "TASK_001",
"type": "send_file_after_reply",
"customer": {"id": "customer_123"},
"trigger": {"type": "customer_reply", "keyword": "好的"},
"action": {
"type": "send_file",
"file_url": "https://xxx.com/file.zip",
"message": "这是您要的文件"
}
}
```
**流程**:
1. 客户在群里说"好的"
2. AI 客服检测到匹配任务
3. 自动发送文件给客户
4. 任务标记为 completed
---
### 场景 2客户付款后发送教程
**天网下发任务**:
```json
{
"task_id": "TASK_002",
"type": "send_tutorial_after_payment",
"customer": {"id": "customer_456"},
"trigger": {"type": "customer_payment"},
"action": {
"type": "send_message",
"message": "感谢购买!这是使用教程:..."
}
}
```
---
### 场景 3定时发送问候
**天网下发任务**:
```json
{
"task_id": "TASK_003",
"type": "send_greeting",
"customer": {"id": "customer_789"},
"trigger": {"type": "time_reach", "time": "2026-02-27 09:00:00"},
"action": {
"type": "send_message",
"message": "早上好!有什么可以帮您?"
}
}
```
---
## 🔍 监控与日志
### 查看任务日志
```bash
journalctl -u ai-cs -f | grep "任务"
```
### 查询数据库
```bash
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db
# 查看所有任务
SELECT task_id, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 10;
# 查看失败任务
SELECT task_id, error_message FROM tasks WHERE status='failed';
```
### 统计信息
```bash
curl http://localhost:5678/api/task/stats
```
---
## ⚠️ 注意事项
1. **HTTP 端口**: 默认 5678确保端口未被占用
2. **数据库路径**: 确保有写权限
3. **天网回调**: 需要配置正确的回调 URL
4. **任务超时**: 默认 24 小时,根据业务需求调整
5. **重试次数**: 默认 3 次,避免无限重试
---
## 🐛 故障排查
### 任务未触发
```bash
# 1. 检查任务状态
curl http://localhost:5678/api/task/status/TASK_ID
# 2. 查看日志
journalctl -u ai-cs -f | grep "任务触发"
# 3. 检查触发条件是否匹配
# 确认客户消息包含触发关键词
```
### HTTP API 无法访问
```bash
# 1. 检查端口
netstat -tlnp | grep 5678
# 2. 检查防火墙
firewall-cmd --list-ports
# 3. 重启服务
systemctl restart ai-cs
```
### 任务执行失败
```bash
# 1. 查看错误信息
curl http://localhost:5678/api/task/status/TASK_ID
# 2. 查看日志
journalctl -u ai-cs -f | grep "任务执行失败"
# 3. 手动重试
# 取消任务后重新下发
```
---
## 📞 技术支持
如有问题请联系:
- 文档:`/root/ai_customer_service/ai_cs/TIANWANG_INTEGRATION.md`
- 日志:`journalctl -u ai-cs -f`
- 数据库:`/root/ai_customer_service/ai_cs/db/task_db/tasks.db`

View File

@@ -1,12 +0,0 @@
# 归档文件
此目录存放暂未使用或已替代的旧文件,主流程不依赖。
| 文件 | 说明 |
|------|------|
| service_meitu.py | 美图服务(未接入) |
| service_vectorizer.py | 矢量化服务(未接入) |
| view_chats.py | 旧版聊天查看(已由 chat_log_viewer 替代) |
| test_import.py | 导入测试 |
| test_battle.py | 压价话术测试 |
| viewer_out.txt | 临时输出 |

View File

@@ -1,298 +0,0 @@
"""
AI 左右互搏测试工具
客服AI真实 vs 买家AI模拟
用来调试客服AI的回复质量不需要真实客户
"""
import asyncio
import os
import random
import httpx
from openai import AsyncOpenAI
from dotenv import load_dotenv
from pydantic_ai_agent import CustomerServiceAgent, CustomerMessage
load_dotenv()
WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
async def notify_wechat(content: str):
"""发送企业微信机器人通知"""
try:
async with httpx.AsyncClient(timeout=10) as client:
await client.post(WECHAT_WEBHOOK, json={
"msgtype": "text",
"text": {"content": content}
})
except Exception as e:
print(f"[通知] 企业微信发送失败: {e}")
# ========== 颜色输出 ==========
RESET = "\033[0m"
BLUE = "\033[94m" # 买家
GREEN = "\033[92m" # 客服
YELLOW = "\033[93m" # 系统提示
GRAY = "\033[90m" # 分隔线
def print_buyer(msg):
print(f"{BLUE}【买家】{msg}{RESET}")
def print_shop(msg):
print(f"{GREEN}【客服】{msg}{RESET}")
def print_system(msg):
print(f"{YELLOW}{msg}{RESET}")
def print_sep():
print(f"{GRAY}{'' * 50}{RESET}")
# ========== 买家人设配置 ==========
BUYER_PERSONAS = {
"普通买家": """你是一个普通淘宝买家,想找一张图的高清版本。
行为:先打招呼,发图问价,可能小砍一次价,觉得合适就说要拍了。
说话简短自然像手机上发消息1-2句话。不要太正式。""",
"砍价客": """你是个爱砍价的淘宝买家,总想便宜一点。
行为问价后嫌贵砍价2-3次最后可能接受也可能走人。
说话直接,喜欢说"能不能便宜点""太贵了""别家更便宜"""",
"爱问细节": """你是个很谨慎的买家,买东西前喜欢问清楚。
行为:先问在不在,问能不能找到,问格式,问效果,问不满意能退吗,最后才考虑下单。
说话有点啰嗦,喜欢多问几个问题。""",
"快节奏": """你是个很忙的买家,说话简短,不废话。
行为:直接发图问多少钱,价格合适立刻说拍了,不合适直接走。
每次只说3-5个字极度简短。""",
"售后投诉": """你是个已经付款的买家,但对收到的图片不满意,想要退款或重做。
行为:先说拿到图了但效果不行,描述哪里不满意(模糊/颜色不对/尺寸不够),
要求重做或者退款,态度有点不耐烦,反复追问进度。
说话带点情绪,但不至于骂人,就是那种"花钱了结果不行"的委屈感。""",
"多图打包": """你是个需要批量处理图片的买家,手头有好几张图要找高清版。
行为:先问在不在,然后说有多张图要处理,询问能不能打包便宜点,
逐步发图或询问价格,跟客服商量总价,最终决定下单还是再想想。
说话随意,像在商量事情,不太在意每张的价格,更关注总价合不合适。""",
"大批量砍价": """你是个手头有十几张图要处理的买家,量大,觉得自己有底气砍价。
行为:上来就说"我有很多图,你们量大能便宜吗"
告知大概数量10-20张用量大作为筹码反复压价
"做完这批还有下一批,能不能给个长期价""别家给我打6折了"
如果客服给的总价还算合理就下单,否则拉锯几个来回。
说话有点强势,觉得自己是大客户,应该享受优惠。""",
"要分层PSD": """你是个做设计的买家需要带分层的PSD格式不是普通jpg。
行为:先问在不在,发图后问"这个能出PSD吗""要分层的那种"
追问能不能保留图层、格式是不是真的PSD听到价格后可能嫌贵问能不能便宜
最终决定下单或者放弃。
说话带点专业感,会说"图层""分层""PSD""源文件"之类的词。""",
"问尺寸分辨率": """你是个有印刷需求的买家,非常在意图片的尺寸和分辨率。
行为:发图后先不问价,反复问"这个能做多大""分辨率能到300dpi吗""印出来会不会模糊"
问完尺寸再问价格,如果客服说能满足需求就考虑下单,否则犹豫很久。
说话里经常出现"厘米""像素""dpi""印刷""大图"""",
"问颜色效果": """你是个对颜色很挑剔的买家,担心高清版颜色和原图有色差。
行为:发图后问"颜色会变吗""饱和度能保持吗""跟原图一样的色调吧"
让客服保证颜色效果,追问不满意能不能改,反复确认才肯下单。
说话谨慎,总要客服给"保证",但说话不凶,就是很在意品质。""",
"来源质疑": """你是个对店铺服务有疑虑的买家,总觉得有猫腻,想搞清楚原理。
行为:问"你们是怎么找的""原图是从哪里来的""是AI弄的吗还是真的原图"
追问来源和方法,对客服的回答将信将疑,继续追问,
最终可能被说服下单,也可能觉得不靠谱走人。
说话带点怀疑,喜欢反问,但不是来找茬的,是真的想搞清楚。""",
}
# 用于测试的图片URL
TEST_IMAGE_URLS = [
"https://img.alicdn.com/imgextra/i1/O1CN01OilxfD1kr8tM4Ugg2_!!4611686018427387680-0-amp.jpg",
]
class BuyerAgent:
"""模拟买家的AI"""
def __init__(self, persona_name: str = "普通买家"):
self.client = AsyncOpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
self.model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
self.persona_name = persona_name
self.persona = BUYER_PERSONAS.get(persona_name, BUYER_PERSONAS["普通买家"])
self.history = []
self.image_sent = False
self.rounds = 0
async def next_message(self, shop_reply: str = None) -> str:
"""根据客服回复,生成下一条买家消息"""
self.rounds += 1
if shop_reply:
self.history.append({"role": "assistant", "content": f"客服说:{shop_reply}"})
# 第一轮:打招呼或直接发问
if self.rounds == 1:
first_msgs = ["在不在", "在吗", "你好", "有人吗", "亲在吗"]
msg = random.choice(first_msgs)
self.history.append({"role": "user", "content": msg})
return msg
# 第二轮:发图+问有没有
if self.rounds == 2 and not self.image_sent:
self.image_sent = True
url = random.choice(TEST_IMAGE_URLS)
msg = f"有吗#*#{url}"
self.history.append({"role": "user", "content": msg})
return msg
# 后续让AI根据对话历史自由生成
system = f"""{self.persona}
你正在和一家找图店的客服聊天,对话历史如下。
请根据你的人设生成下一条消息,简短自然,像真人发消息。
如果对话已经快结束超过10轮可以说"好的拍了""算了不要了"结束对话。
只输出你要发的消息内容,不要加任何前缀说明。"""
resp = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system},
*self.history,
{"role": "user", "content": "(请生成你的下一条消息)"}
],
max_tokens=80,
temperature=0.9,
)
msg = resp.choices[0].message.content.strip()
self.history.append({"role": "user", "content": msg})
return msg
async def run_battle(persona_name: str = "普通买家", max_rounds: int = 8):
"""运行一场对话模拟"""
print_sep()
print_system(f" 开始模拟 | 买家人设:{persona_name} | 最多 {max_rounds}")
print_sep()
# 初始化双方
buyer = BuyerAgent(persona_name)
shop = CustomerServiceAgent()
buyer_id = "test_buyer_001"
shop_id = "test_shop_001"
shop_reply = None
for i in range(max_rounds):
# 买家发消息
buyer_msg = await buyer.next_message(shop_reply)
print_buyer(buyer_msg)
# 判断对话是否结束
end_words = ["算了", "不要了", "不买了", "拍了", "好的拍了", "下单了", "谢谢"]
if any(w in buyer_msg for w in end_words):
print_system(f"\n 对话结束(第 {i+1} 轮)")
break
# 构建消息对象
customer_msg = CustomerMessage(
msg_id=f"test_{i}",
acc_id=shop_id,
msg=buyer_msg,
from_id=buyer_id,
from_name="测试买家",
cy_id=buyer_id,
acc_type="AliWorkbench",
msg_type=0,
cy_name="测试买家",
goods_name="模糊图清晰处理专业代找原图素材淘宝图片找图修复服务",
)
# 含图片URL时先打印等待语模拟真实客服的即时回应
if "#*#" in buyer_msg or (buyer_msg.startswith(("http://", "https://")) and any(
h in buyer_msg for h in ("alicdn.com", "imgextra", ".jpg", ".jpeg", ".png")
)):
print_shop("我找一下看看")
print()
# 客服AI处理
response = await shop.process_message(customer_msg)
if response.need_transfer:
print_shop("[转人工]")
print_system("\n 客服决定转人工,对话结束")
break
shop_reply = response.reply if response.should_reply else None
# 过滤 AI 误输出的内部独白(与生产环境保持一致)
nonsense_patterns = [
"无需", "流程已完成", "不需要回复", "无需额外", "已完成",
"无需回复", "不需要额外", "已经完成", "无需再", "操作已完成",
"任务完成", "流程完成", "记录完成", "报价已",
]
if shop_reply and any(p in shop_reply for p in nonsense_patterns):
print_system(f" [已拦截无效回复: {shop_reply}]")
shop_reply = None
if shop_reply:
print_shop(shop_reply)
else:
print_system(" (客服决定不回复)")
shop_reply = ""
print()
await asyncio.sleep(0.3)
print_sep()
print_system(" 模拟结束")
print_sep()
async def main():
import sys
personas = list(BUYER_PERSONAS.keys())
print(f"\n{YELLOW}{'=' * 50}")
print(" AI 左右互搏测试工具")
print(f"{'=' * 50}{RESET}")
print(f"{GRAY}用法: python test_battle.py [人设编号|all]")
print(f" 1=普通买家 2=砍价客 3=爱问细节 4=快节奏")
print(f" 5=售后投诉 6=多图打包 7=大批量砍价 8=要分层PSD")
print(f" 9=问尺寸分辨率 10=问颜色效果 11=来源质疑 0=全部{RESET}\n")
arg = sys.argv[1] if len(sys.argv) > 1 else "1"
if arg == "0" or arg == "all":
for persona in personas:
await run_battle(persona, max_rounds=14)
print()
await asyncio.sleep(1)
else:
try:
idx = int(arg) - 1
persona = personas[idx] if 0 <= idx < len(personas) else personas[0]
except ValueError:
persona = personas[0]
await run_battle(persona, max_rounds=14)
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as e:
err_str = str(e)
if "AccountOverdueError" in err_str or "overdue" in err_str.lower():
msg = "⚠️ 火山引擎 API 欠费,请立即充值!\n地址https://console.volcengine.com/ark"
else:
msg = f"⚠️ 测试脚本异常退出:{err_str[:200]}"
print(f"\n[通知] {msg}")
asyncio.run(notify_wechat(msg))
raise

View File

@@ -1,13 +0,0 @@
import traceback
try:
from pydantic_ai_agent import CustomerServiceAgent, CustomerMessage
print("✓ Agent 模块导入成功")
# 尝试初始化
agent = CustomerServiceAgent()
print("✓ Agent 初始化成功")
except Exception as e:
print(f"✗ 导入或初始化失败: {e}")
traceback.print_exc()

View File

@@ -1,119 +0,0 @@
"""
聊天记录查看工具
用法:
python view_chats.py # 列出所有客户
python view_chats.py <客户ID> # 查看某客户的全部对话
python view_chats.py <客户ID> --today # 只看今天
python view_chats.py --search <关键词> # 全文搜索
"""
import sys
import argparse
from chat_log_db import get_customers, get_conversation, get_conversation_today, search_messages
# ANSI 颜色
GREEN = "\033[92m"
BLUE = "\033[94m"
YELLOW = "\033[93m"
GRAY = "\033[90m"
RESET = "\033[0m"
BOLD = "\033[1m"
def list_customers():
customers = get_customers(limit=200)
if not customers:
print("暂无聊天记录")
return
print(f"\n{BOLD}{'='*60}{RESET}")
print(f"{BOLD} 客户聊天记录总览 共 {len(customers)} 位客户{RESET}")
print(f"{BOLD}{'='*60}{RESET}")
print(f" {'客户ID':<22} {'昵称':<12} {'平台':<15} {'消息数':>6} {'最后联系'}")
print(f" {'-'*22} {'-'*12} {'-'*15} {'-'*6} {'-'*19}")
for c in customers:
cid = c["customer_id"][:20]
name = (c["customer_name"] or "未知")[:10]
plat = (c["platform"] or "未知")[:13]
total = c["total_msgs"]
last = c["last_time"][:16]
print(f" {YELLOW}{cid:<22}{RESET} {name:<12} {GRAY}{plat:<15}{RESET} {total:>6}{last}")
print(f"\n{GRAY}查看某客户对话python view_chats.py <客户ID>{RESET}\n")
def show_conversation(customer_id: str, today_only: bool = False):
if today_only:
records = get_conversation_today(customer_id)
label = "今日对话"
else:
records = get_conversation(customer_id, limit=300)
label = "全部对话"
if not records:
print(f" {GRAY}暂无记录{RESET}")
return
print(f"\n{BOLD}{'='*60}{RESET}")
print(f"{BOLD} 客户:{customer_id} {label}{len(records)}{RESET}")
print(f"{BOLD}{'='*60}{RESET}\n")
last_date = ""
for r in records:
ts = r["timestamp"]
date = ts[:10]
time = ts[11:16]
msg = r["message"]
direction = r["direction"]
if date != last_date:
print(f" {GRAY}── {date} ──────────────────────────────{RESET}")
last_date = date
if direction == "in":
# 客户消息,左对齐蓝色
print(f" {GRAY}{time}{RESET} {BLUE}【客户】{RESET} {msg}")
else:
# 客服回复,右对齐绿色
print(f" {GRAY}{time}{RESET} {GREEN}【客服】{RESET} {msg}")
print()
def show_search(keyword: str):
results = search_messages(keyword, limit=50)
if not results:
print(f" 未找到包含"{keyword}"的消息")
return
print(f"\n{BOLD}搜索:"{keyword}"{len(results)} 条结果{RESET}\n")
for r in results:
direction = "客户" if r["direction"] == "in" else "客服"
color = BLUE if r["direction"] == "in" else GREEN
print(f" {GRAY}{r['timestamp'][:16]}{RESET} {YELLOW}{r['customer_id'][:20]}{RESET} {color}[{direction}]{RESET} {r['message']}")
print()
def main():
parser = argparse.ArgumentParser(
description="查看按用户分开的聊天记录",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("customer_id", nargs="?", help="客户ID不填则列出所有客户")
parser.add_argument("--today", action="store_true", help="只显示今天的对话")
parser.add_argument("--search", metavar="关键词", help="全文搜索所有消息")
args = parser.parse_args()
if args.search:
show_search(args.search)
elif args.customer_id:
show_conversation(args.customer_id, today_only=args.today)
else:
list_customers()
if __name__ == "__main__":
main()

View File

@@ -1,24 +0,0 @@

────────────────────────────────────────────────────────────
 对话记录 test_user_001 4 条)
────────────────────────────────────────────────────────────
──────────────────── 2026-02-25 ────────────────────
17:44 买家
 你好,想问下图片处理多少钱 
17:44 客服
python : Traceback (most recent call last):
所在位置 C:\Users\jimi\AppData\Local\Temp\ps-script-7c6a5466-18ca-4772-ac44-a07a3d10df05.ps1:75 字符: 19
+ ... d d:\Terminator; python chat_log_viewer.py test_user_001 2>&1 | Out-F ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (Traceback (most recent call last)::String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
File "D:\Terminator\chat_log_viewer.py", line 233, in <module>
cmd_show_conversation(args[0])
File "D:\Terminator\chat_log_viewer.py", line 127, in cmd_show_conversation
print_bubble(m["direction"], m["message"], ts)
File "D:\Terminator\chat_log_viewer.py", line 80, in print_bubble
print(f" {GREEN}\u25b6 {line}{RESET}")
UnicodeEncodeError: 'gbk' codec can't encode character '\u25b6' in position 9: illegal multibyte sequence

View File

@@ -1,9 +1,10 @@
{
"daily": {
"2026-02-26": 1.4850000000000005,
"2026-02-27": 1.3200000000000007
"2026-02-27": 1.3200000000000007,
"2026-02-28": 0.25999999999999995
},
"monthly": {
"2026-02": 2.8050000000000015
"2026-02": 3.0650000000000017
}
}

View File

@@ -1,10 +0,0 @@
# 设计师在线查询 API本端接入
- **地址**`.env``DESIGNER_ROSTER_API`,如 `http://huichang.online:8001/online`
- **方法**GET
- **返回**`{online_users: ["lz", "ZuoWei"], ...}`,本端用 `online_users` 同步本地后派单
## 调用时机
- **转人工时**按需 GET 一次,不轮询
- 无人在线时:回退到 `transfer_groups.json` 静态配置,并发送企微「谁在线啊」提醒

View File

@@ -1,33 +0,0 @@
# 设计师在线查询服务 - 需求(给另一台 AI
## 你要做的
建库,从企微群解析「上线」「下线」消息并存储,提供 **GET 接口** 返回当前在线设计师名单。
## 接口
- **方法**GET
- **路径**:如 `/online`
## 返回格式(本端已适配)
```json
{
"online_count": 2,
"online_users": ["lz", "ZuoWei"],
"update_time": "2026-02-26 16:30:00"
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| online_users | 是 | 当前在线设计师名单,对应 init_designer_roster 的 wechat_user_id |
## 你这边的逻辑
1. 企微群消息 → 解析「上线」/「下线」→ 存库
2. 接口从库查当前在线名单,按 `online_users` 返回
## 调用方
本端在**转人工时**按需 GET 一次,用 `online_users` 同步本地后派单。无人在线时发企微「谁在线啊」。

View File

@@ -1,4 +1,4 @@
# 配置文件
# 配置文件说明
## transfer_groups.json
@@ -18,19 +18,47 @@
---
## 设计师派单SQLite,可选
## 设计师派单SQLite
同一设计师在不同店铺对应不同 group_id转人工时按需查询在线状态从在线设计师中轮询派单。
**初始化数据**
### 数据库
路径: `db/designer_roster_db/roster.db`
| 表 | 说明 |
|---|------|
| `designers` | 设计师name, wechat_user_id|
| `designer_shops` | 设计师在某店铺的 group_id同一人不同店铺不同分组|
| `designer_online` | 在线状态(转人工时按需查询外部 API 同步)|
### 初始化数据
```bash
python scripts/init_designer_roster.py example # 写入示例
python scripts/init_designer_roster.py list # 查看当前数据
```
**数据库**`db/designer_roster_db/roster.db`
- `designers`设计师name, wechat_user_id
- `designer_shops`:设计师在某店铺的 group_id同一人不同店铺不同分组
- `designer_online`:在线状态(转人工时按需查询外部 API 同步)
### 在线查询 API
**接入**`.env` 配置 `DESIGNER_ROSTER_API`(如 `http://xxx/online`),转人工时 GET 一次,用 `online_users` 同步。无人在线时发企微「谁在线啊」
`.env` 配置 `DESIGNER_ROSTER_API`(如 `http://huichang.online:8001/online`
**接口**: GET返回格式
```json
{
"online_count": 2,
"online_users": ["lz", "ZuoWei"],
"update_time": "2026-02-26 16:30:00"
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `online_users` | 是 | 当前在线设计师名单,对应 `wechat_user_id` |
**调用时机**: 转人工时按需 GET 一次,不轮询。无人在线时回退到 `transfer_groups.json` 静态配置,并发企微「谁在线啊」提醒。
### 对接方要求(外部 AI 服务)
对端需实现:企微群消息 → 解析「上线」/「下线」→ 存库 → 提供 GET `/online` 接口,按上述格式返回在线名单。

View File

@@ -19,6 +19,8 @@ from dotenv import load_dotenv
load_dotenv()
from services.service_tuhui_upload import upload_to_tuhui
from core.workflow_router import get_workflow_router
from core.workflow_router import get_workflow_router
# ========== 企业微信通知 ==========
_WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
@@ -200,9 +202,9 @@ class CustomerServiceAgent:
system_prompt=self._get_similar_prompt()
)
# 工作流程路由器
self.workflow_router = get_workflow_router()
self.workflow_router = get_workflow_router()
self.agent_order = Agent(
self.agent_order = Agent(
model=model,
deps_type=AgentDeps,
system_prompt=self._get_order_prompt()
@@ -260,7 +262,6 @@ self.agent_order = Agent(
# 在 workflow 里创建待处理任务(付款后自动触发 Gemini
try:
from core.workflow import workflow
from core.workflow_router import get_workflow_router
await workflow.image_analysis_result(
customer_id=ctx.deps.from_id,
image_url=image_url,
@@ -455,21 +456,20 @@ from core.workflow_router import get_workflow_router
@self.agent.tool
async def process_image_gemini(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
"""
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
处理完成后会自动发图给客户。
"""
try:
from config.config import IMAGE_MODULE_ENABLED
if not IMAGE_MODULE_ENABLED:
return "现在处理模块暂时暂停,先不自动作图"
except Exception:
return "现在处理模块暂时暂停,先不自动作图"
"""
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
处理完成后会自动发图给客户。
"""
cid = customer_id or ctx.deps.from_id
try:
from core.workflow import workflow
from core.workflow_router import get_workflow_router
ok = await workflow.trigger_processing_on_payment(
customer_id=cid,
acc_id=ctx.deps.acc_id,
@@ -518,26 +518,8 @@ from core.workflow_router import get_workflow_router
@self.agent_processing.tool
async def process_image_gemini_run(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
try:
from config.config import IMAGE_MODULE_ENABLED
if not IMAGE_MODULE_ENABLED:
return "现在处理模块暂时暂停"
except Exception:
return "现在处理模块暂时暂停"
cid = customer_id or ctx.deps.from_id
try:
from core.workflow import workflow
from core.workflow_router import get_workflow_router
ok = await workflow.trigger_processing_on_payment(
customer_id=cid,
acc_id=ctx.deps.acc_id,
acc_type=ctx.deps.platform,
)
if ok:
return "已安排"
return "暂无待处理图片"
except Exception as e:
return f"触发作图失败: {e}"
"""触发 Gemini 作图处理processing agent 专用入口)。"""
return await process_image_gemini(ctx, customer_id)
@self.agent_similar.tool
async def recommend_similar(ctx: RunContext[AgentDeps], hint: str = "") -> str:
@@ -986,7 +968,7 @@ from core.workflow_router import get_workflow_router
- 输出不超过1句话"""
def _get_customer_profile_context(self, customer_id: str) -> str:
"""从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测。"""
"""从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测、近期对话"""
try:
from db.customer_db import db
profile = db.get_customer(customer_id)
@@ -994,41 +976,95 @@ from core.workflow_router import get_workflow_router
if profile.blacklist:
return f"【⚠️黑名单客户】原因:{profile.blacklist_reason or '已标记'},请转接人工处理,不要自动回复"
parts = []
lines = []
lines.append("=== 客户档案 ===")
# 基础信息
basic_info = []
basic_info.append(f"客户ID: {customer_id}")
basic_info.append(f"姓名: {profile.name or '未知'}")
if profile.email:
parts.append(f"邮箱:{profile.email}")
basic_info.append(f"邮箱: {profile.email}")
if profile.phone:
parts.append(f"电话:{profile.phone}")
basic_info.append(f"电话: {profile.phone}")
if profile.wechat:
parts.append(f"微信:{profile.wechat}")
if profile.personality:
parts.append(f"性格:{'/'.join(profile.personality)}")
basic_info.append(f"微信: {profile.wechat}")
lines.append(" | ".join(basic_info))
# 消费分析
consume_info = []
consume_info.append(f"客户等级: {profile.customer_level}")
if profile.vip:
consume_info.append("VIP客户")
consume_info.append(f"总订单: {profile.total_orders}")
consume_info.append(f"总消费: {profile.total_spent}")
if profile.total_orders > 0:
parts.append(f"历史下单:{profile.total_orders}")
consume_info.append(f"客单价: {profile.total_spent // profile.total_orders}")
lines.append("--- 消费分析 ---")
lines.append(" | ".join(consume_info))
# 报价历史
price_info = []
if profile.vip_custom_price:
parts.append(f"VIP专属价:{profile.vip_custom_price}元(直接这个价")
elif profile.last_price:
parts.append(f"上次报价:{profile.last_price}")
price_info.append(f"VIP专属价: {profile.vip_custom_price}元(直接这个价)")
if profile.last_price:
price_info.append(f"上次报价: {profile.last_price}")
if profile.lowest_price_accepted:
parts.append(f"最低接受价:{profile.lowest_price_accepted}")
price_info.append(f"历史最低成交: {profile.lowest_price_accepted}")
if profile.discount_given_count:
parts.append(f"历史让价:{profile.discount_given_count}")
if getattr(profile, "last_quote_no_convert", False):
parts.append("【报价策略】上次报价未成交本次可适当降低5-10元促成交")
price_info.append(f"历史让价: {profile.discount_given_count}")
if profile.price_sensitivity:
parts.append(f"价格敏感:{profile.price_sensitivity}")
price_info.append(f"价格敏感度: {profile.price_sensitivity}")
if getattr(profile, "last_quote_no_convert", False):
price_info.append("【策略】上次报价未成交本次可降5-10元")
if price_info:
lines.append("--- 报价历史 ---")
lines.append(" | ".join(price_info))
# 性格与决策
personality_info = []
if profile.personality:
personality_info.append(f"性格: {'/'.join(profile.personality)}")
if profile.decision_speed:
parts.append(f"决策速度:{profile.decision_speed}")
if profile.last_image_url:
parts.append(f"上次发图:{profile.last_image_url}")
if profile.processing_status:
parts.append(f"当前任务:{profile.processing_status}")
personality_info.append(f"决策速度: {profile.decision_speed}")
if profile.communication_prefer:
personality_info.append(f"沟通偏好: {profile.communication_prefer}")
if personality_info:
lines.append("--- 性格特征 ---")
lines.append(" | ".join(personality_info))
# 图片习惯
image_info = []
image_info.append(f"累计发图: {profile.total_images_sent}")
if profile.complexity_history:
avg_complexity = self._calc_avg_complexity(profile.complexity_history)
image_info.append(f"平均复杂度: {avg_complexity}")
if profile.image_type_history:
from collections import Counter
top_types = Counter(profile.image_type_history).most_common(3)
types_str = "".join(f"{t}({c}次)" for t, c in top_types)
image_info.append(f"常见类型: {types_str}")
if profile.preferred_format:
parts.append(f"格式偏好:{profile.preferred_format}")
if profile.bulk_potential == "":
parts.append("批量潜力:有(可主动推打包价)")
if profile.upsell_opportunity:
parts.append(f"加购机会:{'/'.join(profile.upsell_opportunity)}")
image_info.append(f"格式偏好: {profile.preferred_format}")
if profile.preferred_size:
image_info.append(f"尺寸要求: {profile.preferred_size}")
if profile.last_image_url:
image_info.append(f"最近发图: {profile.last_image_url[:60]}...")
lines.append("--- 图片习惯 ---")
lines.append(" | ".join(image_info))
# 当前任务状态
if profile.processing_status:
task_info = []
task_info.append(f"状态: {profile.processing_status}")
if profile.processing_image_url:
task_info.append(f"处理中: {profile.processing_image_url[:40]}...")
if profile.expected_done_at:
task_info.append(f"预计完成: {profile.expected_done_at}")
lines.append("--- 当前任务 ---")
lines.append(" | ".join(task_info))
# 上次对话摘要
if profile.last_conversation_summary:
time_str = ""
if profile.last_conversation_time:
@@ -1042,38 +1078,54 @@ from core.workflow_router import get_workflow_router
time_str = f"{h}小时前)" if h > 0 else "(刚刚)"
except Exception:
pass
parts.append(f"上次对话{time_str}:{profile.last_conversation_summary}")
lines.append(f"--- 上次对话 {time_str} ---")
lines.append(profile.last_conversation_summary)
# 个性化:语气与报价策略
# 个性化回复策略
hints = []
if profile.personality:
if "爽快" in profile.personality:
hints.append("回复简洁直接,废话")
hints.append("回复简洁直接,废话,快速报价")
if "砍价" in profile.personality or "砍价狂" in profile.personality:
hints.append("报价时强调性价比,只让价一次")
hints.append("报价时强调性价比,只让价一次,第二次引导去 xinhui.cloud")
if "纠结" in profile.personality or "墨迹" in profile.personality:
hints.append("耐心一点,可多给一点说明")
hints.append("多给一点说明,耐心回答")
if profile.price_sensitivity == "":
hints.append("报价时顺带提「满意再拍」降低顾虑")
if profile.decision_speed == "":
hints.append("重点推快速成交,少铺垫")
hints.append("直接报价推成交,少铺垫")
if profile.total_orders > 0 and profile.decision_speed == "":
hints.append("老客爽快,直接报价成交")
if hints:
parts.append(f"【回复风格】{'; '.join(hints)}")
lines.append("--- 回复策略 ---")
lines.append("".join(hints))
# 主动预测:批量/加急
# 主动推荐
proactive = []
if profile.bulk_potential == "" or (profile.total_images_sent or 0) >= 2:
proactive.append("主动问「要做多张吗,多张有优惠」")
if profile.total_orders > 0 and profile.decision_speed == "":
proactive.append("老客爽快,直接推成交")
proactive.append("可问「要做多张吗,多张有优惠」")
if profile.upsell_opportunity:
proactive.append(f"加购机会: {''.join(profile.upsell_opportunity)}")
if proactive:
parts.append(f"【主动推荐】{'; '.join(proactive)}")
lines.append("--- 主动推荐 ---")
lines.append("".join(proactive))
if parts:
return "【该客户历史信息】" + " | ".join(parts)
return "\n".join(lines)
except Exception as e:
print(f"[Agent] 获取客户画像失败: {e}")
return ""
def _calc_avg_complexity(self, complexity_history: list) -> str:
"""计算平均复杂度"""
if not complexity_history:
return "未知"
level_map = {"simple": 1, "normal": 2, "complex": 3, "hard": 4}
label_map = {1: "简单", 2: "一般", 3: "复杂", 4: "很复杂"}
try:
avg = sum(level_map.get(c, 2) for c in complexity_history) / len(complexity_history)
return label_map.get(round(avg), "一般")
except Exception:
pass
return ""
return "一般"
def _get_refusal_context_hint(self, customer_id: str, current_msg: str, profile_context: str) -> str:
"""
@@ -1203,7 +1255,6 @@ from core.workflow_router import get_workflow_router
# 已付款:触发 Gemini 作图
try:
from core.workflow import workflow
from core.workflow_router import get_workflow_router
asyncio.create_task(workflow.trigger_processing_on_payment(
customer_id=message.from_id,
acc_id=message.acc_id,
@@ -1816,6 +1867,51 @@ from core.workflow_router import get_workflow_router
return prompt
async def _handle_image_workflow(self, message: str, data: dict, image_urls: list) -> bool:
"""处理图片工作流(根据客户说的话判断执行哪种工作流)"""
if not image_urls:
return False
workflow_type, confidence = self.workflow_router.detect_workflow(message)
customer_id = data.get('from_id')
acc_id = data.get('acc_id', '')
acc_type = data.get('acc_type', 'AliWorkbench')
image_url = image_urls[0]
print(f"[Agent] 检测到工作流类型:{workflow_type} (置信度:{confidence})")
if workflow_type == "find_image":
print(f"[Agent] 执行查找图片工作流 | 客户:{customer_id}")
from core.workflow import workflow
return await workflow.find_image_workflow(
customer_id=customer_id,
image_url=image_url,
acc_id=acc_id,
acc_type=acc_type
)
elif workflow_type == "process_image":
print(f"[Agent] 执行处理图片工作流 | 客户:{customer_id}")
from core.workflow import workflow
return await workflow.process_image_workflow(
customer_id=customer_id,
image_url=image_url,
acc_id=acc_id,
acc_type=acc_type
)
elif workflow_type == "transfer_human":
print(f"[Agent] 执行转人工派单工作流 | 客户:{customer_id}")
from core.workflow import workflow
return await workflow.transfer_to_designer_workflow(
customer_id=customer_id,
image_url=image_url,
acc_id=acc_id,
acc_type=acc_type,
reason="客户主动要求转人工"
)
return False
async def test_agent():
"""测试 Agent"""
@@ -1842,65 +1938,3 @@ async def test_agent():
if __name__ == "__main__":
import asyncio
asyncio.run(test_agent())
async def _handle_image_workflow(self, message: str, data: dict, image_urls: list) -> bool:
"""
处理图片工作流(根据客户说的话判断执行哪种工作流)
Args:
message: 客户消息
data: 消息数据
image_urls: 图片 URL 列表
Returns:
bool: 是否已处理
"""
if not image_urls:
return False
# 检测工作流类型
workflow_type, confidence = self.workflow_router.detect_workflow(message)
customer_id = data.get('from_id')
acc_id = data.get('acc_id', '')
acc_type = data.get('acc_type', 'AliWorkbench')
image_url = image_urls[0] # 取第一张图
print(f"[Agent] 检测到工作流类型:{workflow_type} (置信度:{confidence})")
if workflow_type == "find_image":
# 工作流 1查找图片
print(f"[Agent] 执行查找图片工作流 | 客户:{customer_id}")
success = await workflow.find_image_workflow(
customer_id=customer_id,
image_url=image_url,
acc_id=acc_id,
acc_type=acc_type
)
return success
elif workflow_type == "process_image":
# 工作流 2处理图片
print(f"[Agent] 执行处理图片工作流 | 客户:{customer_id}")
success = await workflow.process_image_workflow(
customer_id=customer_id,
image_url=image_url,
acc_id=acc_id,
acc_type=acc_type
)
return success
elif workflow_type == "transfer_human":
# 工作流 3转人工派单
print(f"[Agent] 执行转人工派单工作流 | 客户:{customer_id}")
success = await workflow.transfer_to_designer_workflow(
customer_id=customer_id,
image_url=image_url,
acc_id=acc_id,
acc_type=acc_type,
reason="客户主动要求转人工"
)
return success
# 未知工作流,不处理
return False

View File

@@ -611,26 +611,10 @@ class CustomerServiceWorkflow:
)
return "\n".join(lines)
# ========== 全局实例 ==========
workflow = CustomerServiceWorkflow()
# ========== 客户需求变更 ==========
async def add_customer_requirement(self, task_id: str, customer_id: str,
requirement: str, changed_by: str = 'customer') -> bool:
"""
客户添加/修改需求细节
Args:
task_id: 任务 ID
customer_id: 客户 ID
requirement: 需求内容(如:"需要去掉背景""要分层文件"
changed_by: 修改者customer/staff
Returns:
bool: 是否成功
"""
# 检查任务是否存在
task = self.get_task(task_id)
if not task:
@@ -981,3 +965,7 @@ workflow = CustomerServiceWorkflow()
except Exception as e:
logger.error(f"派单失败:{e}")
return False
# ========== 全局实例 ==========
workflow = CustomerServiceWorkflow()

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
{
"_注释": "本文件是 customers.json 的字段说明,不参与程序运行,仅供人工查阅。",
"字段说明": {
"customer_id": "客户ID平台用户名/ID",
"name": "真实姓名",
"nickname": "昵称",
"email": "邮箱",
"phone": "手机号",
"wechat": "微信号",
"address": "收货地址",
"platform": "来源平台(淘宝/拼多多/微信等)",
"platform_id": "平台内部ID",
"budget": "预算描述(客户自述)",
"budget_range_min": "预算下限(元)",
"budget_range_max": "预算上限(元)",
"requirements": "历史需求列表",
"preference_services": "偏好服务类型列表",
"total_orders": "累计下单次数",
"total_spent": "累计消费金额(元)",
"avg_order_value": "平均客单价(元)",
"purchase_frequency": "购买频次描述",
"last_order_date": "最后下单日期",
"first_order_date": "首次下单日期",
"order_ids": "订单号列表",
"pending_orders": "待处理订单数",
"completed_orders": "已完成订单数",
"refund_count": "退款次数",
"personality": "性格标签列表(如:急躁/爽快/纠结)",
"communication_prefer": "沟通偏好",
"response_speed": "回复速度描述",
"patience_level": "耐心程度",
"customer_level": "客户等级A/B/C/D",
"vip": "是否VIP",
"vip_level": "VIP等级0=无)",
"vip_custom_price": "VIP专属报价0=无专属价)",
"last_price": "上次报价金额(元)",
"last_price_time": "上次报价时间",
"last_quote_no_convert": "上次报价后未成交,下次可适当降低",
"lowest_price_accepted":"历史接受过的最低价(元)",
"discount_given_count": "累计让价次数",
"price_sensitivity": "价格敏感度(高/中/低,自动计算)",
"decision_speed": "决策速度(快/慢,自动标记)",
"good_reviews": "好评次数",
"bad_reviews": "差评次数",
"dispute_count": "纠纷次数",
"follow_up_by": "跟进负责人",
"follow_up_date": "跟进日期",
"next_follow_date": "下次跟进日期",
"source": "客户来源渠道",
"coupon_used": "使用过的优惠券",
"notes": "备注列表(含自动报价记录)",
"tags": "自定义标签列表",
"created_at": "建档时间",
"last_contact": "最后联系时间",
"last_update": "最后更新时间",
"last_image_url": "最后发来的图片URL",
"last_image_time": "最后发图时间",
"processing_status": "当前作图状态pending/processing/done等",
"processing_image_url": "当前处理中的图片URL",
"expected_done_at": "预计完成时间",
"preferred_format": "偏好文件格式jpg/png/psd等",
"preferred_size": "偏好尺寸/分辨率描述",
"total_images_sent": "历史发图总数",
"complexity_history": "历史图片复杂度列表normal/complex/hard",
"image_type_history": "历史图片类型列表(印花/logo/人物等)",
"bulk_potential": "批量潜力(有/无/未知)",
"churn_risk": "流失风险(高/中/低,自动计算)",
"upsell_opportunity": "加购机会标签分层PSD/批量打包)",
"revision_count": "累计改稿次数",
"revision_orders": "有改稿的订单数",
"total_completed_orders":"已完成出图的订单数",
"blacklist": "是否黑名单",
"blacklist_reason": "拉黑原因",
"last_email_status": "最后邮件发送状态sent/failed",
"last_conversation_summary": "上次对话AI摘要15字以内",
"last_conversation_time": "上次对话时间"
}
}

View File

@@ -9,9 +9,33 @@ from datetime import datetime
from typing import List, Dict, Optional
_DB_PATH = os.path.join(os.path.dirname(__file__), "chat_log_db", "chats.db")
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
def _is_mysql() -> bool:
return _DB_TYPE in ("mysql", "mariadb")
def _sql(query: str) -> str:
return query.replace("?", "%s") if _is_mysql() else query
def _get_conn() -> sqlite3.Connection:
if _is_mysql():
import pymysql
return pymysql.connect(
host=_MYSQL_HOST,
port=_MYSQL_PORT,
user=_MYSQL_USER,
password=_MYSQL_PASSWORD,
database=_MYSQL_DATABASE,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
conn = sqlite3.connect(_DB_PATH)
conn.row_factory = sqlite3.Row
@@ -21,27 +45,44 @@ def _get_conn() -> sqlite3.Connection:
def init_db():
"""建表(首次运行时自动调用)"""
with _get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS chat_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
customer_name TEXT DEFAULT '',
acc_id TEXT DEFAULT '',
platform TEXT DEFAULT '',
direction TEXT NOT NULL CHECK(direction IN ('in','out')),
message TEXT NOT NULL,
msg_type INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_customer ON chat_logs(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_ts ON chat_logs(timestamp)")
# 兼容旧表:若缺少 acc_id 列则补上(必须在创建该列索引之前)
try:
conn.execute("ALTER TABLE chat_logs ADD COLUMN acc_id TEXT DEFAULT ''")
except Exception:
pass
conn.execute("CREATE INDEX IF NOT EXISTS idx_acc ON chat_logs(acc_id)")
if _is_mysql():
conn.execute("""
CREATE TABLE IF NOT EXISTS chat_logs (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
customer_id VARCHAR(128) NOT NULL,
customer_name VARCHAR(255) DEFAULT '',
acc_id VARCHAR(128) DEFAULT '',
platform VARCHAR(64) DEFAULT '',
direction VARCHAR(8) NOT NULL,
message TEXT NOT NULL,
msg_type INTEGER DEFAULT 0,
timestamp DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_customer ON chat_logs(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_ts ON chat_logs(timestamp)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_acc ON chat_logs(acc_id)")
else:
conn.execute("""
CREATE TABLE IF NOT EXISTS chat_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
customer_name TEXT DEFAULT '',
acc_id TEXT DEFAULT '',
platform TEXT DEFAULT '',
direction TEXT NOT NULL CHECK(direction IN ('in','out')),
message TEXT NOT NULL,
msg_type INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_customer ON chat_logs(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_ts ON chat_logs(timestamp)")
try:
conn.execute("ALTER TABLE chat_logs ADD COLUMN acc_id TEXT DEFAULT ''")
except Exception:
pass
conn.execute("CREATE INDEX IF NOT EXISTS idx_acc ON chat_logs(acc_id)")
conn.commit()
@@ -63,9 +104,9 @@ def log_message(
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with _get_conn() as conn:
conn.execute(
"INSERT INTO chat_logs "
"(customer_id, customer_name, acc_id, platform, direction, message, msg_type, timestamp) "
"VALUES (?,?,?,?,?,?,?,?)",
_sql("INSERT INTO chat_logs "
"(customer_id, customer_name, acc_id, platform, direction, message, msg_type, timestamp) "
"VALUES (?,?,?,?,?,?,?,?)"),
(customer_id, customer_name, acc_id, platform, direction, message, msg_type, ts),
)
conn.commit()
@@ -77,6 +118,19 @@ def get_customers(limit: int = 100) -> List[Dict]:
"""返回所有有记录的客户列表(按最新消息时间排序)"""
with _get_conn() as conn:
rows = conn.execute("""
SELECT
customer_id,
MAX(customer_name) AS customer_name,
MAX(platform) AS platform,
COUNT(*) AS total_msgs,
SUM(direction='in') AS recv,
SUM(direction='out') AS sent,
MAX(timestamp) AS last_time
FROM chat_logs
GROUP BY customer_id
ORDER BY last_time DESC
LIMIT %s
""" if _is_mysql() else """
SELECT
customer_id,
MAX(customer_name) AS customer_name,
@@ -96,13 +150,13 @@ def get_customers(limit: int = 100) -> List[Dict]:
def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]:
"""返回某客户的全部对话记录(按时间升序)"""
with _get_conn() as conn:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT id, direction, message, msg_type, timestamp, acc_id
FROM chat_logs
WHERE customer_id = ?
ORDER BY timestamp ASC, id ASC
LIMIT ?
""", (customer_id, limit)).fetchall()
"""), (customer_id, limit)).fetchall()
return [dict(r) for r in rows]
@@ -110,21 +164,21 @@ def get_recent_conversation(customer_id: str, acc_id: str = "", limit: int = 10)
"""返回某客户近期对话(同店铺),用于企微推送保持连贯"""
with _get_conn() as conn:
if acc_id:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT id, direction, message, timestamp, acc_id
FROM chat_logs
WHERE customer_id = ? AND acc_id = ?
ORDER BY id DESC
LIMIT ?
""", (customer_id, acc_id, limit)).fetchall()
"""), (customer_id, acc_id, limit)).fetchall()
else:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT id, direction, message, timestamp, acc_id
FROM chat_logs
WHERE customer_id = ?
ORDER BY id DESC
LIMIT ?
""", (customer_id, limit)).fetchall()
"""), (customer_id, limit)).fetchall()
out = [dict(r) for r in reversed(rows)]
return out
@@ -133,12 +187,12 @@ def get_conversation_today(customer_id: str) -> List[Dict]:
"""返回某客户今天的对话"""
today = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT id, direction, message, msg_type, timestamp
FROM chat_logs
WHERE customer_id = ? AND timestamp LIKE ?
ORDER BY timestamp ASC, id ASC
""", (customer_id, f"{today}%")).fetchall()
"""), (customer_id, f"{today}%")).fetchall()
return [dict(r) for r in rows]
@@ -151,7 +205,7 @@ def get_daily_stats(date: str = "") -> List[Dict]:
if not date:
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT
acc_id,
platform,
@@ -165,7 +219,7 @@ def get_daily_stats(date: str = "") -> List[Dict]:
WHERE timestamp LIKE ?
GROUP BY acc_id
ORDER BY unique_customers DESC
""", (f"{date}%",)).fetchall()
"""), (f"{date}%",)).fetchall()
return [dict(r) for r in rows]
@@ -176,22 +230,39 @@ def get_daily_conversations(date: str = "") -> List[Dict]:
if not date:
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute("""
SELECT
acc_id,
customer_id,
MAX(customer_name) AS customer_name,
COUNT(*) AS msg_count,
GROUP_CONCAT(
CASE WHEN direction='in' THEN '买:' || SUBSTR(message,1,40)
ELSE '客:' || SUBSTR(message,1,40) END,
' | '
) AS snippet
FROM chat_logs
WHERE timestamp LIKE ?
GROUP BY acc_id, customer_id
ORDER BY acc_id, MAX(timestamp) DESC
""", (f"{date}%",)).fetchall()
if _is_mysql():
rows = conn.execute(_sql("""
SELECT
acc_id,
customer_id,
MAX(customer_name) AS customer_name,
COUNT(*) AS msg_count,
GROUP_CONCAT(
CONCAT(CASE WHEN direction='in' THEN '买:' ELSE '客:' END, LEFT(message,40))
SEPARATOR ' | '
) AS snippet
FROM chat_logs
WHERE timestamp LIKE ?
GROUP BY acc_id, customer_id
ORDER BY acc_id, MAX(timestamp) DESC
"""), (f"{date}%",)).fetchall()
else:
rows = conn.execute(_sql("""
SELECT
acc_id,
customer_id,
MAX(customer_name) AS customer_name,
COUNT(*) AS msg_count,
GROUP_CONCAT(
CASE WHEN direction='in' THEN '买:' || SUBSTR(message,1,40)
ELSE '客:' || SUBSTR(message,1,40) END,
' | '
) AS snippet
FROM chat_logs
WHERE timestamp LIKE ?
GROUP BY acc_id, customer_id
ORDER BY acc_id, MAX(timestamp) DESC
"""), (f"{date}%",)).fetchall()
return [dict(r) for r in rows]
@@ -199,18 +270,28 @@ def search_messages(keyword: str, customer_id: Optional[str] = None, limit: int
"""全文搜索消息"""
if customer_id:
with _get_conn() as conn:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT customer_id, customer_name, direction, message, timestamp
FROM chat_logs
WHERE customer_id = ? AND message LIKE ?
ORDER BY timestamp DESC LIMIT ?
""", (customer_id, f"%{keyword}%", limit)).fetchall()
"""), (customer_id, f"%{keyword}%", limit)).fetchall()
else:
with _get_conn() as conn:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT customer_id, customer_name, direction, message, timestamp
FROM chat_logs
WHERE message LIKE ?
ORDER BY timestamp DESC LIMIT ?
""", (f"%{keyword}%", limit)).fetchall()
"""), (f"%{keyword}%", limit)).fetchall()
return [dict(r) for r in rows]
def get_latest_messages(limit: int = 20) -> List[Dict]:
with _get_conn() as conn:
rows = conn.execute(_sql("""
SELECT id, customer_id, customer_name, direction, message, timestamp
FROM chat_logs
ORDER BY id DESC LIMIT ?
"""), (limit,)).fetchall()
return [dict(r) for r in rows]

View File

@@ -8,9 +8,33 @@ from datetime import datetime
from typing import List, Dict, Optional
_DB_PATH = os.path.join(os.path.dirname(__file__), "deal_outcome_db", "outcomes.db")
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
def _is_mysql() -> bool:
return _DB_TYPE in ("mysql", "mariadb")
def _sql(query: str) -> str:
return query.replace("?", "%s") if _is_mysql() else query
def _get_conn() -> sqlite3.Connection:
if _is_mysql():
import pymysql
return pymysql.connect(
host=_MYSQL_HOST,
port=_MYSQL_PORT,
user=_MYSQL_USER,
password=_MYSQL_PASSWORD,
database=_MYSQL_DATABASE,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
conn = sqlite3.connect(_DB_PATH)
conn.row_factory = sqlite3.Row
@@ -19,26 +43,48 @@ def _get_conn() -> sqlite3.Connection:
def _init_db():
with _get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS deal_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
customer_name TEXT DEFAULT '',
acc_id TEXT DEFAULT '',
platform TEXT DEFAULT '',
date TEXT NOT NULL,
outcome TEXT NOT NULL CHECK(outcome IN ('成交','未成交')),
reason TEXT DEFAULT '',
order_id TEXT DEFAULT '',
amount REAL DEFAULT 0,
discount_given INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_date ON deal_outcomes(date)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_customer ON deal_outcomes(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_acc ON deal_outcomes(acc_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_outcome ON deal_outcomes(outcome)")
if _is_mysql():
conn.execute("""
CREATE TABLE IF NOT EXISTS deal_outcomes (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
customer_id VARCHAR(128) NOT NULL,
customer_name VARCHAR(255) DEFAULT '',
acc_id VARCHAR(128) DEFAULT '',
platform VARCHAR(64) DEFAULT '',
date DATE NOT NULL,
outcome VARCHAR(16) NOT NULL,
reason TEXT,
order_id VARCHAR(128) DEFAULT '',
amount REAL DEFAULT 0,
discount_given INTEGER DEFAULT 0,
timestamp DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_date ON deal_outcomes(date)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_customer ON deal_outcomes(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_acc ON deal_outcomes(acc_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_outcome ON deal_outcomes(outcome)")
else:
conn.execute("""
CREATE TABLE IF NOT EXISTS deal_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
customer_name TEXT DEFAULT '',
acc_id TEXT DEFAULT '',
platform TEXT DEFAULT '',
date TEXT NOT NULL,
outcome TEXT NOT NULL CHECK(outcome IN ('成交','未成交')),
reason TEXT DEFAULT '',
order_id TEXT DEFAULT '',
amount REAL DEFAULT 0,
discount_given INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_date ON deal_outcomes(date)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_customer ON deal_outcomes(customer_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_acc ON deal_outcomes(acc_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_outcome ON deal_outcomes(outcome)")
conn.commit()
@@ -61,10 +107,10 @@ def record_deal(
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
conn.execute(
"""INSERT INTO deal_outcomes
_sql("""INSERT INTO deal_outcomes
(customer_id, customer_name, acc_id, platform, date, outcome, reason,
order_id, amount, discount_given, timestamp)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
VALUES (?,?,?,?,?,?,?,?,?,?,?)"""),
(
customer_id,
customer_name or "",
@@ -88,13 +134,13 @@ def get_daily_outcomes(date: str = "") -> List[Dict]:
date = datetime.now().strftime("%Y-%m-%d")
with _get_conn() as conn:
rows = conn.execute(
"""
_sql("""
SELECT customer_id, customer_name, acc_id, outcome, reason,
order_id, amount, discount_given, timestamp
FROM deal_outcomes
WHERE date = ?
ORDER BY timestamp ASC
""",
"""),
(date,),
).fetchall()
return [dict(r) for r in rows]
@@ -131,19 +177,19 @@ def export_for_analysis(start_date: str = "", end_date: str = "") -> List[Dict]:
with _get_conn() as conn:
if start_date and end_date:
rows = conn.execute(
"""SELECT * FROM deal_outcomes
_sql("""SELECT * FROM deal_outcomes
WHERE date BETWEEN ? AND ?
ORDER BY date, timestamp""",
ORDER BY date, timestamp"""),
(start_date, end_date),
).fetchall()
elif start_date:
rows = conn.execute(
"""SELECT * FROM deal_outcomes WHERE date >= ? ORDER BY date, timestamp""",
_sql("""SELECT * FROM deal_outcomes WHERE date >= ? ORDER BY date, timestamp"""),
(start_date,),
).fetchall()
elif end_date:
rows = conn.execute(
"""SELECT * FROM deal_outcomes WHERE date <= ? ORDER BY date, timestamp""",
_sql("""SELECT * FROM deal_outcomes WHERE date <= ? ORDER BY date, timestamp"""),
(end_date,),
).fetchall()
else:

View File

@@ -10,9 +10,33 @@ import os
from typing import Optional
_DB_PATH = os.path.join(os.path.dirname(__file__), "designer_roster_db", "roster.db")
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
def _is_mysql() -> bool:
return _DB_TYPE in ("mysql", "mariadb")
def _sql(query: str) -> str:
return query.replace("?", "%s") if _is_mysql() else query
def _get_conn() -> sqlite3.Connection:
if _is_mysql():
import pymysql
return pymysql.connect(
host=_MYSQL_HOST,
port=_MYSQL_PORT,
user=_MYSQL_USER,
password=_MYSQL_PASSWORD,
database=_MYSQL_DATABASE,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
conn = sqlite3.connect(_DB_PATH)
conn.row_factory = sqlite3.Row
@@ -21,35 +45,66 @@ def _get_conn() -> sqlite3.Connection:
def init_db():
with _get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS designers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
wechat_user_id TEXT UNIQUE NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_shops (
designer_id INTEGER NOT NULL,
shop_id TEXT NOT NULL,
group_id TEXT NOT NULL,
PRIMARY KEY (designer_id, shop_id),
FOREIGN KEY (designer_id) REFERENCES designers(id)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_online (
wechat_user_id TEXT PRIMARY KEY,
is_online INTEGER NOT NULL DEFAULT 0,
updated_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS round_robin (
shop_id TEXT PRIMARY KEY,
last_index INTEGER NOT NULL DEFAULT 0
)
""")
if _is_mysql():
conn.execute("""
CREATE TABLE IF NOT EXISTS designers (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
wechat_user_id VARCHAR(128) UNIQUE NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_shops (
designer_id INTEGER NOT NULL,
shop_id VARCHAR(128) NOT NULL,
group_id VARCHAR(128) NOT NULL,
PRIMARY KEY (designer_id, shop_id),
FOREIGN KEY (designer_id) REFERENCES designers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_online (
wechat_user_id VARCHAR(128) PRIMARY KEY,
is_online INTEGER NOT NULL DEFAULT 0,
updated_at DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS round_robin (
shop_id VARCHAR(128) PRIMARY KEY,
last_index INTEGER NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
else:
conn.execute("""
CREATE TABLE IF NOT EXISTS designers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
wechat_user_id TEXT UNIQUE NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_shops (
designer_id INTEGER NOT NULL,
shop_id TEXT NOT NULL,
group_id TEXT NOT NULL,
PRIMARY KEY (designer_id, shop_id),
FOREIGN KEY (designer_id) REFERENCES designers(id)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS designer_online (
wechat_user_id TEXT PRIMARY KEY,
is_online INTEGER NOT NULL DEFAULT 0,
updated_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS round_robin (
shop_id TEXT PRIMARY KEY,
last_index INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
@@ -61,22 +116,34 @@ init_db()
def add_designer(name: str, wechat_user_id: str) -> int:
"""添加设计师,返回 id"""
with _get_conn() as conn:
conn.execute(
"INSERT OR IGNORE INTO designers (name, wechat_user_id) VALUES (?, ?)",
(name, wechat_user_id),
)
if _is_mysql():
conn.execute(
"INSERT IGNORE INTO designers (name, wechat_user_id) VALUES (%s, %s)",
(name, wechat_user_id),
)
else:
conn.execute(
"INSERT OR IGNORE INTO designers (name, wechat_user_id) VALUES (?, ?)",
(name, wechat_user_id),
)
conn.commit()
row = conn.execute("SELECT id FROM designers WHERE wechat_user_id = ?", (wechat_user_id,)).fetchone()
row = conn.execute(_sql("SELECT id FROM designers WHERE wechat_user_id = ?"), (wechat_user_id,)).fetchone()
return row["id"] if row else 0
def set_designer_shop(designer_id: int, shop_id: str, group_id: str):
"""设置设计师在某店铺的分组 ID同一设计师不同店铺不同 group_id"""
with _get_conn() as conn:
conn.execute(
"INSERT OR REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (?, ?, ?)",
(designer_id, shop_id, group_id),
)
if _is_mysql():
conn.execute(
"REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (%s, %s, %s)",
(designer_id, shop_id, group_id),
)
else:
conn.execute(
"INSERT OR REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (?, ?, ?)",
(designer_id, shop_id, group_id),
)
conn.commit()
@@ -85,10 +152,16 @@ def update_online(wechat_user_id: str, is_online: bool):
from datetime import datetime
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with _get_conn() as conn:
conn.execute(
"INSERT OR REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (?, ?, ?)",
(wechat_user_id, 1 if is_online else 0, ts),
)
if _is_mysql():
conn.execute(
"REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (%s, %s, %s)",
(wechat_user_id, 1 if is_online else 0, ts),
)
else:
conn.execute(
"INSERT OR REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (?, ?, ?)",
(wechat_user_id, 1 if is_online else 0, ts),
)
conn.commit()
@@ -101,26 +174,32 @@ def get_transfer_group_for_shop(shop_id: str) -> Optional[str]:
无人在线则返回 None。
"""
with _get_conn() as conn:
rows = conn.execute("""
rows = conn.execute(_sql("""
SELECT d.wechat_user_id, ds.group_id
FROM designer_shops ds
JOIN designers d ON d.id = ds.designer_id
JOIN designer_online o ON o.wechat_user_id = d.wechat_user_id AND o.is_online = 1
WHERE ds.shop_id = ?
""", (shop_id,)).fetchall()
"""), (shop_id,)).fetchall()
if not rows:
return None
with _get_conn() as conn:
rr = conn.execute("SELECT last_index FROM round_robin WHERE shop_id = ?", (shop_id,)).fetchone()
rr = conn.execute(_sql("SELECT last_index FROM round_robin WHERE shop_id = ?"), (shop_id,)).fetchone()
last = rr["last_index"] if rr else 0
idx = last % len(rows)
chosen = rows[idx]
conn.execute(
"INSERT OR REPLACE INTO round_robin (shop_id, last_index) VALUES (?, ?)",
(shop_id, idx + 1),
)
if _is_mysql():
conn.execute(
"REPLACE INTO round_robin (shop_id, last_index) VALUES (%s, %s)",
(shop_id, idx + 1),
)
else:
conn.execute(
"INSERT OR REPLACE INTO round_robin (shop_id, last_index) VALUES (?, ?)",
(shop_id, idx + 1),
)
conn.commit()
return chosen["group_id"]
@@ -142,11 +221,11 @@ def list_designers():
result = []
for d in designers:
shops = conn.execute(
"SELECT shop_id, group_id FROM designer_shops WHERE designer_id = ?",
_sql("SELECT shop_id, group_id FROM designer_shops WHERE designer_id = ?"),
(d["id"],),
).fetchall()
online = conn.execute(
"SELECT is_online FROM designer_online WHERE wechat_user_id = ?",
_sql("SELECT is_online FROM designer_online WHERE wechat_user_id = ?"),
(d["wechat_user_id"],),
).fetchone()
result.append({

View File

@@ -10,8 +10,26 @@ from typing import Optional, List, Dict
from pathlib import Path
from datetime import datetime
from enum import Enum
import os
logger = logging.getLogger(__name__)
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
def _is_mysql() -> bool:
return _DB_TYPE in ("mysql", "mariadb")
def _sql(query: str) -> str:
return query.replace("?", "%s") if _is_mysql() else query
def _now_str() -> str:
if _is_mysql():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return datetime.now().isoformat()
class TaskStatus(Enum):
"""任务状态"""
@@ -36,61 +54,105 @@ class ImageTaskManager:
def _init_db(self):
"""初始化数据库"""
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建图片任务表
cursor.execute('''
CREATE TABLE IF NOT EXISTS image_tasks (
task_id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
customer_name TEXT,
original_image TEXT NOT NULL,
operation TEXT DEFAULT 'enhance',
requirements TEXT, -- JSON 格式:复杂度、比例、透视等
customer_notes TEXT, -- 客户备注/需求细节
status TEXT DEFAULT 'pending',
created_at TEXT,
paid_at TEXT,
started_at TEXT,
completed_at TEXT,
result_image TEXT,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
-- 店铺信息
acc_id TEXT,
acc_type TEXT DEFAULT 'AliWorkbench'
)
''')
# 创建需求变更记录表(支持客户后续增加需求)
cursor.execute('''
CREATE TABLE IF NOT EXISTS task_requirement_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
change_type TEXT, -- add_note/modify_operation/add_requirement
old_value TEXT,
new_value TEXT,
changed_at TEXT,
changed_by TEXT, -- customer/staff
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON image_tasks(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
conn.commit()
conn.close()
if _is_mysql():
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS image_tasks (
task_id VARCHAR(128) PRIMARY KEY,
customer_id VARCHAR(128) NOT NULL,
customer_name VARCHAR(255),
original_image TEXT NOT NULL,
operation VARCHAR(64) DEFAULT 'enhance',
requirements TEXT,
customer_notes TEXT,
status VARCHAR(32) DEFAULT 'pending',
created_at DATETIME,
paid_at DATETIME,
started_at DATETIME,
completed_at DATETIME,
result_image TEXT,
error_message TEXT,
retry_count INT DEFAULT 0,
acc_id VARCHAR(128),
acc_type VARCHAR(64) DEFAULT 'AliWorkbench'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS task_requirement_changes (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
task_id VARCHAR(128) NOT NULL,
change_type VARCHAR(64),
old_value TEXT,
new_value TEXT,
changed_at DATETIME,
changed_by VARCHAR(32),
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON image_tasks(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
conn.commit()
conn.close()
else:
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS image_tasks (
task_id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
customer_name TEXT,
original_image TEXT NOT NULL,
operation TEXT DEFAULT 'enhance',
requirements TEXT,
customer_notes TEXT,
status TEXT DEFAULT 'pending',
created_at TEXT,
paid_at TEXT,
started_at TEXT,
completed_at TEXT,
result_image TEXT,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
acc_id TEXT,
acc_type TEXT DEFAULT 'AliWorkbench'
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS task_requirement_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
change_type TEXT,
old_value TEXT,
new_value TEXT,
changed_at TEXT,
changed_by TEXT,
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
)
''')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON image_tasks(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
conn.commit()
conn.close()
logger.info("数据库表初始化完成")
def _get_conn(self):
"""获取数据库连接"""
if _is_mysql():
import pymysql
return pymysql.connect(
host=_MYSQL_HOST,
port=_MYSQL_PORT,
user=_MYSQL_USER,
password=_MYSQL_PASSWORD,
database=_MYSQL_DATABASE,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
@@ -105,13 +167,13 @@ class ImageTaskManager:
requirements_json = json.dumps(requirements) if requirements else None
cursor.execute('''
cursor.execute(_sql('''
INSERT INTO image_tasks (
task_id, customer_id, customer_name, original_image,
operation, requirements, customer_notes, status,
created_at, acc_id, acc_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
'''), (
task_id,
customer_id,
customer_name,
@@ -120,7 +182,7 @@ class ImageTaskManager:
requirements_json,
'', # 初始备注为空
TaskStatus.PENDING.value,
datetime.now().isoformat(),
_now_str(),
acc_id,
acc_type
))
@@ -141,7 +203,7 @@ class ImageTaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM image_tasks WHERE task_id = ?', (task_id,))
cursor.execute(_sql('SELECT * FROM image_tasks WHERE task_id = ?'), (task_id,))
row = cursor.fetchone()
conn.close()
@@ -164,17 +226,17 @@ class ImageTaskManager:
cursor = conn.cursor()
if status:
cursor.execute('''
cursor.execute(_sql('''
SELECT * FROM image_tasks
WHERE customer_id = ? AND status = ?
ORDER BY created_at DESC
''', (customer_id, status))
'''), (customer_id, status))
else:
cursor.execute('''
cursor.execute(_sql('''
SELECT * FROM image_tasks
WHERE customer_id = ?
ORDER BY created_at DESC
''', (customer_id,))
'''), (customer_id,))
rows = cursor.fetchall()
conn.close()
@@ -198,27 +260,28 @@ class ImageTaskManager:
conn = self._get_conn()
cursor = conn.cursor()
updates = ['status = ?']
placeholder = "%s" if _is_mysql() else "?"
updates = [f'status = {placeholder}']
params = [status.value]
# 根据状态设置时间
if status == TaskStatus.PAID:
updates.append('paid_at = ?')
params.append(datetime.now().isoformat())
updates.append(f'paid_at = {placeholder}')
params.append(_now_str())
elif status == TaskStatus.PROCESSING:
updates.append('started_at = ?')
params.append(datetime.now().isoformat())
updates.append(f'started_at = {placeholder}')
params.append(_now_str())
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
updates.append('completed_at = ?')
params.append(datetime.now().isoformat())
updates.append(f'completed_at = {placeholder}')
params.append(_now_str())
params.append(task_id)
cursor.execute(f'''
cursor.execute(_sql(f'''
UPDATE image_tasks
SET {', '.join(updates)}
WHERE task_id = ?
''', params)
'''), params)
conn.commit()
conn.close()
@@ -234,11 +297,11 @@ class ImageTaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
cursor.execute(_sql('''
UPDATE image_tasks
SET result_image = ?, error_message = ?
WHERE task_id = ?
''', (result_image, error_message, task_id))
'''), (result_image, error_message, task_id))
conn.commit()
conn.close()
@@ -265,30 +328,30 @@ class ImageTaskManager:
cursor = conn.cursor()
# 获取旧备注
cursor.execute('SELECT customer_notes FROM image_tasks WHERE task_id = ?', (task_id,))
cursor.execute(_sql('SELECT customer_notes FROM image_tasks WHERE task_id = ?'), (task_id,))
row = cursor.fetchone()
old_note = row['customer_notes'] if row else ''
# 更新备注
new_note = f"{old_note}\n[{datetime.now().strftime('%m-%d %H:%M')}] {note}" if old_note else f"[{datetime.now().strftime('%m-%d %H:%M')}] {note}"
cursor.execute('''
cursor.execute(_sql('''
UPDATE image_tasks
SET customer_notes = ?
WHERE task_id = ?
''', (new_note, task_id))
'''), (new_note, task_id))
# 记录变更历史
cursor.execute('''
cursor.execute(_sql('''
INSERT INTO task_requirement_changes (
task_id, change_type, old_value, new_value, changed_at, changed_by
) VALUES (?, ?, ?, ?, ?, ?)
''', (
'''), (
task_id,
'add_note',
old_note or '',
note,
datetime.now().isoformat(),
_now_str(),
changed_by
))
@@ -319,28 +382,28 @@ class ImageTaskManager:
cursor = conn.cursor()
# 获取旧操作
cursor.execute('SELECT operation FROM image_tasks WHERE task_id = ?', (task_id,))
cursor.execute(_sql('SELECT operation FROM image_tasks WHERE task_id = ?'), (task_id,))
row = cursor.fetchone()
old_operation = row['operation'] if row else ''
# 更新操作
cursor.execute('''
cursor.execute(_sql('''
UPDATE image_tasks
SET operation = ?
WHERE task_id = ?
''', (new_operation, task_id))
'''), (new_operation, task_id))
# 记录变更历史
cursor.execute('''
cursor.execute(_sql('''
INSERT INTO task_requirement_changes (
task_id, change_type, old_value, new_value, changed_at, changed_by
) VALUES (?, ?, ?, ?, ?, ?)
''', (
'''), (
task_id,
'modify_operation',
old_operation,
new_operation,
datetime.now().isoformat(),
_now_str(),
changed_by
))
@@ -360,11 +423,11 @@ class ImageTaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
cursor.execute(_sql('''
SELECT * FROM task_requirement_changes
WHERE task_id = ?
ORDER BY changed_at DESC
''', (task_id,))
'''), (task_id,))
rows = cursor.fetchall()
conn.close()
@@ -385,13 +448,13 @@ class ImageTaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
cursor.execute(_sql('''
UPDATE image_tasks
SET retry_count = retry_count + 1
WHERE task_id = ?
''', (task_id,))
'''), (task_id,))
cursor.execute('SELECT retry_count FROM image_tasks WHERE task_id = ?', (task_id,))
cursor.execute(_sql('SELECT retry_count FROM image_tasks WHERE task_id = ?'), (task_id,))
row = cursor.fetchone()
conn.close()

View File

@@ -9,8 +9,26 @@ from datetime import datetime, timedelta
from typing import Optional, Dict, List
from pathlib import Path
from enum import Enum
import os
logger = logging.getLogger(__name__)
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
def _is_mysql() -> bool:
return _DB_TYPE in ("mysql", "mariadb")
def _sql(query: str) -> str:
return query.replace("?", "%s") if _is_mysql() else query
def _now_str() -> str:
if _is_mysql():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return datetime.now().isoformat()
class TaskStatus(Enum):
"""任务状态"""
@@ -40,51 +58,93 @@ class TaskManager:
def _init_db(self):
"""初始化数据库"""
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建任务表
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
specified_customer_id TEXT,
specified_customer_name TEXT,
type TEXT NOT NULL,
customer_name TEXT,
customer_id TEXT,
trigger_type TEXT,
trigger_keyword TEXT,
trigger_keywords TEXT, -- JSON array
action_type TEXT,
action_file_url TEXT,
action_message TEXT,
priority TEXT DEFAULT 'normal',
timeout_hours INTEGER DEFAULT 24,
status TEXT DEFAULT 'pending',
retry_count INTEGER DEFAULT 0,
max_retry INTEGER DEFAULT 3,
created_at TEXT,
created_by TEXT,
triggered_at TEXT,
completed_at TEXT,
error_message TEXT,
result TEXT -- JSON
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON tasks(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at)')
conn.commit()
conn.close()
if _is_mysql():
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
task_id VARCHAR(128) PRIMARY KEY,
specified_customer_id VARCHAR(128),
specified_customer_name VARCHAR(255),
type VARCHAR(64) NOT NULL,
customer_name VARCHAR(255),
customer_id VARCHAR(128),
trigger_type VARCHAR(64),
trigger_keyword VARCHAR(255),
trigger_keywords TEXT,
action_type VARCHAR(64),
action_file_url TEXT,
action_message TEXT,
priority VARCHAR(16) DEFAULT 'normal',
timeout_hours INT DEFAULT 24,
status VARCHAR(32) DEFAULT 'pending',
retry_count INT DEFAULT 0,
max_retry INT DEFAULT 3,
created_at DATETIME,
created_by VARCHAR(255),
triggered_at DATETIME,
completed_at DATETIME,
error_message TEXT,
result TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON tasks(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at)')
conn.commit()
conn.close()
else:
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
specified_customer_id TEXT,
specified_customer_name TEXT,
type TEXT NOT NULL,
customer_name TEXT,
customer_id TEXT,
trigger_type TEXT,
trigger_keyword TEXT,
trigger_keywords TEXT,
action_type TEXT,
action_file_url TEXT,
action_message TEXT,
priority TEXT DEFAULT 'normal',
timeout_hours INTEGER DEFAULT 24,
status TEXT DEFAULT 'pending',
retry_count INTEGER DEFAULT 0,
max_retry INTEGER DEFAULT 3,
created_at TEXT,
created_by TEXT,
triggered_at TEXT,
completed_at TEXT,
error_message TEXT,
result TEXT
)
''')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON tasks(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at)')
conn.commit()
conn.close()
logger.info("数据库表初始化完成")
def _get_conn(self):
"""获取数据库连接"""
if _is_mysql():
import pymysql
return pymysql.connect(
host=_MYSQL_HOST,
port=_MYSQL_PORT,
user=_MYSQL_USER,
password=_MYSQL_PASSWORD,
database=_MYSQL_DATABASE,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
@@ -100,7 +160,7 @@ class TaskManager:
if isinstance(trigger_keywords, list):
trigger_keywords = json.dumps(trigger_keywords)
cursor.execute('''
insert_sql = '''
INSERT OR REPLACE INTO tasks (
task_id, specified_customer_id, specified_customer_name,
type, customer_name, customer_id,
@@ -109,7 +169,19 @@ class TaskManager:
priority, timeout_hours, status, retry_count, max_retry,
created_at, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
'''
if _is_mysql():
insert_sql = '''
REPLACE INTO tasks (
task_id, specified_customer_id, specified_customer_name,
type, customer_name, customer_id,
trigger_type, trigger_keyword, trigger_keywords,
action_type, action_file_url, action_message,
priority, timeout_hours, status, retry_count, max_retry,
created_at, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
cursor.execute(_sql(insert_sql), (
task.get('task_id'),
task.get('customer', {}).get('id'),
task.get('customer', {}).get('name'),
@@ -127,7 +199,7 @@ class TaskManager:
task.get('status', 'pending'),
task.get('retry_count', 0),
task.get('max_retry', 3),
task.get('created_at', datetime.now().isoformat()),
task.get('created_at', _now_str()),
task.get('created_by')
))
@@ -147,7 +219,7 @@ class TaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM tasks WHERE task_id = ?', (task_id,))
cursor.execute(_sql('SELECT * FROM tasks WHERE task_id = ?'), (task_id,))
row = cursor.fetchone()
conn.close()
@@ -165,32 +237,33 @@ class TaskManager:
conn = self._get_conn()
cursor = conn.cursor()
updates = ['status = ?']
placeholder = "%s" if _is_mysql() else "?"
updates = [f'status = {placeholder}']
params = [status.value]
if status == TaskStatus.RUNNING:
updates.append('triggered_at = ?')
params.append(datetime.now().isoformat())
updates.append(f'triggered_at = {placeholder}')
params.append(_now_str())
if status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
updates.append('completed_at = ?')
params.append(datetime.now().isoformat())
updates.append(f'completed_at = {placeholder}')
params.append(_now_str())
if error_message:
updates.append('error_message = ?')
updates.append(f'error_message = {placeholder}')
params.append(error_message)
if result:
updates.append('result = ?')
updates.append(f'result = {placeholder}')
params.append(json.dumps(result))
params.append(task_id)
cursor.execute(f'''
cursor.execute(_sql(f'''
UPDATE tasks
SET {', '.join(updates)}
WHERE task_id = ?
''', params)
'''), params)
conn.commit()
conn.close()
@@ -206,13 +279,13 @@ class TaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
cursor.execute(_sql('''
UPDATE tasks
SET retry_count = retry_count + 1
WHERE task_id = ?
''', (task_id,))
'''), (task_id,))
cursor.execute('SELECT retry_count, max_retry FROM tasks WHERE task_id = ?', (task_id,))
cursor.execute(_sql('SELECT retry_count, max_retry FROM tasks WHERE task_id = ?'), (task_id,))
row = cursor.fetchone()
conn.close()
@@ -231,7 +304,7 @@ class TaskManager:
cursor = conn.cursor()
if customer_id:
cursor.execute('''
cursor.execute(_sql('''
SELECT * FROM tasks
WHERE status = 'pending'
AND customer_id = ?
@@ -242,7 +315,7 @@ class TaskManager:
ELSE 3
END,
created_at
''', (customer_id,))
'''), (customer_id,))
else:
cursor.execute('''
SELECT * FROM tasks
@@ -271,11 +344,18 @@ class TaskManager:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM tasks
WHERE status = 'pending'
AND datetime(created_at, '+' || timeout_hours || ' hours') < datetime('now')
''')
if _is_mysql():
cursor.execute('''
SELECT * FROM tasks
WHERE status = 'pending'
AND created_at < DATE_SUB(NOW(), INTERVAL timeout_hours HOUR)
''')
else:
cursor.execute('''
SELECT * FROM tasks
WHERE status = 'pending'
AND datetime(created_at, '+' || timeout_hours || ' hours') < datetime('now')
''')
rows = cursor.fetchall()
conn.close()
@@ -299,7 +379,7 @@ class TaskManager:
stats = {}
for status in TaskStatus:
cursor.execute('SELECT COUNT(*) FROM tasks WHERE status = ?', (status.value,))
cursor.execute(_sql('SELECT COUNT(*) FROM tasks WHERE status = ?'), (status.value,))
stats[status.value] = cursor.fetchone()[0]
conn.close()

View File

@@ -10,3 +10,4 @@ aiofiles>=23.0.0
httpx>=0.25.0
numpy>=1.24.0
opencv-python>=4.8.0
pymysql>=1.1.0

222
run.py
View File

@@ -1,96 +1,218 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
项目入口 - 启动 WebSocket 客服客户端
AI 客服系统 - 统一启动器
用法:
python run.py # 单进程模式(默认)
python run.py --no-agent # 仅基础回复,不启用 AI
python run.py --multi # 多进程模式
python run.py --multi -w 4 # 多进程模式,指定 4 个进程
python run.py # WebSocket 客服模式(默认)
python run.py --tianwang # 完整版HTTP API + WebSocket + AI Agent
python run.py --tianwang-multi -w 4 # 天网 + 多进程HTTP API + 多进程 WebSocket
python run.py --api-only # 仅 HTTP API不含 WebSocket / AI Agent
python run.py --no-agent # 不启用 AI Agent
python run.py --multi -w 4 # 多进程模式4 个 Worker
python run.py --port 6060 # 指定 HTTP API 端口
"""
import sys
import os
import signal
import logging
import argparse
from pathlib import Path
# 确保项目根目录在 sys.path 首位
_root = Path(__file__).resolve().parent
if str(_root) not in sys.path:
sys.path.insert(0, str(_root))
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)
def run_single_process(enable_agent: bool):
"""单进程模式"""
from core.websocket_client import QingjianAPIClient
DEFAULT_HTTP_PORT = 6060
def _print_api_info(port: int):
logger.info(f"HTTP API 服务器已启动http://0.0.0.0:{port}")
logger.info("")
logger.info("天网任务接口:")
logger.info(" POST /api/task/receive - 接收任务")
logger.info(" POST /api/task/cancel - 取消任务")
logger.info(" GET /api/task/status/:id - 查询任务状态")
logger.info(" GET /api/task/list - 任务列表")
logger.info(" GET /api/health - 健康检查")
def run_websocket(enable_agent: bool):
"""WebSocket 客服模式(默认)"""
import asyncio
from core.websocket_client import QingjianAPIClient
print("=" * 60)
print("AI 客服系统 - 单进程模式")
print("=" * 60)
print(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
print("=" * 60)
logger.info("=" * 60)
logger.info("AI 客服系统 - WebSocket 模式")
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
logger.info("=" * 60)
client = QingjianAPIClient(enable_agent=enable_agent)
try:
asyncio.run(client.run())
except KeyboardInterrupt:
print("\n已停止")
logger.info("已停止")
def run_tianwang(enable_agent: bool, port: int):
"""完整版: HTTP API + WebSocket + AI Agent"""
import asyncio
from api.http_server import start_http_server
from core.websocket_client import QingjianAPIClient
logger.info("=" * 60)
logger.info("AI 客服系统 - 天网协作版(完整)")
logger.info("=" * 60)
start_http_server(host='0.0.0.0', port=port)
_print_api_info(port)
logger.info("=" * 60)
logger.info("正在连接轻简 API...")
client = QingjianAPIClient(enable_agent=enable_agent)
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
logger.info("=" * 60)
logger.info("系统已就绪,等待消息和任务...")
def _signal_handler(signum, frame):
logger.info("收到退出信号,正在停止...")
if hasattr(client, 'task_scheduler'):
asyncio.run(client.task_scheduler.stop())
sys.exit(0)
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
try:
asyncio.run(client.connect())
except KeyboardInterrupt:
logger.info("已停止")
except Exception as e:
logger.error(f"运行异常:{e}")
sys.exit(1)
def run_api_only(port: int):
"""仅 HTTP API不含 WebSocket / AI Agent"""
import time
from api.http_server import start_http_server
logger.info("=" * 60)
logger.info("AI 客服系统 - 天网协作版(仅 API")
logger.info("=" * 60)
start_http_server(host='0.0.0.0', port=port)
_print_api_info(port)
logger.info("=" * 60)
logger.info("系统已就绪,等待天网任务...")
def _signal_handler(signum, frame):
logger.info("收到退出信号")
sys.exit(0)
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("已停止")
def run_multi_process(num_workers: int, enable_agent: bool):
"""多进程模式"""
from scripts.multi_process_launcher import Coordinator
print("=" * 60)
print("AI 客服系统 - 多进程异步并行模式")
print("=" * 60)
print(f"工作进程数:{num_workers}")
print(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
print("=" * 60)
logger.info("=" * 60)
logger.info("AI 客服系统 - 多进程异步并行模式")
logger.info(f"工作进程数:{num_workers}")
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
logger.info("=" * 60)
coordinator = Coordinator(num_workers=num_workers)
coordinator = Coordinator(num_workers=num_workers, enable_agent=enable_agent)
try:
coordinator.start()
except KeyboardInterrupt:
logger.info("已停止")
coordinator.stop()
def run_tianwang_multi(num_workers: int, enable_agent: bool, port: int):
"""天网 + 多进程HTTP API + 多进程 WebSocket 客户端"""
from api.http_server import start_http_server
from scripts.multi_process_launcher import Coordinator
logger.info("=" * 60)
logger.info("AI 客服系统 - 天网协作版HTTP API + 多进程模式")
logger.info("=" * 60)
start_http_server(host='0.0.0.0', port=port)
_print_api_info(port)
logger.info("=" * 60)
logger.info(f"工作进程数:{num_workers}")
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
logger.info("=" * 60)
coordinator = Coordinator(num_workers=num_workers or 0, enable_agent=enable_agent)
def _signal_handler(signum, frame):
logger.info("收到退出信号,正在停止多进程协调器...")
coordinator.stop()
sys.exit(0)
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
try:
coordinator.start()
except KeyboardInterrupt:
print("\n已停止")
logger.info("已停止")
coordinator.stop()
def main():
parser = argparse.ArgumentParser(description='AI 客服系统启动器')
parser.add_argument(
'--no-agent',
action='store_true',
help='不启用 AI Agent仅基础回复'
parser = argparse.ArgumentParser(
description='AI 客服系统启动器',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
启动模式:
(默认) WebSocket 客服模式
--tianwang HTTP API + WebSocket天网完整版
--tianwang-multi HTTP API + 多进程 WebSocket
--api-only 仅 HTTP API不含 WebSocket / AI Agent
--multi 多进程模式
"""
)
parser.add_argument(
'--multi',
action='store_true',
help='多进程模式'
)
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--tianwang', action='store_true', help='天网完整版HTTP API + WebSocket')
mode.add_argument('--tianwang-multi', action='store_true', help='天网 + 多进程HTTP API + 多进程 WebSocket')
mode.add_argument('--api-only', action='store_true', help='仅 HTTP API不含 WebSocket / AI Agent')
mode.add_argument('--multi', action='store_true', help='多进程模式')
parser.add_argument(
'-w', '--workers',
type=int,
default=None,
help='工作进程数默认CPU 核心数,仅多进程模式有效)'
)
parser.add_argument('--no-agent', action='store_true', help='不启用 AI Agent')
parser.add_argument('--port', '-p', type=int, default=DEFAULT_HTTP_PORT, help=f'HTTP API 端口(默认 {DEFAULT_HTTP_PORT}')
parser.add_argument('--workers', '-w', type=int, default=None, help='工作进程数(仅多进程模式,默认 CPU 核心数)')
args = parser.parse_args()
enable_agent = not args.no_agent
if args.multi:
# 多进程模式
run_multi_process(
num_workers=args.workers,
enable_agent=enable_agent
)
if args.api_only:
run_api_only(port=args.port)
elif args.tianwang:
run_tianwang(enable_agent=enable_agent, port=args.port)
elif args.tianwang_multi:
run_tianwang_multi(num_workers=args.workers, enable_agent=enable_agent, port=args.port)
elif args.multi:
run_multi_process(num_workers=args.workers, enable_agent=enable_agent)
else:
# 单进程模式(默认)
run_single_process(enable_agent=enable_agent)
run_websocket(enable_agent=enable_agent)
if __name__ == "__main__":

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
天网协作版 - 简化版(仅 HTTP API + 任务调度,不含 AI Agent
"""
import sys
import os
import logging
import signal
from pathlib import Path
_root = Path(__file__).resolve().parent
if str(_root) not in sys.path:
sys.path.insert(0, str(_root))
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)
def main():
logger.info("=" * 60)
logger.info("天网协作版 - HTTP API 服务器")
logger.info("=" * 60)
# 1. 启动 HTTP API 服务器
from api.http_server import start_http_server
http_thread = start_http_server(host='0.0.0.0', port=6060)
logger.info("HTTP API 服务器已启动http://0.0.0.0:5678")
logger.info("")
logger.info("天网任务接口:")
logger.info(" POST /api/task/receive - 接收任务")
logger.info(" POST /api/task/cancel - 取消任务")
logger.info(" GET /api/task/status/:id - 查询任务状态")
logger.info(" GET /api/task/list - 任务列表")
logger.info(" GET /api/health - 健康检查")
logger.info("=" * 60)
logger.info("系统已就绪,等待天网任务...")
logger.info("")
# 注册信号处理
def signal_handler(signum, frame):
logger.info("收到退出信号")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 保持运行
try:
while True:
import time
time.sleep(1)
except KeyboardInterrupt:
logger.info("已停止")
if __name__ == "__main__":
main()

View File

@@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
"""
AI 客服系统 - 天网协作版启动器
支持 HTTP API 接收天网任务
"""
import sys
import os
import signal
import logging
from pathlib import Path
# 确保项目根目录在 sys.path 首位
_root = Path(__file__).resolve().parent
if str(_root) not in sys.path:
sys.path.insert(0, str(_root))
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)
def main():
logger.info("=" * 60)
logger.info("AI 客服系统 - 天网协作版")
logger.info("=" * 60)
# 1. 启动 HTTP API 服务器
logger.info("正在启动 HTTP API 服务器...")
from api.http_server import start_http_server
http_thread = start_http_server(host='0.0.0.0', port=5678)
logger.info("HTTP API 服务器已启动http://0.0.0.0:5678")
logger.info("")
logger.info("天网任务接口:")
logger.info(" POST /api/task/receive - 接收任务")
logger.info(" POST /api/task/cancel - 取消任务")
logger.info(" GET /api/task/status/:id - 查询任务状态")
logger.info(" GET /api/task/list - 任务列表")
logger.info(" GET /api/health - 健康检查")
logger.info("=" * 60)
# 2. 启动 WebSocket 客服客户端
logger.info("正在连接轻简 API...")
from core.websocket_client import QingjianAPIClient
import argparse
parser = argparse.ArgumentParser(description='AI 客服系统 - 天网协作版')
parser.add_argument('--no-agent', action='store_true', help='不启用 AI Agent')
args = parser.parse_args()
enable_agent = not args.no_agent
client = QingjianAPIClient(enable_agent=enable_agent)
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
logger.info("=" * 60)
logger.info("系统已就绪,等待消息和任务...")
logger.info("")
# 注册信号处理
def signal_handler(signum, frame):
logger.info("收到退出信号,正在停止...")
if hasattr(client, 'task_scheduler'):
import asyncio
asyncio.run(client.task_scheduler.stop())
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 启动客户端
import asyncio
try:
asyncio.run(client.connect())
except KeyboardInterrupt:
logger.info("已停止")
except Exception as e:
logger.error(f"运行异常:{e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -177,17 +177,8 @@ def cmd_live(refresh: int = 3):
try:
while True:
import sqlite3
conn = sqlite3.connect(db._DB_PATH)
conn.row_factory = sqlite3.Row
rows = conn.execute("""
SELECT id, customer_id, customer_name, direction, message, timestamp
FROM chat_logs
ORDER BY id DESC LIMIT 20
""").fetchall()
conn.close()
new_rows = [dict(r) for r in rows if r["id"] not in seen_ids]
rows = db.get_latest_messages(20)
new_rows = [r for r in rows if r["id"] not in seen_ids]
if new_rows:
new_rows.reverse()
for r in new_rows:

View File

@@ -24,22 +24,23 @@ logger = logging.getLogger(__name__)
class WorkerProcess:
"""工作进程"""
def __init__(self, worker_id: int, shard_keys: List[str]):
def __init__(self, worker_id: int, shard_keys: List[str], enable_agent: bool = True):
self.worker_id = worker_id
self.shard_keys = shard_keys
self.enable_agent = enable_agent
self.process = None
def start(self):
"""启动工作进程"""
self.process = Process(
target=self._run,
args=(self.worker_id, self.shard_keys),
args=(self.worker_id, self.shard_keys, self.enable_agent),
name=f"ai-cs-worker-{self.worker_id}"
)
self.process.start()
logger.info(f"Worker {self.worker_id} 启动 (PID: {self.process.pid})")
def _run(self, worker_id: int, shard_keys: List[str]):
def _run(self, worker_id: int, shard_keys: List[str], enable_agent: bool):
"""工作进程入口"""
try:
# 设置进程环境变量
@@ -50,7 +51,7 @@ class WorkerProcess:
from core.websocket_client import QingjianAPIClient
logger.info(f"Worker {worker_id} 初始化 Agent...")
client = QingjianAPIClient(enable_agent=True)
client = QingjianAPIClient(enable_agent=enable_agent)
# 只处理分配给这个 worker 的客户
client.shard_keys = set(shard_keys)
@@ -77,10 +78,11 @@ class WorkerProcess:
class Coordinator:
"""协调器 - 管理多个工作进程"""
def __init__(self, num_workers: int = None):
def __init__(self, num_workers: int = None, enable_agent: bool = True):
self.num_workers = num_workers or max(2, cpu_count())
self.workers: List[WorkerProcess] = []
self.running = False
self.enable_agent = enable_agent
def _get_shard_key(self, acc_id: str, from_id: str) -> int:
"""根据店铺 ID + 客户 ID 计算分片 key"""
@@ -117,7 +119,8 @@ class Coordinator:
for worker_id in range(self.num_workers):
worker = WorkerProcess(
worker_id=worker_id,
shard_keys=shards.get(worker_id, [])
shard_keys=shards.get(worker_id, []),
enable_agent=self.enable_agent
)
worker.start()
self.workers.append(worker)

View File

@@ -16,15 +16,12 @@ from pathlib import Path
import logging
from utils.service_base import BaseService
logger = logging.getLogger(__name__)
class ServiceBase:
"""最小化基类,替代缺失的 utils.service_base"""
pass
class GeminiExtractV2Service(ServiceBase):
class GeminiExtractV2Service(BaseService):
"""Gemini印花提取V2服务类 - 使用服务,更经济"""
SERVICE_NAME = "gemini_extract_v2"
@@ -69,7 +66,7 @@ class GeminiExtractV2Service(ServiceBase):
DEFAULT_PROMPT = "提取印花图案把褶皱移除。补齐缺失的部分要生成完整细节丰富严格按照原图的元素位置生成平面的印花图不要相似的相似度要100%,生成高质量的印刷图"
# DEFAULT_PROMPT = "生成图片,把衣服的图案展开起来做成数码印花印刷平面图。去掉皱褶,生成图案增强细节。排除衣服图案以外内容"
def __init__(self):
super().__init__()
super().__init__(name="gemini_extract_v2")
self.session = None
def image_to_base64(self, image_path: str) -> str:

313
tests/test_ai_chat.py Normal file
View File

@@ -0,0 +1,313 @@
"""
AI Agent 对话测试脚本
从数据库加载聊天记录,测试 AI 回复效果
"""
import sqlite3
import asyncio
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',
}
def cprint(text, color='reset'):
print(f"{COLORS.get(color, '')}{text}{COLORS['reset']}")
def check_database():
"""检查数据库内容"""
db_path = 'db/chat_log_db/chats.db'
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/chat_log_db/chats.db')
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/chat_log_db/chats.db')
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')

View File

@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
"""配置中心测试"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
def test_config_paths():
from config.config import ROOT, LOG_DIR, RESULTS_DIR, CONFIG_DIR
assert ROOT.exists()
assert ROOT.is_dir()
assert (ROOT / "config").samefile(CONFIG_DIR)
assert LOG_DIR == ROOT / "logs"
print("config paths OK")
def test_config_values():
from config.config import (
IMAGE_QUEUE_MAX_CONCURRENT,
IMAGE_QUEUE_MAX_SIZE,
LOG_MAX_BYTES,
LOG_BACKUP_COUNT,
)
assert IMAGE_QUEUE_MAX_CONCURRENT >= 1
assert IMAGE_QUEUE_MAX_SIZE >= 1
assert LOG_MAX_BYTES > 0
assert LOG_BACKUP_COUNT >= 1
print("config values OK")
if __name__ == "__main__":
test_config_paths()
test_config_values()
print("All config tests passed")

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
"""设计师派单数据库测试"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
def test_list_designers():
"""测试列出设计师(不修改数据)"""
from db.designer_roster_db import list_designers
designers = list_designers()
assert isinstance(designers, list)
print(f"designer roster: {len(designers)} designers")
print("designer roster OK")
if __name__ == "__main__":
test_list_designers()
print("All designer roster tests passed")

View File

@@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
"""健康检查测试"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
def test_set_status():
"""测试状态设置"""
from utils.health_check import set_qingjian_connected, set_wechat_ok
set_qingjian_connected(True)
set_qingjian_connected(False)
set_wechat_ok(True)
print("health check status OK")
async def test_run_check():
"""测试执行健康检查(不实际发告警)"""
from utils.health_check import run_health_check
await run_health_check(lambda: True)
print("health check run OK")
if __name__ == "__main__":
import asyncio
test_set_status()
asyncio.run(test_run_check())
print("All health check tests passed")

View File

@@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
"""图片队列测试"""
import sys
import asyncio
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
async def test_queue_semaphore():
"""测试队列并发限制"""
from utils.image_queue import init, run_with_queue, release
init(max_concurrent=2, max_queue=5)
running = 0
max_running = 0
async def fake_task():
nonlocal running, max_running
running += 1
max_running = max(max_running, running)
await asyncio.sleep(0.1)
running -= 1
return "ok"
results = await asyncio.gather(
run_with_queue(fake_task()),
run_with_queue(fake_task()),
run_with_queue(fake_task()),
)
assert all(r == "ok" for r in results)
assert max_running <= 2
print("image queue OK")
if __name__ == "__main__":
asyncio.run(test_queue_semaphore())
print("All image queue tests passed")

View File

@@ -1,140 +0,0 @@
"""
端到端测试:模拟客户付款后的自动图片处理流程
运行python test_process.py
"""
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
import asyncio
import os
from dotenv import load_dotenv
load_dotenv()
# ── 测试参数 ────────────────────────────────────────────────
TEST_CUSTOMER_ID = "test_user_001"
TEST_ACC_ID = "小威哥1216"
TEST_IMAGE_URL = (
"https://img.alicdn.com/imgextra/i3/O1CN01tqQst21qIEOdcUOCQ"
"_!!4611686018427380880-0-amp.jpg"
)
# ────────────────────────────────────────────────────────────
async def step1_analyze():
"""Step 1: 图片分析(模拟 analyze_image 工具调用)"""
print("\n" + "="*60)
print("Step 1: 图片分析")
print("="*60)
from image.image_analyzer import image_analyzer
result = await image_analyzer.analyze(TEST_IMAGE_URL)
print(f"分析结果: {result}")
return result
async def step2_create_task(analysis: dict):
"""Step 2: 创建 Workflow 任务(模拟 image_analysis_result"""
print("\n" + "="*60)
print("Step 2: 创建 Workflow 任务")
print("="*60)
from core.workflow import workflow
await workflow.image_analysis_result(
customer_id = TEST_CUSTOMER_ID,
image_url = TEST_IMAGE_URL,
complexity = analysis.get("complexity", "normal"),
acc_id = TEST_ACC_ID,
acc_type = "AliWorkbench",
gemini_prompt= analysis.get("gemini_prompt", ""),
aspect_ratio = analysis.get("aspect_ratio", "1:1"),
perspective = analysis.get("perspective", "no"),
proc_type = analysis.get("proc_type", ""),
subject = analysis.get("subject", ""),
quality = analysis.get("quality", ""),
)
task_id = workflow.customer_active_task.get(TEST_CUSTOMER_ID)
if task_id:
task = workflow.tasks[task_id]
print(f"任务已创建: {task_id[:8]}...")
print(f" requirements: {task.requirements}")
print(f" image: {task.original_image[:80]}...")
else:
print("⚠️ 任务创建失败!")
sys.exit(1)
return task_id
async def step3_trigger_payment():
"""Step 3: 模拟付款触发处理(等待 Gemini 完成)"""
print("\n" + "="*60)
print("Step 3: 模拟付款,触发 Gemini 处理(同步等待完成)")
print("="*60)
from core.workflow import workflow
# 注入发送函数(避免真实发消息,只打印)
async def fake_send(**kw):
cid = kw.get("customer_id", kw.get("from_id", "?"))
content = kw.get("content", kw.get("msg", ""))
print(f"[FAKE SEND -> {cid}] {str(content)[:120]}")
workflow._send_message = fake_send
# 直接调用 _auto_process 同步等待,而非后台 task
task_id = workflow.customer_active_task.get(TEST_CUSTOMER_ID)
if not task_id:
print(" 找不到待处理任务!")
return
print(f" 开始处理任务: {task_id[:8]}...")
await workflow._auto_process(task_id, acc_id=TEST_ACC_ID, acc_type="AliWorkbench")
async def step4_check_result():
"""Step 4: 检查结果"""
print("\n" + "="*60)
print("Step 4: 检查处理结果")
print("="*60)
result_dir = os.getenv("RESULT_IMAGE_DIR", "results")
if not os.path.exists(result_dir):
print(f"结果目录不存在: {result_dir}")
return
files = sorted(
[f for f in os.listdir(result_dir) if f.startswith("result_")],
key=lambda f: os.path.getmtime(os.path.join(result_dir, f)),
reverse=True,
)
if files:
latest = files[0]
path = os.path.join(result_dir, latest)
size = os.path.getsize(path)
print(f"最新结果文件: {latest}")
print(f" 大小: {size:,} bytes ({size/1024:.1f} KB)")
else:
print("结果目录为空,可能处理失败")
async def main():
print("=" * 60)
print(" 图片处理流程端到端测试")
print("=" * 60)
print(f"测试图片: {TEST_IMAGE_URL[:80]}...")
try:
analysis = await step1_analyze()
await step2_create_task(analysis)
await step3_trigger_payment()
await step4_check_result()
print("\n[OK] 测试完成")
except KeyboardInterrupt:
print("\n测试被中断")
except Exception as e:
import traceback
print(f"\n[FAIL] 测试失败: {e}")
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,53 +0,0 @@
# -*- coding: utf-8 -*-
"""测试转人工流程:设计师在线查询 + 派单 + 无人在线时企微提醒"""
import asyncio
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
# 测试用 API 地址(文档中的)
os.environ.setdefault("DESIGNER_ROSTER_API", "http://huichang.online:8001/online")
async def main():
from utils.designer_roster import poll_and_update_roster
from db.designer_roster_db import list_designers, get_transfer_group_for_shop
print("1. 查询并同步设计师在线状态...")
await poll_and_update_roster()
print(" OK")
print("\n2. 当前设计师状态:")
for d in list_designers():
status = "在线" if d["is_online"] else "离线"
print(f" - {d['name']} ({d['wechat_user_id']}): {status}")
print("\n3. 派单测试 (店铺: 小威哥1216):")
shop_id = "小威哥1216"
group_id = get_transfer_group_for_shop(shop_id)
if group_id:
print(f" 派单成功 -> group_id={group_id}")
else:
print(" 无人在线,将回退到静态配置")
print(" (转人工时会发企微「谁在线啊」)")
print("\n4. 静态回退:")
from config.config import CONFIG_DIR
import json
cfg_path = CONFIG_DIR / "transfer_groups.json"
default = "20252916034"
if cfg_path.exists():
with open(cfg_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
fallback = cfg.get(shop_id, cfg.get("default", default))
else:
fallback = default
print(f" 回退分组: {fallback}")
print("\n测试完成")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
"""转接分组测试"""
import sys
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
def test_get_transfer_group():
"""测试转接分组查找逻辑"""
from config.config import CONFIG_DIR
config_path = CONFIG_DIR / "transfer_groups.json"
default = "20252916034"
if not config_path.exists():
print("transfer_groups.json 不存在,跳过")
return
with open(config_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
got = cfg.get("default", default)
assert got
print(f"default group: {got}")
print("transfer groups OK")
if __name__ == "__main__":
test_get_transfer_group()
print("All transfer tests passed")

View File

@@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
"""测试企微「谁在线啊」消息发送"""
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
async def main():
from config.config import WECHAT_WEBHOOK
import httpx
if not WECHAT_WEBHOOK:
print("未配置 WECHAT_WEBHOOK无法测试")
return
print(f"发送测试消息到企微...")
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(WECHAT_WEBHOOK, json={
"msgtype": "text",
"text": {"content": "谁在线啊"}
})
print(f"状态码: {resp.status_code}")
print(f"响应: {resp.text}")
if resp.status_code == 200:
data = resp.json()
if data.get("errcode") == 0:
print("发送成功,请检查企微群是否收到")
else:
print(f"企微返回错误: {data}")
else:
print("发送失败")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,229 +0,0 @@
# 三种工作流功能说明
## 📋 功能说明
根据客户说的话,自动判断执行不同的工作流程:
1. **"找一下这个图"** → 查找图片工作流
2. **"做一下"** → 处理图片工作流
3. **"做不了"** → 转人工派单工作流
---
## 🔄 工作流 1查找图片
### 触发条件
客户说:
- "找一下这个图"
- "找图"
- "找原图"
- "帮我找"
- "能找到吗"
- "有吗"
- "有没有"
### 执行流程
```
客户:找一下这个图 [发送图片]
AI 检测到"找一下"关键词
执行查找图片工作流
1. 创建任务operation=find
2. 上传图片到图绘平台
3. 更新任务状态为 completed
回复客户:"找到了图片在这里http://tuhui.cloud/works/123"
```
### 示例对话
```
客户:找一下这个图 [图片]
AI: 找到了图片在这里http://tuhui.cloud/works/123
```
---
## 🔄 工作流 2处理图片
### 触发条件
客户说:
- "做一下"
- "处理一下"
- "安排"
- "开始做"
- "弄一下"
- "修一下"
- "P 一下"
- "P 图"
### 执行流程
```
客户:做一下 [发送图片]
AI 检测到"做一下"关键词
执行处理图片工作流
1. 创建任务operation=enhance
2. 回复客户"稍等,我看看...好的,可以做,马上处理"
3. 启动图片处理流程
处理完成后发送结果给客户
```
### 示例对话
```
客户:做一下 [图片]
AI: 稍等,我看看...好的,可以做,马上处理
[处理中...]
AI: 做好了,请查看 [结果图]
```
---
## 🔄 工作流 3转人工派单
### 触发条件
客户说:
- "做不了"
- "处理不了"
- "弄不了"
- "无法处理"
- "做不到"
- "搞不定"
或者 AI 判断无法处理时
### 执行流程
```
AI做不了
检测到"做不了"关键词
执行转人工派单工作流
1. 创建任务operation=manual
2. 查询企业微信在线设计师
- 调用 API: GET http://huichang.online:8001/online
3. 有人在线 → 派单给设计师
无人在线 → 通知客户稍后联系
回复客户:"好的,已帮您安排设计师处理,请稍候"
```
### 示例对话
```
客户:这个能做吗 [图片]
AI: 抱歉,这个我做不了,帮您转接设计师
[查询在线设计师...]
AI: 好的,已帮您安排设计师处理,请稍候
```
---
## 🔧 技术实现
### 1. 工作流程路由器
文件:`/root/ai_customer_service/ai_cs/core/workflow_router.py`
**关键词检测**
```python
find_keywords = ["找一下", "找图", "找原图", ...]
process_keywords = ["做一下", "处理一下", "安排", ...]
unable_keywords = ["做不了", "处理不了", "弄不了", ...]
```
### 2. 工作流执行器
文件:`/root/ai_customer_service/ai_cs/core/workflow.py`
**三种方法**
- `find_image_workflow()` - 查找图片
- `process_image_workflow()` - 处理图片
- `transfer_to_designer_workflow()` - 转人工派单
### 3. 消息处理器
文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py`
**处理方法**
```python
async def _handle_image_workflow(self, message, data, image_urls):
# 检测工作流类型
workflow_type, confidence = self.workflow_router.detect_workflow(message)
# 执行对应工作流
if workflow_type == "find_image":
await workflow.find_image_workflow(...)
elif workflow_type == "process_image":
await workflow.process_image_workflow(...)
elif workflow_type == "transfer_human":
await workflow.transfer_to_designer_workflow(...)
```
---
## 📊 设计师在线状态 API
### 查询在线设计师
**接口**`GET http://huichang.online:8001/online`
**返回**
```json
{
"online_count": 2,
"online_users": ["lz", "ZuoWei"],
"update_time": "2026-02-27 18:40:00"
}
```
### 更新设计师状态
**接口**`POST http://huichang.online:8001/update-status`
**请求**
```json
{
"username": "lz",
"status": "online"
}
```
---
## ⚠️ 注意事项
1. **关键词匹配**:支持多种说法,自动识别
2. **置信度**>0.9 才执行对应工作流
3. **无人在线处理**:通知客户稍后联系,企业微信预警
4. **任务记录**:所有工作流都保存到数据库
5. **状态追踪**:任务状态自动流转
---
## 🔍 日志查看
```bash
# 查看工作流执行日志
grep "工作流" /tmp/ai-cs.log
# 查看派单记录
grep "派单" /tmp/ai-cs.log
# 查看在线设计师查询
grep "在线设计师" /tmp/ai-cs.log
```
---
**文档版本**: v1.0
**更新日期**: 2026-02-27

View File

@@ -1,207 +0,0 @@
# 作图失败转接人工功能
## 📋 功能说明
当 AI 作图失败或效果不佳时,系统会自动转接人工客服处理。
---
## 🔄 触发场景
### 1. AI 作图失败
**触发条件**
- API 调用失败
- 图片处理超时
- 图片质量不达标
- Gemini/Qwen API 报错
**自动转接**
```
作图失败:[错误信息],请稍后重试,我帮您转接人工处理
```
---
### 2. 客户不满意
**触发条件**
- 客户说"效果不好"
- 客户说"不满意"
- 客户要求重做多次
**自动转接**
```
好的,我帮您转接人工客服处理
```
---
### 3. 特殊要求
**触发条件**
- 客户有特殊需求
- AI 无法处理的复杂需求
- 需要人工判断的情况
**自动转接**
```
这个需求比较特殊,我帮您转接人工客服
```
---
## ⚠️ 转接流程
```
客户发送图片
AI 尝试作图
┌───────────────────────┐
│ 作图成功? │
└───────────┬───────────┘
NO │ YES
┌───────────────┐
│ 作图失败 │
│ 自动转人工 │
└───────┬───────┘
┌───────────────┐
│ 通知客户 │
│ "帮您转人工" │
└───────┬───────┘
┌───────────────┐
│ 转接人工客服 │
│ 说明失败原因 │
└───────────────┘
```
---
## 📝 转接话术
### 作图失败
```
- 作图失败:[错误信息],请稍后重试,我帮您转接人工处理
- 作图失败:[错误信息],我帮您转接人工客服处理
- 处理遇到点问题,我帮您转接人工客服
```
### 客户不满意
```
- 好的,我帮您转接人工客服处理
- 明白,让同事来帮您看看
- 这个我帮您转接人工客服
```
### 特殊要求
```
- 这个需求比较特殊,我帮您转接人工客服
- 这个需要人工判断,我帮您转接
- 稍等,我让同事来帮您处理
```
---
## 🔧 技术实现
### 失败检测
文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py`
**作图函数**
```python
async def process_image_gemini(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
try:
# 作图逻辑
...
except Exception as e:
# 作图失败,自动转接
return f"作图失败:{e},请稍后重试,我帮您转接人工处理"
```
---
### 转接工具
**transfer_to_human 工具**
```python
async def transfer_to_human(ctx: RunContext[AgentDeps]) -> str:
"""转接人工客服"""
# 标记需要转接
st.need_transfer = True
st.transfer_reason = "作图失败"
return "好的,帮您转接人工客服"
```
---
## 📊 转接统计
### 转接原因分类
| 原因 | 比例 | 说明 |
|------|------|------|
| 作图失败 | 40% | API 报错/超时/失败 |
| 客户不满意 | 30% | 效果不好/要求重做 |
| 特殊要求 | 20% | AI 无法处理的复杂需求 |
| 其他 | 10% | 投诉/退款等 |
---
## ⚠️ 注意事项
1. **失败必转**:作图失败必须转人工,不自动重试超过 2 次
2. **告知客户**:转接前告知客户原因
3. **记录原因**:记录转接原因便于后续优化
4. **快速响应**:转接后人工客服需快速响应
---
## 🔍 日志查看
### 查看转接记录
```bash
# 查看转接日志
grep "转接人工" /tmp/ai-cs.log
# 查看作图失败
grep "作图失败" /tmp/ai-cs.log
# 查看转接统计
grep "TRANSFER" /tmp/ai-cs.log | wc -l
```
---
## 📈 优化建议
### 降低转接率
1. **提升作图成功率**
- 优化 API 调用逻辑
- 增加重试机制(最多 2 次)
- 降级兜底方案
2. **提升客户满意度**
- 提前告知预期效果
- 提供效果样例
- 设置合理期望
3. **识别特殊需求**
- 提前识别复杂需求
- 直接转人工,避免无效尝试
---
**文档版本**: v1.0
**更新日期**: 2026-02-27

View File

@@ -1,247 +0,0 @@
# 图片任务数据库功能说明
## 📋 功能说明
图片任务现在会保存到 SQLite 数据库,支持:
1. ✅ 任务持久化(重启不丢失)
2. ✅ 客户后续增加需求细节
3. ✅ 需求变更历史记录
4. ✅ 任务状态追踪
---
## 🗄️ 数据库表结构
### 1. image_tasks图片任务表
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | TEXT | 任务 ID主键 |
| customer_id | TEXT | 客户 ID |
| customer_name | TEXT | 客户名称 |
| original_image | TEXT | 原图 URL |
| operation | TEXT | 操作类型enhance/remove_bg/vectorize |
| requirements | TEXT | 需求 JSON复杂度、比例、透视等 |
| customer_notes | TEXT | 客户备注/需求细节 |
| status | TEXT | 状态pending/paid/processing/completed/failed |
| created_at | TEXT | 创建时间 |
| paid_at | TEXT | 付款时间 |
| started_at | TEXT | 开始处理时间 |
| completed_at | TEXT | 完成时间 |
| result_image | TEXT | 结果图 URL |
| error_message | TEXT | 错误信息 |
| retry_count | INTEGER | 重试次数 |
| acc_id | TEXT | 店铺 ID |
| acc_type | TEXT | 平台类型 |
---
### 2. task_requirement_changes需求变更表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INTEGER | 自增 ID主键 |
| task_id | TEXT | 任务 ID外键 |
| change_type | TEXT | 变更类型add_note/modify_operation/add_requirement |
| old_value | TEXT | 旧值 |
| new_value | TEXT | 新值 |
| changed_at | TEXT | 变更时间 |
| changed_by | TEXT | 变更者customer/staff |
---
## 🎯 使用场景
### 场景 1客户发送图片创建任务
```python
# AI 识别图片后自动创建
workflow.image_analysis_result(
customer_id="customer_123",
image_url="https://xxx.com/image.jpg",
complexity="normal",
aspect_ratio="1:1",
perspective="no"
)
# 自动保存到数据库
# 任务状态pending待付款
```
---
### 场景 2客户付款开始处理
```python
# 客户付款后
workflow.trigger_processing_on_payment(
customer_id="customer_123"
)
# 任务状态更新pending → paid → processing
```
---
### 场景 3客户增加需求细节
```python
# 客户说:"对了,需要去掉背景"
await workflow.add_customer_requirement(
task_id="TASK_20260227_001",
customer_id="customer_123",
requirement="需要去掉背景"
)
# 数据库记录:
# customer_notes: "[02-27 18:00] 需要去掉背景"
# 任务变更表新增记录
```
---
### 场景 4客户修改操作类型
```python
# 客户说:"改成要分层文件"
await workflow.modify_operation(
task_id="TASK_20260227_001",
customer_id="customer_123",
new_operation="remove_bg_and_layer"
)
# 数据库更新:
# operation: "enhance" → "remove_bg_and_layer"
# 任务变更表新增记录
```
---
### 场景 5查看需求变更历史
```python
# 查看客户修改了哪些需求
history = workflow.get_task_requirement_history("TASK_20260227_001")
# 返回:
# [
# {"change_type": "modify_operation", "old_value": "enhance", "new_value": "remove_bg_and_layer", ...},
# {"change_type": "add_note", "old_value": "无", "new_value": "需要去掉背景", ...}
# ]
```
---
## 🔄 任务状态流转
```
pending待付款
paid已付款
processing处理中
awaiting_confirm待确认
completed已完成
[failed失败] ← 可能失败
```
---
## 📝 API 接口
### 创建任务
```python
workflow.create_image_task(
customer_id="customer_123",
original_image="https://xxx.com/image.jpg",
operation="enhance"
)
```
### 添加需求
```python
await workflow.add_customer_requirement(
task_id="TASK_001",
customer_id="customer_123",
requirement="需要高分辨率"
)
```
### 修改操作
```python
await workflow.modify_operation(
task_id="TASK_001",
customer_id="customer_123",
new_operation="vectorize"
)
```
### 查询任务
```python
task = workflow.get_task("TASK_001")
tasks = workflow.get_customer_tasks("customer_123")
```
### 查询历史
```python
history = workflow.get_task_requirement_history("TASK_001")
```
---
## 🔍 数据库操作
### 查看数据库文件
```bash
sqlite3 /root/ai_customer_service/ai_cs/db/image_tasks.db
```
### 查询所有任务
```sql
SELECT task_id, customer_id, status, created_at FROM image_tasks ORDER BY created_at DESC LIMIT 10;
```
### 查询待处理任务
```sql
SELECT * FROM image_tasks WHERE status='pending';
```
### 查询客户需求变更
```sql
SELECT task_id, change_type, old_value, new_value, changed_at, changed_by
FROM task_requirement_changes
WHERE task_id='TASK_001'
ORDER BY changed_at DESC;
```
---
## ⚠️ 注意事项
1. **任务持久化**:所有任务自动保存到数据库,重启不丢失
2. **需求变更**:客户可以随时增加需求细节(付款前)
3. **操作修改**:付款前可以修改操作类型,付款后不允许
4. **历史记录**:所有变更都有记录,可追溯
5. **状态管理**:任务状态自动流转,无需手动更新
---
## 📊 数据库位置
```
/root/ai_customer_service/ai_cs/db/image_tasks.db
```
数据库管理器:
```
/root/ai_customer_service/ai_cs/db/image_tasks_db.py
```
---
**文档版本**: v1.0
**更新日期**: 2026-02-27

View File

@@ -1,278 +0,0 @@
# 图绘派单系统集成说明
## 📋 功能说明
AI 客服系统已接入图绘派单系统 API实现自动派单给在线设计师。
---
## 🚀 派单系统 API
### 基础信息
- **API 地址**: http://1.12.50.92:8005
- **API Key**: tuhui_dispatch_key_2026
- **认证方式**: Header: X-API-Key
### 核心接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/dispatch/queue` | GET | 获取派单队列 |
| `/online/designers` | GET | 获取在线设计师 |
| `/tasks` | POST | 创建任务 |
| `/tasks/{id}/assign` | POST | 分配任务 |
| `/tasks/{id}` | GET | 查询任务状态 |
| `/tasks/{id}/complete` | POST | 完成任务 |
---
## 🔄 工作流程
### 转人工派单流程
```
客户:这个能做吗 [图片]
AI: 抱歉,这个我做不了,帮您转接设计师
1. 查询在线设计师
GET /online/designers
返回:["橘子", "婷婷"]
2. 创建派单任务
POST /tasks
返回:{"task_id": "ea853bd9"}
3. 分配给设计师
POST /tasks/ea853bd9/assign
{"designer_name": "橘子"}
4. 企业微信通知设计师
5. 回复客户
"好的,已帮您安排设计师处理,请稍候"
```
---
## 📝 API 调用示例
### 1. 查询在线设计师
```python
from services.service_tuhui_dispatch import get_tuhui_dispatch_client
client = get_tuhui_dispatch_client()
designers = await client.get_online_designers()
# 返回:["橘子", "婷婷"]
```
### 2. 创建任务
```python
task_id = await client.create_task(
task_name="图片处理 -1234",
description="客户需要做高清修复",
task_type="image_process",
priority=2
)
# 返回:"ea853bd9"
```
### 3. 分配任务
```python
success = await client.assign_task(
task_id="ea853bd9",
designer_name="橘子",
notes="AI 客服自动派单"
)
# 返回True
```
### 4. 查询任务状态
```python
status = await client.get_task_status("ea853bd9")
# 返回:{"status": "assigned", "assigned_to": "橘子", ...}
```
### 5. 完成任务
```python
success = await client.complete_task(
task_id="ea853bd9",
notes="客户已确认,效果满意"
)
# 返回True
```
---
## 🔧 集成代码位置
### 派单客户端
文件:`/root/ai_customer_service/ai_cs/services/service_tuhui_dispatch.py`
**类**: `TuhuiDispatchClient`
**方法**:
- `get_online_designers()` - 查询在线设计师
- `create_task()` - 创建任务
- `assign_task()` - 分配任务
- `get_task_status()` - 查询状态
- `complete_task()` - 完成任务
---
### 工作流集成
文件:`/root/ai_customer_service/ai_cs/core/workflow.py`
**方法**:
- `transfer_to_designer_workflow()` - 转人工派单工作流
- `_get_online_designers()` - 查询在线设计师
- `_dispatch_to_designer()` - 派单给设计师
---
## 📊 派单逻辑
### 1. 查询在线设计师
```python
designers = await workflow._get_online_designers()
# 返回:["橘子", "婷婷"]
```
### 2. 创建派单任务
```python
dispatch_task_id = await workflow.dispatch_client.create_task(
task_name=f"图片处理-{customer_id[-4:]}",
description=f"{reason}\\n客户{customer_id}\\n图片{image_url}",
task_type="image_process",
priority=2
)
```
### 3. 分配给设计师
```python
success = await workflow.dispatch_client.assign_task(
task_id=dispatch_task_id,
designer_name=designer_name, # "橘子"
notes=f"AI 客服自动派单\\n原因{reason}\\n客户{customer_id}"
)
```
### 4. 企业微信通知
```python
await _wechat_notify(
f"📋 **新任务派单**\\n"
f"设计师:{designer_name}\\n"
f"任务 ID: {dispatch_task_id}\\n"
f"客户:{customer_id}\\n"
f"原因:{reason}\\n"
f"请及时处理"
)
```
---
## ⚠️ 注意事项
1. **API Key**: 固定为 `tuhui_dispatch_key_2026`
2. **超时设置**: 默认 10 秒
3. **错误处理**: 所有 API 调用都有 try-except
4. **企业微信通知**: 派单成功后自动发送
5. **任务命名**: 格式为 `图片处理-{客户 ID 后 4 位}`
---
## 🔍 日志查看
```bash
# 查看派单日志
grep "派单" /tmp/ai-cs.log
# 查看在线设计师查询
grep "在线设计师" /tmp/ai-cs.log
# 查看任务创建
grep "创建任务" /tmp/ai-cs.log
```
---
## 📈 派单统计
### 查询派单队列
```bash
curl -X GET "http://1.12.50.92:8005/dispatch/queue" \
-H "X-API-Key: tuhui_dispatch_key_2026"
```
**返回**:
```json
{
"pending_tasks": {"count": 3, "tasks": [...]},
"online_designers": {"count": 2, "designers": ["橘子", "婷婷"]},
"suggestion": "橘子"
}
```
---
## 🎯 完整示例
### AI 客服转人工派单
```python
# 客户说"做不了"
await workflow.transfer_to_designer_workflow(
customer_id="customer_123",
image_url="https://xxx.com/image.jpg",
acc_id="shop_001",
acc_type="AliWorkbench",
reason="图片过于模糊AI 无法处理"
)
# 执行流程:
# 1. 查询在线设计师 → ["橘子", "婷婷"]
# 2. 创建任务 → "ea853bd9"
# 3. 分配给"橘子"
# 4. 企业微信通知
# 5. 回复客户:"好的,已帮您安排设计师处理"
```
---
## 📞 技术支持
**派单系统服务器**: 1.12.50.92:8005
**查看派单系统日志**:
```bash
tail -f /var/log/dispatch_api.log
```
**查看派单进程**:
```bash
ps aux | grep dispatch
```
**重启派单服务**:
```bash
pkill -f dispatch_api
cd /root/tuhui/backend
nohup python3 -m uvicorn dispatch_api:app --host 0.0.0.0 --port 8005 > /var/log/dispatch_api.log 2>&1 &
```
---
**文档版本**: v1.0
**更新日期**: 2026-02-27

View File

@@ -1,225 +0,0 @@
# 图片文字检测与加价功能
## 📋 功能说明
AI 客服现在可以分析图片中的文字数量,并根据文字数量和分层需求自动加价。
---
## 💰 价格规则
### 基础价格
| 复杂度 | 价格区间 | 说明 |
|--------|----------|------|
| simple | 10-15 元 | 画面简单干净 |
| normal | 15-20 元 | 一般复杂度 |
| complex | 20-25 元 | 细节偏多 |
| hard | 25-30 元 | 非常复杂 |
### 文字数量加价
| 文字数量 | 加价 | 说明 |
|----------|------|------|
| none | +0 元 | 无文字 |
| 少量 (1-10 字) | +5 元 | 少量文字 |
| 中量 (11-50 字) | +15 元 | 中量文字 |
| 大量 (51-200 字) | +30 元 | 大量文字 |
| 极多 (200 字以上) | +50 元 | 极多文字 |
### 文字分层需求加价
| 分层需求 | 加价 | 说明 |
|----------|------|------|
| no | +0 元 | 普通图片处理 |
| yes (有文字) | +50 元起 | 可编辑分层文件PSD 等) |
| yes (无文字) | +30 元 | 仅需分层文件 |
### 特殊价格:文字分层 + 大量文字
**条件**:文字数量=大量/极多 且 文字分层需求=yes
**价格范围**60-80 元
---
## 🎯 使用场景
### 场景 1少量文字不分层
**客户**[发送一张有 5 个字的图片]
**AI 分析**
- 复杂度simple
- 文字数量:少量 (1-10 字)
- 分层需求no
**报价**
```
基础价格15 元
文字加价:+5 元
总计20 元
```
**AI 回复**
```
这张图比较简单不过有少量文字需要处理20 元。
```
---
### 场景 2大量文字需要分层
**客户**[发送一张有 100 多字的图片]
**客户**:需要可以编辑的分层文件
**AI 分析**
- 复杂度complex
- 文字数量:大量 (51-200 字)
- 分层需求yes
**报价**
```
基础价格25 元
文字加价:+30 元
分层加价:+50 元
总计105 元 → 调整到 80 元(特殊价格上限)
```
**AI 回复**
```
这张图文字比较多,有 100 多字而且需要分层文件处理起来比较麻烦80 元。
文字处理 +30 元,分层 +50 元。
```
---
### 场景 3极多文字需要分层高客单价
**客户**[发送一张有 300 多字的图片]
**客户**:要 PSD 分层文件
**AI 分析**
- 复杂度hard
- 文字数量:极多 (200 字以上)
- 分层需求yes
**报价**
```
基础价格30 元
文字加价:+50 元
分层加价:+50 元
总计130 元 → 调整到 80 元(特殊价格上限)
```
**AI 回复**
```
这张图文字非常多,有 300 多字还需要分层文件处理起来很费时间80 元。
```
---
## 🔧 技术实现
### 1. 图片分析器增强
文件:`/root/ai_customer_service/ai_cs/image/image_analyzer.py`
**新增字段**
- `text_amount`: 文字数量none/少量/中量/大量/极多)
- `text_layer_need`: 分层需求yes/no
- `text_surcharge`: 文字加价金额
- `layer_surcharge`: 分层加价金额
### 2. AI 客服话术
文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py`
**报价说明**
- 自动显示文字加价
- 自动显示分层加价
- 特殊价格自动调整到 60-80 元范围
---
## 📝 配置说明
### 价格配置
修改 `/root/ai_customer_service/ai_cs/image/image_analyzer.py` 中的价格规则:
```python
# 文字数量加价
text_surcharge = 0
if text_amount == "少量 (1-10 字)":
text_surcharge = 5
elif text_amount == "中量 (11-50 字)":
text_surcharge = 15
elif text_amount == "大量 (51-200 字)":
text_surcharge = 30
elif text_amount == "极多 (200 字以上)":
text_surcharge = 50
# 分层加价
if text_layer_need == "yes":
layer_surcharge = max(50, price_suggest)
# 特殊价格60-80 元
if text_amount in ["大量", "极多"] and text_layer_need == "yes":
price_suggest = max(60, min(80, price_suggest))
```
---
## 📊 价格计算流程
```
客户发送图片
AI 分析图片
┌───────────────────────┐
│ 1. 判断基础复杂度 │
│ simple/normal/ │
│ complex/hard │
└───────────┬───────────┘
┌───────────────────────┐
│ 2. 检测文字数量 │
│ none/少量/中量/ │
│ 大量/极多 │
└───────────┬───────────┘
┌───────────────────────┐
│ 3. 询问分层需求 │
│ yes/no │
└───────────┬───────────┘
┌───────────────────────┐
│ 4. 计算总价 │
│ 基础 + 文字 + 分层 │
└───────────┬───────────┘
┌───────────────────────┐
│ 5. 特殊价格处理 │
│ 60-80 元范围 │
└───────────┬───────────┘
报价给客户
```
---
## ⚠️ 注意事项
1. **文字数量检测**:通过视觉 AI 自动识别
2. **分层需求**:需要询问客户或从对话中识别
3. **价格上限**:文字分层 + 大量文字最高 80 元
4. **价格下限**:文字分层 + 大量文字最低 60 元
5. **价格取整**:最终价格必须是 5 的倍数
---
**文档版本**: v1.0
**更新日期**: 2026-02-27

View File

@@ -1,91 +1,228 @@
# AI 客服系统 - 天网协作版部署文档
# AI 客服系统 - 部署与运维文档
## 📋 系统架构
**版本**: v1.0 | **更新日期**: 2026-02-28
---
## 目录
1. [系统架构](#系统架构)
2. [快速部署](#快速部署)
3. [启动方式](#启动方式)
4. [生产环境部署](#生产环境部署)
5. [多进程架构](#多进程架构)
6. [API 接口文档](#api-接口文档)
7. [触发条件详解](#触发条件详解)
8. [数据库](#数据库)
9. [配置说明](#配置说明)
10. [监控与日志](#监控与日志)
11. [故障排查](#故障排查)
---
## 系统架构
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 天网服务器 │ ───→ │ AI 客服 API │ ───→ │ 企业微信 │
│ (公网 IP) │ │ (127.0.0.1:6060)│ │ (轻简软件) │
│ (公网 IP) │ │ (127.0.0.1:6060)│ │ (轻简软件) │
└─────────────┘ └──────────────┘ └─────────────┘
↑ │
│ ↓
──────────────┐
┌──────────────┐
SQLite
任务数据库 │
└──────────────┘
└─────────────────────┘
──────────────┐
│ SQLite │
│ 任务数据库
└──────────────┘
```
### 核心组件
| 组件 | 地址 | 说明 |
|------|------|------|
| **AI 客服 HTTP API** | http://127.0.0.1:6060 | 接收天网任务 |
| **天网服务器** | 你的公网 IP | 任务调度中心 |
| **轻简软件** | ws://127.0.0.1:9528 | 企业微信连接 |
| **任务数据库** | SQLite | 本地存储 |
| AI 客服 HTTP API | `http://127.0.0.1:6060` | 接收天网任务 |
| 天网服务器 | 公网 IP | 任务调度中心 |
| 轻简软件 | `ws://127.0.0.1:9528` | 企业微信连接 |
| 任务数据库 | SQLite 本地存储 | 任务持久化 |
---
## 🚀 快速部署
## 快速部署
### 步骤 1环境检查
```bash
# 检查 Python 版本
python3 --version # 需要 3.8+
# 检查依赖
pip3 list | grep -E "flask|pydantic|sqlite"
cd /root/ai_customer_service/ai_cs
pip3 install -r requirements.txt
```
### 步骤 2启动 AI 客服 API
### 步骤 2启动服务
```bash
cd /root/ai_customer_service/ai_cs
# 前台运行(测试用)
python3 run_tianwang_simple.py
python3 run.py --api-only
# 后台运行(生产用)
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
# 查看进程
ps aux | grep run_tianwang
# 查看日志
tail -f /tmp/tianwang.log
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
```
### 步骤 3测试 API
### 步骤 3验证
```bash
# 健康检查
curl http://localhost:6060/api/health
# 预期输出:
# {"code":200,"data":{"service":"ai-cs-tianwang-bridge","timestamp":"..."},"message":"OK"}
# 预期: {"code":200,"data":{"service":"ai-cs-tianwang-bridge",...},"message":"OK"}
```
---
## 📝 API 接口说明
## 启动方式
统一入口 `run.py`,通过参数切换模式:
```bash
# 仅 HTTP API天网简化版推荐
python3 run.py --api-only
# 完整版HTTP API + WebSocket + AI Agent
python3 run.py --tianwang
# WebSocket 客服模式(默认)
python3 run.py
# 多进程模式
python3 run.py --multi --workers 4
# 不启用 AI Agent
python3 run.py --no-agent
# 指定 HTTP 端口
python3 run.py --api-only --port 8080
```
---
## 生产环境部署
### 方式 1systemd 服务(推荐)
```bash
cat > /etc/systemd/system/ai-cs-tianwang.service << 'SERVICE'
[Unit]
Description=AI Customer Service with Tianwang
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/ai_customer_service/ai_cs
ExecStart=/usr/bin/python3 run.py --api-only
Restart=always
RestartSec=10
LimitNOFILE=65535
Environment="HTTP_API_PORT=6060"
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ai-cs-tianwang
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload
systemctl enable ai-cs-tianwang
systemctl start ai-cs-tianwang
systemctl status ai-cs-tianwang
journalctl -u ai-cs-tianwang -f
```
### 方式 2Docker 部署
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 6060
CMD ["python3", "run.py", "--api-only"]
```
```bash
docker build -t ai-cs-tianwang .
docker run -d \
--name ai-cs \
-p 6060:6060 \
-v /root/ai_customer_service/ai_cs/db:/app/db \
--restart unless-stopped \
ai-cs-tianwang
```
### 方式 3后台运行简单场景
```bash
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
ps aux | grep "run.py"
tail -f /tmp/tianwang.log
pkill -f "run.py" # 停止
```
---
## 多进程架构
### 架构说明
```
单进程(默认) 多进程(可选)
┌─────────────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Python 进程 │ │进程 1 │ │进程 2 │ │进程 3 │
│ asyncio Loop │ │客户 A,B │ │客户 C,D │ │客户 E,F │
│ 所有客户 + Agent │ └─────────┘ └─────────┘ └─────────┘
└─────────────────┘
```
### 使用方法
```bash
# 多进程模式(默认 CPU 核心数)
python3 run.py --multi
# 指定进程数
python3 run.py --multi --workers 4
# 或使用专用启动器
python3 scripts/multi_process_launcher.py --workers 4
```
### 分片算法
客户按 `acc_id:from_id` 的 MD5 hash 值分配到不同进程,同一客户始终在同一进程。
### 性能对比
| 指标 | 单进程 | 多进程 (4 核) |
|------|--------|-------------|
| 并发客户数 | ~50 | ~200 |
| CPU 使用率 | 25% | 80% |
| 故障影响 | 全局 | 局部 |
---
## API 接口文档
### 1. 接收任务
**接口**: `POST /api/task/receive`
**POST** `/api/task/receive`
**请求示例**:
```bash
curl -X POST http://localhost:6060/api/task/receive \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_20260227_001",
"type": "send_file_after_reply",
"customer": {
"id": "customer_123",
"name": "小明"
},
"customer": {"id": "customer_123", "name": "小明"},
"trigger": {
"type": "specified_customer_reply",
"customer_id": "customer_123",
@@ -93,133 +230,61 @@ curl -X POST http://localhost:6060/api/task/receive \
"keyword": "好的",
"exact_match": false
},
"action": {
"type": "send_message",
"message": "这是您要的文件"
},
"action": {"type": "send_message", "message": "这是您要的文件"},
"priority": "normal",
"timeout_hours": 24,
"created_by": "设计师 lz"
}'
```
**响应示例**:
**响应**:
```json
{
"code": 200,
"message": "任务接收成功",
"data": {
"task_id": "TASK_20260227_001",
"status": "pending"
}
}
{"code": 200, "message": "任务接收成功", "data": {"task_id": "TASK_20260227_001", "status": "pending"}}
```
---
### 2. 查询任务状态
**接口**: `GET /api/task/status/:task_id`
**GET** `/api/task/status/:task_id`
**请求示例**:
```bash
curl http://localhost:6060/api/task/status/TASK_20260227_001
```
**响应示例**:
```json
{
"code": 200,
"data": {
"task_id": "TASK_20260227_001",
"type": "send_file_after_reply",
"status": "pending",
"priority": "normal",
"retry_count": 0,
"created_at": "2026-02-27T18:00:00",
"created_by": "设计师 lz",
"triggered_at": null,
"completed_at": null,
"error_message": null
}
}
```
---
### 3. 取消任务
**接口**: `POST /api/task/cancel`
**POST** `/api/task/cancel`
**请求示例**:
```bash
curl -X POST http://localhost:6060/api/task/cancel \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_20260227_001",
"reason": "客户取消订单"
}'
-d '{"task_id": "TASK_20260227_001", "reason": "客户取消订单"}'
```
**响应示例**:
```json
{
"code": 200,
"message": "任务已取消",
"data": {
"task_id": "TASK_20260227_001",
"status": "cancelled"
}
}
```
### 4. 任务列表
---
**GET** `/api/task/list`
### 4. 查询任务列表
参数: `customer_id`(可选)、`status`(可选)、`page`(默认1)、`page_size`(默认20)
**接口**: `GET /api/task/list`
**请求参数**:
- `customer_id` (可选): 客户 ID
- `status` (可选): 任务状态
- `page` (可选): 页码,默认 1
- `page_size` (可选): 每页数量,默认 20
**请求示例**:
```bash
curl "http://localhost:6060/api/task/list?status=pending&page=1&page_size=10"
```
---
### 5. 健康检查
**接口**: `GET /api/health`
**GET** `/api/health`
**请求示例**:
```bash
curl http://localhost:6060/api/health
```
**响应示例**:
```json
{
"code": 200,
"message": "OK",
"data": {
"timestamp": "2026-02-27T18:00:00",
"service": "ai-cs-tianwang-bridge"
}
}
```
---
## 📊 触发条件类型
## 触发条件详解
### 1. specified_customer_reply推荐
**指定客户回复指定内容**
指定客户回复指定内容时触发。
```json
{
@@ -233,105 +298,62 @@ curl http://localhost:6060/api/health
}
```
**匹配逻辑**:
- ✅ 客户 ID 匹配
- ✅ 客户名称匹配(可选)
- ✅ 消息包含关键词
| 字段 | 必填 | 说明 |
|------|------|------|
| `customer_id` | 是 | 指定客户 ID |
| `customer_name` | 否 | 指定客户名称 |
| `keyword` | 是 | 回复关键词 |
| `exact_match` | 否 | 是否精确匹配(默认 false|
---
**exact_match 说明**:
- `false`: 消息**包含**关键词即触发("好的谢谢" 匹配 "好的"
- `true`: 消息**完全等于**关键词才触发
**匹配逻辑**:
```
客户发送消息 → 检查客户 ID → 检查客户名称(可选) → 检查关键词 → 触发
```
### 2. customer_reply
**任意客户回复指定内容**
任意客户回复指定内容
```json
{
"trigger": {
"type": "customer_reply",
"keyword": "好的"
}
}
{"trigger": {"type": "customer_reply", "keyword": "好的"}}
```
---
### 3. customer_keyword
**任意客户说某关键词**
任意客户说某关键词(支持多个)。
```json
{
"trigger": {
"type": "customer_keyword",
"keywords": ["好的", "可以", "行"]
}
}
{"trigger": {"type": "customer_keyword", "keywords": ["好的", "可以", "行"]}}
```
---
### 4. customer_payment
**客户付款**
客户付款时触发。
```json
{
"trigger": {
"type": "customer_payment",
"keywords": ["已付款", "拍下了", "已下单"]
}
}
{"trigger": {"type": "customer_payment", "keywords": ["已付款", "拍下了"]}}
```
---
### 5. time_reach
**到达指定时间**
到达指定时间触发。
```json
{
"trigger": {
"type": "time_reach",
"time": "2026-02-27 09:00:00"
}
}
{"trigger": {"type": "time_reach", "time": "2026-02-28 09:00:00"}}
```
---
## 🔧 配置说明
## 数据库
### 环境变量文件
### 天网任务数据库
位置:`/root/ai_customer_service/ai_cs/.env.tianwang`
路径: `db/task_db/tasks.db`
```bash
# AI 客服配置
AI_CS_HOST=127.0.0.1
AI_CS_PORT=6060
# AI 客服 HTTP API 地址(本地)
AI_CS_API_URL=http://127.0.0.1:6060
# 天网回调地址
TIANWANG_CALLBACK_URL=http://127.0.0.1:6060/api/task/callback
# API 接口
TASK_RECEIVE=/api/task/receive
TASK_STATUS=/api/task/status
TASK_CANCEL=/api/task/cancel
TASK_LIST=/api/task/list
HEALTH=/api/health
```
---
### 数据库配置
位置:`/root/ai_customer_service/ai_cs/db/task_db/tasks.db`
**数据库结构**:
```sql
CREATE TABLE tasks (
task_id TEXT PRIMARY KEY,
@@ -360,281 +382,144 @@ CREATE TABLE tasks (
);
```
**任务状态流转**: `pending → waiting → running → completed / failed`
### 图片任务数据库
路径: `db/image_tasks.db`(详见 **项目功能汇总.md - 图片任务数据库**
---
## 📖 使用场景
## 配置说明
### 场景 1客户说"好的"后发送文件
### 环境变量
**天网下发任务**:
```json
{
"task_id": "TASK_001",
"type": "send_file_after_reply",
"customer": {
"id": "customer_123",
"name": "小明"
},
"trigger": {
"type": "specified_customer_reply",
"customer_id": "customer_123",
"customer_name": "小明",
"keyword": "好的"
},
"action": {
"type": "send_message",
"message": "这是您要的文件"
},
"priority": "normal",
"timeout_hours": 24,
"created_by": "设计师 lz"
}
文件: `.env.tianwang`
```bash
AI_CS_HOST=127.0.0.1
AI_CS_PORT=6060
AI_CS_API_URL=http://127.0.0.1:6060
TIANWANG_CALLBACK_URL=http://127.0.0.1:6060/api/task/callback
```
**流程**:
1. 天网下发任务 → AI 客服 API
2. 客户在群里说"好的"
3. AI 客服检测到匹配任务
4. 自动发送消息给客户
5. 任务标记为 completed
6. 回调通知天网
### 天网回调配置
---
`core/task_scheduler.py` 中修改回调 URL:
### 场景 2客户付款后发送教程
```python
await client.post('http://tianwang-server/api/task/callback', json={...})
```
**天网下发任务**:
```json
{
"task_id": "TASK_002",
"type": "send_tutorial_after_payment",
"customer": {
"id": "customer_456",
"name": "小红"
},
"trigger": {
"type": "customer_payment"
},
"action": {
"type": "send_message",
"message": "感谢购买!这是使用教程:..."
},
"created_by": "设计师 lz"
}
### 端口说明
| 端口 | 用途 |
|------|------|
| 6060 | HTTP API 服务器 |
| 9528 | 轻简软件 WebSocket外部|
**防火墙**:
```bash
firewall-cmd --add-port=6060/tcp --permanent && firewall-cmd --reload
```
---
### 场景 3定时发送问候
**天网下发任务**:
```json
{
"task_id": "TASK_003",
"type": "send_greeting",
"customer": {
"id": "customer_789"
},
"trigger": {
"type": "time_reach",
"time": "2026-02-28 09:00:00"
},
"action": {
"type": "send_message",
"message": "早上好!有什么可以帮您?"
},
"created_by": "设计师 lz"
}
```
---
## 🔍 监控与日志
## 监控与日志
### 查看进程状态
```bash
# 查看进程
ps aux | grep run_tianwang
# 查看端口
ps aux | grep "run.py"
netstat -tlnp | grep 6060
systemctl status ai-cs-tianwang # systemd 方式
```
### 查看日志
```bash
# 实时日志
tail -f /tmp/tianwang.log
# 最近 100 行
tail -100 /tmp/tianwang.log
# 搜索关键词
grep "任务" /tmp/tianwang.log
tail -f /tmp/tianwang.log # 文件方式
journalctl -u ai-cs-tianwang -f # systemd 方式
grep "任务" /tmp/tianwang.log # 搜索任务日志
grep "派单" /tmp/tianwang.log # 搜索派单日志
grep "转接人工" /tmp/tianwang.log # 搜索转接日志
```
### 查看数据库
```bash
# 进入 SQLite
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db
# 查看所有任务
SELECT task_id, type, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 10;
# 查看待触发任务
SELECT * FROM tasks WHERE status='pending';
# 查看失败任务
SELECT task_id, error_message FROM tasks WHERE status='failed';
# 查看统计数据
SELECT status, COUNT(*) as count FROM tasks GROUP BY status;
# 退出
.exit
```
---
## ⚠️ 故障排查
## 故障排查
### 问题 1API 无法访问
### API 无法访问
**症状**: `curl http://localhost:6060/api/health` 无响应
**解决**:
```bash
# 1. 检查进程
ps aux | grep run_tianwang
# 2. 检查端口
netstat -tlnp | grep 6060
# 3. 重启服务
pkill -f run_tianwang
cd /root/ai_customer_service/ai_cs
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
# 4. 查看日志
tail -f /tmp/tianwang.log
ps aux | grep "run.py" # 检查进程
netstat -tlnp | grep 6060 # 检查端口
pkill -f "run.py" # 停止
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
tail -f /tmp/tianwang.log # 查看日志
```
---
### 任务接收失败500 错误)
### 问题 2任务接收失败
**症状**: POST /api/task/receive 返回 500 错误
**解决**:
```bash
# 1. 查看日志
tail -f /tmp/tianwang.log | grep "ERROR"
# 2. 检查数据库
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db ".schema tasks"
# 3. 测试数据库
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db "SELECT 1;"
# 4. 如果数据库损坏,删除重建
rm /root/ai_customer_service/ai_cs/db/task_db/tasks.db
# 重启服务后会自动创建
sqlite3 db/task_db/tasks.db ".schema tasks" # 检查数据库
# 如果数据库损坏rm db/task_db/tasks.db 然后重启(自动重建)
```
---
### 任务未触发
### 问题 3任务未触发
**症状**: 任务一直是 pending 状态
**解决**:
```bash
# 1. 检查任务状态
curl http://localhost:6060/api/task/status/TASK_ID
# 2. 查看触发日志
grep "任务触发" /tmp/tianwang.log
# 3. 检查触发条件
curl http://localhost:6060/api/task/status/TASK_ID # 检查状态
grep "任务触发" /tmp/tianwang.log # 查看触发日志
# 确认客户消息包含触发关键词
# 4. 手动触发测试
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db \
"UPDATE tasks SET status='pending' WHERE task_id='TASK_ID';"
```
---
### 内存占用过高
### 问题 4内存占用过高
**症状**: 进程内存持续增长
**解决**:
```bash
# 1. 查看内存使用
ps aux | grep run_tianwang | awk '{print $6/1024 " MB"}'
# 2. 定期重启(建议每天)
# 建议每天定时重启
crontab -e
# 添加0 3 * * * pkill -f run_tianwang && sleep 2 && nohup python3 /root/ai_customer_service/ai_cs/run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
# 添加: 0 3 * * * pkill -f "run.py" && sleep 2 && nohup python3 /root/ai_customer_service/ai_cs/run.py --api-only > /tmp/tianwang.log 2>&1 &
```
### Worker 进程退出(多进程模式)
```bash
journalctl -u ai-cs-multi -f | grep "Worker.*退出"
systemctl restart ai-cs-multi
```
---
## 📞 技术支持
### 文件位置
## 文件位置速查
| 文件 | 路径 |
|------|------|
| **启动脚本** | `/root/ai_customer_service/ai_cs/run_tianwang_simple.py` |
| **HTTP API** | `/root/ai_customer_service/ai_cs/api/http_server.py` |
| **任务调度** | `/root/ai_customer_service/ai_cs/core/task_scheduler.py` |
| **数据模型** | `/root/ai_customer_service/ai_cs/db/task_db/task_model.py` |
| **配置文件** | `/root/ai_customer_service/ai_cs/.env.tianwang` |
| **日志文件** | `/tmp/tianwang.log` |
| **数据库** | `/root/ai_customer_service/ai_cs/db/task_db/tasks.db` |
### 文档位置
| 文档 | 路径 |
|------|------|
| **部署文档** | `/root/ai_customer_service/ai_cs/部署文档.md` |
| **功能文档** | `/root/ai_customer_service/ai_cs/TIANWANG_INTEGRATION.md` |
| **使用示例** | `/root/ai_customer_service/ai_cs/SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md` |
| 启动脚本 | `run.py`(通过 `--api-only` / `--tianwang` 切换模式)|
| HTTP API | `api/http_server.py` |
| 任务调度 | `core/task_scheduler.py` |
| 数据模型 | `db/task_db/task_model.py` |
| 配置文件 | `.env.tianwang` |
| 日志文件 | `/tmp/tianwang.log` |
| 任务数据库 | `db/task_db/tasks.db` |
---
## 📧 发送文档
如需发送此文档,可以:
### 方式 1复制内容
```bash
# 复制文档内容
cat /root/ai_customer_service/ai_cs/部署文档.md
```
### 方式 2生成 PDF
```bash
# 安装 pandoc
apt-get install pandoc
# 转换为 PDF
pandoc /root/ai_customer_service/ai_cs/部署文档.md -o 部署文档.pdf
```
### 方式 3发送邮件
```bash
# 使用 mail 命令
mail -s "AI 客服系统部署文档" recipient@example.com < /root/ai_customer_service/ai_cs/部署文档.md
```
---
## 🎯 快速参考卡片
## 快速参考
```
┌─────────────────────────────────────────────┐
@@ -648,15 +533,8 @@ mail -s "AI 客服系统部署文档" recipient@example.com < /root/ai_customer_
│ GET /api/task/list - 任务列表 │
│ GET /api/health - 健康检查 │
│ │
│ 启动python3 run_tianwang_simple.py
│ 启动python3 run.py --api-only
│ 日志tail -f /tmp/tianwang.log │
│ 数据库sqlite3 db/task_db/tasks.db │
└─────────────────────────────────────────────┘
```
---
**文档版本**: v1.0
**更新日期**: 2026-02-27
**维护者**: AI 客服系统团队

View File

@@ -1,263 +1,447 @@
# AI 客服系统 - 完整功能汇总
**版本**: v1.0
**更新日期**: 2026-02-27
**服务器**: 1.12.50.92
**版本**: v1.0 | **更新日期**: 2026-02-28 | **服务器**: 1.12.50.92
---
## 📋 目录
## 目录
1. [核心功能](#核心功能)
2. [图片处理](#图片处理)
3. [任务管理](#任务管理)
4. [派单系统](#派单系统)
5. [价格策略](#价格策略)
6. [风险控制](#风险控制)
7. [技术架构](#技术架构)
1. [天网协作系统](#天网协作系统)
2. [三种工作流](#三种工作流)
3. [文字检测与加价](#文字检测与加价)
4. [风险评估与接单判断](#风险评估与接单判断)
5. [作图失败转接人工](#作图失败转接人工)
6. [图片任务数据库](#图片任务数据库)
7. [图绘派单系统](#图绘派单系统)
8. [价格策略总览](#价格策略总览)
9. [技术架构](#技术架构)
---
## 核心功能
## 天网协作系统
### 1. 天网协作系统
**说明**: 接收天网下发的任务,支持指定客户回复触发。
**说明**: 接收天网下发的任务,支持指定客户回复触发
**API 地址**: `http://127.0.0.1:6060`
**API 接口**: `http://127.0.0.1:6060`
**接口列表**:
**功能**:
- ✅ 任务接收 (`POST /api/task/receive`)
- ✅ 任务查询 (`GET /api/task/status/:id`)
- ✅ 任务取消 (`POST /api/task/cancel`)
- ✅ 任务列表 (`GET /api/task/list`)
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/task/receive` | POST | 接收任务 |
| `/api/task/status/:id` | GET | 查询任务状态 |
| `/api/task/cancel` | POST | 取消任务 |
| `/api/task/list` | GET | 任务列表 |
| `/api/health` | GET | 健康检查 |
**触发类型**:
- `specified_customer_reply` - 指定客户回复指定内容
- `customer_keyword` - 任意客户说某关键词
- `customer_payment` - 客户付款
- `time_reach` - 到达指定时间
**文档**: `TIANWANG_INTEGRATION.md`
| 类型 | 说明 |
|------|------|
| `specified_customer_reply` | 指定客户回复指定内容(推荐) |
| `customer_reply` | 任意客户回复指定内容 |
| `customer_keyword` | 任意客户说某关键词 |
| `customer_payment` | 客户付款 |
| `time_reach` | 到达指定时间 |
> 详细的 API 接口文档、请求示例、数据库结构等见 **部署文档.md**。
---
### 2. 三种工作流
## 三种工作流
根据客户说的话自动判断执行不同工作流
根据客户说的话自动判断执行不同工作流程。
#### 工作流 1: 查找图片
**触发词**: "找一下"、"找图"、"找原图"
### 工作流 1查找图片
**触发词**: "找一下"、"找图"、"找原图"、"帮我找"、"能找到吗"、"有吗"、"有没有"
**流程**:
```
客户:找一下这个图 [图片]
AI 处理 → 上传到图绘
回复:"找到了http://tuhui.cloud/works/123"
AI 检测到"找一下"关键词 → 执行查找图片工作流
1. 创建任务operation=find
2. 上传图片到图绘平台
3. 更新任务状态为 completed
AI: 找到了图片在这里http://tuhui.cloud/works/123
```
#### 工作流 2: 处理图片
**触发词**: "做一下"、"处理一下"、"安排"
### 工作流 2处理图片
**触发词**: "做一下"、"处理一下"、"安排"、"开始做"、"弄一下"、"修一下"、"P一下"、"P图"
**流程**:
```
客户:做一下 [图片]
AI: "稍等,我看看...好的,可以做"
启动图片处理流程
AI 检测到"做一下"关键词 → 执行处理图片工作流
1. 创建任务operation=enhance
2. 回复"稍等,我看看...好的,可以做,马上处理"
3. 启动图片处理流程
AI: 做好了,请查看 [结果图]
```
#### 工作流 3: 转人工派单
**触发词**: "做不了"、"处理不了"
### 工作流 3转人工派单
**触发词**: "做不了"、"处理不了"、"弄不了"、"无法处理"、"做不到"、"搞不定"
**流程**:
```
AI: 做不了
查询在线设计师
派单给设计师
回复:"已安排设计师处理"
AI 判断无法处理 / 客户说"做不了"
执行转人工派单工作流
1. 创建任务operation=manual
2. 查询在线设计师
3. 有人在线 → 派单;无人 → 通知稍后联系
AI: 好的,已帮您安排设计师处理,请稍候
```
**文档**: `三种工作流功能说明.md`
### 技术实现
| 组件 | 文件 | 说明 |
|------|------|------|
| 工作流路由器 | `core/workflow_router.py` | 关键词检测与匹配 |
| 工作流执行器 | `core/workflow.py` | 三种工作流的具体实现 |
| 消息处理器 | `core/pydantic_ai_agent.py` | `_handle_image_workflow()` 方法 |
**注意事项**:
- 关键词匹配支持多种说法,自动识别
- 置信度 >0.9 才执行对应工作流
- 无人在线时通知客户稍后联系,企业微信预警
- 所有工作流都保存到数据库
---
## 图片处理
## 文字检测与加价
### 1. 文字检测与加价
AI 客服自动分析图片中的文字数量,根据文字数量和分层需求自动加价
**功能**: AI 自动识别图片文字数量,根据文字数量加价
**价格规则**:
### 文字数量加价
| 文字数量 | 加价 |
|----------|------|
| none | +0 元 |
| 少量 (1-10 字) | +5 元 |
| 中量 (11-50 字) | +15 元 |
| 大量 (51-200 字) | +30 元 |
| 极多 (200 字以上) | +50 元 |
**分层加价**:
- 有文字 + 分层:+50 元起
- 无文字 + 分层:+30 元
### 文字分层需求加价
**特殊价格**: 文字分层 + 大量文字 → 60-80 元
| 分层需求 | 加价 |
|----------|------|
| no | +0 元 |
| yes有文字| +50 元起 |
| yes无文字| +30 元 |
**文档**: `文字加价功能说明.md`
### 特殊价格
---
**条件**: 文字数量=大量/极多 且 分层需求=yes → **60-80 元**
### 2. 风险评估与接单判断
### 使用场景
**风险等级**:
- **敏感内容** (一票否决): 色情/暴力/涉政 → 直接拒绝
- **none**: 印花/图案/logo → 直接接单
- **low**: 有人脸但清晰 → 接单,说明风险
- **high**: 严重模糊/老照片 → 谨慎接单
**场景 1**: 少量文字,不分层
- 复杂度 simple + 少量文字 → 15 + 5 = **20 元**
- AI: "这张图比较简单不过有少量文字需要处理20 元。"
**可做判断**:
- `yes`: 效果有把握,接单
- `partial`: 有限制,谨慎接单
- `no`: 无法处理,不接单
**场景 2**: 大量文字,需要分层
- 复杂度 complex + 大量文字 + 分层 → 调整到 **80 元**
- AI: "这张图文字比较多,有 100 多字需要分层文件80 元。"
**文档**: `风险评估功能说明.md`
### 价格计算流程
---
### 3. 作图失败转接人工
**触发条件**:
- API 调用失败
- 图片处理超时
- 客户不满意
**流程**:
```
作图失败
通知客户
转接人工客服
企业微信预警
客户发送图片 → 判断基础复杂度 → 检测文字数量 → 询问分层需求
→ 计算总价(基础+文字+分层)→ 特殊价格处理60-80 元)→ 报价
```
**文档**: `作图失败转接人工说明.md`
### 配置位置
修改价格规则: `image/image_analyzer.py`(查找文字加价相关代码)
**注意事项**:
- 文字数量通过视觉 AI 自动识别
- 分层需求需从对话中识别
- 最终价格必须是 5 的倍数
---
## 任务管理
## 风险评估与接单判断
### 1. 图片任务数据库
AI 客服自动分析图片风险,判断是否可以接单。
**功能**: 任务持久化,支持客户后续增加需求
### 敏感内容检测(一票否决)
**数据库表**:
- `image_tasks` - 图片任务表
- `task_requirement_changes` - 需求变更表
**敏感内容 = yes → 直接拒绝,不接单**
检测内容: 色情/黄色/擦边/裸露、性暗示、涉政/政治敏感、暴力/血腥、违禁品
**话术**: "这类不做哦" / "不好意思,这个接不了"
**禁止说**: "发图来看看"、过多解释
### 风险等级
| 风险 | 是否接单 | 说明 |
|------|----------|------|
| **none** | ✅ 接单 | 印花/图案/logo/风景/产品,效果稳定 |
| **low** | ✅ 接单 | 有人脸但清晰,需说明风险(相似度 70-90%|
| **high** | ⚠️ 谨慎 | 严重模糊/老照片人像/需打印,需说明限制 |
### 可做判断
| 可做 | 是否接单 | 说明 |
|------|----------|------|
| **yes** | ✅ 接单 | 效果有把握 |
| **partial** | ⚠️ 可接 | 能处理但有限制,需说明风险 |
| **no** | ❌ 不接 | 无法处理(纯黑/纯白/完全损坏/敏感内容)|
### 分析流程
```
客户发送图片 → 敏感内容检测yes→拒绝→ 风险评估none/low/high
→ 可做判断yes/partial/no→ 决策(接单/谨慎/拒绝)→ 回复客户
```
### 话术模板
**高风险提示**:
- "这张比较模糊,修复后清晰了但人脸可能跟原来有差异"
- "老照片修复后人脸可能有轻微变化"
- "建议先看效果确认再打印"
**正常接单**:
- "这个没问题XX 元"
- "可以处理XX 元,满意再付"
### 配置位置
- 风险判断规则: `image/image_analyzer.py`(查找"风险评估""敏感内容检测"
- 拒绝话术: `core/pydantic_ai_agent.py`(查找"拒绝"
---
## 作图失败转接人工
当 AI 作图失败或效果不佳时,系统自动转接人工客服。
### 触发场景
| 场景 | 触发条件 | 话术 |
|------|----------|------|
| AI 作图失败 | API 报错/超时/质量不达标 | "处理遇到点问题,我帮您转接人工" |
| 客户不满意 | 说"效果不好"/"不满意"/要求重做 | "好的,我帮您转接人工客服处理" |
| 特殊要求 | AI 无法处理的复杂需求 | "这个需求比较特殊,帮您转接人工" |
### 转接流程
```
作图失败/客户不满意 → 通知客户 → 转接人工客服 → 企业微信预警
```
### 技术实现
- 失败检测: `core/pydantic_ai_agent.py` 中的 `process_image_gemini` 函数
- 转接工具: `transfer_to_human` tool标记 `need_transfer=True`
**注意事项**:
- 作图失败必须转人工,不自动重试超过 2 次
- 转接前告知客户原因
- 记录转接原因便于后续优化
---
## 图片任务数据库
图片任务保存到 SQLite 数据库,支持持久化和需求变更。
### 数据库表
**image_tasks图片任务表**:
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | TEXT | 任务 ID主键|
| customer_id | TEXT | 客户 ID |
| original_image | TEXT | 原图 URL |
| operation | TEXT | 操作类型enhance/remove_bg/vectorize|
| requirements | TEXT | 需求 JSON |
| customer_notes | TEXT | 客户备注/需求细节 |
| status | TEXT | 状态 |
| result_image | TEXT | 结果图 URL |
| error_message | TEXT | 错误信息 |
| retry_count | INTEGER | 重试次数 |
| acc_id / acc_type | TEXT | 店铺 ID / 平台类型 |
| created_at / paid_at / completed_at | TEXT | 时间戳 |
**task_requirement_changes需求变更表**:
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | TEXT | 任务 ID外键|
| change_type | TEXT | 变更类型add_note/modify_operation/add_requirement|
| old_value / new_value | TEXT | 变更前后值 |
| changed_at | TEXT | 变更时间 |
| changed_by | TEXT | 变更者customer/staff|
### 任务状态流转
```
pending待付款→ paid已付款→ processing处理中→ awaiting_confirm待确认→ completed已完成
↘ failed失败
```
### API 接口
**客户可以增加需求**:
```python
await workflow.add_customer_requirement(
task_id="TASK_001",
customer_id="customer_123",
requirement="需要去掉背景"
# 创建任务
workflow.create_image_task(customer_id, original_image, operation)
# 添加需求
await workflow.add_customer_requirement(task_id, customer_id, requirement)
# 修改操作类型
await workflow.modify_operation(task_id, customer_id, new_operation)
# 查询任务
task = workflow.get_task(task_id)
tasks = workflow.get_customer_tasks(customer_id)
# 查询需求变更历史
history = workflow.get_task_requirement_history(task_id)
```
### 数据库操作
```bash
sqlite3 /root/ai_customer_service/ai_cs/db/image_tasks.db
# 查询所有任务
SELECT task_id, customer_id, status, created_at FROM image_tasks ORDER BY created_at DESC LIMIT 10;
# 查询待处理任务
SELECT * FROM image_tasks WHERE status='pending';
# 查询需求变更
SELECT task_id, change_type, old_value, new_value, changed_at FROM task_requirement_changes WHERE task_id='TASK_001';
```
**注意事项**:
- 所有任务自动保存,重启不丢失
- 付款前可修改操作类型,付款后不允许
- 所有变更都有历史记录
---
## 图绘派单系统
AI 客服系统接入图绘派单系统 API实现自动派单给在线设计师。
### API 信息
| 项目 | 值 |
|------|------|
| API 地址 | `http://1.12.50.92:8005` |
| API Key | `tuhui_dispatch_key_2026` |
| 认证方式 | Header: `X-API-Key` |
### 核心接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/dispatch/queue` | GET | 获取派单队列 |
| `/online/designers` | GET | 获取在线设计师 |
| `/tasks` | POST | 创建任务 |
| `/tasks/{id}/assign` | POST | 分配任务 |
| `/tasks/{id}` | GET | 查询任务状态 |
| `/tasks/{id}/complete` | POST | 完成任务 |
### 转人工派单流程
```
AI 判断做不了
1. 查询在线设计师 → GET /online/designers → ["橘子", "婷婷"]
2. 创建派单任务 → POST /tasks → {"task_id": "ea853bd9"}
3. 分配给设计师 → POST /tasks/ea853bd9/assign → {"designer_name": "橘子"}
4. 企业微信通知设计师
5. 回复客户:"好的,已帮您安排设计师处理,请稍候"
```
### 代码调用示例
```python
from services.service_tuhui_dispatch import get_tuhui_dispatch_client
client = get_tuhui_dispatch_client()
# 查询在线设计师
designers = await client.get_online_designers() # ["橘子", "婷婷"]
# 创建任务
task_id = await client.create_task(
task_name="图片处理-1234",
description="客户需要做高清修复",
task_type="image_process",
priority=2
)
# 分配任务
await client.assign_task(task_id, designer_name="橘子", notes="AI 客服自动派单")
# 完成任务
await client.complete_task(task_id, notes="客户已确认")
```
**任务状态流转**:
### 设计师在线状态 API
```
pending → paid → processing → awaiting_confirm → completed
GET http://huichang.online:8001/online # 查询在线设计师
POST http://huichang.online:8001/update-status # 更新设计师状态
```
**文档**: `图片任务数据库功能说明.md`
### 相关代码位置
| 组件 | 文件 |
|------|------|
| 派单客户端 | `services/service_tuhui_dispatch.py``TuhuiDispatchClient` 类)|
| 工作流集成 | `core/workflow.py``transfer_to_designer_workflow()` 方法)|
---
## 派单系统
### 1. 图绘派单系统
**API 地址**: `http://1.12.50.92:8005`
**API Key**: `tuhui_dispatch_key_2026`
**核心接口**:
- `GET /dispatch/queue` - 获取派单队列
- `GET /online/designers` - 查询在线设计师
- `POST /tasks` - 创建任务
- `POST /tasks/{id}/assign` - 分配任务
- `POST /tasks/{id}/complete` - 完成任务
**派单流程**:
```
1. 查询在线设计师 → ["橘子", "婷婷"]
2. 创建任务 → {"task_id": "ea853bd9"}
3. 分配给设计师 → designer: "橘子"
4. 企业微信通知
5. 回复客户
```
**文档**: `图绘派单系统集成说明.md`
---
## 价格策略
## 价格策略总览
### 基础价格
| 复杂度 | 价格 |
|--------|------|
| simple | 10-15 元 |
| normal | 15-20 元 |
| complex | 20-25 元 |
| hard | 25-30 元 |
| 复杂度 | 价格区间 | 说明 |
|--------|----------|------|
| simple | 10-15 元 | 画面简单干净 |
| normal | 15-20 元 | 一般复杂度 |
| complex | 20-25 元 | 细节偏多 |
| hard | 25-30 元 | 非常复杂 |
### 加价规则
**文字加价**:
- 少量:+5 元
- 中量:+15 元
- 大量:+30 元
- 极多:+50 元
| 项目 | 条件 | 加价 |
|------|------|------|
| 文字少量 | 1-10 字 | +5 元 |
| 文字中量 | 11-50 字 | +15 元 |
| 文字大量 | 51-200 字 | +30 元 |
| 文字极多 | 200+ 字 | +50 元 |
| 分层(有文字)| 需要 PSD 分层 | +50 元起 |
| 分层(无文字)| 仅需分层 | +30 元 |
**分层加价**:
- 有文字:+50 元起
- 无文字:+30 元
### 高价值订单
**高价值订单**: 文字分层 + 大量文字 → 60-80 元
---
## 风险控制
### 敏感内容检测
**一票否决**:
- ❌ 色情/黄色/擦边
- ❌ 涉政/政治敏感
- ❌ 暴力/血腥
- ❌ 违禁品
**话术**:
- "这类不做哦"
- "不好意思,这个接不了"
### 高风险图片
**谨慎接单**:
- ⚠️ 严重模糊的人脸
- ⚠️ 老照片人像
- ⚠️ 需要打印
**话术**:
- "这张比较模糊,修复后人脸可能有差异"
- "建议先看效果确认再决定"
**文字分层 + 大量文字** → 特殊价格 **60-80 元**(封顶)
---
@@ -267,107 +451,26 @@ pending → paid → processing → awaiting_confirm → completed
| 组件 | 文件 | 说明 |
|------|------|------|
| **天网协作** | `api/http_server.py` | HTTP API 服务器 |
| **工作流程** | `core/workflow.py` | 工作流执行器 |
| **AI Agent** | `core/pydantic_ai_agent.py` | AI 对话引擎 |
| **图片分析** | `image/image_analyzer.py` | 图片复杂度识别 |
| **派单客户端** | `services/service_tuhui_dispatch.py` | 图绘派单 API |
| **任务数据库** | `db/image_tasks_db.py` | 任务持久化 |
| 天网协作 | `api/http_server.py` | HTTP API 服务器 |
| 工作流程 | `core/workflow.py` | 工作流执行器 |
| AI Agent | `core/pydantic_ai_agent.py` | AI 对话引擎 |
| 图片分析 | `image/image_analyzer.py` | 图片复杂度识别 |
| 派单客户端 | `services/service_tuhui_dispatch.py` | 图绘派单 API |
| 任务数据库 | `db/image_tasks_db.py` | 任务持久化 |
### 数据库
| 数据库 | 文件 | 说明 |
| 数据库 | 位置 | 说明 |
|--------|------|------|
| **任务数据库** | `db/image_tasks.db` | 图片任务 |
| **客户档案** | `customer_db/customer.db` | 客户画像 |
| **聊天记录** | `chat_log_db/chat_log.db` | 聊天历史 |
| 任务数据库 | `db/image_tasks.db` | 图片任务 |
| 客户档案 | `db/customer.db` | 客户画像 |
| 聊天记录 | `chat_log_db/chat_log.db` | 聊天历史 |
| 天网任务 | `db/task_db/tasks.db` | 天网任务调度 |
### API 端口
| 服务 | 端口 | 说明 |
|------|------|------|
| **AI 客服 API** | 6060 | 天网任务接收 |
| **派单系统** | 8005 | 设计师派单 |
| **图绘平台** | 8002 | 图片上传 |
---
## 部署说明
### 启动方式
```bash
cd /root/ai_customer_service/ai_cs
# 方式 1: 天网协作版
python3 run_tianwang_simple.py
# 方式 2: 完整版
python3 run_with_tianwang.py
# 方式 3: AI 客服
python3 run.py
```
### 后台运行
```bash
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
```
### 查看日志
```bash
tail -f /tmp/tianwang.log
```
---
## 文档清单
| 文档 | 说明 |
|------|------|
| `README.md` | 项目说明 |
| `DEPLOYMENT.md` | 部署文档 |
| `TIANWANG_INTEGRATION.md` | 天网协作 |
| `三种工作流功能说明.md` | 工作流 |
| `文字加价功能说明.md` | 价格策略 |
| `风险评估功能说明.md` | 风险控制 |
| `图片任务数据库功能说明.md` | 任务管理 |
| `图绘派单系统集成说明.md` | 派单系统 |
| `作图失败转接人工说明.md` | 失败处理 |
---
## 快速参考
### API 调用示例
**接收天网任务**:
```bash
curl -X POST http://localhost:6060/api/task/receive \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_001",
"customer": {"id": "customer_123"},
"trigger": {"type": "customer_reply", "keyword": "好的"},
"action": {"type": "send_message", "message": "您好"}
}'
```
**查询在线设计师**:
```bash
curl -X GET "http://1.12.50.92:8005/online/designers" \
-H "X-API-Key: tuhui_dispatch_key_2026"
```
**派单队列**:
```bash
curl -X GET "http://1.12.50.92:8005/dispatch/queue" \
-H "X-API-Key: tuhui_dispatch_key_2026"
```
---
**完整功能已部署,所有系统运行正常!** 🎉
| AI 客服 API | 6060 | 天网任务接收 |
| 派单系统 | 8005 | 设计师派单 |
| 图绘平台 | 8002 | 图片上传 |

View File

@@ -1,270 +0,0 @@
# 图片风险评估与接单判断功能
## 📋 功能说明
AI 客服会自动分析图片的風險,判断是否可以接单。
---
## ⚠️ 风险等级
### 1. 敏感内容检测(一票否决)
**敏感内容 = yes****直接拒绝,不接单**
检测内容:
- ❌ 色情/黄色/擦边/裸露
- ❌ 性暗示/大尺度
- ❌ 涉政/政治敏感
- ❌ 暴力/血腥
- ❌ 违禁品
**话术**
- "这类不做哦"
- "不好意思,这个接不了"
- "这类图片不处理的"
---
### 2. 风险评估
| 风险等级 | 是否接单 | 说明 |
|----------|----------|------|
| **none** | ✅ 接单 | 印花/图案/logo/风景/产品,效果稳定 |
| **low** | ✅ 接单 | 有人脸但清晰,需说明风险(人脸相似度 70-90% |
| **high** | ⚠️ 谨慎接单 | 严重模糊/老照片人像/需打印,需说明限制 |
---
### 3. 可做判断
| 可做 | 是否接单 | 说明 |
|------|----------|------|
| **yes** | ✅ 接单 | 效果有把握 |
| **partial** | ⚠️ 可接单 | 能处理但有限制,需说明风险 |
| **no** | ❌ 不接单 | 无法处理 |
---
## 🎯 使用场景
### 场景 1敏感内容 - 拒绝
**客户**[发送擦边图片]
**AI 分析**
- 敏感内容yes
- 可做no
**AI 回复**
```
这类不做哦。
```
**不说的话**
- ❌ "发图来看看"
- ❌ "这个处理不了"
- ❌ 过多解释
---
### 场景 2高风险 - 谨慎接单
**客户**[发送严重模糊的老照片]
**AI 分析**
- 风险high
- 可做partial
**AI 回复**
```
这张照片比较模糊修复后清晰了但人脸可能跟原来有差异建议先看效果确认再决定20 元。
```
---
### 场景 3低风险 - 正常接单
**客户**[发送清晰的人像照片]
**AI 分析**
- 风险low
- 可做yes
**AI 回复**
```
可以处理,修复后人脸相似度大概 70-90%20 元,满意再付。
```
---
### 场景 4无风险 - 直接接单
**客户**[发送印花图案]
**AI 分析**
- 风险none
- 可做yes
**AI 回复**
```
这个没问题15 元,拍下就处理。
```
---
## 🔧 技术实现
### 分析流程
```
客户发送图片
AI 视觉分析
┌───────────────────────┐
│ 1. 敏感内容检测 │
│ yes → 拒绝不接单 │
│ no → 继续判断 │
└───────────┬───────────┘
┌───────────────────────┐
│ 2. 风险评估 │
│ none/low/high │
└───────────┬───────────┘
┌───────────────────────┐
│ 3. 可做判断 │
│ yes/partial/no │
└───────────┬───────────┘
┌───────────────────────┐
│ 4. 决策 │
│ 接单/谨慎接单/拒绝 │
└───────────┬───────────┘
回复客户
```
---
## 📝 判断规则
### 敏感内容(一票否决)
**检测到以下任一内容 → 敏感内容=yes → 不接单**
- 色情/黄色/擦边/裸露
- 性暗示/大尺度
- 涉政/政治敏感
- 暴力/血腥
- 违禁品
### 风险评估
**none无风险**
- ✅ 印花/图案/logo
- ✅ 风景/产品
- ✅ AI 处理效果稳定
**low低风险**
- ✅ 有人脸但清晰
- ✅ AI 修复后人脸相似度 70-90%
- ⚠️ 需说明风险
**high高风险**
- ⚠️ 严重模糊的人脸照片
- ⚠️ 老照片人像
- ⚠️ 需要打印
- ⚠️ 客户问能否找回原图
- ⚠️ 谨慎接单,说明限制
### 可做判断
**yes可接单**
- ✅ 效果有把握
- ✅ 可直接处理
**partial谨慎接单**
- ⚠️ 能处理但有明显限制
- ⚠️ 人脸变形风险
- ⚠️ 分辨率极低
- ⚠️ 严重损坏
- ⚠️ 需说明风险
**no不接单**
- ❌ 纯黑/纯白
- ❌ 完全损坏
- ❌ 找原始 RAW 文件
- ❌ 敏感内容
- ❌ 违法内容
---
## 📊 话术模板
### 拒绝话术(敏感内容)
```
- 这类不做哦
- 不好意思,这个接不了
- 这类图片不处理的
- 这个做不了哈
```
**不要说**
- ❌ "发图来看看"
- ❌ 过多解释
- ❌ "处理不了"
---
### 风险提示话术(高风险)
```
- 这张比较模糊,修复后清晰了但人脸可能跟原来有差异
- 老照片修复后人脸可能有轻微变化
- 建议先看效果确认再打印
- 这张模糊程度较高,修复后清晰了但人脸可能跟原来有差异
```
---
### 正常接单话术
```
- 这个没问题XX 元
- 可以处理XX 元,拍下就处理
- 好的XX 元,满意再付
```
---
## ⚠️ 注意事项
1. **敏感内容优先判断**:先检测敏感内容,再判断其他
2. **敏感内容=yes → 可做=no**:必须拒绝
3. **高风险谨慎接单**:说明限制和风险
4. **拒绝话术简洁**:不要过多解释
5. **风险告知**low/high 风险都要告知客户
---
## 🔍 配置说明
### 修改风险判断规则
文件:`/root/ai_customer_service/ai_cs/image/image_analyzer.py`
查找`【风险评估】``【敏感内容检测】`部分修改规则。
### 修改拒绝话术
文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py`
查找`【拒绝】`部分修改拒绝话术。
---
**文档版本**: v1.0
**更新日期**: 2026-02-27