新增: 初始化 DevFlow 项目仓库

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

1421
src-tauri/src/commands/ai.rs Normal file

File diff suppressed because it is too large Load Diff

View 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())
}

View 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()
}

View 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())
}

View 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())
}

View 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(())
}