Application 插件开发
Application 插件开发
Section titled “Application 插件开发”Application 是容器的能力扩展。它可以提供新能力(TTS、语音识别)、覆盖容器内置服务、覆盖 UI 页面。和 Friend 不同,Application 可以包含可执行代码。
这一页讲怎么写一个 Application 插件,包含 manifest 字段、入口文件、安全约束和完整示例。
最小可用 Application
Section titled “最小可用 Application”my-app.moodapp (ZIP)├── manifest.json└── main.dartmanifest.json 里 entry 指向 main.dart,language 声明 dart。这就是一个能装的 Application——虽然它什么都不做,但容器能加载它、enable/disable 它。
manifest.json
Section titled “manifest.json”以一个 ElevenLabs TTS 插件为例:
{ "id": "com.example.app.elevenlabs_tts", "type": "application", "name": "ElevenLabs TTS", "description": "用 ElevenLabs 给伙伴加上自然语音", "version": "1.0.0", "author": "你的名字", "icon": "icon.png", "system": false, "uninstallable": true, "defaultEnabled": false, "platforms": { "supported": ["android", "windows", "linux", "macos"], "optimized": ["android"] }, "entry": "main.dart", "language": "dart", "overrides": { "services": { "tts": true } }, "provides": { "triggers": ["on_reply"], "actions": ["speak"], "interfaces": ["tts"] }, "dependencies": { "minContainerVersion": "1.0.0", "services": [] }, "requirements": { "permissions": ["internet"], "minPlatformVersion": { "android": "8.0" }, "runtime": { "dart": ">=3.0" } }, "resources": { "minRam": 64, "minDisk": 5 }, "configSchema": { "apiKey": { "type": "string", "label": "ElevenLabs API Key", "secret": true }, "voice": { "type": "select", "default": "Rachel", "label": "语音角色", "options": ["Rachel", "Domi", "Bella", "Antoni"] }, "speed": { "type": "number", "default": 1.0, "label": "语速" } }}字段含义见 manifest.json 规范。这里讲实际开发要点。
entry 入口文件
Section titled “entry 入口文件”entry 指向插件入口,language 声明实现语言。容器按 language 选运行时:
入口是一个 Dart 文件,容器通过 Flutter 的 plugin 运行时加载它。入口文件需要导出一个插件注册函数(具体运行时协议见容器源码 lib/core/plugin/application_runtime.dart)。
// main.dart — 简化的 Application 入口示例import 'package:http/http.dart' as http;import 'dart:convert';
/// ElevenLabs TTS 插件。////// 容器在 enable 后调用 [speak],把伙伴的回复文字转成语音播放。class ElevenLabsTtsPlugin { final String apiKey; final String voice; final double speed;
ElevenLabsTtsPlugin({ required this.apiKey, required this.voice, required this.speed, });
/// 把文字转成语音并播放。 /// /// 调用 ElevenLabs 的 text-to-speech API,返回音频 bytes。 /// 容器负责播放。 Future<List<int>> speak(String text) async { final response = await http.post( Uri.parse('https://api.elevenlabs.io/v1/text-to-speech/$voice'), headers: { 'xi-api-key': apiKey, 'Content-Type': 'application/json', }, body: jsonEncode({ 'text': text, 'model_id': 'eleven_multilingual_v2', 'voice_settings': {'speed': speed}, }), ); if (response.statusCode != 200) { throw Exception('ElevenLabs API 错误: ${response.statusCode}'); } return response.bodyBytes; }}python
Section titled “python”入口是一个 Python 文件,容器通过子进程方式运行它,用 stdin/stdout 通信(具体协议见容器源码)。
# main.py — 简化的 Python Application 入口import jsonimport sysimport urllib.request
def speak(text: str, api_key: str, voice: str) -> bytes: """把文字转成语音,返回音频 bytes。""" req = urllib.request.Request( f"https://api.elevenlabs.io/v1/text-to-speech/{voice}", data=json.dumps({"text": text}).encode(), headers={"xi-api-key": api_key, "Content-Type": "application/json"}, ) with urllib.request.urlopen(req) as resp: return resp.read()
# 容器通过 stdin 发 JSON 指令,插件通过 stdout 返回 JSON 结果for line in sys.stdin: cmd = json.loads(line) if cmd["action"] == "speak": audio = speak(cmd["text"], cmd["apiKey"], cmd["voice"]) # 实际实现需要把 bytes 编码成 base64 返回 print(json.dumps({"status": "ok", "audioSize": len(audio)})) sys.stdout.flush()binary
Section titled “binary”入口是一个可执行二进制文件,容器直接 spawn 它,通信协议同 python。
overrides — 覆盖容器服务
Section titled “overrides — 覆盖容器服务”overrides.services 声明接管哪些内置服务。比如 "tts": true 表示这个插件启用后,容器的内置 TTS 停止贡献,由这个插件代替:
"overrides": { "services": { "tts": true }}可覆盖的服务名(见容器源码的 service registry):
| 服务名 | 说明 |
|---|---|
tts |
文字转语音 |
memory |
对话记忆/摘要 |
agent |
LLM agent 调度 |
renderer |
伙伴渲染 |
overrides.ui 声明接管哪些 UI 页面:
"overrides": { "ui": { "home_page": true }}接管后原页面由这个插件提供的版本替代。一个服务/页面同时只能被一个启用的 Application 接管——多个插件声明覆盖同一个服务时,最后 enable 的那个生效。
provides — 声明能力
Section titled “provides — 声明能力”provides 让其他插件和容器能发现本插件提供什么:
"provides": { "triggers": ["on_reply"], "actions": ["speak"], "interfaces": ["tts"]}| 字段 | 说明 |
|---|---|
triggers |
响应哪些事件(on_reply = 伙伴回复后触发) |
actions |
提供哪些动作(其他插件可调用) |
interfaces |
实现哪些接口 |
configSchema — 用户配置项
Section titled “configSchema — 用户配置项”Application 经常需要用户配置 API key 之类的参数。configSchema 声明后,容器的插件设置 UI 会自动生成表单:
"configSchema": { "apiKey": { "type": "string", "label": "ElevenLabs API Key", "secret": true }, "voice": { "type": "select", "default": "Rachel", "label": "语音角色", "options": ["Rachel", "Domi", "Bella", "Antoni"] }}secret: true 的字段在 UI 里掩码显示,持久化到安全存储。插件运行时通过容器提供的 config 接口读取用户设的值。
Application 可以执行代码,所以市场有基本安全措施:
-
PR 验证脚本做基础扫描:
scripts/validate.py会扫描 Application 的entry文件,匹配可疑模式(os.system、subprocess shell=True、curl|sh、远程代码 eval 等)。命中则 CI 红叉。 -
人工 review:CI 只挡低级错误,真正的安全靠 PR review。合并前人工看代码。
-
权限声明:
requirements.permissions声明需要的系统权限(internet/microphone等)。容器在 enable 时可以提示用户。 -
沙箱运行时(规划中):未来 Application 会在受限沙箱里跑,限制文件系统和网络访问。目前还没有实现。
写 Application 时的安全自律
Section titled “写 Application 时的安全自律”- 不要偷偷发请求到不明服务器。网络调用只往你 manifest 里声明的 API 端点发。
- 不要读用户私人文件。只读插件自己目录里的文件和容器给你的 config。
- 不要
eval/exec远程拿到的字符串。 - 在 README / description 里说清楚这个插件会联网做什么。
完整示例:一个 TTS 插件包
Section titled “完整示例:一个 TTS 插件包”com.example.app.elevenlabs_tts.moodapp (ZIP)├── manifest.json├── main.dart└── icon.pngmanifest.json:见上方。
main.dart:见上方 entry 章节。
icon.png:512×512 插件图标。
cd my-appzip -r ../com.example.app.elevenlabs_tts.moodapp .
# 验证unzip -l ../com.example.app.elevenlabs_tts.moodapp# manifest.json / main.dart / icon.png 应在根目录和 Friend 一样,ZIP 根目录直接放文件,不要套一层目录。
git clone https://github.com/Tangmjiu/moodpet-plugin-marketcd moodpet-plugin-marketcp /path/to/com.example.app.elevenlabs_tts.moodapp application/cp /path/to/com.example.app.elevenlabs_tts.meta.json application/cp /path/to/com.example.app.elevenlabs_tts.png application/python scripts/validate.py验证脚本会额外扫描 Application 的 entry 文件做安全检查。如果你的 entry 里用了 os.system 之类,会报 suspicious pattern。