新增: 初始化 DevFlow 项目仓库

Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate
(df-ai / df-storage / df-workflow / df-core / df-execute 等)。
核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、
任务/想法/项目/阶段管理、可追溯性,及配套前端组件。
This commit is contained in:
2026-06-12 01:31:05 +08:00
commit 98393b4908
178 changed files with 27859 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
[package]
name = "df-nodes"
version = "0.1.0"
edition = "2021"
[dependencies]
df-core = { path = "../df-core" }
df-execute = { path = "../df-execute" }
df-workflow = { path = "../df-workflow" }
df-ai = { path = "../df-ai" }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
async-trait = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }

View File

@@ -0,0 +1,167 @@
//! AI 节点 — 调用 LLM 完成文本生成/分析任务
//!
//! 工作流中无人值守的 AI 步骤:从节点 config 读取 OpenAI 兼容 provider 配置与
//! prompt自建 client 调用一次 LLM输出文本供下游节点消费。
//!
//! 与 AI Chat侧边栏交互对话的区别AI Node 由 DAG Executor 自动驱动,
//! 适合嵌入自动化链路(如 想法 → AI分析 → 脚本落地 → 人工审批)。
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 节点
pub struct AiNode;
#[async_trait]
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 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 缺少必填参数: promptconfig 或上游输入均无)"))?;
// ── 可选参数 ──
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可设 anthropicGLM 订阅端点 / 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 mut messages = Vec::with_capacity(2);
if let Some(sys) = system_prompt {
messages.push(ChatMessage::system(sys));
}
messages.push(ChatMessage::user(prompt));
let request = CompletionRequest {
model: model.clone(),
messages,
temperature,
max_tokens,
stream: false,
tools: None,
tool_choice: None,
};
tracing::info!(
"AiNode 调用 LLM: model={}, base_url={}",
default_model,
base_url
);
let response = provider.complete(request).await?;
tracing::info!(
"AiNode 完成: model={}, prompt_tokens={}, completion_tokens={}",
response.model,
response.usage.prompt_tokens,
response.usage.completion_tokens
);
Ok(NodeOutput::from_value(serde_json::json!({
"text": response.text,
"model": response.model,
"usage": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens,
},
})))
}
fn schema(&self) -> NodeSchema {
NodeSchema {
params: serde_json::json!({
"type": "object",
"properties": {
"prompt": { "type": "string", "description": "用户提示词(若无则取上游 prompt 输出)" },
"system_prompt": { "type": "string", "description": "系统提示词(可选)" },
"protocol": { "type": "string", "description": "协议类型openai_compat默认或 anthropicGLM订阅/Claude官方" },
"base_url": { "type": "string", "description": "API 地址OpenAI 兼容如 https://api.deepseek.comAnthropic 如 https://open.bigmodel.cn/api/anthropic" },
"api_key": { "type": "string", "description": "API 密钥" },
"model": { "type": "string", "description": "模型名(可选,留空用默认)" },
"temperature": { "type": "number", "description": "温度 0.0~2.0(可选)" },
"max_tokens": { "type": "integer", "description": "最大生成 token可选anthropic 协议无值时默认 4096" }
},
"required": ["prompt", "base_url", "api_key"]
}),
output: serde_json::json!({
"type": "object",
"properties": {
"text": { "type": "string" },
"model": { "type": "string" },
"usage": { "type": "object" }
}
}),
}
}
fn node_type(&self) -> &str {
"ai"
}
}

View File

@@ -0,0 +1,41 @@
//! 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"
}
}

View File

@@ -0,0 +1,41 @@
//! 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"
}
}

View File

@@ -0,0 +1,43 @@
//! 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"
}
}

View File

@@ -0,0 +1,96 @@
//! 人工审批节点 — 阻塞工作流,等待人工确认或输入
use async_trait::async_trait;
use std::time::Duration;
use tokio::time;
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
use df_core::events::WorkflowEvent;
/// 人工审批节点(阻塞节点)
pub struct HumanNode;
#[async_trait]
impl Node for HumanNode {
async fn execute(&self, ctx: NodeContext) -> NodeResult {
let config = ctx.config.as_object().cloned().unwrap_or_default();
let title = config.get("title")
.and_then(|v| v.as_str())
.unwrap_or("请确认");
let description = config.get("description")
.and_then(|v| v.as_str())
.unwrap_or("");
let options = config.get("options")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>())
.unwrap_or_else(|| vec!["同意", "拒绝"]);
// 发送人工审批请求到事件总线
let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest {
execution_id: ctx.execution_id.clone(),
node_id: ctx.node_id.clone(),
title: title.to_string(),
description: description.to_string(),
options: options.iter().map(|s| s.to_string()).collect(),
});
// 等待用户响应(最多等待 1 小时)
let timeout = Duration::from_secs(3600);
let start_time = std::time::Instant::now();
loop {
// 检查是否超时
if start_time.elapsed() > timeout {
return Err(anyhow::anyhow!("人工审批超时"));
}
// 检查节点是否被取消
if ctx.node_status.is_cancelled(&ctx.node_id) {
return Err(anyhow::anyhow!("人工审批被取消"));
}
// TODO: 检查审批响应(需要前端实现)
// 目前直接返回同意
let output = serde_json::json!({
"decision": "同意",
"comment": "",
});
return Ok(NodeOutput::from_value(output));
}
}
fn schema(&self) -> NodeSchema {
NodeSchema {
params: serde_json::json!({
"type": "object",
"properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"options": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["title"]
}),
output: serde_json::json!({
"type": "object",
"properties": {
"decision": { "type": "string" },
"comment": { "type": "string" }
}
}),
}
}
fn is_blocking(&self) -> bool {
true
}
fn node_type(&self) -> &str {
"human"
}
}

View File

@@ -0,0 +1,10 @@
//! df-nodes: 内置节点集合 — AI、脚本、Docker、Git、人工审批、HTTP、子流程、通知
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;

View File

@@ -0,0 +1,41 @@
//! 通知节点 — 发送通知(邮件、飞书、钉钉等)
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"
}
}

View File

@@ -0,0 +1,94 @@
//! 脚本节点 — 执行 Shell 脚本或自定义命令
use async_trait::async_trait;
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
/// 脚本节点
pub struct ScriptNode;
#[async_trait]
impl Node for ScriptNode {
async fn execute(&self, ctx: NodeContext) -> NodeResult {
tracing::info!("ScriptNode 执行: {}", ctx.node_id);
// 从 config 解析 command必填
let command = ctx
.config
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("ScriptNode 缺少必填参数: command"))?;
// 解析可选参数
let timeout_secs = ctx
.config
.get("timeout_secs")
.and_then(|v| v.as_u64());
let working_dir = ctx
.config
.get("working_dir")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// 构建请求并执行
let request = df_execute::shell::ShellRequest {
command: command.to_string(),
working_dir,
env: std::collections::HashMap::new(),
timeout_secs,
};
tracing::info!("ScriptNode 执行命令: {}", command);
let result = df_execute::shell::execute(request).await?;
// 非零退出码视为执行失败
let exit_code = result.exit_code.unwrap_or(-1);
if exit_code != 0 {
anyhow::bail!(
"脚本执行失败 (exit_code={}): {}",
exit_code,
result.stderr.trim()
);
}
tracing::info!(
"ScriptNode 完成: 耗时 {}ms, exit_code={}",
result.duration_ms,
exit_code
);
Ok(NodeOutput::from_value(serde_json::json!({
"stdout": result.stdout,
"stderr": result.stderr,
"exit_code": exit_code,
"duration_ms": result.duration_ms,
})))
}
fn schema(&self) -> NodeSchema {
NodeSchema {
params: serde_json::json!({
"type": "object",
"properties": {
"command": { "type": "string" },
"timeout_secs": { "type": "integer" },
"working_dir": { "type": "string" }
},
"required": ["command"]
}),
output: serde_json::json!({
"type": "object",
"properties": {
"stdout": { "type": "string" },
"stderr": { "type": "string" },
"exit_code": { "type": "integer" },
"duration_ms": { "type": "integer" }
}
}),
}
}
fn node_type(&self) -> &str {
"script"
}
}

View File

@@ -0,0 +1,39 @@
//! 子流程节点 — 嵌套执行另一个工作流
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"
}
}