Skip to content

Commit 1237369

Browse files
author
HelloWorldU
committed
fix: stop-agent无响应 + 日志分流 + 终端格式美化 + Windows删除目录权限
- stopAgent改为async,添加乐观更新和await sendToEngine - 日志分流:system/error技术日志只走终端stderr,input/output及关键状态进UI - 终端日志格式:组件前缀[Agent]/[Kimi]/[Git] + 颜色高亮(vLLM风格) - Rust stdout reader不再回显JSON事件,只回显非JSON行 - agent.stop()改为async,kill后等待进程完全退出(避免Windows文件锁定) - delete-agent增加rmdir /s /q fallback,解决Windows删除权限问题 - 同步更新docs/ARCHITECTURE.md、docs/STATUS.md
1 parent 25ed84e commit 1237369

6 files changed

Lines changed: 140 additions & 26 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ PR 创建时,Store 自动生成 `ReviewEntry[]`,包含所有其他 Agent 作
6767
3. Engine 创建成功后推送 `agent-created` 事件 → 前端列表更新
6868
4. 用户点击"启动"→ `startAgent()` 发送 `start-agent` 命令 → Engine 执行 clone/branch → 推送 `agent-status` 事件
6969
5. **引擎未启动或命令发送失败时**`sendToEngine` 抛出异常,`startAgent` 已添加 `try/catch` 捕获并写入 Agent 日志,禁止静默失败
70+
6. 用户点击"停止"→ `stopAgent()` **乐观更新**前端状态为 `stopped`,再 `await sendToEngine({ type: 'stop-agent' })` → Engine 调用 `agent.stop()` 等待进程退出 → 推送 `agent-status` 事件;后端失败时自动回滚状态
71+
72+
**日志分流**
73+
- `input` / `output` 及关键状态变更(执行完毕/已停止/Token耗尽等)通过 `log` 事件进入前端聊天面板
74+
- `system` / `error` 技术日志(PID、命令行参数、文件变更数、内部异常等)输出到 **stderr**,由终端直接显示(带 `[Agent]`/`[Kimi]`/`[Git]` 组件前缀和颜色),不污染 UI
7075

7176
## 登录流程
7277

docs/STATUS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
| Agent Engine 进程管理 || Rust 后台 spawn Node.js Agent Engine,stdin/stdout 管道通信;Windows 优先使用本地 `tsx.cmd` 避免 PATH 继承问题 | `src-tauri/src/lib.rs`, `agent-engine/src/index.ts` |
3333
| Kimi CLI 接入 || `sendInstruction` 调用 `kimi --print --quiet`,实时 stdout 流式捕获,可取消 | `src/store/useSwarmStore.ts` |
3434
| Token 预算控制 || sendInstruction 前检查预算;process-output 中按输出行长度估算并累加;耗尽时自动 kill 进程 | `src/store/useSwarmStore.ts` |
35-
| Agent 多轮对话交互 || 聊天式气泡 UI,支持 input/output/system/error 消息类型;ready/stopped/completed 状态下可持续对话;working 状态显示执行中指示器 | `src/components/AgentDetail.vue`, `src/store/useSwarmStore.ts`, `agent-engine/src/agent.ts` |
35+
| 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` |
3636

3737
## 质量约束
3838

@@ -59,4 +59,4 @@
5959

6060
---
6161

62-
*Last updated: 2026-05-12*
62+
*Last updated: 2026-05-13*

kimi-code-swarm/agent-engine/src/agent.ts

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const branchName = (name: string) => {
1111
return `agent/${slug}-${generateId().slice(-4)}`
1212
}
1313

14+
const termColors = {
15+
reset: '\x1b[0m',
16+
red: '\x1b[31m',
17+
yellow: '\x1b[33m',
18+
cyan: '\x1b[36m',
19+
gray: '\x1b[90m',
20+
}
21+
1422
export class Agent {
1523
state: AgentState
1624
private process?: KimiProcess
@@ -54,11 +62,63 @@ export class Agent {
5462
}
5563
}
5664

65+
private termLog(component: string, level: 'info' | 'warn' | 'error', message: string) {
66+
const c = level === 'error' ? termColors.red : level === 'warn' ? termColors.yellow : termColors.cyan
67+
const tag = level === 'info' ? '' : `${c}${level.toUpperCase()}${termColors.reset} `
68+
console.error(`${c}[${component}]${termColors.reset} ${tag}${message}`)
69+
}
70+
71+
private isUserVisibleLog(type: LogEntry['type'], content: string): boolean {
72+
if (type === 'input' || type === 'output') return true
73+
const patterns = [
74+
'Agent 已恢复',
75+
'工作空间就绪',
76+
'启动失败',
77+
'Token 预算已耗尽',
78+
'Kimi CLI 未找到',
79+
'启动 Kimi CLI 失败',
80+
'Agent 执行完毕',
81+
'Agent 执行失败',
82+
'Agent 已停止',
83+
'代码已推送',
84+
'推送失败',
85+
'PR #',
86+
'已合并到 main',
87+
'PR 被打回',
88+
'合并被拒绝',
89+
'审阅通过了此 PR',
90+
'审阅拒绝了此 PR',
91+
'全员审阅通过',
92+
'已指派',
93+
'自动修改已达最大轮次',
94+
]
95+
return patterns.some((p) => content.includes(p))
96+
}
97+
98+
private inferComponent(content: string): string {
99+
if (content.includes('Kimi') || content.includes('kimi')) return 'Kimi'
100+
if (content.includes('GitHub') || content.includes('github')) return 'GitHub'
101+
if (content.includes('git ') || content.includes('仓库') || content.includes('分支') || content.includes('推送') || content.includes('代码') || content.includes('克隆')) return 'Git'
102+
if (content.includes('审阅')) return 'Review'
103+
return 'Agent'
104+
}
105+
57106
private log(type: LogEntry['type'], content: string, tokens?: number) {
58107
const entry = this.makeLog(type, content, tokens)
59108
this.state.logs.push(entry)
60109
this.state.lastActivity = new Date().toISOString()
61-
this.emit({ type: 'log', agentId: this.state.id, entry })
110+
111+
const showInUi = this.isUserVisibleLog(type, content)
112+
if (showInUi) {
113+
this.emit({ type: 'log', agentId: this.state.id, entry })
114+
}
115+
116+
// system/error 都输出到终端(带颜色、组件前缀)
117+
if (type === 'system' || type === 'error') {
118+
const level = type === 'error' ? 'error' : 'info'
119+
const component = this.inferComponent(content)
120+
this.termLog(component, level, content)
121+
}
62122
}
63123

64124
private setStatus(status: TaskStatus) {
@@ -159,16 +219,24 @@ export class Agent {
159219
}
160220
})()
161221

162-
// stderr reader — filter out kimi CLI internal logging errors
222+
// stderr reader — filter out kimi CLI internal loguru errors (Windows known issue)
223+
let loguruBlockActive = false
163224
;(async () => {
164225
try {
165226
for await (const line of this.process!.stderr) {
166227
if (!this.running) break
167-
// Skip loguru internal rotation errors on Windows (not actionable)
168-
if (line.includes('Loguru Handler') || line.includes('PermissionError')) {
169-
this.log('system', `[kimi stderr filtered] ${line.substring(0, 120)}`)
228+
// Detect start of loguru error block
229+
if (line.includes('--- Logging error') && line.includes('Loguru')) {
230+
loguruBlockActive = true
231+
this.log('system', '[kimi stderr] loguru logging error filtered (see ~/.kimi/logs)')
170232
continue
171233
}
234+
// End of loguru block: empty line or non-traceback line after PermissionError
235+
if (loguruBlockActive && (line.trim() === '' || (!line.startsWith(' ') && !line.includes('Traceback')))) {
236+
loguruBlockActive = false
237+
continue
238+
}
239+
if (loguruBlockActive) continue
172240
this.log('error', line)
173241
this.emit({ type: 'agent-output', agentId: this.state.id, line, isStderr: true })
174242
}
@@ -208,10 +276,19 @@ export class Agent {
208276
}
209277
}
210278

211-
stop() {
279+
async stop() {
212280
if (this.process) {
213281
this.process.kill()
214282
this.running = false
283+
// 等待进程完全退出(kill() 内部 2s 后会 SIGKILL,这里最多等 3s)
284+
try {
285+
await Promise.race([
286+
this.process.wait(),
287+
new Promise<void>((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
288+
])
289+
} catch {
290+
// 超时或异常,忽略
291+
}
215292
}
216293
this.state.pid = undefined
217294
this.setStatus('stopped')

kimi-code-swarm/agent-engine/src/engine.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,41 @@ export class AgentEngine {
5353

5454
case 'stop-agent': {
5555
const agent = this.agents.get(cmd.agentId)
56-
if (agent) agent.stop()
56+
if (agent) await agent.stop()
5757
break
5858
}
5959

6060
case 'delete-agent': {
6161
const agent = this.agents.get(cmd.agentId)
6262
if (agent) {
63-
agent.stop()
63+
await agent.stop()
6464
const workspace = agent.state.workspace || `E:/workspace/${agent.state.id}`
65+
// 给 Windows 一点时间释放文件句柄
66+
await new Promise((r) => setTimeout(r, 500))
67+
68+
let deleted = false
69+
// 先尝试 Node.js 原生删除
6570
try {
6671
await rm(workspace, { recursive: true, force: true })
72+
deleted = true
73+
} catch {
74+
// fallback: Windows 系统命令(对锁定文件更激进)
75+
try {
76+
const { exec } = await import('child_process')
77+
await new Promise<void>((resolve, reject) => {
78+
exec(`rmdir /s /q "${workspace}"`, (err) => {
79+
if (err) reject(err)
80+
else resolve()
81+
})
82+
})
83+
deleted = true
84+
} catch {}
85+
}
86+
87+
if (deleted) {
6788
this.broadcast({ type: 'log', agentId: cmd.agentId, entry: { id: 'system', timestamp: new Date().toISOString(), type: 'system', content: `工作目录已清理: ${workspace}` } })
68-
} catch (err) {
69-
const msg = `清理工作目录失败: ${String(err)}`
89+
} else {
90+
const msg = `清理工作目录失败: ${workspace},请手动删除`
7091
console.error(`[engine] ${msg}`)
7192
this.broadcast({ type: 'log', agentId: cmd.agentId, entry: { id: 'system', timestamp: new Date().toISOString(), type: 'error', content: msg } })
7293
}
@@ -157,9 +178,7 @@ export class AgentEngine {
157178

158179
case 'shutdown': {
159180
// Stop all agents gracefully
160-
for (const agent of this.agents.values()) {
161-
agent.stop()
162-
}
181+
await Promise.all(Array.from(this.agents.values()).map((a) => a.stop()))
163182
this.agents.clear()
164183
this.broadcast({ type: 'pong', message: 'Engine shutting down' })
165184
break

kimi-code-swarm/src-tauri/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ fn spawn_agent_engine(app: tauri::AppHandle) -> Result<u32, String> {
241241
let reader = BufReader::new(stdout);
242242
for line in reader.lines() {
243243
if let Ok(line) = line {
244+
// 非 JSON 行(如引擎崩溃输出)直接回显终端;JSON 事件由前端处理,不回显
245+
if serde_json::from_str::<serde_json::Value>(&line).is_err() {
246+
println!("{}", line);
247+
}
244248
let _ = app_emit.emit("agent-engine-event", AgentEngineEvent { line });
245249
}
246250
}

kimi-code-swarm/src/store/useSwarmStore.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -371,18 +371,27 @@ export function useSwarmStore() {
371371
sendToEngine({ type: 'send-instruction', agentId: id, instruction })
372372
}
373373

374-
function stopAgent(id: string) {
375-
if (!isTauri) {
376-
const agent = state.agents.find((a) => a.id === id)
377-
if (agent) {
378-
agent.status = 'stopped'
379-
agent.pid = undefined
380-
agent.logs.push({ id: generateId(), timestamp: new Date().toISOString(), type: 'system', content: 'Agent 已停止' })
381-
persistAgents()
382-
}
383-
return
374+
async function stopAgent(id: string) {
375+
const agent = state.agents.find((a) => a.id === id)
376+
if (!agent) return
377+
378+
// 乐观更新:立即改 UI 状态,不再等待后端确认
379+
const previousStatus = agent.status
380+
agent.status = 'stopped'
381+
agent.pid = undefined
382+
agent.logs.push({ id: generateId(), timestamp: new Date().toISOString(), type: 'system', content: 'Agent 已停止' })
383+
persistAgents()
384+
385+
if (!isTauri) return
386+
387+
try {
388+
await sendToEngine({ type: 'stop-agent', agentId: id })
389+
} catch (err) {
390+
log.error('停止 Agent 失败:', err)
391+
// 后端调用失败时恢复状态
392+
agent.status = previousStatus
393+
persistAgents()
384394
}
385-
sendToEngine({ type: 'stop-agent', agentId: id })
386395
}
387396

388397
function submitForReview(id: string) {

0 commit comments

Comments
 (0)