Skip to content

在插件中使用钩子

使用 ctx.hooks.on(name, callback, options) 注册 Hook。callback 接收只读语义的 input 和可修改的 output;通过修改 output 改变后续行为,不需要返回值。

options 目前只有 failureMode

行为
open默认值。回调抛错或输出校验失败时,放弃该回调的修改并继续主流程
closed回调抛错或输出校验失败时,把失败传播到当前 Hook 的调用方;after Hook 已发生的操作不会被回滚

插件入口的 failureMode 是全部 Hook 的默认值,单次注册 options.failureMode 可以覆盖它。OpenDesk 依次执行已注册回调,每个回调都操作输出副本;只有回调成功且输出合法时才合并,因此一个失败回调不会留下部分修改。

插件 API 不提供其他错误调度选项;插件作者只需要按上述语义配置 failureMode

当前 OPENDESK_PLUGIN_HOOK_NAMES 暴露以下 Hook 名(TypeScript 插件可用 PluginHookNameOPENDESK_PLUGIN_HOOK_NAMES 获得同一套有效名称):

分组Hook 名触发时机
对话chat.headers在当前模型请求前追加或改写 HTTP Header
对话chat.params在当前模型请求前调整推理强度和最大输出 token
技能skill.load.beforeAgent 明确加载某个 Skill 时触发,可拒绝加载
技能skill.load.after某个 Skill 成功加载后触发,仅用于观测和审计
技能skill.install.before用户 Skill 安装前触发,可拒绝安装
技能skill.install.after用户 Skill 安装成功后触发,仅用于观测和审计
工具tool.definition调整工具在模型侧可见的描述和参数 Schema
工具tool.validate.before在参数校验前修改工具参数或拒绝本次执行
工具tool.execute.before在执行前(权限检查之后)拒绝本次执行(不可修改参数
工具tool.execute.after在工具完成后修改状态、文本结果或结构化结果
工具permission.ask对权限请求返回 askallowdeny
工具shell.env为终端 Shell 请求调整环境变量
工具shell.execute.before在执行 Shell 命令前修改参数或拒绝执行
工具shell.execute.after在 Shell 命令完成后修改状态、文本或结构化结果

JavaScript 插件注册未知 Hook 名时会在 setup 阶段失败。chat.headerschat.params 共享 PluginChatRequestInput,但使用独立输出对象并依次执行。单个 Hook 回调失败时,只回滚该 Hook 的修改;chat.headers 不会改写模型参数,chat.params 也不会改写 Header。

在每次模型请求准备 Header 时触发。一次用户消息可能产生主回复、终止检查、记忆更新或自动重试等多次模型请求,因此不能假设每条消息只触发一次。

Input:

字段类型说明
taskIdstring,可选当前任务
workspacestring,可选当前工作空间
modelAliasstring,可选当前模型别名

Output:

字段类型说明
headersRecord<string, string>最终请求 Header;值必须是字符串
ctx.hooks.on('chat.headers', (input, output) => {
output.headers['x-request-source'] = 'my-plugin';
});

在每次模型请求准备可公开调整的模型参数时触发。Input 与 chat.headers 相同。

Output:

字段类型说明
reasoningEffort空字符串、none、low、medium、high、max,可选推理强度;空字符串表示交给模型默认值
maxCompletionTokens正整数,可选最大输出 token 数
ctx.hooks.on('chat.params', (input, output) => {
if (input.modelAlias?.includes('reasoning')) {
output.reasoningEffort = 'high';
}
output.maxCompletionTokens = 8192;
});

Agent 即将加载一个具体 Skill 时触发,可允许或拒绝加载。

Input:

字段类型说明
bundleNamestringSkill 所属 bundle
skillNamestringSkill 名
skillDirectorystring,可选Skill 目录
sourceinstalled、config、mounted、builtin、plugin,可选Skill 来源
frontmatterobject已解析的 Skill frontmatter

Output:

字段类型说明
operationallow 或 deny默认 allow;deny 阻止加载
messagestring,可选允许或拒绝原因
metadataobject,可选插件附加记录
ctx.hooks.on('skill.load.before', (input, output) => {
if (input.frontmatter.requiresApproval === true) {
output.operation = 'deny';
output.message = 'This Skill requires separate approval';
output.metadata = { policy: 'requires-approval' };
}
});

skill.load.before 只对应一次具体的 Skill 加载请求,例如 Agent 调用 LoadSkill 或用户通过 /skill-name 预加载。Skill 列举、目录扫描、registry refresh,以及 CallSkillTool 调用已加载 Skill 的内部工具,都不会触发这组 Hook。before 拒绝时不会执行 Skill 的环境变量合并,也不会触发 after

一个具体 Skill 成功加载后触发。Input 与 skill.load.before 相同,Output 为空对象,适合审计、计数或日志记录。它不能修改已经加载的 Skill。

ctx.hooks.on('skill.load.after', (input) => {
console.info('Loaded Skill', input.bundleName, input.skillName);
});

SkillMgr 即将安装 Skill 时触发,可拒绝安装。

Input:

字段类型说明
sourcedirectory、zip、url、registry安装源类型
pathstring,可选directory 或 zip 的本地路径
urlstring,可选URL 安装地址
skillRelPathstring,可选安装源内的 Skill 相对路径
forceboolean,可选是否覆盖已有 Skill
customNamestring,可选用户指定的 Skill 名

Output 与 skill.load.before 相同:operation 为 allow 或 deny,message 和 metadata 可选。

ctx.hooks.on('skill.install.before', (input, output) => {
if (input.source === 'url' && !input.url?.startsWith('https://')) {
output.operation = 'deny';
output.message = 'Only HTTPS Skill URLs are allowed';
}
});

Skill 成功安装后触发。Input 在 skill.install.before 字段之外增加 skillName,Output 为空对象。适合写审计日志或更新插件自己的索引,不能撤销已经完成的安装。

ctx.hooks.on('skill.install.after', (input) => {
console.info('Installed Skill', input.skillName, 'from', input.source);
});

以下 Hook 都围绕一次工具调用、Shell 命令或权限请求:tool.* 作用于工具,shell.* 作用于终端命令,permission.ask 作用于工具调用前的权限确认。

一次工具调用按顺序经过以下阶段,其中绿色节点是插件可注入的 Hook。完整的调用链能帮助你判断某个逻辑应该挂在哪一个 Hook 上:

工具调用顺序与插件 Hook 节点从解析模型参数到执行工具完成,标注插件可注入的 Hook 节点及其可修改或拒绝的能力边界。解析模型参数tool.validate.before可修改参数 · 可拒绝参数校验(使用修改后的参数)权限检查tool.execute.before仅可拒绝 · 不可改参数执行工具tool.execute.after可修改状态与结果插件 Hook 节点(可拦截 / 修改)系统阶段

OpenDesk 为任务组装可用 Tool 定义时,对每一个 Tool 触发。它可能因多个任务、重试或工具集刷新而重复触发。

Input:

字段类型说明
toolstring完整 Tool 名
pluginIdstring,可选Tool 来自插件时为其插件 id,原生 Tool 为空
taskIdstring,可选当前任务
workspacestring,可选当前工作空间

Output:

字段类型说明
descriptionstring给模型读取的说明,修改后仍不能为空
parametersZod object schemaTool 参数 Schema,必须满足与 ToolDefinition.parameters 相同的校验
ctx.hooks.on('tool.definition', (input, output) => {
if (input.tool === 'files-read') {
output.description += ' Prefer paths inside the current workspace.';
}
});

工具参数完成解析、但尚未进入校验和执行前触发。修改后的 args 会用于后续校验和实际执行,因此可以修正或规整参数,也可以拒绝执行。

Input:

字段类型说明
toolstring完整 Tool 名
taskIdstring当前任务
callIdstringTool 调用 id
workspacestring,可选当前工作空间

Output:

字段类型说明
argsunknown即将校验和执行的参数,可以替换
denyobject,可选设置后拒绝执行
deny.messagestring,可选面向调用方的拒绝原因
deny.metadataobject,可选写入 Tool 结构化结果的拒绝信息
ctx.hooks.on('tool.validate.before', (input, output) => {
if (typeof input.args === 'object' && input.args && 'path' in input.args) {
const args = input.args;
args.path = args.path.replace(/^~\//, process.env.HOME + '/');
}
});

Tool 参数完成校验、权限检查之后,但尚未实际执行前触发。该阶段不可修改参数(以免绕过权限检查),只能拒绝执行。

Input:

字段类型说明
toolstring完整 Tool 名
taskIdstring当前任务
callIdstringTool 调用 id
workspacestring,可选当前工作空间

Output:

字段类型说明
denyobject,可选设置后拒绝执行
deny.messagestring,可选面向调用方的拒绝原因
deny.metadataobject,可选写入 Tool 结构化结果的拒绝信息
ctx.hooks.on('tool.execute.before', (input, output) => {
if (input.tool === 'dangerous-tool') {
output.deny = {
message: 'Blocked by plugin policy',
metadata: { policy: 'deny-dangerous-tool' }
};
}
});

Tool 执行结束后触发,可调整返回给模型和界面的状态与结果。

Input:

字段类型说明
toolstring完整 Tool 名
taskIdstring当前任务
callIdstringTool 调用 id
workspacestring,可选当前工作空间
argsunknown实际执行参数

Output:

字段类型说明
statussuccess 或 error最终状态
resultstring最终文本结果
resultObjectobject,可选最终结构化结果
ctx.hooks.on('tool.execute.after', (input, output) => {
output.resultObject = {
...(output.resultObject || {}),
auditedBy: 'my-plugin'
};
});

OpenDesk 即将为一次 Tool 调用询问权限时触发。

Input:

字段类型说明
taskIdstring当前任务
workspacestring,可选当前工作空间
toolstring请求权限的 Tool
callIdstring,可选Tool 调用 id
requestsPermissionRequest[]需要确认的资源、操作和路径

Output:

字段类型说明
decisionask、allow、denyask 保留用户确认;allow 或 deny 直接决定

只有明确的组织策略或安全策略才应自动 allow 或 deny。不要用该 Hook 隐式扩大 Tool 权限。

ctx.hooks.on('permission.ask', (input, output) => {
const usesHttp = input.requests.some((request) =>
request.resourceType === 'network' && request.resourcePath.startsWith('http:')
);
if (usesHttp) output.decision = 'deny';
});

Terminal 应用即将执行 Shell 命令时触发,用于添加、修改或移除本次进程环境变量。

Input:

字段类型说明
taskIdstring,可选当前任务
workspacestring,可选当前工作空间
shellstringShell 可执行文件
commandstring即将执行的命令

Output:

字段类型说明
envRecord<string, string 或 undefined>本次进程环境;字符串设置值,undefined 移除值
ctx.hooks.on('shell.env', (input, output) => {
output.env.MY_PLUGIN_WORKSPACE = input.workspace || '';
});

不要把长期凭据无条件注入所有命令;至少按 workspace、shell 或 command 收窄触发范围。

Terminal 即将执行 Shell 命令、但在参数定稿前触发,可修改参数或拒绝执行。

Input:

字段类型说明
toolstring完整 Tool 名
taskIdstring当前任务
callIdstring调用 id
workspacestring,可选当前工作空间
shellTypebash、zsh、powershellShell 类型

Output:

字段类型说明
argsunknown即将执行的参数,可以替换
denyobject,可选设置后拒绝执行
deny.messagestring,可选面向调用方的拒绝原因
deny.metadataobject,可选写入结构化结果的拒绝信息
ctx.hooks.on('shell.execute.before', (input, output) => {
if (input.shellType === 'powershell') {
output.deny = { message: 'PowerShell execution is blocked by plugin policy' };
}
});

Shell 命令执行结束后触发,可调整返回给模型和界面的状态与结果。

Input:

字段类型说明
toolstring完整 Tool 名
taskIdstring当前任务
callIdstring调用 id
workspacestring,可选当前工作空间
argsunknown实际执行参数
shellTypebash、zsh、powershellShell 类型
returnCodenumber 或 null进程返回码

Output:

字段类型说明
statussuccess 或 error最终状态
resultstring最终文本结果
resultObjectobject,可选最终结构化结果
ctx.hooks.on('shell.execute.after', (input, output) => {
output.resultObject = {
...(output.resultObject || {}),
returnCode: input.returnCode
};
});
import { definePlugin } from '@bitclub.ai/opendesk-plugin-sdk';
export default definePlugin({
id: 'request-policy',
capabilities: ['hook'],
failureMode: 'open',
setup(ctx) {
const headers = ctx.hooks.on('chat.headers', (input, output) => {
output.headers['x-opendesk-workspace'] = input.workspace || 'none';
});
const tools = ctx.hooks.on(
'tool.execute.before',
(input, output) => {
if (input.tool === 'blocked-tool') {
output.deny = { message: 'Blocked by request-policy' };
}
},
{ failureMode: 'closed' }
);
return async () => {
await tools.dispose();
await headers.dispose();
};
}
});
export default {
id: 'request-policy',
capabilities: ['hook'],
failureMode: 'open',
setup(ctx) {
const headers = ctx.hooks.on('chat.headers', (input, output) => {
output.headers['x-opendesk-workspace'] = input.workspace || 'none';
});
const tools = ctx.hooks.on(
'tool.execute.before',
(input, output) => {
if (input.tool === 'blocked-tool') {
output.deny = { message: 'Blocked by request-policy' };
}
},
{ failureMode: 'closed' }
);
return async () => {
await tools.dispose();
await headers.dispose();
};
}
};