- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
本文以 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)实现要点(createInstanceProxy,JsJavaBridge.kt):
call(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); - 未命中字段与内部类时,返回一个“静态方法调用函数”,内部走
callStaticWithCompanionFallback; - Kotlin 伴生对象兜底:
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参数支持两种形态(normalizeExternalCodeLoadOptions,JsJavaBridge.kt):
- 字符串:等价于
{ nativeLibraryDir: 字符串 }; - 对象:支持
nativeLibraryDir?: string与childFirstPrefixes?: string[]两个字段,后者用于声明“优先从加载的 dex/jar 中解析类、再委托给父类加载器”的包名前缀集合,并会做去重处理。
每个成功加载的外部代码路径会登记为一条JavaBridgeLoadedCodePath(index、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。
单接口 / SAM(Single Abstract Method)场景下,契约还保证可以省略接口名:
Java.implement(() => { ... }) Java.proxy(() => { ... })实现层通过“impl未提供且第一个参数是函数或普通对象(非类引用)”来判断省略写法,并将接口名列表置空,由目标参数类型在调用侧推导(JsJavaBridge.kt中implement的入口处理)。
6.2 回调位置直接传 JS 对象 / 函数
如果回调参数位置的目标类型本身就是接口,契约保证可以直接传 JS 对象或 JS 函数,无需显式Java.implement:
button.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与invokeNativeSuspend):
- JS 侧注册一个 Promise 回调对象(
registerJsObject(promiseCallback))拿到callbackId; - 通过
NativeInterface.javaCallStaticSuspend/javaCallInstanceSuspend把调用与callbackId一起投递给 Kotlin 侧; - Kotlin 侧执行
suspend函数后,把结果回传到回调对象,JS 侧据此 resolve / reject Promise。
因此脚本可以放心对任意 Kotlinsuspend fun或可挂起 API 使用await,无需关心线程调度细节。类型声明中实例代理与类代理均带有callSuspend(methodName, ...args): Promise<JavaBridgeValue>。
8. Java → JS 转换:返回值归一化语义
契约对 Java / Kotlin 返回到 JS 的转换做了精确的逐项定义:
| Java / Kotlin | JS |
|---|---|
null/Unit | null |
String/char | string |
Java 方法返回的CharSequence值 | 可按string使用 |
boolean/Boolean | boolean |
Number | number |
Enum | string |
Class<?> | string |
Map/JSONObject | plain object |
Iterable/List/Set | JS array |
| Java 数组 | JS array |
JSONArray | JS 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 时,契约保证按下表按目标参数类型进行转换:
| JS | Java / Kotlin |
|---|---|
null | 非 primitive 参数 |
string | String/char/enum/Class<?>/JSONObject/JSONArray |
number | 各种数字类型 |
boolean | boolean/Boolean |
| JS array | Java 数组 /Collection/JSONArray/ varargs |
| plain object | Map/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 对象 / 数组 / 字符串 / 数字 / 布尔 /
null; - Java 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.ts,caseProxyStaticAndInstance用例恰好逐项验证了这套写法: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_FIELD,Kotlin 类可直接享受伴生对象兜底; - 接口回调:参数位置直接传 JS 对象/函数,或显式
Java.implement/Java.proxy; - 挂起函数:统一
await ...callSuspend(...); - 只有遇到字段/方法同名冲突、调试或排查桥接问题时,才降级到
.call(...)/.get(...)/.set(...)/callStatic(...)。
12. 以契约为验收基准:给测试与实现的对齐建议
契约文档明确把自身定位为“测试的验收基准”与“实现的对齐目标”。从仓库现状看,桥接层已有完整的测试载体:
- 桥接功能用例:java_bridge.ts(覆盖全局暴露、包链访问、静态/实例调用、接口实现、
NativeInterface.java*底层桥); - 运行时入口:JsEngine.kt(
NativeInterface.java*系列方法在此接线到JsJavaBridgeDelegates); - 类型契约:java-bridge.d.ts(供 TS 脚本开发与静态校验使用)。
对于需要扩展桥接能力的开发者,建议遵循同样的流程:先在契约文档中补充承诺 → 在 java_bridge.ts 中增加验收用例 → 再在JsJavaBridgeDelegates与 JS 桥接脚本中实现。这样能保证“文档承诺 → 测试验收 → 实现行为”三者始终对齐,也符合契约文档“实现与文档不一致时,优先修 Bridge / Runtime”的总原则。
参考路径速查
- 接口契约文档:docs/doc-src/dev-core/JAVA_BRIDGE_INTERFACE.md
- JS 侧桥接定义(
buildJavaClassBridgeDefinition):app/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsJavaBridge.kt - Kotlin 侧委托实现(
JsJavaBridgeDelegates):app/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 Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
相关推荐
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 在抓取目标网站后,都会把
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考