# 自动化系统重设计实现计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 修复并重构 ZCLAW Hands 系统,使其成为完整可用的自动化平台 **Architecture:** 扩展现有 handStore/chatStore,不创建新 store;通过类型适配器统一 Hand/Workflow;WebSocket 事件驱动实时状态;结果流注入聊天消息 **Tech Stack:** React, TypeScript, Zustand, Tailwind CSS, WebSocket --- ## 文件结构 ``` desktop/src/ ├── components/ │ ├── Automation/ # 新建目录 │ │ ├── AutomationPanel.tsx # 统一入口(组合层) │ │ ├── AutomationCard.tsx # Hand/Workflow 统一卡片 │ │ ├── AutomationFilters.tsx # 分类筛选器 │ │ ├── ScheduleEditor.tsx # 可视化调度器 │ │ ├── ExecutionResult.tsx # 执行结果展示 │ │ ├── BatchActionBar.tsx # 批量操作栏 │ │ └── ApprovalQueue.tsx # 审批队列 │ └── HandsPanel.tsx # 修改:迁移到 handStore ├── store/ │ └── handStore.ts # 扩展:添加批量操作 ├── types/ │ └── automation.ts # 新建:类型适配器 └── hooks/ └── useAutomationEvents.ts # 新建:WebSocket 事件 Hook ``` --- ## Phase 1: 基础修复(1-2 天) ### Task 1.1: 修复参数传递问题 **Files:** - Modify: `desktop/src/components/HandsPanel.tsx:477-514` - Test: `tests/desktop/hands/param-passing.test.ts` **问题:** `handleActivate` 调用 `triggerHand(hand.id)` 时未传递参数 - [ ] **Step 1: 添加参数状态到 HandsPanel** ```typescript // 在 HandsPanel 组件内添加 const [paramValues, setParamValues] = useState>({}); ``` - [ ] **Step 2: 修改 HandDetailsModal 的 onActivate 回调** ```typescript // 修改 HandDetailsModalProps 接口 interface HandDetailsModalProps { hand: Hand; isOpen: boolean; onClose: () => void; onActivate: (params?: Record) => void; // 添加 params isActivating: boolean; paramValues: Record; // 新增 onParamChange: (values: Record) => void; // 新增 } ``` - [ ] **Step 3: 修改 handleModalActivate 传递参数** ```typescript // 修改 handleModalActivate const handleModalActivate = useCallback(async (params: Record) => { if (!selectedHand) return; setShowModal(false); await handleActivate(selectedHand, params); // 传递参数 }, [selectedHand, handleActivate]); ``` - [ ] **Step 4: 修改 handleActivate 接收并传递参数** ```typescript // 修改 handleActivate const handleActivate = useCallback(async (hand: Hand, params?: Record) => { setActivatingHandId(hand.id); try { const result = await triggerHand(hand.id, params); // 传递参数到 store // ... 其余逻辑 } }, [triggerHand, loadHands, toast]); ``` - [ ] **Step 5: 运行测试验证** Run: `cd desktop && pnpm vitest run tests/desktop/hands/param-passing.test.ts` Expected: PASS - [ ] **Step 6: Commit** ```bash git add desktop/src/components/HandsPanel.tsx tests/desktop/hands/param-passing.test.ts git commit -m "fix(hands): pass form parameters to triggerHand API" ``` --- ### Task 1.2: 迁移 HandsPanel 到 handStore **Files:** - Modify: `desktop/src/components/HandsPanel.tsx:11,458` - Modify: `desktop/src/store/index.ts` **问题:** HandsPanel 使用 gatewayStore 而非独立的 handStore - [ ] **Step 1: 修改 import 从 gatewayStore 到 handStore** ```typescript // 修改第 11 行 // 从: import { useGatewayStore, type Hand, type HandRequirement } from '../store/gatewayStore'; // 改为: import { useHandStore, type Hand, type HandRequirement } from '../store/handStore'; ``` - [ ] **Step 2: 修改组件内的 store 调用** ```typescript // 修改第 458 行 // 从: const { hands, loadHands, triggerHand, isLoading } = useGatewayStore(); // 改为: const { hands, loadHands, triggerHand, isLoading } = useHandStore(); ``` - [ ] **Step 3: 修改 getHandDetails 调用** ```typescript // 修改第 471 行 // 从: const { getHandDetails } = useGatewayStore.getState(); // 改为: const { getHandDetails } = useHandStore.getState(); ``` - [ ] **Step 4: 确保 store/index.ts 正确初始化 handStore client** ```typescript // 检查 store/index.ts 中是否有: import { setHandStoreClient } from './handStore'; // 在初始化时调用 setHandStoreClient(client) ``` - [ ] **Step 5: 运行测试验证** Run: `cd desktop && pnpm vitest run tests/desktop/handStore.test.ts` Expected: PASS - [ ] **Step 6: Commit** ```bash git add desktop/src/components/HandsPanel.tsx desktop/src/store/index.ts git commit -m "refactor(hands): migrate HandsPanel to use handStore" ``` --- ### Task 1.3: 创建类型适配器和分类映射 **Files:** - Create: `desktop/src/types/automation.ts` - Test: `tests/desktop/types/automation.test.ts` - [ ] **Step 1: 创建类型适配器文件** ```typescript // desktop/src/types/automation.ts import type { Hand, HandStatus } from './hands'; import type { Workflow, WorkflowStatus } from './workflow'; // 分类类型 export type CategoryType = 'all' | 'research' | 'data' | 'automation' | 'communication' | 'content'; // 统一自动化项 export type AutomationItem = | (Hand & { type: 'hand'; category: CategoryType }) | (Workflow & { type: 'workflow'; category: CategoryType }); // 7 个 Hand 的分类映射 export const HAND_CATEGORIES: Record = { researcher: 'research', browser: 'automation', lead: 'automation', clip: 'content', collector: 'data', predictor: 'data', twitter: 'communication', }; // Hand 转 AutomationItem export function handToAutomationItem(hand: Hand): AutomationItem { return { ...hand, type: 'hand', category: HAND_CATEGORIES[hand.id] || 'automation', }; } // Workflow 转 AutomationItem export function workflowToAutomationItem(workflow: Workflow): AutomationItem { return { ...workflow, type: 'workflow', category: 'automation', // 默认分类 }; } // 获取分类统计 export function getCategoryStats(items: AutomationItem[]): Record { const stats: Record = { all: items.length, research: 0, data: 0, automation: 0, communication: 0, content: 0, }; items.forEach(item => { if (item.category && stats[item.category] !== undefined) { stats[item.category]++; } }); return stats; } ``` - [ ] **Step 2: 写测试** ```typescript // tests/desktop/types/automation.test.ts import { describe, it, expect } from 'vitest'; import { handToAutomationItem, HAND_CATEGORIES, getCategoryStats } from '../../src/types/automation'; describe('automation types', () => { it('should map researcher to research category', () => { const hand = { id: 'researcher', name: 'Researcher', description: '', status: 'idle' as const }; const item = handToAutomationItem(hand); expect(item.category).toBe('research'); expect(item.type).toBe('hand'); }); it('should calculate category stats correctly', () => { const items = [ { id: '1', type: 'hand' as const, category: 'research' as const, name: '', description: '', status: 'idle' as const }, { id: '2', type: 'hand' as const, category: 'research' as const, name: '', description: '', status: 'idle' as const }, { id: '3', type: 'hand' as const, category: 'data' as const, name: '', description: '', status: 'idle' as const }, ]; const stats = getCategoryStats(items); expect(stats.all).toBe(3); expect(stats.research).toBe(2); expect(stats.data).toBe(1); }); }); ``` - [ ] **Step 3: 运行测试** Run: `cd desktop && pnpm vitest run tests/desktop/types/automation.test.ts` Expected: PASS - [ ] **Step 4: Commit** ```bash git add desktop/src/types/automation.ts tests/desktop/types/automation.test.ts git commit -m "feat(types): add automation type adapters and category mapping" ``` --- ### Task 1.4: 创建 WebSocket 事件 Hook **Files:** - Create: `desktop/src/hooks/useAutomationEvents.ts` - Test: `tests/desktop/hooks/useAutomationEvents.test.ts` - [ ] **Step 1: 创建事件 Hook** ```typescript // desktop/src/hooks/useAutomationEvents.ts import { useEffect, useCallback } from 'react'; import { useHandStore } from '../store/handStore'; import { useChatStore, type Message } from '../store/chatStore'; import { useGatewayStore } from '../store/gatewayStore'; interface HandEvent { hand_name: string; hand_status: string; hand_result?: unknown; run_id?: string; } export function useAutomationEvents() { const updateHandStatus = useHandStore(state => state.getHandDetails); const addMessage = useChatStore(state => state.addMessage); const client = useGatewayStore(state => state.client); // 处理 Hand 事件 const handleHandEvent = useCallback((event: HandEvent) => { // 更新 handStore 中的 Hand 状态 updateHandStatus(event.hand_name); // 如果有结果,注入到聊天流 if (event.hand_result && event.run_id) { const message: Message = { id: `hand-${event.run_id}`, role: 'hand', content: typeof event.hand_result === 'string' ? event.hand_result : JSON.stringify(event.hand_result, null, 2), timestamp: Date.now(), handName: event.hand_name, handStatus: event.hand_status, handResult: event.hand_result, }; addMessage(message); } }, [updateHandStatus, addMessage]); // 订阅 WebSocket 事件 useEffect(() => { if (!client) return; // 订阅 hand 事件 const unsubscribe = client.subscribeToEvents({ onHand: (name, status, result) => { handleHandEvent({ hand_name: name, hand_status: status, hand_result: result, }); }, }); return unsubscribe; }, [client, handleHandEvent]); return { handleHandEvent }; } ``` - [ ] **Step 2: 在 App.tsx 中初始化 Hook** ```typescript // 在 App.tsx 中添加 import { useAutomationEvents } from './hooks/useAutomationEvents'; // 在 App 组件内 function App() { // 初始化自动化事件监听 useAutomationEvents(); // ... 其余代码 } ``` - [ ] **Step 3: 运行测试** Run: `cd desktop && pnpm vitest run tests/desktop/hooks/useAutomationEvents.test.ts` Expected: PASS - [ ] **Step 4: Commit** ```bash git add desktop/src/hooks/useAutomationEvents.ts desktop/src/App.tsx tests/desktop/hooks/ git commit -m "feat(hooks): add useAutomationEvents for WebSocket hand events" ``` --- ## Phase 2: UI 重构(2-3 天) ### Task 2.1: 创建 AutomationPanel 统一入口 **Files:** - Create: `desktop/src/components/Automation/AutomationPanel.tsx` - Create: `desktop/src/components/Automation/index.ts` - [ ] **Step 1: 创建 AutomationPanel 组件** ```typescript // desktop/src/components/Automation/AutomationPanel.tsx import { useState, useEffect, useMemo } from 'react'; import { Zap, RefreshCw, Loader2, Plus } from 'lucide-react'; import { useHandStore } from '../../store/handStore'; import { handToAutomationItem, getCategoryStats, type AutomationItem, type CategoryType } from '../../types/automation'; import { AutomationCard } from './AutomationCard'; import { AutomationFilters } from './AutomationFilters'; import { BatchActionBar } from './BatchActionBar'; export function AutomationPanel() { const { hands, loadHands, isLoading } = useHandStore(); const [selectedIds, setSelectedIds] = useState>(new Set()); const [activeCategory, setActiveCategory] = useState('all'); // 转换为 AutomationItems const items = useMemo(() => hands.map(handToAutomationItem), [hands]); // 分类统计 const categoryStats = useMemo(() => getCategoryStats(items), [items]); // 过滤后的项目 const filteredItems = useMemo(() => { if (activeCategory === 'all') return items; return items.filter(item => item.category === activeCategory); }, [items, activeCategory]); useEffect(() => { loadHands(); }, [loadHands]); // 批量选择 const toggleSelect = (id: string) => { setSelectedIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const selectAll = () => setSelectedIds(new Set(filteredItems.map(i => i.id))); const clearSelection = () => setSelectedIds(new Set()); return (
{/* Header */}

自动化

Hands & Workflows

{/* Filters */} {/* Batch Actions */} {selectedIds.size > 0 && ( {/* TODO */}} onApprove={() => {/* TODO */}} onSchedule={() => {/* TODO */}} onClear={clearSelection} /> )} {/* Cards Grid */}
{filteredItems.map(item => ( toggleSelect(item.id)} /> ))}
{filteredItems.length === 0 && (
暂无可用项目
)}
); } ``` - [ ] **Step 2: 创建 index.ts 导出** ```typescript // desktop/src/components/Automation/index.ts export { AutomationPanel } from './AutomationPanel'; export { AutomationCard } from './AutomationCard'; export { AutomationFilters } from './AutomationFilters'; export { BatchActionBar } from './BatchActionBar'; export { ScheduleEditor } from './ScheduleEditor'; export { ExecutionResult } from './ExecutionResult'; export { ApprovalQueue } from './ApprovalQueue'; ``` - [ ] **Step 3: Commit** ```bash git add desktop/src/components/Automation/ git commit -m "feat(ui): add AutomationPanel unified entry component" ``` --- ### Task 2.2: 创建 AutomationCard 统一卡片 **Files:** - Create: `desktop/src/components/Automation/AutomationCard.tsx` - [ ] **Step 1: 创建卡片组件** ```typescript // desktop/src/components/Automation/AutomationCard.tsx import { Zap, Play, Clock, Settings, CheckCircle, XCircle, Loader2 } from 'lucide-react'; import type { AutomationItem } from '../../types/automation'; import { useHandStore } from '../../store/handStore'; interface AutomationCardProps { item: AutomationItem; selected: boolean; onSelect: () => void; } const STATUS_CONFIG = { idle: { label: '就绪', className: 'bg-green-100 text-green-700', dot: 'bg-green-500' }, running: { label: '运行中', className: 'bg-blue-100 text-blue-700', dot: 'bg-blue-500 animate-pulse' }, needs_approval: { label: '待审批', className: 'bg-yellow-100 text-yellow-700', dot: 'bg-yellow-500' }, error: { label: '错误', className: 'bg-red-100 text-red-700', dot: 'bg-red-500' }, unavailable: { label: '不可用', className: 'bg-gray-100 text-gray-500', dot: 'bg-gray-400' }, setup_needed: { label: '需配置', className: 'bg-orange-100 text-orange-700', dot: 'bg-orange-500' }, completed: { label: '完成', className: 'bg-green-100 text-green-700', dot: 'bg-green-500' }, paused: { label: '暂停', className: 'bg-gray-100 text-gray-500', dot: 'bg-gray-400' }, }; export function AutomationCard({ item, selected, onSelect }: AutomationCardProps) { const { triggerHand, isLoading } = useHandStore(); const status = STATUS_CONFIG[item.status] || STATUS_CONFIG.unavailable; const handleExecute = async () => { if (item.type === 'hand') { await triggerHand(item.id); } }; const canActivate = item.status === 'idle' || item.status === 'setup_needed'; return (
{/* Header */}
{item.icon || '🤖'}

{item.name}

{status.label}
{/* Description */}

{item.description}

{/* Actions */}
); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Automation/AutomationCard.tsx git commit -m "feat(ui): add AutomationCard unified card component" ``` --- ### Task 2.3: 创建分类筛选器 **Files:** - Create: `desktop/src/components/Automation/AutomationFilters.tsx` - [ ] **Step 1: 创建筛选器组件** ```typescript // desktop/src/components/Automation/AutomationFilters.tsx import type { CategoryType } from '../../types/automation'; interface AutomationFiltersProps { stats: Record; activeCategory: CategoryType; onCategoryChange: (category: CategoryType) => void; } const CATEGORIES: { key: CategoryType; label: string }[] = [ { key: 'all', label: '全部' }, { key: 'research', label: '研究' }, { key: 'data', label: '数据' }, { key: 'automation', label: '自动化' }, { key: 'communication', label: '通信' }, { key: 'content', label: '内容' }, ]; export function AutomationFilters({ stats, activeCategory, onCategoryChange }: AutomationFiltersProps) { return (
{CATEGORIES.map(cat => ( ))}
); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Automation/AutomationFilters.tsx git commit -m "feat(ui): add AutomationFilters category filter component" ``` --- ## Phase 3: 高级功能(2-3 天) ### Task 3.1: 创建可视化调度器 **Files:** - Create: `desktop/src/components/Automation/ScheduleEditor.tsx` - [ ] **Step 1: 创建调度器组件** ```typescript // desktop/src/components/Automation/ScheduleEditor.tsx import { useState, useMemo } from 'react'; import { X, Save, Clock } from 'lucide-react'; export interface ScheduleInfo { enabled: boolean; frequency: 'once' | 'daily' | 'weekly' | 'monthly' | 'custom'; time: { hour: number; minute: number }; daysOfWeek?: number[]; dayOfMonth?: number; customCron?: string; timezone: string; endDate?: Date; } interface ScheduleEditorProps { schedule?: ScheduleInfo; onSave: (schedule: ScheduleInfo) => void; onCancel: () => void; } const DAYS = ['一', '二', '三', '四', '五', '六', '日']; const TIMEZONES = ['Asia/Shanghai', 'UTC', 'America/New_York', 'Europe/London']; export function ScheduleEditor({ schedule, onSave, onCancel }: ScheduleEditorProps) { const [enabled, setEnabled] = useState(schedule?.enabled ?? true); const [frequency, setFrequency] = useState(schedule?.frequency ?? 'daily'); const [hour, setHour] = useState(schedule?.time.hour ?? 9); const [minute, setMinute] = useState(schedule?.time.minute ?? 0); const [daysOfWeek, setDaysOfWeek] = useState(schedule?.daysOfWeek ?? [1,2,3,4,5]); const [dayOfMonth, setDayOfMonth] = useState(schedule?.dayOfMonth ?? 1); const [timezone, setTimezone] = useState(schedule?.timezone ?? 'Asia/Shanghai'); // 生成预览文本 const preview = useMemo(() => { const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; const freqText = { once: '一次', daily: '每天', weekly: `每周 ${daysOfWeek.map(d => DAYS[d]).join('、')}`, monthly: `每月 ${dayOfMonth} 日`, custom: '自定义', }[frequency]; return `${freqText} ${timeStr} (${timezone})`; }, [frequency, hour, minute, daysOfWeek, dayOfMonth, timezone]); const toggleDay = (day: number) => { setDaysOfWeek(prev => prev.includes(day) ? prev.filter(d => d !== day) : [...prev, day].sort() ); }; const handleSave = () => { onSave({ enabled, frequency, time: { hour, minute }, daysOfWeek: frequency === 'weekly' ? daysOfWeek : undefined, dayOfMonth: frequency === 'monthly' ? dayOfMonth : undefined, timezone, }); }; return (

调度设置

{/* 频率选择 */}
{(['once', 'daily', 'weekly', 'monthly'] as const).map(f => ( ))}
{/* 时间选择 */}
setHour(+e.target.value)} className="w-16 px-2 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-900" /> : setMinute(+e.target.value)} className="w-16 px-2 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-900" />
{/* 每周选择 */} {frequency === 'weekly' && (
{DAYS.map((day, i) => ( ))}
)} {/* 预览 */}
{preview}
{/* 操作按钮 */}
); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Automation/ScheduleEditor.tsx git commit -m "feat(ui): add ScheduleEditor visual scheduler component" ``` --- ### Task 3.2: 创建批量操作栏 **Files:** - Create: `desktop/src/components/Automation/BatchActionBar.tsx` - [ ] **Step 1: 创建批量操作栏** ```typescript // desktop/src/components/Automation/BatchActionBar.tsx import { Play, Check, X, Clock, XCircle } from 'lucide-react'; interface BatchActionBarProps { selectedCount: number; onExecute: () => void; onApprove: () => void; onReject: () => void; onSchedule: () => void; onClear: () => void; } export function BatchActionBar({ selectedCount, onExecute, onApprove, onReject, onSchedule, onClear }: BatchActionBarProps) { return (
已选择 {selectedCount} 项
); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Automation/BatchActionBar.tsx git commit -m "feat(ui): add BatchActionBar for batch operations" ``` --- ### Task 3.3: 扩展 handStore 支持批量操作 **Files:** - Modify: `desktop/src/store/handStore.ts:166-184` - [ ] **Step 1: 添加批量操作到 store 接口** ```typescript // 在 HandActionsSlice 接口中添加 export interface HandActionsSlice { // ... 现有方法 // 批量操作 executeBatch: (ids: string[], params?: Record) => Promise; approveBatch: (approvals: { handName: string; runId: string }[]) => Promise; rejectBatch: (approvals: { handName: string; runId: string; reason: string }[]) => Promise; } interface BatchResult { successful: string[]; failed: Array<{ id: string; error: string }>; } ``` - [ ] **Step 2: 实现批量操作方法** ```typescript // 在 useHandStore create 中添加 executeBatch: async (ids: string[], params?: Record) => { const client = get().client; if (!client) return { successful: [], failed: ids.map(id => ({ id, error: 'No client' })) }; const results = await Promise.allSettled( ids.map(id => client.triggerHand(id, params)) ); const successful: string[] = []; const failed: Array<{ id: string; error: string }> = []; results.forEach((result, i) => { if (result.status === 'fulfilled' && result.value) { successful.push(ids[i]); } else { failed.push({ id: ids[i], error: result.status === 'rejected' ? String(result.reason) : 'Unknown error' }); } }); await get().loadHands(); return { successful, failed }; }, approveBatch: async (approvals: { handName: string; runId: string }[]) => { const client = get().client; if (!client) return { successful: [], failed: approvals.map(a => ({ id: a.runId, error: 'No client' })) }; const results = await Promise.allSettled( approvals.map(a => client.approveHand(a.handName, a.runId, true)) ); const successful: string[] = []; const failed: Array<{ id: string; error: string }> = []; results.forEach((result, i) => { if (result.status === 'fulfilled') { successful.push(approvals[i].runId); } else { failed.push({ id: approvals[i].runId, error: String(result.reason) }); } }); await get().loadApprovals(); return { successful, failed }; }, rejectBatch: async (approvals: { handName: string; runId: string; reason: string }[]) => { const client = get().client; if (!client) return { successful: [], failed: approvals.map(a => ({ id: a.runId, error: 'No client' })) }; const results = await Promise.allSettled( approvals.map(a => client.approveHand(a.handName, a.runId, false, a.reason)) ); const successful: string[] = []; const failed: Array<{ id: string; error: string }> = []; results.forEach((result, i) => { if (result.status === 'fulfilled') { successful.push(approvals[i].runId); } else { failed.push({ id: approvals[i].runId, error: String(result.reason) }); } }); await get().loadApprovals(); return { successful, failed }; }, ``` - [ ] **Step 3: Commit** ```bash git add desktop/src/store/handStore.ts git commit -m "feat(store): add batch operations to handStore" ``` --- ## Phase 4: 工作流集成(2-3 天) ### Task 4.1: 创建审批队列组件 **Files:** - Create: `desktop/src/components/Automation/ApprovalQueue.tsx` - [ ] **Step 1: 创建审批队列组件** ```typescript // desktop/src/components/Automation/ApprovalQueue.tsx import { useEffect, useState } from 'react'; import { Check, X, Clock } from 'lucide-react'; import { useHandStore, type Approval } from '../../store/handStore'; export function ApprovalQueue() { const { approvals, loadApprovals, respondToApproval, isLoading } = useHandStore(); const [selectedIds, setSelectedIds] = useState>(new Set()); useEffect(() => { loadApprovals('pending'); }, [loadApprovals]); const pendingApprovals = approvals.filter(a => a.status === 'pending'); const toggleSelect = (id: string) => { setSelectedIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleApproveAll = async () => { for (const id of selectedIds) { await respondToApproval(id, true); } setSelectedIds(new Set()); }; const handleRejectAll = async () => { for (const id of selectedIds) { await respondToApproval(id, false, '批量拒绝'); } setSelectedIds(new Set()); }; return (

审批队列 ({pendingApprovals.length} 待处理)

{selectedIds.size > 0 && (
)}
{pendingApprovals.length === 0 ? (
暂无待处理的审批请求
) : (
{pendingApprovals.map(approval => (
toggleSelect(approval.id)} className="mt-1" />

{approval.handName}

{approval.reason}

{new Date(approval.requestedAt).toLocaleString()}

))}
)}
); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Automation/ApprovalQueue.tsx git commit -m "feat(ui): add ApprovalQueue component for batch approvals" ``` --- ### Task 4.2: 创建执行结果展示组件 **Files:** - Create: `desktop/src/components/Automation/ExecutionResult.tsx` - [ ] **Step 1: 创建结果展示组件** ```typescript // desktop/src/components/Automation/ExecutionResult.tsx import { CheckCircle, XCircle, Clock, Loader2, Download, RotateCcw } from 'lucide-react'; interface ExecutionResultProps { type: 'hand' | 'workflow'; name: string; status: 'running' | 'completed' | 'failed' | 'needs_approval'; duration?: number; output?: unknown; error?: string; onRetry?: () => void; onDownload?: () => void; } export function ExecutionResult({ type, name, status, duration, output, error, onRetry, onDownload }: ExecutionResultProps) { const statusConfig = { running: { icon: Loader2, color: 'text-blue-500', label: '运行中', animate: true }, completed: { icon: CheckCircle, color: 'text-green-500', label: '完成', animate: false }, failed: { icon: XCircle, color: 'text-red-500', label: '失败', animate: false }, needs_approval: { icon: Clock, color: 'text-yellow-500', label: '待审批', animate: false }, }; const config = statusConfig[status]; const Icon = config.icon; const formatDuration = (ms: number) => { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; }; return (
{/* Header */}
{type === 'hand' ? '🔍' : '📋'} {name}
{config.label}
{/* Meta */} {duration !== undefined && (
耗时: {formatDuration(duration)}
)} {/* Output */} {output && (
            {typeof output === 'string' ? output : JSON.stringify(output, null, 2)}
          
)} {/* Error */} {error && (

{error}

)} {/* Actions */}
{onDownload && output && ( )} {onRetry && status === 'failed' && ( )}
); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Automation/ExecutionResult.tsx git commit -m "feat(ui): add ExecutionResult display component" ``` --- ### Task 4.3: 更新 Sidebar 导航 **Files:** - Modify: `desktop/src/components/Sidebar.tsx` - [ ] **Step 1: 添加自动化导航入口** ```typescript // 在 Sidebar 导航项中添加或修改 // 将 "Hands" 改为 "自动化" { id: 'automation', label: '自动化', icon: Zap, view: 'automation', } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/Sidebar.tsx git commit -m "feat(ui): update Sidebar with automation navigation" ``` --- ### Task 4.4: 更新 App.tsx 路由 **Files:** - Modify: `desktop/src/App.tsx` - [ ] **Step 1: 添加自动化视图** ```typescript // 在 App.tsx 中添加 automation 视图的处理 import { AutomationPanel } from './components/Automation'; // 在视图切换逻辑中 case 'automation': return ; ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/App.tsx git commit -m "feat(app): add automation view routing" ``` --- ## 验收测试清单 完成所有任务后,运行以下验收测试: - [ ] **验收 1: Hand 执行流程** 1. 打开自动化面板 2. 选择一个 Hand 3. 填写参数表单 4. 点击执行 5. 确认参数正确传递到 API 6. 确认看到实时状态更新 7. 确认结果显示在聊天流 - [ ] **验收 2: 批量操作** 1. 选择 3 个 Hands 2. 点击批量执行 3. 确认全部成功或看到失败报告 - [ ] **验收 3: 调度器** 1. 点击调度按钮 2. 设置"每天 9:00" 3. 保存 4. 确认在调度列表看到正确预览 - [ ] **验收 4: 审批流程** 1. 触发需要审批的 Hand 2. 确认弹出审批请求 3. 批准 4. 确认继续执行 --- ## 风险与缓解 | 风险 | 缓解措施 | |------|---------| | 后端 API 未完全实现 | 使用 fallback 数据,标注"后端未实现" | | WebSocket 不稳定 | 心跳机制 + 自动重连 | | 迁移影响现有功能 | 保留旧组件作为 fallback,分阶段迁移 | --- ## 完成标志 当以下条件全部满足时,视为实现完成: 1. ✅ 所有 Phase 1-4 任务完成 2. ✅ 验收测试 1-4 全部通过 3. ✅ `pnpm vitest run` 全部通过 4. ✅ `pnpm tsc --noEmit` 无错误 5. ✅ 所有 commits 已推送到分支