feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。
**目前能用的(真环境实测,非推断)**:
profile/other : 200, 7075 字节 —— 博主主页指标(粉丝/获赞/作品数/昵称)
aweme/detail : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)
**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
挑中的恰好是「批量拉作品列表」这个最敏感的动作。
2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
代价,过几小时要重测)。
所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。
**排查中控住变量后得到的两条事实**(都写进注释了):
· `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
(我先前猜的 sec-ch-ua 不是关键。)
· 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
从 CDP 取齐 cookie + UA + hints,而不是各自写死。
「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。
测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
@@ -0,0 +1,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()
|
||||
Reference in New Issue
Block a user