Files
sanguo_moziplus_v2/src/frontend/src/components/SettingsPanel.tsx
T
cfdaily 05f9112fab
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 28s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
[moz] refactor(frontend): 工具链 Tab 移入系统设置子页签
2026-06-14 16:36:34 +08:00

299 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 城防设置 — 接线状态、安全防务、版本更新、数据源配置
* 三国术语:设置→城防、连接→接线、配置→防务
*/
import { useState, useCallback } from 'react';
import { api, AgentsStatusData } from '../api';
import ToolchainPanel from './ToolchainPanel';
interface ServiceCheckResult {
name: string;
url: string;
online: boolean;
detail: string;
checkedAt: string;
}
export default function SettingsPanel() {
const [tab, setTab] = useState<'connections' | 'security' | 'version' | 'logs' | 'toolchain'>('connections');
// 接线状态巡检
const [checking, setChecking] = useState(false);
const [checkResults, setCheckResults] = useState<ServiceCheckResult[]>([]);
const [lastInspection, setLastInspection] = useState<string>('');
const runInspection = useCallback(async () => {
setChecking(true);
const results: ServiceCheckResult[] = [];
const now = new Date().toLocaleString('zh-CN', { hour12: false });
// 1. Gateway 状态
let gatewayOnline = false;
let gatewayUrl = 'http://192.168.2.153:18789';
let gatewayVersion = '-';
try {
const status: AgentsStatusData = await api.agentsStatus();
gatewayOnline = status.gateway.alive;
gatewayVersion = status.gateway.status || '-';
gatewayUrl = status.ok ? gatewayUrl : gatewayUrl;
} catch {
gatewayOnline = false;
}
results.push({
name: 'OpenClaw Gateway',
url: gatewayUrl,
online: gatewayOnline,
detail: gatewayOnline ? `在线 · ${gatewayVersion}` : '离线',
checkedAt: now,
});
// 2. moziplus 后端
let backendOnline = false;
try {
await api.agentsStatus();
backendOnline = true;
} catch {
backendOnline = false;
}
results.push({
name: 'moziplus 后端',
url: 'http://192.168.2.154:8088',
online: backendOnline,
detail: backendOnline ? '在线' : '离线',
checkedAt: now,
});
setCheckResults(results);
setLastInspection(now);
setChecking(false);
}, []);
// 静态接线列表
const connections = [
{ name: 'moziplus 后端', url: 'http://localhost:8088', status: 'connected', note: 'moziplus daemon' },
{ name: 'OpenClaw Gateway', url: 'http://192.168.2.153:18789', status: 'connected', note: '主 Gateway' },
{ name: 'NAS 回测服务', url: 'http://192.168.2.154:8088', status: 'connected', note: 'Docker 回测' },
{ name: 'Gitee 远程仓库', url: 'git@gitee.com:cfdaily/sanguo_moziplus.git', status: 'connected', note: 'Git 同步' },
{ name: 'RedisM3', url: '-', status: 'pending', note: 'M3 阶段接入实时推送' },
{ name: '飞书推送(M3', url: '-', status: 'pending', note: 'M3 阶段接入通知推送' },
];
// 安全防务
const risks = [
{ level: 'info' as const, title: 'API 无鉴权', desc: 'Dashboard API 默认无 token 鉴权,本地运行无风险', action: 'M3 考虑加入 token 验证' },
{ level: 'ok' as const, title: 'SSH 密钥正常', desc: 'Gitee SSH 密钥配置正确,Git 同步正常', action: '' },
{ level: 'ok' as const, title: '数据库备份', desc: 'SQLite 数据库自动备份已启用', action: '' },
{ level: 'warn' as const, title: 'Python 3.9 版本较旧', desc: 'Python 3.9.6 不支持 X | None 语法,需 from __future__ import annotations', action: '考虑升级到 Python 3.10+' },
];
return (
<div>
{/* 城防子页签 */}
<div style={{ display: 'flex', gap: 6, marginBottom: 20 }}>
{[
{ key: 'connections' as const, label: '🔌 接线状态' },
{ key: 'security' as const, label: '🛡️ 安全防务' },
{ key: 'version' as const, label: '📦 版本更新' },
{ key: 'logs' as const, label: '📋 城防日志' },
{ key: 'toolchain' as const, label: '⛓️ 工具链' },
].map((t) => (
<button key={t.key} className={`btn ${tab === t.key ? 'btn-primary' : ''}`} onClick={() => setTab(t.key)}>
{t.label}
</button>
))}
</div>
{/* ========== 接线状态 ========== */}
{tab === 'connections' && (
<div>
{/* 巡视城防 */}
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 14, padding: 18, marginBottom: 14 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontWeight: 700, fontSize: 14 }}>🏰 线</span>
<button
className="btn btn-action"
onClick={runInspection}
disabled={checking}
style={{ opacity: checking ? 0.6 : 1, cursor: checking ? 'not-allowed' : 'pointer' }}
>
{checking ? '⏳ 巡视中…' : '🔍 巡视城防'}
</button>
</div>
{lastInspection && (
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 10 }}>
{lastInspection}
</div>
)}
{checkResults.length > 0 && (
<div>
{checkResults.map((r, i) => (
<div
key={i}
style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '12px 0',
borderBottom: i < checkResults.length - 1 ? '1px solid var(--line)' : 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{
width: 10, height: 10, borderRadius: '50%',
background: r.online ? '#2ecc8a' : '#ff5270',
boxShadow: `0 0 6px ${r.online ? '#2ecc8a66' : '#ff527066'}`,
display: 'inline-block', flexShrink: 0,
}} />
<div>
<div style={{ fontWeight: 600, fontSize: 13 }}>{r.name}</div>
<div style={{ fontSize: 10, color: 'var(--muted)', fontFamily: 'monospace' }}>{r.url}</div>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<span style={{
fontSize: 10, padding: '2px 8px', borderRadius: 4,
background: r.online ? '#0a2018' : '#200a0a',
color: r.online ? '#2ecc8a' : '#ff5270',
border: `1px solid ${r.online ? '#2ecc8a44' : '#ff527044'}`,
}}>
{r.detail}
</span>
</div>
</div>
))}
{/* 巡视摘要 */}
<div style={{
marginTop: 12, padding: '10px 14px', borderRadius: 8,
background: checkResults.every(r => r.online) ? '#0a2018' : '#200a0a',
border: `1px solid ${checkResults.every(r => r.online) ? '#2ecc8a44' : '#ff527044'}`,
fontSize: 12,
color: checkResults.every(r => r.online) ? '#2ecc8a' : '#ff5270',
}}>
{checkResults.every(r => r.online)
? `✅ 全部城门正常 (${checkResults.length}/${checkResults.length})`
: `⚠️ ${checkResults.filter(r => !r.online).length} 处城门告急 (${checkResults.filter(r => r.online).length}/${checkResults.length} 正常)`
}
</div>
</div>
)}
{checkResults.length === 0 && !checking && (
<div style={{ fontSize: 12, color: 'var(--muted)', textAlign: 'center', padding: '16px 0' }}>
🔍 线
</div>
)}
</div>
{/* 接线列表 */}
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 14 }}>
<div style={{ padding: '12px 18px 0', fontSize: 12, fontWeight: 700, color: 'var(--muted)' }}>线</div>
{connections.map((c, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 18px', borderBottom: i < connections.length - 1 ? '1px solid #0e1320' : 'none' }}>
<div>
<div style={{ fontWeight: 600, fontSize: 13 }}>{c.name}</div>
<div style={{ fontSize: 10, color: 'var(--muted)', fontFamily: 'monospace' }}>{c.url}</div>
</div>
<div style={{ textAlign: 'right' }}>
<span style={{ fontSize: 10, padding: '2px 8px', borderRadius: 4, background: c.status === 'connected' ? '#0a2018' : '#201a08', color: c.status === 'connected' ? 'var(--ok)' : 'var(--warn)', border: `1px solid ${c.status === 'connected' ? '#2ecc8a44' : '#f5c84244'}` }}>
{c.status === 'connected' ? '✅ 已接线' : '⏳ 待接入'}
</span>
<div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 2 }}>{c.note}</div>
</div>
</div>
))}
</div>
</div>
)}
{/* ========== 安全防务 ========== */}
{tab === 'security' && (
<div>
{risks.map((r, i) => (
<div key={i} style={{ background: 'var(--panel)', border: `1px solid ${r.level === 'warn' ? '#f5c84244' : r.level === 'ok' ? '#2ecc8a44' : '#6a9eff44'}`, borderRadius: 12, padding: 14, marginBottom: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontWeight: 600, fontSize: 13 }}>
{r.level === 'ok' && '✅ '}{r.level === 'warn' && '⚠️ '}{r.level === 'info' && '️ '}
{r.title}
</span>
<span style={{ fontSize: 9, padding: '1px 6px', borderRadius: 3, background: r.level === 'ok' ? '#0a2018' : r.level === 'warn' ? '#201a08' : '#0a1428', color: r.level === 'ok' ? 'var(--ok)' : r.level === 'warn' ? 'var(--warn)' : 'var(--acc)' }}>
{r.level === 'ok' ? '正常' : r.level === 'warn' ? '警告' : '提示'}
</span>
</div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: r.action ? 6 : 0 }}>{r.desc}</div>
{r.action && <div style={{ fontSize: 11, color: 'var(--acc)' }}> {r.action}</div>}
</div>
))}
</div>
)}
{/* ========== 版本更新 ========== */}
{tab === 'version' && (
<div>
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 14, padding: 18 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 4 }}>moziplus</div>
<div style={{ fontSize: 18, fontWeight: 800 }}>v0.4b</div>
<div style={{ fontSize: 10, color: 'var(--muted)' }}>M2 </div>
</div>
<div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 4 }}>Dashboard</div>
<div style={{ fontSize: 18, fontWeight: 800 }}>v1.0</div>
<div style={{ fontSize: 10, color: 'var(--muted)' }}> Edict </div>
</div>
<div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 4 }}>OpenClaw</div>
<div style={{ fontSize: 18, fontWeight: 800 }}>latest</div>
<div style={{ fontSize: 10, color: 'var(--ok)' }}> </div>
</div>
<div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 4 }}>Node.js</div>
<div style={{ fontSize: 18, fontWeight: 800 }}>v22</div>
<div style={{ fontSize: 10, color: 'var(--ok)' }}> </div>
</div>
</div>
</div>
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 14, padding: 18, marginTop: 12 }}>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>📋 </div>
<div style={{ fontSize: 12, color: 'var(--muted)', lineHeight: 1.8 }}>
<div>📦 git clone + PM2</div>
<div>🔄 git pull + npm install + pm2 restart</div>
<div>📝 git@gitee.com:cfdaily/sanguo_moziplus.git</div>
<div>🔀 main develop</div>
</div>
</div>
</div>
)}
{/* ========== 城防日志 ========== */}
{tab === 'logs' && (
<div>
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 14, padding: 18 }}>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>📋 </div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12 }}>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{JSON.parse(localStorage.getItem('opLogs') || '[]').slice(-20).reverse().map((log: { time: string; action: string; detail: string }, i: number) => (
<div key={i} style={{ display: 'flex', gap: 10, alignItems: 'baseline', padding: '6px 0', borderBottom: '1px solid var(--line)' }}>
<span style={{ fontSize: 10, color: 'var(--muted)', width: 140, flexShrink: 0 }}>{log.time}</span>
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--acc)', width: 80 }}>{log.action}</span>
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{log.detail}</span>
</div>
))}
{JSON.parse(localStorage.getItem('opLogs') || '[]').length === 0 && (
<div style={{ fontSize: 12, color: 'var(--muted)', padding: 8 }}></div>
)}
</div>
</div>
</div>
)}
{/* ========== 工具链 ========== */}
{tab === 'toolchain' && <ToolchainPanel />}
</div>
);
}