All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m4s
基于豆包(Doubao) LLM 分析 git commit messages,按仓库维度自动为每个 提交人生成、更新、标记完成 OKR: - 新增 ai_analyzed_commits 表实现增量标记,每条 commit 只分析一次 - objectives/keyResults 新增 source、sourceKey 字段区分 AI 生成与手动创建 - keyResults.status 扩展支持 completed 状态 - 新增 llm-client.ts 封装豆包 Ark API 调用(原生 fetch,零依赖) - 新增 okr-ai-sync.ts 核心服务:按仓库分组 → 构建 prompt → 调用 AI → 执行 actions - scheduler 在 Git 同步后自动触发 AI 分析(受 AI_ENABLED 开关控制) - 新增 POST /api/okr/ai-analyze 手动触发和 preview 预览端点 - 防重复三层保障:commit SHA 标记 + sourceKey 去重 + 项目 OKR 上下文 已验证:501 条 commits 全量分析,生成 37 个 Objectives、164 个 Key Results, 增量去重机制正常(重复调用返回 0 actions)。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
579 lines
20 KiB
TypeScript
579 lines
20 KiB
TypeScript
import { v4 as uuid } from 'uuid';
|
||
import { eq, isNull, sql, inArray } from 'drizzle-orm';
|
||
import { db } from '../db/index';
|
||
import {
|
||
gitCommits, aiAnalyzedCommits, projectRepos, projects,
|
||
objectives, keyResults, krLogs, syncLogs, users,
|
||
} from '../db/schema';
|
||
import { callLLM, parseLLMJson } from './llm-client';
|
||
import { dateToPeriod, recalcObjectiveProgress } from './okr';
|
||
import dayjs from 'dayjs';
|
||
|
||
// ── Types ──
|
||
|
||
interface AIAction {
|
||
type: 'create_objective' | 'create_kr' | 'update_progress' | 'complete_kr';
|
||
ownerId?: string;
|
||
objectiveId?: string;
|
||
krId?: string;
|
||
title?: string;
|
||
sourceKey?: string;
|
||
currentValue?: number;
|
||
newCurrentValue?: number;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
reasoning?: string;
|
||
keyResults?: {
|
||
title: string;
|
||
sourceKey: string;
|
||
currentValue: number;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
}[];
|
||
}
|
||
|
||
interface AIResponse {
|
||
actions: AIAction[];
|
||
summary: string;
|
||
}
|
||
|
||
interface CommitGroup {
|
||
repoName: string;
|
||
projectId: string;
|
||
projectName: string;
|
||
commits: {
|
||
sha: string;
|
||
authorName: string | null;
|
||
userId: string | null;
|
||
message: string | null;
|
||
additions: number;
|
||
deletions: number;
|
||
committedAt: Date;
|
||
}[];
|
||
}
|
||
|
||
// ── System Prompt ──
|
||
|
||
const SYSTEM_PROMPT = `你是一个开发团队的 OKR 管理助手。你的任务是分析 git commit 记录,管理项目的 OKR(目标与关键成果)。
|
||
|
||
你需要分析提交记录,输出 JSON 格式的操作指令:
|
||
|
||
判断逻辑:
|
||
- commit 包含 "完成"、"done"、"finished"、"实现完毕" 等完成语义 → 对应 KR 标记完成
|
||
- commit 是 feat/feature 类型但未完成 → 更新对应 KR 进度(根据描述估算百分比)
|
||
- commit 涉及的功能在现有 KR 中<><E4B8AD>存在 → 创建新 KR(需同时提供时间节点)
|
||
- fix/refactor/chore/docs 类 commit → 只在涉及明确功能时才操作
|
||
- 不要为同一个功能创建重复的 KR,已有相同 sourceKey 的直接更新
|
||
|
||
新建 Objective 规则:
|
||
- 如果提交涉及的功能没有归属到任何已有 Objective → 可以创建新 Objective
|
||
- Objective 的 startDate 和 endDate 由你根据提交内容和功能复杂度判断
|
||
- period 格式为 "YYYY-Qn"(如 "2026-Q2"),根据 endDate 推算
|
||
|
||
新建 KR 规则:
|
||
- targetValue = 100, unit = "%"
|
||
- 根据 commit 语义估算 currentValue(初步实现=30, 基本完成=70, 完成=100)
|
||
- sourceKey 用小写英文短横线格式(如 "user-login", "payment-module")
|
||
- title 用中文描述
|
||
- startDate 和 endDate 由你根据功能复杂度和提交时间判断
|
||
|
||
每个 action 中的 ownerId 必须是提交人的 userId,表示该 OKR 归谁负责。`;
|
||
|
||
// ── Core Functions ──
|
||
|
||
/**
|
||
* 获取未分析的 commits,按 repoName 分组
|
||
*/
|
||
async function gatherUnanalyzedCommits(): Promise<CommitGroup[]> {
|
||
// 获取所有已分析的 SHA
|
||
const analyzed = await db.select({ sha: aiAnalyzedCommits.commitSha }).from(aiAnalyzedCommits);
|
||
const analyzedSet = new Set(analyzed.map(a => a.sha));
|
||
|
||
// 获取所有 commits(有 userId 的,且 4 月份及之后的)
|
||
const cutoffDate = new Date('2026-01-01T00:00:00');
|
||
const allCommits = await db.select().from(gitCommits);
|
||
const unanalyzed = allCommits.filter(c =>
|
||
c.userId && !analyzedSet.has(c.sha) && new Date(c.committedAt) >= cutoffDate
|
||
);
|
||
|
||
if (unanalyzed.length === 0) return [];
|
||
|
||
// 获取 projectRepos 映射 repoName → projectId
|
||
const bindings = await db.select().from(projectRepos);
|
||
const repoToProject = new Map<string, string>();
|
||
for (const b of bindings) {
|
||
// 支持多种格式的 repoName 匹配
|
||
const name = extractRepoName(b.repoName);
|
||
repoToProject.set(name, b.projectId);
|
||
}
|
||
|
||
// 获取 projects 名称
|
||
const allProjects = await db.select().from(projects);
|
||
const projectMap = new Map(allProjects.map(p => [p.id, p.name]));
|
||
|
||
// 按 repoName 分组
|
||
const groups = new Map<string, CommitGroup>();
|
||
for (const commit of unanalyzed) {
|
||
const projectId = repoToProject.get(commit.repoName);
|
||
if (!projectId) continue; // 仓库未绑定项目,跳过
|
||
|
||
if (!groups.has(commit.repoName)) {
|
||
groups.set(commit.repoName, {
|
||
repoName: commit.repoName,
|
||
projectId,
|
||
projectName: projectMap.get(projectId) || commit.repoName,
|
||
commits: [],
|
||
});
|
||
}
|
||
groups.get(commit.repoName)!.commits.push({
|
||
sha: commit.sha,
|
||
authorName: commit.authorName,
|
||
userId: commit.userId,
|
||
message: commit.message,
|
||
additions: commit.additions || 0,
|
||
deletions: commit.deletions || 0,
|
||
committedAt: commit.committedAt,
|
||
});
|
||
}
|
||
|
||
// 按时间排序每组的 commits
|
||
for (const group of groups.values()) {
|
||
group.commits.sort((a, b) => new Date(a.committedAt).getTime() - new Date(b.committedAt).getTime());
|
||
}
|
||
|
||
return Array.from(groups.values());
|
||
}
|
||
|
||
/**
|
||
* 从各种格式中提取纯仓库名
|
||
*/
|
||
function extractRepoName(input: string): string {
|
||
let cleaned = input.trim().replace(/\.git$/, '');
|
||
if (cleaned.includes('://')) {
|
||
try {
|
||
const url = new URL(cleaned);
|
||
const parts = url.pathname.split('/').filter(Boolean);
|
||
return parts.length >= 2 ? parts[1] : parts[0];
|
||
} catch { /* fall through */ }
|
||
}
|
||
if (cleaned.includes('/')) {
|
||
return cleaned.split('/').pop()!;
|
||
}
|
||
return cleaned;
|
||
}
|
||
|
||
/**
|
||
* 构建 AI prompt
|
||
*/
|
||
async function buildUserPrompt(group: CommitGroup): Promise<string> {
|
||
// 获取该项目下所有现有 OKR
|
||
const existingObjs = await db.select().from(objectives)
|
||
.where(eq(objectives.projectId, group.projectId));
|
||
|
||
// 获取开发人员列表(从 commits 中提取 unique userId)
|
||
const authorIds = [...new Set(group.commits.map(c => c.userId).filter(Boolean))] as string[];
|
||
const authorUsers = authorIds.length > 0
|
||
? await db.select().from(users).where(inArray(users.id, authorIds))
|
||
: [];
|
||
const userMap = new Map(authorUsers.map(u => [u.id, u.displayName]));
|
||
|
||
let prompt = `仓库:${group.repoName}\n所属项目:${group.projectName}\n当前日期:${dayjs().format('YYYY-MM-DD')}\n\n`;
|
||
|
||
// 开发人员列表
|
||
prompt += `该仓库的开发人员(commit 提交人 → 系统用户映射):\n`;
|
||
for (const userId of authorIds) {
|
||
prompt += `- userId: "${userId}", 姓名: ${userMap.get(userId) || '未知'}\n`;
|
||
}
|
||
prompt += '\n';
|
||
|
||
// 已有 OKR
|
||
if (existingObjs.length > 0) {
|
||
prompt += '该项目已有的 OKR:\n';
|
||
for (const obj of existingObjs) {
|
||
const owner = obj.ownerId ? userMap.get(obj.ownerId) : null;
|
||
prompt += `Objective: id="${obj.id}", title="${obj.title}", 负责人=${owner || '未指定'}, period=${obj.period}, 时间=${obj.startDate || '?'}~${obj.endDate || '?'}, 进度=${obj.progress || 0}%\n`;
|
||
|
||
const krs = await db.select().from(keyResults)
|
||
.where(eq(keyResults.objectiveId, obj.id));
|
||
|
||
if (krs.length > 0) {
|
||
prompt += ' Key Results:\n';
|
||
for (const kr of krs) {
|
||
prompt += ` - id: "${kr.id}", title: "${kr.title}", sourceKey: "${kr.sourceKey || ''}", status: ${kr.status}, 进度: ${kr.currentValue || 0}/${kr.targetValue}, 时间: ${kr.startDate || '?'}~${kr.endDate || '?'}\n`;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
prompt += '该项目暂无 OKR 记录。\n';
|
||
}
|
||
|
||
// 新增 commits
|
||
prompt += `\n该仓库新增的提交记录(按时间排序,均为增量,之前的已处理过):\n`;
|
||
for (const commit of group.commits) {
|
||
const date = dayjs(commit.committedAt).format('YYYY-MM-DD HH:mm');
|
||
const sha7 = commit.sha.substring(0, 7);
|
||
const name = commit.authorName || '未知';
|
||
const userId = commit.userId || '?';
|
||
prompt += `- [${date}] 提交人: ${name}(userId=${userId}) ${sha7}: ${commit.message || '(无消息)'} (+${commit.additions}/-${commit.deletions})\n`;
|
||
}
|
||
|
||
prompt += `\n请分析以上提交,返回 JSON。注意:每个 action 都必须带 ownerId 字段,值为提交人的 userId,表示该 OKR 归谁负责。
|
||
{
|
||
"actions": [
|
||
{
|
||
"type": "create_objective",
|
||
"ownerId": "提交人的userId",
|
||
"title": "目标标题",
|
||
"startDate": "YYYY-MM-DD",
|
||
"endDate": "YYYY-MM-DD",
|
||
"reasoning": "为什么要创建这个目标",
|
||
"keyResults": [
|
||
{ "title": "...", "sourceKey": "...", "currentValue": 30, "startDate": "YYYY-MM-DD", "endDate": "YYYY-MM-DD" }
|
||
]
|
||
},
|
||
{
|
||
"type": "create_kr",
|
||
"ownerId": "提交人的userId",
|
||
"objectiveId": "已有目标的id",
|
||
"title": "...",
|
||
"sourceKey": "...",
|
||
"currentValue": 30,
|
||
"startDate": "YYYY-MM-DD",
|
||
"endDate": "YYYY-MM-DD",
|
||
"reasoning": "..."
|
||
},
|
||
{
|
||
"type": "update_progress",
|
||
"krId": "已有KR的id",
|
||
"newCurrentValue": 70,
|
||
"reasoning": "..."
|
||
},
|
||
{
|
||
"type": "complete_kr",
|
||
"krId": "已有KR的id",
|
||
"reasoning": "..."
|
||
}
|
||
],
|
||
"summary": "一句话总结该仓库近期开发动态"
|
||
}
|
||
如果没有需要操作的内容,返回 {"actions": [], "summary": "..."}`;
|
||
|
||
return prompt;
|
||
}
|
||
|
||
/**
|
||
* 执行 AI 返回的 actions
|
||
*/
|
||
async function executeActions(actions: AIAction[], projectId: string): Promise<number> {
|
||
let executedCount = 0;
|
||
const now = new Date();
|
||
|
||
for (const action of actions) {
|
||
try {
|
||
switch (action.type) {
|
||
case 'create_objective': {
|
||
if (!action.title || !action.ownerId) break;
|
||
|
||
const objId = uuid();
|
||
// 计算当前季度末
|
||
const now2 = dayjs();
|
||
const currentQuarter = Math.ceil((now2.month() + 1) / 3);
|
||
const quarterEnd = dayjs(`${now2.year()}-${currentQuarter * 3}-01`).endOf('month');
|
||
const endDate = action.endDate || quarterEnd.format('YYYY-MM-DD');
|
||
const period = dateToPeriod(endDate);
|
||
|
||
await db.insert(objectives).values({
|
||
id: objId,
|
||
title: action.title,
|
||
ownerId: action.ownerId,
|
||
projectId,
|
||
period,
|
||
startDate: action.startDate || dayjs().format('YYYY-MM-DD'),
|
||
endDate,
|
||
progress: 0,
|
||
source: 'ai_generated',
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
});
|
||
|
||
// 创建附带的 KRs
|
||
if (action.keyResults?.length) {
|
||
for (const krData of action.keyResults) {
|
||
// sourceKey 去重检查
|
||
if (krData.sourceKey) {
|
||
const existing = await db.select().from(keyResults)
|
||
.where(eq(keyResults.sourceKey, krData.sourceKey));
|
||
if (existing.some(kr => kr.objectiveId === objId)) continue;
|
||
}
|
||
|
||
const krId = uuid();
|
||
await db.insert(keyResults).values({
|
||
id: krId,
|
||
objectiveId: objId,
|
||
title: krData.title,
|
||
targetValue: 100,
|
||
currentValue: krData.currentValue || 0,
|
||
unit: '%',
|
||
weight: 1,
|
||
status: krData.currentValue >= 100 ? 'completed' : 'active',
|
||
startDate: krData.startDate || action.startDate,
|
||
endDate: krData.endDate || action.endDate,
|
||
source: 'ai_generated',
|
||
sourceKey: krData.sourceKey || null,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
});
|
||
|
||
await addAILog(krId, 'created', `AI 自动创建: ${action.reasoning || ''}`);
|
||
executedCount++;
|
||
}
|
||
}
|
||
|
||
await recalcObjectiveProgress(objId);
|
||
executedCount++;
|
||
console.info(`[AI-OKR] Created objective: ${action.title}`);
|
||
break;
|
||
}
|
||
|
||
case 'create_kr': {
|
||
if (!action.objectiveId || !action.title) break;
|
||
|
||
// 验证 objective 存在
|
||
const obj = await db.query.objectives.findFirst({
|
||
where: eq(objectives.id, action.objectiveId),
|
||
});
|
||
if (!obj) {
|
||
console.warn(`[AI-OKR] Objective ${action.objectiveId} not found, skipping create_kr`);
|
||
break;
|
||
}
|
||
|
||
// sourceKey 去重
|
||
if (action.sourceKey) {
|
||
const existing = await db.select().from(keyResults)
|
||
.where(eq(keyResults.sourceKey, action.sourceKey));
|
||
if (existing.some(kr => kr.objectiveId === action.objectiveId)) {
|
||
// 已存在同 sourceKey,转为更新进度
|
||
const existingKR = existing.find(kr => kr.objectiveId === action.objectiveId)!;
|
||
if (action.currentValue && action.currentValue > (existingKR.currentValue || 0)) {
|
||
await db.update(keyResults)
|
||
.set({ currentValue: action.currentValue, updatedAt: now })
|
||
.where(eq(keyResults.id, existingKR.id));
|
||
await addAILog(existingKR.id, 'progress_update',
|
||
`AI 更新(sourceKey 重复转更新): ${existingKR.currentValue} → ${action.currentValue}`);
|
||
await recalcObjectiveProgress(action.objectiveId);
|
||
}
|
||
executedCount++;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const krId = uuid();
|
||
await db.insert(keyResults).values({
|
||
id: krId,
|
||
objectiveId: action.objectiveId,
|
||
title: action.title,
|
||
targetValue: 100,
|
||
currentValue: action.currentValue || 0,
|
||
unit: '%',
|
||
weight: 1,
|
||
status: (action.currentValue || 0) >= 100 ? 'completed' : 'active',
|
||
startDate: action.startDate || null,
|
||
endDate: action.endDate || null,
|
||
source: 'ai_generated',
|
||
sourceKey: action.sourceKey || null,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
});
|
||
|
||
await addAILog(krId, 'created', `AI 自动创建: ${action.reasoning || ''}`);
|
||
await recalcObjectiveProgress(action.objectiveId);
|
||
executedCount++;
|
||
console.info(`[AI-OKR] Created KR: ${action.title}`);
|
||
break;
|
||
}
|
||
|
||
case 'update_progress': {
|
||
if (!action.krId || action.newCurrentValue === undefined) break;
|
||
|
||
const kr = await db.query.keyResults.findFirst({
|
||
where: eq(keyResults.id, action.krId),
|
||
});
|
||
if (!kr) {
|
||
console.warn(`[AI-OKR] KR ${action.krId} not found, skipping update_progress`);
|
||
break;
|
||
}
|
||
|
||
// 只允许进度前进,不允许后退
|
||
if (action.newCurrentValue <= (kr.currentValue || 0)) break;
|
||
|
||
const clampedValue = Math.min(action.newCurrentValue, kr.targetValue);
|
||
await db.update(keyResults)
|
||
.set({ currentValue: clampedValue, updatedAt: now })
|
||
.where(eq(keyResults.id, action.krId));
|
||
|
||
await addAILog(action.krId, 'progress_update',
|
||
`AI 更新进度: ${kr.currentValue} → ${clampedValue},${action.reasoning || ''}`);
|
||
await recalcObjectiveProgress(kr.objectiveId);
|
||
executedCount++;
|
||
console.info(`[AI-OKR] Updated KR progress: ${kr.title} → ${clampedValue}`);
|
||
break;
|
||
}
|
||
|
||
case 'complete_kr': {
|
||
if (!action.krId) break;
|
||
|
||
const kr = await db.query.keyResults.findFirst({
|
||
where: eq(keyResults.id, action.krId),
|
||
});
|
||
if (!kr) {
|
||
console.warn(`[AI-OKR] KR ${action.krId} not found, skipping complete_kr`);
|
||
break;
|
||
}
|
||
|
||
if (kr.status === 'completed') break; // 已完成则跳过
|
||
|
||
await db.update(keyResults)
|
||
.set({ status: 'completed', currentValue: kr.targetValue, updatedAt: now })
|
||
.where(eq(keyResults.id, action.krId));
|
||
|
||
await addAILog(action.krId, 'completed',
|
||
`AI 判定完成: ${action.reasoning || ''}`);
|
||
await recalcObjectiveProgress(kr.objectiveId);
|
||
executedCount++;
|
||
console.info(`[AI-OKR] Completed KR: ${kr.title}`);
|
||
break;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
const msg = err instanceof Error ? err.message : String(err);
|
||
console.error(`[AI-OKR] Error executing action ${action.type}: ${msg}`);
|
||
}
|
||
}
|
||
|
||
return executedCount;
|
||
}
|
||
|
||
/**
|
||
* 写 KR 操作日志(AI 系统操作)
|
||
*/
|
||
async function addAILog(krId: string, action: string, detail: string) {
|
||
await db.insert(krLogs).values({
|
||
id: uuid(),
|
||
krId,
|
||
action,
|
||
detail,
|
||
operatorId: null,
|
||
operatorName: 'AI System',
|
||
createdAt: new Date(),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 标记 commits 为已分析
|
||
*/
|
||
async function markCommitsAnalyzed(shas: string[], batchId: string) {
|
||
const now = new Date();
|
||
for (const sha of shas) {
|
||
try {
|
||
await db.insert(aiAnalyzedCommits).values({
|
||
id: uuid(),
|
||
commitSha: sha,
|
||
batchId,
|
||
createdAt: now,
|
||
});
|
||
} catch {
|
||
// 可能已经存在(unique constraint),忽略
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Main Entry ──
|
||
|
||
export interface AnalyzeResult {
|
||
totalCommits: number;
|
||
reposProcessed: number;
|
||
actionsExecuted: number;
|
||
summaries: { repo: string; summary: string }[];
|
||
}
|
||
|
||
/**
|
||
* 分析未处理的 commits 并自动更新 OKR
|
||
* @param dryRun 如果为 true,只返回 AI 建议不执行
|
||
*/
|
||
export async function analyzeCommitsForOKR(dryRun = false): Promise<AnalyzeResult> {
|
||
const startTime = Date.now();
|
||
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 actionsExecuted = 0;
|
||
const summaries: { repo: string; summary: string }[] = [];
|
||
|
||
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}`);
|
||
}
|
||
}
|
||
|
||
// 记录 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 };
|
||
}
|