feat: implement FlowExecutor for executing flow definitions with DAG traversal and node execution

This commit is contained in:
catlog22
2026-01-30 16:59:18 +08:00
parent 0a7c1454d9
commit a5c3dff8d3
92 changed files with 23875 additions and 542 deletions

View File

@@ -0,0 +1,103 @@
/**
* Dashboard Routes Module
* Provides API endpoints for dashboard initialization and configuration
*
* Endpoints:
* - GET /api/dashboard/init - Returns initial dashboard data (projectPath, recentPaths, platform, initialData)
*/
import type { RouteContext } from './types.js';
import { getRecentPaths, normalizePathForDisplay } from '../../utils/path-resolver.js';
/**
* Dashboard initialization response structure
*/
interface DashboardInitResponse {
projectPath: string;
recentPaths: string[];
platform: string;
initialData: {
generatedAt: string;
activeSessions: unknown[];
archivedSessions: unknown[];
liteTasks: {
litePlan: unknown[];
liteFix: unknown[];
multiCliPlan: unknown[];
};
reviewData: {
dimensions: Record<string, unknown>;
};
projectOverview: null;
statistics: {
totalSessions: number;
activeSessions: number;
totalTasks: number;
completedTasks: number;
reviewFindings: number;
litePlanCount: number;
liteFixCount: number;
multiCliPlanCount: number;
};
};
}
/**
* Handle dashboard routes
* @returns true if route was handled, false otherwise
*/
export async function handleDashboardRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, req, res, initialPath } = ctx;
// GET /api/dashboard/init - Return initial dashboard data
if (pathname === '/api/dashboard/init' && req.method === 'GET') {
try {
const response: DashboardInitResponse = {
projectPath: normalizePathForDisplay(initialPath),
recentPaths: getRecentPaths(),
platform: process.platform,
initialData: {
generatedAt: new Date().toISOString(),
activeSessions: [],
archivedSessions: [],
liteTasks: {
litePlan: [],
liteFix: [],
multiCliPlan: []
},
reviewData: {
dimensions: {}
},
projectOverview: null,
statistics: {
totalSessions: 0,
activeSessions: 0,
totalTasks: 0,
completedTasks: 0,
reviewFindings: 0,
litePlanCount: 0,
liteFixCount: 0,
multiCliPlanCount: 0
}
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
data: response,
timestamp: new Date().toISOString()
}));
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
return true;
}
}
return false;
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,8 @@ import { handleLoopRoutes } from './routes/loop-routes.js';
import { handleLoopV2Routes, initializeCliToolsCache } from './routes/loop-v2-routes.js';
import { handleTestLoopRoutes } from './routes/test-loop-routes.js';
import { handleTaskRoutes } from './routes/task-routes.js';
import { handleDashboardRoutes } from './routes/dashboard-routes.js';
import { handleOrchestratorRoutes } from './routes/orchestrator-routes.js';
// Import WebSocket handling
import { handleWebSocketUpgrade, broadcastToClients, extractSessionIdFromPath } from './websocket.js';
@@ -514,6 +516,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleNavStatusRoutes(routeContext)) return;
}
// Dashboard routes (/api/dashboard/*) - Dashboard initialization
if (pathname.startsWith('/api/dashboard/')) {
if (await handleDashboardRoutes(routeContext)) return;
}
// CLI routes (/api/cli/*)
if (pathname.startsWith('/api/cli/')) {
// CLI Settings routes first (more specific path /api/cli/settings/*)
@@ -577,6 +584,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleCcwRoutes(routeContext)) return;
}
// Orchestrator routes (/api/orchestrator/*)
if (pathname.startsWith('/api/orchestrator/')) {
if (await handleOrchestratorRoutes(routeContext)) return;
}
// Loop V2 routes (/api/loops/v2/*) - must be checked before v1
if (pathname.startsWith('/api/loops/v2')) {
if (await handleLoopV2Routes(routeContext)) return;

File diff suppressed because it is too large Load Diff

View File

@@ -63,6 +63,79 @@ export interface LoopLogEntryMessage {
timestamp: string;
}
/**
* Orchestrator WebSocket message types
*/
export type OrchestratorMessageType =
| 'ORCHESTRATOR_STATE_UPDATE'
| 'ORCHESTRATOR_NODE_STARTED'
| 'ORCHESTRATOR_NODE_COMPLETED'
| 'ORCHESTRATOR_NODE_FAILED'
| 'ORCHESTRATOR_LOG';
/**
* Execution log entry for Orchestrator
*/
export interface ExecutionLog {
timestamp: string;
level: 'info' | 'warn' | 'error' | 'debug';
nodeId?: string;
message: string;
}
/**
* Orchestrator State Update - fired when execution status changes
*/
export interface OrchestratorStateUpdateMessage {
type: 'ORCHESTRATOR_STATE_UPDATE';
execId: string;
status: 'pending' | 'running' | 'paused' | 'completed' | 'failed';
currentNodeId?: string;
timestamp: string;
}
/**
* Orchestrator Node Started - fired when a node begins execution
*/
export interface OrchestratorNodeStartedMessage {
type: 'ORCHESTRATOR_NODE_STARTED';
execId: string;
nodeId: string;
timestamp: string;
}
/**
* Orchestrator Node Completed - fired when a node finishes successfully
*/
export interface OrchestratorNodeCompletedMessage {
type: 'ORCHESTRATOR_NODE_COMPLETED';
execId: string;
nodeId: string;
result?: unknown;
timestamp: string;
}
/**
* Orchestrator Node Failed - fired when a node encounters an error
*/
export interface OrchestratorNodeFailedMessage {
type: 'ORCHESTRATOR_NODE_FAILED';
execId: string;
nodeId: string;
error: string;
timestamp: string;
}
/**
* Orchestrator Log - fired for execution log entries
*/
export interface OrchestratorLogMessage {
type: 'ORCHESTRATOR_LOG';
execId: string;
log: ExecutionLog;
timestamp: string;
}
export function handleWebSocketUpgrade(req: IncomingMessage, socket: Duplex, _head: Buffer): void {
const header = req.headers['sec-websocket-key'];
const key = Array.isArray(header) ? header[0] : header;
@@ -300,3 +373,59 @@ export function broadcastLoopLog(loop_id: string, step_id: string, line: string)
timestamp: new Date().toISOString()
});
}
/**
* Union type for Orchestrator messages (without timestamp - added automatically)
*/
export type OrchestratorMessage =
| Omit<OrchestratorStateUpdateMessage, 'timestamp'>
| Omit<OrchestratorNodeStartedMessage, 'timestamp'>
| Omit<OrchestratorNodeCompletedMessage, 'timestamp'>
| Omit<OrchestratorNodeFailedMessage, 'timestamp'>
| Omit<OrchestratorLogMessage, 'timestamp'>;
/**
* Orchestrator-specific broadcast with throttling
* Throttles ORCHESTRATOR_STATE_UPDATE messages to avoid flooding clients
*/
let lastOrchestratorBroadcast = 0;
const ORCHESTRATOR_BROADCAST_THROTTLE = 1000; // 1 second
/**
* Broadcast orchestrator update with throttling
* STATE_UPDATE messages are throttled to 1 per second
* Other message types are sent immediately
*/
export function broadcastOrchestratorUpdate(message: OrchestratorMessage): void {
const now = Date.now();
// Throttle ORCHESTRATOR_STATE_UPDATE to reduce WebSocket traffic
if (message.type === 'ORCHESTRATOR_STATE_UPDATE' && now - lastOrchestratorBroadcast < ORCHESTRATOR_BROADCAST_THROTTLE) {
return;
}
if (message.type === 'ORCHESTRATOR_STATE_UPDATE') {
lastOrchestratorBroadcast = now;
}
broadcastToClients({
...message,
timestamp: new Date().toISOString()
});
}
/**
* Broadcast orchestrator log entry (no throttling)
* Used for streaming real-time execution logs to Dashboard
*/
export function broadcastOrchestratorLog(execId: string, log: Omit<ExecutionLog, 'timestamp'>): void {
broadcastToClients({
type: 'ORCHESTRATOR_LOG',
execId,
log: {
...log,
timestamp: new Date().toISOString()
},
timestamp: new Date().toISOString()
});
}