新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
@@ -6,12 +6,104 @@
|
||||
//! 与 AI Chat(侧边栏交互对话)的区别:AI Node 由 DAG Executor 自动驱动,
|
||||
//! 适合嵌入自动化链路(如 想法 → AI分析 → 脚本落地 → 人工审批)。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_ai::anthropic_compat::AnthropicCompatProvider;
|
||||
use df_ai::openai_compat::OpenAICompatProvider;
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||
|
||||
/// AI 节点解析后的参数(execute 与参数解析解耦,便于单测覆盖取值/默认/校验逻辑)
|
||||
#[derive(Debug)]
|
||||
struct AiNodeParams {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
prompt: String,
|
||||
system_prompt: Option<String>,
|
||||
model: String,
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<u32>,
|
||||
/// 协议类型:openai_compat(默认)/ anthropic(GLM 订阅 / Claude 官方)
|
||||
protocol: String,
|
||||
/// model 为空时的占位,避免 provider 构造 panic
|
||||
default_model: String,
|
||||
}
|
||||
|
||||
/// 从节点 config + 上游输入解析 AI 节点参数
|
||||
///
|
||||
/// prompt 取值优先级:上游 `inputs["prompt"]` > `config.prompt`,两者皆无则报错。
|
||||
/// model 为空时 default_model 兜底为 "gpt-4o-mini"。protocol 默认 openai_compat。
|
||||
fn parse_params(
|
||||
config: &serde_json::Value,
|
||||
inputs: &HashMap<String, NodeOutput>,
|
||||
) -> anyhow::Result<AiNodeParams> {
|
||||
// ── provider 配置(必填)──
|
||||
let base_url = config
|
||||
.get("base_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: base_url"))?
|
||||
.to_string();
|
||||
let api_key = config
|
||||
.get("api_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: api_key"))?
|
||||
.to_string();
|
||||
|
||||
// ── prompt(必填):优先取上游节点 "prompt" 输出,回退 config.prompt ──
|
||||
let prompt = inputs
|
||||
.get("prompt")
|
||||
.and_then(|o| o.data.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| {
|
||||
config
|
||||
.get("prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: prompt(config 或上游输入均无)"))?;
|
||||
|
||||
// ── 可选参数 ──
|
||||
let model = config
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let temperature = config
|
||||
.get("temperature")
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|f| f as f32);
|
||||
let max_tokens = config
|
||||
.get("max_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as u32);
|
||||
let system_prompt = config
|
||||
.get("system_prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let protocol = config
|
||||
.get("protocol")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("openai_compat")
|
||||
.to_string();
|
||||
// default_model:留空时给一个占位,避免 provider 构造 panic
|
||||
let default_model = if model.is_empty() {
|
||||
"gpt-4o-mini".to_string()
|
||||
} else {
|
||||
model.clone()
|
||||
};
|
||||
|
||||
Ok(AiNodeParams {
|
||||
base_url,
|
||||
api_key,
|
||||
prompt,
|
||||
system_prompt,
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
protocol,
|
||||
default_model,
|
||||
})
|
||||
}
|
||||
|
||||
/// AI 节点
|
||||
pub struct AiNode;
|
||||
|
||||
@@ -20,90 +112,23 @@ impl Node for AiNode {
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult {
|
||||
tracing::info!("AiNode 执行: node_id={}", ctx.node_id);
|
||||
|
||||
// ── provider 配置(必填)──
|
||||
let base_url = ctx
|
||||
.config
|
||||
.get("base_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: base_url"))?
|
||||
.to_string();
|
||||
let p = parse_params(&ctx.config, &ctx.inputs)?;
|
||||
|
||||
let api_key = ctx
|
||||
.config
|
||||
.get("api_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: api_key"))?
|
||||
.to_string();
|
||||
|
||||
// ── prompt(必填):优先取上游节点 "prompt" 输出,回退 config.prompt ──
|
||||
let prompt = ctx
|
||||
.inputs
|
||||
.get("prompt")
|
||||
.and_then(|o| o.data.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| {
|
||||
ctx.config
|
||||
.get("prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: prompt(config 或上游输入均无)"))?;
|
||||
|
||||
// ── 可选参数 ──
|
||||
let model = ctx
|
||||
.config
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let temperature = ctx
|
||||
.config
|
||||
.get("temperature")
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|f| f as f32);
|
||||
let max_tokens = ctx
|
||||
.config
|
||||
.get("max_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as u32);
|
||||
let system_prompt = ctx
|
||||
.config
|
||||
.get("system_prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// 协议类型:默认 openai_compat,可设 anthropic(GLM 订阅端点 / Claude 官方)
|
||||
let protocol = ctx
|
||||
.config
|
||||
.get("protocol")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("openai_compat")
|
||||
.to_string();
|
||||
|
||||
// default_model:留空时给一个占位,避免 provider 构造 panic
|
||||
let default_model = if model.is_empty() {
|
||||
"gpt-4o-mini".to_string()
|
||||
} else {
|
||||
model.clone()
|
||||
};
|
||||
|
||||
let provider: Box<dyn LlmProvider> = match protocol.as_str() {
|
||||
"anthropic" => Box::new(AnthropicCompatProvider::new(&base_url, &api_key, &default_model)),
|
||||
_ => Box::new(OpenAICompatProvider::new(&base_url, &api_key, &default_model)),
|
||||
};
|
||||
let provider: Box<dyn LlmProvider> =
|
||||
df_ai::build_provider(&p.protocol, &p.base_url, &p.api_key, &p.default_model);
|
||||
|
||||
// ── 构建消息 ──
|
||||
let mut messages = Vec::with_capacity(2);
|
||||
if let Some(sys) = system_prompt {
|
||||
if let Some(sys) = p.system_prompt {
|
||||
messages.push(ChatMessage::system(sys));
|
||||
}
|
||||
messages.push(ChatMessage::user(prompt));
|
||||
messages.push(ChatMessage::user(p.prompt));
|
||||
|
||||
let request = CompletionRequest {
|
||||
model: model.clone(),
|
||||
model: p.model.clone(),
|
||||
messages,
|
||||
temperature,
|
||||
max_tokens,
|
||||
temperature: p.temperature,
|
||||
max_tokens: p.max_tokens,
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
@@ -111,8 +136,8 @@ impl Node for AiNode {
|
||||
|
||||
tracing::info!(
|
||||
"AiNode 调用 LLM: model={}, base_url={}",
|
||||
default_model,
|
||||
base_url
|
||||
p.default_model,
|
||||
p.base_url
|
||||
);
|
||||
let response = provider.complete(request).await?;
|
||||
|
||||
@@ -148,7 +173,7 @@ impl Node for AiNode {
|
||||
"temperature": { "type": "number", "description": "温度 0.0~2.0(可选)" },
|
||||
"max_tokens": { "type": "integer", "description": "最大生成 token(可选,anthropic 协议无值时默认 4096)" }
|
||||
},
|
||||
"required": ["prompt", "base_url", "api_key"]
|
||||
"required": ["base_url", "api_key"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
@@ -165,3 +190,137 @@ impl Node for AiNode {
|
||||
"ai"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
/// 造带基础三字段(base_url/api_key/prompt)的 config,overrides 覆盖或追加
|
||||
fn config_with(overrides: serde_json::Value) -> serde_json::Value {
|
||||
let mut base = json!({
|
||||
"base_url": "https://api.example.com",
|
||||
"api_key": "sk-test",
|
||||
"prompt": "config-prompt"
|
||||
});
|
||||
if let (serde_json::Value::Object(b), serde_json::Value::Object(o)) = (&mut base, overrides) {
|
||||
for (k, v) in o {
|
||||
b.insert(k, v);
|
||||
}
|
||||
}
|
||||
base
|
||||
}
|
||||
|
||||
fn empty_inputs() -> HashMap<String, NodeOutput> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_base_url_errors() {
|
||||
let config = json!({ "api_key": "k", "prompt": "p" });
|
||||
let err = parse_params(&config, &empty_inputs()).unwrap_err().to_string();
|
||||
assert!(err.contains("base_url"), "缺 base_url 应报错, 实际: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_api_key_errors() {
|
||||
let config = json!({ "base_url": "http://x", "prompt": "p" });
|
||||
let err = parse_params(&config, &empty_inputs()).unwrap_err().to_string();
|
||||
assert!(err.contains("api_key"), "缺 api_key 应报错, 实际: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_errors() {
|
||||
let config = json!({ "base_url": "http://x", "api_key": "k" });
|
||||
let err = parse_params(&config, &empty_inputs()).unwrap_err().to_string();
|
||||
assert!(err.contains("prompt"), "缺 prompt 应报错, 实际: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_from_config_when_no_upstream() {
|
||||
let p = parse_params(&config_with(json!({})), &empty_inputs()).unwrap();
|
||||
assert_eq!(p.prompt, "config-prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_prefers_upstream_input_over_config() {
|
||||
let mut inputs = empty_inputs();
|
||||
inputs.insert(
|
||||
"prompt".to_string(),
|
||||
NodeOutput::from_value(json!("upstream-prompt")),
|
||||
);
|
||||
let p = parse_params(&config_with(json!({})), &inputs).unwrap();
|
||||
assert_eq!(p.prompt, "upstream-prompt", "上游输入应优先于 config.prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_when_optional_fields_missing() {
|
||||
let p = parse_params(&config_with(json!({})), &empty_inputs()).unwrap();
|
||||
assert_eq!(p.model, "");
|
||||
assert_eq!(p.default_model, "gpt-4o-mini", "model 空时 default_model 兜底");
|
||||
assert_eq!(p.protocol, "openai_compat", "protocol 默认 openai_compat");
|
||||
assert_eq!(p.temperature, None);
|
||||
assert_eq!(p.max_tokens, None);
|
||||
assert_eq!(p.system_prompt, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_fields_parsed_when_present() {
|
||||
let p = parse_params(
|
||||
&config_with(json!({
|
||||
"model": "glm-4",
|
||||
"protocol": "anthropic",
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 1024,
|
||||
"system_prompt": "你是助手"
|
||||
})),
|
||||
&empty_inputs(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p.model, "glm-4");
|
||||
assert_eq!(p.default_model, "glm-4", "model 非空时 default_model = model");
|
||||
assert_eq!(p.protocol, "anthropic");
|
||||
assert_eq!(p.temperature, Some(0.3));
|
||||
assert_eq!(p.max_tokens, Some(1024));
|
||||
assert_eq!(p.system_prompt.as_deref(), Some("你是助手"));
|
||||
}
|
||||
|
||||
/// 真调 GLM 验证 provider 调用层(parse_params 已由上方单测覆盖,此处补 complete 端到端)
|
||||
///
|
||||
/// `#[ignore]`:需真实 GLM 配置(env var),默认不跑。
|
||||
/// 跑法:`GLM_BASE_URL=... GLM_API_KEY=... GLM_MODEL=glm-4-flash \
|
||||
/// cargo test -p df-nodes --lib glm_live_complete -- --ignored --nocapture`
|
||||
/// env var 缺失 → 跳过(非失败)。
|
||||
#[ignore = "需真实 GLM 配置(env var)"]
|
||||
#[tokio::test]
|
||||
async fn glm_live_complete() {
|
||||
let base_url = std::env::var("GLM_BASE_URL").ok().filter(|s| !s.is_empty());
|
||||
let api_key = std::env::var("GLM_API_KEY").ok().filter(|s| !s.is_empty());
|
||||
let (base_url, api_key) = match (base_url, api_key) {
|
||||
(Some(b), Some(k)) => (b, k),
|
||||
_ => {
|
||||
eprintln!("跳过: 未设 GLM_BASE_URL / GLM_API_KEY env var");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let model = std::env::var("GLM_MODEL").unwrap_or_else(|_| "glm-4-flash".to_string());
|
||||
|
||||
let provider: Box<dyn LlmProvider> =
|
||||
df_ai::build_provider("openai_compat", &base_url, &api_key, &model);
|
||||
let request = CompletionRequest {
|
||||
model: model.clone(),
|
||||
messages: vec![ChatMessage::user("只回复两个字:通过")],
|
||||
temperature: Some(0.0),
|
||||
max_tokens: Some(16),
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
};
|
||||
let response = provider.complete(request).await.expect("GLM 调用失败");
|
||||
assert!(!response.text.is_empty(), "GLM 返回空文本");
|
||||
println!(
|
||||
"GLM 响应: model={}, text={}, usage={:?}",
|
||||
response.model, response.text, response.usage
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//! Docker 节点 — 在容器中执行任务
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||
|
||||
/// Docker 节点
|
||||
pub struct DockerNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for DockerNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 接入 df-execute 的 Docker 执行器
|
||||
tracing::info!("DockerNode 执行: 在容器中运行");
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": { "type": "string" },
|
||||
"command": { "type": "string" },
|
||||
"env": { "type": "object" }
|
||||
},
|
||||
"required": ["image"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stdout": { "type": "string" },
|
||||
"exit_code": { "type": "integer" }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"docker"
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//! Git 节点 — 执行 Git 操作(克隆、提交、推送、合并等)
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||
|
||||
/// Git 节点
|
||||
pub struct GitNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for GitNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 接入 df-execute 的 Git 操作
|
||||
tracing::info!("GitNode 执行: Git 操作");
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "enum": ["clone", "commit", "push", "merge", "checkout"] },
|
||||
"repo": { "type": "string" },
|
||||
"branch": { "type": "string" }
|
||||
},
|
||||
"required": ["action"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": { "type": "boolean" },
|
||||
"message": { "type": "string" }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"git"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//! HTTP 节点 — 发起 HTTP 请求
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||
|
||||
/// HTTP 节点
|
||||
pub struct HttpNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for HttpNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 实现HTTP请求逻辑
|
||||
tracing::info!("HttpNode 执行: 发送 HTTP 请求");
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": { "type": "string", "enum": ["GET", "POST", "PUT", "DELETE"] },
|
||||
"url": { "type": "string" },
|
||||
"headers": { "type": "object" },
|
||||
"body": {}
|
||||
},
|
||||
"required": ["method", "url"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": { "type": "integer" },
|
||||
"body": {},
|
||||
"headers": { "type": "object" }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"http"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
//! df-nodes: 内置节点集合 — AI、脚本、Docker、Git、人工审批、HTTP、子流程、通知
|
||||
//! df-nodes: 内置节点集合 — AI、脚本、人工审批
|
||||
|
||||
pub mod ai_node;
|
||||
pub mod docker_node;
|
||||
pub mod git_node;
|
||||
pub mod http_node;
|
||||
pub mod human_node;
|
||||
pub mod notify_node;
|
||||
pub mod script_node;
|
||||
pub mod subflow_node;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//! 通知节点 — 发送通知(邮件、飞书、钉钉等)
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||
|
||||
/// 通知节点
|
||||
pub struct NotifyNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for NotifyNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 实现通知发送逻辑
|
||||
tracing::info!("NotifyNode 执行: 发送通知");
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": { "type": "string", "enum": ["email", "feishu", "dingtalk", "webhook"] },
|
||||
"to": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"body": { "type": "string" }
|
||||
},
|
||||
"required": ["channel", "to", "body"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": { "type": "boolean" }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"notify"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
//! 子流程节点 — 嵌套执行另一个工作流
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||
|
||||
/// 子流程节点
|
||||
pub struct SubflowNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for SubflowNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 实现子工作流加载与执行
|
||||
tracing::info!("SubflowNode 执行: 启动子工作流");
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workflow_id": { "type": "string" },
|
||||
"inputs": { "type": "object" }
|
||||
},
|
||||
"required": ["workflow_id"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outputs": { "type": "object" }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"subflow"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user