diff --git a/.env.tianwang b/.env.tianwang deleted file mode 100644 index bdc3ed5..0000000 --- a/.env.tianwang +++ /dev/null @@ -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 diff --git a/__pycache__/chat_log_db.cpython-310.pyc b/__pycache__/chat_log_db.cpython-310.pyc deleted file mode 100755 index 927e770..0000000 Binary files a/__pycache__/chat_log_db.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/customer_db.cpython-310.pyc b/__pycache__/customer_db.cpython-310.pyc deleted file mode 100755 index 8dfe83f..0000000 Binary files a/__pycache__/customer_db.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/daily_summary.cpython-310.pyc b/__pycache__/daily_summary.cpython-310.pyc deleted file mode 100755 index b8ecae2..0000000 Binary files a/__pycache__/daily_summary.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/email_receiver.cpython-310.pyc b/__pycache__/email_receiver.cpython-310.pyc deleted file mode 100755 index 8104f57..0000000 Binary files a/__pycache__/email_receiver.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/email_sender.cpython-310.pyc b/__pycache__/email_sender.cpython-310.pyc deleted file mode 100755 index 4813bd2..0000000 Binary files a/__pycache__/email_sender.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/image_analyzer.cpython-310.pyc b/__pycache__/image_analyzer.cpython-310.pyc deleted file mode 100755 index c7887a5..0000000 Binary files a/__pycache__/image_analyzer.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/image_processor.cpython-310.pyc b/__pycache__/image_processor.cpython-310.pyc deleted file mode 100755 index 3222338..0000000 Binary files a/__pycache__/image_processor.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/image_qa.cpython-310.pyc b/__pycache__/image_qa.cpython-310.pyc deleted file mode 100755 index 2c447a2..0000000 Binary files a/__pycache__/image_qa.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/image_tools.cpython-310.pyc b/__pycache__/image_tools.cpython-310.pyc deleted file mode 100755 index 3a673e8..0000000 Binary files a/__pycache__/image_tools.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/perspective_fix.cpython-310.pyc b/__pycache__/perspective_fix.cpython-310.pyc deleted file mode 100755 index 4216c51..0000000 Binary files a/__pycache__/perspective_fix.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/pydantic_ai_agent.cpython-310.pyc b/__pycache__/pydantic_ai_agent.cpython-310.pyc deleted file mode 100755 index 944455b..0000000 Binary files a/__pycache__/pydantic_ai_agent.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/service_gemini.cpython-310.pyc b/__pycache__/service_gemini.cpython-310.pyc deleted file mode 100755 index 1e3f7d4..0000000 Binary files a/__pycache__/service_gemini.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/service_qwen.cpython-310.pyc b/__pycache__/service_qwen.cpython-310.pyc deleted file mode 100755 index 5563e6e..0000000 Binary files a/__pycache__/service_qwen.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/workflow.cpython-310.pyc b/__pycache__/workflow.cpython-310.pyc deleted file mode 100755 index fc7a31a..0000000 Binary files a/__pycache__/workflow.cpython-310.pyc and /dev/null differ diff --git a/api/__pycache__/http_server.cpython-311.pyc b/api/__pycache__/http_server.cpython-311.pyc deleted file mode 100644 index f98aae3..0000000 Binary files a/api/__pycache__/http_server.cpython-311.pyc and /dev/null differ diff --git a/config/__pycache__/__init__.cpython-310.pyc b/config/__pycache__/__init__.cpython-310.pyc index 0e2270a..d777f27 100755 Binary files a/config/__pycache__/__init__.cpython-310.pyc and b/config/__pycache__/__init__.cpython-310.pyc differ diff --git a/config/__pycache__/config.cpython-310.pyc b/config/__pycache__/config.cpython-310.pyc index 317a117..b2888f8 100755 Binary files a/config/__pycache__/config.cpython-310.pyc and b/config/__pycache__/config.cpython-310.pyc differ diff --git a/core/__pycache__/__init__.cpython-310.pyc b/core/__pycache__/__init__.cpython-310.pyc index 735cef9..12c7596 100755 Binary files a/core/__pycache__/__init__.cpython-310.pyc and b/core/__pycache__/__init__.cpython-310.pyc differ diff --git a/core/__pycache__/pydantic_ai_agent.cpython-310.pyc b/core/__pycache__/pydantic_ai_agent.cpython-310.pyc deleted file mode 100755 index cc88b6a..0000000 Binary files a/core/__pycache__/pydantic_ai_agent.cpython-310.pyc and /dev/null differ diff --git a/core/__pycache__/websocket_client.cpython-310.pyc b/core/__pycache__/websocket_client.cpython-310.pyc deleted file mode 100755 index cbcd6fe..0000000 Binary files a/core/__pycache__/websocket_client.cpython-310.pyc and /dev/null differ diff --git a/core/__pycache__/workflow.cpython-310.pyc b/core/__pycache__/workflow.cpython-310.pyc deleted file mode 100755 index 96c91fa..0000000 Binary files a/core/__pycache__/workflow.cpython-310.pyc and /dev/null differ diff --git a/core/adapters/base.py b/core/adapters/base.py new file mode 100644 index 0000000..9a9a363 --- /dev/null +++ b/core/adapters/base.py @@ -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 diff --git a/core/adapters/qianniu_adapter.py b/core/adapters/qianniu_adapter.py new file mode 100644 index 0000000..69abb25 --- /dev/null +++ b/core/adapters/qianniu_adapter.py @@ -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)] diff --git a/core/agent_tools.py b/core/agent_tools.py index 76c16fb..b1f4505 100644 --- a/core/agent_tools.py +++ b/core/agent_tools.py @@ -1,688 +1,39 @@ -from __future__ import annotations - import logging -from typing import Any - +import asyncio +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field from pydantic_ai import RunContext - -from db.customer_risk_db import risk_db -from services.service_tuhui_upload import upload_to_tuhui -from core.order_helpers import parse_order_info +from core.schema import StandardResponse +from services.dispatch_service import dispatch_service logger = logging.getLogger("cs_agent") +async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(description="转人工的原因")) -> str: + """ + 【核心工具】执行转人工逻辑。 + 获取设计师姓名并生成精准转接指令。 + """ + logger.info(f"[Tool] 尝试呼叫设计师接手: {reason}") + + # 1. 尝试派单获取设计师姓名 + designer_name = await dispatch_service.assign_designer() + + if designer_name: + # 2. 有设计师在线:生成标准转接指令 + magic_cmd = f"正在为您转接|[转移会话],{designer_name},无原因" + logger.info(f"[Tool] 成功呼叫设计师: {designer_name}") + return magic_cmd + else: + # 3. 设计师下线:返回特定信号 + logger.warning("[Tool] 派单失败:设计师们已下线或不在位") + return "ERROR_NO_DESIGNER_ONLINE" -def register_tools(agent) -> None: - """注册所有 Tool,让 Agent 可以主动调用。""" +async def check_order_status_tool(ctx: RunContext[Any], customer_id: str = Field(description="客户ID")) -> str: + """查询订单状态。""" + return "设计师正在后台加急处理中,请稍等哈。" - @agent.agent.tool - async def analyze_image(ctx: RunContext[Any], image_url: str) -> str: - """ - 分析客户发来的图片复杂度,用于报价。 - 收到图片URL时调用此工具,返回复杂度和建议报价。 - """ - try: - from image.image_analyzer import image_analyzer - result = await image_analyzer.analyze(image_url) - complexity_label = { - "simple": "简单(画面干净)", - "normal": "一般复杂度", - "complex": "细节偏多", - "hard": "非常复杂", - }.get(result["complexity"], result["complexity"]) - # 持久化图片URL和复杂度,重启后仍能记住这张图 - try: - from db.customer_db import db - db.update_last_image( - ctx.deps.from_id, - image_url, - complexity=result["complexity"], - gemini_prompt=result.get("gemini_prompt", ""), - aspect_ratio=result.get("aspect_ratio", "1:1"), - perspective=result.get("perspective", "no"), - ) - except Exception: - pass - - # 存图片类型到客户画像 - try: - from db.customer_db import db as _db - if result.get("subject"): - _db.add_image_type(ctx.deps.from_id, result["subject"]) - except Exception: - pass - - # 在 workflow 里创建待处理任务(付款后自动触发 Gemini) - try: - from core.workflow import workflow - await workflow.image_analysis_result( - customer_id=ctx.deps.from_id, - image_url=image_url, - complexity=result["complexity"], - acc_id=ctx.deps.acc_id, - acc_type=ctx.deps.platform, - gemini_prompt=result.get("gemini_prompt", ""), - aspect_ratio=result.get("aspect_ratio", "1:1"), - perspective=result.get("perspective", "no"), - proc_type=result.get("proc_type", ""), - subject=result.get("subject", ""), - quality=result.get("quality", ""), - ) - logger.info( - "[Agent] Workflow 任务已创建 | 客户: %s | 比例: %s | 透视: %s | 图片: %s...", - ctx.deps.from_id, - result.get("aspect_ratio"), - result.get("perspective"), - image_url[:60], - ) - except Exception as e: - logger.exception("[Agent] Workflow 任务创建失败: %s", e) - - # 组装给 AI 的分析报告 - risk = result.get("risk", "none") - has_face = result.get("has_face", "no") - feasibility = result.get("feasibility", "yes") - note = result.get("note", "") - - lines = [ - f"图片主体:{result['subject'] or '未识别'}", - f"处理类型:{result['proc_type'] or '高清修复'}", - f"原图质量:{result['quality'] or '未知'}", - f"图片类型:{result.get('category', '') or '通用'}", - f"图片尺寸:{(result.get('width') or 0)}x{(result.get('height') or 0)}({result.get('megapixels', 0.0)}MP)", - f"含人脸:{'是' if has_face == 'yes' else '否'}", - f"复杂度:{complexity_label}", - f"原因:{result['reason']}", - ] - if result.get("size_surcharge"): - lines.append(f"尺寸加价:+{result['size_surcharge']}元") - if result.get("size_note"): - lines.append(f"尺寸提示:{result['size_note']}") - try: - st = agent._get_conversation_state(ctx.deps.from_id) - if isinstance(result.get("price_min"), (int, float)): - st.last_min_price = int(result.get("price_min") or 0) - try: - from db.customer_db import db as _db - _db.update_last_min_price(ctx.deps.from_id, st.last_min_price) - except Exception: - pass - except Exception: - pass - - # 根据可做性和风险等级给 AI 不同的行动指引 - if feasibility == "no": - if "敏感" in (note or ""): - lines.append("【拒绝】图片含敏感/黄色/擦边内容,不接单。") - lines.append("→ 直接拒绝,不说「发图来看看」,自然回复如:这类不做/不接。") - else: - lines.append("【无法处理】此图无法处理(纯黑/纯白/完全损坏/要找原始RAW文件)。") - lines.append("→ 告知客户无法处理,建议换图或说明原因,不要报价。") - elif risk == "high": - lines.append(f"【高风险】此图处理风险高:{note or 'AI修复后效果不能保证与原图一致'}") - lines.append(f"建议报价:{result['price_suggest']}元") - lines.append("→ 先自然说明风险(人脸/效果可能不完美),再报价,满意再拍。话术自然。") - elif risk == "low": - lines.append(f"【低风险-含人脸】修复后人脸相似度约70-90%,效果不稳定。") - lines.append(f"建议报价:{result['price_suggest']}元") - lines.append(f"→ 报价时自然加一句风险提示(人脸可能有轻微变化、满意再付等)") - else: - # 无风险,正常报价 - base_price = result.get('price_suggest', 20) - text_surcharge = result.get('text_surcharge', 0) - layer_surcharge = result.get('layer_surcharge', 0) - total_price = base_price + text_surcharge + layer_surcharge - - # 构建报价说明 - price_explanation = f"建议报价:{total_price}元" - if text_surcharge > 0: - price_explanation += f"(含文字处理 +{text_surcharge}元)" - if layer_surcharge > 0: - price_explanation += f"(含分层 +{layer_surcharge}元)" - - lines.append(price_explanation) - - # 添加文字数量说明 - text_amount = result.get('text_amount', 'none') - if text_amount != 'none': - lines.append(f"文字数量:{text_amount},需要精细处理") - - if feasibility == "partial": - lines.append(f"⚠️ 此图有一定难度:{note or '效果可能不完美'},回复时可加「效果不满意退款」") - if note and note not in ("无", ""): - lines.append(f"提示:{note}") - lines.append(f"【立刻回复客户报价 {total_price} 元,话术自然多变】") - - return "\n".join(lines) - except Exception as e: - return f"图片分析失败: {e},请根据经验判断报价" - - @agent.agent.tool - async def get_customer_info(ctx: RunContext[Any], customer_id: str) -> str: - """ - 查询客户历史信息:消费记录、性格标签、报价历史等。 - 对话开始时或需要了解客户背景时调用。 - """ - try: - from db.customer_db import db - return db.get_profile_text(customer_id) - except Exception as e: - return f"查询失败: {e}" - - @agent.agent.tool - async def transfer_to_human(ctx: RunContext[Any]) -> str: - """ - 转接人工客服。 - 遇到退款/投诉/情绪激动/复杂售后时调用。 - """ - return "TRANSFER_REQUESTED" - - @agent.agent.tool - async def get_customer_risk_profile(ctx: RunContext[Any], customer_id: str = "") -> str: - """查询客户风控画像:退款/不付款/差评/人工黑名单等。""" - cid = customer_id or ctx.deps.from_id - try: - info = risk_db.evaluate_customer(cid) - return ( - f"客户:{cid}\n" - f"不接单:{'是' if info.get('do_not_serve') else '否'}\n" - f"风险等级:{info.get('computed_level','low')} 分数:{info.get('computed_score',0)}\n" - f"近30天退款:{info.get('refund_30d',0)}\n" - f"近7天未付款下单:{info.get('unpaid_7d',0)}\n" - f"近90天差评:{info.get('bad_review_90d',0)}\n" - f"备注:{info.get('note','') or '无'}" - ) - except Exception as e: - return f"查询风控画像失败: {e}" - - @agent.agent.tool - async def mark_customer_risk( - ctx: RunContext[Any], - customer_id: str, - do_not_serve: bool = False, - risk_level: str = "low", - risk_score: int = 0, - note: str = "", - tag: str = "", - ) -> str: - """人工标记客户风控画像(不接单/高风险/备注标签)。""" - try: - tags = [tag] if tag else [] - risk_db.set_profile( - customer_id=customer_id, - do_not_serve=do_not_serve, - risk_level=risk_level, - risk_score=risk_score, - note=note, - tags=tags, - ) - return "风控画像已更新" - except Exception as e: - return f"更新风控画像失败: {e}" - - @agent.agent.tool - async def record_customer_risk_event( - ctx: RunContext[Any], - customer_id: str, - event_type: str, - event_count: int = 1, - note: str = "", - ) -> str: - """记录风控事件:refund/unpaid_order/bad_review/blacklist_hit 等。""" - try: - risk_db.record_event( - customer_id=customer_id, - event_type=event_type, - event_count=event_count, - note=note, - ) - return "风控事件已记录" - except Exception as e: - return f"记录风控事件失败: {e}" - - @agent.agent.tool - async def save_customer_note( - ctx: RunContext[Any], - customer_id: str, - note: str - ) -> str: - """ - 记录客户关键信息到画像(邮箱/微信/特殊需求等)。 - 客户提供联系方式或重要信息时调用。 - """ - try: - from db.customer_db import db - db.add_note(customer_id, note) - return "已记录" - except Exception as e: - return f"记录失败: {e}" - - @agent.agent.tool - async def update_contact_info( - ctx: RunContext[Any], - customer_id: str, - contact_type: str, - value: str - ) -> str: - """ - 更新客户联系方式。 - 当客户说出邮箱/手机/微信时调用,比正则提取更准确。 - - contact_type 枚举值: - email - 邮箱 - phone - 手机号 - wechat - 微信号 - """ - try: - from db.customer_db import db - if contact_type == "email": - db.update_email(customer_id, value) - elif contact_type == "phone": - db.update_phone(customer_id, value) - elif contact_type == "wechat": - db.update_wechat(customer_id, value) - else: - return f"未知联系方式类型: {contact_type}" - return f"已保存 {contact_type}: {value}" - except Exception as e: - return f"保存失败: {e}" - - @agent.agent.tool - async def record_quote( - ctx: RunContext[Any], - customer_id: str, - price: int, - description: str = "" - ) -> str: - """ - 记录本次报价到客户画像,用于后续对话保持价格一致。 - 每次给客户报价后调用。 - - Args: - customer_id: 客户ID - price: 报价金额(元) - description: 报价描述,如"单图处理"/"三图打包" - """ - try: - from db.customer_db import db - db.update_last_price(customer_id, price) - if description: - db.add_note(customer_id, f"报价 {price}元({description})") - # 同步到内存状态 - state = agent.conversations.get(customer_id) - if state: - state.last_price = price - return f"已记录报价 {price}元" - except Exception as e: - return f"记录失败: {e}" - - @agent.agent.tool - async def process_image_gemini(ctx: RunContext[Any], customer_id: str = "") -> str: - """ - 触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。 - 会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。 - 处理完成后会自动发图给客户。 - """ - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不自动作图" - except Exception: - return "现在处理模块暂时暂停,先不自动作图" - cid = customer_id or ctx.deps.from_id - try: - from core.workflow import workflow - ok = await workflow.trigger_processing_on_payment( - customer_id=cid, - acc_id=ctx.deps.acc_id, - acc_type=ctx.deps.platform, - ) - if ok: - return "已安排,稍后发你" - return "该客户暂无待处理图片,请先发图" - except Exception as e: - return f"触发作图失败: {e},请稍后重试或转人工" - - @agent.agent_pricing.tool - async def analyze_image_pricing(ctx: RunContext[Any], image_url: str) -> str: - try: - from image.image_analyzer import image_analyzer - result = await image_analyzer.analyze(image_url) - if result.get("feasibility") == "no" or result.get("risk") == "high": - return "该图风险高或不可做:不报价,建议换图或转人工评估。" - if not result.get("success", False): - return "图片识别异常:先不报价,建议客户重发更清晰图片。" - p = result.get("price_suggest", 20) - try: - st = agent._get_conversation_state(ctx.deps.from_id) - if isinstance(result.get("price_min"), (int, float)): - st.last_min_price = int(result.get("price_min") or 0) - try: - from db.customer_db import db as _db - _db.update_last_min_price(ctx.deps.from_id, st.last_min_price) - except Exception: - pass - except Exception: - pass - return f"建议报价:{p}元" - except Exception as e: - return f"图片分析失败: {e}" - - @agent.agent_pricing.tool - async def record_quote_pricing( - ctx: RunContext[Any], - customer_id: str, - price: int, - description: str = "" - ) -> str: - try: - from db.customer_db import db - db.update_last_price(customer_id, price) - return "ok" - except Exception as e: - return f"记录失败: {e}" - - @agent.agent_processing.tool - async def process_image_gemini_run(ctx: RunContext[Any], customer_id: str = "") -> str: - """触发 Gemini 作图处理(processing agent 专用入口)。""" - return await process_image_gemini(ctx, customer_id) - - @agent.agent_similar.tool - async def recommend_similar(ctx: RunContext[Any], hint: str = "") -> str: - try: - return "有类似款,拍下我发你参考图。" - except Exception as e: - return f"推荐失败: {e}" - - @agent.agent_order.tool - async def handle_order(ctx: RunContext[Any], raw_msg: str = "") -> str: - try: - info = parse_order_info(raw_msg or "") - paid_kw = ["等待发货", "已付款", "付款成功", "买家已付款"] - if any(k in (info.get("pay_status", "") or "") for k in paid_kw) or any(k in (info.get("order_status", "") or "") for k in paid_kw): - return "已安排,稍后发你" - return "" - except Exception: - return "" - - @agent.agent_risk.tool - async def risk_filter(ctx: RunContext[Any], text: str = "") -> str: - return "这类不做哈,政治/敏感内容都不接。" - - @agent.agent_risk.tool - async def get_customer_risk_profile_risk(ctx: RunContext[Any], customer_id: str = "") -> str: - return await get_customer_risk_profile(ctx, customer_id) - - @agent.agent_risk.tool - async def mark_customer_risk_risk( - ctx: RunContext[Any], - customer_id: str, - do_not_serve: bool = False, - risk_level: str = "low", - risk_score: int = 0, - note: str = "", - tag: str = "", - ) -> str: - return await mark_customer_risk( - ctx=ctx, - customer_id=customer_id, - do_not_serve=do_not_serve, - risk_level=risk_level, - risk_score=risk_score, - note=note, - tag=tag, - ) - - @agent.agent_risk.tool - async def record_customer_risk_event_risk( - ctx: RunContext[Any], - customer_id: str, - event_type: str, - event_count: int = 1, - note: str = "", - ) -> str: - return await record_customer_risk_event( - ctx=ctx, - customer_id=customer_id, - event_type=event_type, - event_count=event_count, - note=note, - ) - - @agent.agent.tool - async def remove_background(ctx: RunContext[Any], image_url: str) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】去背景,输出白底图。客户只要去背景时调用。""" - try: - from image.image_tools import remove_background as _rb - r = await _rb(image_url) - if r["success"]: - return f"去背景完成,已保存。自然回复客户好了发你" - return f"去背景失败:{r['message']}" - except Exception as e: - return f"去背景失败:{e}" - - @agent.agent.tool - async def perspective_correct(ctx: RunContext[Any], image_url: str) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】透视矫正。输入需白底图,输出展平图。""" - try: - from image.image_tools import perspective_correct as _pc - r = await _pc(image_url) - if r["success"]: - return f"透视矫正完成。自然回复客户好了" - return f"透视矫正失败:{r['message']}" - except Exception as e: - return f"透视矫正失败:{e}" - - @agent.agent.tool - async def extract_pattern_tool( - ctx: RunContext[Any], - image_url: str, - prompt: str = "", - aspect_ratio: str = "1:1" - ) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】印花提取/主处理。按提示词和比例处理。""" - try: - from image.image_tools import extract_pattern - r = await extract_pattern(image_url, prompt=prompt, aspect_ratio=aspect_ratio) - if r["success"]: - return f"提取完成。自然回复客户好了发你" - return f"提取失败:{r['message']}" - except Exception as e: - return f"提取失败:{e}" - - @agent.agent.tool - async def enhance_image_tool(ctx: RunContext[Any], image_url: str) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】高清增强。客户只要清晰化时调用。""" - try: - from image.image_tools import enhance_image - r = await enhance_image(image_url) - if r["success"]: - return f"高清增强完成。自然回复客户好了" - return f"增强失败:{r['message']}" - except Exception as e: - return f"增强失败:{e}" - - @agent.agent.tool - async def color_match_tool( - ctx: RunContext[Any], - orig_url: str, - result_url: str, - strength: float = 0.75 - ) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】颜色匹配。将 result 色调匹配到 orig。""" - try: - from image.image_tools import color_match_images - r = await color_match_images(orig_url, result_url, strength=strength) - if r["success"]: - return f"颜色匹配完成" - return f"颜色匹配失败:{r['message']}" - except Exception as e: - return f"颜色匹配失败:{e}" - - @agent.agent.tool - async def trim_border_tool(ctx: RunContext[Any], image_url: str) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】裁切四周背景边(白/黄/米等)。""" - try: - from image.image_tools import trim_border - r = await trim_border(image_url) - if r["success"]: - return f"裁边完成" - return f"裁边失败:{r['message']}" - except Exception as e: - return f"裁边失败:{e}" - - @agent.agent.tool - async def vectorize_to_eps_tool(ctx: RunContext[Any], image_url: str) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """【独立工具】矢量化 - 将图片转为 EPS 矢量文件。客户要做矢量图、转 EPS、转 AI 格式时调用。""" - try: - from image.image_tools import vectorize_to_eps - r = await vectorize_to_eps(image_url) - if r["success"]: - return f"矢量化完成,已生成 EPS 文件。自然回复客户好了发你" - return f"矢量化失败:{r['message']}" - except Exception as e: - return f"矢量化失败:{e}" - - @agent.agent.tool - async def meitu_enhance_tool( - ctx: RunContext[Any], - image_url: str, - mode: str = "standard" - ) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """ - 【独立工具】美图画质增强。客户要画质增强、清晰化、美图处理时调用。 - - Args: - image_url: 图片 URL 或本地路径 - mode: 处理模式。crystal(极速重绘) standard(标准) enhance(增强) hdr(HDR) portrait(人像优化) - """ - try: - from image.image_tools import meitu_enhance - r = await meitu_enhance(image_url, mode=mode) - if r["success"]: - return f"画质增强完成。自然回复客户好了发你" - return f"画质增强失败:{r['message']}" - except Exception as e: - return f"画质增强失败:{e}" - - @agent.agent.tool - async def resize_image( - ctx: RunContext[Any], - image_url: str, - width: int, - height: int = 0 - ) -> str: - try: - from config.config import IMAGE_MODULE_ENABLED - if not IMAGE_MODULE_ENABLED: - return "现在处理模块暂时暂停,先不处理图片" - except Exception: - return "现在处理模块暂时暂停,先不处理图片" - """ - 改图片尺寸。客户说「改成1920x1080」「弄成横图」「改下尺寸」时调用。 - - Args: - image_url: 图片URL(客户刚发的图,或从对话中获取) - width: 目标宽度(像素),如 1920 - height: 目标高度(0=按宽度等比缩放),如 1080 - - 常用尺寸:1920x1080(横屏) 1080x1920(竖屏) 2000x2000(方图) - """ - try: - from image.image_processor import image_processor - result = await image_processor.resize(image_url, width, height) - if result["success"]: - return f"改尺寸完成:{width}x{height},已保存。自然回复客户改好了" - else: - return f"改尺寸失败:{result['message']},告知客户稍后重试" - except Exception as e: - return f"改尺寸失败:{e}" - - @agent.agent.tool - async def calculate_bulk_price( - ctx: RunContext[Any], - image_count: int, - complexities: str = "" - ) -> str: - """ - 计算多图打包价格。 - 客户要做多张图时调用,返回建议总价。 - - Args: - image_count: 图片数量 - complexities: 各图复杂度,逗号分隔,如 "normal,complex,simple" - 没有识别结果时留空,按平均价格估算 - """ - if image_count <= 0: - return "图片数量无效" - - # 各复杂度单价(必须为5的整数倍) - unit_price = {"simple": 15, "normal": 20, "complex": 25, "hard": 30} - default_unit = 20 # 没有识别结果时的默认单价 - - if complexities: - levels = [c.strip() for c in complexities.split(",")] - total = sum(unit_price.get(lv, default_unit) for lv in levels) - else: - total = image_count * default_unit - - # 打包优惠:3张以上9折,5张以上8折,价格必须为5的整数倍 - if image_count >= 5: - discounted = round(total * 0.8 / 5) * 5 - tip = f"({image_count}张8折优惠)" - elif image_count >= 3: - discounted = round(total * 0.9 / 5) * 5 - tip = f"({image_count}张9折优惠)" - else: - discounted = round(total / 5) * 5 - tip = "" - - return f"建议打包报价:{discounted}元{tip}(原价{total}元)" +def register_agent_tools(agent: Any): + """注册工具""" + agent.tool(transfer_to_human_tool) + agent.tool(check_order_status_tool) + logger.info("[Agent] 工具箱已更新:称呼统一为“设计师”。") diff --git a/core/engine.py b/core/engine.py new file mode 100644 index 0000000..191bc62 --- /dev/null +++ b/core/engine.py @@ -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} + ) + ) diff --git a/core/events/event_bus.py b/core/events/event_bus.py new file mode 100644 index 0000000..cf8063e --- /dev/null +++ b/core/events/event_bus.py @@ -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() diff --git a/core/orchestrator.py b/core/orchestrator.py new file mode 100644 index 0000000..421869f --- /dev/null +++ b/core/orchestrator.py @@ -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 diff --git a/core/pydantic_ai_agent_v2.py b/core/pydantic_ai_agent_v2.py new file mode 100644 index 0000000..aa1394c --- /dev/null +++ b/core/pydantic_ai_agent_v2.py @@ -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}) diff --git a/core/repository.py b/core/repository.py new file mode 100644 index 0000000..0a86175 --- /dev/null +++ b/core/repository.py @@ -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() diff --git a/core/schema.py b/core/schema.py new file mode 100644 index 0000000..a912734 --- /dev/null +++ b/core/schema.py @@ -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 = {} # 额外元数据(如埋点、调试信息) diff --git a/core/skill_manager.py b/core/skill_manager.py new file mode 100644 index 0000000..a929052 --- /dev/null +++ b/core/skill_manager.py @@ -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() diff --git a/core/websocket_client_v2.py b/core/websocket_client_v2.py new file mode 100644 index 0000000..78c0992 --- /dev/null +++ b/core/websocket_client_v2.py @@ -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("已停止") diff --git a/core/websocket_connection_flow.py b/core/websocket_connection_flow.py index e4de8b0..4934da3 100644 --- a/core/websocket_connection_flow.py +++ b/core/websocket_connection_flow.py @@ -1,6 +1,8 @@ import asyncio import websockets +import logging +logger = logging.getLogger("cs_agent") async def connect_flow(client): """连接 WebSocket 服务器并自动重连。""" @@ -9,49 +11,22 @@ async def connect_flow(client): client.logger.info(f"[{client.get_time()}] 正在连接轻简API {client.uri}...") async with websockets.connect(client.uri) as websocket: client.websocket = websocket - from utils.health_check import set_qingjian_connected - set_qingjian_connected(True) client.logger.info(f"[{client.get_time()}] 连接成功!") - if client.enable_agent: - client.logger.info(f"[{client.get_time()}] AI Agent 已启用,将自动处理消息") client.logger.info(f"[{client.get_time()}] 等待接收消息...") - await client.receive_messages() - except ConnectionRefusedError: - from utils.health_check import set_qingjian_connected - set_qingjian_connected(False) - client.logger.info(f"[{client.get_time()}] 连接被拒绝,请检查轻简软件是否已启动") - except websockets.exceptions.InvalidURI: - from utils.health_check import set_qingjian_connected - set_qingjian_connected(False) - client.logger.info(f"[{client.get_time()}] URI格式错误") except Exception as e: - from utils.health_check import set_qingjian_connected - set_qingjian_connected(False) - client.logger.info(f"[{client.get_time()}] 连接错误: {e}") + # 统一捕获异常,避免因为不同版本的 websockets 导致属性错误 + client.logger.info(f"[{client.get_time()}] 连接或运行错误: {e}") if client.running: client.logger.info(f"[{client.get_time()}] 5秒后尝试重连...") await asyncio.sleep(5) - async def receive_messages_flow(client): """持续接收消息。""" try: async for message in client.websocket: await client.handle_message(message) - except websockets.exceptions.ConnectionClosed: - from utils.health_check import set_qingjian_connected - set_qingjian_connected(False) - client.logger.info(f"[{client.get_time()}] 连接已关闭") except Exception as e: - from utils.health_check import set_qingjian_connected - set_qingjian_connected(False) - client.logger.info(f"[{client.get_time()}] 接收消息错误: {e}") - - -async def handle_message_flow(client, message, *, shop_type_resolver): - from core.websocket_inbound_flow import handle_incoming_message - - await handle_incoming_message(client, message, shop_type_resolver=shop_type_resolver) + client.logger.info(f"[{client.get_time()}] 接收消息中断: {e}") diff --git a/core/websocket_send_flow.py b/core/websocket_send_flow.py index 02ea704..df3bcbb 100644 --- a/core/websocket_send_flow.py +++ b/core/websocket_send_flow.py @@ -42,13 +42,14 @@ async def send_message_flow(client, message): await client.websocket.send(msg_json) pretty = json.dumps(message, ensure_ascii=False, indent=2) client.logger.info(f"[{client.get_time()}] 发送成功:\n{pretty}") + data = message.get("data", {}) if isinstance(message, dict) else {} client._activity_log( "send_message_success", - trace_id=message.get("_trace_id", ""), - acc_id=message.get("acc_id", ""), - customer_id=message.get("from_id", ""), - msg_type=message.get("msg_type", 0), - msg=message.get("msg", ""), + trace_id=message.get("_trace_id", "") if isinstance(message, dict) else "", + acc_id=data.get("acc_id", ""), + customer_id=data.get("cy_id", ""), + msg_type=data.get("msg_type", 0), + msg=data.get("msg", ""), ) except Exception as e: client.logger.info(f"[{client.get_time()}] 发送失败: {e}") diff --git a/core/提示词.MD b/core/提示词.MD new file mode 100644 index 0000000..9e659e7 --- /dev/null +++ b/core/提示词.MD @@ -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}" diff --git a/customer_db/customers.json b/customer_db/customers.json index 6d587d7..9e26dfe 100755 --- a/customer_db/customers.json +++ b/customer_db/customers.json @@ -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" - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/db/__pycache__/__init__.cpython-310.pyc b/db/__pycache__/__init__.cpython-310.pyc index fec3c2e..34574f7 100755 Binary files a/db/__pycache__/__init__.cpython-310.pyc and b/db/__pycache__/__init__.cpython-310.pyc differ diff --git a/db/__pycache__/chat_log_db.cpython-310.pyc b/db/__pycache__/chat_log_db.cpython-310.pyc index 6d87751..f151f7b 100755 Binary files a/db/__pycache__/chat_log_db.cpython-310.pyc and b/db/__pycache__/chat_log_db.cpython-310.pyc differ diff --git a/db/__pycache__/customer_db.cpython-310.pyc b/db/__pycache__/customer_db.cpython-310.pyc index e128049..776213f 100755 Binary files a/db/__pycache__/customer_db.cpython-310.pyc and b/db/__pycache__/customer_db.cpython-310.pyc differ diff --git a/db/__pycache__/deal_outcome_db.cpython-310.pyc b/db/__pycache__/deal_outcome_db.cpython-310.pyc deleted file mode 100755 index 8dd4a6e..0000000 Binary files a/db/__pycache__/deal_outcome_db.cpython-310.pyc and /dev/null differ diff --git a/db/__pycache__/designer_roster_db.cpython-310.pyc b/db/__pycache__/designer_roster_db.cpython-310.pyc deleted file mode 100755 index 795744a..0000000 Binary files a/db/__pycache__/designer_roster_db.cpython-310.pyc and /dev/null differ diff --git a/db/chat_log_db.py b/db/chat_log_db.py index e967181..384ab57 100755 --- a/db/chat_log_db.py +++ b/db/chat_log_db.py @@ -199,14 +199,18 @@ def get_customers(limit: int = 100) -> List[Dict]: def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]: - """返回某客户的全部对话记录(按时间升序)""" + """返回某客户的最近对话记录(按时间升序)""" with _get_conn() as conn: + # 核心修复:先取最新的 limit 条,再按时间正序排列 rows = conn.execute(_sql(""" - SELECT id, direction, message, msg_type, timestamp, acc_id - FROM chat_logs - WHERE customer_id = ? + SELECT * FROM ( + SELECT id, direction, message, msg_type, timestamp, acc_id + FROM chat_logs + WHERE customer_id = ? + ORDER BY timestamp DESC, id DESC + LIMIT ? + ) AS recent ORDER BY timestamp ASC, id ASC - LIMIT ? """), (customer_id, limit)).fetchall() return [dict(r) for r in rows] diff --git a/db/chat_log_db/chats.db b/db/chat_log_db/chats.db index 54f6472..3927cce 100755 Binary files a/db/chat_log_db/chats.db and b/db/chat_log_db/chats.db differ diff --git a/db/image_tasks.db b/db/image_tasks.db new file mode 100644 index 0000000..63d4f28 Binary files /dev/null and b/db/image_tasks.db differ diff --git a/db/image_tasks_db.py b/db/image_tasks_db.py index 173419b..b9ce4a8 100644 --- a/db/image_tasks_db.py +++ b/db/image_tasks_db.py @@ -1,480 +1,113 @@ # -*- coding: utf-8 -*- -""" -图片任务数据库管理 -支持客户后续增加需求细节 -""" import sqlite3 import json import logging +import uuid +import os from typing import Optional, List, Dict from pathlib import Path from datetime import datetime from enum import Enum -import os logger = logging.getLogger(__name__) -_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower() -_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1") -_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) -_MYSQL_USER = os.getenv("MYSQL_USER", "root") -_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "") -_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs") - -def _is_mysql() -> bool: - return _DB_TYPE in ("mysql", "mariadb") - -def _sql(query: str) -> str: - return query.replace("?", "%s") if _is_mysql() else query - -def _now_str() -> str: - if _is_mysql(): - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - return datetime.now().isoformat() class TaskStatus(Enum): - """任务状态""" - PENDING = "pending" # 待付款 - PAID = "paid" # 已付款,待处理 - PROCESSING = "processing" # 处理中 - AWAITING_CONFIRM = "awaiting_confirm" # 已完成,待客户确认 - COMPLETED = "completed" # 已完成 - FAILED = "failed" # 失败 - CANCELLED = "cancelled" # 已取消 + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" class ImageTaskManager: - """图片任务管理器""" - def __init__(self, db_path: str = None): if db_path is None: db_path = Path(__file__).parent / "image_tasks.db" - self.db_path = db_path self._init_db() - logger.info(f"图片任务管理器初始化完成,数据库:{self.db_path}") - + def _init_db(self): - """初始化数据库""" - if _is_mysql(): - conn = self._get_conn() - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS image_tasks ( - task_id VARCHAR(128) PRIMARY KEY, - customer_id VARCHAR(128) NOT NULL, - customer_name VARCHAR(255), - original_image TEXT NOT NULL, - operation VARCHAR(64) DEFAULT 'enhance', - requirements TEXT, - customer_notes TEXT, - status VARCHAR(32) DEFAULT 'pending', - created_at DATETIME, - paid_at DATETIME, - started_at DATETIME, - completed_at DATETIME, - result_image TEXT, - error_message TEXT, - retry_count INT DEFAULT 0, - acc_id VARCHAR(128), - acc_type VARCHAR(64) DEFAULT 'AliWorkbench' - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS task_requirement_changes ( - id INTEGER PRIMARY KEY AUTO_INCREMENT, - task_id VARCHAR(128) NOT NULL, - change_type VARCHAR(64), - old_value TEXT, - new_value TEXT, - changed_at DATETIME, - changed_by VARCHAR(32), - FOREIGN KEY (task_id) REFERENCES image_tasks(task_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 - ''') - cursor.execute("SHOW INDEX FROM image_tasks") - exists = {str(r.get("Key_name", "")) for r in cursor.fetchall()} - if "idx_customer" not in exists: - cursor.execute('CREATE INDEX idx_customer ON image_tasks(customer_id)') - if "idx_status" not in exists: - cursor.execute('CREATE INDEX idx_status ON image_tasks(status)') - if "idx_created" not in exists: - cursor.execute('CREATE INDEX idx_created ON image_tasks(created_at)') - conn.commit() - conn.close() - else: - self.db_path.parent.mkdir(parents=True, exist_ok=True) - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS image_tasks ( - task_id TEXT PRIMARY KEY, - customer_id TEXT NOT NULL, - customer_name TEXT, - original_image TEXT NOT NULL, - operation TEXT DEFAULT 'enhance', - requirements TEXT, - customer_notes TEXT, - status TEXT DEFAULT 'pending', - created_at TEXT, - paid_at TEXT, - started_at TEXT, - completed_at TEXT, - result_image TEXT, - error_message TEXT, - retry_count INTEGER DEFAULT 0, - acc_id TEXT, - acc_type TEXT DEFAULT 'AliWorkbench' - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS task_requirement_changes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - task_id TEXT NOT NULL, - change_type TEXT, - old_value TEXT, - new_value TEXT, - changed_at TEXT, - changed_by TEXT, - FOREIGN KEY (task_id) REFERENCES image_tasks(task_id) - ) - ''') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON image_tasks(customer_id)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)') - conn.commit() - conn.close() - logger.info("数据库表初始化完成") - - def _get_conn(self): - """获取数据库连接""" - if _is_mysql(): - import pymysql - return pymysql.connect( - host=_MYSQL_HOST, - port=_MYSQL_PORT, - user=_MYSQL_USER, - password=_MYSQL_PASSWORD, - database=_MYSQL_DATABASE, - charset="utf8mb4", - cursorclass=pymysql.cursors.DictCursor, - autocommit=False, + self.db_path.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + # 核心任务表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS image_tasks ( + task_id TEXT PRIMARY KEY, + customer_id TEXT NOT NULL, + platform TEXT DEFAULT 'qianniu', + original_image TEXT NOT NULL, + operation TEXT DEFAULT 'enhance', + requirements TEXT, + status TEXT DEFAULT 'pending', + result_image TEXT, + price REAL DEFAULT 0.0, + outcome TEXT DEFAULT 'pending', + created_at TEXT, + updated_at TEXT ) + ''') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_cust_plat ON image_tasks(customer_id, platform)') + conn.commit() + conn.close() + + def _get_conn(self): conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row return conn - - def create_task(self, task_id: str, customer_id: str, customer_name: str, - original_image: str, operation: str = 'enhance', - requirements: dict = None, acc_id: str = '', acc_type: str = 'AliWorkbench') -> bool: - """创建图片任务""" + + def add_task(self, customer_id: str, platform: str, original_image: str, operation: str, requirements: str = "", status: str = "pending") -> str: + task_id = str(uuid.uuid4()) + now = datetime.now().isoformat() try: conn = self._get_conn() cursor = conn.cursor() - - requirements_json = json.dumps(requirements) if requirements else None - - cursor.execute(_sql(''' - INSERT INTO image_tasks ( - task_id, customer_id, customer_name, original_image, - operation, requirements, customer_notes, status, - created_at, acc_id, acc_type - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - '''), ( - task_id, - customer_id, - customer_name, - original_image, - operation, - requirements_json, - '', # 初始备注为空 - TaskStatus.PENDING.value, - _now_str(), - acc_id, - acc_type - )) - + cursor.execute(''' + INSERT INTO image_tasks (task_id, customer_id, platform, original_image, operation, requirements, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (task_id, customer_id, platform, original_image, operation, requirements, status, now, now)) conn.commit() conn.close() - - logger.info(f"图片任务创建成功:{task_id}") - return True - + return task_id except Exception as e: - logger.error(f"创建图片任务失败:{e}") - return False - - def get_task(self, task_id: str) -> Optional[dict]: - """查询任务""" + logger.error(f"Failed to add task: {e}") + return "" + + def update_status(self, task_id: str, status: str, result_image: str = ""): + now = datetime.now().isoformat() try: conn = self._get_conn() cursor = conn.cursor() - - cursor.execute(_sql('SELECT * FROM image_tasks WHERE task_id = ?'), (task_id,)) - row = cursor.fetchone() - conn.close() - - if row: - task = dict(row) - # 解析 JSON 字段 - if task.get('requirements'): - task['requirements'] = json.loads(task['requirements']) - return task - return None - - except Exception as e: - logger.error(f"查询任务失败:{e}") - return None - - def get_customer_tasks(self, customer_id: str, status: str = None) -> List[dict]: - """查询客户的任务列表""" - try: - conn = self._get_conn() - cursor = conn.cursor() - - if status: - cursor.execute(_sql(''' - SELECT * FROM image_tasks - WHERE customer_id = ? AND status = ? - ORDER BY created_at DESC - '''), (customer_id, status)) + if result_image: + cursor.execute('UPDATE image_tasks SET status = ?, result_image = ?, updated_at = ? WHERE task_id = ?', + (status, result_image, now, task_id)) else: - cursor.execute(_sql(''' - SELECT * FROM image_tasks - WHERE customer_id = ? - ORDER BY created_at DESC - '''), (customer_id,)) - - rows = cursor.fetchall() - conn.close() - - tasks = [] - for row in rows: - task = dict(row) - if task.get('requirements'): - task['requirements'] = json.loads(task['requirements']) - tasks.append(task) - - return tasks - - except Exception as e: - logger.error(f"查询客户任务失败:{e}") - return [] - - def update_status(self, task_id: str, status: TaskStatus): - """更新任务状态""" - try: - conn = self._get_conn() - cursor = conn.cursor() - - placeholder = "%s" if _is_mysql() else "?" - updates = [f'status = {placeholder}'] - params = [status.value] - - # 根据状态设置时间 - if status == TaskStatus.PAID: - updates.append(f'paid_at = {placeholder}') - params.append(_now_str()) - elif status == TaskStatus.PROCESSING: - updates.append(f'started_at = {placeholder}') - params.append(_now_str()) - elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]: - updates.append(f'completed_at = {placeholder}') - params.append(_now_str()) - - params.append(task_id) - - cursor.execute(_sql(f''' - UPDATE image_tasks - SET {', '.join(updates)} - WHERE task_id = ? - '''), params) - + cursor.execute('UPDATE image_tasks SET status = ?, updated_at = ? WHERE task_id = ?', + (status, now, task_id)) conn.commit() conn.close() - - logger.info(f"任务状态更新:{task_id} -> {status.value}") - except Exception as e: - logger.error(f"更新任务状态失败:{e}") - - def update_result(self, task_id: str, result_image: str, error_message: str = None): - """更新处理结果""" + logger.error(f"Failed to update task status: {e}") + + def update_outcome(self, customer_id: str, platform: str, outcome: str): + """记录任务的最终结局(用于训练样本分类)""" + now = datetime.now().isoformat() try: conn = self._get_conn() cursor = conn.cursor() - - cursor.execute(_sql(''' + cursor.execute(''' UPDATE image_tasks - SET result_image = ?, error_message = ? - WHERE task_id = ? - '''), (result_image, error_message, task_id)) - + SET outcome = ?, updated_at = ? + WHERE task_id = ( + SELECT task_id FROM image_tasks + WHERE customer_id = ? AND platform = ? + ORDER BY created_at DESC LIMIT 1 + ) + ''', (outcome, now, customer_id, platform)) conn.commit() conn.close() - - logger.info(f"任务结果更新:{task_id}") - + logger.info(f"[DB] 客户 {customer_id} 任务结局更新为: {outcome}") except Exception as e: - logger.error(f"更新任务结果失败:{e}") - - def add_customer_note(self, task_id: str, note: str, changed_by: str = 'customer') -> bool: - """ - 客户添加需求备注(支持后续增加细节) - - Args: - task_id: 任务 ID - note: 备注内容 - changed_by: 修改者(customer/staff) - - Returns: - bool: 是否成功 - """ - try: - conn = self._get_conn() - cursor = conn.cursor() - - # 获取旧备注 - cursor.execute(_sql('SELECT customer_notes FROM image_tasks WHERE task_id = ?'), (task_id,)) - row = cursor.fetchone() - old_note = row['customer_notes'] if row else '' - - # 更新备注 - new_note = f"{old_note}\n[{datetime.now().strftime('%m-%d %H:%M')}] {note}" if old_note else f"[{datetime.now().strftime('%m-%d %H:%M')}] {note}" - - cursor.execute(_sql(''' - UPDATE image_tasks - SET customer_notes = ? - WHERE task_id = ? - '''), (new_note, task_id)) - - # 记录变更历史 - cursor.execute(_sql(''' - INSERT INTO task_requirement_changes ( - task_id, change_type, old_value, new_value, changed_at, changed_by - ) VALUES (?, ?, ?, ?, ?, ?) - '''), ( - task_id, - 'add_note', - old_note or '无', - note, - _now_str(), - changed_by - )) - - conn.commit() - conn.close() - - logger.info(f"客户添加备注成功:{task_id}") - return True - - except Exception as e: - logger.error(f"添加客户备注失败:{e}") - return False - - def modify_operation(self, task_id: str, new_operation: str, changed_by: str = 'customer') -> bool: - """ - 修改操作类型(客户后续修改需求) - - Args: - task_id: 任务 ID - new_operation: 新操作类型 - changed_by: 修改者 - - Returns: - bool: 是否成功 - """ - try: - conn = self._get_conn() - cursor = conn.cursor() - - # 获取旧操作 - cursor.execute(_sql('SELECT operation FROM image_tasks WHERE task_id = ?'), (task_id,)) - row = cursor.fetchone() - old_operation = row['operation'] if row else '' - - # 更新操作 - cursor.execute(_sql(''' - UPDATE image_tasks - SET operation = ? - WHERE task_id = ? - '''), (new_operation, task_id)) - - # 记录变更历史 - cursor.execute(_sql(''' - INSERT INTO task_requirement_changes ( - task_id, change_type, old_value, new_value, changed_at, changed_by - ) VALUES (?, ?, ?, ?, ?, ?) - '''), ( - task_id, - 'modify_operation', - old_operation, - new_operation, - _now_str(), - changed_by - )) - - conn.commit() - conn.close() - - logger.info(f"修改操作类型成功:{task_id} -> {new_operation}") - return True - - except Exception as e: - logger.error(f"修改操作类型失败:{e}") - return False - - def get_requirement_history(self, task_id: str) -> List[dict]: - """获取需求变更历史""" - try: - conn = self._get_conn() - cursor = conn.cursor() - - cursor.execute(_sql(''' - SELECT * FROM task_requirement_changes - WHERE task_id = ? - ORDER BY changed_at DESC - '''), (task_id,)) - - rows = cursor.fetchall() - conn.close() - - return [dict(row) for row in rows] - - except Exception as e: - logger.error(f"查询需求历史失败:{e}") - return [] - - def get_pending_tasks(self) -> List[dict]: - """获取所有待处理任务""" - return self.get_customer_tasks('', 'pending') - - def increment_retry(self, task_id: str) -> int: - """增加重试次数""" - try: - conn = self._get_conn() - cursor = conn.cursor() - - cursor.execute(_sql(''' - UPDATE image_tasks - SET retry_count = retry_count + 1 - WHERE task_id = ? - '''), (task_id,)) - - cursor.execute(_sql('SELECT retry_count FROM image_tasks WHERE task_id = ?'), (task_id,)) - row = cursor.fetchone() - conn.close() - - return row['retry_count'] if row else 0 - - except Exception as e: - logger.error(f"增加重试次数失败:{e}") - return 999 + logger.error(f"Failed to update outcome: {e}") # 单例 -_task_manager: Optional[ImageTaskManager] = None - -def get_image_task_manager() -> ImageTaskManager: - """获取图片任务管理器单例""" - global _task_manager - if _task_manager is None: - _task_manager = ImageTaskManager() - return _task_manager +db = ImageTaskManager() diff --git a/db/task_db/__pycache__/task_model.cpython-311.pyc b/db/task_db/__pycache__/task_model.cpython-311.pyc deleted file mode 100644 index e5dea29..0000000 Binary files a/db/task_db/__pycache__/task_model.cpython-311.pyc and /dev/null differ diff --git a/image/__pycache__/__init__.cpython-310.pyc b/image/__pycache__/__init__.cpython-310.pyc index e77975b..d3508cc 100755 Binary files a/image/__pycache__/__init__.cpython-310.pyc and b/image/__pycache__/__init__.cpython-310.pyc differ diff --git a/image/__pycache__/image_analyzer.cpython-310.pyc b/image/__pycache__/image_analyzer.cpython-310.pyc index 03e5735..2879cb5 100755 Binary files a/image/__pycache__/image_analyzer.cpython-310.pyc and b/image/__pycache__/image_analyzer.cpython-310.pyc differ diff --git a/image/__pycache__/image_precheck.cpython-310.pyc b/image/__pycache__/image_precheck.cpython-310.pyc deleted file mode 100755 index b212d0e..0000000 Binary files a/image/__pycache__/image_precheck.cpython-310.pyc and /dev/null differ diff --git a/image/__pycache__/image_processor.cpython-310.pyc b/image/__pycache__/image_processor.cpython-310.pyc deleted file mode 100755 index 30bea40..0000000 Binary files a/image/__pycache__/image_processor.cpython-310.pyc and /dev/null differ diff --git a/image/__pycache__/image_qa.cpython-310.pyc b/image/__pycache__/image_qa.cpython-310.pyc deleted file mode 100755 index 942eac6..0000000 Binary files a/image/__pycache__/image_qa.cpython-310.pyc and /dev/null differ diff --git a/core/agent_pre_rules.py b/legacy/agent_pre_rules.py similarity index 100% rename from core/agent_pre_rules.py rename to legacy/agent_pre_rules.py diff --git a/core/agent_prompts.py b/legacy/agent_prompts.py similarity index 100% rename from core/agent_prompts.py rename to legacy/agent_prompts.py diff --git a/core/ai_reply_flow.py b/legacy/ai_reply_flow.py similarity index 100% rename from core/ai_reply_flow.py rename to legacy/ai_reply_flow.py diff --git a/core/batch_quote_helpers.py b/legacy/batch_quote_helpers.py similarity index 100% rename from core/batch_quote_helpers.py rename to legacy/batch_quote_helpers.py diff --git a/chat_log_db/chats.db b/legacy/chat_log_db/chats.db old mode 100755 new mode 100644 similarity index 100% rename from chat_log_db/chats.db rename to legacy/chat_log_db/chats.db diff --git a/core/collection_intent_helpers.py b/legacy/collection_intent_helpers.py similarity index 100% rename from core/collection_intent_helpers.py rename to legacy/collection_intent_helpers.py diff --git a/core/context_helpers.py b/legacy/context_helpers.py similarity index 100% rename from core/context_helpers.py rename to legacy/context_helpers.py diff --git a/core/conversation_state_store.py b/legacy/conversation_state_store.py similarity index 100% rename from core/conversation_state_store.py rename to legacy/conversation_state_store.py diff --git a/legacy/customer_db/customers.json b/legacy/customer_db/customers.json new file mode 100644 index 0000000..6d587d7 --- /dev/null +++ b/legacy/customer_db/customers.json @@ -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" + } +} \ No newline at end of file diff --git a/db/customer_risk_db.py b/legacy/customer_risk_db.py similarity index 100% rename from db/customer_risk_db.py rename to legacy/customer_risk_db.py diff --git a/utils/daily_summary.py b/legacy/daily_summary.py old mode 100755 new mode 100644 similarity index 100% rename from utils/daily_summary.py rename to legacy/daily_summary.py diff --git a/db/deal_outcome_db.py b/legacy/deal_outcome_db.py old mode 100755 new mode 100644 similarity index 100% rename from db/deal_outcome_db.py rename to legacy/deal_outcome_db.py diff --git a/db/designer_roster_db.py b/legacy/designer_roster_db.py old mode 100755 new mode 100644 similarity index 100% rename from db/designer_roster_db.py rename to legacy/designer_roster_db.py diff --git a/evolution/__init__.py b/legacy/evolution/__init__.py similarity index 100% rename from evolution/__init__.py rename to legacy/evolution/__init__.py diff --git a/evolution/mvp.py b/legacy/evolution/mvp.py similarity index 100% rename from evolution/mvp.py rename to legacy/evolution/mvp.py diff --git a/features/price_negotiation_with_registration.md b/legacy/features/price_negotiation_with_registration.md similarity index 100% rename from features/price_negotiation_with_registration.md rename to legacy/features/price_negotiation_with_registration.md diff --git a/features/risk_customer_detection.md b/legacy/features/risk_customer_detection.md similarity index 100% rename from features/risk_customer_detection.md rename to legacy/features/risk_customer_detection.md diff --git a/features/self_evolution_mvp.md b/legacy/features/self_evolution_mvp.md similarity index 100% rename from features/self_evolution_mvp.md rename to legacy/features/self_evolution_mvp.md diff --git a/features/text_surcharge.md b/legacy/features/text_surcharge.md similarity index 100% rename from features/text_surcharge.md rename to legacy/features/text_surcharge.md diff --git a/features/tuhui_upload.md b/legacy/features/tuhui_upload.md similarity index 100% rename from features/tuhui_upload.md rename to legacy/features/tuhui_upload.md diff --git a/core/find_image_flow.py b/legacy/find_image_flow.py similarity index 100% rename from core/find_image_flow.py rename to legacy/find_image_flow.py diff --git a/core/image_workflow_router.py b/legacy/image_workflow_router.py similarity index 100% rename from core/image_workflow_router.py rename to legacy/image_workflow_router.py diff --git a/utils/intent_analyzer.py b/legacy/intent_analyzer.py old mode 100755 new mode 100644 similarity index 100% rename from utils/intent_analyzer.py rename to legacy/intent_analyzer.py diff --git a/mail/__init__.py b/legacy/mail/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from mail/__init__.py rename to legacy/mail/__init__.py diff --git a/mail/email_receiver.py b/legacy/mail/email_receiver.py old mode 100755 new mode 100644 similarity index 100% rename from mail/email_receiver.py rename to legacy/mail/email_receiver.py diff --git a/mail/email_sender.py b/legacy/mail/email_sender.py old mode 100755 new mode 100644 similarity index 100% rename from mail/email_sender.py rename to legacy/mail/email_sender.py diff --git a/core/message_orchestrator.py b/legacy/message_orchestrator.py similarity index 100% rename from core/message_orchestrator.py rename to legacy/message_orchestrator.py diff --git a/core/order_flow.py b/legacy/order_flow.py similarity index 100% rename from core/order_flow.py rename to legacy/order_flow.py diff --git a/core/post_ops.py b/legacy/post_ops.py similarity index 100% rename from core/post_ops.py rename to legacy/post_ops.py diff --git a/core/prompt_builder.py b/legacy/prompt_builder.py similarity index 100% rename from core/prompt_builder.py rename to legacy/prompt_builder.py diff --git a/core/prompt_flow.py b/legacy/prompt_flow.py similarity index 100% rename from core/prompt_flow.py rename to legacy/prompt_flow.py diff --git a/core/pydantic_ai_agent.py b/legacy/pydantic_ai_agent.py old mode 100755 new mode 100644 similarity index 100% rename from core/pydantic_ai_agent.py rename to legacy/pydantic_ai_agent.py diff --git a/core/reply_finalize_flow.py b/legacy/reply_finalize_flow.py similarity index 100% rename from core/reply_finalize_flow.py rename to legacy/reply_finalize_flow.py diff --git a/results/20260225211854.jpg b/legacy/results/20260225211854.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/20260225211854.jpg rename to legacy/results/20260225211854.jpg diff --git a/results/debug_7debc0124b0441da9945feaeceef93b1.jpg b/legacy/results/debug_7debc0124b0441da9945feaeceef93b1.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/debug_7debc0124b0441da9945feaeceef93b1.jpg rename to legacy/results/debug_7debc0124b0441da9945feaeceef93b1.jpg diff --git a/results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg b/legacy/results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg rename to legacy/results/pfix_final_73bc9c0c4bed4be198b200158be6f813.jpg diff --git a/results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg b/legacy/results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg rename to legacy/results/pfix_final_7debc0124b0441da9945feaeceef93b1.jpg diff --git a/results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg b/legacy/results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg rename to legacy/results/pfix_final_b3dd76cbc37e403ca9425ece8ba2ebcd.jpg diff --git a/results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg b/legacy/results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg rename to legacy/results/pfix_final_bc3c45fd447749f38f62dbb87a942aba.jpg diff --git a/results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg b/legacy/results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg rename to legacy/results/pfix_final_d9679c27640b43c18b9f590047e6c2dd.jpg diff --git a/results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg b/legacy/results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg rename to legacy/results/resize_95152a96618146738c3e6a12a6a6d9d8.jpg diff --git a/results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg b/legacy/results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg rename to legacy/results/resize_d9ef87fa8de14b0b8d030067d0de163e.jpg diff --git a/results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg b/legacy/results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg rename to legacy/results/result_2d5b47961e7b42eabe2fd7beb8c9be1f.jpg diff --git a/results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg b/legacy/results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg rename to legacy/results/result_3e60b204f3a748eabb41a05cc28e1a11.jpg diff --git a/results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg b/legacy/results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg rename to legacy/results/result_4cd07206b2d24c21a81c3d45a3c4e16f.jpg diff --git a/results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg b/legacy/results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg rename to legacy/results/result_5c19d435fc8e4b2caa03c589f53d61ac.jpg diff --git a/results/result_90eaf777934445af81abbd60fe4778c5.jpg b/legacy/results/result_90eaf777934445af81abbd60fe4778c5.jpg old mode 100755 new mode 100644 similarity index 100% rename from results/result_90eaf777934445af81abbd60fe4778c5.jpg rename to legacy/results/result_90eaf777934445af81abbd60fe4778c5.jpg diff --git a/core/risk_text_helpers.py b/legacy/risk_text_helpers.py similarity index 100% rename from core/risk_text_helpers.py rename to legacy/risk_text_helpers.py diff --git a/core/rules/__init__.py b/legacy/rules/__init__.py similarity index 100% rename from core/rules/__init__.py rename to legacy/rules/__init__.py diff --git a/core/rules/engine.py b/legacy/rules/engine.py similarity index 100% rename from core/rules/engine.py rename to legacy/rules/engine.py diff --git a/scripts/chat_log_viewer.py b/legacy/scripts/chat_log_viewer.py old mode 100755 new mode 100644 similarity index 100% rename from scripts/chat_log_viewer.py rename to legacy/scripts/chat_log_viewer.py diff --git a/scripts/chat_ui.py b/legacy/scripts/chat_ui.py old mode 100755 new mode 100644 similarity index 100% rename from scripts/chat_ui.py rename to legacy/scripts/chat_ui.py diff --git a/scripts/evolution_cycle.py b/legacy/scripts/evolution_cycle.py similarity index 100% rename from scripts/evolution_cycle.py rename to legacy/scripts/evolution_cycle.py diff --git a/scripts/init_designer_roster.py b/legacy/scripts/init_designer_roster.py old mode 100755 new mode 100644 similarity index 100% rename from scripts/init_designer_roster.py rename to legacy/scripts/init_designer_roster.py diff --git a/scripts/migrate_chat_logs_to_mysql.py b/legacy/scripts/migrate_chat_logs_to_mysql.py similarity index 100% rename from scripts/migrate_chat_logs_to_mysql.py rename to legacy/scripts/migrate_chat_logs_to_mysql.py diff --git a/scripts/migrate_customers_json_to_mysql.py b/legacy/scripts/migrate_customers_json_to_mysql.py similarity index 100% rename from scripts/migrate_customers_json_to_mysql.py rename to legacy/scripts/migrate_customers_json_to_mysql.py diff --git a/scripts/migrate_remaining_sqlite_to_mysql.py b/legacy/scripts/migrate_remaining_sqlite_to_mysql.py similarity index 100% rename from scripts/migrate_remaining_sqlite_to_mysql.py rename to legacy/scripts/migrate_remaining_sqlite_to_mysql.py diff --git a/scripts/multi_process_launcher.py b/legacy/scripts/multi_process_launcher.py similarity index 100% rename from scripts/multi_process_launcher.py rename to legacy/scripts/multi_process_launcher.py diff --git a/scripts/run_test_ai_chat.ps1 b/legacy/scripts/run_test_ai_chat.ps1 similarity index 100% rename from scripts/run_test_ai_chat.ps1 rename to legacy/scripts/run_test_ai_chat.ps1 diff --git a/core/websocket_agent_reply_flow.py b/legacy/websocket_agent_reply_flow.py similarity index 100% rename from core/websocket_agent_reply_flow.py rename to legacy/websocket_agent_reply_flow.py diff --git a/core/websocket_auto_quote_flow.py b/legacy/websocket_auto_quote_flow.py similarity index 100% rename from core/websocket_auto_quote_flow.py rename to legacy/websocket_auto_quote_flow.py diff --git a/core/websocket_brain_flow.py b/legacy/websocket_brain_flow.py similarity index 100% rename from core/websocket_brain_flow.py rename to legacy/websocket_brain_flow.py diff --git a/core/websocket_callback_flow.py b/legacy/websocket_callback_flow.py similarity index 100% rename from core/websocket_callback_flow.py rename to legacy/websocket_callback_flow.py diff --git a/core/websocket_client.py b/legacy/websocket_client.py old mode 100755 new mode 100644 similarity index 100% rename from core/websocket_client.py rename to legacy/websocket_client.py diff --git a/core/websocket_customer_profile_flow.py b/legacy/websocket_customer_profile_flow.py similarity index 100% rename from core/websocket_customer_profile_flow.py rename to legacy/websocket_customer_profile_flow.py diff --git a/core/websocket_debounce_flow.py b/legacy/websocket_debounce_flow.py similarity index 100% rename from core/websocket_debounce_flow.py rename to legacy/websocket_debounce_flow.py diff --git a/core/websocket_dispatch_flow.py b/legacy/websocket_dispatch_flow.py similarity index 100% rename from core/websocket_dispatch_flow.py rename to legacy/websocket_dispatch_flow.py diff --git a/core/websocket_followup_flow.py b/legacy/websocket_followup_flow.py similarity index 100% rename from core/websocket_followup_flow.py rename to legacy/websocket_followup_flow.py diff --git a/core/websocket_helpers_flow.py b/legacy/websocket_helpers_flow.py similarity index 100% rename from core/websocket_helpers_flow.py rename to legacy/websocket_helpers_flow.py diff --git a/core/websocket_image_entry_flow.py b/legacy/websocket_image_entry_flow.py similarity index 100% rename from core/websocket_image_entry_flow.py rename to legacy/websocket_image_entry_flow.py diff --git a/core/websocket_inbound_flow.py b/legacy/websocket_inbound_flow.py similarity index 100% rename from core/websocket_inbound_flow.py rename to legacy/websocket_inbound_flow.py diff --git a/core/websocket_message_utils_flow.py b/legacy/websocket_message_utils_flow.py similarity index 100% rename from core/websocket_message_utils_flow.py rename to legacy/websocket_message_utils_flow.py diff --git a/core/websocket_misc_rules_flow.py b/legacy/websocket_misc_rules_flow.py similarity index 100% rename from core/websocket_misc_rules_flow.py rename to legacy/websocket_misc_rules_flow.py diff --git a/core/websocket_outbound_arbiter_flow.py b/legacy/websocket_outbound_arbiter_flow.py similarity index 100% rename from core/websocket_outbound_arbiter_flow.py rename to legacy/websocket_outbound_arbiter_flow.py diff --git a/core/websocket_outbound_flow.py b/legacy/websocket_outbound_flow.py similarity index 100% rename from core/websocket_outbound_flow.py rename to legacy/websocket_outbound_flow.py diff --git a/core/websocket_quote_flow.py b/legacy/websocket_quote_flow.py similarity index 100% rename from core/websocket_quote_flow.py rename to legacy/websocket_quote_flow.py diff --git a/core/websocket_summary_flow.py b/legacy/websocket_summary_flow.py similarity index 100% rename from core/websocket_summary_flow.py rename to legacy/websocket_summary_flow.py diff --git a/core/websocket_system_inquiry_flow.py b/legacy/websocket_system_inquiry_flow.py similarity index 100% rename from core/websocket_system_inquiry_flow.py rename to legacy/websocket_system_inquiry_flow.py diff --git a/core/websocket_transfer_flow.py b/legacy/websocket_transfer_flow.py similarity index 100% rename from core/websocket_transfer_flow.py rename to legacy/websocket_transfer_flow.py diff --git a/core/websocket_workflow_flow.py b/legacy/websocket_workflow_flow.py similarity index 100% rename from core/websocket_workflow_flow.py rename to legacy/websocket_workflow_flow.py diff --git a/utils/wechat_chat_log.py b/legacy/wechat_chat_log.py old mode 100755 new mode 100644 similarity index 100% rename from utils/wechat_chat_log.py rename to legacy/wechat_chat_log.py diff --git a/core/workflow.py b/legacy/workflow.py old mode 100755 new mode 100644 similarity index 100% rename from core/workflow.py rename to legacy/workflow.py diff --git a/部署文档.md b/legacy/部署文档.md similarity index 100% rename from 部署文档.md rename to legacy/部署文档.md diff --git a/项目功能汇总.md b/legacy/项目功能汇总.md similarity index 100% rename from 项目功能汇总.md rename to legacy/项目功能汇总.md diff --git a/mail/__pycache__/__init__.cpython-310.pyc b/mail/__pycache__/__init__.cpython-310.pyc deleted file mode 100755 index e759d81..0000000 Binary files a/mail/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/mail/__pycache__/email_receiver.cpython-310.pyc b/mail/__pycache__/email_receiver.cpython-310.pyc deleted file mode 100755 index 2ef5ae2..0000000 Binary files a/mail/__pycache__/email_receiver.cpython-310.pyc and /dev/null differ diff --git a/mail/__pycache__/email_sender.cpython-310.pyc b/mail/__pycache__/email_sender.cpython-310.pyc deleted file mode 100755 index 934686d..0000000 Binary files a/mail/__pycache__/email_sender.cpython-310.pyc and /dev/null differ diff --git a/run.py b/run.py index 3232957..5e1ee70 100755 --- a/run.py +++ b/run.py @@ -47,10 +47,10 @@ def _print_api_info(host: str, port: int): def run_websocket(enable_agent: bool): """WebSocket 客服模式(默认)""" import asyncio - from core.websocket_client import QingjianAPIClient + from core.websocket_client_v2 import QingjianAPIClient logger.info("=" * 60) - logger.info("AI 客服系统 - WebSocket 模式") + logger.info("AI 客服系统 - WebSocket 模式 (新架构 v2)") logger.info(f"AI Agent: {'已启用' if enable_agent else '未启用'}") logger.info("=" * 60) diff --git a/scripts/__pycache__/chat_ui.cpython-310.pyc b/scripts/__pycache__/chat_ui.cpython-310.pyc deleted file mode 100755 index 173d7a1..0000000 Binary files a/scripts/__pycache__/chat_ui.cpython-310.pyc and /dev/null differ diff --git a/services/__pycache__/__init__.cpython-310.pyc b/services/__pycache__/__init__.cpython-310.pyc index 367e83e..5681703 100755 Binary files a/services/__pycache__/__init__.cpython-310.pyc and b/services/__pycache__/__init__.cpython-310.pyc differ diff --git a/services/__pycache__/service_gemini.cpython-310.pyc b/services/__pycache__/service_gemini.cpython-310.pyc deleted file mode 100755 index 79b095c..0000000 Binary files a/services/__pycache__/service_gemini.cpython-310.pyc and /dev/null differ diff --git a/services/__pycache__/service_meitu.cpython-310.pyc b/services/__pycache__/service_meitu.cpython-310.pyc deleted file mode 100755 index dde9f81..0000000 Binary files a/services/__pycache__/service_meitu.cpython-310.pyc and /dev/null differ diff --git a/services/__pycache__/service_vectorizer.cpython-310.pyc b/services/__pycache__/service_vectorizer.cpython-310.pyc deleted file mode 100755 index 3730130..0000000 Binary files a/services/__pycache__/service_vectorizer.cpython-310.pyc and /dev/null differ diff --git a/services/dispatch_service.py b/services/dispatch_service.py new file mode 100644 index 0000000..06f706b --- /dev/null +++ b/services/dispatch_service.py @@ -0,0 +1,49 @@ +import os +import logging +import httpx +from typing import Optional, Dict, Any + +logger = logging.getLogger("cs_agent") + +class DispatchService: + """ + 8006 即时派发服务 API 接入 (正式对齐版) + """ + + def __init__(self): + # 严格按照老大提供的最新对齐信息 + self.base_url = "http://1.12.50.92:8006" + self.api_key = "tuhui_dispatch_key_2026" + self.timeout = 10.0 + + async def assign_designer(self) -> Optional[str]: + """ + 调用 /assign 接口,一键获取当前可用的设计师名字 + """ + url = f"{self.base_url}/assign" + # 严格使用 X-API-Key 请求头和带下划线的 Key + headers = {"X-API-Key": self.api_key} + + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + designer = data.get("assigned_to") + logger.info(f"[Dispatch] 派单成功!设计师: {designer}") + return designer + else: + logger.warning(f"[Dispatch] 派单被拒: {data.get('reason')}") + elif response.status_code == 401: + logger.error(f"[Dispatch] 授权失败(401)!请检查 Key 是否为: {self.api_key}") + else: + logger.error(f"[Dispatch] API 异常,状态码: {response.status_code}") + except Exception as e: + logger.error(f"[Dispatch] 网络请求崩溃: {e}") + + return None + +# 全局单例 +dispatch_service = DispatchService() diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc index 2e6809e..7e6aba0 100755 Binary files a/utils/__pycache__/__init__.cpython-310.pyc and b/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/utils/__pycache__/api_cost_tracker.cpython-310.pyc b/utils/__pycache__/api_cost_tracker.cpython-310.pyc deleted file mode 100755 index 51c3732..0000000 Binary files a/utils/__pycache__/api_cost_tracker.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/content_filter.cpython-310.pyc b/utils/__pycache__/content_filter.cpython-310.pyc deleted file mode 100755 index 4c0d79d..0000000 Binary files a/utils/__pycache__/content_filter.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/daily_summary.cpython-310.pyc b/utils/__pycache__/daily_summary.cpython-310.pyc deleted file mode 100755 index bef795c..0000000 Binary files a/utils/__pycache__/daily_summary.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/designer_roster.cpython-310.pyc b/utils/__pycache__/designer_roster.cpython-310.pyc deleted file mode 100755 index b16b4fa..0000000 Binary files a/utils/__pycache__/designer_roster.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/health_check.cpython-310.pyc b/utils/__pycache__/health_check.cpython-310.pyc deleted file mode 100755 index 2e012d0..0000000 Binary files a/utils/__pycache__/health_check.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/image_queue.cpython-310.pyc b/utils/__pycache__/image_queue.cpython-310.pyc deleted file mode 100755 index 09a11bc..0000000 Binary files a/utils/__pycache__/image_queue.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/intent_analyzer.cpython-310.pyc b/utils/__pycache__/intent_analyzer.cpython-310.pyc deleted file mode 100755 index dc5276e..0000000 Binary files a/utils/__pycache__/intent_analyzer.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/service_base.cpython-310.pyc b/utils/__pycache__/service_base.cpython-310.pyc deleted file mode 100755 index 9c67748..0000000 Binary files a/utils/__pycache__/service_base.cpython-310.pyc and /dev/null differ diff --git a/utils/__pycache__/wechat_chat_log.cpython-310.pyc b/utils/__pycache__/wechat_chat_log.cpython-310.pyc deleted file mode 100755 index 037e15e..0000000 Binary files a/utils/__pycache__/wechat_chat_log.cpython-310.pyc and /dev/null differ