194c44c
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::service::GitService;
use crate::share::errors::GitError;
/// 提交维度的 AI 评审结果视图。
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CommitAiReviewDto {
pub id: Uuid,
pub commit_sha: String,
pub verdict: models::ai::ai_review_result::AiReviewVerdict,
pub summary: String,
pub findings: Vec<AiFindingDto>,
pub truncated: bool,
/// 生成该评审时使用的输出语言(BCP-47 或 `auto`)。
pub language: String,
pub created_at: OffsetDateTime,
}
/// 行级发现视图;提交评论不落库,因此没有 comment_id。
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AiFindingDto {
pub path: String,
pub line: i32,
pub side: Option<String>,
pub severity: Option<String>,
pub body: String,
}
impl CommitAiReviewDto {
pub(crate) fn from_row(row: models::ai::ai_review_result::Model, commit_sha: &str) -> Self {
let findings: Vec<models::ai::ai_review_result::AiFinding> =
serde_json::from_value(row.findings_json).unwrap_or_default();
Self {
id: row.id,
commit_sha: commit_sha.to_string(),
verdict: row.verdict,
summary: row.summary,
findings: findings
.into_iter()
.map(|finding| AiFindingDto {
path: finding.path,
line: finding.line,
side: finding.side,
severity: finding.severity,
body: finding.body,
})
.collect(),
truncated: row.truncated,
language: row.language,
created_at: row.created_at,
}
}
}
/// AI 评审使用的 commit status context。
pub const AI_REVIEW_CONTEXT: &str = "ai-review";
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AiReviewJobDto {
pub job_id: Uuid,
pub status: String,
pub model: String,
}
/// One commit-status write for the `ai-review` context. Grouped so the four
/// string-ish arguments cannot be passed in the wrong order.
pub struct AiReviewStatus {
pub sha: String,
pub state: crate::service::commits::CommitStatusDtoState,
pub description: String,
pub target_url: Option<String>,
}
impl GitService {
/// 触发(或重跑)某个提交的 AI 评审。
///
/// 入队前先写一条 `pending` 的 commit status,让 PR 检查页即时可见;
/// 评审完成后由 aihost 改写为 success/failure/error。
pub async fn trigger_commit_ai_review(
&self,
space_name: String,
repo_name: String,
revision: String,
creator: i64,
language: Option<String>,
) -> Result<AiReviewJobDto, GitError> {
let cfg = &self.state.config.ai;
if !cfg.review_on() {
return Err(GitError::InvalidArgument(
"ai review is disabled by config".to_string(),
));
}
let ctx = self.context(space_name.clone(), repo_name.clone()).await?;
let sha = self
.baby(&ctx)
.rev_parse(&revision)
.await
.map_err(crate::share::errors::map_baby)?
.to_string();
self.sync_commit(&ctx, &sha).await?;
let task = ci_protocol::ai::AgentTask::CommitReview {
job_id: Uuid::now_v7(),
repo_id: ctx.model.id,
commit_sha: sha.clone(),
language: match language.filter(|value| !value.trim().is_empty()) {
Some(explicit) => Some(ci_protocol::ai_languages::normalize(&explicit)),
None => {
crate::sync::ai_language::actor_language(self.db_read(), Some(creator)).await
}
},
trace: export::telemetry::traceparent(),
request_id: export::telemetry::request_id(),
};
let job_id =
crate::sync::ai_task::submit_ai_task(self.db_write(), &self.state.redis, cfg, &task)
.await
.map_err(|e| GitError::Internal(format!("submit ai review: {e}")))?;
self.set_ai_review_status(
space_name,
repo_name,
creator,
AiReviewStatus {
sha,
state: crate::service::commits::CommitStatusDtoState::Pending,
description: "AI 评审进行中".to_string(),
target_url: None,
},
)
.await?;
Ok(AiReviewJobDto {
job_id,
status: "pending".to_string(),
model: cfg.model.trim().to_string(),
})
}
/// 写入/更新 `ai-review` 这一条 commit status。
pub async fn set_ai_review_status(
&self,
space_name: String,
repo_name: String,
creator: i64,
status: AiReviewStatus,
) -> Result<(), GitError> {
let AiReviewStatus {
sha,
state,
description,
target_url,
} = status;
self.set_commit_status(
space_name,
repo_name,
creator,
crate::service::commits::SetCommitStatusCmd {
sha,
state,
context: AI_REVIEW_CONTEXT.to_string(),
description: Some(description),
target_url,
},
)
.await?;
Ok(())
}
/// 该提交是否已有 AI 评审结果(用于前端展示就绪状态)。
pub async fn has_ai_review(&self, repo_id: Uuid, commit_sha: &str) -> bool {
use models::ai::ai_review_result;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
ai_review_result::Entity::find()
.filter(ai_review_result::Column::RepoId.eq(repo_id))
.filter(ai_review_result::Column::CommitSha.eq(commit_sha))
.one(self.db_read())
.await
.ok()
.flatten()
.is_some()
}
/// 列出某个提交的全部 AI 评审结果。
pub async fn list_commit_ai_reviews(
&self,
space_name: String,
repo_name: String,
revision: String,
) -> Result<Vec<crate::service::ai_review::CommitAiReviewDto>, GitError> {
use models::ai::ai_review_result;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
let ctx = self.context(space_name, repo_name).await?;
let sha = self
.baby(&ctx)
.rev_parse(&revision)
.await
.map_err(crate::share::errors::map_baby)?
.to_string();
let rows = ai_review_result::Entity::find()
.filter(ai_review_result::Column::RepoId.eq(ctx.model.id))
.filter(ai_review_result::Column::CommitSha.eq(sha.clone()))
.order_by_desc(ai_review_result::Column::CreatedAt)
.all(self.db_read())
.await?;
Ok(rows
.into_iter()
.map(|row| crate::service::ai_review::CommitAiReviewDto::from_row(row, &sha))
.collect())
}
}