跳转到内容

插件开发指南 ​

更新: 2026/11/7 字数: 0 字 时长: 0 分钟

开发环境准备

要开发 Android 插件,推荐使用 Android Studio 作为主要开发工具。请确保你已完成以下准备工作:

  • 安装最新版本的 Android Studio
  • 正确配置 SDK 和 Gradle 构建环境
  • 具备基础操作能力,例如:
    • 创建 Android 项目
    • 添加依赖和模块
    • 编译与打包 APK

如果你是第一次使用 Android Studio,建议先阅读 官方入门文档。

如何开发一个插件 ​

本示例中的包名均为 org.botjs.plugin.sdk.demo,在实际项目中插件包名可能有所不同。

插件SDK集成 ​

新建一个 Android 项目。在 settings.gradle.kts(或项目根 build.gradle.kts)中添加 JitPack 仓库:

kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

如果项目仍使用 allprojects 方式配置仓库,可改为:

kotlin
allprojects {
    repositories {
        // ...
        maven { url = uri("https://jitpack.io") }
    }
}

在具体模块(比如 app)的 build.gradle.kts 中添加依赖:

kotlin
dependencies {
    // ...
    implementation("com.github.BotJsPro:Bot.js-Plugin-SDK:1.0.0")
}

更多信息参见 Jitpack.io。

关于 compileOnly

如果宿主/插件框架在运行时保证提供同一份 SDK 类(例如插件代码被动态加载到宿主进程,与宿主共享 ClassLoader),可以改为 compileOnly,避免插件 APK 重复打包 SDK:

kotlin
compileOnly("com.github.BotJsPro:Bot.js-Plugin-SDK:1.0.0")

但若插件 APK 的 PluginService 运行在独立进程、且该进程不能从宿主获得 SDK 类,则仍需 implementation,让插件 APK 自己能加载 PluginService / Plugin 等基类。

插件配置 ​

1. 新建 PluginHelloWorld 文件,继承于 Plugin. ​

java
public class PluginHelloWorld extends Plugin {

    public PluginHelloWorld(Context context, Context selfContext, Object runtime, Object topLevelScope) {
        super(context, selfContext, runtime, topLevelScope);
    }

    // 返回插件的 JavaScript 胶水层代码的 assets 目录路径
    @Override
    public String getAssetsScriptDir() {
        return "js-plugin";
    }

    // 插件 public API,可被 JavaScript 代码调用
    public String getStringFromJava() {
        return "Hello, Bot.js Pro!";
    }

    // 插件 public API,可被 JavaScript 代码调用
    public void say(String message) {
        getSelfContext().startActivity(new Intent(getSelfContext(), HelloWorldActivity.class)
                .putExtra("message", message)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
    }

    // 启动普通后台服务(可选)
    public boolean startMyService() {
        Intent serviceIntent = new Intent();
        serviceIntent.setComponent(new ComponentName(getSelfContext(), MyService.class));
        return getSelfContext().startService(serviceIntent) != null;
    }

    // 停止普通后台服务(可选)
    public boolean stopMyService() {
        Intent serviceIntent = new Intent();
        serviceIntent.setComponent(new ComponentName(getSelfContext(), MyService.class));
        return getSelfContext().stopService(serviceIntent);
    }
}

2. 新增 MyPluginRegistry 文件,继承于 PluginRegistry: ​

java
public class MyPluginRegistry extends PluginRegistry {

    static {
        PluginLoader loader = new PluginLoader() {
            @Override
            public Plugin load(Context context, Context selfContext, Object runtime, Object topLevelScope) {
                return new PluginHelloWorld(context, selfContext, runtime, topLevelScope);
            }
        };

        // 供 $plugins.load("org.botjs.plugin.sdk.demo") 使用,注册名要与加载名一致
        registerPlugin("org.botjs.plugin.sdk.demo", loader);

        // 可选:同时注册为默认插件,供默认加载路径使用
        registerDefaultPlugin(loader);
    }
}

注册名必须与加载名一致

$plugins.load(name) 最终按 PluginRegistry.load(name, ...) 查找,只匹配通过 registerPlugin(name, loader) 注册的 loader。registerDefaultPlugin(loader) 注册的是 "::default",只影响默认加载路径,不会让 $plugins.load("org.botjs.plugin.sdk.demo") 命中。如果只调用 registerDefaultPlugin,宿主应走默认加载路径,或补上 registerPlugin("org.botjs.plugin.sdk.demo", loader)。

在 AndroidManifest.xml 中配置以下 meta-data,name 为 "org.botjs.plugin.sdk.registry",value 为 MyPluginRegistry 的完整类名。

xml
<application
        ...>

        <meta-data
            android:name="org.botjs.plugin.sdk.registry"
            android:value="org.botjs.plugin.sdk.demo.MyPluginRegistry" />

        <activity
            android:name=".HelloWorldActivity"
            android:exported="true" />

        <service
            android:name=".HelloworldPluginService"
            android:exported="true" />

        <service
            android:name=".MyService"
            android:exported="false" />
    </application>

3. 编写 JavaScript 胶水层 ​

在 assets 的相应目录(由 Plugin.getAssetsScriptDir() 返回)中添加 index.js 文件,用于作为胶水层导出插件 API。

js
module.exports = function (plugin) {
    let runtime = plugin.runtime;
    let scope = plugin.topLevelScope;

    function helloWorld() {
    }

    // 纯数据属性,调用时不要加括号
    helloWorld.getAssetsScriptDir = plugin.getAssetsScriptDir();
    helloWorld.stringFromJava = plugin.getStringFromJava();

    // 方法
    helloWorld.say = function (message) {
        plugin.say(message);
    };

    helloWorld.remoteAdd = function (a, b) {
        return plugin.waitForConnection().call('add', {
            a: a,
            b: b
        }, null).get('sum');
    };

    helloWorld.startMyService = function () {
        return plugin.startMyService();
    };

    helloWorld.stopMyService = function () {
        return plugin.stopMyService();
    };

    return helloWorld;
};

说明:plugin.runtime / plugin.topLevelScope 在 Rhino 中可直接按属性访问(等价于 getRuntime() / getTopLevelScope()),示例中未使用可省略。

4. 在 Bot.js Pro 中调用 ​

编译插件为 apk(assembleDebug / assembleRelease),安装到设备上。在 Bot.js Pro 中使用以下代码调用:

js
let helloWorld = $plugins.load("org.botjs.plugin.sdk.demo");
console.log(helloWorld.stringFromJava);
helloWorld.say("Hello, Bot.js Pro Plugin");

5. 独立服务 AIDL 方式调用 ​

可以在插件中编写一个 Service,由 Bot.js Pro 唤起和绑定,并可在 js 中通过 AIDL 调用 Service 的接口。

在 Plugin 中重写方法 getService。

java
// 插件服务类,可选,用于 AIDL 方式和 Bot.js Pro 本体通信。可返回 null
@Override
public ComponentName getService() {
    return new ComponentName(getSelfContext().getPackageName(), HelloworldPluginService.class.getName());
}

新建一个 Service 组件(注意在 AndroidManifest 中注册时需要是 exported="true"),继承于 PluginService。

java
public class HelloworldPluginService extends PluginService {
    private static final String ACTION_ADD = "add";

    @Override
    protected Result onRemoteCall(@NonNull String action, @NonNull Map<String, Object> args, @Nullable RemoteCallback callback) throws RuntimeException {
        switch (action) {
            case ACTION_ADD:
                return invokeAdd(args);
        }
        return Result.notImplemented(action);
    }

    private Result invokeAdd(Map<String, Object> args) {
        Number a   = PluginUtils.getNotNull(args, "a");
        Number b   = PluginUtils.getNotNull(args, "b");
        double sum = a.doubleValue() + b.doubleValue();
        return new Result(Collections.singletonMap("sum", sum));
    }
}

在 index.js 中添加胶水层代码:

js
helloWorld.remoteAdd = function (a, b) {
    return plugin.waitForConnection().call('add', {
        a: a,
        b: b
    }, null).get('sum');
};

然后可以在 Bot.js Pro 中调用:

js
let helloWorld = $plugins.load("org.botjs.plugin.sdk.demo");
console.log(helloWorld.remoteAdd(1, 2));

6. 普通后台服务启停(可选) ​

除了 AIDL 方式,插件也可以启动/停止一个普通后台服务。新建 MyService 继承于 PluginService(onBind 返回 null 即不对外提供 Binder 调用):

java
public class MyService extends PluginService {

    private static final String TAG = "MyService";

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: 服务已创建");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand: 服务已启动");
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy: 服务已销毁");
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    protected Result onRemoteCall(@NonNull String var1, @NonNull Map<String, Object> var2, @Nullable RemoteCallback var3) throws RuntimeException {
        return null;
    }
}

在 PluginHelloWorld 中通过 startService / stopService 控制:

java
public boolean startMyService() {
    Intent serviceIntent = new Intent();
    serviceIntent.setComponent(new ComponentName(getSelfContext(), MyService.class));
    return getSelfContext().startService(serviceIntent) != null;
}

public boolean stopMyService() {
    Intent serviceIntent = new Intent();
    serviceIntent.setComponent(new ComponentName(getSelfContext(), MyService.class));
    return getSelfContext().stopService(serviceIntent);
}

在 index.js 中导出:

js
helloWorld.startMyService = function () {
    return plugin.startMyService();
};

helloWorld.stopMyService = function () {
    return plugin.stopMyService();
};

在 Bot.js Pro 中调用:

js
let helloWorld = $plugins.load("org.botjs.plugin.sdk.demo");
console.log(helloWorld.startMyService());
console.log(helloWorld.stopMyService());

返回值只表示 startService() / stopService() 调用是否成功,不代表服务已进入运行状态或已完全停止。


完整调用示例 ​

将上面所有能力串起来,一个完整的调用脚本如下:

js
// ============================================================
// Bot.js Pro 插件完整调用示例
// 对应插件包名: org.botjs.plugin.sdk.demo
// ============================================================

// 1. 加载插件,返回 index.js 胶水层导出的 API 对象
let helloWorld = $plugins.load("org.botjs.plugin.sdk.demo");
console.log("插件加载成功");

// 2. 读取由 Java 层提供的纯数据属性(不要加括号)
console.log("assets 目录: " + helloWorld.getAssetsScriptDir);
console.log("Java 字符串: " + helloWorld.stringFromJava);

// 3. 调用 Java 方法(会在设备上弹出 HelloWorldActivity)
helloWorld.say("Hello, Bot.js Pro Plugin");

// 4. AIDL 远程调用:调用插件 Service 的 "add",同步返回结果
let sum = helloWorld.remoteAdd(10, 32);
console.log("remoteAdd(10, 32) = " + sum);

// 5. 启动 / 停止普通后台服务 MyService
console.log("startMyService() = " + helloWorld.startMyService());
console.log("stopMyService() = " + helloWorld.stopMyService());

console.log("所有插件 API 调用示例执行完毕");

预期控制台日志:

插件加载成功
assets 目录: js-plugin
Java 字符串: Hello, Bot.js Pro!
remoteAdd(10, 32) = 42
startMyService() = true
stopMyService() = true
所有插件 API 调用示例执行完毕

say() 会启动 HelloWorldActivity,该界面不会出现在控制台日志中。


注意事项 ​

  1. 注册名必须与加载名一致:$plugins.load(name) 只匹配 registerPlugin(name, loader);registerDefaultPlugin(loader) 注册的是 "::default",只影响默认加载路径。
  2. 需要 AIDL 调用时要重写 getService():Plugin.getService() 基类默认返回 null,不重写就不会建立 Binder 连接,waitForConnection() 也会一直等待。
  3. 跨进程参数只能传 Parcel 支持的类型:基本类型、String、Bundle、Parcelable 等,不要传 JS 函数、不可序列化对象或超大 Map。
  4. Service 安全:同应用/同 UID 可将 Service 设为 android:exported="false";跨应用宿主需要 bind 时,通常需要 android:exported="true" 并加签名级权限,同时在 Binder 调用入口校验调用方 UID。
  5. 线程模型:onRemoteCall 运行在 Binder 线程,不能做 UI 操作或长耗时任务;Connection.call() 是同步阻塞调用,不要在高频循环中调用。