命令是插件唯一的「动作」单位:面板上的按钮调它,UniMind 也调它,走的是同一条执行路径 (参数校验 → 确认 → 执行 → 撤销栈)。先想清楚命令,再做界面。
三种 kind
| kind | 用途 | 模型调用时 | 界面调用时 |
|---|---|---|---|
read |
查询,不改任何东西 | 直接执行 | 直接执行 |
write |
改数据(存储、日历、文件、网络) | 先弹确认卡片(显示 describe 的返回值),用户点确认才执行 |
直接执行(界面上的点击就是用户意图) |
navigate |
只打开界面(面板) | 直接执行 | 直接执行 |
一个命令的完整形状
const snooze: PluginCommand<Args, Result> = {
action: 'snooze',
titleKey: 'commands.snooze', // locales 里的 key:撤销记录、按钮上的名字
description: '…', // 给模型看(英文):什么时候用、参数含义、限制、结果长什么样
parameters: { … }, // JSON Schema 子集,执行器在 run 之前校验
describe: async (args, ctx) => '…', // 确认卡片上的一句话(read 模式 ctx)
run: async (args, ctx) => result, // 真正做事(模式跟随 kind)
undo: async (result, ctx) => {}, // 可选:撤销(write 模式 ctx)
changed: (result) => boolean, // 可选:这次到底改没改(没改就不进撤销栈)
}describe:确认卡片上写什么
用户在确认卡片上看到的只有这句话,所以要写清楚「将要改什么」,不能只写命令名:
- ✗
Snooze assignment - ✓
把「Lab 3 报告」隐藏到 10 月 3 日 09:00
规则:
- 插值进来的数据一律过
clean()(import { clean } from '@uniflow/plugin-sdk'):作业标题、课程名、导入的日程都可能被 提示词注入污染,clean去掉换行、方向控制符、零宽字符并截断,防止一条标题在卡片上伪造出第二行说明。 - 在 describe 里就把「做不了」报出来:抛
PreconditionError(「日历还没加载完」「卡组已满」),用户确认之前就知道不行, 而不是点了确认才失败。参数指向的东西不存在抛InputError。 - describe 运行在 read 模式:只读数据,不写任何东西。它会被调用两次(确认前、确认后重新比对:两次结果不同 = 确认期间数据 变了,执行器拒绝执行),所以必须确定性、没有副作用。
run:返回什么
- 返回纯 JSON 数据(跨沙箱传输走结构化复制;函数、类实例会报错)。
- 给模型的结果要有预算:列表类命令按字符预算截断,带
truncated/nextOffset让模型翻页(官方闪卡插件的list_decks用 7000 字预算,见官方插件导读)。执行器对单次结果另有 8000 字上限,超出会被截掉。 - 面板要「整批」数据时,另开一个
agentVisible: false的读命令给面板用,不要按参数组合去猜调用方是谁。
错误
| 抛什么 | 执行结果 code | 模型据此 |
|---|---|---|
new InputError('…') |
invalid_args |
改参数重试(例如先去查正确的 id) |
new PreconditionError('…') |
unavailable |
不重试,告诉用户原因 |
| 其他任何异常 | failed |
不重试 |
错误消息会原样给模型与用户看:写成一句人话,必要时用 ctx.t() 本地化。不要把内部堆栈、地址、令牌写进错误消息。
undo:撤销
- 只有 write 命令能有 undo;有 undo 的命令进应用的撤销栈(UniMind 工具卡片上的「撤销」按钮)。
undo(result, ctx)拿到的是run的返回值:把撤销需要的信息放进返回值(例如删除前的原样数据),不要依赖插件内存里的状态 —— 沙箱随时可能被重启。- 撤销要能应对「之后又被改过」:例如恢复一条记录前先看它是否已经被别的操作改了,改了就抛错而不是覆盖(宁可撤销失败,也不要覆盖用户后来的修改)。
- 面板发起的写命令不进撤销栈(面板没有撤销入口):删除类操作在面板里做行内二次确认或软删除。
agentVisible:哪些给模型
默认给。设成 false 的情况:
- 本质上只能由用户本人做的动作(闪卡的「记得 / 忘了」评分 —— 模型替用户评分会让排期失真);
- 只有面板用的整批读取、面板内部的辅助命令;
- 危险但面板有二次确认保护的动作(例如清空回收站)。
每个插件最多 10 个命令给模型,全部插件与应用自己共用约 80 个工具名额。
给模型写 description 的要点
- 第一句说什么时候用:「Use when the user asks …」。
- 写清参数的单位、格式(时间必须带时区:
2026-10-01T09:00:00+08:00)、默认值、上限。 - 写清结果的形状和截断规则(
nextOffset、truncated)。 - 写清它不做什么、要先调哪个命令(「Call list_decks first to get deckId」)。
- 英文、≤ 900 字符;不要写「you must」「ignore previous」这类指令性措辞 —— 第三方插件的说明在工具清单里会被标上
[Third-party plugin "名字"],模型把它当作参考而不是命令。
并发
同一个插件的多个命令可能同时在跑(模型的调用、面板的点击、后台任务)。沙箱里每次能力调用都是异步的,
「读 → 改 → 写回」之间会让出执行权:同一份数据的读改写要按对象串行(官方考试规划插件的 serialByPlan、
日历同步插件的 exclusive(订阅 id))。