Sandbox server (#6383)

* feat: sandbox_server

* docker build

* action
This commit is contained in:
Archer
2026-02-04 20:49:26 +08:00
committed by GitHub
parent 214b3138ad
commit 44d64ce40e
58 changed files with 4757 additions and 213 deletions
@@ -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();
}
+34
View File
@@ -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
};
+79
View File
@@ -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;
};
+23
View File
@@ -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>;
+3
View File
@@ -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';
+55
View File
@@ -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';
+154
View File
@@ -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;