From c652790d9916c61dff97d3342b1f749a162e8a2e Mon Sep 17 00:00:00 2001 From: Idddd <956020859@qq.com> Date: Fri, 22 May 2026 13:48:59 +0800 Subject: [PATCH] fix: add withReloginOnUnauthorized to all AI calls, add curl logging - Wrap SonarQubeCoverageAction, CodeGenerateReviewAction, and SonarCubeToolWindowPanel AI Fix with withReloginOnUnauthorized so they auto-relogin on 401 like commit generate does - Make Coverage dashboard card clickable to jump to coverage dialog - Add curl command logging to all HTTP API calls for debugging Co-Authored-By: Claude Opus 4.7 --- .../ai/plugin/llm/OpenAiCompatibleProvider.kt | 14 ++++++++ .../ai/plugin/CodeGenerateReviewAction.kt | 6 ++-- .../org/openprojectx/ai/plugin/HttpClients.kt | 33 +++++++++++++++++ .../ai/plugin/SonarCubeToolWindowPanel.kt | 36 +++++++++++++------ .../ai/plugin/SonarQubeCoverageAction.kt | 18 +++++----- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/OpenAiCompatibleProvider.kt b/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/OpenAiCompatibleProvider.kt index e0c7160..334c561 100644 --- a/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/OpenAiCompatibleProvider.kt +++ b/llm-client/src/main/kotlin/org/openprojectx/ai/plugin/llm/OpenAiCompatibleProvider.kt @@ -6,6 +6,7 @@ import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json class OpenAiCompatibleProvider( private val http: HttpClient, @@ -29,6 +30,9 @@ class OpenAiCompatibleProvider( temperature = 0.1 ) + val curlCmd = buildCurlCommand(endpoint, apiKey, req) + LlmRuntimeLogger.info("curl | $curlCmd") + val response = http.post(endpoint) { header(HttpHeaders.Authorization, "Bearer $apiKey") contentType(ContentType.Application.Json) @@ -72,4 +76,14 @@ class OpenAiCompatibleProvider( @Serializable data class Choice(val message: Message) } + + companion object { + private val curlJson = Json { prettyPrint = false } + + private fun buildCurlCommand(endpoint: String, apiKey: String, req: ChatCompletionsRequest): String { + val body = curlJson.encodeToString(ChatCompletionsRequest.serializer(), req) + .replace("'", "'\"'\"'") + return "curl -X POST '$endpoint' -H 'Authorization: Bearer ***' -H 'Content-Type: application/json' --data '$body'" + } + } } diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/CodeGenerateReviewAction.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/CodeGenerateReviewAction.kt index 149f099..db4ef3d 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/CodeGenerateReviewAction.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/CodeGenerateReviewAction.kt @@ -67,7 +67,6 @@ class CodeGenerateReviewAction : AnAction("Code Generate & Review"), DumbAware { override fun run(indicator: ProgressIndicator) { try { indicator.text = "Calling LLM..." - val provider = LlmProviderFactory.create(LlmSettingsLoader.load(project)) val finalPrompt = AiPromptDefaults.render( selectedPrompt.template, mapOf( @@ -75,7 +74,10 @@ class CodeGenerateReviewAction : AnAction("Code Generate & Review"), DumbAware { "extraRequirements" to extraRequirements ) ) - val response = runBlocking { provider.generateCode(finalPrompt) } + val response = LlmAuthSessionService.getInstance(project).withReloginOnUnauthorized { settings -> + val provider = LlmProviderFactory.create(settings) + runBlocking { provider.generateCode(finalPrompt) } + } ApplicationManager.getApplication().invokeLater { ContextBoxStateService.getInstance(project).recordCodePromptResult( promptType = selectedPrompt.category.label, diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/HttpClients.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/HttpClients.kt index bfbedf4..56a0bb5 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/HttpClients.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/HttpClients.kt @@ -49,4 +49,37 @@ object HttpClients { builder.sslSocketFactory(sslContext.socketFactory, trustAll[0] as X509TrustManager) builder.hostnameVerifier { _, _ -> true } } + + fun logCurl(method: String, url: String, headers: Map, body: String = "") { + val safeHeaders = headers.mapValues { (name, value) -> + if (name.contains("authorization", true) || + name.contains("token", true) || + name.contains("key", true) || + name.contains("secret", true) || + name.contains("password", true) || + name.contains("cookie", true) + ) "***" else value + } + val safeBody = redactSensitivePayload(body) + val headerArgs = safeHeaders.entries.joinToString(" ") { (name, value) -> + "-H \"$name: $value\"" + } + val bodyArg = if (safeBody.isNotBlank()) " --data '${safeBody.replace("'", "'\"'\"'")}'" else "" + org.openprojectx.ai.plugin.llm.LlmRuntimeLogger.info( + "curl -X $method '$url'$headerArgs$bodyArg" + ) + } + + private fun redactSensitivePayload(text: String): String { + if (text.isBlank()) return text + val sensitiveKeys = listOf("password", "token", "access_token", "id_token", "refresh_token", "apiKey", "api_key", "secret") + var result = text + sensitiveKeys.forEach { key -> + val quotedJsonPattern = Regex("""("${Regex.escape(key)}"\s*:\s*")[^"]*(")""", RegexOption.IGNORE_CASE) + result = result.replace(quotedJsonPattern) { "${it.groupValues[1]}***${it.groupValues[2]}" } + val formPattern = Regex("""(?i)(^|[&\s])(${Regex.escape(key)}=)[^&\s]+""") + result = result.replace(formPattern) { "${it.groupValues[1]}${it.groupValues[2]}***" } + } + return result + } } \ No newline at end of file diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarCubeToolWindowPanel.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarCubeToolWindowPanel.kt index 178fec1..7606520 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarCubeToolWindowPanel.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarCubeToolWindowPanel.kt @@ -216,7 +216,18 @@ object SonarCubeToolWindowPanel { val result = runBlocking { SonarCubeToolWindowClient(config).load() } ApplicationManager.getApplication().invokeLater { - refreshDashboard(dashboardPanel, bgColor, borderColor, commonFont, result, { filterByType(it) }, { resetTypeFilters() }) + refreshDashboard(dashboardPanel, bgColor, borderColor, commonFont, result, { filterByType(it) }, { resetTypeFilters() }) { + val action = com.intellij.openapi.actionSystem.ActionManager.getInstance() + .getAction("org.openprojectx.ai.plugin.SonarQubeCoverageAction") + action?.let { + val event = com.intellij.openapi.actionSystem.AnActionEvent.createFromDataContext( + "SonarCubeToolWindow", null + ) { dataId -> + if (com.intellij.openapi.actionSystem.CommonDataKeys.PROJECT.`is`(dataId)) project else null + } + it.actionPerformed(event) + } + } reportDate.text = result.reportTimestamp ?: "" allIssues.clear() allIssues.addAll(result.issues) @@ -329,8 +340,10 @@ object SonarCubeToolWindowPanel { "sourceCode" to sourceCode ) ) - val provider = LlmProviderFactory.create(LlmSettingsLoader.load(project)) - val response = runBlocking { provider.generateCode(prompt) } + val response = LlmAuthSessionService.getInstance(project).withReloginOnUnauthorized { settings -> + val provider = LlmProviderFactory.create(settings) + runBlocking { provider.generateCode(prompt) } + } ApplicationManager.getApplication().invokeLater { handleFixResponse(project, issue, response, sourceCode) @@ -441,7 +454,8 @@ object SonarCubeToolWindowPanel { font: Font, result: Any, onFilterByType: ((String) -> Unit)? = null, - onResetFilters: (() -> Unit)? = null + onResetFilters: (() -> Unit)? = null, + onCoverageClick: (() -> Unit)? = null ) { container.removeAll() container.background = bgColor @@ -467,7 +481,7 @@ object SonarCubeToolWindowPanel { isOpaque = false } - cards.add(metricCard("Coverage", r.coverage?.formatPercent() ?: "—", "#42A5F5", r.coverage != null, valueFont, cardFont, borderColor)) + cards.add(metricCard("Coverage", r.coverage?.formatPercent() ?: "—", "#42A5F5", r.coverage != null, valueFont, cardFont, borderColor, onCoverageClick)) cards.add(metricCard("Line Cov.", r.lineCoverage?.formatPercent() ?: "—", "#26C6DA", r.lineCoverage != null, valueFont, cardFont, borderColor)) cards.add(metricCard("Branch Cov.", r.branchCoverage?.formatPercent() ?: "—", "#009688", r.branchCoverage != null, valueFont, cardFont, borderColor)) cards.add(metricCard("Uncovered", r.uncoveredLines?.toString() ?: "—", "#EF5350", r.uncoveredLines != null, valueFont, cardFont, borderColor)) @@ -757,14 +771,14 @@ private class SonarCubeToolWindowClient(private val config: SonarQubeConfig) { try { val baseUrl = config.serverUrl.trimEnd('/') val projectKey = encoded(config.projectKey) - val measures: SonarCubeMeasuresResponse = client.get( - "$baseUrl/api/measures/component?component=$projectKey&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines,bugs,vulnerabilities,code_smells" - ) { + val measuresUrl = "$baseUrl/api/measures/component?component=$projectKey&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines,bugs,vulnerabilities,code_smells" + HttpClients.logCurl("GET", measuresUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap()) + val measures: SonarCubeMeasuresResponse = client.get(measuresUrl) { authHeader?.let { header(HttpHeaders.Authorization, it) } }.body() - val issues: SonarCubeIssuesResponse = client.get( - "$baseUrl/api/issues/search?componentKeys=$projectKey&resolved=false&ps=100&s=SEVERITY&asc=false" - ) { + val issuesUrl = "$baseUrl/api/issues/search?componentKeys=$projectKey&resolved=false&ps=100&s=SEVERITY&asc=false" + HttpClients.logCurl("GET", issuesUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap()) + val issues: SonarCubeIssuesResponse = client.get(issuesUrl) { authHeader?.let { header(HttpHeaders.Authorization, it) } }.body() val now = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt index a9030ef..b8212ba 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SonarQubeCoverageAction.kt @@ -63,8 +63,10 @@ class SonarQubeCoverageAction : AnAction("SonarQube Coverage"), DumbAware { val generation = if (request.generateMissingTests) { indicator.text = "Generating tests for uncovered code..." val prompt = SonarQubeCoveragePromptBuilder.build(project, report, request.targetCoverage) - val provider = LlmProviderFactory.create(LlmSettingsLoader.load(project)) - runBlocking { provider.generateCode(prompt) } + LlmAuthSessionService.getInstance(project).withReloginOnUnauthorized { settings -> + val provider = LlmProviderFactory.create(settings) + runBlocking { provider.generateCode(prompt) } + } } else { "" } @@ -184,14 +186,14 @@ private class SonarQubeCoverageClient(private val request: SonarQubeCoverageRequ try { val baseUrl = request.serverUrl.trimEnd('/') val component = encoded(request.projectKey) - val projectMeasures: SonarComponentMeasuresResponse = jsonClient.get( - "$baseUrl/api/measures/component?component=$component&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines" - ) { + val projectMeasuresUrl = "$baseUrl/api/measures/component?component=$component&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines" + HttpClients.logCurl("GET", projectMeasuresUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap()) + val projectMeasures: SonarComponentMeasuresResponse = jsonClient.get(projectMeasuresUrl) { authHeader?.let { header(HttpHeaders.Authorization, it) } }.body() - val fileMeasures: SonarComponentTreeResponse = jsonClient.get( - "$baseUrl/api/measures/component_tree?component=$component&metricKeys=coverage,uncovered_lines&qualifiers=FIL&s=metric&metricSort=uncovered_lines&asc=false&ps=${request.maxFiles.coerceIn(1, 100)}" - ) { + val fileMeasuresUrl = "$baseUrl/api/measures/component_tree?component=$component&metricKeys=coverage,uncovered_lines&qualifiers=FIL&s=metric&metricSort=uncovered_lines&asc=false&ps=${request.maxFiles.coerceIn(1, 100)}" + HttpClients.logCurl("GET", fileMeasuresUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap()) + val fileMeasures: SonarComponentTreeResponse = jsonClient.get(fileMeasuresUrl) { authHeader?.let { header(HttpHeaders.Authorization, it) } }.body()