移植自 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 带网关原话 / 正常返回)。
163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
"""
|
|
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
|