适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:
crates/cheng-macros/src/lib.rs、crates/cheng-nodes/src/nodes/registry.rs、crates/cheng-nodes/src/schema_ext.rs、crates/cheng-core/src/domain/sandbox_capability.rs、chengflow/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_text、ai/llm、utils/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_ctx 和 exec_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-control、x-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。
检查清单
- 在
builtin/<目录>/里创建结构体,派生Node,设置id与元数据。 - 定义
Input和Output,带上Deserialize/Serialize、JsonSchema、FieldOrder。 - 实现
TypedNode或AsyncTypedNode。 - 调用
register_node!/register_async_node!,并带上沙箱能力。 - 在分类的
mod.rs中导出,并在builtin/mod.rs中再导出。 - 若希望智能体调用它,补上
register_tool!和node_skills/<node>/。 - 运行
cargo build,然后跑chengctl missing-i18n。 - 确认属性面板按预期渲染——理论上不需要改任何前端代码。
下一步
- 节点配置与 Schema 驱动表单——面板是怎么决定的。
- Crate 结构——各部分都住在哪里。
- 工具发现与 Tool Hub——智能体如何找到你的工具。

暂无评论内容