This commit is contained in:
van
2026-04-04 16:20:42 +08:00
parent 42cd5f8513
commit f0a3ce5afb
3 changed files with 135 additions and 11 deletions

View File

@@ -1,7 +1,7 @@
package cn.van.business.service;
import cn.hutool.core.util.StrUtil;
import cn.van.business.util.ds.OllamaClientUtil;
import cn.van.business.util.ds.SocialMediaLlmClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
@@ -23,7 +23,7 @@ import java.util.Map;
public class SocialMediaService {
@Autowired
private OllamaClientUtil ollamaClientUtil;
private SocialMediaLlmClient socialMediaLlmClient;
@Autowired
private MarketingImageService marketingImageService;
@@ -102,7 +102,7 @@ public class SocialMediaService {
String promptTemplate = getPromptTemplate("keywords", DEFAULT_KEYWORDS_PROMPT);
String prompt = String.format(promptTemplate, productName);
String response = ollamaClientUtil.getResponse(prompt);
String response = socialMediaLlmClient.getResponse(prompt);
if (StrUtil.isNotBlank(response)) {
// 解析关键词
@@ -191,7 +191,7 @@ public class SocialMediaService {
String prompt = String.format(promptTemplate, productName, priceInfo.toString(), keywordsInfo);
String content = ollamaClientUtil.getResponse(prompt.toString());
String content = socialMediaLlmClient.getResponse(prompt.toString());
if (StrUtil.isNotBlank(content)) {
result.put("success", true);

View File

@@ -1,5 +1,6 @@
package cn.van.business.util.ds;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
@@ -28,12 +29,17 @@ public class OllamaClientUtil {
private String ollamaModel;
/**
* 调用 Ollama /api/chat 并返回助手回复的文本内容
*
* @param inputText 用户输入的文本
* @return 模型回复的文本内容
* 调用 Ollama /api/chat 并返回助手回复的文本内容(使用 application.yml 默认地址与模型)
*/
public String getResponse(String inputText) throws IOException {
return getResponse(inputText, null, null);
}
/**
* @param overrideBaseUrl 非空时覆盖配置的 Ollama 根地址(不含路径,如 http://127.0.0.1:11434
* @param overrideModel 非空时覆盖配置的模型名
*/
public String getResponse(String inputText, String overrideBaseUrl, String overrideModel) throws IOException {
if (inputText == null || inputText.trim().isEmpty()) {
throw new IllegalArgumentException("输入文本不能为空");
}
@@ -41,9 +47,12 @@ public class OllamaClientUtil {
throw new IllegalArgumentException("输入文本过长");
}
String url = ollamaBaseUrl.replaceAll("/$", "") + "/api/chat";
String base = StrUtil.isNotBlank(overrideBaseUrl) ? overrideBaseUrl.trim() : ollamaBaseUrl;
String model = StrUtil.isNotBlank(overrideModel) ? overrideModel.trim() : ollamaModel;
String url = base.replaceAll("/$", "") + "/api/chat";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", ollamaModel);
requestBody.put("model", model);
requestBody.put("messages", new Object[]{
Map.of("role", "user", "content", inputText)
});
@@ -58,7 +67,7 @@ public class OllamaClientUtil {
.body(jsonBody)
.timeout(60000);
logger.info("请求 Ollama API: URL={}, model={}", url, ollamaModel);
logger.info("请求 Ollama API: URL={}, model={}", url, model);
HttpResponse response = request.execute();

View File

@@ -0,0 +1,115 @@
package cn.van.business.util.ds;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 社媒文案/关键词所用大模型客户端:支持本地 Ollama 或与 OpenAI 兼容的 HTTP 接口(含远程 API、Ollama /v1 等)。
* 配置存 Redis与若依后台「大模型接入」一致未配置时走 {@link OllamaClientUtil} 的 yml 默认。
*/
@Component
public class SocialMediaLlmClient {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SocialMediaLlmClient.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String KEY_MODE = "social_media:llm:mode";
private static final String KEY_BASE_URL = "social_media:llm:base_url";
private static final String KEY_API_KEY = "social_media:llm:api_key";
private static final String KEY_MODEL = "social_media:llm:model";
private static final String MODE_OLLAMA = "ollama";
private static final String MODE_OPENAI = "openai";
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
@Autowired
private OllamaClientUtil ollamaClientUtil;
public String getResponse(String inputText) throws IOException {
if (redisTemplate == null) {
return ollamaClientUtil.getResponse(inputText);
}
String mode = trimOrNull(redisTemplate.opsForValue().get(KEY_MODE));
String baseUrl = trimOrNull(redisTemplate.opsForValue().get(KEY_BASE_URL));
String apiKey = trimOrNull(redisTemplate.opsForValue().get(KEY_API_KEY));
String model = trimOrNull(redisTemplate.opsForValue().get(KEY_MODEL));
if (mode == null && baseUrl == null && model == null && apiKey == null) {
return ollamaClientUtil.getResponse(inputText);
}
String effectiveMode = mode != null ? mode.toLowerCase() : MODE_OLLAMA;
if (MODE_OPENAI.equals(effectiveMode)) {
if (StrUtil.isBlank(baseUrl)) {
throw new IOException("OpenAI 兼容模式需在后台配置完整的 Chat Completions 地址");
}
if (StrUtil.isBlank(model)) {
throw new IOException("OpenAI 兼容模式需在后台配置模型名称");
}
return callOpenAiCompatible(baseUrl, apiKey, model, inputText);
}
return ollamaClientUtil.getResponse(inputText, baseUrl, model);
}
private static String trimOrNull(String s) {
if (s == null) {
return null;
}
String t = s.trim();
return t.isEmpty() ? null : t;
}
private String callOpenAiCompatible(String chatCompletionsUrl, String apiKey, String model, String userText)
throws IOException {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model);
requestBody.put("messages", new Map[]{
Map.of("role", "user", "content", userText)
});
requestBody.put("temperature", 0.7);
String jsonBody = objectMapper.writeValueAsString(requestBody);
HttpRequest request = HttpRequest.of(chatCompletionsUrl.trim())
.method(Method.POST)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(jsonBody)
.timeout(120000);
if (StrUtil.isNotBlank(apiKey)) {
request.header("Authorization", "Bearer " + apiKey.trim());
}
log.info("请求 OpenAI 兼容 API: URL={}, model={}", chatCompletionsUrl, model);
HttpResponse response = request.execute();
if (response.getStatus() != 200) {
log.error("OpenAI 兼容 API 失败: status={}, body={}", response.getStatus(), response.body());
throw new IOException("API 调用失败HTTP 状态码: " + response.getStatus());
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode choices = root.path("choices");
if (choices.isEmpty() || !choices.get(0).path("message").path("content").isTextual()) {
throw new IOException("API 返回数据格式异常,未找到回复内容");
}
return choices.get(0).path("message").path("content").asText();
}
}