fix(ai-summary): tell an in-flight job apart from an empty cache
GET /ai-summary returned Option<RepoAiSummaryDto> with a single pending_job_id, so during a first generation the page had nothing to show: no cached row yet, and the one nullable id it carried was keyed to the *current* head while a job queued behind a push targets the head it was submitted for. The endpoint now returns a state object - head_sha, the cached summary for that tip, and the newest unfinished job with its own head, status and language - and the tab renders queued/running, other-head and auto-refresh notices instead of a bare empty panel. Two more gaps closed in the same shape: - pending/running rows are now bounded by an in-flight window derived from request_timeout_secs x max_attempts x 4 (default 24 min), because the queue has no lease and a dead worker used to leave a row that read as "generating" forever; - triggering a summary for a head that already has an unfinished job returns that job id instead of queueing a second model call, so repeat clicks and polling are idempotent. The zh-CN/en repo.ai.pending* strings land here; the repo.aiSettings.* keys in the same files belong to the per-repo switch commit that follows.
ZhenYi committed
Modified lib/git/src/handler/ai_summary/get_ai_summary.rs lib/git/src/handler/ai_summary/get_ai_summary.rs +4 −4 View lib/git/src/handler/ai_summary/get_ai_summary.rs
@@ -5,23 +5,23 @@
55use crate::errors::{ApiError, GitApiError};
66use crate::handler::resolve_reader;
77use crate::service::GitService;
8use crate::service::ai_summary::RepoAiSummaryDto;
8use crate::service::ai_summary::RepoAiSummaryStateDto;
99
1010#[utoipa::path(
1111 get,
1212 path = "/repos/{space_name}/{repo_name}/ai-summary",
1313 tag = "ai",
14 description = "读取当前默认分支的仓库更新总结;尚未生成时 data 为 null。",
14 description = "读取当前默认分支的更新总结状态:已缓存的总结、仍未结束的生成作业,以及两者共同的 head。首次生成期间 data.summary 为 null 而 data.pending 非空。",
1515 params(crate::handler::RepoPath),
1616 responses(
17 (status = 200, description = "repo ai summary", body = AppResponseData<Option<RepoAiSummaryDto>>)
17 (status = 200, description = "repo ai summary state", body = AppResponseData<RepoAiSummaryStateDto>)
1818 )
1919)]
2020pub async fn get_ai_summary(
2121 State(service): State<GitService>,
2222 session: Session,
2323 Path(path): Path<crate::handler::RepoPath>,
24) -> Result<AppResponseData<Option<RepoAiSummaryDto>>, ApiError> {
24) -> Result<AppResponseData<RepoAiSummaryStateDto>, ApiError> {
2525 let _ = resolve_reader(
2626 &service,
2727 &session,
Modified lib/git/src/service/ai_summary.rs lib/git/src/service/ai_summary.rs +100 −16 View lib/git/src/service/ai_summary.rs
@@ -1,11 +1,31 @@
11use crate::share::errors::GitError;
2use models::ai::ai_job::AiJobStatus;
23use models::ai::repo_ai_summary;
3use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
4use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
45use serde::{Deserialize, Serialize};
5use time::OffsetDateTime;
6use time::{Duration, OffsetDateTime};
67use utoipa::ToSchema;
78use uuid::Uuid;
89
10/// 一条作业还能算「在途」多久。
11///
12/// 队列没有租约:worker 崩溃、容器被重启、或在投递前进程退出,都会在
13/// `ai_job` 里留下一条永远停在 `pending`/`running` 的行。管理端的重投只覆盖
14/// `failed`,没人回收这些行,所以「有没有作业」不能只看状态,否则几天前的残骸
15/// 会一直在页面上显示「正在生成」,还会把新任务挡在幂等折叠之外。
16///
17/// 窗口由既有配置推出,不引入新旋钮:单次请求超时 × 尝试次数,再放宽 4 倍
18/// 容纳排队与重试。默认值(120s × 3 × 4)即 24 分钟——超过这个时间还没结束的
19/// 作业,对用户来说已经不是在途,而是失败。
20fn inflight_window(cfg: &config::ai::AiConfig) -> Duration {
21 let seconds = cfg
22 .request_timeout_secs
23 .max(60)
24 .saturating_mul(cfg.max_attempts.max(1) as u64)
25 .saturating_mul(4);
26 Duration::seconds(seconds.min(6 * 60 * 60) as i64)
27}
28
929/// 仓库 AI 总结视图。
1030#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1131pub struct RepoAiSummaryDto {
@@ -24,10 +44,39 @@
2444 pub updated_at: OffsetDateTime,
2545 /// 该总结是否对应当前默认分支提示。
2646 pub current: bool,
27 /// 仍在进行中的作业 id(无进行中任务时为 null)。
28 pub pending_job_id: Option<Uuid>,
2947}
3048
49/// 一条仍未结束的总结作业。
50///
51/// 与结果分开建模:作业可能正为**另一个** head 生成(排队期间有人推送),
52/// 而结果行只能属于它自己的 head。把两者塞进同一个结构会让「有结果」与
53/// 「正在生成」互相掩盖,正是首次生成时页面什么都看不到的原因。
54#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
55pub struct RepoAiSummaryJobDto {
56 pub job_id: Uuid,
57 /// 该作业要总结的 head。
58 pub head_sha: String,
59 /// `pending`(已入队)或 `running`(执行中)。
60 pub status: AiJobStatus,
61 pub language: String,
62 pub created_at: OffsetDateTime,
63 pub started_at: Option<OffsetDateTime>,
64}
65
66/// 总结页签的完整状态:默认分支当前 tip、该 tip 上的结果、以及未结束的作业。
67///
68/// 三个字段一起返回,调用方才能区分「还没生成」与「正在生成」——只给结果时
69/// 两者都是 `null`,界面就只能显示空态。
70#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
71pub struct RepoAiSummaryStateDto {
72 /// 默认分支当前 tip,是结果与作业共同的坐标。
73 pub head_sha: String,
74 /// 该 head 上已缓存的总结;尚未生成时为 null。
75 pub summary: Option<RepoAiSummaryDto>,
76 /// 仍未结束的总结作业;没有则为 null。
77 pub pending: Option<RepoAiSummaryJobDto>,
78}
79
3180#[derive(Debug, Clone, Deserialize, ToSchema)]
3281pub struct TriggerSummaryCmd {
3382 /// 输出语言(BCP-47 或 `auto`);缺省跟随触发者的语言偏好。
@@ -42,12 +91,12 @@
4291}
4392
4493impl super::GitService {
45 /// 读取当前默认分支的 AI 总结(若已生成)。
94 /// 读取当前默认分支的 AI 总结状态:结果可能尚未生成,作业可能仍在路上。
4695 pub async fn get_repo_ai_summary(
4796 &self,
4897 space_name: String,
4998 repo_name: String,
50 ) -> Result<Option<RepoAiSummaryDto>, GitError> {
99 ) -> Result<RepoAiSummaryStateDto, GitError> {
51100 let ctx = self.context(space_name, repo_name).await?;
52101 let head = self
53102 .baby(&ctx)
@@ -60,8 +109,12 @@
60109 .filter(repo_ai_summary::Column::HeadSha.eq(head.clone()))
61110 .one(self.db_read())
62111 .await?;
63 let pending = self.pending_summary_job(ctx.model.id, &head).await;
64 Ok(row.map(|row| Self::summary_dto(row, &head, pending)))
112 let pending = self.pending_summary_job(ctx.model.id).await;
113 Ok(RepoAiSummaryStateDto {
114 summary: row.map(|row| Self::summary_dto(row, &head)),
115 pending,
116 head_sha: head,
117 })
65118 }
66119
67120 /// 触发(或刷新)当前 head 的 AI 总结。
@@ -95,6 +148,11 @@
95148 ),
96149 None => None,
97150 };
151 // 空态文案承诺「同一版本不会重复调用模型」:同一个 head 已有在途作业时
152 // 直接把它交回去,而不是再排一个。重复点击(或界面轮询触发)因此是幂等的。
153 if let Some(queued) = self.queued_summary_job(ctx.model.id, &head).await {
154 return Ok(queued);
155 }
98156 let task = ci_protocol::ai::AgentTask::RepoSummary {
99157 job_id: Uuid::now_v7(),
100158 repo_id: ctx.model.id,
@@ -115,14 +173,45 @@
115173 .map_err(|e| GitError::Internal(format!("submit ai summary: {e}")))
116174 }
117175
118 /// 该 head 上仍在排队/执行的总结作业。
119 async fn pending_summary_job(&self, repo_id: Uuid, head: &str) -> Option<Uuid> {
176 /// 该仓库仍未结束的总结作业,最新的优先。
177 ///
178 /// 按仓库而不是按 head 取:排队期间有人推送时,作业的目标已经不是当前
179 /// tip 了,但它依然是「正在生成」,界面必须能说出来——带上它的 head,
180 /// 用户才知道完成的是哪一版。
181 async fn pending_summary_job(&self, repo_id: Uuid) -> Option<RepoAiSummaryJobDto> {
182 use models::ai::ai_job::{self, AiJobKind, AiJobStatus};
183 let cutoff = OffsetDateTime::now_utc() - inflight_window(&self.state.config.ai);
184 ai_job::Entity::find()
185 .filter(ai_job::Column::RepoId.eq(repo_id))
186 .filter(ai_job::Column::Kind.eq(AiJobKind::RepoSummary))
187 .filter(ai_job::Column::Status.is_in([AiJobStatus::Pending, AiJobStatus::Running]))
188 .filter(ai_job::Column::CreatedAt.gte(cutoff))
189 .order_by_desc(ai_job::Column::CreatedAt)
190 .one(self.db_read())
191 .await
192 .ok()
193 .flatten()
194 .map(|row| RepoAiSummaryJobDto {
195 status: row.status,
196 head_sha: row.target_ref.unwrap_or_default(),
197 language: row.language,
198 created_at: row.created_at,
199 started_at: row.started_at,
200 job_id: row.id,
201 })
202 }
203
204 /// 指定 head 上仍未结束的总结作业 id,用于把重复触发折叠成一次。
205 async fn queued_summary_job(&self, repo_id: Uuid, head: &str) -> Option<Uuid> {
120206 use models::ai::ai_job::{self, AiJobKind, AiJobStatus};
207 let cutoff = OffsetDateTime::now_utc() - inflight_window(&self.state.config.ai);
121208 ai_job::Entity::find()
122209 .filter(ai_job::Column::RepoId.eq(repo_id))
123210 .filter(ai_job::Column::Kind.eq(AiJobKind::RepoSummary))
124211 .filter(ai_job::Column::TargetRef.eq(head.to_string()))
125212 .filter(ai_job::Column::Status.is_in([AiJobStatus::Pending, AiJobStatus::Running]))
213 .filter(ai_job::Column::CreatedAt.gte(cutoff))
214 .order_by_desc(ai_job::Column::CreatedAt)
126215 .one(self.db_read())
127216 .await
128217 .ok()
@@ -130,11 +219,7 @@
130219 .map(|row| row.id)
131220 }
132221
133 fn summary_dto(
134 row: repo_ai_summary::Model,
135 head: &str,
136 pending_job_id: Option<Uuid>,
137 ) -> RepoAiSummaryDto {
222 fn summary_dto(row: repo_ai_summary::Model, head: &str) -> RepoAiSummaryDto {
138223 let highlights: Vec<String> =
139224 serde_json::from_value(row.highlights_json).unwrap_or_default();
140225 let risks: Vec<String> = serde_json::from_value(row.risks_json).unwrap_or_default();
@@ -152,7 +237,6 @@
152237 created_at: row.created_at,
153238 updated_at: row.updated_at,
154239 current: row.head_sha == head,
155 pending_job_id,
156240 }
157241 }
158242}
Modified web/src/entities/repo/model/ai-summary.ts web/src/entities/repo/model/ai-summary.ts +21 −6 View web/src/entities/repo/model/ai-summary.ts
@@ -1,19 +1,34 @@
1/** 仓库级 AI 总结:按 head 缓存,读取不触发模型调用。 */
1/**
2 * 仓库级 AI 总结:按 head 缓存,读取不触发模型调用。
3 *
4 * 读取返回的是**状态**而不是可空结果:`summary` 为空只说明还没有缓存,`pending`
5 * 才说明模型正在跑。两者分开是必需的——首次生成期间结果必然为空,只看结果的话
6 * 界面无法区分「没人做过」与「正在做」。
7 */
28import Apis from '@/shared/api'
39import { unwrapApi } from '@/shared/lib'
410import type {
5 AppResponseData_Option_RepoAiSummaryDto,
11 AppResponseData_RepoAiSummaryStateDto,
12 RepoAiSummaryJobDto,
613 TriggerSummaryCmd,
714} from '@/shared/api'
815
9export type RepoAiSummary = NonNullable<AppResponseData_Option_RepoAiSummaryDto['data']>
16export type RepoAiSummaryState = NonNullable<AppResponseData_RepoAiSummaryStateDto['data']>
17export type RepoAiSummary = NonNullable<RepoAiSummaryState['summary']>
18export type RepoAiSummaryJob = NonNullable<RepoAiSummaryState['pending']>
19
20export type { RepoAiSummaryJobDto }
1021
22/** 当前默认分支的总结状态:已缓存的总结 + 仍未结束的作业。 */
1123export const fetchRepoAiSummary = (space: string, repo: string) =>
12 unwrapApi<RepoAiSummary | null>(
24 unwrapApi<RepoAiSummaryState>(
1325 Apis.ai.get_ai_summary({ pathParams: { space_name: space, repo_name: repo } }),
1426 )
1527
16/** 触发(或刷新)总结;返回作业 id。 */
28/**
29 * 触发(或刷新)总结;返回作业 id。服务端对同一 head 的在途作业做折叠,
30 * 因此重复调用是幂等的,会拿到同一个 id。
31 */
1732export const triggerRepoAiSummary = (
1833 space: string,
1934 repo: string,
@@ -24,4 +39,4 @@
2439 pathParams: { space_name: space, repo_name: repo },
2540 data: cmd,
2641 }),
27 )
No newline at end of file
42 )
Modified web/src/entities/repo/index.ts web/src/entities/repo/index.ts +2 −0 View web/src/entities/repo/index.ts
@@ -177,6 +177,8 @@
177177 fetchRepoAiSummary,
178178 triggerRepoAiSummary,
179179 type RepoAiSummary,
180 type RepoAiSummaryJob,
181 type RepoAiSummaryState,
180182} from './model/ai-summary'
181183
182184// releases
Modified web/src/features/repo-ai/ui/ai-summary-tab.tsx web/src/features/repo-ai/ui/ai-summary-tab.tsx +78 −21 View web/src/features/repo-ai/ui/ai-summary-tab.tsx
@@ -1,9 +1,9 @@
1import { useState } from 'react'
1import { useEffect, useState } from 'react'
22import { AlertTriangle, Loader2, RefreshCw, Sparkles } from 'lucide-react'
33import { useT } from '@/shared/i18n'
44import { Button, ErrorPanel, Markdown, PanelNote } from '@/shared/ui'
55import { fmtRelative, useAsync } from '@/shared/lib'
6import { fetchRepoAiSummary, triggerRepoAiSummary } from '@/entities/repo'
6import { fetchRepoAiSummary, triggerRepoAiSummary, type RepoAiSummaryJob } from '@/entities/repo'
77import { LanguagePicker } from '@/entities/pull-request'
88
99export interface AiSummaryTabProps {
@@ -12,6 +12,9 @@
1212 rev: string
1313}
1414
15/** 有在途作业时的轮询间隔:生成通常几十秒,5 秒足够跟手又不打扰后端。 */
16const PENDING_POLL_MS = 5_000
17
1518/** 仓库 AI 总结页签:读取按 head 缓存的总结,可手动刷新。 */
1619export function AiSummaryTab({ space, repo, rev }: AiSummaryTabProps) {
1720 const { t, tc } = useT()
@@ -25,7 +28,17 @@
2528 const summary = useAsync(() => fetchRepoAiSummary(space, repo), [space, repo, tick])
2629 const failure = summary.error as { code?: number; message?: string } | undefined
2730 const data = summary.data
31 const pending = data?.pending ?? null
32 const result = data?.summary ?? null
2833
34 // 作业在别处(另一个标签页、推送自动触发、管理员重试)发起时,这一页也要
35 // 自己跟上:只要服务端还报有在途作业,就定时重读,直到它消失。
36 useEffect(() => {
37 if (!pending) return
38 const timer = setInterval(() => setTick(n => n + 1), PENDING_POLL_MS)
39 return () => clearInterval(timer)
40 }, [pending])
41
2942 async function refresh() {
3043 setBusy(true)
3144 setError(null)
@@ -33,6 +46,8 @@
3346 try {
3447 await triggerRepoAiSummary(space, repo, language ? { language } : {})
3548 setNotice(t('repo.ai.queued'))
49 // 立刻重读一次,让「正在生成」当场出现,而不是等下一次轮询。
50 setTick(n => n + 1)
3651 } catch (f) {
3752 setError(f instanceof Error ? f.message : t('repo.ai.triggerSummaryFailed'))
3853 } finally {
@@ -46,65 +61,107 @@
4661 return (
4762 <div className="space-y-3">
4863 <div className="flex flex-wrap items-center gap-2">
49 <Button className="h-8 text-[12.5px]" disabled={busy} onClick={() => void refresh()}>
50 {busy ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden /> : <Sparkles className="mr-1.5 h-3.5 w-3.5" aria-hidden />}
51 {data ? t('repo.ai.regenerate') : t('repo.ai.generateSummary')}
64 <Button className="h-8 text-[12.5px]" disabled={busy || Boolean(pending)} onClick={() => void refresh()}>
65 {busy || pending ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden /> : <Sparkles className="mr-1.5 h-3.5 w-3.5" aria-hidden />}
66 {result ? t('repo.ai.regenerate') : t('repo.ai.generateSummary')}
5267 </Button>
5368 <Button variant="secondary" className="h-8 text-[12.5px]" disabled={busy} onClick={() => setTick(t => t + 1)}>
5469 <RefreshCw className="mr-1.5 h-3.5 w-3.5" aria-hidden />
5570 {t('repo.ai.loadResults')}
5671 </Button>
57 <LanguagePicker value={language} onChange={setLanguage} disabled={busy} />
72 <LanguagePicker value={language} onChange={setLanguage} disabled={busy || Boolean(pending)} />
5873 {rev ? <span className="text-[12px] text-shell-ink-muted">{t('repo.ai.branch', { branch: rev })}</span> : null}
5974 </div>
6075
6176 {notice ? <p className="rounded-lg border border-shell-hairline bg-shell-field px-3 py-2 text-[12.5px] text-shell-ink-muted">{notice}</p> : null}
6277 {error ? <p className="text-[12.5px] text-[#b42318]" role="alert">{error}</p> : null}
6378
64 {!data ? (
65 <PanelNote text={t('repo.ai.emptyNote')} />
66 ) : (
79 {pending && data ? <PendingJob pending={pending} headline={data.head_sha} /> : null}
80
81 {result ? (
6782 <>
6883 <section className="rounded-xl border border-shell-hairline bg-shell-surface">
6984 <header className="flex flex-wrap items-center gap-2 border-b border-shell-hairline bg-shell-field px-4 py-2.5 text-[12px] text-shell-ink-muted">
70 <span>{t('repo.ai.version', { version: data.head_sha.slice(0, 8) })}</span>
71 <span>· {tc('repo.ai.commitCount', data.commit_count)}</span>
72 <span>· {fmtRelative(data.updated_at)}</span>
73 {data.current ? (
85 <span>{t('repo.ai.version', { version: result.head_sha.slice(0, 8) })}</span>
86 <span>· {tc('repo.ai.commitCount', result.commit_count)}</span>
87 <span>· {fmtRelative(result.updated_at)}</span>
88 {result.current ? (
7489 <span className="text-[#1a7f37]">{t('repo.ai.currentBranch')}</span>
7590 ) : (
7691 <span className="text-[#8a6100]">{t('repo.ai.stale')}</span>
7792 )}
78 {data.language ? <span>{data.language}</span> : null}
79 {data.pending_job_id ? <span className="text-[#8a6100]">{t('repo.ai.pending')}</span> : null}
93 {result.language ? <span>{result.language}</span> : null}
8094 </header>
8195 <div className="space-y-3 px-4 py-3">
82 <Markdown>{data.content}</Markdown>
96 <Markdown>{result.content}</Markdown>
8397 </div>
8498 </section>
8599
86 {data.highlights.length > 0 ? (
100 {result.highlights.length > 0 ? (
87101 <section className="rounded-xl border border-shell-hairline bg-shell-surface p-4">
88102 <h4 className="mb-2 text-[13px] font-semibold">{t('repo.ai.highlights')}</h4>
89103 <ul className="list-disc space-y-1 pl-5 text-[12.5px] leading-5">
90 {data.highlights.map((item, index) => <li key={index}>{item}</li>)}
104 {result.highlights.map((item, index) => <li key={index}>{item}</li>)}
91105 </ul>
92106 </section>
93107 ) : null}
94108
95 {data.risks.length > 0 ? (
109 {result.risks.length > 0 ? (
96110 <section className="rounded-xl border border-shell-hairline bg-shell-surface p-4">
97111 <h4 className="mb-2 flex items-center gap-1.5 text-[13px] font-semibold">
98112 <AlertTriangle className="h-3.5 w-3.5 text-[#8a6100]" aria-hidden />
99113 {t('repo.ai.risks')}
100114 </h4>
101115 <ul className="list-disc space-y-1 pl-5 text-[12.5px] leading-5">
102 {data.risks.map((item, index) => <li key={index}>{item}</li>)}
116 {result.risks.map((item, index) => <li key={index}>{item}</li>)}
103117 </ul>
104118 </section>
105119 ) : null}
106120 </>
121 ) : pending ? null : (
122 <PanelNote text={t('repo.ai.emptyNote')} />
107123 )}
108124 </div>
109125 )
110}
No newline at end of file
126}
127
128/**
129 * 生成进行中的占位。首次生成时它取代空态,让「已经提交、模型在跑」这件事在
130 * 页面上成立;已有结果时它落在结果卡上方,提示旧内容正在被替换。
131 */
132function PendingJob({
133 pending,
134 headline,
135}: {
136 pending: RepoAiSummaryJob
137 /** 当前分支 tip,用于说明作业目标是否还是它。 */
138 headline: string
139}) {
140 const { t } = useT()
141 const running = pending.status === 'running'
142 const behind = pending.head_sha !== '' && pending.head_sha !== headline
143 return (
144 <section
145 role="status"
146 aria-live="polite"
147 className="rounded-xl border border-dashed border-shell-hairline bg-shell-field px-4 py-3.5"
148 >
149 <div className="flex items-center gap-2 text-[13px] font-semibold">
150 <Loader2 className="h-4 w-4 shrink-0 animate-spin text-shell-ink-muted" aria-hidden />
151 {t('repo.ai.pendingTitle')}
152 </div>
153 <p className="mt-1 text-[12.5px] leading-5 text-shell-ink-muted">
154 {running ? t('repo.ai.pendingRunning') : t('repo.ai.pendingQueued')}
155 </p>
156 {behind ? (
157 <p className="mt-1 text-[12.5px] leading-5 text-[#8a6100]">
158 {t('repo.ai.pendingOtherHead', { version: pending.head_sha.slice(0, 8) })}
159 </p>
160 ) : null}
161 <p className="mt-1.5 text-[11.5px] text-shell-ink-muted">
162 {pending.started_at ? `${t('repo.ai.pendingSince', { time: fmtRelative(pending.started_at) })} · ` : ''}
163 {t('repo.ai.autoRefresh', { seconds: PENDING_POLL_MS / 1000 })}
164 </p>
165 </section>
166 )
167}
Modified web/src/shared/i18n/messages/en/repo.ts web/src/shared/i18n/messages/en/repo.ts +16 −0 View web/src/shared/i18n/messages/en/repo.ts
@@ -359,6 +359,12 @@
359359 'repo.ai.currentBranch': 'Matches the current branch',
360360 'repo.ai.stale': 'Based on an older revision',
361361 'repo.ai.pending': 'A job is running',
362 'repo.ai.pendingTitle': 'Generating the AI summary',
363 'repo.ai.pendingQueued': 'The job is queued and waiting for a worker.',
364 'repo.ai.pendingRunning': 'The model is reading the recent commits; the result appears here on its own once it finishes.',
365 'repo.ai.pendingOtherHead': 'The revision being generated is {version}, not the current branch tip; regenerate afterwards if you want this one.',
366 'repo.ai.pendingSince': 'Started {time}',
367 'repo.ai.autoRefresh': 'Checking again every {seconds}s until it finishes.',
362368 'repo.ai.highlights': 'Highlights',
363369 'repo.ai.risks': 'Risks',
364370 'repo.ai.triggerReviewFailed': 'Failed to trigger the AI review',
@@ -418,6 +424,16 @@
418424 'repo.settings.nav.locks': 'LFS locks',
419425 'repo.settings.nav.protection': 'Protection rules',
420426 'repo.settings.nav.ci': 'CI/CD',
427 'repo.settings.nav.ai': 'AI',
428 'repo.aiSettings.sub': 'Turn this repository’s AI automation on or off per capability. These settings only take effect while the platform-wide AI switch is on.',
429 'repo.aiSettings.reviewOnPush': 'Review commits on push',
430 'repo.aiSettings.reviewOnPushHint': 'After a push to a branch, generate an AI review for the new commit; it shows up on the commit page and in PR checks.',
431 'repo.aiSettings.summaryOnPush': 'Summarise the latest state on push',
432 'repo.aiSettings.summaryOnPushHint': 'After a push to a branch, generate and cache a repository summary for that branch tip. The same revision never calls the model twice.',
433 'repo.aiSettings.reviewOnPull': 'Review new pull requests',
434 'repo.aiSettings.reviewOnPullHint': 'Opening, reopening or updating a pull request submits an AI review automatically; results land in the PR’s AI review tab.',
435 'repo.aiSettings.saved': 'AI settings saved.',
436 'repo.aiSettings.disabled': 'AI is not enabled platform-wide, or no model endpoint is configured. The switches below are saved but no jobs will be submitted.',
421437 'repo.settings.navLabel': 'Repository settings sections',
422438 'repo.settings.generalSub': 'Description, homepage, visibility, Git LFS and the default branch. Space owners only.',
423439 'repo.settings.field.description': 'Description',
Modified web/src/shared/i18n/messages/zh-CN/repo.ts web/src/shared/i18n/messages/zh-CN/repo.ts +16 −0 View web/src/shared/i18n/messages/zh-CN/repo.ts
@@ -359,6 +359,12 @@
359359 'repo.ai.currentBranch': '对应当前分支',
360360 'repo.ai.stale': '基于旧版本',
361361 'repo.ai.pending': '有任务进行中',
362 'repo.ai.pendingTitle': '正在生成 AI 总结',
363 'repo.ai.pendingQueued': '任务已排队,等待执行。',
364 'repo.ai.pendingRunning': '模型正在阅读最近的提交,完成后这里会自动显示结果。',
365 'repo.ai.pendingOtherHead': '正在生成的版本是 {version},与当前分支 tip 不同;完成后可按需重新生成。',
366 'repo.ai.pendingSince': '开始于 {time}',
367 'repo.ai.autoRefresh': '完成前每 {seconds} 秒自动检查一次。',
362368 'repo.ai.highlights': '亮点',
363369 'repo.ai.risks': '风险',
364370 'repo.ai.triggerReviewFailed': '触发 AI 评审失败',
@@ -418,6 +424,16 @@
418424 'repo.settings.nav.locks': 'LFS 锁',
419425 'repo.settings.nav.protection': '保护规则',
420426 'repo.settings.nav.ci': 'CI/CD',
427 'repo.settings.nav.ai': 'AI',
428 'repo.aiSettings.sub': '让这个仓库的 AI 自动化按需开关。平台级 AI 总开关关闭时,这里的设置不生效。',
429 'repo.aiSettings.reviewOnPush': '推送时自动评审提交',
430 'repo.aiSettings.reviewOnPushHint': '每次推送到分支后,为新的提交生成 AI 评审结果,可在提交页与 PR 检查中查看。',
431 'repo.aiSettings.summaryOnPush': '推送时自动总结最新状态',
432 'repo.aiSettings.summaryOnPushHint': '每次推送到分支后,为该分支最新状态生成仓库总结并缓存;同一版本不会重复调用模型。',
433 'repo.aiSettings.reviewOnPull': '新建 PR 时自动评审',
434 'repo.aiSettings.reviewOnPullHint': 'PR 创建、重开或推送新提交时自动投递 AI 评审,结果写入 PR 的 AI 评审页签。',
435 'repo.aiSettings.saved': 'AI 设置已保存。',
436 'repo.aiSettings.disabled': '平台未启用 AI,或未配置模型端点;下面的开关已保存,但不会有任务投递。',
421437 'repo.settings.navLabel': '仓库设置分类',
422438 'repo.settings.generalSub': '仓库描述、主页、可见性、Git LFS 与默认分支。仅空间所有者可修改。',
423439 'repo.settings.field.description': '描述',