Files
tw/core/workflow.py
2026-02-27 16:03:04 +08:00

617 lines
24 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
客服工作流 + 图片任务状态机
架构说明:
- CustomerServiceWorkflow 负责管理图片处理任务的完整生命周期
- 图片AI接入点调用 workflow.image_ai_submit_result(task_id, result_url)
- 消息回调接口:通过 register_send_callback 注入发送函数
"""
import asyncio
import os
import uuid
from enum import Enum
from typing import Optional, Dict, Callable, Awaitable, Any
from datetime import datetime
from dataclasses import dataclass, field
_WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "")
async def _wechat_notify(content: str):
"""workflow 内部异常推送企业微信"""
if not _WECHAT_WEBHOOK:
return
try:
import httpx
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(_WECHAT_WEBHOOK, json={
"msgtype": "markdown",
"markdown": {"content": content}
})
data = resp.json()
if data.get("errcode") == 0:
print(f"[Workflow通知] 企业微信推送成功 ✓")
else:
print(f"[Workflow通知] 企业微信推送失败: {data}")
except Exception as e:
print(f"[Workflow通知] 推送异常: {e}")
from db.customer_db import db
# ========== 任务状态 ==========
class TaskStatus(Enum):
PENDING = "待处理" # 任务已创建等待图片AI处理
PROCESSING = "处理中" # 图片AI正在处理
AWAITING_CONFIRM = "等待客户确认" # 结果已发给客户,等待确认
REVISION = "修改中" # 客户要求修改,重新处理
COMPLETED = "已完成" # 客户确认,邮件已发
FAILED = "失败" # 处理失败
# ========== 任务数据结构 ==========
@dataclass
class ImageTask:
task_id: str
customer_id: str
customer_name: str
original_image: str # 原图路径或URL
operation: str # 处理操作类型
requirements: str = "" # 客户原始需求描述
result_url: str = "" # 处理结果URL
email: str = "" # 客户邮箱
status: TaskStatus = TaskStatus.PENDING
revision_count: int = 0 # 修改次数
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
def update_status(self, status: TaskStatus):
self.status = status
self.updated_at = datetime.now().isoformat()
# ========== 工作流 ==========
class CustomerServiceWorkflow:
"""
客服工作流
图片AI对接方式
1. 调用 create_image_task() 创建任务,获取 task_id
2. 图片AI处理完成后调用 image_ai_submit_result(task_id, result_url)
3. 工作流自动发图给客户确认,并等待客户回复
"""
def __init__(self):
self.tasks: Dict[str, ImageTask] = {} # task_id -> ImageTask
self.customer_active_task: Dict[str, str] = {} # customer_id -> 最新 task_id
self._send_message: Optional[Callable] = None # 注入的消息发送函数
self._agent_notify: Optional[Callable] = None # 注入的 AI 通知函数
self._pending_analysis: Dict[str, dict] = {} # 待报价的识别结果
# ========== 回调注册(由 websocket_client 调用)==========
def register_agent_notify_callback(self, callback: Callable):
"""
注册 AI 通知回调,图片处理完成时调用 AI 生成消息发给客户。
callback 签名:
async def notify(customer_id, acc_id, acc_type, system_prompt)
"""
self._agent_notify = callback
def register_send_callback(self, callback: Callable[[str, str, str, int], Awaitable[None]]):
"""
注册消息发送回调函数
callback 签名:
async def send(customer_id, acc_id, acc_type, content, msg_type=0)
"""
self._send_message = callback
# ========== 任务管理 ==========
def create_image_task(
self,
customer_id: str,
customer_name: str,
original_image: str,
operation: str,
requirements: str = ""
) -> str:
"""
创建图片处理任务,返回 task_id
图片AI收到此 task_id 后开始处理,完成后调用 image_ai_submit_result
"""
task_id = str(uuid.uuid4())
task = ImageTask(
task_id=task_id,
customer_id=customer_id,
customer_name=customer_name,
original_image=original_image,
operation=operation,
requirements=requirements,
)
self.tasks[task_id] = task
self.customer_active_task[customer_id] = task_id
# 记录需求到客户画像
if requirements:
db.add_requirement(customer_id, requirements)
print(f"[Workflow] 创建任务 {task_id} | 客户: {customer_name} | 操作: {operation}")
return task_id
def get_task(self, task_id: str) -> Optional[ImageTask]:
return self.tasks.get(task_id)
def get_customer_active_task(self, customer_id: str) -> Optional[ImageTask]:
task_id = self.customer_active_task.get(customer_id)
return self.tasks.get(task_id) if task_id else None
# ========== 图片识别AI接入点报价用==========
async def image_analysis_result(
self,
customer_id: str,
image_url: str,
complexity: str,
acc_id: str = "",
acc_type: str = "AliWorkbench",
gemini_prompt: str = "",
aspect_ratio: str = "1:1",
perspective: str = "no",
proc_type: str = "",
subject: str = "",
quality: str = "",
) -> bool:
"""
【图片识别AI专用接口】分析完成后调用此方法触发客服AI报价
Args:
customer_id: 客户ID
image_url: 图片URL原图
complexity: 复杂度评估结果,枚举值:
"simple" → 10-20元
"normal" → 20-30元
"complex" → 30元
"hard" → 40元
acc_id: 店铺账号ID
acc_type: 平台类型
Returns:
True = 成功触发报价False = 客户不存在
"""
price_map = {
"simple": "10-15元这张比较简单",
"normal": "15-20元",
"complex": "20-25元",
"hard": "25-30元",
}
price_hint = price_map.get(complexity, "20元")
# 把所有分析字段存入任务
requirements = f"complexity:{complexity}"
if gemini_prompt:
requirements += f"|prompt:{gemini_prompt}"
if aspect_ratio:
requirements += f"|ratio:{aspect_ratio}"
if perspective and perspective != "no":
requirements += f"|perspective:{perspective}"
if proc_type:
requirements += f"|proc_type:{proc_type}"
if subject:
requirements += f"|subject:{subject}"
if quality:
requirements += f"|quality:{quality}"
task_id = self.create_image_task(
customer_id=customer_id,
customer_name=customer_id,
original_image=image_url,
operation="enhance",
requirements=requirements,
)
print(f"[Workflow] 图片识别完成 | 客户:{customer_id} | 复杂度:{complexity} | 建议报价:{price_hint}")
# 通知客服AI报价把识别结果注入消息让AI根据结果报价
if self._send_message:
# 这里不直接发价格,而是触发 agent 重新处理一条带识别结果的内部消息
# 实际报价由客服AI根据 complexity 生成,保持口吻一致
self._pending_analysis[customer_id] = {
"task_id": task_id,
"complexity": complexity,
"price_hint": price_hint,
"image_url": image_url,
}
return True
def get_pending_analysis(self, customer_id: str) -> dict:
"""
客服AI处理消息时调用检查该客户是否有待报价的识别结果
取出后自动清除(一次性)
"""
return self._pending_analysis.pop(customer_id, None)
# ========== 付款后触发 Gemini 作图 ==========
async def trigger_processing_on_payment(
self,
customer_id: str,
acc_id: str = "",
acc_type: str = "AliWorkbench"
) -> bool:
try:
from config.config import IMAGE_MODULE_ENABLED
if not IMAGE_MODULE_ENABLED:
await _wechat_notify(
f" **付款触发但已暂停自动作图**\n客户:{customer_id}\n店铺:{acc_id}\n请人工安排处理"
)
return False
except Exception:
return False
"""
客户付款后调用此方法,找到该客户待处理的任务并启动 Gemini 作图。
由 pydantic_ai_agent 在识别到"已付款"订单通知时调用。
也可作为 tool 由 AI 主动触发。
Returns:
True=已启动处理, False=无待处理任务
"""
task = self.get_customer_active_task(customer_id)
if not task:
# 内存任务丢失(重启场景)→ 从客户档案重建
print(f"[Workflow] 付款触发:内存无任务,尝试从客户档案重建 | 客户: {customer_id}")
task = await self._rebuild_task_from_profile(customer_id, acc_id, acc_type)
if not task:
print(f"[Workflow] 付款触发:客户 {customer_id} 无图片记录,无法重建任务,跳过")
await _wechat_notify(
f"⚠️ **付款但无图片**\n"
f"客户:{customer_id}\n"
f"店铺:{acc_id}\n"
f"已付款但找不到待处理图片,请人工发图处理"
)
return False
if task.status not in (TaskStatus.PENDING,):
print(f"[Workflow] 付款触发:任务 {task.task_id[:8]}... 状态={task.status.value},跳过")
return False
task.operation = task.operation or "enhance"
print(f"[Workflow] 付款确认,启动 Gemini 处理 | 客户: {customer_id} | 任务: {task.task_id[:8]}...")
asyncio.create_task(self._auto_process(task.task_id, acc_id=acc_id, acc_type=acc_type))
return True
async def _rebuild_task_from_profile(
self, customer_id: str, acc_id: str, acc_type: str
) -> Optional["ImageTask"]:
"""
重启后任务丢失时,从客户档案里读取 last_image_url 重建一个 PENDING 任务。
"""
try:
from db.customer_db import db
profile = db.get_customer(customer_id)
image_url = profile.last_image_url
if not image_url:
return None
complexity = profile.complexity_history[-1] if profile.complexity_history else ""
gemini_prompt = getattr(profile, "last_gemini_prompt", "")
aspect_ratio = getattr(profile, "last_aspect_ratio", "1:1")
perspective = getattr(profile, "last_perspective", "no")
requirements = f"complexity:{complexity}" if complexity else ""
if gemini_prompt:
requirements += f"|prompt:{gemini_prompt}"
if aspect_ratio:
requirements += f"|ratio:{aspect_ratio}"
if perspective and perspective != "no":
requirements += f"|perspective:{perspective}"
task_id = str(uuid.uuid4())
task = ImageTask(
task_id=task_id,
customer_id=customer_id,
customer_name=profile.name or customer_id,
original_image=image_url,
operation="enhance",
requirements=requirements,
status=TaskStatus.PENDING,
)
self.tasks[task_id] = task
self.customer_active_task[customer_id] = task_id
print(f"[Workflow] 任务已重建 | 客户: {customer_id} | 图片: {image_url[:60]}...")
return task
except Exception as e:
print(f"[Workflow] 任务重建失败: {e}")
return None
@staticmethod
def _parse_requirements(requirements: str) -> dict:
"""从 requirements 字符串解析各字段,格式: complexity:xxx|prompt:xxx|ratio:xxx"""
parsed = {}
for part in (requirements or "").split("|"):
part = part.strip()
if ":" in part:
k, v = part.split(":", 1)
parsed[k.strip()] = v.strip()
return parsed
async def _auto_process(self, task_id: str, acc_id: str = "", acc_type: str = "AliWorkbench"):
"""付款确认后自动调用 Gemini 处理图片,完成后通知客户"""
try:
from config.config import IMAGE_MODULE_ENABLED
if not IMAGE_MODULE_ENABLED:
return
except Exception:
return
task = self.tasks.get(task_id)
if not task:
return
task.update_status(TaskStatus.PROCESSING)
req = self._parse_requirements(task.requirements)
gemini_prompt = req.get("prompt", "")
aspect_ratio = req.get("ratio", "1:1")
perspective = req.get("perspective", "no")
proc_type = req.get("proc_type", "")
subject = req.get("subject", "")
quality = req.get("quality", "")
revision_note = req.get("revision", "")
# 客户修改意见追加到 prompt 末尾
if revision_note:
gemini_prompt = (gemini_prompt or "") + f"\n【客户修改要求】{revision_note}"
print(f"[Workflow] Gemini 开始处理 | 任务: {task_id[:8]}... | 比例: {aspect_ratio} | 透视: {perspective} | 图片: {task.original_image}")
try:
from image.image_processor import image_processor
from utils.image_queue import run_with_queue
result = await run_with_queue(image_processor.process_image(
task.original_image,
task.operation,
requirements=task.requirements,
gemini_prompt=gemini_prompt,
aspect_ratio=aspect_ratio,
perspective=perspective,
proc_type=proc_type,
subject=subject,
quality=quality,
))
if result["success"]:
attempts = result.get("attempts", 1)
qa_score = result.get("qa_score", 0)
qa_pass = result.get("qa_pass", True)
qa_issue = result.get("qa_issue", "")
print(f"[Workflow] Gemini 处理完成 | 任务: {task_id[:8]}... | 质检: {qa_score}分 | 尝试: {attempts}")
# 质检未通过(已达重试上限,保留结果但人工跟进)
if not qa_pass:
await _wechat_notify(
f"⚠️ **图片质检未通过,请人工核查**\n"
f"客户:{task.customer_id}\n"
f"店铺:{acc_id}\n"
f"质检得分:{qa_score}/100\n"
f"问题:{qa_issue}\n"
f"已处理 {attempts} 次,结果已发出,请人工确认质量"
)
await self.image_ai_submit_result(
task_id=task_id,
result_url=result["result_path"],
acc_id=acc_id,
acc_type=acc_type,
)
else:
err_msg = result['message']
print(f"[Workflow] Gemini 处理失败: {err_msg}")
task.update_status(TaskStatus.FAILED)
# 企业微信预警
await _wechat_notify(
f"⚠️ **Gemini作图失败**\n"
f"客户:{task.customer_id}\n"
f"店铺:{acc_id}\n"
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="您好,图片正在处理中,稍后发您,请稍等",
msg_type=0,
)
except Exception as e:
print(f"[Workflow] 自动处理异常: {e}")
task.update_status(TaskStatus.FAILED)
await _wechat_notify(
f"⚠️ **Workflow处理异常**\n"
f"客户:{task.customer_id}\n"
f"错误:{str(e)[:200]}"
)
# ========== 图片AI接入点作图用==========
async def image_ai_submit_result(
self,
task_id: str,
result_url: str,
acc_id: str = "",
acc_type: str = "AliWorkbench"
) -> bool:
"""
【图片AI专用接口】处理完成后调用此方法
Args:
task_id: create_image_task 返回的任务ID
result_url: 处理后的图片URL或本地路径
acc_id: 店铺账号ID发消息用
acc_type: 平台类型
Returns:
True = 成功False = 任务不存在
"""
task = self.tasks.get(task_id)
if not task:
print(f"[Workflow] 任务不存在: {task_id}")
return False
task.result_url = result_url
task.update_status(TaskStatus.AWAITING_CONFIRM)
print(f"[Workflow] 任务 {task_id} 处理完成,发送给客户确认")
# 先发结果图片
if self._send_message:
await self._send_message(
customer_id=task.customer_id,
acc_id=acc_id,
acc_type=acc_type,
content=result_url,
msg_type=1 # 图片
)
# 让客服 AI 生成完成通知话术(自然口吻,询问邮箱)
if self._agent_notify:
await self._agent_notify(
customer_id=task.customer_id,
acc_id=acc_id,
acc_type=acc_type,
system_hint="【图片已处理完成并发给客户】请用自然口吻告诉客户图发好了让他看一下效果没问题把邮箱发过来你来发给他。不超过1句话。",
)
elif self._send_message:
# 兜底AI 不可用时用固定话术
await self._send_message(
customer_id=task.customer_id,
acc_id=acc_id,
acc_type=acc_type,
content="好了,你看一下效果,没问题把邮箱发我",
msg_type=0,
)
return True
# ========== 客户回复处理 ==========
async def handle_customer_reply(
self,
customer_id: str,
message: str,
acc_id: str = "",
acc_type: str = "AliWorkbench"
) -> Optional[str]:
"""
处理正在等待确认的客户回复
Returns:
需要回复客户的文本None 表示不是确认相关消息
"""
task = self.get_customer_active_task(customer_id)
if not task or task.status != TaskStatus.AWAITING_CONFIRM:
return None
msg = message.strip()
# 提取邮箱
import re
email_match = re.search(r'[\w\.-]+@[\w\.-]+\.\w+', msg)
if email_match:
email = email_match.group()
task.email = email
db.update_email(customer_id, email)
# 发送邮件(调用 email_sender
result = await self._send_email(task)
if result:
task.update_status(TaskStatus.COMPLETED)
db.update_email_status(task.customer_id, "sent")
db.complete_order(task.customer_id, had_revision=task.revision_count > 0)
db.auto_compute_tags(task.customer_id)
return "发到您邮箱了,注意查收哈"
else:
db.update_email_status(task.customer_id, "failed")
return "邮件发送失败了,您再发一次邮箱试试"
# 客户说不满意/要改
negative_keywords = ["不好", "不对", "不满意", "重做", "改一下", "差太多", "不行", "效果不好", "颜色不对"]
if any(kw in msg for kw in negative_keywords):
task.revision_count += 1
task.update_status(TaskStatus.REVISION)
db.record_revision(task.customer_id)
# 把客户的修改意见追加进 requirements下次重做时 Gemini 能看到
if msg:
task.requirements += f"|revision:{msg[:100]}"
return "好,你说一下哪里要改,或者发图告诉我"
# 客户提供了修改说明(处于 REVISION 状态时)
if task.status == TaskStatus.REVISION and msg:
task.requirements += f"|revision:{msg[:100]}"
task.update_status(TaskStatus.PENDING)
# 重新触发处理
asyncio.create_task(
self._auto_process(task.task_id, acc_id=acc_id, acc_type=acc_type)
)
return "好的,重新给你做"
return None
async def _send_email(self, task: ImageTask) -> bool:
"""发送完成作品邮件"""
try:
from mail.email_sender import email_sender
profile = db.get_customer(task.customer_id)
result = email_sender.send_completed_work(
to_email=task.email,
customer_name=profile.name or task.customer_name,
image_description=task.requirements or task.operation,
result_images=[task.result_url]
)
return result.get("success", False)
except Exception as e:
print(f"[Workflow] 邮件发送失败: {e}")
await _wechat_notify(
f"⚠️ **邮件发送失败**\n"
f"客户:{task.customer_id}\n"
f"邮箱:{task.email}\n"
f"错误:{str(e)[:200]}"
)
return False
# ========== 工具方法 ==========
def detect_operation(self, message: str) -> str:
"""根据客户描述识别处理操作"""
msg = message.lower()
if any(kw in msg for kw in ["模糊", "清晰", "高清", "变清"]):
return "enhance"
elif any(kw in msg for kw in ["背景", "去背", "抠图", "透明"]):
return "remove_bg"
elif any(kw in msg for kw in ["尺寸", "大小", "缩放", "分辨率"]):
return "resize"
elif any(kw in msg for kw in ["老照片", "修复", "发黄", "破损"]):
return "fix_old_photo"
elif any(kw in msg for kw in ["分层", "psd"]):
return "layered"
else:
return "enhance"
def get_task_summary(self) -> str:
"""获取当前所有任务摘要(调试用)"""
if not self.tasks:
return "暂无任务"
lines = []
for tid, task in self.tasks.items():
lines.append(
f" [{task.status.value}] {task.customer_name} | {task.operation} | {tid[:8]}..."
)
return "\n".join(lines)
# ========== 全局实例 ==========
workflow = CustomerServiceWorkflow()