Operit 的 QuickJS + Java Bridge 接口契约:从语法糖到底层桥接的完整实践指南 AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载本文以 Operit 仓库中的接口契约文档 JAVA_BRIDGE_INTERFACE.md 为主体结合其 JS 侧桥接定义 JsJavaBridge.kt、Kotlin 侧委托实现 JsJavaBridgeDelegates.kt、运行时类型声明 java-bridge.d.ts 与桥接测试套件 java_bridge.ts面向脚本开发者、桥接维护者与测试工程师完整梳理 Operit 中 QuickJS 脚本运行时与 Android Java/Kotlin 世界之间的接口契约、调用语义与推荐写法。读完本文你将能正确使用Java/Kotlin全局对象完成类与包访问、构造与调用、接口实现、挂起调用并理解 Java ↔ JS 双向类型转换的精确语义从而在 ToolPkg 脚本或调试场景中稳定地驱动 Android 原生能力。1. 契约定位为什么需要一份“接口契约”文档Operit 的脚本运行时基于 QuickJS脚本本身运行在独立的 JS 引擎中而 Android 应用的能力Activity、Context、系统服务、业务类位于 Java/Kotlin 世界。两者之间需要一个稳定、可验证的桥接层。本文档即docs/doc-src/dev-core/JAVA_BRIDGE_INTERFACE.md正是这份桥接的接口契约其目标只有三个给脚本开发者一份简洁、可直接依赖的 API 文档给测试提供明确的验收基准给 Bridge 实现提供需要对齐的目标行为。契约文档特别强调了一个重要原则如果实现与文档不一致应优先把问题视为 Bridge / Runtime 待修项而不是先降低文档承诺。这意味着文档描述的是桥接层的“应当行为”是脚本开发者可以放心依赖的稳定语义测试与实现都应围绕它对齐而不是反过来削足适履。从源码结构看这一契约被分成了两层实现JS 侧buildJavaClassBridgeDefinition()生成的桥接脚本见 JsJavaBridge.kt负责构造Java/Kotlin全局对象、类代理class proxy、实例代理instance proxy、包代理package proxy以及接口实现标记Kotlin 侧JsJavaBridgeDelegates见 JsJavaBridgeDelegates.kt通过反射完成类加载、方法/构造器匹配、参数转换与回调分发并通过NativeInterface.java*系列方法与 JS 侧通信。桥接脚本最终通过globalThis.Java Java; globalThis.Kotlin Java;JsJavaBridge.kt中buildJavaClassBridgeDefinition的结尾部分注入运行时因此脚本中可以直接使用Java与Kotlin两个全局名称。2. 入口Java与Kotlin是同一个桥的两个别名运行时注入两个全局对象JavaKotlin它们是同一个桥的两个别名行为完全一致。也就是说Kotlin.type(java.lang.StringBuilder)与Java.type(java.lang.StringBuilder)返回的是同一套类代理机制。在examples/java_bridge.ts的caseBridgeExposed用例中测试即同时断言了Java与Kotlin两个全局对象的存在assertTrue(typeof Java object, Java global must exist); assertTrue(typeof Kotlin object, Kotlin global must exist);别名存在的意义在于让脚本作者可以按语义选择操作 Java 类时用Java操作 Kotlin 类/伴生对象时用Kotlin但底层不需要维护两套实现。3. 类与包访问五种等价入口契约保证以下五种类/包访问方式全部可用Java.type(java.lang.StringBuilder) Java.use(java.lang.StringBuilder) Java.importClass(java.lang.StringBuilder) Java.package(java.lang) Java.java.lang.StringBuilder Java.android.os.Build.VERSION从实现上看JsJavaBridge.kt的JavaApi定义Java.type(className)核心入口调用createClassProxy(normalized)生成类代理Java.use(className)/Java.importClass(className)直接委托给this.type(className)三种写法完全等价Java.package(packageName)将包名按.拆分为路径段构建一个可继续向下访问的包代理Java.java.lang.StringBuilder、Java.android.os.Build.VERSION通过JavaApi外层Proxy的get钩子实现。每次属性读取时先尝试classExistsRaw(prop)判断是否为类若不是则返回createPackageProxy([prop])继续构建包链——这正是“包链式访问”的底层原理也解释了为什么Java.android.os.Build.VERSION这种“包.包.包.类.静态字段”的写法可以一次到位。契约还保证这些入口返回的都是同一套代理语义因此Java.use与Java.importClass获取的类代理可以混用。4. 构造与调用语法糖类代理与实例代理的完整行为4.1 类代理Cls的构造对类代理Cls契约保证以下三种构造方式等价new Cls(...args) Cls(...args) Cls.newInstance(...args)实现上createClassProxy(className)创建了一个可调用函数作为target该函数在调用时直接转发到target.newInstance.apply(target, arguments)JsJavaBridge.kt同时Proxy的apply与construct两个陷阱也统一指向newInstance从而让new Cls()、Cls()与Cls.newInstance()三条路径殊途同归。这里还有一个值得注意的接口构造糖当传入单个参数且该参数是函数/普通对象、且底层抛出的错误信息包含is an interface; use Java.implement(时newInstance会自动转为JavaApi.implement(className, rawArgs[0])JsJavaBridge.kt中shouldSugarInterfaceConstruction与newInstance的配合。这意味着“构造一个接口类型”的常见错误写法会被桥接层自动修正为接口实现。4.2 实例代理obj的成员访问对实例代理obj契约保证obj.method(...args) // 等价于 obj.call(method, ...args) obj.field // 等价于 obj.get(field) obj.field value // 等价于 obj.set(field, value)实现要点createInstanceProxyJsJavaBridge.ktcall(methodName, ...args)是显式底层写法走javaCallInstance原生桥get(fieldName)走javaGetInstanceField无参调用obj.get()等价于调用实例的get()方法重载消歧set(fieldName, value)走javaSetInstanceField单参obj.set(value)等价于调用实例的set(value)方法Proxy.get陷阱的探测顺序是先查内置成员 → 再探测实例方法javaHasInstanceMethod→ 再尝试字段读取javaGetInstanceField→ 最后兜底返回一个“方法调用函数”Proxy.set陷阱对未定义属性一律按“实例字段写入”处理走javaSetInstanceField。这种“方法优先、字段兜底”的顺序配合契约中obj.method(...args) obj.call(method, ...args)的等式正是脚本开发者判断“读到的到底是字段还是方法”的依据。4.3 类代理Cls的静态成员访问对类代理Cls契约保证Cls.STATIC_FIELD // 静态字段读取 Cls.STATIC_FIELD value // 静态字段写入 Cls.staticMethod(...args) // 静态方法调用 Cls.InnerClass // 内部类代理并且Cls.staticMethod(...args) Cls.callStatic(staticMethod, ...args)实现要点createClassProxy静态字段读取优先走javaGetStaticField失败后按className $ prop探测内部类并返回内部类的类代理同时兼容prop.toUpperCase()的枚举常量命名习惯例如Cls.INNER命中Cls$INNER未命中字段与内部类时返回一个“静态方法调用函数”内部走callStaticWithCompanionFallbackKotlin 伴生对象兜底callStaticWithCompanionFallback在原生静态调用失败且错误信息匹配method ... not found on ...或no method ... matched on ...时会先尝试Companion实例上的同名方法再尝试className$Companion类的静态调用JsJavaBridge.kt。这解释了为什么 Kotlin 类上的companion object方法可以像静态方法一样被脚本调用。4.4 显式底层写法.call(...)/.get(...)/.set(...)/callStatic(...)契约明确指出这些显式写法属于底层写法主要用于调试字段 / 方法同名冲突排查桥接问题。日常开发中应优先使用语法糖仅当出现歧义例如某字段与某方法同名Proxy.get命中方法探测时才降级到显式写法精确定位。5. 顶层桥接 API完整清单与语义契约保证以下顶层 API 全部可用Java.classExists(className) // 类是否存在 Java.newInstance(className, ...args) // 按类名构造实例 Java.callStatic(className, methodName, ...args) // 按类名调用静态方法 Java.callSuspend(className, methodName, ...args) // 挂起式静态调用Kotlin suspend Java.getApplicationContext() // 应用上下文 Java.getCurrentActivity() // 当前 Activity Java.loadDex(path, options?) // 加载外部 dex Java.loadJar(path, options?) // 加载外部 jar Java.listLoadedCodePaths() // 列出已加载的外部代码路径对照 java-bridge.d.ts 的JavaBridgeApi声明实际运行时还额外提供了两个别名getContext()等价于getApplicationContext()getActivity()等价于getCurrentActivity()JsJavaBridge.kt中JavaApi定义。loadDex/loadJar的options参数支持两种形态normalizeExternalCodeLoadOptionsJsJavaBridge.kt字符串等价于{ nativeLibraryDir: 字符串 }对象支持nativeLibraryDir?: string与childFirstPrefixes?: string[]两个字段后者用于声明“优先从加载的 dex/jar 中解析类、再委托给父类加载器”的包名前缀集合并会做去重处理。每个成功加载的外部代码路径会登记为一条JavaBridgeLoadedCodePathindex、type: dex | jar、path、nativeLibraryDir、childFirstPrefixes、alreadyLoaded字段可通过Java.listLoadedCodePaths()获取。6. 接口实现Java.implement/Java.proxy与回调映射规则6.1 两种显式接口实现方式契约保证Java.implement(interfaceNameOrClassProxy, impl) Java.proxy(interfaceNameOrClassProxy, impl)Java.proxy在实现上就是JavaApi.proxy function(...) { return this.implement(...) }两者完全等价JsJavaBridge.kt。接口引用既可以是字符串类名也可以是类代理如Java.java.lang.Runnable对应类型声明中的JavaBridgeInterfaceRef string | JavaBridgeClass。单接口 / SAMSingle Abstract Method场景下契约还保证可以省略接口名Java.implement(() { ... }) Java.proxy(() { ... })实现层通过“impl未提供且第一个参数是函数或普通对象非类引用”来判断省略写法并将接口名列表置空由目标参数类型在调用侧推导JsJavaBridge.kt中implement的入口处理。6.2 回调位置直接传 JS 对象 / 函数如果回调参数位置的目标类型本身就是接口契约保证可以直接传 JS 对象或 JS 函数无需显式Java.implementbutton.setOnClickListener({ onClick(view) { console.log(clicked); } }); someApi.acceptRunnable(() { console.log(run); });这与第 4.1 节的“接口构造自动糖”属于同一设计思路凡是“目标参数类型是接口”的场景桥接层都倾向于把 JS 函数/对象自动包装为接口代理。配合javaPollPendingJsCallback/javaResolvePendingJsCallback轮询机制JsJavaBridge.kt中tryProcessPendingJavaBridgeCallback回调会被送回 QuickJS 运行时线程执行。6.3 接口映射规则契约定义的映射规则如下对象同名方法映射到接口方法JS 对象上的属性名与接口方法名一致的函数会被调用getX()/isX()/setX(v)可映射到对象属性x即 JavaBean 风格的存取器可以简写为属性非void/ 非Unit回调可直接return返回值会作为接口方法返回值回传给 Java/Kotlin 侧。调用完成后返回的标记对象形如{ __javaJsInterface: true, __javaJsObjectId, __javaInterfaces }见JavaBridgeJsInterfaceMarker类型声明可直接作为参数传给期望接口类型的 Java 方法/构造器。7. 挂起调用callSuspend永远返回Promise契约保证callSuspend(...)永远返回Promise并且支持三个层级await Java.callSuspend(com.example.Demo, load, arg) // 顶层 API await SomeClass.callSuspend(load, arg) // 类代理 await someInstance.callSuspend(load, arg) // 实例代理实现机制JsJavaBridge.kt中scheduleSuspendCall与invokeNativeSuspendJS 侧注册一个 Promise 回调对象registerJsObject(promiseCallback)拿到callbackId通过NativeInterface.javaCallStaticSuspend/javaCallInstanceSuspend把调用与callbackId一起投递给 Kotlin 侧Kotlin 侧执行suspend函数后把结果回传到回调对象JS 侧据此 resolve / reject Promise。因此脚本可以放心对任意 Kotlinsuspend fun或可挂起 API 使用await无需关心线程调度细节。类型声明中实例代理与类代理均带有callSuspend(methodName, ...args): PromiseJavaBridgeValue。8. Java → JS 转换返回值归一化语义契约对 Java / Kotlin 返回到 JS 的转换做了精确的逐项定义Java / KotlinJSnull/UnitnullString/charstringJava 方法返回的CharSequence值可按string使用boolean/BooleanbooleanNumbernumberEnumstringClass?stringMap/JSONObjectplain objectIterable/List/SetJS arrayJava 数组JS arrayJSONArrayJS array其他普通对象Java 实例代理两点补充说明契约原文强调表中语义针对的是Java/Kotlin 方法返回值的归一化——即“返回到 JS 时按字符串/数组/对象使用”如果你显式构造普通 Java 对象例如new Java.java.lang.StringBuilder()、new Java.java.util.ArrayList()得到的仍然是Java 实例代理而不会被拍平成 JS primitive / array / object。也就是说“自动转换”发生在方法返回值的边界上而脚本自己构造出的 Java 对象始终以代理形态存在需要时再通过其方法如toString()、toArray()取用。9. JS → Java 转换按目标参数类型转换JS 传给 Java / Kotlin 时契约保证按下表按目标参数类型进行转换JSJava / Kotlinnull非 primitive 参数stringString/char/enum/Class?/JSONObject/JSONArraynumber各种数字类型booleanboolean/BooleanJS arrayJava 数组 /Collection/JSONArray/ varargsplain objectMap/JSONObject/ 接口实现代理Java 实例代理原始 Java 对象Java.implement(...)/Java.proxy(...)返回值Java 接口代理注意“按目标参数类型”这一前提同一份 JS 值传给不同签名的方法时转换目标取决于方法签名。Kotlin 侧JsJavaBridgeDelegates通过反射枚举候选方法/构造器为每个候选计算参数转换得分ConvertedArg(score)与MethodMatch/ConstructorMatch数据结构挑选得分最高的重载执行——这正是桥接层能够自动完成类型归一化与重载分派的底层机制。10. 返回结果两种完成方式与支持的结果类型导出函数允许两种完成方式且都是正式接口return result; complete(result);实现上桥接脚本注册的 Promise 回调对象同时充当“complete 通道”JsJavaBridge.kt中scheduleSuspendCall的promiseCallback因此complete(result)与return result语义一致适用于回调式导出函数的场景。结果对象保证支持普通 JSON 对象 / 数组 / 字符串 / 数字 / 布尔 /nullJava Bridge 实例携带__javaHandle/__javaClass的代理Java Bridge 回调代理__javaJsInterface标记对象。这些结果类型均可被桥接层正确序列化回传也符合 java-bridge.d.ts 中JavaBridgeValue的联合类型定义。11. 推荐写法默认用语法糖底层写法留作调试契约给出的推荐写法示例const File Java.java.io.File; const file new File(/sdcard/demo.txt); const name file.getName(); const path file.absolutePath; const Integer Java.java.lang.Integer; const value Integer.parseInt(123); const max Integer.MAX_VALUE;对应到桥接测试 java_bridge.tscaseProxyStaticAndInstance用例恰好逐项验证了这套写法Java.java.lang.Integer的静态字段与静态方法、Java.use/Java.importClass/Kotlin.type三种入口、以及Java.callStatic(java.lang.Integer, parseInt, 7)顶层写法均断言结果一致。实践中可以遵循以下决策路径构造优先const obj new Cls(...)其次是Cls.newInstance(...)实例成员优先obj.method(...)/obj.field/obj.field v静态成员优先Cls.staticMethod(...)/Cls.STATIC_FIELDKotlin 类可直接享受伴生对象兜底接口回调参数位置直接传 JS 对象/函数或显式Java.implement/Java.proxy挂起函数统一await ...callSuspend(...)只有遇到字段/方法同名冲突、调试或排查桥接问题时才降级到.call(...)/.get(...)/.set(...)/callStatic(...)。12. 以契约为验收基准给测试与实现的对齐建议契约文档明确把自身定位为“测试的验收基准”与“实现的对齐目标”。从仓库现状看桥接层已有完整的测试载体桥接功能用例java_bridge.ts覆盖全局暴露、包链访问、静态/实例调用、接口实现、NativeInterface.java*底层桥运行时入口JsEngine.ktNativeInterface.java*系列方法在此接线到JsJavaBridgeDelegates类型契约java-bridge.d.ts供 TS 脚本开发与静态校验使用。对于需要扩展桥接能力的开发者建议遵循同样的流程先在契约文档中补充承诺 → 在 java_bridge.ts 中增加验收用例 → 再在JsJavaBridgeDelegates与 JS 桥接脚本中实现。这样能保证“文档承诺 → 测试验收 → 实现行为”三者始终对齐也符合契约文档“实现与文档不一致时优先修 Bridge / Runtime”的总原则。参考路径速查接口契约文档docs/doc-src/dev-core/JAVA_BRIDGE_INTERFACE.mdJS 侧桥接定义buildJavaClassBridgeDefinitionapp/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsJavaBridge.ktKotlin 侧委托实现JsJavaBridgeDelegatesapp/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsJavaBridgeDelegates.kt运行时接线NativeInterface入口app/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsEngine.kt桥接类型声明examples/types/java-bridge.d.ts桥接验收用例examples/java_bridge.ts赞分享AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载相关推荐Symfony KeyManagement 组件 Bridge 架构解析从接口契约到七大后端的 DSN 桥接实战Symfony KeyManagement 组件 Bridge 架构解析从接口契约到七大后端的 DSN 桥接实战 在 Symfony 生态中 symfony后端Web框架WxJava 微信接口增量贡献指南从官方接口契约到 SDK 落地的完整实践WxJava 微信接口增量贡献指南从官方接口契约到 SDK 落地的完整实践 本文以 WxJava微信开发 Java SDK的贡献约定文档 skills/w后端即时通讯RSS-Bridge 缓存机制Cache API完全指南从接口契约到自定义实现RSS Bridge 缓存机制Cache API完全指南从接口契约到自定义实现 导读 RSS Bridge 的每个 Bridge 在抓取目标网站后都会把后端上一篇百度网盘秒传链接转存教程10分钟跑通单链转存、批量转存与链接生成下一篇3分钟把PC游戏串到SwitchMoonlight-Switch首次串流通关指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考