From dfeb2865919c45c71f81b0fd4d20e2701b7c4718 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 18 Mar 2026 15:55:18 +0800 Subject: [PATCH] docs(plan): add automation system redesign implementation plan Four phases: - P1: Fix core issues (param passing, store migration, WebSocket events) - P2: UI refactor (AutomationPanel, AutomationCard, filters) - P3: Advanced features (ScheduleEditor, batch operations) - P4: Workflow integration (ApprovalQueue, ExecutionResult) Co-Authored-By: Claude Opus 4.6 --- .../2026-03-18-automation-system-redesign.md | 1333 +++++++++++++++++ 1 file changed, 1333 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-automation-system-redesign.md diff --git a/docs/superpowers/plans/2026-03-18-automation-system-redesign.md b/docs/superpowers/plans/2026-03-18-automation-system-redesign.md new file mode 100644 index 0000000..3f7eafd --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-automation-system-redesign.md @@ -0,0 +1,1333 @@ +# 自动化系统重设计实现计划 + +> **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 已推送到分支