← 返回文档总览

插件开发:自定义编排节点(DSL Node)

面向插件开发者。讲清三件事:怎么注册execute 怎么写前端怎么显示与过校验。 设计背景见 rundls-extension-api.md;Agent 工具(LLM 可调用)的扩展见 AGENT_TOOL_DEVELOPMENT.md;插件体系总览见 PLUGIN_DEVELOPMENT.md


1. 总览:两条扩展点

编排引擎(harness/js)对外只开两个扩展口,都在插件 main.cjs 里经 Cordis 服务注册:

扩展点 注册方式 用途
自定义节点 ctx.harness.registerNode(def) 编排画布上可摆放、连线、执行的新节点类型
自定义工具 ctx.harness.registerTool(def) tool 节点 / agent 可调用的工具(另有 ctx.registerAgentTools 通道,见 AGENT_TOOL_DEVELOPMENT.md)

注册表实现在 harness/js/dsl-registry.cjs(两张内存 Map:nodeRegistry / toolRegistry)。owner(插件 id)由 Cordis 上下文自动注入,插件卸载/热重载时按 owner 一次性摘掉,不会残留旧节点。

⚠ 重要:插件内禁止 require('../../harness/...')。插件运行时加载的是构建产物目录(dist/),相对路径不可达;且 dsl-nodes 等模块带 langchain 依赖。harness 能力一律经 SDK 门面 ctx.harness.* 使用:

SDK 成员(ctx.harness) 用途
registerNode(def) / registerTool(def) 注册节点/工具(owner 自动归属)
unregisterNode(type) / unregisterTool(name) 手动注销(一般不用,卸载自动清理)
listNodes() / listTools() 注册表元数据查询
makeEnvelope 事件信封构造器集合(graphNodeLog / toolProgress / contextCompressed / graphWegirlOutput 等)
resolveShown(cfg, smartValue) 「智能显示」shown 判定(logMode 优先)
interpolate(str, scope) / buildScope(state, runtime) 模板插值(惰性加载)

数据流:

插件 main.cjs apply(ctx)
  └─ ctx.harness.registerNode(def)          # owner 自动 = 当前插件 id
       └─ dsl-registry.cjs nodeRegistry.set(type, def)
            ├─ 主进程:dsl-nodes.cjs executeDSLNode 分发执行
            │    HANDLERS[内置类型] 优先 → dslRegistry.getNodeExecute(type) 兜底
            ├─ IPC harness:listNodes → 渲染端(校验放行 + 元数据查询)
            └─ 渲染端:画布通用渲染 / 配置面板回退表单

2. 最小示例

插件入口是 Cordis 风格的 apply(ctx)(加载器只认 mod.apply;清理钩子经 ctx.on('dispose')apply 返回 { dispose })。在插件 main.cjs 里:

'use strict';

function apply(ctx) {
  // 自定义节点:拉取邮箱最新邮件,写入 state.vars
  ctx.harness.registerNode({
    type: 'mail.fetch',                 // 全局唯一类型 id(建议 <命名空间>.<动作>)
    title: '拉取邮件',                   // 调色板/画布显示名
    category: '邮件',                    // 分组(预留;当前画布按内置分组展示)
    kind: 'action',                     // 'action' 数据流节点 | 'config' 配置子节点
    defaultConfig: { mailbox: 'INBOX', limit: 10, outputVar: 'mails' },
    inputs: [],                         // 声明式输入/输出(预留,编辑器可读)
    outputs: [],
    constraints: null,

    // 执行闭包:只在主进程运行,签名与内置 handler 完全一致
    execute: async (node, dsl, state, runtime) => {
      const cfg = node.config || {};
      const mails = await fetchMail(cfg.mailbox, cfg.limit);   // 你的业务逻辑
      const vars = Object.assign({}, state.vars || {});
      vars[cfg.outputVar || 'mails'] = mails;
      return { vars };                // ← 返回 state 补丁
    }
  });
}

module.exports = { apply };

插件目录结构不变(plugins/<id>/main.cjs → 编译进 dist/,宿主自动扫描加载)。harness 在主进程内,注册/改码后须完全重启应用生效。


3. def 字段说明

字段 必填 说明
type 唯一类型 id。与内置类型重名会被内置 HANDLERS 优先覆盖(等于注册无效),务必用命名空间前缀
execute async (node, dsl, state, runtime) => patch,见 §4
title 建议 显示名,缺省用 type
category 建议 分组名,缺省 'plugin'
kind 建议 'action'(默认,数据流节点)或 'config'(配置子节点,如 Checkpointer 那类)
defaultConfig 建议 默认配置对象;用户在配置面板改完后存进 node.config公共默认 logMode:'smart' 会由前端回退逻辑补上,不必自己写
inputs / outputs 可选 声明式端口描述(预留,编辑器可读,不参与校验)
constraints 可选 声明式结构约束(数据而非函数,跨语言可解释;预留)

4. execute 契约

签名与参数

execute: async (node, dsl, state, runtime) => ({ ...statePatch })
参数 内容
node 当前节点 { id, type, label, config }config 已含用户在面板填的值(与 defaultConfig 合并过)
dsl 整个图(主图或复合 tab),一般用不到
state 图状态:{ vars, input, messages, context, output }vars 是跨节点变量池;input.query 是用户输入
runtime 运行时门面,见下

runtime 常用成员(完整定义见 harness/js/harness-service.cjs 的 runtime 构造处):

成员 用途
runtime.emit(envelope) 广播事件(trace 落库 + 前端渲染)。信封构造用 ctx.harness.makeEnvelope.*
runtime.query / runtime.variables 用户输入与编排启动变量
runtime.signal AbortSignal,长时间操作应监听取消
runtime.resolveModel(alias) / resolveTools(names) 解析模型/工具定义(需要调 LLM 时用)
runtime.kbRetrieve(...) 知识库检索
runtime.requestApproval(info) 请求人工审批,返回 boolean
runtime.runId / sessionId / projectId 关联标识,发事件时带上

返回值

返回一个对象补丁,引擎把它合并进 state。只返回你改动的 key(典型是 { vars: {...} }{ output: ... })。返回 {} 表示无状态变化。

错误处理

execute 抛错会被编译器捕获并广播 graph.node.error(画布节点标红、过程区显示错误),运行继续按错误路径走或终止——与内置节点行为一致。

模板插值

config 里的字符串支持 {{ path }} 取值(vars.x / input.query 等)与 {{ =expr }} 表达式。插值器经 SDK 取用(插件不可直接 require harness 内部模块——安装后路径不可达,且 dsl-nodes 带 langchain 依赖):

const { interpolate, buildScope } = ctx.harness;   // apply(ctx) 内或 execute 闭包里均可
const scope = buildScope(state, runtime);
const rendered = interpolate(cfg.template, scope); // 失败保留原样

条件边表达式

边上的 condition 用同一作用域求值(受限表达式:路径、比较、&&/||/!,不支持函数调用)。作用域字段:vars / input / output / messages / context / query,以及两个引擎预计算的派生变量

变量 含义
shouldContinue 末条消息是否带待处理 tool_calls(LangGraph should_continue 条件边等效)。条件边直接写 shouldContinue / !shouldContinue不要手拼 messages[messages.length - 1].tool_calls ... 冗长表达式(旧写法仍兼容)
vars.loopDetected / vars.loopReason loopDetect 节点每轮写入('true'/'false'),供护栏分支路由

典型 ReAct 路由:shouldContinue → tools;!shouldContinue → end;vars.loopDetected == 'true' → replan。

实例覆盖(overrides)空值语义

composite 节点 config.overrides[子节点id][字段] 合并进子节点默认 config,空值 = 回退共享默认

编译期(expandComposites)与编辑器覆盖面板两侧行为一致。

追加工具(extraTools)

llm 节点的正式配置字段(defaultConfig.extraTools: []):运行时与 tools 合并(按工具名去重), 不是替换。复合节点实例的典型用法:

overrides.agent.tools overrides.agent.extraTools 最终工具
有勾选 勾选的 + 追加的
[] / 缺省 共享复合默认 tools + 追加的(共享新增自动跟随)
有勾选 [] / 缺省 勾选的

编辑器属性面板(普通 llm 节点与复合覆盖分组共用 LLM_FIELDS)在 mode=tools 时显示 「可调用的工具(多选)」与「追加的工具(多选)」两行,无需在插件侧做任何登记。


5. 事件与「智能显示」

节点执行期广播的事件决定前端过程区显示什么。规则(2026-09-06 起,全节点统一):

function apply(ctx) {
  const { makeEnvelope, resolveShown } = ctx.harness;   // SDK:事件信封 + shown 判定

  ctx.harness.registerNode({
    type: 'example.stats',
    // ...
    execute: async (node, dsl, state, runtime) => {
      const shown = resolveShown(node.config, resultCount > 0);
      runtime.emit({ type: 'graph.wegirl.output', runId: runtime.runId, nodeId: node.id, output, shown });
      return {};
    }
  });
}

6. 前端:显示与校验怎么生效

6.1 画布渲染(自动,无需登记)

画布对未知类型有通用回退:NODE_META[type] 缺失时用 { title: type, 灰色, 无配置字段 } 渲染,defaultConfig 来自 registry 元数据,配置面板至少能改 logMode。所以节点能跑、能存、能显示,全链路不需要改宿主前端。

6.2 调色板出现入口(两途径)

主编辑器左侧调色板由宿主的 NODE_META + NODE_ORDERAgentGraphPanel.jsx)静态生成,插件节点不会自动出现。两种接入方式:

  1. 宿主登记(推荐给随应用分发的官方节点):在 AgentGraphPanel.jsxNODE_META 加条目(type/color/desc/fields 配置表单)、NODE_ORDER 加排序位。注意配置面板字段由 NODE_META[type].fields 驱动,不登记就只能靠回退表单(无字段可编辑)。
  2. 插件自建面板:插件渲染端经宿主 SDK 查询注册表元数据,自绘节点列表与拖放:
const nodes = await hostSdk.harness.listNodes();   // [{ type, title, category, defaultConfig, owner }, ...]
const tools = await hostSdk.harness.listTools();

6.3 保存期校验放行(自动,经 IPC 同步)

保存/运行前的 DSL 校验复用 validateDSLharness/js/dsl-types.cjs),它对类型合法性的判定是:

VALID_TYPES.indexOf(n.type) < 0 && !dslRegistry.hasNodeType(n.type)  // → 报「类型非法」

渲染端打包的 dsl-registry.cjs 是与主进程独立的实例(空表),因此 orchestrationService.ensurePluginNodeMetas() 会在校验前经 harness:listNodes IPC 拉一次插件节点元数据,registerNodeMeta 登记进渲染端 registry(幂等 + Promise 缓存)。这一步是自动的(AgentGraphPanel 保存/运行/设计时校验三处都会先 await 它),插件侧无需做任何事。

如果你的自定义编排校验入口是自己新写的,记得先 await ensurePluginNodeMetas()

6.4 人工交互规则同样约束插件节点

若你的节点绑定了 interrupt 类工具(AskUserQuestion / ImageDeliver),或你的节点本身调 interrupt 等待用户输入——编排必须挂 Checkpointer(backend=memory),否则保存期校验直接拦截(checkHumanInterruptRequirements / nodeNeedsCheckpointer,见 dsl-types.cjs)。规则识别的是「节点 config 里绑定的工具名」(INTERRUPT_TOOL_RE = /AskUserQuestion|ImageDeliver/);如果你的自定义节点自身做 interrupt 而不绑定这些工具,请把节点 type 加进该正则,才能被保存期规则覆盖。


7. 调试与注意事项


8. FAQ

Q:插件节点能在复合节点(tab 子图 / 共享复合 .orch.json)里用吗? 能。校验对复合内层节点同样查 hasNodeType;执行期展开后走同一张注册表。

Q:编排里已经有我的节点类型,但插件被卸载了会怎样? 保存期校验会报「类型非法」(渲染端 registry 拉不到元数据);运行期 executeDSLNode 抛「未知节点类型」。重新启用插件即恢复。

Q:execute 里能调 LLM 吗? 能。用 runtime.resolveModel 拿模型定义后自行调用 provider;但更推荐把「LLM 调用」留给内置 llm 节点、你的节点只做数据加工/外部 IO,组合使用职责更清晰。

Q:想让节点出现在画布左侧节点库怎么办? 见 §6.2——官方节点走宿主 NODE_META/NODE_ORDER 登记;纯插件场景自建面板用 hostSdk.harness.listNodes()