Skip to content

内置浏览器 ​

VMark 可以在文档窗口内部托管一个真正的网页浏览器——网页会成为与你的 Markdown 文档并列的一等标签页。它是货真价实的原生 webview(macOS WKWebView),既不是外部的 Chrome 窗口,也不是嵌入式的 frame。

仅限 macOS

内置浏览器在 macOS 上可用,并且默认开启。它在 Windows 和 Linux 上不可用:在这些平台上,下文提到的设置项和新建浏览器标签页命令根本不会出现。

工作区导轨

启用实验性的工作区导轨后,浏览器页面是窗口级全局的:它们在窗口内的每个工作区中都可以访问,永远不会绑定到某个工作区的标签页上。

关闭它 ​

在 macOS 上,浏览器默认开启。新建浏览器标签页位于文件菜单中 (Alt + Mod + Shift + B),也可在命令面板中找到——无需先启用任何东西。

要关闭它,请前往设置 → 高级,关闭内嵌浏览器。这会关闭所有已打开的浏览器标签页, 撤销 AI 积累的所有站点权限,并撤下下文所述的 AI 自动化接口。

两项 AI 姿态设置紧挨在该开关下方,且仅在其开启时才出现。两者的出厂设置都很保守,并且 不会因浏览器被启用而改变:

设置项默认值含义
AI 会话沙盒AI 驱动的页面获得独立会话,而不是共用你已登录的会话
允许回环地址关闭AI 导航到 localhost / 私有网络地址会被拒绝

站点权限不在设置里——它们位于浏览器侧栏中,在拥有它们的那个窗口内。

使用它 ​

浏览器标签页在编辑区打开,与你的文档并列——侧栏、标签栏、终端和状态栏都保持原位。它的控件位于页面上方,共用窗口的标题栏——标题栏是 VMark 自己绘制的。

控件操作
‹ / ›后退 / 前进。无处可去时置灰
⟳ / ✕重新加载,或停止正在进行的加载
地址栏一个多功能框(omnibox):输入 URL 前往该地址,输入其他内容则进行搜索
☆ / ★收藏此页面

新的浏览器标签页会打开 DuckDuckGo(https://duckduckgo.com),多功能框也使用 DuckDuckGo 搜索。以 http:// 或 https:// 开头的输入会按原样打开;不含空格的裸主机名(example.com、localhost:3000、IPv4 地址)会作为网址打开——本地地址使用 http,其他一律使用 https;其余输入则进行搜索。起始页和搜索引擎目前都还不能更改。

地址栏会自动跟踪页面:如果站点发生重定向,或某个链接把你带到别处,地址栏会更新,显示你实际所在的位置。

切换离开时,标签页会保留其页面。查看一下文档再回来,不会重新加载页面,也不会丢失你在页面里输入的内容——页面只是被隐藏起来,直到你关闭它的标签页时才会被销毁。这也正是 AI 能在你写作时继续在浏览器标签页中工作的原因(见下文的协同驾驶)。

如果页面试图打开弹出窗口(window.open、target="_blank" 链接),VMark 会将其拦截,并在页面顶部显示被拦截的地址和一个在新标签页中打开按钮,这样坚持使用弹出窗口的登录流程只需一次点击即可完成,而不是点了却毫无反应。

侧栏跟随标签页 ​

当浏览器标签页处于活动状态时,侧栏会显示浏览历史和书签。当你切回文档时,它会再次显示文件浏览器、大纲和文件历史——这一切都是自动的。没有第二种需要手动保持同步的模式,而且两侧都会记住你上次打开的内容,所以瞥一眼浏览器标签页不会让你丢失正在使用的文件树。

历史记录是按窗口划分的,且只在当前会话内存在:它绝不会写入磁盘。(仍然有一个清除按钮——“退出时它会消失”和“你现在就能清掉它”并不是一回事。)重新加载不会添加重复条目,而将你重定向的站点记录的是你想要访问的页面,而不是沿途的每一跳。

书签则会持久保存。它们以你收藏时的确切 URL 存储——同一页面的不同小节(#install 与 #usage)算作两个书签,而 VMark 不会悄悄“整理”URL 的查询参数,因为被改写过的 URL 未必能带你回到你所看到的内容。

窗口在页面周围转为中性色 ​

VMark 的主题是刻意带有色调的——Paper 是暖灰色,Mint 和 Sepia 则更甚。这用来写作很惬意,却不适合去包裹别人的网页:带颜色的边框会改变你对其中每一种颜色的观感,这正是没有一款真正的浏览器会给自己的界面外框上色的原因。

所以当浏览器标签页获得焦点时,周围的窗口会切换为素净的中性色——在浅色主题下为白色,在深色主题下为深色——并在你回到文档的那一刻切换回来。你的主题没有改变;改变的只是网页周围的部分。

终端遵循同样的规则。如果你在浏览器标签页旁边开着终端,它会采用与之匹配的中性色,而不是保持你主题的颜色,这样窗口的两半就能协调一致,而不是在一条可见的接缝处相接。深色主题得到的是深色终端,而不是白色的——终端里的颜色是针对其背景调校的,强行改成白色会让深色主题的输出难以阅读。

如果页面崩溃 ​

如果某个页面的网页内容进程终止,标签页会显示一个带有重新加载按钮的“此页面已崩溃”覆盖层,而不是空白或冻结的视图。对于短暂的崩溃,VMark 会自动重新加载几次;如果某个页面在加载时反复崩溃,它会停下来等你手动重新加载,这样你就永远不会卡在重新加载的循环里。

它是如何构建的(以及为什么它在设计上就注重隐私) ​

VMark 自行创建平台 webview,并将其作为窗口的原生子级添加进来——它不会向应用框架索要一个。这对隐私很重要:由框架创建的 webview 会向每个页面注入一个内部消息桥,等于把通往应用的通道交给了任何站点。因为 VMark 拥有的是一个全新构造、没有这种桥的 webview,被浏览的页面没有通往 VMark 的通道。页面是被严格单向驱动的(应用可以读取页面并对其操作;页面无法反向触及应用)。

会话(登录状态、cookie)按配置文件持久保存在操作系统 webview 自己的数据存储中,所以每个站点你只需登录一次。VMark 没有自己的密码或 cookie 存储;唯一的例外是你明确批准为 AI 保存的会话(见下文的会话保存 / 加载),其 cookie 和 localStorage 会存入操作系统钥匙串(keychain),绝不会写入文件。

用 AI 驱动浏览器 ​

通过 MCP 连接的 AI 助手可以操作浏览器标签页:

  • 读取(Read)——获取页面的结构化无障碍快照(每个可交互或结构性元素以角色 + 可访问名称表示,外加一个稳定的引用(ref)句柄,如 e5)。快照会遍历开放的 shadow root,并如实说明它无法触及的部分(封闭的 shadow root、frame),以及它是否因大小上限而被截断。
  • 操作(Act)——点击或输入某个目标,可以依据先前读取得到的精确引用(ref),也可以依据 ARIA 的角色 + 可访问名称(例如,点击名为“Learn more”的链接)。ref 只在操作已获授权时才被采信;任何需要你批准的操作都使用角色 + 名称,这样提示才能向你展示一个可读的元素。点击会核实它确实命中了:它会把目标滚动到可见范围内,要求目标已可见渲染——折叠区块内的重复按钮会被跳过,而不会被点击——并对点击点做命中测试,因此被覆盖层遮挡的目标会以“被……遮挡”的形式回报,而不会被穿透点击。当多个可见元素具有相同的角色和名称时,点击会以存在歧义为由被拒绝,而不是按页面中最先出现的那个来解析——页面无法把它自己的“Learn more”链接塞到站点的链接前面。AI 被告知的是实际发生了什么,而不仅仅是它尝试过,所以它无法悄悄地对错误的东西下手却回报成功。文件输入框永远不会被点击。
  • 滚动(Scroll)——将某个元素(依据 ref)滚动到可见范围内,或按像素量滚动。属于操作类(与点击一样受批准约束);批准会绑定所请求的确切滚动。
  • 按键(Key)——向获得焦点的元素或某个 ref 发送一次按键(Enter、Escape、Tab、方向键,可选配 Ctrl/Shift/Alt/Meta)——例如提交表单或关闭对话框。属于操作类,且批准会绑定确切的按键和修饰键。在表单中按 Enter 会提交表单,按 Tab 会移动焦点,与真实输入的效果一样;其他按键则是合成的 DOM 事件,因此只信任真实硬件输入的站点可能会忽略它们。
  • 查询(Query)——通过 CSS 选择器进行结构化 DOM 探测,用于无障碍快照无法命名的内容(表格、计算值、属性)。属于读取类。
  • 提取(Extract)——将页面转为阅读模式的 Markdown(标题、署名、正文散文,去除样板内容),用于 AI 想要阅读而非操作的页面。站点插件会按来源精细化提取——内置的 Wikipedia 插件会按名称剥除维基界面框架——并以一个通用阅读器作为后备。页面只导出字节;提取过程在 VMark 内运行。属于读取类。
  • 样式(Style)——CSS 操作(关闭阻挡视线的覆盖层、高亮某个目标),方式是设置内联样式、切换类,或注入一个 <style> 块(作用于整页,而非局限于选择器)。属于操作类,且批准会绑定确切的样式——在你允许之后,它不能被换成其他 CSS。
  • 执行 JS(Execute JS)——最后的应急手段:运行脚本以完成结构化动词无法表达的事情。它运行在隔离的内容世界中(DOM + CSS,绝不触及页面自己的 JavaScript),逐次批准(从不记忆——它没有“在此站点上允许”这一项),且其结果被视为不可信。批准提示会向你展示确切的脚本,而运行的正是那段脚本——AI 无法诱使你批准一段脚本却运行另一段。请优先使用查询 / 样式;只有在它们力有不逮时才动用这一项。
  • 会话保存 / 加载(Session save / load)——将标签页的当前会话保存在一个句柄(handle)下(一个由你批准的名称),之后再恢复它,让某个流程一开始就已处于登录状态——而 AI 从头到尾都看不到你的 cookie 或令牌。这些值存储在操作系统钥匙串(keychain)中(静态加密),AI 只收到句柄和一份计数摘要。保存和加载都逐次批准,而对某个句柄的批准不能用到另一个句柄上。恢复只对与其保存时同源的页面生效。这是按引用传递凭据:AI 说出会话的名字,秘密由 VMark 掌握。
  • 控制台(Console)——读取页面捕获的 console.* 输出(log/warn/error……),外加未捕获的错误和未处理的 promise 拒绝——这是页面在自身脚本出错时发出的信号,普通的 console 日志从不会显示它——从而让 AI 能够调试它正在驱动的页面。只读,仅限 AI 拥有的标签页(你自己的标签页不带捕获垫片),且输出被视为不可信的页面数据。这一点是为保住“设计上注重隐私”的保证而建的:捕获会写入页面自己的 DOM,VMark 从那里读取,因此不会有消息通道被反向打通回应用。

会话保存/加载——作用范围

一个已保存的会话涵盖 localStorage 和 cookie,两者都被限定在你保存时页面所提交到的 来源。cookie 通过原生 cookie 存储读取和回放,并且是双向按域限定的——保存永远不会复制 你整个 cookie 罐,恢复也永远不会把 cookie 种到某个无关的站点下。

  • 打开(Open)——创建一个 AI 拥有的标签页,将其置于最前,并加载一个 HTTP(S) URL。同一时间最多可以打开八个 AI 拥有的标签页,AI 完成后会关闭自己的标签页(关闭从不受批准约束——停止总是被允许的)。标签页还可以选择以一个具名配置文件打开,这是一个持久的上下文,使登录状态可以按名称复用;每次打开它都会重新询问你,且 AI 永远看不到凭据。
  • 导航(Navigate)——对一个 AI 拥有的标签页进行导航(并将其置于最前),并等待它的导航凭据。当加载出来的页面读起来像是一道关卡、而非所请求的内容时——登录墙、同意插页、人机验证挑战(reCAPTCHA/Turnstile),或限流提示——结果会如实说明,并且 AI 被要求把你牵涉进来,而不是试图绕过它。检测以精确为先:提到“$429”的价格,或写着“Cloudflare”的页脚,都不会误触它。
  • 等待(Wait)——等待某个特定的导航凭据,而不发起另一次加载。
  • 等待条件(Wait for)——轮询直到某个条件成立(依据 ref 或角色 + 名称的元素、一段可见文本,或标签页的 URL 包含某个子串——最后一种可确认由点击触发的导航已经完成;查询字符串和片段永远不参与匹配,因为重定向植入其中的令牌绝不能被探测到),或直到超时,并报告是否匹配。它让多步骤流程变得确定——操作,然后等待结果,再读取——而不是靠猜。等待和等待条件都不会改变你正在查看的标签页。
  • 截图(Screenshot)——获取页面当前渲染的一张 JPEG 图片,让 AI 能看到无障碍快照未曾命名的布局和渲染状态。和读取一样,它不会改动页面:在 AI 拥有的标签页上允许使用,而在人类标签页上只有当你已附加它时才允许。不是当前可见页面的标签页可能会渲染为空白。
  • 运行工作流(Run a workflow)——将一段简短的、已保存的步骤序列(点击 / 输入 / 导航 / 提取,以一种小型文本语法编写,并作为 source 传入)作为一次异步运行来回放:它会立即返回一个运行 id,然后由你轮询其状态,因为多步骤的运行会比单个请求活得更久。其中的每一步都像手动下达的操作一样逐一受批准约束——工作流不是绕过提示的手段——而那些 AI 无法确定性执行的步骤(自由散文式的“目标”、一次“确认”)会暂停运行,交由它手动处理。要在暂停后继续,需手动完成被暂停的那一步,然后由一次新的运行从暂停的那次运行恢复:它会继承已完成的步骤,并把被暂停的那一步视为已完成,因此不会有任何内容被提交两次;以相同输入重新运行同一个工作流时,也会跳过已经成功的写入步骤。运行是有界的(只计算运行时间——等待你的时间不计入),每个标签页一次只有一个,并且可以取消——取消始终被允许,即使某一步正在等待你的批准时也一样,而你自己接管浏览器也会停止运行。
  • 录制工作流(Record a workflow)——你不必手动编写这套语法,而是可以录制一个:在你批准的前提下(每次都会重新征询——录制从不是长期权限),VMark 会捕获你在标签页上执行的点击和字段编辑,并交回可直接运行的工作流文本。它在构造上就不含任何值:你键入的任何内容都不会被保存——每个字段都会变成一个具名的 {input},由你在回放时填入,密码字段会变成一道手动的 confirm: 步骤,而 URL 会被剥离到仅剩来源 + 路径。它记录的是你触碰了哪些控件,而绝非你输入了什么。

AI 浏览器姿态在设置 → 高级下配置:

  • 沙盒(Sandbox)(推荐)使用一个共享的、非持久的 AI webview 存储。它与其他沙盒标签页 共享 cookie,但不与人类标签页共享。
  • 共享配置文件(Shared profile)使用人类的 webview 存储,并在每次 AI 导航前请求对目标 地址的批准,除非该来源已有匹配的 navigate 授权。

AI 创建的标签页是临时的,重启后不会恢复。它们的 URL、模式、标题、代数和加载状态会出现在 session.get_state 中;凭据会从 MCP 响应中被抹除。

操作受批准约束:你未授权的操作不会被执行——AI 会被告知需要批准并等待。文件上传对 AI 绝不允许(由 AI 选择的文件上传会成为一条数据外泄路径);那些操作严格由人类驱动。

批准一次操作 ​

当 AI 请求操作时,VMark 会弹出一个提示并暂停页面。它会告诉你站点、操作和元素(它的角色和可访问名称,例如 button "Publish")——对于携带内容的操作,还会展示内容本身:输入将要键入的文本、按键将要按下的键、运行脚本或样式将要运行的确切脚本。这就是批准所绑定的对象;内容不同的重试会再次询问。

  • 允许一次(Allow once)——恰好授权那一个操作,作用于那个元素、那个页面。它会被立即用掉,不会变成长期权限。
  • 在此站点上允许(Allow on this site)——AI 可以在那个站点上执行那项操作而无需再次询问。它不会扩大到其他操作或其他站点。
  • 拒绝(Deny)——什么都不会发生。按 Escape,或者干脆按 Enter,同样是拒绝:这个提示被刻意设计得倾向于拒绝。提示出现后半秒内按下的允许会被忽略,而且一次按压必须在同一个提示上开始和结束——因此在你手指下被撤回的提示,无法把你的点击转交给下一个提示。

提示向你展示的是对操作的描述,而不是页面的图片——这是有意为之。网页掌控着自己的像素,所以一个恶意页面可以把“Delete everything”按钮的样式做得看起来像“Publish”。VMark 向你展示的,正是安全门所强制执行的那个确切对象,它取自浏览器引擎,而不是取自页面对自身的说辞。

权限还会在页面导航时失效。一个提示描述的是对某个特定页面的操作;如果在你做决定的过程中页面发生了变化,请求会被丢弃,而不会被套用到取而代之加载的任何内容上。一个尚未用掉的“允许一次”也会以同样的方式被作废。

这也包括页面内部的导航。大多数现代站点在各视图之间切换时从不加载新页面——地址变了,内容被改写了,但站点从未离开。这在此处很重要,因为站点和来源保持不变,而你批准的那个 button "Publish" 可能已经不再是叫那个名字的按钮了。所以 VMark 对页面内导航一视同仁:授权会随它所针对的那个视图一同失效,而不仅仅是随页面失效。

不过,真正承担分量的是描述符本身。一个站点可以在任何时刻改写自己的内容而根本不进行导航,且没有任何浏览器引擎会报告这一点。所以一个“允许一次”所授权的,恰恰是一项操作,作用于由角色和可访问名称标识的一个元素,在一个站点上——并且它会被立即用掉。真正需要三思的是“在此站点上允许”:它是对那个站点上那项操作的长期权限,而你授予它的站点,就是你在这件事上信任的站点。

查看和撤销权限 ​

浏览器侧栏(在拥有这些权限的窗口中)会列出你已授权的每个站点,以及它可以做什么。撤销会立即收回权限——那个站点上的下一次 AI 操作会重新询问。权限属于授予它们的那个窗口。

站点权限只保存在内存中:它们绝不会写入磁盘,在 VMark 退出时失效,而关闭浏览器会将它们全部撤销。让 AI 跨重启保有在某个站点上点击的能力,是一个比看上去更重的承诺,所以 VMark 不会悄无声息地做出这个承诺。

当 AI 以人类创建的标签页为目标时,VMark 会先询问是否要把 AI 访问附加到该标签页上。这次附加会绑定到当前的导航代数。允许一次会被浏览器引擎授权的第一次读取或操作用掉(一次随后发现目标被遮挡或隐藏的点击,同样会用掉它);允许直到导航会在下一次完整导航或页面内导航、关闭、禁用或重启时失效。在你附加某个标签页之前,AI 只能看到它的来源——看不到标题或路径。如果附加失败(驱动程序拒绝了,或在你做决定时页面已经跳转),提示会保持打开并说明情况;你可以重试或拒绝。

AI 导航默认拒绝回环地址、私有局域网、链路本地、元数据、格式错误以及不受支持协议的目标;在 AI 拥有的标签页上,一份内容规则列表还会对页面嵌入的内容——frame、图片、脚本、fetch 请求——施加同样的拒绝,因此公共页面无法利用 AI 的标签页触及你的网络。在 AI 发起的 open 或 navigate 发出之前,VMark 还会解析目标的主机名:只要任一解析结果属于上述被拦截的地址,就拒绝该导航(SSRF_BLOCKED,reason: resolves-private);如果该名称在有限的等待时间内无法解析,也会拒绝(reason: unresolved)——这样,一个看似公开、实则指向你的局域网或云元数据服务的名称,会在 WebKit 发出请求之前就被拦下。仍有两项限制,在此直说:这项预检只覆盖 AI 发起的导航,不覆盖重定向目标、页面内的链接点击或页面嵌入的内容(这些仍只依靠 URL 文本检查,以及针对字面私有地址的内容规则列表);而在预检之后发生变化的 DNS 解析结果(重绑定)不会被重新检查,因为 WKWebView 没有为连接实际使用的地址提供逐请求的钩子。

协同驾驶:从终端里看着 AI 驱动浏览器 ​

浏览器是一个窗格,而不是一种模式。这让一种特定的工作流成为可能:在浏览器标签页旁边打开一个终端(`Ctrl + ``),在其中运行一个 AI 智能体,看着页面随它的工作而响应。

终端和浏览器并排摆放——浏览器会调整大小来腾出空间,而不是被遮住。所以在智能体操作页面的整个过程中你都能看到它,而它采取的每一个操作仍然都得经过你这一关(见上文批准一次操作)。

这就是 VMark 中 AI 浏览器用法所设想的形态:智能体提议,页面可见,由你批准。它不是智能体在一个你看不见的窗口里干活。

夺回控制权只需一个动作。当一次 AI 工作流运行正在驱动某个标签页时,它的界面框会显示一个“AI 正在控制 — 点击接管”指示器。点击它——或者干脆自己与页面或它的地址栏交互——都会立即收回标签页并停止运行。你永远不必在智能体的终端里去找一个停止按钮;触碰浏览器本身就是停止按钮。

当页面加载失败时 ​

网络离线、主机名错误、证书被拒,或连接被拒绝,都会在浏览器窗格中产生一条说明出了什么问题的消息,并附带一个重试按钮,因此加载失败绝不会看起来像一个仅仅是加载缓慢的页面。

当前限制 ​

  • 本版本仅支持 macOS。
  • JavaScript 的 alert() 和 confirm() 对话框会呈现出来并由你作答;prompt() 暂时被抑制。弹出窗口(window.open)会被拦截,被拦截的地址会作为新标签页提供给你。
  • 受 HTTP 基本认证(basic authentication)保护的页面会加载失败——目前还没有用户名 / 密码提示。
  • 页内查找和页面缩放尚未实现。
  • VMark 目前还不会自行决定摄像头、麦克风、位置或通知请求;这些请求沿用系统 webview 的默认处理方式。
  • 下载、打印,以及按请求的网络策略尚未实现。悬停和拖拽操作还没有对应的 AI 动作。

这些正在逐步补齐;上文所述的是目前已能使用的功能。