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,113 @@
|
||||
#!/bin/bash
|
||||
# chrome_debug.sh — 启动 Chrome 远程调试(供 launchd 调用)
|
||||
# 确保 Chrome 以 --remote-debugging-port=9222 运行
|
||||
#
|
||||
# v2 — 增强版
|
||||
# - 所有日志带时间戳
|
||||
# - 重启次数限制(最多 5 次,超出后退出等待手动处理)
|
||||
# - 重启间隔递增(5s → 15s → 30s)
|
||||
# - 检测到 OOM 时打印内存状态
|
||||
|
||||
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
USER_DATA_DIR="/tmp/chrome-douyin-profile"
|
||||
PIDFILE="/tmp/chrome-debug.pid"
|
||||
LOG_FILE="/tmp/chrome_debug_launchd.log"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a /tmp/chrome_debug_watch.log
|
||||
}
|
||||
|
||||
is_9222_alive() {
|
||||
curl -s --connect-timeout 3 http://127.0.0.1:9222/json/version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
launch_chrome() {
|
||||
nohup "$CHROME" \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir="$USER_DATA_DIR" \
|
||||
--no-first-run --no-default-browser-check \
|
||||
--disable-features=ChromeWhatsNewUI \
|
||||
--window-position=9999,9999 \
|
||||
about:blank \
|
||||
> "$LOG_FILE" 2>&1 &
|
||||
echo $! > "$PIDFILE"
|
||||
log "Chrome 已启动 (PID $!),屏幕外位置 (9999,9999)"
|
||||
}
|
||||
|
||||
# ── 主流程 ──
|
||||
|
||||
log "=== chrome_debug.sh 启动 ==="
|
||||
|
||||
# 先检查 9222 是否已经在线
|
||||
if is_9222_alive; then
|
||||
log "端口 9222 已有 Chrome 在监听,进入健康检查循环"
|
||||
else
|
||||
log "端口 9222 无响应,启动 Chrome..."
|
||||
launch_chrome
|
||||
# 等最多 15 秒
|
||||
for i in $(seq 1 15); do
|
||||
sleep 1
|
||||
if is_9222_alive; then
|
||||
log "Chrome debug 启动成功(第 ${i} 秒)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ! is_9222_alive; then
|
||||
log "❌ Chrome 启动超时(15 秒),请检查: cat $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 健康检查循环 ──
|
||||
RESTART_COUNT=0
|
||||
MAX_RESTARTS=5
|
||||
|
||||
while true; do
|
||||
sleep 10
|
||||
|
||||
if is_9222_alive; then
|
||||
# 正常:端末还在,继续监控
|
||||
continue
|
||||
fi
|
||||
|
||||
# 端口 9222 挂了
|
||||
RESTART_COUNT=$((RESTART_COUNT + 1))
|
||||
log "⚠️ 端口 9222 无响应(第 ${RESTART_COUNT} 次)"
|
||||
|
||||
if [ "$RESTART_COUNT" -gt "$MAX_RESTARTS" ]; then
|
||||
log "❌ 已连续重启 ${MAX_RESTARTS} 次失败,退出等待手动处理"
|
||||
log " 请检查: 系统内存是否充足(vm_stat),或手动启动 Chrome"
|
||||
log " 手动命令: pkill -f 'remote-debugging-port=9222'"
|
||||
log " $CHROME --remote-debugging-port=9222 --user-data-dir=$USER_DATA_DIR --no-first-run --no-default-browser-check about:blank &"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 递增等待时间:5s → 15s → 30s → 30s → 30s
|
||||
WAIT=$(( RESTART_COUNT <= 1 ? 5 : RESTART_COUNT == 2 ? 15 : 30 ))
|
||||
log "等待 ${WAIT} 秒后重启..."
|
||||
|
||||
# 清理残留 Chrome 进程(防止多实例)
|
||||
pkill -f "remote-debugging-port=9222" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# 打印内存状态帮助排查
|
||||
memory_free=$(vm_stat | awk '/free/ {print $3}' | sed 's/\.//')
|
||||
log "当前空闲内存页: ${memory_free:-?}"
|
||||
|
||||
launch_chrome
|
||||
|
||||
# 等最多 20 秒看有没有起来
|
||||
for i in $(seq 1 20); do
|
||||
sleep 1
|
||||
if is_9222_alive; then
|
||||
log "Chrome 重启成功(第 ${i} 秒)"
|
||||
# 成功一次重置计数
|
||||
RESTART_COUNT=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ! is_9222_alive; then
|
||||
log "❌ Chrome 重启失败(20 秒超时),继续下一轮"
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""chrome_hide.py — 把 9222 采集 Chrome 的窗口移到屏幕外(CDP)
|
||||
|
||||
供 chrome_debug.sh 启动后调用。通过 Chrome DevTools Protocol 操作
|
||||
9222 实例自己的窗口,绝不会影响用户日常 Chrome。
|
||||
|
||||
用法: python3 chrome_hide.py
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
OFFSCREEN = {"left": 10000, "top": 10000, "width": 900, "height": 600, "windowState": "normal"}
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
try:
|
||||
from playwright.async_api import async_playwright
|
||||
except ImportError:
|
||||
print("[chrome_hide] playwright 未安装,跳过窗口隐藏", file=sys.stderr)
|
||||
return 0
|
||||
pw = await async_playwright().start()
|
||||
try:
|
||||
browser = await pw.chromium.connect_over_cdp("http://127.0.0.1:9222")
|
||||
except Exception as e:
|
||||
print(f"[chrome_hide] CDP 连接失败: {e}", file=sys.stderr)
|
||||
await pw.stop()
|
||||
return 1
|
||||
try:
|
||||
cdp = await browser.new_browser_cdp_session()
|
||||
targets = await cdp.send("Target.getTargets")
|
||||
moved = 0
|
||||
for t in targets.get("targetInfos", []):
|
||||
if t.get("type") != "page":
|
||||
continue
|
||||
try:
|
||||
win = await cdp.send("Browser.getWindowForTarget", {"targetId": t["targetId"]})
|
||||
wid = win.get("windowId")
|
||||
if wid:
|
||||
await cdp.send("Browser.setWindowBounds", {"windowId": wid, "bounds": OFFSCREEN})
|
||||
moved += 1
|
||||
except Exception as e:
|
||||
print(f"[chrome_hide] 窗口 {t.get('targetId', '')} 处理失败: {e}", file=sys.stderr)
|
||||
print(f"[chrome_hide] 已将 {moved} 个窗口移到屏幕外")
|
||||
finally:
|
||||
try:
|
||||
await browser.close()
|
||||
except Exception:
|
||||
pass
|
||||
await pw.stop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
# guardd 安装脚本 — 动态生成 plist → 部署到 launchd,设置 300 秒周期
|
||||
# 用法: bash scripts/install.sh
|
||||
# 多机器兼容:自动探测本机路径,无需手动修改 plist 模板
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GUARDD_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PLIST_DEST="$HOME/Library/LaunchAgents/com.agentos.guardd.plist"
|
||||
GUARDD_PY="$GUARDD_DIR/guardd.py"
|
||||
RUNTIME_DIR="$HOME/workbuddy-agent-os/agent-local/runtime/guardd"
|
||||
|
||||
# ── 自动探测 Python ──────────────────────────────────────
|
||||
# 优先使用 agent-os venv,fallback 到当前 python3
|
||||
if [ -x "$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3" ]; then
|
||||
PYTHON_BIN="$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
elif [ -x "$HOME/.workbuddy/binaries/python/envs/default/bin/python3" ]; then
|
||||
PYTHON_BIN="$HOME/.workbuddy/binaries/python/envs/default/bin/python3"
|
||||
else
|
||||
PYTHON_BIN=$(command -v python3)
|
||||
fi
|
||||
PYTHON_LIB_DIR="$(dirname "$(dirname "$PYTHON_BIN")")/versions/$(basename "$(dirname "$(dirname "$PYTHON_BIN")")")/lib" 2>/dev/null || echo ""
|
||||
|
||||
echo "=== guardd 安装脚本 ==="
|
||||
echo "源目录: $GUARDD_DIR"
|
||||
echo "Python: $PYTHON_BIN"
|
||||
|
||||
# 1. 检查 guardd.py 是否存在
|
||||
if [ ! -f "$GUARDD_PY" ]; then
|
||||
echo "❌ 未找到 guardd.py: $GUARDD_PY"
|
||||
echo " 请确保在 guardd 目录下运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 创建运行时目录
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
echo "✅ 运行时目录: $RUNTIME_DIR"
|
||||
|
||||
# 3. 确保 guardd.py 可执行
|
||||
chmod +x "$GUARDD_PY"
|
||||
echo "✅ guardd.py 可执行权限"
|
||||
|
||||
# 4. 动态生成 plist(多机器兼容,不依赖静态模板)
|
||||
cat > "$PLIST_DEST" << PLEOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.agentos.guardd</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$PYTHON_BIN</string>
|
||||
<string>$GUARDD_PY</string>
|
||||
</array>
|
||||
|
||||
<key>StartInterval</key>
|
||||
<integer>300</integer>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>${RUNTIME_DIR}/stdout.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${RUNTIME_DIR}/stderr.log</string>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
||||
<key>GUARDD_DASHBOARD_URL</key>
|
||||
<string>http://localhost:9988</string>
|
||||
</dict>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>300</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
PLEOF
|
||||
echo "✅ plist 已动态生成: $PLIST_DEST"
|
||||
|
||||
# 5. 卸载旧的 launchd job(如果存在)
|
||||
launchctl bootout gui/$(id -u)/com.agentos.guardd 2>/dev/null || true
|
||||
|
||||
# 6. 加载新 plist
|
||||
launchctl bootstrap gui/$(id -u) "$PLIST_DEST"
|
||||
echo "✅ guardd 已加载到 launchd"
|
||||
|
||||
# 7. 立即启动一次(测试)
|
||||
launchctl kickstart -k gui/$(id -u)/com.agentos.guardd
|
||||
echo "✅ guardd 已启动"
|
||||
|
||||
# 8. 验证
|
||||
sleep 2
|
||||
if [ -f "$RUNTIME_DIR/last_run.json" ]; then
|
||||
echo "✅ guardd 首次运行成功"
|
||||
cat "$RUNTIME_DIR/last_run.json"
|
||||
else
|
||||
echo "⏳ guardd 尚未完成首次运行,等待 10 秒..."
|
||||
sleep 8
|
||||
if [ -f "$RUNTIME_DIR/last_run.json" ]; then
|
||||
echo "✅ guardd 首次运行成功"
|
||||
cat "$RUNTIME_DIR/last_run.json"
|
||||
else
|
||||
echo "⚠️ 未检测到 last_run.json,请检查日志: $RUNTIME_DIR/guardd.log"
|
||||
if [ -f "$RUNTIME_DIR/stderr.log" ]; then
|
||||
echo "--- stderr ---"
|
||||
cat "$RUNTIME_DIR/stderr.log"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== guardd 安装完成 ==="
|
||||
echo "查看状态: cat $RUNTIME_DIR/last_run.json"
|
||||
echo "查看日志: cat $RUNTIME_DIR/guardd.log"
|
||||
echo "手动卸载: launchctl bootout gui/$(id -u) $PLIST_DEST"
|
||||
Reference in New Issue
Block a user