import { v4 as uuid } from 'uuid'; import { and, desc, eq, gte } from 'drizzle-orm'; import dayjs from 'dayjs'; import { db } from '../../db/index'; import { projects, projectRepos, gitCommits, objectives, roiDriverFactors, syncLogs, } from '../../db/schema'; import { callLLM, parseLLMJson } from '../llm-client'; import { aggregate } from './aggregator'; export interface DriverFactor { type: '现金流驱动' | '降本增效驱动' | '技术资产驱动'; text: string; } const SYSTEM_PROMPT = `你是软件项目价值分析师。给定项目本月 ROI 数据和近期工作内容,生成 1-3 条"价值驱动因子"文案,告诉管理者这个项目的价值来源是什么。 三种驱动因子类型: - 现金流驱动: 项目直接产生营收,推动现金流入 - 降本增效驱动: 项目通过工具化/自动化节省了内部成本 - 技术资产驱动: 项目沉淀了技术能力/数据/算法,形成长期资产 请基于真实数据判断该项目最显著的 1-3 个驱动因子。回复严格 JSON 格式: { "factors": [ { "type": "现金流驱动" | "降本增效驱动" | "技术资产驱动", "text": "60字内的简短说明" } ] } 不要添加其他字段或文本。每条 text 必须 ≤60 字,客观陈述,不要使用营销语言。`; const ALLOWED_TYPES = new Set(['现金流驱动', '降本增效驱动', '技术资产驱动']); /** * 为单个项目生成"上月"的驱动因子,写入 roi_driver_factors 表。 * periodKey = 上月 YYYY-MM */ export async function generateDriverFactorsForProject(projectId: string, asOf: Date = new Date()): Promise { const lastMonth = dayjs(asOf).subtract(1, 'month'); const periodKey = lastMonth.format('YYYY-MM'); // 上月 ROI const monthStart = lastMonth.startOf('month').toDate(); const monthEnd = lastMonth.endOf('month').toDate(); const monthAgg = await aggregate(projectId, monthStart, monthEnd); // 累计 ROI(从 launchedAt 起) const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1); if (!project) throw new Error(`Project not found: ${projectId}`); const launchedAt = project.launchedAt ? new Date(project.launchedAt) : monthStart; const lifetimeAgg = await aggregate(projectId, launchedAt, monthEnd); // 近期 commits 摘要 const repos = await db.select().from(projectRepos).where(eq(projectRepos.projectId, projectId)); const repoNames = new Set(repos.map(r => r.repoName)); let commitSummary = '(无近期提交)'; if (repoNames.size > 0) { const recent = await db.select().from(gitCommits) .where(gte(gitCommits.committedAt, monthStart)) .orderBy(desc(gitCommits.committedAt)) .limit(30); const projCommits = recent.filter(c => repoNames.has(c.repoName)); if (projCommits.length > 0) { commitSummary = projCommits .map(c => `- ${(c.message || '').split('\n')[0].slice(0, 80)}`) .slice(0, 15) .join('\n'); } } // OKR 进展 const objs = await db.select().from(objectives).where(eq(objectives.projectId, projectId)).limit(5); const okrSummary = objs.length > 0 ? objs.map(o => `- ${o.title} (进度 ${Math.round((o.progress || 0) * 100)}%)`).join('\n') : '(无 OKR)'; const userPrompt = `项目: ${project.name} (定位: ${project.category || '未打标'}) 上月 ROI 数据 (${periodKey}): - 总成本: ¥${monthAgg.totalCost.toLocaleString()} - 总产出: ¥${monthAgg.totalRevenue.toLocaleString()} - 月 ROI: ${monthAgg.roiValue === null ? 'N/A' : monthAgg.roiValue + '%'} - 直接营收占比: ¥${monthAgg.revenueBreakdown.directRevenue} - 节约成本占比: ¥${monthAgg.revenueBreakdown.savedCost} - 资产增值占比: ¥${monthAgg.revenueBreakdown.assetValueAdd} 累计 ROI (立项至今): ${lifetimeAgg.roiValue === null ? 'N/A' : lifetimeAgg.roiValue + '%'} 上月关键 commits: ${commitSummary} OKR 进展: ${okrSummary} 请输出 JSON 驱动因子。`; const raw = await callLLM(SYSTEM_PROMPT, userPrompt); const parsed = parseLLMJson<{ factors: DriverFactor[] }>(raw); // 校验 if (!Array.isArray(parsed.factors)) throw new Error('LLM response missing factors array'); const validFactors = parsed.factors .filter(f => f && ALLOWED_TYPES.has(f.type) && typeof f.text === 'string') .map(f => ({ type: f.type, text: f.text.slice(0, 80) })) .slice(0, 3); if (validFactors.length === 0) throw new Error('LLM returned no valid factors'); // upsert: 先 delete 旧的(同 project + period),再 insert 新的 await db.delete(roiDriverFactors).where(and( eq(roiDriverFactors.projectId, projectId), eq(roiDriverFactors.periodKey, periodKey), )); await db.insert(roiDriverFactors).values({ id: uuid(), projectId, periodKey, factors: validFactors, context: { monthRoi: monthAgg.roiValue, lifetimeRoi: lifetimeAgg.roiValue, monthCost: monthAgg.totalCost, monthRevenue: monthAgg.totalRevenue, }, generatedAt: new Date(), }); return validFactors; } /** * 月度 cron:为所有打标项目生成驱动因子。失败的项目记录但不中断。 */ export async function runMonthlyDriverFactorsGeneration(): Promise { const startedAt = Date.now(); let okCount = 0, failCount = 0; const errors: string[] = []; // 仅为已打标的项目生成 const allProjects = await db.select().from(projects); const candidates = allProjects.filter(p => p.category !== null); for (const p of candidates) { try { await generateDriverFactorsForProject(p.id); okCount += 1; } catch (e) { failCount += 1; const msg = `${p.identifier || p.id}: ${(e as Error).message}`; errors.push(msg); console.warn('[ROI-AI-DRIVER]', msg); } // 简单速率控制:每项目间隔 1 秒,避免 LLM 限流 await new Promise(r => setTimeout(r, 1000)); } const elapsed = Date.now() - startedAt; await db.insert(syncLogs).values({ id: uuid(), source: 'roi_ai_driver', status: failCount === 0 ? 'success' : 'error', message: `driver factors: ok=${okCount} fail=${failCount} elapsed=${elapsed}ms${errors.length > 0 ? ' errors=' + errors.slice(0, 3).join('|') : ''}`, recordsProcessed: okCount, syncedAt: new Date(), }).catch(() => {}); }