From a6c42d505a0235045458c3fceb1209c961e33e8f Mon Sep 17 00:00:00 2001 From: ZuoWei <1847930177@qq.com> Date: Sat, 28 Feb 2026 11:20:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=95=B4=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 天网协作系统 (HTTP API 端口 6060) - 三种工作流 (查找图片/处理图片/转人工派单) - 图片任务数据库 (支持客户后续增加需求) - 图绘派单系统集成 (API: 8005) - 文字检测与加价 (60-80 元高价值订单) - 风险评估与接单判断 - 作图失败自动转人工 新增文档: - 项目功能汇总.md - 三种工作流功能说明.md - 文字加价功能说明.md - 风险评估功能说明.md - 图片任务数据库功能说明.md - 图绘派单系统集成说明.md - 作图失败转接人工说明.md - DEPLOYMENT.md - TIANWANG_INTEGRATION.md 核心修改: - core/pydantic_ai_agent.py - core/workflow.py - core/websocket_client.py - image/image_analyzer.py - services/service_tuhui_dispatch.py - db/image_tasks_db.py 版本:v1.0 日期:2026-02-28 --- .env | 6 + .env.example | 0 .env.tianwang | 20 + .gitignore | 36 + DEPLOYMENT.md | 325 +++++++++ FIX_SUMMARY.md | 105 +++ MULTI_PROCESS.md | 226 ++++++ README.md | 438 ++++-------- SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md | 239 +++++++ TIANWANG_INTEGRATION.md | 402 +++++++++++ __pycache__/chat_log_db.cpython-310.pyc | Bin __pycache__/customer_db.cpython-310.pyc | Bin __pycache__/daily_summary.cpython-310.pyc | Bin __pycache__/email_receiver.cpython-310.pyc | Bin __pycache__/email_sender.cpython-310.pyc | Bin __pycache__/image_analyzer.cpython-310.pyc | Bin __pycache__/image_processor.cpython-310.pyc | Bin __pycache__/image_qa.cpython-310.pyc | Bin __pycache__/image_tools.cpython-310.pyc | Bin __pycache__/perspective_fix.cpython-310.pyc | Bin __pycache__/pydantic_ai_agent.cpython-310.pyc | Bin __pycache__/service_gemini.cpython-310.pyc | Bin __pycache__/service_qwen.cpython-310.pyc | Bin __pycache__/workflow.cpython-310.pyc | Bin api/__pycache__/http_server.cpython-311.pyc | Bin 0 -> 10863 bytes api/http_server.py | 307 ++++++++ archive/README.md | 0 archive/test_battle.py | 0 archive/test_import.py | 0 archive/view_chats.py | 0 archive/viewer_out.txt | 0 chat_log_db/chats.db | Bin config/.api_cost.json | 0 config/DESIGNER_ROSTER_API.md | 0 config/DESIGNER_ROSTER_需求-给另一台AI.md | 0 config/README.md | 0 config/__init__.py | 0 config/__pycache__/__init__.cpython-310.pyc | Bin config/__pycache__/config.cpython-310.pyc | Bin config/config.py | 0 config/shop_prompts.json | 0 config/transfer_groups.json | 0 core/__init__.py | 0 core/__pycache__/__init__.cpython-310.pyc | Bin .../pydantic_ai_agent.cpython-310.pyc | Bin .../websocket_client.cpython-310.pyc | Bin core/__pycache__/workflow.cpython-310.pyc | Bin core/pydantic_ai_agent.py | 117 +++- core/task_scheduler.py | 289 ++++++++ core/task_trigger.py | 211 ++++++ core/websocket_client.py | 116 ++- core/workflow.py | 371 +++++++++- core/workflow_router.py | 93 +++ customer_db/customers.json | 0 customer_db/schema.json | 0 db/__init__.py | 0 db/__pycache__/__init__.cpython-310.pyc | Bin db/__pycache__/chat_log_db.cpython-310.pyc | Bin db/__pycache__/customer_db.cpython-310.pyc | Bin .../deal_outcome_db.cpython-310.pyc | Bin .../designer_roster_db.cpython-310.pyc | Bin db/chat_log_db.py | 0 db/chat_log_db/chats.db | Bin db/customer_db.py | 0 db/deal_outcome_db.py | 0 db/deal_outcome_db/outcomes.db | Bin db/designer_roster_db.py | 0 db/designer_roster_db/roster.db | Bin db/image_tasks_db.py | 412 +++++++++++ .../__pycache__/task_model.cpython-311.pyc | Bin 0 -> 17572 bytes db/task_db/task_model.py | 321 +++++++++ db/task_db/tasks.db | Bin 0 -> 24576 bytes .../price_negotiation_with_registration.md | 101 +++ features/risk_customer_detection.md | 129 ++++ features/text_surcharge.md | 158 +++++ features/tuhui_upload.md | 137 ++++ image/__init__.py | 0 image/__pycache__/__init__.cpython-310.pyc | Bin .../image_analyzer.cpython-310.pyc | Bin .../image_precheck.cpython-310.pyc | Bin .../image_processor.cpython-310.pyc | Bin image/__pycache__/image_qa.cpython-310.pyc | Bin image/image_analyzer.py | 95 ++- image/image_precheck.py | 0 image/image_processor.py | 0 image/image_qa.py | 0 image/image_tools.py | 0 image/perspective_fix.py | 0 logs/chat_2026-02-25.log | 0 logs/chat_2026-02-26.log | 0 logs/chat_2026-02-27.log | 0 mail/__init__.py | 0 mail/__pycache__/__init__.cpython-310.pyc | Bin .../email_receiver.cpython-310.pyc | Bin mail/__pycache__/email_sender.cpython-310.pyc | Bin mail/email_receiver.py | 0 mail/email_sender.py | 0 requirements.txt | 0 results/20260225211854.jpg | Bin ...debug_7debc0124b0441da9945feaeceef93b1.jpg | Bin ...final_73bc9c0c4bed4be198b200158be6f813.jpg | Bin ...final_7debc0124b0441da9945feaeceef93b1.jpg | Bin ...final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg | Bin ...final_bc3c45fd447749f38f62dbb87a942aba.jpg | Bin ...final_d9679c27640b43c18b9f590047e6c2dd.jpg | Bin ...esize_95152a96618146738c3e6a12a6a6d9d8.jpg | Bin ...esize_d9ef87fa8de14b0b8d030067d0de163e.jpg | Bin ...esult_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg | Bin ...esult_3e60b204f3a748eabb41a05cc28e1a11.jpg | Bin ...esult_4cd07206b2d24c21a81c3d45a3c4e16f.jpg | Bin ...esult_5c19d435fc8e4b2caa03c589f53d61ac.jpg | Bin ...esult_90eaf777934445af81abbd60fe4778c5.jpg | Bin run.py | 82 ++- run_tianwang_simple.py | 60 ++ run_with_tianwang.py | 84 +++ scripts/__pycache__/chat_ui.cpython-310.pyc | Bin scripts/chat_log_viewer.py | 0 scripts/chat_ui.py | 0 scripts/init_designer_roster.py | 0 scripts/multi_process_launcher.py | 196 ++++++ services/__init__.py | 0 services/__pycache__/__init__.cpython-310.pyc | Bin .../service_gemini.cpython-310.pyc | Bin .../__pycache__/service_meitu.cpython-310.pyc | Bin .../service_vectorizer.cpython-310.pyc | Bin services/service_gemini.py | 0 services/service_meitu.py | 0 services/service_qwen.py | 0 services/service_tuhui_dispatch.py | 229 ++++++ services/service_tuhui_upload.py | 172 +++++ services/service_vectorizer.py | 0 skills/customer-service/SKILL.md | 0 skills/customer-service/customer-skill.md | 339 +++++++++ skills/gemini-api/SKILL.md | 0 skills/owner-style/SKILL.md | 0 tests/test_config.py | 0 tests/test_designer_roster.py | 0 tests/test_health_check.py | 0 tests/test_image_queue.py | 0 tests/test_process.py | 0 tests/test_transfer_flow.py | 0 tests/test_transfer_groups.py | 0 tests/test_wechat_alert.py | 0 utils/__init__.py | 0 utils/__pycache__/__init__.cpython-310.pyc | Bin .../api_cost_tracker.cpython-310.pyc | Bin .../content_filter.cpython-310.pyc | Bin .../__pycache__/daily_summary.cpython-310.pyc | Bin .../designer_roster.cpython-310.pyc | Bin .../__pycache__/health_check.cpython-310.pyc | Bin utils/__pycache__/image_queue.cpython-310.pyc | Bin .../intent_analyzer.cpython-310.pyc | Bin .../__pycache__/service_base.cpython-310.pyc | Bin .../wechat_chat_log.cpython-310.pyc | Bin utils/api_cost_tracker.py | 0 utils/content_filter.py | 0 utils/daily_summary.py | 0 utils/designer_roster.py | 0 utils/health_check.py | 0 utils/image_queue.py | 0 utils/intent_analyzer.py | 0 utils/service_base.py | 0 utils/wechat_chat_log.py | 0 三种工作流功能说明.md | 229 ++++++ 作图失败转接人工说明.md | 207 ++++++ 图片任务数据库功能说明.md | 247 +++++++ 图绘派单系统集成说明.md | 278 ++++++++ 文字加价功能说明.md | 225 ++++++ 部署文档.md | 662 ++++++++++++++++++ 项目功能汇总.md | 373 ++++++++++ 风险评估功能说明.md | 270 +++++++ 171 files changed, 7979 insertions(+), 328 deletions(-) mode change 100644 => 100755 .env mode change 100644 => 100755 .env.example create mode 100644 .env.tianwang create mode 100644 .gitignore create mode 100644 DEPLOYMENT.md create mode 100644 FIX_SUMMARY.md create mode 100644 MULTI_PROCESS.md mode change 100644 => 100755 README.md create mode 100644 SPECIFIED_CUSTOMER_REPLY_EXAMPLE.md create mode 100644 TIANWANG_INTEGRATION.md mode change 100644 => 100755 __pycache__/chat_log_db.cpython-310.pyc mode change 100644 => 100755 __pycache__/customer_db.cpython-310.pyc mode change 100644 => 100755 __pycache__/daily_summary.cpython-310.pyc mode change 100644 => 100755 __pycache__/email_receiver.cpython-310.pyc mode change 100644 => 100755 __pycache__/email_sender.cpython-310.pyc mode change 100644 => 100755 __pycache__/image_analyzer.cpython-310.pyc mode change 100644 => 100755 __pycache__/image_processor.cpython-310.pyc mode change 100644 => 100755 __pycache__/image_qa.cpython-310.pyc mode change 100644 => 100755 __pycache__/image_tools.cpython-310.pyc mode change 100644 => 100755 __pycache__/perspective_fix.cpython-310.pyc mode change 100644 => 100755 __pycache__/pydantic_ai_agent.cpython-310.pyc mode change 100644 => 100755 __pycache__/service_gemini.cpython-310.pyc mode change 100644 => 100755 __pycache__/service_qwen.cpython-310.pyc mode change 100644 => 100755 __pycache__/workflow.cpython-310.pyc create mode 100644 api/__pycache__/http_server.cpython-311.pyc create mode 100644 api/http_server.py mode change 100644 => 100755 archive/README.md mode change 100644 => 100755 archive/test_battle.py mode change 100644 => 100755 archive/test_import.py mode change 100644 => 100755 archive/view_chats.py mode change 100644 => 100755 archive/viewer_out.txt mode change 100644 => 100755 chat_log_db/chats.db mode change 100644 => 100755 config/.api_cost.json mode change 100644 => 100755 config/DESIGNER_ROSTER_API.md mode change 100644 => 100755 config/DESIGNER_ROSTER_需求-给另一台AI.md mode change 100644 => 100755 config/README.md mode change 100644 => 100755 config/__init__.py mode change 100644 => 100755 config/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 config/__pycache__/config.cpython-310.pyc mode change 100644 => 100755 config/config.py mode change 100644 => 100755 config/shop_prompts.json mode change 100644 => 100755 config/transfer_groups.json mode change 100644 => 100755 core/__init__.py mode change 100644 => 100755 core/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 core/__pycache__/pydantic_ai_agent.cpython-310.pyc mode change 100644 => 100755 core/__pycache__/websocket_client.cpython-310.pyc mode change 100644 => 100755 core/__pycache__/workflow.cpython-310.pyc mode change 100644 => 100755 core/pydantic_ai_agent.py create mode 100644 core/task_scheduler.py create mode 100644 core/task_trigger.py mode change 100644 => 100755 core/websocket_client.py mode change 100644 => 100755 core/workflow.py create mode 100644 core/workflow_router.py mode change 100644 => 100755 customer_db/customers.json mode change 100644 => 100755 customer_db/schema.json mode change 100644 => 100755 db/__init__.py mode change 100644 => 100755 db/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 db/__pycache__/chat_log_db.cpython-310.pyc mode change 100644 => 100755 db/__pycache__/customer_db.cpython-310.pyc mode change 100644 => 100755 db/__pycache__/deal_outcome_db.cpython-310.pyc mode change 100644 => 100755 db/__pycache__/designer_roster_db.cpython-310.pyc mode change 100644 => 100755 db/chat_log_db.py mode change 100644 => 100755 db/chat_log_db/chats.db mode change 100644 => 100755 db/customer_db.py mode change 100644 => 100755 db/deal_outcome_db.py mode change 100644 => 100755 db/deal_outcome_db/outcomes.db mode change 100644 => 100755 db/designer_roster_db.py mode change 100644 => 100755 db/designer_roster_db/roster.db create mode 100644 db/image_tasks_db.py create mode 100644 db/task_db/__pycache__/task_model.cpython-311.pyc create mode 100644 db/task_db/task_model.py create mode 100644 db/task_db/tasks.db create mode 100644 features/price_negotiation_with_registration.md create mode 100644 features/risk_customer_detection.md create mode 100644 features/text_surcharge.md create mode 100644 features/tuhui_upload.md mode change 100644 => 100755 image/__init__.py mode change 100644 => 100755 image/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 image/__pycache__/image_analyzer.cpython-310.pyc mode change 100644 => 100755 image/__pycache__/image_precheck.cpython-310.pyc mode change 100644 => 100755 image/__pycache__/image_processor.cpython-310.pyc mode change 100644 => 100755 image/__pycache__/image_qa.cpython-310.pyc mode change 100644 => 100755 image/image_analyzer.py mode change 100644 => 100755 image/image_precheck.py mode change 100644 => 100755 image/image_processor.py mode change 100644 => 100755 image/image_qa.py mode change 100644 => 100755 image/image_tools.py mode change 100644 => 100755 image/perspective_fix.py mode change 100644 => 100755 logs/chat_2026-02-25.log mode change 100644 => 100755 logs/chat_2026-02-26.log mode change 100644 => 100755 logs/chat_2026-02-27.log mode change 100644 => 100755 mail/__init__.py mode change 100644 => 100755 mail/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 mail/__pycache__/email_receiver.cpython-310.pyc mode change 100644 => 100755 mail/__pycache__/email_sender.cpython-310.pyc mode change 100644 => 100755 mail/email_receiver.py mode change 100644 => 100755 mail/email_sender.py mode change 100644 => 100755 requirements.txt mode change 100644 => 100755 results/20260225211854.jpg mode change 100644 => 100755 results/debug_7debc0124b0441da9945feaeceef93b1.jpg mode change 100644 => 100755 results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg mode change 100644 => 100755 results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg mode change 100644 => 100755 results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg mode change 100644 => 100755 results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg mode change 100644 => 100755 results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg mode change 100644 => 100755 results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg mode change 100644 => 100755 results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg mode change 100644 => 100755 results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg mode change 100644 => 100755 results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg mode change 100644 => 100755 results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg mode change 100644 => 100755 results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg mode change 100644 => 100755 results/result_90eaf777934445af81abbd60fe4778c5.jpg mode change 100644 => 100755 run.py create mode 100755 run_tianwang_simple.py create mode 100644 run_with_tianwang.py mode change 100644 => 100755 scripts/__pycache__/chat_ui.cpython-310.pyc mode change 100644 => 100755 scripts/chat_log_viewer.py mode change 100644 => 100755 scripts/chat_ui.py mode change 100644 => 100755 scripts/init_designer_roster.py create mode 100644 scripts/multi_process_launcher.py mode change 100644 => 100755 services/__init__.py mode change 100644 => 100755 services/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 services/__pycache__/service_gemini.cpython-310.pyc mode change 100644 => 100755 services/__pycache__/service_meitu.cpython-310.pyc mode change 100644 => 100755 services/__pycache__/service_vectorizer.cpython-310.pyc mode change 100644 => 100755 services/service_gemini.py mode change 100644 => 100755 services/service_meitu.py mode change 100644 => 100755 services/service_qwen.py create mode 100644 services/service_tuhui_dispatch.py create mode 100644 services/service_tuhui_upload.py mode change 100644 => 100755 services/service_vectorizer.py mode change 100644 => 100755 skills/customer-service/SKILL.md create mode 100644 skills/customer-service/customer-skill.md mode change 100644 => 100755 skills/gemini-api/SKILL.md mode change 100644 => 100755 skills/owner-style/SKILL.md mode change 100644 => 100755 tests/test_config.py mode change 100644 => 100755 tests/test_designer_roster.py mode change 100644 => 100755 tests/test_health_check.py mode change 100644 => 100755 tests/test_image_queue.py mode change 100644 => 100755 tests/test_process.py mode change 100644 => 100755 tests/test_transfer_flow.py mode change 100644 => 100755 tests/test_transfer_groups.py mode change 100644 => 100755 tests/test_wechat_alert.py mode change 100644 => 100755 utils/__init__.py mode change 100644 => 100755 utils/__pycache__/__init__.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/api_cost_tracker.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/content_filter.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/daily_summary.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/designer_roster.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/health_check.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/image_queue.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/intent_analyzer.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/service_base.cpython-310.pyc mode change 100644 => 100755 utils/__pycache__/wechat_chat_log.cpython-310.pyc mode change 100644 => 100755 utils/api_cost_tracker.py mode change 100644 => 100755 utils/content_filter.py mode change 100644 => 100755 utils/daily_summary.py mode change 100644 => 100755 utils/designer_roster.py mode change 100644 => 100755 utils/health_check.py mode change 100644 => 100755 utils/image_queue.py mode change 100644 => 100755 utils/intent_analyzer.py mode change 100644 => 100755 utils/service_base.py mode change 100644 => 100755 utils/wechat_chat_log.py create mode 100644 三种工作流功能说明.md create mode 100644 作图失败转接人工说明.md create mode 100644 图片任务数据库功能说明.md create mode 100644 图绘派单系统集成说明.md create mode 100644 文字加价功能说明.md create mode 100644 部署文档.md create mode 100644 项目功能汇总.md create mode 100644 风险评估功能说明.md 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 0000000000000000000000000000000000000000..f98aae378ed44d0056c59be44e115e46e6eadc49 GIT binary patch literal 10863 zcmd5?X>1!;lI~_VZ;FRRNz_eUwvuUPA~*Ou(WvEyW7V`ri{xHP*h)1gFdchiSL zWoWMhGtMDr)-zTn3Wm>x{H9TU(FYT?QTbi=wq9jVg>^uvZh!?1DCNXoi^Y1lky&hT3X zEu>w^V2Os}wMVVlPI=oEnxd}3zj6%P;kjm6t%K9^j-#bnF3zwOZaKzV2LDdZc!nA* z=S)B=ct@-<%cZ_@X3jE1UoW`=bGrur%E4LBP*>pgTDF23bge4EwxR_4G6_|yN^o$c zD~zFRnS|<9B{)}DRB`{qUtb@4pYV^DkbWzwi#w^u+H?i`Z>DBWrgDyyBCxUf};V#EX)w zKPm=C{D)8OZ>MDg=aqQLKg_plWcv^=c_d)<40}hsL%blH4npaG^J~t|DHWQWu^*c;DY~O9RzZbl5z`}qK@Y73zyb-Ay;k@hN;4z z08ND{Pz@b&f#`)7-%U^aCjG(b`8O}$dGEP9@4u7&=s)hhcmt-c_lhS+eEy(p9pR6A z_+$Kt zFmZSKxpoa1wOlHMMm(Vr$sh3eg2ST$Uc%+U`hr4Gut7@^-;3KV*x@1@k(ta}sDLuA ze6bM-PPr;!u1T6}A_FkJIdjvjxhb|GVQx#B+v04SfP)xw5MAP`>k@b$??1-N>|>7} zI0$b%eW@G_%t*u(C6KfKHDQK`Yd`B+0&S6 zTzT%o8H6cLN9?&C?uccVEP39Ts+W9i@tYrK!i=z8!nc+=V#e&9`pOxh4^vgH3TIiU zCz>kI1>iKBw{wma-lQGhWHTk$6 zl(2W4y@wH$hS_`QwM@#ZhzhfXGhJn6z^HthCn(N2&OXh8eJ9uB^l|D0^8|IAZZ8k5 zg~e@h!=ae+jB&sDTzcZAyT3i7Sb6fCkjE91?%o;XPWF+SrwBy>O~jP@xu!mM)4?4F z_Ii4|dpC9W_HOcYclR`{Q}LY~<&jC`M>x-6e}MOR4@+S31b#GdQq7n(hQOTfhIBRJ z9VQ~uGZXWZFRI#*r^*zi`DkE;hF_=f;?mW!7%Bg}~?9vbQ9N&e*Vpg`uE zzWgC*Yvo?NKFJ>(S-q2-8j7buR1*>cs4F!7VqaI+i4!L}!A5rBv(7R9XmOiiUK9aJ zvB|>6FV9cjm_I$eaOD&XZvHoC0jX9V;;7&c3V!J%HX8{F!`^^OZWF?FFeG`71Ve%d zEWI04I`j!VAUfwcL>e!=_0hsxm(n+*?!cJR)i}{SA&>LoXmCX2mn|G$ki!x8c6X}; zt7cN*Yedhd&;3q4Jn$&I(7I@a+~Z;-FH$5P2TpzzmZX1VNTnq*a%B>W=fT{E%Hh|& zaO2(iKb*ev;lJOx`NG1#eR${Qdv~wCca=aE$0LrAm(Z@2^_gMFEG{Y8kX=ylKv{dT zPFWsA?A;IEOV7NN{`i^nrEBRoFV4U9r!PMEt!&Dw*bA;yW`MFz88d0NTs_cCKww<_ zPaq-0z2!VtHH1ZIg{M~;p%yMe9T1sCg{lPK`S{iJo0I9+|FH1(We~jhHEJO*Q~j8< zQVfaGA)tNw=1pkz4K~Pml8YB!Ubt}ss`s~-2#8X$0lgXY69oh(L5FqBNbtC9^ozm6 zxFjXnLX?^H6%AgFBi^H|3k30JEVKTR!$Dcg3*a`$#$6|T{3!nNWJZ*PcCFBkGHvLT z`UP09hy8qj6J;&A%6c*6^TBJAX}^}w62%&wx5}zhu9{R$?~<0OwJuRWzN8qZ?Q0}UI?CZ((lbsc zJScH4S*V&v>3H>iIHR^ydCL-|HM$6i?n>F5lLK*^8_rvm^{Lv1RKvEJP`qIqoFLm> z3L@O4Alp?2vR!2$yW9n`T_AvQs0l;a8^5tq^=qbn_VO>Lelb_Kakg$_qHc4tZgZ54 z{w!grPE|F*-A|siexswx8s*8YwB8 z*mrJUylUr+bH@L61Khxwu=FP_{c%fwPV7A>^xJPtl%*ULsI}+GErWH!a?TPj@1Fh< z5I7Tto}{5CZs>tmm~vL8DmT2@KSN*LeSLSlas!;ulgk9dc9}rEb~C8gZU*(1*g?H^ zkjxlYC*Q)cq=f;%RqTc}bGmK7PF-sO@@M-_16)3>+6447yJ=S|^I7!{YL}b&yqbo> z=WZ>~F?-%w^?65pd|+kQ^nc+O`9oaOST4?iLFEpPE_5ltA*EK~EOO7lWu?L0RXK$m z!?B!pnY+uNlO&-_ojF!^MZ4+2i6j1zPINR&MF~xWR7r=t4bWWRc=4lMj$yMdR*+b8OKmt+_ zlg_^rRqU=BbV(5Y|MS=YX^>)8Ka1h~mK~g>(D*}kXJx*Ud*rhg@^Q8) z6K5|51QIc;;D8uhjiidxW9SJkJ2;1u3n}cQC`(Yvl}Y$MGY3El{4lSe@LwJw+LbKp%fabrhE{A zW*kHFj_%%$-c9bFO?~UT`?|YTAr)zm2~faO_DaxXBoqh~i_GNeR2)PB*j$O26zabK z8dKSI2%0dUn3^VFK&-xEUomq{;t|=9jYws)07m;Hl$SO;bH0 zP0gIGW!Bab`*Fh7k+gLz4@1`78<^ND!4JM&(-1RGZBMysC;xfMlB%wsJTcXos&&Vz zleOzo;M6~oZ0JqZH^sIl>$`JTcgq`w*9_D33HJj@_XDY-Vq@zYuGd`CU5UoNWMkh_ zxfwwRiPnms1LRA}il74oiU>OHzW_R$W_se~o8U|sHYW|6SK@GYS8(_K4z zsXy-64)l{=(}14&Wa|!UK*M~xm4?D+8ZFQyf_Ox4mo;`9RvIdGh>iO7}te$a0d;>^#HrvxjDgA=2Ku5{*zsc4_VQk!Qd7;91hX!>#yqeQq@jufR46Nzp#{;-x>` zjh%wWi|F zLN?|kEjpnrpw%E4*UrjXDJXdZ!hWoJ44JiP^F`qyxL*b^ikemc7y_I7+bv4wjP(jb zqJZEcGk(BG0zD4Ij7+YO1URD11o%vHZ3A{+gh3|Z5v*-Rk{h0I0E>?Uk+q;_L6RBB zkIE)8Xb;X)iH3090bFGc1+`3Q`IN4OpThGw_!qA!fK|}~fK^dZdGouC`fM+jPy$p@eHo(zPYw>`OZPqQ={12SzaeM5k=!lU1{}rns%? zmaApXwQ1J1Y38Yft3T=LKd(Kng{)3h4c?@k?m1`gth0CK9}>>(N$2+HF35#BoacAF zWV&FAY9VTyq+fDPK9R7mfs(cCygy-Wh%%}2>dB3%tLgzmQ*~=n zRn1FAEg~KgtpO1a$d{A>5f2Cy5%HXVSBQrW+35l2Kqd8ArD@kX=Cek~QLSY@Z=|8{ z`C2W|F*5V;0=Y_&ZDnQ6TNEvL;A;inlY$o>!*@57h`;U# z1#8Gsma>tM8fDn(K`_u{{x(?X>es?AGFEU;vbl@-lF2JjV%gX|sv zK+%RH1Y4?H*ilpsIn>#*qq)pmn8KVMc~y`Xyso|0=!jzKiAokXy`sPCrfV%?M(=Xu$ z){}v9wj|10lVz=ueYaTC*}bRt#_c<24g!Jm=74fdu>DE4KhE~U`m|SzGTJza`r}v%Oe`bGVKN!@=BV-xM z1`m9>;~(*O#@c?`+U6BuqZ2;t5r6J(Z41D+E&<%7Z5R8Xm<_^2_KufR zKyu1xXM{tj7$1<`?QU7`@o+((#{;`G_(_p~D@IA^;d+zxxbXwJRKC6~bmCA`9t`=Cg`MJ%A15QdS*2=98IvYiC8!wGmi%KHuCW1TNJa@aql zfUK4DmKWtfDY*YHYX#UZ<;h-=k{ra4jldwW3glJF=0jf5@9QT5Wp>a0-H!_Yf^6u~ z5qt8TVJl1lK6~0GoQ67h^_2JkSjr`ars*Y?p;@pMKnxUpUz}=R?MzYD2>DA5fAF&Rk87Kl+ccjj^@JeqWNlZ%JdM!G;4t@%QIy zTuvJH#R^F$#ag0cvrJW-sYwqaU6Ne~H_EwGLb25#iL9*u1Fc{`!2kdN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e5dea299194b1fb9a8c72a537f28a63c3497ec8d GIT binary patch literal 17572 zcmeG^X>c3Yc?)0xEI@!<-j|M*DT{(cQ>SH}lu414DN~d}QkF%>Mj$RIK?VVN0a_9Z zIjk$Ev?528B3n#+K^`CU#BtP2YO9HpIGT3qOw-Onvr}=lql`KUWx0PskKCC|e)ap_ z9@xbK6qCl8{^-N<+qdt1@7s5N*L%x%{C+P5;kl zP(rxRUp8g(to^Ts~yyOHR=PRlL2A3a}$} zknEj#^W9H>dhWMB`N_=bOSfLUeCvhxZ%tjAdF`9t&yYkc&hhQqQ4As>z= zLji}(>^Yi@#ABhTOt(h{lQP{MNhAX{ncf>p9>#k|Y-D&4iD*G<`5hP#==U%My-i_H zLoeG0Xx=g4qx zN5e5b5<4WjkA)&h5)npXF^qTyK1hEHoSr z24!zBI2`9kq8RrDgHMfwqH2yi806!FknG;u(bL}5vr~5OZ|&+Qk=}hhJs9!q*t2_Y zcSnCmyX@+0?dpcRXGd$#j*f1KzDwP~F8IU7RYE+TTosB02S*ag_;6SVCc?tgk-;!Y zORVA#u1Z2fgW4S`6pl6@Js}i9aRN#pl2{91O#e;ORU%zAU0gl3(*W4|v94)<yn2r2={uAV);yIZ?u`k}6!56P~5y*oR4`h^9UPil*G z5;zUiKqS!sz}Wa&k*-x5T2wmLPGV!7W1UKQENW`42NE#c`F8+0t*z!AVH%bQeF_#j z?>t4pRCED!^X@PMOVl$&^B!0}URaVeJQszT>@tJABFOXcH6+&;_Va$gDh@M43@ke5 zK(Sh?1Ta~^Wcg~ol+Y>7p;HFfWkBDYZs*IPR5_Hww^u+-X}%ILD*%&$T311CC0W?D zauJTw?ocdrC@hRDhL)=9<^4;aPJCnLJ5$_!T;Er^Bgrr~^Zm)0XQz5(H-9jA6xQva z&P$*a4Sx>!yYmr%i&V-swjimkVMs|DaLt_Uh@QjSMq%!rw54n*Du}d5NKt8|Z3%Bf z%J=kC89<@rmDkbVgx?Ge7?J?>_o2OkM`cAhQ?(9s-=ebu=Wvv=#zNawHZ> z!lIR3(fA?ga+!|AhT_0E=|ng>M355LBOE8-gho1~L~yjiFi_1}>Jq?7dFOU{&xm)u~A&la675-WN? zZvV$_xWScS`Xr`L)Go3p1`R`)9pZn3R2X9F2)tJbM|8ZjKAc(}-a!!B-m{wp5Db|+ zi*rODWFuYk0O8a~YXUi26k_JdaMGT#7nlMLGesL!5R;xargPqyE(@kDZRmq+lg#ab zTH4Y$t1KZGFNus(4Qa!I(9&Qe+_`m7#O4T=)Tg#Sqx)fIx|Io>keTpt5Zl4{lc8V3 zGV*eUu%ox5wZDVwZ*A-D;JP}wo<03s$D>_+{e2u+OWbl3I+n!rV1(!TJ09)l_V#w| zZtXq5J=}4i$%2_U8Xk-cMZ$bm7OLgug^Qw@yv2&0JaJUXA{Fl0*WGQ!(dF+Pbab48 z^-2m6T<$^rjn)@wC{Kn@9E%G|$J`Ywp|p(S?z@lM)widI3kgE#gry5ZgQ$Pa^>!75 zFs%_GTA;&Jtl@AX0Rp1{(ot3BlWeZNqqB8icR$yt2>Zr7O#)3ZK9UR`j*kcluB)fN zV`oRNUT*DrYl#F=?{es?s$HHJ2;rn~A~+Zyi6t%MS6itMhmHqH5exDFgce7W)*7~OW4a@tbi^paW%FoVG8mb(>Ky4z4%@BhF^J@7D{b=|%+ zrSmgIRQCjmh=3KE08e%fKgmY~*`0VQ3fj#&**zGK#lm2mxCUVw#vvzsJUlp(3`0VE zcsP=jorBRhDB;aeT37`j;1E_LB^pZ#8zGuN^#XVsk=+6+4>@96mHwy;Bz_5CjJnDA z&iEvz{yMYd8na|->6HqRS(0H|B&J1VT4qYB&U}3``ARBN(j=8MjnmVml^5&J*PmN> zVd1!I)Q z!x3nq5>h!D)=4_$$gd6Yh3lU~qhLFo^K561hW5Xvm!oYJq{o;ie?h$Ywqw5UoiDxl z_RDWD{+n|WS->&J{U&-c}sdwQtcT#nd~1`emyC)X+^ zc<7L{1aqyxE=Fgo8q&H;SWCe>Ut4e#b2wsNH%DC`)j%cDzo3Stl(77y)AU|e%QN2j z`jj&t#RC+-;H2{zC%>>yjUBTcqmDZcP{(Y6MZ^tz>qGDwrxjm{>{fMNnNf|ML@ zpisf^$ZmBC$u6arWJa@LWw+WtGNbi{umq;F>^F7DXXq=IT}rPAxI#fg%vy{BYEH68 z@1M_5Qgf!W3J(HR zVnfId&@g3hBoT>0m0-EsqxD1*@gel3Cxy+J zvju@}r^ud;Q@0h@oVU_F>01x?WvtEufEuFz9m!MAeA?aw@+8rUVQBQW9J^f@c4Kq*e~4D*N=bq z_>|}J+RWk&(&7!*7jM3{c=MIb9}AhqyQIatMBjoNCADJRito2)O4djvYi22%|3TZ0 zhQ>EHi_I;WhV4?rc9G?7`pVAsobH*WJPX*_3JRJDCT`#fgwd|@4%SkM!J?0Y2!OqRjc~@<^EyFfSY~yuy zBCi^X40QrvpXna&o`J!K9I>Y3GW{X*L*`0tX7vtf^$tjYE92{w ze4V1N6Q<(C`rpr#)XY+LPi+?5fa2Y!yT#h|mrDV_mGRv#`R*5e_fMDCOx&;ieh*>R z2}R+aK&J6*_p0u7)UVcgci-#y)wYGZImf?pPKXPgkZH2;&}&UByn|q`eJK{6qk!cG zKJy`)MPqTKu{M@q)mON{m)su5AZe0c<0K&SB7hRGq`1-{kjk zgY{QP=dcr<11R$(X3#lkMdx_^t=m6%Lt)3G_x5#kckJlrR&brYdv>cj#QujmdOI|o zf!oGy?@`#ap`gyD1D-6CCJY!PO*lDtI35cVhAY!N>}?A{=qQEvehp&ps+<;Qgn`pK zzy%>3gfuIAMgM}8y=tiwSmpx&$X<&*HFL37l_!ezZ6ELdcvr@^NAm3veS4TnES6vI+0*{;g&JTobK65p(b?2grws3B0o?vy+K?i6p?XiPErF_V6cI#@RP&X9Sv z zKzITHE(8HR90G3F2#+B^qd^g=^pSWZmd}V$MivBL5IZ}1e zf8PJ<;a8&HiMMH@1UHcE>&X4p*A(0hQUGv|2rPZzfv{~A*U#xvp zEWztfrnmq!Vm~POYK6eOK#N2~ql8pwayY{_No+ahe<+VCu!k9-Qsc7Vz9 zdd2+|MgqjsWHxz(Ly%#yYf2(9aG4FmNjT+f#b~{e#pY!G3sU$G0BGe@c&g{*ubSrv zSp`Ft*G<$RpIC@|Vj=R0g~%t$(K{s8?-vI`;(@`8kC%MB=;P;M3$2*0uAAH@RR=^i zFkN0Tu}1SCRHFw$0dw$+>GG#U;9-R7gN`n+u|j;mD*+cr6_ZgN6g zF4Fv+a4I4{-}uIt#W!q14eZ3`=LUJb39-t8_*Tp3&+K{N9jCzi(4KPwFc0Sl!2ZVF zIR~Aea~uM14(xF_k$V8|Aa?`raE+kCqZ~n)`Pp-~-+6oH?72_BdrHv~LodTfSync3 zi+mo5yod@L(ACUF?WmJg8AP(JJ?+L#3RE!jQr*+r4(F`e4&8D7XXyD_j;=4l(r>xvs8`067_trTV|;uu41-|s=jC9 ziO;KxO7H!G0syHRsG~}Wi7-ofxr)yzZ)xQh2;dPw4OLNhar^o0newGl`BK;%GCK8d zlFpv0fF912H%sv3Yt}?>6^dR3=%RPql_g^BHn=jr7RlEl`dX&TE62OCKTY^nq433l zdbZ8m=BKXuy*nt!)#^oUTO3#Kw_$vX6QE^&5|3jq%!WV03K*{M~cp;d#e(ZK1n|VdOc6>1_|mU zz7=KV-2ULXTNgi2z4liuEOcH%vyi~FP(xX%eY3VaD`gch<0qT|U}S5;UqOPEpJ`j$Npu$F<7f3SAB7ZB#LuRP z@^9_;EV~j;0u~&kT#Y4k_lod!d^`vsfvlUbALWA;!hKAYuyP+$wh!u)_y-_@+^6&| z+{d8ne-5gBDJuD;sN|P|lK*F9CRV%uuFRx$b?Z9n7wf!j%N)Phwy151<7&MP;#ZeA z0pd1B2=?y>k%?#ldyX)bQxL%qYSLPc>7KF~zn;o&Nu=!W+00*p!qFiV25QK^{L!G# zw?KIZoTXK^&F~=Y2jj{D4A_WwjVw=9l709?1p!Ykz{C*Xb&A)o9%K5c1NC$?*$Wl0 zg{H&NVBnB%E0wLTYNLEIWGunC(F$eBIqB=n;%m&}wEO#YQefLBkBH3T4AU<${UXz^ zpG#glRV`Mmg)76XlbCg)c3ICS>%Bh)FZnW9dW1UZNYYSwHMCQYKS;JzjdMWDgioYW zbaqQA&z7w{|CW)No%8mr3nc}tI7<GPe|+&BKySjvZi<9Z^Sdp9*~wjaK$Dq+dSc%7`hOJ12H1oaMP%0!5N3~?zuFp ze8(d0rdhe$ywp{%w{4r_YQ>_qjgG5pZ5ZF^1Q@Wvr{X=hdw?IE<$mYuNF)Ve4QAl? z6Uu-MK}@C(#^X_85EF6j3K0ajJxN%Q?1;cemiWzpfE%a67=ph+@C^jdBUpeS`>`-? z90>?e8BCmop%~MD({w9Vr5T+f^-|bcEb zr2wR%PvCYLnx!QfVqO-g(AYwtPyi@ng(D>UmF<~f1hKl>(pF!BOmka?3cp=1^p zH!5C1t)6EAbt|rMWXogJf4eBAQgko4QPc1p|10ou&5fcOk**=_3{;cv+?6j@9s_jQ zjh_g^za~(2&}d9@!I4KyAqxQc4w>xuiDvR}G;&b+_LvUCe^U^UJIfxZQ8)R}SXpaC zI+HcSVPUcYusL!`Fc^x(;>fh(u|zN^V9j&d_XlRH@^24XgqI)}nnfj^066Qg*=)14 z-3G&{0~cj087s!$G*vW4e$y0r#+2n^zH6Fl6boO|RE=o9rm0HNeBJcajaN&ay2*_h z&r->=bj&qNyKS(uWd##pjza1e*{m|!26NF0Cf0+lX{!QVQ_J@~N}~DU=M<)YLEx+c nk2BQrrZNBc@yQ~oWT{B4geya>oTZ(%+R2r(6x<2|GCBSSL9SWY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..977db7f13531ca08b96e16367658b4425a79958b GIT binary patch literal 24576 zcmeI)Z)?*)90%}=U8`M@>I2Ef7@SC{1vk2GZZZ(8F3Pl4?K0Sq5n_9*acxsCS6mxF`O36@srpu=va;3i`}zlBRB1s}H4qk7iAJzxyTk*(Gqty0v=+OE9`^ z^SUYMJUK-ag>6dxOu;I`batHBwUUqI6 ztnXgeSSXiQ+0W!1XP(|uj;pGwyikPspvK(B?vG=B20eeZpm|-P=f~-x(hZf@LUnCJ zr*})MxAUbxDu%V{pzP zd4d?fKjAEbTh%J#MsUW#Av>)7#O5B%AEO+vXOzy&(3@*=kSW6$#C9BTtmM|XLE$+zs zLDz0*rJ!AIA>6_rk?zn5@;6zdVl}EqoPaaI+eX=LHAExPOlh zAnOG@@I1m9{GlO-x12wByA%#7+x4cr!-j=0&TVcC|HW|Tv})ctf&p`2sAMw7V(Mj8 zA?}00d0dk-d&6uA+dVeiyW5y^-y-s1d!qZmJt;ROd6UQ&3Irek0SG_<0uX=z1Rwwb z2tWV=e-zl8S0<*@sWYi`X7Y53q$ZWA6H0!v+fLWkbR(P11q)4rIimCS`Ir5dpI^V~ zJbQom_&{KAz*)1__L|+_a^!kRZ^6L(tn=8DXdWdd700Izz00bZa0SG_<0uX=z Q1pZ5b=}104_`d-B0_MxVBme*a literal 0 HcmV?d00001 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 +