fix(audit): 复查第二波 — alert/confirm 统一 + chat.js 潜伏语法 bug

## Bug #6 — chat.js 末尾多余 `}` 让 Vite build 卡死
src/engines/hermes/pages/chat.js

文件结尾 line 1635 已经闭合 render 函数,但 line 1636 多了一个孤立的
`}`,导致 esbuild 解析陷入回溯(build 卡在 transforming 47 modules
不退出)。删除多余括号后 build 1.55s 通过。

## Bug #7 — alert() / window.confirm 跨平台行为不一致
- src/engines/hermes/pages/setup.js  3 处 alert()
  · API Key 必填校验
  · 配置保存失败
  · Gateway 启动失败 fallback
- src/engines/hermes/pages/memory.js  2 处 window.confirm
  · 关闭未保存编辑窗
  · 取消编辑前的确认
- src/engines/hermes/pages/env-editor.js  1 处 confirm()
  · 删除环境变量

Tauri webview 在 macOS / Windows / Linux 三平台对原生 alert/confirm
处理不同(macOS 需要 webview 设置 navigation_handler,部分场景直接
忽略)。统一改用项目 components/toast + components/modal.showConfirm,
保证跨平台一致 + 可被样式化。

涉及函数 closeWithConfirm 改为 async 以等待 showConfirm Promise。

## Bug #8 — gateways.js 错误显示与其他页面风格不一致
src/engines/hermes/pages/gateways.js

`error = String(e?.message || e)` 直接显示 raw error 字符串,与
profiles/kanban/oauth 三个最近升级的页面不一致。改用 humanizeError
让用户看到友好提示 + 可折叠原始错误详情。

emoji ⚙️ 设置图标改用项目 svg-icons.js 的 settings SVG,与 Bug #5
emoji→SVG 重构保持一致。

## 验证
- npm run build:PASS(1.55s)
- 受影响场景:
  - 安装向导 API Key 缺失提示(toast 而不是阻塞 alert)
  - 记忆页未保存确认(modal 而不是平台原生 confirm)
  - 环境变量删除确认(modal 而不是平台原生 confirm)
  - 多 Gateway 加载失败错误展示(友好 + 可展开技术详情)
This commit is contained in:
晴天
2026-05-14 07:24:47 +08:00
parent b9a7c043d2
commit d30714d406
5 changed files with 37 additions and 13 deletions

View File

@@ -780,7 +780,7 @@ export function render() {
if (hermesInstalled === false) {
return `
<div class="hm-chat-health-banner is-error">
<span class="hm-chat-health-icon" aria-hidden="true"></span>
<span class="hm-chat-health-icon" aria-hidden="true">${svgIcon('alert-triangle', { size: 14 })}</span>
<span class="hm-chat-health-msg">${escHtml(t('engine.chatHealthInstallMissing'))}</span>
<a class="hm-chat-health-action" href="#/h/dashboard">${escHtml(t('engine.chatHealthGoDashboard'))}</a>
</div>
@@ -789,7 +789,7 @@ export function render() {
if (!gwOnline) {
return `
<div class="hm-chat-health-banner is-warn">
<span class="hm-chat-health-icon" aria-hidden="true"></span>
<span class="hm-chat-health-icon" aria-hidden="true">${svgIcon('alert-triangle', { size: 14 })}</span>
<span class="hm-chat-health-msg">${escHtml(t('engine.chatHealthGatewayDown'))}</span>
<a class="hm-chat-health-action" href="#/h/dashboard">${escHtml(t('engine.chatHealthGoDashboard'))}</a>
</div>

View File

@@ -9,6 +9,7 @@
*/
import { api } from '../../../lib/tauri-api.js'
import { toast } from '../../../components/toast.js'
import { showConfirm } from '../../../components/modal.js'
import { humanizeError } from '../../../lib/humanize-error.js'
// NOTE: i18n keys for this page are not yet wired up in src/locales; using
@@ -254,7 +255,12 @@ export function render() {
}
})
rowEl.querySelector('.env-delete-btn')?.addEventListener('click', async () => {
if (!confirm(`确定删除 ${row.key} 吗?`)) return
const ok = await showConfirm({
message: `确定删除 ${row.key} 吗?`,
confirmText: '删除',
variant: 'danger',
})
if (!ok) return
try {
await api.hermesEnvDelete(row.key)
rows.splice(idx, 1)

View File

@@ -18,6 +18,7 @@ import { api } from '../../../lib/tauri-api.js'
import { toast } from '../../../components/toast.js'
import { showModal, showConfirm } from '../../../components/modal.js'
import { humanizeError } from '../../../lib/humanize-error.js'
import { svgIcon } from '../lib/svg-icons.js'
function escHtml(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
@@ -52,7 +53,7 @@ export function render() {
${error ? `<div style="color:var(--error);padding:20px">${escHtml(error)}</div>` : ''}
${(!loading && !error && !gateways.length) ? `
<div class="empty-state empty-compact">
<div class="empty-icon">⚙️</div>
<div class="empty-icon">${svgIcon('settings', { size: 32 })}</div>
<div class="empty-title">${escHtml(t('engine.hermesGatewaysEmpty'))}</div>
<div class="empty-desc" style="margin-top:8px">${escHtml(t('engine.hermesGatewaysEmptyHint'))}</div>
</div>` : ''}
@@ -123,7 +124,8 @@ export function render() {
profiles = arr.map(p => (typeof p === 'string' ? p : (p.name || ''))).filter(Boolean)
if (!profiles.includes('default')) profiles.unshift('default')
} catch (e) {
error = String(e?.message || e)
// 保留 Error 对象、给 humanizeError 输出友好提示
error = humanizeError(e, t('engine.hermesGatewaysLoadFailed') || 'Load failed')
} finally {
loading = false
draw()

View File

@@ -12,7 +12,7 @@
import { t } from '../../../lib/i18n.js'
import { api } from '../../../lib/tauri-api.js'
import { toast } from '../../../components/toast.js'
import { showContentModal } from '../../../components/modal.js'
import { showContentModal, showConfirm } from '../../../components/modal.js'
import { humanizeError } from '../../../lib/humanize-error.js'
function escHtml(s) {
@@ -135,13 +135,20 @@ export function render() {
const ta = overlay.querySelector('#hm-mem-modal-textarea')
const cancelBtn = overlay.querySelector('[data-action="cancel"]')
const saveBtn = overlay.querySelector('#hm-mem-modal-save')
const closeWithConfirm = () => {
const closeWithConfirm = async () => {
if (!editing) {
overlay.remove()
return
}
const dirty = editing.buffer !== (data[editing.key] || '')
if (dirty && !confirm(t('engine.memoryUnsaved'))) return
if (dirty) {
const ok = await showConfirm({
message: t('engine.memoryUnsaved'),
confirmText: t('common.confirm') || 'OK',
variant: 'danger',
})
if (!ok) return
}
editing = null
overlay.remove()
}
@@ -183,10 +190,17 @@ export function render() {
})
}
function cancelEdit() {
async function cancelEdit() {
if (!editing) return
const dirty = editing.buffer !== (data[editing.key] || '')
if (dirty && !confirm(t('engine.memoryUnsaved'))) return
if (dirty) {
const ok = await showConfirm({
message: t('engine.memoryUnsaved'),
confirmText: t('common.confirm') || 'OK',
variant: 'danger',
})
if (!ok) return
}
editing = null
document.querySelector('.hm-mem-modal-overlay')?.remove()
draw()

View File

@@ -5,6 +5,7 @@
*/
import { t } from '../../../lib/i18n.js'
import { api, invalidate, isTauriRuntime } from '../../../lib/tauri-api.js'
import { toast } from '../../../components/toast.js'
import { getActiveEngine } from '../../../lib/engine-manager.js'
import {
loadHermesProviders,
@@ -611,7 +612,7 @@ export function render() {
const provider = matched?.id || 'custom'
if (!apiKey) {
alert('请输入 API Key')
toast(t('engine.installCustomEmpty') || '请输入 API Key', 'warning')
return
}
try {
@@ -619,7 +620,8 @@ export function render() {
phase = 'gateway'
await refreshHermes()
} catch (e) {
alert(`配置保存失败: ${e}`)
const msg = String(e?.message || e).replace(/^Error:\s*/, '')
toast(`${t('engine.configSaveFailed') || '配置保存失败'}: ${msg}`, 'error')
}
}
@@ -640,7 +642,7 @@ export function render() {
errEl.textContent = msg || t('engine.gatewayStartFailed')
errEl.style.display = 'block'
} else {
alert(msg || t('engine.gatewayStartFailed'))
toast(msg || t('engine.gatewayStartFailed'), 'error')
}
} finally {
gwStarting = false