新增: 初始化 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,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 }

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

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

View File

@@ -0,0 +1,6 @@
//! df-storage: 存储层 — SQLite 数据库、模型定义、迁移
pub mod crud;
pub mod db;
pub mod migrations;
pub mod models;

View 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);
";

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