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,2 @@
# agentos - AgentOS 系统管理 CLI
# 统一入口:换机初始化 / 双机同步 / 技能管理 / 备份还原 / 健康检查
@@ -0,0 +1,3 @@
"""agentos CLI 入口 — python -m agentos"""
from .main import main
main()
@@ -0,0 +1,193 @@
"""
agentos backup / restore — 备份与还原 agent-local/
备份内容:
- materials/ (采集的原始素材)
- memory/raw/ (L3 记忆原文)
+ 备份清单 manifest.json
不备份:
- vector_db/ (可重建)
- runtime/cache/ (临时缓存)
"""
import sys
import json
import tarfile
import shutil
from pathlib import Path
from datetime import datetime
from .utils import get_sync_root, get_local_root, info, ok, warn, err, banner
def build_manifest(local_root: Path, backup_dirs: list) -> dict:
"""生成备份清单"""
manifest = {
"created_at": datetime.now().isoformat(),
"hostname": __import__("platform").node(),
"agentos_version": "1.0.0",
"items": [],
}
total_size = 0
entry_count = 0
for rel in backup_dirs:
path = local_root / rel
if not path.exists():
continue
if path.is_dir():
for f in path.rglob("*"):
if f.is_file() and f.name != ".DS_Store":
stat = f.stat()
total_size += stat.st_size
entry_count += 1
manifest["items"].append({
"path": str(f.relative_to(local_root)),
"size": stat.st_size,
"mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
})
else:
stat = path.stat()
total_size += stat.st_size
entry_count += 1
manifest["items"].append({
"path": str(path.relative_to(local_root)),
"size": stat.st_size,
"mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
})
manifest["total_size"] = total_size
manifest["entry_count"] = entry_count
return manifest
def cmd_backup(output_path: str = None, full: bool = False):
"""执行备份"""
banner()
local_root = get_local_root()
hostname = __import__("platform").node()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if not local_root.exists():
err(f"agent-local 目录不存在: {local_root}")
sys.exit(1)
# 确定备份目录
if full:
sync_root = get_sync_root()
backup_dirs = ["."] # 打包整个 agent-local/
archive_name = f"agentos_full_{hostname}_{timestamp}.tar.gz"
else:
backup_dirs = ["materials", "memory/raw"]
archive_name = f"agentos_local_{hostname}_{timestamp}.tar.gz"
# 输出路径
if output_path:
out_dir = Path(output_path)
else:
out_dir = Path.cwd()
out_dir.mkdir(parents=True, exist_ok=True)
archive_path = out_dir / archive_name
# 构建 manifest
info("扫描备份内容...")
manifest = build_manifest(local_root, backup_dirs)
if manifest["entry_count"] == 0:
warn("没有需要备份的文件")
return
print(f" 📊 待备份: {manifest['entry_count']} 个文件, {manifest['total_size'] / 1024 / 1024:.1f} MB")
print(f" 📁 来源: {local_root}")
# 创建 tar.gz
info("打包中...")
with tarfile.open(str(archive_path), "w:gz") as tar:
# 添加 manifest
manifest_bytes = json.dumps(manifest, indent=2, ensure_ascii=False).encode("utf-8")
import io
manifest_file = io.BytesIO(manifest_bytes)
tarinfo = tarfile.TarInfo(name="manifest.json")
tarinfo.size = len(manifest_bytes)
tar.addfile(tarinfo, manifest_file)
# 添加备份目录
for rel in backup_dirs:
path = local_root / rel
if path.exists():
tar.add(str(path), arcname=rel)
file_size_mb = archive_path.stat().st_size / 1024 / 1024
ok(f"备份完成: {archive_path} ({file_size_mb:.1f} MB)")
info(f"还原命令: agentos restore {archive_path}")
print()
def run_restore(args):
"""执行还原"""
banner()
backup_path = Path(args.backup_path)
if not backup_path.exists():
err(f"备份文件不存在: {backup_path}")
sys.exit(1)
local_root = get_local_root()
force = args.force
info(f"从备份还原: {backup_path}")
info(f"目标目录: {local_root}")
# 读取 manifest
manifest = None
with tarfile.open(str(backup_path), "r:gz") as tar:
try:
mf = tar.extractfile("manifest.json")
if mf:
manifest = json.loads(mf.read().decode("utf-8"))
except KeyError:
pass
if manifest:
print(f" 📊 备份信息: {manifest['entry_count']} 个文件, {manifest['total_size'] / 1024 / 1024:.1f} MB")
print(f" 🖥️ 来源机器: {manifest.get('hostname', '?')}")
print(f" 🕐 备份时间: {manifest.get('created_at', '?')}")
else:
info(" 备份文件无 manifest.json(旧版本备份)")
# 冲突检查
conflicts = []
if manifest and not force:
for item in manifest["items"]:
dest = local_root / item["path"]
if dest.exists():
conflicts.append(item["path"])
if conflicts:
print(f"\n⚠️ 发现 {len(conflicts)} 个已存在的文件: (使用 --force 覆盖)")
for c in conflicts[:10]:
print(f" {c}")
if len(conflicts) > 10:
print(f" ... 还有 {len(conflicts) - 10} 个")
print()
# 确认
if not force:
reply = input("是否继续还原?(y/N): ").strip().lower()
if reply != "y":
info("已取消")
return
# 执行还原
info("还原中...")
with tarfile.open(str(backup_path), "r:gz") as tar:
tar.extractall(path=str(local_root), filter="data")
ok("还原完成")
info("后续操作:")
info(" 1. 运行 agentos check 验证状态")
info(" 2. 如果向量库未一起还原,运行 agentos init 重建")
print()
def run(args):
cmd_backup(output_path=args.output, full=args.full)
@@ -0,0 +1,228 @@
"""
agentos check — 全系统健康检查
"""
import sys
import subprocess
from pathlib import Path
from .utils import (
get_sync_root, get_local_root, get_python, get_npx,
info, ok, warn, err, run, banner
)
def check_item(name: str, status: str, detail: str = ""):
"""输出检查项"""
icon = {"ok": "✅", "warn": "⚠️ ", "fail": "❌"}
label = {"ok": "通过", "warn": "警告", "fail": "失败"}
print(f" {icon.get(status, '❓')} [{label.get(status, '未知')}] {name}")
if detail:
print(f" {detail}")
def cmd_check(quick: bool = False):
"""执行全系统健康检查"""
banner()
sync_root = get_sync_root()
local_root = get_local_root()
print("=" * 60)
print(" 全系统健康检查")
print("=" * 60)
print()
# 1. 目录结构
print("📁 目录结构:")
check_item("agent-sync 根目录", "ok" if sync_root.exists() else "fail", str(sync_root))
check_item("agent-local 根目录", "ok" if local_root.exists() else "fail", str(local_root))
sync_skills = sync_root / "02_skills"
sync_skills_count = len([d for d in sync_skills.iterdir() if d.is_dir() and (d / "SKILL.md").exists()]) if sync_skills.exists() else 0
check_item(f"技能目录 ({sync_skills_count} 个)", "ok" if sync_skills_count > 0 else "warn")
local_materials = local_root / "materials" if local_root.exists() else None
local_raw = local_root / "memory/raw" if local_root.exists() else None
materials_exist = local_materials and local_materials.exists()
raw_exist = local_raw and local_raw.exists()
check_item("materials/", "ok" if materials_exist else "warn", "采集素材目录")
check_item("memory/raw/", "ok" if raw_exist else "warn", "L3 记忆原文目录")
# 2. 软链接
print("\n🔗 软链接:")
symlinks_to_check = [
("04_memory/long_term/raw", local_root / "memory/raw"),
("04_memory/vector_db", local_root / "memory/vector_db"),
("06_runtime/cache", local_root / "runtime/cache"),
]
for link_rel, target in symlinks_to_check:
link_path = sync_root / link_rel
if link_path.is_symlink():
actual = link_path.resolve()
expected = target.resolve()
if actual == expected:
check_item(f"{link_rel} → OK", "ok")
else:
check_item(f"{link_rel} → 指向错误", "warn", f"实际: {actual}, 期望: {expected}")
elif link_path.exists():
check_item(f"{link_rel} → 真实目录", "ok")
else:
check_item(f"{link_rel} → 不存在", "fail")
# 3. Python 环境
print("\n🐍 Python 环境:")
py = get_python()
if Path(py).exists():
result = run([py, "--version"])
check_item("Python 版本", "ok" if result.returncode == 0 else "fail", result.stdout.strip() if result.returncode == 0 else "")
else:
check_item("Python 可执行文件", "fail", py)
# 关键依赖检查
deps = ["chromadb", "trafilatura", "feedparser", "sqlite_utils"]
for dep in deps:
result = run([py, "-c", f"import {dep}"])
check_item(f"依赖: {dep}", "ok" if result.returncode == 0 else "fail")
if quick:
print("\n" + "=" * 60)
ok("快速检查完成")
return
# 4. oMLX
print("\n🤖 本地 LLM 引擎:")
try:
import json, urllib.request
data = json.dumps({"model": "Qwen3-Embedding-0.6B-4bit-DWQ", "input": ["test"]}).encode()
req = urllib.request.Request(
"http://localhost:8000/v1/embeddings",
data=data,
headers={"Authorization": "Bearer omlx", "Content-Type": "application/json"},
method="POST",
)
resp = urllib.request.urlopen(req, timeout=5)
check_item("oMLX Embedding API", "ok" if resp.status == 200 else "fail", f"状态码 {resp.status}")
except Exception as e:
check_item("oMLX Embedding API", "fail", str(e))
# 音频转文字能力检查
print("\n🎙️ 音频转文字:")
ffmpeg = run(["which", "ffmpeg"])
if ffmpeg.returncode == 0:
check_item("ffmpeg(系统工具)", "ok", "已安装")
else:
check_item("ffmpeg(系统工具)", "warn", "未安装,音频转文字不可用。运行: brew install ffmpeg")
whisper_check = run([py, "-m", "pip", "show", "openai-whisper"])
if whisper_check.returncode == 0:
import re
ver_match = re.search(r"Version:\s*(\S+)", whisper_check.stdout)
ver = ver_match.group(1) if ver_match else "?"
check_item("openai-whisper(Python包)", "ok", f"v{ver}")
# 检查模型是否已下载
import os as _os
whisper_cache = _os.path.expanduser("~/.cache/whisper")
if _os.path.exists(whisper_cache):
models = [f for f in _os.listdir(whisper_cache) if f.endswith(('.pt', '.bin'))]
if models:
model_sizes = [_os.path.getsize(_os.path.join(whisper_cache, m)) / 1024 / 1024 for m in models]
detail = ", ".join([f"{m} ({s:.0f}MB)" for m, s in zip(models, model_sizes)])
check_item("Whisper 模型权重", "ok", f"已缓存: {detail}")
else:
check_item("Whisper 模型权重", "warn", "首次使用时会自动下载(~1.5GB)")
else:
check_item("Whisper 模型权重", "warn", "未下载,首次运行自动下载(~1.5GB)")
else:
check_item("openai-whisper(Python包)", "fail", "未安装,运行: pip install openai-whisper")
# 5. facts.db
print("\n🗄️ 记忆层:")
facts_db = sync_root / "04_memory" / "long_term" / "facts.db"
if facts_db.exists():
size_kb = facts_db.stat().st_size / 1024
result = run([py, "-c", f"""
import sqlite3
conn = sqlite3.connect('{facts_db}')
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM facts")
count = c.fetchone()[0]
c.execute("SELECT COUNT(DISTINCT subject) FROM facts")
subjects = c.fetchone()[0]
conn.close()
print(f"条目: {{count}}, 主题: {{subjects}}")
"""])
check_item(f"facts.db ({size_kb:.1f} KB)", "ok", result.stdout.strip() if result.returncode == 0 else "读取异常")
else:
check_item("facts.db", "warn", "不存在(首次初始化正常)")
# 6. 向量库
vector_db = local_root / "vector_db" / "global" / "chroma"
if vector_db.exists():
chroma_files = list(vector_db.glob("*.sqlite3"))
check_item("global 向量库", "ok" if chroma_files else "warn", f"目录存在但无 .sqlite3 文件" if not chroma_files else "")
else:
check_item("global 向量库", "warn", "未建,运行 agentos init 或 vector_db_rebuild.py 重建")
# 7. WorkBuddy 技能注册
print("\n📦 技能注册状态:")
wb_skills = Path.home() / ".workbuddy" / "skills"
sync_skills_dir = sync_root / "02_skills"
if wb_skills.exists() and sync_skills_dir.exists():
wb_names = {d.name for d in wb_skills.iterdir() if d.is_dir()}
src_names = {d.name for d in sync_skills_dir.iterdir() if d.is_dir() and (d / "SKILL.md").exists()}
missing = src_names - wb_names
extra = wb_names - src_names
if missing:
check_item(f"未注册到 WorkBuddy ({len(missing)} 个)", "warn", ", ".join(sorted(missing)))
if extra:
check_item(f"WB 中多余的 ({len(extra)} 个)", "warn", ", ".join(sorted(extra)))
if not missing and not extra:
check_item(f"技能一致性 ({len(src_names)} 个)", "ok", "源目录与 WB 注册一致")
# 8. 磁盘
print("\n💾 磁盘空间:")
import shutil
usage = shutil.disk_usage(str(Path.home()))
free_gb = usage.free / (1024 ** 3)
total_gb = usage.total / (1024 ** 3)
pct = usage.free / usage.total * 100
status = "ok" if pct > 10 else "warn"
check_item(f"磁盘剩余", status, f"{free_gb:.0f} GB / {total_gb:.0f} GB ({pct:.0f}%)")
print()
print("=" * 60)
ok("健康检查完成")
print()
def install_deps(dry_run: bool = False):
"""安装 Python 依赖(被 init 调用)"""
py = get_python()
req_file = get_sync_root() / "requirements.txt"
if not req_file.exists():
warn(f"requirements.txt 不存在: {req_file}")
return
if dry_run:
info(f" [DRY-RUN] pip install -r {req_file}")
return
info("安装 Python 依赖...")
result = run([py, "-m", "pip", "install", "-r", str(req_file), "--quiet"])
if result.returncode == 0:
ok("Python 依赖安装完成")
else:
warn(f"部分依赖安装失败:\n{result.stderr[:200]}")
# 检查并提示安装 ffmpeg(音频转文字需要)
ffmpeg = run(["which", "ffmpeg"])
if ffmpeg.returncode != 0:
print()
warn("音频转文字功能需要 ffmpeg 系统工具")
info(" 运行以下命令安装:")
info(" brew install ffmpeg")
print()
def cmd_run(args):
cmd_check(quick=args.quick)
@@ -0,0 +1,424 @@
"""
agentos config — 配置管理模块 (v1.0.0)
管理 01_core/ ←→ ~/.workbuddy/ 的配置同步。
三类配置:
A: 自动覆盖(内核身份文件)
B: 对比选择(服务配置,diff 后用户选择)
C: 仅报告(本机个性化,不同步)
子命令:
agentos config status 查看配置状态
agentos config diff 查看差异详情
agentos config apply 应用配置更新
agentos config rollback 回滚到备份
最后更新: 2026-05-02
"""
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
from .const import __agent_sync_root__, __agent_local_root__, __version__
SYNC_ROOT = Path(__agent_sync_root__)
MANIFEST_PATH = SYNC_ROOT / "01_core" / "CONFIG_MANIFEST.yaml"
# ══════════════════════════════════════════════════════════════
# 配置清单解析
# ══════════════════════════════════════════════════════════════
def load_manifest() -> list:
"""解析 CONFIG_MANIFEST.yaml,返回配置项列表"""
import yaml
if not MANIFEST_PATH.exists():
print(f"❌ 未找到配置清单: {MANIFEST_PATH}")
return []
with open(MANIFEST_PATH) as f:
data = yaml.safe_load(f)
return data.get("configs", [])
def resolve_path(path_str: str) -> Optional[Path]:
"""将 ~/ 开头的路径解析为绝对路径"""
if not path_str:
return None
path_str = path_str.replace("~", str(Path.home()))
p = Path(path_str)
return p if p.exists() else p # 即使不存在也返回路径对象
# ══════════════════════════════════════════════════════════════
# 状态检查
# ══════════════════════════════════════════════════════════════
def check_status() -> list:
"""检查所有配置项状态,返回状态列表"""
items = load_manifest()
if not items:
return []
results = []
for item in items:
cat = item["category"]
name = item["file"]
source = resolve_path(item.get("source"))
target = resolve_path(item.get("target"))
status = {
"name": name,
"category": cat,
"source": str(source) if source else "—",
"target": str(target) if target else "—",
"exists_source": source and source.exists(),
"exists_target": target and target.exists(),
"identical": False,
"diff_lines": 0,
}
# 比较内容
if status["exists_source"] and status["exists_target"]:
try:
s_content = source.read_bytes()
t_content = target.read_bytes()
status["identical"] = s_content == t_content
if not status["identical"]:
# 粗略统计差异行数
s_lines = s_content.decode().splitlines()
t_lines = t_content.decode().splitlines()
status["diff_lines"] = abs(len(s_lines) - len(t_lines))
except Exception:
status["identical"] = False
results.append(status)
return results
# ══════════════════════════════════════════════════════════════
# 差异显示
# ══════════════════════════════════════════════════════════════
def show_diff(item: dict):
"""显示指定配置项的 diff"""
source = resolve_path(item.get("source"))
target = resolve_path(item.get("target"))
if not source or not source.exists():
print(f" ⚠️ 源文件不存在: {source}")
return
if not target or not target.exists():
print(f" ⚠️ 目标文件不存在: {target}")
print(f" → 将创建: {target}")
return
try:
result = subprocess.run(
["diff", "-u", str(source), str(target)],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
print(f" ✅ 文件一致,无差异")
else:
diff_out = result.stdout
lines = diff_out.splitlines()
# 只显示前 30 行
display = lines[:30]
if len(lines) > 30:
display.append(f" ... (还有 {len(lines)-30} 行差异)")
for line in display:
marker = " "
if line.startswith("+"):
marker = "+"
elif line.startswith("-"):
marker = "-"
elif line.startswith("@@"):
marker = "@"
print(f" {marker} {line}")
except Exception as e:
print(f" ❌ diff 失败: {e}")
# ══════════════════════════════════════════════════════════════
# 备份
# ══════════════════════════════════════════════════════════════
def backup_targets(items: list) -> Optional[Path]:
"""备份所有目标文件到时间戳目录"""
backup_dir = Path.home() / ".workbuddy" / f"config_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
count = 0
for item in items:
if item["category"] == "C":
continue
target = resolve_path(item.get("target"))
if target and target.exists():
backup_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(target), str(backup_dir / target.name))
count += 1
if count > 0:
print(f" 💾 已备份 {count} 个文件到: {backup_dir}")
return backup_dir
return None
# ══════════════════════════════════════════════════════════════
# 应用配置
# ══════════════════════════════════════════════════════════════
def apply_config(item: dict) -> bool:
"""应用单个配置项"""
cat = item["category"]
name = item["file"]
source = resolve_path(item.get("source"))
target = resolve_path(item.get("target"))
if not target:
print(f" ⚠️ {name}: 目标路径为空,跳过")
return False
if cat == "A":
# A类: 自动覆盖
if not source or not source.exists():
print(f" ⚠️ {name}: 源文件不存在 ({source}),跳过")
return False
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(source), str(target))
print(f" ✅ {name}: 已部署(A类自动覆盖)")
return True
elif cat == "B":
# B类: 对比后选择
if not source or not source.exists():
print(f" ⚠️ {name}: 源文件不存在 ({source}),跳过")
return False
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
# 检查是否一致
try:
s_content = source.read_bytes()
t_content = target.read_bytes()
if s_content == t_content:
print(f" ✅ {name}: 一致,无需更新")
return True
except Exception:
pass
# 有差异,尝试交互(非交互环境自动跳过)
print(f"\n 📋 {name}: 源与目标存在差异")
print(f" 源: {source}")
print(f" 目标: {target}")
show_diff(item)
print()
try:
choice = input(f" 选择 [K]保留本地 [N]用新版 [D]查看差异 [S]跳过: ").strip().upper()
except (EOFError, OSError):
print(f" ⏭️ {name}: 非交互环境,跳过(运行 agentos config diff {name} 手动处理)")
return False
while True:
if choice == "K":
print(f" ⏭️ {name}: 保留本地版本")
return False
elif choice == "N":
shutil.copy2(str(source), str(target))
print(f" ✅ {name}: 已更新为新版")
return True
elif choice == "D":
show_diff(item)
break
elif choice == "S":
print(f" ⏭️ {name}: 跳过")
return False
else:
break
else:
# 目标不存在,直接部署
shutil.copy2(str(source), str(target))
print(f" ✅ {name}: 已创建(首次部署)")
return True
elif cat == "C":
print(f" ⏭️ {name}: C类本机配置,不同步")
return False
return False
# ══════════════════════════════════════════════════════════════
# CLI 命令
# ══════════════════════════════════════════════════════════════
def cmd_status(args):
"""agentos config status — 查看配置状态"""
results = check_status()
if not results:
print("❌ 无法读取配置清单")
return
print(f"\n{'='*60}")
print(f" 📋 AgentOS 配置状态")
print(f"{'='*60}\n")
for r in results:
cat = r["category"]
name = r["name"]
tag = {"A": "🔵 A-自动", "B": "🟡 B-选择", "C": "⚪ C-本地"}.get(cat, "?")
if cat == "C":
exists = r["exists_target"]
print(f" {tag} {name:20s} {'✅ 存在' if exists else '⏹️ 不存在'}")
elif r["identical"]:
print(f" {tag} {name:20s} ✅ 一致")
elif r["exists_source"] and r["exists_target"]:
print(f" {tag} {name:20s} ⚠️ 有差异(~{r['diff_lines']}行不同)")
elif r["exists_source"] and not r["exists_target"]:
print(f" {tag} {name:20s} 📋 待部署(目标不存在)")
elif not r["exists_source"]:
print(f" {tag} {name:20s} ❌ 源文件不存在")
else:
print(f" {tag} {name:20s} ❓ 未知")
print(f"\n{'─'*60}")
a_count = sum(1 for r in results if r["category"] == "A" and not r["identical"])
b_count = sum(1 for r in results if r["category"] == "B" and not r["identical"])
if a_count + b_count > 0:
print(f" ⚠️ {a_count} 个 A类 + {b_count} 个 B类 待处理")
print(f" 运行: agentos config apply")
else:
print(f" ✅ 所有配置一致")
print()
def cmd_diff(args):
"""agentos config diff — 查看所有差异"""
results = check_status()
if not results:
return
items = load_manifest()
for i, r in enumerate(results):
if r["identical"] or r["category"] == "C":
continue
if r["exists_source"] and r["exists_target"]:
print(f"\n{'='*60}")
print(f" 📄 {r['name']}")
print(f"{'='*60}")
show_diff(items[i])
print()
def cmd_apply(args):
"""agentos config apply — 应用配置更新"""
items = load_manifest()
if not items:
return
print(f"\n{'='*60}")
print(f" 🔄 AgentOS 配置更新")
print(f"{'='*60}\n")
# 先备份
print(f"▶ 备份现有配置...")
backup_dir = backup_targets(items)
if backup_dir:
print(f" 回滚命令: agentos config rollback {backup_dir.name}")
print()
# 逐项应用
changed = 0
for item in items:
if item["category"] == "C":
continue
ok = apply_config(item)
if ok:
changed += 1
print(f"\n{'─'*60}")
if changed > 0:
print(f" ✅ 已更新 {changed} 个配置项")
print(f" 提示: 如果 WorkBuddy 正在运行,可能需要重启生效")
else:
print(f" ⏭️ 无变更")
print()
def cmd_rollback(args):
"""agentos config rollback — 回滚配置"""
backup_name = getattr(args, "backup_name", None)
backup_base = Path.home() / ".workbuddy"
if backup_name:
backup_dir = backup_base / backup_name
else:
# 找最近的备份
backups = sorted(backup_base.glob("config_backup_*"))
if not backups:
print("❌ 未找到备份")
return
backup_dir = backups[-1]
print(f" 使用最近备份: {backup_dir.name}")
if not backup_dir.exists():
print(f"❌ 备份目录不存在: {backup_dir}")
return
files = list(backup_dir.iterdir())
if not files:
print(f"❌ 备份目录为空: {backup_dir}")
return
print(f"\n{'='*60}")
print(f" ↩️ 回滚配置")
print(f"{'='*60}\n")
print(f" 从: {backup_dir}")
print()
for f in files:
target = Path.home() / ".workbuddy" / f.name
shutil.copy2(str(f), str(target))
print(f" ✅ {f.name}: 已恢复")
print(f"\n 共恢复 {len(files)} 个文件")
print()
# ══════════════════════════════════════════════════════════════
# 子命令注册
# ══════════════════════════════════════════════════════════════
def setup_parser(subparsers):
"""向主 CLI 注册 config 子命令"""
p = subparsers.add_parser("config", help="配置管理:状态查看 / 差异对比 / 部署 / 回滚")
config_sub = p.add_subparsers(dest="config_cmd", help="配置子命令")
config_sub.add_parser("status", help="查看配置状态(哪些文件有差异)")
config_sub.add_parser("diff", help="查看配置差异详情")
config_sub.add_parser("apply", help="应用配置更新(A类自动 / B类选择)")
p_rollback = config_sub.add_parser("rollback", help="回滚到上次备份")
p_rollback.add_argument("backup_name", nargs="?", default=None,
help="备份目录名(可选,默认使用最近备份)")
def run(args):
cmd = args.config_cmd
if not cmd:
print("agentos config 子命令: status | diff | apply | rollback")
return
if cmd == "status":
cmd_status(args)
elif cmd == "diff":
cmd_diff(args)
elif cmd == "apply":
cmd_apply(args)
elif cmd == "rollback":
cmd_rollback(args)
@@ -0,0 +1,30 @@
"""
agentos - AgentOS 系统管理 CLI
=================================
统一入口,管理 AgentOS 的全部系统级操作。
用法:
agentos 显示帮助
agentos init 换机一键初始化
agentos sync 双机增量同步
agentos skill list 列出所有技能
agentos skill install 安装技能到 WorkBuddy
agentos skill build 打包技能为 .skill 包
agentos skill search 搜索技能
agentos tool list 列出所有工具
agentos check 全系统健康检查
agentos upgrade [--module <name>] 统一模块升级(拉取代码 + 安装依赖 + 环境检查)
agentos config status 查看配置状态
agentos config diff 查看配置差异
agentos config apply 部署配置(A类自动 / B类选择)
agentos config rollback 回滚至备份
agentos backup 备份 agent-local/(materials + memory/raw)
agentos restore <路径> 从备份还原 agent-local/
"""
from pathlib import Path
__version__ = "2.0.0"
__agent_sync_root__ = str(Path.home() / "workbuddy-agent-os" / "agent-sync")
__agent_local_root__ = str(Path.home() / "workbuddy-agent-os" / "agent-local")
@@ -0,0 +1,385 @@
"""
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 修改角色和能力开关")
@@ -0,0 +1,69 @@
#!/bin/bash
# agentos CLI 安装脚本
# 将 agentos 安装到 ~/.local/bin/ 并将其添加到 PATH
set -euo pipefail
AGENTOS_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_DIR="$HOME/.local/bin"
echo "========================================="
echo " AgentOS CLI 安装"
echo "========================================="
echo ""
echo "来源: $AGENTOS_DIR"
echo "目标: $INSTALL_DIR/agentos"
echo ""
# 创建 ~/.local/bin/
mkdir -p "$INSTALL_DIR"
# 创建入口脚本
cat > "$INSTALL_DIR/agentos" << 'ENTRYEOF'
#!/usr/bin/env python3
"""agentos CLI 入口:自动定位到 agent-sync 并启动主程序"""
import sys
import os
from pathlib import Path
# 尝试定位 agent-sync 根目录
CANDIDATES = [
os.environ.get("AGENT_SYNC_ROOT"),
str(Path.home() / "workbuddy-agent-os" / "agent-sync"),
str(Path.home() / "agent-os"),
]
AGENTOS_MODULE = None
for root in CANDIDATES:
if root:
module_path = Path(root) / "05_tools" / "00_setup" / "agentos"
if module_path.exists() and (module_path / "main.py").exists():
AGENTOS_MODULE = str(module_path)
break
if not AGENTOS_MODULE:
print("❌ 无法定位 AgentOS 模块。请设置环境变量 AGENT_SYNC_ROOT")
sys.exit(1)
sys.path.insert(0, str(Path(AGENTOS_MODULE).parent))
from agentos.main import main
main()
ENTRYEOF
chmod +x "$INSTALL_DIR/agentos"
# 检查 PATH 是否包含 ~/.local/bin
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
echo "⚠️ 请将 ~/.local/bin 添加到 PATH:"
echo ""
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshrc"
echo " source ~/.zshrc"
echo ""
fi
echo "✅ 安装完成!"
echo ""
echo "运行测试: agentos --version"
echo "帮助: agentos --help"
echo ""
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
agentos — AgentOS 统一命令入口
系统管理:
agentos init 换机一键初始化
agentos sync 双机增量同步
agentos skill <子命令> 技能管理
agentos tool <子命令> 工具管理
agentos config <子命令> 配置管理
agentos check 全系统健康检查
agentos backup 备份 agent-local/
agentos upgrade 模块升级
agentos restore <路径> 从备份还原
agentos rebuild-vector 重建向量数据库
agentos localize 生成本机身份
agentos register 注册到集群
agentos cluster-status 集群状态
agentos cluster-cleanup 清理集群
联邦管理(来自 07_matrix/scripts/agentos/plugins/):
agentos matrix 社交矩阵 — 抖音/小红书运营
agentos ave 视频工厂 — 视频制作与编辑
agentos crawl 内容采集 — 互联网内容抓取
agentos fleet 联邦管理 — 多机协同
agentos serve 服务管理 — MCP/Dashboard/调度
"""
import sys
import argparse
import importlib.util
from pathlib import Path
from .const import __version__
from .utils import banner, get_sync_root
def _load_federation_plugins():
"""从 07_matrix/scripts/agentos/ 加载联邦插件"""
sync_root = get_sync_root()
plugin_base = sync_root / "05_tools" / "07_matrix" / "scripts" / "agentos" / "plugins"
if not plugin_base.exists():
return []
# 动态加载 agentos.base 模块
agentos_pkg = plugin_base.parent
spec = importlib.util.spec_from_file_location(
"agentos.base",
agentos_pkg / "base.py",
submodule_search_locations=[str(agentos_pkg)]
)
if not spec or not spec.loader:
return []
base_mod = importlib.util.module_from_spec(spec)
sys.modules["agentos.base"] = base_mod
spec.loader.exec_module(base_mod)
# 扫描 plugins/ 下的每个 .py 文件
plugins = []
for f in sorted(plugin_base.iterdir()):
if f.name.startswith("_") or f.suffix != ".py":
continue
# 动态加载插件模块
mod_name = f"agentos.plugins.{f.stem}"
spec = importlib.util.spec_from_file_location(
mod_name, f,
submodule_search_locations=[str(plugin_base)]
)
if not spec or not spec.loader:
continue
mod = importlib.util.module_from_spec(spec)
# 让模块中的 `from agentos.base import ...` 能找到 base
sys.modules["agentos"] = type(sys)("agentos")
sys.modules["agentos"].plugins = type(sys)("plugins")
sys.modules[mod_name] = mod
spec.loader.exec_module(mod)
# 查找 AgentOSPlugin 子类
for attr_name in dir(mod):
attr = getattr(mod, attr_name)
if isinstance(attr, type) and issubclass(attr, base_mod.AgentOSPlugin) and attr is not base_mod.AgentOSPlugin:
plugins.append(attr())
return plugins, base_mod
def main():
parser = argparse.ArgumentParser(
prog="agentos",
description="AgentOS 统一命令入口 — 系统管理 + 联邦管理",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
系统管理:
agentos init / sync / skill / tool / config / check
agentos backup / restore / upgrade / rebuild-vector
agentos register / cluster-status / cluster-cleanup
联邦管理:
agentos matrix / ave / crawl / fleet / serve
参见:
agentos <command> --help
""",
)
parser.add_argument(
"--version", action="version", version=f"agentos v{__version__}"
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# ---- 系统管理命令 ----
p_init = subparsers.add_parser(
"init", help="换机一键初始化:目录/依赖/技能/MCP/自动化"
)
p_init.add_argument("--dry-run", action="store_true", help="仅预览,不执行")
p_init.add_argument("--skip-deps", action="store_true", help="跳过依赖安装")
p_sync = subparsers.add_parser(
"sync", help="双机增量同步:检测差异并更新本地 WorkBuddy"
)
p_sync.add_argument("--dry-run", action="store_true", help="仅报告差异,不执行")
p_sync.add_argument("--force", action="store_true", help="强制全量覆盖")
p_skill = subparsers.add_parser("skill", help="技能管理")
skill_sub = p_skill.add_subparsers(dest="skill_cmd", help="技能子命令")
p_skill_list = skill_sub.add_parser("list", help="列出所有可用技能")
p_skill_list.add_argument("--verbose", "-v", action="store_true", help="显示详细信息")
skill_sub.add_parser("install", help="安装/更新所有技能到 WorkBuddy")
skill_sub.add_parser("check", help="检查技能一致性")
p_skill_build = skill_sub.add_parser("build", help="打包技能为 .skill 压缩包")
p_skill_build.add_argument("skill_name", help="技能名称(对应 02_skills/ 下的目录名)")
p_skill_build.add_argument("--output", "-o", default=None, help="输出路径(默认: 07_migration/exports/)")
p_skill_search = skill_sub.add_parser("search", help="搜索技能")
p_skill_search.add_argument("keyword", help="搜索关键词")
p_skill_rm = skill_sub.add_parser("uninstall", help="从 WorkBuddy 卸载技能")
p_skill_rm.add_argument("skill_name", help="技能名称")
p_tool = subparsers.add_parser("tool", help="工具管理")
tool_sub = p_tool.add_subparsers(dest="tool_cmd", help="工具子命令")
tool_sub.add_parser("list", help="列出所有可用工具")
p_tool_trae = tool_sub.add_parser("trae", help="Trae Agent AI 编程助手")
trae_sub = p_tool_trae.add_subparsers(dest="trae_cmd", help="trae 子命令")
p_trae_run = trae_sub.add_parser("run", help="执行任务")
p_trae_run.add_argument("task", nargs="*", help="任务描述")
p_trae_run.add_argument("-p", "--provider", help="LLM 提供商")
p_trae_run.add_argument("-m", "--model", help="模型名称")
p_trae_run.add_argument("-f", "--file", help="从文件读取任务")
trae_sub.add_parser("interactive", help="交互式模式")
trae_sub.add_parser("config", help="查看配置")
trae_sub.add_parser("install", help="安装 trae-agent")
from . import config_mgr as config_mod
config_mod.setup_parser(subparsers)
p_check = subparsers.add_parser("check", help="全系统健康检查")
p_check.add_argument("--quick", action="store_true", help="快速检查(仅核心项)")
p_backup = subparsers.add_parser("backup", help="备份 agent-local/(materials + memory/raw)")
p_backup.add_argument("--output", "-o", default=None, help="输出目录(默认: 当前目录)")
p_backup.add_argument("--full", action="store_true", help="打包 agent-sync/ + agent-local/ 全量")
from . import upgrade as upgrade_mod
upgrade_mod.setup_parser(subparsers)
p_restore = subparsers.add_parser("restore", help="从备份文件还原 agent-local/")
p_restore.add_argument("backup_path", help="备份文件路径(.tar.gz)")
p_restore.add_argument("--force", action="store_true", help="覆盖现有文件")
p_vector = subparsers.add_parser("rebuild-vector", help="本地重建向量数据库(升级后执行)")
p_vector.add_argument("--track", choices=["local", "global", "both"], default="both", help="重建范围 (默认 both)")
p_vector.add_argument("--incremental", action="store_true", help="增量模式")
p_vector.add_argument("--dry-run", action="store_true", help="仅预览,不执行")
subparsers.add_parser("localize", help="从模板生成本机身份配置")
subparsers.add_parser("register", help="注册本机到集群注册表")
subparsers.add_parser("cluster-status", help="查看集群所有机器状态")
subparsers.add_parser("cluster-cleanup", help="清理过期注册条目")
# ---- 联邦管理命令(从 07_matrix/ 插件自动加载)----
fed_plugins = []
fed_base_mod = None
try:
fed_plugins, fed_base_mod = _load_federation_plugins()
for plugin in fed_plugins:
plugin.register(subparsers)
except Exception:
pass # 插件加载失败不影响系统命令
# ---- 解析 ----
args = parser.parse_args()
if not args.command:
banner()
parser.print_help()
print()
print(" 输入 agentos <command> --help 查看子命令详情")
print()
return
# ---- 路由到系统管理模块 ----
sys_commands = {
"init": (".init", "do_init"),
"sync": (".sync", "run"),
"skill": (".skill_mgr", "run"),
"tool": (".tool_mgr", "run"),
"config": (".config_mgr", "run"),
"check": (".check", "cmd_run"),
"backup": (".backup", "run"),
"upgrade": (".upgrade", "do_upgrade"),
"restore": (".backup", "run_restore"),
"rebuild-vector": (".upgrade", "do_rebuild_vector"),
"localize": (".init", "do_localize"),
"register": (".upgrade", "do_cluster_register"),
"cluster-status": (".upgrade", "do_cluster_status"),
"cluster-cleanup": (".upgrade", "do_cluster_cleanup"),
}
if args.command in sys_commands:
mod_path, func_name = sys_commands[args.command]
import importlib
mod = importlib.import_module(mod_path, "agentos")
getattr(mod, func_name)(args)
return
# ---- 路由到联邦插件 ----
for plugin in fed_plugins:
if args.command == plugin.name:
sys.exit(plugin.dispatch(args))
# 未识别的命令
print(f"未知命令: {args.command}")
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,275 @@
"""
agentos skill — 技能管理
子命令: list / install / check / build / search / uninstall
"""
import sys
import json
import shutil
import tarfile
from pathlib import Path
from datetime import datetime
from .utils import (
get_sync_root, get_local_root, get_python,
info, ok, warn, err, run, banner
)
# ──────────────────────────────────────────
# 内部函数
# ──────────────────────────────────────────
def get_skill_dirs(skills_root: Path) -> list:
"""从 skills_root 扫描所有技能目录"""
if not skills_root.exists():
return []
result = []
for d in sorted(skills_root.iterdir()):
if d.is_dir() and (d / "SKILL.md").exists():
result.append(d)
return result
def read_skill_card(skill_dir: Path) -> dict:
"""读取 SKILL_CARD.yaml 或 SKILL.md 中的元数据"""
card = {"name": skill_dir.name, "version": "?", "status": "?", "description": ""}
# 优先读 SKILL_CARD.yaml
yaml_path = skill_dir / "SKILL_CARD.yaml"
if yaml_path.exists():
try:
import yaml
with open(yaml_path) as f:
data = yaml.safe_load(f) or {}
card["version"] = data.get("version", "?")
card["status"] = data.get("status", "?")
card["description"] = data.get("description", "")
except Exception:
pass
# 从 SKILL.md 提取触发词
skill_md = skill_dir / "SKILL.md"
if skill_md.exists():
content = skill_md.read_text()
triggers = []
for line in content.split("\n"):
line = line.strip()
if line.startswith("- "):
triggers.append(line[2:])
card["triggers"] = triggers[:5] # 最多取前5个
return card
def get_workbuddy_skills_dir() -> Path:
"""获取 WorkBuddy 技能目录"""
return Path.home() / ".workbuddy" / "skills"
# ──────────────────────────────────────────
# 子命令
# ──────────────────────────────────────────
def cmd_list(verbose: bool):
"""列出所有可用技能"""
banner()
sync_root = get_sync_root()
skills_root = sync_root / "02_skills"
dirs = get_skill_dirs(skills_root)
if not dirs:
info(f"技能目录为空: {skills_root}")
return
print(f"{'技能名':<22} {'版本':<8} {'状态':<10} {'描述'}")
print("-" * 70)
for d in dirs:
card = read_skill_card(d)
desc = card.get("description") or ""
if isinstance(desc, dict):
desc = desc.get("short", "")
desc = desc[:40] if isinstance(desc, str) else ""
print(f"{d.name:<22} {card['version']:<8} {card['status']:<10} {desc}")
print(f"\n共 {len(dirs)} 个技能 ({skills_root})")
print()
if verbose:
print("触发词详情:")
for d in dirs:
card = read_skill_card(d)
triggers = card.get("triggers", [])
if triggers:
print(f" {d.name}: {' | '.join(triggers)}")
def cmd_install():
"""安装所有技能到 WorkBuddy"""
banner()
sync_root = get_sync_root()
skills_root = sync_root / "02_skills"
wb_skills = get_workbuddy_skills_dir()
wb_skills.mkdir(parents=True, exist_ok=True)
dirs = get_skill_dirs(skills_root)
if not dirs:
warn(f"没有可安装的技能: {skills_root}")
return
installed = 0
updated = 0
for d in dirs:
dest = wb_skills / d.name
if dest.exists():
shutil.rmtree(str(dest))
updated += 1
shutil.copytree(str(d), str(dest), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
installed += 1
ok(f"技能安装完成: 新增 {installed},更新 {updated}")
info(f"目标目录: {wb_skills}")
info("重启 WorkBuddy 后新技能将生效")
def install_all(skills_root: Path):
"""(被 init 调用) 安装所有技能"""
wb_skills = get_workbuddy_skills_dir()
wb_skills.mkdir(parents=True, exist_ok=True)
dirs = get_skill_dirs(skills_root)
for d in dirs:
dest = wb_skills / d.name
if dest.exists():
shutil.rmtree(str(dest))
shutil.copytree(str(d), str(dest), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
ok(f" 技能已安装: {d.name}")
def cmd_check():
"""检查技能一致性"""
banner()
sync_root = get_sync_root()
wb_skills = get_workbuddy_skills_dir()
source_dirs = {d.name: d for d in get_skill_dirs(sync_root / "02_skills")}
wb_dirs = {d.name: d for d in get_skill_dirs(wb_skills)}
# 检查源有但 WB 没有的
missing = set(source_dirs.keys()) - set(wb_dirs.keys())
if missing:
for name in sorted(missing):
warn(f"技能未安装到 WorkBuddy: {name}")
# 检查版本差异
diffs = []
for name in set(source_dirs.keys()) & set(wb_dirs.keys()):
src_card = read_skill_card(source_dirs[name])
wb_card = read_skill_card(wb_dirs[name])
if src_card["version"] != wb_card["version"]:
diffs.append((name, src_card["version"], wb_card["version"]))
if diffs:
for name, sv, wv in diffs:
info(f"版本差异: {name} (源: {sv} vs WB: {wv})")
if not missing and not diffs:
ok("所有技能一致,无差异")
else:
info("运行 agentos skill install 以同步")
def cmd_build(skill_name: str, output_path: str = None):
"""打包技能为 .skill 压缩包"""
banner()
skills_root = get_sync_root() / "02_skills"
skill_dir = skills_root / skill_name
if not skill_dir.exists() or not (skill_dir / "SKILL.md").exists():
err(f"技能不存在: {skill_name} ({skill_dir})")
sys.exit(1)
# 读取版本号
card = read_skill_card(skill_dir)
version = card["version"]
# 确定输出路径
if output_path:
out_dir = Path(output_path)
else:
out_dir = get_sync_root() / "07_migration" / "exports"
out_dir.mkdir(parents=True, exist_ok=True)
archive_name = f"{skill_name}_v{version}.tar.gz"
archive_path = out_dir / archive_name
with tarfile.open(str(archive_path), "w:gz") as tar:
tar.add(str(skill_dir), arcname=skill_name)
file_size = archive_path.stat().st_size / 1024
ok(f"技能包已创建: {archive_path} ({file_size:.1f} KB)")
info(f"安装命令: agentos skill install (放到新机后)")
info(f"手动安装: 解压到 ~/.workbuddy/skills/{skill_name}/")
def cmd_search(keyword: str):
"""搜索技能"""
banner()
skills_root = get_sync_root() / "02_skills"
keyword_lower = keyword.lower()
results = []
for d in get_skill_dirs(skills_root):
card = read_skill_card(d)
desc = card.get("description", "").lower()
name = d.name.lower()
triggers = " ".join(card.get("triggers", [])).lower()
if keyword_lower in name or keyword_lower in desc or keyword_lower in triggers:
results.append((d.name, card["version"], card.get("description", "")))
if results:
print(f"找到 {len(results)} 个匹配技能:")
for name, ver, desc in results:
print(f" {name:<20} v{ver:<6} {desc[:50]}")
else:
info(f"未找到匹配 '{keyword}' 的技能")
def cmd_uninstall(skill_name: str):
"""从 WorkBuddy 卸载技能"""
wb_dir = get_workbuddy_skills_dir() / skill_name
if not wb_dir.exists():
err(f"技能未安装: {skill_name}")
sys.exit(1)
shutil.rmtree(str(wb_dir))
ok(f"技能已卸载: {skill_name}")
info("重启 WorkBuddy 后生效")
# ──────────────────────────────────────────
# 路由
# ──────────────────────────────────────────
def run(args):
cmd = args.skill_cmd
if not cmd:
banner()
print("agentos skill 子命令: list | install | check | build | search | uninstall")
print()
return
if cmd == "list":
cmd_list(verbose=args.verbose)
elif cmd == "install":
cmd_install()
elif cmd == "check":
cmd_check()
elif cmd == "build":
cmd_build(args.skill_name, args.output)
elif cmd == "search":
cmd_search(args.keyword)
elif cmd == "uninstall":
cmd_uninstall(args.skill_name)
@@ -0,0 +1,199 @@
"""
agentos sync — 双机增量同步
检查 agent-sync/ 与本地 WorkBuddy 的差异并增量更新:
1. 技能差异检测(对比版本号和文件变化)
2. MCP 配置差异检测
3. 自动化任务差异检测
4. 增量修复差异项
"""
import sys
import json
import shutil
import hashlib
from pathlib import Path
from .utils import (
get_sync_root, get_local_root, get_python,
info, ok, warn, err, run, banner
)
from . import skill_mgr
def file_hash(path: Path) -> str:
"""计算文件 SHA256"""
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()[:16]
def diff_skills(dry_run: bool) -> list:
"""检测技能差异"""
sync_root = get_sync_root()
wb_skills = Path.home() / ".workbuddy" / "skills"
source_dir = sync_root / "02_skills"
changes = []
source_dirs = {d.name: d for d in skill_mgr.get_skill_dirs(source_dir)}
wb_dirs = {d.name: d for d in skill_mgr.get_skill_dirs(wb_skills)} if wb_skills.exists() else {}
for name, src_dir in source_dirs.items():
if name not in wb_dirs:
changes.append({"type": "missing", "name": name, "action": "install"})
continue
src_card = skill_mgr.read_skill_card(src_dir)
wb_card = skill_mgr.read_skill_card(wb_dirs[name])
if src_card["version"] != wb_card["version"]:
changes.append({
"type": "version_diff",
"name": name,
"action": "update",
"src_version": src_card["version"],
"wb_version": wb_card["version"],
})
for name in wb_dirs:
if name not in source_dirs:
changes.append({"type": "extra", "name": name, "action": "uninstall"})
return changes
def diff_mcp(dry_run: bool) -> list:
"""检测 MCP 配置差异"""
sync_root = get_sync_root()
src = sync_root / "01_core" / "mcp.json"
dst = Path.home() / ".workbuddy" / "mcp.json"
changes = []
if not src.exists():
return changes
if not dst.exists():
changes.append({"type": "missing", "name": "mcp.json", "action": "deploy"})
else:
src_hash = file_hash(src)
dst_hash = file_hash(dst)
if src_hash != dst_hash:
changes.append({"type": "content_diff", "name": "mcp.json", "action": "update"})
return changes
def diff_automations(dry_run: bool) -> list:
"""检测自动化任务差异"""
sync_root = get_sync_root()
automations_file = sync_root / "01_core" / "automations.yaml"
if not automations_file.exists():
return []
return [{"type": "info", "name": "automations.yaml", "action": "需手动在 WorkBuddy 中比对"}]
def apply_changes(changes: list, dry_run: bool, force: bool):
"""执行差异修复"""
if dry_run:
return
for ch in changes:
action = ch["action"]
if action == "install":
info(f" 安装技能: {ch['name']}")
src_dir = get_sync_root() / "02_skills" / ch["name"]
skill_mgr.install_all(get_sync_root() / "02_skills")
elif action == "update":
info(f" 更新技能: {ch['name']} ({ch.get('src_version', '?')})")
skill_mgr.install_all(get_sync_root() / "02_skills")
elif action == "uninstall":
warn(f" agent-sync 中已移除: {ch['name']}(建议手动清理 WorkBuddy)")
elif action == "deploy":
info(f" 部署 MCP 配置")
src = get_sync_root() / "01_core" / "mcp.json"
dst = Path.home() / ".workbuddy" / "mcp.json"
if dst.exists():
dst.rename(dst.with_suffix(".json.syncbak"))
shutil.copy2(str(src), str(dst))
ok(" MCP 配置已更新")
elif action == "update" and ch["name"] == "mcp.json":
info(f" 更新 MCP 配置")
src = get_sync_root() / "01_core" / "mcp.json"
dst = Path.home() / ".workbuddy" / "mcp.json"
dst.rename(dst.with_suffix(".json.syncbak"))
shutil.copy2(str(src), str(dst))
ok(" MCP 配置已更新")
def run(args):
dry_run = args.dry_run
force = args.force
banner()
info("双机增量同步" + (" [DRY-RUN]" if dry_run else ""))
print()
# 1. 检测技能差异
print("📦 技能差异:")
skill_changes = diff_skills(dry_run)
if not skill_changes:
ok(" 无差异")
else:
for ch in skill_changes:
action_icon = {"install": "➕", "update": "🔄", "uninstall": "🗑️"}.get(ch["action"], "?")
if ch["type"] == "missing":
warn(f" {action_icon} 新增技能: {ch['name']}")
elif ch["type"] == "version_diff":
info(f" {action_icon} 版本变更: {ch['name']} ({ch['wb_version']} → {ch['src_version']})")
elif ch["type"] == "extra":
warn(f" {action_icon} 多余的: {ch['name']}(源目录已移除)")
print()
# 2. MCP 差异
print("🔧 MCP 配置差异:")
mcp_changes = diff_mcp(dry_run)
if not mcp_changes:
ok(" 无差异")
else:
for ch in mcp_changes:
if ch["action"] == "deploy":
warn(f" 缺失: {ch['name']}(待部署)")
elif ch["action"] == "update":
info(f" 内容变更: {ch['name']}(待更新)")
print()
# 3. 自动化差异
print("⏰ 自动化任务差异:")
auto_changes = diff_automations(dry_run)
if not auto_changes:
ok(" 无自动化配置文件(无差异检测)")
else:
for ch in auto_changes:
info(f" {ch['name']}: {ch['action']}")
print()
# 汇总
all_changes = skill_changes + mcp_changes + [c for c in auto_changes if c["action"] != "需手动在 WorkBuddy 中比对"]
if not all_changes:
ok("完全一致,无需同步")
return
print(f"共 {len(all_changes)} 项差异")
if dry_run:
info("添加 --force 参数执行同步")
else:
# 执行同步
info("执行同步...")
apply_changes(all_changes, dry_run, force)
ok("同步完成,请重启 WorkBuddy 使更改生效")
@@ -0,0 +1,177 @@
"""
agentos tool — 工具管理
子命令: list, trae
"""
import os
import subprocess
import sys
from pathlib import Path
from .utils import get_sync_root, info, ok, warn, err, banner
TOOL_CATEGORIES = {
"00_setup": "安装部署",
"01_system": "系统诊断与健康检查",
"02_browser": "浏览器自动化",
"03_ocr": "OCR 识别",
"04_media": "媒体处理",
"05_crawl": "网页抓取与爬虫",
"06_mobile": "移动端工具",
"07_matrix": "矩阵养号",
"08_trae_agent": "Trae AI 编程助手",
}
def _get_trae_script() -> Path:
"""查找 trae_agent.sh 脚本路径"""
sync_root = get_sync_root()
candidates = [
sync_root / "05_tools" / "08_trae_agent" / "trae_agent.sh",
sync_root / "05_tools" / "08_trae_agent" / "trae_agent.sh",
]
for p in candidates:
if p.exists():
return p
# Fallback: 直接使用 agent-local 路径
fallback = Path.home() / "workbuddy-agent-os" / "agent-local" / "tools" / "trae-agent"
script = fallback.parent.parent / "agent-sync" / "05_tools" / "08_trae_agent" / "trae_agent.sh"
if script.exists():
return script
return candidates[0]
def cmd_list():
"""列出所有可用工具"""
banner()
tools_root = get_sync_root() / "05_tools"
if not tools_root.exists():
warn(f"工具目录不存在: {tools_root}")
return
total = 0
for category_dir in sorted(tools_root.iterdir()):
if not category_dir.is_dir() or category_dir.name.startswith("."):
continue
cat_name = TOOL_CATEGORIES.get(category_dir.name, category_dir.name)
scripts = sorted(category_dir.rglob("*.py")) + sorted(category_dir.rglob("*.sh"))
# 过滤掉 __pycache__ 和 __init__.py
scripts = [s for s in scripts if "__pycache__" not in s.parts and s.name != "__init__.py"]
if not scripts:
continue
print(f"\n{cat_name} ({category_dir.name}/):")
for s in scripts:
rel = s.relative_to(tools_root)
# 显示简要描述(读取文件第一行注释)
desc = ""
try:
with open(s) as f:
first_line = f.readline().strip()
if first_line.startswith("#"):
desc = first_line.lstrip("# ").strip()
elif first_line.startswith('"""'):
desc = s.read_text().split('"""')[1].strip().split("\n")[0].strip()
except Exception:
pass
desc = desc[:50] if desc else ""
print(f" 📄 {rel}")
if desc:
print(f" {desc}")
total += 1
print(f"\n共 {total} 个工具脚本")
print()
def cmd_trae_install():
"""安装 trae-agent"""
sync_root = get_sync_root()
install_script = sync_root / "05_tools" / "08_trae_agent" / "install_trae_agent.sh"
if not install_script.exists():
err(f"安装脚本不存在: {install_script}")
return
info("正在安装 trae-agent...")
result = subprocess.run(["bash", str(install_script)], cwd=str(install_script.parent))
if result.returncode == 0:
ok("trae-agent 安装完成")
else:
err("安装失败")
def cmd_trae_run(args):
"""执行 trae-agent 任务"""
script = _get_trae_script()
if not script.exists():
err(f"trae_agent.sh 不存在: {script}")
err("请先运行: agentos tool trae install")
return
# 绕过系统代理(macOS 系统代理端口 6478 会拦截本地 LLM 请求)
env = {**subprocess._clean_environ() if hasattr(subprocess, '_clean_environ') else {}, **os.environ}
env["NO_PROXY"] = "localhost,127.0.0.1,::1"
env["no_proxy"] = "localhost,127.0.0.1,::1"
cmd = ["bash", str(script), "run"]
if args.provider:
cmd.extend(["-p", args.provider])
if args.model:
cmd.extend(["-m", args.model])
if args.file:
cmd.extend(["-f", args.file])
if args.task:
cmd.append(" ".join(args.task))
result = subprocess.run(cmd, env=env)
sys.exit(result.returncode)
def cmd_trae_interactive():
"""启动 trae-agent 交互模式"""
script = _get_trae_script()
if not script.exists():
err(f"trae_agent.sh 不存在: {script}")
err("请先运行: agentos tool trae install")
return
subprocess.run(["bash", str(script), "interactive"])
def cmd_trae_config():
"""查看 trae-agent 配置"""
script = _get_trae_script()
if not script.exists():
err("trae-agent 未安装")
return
subprocess.run(["bash", str(script), "show-config"])
def run(args):
cmd = args.tool_cmd
if not cmd:
banner()
print("agentos tool 子命令: list, trae")
print()
return
if cmd == "list":
cmd_list()
elif cmd == "trae":
trae_cmd = getattr(args, "trae_cmd", None)
if not trae_cmd:
banner()
print("agentos tool trae 子命令: run, interactive, config, install")
print()
return
if trae_cmd == "run":
cmd_trae_run(args)
elif trae_cmd == "interactive":
cmd_trae_interactive()
elif trae_cmd == "config":
cmd_trae_config()
elif trae_cmd == "install":
cmd_trae_install()
@@ -0,0 +1,473 @@
"""
agentos upgrade — 统一模块升级引擎 (v1.0.0)
扫描 agent-sync/ 下所有模块,对每个模块执行标准化 4 步升级:
1. 检测模块状态(有 MODULE.md?install.sh?)
2. 运行 install.sh(建目录 + 写 local.yaml)
3. 安装 Python 依赖(pip install -r requirements.txt)
4. 运行 check 脚本(验证配置 + 环境完整性)
用法:
agentos upgrade 全量升级所有模块
agentos upgrade --module matrix 只升级指定模块
最后更新: 2026-05-01
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional
from .const import __agent_sync_root__, __agent_local_root__
# 模块类型
MODULE_TYPES = {
"02_skills": "skill",
"05_tools": "tool",
"03_knowledge": "knowledge",
"04_memory": "memory",
}
# 模块扫描目录(按优先级排序)
SCAN_DIRS = ["05_tools", "02_skills"]
class ModuleInfo:
"""解析后的模块信息"""
def __init__(self, path: Path, rel_path: str):
self.path = path # 模块目录绝对路径
self.rel_path = rel_path # 相对 agent-sync/ 的路径
self.name = path.name
self.type = self._detect_type()
self.has_module_md = (path / "MODULE.md").exists()
self.has_install_sh = (path / "install.sh").exists()
self.has_requirements = (path / "requirements.txt").exists()
self.has_check_sh = (path / "check.sh").exists()
self.has_config_template = (path / "config_template").exists()
self.local_dir = self._calc_local_dir()
def _detect_type(self) -> str:
for prefix, mtype in MODULE_TYPES.items():
if self.rel_path.startswith(prefix):
return mtype
return "unknown"
def _calc_local_dir(self) -> Optional[Path]:
"""计算对应的 agent-local 目录路径"""
if self.type == "tool":
# 05_tools/07_matrix → agent-local/tools/matrix
parts = self.rel_path.split("/")
if len(parts) >= 2:
return Path(__agent_local_root__) / "tools" / "_".join(parts[1:])
return None
def __repr__(self):
return f"<{self.type}:{self.name}>"
# ══════════════════════════════════════════════════════════════
# 模块扫描
# ══════════════════════════════════════════════════════════════
def scan_modules(target: str = None) -> list:
"""扫描 agent-sync/ 下所有可用模块
参数:
target: 指定模块名(可选)
返回:
list[ModuleInfo]: 模块列表
"""
sync_root = Path(__agent_sync_root__)
modules = []
for scan_dir in SCAN_DIRS:
scan_path = sync_root / scan_dir
if not scan_path.exists():
continue
for item in sorted(scan_path.iterdir()):
if not item.is_dir():
continue
# 跳过隐藏目录和 __pycache__
if item.name.startswith(".") or item.name == "__pycache__":
continue
# 如果是 05_tools,跳过 00_setup(agentos 自身)和单文件目录
if scan_dir == "05_tools":
parts = item.name.split("_", 1)
if len(parts) < 2 or not parts[0].isdigit():
continue
rel = f"{scan_dir}/{item.name}"
module = ModuleInfo(item, rel)
if target and target != module.name:
continue
modules.append(module)
return modules
# ══════════════════════════════════════════════════════════════
# 升级步骤
# ══════════════════════════════════════════════════════════════
def _run(cmd: list, cwd: Path = None, timeout: int = 120) -> tuple:
"""运行命令并返回 (ok, stdout_preview)"""
try:
result = subprocess.run(
cmd, cwd=cwd or Path.cwd(),
capture_output=True, text=True, timeout=timeout
)
ok = result.returncode == 0
out = (result.stdout or result.stderr or "").strip()[:200]
return ok, out
except Exception as e:
return False, str(e)
def step_git_pull() -> tuple:
"""Step 0: 拉取最新代码"""
sync_root = Path(__agent_sync_root__)
if not (sync_root / ".git").exists():
return "skipped", "非 Git 仓库,跳过"
ok, out = _run(["git", "pull", "origin", "main"], cwd=sync_root)
return ("ok" if ok else "fail"), out
def step_install(module: ModuleInfo) -> tuple:
"""Step 1: 运行 install.sh(如果有)"""
install_path = module.path / "install.sh"
if not install_path.exists():
return "skipped", "无 install.sh"
# 传入 managed Python 路径(所有机器统一版本,避免差异)
from .utils import get_python
env = os.environ.copy()
env["WORKBUDDY_PYTHON"] = get_python()
try:
result = subprocess.run(
["bash", str(install_path)],
capture_output=True, text=True, timeout=120,
env=env
)
ok = result.returncode == 0
out = (result.stdout or result.stderr or "").strip()[:200]
status = "ok" if ok else "fail"
return status, out
except subprocess.TimeoutExpired:
return "fail", "安装超时(网络慢?可稍后手动安装)"
except Exception as e:
return "fail", str(e)
def step_pip(module: ModuleInfo) -> tuple:
"""Step 2: 安装 Python 依赖"""
req_path = module.path / "requirements.txt"
if not req_path.exists():
return "skipped", "无 requirements.txt"
# 使用统一 managed Python(所有机器保持同一版本,避免差异)
from .utils import get_python
python = get_python()
ok, out = _run([python, "-m", "pip", "install", "-r", str(req_path), "-q"])
status = "ok" if ok else "fail"
return status, out
def step_check(module: ModuleInfo) -> tuple:
"""Step 3: 运行 check 脚本(如果有)"""
check_path = module.path / "check.sh"
if not check_path.exists():
return "skipped", "无 check.sh"
ok, out = _run(["bash", str(check_path)])
status = "ok" if ok else "fail"
return status, out
def step_environ(module: ModuleInfo) -> list:
"""Step 4: 环境完整性检查(不执行,只报告)"""
notes = []
# 检查 local 目录
if module.local_dir:
if not module.local_dir.exists():
notes.append("⚠️ agent-local 目录未创建,需要 install.sh")
else:
subdirs = ["config", "data", "logs", "profiles"]
missing = [d for d in subdirs if not (module.local_dir / d).exists()]
if missing:
notes.append(f"⚠️ 缺少子目录: {', '.join(missing)}")
# 检查 config_template
if module.has_config_template:
target_conf = module.local_dir / "config" if module.local_dir else None
if target_conf:
has_config = any(target_conf.glob("*.yaml")) or any(target_conf.glob("*.yml"))
if not has_config:
notes.append("📋 需配置: cp config_template/*.yaml → agent-local/config/")
return notes
# ══════════════════════════════════════════════════════════════
# 主升级流程
# ══════════════════════════════════════════════════════════════
def do_upgrade(args):
"""执行升级流程"""
target = getattr(args, "module", None)
dry_run = getattr(args, "dry_run", False)
print(f"\n{'='*60}")
print(f" 🔄 AgentOS 统一升级")
print(f" {'(dry-run)' if dry_run else '(执行模式)'}")
print(f"{'='*60}\n")
# Step 0: 拉取代码
print(f"▶ git pull")
status, detail = step_git_pull()
print(f" {_icon(status)} {detail}")
# 扫描模块
modules = scan_modules(target)
if not modules:
print(f"\n ❌ 未找到模块" + (f" (匹配: {target})" if target else ""))
return
print(f"\n 扫描到 {len(modules)} 个模块:")
for m in modules:
print(f" {m.type:6s} {m.name:25s} {m.rel_path}")
print()
# 逐模块升级
results = {}
for module in modules:
# 判断模块层级
has_local_needs = module.has_install_sh or module.has_requirements or module.has_config_template
if not has_local_needs:
# L1 纯代码模块
print(f" 📦 {module.name} ({module.type}) — 纯代码模块,无需本地部署")
module_results = {"install": ("skipped", ""), "pip": ("skipped", ""), "check": ("skipped", "")}
print(f" install: ⏭️ 无本地依赖")
print(f" pip: ⏭️ 无 Python 依赖")
print(f" check: ⏭️ 无检查脚本")
else:
print(f"{'─'*60}")
print(f" 📦 {module.name} ({module.type})")
print(f"{'─'*60}")
module_results = {}
# Step 1: install.sh
s, d = step_install(module)
module_results["install"] = (s, d)
print(f" install: {_icon(s)} {d[:80]}")
# Step 2: pip install
s, d = step_pip(module)
module_results["pip"] = (s, d)
print(f" pip: {_icon(s)} {d[:80]}")
# Step 3: check.sh
s, d = step_check(module)
module_results["check"] = (s, d)
print(f" check: {_icon(s)} {d[:80]}")
# Step 4: 环境检查
notes = step_environ(module)
module_results["environ"] = notes
for n in notes:
print(f" {n}")
results[module.name] = module_results
print()
# 如果有 Skill 模块,自动注册到 WorkBuddy
has_skills = any(m.type == "skill" for m in modules)
if has_skills and not dry_run:
print(f"{'─'*60}")
print(f" 🔗 注册技能到 WorkBuddy...")
from . import skill_mgr
try:
skill_mgr.run(argparse.Namespace(skill_cmd="install"))
print(f" ✅ 技能注册完成")
except Exception as e:
print(f" ⚠️ 技能注册失败: {e}")
print(f" 可稍后手动执行: agentos skill install")
print()
# 自动注册到集群
if not dry_run:
print(f"{'─'*60}")
print(f" 📡 自动注册到集群...")
try:
import subprocess
sync_root = get_sync_root()
script = str(sync_root / "05_tools" / "01_system" / "cluster_registry.py")
result = subprocess.run(
[sys.executable, script, "register"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
print(f" ✅ 集群注册完成")
else:
print(f" ⚠️ 集群注册失败: {result.stderr.strip()}")
except Exception as e:
print(f" ⚠️ 集群注册异常: {e}")
print(f" 可稍后手动执行: agentos register")
print()
# 汇总报告
_print_report(results, modules)
def _icon(status: str) -> str:
return {"ok": "✅", "fail": "❌", "skipped": "⏭️"}.get(status, "❓")
def _print_report(results: dict, modules: list):
"""打印升级报告"""
print(f"{'='*60}")
print(f" 📋 升级报告")
print(f"{'='*60}\n")
# 按层级分组统计
l1_count = sum(1 for m in modules if not m.has_install_sh and not m.has_requirements)
l2_count = sum(1 for m in modules if m.has_install_sh or m.has_requirements)
ok_count = 0
warn_count = 0
for module in modules:
r = results.get(module.name, {})
install_s = r.get("install", ("skipped", ""))[0]
pip_s = r.get("pip", ("skipped", ""))[0]
check_s = r.get("check", ("skipped", ""))[0]
notes = r.get("environ", [])
has_local = module.has_install_sh or module.has_requirements
module_ok = all(s == "ok" or s == "skipped" for s in [install_s, pip_s, check_s])
if module_ok:
ok_count += 1
else:
warn_count += 1
if has_local:
print(f" 📦 {module.name:25s} {_icon(install_s)} install {_icon(pip_s)} pip {_icon(check_s)} check")
else:
print(f" 📦 {module.name:25s} ⏭️ 纯代码模块(无本地依赖)")
for n in notes:
print(f" {'':25s} {n}")
print(f"\n L1 纯代码模块: {l1_count} 个(自动跳过,无需部署)")
print(f" L2 本地数据模块: {l2_count} 个(已执行 install/pip/check)")
print()
print(f"{'-'*60}")
if warn_count == 0:
print(f" ✅ 全部 {ok_count} 个模块检查完成")
else:
print(f" ⚠️ {ok_count} 个正常, {warn_count} 个需关注")
print(f"{'-'*60}")
print()
# 手动操作提示
manual_steps = []
for module in modules:
if module.name == "matrix":
manual_steps.append(f" ☐ Matrix: 配置 {__agent_local_root__}/tools/matrix/config/accounts.yaml")
manual_steps.append(f" ☐ Matrix: 运行 switch_account.py --method profile 重新登录")
if manual_steps:
print(f" 需手动操作:")
for s in manual_steps:
print(s)
print()
def setup_parser(subparsers):
"""向主 CLI 注册 upgrade 子命令"""
p = subparsers.add_parser("upgrade", help="统一升级:拉取代码 + 安装依赖 + 环境检查")
p.add_argument("--module", "-m", type=str, default=None,
help="只升级指定模块(如 matrix)")
p.add_argument("--dry-run", action="store_true",
help="仅预览,不执行实际操作")
p.set_defaults(func=do_upgrade)
def do_rebuild_vector(args):
"""重建本地向量数据库"""
import subprocess as sp
from pathlib import Path
from .utils import get_sync_root, get_local_root, get_python
sync_root = get_sync_root()
local_root = get_local_root()
python = get_python()
track = getattr(args, "track", "both")
dry_run = getattr(args, "dry_run", False)
incremental = getattr(args, "incremental", False)
script = sync_root / "02_skills" / "kb_manager" / "vector_db_rebuild.py"
if not script.exists():
print(f"❌ 向量重建脚本不存在: {script}")
return
cmd = [
str(python),
str(script),
"--root", str(sync_root),
"--local", str(local_root),
"--track", track,
]
if incremental:
cmd.append("--incremental")
if dry_run:
cmd.append("--dry-run")
print(f"🔄 重建向量数据库 ({track})")
result = sp.run(cmd, cwd=str(sync_root))
if result.returncode == 0:
print("✅ 向量数据库重建完成")
else:
print(f"❌ 向量数据库重建失败 (rc={result.returncode})")
def _run_cluster_script(args, subcommand: str):
"""运行集群注册表脚本"""
import subprocess as sp
from pathlib import Path
from .utils import get_sync_root
sync_root = get_sync_root()
script = sync_root / "05_tools" / "01_system" / "cluster_registry.py"
if not script.exists():
print(f"❌ 集群脚本不存在: {script}")
return
result = sp.run(
[sys.executable, str(script), subcommand],
cwd=str(sync_root)
)
return result.returncode
def do_cluster_register(args):
"""注册本机到集群"""
_run_cluster_script(args, "register")
def do_cluster_status(args):
"""查看集群状态"""
_run_cluster_script(args, "status")
def do_cluster_cleanup(args):
"""清理过期注册条目"""
_run_cluster_script(args, "cleanup")
@@ -0,0 +1,108 @@
"""
工具函数:路径解析、shell 执行、颜色输出
"""
import os
import sys
import subprocess
from pathlib import Path
from .const import __agent_sync_root__, __agent_local_root__
def get_sync_root() -> Path:
"""获取 agent-sync 根目录"""
env_root = os.environ.get("AGENT_SYNC_ROOT")
if env_root:
return Path(env_root)
return Path(__agent_sync_root__).expanduser()
def get_local_root() -> Path:
"""获取 agent-local 根目录"""
env_root = os.environ.get("AGENT_LOCAL_ROOT")
if env_root:
return Path(env_root)
return Path(__agent_local_root__).expanduser()
def get_python() -> str:
"""获取 WorkBuddy 管理的 Python 路径"""
candidates = [
os.environ.get("AGENTOS_PYTHON"),
str(Path.home() / ".workbuddy/binaries/python/envs/agent-os/bin/python3"),
str(Path.home() / ".workbuddy/binaries/python/versions/3.13.12/bin/python3"),
"python3",
]
for c in candidates:
if c and Path(c).exists():
return c
return "python3"
def get_node() -> str:
"""获取 WorkBuddy 管理的 node 路径"""
candidates = [
str(Path.home() / ".workbuddy/binaries/node/versions/22.12.0/bin/node"),
"node",
]
for c in candidates:
if c and Path(c).exists():
return c
return "node"
def get_npx() -> str:
"""获取 WorkBuddy 管理的 npx 路径"""
node_dir = Path.home() / ".workbuddy/binaries/node/versions/22.12.0/bin"
npx_path = node_dir / "npx"
if npx_path.exists():
return str(npx_path)
return "npx"
def run(cmd: list, **kwargs) -> subprocess.CompletedProcess:
"""运行命令并返回结果"""
default = {"capture_output": True, "text": True, "timeout": 120}
default.update(kwargs)
return subprocess.run(cmd, **default)
def color(text: str, color_name: str = "blue") -> str:
"""终端颜色输出"""
colors = {
"red": "\033[91m",
"green": "\033[92m",
"yellow": "\033[93m",
"blue": "\033[94m",
"cyan": "\033[96m",
"reset": "\033[0m",
}
return f"{colors.get(color_name, '')}{text}{colors['reset']}"
def info(msg: str):
print(f"{color('[INFO]', 'blue')} {msg}")
def ok(msg: str):
print(f"{color('[OK]', 'green')} {msg}")
def warn(msg: str):
print(f"{color('[WARN]', 'yellow')} {msg}")
def err(msg: str):
print(f"{color('[ERROR]', 'red')} {msg}")
def banner():
"""显示欢迎横幅"""
print()
print("=" * 60)
print(f" AgentOS 系统管理 CLI v1.0.0")
print(f" Synced: {get_sync_root()}")
print(f" Local: {get_local_root()}")
print("=" * 60)
print()