refactor: migrate workflow to v2 core and archive legacy modules
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
29
core/adapters/base.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from core.schema import StandardMessage, StandardResponse
|
||||
|
||||
class BaseAdapter(ABC):
|
||||
"""
|
||||
消息适配器基类 (Interface)
|
||||
所有的平台接口(千牛、微信等)都必须继承并实现这几个方法
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def translate_inbound(self, raw_msg: any) -> StandardMessage:
|
||||
"""
|
||||
[接收]:把各个平台的原始 JSON 数据,格式化为大脑认的 StandardMessage
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def translate_outbound(self, response: StandardResponse, user_id: str):
|
||||
"""
|
||||
[发送]:把大脑生成的 StandardResponse,翻译回平台原生的接口发出去
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def platform_id(self) -> str:
|
||||
"""
|
||||
标识当前平台名称 (如 'qianniu', 'wechat')
|
||||
"""
|
||||
pass
|
||||
92
core/adapters/qianniu_adapter.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import re
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
from core.adapters.base import BaseAdapter
|
||||
from core.schema import StandardMessage, StandardResponse
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class QianniuAdapter(BaseAdapter):
|
||||
"""
|
||||
千牛适配器:支持识别消息来源(客户 vs 商家人工)。
|
||||
"""
|
||||
def __init__(self, ws_client=None):
|
||||
self.ws_client = ws_client
|
||||
self._default_group_id = "20252916034"
|
||||
|
||||
def platform_id(self) -> str:
|
||||
return "qianniu"
|
||||
|
||||
def _resolve_group_id(self, acc_id: str) -> str:
|
||||
try:
|
||||
config_path = Path("config/transfer_groups.json")
|
||||
if config_path.exists():
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
return cfg.get(acc_id, self._default_group_id)
|
||||
except Exception: pass
|
||||
return self._default_group_id
|
||||
|
||||
async def translate_inbound(self, raw: dict) -> Tuple[StandardMessage, str]:
|
||||
"""
|
||||
返回: (标准消息, 消息方向)
|
||||
direction: 'in' (客户发给商家), 'out' (商家人工在后台回复)
|
||||
"""
|
||||
if not isinstance(raw, dict): raw = {}
|
||||
|
||||
acc_id = str(raw.get("acc_id") or raw.get("shop_id") or "")
|
||||
from_id = str(raw.get("from_id") or raw.get("cy_id") or "")
|
||||
msg_text = str(raw.get("msg") or raw.get("content") or "")
|
||||
|
||||
# 判断方向:如果 from_id 包含了店铺名或 acc_id,通常说明是商家自己在说话
|
||||
# 或者逆向接口通常有一个特定的标识,这里我们做一个通用的逻辑判断
|
||||
direction = "in"
|
||||
user_id = from_id
|
||||
|
||||
# 逻辑:如果发送者 ID 等于 店铺 ID,说明是【商家人工回复】
|
||||
if from_id == acc_id and acc_id != "":
|
||||
direction = "out"
|
||||
# 此时 cy_id (客户ID) 通常在另一个字段里
|
||||
user_id = str(raw.get("cy_id") or "")
|
||||
|
||||
msg = StandardMessage(
|
||||
platform=self.platform_id(),
|
||||
msg_id=str(raw.get("msg_id", "")),
|
||||
user_id=user_id,
|
||||
user_name=str(raw.get("from_name", "")),
|
||||
content=msg_text,
|
||||
image_urls=self._extract_urls(msg_text),
|
||||
acc_id=acc_id,
|
||||
acc_type=str(raw.get("acc_type") or "AliWorkbench"),
|
||||
raw_data=raw
|
||||
)
|
||||
return msg, direction
|
||||
|
||||
async def translate_outbound(self, res: StandardResponse, user_id: str):
|
||||
if not self.ws_client: return
|
||||
if not res or (not res.should_reply and not res.need_transfer): return
|
||||
|
||||
meta = res.metadata if isinstance(res.metadata, dict) else {}
|
||||
acc_id = meta.get("acc_id", "")
|
||||
acc_type = meta.get("acc_type", "AliWorkbench")
|
||||
|
||||
if "[转移会话]" in res.reply_content:
|
||||
content = res.reply_content
|
||||
elif res.need_transfer:
|
||||
group_id = self._resolve_group_id(acc_id)
|
||||
content = f"正在为您转接|[转移会话],分组{group_id},无原因"
|
||||
else:
|
||||
content = res.reply_content
|
||||
|
||||
try:
|
||||
await self.ws_client.send(customer_id=user_id, acc_id=acc_id, acc_type=acc_type, content=content, msg_type=res.msg_type)
|
||||
except Exception as e:
|
||||
logger.error(f"[QianniuAdapter] 发送失败: {e}")
|
||||
|
||||
def _extract_urls(self, text: str) -> List[str]:
|
||||
if not text: return []
|
||||
image_exts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")
|
||||
candidates = re.findall(r'https?://[^\s#]+', text)
|
||||
return [u for u in candidates if any(ext in u.lower() for ext in image_exts)]
|
||||
@@ -1,688 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import asyncio
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_ai import RunContext
|
||||
|
||||
from db.customer_risk_db import risk_db
|
||||
from services.service_tuhui_upload import upload_to_tuhui
|
||||
from core.order_helpers import parse_order_info
|
||||
from core.schema import StandardResponse
|
||||
from services.dispatch_service import dispatch_service
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
|
||||
def register_tools(agent) -> None:
|
||||
"""注册所有 Tool,让 Agent 可以主动调用。"""
|
||||
|
||||
@agent.agent.tool
|
||||
async def analyze_image(ctx: RunContext[Any], image_url: str) -> str:
|
||||
async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(description="转人工的原因")) -> str:
|
||||
"""
|
||||
分析客户发来的图片复杂度,用于报价。
|
||||
收到图片URL时调用此工具,返回复杂度和建议报价。
|
||||
【核心工具】执行转人工逻辑。
|
||||
获取设计师姓名并生成精准转接指令。
|
||||
"""
|
||||
try:
|
||||
from image.image_analyzer import image_analyzer
|
||||
result = await image_analyzer.analyze(image_url)
|
||||
complexity_label = {
|
||||
"simple": "简单(画面干净)",
|
||||
"normal": "一般复杂度",
|
||||
"complex": "细节偏多",
|
||||
"hard": "非常复杂",
|
||||
}.get(result["complexity"], result["complexity"])
|
||||
# 持久化图片URL和复杂度,重启后仍能记住这张图
|
||||
try:
|
||||
from db.customer_db import db
|
||||
db.update_last_image(
|
||||
ctx.deps.from_id,
|
||||
image_url,
|
||||
complexity=result["complexity"],
|
||||
gemini_prompt=result.get("gemini_prompt", ""),
|
||||
aspect_ratio=result.get("aspect_ratio", "1:1"),
|
||||
perspective=result.get("perspective", "no"),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"[Tool] 尝试呼叫设计师接手: {reason}")
|
||||
|
||||
# 存图片类型到客户画像
|
||||
try:
|
||||
from db.customer_db import db as _db
|
||||
if result.get("subject"):
|
||||
_db.add_image_type(ctx.deps.from_id, result["subject"])
|
||||
except Exception:
|
||||
pass
|
||||
# 1. 尝试派单获取设计师姓名
|
||||
designer_name = await dispatch_service.assign_designer()
|
||||
|
||||
# 在 workflow 里创建待处理任务(付款后自动触发 Gemini)
|
||||
try:
|
||||
from core.workflow import workflow
|
||||
await workflow.image_analysis_result(
|
||||
customer_id=ctx.deps.from_id,
|
||||
image_url=image_url,
|
||||
complexity=result["complexity"],
|
||||
acc_id=ctx.deps.acc_id,
|
||||
acc_type=ctx.deps.platform,
|
||||
gemini_prompt=result.get("gemini_prompt", ""),
|
||||
aspect_ratio=result.get("aspect_ratio", "1:1"),
|
||||
perspective=result.get("perspective", "no"),
|
||||
proc_type=result.get("proc_type", ""),
|
||||
subject=result.get("subject", ""),
|
||||
quality=result.get("quality", ""),
|
||||
)
|
||||
logger.info(
|
||||
"[Agent] Workflow 任务已创建 | 客户: %s | 比例: %s | 透视: %s | 图片: %s...",
|
||||
ctx.deps.from_id,
|
||||
result.get("aspect_ratio"),
|
||||
result.get("perspective"),
|
||||
image_url[:60],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("[Agent] Workflow 任务创建失败: %s", e)
|
||||
|
||||
# 组装给 AI 的分析报告
|
||||
risk = result.get("risk", "none")
|
||||
has_face = result.get("has_face", "no")
|
||||
feasibility = result.get("feasibility", "yes")
|
||||
note = result.get("note", "")
|
||||
|
||||
lines = [
|
||||
f"图片主体:{result['subject'] or '未识别'}",
|
||||
f"处理类型:{result['proc_type'] or '高清修复'}",
|
||||
f"原图质量:{result['quality'] or '未知'}",
|
||||
f"图片类型:{result.get('category', '') or '通用'}",
|
||||
f"图片尺寸:{(result.get('width') or 0)}x{(result.get('height') or 0)}({result.get('megapixels', 0.0)}MP)",
|
||||
f"含人脸:{'是' if has_face == 'yes' else '否'}",
|
||||
f"复杂度:{complexity_label}",
|
||||
f"原因:{result['reason']}",
|
||||
]
|
||||
if result.get("size_surcharge"):
|
||||
lines.append(f"尺寸加价:+{result['size_surcharge']}元")
|
||||
if result.get("size_note"):
|
||||
lines.append(f"尺寸提示:{result['size_note']}")
|
||||
try:
|
||||
st = agent._get_conversation_state(ctx.deps.from_id)
|
||||
if isinstance(result.get("price_min"), (int, float)):
|
||||
st.last_min_price = int(result.get("price_min") or 0)
|
||||
try:
|
||||
from db.customer_db import db as _db
|
||||
_db.update_last_min_price(ctx.deps.from_id, st.last_min_price)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 根据可做性和风险等级给 AI 不同的行动指引
|
||||
if feasibility == "no":
|
||||
if "敏感" in (note or ""):
|
||||
lines.append("【拒绝】图片含敏感/黄色/擦边内容,不接单。")
|
||||
lines.append("→ 直接拒绝,不说「发图来看看」,自然回复如:这类不做/不接。")
|
||||
if designer_name:
|
||||
# 2. 有设计师在线:生成标准转接指令
|
||||
magic_cmd = f"正在为您转接|[转移会话],{designer_name},无原因"
|
||||
logger.info(f"[Tool] 成功呼叫设计师: {designer_name}")
|
||||
return magic_cmd
|
||||
else:
|
||||
lines.append("【无法处理】此图无法处理(纯黑/纯白/完全损坏/要找原始RAW文件)。")
|
||||
lines.append("→ 告知客户无法处理,建议换图或说明原因,不要报价。")
|
||||
elif risk == "high":
|
||||
lines.append(f"【高风险】此图处理风险高:{note or 'AI修复后效果不能保证与原图一致'}")
|
||||
lines.append(f"建议报价:{result['price_suggest']}元")
|
||||
lines.append("→ 先自然说明风险(人脸/效果可能不完美),再报价,满意再拍。话术自然。")
|
||||
elif risk == "low":
|
||||
lines.append(f"【低风险-含人脸】修复后人脸相似度约70-90%,效果不稳定。")
|
||||
lines.append(f"建议报价:{result['price_suggest']}元")
|
||||
lines.append(f"→ 报价时自然加一句风险提示(人脸可能有轻微变化、满意再付等)")
|
||||
else:
|
||||
# 无风险,正常报价
|
||||
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
|
||||
# 3. 设计师下线:返回特定信号
|
||||
logger.warning("[Tool] 派单失败:设计师们已下线或不在位")
|
||||
return "ERROR_NO_DESIGNER_ONLINE"
|
||||
|
||||
# 构建报价说明
|
||||
price_explanation = f"建议报价:{total_price}元"
|
||||
if text_surcharge > 0:
|
||||
price_explanation += f"(含文字处理 +{text_surcharge}元)"
|
||||
if layer_surcharge > 0:
|
||||
price_explanation += f"(含分层 +{layer_surcharge}元)"
|
||||
async def check_order_status_tool(ctx: RunContext[Any], customer_id: str = Field(description="客户ID")) -> str:
|
||||
"""查询订单状态。"""
|
||||
return "设计师正在后台加急处理中,请稍等哈。"
|
||||
|
||||
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"【立刻回复客户报价 {total_price} 元,话术自然多变】")
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"图片分析失败: {e},请根据经验判断报价"
|
||||
|
||||
@agent.agent.tool
|
||||
async def get_customer_info(ctx: RunContext[Any], customer_id: str) -> str:
|
||||
"""
|
||||
查询客户历史信息:消费记录、性格标签、报价历史等。
|
||||
对话开始时或需要了解客户背景时调用。
|
||||
"""
|
||||
try:
|
||||
from db.customer_db import db
|
||||
return db.get_profile_text(customer_id)
|
||||
except Exception as e:
|
||||
return f"查询失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def transfer_to_human(ctx: RunContext[Any]) -> str:
|
||||
"""
|
||||
转接人工客服。
|
||||
遇到退款/投诉/情绪激动/复杂售后时调用。
|
||||
"""
|
||||
return "TRANSFER_REQUESTED"
|
||||
|
||||
@agent.agent.tool
|
||||
async def get_customer_risk_profile(ctx: RunContext[Any], customer_id: str = "") -> str:
|
||||
"""查询客户风控画像:退款/不付款/差评/人工黑名单等。"""
|
||||
cid = customer_id or ctx.deps.from_id
|
||||
try:
|
||||
info = risk_db.evaluate_customer(cid)
|
||||
return (
|
||||
f"客户:{cid}\n"
|
||||
f"不接单:{'是' if info.get('do_not_serve') else '否'}\n"
|
||||
f"风险等级:{info.get('computed_level','low')} 分数:{info.get('computed_score',0)}\n"
|
||||
f"近30天退款:{info.get('refund_30d',0)}\n"
|
||||
f"近7天未付款下单:{info.get('unpaid_7d',0)}\n"
|
||||
f"近90天差评:{info.get('bad_review_90d',0)}\n"
|
||||
f"备注:{info.get('note','') or '无'}"
|
||||
)
|
||||
except Exception as e:
|
||||
return f"查询风控画像失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def mark_customer_risk(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
do_not_serve: bool = False,
|
||||
risk_level: str = "low",
|
||||
risk_score: int = 0,
|
||||
note: str = "",
|
||||
tag: str = "",
|
||||
) -> str:
|
||||
"""人工标记客户风控画像(不接单/高风险/备注标签)。"""
|
||||
try:
|
||||
tags = [tag] if tag else []
|
||||
risk_db.set_profile(
|
||||
customer_id=customer_id,
|
||||
do_not_serve=do_not_serve,
|
||||
risk_level=risk_level,
|
||||
risk_score=risk_score,
|
||||
note=note,
|
||||
tags=tags,
|
||||
)
|
||||
return "风控画像已更新"
|
||||
except Exception as e:
|
||||
return f"更新风控画像失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def record_customer_risk_event(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
event_type: str,
|
||||
event_count: int = 1,
|
||||
note: str = "",
|
||||
) -> str:
|
||||
"""记录风控事件:refund/unpaid_order/bad_review/blacklist_hit 等。"""
|
||||
try:
|
||||
risk_db.record_event(
|
||||
customer_id=customer_id,
|
||||
event_type=event_type,
|
||||
event_count=event_count,
|
||||
note=note,
|
||||
)
|
||||
return "风控事件已记录"
|
||||
except Exception as e:
|
||||
return f"记录风控事件失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def save_customer_note(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
note: str
|
||||
) -> str:
|
||||
"""
|
||||
记录客户关键信息到画像(邮箱/微信/特殊需求等)。
|
||||
客户提供联系方式或重要信息时调用。
|
||||
"""
|
||||
try:
|
||||
from db.customer_db import db
|
||||
db.add_note(customer_id, note)
|
||||
return "已记录"
|
||||
except Exception as e:
|
||||
return f"记录失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def update_contact_info(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
contact_type: str,
|
||||
value: str
|
||||
) -> str:
|
||||
"""
|
||||
更新客户联系方式。
|
||||
当客户说出邮箱/手机/微信时调用,比正则提取更准确。
|
||||
|
||||
contact_type 枚举值:
|
||||
email - 邮箱
|
||||
phone - 手机号
|
||||
wechat - 微信号
|
||||
"""
|
||||
try:
|
||||
from db.customer_db import db
|
||||
if contact_type == "email":
|
||||
db.update_email(customer_id, value)
|
||||
elif contact_type == "phone":
|
||||
db.update_phone(customer_id, value)
|
||||
elif contact_type == "wechat":
|
||||
db.update_wechat(customer_id, value)
|
||||
else:
|
||||
return f"未知联系方式类型: {contact_type}"
|
||||
return f"已保存 {contact_type}: {value}"
|
||||
except Exception as e:
|
||||
return f"保存失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def record_quote(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
price: int,
|
||||
description: str = ""
|
||||
) -> str:
|
||||
"""
|
||||
记录本次报价到客户画像,用于后续对话保持价格一致。
|
||||
每次给客户报价后调用。
|
||||
|
||||
Args:
|
||||
customer_id: 客户ID
|
||||
price: 报价金额(元)
|
||||
description: 报价描述,如"单图处理"/"三图打包"
|
||||
"""
|
||||
try:
|
||||
from db.customer_db import db
|
||||
db.update_last_price(customer_id, price)
|
||||
if description:
|
||||
db.add_note(customer_id, f"报价 {price}元({description})")
|
||||
# 同步到内存状态
|
||||
state = agent.conversations.get(customer_id)
|
||||
if state:
|
||||
state.last_price = price
|
||||
return f"已记录报价 {price}元"
|
||||
except Exception as e:
|
||||
return f"记录失败: {e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def process_image_gemini(ctx: RunContext[Any], customer_id: str = "") -> str:
|
||||
"""
|
||||
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
|
||||
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
|
||||
处理完成后会自动发图给客户。
|
||||
"""
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不自动作图"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不自动作图"
|
||||
cid = customer_id or ctx.deps.from_id
|
||||
try:
|
||||
from core.workflow import workflow
|
||||
ok = await workflow.trigger_processing_on_payment(
|
||||
customer_id=cid,
|
||||
acc_id=ctx.deps.acc_id,
|
||||
acc_type=ctx.deps.platform,
|
||||
)
|
||||
if ok:
|
||||
return "已安排,稍后发你"
|
||||
return "该客户暂无待处理图片,请先发图"
|
||||
except Exception as e:
|
||||
return f"触发作图失败: {e},请稍后重试或转人工"
|
||||
|
||||
@agent.agent_pricing.tool
|
||||
async def analyze_image_pricing(ctx: RunContext[Any], image_url: str) -> str:
|
||||
try:
|
||||
from image.image_analyzer import image_analyzer
|
||||
result = await image_analyzer.analyze(image_url)
|
||||
if result.get("feasibility") == "no" or result.get("risk") == "high":
|
||||
return "该图风险高或不可做:不报价,建议换图或转人工评估。"
|
||||
if not result.get("success", False):
|
||||
return "图片识别异常:先不报价,建议客户重发更清晰图片。"
|
||||
p = result.get("price_suggest", 20)
|
||||
try:
|
||||
st = agent._get_conversation_state(ctx.deps.from_id)
|
||||
if isinstance(result.get("price_min"), (int, float)):
|
||||
st.last_min_price = int(result.get("price_min") or 0)
|
||||
try:
|
||||
from db.customer_db import db as _db
|
||||
_db.update_last_min_price(ctx.deps.from_id, st.last_min_price)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return f"建议报价:{p}元"
|
||||
except Exception as e:
|
||||
return f"图片分析失败: {e}"
|
||||
|
||||
@agent.agent_pricing.tool
|
||||
async def record_quote_pricing(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
price: int,
|
||||
description: str = ""
|
||||
) -> str:
|
||||
try:
|
||||
from db.customer_db import db
|
||||
db.update_last_price(customer_id, price)
|
||||
return "ok"
|
||||
except Exception as e:
|
||||
return f"记录失败: {e}"
|
||||
|
||||
@agent.agent_processing.tool
|
||||
async def process_image_gemini_run(ctx: RunContext[Any], customer_id: str = "") -> str:
|
||||
"""触发 Gemini 作图处理(processing agent 专用入口)。"""
|
||||
return await process_image_gemini(ctx, customer_id)
|
||||
|
||||
@agent.agent_similar.tool
|
||||
async def recommend_similar(ctx: RunContext[Any], hint: str = "") -> str:
|
||||
try:
|
||||
return "有类似款,拍下我发你参考图。"
|
||||
except Exception as e:
|
||||
return f"推荐失败: {e}"
|
||||
|
||||
@agent.agent_order.tool
|
||||
async def handle_order(ctx: RunContext[Any], raw_msg: str = "") -> str:
|
||||
try:
|
||||
info = parse_order_info(raw_msg or "")
|
||||
paid_kw = ["等待发货", "已付款", "付款成功", "买家已付款"]
|
||||
if any(k in (info.get("pay_status", "") or "") for k in paid_kw) or any(k in (info.get("order_status", "") or "") for k in paid_kw):
|
||||
return "已安排,稍后发你"
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@agent.agent_risk.tool
|
||||
async def risk_filter(ctx: RunContext[Any], text: str = "") -> str:
|
||||
return "这类不做哈,政治/敏感内容都不接。"
|
||||
|
||||
@agent.agent_risk.tool
|
||||
async def get_customer_risk_profile_risk(ctx: RunContext[Any], customer_id: str = "") -> str:
|
||||
return await get_customer_risk_profile(ctx, customer_id)
|
||||
|
||||
@agent.agent_risk.tool
|
||||
async def mark_customer_risk_risk(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
do_not_serve: bool = False,
|
||||
risk_level: str = "low",
|
||||
risk_score: int = 0,
|
||||
note: str = "",
|
||||
tag: str = "",
|
||||
) -> str:
|
||||
return await mark_customer_risk(
|
||||
ctx=ctx,
|
||||
customer_id=customer_id,
|
||||
do_not_serve=do_not_serve,
|
||||
risk_level=risk_level,
|
||||
risk_score=risk_score,
|
||||
note=note,
|
||||
tag=tag,
|
||||
)
|
||||
|
||||
@agent.agent_risk.tool
|
||||
async def record_customer_risk_event_risk(
|
||||
ctx: RunContext[Any],
|
||||
customer_id: str,
|
||||
event_type: str,
|
||||
event_count: int = 1,
|
||||
note: str = "",
|
||||
) -> str:
|
||||
return await record_customer_risk_event(
|
||||
ctx=ctx,
|
||||
customer_id=customer_id,
|
||||
event_type=event_type,
|
||||
event_count=event_count,
|
||||
note=note,
|
||||
)
|
||||
|
||||
@agent.agent.tool
|
||||
async def remove_background(ctx: RunContext[Any], image_url: str) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】去背景,输出白底图。客户只要去背景时调用。"""
|
||||
try:
|
||||
from image.image_tools import remove_background as _rb
|
||||
r = await _rb(image_url)
|
||||
if r["success"]:
|
||||
return f"去背景完成,已保存。自然回复客户好了发你"
|
||||
return f"去背景失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"去背景失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def perspective_correct(ctx: RunContext[Any], image_url: str) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】透视矫正。输入需白底图,输出展平图。"""
|
||||
try:
|
||||
from image.image_tools import perspective_correct as _pc
|
||||
r = await _pc(image_url)
|
||||
if r["success"]:
|
||||
return f"透视矫正完成。自然回复客户好了"
|
||||
return f"透视矫正失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"透视矫正失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def extract_pattern_tool(
|
||||
ctx: RunContext[Any],
|
||||
image_url: str,
|
||||
prompt: str = "",
|
||||
aspect_ratio: str = "1:1"
|
||||
) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】印花提取/主处理。按提示词和比例处理。"""
|
||||
try:
|
||||
from image.image_tools import extract_pattern
|
||||
r = await extract_pattern(image_url, prompt=prompt, aspect_ratio=aspect_ratio)
|
||||
if r["success"]:
|
||||
return f"提取完成。自然回复客户好了发你"
|
||||
return f"提取失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"提取失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def enhance_image_tool(ctx: RunContext[Any], image_url: str) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】高清增强。客户只要清晰化时调用。"""
|
||||
try:
|
||||
from image.image_tools import enhance_image
|
||||
r = await enhance_image(image_url)
|
||||
if r["success"]:
|
||||
return f"高清增强完成。自然回复客户好了"
|
||||
return f"增强失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"增强失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def color_match_tool(
|
||||
ctx: RunContext[Any],
|
||||
orig_url: str,
|
||||
result_url: str,
|
||||
strength: float = 0.75
|
||||
) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】颜色匹配。将 result 色调匹配到 orig。"""
|
||||
try:
|
||||
from image.image_tools import color_match_images
|
||||
r = await color_match_images(orig_url, result_url, strength=strength)
|
||||
if r["success"]:
|
||||
return f"颜色匹配完成"
|
||||
return f"颜色匹配失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"颜色匹配失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def trim_border_tool(ctx: RunContext[Any], image_url: str) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】裁切四周背景边(白/黄/米等)。"""
|
||||
try:
|
||||
from image.image_tools import trim_border
|
||||
r = await trim_border(image_url)
|
||||
if r["success"]:
|
||||
return f"裁边完成"
|
||||
return f"裁边失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"裁边失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def vectorize_to_eps_tool(ctx: RunContext[Any], image_url: str) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""【独立工具】矢量化 - 将图片转为 EPS 矢量文件。客户要做矢量图、转 EPS、转 AI 格式时调用。"""
|
||||
try:
|
||||
from image.image_tools import vectorize_to_eps
|
||||
r = await vectorize_to_eps(image_url)
|
||||
if r["success"]:
|
||||
return f"矢量化完成,已生成 EPS 文件。自然回复客户好了发你"
|
||||
return f"矢量化失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"矢量化失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def meitu_enhance_tool(
|
||||
ctx: RunContext[Any],
|
||||
image_url: str,
|
||||
mode: str = "standard"
|
||||
) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""
|
||||
【独立工具】美图画质增强。客户要画质增强、清晰化、美图处理时调用。
|
||||
|
||||
Args:
|
||||
image_url: 图片 URL 或本地路径
|
||||
mode: 处理模式。crystal(极速重绘) standard(标准) enhance(增强) hdr(HDR) portrait(人像优化)
|
||||
"""
|
||||
try:
|
||||
from image.image_tools import meitu_enhance
|
||||
r = await meitu_enhance(image_url, mode=mode)
|
||||
if r["success"]:
|
||||
return f"画质增强完成。自然回复客户好了发你"
|
||||
return f"画质增强失败:{r['message']}"
|
||||
except Exception as e:
|
||||
return f"画质增强失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def resize_image(
|
||||
ctx: RunContext[Any],
|
||||
image_url: str,
|
||||
width: int,
|
||||
height: int = 0
|
||||
) -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不处理图片"
|
||||
"""
|
||||
改图片尺寸。客户说「改成1920x1080」「弄成横图」「改下尺寸」时调用。
|
||||
|
||||
Args:
|
||||
image_url: 图片URL(客户刚发的图,或从对话中获取)
|
||||
width: 目标宽度(像素),如 1920
|
||||
height: 目标高度(0=按宽度等比缩放),如 1080
|
||||
|
||||
常用尺寸:1920x1080(横屏) 1080x1920(竖屏) 2000x2000(方图)
|
||||
"""
|
||||
try:
|
||||
from image.image_processor import image_processor
|
||||
result = await image_processor.resize(image_url, width, height)
|
||||
if result["success"]:
|
||||
return f"改尺寸完成:{width}x{height},已保存。自然回复客户改好了"
|
||||
else:
|
||||
return f"改尺寸失败:{result['message']},告知客户稍后重试"
|
||||
except Exception as e:
|
||||
return f"改尺寸失败:{e}"
|
||||
|
||||
@agent.agent.tool
|
||||
async def calculate_bulk_price(
|
||||
ctx: RunContext[Any],
|
||||
image_count: int,
|
||||
complexities: str = ""
|
||||
) -> str:
|
||||
"""
|
||||
计算多图打包价格。
|
||||
客户要做多张图时调用,返回建议总价。
|
||||
|
||||
Args:
|
||||
image_count: 图片数量
|
||||
complexities: 各图复杂度,逗号分隔,如 "normal,complex,simple"
|
||||
没有识别结果时留空,按平均价格估算
|
||||
"""
|
||||
if image_count <= 0:
|
||||
return "图片数量无效"
|
||||
|
||||
# 各复杂度单价(必须为5的整数倍)
|
||||
unit_price = {"simple": 15, "normal": 20, "complex": 25, "hard": 30}
|
||||
default_unit = 20 # 没有识别结果时的默认单价
|
||||
|
||||
if complexities:
|
||||
levels = [c.strip() for c in complexities.split(",")]
|
||||
total = sum(unit_price.get(lv, default_unit) for lv in levels)
|
||||
else:
|
||||
total = image_count * default_unit
|
||||
|
||||
# 打包优惠:3张以上9折,5张以上8折,价格必须为5的整数倍
|
||||
if image_count >= 5:
|
||||
discounted = round(total * 0.8 / 5) * 5
|
||||
tip = f"({image_count}张8折优惠)"
|
||||
elif image_count >= 3:
|
||||
discounted = round(total * 0.9 / 5) * 5
|
||||
tip = f"({image_count}张9折优惠)"
|
||||
else:
|
||||
discounted = round(total / 5) * 5
|
||||
tip = ""
|
||||
|
||||
return f"建议打包报价:{discounted}元{tip}(原价{total}元)"
|
||||
def register_agent_tools(agent: Any):
|
||||
"""注册工具"""
|
||||
agent.tool(transfer_to_human_tool)
|
||||
agent.tool(check_order_status_tool)
|
||||
logger.info("[Agent] 工具箱已更新:称呼统一为“设计师”。")
|
||||
|
||||
63
core/engine.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import logging
|
||||
from typing import Optional, Any
|
||||
from core.schema import StandardMessage, StandardResponse
|
||||
from core.events.event_bus import bus
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class BusinessEngine:
|
||||
"""
|
||||
业务逻辑中枢:
|
||||
1. 接收 StandardMessage。
|
||||
2. 决定由哪个 AI 工具或流程处理。
|
||||
3. 返回 StandardResponse。
|
||||
4. 对外广播异步事件。
|
||||
"""
|
||||
def __init__(self, agent_instance: Any = None):
|
||||
"""
|
||||
:param agent_instance: 核心 AI Agent 的实例(比如重构后的 CustomerServiceAgent)
|
||||
"""
|
||||
self.agent = agent_instance
|
||||
|
||||
async def handle_message(self, msg: StandardMessage) -> StandardResponse:
|
||||
"""
|
||||
大脑的思考主入口
|
||||
"""
|
||||
logger.info(f"[Engine] 收到来自 {msg.platform} 的消息: {msg.user_id} -> {msg.content[:50]}")
|
||||
|
||||
# TODO: 这里将接入重构后的 Single Agent + Tool Calling
|
||||
# 目前模拟一个简单的规则响应,展示 StandardResponse 的用法
|
||||
|
||||
if "报价" in msg.content or msg.image_urls:
|
||||
return StandardResponse(
|
||||
reply_content="正在为你查看图片,请稍等...",
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
)
|
||||
|
||||
if "转人工" in msg.content:
|
||||
return StandardResponse(
|
||||
reply_content="正在为你转接设计师...",
|
||||
need_transfer=True,
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
)
|
||||
|
||||
# 兜底回复
|
||||
return StandardResponse(
|
||||
reply_content="你好,我是AI助手,有什么可以帮你的?",
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
)
|
||||
|
||||
async def emit_image_result(self, user_id: str, platform: str, url: str, acc_id: str):
|
||||
"""
|
||||
这是一个业务触发器示例:当图片处理完成时,由 Engine 主动发广播。
|
||||
"""
|
||||
await bus.emit(
|
||||
"MESSAGE_OUTBOUND",
|
||||
user_id=user_id,
|
||||
platform=platform,
|
||||
response=StandardResponse(
|
||||
reply_content=url,
|
||||
msg_type=1, # 图片
|
||||
metadata={"acc_id": acc_id}
|
||||
)
|
||||
)
|
||||
36
core/events/event_bus.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Callable, Dict, List, Any, Awaitable
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class AsyncEventBus:
|
||||
"""
|
||||
异步事件总线:解耦业务触发与平台发送。
|
||||
支持一个事件被多个订阅者监听。
|
||||
"""
|
||||
def __init__(self):
|
||||
self._listeners: Dict[str, List[Callable[..., Awaitable[None]]]] = {}
|
||||
|
||||
def subscribe(self, event_type: str, callback: Callable[..., Awaitable[None]]):
|
||||
"""订阅事件"""
|
||||
if event_type not in self._listeners:
|
||||
self._listeners[event_type] = []
|
||||
self._listeners[event_type].append(callback)
|
||||
logger.info(f"[EventBus] 新订阅者已注册到事件: {event_type}")
|
||||
|
||||
async def emit(self, event_type: str, **kwargs):
|
||||
"""发布事件:异步广播给所有订阅者"""
|
||||
if event_type not in self._listeners:
|
||||
return
|
||||
|
||||
tasks = []
|
||||
for callback in self._listeners[event_type]:
|
||||
tasks.append(asyncio.create_task(callback(**kwargs)))
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info(f"[EventBus] 事件 {event_type} 已成功广播给 {len(tasks)} 个订阅者")
|
||||
|
||||
# 全局单例,所有模块共用这一个广播台
|
||||
bus = AsyncEventBus()
|
||||
156
core/orchestrator.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import re
|
||||
import time
|
||||
from typing import Optional, List, Any, Dict
|
||||
from collections import deque
|
||||
from core.schema import StandardMessage, StandardResponse
|
||||
from core.adapters.qianniu_adapter import QianniuAdapter
|
||||
from core.pydantic_ai_agent_v2 import CustomerServiceBrain
|
||||
from core.events.event_bus import bus
|
||||
from core.repository import repo
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class SystemOrchestrator:
|
||||
"""
|
||||
全系统总编排:具备转接冷却、防抖合并、多消息去重、以及精准日志。
|
||||
"""
|
||||
def __init__(self, ws_client=None):
|
||||
self.ws_client = ws_client
|
||||
self.qianniu_adapter = QianniuAdapter(ws_client)
|
||||
self.brain = CustomerServiceBrain()
|
||||
|
||||
# 1. 消息 ID 去重
|
||||
self._processed_msg_ids = deque(maxlen=200)
|
||||
|
||||
# 2. 转接冷却存储 (customer_id -> last_transfer_time)
|
||||
self._last_transfer_time: Dict[str, float] = {}
|
||||
|
||||
# 3. 防抖配置
|
||||
self._debounce_seconds = 5.0
|
||||
self._debounce_tasks: Dict[str, asyncio.Task] = {}
|
||||
self._pending_messages: Dict[str, List[StandardMessage]] = {}
|
||||
self._user_locks: Dict[str, asyncio.Lock] = {}
|
||||
|
||||
bus.subscribe("MESSAGE_OUTBOUND", self.handle_outbound_event)
|
||||
|
||||
def _get_user_lock(self, user_id: str) -> asyncio.Lock:
|
||||
if user_id not in self._user_locks:
|
||||
self._user_locks[user_id] = asyncio.Lock()
|
||||
return self._user_locks[user_id]
|
||||
|
||||
async def on_raw_message_received(self, platform: str, raw_data: dict):
|
||||
"""链路入口"""
|
||||
try:
|
||||
if platform != "qianniu": return
|
||||
|
||||
std_msg, direction = await self.qianniu_adapter.translate_inbound(raw_data)
|
||||
|
||||
# 过滤心跳
|
||||
if not std_msg.content.strip() and not std_msg.image_urls: return
|
||||
|
||||
# 如果是商家人工回复,静默入库
|
||||
if direction == "out":
|
||||
await repo.save_chat(platform, std_msg.user_id, std_msg.content, "out", acc_id=std_msg.acc_id)
|
||||
return
|
||||
|
||||
# 订单消息处理:静默记录
|
||||
if "[系统订单信息]" in std_msg.content:
|
||||
await self._handle_order_packet(platform, std_msg)
|
||||
await repo.save_chat(platform, std_msg.user_id, std_msg.content, "in", acc_id=std_msg.acc_id)
|
||||
return
|
||||
|
||||
# ID 去重
|
||||
if std_msg.msg_id:
|
||||
if std_msg.msg_id in self._processed_msg_ids: return
|
||||
self._processed_msg_ids.append(std_msg.msg_id)
|
||||
|
||||
# 进入防抖
|
||||
user_id = std_msg.user_id
|
||||
if user_id in self._debounce_tasks: self._debounce_tasks[user_id].cancel()
|
||||
if user_id not in self._pending_messages: self._pending_messages[user_id] = []
|
||||
self._pending_messages[user_id].append(std_msg)
|
||||
|
||||
self._debounce_tasks[user_id] = asyncio.create_task(self._debounced_process(user_id, platform))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Orchestrator] 处理失败: {e}")
|
||||
|
||||
async def _handle_order_packet(self, platform: str, msg: StandardMessage):
|
||||
try:
|
||||
price_match = re.search(r"订单金额:金额:\s*([\d\.]+)元", msg.content)
|
||||
if price_match: await repo.update_task_price(platform, msg.user_id, float(price_match.group(1)))
|
||||
if "买家已付款" in msg.content: await repo.update_task_outcome(platform, msg.user_id, "deal_success")
|
||||
elif any(k in msg.content for k in ["退款", "已关闭", "已取消"]): await repo.update_task_outcome(platform, msg.user_id, "refunded")
|
||||
except Exception: pass
|
||||
|
||||
async def _debounced_process(self, user_id: str, platform: str):
|
||||
try:
|
||||
await asyncio.sleep(self._debounce_seconds)
|
||||
async with self._get_user_lock(user_id):
|
||||
messages = self._pending_messages.pop(user_id, [])
|
||||
if not messages: return
|
||||
|
||||
# A. 合并与元数据修复
|
||||
combined_content = "\n".join([m.content for m in messages if m.content.strip()])
|
||||
all_image_urls = []
|
||||
acc_id = messages[-1].acc_id
|
||||
acc_type = messages[-1].acc_type
|
||||
for m in messages:
|
||||
for url in m.image_urls:
|
||||
if url not in all_image_urls: all_image_urls.append(url)
|
||||
|
||||
# 防抖合并后的消息仍需有 msg_id,避免触发 StandardMessage 校验失败
|
||||
merged_msg_id = messages[-1].msg_id if messages[-1].msg_id else f"merged-{user_id}-{int(time.time() * 1000)}"
|
||||
final_msg = StandardMessage(
|
||||
platform=platform,
|
||||
msg_id=merged_msg_id,
|
||||
user_id=user_id,
|
||||
content=combined_content,
|
||||
image_urls=all_image_urls,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type
|
||||
)
|
||||
|
||||
# B. 持久化
|
||||
db_content = combined_content
|
||||
if all_image_urls: db_content = f"【系统:已收到{len(all_image_urls)}张图】\n{combined_content}"
|
||||
await repo.save_chat(platform, user_id, db_content, "in", acc_id=acc_id)
|
||||
|
||||
# C. 冷却检查:如果 60秒内发过转接,告诉大脑“已处于转接中”
|
||||
is_in_cooldown = (time.time() - self._last_transfer_time.get(user_id, 0)) < 60
|
||||
|
||||
# D. 思考
|
||||
history = await repo.get_chat_history(user_id, limit=10)
|
||||
if history and history[-1]['content'] == db_content: history = history[:-1]
|
||||
|
||||
# 如果在冷却中,在当前消息里注入“当前已在转接中”的信息
|
||||
if is_in_cooldown:
|
||||
final_msg.content = f"【系统:当前已向设计师发出转接请求,请勿再次调用转接工具】\n{final_msg.content}"
|
||||
|
||||
std_res = await self.brain.think_and_reply(final_msg, history=history)
|
||||
|
||||
# E. 发送并记录时间
|
||||
if std_res.should_reply:
|
||||
# 关键修复:补全发送时的元数据,解决日志 customer_id 为空的问题
|
||||
std_res.metadata = {"acc_id": acc_id, "acc_type": acc_type}
|
||||
await self.qianniu_adapter.translate_outbound(std_res, user_id)
|
||||
await repo.save_chat(platform, user_id, std_res.reply_content, "out", acc_id=acc_id)
|
||||
|
||||
if "[转移会话]" in std_res.reply_content:
|
||||
self._last_transfer_time[user_id] = time.time()
|
||||
|
||||
except asyncio.CancelledError: pass
|
||||
except Exception as e: logger.exception(f"[Orchestrator] 处理失败: {e}")
|
||||
|
||||
async def handle_outbound_event(self, user_id: str, platform: str, response: StandardResponse):
|
||||
if platform == "qianniu":
|
||||
await self.qianniu_adapter.translate_outbound(response, user_id)
|
||||
|
||||
# 全局单例
|
||||
orchestrator: Optional[SystemOrchestrator] = None
|
||||
def init_orchestrator(ws_client):
|
||||
global orchestrator
|
||||
orchestrator = SystemOrchestrator(ws_client)
|
||||
return orchestrator
|
||||
91
core/pydantic_ai_agent_v2.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Optional, Any, Dict
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic_ai.models.openai import OpenAIChatModel
|
||||
from pydantic_ai.providers.openai import OpenAIProvider
|
||||
from core.schema import StandardMessage, StandardResponse
|
||||
from core.agent_tools import register_agent_tools
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
from core.skill_manager import skill_manager
|
||||
|
||||
class CustomerServiceBrain:
|
||||
"""
|
||||
重构后的单一 Agent 大脑:
|
||||
【全能终极版】统一称呼为“设计师”,支持下线安抚。
|
||||
"""
|
||||
|
||||
def __init__(self, model_name: str = None):
|
||||
self.api_key = os.getenv("OPENAI_API_KEY")
|
||||
self.base_url = os.getenv("OPENAI_BASE_URL")
|
||||
self.model_name = model_name or os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
||||
|
||||
model = OpenAIChatModel(
|
||||
model_name=self.model_name,
|
||||
provider=OpenAIProvider(api_key=self.api_key, base_url=self.base_url)
|
||||
)
|
||||
|
||||
all_skills = skill_manager.get_all_skills_text()
|
||||
|
||||
# --- 统一口径后的 System Prompt ---
|
||||
system_prompt = (
|
||||
"你是一位专注【高清修复】和【找原图】的专业店主。性格干脆,说话高端、专业。\n\n"
|
||||
|
||||
"【统一称呼规范】\n"
|
||||
"1. 严禁使用'师傅'、'客服'、'专员'等词汇!\n"
|
||||
"2. 必须统一称呼为【设计师】。比如:'找设计师看下'、'设计师马上来'、'等设计师核价'。\n\n"
|
||||
|
||||
"【核心逻辑】\n"
|
||||
"1. 业务:只聊高清修复和找原图。引导发图 -> 问需求 -> 找设计师。\n"
|
||||
"2. 下线安抚:如果工具返回 'ERROR_NO_DESIGNER_ONLINE',说明设计师们【下班/下线】了。回:'亲亲,设计师现在下班啦,需求我先记下,明天第一时间回您哈!'。\n"
|
||||
"3. 正在转接中:如果看到系统提示已在转接,回:'设计师正在赶来,我再帮你催下哈!'。\n"
|
||||
"4. 没转接时:引导发图 -> 问需求 -> 调工具转人工。\n\n"
|
||||
"5. 语气:淘宝亲切风,多用'亲亲'、'铁子'。每句回复【严禁超过15字】!\n\n"
|
||||
|
||||
"【必杀令】\n"
|
||||
"1. 每句回复严禁超过15个字!\n"
|
||||
"2. 严禁报价,严禁复读图片已收到的情况。\n"
|
||||
"3. 必须原样输出工具返回的'正在为您转接|'指令。\n\n"
|
||||
|
||||
f"业务参考:\n{all_skills}"
|
||||
)
|
||||
|
||||
self.agent = Agent(model=model, system_prompt=system_prompt)
|
||||
register_agent_tools(self.agent)
|
||||
|
||||
async def think_and_reply(self, msg: StandardMessage, history: List[dict] = []) -> StandardResponse:
|
||||
try:
|
||||
# 构造增强上下文(强灌输)
|
||||
user_content = msg.content
|
||||
if msg.image_urls:
|
||||
user_content = f"【系统通知:收到客户 {len(msg.image_urls)} 张图】\n{user_content}"
|
||||
|
||||
recent_context = ""
|
||||
if history:
|
||||
lines = [f"{('客户' if h['role']=='user' else '我')}:{h['content']}" for h in history[-6:]]
|
||||
recent_context = "【近期对话回顾】\n" + "\n".join(lines) + "\n----------------\n"
|
||||
|
||||
full_input = f"{recent_context}现在的对话:{user_content}"
|
||||
|
||||
result = await self.agent.run(full_input, message_history=history)
|
||||
|
||||
if hasattr(result, 'data') and isinstance(result.data, str):
|
||||
reply_text = result.data
|
||||
elif hasattr(result, 'output') and isinstance(result.output, str):
|
||||
reply_text = result.output
|
||||
else:
|
||||
reply_text = str(result.data) if hasattr(result, 'data') else "在呢铁子。"
|
||||
|
||||
need_transfer = "[转移会话]" in reply_text
|
||||
|
||||
return StandardResponse(
|
||||
reply_content=reply_text,
|
||||
need_transfer=need_transfer,
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Brain Error]: {e}")
|
||||
return StandardResponse(reply_content="好哒,设计师正在看图,稍等回你。", metadata={"acc_id": msg.acc_id})
|
||||
69
core/repository.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Optional, List, Any
|
||||
from datetime import datetime
|
||||
from db.customer_db import db as customer_db
|
||||
from db.image_tasks_db import db as task_db
|
||||
from db.chat_log_db import log_message, get_conversation
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class DataRepository:
|
||||
"""
|
||||
异步数据仓库:使用 asyncio.to_thread 屏蔽底层同步 IO 阻塞。
|
||||
"""
|
||||
def __init__(self):
|
||||
self.customer_db = customer_db
|
||||
self.task_db = task_db
|
||||
|
||||
# --- 聊天记录 (异步化) ---
|
||||
|
||||
async def save_chat(self, platform: str, user_id: str, content: str, direction: str, acc_id: str = ""):
|
||||
"""异步持久化存储聊天记录"""
|
||||
return await asyncio.to_thread(
|
||||
log_message,
|
||||
customer_id=user_id,
|
||||
message=content,
|
||||
direction=direction,
|
||||
platform=platform,
|
||||
acc_id=acc_id
|
||||
)
|
||||
|
||||
async def get_chat_history(self, user_id: str, limit: int = 10) -> List[dict]:
|
||||
"""异步获取历史记录"""
|
||||
rows = await asyncio.to_thread(get_conversation, user_id, limit=limit)
|
||||
history = []
|
||||
for r in rows:
|
||||
role = "user" if r["direction"] == "in" else "assistant"
|
||||
history.append({"role": role, "content": r["message"]})
|
||||
return history
|
||||
|
||||
# --- 客户相关 (异步化) ---
|
||||
|
||||
async def get_customer(self, platform: str, user_id: str):
|
||||
customer_key = f"{platform}:{user_id}"
|
||||
return await asyncio.to_thread(self.customer_db.get_customer, customer_key)
|
||||
|
||||
# --- 任务相关 (异步化) ---
|
||||
|
||||
async def create_task(self, platform: str, user_id: str, image_url: str, operation: str, requirements: str = ""):
|
||||
return await asyncio.to_thread(
|
||||
self.task_db.add_task,
|
||||
customer_id=user_id,
|
||||
platform=platform,
|
||||
original_image=image_url,
|
||||
operation=operation,
|
||||
requirements=requirements,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
async def update_task_price(self, platform: str, user_id: str, price: float):
|
||||
"""异步记录成交价"""
|
||||
return await asyncio.to_thread(self.task_db.update_price, user_id, platform, price)
|
||||
|
||||
async def update_task_outcome(self, platform: str, user_id: str, outcome: str):
|
||||
"""异步记录最终结局"""
|
||||
return await asyncio.to_thread(self.task_db.update_outcome, user_id, platform, outcome)
|
||||
|
||||
# 全局异步仓库单例
|
||||
repo = DataRepository()
|
||||
29
core/schema.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
class StandardMessage(BaseModel):
|
||||
"""全平台通用的输入消息协议"""
|
||||
platform: str = "qianniu" # 来源平台:qianniu, wechat, feishu, console
|
||||
msg_id: str # 消息唯一ID
|
||||
user_id: str # 发送者唯一ID
|
||||
user_name: str = "" # 发送者昵称
|
||||
content: str # 消息文本内容
|
||||
image_urls: List[str] = [] # 提取出来的图片链接
|
||||
acc_id: str = "" # 商家/店铺账号ID
|
||||
acc_type: str = "" # 平台类型标识
|
||||
timestamp: datetime = Field(default_factory=datetime.now)
|
||||
raw_data: Any = None # 原始消息体(仅供调试或特殊逻辑备查)
|
||||
|
||||
# 扩展字段:针对电商场景
|
||||
goods_name: Optional[str] = None
|
||||
goods_order: Optional[str] = None
|
||||
|
||||
class StandardResponse(BaseModel):
|
||||
"""大脑给出的通用回复协议"""
|
||||
reply_content: str # 回复文本或图片URL
|
||||
msg_type: int = 0 # 0: 文本, 1: 图片, 2: 撤回, 9: 转人工
|
||||
should_reply: bool = True # 是否需要发送
|
||||
need_transfer: bool = False # 是否触发转人工
|
||||
transfer_group: str = "" # 转人工的分组ID
|
||||
metadata: dict = {} # 额外元数据(如埋点、调试信息)
|
||||
56
core/skill_manager.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class SkillManager:
|
||||
"""
|
||||
技能包管理器:
|
||||
1. 自动扫描 skills/ 目录下的 SKILL.md 文件。
|
||||
2. 提供按需加载和组合技能的能力。
|
||||
3. 支持热加载(无需重启即可更新 AI 知识)。
|
||||
"""
|
||||
def __init__(self, skills_dir: str = "skills"):
|
||||
self.skills_dir = Path(skills_dir)
|
||||
self._skill_cache: Dict[str, str] = {}
|
||||
self.reload_skills()
|
||||
|
||||
def reload_skills(self):
|
||||
"""扫描并加载所有技能文件"""
|
||||
new_cache = {}
|
||||
skill_files = glob.glob(str(self.skills_dir / "**/SKILL.md"), recursive=True)
|
||||
|
||||
for file_path in skill_files:
|
||||
try:
|
||||
path = Path(file_path)
|
||||
skill_name = path.parent.name.lower()
|
||||
content = path.read_text(encoding="utf-8")
|
||||
new_cache[skill_name] = content
|
||||
except Exception as e:
|
||||
logger.error(f"[SkillManager] 加载技能失败 {file_path}: {e}")
|
||||
|
||||
self._skill_cache = new_cache
|
||||
logger.info(f"[SkillManager] 成功加载 {len(self._skill_cache)} 个技能包: {list(self._skill_cache.keys())}")
|
||||
|
||||
def get_skill(self, name: str) -> str:
|
||||
"""获取单个技能内容"""
|
||||
return self._skill_cache.get(name.lower(), "")
|
||||
|
||||
def compose_skills(self, names: List[str]) -> str:
|
||||
"""组合多个技能内容,用于注入 System Prompt"""
|
||||
parts = []
|
||||
for name in names:
|
||||
content = self.get_skill(name)
|
||||
if content:
|
||||
parts.append(f"### 技能:{name}\n{content}")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
def get_all_skills_text(self) -> str:
|
||||
"""获取所有技能的合集(用于全能大脑模式)"""
|
||||
return self.compose_skills(list(self._skill_cache.keys()))
|
||||
|
||||
# 全局单例
|
||||
skill_manager = SkillManager()
|
||||
81
core/websocket_client_v2.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from core.orchestrator import init_orchestrator
|
||||
from core.websocket_connection_flow import connect_flow, receive_messages_flow
|
||||
from core.websocket_send_flow import send_message_flow
|
||||
from utils.observability import emit_activity
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
class QingjianAPIClient:
|
||||
"""
|
||||
重构后的轻简API客户端 (协议全复刻版)
|
||||
"""
|
||||
|
||||
def __init__(self, uri=None, enable_agent: bool = True):
|
||||
from config.config import QINGJIAN_WS_URI
|
||||
self.uri = uri or QINGJIAN_WS_URI
|
||||
self.websocket = None
|
||||
self.running = True
|
||||
self.logger = logger
|
||||
self.enable_agent = enable_agent
|
||||
|
||||
# 初始化新架构总指挥部
|
||||
self.orchestrator = init_orchestrator(ws_client=self)
|
||||
logger.info("[WebSocket] 新架构 Orchestrator 已就绪。")
|
||||
|
||||
def _activity_log(self, event: str, **kwargs):
|
||||
emit_activity(logger, event=event, **kwargs)
|
||||
|
||||
async def connect(self):
|
||||
await connect_flow(self)
|
||||
|
||||
async def receive_messages(self):
|
||||
await receive_messages_flow(self)
|
||||
|
||||
async def handle_message(self, message):
|
||||
"""收到消息处理"""
|
||||
try:
|
||||
data = json.loads(message)
|
||||
await self.orchestrator.on_raw_message_received(platform="qianniu", raw_data=data)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebSocket] 处理消息异常: {e}")
|
||||
|
||||
async def send(self, customer_id: str, acc_id: str, acc_type: str, content: str, msg_type: int = 0):
|
||||
"""
|
||||
【协议全复刻】严格按照 legacy/websocket_outbound_flow.py 的结构
|
||||
"""
|
||||
# 注意:在这里 from_id 竟然填的是 customer_id,这是逆向接口的特殊要求
|
||||
msg_payload = {
|
||||
"msg_id": "",
|
||||
"acc_id": acc_id,
|
||||
"msg": content,
|
||||
"from_id": customer_id,
|
||||
"from_name": "",
|
||||
"cy_id": customer_id,
|
||||
"acc_type": acc_type,
|
||||
"msg_type": msg_type,
|
||||
"cy_name": "",
|
||||
}
|
||||
await self.send_message(msg_payload)
|
||||
|
||||
async def send_message(self, message_dict: dict):
|
||||
"""底层的 WebSocket 发送"""
|
||||
await send_message_flow(self, message_dict)
|
||||
|
||||
def get_time(self):
|
||||
return datetime.now().strftime("%H:%M:%S")
|
||||
|
||||
async def run(self):
|
||||
await self.connect()
|
||||
await self.receive_messages()
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = QingjianAPIClient()
|
||||
try:
|
||||
asyncio.run(client.run())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("已停止")
|
||||
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import websockets
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
async def connect_flow(client):
|
||||
"""连接 WebSocket 服务器并自动重连。"""
|
||||
@@ -9,49 +11,22 @@ async def connect_flow(client):
|
||||
client.logger.info(f"[{client.get_time()}] 正在连接轻简API {client.uri}...")
|
||||
async with websockets.connect(client.uri) as websocket:
|
||||
client.websocket = websocket
|
||||
from utils.health_check import set_qingjian_connected
|
||||
set_qingjian_connected(True)
|
||||
client.logger.info(f"[{client.get_time()}] 连接成功!")
|
||||
if client.enable_agent:
|
||||
client.logger.info(f"[{client.get_time()}] AI Agent 已启用,将自动处理消息")
|
||||
client.logger.info(f"[{client.get_time()}] 等待接收消息...")
|
||||
|
||||
await client.receive_messages()
|
||||
|
||||
except ConnectionRefusedError:
|
||||
from utils.health_check import set_qingjian_connected
|
||||
set_qingjian_connected(False)
|
||||
client.logger.info(f"[{client.get_time()}] 连接被拒绝,请检查轻简软件是否已启动")
|
||||
except websockets.exceptions.InvalidURI:
|
||||
from utils.health_check import set_qingjian_connected
|
||||
set_qingjian_connected(False)
|
||||
client.logger.info(f"[{client.get_time()}] URI格式错误")
|
||||
except Exception as e:
|
||||
from utils.health_check import set_qingjian_connected
|
||||
set_qingjian_connected(False)
|
||||
client.logger.info(f"[{client.get_time()}] 连接错误: {e}")
|
||||
# 统一捕获异常,避免因为不同版本的 websockets 导致属性错误
|
||||
client.logger.info(f"[{client.get_time()}] 连接或运行错误: {e}")
|
||||
|
||||
if client.running:
|
||||
client.logger.info(f"[{client.get_time()}] 5秒后尝试重连...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def receive_messages_flow(client):
|
||||
"""持续接收消息。"""
|
||||
try:
|
||||
async for message in client.websocket:
|
||||
await client.handle_message(message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
from utils.health_check import set_qingjian_connected
|
||||
set_qingjian_connected(False)
|
||||
client.logger.info(f"[{client.get_time()}] 连接已关闭")
|
||||
except Exception as e:
|
||||
from utils.health_check import set_qingjian_connected
|
||||
set_qingjian_connected(False)
|
||||
client.logger.info(f"[{client.get_time()}] 接收消息错误: {e}")
|
||||
|
||||
|
||||
async def handle_message_flow(client, message, *, shop_type_resolver):
|
||||
from core.websocket_inbound_flow import handle_incoming_message
|
||||
|
||||
await handle_incoming_message(client, message, shop_type_resolver=shop_type_resolver)
|
||||
client.logger.info(f"[{client.get_time()}] 接收消息中断: {e}")
|
||||
|
||||
@@ -42,13 +42,14 @@ async def send_message_flow(client, message):
|
||||
await client.websocket.send(msg_json)
|
||||
pretty = json.dumps(message, ensure_ascii=False, indent=2)
|
||||
client.logger.info(f"[{client.get_time()}] 发送成功:\n{pretty}")
|
||||
data = message.get("data", {}) if isinstance(message, dict) else {}
|
||||
client._activity_log(
|
||||
"send_message_success",
|
||||
trace_id=message.get("_trace_id", ""),
|
||||
acc_id=message.get("acc_id", ""),
|
||||
customer_id=message.get("from_id", ""),
|
||||
msg_type=message.get("msg_type", 0),
|
||||
msg=message.get("msg", ""),
|
||||
trace_id=message.get("_trace_id", "") if isinstance(message, dict) else "",
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("cy_id", ""),
|
||||
msg_type=data.get("msg_type", 0),
|
||||
msg=data.get("msg", ""),
|
||||
)
|
||||
except Exception as e:
|
||||
client.logger.info(f"[{client.get_time()}] 发送失败: {e}")
|
||||
|
||||
19
core/提示词.MD
Normal file
@@ -0,0 +1,19 @@
|
||||
"你是一位专注【高清修复】和【找原图】的专业店主。性格干脆,说话高端、专业。\n\n"
|
||||
|
||||
"【统一称呼规范】\n"
|
||||
"1. 严禁使用'师傅'、'客服'、'专员'等词汇!\n"
|
||||
"2. 必须统一称呼为【设计师】。比如:'找设计师看下'、'设计师马上来'、'等设计师核价'。\n\n"
|
||||
|
||||
"【核心逻辑】\n"
|
||||
"1. 业务:只聊高清修复和找原图。引导发图 -> 问需求 -> 找设计师。\n"
|
||||
"2. 下线安抚:如果工具返回 'ERROR_NO_DESIGNER_ONLINE',说明设计师们【下班/下线】了。回:'亲亲,设计师现在下班啦,需求我先记下,明天第一时间回您哈!'。\n"
|
||||
"3. 正在转接中:如果看到系统提示已在转接,回:'设计师正在赶来,我再帮你催下哈!'。\n\n"
|
||||
"4. 没转接时:引导发图 -> 问需求 -> 调工具转人工。\n\n
|
||||
"5. 语气:淘宝亲切风,多用'亲亲'、'铁子'。每句回复【严禁超过15字】!\n\n"
|
||||
|
||||
"【必杀令】\n"
|
||||
"1. 每句回复严禁超过15个字!\n"
|
||||
"2. 严禁报价,严禁复读图片已收到的情况。\n"
|
||||
"3. 必须原样输出工具返回的'正在为您转接|'指令。\n\n"
|
||||
|
||||
f"业务参考:\n{all_skills}"
|
||||
@@ -1,889 +1 @@
|
||||
{
|
||||
"new_customer_001": {
|
||||
"customer_id": "new_customer_001",
|
||||
"name": "新客户小王",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 0,
|
||||
"total_spent": 0,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 20,
|
||||
"last_price_time": "2026-02-28T15:04:15.181813",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "",
|
||||
"decision_speed": "",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.129715",
|
||||
"last_update": "2026-02-28T15:04:15.184378"
|
||||
},
|
||||
"fast_customer_002": {
|
||||
"customer_id": "fast_customer_002",
|
||||
"name": "爽快老客老李",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 8,
|
||||
"total_spent": 280,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 10,
|
||||
"last_price_time": "2026-02-28T15:06:10.872962",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 2,
|
||||
"lowest_price_accepted": 10,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "中",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 8,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.131384",
|
||||
"last_update": "2026-02-28T15:06:10.875534"
|
||||
},
|
||||
"bargainer_003": {
|
||||
"customer_id": "bargainer_003",
|
||||
"name": "砍价王小张",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 3,
|
||||
"total_spent": 45,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"砍价狂",
|
||||
"纠结"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 10,
|
||||
"last_price_time": "2026-02-28T15:05:45.067204",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 6,
|
||||
"lowest_price_accepted": 10,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "高",
|
||||
"decision_speed": "慢",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.132648",
|
||||
"last_update": "2026-02-28T15:05:45.071818"
|
||||
},
|
||||
"vip_customer_004": {
|
||||
"customer_id": "vip_customer_004",
|
||||
"name": "VIP客户陈总",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 15,
|
||||
"total_spent": 680,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": true,
|
||||
"vip_level": 2,
|
||||
"last_price": 20,
|
||||
"last_price_time": "2026-02-28T15:04:56.155844",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 18,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.134104",
|
||||
"last_update": "2026-02-28T15:04:56.158233"
|
||||
},
|
||||
"high_value_005": {
|
||||
"customer_id": "high_value_005",
|
||||
"name": "高价值客户刘老板",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 20,
|
||||
"total_spent": 1200,
|
||||
"avg_order_value": 60,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 20,
|
||||
"last_price_time": "2026-02-28T15:05:11.156030",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.135396",
|
||||
"last_update": "2026-02-28T15:05:11.160004"
|
||||
},
|
||||
"blacklist_006": {
|
||||
"customer_id": "blacklist_006",
|
||||
"name": "黑名单客户",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 0,
|
||||
"total_spent": 0.0,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 0,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "",
|
||||
"decision_speed": "",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": true,
|
||||
"blacklist_reason": "恶意投诉多次",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.136490",
|
||||
"last_update": "2026-02-28T15:05:27.155220"
|
||||
},
|
||||
"test_new_001": {
|
||||
"customer_id": "test_new_001",
|
||||
"name": "新客户小王",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 0,
|
||||
"total_spent": 0,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 0,
|
||||
"last_price_time": "2026-02-28T15:27:40.801329",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "",
|
||||
"decision_speed": "",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.719291",
|
||||
"last_update": "2026-02-28T15:29:05.719308"
|
||||
},
|
||||
"test_fast_002": {
|
||||
"customer_id": "test_fast_002",
|
||||
"name": "爽快老客老李",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 8,
|
||||
"total_spent": 280,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 25,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 8,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.720944",
|
||||
"last_update": "2026-02-28T15:29:05.720948"
|
||||
},
|
||||
"test_bargain_003": {
|
||||
"customer_id": "test_bargain_003",
|
||||
"name": "砍价王小张",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 3,
|
||||
"total_spent": 45,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"砍价狂",
|
||||
"纠结"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 15,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 4,
|
||||
"lowest_price_accepted": 15,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "高",
|
||||
"decision_speed": "慢",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.722448",
|
||||
"last_update": "2026-02-28T15:29:05.722454"
|
||||
},
|
||||
"test_vip_004": {
|
||||
"customer_id": "test_vip_004",
|
||||
"name": "VIP 客户陈总",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 15,
|
||||
"total_spent": 680,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": true,
|
||||
"vip_level": 2,
|
||||
"last_price": 0,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 18,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.723887",
|
||||
"last_update": "2026-02-28T15:29:05.723890"
|
||||
},
|
||||
"test_highvalue_005": {
|
||||
"customer_id": "test_highvalue_005",
|
||||
"name": "高价值客户刘老板",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 20,
|
||||
"total_spent": 1200,
|
||||
"avg_order_value": 60,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 0,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.725313",
|
||||
"last_update": "2026-02-28T15:29:05.725316"
|
||||
}
|
||||
}
|
||||
{}
|
||||
@@ -199,14 +199,18 @@ def get_customers(limit: int = 100) -> List[Dict]:
|
||||
|
||||
|
||||
def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]:
|
||||
"""返回某客户的全部对话记录(按时间升序)"""
|
||||
"""返回某客户的最近对话记录(按时间升序)"""
|
||||
with _get_conn() as conn:
|
||||
# 核心修复:先取最新的 limit 条,再按时间正序排列
|
||||
rows = conn.execute(_sql("""
|
||||
SELECT * FROM (
|
||||
SELECT id, direction, message, msg_type, timestamp, acc_id
|
||||
FROM chat_logs
|
||||
WHERE customer_id = ?
|
||||
ORDER BY timestamp ASC, id ASC
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT ?
|
||||
) AS recent
|
||||
ORDER BY timestamp ASC, id ASC
|
||||
"""), (customer_id, limit)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
BIN
db/image_tasks.db
Normal file
@@ -1,480 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
图片任务数据库管理
|
||||
支持客户后续增加需求细节
|
||||
"""
|
||||
import sqlite3
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import os
|
||||
from typing import Optional, List, Dict
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
|
||||
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
|
||||
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
|
||||
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
|
||||
|
||||
def _is_mysql() -> bool:
|
||||
return _DB_TYPE in ("mysql", "mariadb")
|
||||
|
||||
def _sql(query: str) -> str:
|
||||
return query.replace("?", "%s") if _is_mysql() else query
|
||||
|
||||
def _now_str() -> str:
|
||||
if _is_mysql():
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
return datetime.now().isoformat()
|
||||
|
||||
class TaskStatus(Enum):
|
||||
"""任务状态"""
|
||||
PENDING = "pending" # 待付款
|
||||
PAID = "paid" # 已付款,待处理
|
||||
PROCESSING = "processing" # 处理中
|
||||
AWAITING_CONFIRM = "awaiting_confirm" # 已完成,待客户确认
|
||||
COMPLETED = "completed" # 已完成
|
||||
FAILED = "failed" # 失败
|
||||
CANCELLED = "cancelled" # 已取消
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
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):
|
||||
"""初始化数据库"""
|
||||
if _is_mysql():
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS image_tasks (
|
||||
task_id VARCHAR(128) PRIMARY KEY,
|
||||
customer_id VARCHAR(128) NOT NULL,
|
||||
customer_name VARCHAR(255),
|
||||
original_image TEXT NOT NULL,
|
||||
operation VARCHAR(64) DEFAULT 'enhance',
|
||||
requirements TEXT,
|
||||
customer_notes TEXT,
|
||||
status VARCHAR(32) DEFAULT 'pending',
|
||||
created_at DATETIME,
|
||||
paid_at DATETIME,
|
||||
started_at DATETIME,
|
||||
completed_at DATETIME,
|
||||
result_image TEXT,
|
||||
error_message TEXT,
|
||||
retry_count INT DEFAULT 0,
|
||||
acc_id VARCHAR(128),
|
||||
acc_type VARCHAR(64) DEFAULT 'AliWorkbench'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
task_id VARCHAR(128) NOT NULL,
|
||||
change_type VARCHAR(64),
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_at DATETIME,
|
||||
changed_by VARCHAR(32),
|
||||
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
''')
|
||||
cursor.execute("SHOW INDEX FROM image_tasks")
|
||||
exists = {str(r.get("Key_name", "")) for r in cursor.fetchall()}
|
||||
if "idx_customer" not in exists:
|
||||
cursor.execute('CREATE INDEX idx_customer ON image_tasks(customer_id)')
|
||||
if "idx_status" not in exists:
|
||||
cursor.execute('CREATE INDEX idx_status ON image_tasks(status)')
|
||||
if "idx_created" not in exists:
|
||||
cursor.execute('CREATE INDEX idx_created ON image_tasks(created_at)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
else:
|
||||
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,
|
||||
platform TEXT DEFAULT 'qianniu',
|
||||
original_image TEXT NOT NULL,
|
||||
operation TEXT DEFAULT 'enhance',
|
||||
requirements TEXT,
|
||||
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'
|
||||
price REAL DEFAULT 0.0,
|
||||
outcome TEXT DEFAULT 'pending',
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
change_type TEXT,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_at TEXT,
|
||||
changed_by TEXT,
|
||||
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)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cust_plat ON image_tasks(customer_id, platform)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info("数据库表初始化完成")
|
||||
|
||||
def _get_conn(self):
|
||||
"""获取数据库连接"""
|
||||
if _is_mysql():
|
||||
import pymysql
|
||||
return pymysql.connect(
|
||||
host=_MYSQL_HOST,
|
||||
port=_MYSQL_PORT,
|
||||
user=_MYSQL_USER,
|
||||
password=_MYSQL_PASSWORD,
|
||||
database=_MYSQL_DATABASE,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
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:
|
||||
"""创建图片任务"""
|
||||
def add_task(self, customer_id: str, platform: str, original_image: str, operation: str, requirements: str = "", status: str = "pending") -> str:
|
||||
task_id = str(uuid.uuid4())
|
||||
now = datetime.now().isoformat()
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
requirements_json = json.dumps(requirements) if requirements else None
|
||||
|
||||
cursor.execute(_sql('''
|
||||
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,
|
||||
_now_str(),
|
||||
acc_id,
|
||||
acc_type
|
||||
))
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO image_tasks (task_id, customer_id, platform, original_image, operation, requirements, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (task_id, customer_id, platform, original_image, operation, requirements, status, now, now))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"图片任务创建成功:{task_id}")
|
||||
return True
|
||||
|
||||
return task_id
|
||||
except Exception as e:
|
||||
logger.error(f"创建图片任务失败:{e}")
|
||||
return False
|
||||
logger.error(f"Failed to add task: {e}")
|
||||
return ""
|
||||
|
||||
def get_task(self, task_id: str) -> Optional[dict]:
|
||||
"""查询任务"""
|
||||
def update_status(self, task_id: str, status: str, result_image: str = ""):
|
||||
now = datetime.now().isoformat()
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(_sql('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(_sql('''
|
||||
SELECT * FROM image_tasks
|
||||
WHERE customer_id = ? AND status = ?
|
||||
ORDER BY created_at DESC
|
||||
'''), (customer_id, status))
|
||||
if result_image:
|
||||
cursor.execute('UPDATE image_tasks SET status = ?, result_image = ?, updated_at = ? WHERE task_id = ?',
|
||||
(status, result_image, now, task_id))
|
||||
else:
|
||||
cursor.execute(_sql('''
|
||||
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()
|
||||
|
||||
placeholder = "%s" if _is_mysql() else "?"
|
||||
updates = [f'status = {placeholder}']
|
||||
params = [status.value]
|
||||
|
||||
# 根据状态设置时间
|
||||
if status == TaskStatus.PAID:
|
||||
updates.append(f'paid_at = {placeholder}')
|
||||
params.append(_now_str())
|
||||
elif status == TaskStatus.PROCESSING:
|
||||
updates.append(f'started_at = {placeholder}')
|
||||
params.append(_now_str())
|
||||
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
|
||||
updates.append(f'completed_at = {placeholder}')
|
||||
params.append(_now_str())
|
||||
|
||||
params.append(task_id)
|
||||
|
||||
cursor.execute(_sql(f'''
|
||||
UPDATE image_tasks
|
||||
SET {', '.join(updates)}
|
||||
WHERE task_id = ?
|
||||
'''), params)
|
||||
|
||||
cursor.execute('UPDATE image_tasks SET status = ?, updated_at = ? WHERE task_id = ?',
|
||||
(status, now, task_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"任务状态更新:{task_id} -> {status.value}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新任务状态失败:{e}")
|
||||
logger.error(f"Failed to update task status: {e}")
|
||||
|
||||
def update_result(self, task_id: str, result_image: str, error_message: str = None):
|
||||
"""更新处理结果"""
|
||||
def update_outcome(self, customer_id: str, platform: str, outcome: str):
|
||||
"""记录任务的最终结局(用于训练样本分类)"""
|
||||
now = datetime.now().isoformat()
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(_sql('''
|
||||
cursor.execute('''
|
||||
UPDATE image_tasks
|
||||
SET result_image = ?, error_message = ?
|
||||
WHERE task_id = ?
|
||||
'''), (result_image, error_message, task_id))
|
||||
|
||||
SET outcome = ?, updated_at = ?
|
||||
WHERE task_id = (
|
||||
SELECT task_id FROM image_tasks
|
||||
WHERE customer_id = ? AND platform = ?
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
)
|
||||
''', (outcome, now, customer_id, platform))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"任务结果更新:{task_id}")
|
||||
|
||||
logger.info(f"[DB] 客户 {customer_id} 任务结局更新为: {outcome}")
|
||||
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(_sql('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(_sql('''
|
||||
UPDATE image_tasks
|
||||
SET customer_notes = ?
|
||||
WHERE task_id = ?
|
||||
'''), (new_note, task_id))
|
||||
|
||||
# 记录变更历史
|
||||
cursor.execute(_sql('''
|
||||
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,
|
||||
_now_str(),
|
||||
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(_sql('SELECT operation FROM image_tasks WHERE task_id = ?'), (task_id,))
|
||||
row = cursor.fetchone()
|
||||
old_operation = row['operation'] if row else ''
|
||||
|
||||
# 更新操作
|
||||
cursor.execute(_sql('''
|
||||
UPDATE image_tasks
|
||||
SET operation = ?
|
||||
WHERE task_id = ?
|
||||
'''), (new_operation, task_id))
|
||||
|
||||
# 记录变更历史
|
||||
cursor.execute(_sql('''
|
||||
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,
|
||||
_now_str(),
|
||||
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(_sql('''
|
||||
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(_sql('''
|
||||
UPDATE image_tasks
|
||||
SET retry_count = retry_count + 1
|
||||
WHERE task_id = ?
|
||||
'''), (task_id,))
|
||||
|
||||
cursor.execute(_sql('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
|
||||
logger.error(f"Failed to update outcome: {e}")
|
||||
|
||||
# 单例
|
||||
_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
|
||||
db = ImageTaskManager()
|
||||
|
||||
0
chat_log_db/chats.db → legacy/chat_log_db/chats.db
Executable file → Normal file
889
legacy/customer_db/customers.json
Normal file
@@ -0,0 +1,889 @@
|
||||
{
|
||||
"new_customer_001": {
|
||||
"customer_id": "new_customer_001",
|
||||
"name": "新客户小王",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 0,
|
||||
"total_spent": 0,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 20,
|
||||
"last_price_time": "2026-02-28T15:04:15.181813",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "",
|
||||
"decision_speed": "",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.129715",
|
||||
"last_update": "2026-02-28T15:04:15.184378"
|
||||
},
|
||||
"fast_customer_002": {
|
||||
"customer_id": "fast_customer_002",
|
||||
"name": "爽快老客老李",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 8,
|
||||
"total_spent": 280,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 10,
|
||||
"last_price_time": "2026-02-28T15:06:10.872962",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 2,
|
||||
"lowest_price_accepted": 10,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "中",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 8,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.131384",
|
||||
"last_update": "2026-02-28T15:06:10.875534"
|
||||
},
|
||||
"bargainer_003": {
|
||||
"customer_id": "bargainer_003",
|
||||
"name": "砍价王小张",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 3,
|
||||
"total_spent": 45,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"砍价狂",
|
||||
"纠结"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 10,
|
||||
"last_price_time": "2026-02-28T15:05:45.067204",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 6,
|
||||
"lowest_price_accepted": 10,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "高",
|
||||
"decision_speed": "慢",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.132648",
|
||||
"last_update": "2026-02-28T15:05:45.071818"
|
||||
},
|
||||
"vip_customer_004": {
|
||||
"customer_id": "vip_customer_004",
|
||||
"name": "VIP客户陈总",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 15,
|
||||
"total_spent": 680,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": true,
|
||||
"vip_level": 2,
|
||||
"last_price": 20,
|
||||
"last_price_time": "2026-02-28T15:04:56.155844",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 18,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.134104",
|
||||
"last_update": "2026-02-28T15:04:56.158233"
|
||||
},
|
||||
"high_value_005": {
|
||||
"customer_id": "high_value_005",
|
||||
"name": "高价值客户刘老板",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 20,
|
||||
"total_spent": 1200,
|
||||
"avg_order_value": 60,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 20,
|
||||
"last_price_time": "2026-02-28T15:05:11.156030",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.135396",
|
||||
"last_update": "2026-02-28T15:05:11.160004"
|
||||
},
|
||||
"blacklist_006": {
|
||||
"customer_id": "blacklist_006",
|
||||
"name": "黑名单客户",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 0,
|
||||
"total_spent": 0.0,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 0,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "",
|
||||
"decision_speed": "",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": true,
|
||||
"blacklist_reason": "恶意投诉多次",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:03:57.136490",
|
||||
"last_update": "2026-02-28T15:05:27.155220"
|
||||
},
|
||||
"test_new_001": {
|
||||
"customer_id": "test_new_001",
|
||||
"name": "新客户小王",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 0,
|
||||
"total_spent": 0,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 0,
|
||||
"last_price_time": "2026-02-28T15:27:40.801329",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "jpg",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "",
|
||||
"decision_speed": "",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "低",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.719291",
|
||||
"last_update": "2026-02-28T15:29:05.719308"
|
||||
},
|
||||
"test_fast_002": {
|
||||
"customer_id": "test_fast_002",
|
||||
"name": "爽快老客老李",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 8,
|
||||
"total_spent": 280,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 25,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 8,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.720944",
|
||||
"last_update": "2026-02-28T15:29:05.720948"
|
||||
},
|
||||
"test_bargain_003": {
|
||||
"customer_id": "test_bargain_003",
|
||||
"name": "砍价王小张",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 3,
|
||||
"total_spent": 45,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"砍价狂",
|
||||
"纠结"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "C",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 15,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 4,
|
||||
"lowest_price_accepted": 15,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "高",
|
||||
"decision_speed": "慢",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.722448",
|
||||
"last_update": "2026-02-28T15:29:05.722454"
|
||||
},
|
||||
"test_vip_004": {
|
||||
"customer_id": "test_vip_004",
|
||||
"name": "VIP 客户陈总",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 15,
|
||||
"total_spent": 680,
|
||||
"avg_order_value": 0.0,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": true,
|
||||
"vip_level": 2,
|
||||
"last_price": 0,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 18,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.723887",
|
||||
"last_update": "2026-02-28T15:29:05.723890"
|
||||
},
|
||||
"test_highvalue_005": {
|
||||
"customer_id": "test_highvalue_005",
|
||||
"name": "高价值客户刘老板",
|
||||
"nickname": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"address": "",
|
||||
"platform": "",
|
||||
"platform_id": "",
|
||||
"budget": "",
|
||||
"budget_range_min": 0,
|
||||
"budget_range_max": 0,
|
||||
"requirements": [],
|
||||
"preference_services": [],
|
||||
"total_orders": 20,
|
||||
"total_spent": 1200,
|
||||
"avg_order_value": 60,
|
||||
"purchase_frequency": "",
|
||||
"last_order_date": "",
|
||||
"first_order_date": "",
|
||||
"order_ids": [],
|
||||
"pending_orders": 0,
|
||||
"completed_orders": 0,
|
||||
"refund_count": 0,
|
||||
"personality": [
|
||||
"爽快"
|
||||
],
|
||||
"communication_prefer": "",
|
||||
"response_speed": "",
|
||||
"patience_level": "",
|
||||
"customer_level": "A",
|
||||
"vip": false,
|
||||
"vip_level": 0,
|
||||
"last_price": 0,
|
||||
"last_price_time": "",
|
||||
"last_quote_no_convert": false,
|
||||
"last_min_price": 0,
|
||||
"last_image_url": "",
|
||||
"last_image_time": "",
|
||||
"last_gemini_prompt": "",
|
||||
"last_aspect_ratio": "1:1",
|
||||
"last_perspective": "no",
|
||||
"processing_status": "",
|
||||
"processing_image_url": "",
|
||||
"expected_done_at": "",
|
||||
"discount_given_count": 0,
|
||||
"lowest_price_accepted": 0,
|
||||
"preferred_format": "",
|
||||
"preferred_size": "",
|
||||
"last_conversation_summary": "",
|
||||
"last_conversation_time": "",
|
||||
"total_images_sent": 0,
|
||||
"complexity_history": [],
|
||||
"image_type_history": [],
|
||||
"price_sensitivity": "低",
|
||||
"decision_speed": "快",
|
||||
"revision_count": 0,
|
||||
"revision_orders": 0,
|
||||
"total_completed_orders": 0,
|
||||
"bulk_potential": "",
|
||||
"churn_risk": "",
|
||||
"upsell_opportunity": [],
|
||||
"blacklist": false,
|
||||
"blacklist_reason": "",
|
||||
"vip_custom_price": 0,
|
||||
"last_email_status": "",
|
||||
"good_reviews": 0,
|
||||
"bad_reviews": 0,
|
||||
"dispute_count": 0,
|
||||
"follow_up_by": "",
|
||||
"follow_up_date": "",
|
||||
"next_follow_date": "",
|
||||
"source": "",
|
||||
"coupon_used": "",
|
||||
"notes": [],
|
||||
"tags": [],
|
||||
"created_at": "",
|
||||
"last_contact": "2026-02-28T15:29:05.725313",
|
||||
"last_update": "2026-02-28T15:29:05.725316"
|
||||
}
|
||||
}
|
||||
0
utils/daily_summary.py → legacy/daily_summary.py
Executable file → Normal file
0
db/deal_outcome_db.py → legacy/deal_outcome_db.py
Executable file → Normal file
0
db/designer_roster_db.py → legacy/designer_roster_db.py
Executable file → Normal file
0
utils/intent_analyzer.py → legacy/intent_analyzer.py
Executable file → Normal file
0
mail/__init__.py → legacy/mail/__init__.py
Executable file → Normal file
0
mail/email_receiver.py → legacy/mail/email_receiver.py
Executable file → Normal file
0
mail/email_sender.py → legacy/mail/email_sender.py
Executable file → Normal file
0
core/pydantic_ai_agent.py → legacy/pydantic_ai_agent.py
Executable file → Normal file
0
results/20260225211854.jpg → legacy/results/20260225211854.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 212 KiB |
0
results/debug_7debc0124b0441da9945feaeceef93b1.jpg → legacy/results/debug_7debc0124b0441da9945feaeceef93b1.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
0
results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg → legacy/results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
0
results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg → legacy/results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
0
results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg → legacy/results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 855 KiB After Width: | Height: | Size: 855 KiB |
0
results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg → legacy/results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 810 KiB After Width: | Height: | Size: 810 KiB |
0
results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg → legacy/results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 883 KiB After Width: | Height: | Size: 883 KiB |
0
results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg → legacy/results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
0
results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg → legacy/results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
0
results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg → legacy/results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
0
results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg → legacy/results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
0
results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg → legacy/results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
0
results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg → legacy/results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
0
results/result_90eaf777934445af81abbd60fe4778c5.jpg → legacy/results/result_90eaf777934445af81abbd60fe4778c5.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |