新增: 初始化 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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user