299 lines
14 KiB
TypeScript
299 lines
14 KiB
TypeScript
/**
|
||
* 城防设置 — 接线状态、安全防务、版本更新、数据源配置
|
||
* 三国术语:设置→城防、连接→接线、配置→防务
|
||
*/
|
||
|
||
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: 'Redis(M3)', 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>
|
||
);
|
||
}
|