From 743af933bd794635873e11148bfd9a1597502e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Tue, 10 Mar 2026 00:12:34 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=81=A2=E5=A4=8D=E6=9C=AA=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E7=A7=BB=E9=99=A4=E7=9A=84=E6=89=A9=E5=B1=95=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/extensions.js | 397 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 src/pages/extensions.js diff --git a/src/pages/extensions.js b/src/pages/extensions.js new file mode 100644 index 0000000..7413b72 --- /dev/null +++ b/src/pages/extensions.js @@ -0,0 +1,397 @@ +/** + * 扩展工具页面 + * 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?.() + } +}