扩展与部署
开发插件
编写插件清单、声明工具与权限,测试文件输入和输出。
对应版本 1.9.8 · 2026-10-05
一个插件目录包含清单,以及本地脚本或 MCP 服务的连接配置。先声明工具、输入输出、权限与凭据,再在本地测试,最后打包分发。
先决定一件事:脚本还是 MCP#
判据只有一条:对方有没有现成的 MCP 服务。
有就别写脚本。再写一层把 stdin 的 JSON 翻成一次 HTTP 调用、再把结果翻回 stdout,是在重新 实现一个已经存在的东西,而且每加一个端点都要改代码。接 MCP 只要一份清单,零行代码 —— TikHub 就是这么接的:它的清单里只写了 MCP 服务的地址和凭据。
没有,或者你要做的是本地计算(读文件、算东西、调一个私有 HTTP 接口),那就写脚本。 下面讲的是这一种。
五分钟:一个能跑的插件#
建一个目录,两个文件:
my-plugin/
mosael.plugin.json
main.py清单:
{
"id": "dev.yourname.my-plugin",
"name": "我的插件",
"version": "0.1.0",
"runtime": { "kind": "process", "entry": "main.py" },
"tools": {
"expose": "all",
"declare": [
{ "name": "shout", "description": "把一段文字变成全大写。", "read_only": true }
]
}
}脚本 —— stdin 一个 JSON 进,stdout 一个 JSON 出:
import json, sys
def shout(payload):
return {"text": str(payload.get("text", "")).upper()}
TOOLS = {"shout": shout}
request = json.loads(sys.stdin.read()) # {"tool": "shout", "input": {...}}
try:
output = TOOLS[request["tool"]](request.get("input") or {})
json.dump({"ok": True, "output": output}, sys.stdout, ensure_ascii=False)
except Exception as exc:
json.dump({"ok": False, "error": str(exc)}, sys.stdout, ensure_ascii=False)把这个目录放进插件目录(应用的插件页会告诉你它在哪 —— 别照着 ~/.mosael/plugins
去找,Windows 上那个路径不存在),点「扫描」。它就出现在列表里了。
id 用反写域名式的写法。它是这台机器上的唯一键:装进来的目录按它命名,重名会被当成
同一个插件。
规矩#
- 进程 60 秒超时,stdout 上限 1MB,
output必须是个对象。 - 崩了、超时了、吐了非 JSON —— 失败的是那一次调用记录,不是应用。
- 每次调用都留痕(输入、输出、耗时),在插件页看得到。
- 失败时
error的第一行写给人看的那句话(出了什么事、下一步怎么办),异常、errno、对方回的原文换行写在后面:界面把第一行当正文, 其余收进「详情」或悬停说明。比如第一行「连不上这台服务器,确认它在运行、地址填对」,第二行才是地址和[Errno 61] Connection refused。 失败原因会被存下来(插件页连接卡片上的出错原因就是上一次刷新时你说的那句,而那次刷新可能是另一种界面语言、或后台刷的), 所以两种语言都写得出就都交:"error": {"zh": "…", "en": "…"},宿主原样存着、给人看时按读的人挑;只交字符串的照旧原样显示。
要参数、要凭据#
在清单里声明,用户在插件页填,运行时以环境变量注入。声明成 credentials 的加密存,
声明成 config 的明文存:
"instance": {
"credentials": [
{ "key": "MY_API_KEY", "label": "API Key", "required": true, "help": "在 xxx 后台生成。" }
],
"config": [
{ "key": "MY_REGION", "label": "区域", "type": "enum", "required": false,
"options": [{ "value": "cn", "label": "国内" }, { "value": "us", "label": "美国" }] }
]
}key = os.environ["MY_API_KEY"]配置项的 type 可以是 string(默认)、enum、number、boolean,以及两种一段代码:json(代码编辑器,
保存前校验,错在第几行第几列当场说)和 code(代码编辑器,language 决定高亮)。一段 JSON 别写成多行文本框 ——
那样用户只看得见第一行,少一个逗号要等插件跑起来才知道。值照旧是字符串,你自己解析。help 里的占位符用反引号
写成行内代码。
环境注入与系统权限是两回事。 子进程只接收基础环境和自己声明的凭据,Mosael 不主动传入应用的模型密钥或会话令牌。但本地插件以当前用户权限运行,并不是操作系统沙箱;它仍可能读取该用户有权访问的文件。请明确声明所需权限,并避免在日志中输出凭据。
网络和镜像别自己声明。 走哪个代理由每个连接的「网络」一行决定,宿主把 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY
注入你的进程,照库的默认读环境变量就行;要装依赖的插件在清单顶层写 "package_sources": ["pypi"](或 "npm"),宿主按连接上
「PyPI 镜像 / npm 镜像」的选择注入 PIP_INDEX_URL、UV_DEFAULT_INDEX 或 npm_config_registry。这些名字、PATH 和 MOSAEL_* 都归宿主,
配置或凭据声明同名的键,装的时候就报错。
要交出一个文件#
上面那条路只搬 JSON,1MB 封顶 —— 一个 2GB 的 mp4 塞不进去。要把文件交给素材库,在
output 里放 artifact,有两种交法:
# 一、你自己下好了。必须写在给你的目录里
out = os.environ["MOSAEL_PLUGIN_OUTPUT_DIR"]
path = os.path.join(out, "video.mp4")
download_to(path)
return {"artifact": {"path": "video.mp4"}}
# 二、你只换到了下载凭据,让宿主去下
return {"artifact": {
"url": "https://.../dlink?sign=...",
"headers": {"User-Agent": "..."}, # 有些接口不带特定头就 403
"filename": "video.mp4",
}}第二种通常更好。 让插件负责换取凭据、宿主负责搬字节 —— 进度、取消、重试、失败隔离 全是现成的,你一行都不用写。而且你这一侧只有一次 60 秒的调用:自己下一个大文件必然超时, 就算不超时,用户也看不到进度、按取消也停不下来。
宿主收下之后,artifact 会被换成 asset_id,调用方拿到的就是一个素材 id,和别的
产素材的工具一样。百度网盘插件走的就是第二种。
一次交出几份,就写 artifacts(一串,每一项和 artifact 同一种写法,最多 64 份):宿主全部收进素材库,返回值里
换成 assets(每份的 asset_id、名字,以及你在那一项上附带的标量说明)和 asset_ids,第一份同时记成 asset_id。
要收一个文件#
反过来:你的工具要处理一份已有的素材 —— 传到网盘、发给外部服务转码。在
input_schema 里给那个字段标上 "format": "asset":
{ "name": "upload",
"input_schema": {
"type": "object",
"properties": {
"asset_id": { "type": "string", "format": "asset" },
"path": { "type": "string" }
},
"required": ["asset_id", "path"]
} }调用方传素材 id,你收到的是一个本地绝对路径:
local = payload["asset_id"] # 已经是路径,不是 id
upload_to_somewhere(local)你不知道素材库存在,也不需要知道。
给的是副本,不是库里那一份 —— 你改坏了或删掉了都伤不到用户的素材。调用结束即删,
所以别往那个路径写你想留下的东西(想留下就用 artifact 交回去)。
一次要几份:{"type": "array", "items": {"type": "string", "format": "asset"}},收到的是一串路径。
为什么不让你自己去取:你的环境里没有数据库、没有 API 令牌、没有媒体目录 —— 那是隔离边界 的一部分,不是疏漏。
范例见百度网盘插件的 pan_upload。
再给那个字段两句提示,宿主替你做掉一半的活:
"x-media": "document"或["audio", "video"]:限定收哪几种素材,表单只列这几种,交错了宿主当场说清。"x-audio": "original":宿主先把声音抽成原采样率的 wav 再给你(降噪、分离这类要进成片的);"speech"是 16k 单声道 wav (识别模型的输入)。你不用自己带 ffmpeg。
字段装的是另一个系统里的编号(ComfyUI 的任务号、网盘的 fs_id、对象存储的路径、翻页游标)时,标 "format": "external_id"。
运行时什么都不换,它只是一句声明:这种工具不上画板 —— 创作者在画板上填不出一个任务号 —— 在工作流和对话里照常用。
后果与确认:effects#
智能体直接调你的工具之前要不要先问用户,由工具的 effects 决定:
effects | 意思 | 智能体调它时 |
|---|---|---|
none | 不花钱、不出门:只读,或者只往 Mosael 里写(收进素材库之类) | 直接跑 |
paid | 会花钱或占付费算力 | 先开确认卡(AI 成本档) |
external | 后果在 Mosael 之外、撤不回:传到别人的服务器、对外发送 | 先开确认卡(对外生效档) |
local-code | 在这台电脑上执行一段调用方写的代码 | 先开确认卡,卡上写明「会在你的电脑上运行代码」 |
默认是 external —— 不只读、也没写 effects 的工具一律先问。read_only: true 就是 none,再写别的 effects 装的时候就报错。
可以用 tools.default_effects 给整个包定一个默认,MCP 插件在 tools.overrides 里按名字写。人自己点的(插件页「试一下」、工作流里的插件节点、
画板上自己点运行)不开卡。插件页和市场里,有后果的工具旁标着「需确认」。
要记住一点东西#
插件进程是无状态的:环境变量进、JSON 出,跑完就没了,你自己没有任何写回的手段。对纯计算没问题,对要续期的凭据是个死结 —— 拿 refresh_token 换一个新的 access_token 很容易,难的是换完之后没地方放。
在响应里放 state(和 output 平级):
json.dump({
"ok": True,
"output": {"files": [...]}, # 回给调用方
"state": {"MY_ACCESS_TOKEN": "新换的"}, # 宿主替你记住
}, sys.stdout, ensure_ascii=False)下一次调用它就在环境变量里了 —— 还是 os.environ["MY_ACCESS_TOKEN"],你不需要知道
这个值是上次自己存的。存在哪儿由清单决定:声明成 credentials 的进加密库,声明成 config
的进明文配置。
和 output 平级不是随手放的:output 会交给调用方和模型,而刚续出来的令牌不该出现
在那里。
只能写清单里声明过的键。写了没声明的键直接失败,不是忽略 —— 忽略的话你以为存下了, 下次拿到旧值,而错误表现在几十分钟后的另一个地方。
令牌作废了就明说。 声明了 oauth 的插件,续不出新令牌(refresh_token 被拒、续过一次还是过期)时,
在失败响应里加一句 "reauthorize": true:插件页上这个连接会标成「需要重新授权」,按钮变成「重新授权」。
报错那句话只写在调用记录里,而用户看的是连接卡片。文件不存在、限流这类与令牌无关的失败别带它。
要跑久一点、要留下大东西#
一次调用默认最多跑 60 秒。确实要更久的工具(渲染一段视频),在 declare 里给它写
timeout_seconds,上限 1800;写错(不是正数)就当没写。智能体那一侧单次工具调用最多等
180 秒,所以要给智能体用的工具请压在这以内 —— 更重的准备工作拆成单独一步。
插件目录不能当存储:更新插件就是把整个目录换掉。要攒下很贵的东西(几百 MB 的依赖、
下载好的模型),写进 MOSAEL_PLUGIN_DATA_DIR:每个插件一份,跨调用、跨更新都在,卸载时
随插件一起删。
{ "name": "render", "timeout_seconds": 170, "input_schema": { … } }范例见 Remotion 插件:依赖装在持久目录,渲染工具声明了自己的预算。
跑几分钟的工具再加一条 "stream": true:stdout 改成一行一个 JSON,进度写成
{"event": "progress", "progress": 0.4, "message": "第 8/20 步"},最后一行照旧是结果。在工作流里跑时,
执行面板上看得到这句进度;任务被取消时 Mosael 先建一个取消文件(MOSAEL_PLUGIN_CANCEL_FILE),你去停掉远端的活
再退出,而不是被直接杀掉。给智能体用、又不用确认的长活,做成「只提交、交回任务号」+「按任务号取回」两个工具;
要确认的(花钱、对外、在本机跑代码)是开卡、批准后在后台跑,不占智能体那一次调用的等待。
替 Mosael 做事:生成与能力#
插件还可以当一家生成供应商:在清单里写 "provides": ["generation"],让一个工具认领它。
Mosael 会调这个工具两种操作 —— models(这个连接有哪些模型、各收什么参数、有哪些参考图槽位)和
generate(做一次:收提示词、参数和参考图文件,一行一个事件地说进度、交回远端任务号,最后交回产出文件)。
取消时 Mosael 建一个取消文件(路径在 MOSAEL_PLUGIN_CANCEL_FILE),你看到它就去停远端的活。
这个工具只给 Mosael 自己调,不出现在智能体和工作流里;预算不写是 1 小时,最长可以声明 6 小时。
模型清单里再给一个 fingerprint(清单的指纹),并支持一个便宜的 op: "fingerprint",Mosael 就每分钟问一次,
变了才重新拉清单 —— 用户在你那边新加的模型不用手动刷新就出现。参数的 title 可以按语言写
({"zh": "步数", "en": "Steps"}),Mosael 给人看时再挑。
完整的协议和字段见仓库里的 docs/PLUGIN_MANIFEST.md「替宿主做生成」,随应用发的 ComfyUI 插件
(plugins/bundled/comfyui)是完整的范例。每个模型还可以声明提示词 prompt: "required" | "optional" | "none":放大、抠图这类
不吃提示词的写 none,界面上就不摆提示词框。
参数上几个值得知道的写法:
- 尺寸只是推荐、任意「宽x高」都收时,
size写examples(推荐的几档)、minimum(每边下限)和multipleOf(你会取整到的倍数), 界面上就是能手填的尺寸下拉;只能从几档里挑才写enum。 - 选模型文件的那一格写
x-model-folder(checkpoints、loras……):连接还认领了模型库时,表单从模型库取缩略图、底模和触发词。 - 尺寸、分辨率、时长、有声这几个宿主有控件的参数上写了
default,调用方没给时宿主照它发;其余参数用户没动过就不发。 - 一次请求里每份产出实际用的参数不一样时(比如每张换一个种子),在那一份上写
parameters,宿主记进那份素材的生成参数; 再交一句note(如「3 张里出了 2 张,第 2 张没出来:……」),它是这次任务的最后一句话。
运行时报出工具:写 "provides": ["tools"],插件在运行时报出自己的工具(ComfyUI 每张工作流一个工具就是这么来的),和清单里写的工具一样进
工具表、节点面板和智能体,目录变了自动刷新。报出的工具可以声明 replaces(它取代了哪个老工具,存着的老节点自动改写过去)和 mirrors
(它和某个生成模型是同一件事,画板上只留生成那一个入口)。
在宿主的能力表里登记。 宿主有几项能力可以交给插件做,和本机引擎并列出现在「设置 → 能力提供方」、节点的引擎下拉、剪辑页、素材库和智能体里:
provides | 做什么 | 入参要按契约写 |
|---|---|---|
document_parse | 把一份文档解析成 Markdown(结果存成那份文档的一次解析) | file:format: asset,x-media: document |
audio_denoise / audio_separation | 降噪 / 拆成人声与背景音 | file:音频或视频,x-audio: original |
transcription | 语音转逐字稿 | file:音频或视频,x-audio: speech |
translation | 把一批文字译成目标语言 | texts(数组)和 target |
speech | 用一个音色念文字 | op:voices 列音色、speak 念一段 |
public_url | 把本地素材变成限时公网链接 | 收 asset_id、expires,交回 url |
包上和一个工具上都写同一项 provides(只给进程形态的插件)。认领这些能力的工具照样是一个普通工具:在插件页的工具表里、能勾,智能体和
工作流也能直接调,行上多一枚能力徽标、展开看得到「用在哪」。宿主不会自动选插件 —— 用户在「能力提供方」里设成默认、或在用到的地方点名才用。
每项的出参和细节见 docs/PLUGIN_MANIFEST.md「声明我能替宿主做成什么」,范例是随应用发的 plugins/bundled/mineru(文档解析)和
plugins/bundled/object-storage(素材外链)。
给连接带来目录。 插件连的是一台装着东西的服务器时,还可以认领两项目录类能力(认领它的工具只给宿主调,不进工具表):
model_library:列出那台服务器上的模型文件(目录、大小、底模、触发词、哪些工作流在用、工作流缺的),解析下载链接,把模型下到它上面。 连接标题行多一颗「打开模型库」。workflow_library:列出存着的工作流(节点图摘要、能填什么、用到的模型、缺的节点),复制、改名、挪进回收目录、恢复,导入别处的工作流, 经节点管理器装缺的节点包、重启。连接标题行多一颗「打开工作流库」。
两项都不许覆盖、不许硬删:撞名回建议名,删除挪进回收目录。协议见 docs/PLUGIN_MANIFEST.md 的 model_library、workflow_library 两节,范例是随应用发的 ComfyUI 插件。
让它进工作流#
工具默认就能在智能体里用。要让它同时成为一个工作流节点,在那条工具上加 node:
{ "name": "pull", "description": "拉一个文件",
"node": { "label": "从网盘导入", "outputs": ["asset_id", "asset_name"] } }outputs 里的键,下游节点就能从对应的口子接上。给工作流连线用、不该落到画板上的输出(素材 id 列表、摘要、任务号)写进
wiring_outputs,画板上就只落成品。
参数是「每一项是一块结构」的数组(比如讲解视频的每一步)时,在 items 里写出 properties,节点表单就是一项一张卡、每个字段分格填,
能加、删、上下挪 —— 不再是一个要手写 JSON 的框。参数和工具的名字都可以写成 {"zh": "…", "en": "…"},跟着界面语言显示。
写说明#
在插件目录里放一个 README.md。它会原样渲染在插件详情页上(比如
百度网盘),所以按给人看的写,别当成内部备忘。
两个坑:相对链接按仓库目录写就好(渲染时会改指回仓库),但别指向不存在的文件;
<https://…> 这种写法在 GitHub 上没问题,写成 [链接](https://…) 更保险。
插件清单里还可以写 author(名字和链接,名字可以按语言写)和 docs(使用文档的地址):插件页和市场里看得到是谁写的,「文档」直接跳到
你那一页,而不是通用的插件指南。再写一句 summary(一句话说清它是干嘛的,纯文字、可以按语言写,140 字以内):市场卡片、
插件的页头和安装确认先摆它;长的介绍还是第一条技能的 description,在详情里折起来,点「展开」看全。
发出去#
插件市场的索引是一份普通 JSON,谁都能架 —— 包括公司内网:
{"plugins": [{
"id": "dev.yourname.my-plugin", "name": "我的插件", "version": "0.1.0",
"summary": "一句话", "description": "……", "author": "你", "homepage": "https://…",
"download": "https://…/my-plugin.zip",
"permissions": []
}]}download 指向一个 zip,里面是你那个目录(外面套一层也认 —— 从 GitHub 下下来的包总是
套一层的)。用户在应用里换成你这个索引地址,就能装了。
官方索引里还有一种条目:"bundled": true、没有 download —— 那是随 Mosael 一起发的插件
(ComfyUI)。它列在市场里是为了搜得到,不是为了装:应用里它标「内置」,跟着应用更新。
permissions 那一栏别写错。 用户是照着它决定装不装的,而装插件是在他机器上放一份
会被执行的代码。如实申报:插件升级时多申报了权限,用户已有的连接会先停用,卡片上写明多要了哪几项、等他一键授予 ——
所以要连哪些站、写哪些目录,第一版就写全,别等用到时才悄悄加。
