新增: 初始化 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:
1421
src-tauri/src/commands/ai.rs
Normal file
1421
src-tauri/src/commands/ai.rs
Normal file
File diff suppressed because it is too large
Load Diff
85
src-tauri/src/commands/idea.rs
Normal file
85
src-tauri/src/commands/idea.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! 想法相关命令
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use df_core::types::new_id;
|
||||
use df_storage::models::IdeaRecord;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::now_millis;
|
||||
|
||||
/// 创建想法入参
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateIdeaInput {
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default = "default_priority")]
|
||||
pub priority: i32,
|
||||
/// 标签 JSON 数组字符串
|
||||
pub tags: Option<String>,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
fn default_priority() -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// 列出全部想法
|
||||
#[tauri::command]
|
||||
pub async fn list_ideas(state: State<'_, AppState>) -> Result<Vec<IdeaRecord>, String> {
|
||||
state.ideas.list_all().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 创建想法,返回完整记录
|
||||
#[tauri::command]
|
||||
pub async fn create_idea(
|
||||
state: State<'_, AppState>,
|
||||
input: CreateIdeaInput,
|
||||
) -> Result<IdeaRecord, String> {
|
||||
let now = now_millis();
|
||||
let record = IdeaRecord {
|
||||
id: new_id(),
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
status: "draft".to_string(),
|
||||
priority: input.priority,
|
||||
score: None,
|
||||
tags: input.tags,
|
||||
source: input.source,
|
||||
promoted_to: None,
|
||||
ai_analysis: None,
|
||||
scores: None,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
state
|
||||
.ideas
|
||||
.insert(record.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// 更新想法单个字段(字段名走 df-storage 白名单校验)
|
||||
#[tauri::command]
|
||||
pub async fn update_idea(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
field: String,
|
||||
value: String,
|
||||
) -> Result<bool, String> {
|
||||
state
|
||||
.ideas
|
||||
.update_field(&id, &field, &value)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 删除想法
|
||||
#[tauri::command]
|
||||
pub async fn delete_idea(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
state.ideas.delete(&id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
18
src-tauri/src/commands/mod.rs
Normal file
18
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! Tauri IPC 命令层 — 按业务域拆分模块
|
||||
//!
|
||||
//! 所有 command 统一返回 `Result<T, String>`,错误经 `to_string()` 传给前端。
|
||||
|
||||
pub mod ai;
|
||||
pub mod idea;
|
||||
pub mod project;
|
||||
pub mod task;
|
||||
pub mod workflow;
|
||||
|
||||
/// 当前时间戳(毫秒字符串)— 与 df-storage 内部的时间格式保持一致
|
||||
pub(crate) fn now_millis() -> String {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
}
|
||||
84
src-tauri/src/commands/project.rs
Normal file
84
src-tauri/src/commands/project.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! 项目相关命令
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use df_core::types::new_id;
|
||||
use df_storage::models::ProjectRecord;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::now_millis;
|
||||
|
||||
/// 创建项目入参
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateProjectInput {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
pub idea_id: Option<String>,
|
||||
}
|
||||
|
||||
/// 列出全部项目
|
||||
#[tauri::command]
|
||||
pub async fn list_projects(state: State<'_, AppState>) -> Result<Vec<ProjectRecord>, String> {
|
||||
state.projects.list_all().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 创建项目,返回完整记录
|
||||
#[tauri::command]
|
||||
pub async fn create_project(
|
||||
state: State<'_, AppState>,
|
||||
input: CreateProjectInput,
|
||||
) -> Result<ProjectRecord, String> {
|
||||
let now = now_millis();
|
||||
let record = ProjectRecord {
|
||||
id: new_id(),
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
status: "planning".to_string(),
|
||||
idea_id: input.idea_id,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
state
|
||||
.projects
|
||||
.insert(record.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// 按 ID 查询项目
|
||||
#[tauri::command]
|
||||
pub async fn get_project(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<Option<ProjectRecord>, String> {
|
||||
state
|
||||
.projects
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 更新项目单个字段(字段名走 df-storage 白名单校验)
|
||||
#[tauri::command]
|
||||
pub async fn update_project(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
field: String,
|
||||
value: String,
|
||||
) -> Result<bool, String> {
|
||||
state
|
||||
.projects
|
||||
.update_field(&id, &field, &value)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 删除项目
|
||||
#[tauri::command]
|
||||
pub async fn delete_project(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
state.projects.delete(&id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
91
src-tauri/src/commands/task.rs
Normal file
91
src-tauri/src/commands/task.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! 任务相关命令
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use df_core::types::new_id;
|
||||
use df_storage::models::TaskRecord;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::now_millis;
|
||||
|
||||
/// 创建任务入参
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateTaskInput {
|
||||
pub project_id: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default = "default_priority")]
|
||||
pub priority: i32,
|
||||
pub branch_name: Option<String>,
|
||||
pub assignee: Option<String>,
|
||||
}
|
||||
|
||||
fn default_priority() -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// 列出任务,可按 project_id 过滤
|
||||
#[tauri::command]
|
||||
pub async fn list_tasks(
|
||||
state: State<'_, AppState>,
|
||||
project_id: Option<String>,
|
||||
) -> Result<Vec<TaskRecord>, String> {
|
||||
let result = match project_id {
|
||||
Some(pid) => state.tasks.query("project_id", &pid).await,
|
||||
None => state.tasks.list_all().await,
|
||||
};
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 创建任务,返回完整记录
|
||||
#[tauri::command]
|
||||
pub async fn create_task(
|
||||
state: State<'_, AppState>,
|
||||
input: CreateTaskInput,
|
||||
) -> Result<TaskRecord, String> {
|
||||
let now = now_millis();
|
||||
let record = TaskRecord {
|
||||
id: new_id(),
|
||||
project_id: input.project_id,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
status: "todo".to_string(),
|
||||
priority: input.priority,
|
||||
branch_name: input.branch_name,
|
||||
assignee: input.assignee,
|
||||
workflow_def_id: None,
|
||||
base_branch: None,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
state
|
||||
.tasks
|
||||
.insert(record.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// 更新任务单个字段(字段名走 df-storage 白名单校验)
|
||||
#[tauri::command]
|
||||
pub async fn update_task(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
field: String,
|
||||
value: String,
|
||||
) -> Result<bool, String> {
|
||||
state
|
||||
.tasks
|
||||
.update_field(&id, &field, &value)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 删除任务
|
||||
#[tauri::command]
|
||||
pub async fn delete_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
state.tasks.delete(&id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
187
src-tauri/src/commands/workflow.rs
Normal file
187
src-tauri/src/commands/workflow.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
//! 工作流相关命令 — 触发执行、查询执行记录
|
||||
//!
|
||||
//! 事件转发:run_workflow 在执行前订阅事件总线,
|
||||
//! 将 WorkflowEvent 包装为 `{ execution_id, event }` 通过 `workflow-event` 事件发给前端。
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
|
||||
use df_core::events::{WorkflowEvent, HumanApprovalResponse};
|
||||
use df_core::types::new_id;
|
||||
use df_storage::crud::WorkflowRepo;
|
||||
use df_storage::models::WorkflowRecord;
|
||||
use df_workflow::dag_def::DagDef;
|
||||
use df_workflow::executor::DagExecutor;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::now_millis;
|
||||
|
||||
/// 转发到前端的事件载荷
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct WorkflowEventPayload {
|
||||
/// 工作流执行记录 ID
|
||||
execution_id: String,
|
||||
/// 原始工作流事件(serde tag = "type")
|
||||
event: WorkflowEvent,
|
||||
}
|
||||
|
||||
/// 触发工作流执行(核心命令)
|
||||
///
|
||||
/// 流程:build_dag 校验 → 写入执行记录(status=running) → 后台异步执行 →
|
||||
/// 事件经 EventBus 转发到前端 → 完成后更新执行记录状态。
|
||||
/// 立即返回执行记录 ID,前端凭此关联后续事件。
|
||||
#[tauri::command]
|
||||
pub async fn run_workflow(
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
name: String,
|
||||
dag: DagDef,
|
||||
config: serde_json::Value,
|
||||
) -> Result<String, String> {
|
||||
// 1. 先构建运行时 DAG,校验失败直接返回,不落库
|
||||
let runtime_dag = state.registry.build_dag(&dag).map_err(|e| e.to_string())?;
|
||||
|
||||
// 2. 写入执行记录(status=running)
|
||||
let execution_id = new_id();
|
||||
let dag_json = serde_json::to_string(&dag).map_err(|e| e.to_string())?;
|
||||
let record = WorkflowRecord {
|
||||
id: execution_id.clone(),
|
||||
name,
|
||||
dag_json,
|
||||
status: "running".to_string(),
|
||||
triggered_by: Some("manual".to_string()),
|
||||
project_id: None,
|
||||
task_id: None,
|
||||
created_at: now_millis(),
|
||||
completed_at: None,
|
||||
};
|
||||
state
|
||||
.workflows
|
||||
.insert(record)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 3. 订阅事件总线,把事件转发给前端(收到完成/失败事件后退出)
|
||||
let mut rx = state.event_bus.subscribe();
|
||||
let forward_app = app.clone();
|
||||
let forward_exec_id = execution_id.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
let finished = matches!(
|
||||
event,
|
||||
WorkflowEvent::WorkflowCompleted { .. }
|
||||
| WorkflowEvent::WorkflowFailed { .. }
|
||||
);
|
||||
let payload = WorkflowEventPayload {
|
||||
execution_id: forward_exec_id.clone(),
|
||||
event,
|
||||
};
|
||||
if let Err(e) = forward_app.emit("workflow-event", &payload) {
|
||||
tracing::warn!("工作流事件转发失败: {}", e);
|
||||
}
|
||||
if finished {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 消费过慢丢失部分事件时继续接收
|
||||
Err(RecvError::Lagged(n)) => {
|
||||
tracing::warn!("工作流事件消费滞后,丢失 {} 条", n);
|
||||
}
|
||||
Err(RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 后台异步执行 DAG,完成后更新执行记录状态
|
||||
let event_bus = state.event_bus.clone();
|
||||
let db = state.db.clone();
|
||||
let exec_id = execution_id.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut executor = DagExecutor::new(event_bus.clone());
|
||||
let result = executor.run(&runtime_dag, config).await;
|
||||
|
||||
let (status, error) = match &result {
|
||||
Ok(_) => ("completed", None),
|
||||
Err(e) => ("failed", Some(format!("{:#}", e))),
|
||||
};
|
||||
|
||||
// 更新执行记录(Repo 内部仅持有 Arc 连接,重建开销可忽略)
|
||||
let workflows = WorkflowRepo::new(&db);
|
||||
if let Err(e) = workflows.update_field(&exec_id, "status", status).await {
|
||||
tracing::error!("更新工作流状态失败: {}", e);
|
||||
}
|
||||
if let Err(e) = workflows
|
||||
.update_field(&exec_id, "completed_at", &now_millis())
|
||||
.await
|
||||
{
|
||||
tracing::error!("更新工作流完成时间失败: {}", e);
|
||||
}
|
||||
|
||||
// 执行失败时补发 WorkflowFailed(执行器内部只发 NodeFailed)
|
||||
if let Some(error) = error {
|
||||
event_bus
|
||||
.send(WorkflowEvent::WorkflowFailed {
|
||||
error,
|
||||
failed_node: String::new(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(execution_id)
|
||||
}
|
||||
|
||||
/// 列出全部工作流执行记录
|
||||
#[tauri::command]
|
||||
pub async fn list_workflow_executions(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<WorkflowRecord>, String> {
|
||||
state.workflows.list_all().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 按 ID 查询工作流执行记录
|
||||
#[tauri::command]
|
||||
pub async fn get_workflow_execution(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<Option<WorkflowRecord>, String> {
|
||||
state
|
||||
.workflows
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 发送人工审批响应
|
||||
#[tauri::command]
|
||||
pub async fn approve_human_approval(
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
execution_id: String,
|
||||
node_id: String,
|
||||
decision: String,
|
||||
comment: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// 发送审批响应到事件总线
|
||||
let response = HumanApprovalResponse {
|
||||
execution_id: execution_id.clone(),
|
||||
node_id: node_id.clone(),
|
||||
decision,
|
||||
comment,
|
||||
};
|
||||
|
||||
let event = WorkflowEvent::HumanApprovalResponse {
|
||||
execution_id: response.execution_id,
|
||||
node_id: response.node_id,
|
||||
decision: response.decision,
|
||||
comment: response.comment,
|
||||
};
|
||||
|
||||
// 直接发送到全局事件总线
|
||||
state.event_bus.send(event).await;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user