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) **
**──────────────────────────────────────────────────────**
数据流:
- Agent 建立 SSH 隧道到 Solo 机器
- CDP 客户端通过 WebSocket 连接 Solo 的 Electron 进程
- 用 CDP 命令模拟键盘输入(
Input.insertText)
- 用 CDP 命令模拟点击(按钮 .click())或键盘事件(Enter)
- 用 DOM 查询读取 Solo 的回复内容
- 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 参数。

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 连接会立即失效。用 autossh 或 systemd 服务保证隧道稳定性。
八、扩展思路
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 这条路就永远走得通。