移植自 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 带网关原话 / 正常返回)。
104 lines
3.6 KiB
Bash
104 lines
3.6 KiB
Bash
#!/bin/bash
|
|
# nurture_runner.sh — 养号执行包装器 v2.0
|
|
# 负责:执行前清理 + 浏览器窗口定位 + mc run + 结果写入
|
|
#
|
|
# 用法:
|
|
# nurture_runner.sh <account_id> <blueprint> <rounds> [run_id] [slot] [pos_x] [pos_y]
|
|
#
|
|
# 窗口位置参数:
|
|
# slot: 槽位ID 1-4 (用于日志)
|
|
# pos_x, pos_y: 窗口左上角坐标(如 0 0 或 702 0)
|
|
#
|
|
# 结果写入: $AGENT_LOCAL/runtime/nurture/results/<run_id>.json
|
|
|
|
set -o pipefail
|
|
|
|
ACCOUNT=$1
|
|
BLUEPRINT=$2
|
|
ROUNDS=$3
|
|
RUN_ID=${4:-$(date +%s)}
|
|
SLOT=${5:-1}
|
|
POS_X=${6:-0}
|
|
POS_Y=${7:-0}
|
|
|
|
# 环境设置
|
|
export AGENT_SYNC="${AGENT_SYNC:-$HOME/workbuddy-agent-os/agent-sync}"
|
|
export AGENT_LOCAL="${AGENT_LOCAL:-$HOME/workbuddy-agent-os/agent-local}"
|
|
MC_PYTHON="${MC_PYTHON:-$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3}"
|
|
SCRIPTS_DIR="$AGENT_SYNC/05_tools/07_matrix/scripts"
|
|
|
|
# 结果目录
|
|
RESULTS_DIR="$AGENT_LOCAL/runtime/nurture/results"
|
|
LOGS_DIR="$AGENT_LOCAL/runtime/nurture/logs"
|
|
mkdir -p "$RESULTS_DIR" "$LOGS_DIR"
|
|
|
|
RESULT_FILE="$RESULTS_DIR/$RUN_ID.json"
|
|
LOG_FILE="$LOGS_DIR/$RUN_ID.log"
|
|
HOSTNAME=$(hostname -s)
|
|
|
|
# ── 预清理:精确匹配同名账号的残留僵尸进程 ──
|
|
# 用 pgrep 取 PID → ps 取命令行 → 只在 --accounts= 中精确匹配才杀
|
|
for _pid in $(pgrep -f "mc run" 2>/dev/null || true); do
|
|
_cmd=$(ps -p $_pid -o command= 2>/dev/null || true)
|
|
if echo "$_cmd" | grep -Eq -- '--accounts=(.*,)?'$ACCOUNT'(,.*)?' 2>/dev/null; then
|
|
kill $_pid 2>/dev/null || true
|
|
fi
|
|
done
|
|
for _pid in $(pgrep -f "camoufox" 2>/dev/null || true); do
|
|
_cmd=$(ps -p $_pid -o command= 2>/dev/null || true)
|
|
if echo "$_cmd" | grep -Eq -- '(accounts|identity).*'$ACCOUNT 2>/dev/null; then
|
|
kill $_pid 2>/dev/null || true
|
|
fi
|
|
done
|
|
|
|
# ── 写初始状态 ──
|
|
START_TS=$(date +%s)
|
|
echo "{\"run_id\":\"$RUN_ID\",\"account\":\"$ACCOUNT\",\"blueprint\":\"$BLUEPRINT\",\"rounds\":$ROUNDS,\"status\":\"running\",\"hostname\":\"$HOSTNAME\",\"slot\":$SLOT,\"position\":[$POS_X,$POS_Y],\"started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"steps\":{\"total\":0,\"success\":0,\"failed\":0}}" > "$RESULT_FILE"
|
|
|
|
# ── 执行(传入窗口位置参数)──
|
|
cd "$SCRIPTS_DIR"
|
|
PYTHONPATH="$SCRIPTS_DIR" $MC_PYTHON -m mc run \
|
|
--accounts="$ACCOUNT" \
|
|
--blueprints="$BLUEPRINT" \
|
|
--rounds="$ROUNDS" \
|
|
--mix --interval=45-90 > "$LOG_FILE" 2>&1
|
|
EXIT_CODE=$?
|
|
END_TS=$(date +%s)
|
|
DURATION=$((END_TS - START_TS))
|
|
|
|
# 解析日志统计
|
|
STEPS_TOTAL=$(grep -o "总步骤: [0-9]*" "$LOG_FILE" | tail -1 | awk '{print $2}')
|
|
STEPS_OK=$(grep -o "成功: [0-9]*" "$LOG_FILE" | tail -1 | awk '{print $2}')
|
|
STEPS_FAIL=$(grep -o "失败: [0-9]*" "$LOG_FILE" | tail -1 | awk '{print $2}')
|
|
|
|
STATUS="completed"
|
|
[ $EXIT_CODE -ne 0 ] && STATUS="failed"
|
|
|
|
LOG_TAIL=$(tail -200 "$LOG_FILE" | head -c 3000)
|
|
|
|
cat > "$RESULT_FILE" << EOF
|
|
{
|
|
"run_id": "$RUN_ID",
|
|
"account": "$ACCOUNT",
|
|
"blueprint": "$BLUEPRINT",
|
|
"rounds": $ROUNDS,
|
|
"status": "$STATUS",
|
|
"hostname": "$HOSTNAME",
|
|
"slot": $SLOT,
|
|
"position": [$POS_X, $POS_Y],
|
|
"started_at": "$(date -u -r $START_TS 2>/dev/null && echo -n "$(date -u -r $START_TS +%Y-%m-%dT%H:%M:%SZ)" || date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"completed_at": "$(date -u -r $END_TS 2>/dev/null && echo -n "$(date -u -r $END_TS +%Y-%m-%dT%H:%M:%SZ)" || date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"duration_secs": $DURATION,
|
|
"exit_code": $EXIT_CODE,
|
|
"steps": {
|
|
"total": ${STEPS_TOTAL:-0},
|
|
"success": ${STEPS_OK:-0},
|
|
"failed": ${STEPS_FAIL:-0}
|
|
},
|
|
"log_path": "$LOG_FILE"
|
|
}
|
|
EOF
|
|
|
|
echo "{\"run_id\":\"$RUN_ID\",\"account\":\"$ACCOUNT\",\"slot\":$SLOT,\"status\":\"$STATUS\",\"steps\":{\"total\":${STEPS_TOTAL:-0},\"success\":${STEPS_OK:-0},\"failed\":${STEPS_FAIL:-0}},\"duration\":$DURATION}"
|
|
exit $EXIT_CODE
|