Files
qiming/qimingclaw/crates/agent-electron-client/src/main/services/sandbox/SandboxManager.ts

372 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 沙箱管理器抽象基类
*
* 定义所有沙箱实现必须遵循的接口规范。
* 支持的沙箱类型Docker、macOS Seatbelt、Linux bwrap、Windows Sandbox
*
* @version 1.1.0
* @updated 2026-04-03
*/
import { EventEmitter } from "events";
import type {
SandboxConfig,
SandboxType,
Workspace,
ExecuteOptions,
ExecuteResult,
FileInfo,
SandboxStatus,
CleanupResult,
RetentionPolicy,
} from "@shared/types/sandbox";
import {
SandboxError,
SandboxErrorCode,
WorkspaceError,
isSandboxError,
} from "@shared/errors/sandbox";
import {
createPlatformAdapter,
isSupportedPlatform,
} from "../system/platformAdapter";
/**
* 沙箱管理器抽象基类
*/
export abstract class SandboxManager extends EventEmitter {
protected config: SandboxConfig;
protected workspaces: Map<string, Workspace> = new Map();
protected initialized: boolean = false;
constructor(config: SandboxConfig) {
super();
this.config = config;
}
// ============================================================================
// 抽象方法 - 子类必须实现
// ============================================================================
/**
* 初始化沙箱
* - 检查沙箱环境是否可用
* - 准备必要的资源
*/
abstract init(): Promise<void>;
/**
* 检查沙箱是否可用
*/
abstract isAvailable(): Promise<boolean>;
/**
* 创建工作区
* @param sessionId 会话 ID
*/
abstract createWorkspace(sessionId: string): Promise<Workspace>;
/**
* 销毁工作区
* @param sessionId 会话 ID
*/
abstract destroyWorkspace(sessionId: string): Promise<void>;
/**
* 在沙箱中执行命令
* @param sessionId 会话 ID
* @param command 命令
* @param args 参数
* @param options 执行选项
*/
abstract execute(
sessionId: string,
command: string,
args: string[],
options?: ExecuteOptions,
): Promise<ExecuteResult>;
/**
* 读取文件
* @param sessionId 会话 ID
* @param path 文件路径
*/
abstract readFile(sessionId: string, path: string): Promise<string>;
/**
* 写入文件
* @param sessionId 会话 ID
* @param path 文件路径
* @param content 文件内容
*/
abstract writeFile(
sessionId: string,
path: string,
content: string,
): Promise<void>;
/**
* 读取目录
* @param sessionId 会话 ID
* @param path 目录路径
*/
abstract readDir(sessionId: string, path: string): Promise<FileInfo[]>;
/**
* 删除文件
* @param sessionId 会话 ID
* @param path 文件路径
*/
abstract deleteFile(sessionId: string, path: string): Promise<void>;
/**
* 清理所有资源
*/
abstract cleanup(): Promise<CleanupResult>;
// ============================================================================
// 具体方法 - 通用实现
// ============================================================================
/**
* 获取工作区
* @param sessionId 会话 ID
*/
getWorkspace(sessionId: string): Workspace | undefined {
return this.workspaces.get(sessionId);
}
/**
* 列出所有工作区
*/
listWorkspaces(): Workspace[] {
return Array.from(this.workspaces.values());
}
/**
* 检查工作区是否存在
* @param sessionId 会话 ID
*/
hasWorkspace(sessionId: string): boolean {
return this.workspaces.has(sessionId);
}
/**
* 获取活跃工作区数量
*/
getActiveWorkspaceCount(): number {
return this.workspaces.size;
}
/**
* 获取沙箱状态
*/
async getStatus(): Promise<SandboxStatus> {
const available = await this.isAvailable();
return {
available,
type: this.config.type,
platform: this.config.platform,
activeWorkspaces: this.workspaces.size,
};
}
/**
* 获取沙箱配置
*/
getConfig(): SandboxConfig {
return { ...this.config };
}
/**
* 检查是否已初始化
*/
isInitialized(): boolean {
return this.initialized;
}
/**
* 更新工作区最后访问时间
* @param sessionId 会话 ID
*/
protected updateLastAccessed(sessionId: string): void {
const workspace = this.workspaces.get(sessionId);
if (workspace) {
workspace.lastAccessedAt = new Date();
}
}
/**
* 验证工作区存在
* @param sessionId 会话 ID
* @throws WorkspaceError 如果工作区不存在
*/
protected validateWorkspaceExists(sessionId: string): Workspace {
const workspace = this.workspaces.get(sessionId);
if (!workspace) {
throw new WorkspaceError(
`Workspace not found: ${sessionId}`,
SandboxErrorCode.WORKSPACE_NOT_FOUND,
{ sessionId },
);
}
return workspace;
}
/**
* 验证路径在工作区内
* @param workspace 工作区
* @param path 要验证的路径
* @throws SandboxError 如果路径在工作区外
*/
protected validatePathInWorkspace(workspace: Workspace, path: string): void {
const normalizedPath = this.normalizePath(path);
const normalizedRoot = this.normalizePath(workspace.rootPath);
if (!normalizedPath.startsWith(normalizedRoot)) {
throw new SandboxError(
`Path outside workspace: ${path}`,
SandboxErrorCode.PERMISSION_DENIED,
{
sessionId: workspace.sessionId,
details: { path, rootPath: workspace.rootPath },
},
);
}
}
/**
* 规范化路径
* @param path 路径
*/
protected normalizePath(path: string): string {
// 子类可以覆盖此方法以处理平台特定的路径规范化
return path.replace(/\\/g, "/").toLowerCase();
}
/**
* 生成工作区 ID
* @param sessionId 会话 ID
*/
protected generateWorkspaceId(sessionId: string): string {
return `workspace-${sessionId}-${Date.now()}`;
}
/**
* 创建默认保留策略
*/
protected createDefaultRetentionPolicy(): RetentionPolicy {
return {
mode: "timeout",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
maxWorkspaces: 10,
preserveOnError: false,
};
}
/**
* 发出事件
*/
protected emitEvent<T extends string>(event: T, data?: unknown): void {
this.emit(event, data);
}
/**
* 销毁(清理所有资源)
*/
async destroy(): Promise<void> {
try {
await this.cleanup();
this.workspaces.clear();
this.initialized = false;
this.removeAllListeners();
} catch (error) {
// 记录错误但不抛出,确保清理继续
console.error("[SandboxManager] Destroy error:", error);
}
}
// ============================================================================
// 静态工具方法
// ============================================================================
/**
* 检测当前平台
*/
static detectPlatform(): NodeJS.Platform {
if (isSupportedPlatform(process.platform)) {
return createPlatformAdapter(process.platform).platform;
}
return process.platform;
}
/**
* 获取推荐的沙箱类型(按平台)
*/
static getRecommendedSandboxType(): SandboxType {
if (!isSupportedPlatform(process.platform)) {
return "none";
}
return createPlatformAdapter(process.platform).getRecommendedSandboxType();
}
/**
* 解析内存限制字符串
* @param memoryLimit 内存限制(如 "2g", "512m"
* @returns 字节数
*/
static parseMemoryLimit(memoryLimit: string): number {
const match = memoryLimit.match(/^(\d+(?:\.\d+)?)([kmg]?)$/i);
if (!match) {
return 0;
}
const value = parseFloat(match[1]);
const unit = match[2].toLowerCase();
switch (unit) {
case "k":
return Math.floor(value * 1024);
case "m":
return Math.floor(value * 1024 * 1024);
case "g":
return Math.floor(value * 1024 * 1024 * 1024);
default:
return Math.floor(value);
}
}
/**
* 格式化内存大小
* @param bytes 字节数
* @returns 格式化字符串(如 "2 GB"
*/
static formatMemory(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
/**
* 判断是否为沙箱错误
*/
static isSandboxError(error: unknown): error is SandboxError {
return isSandboxError(error);
}
}
/**
* 沙箱管理器类型
*/
export type SandboxManagerType = typeof SandboxManager;
export default SandboxManager;