feat(queue): implement queue scheduler service and API routes

- Added QueueSchedulerService to manage task queue lifecycle, including state machine, dependency resolution, and session management.
- Implemented HTTP API endpoints for queue scheduling:
  - POST /api/queue/execute: Submit items to the scheduler.
  - GET /api/queue/scheduler/state: Retrieve full scheduler state.
  - POST /api/queue/scheduler/start: Start scheduling loop with items.
  - POST /api/queue/scheduler/pause: Pause scheduling.
  - POST /api/queue/scheduler/stop: Graceful stop of the scheduler.
  - POST /api/queue/scheduler/config: Update scheduler configuration.
- Introduced types for queue items, scheduler state, and WebSocket messages to ensure type safety and compatibility with the backend.
- Added static model lists for LiteLLM as a fallback for available models.
This commit is contained in:
catlog22
2026-02-27 20:53:46 +08:00
parent 5b54f38aa3
commit 75173312c1
47 changed files with 3813 additions and 307 deletions

View File

@@ -23,6 +23,7 @@ import {
getEnrichedConversation,
getHistoryWithNativeInfo
} from '../../tools/cli-executor.js';
import { listAllNativeSessions } from '../../tools/native-session-discovery.js';
import { SmartContentFormatter } from '../../tools/cli-output-converter.js';
import { generateSmartContext, formatSmartContext } from '../../tools/smart-context.js';
import {
@@ -851,6 +852,35 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// API: List Native CLI Sessions
if (pathname === '/api/cli/native-sessions' && req.method === 'GET') {
const projectPath = url.searchParams.get('path') || null;
const limit = parseInt(url.searchParams.get('limit') || '100', 10);
try {
const sessions = listAllNativeSessions({
workingDir: projectPath || undefined,
limit
});
// Group sessions by tool
const byTool: Record<string, typeof sessions> = {};
for (const session of sessions) {
if (!byTool[session.tool]) {
byTool[session.tool] = [];
}
byTool[session.tool].push(session);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ sessions, byTool }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// API: Execute CLI Tool
if (pathname === '/api/cli/execute' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {

View File

@@ -1,31 +1,56 @@
/**
* Provider Reference Routes Module
* Handles read-only provider model reference API endpoints
*
* Model source priority:
* 1. User configuration (cli-tools.json availableModels)
* 2. LiteLLM static model lists (fallback)
*/
import type { RouteContext } from './types.js';
import {
PROVIDER_MODELS,
getAllProviders,
getProviderModels
} from '../../config/provider-models.js';
import { loadClaudeCliTools, type ClaudeCliToolsConfig } from '../../tools/claude-cli-tools.js';
import { getFallbackModels, hasFallbackModels, type ModelInfo } from '../../config/litellm-static-models.js';
/**
* Get models for a tool, using config or fallback
*/
function getToolModels(toolId: string, configModels?: string[]): ModelInfo[] {
// Priority 1: User config
if (configModels && configModels.length > 0) {
return configModels.map(id => ({ id, name: id }));
}
// Priority 2: LiteLLM static fallback
return getFallbackModels(toolId);
}
/**
* Handle Provider Reference routes
* @returns true if route was handled, false otherwise
*/
export async function handleProviderRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, req, res } = ctx;
const { pathname, req, res, initialPath } = ctx;
// ========== GET ALL PROVIDERS ==========
// GET /api/providers
if (pathname === '/api/providers' && req.method === 'GET') {
try {
const providers = getAllProviders().map(id => ({
id,
name: PROVIDER_MODELS[id].name,
modelCount: PROVIDER_MODELS[id].models.length
}));
const config = loadClaudeCliTools(initialPath);
const providers = Object.entries(config.tools)
.filter(([, tool]) => tool.enabled)
.map(([id, tool]) => {
// Use config models or fallback count
const models = getToolModels(id, tool.availableModels);
return {
id,
name: id.charAt(0).toUpperCase() + id.slice(1),
modelCount: models.length,
primaryModel: tool.primaryModel ?? '',
secondaryModel: tool.secondaryModel ?? '',
type: tool.type ?? 'builtin',
hasCustomModels: !!(tool.availableModels && tool.availableModels.length > 0)
};
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, providers }));
@@ -46,9 +71,10 @@ export async function handleProviderRoutes(ctx: RouteContext): Promise<boolean>
const provider = decodeURIComponent(providerMatch[1]);
try {
const models = getProviderModels(provider);
const config = loadClaudeCliTools(initialPath);
const tool = config.tools[provider];
if (models.length === 0) {
if (!tool || !tool.enabled) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
@@ -57,12 +83,19 @@ export async function handleProviderRoutes(ctx: RouteContext): Promise<boolean>
return true;
}
// Get models from config or fallback
const models = getToolModels(provider, tool.availableModels);
const usingFallback = !tool.availableModels || tool.availableModels.length === 0;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
provider,
providerName: PROVIDER_MODELS[provider].name,
models
providerName: provider.charAt(0).toUpperCase() + provider.slice(1),
models,
primaryModel: tool.primaryModel ?? '',
secondaryModel: tool.secondaryModel ?? '',
source: usingFallback ? 'fallback' : 'config'
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });

View File

@@ -0,0 +1,157 @@
/**
* Queue Scheduler Routes Module
*
* HTTP API endpoints for the Queue Scheduler Service.
* Delegates all business logic to QueueSchedulerService.
*
* API Endpoints:
* - POST /api/queue/execute - Submit items to the scheduler and start
* - GET /api/queue/scheduler/state - Get full scheduler state
* - POST /api/queue/scheduler/start - Start scheduling loop with items
* - POST /api/queue/scheduler/pause - Pause scheduling
* - POST /api/queue/scheduler/stop - Graceful stop
* - POST /api/queue/scheduler/config - Update scheduler configuration
*/
import type { RouteContext } from './types.js';
import type { QueueSchedulerService } from '../services/queue-scheduler-service.js';
import type { QueueItem, QueueSchedulerConfig } from '../../types/queue-types.js';
/**
* Handle queue scheduler routes
* @returns true if route was handled, false otherwise
*/
export async function handleQueueSchedulerRoutes(
ctx: RouteContext,
schedulerService: QueueSchedulerService,
): Promise<boolean> {
const { pathname, req, res, handlePostRequest } = ctx;
// POST /api/queue/execute - Submit items and start the scheduler
if (pathname === '/api/queue/execute' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { items } = body as { items?: QueueItem[] };
if (!items || !Array.isArray(items) || items.length === 0) {
return { error: 'items array is required and must not be empty', status: 400 };
}
try {
const state = schedulerService.getState();
// If idle, start with items; otherwise add items to running scheduler
if (state.status === 'idle') {
schedulerService.start(items);
} else if (state.status === 'running' || state.status === 'paused') {
for (const item of items) {
schedulerService.addItem(item);
}
} else {
return {
error: `Cannot add items when scheduler is in '${state.status}' state`,
status: 409,
};
}
return {
success: true,
state: schedulerService.getState(),
};
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// GET /api/queue/scheduler/state - Return full scheduler state
if (pathname === '/api/queue/scheduler/state' && req.method === 'GET') {
try {
const state = schedulerService.getState();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, state }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// POST /api/queue/scheduler/start - Start scheduling loop with items
if (pathname === '/api/queue/scheduler/start' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { items } = body as { items?: QueueItem[] };
if (!items || !Array.isArray(items) || items.length === 0) {
return { error: 'items array is required and must not be empty', status: 400 };
}
try {
schedulerService.start(items);
return {
success: true,
state: schedulerService.getState(),
};
} catch (err) {
return { error: (err as Error).message, status: 409 };
}
});
return true;
}
// POST /api/queue/scheduler/pause - Pause scheduling
if (pathname === '/api/queue/scheduler/pause' && req.method === 'POST') {
handlePostRequest(req, res, async () => {
try {
schedulerService.pause();
return {
success: true,
state: schedulerService.getState(),
};
} catch (err) {
return { error: (err as Error).message, status: 409 };
}
});
return true;
}
// POST /api/queue/scheduler/stop - Graceful stop
if (pathname === '/api/queue/scheduler/stop' && req.method === 'POST') {
handlePostRequest(req, res, async () => {
try {
await schedulerService.stop();
return {
success: true,
state: schedulerService.getState(),
};
} catch (err) {
return { error: (err as Error).message, status: 409 };
}
});
return true;
}
// POST /api/queue/scheduler/config - Update scheduler configuration
if (pathname === '/api/queue/scheduler/config' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const config = body as Partial<QueueSchedulerConfig>;
if (!config || typeof config !== 'object') {
return { error: 'Configuration object is required', status: 400 };
}
try {
schedulerService.updateConfig(config);
return {
success: true,
config: schedulerService.getState().config,
};
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
return false;
}