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,152 @@
"""
agentos ave — 视频工厂(渲染/脚本/素材/模板)
封装 09_ave/scripts/ 目录下的 96 个脚本,提供 CLI 统一入口。
⚠️ 状态:**已废弃(2026-10-01)** —— 本插件引用的脚本路径全部失效
(`01_director_parser/run.py`、`06_composer/run.py`、`07_service_layer/*.py`、
`asset_manager/run.py` 均不存在;早期编号目录已归档到
`90_archive/ave_legacy_numbered_dirs_20261001/`)。
视频工厂的实际入口:
· CLI 原子能力:`09_ave/scripts/main.py`(voice/material/parse/compose/anchors…)
· 生产模式: `09_ave/scripts/video_factory.py`(run_oral/beat/digital_human/hybrid/story)
· 能力清单: `09_ave/config/capabilities.yaml` + Dashboard「🧩 能力库」
· Dashboard: 视频工厂各视图(导演创作/工作流/能力库…)
保留本文件仅为兼容 `agentos ave --help` 的历史调用,**不建议继续使用**。
"""
import argparse
import subprocess
import sys
from pathlib import Path
from agentos.base import AgentOSPlugin
AGENT_SYNC = Path(__file__).resolve().parent.parent.parent.parent.parent
AVE_SCRIPTS = AGENT_SYNC / "05_tools" / "09_ave" / "scripts"
class AvePlugin(AgentOSPlugin):
name = "ave"
description = "视频工厂 — 渲染/脚本/素材/模板"
nav = {
'group': '视频工厂',
'icon': '🎬',
'order': 3,
'items': [
{'view': 'ave-render', 'label': '渲染任务'},
{'view': 'ave-script', 'label': '脚本生成'},
{'view': 'ave-materials', 'label': '素材库'},
{'view': 'ave-templates', 'label': '模板'},
]
}
def _run_script(self, script_name: str, args: list[str] = None) -> int:
"""运行 AVE scripts 目录下的 Python 脚本"""
script_path = AVE_SCRIPTS / script_name
if not script_path.exists():
print(f"脚本不存在: {script_path}")
return 1
cmd = [sys.executable, str(script_path)] + (args or [])
result = subprocess.run(cmd, cwd=str(AVE_SCRIPTS))
return result.returncode
def register(self, subparsers):
p = subparsers.add_parser("ave", help=self.description)
p_sub = p.add_subparsers(dest="ave_action", help="视频工厂操作")
# render - 视频渲染
pr = p_sub.add_parser("render", help="视频渲染/生成")
pr.add_argument("--strategy", default="oral", help="策略: oral/beat-sync/digital-human")
pr.add_argument("--script", help="脚本路径")
pr.add_argument("--count", type=int, default=1, help="生成数量")
pr.set_defaults(ave_func=self.cmd_render)
# script - 脚本/文案生成
ps = p_sub.add_parser("script", help="脚本/文案生成")
ps.add_argument("action", nargs="?", default="list",
choices=["list", "generate", "edit"],
help="操作")
ps.add_argument("--topic", help="主题/方向")
ps.add_argument("--style", help="风格: 口播/卡点/叙事")
ps.set_defaults(ave_func=self.cmd_script)
# material - 素材管理
pm = p_sub.add_parser("material", help="素材管理")
pm.add_argument("action", nargs="?", default="list",
choices=["list", "search", "add", "stats"],
help="操作")
pm.add_argument("--tag", help="标签过滤")
pm.add_argument("--type", dest="mat_type", help="素材类型: bgm/clip/character")
pm.set_defaults(ave_func=self.cmd_material)
# template - 模板管理
pt = p_sub.add_parser("template", help="模板管理")
pt.add_argument("action", nargs="?", default="list",
choices=["list", "create", "edit", "apply"],
help="操作")
pt.set_defaults(ave_func=self.cmd_template)
# character - 角色管理(包装 character_portrait.py)
pch = p_sub.add_parser("character", help="角色管理")
pch.add_argument("action", nargs="?", default="list",
choices=["list", "create", "generate"],
help="操作")
pch.set_defaults(ave_func=self.cmd_character)
# workflow - 工作流(包装 workflow 模块)
pwf = p_sub.add_parser("workflow", help="工作流管理")
pwf.add_argument("action", nargs="?", default="list",
choices=["list", "run", "status"],
help="操作")
pwf.set_defaults(ave_func=self.cmd_workflow)
return p
def dispatch(self, args):
if hasattr(args, 'ave_func'):
return args.ave_func(args)
print("未知视频工厂命令,使用 agentos ave --help")
return 1
def cmd_render(self, args):
print(f"🎬 渲染任务: strategy={args.strategy}")
if args.strategy == "oral":
return self._run_script("01_director_parser/run.py", [f"--strategy={args.strategy}"])
print(f" 调用 06_composer 模块 (strategy={args.strategy}, count={args.count})")
return self._run_script("06_composer/run.py", [f"--strategy={args.strategy}", f"--count={args.count}"])
def cmd_script(self, args):
if args.action == "generate":
print(f"📝 生成脚本: topic={args.topic or '(自动)'}, style={args.style or '口播'}")
return self._run_script("01_director_parser/run.py",
["--action=generate_script"] +
(["--topic", args.topic] if args.topic else []) +
(["--style", args.style] if args.style else []))
print(f"📝 脚本 {args.action}")
return self._run_script("01_director_parser/run.py", [f"--action={args.action}"])
def cmd_material(self, args):
print(f"📦 素材 {args.action}" + (f" (tag={args.tag})" if args.tag else ""))
return self._run_script("asset_manager/run.py",
[f"--action={args.action}"] +
(["--tag", args.tag] if args.tag else []) +
(["--type", args.mat_type] if args.mat_type else []))
def cmd_template(self, args):
print(f"📋 模板 {args.action}")
if args.action == "list":
return self._run_script("07_service_layer/template_manager.py", ["list"])
elif args.action == "apply":
print("(模板应用需要指定模板ID,功能开发中)")
return 0
def cmd_character(self, args):
print(f"🧑 角色 {args.action}")
return self._run_script("character_portrait.py", [f"--action={args.action}"])
def cmd_workflow(self, args):
print(f"🔧 工作流 {args.action}")
return self._run_script("07_service_layer/workflow_engine.py", [f"--action={args.action}"])
@@ -0,0 +1,150 @@
"""
agentos crawl — 内容采集(网页/视频/提取/调度)
封装 05_crawl/ 目录下的采集脚本 + web_crawler 技能。
"""
import argparse
import subprocess
import sys
from pathlib import Path
from agentos.base import AgentOSPlugin
AGENT_SYNC = Path(__file__).resolve().parent.parent.parent.parent.parent
CRAWL_DIR = AGENT_SYNC / "05_tools" / "05_crawl"
class CrawlPlugin(AgentOSPlugin):
name = "crawl"
description = "内容采集 — 网页/视频/提取/调度"
nav = {
'group': '内容采集',
'icon': '📡',
'order': 4,
'items': [
{'view': 'crawl-tasks', 'label': '采集任务'},
{'view': 'crawl-sources', 'label': '源管理'},
{'view': 'crawl-history', 'label': '采集历史'},
]
}
def _run_script(self, script_name: str, args: list[str] = None) -> int:
"""运行 crawl 目录下的 Python 脚本"""
script_path = CRAWL_DIR / script_name
if not script_path.exists():
print(f"脚本不存在: {script_path}")
return 1
cmd = [sys.executable, str(script_path)] + (args or [])
result = subprocess.run(cmd, cwd=str(CRAWL_DIR))
return result.returncode
def register(self, subparsers):
p = subparsers.add_parser("crawl", help=self.description)
p_sub = p.add_subparsers(dest="crawl_action", help="采集操作")
# web - 网页抓取
pw = p_sub.add_parser("web", help="网页内容抓取")
pw.add_argument("url", nargs="?", help="目标 URL")
pw.add_argument("--output", help="输出文件路径")
pw.add_argument("--depth", type=int, default=1, help="抓取深度")
pw.set_defaults(crawl_func=self.cmd_web)
# video - 视频下载
pv = p_sub.add_parser("video", help="视频下载")
pv.add_argument("url", nargs="?", help="视频 URL")
pv.add_argument("--format", default="mp4", help="输出格式")
pv.set_defaults(crawl_func=self.cmd_video)
# extract - 信息提取
pe = p_sub.add_parser("extract", help="信息提取")
pe.add_argument("file", nargs="?", help="目标文件路径")
pe.add_argument("--fields", help="提取字段 (逗号分隔)")
pe.add_argument("--url", help="从 URL 提取")
pe.set_defaults(crawl_func=self.cmd_extract)
# schedule - 定时采集
psc = p_sub.add_parser("schedule", help="采集定时任务")
psc.add_argument("action", nargs="?", default="list",
choices=["list", "add", "remove"],
help="操作")
psc.set_defaults(crawl_func=self.cmd_schedule)
# source - 采集源管理
ps = p_sub.add_parser("source", help="采集源管理")
ps.add_argument("action", nargs="?", default="list",
choices=["list", "add", "remove", "test"],
help="操作")
ps.add_argument("--name", help="源名称")
ps.add_argument("--url", dest="src_url", help="源URL")
ps.add_argument("--type", dest="src_type",
choices=["rss", "web", "api", "social"],
help="源类型")
ps.set_defaults(crawl_func=self.cmd_source)
# content-inspiration - 内容灵感采集
pci = p_sub.add_parser("inspiration", help="内容灵感采集")
pci.add_argument("action", nargs="?", default="daily",
choices=["daily", "trending", "search"],
help="操作")
pci.add_argument("--keyword", help="搜索关键词")
pci.set_defaults(crawl_func=self.cmd_inspiration)
return p
def dispatch(self, args):
if hasattr(args, 'crawl_func'):
return args.crawl_func(args)
print("未知采集命令,使用 agentos crawl --help")
return 1
def cmd_web(self, args):
print(f"🌐 网页抓取: {args.url or '(交互模式)'}")
if args.url:
return self._run_script("content-inspiration/run.py",
["web", args.url] +
(["--output", args.output] if args.output else []))
print("请提供 URL: agentos crawl web <url>")
return 1
def cmd_video(self, args):
print(f"🎥 视频下载: {args.url or '(交互模式)'}")
if args.url:
return self._run_script("content-inspiration/run.py",
["video", args.url, "--format", args.format])
print("请提供视频 URL: agentos crawl video <url>")
return 1
def cmd_extract(self, args):
print(f"🔍 信息提取: {args.file or '(交互模式)'}")
if args.file:
cmd = ["extract", args.file]
if args.fields:
cmd += ["--fields", args.fields]
return self._run_script("content-inspiration/run.py", cmd)
if args.url:
return self._run_script("content-inspiration/run.py", ["extract", "--url", args.url])
print("请指定文件或URL: agentos crawl extract <file> 或 --url <url>")
return 1
def cmd_schedule(self, args):
print(f"⏰ 采集定时任务 {args.action}")
return self._run_script("content-inspiration/run.py", ["schedule", args.action])
def cmd_source(self, args):
print(f"📡 采集源 {args.action}")
cmd = ["source", args.action]
if args.name:
cmd += ["--name", args.name]
if args.src_url:
cmd += ["--url", args.src_url]
if args.src_type:
cmd += ["--type", args.src_type]
return self._run_script("content-inspiration/run.py", cmd)
def cmd_inspiration(self, args):
print(f"💡 内容灵感 {args.action}")
cmd = ["inspiration", args.action]
if args.keyword:
cmd += ["--keyword", args.keyword]
return self._run_script("content-inspiration/run.py", cmd)
@@ -0,0 +1,123 @@
"""
agentos fleet — 联邦管理(多机协同)
功能:
sync 一键同步所有机器
reconcile 对账检查
exec 远程执行命令
status 集群状态
"""
import argparse
import subprocess
import sys
from pathlib import Path
from agentos.base import AgentOSPlugin
AGENT_SYNC = Path(__file__).resolve().parent.parent.parent.parent.parent.parent # x6 → agent-sync/
class FleetPlugin(AgentOSPlugin):
name = "fleet"
description = "联邦管理 — 多机协同、同步、对账"
nav = {
'group': '联邦管理',
'icon': '🖥️',
'order': 4,
'items': [
{'view': 'matrix-summary', 'label': '集群概览'},
{'view': 'machines', 'label': '机器状态'},
]
}
def register(self, subparsers):
p = subparsers.add_parser("fleet", help=self.description)
p_sub = p.add_subparsers(dest="fleet_action", help="联邦操作")
# sync
ps = p_sub.add_parser("sync", help="一键同步所有机器")
ps.set_defaults(fleet_func=self.cmd_sync)
# reconcile
pr = p_sub.add_parser("reconcile", help="对账检查")
pr.set_defaults(fleet_func=self.cmd_reconcile)
# exec
pe = p_sub.add_parser("exec", help="在远程机器执行命令")
pe.add_argument("host", help="目标机器名或 Tailscale IP")
pe.add_argument("command", nargs="+", help="要执行的命令")
pe.set_defaults(fleet_func=self.cmd_exec)
# status
ps2 = p_sub.add_parser("status", help="集群状态")
ps2.set_defaults(fleet_func=self.cmd_status)
return p
def dispatch(self, args):
if hasattr(args, 'fleet_func'):
return args.fleet_func(args)
print("未知联邦命令,使用 agentos fleet --help")
return 1
def cmd_sync(self, args):
fleet_sh = AGENT_SYNC / "00_bootstrap" / "fleet_sync.sh"
if fleet_sh.exists():
subprocess.run(["bash", str(fleet_sh)])
else:
print(f"fleet_sync.sh 不存在: {fleet_sh}")
return 0
def cmd_reconcile(self, args):
rec_sh = AGENT_SYNC / "00_bootstrap" / "fleet_reconcile.sh"
if rec_sh.exists():
subprocess.run(["bash", str(rec_sh)])
else:
print(f"fleet_reconcile.sh 不存在: {rec_sh}")
return 0
def cmd_exec(self, args):
"""执行远程命令(委托给 mc remote exec)"""
from mc.cli import cmd_remote, _lookup_machines, _ssh_call
# 构造 args 对象
class FakeArgs:
remote_action = "exec"
host = args.host
command = args.command
via = "ssh"
json = False
cmd_remote(FakeArgs())
return 0
def cmd_status(self, args):
from mc.cli import cmd_remote, _lookup_machines
class FakeArgs:
remote_action = "status"
host = ""
json = False
cmd_remote(FakeArgs())
return 0
def cmd_cleanup(self, args):
"""清理所有机器残留的 Playwright 驱动进程"""
import subprocess
machines = [
("本机", "127.0.0.1", ""),
("5kechengdeAir", "100.72.182.121", "5kecheng"),
("7kecheng", "100.65.35.28", "7kecheng"),
]
for name, ip, user in machines:
print(f"\n → {name} ({ip})...")
if name == "本机":
result = subprocess.run(
["pkill", "-f", "playwright/driver/node"],
capture_output=True, text=True)
print(f" 已清理" if result.returncode == 0 else " 无残留")
else:
result = subprocess.run(
["ssh", user, ip, "pkill -f playwright/driver/node"],
capture_output=True, text=True)
print(f" 已清理" if result.returncode == 0 else " 无残留")
print("\n ✅ 清理完成")
return 0
@@ -0,0 +1,162 @@
"""
agentos guardd — 节点代理管理
功能:
status 查看 guardd 运行状态
restart 重启 guardd
logs 查看 guardd 日志
install 安装/更新 guardd plist
tasks 列出运行中任务
stop 停止指定任务
"""
import argparse, json, os, subprocess, time, urllib.request, urllib.error
from pathlib import Path
from agentos.base import AgentOSPlugin
AGENT_SYNC = Path(__file__).resolve().parent.parent.parent.parent.parent.parent
GUARDD_PORT = 9090
def _guardd_api(path: str) -> dict:
"""调用本地 guardd HTTP API"""
try:
r = urllib.request.urlopen(f"http://localhost:{GUARDD_PORT}{path}", timeout=5)
return json.loads(r.read().decode())
except Exception:
return {}
class GuarddPlugin(AgentOSPlugin):
name = "guardd"
description = "节点代理管理 — 部署/监控/任务管理"
def register(self, subparsers):
p = subparsers.add_parser("guardd", help=self.description)
p_sub = p.add_subparsers(dest="guardd_action", help="操作")
ps = p_sub.add_parser("status", help="查看 guardd 运行状态")
ps.set_defaults(guardd_func=self.cmd_status)
pr = p_sub.add_parser("restart", help="重启 guardd")
pr.set_defaults(guardd_func=self.cmd_restart)
pl = p_sub.add_parser("logs", help="查看 guardd 日志")
pl.add_argument("--tail", type=int, default=30, help="显示行数")
pl.set_defaults(guardd_func=self.cmd_logs)
pi = p_sub.add_parser("install", help="安装/更新 guardd plist 并重启")
pi.set_defaults(guardd_func=self.cmd_install)
pt = p_sub.add_parser("tasks", help="列出运行中任务")
pt.set_defaults(guardd_func=self.cmd_tasks)
pk = p_sub.add_parser("stop", help="停止指定任务")
pk.add_argument("run_id", help="任务 ID")
pk.set_defaults(guardd_func=self.cmd_stop)
return p
def dispatch(self, args):
if hasattr(args, 'guardd_func'):
return args.guardd_func(args)
print("未知 guardd 命令,使用 agentos guardd --help")
return 1
def cmd_status(self, args):
"""查看 guardd 状态"""
# launchctl 状态
r = subprocess.run(
["launchctl", "print", f"gui/{os.getuid()}/com.agentos.guardd"],
capture_output=True, text=True, timeout=5
)
if r.returncode == 0:
for line in r.stdout.split("\n"):
if "state = " in line:
print(f" launchd: 🟢 {line.strip()}")
else:
print(" launchd: ❌ 未运行")
# HTTP 健康检查
health = _guardd_api("/health")
if health:
print(f" HTTP: 🟢 运行中 (端口 {GUARDD_PORT})")
print(f" 版本: {health.get('version', '?')}")
print(f" 运行: {health.get('uptime_sec', 0)}s")
print(f" 任务: {health.get('running_tasks', 0)} 运行中 / {health.get('total_tasks', 0)} 总计")
else:
print(f" HTTP: ❌ 无法连接 (localhost:{GUARDD_PORT})")
return 0
def cmd_restart(self, args):
"""重启 guardd"""
print(" ⏹ 停止 guardd...")
subprocess.run(["launchctl", "unload", str(Path.home() / "Library/LaunchAgents/com.agentos.guardd.plist")],
capture_output=True, timeout=5)
time.sleep(1)
print(" 🚀 启动 guardd...")
subprocess.run(["launchctl", "load", str(Path.home() / "Library/LaunchAgents/com.agentos.guardd.plist")],
capture_output=True, timeout=5)
time.sleep(2)
health = _guardd_api("/health")
print(f" {'✅ guardd 已重启' if health else '❌ guardd 启动失败'}")
return 0
def cmd_logs(self, args):
"""查看 guardd 日志"""
log_file = Path.home() / "workbuddy-agent-os" / "agent-local" / "runtime" / "guardd" / "guardd.log"
if log_file.exists():
lines = log_file.read_text().splitlines()
tail = lines[-args.tail:]
print(f" 📋 guardd.log (最后 {args.tail} 行):\n")
for line in tail:
print(f" {line}")
else:
print(" ❌ 日志文件不存在")
return 0
def cmd_install(self, args):
"""安装/更新 guardd plist"""
plist_src = AGENT_SYNC / "05_tools" / "00_setup" / "guardd" / "com.agentos.guardd.plist"
plist_dst = Path.home() / "Library/LaunchAgents" / "com.agentos.guardd.plist"
if not plist_src.exists():
print(f" ❌ 源 plist 不存在: {plist_src}")
return 1
# 替换 __HOME__ 占位符
content = plist_src.read_text().replace("__HOME__", str(Path.home()))
plist_dst.write_text(content)
print(f" ✅ plist 已安装: {plist_dst}")
return self.cmd_restart(args)
def cmd_tasks(self, args):
"""列出 guardd 任务"""
tasks = _guardd_api("/tasks")
if not isinstance(tasks, list):
print(" ❌ 无法获取任务列表(guardd HTTP 不可用)")
return 1
if not tasks:
print(" 📭 无任务记录")
return 0
print(f" 📋 共 {len(tasks)} 个任务:\n")
for t in tasks:
status = t.get("status", "?")
icon = {"running": "🟢", "completed": "✅", "failed": "❌", "cancelled": "⏹"}.get(status, "❓")
cmd = t.get("cmd", "")[:60]
pid = t.get("pid", "?")
elapsed = ""
if t.get("start_time"):
import datetime
st = datetime.datetime.fromisoformat(t["start_time"])
elapsed = f"{(datetime.datetime.now(datetime.timezone.utc) - st).total_seconds():.0f}s"
print(f" {icon} {t.get('run_id','?')[:12]} | {status:10s} | PID={pid} | {elapsed:>6s} | {cmd}")
return 0
def cmd_stop(self, args):
"""停止任务"""
result = _guardd_api(f"/task/{args.run_id}/stop")
if result.get("status") in ("stopped", "already_stopped"):
print(f" ⏹ 任务 {args.run_id} 已停止")
else:
print(f" ❌ 停止失败: {result.get('error', '未知错误')}")
return 0
@@ -0,0 +1,599 @@
"""
agentos matrix — 社交矩阵(抖音/小红书运营)
混合模式:
- 简单命令:原生实现(不依赖 mc)
- 复杂命令:完整前置检查后委托 mc(含并发/预检/优雅退出)
执行约束(源自 99_system/ARCHITECTURE_CONSTITUTION.md):
- MAX_CONCURRENT = 3 每台机器最多3个浏览器
- LAUNCH_STAGGER = 15s 浏览器启动间隔
- AUTO_SHUTDOWN = 30min 超时自动关闭
- MIN_DISK_GB = 5 最小磁盘空间
"""
import sys, os, argparse, json, subprocess, time, asyncio, shutil, glob
from pathlib import Path
from agentos.base import AgentOSPlugin
SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(SCRIPTS_DIR))
# ═══════════════════════════════════════════════
# 执行约束(统一来自 mc.execution_policy)
# ═══════════════════════════════════════════════
from mc.execution_policy import (
MAX_CONCURRENT, LAUNCH_STAGGER, AUTO_SHUTDOWN_MIN, MIN_DISK_GB,
GRACE_PERIOD_LOCAL, GRACE_PERIOD_REMOTE, MAX_TIMEOUT, SLOTS,
preflight, slot_for, check_running_browsers, get_policy,
)
# 需要浏览器的操作
BROWSER_COMMANDS = {'run', 'collect', 'login', 'smart-login', 'nurture', 'publish'}
class MatrixPlugin(AgentOSPlugin):
name = "matrix"
description = "社交矩阵 — 抖音/小红书账号运营、养号、采集、发布"
def register(self, subparsers):
"""注册 matrix 子命令 — 全部通过 nargs='*' 捕获,dispatch 手动路由"""
parser = subparsers.add_parser("matrix", help=self.description)
parser.add_argument("args", nargs="*", help="子命令及参数")
return parser
def dispatch(self, args):
# 不从 args.args 读取,直接从 sys.argv 获取原始参数
# sys.argv = ['agentos', 'matrix', 'publish', 'douyin', '--account', 'x', ...]
raw = sys.argv[:]
# 找到 'matrix' 之后的所有参数
try:
idx = raw.index('matrix')
raw = raw[idx + 1:]
except ValueError:
raw = getattr(args, 'args', [])
if not raw:
print("请指定子命令: account / blueprint / corpus / status\n"
"或 mc 命令: run / collect / login / publish ...")
return 1
cmd = raw[0]
rest = raw[1:]
if cmd == 'account': return self.cmd_account_list(rest)
elif cmd == 'blueprint': return self.cmd_blueprint_list(rest)
elif cmd == 'corpus': return self.cmd_corpus_list(rest)
elif cmd == 'status': return self.cmd_status_all(rest)
elif cmd == 'collect': return self.cmd_collect(rest)
elif cmd in ('login', 'smart-login'): return self.cmd_login(rest)
elif cmd == 'logout': return self.cmd_logout(rest)
elif cmd == 'publish': return self.cmd_publish(rest)
elif cmd == 'schedule': return self.cmd_schedule(rest)
elif cmd == 'run': return self.cmd_run(rest)
else:
return self._execute_with_guards(cmd)
# ═══════════════════════════════════════════════
# 原生命令(接收 rest 参数列表)
# ═══════════════════════════════════════════════
def cmd_account_list(self, rest):
"""agentos matrix account [list]"""
show_json = '--json' in rest or '-j' in rest
try:
from matrix_mgmt import MatrixManager
accounts = MatrixManager().list_accounts()
if not accounts:
print("📋 暂无账号"); return 0
if show_json:
print(json.dumps(accounts, ensure_ascii=False, indent=2))
return 0
print(f"\n📋 账号列表 ({len(accounts)}):")
print(f"{'ID':20s} {'平台':15s} {'状态':10s} {'手机':15s} {'本机':8s}")
print("-" * 70)
for a in accounts:
pid = a.get("id", "?")
plat = a.get("platform", "?")
plat_icon = "🎵" if plat == "douyin" else "📕"
status_raw = a.get("_status", "")
cookie = "🟢 已登录" if status_raw == "logged_in" else "🔴 离线"
phone = a.get("phone", "")[:11]
local = "本机" if a.get("is_local") else "远程"
print(f" {pid:20s} {plat_icon:<5s} {cookie:<10s} {phone:<15s} {local:<8s}")
return 0
except Exception as e:
print(f"❌ 加载账号失败: {e}")
return 1
def cmd_blueprint_list(self, rest):
try:
from matrix_mgmt import MatrixManager
bps = MatrixManager().list_blueprints()
except Exception as e:
print(f"❌ 加载蓝图失败: {e}"); return 1
if not bps: print("📋 暂无蓝图"); return 0
print(f"\n📋 蓝图 ({len(bps)}):")
for bp in bps:
name = bp.get("name", "?")
steps = len(bp.get("steps", []))
platform = bp.get("platform", "通用")
desc = bp.get("description", "")[:50]
print(f" • {name:<25s} ({steps}步) {platform:<12s} {desc}")
return 0
def cmd_corpus_list(self, rest):
try:
from matrix_modules.nurture.comment_corpus import get_categories
cats = get_categories()
except Exception:
cats = []
if not cats: print("📋 语料分类: 暂无"); return 0
print(f"\n📋 语料分类 ({len(cats)}):")
for c in cats:
name = c.get("name", "?") if isinstance(c, dict) else str(c)
print(f" • {name}")
return 0
def cmd_status_all(self, rest):
try:
r = subprocess.run(["curl", "-s", "--max-time", "5",
"http://localhost:9988/api/machines"], capture_output=True, text=True, timeout=8)
data = json.loads(r.stdout)
machines = data.get("machines", []) if isinstance(data, dict) else data
except Exception:
machines = []
print(f"\n📊 多机状态 ({len(machines)}):")
for m in (machines if isinstance(machines, list) else []):
name = m.get("hostname", m.get("name", "?"))
online = m.get("status") == "online"
browsers = m.get("browsers_running", 0)
print(f" {'✅' if online else '🔴'} {name} 浏览器:{browsers}/{MAX_CONCURRENT}")
if m.get("_last_push_sec"):
s = m["_last_push_sec"]
print(f" 🕐 最后通信: {s}s前 ({'在线' if s < 300 else '超时'})")
print(f"\n 约束: MAX_CONCURRENT={MAX_CONCURRENT} STAGGER={LAUNCH_STAGGER}s "
f"TIMEOUT={MAX_TIMEOUT}s SHUTDOWN={AUTO_SHUTDOWN_MIN}min")
return 0
# ═══════════════════════════════════════════════
# collect — 原生实现
# ═══════════════════════════════════════════════
def cmd_collect(self, rest):
"""agentos matrix collect [--phone PHONE] [--account ACCT] [--all] [--status]"""
# 解析参数
phone = ''
account = ''
show_status = False
collect_all = False
i = 0
while i < len(rest):
if rest[i] == '--phone' and i + 1 < len(rest):
phone = rest[i + 1]; i += 2
elif rest[i] == '--account' and i + 1 < len(rest):
account = rest[i + 1]; i += 2
elif rest[i] == '--all':
collect_all = True; i += 1
elif rest[i] == '--status':
show_status = True; i += 1
else:
i += 1
# --status: 查看进度(调用 mc 进度API)
if show_status:
return self._collect_status()
# 构建执行命令
runner = SCRIPTS_DIR / "collect_batch_runner.py"
if not runner.exists():
runner = SCRIPTS_DIR / "collect_homepage_info.py"
cmd = [sys.executable, str(runner)]
if phone:
cmd += ["--single", phone]
elif account:
cmd += ["--single", account]
# --all: 不加参数,默认执行全部
return self._run_collect(cmd)
def _collect_status(self) -> int:
"""查看采集进度"""
try:
from collect_batch_runner import PROGRESS_FILE
import json
if PROGRESS_FILE.exists():
data = json.loads(PROGRESS_FILE.read_text())
print(f"📊 采集进度: {data.get('completed',0)}/{data.get('total_identities','?')}")
print(f" 成功: {data.get('success',0)}, 失败: {data.get('failed',0)}")
print(f" 状态: {data.get('status','?')}")
if data.get('finished_at'):
print(f" 完成时间: {data['finished_at']}")
else:
print("📊 无正在执行的采集任务")
except Exception as e:
print(f"❌ 读取进度失败: {e}")
return 0
def _run_collect(self, cmd: list) -> int:
"""执行采集(走完整保护流程)"""
cmd_str = ' '.join(str(c) for c in cmd[:4]) + '...'
print(f"🚀 启动采集: {cmd_str}")
print(f" runner: {cmd[1]}")
# 保护:通过 _execute_with_guards 但收集结果
needs_browser = True
self._cleanup_stale()
self._check_disk()
if not self._check_concurrent():
return 1
running = self._count_browsers()
stagger = running * LAUNCH_STAGGER
if stagger > 0:
print(f" ⏳ 错峰: 等待 {stagger}s")
time.sleep(min(stagger, 30))
# 执行
env = os.environ.copy()
env['MC_FORWARDED_FROM_AGENTOS'] = '1'
env['PYTHONPATH'] = f"{SCRIPTS_DIR}:{env.get('PYTHONPATH', '')}"
env['AGENT_SYNC'] = str(SCRIPTS_DIR.parent.parent)
env['AGENT_LOCAL'] = str(Path.home() / "workbuddy-agent-os" / "agent-local")
log_dir = Path(env['AGENT_LOCAL']) / "runtime" / "commands"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / f"collect_{int(time.time())}.log"
with open(log_file, 'w') as f:
p = subprocess.Popen(cmd, env=env, cwd=str(SCRIPTS_DIR),
stdout=f, stderr=subprocess.STDOUT)
print(f" PID {p.pid}")
print(f" 日志: {log_file}")
return 0
# ═══════════════════════════════════════════════
# login/logout — 带警告的 mc 委托
# ═══════════════════════════════════════════════
def cmd_login(self, rest):
"""agentos matrix login <account_id> [--phone X] — 委托 mc 执行"""
account_id = rest[0] if rest else ''
if not account_id:
print("❌ 请指定账号ID: agentos matrix login <account_id>")
return 1
print(f"\n⚠️ 即将启动浏览器登录: {account_id}")
print(f" 此操作会打开真实浏览器窗口,约 30-60 秒")
confirm = input(" 确认执行? (y/N): ").strip().lower()
if confirm != 'y':
print(" 已取消")
return 0
# 走 mc 委托(带完整执行保护)
return self._delegate_to_mc('smart-login', needs_browser=True)
def cmd_logout(self, rest):
"""agentos matrix logout <account_id> — 原生实现"""
account_id = rest[0] if rest else ''
if not account_id:
print("❌ 请指定账号ID: agentos matrix logout <account_id>")
return 1
print(f"🔓 清除 {account_id} 的登录状态...")
try:
from matrix_mgmt import MatrixManager
result = MatrixManager().unbind_account(account_id)
msg = result.get('message', '操作完成')
print(f" ✅ {msg}" if result.get("ok") else f" ⚠️ {msg}")
except Exception as e:
print(f" ❌ 失败: {e}")
return 0
# ═══════════════════════════════════════════════
# publish — 原生实现
# ═══════════════════════════════════════════════
def cmd_publish(self, rest):
"""agentos matrix publish <platform> --account X --file Y [--title T] [--desc D]"""
platform = rest[0] if rest and not rest[0].startswith('--') else ''
account = file_path = title = desc = ''
i = 1 if platform else 0
while i < len(rest):
if rest[i] == '--account' and i + 1 < len(rest): account = rest[i+1]; i += 2
elif rest[i] == '--file' and i + 1 < len(rest): file_path = rest[i+1]; i += 2
elif rest[i] == '--title' and i + 1 < len(rest): title = rest[i+1]; i += 2
elif rest[i] == '--desc' and i + 1 < len(rest): desc = rest[i+1]; i += 2
else: i += 1
if not platform or not account or not file_path:
print("❌ 参数不足\n 用法: agentos matrix publish <douyin|xiaohongshu> --account X --file Y [--title T]")
return 1
print(f"📤 发布到 {platform}: {file_path}")
cmd = [sys.executable, str(SCRIPTS_DIR / "publish_video.py"), platform,
"--account", account, "--file", file_path]
if title: cmd += ["--title", title]
if desc: cmd += ["--desc", desc]
result = subprocess.run(cmd, cwd=str(SCRIPTS_DIR),
env={**os.environ, 'PYTHONPATH': str(SCRIPTS_DIR)})
return result.returncode
# ═══════════════════════════════════════════════
# schedule — 原生实现
# ═══════════════════════════════════════════════
def cmd_schedule(self, rest):
"""agentos matrix schedule list|add|remove|history|start|stop [args...]"""
action = rest[0] if rest else 'list'
# 引入 mc.scheduler 的函数
try:
from mc.scheduler import (cmd_schedule_list, cmd_schedule_add,
cmd_schedule_remove, cmd_schedule_history)
except ImportError as e:
print(f"❌ 加载调度器失败: {e}")
return 1
if action == 'list':
cmd_schedule_list()
return 0
elif action == 'add':
if len(rest) < 2:
print("❌ 参数不足\n 用法: agentos matrix schedule add <id> --account X --blueprint Y --time HH:MM")
return 1
sid = rest[1]
# 解析额外参数
account = blueprint = time_val = ''
rounds = 3
days = '1,2,3,4,5,6,7'
extra_args = ''
i = 2
while i < len(rest):
if rest[i] == '--account' and i+1 < len(rest): account = rest[i+1]; i += 2
elif rest[i] == '--blueprint' and i+1 < len(rest): blueprint = rest[i+1]; i += 2
elif rest[i] == '--time' and i+1 < len(rest): time_val = rest[i+1]; i += 2
elif rest[i] == '--rounds' and i+1 < len(rest):
try: rounds = int(rest[i+1])
except: pass
i += 2
elif rest[i] == '--days' and i+1 < len(rest): days = rest[i+1]; i += 2
else: extra_args += f' {rest[i]}'; i += 1
if not account or not blueprint or not time_val:
print("❌ 缺少必要参数: --account --blueprint --time")
print(" 用法: agentos matrix schedule add <id> --account X --blueprint Y --time HH:MM")
return 1
# 创建 FakeArgs 以兼容 mc.scheduler 的接口
class FakeArgs:
id = sid; account = account; blueprint = blueprint
time = time_val; rounds = rounds; days = days
args = extra_args.strip()
cmd_schedule_add(FakeArgs())
return 0
elif action == 'remove':
sid = rest[1] if len(rest) > 1 else ''
if not sid:
print("❌ 请指定任务ID\n 用法: agentos matrix schedule remove <id>")
return 1
cmd_schedule_remove(sid)
return 0
elif action == 'history':
sid = rest[1] if len(rest) > 1 else ''
cmd_schedule_history(sid)
return 0
elif action == 'start':
print("🚀 启动调度器...")
import subprocess
subprocess.Popen([sys.executable, '-m', 'mc', 'schedule', 'start'],
cwd=str(SCRIPTS_DIR),
env={**os.environ, 'PYTHONPATH': str(SCRIPTS_DIR)})
return 0
elif action == 'stop':
print("⏹ 停止调度器...")
import subprocess
subprocess.run(["pkill", "-f", "mc schedule start"], capture_output=True, timeout=5)
return 0
else:
print(f"❌ 未知操作: {action}")
print(" 可用操作: list / add / remove / history / start / stop")
return 1
# ═══════════════════════════════════════════════
# run — 原生实现(养号执行,调用 nurture_runner.sh)
# ═══════════════════════════════════════════════
def cmd_run(self, rest):
"""agentos matrix run --accounts A,B --blueprints X,Y [--rounds N] [--mix]
注意:此操作会启动浏览器,约 10-30 分钟。
"""
# 解析参数
accounts = ''
blueprints = ''
rounds = 10
interval = ''
corpus = ''
engine = 'camoufox'
mix_flag = ''
daemon = False
keep = False
i = 0
while i < len(rest):
if rest[i] == '--accounts' and i+1 < len(rest): accounts = rest[i+1]; i += 2
elif rest[i] == '--blueprints' and i+1 < len(rest): blueprints = rest[i+1]; i += 2
elif rest[i] == '--rounds' and i+1 < len(rest): rounds = rest[i+1]; i += 2
elif rest[i] == '--interval' and i+1 < len(rest): interval = rest[i+1]; i += 2
elif rest[i] == '--corpus' and i+1 < len(rest): corpus = rest[i+1]; i += 2
elif rest[i] == '--engine' and i+1 < len(rest): engine = rest[i+1]; i += 2
elif rest[i] == '--mix': mix_flag = '--mix'; i += 1
elif rest[i] == '--daemon': daemon = True; i += 1
elif rest[i] == '--keep': keep = True; i += 1
elif rest[i] == '--max-browsers' and i+1 < len(rest):
try:
MAX_CONCURRENT = int(rest[i+1])
except: pass
i += 2
else: i += 1
if not accounts or not blueprints:
print("❌ 参数不足: --accounts A,B --blueprints X,Y")
return 1
print(f"\n🌱 养号执行: {accounts}")
print(f" 蓝图: {blueprints} × {rounds} 轮")
print(f" 引擎: {engine}{' 混合模式' if mix_flag else ''}")
# 执行保护
self._cleanup_stale()
self._check_disk()
if not self._check_concurrent():
return 1
# 构建 mc run 命令
cmd = [sys.executable, '-m', 'mc', 'run',
'--accounts', accounts,
'--blueprints', blueprints,
'--rounds', str(rounds)]
if interval: cmd += ['--interval', interval]
if corpus: cmd += ['--corpus', corpus]
if mix_flag: cmd += [mix_flag]
if daemon: cmd += ['--daemon']
if keep: cmd += ['--keep']
if MAX_CONCURRENT != 3: cmd += ['--max-browsers', str(MAX_CONCURRENT)]
# 后台运行
log_dir = Path.home() / "workbuddy-agent-os" / "agent-local" / "runtime" / "commands"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / f"nurture_{accounts.replace(',','_')}_{int(time.time())}.log"
print(f" 🚀 启动养号进程...")
with open(log_file, 'w') as f:
p = subprocess.Popen(cmd, env={**os.environ, 'PYTHONPATH': str(SCRIPTS_DIR)},
cwd=str(SCRIPTS_DIR), stdout=f, stderr=subprocess.STDOUT)
print(f" PID: {p.pid}")
print(f" 日志: {log_file}")
print(f" ℹ️ 浏览器自动运行中,请勿关闭此终端")
return 0
# ═══════════════════════════════════════════════
# 执行保护(前置检查 + 并发控制 + 优雅退出)
# ═══════════════════════════════════════════════
def _execute_with_guards(self, cmd_name: str) -> int:
"""带完整保护的执行入口"""
needs_browser = cmd_name in BROWSER_COMMANDS
if needs_browser:
print(f"\n🔍 预检(轻): {cmd_name}")
# 仅做 MC 不做的检查:清理残留 + 磁盘
self._cleanup_stale()
self._check_disk()
# 注意:并发/browsers/槽位/stagger/冷却等检查不在本层做
# 统一由 mc engine.py 执行时自己控制
return self._delegate_to_mc(cmd_name, needs_browser)
def _cleanup_stale(self):
"""复位:杀残留 + 清理临时文件(对标 preflight.py)"""
killed = 0
for pat in ["camoufox.*orphan", "playwright"]:
try:
r = subprocess.run(["pkill", "-f", pat], capture_output=True, timeout=5)
if r.returncode == 0: killed += 1
except: pass
# 清理临时目录
for pat in ["/tmp/camoufox_*", "/tmp/playwright_*"]:
for fp in glob.glob(pat):
try: shutil.rmtree(fp, ignore_errors=True)
except: pass
if killed:
print(f" 🧹 已清理 {killed} 个残留进程")
def _check_disk(self):
"""检查磁盘空间(对标 preflight.py check_disk_space)"""
try:
r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
parts = r.stdout.strip().split("\n")[1].split()
avail = parts[3] if len(parts) >= 4 else "0"
avail_gb = 0
if avail.endswith("G"): avail_gb = float(avail[:-1])
elif avail.endswith("M"): avail_gb = float(avail[:-1]) / 1024
elif avail.endswith("T"): avail_gb = float(avail[:-1]) * 1024
if avail_gb < MIN_DISK_GB:
print(f" ⚠️ 磁盘空间不足: {avail} ({avail_gb:.1f}GB < {MIN_DISK_GB}GB)")
except: pass
def _count_browsers(self) -> int:
"""统计当前运行浏览器数量(对标 browser_orchestrator.check_running_browsers)"""
try:
r = subprocess.run(
["pgrep", "-f", "camoufox.*-no-remote|HeadlessShell|chrome.*--remote-debugging"],
capture_output=True, text=True, timeout=5
)
return len([p for p in r.stdout.strip().split("\n") if p.strip()])
except: return 0
def _check_concurrent(self) -> bool:
"""并发检查(统一使用 mc.execution_policy.preflight)"""
pf = preflight()
print(f" 📊 状态: 浏览器 {pf['browsers']}/{MAX_CONCURRENT}"
f" · 磁盘 {pf['disk_gb']}GB/{MIN_DISK_GB}GB"
f" · 延迟 {pf['stagger_delay']}s")
if not pf["ok"]:
print(f" ❌ {pf['message']}")
return False
return True
# ═══════════════════════════════════════════════
# mc 委托执行
# ═══════════════════════════════════════════════
def _get_rest_args(self, cmd_name: str) -> list:
"""从 sys.argv 提取 cmd_name 后面的参数"""
try:
idx = sys.argv.index(cmd_name)
return sys.argv[idx + 1:]
except ValueError:
return []
def _delegate_to_mc(self, cmd_name: str, needs_browser: bool = False) -> int:
"""委托 mc 执行"""
python = sys.executable
# 重建 mc 命令: 从当前 args 动态构建
# raw args = ['agentos', 'matrix', 'run', '--accounts', 'X']
# → mc args = ['python', '-m', 'mc', 'run', '--accounts', 'X']
mc_argv = [python, '-m', 'mc', cmd_name] + self._get_rest_args(cmd_name)
env = os.environ.copy()
env['MC_FORWARDED_FROM_AGENTOS'] = '1'
env['PYTHONPATH'] = f"{SCRIPTS_DIR}:{env.get('PYTHONPATH', '')}"
env['AGENT_SYNC'] = str(SCRIPTS_DIR.parent.parent)
env['AGENT_LOCAL'] = str(Path.home() / "workbuddy-agent-os" / "agent-local")
# 浏览器操作:Popen 后台运行 + 跟踪 PID
if needs_browser:
log_dir = Path(env['AGENT_LOCAL']) / "runtime" / "commands"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / f"{cmd_name}_{int(time.time())}.log"
with open(log_file, 'w') as f:
p = subprocess.Popen(
mc_argv, env=env, cwd=str(SCRIPTS_DIR),
stdout=f, stderr=subprocess.STDOUT
)
print(f" 🚀 PID {p.pid} | 日志: {log_file}")
print(f" 📋 并发: {self._count_browsers()}/{MAX_CONCURRENT}")
print(f" ⏱️ 超时: {AUTO_SHUTDOWN_MIN}min 自动关闭")
print(f" ℹ️ 检查: ps aux | grep {p.pid}")
return 0
# 非浏览器操作:同步等待
result = subprocess.run(mc_argv, env=env, cwd=str(SCRIPTS_DIR))
return result.returncode
@@ -0,0 +1,162 @@
"""
agentos serve — 服务管理(MCP/Dashboard/调度)
功能:
mcp 启动 MCP Server
dashboard 启动/停止 Dashboard
schedule 全局定时调度器
"""
import argparse
import subprocess
import sys
from pathlib import Path
from agentos.base import AgentOSPlugin
AGENT_SYNC = Path(__file__).resolve().parent.parent.parent.parent.parent.parent # x6 → agent-sync/
class ServePlugin(AgentOSPlugin):
name = "serve"
description = "服务管理 — MCP/Dashboard/调度"
nav = {
'group': '系统设置',
'icon': '⚙️',
'order': 5,
'items': [
{'view': 'kb', 'label': '知识库'},
]
}
def register(self, subparsers):
p = subparsers.add_parser("serve", help=self.description)
p_sub = p.add_subparsers(dest="serve_action", help="服务操作")
# dashboard
pd = p_sub.add_parser("dashboard", help="启动/管理 Dashboard")
pd.add_argument("action", nargs="?", default="start",
choices=["start", "stop", "restart", "status"],
help="操作")
pd.add_argument("--port", type=int, default=9988, help="端口")
pd.set_defaults(serve_func=self.cmd_dashboard)
# mcp
pm = p_sub.add_parser("mcp", help="启动 MCP Server")
pm.set_defaults(serve_func=self.cmd_mcp)
# schedule
psc = p_sub.add_parser("schedule", help="定时任务管理")
psc.add_argument("action", nargs="?", default="list",
choices=["list", "add", "remove"],
help="操作")
psc.set_defaults(serve_func=self.cmd_schedule)
# guardd
pg = p_sub.add_parser("guardd", help="系统自愈守护进程")
pg.add_argument("action", nargs="?", default="start",
choices=["start", "stop", "status"],
help="操作")
pg.add_argument("--daemon", action="store_true", help="后台运行")
pg.set_defaults(serve_func=self.cmd_guardd)
return p
def dispatch(self, args):
if hasattr(args, 'serve_func'):
return args.serve_func(args)
print("未知服务命令,使用 agentos serve --help")
return 1
def cmd_dashboard(self, args):
dashboard_dir = AGENT_SYNC / "05_tools" / "10_dashboard"
run_py = dashboard_dir / "run.py"
if not run_py.exists():
print(f"Dashboard 不存在: {run_py}")
return 1
if args.action == "start":
pid_file = dashboard_dir / ".dashboard.pid"
if pid_file.exists():
print("Dashboard 已在运行")
return 0
subprocess.Popen(
[sys.executable, str(run_py), str(args.port)],
cwd=str(dashboard_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
print(f"Dashboard 已启动 (port {args.port})")
elif args.action == "stop":
import subprocess
subprocess.run(["pkill", "-f", f"run.py {args.port}"])
print(f"Dashboard (port {args.port}) 已停止")
elif args.action == "status":
import subprocess
r = subprocess.run(
["curl", "-s", "--connect-timeout", "2",
f"http://localhost:{args.port}/api/identity"],
capture_output=True, text=True)
if r.returncode == 0 and r.stdout:
print(f"Dashboard 运行中 (port {args.port})")
else:
print(f"Dashboard 未运行 (port {args.port})")
return 0
def cmd_mcp(self, args):
print("MCP Server 启动中...")
print("(功能开发中,敬请期待)")
return 0
def cmd_schedule(self, args):
print(f"定时任务 {args.action}")
print("(功能开发中,敬请期待)")
return 0
def cmd_guardd(self, args):
"""系统自愈守护进程"""
import subprocess, os
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent
guardd_script = str(SCRIPTS_DIR / "guardd.py")
if args.action == "start":
if args.daemon:
log_file = os.path.expanduser(
"~/workbuddy-agent-os/agent-local/runtime/guardd/daemon.log")
os.makedirs(os.path.dirname(log_file), exist_ok=True)
pid = subprocess.Popen(
[sys.executable, guardd_script],
stdout=open(log_file, 'w'), stderr=subprocess.STDOUT,
).pid
print(f"🛡️ guardd 已后台启动 (PID: {pid})")
print(f" 日志: {log_file}")
else:
os.execvp(sys.executable, [sys.executable, guardd_script])
elif args.action == "stop":
try:
subprocess.run(["pkill", "-f", "guardd.py"], capture_output=True, timeout=5)
print("⏹ guardd 已停止")
except Exception as e:
print(f"❌ 停止失败: {e}")
elif args.action == "status":
sf = os.path.expanduser(
"~/workbuddy-agent-os/agent-local/runtime/guardd/status.json")
if os.path.exists(sf):
import json
try:
d = json.loads(open(sf).read())
print(f"🛡️ guardd 状态")
print(f" 最后检查: {d.get('last_check','?')}")
print(f" 孤儿进程: {d.get('orphans',0)}")
print(f" 磁盘: {d.get('disk_gb','?')}GB")
print(f" 浏览器: {d.get('browsers',0)}")
if d.get('events'):
print(f" 事件: {'; '.join(d['events'])}")
except:
print("🛡️ guardd 状态文件损坏")
else:
print("🛡️ guardd 未运行")
return 0