Files
MediaCrawler/mac-agent-os-main/05_tools/09_ave/scripts/lib/dashboard.py
T
butubb 2112c1a870
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
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 带网关原话 / 正常返回)。
2026-10-10 17:12:25 +08:00

499 lines
16 KiB
Python

from __future__ import annotations
"""
AVE dashboard — 生产监控数据层
职责:
SQLite schema 初始化 + 埋点封装
CLI 自动写 DB, FastAPI 后端读 DB
设计原则:
- 所有操作 try/except 包裹, 失败不影响 CLI 主线
- DB 路径: agent-local/tools/ave/data/ave.db
- 连接池: 每次调用创建新连接 (CLI 场景, 无并发问题)
用法:
from lib.dashboard import init_db, log_production, log_step, log_asset
init_db()
pid = log_production(strategy="oral", script_name="test", config={"key": "val"})
log_step(pid, "tts", "completed", cost=0.3, detail="85字")
log_step(pid, "compose", "completed", cost=0.0, detail="2 clips")
log_asset(pid, "generated_clip", "/tmp/out.mp4", tags=["hermit", "kling"])
"""
import json
import os
import sqlite3
import time
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any
# ── DB 路径 ───────────────────────────────────────────────
LOCAL_ROOT = Path(os.environ.get("AVE_LOCAL_DIR",
str(Path.home() / "workbuddy-agent-os/agent-local")))
DB_DIR = LOCAL_ROOT / "tools" / "ave" / "data"
DB_DIR.mkdir(parents=True, exist_ok=True)
DB_PATH = DB_DIR / "ave.db"
# ── Schema ────────────────────────────────────────────────
# 从 PLANS/DASHBOARD_DESIGN.md 导出, 6 表
SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS productions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
strategy TEXT NOT NULL,
status TEXT DEFAULT 'running',
script_path TEXT,
script_name TEXT,
output_path TEXT,
duration_sec REAL,
total_cost REAL DEFAULT 0.0,
config_json TEXT,
error_message TEXT,
created_at TEXT DEFAULT (datetime('now','localtime')),
completed_at TEXT
);
CREATE TABLE IF NOT EXISTS production_steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
production_id INTEGER NOT NULL REFERENCES productions(id),
step_name TEXT NOT NULL,
status TEXT DEFAULT 'pending',
started_at TEXT,
ended_at TEXT,
duration_sec REAL,
cost REAL DEFAULT 0.0,
detail TEXT,
error_message TEXT
);
CREATE TABLE IF NOT EXISTS assets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
asset_type TEXT NOT NULL,
name TEXT,
file_path TEXT,
file_size INTEGER,
hash TEXT,
source TEXT,
meta_json TEXT,
production_id INTEGER,
created_at TEXT DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS asset_tags (
asset_id INTEGER NOT NULL REFERENCES assets(id),
tag TEXT NOT NULL,
PRIMARY KEY (asset_id, tag)
);
CREATE TABLE IF NOT EXISTS production_assets (
production_id INTEGER NOT NULL REFERENCES productions(id),
asset_id INTEGER NOT NULL REFERENCES assets(id),
role TEXT,
PRIMARY KEY (production_id, asset_id)
);
CREATE TABLE IF NOT EXISTS cost_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
production_id INTEGER REFERENCES productions(id),
step_name TEXT,
service TEXT NOT NULL,
duration_s REAL DEFAULT 0,
resolution TEXT,
chars INTEGER DEFAULT 0,
cost_yuan REAL DEFAULT 0,
status TEXT DEFAULT 'success',
note TEXT,
logged_at TEXT DEFAULT (datetime('now','localtime'))
);
"""
# ═══════════════════════════════════════════════════════════
# 初始化
# ═══════════════════════════════════════════════════════════
def init_db() -> bool:
"""初始化 DB schema, 不存在则创建"""
try:
conn = sqlite3.connect(str(DB_PATH))
conn.executescript(SCHEMA_SQL)
conn.commit()
conn.close()
return True
except Exception as e:
print(f" [dashboard] DB 初始化失败: {e}")
return False
def _get_conn() -> sqlite3.Connection | None:
"""获取 DB 连接 (带 try/except)"""
try:
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
return conn
except Exception as e:
print(f" [dashboard] DB 连接失败: {e}")
return None
# ═══════════════════════════════════════════════════════════
# 埋点函数
# ═══════════════════════════════════════════════════════════
def log_production(
strategy: str,
script_path: str = "",
script_name: str = "",
config: dict | None = None,
) -> int:
"""
记录一条 production 开始
返回:
production_id (失败返回 -1)
"""
conn = _get_conn()
if not conn:
return -1
try:
cur = conn.execute(
"""INSERT INTO productions (strategy, status, script_path, script_name, config_json)
VALUES (?, 'running', ?, ?, ?)""",
(strategy, script_path, script_name, json.dumps(config or {}, ensure_ascii=False)),
)
conn.commit()
pid = cur.lastrowid
return pid
except Exception as e:
print(f" [dashboard] log_production 失败: {e}")
return -1
finally:
conn.close()
def complete_production(
production_id: int,
status: str = "completed",
output_path: str = "",
duration_sec: float = 0.0,
total_cost: float = 0.0,
error_message: str = "",
):
"""标记 production 完成"""
conn = _get_conn()
if not conn:
return
try:
conn.execute(
"""UPDATE productions SET status=?, output_path=?, duration_sec=?,
total_cost=?, error_message=?, completed_at=datetime('now','localtime')
WHERE id=?""",
(status, output_path, duration_sec, total_cost, error_message, production_id),
)
conn.commit()
except Exception as e:
print(f" [dashboard] complete_production 失败: {e}")
finally:
conn.close()
def log_step(
production_id: int,
step_name: str,
status: str = "completed",
cost: float = 0.0,
detail: str = "",
error_message: str = "",
) -> bool:
"""
记录一步生产步骤
参数:
production_id: 所属 production ID
step_name: 步骤名 (tts, search_material, compose, bgm, lipsync, ...)
status: completed / failed
cost: 该步骤费用
detail: 文字详情
"""
conn = _get_conn()
if not conn:
return False
try:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
conn.execute(
"""INSERT INTO production_steps
(production_id, step_name, status, started_at, ended_at, cost, detail, error_message)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(production_id, step_name, status, now, now, cost, detail, error_message),
)
conn.commit()
return True
except Exception as e:
print(f" [dashboard] log_step 失败: {e}")
return False
finally:
conn.close()
def log_asset(
production_id: int,
asset_type: str,
file_path: str = "",
name: str = "",
source: str = "",
tags: list[str] | None = None,
meta: dict | None = None,
) -> int:
"""
记录一个生成/使用的资产
返回:
asset_id (失败返回 -1)
"""
conn = _get_conn()
if not conn:
return -1
try:
file_size = os.path.getsize(file_path) if file_path and os.path.exists(file_path) else 0
cur = conn.execute(
"""INSERT INTO assets (asset_type, name, file_path, file_size, source, meta_json, production_id)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(asset_type, name, file_path, file_size, source,
json.dumps(meta or {}, ensure_ascii=False), production_id),
)
aid = cur.lastrowid
# 关联 production ↔ asset
conn.execute(
"INSERT OR IGNORE INTO production_assets (production_id, asset_id, role) VALUES (?, ?, ?)",
(production_id, aid, "output" if source == "generated" else "input"),
)
# 标签
if tags:
for tag in tags:
conn.execute(
"INSERT OR IGNORE INTO asset_tags (asset_id, tag) VALUES (?, ?)",
(aid, tag.strip().lower()),
)
conn.commit()
return aid
except Exception as e:
print(f" [dashboard] log_asset 失败: {e}")
return -1
finally:
conn.close()
def log_cost(
production_id: int,
service: str,
step_name: str = "",
duration_s: float = 0.0,
resolution: str = "",
chars: int = 0,
cost_yuan: float = 0.0,
status: str = "success",
note: str = "",
):
"""记录费用明细"""
conn = _get_conn()
if not conn:
return
try:
conn.execute(
"""INSERT INTO cost_log
(production_id, step_name, service, duration_s, resolution, chars, cost_yuan, status, note)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(production_id, step_name, service, duration_s, resolution, chars, cost_yuan, status, note),
)
conn.commit()
# 更新 production 累计费用
conn.execute(
"UPDATE productions SET total_cost = total_cost + ? WHERE id = ?",
(cost_yuan, production_id),
)
conn.commit()
except Exception as e:
print(f" [dashboard] log_cost 失败: {e}")
finally:
conn.close()
# ═══════════════════════════════════════════════════════════
# 查询 API (供 FastAPI 后端使用)
# ═══════════════════════════════════════════════════════════
def get_productions(
limit: int = 50,
offset: int = 0,
strategy: str | None = None,
status: str | None = None,
) -> list[dict]:
"""查询 production 列表"""
conn = _get_conn()
if not conn:
return []
try:
where = []
params = []
if strategy:
where.append("strategy = ?")
params.append(strategy)
if status:
where.append("status = ?")
params.append(status)
where_clause = (" WHERE " + " AND ".join(where)) if where else ""
rows = conn.execute(
f"SELECT * FROM productions{where_clause} ORDER BY id DESC LIMIT ? OFFSET ?",
params + [limit, offset],
).fetchall()
return [dict(r) for r in rows]
except Exception as e:
print(f" [dashboard] get_productions 失败: {e}")
return []
finally:
conn.close()
def get_production_detail(production_id: int) -> dict | None:
"""查询单条 production + steps + assets + costs"""
conn = _get_conn()
if not conn:
return None
try:
prod = conn.execute(
"SELECT * FROM productions WHERE id = ?", (production_id,)
).fetchone()
if not prod:
return None
steps = conn.execute(
"SELECT * FROM production_steps WHERE production_id = ? ORDER BY id",
(production_id,),
).fetchall()
assets = conn.execute(
"""SELECT a.*, GROUP_CONCAT(at.tag, ',') as tags
FROM assets a
LEFT JOIN asset_tags at ON at.asset_id = a.id
WHERE a.production_id = ?
GROUP BY a.id ORDER BY a.id""",
(production_id,),
).fetchall()
costs = conn.execute(
"SELECT * FROM cost_log WHERE production_id = ? ORDER BY id",
(production_id,),
).fetchall()
return {
"production": dict(prod),
"steps": [dict(s) for s in steps],
"assets": [dict(a) for a in assets],
"costs": [dict(c) for c in costs],
}
except Exception as e:
print(f" [dashboard] get_production_detail 失败: {e}")
return None
finally:
conn.close()
def get_assets(asset_type: str | None = None, tag: str | None = None,
limit: int = 100, offset: int = 0) -> list[dict]:
"""查询资产列表"""
conn = _get_conn()
if not conn:
return []
try:
where = []
params = []
if asset_type:
where.append("a.asset_type = ?")
params.append(asset_type)
if tag:
where.append("EXISTS (SELECT 1 FROM asset_tags WHERE asset_id = a.id AND tag = ?)")
params.append(tag.lower())
where_clause = (" WHERE " + " AND ".join(where)) if where else ""
rows = conn.execute(
f"""SELECT a.*, GROUP_CONCAT(at.tag, ',') as tags
FROM assets a
LEFT JOIN asset_tags at ON at.asset_id = a.id
{where_clause}
GROUP BY a.id
ORDER BY a.id DESC LIMIT ? OFFSET ?""",
params + [limit, offset],
).fetchall()
return [dict(r) for r in rows]
except Exception as e:
print(f" [dashboard] get_assets 失败: {e}")
return []
finally:
conn.close()
def get_summary() -> dict:
"""获取总览统计"""
conn = _get_conn()
if not conn:
return {}
try:
total_prods = conn.execute("SELECT COUNT(*) FROM productions").fetchone()[0]
completed = conn.execute("SELECT COUNT(*) FROM productions WHERE status='completed'").fetchone()[0]
failed = conn.execute("SELECT COUNT(*) FROM productions WHERE status='failed'").fetchone()[0]
total_cost = conn.execute("SELECT COALESCE(SUM(total_cost), 0) FROM productions").fetchone()[0]
total_assets = conn.execute("SELECT COUNT(*) FROM assets").fetchone()[0]
# 本月
month_start = datetime.now().strftime("%Y-%m-01")
month_prods = conn.execute(
"SELECT COUNT(*) FROM productions WHERE created_at >= ?", (month_start,)
).fetchone()[0]
month_cost = conn.execute(
"SELECT COALESCE(SUM(total_cost), 0) FROM productions WHERE created_at >= ?",
(month_start,),
).fetchone()[0]
return {
"total_productions": total_prods,
"completed": completed,
"failed": failed,
"total_cost": round(total_cost, 2),
"total_assets": total_assets,
"month_productions": month_prods,
"month_cost": round(month_cost, 2),
}
except Exception as e:
print(f" [dashboard] get_summary 失败: {e}")
return {}
finally:
conn.close()
def get_cost_breakdown() -> list[dict]:
"""按策略统计费用"""
conn = _get_conn()
if not conn:
return []
try:
rows = conn.execute(
"""SELECT strategy,
COUNT(*) as count,
COALESCE(SUM(duration_sec), 0) as total_duration,
COALESCE(SUM(total_cost), 0) as total_cost,
ROUND(AVG(total_cost), 2) as avg_cost
FROM productions
GROUP BY strategy
ORDER BY total_cost DESC"""
).fetchall()
return [dict(r) for r in rows]
except Exception:
return []
finally:
conn.close()