新增: 项目多工程系统(工程表+Git 状态查询+AI 工具)
一个项目可含多个工程(Module),每个工程是独立代码仓库, 有自己的目录、Git 地址和技术栈。单仓库项目退化:项目下 只有一个工程,用户无感工程概念。 数据层: - project_modules 表(V34 迁移):工程名/目录/Git 地址/技术栈 - ProjectModuleRepo:完整 CRUD + 按项目列表(排序) 后端接口: - 工程 CRUD:添加/更新/删除/列表 - Git 状态查询:实时跑 git 命令返回当前分支/改动文件/最近提交 (10 秒超时,无 Git 仓库返回空状态) 创建项目适配: - 新建项目时自动创建一个工程(目录=绑定路径,技术栈=探测结果) AI 工具: - list_project_modules:列出项目工程列表(AI 了解项目结构) 同时新增工程系统设计方案文档。
This commit is contained in:
@@ -1270,6 +1270,37 @@ fn register_task_graph_tools(registry: &mut AiToolRegistry, db: &Arc<Database>)
|
||||
})
|
||||
})},
|
||||
);
|
||||
|
||||
// ── list_project_modules (Low 只读,工程系统) ──
|
||||
// 查询项目工程列表(AI 取项目结构上下文:有哪些代码仓库、各自目录/技术栈/Git 地址)。
|
||||
// 多工程场景(Monorepo/微服务/前后端分离)AI 据此决定在哪个工程执行 git/构建操作。
|
||||
registry.register(
|
||||
"list_project_modules", "查询项目工程列表(每个工程是独立代码仓库)。参数:project_id(项目 ID)。返回 { modules: 工程列表(含 name/path/git_url/stack), total }。AI 据此了解项目有哪些代码仓库及各自技术栈",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
]),
|
||||
RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
let repo = df_storage::crud::ProjectModuleRepo::new(&db);
|
||||
let modules = repo.list_by_project(&project_id).await?;
|
||||
let total = modules.len();
|
||||
Ok(serde_json::json!({
|
||||
"modules": modules,
|
||||
"total": total,
|
||||
"project_id": project_id,
|
||||
}))
|
||||
})
|
||||
})},
|
||||
);
|
||||
}
|
||||
|
||||
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
|
||||
@@ -2992,6 +3023,8 @@ mod tests {
|
||||
// register_task_graph_tools 6→7)。
|
||||
// 知识图谱 Phase 3(2026-06-27): data 层 25→27(新增 add_project_service/list_project_services
|
||||
// 项目基础设施配置 D10 不存凭证,register_task_graph_tools 7→9)。
|
||||
// 工程系统(2026-06-29): data 层 27→28(新增 list_project_modules 工程列表查询,
|
||||
// register_task_graph_tools 9→10)
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
41,
|
||||
@@ -3017,6 +3050,8 @@ mod tests {
|
||||
"get_project_timeline",
|
||||
// 知识图谱 Phase 3 项目基础设施配置(register_task_graph_tools 9 个,9/10 为这 2 工具)
|
||||
"add_project_service", "list_project_services",
|
||||
// 工程系统:项目工程列表查询
|
||||
"list_project_modules",
|
||||
// ── file 层 (13) ──(run_command 注册顺序已移至末位降低 LLM 偏好,
|
||||
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621;
|
||||
// detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能)
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod events;
|
||||
pub mod idea;
|
||||
pub mod knowledge;
|
||||
pub mod knowledge_timeline;
|
||||
pub mod module;
|
||||
pub mod project;
|
||||
pub mod services;
|
||||
pub mod settings;
|
||||
|
||||
376
src-tauri/src/commands/module.rs
Normal file
376
src-tauri/src/commands/module.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
//! 工程系统命令(V34,project_modules 表,项目多工程,每个工程独立代码仓库)
|
||||
//!
|
||||
//! 对标 [`services`](super::services) 的 IPC 模式:CRUD 返回完整记录,前端拿回 id 直接用。
|
||||
//!
|
||||
//! 关键设计(对标设计 §五工程系统 + V34 迁移注释):
|
||||
//! - **工程元数据 CRUD**:路径/Git 地址/技术栈存表,create/update 返回完整记录。
|
||||
//! - **Git 状态实时派生**:`get_module_git_status` 在工程目录跑 git 命令(分支/改动/最近提交),
|
||||
//! 10s 超时,无 .git 返回空状态。**前端 IPC 用 std::process::Command**(非 df-execute,
|
||||
//! 因为这是用户读操作,不是 AI 工具调用,不走审批/沙箱)。
|
||||
//! - **路径校验**:trim + 非空校验在前置 IPC 层(给清晰错误)。
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use df_storage::models::ProjectModuleRecord;
|
||||
use df_types::types::new_id;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{err_str, now_millis};
|
||||
|
||||
// ============================================================
|
||||
// 入参 / 出参结构
|
||||
// ============================================================
|
||||
|
||||
/// 新增工程入参(对标设计 §五 add_project_module)。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AddProjectModuleInput {
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
/// 工程目录绝对路径(本机)
|
||||
pub path: String,
|
||||
/// 远程仓库地址,可选(单仓库本地工程可为空)
|
||||
#[serde(default)]
|
||||
pub git_url: Option<String>,
|
||||
/// 技术栈 JSON 字符串(前端检测后传入),可选
|
||||
#[serde(default)]
|
||||
pub stack: Option<String>,
|
||||
}
|
||||
|
||||
/// 更新工程入参(部分更新语义,仅传入字段被覆盖;对标设计 §五 update_project_module)。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateProjectModuleInput {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub git_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stack: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助:Optional 字段 trim + 空串归一为 None
|
||||
// ============================================================
|
||||
|
||||
fn trim_opt(s: Option<String>) -> Option<String> {
|
||||
s.map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC 命令 — 工程 CRUD
|
||||
// ============================================================
|
||||
|
||||
/// 新增工程(对标设计 §五 add_project_module)。返回完整记录。
|
||||
#[tauri::command]
|
||||
pub async fn add_project_module(
|
||||
state: State<'_, AppState>,
|
||||
input: AddProjectModuleInput,
|
||||
) -> Result<ProjectModuleRecord, String> {
|
||||
let project_id = input.project_id.trim().to_string();
|
||||
let name = input.name.trim().to_string();
|
||||
let path = input.path.trim().to_string();
|
||||
if project_id.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err("name 不能为空".to_string());
|
||||
}
|
||||
if path.is_empty() {
|
||||
return Err("path 不能为空".to_string());
|
||||
}
|
||||
// sort_order 取当前项目下工程数(末尾追加,新工程排末位)
|
||||
let existing = state
|
||||
.project_modules
|
||||
.list_by_project(&project_id)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
let sort_order = existing.len() as i32;
|
||||
|
||||
let record = ProjectModuleRecord {
|
||||
id: new_id(),
|
||||
project_id,
|
||||
name,
|
||||
path,
|
||||
git_url: trim_opt(input.git_url),
|
||||
stack: trim_opt(input.stack),
|
||||
auto_detected: false,
|
||||
sort_order,
|
||||
// created_at/updated_at 由 Repo 内部覆盖,此处占位
|
||||
created_at: now_millis(),
|
||||
updated_at: now_millis(),
|
||||
};
|
||||
let record_id = record.id.clone();
|
||||
let ok = state
|
||||
.project_modules
|
||||
.insert(record)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
if !ok {
|
||||
return Err("插入工程记录失败".to_string());
|
||||
}
|
||||
state
|
||||
.project_modules
|
||||
.get_by_id(&record_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| "插入后回读失败".to_string())
|
||||
}
|
||||
|
||||
/// 更新工程(部分更新语义:仅传入字段被覆盖,其余保留)。返回完整记录。
|
||||
#[tauri::command]
|
||||
pub async fn update_project_module(
|
||||
state: State<'_, AppState>,
|
||||
input: UpdateProjectModuleInput,
|
||||
) -> Result<ProjectModuleRecord, String> {
|
||||
let id = input.id.trim().to_string();
|
||||
if id.is_empty() {
|
||||
return Err("id 不能为空".to_string());
|
||||
}
|
||||
// 至少一个可更新字段
|
||||
if input.name.is_none()
|
||||
&& input.path.is_none()
|
||||
&& input.git_url.is_none()
|
||||
&& input.stack.is_none()
|
||||
{
|
||||
return Err("至少提供一个待更新字段 (name/path/git_url/stack)".to_string());
|
||||
}
|
||||
|
||||
// 先校验存在(404 友好错误),再整体更新(保留不可变字段)
|
||||
let mut existing = state
|
||||
.project_modules
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("工程 {id} 不存在"))?;
|
||||
|
||||
if let Some(name) = input.name.map(|s| s.trim().to_string()) {
|
||||
if name.is_empty() {
|
||||
return Err("name 不能为空".to_string());
|
||||
}
|
||||
existing.name = name;
|
||||
}
|
||||
if let Some(path) = input.path.map(|s| s.trim().to_string()) {
|
||||
if path.is_empty() {
|
||||
return Err("path 不能为空".to_string());
|
||||
}
|
||||
existing.path = path;
|
||||
}
|
||||
existing.git_url = trim_opt(input.git_url);
|
||||
existing.stack = trim_opt(input.stack);
|
||||
|
||||
let hit = state
|
||||
.project_modules
|
||||
.update_full(&existing)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
if !hit {
|
||||
return Err(format!("工程 {id} 不存在(更新未命中)"));
|
||||
}
|
||||
state
|
||||
.project_modules
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| "更新后回读失败".to_string())
|
||||
}
|
||||
|
||||
/// 删除工程(按 id 硬删,工程不需要软删审计追溯)。
|
||||
#[tauri::command]
|
||||
pub async fn remove_project_module(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
let id = id.trim().to_string();
|
||||
if id.is_empty() {
|
||||
return Err("id 不能为空".to_string());
|
||||
}
|
||||
state.project_modules.delete(&id).await.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 查询项目全部工程(按 sort_order ASC 返回,前端工程列表稳定顺序)。
|
||||
#[tauri::command]
|
||||
pub async fn list_project_modules(
|
||||
state: State<'_, AppState>,
|
||||
project_id: String,
|
||||
) -> Result<Vec<ProjectModuleRecord>, String> {
|
||||
let project_id = project_id.trim().to_string();
|
||||
if project_id.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
state
|
||||
.project_modules
|
||||
.list_by_project(&project_id)
|
||||
.await
|
||||
.map_err(err_str)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC 命令 — Git 状态查询(实时派生,不进表)
|
||||
// ============================================================
|
||||
|
||||
/// Git 改动文件项(状态码 + 相对路径)。
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GitChangedFile {
|
||||
/// `git status --porcelain` 的状态码(xy 两字符,如 " M"、"M "、"??")
|
||||
status: String,
|
||||
/// 相对仓库根的路径
|
||||
path: String,
|
||||
}
|
||||
|
||||
/// 最近提交项(简短哈希 + subject)。
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GitRecentCommit {
|
||||
hash: String,
|
||||
subject: String,
|
||||
}
|
||||
|
||||
/// Git 状态返回结构(无 .git 时各字段空,前端据此显示"非 Git 仓库")。
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GitStatus {
|
||||
/// 当前分支名(HEAD 处于分离状态时为空)
|
||||
branch: String,
|
||||
/// 改动文件列表(未提交)
|
||||
changed_files: Vec<GitChangedFile>,
|
||||
/// 最近 10 条提交
|
||||
recent_commits: Vec<GitRecentCommit>,
|
||||
/// 该目录是否为 Git 仓库(无 .git 时 false,其余字段空)
|
||||
is_git_repo: bool,
|
||||
}
|
||||
|
||||
/// 默认空状态(无 .git / git 不可用时返回此)。
|
||||
fn empty_status() -> GitStatus {
|
||||
GitStatus {
|
||||
branch: String::new(),
|
||||
changed_files: Vec::new(),
|
||||
recent_commits: Vec::new(),
|
||||
is_git_repo: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询工程目录的 Git 状态(分支/改动文件/最近提交)。
|
||||
///
|
||||
/// 实现选择:**前端 IPC 用 `std::process::Command`**(非 df-execute,因为这是用户读操作,
|
||||
/// 不是 AI 工具调用,不走审批/沙箱)。10s 超时,无 .git 返回空状态(`is_git_repo: false`)。
|
||||
///
|
||||
/// 返回 `serde_json::Value` 以便前端灵活取字段(分支/改动/提交三段)。
|
||||
#[tauri::command]
|
||||
pub async fn get_module_git_status(
|
||||
state: State<'_, AppState>,
|
||||
module_id: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let module_id = module_id.trim().to_string();
|
||||
if module_id.is_empty() {
|
||||
return Err("module_id 不能为空".to_string());
|
||||
}
|
||||
// 取工程记录拿 path
|
||||
let module = state
|
||||
.project_modules
|
||||
.get_by_id(&module_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("工程 {module_id} 不存在"))?;
|
||||
|
||||
let path = std::path::Path::new(&module.path);
|
||||
// 无 .git → 返回空状态
|
||||
if !path.join(".git").exists() {
|
||||
return Ok(serde_json::to_value(empty_status()).map_err(err_str)?);
|
||||
}
|
||||
|
||||
// git 命令在 spawn_blocking 中执行(阻塞 IO 不污染 async runtime),10s 超时
|
||||
let dir = module.path.clone();
|
||||
let status = tokio::task::spawn_blocking(move || run_git_status(&dir))
|
||||
.await
|
||||
.map_err(|e| format!("git 状态查询任务失败: {e}"))?;
|
||||
|
||||
Ok(serde_json::to_value(status).map_err(err_str)?)
|
||||
}
|
||||
|
||||
/// 在指定目录跑 git 命令采集状态(当前分支 / 改动文件 / 最近提交)。
|
||||
///
|
||||
/// 超时:每个 git 子进程 10s(用户读操作,卡死不应阻塞 UI)。任一命令失败时返回已采集部分。
|
||||
fn run_git_status(dir: &str) -> GitStatus {
|
||||
use std::time::Duration;
|
||||
|
||||
let path = std::path::Path::new(dir);
|
||||
if !path.join(".git").exists() {
|
||||
return empty_status();
|
||||
}
|
||||
|
||||
let timeout = Duration::from_secs(10);
|
||||
|
||||
// 1) 当前分支:`git rev-parse --abbrev-ref HEAD`(分离 HEAD 时返回 "HEAD")
|
||||
let branch = run_git_cmd(path, &["rev-parse", "--abbrev-ref", "HEAD"], timeout)
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty() && s != "HEAD")
|
||||
.unwrap_or_default();
|
||||
|
||||
// 2) 改动文件:`git status --porcelain`(每行 "XY path",XY 为两字符状态码)
|
||||
let mut changed_files = Vec::new();
|
||||
if let Some(out) = run_git_cmd(path, &["status", "--porcelain"], timeout) {
|
||||
for line in out.lines() {
|
||||
if line.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
// porcelain 格式:"XY path"(X/Y 各一字符 + 一空格 + path)
|
||||
let status = line[..2].to_string();
|
||||
let file_path = line[3..].trim().to_string();
|
||||
if !file_path.is_empty() {
|
||||
changed_files.push(GitChangedFile {
|
||||
status,
|
||||
path: file_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 最近提交:`git log -10 --oneline`(每行 "hash subject")
|
||||
let mut recent_commits = Vec::new();
|
||||
if let Some(out) = run_git_cmd(path, &["log", "-10", "--oneline"], timeout) {
|
||||
for line in out.lines() {
|
||||
// oneline 格式:"<short-hash> <subject>"
|
||||
if let Some((hash, subject)) = line.split_once(' ') {
|
||||
recent_commits.push(GitRecentCommit {
|
||||
hash: hash.to_string(),
|
||||
subject: subject.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GitStatus {
|
||||
branch,
|
||||
changed_files,
|
||||
recent_commits,
|
||||
is_git_repo: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 跑单个 git 子命令,捕获 stdout(成功)或 None(失败/超时)。
|
||||
///
|
||||
/// 用 `wait_timeout` 实现 10s 超时(标准库 `Command::output` 无超时,需手动 wait)。
|
||||
fn run_git_cmd(cwd: &std::path::Path, args: &[&str], timeout: std::time::Duration) -> Option<String> {
|
||||
use std::sync::mpsc;
|
||||
|
||||
// 在独立线程跑子进程,主线程 select 超时,避免标准库无超时 API 的痛点。
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
std::thread::spawn(move || {
|
||||
let out = Command::new("git")
|
||||
.args(&args)
|
||||
.current_dir(&cwd)
|
||||
.output();
|
||||
let _ = tx.send(out);
|
||||
});
|
||||
|
||||
match rx.recv_timeout(timeout) {
|
||||
Ok(Ok(output)) if output.status.success() => {
|
||||
Some(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,27 @@ async fn create_with_binding(
|
||||
updated_at: now,
|
||||
};
|
||||
state.projects.insert(record.clone()).await.map_err(err_str)?;
|
||||
// 工程系统:创建项目时自动建一个工程(path = 绑定目录,stack = 探测结果)。
|
||||
// 单仓库项目退化:项目下只有一个工程,用户无感工程概念。
|
||||
// 多仓库场景用户后续可添加更多工程(add_project_module IPC)。
|
||||
if let Some(ref module_path) = record.path {
|
||||
let now_str = df_types::now_millis().to_string();
|
||||
let module = df_storage::models::ProjectModuleRecord {
|
||||
id: df_types::types::new_id(),
|
||||
project_id: record.id.clone(),
|
||||
name: record.name.clone(),
|
||||
path: module_path.clone(),
|
||||
git_url: None, // 探测 git remote 补充(失败为 None,不影响)
|
||||
stack: record.stack.clone(), // 复用项目级技术栈探测结果
|
||||
auto_detected: true, // 自动创建,重新探测时可覆盖
|
||||
sort_order: 0,
|
||||
created_at: now_str.clone(),
|
||||
updated_at: now_str,
|
||||
};
|
||||
if let Err(e) = state.project_modules.insert(module).await {
|
||||
tracing::warn!("创建项目时自动建工程失败(非阻断): {}", e);
|
||||
}
|
||||
}
|
||||
// F-260620: 绑定目录变更后立即 reload 白名单(reload_allowed_dirs 读 projects.path 合并 persistent),
|
||||
// 否则新绑定目录需重启/改 Settings 才生效 → AI 访问误弹窗(已绑定重复弹窗主因,agent3 问题5)
|
||||
if record.path.is_some() {
|
||||
|
||||
@@ -319,6 +319,12 @@ pub fn run() {
|
||||
commands::services::update_project_service,
|
||||
commands::services::remove_project_service,
|
||||
commands::services::list_project_services,
|
||||
// 工程系统(对标设计 §五 + V34):项目多工程 CRUD + Git 状态查询
|
||||
commands::module::add_project_module,
|
||||
commands::module::update_project_module,
|
||||
commands::module::remove_project_module,
|
||||
commands::module::list_project_modules,
|
||||
commands::module::get_module_git_status,
|
||||
// 灵感
|
||||
commands::idea::list_ideas,
|
||||
commands::idea::create_idea,
|
||||
|
||||
@@ -37,8 +37,9 @@ use tokio::sync::{Mutex, RwLock};
|
||||
use df_ai::ai_tools::AiToolRegistry;
|
||||
use df_storage::crud::{
|
||||
AiConversationRepo, AiMessageRepo, AiProviderRepo, AiToolExecutionRepo, IdeaEvalRepo, IdeaRepo,
|
||||
KnowledgeEventsRepo, KnowledgeRepo, NodeExecutionRepo, ProjectEventRepo, ProjectRepo,
|
||||
ProjectServiceRepo, ReleaseRepo, SettingsRepo, TaskLinkRepo, TaskRepo, WorkflowRepo,
|
||||
KnowledgeEventsRepo, KnowledgeRepo, NodeExecutionRepo, ProjectEventRepo, ProjectModuleRepo,
|
||||
ProjectRepo, ProjectServiceRepo, ReleaseRepo, SettingsRepo, TaskLinkRepo, TaskRepo,
|
||||
WorkflowRepo,
|
||||
};
|
||||
use df_storage::db::Database;
|
||||
use df_workflow::eventbus::EventBus;
|
||||
@@ -70,6 +71,10 @@ pub struct AppState {
|
||||
/// (设计 §2.3 + §五 add_project_service/list_project_services)。
|
||||
/// D10 安全边界:不存敏感凭证,凭证走环境变量(insert/update_validated 应用层审查拒绝)。
|
||||
pub project_services: ProjectServiceRepo,
|
||||
/// 工程表 Repo(工程系统 V34,project_modules)。
|
||||
/// 项目多工程(每个工程独立代码仓库):Monorepo 多仓库 / 微服务 / 前后端分离。
|
||||
/// 记录工程元数据(路径/Git 地址/技术栈);Git 状态(分支/改动/提交)实时派生不存表。
|
||||
pub project_modules: ProjectModuleRepo,
|
||||
/// 发布表 Repo(预留:ReleaseRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留)
|
||||
#[allow(dead_code)]
|
||||
pub releases: ReleaseRepo,
|
||||
@@ -203,6 +208,7 @@ impl AppState {
|
||||
task_links: TaskLinkRepo::new(&db),
|
||||
project_events: ProjectEventRepo::new(&db),
|
||||
project_services: ProjectServiceRepo::new(&db),
|
||||
project_modules: ProjectModuleRepo::new(&db),
|
||||
releases: ReleaseRepo::new(&db),
|
||||
workflows: WorkflowRepo::new(&db),
|
||||
node_executions: NodeExecutionRepo::new(&db),
|
||||
|
||||
Reference in New Issue
Block a user