feat(okr-ai): AI 分析失败/空跑也写入 sync_logs
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m30s

之前 AI 分析只在「有 commits 且全部成功」时写 success 日志,
出错走 .catch 只打 stdout、空跑直接 return 不写。导致 UI 同步日志页
看不出 AI 是否真的跑过、为什么没产出。

改为:
- 空跑写 success(带 "No unanalyzed commits")
- 单仓库失败累计后写入 message 末尾
- 全仓库失败或外层异常写 status=error 并附 stack
- message 截断到 1000 字符避免超长

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zyc 2026-04-29 13:40:04 +08:00
parent 8561f6190d
commit 33b3a2208e

View File

@ -504,75 +504,105 @@ export async function analyzeCommitsForOKR(dryRun = false): Promise<AnalyzeResul
const startTime = Date.now(); const startTime = Date.now();
const batchId = uuid(); const batchId = uuid();
const groups = await gatherUnanalyzedCommits();
if (groups.length === 0) {
console.info('[AI-OKR] No unanalyzed commits found, skipping');
return { totalCommits: 0, reposProcessed: 0, actionsExecuted: 0, summaries: [] };
}
let totalCommits = 0; let totalCommits = 0;
let actionsExecuted = 0; let actionsExecuted = 0;
const summaries: { repo: string; summary: string }[] = []; const summaries: { repo: string; summary: string }[] = [];
const repoErrors: { repo: string; error: string }[] = [];
const MAX_COMMITS_PER_BATCH = 30; try {
const groups = await gatherUnanalyzedCommits();
for (const group of groups) { if (groups.length === 0) {
try { console.info('[AI-OKR] No unanalyzed commits found, skipping');
totalCommits += group.commits.length; const elapsed = Date.now() - startTime;
await db.insert(syncLogs).values({
// 分批处理,每批最多 30 条 commits id: uuid(),
const batches: typeof group.commits[] = []; source: 'ai_okr',
for (let i = 0; i < group.commits.length; i += MAX_COMMITS_PER_BATCH) { status: 'success',
batches.push(group.commits.slice(i, i + MAX_COMMITS_PER_BATCH)); message: `${dryRun ? '[DRY RUN] ' : ''}No unanalyzed commits, skipped in ${elapsed}ms`,
} recordsProcessed: 0,
syncedAt: new Date(),
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) { });
const batchCommits = batches[batchIdx]; return { totalCommits: 0, reposProcessed: 0, actionsExecuted: 0, summaries: [] };
const batchGroup = { ...group, commits: batchCommits };
console.info(`[AI-OKR] Analyzing ${batchCommits.length} commits for repo: ${group.repoName} (batch ${batchIdx + 1}/${batches.length})`);
// 构建 prompt 并调用 AI
const userPrompt = await buildUserPrompt(batchGroup);
const rawResponse = await callLLM(SYSTEM_PROMPT, userPrompt);
const aiResponse = parseLLMJson<AIResponse>(rawResponse);
if (!aiResponse.actions || !Array.isArray(aiResponse.actions)) {
console.warn(`[AI-OKR] Invalid AI response for ${group.repoName}, skipping batch`);
await markCommitsAnalyzed(batchCommits.map(c => c.sha), batchId);
continue;
}
if (batchIdx === batches.length - 1) {
summaries.push({ repo: group.repoName, summary: aiResponse.summary || '' });
}
if (!dryRun) {
const count = await executeActions(aiResponse.actions, group.projectId);
actionsExecuted += count;
await markCommitsAnalyzed(batchCommits.map(c => c.sha), batchId);
} else {
console.info(`[AI-OKR] [DRY RUN] Would execute ${aiResponse.actions.length} actions for ${group.repoName}`);
actionsExecuted += aiResponse.actions.length;
}
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[AI-OKR] Failed to analyze repo ${group.repoName}: ${msg}`);
} }
const MAX_COMMITS_PER_BATCH = 30;
for (const group of groups) {
try {
totalCommits += group.commits.length;
// 分批处理,每批最多 30 条 commits
const batches: typeof group.commits[] = [];
for (let i = 0; i < group.commits.length; i += MAX_COMMITS_PER_BATCH) {
batches.push(group.commits.slice(i, i + MAX_COMMITS_PER_BATCH));
}
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
const batchCommits = batches[batchIdx];
const batchGroup = { ...group, commits: batchCommits };
console.info(`[AI-OKR] Analyzing ${batchCommits.length} commits for repo: ${group.repoName} (batch ${batchIdx + 1}/${batches.length})`);
// 构建 prompt 并调用 AI
const userPrompt = await buildUserPrompt(batchGroup);
const rawResponse = await callLLM(SYSTEM_PROMPT, userPrompt);
const aiResponse = parseLLMJson<AIResponse>(rawResponse);
if (!aiResponse.actions || !Array.isArray(aiResponse.actions)) {
console.warn(`[AI-OKR] Invalid AI response for ${group.repoName}, skipping batch`);
await markCommitsAnalyzed(batchCommits.map(c => c.sha), batchId);
continue;
}
if (batchIdx === batches.length - 1) {
summaries.push({ repo: group.repoName, summary: aiResponse.summary || '' });
}
if (!dryRun) {
const count = await executeActions(aiResponse.actions, group.projectId);
actionsExecuted += count;
await markCommitsAnalyzed(batchCommits.map(c => c.sha), batchId);
} else {
console.info(`[AI-OKR] [DRY RUN] Would execute ${aiResponse.actions.length} actions for ${group.repoName}`);
actionsExecuted += aiResponse.actions.length;
}
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[AI-OKR] Failed to analyze repo ${group.repoName}: ${msg}`);
repoErrors.push({ repo: group.repoName, error: msg });
}
}
// 记录 sync log成功或部分失败
const elapsed = Date.now() - startTime;
const status = repoErrors.length === groups.length ? 'error' : 'success';
const errorSuffix = repoErrors.length > 0
? `; failed repos: ${repoErrors.map(e => `${e.repo}(${e.error})`).join(' | ')}`
: '';
await db.insert(syncLogs).values({
id: uuid(),
source: 'ai_okr',
status,
message: `${dryRun ? '[DRY RUN] ' : ''}Analyzed ${totalCommits} commits from ${groups.length} repos, executed ${actionsExecuted} actions in ${elapsed}ms${errorSuffix}`.slice(0, 1000),
recordsProcessed: actionsExecuted,
syncedAt: new Date(),
});
console.info(`[AI-OKR] Completed: ${totalCommits} commits, ${groups.length} repos, ${actionsExecuted} actions in ${elapsed}ms`);
return { totalCommits, reposProcessed: groups.length, actionsExecuted, summaries };
} catch (err) {
const msg = err instanceof Error ? `${err.message}\n${err.stack || ''}` : String(err);
const elapsed = Date.now() - startTime;
console.error(`[AI-OKR] Fatal error: ${msg}`);
await db.insert(syncLogs).values({
id: uuid(),
source: 'ai_okr',
status: 'error',
message: `${dryRun ? '[DRY RUN] ' : ''}Fatal error after ${elapsed}ms: ${msg}`.slice(0, 1000),
recordsProcessed: actionsExecuted,
syncedAt: new Date(),
});
throw err;
} }
// 记录 sync log
const elapsed = Date.now() - startTime;
await db.insert(syncLogs).values({
id: uuid(),
source: 'ai_okr',
status: 'success',
message: `${dryRun ? '[DRY RUN] ' : ''}Analyzed ${totalCommits} commits from ${groups.length} repos, executed ${actionsExecuted} actions in ${elapsed}ms`,
recordsProcessed: actionsExecuted,
syncedAt: new Date(),
});
console.info(`[AI-OKR] Completed: ${totalCommits} commits, ${groups.length} repos, ${actionsExecuted} actions in ${elapsed}ms`);
return { totalCommits, reposProcessed: groups.length, actionsExecuted, summaries };
} }