Skip to main content
浏览文档目录
第 4 章

命令

read / write / navigate、describe、确认卡片、撤销、错误、给模型的说明怎么写

命令是插件唯一的「动作」单位:面板上的按钮调它,UniMind 也调它,走的是同一条执行路径 (参数校验 → 确认 → 执行 → 撤销栈)。先想清楚命令,再做界面。

三种 kind

kind 用途 模型调用时 界面调用时
read 查询,不改任何东西 直接执行 直接执行
write 改数据(存储、日历、文件、网络) 先弹确认卡片(显示 describe 的返回值),用户点确认才执行 直接执行(界面上的点击就是用户意图)
navigate 只打开界面(面板) 直接执行 直接执行

一个命令的完整形状

TypeScript
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

规则:

  1. 插值进来的数据一律过 clean()(import { clean } from '@uniflow/plugin-sdk'):作业标题、课程名、导入的日程都可能被 提示词注入污染,clean 去掉换行、方向控制符、零宽字符并截断,防止一条标题在卡片上伪造出第二行说明。
  2. 在 describe 里就把「做不了」报出来:抛 PreconditionError(「日历还没加载完」「卡组已满」),用户确认之前就知道不行, 而不是点了确认才失败。参数指向的东西不存在抛 InputError。
  3. 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 的要点

  1. 第一句说什么时候用:「Use when the user asks …」。
  2. 写清参数的单位、格式(时间必须带时区:2026-10-01T09:00:00+08:00)、默认值、上限。
  3. 写清结果的形状和截断规则(nextOffset、truncated)。
  4. 写清它不做什么、要先调哪个命令(「Call list_decks first to get deckId」)。
  5. 英文、≤ 900 字符;不要写「you must」「ignore previous」这类指令性措辞 —— 第三方插件的说明在工具清单里会被标上 [Third-party plugin "名字"],模型把它当作参考而不是命令。

并发

同一个插件的多个命令可能同时在跑(模型的调用、面板的点击、后台任务)。沙箱里每次能力调用都是异步的, 「读 → 改 → 写回」之间会让出执行权:同一份数据的读改写要按对象串行(官方考试规划插件的 serialByPlan、 日历同步插件的 exclusive(订阅 id))。