253 lines
12 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|