964 lines
33 KiB
Python
964 lines
33 KiB
Python
"""
|
||
微信全量操作路由 — 补全统一路由缺失的端点
|
||
==========================================
|
||
|
||
对齐 agent 端 128 个 RPC 方法,通过 WechatSkillV2 统一分发。
|
||
所有操作通过 WiFi Frida 无线执行,无需 USB。
|
||
|
||
新增端点(统一路由已有的不重复):
|
||
- 视频号操作(6个)
|
||
- 小程序操作(3个)
|
||
- 收藏管理(3个)
|
||
- 公众号操作(4个)
|
||
- 账号安全(8个)
|
||
- 支付操作(6个)
|
||
- 浮窗管理(2个)
|
||
- 表情管理(2个)
|
||
- 搜索(1个)
|
||
- 设备信息(3个)
|
||
"""
|
||
from __future__ import annotations
|
||
import logging
|
||
from typing import Optional, List, Any, Dict
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
|
||
router = APIRouter(prefix="/wechat", tags=["微信全量操作"])
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ============================================================
|
||
# § 1 请求模型
|
||
# ============================================================
|
||
|
||
class WechatBaseRequest(BaseModel):
|
||
device_id: str
|
||
timeout: int = 30
|
||
|
||
class WxidRequest(WechatBaseRequest):
|
||
wxid: str
|
||
|
||
class SearchRequest(WechatBaseRequest):
|
||
keyword: str
|
||
limit: int = 50
|
||
|
||
class ChannelVideoRequest(WechatBaseRequest):
|
||
video_id: str
|
||
|
||
class ChannelCommentRequest(WechatBaseRequest):
|
||
video_id: str
|
||
comment: str
|
||
|
||
class ChannelFollowRequest(WechatBaseRequest):
|
||
channel_id: str
|
||
|
||
class ShareChannelRequest(WechatBaseRequest):
|
||
video_id: str
|
||
to_id: str
|
||
|
||
class MiniProgramRequest(WechatBaseRequest):
|
||
app_id: str
|
||
path: str = ""
|
||
|
||
class ShareMiniProgramRequest(WechatBaseRequest):
|
||
app_id: str
|
||
to_id: str
|
||
title: str = ""
|
||
|
||
class FavoriteRequest(WechatBaseRequest):
|
||
msg_svr_id: str = ""
|
||
local_id: str = ""
|
||
type: str = "message"
|
||
|
||
class OfficialAccountRequest(WechatBaseRequest):
|
||
account_id: str
|
||
|
||
class OfficialAccountArticlesRequest(WechatBaseRequest):
|
||
account_id: str
|
||
limit: int = 10
|
||
|
||
class PasswordRequest(WechatBaseRequest):
|
||
old_password: str
|
||
new_password: str
|
||
|
||
class PhoneRequest(WechatBaseRequest):
|
||
phone: str
|
||
|
||
class DeviceRemoveRequest(WechatBaseRequest):
|
||
target_device_id: str
|
||
|
||
class ToggleRequest(WechatBaseRequest):
|
||
enable: bool = True
|
||
|
||
class RedPacketRequest(WechatBaseRequest):
|
||
to_id: str
|
||
amount: str
|
||
message: str = "恭喜发财"
|
||
confirm: bool = False
|
||
dry_run: bool = True
|
||
|
||
class TransferRequest(WechatBaseRequest):
|
||
to_id: str
|
||
amount: str
|
||
description: str = ""
|
||
|
||
class ReceiveRequest(WechatBaseRequest):
|
||
msg_svr_id: str
|
||
confirm: bool = False
|
||
dry_run: bool = True
|
||
|
||
class FloatRequest(WechatBaseRequest):
|
||
wxid: str
|
||
|
||
class EmojiSendRequest(WechatBaseRequest):
|
||
to_id: str
|
||
emoji_md5: str
|
||
|
||
class EmojiAddRequest(WechatBaseRequest):
|
||
image_path: str
|
||
|
||
class ProfileSetRequest(WechatBaseRequest):
|
||
value: str
|
||
|
||
class AccountProfileUpdateRequest(WechatBaseRequest):
|
||
"""个人微信号统一维护请求;只传需要修改的字段。"""
|
||
nickname: Optional[str] = Field(None, max_length=32, description="微信昵称")
|
||
signature: Optional[str] = Field(None, max_length=120, description="个性签名")
|
||
avatar_path: Optional[str] = Field(None, description="手机可读取的头像绝对路径")
|
||
sex: Optional[str] = Field(None, pattern="^(0|1|2|unknown|male|female)$", description="0未知/1男/2女")
|
||
region: Optional[str] = Field(None, max_length=80, description="地区或我的地址,如福建 厦门")
|
||
status: Optional[str] = Field(None, max_length=80, description="状态/来电名/What’s Up")
|
||
phone: Optional[str] = Field(None, pattern=r"^1[3-9]\d{9}$", description="待绑定手机号")
|
||
wechat_id: Optional[str] = Field(None, min_length=6, max_length=20, description="新微信号")
|
||
dry_run: bool = Field(True, description="true仅生成执行计划,false执行可直接下发的修改")
|
||
confirm_sensitive: bool = Field(False, description="绑定手机号等敏感项的显式确认")
|
||
|
||
model_config = {
|
||
"json_schema_extra": {
|
||
"examples": [{
|
||
"device_id": "xgfe65eimrrofyws",
|
||
"nickname": "卡若",
|
||
"signature": "自律、坚持、长期主义",
|
||
"region": "福建 厦门",
|
||
"status": "专注私域运营",
|
||
"dry_run": True,
|
||
"confirm_sensitive": False,
|
||
}]
|
||
}
|
||
}
|
||
|
||
class PrivacyRequest(WechatBaseRequest):
|
||
setting: str
|
||
value: bool = True
|
||
|
||
class NotificationRequest(WechatBaseRequest):
|
||
type: str = "all"
|
||
enable: bool = True
|
||
|
||
class ChatBgRequest(WechatBaseRequest):
|
||
wxid: str
|
||
image_path: str = ""
|
||
|
||
class DndRequest(WechatBaseRequest):
|
||
wxid: str
|
||
enable: bool = True
|
||
|
||
class PinChatRequest(WechatBaseRequest):
|
||
wxid: str
|
||
pin: bool = True
|
||
|
||
class QrScanRequest(WechatBaseRequest):
|
||
image_path: str = ""
|
||
|
||
class LabelContactRequest(WechatBaseRequest):
|
||
wxid: str
|
||
label_ids: List[str]
|
||
|
||
|
||
# ============================================================
|
||
# § 2 辅助函数
|
||
# ============================================================
|
||
|
||
async def _get_skill(device_id: str):
|
||
"""获取 WechatSkillV2 实例"""
|
||
# 服务端与设备端都存在 skills 包;Frida 路由可能把 agent/ 放到
|
||
# sys.path 首位,直接 `from skills...` 会误载入设备端包。使用服务端
|
||
# 固定文件路径加载,保证 Hook 状态接口稳定。
|
||
import importlib.util
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
module_name = "workphone_server_wechat_skill_v2"
|
||
module = sys.modules.get(module_name)
|
||
if module is None:
|
||
skill_path = Path(__file__).resolve().parents[1] / "skills" / "wechat" / "skill_v2.py"
|
||
spec = importlib.util.spec_from_file_location(module_name, skill_path)
|
||
if spec is None or spec.loader is None:
|
||
raise RuntimeError(f"服务端微信 Skill 文件不可加载: {skill_path}")
|
||
module = importlib.util.module_from_spec(spec)
|
||
sys.modules[module_name] = module
|
||
spec.loader.exec_module(module)
|
||
WechatSkillV2 = module.WechatSkillV2
|
||
return WechatSkillV2(device_id=device_id)
|
||
|
||
|
||
# ============================================================
|
||
# § 3 视频号操作(H27)
|
||
# ============================================================
|
||
|
||
@router.get("/channels/browse", response_model=dict)
|
||
async def browse_channels(device_id: str, limit: int = 10):
|
||
"""浏览视频号"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.browse_channels(limit)
|
||
|
||
@router.post("/channels/like", response_model=dict)
|
||
async def like_channel_video(req: ChannelVideoRequest):
|
||
"""视频号点赞"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.like_channel_video(req.video_id)
|
||
|
||
@router.post("/channels/comment", response_model=dict)
|
||
async def comment_channel_video(req: ChannelCommentRequest):
|
||
"""视频号评论"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.comment_channel_video(req.video_id, req.comment)
|
||
|
||
@router.post("/channels/follow", response_model=dict)
|
||
async def follow_channel(req: ChannelFollowRequest):
|
||
"""关注视频号"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.follow_channel(req.channel_id)
|
||
|
||
@router.post("/channels/unfollow", response_model=dict)
|
||
async def unfollow_channel(req: ChannelFollowRequest):
|
||
"""取消关注视频号"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.unfollow_channel(req.channel_id)
|
||
|
||
@router.post("/channels/share", response_model=dict)
|
||
async def share_channel_video(req: ShareChannelRequest):
|
||
"""分享视频号视频"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.share_channel_video(req.video_id, req.to_id)
|
||
|
||
|
||
# ============================================================
|
||
# § 4 小程序操作(H32)
|
||
# ============================================================
|
||
|
||
@router.post("/mini-program/open", response_model=dict)
|
||
async def open_mini_program(req: MiniProgramRequest):
|
||
"""打开小程序"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.open_mini_program(req.app_id, req.path)
|
||
|
||
@router.get("/mini-program/recent", response_model=dict)
|
||
async def get_recent_mini_programs(device_id: str):
|
||
"""获取最近使用的小程序"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_recent_mini_programs()
|
||
|
||
@router.post("/mini-program/share", response_model=dict)
|
||
async def share_mini_program(req: ShareMiniProgramRequest):
|
||
"""分享小程序"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.share_mini_program(req.app_id, req.to_id, req.title)
|
||
|
||
|
||
# ============================================================
|
||
# § 5 收藏管理(H29)
|
||
# ============================================================
|
||
|
||
@router.get("/favorites", response_model=dict)
|
||
async def get_favorites(device_id: str, limit: int = 50):
|
||
"""获取收藏列表"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_favorites(limit)
|
||
|
||
@router.post("/favorites/add", response_model=dict)
|
||
async def add_favorite(req: FavoriteRequest):
|
||
"""添加收藏"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.add_favorite(req.msg_svr_id, req.type)
|
||
|
||
@router.delete("/favorites/{local_id}", response_model=dict)
|
||
async def delete_favorite(local_id: str, device_id: str = ""):
|
||
"""删除收藏"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.delete_favorite(local_id)
|
||
|
||
|
||
# ============================================================
|
||
# § 6 公众号操作(H36)
|
||
# ============================================================
|
||
|
||
@router.get("/official-accounts", response_model=dict)
|
||
async def get_official_accounts(device_id: str, limit: int = 100):
|
||
"""获取关注的公众号列表"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_official_accounts(limit)
|
||
|
||
@router.post("/official-accounts/follow", response_model=dict)
|
||
async def follow_official_account(req: OfficialAccountRequest):
|
||
"""关注公众号"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.follow_official_account(req.account_id)
|
||
|
||
@router.post("/official-accounts/unfollow", response_model=dict)
|
||
async def unfollow_official_account(req: OfficialAccountRequest):
|
||
"""取消关注公众号"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.unfollow_official_account(req.account_id)
|
||
|
||
@router.get("/official-accounts/{account_id}/articles", response_model=dict)
|
||
async def get_official_account_articles(account_id: str, device_id: str = "", limit: int = 10):
|
||
"""获取公众号文章"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_official_account_articles(account_id, limit)
|
||
|
||
|
||
# ============================================================
|
||
# § 7 账号安全(H24)
|
||
# ============================================================
|
||
|
||
@router.post("/security/unblock", response_model=dict)
|
||
async def unblock_self(req: WechatBaseRequest):
|
||
"""自助解封"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.unblock_self()
|
||
|
||
@router.post("/security/change-password", response_model=dict)
|
||
async def change_password(req: PasswordRequest):
|
||
"""修改密码"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.change_password(req.old_password, req.new_password)
|
||
|
||
@router.post("/security/bind-phone", response_model=dict)
|
||
async def bind_phone(req: PhoneRequest):
|
||
"""绑定手机号"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.bind_phone(req.phone)
|
||
|
||
@router.post("/security/unbind-phone", response_model=dict)
|
||
async def unbind_phone(req: WechatBaseRequest):
|
||
"""解绑手机号"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.unbind_phone()
|
||
|
||
@router.get("/security/login-devices", response_model=dict)
|
||
async def get_login_devices(device_id: str):
|
||
"""获取登录设备列表"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_login_devices()
|
||
|
||
@router.delete("/security/login-device", response_model=dict)
|
||
async def remove_login_device(req: DeviceRemoveRequest):
|
||
"""移除登录设备"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.remove_login_device(req.target_device_id)
|
||
|
||
@router.post("/security/fingerprint", response_model=dict)
|
||
async def enable_fingerprint(req: ToggleRequest):
|
||
"""启用/禁用指纹"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.enable_fingerprint(req.enable)
|
||
|
||
@router.post("/security/account-protection", response_model=dict)
|
||
async def set_account_protection(req: ToggleRequest):
|
||
"""设置账号保护"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_account_protection(req.enable)
|
||
|
||
|
||
# ============================================================
|
||
# § 8 支付操作(H25)
|
||
# ============================================================
|
||
|
||
@router.post("/pay/red-packet", response_model=dict)
|
||
async def send_red_packet(req: RedPacketRequest):
|
||
"""发红包;默认返回确认前演练结果。"""
|
||
if req.dry_run or not req.confirm:
|
||
return {
|
||
"code": 200,
|
||
"success": True,
|
||
"verified": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"action": "send_red_packet",
|
||
"to_id": req.to_id,
|
||
"amount": req.amount,
|
||
}
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.send_red_packet(req.to_id, req.amount, req.message)
|
||
|
||
@router.post("/pay/red-packet/receive", response_model=dict)
|
||
async def receive_red_packet(req: ReceiveRequest):
|
||
"""收红包;默认返回确认前演练结果。"""
|
||
if req.dry_run or not req.confirm:
|
||
return {
|
||
"code": 200,
|
||
"success": True,
|
||
"verified": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"action": "receive_red_packet",
|
||
"msg_svr_id": req.msg_svr_id,
|
||
}
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.receive_red_packet(req.msg_svr_id)
|
||
|
||
@router.post("/pay/transfer", response_model=dict)
|
||
async def send_transfer(req: TransferRequest):
|
||
"""转账"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.send_transfer(req.to_id, req.amount, req.description)
|
||
|
||
@router.post("/pay/transfer/receive", response_model=dict)
|
||
async def receive_transfer(req: ReceiveRequest):
|
||
"""收转账"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.receive_transfer(req.msg_svr_id)
|
||
|
||
@router.get("/pay/balance", response_model=dict)
|
||
async def get_wallet_balance(device_id: str):
|
||
"""获取钱包余额"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_wallet_balance()
|
||
|
||
@router.get("/pay/transactions", response_model=dict)
|
||
async def get_transaction_history(device_id: str, limit: int = 20):
|
||
"""获取交易记录"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_transaction_history(limit)
|
||
|
||
|
||
# ============================================================
|
||
# § 9 浮窗管理(H38)
|
||
# ============================================================
|
||
|
||
@router.post("/float/add", response_model=dict)
|
||
async def add_to_float(req: FloatRequest):
|
||
"""添加到浮窗"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.add_to_float(req.wxid)
|
||
|
||
@router.post("/float/remove", response_model=dict)
|
||
async def remove_from_float(req: FloatRequest):
|
||
"""从浮窗移除"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.remove_from_float(req.wxid)
|
||
|
||
|
||
# ============================================================
|
||
# § 10 表情管理(H37)
|
||
# ============================================================
|
||
|
||
@router.post("/emoji/send-custom", response_model=dict)
|
||
async def send_emoji_custom(req: EmojiSendRequest):
|
||
"""发送自定义表情"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.send_emoji(req.to_id, req.emoji_md5)
|
||
|
||
@router.post("/emoji/add-custom", response_model=dict)
|
||
async def add_custom_emoji(req: EmojiAddRequest):
|
||
"""添加自定义表情"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.add_custom_emoji(req.image_path)
|
||
|
||
|
||
# ============================================================
|
||
# § 11 账号管理(H23)
|
||
# ============================================================
|
||
|
||
@router.get("/profile", response_model=dict)
|
||
async def get_profile(device_id: str):
|
||
"""获取个人资料"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_profile()
|
||
|
||
@router.get("/account/status", response_model=dict)
|
||
async def check_account_status(device_id: str):
|
||
"""检查账号状态"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.check_account_status()
|
||
|
||
@router.post("/profile/nickname", response_model=dict)
|
||
async def set_nickname(req: ProfileSetRequest):
|
||
"""设置昵称"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_nickname(req.value)
|
||
|
||
@router.post("/profile/signature", response_model=dict)
|
||
async def set_signature(req: ProfileSetRequest):
|
||
"""设置签名"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_signature(req.value)
|
||
|
||
@router.post("/profile/avatar", response_model=dict)
|
||
async def set_avatar(req: ProfileSetRequest):
|
||
"""设置头像"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_avatar(req.value)
|
||
|
||
@router.post("/profile/sex", response_model=dict)
|
||
async def set_sex(req: ProfileSetRequest):
|
||
"""设置性别"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_sex(req.value)
|
||
|
||
@router.post("/profile/region", response_model=dict)
|
||
async def set_region(req: ProfileSetRequest):
|
||
"""设置地区"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_region(req.value)
|
||
|
||
@router.post("/profile/status", response_model=dict)
|
||
async def set_what_up(req: ProfileSetRequest):
|
||
"""设置状态"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_what_up(req.value)
|
||
|
||
|
||
@router.get(
|
||
"/account/profile",
|
||
response_model=dict,
|
||
summary="读取个人微信号完整资料",
|
||
)
|
||
async def get_account_profile(device_id: str):
|
||
"""无界面读取昵称、微信内部ID、手机号、签名、性别、地区及版本。"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_profile()
|
||
|
||
|
||
@router.get(
|
||
"/account/maintenance/capabilities",
|
||
response_model=dict,
|
||
summary="获取个人微信号可维护能力",
|
||
)
|
||
async def get_account_maintenance_capabilities():
|
||
"""返回所有资料字段的读取、写入、验证方式及细分接口。"""
|
||
return {
|
||
"code": 200,
|
||
"success": True,
|
||
"data": {
|
||
"read_endpoint": "GET /api/v3/wechat/account/profile",
|
||
"update_endpoint": "POST /api/v3/wechat/account/profile/update",
|
||
"qr_endpoint": "GET /api/v3/wechat/account/qr",
|
||
"fields": {
|
||
"nickname": {"mode": "write", "endpoint": "/profile/nickname", "action": "set_nickname"},
|
||
"signature": {"mode": "write", "endpoint": "/profile/signature", "action": "set_signature"},
|
||
"avatar_path": {"mode": "write", "endpoint": "/profile/avatar", "action": "set_avatar"},
|
||
"sex": {"mode": "write", "endpoint": "/profile/sex", "action": "set_sex"},
|
||
"region": {"mode": "write", "endpoint": "/profile/region", "action": "set_region"},
|
||
"status": {"mode": "write", "endpoint": "/profile/status", "action": "set_what_up"},
|
||
"phone": {
|
||
"mode": "verification_required",
|
||
"endpoint": "/security/bind-phone",
|
||
"action": "bind_phone",
|
||
},
|
||
"wechat_id": {
|
||
"mode": "verification_required",
|
||
"note": "微信号修改受微信次数和账号状态约束,进入微信验证流程后确认",
|
||
},
|
||
"qr_code": {"mode": "read", "endpoint": "/qr/my", "action": "generate_my_qr_code"},
|
||
},
|
||
},
|
||
}
|
||
|
||
|
||
@router.post(
|
||
"/account/profile/update",
|
||
response_model=dict,
|
||
summary="统一维护个人微信号资料",
|
||
description=(
|
||
"无界面批量维护昵称、签名、头像、性别、地区、状态和手机号。"
|
||
"dry_run=true返回计划;false逐项通过Frida执行并返回真实回执。"
|
||
),
|
||
)
|
||
async def update_account_profile(req: AccountProfileUpdateRequest):
|
||
"""按字段编排个人资料修改,保留逐项回执。"""
|
||
operations = [
|
||
("nickname", req.nickname, "set_nickname"),
|
||
("signature", req.signature, "set_signature"),
|
||
("avatar_path", req.avatar_path, "set_avatar"),
|
||
("sex", req.sex, "set_sex"),
|
||
("region", req.region, "set_region"),
|
||
("status", req.status, "set_what_up"),
|
||
]
|
||
plan = [
|
||
{"field": field, "value": value, "action": action}
|
||
for field, value, action in operations if value is not None
|
||
]
|
||
if req.phone is not None:
|
||
plan.append({
|
||
"field": "phone",
|
||
"value": req.phone,
|
||
"action": "bind_phone",
|
||
"requires_confirmation": True,
|
||
})
|
||
if req.wechat_id is not None:
|
||
plan.append({
|
||
"field": "wechat_id",
|
||
"value": req.wechat_id,
|
||
"action": "wechat_verification_flow",
|
||
"requires_confirmation": True,
|
||
})
|
||
if req.dry_run:
|
||
return {
|
||
"code": 200,
|
||
"success": True,
|
||
"dry_run": True,
|
||
"data": {"total": len(plan), "plan": plan},
|
||
"channel_used": "none",
|
||
}
|
||
|
||
skill = await _get_skill(req.device_id)
|
||
results: List[Dict[str, Any]] = []
|
||
method_map = {
|
||
"set_nickname": skill.set_nickname,
|
||
"set_signature": skill.set_signature,
|
||
"set_avatar": skill.set_avatar,
|
||
"set_sex": skill.set_sex,
|
||
"set_region": skill.set_region,
|
||
"set_what_up": skill.set_what_up,
|
||
}
|
||
for field, value, action in operations:
|
||
if value is None:
|
||
continue
|
||
result = await method_map[action](value)
|
||
results.append({"field": field, "action": action, "result": result})
|
||
if req.phone is not None:
|
||
if req.confirm_sensitive:
|
||
result = await skill.bind_phone(req.phone)
|
||
results.append({"field": "phone", "action": "bind_phone", "result": result})
|
||
else:
|
||
results.append({
|
||
"field": "phone",
|
||
"action": "bind_phone",
|
||
"result": {"success": False, "status": "confirmation_required"},
|
||
})
|
||
if req.wechat_id is not None:
|
||
results.append({
|
||
"field": "wechat_id",
|
||
"action": "wechat_verification_flow",
|
||
"result": {
|
||
"success": False,
|
||
"status": "verification_required",
|
||
"requested_wechat_id": req.wechat_id,
|
||
},
|
||
})
|
||
succeeded = sum(bool((item["result"] or {}).get("success")) for item in results)
|
||
return {
|
||
"code": 200,
|
||
"success": succeeded == len(results),
|
||
"dry_run": False,
|
||
"data": {
|
||
"total": len(results),
|
||
"succeeded": succeeded,
|
||
"failed": len(results) - succeeded,
|
||
"results": results,
|
||
},
|
||
"channel_used": "server/frida",
|
||
}
|
||
|
||
|
||
@router.get(
|
||
"/account/qr",
|
||
response_model=dict,
|
||
summary="获取个人微信二维码",
|
||
)
|
||
async def get_account_qr(device_id: str):
|
||
"""获取当前个人微信号二维码回执。"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.generate_my_qr_code()
|
||
|
||
|
||
# ============================================================
|
||
# § 12 搜索(H31)
|
||
# ============================================================
|
||
|
||
@router.get("/search", response_model=dict)
|
||
async def global_search(device_id: str, keyword: str, limit: int = 30):
|
||
"""全局搜索"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.global_search(keyword, limit)
|
||
|
||
|
||
# ============================================================
|
||
# § 13 设置(H30)
|
||
# ============================================================
|
||
|
||
@router.post("/settings/privacy", response_model=dict)
|
||
async def set_privacy(req: PrivacyRequest):
|
||
"""设置隐私"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_privacy(req.setting, req.value)
|
||
|
||
@router.post("/settings/notification", response_model=dict)
|
||
async def set_notification(req: NotificationRequest):
|
||
"""设置通知"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_notification(req.type, req.enable)
|
||
|
||
@router.post("/settings/clear-history", response_model=dict)
|
||
async def clear_chat_history(req: WxidRequest):
|
||
"""清除聊天记录"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.clear_chat_history(req.wxid)
|
||
|
||
@router.post("/settings/chat-background", response_model=dict)
|
||
async def set_chat_background(req: ChatBgRequest):
|
||
"""设置聊天背景"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_chat_background(req.wxid, req.image_path)
|
||
|
||
@router.post("/settings/dnd", response_model=dict)
|
||
async def set_do_not_disturb(req: DndRequest):
|
||
"""设置免打扰"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_do_not_disturb(req.wxid, req.enable)
|
||
|
||
@router.post("/settings/pin-chat", response_model=dict)
|
||
async def pin_chat(req: PinChatRequest):
|
||
"""置顶聊天"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.pin_chat(req.wxid, req.pin)
|
||
|
||
|
||
# ============================================================
|
||
# § 14 二维码(H26)
|
||
# ============================================================
|
||
|
||
@router.post("/qr/scan", response_model=dict)
|
||
async def scan_qr_code(req: QrScanRequest):
|
||
"""扫描二维码"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.scan_qr_code(req.image_path)
|
||
|
||
@router.get("/qr/my", response_model=dict)
|
||
async def generate_my_qr_code(device_id: str):
|
||
"""生成我的二维码"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.generate_my_qr_code()
|
||
|
||
@router.get("/qr/group/{group_id}", response_model=dict)
|
||
async def generate_group_qr_code(group_id: str, device_id: str = ""):
|
||
"""生成群二维码"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.generate_group_qr_code(group_id)
|
||
|
||
|
||
# ============================================================
|
||
# § 15 设备信息(H39)
|
||
# ============================================================
|
||
|
||
@router.get("/device-info", response_model=dict)
|
||
async def get_device_info(device_id: str):
|
||
"""获取设备信息"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_device_info()
|
||
|
||
@router.get("/storage-info", response_model=dict)
|
||
async def get_storage_info(device_id: str):
|
||
"""获取存储信息"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_storage_info()
|
||
|
||
@router.get("/network-info", response_model=dict)
|
||
async def get_network_info(device_id: str):
|
||
"""获取网络信息"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_network_info()
|
||
|
||
|
||
# ============================================================
|
||
# § 16 标签补全(H28)
|
||
# ============================================================
|
||
|
||
@router.post("/tag/set-contact", response_model=dict)
|
||
async def set_contact_label(req: LabelContactRequest):
|
||
"""设置联系人标签"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.set_contact_label(req.wxid, req.label_ids)
|
||
|
||
@router.get("/tag/contacts/{label_id}", response_model=dict)
|
||
async def get_contacts_by_label(label_id: str, device_id: str = ""):
|
||
"""按标签获取联系人"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_contacts_by_label(label_id)
|
||
|
||
|
||
# ============================================================
|
||
# § 17 Hook 状态
|
||
# ============================================================
|
||
|
||
@router.get("/hook/status", response_model=dict)
|
||
async def get_hook_status(device_id: str):
|
||
"""获取 Hook 状态"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_hook_status()
|
||
|
||
@router.get("/hook/process-info", response_model=dict)
|
||
async def get_process_info(device_id: str):
|
||
"""获取微信进程信息"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_process_info()
|
||
|
||
@router.get("/hook/wechat-version", response_model=dict)
|
||
async def get_wechat_version(device_id: str):
|
||
"""获取微信版本"""
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_wechat_version()
|
||
|
||
|
||
# ============================================================
|
||
# § 17 矩阵 H40 补全(128 canonical 扩展)
|
||
# ============================================================
|
||
|
||
@router.get("/ping", response_model=dict)
|
||
async def ping(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.ping()
|
||
|
||
@router.get("/safety-center", response_model=dict)
|
||
async def get_safety_center(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_safety_center()
|
||
|
||
@router.get("/restrictions", response_model=dict)
|
||
async def check_restrictions(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.check_restrictions()
|
||
|
||
@router.get("/top-stories", response_model=dict)
|
||
async def get_top_stories(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_top_stories()
|
||
|
||
@router.get("/steps", response_model=dict)
|
||
async def get_wechat_steps(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_wechat_steps()
|
||
|
||
@router.get("/stickers", response_model=dict)
|
||
async def get_sticker_list(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.get_sticker_list()
|
||
|
||
@router.post("/pay/show-code", response_model=dict)
|
||
async def show_payment_code(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.show_payment_code()
|
||
|
||
@router.post("/steps/like", response_model=dict)
|
||
async def like_wechat_steps(device_id: str, user_id: str = "filehelper"):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.like_wechat_steps(user_id)
|
||
|
||
@router.post("/cache/clear", response_model=dict)
|
||
async def clear_cache(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.clear_cache()
|
||
|
||
@router.get("/update/check", response_model=dict)
|
||
async def check_for_update(device_id: str):
|
||
skill = await _get_skill(device_id)
|
||
return await skill.check_for_update()
|
||
|
||
class CallRequest(WechatBaseRequest):
|
||
to_id: str
|
||
|
||
@router.post("/call/voice", response_model=dict)
|
||
async def voice_call(req: CallRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.voice_call(req.to_id)
|
||
|
||
@router.post("/call/video", response_model=dict)
|
||
async def video_call(req: CallRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.video_call(req.to_id)
|
||
|
||
@router.post("/location/share-realtime", response_model=dict)
|
||
async def share_real_time_location(req: CallRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.share_real_time_location(req.to_id)
|
||
|
||
class DownloadFileRequest(WechatBaseRequest):
|
||
url: str
|
||
save_path: str = "/sdcard/Download/wp_download.bin"
|
||
|
||
@router.post("/file/download", response_model=dict)
|
||
async def download_file(req: DownloadFileRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.download_file(req.url, req.save_path)
|
||
|
||
class MassSendRequest(WechatBaseRequest):
|
||
user_ids: List[str]
|
||
content: str
|
||
|
||
@router.post("/message/mass-send", response_model=dict)
|
||
async def mass_send(req: MassSendRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.mass_send(req.user_ids, req.content)
|
||
|
||
@router.post("/message/batch-send", response_model=dict)
|
||
async def batch_send(req: MassSendRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.batch_send(req.user_ids, req.content)
|
||
|
||
class ReplyCommentRequest(WechatBaseRequest):
|
||
sns_id: str
|
||
comment: str
|
||
|
||
@router.post("/moments/reply-comment", response_model=dict)
|
||
async def reply_comment(req: ReplyCommentRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.reply_comment(req.sns_id, req.comment)
|
||
|
||
class ReceivePaymentRequest(WechatBaseRequest):
|
||
msg_svr_id: str
|
||
|
||
@router.post("/pay/receive", response_model=dict)
|
||
async def receive_payment(req: ReceivePaymentRequest):
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.receive_payment(req.msg_svr_id)
|
||
|
||
|
||
# ============================================================
|
||
# § 18 统一执行入口
|
||
# ============================================================
|
||
|
||
class UnifiedExecuteRequest(BaseModel):
|
||
device_id: str
|
||
action: str
|
||
params: dict = Field(default_factory=dict)
|
||
timeout: int = 30
|
||
|
||
@router.post("/execute", response_model=dict)
|
||
async def unified_execute(req: UnifiedExecuteRequest):
|
||
"""
|
||
统一执行入口 — 支持所有 128 个微信 canonical action(174 catalog 含 alias)
|
||
|
||
通过 action 名直接调用对应的 Frida RPC 方法。
|
||
通道自动选择:Hook > WebSocket Agent > UI 自动化
|
||
"""
|
||
skill = await _get_skill(req.device_id)
|
||
return await skill.execute(req.action, req.params)
|
||
|
||
@router.get("/actions", response_model=dict)
|
||
async def list_all_actions():
|
||
"""列出所有支持的微信操作(128 canonical + 174 catalog)"""
|
||
from skills.wechat.skill_v2 import WECHAT_ACTIONS, MODULES, WechatSkillV2
|
||
catalog = WechatSkillV2.get_catalog_action_list()
|
||
return {
|
||
"success": True,
|
||
"total_actions": len(WECHAT_ACTIONS),
|
||
"catalog_actions": len(catalog),
|
||
"total_modules": len(MODULES),
|
||
"modules": {k: v["name"] for k, v in MODULES.items()},
|
||
"actions": sorted(WECHAT_ACTIONS.keys()),
|
||
"catalog": catalog,
|
||
}
|