mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat(hermes): Batch 1 §C+§D+§C-bis - Approval Flow + Stop 真中断 + 3 类新事件
校对稿(hermes-source-verified)基于真实 Hermes 源码确认了关键事实:
- 真实 SSE 事件 9 类(设计稿推测的 6 个根本不存在)
- 真实 abort 端点:POST /v1/runs/{run_id}/stop(用 run_id 不是 session_id)
- Approval Flow 是 Hermes 重要原生特性,ClawPanel 0% 接入 → 跑代码工具就崩
本 PR 一次解决 3 个必修硬伤:
## 修复 1: Stop 假停(§D)
- 新 Tauri 命令 hermes_run_stop(run_id) → POST /v1/runs/{run_id}/stop
- chat-store: state 新增 currentRunId,来自 hermes-run-started 事件
- stopStreaming() 改为:先调 hermes_run_stop(currentRunId) 通知后端,再 abort 本地 SSE
- 之前 stopStreaming 只 abort 本地 SSE,后端继续跑完 → 是「假停」
## 修复 2: Approval Flow 接入(§C-bis 新增 — 设计稿原本没写)
- 新 Tauri 命令 hermes_run_approval(run_id, choice) → POST /v1/runs/{run_id}/approval
- choice 枚举校验:once / session / always / deny
- chat-store: state.pendingApproval 存待批准信息(tool, args, choices, run_id)
- chat.js: 新增 renderApprovalPanel() 渲染琥珀色气泡 + 4 按钮 + JSON args 预览
- store.respondApproval(choice) 暴露给 UI(乐观清状态 + 失败回滚)
- 用户跑代码工具(terminal/code_execution)时会触发,没接入就会卡死
## 修复 3: SSE 事件白名单补 3 类(§C 校对版)
- normalize_hermes_stream_event 白名单增加:
· approval.request → emit hermes-run-approval-request
· approval.responded → emit hermes-run-approval-responded
· run.cancelled → emit hermes-run-cancelled(终态,返回 Ok(true))
- chat-store 新监听 u5..u9(5 个新事件):
· hermes-run-started → 存 currentRunId
· approval-request → 设 pendingApproval
· approval-responded → 清 pendingApproval
· cancelled → 标记 (stopped) + cleanup
· reasoning → 标记 hasReasoning(设计稿推测的 reasoning.delta 不存在)
- handleStreamEvent(Web 模式)同步加 4 个新分支
## chat.js UI
- renderApprovalPanel:琥珀色边框 + 🔐 emoji + 工具名 + JSON args 预览 + 4 选项按钮
- "deny" 用 btn-secondary(灰色),其他 btn-primary(蓝色)
- 按钮点击 → store.respondApproval(choice) → 后端 POST + 等服务端 approval.responded 作权威清理
- streaming 中显示 aborting 文案当 state.aborting=true
## CSS (.hm-chat-approval*)
- 琥珀色边框 + 半透明背景(明/暗主题各自适配)
- args 单独 monospace 代码块、max-height: 180px 防过长
- 响应式:max-width: 720px,按钮 flex-wrap
## i18n
- engine.chatAborting / chatApprovalTitle / chatApprovalHint
- chatApprovalOnce / chatApprovalSession / chatApprovalAlways / chatApprovalDeny
- chatApprovalFailed
- 3 语言(zh-CN/en/zh-TW),其它语言走 fallback
## 设计稿对照(保留可信细节,砍掉推测)
- ✅ 留:approval.request / approval.responded / run.cancelled / reasoning.available(4 类真实事件)
- ❌ 砍:reasoning.delta / thinking.delta / compression.* / abort.* / usage.updated / run.queued(6 类不存在的事件,hermes-web-ui 内部合成)
- ✅ 修:abort 端点路径 /v1/runs/{run_id}/stop(用 run_id)
## 累计
- Rust: 1 个 helper(read_hermes_api_key) + 2 个新命令 + emit_hermes_stream_event 加 3 分支
- 前端: chat-store 新 4 个 state 字段 + 5 个监听器 + 4 个 handleStreamEvent 分支 + respondApproval API
- chat.js: renderApprovalPanel + 按钮绑定 + aborting 文案
- i18n: +8 个键 × 3 语言
- cargo check ✓ + npm build ✓
This commit is contained in:
@@ -7194,6 +7194,44 @@ const handlers = {
|
||||
return { ok: false, error: `Web 模式下无法预装依赖。请在桌面端 ClawPanel 完成 ${feature} 安装。` }
|
||||
},
|
||||
|
||||
// Batch 1 §D: 真正中断 — POST /v1/runs/{run_id}/stop
|
||||
async hermes_run_stop({ runId } = {}) {
|
||||
if (!runId) throw new Error('run_id 不能为空')
|
||||
const url = `${hermesGatewayUrl()}/v1/runs/${encodeURIComponent(runId)}/stop`
|
||||
const apiKey = _readHermesApiServerKey()
|
||||
const headers = { 'User-Agent': 'ClawPanel-Web' }
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
const resp = await globalThis.fetch(url, { method: 'POST', headers, signal: AbortSignal.timeout(5000) })
|
||||
if (!resp.ok) {
|
||||
const body = await resp.text().catch(() => '')
|
||||
throw new Error(`stop 失败 HTTP ${resp.status}: ${body}`)
|
||||
}
|
||||
return await resp.json().catch(() => ({ ok: true }))
|
||||
},
|
||||
|
||||
// Batch 1 §C-bis: Approval Flow — POST /v1/runs/{run_id}/approval { choice }
|
||||
async hermes_run_approval({ runId, choice } = {}) {
|
||||
if (!runId) throw new Error('run_id 不能为空')
|
||||
if (!['once', 'session', 'always', 'deny'].includes(choice)) {
|
||||
throw new Error(`approval choice 必须是 once/session/always/deny,收到 ${choice}`)
|
||||
}
|
||||
const url = `${hermesGatewayUrl()}/v1/runs/${encodeURIComponent(runId)}/approval`
|
||||
const apiKey = _readHermesApiServerKey()
|
||||
const headers = { 'User-Agent': 'ClawPanel-Web', 'Content-Type': 'application/json' }
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
const resp = await globalThis.fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ choice }),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
const body = await resp.text().catch(() => '')
|
||||
throw new Error(`approval 失败 HTTP ${resp.status}: ${body}`)
|
||||
}
|
||||
return await resp.json().catch(() => ({ ok: true }))
|
||||
},
|
||||
|
||||
// P1-4:完整解析 config.yaml,让前端能读 14+ 高价值字段
|
||||
// Web 模式不引入 yaml 依赖,简单返回 raw + null highlights(前端按需渲染)
|
||||
hermes_read_config_full() {
|
||||
|
||||
Reference in New Issue
Block a user