refactor(hermes): UX 小白化收尾 - 6 个页面统一 humanizeError + 修 usage.js bug

把 raw error toast 升级为 humanizeError,让用户看到分类友好的错误提示
(自动加 action button 如「打开设置」「重试」「查看文档」)。

## sessions.js (5 处)
- 加载会话失败 → humanizeError(err, t('engine.sessionsLoadFailed'))
- 详情加载失败 → humanizeError(err, t('engine.sessionsDetailLoadFailed'))
- 切换会话失败 → humanizeError(err, t('engine.sessionsSwitchFailed'))
- 删除失败 → humanizeError(err, t('engine.chatDeleteFailed'))
- 导出失败 → humanizeError(err, t('engine.sessionsExportFailed'))

## extensions.js (5 处)
- dashboard 打开失败 (probe / start) - 2 处
- 主题保存失败 → humanizeError(err, t('engine.extensionsThemeSaveFailed'))
- 加载扩展失败 → humanizeError(err, t('engine.extensionsLoadFailed'))
- 重新扫描失败 → humanizeError(err, t('engine.extensionsRescanFailed'))

## env-editor.js (4 处)
- reveal / save / delete / load 错误 → humanizeError
- 文案保留 inline 中文(页面尚未 i18n 化)

## usage.js (1 bug + 1 升级)
- **修真 bug**: catch (_) {...} 内部却用了 err 引用 → 改为 catch (err) {...}
- 失败时 humanizeError(err, t('engine.usageLoadFailed'))

## lazy-deps.js (1 处)
- features API 失败显示 humanizeError + escapeHtml

## config.js (2 处)
- 加载配置失败 → humanizeError(err, t('engine.hermesConfigLoadFailed'))
- 保存配置失败 → humanizeError(err, t('engine.hermesConfigSaveFailed'))

## hermes.css 修 lint
- .hm-kanban-task-summary 加 line-clamp 标准属性(不只是 -webkit-line-clamp)

## i18n 新增 8 键
- sessionsLoadFailed / sessionsSwitchFailed / usageLoadFailed
- extensionsThemeSaveFailed / extensionsLoadFailed / extensionsRescanFailed
- hermesConfigLoadFailed / hermesConfigSaveFailed
- 全部 × 3 语言

## 累计影响
- 6 个 hermes 页面(sessions/extensions/env-editor/usage/lazy-deps/config)统一错误处理
- 修 usage.js 1 个真 bug(catch 参数名错配)
- 8 个新 i18n × 3 语言
- 1 个 CSS lint 警告修复
- npm build ✓
This commit is contained in:
晴天
2026-05-14 05:45:33 +08:00
parent debce2f810
commit dcac1d6d21
8 changed files with 34 additions and 19 deletions

View File

@@ -4,6 +4,7 @@
import { t } from '../../../lib/i18n.js'
import { api } from '../../../lib/tauri-api.js'
import { toast } from '../../../components/toast.js'
import { humanizeError } from '../../../lib/humanize-error.js'
export function render() {
const el = document.createElement('div')
@@ -61,7 +62,7 @@ export function render() {
const data = await api.hermesConfigRawRead()
yaml = data?.yaml || ''
} catch (err) {
error = String(err?.message || err).replace(/^Error:\s*/, '')
error = humanizeError(err, t('engine.hermesConfigLoadFailed') || 'Load config failed')
} finally {
loading = false
draw()
@@ -78,7 +79,7 @@ export function render() {
await api.hermesConfigRawWrite(yaml)
toast(t('engine.hermesConfigSaveSuccess'), 'success')
} catch (err) {
error = String(err?.message || err).replace(/^Error:\s*/, '')
error = humanizeError(err, t('engine.hermesConfigSaveFailed') || 'Save failed')
toast(error, 'error')
} finally {
saving = false

View File

@@ -9,6 +9,7 @@
*/
import { api } from '../../../lib/tauri-api.js'
import { toast } from '../../../components/toast.js'
import { humanizeError } from '../../../lib/humanize-error.js'
// NOTE: i18n keys for this page are not yet wired up in src/locales; using
// inline Chinese copy (with occasional English fallback) for now. When the
@@ -214,7 +215,7 @@ export function render() {
row.revealed = true
renderList()
} catch (err) {
toast(String(err).replace(/^Error:\s*/, ''), 'error')
toast(humanizeError(err, '读取失败'), 'error')
}
})
rowEl.querySelector('.env-cancel-btn')?.addEventListener('click', () => {
@@ -249,7 +250,7 @@ export function render() {
toast('已保存', 'success')
renderList()
} catch (err) {
toast(String(err).replace(/^Error:\s*/, ''), 'error')
toast(humanizeError(err, '保存失败'), 'error')
}
})
rowEl.querySelector('.env-delete-btn')?.addEventListener('click', async () => {
@@ -260,7 +261,7 @@ export function render() {
toast('已删除', 'success')
renderList()
} catch (err) {
toast(String(err).replace(/^Error:\s*/, ''), 'error')
toast(humanizeError(err, '删除失败'), 'error')
}
})
})
@@ -282,7 +283,7 @@ export function render() {
isNew: false,
}))
} catch (err) {
loadError = String(err).replace(/^Error:\s*/, '')
loadError = humanizeError(err, '加载失败')
const errEl = el.querySelector('#env-error')
if (errEl) {
errEl.textContent = loadError

View File

@@ -2,6 +2,7 @@ import { api } from '../../../lib/tauri-api.js'
import { icon } from '../../../lib/icons.js'
import { toast } from '../../../components/toast.js'
import { t } from '../../../lib/i18n.js'
import { humanizeError } from '../../../lib/humanize-error.js'
function esc(value) {
return String(value || '')
@@ -137,14 +138,14 @@ export function render() {
const probe = await api.hermesDashboardProbe().catch(() => ({ running: false, port: 9119 }))
if (probe?.running) {
try { await openWith(probe.port || 9119) }
catch (err) { toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error') }
catch (err) { toast(humanizeError(err, t('engine.dashNativePanelOpenFail')), 'error') }
return
}
// 2. auto-start
const r = await api.hermesDashboardStart().catch(() => ({ started: false, kind: 'spawn_failed', port: probe?.port || 9119 }))
if (r?.started) {
try { await openWith(r.port || 9119) }
catch (err) { toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error') }
catch (err) { toast(humanizeError(err, t('engine.dashNativePanelOpenFail')), 'error') }
return
}
// 3. 失败 → toastdashboard 页面有完整安装流程,这里只引导)
@@ -166,7 +167,7 @@ export function render() {
toast(t('engine.extensionsThemeSaved'), 'success')
draw()
} catch (err) {
toast(String(err?.message || err).replace(/^Error:\s*/, ''), 'error')
toast(humanizeError(err, t('engine.extensionsThemeSaveFailed') || 'Save theme failed'), 'error')
}
})
})
@@ -187,7 +188,7 @@ export function render() {
plugins = Array.isArray(pluginData) ? pluginData : []
analytics = usageData || null
} catch (err) {
error = String(err?.message || err).replace(/^Error:\s*/, '')
error = humanizeError(err, t('engine.extensionsLoadFailed') || 'Load failed')
} finally {
loading = false
draw()
@@ -200,7 +201,7 @@ export function render() {
await load()
toast(t('engine.extensionsPluginsRescanned'), 'success')
} catch (err) {
toast(String(err?.message || err).replace(/^Error:\s*/, ''), 'error')
toast(humanizeError(err, t('engine.extensionsRescanFailed') || 'Rescan failed'), 'error')
}
}

View File

@@ -71,7 +71,7 @@ async function loadAndRender(page) {
try {
featuresResp = await api.hermesLazyDepsFeatures()
} catch (e) {
content.innerHTML = `<div style="color:var(--error);padding:20px">${escapeHtml(t('hermesLazyDeps.loadFailed'))}: ${escapeHtml(String(e))}</div>`
content.innerHTML = `<div style="color:var(--error);padding:20px">${escapeHtml(humanizeError(e, t('hermesLazyDeps.loadFailed')))}</div>`
return
}

View File

@@ -4,6 +4,7 @@ import { toast } from '../../../components/toast.js'
import { showConfirm } from '../../../components/modal.js'
import { icon } from '../../../lib/icons.js'
import { getChatStore, getSourceLabel } from '../lib/chat-store.js'
import { humanizeError } from '../../../lib/humanize-error.js'
function escHtml(value) {
return String(value ?? '')
@@ -163,7 +164,7 @@ export function render() {
selected = new Set([...selected].filter(key => rows.some(s => sessionKey(s) === key)))
selectedKey = selectedKey && rows.some(s => sessionKey(s) === selectedKey) ? selectedKey : (visible[0] ? sessionKey(visible[0]) : null)
} catch (err) {
toast(String(err?.message || err), 'error')
toast(humanizeError(err, t('engine.sessionsLoadFailed') || 'Load sessions failed'), 'error')
} finally {
loading = false
draw()
@@ -184,7 +185,7 @@ export function render() {
session.source = session.source || detail?.source || ''
session.messageCount = session.messageCount || session.messages.length
} catch (err) {
toast(t('engine.sessionsDetailLoadFailed') + ': ' + (err?.message || err), 'error')
toast(humanizeError(err, t('engine.sessionsDetailLoadFailed')), 'error')
} finally {
detailLoadingKey = null
if (redraw) draw()
@@ -342,7 +343,7 @@ export function render() {
await store.switchSession(session.id)
window.location.hash = '#/h/chat'
} catch (err) {
toast(String(err?.message || err), 'error')
toast(humanizeError(err, t('engine.sessionsSwitchFailed') || 'Switch failed'), 'error')
} finally {
busy = false
draw()
@@ -364,7 +365,7 @@ export function render() {
if (session.profile === store.state.activeProfile) await store.loadSessions()
toast(t('engine.chatSessionDeleted'), 'success')
} catch (err) {
toast(t('engine.chatDeleteFailed') + ': ' + (err?.message || err), 'error')
toast(humanizeError(err, t('engine.chatDeleteFailed')), 'error')
}
draw()
}
@@ -475,7 +476,7 @@ export function render() {
URL.revokeObjectURL(url)
toast(t('engine.sessionsExportSuccess'), 'success')
} catch (err) {
toast(t('engine.sessionsExportFailed') + ': ' + (err?.message || err), 'error')
toast(humanizeError(err, t('engine.sessionsExportFailed')), 'error')
} finally {
btn.disabled = false
}

View File

@@ -1,6 +1,7 @@
import { api } from '../../../lib/tauri-api.js'
import { t } from '../../../lib/i18n.js'
import { icon } from '../../../lib/icons.js'
import { humanizeError } from '../../../lib/humanize-error.js'
const DAY_MS = 24 * 60 * 60 * 1000
@@ -374,8 +375,8 @@ export function render() {
if (!alive) return
sessions = Array.isArray(rows) ? rows : []
analytics = null
} catch (_) {
error = err?.message || String(err)
} catch (err) {
error = humanizeError(err, t('engine.usageLoadFailed') || 'Load usage failed')
}
} finally {
if (!alive) return

View File

@@ -5265,6 +5265,7 @@ body[data-active-engine="hermes"][data-theme="dark"] {
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@@ -614,6 +614,15 @@ export default {
hermesGroupChatRunFailed: _('运行失败', 'Run failed', '執行失敗'),
hermesGroupChatNoOutput: _('(无输出)', '(no output)', '(無輸出)'),
hermesGroupChatWebUnsupported: _('Web 模式不支持群聊(依赖 hermesAgentRun需桌面端事件桥。请用桌面客户端。', 'Group chat is not supported in Web mode (requires desktop event bridge). Use the desktop app.', 'Web 模式不支援群聊(依賴 hermesAgentRun需桌面端事件橋。請用桌面客戶端。'),
// Hermes UX 小白化:补齐 humanizeError fallback 用到的 i18n
sessionsLoadFailed: _('加载会话失败', 'Load sessions failed', '載入會話失敗'),
sessionsSwitchFailed: _('切换会话失败', 'Switch session failed', '切換會話失敗'),
usageLoadFailed: _('加载用量失败', 'Load usage failed', '載入用量失敗'),
extensionsThemeSaveFailed: _('保存主题失败', 'Save theme failed', '儲存主題失敗'),
extensionsLoadFailed: _('加载扩展失败', 'Load extensions failed', '載入擴充功能失敗'),
extensionsRescanFailed: _('重新扫描失败', 'Rescan failed', '重新掃描失敗'),
hermesConfigLoadFailed: _('加载配置失败', 'Load config failed', '載入設定失敗'),
hermesConfigSaveFailed: _('保存配置失败', 'Save config failed', '儲存設定失敗'),
// Web 模式(远程浏览器)下流式聊天暂不可用
chatWebModeStreamingUnsupported: _(
'Web 模式暂不支持 Hermes 实时流式聊天(依赖桌面端事件桥)。请打开桌面客户端使用此功能。',