fix(web-mode): consolidate Tauri event subscription helper to silence transformCallback errors (#256)

- Add shared safeTauriListen helper in tauri-api.js that returns a noop
  unsubscriber when running outside Tauri, so dynamic-importing
  @tauri-apps/api/event in the browser no longer throws
  'Cannot read properties of undefined (reading transformCallback)'.
- Replace 4 bare 'await import(@tauri-apps/api/event)' call sites
  (about.js hermes upgrade button + channels.js three install/action flows)
  that previously crashed the page on web mode.
- Drop the duplicated local tauriListen helpers in hermes dashboard / chat
  store and route them through the shared helper.
This commit is contained in:
晴天
2026-05-14 01:31:58 +08:00
parent 081ad4af25
commit d0d6950628
5 changed files with 36 additions and 48 deletions

View File

@@ -20,7 +20,7 @@
* - File attachment uploads (kept out of scope for Phase 4).
* - Full tmux-like run resume (Tauri events are in-process and reliable).
*/
import { api, isTauriRuntime } from '../../../lib/tauri-api.js'
import { api, isTauriRuntime, safeTauriListen } from '../../../lib/tauri-api.js'
// ---------- constants ----------
@@ -208,24 +208,6 @@ function mapSessionSummary(s) {
// ---------- Tauri event bridge ----------
//
// Streaming relies on Tauri's `hermes-run-*` events. In Web mode (远程浏览器
// 访问 ClawPanelthese events don't exist — and importing
// `@tauri-apps/api/event` itself touches `window.__TAURI_INTERNALS__.transformCallback`
// which crashes with "Cannot read properties of undefined (reading 'transformCallback')".
//
// To stay safe we short-circuit to a no-op unsubscriber when not running inside
// Tauri.
let _listenFn = null
async function tauriListen(event, cb) {
if (!isTauriRuntime()) return () => {}
if (!_listenFn) {
const mod = await import('@tauri-apps/api/event')
_listenFn = mod.listen
}
return _listenFn(event, cb)
}
// ---------- store implementation ----------
function createStore() {
@@ -645,7 +627,7 @@ function createStore() {
async function attachStreamListeners(runSessionId) {
detachStreamListeners()
const runSession = () => state.sessions.find(x => x.id === runSessionId) || null
const u1 = await tauriListen('hermes-run-delta', (e) => {
const u1 = await safeTauriListen('hermes-run-delta', (e) => {
const delta = e?.payload?.delta || ''
if (!delta) return
const s = runSession()
@@ -659,7 +641,7 @@ function createStore() {
msg.content += delta
notify()
})
const u2 = await tauriListen('hermes-run-tool', (e) => {
const u2 = await safeTauriListen('hermes-run-tool', (e) => {
const evt = e?.payload || {}
const evtType = evt.event || ''
const toolName = evt.tool || evt.tool_name || evt.name || 'tool'
@@ -703,7 +685,7 @@ function createStore() {
}
notify()
})
const u3 = await tauriListen('hermes-run-done', () => {
const u3 = await safeTauriListen('hermes-run-done', () => {
const s = runSession()
if (!s) { cleanupAfterRun(); return }
@@ -740,7 +722,7 @@ function createStore() {
persistSessions()
cleanupAfterRun()
})
const u4 = await tauriListen('hermes-run-error', (e) => {
const u4 = await safeTauriListen('hermes-run-error', (e) => {
const err = e?.payload?.error || 'unknown error'
const s = runSession()
if (s) {

View File

@@ -2,7 +2,7 @@
* Hermes Agent 仪表盘
*/
import { t } from '../../../lib/i18n.js'
import { api, isTauriRuntime } from '../../../lib/tauri-api.js'
import { api, safeTauriListen } from '../../../lib/tauri-api.js'
import {
loadHermesProviders,
inferProviderByBaseUrl,
@@ -20,20 +20,6 @@ const ICONS = {
// Provider registry—异步加载第一次 render 前填充
let hermesProviders = []
// Lazy Tauri event listen (avoid top-level await for vite build).
// Web 模式下 `@tauri-apps/api/event` 的模块顶层会触碰 `window.__TAURI_INTERNALS__.transformCallback`
// 导致 "Cannot read properties of undefined (reading 'transformCallback')"
// 因此非 Tauri 环境直接 noop。
let _listenFn = null
async function tauriListen(event, cb) {
if (!isTauriRuntime()) return () => {}
if (!_listenFn) {
const mod = await import('@tauri-apps/api/event')
_listenFn = mod.listen
}
return _listenFn(event, cb)
}
const HERMES_DASHBOARD_URL = 'http://127.0.0.1:9119/'
/**
@@ -862,7 +848,7 @@ export function render() {
async function setupListeners() {
try {
// 监听 Guardian 推送的状态变化
const unlisten1 = await tauriListen('hermes-gateway-status', (evt) => {
const unlisten1 = await safeTauriListen('hermes-gateway-status', (evt) => {
const data = evt.payload
if (info) {
const wasRunning = info.gatewayRunning
@@ -877,13 +863,13 @@ export function render() {
unlisteners.push(unlisten1)
// 监听 Guardian 日志(显示在消息区)
const unlisten2 = await tauriListen('hermes-guardian-log', (evt) => {
const unlisten2 = await safeTauriListen('hermes-guardian-log', (evt) => {
showGwMsg(evt.payload || '', false)
})
unlisteners.push(unlisten2)
// 监听 config.yaml 自愈事件api_server guardian
const unlisten3 = await tauriListen('hermes-config-patched', async (evt) => {
const unlisten3 = await safeTauriListen('hermes-config-patched', async (evt) => {
const { toast } = await import('../../../components/toast.js')
const msg = evt?.payload?.message || t('engine.dashConfigPatched')
toast(msg, 'info', { duration: 6000 })

View File

@@ -9,6 +9,27 @@ export function isTauriRuntime() {
return !!window.__TAURI_INTERNALS__ || !!window.__TAURI__ || window.location?.hostname === 'tauri.localhost'
}
let _tauriListenFn = null
/**
* 安全订阅 Tauri 事件。Web 模式下返回 noop unsubscriber
* 避免动态 import `@tauri-apps/api/event` 时触碰
* `window.__TAURI_INTERNALS__.transformCallback` 引发
* "Cannot read properties of undefined" 报错issue #256
*
* 用法:
* const unlisten = await safeTauriListen('hermes-install-log', e => ...)
* unlisten() // 取消订阅
*/
export async function safeTauriListen(event, cb) {
if (!isTauriRuntime()) return () => {}
if (!_tauriListenFn) {
const mod = await import('@tauri-apps/api/event')
_tauriListenFn = mod.listen
}
return _tauriListenFn(event, cb)
}
// 仅在 Node.js 后端实现的命令Tauri Rust 不处理),强制走 webInvoke
const WEB_ONLY_CMDS = new Set([
'instance_list', 'instance_add', 'instance_remove', 'instance_set_active',

View File

@@ -2,7 +2,7 @@
* 关于页面
* 版本信息、项目链接、相关项目、系统环境
*/
import { api } from '../lib/tauri-api.js'
import { api, safeTauriListen } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showUpgradeModal, showConfirm, showContentModal } from '../components/modal.js'
import { setUpgrading } from '../lib/app-state.js'
@@ -217,8 +217,7 @@ async function loadHermesData(page) {
let unlisten = null
try {
const { listen } = await import('@tauri-apps/api/event')
unlisten = await listen('hermes-install-log', (e) => {
unlisten = await safeTauriListen('hermes-install-log', (e) => {
modal.appendLog(String(e.payload))
})
} catch (_) {}

View File

@@ -2,7 +2,7 @@
* 消息渠道管理
* 渠道列表 + Agent 对接(多绑定、独立配置、渠道测试)
*/
import { api, invalidate } from '../lib/tauri-api.js'
import { api, invalidate, safeTauriListen } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showContentModal, showConfirm } from '../components/modal.js'
import { icon } from '../lib/icons.js'
@@ -1530,7 +1530,7 @@ async function openConfigDialog(pid, page, state, accountId) {
const logBox = actionResultEl.querySelector('#channel-action-log-box')
const progressBar = actionResultEl.querySelector('#channel-action-progress-bar')
const progressText = actionResultEl.querySelector('#channel-action-progress-text')
const { listen } = await import('@tauri-apps/api/event')
const listen = safeTauriListen
let unlistenLog = null, unlistenProgress = null
let _qrTimer = null
const cleanup = () => { unlistenLog?.(); unlistenProgress?.(); clearTimeout(_qrTimer) }
@@ -1923,7 +1923,7 @@ async function openConfigDialog(pid, page, state, accountId) {
const logBox = actionResultEl.querySelector('#channel-action-log-box')
const progressBar = actionResultEl.querySelector('#channel-action-progress-bar')
const progressText = actionResultEl.querySelector('#channel-action-progress-text')
const { listen } = await import('@tauri-apps/api/event')
const listen = safeTauriListen
let unlistenLog = null
let unlistenProgress = null
let unlistenDone = null
@@ -2110,7 +2110,7 @@ async function openConfigDialog(pid, page, state, accountId) {
const progressText = resultEl.querySelector('#plugin-progress-text')
let unlistenLog, unlistenProgress
try {
const { listen } = await import('@tauri-apps/api/event')
const listen = safeTauriListen
unlistenLog = await listen('plugin-log', (e) => {
logBox.textContent += e.payload + '\n'
logBox.scrollTop = logBox.scrollHeight