import { useCallback, useEffect, useMemo, useState } from 'react' import { AlertCircle, CheckCircle2, Clock3, Loader2, Play, TerminalSquare } from 'lucide-react' import type { Session } from '../../stores/sessionStore' import { getSessionConfig } from '../../stores/connection' import { useSettingsStore } from '../../stores/settingsStore' import Modal from '../common/Modal' import Button from '../common/Button' type BatchCommandStatus = 'idle' | 'running' | 'success' | 'error' interface BatchCommandResult { sessionId: string status: BatchCommandStatus output?: string error?: string durationMs?: number } interface BatchCommandDialogProps { isOpen: boolean sessions: Session[] onClose: () => void } const MAX_CONCURRENCY = 5 export default function BatchCommandDialog({ isOpen, sessions, onClose }: BatchCommandDialogProps) { const [command, setCommand] = useState('') const [running, setRunning] = useState(false) const [results, setResults] = useState([]) const timeout = useSettingsStore((state) => state.settings.timeout) useEffect(() => { if (!isOpen) return setResults(sessions.map((session) => ({ sessionId: session.id, status: 'idle' }))) }, [isOpen, sessions]) const updateResult = useCallback((sessionId: string, updates: Partial) => { setResults((current) => current.map((result) => result.sessionId === sessionId ? { ...result, ...updates } : result )) }, []) const executeForSession = useCallback(async (session: Session, commandText: string) => { const startedAt = Date.now() let connectionId: string | undefined updateResult(session.id, { status: 'running', output: undefined, error: undefined }) try { const config = await getSessionConfig(session.id) if (!config) throw new Error('无法读取会话配置') const connected = await window.opsxApi.sshConnect({ ...config, timeoutSeconds: config.timeoutSeconds ?? timeout, keepAlive: false, }) if (!connected.success || !connected.connectionId) { throw new Error(connected.error || '连接失败') } connectionId = connected.connectionId const executed = await window.opsxApi.sshExec(connectionId, commandText) if (!executed.success) throw new Error(executed.error || '命令执行失败') updateResult(session.id, { status: 'success', output: executed.result?.trimEnd() || '(无输出)', durationMs: Date.now() - startedAt, }) } catch (error) { updateResult(session.id, { status: 'error', error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startedAt, }) } finally { if (connectionId) { await window.opsxApi.sshDisconnect(connectionId).catch(() => undefined) } } }, [timeout, updateResult]) const execute = useCallback(async () => { const commandText = command.trim() if (!commandText || running || sessions.length === 0) return setRunning(true) setResults(sessions.map((session) => ({ sessionId: session.id, status: 'idle' }))) let nextIndex = 0 const worker = async () => { while (nextIndex < sessions.length) { const session = sessions[nextIndex] nextIndex += 1 await executeForSession(session, commandText) } } try { await Promise.all( Array.from({ length: Math.min(MAX_CONCURRENCY, sessions.length) }, () => worker()) ) } finally { setRunning(false) } }, [command, executeForSession, running, sessions]) const summary = useMemo(() => ({ success: results.filter((result) => result.status === 'success').length, error: results.filter((result) => result.status === 'error').length, }), [results]) const handleClose = () => { if (!running) onClose() } return ( } onClose={handleClose} showFooter showConfirm={false} disableBackdropClose={running} size="full" footer={
{summary.success > 0 && 成功 {summary.success}} {summary.success > 0 && summary.error > 0 && ' · '} {summary.error > 0 && 失败 {summary.error}}
} >