Files
MediaCrawler/mac-agent-os-main/05_tools/07_matrix/scripts/agentos/plugins/matrix.py
T
butubb 2112c1a870
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
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 带网关原话 / 正常返回)。
2026-10-10 17:12:25 +08:00

600 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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