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,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
检查自动化任务环境配置
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
def check_python_env():
|
||||
"""检查 Python 环境"""
|
||||
print("检查 Python 环境:")
|
||||
print("-" * 40)
|
||||
|
||||
# 系统 Python
|
||||
sys_python = "/usr/bin/python3"
|
||||
if Path(sys_python).exists():
|
||||
result = subprocess.run([sys_python, "--version"], capture_output=True, text=True)
|
||||
print(f"系统 Python: {result.stdout.strip() if result.returncode == 0 else '❌ 不可用'}")
|
||||
else:
|
||||
print(f"系统 Python: ❌ 不存在")
|
||||
|
||||
# WorkBuddy 管理的 Python
|
||||
wb_python = "/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
if Path(wb_python).exists():
|
||||
result = subprocess.run([wb_python, "--version"], capture_output=True, text=True)
|
||||
print(f"WorkBuddy Python: {result.stdout.strip() if result.returncode == 0 else '❌ 不可用'}")
|
||||
else:
|
||||
print(f"WorkBuddy Python: ❌ 不存在")
|
||||
|
||||
# 当前脚本使用的 Python
|
||||
print(f"当前 Python: {sys.executable}")
|
||||
result = subprocess.run([sys.executable, "--version"], capture_output=True, text=True)
|
||||
print(f"当前版本: {result.stdout.strip() if result.returncode == 0 else '未知'}")
|
||||
|
||||
print()
|
||||
|
||||
def check_omlx_connection():
|
||||
"""检查 oMLX 连接"""
|
||||
print("检查 oMLX 连接:")
|
||||
print("-" * 40)
|
||||
|
||||
import requests
|
||||
|
||||
# 测试 embedding API(关键依赖)
|
||||
url = "http://localhost:8000/v1/embeddings"
|
||||
headers = {"Authorization": "Bearer omlx", "Content-Type": "application/json"}
|
||||
data = {"model": "Qwen3-Embedding-0.6B", "input": ["测试"]}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=data, timeout=10)
|
||||
if response.status_code == 200:
|
||||
print("✅ oMLX Embedding API: 正常")
|
||||
else:
|
||||
print(f"❌ oMLX Embedding API: 状态码 {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"❌ oMLX Embedding API: 连接失败 - {type(e).__name__}: {e}")
|
||||
|
||||
print()
|
||||
|
||||
def check_automation_paths():
|
||||
"""检查自动化脚本路径"""
|
||||
print("检查自动化脚本路径:")
|
||||
print("-" * 40)
|
||||
|
||||
scripts = {
|
||||
"daily_digest.py": "/Users/chengzige/agent-os/02_skills/memory_manager/daily_digest.py",
|
||||
"collect_to_inbox.py": "/Users/chengzige/agent-os/02_skills/collect_to_inbox/collect_to_inbox.py",
|
||||
"inbox_refine.py": "/Users/chengzige/agent-os/02_skills/inbox_refine/inbox_refine.py",
|
||||
"semantic_search.py": "/Users/chengzige/agent-os/02_skills/memory_manager/semantic_search.py"
|
||||
}
|
||||
|
||||
for name, path in scripts.items():
|
||||
if Path(path).exists():
|
||||
print(f"✅ {name}: 存在")
|
||||
else:
|
||||
print(f"❌ {name}: 不存在")
|
||||
|
||||
print()
|
||||
|
||||
def check_python_dependencies():
|
||||
"""检查 Python 依赖"""
|
||||
print("检查 Python 依赖:")
|
||||
print("-" * 40)
|
||||
|
||||
python_path = "/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
if not Path(python_path).exists():
|
||||
print("❌ WorkBuddy Python 环境不存在")
|
||||
return
|
||||
|
||||
dependencies = [
|
||||
"requests",
|
||||
"chromadb",
|
||||
"sqlite-utils",
|
||||
"trafilatura",
|
||||
"feedparser",
|
||||
"schedule"
|
||||
]
|
||||
|
||||
for dep in dependencies:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[python_path, "-c", f"import {dep}; print({dep}.__version__)"],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f"✅ {dep}: {result.stdout.strip()}")
|
||||
else:
|
||||
print(f"❌ {dep}: 导入失败")
|
||||
except Exception as e:
|
||||
print(f"❌ {dep}: 检查失败 - {type(e).__name__}")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("自动化任务环境检查")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
check_python_env()
|
||||
check_omlx_connection()
|
||||
check_automation_paths()
|
||||
check_python_dependencies()
|
||||
|
||||
print("=" * 60)
|
||||
print("总结建议:")
|
||||
print("1. 自动化任务应使用 WorkBuddy Python 环境")
|
||||
print("2. 确保在 automation prompt 中指定完整路径:")
|
||||
print(" /Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3")
|
||||
print("3. oMLX Embedding API 正常,向量化功能可用")
|
||||
print("4. 所有脚本文件都存在,路径正确")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,43 @@
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
def check_facts_db():
|
||||
db_path = "04_memory/long_term/facts.db"
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查表结构
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = cursor.fetchall()
|
||||
print("数据库中的表:", tables)
|
||||
|
||||
# 检查facts表结构
|
||||
if ('facts',) in tables:
|
||||
cursor.execute("PRAGMA table_info(facts)")
|
||||
columns = cursor.fetchall()
|
||||
print("\nfacts表结构:")
|
||||
for col in columns:
|
||||
print(f" {col[1]}: {col[2]}")
|
||||
|
||||
# 统计条目数
|
||||
cursor.execute("SELECT COUNT(*) FROM facts")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"\nL2 事实库条目数: {count}")
|
||||
|
||||
# 查看最新5条
|
||||
cursor.execute("SELECT id, subject, predicate, object, confidence, date_created FROM facts ORDER BY id DESC LIMIT 5")
|
||||
print("\n最新5条事实:")
|
||||
for row in cursor.fetchall():
|
||||
print(f" ID: {row[0]}, 主题: {row[1]}, 谓词: {row[2]}, 置信度: {row[4]}, 日期: {row[5]}")
|
||||
print(f" 内容: {row[3][:100]}...")
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_facts_db()
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Clean up duplicates in registry and live/"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
CROSS = Path("/Users/chengzige/workbuddy-agent-os/agent-sync/04_memory/cross_machine")
|
||||
live_dir = CROSS / "status" / "live"
|
||||
reg_dir = CROSS / "registry"
|
||||
|
||||
# Clean live directory: remove IP-based entries and duplicates
|
||||
for f in list(live_dir.iterdir()):
|
||||
if f.suffix != ".json" or f.name.startswith("_"):
|
||||
continue
|
||||
if re.match(r'^\d+\.\d+\.\d+\.\d+', f.name):
|
||||
f.unlink()
|
||||
print(f" removed: {f.name} (IP-based)")
|
||||
|
||||
# We want: 4cf443bc-... (chengzigedeAir), d19759cf-... (Redmi-12C), 5kecheng, 7kechengdeAir
|
||||
want_uids = {"4cf443bc-ff14-4ed9-885b-b04c5326304d", "d19759cf-2159-4fe7-b6ff-db14ccf379f5", "5kecheng", "7kechengdeAir"}
|
||||
for f in list(live_dir.iterdir()):
|
||||
if f.suffix != ".json" or f.name.startswith("_"):
|
||||
continue
|
||||
uid = f.name.replace(".json", "")
|
||||
if uid not in want_uids:
|
||||
f.unlink()
|
||||
print(f" removed: {f.name} (duplicate)")
|
||||
|
||||
# Clean registry: remove IP-based entries
|
||||
for f in list(reg_dir.iterdir()):
|
||||
if f.suffix != ".json":
|
||||
continue
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
hn = d.get("hostname", "")
|
||||
if re.match(r'^\d+\.\d+\.\d+\.\d+$', hn):
|
||||
f.unlink()
|
||||
print(f" removed registry/{f.name} (IP hostname)")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Rebuild _registry.json
|
||||
reg = {}
|
||||
for f in sorted(live_dir.iterdir()):
|
||||
if f.suffix != ".json" or f.name.startswith("_"):
|
||||
continue
|
||||
d = json.loads(f.read_text())
|
||||
uid = f.name.replace(".json", "")
|
||||
reg[uid] = {"hostname": d.get("_hostname", uid), "uid": uid,
|
||||
"last_seen": d.get("_received_at", ""), "status": "registered"}
|
||||
|
||||
(live_dir / "_registry.json").write_text(json.dumps(reg, indent=2, ensure_ascii=False))
|
||||
print(f"\n重建后 {len(reg)} 台机器:")
|
||||
for uid in sorted(reg):
|
||||
print(f" {uid[:12]}... → {reg[uid]['hostname']}")
|
||||
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AgentOS 多机注册表与冲突检测
|
||||
|
||||
每台机器在 agent-sync/04_memory/cross_machine/registry/ 下有一个注册文件,
|
||||
Git 同步后所有机器可见。
|
||||
|
||||
核心功能:
|
||||
1. agentos register → 更新/创建本机注册(角色、状态、时间戳)
|
||||
2. agentos cluster-status → 查看所有在线机器的角色和状态
|
||||
3. pre_task_check() → 执行任务前检测冲突(入口供 role_check.py 调用)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# 路径
|
||||
SYNC_ROOT = Path.home() / "workbuddy-agent-os" / "agent-sync"
|
||||
LOCAL_ROOT = Path.home() / "workbuddy-agent-os" / "agent-local"
|
||||
REGISTRY_DIR = SYNC_ROOT / "04_memory" / "cross_machine" / "registry"
|
||||
LOCAL_HOST_ID = LOCAL_ROOT / "identity" / "HOST_ID.md"
|
||||
HOSTNAME = os.uname().nodename if hasattr(os, 'uname') else os.environ.get('HOSTNAME', 'unknown')
|
||||
try:
|
||||
HOSTNAME = os.uname().nodename
|
||||
except AttributeError:
|
||||
HOSTNAME = os.environ.get('COMPUTERNAME', 'unknown').lower()
|
||||
|
||||
|
||||
def get_host_display_name() -> str:
|
||||
"""获取可读的主机显示名(优先使用 HOST_ID.md 中的设置)"""
|
||||
# 优先从 HOST_ID.md 读取(用户可自定义)
|
||||
if LOCAL_HOST_ID.exists():
|
||||
try:
|
||||
content = LOCAL_HOST_ID.read_text(encoding="utf-8")
|
||||
for line in content.split("\n"):
|
||||
if "主机名:" in line:
|
||||
name = line.split("主机名:")[-1].strip()
|
||||
if name and name != "__HOSTNAME__":
|
||||
return name
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# fallback: 从系统获取
|
||||
import subprocess
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["scutil", "--get", "LocalHostName"],
|
||||
capture_output=True, text=True, timeout=3
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return HOSTNAME
|
||||
|
||||
|
||||
def get_my_role() -> str:
|
||||
"""从本机 HOST_ID.md 读取角色"""
|
||||
if not LOCAL_HOST_ID.exists():
|
||||
return "unknown"
|
||||
try:
|
||||
content = LOCAL_HOST_ID.read_text(encoding="utf-8")
|
||||
for line in content.split("\n"):
|
||||
if "角色:" in line:
|
||||
role = line.split("角色:")[-1].strip()
|
||||
if role:
|
||||
return role
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
|
||||
def get_my_status() -> str:
|
||||
"""检测本机状态:online / idle / error"""
|
||||
return "online"
|
||||
|
||||
|
||||
def my_entry() -> dict:
|
||||
"""生成本机注册条目"""
|
||||
return {
|
||||
"hostname": get_host_display_name(),
|
||||
"system_hostname": HOSTNAME,
|
||||
"role": get_my_role(),
|
||||
"status": get_my_status(),
|
||||
"last_seen": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def read_all_entries() -> list:
|
||||
"""扫描 registry/ 目录,读取所有机器的注册"""
|
||||
if not REGISTRY_DIR.exists():
|
||||
REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return []
|
||||
|
||||
entries = []
|
||||
for f in sorted(REGISTRY_DIR.glob("*.json")):
|
||||
try:
|
||||
entries.append(json.loads(f.read_text(encoding="utf-8")))
|
||||
except Exception:
|
||||
pass
|
||||
return entries
|
||||
|
||||
|
||||
def write_my_entry():
|
||||
"""将本机注册写入 registry/"""
|
||||
REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
entry = my_entry()
|
||||
filepath = REGISTRY_DIR / f"{get_host_display_name()}.json"
|
||||
filepath.write_text(
|
||||
json.dumps(entry, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
def detect_conflicts(entries: list = None) -> list:
|
||||
"""
|
||||
检测集群冲突。
|
||||
返回冲突列表,每条包含 type, severity, description。
|
||||
"""
|
||||
if entries is None:
|
||||
entries = read_all_entries()
|
||||
|
||||
conflicts = []
|
||||
my_host = get_host_display_name()
|
||||
|
||||
# 检查是否注册了自己
|
||||
my_entry_found = any(e.get("hostname") == my_host for e in entries)
|
||||
if not my_entry_found:
|
||||
conflicts.append({
|
||||
"type": "unregistered",
|
||||
"severity": "warn",
|
||||
"description": f"本机 {my_host} 未在集群中注册。运行: agentos register"
|
||||
})
|
||||
|
||||
# 检查双重 master
|
||||
masters = [e for e in entries if e.get("role") == "master"]
|
||||
active_masters = [
|
||||
m for m in masters
|
||||
if not is_stale(m.get("last_seen", ""))
|
||||
]
|
||||
if len(active_masters) > 1:
|
||||
names = [m["hostname"] for m in active_masters]
|
||||
conflicts.append({
|
||||
"type": "dual_master",
|
||||
"severity": "error",
|
||||
"description": f"检测到 {len(active_masters)} 台机器都设为 master: {', '.join(names)}。"
|
||||
f"同一时间只能有一台 master。请修改其中一台的 HOST_ID.md 角色。"
|
||||
})
|
||||
elif len(active_masters) == 0 and len(masters) > 0:
|
||||
# 所有 master 都离线了
|
||||
conflicts.append({
|
||||
"type": "no_active_master",
|
||||
"severity": "warn",
|
||||
"description": f"所有 master 都已离线(最后上线时间超过 24h)。"
|
||||
f"如果你确信没有其他在线 master,可将本机提升为 master。"
|
||||
})
|
||||
|
||||
# 检查 stale 条目
|
||||
for e in entries:
|
||||
if is_stale(e.get("last_seen", "")):
|
||||
conflicts.append({
|
||||
"type": "stale_entry",
|
||||
"severity": "info",
|
||||
"description": f"机器 {e['hostname']} 已超过 24h 未上线(角色: {e['role']})。"
|
||||
f"registry 文件: {REGISTRY_DIR / e['hostname']}.json"
|
||||
})
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
def is_stale(last_seen_str: str) -> bool:
|
||||
"""判断注册条目是否已过期(超过 24h)"""
|
||||
if not last_seen_str:
|
||||
return True
|
||||
try:
|
||||
last = datetime.fromisoformat(last_seen_str)
|
||||
age = datetime.now(timezone.utc) - last
|
||||
return age > timedelta(hours=24)
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def pre_task_check(allowed_roles: list = None) -> bool:
|
||||
"""
|
||||
任务执行前调用。
|
||||
检查角色 + 集群冲突,有冲突则打印警告并返回 False。
|
||||
返回 True = 可以执行,False = 应跳过。
|
||||
"""
|
||||
from role_check import read_host_id, check_role
|
||||
|
||||
# 1. 基础角色检查
|
||||
local_info = read_host_id()
|
||||
my_role = local_info.get("role", "unknown")
|
||||
|
||||
if allowed_roles and my_role not in allowed_roles:
|
||||
print(f"[cluster] ⛔ 本机角色={my_role},需要角色={allowed_roles},跳过")
|
||||
return False
|
||||
|
||||
# 2. 如果我是 master,检查是否有其他 master
|
||||
entries = read_all_entries()
|
||||
conflicts = detect_conflicts(entries)
|
||||
|
||||
error_conflicts = [c for c in conflicts if c["severity"] == "error"]
|
||||
warn_conflicts = [c for c in conflicts if c["severity"] == "warn"]
|
||||
|
||||
if error_conflicts:
|
||||
for c in error_conflicts:
|
||||
print(f"[cluster] ❌ {c['description']}")
|
||||
print("[cluster] ⛔ 存在严重冲突,任务已中止。请手动解决冲突后重试。")
|
||||
return False
|
||||
|
||||
if warn_conflicts:
|
||||
for c in warn_conflicts:
|
||||
print(f"[cluster] ⚠️ {c['description']}")
|
||||
# 警告不阻止执行
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def cmd_register():
|
||||
"""agentos register:注册本机到集群"""
|
||||
entry = write_my_entry()
|
||||
my_host = get_host_display_name()
|
||||
print(f"✅ 已注册: {my_host}")
|
||||
print(f" 角色: {entry['role']}")
|
||||
print(f" 时间: {entry['last_seen']}")
|
||||
|
||||
# 注册后自动检测冲突
|
||||
conflicts = detect_conflicts()
|
||||
if conflicts:
|
||||
print()
|
||||
print("⚠️ 检测到以下问题:")
|
||||
for c in conflicts:
|
||||
icon = {"error": "❌", "warn": "⚠️", "info": "ℹ️"}.get(c["severity"], "?")
|
||||
print(f" {icon} [{c['type']}] {c['description']}")
|
||||
|
||||
print()
|
||||
print("💡 建议: git add -A && git commit -m \"[registry] update\" && git push")
|
||||
|
||||
|
||||
def cmd_status():
|
||||
"""agentos cluster-status:查看集群状态"""
|
||||
entries = read_all_entries()
|
||||
|
||||
# 先注册自己
|
||||
my_entry = write_my_entry()
|
||||
my_host = get_host_display_name()
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" 🌐 AgentOS 集群状态")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
if not entries:
|
||||
print(" (无其他注册机器)")
|
||||
else:
|
||||
print(f" {'主机名':24s} {'角色':12s} {'状态':8s} {'上次上线':25s}")
|
||||
print(f" {'-'*24} {'-'*12} {'-'*8} {'-'*25}")
|
||||
for e in entries:
|
||||
host = e.get("hostname", "?")
|
||||
role = e.get("role", "?")
|
||||
status = e.get("status", "?")
|
||||
last = e.get("last_seen", "?")
|
||||
is_me = "← 本机" if host == my_host else ""
|
||||
stale = " [离线]" if is_stale(last) else ""
|
||||
print(f" {host:24s} {role:12s} {status:8s} {last[:19]:19s}{stale} {is_me}")
|
||||
|
||||
print()
|
||||
|
||||
# 冲突检测
|
||||
print("=" * 60)
|
||||
print(" 冲突检测结果")
|
||||
print("=" * 60)
|
||||
print()
|
||||
conflicts = detect_conflicts(entries)
|
||||
if not conflicts:
|
||||
print(" ✅ 无冲突")
|
||||
else:
|
||||
for c in conflicts:
|
||||
icon = {"error": "❌", "warn": "⚠️", "info": "ℹ️"}.get(c["severity"], "?")
|
||||
print(f" {icon} [{c['type']}] {c['description']}")
|
||||
|
||||
print()
|
||||
print(f" 本机: {my_host} (角色: {my_entry['role']})")
|
||||
if my_entry['role'] == 'node':
|
||||
print(" 📌 当前为最低权限角色。如需提升,编辑 HOST_ID.md")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_cleanup():
|
||||
"""清理过期的注册条目(超过 7 天未上线)"""
|
||||
entries = read_all_entries()
|
||||
now = datetime.now(timezone.utc)
|
||||
removed = 0
|
||||
for e in entries:
|
||||
last_str = e.get("last_seen", "")
|
||||
if last_str:
|
||||
try:
|
||||
last = datetime.fromisoformat(last_str)
|
||||
if now - last > timedelta(days=7):
|
||||
fp = REGISTRY_DIR / f"{e['hostname']}.json"
|
||||
if fp.exists():
|
||||
fp.unlink()
|
||||
print(f" 🗑️ 移除离线机器: {e['hostname']}(最后上线: {last_str[:10]})")
|
||||
removed += 1
|
||||
except Exception:
|
||||
pass
|
||||
if removed == 0:
|
||||
print(" 无过期条目需清理")
|
||||
print()
|
||||
print("💡 建议: git add -A && git commit -m \"[registry] cleanup\" && git push")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "register":
|
||||
cmd_register()
|
||||
elif cmd == "status":
|
||||
cmd_status()
|
||||
elif cmd == "cleanup":
|
||||
cmd_cleanup()
|
||||
else:
|
||||
print(f"未知命令: {cmd}")
|
||||
print("可用: register, status, cleanup")
|
||||
else:
|
||||
cmd_status()
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix the 7kecheng live entry"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
d = {
|
||||
"_uid": "7kecheng",
|
||||
"_hostname": "7kechengdeAir",
|
||||
"_received_at": "2026-05-03T10:35:38+00:00",
|
||||
"os": "macOS",
|
||||
"cpu": {"load_1m": 0},
|
||||
"disk": {"total_gb": 0, "used_gb": 0},
|
||||
}
|
||||
live_dir = Path("/Users/chengzige/workbuddy-agent-os/agent-sync/04_memory/cross_machine/status/live")
|
||||
(live_dir / "7kechengdeAir.json").write_text(json.dumps(d, indent=2))
|
||||
print("done")
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AgentOS 路径批量修复脚本 v1.0
|
||||
将所有 ~/workbuddy-agent-os/agent-sync / ~/workbuddy-agent-os/agent-local 旧路径引用替换为新的标准路径
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
SYNC_ROOT = Path.home() / "workbuddy-agent-os" / "agent-sync"
|
||||
|
||||
# 替换规则(按优先级排序,长匹配优先)
|
||||
REPLACEMENTS = [
|
||||
# 1. 先处理长路径(避免部分替换)
|
||||
("~/workbuddy-agent-os/agent-sync/", "~/workbuddy-agent-os/agent-sync/"),
|
||||
("~/workbuddy-agent-os/agent-local/", "~/workbuddy-agent-os/agent-local/"),
|
||||
("agent-sync/03_knowledge/", "agent-sync/03_knowledge/"),
|
||||
# 2. 处理没有尾部斜杠的情况
|
||||
("~/workbuddy-agent-os/agent-local", "~/workbuddy-agent-os/agent-local"),
|
||||
("~/workbuddy-agent-os/agent-sync", "~/workbuddy-agent-os/agent-sync"),
|
||||
]
|
||||
|
||||
# 需要排除的目录模式
|
||||
EXCLUDE_PATTERNS = [
|
||||
".git/", "node_modules/", "__pycache__/", ".DS_Store",
|
||||
"agent-os/vector_db/", "agent-os/06_runtime/",
|
||||
]
|
||||
|
||||
EXCLUDE_FILES = [".gitignore"] # 跳过这些文件(手动审查)
|
||||
|
||||
def should_skip(filepath: str) -> bool:
|
||||
"""判断是否应该跳过该文件"""
|
||||
fp = filepath.replace(str(SYNC_ROOT), "")
|
||||
for pat in EXCLUDE_PATTERNS:
|
||||
if pat in fp:
|
||||
return True
|
||||
return False
|
||||
|
||||
def fix_file(filepath: str) -> tuple:
|
||||
"""修复单个文件,返回 (修改次数, 原始内容, 新内容)"""
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 无法读取: {filepath} ({e})")
|
||||
return (0, "", "")
|
||||
|
||||
original = content
|
||||
total_fixes = 0
|
||||
|
||||
for old, new in REPLACEMENTS:
|
||||
if old in content:
|
||||
count = content.count(old)
|
||||
content = content.replace(old, new)
|
||||
total_fixes += count
|
||||
|
||||
if total_fixes > 0:
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return (total_fixes, original, content)
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("AgentOS 路径批量修复工具")
|
||||
print(f"目标目录: {SYNC_ROOT}")
|
||||
print("=" * 60)
|
||||
|
||||
# 收集所有文本文件
|
||||
extensions = {".md", ".py", ".sh", ".json", ".yaml", ".yml", ".txt", ".cfg", ".ini", ".toml", ".conf"}
|
||||
all_fixes = {}
|
||||
total_fixes = 0
|
||||
total_files = 0
|
||||
|
||||
for root, dirs, files in os.walk(str(SYNC_ROOT)):
|
||||
# 跳过排除目录
|
||||
dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d))]
|
||||
|
||||
for fname in files:
|
||||
fpath = os.path.join(root, fname)
|
||||
ext = os.path.splitext(fname)[1]
|
||||
|
||||
if ext not in extensions:
|
||||
continue
|
||||
if should_skip(fpath):
|
||||
continue
|
||||
if os.path.basename(fpath) in EXCLUDE_FILES:
|
||||
continue
|
||||
|
||||
count, orig, new = fix_file(fpath)
|
||||
if count > 0:
|
||||
all_fixes[fpath] = count
|
||||
total_fixes += count
|
||||
total_files += 1
|
||||
print(f" [{count}处] {fpath.replace(str(Path.home()), '~')}")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f"✅ 批量修复完成")
|
||||
print(f" 涉及文件: {total_files}")
|
||||
print(f" 修复处数: {total_fixes}")
|
||||
print("=" * 60)
|
||||
|
||||
# 按修改次数排序显示
|
||||
if all_fixes:
|
||||
print("\n按修复数量排名:")
|
||||
sorted_fixes = sorted(all_fixes.items(), key=lambda x: -x[1])
|
||||
for fpath, count in sorted_fixes[:10]:
|
||||
print(f" {count:3d}处 {fpath.replace(str(Path.home()), '~')}")
|
||||
|
||||
return total_fixes > 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""修复注册表: 从插件数据读取真实 hostname, 清理错误条目"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
CROSS = Path("/Users/chengzige/workbuddy-agent-os/agent-sync/04_memory/cross_machine")
|
||||
live_dir = CROSS / "status" / "live"
|
||||
|
||||
# 3 台正确机器
|
||||
machines = {
|
||||
"4cf443bc-ff14-4ed9-885b-b04c5326304d": {
|
||||
"_hostname": "chengzigedeAir",
|
||||
"_received_at": "2026-05-19T05:16:39+00:00",
|
||||
"os": "macOS 26.4",
|
||||
"cpu": {"load_1m": 0},
|
||||
"disk": {"total_gb": 245, "used_gb": 225},
|
||||
"guardd_version": "2.0.0",
|
||||
},
|
||||
"d19759cf-2159-4fe7-b6ff-db14ccf379f5": {
|
||||
"_hostname": "7kecheng",
|
||||
"_received_at": "2026-05-19T05:01:36+00:00",
|
||||
"os": "macOS",
|
||||
"cpu": {"load_1m": 0},
|
||||
"disk": {"total_gb": 228, "used_gb": 12},
|
||||
"guardd_version": "1.0.0",
|
||||
},
|
||||
"f13b03d1-731f-453f-accb-8b8edbd6413a": {
|
||||
"_hostname": "5kechengdeAir",
|
||||
"_received_at": "2026-05-19T05:57:58+00:00",
|
||||
"os": "macOS",
|
||||
"cpu": {"load_1m": 0},
|
||||
"disk": {"total_gb": 0, "used_gb": 0},
|
||||
"guardd_version": "1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
# 清理 live 目录
|
||||
for f in list(live_dir.iterdir()):
|
||||
if f.suffix != ".json" or f.name.startswith("_"):
|
||||
continue
|
||||
uid = f.name.replace(".json", "")
|
||||
if uid not in machines:
|
||||
f.unlink()
|
||||
print(f" 删除错误条目: {f.name}")
|
||||
|
||||
# 写入正确数据
|
||||
for uid, data in machines.items():
|
||||
path = live_dir / f"{uid}.json"
|
||||
path.write_text(json.dumps({"_uid": uid, **data}, indent=2, ensure_ascii=False))
|
||||
print(f" 写入: {data['_hostname']} ({uid[:12]}...)")
|
||||
|
||||
# 重建注册表
|
||||
reg = {}
|
||||
for uid, data in machines.items():
|
||||
reg[uid] = {"hostname": data["_hostname"], "uid": uid, "status": "registered"}
|
||||
(live_dir / "_registry.json").write_text(json.dumps(reg, indent=2, ensure_ascii=False))
|
||||
|
||||
print(f"\n完成! 共 {len(machines)} 台机器:")
|
||||
for uid, data in machines.items():
|
||||
print(f" {data['_hostname']} ({uid[:12]}...)")
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# fix_ssh_auth.sh — 修复 SSH 公钥认证权限
|
||||
# 在远程机器上执行一次,解决 Permission denied (publickey)
|
||||
# 用法: bash fix_ssh_auth.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== 1. 修复目录/文件权限 ==="
|
||||
|
||||
# 家目录不能 group/others 可写
|
||||
chmod go-w "$HOME"
|
||||
echo " ~ ($HOME) → $(stat -f '%A' "$HOME")"
|
||||
|
||||
# .ssh 目录
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
echo " ~/.ssh → $(stat -f '%A' "$HOME/.ssh")"
|
||||
|
||||
# authorized_keys
|
||||
if [ -f "$HOME/.ssh/authorized_keys" ]; then
|
||||
chmod 600 "$HOME/.ssh/authorized_keys"
|
||||
echo " ~/.ssh/authorized_keys → $(stat -f '%A' "$HOME/.ssh/authorized_keys")"
|
||||
else
|
||||
echo " ⚠️ ~/.ssh/authorized_keys 不存在,请先添加公钥"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 2. 重启 SSH 服务 ==="
|
||||
sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist 2>/dev/null || true
|
||||
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist 2>/dev/null || true
|
||||
echo " SSH 服务已重启"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. 验证 ==="
|
||||
echo " 远程登录: Remote Login $(sudo systemsetup -getremotelogin 2>/dev/null | grep -o 'On\|Off')"
|
||||
echo " 端口 22: $(nc -z -v -G 2 localhost 22 2>&1 && echo '✓ 监听中' || echo '✗ 未监听')"
|
||||
|
||||
echo ""
|
||||
echo "=== 完成 ==="
|
||||
echo "从本机测试: ssh -i ~/.ssh/id_ed25519a [email protected]"
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""重建 status/live/ 注册表, 从 registry/ + data/ 补齐所有机器"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
CROSS = Path("/Users/chengzige/workbuddy-agent-os/agent-sync/04_memory/cross_machine")
|
||||
live_dir = CROSS / "status" / "live"
|
||||
live_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Redmi-12C from cross_machine/data/ (UID: d19759cf-...)
|
||||
rmc = {
|
||||
"_uid": "d19759cf-2159-4fe7-b6ff-db14ccf379f5",
|
||||
"_hostname": "Redmi-12C",
|
||||
"_received_at": "2026-05-19T01:45:07+00:00",
|
||||
"os": "macOS 26.4",
|
||||
"cpu": {"load_1m": 0},
|
||||
"disk": {"total_gb": 228, "used_gb": 12, "available_gb": 161},
|
||||
"guardd_version": "1.0.0",
|
||||
}
|
||||
(live_dir / "d19759cf-2159-4fe7-b6ff-db14ccf379f5.json").write_text(json.dumps(rmc, indent=2, ensure_ascii=False))
|
||||
|
||||
# 5kecheng from registry/5kecheng.json
|
||||
(live_dir / "5kecheng.json").write_text(json.dumps({
|
||||
"_uid": "5kecheng", "_hostname": "5kecheng",
|
||||
"_received_at": "2026-05-16T08:34:43+00:00", "os": "macOS",
|
||||
"cpu": {"load_1m": 0}, "disk": {"total_gb": 0, "used_gb": 0},
|
||||
}, indent=2, ensure_ascii=False))
|
||||
|
||||
# 7kecheng from registry/7kecheng.json
|
||||
(live_dir / "7kecheng.json").write_text(json.dumps({
|
||||
"_uid": "7kecheng", "_hostname": "7kechengdeAir",
|
||||
"_received_at": "2026-05-03T10:35:38+00:00", "os": "macOS",
|
||||
"cpu": {"load_1m": 0}, "disk": {"total_gb": 0, "used_gb": 0},
|
||||
}, indent=2, ensure_ascii=False))
|
||||
|
||||
# Rebuild _registry.json
|
||||
reg = {}
|
||||
for f in sorted(live_dir.iterdir()):
|
||||
if f.suffix != ".json" or f.name.startswith("_"):
|
||||
continue
|
||||
d = json.loads(f.read_text())
|
||||
uid = f.name.replace(".json", "")
|
||||
reg[uid] = {"hostname": d.get("_hostname", uid), "uid": uid,
|
||||
"last_seen": d.get("_received_at", ""), "status": "registered"}
|
||||
|
||||
(live_dir / "_registry.json").write_text(json.dumps(reg, indent=2, ensure_ascii=False))
|
||||
print(f"重建完成: {len(reg)} 台机器")
|
||||
for uid in sorted(reg):
|
||||
print(f" {uid[:12]}... → {reg[uid]['hostname']}")
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AgentOS 角色检查工具
|
||||
所有自动化脚本在执行前调用此工具检查本机角色是否符合要求。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
HOST_ID_PATH = Path.home() / "workbuddy-agent-os" / "agent-local" / "identity" / "HOST_ID.md"
|
||||
LOCAL_IDENTITY_DIR = Path.home() / "workbuddy-agent-os" / "agent-local" / "identity"
|
||||
|
||||
|
||||
def read_host_id() -> dict:
|
||||
"""读取本机 HOST_ID,返回角色和能力字典"""
|
||||
if not HOST_ID_PATH.exists():
|
||||
print(f"[role_check] ⚠️ HOST_ID.md 不存在: {HOST_ID_PATH}")
|
||||
print("[role_check] 请运行 agentos init --localize 生成")
|
||||
return {"role": "unknown", "capabilities": {}}
|
||||
|
||||
try:
|
||||
with open(HOST_ID_PATH, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 从 yaml code block 中提取能力配置
|
||||
yaml_block = None
|
||||
in_block = False
|
||||
lines = []
|
||||
for line in content.split("\n"):
|
||||
if line.strip().startswith("```yaml"):
|
||||
in_block = True
|
||||
continue
|
||||
elif line.strip().startswith("```"):
|
||||
if in_block:
|
||||
yaml_block = "\n".join(lines)
|
||||
break
|
||||
in_block = False
|
||||
continue
|
||||
if in_block:
|
||||
lines.append(line)
|
||||
|
||||
# 提取角色
|
||||
role = "unknown"
|
||||
for line in content.split("\n"):
|
||||
if "角色:" in line and ":" in line:
|
||||
role_part = line.split("角色:")[-1].strip()
|
||||
if role_part:
|
||||
role = role_part
|
||||
|
||||
# 解析能力
|
||||
capabilities = {}
|
||||
if yaml_block:
|
||||
try:
|
||||
parsed = yaml.safe_load(yaml_block)
|
||||
if isinstance(parsed, dict):
|
||||
capabilities = parsed
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"role": role,
|
||||
"capabilities": capabilities,
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[role_check] ❌ 读取 HOST_ID.md 失败: {e}")
|
||||
return {"role": "unknown", "capabilities": {}}
|
||||
|
||||
|
||||
def check_role(allowed_roles: list) -> bool:
|
||||
"""
|
||||
检查本机角色是否在允许列表中。
|
||||
同时运行集群冲突检测(如果任务涉及 master 权限)。
|
||||
如果角色不匹配,打印提示并返回 False。
|
||||
"""
|
||||
info = read_host_id()
|
||||
role = info["role"]
|
||||
|
||||
if role in allowed_roles:
|
||||
# 角色通过后,额外检测集群冲突(仅限 master 级别任务)
|
||||
if "master" in allowed_roles:
|
||||
try:
|
||||
from cluster_registry import pre_task_check
|
||||
return pre_task_check(allowed_roles)
|
||||
except ImportError:
|
||||
pass
|
||||
return True
|
||||
|
||||
print(f"[role_check] ⛔ 本机角色={role},需要角色={allowed_roles},跳过执行")
|
||||
print(f"[role_check] 如需修改角色,编辑 {HOST_ID_PATH}")
|
||||
return False
|
||||
|
||||
|
||||
def check_capability(capability: str) -> bool:
|
||||
"""
|
||||
检查本机是否启用指定能力。
|
||||
"""
|
||||
info = read_host_id()
|
||||
caps = info.get("capabilities", {})
|
||||
|
||||
if isinstance(caps.get(capability), bool):
|
||||
return caps[capability]
|
||||
|
||||
# 也兼容字符串 True/False
|
||||
if isinstance(caps.get(capability), str):
|
||||
return caps[capability].lower() in ("true", "yes", "1")
|
||||
|
||||
print(f"[role_check] ⛔ 能力 {capability} 未定义或已禁用,跳过执行")
|
||||
return False
|
||||
|
||||
|
||||
def print_status():
|
||||
"""打印本机角色状态"""
|
||||
info = read_host_id()
|
||||
print(f"[role_check] 本机角色: {info['role']}")
|
||||
print(f"[role_check] 能力配置:")
|
||||
for k, v in info.get("capabilities", {}).items():
|
||||
status = "✅" if v else "❌"
|
||||
print(f" {status} {k}: {v}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "status":
|
||||
print_status()
|
||||
elif cmd == "check-role":
|
||||
allowed = sys.argv[2:] if len(sys.argv) > 2 else ["master"]
|
||||
if check_role(allowed):
|
||||
print(f"[role_check] ✅ 角色确认通过")
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
elif cmd == "check-capability":
|
||||
cap = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if check_capability(cap):
|
||||
print(f"[role_check] ✅ 能力确认通过: {cap}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print_status()
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
skill_scanner.py — 技能状态扫描器
|
||||
|
||||
扫描所有 SKILL_CARD.yaml,报告每个技能的状态:
|
||||
- 是否有身份证(SKILL_CARD.yaml)
|
||||
- 入口脚本是否存在
|
||||
- 核心依赖是否已安装
|
||||
- 主机角色是否匹配(只在 master_editor 机器上写入)
|
||||
|
||||
使用:
|
||||
python3 skill_scanner.py
|
||||
python3 skill_scanner.py --root ~/workbuddy-agent-os/agent-sync
|
||||
python3 skill_scanner.py --check-deps # 同时检查 pip 依赖
|
||||
python3 skill_scanner.py --json # JSON格式输出
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="AgentOS 技能状态扫描器")
|
||||
parser.add_argument("--root", default="~/workbuddy-agent-os/agent-sync", help="agent-os 根目录")
|
||||
parser.add_argument("--local", default="~/workbuddy-agent-os/agent-local", help="~/workbuddy-agent-os/agent-local")
|
||||
parser.add_argument("--check-deps", action="store_true", help="检查 pip 依赖是否已安装")
|
||||
parser.add_argument("--json", action="store_true", dest="json_output", help="JSON 格式输出")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_yaml_simple(file_path: Path) -> dict:
|
||||
"""简单 YAML 解析(不依赖 pyyaml)"""
|
||||
data = {}
|
||||
current_key = None
|
||||
current_list = None
|
||||
current_section = data
|
||||
|
||||
try:
|
||||
lines = file_path.read_text(encoding="utf-8").splitlines()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
|
||||
indent = len(line) - len(line.lstrip())
|
||||
|
||||
# 列表项
|
||||
if stripped.startswith("- "):
|
||||
if current_list is not None:
|
||||
current_list.append(stripped[2:].strip().strip('"'))
|
||||
continue
|
||||
|
||||
if ":" in stripped:
|
||||
key, _, val = stripped.partition(":")
|
||||
key = key.strip()
|
||||
val = val.strip().strip('"').strip("'")
|
||||
|
||||
if val:
|
||||
if indent == 0:
|
||||
data[key] = val
|
||||
current_section = data
|
||||
current_list = None
|
||||
else:
|
||||
current_section[key] = val
|
||||
current_list = None
|
||||
else:
|
||||
# 嵌套块或列表开始
|
||||
if indent == 0:
|
||||
data[key] = {}
|
||||
current_section = data[key]
|
||||
current_list = None
|
||||
elif key:
|
||||
parent = current_section
|
||||
parent[key] = []
|
||||
current_list = parent[key]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_host_id(local_root: Path) -> str:
|
||||
"""读取本机 HOST_ID"""
|
||||
# 从 machine_info.json 读取
|
||||
machine_info = local_root / "machine_info.json"
|
||||
if machine_info.exists():
|
||||
try:
|
||||
info = json.loads(machine_info.read_text(encoding="utf-8"))
|
||||
return info.get("machine", {}).get("id", "unknown")
|
||||
except Exception:
|
||||
pass
|
||||
# 从 HOST_ID.md 读取
|
||||
host_id_md = local_root.parent / "agent-os" / "01_core" / "HOST_ID.md"
|
||||
# 已知路径
|
||||
for path in [Path("~/workbuddy-agent-os/agent-sync/01_core/HOST_ID.md").expanduser()]:
|
||||
if path.exists():
|
||||
content = path.read_text(encoding="utf-8")
|
||||
import re
|
||||
m = re.search(r'`([^`]+_(?:master|maintainer|node))`', content)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def check_pip_installed(packages: list) -> dict:
|
||||
"""检查 pip 包是否已安装"""
|
||||
results = {}
|
||||
for pkg in packages:
|
||||
pkg_name = pkg.split(">=")[0].split("==")[0].split("[")[0].strip()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", f"import {pkg_name.replace('-', '_')}"],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
results[pkg] = result.returncode == 0
|
||||
except Exception:
|
||||
results[pkg] = False
|
||||
return results
|
||||
|
||||
|
||||
def scan_skill(skill_dir: Path, check_deps: bool, host_id: str) -> dict:
|
||||
"""扫描单个技能目录"""
|
||||
name = skill_dir.name
|
||||
result = {
|
||||
"name": name,
|
||||
"path": str(skill_dir),
|
||||
"has_skill_md": (skill_dir / "SKILL.md").exists(),
|
||||
"has_skill_card": (skill_dir / "SKILL_CARD.yaml").exists(),
|
||||
"has_entry": False,
|
||||
"entry_point": None,
|
||||
"status": "unknown",
|
||||
"version": "?",
|
||||
"category": "?",
|
||||
"master_editor": "?",
|
||||
"is_master": False,
|
||||
"deps_ok": None,
|
||||
"missing_deps": [],
|
||||
"issues": []
|
||||
}
|
||||
|
||||
# 读取 SKILL_CARD.yaml
|
||||
card_path = skill_dir / "SKILL_CARD.yaml"
|
||||
if card_path.exists():
|
||||
card = load_yaml_simple(card_path)
|
||||
skill_info = card.get("skill", {})
|
||||
result["version"] = skill_info.get("version", "?")
|
||||
result["status"] = skill_info.get("status", "?")
|
||||
result["category"] = skill_info.get("category", "?")
|
||||
|
||||
# 读取 ownership
|
||||
ownership = card.get("ownership", {})
|
||||
result["master_editor"] = ownership.get("master_editor", "?")
|
||||
result["is_master"] = host_id != "unknown" and host_id == result["master_editor"]
|
||||
|
||||
# 读取入口脚本
|
||||
usage = card.get("usage", {})
|
||||
entry = usage.get("entry_point", "")
|
||||
if entry:
|
||||
result["entry_point"] = entry
|
||||
result["has_entry"] = (skill_dir / entry).exists()
|
||||
if not result["has_entry"]:
|
||||
result["issues"].append(f"入口脚本不存在: {entry}")
|
||||
|
||||
# 检查依赖
|
||||
if check_deps:
|
||||
env_info = card.get("environment", {})
|
||||
pip_deps = env_info.get("pip_dependencies", [])
|
||||
if isinstance(pip_deps, list) and pip_deps:
|
||||
dep_results = check_pip_installed(pip_deps)
|
||||
missing = [pkg for pkg, ok in dep_results.items() if not ok]
|
||||
result["deps_ok"] = len(missing) == 0
|
||||
result["missing_deps"] = missing
|
||||
if missing:
|
||||
result["issues"].append(f"缺失依赖: {', '.join(missing)}")
|
||||
else:
|
||||
result["issues"].append("缺少 SKILL_CARD.yaml")
|
||||
|
||||
# 没有 SKILL.md 也标记
|
||||
if not result["has_skill_md"]:
|
||||
result["issues"].append("缺少 SKILL.md")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
STATUS_ICON = {
|
||||
"active": "✅",
|
||||
"inactive": "⏸ ",
|
||||
"experimental": "🧪",
|
||||
"unknown": "❓",
|
||||
"?": "❓",
|
||||
}
|
||||
|
||||
|
||||
def print_report(skills: list, host_id: str):
|
||||
"""打印扫描报告"""
|
||||
print("=" * 65)
|
||||
print(f"AgentOS 技能状态报告 (本机: {host_id})")
|
||||
print("=" * 65)
|
||||
|
||||
has_card = [s for s in skills if s["has_skill_card"]]
|
||||
no_card = [s for s in skills if not s["has_skill_card"]]
|
||||
|
||||
if has_card:
|
||||
print("\n📋 已注册技能(有 SKILL_CARD.yaml):\n")
|
||||
for s in has_card:
|
||||
icon = STATUS_ICON.get(s["status"], "❓")
|
||||
master_tag = " 🔑[主机]" if s["is_master"] else ""
|
||||
issues_tag = f" ⚠️ {'; '.join(s['issues'])}" if s["issues"] else ""
|
||||
entry_tag = f" → {s['entry_point']}" if s["entry_point"] else ""
|
||||
print(f" {icon} {s['name']:<22} v{s['version']:<8} [{s['category']}]{master_tag}{entry_tag}{issues_tag}")
|
||||
|
||||
if no_card:
|
||||
print("\n📂 未注册技能(无 SKILL_CARD.yaml):\n")
|
||||
for s in no_card:
|
||||
print(f" ❓ {s['name']}")
|
||||
|
||||
print(f"\n统计: 共 {len(skills)} 个技能,"
|
||||
f"已注册 {len(has_card)},"
|
||||
f"未注册 {len(no_card)},"
|
||||
f"本机主机 {sum(1 for s in skills if s['is_master'])} 个")
|
||||
|
||||
issues_all = [s for s in skills if s["issues"]]
|
||||
if issues_all:
|
||||
print(f"\n⚠️ 需要处理的问题:\n")
|
||||
for s in issues_all:
|
||||
for issue in s["issues"]:
|
||||
print(f" [{s['name']}] {issue}")
|
||||
|
||||
print("\n图例: ✅=活跃 ⏸=暂停 🧪=实验性 🔑=本机主编辑")
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
root = Path(args.root).expanduser()
|
||||
local_root = Path(args.local).expanduser()
|
||||
skills_dir = root / "02_skills"
|
||||
|
||||
if not skills_dir.exists():
|
||||
print(f"[ERROR] 技能目录不存在: {skills_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
host_id = get_host_id(local_root)
|
||||
skills = []
|
||||
|
||||
for skill_dir in sorted(skills_dir.iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
if skill_dir.name.startswith(("_", ".")):
|
||||
continue
|
||||
skill = scan_skill(skill_dir, args.check_deps, host_id)
|
||||
skills.append(skill)
|
||||
|
||||
if args.json_output:
|
||||
print(json.dumps(skills, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print_report(skills, host_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试 oMLX embedding API 是否在自动化环境中正常工作
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
def test_embedding_api():
|
||||
"""测试 embedding API"""
|
||||
url = "http://localhost:8000/v1/embeddings"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer omlx"
|
||||
}
|
||||
data = {
|
||||
"model": "Qwen3-Embedding-0.6B",
|
||||
"input": ["测试文本:这是用于验证 oMLX embedding API 是否正常工作的测试文本。"]
|
||||
}
|
||||
|
||||
try:
|
||||
print("测试 oMLX embedding API...")
|
||||
print(f"URL: {url}")
|
||||
print(f"Headers: {headers}")
|
||||
print(f"Data: {json.dumps(data, ensure_ascii=False)}")
|
||||
|
||||
response = requests.post(url, headers=headers, json=data, timeout=30)
|
||||
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ API 调用成功!")
|
||||
print(f"返回数据: {json.dumps(result, ensure_ascii=False)[:200]}...")
|
||||
|
||||
# 检查向量维度
|
||||
if "data" in result and len(result["data"]) > 0:
|
||||
embedding = result["data"][0]["embedding"]
|
||||
print(f"向量维度: {len(embedding)} (应为 1024)")
|
||||
if len(embedding) == 1024:
|
||||
print("✅ 向量维度正确!")
|
||||
else:
|
||||
print(f"⚠️ 向量维度异常: {len(embedding)}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ API 调用失败: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ 无法连接到 oMLX API (ConnectionError)")
|
||||
print("请确保 oMLX 应用正在运行,端口 8000 可访问")
|
||||
return False
|
||||
except requests.exceptions.Timeout:
|
||||
print("❌ API 请求超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 未知错误: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
def test_chat_api():
|
||||
"""测试 chat completions API"""
|
||||
url = "http://localhost:8000/v1/chat/completions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer omlx"
|
||||
}
|
||||
data = {
|
||||
"model": "Qwen3-8B-MLX-4bit",
|
||||
"messages": [{"role": "user", "content": "你好,请回复'测试成功'"}],
|
||||
"stream": False,
|
||||
"temperature": 0.1
|
||||
}
|
||||
|
||||
try:
|
||||
print("\n测试 oMLX chat completions API...")
|
||||
response = requests.post(url, headers=headers, json=data, timeout=60)
|
||||
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ Chat API 调用成功!")
|
||||
if "choices" in result and len(result["choices"]) > 0:
|
||||
message = result["choices"][0]["message"]["content"]
|
||||
print(f"回复内容: {message}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Chat API 调用失败: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Chat API 错误: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
def test_models_api():
|
||||
"""测试 models API"""
|
||||
url = "http://localhost:8000/v1/models"
|
||||
headers = {"Authorization": "Bearer omlx"}
|
||||
|
||||
try:
|
||||
print("\n测试 oMLX models API...")
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ Models API 调用成功!")
|
||||
print(f"可用模型: {json.dumps(result, ensure_ascii=False)}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Models API 调用失败: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Models API 错误: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("oMLX API 测试脚本")
|
||||
print("=" * 60)
|
||||
|
||||
# 测试 models API
|
||||
models_ok = test_models_api()
|
||||
|
||||
# 测试 embedding API
|
||||
embedding_ok = test_embedding_api()
|
||||
|
||||
# 测试 chat API
|
||||
chat_ok = test_chat_api()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试结果汇总:")
|
||||
print(f" Models API: {'✅ 正常' if models_ok else '❌ 异常'}")
|
||||
print(f" Embedding API: {'✅ 正常' if embedding_ok else '❌ 异常'}")
|
||||
print(f" Chat API: {'✅ 正常' if chat_ok else '❌ 异常'}")
|
||||
print("=" * 60)
|
||||
|
||||
# 关键检查:embedding API 是否正常
|
||||
if embedding_ok:
|
||||
print("\n✅ 关键依赖:embedding API 正常,向量化功能可用")
|
||||
print(" 这意味着 daily_digest.py 的向量化步骤可以正常工作")
|
||||
else:
|
||||
print("\n❌ 关键依赖:embedding API 异常,向量化功能不可用")
|
||||
print(" 这会影响记忆系统的语义检索能力")
|
||||
|
||||
if not chat_ok:
|
||||
print("\n⚠️ Chat API 异常,但这不是关键依赖")
|
||||
print(" inbox_refine.py 的智能分类功能会降级到启发式规则")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
触发词语义匹配检测工具
|
||||
混合模式:先关键词精确匹配,未命中则 Embedding 语义相似度匹配
|
||||
|
||||
用法:
|
||||
python3 trigger_matcher.py "用户说的话"
|
||||
|
||||
返回:
|
||||
matched: true/false
|
||||
category: "meta-thinking" / "cross-domain" / "knowledge-review" / ""
|
||||
match_type: "keyword" / "semantic" / ""
|
||||
similarity: 0.0-1.0
|
||||
"""
|
||||
|
||||
import json, sys, os, urllib.request
|
||||
|
||||
# 触发类别及种子词
|
||||
TRIGGER_CATEGORIES = {
|
||||
"meta-thinking": ["升维思考", "第一性原理", "前提挑战", "深层原因", "本质是什么", "批判地看", "升维", "本质", "前提"],
|
||||
"cross-domain": ["跨界视角", "换个角度", "类比一下", "新视角", "别的领域", "借鉴一下", "跨界", "类比", "新角度", "换个思路"],
|
||||
"knowledge-review": ["入库", "记录这条", "保存知识", "知识审查", "记录", "保存"],
|
||||
}
|
||||
|
||||
EMBEDDING_URL = "http://localhost:8000/v1/embeddings"
|
||||
EMBEDDING_MODEL = "Qwen3-Embedding-0.6B"
|
||||
SIMILARITY_THRESHOLD = 0.8
|
||||
|
||||
|
||||
def keyword_match(text: str) -> tuple:
|
||||
"""关键词精确匹配,返回 (category, trigger_word) 或 ("", "")"""
|
||||
for category, keywords in TRIGGER_CATEGORIES.items():
|
||||
for kw in keywords:
|
||||
if kw in text:
|
||||
return category, kw
|
||||
return "", ""
|
||||
|
||||
|
||||
def get_embedding(text: str) -> list:
|
||||
"""调用 oMLX Embedding API 获取向量"""
|
||||
data = json.dumps({
|
||||
"model": EMBEDDING_MODEL,
|
||||
"input": text
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
EMBEDDING_URL, data=data,
|
||||
headers={"Authorization": "Bearer omlx", "Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
result = json.loads(resp.read())
|
||||
return result["data"][0]["embedding"]
|
||||
|
||||
|
||||
def cosine_similarity(a: list, b: list) -> float:
|
||||
"""计算余弦相似度"""
|
||||
dot = sum(x * y for x, y in zip(a, b))
|
||||
na = sum(x * x for x in a) ** 0.5
|
||||
nb = sum(y * y for y in b) ** 0.5
|
||||
return dot / (na * nb) if na and nb else 0.0
|
||||
|
||||
|
||||
def semantic_match(text: str) -> tuple:
|
||||
"""语义相似度匹配,返回 (best_category, best_score)"""
|
||||
try:
|
||||
text_vec = get_embedding(text)
|
||||
except Exception as e:
|
||||
return ("", 0.0)
|
||||
|
||||
best_cat = ""
|
||||
best_score = 0.0
|
||||
for category, keywords in TRIGGER_CATEGORIES.items():
|
||||
# 用类别名 + 第一个种子词作为类别向量
|
||||
cat_text = f"{category}: {', '.join(keywords[:2])}"
|
||||
try:
|
||||
cat_vec = get_embedding(cat_text)
|
||||
score = cosine_similarity(text_vec, cat_vec)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_cat = category
|
||||
except:
|
||||
continue
|
||||
return (best_cat, best_score)
|
||||
|
||||
|
||||
def detect(text: str) -> dict:
|
||||
"""主检测函数:先关键词,再语义"""
|
||||
cat, kw = keyword_match(text)
|
||||
if cat:
|
||||
return {
|
||||
"matched": True,
|
||||
"category": cat,
|
||||
"match_type": "keyword",
|
||||
"trigger": kw,
|
||||
"similarity": 1.0
|
||||
}
|
||||
|
||||
cat, score = semantic_match(text)
|
||||
if cat and score >= SIMILARITY_THRESHOLD:
|
||||
return {
|
||||
"matched": True,
|
||||
"category": cat,
|
||||
"match_type": "semantic",
|
||||
"trigger": "",
|
||||
"similarity": round(score, 4)
|
||||
}
|
||||
|
||||
return {
|
||||
"matched": False,
|
||||
"category": "",
|
||||
"match_type": "",
|
||||
"trigger": "",
|
||||
"similarity": 0.0
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: trigger_matcher.py <文本>")
|
||||
sys.exit(1)
|
||||
text = sys.argv[1]
|
||||
result = detect(text)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
Reference in New Issue
Block a user