This commit is contained in:
wangli 2026-02-09 23:39:22 +08:00
parent ec792f0c44
commit ad500f84af
9 changed files with 351 additions and 2 deletions

22
pom.xml
View File

@ -19,6 +19,7 @@
<module>xianyu-core</module>
<module>xianyu-server</module>
<module>xianyu-goofish</module>
<module>xianyu-ai</module>
</modules>
<properties>
<revision>0.0.1-SNAPSHOT</revision>
@ -117,9 +118,30 @@
<version>${jjwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<build>
<plugins>
<plugin>

39
xianyu-ai/pom.xml Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.biwin</groupId>
<artifactId>xianyu-freedom</artifactId>
<version>${revision}</version>
</parent>
<artifactId>xianyu-ai</artifactId>
<dependencies>
<dependency>
<groupId>top.biwin</groupId>
<artifactId>xianyu-common</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>0.8.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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<String, ProviderConfig> 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;
}
}

View File

@ -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<BaseResponse> 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);
}
}

View File

@ -0,0 +1,22 @@
package top.biwin.xianyu.ai.service;
/**
* AI 服务接口
* <p>
* 提供与 AI 模型交互的核心能力
* </p>
*
* @author Little Code Sauce
* @since 2025
*/
public interface AiService {
/**
* 发送提示词并获取 AI 回复
*
* @param prompt 用户输入的提示词
* @param provider 指定的 AI 提供商如果为 null则使用默认配置
* @return AI 的文本回复
*/
String chat(String prompt, String provider);
}

View File

@ -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 服务实现类
* <p>
* 支持多厂商动态切换
* </p>
*
* @author Little Code Sauce
* @since 2025
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AiServiceImpl implements AiService {
private final AiProperties aiProperties;
// 本地缓存 ChatClient避免重复创建 overhead
private final Map<String, ChatClient> 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;
}
}
}

View File

@ -27,6 +27,11 @@
<artifactId>xianyu-goofish</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>top.biwin</groupId>
<artifactId>xianyu-ai</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@ -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

View File

@ -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: