Files
Aq-Accounting-Spring/src/main/java/com/accounting/config/AiConfig.java
T
2026-09-23 17:16:40 +08:00

129 lines
4.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.accounting.config;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* AI(LLM)接入配置。
*
* <p>走 OpenAI 兼容的 chat/completions 协议,**不绑定任何供应商** ——
* DeepSeek / 通义(compatible-mode)/ 智谱 / OpenAI 都只需要改 base-url 和 model:
* <pre>
* DeepSeek https://api.deepseek.com/v1
* 通义 https://dashscope.aliyuncs.com/compatible-mode/v1
* 智谱 https://open.bigmodel.cn/api/paas/v4
* OpenAI https://api.openai.com/v1
* </pre>
* 拼接规则是 {@code base-url + "/chat/completions"}。</p>
*
* <p>api-key 只从环境变量读,application.yml 里不写默认值。key 为空时
* {@link #isReady()} 为 false,AI 接口自动降级为「未配置」提示,而不是 500。</p>
*/
@Slf4j
@Configuration
public class AiConfig {
@Value("${ai.enabled:true}")
private boolean enabled;
/** 不带 /chat/completions 后缀 */
@Value("${ai.base-url:}")
private String baseUrl;
@Value("${ai.api-key:}")
private String apiKey;
@Value("${ai.model:}")
private String model;
/** 工具调用最多循环几轮:模型连续要数据时防止无限循环烧 token */
@Value("${ai.max-tool-rounds:3}")
private int maxToolRounds;
/** 单次对话最多带多少条历史消息,太久远的不带 */
@Value("${ai.max-history-messages:20}")
private int maxHistoryMessages;
public boolean isReady() {
return enabled && !baseUrl.isBlank() && !apiKey.isBlank() && !model.isBlank();
}
/**
* 启动时把配置状态打到日志里 —— 配错了不用猜,看 app.log 就知道。
* 注意**永远不打印 api-key 的内容**,只报长度。
*/
@PostConstruct
public void logConfigStatus() {
if (isReady()) {
log.info("AI 已启用:base-url={}, model={}, api-key 长度={}",
getBaseUrl(), model, apiKey.length());
} else {
log.warn("AI 未配置,助手功能将降级(status 返回 false,对话推 error 事件):"
+ "enabled={}, base-url={}, model={}, api-key={}",
enabled,
baseUrl.isBlank() ? "(空)" : baseUrl,
model.isBlank() ? "(空)" : model,
apiKey.isBlank() ? "(空)" : "已设置");
}
}
/**
* 容忍结尾多余的斜杠:否则会拼出 {@code //chat/completions},
* 网关直接 404,而且报错信息完全看不出是这个原因。
*/
public String getBaseUrl() {
return baseUrl.replaceAll("/+$", "");
}
public String getApiKey() {
return apiKey;
}
public String getModel() {
return model;
}
public int getMaxToolRounds() {
return maxToolRounds;
}
public int getMaxHistoryMessages() {
return maxHistoryMessages;
}
/**
* 出站调 LLM 用的 HttpClient。
*
* <p>刻意不复用现有的 RestTemplate —— 那个 5 秒读超时撑不住 LLM 的流式生成。
* 用 JDK 自带的 HttpClient 是为了**零新增 Maven 依赖**,流式读响应体靠
* {@code BodyHandlers.ofInputStream()}。</p>
*/
@Bean
public HttpClient aiHttpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
}
/**
* SSE 的工作线程池:controller 立刻返回 SseEmitter,
* 上游的流式读取和工具循环都在这个池子里跑,不占用 Tomcat 请求线程。
*/
@Bean(destroyMethod = "shutdown")
public ExecutorService aiExecutor() {
return Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "ai-chat");
t.setDaemon(true);
return t;
});
}
}