mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-07 01:02:55 +08:00
@@ -0,0 +1,2 @@
|
||||
export { SealosClient, createSealosClient } from './sealos';
|
||||
export { SandboxClient, createSandboxClient } from './sandbox';
|
||||
@@ -0,0 +1,100 @@
|
||||
import axios, { type AxiosInstance, type AxiosError } from 'axios';
|
||||
import {
|
||||
ExecRequestSchema,
|
||||
ExecResponseSchema,
|
||||
HealthResponseSchema,
|
||||
type ExecRequest,
|
||||
type ExecResponse,
|
||||
type HealthResponse
|
||||
} from '../schemas';
|
||||
|
||||
const DEFAULT_CWD = '/app/sandbox';
|
||||
|
||||
/**
|
||||
* Sandbox Client
|
||||
* Communicates with the sandbox server running inside container
|
||||
*/
|
||||
export class SandboxClient {
|
||||
private readonly client: AxiosInstance;
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
timeout: 60000, // 60s timeout for long-running commands
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError<{ error?: string }>) => {
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
return Promise.reject(new Error('Sandbox server is not reachable'));
|
||||
}
|
||||
|
||||
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
||||
return Promise.reject(new Error('Request timeout'));
|
||||
}
|
||||
|
||||
const status = error.response?.status;
|
||||
if (status === 404) {
|
||||
return Promise.reject(new Error('Endpoint not found'));
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
message: error.response?.data?.error || error.message || 'Request failed',
|
||||
data: error.response?.data
|
||||
};
|
||||
return Promise.reject(responseData);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sandbox server is healthy
|
||||
*/
|
||||
async health(): Promise<HealthResponse> {
|
||||
const response = await this.client.get('/health');
|
||||
return HealthResponseSchema.parse(response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sandbox is healthy (boolean)
|
||||
*/
|
||||
async isHealthy(): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.health();
|
||||
return result.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell command in the sandbox
|
||||
*/
|
||||
async exec(params: ExecRequest): Promise<ExecResponse> {
|
||||
const validated = ExecRequestSchema.parse(params);
|
||||
|
||||
const response = await this.client.post('/exec', {
|
||||
command: validated.command,
|
||||
cwd: validated.cwd || DEFAULT_CWD
|
||||
});
|
||||
|
||||
return ExecResponseSchema.parse(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SandboxClient instance
|
||||
*/
|
||||
export function createSandboxClient(baseUrl: string): SandboxClient {
|
||||
return new SandboxClient(baseUrl);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import axios, { type AxiosInstance, type AxiosError } from 'axios';
|
||||
import {
|
||||
CreateContainerSchema,
|
||||
SealosContainerResponseSchema,
|
||||
type CreateContainerInput,
|
||||
type ContainerInfo,
|
||||
type ContainerStatus
|
||||
} from '../schemas';
|
||||
import { env, containerConfig } from '../env';
|
||||
|
||||
/**
|
||||
* Sealos API Client
|
||||
* Handles container lifecycle management through Sealos API
|
||||
*/
|
||||
export class SealosClient {
|
||||
private readonly client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: env.SEALOS_BASE_URL,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${env.SEALOS_KC}`
|
||||
}
|
||||
});
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => {
|
||||
// Handle empty response for void operations
|
||||
if (response.data === undefined || response.data === null) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check API-level error code
|
||||
if (response.data.code === 404) {
|
||||
return Promise.reject({ status: 404, message: 'Resource not found' });
|
||||
}
|
||||
|
||||
if (response.data.code && response.data.code !== 200) {
|
||||
return Promise.reject(response.data.error || response.data.message || 'API error');
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError<{ error?: string; message?: string }>) => {
|
||||
const status = error.response?.status;
|
||||
const errorData = error.response?.data;
|
||||
console.log(errorData, 2222);
|
||||
if (status === 401 || status === 403) {
|
||||
return Promise.reject(new Error('Authentication failed'));
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
return Promise.reject({
|
||||
status: 404,
|
||||
message: errorData?.message || 'Resource not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (status && status >= 500) {
|
||||
return Promise.reject(new Error(errorData?.message || 'Server error'));
|
||||
}
|
||||
|
||||
const message = errorData?.error || errorData?.message || error.message || 'Request failed';
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new container with fixed configuration from environment variables
|
||||
*/
|
||||
async createContainer(params: CreateContainerInput): Promise<void> {
|
||||
const validated = CreateContainerSchema.parse(params);
|
||||
|
||||
// Parse entrypoint configuration
|
||||
let launchCommand: { command?: string; args?: string } | undefined;
|
||||
if (containerConfig.entrypoint) {
|
||||
try {
|
||||
const parsed = JSON.parse(containerConfig.entrypoint);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
launchCommand = {
|
||||
command: parsed[0],
|
||||
args: parsed.slice(1).join(' ')
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// If not JSON, treat as direct command
|
||||
launchCommand = { command: containerConfig.entrypoint };
|
||||
}
|
||||
}
|
||||
|
||||
await this.client
|
||||
.post('/api/v1/app', {
|
||||
name: validated.name,
|
||||
image: {
|
||||
imageName: containerConfig.image
|
||||
},
|
||||
resource: {
|
||||
cpu: containerConfig.cpu,
|
||||
memory: containerConfig.memory,
|
||||
replicas: 1
|
||||
},
|
||||
ports: [
|
||||
{
|
||||
number: containerConfig.port,
|
||||
exposesPublicDomain: containerConfig.exposesPublicDomain
|
||||
}
|
||||
],
|
||||
launchCommand
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code === 409) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container information by name
|
||||
*/
|
||||
async getContainer(name: string): Promise<ContainerInfo | null> {
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/app/${encodeURIComponent(name)}`);
|
||||
const data = SealosContainerResponseSchema.parse(response.data.data);
|
||||
|
||||
return {
|
||||
name: data.name,
|
||||
image: data.image,
|
||||
status: this.mapContainerStatus(data.status),
|
||||
server: data.ports[0],
|
||||
createdAt: data.createTime
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === 'object' && 'status' in err && err.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a running container
|
||||
*/
|
||||
async pauseContainer(name: string): Promise<void> {
|
||||
try {
|
||||
await this.client.post(`/api/v1/app/${encodeURIComponent(name)}/pause`);
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === 'object' && 'status' in err && err.status === 404) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume/start a paused container
|
||||
*/
|
||||
async resumeContainer(name: string): Promise<void> {
|
||||
try {
|
||||
await this.client.post(`/api/v1/app/${encodeURIComponent(name)}/start`);
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === 'object' && 'status' in err && err.status === 404) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a container
|
||||
*/
|
||||
async deleteContainer(name: string): Promise<void> {
|
||||
try {
|
||||
await this.client.delete(`/api/v1/app/${encodeURIComponent(name)}`);
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === 'object' && 'status' in err && err.status === 404) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Sealos API status to internal status
|
||||
*/
|
||||
private mapContainerStatus(status: {
|
||||
replicas: number;
|
||||
availableReplicas: number;
|
||||
isPause: boolean;
|
||||
}): ContainerStatus {
|
||||
if (status.isPause) {
|
||||
return {
|
||||
state: 'Paused',
|
||||
replicas: status.replicas,
|
||||
availableReplicas: status.availableReplicas
|
||||
};
|
||||
}
|
||||
if (status.availableReplicas > 0) {
|
||||
return {
|
||||
state: 'Running',
|
||||
replicas: status.replicas,
|
||||
availableReplicas: status.availableReplicas
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: 'Creating',
|
||||
replicas: status.replicas,
|
||||
availableReplicas: status.availableReplicas
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SealosClient instance
|
||||
*/
|
||||
export function createSealosClient(): SealosClient {
|
||||
return new SealosClient();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createEnv } from '@t3-oss/env-core';
|
||||
import { z } from 'zod';
|
||||
|
||||
const isTest = process.env.NODE_ENV === 'test';
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
PORT: z.coerce.number().default(3000),
|
||||
TOKEN: isTest ? z.string().default('test-token') : z.string().min(1),
|
||||
SEALOS_BASE_URL: z.string().url().default('https://applaunchpad.hzh.sealos.run'),
|
||||
SEALOS_KC: isTest ? z.string().default('') : z.string().min(1),
|
||||
// Container configuration
|
||||
CONTAINER_IMAGE: isTest ? z.string().default('test-image') : z.string(),
|
||||
CONTAINER_PORT: z.coerce.number().default(8080),
|
||||
CONTAINER_CPU: z.coerce.number().default(0.5),
|
||||
CONTAINER_MEMORY: z.coerce.number().default(1),
|
||||
CONTAINER_ENTRYPOINT: z.string().optional(),
|
||||
CONTAINER_EXPOSES_PUBLIC_DOMAIN: z
|
||||
.string()
|
||||
.default('false')
|
||||
.transform((v) => v === 'true')
|
||||
},
|
||||
runtimeEnv: process.env
|
||||
});
|
||||
|
||||
// Container configuration for SealosClient
|
||||
export const containerConfig = {
|
||||
image: env.CONTAINER_IMAGE,
|
||||
port: env.CONTAINER_PORT,
|
||||
cpu: env.CONTAINER_CPU,
|
||||
memory: env.CONTAINER_MEMORY,
|
||||
entrypoint: env.CONTAINER_ENTRYPOINT || '',
|
||||
exposesPublicDomain: env.CONTAINER_EXPOSES_PUBLIC_DOMAIN
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi';
|
||||
import { apiReference } from '@scalar/hono-api-reference';
|
||||
import { env } from './env';
|
||||
import { authMiddleware, errorHandler, loggerMiddleware } from './middleware';
|
||||
import { createSealosClient } from './clients';
|
||||
import { createContainerRoutes, createSandboxRoutes } from './routes';
|
||||
import { logger } from './utils';
|
||||
|
||||
// Create Hono app with OpenAPI support
|
||||
const app = new OpenAPIHono();
|
||||
|
||||
// Global error handler
|
||||
app.onError(errorHandler);
|
||||
|
||||
// Global logger middleware
|
||||
app.use('*', loggerMiddleware);
|
||||
|
||||
// Create Sealos client
|
||||
const sealosClient = createSealosClient();
|
||||
|
||||
// ==================== Public Routes ====================
|
||||
|
||||
// Health check endpoint (no auth required)
|
||||
app.get('/health', (c) => {
|
||||
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// OpenAPI JSON document
|
||||
app.doc('/openapi', {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Sandbox Server API',
|
||||
version: '1.0.0',
|
||||
description: 'API for managing sandbox containers via Sealos'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: `http://localhost:${env.PORT}`,
|
||||
description: 'Local development server'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Scalar API Reference UI
|
||||
app.get(
|
||||
'/openapi/ui',
|
||||
apiReference({
|
||||
url: '/openapi',
|
||||
theme: 'default'
|
||||
})
|
||||
);
|
||||
|
||||
// ==================== Protected Routes ====================
|
||||
|
||||
// Create v1 router with authentication
|
||||
const v1 = new OpenAPIHono();
|
||||
v1.use('*', authMiddleware);
|
||||
|
||||
// Mount container routes
|
||||
v1.route('/', createContainerRoutes(sealosClient));
|
||||
|
||||
// Mount sandbox routes
|
||||
v1.route('/', createSandboxRoutes(sealosClient));
|
||||
|
||||
// Mount v1 router
|
||||
app.route('/v1', v1);
|
||||
|
||||
// ==================== Start Server ====================
|
||||
|
||||
logger.info('Server', `Starting on port ${env.PORT}`);
|
||||
logger.info('Server', `API Documentation: http://localhost:${env.PORT}/openapi/ui`);
|
||||
|
||||
export default {
|
||||
port: env.PORT,
|
||||
fetch: app.fetch
|
||||
};
|
||||
|
||||
// Export app for testing
|
||||
export { app };
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { env } from '../env';
|
||||
|
||||
/**
|
||||
* Bearer token authentication middleware
|
||||
* Validates Authorization header: Bearer <token>
|
||||
*/
|
||||
export const authMiddleware = createMiddleware(async (c, next) => {
|
||||
const authorization = c.req.header('Authorization');
|
||||
|
||||
if (!authorization) {
|
||||
throw new HTTPException(401, { message: 'Authorization header is required' });
|
||||
}
|
||||
|
||||
if (!authorization.startsWith('Bearer ')) {
|
||||
throw new HTTPException(401, {
|
||||
message: 'Invalid authorization format. Expected: Bearer <token>'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authorization.slice(7);
|
||||
|
||||
if (token !== env.TOKEN) {
|
||||
throw new HTTPException(401, { message: 'Invalid token' });
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ErrorHandler } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { ZodError } from 'zod';
|
||||
import { setLoggerError } from './logger';
|
||||
|
||||
/**
|
||||
* Global error handler
|
||||
* Catches all errors and returns consistent JSON response
|
||||
*/
|
||||
export const errorHandler: ErrorHandler = (err, c) => {
|
||||
// Handle HTTP exceptions
|
||||
if (err instanceof HTTPException) {
|
||||
setLoggerError(c.req.raw, err.message);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
message: err.message
|
||||
},
|
||||
err.status
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (err instanceof ZodError) {
|
||||
const message = 'Validation error';
|
||||
setLoggerError(c.req.raw, message);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
message,
|
||||
errors: err.issues
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Handle generic errors
|
||||
const message = err instanceof Error ? err.message : 'Internal Server Error';
|
||||
setLoggerError(c.req.raw, message);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
message
|
||||
},
|
||||
500
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { authMiddleware } from './auth';
|
||||
export { errorHandler } from './error';
|
||||
export { loggerMiddleware, setLoggerError } from './logger';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { logger } from '../utils';
|
||||
|
||||
// Store for request timing and error info
|
||||
const requestStore = new WeakMap<Request, { startTime: number; errorMessage?: string }>();
|
||||
|
||||
/**
|
||||
* HTTP Logger middleware
|
||||
* Logs request start and completion with timing information
|
||||
*/
|
||||
export const loggerMiddleware = createMiddleware(async (c, next) => {
|
||||
const startTime = Date.now();
|
||||
const method = c.req.method;
|
||||
const path = c.req.path;
|
||||
|
||||
// Store timing info
|
||||
requestStore.set(c.req.raw, { startTime });
|
||||
|
||||
// Log request start
|
||||
logger.httpRequest(method, path);
|
||||
|
||||
await next();
|
||||
|
||||
// Log response
|
||||
const duration = Date.now() - startTime;
|
||||
const status = c.res.status;
|
||||
const stored = requestStore.get(c.req.raw);
|
||||
const errorMessage = status >= 400 ? stored?.errorMessage : undefined;
|
||||
|
||||
logger.httpResponse(method, path, status, duration, errorMessage);
|
||||
|
||||
// Cleanup
|
||||
requestStore.delete(c.req.raw);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set error message for logging (called from errorHandler)
|
||||
*/
|
||||
export function setLoggerError(req: Request, message: string): void {
|
||||
const stored = requestStore.get(req);
|
||||
if (stored) {
|
||||
stored.errorMessage = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
|
||||
import {
|
||||
CreateContainerSchema,
|
||||
ContainerInfoResponseSchema,
|
||||
SuccessResponseSchema,
|
||||
ErrorResponseSchema
|
||||
} from '../schemas';
|
||||
import type { SealosClient } from '../clients';
|
||||
|
||||
// ==================== Route Definitions ====================
|
||||
|
||||
const createContainerRoute = createRoute({
|
||||
method: 'post',
|
||||
path: '/containers',
|
||||
tags: ['Container'],
|
||||
summary: 'Create a new container',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CreateContainerSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SuccessResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container created successfully'
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Bad request'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getContainerRoute = createRoute({
|
||||
method: 'get',
|
||||
path: '/containers/{name}',
|
||||
tags: ['Container'],
|
||||
summary: 'Get container information',
|
||||
request: {
|
||||
params: z.object({
|
||||
name: z.string().openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ContainerInfoResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container information'
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container not found'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const pauseContainerRoute = createRoute({
|
||||
method: 'post',
|
||||
path: '/containers/{name}/pause',
|
||||
tags: ['Container'],
|
||||
summary: 'Pause a running container',
|
||||
request: {
|
||||
params: z.object({
|
||||
name: z.string().openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SuccessResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container paused successfully'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const startContainerRoute = createRoute({
|
||||
method: 'post',
|
||||
path: '/containers/{name}/start',
|
||||
tags: ['Container'],
|
||||
summary: 'Start a paused container',
|
||||
request: {
|
||||
params: z.object({
|
||||
name: z.string().openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SuccessResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container started successfully'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deleteContainerRoute = createRoute({
|
||||
method: 'delete',
|
||||
path: '/containers/{name}',
|
||||
tags: ['Container'],
|
||||
summary: 'Delete a container',
|
||||
request: {
|
||||
params: z.object({
|
||||
name: z.string().openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SuccessResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container deleted successfully'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Controller Factory ====================
|
||||
|
||||
export const createContainerRoutes = (sealosClient: SealosClient) => {
|
||||
const app = new OpenAPIHono();
|
||||
|
||||
// POST /containers - Create container
|
||||
app.openapi(createContainerRoute, async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
await sealosClient.createContainer(body);
|
||||
return c.json({ success: true as const }, 200);
|
||||
});
|
||||
|
||||
// GET /containers/:name - Get container info
|
||||
app.openapi(getContainerRoute, async (c) => {
|
||||
const { name } = c.req.valid('param');
|
||||
const container = await sealosClient.getContainer(name);
|
||||
|
||||
if (!container) {
|
||||
return c.json({ success: false as const, message: 'Container not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json({ success: true as const, data: container }, 200);
|
||||
});
|
||||
|
||||
// POST /containers/:name/pause - Pause container
|
||||
app.openapi(pauseContainerRoute, async (c) => {
|
||||
const { name } = c.req.valid('param');
|
||||
await sealosClient.pauseContainer(name);
|
||||
return c.json({ success: true as const }, 200);
|
||||
});
|
||||
|
||||
// POST /containers/:name/start - Start container
|
||||
app.openapi(startContainerRoute, async (c) => {
|
||||
const { name } = c.req.valid('param');
|
||||
await sealosClient.resumeContainer(name);
|
||||
return c.json({ success: true as const }, 200);
|
||||
});
|
||||
|
||||
// DELETE /containers/:name - Delete container
|
||||
app.openapi(deleteContainerRoute, async (c) => {
|
||||
const { name } = c.req.valid('param');
|
||||
await sealosClient.deleteContainer(name);
|
||||
return c.json({ success: true as const }, 200);
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createContainerRoutes } from './container.route';
|
||||
export { createSandboxRoutes } from './sandbox.route';
|
||||
@@ -0,0 +1,151 @@
|
||||
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
|
||||
import {
|
||||
ExecRequestSchema,
|
||||
ExecResultResponseSchema,
|
||||
HealthCheckResponseSchema,
|
||||
ErrorResponseSchema
|
||||
} from '../schemas';
|
||||
import { createSandboxClient, type SealosClient } from '../clients';
|
||||
|
||||
// ==================== Route Definitions ====================
|
||||
|
||||
const execRoute = createRoute({
|
||||
method: 'post',
|
||||
path: '/sandbox/{name}/exec',
|
||||
tags: ['Sandbox'],
|
||||
summary: 'Execute a command in the sandbox',
|
||||
request: {
|
||||
params: z.object({
|
||||
name: z.string().openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ExecRequestSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ExecResultResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Command executed successfully'
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Bad request'
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container not found'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const healthRoute = createRoute({
|
||||
method: 'get',
|
||||
path: '/sandbox/{name}/health',
|
||||
tags: ['Sandbox'],
|
||||
summary: 'Check sandbox health',
|
||||
request: {
|
||||
params: z.object({
|
||||
name: z.string().openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: HealthCheckResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Health check result'
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
},
|
||||
description: 'Container not found'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Controller Factory ====================
|
||||
|
||||
/**
|
||||
* Factory function to create sandbox routes
|
||||
* @param sealosClient - Sealos client to get container info for sandbox URL
|
||||
*/
|
||||
export const createSandboxRoutes = (sealosClient: SealosClient) => {
|
||||
const app = new OpenAPIHono();
|
||||
|
||||
/**
|
||||
* Get sandbox client by container name
|
||||
* Retrieves container info to get the sandbox server URL
|
||||
*/
|
||||
const getSandboxClient = async (name: string) => {
|
||||
const container = await sealosClient.getContainer(name);
|
||||
if (!container || !container.server) {
|
||||
throw new Error('Container not found or has no server info');
|
||||
}
|
||||
|
||||
// Build sandbox URL from container server info
|
||||
let baseUrl: string;
|
||||
if (container.server.publicDomain && container.server.domain) {
|
||||
baseUrl = `https://${container.server.publicDomain}.${container.server.domain}`;
|
||||
} else {
|
||||
baseUrl = `http://${container.server.serviceName}:${container.server.number}`;
|
||||
}
|
||||
|
||||
return createSandboxClient(baseUrl);
|
||||
};
|
||||
|
||||
// POST /sandbox/:name/exec - Execute command
|
||||
app.openapi(execRoute, async (c) => {
|
||||
const { name } = c.req.valid('param');
|
||||
const body = c.req.valid('json');
|
||||
|
||||
try {
|
||||
const sandboxClient = await getSandboxClient(name);
|
||||
const result = await sandboxClient.exec(body);
|
||||
return c.json({ success: true as const, data: result }, 200);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('not found')) {
|
||||
return c.json({ success: false as const, message: 'Container not found' }, 404);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// GET /sandbox/:name/health - Health check
|
||||
app.openapi(healthRoute, async (c) => {
|
||||
const { name } = c.req.valid('param');
|
||||
|
||||
try {
|
||||
const sandboxClient = await getSandboxClient(name);
|
||||
const healthy = await sandboxClient.isHealthy();
|
||||
return c.json({ success: true as const, healthy }, 200);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('not found')) {
|
||||
return c.json({ success: false as const, message: 'Container not found' }, 404);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { z } from '@hono/zod-openapi';
|
||||
export declare const SuccessResponseSchema: z.ZodObject<
|
||||
{
|
||||
success: z.ZodLiteral<true>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type SuccessResponse = z.infer<typeof SuccessResponseSchema>;
|
||||
export declare const ErrorResponseSchema: z.ZodObject<
|
||||
{
|
||||
success: z.ZodLiteral<false>;
|
||||
message: z.ZodString;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
||||
export declare const NameParamSchema: z.ZodObject<
|
||||
{
|
||||
name: z.ZodString;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type NameParam = z.infer<typeof NameParamSchema>;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from '@hono/zod-openapi';
|
||||
|
||||
// Common response wrapper
|
||||
export const SuccessResponseSchema = z.object({
|
||||
success: z.literal(true)
|
||||
});
|
||||
export type SuccessResponse = z.infer<typeof SuccessResponseSchema>;
|
||||
|
||||
export const ErrorResponseSchema = z.object({
|
||||
success: z.literal(false),
|
||||
message: z.string()
|
||||
});
|
||||
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
||||
|
||||
// Path parameter for container/sandbox name
|
||||
export const NameParamSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.openapi({ param: { name: 'name', in: 'path' }, example: 'my-container' })
|
||||
});
|
||||
export type NameParam = z.infer<typeof NameParamSchema>;
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { z } from '@hono/zod-openapi';
|
||||
export declare const CreateContainerSchema: z.ZodObject<
|
||||
{
|
||||
name: z.ZodString;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type CreateContainerInput = z.infer<typeof CreateContainerSchema>;
|
||||
export declare const ContainerStatusSchema: z.ZodObject<
|
||||
{
|
||||
state: z.ZodEnum<{
|
||||
Running: 'Running';
|
||||
Creating: 'Creating';
|
||||
Paused: 'Paused';
|
||||
Error: 'Error';
|
||||
Unknown: 'Unknown';
|
||||
}>;
|
||||
replicas: z.ZodOptional<z.ZodNumber>;
|
||||
availableReplicas: z.ZodOptional<z.ZodNumber>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ContainerStatus = z.infer<typeof ContainerStatusSchema>;
|
||||
export declare const ContainerServerSchema: z.ZodObject<
|
||||
{
|
||||
serviceName: z.ZodString;
|
||||
number: z.ZodNumber;
|
||||
publicDomain: z.ZodOptional<z.ZodString>;
|
||||
domain: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ContainerServer = z.infer<typeof ContainerServerSchema>;
|
||||
export declare const ContainerInfoSchema: z.ZodObject<
|
||||
{
|
||||
name: z.ZodString;
|
||||
image: z.ZodObject<
|
||||
{
|
||||
imageName: z.ZodString;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
status: z.ZodObject<
|
||||
{
|
||||
state: z.ZodEnum<{
|
||||
Running: 'Running';
|
||||
Creating: 'Creating';
|
||||
Paused: 'Paused';
|
||||
Error: 'Error';
|
||||
Unknown: 'Unknown';
|
||||
}>;
|
||||
replicas: z.ZodOptional<z.ZodNumber>;
|
||||
availableReplicas: z.ZodOptional<z.ZodNumber>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
server: z.ZodOptional<
|
||||
z.ZodObject<
|
||||
{
|
||||
serviceName: z.ZodString;
|
||||
number: z.ZodNumber;
|
||||
publicDomain: z.ZodOptional<z.ZodString>;
|
||||
domain: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>
|
||||
>;
|
||||
createdAt: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ContainerInfo = z.infer<typeof ContainerInfoSchema>;
|
||||
export declare const ContainerInfoResponseSchema: z.ZodObject<
|
||||
{
|
||||
success: z.ZodLiteral<true>;
|
||||
data: z.ZodObject<
|
||||
{
|
||||
name: z.ZodString;
|
||||
image: z.ZodObject<
|
||||
{
|
||||
imageName: z.ZodString;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
status: z.ZodObject<
|
||||
{
|
||||
state: z.ZodEnum<{
|
||||
Running: 'Running';
|
||||
Creating: 'Creating';
|
||||
Paused: 'Paused';
|
||||
Error: 'Error';
|
||||
Unknown: 'Unknown';
|
||||
}>;
|
||||
replicas: z.ZodOptional<z.ZodNumber>;
|
||||
availableReplicas: z.ZodOptional<z.ZodNumber>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
server: z.ZodOptional<
|
||||
z.ZodObject<
|
||||
{
|
||||
serviceName: z.ZodString;
|
||||
number: z.ZodNumber;
|
||||
publicDomain: z.ZodOptional<z.ZodString>;
|
||||
domain: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>
|
||||
>;
|
||||
createdAt: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ContainerInfoResponse = z.infer<typeof ContainerInfoResponseSchema>;
|
||||
export declare const SealosContainerResponseSchema: z.ZodObject<
|
||||
{
|
||||
name: z.ZodString;
|
||||
image: z.ZodObject<
|
||||
{
|
||||
imageName: z.ZodString;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
createTime: z.ZodOptional<z.ZodString>;
|
||||
status: z.ZodObject<
|
||||
{
|
||||
replicas: z.ZodCoercedNumber<unknown>;
|
||||
availableReplicas: z.ZodCoercedNumber<unknown>;
|
||||
isPause: z.ZodCoercedBoolean<unknown>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
ports: z.ZodArray<
|
||||
z.ZodObject<
|
||||
{
|
||||
serviceName: z.ZodString;
|
||||
number: z.ZodCoercedNumber<unknown>;
|
||||
publicDomain: z.ZodOptional<z.ZodString>;
|
||||
domain: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>
|
||||
>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type SealosContainerResponse = z.infer<typeof SealosContainerResponseSchema>;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { z } from '@hono/zod-openapi';
|
||||
|
||||
// ==================== Request Schemas ====================
|
||||
|
||||
export const CreateContainerSchema = z.object({
|
||||
name: z.string().min(1).openapi({ example: 'my-container' })
|
||||
});
|
||||
export type CreateContainerInput = z.infer<typeof CreateContainerSchema>;
|
||||
|
||||
// ==================== Response Schemas ====================
|
||||
|
||||
export const ContainerStatusSchema = z.object({
|
||||
state: z.enum(['Running', 'Creating', 'Paused', 'Error', 'Unknown']),
|
||||
replicas: z.number().optional(),
|
||||
availableReplicas: z.number().optional()
|
||||
});
|
||||
export type ContainerStatus = z.infer<typeof ContainerStatusSchema>;
|
||||
|
||||
export const ContainerServerSchema = z.object({
|
||||
serviceName: z.string(),
|
||||
number: z.number(),
|
||||
publicDomain: z.string().optional(),
|
||||
domain: z.string().optional()
|
||||
});
|
||||
export type ContainerServer = z.infer<typeof ContainerServerSchema>;
|
||||
|
||||
export const ContainerInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
image: z.object({
|
||||
imageName: z.string()
|
||||
}),
|
||||
status: ContainerStatusSchema,
|
||||
server: ContainerServerSchema.optional(),
|
||||
createdAt: z.string().optional()
|
||||
});
|
||||
export type ContainerInfo = z.infer<typeof ContainerInfoSchema>;
|
||||
|
||||
export const ContainerInfoResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
data: ContainerInfoSchema
|
||||
});
|
||||
export type ContainerInfoResponse = z.infer<typeof ContainerInfoResponseSchema>;
|
||||
|
||||
// ==================== Sealos API Response Schemas ====================
|
||||
|
||||
export const SealosContainerResponseSchema = z.object({
|
||||
name: z.string(),
|
||||
image: z.object({
|
||||
imageName: z.string()
|
||||
}),
|
||||
createTime: z.string().optional(),
|
||||
status: z.object({
|
||||
replicas: z.coerce.number(),
|
||||
availableReplicas: z.coerce.number(),
|
||||
isPause: z.coerce.boolean()
|
||||
}),
|
||||
ports: z.array(
|
||||
z.object({
|
||||
serviceName: z.string(),
|
||||
number: z.coerce.number(),
|
||||
publicDomain: z.string().optional(),
|
||||
domain: z.string().optional()
|
||||
})
|
||||
)
|
||||
});
|
||||
export type SealosContainerResponse = z.infer<typeof SealosContainerResponseSchema>;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './common.schema';
|
||||
export * from './container.schema';
|
||||
export * from './sandbox.schema';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './common.schema';
|
||||
export * from './container.schema';
|
||||
export * from './sandbox.schema';
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { z } from '@hono/zod-openapi';
|
||||
export declare const ExecRequestSchema: z.ZodObject<
|
||||
{
|
||||
command: z.ZodString;
|
||||
cwd: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ExecRequest = z.infer<typeof ExecRequestSchema>;
|
||||
export declare const HealthResponseSchema: z.ZodObject<
|
||||
{
|
||||
status: z.ZodString;
|
||||
timestamp: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type HealthResponse = z.infer<typeof HealthResponseSchema>;
|
||||
export declare const ExecResponseSchema: z.ZodObject<
|
||||
{
|
||||
success: z.ZodBoolean;
|
||||
stdout: z.ZodString;
|
||||
stderr: z.ZodString;
|
||||
exitCode: z.ZodNumber;
|
||||
cwd: z.ZodOptional<z.ZodString>;
|
||||
error: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ExecResponse = z.infer<typeof ExecResponseSchema>;
|
||||
export declare const HealthCheckResponseSchema: z.ZodObject<
|
||||
{
|
||||
success: z.ZodLiteral<true>;
|
||||
healthy: z.ZodBoolean;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type HealthCheckResponse = z.infer<typeof HealthCheckResponseSchema>;
|
||||
export declare const ExecResultResponseSchema: z.ZodObject<
|
||||
{
|
||||
success: z.ZodLiteral<true>;
|
||||
data: z.ZodObject<
|
||||
{
|
||||
success: z.ZodBoolean;
|
||||
stdout: z.ZodString;
|
||||
stderr: z.ZodString;
|
||||
exitCode: z.ZodNumber;
|
||||
cwd: z.ZodOptional<z.ZodString>;
|
||||
error: z.ZodOptional<z.ZodString>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
},
|
||||
z.core.$strip
|
||||
>;
|
||||
export type ExecResultResponse = z.infer<typeof ExecResultResponseSchema>;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from '@hono/zod-openapi';
|
||||
|
||||
// ==================== Request Schemas ====================
|
||||
|
||||
export const ExecRequestSchema = z.object({
|
||||
command: z.string().min(1).openapi({ example: 'ls -la' }),
|
||||
cwd: z.string().optional().openapi({ example: '/app/sandbox' })
|
||||
});
|
||||
export type ExecRequest = z.infer<typeof ExecRequestSchema>;
|
||||
|
||||
// ==================== Response Schemas ====================
|
||||
|
||||
export const HealthResponseSchema = z.object({
|
||||
status: z.string(),
|
||||
timestamp: z.string().optional()
|
||||
});
|
||||
export type HealthResponse = z.infer<typeof HealthResponseSchema>;
|
||||
|
||||
export const ExecResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
stdout: z.string(),
|
||||
stderr: z.string(),
|
||||
exitCode: z.number(),
|
||||
cwd: z.string().optional(),
|
||||
error: z.string().optional()
|
||||
});
|
||||
export type ExecResponse = z.infer<typeof ExecResponseSchema>;
|
||||
|
||||
export const HealthCheckResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
healthy: z.boolean()
|
||||
});
|
||||
export type HealthCheckResponse = z.infer<typeof HealthCheckResponseSchema>;
|
||||
|
||||
export const ExecResultResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
data: ExecResponseSchema
|
||||
});
|
||||
export type ExecResultResponse = z.infer<typeof ExecResultResponseSchema>;
|
||||
@@ -0,0 +1 @@
|
||||
export { logger } from './logger';
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Logger utility for sandbox server
|
||||
* Provides structured, formatted logging with color support
|
||||
*/
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
|
||||
// Foreground colors
|
||||
black: '\x1b[30m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
gray: '\x1b[90m'
|
||||
};
|
||||
|
||||
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
|
||||
const levelColors: Record<LogLevel, string> = {
|
||||
DEBUG: colors.gray,
|
||||
INFO: colors.green,
|
||||
WARN: colors.yellow,
|
||||
ERROR: colors.red
|
||||
};
|
||||
|
||||
const methodColors: Record<string, string> = {
|
||||
GET: colors.cyan,
|
||||
POST: colors.green,
|
||||
PUT: colors.yellow,
|
||||
PATCH: colors.yellow,
|
||||
DELETE: colors.red
|
||||
};
|
||||
|
||||
/**
|
||||
* Format timestamp as HH:mm:ss.SSS
|
||||
*/
|
||||
function formatTime(date: Date): string {
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
const ms = date.getMilliseconds().toString().padStart(3, '0');
|
||||
return `${hours}:${minutes}:${seconds}.${ms}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration with appropriate unit
|
||||
*/
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for HTTP status code
|
||||
*/
|
||||
function getStatusColor(status: number): string {
|
||||
if (status >= 500) return colors.red;
|
||||
if (status >= 400) return colors.yellow;
|
||||
if (status >= 300) return colors.cyan;
|
||||
if (status >= 200) return colors.green;
|
||||
return colors.white;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core log function
|
||||
*/
|
||||
function log(
|
||||
level: LogLevel,
|
||||
category: string,
|
||||
message: string,
|
||||
meta?: Record<string, unknown>
|
||||
): void {
|
||||
const now = new Date();
|
||||
const time = formatTime(now);
|
||||
const levelColor = levelColors[level];
|
||||
const levelStr = level.padEnd(5);
|
||||
|
||||
let output = `${colors.dim}${time}${colors.reset} ${levelColor}${levelStr}${colors.reset} ${colors.bright}[${category}]${colors.reset} ${message}`;
|
||||
|
||||
if (meta && Object.keys(meta).length > 0) {
|
||||
const metaStr = Object.entries(meta)
|
||||
.map(([k, v]) => `${colors.dim}${k}=${colors.reset}${v}`)
|
||||
.join(' ');
|
||||
output += ` ${metaStr}`;
|
||||
}
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger interface
|
||||
*/
|
||||
export const logger = {
|
||||
debug: (category: string, message: string, meta?: Record<string, unknown>) =>
|
||||
log('DEBUG', category, message, meta),
|
||||
|
||||
info: (category: string, message: string, meta?: Record<string, unknown>) =>
|
||||
log('INFO', category, message, meta),
|
||||
|
||||
warn: (category: string, message: string, meta?: Record<string, unknown>) =>
|
||||
log('WARN', category, message, meta),
|
||||
|
||||
error: (category: string, message: string, meta?: Record<string, unknown>) =>
|
||||
log('ERROR', category, message, meta),
|
||||
|
||||
/**
|
||||
* Log HTTP request start
|
||||
*/
|
||||
httpRequest: (method: string, path: string) => {
|
||||
const methodColor = methodColors[method] || colors.white;
|
||||
const methodStr = method.padEnd(6);
|
||||
log(
|
||||
'INFO',
|
||||
'HTTP',
|
||||
`${colors.bright}-->${colors.reset} ${methodColor}${methodStr}${colors.reset} ${path}`
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Log HTTP response
|
||||
*/
|
||||
httpResponse: (
|
||||
method: string,
|
||||
path: string,
|
||||
status: number,
|
||||
duration: number,
|
||||
error?: string
|
||||
) => {
|
||||
const methodColor = methodColors[method] || colors.white;
|
||||
const statusColor = getStatusColor(status);
|
||||
const methodStr = method.padEnd(6);
|
||||
const durationStr = formatDuration(duration);
|
||||
const level: LogLevel = status >= 400 ? 'ERROR' : 'INFO';
|
||||
|
||||
let message = `${colors.bright}<--${colors.reset} ${methodColor}${methodStr}${colors.reset} ${path} ${statusColor}${status}${colors.reset} ${colors.dim}${durationStr}${colors.reset}`;
|
||||
|
||||
if (error) {
|
||||
message += ` ${colors.red}${error}${colors.reset}`;
|
||||
}
|
||||
|
||||
log(level, 'HTTP', message);
|
||||
}
|
||||
};
|
||||
|
||||
export default logger;
|
||||
Reference in New Issue
Block a user