/** * 扩展工具页面 * cftunnel 隧道管理 + ClawApp 状态 */ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' import { humanizeError } from '../lib/humanize-error.js' import { statusIcon } from '../lib/icons.js' import { t } from '../lib/i18n.js' // HTML 转义,防止 XSS function escapeHtml(str) { if (!str) return '' return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `
${t('ext.cftunnelTitle')}
${t('ext.cftunnelDesc')}
${t('ext.clawappTitle')}
${t('ext.clawappDesc')}
` bindEvents(page) loadAll(page) return page } async function loadAll(page) { await Promise.all([ loadCftunnel(page), loadClawapp(page), ]) } // ===== cftunnel ===== async function loadCftunnel(page) { const el = page.querySelector('#cftunnel-content') try { const status = await api.getCftunnelStatus() renderCftunnel(el, status) } catch (e) { el.innerHTML = `
${t('common.loadFailed')}: ${e}
` } } function renderCftunnel(el, s) { if (!s.installed) { el.innerHTML = `
${t('ext.cftunnelNotInstalled')}
${t('ext.viewDocs')}
` return } const running = s.running const routes = s.routes || [] el.innerHTML = `
${t('ext.status')}
${running ? t('ext.running') : t('ext.stopped')}
${s.tunnel_name || ''}${s.pid ? ' (PID: ' + s.pid + ')' : ''}
${t('ext.version')}
${s.version || t('ext.unknown')}
${routes.length} ${t('ext.routes')}
${running ? '' : '' }
${renderRoutes(routes)}
` } function renderRoutes(routes) { if (!routes.length) return '
' + t('ext.noRoutes') + '
' return `
${routes.map(r => `
${escapeHtml(r.name)} ${t('ext.active')}
${t('ext.localService')}: ${escapeHtml(r.service)}
`).join('')}
` } // ===== ClawApp ===== async function loadClawapp(page) { const el = page.querySelector('#clawapp-content') try { const status = await api.getClawappStatus() renderClawapp(el, status) } catch (e) { el.innerHTML = `
${t('common.loadFailed')}: ${e}
` } } function renderClawapp(el, s) { if (!s.installed) { el.innerHTML = `
${t('ext.clawappNotInstalled')}
${t('ext.viewDocs')}
` return } const running = s.running el.innerHTML = `
${t('ext.status')}
${running ? t('ext.running') : t('ext.stopped')}
${s.pid ? 'PID: ' + s.pid : ''}${s.port ? ' ' + t('ext.port') + ': ' + s.port : ''}
${t('ext.accessUrl')}
${s.url || 'http://localhost:3210'}
${t('ext.publicUrl')}: chat.qrj.ai
${t('ext.openClawapp')} ${t('ext.openPublicUrl')}
` } // ===== 事件绑定 ===== function bindEvents(page) { page.addEventListener('click', async (e) => { const btn = e.target.closest('[data-action]') if (!btn) return const action = btn.dataset.action switch (action) { case 'cftunnel-up': await handleCftunnelAction(page, 'up') break case 'cftunnel-down': await handleCftunnelAction(page, 'down') break case 'cftunnel-logs': await handleCftunnelLogs(page) break case 'cftunnel-refresh': await loadCftunnel(page) break case 'clawapp-refresh': await loadClawapp(page) break case 'install-cftunnel': await handleInstallCftunnel(page) break case 'install-clawapp': await handleInstallClawapp(page) break } }) } async function handleCftunnelAction(page, action) { const label = action === 'up' ? t('ext.start') : t('ext.stop') const btn = page.querySelector(`[data-action="cftunnel-${action === 'up' ? 'up' : 'down'}"]`) if (btn) { btn.classList.add('btn-loading'); btn.disabled = true; btn.textContent = `${label}...` } try { await api.cftunnelAction(action) toast(t('ext.tunnelActionDone', { action: label }), 'success') await loadCftunnel(page) } catch (e) { toast(humanizeError(e, t('ext.tunnelActionFail', { action: label })), 'error') if (btn) { btn.classList.remove('btn-loading'); btn.disabled = false; btn.textContent = label } } } async function handleCftunnelLogs(page) { const area = page.querySelector('#cftunnel-logs-area') if (!area) return // 切换显示 if (area.innerHTML) { area.innerHTML = '' return } try { const logs = await api.getCftunnelLogs(30) area.innerHTML = `
${t('ext.recentLogs')}
${escapeHtml(logs) || t('ext.noLogs')}
` } catch (e) { area.innerHTML = `
${t('ext.readLogsFailed')}: ${e}
` } } async function handleInstallCftunnel(page) { const area = page.querySelector('#install-progress-area') if (!area) return // 显示进度条 area.innerHTML = `
${t('ext.preparing')}
` const progressFill = area.querySelector('#install-progress-fill') const progressText = area.querySelector('#install-progress-text') const logBox = area.querySelector('#install-log-box') let unlistenLog, unlistenProgress try { if (window.__TAURI_INTERNALS__) { try { const { listen } = await import('@tauri-apps/api/event') unlistenLog = await listen('install-log', (e) => { logBox.textContent += e.payload + '\n' logBox.scrollTop = logBox.scrollHeight }) unlistenProgress = await listen('install-progress', (e) => { const progress = e.payload progressFill.style.width = progress + '%' progressText.textContent = t('ext.installing') + ` ${progress}%` }) } catch { /* Web mode no Tauri event */ } } else { logBox.textContent += t('ext.webModeNoLogs') + '\n' } await api.installCftunnel() progressFill.classList.add('done') progressText.innerHTML = `${statusIcon('ok', 14)} ${t('ext.installDone')}` toast(t('ext.installSuccess', { name: 'cftunnel' }), 'success') // 3 秒后刷新状态 setTimeout(() => loadCftunnel(page), 3000) } catch (e) { progressFill.classList.add('error') progressText.innerHTML = `${statusIcon('err', 14)} ${t('ext.installFailed')}` logBox.textContent += '\n' + t('ext.error') + ': ' + e toast(humanizeError(e, t('ext.installFailed')), 'error') if (window.__openAIDrawerWithError) { window.__openAIDrawerWithError({ title: t('ext.installFailedTitle', { name: 'cftunnel' }), error: logBox.textContent, scene: t('ext.installScene', { name: 'cftunnel' }), hint: String(e), }) } } finally { unlistenLog?.() unlistenProgress?.() } } async function handleInstallClawapp(page) { const area = page.querySelector('#install-clawapp-progress-area') if (!area) return area.innerHTML = `
${t('ext.preparing')}
` const progressFill = area.querySelector('#install-clawapp-progress-fill') const progressText = area.querySelector('#install-clawapp-progress-text') const logBox = area.querySelector('#install-clawapp-log-box') let unlistenLog, unlistenProgress try { if (window.__TAURI_INTERNALS__) { try { const { listen } = await import('@tauri-apps/api/event') unlistenLog = await listen('install-log', (e) => { logBox.textContent += e.payload + '\n' logBox.scrollTop = logBox.scrollHeight }) unlistenProgress = await listen('install-progress', (e) => { const progress = e.payload progressFill.style.width = progress + '%' progressText.textContent = t('ext.installing') + ` ${progress}%` }) } catch { /* Web mode no Tauri event */ } } else { logBox.textContent += t('ext.webModeNoLogs') + '\n' } await api.installClawapp() progressFill.classList.add('done') progressText.innerHTML = `${statusIcon('ok', 14)} ${t('ext.installDone')}` toast(t('ext.installSuccess', { name: 'ClawApp' }), 'success') setTimeout(() => loadClawapp(page), 3000) } catch (e) { progressFill.classList.add('error') progressText.innerHTML = `${statusIcon('err', 14)} ${t('ext.installFailed')}` logBox.textContent += '\n' + t('ext.error') + ': ' + e toast(humanizeError(e, t('ext.installFailed')), 'error') if (window.__openAIDrawerWithError) { window.__openAIDrawerWithError({ title: t('ext.installFailedTitle', { name: 'ClawApp' }), error: logBox.textContent, scene: t('ext.installScene', { name: 'ClawApp' }), hint: String(e), }) } } finally { unlistenLog?.() unlistenProgress?.() } }