自定义节点开发

适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:crates/cheng-macros/src/lib.rscrates/cheng-nodes/src/nodes/registry.rscrates/cheng-nodes/src/schema_ext.rscrates/cheng-core/src/domain/sandbox_capability.rschengflow/docs/i18n-node-convention-guide.md

写一个节点是扩展 ChengOS 最常见的方式。一个节点就是:一个带派生宏的 Rust 结构体、一个有类型的输入、一个有类型的输出,以及一个函数。注册、JSON Schema 生成和属性面板界面都会由此自动得出。

最小的节点

use anyhow::Result;
use cheng_workflow_macros::{FieldOrder, Node};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::nodes::base::TypedNode;
use crate::register_node;

#[derive(Node)]
#[node(
    id = "utils/word_count",
    name = "Word Count",
    category = "Utils",
    description = "Count words in a text",
    ui_icon = "hash",
    ui_width = 280,
    ui_height = 160,
    ui_collapsible = true
)]
pub struct WordCountNode;

impl Default for WordCountNode {
    fn default() -> Self { Self }
}

#[derive(Debug, Deserialize, JsonSchema, FieldOrder)]
pub struct WordCountInput {
    /// Text to count.
    #[schemars(title = "Text")]
    pub text: String,
}

#[derive(Debug, Serialize, JsonSchema, FieldOrder)]
pub struct WordCountOutput {
    #[schemars(title = "Word Count")]
    pub word_count: usize,
}

impl TypedNode for WordCountNode {
    type Input = WordCountInput;
    type Output = WordCountOutput;

    fn run(&self, input: Self::Input) -> Result<Self::Output> {
        Ok(WordCountOutput { word_count: input.text.split_whitespace().count() })
    }
}

register_node!(WordCountNode; cheng_core::domain::sandbox_capability::SandboxCapability::FilesystemFree);

然后在 builtin/<category>/mod.rs 里导出、在 builtin/mod.rs 里再导出,重新构建即可。inventory crate 会在链接期收集这条注册——没有需要手改的注册清单

文件放在哪里

节点实现位于 cheng-nodes/src/nodes/builtin/<目录>/,目录取以下之一:

agent  ai  chat  document  io  rag  table  tools  translation  ui  utils  workspace

宏里的 category给界面用的逻辑分组,可能与目录不同category = "Utils" 并不意味着文件在 utils/

节点 id 是 category/name——io/input_textai/llmutils/preview。它是稳定的公开身份:改了它,所有用到该节点的已保存工作流都会失效。

#[node(...)] 属性

属性 必填 含义
id 稳定的 category/name 身份
name 显示名(默认取结构体名)
category 逻辑分组(默认 General
description 一句话说明
ui_icon 图标名
ui_color 节点颜色
ui_width / ui_height 画布上的尺寸
ui_collapsible 节点是否可折叠
hidden 从节点库中隐藏——用于内部机件节点

选择 trait

Trait 签名 适用于
TypedNode fn run(&self, input) -> Result<Output> 同步纯计算
AsyncTypedNode async fn run_async(&self, input, tool_ctx, exec_ctx) -> Result<Output> I/O 密集或依赖上下文
AsyncNode HashMap<String, Value> 遗留写法——新代码请勿使用

同步用 register_node! 注册,异步用 register_async_node!

AsyncTypedNode 会给你 tool_ctxexec_ctx,节点正是通过它们够到仓储、执行器、产物存储和审批门。只要你需要其中任何一个,即便自身逻辑并不是 I/O 密集的,也得用异步 trait。

沙箱能力——不要跳过这一节

注册时可带一个 SandboxCapability,而这个分类是失败即拒的:

能力 含义
FilesystemFree 不碰宿主路径——纯计算、只用数据库或对象 id、只走网络
SandboxAwareReadOnly 消费引擎注入的可信根目录,只读
SandboxAwareReadWrite 消费可信根与可写标志;变更仍需通过风险/审批门
ProcessIsolated 通过隔离启动器(Bubblewrap)运行宿主进程
WorkspaceProcess 在工作区权限内运行宿主进程,带 cwd 约束、命令策略和强制审批门——是组织性约束,不是内核隔离
UnsupportedForCli 不具备沙箱意识地访问宿主路径;CLI 执行会拒绝它

未分类的新节点一律是 UnsupportedForCli 这是刻意的:加了一个能碰文件系统的节点却没分类,会失败即拒,而不是悄悄放宽边界。如果你的节点在 CLI 里跑不起来而你没料到,原因就在这里。

这道防线设在执行的咽喉点,而不只在校验期,因为嵌套执行——子流程、批量子流程、以工作流实现的技能——继承父级沙箱,却从不经过静态校验。

用 schema 驱动界面

属性面板由你输入结构体的 JSON Schema 生成。用 #[schemars(...)]schema_ext 里的辅助函数控制结果:

辅助函数 渲染为
select_schema::<T> 由枚举生成的下拉框
multi_select_schema 多选
dynamic_select_schema 由 API 接口填充选项的下拉框
switch_schema 开关
slider_schema / slider_schema_with_step 滑块
textarea_schema / textarea_port_schema 多行文本
code_editor_schema 代码编辑器
rich_text_schema 富文本
number_schema 数字输入
datetime_schema 日期时间选择器
color_picker_schema 取色器
credential_select_schema 凭证选择器
file_upload_schema / image_upload_schema 上传控件
workflow_select_schema 工作流选择器
artifact_port_schema 及同族 产物端口
deprecated_schema / deprecated_bool_schema 标记为废弃
with_placeholder 添加占位提示
runtime_conditional 仅在运行时条件下存在的端口

可直接设置的实用扩展键:x-hidden(隐藏字段——用于 workspace_id 这类引擎注入的环境值)、x-port(作为连接端口暴露)、x-controlx-options-source,以及用于级联下拉的 x-depends-on

在输入与输出结构体上派生 #[derive(FieldOrder)] 可控制字段出现顺序。实现 collapsed_visible_output_fields 可指定节点折叠时在画布上显示什么。

把节点暴露为智能体工具

通过 register_tool!,节点会变成智能体可调用的工具,它声明工具 id、名称、描述、分类,以及参数的 JSON Schema。描述正是模型据以判断何时调用的依据——写给模型看,而不是写给扫列表的人看。

在允许模型填写的字段上加 #[llm_input]。没有这个标记的字段属于模型永远看不到的画布静态配置——Hub 节点上的审批开关正是这样待在模型够不到的地方。

同时补一个 node_skills/<node>/ 条目:那是面向 LLM 的用法文档,没有它,智能体只有 schema 而没有指引。

国际化

后端提供 key 和英文源文案;前端查翻译并做兜底。英文是真相来源,中文放在 config/i18n/nodes/*.json

节点级 key 从 i18n key 前缀派生:

{i18nKeyPrefix}.name
{i18nKeyPrefix}.description

随后:

cargo run -p chengctl -- missing-i18n --fail-if-missing
cargo run -p chengctl -- translate-missing --engine llm

只对确实需要输入引导的字段声明 placeholder / x-placeholder——没有节点级通配。见 docs/i18n-node-convention-guide.md

检查清单

  1. builtin/<目录>/ 里创建结构体,派生 Node,设置 id 与元数据。
  2. 定义 InputOutput,带上 Deserialize/SerializeJsonSchemaFieldOrder
  3. 实现 TypedNodeAsyncTypedNode
  4. 调用 register_node! / register_async_node!并带上沙箱能力
  5. 在分类的 mod.rs 中导出,并在 builtin/mod.rs 中再导出。
  6. 若希望智能体调用它,补上 register_tool!node_skills/<node>/
  7. 运行 cargo build,然后跑 chengctl missing-i18n
  8. 确认属性面板按预期渲染——理论上不需要改任何前端代码。

下一步

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容