FeishuDevTools:从设备仿真到 MCP 的飞书 H5 调试工作台

FeishuDevTools:从设备仿真到 MCP 的飞书 H5 调试工作台

调试飞书 H5 应用时,页面能在浏览器里打开,往往只是第一步。布局取决于视口和安全区,JSSDK 会识别容器环境,登录涉及账号与租户,授权又依赖应用配置和平台响应。一个问题横跨这几层时,仅凭页面上的“调用失败”,很难判断应该从哪里查起。

我写了 FeishuDevTools,希望把设备模拟、Chromium DevTools、飞书登录与 JSAPI 调用记录放进同一个桌面工作台,再通过 MCP 把这套调试环境开放给 AI 客户端。

这篇文章记录它的几个实现选择:设备参数何时生效、原生 DevTools 如何嵌入、哪些能力连接真实平台,以及界面操作变成工具调用后,需要补上什么完成语义。

项目官网 · 下载 v0.1.1 · GitHub 源码 · MCP npm 包

FeishuDevTools 浅色模式:左侧设备模拟器,右侧停靠的 Chromium DevTools

实际应用截图,来自仓库使用匿名诊断页生成的公开资源。左侧检查设备环境,右侧直接查看同一页面的 DOM 与样式。文章封面为概念插画。

先把一条调试流程接起来

FeishuDevTools 的范围是飞书 / Lark H5 网页调试。应用可以加载本地开发地址或远程页面,切换 iPhone、Android、iPad、PC 预设,在同一窗口使用 Elements、Console、Sources、Network、Application 等 Chromium 面板。

一条日常流程可以这样进行:

  1. 启动自己的 H5 开发服务,在地址栏打开页面。
  2. 选择设备,检查布局、触摸行为和容器环境识别。
  3. 通过 DevTools 定位样式、脚本与网络请求,通过 JSAPI 日志查看桥接调用。
  4. 涉及平台身份时,使用扫码登录和对应租户继续联调。
  5. 生成真机预览二维码,在真实飞书客户端复核结果。

真机预览会生成 lark://client/web?isDev=1&url=… 链接;遇到 localhost 时转换为局域网地址。手机仍然需要能访问这台开发机,开发服务器也要监听可达的网卡地址。地址替换本身不会建立公网隧道。

我把小程序、网关、工作台、代码编辑器和上传能力留在范围之外。这个项目是独立的开源实现,与飞书官方没有隶属关系;它要解决的是 H5 开发中反复切换环境、丢失上下文的那一段工作。

设备仿真,关键在首个脚本执行之前

手机外框很容易做,但它不能决定网页真正看到的环境。

以响应式页面为例,启动脚本可能马上读取 innerWidth,组件可能根据 devicePixelRatio 选择资源,手势库可能在初始化时检查 navigator.maxTouchPoints。如果先加载页面,再补设备参数,这些逻辑就可能已经按桌面环境完成了初始化。后面即使看起来像手机,也不能证明首屏运行在正确的设备条件下。

因此,FeishuDevTools 的初始化顺序是:

about:blank
    → 等待 guest 附着与布局稳定
    → 设置 UA、viewport、DPR 和触摸仿真
    → 加载目标 URL
    → 报告 guest 就绪

主进程通过 Chrome DevTools Protocol 应用 Emulation.setDeviceMetricsOverrideEmulation.setTouchEmulationEnabled 等指令。PC 自适应模式还要先测量面板的实际尺寸。配置失败时保留空白页并进入重试,而不是让页面在一组只应用了一半的参数下继续运行。具体逻辑可以看 Simulator 初始化主进程仿真实现

这里还有一个容易混淆的区别:工作台里的显示缩放,与网页的 CSS 视口是两件事。

把设备缩小到能在笔记本屏幕上完整显示,不应该顺便改变媒体查询的断点。当前实现通过 CSS transform 缩放设备的显示尺寸,网页仍使用设备配置对应的视口。这样,显示空间不足时可以调整工作台,而不必改变正在验证的布局条件。

当然,桌面 Chromium 的设备仿真不等于手机上的真实内核、系统权限与客户端行为。它适合尽早发现问题;真机仍然负责最后一段兼容性验证。

完整 DevTools 背后,是三个不同的视图角色

我希望调试时保留开发者熟悉的 Chromium 面板。因此,工作台的结构分成三个角色:

部分 实现 职责
应用外壳 BrowserWindow 中的 React 界面 地址栏、设备控制、账号、设置和弹窗
被调试页面 独立的 <webview> guest 加载 H5,承载设备仿真与 JSAPI 桥
调试面板 WebContentsView 承载 guest 对应的 Chromium DevTools 前端

连接 guest 与调试前端的核心调用很短:

guest.setDevToolsWebContents(view.webContents)
guest.openDevTools({ mode: 'detach', activate: false })

这里的 detach 配合的是已经指定的 DevTools WebContents。主进程把它所属的原生视图加入窗口,再按照 React 占位区域上报的坐标设置边界,最终形成右侧停靠布局。实现见 devtools-dock.ts

真正花功夫的是叠层:原生 WebContentsView 位于 DOM 之上,CSS 的 z-index 无法让普通弹窗跨过它。

账号菜单或设置弹窗覆盖到调试面板时,当前实现先截取 DevTools 的画面,放到 React 占位区里,再暂时隐藏原生视图。弹层关闭后恢复原来的调试视图。用户看到的是暂时静止的面板,而不是突然出现的一块空白。这段处理集中在 DevToolsPane.tsx

选择元素也有类似的协调问题。移动设备开启触摸仿真后,鼠标事件会被转换,影响检查器的悬停高亮与点击选取。工具在进入元素检查模式时临时关闭触摸仿真,退出后恢复设备设置。做一个调试工作台,需要同时考虑被调试页面和调试器本身怎样使用同一组输入事件。

JSAPI 要明确区分真实鉴权与本机模拟

容器开发最容易产生误判的地方,是把“回调成功”理解成“平台链路已经验证”。

FeishuDevTools 将 JSAPI 分成几类处理。涉及 tt.configrequestAuthCoderequestAccess 的流程,由主进程使用真实登录会话请求平台;需要用户确认的授权,再通过界面收集决定并提交。Toast、Modal、ActionSheet、导航栏等能力由桌面界面承接;剪贴板等系统能力交给主进程。另有一部分设备、传感器相关调用使用明确列出的固定模拟值,定位也来自设置中的模拟位置。

这些能力的验证价值不同:鉴权请求能暴露应用配置与平台响应的问题,界面模拟便于联调调用参数和交互,固定返回值则只能帮助页面跑过对应分支。不能因为某个 JSAPI 在工作台中返回了成功,就认定它已经在手机上获得了真实权限或访问了真实传感器。

桥接入口位于 guest preload,调用经宿主路由到主进程或界面层,再把结果送回页面。调用和应答同时进入日志缓冲,可通过 MCP 的 get_jsapi_log 读取。关于支持范围与处理方式,可以直接查看仓库中的 JSAPI 定义固定模拟值

登录本身也有独立的生命周期。扫码窗口使用临时会话分区,H5 使用持久的 guest 分区;这属于按角色隔离,当前并没有按项目建立独立 profile。登录 Cookie 只有在受保护的 safeStorage 后端可用时才持久化。Linux 下若落到 basic_textunknown 或没有可用的安全后端,新会话只保留在当前进程内。

之所以检查后端类型,是因为“调用了加密 API”不足以说明凭证得到了怎样的保护。Electron 的 safeStorage 文档 也明确说明,Linux 的实际保护方式取决于桌面环境提供的存储后端。

MCP 接入之后,完成语义成为接口的一部分

FeishuDevTools 内置 18 个 MCP 工具,覆盖页面导航、设备切换、截图、DOM、脚本执行、控制台与 JSAPI 日志。应用启动后,默认在本机提供 Streamable HTTP 端点:

{
  "mcpServers": {
    "feishu-devtools": {
      "url": "http://127.0.0.1:17331/mcp"
    }
  }
}

需要 stdio 桥接或自动拉起桌面应用时,可以使用已发布的 feishu-devtools-mcp@0.1.1。例如,为支持的客户端写入配置:

npx --yes feishu-devtools-mcp@0.1.1 install cursor

MCP 带来的价值,是让 AI 可以围绕当前页面收集证据:查看控制台、读取 DOM、切换设备、截图比较,再读取 JSAPI 的调用结果。讨论可以落在同一份运行状态上,而不必只根据转述猜测。

但把按钮包装成工具还不够。以 set_device 为例,命令发出时,UA 更新、CDP 参数应用和页面重载可能都还没结束。如果立刻返回成功,紧接着的截图就可能捕获旧设备或切换中的画面。

当前实现为每次切机创建 request ID,先订阅对应的完成或失败事件,再发出操作;新的切机请求会明确淘汰仍在等待的旧请求。回执要和发起它的那次操作匹配,后台尺寸上报也不能误满足等待条件。相关代码见 MCP 切机处理

这也是我在工具接口上很看重的一点:成功应该表达具体的完成条件。目前 navigatereload 仍属于命令已发送的回执,不能把它们理解成页面和业务数据都已加载完成;自动化调用者需要继续观察页面状态。

MCP 服务只绑定 127.0.0.1,并检查 Host。它包含 evaluateclickfill 等有实际操作能力的接口,应连接给自己信任的本机客户端;当前没有逐工具授权或令牌认证层。回环监听限定了网络暴露范围,并不等于为所有本地进程建立了权限隔离。

调试便利性需要有明确的权限边界

工作台会打开开发中的页面,也可能访问远程页面,因此 shell 的系统能力不能自然地延伸给 guest。

当前 guest 保持 contextIsolation=truesandbox=truenodeIntegration=false;shell preload 暴露 IPC 白名单,主进程进一步检查发送者是否来自主窗口、主 frame 和可信的 shell URL。敏感浏览器权限按来源询问,未知权限默认拒绝;JSAPI 剪贴板访问同样经过主进程确认。

这与 Electron 安全文档 对远程内容隔离、权限处理和 IPC 来源校验的建议一致。这里的重点是运行时检查:TypeScript 可以约束调用代码的形状,却不能替主进程判断一条消息来自哪个页面。

Linux 打包也保留 Chromium 沙箱要求。应用发现无沙箱启动参数或对应环境开关时会拒绝启动,安装环境的问题需要在系统层解决。这些约束会影响一部分机器的首次运行体验,但它们是一个可以加载网页的桌面工具必须认真处理的边界。

0.1.1 的发布状态与使用范围

截至本文发布,GitHub Releases 已提供 v0.1.1,npm 上的 MCP 包也为 0.1.1。下载包按平台和架构区分:

平台 架构 提供的格式
macOS 13+ Apple Silicon、Intel DMG、ZIP
Windows 10 / 11 x64 安装版、便携版、ZIP
Linux x64 AppImage、DEB、RPM、tar.gz

安装包与 SHA256SUMS.txt 均在 v0.1.1 Release 中。选择与系统架构匹配的文件,并核对同名条目的 SHA-256。macOS 应用目前仅做 ad-hoc 签名且未经公证,Windows、Linux 包尚未签名,具体安装说明以项目 README 为准。

0.1.1 仍处于早期阶段。跨平台构建和启动冒烟检查已有发布流程支撑,真实飞书账号、多租户、应用鉴权,以及各平台安装与更新体验仍需要持续 UAT。本文描述的是当前代码与已发布产物的能力范围,不把自动化检查等同于这些场景已经全部验收。

仓库采用 MIT License,包含桌面应用、MCP 桥接包、测试和发布脚本。如果想从源码运行,准备 Node.js 22.12+、pnpm 10+,在仓库目录执行:

pnpm install
pnpm dev

对我来说,调试工具的价值在于缩短从现象到证据的距离。页面看到什么设备环境、JSAPI 经过了哪一层、一个操作是否真的完成,这些信息越明确,开发者越容易判断下一步。FeishuDevTools 就沿着这条方向继续迭代。

欢迎通过 GitHub Issues 反馈可复现的问题。提供系统与架构、应用版本、复现步骤和脱敏后的日志,会比一张孤立的报错截图更有帮助。

ihopeful Blog 由博主亲笔撰写,重要信息可放心引用。