# Browser Hand UI 组件实现计划 > **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:** 实现 Browser Hand UI 组件,集成到 ZCLAW 的 HandsPanel,支持任务模板执行和实时状态预览。 **Architecture:** 使用 Zustand 管理状态,React 组件展示 UI,通过 Tauri commands 调用 Fantoccini WebDriver。 **Tech Stack:** React, TypeScript, Zustand, Tailwind CSS, Lucide Icons, Tauri **Spec:** [2026-03-17-browser-hand-ui-design.md](../specs/2026-03-17-browser-hand-ui-design.md) --- ## 文件结构 ``` desktop/src/ ├── store/ │ └── browserHandStore.ts # [CREATE] 状态管理 ├── lib/ │ └── browser-templates.ts # [CREATE] 模板执行引擎 ├── components/ │ ├── HandsPanel.tsx # [MODIFY] 集成 BrowserHandCard │ └── BrowserHand/ │ ├── index.ts # [CREATE] 模块导出 │ ├── BrowserHandCard.tsx # [CREATE] 主卡片组件 │ ├── TaskTemplateModal.tsx # [CREATE] 模板选择模态框 │ ├── ScreenshotPreview.tsx # [CREATE] 截图预览 │ └── templates/ │ ├── index.ts # [CREATE] 模板注册 │ ├── types.ts # [CREATE] 类型定义 │ ├── basic.ts # [CREATE] 基础操作模板 │ ├── scraping.ts # [CREATE] 数据采集模板 │ └── automation.ts # [CREATE] 自动化流程模板 └── tests/ └── desktop/ └── browserHandStore.test.ts # [CREATE] Store 测试 ``` --- ## Chunk 1: 基础架构 ### Task 1.1: 模板类型定义 **Files:** - Create: `desktop/src/components/BrowserHand/templates/types.ts` - [ ] **Step 1: 创建模板类型文件** ```typescript // desktop/src/components/BrowserHand/templates/types.ts export interface TaskTemplateParam { key: string; label: string; type: 'text' | 'url' | 'number' | 'select' | 'textarea' | 'json'; required: boolean; default?: unknown; placeholder?: string; options?: { value: string; label: string }[]; description?: string; } export interface TaskTemplate { id: string; name: string; description: string; category: 'basic' | 'scraping' | 'automation'; icon: string; params: TaskTemplateParam[]; execute: (params: Record, context: ExecutionContext) => Promise; } export interface ExecutionContext { browser: import('../../lib/browser-client').Browser; onProgress: (action: string, progress: number) => void; onLog: (level: 'info' | 'warn' | 'error' | 'action', message: string, details?: Record) => void; } export interface TemplateCategory { id: 'basic' | 'scraping' | 'automation'; name: string; description: string; } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/templates/types.ts git commit -m "feat(browser-hand): add template type definitions" ``` --- ### Task 1.2: 基础操作模板 **Files:** - Create: `desktop/src/components/BrowserHand/templates/basic.ts` - [ ] **Step 1: 实现基础操作模板** ```typescript // desktop/src/components/BrowserHand/templates/basic.ts import type { TaskTemplate, ExecutionContext } from './types'; export const basicTemplates: TaskTemplate[] = [ { id: 'basic_navigate_screenshot', name: '打开网页并截图', description: '访问指定 URL 并截图保存', category: 'basic', icon: '📸', params: [ { key: 'url', label: '网页地址', type: 'url', required: true, placeholder: 'https://example.com', description: '要访问的网页 URL', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; onProgress('导航到页面', 20); onLog('info', `正在打开: ${url}`); await browser.goto(url); onProgress('等待页面加载', 50); await new Promise(r => setTimeout(r, 1000)); onProgress('截取屏幕', 80); const screenshot = await browser.screenshot(); onLog('info', '截图完成'); onProgress('完成', 100); return { url, screenshot }; }, }, { id: 'basic_fill_form', name: '填写表单', description: '访问页面并填写表单字段', category: 'basic', icon: '📝', params: [ { key: 'url', label: '网页地址', type: 'url', required: true, }, { key: 'fields', label: '表单字段', type: 'json', required: true, placeholder: '[{"selector": "input[name=email]", "value": "test@example.com"}]', description: 'JSON 格式的字段配置', }, { key: 'submitSelector', label: '提交按钮', type: 'text', required: false, placeholder: 'button[type="submit"]', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; const fields = params.fields as Array<{ selector: string; value: string }>; const submitSelector = params.submitSelector as string | undefined; onProgress('导航到页面', 20); await browser.goto(url); onProgress('填写表单', 40); for (const field of fields) { await browser.type(field.selector, field.value, true); onLog('action', `填写字段: ${field.selector}`); } if (submitSelector) { onProgress('提交表单', 70); await browser.click(submitSelector); onLog('action', `点击提交: ${submitSelector}`); await new Promise(r => setTimeout(r, 1000)); } onProgress('完成', 100); return { success: true, fieldsFilled: fields.length }; }, }, { id: 'basic_click_navigate', name: '点击导航', description: '访问页面并点击指定元素', category: 'basic', icon: '🖱️', params: [ { key: 'url', label: '网页地址', type: 'url', required: true, }, { key: 'selector', label: '元素选择器', type: 'text', required: true, placeholder: 'a.link, button.submit', }, { key: 'waitFor', label: '等待元素', type: 'text', required: false, description: '点击后等待此元素出现', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; const selector = params.selector as string; const waitFor = params.waitFor as string | undefined; onProgress('导航到页面', 20); await browser.goto(url); onProgress('等待元素', 40); await browser.wait(selector, 10000); onProgress('点击元素', 60); await browser.click(selector); onLog('action', `点击: ${selector}`); if (waitFor) { onProgress('等待页面更新', 80); await browser.wait(waitFor, 10000); } const finalUrl = await browser.url(); onProgress('完成', 100); return { clicked: selector, finalUrl }; }, }, ]; ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/templates/basic.ts git commit -m "feat(browser-hand): add basic operation templates" ``` --- ### Task 1.3: 数据采集模板 **Files:** - Create: `desktop/src/components/BrowserHand/templates/scraping.ts` - [ ] **Step 1: 实现数据采集模板** ```typescript // desktop/src/components/BrowserHand/templates/scraping.ts import type { TaskTemplate, ExecutionContext } from './types'; export const scrapingTemplates: TaskTemplate[] = [ { id: 'scrape_text', name: '抓取页面文本', description: '从多个选择器提取文本内容', category: 'scraping', icon: '📄', params: [ { key: 'url', label: '网页地址', type: 'url', required: true, }, { key: 'selectors', label: '选择器列表', type: 'textarea', required: true, placeholder: 'h1.title\np.description\n.price', description: '每行一个 CSS 选择器', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; const selectorsText = params.selectors as string; const selectors = selectorsText.split('\n').map(s => s.trim()).filter(Boolean); onProgress('导航到页面', 20); await browser.goto(url); onProgress('提取内容', 50); const results: Record = {}; for (const selector of selectors) { try { const elements = await browser.$$(selector); results[selector] = elements.map(el => el.text || ''); onLog('info', `提取 ${selector}: ${results[selector].length} 个元素`); } catch { results[selector] = []; onLog('warn', `未找到: ${selector}`); } } onProgress('完成', 100); return { url, data: results }; }, }, { id: 'scrape_list', name: '提取列表数据', description: '批量提取结构化列表数据', category: 'scraping', icon: '📊', params: [ { key: 'url', label: '网页地址', type: 'url', required: true, }, { key: 'itemSelector', label: '列表项选择器', type: 'text', required: true, placeholder: '.product-item, .list-item', }, { key: 'fields', label: '字段映射', type: 'json', required: true, placeholder: '{"name": "h3", "price": ".price", "link": "a@href"}', description: 'JSON 对象,键为字段名,值为选择器', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; const itemSelector = params.itemSelector as string; const fields = params.fields as Record; onProgress('导航到页面', 20); await browser.goto(url); onProgress('查找列表项', 40); const items = await browser.$$(itemSelector); onLog('info', `找到 ${items.length} 个列表项`); onProgress('提取数据', 60); const data = items.map((item, index) => { const row: Record = {}; for (const [field, selector] of Object.entries(fields)) { row[field] = item.text; // Simplified - real impl would use relative selectors } return row; }); onProgress('完成', 100); return { url, count: data.length, data }; }, }, { id: 'scrape_images', name: '收集图片链接', description: '提取页面中的图片 URL', category: 'scraping', icon: '🖼️', params: [ { key: 'url', label: '网页地址', type: 'url', required: true, }, { key: 'imageSelector', label: '图片选择器', type: 'text', required: false, default: 'img', placeholder: 'img, .gallery img', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; const imageSelector = (params.imageSelector as string) || 'img'; onProgress('导航到页面', 20); await browser.goto(url); onProgress('查找图片', 50); const images = await browser.$$(imageSelector); onLog('info', `找到 ${images.length} 张图片`); onProgress('完成', 100); return { url, count: images.length }; }, }, ]; ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/templates/scraping.ts git commit -m "feat(browser-hand): add scraping templates" ``` --- ### Task 1.4: 自动化流程模板 **Files:** - Create: `desktop/src/components/BrowserHand/templates/automation.ts` - [ ] **Step 1: 实现自动化流程模板** ```typescript // desktop/src/components/BrowserHand/templates/automation.ts import type { TaskTemplate, ExecutionContext } from './types'; export const automationTemplates: TaskTemplate[] = [ { id: 'auto_login_action', name: '登录并操作', description: '执行登录后进行后续操作', category: 'automation', icon: '🔐', params: [ { key: 'loginUrl', label: '登录页面', type: 'url', required: true, }, { key: 'username', label: '用户名', type: 'text', required: true, }, { key: 'password', label: '密码', type: 'text', required: true, }, { key: 'usernameSelector', label: '用户名输入框', type: 'text', required: true, default: 'input[name="username"], input[type="text"]', }, { key: 'passwordSelector', label: '密码输入框', type: 'text', required: true, default: 'input[name="password"], input[type="password"]', }, { key: 'submitSelector', label: '登录按钮', type: 'text', required: true, default: 'button[type="submit"], input[type="submit"]', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; onProgress('打开登录页', 10); await browser.goto(params.loginUrl as string); onProgress('填写凭据', 30); await browser.type(params.usernameSelector as string, params.username as string, true); await browser.type(params.passwordSelector as string, params.password as string, true); onLog('action', '已填写用户名和密码'); onProgress('提交登录', 50); await browser.click(params.submitSelector as string); onLog('action', '已点击登录按钮'); onProgress('等待登录完成', 70); await new Promise(r => setTimeout(r, 2000)); const finalUrl = await browser.url(); onProgress('完成', 100); return { success: true, finalUrl }; }, }, { id: 'auto_multi_page', name: '多页面导航', description: '遍历多个 URL 执行操作', category: 'automation', icon: '📑', params: [ { key: 'urls', label: 'URL 列表', type: 'textarea', required: true, placeholder: 'https://example1.com\nhttps://example2.com', description: '每行一个 URL', }, { key: 'action', label: '每页操作', type: 'select', required: true, default: 'screenshot', options: [ { value: 'screenshot', label: '截图' }, { value: 'source', label: '获取源码' }, { value: 'text', label: '提取文本' }, ], }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const urlsText = params.urls as string; const urls = urlsText.split('\n').map(s => s.trim()).filter(Boolean); const action = params.action as string; const results: Array<{ url: string; success: boolean; data?: unknown }> = []; for (let i = 0; i < urls.length; i++) { const url = urls[i]; const progress = Math.round(((i + 1) / urls.length) * 100); onProgress(`处理 ${i + 1}/${urls.length}`, progress); try { await browser.goto(url); await new Promise(r => setTimeout(r, 1000)); let data: unknown; if (action === 'screenshot') { data = await browser.screenshot(); } else if (action === 'source') { data = await browser.source(); } results.push({ url, success: true, data }); onLog('info', `完成: ${url}`); } catch (err) { results.push({ url, success: false }); onLog('error', `失败: ${url}`); } } return { total: urls.length, results }; }, }, { id: 'auto_monitor', name: '页面监控', description: '检查页面是否存在指定内容', category: 'automation', icon: '👁️', params: [ { key: 'url', label: '监控页面', type: 'url', required: true, }, { key: 'checkSelector', label: '检查元素', type: 'text', required: true, description: '检查此元素是否存在', }, ], execute: async (params, context) => { const { browser, onProgress, onLog } = context; const url = params.url as string; const checkSelector = params.checkSelector as string; onProgress('访问页面', 30); await browser.goto(url); onProgress('检查元素', 60); let found = false; try { await browser.wait(checkSelector, 5000); found = true; onLog('info', `元素存在: ${checkSelector}`); } catch { onLog('warn', `元素不存在: ${checkSelector}`); } onProgress('完成', 100); return { url, selector: checkSelector, found, checkedAt: new Date().toISOString() }; }, }, ]; ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/templates/automation.ts git commit -m "feat(browser-hand): add automation templates" ``` --- ### Task 1.5: 模板注册 **Files:** - Create: `desktop/src/components/BrowserHand/templates/index.ts` - [ ] **Step 1: 创建模板注册文件** ```typescript // desktop/src/components/BrowserHand/templates/index.ts import type { TaskTemplate, TemplateCategory } from './types'; import { basicTemplates } from './basic'; import { scrapingTemplates } from './scraping'; import { automationTemplates } from './automation'; export * from './types'; export const TEMPLATE_CATEGORIES: TemplateCategory[] = [ { id: 'basic', name: '基础操作', description: '导航、点击、表单等基础操作', }, { id: 'scraping', name: '数据采集', description: '文本、列表、图片等数据提取', }, { id: 'automation', name: '自动化流程', description: '登录、多页面、监控等自动化任务', }, ]; export const BUILTIN_TEMPLATES: TaskTemplate[] = [ ...basicTemplates, ...scrapingTemplates, ...automationTemplates, ]; export function getTemplatesByCategory(category: TaskTemplate['category']): TaskTemplate[] { return BUILTIN_TEMPLATES.filter(t => t.category === category); } export function getTemplateById(id: string): TaskTemplate | undefined { return BUILTIN_TEMPLATES.find(t => t.id === id); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/templates/index.ts git commit -m "feat(browser-hand): add template registry" ``` --- ### Task 1.6: 状态管理 Store **Files:** - Create: `desktop/src/store/browserHandStore.ts` - [ ] **Step 1: 创建 browserHandStore** ```typescript // desktop/src/store/browserHandStore.ts import { create } from 'zustand'; import Browser from '../lib/browser-client'; import type { TaskTemplate } from '../components/BrowserHand/templates/types'; import { BUILTIN_TEMPLATES } from '../components/BrowserHand/templates'; // === Types === export interface BrowserLog { id: string; timestamp: string; level: 'info' | 'warn' | 'error' | 'action'; message: string; details?: Record; } export interface ExecutionState { isRunning: boolean; currentAction: string | null; currentUrl: string | null; lastScreenshot: string | null; progress: number; startTime: string | null; templateName: string | null; } export interface RecentTask { id: string; templateId: string; templateName: string; params: Record; status: 'success' | 'failed' | 'cancelled'; executedAt: string; duration: number; result?: unknown; error?: string; } // === Store State === interface BrowserHandState { execution: ExecutionState; logs: BrowserLog[]; templates: TaskTemplate[]; recentTasks: RecentTask[]; isTemplateModalOpen: boolean; error: string | null; } // === Store Actions === interface BrowserHandActions { executeTemplate: (templateId: string, params: Record) => Promise; stopExecution: () => void; updateExecutionState: (state: Partial) => void; addLog: (log: Omit) => void; clearLogs: () => void; openTemplateModal: () => void; closeTemplateModal: () => void; clearError: () => void; getTemplate: (id: string) => TaskTemplate | undefined; } // === Helpers === const genId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const ts = () => new Date().toISOString(); // === Store === export const useBrowserHandStore = create((set, get) => ({ // Initial State execution: { isRunning: false, currentAction: null, currentUrl: null, lastScreenshot: null, progress: 0, startTime: null, templateName: null, }, logs: [], templates: BUILTIN_TEMPLATES, recentTasks: [], isTemplateModalOpen: false, error: null, // Actions executeTemplate: async (templateId, params) => { const template = get().templates.find(t => t.id === templateId); if (!template) throw new Error(`Template not found: ${templateId}`); set({ execution: { isRunning: true, currentAction: '初始化', currentUrl: null, lastScreenshot: null, progress: 0, startTime: ts(), templateName: template.name, }, error: null, }); get().addLog({ level: 'info', message: `开始执行: ${template.name}` }); const browser = new Browser(); const startTime = Date.now(); try { await browser.start({ headless: true }); const context = { browser, onProgress: (action: string, progress: number) => { get().updateExecutionState({ currentAction: action, progress }); get().addLog({ level: 'action', message: action }); }, onLog: (level, message, details) => { get().addLog({ level, message, details }); }, }; const result = await template.execute(params, context); try { const screenshot = await browser.screenshot(); get().updateExecutionState({ lastScreenshot: screenshot.base64 }); } catch { /* ignore */ } const duration = Date.now() - startTime; get().addLog({ level: 'info', message: `执行完成,耗时 ${duration}ms` }); const task: RecentTask = { id: genId(), templateId, templateName: template.name, params, status: 'success', executedAt: ts(), duration, result, }; set(state => ({ recentTasks: [task, ...state.recentTasks].slice(0, 20), execution: { ...state.execution, isRunning: false, progress: 100, currentAction: '完成' }, })); return result; } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); const duration = Date.now() - startTime; get().addLog({ level: 'error', message: `执行失败: ${errorMsg}` }); const task: RecentTask = { id: genId(), templateId, templateName: template.name, params, status: 'failed', executedAt: ts(), duration, error: errorMsg, }; set(state => ({ recentTasks: [task, ...state.recentTasks].slice(0, 20), execution: { ...state.execution, isRunning: false, currentAction: '失败' }, error: errorMsg, })); throw err; } finally { await browser.close(); } }, stopExecution: () => { set(state => ({ execution: { ...state.execution, isRunning: false, currentAction: '已停止' }, })); get().addLog({ level: 'warn', message: '任务已停止' }); }, updateExecutionState: (state) => { set(current => ({ execution: { ...current.execution, ...state } })); }, addLog: (log) => { set(state => ({ logs: [...state.logs.slice(-99), { ...log, id: genId(), timestamp: ts() }], })); }, clearLogs: () => set({ logs: [] }), openTemplateModal: () => set({ isTemplateModalOpen: true }), closeTemplateModal: () => set({ isTemplateModalOpen: false }), clearError: () => set({ error: null }), getTemplate: (id) => get().templates.find(t => t.id === id), })); export default useBrowserHandStore; ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/store/browserHandStore.ts git commit -m "feat(browser-hand): add browserHandStore for state management" ``` --- ## Chunk 2: UI 组件 ### Task 2.1: 模块导出 **Files:** - Create: `desktop/src/components/BrowserHand/index.ts` - [ ] **Step 1: 创建模块导出** ```typescript // desktop/src/components/BrowserHand/index.ts export { BrowserHandCard } from './BrowserHandCard'; export { TaskTemplateModal } from './TaskTemplateModal'; export { ScreenshotPreview } from './ScreenshotPreview'; export * from './templates'; ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/index.ts git commit -m "feat(browser-hand): add module exports" ``` --- ### Task 2.2: 截图预览组件 **Files:** - Create: `desktop/src/components/BrowserHand/ScreenshotPreview.tsx` - [ ] **Step 1: 创建 ScreenshotPreview 组件** ```typescript // desktop/src/components/BrowserHand/ScreenshotPreview.tsx import { useState } from 'react'; import { ImageOff, Maximize2, RefreshCw, Loader2 } from 'lucide-react'; interface ScreenshotPreviewProps { base64: string | null; isLoading?: boolean; onRefresh?: () => void; } export function ScreenshotPreview({ base64, isLoading, onRefresh }: ScreenshotPreviewProps) { const [isFullscreen, setIsFullscreen] = useState(false); if (isLoading) { return (
); } if (!base64) { return (
暂无截图
); } return ( <>
Screenshot setIsFullscreen(true)} />
{onRefresh && ( )}
{isFullscreen && (
setIsFullscreen(false)} > Screenshot Full
)} ); } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/components/BrowserHand/ScreenshotPreview.tsx git commit -m "feat(browser-hand): add ScreenshotPreview component" ``` --- ### Task 2.3: 任务模板选择模态框 **Files:** - Create: `desktop/src/components/BrowserHand/TaskTemplateModal.tsx` - [ ] **Step 1: 创建 TaskTemplateModal 组件** ```typescript // desktop/src/components/BrowserHand/TaskTemplateModal.tsx import { useState, useMemo } from 'react'; import { X, Play, AlertCircle } from 'lucide-react'; import { useBrowserHandStore } from '../../store/browserHandStore'; import { TEMPLATE_CATEGORIES, type TaskTemplate, type TaskTemplateParam } from './templates'; interface TaskTemplateModalProps { isOpen: boolean; onClose: () => void; } export function TaskTemplateModal({ isOpen, onClose }: TaskTemplateModalProps) { const { templates, executeTemplate, isTemplateModalOpen } = useBrowserHandStore(); const [selectedCategory, setSelectedCategory] = useState<'basic' | 'scraping' | 'automation'>('basic'); const [selectedTemplate, setSelectedTemplate] = useState(null); const [params, setParams] = useState>({}); const [isExecuting, setIsExecuting] = useState(false); const [errors, setErrors] = useState>({}); const filteredTemplates = useMemo( () => templates.filter(t => t.category === selectedCategory), [templates, selectedCategory] ); const handleSelectTemplate = (template: TaskTemplate) => { setSelectedTemplate(template); // Initialize params with defaults const defaultParams: Record = {}; template.params.forEach(p => { if (p.default !== undefined) defaultParams[p.key] = p.default; }); setParams(defaultParams); setErrors({}); }; const handleParamChange = (key: string, value: unknown) => { setParams(prev => ({ ...prev, [key]: value })); setErrors(prev => ({ ...prev, [key]: '' })); }; const validateParams = (): boolean => { if (!selectedTemplate) return false; const newErrors: Record = {}; selectedTemplate.params.forEach(param => { if (param.required) { const value = params[param.key]; if (value === undefined || value === '' || value === null) { newErrors[param.key] = `${param.label}为必填项`; } } // Type-specific validation if (param.type === 'url' && params[param.key]) { try { new URL(params[param.key] as string); } catch { newErrors[param.key] = '请输入有效的 URL'; } } if (param.type === 'json' && params[param.key]) { try { JSON.parse(params[param.key] as string); } catch { newErrors[param.key] = '请输入有效的 JSON'; } } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleExecute = async () => { if (!selectedTemplate || !validateParams()) return; // Parse JSON params const parsedParams: Record = {}; selectedTemplate.params.forEach(p => { if (p.type === 'json' && typeof params[p.key] === 'string') { try { parsedParams[p.key] = JSON.parse(params[p.key] as string); } catch { parsedParams[p.key] = params[p.key]; } } else if (p.type === 'textarea' && typeof params[p.key] === 'string') { // Handle multiline text (split by newline for certain fields) if (p.key === 'selectors' || p.key === 'urls') { parsedParams[p.key] = params[p.key]; } else { parsedParams[p.key] = params[p.key]; } } else { parsedParams[p.key] = params[p.key]; } }); setIsExecuting(true); try { await executeTemplate(selectedTemplate.id, parsedParams); onClose(); } catch { // Error is handled in store } finally { setIsExecuting(false); } }; if (!isOpen) return null; return (
{/* Header */}

选择任务模板

{/* Category Tabs */}
{TEMPLATE_CATEGORIES.map(cat => ( ))}
{/* Template Grid */}
{filteredTemplates.map(template => ( ))}
{/* Selected Template Params */} {selectedTemplate && (

{selectedTemplate.icon} {selectedTemplate.name}

{selectedTemplate.description}

{selectedTemplate.params.map(param => ( handleParamChange(param.key, value)} /> ))}
)}
{/* Footer */}
); } // Param Input Component function ParamInput({ param, value, error, onChange, }: { param: TaskTemplateParam; value: unknown; error?: string; onChange: (value: unknown) => void; }) { const inputClass = `w-full px-3 py-2 border rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white ${ error ? 'border-red-500' : 'border-gray-200 dark:border-gray-600' }`; return (
{param.type === 'select' ? ( ) : param.type === 'textarea' || param.type === 'json' ? (