/** * 扩展工具页面 * cftunnel 隧道管理 + ClawApp 状态 */ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' import { statusIcon } from '../lib/icons.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 = `
cftunnel 内网穿透
通过 Cloudflare Tunnel 将本地服务暴露到公网,无需公网 IP 和端口映射。
ClawApp 移动客户端
H5 移动聊天客户端,通过代理服务端连接 Gateway。支持本地和外网访问。
` 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 = `
加载失败: ${e}
` } } function renderCftunnel(el, s) { if (!s.installed) { el.innerHTML = `
cftunnel 未安装
查看文档
` return } const running = s.running const routes = s.routes || [] el.innerHTML = `
状态
${running ? '运行中' : '已停止'}
${s.tunnel_name || ''}${s.pid ? ' (PID: ' + s.pid + ')' : ''}
版本
${s.version || '未知'}
${routes.length} 条路由
${running ? '' : '' }
${renderRoutes(routes)}
` } function renderRoutes(routes) { if (!routes.length) return '
暂无路由
' return `
${routes.map(r => `
${escapeHtml(r.name)} 活跃
本地服务: ${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 = `
加载失败: ${e}
` } } function renderClawapp(el, s) { if (!s.installed) { el.innerHTML = `
ClawApp 未安装
查看文档
` return } const running = s.running el.innerHTML = `
状态
${running ? '运行中' : '已停止'}
${s.pid ? 'PID: ' + s.pid : ''}${s.port ? ' 端口: ' + s.port : ''}
访问地址
${s.url || 'http://localhost:3210'}
外网: chat.qrj.ai
打开 ClawApp 打开外网地址
` } // ===== 事件绑定 ===== 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' ? '启动' : '停止' 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(`隧道已${label}`, 'success') await loadCftunnel(page) } catch (e) { toast(`${label}失败: ${e}`, '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 = `
最近日志
${escapeHtml(logs) || '暂无日志'}
` } catch (e) { area.innerHTML = `
读取日志失败: ${e}
` } } async function handleInstallCftunnel(page) { const area = page.querySelector('#install-progress-area') if (!area) return // 显示进度条 area.innerHTML = `
准备安装...
` 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 = `安装中... ${progress}%` }) } catch { /* Web 模式无 Tauri event */ } } else { logBox.textContent += 'Web 模式:安装日志不可用,请等待完成...\n' } await api.installCftunnel() progressFill.classList.add('done') progressText.innerHTML = `${statusIcon('ok', 14)} 安装完成` toast('cftunnel 安装成功', 'success') // 3 秒后刷新状态 setTimeout(() => loadCftunnel(page), 3000) } catch (e) { progressFill.classList.add('error') progressText.innerHTML = `${statusIcon('err', 14)} 安装失败` logBox.textContent += '\n错误: ' + e toast('安装失败: ' + e, 'error') if (window.__openAIDrawerWithError) { window.__openAIDrawerWithError({ title: '安装 cftunnel 失败', error: logBox.textContent, scene: '安装 cftunnel 内网穿透工具', hint: String(e), }) } } finally { unlistenLog?.() unlistenProgress?.() } } async function handleInstallClawapp(page) { const area = page.querySelector('#install-clawapp-progress-area') if (!area) return area.innerHTML = `
准备安装...
` 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 = `安装中... ${progress}%` }) } catch { /* Web 模式无 Tauri event */ } } else { logBox.textContent += 'Web 模式:安装日志不可用,请等待完成...\n' } await api.installClawapp() progressFill.classList.add('done') progressText.innerHTML = `${statusIcon('ok', 14)} 安装完成` toast('ClawApp 安装成功', 'success') setTimeout(() => loadClawapp(page), 3000) } catch (e) { progressFill.classList.add('error') progressText.innerHTML = `${statusIcon('err', 14)} 安装失败` logBox.textContent += '\n错误: ' + e toast('安装失败: ' + e, 'error') if (window.__openAIDrawerWithError) { window.__openAIDrawerWithError({ title: '安装 ClawApp 失败', error: logBox.textContent, scene: '安装 ClawApp 手机客户端', hint: String(e), }) } } finally { unlistenLog?.() unlistenProgress?.() } }