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,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,
|
||||
},
|
||||
}}
|
||||
Reference in New Issue
Block a user