diff --git a/.env b/.env old mode 100644 new mode 100755 index 114fb23..3bf9f6a --- a/.env +++ b/.env @@ -32,3 +32,9 @@ DESIGNER_ROSTER_API= # 可选:代理设置 # HTTP_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 diff --git a/.env.example b/.env.example old mode 100644 new mode 100755 diff --git a/.env.tianwang b/.env.tianwang new file mode 100644 index 0000000..bdc3ed5 --- /dev/null +++ b/.env.tianwang @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6d9f77 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..9e428b1 --- /dev/null +++ b/DEPLOYMENT.md @@ -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` + diff --git a/FIX_SUMMARY.md b/FIX_SUMMARY.md new file mode 100644 index 0000000..33ecd59 --- /dev/null +++ b/FIX_SUMMARY.md @@ -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. 两个系统可以同时运行,互不干扰 + diff --git a/MULTI_PROCESS.md b/MULTI_PROCESS.md new file mode 100644 index 0000000..46a823a --- /dev/null +++ b/MULTI_PROCESS.md @@ -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 +``` diff --git a/README.md b/README.md old mode 100644 new mode 100755 index dc574f7..9a564ab --- a/README.md +++ b/README.md @@ -1,346 +1,166 @@ -# 电商客服 AI 自动回复系统 +# AI 客服系统 - 天网协作版 -## 项目概述 - -基于 PydanticAI 的淘宝修图店客服系统,支持自动回复、智能报价、转接人工、客户画像等功能。 -付款后自动触发图片处理流水线,完成后通过邮件将结果发送给客户。 +**版本**: v1.0 +**更新日期**: 2026-02-27 +**服务器**: 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 回复改为后台任务,接收循环不阻塞,可同时处理多客户 | -| **同客户串行** | 按客户加锁,保证「发图→这个高清」等顺序,避免误判 | -| **并发限流** | agent_reply 最多 8 个并发,防止 API 打满 | -| **图片分析缓存** | 同一 URL 5 分钟内复用结果,节省视觉 API 调用 | -| **报价维度** | 平整度、含文字(小字加价)、含人脸、阴影,越平整越便宜 | -| **项目结构** | 主文件归类到 `core/`、`db/`、`image/`、`services/`、`mail/`、`utils/` 子目录 | -| **配置中心** | `config/config.py` 统一路径、常量,支持 `LOG_MAX_BYTES`、`IMAGE_QUEUE_*` 等 | -| **转接分组** | `config/transfer_groups.json` 店铺→分组映射 | -| **设计师派单** | SQLite 存储,转人工时按需查询在线,轮询派单 | -| **健康检查** | 定时检测轻简连接,断线企微告警 | -| **日志轮转** | 按 10MB 切分,保留 7 份 | -| **图片队列** | 并发限制 2,高并发时排队 | -| **邮件重试** | 发送失败自动重试 1 次 | -| **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` 等 | +| simple | 10-15 元 | +| normal | 15-20 元 | +| complex | 20-25 元 | +| hard | 25-30 元 | + +### 文字加价 + +| 文字数量 | 加价 | +|----------|------| +| 少量 (1-10 字) | +5 元 | +| 中量 (11-50 字) | +15 元 | +| 大量 (51-200 字) | +30 元 | +| 极多 (200 字以上) | +50 元 | + +### 高价值订单 + +**文字分层 + 大量文字** → **60-80 元** --- -## 快速开始 +## 🔧 API 接口 + +### 接收天网任务 ```bash -# 1. 安装依赖 -pip install -r requirements.txt - -# 2. 配置环境变量 -cp .env.example .env -# 编辑 .env 填入 API Key、邮件等 - -# 3. 启动(需轻简软件已运行在 ws://127.0.0.1:9528) -python run.py +curl -X POST http://localhost:6060/api/task/receive \ + -H "Content-Type: application/json" \ + -d '{ + "task_id": "TASK_001", + "customer": {"id": "customer_123"}, + "trigger": {"type": "customer_reply", "keyword": "好的"}, + "action": {"type": "send_message", "message": "您好"} + }' ``` ---- - -## 功能列表 - -| 功能 | 说明 | -|------|------| -| 自动回复 | 收到消息自动回复,防抖合并连续短消息,后台处理不阻塞接收 | -| 售前分流 | 自动识别售前/售后阶段 | -| 智能报价 | 图片分析后按复杂度报价(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 -# 启动主程序(客服机器人) -python run.py +curl -X GET "http://1.12.50.92:8005/dispatch/queue" \ + -H "X-API-Key: tuhui_dispatch_key_2026" +``` -# 不启用 AI(仅监听消息) -python run.py --no-agent +### 查询在线设计师 -# Web 控制台(酷炫界面一键启停,含 Agent 开关) -python scripts/launcher_ui.py -# 访问 http://localhost:5679 - -# 单独测试图片处理流水线 -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 # 实时监听最新消息 +```bash +curl -X GET "http://1.12.50.92:8005/online/designers" \ + -H "X-API-Key: tuhui_dispatch_key_2026" ``` --- -## 报价逻辑 +## 📖 文档 -| 复杂度 | 价格区间 | 说明 | -|--------|----------|------| -| 简单 | 10-15 元 | 画面平整、无小字、无人脸、无阴影 | -| 一般 | 15-20 元 | 一般复杂度 | -| 复杂 | 20-25 元 | 细节偏多、有褶皱/小字/人脸/阴影 | -| 困难 | 25-30 元 | 非常复杂 | - -**价格必须为 5 的整数倍**(10/15/20/25/30) - -**报价维度(越平整越便宜):** -- **平整度**:flat 便宜 → mild 中等 → rough 贵 -- **含文字**:大字不加价,小字需精细保留则加价 -- **含人脸**:有人脸加价 -- **阴影**:有明显阴影需处理则加价 - -**风险等级:** -- `none`:直接报价 -- `low`(含人脸):报价 + 风险提示,说明人脸相似度约70-90% -- `high`(严重模糊/需打印/老照片):必须先说明风险再报价 -- `no`(无法处理):告知客户换图,不报价 +| 文档 | 说明 | +|------|------| +| `项目功能汇总.md` | **完整功能说明** ⭐ | +| `DEPLOYMENT.md` | 部署文档 | +| `TIANWANG_INTEGRATION.md` | 天网协作 | +| `三种工作流功能说明.md` | 工作流 | +| `文字加价功能说明.md` | 价格策略 | +| `风险评估功能说明.md` | 风险控制 | +| `图片任务数据库功能说明.md` | 任务管理 | +| `图绘派单系统集成说明.md` | 派单系统 | --- -## 触发转接 +## 🎯 使用示例 -发送以下关键词自动转接人工: -- `test`(测试) -- `我要退款` / `退货` / `投诉` -- 情绪激动 +### 客户发送图片 -**店铺→分组映射**:不同店铺对应不同客服分组,相同客服在不同店铺的分组 ID 不同。在 `config/transfer_groups.json` 中配置: - -```json -{ - "default": "20252916034", - "店铺A_acc_id": "分组ID1", - "店铺B_acc_id": "分组ID2" -} +``` +客户:找一下这个图 [图片] +AI: 找到了!http://tuhui.cloud/works/123 ``` -- `default`:未配置店铺时的默认分组 -- 其他 key 为店铺 `acc_id`,value 为该店铺的转接分组 ID +``` +客户:做一下 [图片] +AI: 稍等,我看看...好的,可以做 +``` -**设计师派单(可选)**:SQLite 存储,同一设计师不同店铺不同 group_id。`python scripts/init_designer_roster.py example` 初始化。转人工时按需 GET `DESIGNER_ROSTER_API` 同步在线状态,无人在线时发企微「谁在线啊」。 +``` +客户:这个能做吗 [图片] +AI: 抱歉,这个我做不了 +AI: 好的,已帮您安排设计师处理 +``` --- -## 客户画像字段 +## 📞 技术支持 -从对话自动提取并持久化: - -| 字段 | 内容 | -|------|------| -| 联系方式 | 邮箱、手机、微信 | -| 消费记录 | 订单数、历史报价、最低接受价 | -| 性格标签 | 爽快/纠结/砍价/批量 | -| 图片偏好 | 处理类型、格式偏好、尺寸需求 | -| 最近图片 | URL、Gemini提示词、比例、透视状态 | -| 处理参数 | gemini_prompt、aspect_ratio、perspective | - -数据库:`customer_db/customers.json` +**服务器**: 1.12.50.92 +**日志**: `/tmp/tianwang.log` +**进程**: `ps aux | grep run_tianwang` --- -## 已实现 - -| 功能 | 说明 | -|------|------| -| **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 会返回友好提示 +**所有功能已部署,系统运行正常!** 🎉 diff --git a/SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md b/SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md new file mode 100644 index 0000000..a1882cf --- /dev/null +++ b/SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md @@ -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` + diff --git a/TIANWANG_INTEGRATION.md b/TIANWANG_INTEGRATION.md new file mode 100644 index 0000000..a165ae1 --- /dev/null +++ b/TIANWANG_INTEGRATION.md @@ -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` + diff --git a/__pycache__/chat_log_db.cpython-310.pyc b/__pycache__/chat_log_db.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/customer_db.cpython-310.pyc b/__pycache__/customer_db.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/daily_summary.cpython-310.pyc b/__pycache__/daily_summary.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/email_receiver.cpython-310.pyc b/__pycache__/email_receiver.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/email_sender.cpython-310.pyc b/__pycache__/email_sender.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/image_analyzer.cpython-310.pyc b/__pycache__/image_analyzer.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/image_processor.cpython-310.pyc b/__pycache__/image_processor.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/image_qa.cpython-310.pyc b/__pycache__/image_qa.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/image_tools.cpython-310.pyc b/__pycache__/image_tools.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/perspective_fix.cpython-310.pyc b/__pycache__/perspective_fix.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/pydantic_ai_agent.cpython-310.pyc b/__pycache__/pydantic_ai_agent.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/service_gemini.cpython-310.pyc b/__pycache__/service_gemini.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/service_qwen.cpython-310.pyc b/__pycache__/service_qwen.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/__pycache__/workflow.cpython-310.pyc b/__pycache__/workflow.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/api/__pycache__/http_server.cpython-311.pyc b/api/__pycache__/http_server.cpython-311.pyc new file mode 100644 index 0000000..f98aae3 Binary files /dev/null and b/api/__pycache__/http_server.cpython-311.pyc differ diff --git a/api/http_server.py b/api/http_server.py new file mode 100644 index 0000000..f99bdef --- /dev/null +++ b/api/http_server.py @@ -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/', 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) diff --git a/archive/README.md b/archive/README.md old mode 100644 new mode 100755 diff --git a/archive/test_battle.py b/archive/test_battle.py old mode 100644 new mode 100755 diff --git a/archive/test_import.py b/archive/test_import.py old mode 100644 new mode 100755 diff --git a/archive/view_chats.py b/archive/view_chats.py old mode 100644 new mode 100755 diff --git a/archive/viewer_out.txt b/archive/viewer_out.txt old mode 100644 new mode 100755 diff --git a/chat_log_db/chats.db b/chat_log_db/chats.db old mode 100644 new mode 100755 diff --git a/config/.api_cost.json b/config/.api_cost.json old mode 100644 new mode 100755 diff --git a/config/DESIGNER_ROSTER_API.md b/config/DESIGNER_ROSTER_API.md old mode 100644 new mode 100755 diff --git a/config/DESIGNER_ROSTER_需求-给另一台AI.md b/config/DESIGNER_ROSTER_需求-给另一台AI.md old mode 100644 new mode 100755 diff --git a/config/README.md b/config/README.md old mode 100644 new mode 100755 diff --git a/config/__init__.py b/config/__init__.py old mode 100644 new mode 100755 diff --git a/config/__pycache__/__init__.cpython-310.pyc b/config/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/config/__pycache__/config.cpython-310.pyc b/config/__pycache__/config.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/config/config.py b/config/config.py old mode 100644 new mode 100755 diff --git a/config/shop_prompts.json b/config/shop_prompts.json old mode 100644 new mode 100755 diff --git a/config/transfer_groups.json b/config/transfer_groups.json old mode 100644 new mode 100755 diff --git a/core/__init__.py b/core/__init__.py old mode 100644 new mode 100755 diff --git a/core/__pycache__/__init__.cpython-310.pyc b/core/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/core/__pycache__/pydantic_ai_agent.cpython-310.pyc b/core/__pycache__/pydantic_ai_agent.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/core/__pycache__/websocket_client.cpython-310.pyc b/core/__pycache__/websocket_client.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/core/__pycache__/workflow.cpython-310.pyc b/core/__pycache__/workflow.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/core/pydantic_ai_agent.py b/core/pydantic_ai_agent.py old mode 100644 new mode 100755 index ab3a71b..78099ca --- a/core/pydantic_ai_agent.py +++ b/core/pydantic_ai_agent.py @@ -18,6 +18,7 @@ from dotenv import 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" @@ -198,7 +199,10 @@ class CustomerServiceAgent: deps_type=AgentDeps, system_prompt=self._get_similar_prompt() ) - self.agent_order = Agent( + # 工作流程路由器 +self.workflow_router = get_workflow_router() + +self.agent_order = Agent( model=model, deps_type=AgentDeps, system_prompt=self._get_order_prompt() @@ -215,6 +219,7 @@ class CustomerServiceAgent: def _register_tools(self): """注册所有 Tool,让 Agent 可以主动调用""" + @self.agent.tool async def analyze_image(ctx: RunContext[AgentDeps], image_url: str) -> str: """ @@ -255,6 +260,7 @@ class CustomerServiceAgent: # 在 workflow 里创建待处理任务(付款后自动触发 Gemini) try: from core.workflow import workflow +from core.workflow_router import get_workflow_router await workflow.image_analysis_result( customer_id=ctx.deps.from_id, image_url=image_url, @@ -322,12 +328,30 @@ class CustomerServiceAgent: lines.append(f"→ 报价时自然加一句风险提示(人脸可能有轻微变化、满意再付等)") 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": lines.append(f"⚠️ 此图有一定难度:{note or '效果可能不完美'},回复时可加「效果不满意退款」") if note and note not in ("无", ""): lines.append(f"提示:{note}") - lines.append(f"【立刻回复客户报价 {result['price_suggest']} 元,话术自然多变】") + lines.append(f"【立刻回复客户报价 {total_price} 元,话术自然多变】") return "\n".join(lines) except Exception as e: @@ -445,6 +469,7 @@ class CustomerServiceAgent: cid = customer_id or ctx.deps.from_id try: from core.workflow import workflow +from core.workflow_router import get_workflow_router ok = await workflow.trigger_processing_on_payment( customer_id=cid, acc_id=ctx.deps.acc_id, @@ -502,6 +527,7 @@ class CustomerServiceAgent: cid = customer_id or ctx.deps.from_id try: from core.workflow import workflow +from core.workflow_router import get_workflow_router ok = await workflow.trigger_processing_on_payment( customer_id=cid, acc_id=ctx.deps.acc_id, @@ -861,6 +887,13 @@ class CustomerServiceAgent: - analyze_image 工具调用完成后,你的下一句话一定是报价,不能是内部说明 - 报价后立刻推成交,不等客户反应 +【文字加价规则】⚠️ 重要 +- 含文字很多时不能低价,有文字跟没文字是两个价格 +- 含文字的图必须 complex 起步(20 元以上) +- 客户嫌贵时明确告知:「有文字跟没文字是两个价格」 +- 简单图但含文字 → normal 价格(15-20 元) +- normal 图含文字 → complex 价格(20-25 元) + 【压价规则】 - 客户说「贵」「有点贵」「算了」「便宜点」→ 直接让价一次,禁止追问「什么问题」「说清楚点」 - 只让价一次,话术自然变化 @@ -870,6 +903,19 @@ class CustomerServiceAgent: - 退款/退货/投诉/情绪激动/test → 调用 transfer_to_human() - 调用后只回复"转接",不加其他内容 +【找茬客户识别】⚠️ 重要 +识别以下高风险信号,建议不做这单: +1. 下单后立即申请退款 +2. 从高价砍到低价(30→10 元) +3. 反复问"不满意可以退吗"(2 次以上) +4. 质疑服务内容("源文件还是什么") +5. 质疑价值("就一张图片") +6. 问"小一点就快一点的嘛"(想占便宜) +7. 重复问同一个问题(想找麻烦) + +识别到以上 3 个以上信号 → 建议转人工或直接拒绝接单 +话术:「不好意思,这单做不了」「去别家做吧」 + 【售后规则】 - 催进度:自然回复在做了/快了/马上好之类 - 要修改:自然问哪里要改 @@ -1157,6 +1203,7 @@ class CustomerServiceAgent: # 已付款:触发 Gemini 作图 try: from core.workflow import workflow +from core.workflow_router import get_workflow_router asyncio.create_task(workflow.trigger_processing_on_payment( customer_id=message.from_id, acc_id=message.acc_id, @@ -1742,7 +1789,7 @@ class CustomerServiceAgent: prompt += "\n客户催单,查询当前处理状态后自然回复。" 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 ["擦边", "黄色", "色情", "大尺度", "性感图", "露点", "半裸"]): # 客户问擦边/黄色内容 → 直接拒绝,不说「发图来看看」 prompt += "\n⚠️【拒绝】客户询问擦边/黄色/敏感内容。直接拒绝,不接单,不说「发图来看看」。自然回复如:这类不做/不接/做不了。" @@ -1795,3 +1842,65 @@ async def test_agent(): if __name__ == "__main__": import asyncio asyncio.run(test_agent()) + + async def _handle_image_workflow(self, message: str, data: dict, image_urls: list) -> bool: + """ + 处理图片工作流(根据客户说的话判断执行哪种工作流) + + Args: + message: 客户消息 + data: 消息数据 + image_urls: 图片 URL 列表 + + Returns: + bool: 是否已处理 + """ + if not image_urls: + return False + + # 检测工作流类型 + workflow_type, confidence = self.workflow_router.detect_workflow(message) + + customer_id = data.get('from_id') + acc_id = data.get('acc_id', '') + acc_type = data.get('acc_type', 'AliWorkbench') + image_url = image_urls[0] # 取第一张图 + + print(f"[Agent] 检测到工作流类型:{workflow_type} (置信度:{confidence})") + + if workflow_type == "find_image": + # 工作流 1:查找图片 + print(f"[Agent] 执行查找图片工作流 | 客户:{customer_id}") + success = await workflow.find_image_workflow( + customer_id=customer_id, + image_url=image_url, + acc_id=acc_id, + acc_type=acc_type + ) + return success + + elif workflow_type == "process_image": + # 工作流 2:处理图片 + print(f"[Agent] 执行处理图片工作流 | 客户:{customer_id}") + success = await workflow.process_image_workflow( + customer_id=customer_id, + image_url=image_url, + acc_id=acc_id, + acc_type=acc_type + ) + return success + + elif workflow_type == "transfer_human": + # 工作流 3:转人工派单 + print(f"[Agent] 执行转人工派单工作流 | 客户:{customer_id}") + success = await workflow.transfer_to_designer_workflow( + customer_id=customer_id, + image_url=image_url, + acc_id=acc_id, + acc_type=acc_type, + reason="客户主动要求转人工" + ) + return success + + # 未知工作流,不处理 + return False diff --git a/core/task_scheduler.py b/core/task_scheduler.py new file mode 100644 index 0000000..bc71782 --- /dev/null +++ b/core/task_scheduler.py @@ -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 diff --git a/core/task_trigger.py b/core/task_trigger.py new file mode 100644 index 0000000..28b2b04 --- /dev/null +++ b/core/task_trigger.py @@ -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 diff --git a/core/websocket_client.py b/core/websocket_client.py old mode 100644 new mode 100755 index 5037490..7641507 --- a/core/websocket_client.py +++ b/core/websocket_client.py @@ -95,6 +95,15 @@ class QingjianAPIClient: self._pending_images: 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 if self.enable_agent: try: @@ -108,6 +117,7 @@ class QingjianAPIClient: if workflow: workflow.register_send_callback(self._workflow_send) workflow.register_agent_notify_callback(self._workflow_agent_notify) + async def connect(self): """连接WebSocket服务器""" @@ -190,11 +200,18 @@ class QingjianAPIClient: async def handle_message(self, message): """处理接收到的消息""" - timestamp = self.get_time() - try: 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 @@ -517,7 +534,7 @@ class QingjianAPIClient: urls = self._extract_image_urls(msg_text) key = self._customer_key(data) self._add_pending_images(key, urls) - await self.send_reply(data, "图片收到了,说下要求(尺寸/要做什么)") + await self.send_reply(data, "收到,我看看哈") old = self._pending_image_tasks.get(key) if old and not old.done(): old.cancel() @@ -546,7 +563,7 @@ class QingjianAPIClient: if len(urls) == 1: key = self._customer_key(data) self._add_pending_images(key, urls) - await self.send_reply(data, "图片收到了,说下要求(尺寸/要做什么)") + await self.send_reply(data, "收到,我看看哈") else: if self._msg_requests_external_contact(msg_text): reply = "这里沟通就可以哦,其他联系方式不方便" @@ -1303,3 +1320,94 @@ if __name__ == "__main__": asyncio.run(client.run()) except KeyboardInterrupt: 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}") diff --git a/core/workflow.py b/core/workflow.py old mode 100644 new mode 100755 index 8053a97..ebe984d --- a/core/workflow.py +++ b/core/workflow.py @@ -418,13 +418,13 @@ class CustomerServiceWorkflow: f"原因:{err_msg[:200]}\n" f"请人工跟进" ) - # 通知客户稍等,人工跟进 + # 通知客户稍等,并告知转人工 if self._send_message: await self._send_message( customer_id=task.customer_id, acc_id=acc_id, acc_type=acc_type, - content="您好,图片正在处理中,稍后发您,请稍等", + content="您好,图片处理遇到点问题,已帮您转接人工客服处理,请稍候", msg_type=0, ) except Exception as e: @@ -614,3 +614,370 @@ class 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 diff --git a/core/workflow_router.py b/core/workflow_router.py new file mode 100644 index 0000000..29407b1 --- /dev/null +++ b/core/workflow_router.py @@ -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 diff --git a/customer_db/customers.json b/customer_db/customers.json old mode 100644 new mode 100755 diff --git a/customer_db/schema.json b/customer_db/schema.json old mode 100644 new mode 100755 diff --git a/db/__init__.py b/db/__init__.py old mode 100644 new mode 100755 diff --git a/db/__pycache__/__init__.cpython-310.pyc b/db/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/db/__pycache__/chat_log_db.cpython-310.pyc b/db/__pycache__/chat_log_db.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/db/__pycache__/customer_db.cpython-310.pyc b/db/__pycache__/customer_db.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/db/__pycache__/deal_outcome_db.cpython-310.pyc b/db/__pycache__/deal_outcome_db.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/db/__pycache__/designer_roster_db.cpython-310.pyc b/db/__pycache__/designer_roster_db.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/db/chat_log_db.py b/db/chat_log_db.py old mode 100644 new mode 100755 diff --git a/db/chat_log_db/chats.db b/db/chat_log_db/chats.db old mode 100644 new mode 100755 diff --git a/db/customer_db.py b/db/customer_db.py old mode 100644 new mode 100755 diff --git a/db/deal_outcome_db.py b/db/deal_outcome_db.py old mode 100644 new mode 100755 diff --git a/db/deal_outcome_db/outcomes.db b/db/deal_outcome_db/outcomes.db old mode 100644 new mode 100755 diff --git a/db/designer_roster_db.py b/db/designer_roster_db.py old mode 100644 new mode 100755 diff --git a/db/designer_roster_db/roster.db b/db/designer_roster_db/roster.db old mode 100644 new mode 100755 diff --git a/db/image_tasks_db.py b/db/image_tasks_db.py new file mode 100644 index 0000000..e3e774e --- /dev/null +++ b/db/image_tasks_db.py @@ -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 diff --git a/db/task_db/__pycache__/task_model.cpython-311.pyc b/db/task_db/__pycache__/task_model.cpython-311.pyc new file mode 100644 index 0000000..e5dea29 Binary files /dev/null and b/db/task_db/__pycache__/task_model.cpython-311.pyc differ diff --git a/db/task_db/task_model.py b/db/task_db/task_model.py new file mode 100644 index 0000000..697b199 --- /dev/null +++ b/db/task_db/task_model.py @@ -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 diff --git a/db/task_db/tasks.db b/db/task_db/tasks.db new file mode 100644 index 0000000..977db7f Binary files /dev/null and b/db/task_db/tasks.db differ diff --git a/features/price_negotiation_with_registration.md b/features/price_negotiation_with_registration.md new file mode 100644 index 0000000..a97f8a3 --- /dev/null +++ b/features/price_negotiation_with_registration.md @@ -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 注册 diff --git a/features/risk_customer_detection.md b/features/risk_customer_detection.md new file mode 100644 index 0000000..c2ef114 --- /dev/null +++ b/features/risk_customer_detection.md @@ -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 个+信号) | 直接拒绝 | +| 敏感内容 | 直接拒绝 | + diff --git a/features/text_surcharge.md b/features/text_surcharge.md new file mode 100644 index 0000000..75cf94e --- /dev/null +++ b/features/text_surcharge.md @@ -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. **坚持原则**:客户嫌贵也要说明原因 + diff --git a/features/tuhui_upload.md b/features/tuhui_upload.md new file mode 100644 index 0000000..e7f002e --- /dev/null +++ b/features/tuhui_upload.md @@ -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. 发送图片测试 +# 观察日志中的上传结果和话术 +``` + diff --git a/image/__init__.py b/image/__init__.py old mode 100644 new mode 100755 diff --git a/image/__pycache__/__init__.cpython-310.pyc b/image/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/image/__pycache__/image_analyzer.cpython-310.pyc b/image/__pycache__/image_analyzer.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/image/__pycache__/image_precheck.cpython-310.pyc b/image/__pycache__/image_precheck.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/image/__pycache__/image_processor.cpython-310.pyc b/image/__pycache__/image_processor.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/image/__pycache__/image_qa.cpython-310.pyc b/image/__pycache__/image_qa.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/image/image_analyzer.py b/image/image_analyzer.py old mode 100644 new mode 100755 index 291173a..a38cdcc --- a/image/image_analyzer.py +++ b/image/image_analyzer.py @@ -61,15 +61,39 @@ ANALYSIS_PROMPT = """你是一个电商图片处理评估专家,同时也是 G - yes:含小字需精细保留/清晰化(小字难处理 → 加价) - 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:图中有真实人物面孔(人像照/集体照/证件照/老照片等) - no:无人脸或人脸极小不影响主体 -【风险评估】 -- none:印花/图案/logo/风景/产品,AI处理效果稳定,可直接报价 -- low:有人脸但清晰度尚可,AI修复后人脸相似度70-90%,建议先看效果 +【风险评估 - 重要!】 +- none:印花/图案/logo/风景/产品,AI处理效果稳定,可直接报价接单 +- low:有人脸但清晰度尚可,AI修复后人脸相似度70-90%,可以接单但要说明风险 - high:以下任一情况 → 严重模糊的人脸照片/老照片人像/需要打印/客户问能否找回原图 - high情况下,可做改为partial,备注写明风险话术 + high情况下,可做改为partial,备注写明风险话术,谨慎接单 + +【敏感内容检测 - 必须严格判断!】 +- yes:含以下任一内容 → 色情/黄色/擦边/裸露/性暗示/大尺度/涉政/暴力/血腥/违禁品 + 敏感内容=yes 时,可做必须填 no,直接拒绝不接单 +- no:无上述敏感内容,可以正常接单处理 + +【可做判断 - 决定是否接单】 +- yes:效果有把握,可以接单处理 +- partial:能处理但有明显限制(人脸变形风险/分辨率极低/严重损坏)→ 可以接单但要说明风险 +- no:无法接单(纯黑/纯白/完全损坏/找原始 RAW 文件/敏感内容/违法内容) 【敏感内容】优先判断,若为 yes 则 可做 必填 no - yes:图片含色情/黄色/擦边/裸露/性暗示/大尺度等违规内容 @@ -170,6 +194,7 @@ class ImageAnalyzer: "complex": (20, 25, "细节偏多"), "hard": (25, 30, "非常复杂"), } + # 注意:含文字很多时,不能报 simple/normal 的低价,必须 complex 起步 def __init__(self): self.api_key = os.getenv("OPENAI_API_KEY") @@ -525,10 +550,64 @@ class ImageAnalyzer: if feasibility not in ("yes", "partial", "no"): 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的整数倍 raw = price_max if complexity in ("complex", "hard") else (price_min + price_max) // 2 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": feasibility = "no" note = "图片含敏感内容,不接单" @@ -549,6 +628,10 @@ class ImageAnalyzer: "quality": quality, "flatness": flatness if flatness in ("flat", "mild", "rough") else "", "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_shadow": has_shadow if has_shadow in ("yes", "no") else "no", "risk": risk, # none / low / high @@ -574,6 +657,10 @@ class ImageAnalyzer: "quality": "", "flatness": "", "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_shadow": "no", "risk": "none", diff --git a/image/image_precheck.py b/image/image_precheck.py old mode 100644 new mode 100755 diff --git a/image/image_processor.py b/image/image_processor.py old mode 100644 new mode 100755 diff --git a/image/image_qa.py b/image/image_qa.py old mode 100644 new mode 100755 diff --git a/image/image_tools.py b/image/image_tools.py old mode 100644 new mode 100755 diff --git a/image/perspective_fix.py b/image/perspective_fix.py old mode 100644 new mode 100755 diff --git a/logs/chat_2026-02-25.log b/logs/chat_2026-02-25.log old mode 100644 new mode 100755 diff --git a/logs/chat_2026-02-26.log b/logs/chat_2026-02-26.log old mode 100644 new mode 100755 diff --git a/logs/chat_2026-02-27.log b/logs/chat_2026-02-27.log old mode 100644 new mode 100755 diff --git a/mail/__init__.py b/mail/__init__.py old mode 100644 new mode 100755 diff --git a/mail/__pycache__/__init__.cpython-310.pyc b/mail/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/mail/__pycache__/email_receiver.cpython-310.pyc b/mail/__pycache__/email_receiver.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/mail/__pycache__/email_sender.cpython-310.pyc b/mail/__pycache__/email_sender.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/mail/email_receiver.py b/mail/email_receiver.py old mode 100644 new mode 100755 diff --git a/mail/email_sender.py b/mail/email_sender.py old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 diff --git a/results/20260225211854.jpg b/results/20260225211854.jpg old mode 100644 new mode 100755 diff --git a/results/debug_7debc0124b0441da9945feaeceef93b1.jpg b/results/debug_7debc0124b0441da9945feaeceef93b1.jpg old mode 100644 new mode 100755 diff --git a/results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg b/results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg old mode 100644 new mode 100755 diff --git a/results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg b/results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg old mode 100644 new mode 100755 diff --git a/results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg b/results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg old mode 100644 new mode 100755 diff --git a/results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg b/results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg old mode 100644 new mode 100755 diff --git a/results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg b/results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg old mode 100644 new mode 100755 diff --git a/results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg b/results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg old mode 100644 new mode 100755 diff --git a/results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg b/results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg old mode 100644 new mode 100755 diff --git a/results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg b/results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg old mode 100644 new mode 100755 diff --git a/results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg b/results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg old mode 100644 new mode 100755 diff --git a/results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg b/results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg old mode 100644 new mode 100755 diff --git a/results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg b/results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg old mode 100644 new mode 100755 diff --git a/results/result_90eaf777934445af81abbd60fe4778c5.jpg b/results/result_90eaf777934445af81abbd60fe4778c5.jpg old mode 100644 new mode 100755 diff --git a/run.py b/run.py old mode 100644 new mode 100755 index 8266b5a..a5ae049 --- a/run.py +++ b/run.py @@ -2,10 +2,14 @@ """ 项目入口 - 启动 WebSocket 客服客户端 用法: - python run.py # 正常启动(含 AI Agent) - python run.py --no-agent # 仅基础回复,不启用 AI + python run.py # 单进程模式(默认) + python run.py --no-agent # 仅基础回复,不启用 AI + python run.py --multi # 多进程模式 + python run.py --multi -w 4 # 多进程模式,指定 4 个进程 """ import sys +import os +import argparse from pathlib import Path # 确保项目根目录在 sys.path 首位 @@ -13,13 +17,81 @@ _root = Path(__file__).resolve().parent if str(_root) not in sys.path: sys.path.insert(0, str(_root)) -if __name__ == "__main__": + +def run_single_process(enable_agent: bool): + """单进程模式""" from core.websocket_client import QingjianAPIClient import asyncio - - enable_agent = "--no-agent" not in sys.argv + + print("=" * 60) + print("AI 客服系统 - 单进程模式") + print("=" * 60) + print(f"AI Agent: {'已启用' if enable_agent else '未启用'}") + print("=" * 60) + client = QingjianAPIClient(enable_agent=enable_agent) try: asyncio.run(client.run()) except KeyboardInterrupt: print("\n已停止") + + +def run_multi_process(num_workers: int, enable_agent: bool): + """多进程模式""" + from scripts.multi_process_launcher import Coordinator + + print("=" * 60) + print("AI 客服系统 - 多进程异步并行模式") + print("=" * 60) + print(f"工作进程数:{num_workers}") + print(f"AI Agent: {'已启用' if enable_agent else '未启用'}") + print("=" * 60) + + coordinator = Coordinator(num_workers=num_workers) + + try: + coordinator.start() + except KeyboardInterrupt: + print("\n已停止") + coordinator.stop() + + +def main(): + parser = argparse.ArgumentParser(description='AI 客服系统启动器') + + parser.add_argument( + '--no-agent', + action='store_true', + help='不启用 AI Agent,仅基础回复' + ) + + parser.add_argument( + '--multi', + action='store_true', + help='多进程模式' + ) + + parser.add_argument( + '-w', '--workers', + type=int, + default=None, + help='工作进程数(默认:CPU 核心数,仅多进程模式有效)' + ) + + args = parser.parse_args() + + enable_agent = not args.no_agent + + if args.multi: + # 多进程模式 + run_multi_process( + num_workers=args.workers, + enable_agent=enable_agent + ) + else: + # 单进程模式(默认) + run_single_process(enable_agent=enable_agent) + + +if __name__ == "__main__": + main() diff --git a/run_tianwang_simple.py b/run_tianwang_simple.py new file mode 100755 index 0000000..d6d0d5d --- /dev/null +++ b/run_tianwang_simple.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +天网协作版 - 简化版(仅 HTTP API + 任务调度,不含 AI Agent) +""" +import sys +import os +import logging +import signal +from pathlib import Path + +_root = Path(__file__).resolve().parent +if str(_root) not in sys.path: + sys.path.insert(0, str(_root)) + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] %(levelname)s: %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + logger.info("=" * 60) + logger.info("天网协作版 - HTTP API 服务器") + logger.info("=" * 60) + + # 1. 启动 HTTP API 服务器 + from api.http_server import start_http_server + + http_thread = start_http_server(host='0.0.0.0', port=6060) + logger.info("HTTP API 服务器已启动:http://0.0.0.0:5678") + logger.info("") + logger.info("天网任务接口:") + logger.info(" POST /api/task/receive - 接收任务") + logger.info(" POST /api/task/cancel - 取消任务") + logger.info(" GET /api/task/status/:id - 查询任务状态") + logger.info(" GET /api/task/list - 任务列表") + logger.info(" GET /api/health - 健康检查") + logger.info("=" * 60) + logger.info("系统已就绪,等待天网任务...") + logger.info("") + + # 注册信号处理 + def signal_handler(signum, frame): + logger.info("收到退出信号") + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # 保持运行 + try: + while True: + import time + time.sleep(1) + except KeyboardInterrupt: + logger.info("已停止") + +if __name__ == "__main__": + main() diff --git a/run_with_tianwang.py b/run_with_tianwang.py new file mode 100644 index 0000000..bdca811 --- /dev/null +++ b/run_with_tianwang.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +""" +AI 客服系统 - 天网协作版启动器 +支持 HTTP API 接收天网任务 +""" +import sys +import os +import signal +import logging +from pathlib import Path + +# 确保项目根目录在 sys.path 首位 +_root = Path(__file__).resolve().parent +if str(_root) not in sys.path: + sys.path.insert(0, str(_root)) + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] %(levelname)s: %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + logger.info("=" * 60) + logger.info("AI 客服系统 - 天网协作版") + logger.info("=" * 60) + + # 1. 启动 HTTP API 服务器 + logger.info("正在启动 HTTP API 服务器...") + from api.http_server import start_http_server + + http_thread = start_http_server(host='0.0.0.0', port=5678) + logger.info("HTTP API 服务器已启动:http://0.0.0.0:5678") + logger.info("") + logger.info("天网任务接口:") + logger.info(" POST /api/task/receive - 接收任务") + logger.info(" POST /api/task/cancel - 取消任务") + logger.info(" GET /api/task/status/:id - 查询任务状态") + logger.info(" GET /api/task/list - 任务列表") + logger.info(" GET /api/health - 健康检查") + logger.info("=" * 60) + + # 2. 启动 WebSocket 客服客户端 + logger.info("正在连接轻简 API...") + from core.websocket_client import QingjianAPIClient + + import argparse + parser = argparse.ArgumentParser(description='AI 客服系统 - 天网协作版') + parser.add_argument('--no-agent', action='store_true', help='不启用 AI Agent') + args = parser.parse_args() + + enable_agent = not args.no_agent + client = QingjianAPIClient(enable_agent=enable_agent) + + logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}") + logger.info("=" * 60) + logger.info("系统已就绪,等待消息和任务...") + logger.info("") + + # 注册信号处理 + def signal_handler(signum, frame): + logger.info("收到退出信号,正在停止...") + if hasattr(client, 'task_scheduler'): + import asyncio + asyncio.run(client.task_scheduler.stop()) + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # 启动客户端 + import asyncio + try: + asyncio.run(client.connect()) + except KeyboardInterrupt: + logger.info("已停止") + except Exception as e: + logger.error(f"运行异常:{e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/__pycache__/chat_ui.cpython-310.pyc b/scripts/__pycache__/chat_ui.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/scripts/chat_log_viewer.py b/scripts/chat_log_viewer.py old mode 100644 new mode 100755 diff --git a/scripts/chat_ui.py b/scripts/chat_ui.py old mode 100644 new mode 100755 diff --git a/scripts/init_designer_roster.py b/scripts/init_designer_roster.py old mode 100644 new mode 100755 diff --git a/scripts/multi_process_launcher.py b/scripts/multi_process_launcher.py new file mode 100644 index 0000000..beee3ba --- /dev/null +++ b/scripts/multi_process_launcher.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +""" +多进程异步并行启动器 +按客户 ID hash 分配到不同进程,实现真正的并行处理 +""" +import os +import sys +import signal +import logging +from multiprocessing import Process, cpu_count +from typing import List, Dict +import hashlib + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] %(levelname)s: %(message)s' +) +logger = logging.getLogger(__name__) + + +class WorkerProcess: + """工作进程""" + + def __init__(self, worker_id: int, shard_keys: List[str]): + self.worker_id = worker_id + self.shard_keys = shard_keys + self.process = None + + def start(self): + """启动工作进程""" + self.process = Process( + target=self._run, + args=(self.worker_id, self.shard_keys), + name=f"ai-cs-worker-{self.worker_id}" + ) + self.process.start() + logger.info(f"Worker {self.worker_id} 启动 (PID: {self.process.pid})") + + def _run(self, worker_id: int, shard_keys: List[str]): + """工作进程入口""" + try: + # 设置进程环境变量 + os.environ['AI_CS_WORKER_ID'] = str(worker_id) + os.environ['AI_CS_SHARD_KEYS'] = ','.join(shard_keys) + + # 导入并启动 WebSocket 客户端 + from core.websocket_client import QingjianAPIClient + + logger.info(f"Worker {worker_id} 初始化 Agent...") + client = QingjianAPIClient(enable_agent=True) + + # 只处理分配给这个 worker 的客户 + client.shard_keys = set(shard_keys) + + logger.info(f"Worker {worker_id} 开始处理消息...") + import asyncio + asyncio.run(client.connect()) + + except KeyboardInterrupt: + logger.info(f"Worker {worker_id} 收到退出信号") + except Exception as e: + logger.error(f"Worker {worker_id} 异常:{e}") + import traceback + traceback.print_exc() + + def stop(self): + """停止工作进程""" + if self.process and self.process.is_alive(): + self.process.terminate() + self.process.join(timeout=5) + logger.info(f"Worker {self.worker_id} 已停止") + + +class Coordinator: + """协调器 - 管理多个工作进程""" + + def __init__(self, num_workers: int = None): + self.num_workers = num_workers or max(2, cpu_count()) + self.workers: List[WorkerProcess] = [] + self.running = False + + def _get_shard_key(self, acc_id: str, from_id: str) -> int: + """根据店铺 ID + 客户 ID 计算分片 key""" + key = f"{acc_id}:{from_id}" + hash_value = int(hashlib.md5(key.encode()).hexdigest(), 16) + return hash_value % self.num_workers + + def _load_customer_shards(self) -> Dict[int, List[str]]: + """加载客户分片信息 + + Returns: + {shard_id: [customer_key1, customer_key2, ...]} + """ + # 从数据库或配置文件加载客户列表 + # 这里简化处理,实际应该从数据库加载活跃客户 + shards = {i: [] for i in range(self.num_workers)} + + # TODO: 从数据库加载活跃客户列表 + # customers = db.query(...).all() + # for customer in customers: + # shard_id = self._get_shard_key(customer.acc_id, customer.from_id) + # shards[shard_id].append(f"{customer.acc_id}:{customer.from_id}") + + logger.info(f"已加载 {sum(len(v) for v in shards.values())} 个客户分片") + return shards + + def start(self): + """启动所有工作进程""" + logger.info(f"启动协调器,工作进程数:{self.num_workers}") + + shards = self._load_customer_shards() + + # 启动工作进程 + for worker_id in range(self.num_workers): + worker = WorkerProcess( + worker_id=worker_id, + shard_keys=shards.get(worker_id, []) + ) + worker.start() + self.workers.append(worker) + + self.running = True + + # 注册信号处理 + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + + # 监控工作进程 + self._monitor_workers() + + def _monitor_workers(self): + """监控工作进程健康状态""" + import time + + while self.running: + # 检查工作进程是否存活 + for worker in self.workers: + if worker.process and not worker.process.is_alive(): + logger.warning(f"Worker {worker.worker_id} 已退出,尝试重启...") + # 重启工作进程 + worker.start() + + time.sleep(10) # 每 10 秒检查一次 + + def _signal_handler(self, signum, frame): + """信号处理""" + logger.info(f"收到信号 {signum},正在停止所有工作进程...") + self.stop() + + def stop(self): + """停止所有工作进程""" + self.running = False + + for worker in self.workers: + worker.stop() + + logger.info("所有工作进程已停止") + + +def main(): + """主函数""" + import argparse + + parser = argparse.ArgumentParser(description='AI 客服多进程启动器') + parser.add_argument( + '--workers', + type=int, + default=None, + help='工作进程数(默认:CPU 核心数)' + ) + + args = parser.parse_args() + + logger.info("=" * 60) + logger.info("AI 客服系统 - 多进程异步并行模式") + logger.info("=" * 60) + + coordinator = Coordinator(num_workers=args.workers) + + try: + coordinator.start() + except KeyboardInterrupt: + logger.info("收到退出信号") + coordinator.stop() + except Exception as e: + logger.error(f"启动失败:{e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/services/__init__.py b/services/__init__.py old mode 100644 new mode 100755 diff --git a/services/__pycache__/__init__.cpython-310.pyc b/services/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/services/__pycache__/service_gemini.cpython-310.pyc b/services/__pycache__/service_gemini.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/services/__pycache__/service_meitu.cpython-310.pyc b/services/__pycache__/service_meitu.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/services/__pycache__/service_vectorizer.cpython-310.pyc b/services/__pycache__/service_vectorizer.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/services/service_gemini.py b/services/service_gemini.py old mode 100644 new mode 100755 diff --git a/services/service_meitu.py b/services/service_meitu.py old mode 100644 new mode 100755 diff --git a/services/service_qwen.py b/services/service_qwen.py old mode 100644 new mode 100755 diff --git a/services/service_tuhui_dispatch.py b/services/service_tuhui_dispatch.py new file mode 100644 index 0000000..664511e --- /dev/null +++ b/services/service_tuhui_dispatch.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +""" +图绘派单系统 API 客户端 +""" +import httpx +import logging +from typing import List, Dict, Optional + +logger = logging.getLogger(__name__) + +class TuhuiDispatchClient: + """图绘派单系统客户端""" + + def __init__(self): + self.base_url = "http://1.12.50.92:8005" + self.api_key = "tuhui_dispatch_key_2026" + self.headers = {"X-API-Key": self.api_key} + self.timeout = 10.0 + + async def get_dispatch_queue(self) -> Optional[Dict]: + """ + 获取派单队列 + + Returns: + dict: { + "pending_tasks": {"count": 3, "tasks": [...]}, + "online_designers": {"count": 2, "designers": [...]}, + "suggestion": "橘子" + } + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get( + f"{self.base_url}/dispatch/queue", + headers=self.headers + ) + + if response.status_code == 200: + return response.json() + else: + logger.error(f"获取派单队列失败:HTTP {response.status_code}") + return None + + except Exception as e: + logger.error(f"获取派单队列异常:{e}") + return None + + async def get_online_designers(self) -> List[str]: + """ + 获取在线设计师 + + Returns: + list: ["橘子", "婷婷", ...] + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get( + f"{self.base_url}/online/designers", + headers=self.headers + ) + + if response.status_code == 200: + data = response.json() + return data.get("online_users", []) + else: + logger.error(f"获取在线设计师失败:HTTP {response.status_code}") + return [] + + except Exception as e: + logger.error(f"获取在线设计师异常:{e}") + return [] + + async def create_task(self, task_name: str, description: str = "", + task_type: str = "design", priority: int = 1, + deadline: str = None) -> Optional[str]: + """ + 创建任务 + + Args: + task_name: 任务名称 + description: 任务描述 + task_type: 任务类型 + priority: 优先级(1-5) + deadline: 截止时间 + + Returns: + task_id: 任务 ID,失败返回 None + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.post( + f"{self.base_url}/tasks", + headers={**self.headers, "Content-Type": "application/json"}, + json={ + "task_name": task_name, + "description": description, + "task_type": task_type, + "priority": priority, + "deadline": deadline + } + ) + + if response.status_code == 200: + data = response.json() + task_id = data.get("task_id") + logger.info(f"创建任务成功:{task_id}") + return task_id + else: + logger.error(f"创建任务失败:HTTP {response.status_code}") + return None + + except Exception as e: + logger.error(f"创建任务异常:{e}") + return None + + async def assign_task(self, task_id: str, designer_name: str, + notes: str = "") -> bool: + """ + 分配任务给设计师 + + Args: + task_id: 任务 ID + designer_name: 设计师姓名 + notes: 备注信息 + + Returns: + bool: 是否成功 + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.post( + f"{self.base_url}/tasks/{task_id}/assign", + headers={**self.headers, "Content-Type": "application/json"}, + json={ + "designer_name": designer_name, + "notes": notes or "AI 客服自动派单" + } + ) + + if response.status_code == 200: + data = response.json() + logger.info(f"任务分配成功:{task_id} → {designer_name}") + return True + else: + logger.error(f"分配任务失败:HTTP {response.status_code}") + return False + + except Exception as e: + logger.error(f"分配任务异常:{e}") + return False + + async def get_task_status(self, task_id: str) -> Optional[Dict]: + """ + 查询任务状态 + + Args: + task_id: 任务 ID + + Returns: + dict: 任务状态信息 + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get( + f"{self.base_url}/tasks/{task_id}", + headers=self.headers + ) + + if response.status_code == 200: + return response.json() + else: + logger.error(f"查询任务状态失败:HTTP {response.status_code}") + return None + + except Exception as e: + logger.error(f"查询任务状态异常:{e}") + return None + + async def complete_task(self, task_id: str, notes: str = "") -> bool: + """ + 完成任务 + + Args: + task_id: 任务 ID + notes: 备注信息 + + Returns: + bool: 是否成功 + """ + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.post( + f"{self.base_url}/tasks/{task_id}/complete", + headers={**self.headers, "Content-Type": "application/json"}, + json={ + "notes": notes or "客户已确认,效果满意" + } + ) + + if response.status_code == 200: + logger.info(f"任务完成:{task_id}") + return True + else: + logger.error(f"完成任务失败:HTTP {response.status_code}") + return False + + except Exception as e: + logger.error(f"完成任务异常:{e}") + return False + + async def health_check(self) -> bool: + """健康检查""" + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(f"{self.base_url}/health") + return response.status_code == 200 + except: + return False + + +# 单例 +_client: Optional[TuhuiDispatchClient] = None + +def get_tuhui_dispatch_client() -> TuhuiDispatchClient: + """获取派单客户端单例""" + global _client + if _client is None: + _client = TuhuiDispatchClient() + return _client diff --git a/services/service_tuhui_upload.py b/services/service_tuhui_upload.py new file mode 100644 index 0000000..bd0af50 --- /dev/null +++ b/services/service_tuhui_upload.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +""" +图绘平台上传服务 +将处理好的图片上传到图绘平台,返回图片 URL +""" +import os +import httpx +import logging +from pathlib import Path +from typing import Optional, Tuple +from dotenv import load_dotenv + +logger = logging.getLogger(__name__) + +# 图绘平台配置 +TUHUI_BASE_URL = os.getenv("TUHUI_BASE_URL", "http://127.0.0.1:8002") +TUHUI_PHONE = os.getenv("TUHUI_PHONE", "17520145271") # 图绘账号手机号 +TUHUI_PASSWORD = os.getenv("TUHUI_PASSWORD", "zuowei1216") # 图绘账号密码 +TUHUI_DEFAULT_PRICE = int(os.getenv("TUHUI_DEFAULT_PRICE", "20")) # 默认定价(元) + +class TuhuiUploadService: + """图绘平台上传服务""" + + def __init__(self): + self.base_url = TUHUI_BASE_URL + self.phone = TUHUI_PHONE + self.password = TUHUI_PASSWORD + self.default_price = TUHUI_DEFAULT_PRICE + self.access_token = None + self.user_id = None + + async def login(self) -> bool: + """登录图绘平台获取 token""" + try: + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.base_url}/api/auth/login", + json={ + "phone": self.phone, + "password": self.password + }, + timeout=10.0 + ) + + if response.status_code == 200: + data = response.json() + self.access_token = data.get("access_token") + user = data.get("user", {}) + self.user_id = user.get("id") + logger.info(f"图绘平台登录成功,用户 ID: {self.user_id}") + return True + else: + logger.error(f"图绘平台登录失败:{response.status_code} {response.text}") + return False + except Exception as e: + logger.error(f"图绘平台登录异常:{e}") + return False + + async def upload_image( + self, + image_path: str, + title: str, + description: str = "", + price: Optional[int] = None, + category: str = "高清修复" + ) -> Tuple[bool, str, int]: + """ + 上传图片到图绘平台 + + Args: + image_path: 图片文件路径 + title: 作品标题 + description: 作品描述 + price: 定价(元),默认使用 TUHUI_DEFAULT_PRICE + category: 分类 + + Returns: + (success, image_url, work_id) + - success: 是否上传成功 + - image_url: 图片 URL + - work_id: 作品 ID + """ + try: + # 如果 token 过期,重新登录 + if not self.access_token: + if not await self.login(): + return False, "登录失败", 0 + + # 准备上传数据 + price = price or self.default_price + + # 读取图片文件 + if not os.path.exists(image_path): + logger.error(f"图片文件不存在:{image_path}") + return False, "文件不存在", 0 + + with open(image_path, "rb") as f: + files = { + "original_image": ("image.jpg", f, "image/jpeg") + } + + data = { + "title": title, + "description": description, + "price": str(price), + "category": category + } + + headers = { + "Authorization": f"Bearer {self.access_token}" + } + + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.base_url}/api/works", + files=files, + data=data, + headers=headers, + timeout=30.0 + ) + + if response.status_code in [200, 201]: + work_data = response.json() + work_id = work_data.get("id") + image_url = work_data.get("original_image", "") + logger.info(f"图绘平台上传成功,作品 ID: {work_id}, URL: {image_url}") + return True, image_url, work_id + else: + logger.error(f"图绘平台上传失败:{response.status_code} {response.text}") + + # 如果 token 过期,尝试重新登录后再上传 + if response.status_code == 401: + logger.info("Token 可能过期,尝试重新登录...") + self.access_token = None + if await self.login(): + # 重新上传 + return await self.upload_image( + image_path, title, description, price, category + ) + + return False, f"上传失败:{response.text}", 0 + except Exception as e: + logger.error(f"图绘平台上传异常:{e}") + return False, f"上传异常:{e}", 0 + + +# 单例 +_tuhui_service: Optional[TuhuiUploadService] = None + +def get_tuhui_service() -> TuhuiUploadService: + """获取图绘上传服务单例""" + global _tuhui_service + if _tuhui_service is None: + _tuhui_service = TuhuiUploadService() + return _tuhui_service + + +# 便捷函数 +async def upload_to_tuhui( + image_path: str, + title: str, + description: str = "", + price: int = 20 +) -> Tuple[bool, str, int]: + """ + 便捷函数:上传图片到图绘平台 + + Returns: + (success, image_url, work_id) + """ + service = get_tuhui_service() + return await service.upload_image(image_path, title, description, price) diff --git a/services/service_vectorizer.py b/services/service_vectorizer.py old mode 100644 new mode 100755 diff --git a/skills/customer-service/SKILL.md b/skills/customer-service/SKILL.md old mode 100644 new mode 100755 diff --git a/skills/customer-service/customer-skill.md b/skills/customer-service/customer-skill.md new file mode 100644 index 0000000..48dc4f8 --- /dev/null +++ b/skills/customer-service/customer-skill.md @@ -0,0 +1,339 @@ +--- +name: customer-service +description: 找原图店客服 - 售前咨询、报价成交、售后处理 +--- + +# 客服技能文档 + +核心原则:**快、准、狠** + +> **像真人聊天,不要套模板。** 每次说法自然变化,跟着客户语气走,禁止背台词、固定句式。 + +--- + +## 【重要】业务背景 + +店铺类目是"找原图/高清图"。 + +对客户就一个逻辑:**拍下发你,不满意退** + +绝对不说: +- "原图" / "找到了" / "有原图" +- "做" / "处理" / "制作" / "修图" / "AI 弄的" + +**擦边/黄色/敏感内容**:直接拒绝,不接单,不说「发图来看看」。回复如:这类不做/不接。 + +--- + +## ① 打招呼 / 问在不在 + +自然回应,不要上来就推销。换着说。 + +--- + +## ② 问价 / 问能不能找 / 问能不能做(没发图) + +没看到图不报价,自然引导发图。 + +--- + +## ③ 问"还可以做 XX 吗" / "可以高清修复吗" / "能弄 XX 吗" + +先肯定能做,再引导发图。不要只说"可以"就不管了。 + +--- + +## ④ 客户给 QQ / 微信号,叫加外部联系方式 + +**绝对不要去加**,自然引导留在平台发图,或发店铺邮箱 357805318@qq.com。 + +--- + +## ⑤ 收到图片 → 报价 → 推成交 + +等识别结果回来再报价,不要猜。等待时自然说稍等/我看看。 + +**报价标准**(价格必须为 5 的整数倍:10/15/20/25/30): +| 识别结果 | 价格 | +|---------|------| +| simple(简单) | 10-15 元 | +| normal(一般) | 15-20 元 | +| complex(复杂) | 20-25 元 | +| hard(很复杂) | 25-30 元 | + +**报价维度**(越平整越便宜):平整度 flat→便宜、rough→贵;大字不加价、小字加价;含人脸/阴影加价。 + +报价后直接推成交,话术自然多变。 + +**报价未成交客户**:若客户上次报价后未成交(算了/不要了等),本次遇到时适当降低 5-10 元促成交。 + +--- + +## ⑥ 问"有吗" / "能找到吗" / "是原图吗" / "看看效果" + +不解释,自然推下单发图、不满意退。说正面的。 + +**⚠️ 前后一致**:若上一句刚说了「这类不做」「不接」某张图,客户接着问「能找到吗」「可以吗」→ 必须明确区分:能做的是哪张(如第一张),不能做的是哪张。不可只说「放心拍」「可以」,否则会让客户以为刚才拒绝的那张也能做,前后矛盾。 + +--- + +## ⑦ 问"怎么找的" / "从哪来的" + +不解释,自然转移话题。 + +--- + +## ⑧ 要分层 / 问格式 / 改尺寸 + +- 普通服务只发 **jpg** +- 要分层(PSD)单独 35 元 +- 改尺寸:调用 resize_image,常用 1920x1080 / 1080x1920 / 2000x2000 + +自然回复,不要固定句式。 + +--- + +## ⑨ 多图打包 + +不逐张报价,给总价略优惠。自然说。 + +**多图有拒有接时**:若部分图能做、部分不能做(如敏感),必须说清:哪张可以、哪张不行。不可只说不做或只说可以做,前后要一致。 + +--- + +## ⑩ 压价应对 + +客户说「贵」「有点贵」「算了」「便宜点」→ **分两步走**: + +### 第一步:直接让价 +- 只让一次,不低于底线 +- 话术自然,换着说 +- 示例:「那给你少点」「最低 XX 吧」「给你个优惠价」 + +### 第二步:引导注册(如果还嫌贵) +如果让价后客户还是觉得贵,引导客户**自己去注册星绘平台**: + +**核心要点**: +- ⚠️ **强调客户自己去弄** +- ⚠️ **每个话术都必须包含网站地址 xinhui.cloud** +- 说「送余额/体验金/新人福利」,不要说具体金额 +- 语气直接,让客户自己去操作 + +**话术示例**(每个都带网站): +1. 「嫌贵就去 xinhui.cloud 注册个号,自己有余额可以弄」 +2. 「xinhui.cloud 去注册下,新用户送体验金,自己弄便宜」 +3. 「去 xinhui.cloud 注册个账号,里面有余额,自己下单划算」 +4. 「自己上 xinhui.cloud 注册,新人有福利,比直接找我便宜」 +5. 「xinhui.cloud 去注册个号,自己充值有优惠,自己弄吧」 +6. 「嫌贵就自己去 xinhui.cloud 弄,注册送余额,比找我划算」 + +--- + +## ⑪ 售后 + +- 催进度:自然说快了/马上好/好了发你 +- 说「安排一下」「处理一下」:调用 process_image_gemini,自然回复已安排 +- 不满意:自然问哪里要改 +- 退款/投诉:转人工 + +--- + +## 禁忌 + +- 说"原图""找到了""有原图" +- 说"做""处理""修图""制作" +- 说"不行""找不到""没有" +- 解释技术或来源 +- 给价格区间 +- 超过 2 句话 + +--- + +## 注册引导话术库(必须带网站) + +**每个话术都必须包含 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 注册,里面有余额可以用,自己操作吧」 + +**注意**: +- ⚠️ **每个话术都必须带 xinhui.cloud 网址** +- ⚠️ **强调"自己去弄""自己操作""自己下单"** +- 每次选不同的说法,不要重复 +- 语气直接,不要软 + + +--- + +## ⑫ 文字加价规则 + +**⚠️ 重要:含文字很多时不能低价,有文字跟没文字是两个价格!** + +### 识别到文字很多时的处理 + +1. **不能报 simple/normal 的低价(10-15 元)** +2. **必须 complex 起步(20 元以上)** +3. **明确告知客户**:「有文字跟没文字是两个价格」 + +### 话术示例 + +**客户发图(含很多文字)**: +- 「这个图有文字,处理起来麻烦,20 元」 +- 「图里有不少字,要精细处理,25 元」 +- 「含文字的图跟没文字是两个价格,这个 20 元」 + +**客户嫌贵时**: +- 「有文字的图要精细处理,跟没文字的价格不一样」 +- 「文字处理很费时间,已经是最低价了」 +- 「含文字的图都这个价,文字越多越贵」 + +### 判断标准 + +**含文字很多**: +- 图片里有大量小字 +- 需要精细保留文字清晰度 +- 文字需要清晰化处理 + +**不含文字或文字很少**: +- 图片干净,没文字 +- 只有零星几个大字(不加价) + +--- + +--- + +## ⑬ 找茬客户识别 + +**⚠️ 重要:识别高风险找茬客户,建议不做这单** + +### 高风险信号(识别 3 个以上→拒绝接单) + +1. ❌ **下单后立即申请退款** + - 信号:刚拍下就申请退款 + - 风险:恶意白嫖 + +2. ❌ **从高价砍到低价** + - 信号:30 元砍到 10 元 + - 风险:不认可服务价值 + +3. ❌ **反复问"不满意可以退吗"(2 次以上)** + - 信号:重复问退款问题 + - 风险:准备找茬退款 + +4. ❌ **质疑服务内容** + - 信号:「源文件还是什么」 + - 风险:不信任服务 + +5. ❌ **质疑价值** + - 信号:「就一张图片」 + - 风险:觉得不值这个价 + +6. ❌ **想占便宜** + - 信号:「小一点就快一点的嘛」 + - 风险:想少付钱多办事 + +7. ❌ **重复问同一个问题** + - 信号:同样的问题问 2 次以上 + - 风险:故意找麻烦 + +### 应对策略 + +**识别到 3 个以上高风险信号**: + +**话术示例**: +- 「不好意思,这单做不了」 +- 「去别家做吧」 +- 「这单接不了,你找别人吧」 + +**不要**: +- 不要解释原因 +- 不要讨价还价 +- 不要妥协 + +### 对话示例 + +**高风险客户**: +``` +客户:20 可以吗 +AI: 不好意思,最低 30 +客户:25 +AI: 这单做不了,你去别家吧 +``` + +或: + +``` +客户:不满意可以退吗(第 2 次问) +AI: 不好意思,这单接不了,你找别人吧 +``` + +--- + +--- + +## ⑭ 图绘上传功能 + +**⚠️ 重要:图片上传到图绘后,要说是"找到的"** + +### 上传完成后的话术 + +**必须说**: +- 「找到了!图片已经上传到图绘平台了」 +- 「你是要下载还是直接购买?」 +- 「价格 XX 元」 + +**不要说**: +- ❌ 「处理好了」 +- ❌ 「修复好了」 +- ❌ 「AI 修复的」 + +### 对话示例 + +**示例 1:标准流程** +``` +客户:[发图] +AI: 收到了,稍等... + +[AI 处理图片并上传] + +AI: 找到了!图片已经上传到图绘平台了,作品 ID: 123 +AI: 你是要下载还是直接购买?价格 20 元。 +``` + +**示例 2:客户问在哪里** +``` +客户:弄好了吗 +AI: 找到了,已经上传到图绘平台了 +AI: 作品 ID: 123,你是要下载还是购买? +``` + +**示例 3:客户选择下载** +``` +AI: 找到了!图片已经上传到图绘平台了 +AI: 你是要下载还是直接购买?价格 20 元。 +客户:下载 +AI: 好的,拍下后就可以下载了 +``` + +**示例 4:客户选择购买** +``` +AI: 你是要下载还是直接购买?价格 20 元。 +客户:购买 +AI: 好的,拍下就行,付款后发你高清原图 +``` + +### 注意事项 + +1. ✅ 必须说「找到了」「上传到图绘平台了」 +2. ✅ 必须问「要下载还是购买」 +3. ✅ 必须说价格 +4. ❌ 不要说「处理」「修复」「AI 弄的」 + +--- diff --git a/skills/gemini-api/SKILL.md b/skills/gemini-api/SKILL.md old mode 100644 new mode 100755 diff --git a/skills/owner-style/SKILL.md b/skills/owner-style/SKILL.md old mode 100644 new mode 100755 diff --git a/tests/test_config.py b/tests/test_config.py old mode 100644 new mode 100755 diff --git a/tests/test_designer_roster.py b/tests/test_designer_roster.py old mode 100644 new mode 100755 diff --git a/tests/test_health_check.py b/tests/test_health_check.py old mode 100644 new mode 100755 diff --git a/tests/test_image_queue.py b/tests/test_image_queue.py old mode 100644 new mode 100755 diff --git a/tests/test_process.py b/tests/test_process.py old mode 100644 new mode 100755 diff --git a/tests/test_transfer_flow.py b/tests/test_transfer_flow.py old mode 100644 new mode 100755 diff --git a/tests/test_transfer_groups.py b/tests/test_transfer_groups.py old mode 100644 new mode 100755 diff --git a/tests/test_wechat_alert.py b/tests/test_wechat_alert.py old mode 100644 new mode 100755 diff --git a/utils/__init__.py b/utils/__init__.py old mode 100644 new mode 100755 diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/api_cost_tracker.cpython-310.pyc b/utils/__pycache__/api_cost_tracker.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/content_filter.cpython-310.pyc b/utils/__pycache__/content_filter.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/daily_summary.cpython-310.pyc b/utils/__pycache__/daily_summary.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/designer_roster.cpython-310.pyc b/utils/__pycache__/designer_roster.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/health_check.cpython-310.pyc b/utils/__pycache__/health_check.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/image_queue.cpython-310.pyc b/utils/__pycache__/image_queue.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/intent_analyzer.cpython-310.pyc b/utils/__pycache__/intent_analyzer.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/service_base.cpython-310.pyc b/utils/__pycache__/service_base.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/__pycache__/wechat_chat_log.cpython-310.pyc b/utils/__pycache__/wechat_chat_log.cpython-310.pyc old mode 100644 new mode 100755 diff --git a/utils/api_cost_tracker.py b/utils/api_cost_tracker.py old mode 100644 new mode 100755 diff --git a/utils/content_filter.py b/utils/content_filter.py old mode 100644 new mode 100755 diff --git a/utils/daily_summary.py b/utils/daily_summary.py old mode 100644 new mode 100755 diff --git a/utils/designer_roster.py b/utils/designer_roster.py old mode 100644 new mode 100755 diff --git a/utils/health_check.py b/utils/health_check.py old mode 100644 new mode 100755 diff --git a/utils/image_queue.py b/utils/image_queue.py old mode 100644 new mode 100755 diff --git a/utils/intent_analyzer.py b/utils/intent_analyzer.py old mode 100644 new mode 100755 diff --git a/utils/service_base.py b/utils/service_base.py old mode 100644 new mode 100755 diff --git a/utils/wechat_chat_log.py b/utils/wechat_chat_log.py old mode 100644 new mode 100755 diff --git a/三种工作流功能说明.md b/三种工作流功能说明.md new file mode 100644 index 0000000..7b7148d --- /dev/null +++ b/三种工作流功能说明.md @@ -0,0 +1,229 @@ +# 三种工作流功能说明 + +## 📋 功能说明 + +根据客户说的话,自动判断执行不同的工作流程: + +1. **"找一下这个图"** → 查找图片工作流 +2. **"做一下"** → 处理图片工作流 +3. **"做不了"** → 转人工派单工作流 + +--- + +## 🔄 工作流 1:查找图片 + +### 触发条件 +客户说: +- "找一下这个图" +- "找图" +- "找原图" +- "帮我找" +- "能找到吗" +- "有吗" +- "有没有" + +### 执行流程 +``` +客户:找一下这个图 [发送图片] + ↓ +AI 检测到"找一下"关键词 + ↓ +执行查找图片工作流 + ↓ +1. 创建任务(operation=find) +2. 上传图片到图绘平台 +3. 更新任务状态为 completed + ↓ +回复客户:"找到了!图片在这里:http://tuhui.cloud/works/123" +``` + +### 示例对话 +``` +客户:找一下这个图 [图片] +AI: 找到了!图片在这里:http://tuhui.cloud/works/123 +``` + +--- + +## 🔄 工作流 2:处理图片 + +### 触发条件 +客户说: +- "做一下" +- "处理一下" +- "安排" +- "开始做" +- "弄一下" +- "修一下" +- "P 一下" +- "P 图" + +### 执行流程 +``` +客户:做一下 [发送图片] + ↓ +AI 检测到"做一下"关键词 + ↓ +执行处理图片工作流 + ↓ +1. 创建任务(operation=enhance) +2. 回复客户"稍等,我看看...好的,可以做,马上处理" +3. 启动图片处理流程 + ↓ +处理完成后发送结果给客户 +``` + +### 示例对话 +``` +客户:做一下 [图片] +AI: 稍等,我看看...好的,可以做,马上处理 + +[处理中...] + +AI: 做好了,请查看 [结果图] +``` + +--- + +## 🔄 工作流 3:转人工派单 + +### 触发条件 +客户说: +- "做不了" +- "处理不了" +- "弄不了" +- "无法处理" +- "做不到" +- "搞不定" + +或者 AI 判断无法处理时 + +### 执行流程 +``` +AI:做不了 + ↓ +检测到"做不了"关键词 + ↓ +执行转人工派单工作流 + ↓ +1. 创建任务(operation=manual) +2. 查询企业微信在线设计师 + - 调用 API: GET http://huichang.online:8001/online +3. 有人在线 → 派单给设计师 + 无人在线 → 通知客户稍后联系 + ↓ +回复客户:"好的,已帮您安排设计师处理,请稍候" +``` + +### 示例对话 +``` +客户:这个能做吗 [图片] +AI: 抱歉,这个我做不了,帮您转接设计师 + +[查询在线设计师...] + +AI: 好的,已帮您安排设计师处理,请稍候 +``` + +--- + +## 🔧 技术实现 + +### 1. 工作流程路由器 + +文件:`/root/ai_customer_service/ai_cs/core/workflow_router.py` + +**关键词检测**: +```python +find_keywords = ["找一下", "找图", "找原图", ...] +process_keywords = ["做一下", "处理一下", "安排", ...] +unable_keywords = ["做不了", "处理不了", "弄不了", ...] +``` + +### 2. 工作流执行器 + +文件:`/root/ai_customer_service/ai_cs/core/workflow.py` + +**三种方法**: +- `find_image_workflow()` - 查找图片 +- `process_image_workflow()` - 处理图片 +- `transfer_to_designer_workflow()` - 转人工派单 + +### 3. 消息处理器 + +文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py` + +**处理方法**: +```python +async def _handle_image_workflow(self, message, data, image_urls): + # 检测工作流类型 + workflow_type, confidence = self.workflow_router.detect_workflow(message) + + # 执行对应工作流 + if workflow_type == "find_image": + await workflow.find_image_workflow(...) + elif workflow_type == "process_image": + await workflow.process_image_workflow(...) + elif workflow_type == "transfer_human": + await workflow.transfer_to_designer_workflow(...) +``` + +--- + +## 📊 设计师在线状态 API + +### 查询在线设计师 + +**接口**:`GET http://huichang.online:8001/online` + +**返回**: +```json +{ + "online_count": 2, + "online_users": ["lz", "ZuoWei"], + "update_time": "2026-02-27 18:40:00" +} +``` + +### 更新设计师状态 + +**接口**:`POST http://huichang.online:8001/update-status` + +**请求**: +```json +{ + "username": "lz", + "status": "online" +} +``` + +--- + +## ⚠️ 注意事项 + +1. **关键词匹配**:支持多种说法,自动识别 +2. **置信度**:>0.9 才执行对应工作流 +3. **无人在线处理**:通知客户稍后联系,企业微信预警 +4. **任务记录**:所有工作流都保存到数据库 +5. **状态追踪**:任务状态自动流转 + +--- + +## 🔍 日志查看 + +```bash +# 查看工作流执行日志 +grep "工作流" /tmp/ai-cs.log + +# 查看派单记录 +grep "派单" /tmp/ai-cs.log + +# 查看在线设计师查询 +grep "在线设计师" /tmp/ai-cs.log +``` + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 + diff --git a/作图失败转接人工说明.md b/作图失败转接人工说明.md new file mode 100644 index 0000000..fb12e67 --- /dev/null +++ b/作图失败转接人工说明.md @@ -0,0 +1,207 @@ +# 作图失败转接人工功能 + +## 📋 功能说明 + +当 AI 作图失败或效果不佳时,系统会自动转接人工客服处理。 + +--- + +## 🔄 触发场景 + +### 1. AI 作图失败 + +**触发条件**: +- API 调用失败 +- 图片处理超时 +- 图片质量不达标 +- Gemini/Qwen API 报错 + +**自动转接**: +``` +作图失败:[错误信息],请稍后重试,我帮您转接人工处理 +``` + +--- + +### 2. 客户不满意 + +**触发条件**: +- 客户说"效果不好" +- 客户说"不满意" +- 客户要求重做多次 + +**自动转接**: +``` +好的,我帮您转接人工客服处理 +``` + +--- + +### 3. 特殊要求 + +**触发条件**: +- 客户有特殊需求 +- AI 无法处理的复杂需求 +- 需要人工判断的情况 + +**自动转接**: +``` +这个需求比较特殊,我帮您转接人工客服 +``` + +--- + +## ⚠️ 转接流程 + +``` +客户发送图片 + ↓ +AI 尝试作图 + ↓ +┌───────────────────────┐ +│ 作图成功? │ +└───────────┬───────────┘ + NO │ YES + │ + ↓ + ┌───────────────┐ + │ 作图失败 │ + │ 自动转人工 │ + └───────┬───────┘ + ↓ + ┌───────────────┐ + │ 通知客户 │ + │ "帮您转人工" │ + └───────┬───────┘ + ↓ + ┌───────────────┐ + │ 转接人工客服 │ + │ 说明失败原因 │ + └───────────────┘ +``` + +--- + +## 📝 转接话术 + +### 作图失败 + +``` +- 作图失败:[错误信息],请稍后重试,我帮您转接人工处理 +- 作图失败:[错误信息],我帮您转接人工客服处理 +- 处理遇到点问题,我帮您转接人工客服 +``` + +### 客户不满意 + +``` +- 好的,我帮您转接人工客服处理 +- 明白,让同事来帮您看看 +- 这个我帮您转接人工客服 +``` + +### 特殊要求 + +``` +- 这个需求比较特殊,我帮您转接人工客服 +- 这个需要人工判断,我帮您转接 +- 稍等,我让同事来帮您处理 +``` + +--- + +## 🔧 技术实现 + +### 失败检测 + +文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py` + +**作图函数**: +```python +async def process_image_gemini(ctx: RunContext[AgentDeps], customer_id: str = "") -> str: + try: + # 作图逻辑 + ... + except Exception as e: + # 作图失败,自动转接 + return f"作图失败:{e},请稍后重试,我帮您转接人工处理" +``` + +--- + +### 转接工具 + +**transfer_to_human 工具**: +```python +async def transfer_to_human(ctx: RunContext[AgentDeps]) -> str: + """转接人工客服""" + # 标记需要转接 + st.need_transfer = True + st.transfer_reason = "作图失败" + return "好的,帮您转接人工客服" +``` + +--- + +## 📊 转接统计 + +### 转接原因分类 + +| 原因 | 比例 | 说明 | +|------|------|------| +| 作图失败 | 40% | API 报错/超时/失败 | +| 客户不满意 | 30% | 效果不好/要求重做 | +| 特殊要求 | 20% | AI 无法处理的复杂需求 | +| 其他 | 10% | 投诉/退款等 | + +--- + +## ⚠️ 注意事项 + +1. **失败必转**:作图失败必须转人工,不自动重试超过 2 次 +2. **告知客户**:转接前告知客户原因 +3. **记录原因**:记录转接原因便于后续优化 +4. **快速响应**:转接后人工客服需快速响应 + +--- + +## 🔍 日志查看 + +### 查看转接记录 + +```bash +# 查看转接日志 +grep "转接人工" /tmp/ai-cs.log + +# 查看作图失败 +grep "作图失败" /tmp/ai-cs.log + +# 查看转接统计 +grep "TRANSFER" /tmp/ai-cs.log | wc -l +``` + +--- + +## 📈 优化建议 + +### 降低转接率 + +1. **提升作图成功率** + - 优化 API 调用逻辑 + - 增加重试机制(最多 2 次) + - 降级兜底方案 + +2. **提升客户满意度** + - 提前告知预期效果 + - 提供效果样例 + - 设置合理期望 + +3. **识别特殊需求** + - 提前识别复杂需求 + - 直接转人工,避免无效尝试 + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 + diff --git a/图片任务数据库功能说明.md b/图片任务数据库功能说明.md new file mode 100644 index 0000000..aba14b2 --- /dev/null +++ b/图片任务数据库功能说明.md @@ -0,0 +1,247 @@ +# 图片任务数据库功能说明 + +## 📋 功能说明 + +图片任务现在会保存到 SQLite 数据库,支持: +1. ✅ 任务持久化(重启不丢失) +2. ✅ 客户后续增加需求细节 +3. ✅ 需求变更历史记录 +4. ✅ 任务状态追踪 + +--- + +## 🗄️ 数据库表结构 + +### 1. image_tasks(图片任务表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| task_id | TEXT | 任务 ID(主键) | +| customer_id | TEXT | 客户 ID | +| customer_name | TEXT | 客户名称 | +| original_image | TEXT | 原图 URL | +| operation | TEXT | 操作类型(enhance/remove_bg/vectorize) | +| requirements | TEXT | 需求 JSON(复杂度、比例、透视等) | +| customer_notes | TEXT | 客户备注/需求细节 | +| status | TEXT | 状态(pending/paid/processing/completed/failed) | +| created_at | TEXT | 创建时间 | +| paid_at | TEXT | 付款时间 | +| started_at | TEXT | 开始处理时间 | +| completed_at | TEXT | 完成时间 | +| result_image | TEXT | 结果图 URL | +| error_message | TEXT | 错误信息 | +| retry_count | INTEGER | 重试次数 | +| acc_id | TEXT | 店铺 ID | +| acc_type | TEXT | 平台类型 | + +--- + +### 2. task_requirement_changes(需求变更表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | INTEGER | 自增 ID(主键) | +| task_id | TEXT | 任务 ID(外键) | +| change_type | TEXT | 变更类型(add_note/modify_operation/add_requirement) | +| old_value | TEXT | 旧值 | +| new_value | TEXT | 新值 | +| changed_at | TEXT | 变更时间 | +| changed_by | TEXT | 变更者(customer/staff) | + +--- + +## 🎯 使用场景 + +### 场景 1:客户发送图片,创建任务 + +```python +# AI 识别图片后自动创建 +workflow.image_analysis_result( + customer_id="customer_123", + image_url="https://xxx.com/image.jpg", + complexity="normal", + aspect_ratio="1:1", + perspective="no" +) + +# 自动保存到数据库 +# 任务状态:pending(待付款) +``` + +--- + +### 场景 2:客户付款,开始处理 + +```python +# 客户付款后 +workflow.trigger_processing_on_payment( + customer_id="customer_123" +) + +# 任务状态更新:pending → paid → processing +``` + +--- + +### 场景 3:客户增加需求细节 + +```python +# 客户说:"对了,需要去掉背景" +await workflow.add_customer_requirement( + task_id="TASK_20260227_001", + customer_id="customer_123", + requirement="需要去掉背景" +) + +# 数据库记录: +# customer_notes: "[02-27 18:00] 需要去掉背景" +# 任务变更表新增记录 +``` + +--- + +### 场景 4:客户修改操作类型 + +```python +# 客户说:"改成要分层文件" +await workflow.modify_operation( + task_id="TASK_20260227_001", + customer_id="customer_123", + new_operation="remove_bg_and_layer" +) + +# 数据库更新: +# operation: "enhance" → "remove_bg_and_layer" +# 任务变更表新增记录 +``` + +--- + +### 场景 5:查看需求变更历史 + +```python +# 查看客户修改了哪些需求 +history = workflow.get_task_requirement_history("TASK_20260227_001") + +# 返回: +# [ +# {"change_type": "modify_operation", "old_value": "enhance", "new_value": "remove_bg_and_layer", ...}, +# {"change_type": "add_note", "old_value": "无", "new_value": "需要去掉背景", ...} +# ] +``` + +--- + +## 🔄 任务状态流转 + +``` +pending(待付款) + ↓ +paid(已付款) + ↓ +processing(处理中) + ↓ +awaiting_confirm(待确认) + ↓ +completed(已完成) + ↓ +[failed(失败)] ← 可能失败 +``` + +--- + +## 📝 API 接口 + +### 创建任务 +```python +workflow.create_image_task( + customer_id="customer_123", + original_image="https://xxx.com/image.jpg", + operation="enhance" +) +``` + +### 添加需求 +```python +await workflow.add_customer_requirement( + task_id="TASK_001", + customer_id="customer_123", + requirement="需要高分辨率" +) +``` + +### 修改操作 +```python +await workflow.modify_operation( + task_id="TASK_001", + customer_id="customer_123", + new_operation="vectorize" +) +``` + +### 查询任务 +```python +task = workflow.get_task("TASK_001") +tasks = workflow.get_customer_tasks("customer_123") +``` + +### 查询历史 +```python +history = workflow.get_task_requirement_history("TASK_001") +``` + +--- + +## 🔍 数据库操作 + +### 查看数据库文件 +```bash +sqlite3 /root/ai_customer_service/ai_cs/db/image_tasks.db +``` + +### 查询所有任务 +```sql +SELECT task_id, customer_id, status, created_at FROM image_tasks ORDER BY created_at DESC LIMIT 10; +``` + +### 查询待处理任务 +```sql +SELECT * FROM image_tasks WHERE status='pending'; +``` + +### 查询客户需求变更 +```sql +SELECT task_id, change_type, old_value, new_value, changed_at, changed_by +FROM task_requirement_changes +WHERE task_id='TASK_001' +ORDER BY changed_at DESC; +``` + +--- + +## ⚠️ 注意事项 + +1. **任务持久化**:所有任务自动保存到数据库,重启不丢失 +2. **需求变更**:客户可以随时增加需求细节(付款前) +3. **操作修改**:付款前可以修改操作类型,付款后不允许 +4. **历史记录**:所有变更都有记录,可追溯 +5. **状态管理**:任务状态自动流转,无需手动更新 + +--- + +## 📊 数据库位置 + +``` +/root/ai_customer_service/ai_cs/db/image_tasks.db +``` + +数据库管理器: +``` +/root/ai_customer_service/ai_cs/db/image_tasks_db.py +``` + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 + diff --git a/图绘派单系统集成说明.md b/图绘派单系统集成说明.md new file mode 100644 index 0000000..471925e --- /dev/null +++ b/图绘派单系统集成说明.md @@ -0,0 +1,278 @@ +# 图绘派单系统集成说明 + +## 📋 功能说明 + +AI 客服系统已接入图绘派单系统 API,实现自动派单给在线设计师。 + +--- + +## 🚀 派单系统 API + +### 基础信息 +- **API 地址**: http://1.12.50.92:8005 +- **API Key**: tuhui_dispatch_key_2026 +- **认证方式**: Header: X-API-Key + +### 核心接口 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/dispatch/queue` | GET | 获取派单队列 | +| `/online/designers` | GET | 获取在线设计师 | +| `/tasks` | POST | 创建任务 | +| `/tasks/{id}/assign` | POST | 分配任务 | +| `/tasks/{id}` | GET | 查询任务状态 | +| `/tasks/{id}/complete` | POST | 完成任务 | + +--- + +## 🔄 工作流程 + +### 转人工派单流程 + +``` +客户:这个能做吗 [图片] +AI: 抱歉,这个我做不了,帮您转接设计师 + ↓ +1. 查询在线设计师 + GET /online/designers + 返回:["橘子", "婷婷"] + ↓ +2. 创建派单任务 + POST /tasks + 返回:{"task_id": "ea853bd9"} + ↓ +3. 分配给设计师 + POST /tasks/ea853bd9/assign + {"designer_name": "橘子"} + ↓ +4. 企业微信通知设计师 + ↓ +5. 回复客户 + "好的,已帮您安排设计师处理,请稍候" +``` + +--- + +## 📝 API 调用示例 + +### 1. 查询在线设计师 + +```python +from services.service_tuhui_dispatch import get_tuhui_dispatch_client + +client = get_tuhui_dispatch_client() +designers = await client.get_online_designers() +# 返回:["橘子", "婷婷"] +``` + +### 2. 创建任务 + +```python +task_id = await client.create_task( + task_name="图片处理 -1234", + description="客户需要做高清修复", + task_type="image_process", + priority=2 +) +# 返回:"ea853bd9" +``` + +### 3. 分配任务 + +```python +success = await client.assign_task( + task_id="ea853bd9", + designer_name="橘子", + notes="AI 客服自动派单" +) +# 返回:True +``` + +### 4. 查询任务状态 + +```python +status = await client.get_task_status("ea853bd9") +# 返回:{"status": "assigned", "assigned_to": "橘子", ...} +``` + +### 5. 完成任务 + +```python +success = await client.complete_task( + task_id="ea853bd9", + notes="客户已确认,效果满意" +) +# 返回:True +``` + +--- + +## 🔧 集成代码位置 + +### 派单客户端 + +文件:`/root/ai_customer_service/ai_cs/services/service_tuhui_dispatch.py` + +**类**: `TuhuiDispatchClient` + +**方法**: +- `get_online_designers()` - 查询在线设计师 +- `create_task()` - 创建任务 +- `assign_task()` - 分配任务 +- `get_task_status()` - 查询状态 +- `complete_task()` - 完成任务 + +--- + +### 工作流集成 + +文件:`/root/ai_customer_service/ai_cs/core/workflow.py` + +**方法**: +- `transfer_to_designer_workflow()` - 转人工派单工作流 +- `_get_online_designers()` - 查询在线设计师 +- `_dispatch_to_designer()` - 派单给设计师 + +--- + +## 📊 派单逻辑 + +### 1. 查询在线设计师 + +```python +designers = await workflow._get_online_designers() +# 返回:["橘子", "婷婷"] +``` + +### 2. 创建派单任务 + +```python +dispatch_task_id = await workflow.dispatch_client.create_task( + task_name=f"图片处理-{customer_id[-4:]}", + description=f"{reason}\\n客户:{customer_id}\\n图片:{image_url}", + task_type="image_process", + priority=2 +) +``` + +### 3. 分配给设计师 + +```python +success = await workflow.dispatch_client.assign_task( + task_id=dispatch_task_id, + designer_name=designer_name, # "橘子" + notes=f"AI 客服自动派单\\n原因:{reason}\\n客户:{customer_id}" +) +``` + +### 4. 企业微信通知 + +```python +await _wechat_notify( + f"📋 **新任务派单**\\n" + f"设计师:{designer_name}\\n" + f"任务 ID: {dispatch_task_id}\\n" + f"客户:{customer_id}\\n" + f"原因:{reason}\\n" + f"请及时处理" +) +``` + +--- + +## ⚠️ 注意事项 + +1. **API Key**: 固定为 `tuhui_dispatch_key_2026` +2. **超时设置**: 默认 10 秒 +3. **错误处理**: 所有 API 调用都有 try-except +4. **企业微信通知**: 派单成功后自动发送 +5. **任务命名**: 格式为 `图片处理-{客户 ID 后 4 位}` + +--- + +## 🔍 日志查看 + +```bash +# 查看派单日志 +grep "派单" /tmp/ai-cs.log + +# 查看在线设计师查询 +grep "在线设计师" /tmp/ai-cs.log + +# 查看任务创建 +grep "创建任务" /tmp/ai-cs.log +``` + +--- + +## 📈 派单统计 + +### 查询派单队列 + +```bash +curl -X GET "http://1.12.50.92:8005/dispatch/queue" \ + -H "X-API-Key: tuhui_dispatch_key_2026" +``` + +**返回**: +```json +{ + "pending_tasks": {"count": 3, "tasks": [...]}, + "online_designers": {"count": 2, "designers": ["橘子", "婷婷"]}, + "suggestion": "橘子" +} +``` + +--- + +## 🎯 完整示例 + +### AI 客服转人工派单 + +```python +# 客户说"做不了" +await workflow.transfer_to_designer_workflow( + customer_id="customer_123", + image_url="https://xxx.com/image.jpg", + acc_id="shop_001", + acc_type="AliWorkbench", + reason="图片过于模糊,AI 无法处理" +) + +# 执行流程: +# 1. 查询在线设计师 → ["橘子", "婷婷"] +# 2. 创建任务 → "ea853bd9" +# 3. 分配给"橘子" +# 4. 企业微信通知 +# 5. 回复客户:"好的,已帮您安排设计师处理" +``` + +--- + +## 📞 技术支持 + +**派单系统服务器**: 1.12.50.92:8005 + +**查看派单系统日志**: +```bash +tail -f /var/log/dispatch_api.log +``` + +**查看派单进程**: +```bash +ps aux | grep dispatch +``` + +**重启派单服务**: +```bash +pkill -f dispatch_api +cd /root/tuhui/backend +nohup python3 -m uvicorn dispatch_api:app --host 0.0.0.0 --port 8005 > /var/log/dispatch_api.log 2>&1 & +``` + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 + diff --git a/文字加价功能说明.md b/文字加价功能说明.md new file mode 100644 index 0000000..4c84335 --- /dev/null +++ b/文字加价功能说明.md @@ -0,0 +1,225 @@ +# 图片文字检测与加价功能 + +## 📋 功能说明 + +AI 客服现在可以分析图片中的文字数量,并根据文字数量和分层需求自动加价。 + +--- + +## 💰 价格规则 + +### 基础价格 + +| 复杂度 | 价格区间 | 说明 | +|--------|----------|------| +| simple | 10-15 元 | 画面简单干净 | +| normal | 15-20 元 | 一般复杂度 | +| complex | 20-25 元 | 细节偏多 | +| hard | 25-30 元 | 非常复杂 | + +### 文字数量加价 + +| 文字数量 | 加价 | 说明 | +|----------|------|------| +| none | +0 元 | 无文字 | +| 少量 (1-10 字) | +5 元 | 少量文字 | +| 中量 (11-50 字) | +15 元 | 中量文字 | +| 大量 (51-200 字) | +30 元 | 大量文字 | +| 极多 (200 字以上) | +50 元 | 极多文字 | + +### 文字分层需求加价 + +| 分层需求 | 加价 | 说明 | +|----------|------|------| +| no | +0 元 | 普通图片处理 | +| yes (有文字) | +50 元起 | 可编辑分层文件(PSD 等) | +| yes (无文字) | +30 元 | 仅需分层文件 | + +### 特殊价格:文字分层 + 大量文字 + +**条件**:文字数量=大量/极多 且 文字分层需求=yes + +**价格范围**:60-80 元 + +--- + +## 🎯 使用场景 + +### 场景 1:少量文字,不分层 + +**客户**:[发送一张有 5 个字的图片] + +**AI 分析**: +- 复杂度:simple +- 文字数量:少量 (1-10 字) +- 分层需求:no + +**报价**: +``` +基础价格:15 元 +文字加价:+5 元 +总计:20 元 +``` + +**AI 回复**: +``` +这张图比较简单,不过有少量文字需要处理,20 元。 +``` + +--- + +### 场景 2:大量文字,需要分层 + +**客户**:[发送一张有 100 多字的图片] +**客户**:需要可以编辑的分层文件 + +**AI 分析**: +- 复杂度:complex +- 文字数量:大量 (51-200 字) +- 分层需求:yes + +**报价**: +``` +基础价格:25 元 +文字加价:+30 元 +分层加价:+50 元 +总计:105 元 → 调整到 80 元(特殊价格上限) +``` + +**AI 回复**: +``` +这张图文字比较多,有 100 多字,而且需要分层文件,处理起来比较麻烦,80 元。 +文字处理 +30 元,分层 +50 元。 +``` + +--- + +### 场景 3:极多文字,需要分层(高客单价) + +**客户**:[发送一张有 300 多字的图片] +**客户**:要 PSD 分层文件 + +**AI 分析**: +- 复杂度:hard +- 文字数量:极多 (200 字以上) +- 分层需求:yes + +**报价**: +``` +基础价格:30 元 +文字加价:+50 元 +分层加价:+50 元 +总计:130 元 → 调整到 80 元(特殊价格上限) +``` + +**AI 回复**: +``` +这张图文字非常多,有 300 多字,还需要分层文件,处理起来很费时间,80 元。 +``` + +--- + +## 🔧 技术实现 + +### 1. 图片分析器增强 + +文件:`/root/ai_customer_service/ai_cs/image/image_analyzer.py` + +**新增字段**: +- `text_amount`: 文字数量(none/少量/中量/大量/极多) +- `text_layer_need`: 分层需求(yes/no) +- `text_surcharge`: 文字加价金额 +- `layer_surcharge`: 分层加价金额 + +### 2. AI 客服话术 + +文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py` + +**报价说明**: +- 自动显示文字加价 +- 自动显示分层加价 +- 特殊价格自动调整到 60-80 元范围 + +--- + +## 📝 配置说明 + +### 价格配置 + +修改 `/root/ai_customer_service/ai_cs/image/image_analyzer.py` 中的价格规则: + +```python +# 文字数量加价 +text_surcharge = 0 +if text_amount == "少量 (1-10 字)": + text_surcharge = 5 +elif text_amount == "中量 (11-50 字)": + text_surcharge = 15 +elif text_amount == "大量 (51-200 字)": + text_surcharge = 30 +elif text_amount == "极多 (200 字以上)": + text_surcharge = 50 + +# 分层加价 +if text_layer_need == "yes": + layer_surcharge = max(50, price_suggest) + +# 特殊价格:60-80 元 +if text_amount in ["大量", "极多"] and text_layer_need == "yes": + price_suggest = max(60, min(80, price_suggest)) +``` + +--- + +## 📊 价格计算流程 + +``` +客户发送图片 + ↓ +AI 分析图片 + ↓ +┌───────────────────────┐ +│ 1. 判断基础复杂度 │ +│ simple/normal/ │ +│ complex/hard │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 2. 检测文字数量 │ +│ none/少量/中量/ │ +│ 大量/极多 │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 3. 询问分层需求 │ +│ yes/no │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 4. 计算总价 │ +│ 基础 + 文字 + 分层 │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 5. 特殊价格处理 │ +│ 60-80 元范围 │ +└───────────┬───────────┘ + ↓ + 报价给客户 +``` + +--- + +## ⚠️ 注意事项 + +1. **文字数量检测**:通过视觉 AI 自动识别 +2. **分层需求**:需要询问客户或从对话中识别 +3. **价格上限**:文字分层 + 大量文字最高 80 元 +4. **价格下限**:文字分层 + 大量文字最低 60 元 +5. **价格取整**:最终价格必须是 5 的倍数 + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 + diff --git a/部署文档.md b/部署文档.md new file mode 100644 index 0000000..6436105 --- /dev/null +++ b/部署文档.md @@ -0,0 +1,662 @@ +# AI 客服系统 - 天网协作版部署文档 + +## 📋 系统架构 + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ 天网服务器 │ ───→ │ AI 客服 API │ ───→ │ 企业微信 │ +│ (公网 IP) │ │ (127.0.0.1:6060)│ │ (轻简软件) │ +└─────────────┘ └──────────────┘ └─────────────┘ + ↑ │ + │ ↓ + └──────────────┐ + ┌──────────────┐ + │ SQLite │ + │ 任务数据库 │ + └──────────────┘ +``` + +### 核心组件 + +| 组件 | 地址 | 说明 | +|------|------|------| +| **AI 客服 HTTP API** | http://127.0.0.1:6060 | 接收天网任务 | +| **天网服务器** | 你的公网 IP | 任务调度中心 | +| **轻简软件** | ws://127.0.0.1:9528 | 企业微信连接 | +| **任务数据库** | SQLite | 本地存储 | + +--- + +## 🚀 快速部署 + +### 步骤 1:环境检查 + +```bash +# 检查 Python 版本 +python3 --version # 需要 3.8+ + +# 检查依赖 +pip3 list | grep -E "flask|pydantic|sqlite" +``` + +### 步骤 2:启动 AI 客服 API + +```bash +cd /root/ai_customer_service/ai_cs + +# 前台运行(测试用) +python3 run_tianwang_simple.py + +# 后台运行(生产用) +nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 & + +# 查看进程 +ps aux | grep run_tianwang + +# 查看日志 +tail -f /tmp/tianwang.log +``` + +### 步骤 3:测试 API + +```bash +# 健康检查 +curl http://localhost:6060/api/health + +# 预期输出: +# {"code":200,"data":{"service":"ai-cs-tianwang-bridge","timestamp":"..."},"message":"OK"} +``` + +--- + +## 📝 API 接口说明 + +### 1. 接收任务 + +**接口**: `POST /api/task/receive` + +**请求示例**: +```bash +curl -X POST http://localhost:6060/api/task/receive \ + -H "Content-Type: application/json" \ + -d '{ + "task_id": "TASK_20260227_001", + "type": "send_file_after_reply", + "customer": { + "id": "customer_123", + "name": "小明" + }, + "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" + }' +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "任务接收成功", + "data": { + "task_id": "TASK_20260227_001", + "status": "pending" + } +} +``` + +--- + +### 2. 查询任务状态 + +**接口**: `GET /api/task/status/:task_id` + +**请求示例**: +```bash +curl http://localhost:6060/api/task/status/TASK_20260227_001 +``` + +**响应示例**: +```json +{ + "code": 200, + "data": { + "task_id": "TASK_20260227_001", + "type": "send_file_after_reply", + "status": "pending", + "priority": "normal", + "retry_count": 0, + "created_at": "2026-02-27T18:00:00", + "created_by": "设计师 lz", + "triggered_at": null, + "completed_at": null, + "error_message": null + } +} +``` + +--- + +### 3. 取消任务 + +**接口**: `POST /api/task/cancel` + +**请求示例**: +```bash +curl -X POST http://localhost:6060/api/task/cancel \ + -H "Content-Type: application/json" \ + -d '{ + "task_id": "TASK_20260227_001", + "reason": "客户取消订单" + }' +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "任务已取消", + "data": { + "task_id": "TASK_20260227_001", + "status": "cancelled" + } +} +``` + +--- + +### 4. 查询任务列表 + +**接口**: `GET /api/task/list` + +**请求参数**: +- `customer_id` (可选): 客户 ID +- `status` (可选): 任务状态 +- `page` (可选): 页码,默认 1 +- `page_size` (可选): 每页数量,默认 20 + +**请求示例**: +```bash +curl "http://localhost:6060/api/task/list?status=pending&page=1&page_size=10" +``` + +--- + +### 5. 健康检查 + +**接口**: `GET /api/health` + +**请求示例**: +```bash +curl http://localhost:6060/api/health +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "OK", + "data": { + "timestamp": "2026-02-27T18:00:00", + "service": "ai-cs-tianwang-bridge" + } +} +``` + +--- + +## 📊 触发条件类型 + +### 1. specified_customer_reply(推荐) + +**指定客户回复指定内容** + +```json +{ + "trigger": { + "type": "specified_customer_reply", + "customer_id": "customer_123", + "customer_name": "小明", + "keyword": "好的", + "exact_match": false + } +} +``` + +**匹配逻辑**: +- ✅ 客户 ID 匹配 +- ✅ 客户名称匹配(可选) +- ✅ 消息包含关键词 + +--- + +### 2. customer_reply + +**任意客户回复指定内容** + +```json +{ + "trigger": { + "type": "customer_reply", + "keyword": "好的" + } +} +``` + +--- + +### 3. customer_keyword + +**任意客户说某关键词** + +```json +{ + "trigger": { + "type": "customer_keyword", + "keywords": ["好的", "可以", "行"] + } +} +``` + +--- + +### 4. customer_payment + +**客户付款** + +```json +{ + "trigger": { + "type": "customer_payment", + "keywords": ["已付款", "拍下了", "已下单"] + } +} +``` + +--- + +### 5. time_reach + +**到达指定时间** + +```json +{ + "trigger": { + "type": "time_reach", + "time": "2026-02-27 09:00:00" + } +} +``` + +--- + +## 🔧 配置说明 + +### 环境变量文件 + +位置:`/root/ai_customer_service/ai_cs/.env.tianwang` + +```bash +# AI 客服配置 +AI_CS_HOST=127.0.0.1 +AI_CS_PORT=6060 + +# AI 客服 HTTP API 地址(本地) +AI_CS_API_URL=http://127.0.0.1:6060 + +# 天网回调地址 +TIANWANG_CALLBACK_URL=http://127.0.0.1:6060/api/task/callback + +# API 接口 +TASK_RECEIVE=/api/task/receive +TASK_STATUS=/api/task/status +TASK_CANCEL=/api/task/cancel +TASK_LIST=/api/task/list +HEALTH=/api/health +``` + +--- + +### 数据库配置 + +位置:`/root/ai_customer_service/ai_cs/db/task_db/tasks.db` + +**数据库结构**: +```sql +CREATE TABLE tasks ( + task_id TEXT PRIMARY KEY, + specified_customer_id TEXT, + specified_customer_name TEXT, + type TEXT NOT NULL, + customer_name TEXT, + customer_id TEXT, + trigger_type TEXT, + trigger_keyword TEXT, + trigger_keywords TEXT, + action_type TEXT, + action_file_url TEXT, + action_message TEXT, + priority TEXT DEFAULT 'normal', + timeout_hours INTEGER DEFAULT 24, + status TEXT DEFAULT 'pending', + retry_count INTEGER DEFAULT 0, + max_retry INTEGER DEFAULT 3, + created_at TEXT, + created_by TEXT, + triggered_at TEXT, + completed_at TEXT, + error_message TEXT, + result TEXT +); +``` + +--- + +## 📖 使用场景 + +### 场景 1:客户说"好的"后发送文件 + +**天网下发任务**: +```json +{ + "task_id": "TASK_001", + "type": "send_file_after_reply", + "customer": { + "id": "customer_123", + "name": "小明" + }, + "trigger": { + "type": "specified_customer_reply", + "customer_id": "customer_123", + "customer_name": "小明", + "keyword": "好的" + }, + "action": { + "type": "send_message", + "message": "这是您要的文件" + }, + "priority": "normal", + "timeout_hours": 24, + "created_by": "设计师 lz" +} +``` + +**流程**: +1. 天网下发任务 → AI 客服 API +2. 客户在群里说"好的" +3. AI 客服检测到匹配任务 +4. 自动发送消息给客户 +5. 任务标记为 completed +6. 回调通知天网 + +--- + +### 场景 2:客户付款后发送教程 + +**天网下发任务**: +```json +{ + "task_id": "TASK_002", + "type": "send_tutorial_after_payment", + "customer": { + "id": "customer_456", + "name": "小红" + }, + "trigger": { + "type": "customer_payment" + }, + "action": { + "type": "send_message", + "message": "感谢购买!这是使用教程:..." + }, + "created_by": "设计师 lz" +} +``` + +--- + +### 场景 3:定时发送问候 + +**天网下发任务**: +```json +{ + "task_id": "TASK_003", + "type": "send_greeting", + "customer": { + "id": "customer_789" + }, + "trigger": { + "type": "time_reach", + "time": "2026-02-28 09:00:00" + }, + "action": { + "type": "send_message", + "message": "早上好!有什么可以帮您?" + }, + "created_by": "设计师 lz" +} +``` + +--- + +## 🔍 监控与日志 + +### 查看进程状态 + +```bash +# 查看进程 +ps aux | grep run_tianwang + +# 查看端口 +netstat -tlnp | grep 6060 +``` + +### 查看日志 + +```bash +# 实时日志 +tail -f /tmp/tianwang.log + +# 最近 100 行 +tail -100 /tmp/tianwang.log + +# 搜索关键词 +grep "任务" /tmp/tianwang.log +``` + +### 查看数据库 + +```bash +# 进入 SQLite +sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db + +# 查看所有任务 +SELECT task_id, type, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 10; + +# 查看待触发任务 +SELECT * FROM tasks WHERE status='pending'; + +# 查看失败任务 +SELECT task_id, error_message FROM tasks WHERE status='failed'; + +# 查看统计数据 +SELECT status, COUNT(*) as count FROM tasks GROUP BY status; + +# 退出 +.exit +``` + +--- + +## ⚠️ 故障排查 + +### 问题 1:API 无法访问 + +**症状**: `curl http://localhost:6060/api/health` 无响应 + +**解决**: +```bash +# 1. 检查进程 +ps aux | grep run_tianwang + +# 2. 检查端口 +netstat -tlnp | grep 6060 + +# 3. 重启服务 +pkill -f run_tianwang +cd /root/ai_customer_service/ai_cs +nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 & + +# 4. 查看日志 +tail -f /tmp/tianwang.log +``` + +--- + +### 问题 2:任务接收失败 + +**症状**: POST /api/task/receive 返回 500 错误 + +**解决**: +```bash +# 1. 查看日志 +tail -f /tmp/tianwang.log | grep "ERROR" + +# 2. 检查数据库 +sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db ".schema tasks" + +# 3. 测试数据库 +sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db "SELECT 1;" + +# 4. 如果数据库损坏,删除重建 +rm /root/ai_customer_service/ai_cs/db/task_db/tasks.db +# 重启服务后会自动创建 +``` + +--- + +### 问题 3:任务未触发 + +**症状**: 任务一直是 pending 状态 + +**解决**: +```bash +# 1. 检查任务状态 +curl http://localhost:6060/api/task/status/TASK_ID + +# 2. 查看触发日志 +grep "任务触发" /tmp/tianwang.log + +# 3. 检查触发条件 +# 确认客户消息包含触发关键词 + +# 4. 手动触发测试 +sqlite3 /root/ai_customer_service/ai_cs/db/task_db/tasks.db \ + "UPDATE tasks SET status='pending' WHERE task_id='TASK_ID';" +``` + +--- + +### 问题 4:内存占用过高 + +**症状**: 进程内存持续增长 + +**解决**: +```bash +# 1. 查看内存使用 +ps aux | grep run_tianwang | awk '{print $6/1024 " MB"}' + +# 2. 定期重启(建议每天) +crontab -e +# 添加:0 3 * * * pkill -f run_tianwang && sleep 2 && nohup python3 /root/ai_customer_service/ai_cs/run_tianwang_simple.py > /tmp/tianwang.log 2>&1 & +``` + +--- + +## 📞 技术支持 + +### 文件位置 + +| 文件 | 路径 | +|------|------| +| **启动脚本** | `/root/ai_customer_service/ai_cs/run_tianwang_simple.py` | +| **HTTP API** | `/root/ai_customer_service/ai_cs/api/http_server.py` | +| **任务调度** | `/root/ai_customer_service/ai_cs/core/task_scheduler.py` | +| **数据模型** | `/root/ai_customer_service/ai_cs/db/task_db/task_model.py` | +| **配置文件** | `/root/ai_customer_service/ai_cs/.env.tianwang` | +| **日志文件** | `/tmp/tianwang.log` | +| **数据库** | `/root/ai_customer_service/ai_cs/db/task_db/tasks.db` | + +### 文档位置 + +| 文档 | 路径 | +|------|------| +| **部署文档** | `/root/ai_customer_service/ai_cs/部署文档.md` | +| **功能文档** | `/root/ai_customer_service/ai_cs/TIANWANG_INTEGRATION.md` | +| **使用示例** | `/root/ai_customer_service/ai_cs/SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md` | + +--- + +## 📧 发送文档 + +如需发送此文档,可以: + +### 方式 1:复制内容 +```bash +# 复制文档内容 +cat /root/ai_customer_service/ai_cs/部署文档.md +``` + +### 方式 2:生成 PDF +```bash +# 安装 pandoc +apt-get install pandoc + +# 转换为 PDF +pandoc /root/ai_customer_service/ai_cs/部署文档.md -o 部署文档.pdf +``` + +### 方式 3:发送邮件 +```bash +# 使用 mail 命令 +mail -s "AI 客服系统部署文档" recipient@example.com < /root/ai_customer_service/ai_cs/部署文档.md +``` + +--- + +## 🎯 快速参考卡片 + +``` +┌─────────────────────────────────────────────┐ +│ AI 客服 API - 快速参考 │ +├─────────────────────────────────────────────┤ +│ 地址:http://localhost:6060 │ +│ │ +│ POST /api/task/receive - 接收任务 │ +│ GET /api/task/status/:id - 查询状态 │ +│ POST /api/task/cancel - 取消任务 │ +│ GET /api/task/list - 任务列表 │ +│ GET /api/health - 健康检查 │ +│ │ +│ 启动:python3 run_tianwang_simple.py │ +│ 日志:tail -f /tmp/tianwang.log │ +│ 数据库:sqlite3 db/task_db/tasks.db │ +└─────────────────────────────────────────────┘ +``` + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 +**维护者**: AI 客服系统团队 + diff --git a/项目功能汇总.md b/项目功能汇总.md new file mode 100644 index 0000000..e75fffe --- /dev/null +++ b/项目功能汇总.md @@ -0,0 +1,373 @@ +# AI 客服系统 - 完整功能汇总 + +**版本**: v1.0 +**更新日期**: 2026-02-27 +**服务器**: 1.12.50.92 + +--- + +## 📋 目录 + +1. [核心功能](#核心功能) +2. [图片处理](#图片处理) +3. [任务管理](#任务管理) +4. [派单系统](#派单系统) +5. [价格策略](#价格策略) +6. [风险控制](#风险控制) +7. [技术架构](#技术架构) + +--- + +## 核心功能 + +### 1. 天网协作系统 + +**说明**: 接收天网下发的任务,支持指定客户回复触发 + +**API 接口**: `http://127.0.0.1:6060` + +**功能**: +- ✅ 任务接收 (`POST /api/task/receive`) +- ✅ 任务查询 (`GET /api/task/status/:id`) +- ✅ 任务取消 (`POST /api/task/cancel`) +- ✅ 任务列表 (`GET /api/task/list`) + +**触发类型**: +- `specified_customer_reply` - 指定客户回复指定内容 +- `customer_keyword` - 任意客户说某关键词 +- `customer_payment` - 客户付款 +- `time_reach` - 到达指定时间 + +**文档**: `TIANWANG_INTEGRATION.md` + +--- + +### 2. 三种工作流 + +根据客户说的话自动判断执行不同工作流: + +#### 工作流 1: 查找图片 +**触发词**: "找一下"、"找图"、"找原图" + +**流程**: +``` +客户:找一下这个图 [图片] + ↓ +AI 处理 → 上传到图绘 + ↓ +回复:"找到了!http://tuhui.cloud/works/123" +``` + +#### 工作流 2: 处理图片 +**触发词**: "做一下"、"处理一下"、"安排" + +**流程**: +``` +客户:做一下 [图片] + ↓ +AI: "稍等,我看看...好的,可以做" + ↓ +启动图片处理流程 +``` + +#### 工作流 3: 转人工派单 +**触发词**: "做不了"、"处理不了" + +**流程**: +``` +AI: 做不了 + ↓ +查询在线设计师 + ↓ +派单给设计师 + ↓ +回复:"已安排设计师处理" +``` + +**文档**: `三种工作流功能说明.md` + +--- + +## 图片处理 + +### 1. 文字检测与加价 + +**功能**: AI 自动识别图片文字数量,根据文字数量加价 + +**价格规则**: + +| 文字数量 | 加价 | +|----------|------| +| 少量 (1-10 字) | +5 元 | +| 中量 (11-50 字) | +15 元 | +| 大量 (51-200 字) | +30 元 | +| 极多 (200 字以上) | +50 元 | + +**分层加价**: +- 有文字 + 分层:+50 元起 +- 无文字 + 分层:+30 元 + +**特殊价格**: 文字分层 + 大量文字 → 60-80 元 + +**文档**: `文字加价功能说明.md` + +--- + +### 2. 风险评估与接单判断 + +**风险等级**: +- **敏感内容** (一票否决): 色情/暴力/涉政 → 直接拒绝 +- **none**: 印花/图案/logo → 直接接单 +- **low**: 有人脸但清晰 → 接单,说明风险 +- **high**: 严重模糊/老照片 → 谨慎接单 + +**可做判断**: +- `yes`: 效果有把握,接单 +- `partial`: 有限制,谨慎接单 +- `no`: 无法处理,不接单 + +**文档**: `风险评估功能说明.md` + +--- + +### 3. 作图失败转接人工 + +**触发条件**: +- API 调用失败 +- 图片处理超时 +- 客户不满意 + +**流程**: +``` +作图失败 + ↓ +通知客户 + ↓ +转接人工客服 + ↓ +企业微信预警 +``` + +**文档**: `作图失败转接人工说明.md` + +--- + +## 任务管理 + +### 1. 图片任务数据库 + +**功能**: 任务持久化,支持客户后续增加需求 + +**数据库表**: +- `image_tasks` - 图片任务表 +- `task_requirement_changes` - 需求变更表 + +**客户可以增加需求**: +```python +await workflow.add_customer_requirement( + task_id="TASK_001", + customer_id="customer_123", + requirement="需要去掉背景" +) +``` + +**任务状态流转**: +``` +pending → paid → processing → awaiting_confirm → completed +``` + +**文档**: `图片任务数据库功能说明.md` + +--- + +## 派单系统 + +### 1. 图绘派单系统 + +**API 地址**: `http://1.12.50.92:8005` +**API Key**: `tuhui_dispatch_key_2026` + +**核心接口**: +- `GET /dispatch/queue` - 获取派单队列 +- `GET /online/designers` - 查询在线设计师 +- `POST /tasks` - 创建任务 +- `POST /tasks/{id}/assign` - 分配任务 +- `POST /tasks/{id}/complete` - 完成任务 + +**派单流程**: +``` +1. 查询在线设计师 → ["橘子", "婷婷"] +2. 创建任务 → {"task_id": "ea853bd9"} +3. 分配给设计师 → designer: "橘子" +4. 企业微信通知 +5. 回复客户 +``` + +**文档**: `图绘派单系统集成说明.md` + +--- + +## 价格策略 + +### 基础价格 + +| 复杂度 | 价格 | +|--------|------| +| simple | 10-15 元 | +| normal | 15-20 元 | +| complex | 20-25 元 | +| hard | 25-30 元 | + +### 加价规则 + +**文字加价**: +- 少量:+5 元 +- 中量:+15 元 +- 大量:+30 元 +- 极多:+50 元 + +**分层加价**: +- 有文字:+50 元起 +- 无文字:+30 元 + +**高价值订单**: 文字分层 + 大量文字 → 60-80 元 + +--- + +## 风险控制 + +### 敏感内容检测 + +**一票否决**: +- ❌ 色情/黄色/擦边 +- ❌ 涉政/政治敏感 +- ❌ 暴力/血腥 +- ❌ 违禁品 + +**话术**: +- "这类不做哦" +- "不好意思,这个接不了" + +### 高风险图片 + +**谨慎接单**: +- ⚠️ 严重模糊的人脸 +- ⚠️ 老照片人像 +- ⚠️ 需要打印 + +**话术**: +- "这张比较模糊,修复后人脸可能有差异" +- "建议先看效果确认再决定" + +--- + +## 技术架构 + +### 核心组件 + +| 组件 | 文件 | 说明 | +|------|------|------| +| **天网协作** | `api/http_server.py` | HTTP API 服务器 | +| **工作流程** | `core/workflow.py` | 工作流执行器 | +| **AI Agent** | `core/pydantic_ai_agent.py` | AI 对话引擎 | +| **图片分析** | `image/image_analyzer.py` | 图片复杂度识别 | +| **派单客户端** | `services/service_tuhui_dispatch.py` | 图绘派单 API | +| **任务数据库** | `db/image_tasks_db.py` | 任务持久化 | + +### 数据库 + +| 数据库 | 文件 | 说明 | +|--------|------|------| +| **任务数据库** | `db/image_tasks.db` | 图片任务 | +| **客户档案** | `customer_db/customer.db` | 客户画像 | +| **聊天记录** | `chat_log_db/chat_log.db` | 聊天历史 | + +### API 端口 + +| 服务 | 端口 | 说明 | +|------|------|------| +| **AI 客服 API** | 6060 | 天网任务接收 | +| **派单系统** | 8005 | 设计师派单 | +| **图绘平台** | 8002 | 图片上传 | + +--- + +## 部署说明 + +### 启动方式 + +```bash +cd /root/ai_customer_service/ai_cs + +# 方式 1: 天网协作版 +python3 run_tianwang_simple.py + +# 方式 2: 完整版 +python3 run_with_tianwang.py + +# 方式 3: AI 客服 +python3 run.py +``` + +### 后台运行 + +```bash +nohup python3 run_tianwang_simple.py > /tmp/tianwang.log 2>&1 & +``` + +### 查看日志 + +```bash +tail -f /tmp/tianwang.log +``` + +--- + +## 文档清单 + +| 文档 | 说明 | +|------|------| +| `README.md` | 项目说明 | +| `DEPLOYMENT.md` | 部署文档 | +| `TIANWANG_INTEGRATION.md` | 天网协作 | +| `三种工作流功能说明.md` | 工作流 | +| `文字加价功能说明.md` | 价格策略 | +| `风险评估功能说明.md` | 风险控制 | +| `图片任务数据库功能说明.md` | 任务管理 | +| `图绘派单系统集成说明.md` | 派单系统 | +| `作图失败转接人工说明.md` | 失败处理 | + +--- + +## 快速参考 + +### API 调用示例 + +**接收天网任务**: +```bash +curl -X POST http://localhost:6060/api/task/receive \ + -H "Content-Type: application/json" \ + -d '{ + "task_id": "TASK_001", + "customer": {"id": "customer_123"}, + "trigger": {"type": "customer_reply", "keyword": "好的"}, + "action": {"type": "send_message", "message": "您好"} + }' +``` + +**查询在线设计师**: +```bash +curl -X GET "http://1.12.50.92:8005/online/designers" \ + -H "X-API-Key: tuhui_dispatch_key_2026" +``` + +**派单队列**: +```bash +curl -X GET "http://1.12.50.92:8005/dispatch/queue" \ + -H "X-API-Key: tuhui_dispatch_key_2026" +``` + +--- + +**完整功能已部署,所有系统运行正常!** 🎉 + diff --git a/风险评估功能说明.md b/风险评估功能说明.md new file mode 100644 index 0000000..0c00b5b --- /dev/null +++ b/风险评估功能说明.md @@ -0,0 +1,270 @@ +# 图片风险评估与接单判断功能 + +## 📋 功能说明 + +AI 客服会自动分析图片的風險,判断是否可以接单。 + +--- + +## ⚠️ 风险等级 + +### 1. 敏感内容检测(一票否决) + +**敏感内容 = yes** → **直接拒绝,不接单** + +检测内容: +- ❌ 色情/黄色/擦边/裸露 +- ❌ 性暗示/大尺度 +- ❌ 涉政/政治敏感 +- ❌ 暴力/血腥 +- ❌ 违禁品 + +**话术**: +- "这类不做哦" +- "不好意思,这个接不了" +- "这类图片不处理的" + +--- + +### 2. 风险评估 + +| 风险等级 | 是否接单 | 说明 | +|----------|----------|------| +| **none** | ✅ 接单 | 印花/图案/logo/风景/产品,效果稳定 | +| **low** | ✅ 接单 | 有人脸但清晰,需说明风险(人脸相似度 70-90%) | +| **high** | ⚠️ 谨慎接单 | 严重模糊/老照片人像/需打印,需说明限制 | + +--- + +### 3. 可做判断 + +| 可做 | 是否接单 | 说明 | +|------|----------|------| +| **yes** | ✅ 接单 | 效果有把握 | +| **partial** | ⚠️ 可接单 | 能处理但有限制,需说明风险 | +| **no** | ❌ 不接单 | 无法处理 | + +--- + +## 🎯 使用场景 + +### 场景 1:敏感内容 - 拒绝 + +**客户**:[发送擦边图片] + +**AI 分析**: +- 敏感内容:yes +- 可做:no + +**AI 回复**: +``` +这类不做哦。 +``` + +**不说的话**: +- ❌ "发图来看看" +- ❌ "这个处理不了" +- ❌ 过多解释 + +--- + +### 场景 2:高风险 - 谨慎接单 + +**客户**:[发送严重模糊的老照片] + +**AI 分析**: +- 风险:high +- 可做:partial + +**AI 回复**: +``` +这张照片比较模糊,修复后清晰了但人脸可能跟原来有差异,建议先看效果确认再决定,20 元。 +``` + +--- + +### 场景 3:低风险 - 正常接单 + +**客户**:[发送清晰的人像照片] + +**AI 分析**: +- 风险:low +- 可做:yes + +**AI 回复**: +``` +可以处理,修复后人脸相似度大概 70-90%,20 元,满意再付。 +``` + +--- + +### 场景 4:无风险 - 直接接单 + +**客户**:[发送印花图案] + +**AI 分析**: +- 风险:none +- 可做:yes + +**AI 回复**: +``` +这个没问题,15 元,拍下就处理。 +``` + +--- + +## 🔧 技术实现 + +### 分析流程 + +``` +客户发送图片 + ↓ +AI 视觉分析 + ↓ +┌───────────────────────┐ +│ 1. 敏感内容检测 │ +│ yes → 拒绝不接单 │ +│ no → 继续判断 │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 2. 风险评估 │ +│ none/low/high │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 3. 可做判断 │ +│ yes/partial/no │ +└───────────┬───────────┘ + ↓ +┌───────────────────────┐ +│ 4. 决策 │ +│ 接单/谨慎接单/拒绝 │ +└───────────┬───────────┘ + ↓ + 回复客户 +``` + +--- + +## 📝 判断规则 + +### 敏感内容(一票否决) + +**检测到以下任一内容 → 敏感内容=yes → 不接单**: +- 色情/黄色/擦边/裸露 +- 性暗示/大尺度 +- 涉政/政治敏感 +- 暴力/血腥 +- 违禁品 + +### 风险评估 + +**none(无风险)**: +- ✅ 印花/图案/logo +- ✅ 风景/产品 +- ✅ AI 处理效果稳定 + +**low(低风险)**: +- ✅ 有人脸但清晰 +- ✅ AI 修复后人脸相似度 70-90% +- ⚠️ 需说明风险 + +**high(高风险)**: +- ⚠️ 严重模糊的人脸照片 +- ⚠️ 老照片人像 +- ⚠️ 需要打印 +- ⚠️ 客户问能否找回原图 +- ⚠️ 谨慎接单,说明限制 + +### 可做判断 + +**yes(可接单)**: +- ✅ 效果有把握 +- ✅ 可直接处理 + +**partial(谨慎接单)**: +- ⚠️ 能处理但有明显限制 +- ⚠️ 人脸变形风险 +- ⚠️ 分辨率极低 +- ⚠️ 严重损坏 +- ⚠️ 需说明风险 + +**no(不接单)**: +- ❌ 纯黑/纯白 +- ❌ 完全损坏 +- ❌ 找原始 RAW 文件 +- ❌ 敏感内容 +- ❌ 违法内容 + +--- + +## 📊 话术模板 + +### 拒绝话术(敏感内容) + +``` +- 这类不做哦 +- 不好意思,这个接不了 +- 这类图片不处理的 +- 这个做不了哈 +``` + +**不要说**: +- ❌ "发图来看看" +- ❌ 过多解释 +- ❌ "处理不了" + +--- + +### 风险提示话术(高风险) + +``` +- 这张比较模糊,修复后清晰了但人脸可能跟原来有差异 +- 老照片修复后人脸可能有轻微变化 +- 建议先看效果确认再打印 +- 这张模糊程度较高,修复后清晰了但人脸可能跟原来有差异 +``` + +--- + +### 正常接单话术 + +``` +- 这个没问题,XX 元 +- 可以处理,XX 元,拍下就处理 +- 好的,XX 元,满意再付 +``` + +--- + +## ⚠️ 注意事项 + +1. **敏感内容优先判断**:先检测敏感内容,再判断其他 +2. **敏感内容=yes → 可做=no**:必须拒绝 +3. **高风险谨慎接单**:说明限制和风险 +4. **拒绝话术简洁**:不要过多解释 +5. **风险告知**:low/high 风险都要告知客户 + +--- + +## 🔍 配置说明 + +### 修改风险判断规则 + +文件:`/root/ai_customer_service/ai_cs/image/image_analyzer.py` + +查找`【风险评估】`和`【敏感内容检测】`部分修改规则。 + +### 修改拒绝话术 + +文件:`/root/ai_customer_service/ai_cs/core/pydantic_ai_agent.py` + +查找`【拒绝】`部分修改拒绝话术。 + +--- + +**文档版本**: v1.0 +**更新日期**: 2026-02-27 +