refactor: migrate workflow to v2 core and archive legacy modules

This commit is contained in:
2026-03-04 21:52:24 +08:00
parent e1ce17f2aa
commit fa61b11b02
156 changed files with 1781 additions and 2066 deletions

View File

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

Binary file not shown.

Binary file not shown.

29
core/adapters/base.py Normal file
View 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

View 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)]

View File

@@ -1,688 +1,39 @@
from __future__ import annotations
import logging 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 pydantic_ai import RunContext
from core.schema import StandardResponse
from db.customer_risk_db import risk_db from services.dispatch_service import dispatch_service
from services.service_tuhui_upload import upload_to_tuhui
from core.order_helpers import parse_order_info
logger = logging.getLogger("cs_agent") logger = logging.getLogger("cs_agent")
async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(description="转人工的原因")) -> str:
def register_tools(agent) -> None:
"""注册所有 Tool让 Agent 可以主动调用。"""
@agent.agent.tool
async def analyze_image(ctx: RunContext[Any], image_url: str) -> str:
""" """
分析客户发来的图片复杂度,用于报价 【核心工具】执行转人工逻辑
收到图片URL时调用此工具返回复杂度和建议报价 获取设计师姓名并生成精准转接指令
""" """
try: logger.info(f"[Tool] 尝试呼叫设计师接手: {reason}")
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
# 存图片类型到客户画像 # 1. 尝试派单获取设计师姓名
try: designer_name = await dispatch_service.assign_designer()
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
# 在 workflow 里创建待处理任务(付款后自动触发 Gemini if designer_name:
try: # 2. 有设计师在线:生成标准转接指令
from core.workflow import workflow magic_cmd = f"正在为您转接|[转移会话],{designer_name},无原因"
await workflow.image_analysis_result( logger.info(f"[Tool] 成功呼叫设计师: {designer_name}")
customer_id=ctx.deps.from_id, return magic_cmd
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("→ 直接拒绝,不说「发图来看看」,自然回复如:这类不做/不接。")
else: else:
lines.append("【无法处理】此图无法处理(纯黑/纯白/完全损坏/要找原始RAW文件") # 3. 设计师下线:返回特定信号
lines.append("→ 告知客户无法处理,建议换图或说明原因,不要报价。") logger.warning("[Tool] 派单失败:设计师们已下线或不在位")
elif risk == "high": return "ERROR_NO_DESIGNER_ONLINE"
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
# 构建报价说明 async def check_order_status_tool(ctx: RunContext[Any], customer_id: str = Field(description="客户ID")) -> str:
price_explanation = f"建议报价:{total_price}" """查询订单状态。"""
if text_surcharge > 0: return "设计师正在后台加急处理中,请稍等哈。"
price_explanation += f"(含文字处理 +{text_surcharge}元)"
if layer_surcharge > 0:
price_explanation += f"(含分层 +{layer_surcharge}元)"
lines.append(price_explanation) def register_agent_tools(agent: Any):
"""注册工具"""
# 添加文字数量说明 agent.tool(transfer_to_human_tool)
text_amount = result.get('text_amount', 'none') agent.tool(check_order_status_tool)
if text_amount != 'none': logger.info("[Agent] 工具箱已更新:称呼统一为“设计师”。")
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}元)"

63
core/engine.py Normal file
View 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
View 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
View 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

View 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
View 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
View 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
View 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()

View 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("已停止")

View File

@@ -1,6 +1,8 @@
import asyncio import asyncio
import websockets import websockets
import logging
logger = logging.getLogger("cs_agent")
async def connect_flow(client): async def connect_flow(client):
"""连接 WebSocket 服务器并自动重连。""" """连接 WebSocket 服务器并自动重连。"""
@@ -9,49 +11,22 @@ async def connect_flow(client):
client.logger.info(f"[{client.get_time()}] 正在连接轻简API {client.uri}...") client.logger.info(f"[{client.get_time()}] 正在连接轻简API {client.uri}...")
async with websockets.connect(client.uri) as websocket: async with websockets.connect(client.uri) as websocket:
client.websocket = websocket client.websocket = websocket
from utils.health_check import set_qingjian_connected
set_qingjian_connected(True)
client.logger.info(f"[{client.get_time()}] 连接成功!") 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()}] 等待接收消息...") client.logger.info(f"[{client.get_time()}] 等待接收消息...")
await client.receive_messages() 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: except Exception as e:
from utils.health_check import set_qingjian_connected # 统一捕获异常,避免因为不同版本的 websockets 导致属性错误
set_qingjian_connected(False) client.logger.info(f"[{client.get_time()}] 连接或运行错误: {e}")
client.logger.info(f"[{client.get_time()}] 连接错误: {e}")
if client.running: if client.running:
client.logger.info(f"[{client.get_time()}] 5秒后尝试重连...") client.logger.info(f"[{client.get_time()}] 5秒后尝试重连...")
await asyncio.sleep(5) await asyncio.sleep(5)
async def receive_messages_flow(client): async def receive_messages_flow(client):
"""持续接收消息。""" """持续接收消息。"""
try: try:
async for message in client.websocket: async for message in client.websocket:
await client.handle_message(message) 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: except Exception as e:
from utils.health_check import set_qingjian_connected client.logger.info(f"[{client.get_time()}] 接收消息中断: {e}")
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)

View File

@@ -42,13 +42,14 @@ async def send_message_flow(client, message):
await client.websocket.send(msg_json) await client.websocket.send(msg_json)
pretty = json.dumps(message, ensure_ascii=False, indent=2) pretty = json.dumps(message, ensure_ascii=False, indent=2)
client.logger.info(f"[{client.get_time()}] 发送成功:\n{pretty}") client.logger.info(f"[{client.get_time()}] 发送成功:\n{pretty}")
data = message.get("data", {}) if isinstance(message, dict) else {}
client._activity_log( client._activity_log(
"send_message_success", "send_message_success",
trace_id=message.get("_trace_id", ""), trace_id=message.get("_trace_id", "") if isinstance(message, dict) else "",
acc_id=message.get("acc_id", ""), acc_id=data.get("acc_id", ""),
customer_id=message.get("from_id", ""), customer_id=data.get("cy_id", ""),
msg_type=message.get("msg_type", 0), msg_type=data.get("msg_type", 0),
msg=message.get("msg", ""), msg=data.get("msg", ""),
) )
except Exception as e: except Exception as e:
client.logger.info(f"[{client.get_time()}] 发送失败: {e}") client.logger.info(f"[{client.get_time()}] 发送失败: {e}")

19
core/提示词.MD Normal file
View 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}"

View File

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

View File

@@ -199,14 +199,18 @@ def get_customers(limit: int = 100) -> List[Dict]:
def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]: def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]:
"""返回某客户的全部对话记录(按时间升序)""" """返回某客户的最近对话记录(按时间升序)"""
with _get_conn() as conn: with _get_conn() as conn:
# 核心修复:先取最新的 limit 条,再按时间正序排列
rows = conn.execute(_sql(""" rows = conn.execute(_sql("""
SELECT * FROM (
SELECT id, direction, message, msg_type, timestamp, acc_id SELECT id, direction, message, msg_type, timestamp, acc_id
FROM chat_logs FROM chat_logs
WHERE customer_id = ? WHERE customer_id = ?
ORDER BY timestamp ASC, id ASC ORDER BY timestamp DESC, id DESC
LIMIT ? LIMIT ?
) AS recent
ORDER BY timestamp ASC, id ASC
"""), (customer_id, limit)).fetchall() """), (customer_id, limit)).fetchall()
return [dict(r) for r in rows] return [dict(r) for r in rows]

Binary file not shown.

BIN
db/image_tasks.db Normal file

Binary file not shown.

View File

@@ -1,480 +1,113 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
图片任务数据库管理
支持客户后续增加需求细节
"""
import sqlite3 import sqlite3
import json import json
import logging import logging
import uuid
import os
from typing import Optional, List, Dict from typing import Optional, List, Dict
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
import os
logger = logging.getLogger(__name__) 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): class TaskStatus(Enum):
"""任务状态""" PENDING = "pending"
PENDING = "pending" # 待付款 PROCESSING = "processing"
PAID = "paid" # 已付款,待处理 COMPLETED = "completed"
PROCESSING = "processing" # 处理中 FAILED = "failed"
AWAITING_CONFIRM = "awaiting_confirm" # 已完成,待客户确认
COMPLETED = "completed" # 已完成
FAILED = "failed" # 失败
CANCELLED = "cancelled" # 已取消
class ImageTaskManager: class ImageTaskManager:
"""图片任务管理器"""
def __init__(self, db_path: str = None): def __init__(self, db_path: str = None):
if db_path is None: if db_path is None:
db_path = Path(__file__).parent / "image_tasks.db" db_path = Path(__file__).parent / "image_tasks.db"
self.db_path = db_path self.db_path = db_path
self._init_db() self._init_db()
logger.info(f"图片任务管理器初始化完成,数据库:{self.db_path}")
def _init_db(self): 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) self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
# 核心任务表
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS image_tasks ( CREATE TABLE IF NOT EXISTS image_tasks (
task_id TEXT PRIMARY KEY, task_id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL, customer_id TEXT NOT NULL,
customer_name TEXT, platform TEXT DEFAULT 'qianniu',
original_image TEXT NOT NULL, original_image TEXT NOT NULL,
operation TEXT DEFAULT 'enhance', operation TEXT DEFAULT 'enhance',
requirements TEXT, requirements TEXT,
customer_notes TEXT,
status TEXT DEFAULT 'pending', status TEXT DEFAULT 'pending',
created_at TEXT,
paid_at TEXT,
started_at TEXT,
completed_at TEXT,
result_image TEXT, result_image TEXT,
error_message TEXT, price REAL DEFAULT 0.0,
retry_count INTEGER DEFAULT 0, outcome TEXT DEFAULT 'pending',
acc_id TEXT, created_at TEXT,
acc_type TEXT DEFAULT 'AliWorkbench' 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_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.commit()
conn.close() conn.close()
logger.info("数据库表初始化完成")
def _get_conn(self): 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 = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
def create_task(self, task_id: str, customer_id: str, customer_name: str, def add_task(self, customer_id: str, platform: str, original_image: str, operation: str, requirements: str = "", status: str = "pending") -> str:
original_image: str, operation: str = 'enhance', task_id = str(uuid.uuid4())
requirements: dict = None, acc_id: str = '', acc_type: str = 'AliWorkbench') -> bool: now = datetime.now().isoformat()
"""创建图片任务"""
try: try:
conn = self._get_conn() conn = self._get_conn()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('''
requirements_json = json.dumps(requirements) if requirements else None INSERT INTO image_tasks (task_id, customer_id, platform, original_image, operation, requirements, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
cursor.execute(_sql(''' ''', (task_id, customer_id, platform, original_image, operation, requirements, status, now, now))
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
))
conn.commit() conn.commit()
conn.close() conn.close()
return task_id
logger.info(f"图片任务创建成功:{task_id}")
return True
except Exception as e: except Exception as e:
logger.error(f"创建图片任务失败:{e}") logger.error(f"Failed to add task: {e}")
return False 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: try:
conn = self._get_conn() conn = self._get_conn()
cursor = conn.cursor() cursor = conn.cursor()
if result_image:
cursor.execute(_sql('SELECT * FROM image_tasks WHERE task_id = ?'), (task_id,)) cursor.execute('UPDATE image_tasks SET status = ?, result_image = ?, updated_at = ? WHERE task_id = ?',
row = cursor.fetchone() (status, result_image, now, task_id))
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))
else: else:
cursor.execute(_sql(''' cursor.execute('UPDATE image_tasks SET status = ?, updated_at = ? WHERE task_id = ?',
SELECT * FROM image_tasks (status, now, task_id))
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)
conn.commit() conn.commit()
conn.close() conn.close()
logger.info(f"任务状态更新:{task_id} -> {status.value}")
except Exception as e: 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: try:
conn = self._get_conn() conn = self._get_conn()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('''
cursor.execute(_sql('''
UPDATE image_tasks UPDATE image_tasks
SET result_image = ?, error_message = ? SET outcome = ?, updated_at = ?
WHERE task_id = ? WHERE task_id = (
'''), (result_image, error_message, 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.commit()
conn.close() conn.close()
logger.info(f"[DB] 客户 {customer_id} 任务结局更新为: {outcome}")
logger.info(f"任务结果更新:{task_id}")
except Exception as e: except Exception as e:
logger.error(f"更新任务结果失败:{e}") logger.error(f"Failed to update outcome: {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
# 单例 # 单例
_task_manager: Optional[ImageTaskManager] = None db = ImageTaskManager()
def get_image_task_manager() -> ImageTaskManager:
"""获取图片任务管理器单例"""
global _task_manager
if _task_manager is None:
_task_manager = ImageTaskManager()
return _task_manager

0
chat_log_db/chats.db → legacy/chat_log_db/chats.db Executable file → Normal file
View File

View 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
View File

0
db/deal_outcome_db.py → legacy/deal_outcome_db.py Executable file → Normal file
View File

View File

0
utils/intent_analyzer.py → legacy/intent_analyzer.py Executable file → Normal file
View File

0
mail/__init__.py → legacy/mail/__init__.py Executable file → Normal file
View File

View File

0
mail/email_sender.py → legacy/mail/email_sender.py Executable file → Normal file
View File

View File

View File

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

View File

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

Before

Width:  |  Height:  |  Size: 855 KiB

After

Width:  |  Height:  |  Size: 855 KiB

View File

Before

Width:  |  Height:  |  Size: 810 KiB

After

Width:  |  Height:  |  Size: 810 KiB

View File

Before

Width:  |  Height:  |  Size: 883 KiB

After

Width:  |  Height:  |  Size: 883 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Some files were not shown because too many files have changed in this diff Show More