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.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
+
+
+
+ `
+}
+
+// ===== 事件绑定 =====
+
+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?.()
+ }
+}