feat: 完整功能部署 v1.0

新增功能:
- 天网协作系统 (HTTP API 端口 6060)
- 三种工作流 (查找图片/处理图片/转人工派单)
- 图片任务数据库 (支持客户后续增加需求)
- 图绘派单系统集成 (API: 8005)
- 文字检测与加价 (60-80 元高价值订单)
- 风险评估与接单判断
- 作图失败自动转人工

新增文档:
- 项目功能汇总.md
- 三种工作流功能说明.md
- 文字加价功能说明.md
- 风险评估功能说明.md
- 图片任务数据库功能说明.md
- 图绘派单系统集成说明.md
- 作图失败转接人工说明.md
- DEPLOYMENT.md
- TIANWANG_INTEGRATION.md

核心修改:
- core/pydantic_ai_agent.py
- core/workflow.py
- core/websocket_client.py
- image/image_analyzer.py
- services/service_tuhui_dispatch.py
- db/image_tasks_db.py

版本:v1.0
日期:2026-02-28
This commit is contained in:
2026-02-28 11:20:40 +08:00
parent 5aedf1665d
commit a6c42d505a
171 changed files with 7979 additions and 328 deletions

0
core/__init__.py Normal file → Executable file
View File

0
core/__pycache__/__init__.cpython-310.pyc Normal file → Executable file
View File

0
core/__pycache__/pydantic_ai_agent.cpython-310.pyc Normal file → Executable file
View File

0
core/__pycache__/websocket_client.cpython-310.pyc Normal file → Executable file
View File

0
core/__pycache__/workflow.cpython-310.pyc Normal file → Executable file
View File

117
core/pydantic_ai_agent.py Normal file → Executable file
View File

@@ -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

289
core/task_scheduler.py Normal file
View File

@@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
"""
天网任务调度系统
负责任务的执行、重试、超时处理
"""
import asyncio
import logging
from typing import Optional, Dict
from datetime import datetime
from .websocket_client import QingjianAPIClient
from db.task_db.task_model import get_task_manager, TaskStatus, TaskPriority
logger = logging.getLogger(__name__)
class TaskScheduler:
"""任务调度器"""
def __init__(self, client: QingjianAPIClient = None):
self.task_manager = get_task_manager()
self.client = client
self.running = False
self.task_timeout_checker = None
logger.info("任务调度器初始化完成")
async def start(self):
"""启动任务调度器"""
self.running = True
logger.info("任务调度器已启动")
# 启动超时检查任务
self.task_timeout_checker = asyncio.create_task(self._check_timeouts())
# 启动任务执行队列
await self._process_task_queue()
async def stop(self):
"""停止任务调度器"""
self.running = False
if self.task_timeout_checker:
self.task_timeout_checker.cancel()
logger.info("任务调度器已停止")
async def _check_timeouts(self):
"""检查超时任务"""
while self.running:
try:
timeout_tasks = self.task_manager.get_timeout_tasks()
for task in timeout_tasks:
logger.warning(f"任务超时:{task['task_id']}")
self.task_manager.cancel_task(
task['task_id'],
reason=f"任务超时({task['timeout_hours']}小时)"
)
# 通知天网任务超时
await self._notify_tianwang(task['task_id'], 'timeout')
# 每 5 分钟检查一次
await asyncio.sleep(300)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"超时检查失败:{e}")
await asyncio.sleep(60)
async def _process_task_queue(self):
"""处理任务队列"""
# 持续监听,当有任务时执行
while self.running:
try:
# 这里实际应该从队列获取任务
# 简化处理:每秒检查一次待触发任务
await asyncio.sleep(1)
except Exception as e:
logger.error(f"任务队列处理失败:{e}")
async def execute_task(self, task: dict) -> bool:
"""
执行任务
Args:
task: 任务数据
Returns:
是否执行成功
"""
task_id = task['task_id']
try:
# 更新状态为执行中
self.task_manager.update_task_status(task_id, TaskStatus.RUNNING)
logger.info(f"开始执行任务:{task_id}")
# 根据任务类型执行
task_type = task.get('type')
if task_type == 'send_file_after_reply':
success = await self._execute_send_file(task)
elif task_type == 'send_message':
success = await self._execute_send_message(task)
elif task_type == 'send_link':
success = await self._execute_send_link(task)
else:
logger.error(f"未知任务类型:{task_type}")
success = False
if success:
self.task_manager.update_task_status(
task_id,
TaskStatus.COMPLETED,
result={'executed_at': datetime.now().isoformat()}
)
logger.info(f"任务执行成功:{task_id}")
# 通知天网
await self._notify_tianwang(task_id, 'completed')
else:
# 失败重试逻辑
await self._handle_task_failure(task)
return success
except Exception as e:
logger.error(f"任务执行失败:{task_id} - {e}")
await self._handle_task_failure(task, error=str(e))
return False
async def _execute_send_file(self, task: dict) -> bool:
"""执行发送文件任务"""
try:
customer_id = task.get('customer', {}).get('id')
file_url = task.get('action', {}).get('file_url')
message = task.get('action', {}).get('message', '这是您要的文件')
if not self.client:
logger.error("WebSocket 客户端未初始化")
return False
# 调用发送文件方法
# TODO: 实现 send_file 方法
logger.info(f"发送文件给 {customer_id}: {file_url}")
return True
except Exception as e:
logger.error(f"发送文件失败:{e}")
return False
async def _execute_send_message(self, task: dict) -> bool:
"""执行发送消息任务"""
try:
customer_id = task.get('customer', {}).get('id')
message = task.get('action', {}).get('message')
if not self.client:
return False
# 发送消息
await self.client.send_message(customer_id, message)
logger.info(f"发送消息给 {customer_id}: {message}")
return True
except Exception as e:
logger.error(f"发送消息失败:{e}")
return False
async def _execute_send_link(self, task: dict) -> bool:
"""执行发送链接任务"""
try:
customer_id = task.get('customer', {}).get('id')
link_url = task.get('action', {}).get('link_url')
message = task.get('action', {}).get('message', '')
full_message = f"{message}\n\n{link_url}" if message else link_url
return await self._execute_send_message({
'customer': {'id': customer_id},
'action': {'message': full_message}
})
except Exception as e:
logger.error(f"发送链接失败:{e}")
return False
async def _handle_task_failure(self, task: dict, error: str = None):
"""处理任务失败"""
task_id = task['task_id']
max_retry = task.get('max_retry', 3)
retry_count = self.task_manager.increment_retry(task_id)
if retry_count <= max_retry:
# 重试
logger.warning(f"任务失败,{retry_count}/{max_retry} 次重试:{task_id}")
await asyncio.sleep(60 * retry_count) # 递增重试间隔
await self.execute_task(task)
else:
# 超过最大重试次数,标记失败
self.task_manager.update_task_status(
task_id,
TaskStatus.FAILED,
error_message=error or '超过最大重试次数'
)
logger.error(f"任务最终失败:{task_id}")
# 通知天网
await self._notify_tianwang(task_id, 'failed', error)
async def _notify_tianwang(self, task_id: str, status: str, error: str = None):
"""通知天网任务状态"""
try:
# TODO: 实现天网回调 API
# 可以通过 HTTP POST 通知天网
logger.info(f"通知天网 - 任务 {task_id} 状态:{status}")
# 示例代码(实际使用时取消注释并配置天网 URL
'''
import httpx
async with httpx.AsyncClient() as client:
await client.post(
'http://127.0.0.1:6060/api/task/callback',
json={
'task_id': task_id,
'status': status,
'error': error,
'timestamp': datetime.now().isoformat()
}
)
'''
except Exception as e:
logger.error(f"通知天网失败:{e}")
def check_trigger_match(self, message: str, trigger: dict) -> bool:
"""
检查消息是否匹配触发条件
Args:
message: 客户消息内容
trigger: 触发条件配置
Returns:
是否匹配
"""
trigger_type = trigger.get('type')
if trigger_type == 'customer_reply':
keyword = trigger.get('keyword')
return keyword and keyword in message
elif trigger_type == 'customer_keyword':
keywords = trigger.get('keywords', [])
if isinstance(keywords, str):
import json
keywords = json.loads(keywords)
return any(kw in message for kw in keywords)
elif trigger_type == 'customer_payment':
# 付款检测逻辑
payment_keywords = ['已付款', '拍下了', '已下单', '付款了']
return any(kw in message for kw in payment_keywords)
elif trigger_type == 'time_reach':
# 时间判断
target_time = trigger.get('time')
if target_time:
try:
target = datetime.fromisoformat(target_time)
return datetime.now() >= target
except:
pass
return False
return False
# 单例
_scheduler: Optional[TaskScheduler] = None
def get_task_scheduler(client: QingjianAPIClient = None) -> TaskScheduler:
"""获取任务调度器单例"""
global _scheduler
if _scheduler is None:
_scheduler = TaskScheduler(client)
elif client and _scheduler.client is None:
_scheduler.client = client
return _scheduler

211
core/task_trigger.py Normal file
View File

@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
"""
任务触发条件匹配引擎
支持多种触发条件类型
"""
import re
import logging
from typing import Dict, List, Optional
from datetime import datetime
logger = logging.getLogger(__name__)
class TaskTriggerEngine:
"""任务触发引擎"""
def __init__(self):
# 触发器类型注册表
self.trigger_handlers = {
'customer_reply': self._check_customer_reply,
'customer_keyword': self._check_customer_keyword,
'customer_payment': self._check_customer_payment,
'customer_order': self._check_customer_order,
'time_reach': self._check_time_reach,
'custom_event': self._check_custom_event,
'specified_customer_reply': self._check_specified_customer_reply, # 新增:指定客户回复
}
def check_trigger(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
检查消息是否匹配触发条件
Args:
message: 客户消息内容
trigger: 触发条件配置
context: 上下文信息(客户 ID、店铺 ID 等)
Returns:
是否匹配
"""
trigger_type = trigger.get('type')
if trigger_type not in self.trigger_handlers:
logger.warning(f"未知触发类型:{trigger_type}")
return False
handler = self.trigger_handlers[trigger_type]
try:
return handler(message, trigger, context)
except Exception as e:
logger.error(f"触发条件检查失败:{e}")
return False
def _check_customer_reply(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
检查客户回复指定内容
触发条件:
{
"type": "customer_reply",
"keyword": "好的" // 包含"好的"即匹配
}
"""
keyword = trigger.get('keyword')
if not keyword:
return False
return keyword in message
def _check_customer_keyword(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
检查客户说某关键词
触发条件:
{
"type": "customer_keyword",
"keywords": ["好的", "可以", ""] // 任一关键词匹配
}
"""
keywords = trigger.get('keywords', [])
if isinstance(keywords, str):
import json
keywords = json.loads(keywords)
if not keywords:
return False
return any(kw in message for kw in keywords)
def _check_customer_payment(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
检查客户付款
触发条件:
{
"type": "customer_payment",
"keywords": ["已付款", "拍下了", "已下单", "付款了"]
}
"""
payment_keywords = trigger.get('keywords', [
'已付款', '拍下了', '已下单', '付款了', '已支付', '拍下付款'
])
return any(kw in message for kw in payment_keywords)
def _check_customer_order(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
检查客户下单
触发条件:
{
"type": "customer_order",
"keywords": ["下单了", "拍了", "购买了"]
}
"""
order_keywords = trigger.get('keywords', [
'下单了', '拍了', '购买了', '买了'
])
return any(kw in message for kw in order_keywords)
def _check_time_reach(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
检查到达指定时间
触发条件:
{
"type": "time_reach",
"time": "2026-02-27 09:00:00"
}
"""
target_time = trigger.get('time')
if not target_time:
return False
try:
target = datetime.fromisoformat(target_time)
return datetime.now() >= target
except Exception as e:
logger.error(f"时间解析失败:{e}")
return False
def _check_custom_event(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
自定义事件触发
触发条件:
{
"type": "custom_event",
"event_name": "customer_complaint"
}
"""
# 预留自定义事件接口
logger.info(f"自定义事件触发:{trigger.get('event_name')}")
return True
def _check_specified_customer_reply(self, message: str, trigger: dict, context: dict = None) -> bool:
"""
【新增】指定客户回复指定内容
触发条件:
{
"type": "specified_customer_reply",
"customer_id": "customer_123", // 指定客户 ID
"customer_name": "小明", // 可选:客户名称
"keyword": "好的", // 回复内容
"exact_match": false // 可选:是否精确匹配
}
匹配逻辑:
1. 检查当前消息的客户 ID 是否匹配
2. 检查消息内容是否包含关键词
"""
# 1. 检查客户 ID 是否匹配
specified_customer_id = trigger.get('customer_id')
specified_customer_name = trigger.get('customer_name')
if context:
current_customer_id = context.get('customer_id')
current_customer_name = context.get('customer_name')
# 客户 ID 匹配检查
if specified_customer_id and current_customer_id:
if specified_customer_id != current_customer_id:
return False
# 客户名称匹配检查(可选)
if specified_customer_name and current_customer_name:
if specified_customer_name != current_customer_name:
return False
else:
logger.warning("缺少上下文信息,无法检查指定客户")
return False
# 2. 检查回复内容
keyword = trigger.get('keyword')
if not keyword:
return False
exact_match = trigger.get('exact_match', False)
if exact_match:
# 精确匹配:消息内容完全等于关键词
return message.strip() == keyword.strip()
else:
# 模糊匹配:消息包含关键词
return keyword in message
# 单例
_trigger_engine: Optional[TaskTriggerEngine] = None
def get_trigger_engine() -> TaskTriggerEngine:
"""获取触发引擎单例"""
global _trigger_engine
if _trigger_engine is None:
_trigger_engine = TaskTriggerEngine()
return _trigger_engine

116
core/websocket_client.py Normal file → Executable file
View File

@@ -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}")

371
core/workflow.py Normal file → Executable file
View File

@@ -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

93
core/workflow_router.py Normal file
View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
工作流程路由器
根据客户说的话判断执行哪种工作流
"""
import logging
from typing import Tuple, Optional
import re
logger = logging.getLogger(__name__)
class WorkflowRouter:
"""工作流程路由器"""
def __init__(self):
# 查找图片关键词
self.find_keywords = [
"找一下", "找图", "找原图", "帮我找", "能找到吗",
"有吗", "有没有", "找到", "搜一下"
]
# 处理图片关键词
self.process_keywords = [
"做一下", "处理一下", "安排", "开始做",
"弄一下", "修一下", "P 一下", "P 图"
]
# 无法处理关键词
self.unable_keywords = [
"做不了", "处理不了", "弄不了", "无法处理",
"做不到", "搞不定"
]
def detect_workflow(self, message: str) -> Tuple[str, str]:
"""
检测工作流程类型
Args:
message: 客户消息
Returns:
(workflow_type, confidence)
workflow_type: "find_image" / "process_image" / "transfer_human"
confidence: 置信度 0-1
"""
message_lower = message.lower()
# 1. 检测查找图片
for kw in self.find_keywords:
if kw in message_lower:
logger.info(f"检测到查找图片意图:{kw}")
return ("find_image", 0.9)
# 2. 检测处理图片
for kw in self.process_keywords:
if kw in message_lower:
logger.info(f"检测到处理图片意图:{kw}")
return ("process_image", 0.9)
# 3. 检测无法处理
for kw in self.unable_keywords:
if kw in message_lower:
logger.info(f"检测到无法处理:{kw}")
return ("transfer_human", 0.9)
# 默认返回未知
return ("unknown", 0.5)
def should_find_image(self, message: str) -> bool:
"""是否应该查找图片"""
workflow_type, _ = self.detect_workflow(message)
return workflow_type == "find_image"
def should_process_image(self, message: str) -> bool:
"""是否应该处理图片"""
workflow_type, _ = self.detect_workflow(message)
return workflow_type == "process_image"
def should_transfer_human(self, message: str) -> bool:
"""是否应该转人工"""
workflow_type, _ = self.detect_workflow(message)
return workflow_type == "transfer_human"
# 单例
_router: Optional[WorkflowRouter] = None
def get_workflow_router() -> WorkflowRouter:
"""获取工作流程路由器单例"""
global _router
if _router is None:
_router = WorkflowRouter()
return _router