v1.0.49 #61

Merged
timi merged 6 commits from dev into master 2026-09-16 17:34:23 +00:00
10 changed files with 441 additions and 72 deletions
Showing only changes of commit 0d155ec5b2 - Show all commits
@@ -14,10 +14,6 @@ import org.springframework.stereotype.Component;
@Component
public class GaoAiProperties {
/// MCP 服务间密钥
@Value("${gao.ai.mcp-service-key:}")
private String mcpServiceKey;
/// AI 会话有效期,单位秒
@Value("${gao.ai.session-ttl-seconds:300}")
private long sessionTtlSeconds;
@@ -33,16 +29,4 @@ public class GaoAiProperties {
/// Dify 流式读取空闲超时,单位秒
@Value("${gao.ai.read-timeout-seconds:600}")
private long readTimeoutSeconds;
/// true 为允许缺少 AI 会话时使用固定测试会话,仅用于本地调试
@Value("${gao.ai.allow-missing-session:false}")
private boolean allowMissingSession;
/// 缺少 AI 会话时使用的测试用户 ID
@Value("${gao.ai.debug-user-id:dify-agent-debug}")
private String debugUserId;
/// 缺少 AI 会话时使用的测试门店 ID
@Value("${gao.ai.debug-store-id:00000000-0000-0000-0000-000000000001}")
private String debugStoreId;
}
@@ -1,8 +1,11 @@
package com.imyeyu.api.modules.gao.ai.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.gao.ai.service.GaoAiAccessException;
import com.imyeyu.api.modules.gao.ai.service.GaoAiChatService;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationListRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationReadRequest;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
@@ -10,6 +13,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -38,7 +42,57 @@ public class GaoAiChatController {
try {
return chatService.chat(request);
} catch (GaoAiAccessException e) {
throw new ResponseStatusException(HttpStatus.valueOf(e.getStatus()), e.getMessage(), e);
throw accessException(e);
}
}
/// 列出当前登录用户的历史会话
///
/// @param request 历史会话分页请求
/// @return 历史会话分页结果
@RequestRateLimit
@PostMapping("/conversation/list")
public JsonNode listConversation(@RequestBody(required = false) GaoAiConversationListRequest request) {
try {
return chatService.listConversation(request);
} catch (GaoAiAccessException e) {
throw accessException(e);
}
}
/// 读取指定历史会话的消息
///
/// @param request 历史会话读取请求
/// @return 会话消息分页结果
@RequestRateLimit
@PostMapping("/conversation/read")
public JsonNode readConversation(@RequestBody GaoAiConversationReadRequest request) {
try {
return chatService.readConversation(request);
} catch (GaoAiAccessException e) {
throw accessException(e);
}
}
/// 删除当前登录用户的历史会话
///
/// @param conversationId Dify 会话 ID
/// @return 无
@RequestRateLimit
@PostMapping("/conversation/delete")
public void deleteConversation(@RequestParam String conversationId) {
try {
chatService.deleteConversation(conversationId);
} catch (GaoAiAccessException e) {
throw accessException(e);
}
}
/// 转换智能体访问异常
///
/// @param exception 智能体访问异常
/// @return HTTP 响应状态异常
private ResponseStatusException accessException(GaoAiAccessException exception) {
return new ResponseStatusException(HttpStatus.valueOf(exception.getStatus()), exception.getMessage(), exception);
}
}
@@ -1,9 +1,11 @@
package com.imyeyu.api.modules.gao.ai.mcp;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.gao.ai.config.GaoAiProperties;
import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession;
import com.imyeyu.api.modules.gao.ai.service.GaoAiSessionService;
import com.imyeyu.api.modules.gao.ai.service.implement.GaoAiSessionServiceImplement.GaoAiSessionException;
import com.imyeyu.api.modules.gao.bean.GaoSystemSetting;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -21,22 +23,21 @@ import java.security.MessageDigest;
public class GaoAiMcpAuthentication {
private final GaoAiProperties properties;
private final SettingService settingService;
private final GaoAiSessionService sessionService;
/// 校验 Dify 服务密钥和 AI 会话
///
/// @param authorization Authorization 请求头
/// @param sessionId X-Timi-AI-Session 请求头
/// @param headerSessionId X-Timi-AI-Session 请求头
/// @param argumentSessionId MCP 工具参数中的 AI 会话 ID
/// @param toolName 工具名称,可为空
/// @return 已认证会话
public GaoAiSession authenticate(String authorization, String sessionId, String toolName) {
public GaoAiSession authenticate(String authorization, String headerSessionId, String argumentSessionId, String toolName) {
authenticateService(authorization);
if (isBlank(sessionId) && properties.isAllowMissingSession()) {
GaoAiSession session = debugSession();
if (toolName != null && !session.getAllowedTools().contains(toolName)) {
throw new GaoAiSessionException("AI 会话无权调用该工具", 403);
}
return session;
String sessionId = headerSessionId;
if (isBlank(sessionId) && !isBlank(argumentSessionId)) {
sessionId = argumentSessionId;
}
GaoAiSession session = sessionService.require(sessionId);
if (toolName != null && (session.getAllowedTools() == null || !session.getAllowedTools().contains(toolName))) {
@@ -51,9 +52,9 @@ public class GaoAiMcpAuthentication {
///
/// @param authorization Authorization 请求头
public void authenticateService(String authorization) {
String expectedKey = trim(properties.getMcpServiceKey());
String key = settingService.getSystem(GaoSystemSetting.DIFY_MCP_SERVICE_KEY).asString();
String actualKey = bearerValue(authorization);
if (expectedKey.isEmpty() || actualKey.isEmpty() || !constantTimeEquals(expectedKey, actualKey)) {
if (key.isEmpty() || actualKey.isEmpty() || !constantTimeEquals(key, actualKey)) {
throw new GaoAiSessionException("MCP 服务认证失败", 401);
}
}
@@ -77,24 +78,6 @@ public class GaoAiMcpAuthentication {
return value == null ? "" : value.trim();
}
/// 创建固定的开发测试会话
///
/// @return 开发测试会话
private GaoAiSession debugSession() {
String userId = trim(properties.getDebugUserId());
String storeId = trim(properties.getDebugStoreId());
if (userId.isEmpty() || storeId.isEmpty()) {
throw new GaoAiSessionException("开发测试会话缺少用户或门店配置", 401);
}
GaoAiSession session = new GaoAiSession();
session.setSessionId("dify-agent-debug-bypass");
session.setUserId(userId);
session.setStoreId(storeId);
session.setAllowedTools(GaoAiToolNames.orderedReadOnlyTools());
session.setExpiresAt(Long.MAX_VALUE);
return session;
}
/// 判断字符串是否为空
///
/// @param value 待判断字符串
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.ai.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.gao.ai.entity.GaoAiAuditLog;
import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession;
import com.imyeyu.api.modules.gao.ai.mcp.tool.GaoAiTool;
@@ -90,12 +91,13 @@ public class GaoAiMcpController {
yield response(200, success(id, toolsResult()));
}
case "tools/call" -> {
GaoAiSession session = authentication.authenticate(authorization, aiSessionId, text(request.path("params"), "name"));
if (!hasText(aiSessionId)) {
log.warn("GAO MCP 使用开发测试会话:toolName={}, userId={}, storeId={}",
toolName, session.getUserId(), session.getStoreId());
JsonNode params = request.path("params");
String argumentSessionId = text(params.path("arguments"), "aiSessionId");
GaoAiSession session = authentication.authenticate(authorization, aiSessionId, argumentSessionId, text(params, "name"));
if (hasText(argumentSessionId) && !hasText(aiSessionId)) {
log.info("GAO MCP 使用工具参数中的 AI 会话:toolName={}, aiSessionLength={}", toolName, length(argumentSessionId));
}
yield response(200, success(id, executeTool(session, request.path("params"), aiSessionId)));
yield response(200, success(id, executeTool(session, params, hasText(aiSessionId) ? aiSessionId : argumentSessionId)));
}
default -> response(400, error(id, -32601, "MCP 方法不支持"));
};
@@ -126,12 +128,14 @@ public class GaoAiMcpController {
if (arguments == null || !arguments.isObject()) {
throw new GaoAiToolException("工具参数格式无效", 400);
}
if (containsField(arguments, "storeId")) {
ObjectNode businessArguments = ((ObjectNode) arguments).deepCopy();
businessArguments.remove("aiSessionId");
if (containsField(businessArguments, "storeId")) {
throw new GaoAiToolException("工具请求不允许携带 storeId", 403);
}
GaoAiToolRequest request;
try {
request = jackson.treeToValue(arguments, GaoAiToolRequest.class);
request = jackson.treeToValue(businessArguments, GaoAiToolRequest.class);
} catch (Exception e) {
throw new GaoAiToolException("工具参数格式无效", 400);
}
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiToolRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiToolResult;
import java.util.LinkedHashMap;
import java.util.Map;
/// GAO 智能体只读工具
@@ -36,15 +37,19 @@ public interface GaoAiTool {
),
"additionalProperties", false
);
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("query", Map.of("type", "string", "description", "自然语言关键词"));
properties.put("timeRange", timeRange);
properties.put("filters", Map.of("type", "object"));
properties.put("pageSize", Map.of("type", "integer", "minimum", 1, "maximum", 20));
properties.put("cursor", Map.of("type", "string"));
properties.put("aiSessionId", Map.of(
"type", "string",
"description", "由工作流变量注入的 AI 会话 ID,不允许由模型生成"
));
return Map.of(
"type", "object",
"properties", Map.of(
"query", Map.of("type", "string", "description", "自然语言关键词"),
"timeRange", timeRange,
"filters", Map.of("type", "object"),
"pageSize", Map.of("type", "integer", "minimum", 1, "maximum", 20),
"cursor", Map.of("type", "string")
),
"properties", properties,
"additionalProperties", false
);
}
@@ -1,6 +1,9 @@
package com.imyeyu.api.modules.gao.ai.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationListRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationReadRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/// GAO 智能体聊天服务
@@ -14,4 +17,21 @@ public interface GaoAiChatService {
/// @param request 聊天请求
/// @return SSE 发射器
SseEmitter chat(GaoAiChatRequest request);
/// 列出当前登录用户的历史会话
///
/// @param request 历史会话分页请求
/// @return 历史会话分页结果
JsonNode listConversation(GaoAiConversationListRequest request);
/// 读取当前登录用户的历史会话消息
///
/// @param request 历史会话读取请求
/// @return 会话消息分页结果
JsonNode readConversation(GaoAiConversationReadRequest request);
/// 删除当前登录用户的历史会话
///
/// @param conversationId Dify 会话 ID
void deleteConversation(String conversationId);
}
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.ai.service.implement;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.service.SettingService;
@@ -13,6 +14,8 @@ import com.imyeyu.api.modules.gao.ai.service.GaoAiAuditService;
import com.imyeyu.api.modules.gao.ai.service.GaoAiChatService;
import com.imyeyu.api.modules.gao.ai.service.GaoAiSessionService;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationListRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationReadRequest;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.bean.GaoSystemSetting;
import com.imyeyu.api.modules.gao.entity.GaoStore;
@@ -33,6 +36,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@@ -101,13 +105,7 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
if (request.getConversationId() != null && 128 < request.getConversationId().length()) {
throw new GaoAiAccessException("会话 ID 无效", 400);
}
User user = userLoginService.getLoginUser();
if (user == null) {
throw new GaoAiAccessException("登录已过期,请重新登录", 401);
}
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name())) {
throw new GaoAiAccessException("当前账号无权使用门店智能助手", 403);
}
User user = requireAiUser();
String storeId;
try {
storeId = storeScopeService.resolveStoreId(request.getStoreId());
@@ -122,6 +120,50 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
return stream(session, message);
}
@Override
public JsonNode listConversation(GaoAiConversationListRequest request) {
User user = requireAiUser();
String lastId = optionalValue(request == null ? null : request.getLastId(), "上一页游标", 128);
int limit = normalizeLimit(request == null ? null : request.getLimit());
String sortBy = normalizeSortBy(request == null ? null : request.getSortBy());
StringBuilder url = new StringBuilder("/conversations");
appendQueryParameter(url, "user", difyUser(user));
appendQueryParameter(url, "last_id", lastId);
appendQueryParameter(url, "limit", String.valueOf(limit));
appendQueryParameter(url, "sort_by", sortBy);
return sanitizeConversationList(parseJson(sendDify(newDifyRequest(url.toString()).GET().build(), "查询历史会话")));
}
@Override
public JsonNode readConversation(GaoAiConversationReadRequest request) {
User user = requireAiUser();
if (request == null) {
throw new GaoAiAccessException("会话读取参数不能为空", 400);
}
String conversationId = requiredValue(request.getConversationId(), "会话 ID", 128);
String firstId = optionalValue(request.getFirstId(), "上一页游标", 128);
int limit = normalizeLimit(request.getLimit());
StringBuilder url = new StringBuilder("/messages");
appendQueryParameter(url, "user", difyUser(user));
appendQueryParameter(url, "conversation_id", conversationId);
appendQueryParameter(url, "first_id", firstId);
appendQueryParameter(url, "limit", String.valueOf(limit));
return sanitizeConversationMessages(parseJson(sendDify(newDifyRequest(url.toString()).GET().build(), "读取历史会话")));
}
@Override
public void deleteConversation(String conversationId) {
User user = requireAiUser();
String value = requiredValue(conversationId, "会话 ID", 128);
ObjectNode body = jackson.createObjectNode();
body.put("user", difyUser(user));
HttpRequest request = newDifyRequest("/conversations/" + encodePathSegment(value))
.method("DELETE", HttpRequest.BodyPublishers.ofString(writeJson(body), StandardCharsets.UTF_8))
.header("Content-Type", "application/json")
.build();
sendDify(request, "删除历史会话");
}
private SseEmitter stream(GaoAiSession session, String message) {
long readTimeoutSeconds = positive(properties.getReadTimeoutSeconds(), 600L);
long emitterTimeout = Math.min(Long.MAX_VALUE - 1L, readTimeoutSeconds * 1000L + 30_000L);
@@ -190,19 +232,250 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
GaoStore store = storeService.get(session.getStoreId());
inputs.put("storeName", store == null ? "当前门店" : store.getName());
inputs.put("today", LocalDate.now(ZONE).toString());
String url = settingService.getSystem(DifySetting.BASE_URL).asString();
String appApiKey = settingService.getSystem(GaoSystemSetting.DIFY_APP_API_KEY).asString();
return HttpRequest.newBuilder()
.uri(URI.create(url + "/chat-messages"))
return newDifyRequest("/chat-messages")
.timeout(Duration.ofSeconds(readTimeoutSeconds))
.header("Authorization", "Bearer " + appApiKey)
.header("X-Timi-AI-Session", session.getSessionId())
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.setHeader("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(writeJson(body), StandardCharsets.UTF_8))
.build();
}
/// 获取当前登录且有权使用智能体的用户
///
/// @return 当前登录用户
private User requireAiUser() {
User user = userLoginService.getLoginUser();
if (user == null) {
throw new GaoAiAccessException("登录已过期,请重新登录", 401);
}
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name())) {
throw new GaoAiAccessException("当前账号无权使用门店智能助手", 403);
}
return user;
}
/// 创建 Dify API 请求
///
/// @param path Dify API 路径,可包含已编码的查询参数
/// @return Dify API 请求构造器
private HttpRequest.Builder newDifyRequest(String path) {
String appApiKey = settingService.getSystem(GaoSystemSetting.DIFY_APP_API_KEY).asString();
if (isBlank(appApiKey)) {
throw new GaoAiAccessException("智能体服务未配置", 503);
}
return HttpRequest.newBuilder(difyUri(path))
.timeout(Duration.ofSeconds(Math.min(60L, positive(properties.getReadTimeoutSeconds(), 600L))))
.header("Authorization", "Bearer " + appApiKey)
.header("Accept", "application/json");
}
/// 调用 Dify API
///
/// @param request Dify API 请求
/// @param operation 操作名称
/// @return Dify 响应正文
private String sendDify(HttpRequest request, String operation) {
HttpResponse<String> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GaoAiAccessException("智能体服务请求被中断", 503);
} catch (IOException | RuntimeException e) {
log.warn("{}失败", operation, e);
throw new GaoAiAccessException("智能体服务暂时不可用,请稍后重试", 503);
}
if (response.statusCode() < 200 || 299 < response.statusCode()) {
log.warn("{}失败:statusCode={}", operation, response.statusCode());
int status = response.statusCode() == 404 ? 404 : 503;
String message = status == 404 ? "历史会话不存在" : "智能体服务暂时不可用,请稍后重试";
throw new GaoAiAccessException(message, status);
}
return response.body();
}
/// 解析 Dify JSON 响应
///
/// @param responseBody 响应正文
/// @return JSON 节点
private JsonNode parseJson(String responseBody) {
try {
return jackson.readTree(responseBody);
} catch (IOException | RuntimeException e) {
log.warn("解析智能体历史会话响应失败", e);
throw new GaoAiAccessException("智能体服务返回数据无效", 503);
}
}
/// 过滤会话列表中的 Dify 内部字段
///
/// @param response Dify 响应
/// @return 脱敏后的响应
private JsonNode sanitizeConversationList(JsonNode response) {
if (response == null || !response.isObject()) {
return response;
}
ObjectNode result = jackson.createObjectNode();
copyFields(response, result, "has_more", "limit");
ArrayNode data = result.putArray("data");
for (JsonNode item : response.path("data")) {
if (!item.isObject()) {
continue;
}
ObjectNode conversation = data.addObject();
copyFields(item, conversation, "id", "name", "status", "introduction", "created_at", "updated_at");
}
return result;
}
/// 过滤会话消息中的 Dify 内部字段
///
/// @param response Dify 响应
/// @return 脱敏后的响应
private JsonNode sanitizeConversationMessages(JsonNode response) {
if (response == null || !response.isObject()) {
return response;
}
ObjectNode result = jackson.createObjectNode();
copyFields(response, result, "has_more", "limit");
ArrayNode data = result.putArray("data");
for (JsonNode item : response.path("data")) {
if (!item.isObject()) {
continue;
}
ObjectNode message = data.addObject();
copyFields(item, message, "id", "conversation_id", "query", "answer", "created_at", "status", "feedback");
}
return result;
}
/// 复制指定的 JSON 字段
///
/// @param source 源 JSON 对象
/// @param target 目标 JSON 对象
/// @param fieldNames 字段名称
/// @return 无
private void copyFields(JsonNode source, ObjectNode target, String... fieldNames) {
for (String fieldName : fieldNames) {
if (source.has(fieldName)) {
target.set(fieldName, source.get(fieldName));
}
}
}
/// 生成 Dify 用户标识
///
/// @param user 登录用户
/// @return Dify 用户标识
private String difyUser(User user) {
return "gao-store-manager:" + user.getId();
}
/// 添加 URL 查询参数
///
/// @param url URL 构造器
/// @param name 参数名称
/// @param value 参数值
/// @return 无
private void appendQueryParameter(StringBuilder url, String name, String value) {
if (isBlank(value)) {
return;
}
url.append(url.indexOf("?") < 0 ? '?' : '&')
.append(name)
.append('=')
.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
}
/// 生成 Dify API URI
///
/// @param path Dify API 路径
/// @return Dify API URI
private URI difyUri(String path) {
String baseUrl = settingService.getSystem(DifySetting.BASE_URL).asString();
if (isBlank(baseUrl)) {
throw new GaoAiAccessException("智能体服务未配置", 503);
}
String normalizedBaseUrl = baseUrl.trim();
while (normalizedBaseUrl.endsWith("/")) {
normalizedBaseUrl = normalizedBaseUrl.substring(0, normalizedBaseUrl.length() - 1);
}
try {
URI uri = URI.create(normalizedBaseUrl + (path.startsWith("/") ? path : "/" + path));
if (!uri.isAbsolute() || isBlank(uri.getScheme()) || isBlank(uri.getHost())) {
throw new IllegalArgumentException("Dify 地址必须是绝对 HTTP 地址");
}
return uri;
} catch (IllegalArgumentException e) {
throw new GaoAiAccessException("智能体服务配置无效", 503);
}
}
/// 编码 URL 路径片段
///
/// @param value 路径片段
/// @return 编码后的路径片段
private String encodePathSegment(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
}
/// 标准化分页条数
///
/// @param limit 请求的分页条数
/// @return 标准化后的分页条数
private int normalizeLimit(Integer limit) {
int value = limit == null ? 20 : limit;
if (!(0 < value) || 100 < value) {
throw new GaoAiAccessException("分页条数必须在 1 至 100 之间", 400);
}
return value;
}
/// 标准化会话排序方式
///
/// @param sortBy 请求的排序方式
/// @return 标准化后的排序方式
private String normalizeSortBy(String sortBy) {
String value = isBlank(sortBy) ? "-updated_at" : sortBy.trim();
if (!"created_at".equals(value) && !"-created_at".equals(value)
&& !"updated_at".equals(value) && !"-updated_at".equals(value)) {
throw new GaoAiAccessException("会话排序方式无效", 400);
}
return value;
}
/// 校验必填文本参数
///
/// @param value 参数值
/// @param name 参数名称
/// @param maxLength 最大长度
/// @return 去除首尾空白后的参数值
private String requiredValue(String value, String name, int maxLength) {
String result = optionalValue(value, name, maxLength);
if (result == null) {
throw new GaoAiAccessException(name + "不能为空", 400);
}
return result;
}
/// 校验可选文本参数
///
/// @param value 参数值
/// @param name 参数名称
/// @param maxLength 最大长度
/// @return 去除首尾空白后的参数值,无值时返回 null
private String optionalValue(String value, String name, int maxLength) {
if (isBlank(value)) {
return null;
}
String result = value.trim();
if (maxLength < result.length()) {
throw new GaoAiAccessException(name + "无效", 400);
}
return result;
}
private void readResponse(HttpResponse<InputStream> response, SseEmitter emitter, AtomicBoolean finished,
AtomicReference<CompletableFuture<HttpResponse<InputStream>>> upstreamRef, AtomicReference<ScheduledFuture<?>> timeoutRef,
AtomicReference<String> status, AtomicBoolean auditRecorded, GaoAiSession session, String message, long startedAt,
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.ai.vo;
import lombok.Data;
/// GAO 智能体历史会话列表请求
///
/// 字段与 Dify 会话列表分页参数对应,但用户标识由后端注入
///
/// @author Codex
/// @since 2026-09-16
@Data
public class GaoAiConversationListRequest {
/// 上一页最后一条会话 ID,可为空
private String lastId;
/// 返回条数,可为空
private Integer limit;
/// 排序方式,可选 `created_at`、`-created_at`、`updated_at`、`-updated_at`
private String sortBy;
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.ai.vo;
import lombok.Data;
/// GAO 智能体历史会话消息读取请求
///
/// 用户标识由后端注入,前端只能指定会话和分页游标
///
/// @author Codex
/// @since 2026-09-16
@Data
public class GaoAiConversationReadRequest {
/// Dify 会话 ID
private String conversationId;
/// 上一页第一条消息 ID,可为空
private String firstId;
/// 返回条数,可为空
private Integer limit;
}
@@ -17,6 +17,8 @@ public enum GaoSystemSetting implements SettingEnum {
DIFY_APP_API_KEY(null, Setting.ValueType.STRING),
DIFY_MCP_SERVICE_KEY(null, Setting.ValueType.STRING),
SERVICE_LICENSE(null, Setting.ValueType.STRING),
PRIVACY_LICENSE(null, Setting.ValueType.STRING),