feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,162 @@
"""
agentos guardd — 节点代理管理
功能:
status 查看 guardd 运行状态
restart 重启 guardd
logs 查看 guardd 日志
install 安装/更新 guardd plist
tasks 列出运行中任务
stop 停止指定任务
"""
import argparse, json, os, subprocess, time, urllib.request, urllib.error
from pathlib import Path
from agentos.base import AgentOSPlugin
AGENT_SYNC = Path(__file__).resolve().parent.parent.parent.parent.parent.parent
GUARDD_PORT = 9090
def _guardd_api(path: str) -> dict:
"""调用本地 guardd HTTP API"""
try:
r = urllib.request.urlopen(f"http://localhost:{GUARDD_PORT}{path}", timeout=5)
return json.loads(r.read().decode())
except Exception:
return {}
class GuarddPlugin(AgentOSPlugin):
name = "guardd"
description = "节点代理管理 — 部署/监控/任务管理"
def register(self, subparsers):
p = subparsers.add_parser("guardd", help=self.description)
p_sub = p.add_subparsers(dest="guardd_action", help="操作")
ps = p_sub.add_parser("status", help="查看 guardd 运行状态")
ps.set_defaults(guardd_func=self.cmd_status)
pr = p_sub.add_parser("restart", help="重启 guardd")
pr.set_defaults(guardd_func=self.cmd_restart)
pl = p_sub.add_parser("logs", help="查看 guardd 日志")
pl.add_argument("--tail", type=int, default=30, help="显示行数")
pl.set_defaults(guardd_func=self.cmd_logs)
pi = p_sub.add_parser("install", help="安装/更新 guardd plist 并重启")
pi.set_defaults(guardd_func=self.cmd_install)
pt = p_sub.add_parser("tasks", help="列出运行中任务")
pt.set_defaults(guardd_func=self.cmd_tasks)
pk = p_sub.add_parser("stop", help="停止指定任务")
pk.add_argument("run_id", help="任务 ID")
pk.set_defaults(guardd_func=self.cmd_stop)
return p
def dispatch(self, args):
if hasattr(args, 'guardd_func'):
return args.guardd_func(args)
print("未知 guardd 命令,使用 agentos guardd --help")
return 1
def cmd_status(self, args):
"""查看 guardd 状态"""
# launchctl 状态
r = subprocess.run(
["launchctl", "print", f"gui/{os.getuid()}/com.agentos.guardd"],
capture_output=True, text=True, timeout=5
)
if r.returncode == 0:
for line in r.stdout.split("\n"):
if "state = " in line:
print(f" launchd: 🟢 {line.strip()}")
else:
print(" launchd: ❌ 未运行")
# HTTP 健康检查
health = _guardd_api("/health")
if health:
print(f" HTTP: 🟢 运行中 (端口 {GUARDD_PORT})")
print(f" 版本: {health.get('version', '?')}")
print(f" 运行: {health.get('uptime_sec', 0)}s")
print(f" 任务: {health.get('running_tasks', 0)} 运行中 / {health.get('total_tasks', 0)} 总计")
else:
print(f" HTTP: ❌ 无法连接 (localhost:{GUARDD_PORT})")
return 0
def cmd_restart(self, args):
"""重启 guardd"""
print(" ⏹ 停止 guardd...")
subprocess.run(["launchctl", "unload", str(Path.home() / "Library/LaunchAgents/com.agentos.guardd.plist")],
capture_output=True, timeout=5)
time.sleep(1)
print(" 🚀 启动 guardd...")
subprocess.run(["launchctl", "load", str(Path.home() / "Library/LaunchAgents/com.agentos.guardd.plist")],
capture_output=True, timeout=5)
time.sleep(2)
health = _guardd_api("/health")
print(f" {'✅ guardd 已重启' if health else '❌ guardd 启动失败'}")
return 0
def cmd_logs(self, args):
"""查看 guardd 日志"""
log_file = Path.home() / "workbuddy-agent-os" / "agent-local" / "runtime" / "guardd" / "guardd.log"
if log_file.exists():
lines = log_file.read_text().splitlines()
tail = lines[-args.tail:]
print(f" 📋 guardd.log (最后 {args.tail} 行):\n")
for line in tail:
print(f" {line}")
else:
print(" ❌ 日志文件不存在")
return 0
def cmd_install(self, args):
"""安装/更新 guardd plist"""
plist_src = AGENT_SYNC / "05_tools" / "00_setup" / "guardd" / "com.agentos.guardd.plist"
plist_dst = Path.home() / "Library/LaunchAgents" / "com.agentos.guardd.plist"
if not plist_src.exists():
print(f" ❌ 源 plist 不存在: {plist_src}")
return 1
# 替换 __HOME__ 占位符
content = plist_src.read_text().replace("__HOME__", str(Path.home()))
plist_dst.write_text(content)
print(f" ✅ plist 已安装: {plist_dst}")
return self.cmd_restart(args)
def cmd_tasks(self, args):
"""列出 guardd 任务"""
tasks = _guardd_api("/tasks")
if not isinstance(tasks, list):
print(" ❌ 无法获取任务列表(guardd HTTP 不可用)")
return 1
if not tasks:
print(" 📭 无任务记录")
return 0
print(f" 📋 共 {len(tasks)} 个任务:\n")
for t in tasks:
status = t.get("status", "?")
icon = {"running": "🟢", "completed": "✅", "failed": "❌", "cancelled": "⏹"}.get(status, "❓")
cmd = t.get("cmd", "")[:60]
pid = t.get("pid", "?")
elapsed = ""
if t.get("start_time"):
import datetime
st = datetime.datetime.fromisoformat(t["start_time"])
elapsed = f"{(datetime.datetime.now(datetime.timezone.utc) - st).total_seconds():.0f}s"
print(f" {icon} {t.get('run_id','?')[:12]} | {status:10s} | PID={pid} | {elapsed:>6s} | {cmd}")
return 0
def cmd_stop(self, args):
"""停止任务"""
result = _guardd_api(f"/task/{args.run_id}/stop")
if result.get("status") in ("stopped", "already_stopped"):
print(f" ⏹ 任务 {args.run_id} 已停止")
else:
print(f" ❌ 停止失败: {result.get('error', '未知错误')}")
return 0