From ad500f84af1df6070453fb584d3f34e7e60cddfc Mon Sep 17 00:00:00 2001 From: wangli Date: Mon, 9 Feb 2026 23:39:22 +0800 Subject: [PATCH] init --- pom.xml | 22 +++++ xianyu-ai/pom.xml | 39 ++++++++ .../biwin/xianyu/ai/config/AiProperties.java | 57 +++++++++++ .../xianyu/ai/controller/AiController.java | 53 ++++++++++ .../biwin/xianyu/ai/service/AiService.java | 22 +++++ .../xianyu/ai/service/impl/AiServiceImpl.java | 99 +++++++++++++++++++ xianyu-server/pom.xml | 5 + .../server/XianyuFreedomApplication.java | 2 +- .../src/main/resources/application.yml | 54 +++++++++- 9 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 xianyu-ai/pom.xml create mode 100644 xianyu-ai/src/main/java/top/biwin/xianyu/ai/config/AiProperties.java create mode 100644 xianyu-ai/src/main/java/top/biwin/xianyu/ai/controller/AiController.java create mode 100644 xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/AiService.java create mode 100644 xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/impl/AiServiceImpl.java diff --git a/pom.xml b/pom.xml index 5facaa2..fe786b0 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ xianyu-core xianyu-server xianyu-goofish + xianyu-ai 0.0.1-SNAPSHOT @@ -117,9 +118,30 @@ ${jjwt.version} + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + diff --git a/xianyu-ai/pom.xml b/xianyu-ai/pom.xml new file mode 100644 index 0000000..83b62da --- /dev/null +++ b/xianyu-ai/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + top.biwin + xianyu-freedom + ${revision} + + xianyu-ai + + + + top.biwin + xianyu-common + ${revision} + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + 0.8.1 + + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-web + + + cn.hutool + hutool-all + + + + diff --git a/xianyu-ai/src/main/java/top/biwin/xianyu/ai/config/AiProperties.java b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/config/AiProperties.java new file mode 100644 index 0000000..6355906 --- /dev/null +++ b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/config/AiProperties.java @@ -0,0 +1,57 @@ +package top.biwin.xianyu.ai.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * AI 模块配置属性 + * + * @author Little Code Sauce + * @since 2025 + */ +@Data +@Component +@ConfigurationProperties(prefix = "spring.ai.xianyu") +public class AiProperties { + + /** + * 默认使用的提供商名称 + */ + private String defaultProvider = "openai"; + + /** + * 提供商配置映射 + * key: 提供商名称 (e.g., openai, deepseek, qwen) + */ + private Map providers = new HashMap<>(); + + /** + * 单个提供商的配置 + */ + @Data + public static class ProviderConfig { + /** + * API Base URL + */ + private String baseUrl; + + /** + * API Key + */ + private String apiKey; + + /** + * 模型名称 + */ + private String model; + + /** + * 温度 (0.0 - 1.0) + */ + private Double temperature = 0.7; + } +} diff --git a/xianyu-ai/src/main/java/top/biwin/xianyu/ai/controller/AiController.java b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/controller/AiController.java new file mode 100644 index 0000000..77ed338 --- /dev/null +++ b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/controller/AiController.java @@ -0,0 +1,53 @@ +package top.biwin.xianyu.ai.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import top.biwin.xianyu.ai.service.AiService; +import top.biwin.xinayu.common.dto.response.BaseResponse; + +/** + * AI 交互控制器 + * + * @author Little Code Sauce + * @since 2025 + */ +@RestController +@RequestMapping("/ai") +@RequiredArgsConstructor +public class AiController { + + private final AiService aiService; + + /** + * 与 AI 对话 + * + * @param prompt 提示词 + * @param provider 提供商(可选,如 openai, deepseek, qwen) + * @return AI 回复 + */ + @PostMapping("/chat") + public ResponseEntity chat(@RequestParam("prompt") String prompt, + @RequestParam(value = "provider", required = false) String provider) { + if (!StringUtils.hasText(prompt)) { + return ResponseEntity.ok(BaseResponse.builder() + .success(false) + .message("提示词不能为空") + .build()); + } + + String responseContent = aiService.chat(prompt, provider); + + BaseResponse response = BaseResponse.builder() + .success(true) + .message("操作成功") + .data(responseContent) + .build(); + + return ResponseEntity.ok(response); + } +} diff --git a/xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/AiService.java b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/AiService.java new file mode 100644 index 0000000..42981a7 --- /dev/null +++ b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/AiService.java @@ -0,0 +1,22 @@ +package top.biwin.xianyu.ai.service; + +/** + * AI 服务接口 + *

+ * 提供与 AI 模型交互的核心能力 + *

+ * + * @author Little Code Sauce + * @since 2025 + */ +public interface AiService { + + /** + * 发送提示词并获取 AI 回复 + * + * @param prompt 用户输入的提示词 + * @param provider 指定的 AI 提供商(如果为 null,则使用默认配置) + * @return AI 的文本回复 + */ + String chat(String prompt, String provider); +} diff --git a/xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/impl/AiServiceImpl.java b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/impl/AiServiceImpl.java new file mode 100644 index 0000000..0d1fee9 --- /dev/null +++ b/xianyu-ai/src/main/java/top/biwin/xianyu/ai/service/impl/AiServiceImpl.java @@ -0,0 +1,99 @@ +package top.biwin.xianyu.ai.service.impl; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.ChatClient; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatClient; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.stereotype.Service; +import top.biwin.xianyu.ai.config.AiProperties; +import top.biwin.xianyu.ai.service.AiService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * AI 服务实现类 + *

+ * 支持多厂商动态切换 + *

+ * + * @author Little Code Sauce + * @since 2025 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AiServiceImpl implements AiService { + + private final AiProperties aiProperties; + + // 本地缓存 ChatClient,避免重复创建 overhead + private final Map clientCache = new ConcurrentHashMap<>(); + + @Override + public String chat(String promptText, String provider) { + // 1. 确定使用的 provider + String targetProvider = StrUtil.isBlank(provider) ? aiProperties.getDefaultProvider() : provider; + log.info("Processing AI request with provider: {}", targetProvider); + + // 2. 获取或创建 Client + ChatClient chatClient = getClient(targetProvider); + if (chatClient == null) { + return "Configuration for provider '" + targetProvider + "' not found. Please check your application.yml 🐶"; + } + + // 3. 执行调用 + try { + // 注意:0.8.1 版本中 OpenAiChatClient.call(String) 返回 String + return chatClient.call(promptText); + } catch (Exception e) { + log.error("AI call failed for provider: {}", targetProvider, e); + return "AI request failed: " + e.getMessage(); + } + } + + /** + * 获取或懒加载创建 ChatClient + */ + private ChatClient getClient(String providerName) { + if (clientCache.containsKey(providerName)) { + return clientCache.get(providerName); + } + + AiProperties.ProviderConfig config = aiProperties.getProviders().get(providerName); + if (config == null) { + log.warn("No configuration found for provider: {}", providerName); + return null; + } + + log.info("Creating new ChatClient for provider: {} (URL: {}, Model: {})", + providerName, config.getBaseUrl(), config.getModel()); + + try { + // 构造 OpenAiApi + // 0.8.1 API: new OpenAiApi(baseUrl, apiKey) + OpenAiApi openAiApi = new OpenAiApi(config.getBaseUrl(), config.getApiKey()); + + // 构造 Options + OpenAiChatOptions options = OpenAiChatOptions.builder() + .withModel(config.getModel()) + .withTemperature(config.getTemperature().floatValue()) + .build(); + + // 构造 Client + // 0.8.1 API: new OpenAiChatClient(api, options) + OpenAiChatClient client = new OpenAiChatClient(openAiApi, options); + + clientCache.put(providerName, client); + return client; + } catch (Exception e) { + log.error("Failed to create ChatClient for provider: {}", providerName, e); + return null; + } + } +} diff --git a/xianyu-server/pom.xml b/xianyu-server/pom.xml index 95c3a69..01bc567 100644 --- a/xianyu-server/pom.xml +++ b/xianyu-server/pom.xml @@ -27,6 +27,11 @@ xianyu-goofish ${revision} + + top.biwin + xianyu-ai + ${revision} + org.springframework.boot spring-boot-starter-web diff --git a/xianyu-server/src/main/java/top/biwin/xianyu/server/XianyuFreedomApplication.java b/xianyu-server/src/main/java/top/biwin/xianyu/server/XianyuFreedomApplication.java index a39ff46..e22b35c 100644 --- a/xianyu-server/src/main/java/top/biwin/xianyu/server/XianyuFreedomApplication.java +++ b/xianyu-server/src/main/java/top/biwin/xianyu/server/XianyuFreedomApplication.java @@ -15,7 +15,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author wangli * @since 2026-01-20 23:51 */ -@SpringBootApplication(scanBasePackages = "top.biwin") +@SpringBootApplication(scanBasePackages = "top.biwin", exclude = {org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration.class}) @EntityScan(basePackages = "top.biwin.xianyu.core.entity") @EnableJpaRepositories(basePackages = "top.biwin.xianyu.core.repository") //@EnableConfigurationProperties diff --git a/xianyu-server/src/main/resources/application.yml b/xianyu-server/src/main/resources/application.yml index d463285..4221de1 100644 --- a/xianyu-server/src/main/resources/application.yml +++ b/xianyu-server/src/main/resources/application.yml @@ -12,6 +12,58 @@ spring: application: name: xianyu-freedom + # Spring AI Multi-Provider Configuration 🐶 + ai: + xianyu: + default-provider: qwen # 默认使用 openai + providers: + # 1. OpenAI (官方) + openai: + base-url: ${OPENAI_BASE_URL:https://api.openai.com/v1} + api-key: ${OPENAI_API_KEY:sk-placeholder} + model: gpt-3.5-turbo + temperature: 0.7 + + # 2. DeepSeek (深度求索) - OpenAI Compatible + deepseek: + base-url: ${DEEPSEEK_BASE_URL:https://api.deepseek.com/v1} + api-key: ${DEEPSEEK_API_KEY:sk-placeholder} + model: deepseek-chat + temperature: 0.7 + + # 3. QianWen (通义千问) - OpenAI Compatible (or via DashScope) + qwen: + base-url: ${QWEN_BASE_URL:https://dashscope.aliyuncs.com/compatible-mode} + api-key: ${QWEN_API_KEY:sk-9e676358ac5e4c4cab54c953f59c28dc} + model: qwen-plus + temperature: 0.8 + + # 4. Moonshot (Kimi) + moonshot: + base-url: ${MOONSHOT_BASE_URL:https://api.moonshot.cn/v1} + api-key: ${MOONSHOT_API_KEY:sk-placeholder} + model: moonshot-v1-8k + + # 5. Grok (xAI) + grok: + base-url: ${GROK_BASE_URL:https://api.x.ai/v1} + api-key: ${GROK_API_KEY:sk-placeholder} + model: grok-beta + + # 6. Doubao (Bytedance) - Via Ark + doubao: + base-url: ${DOUBAO_BASE_URL:https://ark.cn-beijing.volces.com/api/v3} + api-key: ${DOUBAO_API_KEY:sk-placeholder} + model: ${DOUBAO_MODEL:ep-20240604015538-2q2h9} # Doubao models need specific Endpoint IDs + + # 7. Gemini (Google) - Via OpenAI Compatible Endpoint (Proxy) recommended for 0.8.1 + # Note: Direct Gemini requires VertexAI or Gemini specific starter, keeping it consistent here via proxy if available + # Or you can define it here and we might use a different logic in future, but for now assuming OpenAI compat layer + gemini: + base-url: ${GEMINI_BASE_URL:https://generativelanguage.googleapis.com/v1beta/openai/} + api-key: ${GEMINI_API_KEY:placeholder} + model: gemini-1.5-flash + datasource: driver-class-name: org.sqlite.JDBC url: jdbc:sqlite:db/xianyu_data.db @@ -26,7 +78,7 @@ spring: database-platform: org.hibernate.community.dialect.SQLiteDialect hibernate: ddl-auto: ${app.ddl-auto:update} - show-sql: true # Set to false to disable SQL logging + show-sql: false # Set to false to disable SQL logging open-in-view: false # 生产环境最佳实践,避免懒加载问题 properties: hibernate: