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

@@ -1,32 +1,113 @@
import { Plus, RefreshCw } from 'lucide-react';
import { useEffect } from 'react';
import { Radio, RefreshCw, MessageCircle, Settings2 } from 'lucide-react';
import { useGatewayStore } from '../../store/gatewayStore';
const CHANNEL_ICONS: Record<string, string> = {
feishu: '飞',
qqbot: 'QQ',
wechat: '微',
};
export function IMChannels() {
const { channels, connectionState, loadChannels, loadPluginStatus } = useGatewayStore();
const connected = connectionState === 'connected';
const loading = connectionState === 'connecting' || connectionState === 'reconnecting' || connectionState === 'handshaking';
useEffect(() => {
if (connected) {
loadPluginStatus().then(() => loadChannels());
}
}, [connected]);
const handleRefresh = () => {
loadPluginStatus().then(() => loadChannels());
};
const knownChannels = [
{ id: 'feishu', type: 'feishu', label: '飞书 (Feishu)' },
{ id: 'qqbot', type: 'qqbot', label: 'QQ 机器人' },
{ id: 'wechat', type: 'wechat', label: '微信' },
];
const availableChannels = knownChannels.filter(
(channel) => !channels.some((item) => item.type === channel.type)
);
return (
<div>
<div className="flex justify-between items-center mb-2">
<h1 className="text-2xl font-bold text-gray-900">IM </h1>
<div className="max-w-3xl">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-bold text-gray-900">IM </h1>
<div className="flex gap-2">
<button className="text-sm border border-gray-300 rounded-lg px-3 py-1.5 hover:bg-gray-50 flex items-center gap-1">
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button className="text-sm bg-orange-500 text-white rounded-lg px-3 py-1.5 hover:bg-orange-600 flex items-center gap-1">
<Plus className="w-3.5 h-3.5" />
<span className="text-xs text-gray-400 flex items-center">
{connected ? `${channels.length} 个已识别频道` : loading ? '连接中...' : '未连接 Gateway'}
</span>
<button
onClick={handleRefresh}
disabled={!connected}
className="text-xs text-white bg-orange-500 hover:bg-orange-600 px-3 py-1.5 rounded-lg flex items-center gap-1 transition-colors disabled:opacity-50"
>
<RefreshCw className="w-3 h-3" />
</button>
</div>
</div>
<div className="bg-gray-50 rounded-xl p-8 text-center mt-6">
<button className="bg-orange-500 text-white text-sm rounded-lg px-4 py-2 hover:bg-orange-600 mb-3">
</button>
<p className="text-sm text-gray-500"> IM </p>
<p className="text-xs text-gray-400 mt-1"> IM </p>
</div>
{!connected ? (
<div className="bg-white rounded-xl border border-gray-200 h-64 flex flex-col items-center justify-center mb-6 shadow-sm text-gray-400">
<Radio className="w-8 h-8 mb-3 opacity-40" />
<span className="text-sm"> Gateway IM </span>
</div>
) : (
<div className="bg-white rounded-xl border border-gray-200 mb-6 shadow-sm divide-y divide-gray-100">
{channels.length > 0 ? channels.map((channel) => (
<div key={channel.id} className="p-4 flex items-center gap-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-white text-sm font-semibold ${
channel.status === 'active'
? 'bg-gradient-to-br from-blue-500 to-indigo-500'
: channel.status === 'error'
? 'bg-gradient-to-br from-red-500 to-rose-500'
: 'bg-gray-300'
}`}>
{CHANNEL_ICONS[channel.type] || <MessageCircle className="w-4 h-4" />}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900">{channel.label}</div>
<div className={`text-xs mt-1 ${
channel.status === 'active'
? 'text-green-600'
: channel.status === 'error'
? 'text-red-500'
: 'text-gray-400'
}`}>
{channel.status === 'active' ? '已连接' : channel.status === 'error' ? channel.error || '错误' : '未配置'}
{channel.accounts !== undefined && channel.accounts > 0 ? ` · ${channel.accounts} 个账号` : ''}
</div>
</div>
<div className="text-xs text-gray-400">{channel.type}</div>
</div>
)) : (
<div className="h-40 flex items-center justify-center text-sm text-gray-400">
</div>
)}
</div>
)}
<div className="mt-8">
<h2 className="text-sm font-medium text-gray-700 mb-3"></h2>
<div className="flex gap-2">
<button className="text-sm bg-orange-500 text-white rounded-lg px-4 py-1.5 hover:bg-orange-600">+ </button>
<div>
<div className="text-xs text-gray-500 mb-3"></div>
<div className="flex flex-wrap gap-3">
{availableChannels.map((channel) => (
<span
key={channel.id}
className="text-xs text-gray-500 bg-gray-100 px-4 py-2 rounded-lg"
>
{channel.label}
</span>
))}
<div className="text-xs text-gray-400 flex items-center gap-1">
<Settings2 className="w-3 h-3" />
channelaccountbinding Gateway
</div>
</div>
</div>
</div>