diff --git a/.gitignore b/.gitignore
index 6fda8892..e166b540 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,3 +64,5 @@ webview/src/version/version.ts
# 自定义沙箱目录(如果启用)
.sandbox/
+logs/*.log
+.intellijPlatform/
diff --git a/build.gradle b/build.gradle
index 50c50744..65686f3c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,132 +1,212 @@
-import org.jetbrains.intellij.IntelliJPluginExtension
-
plugins {
id 'java'
- id 'org.jetbrains.intellij' version '1.17.4'
+ id 'org.jetbrains.intellij.platform' version '2.10.5'
}
-group 'com.github.idea-claude-code-gui'
-version '0.1.0-beta3'
+group = 'com.github.idea-claude-code-gui'
+version = '0.1.0-beta3'
-sourceCompatibility = 17
-targetCompatibility = 17
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
repositories {
mavenCentral()
+ intellijPlatform {
+ defaultRepositories()
+ }
}
dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
+
+ intellijPlatform {
+ intellijIdeaCommunity('2024.3.1')
+ instrumentationTools()
+ }
}
-// 配置 IntelliJ Platform 插件
-intellij {
- version = '2024.3' // 使用 2024.3 稳定版本
- type = 'IC' // IC = IntelliJ IDEA Community Edition
-
- // 下载源码和 JavaDoc
- downloadSources = true
-
- // 插件依赖(如果需要)
- plugins = []
-
- // 禁用字节码插桩,避免生成 instrumented JAR
- instrumentCode = false
-}
-
-tasks.withType(JavaCompile) {
+tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
+// Ensure plugin.xml is patched with the correct since/until build and change notes
patchPluginXml {
+ // plugin compatibility range
sinceBuild = '233'
- untilBuild = '263.*' // 兼容到 2026.3 版本,支持更新的 IDE 版本
+ untilBuild = '263.*'
- changeNotes = """
-
- - 初始版本发布
- - 实现右侧工具栏窗口
- - 最小支持IDEA 2023.3版本
- - 兼容至 IDEA 2026.3 版本(build 263.*)
-
- """
+ // Release notes shown in plugin repositories
+ // Dynamically load the latest changelog entry from CHANGELOG.md and convert to simple HTML.
+ def changelogFile = file('CHANGELOG.md')
+ def computedChangeNotes = ''
+ if (changelogFile.exists()) {
+ def lines = changelogFile.readLines('UTF-8')
+ // Find first section header (lines starting with '#####')
+ def start = -1
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines[i].trim().startsWith('#####')) {
+ start = i
+ break
+ }
+ }
+
+ if (start >= 0) {
+ def sb = new StringBuilder()
+ boolean inList = false
+ int i = start + 1
+ while (i < lines.size() && !lines[i].trim().startsWith('#####')) {
+ def line = lines[i]
+ def t = line.trim()
+
+ if (t.startsWith('- ')) {
+ if (!inList) { sb.append(''); inList = true }
+ def item = t.substring(2)
+ // escape HTML special chars for safety
+ item = item.replace('&', '&').replace('<', '<').replace('>', '>')
+ sb.append('- ' + item + '
')
+ } else if (t.startsWith('![') || t.startsWith('
'); inList = false }
+ // convert markdown image syntax to img tag if necessary
+ if (t.startsWith('![')) {
+ def pattern = Pattern.compile(/!\[(.*?)\]\((.*?)\)/)
+ def matcher = pattern.matcher(t)
+ if (matcher.find()) {
+ def alt = matcher.group(1)
+ def src = matcher.group(2)
+ sb.append("
")
+ } else {
+ sb.append(t)
+ }
+ } else {
+ sb.append(t)
+ }
+ } else if (t.length() == 0) {
+ if (inList) { sb.append('
'); inList = false }
+ sb.append('
')
+ } else {
+ if (inList) { sb.append(''); inList = false }
+ // paragraph
+ def para = t.replace('&', '&').replace('<', '<').replace('>', '>')
+ sb.append('' + para + '
')
+ }
+
+ i++
+ }
+ if (inList) { sb.append('') }
+ computedChangeNotes = sb.toString()
+ }
+ }
+
+ changeNotes = computedChangeNotes ?: 'No changelog found.
'
}
-runIde {
- // 启用 JCEF 支持
- jvmArgs '-Djcef.sandbox.enable=false'
-}
-
-buildSearchableOptions {
+// Disable product-specific searchable options/instrument tasks if present
+// Use matching to cover task name variants like buildSearchableOptionsIC / buildSearchableOptionsIU etc.
+tasks.matching { it.name.startsWith('buildSearchableOptions') }.configureEach {
enabled = false
}
-// 禁用 instrumented JAR 生成(这是导致 "invalid plugin descriptor" 错误的根源)
-// instrumented JAR 是用于代码覆盖率测试的,不应该被打包到最终的插件分发包中
-tasks.named("instrumentCode") {
+tasks.named('buildSearchableOptions') {
enabled = false
}
-tasks.named("instrumentTestCode") {
+tasks.named('instrumentCode') {
enabled = false
}
+tasks.named('jarSearchableOptions') {
+ enabled = false
+}
+
+tasks.named('runIde') {
+ jvmArgumentProviders.add({
+ [
+ '-Djcef.sandbox.enable=false',
+ // 禁用插件自动卸载(避免检测到文件变更时自动卸载)
+ '-Didea.auto.reload.plugins=false',
+ // 禁用插件动态卸载
+ '-Didea.dynamic.plugins.allowed=false',
+ // 启用内部模式
+ '-Didea.is.internal=true',
+ // 禁用文件系统事件监控(可选)
+ // '-Didea.filewatcher.disabled=true',
+ // 增加插件加载超时
+ '-Didea.plugins.load.timeout=60000'
+ ]
+ } as CommandLineArgumentProvider)
+
+ // 设置系统属性
+ systemProperty 'idea.auto.reload.plugins', 'false'
+ systemProperty 'idea.dynamic.plugins.allowed', 'false'
+ systemProperty 'idea.is.internal', 'true'
+ systemProperty 'idea.plugin.in.sandbox.mode', 'true'
+
+ // 复制自定义配置到sandbox
+ doFirst {
+ def sandboxConfig = file('sandbox-idea.properties')
+ if (sandboxConfig.exists()) {
+ def sandboxDir = layout.buildDirectory.dir("idea-sandbox").get().asFile
+ def configDir = new File(sandboxDir, 'config')
+ configDir.mkdirs()
+
+ copy {
+ from sandboxConfig
+ into configDir
+ rename { 'idea.properties' }
+ }
+ println "已复制sandbox配置到: ${configDir}/idea.properties"
+ }
+ }
+}
+
// 统一的 AI Bridge(合并 Claude 和 Codex)
def aiBridgeDir = file("ai-bridge")
-def aiBridgePackDir = file("$buildDir/ai-bridge-pack")
-def aiBridgeArchive = new File(aiBridgePackDir, "ai-bridge.zip")
+def aiBridgePackDir = layout.buildDirectory.dir('ai-bridge-pack').get().asFile
+def aiBridgeArchive = new File(aiBridgePackDir, 'ai-bridge.zip')
// 自动构建 webview
def webviewDir = file('webview')
tasks.register('buildWebview', Exec) {
workingDir webviewDir
- // 根据操作系统选择命令
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
commandLine 'cmd', '/c', 'npm', 'run', 'build'
} else {
commandLine 'npm', 'run', 'build'
}
- // 只在 webview 目录存在时执行(使用变量避免配置缓存问题)
onlyIf { webviewDir.exists() }
-
- // 输出构建信息
- doFirst {
- println "Building webview..."
- }
- doLast {
- println "Webview build completed."
- }
+ doFirst { println "Building webview..." }
+ doLast { println "Webview build completed." }
}
tasks.register('packageAiBridge', Zip) {
onlyIf { aiBridgeDir.exists() }
- archiveFileName = "ai-bridge.zip"
+ archiveFileName = 'ai-bridge.zip'
destinationDirectory = aiBridgePackDir
from(aiBridgeDir)
- exclude("node_modules/.pnpm/**")
+ exclude('node_modules/.pnpm/**')
includeEmptyDirs = true
}
-// 提取插件名称,避免在闭包中访问 extensions
-def intellijExt = project.extensions.getByType(IntelliJPluginExtension)
-def pluginName = intellijExt.pluginName.getOrElse(project.name)
+// 提取插件名称
+def pluginName = project.name
-tasks.named("prepareSandbox") {
- dependsOn("buildWebview", "packageAiBridge")
- def sandboxRootProvider = layout.buildDirectory.dir("idea-sandbox")
+tasks.matching { it.name.startsWith('prepareSandbox') }.configureEach {
+ dependsOn('buildWebview', 'packageAiBridge')
+ def sandboxRootProvider = layout.buildDirectory.dir('idea-sandbox')
doLast {
- if (!aiBridgeDir.exists()) {
- throw new GradleException("ai-bridge 目录不存在,请确保已拉取并安装依赖。")
+ if (!aiBridgeDir.exists() && !aiBridgeArchive.exists()) {
+ throw new GradleException('ai-bridge 目录不存在,请确保已拉取并安装依赖。')
}
def sandboxRoot = sandboxRootProvider.get().asFile
def pluginDir = new File(sandboxRoot, "plugins/${pluginName}")
- // 部署 ai-bridge
- def extractedDir = new File(pluginDir, "ai-bridge")
- def archiveTarget = new File(pluginDir, "ai-bridge.zip")
+ def extractedDir = new File(pluginDir, 'ai-bridge')
+ def archiveTarget = new File(pluginDir, 'ai-bridge.zip')
project.delete(extractedDir)
project.delete(archiveTarget)
project.copy {
@@ -136,6 +216,53 @@ tasks.named("prepareSandbox") {
}
}
-tasks.named("buildPlugin") {
- dependsOn("packageAiBridge")
+// 配置 prepareSandbox 之后的任务,确保 ai-bridge.zip 被包含
+tasks.matching { it.name == 'preparePluginForDistribution' || it.name == 'composedJar' }.configureEach {
+ dependsOn('packageAiBridge')
}
+
+tasks.named('buildPlugin') {
+ dependsOn('packageAiBridge')
+
+ doLast {
+ // 解压插件 zip,添加 ai-bridge.zip,然后重新打包
+ def distributionFile = archiveFile.get().asFile
+ if (distributionFile.exists() && aiBridgeArchive.exists()) {
+ def tempDir = file("${layout.buildDirectory.get()}/tmp/plugin-repack")
+ delete(tempDir)
+
+ // 解压
+ copy {
+ from zipTree(distributionFile)
+ into tempDir
+ }
+
+ // 找到插件目录并添加 ai-bridge.zip
+ def pluginDir = new File(tempDir, pluginName)
+ if (pluginDir.exists()) {
+ copy {
+ from aiBridgeArchive
+ into pluginDir
+ }
+
+ // 删除 searchableOptions jar(如果存在)
+ fileTree(new File(pluginDir, 'lib')).matching {
+ include '**/*searchableOptions*.jar'
+ }.each { jarFile ->
+ delete(jarFile)
+ println "已删除 searchableOptions jar: ${jarFile.name}"
+ }
+
+ // 重新打包
+ delete(distributionFile)
+ ant.zip(destfile: distributionFile) {
+ fileset(dir: tempDir)
+ }
+ println "已将 ai-bridge.zip 添加到插件包并移除 searchableOptions jar"
+ }
+
+ delete(tempDir)
+ }
+ }
+}
+
diff --git a/gradle.properties b/gradle.properties
index 7fc6f1ff..44c22ed3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1,4 @@
kotlin.code.style=official
+# org.gradle.unsafe.configuration-cache=true
+org.jetbrains.intellij.buildFeature.selfUpdateCheck=false
+
diff --git a/sandbox-idea.properties b/sandbox-idea.properties
new file mode 100644
index 00000000..af3548a2
--- /dev/null
+++ b/sandbox-idea.properties
@@ -0,0 +1,5 @@
+idea.auto.reload.plugins=false
+idea.is.internal=true
+idea.plugin.in.sandbox.mode=true
+idea.plugins.load.timeout=60000
+
diff --git a/settings.gradle b/settings.gradle
index 2721d753..2f423eb6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,9 @@
-rootProject.name = 'idea-claude-code-gui'
\ No newline at end of file
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+
+ }
+}
+
+rootProject.name = 'idea-claude-code-gui'
diff --git a/src/main/java/com/github/claudecodegui/ClaudeSDKToolWindow.java b/src/main/java/com/github/claudecodegui/ClaudeSDKToolWindow.java
index 882710a7..52405292 100644
--- a/src/main/java/com/github/claudecodegui/ClaudeSDKToolWindow.java
+++ b/src/main/java/com/github/claudecodegui/ClaudeSDKToolWindow.java
@@ -757,26 +757,48 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
private void pushUsageUpdateFromMessages(List messages) {
try {
+ System.out.println("[Backend] pushUsageUpdateFromMessages called with " + messages.size() + " messages");
+
JsonObject lastUsage = null;
for (int i = messages.size() - 1; i >= 0; i--) {
ClaudeSession.Message msg = messages.get(i);
- if (msg.type != ClaudeSession.Message.Type.ASSISTANT || msg.raw == null) continue;
- if (!msg.raw.has("message")) continue;
- JsonObject message = msg.raw.getAsJsonObject("message");
- if (message.has("usage")) {
- lastUsage = message.getAsJsonObject("usage");
+
+ if (msg.type != ClaudeSession.Message.Type.ASSISTANT || msg.raw == null) {
+ continue;
+ }
+
+ // 检查不同的可能结构
+ if (msg.raw.has("message")) {
+ JsonObject message = msg.raw.getAsJsonObject("message");
+ if (message.has("usage")) {
+ lastUsage = message.getAsJsonObject("usage");
+ break;
+ }
+ }
+
+ // 检查usage是否在raw的根级别
+ if (msg.raw.has("usage")) {
+ lastUsage = msg.raw.getAsJsonObject("usage");
break;
}
}
+ if (lastUsage == null) {
+ System.out.println("[Backend] WARNING: No usage info found in messages!");
+ }
+
int inputTokens = lastUsage != null && lastUsage.has("input_tokens") ? lastUsage.get("input_tokens").getAsInt() : 0;
int cacheWriteTokens = lastUsage != null && lastUsage.has("cache_creation_input_tokens") ? lastUsage.get("cache_creation_input_tokens").getAsInt() : 0;
int cacheReadTokens = lastUsage != null && lastUsage.has("cache_read_input_tokens") ? lastUsage.get("cache_read_input_tokens").getAsInt() : 0;
+ int outputTokens = lastUsage != null && lastUsage.has("output_tokens") ? lastUsage.get("output_tokens").getAsInt() : 0;
- int usedTokens = inputTokens + cacheWriteTokens + cacheReadTokens;
+ int usedTokens = inputTokens + cacheWriteTokens + cacheReadTokens + outputTokens;
int maxTokens = MODEL_CONTEXT_LIMITS.getOrDefault(currentModel, 200_000);
int percentage = Math.min(100, maxTokens > 0 ? (int) ((usedTokens * 100.0) / maxTokens) : 0);
+ System.out.println("[Backend] Pushing usage update: input=" + inputTokens + ", cacheWrite=" + cacheWriteTokens + ", cacheRead=" + cacheReadTokens + ", output=" + outputTokens + ", total=" + usedTokens + ", max=" + maxTokens + ", percentage=" + percentage + "%");
+
+
JsonObject usageUpdate = new JsonObject();
usageUpdate.addProperty("percentage", percentage);
usageUpdate.addProperty("totalTokens", usedTokens);
@@ -786,13 +808,22 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
String usageJson = new Gson().toJson(usageUpdate);
SwingUtilities.invokeLater(() -> {
- String js = "if (window.onUsageUpdate) { window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "'); }";
if (browser != null && !disposed) {
+ // 使用安全的调用方式,检查函数是否存在
+ String js = "(function() {" +
+ " if (typeof window.onUsageUpdate === 'function') {" +
+ " window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "');" +
+ " console.log('[Backend->Frontend] Usage update sent successfully');" +
+ " } else {" +
+ " console.warn('[Backend->Frontend] window.onUsageUpdate not found');" +
+ " }" +
+ "})();";
browser.getCefBrowser().executeJavaScript(js, browser.getCefBrowser().getURL(), 0);
}
});
} catch (Exception e) {
System.err.println("[Backend] Failed to push usage update: " + e.getMessage());
+ e.printStackTrace();
}
}
@@ -842,8 +873,17 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
usageUpdate.addProperty("maxTokens", maxTokens);
String usageJson = new Gson().toJson(usageUpdate);
- String js = "if (window.onUsageUpdate) { window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "'); }";
+
if (browser != null && !disposed) {
+ // 使用安全的调用方式
+ String js = "(function() {" +
+ " if (typeof window.onUsageUpdate === 'function') {" +
+ " window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "');" +
+ " console.log('[Backend->Frontend] Usage reset for new session');" +
+ " } else {" +
+ " console.warn('[Backend->Frontend] window.onUsageUpdate not found');" +
+ " }" +
+ "})();";
browser.getCefBrowser().executeJavaScript(js, browser.getCefBrowser().getURL(), 0);
}
});
@@ -879,14 +919,34 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
private void callJavaScript(String functionName, String... args) {
if (disposed || browser == null) {
+ System.err.println("[ClaudeSDKToolWindow] 无法调用 JS 函数 " + functionName + ": disposed=" + disposed + ", browser=" + (browser == null ? "null" : "exists"));
return;
}
- try {
- String js = JsUtils.buildJsCall(functionName, args);
- browser.getCefBrowser().executeJavaScript(js, browser.getCefBrowser().getURL(), 0);
- } catch (Exception e) {
- System.err.println("[ClaudeSDKToolWindow] 调用 JS 函数失败: " + functionName + ", 错误: " + e.getMessage());
- }
+
+ SwingUtilities.invokeLater(() -> {
+ if (disposed || browser == null) {
+ return;
+ }
+ try {
+ String js = JsUtils.buildJsCall(functionName, args);
+
+ // 先检查函数是否存在,再调用
+ String checkAndCall =
+ "(function() {" +
+ " if (typeof window." + functionName + " === 'function') {" +
+ " " + js +
+ " console.log('[Backend->Frontend] Successfully called " + functionName + "');" +
+ " } else {" +
+ " console.warn('[Backend->Frontend] Function " + functionName + " not found on window');" +
+ " }" +
+ "})();";
+
+ browser.getCefBrowser().executeJavaScript(checkAndCall, browser.getCefBrowser().getURL(), 0);
+ } catch (Exception e) {
+ System.err.println("[ClaudeSDKToolWindow] 调用 JS 函数失败: " + functionName + ", 错误: " + e.getMessage());
+ e.printStackTrace();
+ }
+ });
}
/**
diff --git a/src/main/java/com/github/claudecodegui/ClaudeSession.java b/src/main/java/com/github/claudecodegui/ClaudeSession.java
index 57a9430f..3a5f1e00 100644
--- a/src/main/java/com/github/claudecodegui/ClaudeSession.java
+++ b/src/main/java/com/github/claudecodegui/ClaudeSession.java
@@ -546,6 +546,44 @@ public class ClaudeSession {
loading = false;
updateState();
System.out.println("[ClaudeSession] Message end received, loading set to false");
+ } else if ("result".equals(type) && content.startsWith("{")) {
+ // 处理结果消息(包含最终的usage信息)
+ try {
+ JsonObject resultJson = gson.fromJson(content, JsonObject.class);
+ System.out.println("[ClaudeSession] Result message received");
+
+ // 如果当前消息的raw中usage为0,则用result中的usage进行更新
+ if (currentAssistantMessage != null && currentAssistantMessage.raw != null) {
+ JsonObject message = currentAssistantMessage.raw.has("message") && currentAssistantMessage.raw.get("message").isJsonObject()
+ ? currentAssistantMessage.raw.getAsJsonObject("message")
+ : null;
+
+ // 检查当前消息的usage是否全为0
+ boolean needsUsageUpdate = false;
+ if (message != null && message.has("usage")) {
+ JsonObject usage = message.getAsJsonObject("usage");
+ int inputTokens = usage.has("input_tokens") ? usage.get("input_tokens").getAsInt() : 0;
+ int outputTokens = usage.has("output_tokens") ? usage.get("output_tokens").getAsInt() : 0;
+ if (inputTokens == 0 && outputTokens == 0) {
+ needsUsageUpdate = true;
+ }
+ } else {
+ needsUsageUpdate = true;
+ }
+
+ if (needsUsageUpdate && resultJson.has("usage")) {
+ JsonObject resultUsage = resultJson.getAsJsonObject("usage");
+ if (message != null) {
+ message.add("usage", resultUsage);
+ currentAssistantMessage.raw = currentAssistantMessage.raw;
+ notifyMessageUpdate();
+ System.out.println("[ClaudeSession] Updated assistant message usage from result message");
+ }
+ }
+ }
+ } catch (Exception e) {
+ System.err.println("[ClaudeSession] Failed to parse result message: " + e.getMessage());
+ }
} else if ("slash_commands".equals(type)) {
// 处理斜杠命令列表
try {
diff --git a/src/main/java/com/github/claudecodegui/bridge/BridgeDirectoryResolver.java b/src/main/java/com/github/claudecodegui/bridge/BridgeDirectoryResolver.java
index ff7be99e..41159355 100644
--- a/src/main/java/com/github/claudecodegui/bridge/BridgeDirectoryResolver.java
+++ b/src/main/java/com/github/claudecodegui/bridge/BridgeDirectoryResolver.java
@@ -158,14 +158,14 @@ public class BridgeDirectoryResolver {
try {
String pluginsRoot = PathManager.getPluginsPath();
- if (pluginsRoot != null && !pluginsRoot.isEmpty()) {
+ if (!pluginsRoot.isEmpty()) {
addCandidate(possibleDirs, Paths.get(pluginsRoot, PLUGIN_DIR_NAME, SDK_DIR_NAME).toFile());
addCandidate(possibleDirs, Paths.get(pluginsRoot, PLUGIN_ID, SDK_DIR_NAME).toFile());
}
// 使用系统路径下的 plugins 目录代替已废弃的 getPluginTempPath()
String systemPath = PathManager.getSystemPath();
- if (systemPath != null && !systemPath.isEmpty()) {
+ if (!systemPath.isEmpty()) {
Path sandboxPath = Paths.get(systemPath, "plugins");
addCandidate(possibleDirs, sandboxPath.resolve(PLUGIN_DIR_NAME).resolve(SDK_DIR_NAME).toFile());
addCandidate(possibleDirs, sandboxPath.resolve(PLUGIN_ID).resolve(SDK_DIR_NAME).toFile());
@@ -282,7 +282,55 @@ public class BridgeDirectoryResolver {
}
}
}
- return null;
+
+ // 如果在插件目录或 lib 下找不到,尝试查找常见的 sandbox 顶级 plugins 目录和 system/config 下的 plugins
+ List fallbackCandidates = new ArrayList<>();
+ try {
+ // 向上查找祖先,寻找可能的 idea-sandbox 根目录或包含顶级 plugins 的目录
+ File ancestor = pluginDir;
+ int climbs = 0;
+ while (climbs < 6) {
+ File parent = ancestor.getParentFile();
+ if (parent == null) break;
+
+ File maybeTopPlugins = new File(parent, "plugins");
+ if (maybeTopPlugins.exists() && maybeTopPlugins.isDirectory()) {
+ fallbackCandidates.add(new File(maybeTopPlugins, PLUGIN_DIR_NAME + File.separator + SDK_ARCHIVE_NAME));
+ fallbackCandidates.add(new File(maybeTopPlugins, PLUGIN_ID + File.separator + SDK_ARCHIVE_NAME));
+ }
+
+ // system/config siblings under this parent
+ File maybeSystemPlugins = new File(parent, "system/plugins");
+ File maybeConfigPlugins = new File(parent, "config/plugins");
+ if (maybeSystemPlugins.exists() && maybeSystemPlugins.isDirectory()) {
+ fallbackCandidates.add(new File(maybeSystemPlugins, PLUGIN_DIR_NAME + File.separator + SDK_ARCHIVE_NAME));
+ fallbackCandidates.add(new File(maybeSystemPlugins, PLUGIN_ID + File.separator + SDK_ARCHIVE_NAME));
+ }
+ if (maybeConfigPlugins.exists() && maybeConfigPlugins.isDirectory()) {
+ fallbackCandidates.add(new File(maybeConfigPlugins, PLUGIN_DIR_NAME + File.separator + SDK_ARCHIVE_NAME));
+ fallbackCandidates.add(new File(maybeConfigPlugins, PLUGIN_ID + File.separator + SDK_ARCHIVE_NAME));
+ }
+
+ ancestor = parent;
+ climbs++;
+ }
+ } catch (Throwable ignore) {
+ // ignore fallback discovery errors
+ }
+
+ // 打印并尝试这些候选路径
+ for (File f : fallbackCandidates) {
+ System.out.println("[BridgeResolver] 尝试候选路径: " + f.getAbsolutePath() + " (存在: " + f.exists() + ")");
+ if (f.exists()) {
+ archiveFile = f;
+ break;
+ }
+ }
+
+ if (!archiveFile.exists()) {
+ return null;
+ }
+
}
File extractedDir = new File(pluginDir, SDK_DIR_NAME);
@@ -372,26 +420,27 @@ public class BridgeDirectoryResolver {
}
/**
- * 手动设置 claude-bridge 目录路径
+ * 手动设置 claude-bridge 目录路径.
*/
public void setSdkDir(String path) {
this.cachedSdkDir = new File(path);
}
/**
- * 获取当前使用的 claude-bridge 目录
+ * 获取当前使用的 claude-bridge 目录.
*/
public File getSdkDir() {
- if (cachedSdkDir == null) {
- return findSdkDir();
+ if (this.cachedSdkDir == null) {
+ return this.findSdkDir();
}
- return cachedSdkDir;
+ return this.cachedSdkDir;
}
/**
- * 清除缓存
+ * 清除缓存.
*/
public void clearCache() {
this.cachedSdkDir = null;
}
}
+
diff --git a/webview/scripts/extract-version.mjs b/webview/scripts/extract-version.mjs
index c6405d6a..cb085f5d 100644
--- a/webview/scripts/extract-version.mjs
+++ b/webview/scripts/extract-version.mjs
@@ -16,14 +16,14 @@ const buildGradlePath = path.join(projectRoot, 'build.gradle');
const buildGradleContent = fs.readFileSync(buildGradlePath, 'utf8');
// 提取版本号
-// 查找类似 version '0.1.0-beta3' 这样的行
-let versionMatch = buildGradleContent.match(/^version\s+'(.+)'$/m);
+// 查找类似 version = '0.1.0-beta3' 这样的行
+let versionMatch = buildGradleContent.match(/^version\s*=\s*'(.+)'$/m);
if (!versionMatch) {
// 如果上面的正则失败,尝试另一种方式
const lines = buildGradleContent.split('\n');
- const versionLine = lines.find(line => line.trim().startsWith('version '));
+ const versionLine = lines.find(line => line.trim().startsWith('version ='));
if (versionLine) {
- const match = versionLine.match(/version\s+'(.+)'/);
+ const match = versionLine.match(/version\s*=\s*'(.+)'/);
if (match) {
versionMatch = match;
}