diff --git a/electron/ConfigHelper.ts b/electron/ConfigHelper.ts index 6d1d2dba..076113bb 100644 --- a/electron/ConfigHelper.ts +++ b/electron/ConfigHelper.ts @@ -5,14 +5,28 @@ import { app } from "electron" import { EventEmitter } from "events" import { OpenAI } from "openai" +// API URL constants for Chinese AI providers +const API_URLS = { + deepseek: 'https://api.deepseek.com', + zhipu: 'https://open.bigmodel.cn/api/paas/v4', + bailian: 'https://coding.dashscope.aliyuncs.com/v1' // Coding Plan 专属 URL +} as const; + interface Config { - apiKey: string; - apiProvider: "openai" | "gemini" | "anthropic"; // Added provider selection + apiKey: string; // Legacy: used for OpenAI/Gemini/Anthropic + apiProvider: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu" | "bailian"; extractionModel: string; solutionModel: string; debuggingModel: string; language: string; opacity: number; + // Separate API keys for each provider + openaiApiKey?: string; + geminiApiKey?: string; + anthropicApiKey?: string; + deepseekApiKey?: string; + zhipuApiKey?: string; + bailianApiKey?: string; } export class ConfigHelper extends EventEmitter { @@ -58,7 +72,7 @@ export class ConfigHelper extends EventEmitter { /** * Validate and sanitize model selection to ensure only allowed models are used */ - private sanitizeModelSelection(model: string, provider: "openai" | "gemini" | "anthropic"): string { + private sanitizeModelSelection(model: string, provider: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu" | "bailian"): string { if (provider === "openai") { // Only allow gpt-4o and gpt-4o-mini for OpenAI const allowedModels = ['gpt-4o', 'gpt-4o-mini']; @@ -72,10 +86,10 @@ export class ConfigHelper extends EventEmitter { const allowedModels = ['gemini-1.5-pro', 'gemini-2.0-flash']; if (!allowedModels.includes(model)) { console.warn(`Invalid Gemini model specified: ${model}. Using default model: gemini-2.0-flash`); - return 'gemini-2.0-flash'; // Changed default to flash + return 'gemini-2.0-flash'; } return model; - } else if (provider === "anthropic") { + } else if (provider === "anthropic") { // Only allow Claude models const allowedModels = ['claude-3-7-sonnet-20250219', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229']; if (!allowedModels.includes(model)) { @@ -83,6 +97,45 @@ export class ConfigHelper extends EventEmitter { return 'claude-3-7-sonnet-20250219'; } return model; + } else if (provider === "deepseek") { + // Deepseek models - OpenAI compatible API + const allowedModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner']; + if (!allowedModels.includes(model)) { + console.warn(`Invalid Deepseek model specified: ${model}. Using default model: deepseek-chat`); + return 'deepseek-chat'; + } + return model; + } else if (provider === "zhipu") { + // Zhipu/GLM models (including GLM-4.5 and GLM-5 series) + const allowedModels = [ + 'glm-4v-flash', 'glm-4v-plus', // Vision models + 'glm-4-flash', 'glm-4-plus', 'glm-4-long', // GLM-4 text models + 'glm-4.5', 'glm-4.5-air', 'glm-4.5-airx', // GLM-4.5 series + 'glm-5', 'glm-5-plus' // GLM-5 series (latest) + ]; + if (!allowedModels.includes(model)) { + console.warn(`Invalid Zhipu model specified: ${model}. Using default model: glm-4v-flash`); + return 'glm-4v-flash'; + } + return model; + } else if (provider === "bailian") { + // Alibaba Bailian Coding Plan models + const allowedModels = [ + // Coding Plan Pro 推荐模型 (支持图片理解) + 'qwen3.5-plus', 'kimi-k2.5', + // Coding Plan Pro 其他模型 + 'glm-5', 'MiniMax-M2.5', 'glm-4.7', + 'qwen3-max-2026-01-23', 'qwen3-coder-next', 'qwen3-coder-plus', + // 兼容旧版本的普通百炼模型 (以防用户切换) + 'qwen-vl-max', 'qwen-vl-plus', 'qwen3-vl-plus', 'qwen3-vl-flash', + 'qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen3-max', + 'qwq-plus' + ]; + if (!allowedModels.includes(model)) { + console.warn(`Invalid Bailian model specified: ${model}. Using default model: qwen3.5-plus`); + return 'qwen3.5-plus'; + } + return model; } // Default fallback return model; @@ -95,7 +148,8 @@ export class ConfigHelper extends EventEmitter { const config = JSON.parse(configData); // Ensure apiProvider is a valid value - if (config.apiProvider !== "openai" && config.apiProvider !== "gemini" && config.apiProvider !== "anthropic") { + const validProviders = ["openai", "gemini", "anthropic", "deepseek", "zhipu", "bailian"]; + if (!validProviders.includes(config.apiProvider)) { config.apiProvider = "gemini"; // Default to Gemini if invalid } @@ -178,6 +232,18 @@ export class ConfigHelper extends EventEmitter { updates.extractionModel = "claude-3-7-sonnet-20250219"; updates.solutionModel = "claude-3-7-sonnet-20250219"; updates.debuggingModel = "claude-3-7-sonnet-20250219"; + } else if (updates.apiProvider === "deepseek") { + updates.extractionModel = "deepseek-chat"; + updates.solutionModel = "deepseek-chat"; + updates.debuggingModel = "deepseek-chat"; + } else if (updates.apiProvider === "zhipu") { + updates.extractionModel = "glm-4v-flash"; + updates.solutionModel = "glm-4v-flash"; + updates.debuggingModel = "glm-4v-flash"; + } else if (updates.apiProvider === "bailian") { + updates.extractionModel = "qwen-vl-plus"; + updates.solutionModel = "qwen-plus"; + updates.debuggingModel = "qwen-vl-plus"; } else { updates.extractionModel = "gemini-2.0-flash"; updates.solutionModel = "gemini-2.0-flash"; @@ -195,18 +261,36 @@ export class ConfigHelper extends EventEmitter { if (updates.debuggingModel) { updates.debuggingModel = this.sanitizeModelSelection(updates.debuggingModel, provider); } - + + // Save API key to the provider-specific field + if (updates.apiKey !== undefined) { + const targetProvider = updates.apiProvider || currentConfig.apiProvider; + if (targetProvider === "openai") { + updates.openaiApiKey = updates.apiKey; + } else if (targetProvider === "gemini") { + updates.geminiApiKey = updates.apiKey; + } else if (targetProvider === "anthropic") { + updates.anthropicApiKey = updates.apiKey; + } else if (targetProvider === "deepseek") { + updates.deepseekApiKey = updates.apiKey; + } else if (targetProvider === "zhipu") { + updates.zhipuApiKey = updates.apiKey; + } else if (targetProvider === "bailian") { + updates.bailianApiKey = updates.apiKey; + } + } + const newConfig = { ...currentConfig, ...updates }; this.saveConfig(newConfig); - + // Only emit update event for changes other than opacity // This prevents re-initializing the AI client when only opacity changes - if (updates.apiKey !== undefined || updates.apiProvider !== undefined || - updates.extractionModel !== undefined || updates.solutionModel !== undefined || + if (updates.apiKey !== undefined || updates.apiProvider !== undefined || + updates.extractionModel !== undefined || updates.solutionModel !== undefined || updates.debuggingModel !== undefined || updates.language !== undefined) { this.emit('config-updated', newConfig); } - + return newConfig; } catch (error) { console.error('Error updating config:', error); @@ -215,17 +299,43 @@ export class ConfigHelper extends EventEmitter { } /** - * Check if the API key is configured + * Get the API key for the current provider */ - public hasApiKey(): boolean { + public getApiKeyForProvider(provider?: string): string { const config = this.loadConfig(); - return !!config.apiKey && config.apiKey.trim().length > 0; + const targetProvider = provider || config.apiProvider; + + // First check provider-specific keys + if (targetProvider === "openai" && config.openaiApiKey) { + return config.openaiApiKey; + } else if (targetProvider === "gemini" && config.geminiApiKey) { + return config.geminiApiKey; + } else if (targetProvider === "anthropic" && config.anthropicApiKey) { + return config.anthropicApiKey; + } else if (targetProvider === "deepseek" && config.deepseekApiKey) { + return config.deepseekApiKey; + } else if (targetProvider === "zhipu" && config.zhipuApiKey) { + return config.zhipuApiKey; + } else if (targetProvider === "bailian" && config.bailianApiKey) { + return config.bailianApiKey; + } + + // Fallback to legacy apiKey field + return config.apiKey || ""; + } + + /** + * Check if the API key is configured for the current provider + */ + public hasApiKey(): boolean { + const apiKey = this.getApiKeyForProvider(); + return !!apiKey && apiKey.trim().length > 0; } /** * Validate the API key format */ - public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" ): boolean { + public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu" | "bailian"): boolean { // If provider is not specified, attempt to auto-detect if (!provider) { if (apiKey.trim().startsWith('sk-')) { @@ -238,18 +348,27 @@ export class ConfigHelper extends EventEmitter { provider = "gemini"; } } - + if (provider === "openai") { // Basic format validation for OpenAI API keys return /^sk-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); } else if (provider === "gemini") { // Basic format validation for Gemini API keys (usually alphanumeric with no specific prefix) - return apiKey.trim().length >= 10; // Assuming Gemini keys are at least 10 chars + return apiKey.trim().length >= 10; } else if (provider === "anthropic") { // Basic format validation for Anthropic API keys return /^sk-ant-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); + } else if (provider === "deepseek") { + // Deepseek API keys typically start with "sk-" + return apiKey.trim().length >= 10; + } else if (provider === "zhipu") { + // Zhipu/GLM API keys - format varies, just check length + return apiKey.trim().length >= 10; + } else if (provider === "bailian") { + // Alibaba Bailian API keys - format varies, just check length + return apiKey.trim().length >= 10; } - + return false; } @@ -288,7 +407,7 @@ export class ConfigHelper extends EventEmitter { /** * Test API key with the selected provider */ - public async testApiKey(apiKey: string, provider?: "openai" | "gemini" | "anthropic"): Promise<{valid: boolean, error?: string}> { + public async testApiKey(apiKey: string, provider?: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu" | "bailian"): Promise<{valid: boolean, error?: string}> { // Auto-detect provider based on key format if not specified if (!provider) { if (apiKey.trim().startsWith('sk-')) { @@ -304,15 +423,21 @@ export class ConfigHelper extends EventEmitter { console.log("Using Gemini API key format for testing (default)"); } } - + if (provider === "openai") { return this.testOpenAIKey(apiKey); } else if (provider === "gemini") { return this.testGeminiKey(apiKey); } else if (provider === "anthropic") { return this.testAnthropicKey(apiKey); + } else if (provider === "deepseek") { + return this.testDeepseekKey(apiKey); + } else if (provider === "zhipu") { + return this.testZhipuKey(apiKey); + } else if (provider === "bailian") { + return this.testBailianKey(apiKey); } - + return { valid: false, error: "Unknown API provider" }; } @@ -386,11 +511,110 @@ export class ConfigHelper extends EventEmitter { } catch (error: any) { console.error('Anthropic API key test failed:', error); let errorMessage = 'Unknown error validating Anthropic API key'; - + if (error.message) { errorMessage = `Error: ${error.message}`; } - + + return { valid: false, error: errorMessage }; + } + } + + /** + * Test Deepseek API key + * Deepseek uses OpenAI-compatible API + */ + private async testDeepseekKey(apiKey: string): Promise<{valid: boolean, error?: string}> { + try { + // Deepseek uses OpenAI-compatible API with different base URL + const openai = new OpenAI({ + apiKey, + baseURL: API_URLS.deepseek + }); + // Make a simple API call to test the key + await openai.models.list(); + return { valid: true }; + } catch (error: any) { + console.error('Deepseek API key test failed:', error); + + let errorMessage = 'Unknown error validating Deepseek API key'; + + if (error.status === 401) { + errorMessage = 'Invalid API key. Please check your Deepseek key and try again.'; + } else if (error.status === 429) { + errorMessage = 'Rate limit exceeded. Your Deepseek API key has reached its request limit.'; + } else if (error.status === 500) { + errorMessage = 'Deepseek server error. Please try again later.'; + } else if (error.message) { + errorMessage = `Error: ${error.message}`; + } + + return { valid: false, error: errorMessage }; + } + } + + /** + * Test Zhipu/GLM API key + * Zhipu uses OpenAI-compatible API + */ + private async testZhipuKey(apiKey: string): Promise<{valid: boolean, error?: string}> { + try { + // Zhipu uses OpenAI-compatible API with different base URL + const openai = new OpenAI({ + apiKey, + baseURL: API_URLS.zhipu + }); + // Make a simple API call to test the key + await openai.models.list(); + return { valid: true }; + } catch (error: any) { + console.error('Zhipu API key test failed:', error); + + let errorMessage = 'Unknown error validating Zhipu API key'; + + if (error.status === 401) { + errorMessage = 'Invalid API key. Please check your Zhipu key and try again.'; + } else if (error.status === 429) { + errorMessage = 'Rate limit exceeded. Your Zhipu API key has reached its request limit.'; + } else if (error.status === 500) { + errorMessage = 'Zhipu server error. Please try again later.'; + } else if (error.message) { + errorMessage = `Error: ${error.message}`; + } + + return { valid: false, error: errorMessage }; + } + } + + /** + * Test Alibaba Bailian API key + * Bailian uses OpenAI-compatible API + */ + private async testBailianKey(apiKey: string): Promise<{valid: boolean, error?: string}> { + try { + // Bailian uses OpenAI-compatible API with different base URL + const openai = new OpenAI({ + apiKey, + baseURL: API_URLS.bailian + }); + // Make a simple API call to test the key + await openai.models.list(); + return { valid: true }; + } catch (error: any) { + console.error('Bailian API key test failed:', error); + + let errorMessage = 'Unknown error validating Bailian API key'; + + if (error.status === 401) { + errorMessage = 'Invalid API key. Please check your Bailian/DashScope key and try again.'; + } else if (error.status === 429) { + errorMessage = 'Rate limit exceeded. Your Bailian API key has reached its request limit.'; + } else if (error.status === 500) { + errorMessage = 'Bailian server error. Please try again later.'; + } else if (error.message) { + errorMessage = `Error: ${error.message}`; + } + return { valid: false, error: errorMessage }; } } diff --git a/electron/ProcessingHelper.ts b/electron/ProcessingHelper.ts index 0dcd26f0..8b3d6a18 100644 --- a/electron/ProcessingHelper.ts +++ b/electron/ProcessingHelper.ts @@ -8,6 +8,14 @@ import { app, BrowserWindow, dialog } from "electron" import { OpenAI } from "openai" import { configHelper } from "./ConfigHelper" import Anthropic from '@anthropic-ai/sdk'; +import { jsonrepair } from 'jsonrepair'; + +// API URL constants for Chinese AI providers +const API_URLS = { + deepseek: 'https://api.deepseek.com', + zhipu: 'https://open.bigmodel.cn/api/paas/v4', + bailian: 'https://coding.dashscope.aliyuncs.com/v1' // Coding Plan 专属 URL +} as const; // Interface for Gemini API requests interface GeminiMessage { @@ -73,53 +81,82 @@ export class ProcessingHelper { private initializeAIClient(): void { try { const config = configHelper.loadConfig(); - + const apiKey = configHelper.getApiKeyForProvider(); + + // Reset all clients first + this.openaiClient = null; + this.geminiApiKey = null; + this.anthropicClient = null; + if (config.apiProvider === "openai") { - if (config.apiKey) { - this.openaiClient = new OpenAI({ - apiKey: config.apiKey, + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, timeout: 60000, // 60 second timeout maxRetries: 2 // Retry up to 2 times }); - this.geminiApiKey = null; - this.anthropicClient = null; console.log("OpenAI client initialized successfully"); } else { - this.openaiClient = null; - this.geminiApiKey = null; - this.anthropicClient = null; console.warn("No API key available, OpenAI client not initialized"); } - } else if (config.apiProvider === "gemini"){ + } else if (config.apiProvider === "gemini") { // Gemini client initialization - this.openaiClient = null; - this.anthropicClient = null; - if (config.apiKey) { - this.geminiApiKey = config.apiKey; + if (apiKey) { + this.geminiApiKey = apiKey; console.log("Gemini API key set successfully"); } else { - this.openaiClient = null; - this.geminiApiKey = null; - this.anthropicClient = null; console.warn("No API key available, Gemini client not initialized"); } } else if (config.apiProvider === "anthropic") { - // Reset other clients - this.openaiClient = null; - this.geminiApiKey = null; - if (config.apiKey) { + if (apiKey) { this.anthropicClient = new Anthropic({ - apiKey: config.apiKey, + apiKey: apiKey, timeout: 60000, maxRetries: 2 }); console.log("Anthropic client initialized successfully"); } else { - this.openaiClient = null; - this.geminiApiKey = null; - this.anthropicClient = null; console.warn("No API key available, Anthropic client not initialized"); } + } else if (config.apiProvider === "deepseek") { + // Deepseek uses OpenAI-compatible API + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, + baseURL: API_URLS.deepseek, + timeout: 60000, + maxRetries: 2 + }); + console.log("Deepseek client initialized successfully (OpenAI-compatible)"); + } else { + console.warn("No API key available, Deepseek client not initialized"); + } + } else if (config.apiProvider === "zhipu") { + // Zhipu/GLM uses OpenAI-compatible API + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, + baseURL: API_URLS.zhipu, + timeout: 60000, + maxRetries: 2 + }); + console.log("Zhipu/GLM client initialized successfully (OpenAI-compatible)"); + } else { + console.warn("No API key available, Zhipu client not initialized"); + } + } else if (config.apiProvider === "bailian") { + // Alibaba Bailian uses OpenAI-compatible API + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, + baseURL: API_URLS.bailian, + timeout: 120000, // 2 minute timeout for Bailian + maxRetries: 2 + }); + console.log("Bailian client initialized successfully (OpenAI-compatible)"); + } else { + console.warn("No API key available, Bailian client not initialized"); + } } } catch (error) { console.error("Failed to initialize AI client:", error); @@ -129,6 +166,144 @@ export class ProcessingHelper { } } + /** + * Fix malformed JSON from GLM API: + * 1. Single-quoted string values: "key": 'value' -> "key": "value" + * 2. Unescaped quotes inside string values: "例如:"abc"" -> "例如:\"abc\"" + * 3. Chinese curly quotes: " " -> escaped regular quotes + */ + private fixChineseQuotesInJson(text: string): string { + // Step 1: Replace single-quoted values with double-quoted values + let result = ''; + let i = 0; + + while (i < text.length) { + if (text[i] === ':') { + result += ':'; + i++; + + // Skip whitespace after colon + while (i < text.length && /\s/.test(text[i])) { + result += text[i]; + i++; + } + + // Check if the value starts with a single quote + if (i < text.length && text[i] === "'") { + // Find the matching closing single quote (followed by , } ] or end) + let endPos = -1; + let j = i + 1; + while (j < text.length) { + if (text[j] === "'") { + let k = j + 1; + while (k < text.length && /\s/.test(text[k])) k++; + if (k >= text.length || text[k] === ',' || text[k] === '}' || text[k] === ']') { + endPos = j; + break; + } + } + j++; + } + + if (endPos !== -1) { + const content = text.substring(i + 1, endPos); + const escaped = content.replace(/"/g, '\\"'); + result += '"' + escaped + '"'; + i = endPos + 1; + continue; + } + } + } else { + result += text[i]; + i++; + } + } + + // Step 2: Fix unescaped quotes inside JSON string values + // This handles cases like: "problem": "例如:"0.1" 和 "1.2"" + // The inner quotes need to be escaped + let finalResult = ''; + let inString = false; + i = 0; + + while (i < result.length) { + const char = result[i]; + + if (char === '"') { + // Check if this quote is escaped + let backslashCount = 0; + let j = i - 1; + while (j >= 0 && result[j] === '\\') { + backslashCount++; + j--; + } + const isEscaped = backslashCount % 2 === 1; + + if (!isEscaped) { + if (!inString) { + // Starting a string + inString = true; + finalResult += char; + } else { + // This might be the end of string OR an unescaped quote inside + // Look ahead to determine if this is really the end of the string + let k = i + 1; + while (k < result.length && /\s/.test(result[k])) k++; + + // If followed by : , } ] or end of input, it's a real string end + if (k >= result.length || result[k] === ':' || result[k] === ',' || + result[k] === '}' || result[k] === ']') { + inString = false; + finalResult += char; + } else { + // This is an unescaped quote inside the string - escape it + finalResult += '\\"'; + } + } + } else { + finalResult += char; + } + } + // Replace Chinese curly quotes with escaped regular quotes + else if (inString && (char === '\u201C' || char === '\u201D')) { + finalResult += '\\"'; + } + // Replace Chinese single quotes + else if (inString && (char === '\u2018' || char === '\u2019')) { + finalResult += "'"; + } + else { + finalResult += char; + } + i++; + } + + return finalResult; + } + + /** + * Make a Zhipu API call using native HTTP (bypasses OpenAI SDK quirks) + */ + private async callZhipuAPI( + messages: Array<{ role: string; content: string | Array<{ type: string; text?: string; image_url?: { url: string } }> }>, + model: string, + timeout: number = 60000 + ): Promise<{ choices: Array<{ message: { content: string } }> }> { + const zhipuApiKey = configHelper.getApiKeyForProvider("zhipu"); + const response = await axios.default.post( + `${API_URLS.zhipu}/chat/completions`, + { model, messages }, + { + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${zhipuApiKey}` + }, + timeout + } + ); + return response.data; + } + private async waitForInitialization( mainWindow: BrowserWindow ): Promise { @@ -459,63 +634,133 @@ export class ProcessingHelper { } let problemInfo; - - if (config.apiProvider === "openai") { - // Verify OpenAI client + + // OpenAI, Deepseek, Zhipu, and Bailian all use OpenAI-compatible API + if (config.apiProvider === "openai" || config.apiProvider === "deepseek" || config.apiProvider === "zhipu" || config.apiProvider === "bailian") { + // Verify OpenAI-compatible client if (!this.openaiClient) { this.initializeAIClient(); // Try to reinitialize - + if (!this.openaiClient) { + const providerName = config.apiProvider === "deepseek" ? "Deepseek" : + config.apiProvider === "zhipu" ? "Zhipu/GLM" : + config.apiProvider === "bailian" ? "Bailian" : "OpenAI"; return { success: false, - error: "OpenAI API key not configured or invalid. Please check your settings." + error: `${providerName} API key not configured or invalid. Please check your settings.` }; } } - // Use OpenAI for processing - const messages = [ - { - role: "system" as const, - content: "You are a coding challenge interpreter. Analyze the screenshot of the coding problem and extract all relevant information. Return the information in JSON format with these fields: problem_statement, constraints, example_input, example_output. Just return the structured JSON without any other text." - }, - { - role: "user" as const, - content: [ - { - type: "text" as const, - text: `Extract the coding problem details from these screenshots. Return in JSON format. Preferred coding language we gonna use for this problem is ${language}.` - }, - ...imageDataList.map(data => ({ - type: "image_url" as const, - image_url: { url: `data:image/png;base64,${data}` } - })) - ] + // Get the appropriate model for the provider + let extractionModel = config.extractionModel; + if (config.apiProvider === "deepseek") { + extractionModel = extractionModel || "deepseek-chat"; + } else if (config.apiProvider === "zhipu") { + // GLM-4V models support vision + extractionModel = extractionModel || "glm-4v-flash"; + } else if (config.apiProvider === "bailian") { + // Coding Plan 支持图片理解的模型 + extractionModel = extractionModel || "qwen3.5-plus"; + } else { + extractionModel = extractionModel || "gpt-4o"; + } + + // Build messages based on provider + let messages; + + let extractionResponse; + + if (config.apiProvider === "zhipu") { + // Zhipu GLM-4V: Use native HTTP request to bypass OpenAI SDK quirks + const systemPrompt = "You are a coding challenge interpreter. Analyze the screenshots of the coding problem and extract all relevant information. Return the information in JSON format with these fields: problem_statement, constraints, example_input, example_output. Just return the structured JSON without any other text."; + + // Build content array with text first, then ALL images + const contentArray: any[] = [ + { + type: "text", + text: `${systemPrompt}\n\nExtract the coding problem details from these screenshots. Return in JSON format. Preferred coding language we gonna use for this problem is ${language}.` + } + ]; + + // Add all screenshots to the request + for (const imageData of imageDataList) { + const imageUrl = imageData.startsWith('data:') ? imageData : `data:image/png;base64,${imageData}`; + contentArray.push({ + type: "image_url", + image_url: { url: imageUrl } + }); } - ]; - // Send to OpenAI Vision API - const extractionResponse = await this.openaiClient.chat.completions.create({ - model: config.extractionModel || "gpt-4o", - messages: messages, - max_tokens: 4000, - temperature: 0.2 - }); + console.log(`Solve mode: sending ${imageDataList.length} screenshots to Zhipu API for problem extraction`); + + const zhipuMessages = [ + { + role: "user", + content: contentArray + } + ]; + + // Increase timeout for multiple images (60s per image, minimum 120s) + const extractionTimeout = Math.max(120000, imageDataList.length * 60000); + console.log(`Problem extraction timeout: ${extractionTimeout / 1000}s for ${imageDataList.length} images`); + extractionResponse = await this.callZhipuAPI(zhipuMessages, extractionModel, extractionTimeout); + } else { + // OpenAI and Deepseek: standard format via OpenAI SDK + messages = [ + { + role: "system" as const, + content: "You are a coding challenge interpreter. Analyze the screenshot of the coding problem and extract all relevant information. Return the information in JSON format with these fields: problem_statement, constraints, example_input, example_output. Just return the structured JSON without any other text." + }, + { + role: "user" as const, + content: [ + { + type: "text" as const, + text: `Extract the coding problem details from these screenshots. Return in JSON format. Preferred coding language we gonna use for this problem is ${language}.` + }, + ...imageDataList.map(data => ({ + type: "image_url" as const, + image_url: { url: `data:image/png;base64,${data}` } + })) + ] + } + ]; + + // Send to OpenAI-compatible Vision API + extractionResponse = await this.openaiClient.chat.completions.create({ + model: extractionModel, + messages: messages, + max_tokens: 4000, + temperature: 0.2 + }); + } // Parse the response try { const responseText = extractionResponse.choices[0].message.content; - // Handle when OpenAI might wrap the JSON in markdown code blocks - const jsonText = responseText.replace(/```json|```/g, '').trim(); - problemInfo = JSON.parse(jsonText); + // Handle when AI might wrap the JSON in markdown code blocks + let jsonText = responseText.replace(/```json\n?|```\n?/g, '').trim(); + // Also try to extract JSON from text if AI added extra content + const jsonMatch = jsonText.match(/\{[\s\S]*\}/); + if (jsonMatch) { + jsonText = jsonMatch[0]; + } + // Fix Chinese quotes and single-quoted arrays using character traversal + // This safely handles Chinese quotes (U+201C, U+201D) inside JSON strings + jsonText = this.fixChineseQuotesInJson(jsonText); + + // Use jsonrepair to fix remaining issues (handles single quotes, trailing commas, etc.) + const repairedJson = jsonrepair(jsonText); + problemInfo = JSON.parse(repairedJson); } catch (error) { - console.error("Error parsing OpenAI response:", error); + console.error("Error parsing API response:", error); return { success: false, error: "Failed to parse problem information. Please try again or use clearer screenshots." }; } - } else if (config.apiProvider === "gemini") { + } else if (config.apiProvider === "gemini") { // Use Gemini API if (!this.geminiApiKey) { return { @@ -735,57 +980,99 @@ export class ProcessingHelper { // Create prompt for solution generation const promptText = ` -Generate a detailed solution for the following coding problem: +请为以下编程题目提供详细的解答。**重要:除了代码本身,所有文字说明必须使用中文!** -PROBLEM STATEMENT: +【题目描述】 ${problemInfo.problem_statement} -CONSTRAINTS: -${problemInfo.constraints || "No specific constraints provided."} +【约束条件】 +${problemInfo.constraints || "未提供具体约束条件。"} -EXAMPLE INPUT: -${problemInfo.example_input || "No example input provided."} +【输入示例】 +${problemInfo.example_input || "未提供输入示例。"} -EXAMPLE OUTPUT: -${problemInfo.example_output || "No example output provided."} +【输出示例】 +${problemInfo.example_output || "未提供输出示例。"} -LANGUAGE: ${language} +【编程语言】${language} -I need the response in the following format: -1. Code: A clean, optimized implementation in ${language} -2. Your Thoughts: A list of key insights and reasoning behind your approach -3. Time complexity: O(X) with a detailed explanation (at least 2 sentences) -4. Space complexity: O(X) with a detailed explanation (at least 2 sentences) +请严格按以下 JSON 格式回答(不要添加任何其他内容): -For complexity explanations, please be thorough. For example: "Time complexity: O(n) because we iterate through the array only once. This is optimal as we need to examine each element at least once to find the solution." or "Space complexity: O(n) because in the worst case, we store all elements in the hashmap. The additional space scales linearly with the input size." +\`\`\`json +{ + "code": "你的 ${language} 代码(代码注释用中文)", + "thoughts": [ + "**核心算法**:描述你使用的主要算法或数据结构...", + "**优化策略**:描述任何优化或剪枝策略..." + ], + "time_complexity": "O(X) - 用中文详细解释原因,说明循环次数、操作复杂度等(至少2句话)", + "space_complexity": "O(X) - 用中文详细解释原因,说明使用了哪些额外空间(至少2句话)" +} +\`\`\` -Your solution should be efficient, well-commented, and handle edge cases. +**重要**: +1. 必须返回有效的 JSON 格式 +2. code 字段中的换行符用 \\n 表示 +3. 解题思路、复杂度分析必须是中文 +4. 复杂度分析必须基于你生成的代码,不要使用模板答案 `; let responseContent; - - if (config.apiProvider === "openai") { - // OpenAI processing + + // OpenAI, Deepseek, Zhipu, and Bailian all use OpenAI-compatible API + if (config.apiProvider === "openai" || config.apiProvider === "deepseek" || config.apiProvider === "zhipu" || config.apiProvider === "bailian") { if (!this.openaiClient) { + const providerName = config.apiProvider === "deepseek" ? "Deepseek" : + config.apiProvider === "zhipu" ? "Zhipu/GLM" : + config.apiProvider === "bailian" ? "Bailian" : "OpenAI"; return { success: false, - error: "OpenAI API key not configured. Please check your settings." + error: `${providerName} API key not configured. Please check your settings.` }; } - - // Send to OpenAI API - const solutionResponse = await this.openaiClient.chat.completions.create({ - model: config.solutionModel || "gpt-4o", - messages: [ - { role: "system", content: "You are an expert coding interview assistant. Provide clear, optimal solutions with detailed explanations." }, - { role: "user", content: promptText } - ], - max_tokens: 4000, - temperature: 0.2 - }); - responseContent = solutionResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { + // Get the appropriate model for the provider + // Re-read config to get latest solutionModel + const latestConfig = configHelper.loadConfig(); + let solutionModel = latestConfig.solutionModel || config.solutionModel; + if (config.apiProvider === "deepseek") { + solutionModel = solutionModel || "deepseek-chat"; + } else if (config.apiProvider === "zhipu") { + solutionModel = solutionModel || "glm-4-flash"; + } else if (config.apiProvider === "bailian") { + solutionModel = solutionModel || "qwen3.5-plus"; + } else { + solutionModel = solutionModel || "gpt-4o"; + } + + let solutionResponse; + + if (config.apiProvider === "zhipu") { + // Zhipu: Use native HTTP request + const zhipuMessages = [ + { role: "user", content: `你是一位资深的编程面试助手。请提供清晰、最优的解决方案,并附带详细的解释。\n\n${promptText}` } + ]; + + const zhipuResponse = await this.callZhipuAPI(zhipuMessages, solutionModel, 120000); + responseContent = zhipuResponse.choices[0].message.content; + } else { + // OpenAI/Deepseek/Bailian: Use OpenAI SDK + const solutionMessages = [ + { role: "system" as const, content: "你是一位资深的编程面试助手。请提供清晰、最优的解决方案,并附带详细的解释。" }, + { role: "user" as const, content: promptText } + ]; + + // Send to OpenAI-compatible API + solutionResponse = await this.openaiClient.chat.completions.create({ + model: solutionModel, + messages: solutionMessages, + max_tokens: 4000, + temperature: 0.2 + }); + + responseContent = solutionResponse.choices[0].message.content; + } + } else if (config.apiProvider === "gemini") { // Gemini processing if (!this.geminiApiKey) { return { @@ -801,7 +1088,7 @@ Your solution should be efficient, well-commented, and handle edge cases. role: "user", parts: [ { - text: `You are an expert coding interview assistant. Provide a clear, optimal solution with detailed explanations for this problem:\n\n${promptText}` + text: `你是一位资深的编程面试助手。请为以下问题提供清晰、最优的解决方案,并附带详细的解释:\n\n${promptText}` } ] } @@ -850,7 +1137,7 @@ Your solution should be efficient, well-commented, and handle edge cases. content: [ { type: "text" as const, - text: `You are an expert coding interview assistant. Provide a clear, optimal solution with detailed explanations for this problem:\n\n${promptText}` + text: `你是一位资深的编程面试助手。请为以下问题提供清晰、最优的解决方案,并附带详细的解释:\n\n${promptText}` } ] } @@ -888,72 +1175,48 @@ Your solution should be efficient, well-commented, and handle edge cases. } } - // Extract parts from the response - const codeMatch = responseContent.match(/```(?:\w+)?\s*([\s\S]*?)```/); - const code = codeMatch ? codeMatch[1].trim() : responseContent; - - // Extract thoughts, looking for bullet points or numbered lists - const thoughtsRegex = /(?:Thoughts:|Key Insights:|Reasoning:|Approach:)([\s\S]*?)(?:Time complexity:|$)/i; - const thoughtsMatch = responseContent.match(thoughtsRegex); - let thoughts: string[] = []; - - if (thoughtsMatch && thoughtsMatch[1]) { - // Extract bullet points or numbered items - const bulletPoints = thoughtsMatch[1].match(/(?:^|\n)\s*(?:[-*•]|\d+\.)\s*(.*)/g); - if (bulletPoints) { - thoughts = bulletPoints.map(point => - point.replace(/^\s*(?:[-*•]|\d+\.)\s*/, '').trim() - ).filter(Boolean); - } else { - // If no bullet points found, split by newlines and filter empty lines - thoughts = thoughtsMatch[1].split('\n') - .map((line) => line.trim()) - .filter(Boolean); - } - } - - // Extract complexity information - const timeComplexityPattern = /Time complexity:?\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:Space complexity|$))/i; - const spaceComplexityPattern = /Space complexity:?\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:[A-Z]|$))/i; - - let timeComplexity = "O(n) - Linear time complexity because we only iterate through the array once. Each element is processed exactly one time, and the hashmap lookups are O(1) operations."; - let spaceComplexity = "O(n) - Linear space complexity because we store elements in the hashmap. In the worst case, we might need to store all elements before finding the solution pair."; - - const timeMatch = responseContent.match(timeComplexityPattern); - if (timeMatch && timeMatch[1]) { - timeComplexity = timeMatch[1].trim(); - if (!timeComplexity.match(/O\([^)]+\)/i)) { - timeComplexity = `O(n) - ${timeComplexity}`; - } else if (!timeComplexity.includes('-') && !timeComplexity.includes('because')) { - const notationMatch = timeComplexity.match(/O\([^)]+\)/i); - if (notationMatch) { - const notation = notationMatch[0]; - const rest = timeComplexity.replace(notation, '').trim(); - timeComplexity = `${notation} - ${rest}`; - } - } - } - - const spaceMatch = responseContent.match(spaceComplexityPattern); - if (spaceMatch && spaceMatch[1]) { - spaceComplexity = spaceMatch[1].trim(); - if (!spaceComplexity.match(/O\([^)]+\)/i)) { - spaceComplexity = `O(n) - ${spaceComplexity}`; - } else if (!spaceComplexity.includes('-') && !spaceComplexity.includes('because')) { - const notationMatch = spaceComplexity.match(/O\([^)]+\)/i); - if (notationMatch) { - const notation = notationMatch[0]; - const rest = spaceComplexity.replace(notation, '').trim(); - spaceComplexity = `${notation} - ${rest}`; - } + // Parse JSON response from AI + let parsedResponse: { + code: string; + thoughts: string[]; + time_complexity: string; + space_complexity: string; + }; + + try { + // Extract JSON from response (may be wrapped in ```json ... ```) + let jsonText = responseContent; + const jsonMatch = responseContent.match(/```(?:json)?\s*([\s\S]*?)```/); + if (jsonMatch) { + jsonText = jsonMatch[1].trim(); } + + // Fix common JSON issues using jsonrepair + const fixedJson = this.fixChineseQuotesInJson(jsonText); + parsedResponse = JSON.parse(fixedJson); + } catch (parseError) { + // Fallback: try to extract using regex if JSON parsing fails + console.warn("JSON parsing failed, attempting regex fallback:", parseError); + + const codeMatch = responseContent.match(/```(?:\w+)?\s*([\s\S]*?)```/); + const code = codeMatch ? codeMatch[1].trim() : responseContent; + + parsedResponse = { + code: code, + thoughts: ["AI 返回格式异常,无法解析解题思路"], + time_complexity: "无法解析 - AI 返回格式不符合预期,请重新生成", + space_complexity: "无法解析 - AI 返回格式不符合预期,请重新生成" + }; } + // Validate and format the response const formattedResponse = { - code: code, - thoughts: thoughts.length > 0 ? thoughts : ["Solution approach based on efficiency and readability"], - time_complexity: timeComplexity, - space_complexity: spaceComplexity + code: parsedResponse.code || "", + thoughts: Array.isArray(parsedResponse.thoughts) && parsedResponse.thoughts.length > 0 + ? parsedResponse.thoughts + : ["基于效率和可读性的解题方法"], + time_complexity: parsedResponse.time_complexity || "未提供时间复杂度分析", + space_complexity: parsedResponse.space_complexity || "未提供空间复杂度分析" }; return { success: true, data: formattedResponse }; @@ -1008,56 +1271,65 @@ Your solution should be efficient, well-commented, and handle edge cases. const imageDataList = screenshots.map(screenshot => screenshot.data); let debugContent; - - if (config.apiProvider === "openai") { + + // OpenAI, Deepseek, Zhipu, and Bailian all use OpenAI-compatible API + if (config.apiProvider === "openai" || config.apiProvider === "deepseek" || config.apiProvider === "zhipu" || config.apiProvider === "bailian") { if (!this.openaiClient) { + const providerName = config.apiProvider === "deepseek" ? "Deepseek" : + config.apiProvider === "zhipu" ? "Zhipu/GLM" : + config.apiProvider === "bailian" ? "Bailian" : "OpenAI"; return { success: false, - error: "OpenAI API key not configured. Please check your settings." + error: `${providerName} API key not configured. Please check your settings.` }; } - - const messages = [ - { - role: "system" as const, - content: `You are a coding interview assistant helping debug and improve solutions. Analyze these screenshots which include either error messages, incorrect outputs, or test cases, and provide detailed debugging help. -Your response MUST follow this exact structure with these section headers (use ### for headers): -### Issues Identified -- List each issue as a bullet point with clear explanation - -### Specific Improvements and Corrections -- List specific code changes needed as bullet points + // Get the appropriate model for the provider + // IMPORTANT: Debugging requires vision models since we're processing screenshots + let debuggingModel = config.debuggingModel; + if (config.apiProvider === "deepseek") { + debuggingModel = debuggingModel || "deepseek-chat"; + } else if (config.apiProvider === "zhipu") { + // GLM-4V models support vision - MUST use vision model for debugging + // Force vision model even if user configured a non-vision model + const isVisionModel = debuggingModel && (debuggingModel.includes("4v") || debuggingModel.includes("4V")); + debuggingModel = isVisionModel ? debuggingModel : "glm-4v-flash"; + } else if (config.apiProvider === "bailian") { + // Coding Plan 支持图片理解的模型 + debuggingModel = debuggingModel || "qwen3.5-plus"; + } else { + debuggingModel = debuggingModel || "gpt-4o"; + } -### Optimizations -- List any performance optimizations if applicable + const systemPrompt = `你是一个编程面试助手,帮助调试和改进解决方案。分析截图中的错误信息、错误输出或测试用例,提供详细的调试帮助。 + +你的回复必须严格按照以下 JSON 格式(不要添加任何其他内容): + +{ + "fixed_code": "完整的修复后代码,纯代码文本,不要包含 markdown 代码块标记", + "issues": [ + "问题1:具体描述发现的第一个问题", + "问题2:具体描述发现的第二个问题" + ], + "changes": [ + "修改1:描述你做的第一个修改", + "修改2:描述你做的第二个修改" + ], + "explanation": "用中文详细解释为什么需要这些修改,以及修改后代码如何解决原来的问题" +} -### Explanation of Changes Needed -Here provide a clear explanation of why the changes are needed +重要要求: +1. fixed_code 必须是完整的、可直接提交运行的代码,不要用 \`\`\` 包裹 +2. 代码注释用中文 +3. 所有分析内容用中文 +4. 只返回 JSON,不要有其他任何内容`; -### Key Points -- Summary bullet points of the most important takeaways + const userPrompt = `我正在解决这道编程题:「${problemInfo.problem_statement}」,使用 ${language} 语言。 -If you include code examples, use proper markdown code blocks with language specification (e.g. \`\`\`java).` - }, - { - role: "user" as const, - content: [ - { - type: "text" as const, - text: `I'm solving this coding problem: "${problemInfo.problem_statement}" in ${language}. I need help with debugging or improving my solution. Here are screenshots of my code, the errors or test cases. Please provide a detailed analysis with: -1. What issues you found in my code -2. Specific improvements and corrections -3. Any optimizations that would make the solution better -4. A clear explanation of the changes needed` - }, - ...imageDataList.map(data => ({ - type: "image_url" as const, - image_url: { url: `data:image/png;base64,${data}` } - })) - ] - } - ]; +截图中包含我的代码和错误信息/测试结果。请: +1. 分析我的代码存在的问题 +2. 提供完整的修复后代码(不是代码片段,是完整可运行的代码) +3. 解释你做了哪些修改以及为什么`; if (mainWindow) { mainWindow.webContents.send("processing-status", { @@ -1066,15 +1338,71 @@ If you include code examples, use proper markdown code blocks with language spec }); } - const debugResponse = await this.openaiClient.chat.completions.create({ - model: config.debuggingModel || "gpt-4o", - messages: messages, - max_tokens: 4000, - temperature: 0.2 - }); - - debugContent = debugResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { + if (config.apiProvider === "zhipu") { + // Zhipu: Use native HTTP request for vision + // Build content array with text first, then ALL images + const contentArray: any[] = [ + { + type: "text", + text: `${systemPrompt}\n\n${userPrompt}` + } + ]; + + // Add all screenshots to the request + for (const imageData of imageDataList) { + const imageUrl = imageData.startsWith('data:') ? imageData : `data:image/png;base64,${imageData}`; + contentArray.push({ + type: "image_url", + image_url: { url: imageUrl } + }); + } + + console.log(`Debug mode: sending ${imageDataList.length} screenshots to Zhipu API`); + + const zhipuMessages = [ + { + role: "user", + content: contentArray + } + ]; + + // Increase timeout for multiple images (60s per image, minimum 120s) + const debugTimeout = Math.max(120000, imageDataList.length * 60000); + console.log(`Debug mode timeout: ${debugTimeout / 1000}s for ${imageDataList.length} images`); + const zhipuResponse = await this.callZhipuAPI(zhipuMessages, debuggingModel, debugTimeout); + debugContent = zhipuResponse.choices[0].message.content; + } else { + // OpenAI/Deepseek: Use OpenAI SDK + const debugMessages = [ + { + role: "system" as const, + content: systemPrompt + }, + { + role: "user" as const, + content: [ + { + type: "text" as const, + text: userPrompt + }, + ...imageDataList.map(data => ({ + type: "image_url" as const, + image_url: { url: `data:image/png;base64,${data}` } + })) + ] + } + ]; + + const debugResponse = await this.openaiClient.chat.completions.create({ + model: debuggingModel, + messages: debugMessages, + max_tokens: 4000, + temperature: 0.2 + }); + + debugContent = debugResponse.choices[0].message.content; + } + } else if (config.apiProvider === "gemini") { if (!this.geminiApiKey) { return { success: false, @@ -1254,33 +1582,93 @@ If you include code examples, use proper markdown code blocks with language spec }); } - let extractedCode = "// Debug mode - see analysis below"; - const codeMatch = debugContent.match(/```(?:[a-zA-Z]+)?([\s\S]*?)```/); - if (codeMatch && codeMatch[1]) { - extractedCode = codeMatch[1].trim(); - } - + // Try to parse JSON response from debug mode + let extractedCode = "// 调试模式 - 见下方分析"; + let thoughts: string[] = ["基于截图的调试分析"]; let formattedDebugContent = debugContent; - - if (!debugContent.includes('# ') && !debugContent.includes('## ')) { - formattedDebugContent = debugContent - .replace(/issues identified|problems found|bugs found/i, '## Issues Identified') - .replace(/code improvements|improvements|suggested changes/i, '## Code Improvements') - .replace(/optimizations|performance improvements/i, '## Optimizations') - .replace(/explanation|detailed analysis/i, '## Explanation'); + + // Helper function to extract code from markdown code block + const extractCodeFromMarkdown = (text: string): string => { + const codeMatch = text.match(/```(?:[a-zA-Z]+)?\s*([\s\S]*?)```/); + if (codeMatch && codeMatch[1]) { + return codeMatch[1].trim(); + } + // Try with double backticks (AI sometimes returns ``go instead of ```go) + const codeMatch2 = text.match(/``(?:[a-zA-Z]+)?\s*([\s\S]*?)``/); + if (codeMatch2 && codeMatch2[1]) { + return codeMatch2[1].trim(); + } + return text; + }; + + try { + // First, try to find JSON object pattern directly (handles nested code blocks better) + let debugData: any = null; + + // Try to extract JSON using a more robust pattern + const jsonObjectMatch = debugContent.match(/\{[\s\S]*"fixed_code"[\s\S]*"explanation"[\s\S]*\}/); + if (jsonObjectMatch) { + let jsonStr = jsonObjectMatch[0]; + + // Fix common JSON issues (Chinese quotes, etc.) + jsonStr = this.fixChineseQuotesInJson(jsonStr); + + // Try to parse and repair JSON + const { jsonrepair } = require('jsonrepair'); + const repairedJson = jsonrepair(jsonStr); + debugData = JSON.parse(repairedJson); + } + + if (debugData) { + // Extract fixed code + if (debugData.fixed_code) { + // Remove markdown code block markers if present + extractedCode = extractCodeFromMarkdown(debugData.fixed_code); + } + + // Build formatted analysis content + const sections: string[] = []; + + if (debugData.issues && debugData.issues.length > 0) { + sections.push("## 发现的问题\n" + debugData.issues.map((issue: string) => `- ${issue}`).join("\n")); + thoughts = debugData.issues.slice(0, 3); + } + + if (debugData.changes && debugData.changes.length > 0) { + sections.push("## 修改内容\n" + debugData.changes.map((change: string) => `- ${change}`).join("\n")); + } + + if (debugData.explanation) { + sections.push("## 详细解释\n" + debugData.explanation); + } + + if (sections.length > 0) { + formattedDebugContent = sections.join("\n\n"); + } + + console.log("Debug JSON parsed successfully"); + } else { + throw new Error("No valid JSON object found"); + } + } catch (parseError) { + console.warn("Failed to parse debug JSON, falling back to regex extraction:", parseError); + + // Fallback: try to extract code from markdown code block + extractedCode = extractCodeFromMarkdown(debugContent); + + // Fallback: extract bullet points as thoughts + const bulletPoints = debugContent.match(/(?:^|\n)[ ]*(?:[-*•]|\d+\.)[ ]+([^\n]+)/g); + if (bulletPoints) { + thoughts = bulletPoints.map(point => point.replace(/^[ ]*(?:[-*•]|\d+\.)[ ]+/, '').trim()).slice(0, 5); + } } - const bulletPoints = formattedDebugContent.match(/(?:^|\n)[ ]*(?:[-*•]|\d+\.)[ ]+([^\n]+)/g); - const thoughts = bulletPoints - ? bulletPoints.map(point => point.replace(/^[ ]*(?:[-*•]|\d+\.)[ ]+/, '').trim()).slice(0, 5) - : ["Debug analysis based on your screenshots"]; - const response = { code: extractedCode, debug_analysis: formattedDebugContent, thoughts: thoughts, - time_complexity: "N/A - Debug mode", - space_complexity: "N/A - Debug mode" + time_complexity: "N/A - 调试模式", + space_complexity: "N/A - 调试模式" }; return { success: true, data: response }; diff --git a/electron/shortcuts.ts b/electron/shortcuts.ts index a6fa5ebb..40650a3d 100644 --- a/electron/shortcuts.ts +++ b/electron/shortcuts.ts @@ -77,6 +77,16 @@ export class ShortcutsHelper { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("reset-view") mainWindow.webContents.send("reset") + + // Reset window position to center of screen + const { screen } = require("electron") + const primaryDisplay = screen.getPrimaryDisplay() + const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize + const [windowWidth, windowHeight] = mainWindow.getSize() + const x = Math.round((screenWidth - windowWidth) / 2) + const y = Math.round((screenHeight - windowHeight) / 2) + mainWindow.setPosition(x, y) + console.log(`Window position reset to center: (${x}, ${y})`) } }) diff --git a/package-lock.json b/package-lock.json index da87e3a0..4f6cfbca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,15 +19,18 @@ "@radix-ui/react-toast": "^1.2.2", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.64.0", + "@types/dompurify": "^3.0.5", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "diff": "^7.0.0", + "dompurify": "^3.3.1", "dotenv": "^16.4.7", "electron-log": "^5.2.4", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", "form-data": "^4.0.1", + "jsonrepair": "^3.13.2", "lucide-react": "^0.460.0", "openai": "^4.28.4", "react": "^18.2.0", @@ -2847,6 +2850,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/electron-store": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/electron-store/-/electron-store-1.3.1.tgz", @@ -3037,6 +3049,12 @@ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5451,6 +5469,15 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dot-prop": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", @@ -7809,6 +7836,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonrepair": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.2.tgz", + "integrity": "sha512-Leuly0nbM4R+S5SVJk3VHfw1oxnlEK9KygdZvfUtEtTawNDyzB4qa1xWTmFt1aeoA7sXZkVTRuIixJ8bAvqVUg==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 1fffcfb5..608a4f1d 100644 --- a/package.json +++ b/package.json @@ -132,15 +132,18 @@ "@radix-ui/react-toast": "^1.2.2", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.64.0", + "@types/dompurify": "^3.0.5", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "diff": "^7.0.0", + "dompurify": "^3.3.1", "dotenv": "^16.4.7", "electron-log": "^5.2.4", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", "form-data": "^4.0.1", + "jsonrepair": "^3.13.2", "lucide-react": "^0.460.0", "openai": "^4.28.4", "react": "^18.2.0", diff --git a/src/_pages/Solutions.tsx b/src/_pages/Solutions.tsx index e1939451..176a5cb4 100644 --- a/src/_pages/Solutions.tsx +++ b/src/_pages/Solutions.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef } from "react" import { useQuery, useQueryClient } from "@tanstack/react-query" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" +import DOMPurify from "dompurify" import ScreenshotQueue from "../components/Queue/ScreenshotQueue" @@ -15,29 +16,48 @@ import { COMMAND_KEY } from "../utils/platform" export const ContentSection = ({ title, content, - isLoading + isLoading, + collapsible = false, + defaultCollapsed = false }: { title: string content: React.ReactNode isLoading: boolean -}) => ( -
-

- {title} -

- {isLoading ? ( -
-

- Extracting problem statement... -

-
- ) : ( -
- {content} -
- )} -
-) + collapsible?: boolean + defaultCollapsed?: boolean +}) => { + const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); + + return ( +
+

collapsible && setIsCollapsed(!isCollapsed)} + > + {collapsible && ( + + )} + {title} + {collapsible && ( + + {isCollapsed ? '(点击展开)' : '(点击收起)'} + + )} +

+ {isLoading ? ( +
+

+ Extracting problem statement... +

+
+ ) : ( +
+ {content} +
+ )} +
+ ); +} const SolutionSection = ({ title, content, @@ -524,20 +544,33 @@ const Solutions: React.FC = ({ {solutionData && ( <>
- {thoughtsData.map((thought, index) => ( -
-
-
{thought}
-
- ))} + {thoughtsData.map((thought, index) => { + // Simple markdown rendering: **bold** -> bold + const formattedThought = thought + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1'); + // Sanitize HTML to prevent XSS attacks from AI-generated content + const sanitizedHtml = DOMPurify.sanitize(formattedThought, { + ALLOWED_TAGS: ['strong', 'code'], + ALLOWED_ATTR: ['class'] + }); + return ( +
+
+
+
+ ); + })}
) diff --git a/src/components/Settings/SettingsDialog.tsx b/src/components/Settings/SettingsDialog.tsx index 463ea120..64c95b5e 100644 --- a/src/components/Settings/SettingsDialog.tsx +++ b/src/components/Settings/SettingsDialog.tsx @@ -13,7 +13,7 @@ import { Button } from "../ui/button"; import { Settings } from "lucide-react"; import { useToast } from "../../contexts/toast"; -type APIProvider = "openai" | "gemini" | "anthropic"; +type APIProvider = "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu" | "bailian"; type AIModel = { id: string; @@ -28,6 +28,9 @@ type ModelCategory = { openaiModels: AIModel[]; geminiModels: AIModel[]; anthropicModels: AIModel[]; + deepseekModels: AIModel[]; + zhipuModels: AIModel[]; + bailianModels: AIModel[]; }; // Define available models for each category @@ -76,6 +79,47 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + deepseekModels: [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + description: "Best for general coding tasks" + }, + { + id: "deepseek-coder", + name: "DeepSeek Coder", + description: "Specialized for code generation" + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + description: "Advanced reasoning capabilities" + } + ], + zhipuModels: [ + { + id: "glm-4v-flash", + name: "GLM-4V Flash", + description: "Fast vision model for screenshots" + }, + { + id: "glm-4v-plus", + name: "GLM-4V Plus", + description: "Enhanced vision model" + } + ], + bailianModels: [ + { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + description: "推荐模型,支持图片理解" + }, + { + id: "kimi-k2.5", + name: "Kimi K2.5", + description: "Kimi 最新模型,支持图片理解" + } ] }, { @@ -122,6 +166,82 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + deepseekModels: [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + description: "Best for general coding tasks" + }, + { + id: "deepseek-coder", + name: "DeepSeek Coder", + description: "Specialized for code generation" + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + description: "Advanced reasoning capabilities" + } + ], + zhipuModels: [ + { + id: "glm-4-flash", + name: "GLM-4 Flash", + description: "Fast and efficient text model" + }, + { + id: "glm-4-plus", + name: "GLM-4 Plus", + description: "Enhanced text model" + }, + { + id: "glm-4.5", + name: "GLM-4.5", + description: "Latest GLM-4.5 model (recommended)" + }, + { + id: "glm-5", + name: "GLM-5", + description: "Latest GLM-5 series" + } + ], + bailianModels: [ + { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + description: "推荐模型,支持图片理解" + }, + { + id: "kimi-k2.5", + name: "Kimi K2.5", + description: "Kimi 最新模型,支持图片理解" + }, + { + id: "glm-5", + name: "GLM-5", + description: "智谱 GLM-5" + }, + { + id: "MiniMax-M2.5", + name: "MiniMax M2.5", + description: "MiniMax 最新模型" + }, + { + id: "qwen3-coder-plus", + name: "Qwen3 Coder Plus", + description: "代码专用模型" + }, + { + id: "qwen3-coder-next", + name: "Qwen3 Coder Next", + description: "下一代代码模型" + }, + { + id: "glm-4.7", + name: "GLM-4.7", + description: "智谱 GLM-4.7" + } ] }, { @@ -168,6 +288,47 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + deepseekModels: [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + description: "Best for debugging and code analysis" + }, + { + id: "deepseek-coder", + name: "DeepSeek Coder", + description: "Specialized for code analysis" + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + description: "Advanced reasoning for complex bugs" + } + ], + zhipuModels: [ + { + id: "glm-4v-flash", + name: "GLM-4V Flash", + description: "Fast vision model for debugging screenshots" + }, + { + id: "glm-4v-plus", + name: "GLM-4V Plus", + description: "Enhanced vision model" + } + ], + bailianModels: [ + { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + description: "推荐模型,支持图片理解" + }, + { + id: "kimi-k2.5", + name: "Kimi K2.5", + description: "Kimi 最新模型,支持图片理解" + } ] } ]; @@ -203,6 +364,35 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia } }; + // Store all provider API keys + const [providerApiKeys, setProviderApiKeys] = useState<{ + openai?: string; + gemini?: string; + anthropic?: string; + deepseek?: string; + zhipu?: string; + bailian?: string; + }>({}); + + // Helper to get API key for a specific provider + const getApiKeyForProvider = (provider: APIProvider, config: { + apiKey?: string; + openaiApiKey?: string; + geminiApiKey?: string; + anthropicApiKey?: string; + deepseekApiKey?: string; + zhipuApiKey?: string; + bailianApiKey?: string; + }): string => { + if (provider === "openai" && config.openaiApiKey) return config.openaiApiKey; + if (provider === "gemini" && config.geminiApiKey) return config.geminiApiKey; + if (provider === "anthropic" && config.anthropicApiKey) return config.anthropicApiKey; + if (provider === "deepseek" && config.deepseekApiKey) return config.deepseekApiKey; + if (provider === "zhipu" && config.zhipuApiKey) return config.zhipuApiKey; + if (provider === "bailian" && config.bailianApiKey) return config.bailianApiKey; + return config.apiKey || ""; + }; + // Load current config on dialog open useEffect(() => { if (open) { @@ -213,13 +403,36 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia extractionModel?: string; solutionModel?: string; debuggingModel?: string; + openaiApiKey?: string; + geminiApiKey?: string; + anthropicApiKey?: string; + deepseekApiKey?: string; + zhipuApiKey?: string; + bailianApiKey?: string; } window.electronAPI .getConfig() .then((config: Config) => { - setApiKey(config.apiKey || ""); - setApiProvider(config.apiProvider || "openai"); + const provider = config.apiProvider || "openai"; + console.log("DEBUG: Loading config", { + provider, + extractionModel: config.extractionModel, + solutionModel: config.solutionModel, + debuggingModel: config.debuggingModel + }); + // Store all provider keys + setProviderApiKeys({ + openai: config.openaiApiKey || "", + gemini: config.geminiApiKey || "", + anthropic: config.anthropicApiKey || "", + deepseek: config.deepseekApiKey || "", + zhipu: config.zhipuApiKey || "", + bailian: config.bailianApiKey || "", + }); + // Set current provider's API key + setApiKey(getApiKeyForProvider(provider, config)); + setApiProvider(provider); setExtractionModel(config.extractionModel || "gpt-4o"); setSolutionModel(config.solutionModel || "gpt-4o"); setDebuggingModel(config.debuggingModel || "gpt-4o"); @@ -236,8 +449,18 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia // Handle API provider change const handleProviderChange = (provider: APIProvider) => { + // Save current API key to the current provider before switching + setProviderApiKeys(prev => ({ + ...prev, + [apiProvider]: apiKey + })); + + // Switch to new provider setApiProvider(provider); - + + // Load the API key for the new provider + setApiKey(providerApiKeys[provider] || ""); + // Reset models to defaults when changing provider if (provider === "openai") { setExtractionModel("gpt-4o"); @@ -251,6 +474,18 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia setExtractionModel("claude-3-7-sonnet-20250219"); setSolutionModel("claude-3-7-sonnet-20250219"); setDebuggingModel("claude-3-7-sonnet-20250219"); + } else if (provider === "deepseek") { + setExtractionModel("deepseek-chat"); + setSolutionModel("deepseek-chat"); + setDebuggingModel("deepseek-chat"); + } else if (provider === "zhipu") { + setExtractionModel("glm-4v-flash"); + setSolutionModel("glm-4-flash"); + setDebuggingModel("glm-4v-flash"); + } else if (provider === "bailian") { + setExtractionModel("qwen3.5-plus"); + setSolutionModel("qwen3.5-plus"); + setDebuggingModel("qwen3.5-plus"); } }; @@ -387,13 +622,78 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia
+
+
handleProviderChange("deepseek")} + > +
+
+
+

DeepSeek

+

DeepSeek models

+
+
+
+
handleProviderChange("zhipu")} + > +
+
+
+

智谱 GLM

+

GLM-4 models

+
+
+
+
handleProviderChange("bailian")} + > +
+
+
+

百炼 Coding

+

Coding Plan

+
+
+
+
setApiKey(e.target.value)} placeholder={ - apiProvider === "openai" ? "sk-..." : + apiProvider === "openai" ? "sk-..." : apiProvider === "gemini" ? "Enter your Gemini API key" : - "sk-ant-..." + apiProvider === "anthropic" ? "sk-ant-..." : + apiProvider === "deepseek" ? "sk-..." : + apiProvider === "zhipu" ? "Enter your Zhipu API key" : + "sk-... (百炼 DashScope API Key)" } className="bg-black/50 border-white/10 text-white" /> @@ -413,46 +716,89 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia

)}

- Your API key is stored locally and never sent to any server except {apiProvider === "openai" ? "OpenAI" : "Google"} + Your API key is stored locally and never sent to any server except { + apiProvider === "openai" ? "OpenAI" : + apiProvider === "gemini" ? "Google" : + apiProvider === "anthropic" ? "Anthropic" : + apiProvider === "deepseek" ? "DeepSeek" : + apiProvider === "zhipu" ? "智谱 AI" : + "阿里云百炼" + }

Don't have an API key?

{apiProvider === "openai" ? ( <> -

1. Create an account at

-

2. Go to section

3. Create a new secret key and paste it here

- ) : apiProvider === "gemini" ? ( + ) : apiProvider === "gemini" ? ( <> -

1. Create an account at

-

2. Go to the section

3. Create a new API key and paste it here

- ) : ( + ) : apiProvider === "anthropic" ? ( <> -

1. Create an account at

-

2. Go to the section +

+

3. Create a new API key and paste it here

+ + ) : apiProvider === "deepseek" ? ( + <> +

1. Create an account at +

+

2. Go to the section

3. Create a new API key and paste it here

+ ) : apiProvider === "zhipu" ? ( + <> +

1. 在 注册账号 +

+

2. 进入 页面 +

+

3. 创建新的 API Key 并粘贴到这里

+ + ) : ( + <> +

1. 购买 +

+

2. 在 Coding Plan 页面获取 (格式 sk-sp-xxx) +

+

3. 粘贴到这里(注意:不是普通百炼 API Key)

+ )}
@@ -508,10 +854,13 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia {modelCategories.map((category) => { // Get the appropriate model list based on selected provider - const models = - apiProvider === "openai" ? category.openaiModels : + const models = + apiProvider === "openai" ? category.openaiModels : apiProvider === "gemini" ? category.geminiModels : - category.anthropicModels; + apiProvider === "anthropic" ? category.anthropicModels : + apiProvider === "deepseek" ? category.deepseekModels : + apiProvider === "zhipu" ? category.zhipuModels : + category.bailianModels; return (
@@ -523,22 +872,28 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia
{models.map((m) => { // Determine which state to use based on category key - const currentValue = + const currentValue = category.key === 'extractionModel' ? extractionModel : category.key === 'solutionModel' ? solutionModel : debuggingModel; - + // Determine which setter function to use - const setValue = + const setValue = category.key === 'extractionModel' ? setExtractionModel : category.key === 'solutionModel' ? setSolutionModel : setDebuggingModel; - + + const isSelected = currentValue === m.id; + // Debug: log selection state + if (m.id.includes('kimi')) { + console.log(`DEBUG: ${category.key} - model ${m.id}, currentValue=${currentValue}, isSelected=${isSelected}`); + } + return (