chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,846 @@
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<FileServerConfig>) {
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<void> {
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<void> {
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 webviewJava 前端 → Java 后端 → Electron
// chat() / streamChat() 不经过此路径,[PERF][Frontend] 日志在实际链路中不触发。
// 代码保留供 Electron renderer 直连场景使用(如未来去除 webview 的方案)。
async chat(request: ChatRequest): Promise<ChatResponse> {
const t0 = Date.now();
// 优先通过 IPCAcpEngine 直接处理,返回 HttpResult<ComputerChatResponse>
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),
};
}
// 回退到 HTTPrcoder 返回 HttpResult<ChatResponse> 格式)
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<ChatMessage> {
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<AgentStatusResponse> {
// 优先通过 IPC返回 HttpResult<ComputerAgentStatusResponse>
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,
};
}
// 回退到 HTTPrcoder 返回 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<AgentStopResponse> {
// 优先通过 IPC返回 HttpResult<ComputerAgentStopResponse>
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,
};
}
// 回退到 HTTPrcoder 返回 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<ComputerAgentCancelResponse>
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,
};
}
// 回退到 HTTPrcoder 返回 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<WorkspaceResult> {
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<FileListResult> {
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<any> {
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<any> {
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<Blob> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<Blob> {
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<any> {
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<boolean> {
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();