mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat(hermes): Batch 1 §E - Sessions 导出(走 dashboard /api/sessions/{id}/messages)
校对稿订正:不走 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 语言
This commit is contained in:
@@ -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 不能为空')
|
||||
|
||||
@@ -3795,6 +3795,43 @@ pub async fn hermes_run_approval(run_id: String, choice: String) -> Result<Value
|
||||
Ok(resp.json::<Value>().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<Value, String> {
|
||||
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::<Value>().await.map_err(|e| format!("解析 JSON 失败: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hermes_agent_run(
|
||||
app: tauri::AppHandle,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -249,6 +249,7 @@ export function render() {
|
||||
<div class="hm-session-detail-actions">
|
||||
<button class="hm-sessions-btn" id="hm-session-open-chat">${icon('message-circle', 14)}${escHtml(t('engine.sessionsOpenChat'))}</button>
|
||||
${canPin ? `<button class="hm-sessions-btn" id="hm-session-pin">${icon(store.state.pinned.has(session.id) ? 'crown' : 'target', 14)}${escHtml(store.state.pinned.has(session.id) ? t('engine.sessionsUnpin') : t('engine.sessionsPin'))}</button>` : ''}
|
||||
<button class="hm-sessions-btn" id="hm-session-export" data-session-id="${escAttr(session.id)}">${icon('download', 14)}${escHtml(t('engine.sessionsExport'))}</button>
|
||||
<button class="hm-sessions-btn is-danger" id="hm-session-delete" data-session-key="${escAttr(key)}">${icon('trash', 14)}${escHtml(t('engine.chatDeleteSession'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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', '已停止目前回覆'),
|
||||
|
||||
Reference in New Issue
Block a user