/**
* 扩展工具页面
* 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')}
`
return
}
const running = s.running
const routes = s.routes || []
el.innerHTML = `
${running ? t('ext.running') : t('ext.stopped')}
${s.tunnel_name || ''}${s.pid ? ' (PID: ' + s.pid + ')' : ''}
${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 => `
${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')}
`
return
}
const running = s.running
el.innerHTML = `
${running ? t('ext.running') : t('ext.stopped')}
${s.pid ? 'PID: ' + s.pid : ''}${s.port ? ' ' + t('ext.port') + ': ' + s.port : ''}
${s.url || 'http://localhost:3210'}
${t('ext.publicUrl')}: 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' ? 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 = `
`
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 = `
`
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?.()
}
}