D1 免登录改造:App 启动直接进主界面,删 LoginPage/github.rs/OAuth 全套命令、
Sidebar 账户区与 GitHub OAuth 设置弹窗、健康检查 GitHub Token 项
D2 移除发布/导入/PR/CI 入口:删 PublishModal/ImportRepoModal、ProjectCard 的
PR compare 链接与 Actions 状态灯、RepoRegistryModal 同步 tab、CicdModal runs tab、
publish.rs 的 github_create_repo/git_push_to_github
D3 spacesSync 单机化:删 spacesSync/useSpacesSync/SpacesPage/DiscoveryPanel,
拆除 App→Dashboard→ProjectCard 的 spacesJson 链
D4 git_ops 凭据链重写:ssh agent → git credential helper → gitea_instances token
前缀匹配 → default,不再依赖 github_token
保留:server_software GitHub 镜像(Releases 下载加速)、gitea_migrate 迁移入口、
cicd.rs(待 Gitea Actions 改造卡);DB 表不删(R05 migration 兼容)
顺带:修复 servers.rs 两个存量测试的 schema 漂移(测试建表缺 deploy_type 列);
包含会话前未提交的 agent-infra F3(get_agent_health 健康诊断,与 lib.rs/
commands.ts 物理耦合无法拆分提交)
验证:cargo test 53 passed / typecheck 全绿 / vitest passWithNoTests
352 lines
11 KiB
Rust
352 lines
11 KiB
Rust
use rusqlite::Row;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct Project {
|
||
/// workspace.id(也是旧 projects.id,向后兼容所有关联表)
|
||
pub id: String,
|
||
/// profile.id,用于跨空间共享更新
|
||
pub profile_id: Option<String>,
|
||
/// 等于 id,前端可用于区分语义
|
||
pub workspace_id: Option<String>,
|
||
/// 关联的 git_repos.id(仓库锚点)
|
||
pub repo_id: Option<String>,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
#[serde(rename = "type")]
|
||
pub type_: Option<String>,
|
||
// ── profile 级(项目档案,跨空间共享)────────────────────────
|
||
pub status: Option<String>,
|
||
pub priority: Option<String>,
|
||
pub tech_stack: Option<String>,
|
||
pub tags: Option<String>,
|
||
pub repo_url: Option<String>,
|
||
pub upstream_url: Option<String>,
|
||
pub default_branch: Option<String>,
|
||
pub staging_url: Option<String>,
|
||
pub prod_url: Option<String>,
|
||
pub server_note: Option<String>,
|
||
// ── workspace 级(每空间独立)───────────────────────────────
|
||
pub space_id: Option<String>,
|
||
pub wsl_path: Option<String>,
|
||
pub win_path: Option<String>,
|
||
pub platform: Option<String>,
|
||
pub recent_focus: Option<String>,
|
||
pub startup_commands: Option<String>,
|
||
pub last_push_at: Option<String>,
|
||
pub updated_at: Option<String>,
|
||
}
|
||
|
||
impl Project {
|
||
/// 从 project_workspaces JOIN project_profiles 的查询结果构造
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
profile_id: row.get("profile_id").ok(),
|
||
workspace_id: row.get("workspace_id").ok(),
|
||
repo_id: row.get("repo_id").ok(),
|
||
name: row.get("name")?,
|
||
description: row.get("description").ok(),
|
||
type_: row.get("type").ok(),
|
||
status: row.get("status").ok(),
|
||
priority: row.get("priority").ok(),
|
||
tech_stack: row.get("tech_stack").ok(),
|
||
tags: row.get("tags").ok(),
|
||
repo_url: row.get("repo_url").ok(),
|
||
upstream_url: row.get("upstream_url").ok(),
|
||
default_branch: row.get("default_branch").ok(),
|
||
staging_url: row.get("staging_url").ok(),
|
||
prod_url: row.get("prod_url").ok(),
|
||
server_note: row.get("server_note").ok(),
|
||
space_id: row.get("space_id").ok(),
|
||
wsl_path: row.get("wsl_path").ok(),
|
||
win_path: row.get("win_path").ok(),
|
||
platform: row.get("platform").ok(),
|
||
recent_focus: row.get("recent_focus").ok(),
|
||
startup_commands: row.get("startup_commands").ok(),
|
||
last_push_at: row.get("last_push_at").ok(),
|
||
updated_at: row.get("updated_at").ok(),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct ActivityLog {
|
||
pub id: i64,
|
||
pub project_id: String,
|
||
pub summary: String,
|
||
pub created_at: Option<String>,
|
||
}
|
||
|
||
impl ActivityLog {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
project_id: row.get("project_id")?,
|
||
summary: row.get("summary")?,
|
||
created_at: row.get("created_at")?,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct ProductGroup {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub space_id: Option<String>,
|
||
pub sort_order: i32,
|
||
}
|
||
|
||
impl ProductGroup {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
name: row.get("name")?,
|
||
description: row.get("description")?,
|
||
space_id: row.get("space_id").ok(),
|
||
sort_order: row.get("sort_order").unwrap_or(0),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct ProjectGroupMap {
|
||
pub project_id: String,
|
||
pub group_id: String,
|
||
pub role: Option<String>,
|
||
}
|
||
|
||
impl ProjectGroupMap {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
project_id: row.get("project_id")?,
|
||
group_id: row.get("group_id")?,
|
||
role: row.get("role")?,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct Space {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub is_current: bool,
|
||
pub created_at: Option<String>,
|
||
}
|
||
|
||
impl Space {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
name: row.get("name")?,
|
||
is_current: row.get::<_, i64>("is_current")? != 0,
|
||
created_at: row.get("created_at")?,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct DeployCommand {
|
||
pub id: String,
|
||
pub project_id: String,
|
||
pub name: String,
|
||
pub command: String,
|
||
pub run_in: String,
|
||
}
|
||
|
||
impl DeployCommand {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
project_id: row.get("project_id")?,
|
||
name: row.get("name")?,
|
||
command: row.get("command")?,
|
||
run_in: row.get("run_in")?,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct CicdConfig {
|
||
pub id: String,
|
||
pub project_id: String,
|
||
pub config_type: String,
|
||
pub trigger_branch: String,
|
||
pub build_command: Option<String>,
|
||
pub node_version: Option<String>,
|
||
pub server_host: Option<String>,
|
||
pub server_user: Option<String>,
|
||
pub deploy_path: Option<String>,
|
||
pub start_command: Option<String>,
|
||
pub oss_endpoint: Option<String>,
|
||
pub created_at: Option<String>,
|
||
}
|
||
|
||
impl CicdConfig {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
project_id: row.get("project_id")?,
|
||
config_type: row.get("config_type")?,
|
||
trigger_branch: row.get("trigger_branch")?,
|
||
build_command: row.get("build_command").ok(),
|
||
node_version: row.get("node_version").ok(),
|
||
server_host: row.get("server_host").ok(),
|
||
server_user: row.get("server_user").ok(),
|
||
deploy_path: row.get("deploy_path").ok(),
|
||
start_command: row.get("start_command").ok(),
|
||
oss_endpoint: row.get("oss_endpoint").ok(),
|
||
created_at: row.get("created_at").ok(),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct WorkflowResult {
|
||
pub content: String,
|
||
pub path: String,
|
||
pub secrets: Vec<String>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct GitRepo {
|
||
pub id: String,
|
||
pub repo_url: String,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub default_branch: Option<String>,
|
||
pub upstream_url: Option<String>,
|
||
pub staging_url: Option<String>,
|
||
pub prod_url: Option<String>,
|
||
pub server_note: Option<String>,
|
||
pub status: Option<String>,
|
||
pub priority: Option<String>,
|
||
pub tech_stack: Option<String>,
|
||
pub last_synced_at: Option<String>,
|
||
pub created_at: Option<String>,
|
||
}
|
||
|
||
impl GitRepo {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
repo_url: row.get("repo_url")?,
|
||
name: row.get("name")?,
|
||
description: row.get("description").ok(),
|
||
default_branch: row.get("default_branch").ok(),
|
||
upstream_url: row.get("upstream_url").ok(),
|
||
staging_url: row.get("staging_url").ok(),
|
||
prod_url: row.get("prod_url").ok(),
|
||
server_note: row.get("server_note").ok(),
|
||
status: row.get("status").ok(),
|
||
priority: row.get("priority").ok(),
|
||
tech_stack: row.get("tech_stack").ok(),
|
||
last_synced_at: row.get("last_synced_at").ok(),
|
||
created_at: row.get("created_at").ok(),
|
||
})
|
||
}
|
||
}
|
||
|
||
// ── 源码库 ─────────────────────────────────────────────────────────────────
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct SourceCategory {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub icon: String,
|
||
pub color: String,
|
||
pub sort_order: i32,
|
||
}
|
||
|
||
impl SourceCategory {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
name: row.get("name")?,
|
||
icon: row.get::<_, String>("icon").unwrap_or_else(|_| "📁".into()),
|
||
color: row.get::<_, String>("color").unwrap_or_else(|_| "#6B7280".into()),
|
||
sort_order: row.get("sort_order").unwrap_or(0),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct SourceProject {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub category_id: Option<String>,
|
||
pub language: Option<String>,
|
||
pub tags: Option<String>,
|
||
pub repo_url: Option<String>,
|
||
pub introduction: Option<String>,
|
||
pub readme: Option<String>,
|
||
pub notes: Option<String>,
|
||
pub created_at: Option<String>,
|
||
pub updated_at: Option<String>,
|
||
// join 字段
|
||
pub category_name: Option<String>,
|
||
pub version_count: Option<i32>,
|
||
}
|
||
|
||
impl SourceProject {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
name: row.get("name")?,
|
||
description: row.get("description").ok(),
|
||
category_id: row.get("category_id").ok(),
|
||
language: row.get("language").ok(),
|
||
tags: row.get("tags").ok(),
|
||
repo_url: row.get("repo_url").ok(),
|
||
introduction: row.get("introduction").ok(),
|
||
readme: row.get("readme").ok(),
|
||
notes: row.get("notes").ok(),
|
||
created_at: row.get("created_at").ok(),
|
||
updated_at: row.get("updated_at").ok(),
|
||
category_name: row.get("category_name").ok(),
|
||
version_count: row.get("version_count").ok(),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct SourceVersion {
|
||
pub id: String,
|
||
pub project_id: String,
|
||
pub version: String,
|
||
pub file_path: String,
|
||
pub archive_path: Option<String>,
|
||
pub file_size: i64,
|
||
pub is_primary: bool,
|
||
pub dir_tree: Option<String>,
|
||
pub notes: Option<String>,
|
||
pub imported_at: Option<String>,
|
||
}
|
||
|
||
impl SourceVersion {
|
||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
||
Ok(Self {
|
||
id: row.get("id")?,
|
||
project_id: row.get("project_id")?,
|
||
version: row.get("version")?,
|
||
file_path: row.get("file_path")?,
|
||
archive_path: row.get("archive_path").ok(),
|
||
file_size: row.get("file_size").unwrap_or(0),
|
||
is_primary: row.get::<_, i64>("is_primary").unwrap_or(0) != 0,
|
||
dir_tree: row.get("dir_tree").ok(),
|
||
notes: row.get("notes").ok(),
|
||
imported_at: row.get("imported_at").ok(),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct DirEntry {
|
||
pub name: String,
|
||
#[serde(rename = "type")]
|
||
pub type_: String,
|
||
pub children_count: Option<i32>,
|
||
}
|