Files
MediaCrawler/mac-agent-os-main/02_skills/inbox_refine/inbox_refine.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

462 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
inbox_refine.py - 知识库收件箱提纯脚本
将 00_inbox/ 中的原始内容按知识属性分类归档到对应目录,
更新知识库首页统计和变更日志。
用法:
python3 inbox_refine.py --root ~/workbuddy-agent-os/agent-sync [--dry-run] [--date YYYY-MM-DD]
"""
import os
import sys
import json
import re
import shutil
import argparse
from datetime import datetime
from pathlib import Path
# ─── 角色检查:仅 master 可执行收件箱提纯 ───
try:
sys.path.insert(0, str(Path.home() / "workbuddy-agent-os" / "agent-sync" / "05_tools" / "01_system"))
from role_check import check_role, check_capability
if not check_role(["master"]):
print("⛔ 非 master 角色,跳过 inbox_refine")
sys.exit(0)
if not check_capability("inbox_refine"):
print("⛔ 能力 inbox_refine 未启用,跳过")
sys.exit(0)
except ImportError as e:
print(f"⚠️ 角色检查模块不可用 ({e}),继续执行")
# 导入 LLM 分类器
try:
from llm_classifier import classify_content_enhanced
LLM_AVAILABLE = True
except ImportError:
print("⚠️ LLM 分类器不可用,将使用启发式规则")
LLM_AVAILABLE = False
# ── 配置 ──────────────────────────────────────────
NATURE_TO_DIR = {
"fact": "30_facts",
"method": "20_methods",
"concept": "10_concepts",
"axiom": "10_concepts",
"regulation": "30_facts",
"reference": "40_references",
"data": "30_facts",
"opinion": "60_opinions",
"quote": "60_opinions",
}
SOURCE_TYPE_CONFIDENCE = {
"official_doc": 0.9,
"literature": 0.8,
"experiment": 0.7,
"personal_exp": 0.5,
"social_media": 0.3,
"unknown": 0.4,
}
DIR_CHINESE = {
"00_inbox": "📥 收件箱",
"01_daily": "📅 日记",
"10_concepts": "💡 概念层",
"20_methods": "🔧 方法层",
"30_facts": "📋 事实层",
"40_references": "📎 参考层",
"50_resources": "🛠 资源层",
"60_opinions": "💭 观点层",
"90_archive": "🗄 归档层",
}
TRACKED_DIRS = [
"00_inbox", "01_daily", "10_concepts", "20_methods",
"30_facts", "40_references", "50_resources", "60_opinions", "90_archive"
]
# ── 工具函数 ──────────────────────────────────────
def count_md_files(directory: Path) -> int:
"""统计目录下 .md 文件数量"""
if not directory.exists():
return 0
return sum(1 for f in directory.rglob("*.md") if f.is_file())
def latest_modified(directory: Path) -> str:
"""获取目录下最近修改的 .md 文件日期"""
if not directory.exists():
return "—"
md_files = list(directory.rglob("*.md"))
if not md_files:
return "—"
latest = max(f.stat().st_mtime for f in md_files)
return datetime.fromtimestamp(latest).strftime("%Y-%m-%d")
def generate_id(date_str: str, knowledge_root: Path) -> str:
"""生成唯一知识ID:KB-YYYYMMDD-NNN"""
prefix = f"KB-{date_str.replace('-', '')}"
# 扫描已有ID确定序号
existing = set()
for md in knowledge_root.rglob("*.md"):
content = md.read_text(encoding="utf-8", errors="ignore")[:500]
m = re.search(r'^id:\s*(KB-\d{8}-\d+)', content, re.MULTILINE)
if m:
existing.add(m.group(1))
n = 1
while f"{prefix}-{n:03d}" in existing:
n += 1
return f"{prefix}-{n:03d}"
def parse_frontmatter(text: str) -> dict:
"""解析 YAML frontmatter"""
if not text.startswith("---"):
return {}
match = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
if not match:
return {}
fm = {}
for line in match.group(1).split("\n"):
if ":" in line:
key, val = line.split(":", 1)
fm[key.strip()] = val.strip().strip('"').strip("'")
return fm
def build_frontmatter(kb_id: str, title: str, nature: str, domain: str,
confidence: float, source: str, source_type: str,
date_str: str, summary: str = "", tags: list = None) -> str:
"""构建知识卡片 frontmatter"""
if tags is None:
tags = ["待补充"]
# 确保 domain 是列表格式
if isinstance(domain, str):
domain_list = [domain]
else:
domain_list = domain if isinstance(domain, list) else ["other"]
# 构建 tags 字符串
tags_str = ", ".join([f'"{tag}"' for tag in tags])
return f"""---
id: {kb_id}
title: "{title}"
type: {nature if nature in ('concept', 'fact', 'method', 'opinion', 'reference') else 'concept'}
status: published
nature: {nature}
domain: [{", ".join(domain_list)}]
subdomain: []
tags: [{tags_str}]
confidence: {confidence}
source: "{source}"
source_type: {source_type}
date_created: {date_str}
date_modified: {date_str}
version: 1
previous_version: ""
superseded_by: ""
summary: "{summary}"
---"""
# ── 分类逻辑 ──────────────────────────────────────
def classify_content(text: str, fm: dict) -> dict:
"""
增强版分类函数:优先使用 LLM 智能分类,失败时降级到启发式规则。
如果 frontmatter 已有 nature/domain,优先使用。
"""
# 如果 LLM 可用,直接使用增强版分类
if LLM_AVAILABLE:
try:
# 直接使用 LLM 分类,分类器内部会处理降级
result = classify_content_enhanced(text, fm, use_llm=True)
# 适配现有结构
return {
"nature": result["nature"],
"domain": result["domain"][0] if result["domain"] else "other",
"confidence": result["confidence"],
"source_type": result.get("source_type", "unknown"),
"source": result.get("source", ""),
"title": result.get("title", ""),
"summary": result.get("summary", ""),
"tags": result.get("tags", [])
}
except Exception as e:
print(f"⚠️ LLM 分类失败,回退到启发式规则: {e}")
# LLM 不可用或失败,使用启发式规则
result = {
"nature": "concept",
"domain": "other",
"confidence": 0.4,
"source_type": "unknown",
"source": "",
"title": "",
"summary": "",
"tags": []
}
# 优先使用已有 frontmatter
if fm.get("nature") and fm["nature"] in NATURE_TO_DIR:
result["nature"] = fm["nature"]
if fm.get("domain"):
result["domain"] = fm["domain"]
if fm.get("confidence"):
try:
result["confidence"] = float(fm["confidence"])
except ValueError:
pass
if fm.get("source"):
result["source"] = fm["source"]
if fm.get("source_type"):
result["source_type"] = fm["source_type"]
if fm.get("title"):
result["title"] = fm["title"]
# 如果没有 nature,用启发式规则
if not fm.get("nature"):
lower = text.lower()
# 方法类关键词
method_kw = ["步骤", "教程", "如何", "how to", "方法", "指南", "最佳实践", "best practice"]
fact_kw = ["数据", "统计", "报告", "结果", "事实", "date", "report"]
opinion_kw = ["认为", "觉得", "观点", "推测", "假设", "opinion", "think"]
reference_kw = ["论文", "文献", "引用", "参考", "paper", "reference"]
if any(kw in lower for kw in method_kw):
result["nature"] = "method"
elif any(kw in lower for kw in fact_kw):
result["nature"] = "fact"
elif any(kw in lower for kw in opinion_kw):
result["nature"] = "opinion"
elif any(kw in lower for kw in reference_kw):
result["nature"] = "reference"
# confidence from source_type
if result["source_type"] in SOURCE_TYPE_CONFIDENCE:
result["confidence"] = SOURCE_TYPE_CONFIDENCE[result["source_type"]]
# 生成简单标题和摘要
if not result["title"]:
first_line = text.split('\n')[0].strip('# ')
if len(first_line) < 50:
result["title"] = first_line
if not result["summary"]:
clean_text = re.sub(r'[#*\-`]', '', text)
result["summary"] = clean_text[:100] + ("..." if len(clean_text) > 100 else "")
return result
# ── 主流程 ─────────────────────────────────────────
def refine_inbox(root: str, dry_run: bool = False, date_str: str = None):
"""执行收件箱提纯"""
root = Path(os.path.expanduser(root))
kb_root = root / "03_knowledge"
inbox = kb_root / "00_inbox"
if not inbox.exists():
print(f"❌ 收件箱不存在: {inbox}")
return
date_str = date_str or datetime.now().strftime("%Y-%m-%d")
md_files = sorted(inbox.glob("*.md"))
if not md_files:
print(f"📭 收件箱为空,无需提纯 ({date_str})")
# 仍然更新首页统计
update_homepage(kb_root)
return
print(f"📬 收件箱有 {len(md_files)} 个文件待提纯")
archived = []
skipped = []
errors = []
for md_file in md_files:
try:
text = md_file.read_text(encoding="utf-8", errors="ignore")
fm = parse_frontmatter(text)
title = fm.get("title", md_file.stem)
# 分类
classification = classify_content(text, fm)
nature = classification["nature"]
target_dir_name = NATURE_TO_DIR.get(nature, "10_concepts")
target_dir = kb_root / target_dir_name
# 去重检查
is_dup = False
for existing in target_dir.rglob("*.md"):
existing_text = existing.read_text(encoding="utf-8", errors="ignore")[:200]
existing_fm = parse_frontmatter(existing_text)
if existing_fm.get("title") == title:
is_dup = True
break
if is_dup:
print(f" ⏭ 重复跳过: {title}")
skipped.append(title)
if not dry_run:
md_file.unlink()
continue
# 生成 ID 和新文件
kb_id = generate_id(date_str, kb_root)
domain = classification["domain"]
confidence = classification["confidence"]
source = classification["source"]
source_type = classification["source_type"]
# 构建新内容
body = text
if text.startswith("---"):
# 去掉旧 frontmatter
body = re.sub(r'^---\s*\n.*?\n---\s*\n', '', text, count=1, flags=re.DOTALL)
new_content = build_frontmatter(
kb_id,
classification.get("title", title),
nature,
domain,
confidence,
source,
source_type,
date_str,
classification.get("summary", ""),
classification.get("tags", [])
) + "\n\n" + body.strip() + "\n"
# 确定目标文件名
safe_title = re.sub(r'[^\w\u4e00-\u9fff-]', '_', title)[:50]
target_file = target_dir / f"{date_str}_{safe_title}.md"
if dry_run:
print(f" 🔍 [DRY] {title} → {target_dir_name}/ ({nature})")
else:
target_dir.mkdir(parents=True, exist_ok=True)
target_file.write_text(new_content, encoding="utf-8")
md_file.unlink()
print(f" ✅ {title} → {target_dir_name}/ ({nature}) [{kb_id}]")
archived.append({
"id": kb_id,
"title": title,
"nature": nature,
"dir": target_dir_name,
})
except Exception as e:
print(f" ❌ 处理失败: {md_file.name} - {e}")
errors.append(md_file.name)
# 更新首页
if not dry_run:
update_homepage(kb_root)
update_changelog(kb_root, date_str, archived, skipped)
print(f"\n📊 提纯完成: 归档 {len(archived)}, 跳过 {len(skipped)}, 失败 {len(errors)}")
def update_homepage(kb_root: Path):
"""更新知识库首页统计"""
readme = kb_root / "README.md"
if not readme.exists():
print("⚠️ 首页 README.md 不存在,跳过统计更新")
return
# 计算各目录统计
stats = {}
total = 0
for dirname in TRACKED_DIRS:
dirpath = kb_root / dirname
count = count_md_files(dirpath)
latest = latest_modified(dirpath)
stats[dirname] = (count, latest)
total += count
# 构建统计表格
table_lines = []
for dirname in TRACKED_DIRS:
count, latest = stats[dirname]
emoji_name = DIR_CHINESE.get(dirname, dirname)
table_lines.append(f"| {emoji_name} | {count} | {latest} |")
table_lines.append(f"| **总计** | **{total}** | — |")
table_text = "\n".join(table_lines)
# 替换 README 中的统计部分
content = readme.read_text(encoding="utf-8")
pattern = r'(\| 📥 收件箱 \|.*?)(\| \*\*总计\*\* \| \*\*\d+\*\* \| — \|)'
replacement = table_text
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
# 更新最后更新日期
today = datetime.now().strftime("%Y-%m-%d")
content = re.sub(
r'\*最后更新:.*? by inbox_refine 技能\*',
f'*最后更新:{today} by inbox_refine 技能*',
content
)
readme.write_text(content, encoding="utf-8")
print(f"📄 首页统计已更新 (总计 {total} 个文件)")
def update_changelog(kb_root: Path, date_str: str, archived: list, skipped: list):
"""更新变更日志"""
changelog = kb_root / "CHANGELOG.md"
if not changelog.exists():
initial = "# 知识库变更日志\n"
changelog.write_text(initial, encoding="utf-8")
content = changelog.read_text(encoding="utf-8")
# 构建新条目
entry = f"\n## {date_str}\n\n"
if archived:
entry += "### 归档\n"
for item in archived:
entry += f"- [{item['nature']}] {item['title']} → {item['dir']}/ ({item['id']})\n"
if skipped:
entry += f"\n### 跳过(重复)\n"
for title in skipped:
entry += f"- {title}\n"
# 在第一个 ## 之前插入(跳过标题行)
lines = content.split("\n")
insert_idx = 1
for i, line in enumerate(lines):
if line.startswith("## "):
insert_idx = i
break
lines.insert(insert_idx, entry)
changelog.write_text("\n".join(lines), encoding="utf-8")
print(f"📝 变更日志已更新")
# ── 入口 ──────────────────────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="AgentOS 知识库收件箱提纯")
parser.add_argument("--root", default="~/workbuddy-agent-os/agent-sync", help="agent-os 根目录")
parser.add_argument("--dry-run", action="store_true", help="试运行,不实际修改文件")
parser.add_argument("--date", default=None, help="日期 (YYYY-MM-DD)")
args = parser.parse_args()
refine_inbox(args.root, args.dry_run, args.date)