Declarative Plugin API 2

给故事增加能力,
不交出系统边界。

造梦外置插件使用声明式 JSON 描述能力,由 Kotlin 宿主执行经过审核的操作。无需编译代码,也不会运行插件包里的 Python、JAR 或脚本。

先选对扩展方式

绝大多数第三方插件,都应该从声明式开始。

进阶

内置 Kotlin 插件

需要新宿主能力或复杂业务编排时,在 kmp/plugins-api 实现稳定契约,并随应用一起发布。

需要修改源码 · 接受项目代码审查
兼容保留

旧 main.py 插件

应用可以检查和保存旧包,但不会执行其中的 Python 或其它任意代码。

请迁移到声明式插件

五分钟快速开始

创建一个“快捷接话”插件。

新建目录 my-quick-reply/,在里面放置以下 plugin.json。这个插件会读取当前聊天上下文,调用用户已经配置的模型,并把结果作为可编辑草稿返回。

my-quick-reply/plugin.json
{
  "id": "example-quick-reply",
  "name": "快捷接话",
  "version": "0.1.0",
  "apiVersion": "2",
  "description": "结合当前场景生成一句可编辑的回复草稿。",
  "defaultEnabled": false,
  "permissions": [
    "chat.context.read",
    "chat.draft.write",
    "model.invoke"
  ],
  "settings": [
    {
      "key": "tone",
      "title": "语气",
      "type": "enum",
      "default": "克制",
      "options": ["克制", "温柔", "直接"]
    }
  ],
  "contributes": {
    "chatActions": [
      {
        "id": "quick-reply",
        "title": "快捷接话",
        "placement": "composer",
        "icon": "sparkles"
      }
    ]
  },
  "execution": {
    "mode": "declarative",
    "chatActions": {
      "quick-reply": {
        "operation": "suggest",
        "direction": "结合当前场景生成下一句。语气:{{config.tone}}。草稿:{{seed_text}}"
      }
    }
  }
}
  1. 1
    压缩目录内容

    ZIP 根目录或 ZIP 内第一层目录必须包含 plugin.json

  2. 2
    在应用中检查

    打开“设置 → 插件”,选择 ZIP;确认名称、API 版本和权限。

  3. 3
    安装并启用

    安装后插件默认保持关闭。手动启用,再进入聊天的插件菜单。

  4. 4
    点击“快捷接话”

    生成结果只写入草稿,由用户确认后发送。

清单格式

plugin.json 是插件唯一的入口。

字段是否必填说明
id稳定且唯一;只能包含字母、数字、下划线和连字符,例如 acme-quick-reply
name插件管理和聊天菜单中显示的名称。
version建议插件版本,例如 0.1.0
apiVersion建议新插件写 2;当前宿主兼容 1/2,缺省按 1 处理。
description解释用途、数据范围和用户可见效果。
defaultEnabled第三方包安装后仍保持关闭,需用户显式启用。
permissions按能力最小权限集合;缺少所需权限的配方不会进入可执行态。
settings由宿主渲染的 booleanintegerenum 设置。
contributes向 UI 声明动作、生成增强器或临时 NPC 生成器。
executionmode 必须为 declarative,并为每个贡献点提供同 ID 配方。

贡献与执行必须成对。contributes.chatActions 声明了 quick-reply,就必须在 execution.chatActions.quick-reply 提供配方;ID 不一致会使插件不可执行。

模板与设置

用宿主管理的变量组合行为。

{{seed_text}}当前输入框里的草稿
{{direction}}动作调用时附带的方向
{{config.tone}}插件设置中的 tone
{{storage.notes}}插件独立数据区的 notes

设置值由宿主保存到插件自己的配置区,并通过 {{config.<key>}} 注入模板。不要把 API Key 或长期密钥放入普通设置;当前外置插件没有密钥保险箱能力。

贡献点

三种方式进入对话。

01

chatActions

显示在聊天输入区插件菜单中。适合生成草稿、候选回复、存取插件数据、网络查询和角色控制。

02

generationEnhancers

按会话开关,把规则注入后续主对话生成。关闭或停用插件后,宿主会清除残留规则。

03

temporaryNpcGenerators

结合当前场景生成临时角色,并由宿主安全写入当前会话的参与者与入场消息。

组合玩法规则

当玩法涉及关键词、回合数、概率或会话状态时,把规则放进 execution.rules。同一插件可包含多条规则,每条规则也可串联多个动作;状态只属于当前会话。

execution.rules
"permissions": ["chat.context.read", "generation.enhance", "model.invoke", "chat.state.write"],
"execution": {
  "mode": "declarative",
  "rules": [
    {
      "id": "merchant-arrives",
      "title": "神秘商人登场",
      "event": "before_generation",
      "match": {"everyTurns": 5, "chancePercent": 30},
      "actions": [
        {"type": "add_instruction", "instruction": "让一名神秘商人自然进入场景。"}
      ]
    },
    {
      "id": "remember-refusal",
      "title": "记录拒绝",
      "event": "after_turn",
      "match": {"keywords": ["拒绝", "不买"]},
      "actions": [
        {"type": "increment_state", "key": "refusals", "amount": 1}
      ]
    }
  ]
}

规则仍由 Kotlin 宿主解释执行。生成前规则可追加本轮指令;回合结束规则可设置或增减状态。同一回合重试不会重复累计,插件也不能借此运行脚本或访问任意文件。

生成增强器示例

contributes + execution
"permissions": ["chat.context.read", "generation.enhance", "model.invoke"],
"contributes": {
  "generationEnhancers": [
    {"id": "short-lines", "title": "短句模式", "icon": "short_text"}
  ]
},
"execution": {
  "mode": "declarative",
  "generationEnhancers": {
    "short-lines": {
      "rule": "每名角色每次最多说两句,保留自然停顿,不要压缩关键信息。"
    }
  }
}

临时 NPC 示例

contributes + execution
"permissions": ["chat.context.read", "chat.cast.write", "model.invoke"],
"contributes": {
  "temporaryNpcGenerators": [
    {"id": "visitor", "title": "引入来客", "icon": "person_add"}
  ]
},
"execution": {
  "mode": "declarative",
  "temporaryNpcGenerators": {
    "visitor": {
      "direction": "生成一名与当前冲突有关、动机明确但不过度抢戏的来客。",
      "notice": "一名新人物进入了场景。"
    }
  }
}

聊天动作操作

选择最小、最明确的宿主操作。

operation关键字段主要权限结果
suggestdirection上下文、草稿、模型一段建议草稿
variantsdirection上下文、草稿、模型多个候选回复
storage_getkeystorage.read读取插件文本数据
storage_setkeyvaluestorage.write写入插件文本数据
http_geturlheadersnetwork.access响应文本写入草稿
http_postbodynetwork.access响应文本写入草稿
reply_as_characterdirection人物、上下文、草稿、模型选择角色并生成草稿
mute_charactercharacterchat.cast.write当前会话角色禁言
unmute_charactercharacterchat.cast.write解除角色禁言

未知操作、空的必填字段、缺失权限或未在清单中声明的 action ID 都会被宿主拒绝。

权限模型

只声明配方实际需要的能力。

chat.context.read读取当前聊天的有界上下文
chat.draft.write把结果写入用户可编辑草稿
chat.cast.write修改当前会话角色或临时 NPC
generation.enhance影响后续主对话生成规则
chat.state.write写入当前会话内的插件规则状态
run.personas.read读取当前书卷已蒸馏人物摘要
model.invoke使用应用配置的模型能力
storage.read读取本插件独立数据区
storage.write写入本插件独立数据区
network.access通过宿主发起受限 HTTPS 请求

权限不是装饰字段。 安装时用户必须确认权限,运行时宿主会再次核对插件启用状态、贡献点 ID 和所需权限。不要为了“以后可能用到”申请额外能力。

打包、安装与调试

一个 ZIP,就是一个可分发插件。

Windows PowerShell

Compress-Archive -Path .\my-quick-reply\* -DestinationPath .\my-quick-reply.zip

macOS / Linux

(cd my-quick-reply && zip -r ../my-quick-reply.zip .)
  • ZIP 不超过 10 MiB,文件数不超过 500,解压后总量不超过 100 MiB。
  • 不包含绝对路径、.. 路径穿越或符号链接。
  • 更新同 ID 插件时,应用保留原来的配置、数据和日志;更新后需重新启用。
  • 在“设置 → 插件 → 日志与详情”查看宿主状态和最近日志。
  • 若显示“仅保存”,检查 execution.mode、API 版本、权限和贡献点配方是否完整。

安全边界

插件得到能力,不得到应用进程。

不执行任意代码

外置 ZIP 中的 Python、JAR、原生库和脚本不会被加载。

存储按插件隔离

数据键只能使用安全标识符,不能逃逸插件自己的目录。

网络由宿主代理

仅 HTTPS;拒绝显式本地和私有地址、用户凭据 URL、重定向与危险请求头;30 秒超时,响应上限 1 MiB。

模型密钥不可见

插件只能请求宿主执行指定模型任务,不能读取用户 API Key。

会话写入受控

角色和 NPC 变更由宿主校验并原子写入,插件不能直接操作 Room 或会话文件。

禁用立即生效

执行端点会检查启用态,禁用时同时移除会话中的生成增强规则。

发布前

最后检查一次。

  1. 01

    ID 与版本稳定插件 ID 使用独特、可识别的安全标识,升级只变更 version

  2. 02

    API 明确新插件写 "apiVersion": "2"

  3. 03

    最小权限逐个对照操作表,不申请配方未使用的权限。

  4. 04

    ID 全部对齐contributes 中每个 ID 都有同名 execution 配方。

  5. 05

    失败可读为可能返回空结果的动作设置清楚的 empty_notice

  6. 06

    真机验证完成检查、安装、启用、执行、禁用、升级和卸载完整流程。

准备开始

从官方声明式模板复制一份。

模板与宿主实现同仓维护,遇到边界问题可以直接提交 Issue。