308 lines
11 KiB
Python
308 lines
11 KiB
Python
"""
|
||
工作手机SDK v3.0 - 存客宝对接路由
|
||
CunKeBao API Router
|
||
|
||
路由前缀:/api/v3/cunke-bao
|
||
功能:
|
||
- 配置管理(设置/查询 API Key 等)
|
||
- 手动触发线索上报
|
||
- Frida Hook 事件接收与转发
|
||
- 服务状态查询
|
||
|
||
作者:Manus AI
|
||
版本:v1.0.0
|
||
日期:2026-05-15
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||
from pydantic import BaseModel, Field
|
||
from typing import Optional, List, Dict, Any
|
||
import logging
|
||
import time
|
||
|
||
from services.cunke_bao_service import (
|
||
cunke_bao_service,
|
||
CunKeBaoConfig,
|
||
LeadData,
|
||
FriendAddEvent,
|
||
GroupChangeEvent,
|
||
)
|
||
|
||
router = APIRouter()
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ═══════════════════════════════════════════════
|
||
# 一、请求/响应模型
|
||
# ═══════════════════════════════════════════════
|
||
|
||
class CKBConfigRequest(BaseModel):
|
||
"""存客宝配置请求"""
|
||
api_key: str = Field(..., description="存客宝 API Key")
|
||
base_url: str = Field(
|
||
"https://ckbapi.quwanzhi.com/v1/api/scenarios",
|
||
description="存客宝接口地址"
|
||
)
|
||
timeout: int = Field(10, ge=1, le=60, description="请求超时秒数")
|
||
max_retries: int = Field(3, ge=1, le=10, description="最大重试次数")
|
||
rate_limit_per_minute: int = Field(60, ge=1, le=600, description="每分钟最大请求数")
|
||
enabled: bool = Field(True, description="是否启用")
|
||
|
||
|
||
class LeadReportRequest(BaseModel):
|
||
"""线索上报请求"""
|
||
phone: str = Field("", description="手机号")
|
||
name: str = Field("", description="姓名")
|
||
source: str = Field("微信", description="来源渠道")
|
||
wechat_id: str = Field("", description="微信号")
|
||
wechat_nickname: str = Field("", description="微信昵称")
|
||
wechat_remark: str = Field("", description="备注名")
|
||
tags: List[str] = Field(default_factory=list, description="标签列表")
|
||
group_name: str = Field("", description="所在群名")
|
||
device_id: str = Field("", description="设备ID")
|
||
extra: Dict[str, Any] = Field(default_factory=dict, description="扩展字段")
|
||
|
||
|
||
class FriendAddEventRequest(BaseModel):
|
||
"""新好友添加事件请求(来自 Frida Hook)"""
|
||
device_id: str = Field(..., description="设备ID")
|
||
wechat_id: str = Field(..., description="微信号")
|
||
nickname: str = Field("", description="昵称")
|
||
avatar: str = Field("", description="头像URL")
|
||
source: str = Field("微信添加", description="来源")
|
||
|
||
|
||
class GroupChangeEventRequest(BaseModel):
|
||
"""群变动事件请求(来自 Frida Hook)"""
|
||
device_id: str = Field(..., description="设备ID")
|
||
group_id: str = Field(..., description="群ID")
|
||
group_name: str = Field("", description="群名称")
|
||
event_type: str = Field(..., description="事件类型: member_join/member_leave/group_create")
|
||
member_wechat_id: str = Field("", description="成员微信号")
|
||
member_nickname: str = Field("", description="成员昵称")
|
||
|
||
|
||
class BatchContactRequest(BaseModel):
|
||
"""批量联系人上报请求"""
|
||
device_id: str = Field(..., description="设备ID")
|
||
source: str = Field("微信通讯录", description="来源")
|
||
contacts: List[Dict[str, Any]] = Field(..., description="联系人列表")
|
||
|
||
|
||
# ═══════════════════════════════════════════════
|
||
# 二、配置管理接口
|
||
# ═══════════════════════════════════════════════
|
||
|
||
@router.post("/cunke-bao/config", tags=["存客宝对接"])
|
||
async def set_config(req: CKBConfigRequest):
|
||
"""
|
||
设置存客宝对接配置
|
||
|
||
配置 API Key、接口地址、超时等参数,并启用/禁用服务。
|
||
"""
|
||
config = CunKeBaoConfig(
|
||
api_key=req.api_key,
|
||
base_url=req.base_url,
|
||
timeout=req.timeout,
|
||
max_retries=req.max_retries,
|
||
rate_limit_per_minute=req.rate_limit_per_minute,
|
||
enabled=req.enabled,
|
||
)
|
||
cunke_bao_service.update_config(config)
|
||
return {
|
||
"code": 200,
|
||
"msg": "配置已更新",
|
||
"data": {
|
||
"enabled": config.enabled,
|
||
"base_url": config.base_url,
|
||
"api_key": "***" + config.api_key[-4:] if len(config.api_key) > 4 else "***",
|
||
}
|
||
}
|
||
|
||
|
||
@router.get("/cunke-bao/config", tags=["存客宝对接"])
|
||
async def get_config():
|
||
"""查询当前存客宝配置(API Key 脱敏显示)"""
|
||
cfg = cunke_bao_service.config
|
||
return {
|
||
"code": 200,
|
||
"data": {
|
||
"enabled": cfg.enabled,
|
||
"base_url": cfg.base_url,
|
||
"api_key_set": bool(cfg.api_key),
|
||
"api_key_preview": ("***" + cfg.api_key[-4:]) if len(cfg.api_key) > 4 else ("***" if cfg.api_key else ""),
|
||
"timeout": cfg.timeout,
|
||
"max_retries": cfg.max_retries,
|
||
"rate_limit_per_minute": cfg.rate_limit_per_minute,
|
||
}
|
||
}
|
||
|
||
|
||
@router.get("/cunke-bao/stats", tags=["存客宝对接"])
|
||
async def get_stats():
|
||
"""查询存客宝服务统计信息"""
|
||
return {
|
||
"code": 200,
|
||
"data": cunke_bao_service.get_stats()
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════
|
||
# 三、线索上报接口
|
||
# ═══════════════════════════════════════════════
|
||
|
||
@router.post("/cunke-bao/report-lead", tags=["存客宝对接"])
|
||
async def report_lead(req: LeadReportRequest):
|
||
"""
|
||
手动上报线索到存客宝
|
||
|
||
可用于测试或特殊场景下的手动触发。
|
||
"""
|
||
lead = LeadData(
|
||
phone=req.phone,
|
||
name=req.name,
|
||
source=req.source,
|
||
wechat_id=req.wechat_id,
|
||
wechat_nickname=req.wechat_nickname,
|
||
wechat_remark=req.wechat_remark,
|
||
tags=req.tags,
|
||
group_name=req.group_name,
|
||
device_id=req.device_id,
|
||
extra=req.extra,
|
||
)
|
||
result = await cunke_bao_service.report_lead(lead)
|
||
return result
|
||
|
||
|
||
@router.post("/cunke-bao/batch-contacts", tags=["存客宝对接"])
|
||
async def batch_report_contacts(req: BatchContactRequest, background_tasks: BackgroundTasks):
|
||
"""
|
||
批量上报联系人到存客宝
|
||
|
||
通常由 Frida Hook 获取通讯录后调用,支持后台异步处理。
|
||
"""
|
||
if not req.contacts:
|
||
raise HTTPException(400, "contacts 列表不能为空")
|
||
|
||
if len(req.contacts) > 500:
|
||
raise HTTPException(400, "单次批量上报最多 500 条")
|
||
|
||
# 超过 10 条时异步处理,避免请求超时
|
||
if len(req.contacts) > 10:
|
||
background_tasks.add_task(
|
||
cunke_bao_service.batch_report_contacts,
|
||
req.contacts,
|
||
req.device_id,
|
||
req.source,
|
||
)
|
||
return {
|
||
"code": 200,
|
||
"msg": f"已接收 {len(req.contacts)} 条联系人,正在后台处理",
|
||
"data": {"total": len(req.contacts), "async": True}
|
||
}
|
||
else:
|
||
result = await cunke_bao_service.batch_report_contacts(
|
||
req.contacts, req.device_id, req.source
|
||
)
|
||
return {"code": 200, "msg": "批量上报完成", "data": result}
|
||
|
||
|
||
# ═══════════════════════════════════════════════
|
||
# 四、Frida Hook 事件接收接口
|
||
# ═══════════════════════════════════════════════
|
||
|
||
@router.post("/cunke-bao/hook/friend-add", tags=["存客宝对接", "Frida Hook"])
|
||
async def hook_friend_add(req: FriendAddEventRequest):
|
||
"""
|
||
接收 Frida Hook 上报的新好友添加事件
|
||
|
||
设备端 Agent 在检测到新好友添加时调用此接口,
|
||
服务端将自动转换为存客宝线索并上报。
|
||
"""
|
||
event = FriendAddEvent(
|
||
device_id=req.device_id,
|
||
wechat_id=req.wechat_id,
|
||
nickname=req.nickname,
|
||
avatar=req.avatar,
|
||
source=req.source,
|
||
)
|
||
logger.info(f"[CKB] 收到新好友事件: device={req.device_id}, wechat_id={req.wechat_id}")
|
||
result = await cunke_bao_service.report_friend_add(event)
|
||
return result
|
||
|
||
|
||
@router.post("/cunke-bao/hook/group-change", tags=["存客宝对接", "Frida Hook"])
|
||
async def hook_group_change(req: GroupChangeEventRequest):
|
||
"""
|
||
接收 Frida Hook 上报的群变动事件
|
||
|
||
设备端 Agent 在检测到群成员变动时调用此接口,
|
||
服务端将自动处理并按需上报到存客宝。
|
||
"""
|
||
event = GroupChangeEvent(
|
||
device_id=req.device_id,
|
||
group_id=req.group_id,
|
||
group_name=req.group_name,
|
||
event_type=req.event_type,
|
||
member_wechat_id=req.member_wechat_id,
|
||
member_nickname=req.member_nickname,
|
||
)
|
||
logger.info(f"[CKB] 收到群变动事件: type={req.event_type}, group={req.group_name}")
|
||
result = await cunke_bao_service.report_group_change(event)
|
||
return result
|
||
|
||
|
||
@router.post("/cunke-bao/hook/batch-events", tags=["存客宝对接", "Frida Hook"])
|
||
async def hook_batch_events(
|
||
events: List[Dict[str, Any]],
|
||
background_tasks: BackgroundTasks
|
||
):
|
||
"""
|
||
批量接收 Frida Hook 事件
|
||
|
||
设备端可将多个事件打包一次性上报,减少网络请求次数。
|
||
支持的事件类型:friend_add / group_change / contact_update
|
||
"""
|
||
if not events:
|
||
raise HTTPException(400, "events 列表不能为空")
|
||
|
||
async def process_events():
|
||
for evt in events:
|
||
evt_type = evt.get("type", "")
|
||
try:
|
||
if evt_type == "friend_add":
|
||
event = FriendAddEvent(
|
||
device_id=evt.get("device_id", ""),
|
||
wechat_id=evt.get("wechat_id", ""),
|
||
nickname=evt.get("nickname", ""),
|
||
avatar=evt.get("avatar", ""),
|
||
source=evt.get("source", "微信添加"),
|
||
)
|
||
await cunke_bao_service.report_friend_add(event)
|
||
elif evt_type == "group_change":
|
||
event = GroupChangeEvent(
|
||
device_id=evt.get("device_id", ""),
|
||
group_id=evt.get("group_id", ""),
|
||
group_name=evt.get("group_name", ""),
|
||
event_type=evt.get("event_type", ""),
|
||
member_wechat_id=evt.get("member_wechat_id", ""),
|
||
member_nickname=evt.get("member_nickname", ""),
|
||
)
|
||
await cunke_bao_service.report_group_change(event)
|
||
elif evt_type == "contact_update":
|
||
contacts = evt.get("contacts", [])
|
||
device_id = evt.get("device_id", "")
|
||
if contacts and device_id:
|
||
await cunke_bao_service.batch_report_contacts(
|
||
contacts, device_id, "微信通讯录更新"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"[CKB] 处理事件失败 type={evt_type}: {e}")
|
||
|
||
background_tasks.add_task(process_events)
|
||
return {
|
||
"code": 200,
|
||
"msg": f"已接收 {len(events)} 个事件,正在后台处理",
|
||
"data": {"total": len(events)}
|
||
}
|