收起左侧

飞牛openclaw跨设备局域网操控trae solo方案

1
回复
55
查看
[ 复制链接 ]

1

主题

1

回帖

0

牛值

江湖小虾

Trae Solo CN 远程操控方案 v2:从零搭建 AI 编程助手遥控通道

作者:赵小果
日期:2026-06-02
适用场景:通过 CDP(Chrome DevTools Protocol)远程操控 Trae Solo CN,让服务端 Agent 能调度 Solo 的 AI 能力


一、为什么需要这套方案?

Trae Solo CN 是字节跳动推出的 AI 编程助手,它有两种模式

模式 用途 适合谁
Work 模式 快速问答、分析、聊天式任务 日常使用
Code 模式 写代码、开发项目、全栈编程 开发场景

但 Solo 是个桌面程序——它的交互全在界面上,没有正式 API。要让自动化系统(比如 OpenClaw Agent、服务端脚本)调用 Solo,就得绕过它没有 API 这个限制。

方案本质: 通过 Chrome DevTools Protocol 模拟人类操作 Solo 界面——输入文字、点击按钮、读取回复。就像有一个看不见的手在操控 Solo。

不是什么新鲜方案: CDP 是 Chrome/Chromium 家族的标准调试协议。Electron 应用(Solo 基于 Electron)天生支持 CDP。


二、架构总览

**─────────────────────────────────────────────────────**
**             自动化平台(OpenClaw Agent)              **
**                你本机的 AI 服务端                     **
**                172.16.x.x / 飞牛 NAS                  **
**──────────────────**──────────────────────────────────**
                   ** SSH 端口转发
                   ** ssh -L 9222:127.0.0.1:9222
                   **
**──────────────────▼──────────────────────────────────**
**              Solo CN(Trae Solo)                    **
**                用户本地电脑 / 开发机                   **
**                192.168.x.x:9222(CDP 调试端口)       **
**                                                      **
**  Electron 进程 ── WebSocket ── 操控客户端              **
**      ↓                                                **
**  solo-lite.html(界面渲染)                            **
**      **── 输入框(contenteditable)                     **
**      **── 发送按钮                                      **
**      **── 回复区域(core-finish-card)                   **
**──────────────────────────────────────────────────────**

数据流:

  1. Agent 建立 SSH 隧道到 Solo 机器
  2. CDP 客户端通过 WebSocket 连接 Solo 的 Electron 进程
  3. 用 CDP 命令模拟键盘输入(Input.insertText
  4. 用 CDP 命令模拟点击(按钮 .click())或键盘事件(Enter)
  5. 用 DOM 查询读取 Solo 的回复内容
  6. Agent 拿到结果后返回给用户

核心原则: Solo 不用做任何改造,就像有人坐在你电脑前帮你操作。


三、原理:为什么用 CDP?

3.1 CDP 是什么?

Chrome DevTools Protocol(CDP)是 Chrome/Chromium 家族的标准调试协议。所有基于 Electron 的应用都内置支持 CDP。通过 WebSocket 连接后,可以发送 JSON 格式的命令,控制浏览器的一切

最核心的能力:

命令 作用
Runtime.evaluate 在页面中执行任意 JavaScript
DOM.enable / DOM.querySelector 读取和操作 DOM 节点
Input.insertText 模拟真实键盘输入(关键)
Input.dispatchKeyEvent 模拟键盘按键事件
Runtime.enable 启用执行环境

3.2 为什么不用 HTTP、WebAPI、Selenium?

方案 问题
HTTP API Solo 没有提供任何 HTTP API
Selenium/Playwright 太重,需要额外的驱动和浏览器配置
OCR + 键鼠模拟 不确定性高,出错概率大
CDP Electron 原生支持,轻量、可靠、可编程

3.3 为什么 v1 方案会失效?

Solo CN 的界面使用了 WebComponents + Lexical 编辑器渲染。v1 方案用 innerText 往输入框写内容,在普通网页上没问题,但在 WebComponents 的自定义 shadow DOM 里,innerText 赋值后读出来还是空的。

v2 关键修复: 改用了 CDP 的 Input.insertText 命令。这个命令模拟的是操作系统级别的键盘输入——绕过所有前端框架的抽象层,直接在内核层面注入文本。WebComponents、Lexical、Vue、React,统统不管用,只要浏览器认识键盘事件就行。

3.4 状态检测机制

任务完成检测是另一大改进。v1 靠不稳定的 CSS 选择器判断 Solo 是否完成回复。v2 改用 core-finish-card 节点检测

Solo 回复过程的 DOM 状态机:

初始状态 → 发送消息(输入框清空)
    ↓
Loading 状态(typing-indicator 出现)
    ↓
任务处理中(Solo 正在调用云端模型)
    ↓
任务完成(core-finish-card DOM 节点出现)
    ↓
文本稳定(连续两次检测内容不再变化)
    ↓
结果提取(读取 SOLO Work ~ SOLO Auto Model 之间的文本)

检测到 core-finish-card 后不会立即提取,而是连续检测两次确认内容稳定——避免 Solo 边生成边刷新导致只抓到半截内容。


四、布置方法(完整步骤)

4.1 前置准备

Solo 端(用户电脑):

Solo 必须以调试模式启动,带上 --remote-debugging-port 参数。
image.png

Windows 快捷方式改造:

目标(T): "C:\Program Files\Trae Solo\solo.exe" --remote-debugging-port=9222

这样 Solo 启动后会同时开启一个 WebSocket 调试服务监听 9222 端口。

自动化端(服务端):

准备一个 Node.js 环境(v18+),安装 ws 包:

npm init -y
npm install ws

如果需要从另一台机器操控,需要 SSH 端口转发:

ssh -L 9222:127.0.0.1:9222 -f -N user@solo-machine-ip

验证 CDP 是否就绪:

curl http://127.0.0.1:9222/json/list
# 应该返回类似:
# [{"id":"...","title":"Trae Solo...","url":"...","webSocketDebuggerUrl":"ws://127.0.0.1:9222/devtools/page/..."}]

4.2 核心代码

CDP 客户端类(核心):

const WebSocket = require('ws');
const http = require('http');

class CDPClient {
  constructor(host = '127.0.0.1', port = 9222) {
    this.host = host;
    this.port = port;
    this.ws = null;
    this.mid = 1;
    this.pending = new Map();
  }

  // 动态获取最新的 Solo 页面 WebSocket URL
  async getWSURL() {
    return new Promise((resolve, reject) => {
      http.get(`http://${this.host}:${this.port}/json/list`, (res) => {
        let data = '';
        res.on('data', c => data += c);
        res.on('end', () => {
          const pages = JSON.parse(data);
          if (!pages.length) return reject(new Error('No pages'));
          // 动态过滤 Solo 页面(按标题匹配)
          const solo = pages.find(p =>
            p.title && (p.title.includes('SOLO') || p.title.includes('Solo'))
          ) || pages[pages.length - 1];
          resolve(solo.webSocketDebuggerUrl);
        });
      });
    });
  }

  // 建立 CDP WebSocket 连接
  async connect() {
    const url = await this.getWSURL();
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(url);
      this.ws.on('open', async () => {
        // 启用必要的能力
        await this.cmd('Runtime.enable');
        await this.cmd('DOM.enable');
        resolve();
      });
      this.ws.on('message', (data) => {
        const msg = JSON.parse(data);
        if (msg.id && this.pending.has(msg.id)) {
          const { resolve, reject, timer } = this.pending.get(msg.id);
          clearTimeout(timer);
          msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
          this.pending.delete(msg.id);
        }
      });
    });
  }

  // 发送 CDP 命令
  cmd(method, params = {}) {
    return new Promise((resolve, reject) => {
      const id = ++this.mid;
      const timer = setTimeout(() => reject(new Error(`${method} timeout`)), 30000);
      this.pending.set(id, { resolve, reject, timer });
      this.ws.send(JSON.stringify({ id, method, params }));
    });
  }

  // 执行 JavaScript 并获取返回值
  async eval(expr) {
    const r = await this.cmd('Runtime.evaluate', {
      expression: expr,
      returnByValue: true
    });
    if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception.description);
    return r.result.value;
  }
}

发送任务到 Solo:

// 1. 连接
const client = new CDPClient();
await client.connect();

// 2. 切换模式(可选,默认 Work)
// 点击胶囊按钮切换
await client.eval(`document.querySelector('.index-module__capsule___Krh_h')?.click()`);

// 3. 创建新任务
await client.eval(`document.querySelector('.task-list-new-task-item')?.click()`);

// 4. 聚焦输入框
await client.eval(`document.querySelector('[contenteditable="true"]')?.focus()`);

// 5. 用 Input.insertText 注入文本(关键!不要用 innerText)
const text = "用中文介绍一下你自己";
await client.cmd('Input.insertText', { text });

// 6. 点击发送按钮
const btnState = await client.eval(`(function(){
  const btn = document.querySelector('button.chat-input-v2-send-button');
  if (btn && !btn.disabled) { btn.click(); return 'clicked'; }
  return btn ? 'disabled' : 'not_found';
})()`);

// 如果按钮不可点,回退到回车发送
if (btnState !== 'clicked') {
  await client.cmd('Input.dispatchKeyEvent', {
    type: 'rawKeyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13
  });
}

// 7. 等待回复完成(轮询检测 core-finish-card)
while (true) {
  const hasFinishCard = await client.eval(`!!document.querySelector('.core-finish-card')`);
  if (hasFinishCard) break;
  await new Promise(r => setTimeout(r, 1500));
}

// 8. 提取回复内容
const content = await client.eval(`(function(){
  const root = document.getElementById('solo-lite-root');
  const text = root ? root.innerText : '';
  // 找到 SOLO Work/Code 标签之间的核心回复
  const soloIdx = text.indexOf('SOLO Work');
  const soloCodeIdx = text.indexOf('SOLO Code');
  const startIdx = Math.min(
    soloIdx >= 0 ? soloIdx : Infinity,
    soloCodeIdx >= 0 ? soloCodeIdx : Infinity
  );
  const endIdx = text.indexOf('SOLO Auto Model');
  return text.substring(startIdx, endIdx > startIdx ? endIdx : startIdx + 2000);
})()`);

console.log(content);
client.close();

4.3 环境检查脚本

#!/bin/bash
# check-solo-ready.sh
echo "=== Solo CDP 环境检查 ==="

# 1. 检查 CDP 端口
curl -s --max-time 3 http://127.0.0.1:9222/json/list > /dev/null
if [ $? -eq 0 ]; then
  echo "✅ CDP 9222 端口可达"
  echo "   页面数: $(curl -s http://127.0.0.1:9222/json/list | grep -o '"id"' | wc -l)"
  curl -s http://127.0.0.1:9222/json/list | grep -o '"title":"[^"]*"' | head -3
else
  echo "❌ CDP 9222 端口不可达"
  echo "   → Solo 是否以 --remote-debugging-port=9222 启动?"
  echo "   → SSH 端口转发是否存活?"
fi

# 2. 检查 Node.js 环境
echo ""
echo "Node.js: $(node --version 2>/dev/null || echo '❌ 未安装')"
echo "ws 包: $(node -e \"try{require('ws');console.log('OK')}catch(e){console.log('❌ 未安装')}\")"

# 3. 尝试连接
echo ""
echo "尝试建立 CDP 连接..."
node -e "
const { CDPClient } = require('./solo.js');
const c = new CDPClient();
c.connect().then(() => {
  console.log('✅ CDP 连接成功');
  c.close();
}).catch(e => {
  console.log('❌ CDP 连接失败:', e.message);
  process.exit(1);
});
"

五、关键代码详解

5.1 输入文本——为什么不用 innerText?

// ❌ 错误(v1 方案,webcomponents 下失效)
input.innerText = "Hello";
input.dispatchEvent(new Event('input', { bubbles: true }));

// ✅ 正确(CDP Input.insertText)
await client.cmd('Input.insertText', { text: "Hello" });

Input.insertText 是 CDP 提供的操作系统级键盘输入模拟。它不是往 DOM 写字符串,而是模拟"人打字"的过程——每个字符触发 keyboard 事件、composition 事件、input 事件。对于使用了自定义编辑器(Lexical、Quill、Monaco)的应用,这是唯一可靠的方式。

5.2 发送消息——为什么需要双通道?

// 方案 A:点击发送按钮(优先)
const btn = document.querySelector('button.chat-input-v2-send-button');
if (btn && !btn.disabled) { btn.click(); return 'clicked'; }

// 方案 B:回车键发送(回退)
// 适用场景:按钮被隐藏、按钮位置变化、按钮被 disable
await client.cmd('Input.dispatchKeyEvent', {
  type: 'rawKeyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13
});
await client.cmd('Input.dispatchKeyEvent', {
  type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13
});

两种方式互备——按钮能点就点,按钮被隐藏或 disable 就回车,保证至少有一条路能发出去。

5.3 动态 page ID——为什么不能硬编码?

// ❌ 错误:硬编码 page ID
const wsUrl = 'ws://127.0.0.1:9222/devtools/page/211DC9E1...';
const ws = new WebSocket(wsUrl);

// ✅ 正确:从 /json/list 动态获取
const soloPage = pages.find(p => p.title.includes('Solo'));
const wsUrl = soloPage.webSocketDebuggerUrl;

每次 Solo 刷新、切换任务、超时重连,CDP 都会分配新的 Page ID。硬编码一次成功,下次一定失败。

5.4 完成检测——core-finish-card 节点

async function waitForCompletion(client, maxWaitMs = 300000) {
  let stableCount = 0;
  let lastText = null;

  while (Date.now() - start < maxWaitMs) {
    // 检测完成卡片 DOM 节点
    const hasCard = await client.eval(`!!document.querySelector('.core-finish-card')`);
  
    // 第一层:卡片出现了
    if (hasCard) {
      // 第二层:卡片文本稳定(连续两次检测一致)
      const currentText = await client.eval(`document.querySelector('.core-finish-card')?.innerText?.substring(0,500)`);
    
      if (lastText && currentText === lastText && stableCount >= 1) {
        break;  // 文本稳定,确认完成
      }
      stableCount++;
      lastText = currentText;
    }
  
    await sleep(1500);
  }
}

两层检测机制:先确认 DOM 节点出现(任务有结果了),再确认文本不再变化(SLM 已经全部生成完)。


六、完整 CLI 工具

最终打包成一个 solo.js 可执行文件,提供以下命令:

node solo.js status                 # 检查 Solo 是否在线
node solo.js task "帮我写个脚本"     # 发任务(Work 模式)
node solo.js task-code "开发项目"    # 发任务(Code 模式)
node solo.js mode work|code          # 切换模式
node solo.js chat "继续说"           # 在当前会话继续提问
node solo.js attach /path/to/file    # 引用本地文件上下文
node solo.js history                  # 列出所有历史会话
node solo.js history-open "关键词"    # 搜索并打开历史会话
node solo.js mcp status               # 检查 MCP Bridge 状态

Code 模式 vs Work 模式的关键区别:

方面 Work 模式 Code 模式
需要选文件夹 ❌ 不需要 ✅ 需要(在 Solo UI 上手动选)
任务模板 4 个(应用/理解/游戏/工具)
对话分组 所有 Work 任务在一个组 每个文件夹创建一个任务组
适用场景 问答、分析 编程、开发
自动化程度 完全可自动化 需要一次手动选目录

七、注意事项与常见坑

⚠️ 最大坑:历史会话跨模式不可见

这是 Solo 设计的预期行为——任务列表按模式分组。Work 模式下只能看到 Work 模式的任务,Code 模式下同理。

解决方案: 查找历史会话时,当前模式搜不到就自动切到另一模式再搜。

// 伪代码
let result = searchInCurrentMode(query);
if (!result) {
  switchToOtherMode();
  result = searchInOtherMode(query);
  if (!found) switchBack(); // 切回原模式
}

⚠️ Code 模式必须先选文件夹

Solo 左下角提示"请选择一个项目"时,发送按钮是灰色的。选好文件夹后才能发消息。这一步无法自动化,需要人类在 Solo UI 上操作一次。

选好之后,所有 Code 模式的任务会自动使用这个工作目录。除非 Solo 重启,否则不需要再选。

⚠️ Solo 同一时间只能处理一个任务

Solo 是单线程任务队列模式。发送新任务时,如果 Solo 还在处理上一个任务,新消息会被排队。

建议: 每次发任务前先检查是否有完成卡片;如果有未完成的任务残留,手动创建一个新任务触发 Solo 重置状态。

⚠️ 避免硬编码

要硬编码的 不要硬编码的
CDP 端口号(9222) Page ID(必须动态获取)
DOM 选择器(但要注意更新) CSS class 名称(可能随版本变化)
SSH 隧道地址 Solo 进程 PID

⚠️ DOM 选择器可能过时

Solo 是持续更新产品,界面元素的选择器可能随版本变化。如果某个选择器失效,需要通过浏览器 DevTools 手动检查当前版本的实际 DOM 结构。

维护建议: 每次 Solo 更新后,运行测试套件验证所有功能正常。

⚠️ 网络和 SSH 隧道

CDP 端口只在 localhost 监听,从其他机器访问需要 SSH 隧道:

# 保持隧道存活(自动重连)
autossh -M 0 -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" \
  -L 9222:127.0.0.1:9222 -N user@solo-machine-ip

如果 SSH 断线,CDP 连接会立即失效。用 autosshsystemd 服务保证隧道稳定性。


八、扩展思路

MCP 协议:双向通道

TRAE(包括 Solo)支持 MCP(Model Context Protocol)。这意味着:

  • CDP 是外向通道:Agent → Solo(发任务、读回复)
  • MCP 是内向通道:Solo → 外部工具(读文件、查数据库、调用 API)

如果 Solo 在执行任务时需要访问本地文件、数据库、知识库,可以通过 MCP Server 暴露给 Solo。这样 Solo 的 AI 智能体在执行任务时能自主调用外部工具。

多实例调度

如果 Solo 任务量很大,可以部署多个 Solo 实例(不同的端口),用负载均衡器分派任务给不同实例。但 Solo 的资源消耗较高(云端模型调用),需要评估实际需求。

自动化编排

可以配合 cron 或任务队列做定时任务编排:

每天早上 9:00 → Solo 执行日报分析
每天中午 12:00 → Solo 检查代码库更新
触发 PR → Solo 自动代码**

附录:完整源码

完整 solo.js (~400行) 和 SKILL.md 可在 GitHub 仓库获取(待上传)。

核心依赖: Node.js 18+、ws 包(WebSocket)

文件结构:

skills/trae-solo/
**── solo.js          # 主入口,CDP 客户端 + CLI
**── SKILL.md         # OpenClaw Skill 定义
**── README.md        # 本文档

总结:CDP 远程操控 Solo 的精髓就是「假装是人在操作」。
不用内嵌 API、不用魔改 Solo、不用依赖 Solo 特定版本。
只要 Solo 是 Electron 应用,CDP 这条路就永远走得通。

收藏
送赞 1
分享

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

1

主题

1

回帖

0

牛值

江湖小虾

3 小时前 楼主 显示全部楼层
以上为飞牛openclaw整理分享。题主为技术脑、残。感兴趣的小伙伴,可发给自家龙虾参考、整理,修改。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则