移植自 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 带网关原话 / 正常返回)。
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AgentOS 角色检查工具
|
|
所有自动化脚本在执行前调用此工具检查本机角色是否符合要求。
|
|
"""
|
|
import os
|
|
import sys
|
|
import yaml
|
|
from pathlib import Path
|
|
|
|
HOST_ID_PATH = Path.home() / "workbuddy-agent-os" / "agent-local" / "identity" / "HOST_ID.md"
|
|
LOCAL_IDENTITY_DIR = Path.home() / "workbuddy-agent-os" / "agent-local" / "identity"
|
|
|
|
|
|
def read_host_id() -> dict:
|
|
"""读取本机 HOST_ID,返回角色和能力字典"""
|
|
if not HOST_ID_PATH.exists():
|
|
print(f"[role_check] ⚠️ HOST_ID.md 不存在: {HOST_ID_PATH}")
|
|
print("[role_check] 请运行 agentos init --localize 生成")
|
|
return {"role": "unknown", "capabilities": {}}
|
|
|
|
try:
|
|
with open(HOST_ID_PATH, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
# 从 yaml code block 中提取能力配置
|
|
yaml_block = None
|
|
in_block = False
|
|
lines = []
|
|
for line in content.split("\n"):
|
|
if line.strip().startswith("```yaml"):
|
|
in_block = True
|
|
continue
|
|
elif line.strip().startswith("```"):
|
|
if in_block:
|
|
yaml_block = "\n".join(lines)
|
|
break
|
|
in_block = False
|
|
continue
|
|
if in_block:
|
|
lines.append(line)
|
|
|
|
# 提取角色
|
|
role = "unknown"
|
|
for line in content.split("\n"):
|
|
if "角色:" in line and ":" in line:
|
|
role_part = line.split("角色:")[-1].strip()
|
|
if role_part:
|
|
role = role_part
|
|
|
|
# 解析能力
|
|
capabilities = {}
|
|
if yaml_block:
|
|
try:
|
|
parsed = yaml.safe_load(yaml_block)
|
|
if isinstance(parsed, dict):
|
|
capabilities = parsed
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"role": role,
|
|
"capabilities": capabilities,
|
|
}
|
|
except Exception as e:
|
|
print(f"[role_check] ❌ 读取 HOST_ID.md 失败: {e}")
|
|
return {"role": "unknown", "capabilities": {}}
|
|
|
|
|
|
def check_role(allowed_roles: list) -> bool:
|
|
"""
|
|
检查本机角色是否在允许列表中。
|
|
同时运行集群冲突检测(如果任务涉及 master 权限)。
|
|
如果角色不匹配,打印提示并返回 False。
|
|
"""
|
|
info = read_host_id()
|
|
role = info["role"]
|
|
|
|
if role in allowed_roles:
|
|
# 角色通过后,额外检测集群冲突(仅限 master 级别任务)
|
|
if "master" in allowed_roles:
|
|
try:
|
|
from cluster_registry import pre_task_check
|
|
return pre_task_check(allowed_roles)
|
|
except ImportError:
|
|
pass
|
|
return True
|
|
|
|
print(f"[role_check] ⛔ 本机角色={role},需要角色={allowed_roles},跳过执行")
|
|
print(f"[role_check] 如需修改角色,编辑 {HOST_ID_PATH}")
|
|
return False
|
|
|
|
|
|
def check_capability(capability: str) -> bool:
|
|
"""
|
|
检查本机是否启用指定能力。
|
|
"""
|
|
info = read_host_id()
|
|
caps = info.get("capabilities", {})
|
|
|
|
if isinstance(caps.get(capability), bool):
|
|
return caps[capability]
|
|
|
|
# 也兼容字符串 True/False
|
|
if isinstance(caps.get(capability), str):
|
|
return caps[capability].lower() in ("true", "yes", "1")
|
|
|
|
print(f"[role_check] ⛔ 能力 {capability} 未定义或已禁用,跳过执行")
|
|
return False
|
|
|
|
|
|
def print_status():
|
|
"""打印本机角色状态"""
|
|
info = read_host_id()
|
|
print(f"[role_check] 本机角色: {info['role']}")
|
|
print(f"[role_check] 能力配置:")
|
|
for k, v in info.get("capabilities", {}).items():
|
|
status = "✅" if v else "❌"
|
|
print(f" {status} {k}: {v}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) > 1:
|
|
cmd = sys.argv[1]
|
|
if cmd == "status":
|
|
print_status()
|
|
elif cmd == "check-role":
|
|
allowed = sys.argv[2:] if len(sys.argv) > 2 else ["master"]
|
|
if check_role(allowed):
|
|
print(f"[role_check] ✅ 角色确认通过")
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
elif cmd == "check-capability":
|
|
cap = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
if check_capability(cap):
|
|
print(f"[role_check] ✅ 能力确认通过: {cap}")
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
else:
|
|
print_status()
|