Directus trigger-flow AI 工具:程序化触发 Flows 的参数契约、校验规则与执行管线深度解析
本文围绕 Directus 内置 AI 工具 trigger-flow 的设计文档 prompt.md 展开,完整继承其中的参数说明、流程类型、校验规则与常见工作流,并结合 工具实现、Zod 参数 Schema 与 FlowManager 源码,解析该工具从参数校验到流程执行的完整调用链。读完后,你将掌握:AI 工具如何安全地以编程方式触发 Directus 手动流程、参数如何映射到流程内部的 $trigger 上下文,以及如何避开文档列出的六类常见错误。
1. trigger-flow 在 Directus AI 工具体系中的定位
trigger-flow 是 Directus 内置 AI 能力(工具注册表)中的一个工具,用于"程序化执行 Flows":触发手动流程、向流程传参、并将多个流程串联成复杂自动化。该文档 prompt.md 并非普通的说明文档,而是工具运行时加载的指令文本:在 trigger-flow/index.ts 中,工具通过 requireText(resolve(__dirname, './prompt.md')) 将该文件内容挂到工具的 instructions 字段上,供大模型在调用前理解参数约束。
从源码结构看,工具通过 defineTool 定义,核心元数据为:
name: 'trigger-flow';description: 'Runs an active manual Directus flow. Use when the user asks to trigger, execute, or test an existing manual flow.';keywords: ['run flow', 'execute flow', 'manual flow', 'trigger automation', 'webhook flow'],用于工具的关键词检索;annotations.title: 'Directus - Trigger Flow'。
它与其他内置工具一起被汇总进 ALL_TOOLS,由 ToolRegistry 统一挂载:模型先通过根工具 search 检索并加载工具详情,再经 execute 分发到具体工具的 handler。值得注意的是,trigger-flow 没有标记 admin: true(可对比 flows 工具 的 admin: true),即非管理员账户在可见性过滤(#isToolVisible)通过后也能使用该工具,但后续执行仍受 Directus 权限体系约束。
2. 关键概念:文档定义的四条核心原则
原文档在 "Key Concepts" 一节给出了使用该工具前必须建立的四条认知,这也是整个工具的调用哲学:
- 前置条件(Prerequisite):触发前必须先用
flows工具读取流程的完整定义(flows.read),因为后续所有校验(集合范围、必填字段)都依赖这份定义; - 手动流程(Manual Flows):
trigger: "manual"的流程专为 UI 或本工具触发而设计; - 流程串联(Flow Chaining):任意流程都可以被触发,数据通过
$trigger.body接收,从而支持流程嵌套调用; - 校验(Validation):工具会校验集合支持性(collection 是否在流程的
options.collections内)、必填字段、以及选择要求(requireSelection)。
3. 参数契约:文档参数表与真实 Zod Schema 对照
3.1 文档中的参数说明
原文档给出的概念性参数结构如下:
{
"flowDefinition": {}, // FULL flow object from flows.read
"flowId": "uuid", // Flow ID to trigger
"collection": "name", // Collection context
"keys": ["id1"], // Item IDs (required if flow needs selection)
"method": "POST", // GET or POST (default: GET)
"data": {}, // Optional payload data
"query": {}, // Optional query parameters
"headers": {} // Optional headers
}
3.2 实际生效的输入 Schema
需要特别指出的是,真正被注册表解析、校验的契约以 schema.ts 中的两个 Zod Schema 为准:
export const TriggerFlowInputSchema = z.object({
id: PrimaryKeyInputSchema, // number | string,流程主键
collection: z.string(), // 必填,集合上下文
keys: z.array(PrimaryKeyInputSchema).optional(),
headers: z.record(z.string(), z.any()).optional(),
query: z.record(z.string(), z.any()).optional(),
data: z.record(z.string(), z.any()).optional(),
});
export const TriggerFlowValidateSchema = z.strictObject({ /* 同上,strictObject 禁止多余字段 */ });
即实际必填参数只有 id(流程主键)和 collection 两个,keys、headers、query、data 均可选。注册表在执行前会用 validateSchema.safeParse 做严格解析(#parseInput),多余字段会直接报 InvalidPayloadError——这正对应文档中"Common Mistakes"第 6 条:不要传字符串化的 JSON,要用原生对象(data、headers、query 都必须是对象)。
3.3 文档与实现的差异点(重要)
将文档参数表与 handler 实现 对照后,存在三处需要在实操中注意的差异:
| 文档概念参数 | 实际实现 | 说明 |
|---|---|---|
flowId |
id |
Schema 字段名为 id,类型为 number | string |
flowDefinition |
不传 | 工具 handler 内部会用 FlowsService.readOne 自行按 id 读取流程,并强制过滤 status: 'active' 且 trigger: 'manual' |
method: GET/POST |
固定 POST |
handler 调用 flowManager.runWebhookFlow 时 method 恒为 'POST' |
"先用 flows 工具读取流程定义"这一要求,在实现层面的对应是:工具自身也会通过 flowsService.readOne(args.id, { filter: { status: { _eq: 'active' }, trigger: { _eq: 'manual' } } }) 重新取一次流程(用于提取必填字段),而不依赖模型传入的完整定义对象。因此文档中"ALWAYS read the flow first"更多是面向模型的编排纪律:只有先读取过定义,模型才知道该传哪些 keys 和 data 字段。
4. 三类流程的调用示例(继承原文档)
4.1 需要选择项的手动流程(requireSelection: true 或未设置)
{
"flowDefinition": {
"id": "abc-123",
"trigger": "manual",
"options": {
"collections": ["products", "orders"],
"requireSelection": true,
"fields": [
{ "field": "reason", "name": "Reason", "meta": { "required": true } }
]
}
},
"flowId": "abc-123",
"collection": "products",
"keys": ["prod-1", "prod-2"], // REQUIRED
"data": { "reason": "Bulk update" } // Required field
}
4.2 无需选择项的手动流程(requireSelection: false)
{
"flowDefinition": {
"id": "xyz-456",
"trigger": "manual",
"options": { "collections": ["reports"], "requireSelection": false }
},
"flowId": "xyz-456",
"collection": "reports",
"keys": [], // requireSelection: false 时可为空
"data": { "type": "monthly" }
}
4.3 Webhook / Operation 触发型流程
{
"flowDefinition": {
"id": "webhook-flow",
"trigger": "webhook",
"options": { "collections": ["*"] }
},
"flowId": "webhook-flow",
"collection": "any_collection",
"method": "POST",
"data": { "custom": "payload" },
"headers": { "X-Custom-Header": "value" }
}
从源码结构看,Webhook 流程的真实触发入口是 REST 路由 controllers/flows.ts 中的 GET/POST /trigger/:pk,而 AI 工具 handler 读取流程时带了 trigger: { _eq: 'manual' } 过滤条件,即经由 trigger-flow 工具实际执行的是 active 状态的手动流程;文档中 Webhook 示例应理解为对"流程触发模型"的完整展示(说明任意触发类型的流程都可被程序化触达,数据都汇入 $trigger)。
4.4 服务端校验链:文档规则与源码逐条对应
文档列出的 5 条校验规则中,前 4 条由服务端执行,第 5 条是工具层附加校验。以 FlowManager.load 中注册的手动触发 handler 为例,逐条印证:
- 集合支持性:
enabledCollections为空或targetCollection不在其内 → 抛ForbiddenError(源码中即Specified collection must be one of: ...警告 + 403); - 选择要求:
requireSelection取值逻辑为flow.options?.['requireSelection'] ?? true,即未显式设置为false时一律视为需要选择(这正是文档"Common Mistakes"第 5 条"不要假设 requireSelection,要显式检查"的依据);要求选择时,keys必须是数组且非空,否则 403; - 认证要求:
isUnauthenticated(accountability)为真时直接拒绝——手动流程仅允许认证用户触发; - 权限要求:非管理员(
accountability.admin === false)需通过策略/权限检查获取目标集合的read权限,并且会用getService(targetCollection).readMany(targetKeys)回读所有传入的 keys,只要有一个 key 不可见就整体拒绝(防止借流程枚举不可见数据); - 必填字段:工具层在 index.ts 中从
flow.options.fields里挑出meta.required === true的字段,逐一检查args.data是否包含该 key,缺失则抛InvalidPayloadError({ reason: 'Required field "xxx" is missing' })。
5. 执行管线:从工具调用到 runWebhookFlow
工具 handler 的执行路径可概括为三步(源码):
第一步:读取流程并做必填字段预检
const flowsService = new FlowsService({ schema, accountability });
const flow = await flowsService.readOne(args.id, {
filter: { status: { _eq: 'active' }, trigger: { _eq: 'manual' } },
fields: ['options'],
});
第二步:组装伪请求并复用 Webhook 执行通道
const { result } = await flowManager.runWebhookFlow(
`POST-${args.id}`,
{
path: `/trigger/${args.id}`,
query: args.query ?? {},
method: 'POST',
body: { collection: args.collection, keys: args.keys, ...(args.data ?? {}) },
headers: args.headers ?? {},
},
{ accountability, schema },
);
这里的关键设计是:手动流程与 Webhook 流程共享同一条执行通道。FlowManager.load 中,manual 触发型的 handler 以 `POST-${flow.id}` 为键注册进 webhookFlowHandlers,与 webhook 触发型的 `${method}-${flow.id}` 并存。runWebhookFlow(见 flows.ts#L134-L151)先等待 reloadQueue 空闲(保证流程定义热加载完成),再按 `${method}-${id}` 查找 handler;找不到则记日志并抛 ForbiddenError。注意 body 的组装方式:collection 和 keys 被提升到 body 顶层,data 展开后与它们平级——这直接决定了流程内部如何取数(下一节)。
第三步:返回结果
handler 最终以 { type: 'text', data: result } 形式返回,其中 result 由 executeFlow 根据 flow.options['return'] 决定:manual 流程默认 return = '$last'(加载时补写,见 flows.ts#L359-L360),'$all' 返回完整的 keyedData,其他表达式用 micromustache 的 get 从上下文取值。若流程开启 async,则立即返回 undefined 结果;若 error_on_reject === true 且最后一步被 reject,则把最后结果作为错误抛出。
6. 被触发流程中的数据访问
流程执行时,executeFlow 构造了如下上下文变量:
const keyedData: Record<string, unknown> = {
[TRIGGER_KEY]: data, // $trigger
[LAST_KEY]: data, // $last
[ACCOUNTABILITY_KEY]: context?.['accountability'] ?? null, // $accountability
[ENV_KEY]: this.envs, // $env(受 FLOWS_ENV_ALLOW_LIST 白名单控制)
};
结合第 5 节的 body 组装方式,文档中"Data Access in Triggered Flow"一节的映射关系可精确落位:
| 流程内变量 | 来源(工具参数) |
|---|---|
$trigger.body |
data 参数的展开字段,以及顶层的 collection、keys |
$trigger.body.collection |
collection 参数 |
$trigger.body.keys |
keys 参数 |
$trigger.query |
query 参数 |
$trigger.headers |
headers 参数 |
$trigger.method / $trigger.path |
固定为 POST 与 /trigger/<flowId> |
$accountability |
当前用户/权限上下文,由注册表注入 |
$last |
每执行完一个 operation 即更新为该步骤输出,可用于跨步骤引用 |
$env |
白名单内的环境变量 |
文档还提到:若 data 传入数组,流程会对数组每一项各执行一次(For-Each 语义);流程可通过 trigger 的 return 选项返回数据;权限上遵循流程的 accountability 配置(源码印证:flow.accountability !== null 时写入 Activity 记录,为 'all' 时再写 Revisions,见 flows.ts#L428-L483)。
7. 常见工作流(继承原文档三个实战场景)
7.1 导出选中项(Export Selected Items)
// Step 1: 先用 flows 工具拿流程定义
flows.read({ filter: { name: { _eq: "Export Items" } } })
// Step 2: 带选择触发
{
"collection": "products",
"keys": ["1", "2", "3"],
"data": { "format": "csv", "email": "user@example.com" }
}
7.2 无选择批量处理(Process Batch Without Selection)
{
"collection": "orders",
"keys": [], // 流程 requireSelection: false 时传空数组
"data": { "status": "pending", "date_range": "last_30_days" }
}
7.3 流程串联(Chain Flows Together)
{
"collection": "notifications",
"data": {
"parent_result": "{{ $last }}", // 引用父流程最后一步的输出
"step": 2
}
}
串联的本质:父流程的某个 operation(如 trigger 类型)调用子流程,子流程的 $trigger.body 即父流程注入的数据,$last 提供跨步骤引用能力。
8. 重要注意事项与常见错误(继承原文档)
Important Notes:
- 先读后触发:触发前务必读取流程定义,确认
options.collections、options.fields、requireSelection; - 通配集合:collections 中的
"*"表示接受任意集合(服务端对 manual 流程实际校验enabledCollections.includes(targetCollection),通配需由流程配置显式支持); - 必填字段:检查
options.fields中meta.required的输入项; - For-Each:
data传数组时流程对每项各执行一次; - 返回值:由 trigger 的
return选项决定,manual 流程默认$last; - 权限:遵循流程
accountability配置与用户策略/权限。
六类常见错误(原文档 "Common Mistakes" 全量继承):
- 不先读取流程定义就触发;
- 流程需要选择时遗漏
keys; - 忽略流程配置中的必填字段;
- 使用了不在流程
collections列表中的集合; - 主观假设
requireSelection的取值而不显式检查; - 传递字符串化的 JSON 而非原生对象。
9. 测试用例提供的行为佐证
index.test.ts 为该工具提供了可验证的行为基线:
- 最小参数调用:仅传
{ id: 'flow-123', collection: 'articles' }时,断言runWebhookFlow收到的正是POST-flow-123、path: /trigger/flow-123、method: 'POST'、query: {}、headers: {}以及 body 仅含collection(见 index.test.ts#L43-L78); - 错误传播:
FlowsService.readOne抛出Forbidden(如无读权限或流程非 active/manual)时,工具原样向调用方传播该错误; - 必填字段缺失:
options.fields含required: true字段而data未提供时,断言抛出Invalid payload. Required field "title" is missing.;多字段场景只校验并报告第一个缺失项; - 工具配置:断言工具名为
trigger-flow、非 admin 工具(admin未定义)、具有描述与输入/校验 Schema。
10. 决策树:触发前的检查顺序
原文档末尾给出的决策树与源码校验顺序完全一致,可作为实操检查单:
1. 用 flows 工具读取流程定义
2. 检查触发类型:
- manual → 检查 requireSelection
- webhook/operation → keys 可选
3. 校验 collection 是否在 flow.options.collections 内
4. 若 requireSelection !== false → keys 必填且非空
5. 检查 flow.options.fields 中的必填数据字段
6. 以全部通过校验的参数发起触发
适用前提与限制:本文结论基于当前仓库版本的实现——工具经 FlowsService 强制过滤 status: 'active' 与 trigger: 'manual',执行通道复用 POST-${flowId} 的 webhook handler,参数契约以 TriggerFlowInputSchema/TriggerFlowValidateSchema 为准;文档中 flowDefinition/flowId/method 等概念化参数名用于指导模型编排,实际调用时请使用第 3.2 节的真实字段。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00