Files
MediaCrawler/mac-agent-os-main/05_tools/00_setup/agentos/init.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

386 lines
13 KiB
Python

"""
agentos init — 换机一键初始化
流程:
1. 创建 agent-local/ 目录结构
2. 建立软链接
3. 安装 Python/Node 依赖
4. 注册技能到 WorkBuddy
5. 部署 MCP 配置
6. 创建自动化任务
7. 重建向量库
8. 环境验证
"""
import os
import sys
import shutil
from pathlib import Path
from .utils import (
get_sync_root, get_local_root, get_python, get_npx,
info, ok, warn, err, run, banner
)
from . import skill_mgr
# 本机专属目录结构
LOCAL_DIRS = [
"memory/raw",
"runtime/cache",
"materials/web",
"materials/video",
"materials/audio",
"materials/screenshots",
"materials/refined_for_inbox/clipping",
"materials/refined_for_inbox/feed",
"materials/refined_for_inbox/personal",
"materials/refined_for_inbox/memory",
]
# 软链接映射 {相对路径: 目标路径}
SYMLINKS = {
"04_memory/long_term/raw": "memory/raw",
"04_memory/vector_db": "memory/vector_db",
"06_runtime/cache": "runtime/cache",
}
# 自动化任务定义 (name, prompt, rrule, schedule_type)
AUTOMATIONS = [
{
"name": "AgentOS 每日记忆提炼",
"schedule_type": "recurring",
"rrule": "FREQ=DAILY;BYHOUR=2;BYMINUTE=0",
"prompt": f"""执行 AgentOS 每日记忆提炼任务。
先读取自动化记忆文件(如存在)了解历史。
执行命令:
{get_python()} {get_sync_root() / '02_skills/memory_manager/daily_digest.py'} --root {get_sync_root()}
汇报结果:新增了几条事实、去重了几条、冲突了几条。
写执行摘要到自动化记忆文件。""",
},
{
"name": "AgentOS 收件箱汇聚",
"schedule_type": "recurring",
"rrule": "FREQ=DAILY;BYHOUR=2;BYMINUTE=30",
"prompt": f"""执行 AgentOS 收件箱汇聚任务:扫描知识库各分类目录中的文件,提取主要内容生成标准化 MD 文件放入 00_inbox/。
执行命令:
{get_python()} {get_sync_root() / '02_skills/collect_to_inbox/collect_to_inbox.py'} --root {get_sync_root()}
汇报结果:扫描了几个目录、处理了几个文件、新增了几个收件箱文件。""",
},
{
"name": "AgentOS 收件箱提纯",
"schedule_type": "recurring",
"rrule": "FREQ=DAILY;BYHOUR=3;BYMINUTE=0",
"prompt": f"""执行 AgentOS 收件箱提纯任务:每日将 00_inbox/ 中的内容分类归档到知识库,更新首页统计和变更日志。
执行命令:
{get_python()} {get_sync_root() / '02_skills/inbox_refine/inbox_refine.py'} --root {get_sync_root()}
汇报结果:处理了几个收件箱文件、归档到了哪个分类目录。""",
},
]
def step_local_dirs(dry_run: bool):
"""步骤1: 创建 agent-local/ 目录结构"""
local_root = get_local_root()
info(f"创建本地目录结构: {local_root}")
for subdir in LOCAL_DIRS:
target = local_root / subdir
if not target.exists():
if dry_run:
info(f" [DRY-RUN] 创建: {target}")
else:
target.mkdir(parents=True, exist_ok=True)
ok(f" 创建: {subdir}")
else:
ok(f" 已存在: {subdir}")
def step_symlinks(dry_run: bool):
"""步骤2: 重建软链接"""
sync_root = get_sync_root()
local_root = get_local_root()
info("重建软链接...")
for link_rel, target_rel in SYMLINKS.items():
link_path = sync_root / link_rel
target_path = local_root / target_rel
if dry_run:
info(f" [DRY-RUN] 软链: {link_rel} → {target_path}")
continue
if link_path.is_symlink():
current_target = os.readlink(str(link_path))
if current_target == str(target_path):
ok(f" 软链正确: {link_rel}")
continue
info(f" 更新软链: {link_rel} → {target_path}")
link_path.unlink()
elif link_path.exists():
warn(f" {link_rel} 是真实目录,保留不动")
continue
link_path.parent.mkdir(parents=True, exist_ok=True)
link_path.symlink_to(target_path)
ok(f" 创建软链: {link_rel} → {target_path}")
def step_install_skills(dry_run: bool):
"""步骤3: 注册技能到 WorkBuddy"""
if dry_run:
info(" [DRY-RUN] 安装所有技能到 WorkBuddy")
return
info("安装技能到 WorkBuddy...")
skill_mgr.install_all(get_sync_root() / "02_skills")
def step_deploy_mcp(dry_run: bool):
"""步骤4: 部署 MCP 配置"""
sync_root = get_sync_root()
mcp_src = sync_root / "01_core" / "mcp.json"
mcp_dst = Path.home() / ".workbuddy" / "mcp.json"
if not mcp_src.exists():
warn(f"MCP 配置源文件不存在: {mcp_src}")
return
if dry_run:
info(f" [DRY-RUN] 部署 MCP: {mcp_src} → {mcp_dst}")
return
# 备份现有配置
if mcp_dst.exists():
backup_dst = mcp_dst.with_suffix(".json.bak")
shutil.copy2(str(mcp_dst), str(backup_dst))
info(f" 备份旧 MCP 配置: {backup_dst}")
shutil.copy2(str(mcp_src), str(mcp_dst))
ok(f"MCP 配置已部署: {mcp_dst}")
def step_create_automations(dry_run: bool):
"""步骤5: 创建自动化任务"""
if dry_run:
info(f" [DRY-RUN] 创建 {len(AUTOMATIONS)} 个自动化任务")
return
# 自动化任务通过 WorkBuddy 自动化工具创建
# 当前方案:在 init 完成后提示用户手动在 WorkBuddy 中配置
info("自动化任务定义:")
for a in AUTOMATIONS:
info(f" 📋 {a['name']} — {a['rrule']}")
info("请在新机 WorkBuddy 中配置自动化任务,或运行 agentos sync 同步")
def step_rebuild_vector_db(dry_run: bool):
"""步骤6: 重建向量库"""
py = get_python()
script = get_sync_root() / "02_skills/kb_manager/vector_db_rebuild.py"
if not script.exists():
warn(f"向量库重建脚本不存在: {script},跳过")
return
if dry_run:
info(f" [DRY-RUN] 重建向量库: {py} {script}")
return
info("重建向量库...")
result = run([
py, str(script),
"--root", str(get_sync_root()),
"--local", str(get_local_root()),
"--track", "both",
])
if result.returncode == 0:
ok("向量库重建完成")
else:
warn(f"向量库重建失败:\n{result.stderr}")
def step_verify(dry_run: bool):
"""步骤7: 环境验证"""
if dry_run:
info(" [DRY-RUN] 环境验证跳过")
return
info("验证环境...")
# 验证 Python
py = get_python()
result = run([py, "--version"])
ok(f"Python: {result.stdout.strip()}" if result.returncode == 0 else "Python: 异常")
# 验证 oMLX
import json, urllib.request
try:
data = json.dumps({"model": "mlx-community--Qwen3-Embedding-0.6B-4bit-DWQ", "input": ["test"]}).encode()
req = urllib.request.Request(
"http://localhost:8000/v1/embeddings",
data=data,
headers={"Authorization": "Bearer 5omlx", "Content-Type": "application/json"},
method="POST",
)
resp = urllib.request.urlopen(req, timeout=5)
if resp.status == 200:
ok("oMLX Embedding API: 正常")
else:
warn(f"oMLX Embedding API: 状态码 {resp.status}")
except Exception as e:
warn(f"oMLX Embedding API: 连接失败 ({type(e).__name__})")
# 验证 facts.db
facts_db = get_sync_root() / "04_memory/long_term/facts.db"
if facts_db.exists():
size_kb = facts_db.stat().st_size / 1024
ok(f"facts.db: {size_kb:.1f} KB")
else:
warn("facts.db 不存在(首次初始化正常)")
def step_check_path(dry_run: bool):
"""检查 agentos CLI 是否在 PATH 中,不在则添加"""
local_bin = Path.home() / ".local" / "bin"
if not local_bin.exists():
return
if not (local_bin / "agentos").exists():
return
path_str = os.environ.get("PATH", "")
local_bin_str = str(local_bin)
if local_bin_str in path_str:
info(f"agentos CLI 已在 PATH 中: {local_bin}/agentos")
return
shell_rc = None
shell = os.environ.get("SHELL", "")
if "zsh" in shell:
shell_rc = Path.home() / ".zshrc"
elif "bash" in shell:
shell_rc = Path.home() / ".bash_profile"
if shell_rc and shell_rc.exists():
export_line = f'\nexport PATH="$HOME/.local/bin:$PATH"\n'
if dry_run:
info(f"[DRY-RUN] 将添加 PATH 到 {shell_rc}")
return
with open(shell_rc, "a") as f:
f.write(export_line)
ok(f"已添加 ~/.local/bin 到 PATH ({shell_rc})")
info(f"执行以下命令生效: source {shell_rc.name}")
else:
warn(f"~/.local/bin 未在 PATH 中")
info(f"手动添加: echo 'export PATH=\"$HOME/.local/bin:\$PATH\"' >> ~/.zshrc")
def do_init(args):
"""执行 init 流程"""
dry_run = args.dry_run
skip_deps = args.skip_deps
banner()
info("开始换机一键初始化" + (" [DRY-RUN]" if dry_run else ""))
print()
# 依次执行各步骤
step_local_dirs(dry_run)
print()
step_symlinks(dry_run)
print()
if not skip_deps:
from . import check as check_mod
check_mod.install_deps(dry_run)
print()
step_install_skills(dry_run)
print()
step_deploy_mcp(dry_run)
print()
step_create_automations(dry_run)
print()
step_rebuild_vector_db(dry_run)
print()
step_verify(dry_run)
# 检查 agentos CLI 是否在 PATH 中
step_check_path(dry_run)
print()
print("=" * 60)
if dry_run:
info("DRY-RUN 完成,以上为即将执行的操作")
else:
ok("初始化完成!请重启 WorkBuddy 使更改生效。")
info("后续操作:")
info(" 1. 重启 WorkBuddy")
info(" 2. 如有旧机备份,运行: agentos restore <备份文件>")
info(" 3. 运行: agentos upgrade 统一升级所有模块")
info(" 4. 运行: agentos check 验证系统状态")
info(" 5. 运行: agentos localize 生成本机身份配置")
print()
# 自动注册到集群
if not dry_run:
info("自动注册本机到集群...")
try:
import subprocess
cluster_script = str(get_sync_root() / "05_tools" / "01_system" / "cluster_registry.py")
result = subprocess.run(
[sys.executable, cluster_script, "register"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
ok("集群注册成功")
info("查看集群状态: agentos cluster-status")
else:
warn("集群注册失败(不影响初始化)")
except Exception as e:
warn(f"集群注册异常: {e}")
def do_localize(args):
"""从模板生成本机身份配置"""
from pathlib import Path
from .utils import get_sync_root, get_local_root, info, ok, warn
sync_root = get_sync_root()
local_root = get_local_root()
identity_dir = local_root / "identity"
identity_dir.mkdir(parents=True, exist_ok=True)
templates = {
sync_root / "01_core" / "IDENTITY.tpl.md": identity_dir / "IDENTITY.md",
sync_root / "01_core" / "USER.tpl.md": identity_dir / "USER.md",
sync_root / "01_core" / "HOST_ID.tpl.md": identity_dir / "HOST_ID.md",
}
dry_run = getattr(args, "dry_run", False)
print()
print("=" * 60)
print(" 🆔 AgentOS 本机身份生成")
print("=" * 60)
print()
for src, dst in templates.items():
if not src.exists():
warn(f"模板文件不存在: {src}")
continue
if dry_run:
info(f"[DRY-RUN] 生成: {dst}")
continue
if dst.exists():
ok(f" ✓ {dst.name} (跳过,已存在)")
continue
import shutil
shutil.copy2(str(src), str(dst))
ok(f" ✓ {dst.name}")
host_id = identity_dir / "HOST_ID.md"
if host_id.exists() and not dry_run:
print()
info("HOST_ID.md 已生成,默认角色为 node(最低权限)。")
info("如需提升权限,编辑角色字段:")
print(f" 文件路径: {host_id}")
print()
print(" 可选角色: master, maintainer, node")
print(" master → 知识提纯/记忆汇总/核心维护")
print(" maintainer → 内容采集/本地记忆/提交有价值内容")
print(" node → 信息采集/素材上传(默认)")
print()
info("安全建议:先以 node 运行,确认无误后再提升权限。")
print()
ok("本机身份文件生成完成")
info(f"身份目录: {identity_dir}")
info("可手动编辑 HOST_ID.md 修改角色和能力开关")