Skip to content

Application 插件开发

Application 是容器的能力扩展。它可以提供新能力(TTS、语音识别)、覆盖容器内置服务、覆盖 UI 页面。和 Friend 不同,Application 可以包含可执行代码。

这一页讲怎么写一个 Application 插件,包含 manifest 字段、入口文件、安全约束和完整示例。

my-app.moodapp (ZIP)
├── manifest.json
└── main.dart

manifest.json 里 entry 指向 main.dart,language 声明 dart。这就是一个能装的 Application——虽然它什么都不做,但容器能加载它、enable/disable 它。

以一个 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 指向插件入口,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 文件,容器通过子进程方式运行它,用 stdin/stdout 通信(具体协议见容器源码)。

# main.py — 简化的 Python Application 入口
import json
import sys
import 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()

入口是一个可执行二进制文件,容器直接 spawn 它,通信协议同 python。

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 让其他插件和容器能发现本插件提供什么:

"provides": {
"triggers": ["on_reply"],
"actions": ["speak"],
"interfaces": ["tts"]
}
字段 说明
triggers 响应哪些事件(on_reply = 伙伴回复后触发)
actions 提供哪些动作(其他插件可调用)
interfaces 实现哪些接口

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 可以执行代码,所以市场有基本安全措施:

  1. PR 验证脚本做基础扫描:scripts/validate.py 会扫描 Application 的 entry 文件,匹配可疑模式(os.system、subprocess shell=True、curl|sh、远程代码 eval 等)。命中则 CI 红叉。

  2. 人工 review:CI 只挡低级错误,真正的安全靠 PR review。合并前人工看代码。

  3. 权限声明:requirements.permissions 声明需要的系统权限(internet / microphone 等)。容器在 enable 时可以提示用户。

  4. 沙箱运行时(规划中):未来 Application 会在受限沙箱里跑,限制文件系统和网络访问。目前还没有实现。

  • 不要偷偷发请求到不明服务器。网络调用只往你 manifest 里声明的 API 端点发。
  • 不要读用户私人文件。只读插件自己目录里的文件和容器给你的 config。
  • 不要 eval / exec 远程拿到的字符串。
  • 在 README / description 里说清楚这个插件会联网做什么。
com.example.app.elevenlabs_tts.moodapp (ZIP)
├── manifest.json
├── main.dart
└── icon.png

manifest.json:见上方。

main.dart:见上方 entry 章节。

icon.png:512×512 插件图标。

Terminal window
cd my-app
zip -r ../com.example.app.elevenlabs_tts.moodapp .
# 验证
unzip -l ../com.example.app.elevenlabs_tts.moodapp
# manifest.json / main.dart / icon.png 应在根目录

和 Friend 一样,ZIP 根目录直接放文件,不要套一层目录。

Terminal window
git clone https://github.com/Tangmjiu/moodpet-plugin-market
cd moodpet-plugin-market
cp /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。