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:
36
.gitignore
vendored
36
.gitignore
vendored
@@ -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
|
||||
325
DEPLOYMENT.md
325
DEPLOYMENT.md
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 生产环境部署
|
||||
|
||||
### 方式 1:systemd 服务(推荐)
|
||||
|
||||
创建服务文件:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 方式 2:Docker 部署
|
||||
|
||||
创建 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`
|
||||
|
||||
105
FIX_SUMMARY.md
105
FIX_SUMMARY.md
@@ -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. 两个系统可以同时运行,互不干扰
|
||||
|
||||
226
MULTI_PROCESS.md
226
MULTI_PROCESS.md
@@ -1,226 +0,0 @@
|
||||
# 多进程异步并行架构
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 当前架构(改造前)
|
||||
|
||||
```
|
||||
单进程 + 单事件循环
|
||||
┌─────────────────────┐
|
||||
│ Python 进程 │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ asyncio Loop │ │
|
||||
│ │ 所有客户 + Agent │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- ❌ 单 CPU 核心
|
||||
- ❌ 一个客户卡住影响全局
|
||||
- ❌ 无法利用多核 CPU
|
||||
|
||||
---
|
||||
|
||||
### 新架构(改造后)
|
||||
|
||||
```
|
||||
多进程 + 多事件循环
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│进程 1 │ │进程 2 │ │进程 3 │
|
||||
│Loop + │ │Loop + │ │Loop + │
|
||||
│客户 A,B │ │客户 C,D │ │客户 E,F │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
↓ ↓ ↓
|
||||
独立运行 独立运行 独立运行
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- ✅ 真正的多核并行
|
||||
- ✅ 故障隔离
|
||||
- ✅ 负载均衡
|
||||
- ✅ 可动态扩缩容
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方式 1:systemd 服务(推荐)
|
||||
|
||||
```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
176
README.md
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 | 临时输出 |
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
[1m[36m────────────────────────────────────────────────────────────[0m
|
||||
[1m[36m 对话记录 test_user_001 (4 条)[0m
|
||||
[1m[36m────────────────────────────────────────────────────────────[0m
|
||||
|
||||
[2m──────────────────── 2026-02-25 ────────────────────[0m
|
||||
[2m17:44[0m [97m买家[0m
|
||||
[48;5;236m 你好,想问下图片处理多少钱 [0m
|
||||
|
||||
[2m17:44[0m [32m客服[0m
|
||||
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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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` 静态配置,并发送企微「谁在线啊」提醒
|
||||
@@ -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` 同步本地后派单。无人在线时发企微「谁在线啊」。
|
||||
@@ -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` 接口,按上述格式返回在线名单。
|
||||
|
||||
@@ -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"
|
||||
@@ -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,39 +1078,55 @@ 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)
|
||||
except Exception:
|
||||
pass
|
||||
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:
|
||||
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
|
||||
|
||||
@@ -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
@@ -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": "上次对话时间"
|
||||
}
|
||||
}
|
||||
@@ -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,6 +45,24 @@ def _get_conn() -> sqlite3.Connection:
|
||||
def init_db():
|
||||
"""建表(首次运行时自动调用)"""
|
||||
with _get_conn() as conn:
|
||||
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,
|
||||
@@ -36,7 +78,6 @@ def init_db():
|
||||
""")
|
||||
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:
|
||||
@@ -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 "
|
||||
_sql("INSERT INTO chat_logs "
|
||||
"(customer_id, customer_name, acc_id, platform, direction, message, msg_type, timestamp) "
|
||||
"VALUES (?,?,?,?,?,?,?,?)",
|
||||
"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,7 +230,24 @@ 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("""
|
||||
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,
|
||||
@@ -191,7 +262,7 @@ def get_daily_conversations(date: str = "") -> List[Dict]:
|
||||
WHERE timestamp LIKE ?
|
||||
GROUP BY acc_id, customer_id
|
||||
ORDER BY acc_id, MAX(timestamp) DESC
|
||||
""", (f"{date}%",)).fetchall()
|
||||
"""), (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]
|
||||
|
||||
@@ -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,6 +43,28 @@ def _get_conn() -> sqlite3.Connection:
|
||||
|
||||
def _init_db():
|
||||
with _get_conn() as conn:
|
||||
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,
|
||||
@@ -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:
|
||||
|
||||
@@ -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,6 +45,37 @@ def _get_conn() -> sqlite3.Connection:
|
||||
|
||||
def init_db():
|
||||
with _get_conn() as conn:
|
||||
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,
|
||||
@@ -61,18 +116,30 @@ init_db()
|
||||
def add_designer(name: str, wechat_user_id: str) -> int:
|
||||
"""添加设计师,返回 id"""
|
||||
with _get_conn() as conn:
|
||||
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:
|
||||
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),
|
||||
@@ -85,6 +152,12 @@ 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:
|
||||
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),
|
||||
@@ -101,22 +174,28 @@ 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]
|
||||
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),
|
||||
@@ -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({
|
||||
|
||||
@@ -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,12 +54,51 @@ class ImageTaskManager:
|
||||
|
||||
def _init_db(self):
|
||||
"""初始化数据库"""
|
||||
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,
|
||||
@@ -49,8 +106,8 @@ class ImageTaskManager:
|
||||
customer_name TEXT,
|
||||
original_image TEXT NOT NULL,
|
||||
operation TEXT DEFAULT 'enhance',
|
||||
requirements TEXT, -- JSON 格式:复杂度、比例、透视等
|
||||
customer_notes TEXT, -- 客户备注/需求细节
|
||||
requirements TEXT,
|
||||
customer_notes TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TEXT,
|
||||
paid_at TEXT,
|
||||
@@ -59,38 +116,43 @@ class ImageTaskManager:
|
||||
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
|
||||
change_type TEXT,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_at TEXT,
|
||||
changed_by TEXT, -- customer/staff
|
||||
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()
|
||||
|
||||
|
||||
@@ -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,12 +58,45 @@ class TaskManager:
|
||||
|
||||
def _init_db(self):
|
||||
"""初始化数据库"""
|
||||
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,
|
||||
@@ -56,7 +107,7 @@ class TaskManager:
|
||||
customer_id TEXT,
|
||||
trigger_type TEXT,
|
||||
trigger_keyword TEXT,
|
||||
trigger_keywords TEXT, -- JSON array
|
||||
trigger_keywords TEXT,
|
||||
action_type TEXT,
|
||||
action_file_url TEXT,
|
||||
action_message TEXT,
|
||||
@@ -70,21 +121,30 @@ class TaskManager:
|
||||
triggered_at TEXT,
|
||||
completed_at TEXT,
|
||||
error_message TEXT,
|
||||
result TEXT -- JSON
|
||||
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,6 +344,13 @@ class TaskManager:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
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'
|
||||
@@ -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()
|
||||
|
||||
@@ -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
222
run.py
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
313
tests/test_ai_chat.py
Normal 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')
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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())
|
||||
229
三种工作流功能说明.md
229
三种工作流功能说明.md
@@ -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
|
||||
|
||||
207
作图失败转接人工说明.md
207
作图失败转接人工说明.md
@@ -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
|
||||
|
||||
247
图片任务数据库功能说明.md
247
图片任务数据库功能说明.md
@@ -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
|
||||
|
||||
278
图绘派单系统集成说明.md
278
图绘派单系统集成说明.md
@@ -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
|
||||
|
||||
225
文字加价功能说明.md
225
文字加价功能说明.md
@@ -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
|
||||
|
||||
680
部署文档.md
680
部署文档.md
@@ -1,6 +1,26 @@
|
||||
# AI 客服系统 - 天网协作版部署文档
|
||||
# AI 客服系统 - 部署与运维文档
|
||||
|
||||
## 📋 系统架构
|
||||
**版本**: v1.0 | **更新日期**: 2026-02-28
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统架构](#系统架构)
|
||||
2. [快速部署](#快速部署)
|
||||
3. [启动方式](#启动方式)
|
||||
4. [生产环境部署](#生产环境部署)
|
||||
5. [多进程架构](#多进程架构)
|
||||
6. [API 接口文档](#api-接口文档)
|
||||
7. [触发条件详解](#触发条件详解)
|
||||
8. [数据库](#数据库)
|
||||
9. [配置说明](#配置说明)
|
||||
10. [监控与日志](#监控与日志)
|
||||
11. [故障排查](#故障排查)
|
||||
|
||||
---
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
@@ -8,8 +28,7 @@
|
||||
│ (公网 IP) │ │ (127.0.0.1:6060)│ │ (轻简软件) │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘
|
||||
↑ │
|
||||
│ ↓
|
||||
└──────────────┐
|
||||
└─────────────────────┘
|
||||
┌──────────────┐
|
||||
│ SQLite │
|
||||
│ 任务数据库 │
|
||||
@@ -20,72 +39,190 @@
|
||||
|
||||
| 组件 | 地址 | 说明 |
|
||||
|------|------|------|
|
||||
| **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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 方式 1:systemd 服务(推荐)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 方式 2:Docker 部署
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 故障排查
|
||||
## 故障排查
|
||||
|
||||
### 问题 1:API 无法访问
|
||||
### 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 客服系统团队
|
||||
|
||||
|
||||
631
项目功能汇总.md
631
项目功能汇总.md
@@ -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 处理 → 上传到图绘
|
||||
AI 检测到"找一下"关键词 → 执行查找图片工作流
|
||||
↓
|
||||
回复:"找到了!http://tuhui.cloud/works/123"
|
||||
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 | 图片上传 |
|
||||
|
||||
270
风险评估功能说明.md
270
风险评估功能说明.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user