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,403 @@
"""
plugins/system_plugins.py — 系统级插件合集 (v2)
包含: Collector / Skills / Knowledge / Automation / Tools / System
版本: 2.0.0 | 更新: 2026-05-18
"""
from pathlib import Path
import json
from plugins.base import DashboardPlugin, AGENT_SYNC, AGENT_LOCAL, CROSS_MACHINE, HOSTNAME, MACHINE_UID
from plugins._registry import get_plugin_data
class CollectorPlugin(DashboardPlugin):
name = "collector"
label = "内容采集"
icon = "📡"
version = "2.0.0"
description = "采集工具 / 收件箱 / 使用统计"
order = 40
MANUAL_DESC = {
"doubao_driver": "豆包浏览器驱动 — 抖音视频文字提取/分析",
"analyze": "AI分析 — 口播素材内容解析与分类",
"collect": "搜索采集 — OpenCLI桥接/百度HTTP双模式",
"app": "Web界面 — Gradio交互式素材管理中心",
"downloader": "下载器 — 视频/音频/图片批量下载",
"script_factory": "脚本工厂 — 自动生成采集脚本",
"utils": "工具函数 — Cookie管理/缓存/日志",
}
def _scan_tools(self):
"""扫描现有采集工具"""
tools_dir = AGENT_SYNC / "05_tools" / "05_crawl" / "content-inspiration"
tools = []
if tools_dir.exists():
for f in sorted(tools_dir.iterdir()):
if f.suffix == ".py" and not f.name.startswith("_"):
desc = self.MANUAL_DESC.get(f.stem, "")
if not desc:
content = f.read_text(encoding="utf-8").split("\n")
for line in content[:8]:
s = line.strip().strip('"').strip("'")
if s and not s.startswith("usage") and not s.startswith("用法"):
desc = s[:60]
break
tools.append({"name": f.stem, "desc": desc or "采集脚本"})
return tools
def _count_inbox(self):
inbox = AGENT_SYNC / "03_knowledge" / "00_inbox"
subs = AGENT_SYNC / "03_knowledge" / "01_submissions"
return {
"收件箱": len(list(inbox.rglob("*.md"))) if inbox.exists() else 0,
"提交箱": len(list(subs.rglob("*.md"))) if subs.exists() else 0,
}
def summary(self, machines):
cnt = self._count_inbox()
tools = self._scan_tools()
return {
"采集工具": len(tools),
"待处理": cnt["收件箱"],
"提交箱": cnt["提交箱"],
"各机器": {HOSTNAME: {
"工具数": len(tools),
"待处理": cnt["收件箱"],
}}
}
def detail(self, machine=""):
return {HOSTNAME: {
"采集工具": self._scan_tools(),
"收件箱统计": self._count_inbox(),
"收件箱路径": str(AGENT_SYNC / "03_knowledge" / "00_inbox"),
}}
class SkillsPlugin(DashboardPlugin):
name = "skills"
label = "技能树"
icon = "🧩"
version = "1.0.0"
description = "所有已安装技能的清单和版本"
order = 50
def _scan_skills(self):
skills_dir = Path.home() / ".workbuddy" / "skills"
result = []
if skills_dir.exists():
for d in sorted(skills_dir.iterdir()):
if d.is_dir():
sk = d / "SKILL.md"
ver = ""
desc = ""
if sk.exists():
for line in sk.read_text().split("\n"):
if line.startswith("version:"):
ver = line.split(":")[1].strip().strip('"')
if line.startswith("description:"):
desc = line.split(":")[1].strip().strip('"')[:60]
result.append({"name": d.name, "version": ver or "-", "description": desc})
return result
def summary(self, machines):
skills = self._scan_skills()
custom = sum(1 for s in skills if s["version"] != "-")
return {
"总技能": len(skills),
"自定义": custom,
"各机器": {HOSTNAME: {"技能数": len(skills), "自定义": custom}},
}
def detail(self, machine=""):
return {HOSTNAME: {"技能列表": self._scan_skills()}}
class KnowledgePlugin(DashboardPlugin):
name = "knowledge"
label = "知识库"
icon = "📚"
version = "2.0.0"
description = "分仓统计 / 浓缩卡片 / 教科书体系"
order = 60
CATEGORY_LABELS = {
"00_inbox": "待提纯",
"01_submissions": "提交箱",
"10_concepts": "概念",
"20_methods": "方法",
"30_facts": "事实",
"40_references": "参考",
"50_resources": "素材",
"60_opinions": "观点",
"90_archive": "归档",
"99_system": "系统",
}
def _count_knowledge(self):
kb = AGENT_SYNC / "03_knowledge"
stats = {}
for d in sorted(kb.iterdir()):
if d.is_dir() and not d.name.startswith("."):
count = len(list(d.rglob("*.md")))
if count > 0:
label = self.CATEGORY_LABELS.get(d.name, d.name)
stats[d.name] = {"label": label, "count": count}
return stats
def _get_textbook_info(self):
"""AI视频教科书信息"""
textbook_dir = AGENT_SYNC / "03_knowledge" / "20_methods"
chapters = []
if textbook_dir.exists():
for f in sorted(textbook_dir.iterdir()):
if f.suffix == ".md" and not f.name.startswith("_"):
chapters.append(f.stem)
ai_dir = textbook_dir / "ai-video-system"
ai_files = 0
if ai_dir.exists():
ai_files = len(list(ai_dir.rglob("*.md")))
return {
"教材章节": len(chapters),
"AI视频教科书": f"{len(chapters)}个文件",
"Kling知识": ai_files,
}
def summary(self, machines):
stats = self._count_knowledge()
total = sum(v["count"] for v in stats.values())
textbook = self._get_textbook_info()
return {
"总知识": total,
"分类": len(stats),
"AI视频教材": textbook.get("AI视频教科书", ""),
"各机器": {HOSTNAME: {
"分类数": len(stats),
"总文件": total,
"教材章节": textbook.get("教材章节", 0),
}}
}
def detail(self, machine=""):
stats = self._count_knowledge()
textbook = self._get_textbook_info()
return {HOSTNAME: {
"分类统计": stats,
"教科书": textbook,
"README路径": str(AGENT_SYNC / "03_knowledge" / "README.md"),
}}
class AutomationPlugin(DashboardPlugin):
name = "automation"
label = "自动化任务"
icon = "⏰"
version = "1.0.0"
description = "定时任务列表和执行历史"
order = 70
def _read_tasks(self):
task_dir = CROSS_MACHINE / "tasks"
stats = {"pending": 0, "completed": 0}
for status in ["pending", "completed"]:
d = task_dir / status
if d.exists():
for f in d.iterdir():
if f.suffix == ".json":
stats[status] += 1
return stats
def _read_pending_tasks(self):
task_dir = CROSS_MACHINE / "tasks" / "pending"
tasks = []
if task_dir.exists():
for f in sorted(task_dir.iterdir()):
if f.suffix == ".json":
try:
tasks.append(json.loads(f.read_text()))
except:
pass
return tasks
def summary(self, machines):
stats = self._read_tasks()
pending = self._read_pending_tasks()
by_machine = {}
for t in pending:
target = t.get("target_host", "未知")
by_machine.setdefault(target, 0)
by_machine[target] += 1
return {"待处理": stats.get("pending", 0), "已完成": stats.get("completed", 0), "各机器": by_machine}
def detail(self, machine=""):
return {HOSTNAME: {"待处理任务": self._read_pending_tasks()}}
class ToolsPlugin(DashboardPlugin):
name = "tools"
label = "工具集"
icon = "🔧"
version = "2.0.0"
description = "05_tools/ 工具模块清单 + 说明 + 路径"
order = 80
CATEGORY_MAP = {
"00_setup": "系统设置",
"01_system": "系统",
"02_browser": "浏览器",
"03_ocr": "OCR",
"04_ai": "AI",
"04_media": "媒体",
"05_crawl": "采集",
"06_mobile": "移动端",
"07_matrix": "矩阵",
"08_trae_agent": "Trae代理",
"09_ave": "视频工厂",
"10_dashboard": "看板",
}
def _scan_tools(self):
tools_dir = AGENT_SYNC / "05_tools"
result = []
if tools_dir.exists():
for d in sorted(tools_dir.iterdir()):
if d.is_dir() and not d.name.startswith("."):
readme = d / "README.md"
desc = ""
if readme.exists():
for line in readme.read_text().split("\n")[:3]:
if line.strip() and not line.startswith("#"):
desc = line.strip()[:60]
break
cat = self.CATEGORY_MAP.get(d.name, "其他")
result.append({"name": d.name, "cat": cat, "desc": desc, "path": str(d)})
return result
def summary(self, machines):
tools = self._scan_tools()
return {"总工具": len(tools), "各机器": {HOSTNAME: {"工具数": len(tools)}}}
def detail(self, machine=""):
return {HOSTNAME: {"工具列表": self._scan_tools()}}
class SystemPlugin(DashboardPlugin):
name = "system"
label = "系统核心"
icon = "⚙️"
version = "2.0.0"
description = "AgentOS内核 / AI引擎 / 记忆系统 / 知识库"
order = 90
def _get_kernel_info(self):
import subprocess, json
agentos_ver = "v2.1.0"
arch_ver = "联邦版 (2026-05)"
llm_engine = "oMLX v0.3.6 (Apple MLX)"
llm_models = []
models_dir = Path.home() / ".omlx" / "models"
if models_dir.exists():
for m in sorted(models_dir.iterdir()):
llm_models.append(m.name)
memory = {
"架构": "四层记忆 + 自然语言知识库 双轨",
"L0 硬约束": "SOUL.md (~3KB), 不可绕过",
"L1 关键词索引": "keyword_index.json + ChromaDB 语义向量库",
"L2 结构化事实": "facts.db (SQLite), 结构化三元组",
"L3 原文": "raw/ 目录, 按需加载",
"检索引擎": "BM25 + ChromaDB 向量 + RRF 融合",
"向量维度": "1024 (Qwen3-Embedding-0.6B)",
}
kb_dirs = AGENT_SYNC / "03_knowledge"
kb_stats = {}
if kb_dirs.exists():
for d in sorted(kb_dirs.iterdir()):
if d.is_dir() and not d.name.startswith("."):
count = len(list(d.rglob("*.md")))
if count > 0:
kb_stats[d.name] = count
skills_dir = Path.home() / ".workbuddy" / "skills"
skill_count = 0
if skills_dir.exists():
skill_count = len([d for d in skills_dir.iterdir() if d.is_dir()])
machine_count = 0
try:
r = subprocess.run(["curl","-s","http://localhost:9988/api/machines"],
capture_output=True, text=True, timeout=3)
if r.stdout:
d = json.loads(r.stdout)
machine_count = d.get("total", 0)
except: pass
return {
"AgentOS 版本": agentos_ver,
"架构版本": arch_ver,
"LLM引擎": llm_engine,
"已加载模型": llm_models,
"记忆系统": memory,
"知识库规模": kb_stats,
"技能总数": skill_count,
"联邦机器数": machine_count,
"仓库": "mac-agent-os (Gitee + GitHub 双同步)",
"运行主机": HOSTNAME,
}
def summary(self, machines):
import subprocess, shutil
git_ver = ""
try:
r = subprocess.run(["git","log","-1","--format=%h %s"], capture_output=True, text=True,
timeout=5, cwd=str(AGENT_SYNC))
git_ver = r.stdout.strip()
except: pass
disk = shutil.disk_usage(str(AGENT_SYNC))
kernel = self._get_kernel_info()
return {
"各机器": {HOSTNAME: {
"AgentOS版本": kernel.get("AgentOS 版本", "-"),
"LLM引擎": kernel.get("LLM引擎", "-"),
"模型数": len(kernel.get("已加载模型", [])),
"记忆层级": "L0+L1+L2+L3",
"技能数": kernel.get("技能总数", 0),
"知识库": sum(kernel.get("知识库规模", {}).values()),
"Git版本": git_ver[:25] if git_ver else "-",
"磁盘总GB": round(disk.total / 1e9, 1),
"磁盘可用GB": round(disk.free / 1e9, 1),
}}
}
def detail(self, machine=""):
import subprocess, shutil, os
git_log = []
try:
r = subprocess.run(["git","log","--oneline","-5"], capture_output=True, text=True,
timeout=5, cwd=str(AGENT_SYNC))
git_log = [l.strip() for l in r.stdout.strip().split("\n") if l]
except: pass
remotes = []
try:
r = subprocess.run(["git","remote","-v"], capture_output=True, text=True,
timeout=5, cwd=str(AGENT_SYNC))
remotes = [l.strip() for l in r.stdout.strip().split("\n") if l]
except: pass
disk = shutil.disk_usage(str(AGENT_SYNC))
kernel = self._get_kernel_info()
return {HOSTNAME: {
"AI内核": kernel,
"Git仓库": remotes,
"最近提交": git_log,
"磁盘": {"总": round(disk.total/1e9,1), "已用": round((disk.total-disk.free)/1e9,1), "可用": round(disk.free/1e9,1)},
"运行时": {
"Python": os.popen("python3 --version 2>/dev/null").read().strip() or "-",
"Node": os.popen("node --version 2>/dev/null").read().strip() or "-",
"Playwright": "✅ 已安装",
"AgentOS路径": str(AGENT_SYNC),
"机器UID": MACHINE_UID,
"主机名": HOSTNAME,
},
}}