From 112963b2b7320af94b28a8d957b1db539eaef4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Thu, 14 May 2026 04:54:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(hermes):=20Batch=201=20=C2=A7E=20-=20Sessi?= =?UTF-8?q?ons=20=E5=AF=BC=E5=87=BA=EF=BC=88=E8=B5=B0=20dashboard=20/api/s?= =?UTF-8?q?essions/{id}/messages=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 校对稿订正:不走 CLI `hermes sessions export`,直接调 dashboard 9119 HTTP API。 ## 后端 - 新 Tauri 命令 hermes_session_export(session_id): · GET http://127.0.0.1:{dashboard_port}/api/sessions/{id}/messages · 拿原始 JSON 返回前端 · 错误提示「请先启动 Dashboard」(dashboard server 必须运行) ## 前端 - tauri-api.js: hermesSessionExport wrapper - sessions.js: 详情面板「打开会话 / Pin / 导出 / 删除」并列布局 · 点导出 → Blob + URL.createObjectURL + a.download 浏览器下载 hermes-session-{id}.json · toast 成功/失败 - dev-api.js: Web 模式 handler 同步调 dashboard 端口 ## i18n - sessionsExport / sessionsExportSuccess / sessionsExportFailed × 3 语言 --- scripts/dev-api.js | 13 ++++++++++ src-tauri/src/commands/hermes.rs | 37 ++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/engines/hermes/pages/sessions.js | 26 +++++++++++++++++++ src/lib/tauri-api.js | 2 ++ src/locales/modules/engine.js | 4 +++ 6 files changed, 83 insertions(+) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 773e965..060c384 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -7209,6 +7209,19 @@ const handlers = { return await resp.json().catch(() => ({ ok: true })) }, + // Batch 1 §E: Sessions 导出(走 dashboard 9119) + async hermes_session_export({ sessionId } = {}) { + if (!sessionId) throw new Error('session_id 不能为空') + const port = handlers._hermesDashboardPort() + const url = `http://127.0.0.1:${port}/api/sessions/${encodeURIComponent(sessionId)}/messages` + const resp = await globalThis.fetch(url, { signal: AbortSignal.timeout(30000) }) + if (!resp.ok) { + const body = await resp.text().catch(() => '') + throw new Error(`export 失败 HTTP ${resp.status}: ${body}(提示:请先启动 Dashboard)`) + } + return await resp.json() + }, + // 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 不能为空') diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index c0ee2e3..bedd9b2 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -3795,6 +3795,43 @@ pub async fn hermes_run_approval(run_id: String, choice: String) -> Result().await.unwrap_or(serde_json::json!({ "ok": true }))) } +// --------------------------------------------------------------------------- +// Batch 1 §E: hermes_session_export — 导出会话消息(走 dashboard 9119) +// +// 校对稿订正:不走 CLI `hermes sessions export`,直接调 +// `GET http://127.0.0.1:{dashboard_port}/api/sessions/{session_id}/messages` +// 拿 JSON 后由前端打包下载(避免 CLI 子进程开销 + Web 模式不可达)。 +// +// 注意:dashboard server 需要先启动(用户没启的话调 hermes_dashboard_start) +// --------------------------------------------------------------------------- + +#[tauri::command] +pub async fn hermes_session_export(session_id: String) -> Result { + if session_id.is_empty() { + return Err("session_id 不能为空".to_string()); + } + let port = hermes_dashboard_port(); + let url = format!("http://127.0.0.1:{port}/api/sessions/{session_id}/messages"); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| format!("HTTP 客户端创建失败: {e}"))?; + + let resp = client + .get(&url) + .send() + .await + .map_err(|e| format!("export 请求失败: {}(提示:请先启动 Dashboard)", reqwest_error_detail(&e)))?; + let status = resp.status(); + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + return Err(format!("export 失败 HTTP {}: {}", status.as_u16(), body)); + } + // 让前端拿原始 JSON 自己打包下载(保留完整结构) + resp.json::().await.map_err(|e| format!("解析 JSON 失败: {e}")) +} + #[tauri::command] pub async fn hermes_agent_run( app: tauri::AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b804f3e..8969f15 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -241,6 +241,7 @@ pub fn run() { hermes::hermes_agent_run, hermes::hermes_run_stop, hermes::hermes_run_approval, + hermes::hermes_session_export, hermes::hermes_read_config, hermes::hermes_read_config_full, hermes::hermes_lazy_deps_features, diff --git a/src/engines/hermes/pages/sessions.js b/src/engines/hermes/pages/sessions.js index b34fb42..ae58bae 100644 --- a/src/engines/hermes/pages/sessions.js +++ b/src/engines/hermes/pages/sessions.js @@ -249,6 +249,7 @@ export function render() {
${canPin ? `` : ''} +
@@ -454,6 +455,31 @@ export function render() { el.querySelector('#hm-session-delete')?.addEventListener('click', async () => { await deleteOne(currentSession()) }) + + // Batch 1 §E: 会话导出 + el.querySelector('#hm-session-export')?.addEventListener('click', async (e) => { + const sid = e.currentTarget.dataset.sessionId + if (!sid) return + const btn = e.currentTarget + btn.disabled = true + try { + const data = await api.hermesSessionExport(sid) + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `hermes-session-${sid}.json` + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + toast(t('engine.sessionsExportSuccess'), 'success') + } catch (err) { + toast(t('engine.sessionsExportFailed') + ': ' + (err?.message || err), 'error') + } finally { + btn.disabled = false + } + }) } async function init() { diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index b472e1b..232fbc1 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -478,6 +478,8 @@ export const api = { // Batch 1 §D + §C-bis: 真正中断 + Approval Flow(用 run_id) hermesRunStop: (runId) => invoke('hermes_run_stop', { runId }), hermesRunApproval: (runId, choice) => invoke('hermes_run_approval', { runId, choice }), + // Batch 1 §E: 会话消息导出(走 dashboard /api/sessions/{id}/messages) + hermesSessionExport: (sessionId) => invoke('hermes_session_export', { sessionId }), hermesReadConfig: () => invoke('hermes_read_config'), hermesReadConfigFull: () => invoke('hermes_read_config_full'), hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000), diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 5446ce7..2e6ffa8 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -437,6 +437,10 @@ export default { hermesConfigStatusSaving: _('保存中…', 'Saving…', '儲存中…'), hermesConfigStatusLoading: _('加载中…', 'Loading…', '載入中…'), hermesConfigStatusReady: _('raw yaml 编辑器', 'raw yaml editor', 'raw yaml 編輯器'), + // Batch 1 §E: 会话导出 + sessionsExport: _('导出', 'Export', '匯出'), + sessionsExportSuccess: _('已导出', 'Exported', '已匯出'), + sessionsExportFailed: _('导出失败', 'Export failed', '匯出失敗'), // 停止流式 chatStop: _('停止', 'Stop', '停止'), chatStopped: _('已停止当前回复', 'Run stopped', '已停止目前回覆'),