后端: - 事件流模型(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>
61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
/**
|
|
* 一次性脚本:用 AI 给所有未打标的项目自动建议 + 应用 category。
|
|
* 已打标的项目跳过(避免覆盖人工决策)。
|
|
* - 默认立项日:取项目 created_at(若有)否则用 today-90天
|
|
* - 护城河项目需要 V_asset,AI 推荐 moat 时默认填 100,000(占位,管理员后续手填)
|
|
* 用法: bun run scripts/ai-tag-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';
|
|
|
|
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.identifier || '?'} (${p.name})`;
|
|
if (!force && p.category) {
|
|
console.log(` ⊘ SKIP ${label} — already tagged as ${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();
|
|
|
|
// 护城河默认 V_asset 占位
|
|
const needsAsset = sug.suggestedCategory === 'moat';
|
|
const vAsset = needsAsset ? (p.vAsset ?? 100_000) : (p.vAsset ?? null);
|
|
|
|
await db.update(projects).set({
|
|
category: sug.suggestedCategory,
|
|
launchedAt: launchedAt as any,
|
|
vAsset: vAsset,
|
|
updatedAt: new Date(),
|
|
}).where(eq(projects.id, p.id));
|
|
|
|
console.log(` ✓ ${sug.suggestedCategory} (conf=${sug.confidence}) — ${sug.reasoning.slice(0, 60)}`);
|
|
okCount += 1;
|
|
} catch (e) {
|
|
console.error(` ✗ FAIL ${label}: ${(e as Error).message.slice(0, 200)}`);
|
|
failCount += 1;
|
|
}
|
|
|
|
// 1 秒间隔避免 LLM 限流
|
|
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);
|