import { LOCALHOST_HOSTNAME, DEFAULT_FILE_SERVER_PORT, } from "@shared/constants"; /** 发送 PERF 日志到主进程写入专属日志文件,不可用时降级到 console.log */ function perfLog(msg: string): void { if (window.electronAPI?.perf) { window.electronAPI.perf.log(msg); } else { console.log(msg); } } export interface FileServerConfig { baseUrl: string; apiKey?: string; } export interface WorkspaceResult { success: boolean; message: string; workspaceRoot?: string; } export interface FileInfo { name: string; path: string; isDirectory: boolean; size?: number; modifiedTime?: number; } export interface FileListResult { success: boolean; files: FileInfo[]; } // ==================== Chat SSE Types ==================== export interface ChatMessage { sessionId: string; messageType: string; subType: string; data: any; timestamp: string; } export interface ChatRequest { user_id: string; project_id?: string; prompt: string; session_id?: string; attachments?: any[]; data_source_attachments?: string[]; model_provider?: ModelProviderConfig; request_id?: string; system_prompt?: string; user_prompt?: string; agent_config?: any; } export interface ModelProviderConfig { provider: "anthropic" | "openai" | "google" | "azure"; api_key?: string; base_url?: string; model?: string; max_tokens?: number; temperature?: number; } export interface ChatResponse { success: boolean; project_id: string; session_id: string; message?: string; error?: string; } export interface AgentStatusRequest { user_id: string; project_id?: string; session_id?: string; } export interface AgentStatusResponse { success: boolean; status: "Idle" | "Busy"; session_id?: string; project_id?: string; } export interface AgentStopRequest { user_id: string; project_id?: string; session_id?: string; } export interface AgentStopResponse { success: boolean; message: string; } // ==================== File Server Service ==================== class FileServerService { private config: FileServerConfig = { baseUrl: `http://${LOCALHOST_HOSTNAME}:${DEFAULT_FILE_SERVER_PORT}`, }; setConfig(config: Partial) { this.config = { ...this.config, ...config }; } getConfig(): FileServerConfig { return { ...this.config }; } private getHeaders(): HeadersInit { return this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}; } async loadConfig(): Promise { try { const saved = await window.electronAPI?.settings.get("file_server_config"); if (saved) { this.config = { ...this.config, ...(saved as FileServerConfig) }; } } catch (error) { console.error("Failed to load file server config:", error); } } async saveConfig(): Promise { try { await window.electronAPI?.settings.set("file_server_config", this.config); } catch (error) { console.error("Failed to save file server config:", error); } } // ==================== Chat/SSE API (Agent Runner) ==================== // POST /computer/chat - 发送聊天消息 // 注:当前 UI 为 embedded webview(Java 前端 → Java 后端 → Electron), // chat() / streamChat() 不经过此路径,[PERF][Frontend] 日志在实际链路中不触发。 // 代码保留供 Electron renderer 直连场景使用(如未来去除 webview 的方案)。 async chat(request: ChatRequest): Promise { const t0 = Date.now(); // 优先通过 IPC(AcpEngine 直接处理,返回 HttpResult) if (window.electronAPI?.computer) { perfLog( `[PERF][Frontend][chat] request sent: project_id=${request.project_id}, t=${t0}`, ); const result = await window.electronAPI.computer.chat(request); const t1 = Date.now(); perfLog( `[PERF][Frontend][chat] IPC response: session_id=${result.data?.session_id}, duration=${t1 - t0}ms`, ); // 从 HttpResult 中提取 data,映射到 fileServer 本地 ChatResponse 格式 return { success: result.success, project_id: result.data?.project_id || "", session_id: result.data?.session_id || "", error: result.data?.error || (result.success ? undefined : result.message), }; } // 回退到 HTTP(rcoder 返回 HttpResult 格式) perfLog( `[PERF][Frontend][chat] HTTP request sent: project_id=${request.project_id}, t=${t0}`, ); const response = await fetch(`${this.config.baseUrl}/computer/chat`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify(request), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || `Chat failed: ${response.statusText}`); } // rcoder 返回 HttpResult 格式,提取 data const httpResult = await response.json(); const t1 = Date.now(); perfLog( `[PERF][Frontend][chat] HTTP response: session_id=${httpResult.data?.session_id}, duration=${t1 - t0}ms`, ); return { success: httpResult.success ?? false, project_id: httpResult.data?.project_id || "", session_id: httpResult.data?.session_id || "", error: httpResult.data?.error || (httpResult.success ? undefined : httpResult.message), }; } // GET /computer/progress/{session_id} - SSE 流式进度 // 注:同 chat(),当前链路不经过此路径,[PERF][Frontend][streamChat] 日志在实际链路中不触发。 async *streamChat(sessionId: string): AsyncGenerator { const t0 = Date.now(); perfLog( `[PERF][Frontend][streamChat] connection initiated: session_id=${sessionId}, t=${t0}`, ); const response = await fetch( `${this.config.baseUrl}/computer/progress/${sessionId}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) { throw new Error( `Failed to connect to progress stream: ${response.statusText}`, ); } if (!response.body) { throw new Error("No response body"); } perfLog( `[PERF][Frontend][streamChat] SSE connected: session_id=${sessionId}, duration=${Date.now() - t0}ms`, ); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let firstChunk = true; while (true) { const { done, value } = await reader.read(); if (done) { perfLog( `[PERF][Frontend][streamChat] SSE ended: session_id=${sessionId}, total_duration=${Date.now() - t0}ms`, ); break; } if (firstChunk) { perfLog( `[PERF][Frontend][streamChat] SSE first chunk: session_id=${sessionId}, duration=${Date.now() - t0}ms`, ); firstChunk = false; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const data = line.slice(6); // Skip heartbeat events if (data.includes('"ping"') || data.includes("heartbeat")) { continue; } try { const message = JSON.parse(data); yield message; } catch { // Not JSON, might be plain text yield { data, messageType: "text", subType: "message", timestamp: new Date().toISOString(), }; } } // Handle event types if (line.startsWith("event: ")) { const eventType = line.slice(7); // Event type in 'event:' header } } } } // POST /computer/agent/status - 获取 Agent 状态 async getAgentStatus( request: AgentStatusRequest, ): Promise { // 优先通过 IPC(返回 HttpResult) if (window.electronAPI?.computer) { const result = await window.electronAPI.computer.agentStatus(request); return { success: result.success, status: (result.data?.status === "Busy" ? "Busy" : "Idle") as | "Idle" | "Busy", session_id: result.data?.session_id ?? undefined, project_id: result.data?.project_id ?? request.project_id, }; } // 回退到 HTTP(rcoder 返回 HttpResult 格式) const response = await fetch( `${this.config.baseUrl}/computer/agent/status`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify(request), }, ); if (!response.ok) { throw new Error(`Failed to get status: ${response.statusText}`); } const httpResult = await response.json(); return { success: httpResult.success ?? false, status: (httpResult.data?.status === "Busy" ? "Busy" : "Idle") as | "Idle" | "Busy", session_id: httpResult.data?.session_id, project_id: httpResult.data?.project_id, }; } // POST /computer/agent/stop - 停止 Agent async stopAgent(request: AgentStopRequest): Promise { // 优先通过 IPC(返回 HttpResult) if (window.electronAPI?.computer) { const result = await window.electronAPI.computer.agentStop(request); return { success: result.data?.success ?? result.success, message: result.data?.message ?? result.message, }; } // 回退到 HTTP(rcoder 返回 HttpResult 格式) const response = await fetch(`${this.config.baseUrl}/computer/agent/stop`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify(request), }); if (!response.ok) { throw new Error(`Failed to stop agent: ${response.statusText}`); } const httpResult = await response.json(); return { success: httpResult.data?.success ?? httpResult.success, message: httpResult.data?.message ?? httpResult.message ?? "Stopped", }; } // POST /computer/agent/session/cancel - 取消会话 async cancelSession(request: { user_id: string; session_id: string; }): Promise<{ success: boolean; message: string }> { // 优先通过 IPC(返回 HttpResult) if (window.electronAPI?.computer) { const result = await window.electronAPI.computer.cancelSession(request); return { success: result.data?.success ?? result.success, message: result.success ? "Cancelled" : result.message, }; } // 回退到 HTTP(rcoder 返回 HttpResult 格式) const response = await fetch( `${this.config.baseUrl}/computer/agent/session/cancel`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify(request), }, ); if (!response.ok) { throw new Error(`Failed to cancel session: ${response.statusText}`); } const httpResult = await response.json(); return { success: httpResult.data?.success ?? httpResult.success, message: httpResult.success ? "Cancelled" : (httpResult.message ?? "Failed"), }; } // ==================== Computer Routes ==================== // POST /computer/create-workspace - 创建工作空间(支持skills同步) async createWorkspace( userId: string, cId: string, zipFile?: File, ): Promise { const formData = new FormData(); formData.append("userId", userId); formData.append("cId", cId); if (zipFile) { formData.append("file", zipFile); } const response = await fetch( `${this.config.baseUrl}/computer/create-workspace`, { method: "POST", headers: this.getHeaders(), body: formData, }, ); if (!response.ok) { throw new Error(`Failed to create workspace: ${response.statusText}`); } return response.json(); } // GET /computer/get-file-list - 获取文件列表 async getFileList( userId: string, cId: string, proxyPath?: string, ): Promise { const params = new URLSearchParams({ userId, cId }); if (proxyPath) { params.append("proxyPath", proxyPath); } const response = await fetch( `${this.config.baseUrl}/computer/get-file-list?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) { throw new Error(`Failed to get file list: ${response.statusText}`); } return response.json(); } // POST /computer/files-update - 批量更新文件 async updateFiles( userId: string, cId: string, files: { name: string; contents: string }[], ): Promise<{ success: boolean }> { const response = await fetch( `${this.config.baseUrl}/computer/files-update`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ userId, cId, files }), }, ); if (!response.ok) { throw new Error(`Failed to update files: ${response.statusText}`); } return response.json(); } // POST /computer/upload-file - 上传单个文件 async uploadFile( userId: string, cId: string, file: File, filePath: string, ): Promise { const formData = new FormData(); formData.append("userId", userId); formData.append("cId", cId); formData.append("filePath", filePath); formData.append("file", file); const response = await fetch( `${this.config.baseUrl}/computer/upload-file`, { method: "POST", headers: this.getHeaders(), body: formData, }, ); if (!response.ok) { throw new Error(`Failed to upload file: ${response.statusText}`); } return response.json(); } // POST /computer/upload-files - 批量上传文件 async uploadFiles( userId: string, cId: string, files: File[], filePaths: string[], ): Promise { const formData = new FormData(); formData.append("userId", userId); formData.append("cId", cId); formData.append("filePaths", JSON.stringify(filePaths)); files.forEach((file, i) => { formData.append("files", file); }); const response = await fetch( `${this.config.baseUrl}/computer/upload-files`, { method: "POST", headers: this.getHeaders(), body: formData, }, ); if (!response.ok) { throw new Error(`Failed to upload files: ${response.statusText}`); } return response.json(); } // GET /computer/download-all-files - 下载所有文件 async downloadAllFiles(userId: string, cId: string): Promise { const params = new URLSearchParams({ userId, cId }); const response = await fetch( `${this.config.baseUrl}/computer/download-all-files?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) { throw new Error(`Failed to download files: ${response.statusText}`); } return response.blob(); } // ==================== Build Routes ==================== async startDev(projectId: string): Promise { const params = new URLSearchParams({ projectId }); const response = await fetch( `${this.config.baseUrl}/build/start-dev?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to start dev: ${response.statusText}`); return response.json(); } async stopDev(projectId: string, pid: string): Promise { const params = new URLSearchParams({ projectId, pid }); const response = await fetch( `${this.config.baseUrl}/build/stop-dev?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to stop dev: ${response.statusText}`); return response.json(); } async build(projectId: string): Promise { const params = new URLSearchParams({ projectId }); const response = await fetch( `${this.config.baseUrl}/build/build?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to build: ${response.statusText}`); return response.json(); } async restartDev(projectId: string): Promise { const params = new URLSearchParams({ projectId }); const response = await fetch( `${this.config.baseUrl}/build/restart-dev?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to restart dev: ${response.statusText}`); return response.json(); } async listDev(): Promise<{ success: boolean; list: any[] }> { const response = await fetch(`${this.config.baseUrl}/build/list-dev`, { method: "GET", headers: this.getHeaders(), }); if (!response.ok) throw new Error(`Failed to list dev: ${response.statusText}`); return response.json(); } async getDevLog( projectId: string, startIndex: number = 1, logType: string = "temp", ): Promise { const params = new URLSearchParams({ projectId, startIndex: String(startIndex), logType, }); const response = await fetch( `${this.config.baseUrl}/build/get-dev-log?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to get dev log: ${response.statusText}`); return response.json(); } // ==================== Code Routes ==================== async updateAllFiles( projectId: string, codeVersion: string, files: any[], basePath?: string, pid?: string, ): Promise { const response = await fetch( `${this.config.baseUrl}/code/all-files-update`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ projectId, codeVersion, files, basePath, pid }), }, ); if (!response.ok) throw new Error(`Failed to update files: ${response.statusText}`); return response.json(); } async uploadSingleCodeFile( projectId: string, codeVersion: string, file: File, filePath: string, ): Promise { const formData = new FormData(); formData.append("projectId", projectId); formData.append("codeVersion", codeVersion); formData.append("filePath", filePath); formData.append("file", file); const response = await fetch( `${this.config.baseUrl}/code/upload-single-file`, { method: "POST", headers: this.getHeaders(), body: formData, }, ); if (!response.ok) throw new Error(`Failed to upload code file: ${response.statusText}`); return response.json(); } async rollbackVersion( projectId: string, codeVersion: string, rollbackTo: string, ): Promise { const response = await fetch( `${this.config.baseUrl}/code/rollback-version`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ projectId, codeVersion, rollbackTo }), }, ); if (!response.ok) throw new Error(`Failed to rollback: ${response.statusText}`); return response.json(); } // ==================== Project Routes ==================== async createProject(projectId: string): Promise { const response = await fetch( `${this.config.baseUrl}/project/create-project`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ projectId }), }, ); if (!response.ok) throw new Error(`Failed to create project: ${response.statusText}`); return response.json(); } async getProjectContent( projectId: string, command?: string, proxyPath?: string, ): Promise { const params = new URLSearchParams({ projectId }); if (command) params.append("command", command); if (proxyPath) params.append("proxyPath", proxyPath); const response = await fetch( `${this.config.baseUrl}/project/get-project-content?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to get project content: ${response.statusText}`); return response.json(); } async backupVersion(projectId: string, codeVersion: string): Promise { const response = await fetch( `${this.config.baseUrl}/project/backup-current-version`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ projectId, codeVersion }), }, ); if (!response.ok) throw new Error(`Failed to backup version: ${response.statusText}`); return response.json(); } async exportProject( projectId: string, codeVersion: string, exportType: string = "zip", config?: any, ): Promise { const response = await fetch( `${this.config.baseUrl}/project/export-project`, { method: "POST", headers: { ...this.getHeaders(), "Content-Type": "application/json", }, body: JSON.stringify({ projectId, codeVersion, exportType, config }), }, ); if (!response.ok) throw new Error(`Failed to export project: ${response.statusText}`); return response.blob(); } async deleteProject(projectId: string, pid?: string): Promise { const params = new URLSearchParams({ projectId }); if (pid) params.append("pid", pid); const response = await fetch( `${this.config.baseUrl}/project/delete-project?${params}`, { method: "GET", headers: this.getHeaders(), }, ); if (!response.ok) throw new Error(`Failed to delete project: ${response.statusText}`); return response.json(); } // Check connection async checkConnection(): Promise { try { // 优先通过 IPC if (window.electronAPI?.computer) { const result = await window.electronAPI.computer.health(); return result.status === "healthy"; } // 回退到 HTTP const response = await fetch(`${this.config.baseUrl}/health`, { method: "GET", }); return response.ok; } catch { return false; } } } export const fileServerService = new FileServerService();