feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,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))