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,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
|
||||
Reference in New Issue
Block a user