diff --git a/backend/src/services/okr-ai-sync.ts b/backend/src/services/okr-ai-sync.ts index 4f451e6..cd9862d 100644 --- a/backend/src/services/okr-ai-sync.ts +++ b/backend/src/services/okr-ai-sync.ts @@ -504,75 +504,105 @@ export async function analyzeCommitsForOKR(dryRun = false): Promise(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}`); + if (groups.length === 0) { + console.info('[AI-OKR] No unanalyzed commits found, skipping'); + const elapsed = Date.now() - startTime; + await db.insert(syncLogs).values({ + id: uuid(), + source: 'ai_okr', + status: 'success', + message: `${dryRun ? '[DRY RUN] ' : ''}No unanalyzed commits, skipped in ${elapsed}ms`, + recordsProcessed: 0, + syncedAt: new Date(), + }); + return { totalCommits: 0, reposProcessed: 0, actionsExecuted: 0, summaries: [] }; } + + 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(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 }; }