Files
happy-life-star/server/src/main/java/com/emotion/controller/AiRoutingController.java
T

253 lines
12 KiB
Java

package com.emotion.controller;
import com.alibaba.fastjson2.JSONObject;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.ai.AiCallLogQueryRequest;
import com.emotion.dto.request.ai.AiRuntimeRequest;
import javax.validation.Valid;
import com.emotion.dto.response.ai.AiTestTemplateResponse;
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
import com.emotion.dto.response.ai.AiStreamEvent;
import com.emotion.entity.AiCallLog;
import com.emotion.entity.AiEndpointConfig;
import com.emotion.entity.AiProvider;
import com.emotion.entity.AiSceneBinding;
import com.emotion.service.AiCallLogService;
import com.emotion.service.AiEndpointConfigService;
import com.emotion.service.AiProviderService;
import com.emotion.service.AiRuntimeService;
import com.emotion.service.AiSceneBindingService;
import com.emotion.util.UserContextHolder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Slf4j
@RestController
@RequestMapping("/ai")
@Tag(name = "AI 路由管理", description = "AI 服务提供商路由管理接口,包括 Provider/Endpoint/Scene 的 CRUD、调用日志、运行时测试和流式测试")
public class AiRoutingController {
private final AiProviderService providerService;
private final AiEndpointConfigService endpointConfigService;
private final AiSceneBindingService sceneBindingService;
private final AiCallLogService callLogService;
private final AiRuntimeService runtimeService;
public AiRoutingController(AiProviderService providerService,
AiEndpointConfigService endpointConfigService,
AiSceneBindingService sceneBindingService,
AiCallLogService callLogService,
AiRuntimeService runtimeService) {
this.providerService = providerService;
this.endpointConfigService = endpointConfigService;
this.sceneBindingService = sceneBindingService;
this.callLogService = callLogService;
this.runtimeService = runtimeService;
}
@Operation(summary = "查询 Provider 列表", description = "查询所有可见的 AI Provider 列表。")
@GetMapping("/providers")
public Result<List<AiProvider>> providers() {
return Result.success(providerService.listVisible());
}
@Operation(summary = "创建 Provider", description = "创建一个新的 AI Provider 配置。")
@PostMapping("/providers")
public Result<AiProvider> createProvider(@RequestBody AiProvider provider) {
return Result.success(providerService.saveProvider(provider));
}
@Operation(summary = "更新 Provider", description = "更新已有的 AI Provider 配置。")
@PutMapping("/providers")
public Result<AiProvider> updateProvider(@RequestBody AiProvider provider) {
return Result.success(providerService.updateProvider(provider));
}
@Operation(summary = "删除 Provider", description = "根据 ID 删除指定的 AI Provider 配置。")
@DeleteMapping("/providers")
public Result<Void> deleteProvider(@Parameter(description = "Provider ID", required = true) @RequestParam String id) {
providerService.removeById(id);
return Result.success();
}
@Operation(summary = "查询 Endpoint 列表", description = "查询所有可见的 AI Endpoint 配置列表。")
@GetMapping("/endpoints")
public Result<List<AiEndpointConfig>> endpoints() {
return Result.success(endpointConfigService.listVisible());
}
@Operation(summary = "获取 Endpoint 测试模板", description = "根据 Endpoint ID 获取对应的测试模板。")
@GetMapping("/endpoints/test-template")
public Result<AiTestTemplateResponse> endpointTestTemplate(@Parameter(description = "Endpoint ID", required = true) @RequestParam String id) {
return Result.success(runtimeService.buildEndpointTestTemplate(id));
}
@Operation(summary = "创建 Endpoint", description = "创建一个新的 AI Endpoint 配置。")
@PostMapping("/endpoints")
public Result<AiEndpointConfig> createEndpoint(@RequestBody AiEndpointConfig endpoint) {
return Result.success(endpointConfigService.saveEndpoint(endpoint));
}
@Operation(summary = "更新 Endpoint", description = "更新已有的 AI Endpoint 配置。")
@PutMapping("/endpoints")
public Result<AiEndpointConfig> updateEndpoint(@RequestBody AiEndpointConfig endpoint) {
return Result.success(endpointConfigService.updateEndpoint(endpoint));
}
@Operation(summary = "删除 Endpoint", description = "根据 ID 删除指定的 AI Endpoint 配置。")
@DeleteMapping("/endpoints")
public Result<Void> deleteEndpoint(@Parameter(description = "Endpoint ID", required = true) @RequestParam String id) {
endpointConfigService.removeById(id);
return Result.success();
}
@Operation(summary = "查询 Scene 列表", description = "查询所有可见的 AI Scene 绑定配置列表。")
@GetMapping("/scenes")
public Result<List<AiSceneBinding>> scenes() {
return Result.success(sceneBindingService.listVisible());
}
@Operation(summary = "获取 Scene 测试模板", description = "根据场景编码获取对应的测试模板。")
@GetMapping("/scenes/test-template")
public Result<AiTestTemplateResponse> sceneTestTemplate(@Parameter(description = "场景编码", required = true) @RequestParam String sceneCode) {
return Result.success(runtimeService.buildSceneTestTemplate(sceneCode));
}
@Operation(summary = "创建 Scene", description = "创建一个新的 AI Scene 绑定配置。")
@PostMapping("/scenes")
public Result<AiSceneBinding> createScene(@RequestBody AiSceneBinding scene) {
if (scene.getIsEnabled() == null) {
scene.setIsEnabled(1);
}
if (scene.getRequiredStream() == null) {
scene.setRequiredStream(1);
}
sceneBindingService.save(scene);
return Result.success(scene);
}
@Operation(summary = "更新 Scene", description = "更新已有的 AI Scene 绑定配置。")
@PutMapping("/scenes")
public Result<AiSceneBinding> updateScene(@RequestBody AiSceneBinding scene) {
sceneBindingService.updateById(scene);
return Result.success(sceneBindingService.getById(scene.getId()));
}
@Operation(summary = "删除 Scene", description = "根据 ID 删除指定的 AI Scene 绑定配置。")
@DeleteMapping("/scenes")
public Result<Void> deleteScene(@Parameter(description = "Scene ID", required = true) @RequestParam String id) {
sceneBindingService.removeById(id);
return Result.success();
}
@Operation(summary = "查询调用日志", description = "查询 AI 调用日志列表,支持限制返回数量。")
@GetMapping("/call-logs")
public Result<List<AiCallLog>> callLogs(@Parameter(description = "返回数量限制") @RequestParam(required = false) Integer limit) {
return Result.success(callLogService.latest(limit));
}
@Operation(summary = "分页查询调用日志", description = "分页查询 AI 调用日志,支持多条件筛选和关键词搜索。")
@PostMapping("/call-logs")
public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLogQueryRequest request) {
return Result.success(callLogService.query(request));
}
@Operation(summary = "查询运行时调用结果", description = "用户端根据 requestId 查询刚刚触发的 AI 调用结果,用于流式连接异常后的结果恢复。")
@GetMapping("/runtime/result")
public Result<AiCallLog> runtimeResult(@RequestParam String requestId) {
AiCallLog log = callLogService.findByRequestId(requestId, UserContextHolder.getCurrentUserId());
if (log == null) {
return Result.notFound("AI 调用结果未生成");
}
return Result.success(log);
}
@Operation(summary = "运行时测试", description = "对指定的 AI 配置进行运行时连通性测试,支持同步和流式模式。")
@PostMapping("/runtime/test")
public Result<AiRuntimeTestResponse> runtimeTest(@RequestBody JSONObject payload) {
AiRuntimeRequest request = withCurrentUser(AiRuntimeRequest.fromPayload(payload));
return Result.success(runtimeService.test(request));
}
@Operation(summary = "流式运行时测试", description = "对指定的 AI 配置进行流式运行时连通性测试,以 SSE 事件流返回结果。")
@PostMapping("/runtime/stream")
public SseEmitter runtimeStream(@RequestBody JSONObject payload) {
AiRuntimeRequest request = withCurrentUser(AiRuntimeRequest.fromPayload(payload));
SseEmitter emitter = new SseEmitter(0L);
CompletableFuture.runAsync(() -> {
runtimeService.invokeStream(request, event -> sendEvent(emitter, event));
emitter.complete();
}).exceptionally(error -> {
sendEvent(emitter, AiStreamEvent.error("AI_STREAM_INTERRUPTED", error.getMessage()));
emitter.completeWithError(error);
return null;
});
return emitter;
}
@Operation(summary = "Endpoint 运行时测试", description = "对指定的 Endpoint 进行运行时连通性测试。")
@PostMapping("/endpoint/test")
public Result<AiRuntimeTestResponse> endpointTest(@RequestBody JSONObject payload) {
String endpointId = payload.getString("endpointId");
JSONObject inputs = payload.getJSONObject("inputs");
Map<String, Object> inputMap = inputs == null ? Map.of() : inputs;
return Result.success(runtimeService.testEndpoint(endpointId, inputMap));
}
@Operation(summary = "Endpoint 流式测试", description = "对指定的 Endpoint 进行流式运行时测试,以 SSE 事件流返回结果。")
@PostMapping("/endpoint/stream")
public SseEmitter endpointStream(@RequestBody JSONObject payload) {
String endpointId = payload.getString("endpointId");
JSONObject inputs = payload.getJSONObject("inputs");
Map<String, Object> inputMap = inputs == null ? Map.of() : inputs;
SseEmitter emitter = new SseEmitter(0L);
CompletableFuture.runAsync(() -> {
runtimeService.invokeEndpointStream(endpointId, inputMap, event -> sendEvent(emitter, event));
emitter.complete();
}).exceptionally(error -> {
sendEvent(emitter, AiStreamEvent.error("AI_ENDPOINT_TEST_INTERRUPTED", error.getMessage()));
emitter.completeWithError(error);
return null;
});
return emitter;
}
private AiRuntimeRequest withCurrentUser(AiRuntimeRequest request) {
request.setUserId(UserContextHolder.getCurrentUserId());
request.setUserName(UserContextHolder.getCurrentUsername());
request.setUserType(UserContextHolder.getCurrentUserType());
if (!org.springframework.util.StringUtils.hasText(request.getRequestId())) {
request.setRequestId(UserContextHolder.getRequestId());
}
return request;
}
private void sendEvent(SseEmitter emitter, AiStreamEvent event) {
try {
emitter.send(SseEmitter.event()
.name(event.getType())
.data(event));
} catch (IOException e) {
log.warn("AI stream client disconnected: {}", e.getMessage());
throw new IllegalStateException("AI_STREAM_CLIENT_DISCONNECTED", e);
}
}
}