This commit is contained in:
weishu
2025-12-16 15:03:50 +08:00
commit b4654acb92
209 changed files with 29235 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
/**
* HTTP control server for daemon management
* Provides endpoints for listing sessions, stopping sessions, and daemon shutdown
*/
import fastify, { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from 'fastify-type-provider-zod';
import { logger } from '@/ui/logger';
import { Metadata } from '@/api/types';
import { TrackedSession } from './types';
import { SpawnSessionOptions, SpawnSessionResult } from '@/modules/common/registerCommonHandlers';
export function startDaemonControlServer({
getChildren,
stopSession,
spawnSession,
requestShutdown,
onHappySessionWebhook
}: {
getChildren: () => TrackedSession[];
stopSession: (sessionId: string) => boolean;
spawnSession: (options: SpawnSessionOptions) => Promise<SpawnSessionResult>;
requestShutdown: () => void;
onHappySessionWebhook: (sessionId: string, metadata: Metadata) => void;
}): Promise<{ port: number; stop: () => Promise<void> }> {
return new Promise((resolve) => {
const app = fastify({
logger: false // We use our own logger
});
// Set up Zod type provider
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
const typed = app.withTypeProvider<ZodTypeProvider>();
// Session reports itself after creation
typed.post('/session-started', {
schema: {
body: z.object({
sessionId: z.string(),
metadata: z.any() // Metadata type from API
}),
response: {
200: z.object({
status: z.literal('ok')
})
}
}
}, async (request) => {
const { sessionId, metadata } = request.body;
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
onHappySessionWebhook(sessionId, metadata);
return { status: 'ok' as const };
});
// List all tracked sessions
typed.post('/list', {
schema: {
response: {
200: z.object({
children: z.array(z.object({
startedBy: z.string(),
happySessionId: z.string(),
pid: z.number()
}))
})
}
}
}, async () => {
const children = getChildren();
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
return {
children: children
.filter(child => child.happySessionId !== undefined)
.map(child => ({
startedBy: child.startedBy,
happySessionId: child.happySessionId!,
pid: child.pid
}))
}
});
// Stop specific session
typed.post('/stop-session', {
schema: {
body: z.object({
sessionId: z.string()
}),
response: {
200: z.object({
success: z.boolean()
})
}
}
}, async (request) => {
const { sessionId } = request.body;
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
const success = stopSession(sessionId);
return { success };
});
// Spawn new session
typed.post('/spawn-session', {
schema: {
body: z.object({
directory: z.string(),
sessionId: z.string().optional()
}),
response: {
200: z.object({
success: z.boolean(),
sessionId: z.string().optional(),
approvedNewDirectoryCreation: z.boolean().optional()
}),
409: z.object({
success: z.boolean(),
requiresUserApproval: z.boolean().optional(),
actionRequired: z.string().optional(),
directory: z.string().optional()
}),
500: z.object({
success: z.boolean(),
error: z.string().optional()
})
}
}
}, async (request, reply) => {
const { directory, sessionId } = request.body;
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || 'new'}`);
const result = await spawnSession({ directory, sessionId });
switch (result.type) {
case 'success':
// Check if sessionId exists, if not return error
if (!result.sessionId) {
reply.code(500);
return {
success: false,
error: 'Failed to spawn session: no session ID returned'
};
}
return {
success: true,
sessionId: result.sessionId,
approvedNewDirectoryCreation: true
};
case 'requestToApproveDirectoryCreation':
reply.code(409); // Conflict - user input needed
return {
success: false,
requiresUserApproval: true,
actionRequired: 'CREATE_DIRECTORY',
directory: result.directory
};
case 'error':
reply.code(500);
return {
success: false,
error: result.errorMessage
};
}
});
// Stop daemon
typed.post('/stop', {
schema: {
response: {
200: z.object({
status: z.string()
})
}
}
}, async () => {
logger.debug('[CONTROL SERVER] Stop daemon request received');
// Give time for response to arrive
setTimeout(() => {
logger.debug('[CONTROL SERVER] Triggering daemon shutdown');
requestShutdown();
}, 50);
return { status: 'stopping' };
});
app.listen({ port: 0, host: '127.0.0.1' }, (err, address) => {
if (err) {
logger.debug('[CONTROL SERVER] Failed to start:', err);
throw err;
}
const port = parseInt(address.split(':').pop()!);
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
resolve({
port,
stop: async () => {
logger.debug('[CONTROL SERVER] Stopping server');
await app.close();
logger.debug('[CONTROL SERVER] Server stopped');
}
});
});
});
}