diff --git a/src/engines/hermes/lib/chat-store.js b/src/engines/hermes/lib/chat-store.js index 1ed9194..83d750e 100644 --- a/src/engines/hermes/lib/chat-store.js +++ b/src/engines/hermes/lib/chat-store.js @@ -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 (远程浏览器 -// 访问 ClawPanel)these 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) { diff --git a/src/engines/hermes/pages/dashboard.js b/src/engines/hermes/pages/dashboard.js index 0660cab..c1f006f 100644 --- a/src/engines/hermes/pages/dashboard.js +++ b/src/engines/hermes/pages/dashboard.js @@ -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 }) diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index af42aa0..ac833d6 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -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', diff --git a/src/pages/about.js b/src/pages/about.js index 2352cf2..67379c3 100644 --- a/src/pages/about.js +++ b/src/pages/about.js @@ -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 (_) {} diff --git a/src/pages/channels.js b/src/pages/channels.js index 4880875..0b6b035 100644 --- a/src/pages/channels.js +++ b/src/pages/channels.js @@ -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