mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
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:
@@ -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)`)
|
||||
|
||||
Reference in New Issue
Block a user