feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 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:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""生成可灵任务对账表(供与账单核对)"""
import datetime
import json
import urllib.request
from pathlib import Path
import yaml
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
urllib.request.install_opener(opener)
CFG = Path.home() / "workbuddy-agent-os" / "agent-local" / "tools" / "ave" / "config" / "local.yaml"
AVE = Path.home() / "workbuddy-agent-os" / "agent-local" / "runtime" / "ave"
cfg = yaml.safe_load(CFG.read_text(encoding="utf-8"))
k = cfg["kling"]
hdr = {"Authorization": f"Bearer {k['api_key']}"}
base = k["base_url"]
with urllib.request.urlopen(
urllib.request.Request(base + "/v1/videos/text2video", headers=hdr), timeout=30) as r:
vids = json.loads(r.read().decode()).get("data") or []
local_files = []
for p in AVE.rglob("*.mp4"):
local_files.append((p.stat().st_mtime, str(p.relative_to(AVE))))
local_files.sort(reverse=True)
NOTES = {
"934535832286281787": "首次验证 kling-v3 可用",
"934536473163501607": "端到端流程 wf_5d5ffff0fc06 镜头1",
"934536909153013802": "端到端流程 wf_5d5ffff0fc06 镜头2",
"934537556619169795": "端到端流程 wf_5d5ffff0fc06 镜头3",
"934539639007219781": "口播线验证(oral_verify2)",
"934540864327012427": "并发生成测试 镜头1",
"934540864352174168": "并发生成测试 镜头2",
"934540864385880141": "并发生成测试 镜头3",
"934542570569990201": "流程测试",
"934612690390495243": "P2 编排通电验证",
"934618791945191489": "完整链路验证 镜头1",
"934618965245431828": "完整链路验证 镜头2",
}
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
lines = ["# 可灵任务对账表", "", f"生成时间:{now}", "",
"## 全部任务", "",
"| 时间 | 任务号 | 状态 | 时长 | 用途 |",
"|:--|:--|:--|:--|:--|"]
today_cnt = today_sec = 0
rows = sorted(vids, key=lambda t: -int(t.get("created_at") or 0))
for t in rows:
ms = int(t.get("created_at") or 0)
dts = datetime.datetime.fromtimestamp(ms / 1000).strftime("%Y-%m-%d %H:%M")
vs = (t.get("task_result") or {}).get("videos") or []
dur = float(vs[0].get("duration", 5) or 5) if vs else 5.0
note = NOTES.get(str(t.get("task_id")), "(历史任务)")
if dts.startswith("2026-10-01"):
today_cnt += 1
today_sec += dur
lines.append(f"| {dts} | `{t.get('task_id')}` | {t.get('task_status')} | {dur:.1f}s | {note} |")
total_sec = sum(
float(((t.get("task_result") or {}).get("videos") or [{}])[0].get("duration", 5) or 5)
for t in rows
)
lines += ["",
f"**今天(2026-10-01):{today_cnt} 条 / {today_sec:.1f} 秒**",
f"**全部:{len(rows)} 条 / {total_sec:.1f} 秒**", "",
"## 费用估算(按不同单价档)", "",
"| 单价档 | 今天 | 全部 |", "|:--|:--|:--|",
f"| ¥0.6/秒(无声 720P) | ¥{today_sec * 0.6:.1f} | ¥{total_sec * 0.6:.1f} |",
f"| ¥1.2/秒(有声 / 1080P) | ¥{today_sec * 1.2:.1f} | ¥{total_sec * 1.2:.1f} |",
f"| ¥2.4/秒(高配) | ¥{today_sec * 2.4:.1f} | ¥{total_sec * 2.4:.1f} |",
"",
"> ⚠️ 以上为**估算**。准确金额请以可灵控制台「消费明细」为准。",
"> 若单价与我估算不符,请把明细发我,我核对后调整代码里的计费参数。",
"",
"## 本地产物文件(最新 20 个)", ""]
for mt, rel in local_files[:20]:
dts = datetime.datetime.fromtimestamp(mt).strftime("%m-%d %H:%M")
lines.append(f"- `{dts}` {rel}")
out = AVE / "可灵任务对账表.md"
out.write_text("\n".join(lines), encoding="utf-8")
print(f"✅ 已生成: {out}")
print(f" 今天 {today_cnt} 条 / {today_sec:.1f} 秒 · 全部 {len(rows)} 条 / {total_sec:.1f} 秒")