feat(orchestrator): redesign orchestrator page as template editor with terminal execution

Phase 1: Orchestrator Simplification
- Remove ExecutionMonitor from OrchestratorPage
- Replace "Run Workflow" button with "Send to Terminal" button
- Update i18n texts for template editor context

Phase 2: Session Lock Mechanism
- Add 'locked' status to TerminalStatus type
- Extend TerminalMeta with isLocked, lockReason, lockedByExecutionId, lockedAt
- Implement lockSession/unlockSession in sessionManagerStore
- Create SessionLockConfirmDialog component for input interception

Phase 3: Execution Monitor Panel
- Create executionMonitorStore for execution state management
- Create ExecutionMonitorPanel component with step progress display
- Add execution panel to DashboardToolbar and TerminalDashboardPage
- Support WebSocket message handling for execution updates

Phase 4: Execution Bridge
- Add POST /api/orchestrator/flows/:id/execute-in-session endpoint
- Create useExecuteFlowInSession hook for frontend API calls
- Broadcast EXECUTION_STARTED and CLI_SESSION_LOCKED WebSocket messages
- Lock session when execution starts, unlock on completion
This commit is contained in:
catlog22
2026-02-20 21:49:05 +08:00
parent b38750f0cf
commit f8ff9eaa7f
13 changed files with 1156 additions and 234 deletions

View File

@@ -11,12 +11,13 @@
* - POST /api/orchestrator/flows/:id/duplicate - Duplicate flow
*
* Execution Control Endpoints:
* - POST /api/orchestrator/flows/:id/execute - Start flow execution
* - POST /api/orchestrator/executions/:execId/pause - Pause execution
* - POST /api/orchestrator/executions/:execId/resume - Resume execution
* - POST /api/orchestrator/executions/:execId/stop - Stop execution
* - GET /api/orchestrator/executions/:execId - Get execution state
* - GET /api/orchestrator/executions/:execId/logs - Get execution logs
* - POST /api/orchestrator/flows/:id/execute - Start flow execution
* - POST /api/orchestrator/flows/:id/execute-in-session - Start flow execution in PTY session
* - POST /api/orchestrator/executions/:execId/pause - Pause execution
* - POST /api/orchestrator/executions/:execId/resume - Resume execution
* - POST /api/orchestrator/executions/:execId/stop - Stop execution
* - GET /api/orchestrator/executions/:execId - Get execution state
* - GET /api/orchestrator/executions/:execId/logs - Get execution logs
*
* Template Management Endpoints:
* - GET /api/orchestrator/templates - List local + builtin templates
@@ -1277,6 +1278,134 @@ export async function handleOrchestratorRoutes(ctx: RouteContext): Promise<boole
return true;
}
// ==== EXECUTE FLOW IN SESSION ====
// POST /api/orchestrator/flows/:id/execute-in-session
if (pathname.match(/^\/api\/orchestrator\/flows\/[^/]+\/execute-in-session$/) && req.method === 'POST') {
const flowId = pathname.split('/').slice(-2)[0];
if (!flowId || !isValidFlowId(flowId)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Invalid flow ID format' }));
return true;
}
handlePostRequest(req, res, async (body) => {
const {
sessionConfig,
sessionKey: existingSessionKey,
variables: inputVariables,
stepTimeout,
errorStrategy = 'pause'
} = body as {
sessionConfig?: {
tool?: string;
model?: string;
preferredShell?: string;
};
sessionKey?: string;
variables?: Record<string, unknown>;
stepTimeout?: number;
errorStrategy?: 'pause' | 'skip' | 'stop';
};
try {
// Verify flow exists
const flow = await readFlowStorage(workflowDir, flowId);
if (!flow) {
return { success: false, error: 'Flow not found', status: 404 };
}
// Generate execution ID
const execId = generateExecutionId();
const now = new Date().toISOString();
// Determine session key
let sessionKey = existingSessionKey;
if (!sessionKey) {
// Create new session if not provided
// This would typically call the session manager
sessionKey = `cli-session-${Date.now()}-${randomBytes(4).toString('hex')}`;
}
// Create execution state
const nodeStates: Record<string, NodeExecutionState> = {};
for (const node of flow.nodes) {
nodeStates[node.id] = {
status: 'pending'
};
}
const execution: ExecutionState = {
id: execId,
flowId: flowId,
status: 'pending',
startedAt: now,
variables: { ...flow.variables, ...inputVariables },
nodeStates,
logs: [{
timestamp: now,
level: 'info',
message: `Execution started in session: ${sessionKey}`
}]
};
// Save execution state
await writeExecutionStorage(workflowDir, execution);
// Broadcast execution created
broadcastExecutionStateUpdate(execution);
// Broadcast EXECUTION_STARTED to WebSocket clients
if (wsBroadcast) {
wsBroadcast({
type: 'EXECUTION_STARTED',
payload: {
executionId: execId,
flowId: flowId,
sessionKey: sessionKey,
stepName: flow.name,
timestamp: now
}
});
}
// Lock the session (via WebSocket broadcast for frontend to handle)
if (wsBroadcast) {
wsBroadcast({
type: 'CLI_SESSION_LOCKED',
payload: {
sessionKey: sessionKey,
reason: `Executing workflow: ${flow.name}`,
executionId: execId,
timestamp: now
}
});
}
// TODO: Implement actual step-by-step execution in PTY session
// For now, mark as running and let the frontend handle the orchestration
execution.status = 'running';
await writeExecutionStorage(workflowDir, execution);
broadcastExecutionStateUpdate(execution);
return {
success: true,
data: {
executionId: execution.id,
flowId: execution.flowId,
sessionKey: sessionKey,
status: execution.status,
totalSteps: flow.nodes.length,
startedAt: execution.startedAt
},
message: 'Execution started in session'
};
} catch (error) {
return { success: false, error: (error as Error).message, status: 500 };
}
});
return true;
}
// ==== PAUSE EXECUTION ====
// POST /api/orchestrator/executions/:execId/pause
if (pathname.match(/^\/api\/orchestrator\/executions\/[^/]+\/pause$/) && req.method === 'POST') {