From 9aa81b1794da03788ec1a54ff13090df20b26afb Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Wed, 27 May 2026 18:42:00 +0800 Subject: [PATCH 1/3] fix: use inline config for AI proxy to avoid configId lookup failure When backend mode is enabled, AI service requests (test connection, repo analysis, etc.) used configId to look up the config in the backend DB. This failed when the auto-sync hadn't pushed the config yet (2s debounce window), causing "AI config not found" 404 errors. Now always sends the full config inline via proxyAIRequestWithConfig, matching what the form test connection already does successfully. Fixes #166 Co-Authored-By: Claude Opus 4.7 --- src/services/aiService.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 9ce310a7..d364bc18 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -154,11 +154,7 @@ export class AIService { let data: Record; if (backend.isAvailable) { - if (this.config.id) { - data = await backend.proxyAIRequest(this.config.id, requestBody, options.signal) as Record; - } else { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal) as Record; - } + data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal) as Record; } else { const url = buildFinalApiUrl(this.config.baseUrl, apiType); const response = await fetch(url, { @@ -216,11 +212,7 @@ export class AIService { let data: unknown; if (backend.isAvailable) { - if (this.config.id) { - data = await backend.proxyAIRequest(this.config.id, requestBody, options.signal); - } else { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); - } + data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); } else { const url = buildApiUrl(this.config.baseUrl, 'v1/messages'); const response = await fetch(url, { @@ -276,11 +268,7 @@ ${options.user}` : options.user; let data: unknown; if (backend.isAvailable) { - if (this.config.id) { - data = await backend.proxyAIRequest(this.config.id, requestBody, options.signal); - } else { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); - } + data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); } else { const path = `v1beta/models/${encodeURIComponent(model)}:generateContent`; const urlObj = new URL(buildApiUrl(this.config.baseUrl, path)); From 0d2764943587cc9704b9d44e268e16f4ad03f7ec Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Wed, 27 May 2026 18:53:00 +0800 Subject: [PATCH 2/3] fix: use configId-first with inline fallback for AI proxy Address CodeRabbit review: avoid sending API key inline when the config exists in the backend DB. Added proxyAIRequestWithFallback that first tries configId lookup, and only falls back to inline config on 404. Co-Authored-By: Claude Opus 4.7 --- src/services/aiService.ts | 6 +++--- src/services/backendAdapter.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/services/aiService.ts b/src/services/aiService.ts index d364bc18..5b4d3a67 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -154,7 +154,7 @@ export class AIService { let data: Record; if (backend.isAvailable) { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal) as Record; + data = await backend.proxyAIRequestWithFallback(this.config.id, this.config, requestBody, options.signal) as Record; } else { const url = buildFinalApiUrl(this.config.baseUrl, apiType); const response = await fetch(url, { @@ -212,7 +212,7 @@ export class AIService { let data: unknown; if (backend.isAvailable) { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); + data = await backend.proxyAIRequestWithFallback(this.config.id, this.config, requestBody, options.signal); } else { const url = buildApiUrl(this.config.baseUrl, 'v1/messages'); const response = await fetch(url, { @@ -268,7 +268,7 @@ ${options.user}` : options.user; let data: unknown; if (backend.isAvailable) { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); + data = await backend.proxyAIRequestWithFallback(this.config.id, this.config, requestBody, options.signal); } else { const path = `v1beta/models/${encodeURIComponent(model)}:generateContent`; const urlObj = new URL(buildApiUrl(this.config.baseUrl, path)); diff --git a/src/services/backendAdapter.ts b/src/services/backendAdapter.ts index 5a9829a1..85b9b5d6 100644 --- a/src/services/backendAdapter.ts +++ b/src/services/backendAdapter.ts @@ -252,6 +252,32 @@ class BackendAdapter { return res.json(); } + async proxyAIRequestWithFallback(configId: string, aiConfig: { apiType?: string; baseUrl: string; apiKey: string; model: string; reasoningEffort?: string }, body: object, signal?: AbortSignal): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + // Try configId lookup first to avoid sending API key inline + if (configId) { + try { + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/ai`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ configId, body }), + signal, + }, 120000); + if (res.ok) return res.json(); + // Fall through to inline config on 404 (config not synced yet) + if (res.status !== 404) await this.throwTranslatedError(res, 'AI proxy error'); + } catch (err) { + // If it's a non-404 error, rethrow; otherwise fall through + const msg = err instanceof Error ? err.message : ''; + if (!msg.includes('AI config not found') && !msg.includes('AI_CONFIG_NOT_FOUND')) throw err; + } + } + + // Fallback: send full config inline + return this.proxyAIRequestWithConfig(aiConfig, body, signal); + } + // === WebDAV Proxy === async proxyWebDAV(configId: string, method: string, path: string, body?: string, headers?: Record): Promise { From 41782d92e9da08153ce86c9bb7d06315e9a4c3af Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Wed, 27 May 2026 19:00:52 +0800 Subject: [PATCH 3/3] fix: use stable error properties instead of message text for fallback Address CodeRabbit review: throwTranslatedError now attaches statusCode and code to the Error object. proxyAIRequestWithFallback checks these stable properties instead of matching localized message strings. Co-Authored-By: Claude Opus 4.7 --- src/services/backendAdapter.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/services/backendAdapter.ts b/src/services/backendAdapter.ts index 85b9b5d6..61df6b84 100644 --- a/src/services/backendAdapter.ts +++ b/src/services/backendAdapter.ts @@ -136,7 +136,10 @@ class BackendAdapter { } } catch { /* body not JSON */ } const translated = translateBackendError(code, `${fallbackPrefix}: ${res.status}`); - throw new Error(detail ? `${translated} - ${detail}` : translated); + const error = new Error(detail ? `${translated} - ${detail}` : translated) as Error & { statusCode?: number; code?: string }; + error.statusCode = res.status; + if (code) error.code = code; + throw error; } // === GitHub Proxy === @@ -268,9 +271,9 @@ class BackendAdapter { // Fall through to inline config on 404 (config not synced yet) if (res.status !== 404) await this.throwTranslatedError(res, 'AI proxy error'); } catch (err) { - // If it's a non-404 error, rethrow; otherwise fall through - const msg = err instanceof Error ? err.message : ''; - if (!msg.includes('AI config not found') && !msg.includes('AI_CONFIG_NOT_FOUND')) throw err; + // Rethrow non-404 errors; fall through to inline config on config-not-found + const e = err as Error & { statusCode?: number; code?: string }; + if (e.statusCode !== 404 && e.code !== 'AI_CONFIG_NOT_FOUND') throw err; } }