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 客服系统 - 天网协作版
|
# AI 客服系统 - 天网协作版
|
||||||
|
|
||||||
**版本**: v1.0
|
**版本**: v1.0 | **服务器**: 1.12.50.92
|
||||||
**更新日期**: 2026-02-27
|
|
||||||
**服务器**: 1.12.50.92
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 功能概览
|
## 功能概览
|
||||||
|
|
||||||
### 核心功能
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
1. **天网协作** - 接收天网任务,支持指定客户回复触发
|
| 天网协作 | 接收天网任务,支持指定客户回复触发 |
|
||||||
2. **三种工作流** - 根据客户说的话自动判断执行
|
| 三种工作流 | 找图 / 处理图片 / 转人工派单 |
|
||||||
3. **图片任务数据库** - 任务持久化,支持后续增加需求
|
| 图片任务数据库 | 任务持久化,支持后续增加需求 |
|
||||||
4. **图绘派单系统** - 自动派单给在线设计师
|
| 图绘派单系统 | 自动派单给在线设计师 |
|
||||||
5. **文字检测加价** - 自动识别文字数量并加价(60-80 元高价值订单)
|
| 文字检测加价 | 自动识别文字数量并加价 |
|
||||||
6. **风险评估** - 自动识别敏感内容,拒绝不良订单
|
| 风险评估 | 自动识别敏感内容,拒绝不良订单 |
|
||||||
7. **作图失败转人工** - 失败自动转接人工客服
|
| 作图失败转人工 | 失败自动转接人工客服 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 启动服务
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /root/ai_customer_service/ai_cs
|
cd /root/ai_customer_service/ai_cs
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
# 启动天网协作版(推荐)
|
# 天网协作版(仅 HTTP API)
|
||||||
python3 run_tianwang_simple.py
|
python3 run.py --api-only
|
||||||
|
|
||||||
# 后台运行
|
# 完整版(HTTP API + WebSocket + AI Agent)
|
||||||
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
|
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
|
```bash
|
||||||
curl -X POST http://localhost:6060/api/task/receive \
|
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
|
||||||
-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
|
```bash
|
||||||
curl -X GET "http://1.12.50.92:8005/dispatch/queue" \
|
curl http://localhost:6060/api/health
|
||||||
-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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 文档
|
## API 地址
|
||||||
|
|
||||||
| 文档 | 说明 |
|
| 服务 | 地址 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `项目功能汇总.md` | **完整功能说明** ⭐ |
|
| AI 客服 API | `http://127.0.0.1:6060` |
|
||||||
| `DEPLOYMENT.md` | 部署文档 |
|
| 派单系统 | `http://1.12.50.92:8005` |
|
||||||
| `TIANWANG_INTEGRATION.md` | 天网协作 |
|
| 图绘平台 | `http://1.12.50.92:8002` |
|
||||||
| `三种工作流功能说明.md` | 工作流 |
|
|
||||||
| `文字加价功能说明.md` | 价格策略 |
|
|
||||||
| `风险评估功能说明.md` | 风险控制 |
|
|
||||||
| `图片任务数据库功能说明.md` | 任务管理 |
|
|
||||||
| `图绘派单系统集成说明.md` | 派单系统 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 使用示例
|
## 文档
|
||||||
|
|
||||||
### 客户发送图片
|
| 文档 | 内容 |
|
||||||
|
|------|------|
|
||||||
```
|
| **项目功能汇总.md** | 全部功能详细说明(工作流、报价、风险、派单、数据库等) |
|
||||||
客户:找一下这个图 [图片]
|
| **部署文档.md** | 部署、API 接口、天网集成、多进程、故障排查 |
|
||||||
AI: 找到了!http://tuhui.cloud/works/123
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
客户:做一下 [图片]
|
|
||||||
AI: 稍等,我看看...好的,可以做
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
客户:这个能做吗 [图片]
|
|
||||||
AI: 抱歉,这个我做不了
|
|
||||||
AI: 好的,已帮您安排设计师处理
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📞 技术支持
|
## 项目结构
|
||||||
|
|
||||||
**服务器**: 1.12.50.92
|
```
|
||||||
**日志**: `/tmp/tianwang.log`
|
├── api/ # HTTP API 服务器
|
||||||
**进程**: `ps aux | grep run_tianwang`
|
├── 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": {
|
"daily": {
|
||||||
"2026-02-26": 1.4850000000000005,
|
"2026-02-26": 1.4850000000000005,
|
||||||
"2026-02-27": 1.3200000000000007
|
"2026-02-27": 1.3200000000000007,
|
||||||
|
"2026-02-28": 0.25999999999999995
|
||||||
},
|
},
|
||||||
"monthly": {
|
"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
|
## transfer_groups.json
|
||||||
|
|
||||||
@@ -18,19 +18,47 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 设计师派单(SQLite,可选)
|
## 设计师派单(SQLite)
|
||||||
|
|
||||||
同一设计师在不同店铺对应不同 group_id,转人工时按需查询在线状态,从在线设计师中轮询派单。
|
同一设计师在不同店铺对应不同 group_id,转人工时按需查询在线状态,从在线设计师中轮询派单。
|
||||||
|
|
||||||
**初始化数据**:
|
### 数据库
|
||||||
|
|
||||||
|
路径: `db/designer_roster_db/roster.db`
|
||||||
|
|
||||||
|
| 表 | 说明 |
|
||||||
|
|---|------|
|
||||||
|
| `designers` | 设计师(name, wechat_user_id)|
|
||||||
|
| `designer_shops` | 设计师在某店铺的 group_id(同一人不同店铺不同分组)|
|
||||||
|
| `designer_online` | 在线状态(转人工时按需查询外部 API 同步)|
|
||||||
|
|
||||||
|
### 初始化数据
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/init_designer_roster.py example # 写入示例
|
python scripts/init_designer_roster.py example # 写入示例
|
||||||
python scripts/init_designer_roster.py list # 查看当前数据
|
python scripts/init_designer_roster.py list # 查看当前数据
|
||||||
```
|
```
|
||||||
|
|
||||||
**数据库**:`db/designer_roster_db/roster.db`
|
### 在线查询 API
|
||||||
- `designers`:设计师(name, wechat_user_id)
|
|
||||||
- `designer_shops`:设计师在某店铺的 group_id(同一人不同店铺不同分组)
|
|
||||||
- `designer_online`:在线状态(转人工时按需查询外部 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()
|
load_dotenv()
|
||||||
|
|
||||||
from services.service_tuhui_upload import upload_to_tuhui
|
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"
|
_WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
|
||||||
@@ -200,9 +202,9 @@ class CustomerServiceAgent:
|
|||||||
system_prompt=self._get_similar_prompt()
|
system_prompt=self._get_similar_prompt()
|
||||||
)
|
)
|
||||||
# 工作流程路由器
|
# 工作流程路由器
|
||||||
self.workflow_router = get_workflow_router()
|
self.workflow_router = get_workflow_router()
|
||||||
|
|
||||||
self.agent_order = Agent(
|
self.agent_order = Agent(
|
||||||
model=model,
|
model=model,
|
||||||
deps_type=AgentDeps,
|
deps_type=AgentDeps,
|
||||||
system_prompt=self._get_order_prompt()
|
system_prompt=self._get_order_prompt()
|
||||||
@@ -260,7 +262,6 @@ self.agent_order = Agent(
|
|||||||
# 在 workflow 里创建待处理任务(付款后自动触发 Gemini)
|
# 在 workflow 里创建待处理任务(付款后自动触发 Gemini)
|
||||||
try:
|
try:
|
||||||
from core.workflow import workflow
|
from core.workflow import workflow
|
||||||
from core.workflow_router import get_workflow_router
|
|
||||||
await workflow.image_analysis_result(
|
await workflow.image_analysis_result(
|
||||||
customer_id=ctx.deps.from_id,
|
customer_id=ctx.deps.from_id,
|
||||||
image_url=image_url,
|
image_url=image_url,
|
||||||
@@ -455,21 +456,20 @@ from core.workflow_router import get_workflow_router
|
|||||||
|
|
||||||
@self.agent.tool
|
@self.agent.tool
|
||||||
async def process_image_gemini(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
|
async def process_image_gemini(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
|
||||||
|
"""
|
||||||
|
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
|
||||||
|
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
|
||||||
|
处理完成后会自动发图给客户。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from config.config import IMAGE_MODULE_ENABLED
|
from config.config import IMAGE_MODULE_ENABLED
|
||||||
if not IMAGE_MODULE_ENABLED:
|
if not IMAGE_MODULE_ENABLED:
|
||||||
return "现在处理模块暂时暂停,先不自动作图"
|
return "现在处理模块暂时暂停,先不自动作图"
|
||||||
except Exception:
|
except Exception:
|
||||||
return "现在处理模块暂时暂停,先不自动作图"
|
return "现在处理模块暂时暂停,先不自动作图"
|
||||||
"""
|
|
||||||
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
|
|
||||||
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
|
|
||||||
处理完成后会自动发图给客户。
|
|
||||||
"""
|
|
||||||
cid = customer_id or ctx.deps.from_id
|
cid = customer_id or ctx.deps.from_id
|
||||||
try:
|
try:
|
||||||
from core.workflow import workflow
|
from core.workflow import workflow
|
||||||
from core.workflow_router import get_workflow_router
|
|
||||||
ok = await workflow.trigger_processing_on_payment(
|
ok = await workflow.trigger_processing_on_payment(
|
||||||
customer_id=cid,
|
customer_id=cid,
|
||||||
acc_id=ctx.deps.acc_id,
|
acc_id=ctx.deps.acc_id,
|
||||||
@@ -518,26 +518,8 @@ from core.workflow_router import get_workflow_router
|
|||||||
|
|
||||||
@self.agent_processing.tool
|
@self.agent_processing.tool
|
||||||
async def process_image_gemini_run(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
|
async def process_image_gemini_run(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
|
||||||
try:
|
"""触发 Gemini 作图处理(processing agent 专用入口)。"""
|
||||||
from config.config import IMAGE_MODULE_ENABLED
|
return await process_image_gemini(ctx, customer_id)
|
||||||
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}"
|
|
||||||
|
|
||||||
@self.agent_similar.tool
|
@self.agent_similar.tool
|
||||||
async def recommend_similar(ctx: RunContext[AgentDeps], hint: str = "") -> str:
|
async def recommend_similar(ctx: RunContext[AgentDeps], hint: str = "") -> str:
|
||||||
@@ -986,7 +968,7 @@ from core.workflow_router import get_workflow_router
|
|||||||
- 输出不超过1句话"""
|
- 输出不超过1句话"""
|
||||||
|
|
||||||
def _get_customer_profile_context(self, customer_id: str) -> str:
|
def _get_customer_profile_context(self, customer_id: str) -> str:
|
||||||
"""从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测。"""
|
"""从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测、近期对话。"""
|
||||||
try:
|
try:
|
||||||
from db.customer_db import db
|
from db.customer_db import db
|
||||||
profile = db.get_customer(customer_id)
|
profile = db.get_customer(customer_id)
|
||||||
@@ -994,41 +976,95 @@ from core.workflow_router import get_workflow_router
|
|||||||
if profile.blacklist:
|
if profile.blacklist:
|
||||||
return f"【⚠️黑名单客户】原因:{profile.blacklist_reason or '已标记'},请转接人工处理,不要自动回复"
|
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:
|
if profile.email:
|
||||||
parts.append(f"邮箱:{profile.email}")
|
basic_info.append(f"邮箱: {profile.email}")
|
||||||
if profile.phone:
|
if profile.phone:
|
||||||
parts.append(f"电话:{profile.phone}")
|
basic_info.append(f"电话: {profile.phone}")
|
||||||
if profile.wechat:
|
if profile.wechat:
|
||||||
parts.append(f"微信:{profile.wechat}")
|
basic_info.append(f"微信: {profile.wechat}")
|
||||||
if profile.personality:
|
lines.append(" | ".join(basic_info))
|
||||||
parts.append(f"性格:{'/'.join(profile.personality)}")
|
|
||||||
|
# 消费分析
|
||||||
|
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:
|
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:
|
if profile.vip_custom_price:
|
||||||
parts.append(f"VIP专属价:{profile.vip_custom_price}元(直接用这个价报)")
|
price_info.append(f"VIP专属价: {profile.vip_custom_price}元(直接报这个价)")
|
||||||
elif profile.last_price:
|
if profile.last_price:
|
||||||
parts.append(f"上次报价:{profile.last_price}元")
|
price_info.append(f"上次报价: {profile.last_price}元")
|
||||||
if profile.lowest_price_accepted:
|
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:
|
if profile.discount_given_count:
|
||||||
parts.append(f"历史让价:{profile.discount_given_count}次")
|
price_info.append(f"历史让价: {profile.discount_given_count}次")
|
||||||
if getattr(profile, "last_quote_no_convert", False):
|
|
||||||
parts.append("【报价策略】上次报价未成交,本次可适当降低5-10元促成交")
|
|
||||||
if profile.price_sensitivity:
|
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:
|
if profile.decision_speed:
|
||||||
parts.append(f"决策速度:{profile.decision_speed}")
|
personality_info.append(f"决策速度: {profile.decision_speed}")
|
||||||
if profile.last_image_url:
|
if profile.communication_prefer:
|
||||||
parts.append(f"上次发图:{profile.last_image_url}")
|
personality_info.append(f"沟通偏好: {profile.communication_prefer}")
|
||||||
if profile.processing_status:
|
if personality_info:
|
||||||
parts.append(f"当前任务:{profile.processing_status}")
|
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:
|
if profile.preferred_format:
|
||||||
parts.append(f"格式偏好:{profile.preferred_format}")
|
image_info.append(f"格式偏好: {profile.preferred_format}")
|
||||||
if profile.bulk_potential == "有":
|
if profile.preferred_size:
|
||||||
parts.append("批量潜力:有(可主动推打包价)")
|
image_info.append(f"尺寸要求: {profile.preferred_size}")
|
||||||
if profile.upsell_opportunity:
|
if profile.last_image_url:
|
||||||
parts.append(f"加购机会:{'/'.join(profile.upsell_opportunity)}")
|
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:
|
if profile.last_conversation_summary:
|
||||||
time_str = ""
|
time_str = ""
|
||||||
if profile.last_conversation_time:
|
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 "(刚刚)"
|
time_str = f"({h}小时前)" if h > 0 else "(刚刚)"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
parts.append(f"上次对话{time_str}:{profile.last_conversation_summary}")
|
lines.append(f"--- 上次对话 {time_str} ---")
|
||||||
|
lines.append(profile.last_conversation_summary)
|
||||||
|
|
||||||
# 个性化:语气与报价策略
|
# 个性化回复策略
|
||||||
hints = []
|
hints = []
|
||||||
if profile.personality:
|
if profile.personality:
|
||||||
if "爽快" in profile.personality:
|
if "爽快" in profile.personality:
|
||||||
hints.append("回复简洁直接,少废话")
|
hints.append("回复简洁直接,不废话,快速报价")
|
||||||
if "砍价" in profile.personality or "砍价狂" in profile.personality:
|
if "砍价" in profile.personality or "砍价狂" in profile.personality:
|
||||||
hints.append("报价时强调性价比,只让价一次")
|
hints.append("报价时强调性价比,只让价一次,第二次引导去 xinhui.cloud")
|
||||||
if "纠结" in profile.personality or "墨迹" in profile.personality:
|
if "纠结" in profile.personality or "墨迹" in profile.personality:
|
||||||
hints.append("耐心一点,可多给一点说明")
|
hints.append("多给一点说明,耐心回答")
|
||||||
if profile.price_sensitivity == "高":
|
if profile.price_sensitivity == "高":
|
||||||
hints.append("报价时顺带提「满意再拍」降低顾虑")
|
hints.append("报价时顺带提「满意再拍」降低顾虑")
|
||||||
if profile.decision_speed == "快":
|
if profile.decision_speed == "快":
|
||||||
hints.append("重点推快速成交,少铺垫")
|
hints.append("直接报价推成交,少铺垫")
|
||||||
|
if profile.total_orders > 0 and profile.decision_speed == "快":
|
||||||
|
hints.append("老客爽快,直接报价成交")
|
||||||
if hints:
|
if hints:
|
||||||
parts.append(f"【回复风格】{'; '.join(hints)}")
|
lines.append("--- 回复策略 ---")
|
||||||
|
lines.append(";".join(hints))
|
||||||
|
|
||||||
# 主动预测:批量/加急
|
# 主动推荐
|
||||||
proactive = []
|
proactive = []
|
||||||
if profile.bulk_potential == "有" or (profile.total_images_sent or 0) >= 2:
|
if profile.bulk_potential == "有" or (profile.total_images_sent or 0) >= 2:
|
||||||
proactive.append("可主动问「要做多张吗,多张有优惠」")
|
proactive.append("可问「要做多张吗,多张有优惠」")
|
||||||
if profile.total_orders > 0 and profile.decision_speed == "快":
|
if profile.upsell_opportunity:
|
||||||
proactive.append("老客爽快,直接推成交")
|
proactive.append(f"加购机会: {'、'.join(profile.upsell_opportunity)}")
|
||||||
if proactive:
|
if proactive:
|
||||||
parts.append(f"【主动推荐】{'; '.join(proactive)}")
|
lines.append("--- 主动推荐 ---")
|
||||||
|
lines.append(";".join(proactive))
|
||||||
|
|
||||||
if parts:
|
return "\n".join(lines)
|
||||||
return "【该客户历史信息】" + " | ".join(parts)
|
except Exception as e:
|
||||||
except Exception:
|
print(f"[Agent] 获取客户画像失败: {e}")
|
||||||
pass
|
|
||||||
return ""
|
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:
|
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 作图
|
# 已付款:触发 Gemini 作图
|
||||||
try:
|
try:
|
||||||
from core.workflow import workflow
|
from core.workflow import workflow
|
||||||
from core.workflow_router import get_workflow_router
|
|
||||||
asyncio.create_task(workflow.trigger_processing_on_payment(
|
asyncio.create_task(workflow.trigger_processing_on_payment(
|
||||||
customer_id=message.from_id,
|
customer_id=message.from_id,
|
||||||
acc_id=message.acc_id,
|
acc_id=message.acc_id,
|
||||||
@@ -1816,6 +1867,51 @@ from core.workflow_router import get_workflow_router
|
|||||||
|
|
||||||
return prompt
|
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():
|
async def test_agent():
|
||||||
"""测试 Agent"""
|
"""测试 Agent"""
|
||||||
@@ -1842,65 +1938,3 @@ async def test_agent():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import asyncio
|
import asyncio
|
||||||
asyncio.run(test_agent())
|
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)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# ========== 全局实例 ==========
|
|
||||||
workflow = CustomerServiceWorkflow()
|
|
||||||
|
|
||||||
# ========== 客户需求变更 ==========
|
# ========== 客户需求变更 ==========
|
||||||
|
|
||||||
async def add_customer_requirement(self, task_id: str, customer_id: str,
|
async def add_customer_requirement(self, task_id: str, customer_id: str,
|
||||||
requirement: str, changed_by: str = 'customer') -> bool:
|
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)
|
task = self.get_task(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
@@ -981,3 +965,7 @@ workflow = CustomerServiceWorkflow()
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"派单失败:{e}")
|
logger.error(f"派单失败:{e}")
|
||||||
return False
|
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
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
_DB_PATH = os.path.join(os.path.dirname(__file__), "chat_log_db", "chats.db")
|
_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:
|
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)
|
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
|
||||||
conn = sqlite3.connect(_DB_PATH)
|
conn = sqlite3.connect(_DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -21,6 +45,24 @@ def _get_conn() -> sqlite3.Connection:
|
|||||||
def init_db():
|
def init_db():
|
||||||
"""建表(首次运行时自动调用)"""
|
"""建表(首次运行时自动调用)"""
|
||||||
with _get_conn() as conn:
|
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("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS chat_logs (
|
CREATE TABLE IF NOT EXISTS chat_logs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
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_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_ts ON chat_logs(timestamp)")
|
||||||
# 兼容旧表:若缺少 acc_id 列则补上(必须在创建该列索引之前)
|
|
||||||
try:
|
try:
|
||||||
conn.execute("ALTER TABLE chat_logs ADD COLUMN acc_id TEXT DEFAULT ''")
|
conn.execute("ALTER TABLE chat_logs ADD COLUMN acc_id TEXT DEFAULT ''")
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -63,9 +104,9 @@ def log_message(
|
|||||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chat_logs "
|
_sql("INSERT INTO chat_logs "
|
||||||
"(customer_id, customer_name, acc_id, platform, direction, message, msg_type, timestamp) "
|
"(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),
|
(customer_id, customer_name, acc_id, platform, direction, message, msg_type, ts),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -77,6 +118,19 @@ def get_customers(limit: int = 100) -> List[Dict]:
|
|||||||
"""返回所有有记录的客户列表(按最新消息时间排序)"""
|
"""返回所有有记录的客户列表(按最新消息时间排序)"""
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
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
|
SELECT
|
||||||
customer_id,
|
customer_id,
|
||||||
MAX(customer_name) AS customer_name,
|
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]:
|
def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]:
|
||||||
"""返回某客户的全部对话记录(按时间升序)"""
|
"""返回某客户的全部对话记录(按时间升序)"""
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT id, direction, message, msg_type, timestamp, acc_id
|
SELECT id, direction, message, msg_type, timestamp, acc_id
|
||||||
FROM chat_logs
|
FROM chat_logs
|
||||||
WHERE customer_id = ?
|
WHERE customer_id = ?
|
||||||
ORDER BY timestamp ASC, id ASC
|
ORDER BY timestamp ASC, id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", (customer_id, limit)).fetchall()
|
"""), (customer_id, limit)).fetchall()
|
||||||
return [dict(r) for r in rows]
|
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:
|
with _get_conn() as conn:
|
||||||
if acc_id:
|
if acc_id:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT id, direction, message, timestamp, acc_id
|
SELECT id, direction, message, timestamp, acc_id
|
||||||
FROM chat_logs
|
FROM chat_logs
|
||||||
WHERE customer_id = ? AND acc_id = ?
|
WHERE customer_id = ? AND acc_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", (customer_id, acc_id, limit)).fetchall()
|
"""), (customer_id, acc_id, limit)).fetchall()
|
||||||
else:
|
else:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT id, direction, message, timestamp, acc_id
|
SELECT id, direction, message, timestamp, acc_id
|
||||||
FROM chat_logs
|
FROM chat_logs
|
||||||
WHERE customer_id = ?
|
WHERE customer_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", (customer_id, limit)).fetchall()
|
"""), (customer_id, limit)).fetchall()
|
||||||
out = [dict(r) for r in reversed(rows)]
|
out = [dict(r) for r in reversed(rows)]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -133,12 +187,12 @@ def get_conversation_today(customer_id: str) -> List[Dict]:
|
|||||||
"""返回某客户今天的对话"""
|
"""返回某客户今天的对话"""
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT id, direction, message, msg_type, timestamp
|
SELECT id, direction, message, msg_type, timestamp
|
||||||
FROM chat_logs
|
FROM chat_logs
|
||||||
WHERE customer_id = ? AND timestamp LIKE ?
|
WHERE customer_id = ? AND timestamp LIKE ?
|
||||||
ORDER BY timestamp ASC, id ASC
|
ORDER BY timestamp ASC, id ASC
|
||||||
""", (customer_id, f"{today}%")).fetchall()
|
"""), (customer_id, f"{today}%")).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
@@ -151,7 +205,7 @@ def get_daily_stats(date: str = "") -> List[Dict]:
|
|||||||
if not date:
|
if not date:
|
||||||
date = datetime.now().strftime("%Y-%m-%d")
|
date = datetime.now().strftime("%Y-%m-%d")
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT
|
SELECT
|
||||||
acc_id,
|
acc_id,
|
||||||
platform,
|
platform,
|
||||||
@@ -165,7 +219,7 @@ def get_daily_stats(date: str = "") -> List[Dict]:
|
|||||||
WHERE timestamp LIKE ?
|
WHERE timestamp LIKE ?
|
||||||
GROUP BY acc_id
|
GROUP BY acc_id
|
||||||
ORDER BY unique_customers DESC
|
ORDER BY unique_customers DESC
|
||||||
""", (f"{date}%",)).fetchall()
|
"""), (f"{date}%",)).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
@@ -176,7 +230,24 @@ def get_daily_conversations(date: str = "") -> List[Dict]:
|
|||||||
if not date:
|
if not date:
|
||||||
date = datetime.now().strftime("%Y-%m-%d")
|
date = datetime.now().strftime("%Y-%m-%d")
|
||||||
with _get_conn() as conn:
|
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
|
SELECT
|
||||||
acc_id,
|
acc_id,
|
||||||
customer_id,
|
customer_id,
|
||||||
@@ -191,7 +262,7 @@ def get_daily_conversations(date: str = "") -> List[Dict]:
|
|||||||
WHERE timestamp LIKE ?
|
WHERE timestamp LIKE ?
|
||||||
GROUP BY acc_id, customer_id
|
GROUP BY acc_id, customer_id
|
||||||
ORDER BY acc_id, MAX(timestamp) DESC
|
ORDER BY acc_id, MAX(timestamp) DESC
|
||||||
""", (f"{date}%",)).fetchall()
|
"""), (f"{date}%",)).fetchall()
|
||||||
return [dict(r) for r in rows]
|
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:
|
if customer_id:
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT customer_id, customer_name, direction, message, timestamp
|
SELECT customer_id, customer_name, direction, message, timestamp
|
||||||
FROM chat_logs
|
FROM chat_logs
|
||||||
WHERE customer_id = ? AND message LIKE ?
|
WHERE customer_id = ? AND message LIKE ?
|
||||||
ORDER BY timestamp DESC LIMIT ?
|
ORDER BY timestamp DESC LIMIT ?
|
||||||
""", (customer_id, f"%{keyword}%", limit)).fetchall()
|
"""), (customer_id, f"%{keyword}%", limit)).fetchall()
|
||||||
else:
|
else:
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT customer_id, customer_name, direction, message, timestamp
|
SELECT customer_id, customer_name, direction, message, timestamp
|
||||||
FROM chat_logs
|
FROM chat_logs
|
||||||
WHERE message LIKE ?
|
WHERE message LIKE ?
|
||||||
ORDER BY timestamp DESC LIMIT ?
|
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]
|
return [dict(r) for r in rows]
|
||||||
|
|||||||
@@ -8,9 +8,33 @@ from datetime import datetime
|
|||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
_DB_PATH = os.path.join(os.path.dirname(__file__), "deal_outcome_db", "outcomes.db")
|
_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:
|
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)
|
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
|
||||||
conn = sqlite3.connect(_DB_PATH)
|
conn = sqlite3.connect(_DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -19,6 +43,28 @@ def _get_conn() -> sqlite3.Connection:
|
|||||||
|
|
||||||
def _init_db():
|
def _init_db():
|
||||||
with _get_conn() as conn:
|
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("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS deal_outcomes (
|
CREATE TABLE IF NOT EXISTS deal_outcomes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -61,10 +107,10 @@ def record_deal(
|
|||||||
date = datetime.now().strftime("%Y-%m-%d")
|
date = datetime.now().strftime("%Y-%m-%d")
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO deal_outcomes
|
_sql("""INSERT INTO deal_outcomes
|
||||||
(customer_id, customer_name, acc_id, platform, date, outcome, reason,
|
(customer_id, customer_name, acc_id, platform, date, outcome, reason,
|
||||||
order_id, amount, discount_given, timestamp)
|
order_id, amount, discount_given, timestamp)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)"""),
|
||||||
(
|
(
|
||||||
customer_id,
|
customer_id,
|
||||||
customer_name or "",
|
customer_name or "",
|
||||||
@@ -88,13 +134,13 @@ def get_daily_outcomes(date: str = "") -> List[Dict]:
|
|||||||
date = datetime.now().strftime("%Y-%m-%d")
|
date = datetime.now().strftime("%Y-%m-%d")
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
_sql("""
|
||||||
SELECT customer_id, customer_name, acc_id, outcome, reason,
|
SELECT customer_id, customer_name, acc_id, outcome, reason,
|
||||||
order_id, amount, discount_given, timestamp
|
order_id, amount, discount_given, timestamp
|
||||||
FROM deal_outcomes
|
FROM deal_outcomes
|
||||||
WHERE date = ?
|
WHERE date = ?
|
||||||
ORDER BY timestamp ASC
|
ORDER BY timestamp ASC
|
||||||
""",
|
"""),
|
||||||
(date,),
|
(date,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
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:
|
with _get_conn() as conn:
|
||||||
if start_date and end_date:
|
if start_date and end_date:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""SELECT * FROM deal_outcomes
|
_sql("""SELECT * FROM deal_outcomes
|
||||||
WHERE date BETWEEN ? AND ?
|
WHERE date BETWEEN ? AND ?
|
||||||
ORDER BY date, timestamp""",
|
ORDER BY date, timestamp"""),
|
||||||
(start_date, end_date),
|
(start_date, end_date),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
elif start_date:
|
elif start_date:
|
||||||
rows = conn.execute(
|
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,),
|
(start_date,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
elif end_date:
|
elif end_date:
|
||||||
rows = conn.execute(
|
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,),
|
(end_date,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -10,9 +10,33 @@ import os
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
_DB_PATH = os.path.join(os.path.dirname(__file__), "designer_roster_db", "roster.db")
|
_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:
|
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)
|
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
|
||||||
conn = sqlite3.connect(_DB_PATH)
|
conn = sqlite3.connect(_DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -21,6 +45,37 @@ def _get_conn() -> sqlite3.Connection:
|
|||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
with _get_conn() as conn:
|
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("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS designers (
|
CREATE TABLE IF NOT EXISTS designers (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -61,18 +116,30 @@ init_db()
|
|||||||
def add_designer(name: str, wechat_user_id: str) -> int:
|
def add_designer(name: str, wechat_user_id: str) -> int:
|
||||||
"""添加设计师,返回 id"""
|
"""添加设计师,返回 id"""
|
||||||
with _get_conn() as conn:
|
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(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO designers (name, wechat_user_id) VALUES (?, ?)",
|
"INSERT OR IGNORE INTO designers (name, wechat_user_id) VALUES (?, ?)",
|
||||||
(name, wechat_user_id),
|
(name, wechat_user_id),
|
||||||
)
|
)
|
||||||
conn.commit()
|
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
|
return row["id"] if row else 0
|
||||||
|
|
||||||
|
|
||||||
def set_designer_shop(designer_id: int, shop_id: str, group_id: str):
|
def set_designer_shop(designer_id: int, shop_id: str, group_id: str):
|
||||||
"""设置设计师在某店铺的分组 ID(同一设计师不同店铺不同 group_id)"""
|
"""设置设计师在某店铺的分组 ID(同一设计师不同店铺不同 group_id)"""
|
||||||
with _get_conn() as conn:
|
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(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (?, ?, ?)",
|
"INSERT OR REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (?, ?, ?)",
|
||||||
(designer_id, shop_id, group_id),
|
(designer_id, shop_id, group_id),
|
||||||
@@ -85,6 +152,12 @@ def update_online(wechat_user_id: str, is_online: bool):
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
with _get_conn() as conn:
|
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(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (?, ?, ?)",
|
"INSERT OR REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (?, ?, ?)",
|
||||||
(wechat_user_id, 1 if is_online else 0, ts),
|
(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。
|
无人在线则返回 None。
|
||||||
"""
|
"""
|
||||||
with _get_conn() as conn:
|
with _get_conn() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(_sql("""
|
||||||
SELECT d.wechat_user_id, ds.group_id
|
SELECT d.wechat_user_id, ds.group_id
|
||||||
FROM designer_shops ds
|
FROM designer_shops ds
|
||||||
JOIN designers d ON d.id = ds.designer_id
|
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
|
JOIN designer_online o ON o.wechat_user_id = d.wechat_user_id AND o.is_online = 1
|
||||||
WHERE ds.shop_id = ?
|
WHERE ds.shop_id = ?
|
||||||
""", (shop_id,)).fetchall()
|
"""), (shop_id,)).fetchall()
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with _get_conn() as conn:
|
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
|
last = rr["last_index"] if rr else 0
|
||||||
idx = last % len(rows)
|
idx = last % len(rows)
|
||||||
chosen = rows[idx]
|
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(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO round_robin (shop_id, last_index) VALUES (?, ?)",
|
"INSERT OR REPLACE INTO round_robin (shop_id, last_index) VALUES (?, ?)",
|
||||||
(shop_id, idx + 1),
|
(shop_id, idx + 1),
|
||||||
@@ -142,11 +221,11 @@ def list_designers():
|
|||||||
result = []
|
result = []
|
||||||
for d in designers:
|
for d in designers:
|
||||||
shops = conn.execute(
|
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"],),
|
(d["id"],),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
online = conn.execute(
|
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"],),
|
(d["wechat_user_id"],),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
result.append({
|
result.append({
|
||||||
|
|||||||
@@ -10,8 +10,26 @@ from typing import Optional, List, Dict
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class TaskStatus(Enum):
|
||||||
"""任务状态"""
|
"""任务状态"""
|
||||||
@@ -36,12 +54,51 @@ class ImageTaskManager:
|
|||||||
|
|
||||||
def _init_db(self):
|
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)
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 创建图片任务表
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS image_tasks (
|
CREATE TABLE IF NOT EXISTS image_tasks (
|
||||||
task_id TEXT PRIMARY KEY,
|
task_id TEXT PRIMARY KEY,
|
||||||
@@ -49,8 +106,8 @@ class ImageTaskManager:
|
|||||||
customer_name TEXT,
|
customer_name TEXT,
|
||||||
original_image TEXT NOT NULL,
|
original_image TEXT NOT NULL,
|
||||||
operation TEXT DEFAULT 'enhance',
|
operation TEXT DEFAULT 'enhance',
|
||||||
requirements TEXT, -- JSON 格式:复杂度、比例、透视等
|
requirements TEXT,
|
||||||
customer_notes TEXT, -- 客户备注/需求细节
|
customer_notes TEXT,
|
||||||
status TEXT DEFAULT 'pending',
|
status TEXT DEFAULT 'pending',
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
paid_at TEXT,
|
paid_at TEXT,
|
||||||
@@ -59,38 +116,43 @@ class ImageTaskManager:
|
|||||||
result_image TEXT,
|
result_image TEXT,
|
||||||
error_message TEXT,
|
error_message TEXT,
|
||||||
retry_count INTEGER DEFAULT 0,
|
retry_count INTEGER DEFAULT 0,
|
||||||
|
|
||||||
-- 店铺信息
|
|
||||||
acc_id TEXT,
|
acc_id TEXT,
|
||||||
acc_type TEXT DEFAULT 'AliWorkbench'
|
acc_type TEXT DEFAULT 'AliWorkbench'
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# 创建需求变更记录表(支持客户后续增加需求)
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
task_id TEXT NOT NULL,
|
task_id TEXT NOT NULL,
|
||||||
change_type TEXT, -- add_note/modify_operation/add_requirement
|
change_type TEXT,
|
||||||
old_value TEXT,
|
old_value TEXT,
|
||||||
new_value TEXT,
|
new_value TEXT,
|
||||||
changed_at TEXT,
|
changed_at TEXT,
|
||||||
changed_by TEXT, -- customer/staff
|
changed_by TEXT,
|
||||||
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
|
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_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_status ON image_tasks(status)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
logger.info("数据库表初始化完成")
|
logger.info("数据库表初始化完成")
|
||||||
|
|
||||||
def _get_conn(self):
|
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 = sqlite3.connect(self.db_path)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
return conn
|
||||||
@@ -105,13 +167,13 @@ class ImageTaskManager:
|
|||||||
|
|
||||||
requirements_json = json.dumps(requirements) if requirements else None
|
requirements_json = json.dumps(requirements) if requirements else None
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
INSERT INTO image_tasks (
|
INSERT INTO image_tasks (
|
||||||
task_id, customer_id, customer_name, original_image,
|
task_id, customer_id, customer_name, original_image,
|
||||||
operation, requirements, customer_notes, status,
|
operation, requirements, customer_notes, status,
|
||||||
created_at, acc_id, acc_type
|
created_at, acc_id, acc_type
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
'''), (
|
||||||
task_id,
|
task_id,
|
||||||
customer_id,
|
customer_id,
|
||||||
customer_name,
|
customer_name,
|
||||||
@@ -120,7 +182,7 @@ class ImageTaskManager:
|
|||||||
requirements_json,
|
requirements_json,
|
||||||
'', # 初始备注为空
|
'', # 初始备注为空
|
||||||
TaskStatus.PENDING.value,
|
TaskStatus.PENDING.value,
|
||||||
datetime.now().isoformat(),
|
_now_str(),
|
||||||
acc_id,
|
acc_id,
|
||||||
acc_type
|
acc_type
|
||||||
))
|
))
|
||||||
@@ -141,7 +203,7 @@ class ImageTaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
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()
|
row = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -164,17 +226,17 @@ class ImageTaskManager:
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
SELECT * FROM image_tasks
|
SELECT * FROM image_tasks
|
||||||
WHERE customer_id = ? AND status = ?
|
WHERE customer_id = ? AND status = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
''', (customer_id, status))
|
'''), (customer_id, status))
|
||||||
else:
|
else:
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
SELECT * FROM image_tasks
|
SELECT * FROM image_tasks
|
||||||
WHERE customer_id = ?
|
WHERE customer_id = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
''', (customer_id,))
|
'''), (customer_id,))
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -198,27 +260,28 @@ class ImageTaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
updates = ['status = ?']
|
placeholder = "%s" if _is_mysql() else "?"
|
||||||
|
updates = [f'status = {placeholder}']
|
||||||
params = [status.value]
|
params = [status.value]
|
||||||
|
|
||||||
# 根据状态设置时间
|
# 根据状态设置时间
|
||||||
if status == TaskStatus.PAID:
|
if status == TaskStatus.PAID:
|
||||||
updates.append('paid_at = ?')
|
updates.append(f'paid_at = {placeholder}')
|
||||||
params.append(datetime.now().isoformat())
|
params.append(_now_str())
|
||||||
elif status == TaskStatus.PROCESSING:
|
elif status == TaskStatus.PROCESSING:
|
||||||
updates.append('started_at = ?')
|
updates.append(f'started_at = {placeholder}')
|
||||||
params.append(datetime.now().isoformat())
|
params.append(_now_str())
|
||||||
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
|
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
|
||||||
updates.append('completed_at = ?')
|
updates.append(f'completed_at = {placeholder}')
|
||||||
params.append(datetime.now().isoformat())
|
params.append(_now_str())
|
||||||
|
|
||||||
params.append(task_id)
|
params.append(task_id)
|
||||||
|
|
||||||
cursor.execute(f'''
|
cursor.execute(_sql(f'''
|
||||||
UPDATE image_tasks
|
UPDATE image_tasks
|
||||||
SET {', '.join(updates)}
|
SET {', '.join(updates)}
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
''', params)
|
'''), params)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -234,11 +297,11 @@ class ImageTaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
UPDATE image_tasks
|
UPDATE image_tasks
|
||||||
SET result_image = ?, error_message = ?
|
SET result_image = ?, error_message = ?
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
''', (result_image, error_message, task_id))
|
'''), (result_image, error_message, task_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -265,30 +328,30 @@ class ImageTaskManager:
|
|||||||
cursor = conn.cursor()
|
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()
|
row = cursor.fetchone()
|
||||||
old_note = row['customer_notes'] if row else ''
|
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}"
|
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
|
UPDATE image_tasks
|
||||||
SET customer_notes = ?
|
SET customer_notes = ?
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
''', (new_note, task_id))
|
'''), (new_note, task_id))
|
||||||
|
|
||||||
# 记录变更历史
|
# 记录变更历史
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
INSERT INTO task_requirement_changes (
|
INSERT INTO task_requirement_changes (
|
||||||
task_id, change_type, old_value, new_value, changed_at, changed_by
|
task_id, change_type, old_value, new_value, changed_at, changed_by
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
'''), (
|
||||||
task_id,
|
task_id,
|
||||||
'add_note',
|
'add_note',
|
||||||
old_note or '无',
|
old_note or '无',
|
||||||
note,
|
note,
|
||||||
datetime.now().isoformat(),
|
_now_str(),
|
||||||
changed_by
|
changed_by
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -319,28 +382,28 @@ class ImageTaskManager:
|
|||||||
cursor = conn.cursor()
|
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()
|
row = cursor.fetchone()
|
||||||
old_operation = row['operation'] if row else ''
|
old_operation = row['operation'] if row else ''
|
||||||
|
|
||||||
# 更新操作
|
# 更新操作
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
UPDATE image_tasks
|
UPDATE image_tasks
|
||||||
SET operation = ?
|
SET operation = ?
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
''', (new_operation, task_id))
|
'''), (new_operation, task_id))
|
||||||
|
|
||||||
# 记录变更历史
|
# 记录变更历史
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
INSERT INTO task_requirement_changes (
|
INSERT INTO task_requirement_changes (
|
||||||
task_id, change_type, old_value, new_value, changed_at, changed_by
|
task_id, change_type, old_value, new_value, changed_at, changed_by
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
'''), (
|
||||||
task_id,
|
task_id,
|
||||||
'modify_operation',
|
'modify_operation',
|
||||||
old_operation,
|
old_operation,
|
||||||
new_operation,
|
new_operation,
|
||||||
datetime.now().isoformat(),
|
_now_str(),
|
||||||
changed_by
|
changed_by
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -360,11 +423,11 @@ class ImageTaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
SELECT * FROM task_requirement_changes
|
SELECT * FROM task_requirement_changes
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
ORDER BY changed_at DESC
|
ORDER BY changed_at DESC
|
||||||
''', (task_id,))
|
'''), (task_id,))
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -385,13 +448,13 @@ class ImageTaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
UPDATE image_tasks
|
UPDATE image_tasks
|
||||||
SET retry_count = retry_count + 1
|
SET retry_count = retry_count + 1
|
||||||
WHERE task_id = ?
|
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()
|
row = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,26 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Optional, Dict, List
|
from typing import Optional, Dict, List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class TaskStatus(Enum):
|
||||||
"""任务状态"""
|
"""任务状态"""
|
||||||
@@ -40,12 +58,45 @@ class TaskManager:
|
|||||||
|
|
||||||
def _init_db(self):
|
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)
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 创建任务表
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS tasks (
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
task_id TEXT PRIMARY KEY,
|
task_id TEXT PRIMARY KEY,
|
||||||
@@ -56,7 +107,7 @@ class TaskManager:
|
|||||||
customer_id TEXT,
|
customer_id TEXT,
|
||||||
trigger_type TEXT,
|
trigger_type TEXT,
|
||||||
trigger_keyword TEXT,
|
trigger_keyword TEXT,
|
||||||
trigger_keywords TEXT, -- JSON array
|
trigger_keywords TEXT,
|
||||||
action_type TEXT,
|
action_type TEXT,
|
||||||
action_file_url TEXT,
|
action_file_url TEXT,
|
||||||
action_message TEXT,
|
action_message TEXT,
|
||||||
@@ -70,21 +121,30 @@ class TaskManager:
|
|||||||
triggered_at TEXT,
|
triggered_at TEXT,
|
||||||
completed_at TEXT,
|
completed_at TEXT,
|
||||||
error_message 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_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_customer ON tasks(customer_id)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at)')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
logger.info("数据库表初始化完成")
|
logger.info("数据库表初始化完成")
|
||||||
|
|
||||||
def _get_conn(self):
|
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 = sqlite3.connect(self.db_path)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
return conn
|
||||||
@@ -100,7 +160,7 @@ class TaskManager:
|
|||||||
if isinstance(trigger_keywords, list):
|
if isinstance(trigger_keywords, list):
|
||||||
trigger_keywords = json.dumps(trigger_keywords)
|
trigger_keywords = json.dumps(trigger_keywords)
|
||||||
|
|
||||||
cursor.execute('''
|
insert_sql = '''
|
||||||
INSERT OR REPLACE INTO tasks (
|
INSERT OR REPLACE INTO tasks (
|
||||||
task_id, specified_customer_id, specified_customer_name,
|
task_id, specified_customer_id, specified_customer_name,
|
||||||
type, customer_name, customer_id,
|
type, customer_name, customer_id,
|
||||||
@@ -109,7 +169,19 @@ class TaskManager:
|
|||||||
priority, timeout_hours, status, retry_count, max_retry,
|
priority, timeout_hours, status, retry_count, max_retry,
|
||||||
created_at, created_by
|
created_at, created_by
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) 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('task_id'),
|
||||||
task.get('customer', {}).get('id'),
|
task.get('customer', {}).get('id'),
|
||||||
task.get('customer', {}).get('name'),
|
task.get('customer', {}).get('name'),
|
||||||
@@ -127,7 +199,7 @@ class TaskManager:
|
|||||||
task.get('status', 'pending'),
|
task.get('status', 'pending'),
|
||||||
task.get('retry_count', 0),
|
task.get('retry_count', 0),
|
||||||
task.get('max_retry', 3),
|
task.get('max_retry', 3),
|
||||||
task.get('created_at', datetime.now().isoformat()),
|
task.get('created_at', _now_str()),
|
||||||
task.get('created_by')
|
task.get('created_by')
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -147,7 +219,7 @@ class TaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
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()
|
row = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -165,32 +237,33 @@ class TaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
updates = ['status = ?']
|
placeholder = "%s" if _is_mysql() else "?"
|
||||||
|
updates = [f'status = {placeholder}']
|
||||||
params = [status.value]
|
params = [status.value]
|
||||||
|
|
||||||
if status == TaskStatus.RUNNING:
|
if status == TaskStatus.RUNNING:
|
||||||
updates.append('triggered_at = ?')
|
updates.append(f'triggered_at = {placeholder}')
|
||||||
params.append(datetime.now().isoformat())
|
params.append(_now_str())
|
||||||
|
|
||||||
if status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
|
if status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
|
||||||
updates.append('completed_at = ?')
|
updates.append(f'completed_at = {placeholder}')
|
||||||
params.append(datetime.now().isoformat())
|
params.append(_now_str())
|
||||||
|
|
||||||
if error_message:
|
if error_message:
|
||||||
updates.append('error_message = ?')
|
updates.append(f'error_message = {placeholder}')
|
||||||
params.append(error_message)
|
params.append(error_message)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
updates.append('result = ?')
|
updates.append(f'result = {placeholder}')
|
||||||
params.append(json.dumps(result))
|
params.append(json.dumps(result))
|
||||||
|
|
||||||
params.append(task_id)
|
params.append(task_id)
|
||||||
|
|
||||||
cursor.execute(f'''
|
cursor.execute(_sql(f'''
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET {', '.join(updates)}
|
SET {', '.join(updates)}
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
''', params)
|
'''), params)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -206,13 +279,13 @@ class TaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET retry_count = retry_count + 1
|
SET retry_count = retry_count + 1
|
||||||
WHERE task_id = ?
|
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()
|
row = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -231,7 +304,7 @@ class TaskManager:
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
if customer_id:
|
if customer_id:
|
||||||
cursor.execute('''
|
cursor.execute(_sql('''
|
||||||
SELECT * FROM tasks
|
SELECT * FROM tasks
|
||||||
WHERE status = 'pending'
|
WHERE status = 'pending'
|
||||||
AND customer_id = ?
|
AND customer_id = ?
|
||||||
@@ -242,7 +315,7 @@ class TaskManager:
|
|||||||
ELSE 3
|
ELSE 3
|
||||||
END,
|
END,
|
||||||
created_at
|
created_at
|
||||||
''', (customer_id,))
|
'''), (customer_id,))
|
||||||
else:
|
else:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT * FROM tasks
|
SELECT * FROM tasks
|
||||||
@@ -271,6 +344,13 @@ class TaskManager:
|
|||||||
conn = self._get_conn()
|
conn = self._get_conn()
|
||||||
cursor = conn.cursor()
|
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('''
|
cursor.execute('''
|
||||||
SELECT * FROM tasks
|
SELECT * FROM tasks
|
||||||
WHERE status = 'pending'
|
WHERE status = 'pending'
|
||||||
@@ -299,7 +379,7 @@ class TaskManager:
|
|||||||
|
|
||||||
stats = {}
|
stats = {}
|
||||||
for status in TaskStatus:
|
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]
|
stats[status.value] = cursor.fetchone()[0]
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ aiofiles>=23.0.0
|
|||||||
httpx>=0.25.0
|
httpx>=0.25.0
|
||||||
numpy>=1.24.0
|
numpy>=1.24.0
|
||||||
opencv-python>=4.8.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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
项目入口 - 启动 WebSocket 客服客户端
|
AI 客服系统 - 统一启动器
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python run.py # 单进程模式(默认)
|
python run.py # WebSocket 客服模式(默认)
|
||||||
python run.py --no-agent # 仅基础回复,不启用 AI
|
python run.py --tianwang # 完整版(HTTP API + WebSocket + AI Agent)
|
||||||
python run.py --multi # 多进程模式
|
python run.py --tianwang-multi -w 4 # 天网 + 多进程(HTTP API + 多进程 WebSocket)
|
||||||
python run.py --multi -w 4 # 多进程模式,指定 4 个进程
|
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 sys
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# 确保项目根目录在 sys.path 首位
|
|
||||||
_root = Path(__file__).resolve().parent
|
_root = Path(__file__).resolve().parent
|
||||||
if str(_root) not in sys.path:
|
if str(_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_root))
|
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):
|
DEFAULT_HTTP_PORT = 6060
|
||||||
"""单进程模式"""
|
|
||||||
from core.websocket_client import QingjianAPIClient
|
|
||||||
|
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
|
import asyncio
|
||||||
|
from core.websocket_client import QingjianAPIClient
|
||||||
|
|
||||||
print("=" * 60)
|
logger.info("=" * 60)
|
||||||
print("AI 客服系统 - 单进程模式")
|
logger.info("AI 客服系统 - WebSocket 模式")
|
||||||
print("=" * 60)
|
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
|
||||||
print(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
|
logger.info("=" * 60)
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
client = QingjianAPIClient(enable_agent=enable_agent)
|
client = QingjianAPIClient(enable_agent=enable_agent)
|
||||||
try:
|
try:
|
||||||
asyncio.run(client.run())
|
asyncio.run(client.run())
|
||||||
except KeyboardInterrupt:
|
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):
|
def run_multi_process(num_workers: int, enable_agent: bool):
|
||||||
"""多进程模式"""
|
"""多进程模式"""
|
||||||
from scripts.multi_process_launcher import Coordinator
|
from scripts.multi_process_launcher import Coordinator
|
||||||
|
|
||||||
print("=" * 60)
|
logger.info("=" * 60)
|
||||||
print("AI 客服系统 - 多进程异步并行模式")
|
logger.info("AI 客服系统 - 多进程异步并行模式")
|
||||||
print("=" * 60)
|
logger.info(f"工作进程数:{num_workers}")
|
||||||
print(f"工作进程数:{num_workers}")
|
logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
|
||||||
print(f"AI Agent: {'已启用' if enable_agent else '未启用'}")
|
logger.info("=" * 60)
|
||||||
print("=" * 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:
|
try:
|
||||||
coordinator.start()
|
coordinator.start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n已停止")
|
logger.info("已停止")
|
||||||
coordinator.stop()
|
coordinator.stop()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='AI 客服系统启动器')
|
parser = argparse.ArgumentParser(
|
||||||
|
description='AI 客服系统启动器',
|
||||||
parser.add_argument(
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
'--no-agent',
|
epilog="""
|
||||||
action='store_true',
|
启动模式:
|
||||||
help='不启用 AI Agent,仅基础回复'
|
(默认) WebSocket 客服模式
|
||||||
|
--tianwang HTTP API + WebSocket(天网完整版)
|
||||||
|
--tianwang-multi HTTP API + 多进程 WebSocket
|
||||||
|
--api-only 仅 HTTP API(不含 WebSocket / AI Agent)
|
||||||
|
--multi 多进程模式
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
mode = parser.add_mutually_exclusive_group()
|
||||||
'--multi',
|
mode.add_argument('--tianwang', action='store_true', help='天网完整版(HTTP API + WebSocket)')
|
||||||
action='store_true',
|
mode.add_argument('--tianwang-multi', action='store_true', help='天网 + 多进程(HTTP API + 多进程 WebSocket)')
|
||||||
help='多进程模式'
|
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(
|
parser.add_argument('--no-agent', action='store_true', help='不启用 AI Agent')
|
||||||
'-w', '--workers',
|
parser.add_argument('--port', '-p', type=int, default=DEFAULT_HTTP_PORT, help=f'HTTP API 端口(默认 {DEFAULT_HTTP_PORT})')
|
||||||
type=int,
|
parser.add_argument('--workers', '-w', type=int, default=None, help='工作进程数(仅多进程模式,默认 CPU 核心数)')
|
||||||
default=None,
|
|
||||||
help='工作进程数(默认:CPU 核心数,仅多进程模式有效)'
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
enable_agent = not args.no_agent
|
enable_agent = not args.no_agent
|
||||||
|
|
||||||
if args.multi:
|
if args.api_only:
|
||||||
# 多进程模式
|
run_api_only(port=args.port)
|
||||||
run_multi_process(
|
elif args.tianwang:
|
||||||
num_workers=args.workers,
|
run_tianwang(enable_agent=enable_agent, port=args.port)
|
||||||
enable_agent=enable_agent
|
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:
|
else:
|
||||||
# 单进程模式(默认)
|
run_websocket(enable_agent=enable_agent)
|
||||||
run_single_process(enable_agent=enable_agent)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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:
|
try:
|
||||||
while True:
|
while True:
|
||||||
import sqlite3
|
rows = db.get_latest_messages(20)
|
||||||
conn = sqlite3.connect(db._DB_PATH)
|
new_rows = [r for r in rows if r["id"] not in seen_ids]
|
||||||
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]
|
|
||||||
if new_rows:
|
if new_rows:
|
||||||
new_rows.reverse()
|
new_rows.reverse()
|
||||||
for r in new_rows:
|
for r in new_rows:
|
||||||
|
|||||||
@@ -24,22 +24,23 @@ logger = logging.getLogger(__name__)
|
|||||||
class WorkerProcess:
|
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.worker_id = worker_id
|
||||||
self.shard_keys = shard_keys
|
self.shard_keys = shard_keys
|
||||||
|
self.enable_agent = enable_agent
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""启动工作进程"""
|
"""启动工作进程"""
|
||||||
self.process = Process(
|
self.process = Process(
|
||||||
target=self._run,
|
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}"
|
name=f"ai-cs-worker-{self.worker_id}"
|
||||||
)
|
)
|
||||||
self.process.start()
|
self.process.start()
|
||||||
logger.info(f"Worker {self.worker_id} 启动 (PID: {self.process.pid})")
|
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:
|
try:
|
||||||
# 设置进程环境变量
|
# 设置进程环境变量
|
||||||
@@ -50,7 +51,7 @@ class WorkerProcess:
|
|||||||
from core.websocket_client import QingjianAPIClient
|
from core.websocket_client import QingjianAPIClient
|
||||||
|
|
||||||
logger.info(f"Worker {worker_id} 初始化 Agent...")
|
logger.info(f"Worker {worker_id} 初始化 Agent...")
|
||||||
client = QingjianAPIClient(enable_agent=True)
|
client = QingjianAPIClient(enable_agent=enable_agent)
|
||||||
|
|
||||||
# 只处理分配给这个 worker 的客户
|
# 只处理分配给这个 worker 的客户
|
||||||
client.shard_keys = set(shard_keys)
|
client.shard_keys = set(shard_keys)
|
||||||
@@ -77,10 +78,11 @@ class WorkerProcess:
|
|||||||
class Coordinator:
|
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.num_workers = num_workers or max(2, cpu_count())
|
||||||
self.workers: List[WorkerProcess] = []
|
self.workers: List[WorkerProcess] = []
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self.enable_agent = enable_agent
|
||||||
|
|
||||||
def _get_shard_key(self, acc_id: str, from_id: str) -> int:
|
def _get_shard_key(self, acc_id: str, from_id: str) -> int:
|
||||||
"""根据店铺 ID + 客户 ID 计算分片 key"""
|
"""根据店铺 ID + 客户 ID 计算分片 key"""
|
||||||
@@ -117,7 +119,8 @@ class Coordinator:
|
|||||||
for worker_id in range(self.num_workers):
|
for worker_id in range(self.num_workers):
|
||||||
worker = WorkerProcess(
|
worker = WorkerProcess(
|
||||||
worker_id=worker_id,
|
worker_id=worker_id,
|
||||||
shard_keys=shards.get(worker_id, [])
|
shard_keys=shards.get(worker_id, []),
|
||||||
|
enable_agent=self.enable_agent
|
||||||
)
|
)
|
||||||
worker.start()
|
worker.start()
|
||||||
self.workers.append(worker)
|
self.workers.append(worker)
|
||||||
|
|||||||
@@ -16,15 +16,12 @@ from pathlib import Path
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
from utils.service_base import BaseService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ServiceBase:
|
class GeminiExtractV2Service(BaseService):
|
||||||
"""最小化基类,替代缺失的 utils.service_base"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GeminiExtractV2Service(ServiceBase):
|
|
||||||
"""Gemini印花提取V2服务类 - 使用服务,更经济"""
|
"""Gemini印花提取V2服务类 - 使用服务,更经济"""
|
||||||
|
|
||||||
SERVICE_NAME = "gemini_extract_v2"
|
SERVICE_NAME = "gemini_extract_v2"
|
||||||
@@ -69,7 +66,7 @@ class GeminiExtractV2Service(ServiceBase):
|
|||||||
DEFAULT_PROMPT = "提取印花图案,把褶皱移除。补齐缺失的部分,要生成完整,细节丰富,严格按照原图的元素位置生成平面的印花图,不要相似的,相似度要100%,生成高质量的印刷图"
|
DEFAULT_PROMPT = "提取印花图案,把褶皱移除。补齐缺失的部分,要生成完整,细节丰富,严格按照原图的元素位置生成平面的印花图,不要相似的,相似度要100%,生成高质量的印刷图"
|
||||||
# DEFAULT_PROMPT = "生成图片,把衣服的图案展开起来做成数码印花印刷平面图。去掉皱褶,生成图案增强细节。排除衣服图案以外内容"
|
# DEFAULT_PROMPT = "生成图片,把衣服的图案展开起来做成数码印花印刷平面图。去掉皱褶,生成图案增强细节。排除衣服图案以外内容"
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__(name="gemini_extract_v2")
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
def image_to_base64(self, image_path: str) -> str:
|
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)│ │ (轻简软件) │
|
│ (公网 IP) │ │ (127.0.0.1:6060)│ │ (轻简软件) │
|
||||||
└─────────────┘ └──────────────┘ └─────────────┘
|
└─────────────┘ └──────────────┘ └─────────────┘
|
||||||
↑ │
|
↑ │
|
||||||
│ ↓
|
└─────────────────────┘
|
||||||
└──────────────┐
|
|
||||||
┌──────────────┐
|
┌──────────────┐
|
||||||
│ SQLite │
|
│ SQLite │
|
||||||
│ 任务数据库 │
|
│ 任务数据库 │
|
||||||
@@ -20,72 +39,190 @@
|
|||||||
|
|
||||||
| 组件 | 地址 | 说明 |
|
| 组件 | 地址 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **AI 客服 HTTP API** | http://127.0.0.1:6060 | 接收天网任务 |
|
| AI 客服 HTTP API | `http://127.0.0.1:6060` | 接收天网任务 |
|
||||||
| **天网服务器** | 你的公网 IP | 任务调度中心 |
|
| 天网服务器 | 公网 IP | 任务调度中心 |
|
||||||
| **轻简软件** | ws://127.0.0.1:9528 | 企业微信连接 |
|
| 轻简软件 | `ws://127.0.0.1:9528` | 企业微信连接 |
|
||||||
| **任务数据库** | SQLite | 本地存储 |
|
| 任务数据库 | SQLite 本地存储 | 任务持久化 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 快速部署
|
## 快速部署
|
||||||
|
|
||||||
### 步骤 1:环境检查
|
### 步骤 1:环境检查
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 检查 Python 版本
|
|
||||||
python3 --version # 需要 3.8+
|
python3 --version # 需要 3.8+
|
||||||
|
cd /root/ai_customer_service/ai_cs
|
||||||
# 检查依赖
|
pip3 install -r requirements.txt
|
||||||
pip3 list | grep -E "flask|pydantic|sqlite"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 步骤 2:启动 AI 客服 API
|
### 步骤 2:启动服务
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /root/ai_customer_service/ai_cs
|
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 &
|
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
|
||||||
|
|
||||||
# 查看进程
|
|
||||||
ps aux | grep run_tianwang
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
tail -f /tmp/tianwang.log
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 步骤 3:测试 API
|
### 步骤 3:验证
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 健康检查
|
|
||||||
curl http://localhost:6060/api/health
|
curl http://localhost:6060/api/health
|
||||||
|
# 预期: {"code":200,"data":{"service":"ai-cs-tianwang-bridge",...},"message":"OK"}
|
||||||
# 预期输出:
|
|
||||||
# {"code":200,"data":{"service":"ai-cs-tianwang-bridge","timestamp":"..."},"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. 接收任务
|
### 1. 接收任务
|
||||||
|
|
||||||
**接口**: `POST /api/task/receive`
|
**POST** `/api/task/receive`
|
||||||
|
|
||||||
**请求示例**:
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:6060/api/task/receive \
|
curl -X POST http://localhost:6060/api/task/receive \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"task_id": "TASK_20260227_001",
|
"task_id": "TASK_20260227_001",
|
||||||
"type": "send_file_after_reply",
|
"type": "send_file_after_reply",
|
||||||
"customer": {
|
"customer": {"id": "customer_123", "name": "小明"},
|
||||||
"id": "customer_123",
|
|
||||||
"name": "小明"
|
|
||||||
},
|
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"type": "specified_customer_reply",
|
"type": "specified_customer_reply",
|
||||||
"customer_id": "customer_123",
|
"customer_id": "customer_123",
|
||||||
@@ -93,133 +230,61 @@ curl -X POST http://localhost:6060/api/task/receive \
|
|||||||
"keyword": "好的",
|
"keyword": "好的",
|
||||||
"exact_match": false
|
"exact_match": false
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {"type": "send_message", "message": "这是您要的文件"},
|
||||||
"type": "send_message",
|
|
||||||
"message": "这是您要的文件"
|
|
||||||
},
|
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"timeout_hours": 24,
|
"timeout_hours": 24,
|
||||||
"created_by": "设计师 lz"
|
"created_by": "设计师 lz"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应示例**:
|
**响应**:
|
||||||
```json
|
```json
|
||||||
{
|
{"code": 200, "message": "任务接收成功", "data": {"task_id": "TASK_20260227_001", "status": "pending"}}
|
||||||
"code": 200,
|
|
||||||
"message": "任务接收成功",
|
|
||||||
"data": {
|
|
||||||
"task_id": "TASK_20260227_001",
|
|
||||||
"status": "pending"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 查询任务状态
|
### 2. 查询任务状态
|
||||||
|
|
||||||
**接口**: `GET /api/task/status/:task_id`
|
**GET** `/api/task/status/:task_id`
|
||||||
|
|
||||||
**请求示例**:
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:6060/api/task/status/TASK_20260227_001
|
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. 取消任务
|
### 3. 取消任务
|
||||||
|
|
||||||
**接口**: `POST /api/task/cancel`
|
**POST** `/api/task/cancel`
|
||||||
|
|
||||||
**请求示例**:
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:6060/api/task/cancel \
|
curl -X POST http://localhost:6060/api/task/cancel \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{"task_id": "TASK_20260227_001", "reason": "客户取消订单"}'
|
||||||
"task_id": "TASK_20260227_001",
|
|
||||||
"reason": "客户取消订单"
|
|
||||||
}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应示例**:
|
### 4. 任务列表
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "任务已取消",
|
|
||||||
"data": {
|
|
||||||
"task_id": "TASK_20260227_001",
|
|
||||||
"status": "cancelled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
**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
|
```bash
|
||||||
curl "http://localhost:6060/api/task/list?status=pending&page=1&page_size=10"
|
curl "http://localhost:6060/api/task/list?status=pending&page=1&page_size=10"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 健康检查
|
### 5. 健康检查
|
||||||
|
|
||||||
**接口**: `GET /api/health`
|
**GET** `/api/health`
|
||||||
|
|
||||||
**请求示例**:
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:6060/api/health
|
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(推荐)
|
### 1. specified_customer_reply(推荐)
|
||||||
|
|
||||||
**指定客户回复指定内容**
|
指定客户回复指定内容时触发。
|
||||||
|
|
||||||
```json
|
```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
|
### 2. customer_reply
|
||||||
|
|
||||||
**任意客户回复指定内容**
|
任意客户回复指定内容。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{"trigger": {"type": "customer_reply", "keyword": "好的"}}
|
||||||
"trigger": {
|
|
||||||
"type": "customer_reply",
|
|
||||||
"keyword": "好的"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. customer_keyword
|
### 3. customer_keyword
|
||||||
|
|
||||||
**任意客户说某关键词**
|
任意客户说某关键词(支持多个)。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{"trigger": {"type": "customer_keyword", "keywords": ["好的", "可以", "行"]}}
|
||||||
"trigger": {
|
|
||||||
"type": "customer_keyword",
|
|
||||||
"keywords": ["好的", "可以", "行"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. customer_payment
|
### 4. customer_payment
|
||||||
|
|
||||||
**客户付款**
|
客户付款时触发。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{"trigger": {"type": "customer_payment", "keywords": ["已付款", "拍下了"]}}
|
||||||
"trigger": {
|
|
||||||
"type": "customer_payment",
|
|
||||||
"keywords": ["已付款", "拍下了", "已下单"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. time_reach
|
### 5. time_reach
|
||||||
|
|
||||||
**到达指定时间**
|
到达指定时间触发。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{"trigger": {"type": "time_reach", "time": "2026-02-28 09:00:00"}}
|
||||||
"trigger": {
|
|
||||||
"type": "time_reach",
|
|
||||||
"time": "2026-02-27 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
|
```sql
|
||||||
CREATE TABLE tasks (
|
CREATE TABLE tasks (
|
||||||
task_id TEXT PRIMARY KEY,
|
task_id TEXT PRIMARY KEY,
|
||||||
@@ -360,281 +382,144 @@ CREATE TABLE tasks (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**任务状态流转**: `pending → waiting → running → completed / failed`
|
||||||
|
|
||||||
|
### 图片任务数据库
|
||||||
|
|
||||||
|
路径: `db/image_tasks.db`(详见 **项目功能汇总.md - 图片任务数据库**)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 使用场景
|
## 配置说明
|
||||||
|
|
||||||
### 场景 1:客户说"好的"后发送文件
|
### 环境变量
|
||||||
|
|
||||||
**天网下发任务**:
|
文件: `.env.tianwang`
|
||||||
```json
|
|
||||||
{
|
```bash
|
||||||
"task_id": "TASK_001",
|
AI_CS_HOST=127.0.0.1
|
||||||
"type": "send_file_after_reply",
|
AI_CS_PORT=6060
|
||||||
"customer": {
|
AI_CS_API_URL=http://127.0.0.1:6060
|
||||||
"id": "customer_123",
|
TIANWANG_CALLBACK_URL=http://127.0.0.1:6060/api/task/callback
|
||||||
"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"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**流程**:
|
### 天网回调配置
|
||||||
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",
|
| 6060 | HTTP API 服务器 |
|
||||||
"customer": {
|
| 9528 | 轻简软件 WebSocket(外部)|
|
||||||
"id": "customer_456",
|
|
||||||
"name": "小红"
|
**防火墙**:
|
||||||
},
|
```bash
|
||||||
"trigger": {
|
firewall-cmd --add-port=6060/tcp --permanent && firewall-cmd --reload
|
||||||
"type": "customer_payment"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"type": "send_message",
|
|
||||||
"message": "感谢购买!这是使用教程:..."
|
|
||||||
},
|
|
||||||
"created_by": "设计师 lz"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 场景 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
|
```bash
|
||||||
# 查看进程
|
ps aux | grep "run.py"
|
||||||
ps aux | grep run_tianwang
|
|
||||||
|
|
||||||
# 查看端口
|
|
||||||
netstat -tlnp | grep 6060
|
netstat -tlnp | grep 6060
|
||||||
|
systemctl status ai-cs-tianwang # systemd 方式
|
||||||
```
|
```
|
||||||
|
|
||||||
### 查看日志
|
### 查看日志
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 实时日志
|
tail -f /tmp/tianwang.log # 文件方式
|
||||||
tail -f /tmp/tianwang.log
|
journalctl -u ai-cs-tianwang -f # systemd 方式
|
||||||
|
grep "任务" /tmp/tianwang.log # 搜索任务日志
|
||||||
# 最近 100 行
|
grep "派单" /tmp/tianwang.log # 搜索派单日志
|
||||||
tail -100 /tmp/tianwang.log
|
grep "转接人工" /tmp/tianwang.log # 搜索转接日志
|
||||||
|
|
||||||
# 搜索关键词
|
|
||||||
grep "任务" /tmp/tianwang.log
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 查看数据库
|
### 查看数据库
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进入 SQLite
|
|
||||||
sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db
|
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 task_id, type, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 10;
|
||||||
|
|
||||||
# 查看待触发任务
|
|
||||||
SELECT * FROM tasks WHERE status='pending';
|
SELECT * FROM tasks WHERE status='pending';
|
||||||
|
|
||||||
# 查看失败任务
|
|
||||||
SELECT task_id, error_message FROM tasks WHERE status='failed';
|
SELECT task_id, error_message FROM tasks WHERE status='failed';
|
||||||
|
|
||||||
# 查看统计数据
|
|
||||||
SELECT status, COUNT(*) as count FROM tasks GROUP BY status;
|
SELECT status, COUNT(*) as count FROM tasks GROUP BY status;
|
||||||
|
|
||||||
# 退出
|
|
||||||
.exit
|
.exit
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ 故障排查
|
## 故障排查
|
||||||
|
|
||||||
### 问题 1:API 无法访问
|
### API 无法访问
|
||||||
|
|
||||||
**症状**: `curl http://localhost:6060/api/health` 无响应
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 检查进程
|
ps aux | grep "run.py" # 检查进程
|
||||||
ps aux | grep run_tianwang
|
netstat -tlnp | grep 6060 # 检查端口
|
||||||
|
pkill -f "run.py" # 停止
|
||||||
# 2. 检查端口
|
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
|
||||||
netstat -tlnp | grep 6060
|
tail -f /tmp/tianwang.log # 查看日志
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 任务接收失败(500 错误)
|
||||||
|
|
||||||
### 问题 2:任务接收失败
|
|
||||||
|
|
||||||
**症状**: POST /api/task/receive 返回 500 错误
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 查看日志
|
|
||||||
tail -f /tmp/tianwang.log | grep "ERROR"
|
tail -f /tmp/tianwang.log | grep "ERROR"
|
||||||
|
sqlite3 db/task_db/tasks.db ".schema tasks" # 检查数据库
|
||||||
# 2. 检查数据库
|
# 如果数据库损坏:rm db/task_db/tasks.db 然后重启(自动重建)
|
||||||
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
|
|
||||||
# 重启服务后会自动创建
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 任务未触发
|
||||||
|
|
||||||
### 问题 3:任务未触发
|
|
||||||
|
|
||||||
**症状**: 任务一直是 pending 状态
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 检查任务状态
|
curl http://localhost:6060/api/task/status/TASK_ID # 检查状态
|
||||||
curl http://localhost:6060/api/task/status/TASK_ID
|
grep "任务触发" /tmp/tianwang.log # 查看触发日志
|
||||||
|
|
||||||
# 2. 查看触发日志
|
|
||||||
grep "任务触发" /tmp/tianwang.log
|
|
||||||
|
|
||||||
# 3. 检查触发条件
|
|
||||||
# 确认客户消息包含触发关键词
|
# 确认客户消息包含触发关键词
|
||||||
|
|
||||||
# 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
|
```bash
|
||||||
# 1. 查看内存使用
|
|
||||||
ps aux | grep run_tianwang | awk '{print $6/1024 " MB"}'
|
ps aux | grep run_tianwang | awk '{print $6/1024 " MB"}'
|
||||||
|
# 建议每天定时重启
|
||||||
# 2. 定期重启(建议每天)
|
|
||||||
crontab -e
|
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` |
|
| 启动脚本 | `run.py`(通过 `--api-only` / `--tianwang` 切换模式)|
|
||||||
| **HTTP API** | `/root/ai_customer_service/ai_cs/api/http_server.py` |
|
| HTTP API | `api/http_server.py` |
|
||||||
| **任务调度** | `/root/ai_customer_service/ai_cs/core/task_scheduler.py` |
|
| 任务调度 | `core/task_scheduler.py` |
|
||||||
| **数据模型** | `/root/ai_customer_service/ai_cs/db/task_db/task_model.py` |
|
| 数据模型 | `db/task_db/task_model.py` |
|
||||||
| **配置文件** | `/root/ai_customer_service/ai_cs/.env.tianwang` |
|
| 配置文件 | `.env.tianwang` |
|
||||||
| **日志文件** | `/tmp/tianwang.log` |
|
| 日志文件 | `/tmp/tianwang.log` |
|
||||||
| **数据库** | `/root/ai_customer_service/ai_cs/db/task_db/tasks.db` |
|
| 任务数据库 | `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` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📧 发送文档
|
## 快速参考
|
||||||
|
|
||||||
如需发送此文档,可以:
|
|
||||||
|
|
||||||
### 方式 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/task/list - 任务列表 │
|
||||||
│ GET /api/health - 健康检查 │
|
│ GET /api/health - 健康检查 │
|
||||||
│ │
|
│ │
|
||||||
│ 启动:python3 run_tianwang_simple.py │
|
│ 启动:python3 run.py --api-only │
|
||||||
│ 日志:tail -f /tmp/tianwang.log │
|
│ 日志:tail -f /tmp/tianwang.log │
|
||||||
│ 数据库:sqlite3 db/task_db/tasks.db │
|
│ 数据库:sqlite3 db/task_db/tasks.db │
|
||||||
└─────────────────────────────────────────────┘
|
└─────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**文档版本**: v1.0
|
|
||||||
**更新日期**: 2026-02-27
|
|
||||||
**维护者**: AI 客服系统团队
|
|
||||||
|
|
||||||
|
|||||||
631
项目功能汇总.md
631
项目功能汇总.md
@@ -1,263 +1,447 @@
|
|||||||
# AI 客服系统 - 完整功能汇总
|
# AI 客服系统 - 完整功能汇总
|
||||||
|
|
||||||
**版本**: v1.0
|
**版本**: v1.0 | **更新日期**: 2026-02-28 | **服务器**: 1.12.50.92
|
||||||
**更新日期**: 2026-02-27
|
|
||||||
**服务器**: 1.12.50.92
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 目录
|
## 目录
|
||||||
|
|
||||||
1. [核心功能](#核心功能)
|
1. [天网协作系统](#天网协作系统)
|
||||||
2. [图片处理](#图片处理)
|
2. [三种工作流](#三种工作流)
|
||||||
3. [任务管理](#任务管理)
|
3. [文字检测与加价](#文字检测与加价)
|
||||||
4. [派单系统](#派单系统)
|
4. [风险评估与接单判断](#风险评估与接单判断)
|
||||||
5. [价格策略](#价格策略)
|
5. [作图失败转接人工](#作图失败转接人工)
|
||||||
6. [风险控制](#风险控制)
|
6. [图片任务数据库](#图片任务数据库)
|
||||||
7. [技术架构](#技术架构)
|
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`)
|
| `/api/task/receive` | POST | 接收任务 |
|
||||||
- ✅ 任务取消 (`POST /api/task/cancel`)
|
| `/api/task/status/:id` | GET | 查询任务状态 |
|
||||||
- ✅ 任务列表 (`GET /api/task/list`)
|
| `/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 元 |
|
| 少量 (1-10 字) | +5 元 |
|
||||||
| 中量 (11-50 字) | +15 元 |
|
| 中量 (11-50 字) | +15 元 |
|
||||||
| 大量 (51-200 字) | +30 元 |
|
| 大量 (51-200 字) | +30 元 |
|
||||||
| 极多 (200 字以上) | +50 元 |
|
| 极多 (200 字以上) | +50 元 |
|
||||||
|
|
||||||
**分层加价**:
|
### 文字分层需求加价
|
||||||
- 有文字 + 分层:+50 元起
|
|
||||||
- 无文字 + 分层:+30 元
|
|
||||||
|
|
||||||
**特殊价格**: 文字分层 + 大量文字 → 60-80 元
|
| 分层需求 | 加价 |
|
||||||
|
|----------|------|
|
||||||
|
| no | +0 元 |
|
||||||
|
| yes(有文字)| +50 元起 |
|
||||||
|
| yes(无文字)| +30 元 |
|
||||||
|
|
||||||
**文档**: `文字加价功能说明.md`
|
### 特殊价格
|
||||||
|
|
||||||
---
|
**条件**: 文字数量=大量/极多 且 分层需求=yes → **60-80 元**
|
||||||
|
|
||||||
### 2. 风险评估与接单判断
|
### 使用场景
|
||||||
|
|
||||||
**风险等级**:
|
**场景 1**: 少量文字,不分层
|
||||||
- **敏感内容** (一票否决): 色情/暴力/涉政 → 直接拒绝
|
- 复杂度 simple + 少量文字 → 15 + 5 = **20 元**
|
||||||
- **none**: 印花/图案/logo → 直接接单
|
- AI: "这张图比较简单,不过有少量文字需要处理,20 元。"
|
||||||
- **low**: 有人脸但清晰 → 接单,说明风险
|
|
||||||
- **high**: 严重模糊/老照片 → 谨慎接单
|
|
||||||
|
|
||||||
**可做判断**:
|
**场景 2**: 大量文字,需要分层
|
||||||
- `yes`: 效果有把握,接单
|
- 复杂度 complex + 大量文字 + 分层 → 调整到 **80 元**
|
||||||
- `partial`: 有限制,谨慎接单
|
- AI: "这张图文字比较多,有 100 多字,需要分层文件,80 元。"
|
||||||
- `no`: 无法处理,不接单
|
|
||||||
|
|
||||||
**文档**: `风险评估功能说明.md`
|
### 价格计算流程
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 作图失败转接人工
|
|
||||||
|
|
||||||
**触发条件**:
|
|
||||||
- API 调用失败
|
|
||||||
- 图片处理超时
|
|
||||||
- 客户不满意
|
|
||||||
|
|
||||||
**流程**:
|
|
||||||
```
|
```
|
||||||
作图失败
|
客户发送图片 → 判断基础复杂度 → 检测文字数量 → 询问分层需求
|
||||||
↓
|
→ 计算总价(基础+文字+分层)→ 特殊价格处理(60-80 元)→ 报价
|
||||||
通知客户
|
|
||||||
↓
|
|
||||||
转接人工客服
|
|
||||||
↓
|
|
||||||
企业微信预警
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**文档**: `作图失败转接人工说明.md`
|
### 配置位置
|
||||||
|
|
||||||
|
修改价格规则: `image/image_analyzer.py`(查找文字加价相关代码)
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 文字数量通过视觉 AI 自动识别
|
||||||
|
- 分层需求需从对话中识别
|
||||||
|
- 最终价格必须是 5 的倍数
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 任务管理
|
## 风险评估与接单判断
|
||||||
|
|
||||||
### 1. 图片任务数据库
|
AI 客服自动分析图片风险,判断是否可以接单。
|
||||||
|
|
||||||
**功能**: 任务持久化,支持客户后续增加需求
|
### 敏感内容检测(一票否决)
|
||||||
|
|
||||||
**数据库表**:
|
**敏感内容 = yes → 直接拒绝,不接单**
|
||||||
- `image_tasks` - 图片任务表
|
|
||||||
- `task_requirement_changes` - 需求变更表
|
检测内容: 色情/黄色/擦边/裸露、性暗示、涉政/政治敏感、暴力/血腥、违禁品
|
||||||
|
|
||||||
|
**话术**: "这类不做哦" / "不好意思,这个接不了"
|
||||||
|
|
||||||
|
**禁止说**: "发图来看看"、过多解释
|
||||||
|
|
||||||
|
### 风险等级
|
||||||
|
|
||||||
|
| 风险 | 是否接单 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| **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
|
```python
|
||||||
await workflow.add_customer_requirement(
|
# 创建任务
|
||||||
task_id="TASK_001",
|
workflow.create_image_task(customer_id, original_image, operation)
|
||||||
customer_id="customer_123",
|
|
||||||
requirement="需要去掉背景"
|
# 添加需求
|
||||||
|
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 元 |
|
| simple | 10-15 元 | 画面简单干净 |
|
||||||
| normal | 15-20 元 |
|
| normal | 15-20 元 | 一般复杂度 |
|
||||||
| complex | 20-25 元 |
|
| complex | 20-25 元 | 细节偏多 |
|
||||||
| hard | 25-30 元 |
|
| hard | 25-30 元 | 非常复杂 |
|
||||||
|
|
||||||
### 加价规则
|
### 加价规则
|
||||||
|
|
||||||
**文字加价**:
|
| 项目 | 条件 | 加价 |
|
||||||
- 少量:+5 元
|
|------|------|------|
|
||||||
- 中量:+15 元
|
| 文字少量 | 1-10 字 | +5 元 |
|
||||||
- 大量:+30 元
|
| 文字中量 | 11-50 字 | +15 元 |
|
||||||
- 极多:+50 元
|
| 文字大量 | 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 服务器 |
|
| 天网协作 | `api/http_server.py` | HTTP API 服务器 |
|
||||||
| **工作流程** | `core/workflow.py` | 工作流执行器 |
|
| 工作流程 | `core/workflow.py` | 工作流执行器 |
|
||||||
| **AI Agent** | `core/pydantic_ai_agent.py` | AI 对话引擎 |
|
| AI Agent | `core/pydantic_ai_agent.py` | AI 对话引擎 |
|
||||||
| **图片分析** | `image/image_analyzer.py` | 图片复杂度识别 |
|
| 图片分析 | `image/image_analyzer.py` | 图片复杂度识别 |
|
||||||
| **派单客户端** | `services/service_tuhui_dispatch.py` | 图绘派单 API |
|
| 派单客户端 | `services/service_tuhui_dispatch.py` | 图绘派单 API |
|
||||||
| **任务数据库** | `db/image_tasks_db.py` | 任务持久化 |
|
| 任务数据库 | `db/image_tasks_db.py` | 任务持久化 |
|
||||||
|
|
||||||
### 数据库
|
### 数据库
|
||||||
|
|
||||||
| 数据库 | 文件 | 说明 |
|
| 数据库 | 位置 | 说明 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| **任务数据库** | `db/image_tasks.db` | 图片任务 |
|
| 任务数据库 | `db/image_tasks.db` | 图片任务 |
|
||||||
| **客户档案** | `customer_db/customer.db` | 客户画像 |
|
| 客户档案 | `db/customer.db` | 客户画像 |
|
||||||
| **聊天记录** | `chat_log_db/chat_log.db` | 聊天历史 |
|
| 聊天记录 | `chat_log_db/chat_log.db` | 聊天历史 |
|
||||||
|
| 天网任务 | `db/task_db/tasks.db` | 天网任务调度 |
|
||||||
|
|
||||||
### API 端口
|
### API 端口
|
||||||
|
|
||||||
| 服务 | 端口 | 说明 |
|
| 服务 | 端口 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **AI 客服 API** | 6060 | 天网任务接收 |
|
| AI 客服 API | 6060 | 天网任务接收 |
|
||||||
| **派单系统** | 8005 | 设计师派单 |
|
| 派单系统 | 8005 | 设计师派单 |
|
||||||
| **图绘平台** | 8002 | 图片上传 |
|
| 图绘平台 | 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"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**完整功能已部署,所有系统运行正常!** 🎉
|
|
||||||
|
|
||||||
|
|||||||
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