让 UI-TARS 跑在 Web 上:UI-TARS-desktop 仓库 Open Operator 示例,基于 Browserbase 与 Stagehand 构建 Web GUI Agent
本文围绕 UI-TARS-desktop 仓库中的 Open Operator 示例 展开。该示例是一个 Next.js 全栈应用,把 @ui-tars/sdk 中的 GUIAgent 与 @ui-tars/operator-browserbase(Browserbase + Stagehand)组合起来,实现"用户输入自然语言目标 → 云端浏览器实时操作 → SSE 流式回传每一步动作与推理"的完整链路。读完本文,你将掌握该示例的本地运行与配置方法、/api/session 与 /api/agent 两个服务端接口的实现原理,以及 BrowserbaseOperator 的动作空间与截图/执行机制。
项目定位:一个明确的 PoC 示例
该目录的 README 开宗明义:它是 fork 自 Browserbase 的 open-operator 项目,并且"this is simply a proof of concept(这只是一个概念验证)"。上游定位是"不与 Web Agent 竞争,而是提供构建 Web Agent 所需的全部工具",本 fork 的差异在于把模型层换成了 UI-TARS 系列服务——README 明确要求读者从 UI-TARS 项目获取 UI_TARS_BASE_URL、UI_TARS_API_KEY、UI_TARS_MODEL 三项配置,而不是只配置一个通用 LLM 的 key。
从 依赖清单 看,该示例与主仓库 monorepo 的衔接点非常清晰:
@ui-tars/sdk与@ui-tars/operator-browserbase,版本均为^1.2.0-beta.12,即仓库 packages/ui-tars/sdk 与 packages/ui-tars/operators/browserbase 发布的 npm 包;@browserbasehq/sdk(^2.0.0),用于服务端直接创建/释放 Browserbase 会话;next(15.1.9)+react(^19.0.0)+framer-motion+jotai+zod,构成前端框架与状态管理。
@ui-tars/operator-browserbase 包自身的 README 也指向本示例作为用法参考,两者互为印证。
五分钟跑起来:安装、环境变量与启动
安装依赖(必须使用 pnpm)
README 中特别注明"works with pnpm only,NPM 不可用,yarn 未测试"。因此克隆仓库后进入该目录执行:
pnpm install
准备环境变量
README 给出的步骤是先复制示例环境文件(cp .env.example .env.local),再把以下 5 个变量写入 .env.local。这些变量的实际消费点都在服务端路由代码里,逐一对应关系如下:
| 变量 | 用途 | 在源码中的消费位置 |
|---|---|---|
UI_TARS_BASE_URL |
UI-TARS 模型服务的 Base URL | agent 路由 中传入 GUIAgent 的 model.baseURL |
UI_TARS_API_KEY |
UI-TARS 模型服务的 API Key | 同上,model.apiKey |
UI_TARS_MODEL |
使用的 UI-TARS 模型名 | 同上,model.model |
BROWSERBASE_API_KEY |
Browserbase API Key | session 路由 中构造 Browserbase SDK 客户端 |
BROWSERBASE_PROJECT_ID |
Browserbase 项目 ID | 同上,创建 context 与 session 时必填 |
启动开发服务器
pnpm dev
对应 package.json 中的 next dev --turbopack。启动后打开 http://localhost:3000 即可看到首页:一个输入框加"Run"按钮,以及"Who is the top contributor to Stagehand?"等几个预设示例任务(见 首页组件)。
端到端架构:前端、两个 API 路由与 SSE 流
整体数据流可以从 ChatFeed 组件 的 initializeSession 逻辑完整还原,顺序如下:
- 创建云端浏览器会话:前端以
POST /api/session提交{ timezone, contextId },其中 timezone 取自浏览器的Intl.DateTimeFormat().resolvedOptions().timeZone。后端返回{ sessionId, sessionUrl, contextId }。 - 内嵌实时画面:前端把
sessionUrl做了一次字符串替换(把 devtools-fullscreen 的 inspector 页面替换为devtools-internal-compiled/index.html),再放进<iframe sandbox="allow-same-origin allow-scripts allow-forms">中,实现"边操作边观看"的直播效果。 - 发起 Agent 任务:
POST /api/agent,请求体为{ goal, sessionId, action: 'START' },请求头声明Accept: text/event-stream。 - 消费 SSE 流:前端用 XStream 工具(改编自 Ant Design X 的 x-stream,按
\n\n分段解析data:前缀的 SSE 帧)逐帧取出steps,渲染为"Step N + 动作文本 + Reasoning"卡片;当某步tool === 'CLOSE'或done === true时停止消费,并通过DELETE /api/session释放浏览器会话。
会话路由:时区选区、Context 持久化与调试画面
session 路由 是整个示例中最"工程化"的一段,值得逐点拆解:
- 时区 → 区域映射:
getClosestRegion(timezone)三级降级——先精确匹配(如America/New_York→us-east-1),再按前缀映射(America/US/Canada→us-west-2、Europe/Africa→eu-central-1、Asia/Australia/Pacific→ap-southeast-1),最后用Intl.DateTimeFormat算出 UTC 偏移量,落在-24~-4 / -3~4 / 5~24三个区间分别对应美西/欧洲/新加坡区域;无法解析时默认us-west-2。目的是让云端浏览器尽量靠近用户所在地,降低直播延迟。 - Context 复用:
createSession支持传入已有contextId。传入时直接{ context: { id, persist: true } };否则调用bb.contexts.create({ projectId })新建一个持久化 context 再挂载。这使浏览器的登录态、Cookie 可以在多次会话间延续。 - 会话参数:
bb.sessions.create时显式设置keepAlive: true与region,保证 Agent 执行期间会话不被回收。 - 调试 URL:
getDebugUrl调用bb.sessions.debug(sessionId)拿到debuggerFullscreenUrl,这就是前端 iframe 里看到的实时画面来源。 - 释放:
DELETE通过bb.sessions.update(sessionId, { status: 'REQUEST_RELEASE' })释放会话。
前端侧还有一个细节:atoms.ts 用 jotai 的 atomWithStorage('contextId', '') 把 contextId 持久化到浏览器本地存储,刷新页面后仍能复用同一个 Browserbase context,避免每次都从零开始。
Agent 路由:GUIAgent + BrowserbaseOperator + SSE
agent 路由 只有百余行,却包含了完整的 Agent 装配逻辑:
1. 系统提示词由动作空间自动拼装
SYSTEM_PROMPT 要求模型输出 Thought: ... / Action: ... 两段式格式,其中动作空间与示例都来自 BrowserbaseOperator.MANUAL 静态属性,而非手写:
## Action Space
${BrowserbaseOperator.MANUAL.ACTION_SPACES.join('\n')}
## Note
- The first step should be to GOTO a specific website
- Write a small plan and finally summarize your next action (with its target element)
in one sentence in `Thought` part.
## Example
${BrowserbaseOperator.MANUAL.EXAMPLES.join('\n')}
## User Instruction
这个设计保证了"提示词里承诺的动作空间"与"Operator 实际能执行的动作"永远一致。
2. 装配 GUIAgent
const operator = new BrowserbaseOperator({ env: 'LOCAL' });
const guiAgent = new GUIAgent({
systemPrompt: SYSTEM_PROMPT,
model: {
baseURL: process.env.UI_TARS_BASE_URL,
apiKey: process.env.UI_TARS_API_KEY,
model: process.env.UI_TARS_MODEL!,
},
operator,
onData: async ({ data }) => { /* 解析并写入 SSE 流 */ },
onError: ({ error }) => { /* 写 error 帧 */ },
});
guiAgent.run(goal);
其中 GUIAgent 是 monorepo 中 SDK 的核心类:构造时若 config.model 不是 UITarsModel 实例会自动包一层,systemPrompt 缺省时用内置模板构建;run(instruction) 启动"截图 → 模型推理 → 解析动作 → Operator 执行"的循环,循环上限由 maxLoopCount 控制,每轮通过 onData 回调抛出 GUIAgentData(含 status、conversations 等字段)。
3. 把 Agent 回调翻译成 SSE 帧
onData 中取 data.conversations 的最后一条 lastConversation,若存在 predictionParsed 则映射为前端可渲染的 step:
const steps = lastConversation?.predictionParsed?.map((p) => ({
text: `${p.action_type}: ${JSON.stringify(p.action_inputs)}`,
reasoning: p.thought,
tool: p.action_type,
instruction: p.action_inputs?.content,
}));
const nextData = {
success: true,
...(lastConversation?.from === 'gpt' && lastConversation?.value && {
reasoning: lastConversation.value,
}),
...(steps.length > 0 && { steps, result: steps[0] }),
done: [StatusEnum.END, StatusEnum.MAX_LOOP].includes(data.status),
};
await writer.write(encoder.encode(`data: ${JSON.stringify(nextData)}\n\n`));
注意结束判定:状态到达 StatusEnum.END(任务完成)或 StatusEnum.MAX_LOOP(达到最大循环数)时 done 为 true;仅在 END 时关闭写端。响应头声明 Content-Type: text/event-stream 与 Cache-Control: no-cache, no-transform,配合 vercel.json 中把 app/api/**/* 的 maxDuration 提到 300 秒——长耗时 Agent 循环不会提前被函数超时掐断。
BrowserbaseOperator:动作空间与执行细节
@ui-tars/operator-browserbase 的源码在 packages/ui-tars/operators/browserbase/src/index.ts,它继承 SDK 的 Operator 基类,内部惰性初始化一个 Stagehand 实例(new Stagehand(options) 后 await init()),并实现三个关键成员:
1. 动作空间声明(MANUAL.ACTION_SPACES)
| 动作 | 参数 | 语义 |
|---|---|---|
GOTO |
url='' |
确定最佳起始 URL 并导航到目标页面 |
ACT |
description='' |
对当前页面执行一步自然语言描述的操作 |
EXTRACT |
description='' |
从页面中抽取信息 |
OBSERVE |
description='' |
观察并分析网页以决定下一步 |
CLOSE |
无 | 任务完成,不再执行动作 |
NAVBACK |
无 | 返回上一页 |
MANUAL.EXAMPLES 则给出一段完整的 few-shot:查"Stagehand 项目 GitHub 头号贡献者",依次 GOTO 仓库 → ACT 点 Insights → ACT 点 Contributors → CLOSE 收尾,与提示词中"第一步必须 GOTO"的约束相呼应。
2. 截图:走 CDP 而非 Stagehand API
public async screenshot(): Promise<ScreenshotOutput> {
const stagehand = await this.getStagehand();
const page = stagehand.page;
const cdpSession = await page.context().newCDPSession(page);
const { data: base64 } = await cdpSession.send('Page.captureScreenshot');
return { base64, scaleFactor: 1 };
}
从源码结构看,这里绕开高层 API 直接用 Playwright 的 page 对象建 CDP 会话调 Page.captureScreenshot,返回的 { base64, scaleFactor: 1 } 正是 SDK ScreenshotOutput 约定格式——这个 base64 会被 GUIAgent 拼入多模态消息发给 UI-TARS 模型。
3. execute:动作分发
execute(params) 读取 params.parsedPrediction(即模型输出的解析结果),按 action_type 分派:
GOTO:page.goto(url, { waitUntil: 'commit', timeout: 60000 }),60 秒超时;ACT:page.act(description),把自然语言动作交给 Stagehand 转成具体 DOM 操作;EXTRACT:page.extract(description)拿结构化结果(源码中标注 TODO:返回值的ObserveResult尚未完整回传给 Agent 循环);OBSERVE:page.observe({ instruction, useAccessibilityTree: true }),基于可访问性树观察页面;WAIT:setTimeout毫秒级休眠(注意:WAIT在execute的 switch 中可执行,但未列入MANUAL.ACTION_SPACES,即不会主动写进提示词的动作空间);NAVBACK:page.goBack();CLOSE:stagehand.close()并返回{ status: StatusEnum.END },这一状态最终触发 agent 路由的done帧,前端随之DELETE /api/session。
前端呈现:步骤卡片与直播画面的联动
ChatFeed 定义了 BrowserStep 类型(tool 取值 GOTO | ACT | EXTRACT | OBSERVE | CLOSE | WAIT | NAVBACK),每收到一个 SSE 帧就把 result 追加为带 stepNumber 的卡片,左侧是 iframe 直播画面(任务未完成时)或完成提示("The agent has completed the task"),右侧是滚动到底部的步骤列表。此外组件里还有几个可观察的交互约定:
- 输入页支持
Cmd/Ctrl+Enter提交、Cmd/Ctrl+K聚焦输入框、Esc关闭对话视图(见 page.tsx 的全局键盘监听); - 任务完成(末步为
CLOSE)时自动触发DELETE /api/session,与后端REQUEST_RELEASE对接,形成资源释放闭环。
关键技术栈与和上游 README 的一处差异
README 的 "Key Technologies" 一节列出:Browserbase(核心浏览器自动化与交互能力)、Stagehand(精确 DOM 操作与状态管理)、Next.js(Web 框架基座)、OpenAI(自然语言理解与决策)。需要说明的是,这一节继承自上游 open-operator 的描述;而本 fork 的实际代码里,"理解用户意图"的模型调用走的是 UI_TARS_BASE_URL / UI_TARS_API_KEY / UI_TARS_MODEL 指向的 UI-TARS 服务(见 agent 路由的 GUIAgent 配置),因此可以把该示例理解为"上游 Stagehand/Browserbase 架构 + UI-TARS 模型层"的组合,这也是它放在 UI-TARS-desktop 仓库 examples 目录下的意义:展示如何用 @ui-tars/sdk 把 UI-TARS 模型接到云浏览器上,构建一个可被他人复用的 Web Agent 样板。
适用前提与限制
- 该示例是显式声明的 PoC:README 的 WARNING 提示其目标是演示工具链,而非生产级产品;
- 依赖 pnpm(README 注明 NPM 不可用),并需要可访问的 UI-TARS 模型服务与 Browserbase 账号(API Key + Project ID)两套凭据;
- 云端会话的创建、直播画面(devtools 调试 URL 的替换)、会话释放都依赖 Browserbase SDK 的
sessions/contexts接口行为; - 部署到 Vercel 时,长任务能力由 vercel.json 的
maxDuration: 300保障,本地pnpm dev则无此限制。
小结
这个不到 20 个源文件的 Next.js 示例,串起了 UI-TARS-desktop 仓库中几个重要构件:@ui-tars/sdk 的 GUIAgent 主循环、@ui-tars/operator-browserbase 的 Stagehand/CDP 执行层、Browserbase 的会话与 context 管理、以及 SSE 流式 UI。若你希望在浏览器环境中运行 UI-TARS GUI Agent 并让用户实时观看执行过程,可以从 examples/operator-browserbase 入手,按"配置 5 个环境变量 → pnpm install → pnpm dev"三步跑通,再顺着 agent 路由 与 BrowserbaseOperator 源码 两条线深入阅读。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


