Operit Kotlin 与 TypeScript 桥接接口对齐:NativeInterface 公开接口清点与类型声明修复实战

原创2026-09-26 12:20:181,637 阅读
文章标签:AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化

Operit Kotlin 与 TypeScript 桥接接口对齐:NativeInterface 公开接口清点与类型声明修复实战

导读

本文围绕 Operit 中 JsEngine 通过 @JavascriptInterface 暴露给脚本运行时的桥接接口,系统讲解"公开脚本接口"与"ToolPkg/运行时内部桥接"的边界划分,以及如何保证 examples/types/core.d.ts 中的每个 NativeInterface 公共方法都能在 Kotlin 侧找到真实实现。文章以仓库中 docs/TODO/kt_ts_bridge_alignment_20260809/1_NativeBridgeInventory.md 为骨架,结合源码给出可复现的核对方法与参数归一化细节,读完即可掌握:哪些接口应进入公开类型、如何移除无实现的旧声明、以及如何为 Kotlin 已注入的运行时方法补齐 TypeScript 类型。

一、问题背景:脚本桥接接口为什么会"对不齐"

Operit 的脚本运行时(JsEngine)通过 Android 的 @JavascriptInterface 注解向 JavaScript 暴露原生能力。经过长时间演进,桥接接口出现了两类典型的不一致:

  1. Kotlin 侧已注入、类型声明缺失:JsTools 在运行时向 Tools.Net 注入了 browserTakeScreenshot 截图方法,但 examples/types/network.d.ts 中没有对应声明,脚本作者在 TypeScript 中无法获得任何类型提示。
  2. 类型声明存在、Kotlin 侧无实现:examples/types/core.d.ts 的 NativeInterface 中保留了 setResult 与 setError 两个声明,但运行时根本没有对应的 Kotlin 实现——它们是历史遗留的"幽灵接口"。

同时,JsEngine 上还有大量未在 core.d.ts 声明的 @JavascriptInterface 方法。它们并非遗漏,而是 ToolPkg 注册机制与运行时生命周期管理专用的内部桥接,本就不应该出现在公开脚本接口中。因此对齐任务的第一步,就是做一次严格的"公开接口清点"。

二、旧实现盘点:内部桥接与公开接口的分野

2.1 JsEngine 上的 @JavascriptInterface 方法

以 JsEngine.kt 为例,@JavascriptInterface 方法分布在约 100 个位置,可以清晰地分成两组:

  • 脚本开发者直接可用的公开方法,例如 callTool、callToolAsync、callToolAsyncStreaming、logInfo、logError、logDebug、reportError、registerImageFromBase64、javaClassExists 等,它们在 core.d.ts 的 NativeInterface 命名空间中有对应声明。
  • ToolPkg 与运行时内部桥接,例如与 registerToolPkg() 注册会话相关的各类注册方法(registerToolPkgToolboxUiModule、registerToolPkgAppLifecycleHook、registerToolPkgMessageProcessingPlugin 等),以及 decompress、getEnvForCall、setEnv、isPackageImported 等运行时支撑方法。它们由 ToolPkg 与运行时包装后使用,不属于 NativeInterface 的公开脚本接口。

实际实现上,多数公开方法并非直接在 JsEngine 内联实现,而是委托给 JsNativeInterfaceDelegates.kt 中的函数。例如 callToolAsync(JsEngine.kt#L2364)委托到 JsNativeInterfaceDelegates.callToolAsync,后者在后台 Thread 中执行工具并回调;decompress(JsEngine.kt#L1531-L1539)委托到 decompress 完成 deflate 解压。这种"接口薄、委托厚"的结构,使桥接层只负责参数透传,业务逻辑集中在可单测的委托对象中。

2.2 JsTools 注入的 Tools.Net.browserTakeScreenshot

JsTools 在运行时向 JS 侧 Tools.Net 注入了浏览器截图封装(JsTools.kt#L478-L505),其参数归一化逻辑是类型声明的直接依据:

  • type:可选字符串,省略或为空时默认 "png";
  • element 与 ref:必须成对提供,只给其一会抛出 "element is required when ref is provided" 或 "ref is required when element is provided";
  • fullPage:通过 !!params.fullPage 强制转成布尔值。

对应地,Kotlin 工具层在 StandardBrowserSessionTools.kt 中按 browser_take_screenshot 分发到 browserTakeScreenshot(tool)(L1609)执行真正的截图。清点结论明确:JsTools 向 Tools.Net 注入了 browserTakeScreenshot,但类型声明没有对应方法——这是一处必须补齐的缺口。

三、修改意图:公开接口边界规则

清点后的修改意图非常明确,可归纳为三条边界规则:

  1. 公开类型只描述脚本开发者可直接使用的桥接方法:NativeInterface 类型是脚本侧对原生能力的"合同",只应包含有真实 Kotlin 实现、且面向脚本开放的公共方法。
  2. 移除没有 Kotlin 实现的声明:setResult 与 setError 在 Kotlin 侧不存在实现,属于过时残留,必须从 core.d.ts 中删除。
  3. 不把内部桥接方法复制到 core.d.ts:ToolPkg 注册类方法、运行时支撑类方法由框架包装使用,写入公开类型反而会让脚本作者误用或形成错误契约,因此保持其"实现存在、类型不公开"的状态。

四、完成记录:具体落地内容

4.1 移除 setResult 与 setError

NativeInterface 已移除 setResult 与 setError 两个声明。核对 core.d.ts 的 NativeInterface 命名空间(从第 94 行开始),当前保留的成员均能在 Kotlin 侧找到 @JavascriptInterface 实现,例如:

core.d.ts 声明 Kotlin 实现(JsEngine.kt)
callTool(toolType, toolName, paramsJson) fun callTool(...)(L2350)
callToolAsync(...) fun callToolAsync(...)(L2364)
callToolAsyncStreaming(...) fun callToolAsyncStreaming(...)(L2386)
logInfo(message) fun logInfo(message: String)(L2555)
reportError(...) fun reportError(...)(L2583)
registerImageFromBase64(...) fun registerImageFromBase64(...)(L2278)
javaClassExists(className) fun javaClassExists(className: String): Boolean(L2063)

其余公共声明均映射到 Kotlin 的 @JavascriptInterface 实现;ToolPkg 与运行时内部桥接未进入公共声明,符合边界规则。

4.2 补齐 Tools.Net.browserTakeScreenshot 的类型声明

在 network.d.ts 中新增了与运行时归一化逻辑一致的声明:

function browserTakeScreenshot(options: {
    type?: string;
    element?: string;
    ref?: string;
    fullPage?: boolean;
}): Promise<string>;

四个参数与 Kotlin 归一化逻辑一一对应:type 省略时默认 png;ref 与 element 必须成对;fullPage 为布尔开关。该声明同时被同步到面向脚本开发者的文档 docs/doc-src/package-dev/network.md,其中明确说明"type 省略时使用 png,ref 与 element 必须成对提供,fullPage 控制是否截取完整页面",与 JsTools.kt 的运行时校验保持完全一致。

4.3 Compose DSL 节点的公开化

同类对齐还覆盖了 Compose DSL:AiChat 与 AdaptiveSidePanel 两个节点此前缺少公开类型与渲染路径。对齐后:

  • TypeScript 侧在 compose-dsl.d.ts 中新增 AiChatProps、AdaptiveSidePanelProps 接口与对应 ComposeNodeFactory;
  • Kotlin 侧在 ToolPkgComposeDslScreen.kt 中由 renderAiChatNode 与 renderAdaptiveSidePanelNode(L2477、L2487)提供渲染路径。其中 AdaptiveSidePanel 的 Kotlin 实现支持宽屏拖拽调宽与窄屏遮罩关闭两种交互形态。

五、静态核对方法(可复现)

本任务不运行编译、构建或测试命令,验证采用以下静态手段(详见任务索引 index.md 与核对文档 3_StaticVerification.md):

  1. 公开接口映射核对:逐项检查 core.d.ts 的 NativeInterface 是否每个成员都能在 Kotlin @JavascriptInterface 中找到实现;
  2. 运行时注入与类型声明比对:比较 JsTools.kt 注入的浏览器截图方法与 network.d.ts 的 Net 成员,确认参数与返回值一致;
  3. Compose DSL 集合比较:比较 compose-dsl.d.ts 的新增节点与 Kotlin Compose DSL 节点分发集合——核对结果为类型工厂与 Kotlin 渲染节点各 88 个,双向集合无差异;
  4. 过时名称排查:确认 setResult 与 setError 不再出现在公开类型声明或开发者文档中;
  5. 代码卫生检查:git diff --check 未发现空白错误。

全部完成标准通过:browserTakeScreenshot 的参数字段与 Kotlin 参数归一化逻辑一致,AiChat 与 AdaptiveSidePanel 均有 TS 类型和 Kotlin 渲染路径,过时名称彻底消失。

六、结论与启示

NativeInterface 已移除 setResult 与 setError;Tools.Net.browserTakeScreenshot、AiChat、AdaptiveSidePanel 的公开类型、Kotlin 实现与开发者文档已三方对齐。这次清点沉淀下的核心方法论值得复用:

  • 接口契约以"有实现"为唯一标准:类型声明不是文档愿望清单,而是运行时事实的投影,删除无实现的声明比保留占位符更安全;
  • 内部桥接与公开接口严格分界:框架内部方法即便暴露在 JS 运行时中,也不应进入公开类型,避免脚本作者误用;
  • 静态核对可替代编译验证:通过"声明集合 vs 实现集合"的双向比对,可以在不引入构建开销的前提下发现接口漂移,适合作为桥接层 CI 检查的轻量手段。

后续若继续新增 @JavascriptInterface 方法或 Tools 注入能力,建议沿用本文的清单法:先写清点文档、再补类型与文档、最后做静态双向核对,保证 Kotlin 桥接层与 TypeScript 类型声明始终同步。

登录后查看全文
Operit