/** * 后端代码运行器 * 连接到 code-backend Rust 服务进行代码执行 */ import type { RunResult, TestCase, TestCaseResult } from "../types"; export interface BackendConfig { /** API 基础 URL */ apiUrl?: string; /** 请求超时时间(毫秒) */ timeout?: number; /** 是否启用调试日志 */ debug?: boolean; } export interface BackendResponse { stdout: string; stderr: string; exit_code: number; execution_time_ms: number; error: string; test_results?: TestCaseResult[]; } /** * 后端运行器 * 通过 HTTP API 调用 code-backend 服务 */ export class BackendRunner { private config: Required; constructor(config: BackendConfig = {}) { this.config = { apiUrl: config.apiUrl || "http://192.168.60.98:8080", timeout: config.timeout || 30000, debug: config.debug || false, }; } /** * 运行代码 */ async run( language: string, code: string, testCases?: TestCase[], questionId?: number, ): Promise { const startTime = performance.now(); try { if (this.config.debug) { console.log("[BackendRunner] Running code:", { language, codeLength: code.length, testCasesCount: testCases?.length || 0, questionId, }); } const response = await this.fetchWithTimeout( `${this.config.apiUrl}/api/v1/run`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ language, code, stdin: "", testCases: testCases || null, questionId: questionId || null, }), }, this.config.timeout, ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result: BackendResponse = await response.json(); if (this.config.debug) { console.log("[BackendRunner] Response:", result); } const executionTime = result.execution_time_ms || performance.now() - startTime; // 转换为前端格式 return this.convertResponse(result, executionTime); } catch (error: any) { if (this.config.debug) { console.error("[BackendRunner] Error:", error); } return { output: "", error: this.formatError(error), executionTime: performance.now() - startTime, status: "error", }; } } /** * 提交代码(异步执行) */ async submit( language: string, code: string, testCases?: TestCase[], questionId?: number, ): Promise { try { const response = await this.fetchWithTimeout( `${this.config.apiUrl}/api/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ language, code, stdin: "", testCases: testCases || null, questionId: questionId || null, }), }, this.config.timeout, ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); return result.submissionId; } catch (error: any) { throw new Error(`提交失败: ${this.formatError(error)}`); } } /** * 查询提交状态 */ async getStatus(submissionId: string): Promise<{ status: "Pending" | "Running" | "Completed" | "Failed"; result?: BackendResponse; }> { try { const response = await fetch( `${this.config.apiUrl}/api/v1/status/${submissionId}`, ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data; } catch (error: any) { throw new Error(`查询状态失败: ${this.formatError(error)}`); } } /** * 轮询等待结果 */ async waitForResult( submissionId: string, maxAttempts: number = 30, interval: number = 1000, ): Promise { for (let i = 0; i < maxAttempts; i++) { const status = await this.getStatus(submissionId); if (status.status === "Completed" && status.result) { return this.convertResponse( status.result, status.result.execution_time_ms, ); } if (status.status === "Failed") { throw new Error("执行失败"); } // 等待 await new Promise((resolve) => setTimeout(resolve, interval)); } throw new Error("等待超时"); } /** * 获取题目信息 */ async getQuestion(questionId?: number): Promise { try { const url = questionId ? `${this.config.apiUrl}/api/v1/question?id=${questionId}` : `${this.config.apiUrl}/api/v1/question`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error: any) { throw new Error(`获取题目失败: ${this.formatError(error)}`); } } /** * 检查后端健康状态 */ async healthCheck(): Promise { try { const response = await this.fetchWithTimeout( `${this.config.apiUrl}/api/v1/question`, { method: "GET" }, 5000, ); return response.ok; } catch { return false; } } /** * 转换后端响应为前端格式 */ private convertResponse( backendResult: BackendResponse, executionTime: number, ): RunResult { const hasTestResults = backendResult.test_results && backendResult.test_results.length > 0; // 确定整体状态 let status: RunResult["status"] = "success"; if (backendResult.error) { status = "error"; } else if (backendResult.exit_code !== 0) { status = "error"; } else if (hasTestResults) { const hasError = backendResult.test_results!.some( (t) => t.status === "error", ); const hasFailed = backendResult.test_results!.some( (t) => t.status === "failed", ); if (hasError || hasFailed) { status = "error"; } } return { output: backendResult.stdout, error: backendResult.stderr || backendResult.error || undefined, executionTime, status, testResults: backendResult.test_results, }; } /** * 格式化错误信息 */ private formatError(error: any): string { if (error.name === "AbortError") { return `请求超时(${this.config.timeout}ms)`; } if (error.message) { return error.message; } return String(error); } /** * 带超时的 fetch */ private fetchWithTimeout( url: string, options: RequestInit, timeout: number, ): Promise { return Promise.race([ fetch(url, options), new Promise((_, reject) => { setTimeout(() => { const error = new Error("Request timeout"); error.name = "AbortError"; reject(error); }, timeout); }), ]); } /** * 更新配置 */ setConfig(config: Partial): void { this.config = { ...this.config, ...config }; } /** * 获取当前配置 */ getConfig(): Readonly> { return { ...this.config }; } } /** * 创建默认的后端运行器实例 */ export function createBackendRunner(config?: BackendConfig): BackendRunner { return new BackendRunner(config); }