From 59ed1aefb7dca3c0167b0a0364f1643a7181a0c4 Mon Sep 17 00:00:00 2001 From: HelloWorldU Date: Thu, 14 May 2026 13:03:25 +0800 Subject: [PATCH] feat(agent-engine): CI failure auto-detect & fix loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现 PR 创建后的 CI 事后闭环: - github-api.ts: 新增 getCheckRuns + getCheckRunLogs,轮询 GitHub Checks API - agent.ts: 新增 startCiMonitor / stopCiMonitor / fixBasedOnCiFailure - PR 创建后自动启动 30s 间隔轮询 - CI 失败时获取日志 → runInstructionSilent 修复 → autoSubmitForReview 重新提交 - 支持已有 PR 追加 commit(不重复创建 PR) - Agent stop / engine delete-agent 时自动清理定时器 - engine.ts: delete-agent 前调用 stopCiMonitor 防止内存泄漏 - types/schemas: 新增可选 ciStatus 字段(不改 TaskStatus,最小侵入) - docs: 同步 ARCHITECTURE.md + STATUS.md --- docs/ARCHITECTURE.md | 2 +- docs/STATUS.md | 2 +- kimi-code-swarm/agent-engine/src/agent.ts | 108 +++++++++++++++++- kimi-code-swarm/agent-engine/src/engine.ts | 2 + .../agent-engine/src/github-api.ts | 77 ++++++++++++- kimi-code-swarm/agent-engine/src/schemas.ts | 2 + kimi-code-swarm/agent-engine/src/types.ts | 3 + 7 files changed, 191 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ab94e84..413e137 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -68,7 +68,7 @@ PR 创建时,Store 自动生成 `ReviewEntry[]`,包含所有其他 Agent 作 4. 用户点击"启动"→ `startAgent()` 发送 `start-agent` 命令 → Engine 执行 clone/branch → 推送 `agent-status` 事件 5. **引擎未启动或命令发送失败时**:`sendToEngine` 抛出异常,`startAgent` 已添加 `try/catch` 捕获并写入 Agent 日志,禁止静默失败 6. 用户点击"停止"→ `stopAgent()` **乐观更新**前端状态为 `stopped`,再 `await sendToEngine({ type: 'stop-agent' })` → Engine 调用 `agent.stop()` 等待进程退出 → 推送 `agent-status` 事件;后端失败时自动回滚状态 -7. 用户发送指令 → `sendInstruction()` 执行完毕且检测到文件变更 → Engine **自动调用 `autoSubmitForReview()`**:git add/commit/push → 创建 PR;任何步骤失败时,Engine 将完整执行日志(stdout + stderr + exit code)全量回传给 Kimi CLI,由 Agent 自主判断并修复,然后重试(最多 3 轮)→ 状态变为 `reviewing` +7. 用户发送指令 → `sendInstruction()` 执行完毕且检测到文件变更 → Engine **自动调用 `autoSubmitForReview()`**:git add/commit/push → 创建 PR;任何步骤失败时,Engine 将完整执行日志(stdout + stderr + exit code)全量回传给 Kimi CLI,由 Agent 自主判断并修复,然后重试(最多 3 轮)→ 状态变为 `reviewing` → **自动启动 CI 监控**:Engine 每 30s 轮询 GitHub Checks API,CI 失败时自动将失败日志回传给 Agent 修复并重新提交(最多 3 轮);CI 通过或超时后停止轮询 **日志分流**: - `input` / `output` 及关键状态变更(执行完毕/已停止/Token耗尽等)通过 `log` 事件进入前端聊天面板 diff --git a/docs/STATUS.md b/docs/STATUS.md index 7c29271..ab836f7 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -33,7 +33,7 @@ | Kimi CLI 接入 | ✅ | `sendInstruction` 调用 `kimi --print --quiet`,实时 stdout 流式捕获,可取消 | `src/store/useSwarmStore.ts` | | Token 预算控制 | ✅ | sendInstruction 前检查预算;process-output 中按输出行长度估算并累加;耗尽时自动 kill 进程 | `src/store/useSwarmStore.ts` | | Agent 多轮对话交互 | ✅ | 聊天式气泡 UI,支持 input/output/system/error 消息类型;ready/stopped/completed 状态下可持续对话;working 状态显示执行中指示器;**日志已分流**(system/error 技术日志带组件前缀+颜色走终端 stderr,input/output 及关键状态变更进 UI);**stop-agent 已修复**(前端乐观更新 + await IPC) | `src/components/AgentDetail.vue`, `src/store/useSwarmStore.ts`, `agent-engine/src/agent.ts` | -| Agent 自动提交审阅 | ✅ | Agent 执行完指令后检测到文件变更自动 `git add/commit/push` 并创建 PR;任何步骤失败时,Engine 将完整执行日志(stdout + stderr + exit code)全量回传给 Kimi CLI,由 Agent 自主判断并修复,然后重试(最多 3 轮);无 GitHub Token 时降级为 Mock PR | `agent-engine/src/agent.ts` | +| Agent 自动提交审阅 | ✅ | Agent 执行完指令后检测到文件变更自动 `git add/commit/push` 并创建 PR;pre-commit 失败时将完整执行日志全量回传修复(最多 3 轮);PR 创建后自动轮询 GitHub Actions CI(30s 间隔),CI 失败时自动获取日志并修复重新提交(最多 3 轮);无 GitHub Token 时降级为 Mock PR | `agent-engine/src/agent.ts` | ## 质量约束 diff --git a/kimi-code-swarm/agent-engine/src/agent.ts b/kimi-code-swarm/agent-engine/src/agent.ts index 277be66..35d4cc3 100644 --- a/kimi-code-swarm/agent-engine/src/agent.ts +++ b/kimi-code-swarm/agent-engine/src/agent.ts @@ -1,7 +1,7 @@ import type { AgentState, LogEntry, ReviewEntry, TaskStatus, EngineEvent } from './types.js' import { runKimi, detectKimiCli, type KimiProcess } from './kimi.js' import { getChangedFiles, getFileDiff, gitAdd, gitCommit, gitPush, createBranch, cloneRepo, gitFetch, getBranchDiff, gitDeleteRemoteBranch } from './git.js' -import { createPullRequest, mergePullRequest } from './github-api.js' +import { createPullRequest, mergePullRequest, getPullRequest, getCheckRuns, getCheckRunLogs } from './github-api.js' interface SubmitStep { name: string @@ -33,6 +33,11 @@ export class Agent { private running = false private reviewRound = 0 private githubToken?: string + private ciMonitorTimer?: NodeJS.Timeout + private ciRetryCount = 0 + private readonly CI_MAX_RETRIES = 3 + private readonly CI_POLL_INTERVAL_MS = 30000 + private readonly CI_TIMEOUT_MS = 600000 constructor( name: string, @@ -299,6 +304,7 @@ export class Agent { } async stop() { + this.stopCiMonitor() if (this.process) { this.process.kill() this.running = false @@ -352,6 +358,13 @@ export class Agent { this.setStatus('reviewing') + // 如果已有 open PR,跳过创建,直接启动 CI 监控 + if (this.state.prStatus === 'open' && this.state.prNumber && githubToken) { + this.log('system', `PR #${this.state.prNumber} 已存在,新 commit 已追加`) + this.startCiMonitor(githubToken) + return { ok: true, steps } + } + // 如果有 GitHub Token,调用真实 API 创建 PR if (githubToken) { try { @@ -361,6 +374,7 @@ export class Agent { this.state.prNumber = pr.number this.state.prUrl = pr.html_url this.log('system', `PR #${pr.number} 已创建: ${pr.html_url}`) + this.startCiMonitor(githubToken) return { ok: true, steps } } } catch (err) { @@ -376,6 +390,98 @@ export class Agent { return { ok: true, steps } } + /** + * 启动 GitHub Actions CI 轮询监控 + * PR 创建成功后调用,自动检测 CI 失败并触发修复 + */ + async startCiMonitor(githubToken: string): Promise { + this.stopCiMonitor() + + if (!this.state.prNumber || !githubToken) return + + this.state.ciStatus = 'pending' + this.log('system', '开始监控 GitHub Actions CI 状态...') + + const startTime = Date.now() + + this.ciMonitorTimer = setInterval(async () => { + // 超时检查 + if (Date.now() - startTime > this.CI_TIMEOUT_MS) { + this.stopCiMonitor() + this.state.ciStatus = 'unknown' + this.log('error', 'CI 监控超时(10分钟),请指挥官人工检查 CI 状态') + return + } + + // 查询 PR 获取 head sha + const pr = await getPullRequest(githubToken, this.state.repoUrl, this.state.prNumber!) + if (!pr) return + + // 查询 check runs + const checks = await getCheckRuns(githubToken, this.state.repoUrl, pr.head.sha) + if (!checks || checks.total_count === 0) return + + // 检查是否还有进行中 + const hasInProgress = checks.check_runs.some((r) => r.status !== 'completed') + if (hasInProgress) return + + // 所有 check 都完成了 + const failedRun = checks.check_runs.find((r) => r.conclusion === 'failure') + if (failedRun) { + this.stopCiMonitor() + this.state.ciStatus = 'failure' + this.log('error', `CI 失败: ${failedRun.name}`) + + const logs = await getCheckRunLogs(githubToken, this.state.repoUrl, failedRun.id) + await this.fixBasedOnCiFailure(logs || `Check run "${failedRun.name}" failed. No logs available.`, githubToken) + return + } + + // 全部通过 + this.stopCiMonitor() + this.state.ciStatus = 'success' + this.log('system', 'GitHub Actions CI 全部通过 ✅') + }, this.CI_POLL_INTERVAL_MS) + } + + /** + * 停止 CI 轮询定时器 + */ + stopCiMonitor(): void { + if (this.ciMonitorTimer) { + clearInterval(this.ciMonitorTimer) + this.ciMonitorTimer = undefined + } + } + + /** + * 基于 CI 失败日志自动修复代码并重新提交 + */ + private async fixBasedOnCiFailure(ciLogs: string, githubToken: string): Promise { + this.ciRetryCount++ + if (this.ciRetryCount > this.CI_MAX_RETRIES) { + this.log('error', `CI 修复已达最大轮次(${this.CI_MAX_RETRIES} 次),请指挥官人工介入`) + this.setStatus('ready') + return + } + + this.log('system', `CI 失败,第 ${this.ciRetryCount}/${this.CI_MAX_RETRIES} 轮自动修复...`) + const fixPrompt = `GitHub Actions CI 检查失败了,日志如下:\n\n${ciLogs}\n\n请根据上述日志修改代码文件,使其能够通过 CI 检查。直接修改相关文件,不需要额外说明。` + await this.runInstructionSilent(fixPrompt) + + // 修复后重新检测变更 + if (this.state.workspace) { + try { + this.state.changedFiles = await getChangedFiles(this.state.workspace) + } catch { + // 忽略检测失败 + } + } + + // 重新提交(autoSubmitForReview 成功后会再次启动 CI 监控) + await this.autoSubmitForReview() + } + /** * 自动提交审阅:失败时捕获错误并让 Agent 修复,最多重试 3 次 */ diff --git a/kimi-code-swarm/agent-engine/src/engine.ts b/kimi-code-swarm/agent-engine/src/engine.ts index c3bc814..a753356 100644 --- a/kimi-code-swarm/agent-engine/src/engine.ts +++ b/kimi-code-swarm/agent-engine/src/engine.ts @@ -103,6 +103,8 @@ export class AgentEngine { break } + // 先停止 CI 轮询,避免 Agent 删除后定时器还在跑 + agent.stopCiMonitor() this.broadcast({ type: 'log', agentId: cmd.agentId, entry: { id: 'system', timestamp: new Date().toISOString(), type: 'system', content: `[delete-agent] 停止 agent 进程...` } }) await agent.stop() const workspace = agent.state.workspace || `E:/workspace/${agent.state.id}` diff --git a/kimi-code-swarm/agent-engine/src/github-api.ts b/kimi-code-swarm/agent-engine/src/github-api.ts index ae9036d..d19c34a 100644 --- a/kimi-code-swarm/agent-engine/src/github-api.ts +++ b/kimi-code-swarm/agent-engine/src/github-api.ts @@ -86,7 +86,7 @@ export async function getPullRequest( token: string, repoUrl: string, prNumber: number, -): Promise<{ state: string; merged: boolean } | null> { +): Promise<{ state: string; merged: boolean; head: { sha: string } } | null> { const repo = parseRepoUrl(repoUrl) if (!repo) return null @@ -95,7 +95,7 @@ export async function getPullRequest( try { const res = await fetch(url, { headers: getHeaders(token) }) if (!res.ok) return null - const data = (await res.json()) as { state: string; merged: boolean } + const data = (await res.json()) as { state: string; merged: boolean; head: { sha: string } } return data } catch (err) { const msg = `GitHub API 查询 PR 失败: ${String(err)}` @@ -103,3 +103,76 @@ export async function getPullRequest( throw new Error(msg) } } + +export interface CheckRun { + id: number + name: string + status: string + conclusion: string | null + html_url: string + started_at: string | null +} + +export interface CheckRunsResult { + total_count: number + check_runs: CheckRun[] +} + +/** + * 查询指定 commit 的 check runs(CI 状态) + */ +export async function getCheckRuns( + token: string, + repoUrl: string, + ref: string, +): Promise { + const repo = parseRepoUrl(repoUrl) + if (!repo) return null + + const url = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/commits/${ref}/check-runs` + + try { + const res = await fetch(url, { headers: getHeaders(token) }) + if (!res.ok) { + const err = await res.text() + console.error(`[github-api] getCheckRuns ${res.status}: ${err}`) + return null + } + return (await res.json()) as CheckRunsResult + } catch (err) { + console.error(`[github-api] getCheckRuns 异常: ${String(err)}`) + return null + } +} + +/** + * 获取失败 check run 的日志文本 + * GitHub 返回的是 text/plain 的日志文件,跟随重定向获取 + */ +export async function getCheckRunLogs( + token: string, + repoUrl: string, + checkRunId: number, +): Promise { + const repo = parseRepoUrl(repoUrl) + if (!repo) return null + + const url = `${GITHUB_API}/repos/${repo.owner}/${repo.repo}/check-runs/${checkRunId}/logs` + + try { + const res = await fetch(url, { + headers: { ...getHeaders(token), Accept: 'application/vnd.github.v3+json' }, + redirect: 'follow', + }) + if (!res.ok) { + console.error(`[github-api] getCheckRunLogs ${res.status}`) + return null + } + // 日志可能很大,截断到 8000 字符以内 + const text = await res.text() + return text.length > 8000 ? text.slice(0, 8000) + '\n...[truncated]' : text + } catch (err) { + console.error(`[github-api] getCheckRunLogs 异常: ${String(err)}`) + return null + } +} diff --git a/kimi-code-swarm/agent-engine/src/schemas.ts b/kimi-code-swarm/agent-engine/src/schemas.ts index 3b963f3..e69173d 100644 --- a/kimi-code-swarm/agent-engine/src/schemas.ts +++ b/kimi-code-swarm/agent-engine/src/schemas.ts @@ -11,6 +11,7 @@ export const TaskStatusSchema = z.enum([ ]) export const PrStatusSchema = z.enum(['none', 'open', 'merged', 'closed']) +export const CiStatusSchema = z.enum(['pending', 'success', 'failure', 'unknown']) // ── LogEntry ── export const LogEntrySchema = z.object({ @@ -50,6 +51,7 @@ export const AgentStateSchema = z.object({ logs: z.array(LogEntrySchema), reviews: z.array(ReviewEntrySchema), changedFiles: z.array(z.string()).optional(), + ciStatus: CiStatusSchema.optional(), }) // ── EngineCommand (Discriminated Union) ── diff --git a/kimi-code-swarm/agent-engine/src/types.ts b/kimi-code-swarm/agent-engine/src/types.ts index 618c6bb..06ff992 100644 --- a/kimi-code-swarm/agent-engine/src/types.ts +++ b/kimi-code-swarm/agent-engine/src/types.ts @@ -9,6 +9,8 @@ export type TaskStatus = export type PrStatus = 'none' | 'open' | 'merged' | 'closed' +export type CiStatus = 'pending' | 'success' | 'failure' | 'unknown' + export interface ReviewEntry { reviewerAgentId: string reviewerName: string @@ -44,6 +46,7 @@ export interface AgentState { logs: LogEntry[] reviews: ReviewEntry[] changedFiles?: string[] + ciStatus?: CiStatus } // ── Commands from Rust → Node.js ──