后端: - 事件流模型(project_cost_events / project_revenue_events)+ launchedAt 截断 - 3 大业务体系归属(airhubs/airflow/aircore) + 项目类型(hw/sw) + identifier 自动生成 - AI 三件套推荐(category + bizSystem + projectType) - 营收 mock API + 外部对接规范 + 资产摊销 cron - 5 个 migration(0003 ROI 引擎 / 0004 driver factors / 0005 biz system) - 单测 11/11 过 前端: - 项目级 ROI 看板:4 KPI 卡片 + 折线图(周/月/年)+ 成本/产出事件流并排 - 全公司决策罗盘:3 大 ROI 指标 + 业务线堆叠 + 分类筛选 chip - 项目列表 + 侧边栏:按产品线分组(可折叠 + localStorage 持久化) - Admin: ROI 策略配置 + 项目映射 + 未映射收容 数据: - 23 项目全部 AI 自动分类 + 自动 identifier(airhubs-hw-001 这种) - launchedAt 按各项目首次 commit 时间设置 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
/**
|
|
* 一次性脚本:对所有项目跑 AI 三件套分类(category + bizSystem + projectType),
|
|
* 同时自动生成新 identifier(airhubs-hw-001 这种)+ 同步更新 mapping。
|
|
*
|
|
* 用法:
|
|
* bun run scripts/ai-classify-all.ts # 仅处理未完整分类的项目
|
|
* bun run scripts/ai-classify-all.ts --force # 强制重跑所有项目(覆盖)
|
|
*/
|
|
import dayjs from 'dayjs';
|
|
import { eq } from 'drizzle-orm';
|
|
import { db } from '../src/db/index';
|
|
import { projects } from '../src/db/schema';
|
|
import { suggestProjectTag } from '../src/services/roi/ai-tag-suggester';
|
|
import { applyAutoIdentifier } from '../src/services/roi/identifier-generator';
|
|
|
|
const force = process.argv.includes('--force');
|
|
|
|
const all = await db.select().from(projects);
|
|
console.log(`Total projects: ${all.length}, force=${force}`);
|
|
|
|
let okCount = 0, skipCount = 0, failCount = 0;
|
|
const startedAt = Date.now();
|
|
|
|
for (const p of all) {
|
|
const label = `${p.planeIdentifier || p.identifier || '?'} (${p.name})`;
|
|
|
|
const alreadyFull = p.category && p.bizSystem && p.projectType;
|
|
if (!force && alreadyFull) {
|
|
console.log(` ⊘ SKIP ${label} — fully classified (${p.bizSystem}/${p.projectType}/${p.category})`);
|
|
skipCount += 1;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
console.log(` → AI ${label} ...`);
|
|
const sug = await suggestProjectTag(p.id);
|
|
const launchedAt = p.launchedAt ?? p.createdAt ?? dayjs().subtract(90, 'day').toDate();
|
|
|
|
const needsAsset = sug.suggestedCategory === 'moat';
|
|
const vAsset = needsAsset ? (p.vAsset ?? 100_000) : (p.vAsset ?? null);
|
|
|
|
// 1. 更新分类字段
|
|
await db.update(projects).set({
|
|
category: sug.suggestedCategory,
|
|
launchedAt: launchedAt as any,
|
|
vAsset: vAsset,
|
|
updatedAt: new Date(),
|
|
}).where(eq(projects.id, p.id));
|
|
|
|
// 2. 自动生成新 identifier + 同步 mapping
|
|
const newId = await applyAutoIdentifier(p.id, sug.suggestedBizSystem, sug.suggestedProjectType);
|
|
|
|
console.log(` ✓ ${newId} | ${sug.suggestedBizSystem}/${sug.suggestedProjectType}/${sug.suggestedCategory} conf=${sug.confidence}`);
|
|
console.log(` ↳ ${sug.reasoning.slice(0, 80)}`);
|
|
okCount += 1;
|
|
} catch (e) {
|
|
console.error(` ✗ FAIL ${label}: ${(e as Error).message.slice(0, 200)}`);
|
|
failCount += 1;
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
console.log('');
|
|
console.log(`Done. ok=${okCount} skipped=${skipCount} failed=${failCount} elapsed=${elapsed}s`);
|
|
process.exit(0);
|