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>
422 lines
15 KiB
Markdown
422 lines
15 KiB
Markdown
# AI 驱动的 OKR 自动生成与同步
|
||
|
||
## Context
|
||
|
||
DevPerf 已具备完整的 Git 同步管线(Gitea → gitCommits 表)和 OKR CRUD 系统。
|
||
当前 OKR 需人工创建,缺少与 Git 提交的联动。
|
||
|
||
**目标**:每次 Git 同步后,AI 分析新增 commit messages,按仓库维度:
|
||
- 新增 KR — commit 提到的功能点尚无对应 KR
|
||
- 更新进度 — commit 涉及已有 KR 的功能
|
||
- 标记完成 — commit 明确表示功能已完成
|
||
|
||
**分析粒度**:按仓库(每个仓库的新增 commits 做一次 AI 分析,prompt 中包含提交人信息)
|
||
**OKR 归属**:commit 的提交人 = KR/Objective 的负责人(ownerId),通过 gitCommits.userId 关联
|
||
**增量机制**:每条 commit 只分析一次,分析后打标记,后续同步不再重复处理
|
||
|
||
---
|
||
|
||
## 第一阶段:Schema 变更
|
||
|
||
**文件**: `backend/src/db/schema.ts`
|
||
|
||
### 1a. 新增 `ai_analyzed_commits` 表(增量标记,防止重复分析)
|
||
|
||
```
|
||
aiAnalyzedCommits:
|
||
id varchar(50) PK
|
||
commitSha varchar(200) NOT NULL UNIQUE -- 对应 gitCommits.sha
|
||
batchId varchar(50) NOT NULL -- 一次 AI 调用的批次 ID
|
||
createdAt datetime NOT NULL
|
||
```
|
||
|
||
**核心作用**:每次同步后,只提取 `gitCommits` 中尚未出现在此表的 commit 送给 AI。分析完成后立即写入标记,确保该 commit 不会被重复处理。
|
||
|
||
### 1b. objectives 表加字段
|
||
|
||
- `source` varchar(50) default `'manual'` — 值: `'manual'` | `'ai_generated'`
|
||
|
||
### 1c. keyResults 表加字段
|
||
|
||
- `source` varchar(50) default `'manual'` — 值: `'manual'` | `'ai_generated'`
|
||
- `sourceKey` varchar(300) nullable — AI 分配的功能标识符(如 `"user-login"`),用于语义去重
|
||
|
||
### 1d. keyResults.status 扩展
|
||
|
||
现有 `'active' | 'paused' | 'cancelled'`,新增 `'completed'`。
|
||
字段类型是 varchar,无需改 DDL,只需让 `recalcObjectiveProgress` 正确处理 completed。
|
||
|
||
### 1e. syncLogs.source 枚举扩展
|
||
|
||
将 `mysqlEnum('source', ['plane', 'gitea'])` 改为 `['plane', 'gitea', 'ai_okr']`。
|
||
|
||
---
|
||
|
||
## 第二阶段:AI 配置与 LLM 客户端(豆包 Doubao)
|
||
|
||
### 2a. 环境变量 (`backend/src/config.ts`)
|
||
|
||
新增:
|
||
```typescript
|
||
AI_ENABLED: z.coerce.boolean().default(false),
|
||
AI_API_KEY: z.string().default(''),
|
||
AI_MODEL: z.string().default('doubao-seed-2-0-pro-260215'),
|
||
AI_BASE_URL: z.string().default('https://ark.cn-beijing.volces.com/api/v3'),
|
||
```
|
||
|
||
**.env 示例**:
|
||
```env
|
||
AI_ENABLED=true
|
||
AI_API_KEY=846b6981-9954-4c58-bb39-63079393bdb8
|
||
AI_MODEL=doubao-seed-2-0-pro-260215
|
||
AI_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||
```
|
||
|
||
### 2b. 新建 `backend/src/services/llm-client.ts`
|
||
|
||
使用豆包(火山引擎 Ark)API,原生 fetch 实现,零额外依赖。
|
||
|
||
**豆包 API 调用格式**:
|
||
|
||
```typescript
|
||
// POST https://ark.cn-beijing.volces.com/api/v3/chat/completions
|
||
// Header: Authorization: Bearer {AI_API_KEY}
|
||
// Header: Content-Type: application/json
|
||
|
||
export async function callLLM(systemPrompt: string, userPrompt: string): Promise<string> {
|
||
const url = `${config.AI_BASE_URL}/chat/completions`;
|
||
|
||
const body = {
|
||
model: config.AI_MODEL, // "doubao-seed-2-0-pro-260215"
|
||
messages: [
|
||
{ role: "system", content: systemPrompt },
|
||
{ role: "user", content: userPrompt }
|
||
],
|
||
temperature: 0.3, // 低温度保证输出稳定
|
||
response_format: { type: "json_object" } // 强制 JSON 输出
|
||
};
|
||
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${config.AI_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(body),
|
||
signal: AbortSignal.timeout(60000), // 60s 超时
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Doubao API error: ${response.status} ${await response.text()}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
return data.choices[0].message.content;
|
||
}
|
||
```
|
||
|
||
**要点**:
|
||
- 豆包 Ark API 兼容 OpenAI `/chat/completions` 格式
|
||
- 使用 `response_format: { type: "json_object" }` 强制返回 JSON
|
||
- 超时 60s,失败重试 1 次(间隔 3s)
|
||
- `temperature: 0.3` 保证分析结果稳定一致
|
||
|
||
---
|
||
|
||
## 第三阶段:核心服务 — `backend/src/services/okr-ai-sync.ts`
|
||
|
||
### 整体流程
|
||
|
||
```
|
||
analyzeCommitsForOKR()
|
||
│
|
||
├─ 1. gatherUnanalyzedCommits()
|
||
│ SELECT gc.* FROM git_commits gc
|
||
│ LEFT JOIN ai_analyzed_commits aac ON gc.sha = aac.commit_sha
|
||
│ WHERE aac.id IS NULL
|
||
│ → 按 repoName 分组(每个仓库一批)
|
||
│
|
||
├─ 2. 遍历每个仓库的未分析 commits:
|
||
│ a. 通过 projectRepos 找到该仓库关联的 projectId
|
||
│ b. 获取该项目当前所有 Objectives + KRs(含 sourceKey)
|
||
│ c. 构建 AI prompt(该仓库的新 commits,含每条 commit 的提交人信息 + 项目现有 OKR)
|
||
│ d. 调用豆包 AI → 解析 JSON 响应
|
||
│ e. 执行 AI 返回的 actions:
|
||
│ - create_objective → 创建时 ownerId = commit 提交人的 userId(AI 返回 authorUserId)
|
||
│ - create_kr → 先查 sourceKey 防重 → 不存在才创建,归属到对应提交人
|
||
│ - update_progress → 更新 currentValue + 写 krLog
|
||
│ - complete_kr → status='completed' + 写 krLog
|
||
│ f. 重算 Objective progress
|
||
│ g. 标记这批 commits 为已分析(写入 aiAnalyzedCommits)
|
||
│
|
||
└─ 3. 写入 syncLogs (source='ai_okr')
|
||
```
|
||
|
||
### 增量更新机制(核心)
|
||
|
||
```
|
||
第一次同步:
|
||
gitCommits: [A, B, C, D, E]
|
||
aiAnalyzedCommits: []
|
||
→ 未分析: [A, B, C, D, E] → 全部送 AI → 分析后标记 [A, B, C, D, E]
|
||
|
||
第二次同步(新增了 F, G):
|
||
gitCommits: [A, B, C, D, E, F, G]
|
||
aiAnalyzedCommits: [A, B, C, D, E]
|
||
→ 未分析: [F, G] → 只送 F, G 给 AI → 分析后标记 [F, G]
|
||
|
||
第三次同步(无新增):
|
||
→ 未分析: [] → 跳过,不调用 AI
|
||
```
|
||
|
||
### AI Prompt 设计
|
||
|
||
**System Prompt**:
|
||
```
|
||
你是一个开发团队的 OKR 管理助手。你的任务是分析 git commit 记录,管理项目的 OKR(目标与关键成果)。
|
||
|
||
你需要分析提交记录,输出 JSON 格式的操作指令:
|
||
|
||
判断逻辑:
|
||
- commit 包含 "完成"、"done"、"finished"、"实现完毕" 等完成语义 → 对应 KR 标记完成
|
||
- commit 是 feat/feature 类型但未完成 → 更新对应 KR 进度(根据描述估算百分比)
|
||
- commit 涉及的功能在现有 KR 中不存在 → 创建新 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 由你根据功能复杂度和提交时间判断
|
||
```
|
||
|
||
**User Prompt 模板**:
|
||
```
|
||
仓库:{{repoName}}
|
||
所属项目:{{projectName}}
|
||
当前日期:{{today}}
|
||
|
||
该项目已有的 OKR:
|
||
{{#each existingObjectives}}
|
||
Objective: id={{id}}, title="{{title}}", period={{period}}, 时间={{startDate}}~{{endDate}}, 进度={{progress}}%
|
||
Key Results:
|
||
{{#each keyResults}}
|
||
- id: {{id}}, title: "{{title}}", sourceKey: "{{sourceKey}}", status: {{status}}, 进度: {{currentValue}}/{{targetValue}}, 时间: {{startDate}}~{{endDate}}
|
||
{{/each}}
|
||
{{/each}}
|
||
|
||
{{如果没有已有 OKR: "该项目暂无 OKR 记录。"}}
|
||
|
||
该仓库的开发人员(commit 提交人 → 系统用户映射):
|
||
{{#each authors}}
|
||
- userId: "{{userId}}", 姓名: {{displayName}}
|
||
{{/each}}
|
||
|
||
该仓库新增的提交记录(按时间排序,均为增量,之前的已处理过):
|
||
{{#each commits}}
|
||
- [{{committedAt}}] 提交人: {{authorName}}(userId={{userId}}) {{sha前7位}}: {{message}} (+{{additions}}/-{{deletions}})
|
||
{{/each}}
|
||
|
||
请分析以上提交,返回 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": number, "startDate": "YYYY-MM-DD", "endDate": "YYYY-MM-DD" }
|
||
]
|
||
},
|
||
{
|
||
"type": "create_kr",
|
||
"ownerId": "提交人的userId",
|
||
"objectiveId": "已有目标的id",
|
||
"title": "...",
|
||
"sourceKey": "...",
|
||
"currentValue": number,
|
||
"startDate": "YYYY-MM-DD",
|
||
"endDate": "YYYY-MM-DD",
|
||
"reasoning": "..."
|
||
},
|
||
{
|
||
"type": "update_progress",
|
||
"krId": "已有KR的id",
|
||
"newCurrentValue": number,
|
||
"reasoning": "..."
|
||
},
|
||
{
|
||
"type": "complete_kr",
|
||
"krId": "已有KR的id",
|
||
"reasoning": "..."
|
||
}
|
||
],
|
||
"summary": "一句话总结该仓库近期开发动态"
|
||
}
|
||
如果没有需要操作的内容,返回 {"actions": [], "summary": "..."}
|
||
```
|
||
|
||
### 防重复策略(三层保障)
|
||
|
||
| 层级 | 机制 | 说明 |
|
||
|------|------|------|
|
||
| **Commit 级** | `aiAnalyzedCommits` 表 | 已分析的 SHA 打标记,下次同步时过滤掉,不再送给 AI |
|
||
| **KR 级** | `sourceKey` 唯一性检查 | 创建前查 DB,同 Objective 下同 sourceKey 只允许一条,重复则转为 update |
|
||
| **Objective 级** | 项目下已有 OKR 传入 prompt | AI 看到完整上下文,避免建议创建重复目标 |
|
||
|
||
### OKR 负责人归属
|
||
|
||
- commit 有 `userId`(通过 author-matching 映射到系统用户)
|
||
- AI prompt 中包含每条 commit 的提交人信息(userId + displayName)
|
||
- AI 返回 action 时必须携带 `ownerId`,即该 OKR 归哪个提交人负责
|
||
- 执行时:`create_objective` 的 `ownerId` = AI 指定的 userId,`create_kr` 同理
|
||
- 跳过 `userId` 为 null 的 commits(作者未映射到系统用户)
|
||
|
||
**示例**:仓库 rtc_backend 有张三、李四两个人提交代码
|
||
- 张三的 commit 涉及"用户登录" → AI 创建 KR 归属张三
|
||
- 李四的 commit 涉及"支付模块" → AI 创建 KR 归属李四
|
||
- 同一个 Objective 下可以有不同负责人的 KR
|
||
|
||
### 时间节点(由 AI 判断)
|
||
|
||
- **不再固定取当前季度**,而是由 AI 根据以下信息决定:
|
||
- commit 的提交时间
|
||
- 功能的复杂度(从 commit 内容推断)
|
||
- 已有 OKR 的时间范围(避免冲突)
|
||
- AI 返回的 `startDate`、`endDate` 直接写入 Objective 和 KR
|
||
- `period` 根据 endDate 自动推算(复用已有 `dateToPeriod()`)
|
||
- Objective 创建后,其日期范围由 AI 决定,后续也可由 AI 在新的分析中调整
|
||
|
||
### 完成判定
|
||
|
||
AI 返回 `complete_kr` action 时:
|
||
1. `currentValue = targetValue`(即 100)
|
||
2. `status = 'completed'`
|
||
3. 写 krLog:`action='completed'`, `detail='AI 根据 commit {sha} 判定完成:{reasoning}'`
|
||
4. `recalcObjectiveProgress()` 时 completed 的 KR 按 100% 计入
|
||
|
||
---
|
||
|
||
## 第四阶段:集成到同步流程
|
||
|
||
### 4a. 修改 `backend/src/sync/scheduler.ts`
|
||
|
||
```typescript
|
||
import { analyzeCommitsForOKR } from '../services/okr-ai-sync';
|
||
|
||
// 在 syncGitea() 之后调用
|
||
giteaJob = new Cron('0 2,19 * * *', async () => {
|
||
await syncGitea();
|
||
if (config.AI_ENABLED) {
|
||
console.info('[SCHEDULER] AI OKR 分析开始...');
|
||
await analyzeCommitsForOKR().catch(e =>
|
||
console.error('[SCHEDULER] AI OKR analysis failed:', e)
|
||
);
|
||
}
|
||
});
|
||
```
|
||
|
||
### 4b. 新增 API 端点 (`backend/src/routes/okr.ts`)
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/okr/ai-analyze` | 手动触发 AI 分析(admin/manager) |
|
||
| POST | `/api/okr/ai-analyze/preview` | 预览模式:返回 AI 建议但不执行 |
|
||
|
||
### 4c. 修改 `backend/src/services/okr.ts`
|
||
|
||
- 导出 `dateToPeriod()` 函数供 okr-ai-sync 使用
|
||
- `recalcObjectiveProgress()` 增加对 `status === 'completed'` 的处理(按 100% 计入进度)
|
||
|
||
---
|
||
|
||
## 第五阶段:DB 迁移
|
||
|
||
新增迁移文件 `backend/drizzle/0002_add_ai_okr_fields.sql`:
|
||
|
||
```sql
|
||
-- 新表:增量标记(已分析过的 commit 不再重复处理)
|
||
CREATE TABLE ai_analyzed_commits (
|
||
id VARCHAR(50) PRIMARY KEY,
|
||
commit_sha VARCHAR(200) NOT NULL,
|
||
batch_id VARCHAR(50) NOT NULL,
|
||
created_at DATETIME NOT NULL,
|
||
UNIQUE INDEX uniq_analyzed_sha (commit_sha)
|
||
);
|
||
|
||
-- objectives 加字段
|
||
ALTER TABLE objectives ADD COLUMN source VARCHAR(50) DEFAULT 'manual';
|
||
|
||
-- key_results 加字段
|
||
ALTER TABLE key_results ADD COLUMN source VARCHAR(50) DEFAULT 'manual';
|
||
ALTER TABLE key_results ADD COLUMN source_key VARCHAR(300) NULL;
|
||
|
||
-- syncLogs source 枚举扩展
|
||
ALTER TABLE sync_logs MODIFY COLUMN source ENUM('plane', 'gitea', 'ai_okr') NOT NULL;
|
||
```
|
||
|
||
---
|
||
|
||
## 关键文件清单
|
||
|
||
| 文件 | 操作 | 说明 |
|
||
|------|------|------|
|
||
| `backend/src/db/schema.ts` | 修改 | 新表 + 新字段 |
|
||
| `backend/src/config.ts` | 修改 | 豆包 AI 环境变量 |
|
||
| `backend/src/services/llm-client.ts` | **新建** | 豆包 API 调用封装 |
|
||
| `backend/src/services/okr-ai-sync.ts` | **新建** | 核心 AI 分析 + 增量更新逻辑 |
|
||
| `backend/src/services/okr.ts` | 修改 | 导出 dateToPeriod, completed 状态处理 |
|
||
| `backend/src/sync/scheduler.ts` | 修改 | 同步后触发 AI 分析 |
|
||
| `backend/src/routes/okr.ts` | 修改 | 新增手动触发端点 |
|
||
| `backend/drizzle/0002_*.sql` | **新建** | DB 迁移 |
|
||
|
||
---
|
||
|
||
## 实施顺序
|
||
|
||
1. Schema 变更 + 迁移文件(第一、五阶段)
|
||
2. config.ts 加豆包 AI 环境变量(第二阶段 2a)
|
||
3. llm-client.ts — 豆包 API 封装(第二阶段 2b)
|
||
4. okr.ts 小改(导出 dateToPeriod + completed 处理)
|
||
5. okr-ai-sync.ts 核心服务(第三阶段)
|
||
6. scheduler.ts 集成 + 新 API 端点(第四阶段)
|
||
7. 端到端测试
|
||
|
||
---
|
||
|
||
## 验证方案
|
||
|
||
1. 配置 `.env`:`AI_ENABLED=true`,`AI_API_KEY=...`,`AI_MODEL=doubao-seed-2-0-pro-260215`
|
||
2. 确保数据库中已有项目、用户、项目-仓库绑定、git commits 数据
|
||
3. 调用 `POST /api/okr/ai-analyze/preview` 查看 AI 返回的 actions(不写库)
|
||
4. 确认 actions 合理后,调用 `POST /api/okr/ai-analyze` 执行
|
||
5. 通过 `GET /api/okr` 验证 OKR 数据已生成,时间节点由 AI 合理设定
|
||
6. 再次调用 `POST /api/okr/ai-analyze`,确认不会重复生成(增量标记生效)
|
||
7. 手动触发 Git 同步拉到新 commits,再次分析,确认只处理新增 commits
|
||
8. 检查 krLogs 中有 AI 操作记录
|
||
9. 前端 OKR 页面查看显示是否正常
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
| 场景 | 处理 |
|
||
|------|------|
|
||
| AI_ENABLED=false 或 API key 为空 | 静默跳过,不影响 Git 同步 |
|
||
| 豆包 API 返回非 JSON | 尝试用正则提取 ```json 块,失败则记 syncLogs error |
|
||
| 豆包 API 超时(60s) | 记错误日志,commits 保持"未分析"状态,下次同步重试 |
|
||
| AI 建议更新不存在的 KR | 跳过该 action,记 warning |
|
||
| AI 建议创建重复 sourceKey 的 KR | 转为 update_progress |
|
||
| commit 的 repo 没有绑定到任何项目 | 跳过该仓库的分析 |
|
||
| 某个仓库无新增 commits | 跳过,不调用 AI |
|