新增: 初始化 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:
3
src-tauri/.gitignore
vendored
Normal file
3
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Generated
|
||||
target/
|
||||
Cargo.lock
|
||||
31
src-tauri/Cargo.toml
Normal file
31
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "devflow"
|
||||
version = "0.1.0"
|
||||
description = "DevFlow Desktop App"
|
||||
authors = [""]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "devflow_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
# 后端 crate
|
||||
df-core = { path = "../crates/df-core" }
|
||||
df-storage = { path = "../crates/df-storage" }
|
||||
df-workflow = { path = "../crates/df-workflow" }
|
||||
df-nodes = { path = "../crates/df-nodes" }
|
||||
df-execute = { path = "../crates/df-execute" }
|
||||
df-ai = { path = "../crates/df-ai" }
|
||||
futures = "0.3"
|
||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
20
src-tauri/capabilities/default.json
Normal file
20
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"identifier": "default",
|
||||
"description": "DevFlow default permissions",
|
||||
"windows": ["main", "ai-detached"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-emit",
|
||||
"core:window:allow-create",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-position",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-outer-position",
|
||||
"core:window:allow-inner-size",
|
||||
"core:webview:allow-create-webview-window"
|
||||
]
|
||||
}
|
||||
89
src-tauri/docs/code-review-2025-06-11.md
Normal file
89
src-tauri/docs/code-review-2025-06-11.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# DevFlow 代码审查报告
|
||||
|
||||
**审查日期:** 2025-06-11
|
||||
**审查范围:** `src/` 目录全部 Rust 源码(6 个文件,约 1,700 行)
|
||||
**审查版本:** v0.1.0
|
||||
|
||||
---
|
||||
|
||||
## 整体评分
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 架构设计 | ⭐⭐⭐⭐ | 模块划分清晰,Tauri 命令层 + Repo 层 + 工作流引擎分层合理 |
|
||||
| 代码质量 | ⭐⭐⭐⭐ | 注释详尽,命名规范,错误处理一致 |
|
||||
| 安全性 | ⭐⭐⭐ | 有路径校验和风险分级,但存在改进空间 |
|
||||
| 健壮性 | ⭐⭐⭐ | 异步并发处理有隐患,部分边界情况未覆盖 |
|
||||
| 可维护性 | ⭐⭐⭐ | `ai.rs` 文件过大(44KB),职责过多 |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 严重问题(P0)
|
||||
|
||||
### #1 API Key 明文存储在数据库
|
||||
- **文件:** `src/commands/ai.rs` — `ai_save_provider`
|
||||
- **问题:** `api_key` 以明文存入 SQLite,任何能访问数据库文件的人都能读取
|
||||
- **建议:** 使用 OS 级密钥存储或 AES 加密后存储
|
||||
|
||||
### #2 工作流事件转发存在多执行实例串扰
|
||||
- **文件:** `src/commands/workflow.rs` — `run_workflow`
|
||||
- **问题:** EventBus 全局共享,并发工作流事件会串扰
|
||||
- **建议:** 为每次执行创建独立事件通道,或转发时按 execution_id 过滤
|
||||
|
||||
---
|
||||
|
||||
## 🟡 中等问题(P1)
|
||||
|
||||
### #3 ai_chat_send 锁释放竞态条件
|
||||
- **文件:** `src/commands/ai.rs` — `ai_chat_send`
|
||||
- **问题:** drop 锁后重新获取锁之间,并发请求可能覆盖 conversation_id
|
||||
- **建议:** 移到锁外预处理或使用 OnceLock
|
||||
|
||||
### #4 ai.rs 文件过大(1129 行 / 44KB)
|
||||
- **建议拆分为:** `chat.rs` / `agent.rs` / `tools.rs` / `prompt.rs`
|
||||
|
||||
### #5 AI 工具注册为占位实现
|
||||
- **文件:** `src/state.rs`
|
||||
- **问题:** 工具闭包全是占位,真正逻辑在 ai.rs 硬编码 match,两处定义易不同步
|
||||
- **建议:** 统一到 AiToolRegistry 或 trait 对象分发
|
||||
|
||||
### #6 缺少单元测试
|
||||
- **建议优先测试:** validate_path / execute_tool_on_repo / extract_title / replace_tool_result
|
||||
|
||||
---
|
||||
|
||||
## 🟢 轻微问题(P2)
|
||||
|
||||
### #7 now_millis() 隐藏时间异常
|
||||
- `unwrap_or_default()` 静默返回 0,建议加 tracing::warn
|
||||
|
||||
### #8 Cargo.toml 缺少 authors 和 license
|
||||
- 补充作者信息和开源许可证
|
||||
|
||||
### #9 .gitignore 忽略了 Cargo.lock
|
||||
- 二进制应用应提交 lock 文件
|
||||
|
||||
### #10 update_field 使用字符串字段名
|
||||
- 考虑使用 enum 定义可更新字段,编译时保证安全
|
||||
|
||||
### #11 validate_path 黑名单不够全面
|
||||
- 建议改用白名单模式(只允许工作目录下的路径)
|
||||
|
||||
### #12 tauri.conf.json CSP 设为 null
|
||||
- 生产环境应配置严格 CSP 策略
|
||||
|
||||
### #13 WorkflowFailed 中 failed_node 始终为空
|
||||
- 应从执行器错误信息提取失败节点 ID
|
||||
|
||||
### #14 greet 命令为脚手架残留
|
||||
- 确认前端不再使用后移除
|
||||
|
||||
---
|
||||
|
||||
## ✅ 代码亮点
|
||||
|
||||
1. 注释质量极高 — 解释"为什么"而非"是什么"
|
||||
2. Agentic 循环设计精良 — 停止信号、idle timeout、断连检测、审批门控
|
||||
3. 风险分级机制 — Low/Medium/High 三级控制
|
||||
4. 错误处理一致 — 统一 Result<T, String>
|
||||
5. 对话持久化 — 自动保存,支持多对话切换
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
BIN
src-tauri/icons/128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
BIN
src-tauri/icons/32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.icns
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/icon.ico
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
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(())
|
||||
}
|
||||
70
src-tauri/src/lib.rs
Normal file
70
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! DevFlow Tauri 入口 — 初始化 AppState 并注册全部 IPC 命令
|
||||
|
||||
mod commands;
|
||||
mod state;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
use state::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! Welcome to DevFlow.", name)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
// 数据库放在系统应用数据目录下:<app_data_dir>/devflow.db
|
||||
let data_dir = app.path().app_data_dir()?;
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
let db_path = data_dir.join("devflow.db");
|
||||
|
||||
// 初始化全局状态(打开数据库 + 执行迁移)
|
||||
let app_state = tauri::async_runtime::block_on(AppState::init(&db_path))?;
|
||||
app.manage(app_state);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
// 项目
|
||||
commands::project::list_projects,
|
||||
commands::project::create_project,
|
||||
commands::project::get_project,
|
||||
commands::project::update_project,
|
||||
commands::project::delete_project,
|
||||
// 任务
|
||||
commands::task::list_tasks,
|
||||
commands::task::create_task,
|
||||
commands::task::update_task,
|
||||
commands::task::delete_task,
|
||||
// 想法
|
||||
commands::idea::list_ideas,
|
||||
commands::idea::create_idea,
|
||||
commands::idea::update_idea,
|
||||
commands::idea::delete_idea,
|
||||
// 工作流
|
||||
commands::workflow::run_workflow,
|
||||
commands::workflow::list_workflow_executions,
|
||||
commands::workflow::get_workflow_execution,
|
||||
commands::workflow::approve_human_approval,
|
||||
// AI 聊天
|
||||
commands::ai::ai_chat_send,
|
||||
commands::ai::ai_chat_stop,
|
||||
commands::ai::ai_approve,
|
||||
commands::ai::ai_chat_clear,
|
||||
commands::ai::ai_list_providers,
|
||||
commands::ai::ai_save_provider,
|
||||
commands::ai::ai_set_provider,
|
||||
// AI 对话管理
|
||||
commands::ai::ai_conversation_create,
|
||||
commands::ai::ai_conversation_list,
|
||||
commands::ai::ai_conversation_switch,
|
||||
commands::ai::ai_conversation_delete,
|
||||
commands::ai::ai_conversation_rename,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
devflow_lib::run()
|
||||
}
|
||||
282
src-tauri/src/state.rs
Normal file
282
src-tauri/src/state.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
//! 应用全局状态 — 数据库、Repo、事件总线、节点注册表、AI 会话
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_storage::crud::{
|
||||
AiConversationRepo, AiProviderRepo, AiToolExecutionRepo, IdeaRepo, NodeExecutionRepo,
|
||||
ProjectRepo, ReleaseRepo, TaskRepo, WorkflowRepo,
|
||||
};
|
||||
use df_storage::db::Database;
|
||||
use df_workflow::eventbus::EventBus;
|
||||
use df_workflow::registry::NodeRegistry;
|
||||
|
||||
use crate::commands::ai::AiSession;
|
||||
|
||||
/// 应用全局状态 — 通过 `app.manage()` 注入,command 中以 `State<'_, AppState>` 取用
|
||||
pub struct AppState {
|
||||
/// 数据库句柄(Arc 包装,便于在异步任务中重建 Repo)
|
||||
pub db: Arc<Database>,
|
||||
/// 想法表 Repo
|
||||
pub ideas: IdeaRepo,
|
||||
/// 项目表 Repo
|
||||
pub projects: ProjectRepo,
|
||||
/// 任务表 Repo
|
||||
pub tasks: TaskRepo,
|
||||
/// 发布表 Repo
|
||||
pub releases: ReleaseRepo,
|
||||
/// 工作流执行表 Repo
|
||||
pub workflows: WorkflowRepo,
|
||||
/// 节点执行表 Repo
|
||||
pub node_executions: NodeExecutionRepo,
|
||||
/// 工作流事件总线(tokio broadcast)
|
||||
pub event_bus: EventBus,
|
||||
/// 节点注册表(已注册内置节点)
|
||||
pub registry: Arc<NodeRegistry>,
|
||||
// ── AI ──
|
||||
/// AI 提供商配置 Repo
|
||||
pub ai_providers: AiProviderRepo,
|
||||
/// AI 对话历史 Repo
|
||||
pub ai_conversations: AiConversationRepo,
|
||||
/// AI 工具执行审计 Repo
|
||||
pub ai_tool_executions: AiToolExecutionRepo,
|
||||
/// AI 工具注册表
|
||||
pub ai_tools: Arc<AiToolRegistry>,
|
||||
/// AI 会话状态
|
||||
pub ai_session: Arc<Mutex<AiSession>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// 初始化应用状态:打开(或创建)数据库并执行迁移,构建各 Repo 与节点注册表
|
||||
pub async fn init(db_path: &Path) -> Result<Self> {
|
||||
let db = Arc::new(Database::open(db_path).await?);
|
||||
let ai_tools = Arc::new(build_ai_tool_registry());
|
||||
Ok(Self {
|
||||
ideas: IdeaRepo::new(&db),
|
||||
projects: ProjectRepo::new(&db),
|
||||
tasks: TaskRepo::new(&db),
|
||||
releases: ReleaseRepo::new(&db),
|
||||
workflows: WorkflowRepo::new(&db),
|
||||
node_executions: NodeExecutionRepo::new(&db),
|
||||
ai_providers: AiProviderRepo::new(&db),
|
||||
ai_conversations: AiConversationRepo::new(&db),
|
||||
ai_tool_executions: AiToolExecutionRepo::new(&db),
|
||||
ai_session: Arc::new(Mutex::new(AiSession::new())),
|
||||
db,
|
||||
event_bus: EventBus::new(),
|
||||
registry: Arc::new(build_registry()),
|
||||
ai_tools,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建节点注册表 — 注册内置节点
|
||||
///
|
||||
/// 注意:不使用 `NodeRegistry::default()`,其 script 工厂为占位实现(会 panic),
|
||||
/// 这里注册 df-nodes 提供的真实 ScriptNode 和 HumanNode。
|
||||
fn build_registry() -> NodeRegistry {
|
||||
let mut registry = NodeRegistry::new();
|
||||
registry.register("script", |_config| {
|
||||
Box::new(df_nodes::script_node::ScriptNode)
|
||||
});
|
||||
registry.register("human", |_config| {
|
||||
Box::new(df_nodes::human_node::HumanNode)
|
||||
});
|
||||
registry.register("ai", |_config| {
|
||||
Box::new(df_nodes::ai_node::AiNode)
|
||||
});
|
||||
registry
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AI 工具注册
|
||||
// ============================================================
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
fn object_schema(fields: Vec<(&str, &str, bool)>) -> serde_json::Value {
|
||||
let mut props = serde_json::Map::new();
|
||||
let mut required = Vec::new();
|
||||
|
||||
for (name, typ, req) in &fields {
|
||||
props.insert(name.to_string(), json!({ "type": typ }));
|
||||
if *req {
|
||||
required.push(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": required,
|
||||
})
|
||||
}
|
||||
|
||||
/// 构建 AI 工具注册表 — 注册所有 CRUD 操作为可调用工具
|
||||
fn build_ai_tool_registry() -> AiToolRegistry {
|
||||
let mut registry = AiToolRegistry::new();
|
||||
|
||||
// ── 只读工具 (Low risk) ──
|
||||
|
||||
registry.register(
|
||||
"list_projects",
|
||||
"列出所有项目,返回项目列表(ID、名称、状态、描述)",
|
||||
object_schema(vec![]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"list_tasks",
|
||||
"列出任务,可按 project_id 筛选",
|
||||
object_schema(vec![("project_id", "string", false)]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"list_ideas",
|
||||
"列出所有想法",
|
||||
object_schema(vec![]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
// ── 创建工具 (Medium risk) ──
|
||||
|
||||
registry.register(
|
||||
"update_project",
|
||||
"更新项目的指定字段(name/status/description),需要提供项目 ID、字段名和新值",
|
||||
object_schema(vec![
|
||||
("id", "string", true),
|
||||
("field", "string", true),
|
||||
("value", "string", true),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"create_project",
|
||||
"创建新项目",
|
||||
object_schema(vec![
|
||||
("name", "string", true),
|
||||
("description", "string", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"create_task",
|
||||
"在指定项目下创建新任务",
|
||||
object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("title", "string", true),
|
||||
("description", "string", false),
|
||||
("priority", "integer", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"create_idea",
|
||||
"捕获一个新想法",
|
||||
object_schema(vec![
|
||||
("title", "string", true),
|
||||
("description", "string", false),
|
||||
("tags", "string", false),
|
||||
("source", "string", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
// ── 高风险工具 (High risk) ──
|
||||
|
||||
registry.register(
|
||||
"delete_project",
|
||||
"删除项目及其所有关联数据",
|
||||
object_schema(vec![("id", "string", true)]),
|
||||
RiskLevel::High,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"run_workflow",
|
||||
"运行指定的工作流 DAG",
|
||||
object_schema(vec![
|
||||
("name", "string", true),
|
||||
("dag", "object", true),
|
||||
]),
|
||||
RiskLevel::High,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
// ── 文件系统工具 ──
|
||||
|
||||
registry.register(
|
||||
"read_file",
|
||||
"读取文件内容,返回文本内容。支持 offset 和 limit 参数分页读取大文件",
|
||||
object_schema(vec![
|
||||
("path", "string", true),
|
||||
("offset", "integer", false),
|
||||
("limit", "integer", false),
|
||||
]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"list_directory",
|
||||
"列出目录内容,返回文件和子目录列表(名称、类型、大小)",
|
||||
object_schema(vec![
|
||||
("path", "string", true),
|
||||
("recursive", "boolean", false),
|
||||
]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"write_file",
|
||||
"写入或创建文件,自动创建不存在的父目录",
|
||||
object_schema(vec![
|
||||
("path", "string", true),
|
||||
("content", "string", true),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry
|
||||
}
|
||||
37
src-tauri/tauri.conf.json
Normal file
37
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"productName": "devflow",
|
||||
"version": "0.1.0",
|
||||
"identifier": "top.1216.devflow",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "bun run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "DevFlow",
|
||||
"width": 1024,
|
||||
"height": 768,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"devtools": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user