清理: 删除后端 ci_status 死命令与悬挂冲突类型

- ci_status.rs 单命令文件随前端 CIStatus 组件删除(get_commit_status 零消费)
- types.ts 删 ConflictInfo(ConflictResolver 死组件连带,全仓零引用)
- lib.rs 注册行删除随其他在途改动另行入库
This commit is contained in:
lxy
2026-10-25 22:33:05 +08:00
parent 62776a30f1
commit 1d9c511b84
3 changed files with 24 additions and 236 deletions
-223
View File
@@ -1,223 +0,0 @@
//! CI 检查状态查询命令 — 读取 Gitea commit 检查状态(对标 GitHub commit status API)
//!
//! 调用 Gitea `{api_base}/api/v1/repos/{owner}/{repo}/commits/{sha}/statuses` 拉取该
//! commit 的 CI 检查项列表,转成 CheckStatus 返回前端(供 commit 详情面板展示 ✓/✗/⏳)。
//!
//! 设计取舍:
//! - **容错优先**:网络错误 / URL 解析失败 / Gitea 异常 → 返回空 vec,**不阻断工作流**
//! (CI 状态属于辅助信息,失败不应阻塞 Git 浏览等主功能)。
//! - URL 解析:从 repo_url(如 `https://gitea.1216.top/lxy/DevFlow`)拆出
//! `{scheme}://{host}` 作 api_base,路径段尾两段作 owner/repo。
//! - 字段映射:Gitea status.state 取值 `pending/success/failure/error/...`,
//! error/warning 归一为 failure 展示;`context` 作检查名,`target_url` 作详情链接。
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::state::AppState;
/// 前端展示用的 CI 检查状态(归一化后)
#[derive(Debug, Clone, Serialize)]
pub struct CheckStatus {
/// 检查名(如 "ci/cargo-test"、"continuous-integration/drone/push")
pub name: String,
/// 归一状态:"pending" | "success" | "failure"
pub status: String,
/// 详情链接(Gitea target_url,可能为空)
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
/// Gitea API 原始返回项(仅取关心的字段,其余忽略)
#[derive(Debug, Deserialize)]
struct GiteaStatus {
/// 检查上下文名(Gitea 字段名 context)
context: Option<String>,
/// 原始状态:pending / success / failure / error / warning / ...
state: Option<String>,
/// 详情链接
target_url: Option<String>,
}
/// 从 repo_url 解析 (api_base, owner, repo)。
///
/// 例:`https://gitea.1216.top/lxy/DevFlow` →
/// (`https://gitea.1216.top`, `lxy`, `DevFlow`)
///
/// `.git` 后缀容忍;路径段不足两段返回 None。
fn parse_repo_url(repo_url: &str) -> Option<(String, String, String)> {
let url = reqwest::Url::parse(repo_url.trim()).ok()?;
if !matches!(url.scheme(), "http" | "https") {
return None;
}
let host = url.host_str()?;
// port_or_known_default() 对 https 返 Some(443)、http 返 Some(80),
// 致 authority 总是带默认端口(:443/:80),base URL 形如 "https://host:443" 而非规范
// "https://host"。下游 Gitea API 调用虽大多容忍,但测试断言与可读性受影响。
// 修复:仅当端口非 scheme 默认端口时才拼入 authority(显式非默认端口才保留)。
let port = url.port(); // 仅取显式端口(未在 URL 写出的不返)
let authority = match port {
Some(p) => format!("{}:{}", host, p),
None => host.to_string(),
};
let api_base = format!("{}://{}", url.scheme(), authority);
// 路径段:去空 / 去 .git 后缀 → 取末两段 owner / repo
// (url::Url::path_segments() 返 Option<Split>,Split 不实现 Default,需手动兜底)
let path_iter = url.path_segments().unwrap_or("".split('/'));
let mut segments: Vec<String> = path_iter
.filter(|s| !s.is_empty())
.map(|s| s.trim_end_matches(".git").to_string())
.filter(|s| !s.is_empty())
.collect();
if segments.len() < 2 {
return None;
}
let repo = segments.pop()?;
let owner = segments.pop()?;
Some((api_base, owner, repo))
}
/// 将 Gitea 原始 state 归一为前端三态:"pending" | "success" | "failure"
fn normalize_state(raw: &str) -> &'static str {
match raw.to_lowercase().as_str() {
"success" => "success",
"pending" | "" => "pending",
// failure / error / warning / blocked / 其他均视为失败展示
_ => "failure",
}
}
/// 查询指定 commit 的 CI 检查状态列表。
///
/// 任一环节失败(网络/解析/Gitea 异常)均返回空 vec,日志告警但不返回错误,
/// 避免阻断前端 Git 浏览等主流程。
#[tauri::command]
pub async fn get_commit_status(
_state: State<'_, AppState>,
repo_url: String,
commit_sha: String,
) -> Result<Vec<CheckStatus>, String> {
let (api_base, owner, repo) = match parse_repo_url(&repo_url) {
Some(parsed) => parsed,
None => {
tracing::warn!(
repo_url = %repo_url,
"[ci_status] repo_url 解析失败,返回空列表"
);
return Ok(vec![]);
}
};
let url = format!(
"{}/api/v1/repos/{}/{}/commits/{}/statuses",
api_base, owner, repo, commit_sha
);
tracing::info!(url = %url, "[ci_status] 查询 Gitea commit statuses");
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
{
Ok(c) => c,
Err(e) => {
tracing::warn!(error = %e, "[ci_status] 构建 reqwest client 失败");
return Ok(vec![]);
}
};
let resp = match client.get(&url).send().await {
Ok(r) => r,
Err(e) => {
tracing::warn!(error = %e, "[ci_status] 请求 Gitea 失败(网络不可达或未鉴权)");
return Ok(vec![]);
}
};
if !resp.status().is_success() {
tracing::warn!(
status = %resp.status(),
"[ci_status] Gitea 响应非 2xx,返回空列表"
);
return Ok(vec![]);
}
let raw: Vec<GiteaStatus> = match resp.json().await {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "[ci_status] 解析 Gitea 响应 JSON 失败");
return Ok(vec![]);
}
};
let checks = raw
.into_iter()
.map(|s| CheckStatus {
name: s.context.unwrap_or_else(|| "unknown".to_string()),
status: normalize_state(s.state.as_deref().unwrap_or("")).to_string(),
url: s.target_url.filter(|u| !u.is_empty()),
})
.collect();
Ok(checks)
}
#[cfg(test)]
mod tests {
use super::*;
// ── parse_repo_url ──
#[test]
fn parse_standard_gitea_url() {
let (base, owner, repo) = parse_repo_url("https://gitea.1216.top/lxy/DevFlow").unwrap();
assert_eq!(base, "https://gitea.1216.top");
assert_eq!(owner, "lxy");
assert_eq!(repo, "DevFlow");
}
#[test]
fn parse_strips_git_suffix() {
let (_, owner, repo) = parse_repo_url("https://gitea.1216.top/lxy/DevFlow.git").unwrap();
assert_eq!(owner, "lxy");
assert_eq!(repo, "DevFlow");
}
#[test]
fn parse_with_port() {
let (base, _, _) = parse_repo_url("http://gitea.local:3000/team/proj").unwrap();
assert_eq!(base, "http://gitea.local:3000");
}
#[test]
fn parse_rejects_non_http() {
assert!(parse_repo_url("ftp://gitea/x/y").is_none());
assert!(parse_repo_url("file:///x/y").is_none());
}
#[test]
fn parse_rejects_short_path() {
assert!(parse_repo_url("https://gitea.1216.top/").is_none());
assert!(parse_repo_url("https://gitea.1216.top/onlyone").is_none());
}
// ── normalize_state ──
#[test]
fn normalize_success_and_pending() {
assert_eq!(normalize_state("success"), "success");
assert_eq!(normalize_state("pending"), "pending");
assert_eq!(normalize_state(""), "pending");
}
#[test]
fn normalize_failure_variants() {
assert_eq!(normalize_state("failure"), "failure");
assert_eq!(normalize_state("error"), "failure");
assert_eq!(normalize_state("warning"), "failure");
assert_eq!(normalize_state("blocked"), "failure");
}
}
-1
View File
@@ -3,7 +3,6 @@
//! 所有 command 统一返回 `Result<T, String>`,错误经 `to_string()` 传给前端。 //! 所有 command 统一返回 `Result<T, String>`,错误经 `to_string()` 传给前端。
pub mod ai; pub mod ai;
pub mod ci_status;
pub mod events; pub mod events;
pub mod idea; pub mod idea;
pub mod knowledge; pub mod knowledge;
+24 -12
View File
@@ -942,10 +942,33 @@ export interface KnowledgeEventRecord {
timestamp: string timestamp: string
} }
/** 知识详情聚合负载(基本信息 + 全部生命线事件) */ /** 知识关联边(阶段1 连边,双向语义) */
export interface KnowledgeEdgeRecord {
id: string
src_id: string
dst_id: string
/** similar 强相关 | evolves 演进 | causal 因果 | merges 合并 | anchors 锚点 */
kind: string
weight: number
/** llm-tag | embed-sim | co-cite | manual | tag-co-occur */
origin: string
created_at: string
last_verified_at: string
}
/** 关联知识条目(边 + 对端知识展开,direction: out=指出/in=指入) */
export interface RelatedKnowledge {
edge: KnowledgeEdgeRecord
knowledge: KnowledgeRecord
direction: 'out' | 'in'
}
/** 知识详情聚合负载(基本信息 + 全部生命线事件 + 关联知识) */
export interface KnowledgeDetailPayload { export interface KnowledgeDetailPayload {
knowledge: KnowledgeRecord knowledge: KnowledgeRecord
events: KnowledgeEventRecord[] events: KnowledgeEventRecord[]
/** 关联知识(老库/无边时为空数组) */
related: RelatedKnowledge[]
} }
/** 编辑知识入参(部分更新,仅传需改字段) */ /** 编辑知识入参(部分更新,仅传需改字段) */
@@ -1029,14 +1052,3 @@ export interface ConflictRecord {
created_at: string created_at: string
resolved_at: string | null resolved_at: string | null
} }
/** 冲突信息(AiMergeCompleted 事件载荷) */
export interface ConflictInfo {
id: string
file_path: string
conflict_type: ConflictType
subtask_a: string | null
subtask_b: string | null
diff_a?: string | null
diff_b?: string | null
}