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:
0
core/__init__.py
Normal file → Executable file
0
core/__init__.py
Normal file → Executable file
0
core/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/__init__.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/pydantic_ai_agent.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/pydantic_ai_agent.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/websocket_client.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/websocket_client.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/workflow.cpython-310.pyc
Normal file → Executable file
0
core/__pycache__/workflow.cpython-310.pyc
Normal file → Executable file
117
core/pydantic_ai_agent.py
Normal file → Executable file
117
core/pydantic_ai_agent.py
Normal file → Executable file
@@ -18,6 +18,7 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
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
289
core/task_scheduler.py
Normal file
@@ -0,0 +1,289 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
天网任务调度系统
|
||||
负责任务的执行、重试、超时处理
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Dict
|
||||
from datetime import datetime
|
||||
from .websocket_client import QingjianAPIClient
|
||||
from db.task_db.task_model import get_task_manager, TaskStatus, TaskPriority
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskScheduler:
|
||||
"""任务调度器"""
|
||||
|
||||
def __init__(self, client: QingjianAPIClient = None):
|
||||
self.task_manager = get_task_manager()
|
||||
self.client = client
|
||||
self.running = False
|
||||
self.task_timeout_checker = None
|
||||
logger.info("任务调度器初始化完成")
|
||||
|
||||
async def start(self):
|
||||
"""启动任务调度器"""
|
||||
self.running = True
|
||||
logger.info("任务调度器已启动")
|
||||
|
||||
# 启动超时检查任务
|
||||
self.task_timeout_checker = asyncio.create_task(self._check_timeouts())
|
||||
|
||||
# 启动任务执行队列
|
||||
await self._process_task_queue()
|
||||
|
||||
async def stop(self):
|
||||
"""停止任务调度器"""
|
||||
self.running = False
|
||||
if self.task_timeout_checker:
|
||||
self.task_timeout_checker.cancel()
|
||||
logger.info("任务调度器已停止")
|
||||
|
||||
async def _check_timeouts(self):
|
||||
"""检查超时任务"""
|
||||
while self.running:
|
||||
try:
|
||||
timeout_tasks = self.task_manager.get_timeout_tasks()
|
||||
for task in timeout_tasks:
|
||||
logger.warning(f"任务超时:{task['task_id']}")
|
||||
self.task_manager.cancel_task(
|
||||
task['task_id'],
|
||||
reason=f"任务超时({task['timeout_hours']}小时)"
|
||||
)
|
||||
# 通知天网任务超时
|
||||
await self._notify_tianwang(task['task_id'], 'timeout')
|
||||
|
||||
# 每 5 分钟检查一次
|
||||
await asyncio.sleep(300)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"超时检查失败:{e}")
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def _process_task_queue(self):
|
||||
"""处理任务队列"""
|
||||
# 持续监听,当有任务时执行
|
||||
while self.running:
|
||||
try:
|
||||
# 这里实际应该从队列获取任务
|
||||
# 简化处理:每秒检查一次待触发任务
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"任务队列处理失败:{e}")
|
||||
|
||||
async def execute_task(self, task: dict) -> bool:
|
||||
"""
|
||||
执行任务
|
||||
|
||||
Args:
|
||||
task: 任务数据
|
||||
|
||||
Returns:
|
||||
是否执行成功
|
||||
"""
|
||||
task_id = task['task_id']
|
||||
|
||||
try:
|
||||
# 更新状态为执行中
|
||||
self.task_manager.update_task_status(task_id, TaskStatus.RUNNING)
|
||||
logger.info(f"开始执行任务:{task_id}")
|
||||
|
||||
# 根据任务类型执行
|
||||
task_type = task.get('type')
|
||||
|
||||
if task_type == 'send_file_after_reply':
|
||||
success = await self._execute_send_file(task)
|
||||
elif task_type == 'send_message':
|
||||
success = await self._execute_send_message(task)
|
||||
elif task_type == 'send_link':
|
||||
success = await self._execute_send_link(task)
|
||||
else:
|
||||
logger.error(f"未知任务类型:{task_type}")
|
||||
success = False
|
||||
|
||||
if success:
|
||||
self.task_manager.update_task_status(
|
||||
task_id,
|
||||
TaskStatus.COMPLETED,
|
||||
result={'executed_at': datetime.now().isoformat()}
|
||||
)
|
||||
logger.info(f"任务执行成功:{task_id}")
|
||||
|
||||
# 通知天网
|
||||
await self._notify_tianwang(task_id, 'completed')
|
||||
else:
|
||||
# 失败重试逻辑
|
||||
await self._handle_task_failure(task)
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"任务执行失败:{task_id} - {e}")
|
||||
await self._handle_task_failure(task, error=str(e))
|
||||
return False
|
||||
|
||||
async def _execute_send_file(self, task: dict) -> bool:
|
||||
"""执行发送文件任务"""
|
||||
try:
|
||||
customer_id = task.get('customer', {}).get('id')
|
||||
file_url = task.get('action', {}).get('file_url')
|
||||
message = task.get('action', {}).get('message', '这是您要的文件')
|
||||
|
||||
if not self.client:
|
||||
logger.error("WebSocket 客户端未初始化")
|
||||
return False
|
||||
|
||||
# 调用发送文件方法
|
||||
# TODO: 实现 send_file 方法
|
||||
logger.info(f"发送文件给 {customer_id}: {file_url}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送文件失败:{e}")
|
||||
return False
|
||||
|
||||
async def _execute_send_message(self, task: dict) -> bool:
|
||||
"""执行发送消息任务"""
|
||||
try:
|
||||
customer_id = task.get('customer', {}).get('id')
|
||||
message = task.get('action', {}).get('message')
|
||||
|
||||
if not self.client:
|
||||
return False
|
||||
|
||||
# 发送消息
|
||||
await self.client.send_message(customer_id, message)
|
||||
logger.info(f"发送消息给 {customer_id}: {message}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息失败:{e}")
|
||||
return False
|
||||
|
||||
async def _execute_send_link(self, task: dict) -> bool:
|
||||
"""执行发送链接任务"""
|
||||
try:
|
||||
customer_id = task.get('customer', {}).get('id')
|
||||
link_url = task.get('action', {}).get('link_url')
|
||||
message = task.get('action', {}).get('message', '')
|
||||
|
||||
full_message = f"{message}\n\n{link_url}" if message else link_url
|
||||
|
||||
return await self._execute_send_message({
|
||||
'customer': {'id': customer_id},
|
||||
'action': {'message': full_message}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送链接失败:{e}")
|
||||
return False
|
||||
|
||||
async def _handle_task_failure(self, task: dict, error: str = None):
|
||||
"""处理任务失败"""
|
||||
task_id = task['task_id']
|
||||
max_retry = task.get('max_retry', 3)
|
||||
|
||||
retry_count = self.task_manager.increment_retry(task_id)
|
||||
|
||||
if retry_count <= max_retry:
|
||||
# 重试
|
||||
logger.warning(f"任务失败,{retry_count}/{max_retry} 次重试:{task_id}")
|
||||
await asyncio.sleep(60 * retry_count) # 递增重试间隔
|
||||
await self.execute_task(task)
|
||||
else:
|
||||
# 超过最大重试次数,标记失败
|
||||
self.task_manager.update_task_status(
|
||||
task_id,
|
||||
TaskStatus.FAILED,
|
||||
error_message=error or '超过最大重试次数'
|
||||
)
|
||||
logger.error(f"任务最终失败:{task_id}")
|
||||
|
||||
# 通知天网
|
||||
await self._notify_tianwang(task_id, 'failed', error)
|
||||
|
||||
async def _notify_tianwang(self, task_id: str, status: str, error: str = None):
|
||||
"""通知天网任务状态"""
|
||||
try:
|
||||
# TODO: 实现天网回调 API
|
||||
# 可以通过 HTTP POST 通知天网
|
||||
logger.info(f"通知天网 - 任务 {task_id} 状态:{status}")
|
||||
|
||||
# 示例代码(实际使用时取消注释并配置天网 URL)
|
||||
'''
|
||||
import httpx
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(
|
||||
'http://127.0.0.1:6060/api/task/callback',
|
||||
json={
|
||||
'task_id': task_id,
|
||||
'status': status,
|
||||
'error': error,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
)
|
||||
'''
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"通知天网失败:{e}")
|
||||
|
||||
def check_trigger_match(self, message: str, trigger: dict) -> bool:
|
||||
"""
|
||||
检查消息是否匹配触发条件
|
||||
|
||||
Args:
|
||||
message: 客户消息内容
|
||||
trigger: 触发条件配置
|
||||
|
||||
Returns:
|
||||
是否匹配
|
||||
"""
|
||||
trigger_type = trigger.get('type')
|
||||
|
||||
if trigger_type == 'customer_reply':
|
||||
keyword = trigger.get('keyword')
|
||||
return keyword and keyword in message
|
||||
|
||||
elif trigger_type == 'customer_keyword':
|
||||
keywords = trigger.get('keywords', [])
|
||||
if isinstance(keywords, str):
|
||||
import json
|
||||
keywords = json.loads(keywords)
|
||||
return any(kw in message for kw in keywords)
|
||||
|
||||
elif trigger_type == 'customer_payment':
|
||||
# 付款检测逻辑
|
||||
payment_keywords = ['已付款', '拍下了', '已下单', '付款了']
|
||||
return any(kw in message for kw in payment_keywords)
|
||||
|
||||
elif trigger_type == 'time_reach':
|
||||
# 时间判断
|
||||
target_time = trigger.get('time')
|
||||
if target_time:
|
||||
try:
|
||||
target = datetime.fromisoformat(target_time)
|
||||
return datetime.now() >= target
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# 单例
|
||||
_scheduler: Optional[TaskScheduler] = None
|
||||
|
||||
def get_task_scheduler(client: QingjianAPIClient = None) -> TaskScheduler:
|
||||
"""获取任务调度器单例"""
|
||||
global _scheduler
|
||||
if _scheduler is None:
|
||||
_scheduler = TaskScheduler(client)
|
||||
elif client and _scheduler.client is None:
|
||||
_scheduler.client = client
|
||||
return _scheduler
|
||||
211
core/task_trigger.py
Normal file
211
core/task_trigger.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
任务触发条件匹配引擎
|
||||
支持多种触发条件类型
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskTriggerEngine:
|
||||
"""任务触发引擎"""
|
||||
|
||||
def __init__(self):
|
||||
# 触发器类型注册表
|
||||
self.trigger_handlers = {
|
||||
'customer_reply': self._check_customer_reply,
|
||||
'customer_keyword': self._check_customer_keyword,
|
||||
'customer_payment': self._check_customer_payment,
|
||||
'customer_order': self._check_customer_order,
|
||||
'time_reach': self._check_time_reach,
|
||||
'custom_event': self._check_custom_event,
|
||||
'specified_customer_reply': self._check_specified_customer_reply, # 新增:指定客户回复
|
||||
}
|
||||
|
||||
def check_trigger(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
检查消息是否匹配触发条件
|
||||
|
||||
Args:
|
||||
message: 客户消息内容
|
||||
trigger: 触发条件配置
|
||||
context: 上下文信息(客户 ID、店铺 ID 等)
|
||||
|
||||
Returns:
|
||||
是否匹配
|
||||
"""
|
||||
trigger_type = trigger.get('type')
|
||||
|
||||
if trigger_type not in self.trigger_handlers:
|
||||
logger.warning(f"未知触发类型:{trigger_type}")
|
||||
return False
|
||||
|
||||
handler = self.trigger_handlers[trigger_type]
|
||||
try:
|
||||
return handler(message, trigger, context)
|
||||
except Exception as e:
|
||||
logger.error(f"触发条件检查失败:{e}")
|
||||
return False
|
||||
|
||||
def _check_customer_reply(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
检查客户回复指定内容
|
||||
触发条件:
|
||||
{
|
||||
"type": "customer_reply",
|
||||
"keyword": "好的" // 包含"好的"即匹配
|
||||
}
|
||||
"""
|
||||
keyword = trigger.get('keyword')
|
||||
if not keyword:
|
||||
return False
|
||||
|
||||
return keyword in message
|
||||
|
||||
def _check_customer_keyword(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
检查客户说某关键词
|
||||
触发条件:
|
||||
{
|
||||
"type": "customer_keyword",
|
||||
"keywords": ["好的", "可以", "行"] // 任一关键词匹配
|
||||
}
|
||||
"""
|
||||
keywords = trigger.get('keywords', [])
|
||||
if isinstance(keywords, str):
|
||||
import json
|
||||
keywords = json.loads(keywords)
|
||||
|
||||
if not keywords:
|
||||
return False
|
||||
|
||||
return any(kw in message for kw in keywords)
|
||||
|
||||
def _check_customer_payment(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
检查客户付款
|
||||
触发条件:
|
||||
{
|
||||
"type": "customer_payment",
|
||||
"keywords": ["已付款", "拍下了", "已下单", "付款了"]
|
||||
}
|
||||
"""
|
||||
payment_keywords = trigger.get('keywords', [
|
||||
'已付款', '拍下了', '已下单', '付款了', '已支付', '拍下付款'
|
||||
])
|
||||
|
||||
return any(kw in message for kw in payment_keywords)
|
||||
|
||||
def _check_customer_order(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
检查客户下单
|
||||
触发条件:
|
||||
{
|
||||
"type": "customer_order",
|
||||
"keywords": ["下单了", "拍了", "购买了"]
|
||||
}
|
||||
"""
|
||||
order_keywords = trigger.get('keywords', [
|
||||
'下单了', '拍了', '购买了', '买了'
|
||||
])
|
||||
|
||||
return any(kw in message for kw in order_keywords)
|
||||
|
||||
def _check_time_reach(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
检查到达指定时间
|
||||
触发条件:
|
||||
{
|
||||
"type": "time_reach",
|
||||
"time": "2026-02-27 09:00:00"
|
||||
}
|
||||
"""
|
||||
target_time = trigger.get('time')
|
||||
if not target_time:
|
||||
return False
|
||||
|
||||
try:
|
||||
target = datetime.fromisoformat(target_time)
|
||||
return datetime.now() >= target
|
||||
except Exception as e:
|
||||
logger.error(f"时间解析失败:{e}")
|
||||
return False
|
||||
|
||||
def _check_custom_event(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
自定义事件触发
|
||||
触发条件:
|
||||
{
|
||||
"type": "custom_event",
|
||||
"event_name": "customer_complaint"
|
||||
}
|
||||
"""
|
||||
# 预留自定义事件接口
|
||||
logger.info(f"自定义事件触发:{trigger.get('event_name')}")
|
||||
return True
|
||||
|
||||
def _check_specified_customer_reply(self, message: str, trigger: dict, context: dict = None) -> bool:
|
||||
"""
|
||||
【新增】指定客户回复指定内容
|
||||
触发条件:
|
||||
{
|
||||
"type": "specified_customer_reply",
|
||||
"customer_id": "customer_123", // 指定客户 ID
|
||||
"customer_name": "小明", // 可选:客户名称
|
||||
"keyword": "好的", // 回复内容
|
||||
"exact_match": false // 可选:是否精确匹配
|
||||
}
|
||||
|
||||
匹配逻辑:
|
||||
1. 检查当前消息的客户 ID 是否匹配
|
||||
2. 检查消息内容是否包含关键词
|
||||
"""
|
||||
# 1. 检查客户 ID 是否匹配
|
||||
specified_customer_id = trigger.get('customer_id')
|
||||
specified_customer_name = trigger.get('customer_name')
|
||||
|
||||
if context:
|
||||
current_customer_id = context.get('customer_id')
|
||||
current_customer_name = context.get('customer_name')
|
||||
|
||||
# 客户 ID 匹配检查
|
||||
if specified_customer_id and current_customer_id:
|
||||
if specified_customer_id != current_customer_id:
|
||||
return False
|
||||
|
||||
# 客户名称匹配检查(可选)
|
||||
if specified_customer_name and current_customer_name:
|
||||
if specified_customer_name != current_customer_name:
|
||||
return False
|
||||
else:
|
||||
logger.warning("缺少上下文信息,无法检查指定客户")
|
||||
return False
|
||||
|
||||
# 2. 检查回复内容
|
||||
keyword = trigger.get('keyword')
|
||||
if not keyword:
|
||||
return False
|
||||
|
||||
exact_match = trigger.get('exact_match', False)
|
||||
|
||||
if exact_match:
|
||||
# 精确匹配:消息内容完全等于关键词
|
||||
return message.strip() == keyword.strip()
|
||||
else:
|
||||
# 模糊匹配:消息包含关键词
|
||||
return keyword in message
|
||||
|
||||
|
||||
# 单例
|
||||
_trigger_engine: Optional[TaskTriggerEngine] = None
|
||||
|
||||
def get_trigger_engine() -> TaskTriggerEngine:
|
||||
"""获取触发引擎单例"""
|
||||
global _trigger_engine
|
||||
if _trigger_engine is None:
|
||||
_trigger_engine = TaskTriggerEngine()
|
||||
return _trigger_engine
|
||||
116
core/websocket_client.py
Normal file → Executable file
116
core/websocket_client.py
Normal file → Executable file
@@ -95,6 +95,15 @@ class QingjianAPIClient:
|
||||
self._pending_images: dict = {}
|
||||
self._pending_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
371
core/workflow.py
Normal file → Executable 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
93
core/workflow_router.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
工作流程路由器
|
||||
根据客户说的话判断执行哪种工作流
|
||||
"""
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WorkflowRouter:
|
||||
"""工作流程路由器"""
|
||||
|
||||
def __init__(self):
|
||||
# 查找图片关键词
|
||||
self.find_keywords = [
|
||||
"找一下", "找图", "找原图", "帮我找", "能找到吗",
|
||||
"有吗", "有没有", "找到", "搜一下"
|
||||
]
|
||||
|
||||
# 处理图片关键词
|
||||
self.process_keywords = [
|
||||
"做一下", "处理一下", "安排", "开始做",
|
||||
"弄一下", "修一下", "P 一下", "P 图"
|
||||
]
|
||||
|
||||
# 无法处理关键词
|
||||
self.unable_keywords = [
|
||||
"做不了", "处理不了", "弄不了", "无法处理",
|
||||
"做不到", "搞不定"
|
||||
]
|
||||
|
||||
def detect_workflow(self, message: str) -> Tuple[str, str]:
|
||||
"""
|
||||
检测工作流程类型
|
||||
|
||||
Args:
|
||||
message: 客户消息
|
||||
|
||||
Returns:
|
||||
(workflow_type, confidence)
|
||||
workflow_type: "find_image" / "process_image" / "transfer_human"
|
||||
confidence: 置信度 0-1
|
||||
"""
|
||||
message_lower = message.lower()
|
||||
|
||||
# 1. 检测查找图片
|
||||
for kw in self.find_keywords:
|
||||
if kw in message_lower:
|
||||
logger.info(f"检测到查找图片意图:{kw}")
|
||||
return ("find_image", 0.9)
|
||||
|
||||
# 2. 检测处理图片
|
||||
for kw in self.process_keywords:
|
||||
if kw in message_lower:
|
||||
logger.info(f"检测到处理图片意图:{kw}")
|
||||
return ("process_image", 0.9)
|
||||
|
||||
# 3. 检测无法处理
|
||||
for kw in self.unable_keywords:
|
||||
if kw in message_lower:
|
||||
logger.info(f"检测到无法处理:{kw}")
|
||||
return ("transfer_human", 0.9)
|
||||
|
||||
# 默认返回未知
|
||||
return ("unknown", 0.5)
|
||||
|
||||
def should_find_image(self, message: str) -> bool:
|
||||
"""是否应该查找图片"""
|
||||
workflow_type, _ = self.detect_workflow(message)
|
||||
return workflow_type == "find_image"
|
||||
|
||||
def should_process_image(self, message: str) -> bool:
|
||||
"""是否应该处理图片"""
|
||||
workflow_type, _ = self.detect_workflow(message)
|
||||
return workflow_type == "process_image"
|
||||
|
||||
def should_transfer_human(self, message: str) -> bool:
|
||||
"""是否应该转人工"""
|
||||
workflow_type, _ = self.detect_workflow(message)
|
||||
return workflow_type == "transfer_human"
|
||||
|
||||
|
||||
# 单例
|
||||
_router: Optional[WorkflowRouter] = None
|
||||
|
||||
def get_workflow_router() -> WorkflowRouter:
|
||||
"""获取工作流程路由器单例"""
|
||||
global _router
|
||||
if _router is None:
|
||||
_router = WorkflowRouter()
|
||||
return _router
|
||||
Reference in New Issue
Block a user