refactor: extract tool registration implementations from agent

This commit is contained in:
2026-03-01 16:14:29 +08:00
parent 872c44a0c0
commit 4b2d3347da
2 changed files with 681 additions and 668 deletions

679
core/agent_tools.py Normal file
View File

@@ -0,0 +1,679 @@
from __future__ import annotations
from typing import Any
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
def register_tools(agent) -> None:
"""注册所有 Tool让 Agent 可以主动调用。"""
@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", ""),
)
print(f"[Agent] Workflow 任务已创建 | 客户: {ctx.deps.from_id} | 比例: {result.get('aspect_ratio')} | 透视: {result.get('perspective')} | 图片: {image_url[:60]}...")
except Exception as e:
print(f"[Agent] Workflow 任务创建失败: {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}元)"

View File

@@ -81,7 +81,6 @@ load_dotenv()
from services.service_tuhui_upload import upload_to_tuhui from services.service_tuhui_upload import upload_to_tuhui
from core.workflow_router import get_workflow_router from core.workflow_router import get_workflow_router
from core.workflow_router import get_workflow_router from core.workflow_router import get_workflow_router
from db.customer_risk_db import risk_db
# ========== 企业微信通知 ========== # ========== 企业微信通知 ==========
_WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "") _WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "")
@@ -585,673 +584,8 @@ class CustomerServiceAgent:
def _register_tools(self): def _register_tools(self):
"""注册所有 Tool让 Agent 可以主动调用""" """注册所有 Tool让 Agent 可以主动调用"""
from core.agent_tools import register_tools
register_tools(self)
@self.agent.tool
async def analyze_image(ctx: RunContext[AgentDeps], 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", ""),
)
print(f"[Agent] Workflow 任务已创建 | 客户: {ctx.deps.from_id} | 比例: {result.get('aspect_ratio')} | 透视: {result.get('perspective')} | 图片: {image_url[:60]}...")
except Exception as e:
print(f"[Agent] Workflow 任务创建失败: {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 = self._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},请根据经验判断报价"
@self.agent.tool
async def get_customer_info(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def transfer_to_human(ctx: RunContext[AgentDeps]) -> str:
"""
转接人工客服。
遇到退款/投诉/情绪激动/复杂售后时调用。
"""
return "TRANSFER_REQUESTED"
@self.agent.tool
async def get_customer_risk_profile(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def mark_customer_risk(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def record_customer_risk_event(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def save_customer_note(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def update_contact_info(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def record_quote(
ctx: RunContext[AgentDeps],
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 = self.conversations.get(customer_id)
if state:
state.last_price = price
return f"已记录报价 {price}"
except Exception as e:
return f"记录失败: {e}"
@self.agent.tool
async def process_image_gemini(ctx: RunContext[AgentDeps], 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},请稍后重试或转人工"
@self.agent_pricing.tool
async def analyze_image_pricing(ctx: RunContext[AgentDeps], 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 = self._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}"
@self.agent_pricing.tool
async def record_quote_pricing(
ctx: RunContext[AgentDeps],
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}"
@self.agent_processing.tool
async def process_image_gemini_run(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
"""触发 Gemini 作图处理processing agent 专用入口)。"""
return await process_image_gemini(ctx, customer_id)
@self.agent_similar.tool
async def recommend_similar(ctx: RunContext[AgentDeps], hint: str = "") -> str:
try:
return "有类似款,拍下我发你参考图。"
except Exception as e:
return f"推荐失败: {e}"
@self.agent_order.tool
async def handle_order(ctx: RunContext[AgentDeps], 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 ""
@self.agent_risk.tool
async def risk_filter(ctx: RunContext[AgentDeps], text: str = "") -> str:
return "这类不做哈,政治/敏感内容都不接。"
@self.agent_risk.tool
async def get_customer_risk_profile_risk(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
return await get_customer_risk_profile(ctx, customer_id)
@self.agent_risk.tool
async def mark_customer_risk_risk(
ctx: RunContext[AgentDeps],
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,
)
@self.agent_risk.tool
async def record_customer_risk_event_risk(
ctx: RunContext[AgentDeps],
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,
)
@self.agent.tool
async def remove_background(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def perspective_correct(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def extract_pattern_tool(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def enhance_image_tool(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def color_match_tool(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def trim_border_tool(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def vectorize_to_eps_tool(ctx: RunContext[AgentDeps], 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}"
@self.agent.tool
async def meitu_enhance_tool(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def resize_image(
ctx: RunContext[AgentDeps],
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}"
@self.agent.tool
async def calculate_bulk_price(
ctx: RunContext[AgentDeps],
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}元)"
# 对话状态超过多少小时后重置(避免昨天的售后状态影响今天) # 对话状态超过多少小时后重置(避免昨天的售后状态影响今天)
CONVERSATION_TIMEOUT_HOURS = 12 CONVERSATION_TIMEOUT_HOURS = 12