Files
workphone-sdk/sdk/scripts/wechat_unblock_batch.py
Manus AI 42fe10401a 1
1
2026-05-24 17:50:04 +08:00

323 lines
12 KiB
Python
Executable File
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.

#!/usr/bin/env python3
"""
微信「联系客服解封」常规批量执行器
适用:以后任意设备、任意微信号被限制,编辑配置文件即可一键全自动解封。
链路:调用 SDK `POST /api/v3/account/unblock-customer-service`
→ 设备端 unblock_via_customer_service skill
→ 自动跑「安全保护中 → 联系客服 → 终止使用说明 → 联系专属客服 → 允许 → 微信安全专属客服 → AI 持续对话」
用法:
python3 sdk/scripts/wechat_unblock_batch.py -c sdk/config/unblock_targets.yaml
python3 sdk/scripts/wechat_unblock_batch.py -c xxx.yaml --dry-run # 只校验不执行
python3 sdk/scripts/wechat_unblock_batch.py -c xxx.yaml --only 192.168.110.80:5555
输出:
sdk/tmp/unblock_batch_{ts}/
├── summary.md 汇总表格
├── {device}_{wxid}.json 每条任务原始返回(含 turns/screenshots
└── {device}_{wxid}.log 每条任务日志
"""
from __future__ import annotations
import argparse
import asyncio
import json
import logging
import os
import sys
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
LOG = logging.getLogger("unblock_batch")
# ---------- 配置加载 ----------
def _load_yaml(path: str) -> dict:
try:
import yaml # type: ignore
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except ImportError:
# 退化:把简单 yaml 当 json-like 解析(仅支持本配置示例)
import re
content = open(path, "r", encoding="utf-8").read()
content = re.sub(r"#.*$", "", content, flags=re.MULTILINE)
# 极简退化:要求用户安装 PyYAML
raise RuntimeError("未安装 PyYAML请执行: pip install pyyaml") from None
@dataclass
class TargetTask:
device_id: str
wxid: str = ""
phone: str = ""
reason: str = ""
max_rounds: int = 18
chat_interval_sec: int = 8
use_web_search: bool = True
platform: str = "wechat"
enabled: bool = True
note: str = ""
# 运行时
started_at: float = 0
finished_at: float = 0
status: str = "pending" # pending / running / success / reject / timeout / fallback / error / skipped
final_message: str = ""
rounds: int = 0
raw_response: Optional[Dict[str, Any]] = field(default=None)
# ---------- 调用 SDK ----------
def _call_unblock(sdk_base: str, t: TargetTask, timeout: int) -> Dict[str, Any]:
qs = urllib.parse.urlencode({
"device_id": t.device_id,
"platform": t.platform,
"reason": t.reason,
"phone": t.phone,
"wxid": t.wxid,
"max_rounds": t.max_rounds,
"chat_interval_sec": t.chat_interval_sec,
"use_web_search": "true" if t.use_web_search else "false",
})
url = f"{sdk_base.rstrip('/')}/api/v3/account/unblock-customer-service?{qs}"
LOG.info(f"[{t.device_id}] POST {url}")
req = urllib.request.Request(url, method="POST", headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode("utf-8")
try:
return json.loads(body)
except Exception:
return {"code": 599, "raw_body": body[:2000]}
# ---------- 单条任务 ----------
def _run_one(sdk_base: str, t: TargetTask, out_dir: Path,
retry_on_fallback: int) -> TargetTask:
if not t.enabled:
t.status = "skipped"
LOG.info(f"[{t.device_id}] disabled -> skip")
return t
t.started_at = time.time()
t.status = "running"
log_path = out_dir / f"{_safe(t.device_id)}_{_safe(t.wxid or 'noid')}.log"
json_path = out_dir / f"{_safe(t.device_id)}_{_safe(t.wxid or 'noid')}.json"
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
LOG.addHandler(file_handler)
attempts = 0
response = None
try:
attempts += 1
# 单条最长耗时max_rounds * (chat_interval + 5) + 90 兜底
timeout = t.max_rounds * (t.chat_interval_sec + 5) + 90
response = _call_unblock(sdk_base, t, timeout=timeout)
t.raw_response = response
data = response.get("data", {}) if isinstance(response, dict) else {}
session = data.get("session", {}) if isinstance(data, dict) else {}
status = data.get("status") or session.get("final_status") or "unknown"
msg = data.get("message") or data.get("error") or session.get("final_message") or ""
rounds = session.get("rounds", 0)
t.status = status
t.final_message = msg[:300]
t.rounds = int(rounds or 0)
# 链路失败时按需重试
while t.status == "fallback" and attempts <= retry_on_fallback:
LOG.warning(f"[{t.device_id}] fallback重试 {attempts}/{retry_on_fallback}")
time.sleep(8)
attempts += 1
response = _call_unblock(sdk_base, t, timeout=timeout)
t.raw_response = response
data = response.get("data", {}) if isinstance(response, dict) else {}
session = data.get("session", {}) if isinstance(data, dict) else {}
t.status = data.get("status") or session.get("final_status") or "unknown"
t.final_message = (data.get("message") or session.get("final_message") or "")[:300]
t.rounds = int(session.get("rounds", 0) or 0)
except Exception as exc:
t.status = "error"
t.final_message = f"{type(exc).__name__}: {exc}"[:300]
LOG.exception(f"[{t.device_id}] 异常")
response = {"error": str(exc)}
finally:
t.finished_at = time.time()
try:
json_path.write_text(
json.dumps(response or {}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
except Exception:
pass
LOG.removeHandler(file_handler)
file_handler.close()
return t
def _safe(s: str) -> str:
return "".join(c if c.isalnum() else "_" for c in s)[:48]
# ---------- 主流程 ----------
def main() -> int:
parser = argparse.ArgumentParser(description="微信联系客服解封 · 批量执行器")
parser.add_argument("-c", "--config", required=True, help="yaml 配置文件路径")
parser.add_argument("--dry-run", action="store_true", help="只校验不执行")
parser.add_argument("--only", help="只跑某个 device_id覆盖配置")
parser.add_argument("--out", help="输出目录(默认 sdk/tmp/unblock_batch_{ts}")
parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
logging.basicConfig(
level=getattr(logging, args.log_level.upper(), logging.INFO),
format="%(asctime)s %(levelname)s %(message)s",
)
cfg = _load_yaml(args.config)
sdk_base = cfg.get("sdk_base") or os.environ.get("SDK_BASE_URL") or "http://127.0.0.1:8899"
platform = cfg.get("platform", "wechat")
default_max = int(cfg.get("default_max_rounds", 18))
default_interval = int(cfg.get("default_chat_interval_sec", 8))
default_web = bool(cfg.get("default_use_web_search", True))
concurrency = max(1, int(cfg.get("concurrency", 1)))
retry_on_fallback = int(cfg.get("retry_on_fallback", 1))
between_sleep = int(cfg.get("between_task_sleep_sec", 6))
raw_targets = cfg.get("targets") or []
tasks: List[TargetTask] = []
for raw in raw_targets:
if not isinstance(raw, dict):
continue
dev = raw.get("device_id")
if not dev:
continue
if args.only and dev != args.only:
continue
tasks.append(TargetTask(
device_id=dev,
wxid=raw.get("wxid", "") or "",
phone=raw.get("phone", "") or "",
reason=raw.get("reason", "") or "",
max_rounds=int(raw.get("max_rounds", default_max)),
chat_interval_sec=int(raw.get("chat_interval_sec", default_interval)),
use_web_search=bool(raw.get("use_web_search", default_web)),
platform=raw.get("platform", platform),
enabled=bool(raw.get("enabled", True)),
note=raw.get("note", "") or "",
))
if not tasks:
LOG.error("配置中未找到任何 target")
return 2
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
if args.out:
out_dir = Path(args.out)
else:
sdk_root = Path(__file__).resolve().parents[1]
out_dir = sdk_root / "tmp" / f"unblock_batch_{ts}"
out_dir.mkdir(parents=True, exist_ok=True)
LOG.info(f"配置: {args.config}")
LOG.info(f"SDK : {sdk_base}")
LOG.info(f"任务数: {len(tasks)} (concurrency={concurrency})")
LOG.info(f"输出: {out_dir}")
if args.dry_run:
LOG.info("--- DRY RUN ---")
for t in tasks:
LOG.info(f" {t.device_id} wxid={t.wxid} reason={t.reason[:30]} enabled={t.enabled}")
return 0
# 健康检查
try:
with urllib.request.urlopen(f"{sdk_base.rstrip('/')}/api/v3/connection/status", timeout=10) as resp:
conn = json.loads(resp.read().decode())
data = conn.get("data", {})
LOG.info(f"SDK 健康 ws_online={data.get('online_ws_count')} adb={data.get('adb_count')}")
except Exception as exc:
LOG.error(f"SDK 不可达: {exc}")
return 3
# 执行(串行 / 并发)
if concurrency <= 1:
for t in tasks:
_run_one(sdk_base, t, out_dir, retry_on_fallback)
if between_sleep > 0:
time.sleep(between_sleep)
else:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
futures = {ex.submit(_run_one, sdk_base, t, out_dir, retry_on_fallback): t for t in tasks}
for fut in concurrent.futures.as_completed(futures):
t = futures[fut]
try:
fut.result()
except Exception as exc:
LOG.exception(f"[{t.device_id}] 调度异常: {exc}")
# 汇总报告
_write_summary(tasks, out_dir, sdk_base, ts)
success = sum(1 for t in tasks if t.status == "success")
print(f"\n=== 完成 {success}/{len(tasks)} ===\n报告: {out_dir / 'summary.md'}")
return 0 if success == len(tasks) else 1
def _write_summary(tasks: List[TargetTask], out_dir: Path, sdk_base: str, ts: str) -> None:
lines = [
f"# 微信联系客服解封 · 批量执行报告 {ts}",
"",
f"- SDK: `{sdk_base}`",
f"- 任务数: {len(tasks)}",
f"- 成功: {sum(1 for t in tasks if t.status == 'success')}",
f"- 拒绝: {sum(1 for t in tasks if t.status == 'reject')}",
f"- 超时: {sum(1 for t in tasks if t.status == 'timeout')}",
f"- 链路失败: {sum(1 for t in tasks if t.status == 'fallback')}",
f"- 异常: {sum(1 for t in tasks if t.status == 'error')}",
f"- 跳过: {sum(1 for t in tasks if t.status == 'skipped')}",
"",
"| # | device | wxid | 状态 | 轮次 | 耗时(s) | 客服最终回复 |",
"|---|--------|------|------|------|---------|--------------|",
]
for i, t in enumerate(tasks, 1):
elapsed = round(t.finished_at - t.started_at, 1) if t.started_at else 0
msg = (t.final_message or "").replace("|", "/").replace("\n", " ")[:80]
lines.append(f"| {i} | `{t.device_id}` | `{t.wxid}` | **{t.status}** | {t.rounds} | {elapsed} | {msg} |")
lines += [
"",
"## 状态说明",
"- **success**:客服已解封 / 已恢复使用",
"- **reject**:客服明确拒绝解封",
"- **timeout**:达到最大轮次或客服长时间无回复",
"- **fallback**:未能进入客服小程序(路径变化)",
"- **error**:调用异常(网络 / 设备不在线等)",
"- **skipped**:配置 enabled=false",
"",
"## 详细 JSON",
"",
"每条任务详见同目录 `{device_id}_{wxid}.json`(含 turns / screenshots / web_snippets",
]
(out_dir / "summary.md").write_text("\n".join(lines), encoding="utf-8")
if __name__ == "__main__":
sys.exit(main())