feat(hands): restructure Hands UI with Chinese localization
Major changes: - Add HandList.tsx component for left sidebar - Add HandTaskPanel.tsx for middle content area - Restructure Sidebar tabs: 分身/HANDS/Workflow - Remove Hands tab from RightPanel - Localize all UI text to Chinese - Archive legacy OpenClaw documentation - Add Hands integration lessons document - Update feature checklist with new components UI improvements: - Left sidebar now shows Hands list with status icons - Middle area shows selected Hand's tasks and results - Consistent styling with Tailwind CSS - Chinese status labels and buttons Documentation: - Create docs/archive/openclaw-legacy/ for old docs - Add docs/knowledge-base/hands-integration-lessons.md - Update docs/knowledge-base/feature-checklist.md - Update docs/knowledge-base/README.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
224
desktop/src/components/SecurityStatus.tsx
Normal file
224
desktop/src/components/SecurityStatus.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Shield, ShieldCheck, ShieldAlert, ShieldX, RefreshCw } from 'lucide-react';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
|
||||
// OpenFang 16-layer security architecture names (Chinese)
|
||||
const SECURITY_LAYER_NAMES: Record<string, string> = {
|
||||
// Layer 1: Network
|
||||
'network.firewall': '网络防火墙',
|
||||
'network.tls': 'TLS 加密',
|
||||
'network.rate_limit': '速率限制',
|
||||
// Layer 2: Authentication
|
||||
'auth.device': '设备认证',
|
||||
'auth.jwt': 'JWT 令牌',
|
||||
'auth.session': '会话管理',
|
||||
// Layer 3: Authorization
|
||||
'auth.rbac': '角色权限',
|
||||
'auth.capabilities': '能力控制',
|
||||
// Layer 4: Input Validation
|
||||
'input.sanitization': '输入净化',
|
||||
'input.schema': '模式验证',
|
||||
// Layer 5: Execution
|
||||
'exec.sandbox': '沙箱隔离',
|
||||
'exec.timeout': '执行超时',
|
||||
'exec.resource_limit': '资源限制',
|
||||
// Layer 6: Audit & Logging
|
||||
'audit.logging': '审计日志',
|
||||
'audit.tracing': '请求追踪',
|
||||
};
|
||||
|
||||
// Default 16 layers for display when API returns minimal data
|
||||
const DEFAULT_LAYERS = [
|
||||
{ name: 'network.firewall', enabled: false },
|
||||
{ name: 'network.tls', enabled: false },
|
||||
{ name: 'network.rate_limit', enabled: false },
|
||||
{ name: 'auth.device', enabled: false },
|
||||
{ name: 'auth.jwt', enabled: false },
|
||||
{ name: 'auth.session', enabled: false },
|
||||
{ name: 'auth.rbac', enabled: false },
|
||||
{ name: 'auth.capabilities', enabled: false },
|
||||
{ name: 'input.sanitization', enabled: false },
|
||||
{ name: 'input.schema', enabled: false },
|
||||
{ name: 'exec.sandbox', enabled: false },
|
||||
{ name: 'exec.timeout', enabled: false },
|
||||
{ name: 'exec.resource_limit', enabled: false },
|
||||
{ name: 'audit.logging', enabled: false },
|
||||
{ name: 'audit.tracing', enabled: false },
|
||||
{ name: 'audit.alerting', enabled: false },
|
||||
];
|
||||
|
||||
function getSecurityIcon(level: 'critical' | 'high' | 'medium' | 'low') {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
return <ShieldCheck className="w-5 h-5 text-green-600" />;
|
||||
case 'high':
|
||||
return <Shield className="w-5 h-5 text-blue-600" />;
|
||||
case 'medium':
|
||||
return <ShieldAlert className="w-5 h-5 text-yellow-600" />;
|
||||
case 'low':
|
||||
return <ShieldX className="w-5 h-5 text-red-600" />;
|
||||
}
|
||||
}
|
||||
|
||||
function getSecurityLabel(level: 'critical' | 'high' | 'medium' | 'low') {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
return { text: '极高', color: 'text-green-600 bg-green-50 border-green-200' };
|
||||
case 'high':
|
||||
return { text: '高', color: 'text-blue-600 bg-blue-50 border-blue-200' };
|
||||
case 'medium':
|
||||
return { text: '中', color: 'text-yellow-600 bg-yellow-50 border-yellow-200' };
|
||||
case 'low':
|
||||
return { text: '低', color: 'text-red-600 bg-red-50 border-red-200' };
|
||||
}
|
||||
}
|
||||
|
||||
export function SecurityStatus() {
|
||||
const { connectionState, securityStatus, loadSecurityStatus } = useGatewayStore();
|
||||
const connected = connectionState === 'connected';
|
||||
|
||||
useEffect(() => {
|
||||
if (connected) {
|
||||
loadSecurityStatus();
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
if (!connected) {
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Shield className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm font-semibold text-gray-900">安全状态</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">连接后可用</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Use default layers if no data, or merge with API data
|
||||
const displayLayers = securityStatus?.layers?.length
|
||||
? DEFAULT_LAYERS.map((defaultLayer) => {
|
||||
const apiLayer = securityStatus.layers.find((l) => l.name === defaultLayer.name);
|
||||
return apiLayer || defaultLayer;
|
||||
})
|
||||
: DEFAULT_LAYERS;
|
||||
|
||||
const enabledCount = displayLayers.filter((l) => l.enabled).length;
|
||||
const totalCount = displayLayers.length;
|
||||
const securityLevel = securityStatus?.securityLevel ||
|
||||
(enabledCount >= 14 ? 'critical' : enabledCount >= 10 ? 'high' : enabledCount >= 6 ? 'medium' : 'low');
|
||||
|
||||
const levelLabel = getSecurityLabel(securityLevel);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{getSecurityIcon(securityLevel)}
|
||||
<span className="text-sm font-semibold text-gray-900">安全状态</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full border ${levelLabel.color}`}>
|
||||
{levelLabel.text}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => loadSecurityStatus()}
|
||||
className="p-1 text-gray-400 hover:text-orange-500 rounded transition-colors"
|
||||
title="刷新安全状态"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="mb-3 text-xs text-gray-500">
|
||||
已启用 {enabledCount} / {totalCount} 层防护
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="mb-4 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${
|
||||
securityLevel === 'critical'
|
||||
? 'bg-green-500'
|
||||
: securityLevel === 'high'
|
||||
? 'bg-blue-500'
|
||||
: securityLevel === 'medium'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${(enabledCount / totalCount) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Layers Grid */}
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{displayLayers.map((layer) => {
|
||||
const label = SECURITY_LAYER_NAMES[layer.name] || layer.name;
|
||||
return (
|
||||
<div
|
||||
key={layer.name}
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs ${
|
||||
layer.enabled
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'bg-gray-50 text-gray-400'
|
||||
}`}
|
||||
title={layer.name}
|
||||
>
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
layer.enabled ? 'bg-green-500' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Layer Categories */}
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<CategorySummary
|
||||
label="网络"
|
||||
layers={displayLayers.filter((l) => l.name.startsWith('network.'))}
|
||||
/>
|
||||
<CategorySummary
|
||||
label="认证"
|
||||
layers={displayLayers.filter((l) => l.name.startsWith('auth.'))}
|
||||
/>
|
||||
<CategorySummary
|
||||
label="执行"
|
||||
layers={displayLayers.filter((l) => l.name.startsWith('exec.'))}
|
||||
/>
|
||||
<CategorySummary
|
||||
label="输入"
|
||||
layers={displayLayers.filter((l) => l.name.startsWith('input.'))}
|
||||
/>
|
||||
<CategorySummary
|
||||
label="审计"
|
||||
layers={displayLayers.filter((l) => l.name.startsWith('audit.'))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategorySummary({ label, layers }: { label: string; layers: { enabled: boolean }[] }) {
|
||||
if (layers.length === 0) return null;
|
||||
const enabled = layers.filter((l) => l.enabled).length;
|
||||
const total = layers.length;
|
||||
const allEnabled = enabled === total;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`font-medium ${allEnabled ? 'text-green-600' : 'text-gray-500'}`}>
|
||||
{enabled}/{total}
|
||||
</span>
|
||||
<span className="text-gray-400">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user