跳到主要内容

Midscene — 架构与原理(视觉驱动的计算机操作)

30 秒导读: Midscene 让你用大白话指挥 AI 操作界面——"点登录按钮""在搜索框输入 coffee"。 它不读 DOM、不认 CSS 选择器,而是截一张图丢给多模态大模型,模型看图回答"下一步做什么、 点哪个坐标",引擎把这个坐标精确映射回真实屏幕像素并落成一次点击,然后再截一张图问模型 下一步——如此循环直到任务完成。同一套机制既能自动化浏览器,也能操作整台电脑桌面(computer-use)。


1. 这是什么(零基础也能懂)

一句话定义: Midscene 是一个视觉驱动的 UI 自动化 / 测试引擎——你用自然语言描述要做的事, 它靠"看屏幕截图"的多模态模型来完成操作,而不是靠页面结构。

解决什么问题 / 给谁用。 传统 UI 自动化(包括读 DOM、读无障碍树的 AI 工具)都依赖页面结构, 而结构既脆弱又不全:

  • 选择器一重构就失效,维护成本高;
  • 图标按钮、自定义控件、<canvas> 这类没有语义标记的元素,结构层根本"看不见";
  • 原生 App、跨域 iframe 够不着;
  • 它判断不了"东西到底长得对不对"(颜色、高亮、布局)。

Midscene 换了个思路:只要人眼能看见,它就能操作。因为它只从截图工作,你只需一句句地用自然 语言描述每一步。这段"为什么"来自 README 的 "Why Midscene" 一节 1

它能做什么(功能):

  • 用 JS SDK 或 YAML 写自动化脚本,或交给 AI agent 自主执行;
  • 覆盖 Web 浏览器、Android、iOS、HarmonyOS、桌面电脑,以及任意自定义界面——一套 API;
  • 核心方法:aiAct(执行一段自然语言任务)、aiQuery(从页面抽数据)、aiAssert(断言页面状态)等。

用起来什么样。 面向"整台电脑"的最小用法(computer-use):拿到一个操作本机桌面的 agent, 然后用一句话下命令 2

// 示意,非源码:core 用法一瞥
import { agentForComputer } from '@midscene/computer';

const agent = await agentForComputer(); // 连接到本机主显示器
await agent.aiAct('打开浏览器,搜索 "midscene",点第一个结果');
// 引擎内部:截图 → 问模型 → 点击/输入 → 再截图 → …… 直到完成

一句话直觉 / 类比。 把它想成一个只会看屏幕、只会用鼠标键盘的实习生:你不给它页面源码, 只给它"你现在看到的这张屏幕",它看图、决定动哪、你替它动手,然后你再拍一张新屏幕给它看。 它的全部智能都建立在"看图 → 说下一步"这一个动作上。

本节不涉及底层代码。目标:完全不懂的人读完知道"它是干嘛的、凭什么不看 DOM 也能操作"。


2. 顶层全景(它大概怎么转)

2.1 一张图看懂主循环

Midscene 的心脏是一个**「计划 → 执行 → 再截图 → 重规划」的闭环**。怎么读这张图:从上往下是一轮, 最后一步"没做完就回到顶上再来一轮",做完就退出。

用户一句话任务:"点登录按钮"


┌─────────────────────────────┐
│ ① 截图 │ 截当前屏幕 → base64 (Device.screenshotBase64)
└─────────────────────────────┘
│ 截图 + 任务 + 可用动作清单(actionSpace)

┌─────────────────────────────┐
│ ② 计划(问多模态模型) │ 模型看图,输出:下一步动作 + 目标坐标 + 是否完成
└─────────────────────────────┘ (genericXmlPlan / 自定义 planFn)
│ 模型给的坐标是"归一化 bbox"(如 0–1000)

┌─────────────────────────────┐
│ ③ 视觉定位:坐标 → 真实像素 │ 按图像尺寸把归一化坐标映射回像素、取中心点
└─────────────────────────────┘ (mapLocateResultToPixelBboxByCoordinates)
│ 得到 {x, y} 像素中心

┌─────────────────────────────┐
│ ④ 执行:落成真实点击/输入 │ {x,y} → 移动鼠标 → 按下 (InputPrimitives.pointer.tap)
└─────────────────────────────┘


模型说"完成了"吗?
├── 否 → 回到 ①,再截一张图,带着历史重新规划(replan)
└── 是 → 结束,返回结果

这套循环的代码主线分三层:Agent.aiAct 3 收下任务 → 调 TaskExecutor.action(公开入口, 内部委托私有 runAction)4,runAction 里是一个 while (true)统一 plan/replan 循环 5; 每轮先问模型拿计划,再把计划"翻译成可执行任务"交给 TaskRunner 跑掉,若模型没说"完成" (shouldContinuePlanning)就再转一圈 6

2.2 部件一句话职责

部件干什么在哪(包/文件)
Agent (PageAgent)门面层:对外提供 aiAct/aiQuery/aiAssert,管模型运行时与缓存packages/core/src/agent/agent.ts
TaskExecutor编排层:runAction 主循环——plan → convert → 执行 → 判断是否 replanpackages/core/src/agent/tasks.ts
TaskRunner执行层:带状态机的任务队列,逐个跑 Planning/Action/Insight 任务并管截图与快照packages/core/src/task-runner.ts
规划器 (genericXmlPlan / planFn)把"截图+任务+动作空间"喂给模型,解析出下一步动作packages/core/src/ai-model/llm-planning.ts
模型适配层 (model-adapter / models)每个模型家族一套:如何组 prompt、如何解析坐标、坐标怎么归一化packages/core/src/ai-model/models/*
视觉定位映射归一化 bbox → 真实像素 bbox → 中心点.../shared/model-locate-result/*
AbstractInterface + InputPrimitives设备抽象:声明有哪些原子动作(tap/type/scroll…)packages/core/src/device/index.ts
ComputerDevice / RDPDevice具体设备:把 {x,y} 落成真实鼠标键盘(本地桌面 / 远程 RDP)packages/computer/src/*
agent-tools(computer_*)把 agent 暴露成 MCP / CLI 工具packages/computer/src/agent-tools.ts

2.3 主线走一遍(高层,不进代码)

  1. 你调 agent.aiAct("点登录按钮")
  2. TaskExecutor 截一张屏,连同任务和"这台设备支持哪些动作"一起发给模型。
  3. 模型回一段结构化文本:<action-type>Tap</action-type> + 一个归一化的目标坐标 + 一段思考。
  4. 引擎把归一化坐标按截图尺寸换算成真实像素,取 bbox 中心得到 {x, y}
  5. TaskRunner 把动作 task 跑掉:设备层把 {x, y} 映射到真实屏幕(桌面多显示器时还要加显示器偏移),移动鼠标、按下。
  6. 模型若没在回复里标 <complete>,引擎再截一张新图、带上历史,重复 2–5,直到完成或超过重规划上限。

3. 阅读地图(建议顺序)

Midscene 是个大型 monorepo(20+ 包),但理解它只需抓住一条主线:视觉驱动的动作闭环。 下面按"由浅入深"排好,建议顺序阅读:

  1. 主循环:aiAct 的「计划—执行—重规划」 —— 先看这条主线怎么转: aiAct → TaskExecutor.runActionwhile 循环、什么时候 replan、计划怎么被"翻译成可执行任务" 交给 TaskRunner、计划缓存(YAML)如何复用。这是全库的骨架。

  2. 纯视觉定位:从截图到屏幕像素 —— 全库工程含量最高的一环: 模型只会给"图上大概哪块"(归一化 bbox),引擎如何按图像尺寸精确换算成像素、取中心、裁掉给模型 补的 padding、再加显示器偏移落到真实屏幕。定位准不准,全在这里。

  3. 可插拔模型家族:standard 与 custom 两条路 —— 为什么 Qwen、 Doubao、Gemini、UI-TARS、GLM 能即插即用:每个家族一套适配器,声明"坐标顺序 xy 还是 yx、归一化到 多少、怎么解析、要不要 padding";两条规划路线(标准 XML 规划 vs 完全自定义 planFn)。

  4. 设备抽象与动作落地:从 InputPrimitives 到真实桌面 —— 一套 InputPrimitives(tap/type/scroll/drag…)如何自动生成"动作空间"喂给模型;ComputerDevice 如何用 libnut / AppleScript 把动作落成真实鼠标键盘;截图、多显示器几何、坐标偏移的处理。

  5. 对外形态:computer_* 工具、本地/RDP 两种后端 —— Midscene 怎么 被别的 agent 使用:computer_connect / computer_list_displays 等 MCP/CLI 工具,以及同一套 agent 如何切换"操作本机桌面"与"通过 RDP 操作远程 Windows"两种后端。

只想要一句话精华,读完 §2 就够;想真正读源码,按上面 1→5 顺序下钻。


4. 巧妙之处(可借鉴的技术)

这些是读完值得带走的设计决策。每条先点"妙在哪",再给源码锚点。

4.1 「纯视觉」把"定位"变成模型的事,把"精确"留给引擎

传统方案要引擎去理解页面结构才能定位元素;Midscene 把定位职责整个交给多模态模型——模型只需 在图上圈出"大概这块"(一个归一化 bbox 或一个点)。引擎不负责"理解界面",只负责一件确定性的事: 把模型给的归一化坐标,按当前图像尺寸精确换算回像素,再取 bbox 中心作为落点 7 8

妙在分工:模糊的语义理解交给模型(它擅长),精确的像素换算交给代码(它可靠)。两边都做自己 最擅长的事。pointFromLocate 最后就是简单地取 locate.center{x, y} 9——所有 复杂度都被前面的映射吸收掉了。

4.2 "给模型补 padding,再把补的裁掉":preparedSize vs contentSize

有些模型要求输入图像的边长是某个块大小的整数倍(如 Qwen 的 padBlockSize: 28)。Midscene 会给截图 补边到合规尺寸再发给模型,但记住两个尺寸:preparedSize(补过的,模型看到的)和 contentSize (真实截图内容)10

妙在:坐标preparedSize 解析(因为模型是对着补过的图说的),但最终裁剪到 contentSize—— 这样模型偶尔"点到"补出来的空白 padding 上,也不会被当成有效 UI 位置 11。一个尺寸对模型、 一个尺寸对现实,互不污染。

4.3 坐标系"数据化":一张配置表适配所有模型家族

不同模型吐坐标的习惯不一样:Qwen/Doubao 是 xy 顺序、归一化到 1000;Gemini 是 yx 顺序、也归一化到 1000;还有的直接给绝对像素。Midscene 没有为每个模型写 if-else,而是把这些差异声明成一份数据配置 { shape, order, normalizedBy },由同一个映射函数消费 12 13 14

妙在:加一个新模型 = 加一条配置,不动映射逻辑。order: 'yx' 时映射函数自动把坐标翻成 xy 再算 15。数据驱动,而非逻辑分叉。

4.4 动作空间"从原语自动长出来",还顺带喂给模型

设备只要声明自己有哪些输入原语(InputPrimitives:tap、typeText、scroll、dragAndDrop…), defineActionsFromInputPrimitives 就自动把它们编译成一组带 zod schema 的 DeviceAction 16。 这份"动作空间"既是执行时的分发表,又被序列化进 prompt 告诉模型"你现在只能用这些动作" 17

妙在:新设备只需实现几个原语,动作空间与模型提示自动同步。桌面、浏览器、手机共用这套机制, 所以"一套 API 打通所有平台"不是口号,而是这层抽象的自然结果。

4.5 计划可缓存成 YAML,命中就跳过问模型

aiAct 成功后,会把这一轮规划出来的动作序列存成 YAML 工作流缓存;下次同样的 prompt 命中缓存时, 直接回放 YAML、不再问模型 18。若回放失败(界面变了),再退回正常的"问模型"路径并让旧缓存 失效 19。妙在用一次真实规划的结果换后续的确定性与速度,同时对界面漂移有兜底。

4.6 同一个 agent,本地桌面与远程 RDP 无缝切换

agentForComputer 造本地 ComputerDevice,agentForRDPComputerRDPDevice,两者都实现同一个 AbstractInterface 20computer_* 工具里,只要调用方传了 host 就自动切到 RDP 模式, 不传就是本机 21。妙在:上层 aiAct 循环完全不知道自己在操作本机还是一台远程 Windows—— 差异被死死封在设备层。


5. 代码地图(导航索引)

用符号名可 grep,比行号抗漂移。all as-of commit 948eded9

主题文件路径关键符号
对外门面 / aiAct 入口packages/core/src/agent/agent.tsAgentaiAct
主循环 plan/replanpackages/core/src/agent/tasks.tsTaskExecutor.runActionconvertPlanToExecutable
单批执行 / 任务状态机packages/core/src/task-runner.tsTaskRunner.flush
是否继续规划的判定packages/core/src/agent/tasks.tsshouldContinuePlanning
标准 XML 规划器packages/core/src/ai-model/llm-planning.tsplanparseXMLPlanningResponse
规划器导出别名packages/core/src/ai-model/workflows/planning/index.tsgenericXmlPlan
归一化坐标→像素 bbox.../ai-model/shared/model-locate-result/pixel-bbox-mapper.tsmapLocateResultToPixelBboxByCoordinates
归一化换算与裁剪.../ai-model/shared/model-locate-result/bbox.tsmapNormalizedCoordinatesToPixelBboxfinalizePixelBbox
定位坐标系默认配置packages/core/src/ai-model/model-adapter/locate.tsdefaultLocateResultAdapterDefinitionresolveLocate
标准/自定义规划解析packages/core/src/ai-model/model-adapter/planning.tsresolvePlanning
模型家族注册表packages/core/src/ai-model/models/registry.tsMODEL_ADAPTER_CONFIGSgetModelAdapter
各家族坐标配置packages/core/src/ai-model/models/{qwen,doubao,gemini}.tsqwenAdaptersdoubaoVisionAdaptergeminiAdapters
图像预处理(补边/双尺寸)packages/core/src/ai-model/workflows/image-preprocess.tsprepareModelImagepreparedSize/contentSize
设备抽象 & 动作空间生成packages/core/src/device/index.tsAbstractInterfaceInputPrimitivesdefineActionsFromInputPrimitivespointFromLocate
桌面设备实现packages/computer/src/device.tsComputerDeviceinputPrimitivestoGlobalPointscreenshotBase64actionSpace
局部坐标→全局屏幕偏移packages/computer/src/device.tsmapDisplayLocalPointToGlobal
底层鼠标键盘驱动packages/computer/src/input-driver.tsComputerInputDriver
本地/RDP agent 工厂packages/computer/src/agent.tsagentForComputeragentForRDPComputer
远程桌面后端packages/computer/src/rdp/device.tsRDPDevice
MCP/CLI 工具packages/computer/src/agent-tools.tsComputerMidsceneToolscomputer_connect
CLI 入口packages/computer/src/cli.tsrunToolsCLI(stripPrefix: 'computer_')

Footnotes

  1. README.md:45-54,"💡 Why Midscene" 一节陈述"依赖结构脆弱、只从截图工作"的设计理由。

  2. packages/computer/src/agent.ts:53-66,agentForComputer 造本地 ComputerDeviceconnect(),返回继承自 PageAgentComputerAgent

  3. packages/core/src/agent/agent.ts:870-1011,aiAct 解析 prompt、选规划/默认模型、查计划缓存,再委托给 taskExecutor.action(agent.ts:963)。

  4. packages/core/src/agent/tasks.ts:356,TaskExecutor.action 是公开入口,内部委托私有 runAction(tasks.ts:431)承载主循环。

  5. packages/core/src/agent/tasks.ts:520,位于 runAction 内的 while (true) 统一 plan/replan 循环。

  6. packages/core/src/agent/tasks.ts:792-794,if (!planResult?.shouldContinuePlanning) break;——模型标了完成就退出,否则继续 replan(上限见 replanningCycleLimit,tasks.ts:808)。

  7. .../shared/model-locate-result/pixel-bbox-mapper.ts:110-140,mapLocateResultToPixelBboxByCoordinates 依据 preparedSize 把解析出的坐标换算成像素 bbox。

  8. .../shared/model-locate-result/bbox.ts:20-35,mapNormalizedCoordinatesToPixelBbox[0, normalizedBy] 的归一化值按 size-1 映射到像素索引。

  9. packages/core/src/device/index.ts:248-256,pointFromLocate 直接取 locate.center 作为 {x, y}——复杂度已被前面的映射吸收。

  10. packages/core/src/ai-model/workflows/image-preprocess.ts,prepareModelImage 返回 preparedSize(补边后、模型所见)与 contentSize(真实截图)。

  11. .../shared/model-locate-result/bbox.ts:106-126,finalizePixelBbox 断言坐标在 preparedSize 内,但最终 clampcontentSize,滤掉落在 padding 上的点。

  12. packages/core/src/ai-model/model-adapter/locate.ts:7-9,defaultLocateResultAdapterDefinition = { coordinates: { shape: 'bbox', order: 'xy', normalizedBy: 1000 } }

  13. packages/core/src/ai-model/models/qwen.ts:112:128-132,Qwen 一档 order: 'xy', normalizedBy: 1000,另一档 padBlockSize: 28 且用绝对像素坐标。

  14. packages/core/src/ai-model/models/gemini.ts:205,Gemini 用 order: 'yx', normalizedBy: 1000(与 Qwen 的 xy 相反)。

  15. .../shared/model-locate-result/pixel-bbox-mapper.ts:81-108,reorderCoordinatesToXyorder === 'yx' 时把坐标翻成 xy 再计算。

  16. packages/core/src/device/index.ts:989-1061,defineActionsFromInputPrimitives 依据存在的原语(pointer/keyboard/scroll/touch/system)编译出对应 DeviceAction 列表。

  17. packages/computer/src/device.ts:1481-1490,ComputerDevice.actionSpace() = 原语动作 + 平台动作(如 ListDisplays)+ 自定义动作;该清单在规划时 getActionSpace() 被读取喂给模型(tasks.ts:565)。

  18. packages/core/src/agent/agent.ts:934-950,命中且可用的计划缓存直接 runYaml(yaml) 回放,不再问模型。

  19. packages/core/src/agent/agent.ts:951-958,回放抛错则置 cachedYamlFailed、告警并退回正常规划路径。

  20. packages/computer/src/agent.ts:60-79,agentForComputer / agentForRDPComputer 分别造 ComputerDevice / RDPDevice,两者皆为 AbstractInterface

  21. packages/computer/src/agent-tools.ts:125-147,adaptComputerInitArgs:传了 hostmode: 'rdp',否则 mode: 'local';computer_connect 据此选后端(agent-tools.ts:219-246)。