commands 服务
斜杠指令(/command)的注册与分发。第三方插件用它把斜杠命令挂进所有平台共享的入站管道。
- 服务注册名:描述符
commands(name: 'commands'),绑定接口BoundCommands。 - 契约包:
@aalis/api-commands(类型 + 按激活绑定的commandbuilder)。 - 参考实现:
@aalis/plugin-commands,核心类是CommandRegistry。 - 内核视角文档:
docs/plugins/plugin-commands.md。
绝大多数插件作者只需要登记命令。登记一律走
commands.use.command(...),详见第 4 节。第 3 节只为想替换整个指令引擎的高级作者准备。
1. 契约:CommandService 接口
export interface CommandService {
prefix: string;
command(name: string, description?: string | TextRef, meta?: InternalCommandMeta): CommandBuilder;
unregister(name: string, contextId?: string): void;
execute(name: string, ctx: ExecutionInput): Promise<string | undefined>;
parseCommand(input: string): { name: string; args: string[]; raw: string } | null;
hasMatch(head: string, tokens?: string[]): boolean;
has(name: string): boolean;
get(name: string): Command | undefined;
getNode(name: string | string[]): Command | undefined;
getAll(): Command[];
setExecutionGuard(guard: ExecutionGuard | undefined): void; // 传 undefined 摘掉,退回 fail-closed
}
export interface BoundCommands extends CommandService {
command(name: string, description?: string | TextRef, meta?: CommandMeta): CommandBuilder;
}Builder 链式追加 alias / option / argument / action / usage / example。commands.use.command 把 pluginName 填成本次激活 id;撤回时只摘自己那一层。不要自己调不带 contextId 的 unregister。
重要类型:
- 命令名 = 完整点路径(
'memory.clear.all')。名字段必须匹配^[a-z][a-z0-9-]*$。 - inline DSL:
<name:type>或[name:type],type ∈ string|number|boolean|text。text会吞掉剩余 token。有子指令的父指令不声明位置参数时,多余的 token 不被忽略,回「未知子指令或多余参数」、不执行(见 api-commands 的「子指令」一节)。 - CommandHandler:
(argv: CommandArgv, ...positionals: unknown[]) => ...。返回字符串即回复,undefined则静默。 - ExecutionInput.skipConfirm:供 scheduler 触发的指令与 runtime 一次性子命令免
session档的确认,只对授权身份是 owner 的执行生效,不绕过授权。 - ExecutionInput.confirmIntent:人亲手输入的指令由入站相位带上,执行守卫需要确认时调它向发出者本人核对意图;没有就按无从确认拒绝。
2. 谁提供 / 谁消费
唯一参考实现:@aalis/plugin-commands。provides: [commands],uses 含 gateway required,provide.use(commands, registry)。入站分发发生在 INBOUND_PHASE.COMMAND,只解析不带 source 的真人入站与声明了 ownerApproved 的注入(判据见 security-model),替换指令引擎的实现要照此判。运行时依赖 @aalis/api-authority 的 riskDefaults。
典型消费点:plugin-authority、plugin-agent、plugin-doctor、plugin-tool-system、plugin-user-profile、plugin-user-relation、plugin-maimai 等。commands 几乎总是 optional。plugin-authority 用 commands.follow(svc => { svc.setExecutionGuard(guard); ... }) 注入守卫,follow 返回的清理调 setExecutionGuard(undefined) 摘掉。
3. 写一个 provider(替换指令引擎,高级)
最小必须实现:CommandService 全部方法。
command()返回可用的 builder,unregister(name, contextId)只摘这一个contextId那一层。记下 builder 调用、提供者在场时同步转发、换人时重放到新提供者的是绑定门面(经资源口的registrar),provider 不必自己记账;provider 拒收登记时抛错,提供者在场时这个错误原样交给调用方(规则见 api-commands)。execute()在调用 handler 之前必须先执行已注入的ExecutionGuard。parseCommand/hasMatch要能区分「带前缀但无人注册」与「已命中」。
import { commands, type CommandService } from '@aalis/api-commands';
import { gateway } from '@aalis/api-gateway';
import { definePlugin, provide } from '@aalis/core';
export default definePlugin({
name: '@aalis/plugin-my-commands',
provides: [commands],
uses: { provide, gateway },
apply({ provide }) {
const svc = {} as CommandService;
provide.use(commands, svc);
},
});同名 provider 中,消费方选中哪一家按本插件偏好 > 全局偏好 > priority > 注册顺序。
4. 标准消费方式:登记命令
import { commands } from '@aalis/api-commands';
import { definePlugin, optional } from '@aalis/core';
export default definePlugin({
name: '@aalis/plugin-weather',
uses: { commands: optional(commands) },
apply({ commands }) {
commands.use
.command('weather <city:string> [day:number]', '查询天气。默认查今天,day 写几就查几天后')
.argument('city', '城市名')
.argument('day', '几天后,0 是今天')
.option('unit', '-u <unit:string>', {
description: '温度单位',
choices: [
{ value: 'c', description: '摄氏度' },
{ value: 'f', description: '华氏度' },
],
default: 'c',
})
.example('/weather 北京')
.action(async (argv, city, day) => {
const unit = argv.options.unit as string;
return `${String(city)} 天气(${String(day ?? 0)} 天后, ${unit})…`;
});
},
});option syntax 速查:
| syntax | 含义 |
|---|---|
'-v' | boolean flag,别名 v。 |
'-p <page:number>' | number 选项,别名 p,必带值。 |
'-p [page:number]' | 值可选。 |
'<page:number>' | 仅长名 --page,必带值。 |
'-t <type:string[]>' | string[]。 |
''(空) | 纯 boolean flag。 |
登记侧不用判断 commands 是否存在:builder 在提供者未就绪时缓存调用、上线后重放。若要读 getAll(),先看 commands.present,再调 commands.use.getAll();业务句柄的规则见 选择层与业务层。
写给 /help 看的几条:
- 描述的第一句是摘要:概览与子指令列表只显示第一句(按句子切分,分号与换行另算句末),显示宽度超过 48 列(中日韩文字记 2 列)会被截断。第一句写成一句短话,细节写在后面;不要把用法写进描述,用位置参数的说明与
example写。 - 位置参数用
argument(name, description)加说明,帮助里写成<名字>与[名字]。 - 取值清单用
choices,每项可带说明,帮助里逐项列在选项下面。string选项按它校验取值,string[]不校验。取值随运行时变化的写成返回它们的函数,帮助与校验每次现取。 -h与--help是保留选项,回这条指令的用法;不能声明名为help、h或别名为-h、--help的选项。- 有子指令、自身不执行的父指令只登记描述、不写
action:裸敲它回的就是/help <它>,不要手写一份帮助。 - 帮助只列调用者能用的指令,判据与执行同一份,见 plugin-commands 的「帮助」一节。帮助按各平台通用的纯文本输出,
usage写的整段帮助也应如此。
错误边界:handler 抛错会被 execute 捕获,转成 指令执行失败:<msg>。
5. 能力 / 风险 → 影响
指令权限走两轴正交闸,与工具共用同一套 ExecutionGuard。
- 轴 A:
visibility: 'public' | 'restricted' - 轴 B:
confirm: 'session' | 'always' - risk 糖:
safe/sensitive/dangerous - 沿点路径继承
skipConfirm: true只免确认,授权仍评估
声明写在 commands.use.command 的第三参 meta 上:
// risk 糖一次设定两轴默认:dangerous = restricted + confirm:'session'
commands.use.command('weather.reset', '清空天气缓存', { risk: 'dangerous' }).action(async () => '已清空');
// 显式声明覆盖 risk 推导:每次都确认
commands.use.command('weather.purge', '删除全部天气数据', { risk: 'dangerous', confirm: 'always' }).action(async () => '已删除');
// 只收紧可见性、不要求确认;子命令(weather.admin.*)沿点路径继承
commands.use.command('weather.admin', '天气管理', { visibility: 'restricted' });handler 内对外抓取须走 safeFetch;文件读写走 storage URI。storage 不是沙盒。
6. 边界与注意事项
- 没有守卫时 fail-closed:
plugin-authority未加载时,等同所有人都是默认等级、没人能确认——需要更高等级(restricted 或 risk 为 sensitive / dangerous)或声明了confirm的命令一律拒绝并提示缺少权限插件,其余照常执行。 - 守卫随 follow 重挂期间存在窗口:守卫由 plugin-authority 经
commands.follow注入,与命令执行独立;provider 换人瞬间新实例尚未注入守卫,这段时间内需要鉴权或确认的命令按上一条被拒。 - 同名命令分层覆盖:不同激活登记同名命令时,plugin-commands 记一条 warn、压上新的一层,执行最新那层的实现,安全轴取各层里最严的;后登记的撤回后复位到下一层。同一激活再登记同名命令是替换:先撤回自己原来那层。第三方应加领域前缀。
- 位置参数都是
unknown。 text类型贪婪,必须放在最后。- 纯关键词模式(prefix='')需命中首段。
- 命令命中即终止入站管道;带前缀却无人注册会放行到下游,带
source又没声明ownerApproved的注入不解析、同样放行。
7. 交叉链接
- 概念:
docs/concepts/service-model.md、docs/concepts/lazy-service-access.md、docs/concepts/manifest-metadata.md、docs/concepts/security-model.md、docs/concepts/message-llm-pipeline.md、docs/concepts/storage-uri-grammar.md。 - 内核文档:
docs/plugins/plugin-commands.md、docs/plugins/plugin-authority.md、docs/plugins/plugin-tools.md、docs/core/service.md、docs/core/context.md。