Operit Kotlin 与 TypeScript 桥接接口对齐:NativeInterface 公开接口清点与类型声明修复实战
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 暴露原生能力。经过长时间演进,桥接接口出现了两类典型的不一致:
- Kotlin 侧已注入、类型声明缺失:
JsTools在运行时向Tools.Net注入了browserTakeScreenshot截图方法,但examples/types/network.d.ts中没有对应声明,脚本作者在 TypeScript 中无法获得任何类型提示。 - 类型声明存在、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,但类型声明没有对应方法——这是一处必须补齐的缺口。
三、修改意图:公开接口边界规则
清点后的修改意图非常明确,可归纳为三条边界规则:
- 公开类型只描述脚本开发者可直接使用的桥接方法:
NativeInterface类型是脚本侧对原生能力的"合同",只应包含有真实 Kotlin 实现、且面向脚本开放的公共方法。 - 移除没有 Kotlin 实现的声明:
setResult与setError在 Kotlin 侧不存在实现,属于过时残留,必须从core.d.ts中删除。 - 不把内部桥接方法复制到
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):
- 公开接口映射核对:逐项检查
core.d.ts的NativeInterface是否每个成员都能在 Kotlin@JavascriptInterface中找到实现; - 运行时注入与类型声明比对:比较
JsTools.kt注入的浏览器截图方法与network.d.ts的Net成员,确认参数与返回值一致; - Compose DSL 集合比较:比较
compose-dsl.d.ts的新增节点与 Kotlin Compose DSL 节点分发集合——核对结果为类型工厂与 Kotlin 渲染节点各 88 个,双向集合无差异; - 过时名称排查:确认
setResult与setError不再出现在公开类型声明或开发者文档中; - 代码卫生检查:
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 类型声明始终同步。