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> providers() { return Result.success(providerService.listVisible()); } @Operation(summary = "创建 Provider", description = "创建一个新的 AI Provider 配置。") @PostMapping("/providers") public Result createProvider(@RequestBody AiProvider provider) { return Result.success(providerService.saveProvider(provider)); } @Operation(summary = "更新 Provider", description = "更新已有的 AI Provider 配置。") @PutMapping("/providers") public Result updateProvider(@RequestBody AiProvider provider) { return Result.success(providerService.updateProvider(provider)); } @Operation(summary = "删除 Provider", description = "根据 ID 删除指定的 AI Provider 配置。") @DeleteMapping("/providers") public Result 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> endpoints() { return Result.success(endpointConfigService.listVisible()); } @Operation(summary = "获取 Endpoint 测试模板", description = "根据 Endpoint ID 获取对应的测试模板。") @GetMapping("/endpoints/test-template") public Result 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 createEndpoint(@RequestBody AiEndpointConfig endpoint) { return Result.success(endpointConfigService.saveEndpoint(endpoint)); } @Operation(summary = "更新 Endpoint", description = "更新已有的 AI Endpoint 配置。") @PutMapping("/endpoints") public Result updateEndpoint(@RequestBody AiEndpointConfig endpoint) { return Result.success(endpointConfigService.updateEndpoint(endpoint)); } @Operation(summary = "删除 Endpoint", description = "根据 ID 删除指定的 AI Endpoint 配置。") @DeleteMapping("/endpoints") public Result 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> scenes() { return Result.success(sceneBindingService.listVisible()); } @Operation(summary = "获取 Scene 测试模板", description = "根据场景编码获取对应的测试模板。") @GetMapping("/scenes/test-template") public Result sceneTestTemplate(@Parameter(description = "场景编码", required = true) @RequestParam String sceneCode) { return Result.success(runtimeService.buildSceneTestTemplate(sceneCode)); } @Operation(summary = "创建 Scene", description = "创建一个新的 AI Scene 绑定配置。") @PostMapping("/scenes") public Result 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 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 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> callLogs(@Parameter(description = "返回数量限制") @RequestParam(required = false) Integer limit) { return Result.success(callLogService.latest(limit)); } @Operation(summary = "分页查询调用日志", description = "分页查询 AI 调用日志,支持多条件筛选和关键词搜索。") @PostMapping("/call-logs") public Result> queryCallLogs(@RequestBody @Valid AiCallLogQueryRequest request) { return Result.success(callLogService.query(request)); } @Operation(summary = "查询运行时调用结果", description = "用户端根据 requestId 查询刚刚触发的 AI 调用结果,用于流式连接异常后的结果恢复。") @GetMapping("/runtime/result") public Result 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 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 endpointTest(@RequestBody JSONObject payload) { String endpointId = payload.getString("endpointId"); JSONObject inputs = payload.getJSONObject("inputs"); Map 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 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); } } }