新增: 初始化 DevFlow 项目仓库

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

View File

@@ -0,0 +1,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()))?
}
}