Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 事件进入前端聊天面板
Expand Down
2 changes: 1 addition & 1 deletion docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

## 质量约束

Expand Down
108 changes: 107 additions & 1 deletion kimi-code-swarm/agent-engine/src/agent.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -299,6 +304,7 @@ export class Agent {
}

async stop() {
this.stopCiMonitor()
if (this.process) {
this.process.kill()
this.running = false
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -376,6 +390,98 @@ export class Agent {
return { ok: true, steps }
}

/**
* 启动 GitHub Actions CI 轮询监控
* PR 创建成功后调用,自动检测 CI 失败并触发修复
*/
async startCiMonitor(githubToken: string): Promise<void> {
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<void> {
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 次
*/
Expand Down
2 changes: 2 additions & 0 deletions kimi-code-swarm/agent-engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down
77 changes: 75 additions & 2 deletions kimi-code-swarm/agent-engine/src/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -95,11 +95,84 @@ 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)}`
console.error(`[github-api] ${msg}`)
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<CheckRunsResult | null> {
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<string | null> {
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
}
}
2 changes: 2 additions & 0 deletions kimi-code-swarm/agent-engine/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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) ──
Expand Down
3 changes: 3 additions & 0 deletions kimi-code-swarm/agent-engine/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,6 +46,7 @@ export interface AgentState {
logs: LogEntry[]
reviews: ReviewEntry[]
changedFiles?: string[]
ciStatus?: CiStatus
}

// ── Commands from Rust → Node.js ──
Expand Down
Loading