Files
workphone-sdk/sdk/app/routers/cunke_bao.py

308 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
工作手机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)}
}