fix(critical): 5 个用户报告的真 bug 一次性修

## P0 Bug 1: chat.js `t is not defined` (页面渲染崩溃)
- chat.js 用了 30+ 处 t() 但**完全忘记 import i18n**
- 路由进入 /h/chat 立即抛 ReferenceError,整页渲染失败
- 修复:补 `import { t } from '../../../lib/i18n.js'`

## P0 Bug 2: i18n 嵌套对象解析失败 (所有 humanizeError 显示原始 key)
- 用户截图:`common.errorHint.generic` 直接显示而非翻译
- 根因:buildLocales() 不递归嵌套对象,把 common.error / errorHint /
  errorAction 这种嵌套 dict 当作 _() 翻译对象处理 →
  `result[lang].common.errorHint = 'errorHint'` (字符串)
- 影响:t('common.errorHint.network'/auth/timeout/...) 全部返回 key 自身
  → toast 用 humanizeError 的所有页面都看到原始 key
- 修复:buildLocales 加 _isTranslationObject + _materialize 递归

## P0 Bug 3: dev-api.js `Dynamic require of "node:fs" is not supported`
- 用户截图:文件管理器红字 "Dynamic require of node:fs"
- 根因:我之前在 hermes_fs_* / _validateFsPath / _hermesHome 用了
  `const fs = require('node:fs')` —— 但 Vite 插件运行在 ESM 上下文,
  CommonJS dynamic require 不支持
- 修复:删除所有 require('node:fs/path/os') —— 文件顶部已经 ESM
  `import fs from 'fs'` 等,直接复用即可

## P1 Bug 4: profiles/kanban/oauth fetch failed 错误丑陋
- 用户截图:3 个页面都显示红字 "fetch failed"(无任何上下文)
- 修复 1(dev-api.js):hermes_dashboard_api_proxy 把 ECONNREFUSED /
  fetch failed 转成 "Hermes Dashboard 未运行(端口 9119 无服务)。
  请在桌面端 ClawPanel 启动 Hermes Agent" — 含「未运行」关键字会被
  humanizeError 的 gatewayDown 正则匹配
- 修复 2(3 个页面):用 humanizeError 拆出 message + hint + 折叠 raw,
  渲染统一的 .page-inline-error 卡片(icon + 主行 + 副提示 + 技术详情
  details)。错误对象不再 stringify,保留给 humanizeError 完整识别

## P2 Bug 5: 重复的 require('node:path') in hermes_fs_list
- 同 Bug 3,循环里又 require 了一次 path,现已用顶部 import

## 影响面
 /h/chat 不再崩溃,profile switcher 正常显示
 所有用 humanizeError 的 toast 看到正确翻译(不再显示原始 key)
 文件管理器 Web 模式正常列出 ~/.hermes 目录
 Profile / Kanban / OAuth 显示「Hermes Dashboard 未运行」+
   「请前往仪表盘启动 Gateway 后重试」+ 折叠原始错误(点击展开)
 npm build pass
This commit is contained in:
晴天
2026-05-14 06:35:20 +08:00
parent 6a0d87479c
commit 1873e23371
7 changed files with 164 additions and 22 deletions

View File

@@ -7227,15 +7227,10 @@ const handlers = {
// Batch 3 §L: 文件管理器Web 模式走 Node fs限定 hermes_home 子树)
_hermesHome() {
const fs = require('node:fs')
const path = require('node:path')
const os = require('node:os')
if (process.env.HERMES_HOME) return process.env.HERMES_HOME
return path.join(os.homedir(), '.hermes')
},
_validateFsPath(rel) {
const fs = require('node:fs')
const path = require('node:path')
const root = handlers._hermesHome()
const target = rel ? path.resolve(root, rel) : root
const canonRoot = fs.realpathSync.native?.(root) || root
@@ -7243,14 +7238,13 @@ const handlers = {
return target
},
async hermes_fs_list({ path: p = '' } = {}) {
const fs = require('node:fs')
const target = handlers._validateFsPath(p)
if (!fs.existsSync(target)) throw new Error(`目录不存在: ${target}`)
const stat = fs.statSync(target)
if (!stat.isDirectory()) throw new Error(`不是目录: ${target}`)
let entries = fs.readdirSync(target, { withFileTypes: true }).filter(e => !e.name.startsWith('.') || e.name === '.env')
entries = entries.slice(0, 2000).map(e => {
const sub = require('node:path').join(target, e.name)
const sub = path.join(target, e.name)
const m = fs.statSync(sub)
return {
name: e.name,
@@ -7263,7 +7257,6 @@ const handlers = {
return { path: target, entries }
},
async hermes_fs_read({ path: p } = {}) {
const fs = require('node:fs')
const target = handlers._validateFsPath(p)
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error(`不是文件: ${target}`)
const stat = fs.statSync(target)
@@ -7275,7 +7268,6 @@ const handlers = {
return { path: target, size: stat.size, text, binary_b64 }
},
async hermes_fs_write({ path: p, content } = {}) {
const fs = require('node:fs')
const target = handlers._validateFsPath(p)
if (Buffer.byteLength(content || '', 'utf8') > 5 * 1024 * 1024) throw new Error('内容过大')
fs.writeFileSync(target, content || '', 'utf8')
@@ -7318,12 +7310,29 @@ const handlers = {
}
return opts
}
// 把网络错误fetch failed / ECONNREFUSED转成友好错方便前端 humanizeError 归类
const friendly = (err) => {
const msg = String(err?.message || err || '')
if (/fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|aborted/i.test(msg)) {
return new Error(`Hermes Dashboard 未运行(端口 ${port} 无服务)。请在桌面端 ClawPanel 启动 Hermes Agent或在 Settings 中配置远端 Dashboard 地址`)
}
return err instanceof Error ? err : new Error(msg)
}
let token = await handlers._getDashboardToken(port, false).catch(() => null)
let resp = await globalThis.fetch(url, buildOpts(token))
let resp
try {
resp = await globalThis.fetch(url, buildOpts(token))
} catch (err) {
throw friendly(err)
}
if (resp.status === 401) {
// 强制刷新 + 重试
token = await handlers._getDashboardToken(port, true)
resp = await globalThis.fetch(url, buildOpts(token))
try {
token = await handlers._getDashboardToken(port, true)
resp = await globalThis.fetch(url, buildOpts(token))
} catch (err) {
throw friendly(err)
}
}
const text = await resp.text().catch(() => '')
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text}(提示:请先启动 Dashboard`)