diff --git a/run.bat b/run.bat index 810a1d3..06a847a 100644 --- a/run.bat +++ b/run.bat @@ -12,7 +12,18 @@ REM === 抑制 Maven JVM 的 JDK25 兼容性警告 === set MAVEN_OPTS=--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow REM === AI API 配置(按需修改) === +REM 选择 API 提供者:openai(默认)或 anthropic +REM set CLAUDE_CODE_PROVIDER=openai +REM set CLAUDE_CODE_PROVIDER=anthropic + +REM OpenAI 兼容 API 配置(默认) +REM set AI_API_KEY=your-api-key-here +REM set AI_BASE_URL=https://api.openai.com +REM set AI_OPENAI_MODEL=gpt-4o + +REM Anthropic 原生 API 配置 REM set ANTHROPIC_API_KEY=your-api-key-here +REM set ANTHROPIC_BASE_URL=https://api.anthropic.com REM set AI_MODEL=claude-sonnet-4-20250514 REM === 设置控制台 UTF-8 编码(支持 emoji 等字符) === diff --git a/run.ps1 b/run.ps1 index f22896e..19681df 100644 --- a/run.ps1 +++ b/run.ps1 @@ -11,7 +11,18 @@ $env:Path = "D:\Dev\jdk-25\bin;$env:Path" $env:MAVEN_OPTS = "--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow" # === AI API 配置(按需修改) === +# 选择 API 提供者:openai(默认)或 anthropic +# $env:CLAUDE_CODE_PROVIDER = "openai" # 使用 OpenAI 兼容 API(支持代理) +# $env:CLAUDE_CODE_PROVIDER = "anthropic" # 使用 Anthropic 原生 API + +# OpenAI 兼容 API 配置(默认) +# $env:AI_API_KEY = "your-api-key-here" +# $env:AI_BASE_URL = "https://api.openai.com" +# $env:AI_OPENAI_MODEL = "gpt-4o" + +# Anthropic 原生 API 配置 # $env:ANTHROPIC_API_KEY = "your-api-key-here" +# $env:ANTHROPIC_BASE_URL = "https://api.anthropic.com" # $env:AI_MODEL = "claude-sonnet-4-20250514" # === 设置控制台 UTF-8 编码(支持 emoji 等字符) === diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java index a736339..a5ec364 100644 --- a/src/main/java/com/claudecode/config/AppConfig.java +++ b/src/main/java/com/claudecode/config/AppConfig.java @@ -12,21 +12,31 @@ import com.claudecode.repl.ReplSession; import com.claudecode.tool.ToolContext; import com.claudecode.tool.ToolRegistry; import com.claudecode.tool.impl.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.nio.file.Path; +import java.util.Map; /** * 应用配置类 —— Spring Bean 装配。 *

* 集中管理所有组件的创建和依赖注入。 + * 通过 claude-code.provider 配置切换 API 提供者(openai / anthropic)。 */ @Configuration public class AppConfig { + private static final Logger log = LoggerFactory.getLogger(AppConfig.class); + + @Value("${claude-code.provider:openai}") + private String provider; + @Bean public ToolContext toolContext() { return ToolContext.defaultContext(); @@ -69,11 +79,45 @@ public class AppConfig { return registry; } + /** + * 根据 claude-code.provider 配置选择 ChatModel。 + * - "anthropic" → 使用 anthropicChatModel + * - "openai"(默认)→ 使用 openAiChatModel + */ + @Bean + public ChatModel activeChatModel( + @Qualifier("openAiChatModel") ChatModel openAiModel, + @Qualifier("anthropicChatModel") ChatModel anthropicModel) { + + if ("anthropic".equalsIgnoreCase(provider)) { + log.info("使用 Anthropic 原生 API"); + return anthropicModel; + } else { + log.info("使用 OpenAI 兼容 API"); + return openAiModel; + } + } + @Bean - public TokenTracker tokenTracker() { + public ProviderInfo providerInfo() { + String baseUrl; + String model; + + if ("anthropic".equalsIgnoreCase(provider)) { + baseUrl = System.getenv().getOrDefault("ANTHROPIC_BASE_URL", "https://api.anthropic.com"); + model = System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514"); + } else { + baseUrl = System.getenv().getOrDefault("AI_BASE_URL", "https://api.openai.com"); + model = System.getenv().getOrDefault("AI_OPENAI_MODEL", "gpt-4o"); + } + + return new ProviderInfo(provider, baseUrl, model); + } + + @Bean + public TokenTracker tokenTracker(ProviderInfo info) { TokenTracker tracker = new TokenTracker(); - String model = System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514"); - tracker.setModel(model); + tracker.setModel(info.model()); return tracker; } @@ -81,16 +125,13 @@ public class AppConfig { public String systemPrompt() { Path projectDir = Path.of(System.getProperty("user.dir")); - // 加载 CLAUDE.md ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir); String claudeMd = claudeLoader.load(); - // 加载 Skills SkillLoader skillLoader = new SkillLoader(projectDir); skillLoader.loadAll(); String skillsSummary = skillLoader.buildSkillsSummary(); - // 收集 Git 上下文 GitContext gitContext = new GitContext(projectDir).collect(); String gitSummary = gitContext.buildSummary(); @@ -102,14 +143,14 @@ public class AppConfig { } @Bean - public AgentLoop agentLoop(@Qualifier("anthropicChatModel") ChatModel chatModel, ToolRegistry toolRegistry, + public AgentLoop agentLoop(ChatModel activeChatModel, ToolRegistry toolRegistry, ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker) { - AgentLoop mainLoop = new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt, tokenTracker); + AgentLoop mainLoop = new AgentLoop(activeChatModel, toolRegistry, toolContext, systemPrompt, tokenTracker); - // 注册子 Agent 工厂到 ToolContext,使 AgentTool 能创建独立的 AgentLoop + // 注册子 Agent 工厂 toolContext.set(AgentTool.AGENT_FACTORY_KEY, (java.util.function.Function) prompt -> { - AgentLoop subLoop = new AgentLoop(chatModel, toolRegistry, toolContext, systemPrompt); + AgentLoop subLoop = new AgentLoop(activeChatModel, toolRegistry, toolContext, systemPrompt); return subLoop.run(prompt); }); @@ -118,7 +159,11 @@ public class AppConfig { @Bean public ReplSession replSession(AgentLoop agentLoop, ToolRegistry toolRegistry, - CommandRegistry commandRegistry) { - return new ReplSession(agentLoop, toolRegistry, commandRegistry); + CommandRegistry commandRegistry, ProviderInfo providerInfo) { + return new ReplSession(agentLoop, toolRegistry, commandRegistry, providerInfo); + } + + /** API 提供者信息,供 Banner 和命令显示 */ + public record ProviderInfo(String provider, String baseUrl, String model) { } } diff --git a/src/main/java/com/claudecode/repl/ReplSession.java b/src/main/java/com/claudecode/repl/ReplSession.java index 2521682..9fc8cd5 100644 --- a/src/main/java/com/claudecode/repl/ReplSession.java +++ b/src/main/java/com/claudecode/repl/ReplSession.java @@ -1,5 +1,6 @@ package com.claudecode.repl; +import com.claudecode.config.AppConfig.ProviderInfo; import com.claudecode.command.CommandContext; import com.claudecode.command.CommandRegistry; import com.claudecode.console.*; @@ -40,6 +41,7 @@ public class ReplSession { private final AgentLoop agentLoop; private final ToolRegistry toolRegistry; private final CommandRegistry commandRegistry; + private final ProviderInfo providerInfo; private final PrintStream out; private final ToolStatusRenderer toolStatusRenderer; private final MarkdownRenderer markdownRenderer; @@ -49,10 +51,12 @@ public class ReplSession { public ReplSession(AgentLoop agentLoop, ToolRegistry toolRegistry, - CommandRegistry commandRegistry) { + CommandRegistry commandRegistry, + ProviderInfo providerInfo) { this.agentLoop = agentLoop; this.toolRegistry = toolRegistry; this.commandRegistry = commandRegistry; + this.providerInfo = providerInfo; // 强制使用 UTF-8 编码输出,确保 emoji 等 Unicode 字符在 Windows 终端正常显示 this.out = new PrintStream(System.out, true, StandardCharsets.UTF_8); this.toolStatusRenderer = new ToolStatusRenderer(out); @@ -163,7 +167,13 @@ public class ReplSession { /** 打印启动 Banner(JLine 模式) */ private void printBanner(Terminal terminal) { BannerPrinter.printCompact(out); - out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir"))); + + // 显示 API 提供者、模型和 URL + out.println(AnsiStyle.dim(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase()) + + AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model())); + out.println(AnsiStyle.dim(" API URL: ") + AnsiStyle.cyan(providerInfo.baseUrl())); + + out.println(AnsiStyle.dim(" Work Dir: " + System.getProperty("user.dir"))); out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered")); boolean isDumb = "dumb".equals(terminal.getType()); @@ -188,7 +198,10 @@ public class ReplSession { private void startWithScanner() { BannerPrinter.printCompact(out); - out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir"))); + out.println(AnsiStyle.dim(" Provider: ") + AnsiStyle.cyan(providerInfo.provider().toUpperCase()) + + AnsiStyle.dim(" Model: ") + AnsiStyle.cyan(providerInfo.model())); + out.println(AnsiStyle.dim(" API URL: ") + AnsiStyle.cyan(providerInfo.baseUrl())); + out.println(AnsiStyle.dim(" Work Dir: " + System.getProperty("user.dir"))); out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered")); out.println(AnsiStyle.dim(" Mode: Scanner (basic input)")); out.println(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 224f476..5272061 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,21 +1,25 @@ spring: - # 主配置:使用 Anthropic 原生 API ai: + # === Anthropic 原生 API 配置 === + # 启用时设置 claude-code.provider=anthropic anthropic: api-key: ${ANTHROPIC_API_KEY:} - base-url: ${ANTHROPIC_BASE_URL:https://open.bigmodel.cn/api/anthropic} + base-url: ${ANTHROPIC_BASE_URL:https://api.anthropic.com} chat: options: - model: ${AI_MODEL:claude-sonnet-4} + model: ${AI_MODEL:claude-sonnet-4-20250514} max-tokens: ${AI_MAX_TOKENS:8096} temperature: 0.7 - # 备选:兼容 OpenAI 格式的 API(如自建代理) + + # === OpenAI 兼容 API 配置 === + # 启用时设置 claude-code.provider=openai(默认) + # 支持 OpenAI、Azure OpenAI、兼容 API 代理等 openai: api-key: ${AI_API_KEY:} base-url: ${AI_BASE_URL:https://api.openai.com} chat: options: - model: ${AI_OPENAI_MODEL:} + model: ${AI_OPENAI_MODEL:gpt-4o} # 不启动 Web 服务器(纯 CLI 模式) main: @@ -27,6 +31,11 @@ spring: virtual: enabled: true +# === Claude Code Java 自定义配置 === +claude-code: + # API 提供者:openai 或 anthropic + provider: ${CLAUDE_CODE_PROVIDER:openai} + # 日志:CLI 模式下只显示 WARN 及以上,保持控制台干净 logging: level: