feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 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:
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
身份登录 — 首次手动登录,登录态保存到持久化目录
|
||||
|
||||
用法:
|
||||
python login_identity.py <identity_name> [--platform douyin|xiaohongshu]
|
||||
|
||||
流程:
|
||||
1. 通过 cdp_connector.CDPConnector(identity_dir=...) 启动 Camoufox 持久化
|
||||
2. 用户手动登录
|
||||
3. 按 Enter 触发 → 状态自动保存
|
||||
|
||||
支持平台:
|
||||
- douyin → douyin.com
|
||||
- xiaohongshu → xiaohongshu.com/explore
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from matrix_mgmt import AGENT_LOCAL
|
||||
IDENTITIES_ROOT = AGENT_LOCAL / "tools" / "matrix" / "identities"
|
||||
|
||||
# ── 平台 URL 映射 ──
|
||||
PLATFORM_URLS = {
|
||||
"douyin": "https://www.douyin.com/",
|
||||
"xiaohongshu": "https://www.xiaohongshu.com/explore",
|
||||
}
|
||||
|
||||
PLATFORM_NAMES = {
|
||||
"douyin": "抖音",
|
||||
"xiaohongshu": "小红书",
|
||||
}
|
||||
|
||||
|
||||
async def login_identity(identity_name: str, platform: str = "auto"):
|
||||
"""执行首次手动登录
|
||||
Args:
|
||||
identity_name: 账号ID
|
||||
platform: "auto" 时从 accounts.yaml 自动解析
|
||||
"""
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# 从账号注册表读取 identity_dir 和 platform
|
||||
acct_info = {}
|
||||
try:
|
||||
from matrix_mgmt import MatrixManager
|
||||
mgr = MatrixManager()
|
||||
for a in mgr.list_accounts():
|
||||
if a["id"] == identity_name:
|
||||
acct_info = a
|
||||
break
|
||||
except: pass
|
||||
|
||||
# 身份目录:优先用 accounts.yaml 中的 identity_dir,否则用账号ID
|
||||
acct_identity = acct_info.get("identity_dir") or acct_info.get("identity_hint") or identity_name
|
||||
acct_identity = acct_identity.replace("identities/", "")
|
||||
identity_dir = str(IDENTITIES_ROOT / acct_identity)
|
||||
|
||||
# 平台:auto 时从账号信息解析
|
||||
if platform == "auto":
|
||||
p = acct_info.get("platform", "douyin")
|
||||
platform = p if p in ("douyin", "xiaohongshu") else "douyin"
|
||||
|
||||
signal_file = Path(f"/tmp/login_identity_{identity_name}.signal")
|
||||
platform_url = PLATFORM_URLS.get(platform, PLATFORM_URLS["douyin"])
|
||||
platform_name = PLATFORM_NAMES.get(platform, platform)
|
||||
|
||||
# 前置检查
|
||||
if not Path(identity_dir).exists():
|
||||
print(f"❌ 身份 '{identity_name}' 不存在")
|
||||
print(f" 请先运行: python create_identity.py {identity_name}")
|
||||
sys.exit(1)
|
||||
|
||||
# 清理旧信号
|
||||
if signal_file.exists():
|
||||
signal_file.unlink()
|
||||
|
||||
from cdp_connector import CDPConnector
|
||||
|
||||
print(f"\n{'='*55}")
|
||||
print(f" 🔐 {platform_name} 身份登录: {identity_name}")
|
||||
print(f"{'='*55}")
|
||||
print(f" 持久化目录: {identity_dir}/user_data/")
|
||||
|
||||
# 通过 cdp_connector 启动持久化 Camoufox
|
||||
conn = CDPConnector(
|
||||
identity_dir=identity_dir,
|
||||
headless=False,
|
||||
window=(702, 783),
|
||||
)
|
||||
|
||||
try:
|
||||
await conn.connect()
|
||||
except Exception as e:
|
||||
print(f"❌ 浏览器连接失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 设置视口
|
||||
try:
|
||||
await conn.page.set_viewport_size({"width": 702, "height": 783})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 导航到平台首页
|
||||
print(f" 📍 打开 {platform_name} 首页...")
|
||||
try:
|
||||
await conn.page.goto(platform_url, timeout=30000, wait_until="domcontentloaded")
|
||||
except Exception:
|
||||
print(f" ⚠️ 页面加载超时,但浏览器已打开,请手动导航")
|
||||
print(f" 🌐 当前页面: {conn.page.url}")
|
||||
|
||||
# 创建信号文件表示浏览器已就绪
|
||||
signal_file.write_text("ready")
|
||||
|
||||
print(f"\n 🟡 请手动登录账号 [{identity_name}] ({platform_name})")
|
||||
print(f" Camoufox 窗口已打开(Windows 平板模式 702×783)")
|
||||
print(f" {'💡 打开小红书后如果出现登录弹窗,先手动关闭' if platform == 'xiaohongshu' else ''}")
|
||||
print(f" 登录完成后,在终端按 Enter 键确认")
|
||||
print(f" 或: touch {signal_file}")
|
||||
print()
|
||||
|
||||
# 轮询等待信号
|
||||
while signal_file.exists() and signal_file.read_text().strip() == "ready":
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print(f"\n ✅ 登录态准备保存")
|
||||
|
||||
# 验证登录状态
|
||||
print(f" 🔍 验证登录状态...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
cookies = await conn.context.cookies()
|
||||
platform_cookies = [c for c in cookies if platform.replace("_", "").replace("hongshu", "") in c.get('domain', '')]
|
||||
|
||||
# 使用 auth_manager 的规则检测
|
||||
from auth_manager import check_login_by_cookie_sync, get_session_id
|
||||
logged_in = check_login_by_cookie_sync(cookies, platform)
|
||||
session_val = get_session_id(cookies, platform)
|
||||
|
||||
if logged_in:
|
||||
cnt = len(platform_cookies)
|
||||
print(f" 登录: ✅ 检测到 {platform_name} session cookie")
|
||||
if session_val:
|
||||
print(f" session: {session_val[:20]}...")
|
||||
else:
|
||||
print(f" 登录: ⚠️ 未检测到 {platform_name} session cookie")
|
||||
print(f" 平台 Cookie 数: {len(platform_cookies)}")
|
||||
print(f" 请确认是否已成功登录(全部 Cookie: {len(cookies)} 个)")
|
||||
# 兜底:只要平台 cookie 够多就认为登录了
|
||||
if len(platform_cookies) > 30:
|
||||
print(f" → 平台 Cookie 数量充足,判定为已登录 ✅")
|
||||
logged_in = True
|
||||
|
||||
print(f"\n{'='*55}")
|
||||
if logged_in:
|
||||
print(f" ✅ 身份 '{identity_name}' 登录完成!")
|
||||
else:
|
||||
print(f" ⚠️ 身份 '{identity_name}' 登录状态不确定")
|
||||
print(f" Cookie 已保存,可尝试运行养号查看效果")
|
||||
print(f" 浏览器保持运行,可进行养号操作")
|
||||
print(f"{'='*55}")
|
||||
|
||||
# 保持浏览器运行(不调用 conn.close())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="身份登录 — 首次手动登录")
|
||||
parser.add_argument("identity_name", help="身份名称 (如 douyin_01)")
|
||||
parser.add_argument("--platform", "-p", default="douyin",
|
||||
choices=["douyin", "xiaohongshu"],
|
||||
help="平台 (douyin/xiaohongshu)")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(login_identity(args.identity_name, args.platform))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user