mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
feat: 新增「关于」和「扩展工具」页面
- 关于页面:版本信息、相关项目链接、快捷链接、开源协议 - 扩展页面:cftunnel 隧道状态/路由/启停/日志 + ClawApp 状态/快捷访问 - Rust 后端:新增 extensions.rs(4 个命令:状态/操作/日志/ClawApp 检测) - 侧边栏新增「扩展」和「关于」导航项,总计 8 个页面
This commit is contained in:
160
src-tauri/src/commands/extensions.rs
Normal file
160
src-tauri/src/commands/extensions.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
/// 扩展工具命令(cftunnel + ClawApp)
|
||||
use serde_json::Value;
|
||||
use std::process::Command;
|
||||
|
||||
/// 解析 cftunnel status 输出
|
||||
fn parse_cftunnel_status(output: &str) -> serde_json::Map<String, Value> {
|
||||
let mut map = serde_json::Map::new();
|
||||
for line in output.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("隧道:") || line.starts_with("隧道:") {
|
||||
let rest = line.splitn(2, ':').nth(1).unwrap_or("").trim();
|
||||
// "mac-home (uuid)" → 取名称
|
||||
let name = rest.split('(').next().unwrap_or(rest).trim();
|
||||
map.insert("tunnel_name".into(), Value::String(name.to_string()));
|
||||
} else if line.starts_with("状态:") || line.starts_with("状态:") {
|
||||
let rest = line.splitn(2, ':').nth(1).unwrap_or("").trim();
|
||||
let running = rest.contains("运行中");
|
||||
map.insert("running".into(), Value::Bool(running));
|
||||
// 提取 PID
|
||||
if let Some(pid_str) = rest.split("PID:").nth(1) {
|
||||
let pid = pid_str.trim().trim_end_matches(')').trim();
|
||||
if let Ok(p) = pid.parse::<u64>() {
|
||||
map.insert("pid".into(), Value::Number(p.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// 解析 cftunnel list 输出为路由数组
|
||||
fn parse_cftunnel_routes(output: &str) -> Vec<Value> {
|
||||
let mut routes = Vec::new();
|
||||
for line in output.lines() {
|
||||
let line = line.trim();
|
||||
// 跳过表头行
|
||||
if line.is_empty() || line.starts_with("名称") || line.starts_with("---") {
|
||||
continue;
|
||||
}
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert("name".into(), Value::String(parts[0].to_string()));
|
||||
obj.insert("domain".into(), Value::String(parts[1].to_string()));
|
||||
obj.insert("service".into(), Value::String(parts[2].to_string()));
|
||||
routes.push(Value::Object(obj));
|
||||
}
|
||||
}
|
||||
routes
|
||||
}
|
||||
|
||||
fn cftunnel_bin() -> String {
|
||||
// 优先查找用户 bin 目录
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
let user_bin = home.join("bin").join("cftunnel");
|
||||
if user_bin.exists() {
|
||||
return user_bin.to_string_lossy().to_string();
|
||||
}
|
||||
"cftunnel".to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_cftunnel_status() -> Result<Value, String> {
|
||||
let bin = cftunnel_bin();
|
||||
let mut result = serde_json::Map::new();
|
||||
|
||||
// 检查是否安装
|
||||
let version_out = Command::new(&bin).arg("version").output();
|
||||
match version_out {
|
||||
Ok(out) => {
|
||||
let ver = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
result.insert("installed".into(), Value::Bool(true));
|
||||
result.insert("version".into(), Value::String(ver));
|
||||
}
|
||||
Err(_) => {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
return Ok(Value::Object(result));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态
|
||||
if let Ok(out) = Command::new(&bin).arg("status").output() {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
let status = parse_cftunnel_status(&text);
|
||||
for (k, v) in status {
|
||||
result.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取路由列表
|
||||
if let Ok(out) = Command::new(&bin).arg("list").output() {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
let routes = parse_cftunnel_routes(&text);
|
||||
result.insert("routes".into(), Value::Array(routes));
|
||||
}
|
||||
|
||||
Ok(Value::Object(result))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cftunnel_action(action: String) -> Result<(), String> {
|
||||
let bin = cftunnel_bin();
|
||||
match action.as_str() {
|
||||
"up" | "down" => {}
|
||||
_ => return Err(format!("不支持的操作: {action}")),
|
||||
}
|
||||
let output = Command::new(&bin)
|
||||
.arg(&action)
|
||||
.output()
|
||||
.map_err(|e| format!("执行 cftunnel {action} 失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("cftunnel {action} 失败: {stderr}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_cftunnel_logs(lines: Option<u32>) -> Result<String, String> {
|
||||
let bin = cftunnel_bin();
|
||||
let n = lines.unwrap_or(20).to_string();
|
||||
let output = Command::new(&bin)
|
||||
.args(["logs", "--tail", &n])
|
||||
.output()
|
||||
.map_err(|e| format!("读取 cftunnel 日志失败: {e}"))?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_clawapp_status() -> Result<Value, String> {
|
||||
let mut result = serde_json::Map::new();
|
||||
|
||||
// 用 lsof 检测 :3210 端口
|
||||
let output = Command::new("lsof")
|
||||
.args(["-i", ":3210", "-P", "-t"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if text.is_empty() {
|
||||
result.insert("running".into(), Value::Bool(false));
|
||||
} else {
|
||||
result.insert("running".into(), Value::Bool(true));
|
||||
if let Ok(pid) = text.lines().next().unwrap_or("").parse::<u64>() {
|
||||
result.insert("pid".into(), Value::Number(pid.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
result.insert("running".into(), Value::Bool(false));
|
||||
}
|
||||
}
|
||||
|
||||
result.insert("port".into(), Value::Number(3210.into()));
|
||||
result.insert("url".into(), Value::String("http://localhost:3210".into()));
|
||||
Ok(Value::Object(result))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod config;
|
||||
pub mod extensions;
|
||||
pub mod logs;
|
||||
pub mod memory;
|
||||
pub mod service;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod commands;
|
||||
mod models;
|
||||
|
||||
use commands::{config, logs, memory, service};
|
||||
use commands::{config, extensions, logs, memory, service};
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -33,6 +33,11 @@ pub fn run() {
|
||||
memory::write_memory_file,
|
||||
memory::delete_memory_file,
|
||||
memory::export_memory_zip,
|
||||
// 扩展工具
|
||||
extensions::get_cftunnel_status,
|
||||
extensions::cftunnel_action,
|
||||
extensions::get_cftunnel_logs,
|
||||
extensions::get_clawapp_status,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("启动 ClawPanel 失败");
|
||||
|
||||
@@ -25,6 +25,18 @@ const NAV_ITEMS = [
|
||||
items: [
|
||||
{ route: '/memory', label: '记忆文件', icon: 'memory' },
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '扩展',
|
||||
items: [
|
||||
{ route: '/extensions', label: '扩展工具', icon: 'extensions' },
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '',
|
||||
items: [
|
||||
{ route: '/about', label: '关于', icon: 'about' },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -35,6 +47,8 @@ const ICONS = {
|
||||
models: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12"/></svg>',
|
||||
gateway: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>',
|
||||
memory: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
|
||||
extensions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>',
|
||||
about: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
|
||||
}
|
||||
|
||||
let _delegated = false
|
||||
|
||||
@@ -107,6 +107,18 @@ function mockInvoke(cmd, args) {
|
||||
create_backup: () => ({ name: 'openclaw-20260226-160000.json', size: 8542 }),
|
||||
restore_backup: () => true,
|
||||
delete_backup: () => true,
|
||||
get_cftunnel_status: () => ({
|
||||
installed: true, version: 'cftunnel 0.7.0', running: true,
|
||||
tunnel_name: 'mac-home', pid: 73325,
|
||||
routes: [
|
||||
{ name: 'clawapp', domain: 'chat.qrj.ai', service: 'http://localhost:3210' },
|
||||
{ name: 'newapi', domain: 'newapi.qrj.ai', service: 'http://localhost:30080' },
|
||||
{ name: 'webhook', domain: 'webhook.qrj.ai', service: 'http://localhost:9801' },
|
||||
],
|
||||
}),
|
||||
cftunnel_action: () => true,
|
||||
get_cftunnel_logs: () => '2026-02-26 13:29:01 [INFO] Tunnel started\n2026-02-26 13:30:00 [INFO] Connection healthy',
|
||||
get_clawapp_status: () => ({ running: true, pid: 7752, port: 3210, url: 'http://localhost:3210' }),
|
||||
}
|
||||
const fn = mocks[cmd]
|
||||
return fn ? Promise.resolve(fn(args)) : Promise.reject(`未知命令: ${cmd}`)
|
||||
@@ -148,4 +160,10 @@ export const api = {
|
||||
createBackup: () => invoke('create_backup'),
|
||||
restoreBackup: (name) => invoke('restore_backup', { name }),
|
||||
deleteBackup: (name) => invoke('delete_backup', { name }),
|
||||
|
||||
// 扩展工具
|
||||
getCftunnelStatus: () => invoke('get_cftunnel_status'),
|
||||
cftunnelAction: (action) => invoke('cftunnel_action', { action }),
|
||||
getCftunnelLogs: (lines = 20) => invoke('get_cftunnel_logs', { lines }),
|
||||
getClawappStatus: () => invoke('get_clawapp_status'),
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ registerRoute('/logs', () => import('./pages/logs.js'))
|
||||
registerRoute('/models', () => import('./pages/models.js'))
|
||||
registerRoute('/gateway', () => import('./pages/gateway.js'))
|
||||
registerRoute('/memory', () => import('./pages/memory.js'))
|
||||
registerRoute('/extensions', () => import('./pages/extensions.js'))
|
||||
registerRoute('/about', () => import('./pages/about.js'))
|
||||
|
||||
// 初始化主题
|
||||
initTheme()
|
||||
|
||||
122
src/pages/about.js
Normal file
122
src/pages/about.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 关于页面
|
||||
* 版本信息、项目链接、相关项目、系统环境
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">关于</h1>
|
||||
<p class="page-desc">ClawPanel — OpenClaw 可视化管理面板</p>
|
||||
</div>
|
||||
<div class="stat-cards" id="version-cards">
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">相关项目</div>
|
||||
<div id="projects-list"></div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">快捷链接</div>
|
||||
<div id="links-list"></div>
|
||||
</div>
|
||||
<div class="config-section" style="color:var(--text-tertiary);font-size:var(--font-size-xs)">
|
||||
<p>ClawPanel 基于 Tauri v2 构建,前端 Vanilla JS + Vite,后端 Rust。</p>
|
||||
<p style="margin-top:8px">MIT License © 2026 qingchencloud</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
loadData(page)
|
||||
renderProjects(page)
|
||||
renderLinks(page)
|
||||
return page
|
||||
}
|
||||
|
||||
async function loadData(page) {
|
||||
const cards = page.querySelector('#version-cards')
|
||||
try {
|
||||
const [version, install] = await Promise.all([
|
||||
api.getVersionInfo(),
|
||||
api.checkInstallation(),
|
||||
])
|
||||
cards.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">ClawPanel</span></div>
|
||||
<div class="stat-card-value">0.1.0</div>
|
||||
<div class="stat-card-meta">Tauri v2 桌面应用</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">OpenClaw</span></div>
|
||||
<div class="stat-card-value">${version.current || '未知'}</div>
|
||||
<div class="stat-card-meta">${version.update_available ? '有新版本可用' : '已是最新'}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">安装路径</span></div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-sm);word-break:break-all">${install.path || '未知'}</div>
|
||||
<div class="stat-card-meta">${install.installed ? '已安装' : '未安装'}</div>
|
||||
</div>
|
||||
`
|
||||
} catch {
|
||||
cards.innerHTML = '<div class="stat-card"><div class="stat-card-label">加载失败</div></div>'
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECTS = [
|
||||
{
|
||||
name: 'OpenClaw',
|
||||
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理',
|
||||
url: 'https://github.com/openclaw-labs/openclaw',
|
||||
},
|
||||
{
|
||||
name: 'ClawApp',
|
||||
desc: '跨平台移动聊天客户端,H5 + 代理服务器架构,支持离线和流式传输',
|
||||
url: 'https://github.com/qingchencloud/clawapp',
|
||||
},
|
||||
{
|
||||
name: 'cftunnel',
|
||||
desc: '全协议内网穿透工具,Cloud 模式免费 HTTP/WS + Relay 模式自建中继',
|
||||
url: 'https://github.com/qingchencloud/cftunnel',
|
||||
},
|
||||
{
|
||||
name: 'ClawPanel',
|
||||
desc: 'OpenClaw 可视化管理面板,Tauri v2 桌面应用',
|
||||
url: 'https://github.com/qingchencloud/clawpanel',
|
||||
},
|
||||
]
|
||||
|
||||
function renderProjects(page) {
|
||||
const el = page.querySelector('#projects-list')
|
||||
el.innerHTML = PROJECTS.map(p => `
|
||||
<div class="service-card">
|
||||
<div class="service-info">
|
||||
<div>
|
||||
<div class="service-name">${p.name}</div>
|
||||
<div class="service-desc">${p.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<a class="btn btn-secondary btn-sm" href="${p.url}" target="_blank" rel="noopener">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
|
||||
const LINKS = [
|
||||
{ label: 'cftunnel 官网', url: 'https://cftunnel.qt.cool' },
|
||||
{ label: 'cftunnel 桌面客户端', url: 'https://github.com/qingchencloud/cftunnel-app/releases' },
|
||||
{ label: 'OpenClaw 中文翻译', url: 'https://github.com/1186258278/OpenClawChineseTranslation' },
|
||||
{ label: 'ClawApp 文档', url: 'https://github.com/qingchencloud/clawapp#readme' },
|
||||
]
|
||||
|
||||
function renderLinks(page) {
|
||||
const el = page.querySelector('#links-list')
|
||||
el.innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:var(--space-sm)">
|
||||
${LINKS.map(l => `<a class="btn btn-secondary btn-sm" href="${l.url}" target="_blank" rel="noopener">${l.label}</a>`).join('')}
|
||||
</div>`
|
||||
}
|
||||
215
src/pages/extensions.js
Normal file
215
src/pages/extensions.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 扩展工具页面
|
||||
* cftunnel 隧道管理 + ClawApp 状态
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
|
||||
let _delegated = false
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">扩展工具</h1>
|
||||
<p class="page-desc">管理 cftunnel 内网穿透和 ClawApp 移动客户端</p>
|
||||
</div>
|
||||
<div id="cftunnel-card" class="config-section">
|
||||
<div class="config-section-title">cftunnel 内网穿透</div>
|
||||
<div id="cftunnel-content">加载中...</div>
|
||||
</div>
|
||||
<div id="clawapp-card" class="config-section">
|
||||
<div class="config-section-title">ClawApp 移动客户端</div>
|
||||
<div id="clawapp-content">加载中...</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
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 = `<div style="color:var(--error)">加载失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
function renderCftunnel(el, s) {
|
||||
if (!s.installed) {
|
||||
el.innerHTML = `
|
||||
<div style="color:var(--text-tertiary)">cftunnel 未安装</div>
|
||||
<a class="btn btn-primary btn-sm" href="https://github.com/qingchencloud/cftunnel" target="_blank" rel="noopener" style="margin-top:var(--space-md)">前往安装</a>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
const running = s.running
|
||||
const routes = s.routes || []
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="stat-cards" style="margin-bottom:var(--space-md)">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">状态</span>
|
||||
<span class="status-dot ${running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${running ? '运行中' : '已停止'}</div>
|
||||
<div class="stat-card-meta">${s.tunnel_name || ''}${s.pid ? ' (PID: ' + s.pid + ')' : ''}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">版本</span></div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-md)">${s.version || '未知'}</div>
|
||||
<div class="stat-card-meta">${routes.length} 条路由</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-md)">
|
||||
${running
|
||||
? '<button class="btn btn-danger btn-sm" data-action="cftunnel-down">停止隧道</button>'
|
||||
: '<button class="btn btn-primary btn-sm" data-action="cftunnel-up">启动隧道</button>'
|
||||
}
|
||||
<button class="btn btn-secondary btn-sm" data-action="cftunnel-logs">查看日志</button>
|
||||
<button class="btn btn-secondary btn-sm" data-action="cftunnel-refresh">刷新</button>
|
||||
</div>
|
||||
${renderRoutes(routes)}
|
||||
<div id="cftunnel-logs-area"></div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderRoutes(routes) {
|
||||
if (!routes.length) return '<div style="color:var(--text-tertiary)">暂无路由</div>'
|
||||
return `
|
||||
<table class="data-table" style="margin-bottom:0">
|
||||
<thead><tr><th>名称</th><th>域名</th><th>本地服务</th></tr></thead>
|
||||
<tbody>
|
||||
${routes.map(r => `
|
||||
<tr>
|
||||
<td>${r.name}</td>
|
||||
<td><a href="https://${r.domain}" target="_blank" rel="noopener">${r.domain}</a></td>
|
||||
<td><code>${r.service}</code></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
// ===== ClawApp =====
|
||||
|
||||
async function loadClawapp(page) {
|
||||
const el = page.querySelector('#clawapp-content')
|
||||
try {
|
||||
const status = await api.getClawappStatus()
|
||||
renderClawapp(el, status)
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div style="color:var(--error)">加载失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
function renderClawapp(el, s) {
|
||||
const running = s.running
|
||||
el.innerHTML = `
|
||||
<div class="stat-cards" style="margin-bottom:var(--space-md)">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">状态</span>
|
||||
<span class="status-dot ${running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${running ? '运行中' : '已停止'}</div>
|
||||
<div class="stat-card-meta">${s.pid ? 'PID: ' + s.pid : ''}${s.port ? ' 端口: ' + s.port : ''}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">访问地址</span></div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-sm)">${s.url || 'http://localhost:3210'}</div>
|
||||
<div class="stat-card-meta">外网: chat.qrj.ai</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-sm)">
|
||||
<a class="btn btn-primary btn-sm" href="${s.url || 'http://localhost:3210'}" target="_blank" rel="noopener">打开 ClawApp</a>
|
||||
<a class="btn btn-secondary btn-sm" href="https://chat.qrj.ai" target="_blank" rel="noopener">打开外网地址</a>
|
||||
<button class="btn btn-secondary btn-sm" data-action="clawapp-refresh">刷新</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// ===== 事件绑定 =====
|
||||
|
||||
function bindEvents(page) {
|
||||
if (_delegated) return
|
||||
_delegated = true
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCftunnelAction(page, action) {
|
||||
const label = action === 'up' ? '启动' : '停止'
|
||||
try {
|
||||
toast(`正在${label}隧道...`, 'info')
|
||||
await api.cftunnelAction(action)
|
||||
toast(`隧道已${label}`, 'success')
|
||||
await loadCftunnel(page)
|
||||
} catch (e) {
|
||||
toast(`${label}失败: ${e}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div style="margin-top:var(--space-md)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-sm)">
|
||||
<span style="font-weight:600;font-size:var(--font-size-sm)">最近日志</span>
|
||||
<button class="btn btn-secondary btn-sm" data-action="cftunnel-logs">收起</button>
|
||||
</div>
|
||||
<pre class="log-viewer">${logs || '暂无日志'}</pre>
|
||||
</div>
|
||||
`
|
||||
} catch (e) {
|
||||
area.innerHTML = `<div style="color:var(--error);margin-top:var(--space-sm)">读取日志失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user