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:
iven
2026-03-14 23:16:32 +08:00
parent 67e1da635d
commit 07079293f4
126 changed files with 36229 additions and 1035 deletions

View 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>
);
}