新增: 初始化 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:
18
crates/df-ai/Cargo.toml
Normal file
18
crates/df-ai/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "df-ai"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
async-trait = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# HTTP + 流式
|
||||
reqwest = { version = "0.12", features = ["stream", "json"] }
|
||||
futures = "0.3"
|
||||
eventsource-stream = "0.2"
|
||||
166
crates/df-ai/src/ai_tools.rs
Normal file
166
crates/df-ai/src/ai_tools.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
//! AI 工具注册 — 将现有 CRUD 操作注册为 LLM 可调用的 Tool
|
||||
//!
|
||||
//! 风险分级:
|
||||
//! - Low: 只读操作(list/get),AI 自动执行
|
||||
//! - Medium: 创建操作(create),显示意图,可配置自动批准
|
||||
//! - High: 破坏性操作(delete / run_workflow),必须人工批准
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::provider::ToolDefinition;
|
||||
|
||||
// ============================================================
|
||||
// 风险级别
|
||||
// ============================================================
|
||||
|
||||
/// 工具调用风险级别
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RiskLevel {
|
||||
/// 只读操作 — AI 自动执行
|
||||
Low,
|
||||
/// 创建操作 — 可配置自动批准
|
||||
Medium,
|
||||
/// 破坏性操作 — 必须人工批准
|
||||
High,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具定义
|
||||
// ============================================================
|
||||
|
||||
/// 已注册的 AI 工具
|
||||
pub struct AiTool {
|
||||
/// 工具定义(发送给 LLM 的 JSON Schema)
|
||||
pub definition: ToolDefinition,
|
||||
/// 风险级别
|
||||
pub risk_level: RiskLevel,
|
||||
/// 执行处理器
|
||||
pub handler: AiToolHandler,
|
||||
}
|
||||
|
||||
/// 工具处理器类型 — 异步函数,接收 JSON 参数,返回 JSON 结果
|
||||
pub type AiToolHandler =
|
||||
Box<dyn Fn(Value) -> Pin<Box<dyn Future<Output = anyhow::Result<Value>> + Send>> + Send + Sync>;
|
||||
|
||||
// ============================================================
|
||||
// 工具注册表
|
||||
// ============================================================
|
||||
|
||||
/// AI 工具注册表
|
||||
pub struct AiToolRegistry {
|
||||
tools: HashMap<String, AiTool>,
|
||||
}
|
||||
|
||||
impl AiToolRegistry {
|
||||
/// 创建空注册表
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册一个工具
|
||||
pub fn register(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
parameters: Value,
|
||||
risk_level: RiskLevel,
|
||||
handler: AiToolHandler,
|
||||
) {
|
||||
let name_str = name.into();
|
||||
let definition = ToolDefinition::function(&name_str, description, parameters);
|
||||
self.tools.insert(
|
||||
name_str,
|
||||
AiTool {
|
||||
definition,
|
||||
risk_level,
|
||||
handler,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取所有工具定义(发送给 LLM 的 tools 参数)
|
||||
pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
|
||||
self.tools.values().map(|t| t.definition.clone()).collect()
|
||||
}
|
||||
|
||||
/// 根据名称获取工具
|
||||
pub fn get(&self, name: &str) -> Option<&AiTool> {
|
||||
self.tools.get(name)
|
||||
}
|
||||
|
||||
/// 获取所有已注册工具名称
|
||||
pub fn tool_names(&self) -> Vec<String> {
|
||||
self.tools.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// 已注册工具数量
|
||||
pub fn len(&self) -> usize {
|
||||
self.tools.len()
|
||||
}
|
||||
|
||||
/// 是否为空
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tools.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AiToolRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具执行结果
|
||||
// ============================================================
|
||||
|
||||
/// 工具执行结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolExecutionResult {
|
||||
/// 工具调用 ID
|
||||
pub tool_call_id: String,
|
||||
/// 工具名称
|
||||
pub tool_name: String,
|
||||
/// 执行参数
|
||||
pub arguments: Value,
|
||||
/// 执行结果
|
||||
pub result: Value,
|
||||
/// 是否需要人工批准
|
||||
pub approval_required: bool,
|
||||
/// 风险级别
|
||||
pub risk_level: RiskLevel,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助: 构建 JSON Schema 参数
|
||||
// ============================================================
|
||||
|
||||
/// 构建一个简单的 object JSON Schema
|
||||
pub fn object_schema(properties: Vec<(&str, &str, bool)>) -> Value {
|
||||
let mut props = serde_json::Map::new();
|
||||
let mut required = Vec::new();
|
||||
|
||||
for (name, type_str, is_required) in properties {
|
||||
props.insert(
|
||||
name.to_string(),
|
||||
serde_json::json!({ "type": type_str, "description": "" }),
|
||||
);
|
||||
if is_required {
|
||||
required.push(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": required,
|
||||
})
|
||||
}
|
||||
426
crates/df-ai/src/anthropic_compat.rs
Normal file
426
crates/df-ai/src/anthropic_compat.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
//! Anthropic 兼容 Provider — 通过 /v1/messages 端点实现
|
||||
//!
|
||||
//! 覆盖: Claude 官方 / GLM 订阅端点 (open.bigmodel.cn/api/anthropic) / 任意 Messages API 网关
|
||||
//! 支持: 同步调用 + SSE 流式 + Tool Use
|
||||
//!
|
||||
//! 与 OpenAI 协议的关键差异由本模块内部完成转换,对外仍暴露统一的 LlmProvider trait,
|
||||
//! 上层 (Agentic Loop / AiNode) 无需感知协议。
|
||||
|
||||
use async_trait::async_trait;
|
||||
use eventsource_stream::Eventsource;
|
||||
use futures::{Stream, StreamExt};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::provider::{
|
||||
CompletionRequest, CompletionResponse, LlmProvider, MessageRole, ProviderFeatures,
|
||||
StreamChunk, StreamResult, TokenUsage, ToolCall, ToolCallDelta,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Anthropic API 请求/响应结构体
|
||||
// ============================================================
|
||||
|
||||
/// Anthropic 请求体
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AnthropicRequest {
|
||||
model: String,
|
||||
messages: Vec<serde_json::Value>,
|
||||
max_tokens: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
system: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
temperature: Option<f32>,
|
||||
stream: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tools: Option<Vec<AnthropicToolDef>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_choice: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Anthropic 工具定义(input_schema 对应 OpenAI 的 parameters)
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AnthropicToolDef {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
input_schema: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Anthropic 同步响应
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnthropicResponse {
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
model: String,
|
||||
content: Vec<AnthropicContentBlock>,
|
||||
#[allow(dead_code)]
|
||||
stop_reason: Option<String>,
|
||||
usage: AnthropicUsage,
|
||||
}
|
||||
|
||||
/// 响应 content 块(text 或 tool_use)
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnthropicContentBlock {
|
||||
#[serde(rename = "type")]
|
||||
block_type: String,
|
||||
#[serde(default)]
|
||||
text: Option<String>,
|
||||
/// tool_use 块字段
|
||||
id: Option<String>,
|
||||
name: Option<String>,
|
||||
input: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnthropicUsage {
|
||||
input_tokens: u32,
|
||||
output_tokens: u32,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Provider 实现
|
||||
// ============================================================
|
||||
|
||||
/// Anthropic 兼容 LLM Provider
|
||||
pub struct AnthropicCompatProvider {
|
||||
client: Client,
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
default_model: String,
|
||||
}
|
||||
|
||||
/// Anthropic 流式协议版本头
|
||||
const ANTHROPIC_VERSION: &str = "2023-06-01";
|
||||
/// Anthropic max_tokens 必填,缺省时的兜底值
|
||||
const DEFAULT_MAX_TOKENS: u32 = 4096;
|
||||
|
||||
impl AnthropicCompatProvider {
|
||||
/// 创建 Provider
|
||||
///
|
||||
/// - `base_url`: 如 `https://api.anthropic.com`、`https://open.bigmodel.cn/api/anthropic`
|
||||
/// - `api_key`: API 密钥(Anthropic 用 x-api-key 头,非 Bearer)
|
||||
/// - `default_model`: 默认模型名称
|
||||
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
|
||||
let client = Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("reqwest builder 失败,降级为默认 client: {}", e);
|
||||
Client::new()
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
api_key: api_key.into(),
|
||||
base_url: base_url.into(),
|
||||
default_model: default_model.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 messages 端点 URL
|
||||
///
|
||||
/// - 已含 `/v1/messages` → 直接用
|
||||
/// - 以 `/v1` 结尾 → 补 `/messages`
|
||||
/// - 否则(如 `.../api/anthropic`、`api.anthropic.com`)→ 补 `/v1/messages`
|
||||
fn messages_url(&self) -> String {
|
||||
let base = self.base_url.trim_end_matches('/');
|
||||
if base.ends_with("/v1/messages") {
|
||||
return base.to_string();
|
||||
}
|
||||
if base.ends_with("/v1") {
|
||||
return format!("{}/messages", base);
|
||||
}
|
||||
format!("{}/v1/messages", base)
|
||||
}
|
||||
|
||||
/// 将统一 CompletionRequest 转换为 Anthropic 请求体
|
||||
///
|
||||
/// 转换要点:
|
||||
/// - system 消息从 messages 抽离到顶层 system 字段
|
||||
/// - assistant 带 tool_calls → content 数组含 text + tool_use 块
|
||||
/// - tool_result(role=Tool)→ user 消息含 tool_result 块;连续多个合并为一条 user
|
||||
/// - tool_definitions 的 parameters → input_schema
|
||||
fn convert_request(&self, req: CompletionRequest) -> AnthropicRequest {
|
||||
let model = if req.model.is_empty() {
|
||||
self.default_model.clone()
|
||||
} else {
|
||||
req.model
|
||||
};
|
||||
|
||||
// 抽离 system 消息
|
||||
let system: Option<String> = {
|
||||
let sys: Vec<String> = req
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::System))
|
||||
.map(|m| m.content.clone())
|
||||
.collect();
|
||||
if sys.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sys.join("\n\n"))
|
||||
}
|
||||
};
|
||||
|
||||
// 构建非 system 消息(保留顺序,合并连续 tool_result)
|
||||
let mut messages: Vec<serde_json::Value> = Vec::new();
|
||||
let mut pending_tool_results: Vec<serde_json::Value> = Vec::new();
|
||||
|
||||
for m in req.messages.iter() {
|
||||
match m.role {
|
||||
MessageRole::System => continue,
|
||||
MessageRole::Tool => {
|
||||
// 累积 tool_result 块,遇到非 Tool 消息时 flush
|
||||
pending_tool_results.push(serde_json::json!({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": m.tool_call_id,
|
||||
"content": m.content,
|
||||
}));
|
||||
}
|
||||
MessageRole::User => {
|
||||
Self::flush_tool_results(&mut messages, &mut pending_tool_results);
|
||||
messages.push(serde_json::json!({ "role": "user", "content": m.content }));
|
||||
}
|
||||
MessageRole::Assistant => {
|
||||
Self::flush_tool_results(&mut messages, &mut pending_tool_results);
|
||||
let mut content: Vec<serde_json::Value> = Vec::new();
|
||||
if !m.content.is_empty() {
|
||||
content.push(serde_json::json!({ "type": "text", "text": m.content }));
|
||||
}
|
||||
if let Some(calls) = &m.tool_calls {
|
||||
for tc in calls {
|
||||
let input: serde_json::Value =
|
||||
serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::Value::Null);
|
||||
content.push(serde_json::json!({
|
||||
"type": "tool_use",
|
||||
"id": tc.id,
|
||||
"name": tc.function.name,
|
||||
"input": input,
|
||||
}));
|
||||
}
|
||||
}
|
||||
if content.is_empty() {
|
||||
content.push(serde_json::json!({ "type": "text", "text": "" }));
|
||||
}
|
||||
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::flush_tool_results(&mut messages, &mut pending_tool_results);
|
||||
|
||||
let tools = req.tools.map(|defs| {
|
||||
defs.into_iter()
|
||||
.map(|d| AnthropicToolDef {
|
||||
name: d.function.name,
|
||||
description: Some(d.function.description).filter(|s| !s.is_empty()),
|
||||
input_schema: d.function.parameters,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
AnthropicRequest {
|
||||
model,
|
||||
messages,
|
||||
max_tokens: req.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS),
|
||||
system,
|
||||
temperature: req.temperature,
|
||||
stream: req.stream,
|
||||
tools,
|
||||
tool_choice: req.tool_choice,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将累积的 tool_result 块作为一条 user 消息 flush 进消息列表
|
||||
fn flush_tool_results(
|
||||
messages: &mut Vec<serde_json::Value>,
|
||||
pending: &mut Vec<serde_json::Value>,
|
||||
) {
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
let blocks: Vec<serde_json::Value> = pending.drain(..).collect();
|
||||
messages.push(serde_json::json!({ "role": "user", "content": blocks }));
|
||||
}
|
||||
|
||||
/// 统一鉴权头:x-api-key + anthropic-version
|
||||
fn auth_headers(&self, rb: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
rb.header("x-api-key", &self.api_key)
|
||||
.header("anthropic-version", ANTHROPIC_VERSION)
|
||||
.header("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for AnthropicCompatProvider {
|
||||
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse> {
|
||||
let mut req = request;
|
||||
req.stream = false;
|
||||
let body = self.convert_request(req);
|
||||
|
||||
debug!(model = %body.model, "Anthropic 同步调用");
|
||||
|
||||
let resp = self
|
||||
.auth_headers(self.client.post(self.messages_url()))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
error!(%status, %text, "Anthropic API 调用失败");
|
||||
anyhow::bail!("Anthropic API 错误 {}: {}", status, text);
|
||||
}
|
||||
|
||||
let resp: AnthropicResponse = resp.json().await?;
|
||||
|
||||
// content 块中拼接 text,收集 tool_use
|
||||
let mut text = String::new();
|
||||
let mut tool_calls: Vec<ToolCall> = Vec::new();
|
||||
for block in resp.content {
|
||||
match block.block_type.as_str() {
|
||||
"text" => {
|
||||
if let Some(t) = block.text {
|
||||
text.push_str(&t);
|
||||
}
|
||||
}
|
||||
"tool_use" => {
|
||||
let id = block.id.unwrap_or_default();
|
||||
let name = block.name.unwrap_or_default();
|
||||
let args = block
|
||||
.input
|
||||
.map(|v| serde_json::to_string(&v).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
tool_calls.push(ToolCall::new(id, name, args));
|
||||
}
|
||||
other => warn!(block_type = other, "Anthropic 响应含未知 content 块类型,已忽略"),
|
||||
}
|
||||
}
|
||||
|
||||
let usage = TokenUsage {
|
||||
prompt_tokens: resp.usage.input_tokens,
|
||||
completion_tokens: resp.usage.output_tokens,
|
||||
total_tokens: resp.usage.input_tokens + resp.usage.output_tokens,
|
||||
};
|
||||
|
||||
Ok(CompletionResponse {
|
||||
text,
|
||||
model: resp.model,
|
||||
usage,
|
||||
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream(&self, request: CompletionRequest) -> anyhow::Result<StreamResult> {
|
||||
let mut req = request;
|
||||
req.stream = true;
|
||||
let body = self.convert_request(req);
|
||||
|
||||
debug!(model = %body.model, "Anthropic 流式调用");
|
||||
|
||||
let resp = self
|
||||
.auth_headers(self.client.post(self.messages_url()))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
error!(%status, %text, "Anthropic 流式 API 调用失败");
|
||||
anyhow::bail!("Anthropic 流式 API 错误 {}: {}", status, text);
|
||||
}
|
||||
|
||||
// 流式解析:eventsource 逐事件处理,按 type 字段分发转 StreamChunk
|
||||
let stream = resp
|
||||
.bytes_stream()
|
||||
.eventsource()
|
||||
.map(move |event| match event {
|
||||
Ok(ev) => {
|
||||
// 解析 data 中的 JSON,按 type 字段决定如何转 StreamChunk
|
||||
let v: serde_json::Value = match serde_json::from_str(&ev.data) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }),
|
||||
};
|
||||
let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
match ty {
|
||||
// 文本增量
|
||||
"content_block_delta" => {
|
||||
if let Some(delta) = v.get("delta") {
|
||||
if delta.get("type").and_then(|t| t.as_str()) == Some("text_delta") {
|
||||
let text = delta.get("text").and_then(|t| t.as_str()).unwrap_or("").to_string();
|
||||
return Ok(StreamChunk { delta: text, finished: false, tool_calls: None });
|
||||
}
|
||||
// 工具入参增量
|
||||
if delta.get("type").and_then(|t| t.as_str()) == Some("input_json_delta") {
|
||||
let partial = delta.get("partial_json").and_then(|t| t.as_str()).unwrap_or("").to_string();
|
||||
let idx = v.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as u32;
|
||||
return Ok(StreamChunk {
|
||||
delta: String::new(),
|
||||
finished: false,
|
||||
tool_calls: Some(vec![ToolCallDelta {
|
||||
index: idx,
|
||||
id: None,
|
||||
function_name: None,
|
||||
function_arguments: Some(partial),
|
||||
}]),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None })
|
||||
}
|
||||
// 工具块开始:带 id + name
|
||||
"content_block_start" => {
|
||||
if let Some(cb) = v.get("content_block") {
|
||||
if cb.get("type").and_then(|t| t.as_str()) == Some("tool_use") {
|
||||
let idx = v.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as u32;
|
||||
let id = cb.get("id").and_then(|t| t.as_str()).map(|s| s.to_string());
|
||||
let name = cb.get("name").and_then(|t| t.as_str()).map(|s| s.to_string());
|
||||
return Ok(StreamChunk {
|
||||
delta: String::new(),
|
||||
finished: false,
|
||||
tool_calls: Some(vec![ToolCallDelta {
|
||||
index: idx,
|
||||
id,
|
||||
function_name: name,
|
||||
function_arguments: None,
|
||||
}]),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None })
|
||||
}
|
||||
// 消息结束
|
||||
"message_stop" => Ok(StreamChunk { delta: String::new(), finished: true, tool_calls: None }),
|
||||
// 错误事件
|
||||
"error" => {
|
||||
let msg = v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()).unwrap_or("stream error");
|
||||
error!(%msg, "Anthropic 流式错误事件");
|
||||
Ok(StreamChunk { delta: String::new(), finished: true, tool_calls: None })
|
||||
}
|
||||
// message_start / content_block_stop / message_delta 等不产出 chunk
|
||||
_ => Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "Anthropic SSE 事件流错误");
|
||||
Err(anyhow::anyhow!("Anthropic SSE 错误: {}", e))
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"anthropic-compat"
|
||||
}
|
||||
|
||||
fn supported_features(&self) -> ProviderFeatures {
|
||||
ProviderFeatures {
|
||||
streaming: true,
|
||||
function_calling: true,
|
||||
vision: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/df-ai/src/context.rs
Normal file
59
crates/df-ai/src/context.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! 上下文管理器 — 管理对话上下文和 token 预算
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::provider::ChatMessage;
|
||||
|
||||
/// 上下文窗口配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContextConfig {
|
||||
/// 最大 token 数
|
||||
pub max_tokens: u32,
|
||||
/// 保留的系统提示 token 数
|
||||
pub system_reserve: u32,
|
||||
}
|
||||
|
||||
impl Default for ContextConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_tokens: 128_000,
|
||||
system_reserve: 4_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上下文管理器
|
||||
pub struct ContextManager {
|
||||
/// 消息历史
|
||||
messages: VecDeque<ChatMessage>,
|
||||
/// 配置
|
||||
config: ContextConfig,
|
||||
}
|
||||
|
||||
impl ContextManager {
|
||||
/// 创建上下文管理器
|
||||
pub fn new(config: ContextConfig) -> Self {
|
||||
Self {
|
||||
messages: VecDeque::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加消息
|
||||
pub fn push(&mut self, message: ChatMessage) {
|
||||
self.messages.push_back(message);
|
||||
// TODO: 当超过 token 预算时,淘汰旧消息
|
||||
}
|
||||
|
||||
/// 获取当前消息列表
|
||||
pub fn messages(&self) -> &VecDeque<ChatMessage> {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
/// 清空上下文
|
||||
pub fn clear(&mut self) {
|
||||
self.messages.clear();
|
||||
}
|
||||
}
|
||||
28
crates/df-ai/src/coordinator.rs
Normal file
28
crates/df-ai/src/coordinator.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! Agent 协调器 — 管理多 Agent 协作
|
||||
|
||||
/// Agent 协调器
|
||||
///
|
||||
/// TODO: 实现多 Agent 协作逻辑
|
||||
pub struct AgentCoordinator;
|
||||
|
||||
impl AgentCoordinator {
|
||||
/// 创建协调器
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// 启动 Agent 协作任务
|
||||
///
|
||||
/// TODO: 实现 Agent 间消息传递和任务分配
|
||||
pub async fn run(&self, _task: &str) -> anyhow::Result<String> {
|
||||
tracing::info!("AgentCoordinator: 协调任务开始");
|
||||
// TODO: 实现多 Agent 协作
|
||||
Ok("TODO: Agent 协作结果".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AgentCoordinator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
10
crates/df-ai/src/lib.rs
Normal file
10
crates/df-ai/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! df-ai: AI 编排 — LLM Provider、模型路由、Agent 协调、上下文管理、流式处理、工具注册
|
||||
|
||||
pub mod ai_tools;
|
||||
pub mod anthropic_compat;
|
||||
pub mod context;
|
||||
pub mod coordinator;
|
||||
pub mod openai_compat;
|
||||
pub mod provider;
|
||||
pub mod router;
|
||||
pub mod stream;
|
||||
410
crates/df-ai/src/openai_compat.rs
Normal file
410
crates/df-ai/src/openai_compat.rs
Normal file
@@ -0,0 +1,410 @@
|
||||
//! OpenAI 兼容 Provider — 通过 /v1/chat/completions 端点实现
|
||||
//!
|
||||
//! 覆盖: OpenAI / GLM (open.bigmodel.cn) / DeepSeek / Claude OpenAI 兼容模式
|
||||
//! 支持: 同步调用 + SSE 流式 + Function Calling / Tool Use
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use eventsource_stream::Eventsource;
|
||||
use futures::{Stream, StreamExt};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::provider::{
|
||||
CompletionRequest, CompletionResponse, LlmProvider, ProviderFeatures, StreamChunk, StreamResult,
|
||||
TokenUsage, ToolCall, ToolCallDelta,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// OpenAI API 请求/响应结构体
|
||||
// ============================================================
|
||||
|
||||
/// OpenAI 兼容请求体
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OpenAiRequest {
|
||||
model: String,
|
||||
messages: Vec<OpenAiMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_tokens: Option<u32>,
|
||||
stream: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tools: Option<Vec<serde_json::Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_choice: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// OpenAI 消息格式
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct OpenAiMessage {
|
||||
role: String,
|
||||
content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_call_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_calls: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// OpenAI 同步响应
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiResponse {
|
||||
choices: Vec<OpenAiChoice>,
|
||||
model: String,
|
||||
usage: Option<OpenAiUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiChoice {
|
||||
message: OpenAiMessageResp,
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiMessageResp {
|
||||
content: Option<String>,
|
||||
tool_calls: Option<Vec<OpenAiToolCallResp>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiToolCallResp {
|
||||
id: String,
|
||||
#[serde(rename = "type")]
|
||||
call_type: String,
|
||||
function: OpenAiFunctionResp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiFunctionResp {
|
||||
name: String,
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiUsage {
|
||||
prompt_tokens: u32,
|
||||
completion_tokens: u32,
|
||||
total_tokens: u32,
|
||||
}
|
||||
|
||||
/// SSE 流式响应 chunk
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiStreamChunk {
|
||||
choices: Vec<OpenAiStreamChoice>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiStreamChoice {
|
||||
delta: OpenAiStreamDelta,
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiStreamDelta {
|
||||
content: Option<String>,
|
||||
tool_calls: Option<Vec<OpenAiStreamToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiStreamToolCall {
|
||||
index: u32,
|
||||
id: Option<String>,
|
||||
function: Option<OpenAiStreamFunction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenAiStreamFunction {
|
||||
name: Option<String>,
|
||||
arguments: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OpenAI Compat Provider
|
||||
// ============================================================
|
||||
|
||||
/// OpenAI 兼容 LLM Provider
|
||||
pub struct OpenAICompatProvider {
|
||||
client: Client,
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
default_model: String,
|
||||
}
|
||||
|
||||
impl OpenAICompatProvider {
|
||||
/// 创建 Provider
|
||||
///
|
||||
/// - `base_url`: 如 "https://api.openai.com", "https://open.bigmodel.cn/api/paas", "https://api.deepseek.com"
|
||||
/// - `api_key`: API 密钥
|
||||
/// - `default_model`: 默认模型名称
|
||||
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
|
||||
// connect_timeout 防连接阶段无限 hang(网络静默断);不设总 timeout——
|
||||
// reqwest 的 .timeout() 会限制整个响应 body 时长,流式长生成任务会被误砍。
|
||||
// 读取阶段的中途静默由上层 stream_llm 的 idle timeout 兜底。
|
||||
let client = Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("reqwest builder 失败,降级为默认 client(无 connect_timeout): {}", e);
|
||||
Client::new()
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
api_key: api_key.into(),
|
||||
base_url: base_url.into(),
|
||||
default_model: default_model.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建完整 API URL
|
||||
///
|
||||
/// 智能拼接,兼容三种 base_url 约定:
|
||||
/// - 已含完整端点(…/chat/completions)→ 直接用
|
||||
/// - 已含版本段(…/v1 …/v4 等,如 GLM 的 /api/paas/v4)→ 补 /chat/completions
|
||||
/// - 仅域名无版本(如 api.openai.com / api.deepseek.com)→ 补 /v1/chat/completions(OpenAI 约定)
|
||||
fn chat_url(&self) -> String {
|
||||
let base = self.base_url.trim_end_matches('/');
|
||||
if base.ends_with("/chat/completions") {
|
||||
return base.to_string();
|
||||
}
|
||||
if Self::ends_with_version(base) {
|
||||
return format!("{}/chat/completions", base);
|
||||
}
|
||||
format!("{}/v1/chat/completions", base)
|
||||
}
|
||||
|
||||
/// base_url 是否以 `/v<数字>` 结尾(如 /v1 /v4)
|
||||
fn ends_with_version(base: &str) -> bool {
|
||||
match base.rsplit_once('/') {
|
||||
Some((_, last)) if last.starts_with('v') && last.len() > 1 => {
|
||||
last[1..].bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将通用请求转换为 OpenAI 格式
|
||||
fn convert_request(&self, req: CompletionRequest) -> OpenAiRequest {
|
||||
let model = if req.model.is_empty() {
|
||||
self.default_model.clone()
|
||||
} else {
|
||||
req.model
|
||||
};
|
||||
|
||||
let messages: Vec<OpenAiMessage> = req
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let role = match m.role {
|
||||
crate::provider::MessageRole::System => "system",
|
||||
crate::provider::MessageRole::User => "user",
|
||||
crate::provider::MessageRole::Assistant => "assistant",
|
||||
crate::provider::MessageRole::Tool => "tool",
|
||||
};
|
||||
let tool_calls = m.tool_calls.map(|calls| {
|
||||
calls
|
||||
.into_iter()
|
||||
.map(|tc| {
|
||||
serde_json::json!({
|
||||
"id": tc.id,
|
||||
"type": tc.call_type,
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
OpenAiMessage {
|
||||
role: role.to_string(),
|
||||
content: m.content,
|
||||
tool_call_id: m.tool_call_id,
|
||||
tool_calls,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tools = req.tools.map(|defs| {
|
||||
defs.into_iter()
|
||||
.map(|d| serde_json::to_value(d).unwrap_or_default())
|
||||
.collect()
|
||||
});
|
||||
|
||||
OpenAiRequest {
|
||||
model,
|
||||
messages,
|
||||
temperature: req.temperature,
|
||||
max_tokens: req.max_tokens,
|
||||
stream: req.stream,
|
||||
tools,
|
||||
tool_choice: req.tool_choice,
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析同步响应中的工具调用
|
||||
fn parse_tool_calls(calls: Vec<OpenAiToolCallResp>) -> Vec<ToolCall> {
|
||||
calls
|
||||
.into_iter()
|
||||
.map(|c| ToolCall::new(c.id, c.function.name, c.function.arguments))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for OpenAICompatProvider {
|
||||
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse> {
|
||||
let mut req = request;
|
||||
req.stream = false;
|
||||
let openai_req = self.convert_request(req);
|
||||
|
||||
debug!(model = %openai_req.model, "OpenAI 同步调用");
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.post(self.chat_url())
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&openai_req)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
error!(%status, %body, "LLM API 调用失败");
|
||||
anyhow::bail!("LLM API 错误 {}: {}", status, body);
|
||||
}
|
||||
|
||||
let body: OpenAiResponse = resp.json().await?;
|
||||
let choice = body
|
||||
.choices
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("LLM 响应无 choices"))?;
|
||||
|
||||
let text = choice.message.content.unwrap_or_default();
|
||||
let tool_calls = choice.message.tool_calls.map(Self::parse_tool_calls);
|
||||
|
||||
let usage = body.usage.map(|u| TokenUsage {
|
||||
prompt_tokens: u.prompt_tokens,
|
||||
completion_tokens: u.completion_tokens,
|
||||
total_tokens: u.total_tokens,
|
||||
}).unwrap_or(TokenUsage {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
});
|
||||
|
||||
Ok(CompletionResponse {
|
||||
text,
|
||||
model: body.model,
|
||||
usage,
|
||||
tool_calls,
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream(&self, request: CompletionRequest) -> anyhow::Result<StreamResult> {
|
||||
let mut req = request;
|
||||
req.stream = true;
|
||||
let openai_req = self.convert_request(req);
|
||||
|
||||
debug!(model = %openai_req.model, "OpenAI 流式调用");
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.post(self.chat_url())
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&openai_req)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
error!(%status, %body, "LLM 流式 API 调用失败");
|
||||
anyhow::bail!("LLM 流式 API 错误 {}: {}", status, body);
|
||||
}
|
||||
|
||||
let stream = resp
|
||||
.bytes_stream()
|
||||
.eventsource()
|
||||
.map(move |event| {
|
||||
match event {
|
||||
Ok(event) => {
|
||||
// OpenAI 发送 "data: [DONE]" 表示流结束
|
||||
if event.data == "[DONE]" {
|
||||
return Ok(StreamChunk {
|
||||
delta: String::new(),
|
||||
finished: true,
|
||||
tool_calls: None,
|
||||
});
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OpenAiStreamChunk>(&event.data) {
|
||||
Ok(chunk) => {
|
||||
if let Some(choice) = chunk.choices.into_iter().next() {
|
||||
let delta_text = choice.delta.content.unwrap_or_default();
|
||||
// "length" = max_tokens 截断,属正常终止(非断连),纳入 finished
|
||||
let finished = choice.finish_reason.as_deref() == Some("stop")
|
||||
|| choice.finish_reason.as_deref() == Some("tool_calls")
|
||||
|| choice.finish_reason.as_deref() == Some("length");
|
||||
|
||||
let tool_calls = choice.delta.tool_calls.map(|tcs| {
|
||||
tcs.into_iter()
|
||||
.map(|tc| ToolCallDelta {
|
||||
index: tc.index,
|
||||
id: tc.id,
|
||||
function_name: tc.function.as_ref().and_then(|f| f.name.clone()),
|
||||
function_arguments: tc.function.and_then(|f| f.arguments),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
Ok(StreamChunk {
|
||||
delta: delta_text,
|
||||
finished,
|
||||
tool_calls,
|
||||
})
|
||||
} else {
|
||||
Ok(StreamChunk {
|
||||
delta: String::new(),
|
||||
finished: false,
|
||||
tool_calls: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("SSE 数据解析失败: {} — data: {}", e, event.data);
|
||||
Ok(StreamChunk {
|
||||
delta: String::new(),
|
||||
finished: false,
|
||||
tool_calls: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("SSE 流错误: {}", e);
|
||||
Err(anyhow::anyhow!("SSE 流错误: {}", e))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.default_model
|
||||
}
|
||||
|
||||
fn supported_features(&self) -> ProviderFeatures {
|
||||
ProviderFeatures {
|
||||
streaming: true,
|
||||
function_calling: true,
|
||||
vision: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
207
crates/df-ai/src/provider.rs
Normal file
207
crates/df-ai/src/provider.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
//! LLM Provider trait — 统一的 LLM 调用抽象
|
||||
//!
|
||||
//! 支持 OpenAI 兼容 API(覆盖 OpenAI / GLM / DeepSeek / Claude 兼容模式),
|
||||
//! 含 function calling / tool use 能力。
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::Stream;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================
|
||||
// 核心数据结构
|
||||
// ============================================================
|
||||
|
||||
/// LLM 调用请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompletionRequest {
|
||||
/// 模型名称
|
||||
pub model: String,
|
||||
/// 提示消息列表
|
||||
pub messages: Vec<ChatMessage>,
|
||||
/// 温度(0.0 ~ 2.0)
|
||||
pub temperature: Option<f32>,
|
||||
/// 最大生成 token 数
|
||||
pub max_tokens: Option<u32>,
|
||||
/// 是否流式输出
|
||||
pub stream: bool,
|
||||
/// 可调用的工具定义
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<ToolDefinition>>,
|
||||
/// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"}
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 聊天消息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub role: MessageRole,
|
||||
pub content: String,
|
||||
/// 工具调用 ID(role=Tool 时必填)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_call_id: Option<String>,
|
||||
/// AI 发起的工具调用列表(role=Assistant 时可能有)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::System, content: content.into(), tool_call_id: None, tool_calls: None }
|
||||
}
|
||||
pub fn user(content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::User, content: content.into(), tool_call_id: None, tool_calls: None }
|
||||
}
|
||||
pub fn assistant(content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: None }
|
||||
}
|
||||
pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
|
||||
Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: Some(tool_calls) }
|
||||
}
|
||||
pub fn tool_result(call_id: impl Into<String>, content: impl Into<String>) -> Self {
|
||||
Self { role: MessageRole::Tool, content: content.into(), tool_call_id: Some(call_id.into()), tool_calls: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息角色
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MessageRole {
|
||||
System,
|
||||
User,
|
||||
Assistant,
|
||||
Tool,
|
||||
}
|
||||
|
||||
/// 工具定义
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDefinition {
|
||||
#[serde(rename = "type")]
|
||||
pub tool_type: String,
|
||||
pub function: ToolFunction,
|
||||
}
|
||||
|
||||
impl ToolDefinition {
|
||||
pub fn function(name: impl Into<String>, description: impl Into<String>, parameters: serde_json::Value) -> Self {
|
||||
Self {
|
||||
tool_type: "function".into(),
|
||||
function: ToolFunction { name: name.into(), description: description.into(), parameters },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 函数定义
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolFunction {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 工具调用(AI 发起)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCall {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub call_type: String,
|
||||
pub function: ToolCallFunction,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
call_type: "function".into(),
|
||||
function: ToolCallFunction { name: name.into(), arguments: arguments.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 工具调用函数部分
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCallFunction {
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
/// LLM 调用响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompletionResponse {
|
||||
/// 生成的文本
|
||||
pub text: String,
|
||||
/// 使用的模型
|
||||
pub model: String,
|
||||
/// 消耗的 token 数
|
||||
pub usage: TokenUsage,
|
||||
/// AI 发起的工具调用(如有)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
/// Token 用量
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TokenUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
/// Provider 支持的特性标志
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ProviderFeatures {
|
||||
pub streaming: bool,
|
||||
pub function_calling: bool,
|
||||
pub vision: bool,
|
||||
}
|
||||
|
||||
/// 流式输出的 chunk
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamChunk {
|
||||
/// 增量文本
|
||||
pub delta: String,
|
||||
/// 是否结束
|
||||
pub finished: bool,
|
||||
/// 工具调用增量(如有)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCallDelta>>,
|
||||
}
|
||||
|
||||
/// 工具调用增量(流式中的片段)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolCallDelta {
|
||||
/// 索引
|
||||
pub index: u32,
|
||||
/// 工具调用 ID(仅第一个 chunk 有)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
/// 函数名片段
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub function_name: Option<String>,
|
||||
/// 函数参数片段
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub function_arguments: Option<String>,
|
||||
}
|
||||
|
||||
/// 异步流类型别名
|
||||
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
|
||||
|
||||
/// LLM Provider trait
|
||||
#[async_trait]
|
||||
pub trait LlmProvider: Send + Sync {
|
||||
/// 同步调用
|
||||
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse>;
|
||||
|
||||
/// 流式调用(返回异步流)
|
||||
async fn stream(
|
||||
&self,
|
||||
request: CompletionRequest,
|
||||
) -> anyhow::Result<StreamResult>;
|
||||
|
||||
/// Provider 名称
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// 支持的特性
|
||||
fn supported_features(&self) -> ProviderFeatures;
|
||||
}
|
||||
51
crates/df-ai/src/router.rs
Normal file
51
crates/df-ai/src/router.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! 模型路由 — 根据任务类型选择最优模型
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 任务类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskType {
|
||||
/// 代码生成
|
||||
CodeGeneration,
|
||||
/// 代码审查
|
||||
CodeReview,
|
||||
/// 文档生成
|
||||
Documentation,
|
||||
/// 分析推理
|
||||
Analysis,
|
||||
/// 摘要总结
|
||||
Summarization,
|
||||
/// 通用对话
|
||||
Chat,
|
||||
}
|
||||
|
||||
/// 模型路由器
|
||||
///
|
||||
/// 根据任务类型、成本、延迟等选择最优模型
|
||||
pub struct ModelRouter {
|
||||
/// 默认模型
|
||||
default_model: String,
|
||||
}
|
||||
|
||||
impl ModelRouter {
|
||||
/// 创建路由器
|
||||
pub fn new(default_model: &str) -> Self {
|
||||
Self {
|
||||
default_model: default_model.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据任务类型路由到合适的模型
|
||||
pub fn route(&self, task_type: &TaskType) -> String {
|
||||
// TODO: 实现基于规则的模型路由
|
||||
match task_type {
|
||||
TaskType::CodeGeneration => self.default_model.clone(),
|
||||
TaskType::CodeReview => self.default_model.clone(),
|
||||
TaskType::Documentation => self.default_model.clone(),
|
||||
TaskType::Analysis => self.default_model.clone(),
|
||||
TaskType::Summarization => self.default_model.clone(),
|
||||
TaskType::Chat => self.default_model.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
45
crates/df-ai/src/stream.rs
Normal file
45
crates/df-ai/src/stream.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! 流式处理 — LLM 响应的流式输出管理
|
||||
|
||||
use crate::provider::StreamChunk;
|
||||
|
||||
/// 流式响应收集器
|
||||
pub struct StreamCollector {
|
||||
/// 已收集的文本
|
||||
text: String,
|
||||
/// 是否完成
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl StreamCollector {
|
||||
/// 创建收集器
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
text: String::new(),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 追加一个 chunk
|
||||
pub fn push(&mut self, chunk: &StreamChunk) {
|
||||
self.text.push_str(&chunk.delta);
|
||||
if chunk.finished {
|
||||
self.finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取已收集的文本
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
/// 是否已完成
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.finished
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StreamCollector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
11
crates/df-core/Cargo.toml
Normal file
11
crates/df-core/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "df-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
49
crates/df-core/src/error.rs
Normal file
49
crates/df-core/src/error.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! 统一错误类型定义
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// 统一错误类型
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("未找到: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("已存在: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("验证失败: {0}")]
|
||||
Validation(String),
|
||||
|
||||
#[error("状态错误: 当前状态 {current}, 期望 {expected}")]
|
||||
InvalidState {
|
||||
current: String,
|
||||
expected: String,
|
||||
},
|
||||
|
||||
#[error("工作流错误: {0}")]
|
||||
Workflow(String),
|
||||
|
||||
#[error("执行错误: {0}")]
|
||||
Execution(String),
|
||||
|
||||
#[error("存储错误: {0}")]
|
||||
Storage(String),
|
||||
|
||||
#[error("插件错误: {0}")]
|
||||
Plugin(String),
|
||||
|
||||
#[error("AI 提供者错误: {0}")]
|
||||
AiProvider(String),
|
||||
|
||||
#[error("配置错误: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("IO 错误: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("序列化错误: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// 统一 Result 别名
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
74
crates/df-core/src/events.rs
Normal file
74
crates/df-core/src/events.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! 工作流事件定义
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::NodeId;
|
||||
|
||||
/// 人工审批响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HumanApprovalResponse {
|
||||
pub execution_id: String,
|
||||
pub node_id: NodeId,
|
||||
pub decision: String,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
/// 工作流事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum WorkflowEvent {
|
||||
/// 节点开始执行
|
||||
NodeStarted {
|
||||
node_id: NodeId,
|
||||
},
|
||||
/// 节点执行进度更新
|
||||
NodeProgress {
|
||||
node_id: NodeId,
|
||||
progress: f32,
|
||||
message: String,
|
||||
},
|
||||
/// 节点产生输出
|
||||
NodeOutput {
|
||||
node_id: NodeId,
|
||||
output: String,
|
||||
},
|
||||
/// 节点执行完成
|
||||
NodeCompleted {
|
||||
node_id: NodeId,
|
||||
duration_ms: u64,
|
||||
},
|
||||
/// 节点执行失败
|
||||
NodeFailed {
|
||||
node_id: NodeId,
|
||||
error: String,
|
||||
},
|
||||
/// 工作流暂停(等待外部输入)
|
||||
WorkflowPaused {
|
||||
reason: String,
|
||||
waiting_node: NodeId,
|
||||
},
|
||||
/// 工作流执行完成
|
||||
WorkflowCompleted {
|
||||
total_duration_ms: u64,
|
||||
},
|
||||
/// 工作流执行失败
|
||||
WorkflowFailed {
|
||||
error: String,
|
||||
failed_node: NodeId,
|
||||
},
|
||||
/// 人工审批请求
|
||||
HumanApprovalRequest {
|
||||
execution_id: String,
|
||||
node_id: NodeId,
|
||||
title: String,
|
||||
description: String,
|
||||
options: Vec<String>,
|
||||
},
|
||||
/// 人工审批响应
|
||||
HumanApprovalResponse {
|
||||
execution_id: String,
|
||||
node_id: NodeId,
|
||||
decision: String,
|
||||
comment: Option<String>,
|
||||
},
|
||||
}
|
||||
5
crates/df-core/src/lib.rs
Normal file
5
crates/df-core/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! df-core: 核心类型定义,所有 crate 的基础依赖
|
||||
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod types;
|
||||
301
crates/df-core/src/types.rs
Normal file
301
crates/df-core/src/types.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
//! 核心类型定义:ID 别名、状态枚举、优先级等
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================
|
||||
// ID 类型别名
|
||||
// ============================================================
|
||||
|
||||
/// 想法 ID
|
||||
pub type IdeaId = String;
|
||||
/// 项目 ID
|
||||
pub type ProjectId = String;
|
||||
/// 任务 ID
|
||||
pub type TaskId = String;
|
||||
/// 工作流 ID
|
||||
pub type WorkflowId = String;
|
||||
/// 节点 ID
|
||||
pub type NodeId = String;
|
||||
/// 发布 ID
|
||||
pub type ReleaseId = String;
|
||||
/// 分支 ID
|
||||
pub type BranchId = String;
|
||||
/// 插件 ID
|
||||
pub type PluginId = String;
|
||||
/// 执行 ID
|
||||
pub type ExecutionId = String;
|
||||
/// 决策 ID
|
||||
pub type DecisionId = String;
|
||||
|
||||
// ============================================================
|
||||
// 状态枚举
|
||||
// ============================================================
|
||||
|
||||
/// 想法状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IdeaStatus {
|
||||
/// 草稿
|
||||
Draft,
|
||||
/// 待评估
|
||||
PendingReview,
|
||||
/// 已批准
|
||||
Approved,
|
||||
/// 已拒绝
|
||||
Rejected,
|
||||
/// 已转为项目
|
||||
Promoted,
|
||||
/// 已归档
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl IdeaStatus {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
IdeaStatus::Draft => "draft",
|
||||
IdeaStatus::PendingReview => "pending_review",
|
||||
IdeaStatus::Approved => "approved",
|
||||
IdeaStatus::Rejected => "rejected",
|
||||
IdeaStatus::Promoted => "promoted",
|
||||
IdeaStatus::Archived => "archived",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IdeaStatus {
|
||||
fn default() -> Self {
|
||||
IdeaStatus::Draft
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectStatus {
|
||||
/// 规划中
|
||||
Planning,
|
||||
/// 开发中
|
||||
InProgress,
|
||||
/// 测试中
|
||||
Testing,
|
||||
/// 发布中
|
||||
Releasing,
|
||||
/// 已完成
|
||||
Completed,
|
||||
/// 已暂停
|
||||
Paused,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl ProjectStatus {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ProjectStatus::Planning => "planning",
|
||||
ProjectStatus::InProgress => "in_progress",
|
||||
ProjectStatus::Testing => "testing",
|
||||
ProjectStatus::Releasing => "releasing",
|
||||
ProjectStatus::Completed => "completed",
|
||||
ProjectStatus::Paused => "paused",
|
||||
ProjectStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProjectStatus {
|
||||
fn default() -> Self {
|
||||
ProjectStatus::Planning
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskStatus {
|
||||
/// 待开始
|
||||
Todo,
|
||||
/// 进行中
|
||||
InProgress,
|
||||
/// 代码审查中
|
||||
InReview,
|
||||
/// 测试中
|
||||
Testing,
|
||||
/// 已完成
|
||||
Done,
|
||||
/// 已阻塞
|
||||
Blocked,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl TaskStatus {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TaskStatus::Todo => "todo",
|
||||
TaskStatus::InProgress => "in_progress",
|
||||
TaskStatus::InReview => "in_review",
|
||||
TaskStatus::Testing => "testing",
|
||||
TaskStatus::Done => "done",
|
||||
TaskStatus::Blocked => "blocked",
|
||||
TaskStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TaskStatus {
|
||||
fn default() -> Self {
|
||||
TaskStatus::Todo
|
||||
}
|
||||
}
|
||||
|
||||
/// 工作流状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkflowStatus {
|
||||
/// 待执行
|
||||
Pending,
|
||||
/// 运行中
|
||||
Running,
|
||||
/// 已暂停(等待人工输入等)
|
||||
Paused,
|
||||
/// 已完成
|
||||
Completed,
|
||||
/// 已失败
|
||||
Failed,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl WorkflowStatus {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
WorkflowStatus::Pending => "pending",
|
||||
WorkflowStatus::Running => "running",
|
||||
WorkflowStatus::Paused => "paused",
|
||||
WorkflowStatus::Completed => "completed",
|
||||
WorkflowStatus::Failed => "failed",
|
||||
WorkflowStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WorkflowStatus {
|
||||
fn default() -> Self {
|
||||
WorkflowStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// 节点状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NodeStatus {
|
||||
/// 待执行
|
||||
Pending,
|
||||
/// 运行中
|
||||
Running,
|
||||
/// 已完成
|
||||
Completed,
|
||||
/// 已失败
|
||||
Failed,
|
||||
/// 已跳过
|
||||
Skipped,
|
||||
/// 等待中(如等待人工操作)
|
||||
Waiting,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl NodeStatus {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
NodeStatus::Pending => "pending",
|
||||
NodeStatus::Running => "running",
|
||||
NodeStatus::Completed => "completed",
|
||||
NodeStatus::Failed => "failed",
|
||||
NodeStatus::Skipped => "skipped",
|
||||
NodeStatus::Waiting => "waiting",
|
||||
NodeStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NodeStatus {
|
||||
fn default() -> Self {
|
||||
NodeStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// 分支状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BranchStatus {
|
||||
/// 活跃中
|
||||
Active,
|
||||
/// 已合并
|
||||
Merged,
|
||||
/// 已废弃
|
||||
Abandoned,
|
||||
}
|
||||
|
||||
impl BranchStatus {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
BranchStatus::Active => "active",
|
||||
BranchStatus::Merged => "merged",
|
||||
BranchStatus::Abandoned => "abandoned",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BranchStatus {
|
||||
fn default() -> Self {
|
||||
BranchStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// 优先级
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Priority {
|
||||
/// 低
|
||||
Low = 0,
|
||||
/// 中
|
||||
Medium = 1,
|
||||
/// 高
|
||||
High = 2,
|
||||
/// 紧急
|
||||
Critical = 3,
|
||||
}
|
||||
|
||||
impl Priority {
|
||||
/// 返回数据库存储用的小写字符串
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Priority::Low => "low",
|
||||
Priority::Medium => "medium",
|
||||
Priority::High => "high",
|
||||
Priority::Critical => "critical",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Priority {
|
||||
fn default() -> Self {
|
||||
Priority::Medium
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
/// 生成新的 UUID v4 字符串
|
||||
pub fn new_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
12
crates/df-evolve/Cargo.toml
Normal file
12
crates/df-evolve/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "df-evolve"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
58
crates/df-evolve/src/evolve_engine.rs
Normal file
58
crates/df-evolve/src/evolve_engine.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! 进化引擎:知识沉淀的核心闭环
|
||||
//!
|
||||
//! 使用 → 沉淀 → 复用 → 改进 → 再沉淀
|
||||
|
||||
use crate::knowledge::{Knowledge, KnowledgeKind, KnowledgeStore};
|
||||
use crate::pattern::PatternExtractor;
|
||||
|
||||
/// 进化引擎
|
||||
pub struct EvolveEngine {
|
||||
extractor: PatternExtractor,
|
||||
}
|
||||
|
||||
impl EvolveEngine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
extractor: PatternExtractor,
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动扫描项目事件,提取可沉淀的知识
|
||||
///
|
||||
/// 触发时机:
|
||||
/// - 工作流节点完成后
|
||||
/// - 代码审查完成后
|
||||
/// - Bug 修复完成后
|
||||
/// - 发布完成后
|
||||
pub async fn evolve_from_events(&self, _events: &[serde_json::Value]) -> Vec<Knowledge> {
|
||||
let mut new_knowledge = Vec::new();
|
||||
|
||||
// TODO: 遍历事件,分类处理
|
||||
// 1. 审查事件 → 提取审查规则
|
||||
// 2. Bug 修复事件 → 提取诊断知识
|
||||
// 3. 发布事件 → 提取部署经验
|
||||
// 4. Prompt 事件 → 提取 Prompt 模板
|
||||
|
||||
new_knowledge
|
||||
}
|
||||
|
||||
/// 查询当前任务相关的知识(供 AI 节点使用)
|
||||
///
|
||||
/// AI 在执行任务前可以查询知识库,获取相关经验和规则
|
||||
pub fn query_relevant(
|
||||
&self,
|
||||
_context: &str,
|
||||
_kind: Option<&KnowledgeKind>,
|
||||
) -> Vec<Knowledge> {
|
||||
// TODO: 语义搜索知识库
|
||||
KnowledgeStore::search(_context, _kind, 5)
|
||||
}
|
||||
|
||||
/// 验证知识的有效性(定期执行)
|
||||
///
|
||||
/// 检查知识是否仍然适用(依赖版本是否过时、规则是否仍有意义等)
|
||||
pub async fn validate_knowledge(&self) -> Vec<String> {
|
||||
// TODO: 遍历知识库,标记过时的知识
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
98
crates/df-evolve/src/knowledge.rs
Normal file
98
crates/df-evolve/src/knowledge.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! 知识条目:经验沉淀的基本单元
|
||||
|
||||
use df_core::types::ProjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 知识类型
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum KnowledgeKind {
|
||||
/// 代码审查规则(如"禁止在循环中创建连接")
|
||||
ReviewRule,
|
||||
/// 有效的 Prompt 模板
|
||||
PromptTemplate,
|
||||
/// 踩坑经验
|
||||
Pitfall,
|
||||
/// 架构模式
|
||||
ArchitecturePattern,
|
||||
/// 诊断知识(Bug 根因分析)
|
||||
Diagnosis,
|
||||
/// 部署经验
|
||||
DeploymentNote,
|
||||
/// 工作流优化建议
|
||||
WorkflowOptimization,
|
||||
}
|
||||
|
||||
/// 知识条目
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Knowledge {
|
||||
pub id: String,
|
||||
pub kind: KnowledgeKind,
|
||||
/// 标题
|
||||
pub title: String,
|
||||
/// 内容
|
||||
pub content: String,
|
||||
/// 标签
|
||||
pub tags: Vec<String>,
|
||||
/// 来源项目
|
||||
pub source_project: Option<ProjectId>,
|
||||
/// 来源实体(如某次审查、某个 Bug 修复)
|
||||
pub source_ref: Option<String>,
|
||||
/// 被复用次数
|
||||
pub reuse_count: usize,
|
||||
/// 效果评分 (0-100,由用户或 AI 评估)
|
||||
pub effectiveness: Option<f32>,
|
||||
/// 是否已验证有效
|
||||
pub verified: bool,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
impl Knowledge {
|
||||
pub fn new(kind: KnowledgeKind, title: String, content: String) -> Self {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
Self {
|
||||
id: df_core::types::new_id(),
|
||||
kind,
|
||||
title,
|
||||
content,
|
||||
tags: vec![],
|
||||
source_project: None,
|
||||
source_ref: None,
|
||||
reuse_count: 0,
|
||||
effectiveness: None,
|
||||
verified: false,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录一次复用
|
||||
pub fn record_reuse(&mut self) {
|
||||
self.reuse_count += 1;
|
||||
self.updated_at = chrono::Utc::now().timestamp();
|
||||
}
|
||||
}
|
||||
|
||||
/// 知识库(内存索引,持久化到 SQLite)
|
||||
pub struct KnowledgeStore;
|
||||
|
||||
impl KnowledgeStore {
|
||||
/// 搜索相关知识
|
||||
pub fn search(_query: &str, _kind: Option<&KnowledgeKind>, _limit: usize) -> Vec<Knowledge> {
|
||||
// TODO: SQLite 全文搜索或向量搜索
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// 获取最常用的知识
|
||||
pub fn top_used(_limit: usize) -> Vec<Knowledge> {
|
||||
// TODO: 按 reuse_count 降序
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// 保存知识条目
|
||||
pub fn save(_knowledge: &Knowledge) -> anyhow::Result<()> {
|
||||
// TODO: SQLite INSERT/UPDATE
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
12
crates/df-evolve/src/lib.rs
Normal file
12
crates/df-evolve/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! 经验进化引擎:从开发过程中自动沉淀知识,持续进化复用
|
||||
//!
|
||||
//! 核心闭环:使用 → 沉淀 → 复用 → 改进 → 再沉淀
|
||||
|
||||
pub mod knowledge;
|
||||
pub mod pattern;
|
||||
pub mod prompt_template;
|
||||
pub mod review_rule;
|
||||
pub mod evolve_engine;
|
||||
|
||||
pub use evolve_engine::EvolveEngine;
|
||||
pub use knowledge::{Knowledge, KnowledgeKind, KnowledgeStore};
|
||||
47
crates/df-evolve/src/pattern.rs
Normal file
47
crates/df-evolve/src/pattern.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! 模式提取器:从开发过程中自动识别可沉淀的模式
|
||||
|
||||
use crate::knowledge::{Knowledge, KnowledgeKind};
|
||||
|
||||
/// 模式提取器
|
||||
///
|
||||
/// 自动从以下场景中识别可沉淀的模式:
|
||||
/// - 代码审查 → 审查规则
|
||||
/// - Bug 修复 → 诊断知识
|
||||
/// - 发布流程 → 部署经验
|
||||
/// - Prompt 调优 → Prompt 模板
|
||||
pub struct PatternExtractor;
|
||||
|
||||
impl PatternExtractor {
|
||||
/// 从代码审查结果中提取审查规则
|
||||
///
|
||||
/// 如果同一类问题在多次审查中重复出现,自动沉淀为规则
|
||||
pub fn extract_review_rule(
|
||||
_findings: &[serde_json::Value],
|
||||
_occurrence_threshold: usize,
|
||||
) -> Option<Knowledge> {
|
||||
// TODO:
|
||||
// 1. 分析 findings 的共性
|
||||
// 2. 如果出现次数 >= threshold,生成规则
|
||||
// 3. 去重(与已有规则比较)
|
||||
None
|
||||
}
|
||||
|
||||
/// 从 Bug 修复过程中提取诊断知识
|
||||
pub fn extract_diagnosis(
|
||||
_bug_description: &str,
|
||||
_root_cause: &str,
|
||||
_fix_description: &str,
|
||||
) -> Option<Knowledge> {
|
||||
// TODO: AI 总结为可复用的诊断知识
|
||||
None
|
||||
}
|
||||
|
||||
/// 从成功的 Prompt 中提取模板
|
||||
pub fn extract_prompt_template(
|
||||
_prompt: &str,
|
||||
_result_quality: f32,
|
||||
) -> Option<Knowledge> {
|
||||
// TODO: 如果 result_quality > 0.8,提取为模板
|
||||
None
|
||||
}
|
||||
}
|
||||
41
crates/df-evolve/src/prompt_template.rs
Normal file
41
crates/df-evolve/src/prompt_template.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Prompt 模板管理:AI 交互经验的沉淀与复用
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Prompt 模板
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptTemplate {
|
||||
pub id: String,
|
||||
/// 模板名称
|
||||
pub name: String,
|
||||
/// 模板内容(支持 {variable} 占位符)
|
||||
pub template: String,
|
||||
/// 变量说明
|
||||
pub variables: Vec<TemplateVariable>,
|
||||
/// 适用场景
|
||||
pub applicable_scenarios: Vec<String>,
|
||||
/// 效果评分
|
||||
pub avg_score: f32,
|
||||
/// 使用次数
|
||||
pub use_count: usize,
|
||||
}
|
||||
|
||||
/// 模板变量
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TemplateVariable {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub default_value: Option<String>,
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
impl PromptTemplate {
|
||||
/// 渲染模板(替换变量)
|
||||
pub fn render(&self, vars: &std::collections::HashMap<String, String>) -> String {
|
||||
let mut result = self.template.clone();
|
||||
for (key, value) in vars {
|
||||
result = result.replace(&format!("{{{}}}", key), value);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
45
crates/df-evolve/src/review_rule.rs
Normal file
45
crates/df-evolve/src/review_rule.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! 审查规则:从历史审查经验中沉淀的代码审查规则
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 审查规则
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReviewRule {
|
||||
pub id: String,
|
||||
/// 规则标题
|
||||
pub title: String,
|
||||
/// 规则描述
|
||||
pub description: String,
|
||||
/// 严重级别
|
||||
pub severity: RuleSeverity,
|
||||
/// 适用的语言/框架
|
||||
pub scope: Vec<String>,
|
||||
/// 检查方式(正则/AST/AI)
|
||||
pub check_method: CheckMethod,
|
||||
/// 发现次数(历史累计)
|
||||
pub found_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RuleSeverity {
|
||||
/// 必须修复
|
||||
MustFix,
|
||||
/// 建议改进
|
||||
ShouldFix,
|
||||
/// 可选优化
|
||||
NiceToHave,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CheckMethod {
|
||||
/// 正则匹配
|
||||
Regex,
|
||||
/// AST 分析
|
||||
Ast,
|
||||
/// AI 判断
|
||||
AiAnalysis,
|
||||
/// 人工判断
|
||||
Manual,
|
||||
}
|
||||
13
crates/df-execute/Cargo.toml
Normal file
13
crates/df-execute/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "df-execute"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
46
crates/df-execute/src/docker.rs
Normal file
46
crates/df-execute/src/docker.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Docker 执行器 — 在容器中运行任务
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Docker 容器执行请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerRequest {
|
||||
/// 镜像名称
|
||||
pub image: String,
|
||||
/// 容器内执行的命令
|
||||
pub command: Option<String>,
|
||||
/// 环境变量
|
||||
pub env: std::collections::HashMap<String, String>,
|
||||
/// 挂载卷
|
||||
pub volumes: Vec<VolumeMount>,
|
||||
/// 是否在执行后自动删除容器
|
||||
pub auto_remove: bool,
|
||||
}
|
||||
|
||||
/// 卷挂载
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VolumeMount {
|
||||
pub host_path: String,
|
||||
pub container_path: String,
|
||||
pub read_only: bool,
|
||||
}
|
||||
|
||||
/// Docker 执行结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerResult {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
/// 在 Docker 容器中执行命令
|
||||
///
|
||||
/// TODO: 实现 Docker API 调用或 CLI 包装
|
||||
pub async fn execute(_request: DockerRequest) -> anyhow::Result<DockerResult> {
|
||||
tracing::info!("Docker 执行: TODO");
|
||||
Ok(DockerResult {
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
exit_code: None,
|
||||
})
|
||||
}
|
||||
47
crates/df-execute/src/git_ops.rs
Normal file
47
crates/df-execute/src/git_ops.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Git 操作 — 克隆、提交、推送、合并等
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Git 操作类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitAction {
|
||||
Clone,
|
||||
Commit,
|
||||
Push,
|
||||
Pull,
|
||||
Merge,
|
||||
Checkout,
|
||||
CreateBranch,
|
||||
}
|
||||
|
||||
/// Git 操作请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitRequest {
|
||||
/// 操作类型
|
||||
pub action: GitAction,
|
||||
/// 仓库路径(本地路径或远程 URL)
|
||||
pub repo: String,
|
||||
/// 分支名
|
||||
pub branch: Option<String>,
|
||||
/// 提交消息
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Git 操作结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitResult {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// 执行 Git 操作
|
||||
///
|
||||
/// TODO: 实现完整的 Git 操作(可包装 git CLI 或使用 git2 crate)
|
||||
pub async fn execute(_request: GitRequest) -> anyhow::Result<GitResult> {
|
||||
tracing::info!("Git 操作: TODO");
|
||||
Ok(GitResult {
|
||||
success: true,
|
||||
message: "TODO: 未实现".to_string(),
|
||||
})
|
||||
}
|
||||
6
crates/df-execute/src/lib.rs
Normal file
6
crates/df-execute/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-execute: 执行运行时 — Shell、Docker、SSH、Git 操作
|
||||
|
||||
pub mod docker;
|
||||
pub mod git_ops;
|
||||
pub mod shell;
|
||||
pub mod ssh;
|
||||
73
crates/df-execute/src/shell.rs
Normal file
73
crates/df-execute/src/shell.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Shell 执行器 — 通过 tokio::process 执行 shell 命令
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Shell 命令执行结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShellResult {
|
||||
/// 标准输出
|
||||
pub stdout: String,
|
||||
/// 标准错误
|
||||
pub stderr: String,
|
||||
/// 退出码
|
||||
pub exit_code: Option<i32>,
|
||||
/// 执行耗时(毫秒)
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Shell 命令执行请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShellRequest {
|
||||
/// 要执行的命令
|
||||
pub command: String,
|
||||
/// 工作目录
|
||||
pub working_dir: Option<String>,
|
||||
/// 环境变量
|
||||
pub env: std::collections::HashMap<String, String>,
|
||||
/// 超时时间(秒),None 表示不超时
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// 执行 Shell 命令
|
||||
///
|
||||
/// TODO: 完整实现,支持超时、环境变量、工作目录等
|
||||
pub async fn execute(request: ShellRequest) -> anyhow::Result<ShellResult> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let mut cmd = if cfg!(windows) {
|
||||
let mut c = tokio::process::Command::new("cmd");
|
||||
c.arg("/C").arg(&request.command);
|
||||
c
|
||||
} else {
|
||||
let mut c = tokio::process::Command::new("sh");
|
||||
c.arg("-c").arg(&request.command);
|
||||
c
|
||||
};
|
||||
|
||||
if let Some(dir) = &request.working_dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
|
||||
for (key, value) in &request.env {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
let output = match request.timeout_secs {
|
||||
Some(secs) => tokio::time::timeout(
|
||||
std::time::Duration::from_secs(secs),
|
||||
cmd.output(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("命令执行超时: {}秒", secs))??,
|
||||
None => cmd.output().await?,
|
||||
};
|
||||
|
||||
let duration = start.elapsed().as_millis() as u64;
|
||||
|
||||
Ok(ShellResult {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
exit_code: output.status.code(),
|
||||
duration_ms: duration,
|
||||
})
|
||||
}
|
||||
38
crates/df-execute/src/ssh.rs
Normal file
38
crates/df-execute/src/ssh.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
//! SSH 执行器 — 远程命令执行
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// SSH 执行请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshRequest {
|
||||
/// 主机地址
|
||||
pub host: String,
|
||||
/// 端口
|
||||
pub port: u16,
|
||||
/// 用户名
|
||||
pub user: String,
|
||||
/// 要执行的命令
|
||||
pub command: String,
|
||||
/// 超时时间(秒)
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// SSH 执行结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshResult {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
/// 通过 SSH 执行远程命令
|
||||
///
|
||||
/// TODO: 实现SSH连接(可用 ssh2 crate 或包装 ssh 命令)
|
||||
pub async fn execute(_request: SshRequest) -> anyhow::Result<SshResult> {
|
||||
tracing::info!("SSH 执行: TODO");
|
||||
Ok(SshResult {
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
exit_code: None,
|
||||
})
|
||||
}
|
||||
13
crates/df-ideas/Cargo.toml
Normal file
13
crates/df-ideas/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "df-ideas"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
308
crates/df-ideas/src/adversarial.rs
Normal file
308
crates/df-ideas/src/adversarial.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
//! 对抗式评估系统 — 正反方辩论 + AI 分析师
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::IdeaId;
|
||||
use crate::capture::Idea;
|
||||
|
||||
/// 对抗评估结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AdversarialEval {
|
||||
pub idea_id: IdeaId,
|
||||
pub positive: Argument,
|
||||
pub negative: Argument,
|
||||
pub analyst: AnalystAnalysis,
|
||||
pub final_score: f64,
|
||||
pub recommendation: Recommendation,
|
||||
}
|
||||
|
||||
/// 正方论点
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Argument {
|
||||
pub thesis: String, // 核心观点
|
||||
pub evidence: Vec<String>, // 证据支持
|
||||
pub reasoning: Vec<String>, // 推理过程
|
||||
pub confidence: f64, // 置信度 0-1
|
||||
}
|
||||
|
||||
/// 反方论点
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CounterArgument {
|
||||
pub thesis: String, // 反对观点
|
||||
pub evidence: Vec<String>, // 反对证据
|
||||
pub reasoning: Vec<String>, // 反驳推理
|
||||
pub confidence: f64, // 置信度 0-1
|
||||
}
|
||||
|
||||
/// AI 分析师综合分析
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnalystAnalysis {
|
||||
pub summary: String, // 综合总结
|
||||
pub strengths: Vec<String>, // 主要优势
|
||||
pub weaknesses: Vec<String>, // 主要劣势
|
||||
pub risks: Vec<String>, // 潜在风险
|
||||
pub opportunities: Vec<String>, // 机会点
|
||||
pub final_assessment: AssessmentLevel, // 最终评估
|
||||
}
|
||||
|
||||
/// 评估等级
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AssessmentLevel {
|
||||
StrongGo, // 强烈推荐执行
|
||||
Recommended, // 推荐执行
|
||||
Conditional, // 有条件执行
|
||||
Revised, // 需要修改后执行
|
||||
Defer, // 推迟执行
|
||||
Reject, // 不推荐执行
|
||||
}
|
||||
|
||||
/// 最终建议
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Recommendation {
|
||||
ImmediateAction, // 立即行动
|
||||
Soon, // 尽快行动
|
||||
WithResources, // 配置资源后行动
|
||||
ResearchMore, // 需要更多研究
|
||||
Monitor, // 持续监控
|
||||
Cancel, // 取消想法
|
||||
}
|
||||
|
||||
/// 对抗评估引擎
|
||||
pub struct AdversarialEngine;
|
||||
|
||||
impl AdversarialEngine {
|
||||
/// 执行完整的对抗评估
|
||||
pub async fn evaluate(idea: &Idea) -> Result<AdversarialEval> {
|
||||
// 1. 生成正方观点
|
||||
let positive = Self::generate_positive_argument(idea).await?;
|
||||
|
||||
// 2. 生成反方观点
|
||||
let negative = Self::generate_negative_argument(idea, &positive).await?;
|
||||
|
||||
// 3. AI 分析师综合分析
|
||||
let analyst = Self::analyst_analysis(idea, &positive, &negative).await?;
|
||||
|
||||
// 4. 计算最终分数和建议
|
||||
let (final_score, recommendation) = Self::compute_final_assessment(&analyst);
|
||||
|
||||
Ok(AdversarialEval {
|
||||
idea_id: idea.id.clone(),
|
||||
positive,
|
||||
negative,
|
||||
analyst,
|
||||
final_score,
|
||||
recommendation,
|
||||
})
|
||||
}
|
||||
|
||||
/// 生成正方观点(支持执行)
|
||||
async fn generate_positive_argument(idea: &Idea) -> Result<Argument> {
|
||||
// TODO: 接入 AI 生成正方观点
|
||||
// 当前使用启发式模板
|
||||
|
||||
let title = &idea.title;
|
||||
let desc = &idea.description;
|
||||
|
||||
Ok(Argument {
|
||||
thesis: format!("{} 具有很高的价值和可行性,应该优先执行", title),
|
||||
evidence: vec![
|
||||
format!("满足业务需求:{}", desc),
|
||||
"投入产出比高".to_string(),
|
||||
"技术实现可行".to_string(),
|
||||
"时间窗口合适".to_string(),
|
||||
],
|
||||
reasoning: vec![
|
||||
"能够解决现有痛点".to_string(),
|
||||
"竞争优势明显".to_string(),
|
||||
"风险可控".to_string(),
|
||||
],
|
||||
confidence: 0.75,
|
||||
})
|
||||
}
|
||||
|
||||
/// 生成反方观点(反对或谨慎)
|
||||
async fn generate_negative_argument(idea: &Idea, positive: &Argument) -> Result<CounterArgument> {
|
||||
// TODO: 接入 AI 生成反方观点,考虑正方观点
|
||||
|
||||
let title = &idea.title;
|
||||
|
||||
Ok(CounterArgument {
|
||||
thesis: format!("{} 需要谨慎评估,存在一定风险", title),
|
||||
evidence: vec![
|
||||
"资源投入较大".to_string(),
|
||||
"市场不确定性高".to_string(),
|
||||
"技术挑战存在".to_string(),
|
||||
"机会成本高".to_string(),
|
||||
],
|
||||
reasoning: vec![
|
||||
"ROI 需要进一步验证".to_string(),
|
||||
"优先级可能过高".to_string(),
|
||||
"存在更优替代方案".to_string(),
|
||||
],
|
||||
confidence: 0.65,
|
||||
})
|
||||
}
|
||||
|
||||
/// AI 分析师综合分析
|
||||
async fn analyst_analysis(
|
||||
idea: &Idea,
|
||||
positive: &Argument,
|
||||
negative: &CounterArgument,
|
||||
) -> Result<AnalystAnalysis> {
|
||||
// TODO: 接入 AI 进行深度分析
|
||||
|
||||
let positive_strengths = vec![
|
||||
"方向正确,符合业务战略".to_string(),
|
||||
"技术创新性较强".to_string(),
|
||||
"用户价值明确".to_string(),
|
||||
];
|
||||
|
||||
let weaknesses = vec![
|
||||
"资源需求评估不足".to_string(),
|
||||
"风险控制需要加强".to_string(),
|
||||
"时间规划可能过于乐观".to_string(),
|
||||
];
|
||||
|
||||
let risks = vec![
|
||||
"技术实现难度超出预期".to_string(),
|
||||
"市场竞争加剧".to_string(),
|
||||
"用户接受度不确定".to_string(),
|
||||
];
|
||||
|
||||
let opportunities = vec![
|
||||
"可能形成新的竞争优势".to_string(),
|
||||
"技术积累价值显著".to_string(),
|
||||
"市场机会窗口良好".to_string(),
|
||||
];
|
||||
|
||||
// 基于正反方观点的强度计算
|
||||
let positive_strength = positive.confidence;
|
||||
let negative_strength = negative.confidence;
|
||||
let net_positive = (positive_strength - negative_strength + 1.0) / 2.0;
|
||||
|
||||
let final_assessment = if net_positive > 0.7 {
|
||||
AssessmentLevel::StrongGo
|
||||
} else if net_positive > 0.5 {
|
||||
AssessmentLevel::Recommended
|
||||
} else if net_positive > 0.3 {
|
||||
AssessmentLevel::Conditional
|
||||
} else if net_positive > 0.1 {
|
||||
AssessmentLevel::Revised
|
||||
} else {
|
||||
AssessmentLevel::Defer
|
||||
};
|
||||
|
||||
Ok(AnalystAnalysis {
|
||||
summary: format!(
|
||||
"该想法整体价值评估中等偏上,建议在有条件的情况下执行。主要价值在于{},需要关注{}。",
|
||||
idea.title,
|
||||
if net_positive > 0.5 { "风险控制" } else { "价值验证" }
|
||||
),
|
||||
strengths: positive_strengths,
|
||||
weaknesses,
|
||||
risks,
|
||||
opportunities,
|
||||
final_assessment,
|
||||
})
|
||||
}
|
||||
|
||||
/// 计算最终评估分数和建议
|
||||
fn compute_final_assessment(analyst: &AnalystAnalysis) -> (f64, Recommendation) {
|
||||
// 基于评估等级映射分数
|
||||
let base_score = match analyst.final_assessment {
|
||||
AssessmentLevel::StrongGo => 8.5,
|
||||
AssessmentLevel::Recommended => 7.0,
|
||||
AssessmentLevel::Conditional => 5.5,
|
||||
AssessmentLevel::Revised => 4.0,
|
||||
AssessmentLevel::Defer => 2.5,
|
||||
AssessmentLevel::Reject => 1.0,
|
||||
};
|
||||
|
||||
// 根据优劣势微调分数
|
||||
let strength_count = analyst.strengths.len() as f64;
|
||||
let weakness_count = analyst.weaknesses.len() as f64;
|
||||
let score_adjustment = (strength_count - weakness_count) * 0.3;
|
||||
|
||||
let final_score = (base_score + score_adjustment).clamp(0.0, 10.0);
|
||||
|
||||
let recommendation = match analyst.final_assessment {
|
||||
AssessmentLevel::StrongGo => Recommendation::ImmediateAction,
|
||||
AssessmentLevel::Recommended => Recommendation::Soon,
|
||||
AssessmentLevel::Conditional => Recommendation::WithResources,
|
||||
AssessmentLevel::Revised => Recommendation::ResearchMore,
|
||||
AssessmentLevel::Defer => Recommendation::Monitor,
|
||||
AssessmentLevel::Reject => Recommendation::Cancel,
|
||||
};
|
||||
|
||||
(final_score, recommendation)
|
||||
}
|
||||
}
|
||||
|
||||
/// 评估结果展示格式
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EvalDisplay {
|
||||
pub idea_title: String,
|
||||
pub positive_strength: f64,
|
||||
pub negative_strength: f64,
|
||||
pub net_sentiment: f64, // -1 到 1,正为正面
|
||||
pub assessment_level: String,
|
||||
pub key_takeaways: Vec<String>,
|
||||
pub action_items: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<AdversarialEval> for EvalDisplay {
|
||||
fn from(eval: AdversarialEval) -> Self {
|
||||
let net_sentiment = (eval.positive.confidence - eval.negative.confidence) as f64;
|
||||
|
||||
let key_takeaways = vec![
|
||||
format!("优势:{}", eval.analyst.strengths.join("、")),
|
||||
format!("风险:{}", eval.analyst.risks.join("、")),
|
||||
format!("建议:{:?}", eval.recommendation),
|
||||
];
|
||||
|
||||
let action_items = match eval.recommendation {
|
||||
Recommendation::ImmediateAction => vec![
|
||||
"立即组建项目团队".to_string(),
|
||||
"制定详细执行计划".to_string(),
|
||||
"分配必要资源".to_string(),
|
||||
],
|
||||
Recommendation::Soon => vec![
|
||||
"下周启动项目".to_string(),
|
||||
"准备资源需求".to_string(),
|
||||
"制定时间表".to_string(),
|
||||
],
|
||||
Recommendation::WithResources => vec![
|
||||
"确认资源预算".to_string(),
|
||||
"评估ROI".to_string(),
|
||||
"制定风险预案".to_string(),
|
||||
],
|
||||
Recommendation::ResearchMore => vec![
|
||||
"进行市场调研".to_string(),
|
||||
"收集用户反馈".to_string(),
|
||||
"验证技术可行性".to_string(),
|
||||
],
|
||||
Recommendation::Monitor => vec![
|
||||
"持续跟踪相关指标".to_string(),
|
||||
"定期评估进展".to_string(),
|
||||
"等待更好的时机".to_string(),
|
||||
],
|
||||
Recommendation::Cancel => vec![
|
||||
"记录归档原因".to_string(),
|
||||
"释放相关资源".to_string(),
|
||||
"提取经验教训".to_string(),
|
||||
],
|
||||
};
|
||||
|
||||
EvalDisplay {
|
||||
idea_title: eval.positive.thesis.split(' ').take(3).collect::<Vec<_>>().join(" "),
|
||||
positive_strength: eval.positive.confidence,
|
||||
negative_strength: eval.negative.confidence,
|
||||
net_sentiment,
|
||||
assessment_level: format!("{:?}", eval.analyst.final_assessment),
|
||||
key_takeaways,
|
||||
action_items,
|
||||
}
|
||||
}
|
||||
}
|
||||
98
crates/df-ideas/src/capture.rs
Normal file
98
crates/df-ideas/src/capture.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! 想法捕获 — 快速记录与管理
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::{IdeaId, Priority};
|
||||
|
||||
/// 捕获一个新想法的输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CaptureInput {
|
||||
/// 标题
|
||||
pub title: String,
|
||||
/// 详细描述
|
||||
pub description: String,
|
||||
/// 优先级
|
||||
#[serde(default)]
|
||||
pub priority: Priority,
|
||||
/// 标签
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
/// 来源(如 "用户输入"、"AI 生成"、"会议记录")
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
/// 想法实体
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Idea {
|
||||
/// 唯一 ID
|
||||
pub id: IdeaId,
|
||||
/// 标题
|
||||
pub title: String,
|
||||
/// 详细描述
|
||||
pub description: String,
|
||||
/// 当前状态
|
||||
pub status: df_core::types::IdeaStatus,
|
||||
/// 优先级
|
||||
pub priority: Priority,
|
||||
/// 评分
|
||||
pub scores: Option<IdeaScores>,
|
||||
/// 标签
|
||||
pub tags: Vec<String>,
|
||||
/// 来源
|
||||
pub source: Option<String>,
|
||||
/// 关联的想法 ID
|
||||
pub related_ids: Vec<IdeaId>,
|
||||
/// 创建时间
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
/// 更新时间
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 想法评分详情
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IdeaScores {
|
||||
/// 可行性评分 (0-10)
|
||||
pub feasibility: f64,
|
||||
/// 影响力评分 (0-10)
|
||||
pub impact: f64,
|
||||
/// 紧急度评分 (0-10)
|
||||
pub urgency: f64,
|
||||
/// 综合评分 (加权平均)
|
||||
pub overall: f64,
|
||||
}
|
||||
|
||||
/// 想法捕获器
|
||||
pub struct IdeaCapture;
|
||||
|
||||
impl IdeaCapture {
|
||||
/// 捕获一个新想法
|
||||
///
|
||||
/// TODO: 接入存储层持久化
|
||||
pub fn capture(input: CaptureInput) -> Idea {
|
||||
let now = chrono::Utc::now();
|
||||
Idea {
|
||||
id: df_core::types::new_id(),
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
status: df_core::types::IdeaStatus::Draft,
|
||||
priority: input.priority,
|
||||
scores: None,
|
||||
tags: input.tags,
|
||||
source: input.source,
|
||||
related_ids: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// 快速捕获(仅标题)
|
||||
pub fn quick_capture(title: String) -> Idea {
|
||||
Self::capture(CaptureInput {
|
||||
title,
|
||||
description: String::new(),
|
||||
priority: Priority::default(),
|
||||
tags: Vec::new(),
|
||||
source: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
79
crates/df-ideas/src/evaluator.rs
Normal file
79
crates/df-ideas/src/evaluator.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! 想法评估器 — 对想法进行多维度评估
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use df_core::types::IdeaId;
|
||||
|
||||
use crate::adversarial::{AdversarialEngine, AdversarialEval};
|
||||
use crate::capture::Idea;
|
||||
use crate::scoring::IdeaScores;
|
||||
|
||||
/// 评估维度
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum EvalDimension {
|
||||
/// 可行性
|
||||
Feasibility,
|
||||
/// 影响力
|
||||
Impact,
|
||||
/// 紧急度
|
||||
Urgency,
|
||||
}
|
||||
|
||||
/// 评估结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EvalResult {
|
||||
pub idea_id: IdeaId,
|
||||
pub scores: IdeaScores,
|
||||
pub recommendation: Recommendation,
|
||||
pub comments: Vec<String>,
|
||||
}
|
||||
|
||||
/// 评估建议
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Recommendation {
|
||||
/// 强烈推荐立即执行
|
||||
StrongApprove,
|
||||
/// 推荐执行
|
||||
Approve,
|
||||
/// 需要更多信息
|
||||
NeedsInfo,
|
||||
/// 建议推迟
|
||||
Defer,
|
||||
/// 不推荐
|
||||
Reject,
|
||||
}
|
||||
|
||||
/// 想法评估器
|
||||
pub struct IdeaEvaluator;
|
||||
|
||||
impl IdeaEvaluator {
|
||||
/// 评估一个想法 - 使用对抗式评估
|
||||
pub async fn evaluate_adversarial(idea: &Idea) -> Result<AdversarialEval> {
|
||||
AdversarialEngine::evaluate(idea).await
|
||||
}
|
||||
|
||||
/// 评估一个想法 - 保持向后兼容
|
||||
pub fn evaluate(idea: &Idea) -> Result<EvalResult> {
|
||||
// 使用简单评分作为后备
|
||||
let scores = crate::scoring::ScoringEngine::compute_default(idea);
|
||||
|
||||
let recommendation = if scores.overall >= 8.0 {
|
||||
Recommendation::StrongApprove
|
||||
} else if scores.overall >= 6.0 {
|
||||
Recommendation::Approve
|
||||
} else if scores.overall >= 4.0 {
|
||||
Recommendation::NeedsInfo
|
||||
} else if scores.overall >= 2.0 {
|
||||
Recommendation::Defer
|
||||
} else {
|
||||
Recommendation::Reject
|
||||
};
|
||||
|
||||
Ok(EvalResult {
|
||||
idea_id: idea.id.clone(),
|
||||
scores,
|
||||
recommendation,
|
||||
comments: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
76
crates/df-ideas/src/graph.rs
Normal file
76
crates/df-ideas/src/graph.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! 想法关联图 — 管理想法之间的关系
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::IdeaId;
|
||||
|
||||
/// 想法之间的关系类型
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RelationKind {
|
||||
/// 相似(语义相近)
|
||||
Similar,
|
||||
/// 依赖(A 依赖 B)
|
||||
DependsOn,
|
||||
/// 衍生(A 衍生自 B)
|
||||
DerivedFrom,
|
||||
/// 互补(A 和 B 可以互补)
|
||||
Complementary,
|
||||
}
|
||||
|
||||
/// 想法关系边
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Relation {
|
||||
pub source_id: IdeaId,
|
||||
pub target_id: IdeaId,
|
||||
pub kind: RelationKind,
|
||||
pub strength: f64, // 0.0 ~ 1.0
|
||||
}
|
||||
|
||||
/// 想法关联图
|
||||
pub struct IdeaGraph {
|
||||
/// 邻接表(idea_id -> 相关关系列表)
|
||||
edges: HashMap<IdeaId, Vec<Relation>>,
|
||||
}
|
||||
|
||||
impl IdeaGraph {
|
||||
/// 创建空图
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
edges: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加关系
|
||||
pub fn add_relation(&mut self, source_id: IdeaId, target_id: IdeaId, kind: RelationKind, strength: f64) {
|
||||
let relation = Relation {
|
||||
source_id: source_id.clone(),
|
||||
target_id: target_id.clone(),
|
||||
kind,
|
||||
strength,
|
||||
};
|
||||
self.edges.entry(source_id).or_default().push(relation.clone());
|
||||
self.edges.entry(target_id).or_default().push(relation);
|
||||
}
|
||||
|
||||
/// 获取与指定想法相关的所有关系
|
||||
pub fn get_relations(&self, idea_id: &IdeaId) -> Vec<&Relation> {
|
||||
self.edges.get(idea_id).map(|r| r.iter().collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 查找相似想法
|
||||
pub fn find_similar(&self, idea_id: &IdeaId) -> Vec<&Relation> {
|
||||
self.get_relations(idea_id)
|
||||
.into_iter()
|
||||
.filter(|r| r.kind == RelationKind::Similar)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// TODO: 基于向量相似度的自动关联发现
|
||||
// TODO: 图遍历、聚类算法
|
||||
}
|
||||
|
||||
impl Default for IdeaGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
8
crates/df-ideas/src/lib.rs
Normal file
8
crates/df-ideas/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! df-ideas: 想法池 — 捕获、评估、评分、关联图、晋升
|
||||
|
||||
pub mod adversarial;
|
||||
pub mod capture;
|
||||
pub mod evaluator;
|
||||
pub mod graph;
|
||||
pub mod promotion;
|
||||
pub mod scoring;
|
||||
91
crates/df-ideas/src/promotion.rs
Normal file
91
crates/df-ideas/src/promotion.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! 想法晋升 — 将想法转为项目
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use df_core::types::{IdeaId, ProjectId};
|
||||
|
||||
use crate::capture::Idea;
|
||||
use crate::evaluator::Recommendation;
|
||||
|
||||
/// 晋升结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromotionResult {
|
||||
pub idea_id: IdeaId,
|
||||
pub project_id: ProjectId,
|
||||
pub promoted: bool,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// 晋升策略
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PromotionPolicy {
|
||||
/// 自动晋升(评分达标时自动转为项目)
|
||||
Auto,
|
||||
/// 手动确认(需要人工审批)
|
||||
Manual,
|
||||
/// 半自动(AI 评估 + 人工确认)
|
||||
SemiAuto,
|
||||
}
|
||||
|
||||
/// 想法晋升器
|
||||
pub struct IdeaPromoter {
|
||||
policy: PromotionPolicy,
|
||||
}
|
||||
|
||||
impl IdeaPromoter {
|
||||
/// 创建晋升器
|
||||
pub fn new(policy: PromotionPolicy) -> Self {
|
||||
Self { policy }
|
||||
}
|
||||
|
||||
/// 评估并尝试晋升想法
|
||||
///
|
||||
/// TODO: 接入 df-project 创建项目
|
||||
pub fn try_promote(&self, idea: &Idea, recommendation: &Recommendation) -> Result<PromotionResult> {
|
||||
match self.policy {
|
||||
PromotionPolicy::Auto => {
|
||||
if matches!(recommendation, Recommendation::StrongApprove | Recommendation::Approve) {
|
||||
self.do_promote(idea)
|
||||
} else {
|
||||
Ok(PromotionResult {
|
||||
idea_id: idea.id.clone(),
|
||||
project_id: String::new(),
|
||||
promoted: false,
|
||||
reason: format!("评估建议 {:?},未达到自动晋升标准", recommendation),
|
||||
})
|
||||
}
|
||||
}
|
||||
PromotionPolicy::Manual => {
|
||||
Ok(PromotionResult {
|
||||
idea_id: idea.id.clone(),
|
||||
project_id: String::new(),
|
||||
promoted: false,
|
||||
reason: "手动模式,等待人工审批".to_string(),
|
||||
})
|
||||
}
|
||||
PromotionPolicy::SemiAuto => {
|
||||
// TODO: AI 评估 + 人工确认流程
|
||||
Ok(PromotionResult {
|
||||
idea_id: idea.id.clone(),
|
||||
project_id: String::new(),
|
||||
promoted: false,
|
||||
reason: "半自动模式,等待确认".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行晋升操作
|
||||
fn do_promote(&self, idea: &Idea) -> Result<PromotionResult> {
|
||||
let project_id = df_core::types::new_id();
|
||||
// TODO: 调用 df-project 创建项目
|
||||
tracing::info!("想法 {} 晋升为项目 {}", idea.id, project_id);
|
||||
|
||||
Ok(PromotionResult {
|
||||
idea_id: idea.id.clone(),
|
||||
project_id,
|
||||
promoted: true,
|
||||
reason: "评估达标,自动晋升".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
75
crates/df-ideas/src/scoring.rs
Normal file
75
crates/df-ideas/src/scoring.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! 评分引擎 — 多维度加权评分
|
||||
|
||||
use crate::capture::Idea;
|
||||
|
||||
/// 想法评分详情(重新导出 capture 模块中的定义)
|
||||
pub use crate::capture::IdeaScores;
|
||||
|
||||
/// 评分权重配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScoringWeights {
|
||||
pub feasibility: f64,
|
||||
pub impact: f64,
|
||||
pub urgency: f64,
|
||||
}
|
||||
|
||||
impl Default for ScoringWeights {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
feasibility: 0.4,
|
||||
impact: 0.4,
|
||||
urgency: 0.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 评分引擎
|
||||
pub struct ScoringEngine;
|
||||
|
||||
impl ScoringEngine {
|
||||
/// 使用默认权重计算评分
|
||||
///
|
||||
/// TODO: 接入 AI 进行深度评分,当前返回基于启发式的分数
|
||||
pub fn compute_default(idea: &Idea) -> IdeaScores {
|
||||
let weights = ScoringWeights::default();
|
||||
Self::compute(idea, &weights)
|
||||
}
|
||||
|
||||
/// 使用指定权重计算评分
|
||||
pub fn compute(idea: &Idea, weights: &ScoringWeights) -> IdeaScores {
|
||||
// TODO: 基于想法内容、历史数据、AI 分析等多维度评分
|
||||
// 当前使用基于启发式的占位评分
|
||||
let feasibility = Self::heuristic_feasibility(idea);
|
||||
let impact = Self::heuristic_impact(idea);
|
||||
let urgency = Self::heuristic_urgency(idea);
|
||||
|
||||
let overall = feasibility * weights.feasibility
|
||||
+ impact * weights.impact
|
||||
+ urgency * weights.urgency;
|
||||
|
||||
IdeaScores {
|
||||
feasibility,
|
||||
impact,
|
||||
urgency,
|
||||
overall,
|
||||
}
|
||||
}
|
||||
|
||||
/// 启发式可行性评分
|
||||
fn heuristic_feasibility(_idea: &Idea) -> f64 {
|
||||
// TODO: 基于描述复杂度、资源需求等评估
|
||||
5.0
|
||||
}
|
||||
|
||||
/// 启发式影响力评分
|
||||
fn heuristic_impact(_idea: &Idea) -> f64 {
|
||||
// TODO: 基于业务价值、用户影响等评估
|
||||
5.0
|
||||
}
|
||||
|
||||
/// 启发式紧急度评分
|
||||
fn heuristic_urgency(_idea: &Idea) -> f64 {
|
||||
// TODO: 基于优先级、时间窗口等评估
|
||||
5.0
|
||||
}
|
||||
}
|
||||
16
crates/df-nodes/Cargo.toml
Normal file
16
crates/df-nodes/Cargo.toml
Normal 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 }
|
||||
167
crates/df-nodes/src/ai_node.rs
Normal file
167
crates/df-nodes/src/ai_node.rs
Normal 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 缺少必填参数: 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 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(默认)或 anthropic(GLM订阅/Claude官方)" },
|
||||
"base_url": { "type": "string", "description": "API 地址,OpenAI 兼容如 https://api.deepseek.com;Anthropic 如 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"
|
||||
}
|
||||
}
|
||||
41
crates/df-nodes/src/docker_node.rs
Normal file
41
crates/df-nodes/src/docker_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
41
crates/df-nodes/src/git_node.rs
Normal file
41
crates/df-nodes/src/git_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
43
crates/df-nodes/src/http_node.rs
Normal file
43
crates/df-nodes/src/http_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
96
crates/df-nodes/src/human_node.rs
Normal file
96
crates/df-nodes/src/human_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
10
crates/df-nodes/src/lib.rs
Normal file
10
crates/df-nodes/src/lib.rs
Normal 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;
|
||||
41
crates/df-nodes/src/notify_node.rs
Normal file
41
crates/df-nodes/src/notify_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
94
crates/df-nodes/src/script_node.rs
Normal file
94
crates/df-nodes/src/script_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
39
crates/df-nodes/src/subflow_node.rs
Normal file
39
crates/df-nodes/src/subflow_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
14
crates/df-plugin/Cargo.toml
Normal file
14
crates/df-plugin/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "df-plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
df-workflow = { path = "../df-workflow" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
66
crates/df-plugin/src/host.rs
Normal file
66
crates/df-plugin/src/host.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! 插件宿主环境 — 管理插件生命周期与沙箱
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::PluginId;
|
||||
use df_workflow::node::Node;
|
||||
|
||||
use crate::loader::{LoadedPlugin, PluginLoader, PluginMetadata};
|
||||
|
||||
/// 插件宿主环境
|
||||
pub struct PluginHost {
|
||||
/// 插件加载器
|
||||
loader: PluginLoader,
|
||||
/// 已加载的插件
|
||||
plugins: HashMap<PluginId, LoadedPlugin>,
|
||||
}
|
||||
|
||||
impl PluginHost {
|
||||
/// 创建宿主环境
|
||||
pub fn new(loader: PluginLoader) -> Self {
|
||||
Self {
|
||||
loader,
|
||||
plugins: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化:加载所有插件
|
||||
pub async fn initialize(&mut self) -> anyhow::Result<()> {
|
||||
self.plugins = self.loader.load_all()?;
|
||||
tracing::info!("插件宿主环境初始化完成,加载了 {} 个插件", self.plugins.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有已注册的节点
|
||||
pub fn all_nodes(&self) -> Vec<(&PluginId, &Box<dyn Node>)> {
|
||||
let mut nodes = Vec::new();
|
||||
for (plugin_id, plugin) in &self.plugins {
|
||||
for node in &plugin.nodes {
|
||||
nodes.push((plugin_id, node));
|
||||
}
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
/// 获取指定插件的元数据
|
||||
pub fn get_metadata(&self, plugin_id: &PluginId) -> Option<&PluginMetadata> {
|
||||
self.plugins.get(plugin_id).map(|p| &p.metadata)
|
||||
}
|
||||
|
||||
/// 卸载指定插件
|
||||
///
|
||||
/// TODO: 实现插件卸载(清理资源、取消注册节点等)
|
||||
pub fn unload(&mut self, plugin_id: &PluginId) -> anyhow::Result<()> {
|
||||
if let Some(plugin) = self.plugins.remove(plugin_id) {
|
||||
tracing::info!("插件 {} 已卸载", plugin.metadata.name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 关闭宿主环境,卸载所有插件
|
||||
pub fn shutdown(&mut self) {
|
||||
let count = self.plugins.len();
|
||||
self.plugins.clear();
|
||||
tracing::info!("插件宿主环境关闭,卸载了 {} 个插件", count);
|
||||
}
|
||||
}
|
||||
5
crates/df-plugin/src/lib.rs
Normal file
5
crates/df-plugin/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! df-plugin: 插件系统 — 插件加载、宿主环境、SDK
|
||||
|
||||
pub mod host;
|
||||
pub mod loader;
|
||||
pub mod sdk;
|
||||
94
crates/df-plugin/src/loader.rs
Normal file
94
crates/df-plugin/src/loader.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! 插件加载器 — 发现、加载、注册插件
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use df_core::types::PluginId;
|
||||
use df_workflow::node::Node;
|
||||
|
||||
/// 插件元数据
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginMetadata {
|
||||
/// 插件 ID
|
||||
pub id: PluginId,
|
||||
/// 插件名称
|
||||
pub name: String,
|
||||
/// 版本
|
||||
pub version: String,
|
||||
/// 描述
|
||||
pub description: String,
|
||||
/// 作者
|
||||
pub author: Option<String>,
|
||||
/// 入口文件路径
|
||||
pub entry_path: PathBuf,
|
||||
}
|
||||
|
||||
/// 已加载的插件
|
||||
pub struct LoadedPlugin {
|
||||
pub metadata: PluginMetadata,
|
||||
pub nodes: Vec<Box<dyn Node>>,
|
||||
}
|
||||
|
||||
/// 插件加载器
|
||||
pub struct PluginLoader {
|
||||
/// 插件搜索目录
|
||||
search_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl PluginLoader {
|
||||
/// 创建插件加载器
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
search_dirs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加插件搜索目录
|
||||
pub fn add_search_dir(&mut self, dir: PathBuf) {
|
||||
self.search_dirs.push(dir);
|
||||
}
|
||||
|
||||
/// 扫描并发现所有可用插件
|
||||
///
|
||||
/// TODO: 实现文件系统扫描、manifest 解析
|
||||
pub fn discover(&self) -> anyhow::Result<Vec<PluginMetadata>> {
|
||||
tracing::info!("扫描插件目录: {:?}", self.search_dirs);
|
||||
// TODO: 实现插件发现逻辑
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// 加载指定插件
|
||||
///
|
||||
/// TODO: 实现动态库加载(dlopen/LoadLibrary)或 WASM 加载
|
||||
pub fn load(&self, _metadata: &PluginMetadata) -> anyhow::Result<LoadedPlugin> {
|
||||
// TODO: 实现插件加载
|
||||
tracing::warn!("插件加载尚未实现");
|
||||
Err(anyhow::anyhow!("插件加载尚未实现"))
|
||||
}
|
||||
|
||||
/// 批量加载所有发现的插件
|
||||
pub fn load_all(&self) -> anyhow::Result<HashMap<PluginId, LoadedPlugin>> {
|
||||
let metadata_list = self.discover()?;
|
||||
let mut loaded = HashMap::new();
|
||||
|
||||
for meta in metadata_list {
|
||||
match self.load(&meta) {
|
||||
Ok(plugin) => {
|
||||
tracing::info!("插件 {} v{} 加载成功", meta.name, meta.version);
|
||||
loaded.insert(meta.id.clone(), plugin);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("插件 {} 加载失败: {}", meta.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(loaded)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginLoader {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
90
crates/df-plugin/src/sdk.rs
Normal file
90
crates/df-plugin/src/sdk.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! 插件 SDK — 插件开发者使用的工具与trait
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::Node;
|
||||
|
||||
/// 插件入口 trait — 所有插件必须实现
|
||||
#[async_trait]
|
||||
pub trait Plugin: Send + Sync {
|
||||
/// 插件 ID
|
||||
fn id(&self) -> &str;
|
||||
|
||||
/// 插件名称
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// 插件版本
|
||||
fn version(&self) -> &str;
|
||||
|
||||
/// 注册的所有节点
|
||||
fn nodes(&self) -> Vec<Box<dyn Node>>;
|
||||
|
||||
/// 插件初始化(可选)
|
||||
async fn initialize(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 插件销毁(可选)
|
||||
async fn shutdown(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件构建器 — 简化插件定义
|
||||
pub struct PluginBuilder {
|
||||
id: String,
|
||||
name: String,
|
||||
version: String,
|
||||
nodes: Vec<Box<dyn Node>>,
|
||||
}
|
||||
|
||||
impl PluginBuilder {
|
||||
/// 创建插件构建器
|
||||
pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
version: version.into(),
|
||||
nodes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册节点
|
||||
pub fn node(mut self, node: Box<dyn Node>) -> Self {
|
||||
self.nodes.push(node);
|
||||
self
|
||||
}
|
||||
|
||||
/// 获取插件 ID
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// 获取插件名称
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// 获取插件版本
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
/// 构建并获取所有节点
|
||||
pub fn build(self) -> Vec<Box<dyn Node>> {
|
||||
self.nodes
|
||||
}
|
||||
}
|
||||
|
||||
/// 声明插件的宏(简化版)
|
||||
///
|
||||
/// TODO: 实现完整的声明宏
|
||||
/// 使用示例:
|
||||
/// ```
|
||||
/// declare_plugin!("my-plugin", "My Plugin", "0.1.0");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! declare_plugin {
|
||||
($id:expr, $name:expr, $version:expr) => {
|
||||
// TODO: 实现宏
|
||||
};
|
||||
}
|
||||
13
crates/df-project/Cargo.toml
Normal file
13
crates/df-project/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "df-project"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
52
crates/df-project/src/context.rs
Normal file
52
crates/df-project/src/context.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! 项目上下文 — 项目运行时的环境与配置信息
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::ProjectId;
|
||||
|
||||
/// 项目上下文
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectContext {
|
||||
/// 项目 ID
|
||||
pub project_id: ProjectId,
|
||||
/// 项目根目录(本地文件系统路径)
|
||||
pub root_path: Option<String>,
|
||||
/// Git 仓库 URL
|
||||
pub repo_url: Option<String>,
|
||||
/// 当前分支
|
||||
pub current_branch: Option<String>,
|
||||
/// 环境变量
|
||||
pub env_vars: std::collections::HashMap<String, String>,
|
||||
/// 技术栈
|
||||
pub tech_stack: Vec<String>,
|
||||
/// AI 上下文(项目相关的 AI 记忆)
|
||||
pub ai_context: Option<AiContext>,
|
||||
}
|
||||
|
||||
/// AI 上下文信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiContext {
|
||||
/// 项目摘要
|
||||
pub summary: String,
|
||||
/// 架构描述
|
||||
pub architecture: Option<String>,
|
||||
/// 关键决策记录
|
||||
pub decisions: Vec<String>,
|
||||
/// 最近修改摘要
|
||||
pub recent_changes: Vec<String>,
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
/// 创建空上下文
|
||||
pub fn new(project_id: ProjectId) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
root_path: None,
|
||||
repo_url: None,
|
||||
current_branch: None,
|
||||
env_vars: std::collections::HashMap::new(),
|
||||
tech_stack: Vec::new(),
|
||||
ai_context: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/df-project/src/lib.rs
Normal file
6
crates/df-project/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-project: 项目管理 — 项目创建、调度、上下文、时间线
|
||||
|
||||
pub mod context;
|
||||
pub mod manager;
|
||||
pub mod scheduler;
|
||||
pub mod timeline;
|
||||
84
crates/df-project/src/manager.rs
Normal file
84
crates/df-project/src/manager.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! 项目管理器 — 项目的 CRUD 与生命周期管理
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::{IdeaId, Priority, ProjectId, ProjectStatus};
|
||||
|
||||
/// 项目实体
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
/// 唯一 ID
|
||||
pub id: ProjectId,
|
||||
/// 项目名称
|
||||
pub name: String,
|
||||
/// 项目描述
|
||||
pub description: String,
|
||||
/// 当前状态
|
||||
pub status: ProjectStatus,
|
||||
/// 来源想法 ID(可选,如果是从想法晋升来的)
|
||||
pub idea_id: Option<IdeaId>,
|
||||
/// 优先级
|
||||
pub priority: Priority,
|
||||
/// 标签
|
||||
pub tags: Vec<String>,
|
||||
/// 创建时间
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
/// 更新时间
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 创建项目的输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateProjectInput {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub idea_id: Option<IdeaId>,
|
||||
#[serde(default)]
|
||||
pub priority: Priority,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// 项目管理器
|
||||
pub struct ProjectManager;
|
||||
|
||||
impl ProjectManager {
|
||||
/// 创建新项目
|
||||
///
|
||||
/// TODO: 接入存储层持久化
|
||||
pub fn create(input: CreateProjectInput) -> Project {
|
||||
let now = chrono::Utc::now();
|
||||
Project {
|
||||
id: df_core::types::new_id(),
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: input.idea_id,
|
||||
priority: input.priority,
|
||||
tags: input.tags,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从想法创建项目
|
||||
pub fn create_from_idea(name: String, description: String, idea_id: IdeaId) -> Project {
|
||||
Self::create(CreateProjectInput {
|
||||
name,
|
||||
description,
|
||||
idea_id: Some(idea_id),
|
||||
priority: Priority::default(),
|
||||
tags: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新项目状态
|
||||
///
|
||||
/// TODO: 状态转换校验
|
||||
pub fn transition_status(project: &mut Project, new_status: ProjectStatus) -> anyhow::Result<()> {
|
||||
// TODO: 校验状态转换是否合法
|
||||
project.status = new_status;
|
||||
project.updated_at = chrono::Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
66
crates/df-project/src/scheduler.rs
Normal file
66
crates/df-project/src/scheduler.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! 项目调度器 — 自动化任务分配与调度
|
||||
|
||||
use df_core::types::{ProjectId, TaskId};
|
||||
|
||||
/// 调度策略
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SchedulingStrategy {
|
||||
/// FIFO(先进先出)
|
||||
Fifo,
|
||||
/// 按优先级调度
|
||||
Priority,
|
||||
/// 最短任务优先
|
||||
ShortestFirst,
|
||||
/// AI 智能调度
|
||||
AiDriven,
|
||||
}
|
||||
|
||||
/// 调度决策
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchedulingDecision {
|
||||
pub project_id: ProjectId,
|
||||
pub task_order: Vec<TaskId>,
|
||||
pub strategy: SchedulingStrategy,
|
||||
}
|
||||
|
||||
/// 项目调度器
|
||||
pub struct ProjectScheduler {
|
||||
strategy: SchedulingStrategy,
|
||||
}
|
||||
|
||||
impl ProjectScheduler {
|
||||
/// 创建调度器
|
||||
pub fn new(strategy: SchedulingStrategy) -> Self {
|
||||
Self { strategy }
|
||||
}
|
||||
|
||||
/// 为项目生成调度计划
|
||||
///
|
||||
/// TODO: 实现基于策略的调度算法
|
||||
pub fn schedule(&self, project_id: &ProjectId) -> anyhow::Result<SchedulingDecision> {
|
||||
match self.strategy {
|
||||
SchedulingStrategy::Fifo => {
|
||||
// TODO: 按创建时间排序
|
||||
tracing::info!("FIFO 调度,项目: {}", project_id);
|
||||
}
|
||||
SchedulingStrategy::Priority => {
|
||||
// TODO: 按优先级排序
|
||||
tracing::info!("优先级调度,项目: {}", project_id);
|
||||
}
|
||||
SchedulingStrategy::ShortestFirst => {
|
||||
// TODO: 按预估工作量排序
|
||||
tracing::info!("最短任务优先调度,项目: {}", project_id);
|
||||
}
|
||||
SchedulingStrategy::AiDriven => {
|
||||
// TODO: 接入 AI 进行智能调度
|
||||
tracing::info!("AI 智能调度,项目: {}", project_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SchedulingDecision {
|
||||
project_id: project_id.clone(),
|
||||
task_order: Vec::new(),
|
||||
strategy: self.strategy,
|
||||
})
|
||||
}
|
||||
}
|
||||
64
crates/df-project/src/timeline.rs
Normal file
64
crates/df-project/src/timeline.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! 项目时间线 — 里程碑与进度追踪
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::ProjectId;
|
||||
|
||||
/// 里程碑
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Milestone {
|
||||
/// 唯一 ID
|
||||
pub id: String,
|
||||
/// 所属项目 ID
|
||||
pub project_id: ProjectId,
|
||||
/// 里程碑名称
|
||||
pub name: String,
|
||||
/// 描述
|
||||
pub description: String,
|
||||
/// 计划完成时间
|
||||
pub due_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
/// 实际完成时间
|
||||
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
/// 进度百分比 (0-100)
|
||||
pub progress: u8,
|
||||
/// 是否已完成
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
/// 项目时间线
|
||||
pub struct Timeline {
|
||||
pub project_id: ProjectId,
|
||||
pub milestones: Vec<Milestone>,
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
/// 创建空时间线
|
||||
pub fn new(project_id: ProjectId) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
milestones: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加里程碑
|
||||
pub fn add_milestone(&mut self, milestone: Milestone) {
|
||||
self.milestones.push(milestone);
|
||||
}
|
||||
|
||||
/// 计算整体进度
|
||||
pub fn overall_progress(&self) -> f64 {
|
||||
if self.milestones.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let total: f64 = self.milestones.iter().map(|m| m.progress as f64).sum();
|
||||
total / self.milestones.len() as f64
|
||||
}
|
||||
|
||||
/// 获取下一个待完成的里程碑
|
||||
pub fn next_milestone(&self) -> Option<&Milestone> {
|
||||
self.milestones
|
||||
.iter()
|
||||
.filter(|m| !m.completed)
|
||||
.min_by_key(|m| m.due_date)
|
||||
}
|
||||
}
|
||||
14
crates/df-stages/Cargo.toml
Normal file
14
crates/df-stages/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "df-stages"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
df-workflow = { path = "../df-workflow" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
50
crates/df-stages/src/coding.rs
Normal file
50
crates/df-stages/src/coding.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
//! 编码阶段 — 代码生成与审查阶段模板
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema};
|
||||
|
||||
/// 代码生成节点
|
||||
pub struct CodeGenNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for CodeGenNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 AI 生成代码
|
||||
tracing::info!("代码生成节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.coding.codegen"
|
||||
}
|
||||
}
|
||||
|
||||
/// 代码审查节点
|
||||
pub struct CodeReviewNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for CodeReviewNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 AI 审查代码
|
||||
tracing::info!("代码审查节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.coding.review"
|
||||
}
|
||||
}
|
||||
57
crates/df-stages/src/idea.rs
Normal file
57
crates/df-stages/src/idea.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! 想法阶段 — 想法捕获与评估阶段模板
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema};
|
||||
|
||||
/// 想法捕获节点
|
||||
pub struct IdeaCaptureNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for IdeaCaptureNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 df-ideas 的捕获功能
|
||||
tracing::info!("想法捕获节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" }
|
||||
},
|
||||
"required": ["title"]
|
||||
}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.idea.capture"
|
||||
}
|
||||
}
|
||||
|
||||
/// 想法评估节点
|
||||
pub struct IdeaEvalNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for IdeaEvalNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 df-ideas 的评估功能
|
||||
tracing::info!("想法评估节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.idea.eval"
|
||||
}
|
||||
}
|
||||
7
crates/df-stages/src/lib.rs
Normal file
7
crates/df-stages/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! df-stages: 阶段插件 — 开发流程各阶段的节点模板注册
|
||||
|
||||
pub mod coding;
|
||||
pub mod idea;
|
||||
pub mod release;
|
||||
pub mod requirement;
|
||||
pub mod testing;
|
||||
73
crates/df-stages/src/release.rs
Normal file
73
crates/df-stages/src/release.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! 发布阶段 — 构建、部署与发布阶段模板
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema};
|
||||
|
||||
/// 构建节点
|
||||
pub struct BuildNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for BuildNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 df-execute 执行构建命令
|
||||
tracing::info!("构建节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.release.build"
|
||||
}
|
||||
}
|
||||
|
||||
/// 部署节点
|
||||
pub struct DeployNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for DeployNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 df-execute 执行部署命令
|
||||
tracing::info!("部署节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.release.deploy"
|
||||
}
|
||||
}
|
||||
|
||||
/// 发布公告节点
|
||||
pub struct ReleaseNotesNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for ReleaseNotesNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 AI 生成发布公告
|
||||
tracing::info!("发布公告节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.release.notes"
|
||||
}
|
||||
}
|
||||
50
crates/df-stages/src/requirement.rs
Normal file
50
crates/df-stages/src/requirement.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
//! 需求阶段 — 需求分析与文档生成阶段模板
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema};
|
||||
|
||||
/// 需求分析节点
|
||||
pub struct RequirementAnalysisNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for RequirementAnalysisNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 AI 分析需求
|
||||
tracing::info!("需求分析节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.requirement.analysis"
|
||||
}
|
||||
}
|
||||
|
||||
/// 需求文档生成节点
|
||||
pub struct RequirementDocNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for RequirementDocNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 AI 生成需求文档
|
||||
tracing::info!("需求文档生成节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.requirement.doc"
|
||||
}
|
||||
}
|
||||
50
crates/df-stages/src/testing.rs
Normal file
50
crates/df-stages/src/testing.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
//! 测试阶段 — 测试生成与执行阶段模板
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema};
|
||||
|
||||
/// 测试用例生成节点
|
||||
pub struct TestGenNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for TestGenNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 AI 生成测试用例
|
||||
tracing::info!("测试用例生成节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.testing.testgen"
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试执行节点
|
||||
pub struct TestRunNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for TestRunNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
// TODO: 调用 df-execute 执行测试命令
|
||||
tracing::info!("测试执行节点执行");
|
||||
Ok(df_workflow::node::NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::json!({"type": "object"}),
|
||||
output: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"stage.testing.run"
|
||||
}
|
||||
}
|
||||
13
crates/df-storage/Cargo.toml
Normal file
13
crates/df-storage/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "df-storage"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
614
crates/df-storage/src/crud.rs
Normal file
614
crates/df-storage/src/crud.rs
Normal file
@@ -0,0 +1,614 @@
|
||||
//! Repository 模式的 CRUD 操作
|
||||
//!
|
||||
//! 为 7 张表各提供一个 Repo 结构体,统一实现 insert / get_by_id / list_all / query / update_field / delete。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use rusqlite::{params, Connection, OptionalExtension, Row};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_core::error::{Error, Result};
|
||||
|
||||
use crate::db::Database;
|
||||
use crate::models::{
|
||||
AiConversationRecord, AiProviderRecord, AiToolExecutionRecord, BranchRecord, IdeaRecord,
|
||||
NodeExecutionRecord, ProjectRecord, ReleaseRecord, TaskRecord, WorkflowRecord,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 辅助宏 — 消除 6 个 Repo 的重复样板
|
||||
// ============================================================
|
||||
|
||||
/// 一次性为 7 张表生成完整的 Repo 结构体及其 CRUD 实现。
|
||||
///
|
||||
/// 用法: `impl_repo!(RepoName, ModelType, "table_name", from_row_body, insert_body);`
|
||||
macro_rules! impl_repo {
|
||||
(
|
||||
$(#[$meta:meta])*
|
||||
$name:ident, $record:ident, $table:literal,
|
||||
from_row => |$row:ident| $from_body:expr,
|
||||
insert => |$conn:ident, $rec:ident| $insert_body:expr
|
||||
) => {
|
||||
$(#[$meta])*
|
||||
pub struct $name {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn new(db: &Database) -> Self {
|
||||
Self { conn: db.conn() }
|
||||
}
|
||||
|
||||
pub async fn insert(&self, record: $record) -> Result<String> {
|
||||
let conn = self.conn.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let id = record.id.clone();
|
||||
let $conn = &*guard;
|
||||
let $rec = &record;
|
||||
$insert_body.map_err(|e: rusqlite::Error| Error::Storage(e.to_string()))?;
|
||||
Ok(id)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: &str) -> Result<Option<$record>> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&format!("SELECT * FROM {} WHERE id = ?1", $table))
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let row = stmt
|
||||
.query_row(params![id], |$row| $from_body)
|
||||
.optional()
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
Ok(row)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn list_all(&self) -> Result<Vec<$record>> {
|
||||
let conn = self.conn.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&format!("SELECT * FROM {} ORDER BY created_at DESC", $table))
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let rows = stmt
|
||||
.query_map([], |$row| $from_body)
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(
|
||||
r.map_err(|e| Error::Storage(e.to_string()))?,
|
||||
);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn query(&self, field: &str, value: &str) -> Result<Vec<$record>> {
|
||||
// 白名单校验,防止 SQL 注入
|
||||
validate_column_name(field)?;
|
||||
let conn = self.conn.clone();
|
||||
let sql = format!("SELECT * FROM {} WHERE {} = ?1 ORDER BY created_at DESC", $table, field);
|
||||
let value = value.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&sql)
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let rows = stmt
|
||||
.query_map(params![value], |$row| $from_body)
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(
|
||||
r.map_err(|e| Error::Storage(e.to_string()))?,
|
||||
);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn update_field(&self, id: &str, field: &str, value: &str) -> Result<bool> {
|
||||
validate_column_name(field)?;
|
||||
let conn = self.conn.clone();
|
||||
let sql = format!("UPDATE {} SET {} = ?1, updated_at = ?2 WHERE id = ?3", $table, field);
|
||||
let id = id.to_owned();
|
||||
let value = value.to_owned();
|
||||
let now = now_iso();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(&sql, params![value, now, id])
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: &str) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(&format!("DELETE FROM {} WHERE id = ?1", $table), params![id])
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 列名白名单 — 防止 SQL 注入
|
||||
// ============================================================
|
||||
|
||||
/// 所有表允许用于 query / update_field 的列名
|
||||
const ALLOWED_COLUMNS: &[&str] = &[
|
||||
// ideas
|
||||
"id",
|
||||
"title",
|
||||
"description",
|
||||
"status",
|
||||
"priority",
|
||||
"score",
|
||||
"tags",
|
||||
"source",
|
||||
"promoted_to",
|
||||
"ai_analysis",
|
||||
"scores",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
// projects / tasks 额外
|
||||
"name",
|
||||
"idea_id",
|
||||
"project_id",
|
||||
"branch_name",
|
||||
"assignee",
|
||||
"workflow_def_id",
|
||||
"base_branch",
|
||||
// branches 额外
|
||||
"task_id",
|
||||
"base",
|
||||
"merged_at",
|
||||
// releases 额外
|
||||
"version",
|
||||
"task_ids",
|
||||
"changelog",
|
||||
"released_at",
|
||||
// workflow / node 额外
|
||||
"dag_json",
|
||||
"triggered_by",
|
||||
"completed_at",
|
||||
"workflow_id",
|
||||
"node_id",
|
||||
"node_type",
|
||||
"input_json",
|
||||
"output_json",
|
||||
"error_message",
|
||||
"started_at",
|
||||
// AI 相关
|
||||
"provider_type",
|
||||
"api_key",
|
||||
"base_url",
|
||||
"default_model",
|
||||
"models",
|
||||
"is_default",
|
||||
"config",
|
||||
"conversation_id",
|
||||
"tool_call_id",
|
||||
"tool_name",
|
||||
"arguments",
|
||||
"result",
|
||||
"risk_level",
|
||||
"requested_at",
|
||||
"executed_at",
|
||||
"decided_by",
|
||||
];
|
||||
|
||||
fn validate_column_name(field: &str) -> Result<()> {
|
||||
if ALLOWED_COLUMNS.contains(&field) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Storage(format!("不允许的字段名: {}", field)))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 时间工具
|
||||
// ============================================================
|
||||
|
||||
fn now_iso() -> String {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// from_row 辅助函数
|
||||
// ============================================================
|
||||
|
||||
fn idea_from_row(row: &Row<'_>) -> std::result::Result<IdeaRecord, rusqlite::Error> {
|
||||
Ok(IdeaRecord {
|
||||
id: row.get("id")?,
|
||||
title: row.get("title")?,
|
||||
description: row.get("description")?,
|
||||
status: row.get("status")?,
|
||||
priority: row.get("priority")?,
|
||||
score: row.get("score")?,
|
||||
tags: row.get("tags")?,
|
||||
source: row.get("source")?,
|
||||
promoted_to: row.get("promoted_to")?,
|
||||
ai_analysis: row.get("ai_analysis")?,
|
||||
scores: row.get("scores")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn project_from_row(row: &Row<'_>) -> std::result::Result<ProjectRecord, rusqlite::Error> {
|
||||
Ok(ProjectRecord {
|
||||
id: row.get("id")?,
|
||||
name: row.get("name")?,
|
||||
description: row.get("description")?,
|
||||
status: row.get("status")?,
|
||||
idea_id: row.get("idea_id")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn task_from_row(row: &Row<'_>) -> std::result::Result<TaskRecord, rusqlite::Error> {
|
||||
Ok(TaskRecord {
|
||||
id: row.get("id")?,
|
||||
project_id: row.get("project_id")?,
|
||||
title: row.get("title")?,
|
||||
description: row.get("description")?,
|
||||
status: row.get("status")?,
|
||||
priority: row.get("priority")?,
|
||||
branch_name: row.get("branch_name")?,
|
||||
assignee: row.get("assignee")?,
|
||||
workflow_def_id: row.get("workflow_def_id")?,
|
||||
base_branch: row.get("base_branch")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn release_from_row(row: &Row<'_>) -> std::result::Result<ReleaseRecord, rusqlite::Error> {
|
||||
Ok(ReleaseRecord {
|
||||
id: row.get("id")?,
|
||||
project_id: row.get("project_id")?,
|
||||
version: row.get("version")?,
|
||||
status: row.get("status")?,
|
||||
task_ids: row.get("task_ids")?,
|
||||
changelog: row.get("changelog")?,
|
||||
created_at: row.get("created_at")?,
|
||||
released_at: row.get("released_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn workflow_from_row(row: &Row<'_>) -> std::result::Result<WorkflowRecord, rusqlite::Error> {
|
||||
Ok(WorkflowRecord {
|
||||
id: row.get("id")?,
|
||||
name: row.get("name")?,
|
||||
dag_json: row.get("dag_json")?,
|
||||
status: row.get("status")?,
|
||||
triggered_by: row.get("triggered_by")?,
|
||||
project_id: row.get("project_id")?,
|
||||
task_id: row.get("task_id")?,
|
||||
created_at: row.get("created_at")?,
|
||||
completed_at: row.get("completed_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn branch_from_row(row: &Row<'_>) -> std::result::Result<BranchRecord, rusqlite::Error> {
|
||||
Ok(BranchRecord {
|
||||
id: row.get("id")?,
|
||||
project_id: row.get("project_id")?,
|
||||
task_id: row.get("task_id")?,
|
||||
name: row.get("name")?,
|
||||
base: row.get("base")?,
|
||||
status: row.get("status")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
merged_at: row.get("merged_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn node_execution_from_row(
|
||||
row: &Row<'_>,
|
||||
) -> std::result::Result<NodeExecutionRecord, rusqlite::Error> {
|
||||
Ok(NodeExecutionRecord {
|
||||
id: row.get("id")?,
|
||||
workflow_id: row.get("workflow_id")?,
|
||||
node_id: row.get("node_id")?,
|
||||
node_type: row.get("node_type")?,
|
||||
status: row.get("status")?,
|
||||
input_json: row.get("input_json")?,
|
||||
output_json: row.get("output_json")?,
|
||||
error_message: row.get("error_message")?,
|
||||
started_at: row.get("started_at")?,
|
||||
completed_at: row.get("completed_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 7 个 Repo 实现
|
||||
// ============================================================
|
||||
|
||||
impl_repo!(
|
||||
/// 想法表 CRUD
|
||||
IdeaRepo,
|
||||
IdeaRecord,
|
||||
"ideas",
|
||||
from_row => |row| idea_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO ideas (id, title, description, status, priority, score, tags, source, promoted_to, ai_analysis, scores, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
params![
|
||||
rec.id, rec.title, rec.description, rec.status, rec.priority,
|
||||
rec.score, rec.tags, rec.source, rec.promoted_to, rec.ai_analysis,
|
||||
rec.scores, rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// 项目表 CRUD
|
||||
ProjectRepo,
|
||||
ProjectRecord,
|
||||
"projects",
|
||||
from_row => |row| project_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO projects (id, name, description, status, idea_id, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
rec.id, rec.name, rec.description, rec.status, rec.idea_id,
|
||||
rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// 任务表 CRUD
|
||||
TaskRepo,
|
||||
TaskRecord,
|
||||
"tasks",
|
||||
from_row => |row| task_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||
params![
|
||||
rec.id, rec.project_id, rec.title, rec.description, rec.status, rec.priority,
|
||||
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||
rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// 分支表 CRUD
|
||||
BranchRepo,
|
||||
BranchRecord,
|
||||
"branches",
|
||||
from_row => |row| branch_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO branches (id, project_id, task_id, name, base, status, created_at, updated_at, merged_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
rec.id, rec.project_id, rec.task_id, rec.name, rec.base,
|
||||
rec.status, rec.created_at, rec.updated_at, rec.merged_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// 发布表 CRUD
|
||||
ReleaseRepo,
|
||||
ReleaseRecord,
|
||||
"releases",
|
||||
from_row => |row| release_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO releases (id, project_id, version, status, task_ids, changelog, created_at, released_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![
|
||||
rec.id, rec.project_id, rec.version, rec.status, rec.task_ids,
|
||||
rec.changelog, rec.created_at, rec.released_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// 工作流执行表 CRUD
|
||||
WorkflowRepo,
|
||||
WorkflowRecord,
|
||||
"workflow_executions",
|
||||
from_row => |row| workflow_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO workflow_executions (id, name, dag_json, status, triggered_by, project_id, task_id, created_at, completed_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
rec.id, rec.name, rec.dag_json, rec.status, rec.triggered_by,
|
||||
rec.project_id, rec.task_id, rec.created_at, rec.completed_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// 节点执行表 CRUD
|
||||
NodeExecutionRepo,
|
||||
NodeExecutionRecord,
|
||||
"node_executions",
|
||||
from_row => |row| node_execution_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO node_executions (id, workflow_id, node_id, node_type, status, input_json, output_json, error_message, started_at, completed_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
params![
|
||||
rec.id, rec.workflow_id, rec.node_id, rec.node_type, rec.status,
|
||||
rec.input_json, rec.output_json, rec.error_message, rec.started_at, rec.completed_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// AI 相关 Repo (V3)
|
||||
// ============================================================
|
||||
|
||||
fn ai_provider_from_row(row: &Row<'_>) -> std::result::Result<AiProviderRecord, rusqlite::Error> {
|
||||
Ok(AiProviderRecord {
|
||||
id: row.get("id")?,
|
||||
name: row.get("name")?,
|
||||
provider_type: row.get("provider_type")?,
|
||||
api_key: row.get("api_key")?,
|
||||
base_url: row.get("base_url")?,
|
||||
default_model: row.get("default_model")?,
|
||||
models: row.get("models")?,
|
||||
is_default: row.get::<_, i32>("is_default")? != 0,
|
||||
config: row.get("config")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn ai_conversation_from_row(row: &Row<'_>) -> std::result::Result<AiConversationRecord, rusqlite::Error> {
|
||||
Ok(AiConversationRecord {
|
||||
id: row.get("id")?,
|
||||
title: row.get("title")?,
|
||||
messages: row.get("messages")?,
|
||||
provider_id: row.get("provider_id")?,
|
||||
model: row.get("model")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn ai_tool_execution_from_row(row: &Row<'_>) -> std::result::Result<AiToolExecutionRecord, rusqlite::Error> {
|
||||
Ok(AiToolExecutionRecord {
|
||||
id: row.get("id")?,
|
||||
conversation_id: row.get("conversation_id")?,
|
||||
tool_call_id: row.get("tool_call_id")?,
|
||||
tool_name: row.get("tool_name")?,
|
||||
arguments: row.get("arguments")?,
|
||||
result: row.get("result")?,
|
||||
status: row.get("status")?,
|
||||
risk_level: row.get("risk_level")?,
|
||||
requested_at: row.get("requested_at")?,
|
||||
executed_at: row.get("executed_at")?,
|
||||
decided_by: row.get("decided_by")?,
|
||||
})
|
||||
}
|
||||
|
||||
impl_repo!(
|
||||
/// AI 提供商配置表 CRUD
|
||||
AiProviderRepo,
|
||||
AiProviderRecord,
|
||||
"ai_providers",
|
||||
from_row => |row| ai_provider_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
let is_default = if rec.is_default { 1i32 } else { 0i32 };
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO ai_providers (id, name, provider_type, api_key, base_url, default_model, models, is_default, config, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
rec.id, rec.name, rec.provider_type, rec.api_key, rec.base_url,
|
||||
rec.default_model, rec.models, is_default, rec.config, rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// AI 对话历史表 CRUD
|
||||
AiConversationRepo,
|
||||
AiConversationRecord,
|
||||
"ai_conversations",
|
||||
from_row => |row| ai_conversation_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO ai_conversations (id, title, messages, provider_id, model, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
rec.id, rec.title, rec.messages, rec.provider_id, rec.model,
|
||||
rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
impl_repo!(
|
||||
/// AI 工具执行审计表 CRUD
|
||||
AiToolExecutionRepo,
|
||||
AiToolExecutionRecord,
|
||||
"ai_tool_executions",
|
||||
from_row => |row| ai_tool_execution_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO ai_tool_executions (id, conversation_id, tool_call_id, tool_name, arguments, result, status, risk_level, requested_at, executed_at, decided_by)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
rec.id, rec.conversation_id, rec.tool_call_id, rec.tool_name,
|
||||
rec.arguments, rec.result, rec.status, rec.risk_level,
|
||||
rec.requested_at, rec.executed_at, rec.decided_by
|
||||
],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// AiConversationRepo 扩展方法(绕过 update_field 白名单限制)
|
||||
// ============================================================
|
||||
|
||||
impl AiConversationRepo {
|
||||
/// 整体更新对话记录(title + messages + updated_at)
|
||||
pub async fn update_full(&self, record: &AiConversationRecord) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = record.id.clone();
|
||||
let title = record.title.clone();
|
||||
let messages = record.messages.clone();
|
||||
let provider_id = record.provider_id.clone();
|
||||
let model = record.model.clone();
|
||||
let updated_at = record.updated_at.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard.execute(
|
||||
"UPDATE ai_conversations SET title = ?1, messages = ?2, provider_id = ?3, model = ?4, updated_at = ?5 WHERE id = ?6",
|
||||
params![title, messages, provider_id, model, updated_at, id],
|
||||
).map_err(|e| Error::Storage(e.to_string()))?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
}
|
||||
61
crates/df-storage/src/db.rs
Normal file
61
crates/df-storage/src/db.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! SQLite 数据库连接管理
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::migrations;
|
||||
|
||||
/// SQLite 数据库管理器
|
||||
pub struct Database {
|
||||
/// 数据库连接(线程安全)
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// 打开(或创建)数据库文件
|
||||
pub async fn open(path: &Path) -> Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
|
||||
|
||||
// 执行迁移
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
};
|
||||
db.run_migrations().await?;
|
||||
|
||||
tracing::info!("数据库已打开: {}", path.display());
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// 打开内存数据库(用于测试)
|
||||
pub async fn open_in_memory() -> Result<Self> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
conn.execute_batch("PRAGMA foreign_keys=ON;")?;
|
||||
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
};
|
||||
db.run_migrations().await?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// 执行数据库迁移
|
||||
async fn run_migrations(&self) -> Result<()> {
|
||||
let conn = self.conn.lock().await;
|
||||
migrations::run(&conn)?;
|
||||
tracing::info!("数据库迁移完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取连接的锁守卫
|
||||
///
|
||||
/// TODO: 考虑使用 r2d2 连接池替代单连接 Mutex
|
||||
pub fn conn(&self) -> Arc<Mutex<Connection>> {
|
||||
Arc::clone(&self.conn)
|
||||
}
|
||||
}
|
||||
6
crates/df-storage/src/lib.rs
Normal file
6
crates/df-storage/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-storage: 存储层 — SQLite 数据库、模型定义、迁移
|
||||
|
||||
pub mod crud;
|
||||
pub mod db;
|
||||
pub mod migrations;
|
||||
pub mod models;
|
||||
174
crates/df-storage/src/migrations.rs
Normal file
174
crates/df-storage/src/migrations.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! 数据库迁移 — 建表 SQL 与版本管理
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// 当前迁移版本
|
||||
const MIGRATION_VERSION: i32 = 2;
|
||||
|
||||
/// 执行所有迁移
|
||||
pub fn run(conn: &Connection) -> Result<()> {
|
||||
// 创建迁移版本表
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY
|
||||
);"
|
||||
)?;
|
||||
|
||||
let current_version: i32 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
if current_version < 1 {
|
||||
migrate_v1(conn)?;
|
||||
}
|
||||
|
||||
if current_version < 2 {
|
||||
migrate_v2(conn)?;
|
||||
}
|
||||
|
||||
// 未来迁移在此扩展:
|
||||
// if current_version < 3 { migrate_v3(conn)?; }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V1: 初始表结构
|
||||
fn migrate_v1(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V1_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [1])?;
|
||||
tracing::info!("迁移 v1 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V2: 补齐关联字段 + branches 表
|
||||
fn migrate_v2(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(V2_SQL)?;
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [2])?;
|
||||
tracing::info!("迁移 v2 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V1 建表 SQL
|
||||
const V1_SQL: &str = "
|
||||
-- 想法表
|
||||
CREATE TABLE IF NOT EXISTS ideas (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
priority INTEGER NOT NULL DEFAULT 1,
|
||||
score REAL,
|
||||
tags TEXT,
|
||||
source TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- 项目表
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'planning',
|
||||
idea_id TEXT REFERENCES ideas(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- 任务表
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'todo',
|
||||
priority INTEGER NOT NULL DEFAULT 1,
|
||||
branch_name TEXT,
|
||||
assignee TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- 发布表
|
||||
CREATE TABLE IF NOT EXISTS releases (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
version TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'planned',
|
||||
task_ids TEXT NOT NULL DEFAULT '[]',
|
||||
changelog TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
released_at TEXT
|
||||
);
|
||||
|
||||
-- 工作流执行表
|
||||
CREATE TABLE IF NOT EXISTS workflow_executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
dag_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
triggered_by TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
-- 节点执行表
|
||||
CREATE TABLE IF NOT EXISTS node_executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
workflow_id TEXT NOT NULL REFERENCES workflow_executions(id),
|
||||
node_id TEXT NOT NULL,
|
||||
node_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
input_json TEXT,
|
||||
output_json TEXT,
|
||||
error_message TEXT,
|
||||
started_at TEXT,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_releases_project_id ON releases(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_node_executions_workflow_id ON node_executions(workflow_id);
|
||||
";
|
||||
|
||||
/// V2 迁移 SQL — 补齐数据层断裂字段
|
||||
///
|
||||
/// 注意: SQLite 的 ALTER TABLE ADD COLUMN 一条语句只能加一列。
|
||||
const V2_SQL: &str = "
|
||||
-- 想法表: 晋升关联 + AI 分析 + 多维评分
|
||||
ALTER TABLE ideas ADD COLUMN promoted_to TEXT;
|
||||
ALTER TABLE ideas ADD COLUMN ai_analysis TEXT;
|
||||
ALTER TABLE ideas ADD COLUMN scores TEXT;
|
||||
|
||||
-- 任务表: 工作流定义关联 + 基础分支
|
||||
ALTER TABLE tasks ADD COLUMN workflow_def_id TEXT;
|
||||
ALTER TABLE tasks ADD COLUMN base_branch TEXT;
|
||||
|
||||
-- 工作流执行表: 项目 / 任务关联
|
||||
ALTER TABLE workflow_executions ADD COLUMN project_id TEXT;
|
||||
ALTER TABLE workflow_executions ADD COLUMN task_id TEXT;
|
||||
|
||||
-- 分支表 — 任务与 Git 分支绑定(核心功能)
|
||||
CREATE TABLE IF NOT EXISTS branches (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
task_id TEXT REFERENCES tasks(id),
|
||||
name TEXT NOT NULL,
|
||||
base TEXT NOT NULL DEFAULT 'main',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
merged_at TEXT
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_branches_project_id ON branches(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_branches_task_id ON branches(task_id);
|
||||
";
|
||||
170
crates/df-storage/src/models.rs
Normal file
170
crates/df-storage/src/models.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
//! 数据模型定义 — 与数据库表对应的 Rust 结构体
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================
|
||||
// 想法相关模型
|
||||
// ============================================================
|
||||
|
||||
/// 想法记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IdeaRecord {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
pub priority: i32,
|
||||
pub score: Option<f64>,
|
||||
pub tags: Option<String>, // JSON 数组
|
||||
pub source: Option<String>,
|
||||
pub promoted_to: Option<String>, // 晋升后的 project_id
|
||||
pub ai_analysis: Option<String>, // AI 分析结果 JSON
|
||||
pub scores: Option<String>, // 多维评分 JSON (feasibility/impact/urgency/overall)
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 项目相关模型
|
||||
// ============================================================
|
||||
|
||||
/// 项目记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectRecord {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
pub idea_id: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 任务相关模型
|
||||
// ============================================================
|
||||
|
||||
/// 任务记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskRecord {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
pub priority: i32,
|
||||
pub branch_name: Option<String>,
|
||||
pub assignee: Option<String>,
|
||||
pub workflow_def_id: Option<String>, // 关联的工作流定义 ID
|
||||
pub base_branch: Option<String>, // 基础分支
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// 分支记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BranchRecord {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub task_id: Option<String>,
|
||||
pub name: String,
|
||||
pub base: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub merged_at: Option<String>,
|
||||
}
|
||||
|
||||
/// 发布记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReleaseRecord {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub version: String,
|
||||
pub status: String,
|
||||
pub task_ids: String, // JSON 数组
|
||||
pub changelog: Option<String>,
|
||||
pub created_at: String,
|
||||
pub released_at: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工作流相关模型
|
||||
// ============================================================
|
||||
|
||||
/// 工作流执行记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkflowRecord {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub dag_json: String, // DAG 的 JSON 序列化
|
||||
pub status: String,
|
||||
pub triggered_by: Option<String>,
|
||||
pub project_id: Option<String>, // 关联项目 ID
|
||||
pub task_id: Option<String>, // 关联任务 ID
|
||||
pub created_at: String,
|
||||
pub completed_at: Option<String>,
|
||||
}
|
||||
|
||||
/// 节点执行记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeExecutionRecord {
|
||||
pub id: String,
|
||||
pub workflow_id: String,
|
||||
pub node_id: String,
|
||||
pub node_type: String,
|
||||
pub status: String,
|
||||
pub input_json: Option<String>,
|
||||
pub output_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
pub started_at: Option<String>,
|
||||
pub completed_at: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AI 相关模型 (V3)
|
||||
// ============================================================
|
||||
|
||||
/// AI 提供商配置记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiProviderRecord {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub provider_type: String,
|
||||
pub api_key: String,
|
||||
pub base_url: String,
|
||||
pub default_model: String,
|
||||
pub models: Option<String>, // JSON array of model names
|
||||
pub is_default: bool,
|
||||
pub config: Option<String>, // JSON extra config
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// AI 对话记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiConversationRecord {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub messages: String, // JSON array of ChatMessage
|
||||
pub provider_id: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// AI 工具执行审计记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiToolExecutionRecord {
|
||||
pub id: String,
|
||||
pub conversation_id: Option<String>,
|
||||
pub tool_call_id: String,
|
||||
pub tool_name: String,
|
||||
pub arguments: String,
|
||||
pub result: Option<String>,
|
||||
pub status: String, // pending/approved/rejected/executing/completed/failed
|
||||
pub risk_level: String, // low/medium/high
|
||||
pub requested_at: String,
|
||||
pub executed_at: Option<String>,
|
||||
pub decided_by: Option<String>, // human/auto
|
||||
}
|
||||
13
crates/df-task/Cargo.toml
Normal file
13
crates/df-task/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "df-task"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
74
crates/df-task/src/branch.rs
Normal file
74
crates/df-task/src/branch.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! 分支管理 — Git 分支的创建、跟踪与生命周期
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::{BranchId, ProjectId, TaskId};
|
||||
|
||||
/// 分支状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BranchStatus {
|
||||
/// 活跃开发中
|
||||
Active,
|
||||
/// 待合并
|
||||
ReadyToMerge,
|
||||
/// 已合并
|
||||
Merged,
|
||||
/// 已删除
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// 分支实体
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Branch {
|
||||
/// 唯一 ID
|
||||
pub id: BranchId,
|
||||
/// 分支名称
|
||||
pub name: String,
|
||||
/// 所属项目 ID
|
||||
pub project_id: ProjectId,
|
||||
/// 关联任务 ID
|
||||
pub task_id: Option<TaskId>,
|
||||
/// 基于的分支(通常是 main)
|
||||
pub base_branch: String,
|
||||
/// 当前状态
|
||||
pub status: BranchStatus,
|
||||
/// 创建时间
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 分支管理器
|
||||
pub struct BranchManager;
|
||||
|
||||
impl BranchManager {
|
||||
/// 为任务创建分支
|
||||
///
|
||||
/// TODO: 接入 df-execute 的 git_ops 执行实际的 Git 操作
|
||||
pub fn create_for_task(
|
||||
task_id: &TaskId,
|
||||
project_id: &ProjectId,
|
||||
base_branch: &str,
|
||||
) -> Branch {
|
||||
// 生成分支名称:task/{task_id 前缀}
|
||||
let short_id = &task_id[..8.min(task_id.len())];
|
||||
let branch_name = format!("task/{}", short_id);
|
||||
|
||||
Branch {
|
||||
id: df_core::types::new_id(),
|
||||
name: branch_name,
|
||||
project_id: project_id.clone(),
|
||||
task_id: Some(task_id.clone()),
|
||||
base_branch: base_branch.to_string(),
|
||||
status: BranchStatus::Active,
|
||||
created_at: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出项目的所有活跃分支
|
||||
///
|
||||
/// TODO: 接入 df-execute 的 git_ops
|
||||
pub fn list_active(_project_id: &ProjectId) -> Vec<Branch> {
|
||||
// TODO: 实现
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
6
crates/df-task/src/lib.rs
Normal file
6
crates/df-task/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-task: 任务/分支管理 — 任务 CRUD、分支管理、合并协调、发布计划
|
||||
|
||||
pub mod branch;
|
||||
pub mod merge;
|
||||
pub mod release;
|
||||
pub mod task;
|
||||
91
crates/df-task/src/merge.rs
Normal file
91
crates/df-task/src/merge.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! 合并协调器 — 分支合并、冲突检测、AI 辅助解决
|
||||
|
||||
use df_core::types::{BranchId, TaskId};
|
||||
|
||||
/// 合并状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MergeStatus {
|
||||
/// 待合并
|
||||
Pending,
|
||||
/// 自动合并中
|
||||
AutoMerging,
|
||||
/// 存在冲突,等待解决
|
||||
Conflicted,
|
||||
/// 已解决冲突
|
||||
Resolved,
|
||||
/// 合并完成
|
||||
Completed,
|
||||
/// 合并失败
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// 合并请求
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MergeRequest {
|
||||
pub id: String,
|
||||
pub source_branch: BranchId,
|
||||
pub target_branch: String,
|
||||
pub task_id: Option<TaskId>,
|
||||
pub status: MergeStatus,
|
||||
pub conflicts: Vec<Conflict>,
|
||||
}
|
||||
|
||||
/// 冲突信息
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Conflict {
|
||||
pub file_path: String,
|
||||
pub conflict_type: ConflictType,
|
||||
pub description: String,
|
||||
pub auto_resolvable: bool,
|
||||
}
|
||||
|
||||
/// 冲突类型
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConflictType {
|
||||
/// 文件级别冲突(双方修改了同一文件)
|
||||
FileModified,
|
||||
/// 删除/修改冲突
|
||||
DeleteModify,
|
||||
/// 二进制文件冲突
|
||||
Binary,
|
||||
}
|
||||
|
||||
/// 合并协调器
|
||||
pub struct MergeCoordinator;
|
||||
|
||||
impl MergeCoordinator {
|
||||
/// 创建合并请求
|
||||
///
|
||||
/// TODO: 接入 Git 操作执行实际合并
|
||||
pub fn create_request(
|
||||
source_branch: BranchId,
|
||||
target_branch: String,
|
||||
task_id: Option<TaskId>,
|
||||
) -> MergeRequest {
|
||||
MergeRequest {
|
||||
id: df_core::types::new_id(),
|
||||
source_branch,
|
||||
target_branch,
|
||||
task_id,
|
||||
status: MergeStatus::Pending,
|
||||
conflicts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检测冲突
|
||||
///
|
||||
/// TODO: 调用 git merge --no-commit --no-ff 检测
|
||||
pub fn detect_conflicts(_merge_request: &MergeRequest) -> Vec<Conflict> {
|
||||
// TODO: 实现冲突检测
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// AI 辅助解决冲突
|
||||
///
|
||||
/// TODO: 调用 df-ai 的 LLM 分析冲突并提供解决方案
|
||||
pub fn ai_resolve_conflict(_conflict: &Conflict) -> anyhow::Result<String> {
|
||||
// TODO: 实现 AI 辅助冲突解决
|
||||
tracing::warn!("AI 冲突解决尚未实现");
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
132
crates/df-task/src/release.rs
Normal file
132
crates/df-task/src/release.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! 发布计划 — 多任务合并、版本号管理、发布流程
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::{ProjectId, ReleaseId, TaskId};
|
||||
|
||||
/// 发布状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReleaseStatus {
|
||||
/// 计划中
|
||||
Planned,
|
||||
/// 准备中(收集已完成的任务)
|
||||
Preparing,
|
||||
/// 构建中
|
||||
Building,
|
||||
/// 测试中
|
||||
Testing,
|
||||
/// 待发布
|
||||
Ready,
|
||||
/// 发布中
|
||||
Releasing,
|
||||
/// 已发布
|
||||
Released,
|
||||
/// 已回滚
|
||||
RolledBack,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// 版本号
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SemanticVersion {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
pub pre_release: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SemanticVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.pre_release {
|
||||
Some(pre) => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
|
||||
None => write!(f, "{}.{}.{}", self.major, self.minor, self.patch),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 发布计划
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Release {
|
||||
/// 唯一 ID
|
||||
pub id: ReleaseId,
|
||||
/// 所属项目 ID
|
||||
pub project_id: ProjectId,
|
||||
/// 版本号
|
||||
pub version: SemanticVersion,
|
||||
/// 当前状态
|
||||
pub status: ReleaseStatus,
|
||||
/// 包含的任务 ID 列表
|
||||
pub task_ids: Vec<TaskId>,
|
||||
/// 变更日志
|
||||
pub changelog: Option<String>,
|
||||
/// 创建时间
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
/// 发布时间
|
||||
pub released_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
/// 版本号递增类型
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum VersionBump {
|
||||
Major,
|
||||
Minor,
|
||||
Patch,
|
||||
}
|
||||
|
||||
/// 发布管理器
|
||||
pub struct ReleaseManager;
|
||||
|
||||
impl ReleaseManager {
|
||||
/// 创建发布计划
|
||||
pub fn create(
|
||||
project_id: &ProjectId,
|
||||
version: SemanticVersion,
|
||||
task_ids: Vec<TaskId>,
|
||||
) -> Release {
|
||||
Release {
|
||||
id: df_core::types::new_id(),
|
||||
project_id: project_id.clone(),
|
||||
version,
|
||||
status: ReleaseStatus::Planned,
|
||||
task_ids,
|
||||
changelog: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
released_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动生成变更日志
|
||||
///
|
||||
/// TODO: 从 Git 提交历史、任务描述中生成
|
||||
pub fn generate_changelog(_release: &mut Release) -> anyhow::Result<()> {
|
||||
// TODO: 实现
|
||||
tracing::warn!("自动变更日志生成尚未实现");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 自动递增版本号
|
||||
pub fn bump_version(current: &SemanticVersion, bump: VersionBump) -> SemanticVersion {
|
||||
match bump {
|
||||
VersionBump::Major => SemanticVersion {
|
||||
major: current.major + 1,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
pre_release: None,
|
||||
},
|
||||
VersionBump::Minor => SemanticVersion {
|
||||
major: current.major,
|
||||
minor: current.minor + 1,
|
||||
patch: 0,
|
||||
pre_release: None,
|
||||
},
|
||||
VersionBump::Patch => SemanticVersion {
|
||||
major: current.major,
|
||||
minor: current.minor,
|
||||
patch: current.patch + 1,
|
||||
pre_release: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
87
crates/df-task/src/task.rs
Normal file
87
crates/df-task/src/task.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! 任务管理 — 任务实体与 CRUD
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::{BranchId, Priority, ProjectId, TaskId, TaskStatus};
|
||||
|
||||
/// 任务实体
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
/// 唯一 ID
|
||||
pub id: TaskId,
|
||||
/// 所属项目 ID
|
||||
pub project_id: ProjectId,
|
||||
/// 任务标题
|
||||
pub title: String,
|
||||
/// 任务描述
|
||||
pub description: String,
|
||||
/// 当前状态
|
||||
pub status: TaskStatus,
|
||||
/// 优先级
|
||||
pub priority: Priority,
|
||||
/// 关联分支名称
|
||||
pub branch_name: Option<String>,
|
||||
/// 关联分支 ID
|
||||
pub branch_id: Option<BranchId>,
|
||||
/// 指派人
|
||||
pub assignee: Option<String>,
|
||||
/// 标签
|
||||
pub tags: Vec<String>,
|
||||
/// 预估工时(小时)
|
||||
pub estimate_hours: Option<f64>,
|
||||
/// 实际工时(小时)
|
||||
pub actual_hours: Option<f64>,
|
||||
/// 创建时间
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
/// 更新时间
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 创建任务的输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateTaskInput {
|
||||
pub project_id: ProjectId,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub priority: Priority,
|
||||
pub assignee: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub estimate_hours: Option<f64>,
|
||||
}
|
||||
|
||||
/// 任务管理器
|
||||
pub struct TaskManager;
|
||||
|
||||
impl TaskManager {
|
||||
/// 创建新任务
|
||||
///
|
||||
/// TODO: 接入存储层持久化
|
||||
pub fn create(input: CreateTaskInput) -> Task {
|
||||
let now = chrono::Utc::now();
|
||||
Task {
|
||||
id: df_core::types::new_id(),
|
||||
project_id: input.project_id,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
status: TaskStatus::Todo,
|
||||
priority: input.priority,
|
||||
branch_name: None,
|
||||
branch_id: None,
|
||||
assignee: input.assignee,
|
||||
tags: input.tags,
|
||||
estimate_hours: input.estimate_hours,
|
||||
actual_hours: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新任务状态
|
||||
pub fn transition_status(task: &mut Task, new_status: TaskStatus) -> anyhow::Result<()> {
|
||||
// TODO: 校验状态转换合法性
|
||||
task.status = new_status;
|
||||
task.updated_at = chrono::Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
11
crates/df-traceability/Cargo.toml
Normal file
11
crates/df-traceability/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "df-traceability"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
130
crates/df-traceability/src/annotation.rs
Normal file
130
crates/df-traceability/src/annotation.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! 标注系统:FIXME/TODO/QUESTION 等标记,批量收集后交给 AI 处理
|
||||
|
||||
use df_core::types::ProjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 标注标记类型
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnnotationMarker {
|
||||
/// 需要修复
|
||||
Fixme,
|
||||
/// 待办事项
|
||||
Todo,
|
||||
/// 疑问待确认
|
||||
Question,
|
||||
/// 风险标记
|
||||
Risk,
|
||||
/// 决策标记
|
||||
Decision,
|
||||
/// 优化建议
|
||||
Optimize,
|
||||
}
|
||||
|
||||
/// 标注状态
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnnotationStatus {
|
||||
/// 未处理
|
||||
Open,
|
||||
/// AI 处理中
|
||||
AiProcessing,
|
||||
/// 已解决
|
||||
Resolved,
|
||||
/// 不处理
|
||||
WontFix,
|
||||
}
|
||||
|
||||
/// 标注可附加的实体类型
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EntityType {
|
||||
Requirement,
|
||||
Feature,
|
||||
Code,
|
||||
TestCase,
|
||||
TestReport,
|
||||
DesignDoc,
|
||||
}
|
||||
|
||||
/// 标注
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Annotation {
|
||||
pub id: String,
|
||||
pub project_id: ProjectId,
|
||||
/// 附加的实体类型
|
||||
pub entity_type: EntityType,
|
||||
/// 附加的实体 ID
|
||||
pub entity_id: String,
|
||||
/// 标记类型
|
||||
pub marker: AnnotationMarker,
|
||||
/// 标注内容
|
||||
pub content: String,
|
||||
/// 位置(文件路径+行号 / 文档段落)
|
||||
pub location: Option<String>,
|
||||
/// 状态
|
||||
pub status: AnnotationStatus,
|
||||
/// 处理者
|
||||
pub resolved_by: Option<String>,
|
||||
/// 处理结果
|
||||
pub resolution: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub resolved_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl Annotation {
|
||||
pub fn new(
|
||||
project_id: ProjectId,
|
||||
entity_type: EntityType,
|
||||
entity_id: String,
|
||||
marker: AnnotationMarker,
|
||||
content: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: df_core::types::new_id(),
|
||||
project_id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
marker,
|
||||
content,
|
||||
location: None,
|
||||
status: AnnotationStatus::Open,
|
||||
resolved_by: None,
|
||||
resolution: None,
|
||||
created_at: chrono::Utc::now().timestamp(),
|
||||
resolved_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 标注批量处理器
|
||||
pub struct AnnotationBatchProcessor;
|
||||
|
||||
impl AnnotationBatchProcessor {
|
||||
/// 收集项目中所有未处理的标注
|
||||
pub fn collect_open_annotations(_project_id: &ProjectId) -> Vec<Annotation> {
|
||||
// TODO: 从 SQLite 查询所有 status = Open 的标注
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// 按类型分组
|
||||
pub fn group_by_marker(annotations: &[Annotation]) -> std::collections::HashMap<AnnotationMarker, Vec<&Annotation>> {
|
||||
let mut groups: std::collections::HashMap<AnnotationMarker, Vec<&Annotation>> = std::collections::HashMap::new();
|
||||
for ann in annotations {
|
||||
groups.entry(ann.marker.clone()).or_default().push(ann);
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
/// 将分组后的标注交给 AI 批量处理
|
||||
///
|
||||
/// AI 会逐条处理每个标注,更新内容和状态
|
||||
pub async fn batch_process(_annotations: &[Annotation]) -> anyhow::Result<Vec<Annotation>> {
|
||||
// TODO: 调用 df-ai 编排器,构建批量处理 prompt
|
||||
// 1. 按类型分组
|
||||
// 2. 每组构建一个 AI 请求
|
||||
// 3. AI 返回处理结果
|
||||
// 4. 更新标注状态和 resolution
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
95
crates/df-traceability/src/decision.rs
Normal file
95
crates/df-traceability/src/decision.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
//! 决策留痕:所有关键决策的记录、检索和审计
|
||||
|
||||
use df_core::types::{DecisionId, ProjectId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 决策记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Decision {
|
||||
pub id: DecisionId,
|
||||
pub project_id: ProjectId,
|
||||
/// 决策背景
|
||||
pub context: String,
|
||||
/// 需要决定的问题
|
||||
pub question: String,
|
||||
/// 考虑过的方案
|
||||
pub alternatives: Vec<String>,
|
||||
/// 最终决定
|
||||
pub decision: String,
|
||||
/// 决策原因
|
||||
pub reason: String,
|
||||
/// 决策者
|
||||
pub decided_by: DecidedBy,
|
||||
/// 关联实体类型
|
||||
pub entity_type: Option<String>,
|
||||
/// 关联实体 ID
|
||||
pub entity_id: Option<String>,
|
||||
/// 影响范围
|
||||
pub impact: Option<String>,
|
||||
/// 所处阶段
|
||||
pub stage: Option<String>,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
/// 决策者
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DecidedBy {
|
||||
/// AI 自动决策
|
||||
Ai,
|
||||
/// 人工决策
|
||||
Human,
|
||||
/// AI 建议后人工确认
|
||||
AiSuggested,
|
||||
}
|
||||
|
||||
/// 决策日志
|
||||
pub struct DecisionJournal;
|
||||
|
||||
impl DecisionJournal {
|
||||
/// 记录新决策
|
||||
pub fn record(
|
||||
project_id: ProjectId,
|
||||
context: String,
|
||||
question: String,
|
||||
alternatives: Vec<String>,
|
||||
decision: String,
|
||||
reason: String,
|
||||
decided_by: DecidedBy,
|
||||
) -> Decision {
|
||||
Decision {
|
||||
id: df_core::types::new_id(),
|
||||
project_id,
|
||||
context,
|
||||
question,
|
||||
alternatives,
|
||||
decision,
|
||||
reason,
|
||||
decided_by,
|
||||
entity_type: None,
|
||||
entity_id: None,
|
||||
impact: None,
|
||||
stage: None,
|
||||
created_at: chrono::Utc::now().timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询项目的决策历史
|
||||
pub fn query_by_project(_project_id: &ProjectId) -> Vec<Decision> {
|
||||
// TODO: SQLite 查询
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// 按阶段筛选
|
||||
pub fn filter_by_stage<'a>(decisions: &'a [Decision], stage: &str) -> Vec<&'a Decision> {
|
||||
decisions.iter().filter(|d| d.stage.as_deref() == Some(stage)).collect()
|
||||
}
|
||||
|
||||
/// 生成决策时间线
|
||||
///
|
||||
/// 按时间顺序展示项目的所有决策,用于复盘和审计
|
||||
pub fn timeline(_project_id: &ProjectId) -> Vec<Decision> {
|
||||
// TODO: 查询并按 created_at 排序
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
9
crates/df-traceability/src/lib.rs
Normal file
9
crates/df-traceability/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! 可追溯性引擎:标注系统、决策留痕、需求-测试映射
|
||||
|
||||
pub mod annotation;
|
||||
pub mod decision;
|
||||
pub mod traceability;
|
||||
|
||||
pub use annotation::{Annotation, AnnotationMarker, AnnotationStatus};
|
||||
pub use decision::{Decision, DecisionJournal};
|
||||
pub use traceability::TraceabilityMatrix;
|
||||
78
crates/df-traceability/src/traceability.rs
Normal file
78
crates/df-traceability/src/traceability.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! 需求-功能-测试可追溯矩阵
|
||||
//!
|
||||
//! 建立需求 → 功能 → 测试用例 → 测试报告 的双向追溯关系
|
||||
|
||||
use df_core::types::ProjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 追溯关系
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TraceLink {
|
||||
/// 上游实体类型 (Requirement / Feature)
|
||||
pub source_type: String,
|
||||
pub source_id: String,
|
||||
/// 下游实体类型 (Feature / TestCase / TestRun)
|
||||
pub target_type: String,
|
||||
pub target_id: String,
|
||||
}
|
||||
|
||||
/// 覆盖率统计
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CoverageReport {
|
||||
/// 功能总数
|
||||
pub total_features: usize,
|
||||
/// 已选中功能数
|
||||
pub selected_features: usize,
|
||||
/// 有测试用例的功能数
|
||||
pub features_with_tests: usize,
|
||||
/// 测试覆盖率百分比
|
||||
pub coverage_percent: f32,
|
||||
/// 测试通过率
|
||||
pub pass_rate: f32,
|
||||
}
|
||||
|
||||
/// 可追溯矩阵
|
||||
pub struct TraceabilityMatrix;
|
||||
|
||||
impl TraceabilityMatrix {
|
||||
/// 创建追溯关系
|
||||
pub fn link(
|
||||
source_type: &str,
|
||||
source_id: &str,
|
||||
target_type: &str,
|
||||
target_id: &str,
|
||||
) -> TraceLink {
|
||||
TraceLink {
|
||||
source_type: source_type.to_string(),
|
||||
source_id: source_id.to_string(),
|
||||
target_type: target_type.to_string(),
|
||||
target_id: target_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询功能的完整追溯链
|
||||
///
|
||||
/// Feature → [TestCases] → [TestRuns] → [TestReports]
|
||||
pub fn trace_feature(_feature_id: &str) -> Vec<TraceLink> {
|
||||
// TODO: SQLite 查询完整链路
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// 生成覆盖率报告
|
||||
pub fn coverage_report(_project_id: &ProjectId) -> CoverageReport {
|
||||
// TODO: 统计功能-测试用例覆盖情况
|
||||
CoverageReport {
|
||||
total_features: 0,
|
||||
selected_features: 0,
|
||||
features_with_tests: 0,
|
||||
coverage_percent: 0.0,
|
||||
pass_rate: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找未覆盖的功能(有功能但无测试用例)
|
||||
pub fn uncovered_features(_project_id: &ProjectId) -> Vec<String> {
|
||||
// TODO: 找出没有关联测试用例的功能
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
15
crates/df-workflow/Cargo.toml
Normal file
15
crates/df-workflow/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "df-workflow"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
futures = "0.3"
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
33
crates/df-workflow/src/conditions.rs
Normal file
33
crates/df-workflow/src/conditions.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! 条件表达式引擎 — 用于 DAG 边上的条件判断
|
||||
//!
|
||||
//! TODO: 实现完整的条件表达式解析与求值
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// 条件表达式求值器
|
||||
pub struct ConditionEngine;
|
||||
|
||||
impl ConditionEngine {
|
||||
/// 求值条件表达式
|
||||
///
|
||||
/// TODO: 实现完整的表达式解析,当前仅支持简单的 JSON Path 比较
|
||||
pub fn evaluate(expr: &str, _context: &Value) -> anyhow::Result<bool> {
|
||||
// 骨架:简单的 true/false 字面量
|
||||
let trimmed = expr.trim();
|
||||
if trimmed == "true" {
|
||||
return Ok(true);
|
||||
}
|
||||
if trimmed == "false" {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// TODO: 实现如下语法:
|
||||
// - "$.status == 'completed'" — JSON Path 比较
|
||||
// - "$.count > 10" — 数值比较
|
||||
// - "$.tags contains 'ai'" — 包含检查
|
||||
// - "and/or/not" — 逻辑组合
|
||||
|
||||
tracing::warn!("条件表达式引擎尚未完整实现,表达式: {}", expr);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
141
crates/df-workflow/src/dag.rs
Normal file
141
crates/df-workflow/src/dag.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! DAG(有向无环图)定义与拓扑排序
|
||||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use df_core::error::Result;
|
||||
use df_core::types::NodeId;
|
||||
|
||||
use crate::node::Node;
|
||||
|
||||
/// 图的边:从 source 到 target
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Edge {
|
||||
pub source: NodeId,
|
||||
pub target: NodeId,
|
||||
/// 边的条件表达式(可选)
|
||||
pub condition: Option<String>,
|
||||
}
|
||||
|
||||
/// DAG 结构
|
||||
pub struct Dag {
|
||||
/// 节点集合
|
||||
pub nodes: HashMap<NodeId, Box<dyn Node>>,
|
||||
/// 边集合
|
||||
pub edges: Vec<Edge>,
|
||||
}
|
||||
|
||||
impl Dag {
|
||||
/// 创建空 DAG
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
edges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加节点
|
||||
pub fn add_node(&mut self, id: NodeId, node: Box<dyn Node>) {
|
||||
self.nodes.insert(id, node);
|
||||
}
|
||||
|
||||
/// 添加边
|
||||
pub fn add_edge(&mut self, source: NodeId, target: NodeId) {
|
||||
self.edges.push(Edge {
|
||||
source,
|
||||
target,
|
||||
condition: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// 添加带条件的边
|
||||
pub fn add_edge_with_condition(
|
||||
&mut self,
|
||||
source: NodeId,
|
||||
target: NodeId,
|
||||
condition: String,
|
||||
) {
|
||||
self.edges.push(Edge {
|
||||
source,
|
||||
target,
|
||||
condition: Some(condition),
|
||||
});
|
||||
}
|
||||
|
||||
/// 获取指定节点的所有上游节点 ID
|
||||
pub fn predecessors(&self, node_id: &NodeId) -> Vec<NodeId> {
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|e| &e.target == node_id)
|
||||
.map(|e| e.source.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 获取指定节点的所有下游节点 ID
|
||||
pub fn successors(&self, node_id: &NodeId) -> Vec<NodeId> {
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|e| &e.source == node_id)
|
||||
.map(|e| e.target.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 拓扑排序 — 返回按执行顺序排列的节点 ID 层级
|
||||
///
|
||||
/// 返回 Vec<Vec<NodeId>>,每层内的节点可以并行执行
|
||||
pub fn topological_layers(&self) -> Result<Vec<Vec<NodeId>>> {
|
||||
let node_ids: HashSet<NodeId> = self.nodes.keys().cloned().collect();
|
||||
let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
|
||||
|
||||
// 初始化入度
|
||||
for id in &node_ids {
|
||||
in_degree.insert(id.clone(), 0);
|
||||
}
|
||||
for edge in &self.edges {
|
||||
if node_ids.contains(&edge.source) && node_ids.contains(&edge.target) {
|
||||
*in_degree.entry(edge.target.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// BFS 分层
|
||||
let mut layers = Vec::new();
|
||||
let mut queue: VecDeque<NodeId> = in_degree
|
||||
.iter()
|
||||
.filter(|(_, °)| deg == 0)
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
|
||||
let mut processed = 0usize;
|
||||
while !queue.is_empty() {
|
||||
let mut layer: Vec<NodeId> = Vec::new();
|
||||
let layer_size = queue.len();
|
||||
for _ in 0..layer_size {
|
||||
if let Some(id) = queue.pop_front() {
|
||||
layer.push(id.clone());
|
||||
for succ in self.successors(&id) {
|
||||
let deg = in_degree.get_mut(&succ).unwrap();
|
||||
*deg -= 1;
|
||||
if *deg == 0 {
|
||||
queue.push_back(succ);
|
||||
}
|
||||
}
|
||||
processed += 1;
|
||||
}
|
||||
}
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
if processed != node_ids.len() {
|
||||
return Err(df_core::error::Error::Workflow(
|
||||
"DAG 中存在环,无法进行拓扑排序".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(layers)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Dag {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
104
crates/df-workflow/src/dag_def.rs
Normal file
104
crates/df-workflow/src/dag_def.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
//! DAG 定义 — 可序列化/反序列化,用于持久化和模板
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::dag::Dag;
|
||||
|
||||
/// DAG 定义 — 可序列化/反序列化,用于持久化和模板
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DagDef {
|
||||
pub nodes: HashMap<String, NodeDef>,
|
||||
pub edges: Vec<EdgeDef>,
|
||||
}
|
||||
|
||||
/// 节点定义
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct NodeDef {
|
||||
pub id: String,
|
||||
pub node_type: String,
|
||||
pub config: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// 边定义
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EdgeDef {
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub condition: Option<String>,
|
||||
}
|
||||
|
||||
impl DagDef {
|
||||
/// 创建空的 DAG 定义
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
edges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加节点定义
|
||||
pub fn add_node(&mut self, id: impl Into<String>, node_type: impl Into<String>, config: serde_json::Value) {
|
||||
let id = id.into();
|
||||
self.nodes.insert(id.clone(), NodeDef {
|
||||
id,
|
||||
node_type: node_type.into(),
|
||||
config,
|
||||
label: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// 添加边定义
|
||||
pub fn add_edge(&mut self, source: impl Into<String>, target: impl Into<String>) {
|
||||
self.edges.push(EdgeDef {
|
||||
source: source.into(),
|
||||
target: target.into(),
|
||||
condition: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// 添加带条件的边定义
|
||||
pub fn add_edge_with_condition(
|
||||
&mut self,
|
||||
source: impl Into<String>,
|
||||
target: impl Into<String>,
|
||||
condition: impl Into<String>,
|
||||
) {
|
||||
self.edges.push(EdgeDef {
|
||||
source: source.into(),
|
||||
target: target.into(),
|
||||
condition: Some(condition.into()),
|
||||
});
|
||||
}
|
||||
|
||||
/// 从已有的运行时 Dag 提取定义(忽略节点实例,仅保留元数据)
|
||||
///
|
||||
/// 注意:此方法只能提取边的定义,节点配置无法从 trait object 反推
|
||||
pub fn from_dag_edges(dag: &Dag) -> Self {
|
||||
let edges: Vec<EdgeDef> = dag.edges.iter().map(|e| EdgeDef {
|
||||
source: e.source.clone(),
|
||||
target: e.target.clone(),
|
||||
condition: e.condition.clone(),
|
||||
}).collect();
|
||||
|
||||
let nodes: HashMap<String, NodeDef> = dag.nodes.iter().map(|(id, node)| {
|
||||
(id.clone(), NodeDef {
|
||||
id: id.clone(),
|
||||
node_type: node.node_type().to_string(),
|
||||
config: serde_json::Value::Null,
|
||||
label: None,
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Self { nodes, edges }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DagDef {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
68
crates/df-workflow/src/eventbus.rs
Normal file
68
crates/df-workflow/src/eventbus.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! 事件总线 — 基于 tokio::sync::broadcast 的发布/订阅
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::SendError;
|
||||
use df_core::events::{WorkflowEvent, HumanApprovalResponse};
|
||||
|
||||
/// 事件总线
|
||||
#[derive(Debug)]
|
||||
pub struct EventBus {
|
||||
sender: broadcast::Sender<WorkflowEvent>,
|
||||
}
|
||||
|
||||
/// 事件订阅者
|
||||
pub type EventSubscriber = broadcast::Receiver<WorkflowEvent>;
|
||||
|
||||
/// 默认事件通道容量
|
||||
const DEFAULT_CAPACITY: usize = 256;
|
||||
|
||||
impl EventBus {
|
||||
/// 创建事件总线
|
||||
pub fn new() -> Self {
|
||||
let (sender, _) = broadcast::channel(DEFAULT_CAPACITY);
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
/// 创建指定容量的事件总线
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
let (sender, _) = broadcast::channel(capacity);
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
/// 发送事件
|
||||
pub async fn send(&self, event: WorkflowEvent) {
|
||||
// broadcast::send 是同步的,忽略接收者已关闭的错误
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
|
||||
/// 订阅事件
|
||||
pub fn subscribe(&self) -> EventSubscriber {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
|
||||
/// 发送人工审批请求
|
||||
pub fn emit_human_approval_request(&self, event: WorkflowEvent) -> Result<usize, SendError<WorkflowEvent>> {
|
||||
self.sender.send(event)
|
||||
}
|
||||
|
||||
/// 尝试获取人工审批响应(简化版本,实际需要实现状态存储)
|
||||
pub fn try_recv_human_approval(&self, _execution_id: &str, _node_id: &str) -> Option<HumanApprovalResponse> {
|
||||
// TODO: 实现审批响应的存储和检索
|
||||
// 目前返回 None,需要配合前端实现
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventBus {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for EventBus {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
sender: self.sender.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
235
crates/df-workflow/src/executor.rs
Normal file
235
crates/df-workflow/src/executor.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
//! DAG 执行器 — 按拓扑层级调度执行
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::events::WorkflowEvent;
|
||||
use df_core::types::NodeId;
|
||||
|
||||
use crate::dag::Dag;
|
||||
use crate::eventbus::EventBus;
|
||||
use crate::node::{NodeContext, NodeOutput};
|
||||
use crate::state::StateMachine;
|
||||
|
||||
/// DAG 执行器
|
||||
pub struct DagExecutor {
|
||||
/// 事件总线
|
||||
event_bus: EventBus,
|
||||
/// 节点状态机
|
||||
state_machine: StateMachine,
|
||||
}
|
||||
|
||||
impl DagExecutor {
|
||||
/// 创建执行器
|
||||
pub fn new(event_bus: EventBus) -> Self {
|
||||
Self {
|
||||
event_bus,
|
||||
state_machine: StateMachine::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行 DAG 工作流
|
||||
///
|
||||
/// 同一拓扑层内的节点并发执行,层与层之间串行;
|
||||
/// 本层全部节点完成后统一更新状态,任一失败则中止后续层。
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
dag: &Dag,
|
||||
initial_config: serde_json::Value,
|
||||
) -> anyhow::Result<HashMap<NodeId, NodeOutput>> {
|
||||
let layers = dag.topological_layers()?;
|
||||
let mut outputs: HashMap<NodeId, NodeOutput> = HashMap::new();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
tracing::info!("DAG 执行开始,共 {} 层", layers.len());
|
||||
|
||||
for (layer_idx, layer) in layers.iter().enumerate() {
|
||||
tracing::info!("执行第 {} 层,共 {} 个节点", layer_idx, layer.len());
|
||||
|
||||
// 阶段一:逐节点发 NodeStarted、置为运行中,并构建执行 future
|
||||
let mut node_futures = Vec::with_capacity(layer.len());
|
||||
for node_id in layer {
|
||||
let node = dag.nodes.get(node_id).ok_or_else(|| {
|
||||
anyhow::anyhow!("节点 {} 不存在于 DAG 中", node_id)
|
||||
})?;
|
||||
|
||||
// 发送 NodeStarted 事件
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::NodeStarted {
|
||||
node_id: node_id.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
self.state_machine.set_running(node_id.clone())?;
|
||||
|
||||
// 构建节点上下文
|
||||
let mut inputs = HashMap::new();
|
||||
for pred_id in dag.predecessors(node_id) {
|
||||
if let Some(out) = outputs.get(&pred_id) {
|
||||
inputs.insert(pred_id, out.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let ctx = NodeContext {
|
||||
node_id: node_id.clone(),
|
||||
inputs,
|
||||
config: initial_config.clone(),
|
||||
execution_id: "dummy-execution-id".to_string(), // TODO: 应该从 workflow_execution 获取
|
||||
event_bus: self.event_bus.clone(),
|
||||
node_status: StateMachine::new(),
|
||||
};
|
||||
|
||||
// 仅捕获节点共享引用与所有权上下文,避免与 self 借用冲突
|
||||
let id = node_id.clone();
|
||||
node_futures.push(async move {
|
||||
let node_start = std::time::Instant::now();
|
||||
let result = node.execute(ctx).await;
|
||||
(id, result, node_start.elapsed().as_millis() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
// 阶段二:同层节点并发执行,等待全部完成
|
||||
let results = futures::future::join_all(node_futures).await;
|
||||
|
||||
// 阶段三:统一更新状态并发送事件,任一失败则整体返回 Err
|
||||
let mut first_err: Option<anyhow::Error> = None;
|
||||
for (node_id, result, duration) in results {
|
||||
match result {
|
||||
Ok(output) => {
|
||||
self.state_machine.set_completed(node_id.clone())?;
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::NodeCompleted {
|
||||
node_id: node_id.clone(),
|
||||
duration_ms: duration,
|
||||
})
|
||||
.await;
|
||||
outputs.insert(node_id, output);
|
||||
}
|
||||
Err(e) => {
|
||||
self.state_machine.set_failed(node_id.clone())?;
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::NodeFailed {
|
||||
node_id: node_id.clone(),
|
||||
error: e.to_string(),
|
||||
})
|
||||
.await;
|
||||
// 同层多个失败时只报告第一个
|
||||
if first_err.is_none() {
|
||||
first_err = Some(e.context(format!("节点 {} 执行失败", node_id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(e) = first_err {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
let total = start.elapsed().as_millis() as u64;
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::WorkflowCompleted {
|
||||
total_duration_ms: total,
|
||||
})
|
||||
.await;
|
||||
|
||||
tracing::info!("DAG 执行完成,耗时 {}ms", total);
|
||||
Ok(outputs)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::node::{Node, NodeResult, NodeSchema};
|
||||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
|
||||
/// 测试节点:sleep 指定毫秒后返回空输出
|
||||
struct SleepNode {
|
||||
sleep_ms: u64,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Node for SleepNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
tokio::time::sleep(Duration::from_millis(self.sleep_ms)).await;
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::Value::Null,
|
||||
output: serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"sleep"
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试节点:直接返回错误
|
||||
struct FailNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for FailNode {
|
||||
async fn execute(&self, _ctx: NodeContext) -> NodeResult {
|
||||
Err(anyhow::anyhow!("故意失败"))
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::Value::Null,
|
||||
output: serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"fail"
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_same_layer_runs_in_parallel() {
|
||||
// 两个无依赖节点位于同一层,各 sleep 100ms
|
||||
let mut dag = Dag::new();
|
||||
dag.add_node("a".to_string(), Box::new(SleepNode { sleep_ms: 100 }));
|
||||
dag.add_node("b".to_string(), Box::new(SleepNode { sleep_ms: 100 }));
|
||||
|
||||
let mut executor = DagExecutor::new(EventBus::new());
|
||||
let start = std::time::Instant::now();
|
||||
let outputs = executor.run(&dag, serde_json::Value::Null).await.unwrap();
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
assert_eq!(outputs.len(), 2);
|
||||
// 串行需要约 200ms,并行应明显小于 180ms
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(180),
|
||||
"同层节点应并行执行,实际耗时 {:?}",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layer_failure_aborts_following_layers() {
|
||||
// a(失败) 与 b(成功) 同层,c 依赖 a,失败后 c 不应执行
|
||||
let mut dag = Dag::new();
|
||||
dag.add_node("a".to_string(), Box::new(FailNode));
|
||||
dag.add_node("b".to_string(), Box::new(SleepNode { sleep_ms: 10 }));
|
||||
dag.add_node("c".to_string(), Box::new(SleepNode { sleep_ms: 10 }));
|
||||
dag.add_edge("a".to_string(), "c".to_string());
|
||||
|
||||
let mut executor = DagExecutor::new(EventBus::new());
|
||||
let err = executor
|
||||
.run(&dag, serde_json::Value::Null)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("节点 a 执行失败"));
|
||||
|
||||
// 同层成功节点状态正常更新,下游节点保持 Pending
|
||||
use df_core::types::NodeStatus;
|
||||
assert_eq!(executor.state_machine.get(&"a".to_string()), NodeStatus::Failed);
|
||||
assert_eq!(executor.state_machine.get(&"b".to_string()), NodeStatus::Completed);
|
||||
assert_eq!(executor.state_machine.get(&"c".to_string()), NodeStatus::Pending);
|
||||
}
|
||||
}
|
||||
10
crates/df-workflow/src/lib.rs
Normal file
10
crates/df-workflow/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! df-workflow: 工作流引擎 — DAG 定义、节点抽象、执行器、状态机、事件总线
|
||||
|
||||
pub mod conditions;
|
||||
pub mod dag;
|
||||
pub mod dag_def;
|
||||
pub mod eventbus;
|
||||
pub mod executor;
|
||||
pub mod node;
|
||||
pub mod registry;
|
||||
pub mod state;
|
||||
83
crates/df-workflow/src/node.rs
Normal file
83
crates/df-workflow/src/node.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! 节点抽象:Node trait、NodeContext、NodeOutput
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_core::types::NodeId;
|
||||
|
||||
use super::eventbus::EventBus;
|
||||
use super::state::StateMachine;
|
||||
|
||||
/// 节点输入/输出上下文
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeContext {
|
||||
/// 节点 ID
|
||||
pub node_id: NodeId,
|
||||
/// 上游节点输出(key 为上游节点 ID)
|
||||
pub inputs: std::collections::HashMap<String, NodeOutput>,
|
||||
/// 节点配置参数
|
||||
pub config: serde_json::Value,
|
||||
/// 工作流执行 ID
|
||||
pub execution_id: String,
|
||||
/// 事件总线
|
||||
pub event_bus: EventBus,
|
||||
/// 节点状态机(用于检查取消状态)
|
||||
pub node_status: StateMachine,
|
||||
}
|
||||
|
||||
/// 节点执行输出
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeOutput {
|
||||
/// 输出数据
|
||||
pub data: serde_json::Value,
|
||||
/// 输出元数据
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl NodeOutput {
|
||||
/// 创建空输出
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
data: serde_json::Value::Null,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 JSON 值创建输出
|
||||
pub fn from_value(data: serde_json::Value) -> Self {
|
||||
Self {
|
||||
data,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 节点执行结果
|
||||
pub type NodeResult = anyhow::Result<NodeOutput>;
|
||||
|
||||
/// 节点参数 Schema(JSON Schema 格式)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeSchema {
|
||||
/// 参数 JSON Schema
|
||||
pub params: serde_json::Value,
|
||||
/// 输出 JSON Schema
|
||||
pub output: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 节点 trait — 所有工作流节点必须实现
|
||||
#[async_trait]
|
||||
pub trait Node: Send + Sync {
|
||||
/// 执行节点逻辑
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult;
|
||||
|
||||
/// 返回节点的参数 Schema
|
||||
fn schema(&self) -> NodeSchema;
|
||||
|
||||
/// 该节点是否为阻塞节点(阻塞工作流,等待外部输入)
|
||||
fn is_blocking(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// 节点类型名称
|
||||
fn node_type(&self) -> &str;
|
||||
}
|
||||
87
crates/df-workflow/src/registry.rs
Normal file
87
crates/df-workflow/src/registry.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! 节点注册表 — 根据 node_type 字符串创建节点实例
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::dag::Dag;
|
||||
use crate::dag_def::DagDef;
|
||||
use crate::node::Node;
|
||||
|
||||
/// 节点工厂函数类型
|
||||
type NodeFactory = Box<dyn Fn(&serde_json::Value) -> Box<dyn Node> + Send + Sync>;
|
||||
|
||||
/// 节点注册表 — 根据 node_type 字符串创建节点实例
|
||||
pub struct NodeRegistry {
|
||||
factories: HashMap<String, NodeFactory>,
|
||||
}
|
||||
|
||||
impl NodeRegistry {
|
||||
/// 创建空的注册表
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
factories: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册一个节点工厂
|
||||
pub fn register<F>(&mut self, type_name: &str, factory: F)
|
||||
where
|
||||
F: Fn(&serde_json::Value) -> Box<dyn Node> + Send + Sync + 'static,
|
||||
{
|
||||
self.factories.insert(type_name.to_string(), Box::new(factory));
|
||||
}
|
||||
|
||||
/// 根据 node_type 创建节点实例
|
||||
pub fn create(&self, type_name: &str, config: &serde_json::Value) -> anyhow::Result<Box<dyn Node>> {
|
||||
self.factories
|
||||
.get(type_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("未注册的节点类型: {}", type_name))
|
||||
.map(|factory| factory(config))
|
||||
}
|
||||
|
||||
/// 从 DagDef 构建完整的运行时 Dag
|
||||
pub fn build_dag(&self, def: &DagDef) -> anyhow::Result<Dag> {
|
||||
let mut dag = Dag::new();
|
||||
|
||||
// 创建所有节点实例
|
||||
for (id, node_def) in &def.nodes {
|
||||
let node = self.create(&node_def.node_type, &node_def.config)?;
|
||||
dag.add_node(id.clone(), node);
|
||||
}
|
||||
|
||||
// 添加所有边
|
||||
for edge_def in &def.edges {
|
||||
match &edge_def.condition {
|
||||
Some(cond) => dag.add_edge_with_condition(
|
||||
edge_def.source.clone(),
|
||||
edge_def.target.clone(),
|
||||
cond.clone(),
|
||||
),
|
||||
None => dag.add_edge(edge_def.source.clone(), edge_def.target.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dag)
|
||||
}
|
||||
|
||||
/// 检查是否已注册指定类型
|
||||
pub fn is_registered(&self, type_name: &str) -> bool {
|
||||
self.factories.contains_key(type_name)
|
||||
}
|
||||
|
||||
/// 列出所有已注册的节点类型
|
||||
pub fn registered_types(&self) -> Vec<&str> {
|
||||
self.factories.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NodeRegistry {
|
||||
fn default() -> Self {
|
||||
let mut registry = Self::new();
|
||||
registry.register("script", |_config| {
|
||||
// ScriptNode 的工厂 — 需要 df-nodes 依赖后才可用
|
||||
// 这里返回一个占位实现,实际项目中由 df-nodes crate 注册
|
||||
unimplemented!("ScriptNode 需要通过 df-nodes 注册")
|
||||
});
|
||||
registry
|
||||
}
|
||||
}
|
||||
147
crates/df-workflow/src/state.rs
Normal file
147
crates/df-workflow/src/state.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! 节点状态机 — 管理节点的状态转换
|
||||
//!
|
||||
//! 合法转换:Pending → Running,Running → Completed / Failed,
|
||||
//! 非法转换返回错误,避免状态被随意覆盖。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::{NodeId, NodeStatus};
|
||||
|
||||
/// 节点状态机
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StateMachine {
|
||||
/// 节点状态映射
|
||||
states: HashMap<NodeId, NodeStatus>,
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
/// 创建状态机
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取节点状态
|
||||
pub fn get(&self, node_id: &NodeId) -> NodeStatus {
|
||||
self.states
|
||||
.get(node_id)
|
||||
.cloned()
|
||||
.unwrap_or(NodeStatus::Pending)
|
||||
}
|
||||
|
||||
/// 判断状态转换是否合法
|
||||
fn is_legal(from: &NodeStatus, to: &NodeStatus) -> bool {
|
||||
matches!(
|
||||
(from, to),
|
||||
(NodeStatus::Pending, NodeStatus::Running)
|
||||
| (NodeStatus::Running, NodeStatus::Completed)
|
||||
| (NodeStatus::Running, NodeStatus::Failed)
|
||||
)
|
||||
}
|
||||
|
||||
/// 状态转换 — 校验合法性后更新,非法转换返回错误
|
||||
pub fn transition(&mut self, node_id: NodeId, target: NodeStatus) -> anyhow::Result<()> {
|
||||
let current = self.get(&node_id);
|
||||
if !Self::is_legal(¤t, &target) {
|
||||
anyhow::bail!(
|
||||
"节点 {} 状态转换非法:{} -> {}",
|
||||
node_id,
|
||||
current.as_str(),
|
||||
target.as_str()
|
||||
);
|
||||
}
|
||||
self.states.insert(node_id, target);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 设置节点为运行中(仅允许 Pending -> Running)
|
||||
pub fn set_running(&mut self, node_id: NodeId) -> anyhow::Result<()> {
|
||||
self.transition(node_id, NodeStatus::Running)
|
||||
}
|
||||
|
||||
/// 设置节点为已完成(仅允许 Running -> Completed)
|
||||
pub fn set_completed(&mut self, node_id: NodeId) -> anyhow::Result<()> {
|
||||
self.transition(node_id, NodeStatus::Completed)
|
||||
}
|
||||
|
||||
/// 设置节点为失败(仅允许 Running -> Failed)
|
||||
pub fn set_failed(&mut self, node_id: NodeId) -> anyhow::Result<()> {
|
||||
self.transition(node_id, NodeStatus::Failed)
|
||||
}
|
||||
|
||||
/// 设置节点为等待中(暂未纳入转换校验)
|
||||
pub fn set_waiting(&mut self, node_id: NodeId) {
|
||||
self.states.insert(node_id, NodeStatus::Waiting);
|
||||
}
|
||||
|
||||
/// 设置节点为已跳过(暂未纳入转换校验)
|
||||
pub fn set_skipped(&mut self, node_id: NodeId) {
|
||||
self.states.insert(node_id, NodeStatus::Skipped);
|
||||
}
|
||||
|
||||
/// 获取所有状态快照
|
||||
pub fn snapshot(&self) -> &HashMap<NodeId, NodeStatus> {
|
||||
&self.states
|
||||
}
|
||||
|
||||
/// 检查节点是否被取消
|
||||
pub fn is_cancelled(&self, node_id: &NodeId) -> bool {
|
||||
matches!(self.get(node_id), NodeStatus::Cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StateMachine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_legal_transitions() {
|
||||
let mut sm = StateMachine::new();
|
||||
let id = "node-a".to_string();
|
||||
|
||||
// Pending -> Running -> Completed 全程合法
|
||||
assert!(sm.set_running(id.clone()).is_ok());
|
||||
assert_eq!(sm.get(&id), NodeStatus::Running);
|
||||
assert!(sm.set_completed(id.clone()).is_ok());
|
||||
assert_eq!(sm.get(&id), NodeStatus::Completed);
|
||||
|
||||
// Running -> Failed 合法
|
||||
let id2 = "node-b".to_string();
|
||||
sm.set_running(id2.clone()).unwrap();
|
||||
assert!(sm.set_failed(id2.clone()).is_ok());
|
||||
assert_eq!(sm.get(&id2), NodeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_illegal_transitions_rejected() {
|
||||
let mut sm = StateMachine::new();
|
||||
let id = "node-a".to_string();
|
||||
|
||||
// Pending -> Completed 非法
|
||||
let err = sm.set_completed(id.clone()).unwrap_err();
|
||||
assert!(err.to_string().contains("状态转换非法"));
|
||||
assert!(err.to_string().contains("node-a"));
|
||||
// 状态保持不变
|
||||
assert_eq!(sm.get(&id), NodeStatus::Pending);
|
||||
|
||||
// Pending -> Failed 非法
|
||||
assert!(sm.set_failed(id.clone()).is_err());
|
||||
|
||||
// Completed 为终态,不允许再转 Running
|
||||
sm.set_running(id.clone()).unwrap();
|
||||
sm.set_completed(id.clone()).unwrap();
|
||||
assert!(sm.set_running(id.clone()).is_err());
|
||||
|
||||
// Running -> Running 重复置位非法
|
||||
let id2 = "node-b".to_string();
|
||||
sm.set_running(id2.clone()).unwrap();
|
||||
assert!(sm.set_running(id2.clone()).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user