新增: 知识图谱Phase3基础设施(父⑥) — project_services+D10凭证审查+IPC+AI工具
父⑥ Phase 3(对标设计 §2.3): - V31 迁移:project_services 表(id/project_id FK/name/service_type/endpoint/config_json/environment/remark)+ idx_project_services_project - ProjectServiceRecord + ProjectServiceRepo(impl_repo! 宏 + insert_validated/update_full_validated/list_by_project/list_by_project_env) - service_type 白名单(mysql/postgresql/sqlite/redis/mongodb/mq/api/other,应用层校验) - D10 凭证审查:config_json/endpoint/remark 含 password/passwd/secret/api_key/apikey/token 拒绝(不存敏感凭证,凭证走环境变量) - 4 IPC(add/update/remove/list_project_services,环境过滤)+ lib.rs 注册 - AI 工具 add/list_project_services(Medium/Low)+ 基线 39→41(data 25→27)+ 3 端到端测试 P2 修复(verify agent 发现): - settings.rs project_services 白名单移除 service_type(对齐注释"不列入"+ status 收口,防 update_field 旁路绕过 validate_service_type) df-storage 117(105+12) + df-ai 331 + 基线 41 测试全过。
This commit is contained in:
285
src-tauri/src/commands/services.rs
Normal file
285
src-tauri/src/commands/services.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
//! 项目基础设施配置相关命令(知识图谱 Phase 3 V31,对标设计 §2.3 + §五 + D10 安全边界)
|
||||
//!
|
||||
//! 记录项目依赖的基础设施(数据库/缓存/MQ/API 等),为 AI 执行任务时提供基础设施上下文:
|
||||
//! "这项目用了什么数据库、Redis 在哪、有没有 MQ"。
|
||||
//!
|
||||
//! 关键设计(对标 events.rs Phase 2 模式 + project.rs IPC 模式):
|
||||
//! - **service_type 白名单**:mysql/postgresql/sqlite/redis/mongodb/mq/api/other,
|
||||
//! 校验下沉 ProjectServiceRepo::insert_validated / update_full_validated(单一真相源)。
|
||||
//! - **D10 安全边界**:不存敏感凭证。ProjectServiceRepo 在 insert/update 时审查
|
||||
//! config_json / endpoint / remark 是否含 password/secret/token/api_key 子串,命中即拒。
|
||||
//! IPC 入参 schema 在此注明,config_json 不含密码 —— 凭证走环境变量。
|
||||
//! - create/update IPC 返回完整记录(对标 create_project),前端拿回 id 直接用。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use df_storage::crud::PROJECT_SERVICE_TYPES;
|
||||
use df_storage::models::ProjectServiceRecord;
|
||||
use df_types::types::new_id;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{err_str, now_millis};
|
||||
|
||||
// ============================================================
|
||||
// 入参 / 出参结构
|
||||
// ============================================================
|
||||
|
||||
/// 新增基础设施配置入参(对标设计 §五 add_project_service + D10 边界)。
|
||||
///
|
||||
/// ⚠️ **D10 安全边界**:不存敏感凭证(密码/密钥/Token)。config_json / endpoint / remark
|
||||
/// 含 password/secret/token/api_key 子串会被 Repo 层审查拒绝。凭证走环境变量,此处只存
|
||||
/// 连接信息(host/port/数据库名/连接池配置等元数据)。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AddProjectServiceInput {
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
/// 基础设施类型,白名单 mysql/postgresql/sqlite/redis/mongodb/mq/api/other
|
||||
pub service_type: String,
|
||||
/// 连接地址(localhost:3306 / URL),可选。不含凭证(query 带 token= 会被拒)
|
||||
#[serde(default)]
|
||||
pub endpoint: Option<String>,
|
||||
/// 类型相关配置 JSON 字符串(如 {"pool_size":10}),可选。不含密码字段
|
||||
#[serde(default)]
|
||||
pub config_json: Option<String>,
|
||||
/// 环境 development/staging/production,默认 development
|
||||
#[serde(default = "default_environment")]
|
||||
pub environment: String,
|
||||
#[serde(default)]
|
||||
pub remark: Option<String>,
|
||||
}
|
||||
|
||||
/// 整体更新入参(对标 Repo::update_full_validated,保留 id 与 created_at)。
|
||||
/// 全字段必填(整体替换语义,与 IdeaRepo::update_full 一致)。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateProjectServiceInput {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub service_type: String,
|
||||
#[serde(default)]
|
||||
pub endpoint: Option<String>,
|
||||
#[serde(default)]
|
||||
pub config_json: Option<String>,
|
||||
#[serde(default = "default_environment")]
|
||||
pub environment: String,
|
||||
#[serde(default)]
|
||||
pub remark: Option<String>,
|
||||
}
|
||||
|
||||
/// 列表查询入参(对标设计 §五 list_project_services:按项目过滤,可选按环境过滤)。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListProjectServicesInput {
|
||||
pub project_id: String,
|
||||
/// 可选,按环境过滤(development/staging/production)。空/None = 返回所有环境。
|
||||
#[serde(default)]
|
||||
pub environment: Option<String>,
|
||||
}
|
||||
|
||||
/// 列表返回(对标 events.rs TimelineResult,外层补 total 便于前端计数)。
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListProjectServicesResult {
|
||||
pub items: Vec<ProjectServiceRecord>,
|
||||
pub total: usize,
|
||||
pub project_id: String,
|
||||
}
|
||||
|
||||
fn default_environment() -> String {
|
||||
"development".to_string()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC 命令
|
||||
// ============================================================
|
||||
|
||||
/// 新增基础设施配置(对标设计 §五 add_project_service)。
|
||||
///
|
||||
/// service_type 白名单 + D10 凭证审查下沉 Repo::insert_validated(单一真相源,与 AI 工具
|
||||
/// add_project_service 同源)。返回完整记录。
|
||||
#[tauri::command]
|
||||
pub async fn add_project_service(
|
||||
state: State<'_, AppState>,
|
||||
input: AddProjectServiceInput,
|
||||
) -> Result<ProjectServiceRecord, String> {
|
||||
let project_id = input.project_id.trim().to_string();
|
||||
let name = input.name.trim().to_string();
|
||||
let service_type = input.service_type.trim().to_string();
|
||||
if project_id.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err("name 不能为空".to_string());
|
||||
}
|
||||
if service_type.is_empty() {
|
||||
return Err("service_type 不能为空".to_string());
|
||||
}
|
||||
// service_type 白名单 fail-fast(给清晰错误,虽 Repo 层也会校验)
|
||||
if !PROJECT_SERVICE_TYPES.contains(&service_type.as_str()) {
|
||||
return Err(format!(
|
||||
"非法 service_type: {service_type},合法值: {:?}",
|
||||
PROJECT_SERVICE_TYPES
|
||||
));
|
||||
}
|
||||
|
||||
let record = ProjectServiceRecord {
|
||||
id: new_id(),
|
||||
project_id,
|
||||
name,
|
||||
service_type,
|
||||
endpoint: input
|
||||
.endpoint
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
config_json: input
|
||||
.config_json
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
environment: input.environment.trim().to_string(),
|
||||
remark: input
|
||||
.remark
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
// created_at/updated_at 由 Repo 内部覆盖,此处占位
|
||||
created_at: now_millis(),
|
||||
updated_at: now_millis(),
|
||||
};
|
||||
// insert_validated 内含 service_type 白名单 + D10 凭证审查(密码/token/api_key 子串拒绝)。
|
||||
let id = state
|
||||
.project_services
|
||||
.insert_validated(record)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
state
|
||||
.project_services
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| "插入后回读失败".to_string())
|
||||
}
|
||||
|
||||
/// 整体更新基础设施配置(保留 id 与 created_at,updated_at 由 Repo 内部覆盖)。
|
||||
///
|
||||
/// service_type 变更须走此整体更新路径(对标 Repo 注释:update_field 白名单不含 service_type,
|
||||
/// 防旁路 update_field 改类型)。D10 凭证审查下沉 update_full_validated。
|
||||
#[tauri::command]
|
||||
pub async fn update_project_service(
|
||||
state: State<'_, AppState>,
|
||||
input: UpdateProjectServiceInput,
|
||||
) -> Result<ProjectServiceRecord, String> {
|
||||
let id = input.id.trim().to_string();
|
||||
let project_id = input.project_id.trim().to_string();
|
||||
let name = input.name.trim().to_string();
|
||||
let service_type = input.service_type.trim().to_string();
|
||||
if id.is_empty() {
|
||||
return Err("id 不能为空".to_string());
|
||||
}
|
||||
if project_id.is_empty() || name.is_empty() || service_type.is_empty() {
|
||||
return Err("project_id / name / service_type 不能为空".to_string());
|
||||
}
|
||||
if !PROJECT_SERVICE_TYPES.contains(&service_type.as_str()) {
|
||||
return Err(format!(
|
||||
"非法 service_type: {service_type},合法值: {:?}",
|
||||
PROJECT_SERVICE_TYPES
|
||||
));
|
||||
}
|
||||
|
||||
// 先校验存在(404 友好错误),再整体更新
|
||||
let existing = state
|
||||
.project_services
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("基础设施配置 {id} 不存在"))?;
|
||||
|
||||
let record = ProjectServiceRecord {
|
||||
id: id.clone(),
|
||||
project_id,
|
||||
name,
|
||||
service_type,
|
||||
endpoint: input
|
||||
.endpoint
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
config_json: input
|
||||
.config_json
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
environment: input.environment.trim().to_string(),
|
||||
remark: input
|
||||
.remark
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
// 保留原 created_at(update_full_validated 不覆盖 created_at)
|
||||
created_at: existing.created_at,
|
||||
updated_at: now_millis(),
|
||||
};
|
||||
let hit = state
|
||||
.project_services
|
||||
.update_full_validated(&record)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
if !hit {
|
||||
return Err(format!("基础设施配置 {id} 不存在(更新未命中)"));
|
||||
}
|
||||
state
|
||||
.project_services
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| "更新后回读失败".to_string())
|
||||
}
|
||||
|
||||
/// 删除基础设施配置(按 id 物理删,对标 Repo::delete)。
|
||||
#[tauri::command]
|
||||
pub async fn remove_project_service(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<bool, String> {
|
||||
let id = id.trim().to_string();
|
||||
if id.is_empty() {
|
||||
return Err("id 不能为空".to_string());
|
||||
}
|
||||
state.project_services.delete(&id).await.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 查询项目基础设施配置(对标设计 §五 list_project_services)。
|
||||
///
|
||||
/// 按项目过滤,可选按环境过滤(production 部署任务只取 production 服务,开发调试取 development,
|
||||
/// 环境隔离避免误连生产库)。返回时间倒序列表。
|
||||
#[tauri::command]
|
||||
pub async fn list_project_services(
|
||||
state: State<'_, AppState>,
|
||||
input: ListProjectServicesInput,
|
||||
) -> Result<ListProjectServicesResult, String> {
|
||||
let project_id = input.project_id.trim().to_string();
|
||||
if project_id.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
let env_filter = input
|
||||
.environment
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let items = if let Some(env) = &env_filter {
|
||||
state
|
||||
.project_services
|
||||
.list_by_project_env(&project_id, env)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
} else {
|
||||
state
|
||||
.project_services
|
||||
.list_by_project(&project_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
};
|
||||
let total = items.len();
|
||||
Ok(ListProjectServicesResult {
|
||||
items,
|
||||
total,
|
||||
project_id,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user