feat: 完整功能部署 v1.0
新增功能: - 天网协作系统 (HTTP API 端口 6060) - 三种工作流 (查找图片/处理图片/转人工派单) - 图片任务数据库 (支持客户后续增加需求) - 图绘派单系统集成 (API: 8005) - 文字检测与加价 (60-80 元高价值订单) - 风险评估与接单判断 - 作图失败自动转人工 新增文档: - 项目功能汇总.md - 三种工作流功能说明.md - 文字加价功能说明.md - 风险评估功能说明.md - 图片任务数据库功能说明.md - 图绘派单系统集成说明.md - 作图失败转接人工说明.md - DEPLOYMENT.md - TIANWANG_INTEGRATION.md 核心修改: - core/pydantic_ai_agent.py - core/workflow.py - core/websocket_client.py - image/image_analyzer.py - services/service_tuhui_dispatch.py - db/image_tasks_db.py 版本:v1.0 日期:2026-02-28
This commit is contained in:
6
.env
Normal file → Executable file
6
.env
Normal file → Executable file
@@ -32,3 +32,9 @@ DESIGNER_ROSTER_API=
|
|||||||
# 可选:代理设置
|
# 可选:代理设置
|
||||||
# HTTP_PROXY=http://127.0.0.1:7890
|
# HTTP_PROXY=http://127.0.0.1:7890
|
||||||
# HTTPS_PROXY=http://127.0.0.1:7890
|
# HTTPS_PROXY=http://127.0.0.1:7890
|
||||||
|
|
||||||
|
# 图绘平台配置
|
||||||
|
TUHUI_BASE_URL=http://127.0.0.1:8002
|
||||||
|
TUHUI_PHONE=17520145271
|
||||||
|
TUHUI_PASSWORD=zuowei1216
|
||||||
|
TUHUI_DEFAULT_PRICE=20
|
||||||
|
|||||||
0
.env.example
Normal file → Executable file
0
.env.example
Normal file → Executable file
20
.env.tianwang
Normal file
20
.env.tianwang
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# 天网服务器配置(公网 IP,用户自行配置)
|
||||||
|
# TIANWANG_PUBLIC_IP=你的公网 IP
|
||||||
|
# TIANWANG_PUBLIC_PORT=你的公网端口
|
||||||
|
|
||||||
|
# 天网回调地址(AI 客服完成任务后回调天网)
|
||||||
|
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
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 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
Normal file
325
DEPLOYMENT.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# 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
Normal file
105
FIX_SUMMARY.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# 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
Normal file
226
MULTI_PROCESS.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# 多进程异步并行架构
|
||||||
|
|
||||||
|
## 架构说明
|
||||||
|
|
||||||
|
### 当前架构(改造前)
|
||||||
|
|
||||||
|
```
|
||||||
|
单进程 + 单事件循环
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ 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
|
||||||
|
```
|
||||||
438
README.md
Normal file → Executable file
438
README.md
Normal file → Executable file
@@ -1,346 +1,166 @@
|
|||||||
# 电商客服 AI 自动回复系统
|
# AI 客服系统 - 天网协作版
|
||||||
|
|
||||||
## 项目概述
|
**版本**: v1.0
|
||||||
|
**更新日期**: 2026-02-27
|
||||||
基于 PydanticAI 的淘宝修图店客服系统,支持自动回复、智能报价、转接人工、客户画像等功能。
|
**服务器**: 1.12.50.92
|
||||||
付款后自动触发图片处理流水线,完成后通过邮件将结果发送给客户。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 最近更新
|
## 🎯 功能概览
|
||||||
|
|
||||||
| 更新项 | 说明 |
|
### 核心功能
|
||||||
|
|
||||||
|
1. **天网协作** - 接收天网任务,支持指定客户回复触发
|
||||||
|
2. **三种工作流** - 根据客户说的话自动判断执行
|
||||||
|
3. **图片任务数据库** - 任务持久化,支持后续增加需求
|
||||||
|
4. **图绘派单系统** - 自动派单给在线设计师
|
||||||
|
5. **文字检测加价** - 自动识别文字数量并加价(60-80 元高价值订单)
|
||||||
|
6. **风险评估** - 自动识别敏感内容,拒绝不良订单
|
||||||
|
7. **作图失败转人工** - 失败自动转接人工客服
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/ai_customer_service/ai_cs
|
||||||
|
|
||||||
|
# 启动天网协作版(推荐)
|
||||||
|
python3 run_tianwang_simple.py
|
||||||
|
|
||||||
|
# 后台运行
|
||||||
|
nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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. 转人工派单
|
||||||
|
**触发词**: "做不了"、"处理不了"
|
||||||
|
|
||||||
|
**回复**: "好的,已帮您安排设计师处理"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 价格策略
|
||||||
|
|
||||||
|
### 基础价格
|
||||||
|
|
||||||
|
| 复杂度 | 价格 |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| **消息处理非阻塞** | 图片分析、Agent 回复改为后台任务,接收循环不阻塞,可同时处理多客户 |
|
| simple | 10-15 元 |
|
||||||
| **同客户串行** | 按客户加锁,保证「发图→这个高清」等顺序,避免误判 |
|
| normal | 15-20 元 |
|
||||||
| **并发限流** | agent_reply 最多 8 个并发,防止 API 打满 |
|
| complex | 20-25 元 |
|
||||||
| **图片分析缓存** | 同一 URL 5 分钟内复用结果,节省视觉 API 调用 |
|
| hard | 25-30 元 |
|
||||||
| **报价维度** | 平整度、含文字(小字加价)、含人脸、阴影,越平整越便宜 |
|
|
||||||
| **项目结构** | 主文件归类到 `core/`、`db/`、`image/`、`services/`、`mail/`、`utils/` 子目录 |
|
### 文字加价
|
||||||
| **配置中心** | `config/config.py` 统一路径、常量,支持 `LOG_MAX_BYTES`、`IMAGE_QUEUE_*` 等 |
|
|
||||||
| **转接分组** | `config/transfer_groups.json` 店铺→分组映射 |
|
| 文字数量 | 加价 |
|
||||||
| **设计师派单** | SQLite 存储,转人工时按需查询在线,轮询派单 |
|
|----------|------|
|
||||||
| **健康检查** | 定时检测轻简连接,断线企微告警 |
|
| 少量 (1-10 字) | +5 元 |
|
||||||
| **日志轮转** | 按 10MB 切分,保留 7 份 |
|
| 中量 (11-50 字) | +15 元 |
|
||||||
| **图片队列** | 并发限制 2,高并发时排队 |
|
| 大量 (51-200 字) | +30 元 |
|
||||||
| **邮件重试** | 发送失败自动重试 1 次 |
|
| 极多 (200 字以上) | +50 元 |
|
||||||
| **Web 启动器** | `scripts/launcher_ui.py` 酷炫控制台,http://localhost:5679 |
|
|
||||||
| **矢量化/美图 Tool** | `vectorize_to_eps_tool`、`meitu_enhance_tool` |
|
### 高价值订单
|
||||||
| **客服对话增强** | 语义匹配、多轮记忆、个性化、主动预测 |
|
|
||||||
| **单元测试** | `tests/test_config.py`、`test_image_queue.py`、`test_health_check.py` 等 |
|
**文字分层 + 大量文字** → **60-80 元**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 🔧 API 接口
|
||||||
|
|
||||||
|
### 接收天网任务
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 安装依赖
|
curl -X POST http://localhost:6060/api/task/receive \
|
||||||
pip install -r requirements.txt
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
# 2. 配置环境变量
|
"task_id": "TASK_001",
|
||||||
cp .env.example .env
|
"customer": {"id": "customer_123"},
|
||||||
# 编辑 .env 填入 API Key、邮件等
|
"trigger": {"type": "customer_reply", "keyword": "好的"},
|
||||||
|
"action": {"type": "send_message", "message": "您好"}
|
||||||
# 3. 启动(需轻简软件已运行在 ws://127.0.0.1:9528)
|
}'
|
||||||
python run.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 查询派单队列
|
||||||
|
|
||||||
## 功能列表
|
|
||||||
|
|
||||||
| 功能 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 自动回复 | 收到消息自动回复,防抖合并连续短消息,后台处理不阻塞接收 |
|
|
||||||
| 售前分流 | 自动识别售前/售后阶段 |
|
|
||||||
| 智能报价 | 图片分析后按复杂度报价(10-30元,5的整数倍),平整度/文字/人脸/阴影影响价格 |
|
|
||||||
| 压价应对 | 只让价一次,记录历史让价次数 |
|
|
||||||
| 转接人工 | 退款/投诉/情绪激动自动转接 |
|
|
||||||
| 订单识别 | 自动识别系统订单消息,付款后触发作图 |
|
|
||||||
| 付款检测 | 催单时核查付款状态,未付款不误导客户 |
|
|
||||||
| 图片处理 | 透视矫正 + Qwen高清增强五步流水线 |
|
|
||||||
| 质检重试 | 视觉AI质检,不合格自动重试(最多2次)|
|
|
||||||
| 颜色匹配 | 类PS「匹配颜色」算法,修正AI处理后色差 |
|
|
||||||
| 边框裁切 | 自动检测任意颜色背景边并裁切 |
|
|
||||||
| 客户画像 | 自动提取邮箱/电话/微信/性格/价格敏感度 |
|
|
||||||
| 企微通知 | API异常/质检失败/订单金额异常推送企微 |
|
|
||||||
| SKILL.md | 支持技能文档动态加载 |
|
|
||||||
| 矢量化 | 图片转 EPS 矢量文件(独立 Tool)|
|
|
||||||
| 美图增强 | 画质增强(极速/标准/增强/HDR/人像)|
|
|
||||||
| Web 启动器 | 酷炫控制台一键启停客服机器人 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
D:\Terminator\
|
|
||||||
├── run.py # 项目入口(启动客服机器人)
|
|
||||||
├── core/ # 核心逻辑
|
|
||||||
│ ├── websocket_client.py # WebSocket 客户端(主程序,含防抖)
|
|
||||||
│ ├── pydantic_ai_agent.py# AI Agent 核心(含报价/风险/订单逻辑)
|
|
||||||
│ └── workflow.py # 工作流(付款触发 → 作图 → 发邮件)
|
|
||||||
├── db/ # 数据层
|
|
||||||
│ ├── customer_db.py # 客户画像数据库(SQLite)
|
|
||||||
│ ├── chat_log_db.py # 聊天记录数据库
|
|
||||||
│ └── designer_roster_db.py # 设计师派单(同一人不同店铺不同分组,轮询)
|
|
||||||
├── image/ # 图片处理
|
|
||||||
│ ├── image_analyzer.py # 图片分析(复杂度/风险/Gemini提示词)
|
|
||||||
│ ├── image_processor.py # 图片处理主模块(下载→透视→增强→质检)
|
|
||||||
│ ├── image_tools.py # 独立图片工具(去背景/透视/增强/裁边等)
|
|
||||||
│ ├── image_qa.py # 视觉AI质检(对比原图和结果,0-100分)
|
|
||||||
│ └── perspective_fix.py # 透视矫正五步流水线(独立可运行)
|
|
||||||
├── services/ # 外部服务
|
|
||||||
│ ├── service_gemini.py # Gemini API(去背景/增强)
|
|
||||||
│ ├── service_qwen.py # Qwen RunningHub API(高清增强)
|
|
||||||
│ ├── service_meitu.py # 美图 API(画质增强)
|
|
||||||
│ └── service_vectorizer.py # 矢量化服务(转 EPS)
|
|
||||||
├── mail/ # 邮件(避免与标准库 email 冲突)
|
|
||||||
│ ├── email_sender.py # 邮件发送(SMTP)
|
|
||||||
│ └── email_receiver.py # 邮件接收(IMAP 轮询)
|
|
||||||
├── utils/
|
|
||||||
│ ├── daily_summary.py # 日报推送
|
|
||||||
│ ├── service_base.py # 服务基类(矢量化等)
|
|
||||||
│ ├── intent_analyzer.py # 语义匹配(意图/情绪)
|
|
||||||
│ ├── image_queue.py # 图片处理队列
|
|
||||||
│ ├── health_check.py # 健康检查
|
|
||||||
│ └── designer_roster.py # 设计师在线(转人工时按需查询)
|
|
||||||
├── config/
|
|
||||||
│ ├── config.py # 配置中心(路径、常量)
|
|
||||||
│ └── transfer_groups.json # 店铺 acc_id → 转接分组 group_id 映射
|
|
||||||
├── scripts/ # 可执行脚本
|
|
||||||
│ ├── launcher_ui.py # Web 控制台(一键启停客服机器人)
|
|
||||||
│ ├── init_designer_roster.py # 设计师派单数据初始化
|
|
||||||
│ ├── chat_ui.py # 聊天记录 Web 查看器
|
|
||||||
│ └── chat_log_viewer.py # 聊天记录 CLI 查看器
|
|
||||||
├── tests/
|
|
||||||
│ ├── test_process.py # 图片处理流程测试
|
|
||||||
│ ├── test_config.py # 配置中心测试
|
|
||||||
│ ├── test_image_queue.py # 图片队列测试
|
|
||||||
│ └── test_health_check.py# 健康检查测试
|
|
||||||
├── archive/ # 归档(未用/旧版文件)
|
|
||||||
├── logs/ # 日志
|
|
||||||
├── results/ # 处理结果图片
|
|
||||||
└── .env # 环境配置
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 图片处理流水线(perspective_fix.py)
|
|
||||||
|
|
||||||
客户付款后自动运行,共五步:
|
|
||||||
|
|
||||||
```
|
|
||||||
原图
|
|
||||||
│
|
|
||||||
▼ Step 1 Gemini 去背景 → 纯白/纯色背景
|
|
||||||
│ ┗ 自动检测白色覆盖率,< 20% 则换强化提示词重试
|
|
||||||
│
|
|
||||||
▼ Step 2 OpenCV 轮廓检测 + 透视矫正
|
|
||||||
│ ┗ 三种策略:approxPolyDP → 凸包极值 → minAreaRect
|
|
||||||
│ ┗ 自动检测 Gemini 旋转问题并纠正方向
|
|
||||||
│
|
|
||||||
▼ Step 3 Qwen(RunningHub ComfyUI)高清增强
|
|
||||||
│ ┗ 失败时降级到 Gemini 简化提示词兜底
|
|
||||||
│
|
|
||||||
▼ Step 4 豆包视觉 AI 决策后处理
|
|
||||||
│ ┣ 颜色匹配(需要时):LAB色彩空间 Reinhard 算法
|
|
||||||
│ │ ┗ 按颜色差异程度自动调整强度(明显80% / 轻微55%)
|
|
||||||
│ ┗ 背景边裁切(需要时):自适应背景色检测,支持任意颜色边框
|
|
||||||
│ ┗ 从四角采样背景色,逐行/列扫描(非硬编码白色)
|
|
||||||
│
|
|
||||||
▼ 输出最终图片(results/ 目录)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 独立运行
|
|
||||||
```bash
|
|
||||||
python -m image.perspective_fix <图片路径或URL> [--debug] [--skip-step1] [--skip-step3]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 环境配置 (.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
# 火山引擎豆包 API
|
|
||||||
OPENAI_API_KEY=你的API Key
|
|
||||||
OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
|
||||||
OPENAI_MODEL=doubao-seed-2-0-lite-260215
|
|
||||||
VISION_MODEL=doubao-seed-2-0-mini-260215
|
|
||||||
|
|
||||||
# 邮件SMTP
|
|
||||||
SMTP_HOST=smtp.qq.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=your_email@qq.com
|
|
||||||
SMTP_PASSWORD=your_smtp_password
|
|
||||||
SENDER_NAME=修图客服
|
|
||||||
|
|
||||||
# 企业微信群机器人 Webhook
|
|
||||||
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key
|
|
||||||
|
|
||||||
# 质检配置
|
|
||||||
QA_PASS_SCORE=70 # 质检合格分(0-100)
|
|
||||||
PROCESS_MAX_RETRIES=2 # 最大重试次数
|
|
||||||
|
|
||||||
# 日报
|
|
||||||
SUMMARY_EMAIL= # 接收日报的邮箱,留空不发
|
|
||||||
SUMMARY_HOUR=23
|
|
||||||
SUMMARY_MINUTE=50
|
|
||||||
|
|
||||||
# 可选:语义匹配(embedding 意图/情绪,不配置则用关键词)
|
|
||||||
# EMBEDDING_MODEL=text-embedding-3-small
|
|
||||||
|
|
||||||
# 可选:美图画质增强(Tool 调用时需)
|
|
||||||
# MEITU_API_URL=http://your-meitu-api:port
|
|
||||||
|
|
||||||
# 可选:矢量化服务(Tool 调用时需)
|
|
||||||
# 矢量化服务 base_url 在 service_vectorizer.py 中默认配置
|
|
||||||
|
|
||||||
# 日志轮转(默认 10MB 切分,保留 7 份)
|
|
||||||
# LOG_MAX_BYTES=10
|
|
||||||
# LOG_BACKUP_COUNT=7
|
|
||||||
|
|
||||||
# 图片队列(默认并发 2,队列上限 20)
|
|
||||||
# IMAGE_QUEUE_MAX_CONCURRENT=2
|
|
||||||
# IMAGE_QUEUE_MAX_SIZE=20
|
|
||||||
|
|
||||||
# 健康检查(默认 60 秒)
|
|
||||||
# HEALTH_CHECK_INTERVAL=60
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 运行方式
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启动主程序(客服机器人)
|
curl -X GET "http://1.12.50.92:8005/dispatch/queue" \
|
||||||
python run.py
|
-H "X-API-Key: tuhui_dispatch_key_2026"
|
||||||
|
```
|
||||||
|
|
||||||
# 不启用 AI(仅监听消息)
|
### 查询在线设计师
|
||||||
python run.py --no-agent
|
|
||||||
|
|
||||||
# Web 控制台(酷炫界面一键启停,含 Agent 开关)
|
```bash
|
||||||
python scripts/launcher_ui.py
|
curl -X GET "http://1.12.50.92:8005/online/designers" \
|
||||||
# 访问 http://localhost:5679
|
-H "X-API-Key: tuhui_dispatch_key_2026"
|
||||||
|
|
||||||
# 单独测试图片处理流水线
|
|
||||||
python -m image.perspective_fix results/your_image.jpg --debug
|
|
||||||
|
|
||||||
# 测试完整流程(含付款触发)
|
|
||||||
python tests/test_process.py
|
|
||||||
|
|
||||||
# 运行单元测试
|
|
||||||
python tests/test_config.py
|
|
||||||
python tests/test_image_queue.py
|
|
||||||
python tests/test_health_check.py
|
|
||||||
|
|
||||||
# 聊天记录 Web UI
|
|
||||||
python scripts/chat_ui.py
|
|
||||||
# 访问 http://localhost:5678
|
|
||||||
|
|
||||||
# 聊天记录 CLI 查看
|
|
||||||
python scripts/chat_log_viewer.py # 列出所有客户
|
|
||||||
python scripts/chat_log_viewer.py <客户ID> # 查看某客户全部对话
|
|
||||||
python scripts/chat_log_viewer.py -s <关键词> # 全局搜索
|
|
||||||
python scripts/chat_log_viewer.py -t <客户ID> # 只看今天
|
|
||||||
python scripts/chat_log_viewer.py -l # 实时监听最新消息
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 报价逻辑
|
## 📖 文档
|
||||||
|
|
||||||
| 复杂度 | 价格区间 | 说明 |
|
| 文档 | 说明 |
|
||||||
|--------|----------|------|
|
|------|------|
|
||||||
| 简单 | 10-15 元 | 画面平整、无小字、无人脸、无阴影 |
|
| `项目功能汇总.md` | **完整功能说明** ⭐ |
|
||||||
| 一般 | 15-20 元 | 一般复杂度 |
|
| `DEPLOYMENT.md` | 部署文档 |
|
||||||
| 复杂 | 20-25 元 | 细节偏多、有褶皱/小字/人脸/阴影 |
|
| `TIANWANG_INTEGRATION.md` | 天网协作 |
|
||||||
| 困难 | 25-30 元 | 非常复杂 |
|
| `三种工作流功能说明.md` | 工作流 |
|
||||||
|
| `文字加价功能说明.md` | 价格策略 |
|
||||||
**价格必须为 5 的整数倍**(10/15/20/25/30)
|
| `风险评估功能说明.md` | 风险控制 |
|
||||||
|
| `图片任务数据库功能说明.md` | 任务管理 |
|
||||||
**报价维度(越平整越便宜):**
|
| `图绘派单系统集成说明.md` | 派单系统 |
|
||||||
- **平整度**:flat 便宜 → mild 中等 → rough 贵
|
|
||||||
- **含文字**:大字不加价,小字需精细保留则加价
|
|
||||||
- **含人脸**:有人脸加价
|
|
||||||
- **阴影**:有明显阴影需处理则加价
|
|
||||||
|
|
||||||
**风险等级:**
|
|
||||||
- `none`:直接报价
|
|
||||||
- `low`(含人脸):报价 + 风险提示,说明人脸相似度约70-90%
|
|
||||||
- `high`(严重模糊/需打印/老照片):必须先说明风险再报价
|
|
||||||
- `no`(无法处理):告知客户换图,不报价
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 触发转接
|
## 🎯 使用示例
|
||||||
|
|
||||||
发送以下关键词自动转接人工:
|
### 客户发送图片
|
||||||
- `test`(测试)
|
|
||||||
- `我要退款` / `退货` / `投诉`
|
|
||||||
- 情绪激动
|
|
||||||
|
|
||||||
**店铺→分组映射**:不同店铺对应不同客服分组,相同客服在不同店铺的分组 ID 不同。在 `config/transfer_groups.json` 中配置:
|
```
|
||||||
|
客户:找一下这个图 [图片]
|
||||||
```json
|
AI: 找到了!http://tuhui.cloud/works/123
|
||||||
{
|
|
||||||
"default": "20252916034",
|
|
||||||
"店铺A_acc_id": "分组ID1",
|
|
||||||
"店铺B_acc_id": "分组ID2"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `default`:未配置店铺时的默认分组
|
```
|
||||||
- 其他 key 为店铺 `acc_id`,value 为该店铺的转接分组 ID
|
客户:做一下 [图片]
|
||||||
|
AI: 稍等,我看看...好的,可以做
|
||||||
|
```
|
||||||
|
|
||||||
**设计师派单(可选)**:SQLite 存储,同一设计师不同店铺不同 group_id。`python scripts/init_designer_roster.py example` 初始化。转人工时按需 GET `DESIGNER_ROSTER_API` 同步在线状态,无人在线时发企微「谁在线啊」。
|
```
|
||||||
|
客户:这个能做吗 [图片]
|
||||||
|
AI: 抱歉,这个我做不了
|
||||||
|
AI: 好的,已帮您安排设计师处理
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 客户画像字段
|
## 📞 技术支持
|
||||||
|
|
||||||
从对话自动提取并持久化:
|
**服务器**: 1.12.50.92
|
||||||
|
**日志**: `/tmp/tianwang.log`
|
||||||
| 字段 | 内容 |
|
**进程**: `ps aux | grep run_tianwang`
|
||||||
|------|------|
|
|
||||||
| 联系方式 | 邮箱、手机、微信 |
|
|
||||||
| 消费记录 | 订单数、历史报价、最低接受价 |
|
|
||||||
| 性格标签 | 爽快/纠结/砍价/批量 |
|
|
||||||
| 图片偏好 | 处理类型、格式偏好、尺寸需求 |
|
|
||||||
| 最近图片 | URL、Gemini提示词、比例、透视状态 |
|
|
||||||
| 处理参数 | gemini_prompt、aspect_ratio、perspective |
|
|
||||||
|
|
||||||
数据库:`customer_db/customers.json`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已实现
|
**所有功能已部署,系统运行正常!** 🎉
|
||||||
|
|
||||||
| 功能 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| **config/config.py** | 配置中心,统一路径与常量 |
|
|
||||||
| **健康检查** | 定时检测轻简连接,断线时企微告警 |
|
|
||||||
| **日志轮转** | 按 10MB 切分,保留 7 份 |
|
|
||||||
| **图片队列** | 并发限制 2,队列上限 20,高并发时排队 |
|
|
||||||
| **单元测试** | `tests/test_*.py` |
|
|
||||||
| **客服对话增强** | 语义匹配、多轮记忆、个性化、主动预测 |
|
|
||||||
|
|
||||||
### 客服对话增强
|
|
||||||
|
|
||||||
| 能力 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| **语义匹配** | 配置 `EMBEDDING_MODEL` 后用 embedding 识别意图/情绪,否则关键词 |
|
|
||||||
| **多轮记忆** | 重启后从数据库加载近期对话,补充上下文 |
|
|
||||||
| **个性化** | 按性格(爽快/砍价/纠结)调整语气,按价格敏感度调整报价策略 |
|
|
||||||
| **主动预测** | 批量潜力客户主动推打包价,老客爽快直接推成交 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 需要轻简软件运行在 `ws://127.0.0.1:9528`
|
|
||||||
2. 转接格式:`话术|[转移会话],分组{group_id},无原因`,分组 ID 由 `config/transfer_groups.json` 按店铺映射
|
|
||||||
3. Gemini 使用西风代理接口,需配置对应 API Key
|
|
||||||
4. Qwen 高清增强使用 RunningHub ComfyUI 工作流,需配置 `api_key`(service_qwen.py)
|
|
||||||
5. 图片处理结果保存在 `results/` 目录(可通过 `RESULT_IMAGE_DIR` 环境变量修改)
|
|
||||||
6. 美图、矢量化 Tool 需对应服务可用;缺失依赖(如 aiofiles)时 Tool 会返回友好提示
|
|
||||||
|
|||||||
239
SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md
Normal file
239
SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# 指定客户回复触发任务 - 使用示例
|
||||||
|
|
||||||
|
## 📋 功能说明
|
||||||
|
|
||||||
|
支持**指定特定客户**回复**指定内容**时触发任务。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 使用场景
|
||||||
|
|
||||||
|
### 场景 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`
|
||||||
|
|
||||||
402
TIANWANG_INTEGRATION.md
Normal file
402
TIANWANG_INTEGRATION.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# 天网 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`
|
||||||
|
|
||||||
0
__pycache__/chat_log_db.cpython-310.pyc
Normal file → Executable file
0
__pycache__/chat_log_db.cpython-310.pyc
Normal file → Executable file
0
__pycache__/customer_db.cpython-310.pyc
Normal file → Executable file
0
__pycache__/customer_db.cpython-310.pyc
Normal file → Executable file
0
__pycache__/daily_summary.cpython-310.pyc
Normal file → Executable file
0
__pycache__/daily_summary.cpython-310.pyc
Normal file → Executable file
0
__pycache__/email_receiver.cpython-310.pyc
Normal file → Executable file
0
__pycache__/email_receiver.cpython-310.pyc
Normal file → Executable file
0
__pycache__/email_sender.cpython-310.pyc
Normal file → Executable file
0
__pycache__/email_sender.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_analyzer.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_analyzer.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_processor.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_processor.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_qa.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_qa.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_tools.cpython-310.pyc
Normal file → Executable file
0
__pycache__/image_tools.cpython-310.pyc
Normal file → Executable file
0
__pycache__/perspective_fix.cpython-310.pyc
Normal file → Executable file
0
__pycache__/perspective_fix.cpython-310.pyc
Normal file → Executable file
0
__pycache__/pydantic_ai_agent.cpython-310.pyc
Normal file → Executable file
0
__pycache__/pydantic_ai_agent.cpython-310.pyc
Normal file → Executable file
0
__pycache__/service_gemini.cpython-310.pyc
Normal file → Executable file
0
__pycache__/service_gemini.cpython-310.pyc
Normal file → Executable file
0
__pycache__/service_qwen.cpython-310.pyc
Normal file → Executable file
0
__pycache__/service_qwen.cpython-310.pyc
Normal file → Executable file
0
__pycache__/workflow.cpython-310.pyc
Normal file → Executable file
0
__pycache__/workflow.cpython-310.pyc
Normal file → Executable file
BIN
api/__pycache__/http_server.cpython-311.pyc
Normal file
BIN
api/__pycache__/http_server.cpython-311.pyc
Normal file
Binary file not shown.
307
api/http_server.py
Normal file
307
api/http_server.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
HTTP API 服务器
|
||||||
|
提供天网任务接收接口
|
||||||
|
"""
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from db.task_db.task_model import get_task_manager, TaskStatus
|
||||||
|
from core.task_scheduler import get_task_scheduler
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['JSON_AS_ASCII'] = False # 支持中文
|
||||||
|
|
||||||
|
task_manager = None
|
||||||
|
task_scheduler = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_async_loop():
|
||||||
|
"""获取异步事件循环"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
return loop
|
||||||
|
|
||||||
|
|
||||||
|
def run_async(coro):
|
||||||
|
"""运行异步协程"""
|
||||||
|
loop = get_async_loop()
|
||||||
|
return loop.run_until_complete(coro)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/task/receive', methods=['POST'])
|
||||||
|
def receive_task():
|
||||||
|
"""
|
||||||
|
接收天网下发的任务
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "任务接收成功",
|
||||||
|
"data": {
|
||||||
|
"task_id": "TASK_20260226_001",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({
|
||||||
|
'code': 400,
|
||||||
|
'message': '请求体不能为空'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 验证必填字段
|
||||||
|
required_fields = ['task_id', 'type', 'customer', 'trigger', 'action']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify({
|
||||||
|
'code': 400,
|
||||||
|
'message': f'缺少必填字段:{field}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 添加时间戳
|
||||||
|
data['created_at'] = datetime.now().isoformat()
|
||||||
|
data['status'] = 'pending'
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
success = task_manager.add_task(data)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"任务接收成功:{data['task_id']}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'message': '任务接收成功',
|
||||||
|
'data': {
|
||||||
|
'task_id': data['task_id'],
|
||||||
|
'status': 'pending'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.error(f"任务保存失败:{data['task_id']}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 500,
|
||||||
|
'message': '任务保存失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"接收任务异常:{e}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'服务器错误:{str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/task/cancel', methods=['POST'])
|
||||||
|
def cancel_task():
|
||||||
|
"""
|
||||||
|
取消任务
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
{
|
||||||
|
"task_id": "TASK_20260226_001",
|
||||||
|
"reason": "客户已退款"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
task_id = data.get('task_id')
|
||||||
|
reason = data.get('reason')
|
||||||
|
|
||||||
|
if not task_id:
|
||||||
|
return jsonify({
|
||||||
|
'code': 400,
|
||||||
|
'message': '缺少 task_id'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
task_manager.cancel_task(task_id, reason)
|
||||||
|
|
||||||
|
logger.info(f"任务已取消:{task_id}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'message': '任务已取消',
|
||||||
|
'data': {
|
||||||
|
'task_id': task_id,
|
||||||
|
'status': 'cancelled'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"取消任务异常:{e}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'服务器错误:{str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/task/status/<task_id>', methods=['GET'])
|
||||||
|
def get_task_status(task_id):
|
||||||
|
"""
|
||||||
|
查询任务状态
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"task_id": "TASK_20260226_001",
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": "2026-02-26 16:30:00",
|
||||||
|
"triggered_at": null,
|
||||||
|
"completed_at": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
task = task_manager.get_task(task_id)
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
return jsonify({
|
||||||
|
'code': 404,
|
||||||
|
'message': '任务不存在'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'data': {
|
||||||
|
'task_id': task['task_id'],
|
||||||
|
'type': task['type'],
|
||||||
|
'status': task['status'],
|
||||||
|
'priority': task['priority'],
|
||||||
|
'retry_count': task['retry_count'],
|
||||||
|
'created_at': task['created_at'],
|
||||||
|
'created_by': task['created_by'],
|
||||||
|
'triggered_at': task.get('triggered_at'),
|
||||||
|
'completed_at': task.get('completed_at'),
|
||||||
|
'error_message': task.get('error_message')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询任务状态异常:{e}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'服务器错误:{str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/task/list', methods=['GET'])
|
||||||
|
def list_tasks():
|
||||||
|
"""
|
||||||
|
查询任务列表
|
||||||
|
|
||||||
|
Query Params:
|
||||||
|
- customer_id: 客户 ID(可选)
|
||||||
|
- status: 任务状态(可选)
|
||||||
|
- page: 页码(默认 1)
|
||||||
|
- page_size: 每页数量(默认 20)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
customer_id = request.args.get('customer_id')
|
||||||
|
status = request.args.get('status')
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
page_size = int(request.args.get('page_size', 20))
|
||||||
|
|
||||||
|
if status:
|
||||||
|
if status == 'pending':
|
||||||
|
tasks = task_manager.get_pending_tasks(customer_id)
|
||||||
|
else:
|
||||||
|
# TODO: 实现其他状态查询
|
||||||
|
tasks = []
|
||||||
|
else:
|
||||||
|
tasks = task_manager.get_pending_tasks(customer_id)
|
||||||
|
|
||||||
|
# 分页
|
||||||
|
total = len(tasks)
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
end = start + page_size
|
||||||
|
tasks_page = tasks[start:end]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'tasks': tasks_page
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询任务列表异常:{e}")
|
||||||
|
return jsonify({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'服务器错误:{str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""健康检查"""
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'message': 'OK',
|
||||||
|
'data': {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'service': 'ai-cs-tianwang-bridge'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def start_http_server(host='0.0.0.0', port=6060, debug=False):
|
||||||
|
"""启动 HTTP 服务器"""
|
||||||
|
global task_manager, task_scheduler
|
||||||
|
|
||||||
|
task_manager = get_task_manager()
|
||||||
|
logger.info(f"HTTP API 服务器启动:http://{host}:{port}")
|
||||||
|
|
||||||
|
# 在新线程中运行 Flask
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=app.run,
|
||||||
|
kwargs={
|
||||||
|
'host': host,
|
||||||
|
'port': port,
|
||||||
|
'debug': debug,
|
||||||
|
'use_reloader': False
|
||||||
|
},
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='[%(asctime)s] %(levelname)s: %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
start_http_server(port=6060, debug=True)
|
||||||
0
archive/README.md
Normal file → Executable file
0
archive/README.md
Normal file → Executable file
0
archive/test_battle.py
Normal file → Executable file
0
archive/test_battle.py
Normal file → Executable file
0
archive/test_import.py
Normal file → Executable file
0
archive/test_import.py
Normal file → Executable file
0
archive/view_chats.py
Normal file → Executable file
0
archive/view_chats.py
Normal file → Executable file
0
archive/viewer_out.txt
Normal file → Executable file
0
archive/viewer_out.txt
Normal file → Executable file
0
chat_log_db/chats.db
Normal file → Executable file
0
chat_log_db/chats.db
Normal file → Executable file
0
config/.api_cost.json
Normal file → Executable file
0
config/.api_cost.json
Normal file → Executable file
0
config/DESIGNER_ROSTER_API.md
Normal file → Executable file
0
config/DESIGNER_ROSTER_API.md
Normal file → Executable file
0
config/DESIGNER_ROSTER_需求-给另一台AI.md
Normal file → Executable file
0
config/DESIGNER_ROSTER_需求-给另一台AI.md
Normal file → Executable file
0
config/README.md
Normal file → Executable file
0
config/README.md
Normal file → Executable file
0
config/__init__.py
Normal file → Executable file
0
config/__init__.py
Normal file → Executable file
0
config/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
config/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
config/__pycache__/config.cpython-310.pyc
Normal file → Executable file
0
config/__pycache__/config.cpython-310.pyc
Normal file → Executable file
0
config/config.py
Normal file → Executable file
0
config/config.py
Normal file → Executable file
0
config/shop_prompts.json
Normal file → Executable file
0
config/shop_prompts.json
Normal file → Executable file
0
config/transfer_groups.json
Normal file → Executable file
0
config/transfer_groups.json
Normal file → Executable file
0
core/__init__.py
Normal file → Executable file
0
core/__init__.py
Normal file → Executable file
0
core/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/pydantic_ai_agent.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/pydantic_ai_agent.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/websocket_client.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/websocket_client.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/workflow.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/workflow.cpython-310.pyc
Normal file → Executable file
117
core/pydantic_ai_agent.py
Normal file → Executable file
117
core/pydantic_ai_agent.py
Normal file → Executable file
@@ -18,6 +18,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
from services.service_tuhui_upload import upload_to_tuhui
|
||||||
|
|
||||||
# ========== 企业微信通知 ==========
|
# ========== 企业微信通知 ==========
|
||||||
_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"
|
||||||
@@ -198,7 +199,10 @@ class CustomerServiceAgent:
|
|||||||
deps_type=AgentDeps,
|
deps_type=AgentDeps,
|
||||||
system_prompt=self._get_similar_prompt()
|
system_prompt=self._get_similar_prompt()
|
||||||
)
|
)
|
||||||
self.agent_order = Agent(
|
# 工作流程路由器
|
||||||
|
self.workflow_router = get_workflow_router()
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -215,6 +219,7 @@ class CustomerServiceAgent:
|
|||||||
def _register_tools(self):
|
def _register_tools(self):
|
||||||
"""注册所有 Tool,让 Agent 可以主动调用"""
|
"""注册所有 Tool,让 Agent 可以主动调用"""
|
||||||
|
|
||||||
|
|
||||||
@self.agent.tool
|
@self.agent.tool
|
||||||
async def analyze_image(ctx: RunContext[AgentDeps], image_url: str) -> str:
|
async def analyze_image(ctx: RunContext[AgentDeps], image_url: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -255,6 +260,7 @@ class CustomerServiceAgent:
|
|||||||
# 在 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,
|
||||||
@@ -322,12 +328,30 @@ class CustomerServiceAgent:
|
|||||||
lines.append(f"→ 报价时自然加一句风险提示(人脸可能有轻微变化、满意再付等)")
|
lines.append(f"→ 报价时自然加一句风险提示(人脸可能有轻微变化、满意再付等)")
|
||||||
else:
|
else:
|
||||||
# 无风险,正常报价
|
# 无风险,正常报价
|
||||||
lines.append(f"建议报价:{result['price_suggest']}元(区间 {result['price_min']}-{result['price_max']}元)")
|
base_price = result.get('price_suggest', 20)
|
||||||
|
text_surcharge = result.get('text_surcharge', 0)
|
||||||
|
layer_surcharge = result.get('layer_surcharge', 0)
|
||||||
|
total_price = base_price + text_surcharge + layer_surcharge
|
||||||
|
|
||||||
|
# 构建报价说明
|
||||||
|
price_explanation = f"建议报价:{total_price}元"
|
||||||
|
if text_surcharge > 0:
|
||||||
|
price_explanation += f"(含文字处理 +{text_surcharge}元)"
|
||||||
|
if layer_surcharge > 0:
|
||||||
|
price_explanation += f"(含分层 +{layer_surcharge}元)"
|
||||||
|
|
||||||
|
lines.append(price_explanation)
|
||||||
|
|
||||||
|
# 添加文字数量说明
|
||||||
|
text_amount = result.get('text_amount', 'none')
|
||||||
|
if text_amount != 'none':
|
||||||
|
lines.append(f"文字数量:{text_amount},需要精细处理")
|
||||||
|
|
||||||
if feasibility == "partial":
|
if feasibility == "partial":
|
||||||
lines.append(f"⚠️ 此图有一定难度:{note or '效果可能不完美'},回复时可加「效果不满意退款」")
|
lines.append(f"⚠️ 此图有一定难度:{note or '效果可能不完美'},回复时可加「效果不满意退款」")
|
||||||
if note and note not in ("无", ""):
|
if note and note not in ("无", ""):
|
||||||
lines.append(f"提示:{note}")
|
lines.append(f"提示:{note}")
|
||||||
lines.append(f"【立刻回复客户报价 {result['price_suggest']} 元,话术自然多变】")
|
lines.append(f"【立刻回复客户报价 {total_price} 元,话术自然多变】")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -445,6 +469,7 @@ class CustomerServiceAgent:
|
|||||||
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,
|
||||||
@@ -502,6 +527,7 @@ class CustomerServiceAgent:
|
|||||||
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,
|
||||||
@@ -861,6 +887,13 @@ class CustomerServiceAgent:
|
|||||||
- analyze_image 工具调用完成后,你的下一句话一定是报价,不能是内部说明
|
- analyze_image 工具调用完成后,你的下一句话一定是报价,不能是内部说明
|
||||||
- 报价后立刻推成交,不等客户反应
|
- 报价后立刻推成交,不等客户反应
|
||||||
|
|
||||||
|
【文字加价规则】⚠️ 重要
|
||||||
|
- 含文字很多时不能低价,有文字跟没文字是两个价格
|
||||||
|
- 含文字的图必须 complex 起步(20 元以上)
|
||||||
|
- 客户嫌贵时明确告知:「有文字跟没文字是两个价格」
|
||||||
|
- 简单图但含文字 → normal 价格(15-20 元)
|
||||||
|
- normal 图含文字 → complex 价格(20-25 元)
|
||||||
|
|
||||||
【压价规则】
|
【压价规则】
|
||||||
- 客户说「贵」「有点贵」「算了」「便宜点」→ 直接让价一次,禁止追问「什么问题」「说清楚点」
|
- 客户说「贵」「有点贵」「算了」「便宜点」→ 直接让价一次,禁止追问「什么问题」「说清楚点」
|
||||||
- 只让价一次,话术自然变化
|
- 只让价一次,话术自然变化
|
||||||
@@ -870,6 +903,19 @@ class CustomerServiceAgent:
|
|||||||
- 退款/退货/投诉/情绪激动/test → 调用 transfer_to_human()
|
- 退款/退货/投诉/情绪激动/test → 调用 transfer_to_human()
|
||||||
- 调用后只回复"转接",不加其他内容
|
- 调用后只回复"转接",不加其他内容
|
||||||
|
|
||||||
|
【找茬客户识别】⚠️ 重要
|
||||||
|
识别以下高风险信号,建议不做这单:
|
||||||
|
1. 下单后立即申请退款
|
||||||
|
2. 从高价砍到低价(30→10 元)
|
||||||
|
3. 反复问"不满意可以退吗"(2 次以上)
|
||||||
|
4. 质疑服务内容("源文件还是什么")
|
||||||
|
5. 质疑价值("就一张图片")
|
||||||
|
6. 问"小一点就快一点的嘛"(想占便宜)
|
||||||
|
7. 重复问同一个问题(想找麻烦)
|
||||||
|
|
||||||
|
识别到以上 3 个以上信号 → 建议转人工或直接拒绝接单
|
||||||
|
话术:「不好意思,这单做不了」「去别家做吧」
|
||||||
|
|
||||||
【售后规则】
|
【售后规则】
|
||||||
- 催进度:自然回复在做了/快了/马上好之类
|
- 催进度:自然回复在做了/快了/马上好之类
|
||||||
- 要修改:自然问哪里要改
|
- 要修改:自然问哪里要改
|
||||||
@@ -1157,6 +1203,7 @@ class CustomerServiceAgent:
|
|||||||
# 已付款:触发 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,
|
||||||
@@ -1742,7 +1789,7 @@ class CustomerServiceAgent:
|
|||||||
prompt += "\n客户催单,查询当前处理状态后自然回复。"
|
prompt += "\n客户催单,查询当前处理状态后自然回复。"
|
||||||
elif any(kw in customer_text for kw in ["贵", "有点贵", "太贵", "算了", "便宜点", "少点", "打折", "贵哦"]):
|
elif any(kw in customer_text for kw in ["贵", "有点贵", "太贵", "算了", "便宜点", "少点", "打折", "贵哦"]):
|
||||||
# 客户嫌贵/要放弃 → 直接让价一次,不问「什么问题」
|
# 客户嫌贵/要放弃 → 直接让价一次,不问「什么问题」
|
||||||
prompt += "\n⚠️【客户嫌贵】客户已表达价格顾虑或要放弃。禁止追问「什么问题」「说清楚点」。直接让价一次(如原价20→15)。话术自然,像真人聊天,只让一次。"
|
prompt += "\n⚠️【客户嫌贵】客户已表达价格顾虑或要放弃。禁止追问「什么问题」「说清楚点」。\n→ 第一步:直接让价一次(如原价 20→15),话术自然。\n→ 第二步:如果让价后客户还是嫌贵,引导客户自己去注册星绘平台:\n - 每个话术都必须带网址 xinhui.cloud\n - 强调「自己去弄」「自己操作」「自己下单」\n - 示例:「嫌贵就去 xinhui.cloud 注册个号,自己有余额可以弄」「xinhui.cloud 去注册下,新用户送体验金,自己弄便宜」\n - 语气直接,让客户自己去操作」"
|
||||||
elif any(kw in customer_text for kw in ["擦边", "黄色", "色情", "大尺度", "性感图", "露点", "半裸"]):
|
elif any(kw in customer_text for kw in ["擦边", "黄色", "色情", "大尺度", "性感图", "露点", "半裸"]):
|
||||||
# 客户问擦边/黄色内容 → 直接拒绝,不说「发图来看看」
|
# 客户问擦边/黄色内容 → 直接拒绝,不说「发图来看看」
|
||||||
prompt += "\n⚠️【拒绝】客户询问擦边/黄色/敏感内容。直接拒绝,不接单,不说「发图来看看」。自然回复如:这类不做/不接/做不了。"
|
prompt += "\n⚠️【拒绝】客户询问擦边/黄色/敏感内容。直接拒绝,不接单,不说「发图来看看」。自然回复如:这类不做/不接/做不了。"
|
||||||
@@ -1795,3 +1842,65 @@ 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
|
||||||
|
|||||||
289
core/task_scheduler.py
Normal file
289
core/task_scheduler.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
天网任务调度系统
|
||||||
|
负责任务的执行、重试、超时处理
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
from .websocket_client import QingjianAPIClient
|
||||||
|
from db.task_db.task_model import get_task_manager, TaskStatus, TaskPriority
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TaskScheduler:
|
||||||
|
"""任务调度器"""
|
||||||
|
|
||||||
|
def __init__(self, client: QingjianAPIClient = None):
|
||||||
|
self.task_manager = get_task_manager()
|
||||||
|
self.client = client
|
||||||
|
self.running = False
|
||||||
|
self.task_timeout_checker = None
|
||||||
|
logger.info("任务调度器初始化完成")
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""启动任务调度器"""
|
||||||
|
self.running = True
|
||||||
|
logger.info("任务调度器已启动")
|
||||||
|
|
||||||
|
# 启动超时检查任务
|
||||||
|
self.task_timeout_checker = asyncio.create_task(self._check_timeouts())
|
||||||
|
|
||||||
|
# 启动任务执行队列
|
||||||
|
await self._process_task_queue()
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""停止任务调度器"""
|
||||||
|
self.running = False
|
||||||
|
if self.task_timeout_checker:
|
||||||
|
self.task_timeout_checker.cancel()
|
||||||
|
logger.info("任务调度器已停止")
|
||||||
|
|
||||||
|
async def _check_timeouts(self):
|
||||||
|
"""检查超时任务"""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
timeout_tasks = self.task_manager.get_timeout_tasks()
|
||||||
|
for task in timeout_tasks:
|
||||||
|
logger.warning(f"任务超时:{task['task_id']}")
|
||||||
|
self.task_manager.cancel_task(
|
||||||
|
task['task_id'],
|
||||||
|
reason=f"任务超时({task['timeout_hours']}小时)"
|
||||||
|
)
|
||||||
|
# 通知天网任务超时
|
||||||
|
await self._notify_tianwang(task['task_id'], 'timeout')
|
||||||
|
|
||||||
|
# 每 5 分钟检查一次
|
||||||
|
await asyncio.sleep(300)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"超时检查失败:{e}")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def _process_task_queue(self):
|
||||||
|
"""处理任务队列"""
|
||||||
|
# 持续监听,当有任务时执行
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# 这里实际应该从队列获取任务
|
||||||
|
# 简化处理:每秒检查一次待触发任务
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"任务队列处理失败:{e}")
|
||||||
|
|
||||||
|
async def execute_task(self, task: dict) -> bool:
|
||||||
|
"""
|
||||||
|
执行任务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task: 任务数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否执行成功
|
||||||
|
"""
|
||||||
|
task_id = task['task_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 更新状态为执行中
|
||||||
|
self.task_manager.update_task_status(task_id, TaskStatus.RUNNING)
|
||||||
|
logger.info(f"开始执行任务:{task_id}")
|
||||||
|
|
||||||
|
# 根据任务类型执行
|
||||||
|
task_type = task.get('type')
|
||||||
|
|
||||||
|
if task_type == 'send_file_after_reply':
|
||||||
|
success = await self._execute_send_file(task)
|
||||||
|
elif task_type == 'send_message':
|
||||||
|
success = await self._execute_send_message(task)
|
||||||
|
elif task_type == 'send_link':
|
||||||
|
success = await self._execute_send_link(task)
|
||||||
|
else:
|
||||||
|
logger.error(f"未知任务类型:{task_type}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.task_manager.update_task_status(
|
||||||
|
task_id,
|
||||||
|
TaskStatus.COMPLETED,
|
||||||
|
result={'executed_at': datetime.now().isoformat()}
|
||||||
|
)
|
||||||
|
logger.info(f"任务执行成功:{task_id}")
|
||||||
|
|
||||||
|
# 通知天网
|
||||||
|
await self._notify_tianwang(task_id, 'completed')
|
||||||
|
else:
|
||||||
|
# 失败重试逻辑
|
||||||
|
await self._handle_task_failure(task)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"任务执行失败:{task_id} - {e}")
|
||||||
|
await self._handle_task_failure(task, error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _execute_send_file(self, task: dict) -> bool:
|
||||||
|
"""执行发送文件任务"""
|
||||||
|
try:
|
||||||
|
customer_id = task.get('customer', {}).get('id')
|
||||||
|
file_url = task.get('action', {}).get('file_url')
|
||||||
|
message = task.get('action', {}).get('message', '这是您要的文件')
|
||||||
|
|
||||||
|
if not self.client:
|
||||||
|
logger.error("WebSocket 客户端未初始化")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 调用发送文件方法
|
||||||
|
# TODO: 实现 send_file 方法
|
||||||
|
logger.info(f"发送文件给 {customer_id}: {file_url}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送文件失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _execute_send_message(self, task: dict) -> bool:
|
||||||
|
"""执行发送消息任务"""
|
||||||
|
try:
|
||||||
|
customer_id = task.get('customer', {}).get('id')
|
||||||
|
message = task.get('action', {}).get('message')
|
||||||
|
|
||||||
|
if not self.client:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 发送消息
|
||||||
|
await self.client.send_message(customer_id, message)
|
||||||
|
logger.info(f"发送消息给 {customer_id}: {message}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送消息失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _execute_send_link(self, task: dict) -> bool:
|
||||||
|
"""执行发送链接任务"""
|
||||||
|
try:
|
||||||
|
customer_id = task.get('customer', {}).get('id')
|
||||||
|
link_url = task.get('action', {}).get('link_url')
|
||||||
|
message = task.get('action', {}).get('message', '')
|
||||||
|
|
||||||
|
full_message = f"{message}\n\n{link_url}" if message else link_url
|
||||||
|
|
||||||
|
return await self._execute_send_message({
|
||||||
|
'customer': {'id': customer_id},
|
||||||
|
'action': {'message': full_message}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送链接失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _handle_task_failure(self, task: dict, error: str = None):
|
||||||
|
"""处理任务失败"""
|
||||||
|
task_id = task['task_id']
|
||||||
|
max_retry = task.get('max_retry', 3)
|
||||||
|
|
||||||
|
retry_count = self.task_manager.increment_retry(task_id)
|
||||||
|
|
||||||
|
if retry_count <= max_retry:
|
||||||
|
# 重试
|
||||||
|
logger.warning(f"任务失败,{retry_count}/{max_retry} 次重试:{task_id}")
|
||||||
|
await asyncio.sleep(60 * retry_count) # 递增重试间隔
|
||||||
|
await self.execute_task(task)
|
||||||
|
else:
|
||||||
|
# 超过最大重试次数,标记失败
|
||||||
|
self.task_manager.update_task_status(
|
||||||
|
task_id,
|
||||||
|
TaskStatus.FAILED,
|
||||||
|
error_message=error or '超过最大重试次数'
|
||||||
|
)
|
||||||
|
logger.error(f"任务最终失败:{task_id}")
|
||||||
|
|
||||||
|
# 通知天网
|
||||||
|
await self._notify_tianwang(task_id, 'failed', error)
|
||||||
|
|
||||||
|
async def _notify_tianwang(self, task_id: str, status: str, error: str = None):
|
||||||
|
"""通知天网任务状态"""
|
||||||
|
try:
|
||||||
|
# TODO: 实现天网回调 API
|
||||||
|
# 可以通过 HTTP POST 通知天网
|
||||||
|
logger.info(f"通知天网 - 任务 {task_id} 状态:{status}")
|
||||||
|
|
||||||
|
# 示例代码(实际使用时取消注释并配置天网 URL)
|
||||||
|
'''
|
||||||
|
import httpx
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
await client.post(
|
||||||
|
'http://127.0.0.1:6060/api/task/callback',
|
||||||
|
json={
|
||||||
|
'task_id': task_id,
|
||||||
|
'status': status,
|
||||||
|
'error': error,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"通知天网失败:{e}")
|
||||||
|
|
||||||
|
def check_trigger_match(self, message: str, trigger: dict) -> bool:
|
||||||
|
"""
|
||||||
|
检查消息是否匹配触发条件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 客户消息内容
|
||||||
|
trigger: 触发条件配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否匹配
|
||||||
|
"""
|
||||||
|
trigger_type = trigger.get('type')
|
||||||
|
|
||||||
|
if trigger_type == 'customer_reply':
|
||||||
|
keyword = trigger.get('keyword')
|
||||||
|
return keyword and keyword in message
|
||||||
|
|
||||||
|
elif trigger_type == 'customer_keyword':
|
||||||
|
keywords = trigger.get('keywords', [])
|
||||||
|
if isinstance(keywords, str):
|
||||||
|
import json
|
||||||
|
keywords = json.loads(keywords)
|
||||||
|
return any(kw in message for kw in keywords)
|
||||||
|
|
||||||
|
elif trigger_type == 'customer_payment':
|
||||||
|
# 付款检测逻辑
|
||||||
|
payment_keywords = ['已付款', '拍下了', '已下单', '付款了']
|
||||||
|
return any(kw in message for kw in payment_keywords)
|
||||||
|
|
||||||
|
elif trigger_type == 'time_reach':
|
||||||
|
# 时间判断
|
||||||
|
target_time = trigger.get('time')
|
||||||
|
if target_time:
|
||||||
|
try:
|
||||||
|
target = datetime.fromisoformat(target_time)
|
||||||
|
return datetime.now() >= target
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 单例
|
||||||
|
_scheduler: Optional[TaskScheduler] = None
|
||||||
|
|
||||||
|
def get_task_scheduler(client: QingjianAPIClient = None) -> TaskScheduler:
|
||||||
|
"""获取任务调度器单例"""
|
||||||
|
global _scheduler
|
||||||
|
if _scheduler is None:
|
||||||
|
_scheduler = TaskScheduler(client)
|
||||||
|
elif client and _scheduler.client is None:
|
||||||
|
_scheduler.client = client
|
||||||
|
return _scheduler
|
||||||
211
core/task_trigger.py
Normal file
211
core/task_trigger.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
任务触发条件匹配引擎
|
||||||
|
支持多种触发条件类型
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskTriggerEngine:
|
||||||
|
"""任务触发引擎"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 触发器类型注册表
|
||||||
|
self.trigger_handlers = {
|
||||||
|
'customer_reply': self._check_customer_reply,
|
||||||
|
'customer_keyword': self._check_customer_keyword,
|
||||||
|
'customer_payment': self._check_customer_payment,
|
||||||
|
'customer_order': self._check_customer_order,
|
||||||
|
'time_reach': self._check_time_reach,
|
||||||
|
'custom_event': self._check_custom_event,
|
||||||
|
'specified_customer_reply': self._check_specified_customer_reply, # 新增:指定客户回复
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_trigger(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
检查消息是否匹配触发条件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 客户消息内容
|
||||||
|
trigger: 触发条件配置
|
||||||
|
context: 上下文信息(客户 ID、店铺 ID 等)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否匹配
|
||||||
|
"""
|
||||||
|
trigger_type = trigger.get('type')
|
||||||
|
|
||||||
|
if trigger_type not in self.trigger_handlers:
|
||||||
|
logger.warning(f"未知触发类型:{trigger_type}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
handler = self.trigger_handlers[trigger_type]
|
||||||
|
try:
|
||||||
|
return handler(message, trigger, context)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"触发条件检查失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_customer_reply(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
检查客户回复指定内容
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "customer_reply",
|
||||||
|
"keyword": "好的" // 包含"好的"即匹配
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
keyword = trigger.get('keyword')
|
||||||
|
if not keyword:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return keyword in message
|
||||||
|
|
||||||
|
def _check_customer_keyword(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
检查客户说某关键词
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "customer_keyword",
|
||||||
|
"keywords": ["好的", "可以", "行"] // 任一关键词匹配
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
keywords = trigger.get('keywords', [])
|
||||||
|
if isinstance(keywords, str):
|
||||||
|
import json
|
||||||
|
keywords = json.loads(keywords)
|
||||||
|
|
||||||
|
if not keywords:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return any(kw in message for kw in keywords)
|
||||||
|
|
||||||
|
def _check_customer_payment(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
检查客户付款
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "customer_payment",
|
||||||
|
"keywords": ["已付款", "拍下了", "已下单", "付款了"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
payment_keywords = trigger.get('keywords', [
|
||||||
|
'已付款', '拍下了', '已下单', '付款了', '已支付', '拍下付款'
|
||||||
|
])
|
||||||
|
|
||||||
|
return any(kw in message for kw in payment_keywords)
|
||||||
|
|
||||||
|
def _check_customer_order(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
检查客户下单
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "customer_order",
|
||||||
|
"keywords": ["下单了", "拍了", "购买了"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
order_keywords = trigger.get('keywords', [
|
||||||
|
'下单了', '拍了', '购买了', '买了'
|
||||||
|
])
|
||||||
|
|
||||||
|
return any(kw in message for kw in order_keywords)
|
||||||
|
|
||||||
|
def _check_time_reach(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
检查到达指定时间
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "time_reach",
|
||||||
|
"time": "2026-02-27 09:00:00"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
target_time = trigger.get('time')
|
||||||
|
if not target_time:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
target = datetime.fromisoformat(target_time)
|
||||||
|
return datetime.now() >= target
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"时间解析失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_custom_event(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
自定义事件触发
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "custom_event",
|
||||||
|
"event_name": "customer_complaint"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# 预留自定义事件接口
|
||||||
|
logger.info(f"自定义事件触发:{trigger.get('event_name')}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _check_specified_customer_reply(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||||
|
"""
|
||||||
|
【新增】指定客户回复指定内容
|
||||||
|
触发条件:
|
||||||
|
{
|
||||||
|
"type": "specified_customer_reply",
|
||||||
|
"customer_id": "customer_123", // 指定客户 ID
|
||||||
|
"customer_name": "小明", // 可选:客户名称
|
||||||
|
"keyword": "好的", // 回复内容
|
||||||
|
"exact_match": false // 可选:是否精确匹配
|
||||||
|
}
|
||||||
|
|
||||||
|
匹配逻辑:
|
||||||
|
1. 检查当前消息的客户 ID 是否匹配
|
||||||
|
2. 检查消息内容是否包含关键词
|
||||||
|
"""
|
||||||
|
# 1. 检查客户 ID 是否匹配
|
||||||
|
specified_customer_id = trigger.get('customer_id')
|
||||||
|
specified_customer_name = trigger.get('customer_name')
|
||||||
|
|
||||||
|
if context:
|
||||||
|
current_customer_id = context.get('customer_id')
|
||||||
|
current_customer_name = context.get('customer_name')
|
||||||
|
|
||||||
|
# 客户 ID 匹配检查
|
||||||
|
if specified_customer_id and current_customer_id:
|
||||||
|
if specified_customer_id != current_customer_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 客户名称匹配检查(可选)
|
||||||
|
if specified_customer_name and current_customer_name:
|
||||||
|
if specified_customer_name != current_customer_name:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.warning("缺少上下文信息,无法检查指定客户")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. 检查回复内容
|
||||||
|
keyword = trigger.get('keyword')
|
||||||
|
if not keyword:
|
||||||
|
return False
|
||||||
|
|
||||||
|
exact_match = trigger.get('exact_match', False)
|
||||||
|
|
||||||
|
if exact_match:
|
||||||
|
# 精确匹配:消息内容完全等于关键词
|
||||||
|
return message.strip() == keyword.strip()
|
||||||
|
else:
|
||||||
|
# 模糊匹配:消息包含关键词
|
||||||
|
return keyword in message
|
||||||
|
|
||||||
|
|
||||||
|
# 单例
|
||||||
|
_trigger_engine: Optional[TaskTriggerEngine] = None
|
||||||
|
|
||||||
|
def get_trigger_engine() -> TaskTriggerEngine:
|
||||||
|
"""获取触发引擎单例"""
|
||||||
|
global _trigger_engine
|
||||||
|
if _trigger_engine is None:
|
||||||
|
_trigger_engine = TaskTriggerEngine()
|
||||||
|
return _trigger_engine
|
||||||
116
core/websocket_client.py
Normal file → Executable file
116
core/websocket_client.py
Normal file → Executable file
@@ -95,6 +95,15 @@ class QingjianAPIClient:
|
|||||||
self._pending_images: dict = {}
|
self._pending_images: dict = {}
|
||||||
self._pending_image_tasks: dict = {}
|
self._pending_image_tasks: dict = {}
|
||||||
|
|
||||||
|
# 延迟加载任务模块(避免循环导入)
|
||||||
|
self.task_scheduler = None
|
||||||
|
self.task_manager = None
|
||||||
|
self.trigger_engine = None
|
||||||
|
|
||||||
|
# 多进程分片支持
|
||||||
|
self.shard_keys: set = set() # 本进程负责的客户 key 集合
|
||||||
|
self.worker_id = int(os.getenv('AI_CS_WORKER_ID', '0'))
|
||||||
|
|
||||||
# 初始化 Agent
|
# 初始化 Agent
|
||||||
if self.enable_agent:
|
if self.enable_agent:
|
||||||
try:
|
try:
|
||||||
@@ -109,6 +118,7 @@ class QingjianAPIClient:
|
|||||||
workflow.register_send_callback(self._workflow_send)
|
workflow.register_send_callback(self._workflow_send)
|
||||||
workflow.register_agent_notify_callback(self._workflow_agent_notify)
|
workflow.register_agent_notify_callback(self._workflow_agent_notify)
|
||||||
|
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""连接WebSocket服务器"""
|
"""连接WebSocket服务器"""
|
||||||
while self.running:
|
while self.running:
|
||||||
@@ -190,11 +200,18 @@ class QingjianAPIClient:
|
|||||||
|
|
||||||
async def handle_message(self, message):
|
async def handle_message(self, message):
|
||||||
"""处理接收到的消息"""
|
"""处理接收到的消息"""
|
||||||
timestamp = self.get_time()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(message)
|
data = json.loads(message)
|
||||||
|
|
||||||
|
# 多进程分片检查:只处理分配给本进程的客户
|
||||||
|
if self.shard_keys:
|
||||||
|
customer_key = self._customer_key(data)
|
||||||
|
if customer_key not in self.shard_keys:
|
||||||
|
# 不属于本进程的客户,跳过
|
||||||
|
return
|
||||||
|
|
||||||
|
timestamp = self.get_time()
|
||||||
|
|
||||||
# 保存最后一条消息用于回复
|
# 保存最后一条消息用于回复
|
||||||
self.last_msg = data
|
self.last_msg = data
|
||||||
|
|
||||||
@@ -517,7 +534,7 @@ class QingjianAPIClient:
|
|||||||
urls = self._extract_image_urls(msg_text)
|
urls = self._extract_image_urls(msg_text)
|
||||||
key = self._customer_key(data)
|
key = self._customer_key(data)
|
||||||
self._add_pending_images(key, urls)
|
self._add_pending_images(key, urls)
|
||||||
await self.send_reply(data, "图片收到了,说下要求(尺寸/要做什么)")
|
await self.send_reply(data, "收到,我看看哈")
|
||||||
old = self._pending_image_tasks.get(key)
|
old = self._pending_image_tasks.get(key)
|
||||||
if old and not old.done():
|
if old and not old.done():
|
||||||
old.cancel()
|
old.cancel()
|
||||||
@@ -546,7 +563,7 @@ class QingjianAPIClient:
|
|||||||
if len(urls) == 1:
|
if len(urls) == 1:
|
||||||
key = self._customer_key(data)
|
key = self._customer_key(data)
|
||||||
self._add_pending_images(key, urls)
|
self._add_pending_images(key, urls)
|
||||||
await self.send_reply(data, "图片收到了,说下要求(尺寸/要做什么)")
|
await self.send_reply(data, "收到,我看看哈")
|
||||||
else:
|
else:
|
||||||
if self._msg_requests_external_contact(msg_text):
|
if self._msg_requests_external_contact(msg_text):
|
||||||
reply = "这里沟通就可以哦,其他联系方式不方便"
|
reply = "这里沟通就可以哦,其他联系方式不方便"
|
||||||
@@ -1303,3 +1320,94 @@ if __name__ == "__main__":
|
|||||||
asyncio.run(client.run())
|
asyncio.run(client.run())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n已停止")
|
print("\n已停止")
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_task_modules(self):
|
||||||
|
"""延迟加载任务模块,避免循环导入"""
|
||||||
|
from core.task_scheduler import get_task_scheduler
|
||||||
|
from core.task_trigger import get_trigger_engine
|
||||||
|
from db.task_db.task_model import get_task_manager
|
||||||
|
self.trigger_engine = get_trigger_engine()
|
||||||
|
|
||||||
|
async def check_and_trigger_tasks(self, data: dict):
|
||||||
|
"""检查并触发匹配的任务"""
|
||||||
|
try:
|
||||||
|
customer_key = self._customer_key(data)
|
||||||
|
customer_id = data.get('from_id')
|
||||||
|
message = data.get('content', '')
|
||||||
|
|
||||||
|
# 获取该客户的待触发任务
|
||||||
|
pending_tasks = self.task_manager.get_pending_tasks(customer_id)
|
||||||
|
|
||||||
|
for task in pending_tasks:
|
||||||
|
trigger = {
|
||||||
|
'type': task['trigger_type'],
|
||||||
|
'keyword': task['trigger_keyword'],
|
||||||
|
'keywords': task['trigger_keywords']
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查是否匹配触发条件
|
||||||
|
if self.task_scheduler.check_trigger_match(message, trigger):
|
||||||
|
logger.info(f"任务触发条件匹配:{task['task_id']}")
|
||||||
|
|
||||||
|
# 异步执行任务
|
||||||
|
asyncio.create_task(self.task_scheduler.execute_task(task))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"检查任务触发失败:{e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_task_modules(self):
|
||||||
|
"""延迟加载任务模块,避免循环导入"""
|
||||||
|
from core.task_scheduler import get_task_scheduler
|
||||||
|
from core.task_trigger import get_trigger_engine
|
||||||
|
from db.task_db.task_model import get_task_manager
|
||||||
|
self.trigger_engine = get_trigger_engine()
|
||||||
|
|
||||||
|
async def _load_task_modules(self):
|
||||||
|
"""延迟加载任务模块"""
|
||||||
|
if self.task_scheduler is None:
|
||||||
|
from core.task_scheduler import get_task_scheduler
|
||||||
|
from core.task_trigger import get_trigger_engine
|
||||||
|
from db.task_db.task_model import get_task_manager
|
||||||
|
self.trigger_engine = get_trigger_engine()
|
||||||
|
|
||||||
|
async def check_and_trigger_tasks_v2(self, data: dict):
|
||||||
|
"""增强版:检查并触发匹配的任务(支持指定客户)"""
|
||||||
|
# 确保任务模块已加载
|
||||||
|
await self._load_task_modules()
|
||||||
|
try:
|
||||||
|
customer_key = self._customer_key(data)
|
||||||
|
customer_id = data.get('from_id')
|
||||||
|
customer_name = data.get('from_name')
|
||||||
|
message = data.get('content', '')
|
||||||
|
|
||||||
|
# 准备上下文
|
||||||
|
context = {
|
||||||
|
'customer_id': customer_id,
|
||||||
|
'customer_name': customer_name,
|
||||||
|
'acc_id': data.get('acc_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取该客户的待触发任务
|
||||||
|
pending_tasks = self.task_manager.get_pending_tasks(customer_id)
|
||||||
|
|
||||||
|
for task in pending_tasks:
|
||||||
|
trigger = {
|
||||||
|
'type': task['trigger_type'],
|
||||||
|
'keyword': task['trigger_keyword'],
|
||||||
|
'keywords': task['trigger_keywords'],
|
||||||
|
# 指定客户相关字段
|
||||||
|
'customer_id': task.get('specified_customer_id'),
|
||||||
|
'customer_name': task.get('specified_customer_name')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 使用触发引擎检查是否匹配
|
||||||
|
if self.trigger_engine.check_trigger(message, trigger, context):
|
||||||
|
logger.info(f"任务触发条件匹配:{task['task_id']} (客户:{customer_name}/{customer_id})")
|
||||||
|
|
||||||
|
# 异步执行任务
|
||||||
|
asyncio.create_task(self.task_scheduler.execute_task(task))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"检查任务触发失败:{e}")
|
||||||
|
|||||||
371
core/workflow.py
Normal file → Executable file
371
core/workflow.py
Normal file → Executable file
@@ -418,13 +418,13 @@ class CustomerServiceWorkflow:
|
|||||||
f"原因:{err_msg[:200]}\n"
|
f"原因:{err_msg[:200]}\n"
|
||||||
f"请人工跟进"
|
f"请人工跟进"
|
||||||
)
|
)
|
||||||
# 通知客户稍等,人工跟进
|
# 通知客户稍等,并告知转人工
|
||||||
if self._send_message:
|
if self._send_message:
|
||||||
await self._send_message(
|
await self._send_message(
|
||||||
customer_id=task.customer_id,
|
customer_id=task.customer_id,
|
||||||
acc_id=acc_id,
|
acc_id=acc_id,
|
||||||
acc_type=acc_type,
|
acc_type=acc_type,
|
||||||
content="您好,图片正在处理中,稍后发您,请稍等",
|
content="您好,图片处理遇到点问题,已帮您转接人工客服处理,请稍候",
|
||||||
msg_type=0,
|
msg_type=0,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -614,3 +614,370 @@ class CustomerServiceWorkflow:
|
|||||||
|
|
||||||
# ========== 全局实例 ==========
|
# ========== 全局实例 ==========
|
||||||
workflow = CustomerServiceWorkflow()
|
workflow = CustomerServiceWorkflow()
|
||||||
|
|
||||||
|
# ========== 客户需求变更 ==========
|
||||||
|
|
||||||
|
async def add_customer_requirement(self, task_id: str, customer_id: str,
|
||||||
|
requirement: str, changed_by: str = 'customer') -> bool:
|
||||||
|
"""
|
||||||
|
客户添加/修改需求细节
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务 ID
|
||||||
|
customer_id: 客户 ID
|
||||||
|
requirement: 需求内容(如:"需要去掉背景"、"要分层文件")
|
||||||
|
changed_by: 修改者(customer/staff)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
# 检查任务是否存在
|
||||||
|
task = self.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
# 尝试从数据库加载
|
||||||
|
db_task = self.db.get_task(task_id)
|
||||||
|
if db_task:
|
||||||
|
print(f"[Workflow] 从数据库加载任务:{task_id[:8]}...")
|
||||||
|
# 可以在这里重建内存任务
|
||||||
|
else:
|
||||||
|
print(f"[Workflow] 任务不存在:{task_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 添加到数据库
|
||||||
|
success = self.db.add_customer_note(task_id, requirement, changed_by)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"[Workflow] 客户添加需求:{task_id[:8]}... | {requirement}")
|
||||||
|
|
||||||
|
# 如果任务还在待处理状态,通知 AI 客服
|
||||||
|
if task and task.status.value == 'pending':
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=task.acc_id,
|
||||||
|
acc_type=task.acc_type,
|
||||||
|
content=f"好的,已记录您的需求:{requirement},处理时会注意的",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
async def modify_operation(self, task_id: str, customer_id: str,
|
||||||
|
new_operation: str, changed_by: str = 'customer') -> bool:
|
||||||
|
"""
|
||||||
|
客户修改操作类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务 ID
|
||||||
|
customer_id: 客户 ID
|
||||||
|
new_operation: 新操作(enhance/remove_bg/vectorize 等)
|
||||||
|
changed_by: 修改者
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
task = self.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
db_task = self.db.get_task(task_id)
|
||||||
|
if not db_task:
|
||||||
|
print(f"[Workflow] 任务不存在:{task_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查状态,已处理完成的不允许修改
|
||||||
|
if task and task.status.value in ['completed', 'processing']:
|
||||||
|
print(f"[Workflow] 任务已开始处理,不允许修改操作:{task_id}")
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=task.acc_id,
|
||||||
|
acc_type=task.acc_type,
|
||||||
|
content="抱歉,图片已经开始处理了,无法修改操作类型",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 修改数据库
|
||||||
|
success = self.db.modify_operation(task_id, new_operation, changed_by)
|
||||||
|
|
||||||
|
if success and task:
|
||||||
|
task.operation = new_operation
|
||||||
|
print(f"[Workflow] 修改操作类型:{task_id[:8]}... -> {new_operation}")
|
||||||
|
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=task.acc_id,
|
||||||
|
acc_type=task.acc_type,
|
||||||
|
content=f"好的,已为您修改为{new_operation}操作",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def get_task_requirement_history(self, task_id: str) -> List[dict]:
|
||||||
|
"""获取任务需求变更历史"""
|
||||||
|
return self.db.get_requirement_history(task_id)
|
||||||
|
|
||||||
|
# ========== 三种工作流 ==========
|
||||||
|
|
||||||
|
async def find_image_workflow(self, customer_id: str, image_url: str,
|
||||||
|
acc_id: str = "", acc_type: str = "AliWorkbench") -> bool:
|
||||||
|
"""
|
||||||
|
工作流 1:查找图片
|
||||||
|
客户说"找一下这个图" → 自己处理 → 上传到图绘 → 返回 URL
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer_id: 客户 ID
|
||||||
|
image_url: 图片 URL
|
||||||
|
acc_id: 店铺 ID
|
||||||
|
acc_type: 平台类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"[Workflow] 启动查找图片工作流 | 客户:{customer_id}")
|
||||||
|
|
||||||
|
# 1. 创建任务
|
||||||
|
task_id = self.create_image_task(
|
||||||
|
customer_id=customer_id,
|
||||||
|
customer_name=customer_id,
|
||||||
|
original_image=image_url,
|
||||||
|
operation="find", # 查找操作
|
||||||
|
requirements="type:find",
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 这里调用图绘 API 上传图片
|
||||||
|
# TODO: 调用图绘上传 API
|
||||||
|
# tuhui_url = await self._upload_to_tuhui(image_url)
|
||||||
|
|
||||||
|
# 临时模拟
|
||||||
|
tuhui_url = f"http://tuhui.cloud/works/123"
|
||||||
|
|
||||||
|
# 3. 更新任务结果
|
||||||
|
self.db.update_result(task_id, tuhui_url)
|
||||||
|
self.db.update_status(task_id, DBTaskStatus.COMPLETED)
|
||||||
|
|
||||||
|
# 4. 回复客户
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type,
|
||||||
|
content=f"找到了!图片在这里:{tuhui_url}",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[Workflow] 查找图片完成 | 客户:{customer_id} | URL: {tuhui_url}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查找图片工作流失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def process_image_workflow(self, customer_id: str, image_url: str,
|
||||||
|
acc_id: str = "", acc_type: str = "AliWorkbench") -> bool:
|
||||||
|
"""
|
||||||
|
工作流 2:处理图片
|
||||||
|
客户说"做一下" → 评估图片 → 稍等做
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer_id: 客户 ID
|
||||||
|
image_url: 图片 URL
|
||||||
|
acc_id: 店铺 ID
|
||||||
|
acc_type: 平台类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"[Workflow] 启动处理图片工作流 | 客户:{customer_id}")
|
||||||
|
|
||||||
|
# 1. 创建任务
|
||||||
|
task_id = self.create_image_task(
|
||||||
|
customer_id=customer_id,
|
||||||
|
customer_name=customer_id,
|
||||||
|
original_image=image_url,
|
||||||
|
operation="enhance",
|
||||||
|
requirements="type:process",
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 回复客户稍等
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type,
|
||||||
|
content="稍等,我看看...好的,可以做,马上处理",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 启动处理
|
||||||
|
await self.trigger_processing_on_payment(customer_id, acc_id, acc_type)
|
||||||
|
|
||||||
|
print(f"[Workflow] 处理图片已启动 | 客户:{customer_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理图片工作流失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def transfer_to_designer_workflow(self, customer_id: str, image_url: str,
|
||||||
|
acc_id: str = "", acc_type: str = "AliWorkbench",
|
||||||
|
reason: str = "做不了") -> bool:
|
||||||
|
"""
|
||||||
|
工作流 3:转人工派单
|
||||||
|
做不了 → 查询企业微信在线设计师 → 派单
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer_id: 客户 ID
|
||||||
|
image_url: 图片 URL
|
||||||
|
acc_id: 店铺 ID
|
||||||
|
acc_type: 平台类型
|
||||||
|
reason: 转接原因
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"[Workflow] 启动转人工派单工作流 | 客户:{customer_id} | 原因:{reason}")
|
||||||
|
|
||||||
|
# 1. 创建任务
|
||||||
|
task_id = self.create_image_task(
|
||||||
|
customer_id=customer_id,
|
||||||
|
customer_name=customer_id,
|
||||||
|
original_image=image_url,
|
||||||
|
operation="manual",
|
||||||
|
requirements=f"type:transfer|reason:{reason}",
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 查询企业微信在线设计师
|
||||||
|
online_designers = await self._get_online_designers()
|
||||||
|
|
||||||
|
if not online_designers:
|
||||||
|
# 无人在线,通知客户
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type,
|
||||||
|
content="抱歉,现在设计师都不在线,稍后会有人联系您",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 企业微信预警
|
||||||
|
await _wechat_notify(
|
||||||
|
f"⚠️ **人工派单但无人在线**\n"
|
||||||
|
f"客户:{customer_id}\n"
|
||||||
|
f"店铺:{acc_id}\n"
|
||||||
|
f"原因:{reason}\n"
|
||||||
|
f"请安排设计师上线"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[Workflow] 无人在线 | 客户:{customer_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3. 派单给在线设计师
|
||||||
|
designer_name = online_designers[0] # 取第一个在线的
|
||||||
|
success = await self._dispatch_to_designer(task_id, designer_name, customer_id, image_url, reason)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
logger.error("派单失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 4. 回复客户
|
||||||
|
if self._send_message:
|
||||||
|
await self._send_message(
|
||||||
|
customer_id=customer_id,
|
||||||
|
acc_id=acc_id,
|
||||||
|
acc_type=acc_type,
|
||||||
|
content="好的,已帮您安排设计师处理,请稍候",
|
||||||
|
msg_type=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[Workflow] 已派单给设计师:{designer} | 客户:{customer_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"转人工派单工作流失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _get_online_designers(self) -> list:
|
||||||
|
"""
|
||||||
|
查询在线设计师(使用图绘派单 API)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 在线设计师名单 ["橘子", "婷婷", ...]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
designers = await self.dispatch_client.get_online_designers()
|
||||||
|
print(f"[Workflow] 查询在线设计师:{len(designers)}人在线 | {designers}")
|
||||||
|
return designers
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询在线设计师失败:{e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def _dispatch_to_designer(self, task_id: str, designer_name: str,
|
||||||
|
customer_id: str, image_url: str, reason: str) -> bool:
|
||||||
|
"""
|
||||||
|
派单给设计师(使用图绘派单 API)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务 ID
|
||||||
|
designer_name: 设计师姓名
|
||||||
|
customer_id: 客户 ID
|
||||||
|
image_url: 图片 URL
|
||||||
|
reason: 转接原因
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 在派单系统创建任务
|
||||||
|
dispatch_task_id = await self.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,
|
||||||
|
deadline=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dispatch_task_id:
|
||||||
|
logger.error("创建派单任务失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. 分配给设计师
|
||||||
|
success = await self.dispatch_client.assign_task(
|
||||||
|
task_id=dispatch_task_id,
|
||||||
|
designer_name=designer_name,
|
||||||
|
notes=f"AI 客服自动派单\n原因:{reason}\n客户:{customer_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"[Workflow] 派单成功:{dispatch_task_id} → {designer_name} | 客户:{customer_id}")
|
||||||
|
|
||||||
|
# 企业微信通知
|
||||||
|
await _wechat_notify(
|
||||||
|
f"📋 **新任务派单**\n"
|
||||||
|
f"设计师:{designer_name}\n"
|
||||||
|
f"任务 ID: {dispatch_task_id}\n"
|
||||||
|
f"客户:{customer_id}\n"
|
||||||
|
f"原因:{reason}\n"
|
||||||
|
f"请及时处理"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("分配任务失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"派单失败:{e}")
|
||||||
|
return False
|
||||||
|
|||||||
93
core/workflow_router.py
Normal file
93
core/workflow_router.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
工作流程路由器
|
||||||
|
根据客户说的话判断执行哪种工作流
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class WorkflowRouter:
|
||||||
|
"""工作流程路由器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 查找图片关键词
|
||||||
|
self.find_keywords = [
|
||||||
|
"找一下", "找图", "找原图", "帮我找", "能找到吗",
|
||||||
|
"有吗", "有没有", "找到", "搜一下"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 处理图片关键词
|
||||||
|
self.process_keywords = [
|
||||||
|
"做一下", "处理一下", "安排", "开始做",
|
||||||
|
"弄一下", "修一下", "P 一下", "P 图"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 无法处理关键词
|
||||||
|
self.unable_keywords = [
|
||||||
|
"做不了", "处理不了", "弄不了", "无法处理",
|
||||||
|
"做不到", "搞不定"
|
||||||
|
]
|
||||||
|
|
||||||
|
def detect_workflow(self, message: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
检测工作流程类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 客户消息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(workflow_type, confidence)
|
||||||
|
workflow_type: "find_image" / "process_image" / "transfer_human"
|
||||||
|
confidence: 置信度 0-1
|
||||||
|
"""
|
||||||
|
message_lower = message.lower()
|
||||||
|
|
||||||
|
# 1. 检测查找图片
|
||||||
|
for kw in self.find_keywords:
|
||||||
|
if kw in message_lower:
|
||||||
|
logger.info(f"检测到查找图片意图:{kw}")
|
||||||
|
return ("find_image", 0.9)
|
||||||
|
|
||||||
|
# 2. 检测处理图片
|
||||||
|
for kw in self.process_keywords:
|
||||||
|
if kw in message_lower:
|
||||||
|
logger.info(f"检测到处理图片意图:{kw}")
|
||||||
|
return ("process_image", 0.9)
|
||||||
|
|
||||||
|
# 3. 检测无法处理
|
||||||
|
for kw in self.unable_keywords:
|
||||||
|
if kw in message_lower:
|
||||||
|
logger.info(f"检测到无法处理:{kw}")
|
||||||
|
return ("transfer_human", 0.9)
|
||||||
|
|
||||||
|
# 默认返回未知
|
||||||
|
return ("unknown", 0.5)
|
||||||
|
|
||||||
|
def should_find_image(self, message: str) -> bool:
|
||||||
|
"""是否应该查找图片"""
|
||||||
|
workflow_type, _ = self.detect_workflow(message)
|
||||||
|
return workflow_type == "find_image"
|
||||||
|
|
||||||
|
def should_process_image(self, message: str) -> bool:
|
||||||
|
"""是否应该处理图片"""
|
||||||
|
workflow_type, _ = self.detect_workflow(message)
|
||||||
|
return workflow_type == "process_image"
|
||||||
|
|
||||||
|
def should_transfer_human(self, message: str) -> bool:
|
||||||
|
"""是否应该转人工"""
|
||||||
|
workflow_type, _ = self.detect_workflow(message)
|
||||||
|
return workflow_type == "transfer_human"
|
||||||
|
|
||||||
|
|
||||||
|
# 单例
|
||||||
|
_router: Optional[WorkflowRouter] = None
|
||||||
|
|
||||||
|
def get_workflow_router() -> WorkflowRouter:
|
||||||
|
"""获取工作流程路由器单例"""
|
||||||
|
global _router
|
||||||
|
if _router is None:
|
||||||
|
_router = WorkflowRouter()
|
||||||
|
return _router
|
||||||
0
customer_db/customers.json
Normal file → Executable file
0
customer_db/customers.json
Normal file → Executable file
0
customer_db/schema.json
Normal file → Executable file
0
customer_db/schema.json
Normal file → Executable file
0
db/__init__.py
Normal file → Executable file
0
db/__init__.py
Normal file → Executable file
0
db/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/chat_log_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/chat_log_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/customer_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/customer_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/deal_outcome_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/deal_outcome_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/designer_roster_db.cpython-310.pyc
Normal file → Executable file
0
db/__pycache__/designer_roster_db.cpython-310.pyc
Normal file → Executable file
0
db/chat_log_db.py
Normal file → Executable file
0
db/chat_log_db.py
Normal file → Executable file
0
db/chat_log_db/chats.db
Normal file → Executable file
0
db/chat_log_db/chats.db
Normal file → Executable file
0
db/customer_db.py
Normal file → Executable file
0
db/customer_db.py
Normal file → Executable file
0
db/deal_outcome_db.py
Normal file → Executable file
0
db/deal_outcome_db.py
Normal file → Executable file
0
db/deal_outcome_db/outcomes.db
Normal file → Executable file
0
db/deal_outcome_db/outcomes.db
Normal file → Executable file
0
db/designer_roster_db.py
Normal file → Executable file
0
db/designer_roster_db.py
Normal file → Executable file
0
db/designer_roster_db/roster.db
Normal file → Executable file
0
db/designer_roster_db/roster.db
Normal file → Executable file
412
db/image_tasks_db.py
Normal file
412
db/image_tasks_db.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
图片任务数据库管理
|
||||||
|
支持客户后续增加需求细节
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TaskStatus(Enum):
|
||||||
|
"""任务状态"""
|
||||||
|
PENDING = "pending" # 待付款
|
||||||
|
PAID = "paid" # 已付款,待处理
|
||||||
|
PROCESSING = "processing" # 处理中
|
||||||
|
AWAITING_CONFIRM = "awaiting_confirm" # 已完成,待客户确认
|
||||||
|
COMPLETED = "completed" # 已完成
|
||||||
|
FAILED = "failed" # 失败
|
||||||
|
CANCELLED = "cancelled" # 已取消
|
||||||
|
|
||||||
|
class ImageTaskManager:
|
||||||
|
"""图片任务管理器"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = None):
|
||||||
|
if db_path is None:
|
||||||
|
db_path = Path(__file__).parent / "image_tasks.db"
|
||||||
|
|
||||||
|
self.db_path = db_path
|
||||||
|
self._init_db()
|
||||||
|
logger.info(f"图片任务管理器初始化完成,数据库:{self.db_path}")
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""初始化数据库"""
|
||||||
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 创建图片任务表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS image_tasks (
|
||||||
|
task_id TEXT PRIMARY KEY,
|
||||||
|
customer_id TEXT NOT NULL,
|
||||||
|
customer_name TEXT,
|
||||||
|
original_image TEXT NOT NULL,
|
||||||
|
operation TEXT DEFAULT 'enhance',
|
||||||
|
requirements TEXT, -- JSON 格式:复杂度、比例、透视等
|
||||||
|
customer_notes TEXT, -- 客户备注/需求细节
|
||||||
|
status TEXT DEFAULT 'pending',
|
||||||
|
created_at TEXT,
|
||||||
|
paid_at TEXT,
|
||||||
|
started_at TEXT,
|
||||||
|
completed_at TEXT,
|
||||||
|
result_image TEXT,
|
||||||
|
error_message TEXT,
|
||||||
|
retry_count INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- 店铺信息
|
||||||
|
acc_id TEXT,
|
||||||
|
acc_type TEXT DEFAULT 'AliWorkbench'
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 创建需求变更记录表(支持客户后续增加需求)
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id TEXT NOT NULL,
|
||||||
|
change_type TEXT, -- add_note/modify_operation/add_requirement
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_at TEXT,
|
||||||
|
changed_by TEXT, -- customer/staff
|
||||||
|
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 创建索引
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON image_tasks(customer_id)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
logger.info("数据库表初始化完成")
|
||||||
|
|
||||||
|
def _get_conn(self):
|
||||||
|
"""获取数据库连接"""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def create_task(self, task_id: str, customer_id: str, customer_name: str,
|
||||||
|
original_image: str, operation: str = 'enhance',
|
||||||
|
requirements: dict = None, acc_id: str = '', acc_type: str = 'AliWorkbench') -> bool:
|
||||||
|
"""创建图片任务"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
requirements_json = json.dumps(requirements) if requirements else None
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO image_tasks (
|
||||||
|
task_id, customer_id, customer_name, original_image,
|
||||||
|
operation, requirements, customer_notes, status,
|
||||||
|
created_at, acc_id, acc_type
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
task_id,
|
||||||
|
customer_id,
|
||||||
|
customer_name,
|
||||||
|
original_image,
|
||||||
|
operation,
|
||||||
|
requirements_json,
|
||||||
|
'', # 初始备注为空
|
||||||
|
TaskStatus.PENDING.value,
|
||||||
|
datetime.now().isoformat(),
|
||||||
|
acc_id,
|
||||||
|
acc_type
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"图片任务创建成功:{task_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建图片任务失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_task(self, task_id: str) -> Optional[dict]:
|
||||||
|
"""查询任务"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('SELECT * FROM image_tasks WHERE task_id = ?', (task_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
task = dict(row)
|
||||||
|
# 解析 JSON 字段
|
||||||
|
if task.get('requirements'):
|
||||||
|
task['requirements'] = json.loads(task['requirements'])
|
||||||
|
return task
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询任务失败:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_customer_tasks(self, customer_id: str, status: str = None) -> List[dict]:
|
||||||
|
"""查询客户的任务列表"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if status:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM image_tasks
|
||||||
|
WHERE customer_id = ? AND status = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
''', (customer_id, status))
|
||||||
|
else:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM image_tasks
|
||||||
|
WHERE customer_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
''', (customer_id,))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for row in rows:
|
||||||
|
task = dict(row)
|
||||||
|
if task.get('requirements'):
|
||||||
|
task['requirements'] = json.loads(task['requirements'])
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询客户任务失败:{e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def update_status(self, task_id: str, status: TaskStatus):
|
||||||
|
"""更新任务状态"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
updates = ['status = ?']
|
||||||
|
params = [status.value]
|
||||||
|
|
||||||
|
# 根据状态设置时间
|
||||||
|
if status == TaskStatus.PAID:
|
||||||
|
updates.append('paid_at = ?')
|
||||||
|
params.append(datetime.now().isoformat())
|
||||||
|
elif status == TaskStatus.PROCESSING:
|
||||||
|
updates.append('started_at = ?')
|
||||||
|
params.append(datetime.now().isoformat())
|
||||||
|
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
|
||||||
|
updates.append('completed_at = ?')
|
||||||
|
params.append(datetime.now().isoformat())
|
||||||
|
|
||||||
|
params.append(task_id)
|
||||||
|
|
||||||
|
cursor.execute(f'''
|
||||||
|
UPDATE image_tasks
|
||||||
|
SET {', '.join(updates)}
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', params)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"任务状态更新:{task_id} -> {status.value}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新任务状态失败:{e}")
|
||||||
|
|
||||||
|
def update_result(self, task_id: str, result_image: str, error_message: str = None):
|
||||||
|
"""更新处理结果"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE image_tasks
|
||||||
|
SET result_image = ?, error_message = ?
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', (result_image, error_message, task_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"任务结果更新:{task_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新任务结果失败:{e}")
|
||||||
|
|
||||||
|
def add_customer_note(self, task_id: str, note: str, changed_by: str = 'customer') -> bool:
|
||||||
|
"""
|
||||||
|
客户添加需求备注(支持后续增加细节)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务 ID
|
||||||
|
note: 备注内容
|
||||||
|
changed_by: 修改者(customer/staff)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 获取旧备注
|
||||||
|
cursor.execute('SELECT customer_notes FROM image_tasks WHERE task_id = ?', (task_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
old_note = row['customer_notes'] if row else ''
|
||||||
|
|
||||||
|
# 更新备注
|
||||||
|
new_note = f"{old_note}\n[{datetime.now().strftime('%m-%d %H:%M')}] {note}" if old_note else f"[{datetime.now().strftime('%m-%d %H:%M')}] {note}"
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE image_tasks
|
||||||
|
SET customer_notes = ?
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', (new_note, task_id))
|
||||||
|
|
||||||
|
# 记录变更历史
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO task_requirement_changes (
|
||||||
|
task_id, change_type, old_value, new_value, changed_at, changed_by
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
task_id,
|
||||||
|
'add_note',
|
||||||
|
old_note or '无',
|
||||||
|
note,
|
||||||
|
datetime.now().isoformat(),
|
||||||
|
changed_by
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"客户添加备注成功:{task_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"添加客户备注失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def modify_operation(self, task_id: str, new_operation: str, changed_by: str = 'customer') -> bool:
|
||||||
|
"""
|
||||||
|
修改操作类型(客户后续修改需求)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务 ID
|
||||||
|
new_operation: 新操作类型
|
||||||
|
changed_by: 修改者
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 获取旧操作
|
||||||
|
cursor.execute('SELECT operation FROM image_tasks WHERE task_id = ?', (task_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
old_operation = row['operation'] if row else ''
|
||||||
|
|
||||||
|
# 更新操作
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE image_tasks
|
||||||
|
SET operation = ?
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', (new_operation, task_id))
|
||||||
|
|
||||||
|
# 记录变更历史
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO task_requirement_changes (
|
||||||
|
task_id, change_type, old_value, new_value, changed_at, changed_by
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
task_id,
|
||||||
|
'modify_operation',
|
||||||
|
old_operation,
|
||||||
|
new_operation,
|
||||||
|
datetime.now().isoformat(),
|
||||||
|
changed_by
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"修改操作类型成功:{task_id} -> {new_operation}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"修改操作类型失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_requirement_history(self, task_id: str) -> List[dict]:
|
||||||
|
"""获取需求变更历史"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM task_requirement_changes
|
||||||
|
WHERE task_id = ?
|
||||||
|
ORDER BY changed_at DESC
|
||||||
|
''', (task_id,))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询需求历史失败:{e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_pending_tasks(self) -> List[dict]:
|
||||||
|
"""获取所有待处理任务"""
|
||||||
|
return self.get_customer_tasks('', 'pending')
|
||||||
|
|
||||||
|
def increment_retry(self, task_id: str) -> int:
|
||||||
|
"""增加重试次数"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE image_tasks
|
||||||
|
SET retry_count = retry_count + 1
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', (task_id,))
|
||||||
|
|
||||||
|
cursor.execute('SELECT retry_count FROM image_tasks WHERE task_id = ?', (task_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return row['retry_count'] if row else 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"增加重试次数失败:{e}")
|
||||||
|
return 999
|
||||||
|
|
||||||
|
# 单例
|
||||||
|
_task_manager: Optional[ImageTaskManager] = None
|
||||||
|
|
||||||
|
def get_image_task_manager() -> ImageTaskManager:
|
||||||
|
"""获取图片任务管理器单例"""
|
||||||
|
global _task_manager
|
||||||
|
if _task_manager is None:
|
||||||
|
_task_manager = ImageTaskManager()
|
||||||
|
return _task_manager
|
||||||
BIN
db/task_db/__pycache__/task_model.cpython-311.pyc
Normal file
BIN
db/task_db/__pycache__/task_model.cpython-311.pyc
Normal file
Binary file not shown.
321
db/task_db/task_model.py
Normal file
321
db/task_db/task_model.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
天网任务数据模型
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, List
|
||||||
|
from pathlib import Path
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TaskStatus(Enum):
|
||||||
|
"""任务状态"""
|
||||||
|
PENDING = "pending" # 待触发
|
||||||
|
WAITING = "waiting" # 等待触发
|
||||||
|
RUNNING = "running" # 执行中
|
||||||
|
COMPLETED = "completed" # 已完成
|
||||||
|
FAILED = "failed" # 失败
|
||||||
|
CANCELLED = "cancelled" # 已取消
|
||||||
|
|
||||||
|
class TaskPriority(Enum):
|
||||||
|
"""任务优先级"""
|
||||||
|
NORMAL = "normal"
|
||||||
|
HIGH = "high"
|
||||||
|
URGENT = "urgent"
|
||||||
|
|
||||||
|
class TaskManager:
|
||||||
|
"""任务管理器 - SQLite 存储"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = None):
|
||||||
|
if db_path is None:
|
||||||
|
db_path = Path(__file__).parent / "tasks.db"
|
||||||
|
|
||||||
|
self.db_path = db_path
|
||||||
|
self._init_db()
|
||||||
|
logger.info(f"任务管理器初始化完成,数据库:{self.db_path}")
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""初始化数据库"""
|
||||||
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 创建任务表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
task_id TEXT PRIMARY KEY,
|
||||||
|
specified_customer_id TEXT,
|
||||||
|
specified_customer_name TEXT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
customer_name TEXT,
|
||||||
|
customer_id TEXT,
|
||||||
|
trigger_type TEXT,
|
||||||
|
trigger_keyword TEXT,
|
||||||
|
trigger_keywords TEXT, -- JSON array
|
||||||
|
action_type TEXT,
|
||||||
|
action_file_url TEXT,
|
||||||
|
action_message TEXT,
|
||||||
|
priority TEXT DEFAULT 'normal',
|
||||||
|
timeout_hours INTEGER DEFAULT 24,
|
||||||
|
status TEXT DEFAULT 'pending',
|
||||||
|
retry_count INTEGER DEFAULT 0,
|
||||||
|
max_retry INTEGER DEFAULT 3,
|
||||||
|
created_at TEXT,
|
||||||
|
created_by TEXT,
|
||||||
|
triggered_at TEXT,
|
||||||
|
completed_at TEXT,
|
||||||
|
error_message TEXT,
|
||||||
|
result TEXT -- JSON
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 创建索引
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON tasks(status)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON tasks(customer_id)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON tasks(created_at)')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
logger.info("数据库表初始化完成")
|
||||||
|
|
||||||
|
def _get_conn(self):
|
||||||
|
"""获取数据库连接"""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def add_task(self, task: dict) -> bool:
|
||||||
|
"""添加任务"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 处理 keywords 数组
|
||||||
|
trigger_keywords = task.get('trigger', {}).get('keywords', [])
|
||||||
|
if isinstance(trigger_keywords, list):
|
||||||
|
trigger_keywords = json.dumps(trigger_keywords)
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT OR 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
task.get('task_id'),
|
||||||
|
task.get('customer', {}).get('id'),
|
||||||
|
task.get('customer', {}).get('name'),
|
||||||
|
task.get('type'),
|
||||||
|
task.get('customer', {}).get('name'),
|
||||||
|
task.get('customer', {}).get('id'),
|
||||||
|
task.get('trigger', {}).get('type'),
|
||||||
|
task.get('trigger', {}).get('keyword'),
|
||||||
|
trigger_keywords,
|
||||||
|
task.get('action', {}).get('type'),
|
||||||
|
task.get('action', {}).get('file_url'),
|
||||||
|
task.get('action', {}).get('message'),
|
||||||
|
task.get('priority', 'normal'),
|
||||||
|
task.get('timeout_hours', 24),
|
||||||
|
task.get('status', 'pending'),
|
||||||
|
task.get('retry_count', 0),
|
||||||
|
task.get('max_retry', 3),
|
||||||
|
task.get('created_at', datetime.now().isoformat()),
|
||||||
|
task.get('created_by')
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"任务添加成功:{task.get('task_id')}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"添加任务失败:{e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_task(self, task_id: str) -> Optional[dict]:
|
||||||
|
"""查询任务"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('SELECT * FROM tasks WHERE task_id = ?', (task_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
return dict(row)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询任务失败:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_task_status(self, task_id: str, status: TaskStatus, error_message: str = None, result: dict = None):
|
||||||
|
"""更新任务状态"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
updates = ['status = ?']
|
||||||
|
params = [status.value]
|
||||||
|
|
||||||
|
if status == TaskStatus.RUNNING:
|
||||||
|
updates.append('triggered_at = ?')
|
||||||
|
params.append(datetime.now().isoformat())
|
||||||
|
|
||||||
|
if status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
|
||||||
|
updates.append('completed_at = ?')
|
||||||
|
params.append(datetime.now().isoformat())
|
||||||
|
|
||||||
|
if error_message:
|
||||||
|
updates.append('error_message = ?')
|
||||||
|
params.append(error_message)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
updates.append('result = ?')
|
||||||
|
params.append(json.dumps(result))
|
||||||
|
|
||||||
|
params.append(task_id)
|
||||||
|
|
||||||
|
cursor.execute(f'''
|
||||||
|
UPDATE tasks
|
||||||
|
SET {', '.join(updates)}
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', params)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"任务状态更新:{task_id} -> {status.value}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新任务状态失败:{e}")
|
||||||
|
|
||||||
|
def increment_retry(self, task_id: str) -> int:
|
||||||
|
"""增加重试次数,返回当前重试次数"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE tasks
|
||||||
|
SET retry_count = retry_count + 1
|
||||||
|
WHERE task_id = ?
|
||||||
|
''', (task_id,))
|
||||||
|
|
||||||
|
cursor.execute('SELECT retry_count, max_retry FROM tasks WHERE task_id = ?', (task_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
return row['retry_count']
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"增加重试次数失败:{e}")
|
||||||
|
return 999 # 超过最大重试次数
|
||||||
|
|
||||||
|
def get_pending_tasks(self, customer_id: str = None) -> List[dict]:
|
||||||
|
"""获取待触发的任务"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if customer_id:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM tasks
|
||||||
|
WHERE status = 'pending'
|
||||||
|
AND customer_id = ?
|
||||||
|
ORDER BY
|
||||||
|
CASE priority
|
||||||
|
WHEN 'urgent' THEN 1
|
||||||
|
WHEN 'high' THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
created_at
|
||||||
|
''', (customer_id,))
|
||||||
|
else:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM tasks
|
||||||
|
WHERE status = 'pending'
|
||||||
|
ORDER BY
|
||||||
|
CASE priority
|
||||||
|
WHEN 'urgent' THEN 1
|
||||||
|
WHEN 'high' THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
created_at
|
||||||
|
''')
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取待触发任务失败:{e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_timeout_tasks(self) -> List[dict]:
|
||||||
|
"""获取超时任务"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM tasks
|
||||||
|
WHERE status = 'pending'
|
||||||
|
AND datetime(created_at, '+' || timeout_hours || ' hours') < datetime('now')
|
||||||
|
''')
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取超时任务失败:{e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def cancel_task(self, task_id: str, reason: str = None):
|
||||||
|
"""取消任务"""
|
||||||
|
self.update_task_status(task_id, TaskStatus.CANCELLED, error_message=reason)
|
||||||
|
logger.info(f"任务已取消:{task_id}")
|
||||||
|
|
||||||
|
def get_statistics(self) -> dict:
|
||||||
|
"""获取任务统计"""
|
||||||
|
try:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
for status in TaskStatus:
|
||||||
|
cursor.execute('SELECT COUNT(*) FROM tasks WHERE status = ?', (status.value,))
|
||||||
|
stats[status.value] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取统计失败:{e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# 单例
|
||||||
|
_task_manager: Optional[TaskManager] = None
|
||||||
|
|
||||||
|
def get_task_manager() -> TaskManager:
|
||||||
|
"""获取任务管理器单例"""
|
||||||
|
global _task_manager
|
||||||
|
if _task_manager is None:
|
||||||
|
_task_manager = TaskManager()
|
||||||
|
return _task_manager
|
||||||
BIN
db/task_db/tasks.db
Normal file
BIN
db/task_db/tasks.db
Normal file
Binary file not shown.
101
features/price_negotiation_with_registration.md
Normal file
101
features/price_negotiation_with_registration.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# 压价引导注册功能
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
当客户觉得价格太贵时,AI 客服会分两步应对:
|
||||||
|
|
||||||
|
### 第一步:直接让价
|
||||||
|
- 只让价一次
|
||||||
|
- 不低于底线价格
|
||||||
|
- 话术自然多变
|
||||||
|
|
||||||
|
### 第二步:引导客户自己去注册
|
||||||
|
如果让价后客户还是嫌贵,引导客户**自己去注册 xinhui.cloud**获取新用户余额。
|
||||||
|
|
||||||
|
## ⚠️ 核心要求
|
||||||
|
|
||||||
|
1. **每个话术都必须带网站地址 xinhui.cloud**
|
||||||
|
2. **强调客户自己去弄**(自己去操作/自己下单/自己弄)
|
||||||
|
3. 语气直接,不要软
|
||||||
|
4. 不要说具体金额
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
客户消息包含以下关键词:
|
||||||
|
- "贵"
|
||||||
|
- "有点贵"
|
||||||
|
- "太贵了"
|
||||||
|
- "便宜点"
|
||||||
|
- "少点"
|
||||||
|
- "打折"
|
||||||
|
|
||||||
|
## 话术示例(每个都带网站)
|
||||||
|
|
||||||
|
### 让价话术
|
||||||
|
- "那给你少点,XX 吧"
|
||||||
|
- "最低 XX 了"
|
||||||
|
- "给你个优惠价 XX"
|
||||||
|
- "看你诚心要,XX 给你"
|
||||||
|
|
||||||
|
### 注册引导话术(让价后仍嫌贵时使用)
|
||||||
|
**每个话术都必须包含 xinhui.cloud 网址!**
|
||||||
|
|
||||||
|
1. 「嫌贵就去 xinhui.cloud 注册个号,自己有余额可以弄」
|
||||||
|
2. 「xinhui.cloud 去注册下,新用户送体验金,自己弄便宜」
|
||||||
|
3. 「去 xinhui.cloud 注册个账号,里面有余额,自己下单划算」
|
||||||
|
4. 「自己上 xinhui.cloud 注册,新人有福利,比直接找我便宜」
|
||||||
|
5. 「xinhui.cloud 去注册个号,自己充值有优惠,自己弄吧」
|
||||||
|
6. 「嫌贵就自己去 xinhui.cloud 弄,注册送余额,比找我划算」
|
||||||
|
7. 「xinhui.cloud 注册个号,自己充值自己弄,便宜些」
|
||||||
|
8. 「去 xinhui.cloud 注册,里面有余额可以用,自己操作吧」
|
||||||
|
|
||||||
|
## 对话示例
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:这个多少钱?
|
||||||
|
AI: 20 元
|
||||||
|
|
||||||
|
客户:太贵了,便宜点吧
|
||||||
|
AI: 那给你少点,15 吧
|
||||||
|
|
||||||
|
客户:还是有点贵
|
||||||
|
AI: 嫌贵就去 xinhui.cloud 注册个号,自己有余额可以弄
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:有点贵啊
|
||||||
|
AI: 最低 15 了
|
||||||
|
|
||||||
|
客户:还是贵
|
||||||
|
AI: xinhui.cloud 去注册下,新用户送体验金,自己弄便宜
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. ⚠️ **每个话术都必须带 xinhui.cloud 网址**
|
||||||
|
2. ⚠️ **强调"自己去弄""自己操作""自己下单"**
|
||||||
|
3. 不说具体余额金额
|
||||||
|
4. 每次选择不同话术变体
|
||||||
|
5. 只在让价后仍嫌贵时使用
|
||||||
|
6. 语气直接,不要软
|
||||||
|
|
||||||
|
## 代码位置
|
||||||
|
|
||||||
|
- 技能文档:`/skills/customer-service/customer-skill.md`
|
||||||
|
- Agent 代码:`/core/pydantic_ai_agent.py` 第 1745 行
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 AI 客服系统
|
||||||
|
systemctl start ai-cs
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
journalctl -u ai-cs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
模拟客户消息:"太贵了,便宜点吧"
|
||||||
|
|
||||||
|
预期 AI 回复:先让价,如仍嫌贵则引导客户自己去 xinhui.cloud 注册
|
||||||
129
features/risk_customer_detection.md
Normal file
129
features/risk_customer_detection.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# 找茬客户识别功能
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
识别高风险找茬客户,建议 AI 客服不做这单,直接拒绝或转人工。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 高风险信号(识别 3 个以上→拒绝接单)
|
||||||
|
|
||||||
|
| 信号 | 示例 | 风险等级 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **下单后立即申请退款** | 刚拍下就退款 0.88 元 | 🔴 高 |
|
||||||
|
| **从高价砍到低价** | 30 元砍到 10 元 | 🔴 高 |
|
||||||
|
| **反复问退款(2 次+)** | "不满意可以退吗"×2 | 🔴 高 |
|
||||||
|
| **质疑服务内容** | "源文件还是什么" | 🟡 中 |
|
||||||
|
| **质疑价值** | "就一张图片" | 🟡 中 |
|
||||||
|
| **想占便宜** | "小一点就快一点的嘛" | 🟡 中 |
|
||||||
|
| **重复问同一问题** | 同样的问题问 2 次+ | 🔴 高 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 应对策略
|
||||||
|
|
||||||
|
### 识别到 3 个以上高风险信号
|
||||||
|
|
||||||
|
**直接拒绝接单**
|
||||||
|
|
||||||
|
**话术示例**:
|
||||||
|
- 「不好意思,这单做不了」
|
||||||
|
- 「去别家做吧」
|
||||||
|
- 「这单接不了,你找别人吧」
|
||||||
|
|
||||||
|
**不要**:
|
||||||
|
- ❌ 不要解释原因
|
||||||
|
- ❌ 不要讨价还价
|
||||||
|
- ❌ 不要妥协
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 真实案例分析
|
||||||
|
|
||||||
|
### 案例:高风险找茬客户
|
||||||
|
|
||||||
|
**对话记录**:
|
||||||
|
```
|
||||||
|
客户:这个原图有吗
|
||||||
|
AI: 没问题,拍下安排。
|
||||||
|
客户:拍啦
|
||||||
|
[客户立即申请退款 0.88 元]
|
||||||
|
客户:太贵了,10 元
|
||||||
|
AI: 30
|
||||||
|
[退款成功]
|
||||||
|
客户:要多久?
|
||||||
|
客户:300×50cm
|
||||||
|
AI: 1 个小时
|
||||||
|
客户:小一点就快一点的嘛
|
||||||
|
客户:20 可以吗
|
||||||
|
AI: [不回应]
|
||||||
|
客户:25
|
||||||
|
客户:源文件还是什么?
|
||||||
|
客户:就一张图片
|
||||||
|
客户:不满意可以退吗
|
||||||
|
客户:不满意可以退吗(第 2 次问)
|
||||||
|
AI: 去别家做吧
|
||||||
|
```
|
||||||
|
|
||||||
|
**风险信号识别**:
|
||||||
|
1. ✅ 下单后立即申请退款
|
||||||
|
2. ✅ 从 30 砍到 10 元
|
||||||
|
3. ✅ 质疑价值("就一张图片")
|
||||||
|
4. ✅ 想占便宜("小一点就快一点")
|
||||||
|
5. ✅ 重复问退款(2 次)
|
||||||
|
|
||||||
|
**结论**:5 个高风险信号 → **拒绝接单** ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码位置
|
||||||
|
|
||||||
|
- Agent 代码:`/core/pydantic_ai_agent.py` - 找茬客户识别规则
|
||||||
|
- 技能文档:`/skills/customer-service/customer-skill.md` - 客服话术指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 模拟高风险客户
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 AI 客服
|
||||||
|
systemctl start ai-cs
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
journalctl -u ai-cs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**模拟对话**:
|
||||||
|
```
|
||||||
|
客户:20 可以吗
|
||||||
|
AI: 最低 30
|
||||||
|
客户:25
|
||||||
|
客户:不满意可以退吗
|
||||||
|
客户:不满意可以退吗(第 2 次)
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期 AI 回复**:
|
||||||
|
- 「不好意思,这单做不了」
|
||||||
|
- 「去别家做吧」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **识别 3 个以上信号才拒绝**:不要误伤正常客户
|
||||||
|
2. **话术简洁**:不要解释原因
|
||||||
|
3. **态度坚定**:不要妥协
|
||||||
|
4. **不调用报价工具**:直接拒绝
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与转人工的区别
|
||||||
|
|
||||||
|
| 情况 | 处理方式 |
|
||||||
|
|------|----------|
|
||||||
|
| 退款/投诉/情绪激动 | 转人工 |
|
||||||
|
| 找茬客户(3 个+信号) | 直接拒绝 |
|
||||||
|
| 敏感内容 | 直接拒绝 |
|
||||||
|
|
||||||
158
features/text_surcharge.md
Normal file
158
features/text_surcharge.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 文字加价功能
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
当识别到图片含有很多文字时,AI 客服系统会自动提高报价,不能低价。
|
||||||
|
|
||||||
|
**核心原则**:有文字跟没文字是两个价格!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 价格规则
|
||||||
|
|
||||||
|
### 含文字很多时
|
||||||
|
|
||||||
|
| 原复杂度 | 原价区间 | 加价后 | 加价后区间 |
|
||||||
|
|---------|---------|--------|----------|
|
||||||
|
| simple | 10-15 元 | → normal | 15-20 元 |
|
||||||
|
| normal | 15-20 元 | → complex | 20-25 元 |
|
||||||
|
| complex | 20-25 元 | 保持不变 | 20-25 元 |
|
||||||
|
| hard | 25-30 元 | 保持不变 | 25-30 元 |
|
||||||
|
|
||||||
|
### 判断标准
|
||||||
|
|
||||||
|
**含文字很多**(需要加价):
|
||||||
|
- ✅ 图片里有大量小字
|
||||||
|
- ✅ 需要精细保留文字清晰度
|
||||||
|
- ✅ 文字需要清晰化处理
|
||||||
|
|
||||||
|
**不含文字或文字很少**(不加价):
|
||||||
|
- ❌ 图片干净,没文字
|
||||||
|
- ❌ 只有零星几个大字
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码修改
|
||||||
|
|
||||||
|
### 1. image_analyzer.py
|
||||||
|
|
||||||
|
文件:`/root/ai_customer_service/ai_cs/image/image_analyzer.py`
|
||||||
|
|
||||||
|
**修改位置**:第 528-542 行
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 【重要】含文字很多时,不能低价,必须 complex 起步(20 元以上)
|
||||||
|
# 有文字跟没文字是两个价格
|
||||||
|
if has_text == "yes":
|
||||||
|
if complexity == "simple":
|
||||||
|
# 简单但含文字 → 提升到 normal 价格
|
||||||
|
price_min, price_max = self.PRICE_MAP["normal"]
|
||||||
|
reason = "含文字,需精细处理"
|
||||||
|
elif complexity == "normal":
|
||||||
|
# normal 含文字 → 提升到 complex 价格
|
||||||
|
price_min, price_max = self.PRICE_MAP["complex"]
|
||||||
|
reason = "含文字,需精细处理"
|
||||||
|
# complex/hard 保持原价,已经够高
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. pydantic_ai_agent.py
|
||||||
|
|
||||||
|
文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py`
|
||||||
|
|
||||||
|
**修改位置**:第 863-869 行
|
||||||
|
|
||||||
|
```python
|
||||||
|
【文字加价规则】⚠️ 重要
|
||||||
|
- 含文字很多时不能低价,有文字跟没文字是两个价格
|
||||||
|
- 含文字的图必须 complex 起步(20 元以上)
|
||||||
|
- 客户嫌贵时明确告知:「有文字跟没文字是两个价格」
|
||||||
|
- 简单图但含文字 → normal 价格(15-20 元)
|
||||||
|
- normal 图含文字 → complex 价格(20-25 元)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. customer-skill.md
|
||||||
|
|
||||||
|
文件:`/root/ai_customer_service/ai_cs/skills/customer-service/customer-skill.md`
|
||||||
|
|
||||||
|
**新增章节**:⑫ 文字加价规则
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 对话示例
|
||||||
|
|
||||||
|
### 示例 1:简单图但含文字
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:[发送一张含文字的图片]
|
||||||
|
AI: 图里有不少字,要精细处理,20 元
|
||||||
|
客户:这么贵
|
||||||
|
AI: 有文字的图跟没文字的价格不一样,已经是最低价了
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2:normal 图含文字
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:这个多少钱?
|
||||||
|
AI: 25 元
|
||||||
|
客户:太贵了
|
||||||
|
AI: 含文字的图要精细处理,成本更高,跟没文字的价格不一样
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3:客户问为什么贵
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:这个为什么比那个贵?
|
||||||
|
AI: 这个图含文字,需要精细处理,有文字跟没文字是两个价格
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 话术要点
|
||||||
|
|
||||||
|
### 必须包含的信息
|
||||||
|
1. ✅ 明确告知「有文字跟没文字是两个价格」
|
||||||
|
2. ✅ 说明「文字需要精细处理」
|
||||||
|
3. ✅ 强调「已经是最低价」
|
||||||
|
|
||||||
|
### 常用话术
|
||||||
|
- 「有文字跟没文字是两个价格」
|
||||||
|
- 「文字处理要精细,成本高」
|
||||||
|
- 「含文字的图都这个价」
|
||||||
|
- 「文字越多越贵」
|
||||||
|
- 「已经是最低价了,含文字的都这样」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 测试步骤
|
||||||
|
|
||||||
|
1. **准备测试图片**
|
||||||
|
- 一张含有很多小字的图片
|
||||||
|
- 一张干净的图片(对比)
|
||||||
|
|
||||||
|
2. **发送给 AI 客服**
|
||||||
|
```bash
|
||||||
|
systemctl start ai-cs
|
||||||
|
journalctl -u ai-cs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证报价**
|
||||||
|
- 含文字图片:应该报价 20 元以上
|
||||||
|
- 干净图片:正常报价 10-15 元
|
||||||
|
|
||||||
|
### 预期结果
|
||||||
|
|
||||||
|
- ✅ 含文字图片自动加价
|
||||||
|
- ✅ 客户嫌贵时明确告知「有文字跟没文字是两个价格」
|
||||||
|
- ✅ 价格必须为 5 的整数倍
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **只针对小字**:大字不加价
|
||||||
|
2. **价格必须为 5 的倍数**:15/20/25/30
|
||||||
|
3. **明确告知客户**:不要偷偷加价
|
||||||
|
4. **坚持原则**:客户嫌贵也要说明原因
|
||||||
|
|
||||||
137
features/tuhui_upload.md
Normal file
137
features/tuhui_upload.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 图绘上传功能
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
AI 客服处理完图片后,上传到图绘平台并定价,告诉客户「这个图片是找到的」,然后问客户要下载还是购买。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 重要话术
|
||||||
|
|
||||||
|
**必须说**:
|
||||||
|
- ✅ 「找到了!图片已经上传到图绘平台了」
|
||||||
|
- ✅ 「你是要下载还是直接购买?」
|
||||||
|
- ✅ 「价格 XX 元」
|
||||||
|
|
||||||
|
**不要说**:
|
||||||
|
- ❌ 「处理好了」
|
||||||
|
- ❌ 「修复好了」
|
||||||
|
- ❌ 「AI 修复的」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
1. **客户发图** → AI 客服收到图片
|
||||||
|
2. **图片处理** → 调用图片处理工具
|
||||||
|
3. **上传图绘** → 调用 `upload_to_tuhui_platform()` 工具
|
||||||
|
4. **返回话术** → 「找到了!...你是要下载还是购买?」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 对话示例
|
||||||
|
|
||||||
|
### 示例 1:标准流程
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:[发送图片]
|
||||||
|
AI: 收到了,稍等...
|
||||||
|
|
||||||
|
[AI 调用图片处理工具]
|
||||||
|
[AI 调用 upload_to_tuhui_platform 工具]
|
||||||
|
|
||||||
|
AI: 找到了!图片已经上传到图绘平台了,作品 ID: 123
|
||||||
|
AI: 你是要下载还是直接购买?价格 20 元。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2:客户选择下载
|
||||||
|
|
||||||
|
```
|
||||||
|
AI: 找到了!图片已经上传到图绘平台了
|
||||||
|
AI: 你是要下载还是直接购买?价格 20 元。
|
||||||
|
|
||||||
|
客户:下载
|
||||||
|
AI: 好的,拍下后就可以下载了
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3:客户选择购买
|
||||||
|
|
||||||
|
```
|
||||||
|
AI: 你是要下载还是直接购买?价格 20 元。
|
||||||
|
|
||||||
|
客户:购买
|
||||||
|
AI: 好的,拍下就行,付款后发你高清原图
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 4:客户问在哪里
|
||||||
|
|
||||||
|
```
|
||||||
|
客户:弄好了吗
|
||||||
|
AI: 找到了,已经上传到图绘平台了
|
||||||
|
AI: 作品 ID: 123,你是要下载还是购买?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### .env 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 图绘平台配置
|
||||||
|
TUHUI_BASE_URL=http://127.0.0.1:8002
|
||||||
|
TUHUI_PHONE=17520145271 # 图绘账号手机号
|
||||||
|
TUHUI_PASSWORD=zuowei1216 # 图绘账号密码
|
||||||
|
TUHUI_DEFAULT_PRICE=20 # 默认定价(元)
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI Agent 工具
|
||||||
|
|
||||||
|
```python
|
||||||
|
@self.agent.tool
|
||||||
|
async def upload_to_tuhui_platform(
|
||||||
|
ctx: RunContext[AgentDeps],
|
||||||
|
image_path: str,
|
||||||
|
title: str,
|
||||||
|
price: int = 20
|
||||||
|
) -> str:
|
||||||
|
"""将处理好的图片上传到图绘平台并定价"""
|
||||||
|
# 返回:「找到了!图片已经上传到图绘平台了,作品 ID: 123。你是要下载还是直接购买?价格 20 元。」
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码位置
|
||||||
|
|
||||||
|
- 上传服务:`/services/service_tuhui_upload.py`
|
||||||
|
- Agent 工具:`/core/pydantic_ai_agent.py` 第 220 行
|
||||||
|
- 客服话术:`/skills/customer-service/customer-skill.md` 第⑭节
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. ⚠️ **必须说「找到了」**,不要说「处理好了」
|
||||||
|
2. ⚠️ **必须问「要下载还是购买」**
|
||||||
|
3. ⚠️ **必须说价格**
|
||||||
|
4. ✅ 图片是"找到的",不是"处理的"
|
||||||
|
5. ✅ 客户可以选择下载或购买
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 配置图绘账号
|
||||||
|
vi /root/ai_customer_service/ai_cs/.env
|
||||||
|
|
||||||
|
# 2. 重启 AI 客服
|
||||||
|
systemctl restart ai-cs
|
||||||
|
|
||||||
|
# 3. 查看日志
|
||||||
|
journalctl -u ai-cs -f
|
||||||
|
|
||||||
|
# 4. 发送图片测试
|
||||||
|
# 观察日志中的上传结果和话术
|
||||||
|
```
|
||||||
|
|
||||||
0
image/__init__.py
Normal file → Executable file
0
image/__init__.py
Normal file → Executable file
0
image/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_analyzer.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_analyzer.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_precheck.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_precheck.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_processor.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_processor.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_qa.cpython-310.pyc
Normal file → Executable file
0
image/__pycache__/image_qa.cpython-310.pyc
Normal file → Executable file
95
image/image_analyzer.py
Normal file → Executable file
95
image/image_analyzer.py
Normal file → Executable file
@@ -61,15 +61,39 @@ ANALYSIS_PROMPT = """你是一个电商图片处理评估专家,同时也是 G
|
|||||||
- yes:含小字需精细保留/清晰化(小字难处理 → 加价)
|
- yes:含小字需精细保留/清晰化(小字难处理 → 加价)
|
||||||
- no:无文字,或仅有大字(大字没关系 → 不加价)
|
- no:无文字,或仅有大字(大字没关系 → 不加价)
|
||||||
|
|
||||||
|
【文字数量加价规则】
|
||||||
|
- none:无文字,不加价
|
||||||
|
- 少量 (1-10 字):+5 元
|
||||||
|
- 中量 (11-50 字):+10-15 元
|
||||||
|
- 大量 (51-200 字):+20-30 元
|
||||||
|
- 极多 (200 字以上):+30-50 元
|
||||||
|
|
||||||
|
【文字分层需求】
|
||||||
|
- yes:客户要求可编辑分层文件(PSD 等) → 基础价格 x2 或 +50 元起
|
||||||
|
- no:普通图片处理 → 正常价格
|
||||||
|
|
||||||
|
【文字分层 + 大量文字】
|
||||||
|
- 如果 文字数量=大量/极多 且 文字分层需求=yes → 总价可达 60-80 元
|
||||||
|
|
||||||
【含人脸】
|
【含人脸】
|
||||||
- yes:图中有真实人物面孔(人像照/集体照/证件照/老照片等)
|
- yes:图中有真实人物面孔(人像照/集体照/证件照/老照片等)
|
||||||
- no:无人脸或人脸极小不影响主体
|
- no:无人脸或人脸极小不影响主体
|
||||||
|
|
||||||
【风险评估】
|
【风险评估 - 重要!】
|
||||||
- none:印花/图案/logo/风景/产品,AI处理效果稳定,可直接报价
|
- none:印花/图案/logo/风景/产品,AI处理效果稳定,可直接报价接单
|
||||||
- low:有人脸但清晰度尚可,AI修复后人脸相似度70-90%,建议先看效果
|
- low:有人脸但清晰度尚可,AI修复后人脸相似度70-90%,可以接单但要说明风险
|
||||||
- high:以下任一情况 → 严重模糊的人脸照片/老照片人像/需要打印/客户问能否找回原图
|
- high:以下任一情况 → 严重模糊的人脸照片/老照片人像/需要打印/客户问能否找回原图
|
||||||
high情况下,可做改为partial,备注写明风险话术
|
high情况下,可做改为partial,备注写明风险话术,谨慎接单
|
||||||
|
|
||||||
|
【敏感内容检测 - 必须严格判断!】
|
||||||
|
- yes:含以下任一内容 → 色情/黄色/擦边/裸露/性暗示/大尺度/涉政/暴力/血腥/违禁品
|
||||||
|
敏感内容=yes 时,可做必须填 no,直接拒绝不接单
|
||||||
|
- no:无上述敏感内容,可以正常接单处理
|
||||||
|
|
||||||
|
【可做判断 - 决定是否接单】
|
||||||
|
- yes:效果有把握,可以接单处理
|
||||||
|
- partial:能处理但有明显限制(人脸变形风险/分辨率极低/严重损坏)→ 可以接单但要说明风险
|
||||||
|
- no:无法接单(纯黑/纯白/完全损坏/找原始 RAW 文件/敏感内容/违法内容)
|
||||||
|
|
||||||
【敏感内容】优先判断,若为 yes 则 可做 必填 no
|
【敏感内容】优先判断,若为 yes 则 可做 必填 no
|
||||||
- yes:图片含色情/黄色/擦边/裸露/性暗示/大尺度等违规内容
|
- yes:图片含色情/黄色/擦边/裸露/性暗示/大尺度等违规内容
|
||||||
@@ -170,6 +194,7 @@ class ImageAnalyzer:
|
|||||||
"complex": (20, 25, "细节偏多"),
|
"complex": (20, 25, "细节偏多"),
|
||||||
"hard": (25, 30, "非常复杂"),
|
"hard": (25, 30, "非常复杂"),
|
||||||
}
|
}
|
||||||
|
# 注意:含文字很多时,不能报 simple/normal 的低价,必须 complex 起步
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.api_key = os.getenv("OPENAI_API_KEY")
|
self.api_key = os.getenv("OPENAI_API_KEY")
|
||||||
@@ -525,10 +550,64 @@ class ImageAnalyzer:
|
|||||||
if feasibility not in ("yes", "partial", "no"):
|
if feasibility not in ("yes", "partial", "no"):
|
||||||
feasibility = "yes"
|
feasibility = "yes"
|
||||||
|
|
||||||
|
|
||||||
|
# 【重要】含文字很多时,不能低价,必须 complex 起步(20 元以上)
|
||||||
|
# 有文字跟没文字是两个价格
|
||||||
|
if has_text == "yes":
|
||||||
|
if complexity == "simple":
|
||||||
|
# 简单但含文字 → 提升到 normal 价格
|
||||||
|
price_min, price_max = self.PRICE_MAP["normal"]
|
||||||
|
reason = "含文字,需精细处理"
|
||||||
|
elif complexity == "normal":
|
||||||
|
# normal 含文字 → 提升到 complex 价格
|
||||||
|
price_min, price_max = self.PRICE_MAP["complex"]
|
||||||
|
reason = "含文字,需精细处理"
|
||||||
|
# complex/hard 保持原价,已经够高
|
||||||
# 建议报价:complex/hard 取固定值,simple/normal 取中间,且必须为5的整数倍
|
# 建议报价:complex/hard 取固定值,simple/normal 取中间,且必须为5的整数倍
|
||||||
raw = price_max if complexity in ("complex", "hard") else (price_min + price_max) // 2
|
raw = price_max if complexity in ("complex", "hard") else (price_min + price_max) // 2
|
||||||
price_suggest = round(raw / 5) * 5
|
price_suggest = round(raw / 5) * 5
|
||||||
|
|
||||||
|
# 【文字数量加价】
|
||||||
|
text_surcharge = 0
|
||||||
|
if text_amount == "少量 (1-10 字)":
|
||||||
|
text_surcharge = 5
|
||||||
|
reason += " | 含少量文字"
|
||||||
|
elif text_amount == "中量 (11-50 字)":
|
||||||
|
text_surcharge = 15
|
||||||
|
reason += " | 含中量文字"
|
||||||
|
elif text_amount == "大量 (51-200 字)":
|
||||||
|
text_surcharge = 30
|
||||||
|
reason += " | 含大量文字"
|
||||||
|
elif text_amount == "极多 (200 字以上)":
|
||||||
|
text_surcharge = 50
|
||||||
|
reason += " | 含极多文字"
|
||||||
|
|
||||||
|
# 【文字分层需求加价】
|
||||||
|
layer_surcharge = 0
|
||||||
|
if text_layer_need == "yes":
|
||||||
|
if text_surcharge > 0:
|
||||||
|
# 有文字且需要分层 → 价格 x2 或 +50 元
|
||||||
|
layer_surcharge = max(50, price_suggest)
|
||||||
|
reason += " | 需要文字分层"
|
||||||
|
else:
|
||||||
|
# 无文字但需要分层 → +30 元
|
||||||
|
layer_surcharge = 30
|
||||||
|
reason += " | 需要分层文件"
|
||||||
|
|
||||||
|
# 加上文字加价
|
||||||
|
price_suggest += text_surcharge + layer_surcharge
|
||||||
|
|
||||||
|
# 【文字分层 + 大量文字】特殊处理 → 60-80 元
|
||||||
|
if text_amount in ["大量 (51-200 字)", "极多 (200 字以上)"] and text_layer_need == "yes":
|
||||||
|
if price_suggest < 60:
|
||||||
|
price_suggest = 60
|
||||||
|
elif price_suggest > 80:
|
||||||
|
price_suggest = 80
|
||||||
|
reason += " | 大量文字分层"
|
||||||
|
|
||||||
|
# 确保是 5 的倍数
|
||||||
|
price_suggest = round(price_suggest / 5) * 5
|
||||||
|
|
||||||
if sensitive == "yes":
|
if sensitive == "yes":
|
||||||
feasibility = "no"
|
feasibility = "no"
|
||||||
note = "图片含敏感内容,不接单"
|
note = "图片含敏感内容,不接单"
|
||||||
@@ -549,6 +628,10 @@ class ImageAnalyzer:
|
|||||||
"quality": quality,
|
"quality": quality,
|
||||||
"flatness": flatness if flatness in ("flat", "mild", "rough") else "",
|
"flatness": flatness if flatness in ("flat", "mild", "rough") else "",
|
||||||
"has_text": has_text if has_text in ("yes", "no") else "no",
|
"has_text": has_text if has_text in ("yes", "no") else "no",
|
||||||
|
"text_amount": text_amount,
|
||||||
|
"text_layer_need": text_layer_need,
|
||||||
|
"text_surcharge": text_surcharge,
|
||||||
|
"layer_surcharge": layer_surcharge,
|
||||||
"has_face": has_face, # yes / no
|
"has_face": has_face, # yes / no
|
||||||
"has_shadow": has_shadow if has_shadow in ("yes", "no") else "no",
|
"has_shadow": has_shadow if has_shadow in ("yes", "no") else "no",
|
||||||
"risk": risk, # none / low / high
|
"risk": risk, # none / low / high
|
||||||
@@ -574,6 +657,10 @@ class ImageAnalyzer:
|
|||||||
"quality": "",
|
"quality": "",
|
||||||
"flatness": "",
|
"flatness": "",
|
||||||
"has_text": "no",
|
"has_text": "no",
|
||||||
|
"text_amount": text_amount,
|
||||||
|
"text_layer_need": text_layer_need,
|
||||||
|
"text_surcharge": text_surcharge,
|
||||||
|
"layer_surcharge": layer_surcharge,
|
||||||
"has_face": "no",
|
"has_face": "no",
|
||||||
"has_shadow": "no",
|
"has_shadow": "no",
|
||||||
"risk": "none",
|
"risk": "none",
|
||||||
|
|||||||
0
image/image_precheck.py
Normal file → Executable file
0
image/image_precheck.py
Normal file → Executable file
0
image/image_processor.py
Normal file → Executable file
0
image/image_processor.py
Normal file → Executable file
0
image/image_qa.py
Normal file → Executable file
0
image/image_qa.py
Normal file → Executable file
0
image/image_tools.py
Normal file → Executable file
0
image/image_tools.py
Normal file → Executable file
0
image/perspective_fix.py
Normal file → Executable file
0
image/perspective_fix.py
Normal file → Executable file
0
logs/chat_2026-02-25.log
Normal file → Executable file
0
logs/chat_2026-02-25.log
Normal file → Executable file
0
logs/chat_2026-02-26.log
Normal file → Executable file
0
logs/chat_2026-02-26.log
Normal file → Executable file
0
logs/chat_2026-02-27.log
Normal file → Executable file
0
logs/chat_2026-02-27.log
Normal file → Executable file
0
mail/__init__.py
Normal file → Executable file
0
mail/__init__.py
Normal file → Executable file
0
mail/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
mail/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
mail/__pycache__/email_receiver.cpython-310.pyc
Normal file → Executable file
0
mail/__pycache__/email_receiver.cpython-310.pyc
Normal file → Executable file
0
mail/__pycache__/email_sender.cpython-310.pyc
Normal file → Executable file
0
mail/__pycache__/email_sender.cpython-310.pyc
Normal file → Executable file
0
mail/email_receiver.py
Normal file → Executable file
0
mail/email_receiver.py
Normal file → Executable file
0
mail/email_sender.py
Normal file → Executable file
0
mail/email_sender.py
Normal file → Executable file
0
requirements.txt
Normal file → Executable file
0
requirements.txt
Normal file → Executable file
0
results/20260225211854.jpg
Normal file → Executable file
0
results/20260225211854.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 212 KiB |
0
results/debug_7debc0124b0441da9945feaeceef93b1.jpg
Normal file → Executable file
0
results/debug_7debc0124b0441da9945feaeceef93b1.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user