feat: AI端点测试动态参数表单、接口工作流行内测试、本地开发环境改为线上域名
- 后端新增 /ai/endpoint/test 和 /ai/endpoint/stream 接口,支持直接端点测试 - 前端增加行内测试功能(场景绑定+接口工作流) - 测试对话框增加动态参数表单和参数定义编辑 - 支持 _meta 格式的默认输入参数处理 - web、web-admin 本地开发环境 API 调用改为线上域名 https://lifescript.happylifeos.com Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package com.emotion.controller;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
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;
|
||||
@@ -80,6 +81,11 @@ public class AiRoutingController {
|
||||
return Result.success(endpointConfigService.listVisible());
|
||||
}
|
||||
|
||||
@GetMapping("/endpoints/test-template")
|
||||
public Result<AiTestTemplateResponse> endpointTestTemplate(@RequestParam String id) {
|
||||
return Result.success(runtimeService.buildEndpointTestTemplate(id));
|
||||
}
|
||||
|
||||
@PostMapping("/endpoints")
|
||||
public Result<AiEndpointConfig> createEndpoint(@RequestBody AiEndpointConfig endpoint) {
|
||||
return Result.success(endpointConfigService.saveEndpoint(endpoint));
|
||||
@@ -101,6 +107,11 @@ public class AiRoutingController {
|
||||
return Result.success(sceneBindingService.listVisible());
|
||||
}
|
||||
|
||||
@GetMapping("/scenes/test-template")
|
||||
public Result<AiTestTemplateResponse> sceneTestTemplate(@RequestParam String sceneCode) {
|
||||
return Result.success(runtimeService.buildSceneTestTemplate(sceneCode));
|
||||
}
|
||||
|
||||
@PostMapping("/scenes")
|
||||
public Result<AiSceneBinding> createScene(@RequestBody AiSceneBinding scene) {
|
||||
if (scene.getIsEnabled() == null) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.emotion.dto.response.ai;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiTestTemplateResponse {
|
||||
|
||||
private String sceneCode;
|
||||
|
||||
private String endpointId;
|
||||
|
||||
private String endpointCode;
|
||||
|
||||
private String endpointName;
|
||||
|
||||
private String providerType;
|
||||
|
||||
@Builder.Default
|
||||
private Map<String, Object> inputs = new LinkedHashMap<>();
|
||||
|
||||
@Builder.Default
|
||||
private List<ParamField> paramFields = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ParamField {
|
||||
private String name;
|
||||
private String label;
|
||||
private String type;
|
||||
private Object value;
|
||||
private Boolean required;
|
||||
private String placeholder;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import com.emotion.dto.response.ai.AiTestTemplateResponse;
|
||||
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
|
||||
@@ -16,4 +17,8 @@ public interface AiRuntimeService {
|
||||
AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs);
|
||||
|
||||
void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer);
|
||||
|
||||
AiTestTemplateResponse buildEndpointTestTemplate(String endpointId);
|
||||
|
||||
AiTestTemplateResponse buildSceneTestTemplate(String sceneCode);
|
||||
}
|
||||
|
||||
@@ -76,8 +76,12 @@ public class CozeProviderAdapter implements AiProviderAdapter {
|
||||
JSONObject body = new JSONObject();
|
||||
if ("workflow".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
body.put("workflow_id", endpoint.getWorkflowId());
|
||||
if (StringUtils.hasText(endpoint.getBotId())) {
|
||||
body.put("bot_id", endpoint.getBotId());
|
||||
}
|
||||
body.put("user_id", StringUtils.hasText(request.getUserId()) ? request.getUserId() : "anonymous");
|
||||
body.put("stream", true);
|
||||
body.put("parameters", inputs);
|
||||
body.put("is_async", false);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -90,8 +94,13 @@ public class CozeProviderAdapter implements AiProviderAdapter {
|
||||
message.put("role", "user");
|
||||
message.put("content_type", "text");
|
||||
message.put("content", templateRenderer.firstText(inputs));
|
||||
message.put("type", "question");
|
||||
messages.add(message);
|
||||
body.put("additional_messages", messages);
|
||||
body.put("parameters", new JSONObject());
|
||||
if (StringUtils.hasText(endpoint.getWorkflowId())) {
|
||||
body.put("workflow_id", endpoint.getWorkflowId());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -108,19 +117,27 @@ public class CozeProviderAdapter implements AiProviderAdapter {
|
||||
if (StringUtils.hasText(content) && (type == null || type.contains("answer") || type.contains("delta"))) {
|
||||
return content;
|
||||
}
|
||||
String output = json.getString("output");
|
||||
if (StringUtils.hasText(output)) {
|
||||
return output;
|
||||
}
|
||||
String answer = json.getString("answer");
|
||||
if (StringUtils.hasText(answer)) {
|
||||
return answer;
|
||||
}
|
||||
JSONObject message = json.getJSONObject("message");
|
||||
if (message != null && StringUtils.hasText(message.getString("content"))) {
|
||||
return message.getString("content");
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
if (data != null) {
|
||||
String output = data.getString("output");
|
||||
if (StringUtils.hasText(output)) {
|
||||
return output;
|
||||
String dataOutput = data.getString("output");
|
||||
if (StringUtils.hasText(dataOutput)) {
|
||||
return dataOutput;
|
||||
}
|
||||
String answer = data.getString("answer");
|
||||
if (StringUtils.hasText(answer)) {
|
||||
return answer;
|
||||
String dataAnswer = data.getString("answer");
|
||||
if (StringUtils.hasText(dataAnswer)) {
|
||||
return dataAnswer;
|
||||
}
|
||||
String dataContent = data.getString("content");
|
||||
if (StringUtils.hasText(dataContent)) {
|
||||
|
||||
@@ -76,6 +76,10 @@ public class DifyProviderAdapter implements AiProviderAdapter {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("response_mode", "streaming");
|
||||
body.put("user", user(request));
|
||||
Object conversationId = inputs.get("conversation_id");
|
||||
if (conversationId != null && StringUtils.hasText(String.valueOf(conversationId))) {
|
||||
body.put("conversation_id", conversationId);
|
||||
}
|
||||
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
body.put("query", templateRenderer.firstText(inputs));
|
||||
body.put("inputs", inputs);
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -30,6 +31,7 @@ public class ProviderHttpSupport {
|
||||
|
||||
public void applyHeaders(HttpHeaders headers, AiProvider provider, AiEndpointConfig endpoint) {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(List.of(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON));
|
||||
if (StringUtils.hasText(provider.getApiKey())) {
|
||||
headers.setBearerAuth(provider.getApiKey());
|
||||
}
|
||||
@@ -43,6 +45,17 @@ public class ProviderHttpSupport {
|
||||
}
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(data);
|
||||
String event = json.getString("event");
|
||||
String type = json.getString("type");
|
||||
if ("error".equalsIgnoreCase(event) || "error".equalsIgnoreCase(type)) {
|
||||
String code = firstText(json, "code", "error_code", "errorCode");
|
||||
String message = firstText(json, "message", "error", "error_msg", "errorMessage");
|
||||
if (!StringUtils.hasText(message)) {
|
||||
message = data;
|
||||
}
|
||||
consumer.accept(AiStreamEvent.error(StringUtils.hasText(code) ? code : "AI_PROVIDER_ERROR", message));
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
String delta = extractor.extract(json);
|
||||
if (StringUtils.hasText(delta)) {
|
||||
counter.increment();
|
||||
@@ -54,6 +67,25 @@ public class ProviderHttpSupport {
|
||||
}
|
||||
}
|
||||
|
||||
private String firstText(JSONObject json, String... keys) {
|
||||
for (String key : keys) {
|
||||
String value = json.getString(key);
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
if (data != null) {
|
||||
for (String key : keys) {
|
||||
String value = data.getString(key);
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyJsonHeaders(HttpHeaders headers, String jsonText) {
|
||||
if (!StringUtils.hasText(jsonText)) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
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;
|
||||
@@ -20,6 +22,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -247,6 +251,172 @@ public class AiRuntimeServiceImpl implements AiRuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiTestTemplateResponse buildEndpointTestTemplate(String endpointId) {
|
||||
AiEndpointConfig endpoint = endpointConfigService.getById(endpointId);
|
||||
if (endpoint == null || Integer.valueOf(1).equals(endpoint.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_ENDPOINT_NOT_FOUND");
|
||||
}
|
||||
AiProvider provider = providerService.getById(endpoint.getProviderId());
|
||||
if (provider == null || Integer.valueOf(1).equals(provider.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_PROVIDER_NOT_FOUND");
|
||||
}
|
||||
return buildTemplate(null, endpoint, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiTestTemplateResponse buildSceneTestTemplate(String sceneCode) {
|
||||
AiSceneBinding scene = sceneBindingService.resolveScene(sceneCode);
|
||||
if (scene == null) {
|
||||
throw new IllegalArgumentException("AI_SCENE_NOT_BOUND");
|
||||
}
|
||||
AiEndpointConfig endpoint = endpointConfigService.getById(scene.getEndpointId());
|
||||
if (endpoint == null || Integer.valueOf(1).equals(endpoint.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_ENDPOINT_NOT_FOUND");
|
||||
}
|
||||
AiProvider provider = providerService.getById(endpoint.getProviderId());
|
||||
if (provider == null || Integer.valueOf(1).equals(provider.getIsDeleted())) {
|
||||
throw new IllegalArgumentException("AI_PROVIDER_NOT_FOUND");
|
||||
}
|
||||
return buildTemplate(sceneCode, endpoint, provider);
|
||||
}
|
||||
|
||||
private AiTestTemplateResponse buildTemplate(String sceneCode, AiEndpointConfig endpoint, AiProvider provider) {
|
||||
LinkedHashMap<String, Object> inputs = new LinkedHashMap<>();
|
||||
applyDefaultInputs(inputs, endpoint.getDefaultInputs());
|
||||
applyProviderSampleInputs(inputs, sceneCode, endpoint, provider);
|
||||
|
||||
List<AiTestTemplateResponse.ParamField> fields = new ArrayList<>();
|
||||
inputs.forEach((key, value) -> fields.add(AiTestTemplateResponse.ParamField.builder()
|
||||
.name(key)
|
||||
.label(paramLabel(key))
|
||||
.type(paramType(value))
|
||||
.value(value)
|
||||
.required(requiredParam(key, provider.getProviderType()))
|
||||
.placeholder(paramPlaceholder(key))
|
||||
.build()));
|
||||
|
||||
return AiTestTemplateResponse.builder()
|
||||
.sceneCode(sceneCode)
|
||||
.endpointId(endpoint.getId())
|
||||
.endpointCode(endpoint.getEndpointCode())
|
||||
.endpointName(endpoint.getEndpointName())
|
||||
.providerType(provider.getProviderType())
|
||||
.inputs(inputs)
|
||||
.paramFields(fields)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void applyDefaultInputs(LinkedHashMap<String, Object> inputs, String defaultInputs) {
|
||||
if (!StringUtils.hasText(defaultInputs)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject parsed = JSON.parseObject(defaultInputs);
|
||||
parsed.forEach((key, value) -> {
|
||||
if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|
||||
inputs.put(key, ((JSONObject) value).get("value"));
|
||||
} else {
|
||||
inputs.put(key, value);
|
||||
}
|
||||
});
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyProviderSampleInputs(LinkedHashMap<String, Object> inputs,
|
||||
String sceneCode,
|
||||
AiEndpointConfig endpoint,
|
||||
AiProvider provider) {
|
||||
String providerType = provider.getProviderType();
|
||||
String sample = samplePrompt(sceneCode, endpoint);
|
||||
|
||||
if ("dify".equalsIgnoreCase(providerType)) {
|
||||
inputs.putIfAbsent("query", sample);
|
||||
inputs.putIfAbsent("inputs", Map.of("input", sample, "user_id", "admin-test-user"));
|
||||
inputs.putIfAbsent("response_mode", "streaming");
|
||||
inputs.putIfAbsent("conversation_id", "");
|
||||
inputs.putIfAbsent("user", "admin-test-user");
|
||||
inputs.putIfAbsent("user_id", "admin-test-user");
|
||||
return;
|
||||
}
|
||||
|
||||
inputs.putIfAbsent("input", sample);
|
||||
inputs.putIfAbsent("prompt", sample);
|
||||
inputs.putIfAbsent("message", sample);
|
||||
inputs.putIfAbsent("user_id", "admin-test-user");
|
||||
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||
inputs.putIfAbsent("conversationId", "admin-test-conversation");
|
||||
}
|
||||
}
|
||||
|
||||
private String samplePrompt(String sceneCode, AiEndpointConfig endpoint) {
|
||||
String code = StringUtils.hasText(sceneCode) ? sceneCode : endpoint.getEndpointCode();
|
||||
if (code == null) {
|
||||
return "请用一句中文回复测试成功。";
|
||||
}
|
||||
if (code.contains("script") || code.contains("life.generate")) {
|
||||
return "请生成一个关于普通人重新找回生活热情的短剧本,要求结构清晰、中文输出。";
|
||||
}
|
||||
if (code.contains("short_story") || code.contains("story")) {
|
||||
return "请生成一篇 300 字以内的治愈短篇小说,主题是雨后的新开始。";
|
||||
}
|
||||
if (code.contains("diary") || code.contains("healing") || code.contains("life")) {
|
||||
return "今天工作压力很大,但傍晚散步时感觉心情慢慢平静下来,请给我一段温和的回应。";
|
||||
}
|
||||
if (code.contains("summary")) {
|
||||
return "用户今天先表达焦虑,随后通过散步和朋友聊天逐渐恢复平静,请生成一段情绪总结。";
|
||||
}
|
||||
if (code.contains("chat")) {
|
||||
return "你好,请用一句中文回复:AI 接口测试成功。";
|
||||
}
|
||||
return "请用一句中文回复测试成功。";
|
||||
}
|
||||
|
||||
private String paramLabel(String key) {
|
||||
Map<String, String> labels = Map.ofEntries(
|
||||
Map.entry("input", "输入内容"),
|
||||
Map.entry("prompt", "提示词"),
|
||||
Map.entry("message", "消息内容"),
|
||||
Map.entry("query", "用户问题"),
|
||||
Map.entry("inputs", "Dify 变量"),
|
||||
Map.entry("response_mode", "响应模式"),
|
||||
Map.entry("conversation_id", "会话 ID"),
|
||||
Map.entry("user", "用户标识"),
|
||||
Map.entry("user_id", "用户标识"),
|
||||
Map.entry("conversationId", "会话 ID")
|
||||
);
|
||||
return labels.getOrDefault(key, key);
|
||||
}
|
||||
|
||||
private String paramType(Object value) {
|
||||
if (value instanceof Number) {
|
||||
return "number";
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
return "boolean";
|
||||
}
|
||||
if (value instanceof Map || value instanceof JSONObject) {
|
||||
return "json";
|
||||
}
|
||||
String text = value == null ? "" : String.valueOf(value);
|
||||
return text.length() > 60 ? "textarea" : "string";
|
||||
}
|
||||
|
||||
private Boolean requiredParam(String key, String providerType) {
|
||||
if ("dify".equalsIgnoreCase(providerType)) {
|
||||
return "query".equals(key) || "response_mode".equals(key) || "user".equals(key);
|
||||
}
|
||||
return "input".equals(key) || "user_id".equals(key);
|
||||
}
|
||||
|
||||
private String paramPlaceholder(String key) {
|
||||
if ("inputs".equals(key)) {
|
||||
return "{\"input\":\"测试内容\"}";
|
||||
}
|
||||
return "请输入" + paramLabel(key);
|
||||
}
|
||||
|
||||
private RuntimeTarget resolveTarget(AiRuntimeRequest request) {
|
||||
if (!StringUtils.hasText(request.getSceneCode())) {
|
||||
throw new IllegalArgumentException("AI_SCENE_REQUIRED");
|
||||
|
||||
Reference in New Issue
Block a user