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.alibaba.fastjson2.JSONObject;
|
||||||
import com.emotion.common.Result;
|
import com.emotion.common.Result;
|
||||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
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.AiRuntimeTestResponse;
|
||||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||||
import com.emotion.entity.AiCallLog;
|
import com.emotion.entity.AiCallLog;
|
||||||
@@ -80,6 +81,11 @@ public class AiRoutingController {
|
|||||||
return Result.success(endpointConfigService.listVisible());
|
return Result.success(endpointConfigService.listVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/endpoints/test-template")
|
||||||
|
public Result<AiTestTemplateResponse> endpointTestTemplate(@RequestParam String id) {
|
||||||
|
return Result.success(runtimeService.buildEndpointTestTemplate(id));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/endpoints")
|
@PostMapping("/endpoints")
|
||||||
public Result<AiEndpointConfig> createEndpoint(@RequestBody AiEndpointConfig endpoint) {
|
public Result<AiEndpointConfig> createEndpoint(@RequestBody AiEndpointConfig endpoint) {
|
||||||
return Result.success(endpointConfigService.saveEndpoint(endpoint));
|
return Result.success(endpointConfigService.saveEndpoint(endpoint));
|
||||||
@@ -101,6 +107,11 @@ public class AiRoutingController {
|
|||||||
return Result.success(sceneBindingService.listVisible());
|
return Result.success(sceneBindingService.listVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/scenes/test-template")
|
||||||
|
public Result<AiTestTemplateResponse> sceneTestTemplate(@RequestParam String sceneCode) {
|
||||||
|
return Result.success(runtimeService.buildSceneTestTemplate(sceneCode));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/scenes")
|
@PostMapping("/scenes")
|
||||||
public Result<AiSceneBinding> createScene(@RequestBody AiSceneBinding scene) {
|
public Result<AiSceneBinding> createScene(@RequestBody AiSceneBinding scene) {
|
||||||
if (scene.getIsEnabled() == null) {
|
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;
|
package com.emotion.service;
|
||||||
|
|
||||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
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.AiRuntimeTestResponse;
|
||||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||||
|
|
||||||
@@ -16,4 +17,8 @@ public interface AiRuntimeService {
|
|||||||
AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs);
|
AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs);
|
||||||
|
|
||||||
void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer);
|
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();
|
JSONObject body = new JSONObject();
|
||||||
if ("workflow".equalsIgnoreCase(endpoint.getEndpointType())) {
|
if ("workflow".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||||
body.put("workflow_id", endpoint.getWorkflowId());
|
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("parameters", inputs);
|
||||||
body.put("is_async", false);
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +94,13 @@ public class CozeProviderAdapter implements AiProviderAdapter {
|
|||||||
message.put("role", "user");
|
message.put("role", "user");
|
||||||
message.put("content_type", "text");
|
message.put("content_type", "text");
|
||||||
message.put("content", templateRenderer.firstText(inputs));
|
message.put("content", templateRenderer.firstText(inputs));
|
||||||
|
message.put("type", "question");
|
||||||
messages.add(message);
|
messages.add(message);
|
||||||
body.put("additional_messages", messages);
|
body.put("additional_messages", messages);
|
||||||
|
body.put("parameters", new JSONObject());
|
||||||
|
if (StringUtils.hasText(endpoint.getWorkflowId())) {
|
||||||
|
body.put("workflow_id", endpoint.getWorkflowId());
|
||||||
|
}
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,19 +117,27 @@ public class CozeProviderAdapter implements AiProviderAdapter {
|
|||||||
if (StringUtils.hasText(content) && (type == null || type.contains("answer") || type.contains("delta"))) {
|
if (StringUtils.hasText(content) && (type == null || type.contains("answer") || type.contains("delta"))) {
|
||||||
return content;
|
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");
|
JSONObject message = json.getJSONObject("message");
|
||||||
if (message != null && StringUtils.hasText(message.getString("content"))) {
|
if (message != null && StringUtils.hasText(message.getString("content"))) {
|
||||||
return message.getString("content");
|
return message.getString("content");
|
||||||
}
|
}
|
||||||
JSONObject data = json.getJSONObject("data");
|
JSONObject data = json.getJSONObject("data");
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
String output = data.getString("output");
|
String dataOutput = data.getString("output");
|
||||||
if (StringUtils.hasText(output)) {
|
if (StringUtils.hasText(dataOutput)) {
|
||||||
return output;
|
return dataOutput;
|
||||||
}
|
}
|
||||||
String answer = data.getString("answer");
|
String dataAnswer = data.getString("answer");
|
||||||
if (StringUtils.hasText(answer)) {
|
if (StringUtils.hasText(dataAnswer)) {
|
||||||
return answer;
|
return dataAnswer;
|
||||||
}
|
}
|
||||||
String dataContent = data.getString("content");
|
String dataContent = data.getString("content");
|
||||||
if (StringUtils.hasText(dataContent)) {
|
if (StringUtils.hasText(dataContent)) {
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ public class DifyProviderAdapter implements AiProviderAdapter {
|
|||||||
JSONObject body = new JSONObject();
|
JSONObject body = new JSONObject();
|
||||||
body.put("response_mode", "streaming");
|
body.put("response_mode", "streaming");
|
||||||
body.put("user", user(request));
|
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())) {
|
if ("chat".equalsIgnoreCase(endpoint.getEndpointType())) {
|
||||||
body.put("query", templateRenderer.firstText(inputs));
|
body.put("query", templateRenderer.firstText(inputs));
|
||||||
body.put("inputs", inputs);
|
body.put("inputs", inputs);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ public class ProviderHttpSupport {
|
|||||||
|
|
||||||
public void applyHeaders(HttpHeaders headers, AiProvider provider, AiEndpointConfig endpoint) {
|
public void applyHeaders(HttpHeaders headers, AiProvider provider, AiEndpointConfig endpoint) {
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setAccept(List.of(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON));
|
||||||
if (StringUtils.hasText(provider.getApiKey())) {
|
if (StringUtils.hasText(provider.getApiKey())) {
|
||||||
headers.setBearerAuth(provider.getApiKey());
|
headers.setBearerAuth(provider.getApiKey());
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,17 @@ public class ProviderHttpSupport {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
JSONObject json = JSON.parseObject(data);
|
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);
|
String delta = extractor.extract(json);
|
||||||
if (StringUtils.hasText(delta)) {
|
if (StringUtils.hasText(delta)) {
|
||||||
counter.increment();
|
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) {
|
private void applyJsonHeaders(HttpHeaders headers, String jsonText) {
|
||||||
if (!StringUtils.hasText(jsonText)) {
|
if (!StringUtils.hasText(jsonText)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.emotion.service.impl;
|
package com.emotion.service.impl;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
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.AiRuntimeTestResponse;
|
||||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||||
import com.emotion.entity.AiCallLog;
|
import com.emotion.entity.AiCallLog;
|
||||||
@@ -20,6 +22,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
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) {
|
private RuntimeTarget resolveTarget(AiRuntimeRequest request) {
|
||||||
if (!StringUtils.hasText(request.getSceneCode())) {
|
if (!StringUtils.hasText(request.getSceneCode())) {
|
||||||
throw new IllegalArgumentException("AI_SCENE_REQUIRED");
|
throw new IllegalArgumentException("AI_SCENE_REQUIRED");
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# AI Endpoint Test Fix Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make every AI provider endpoint in web-admin open with complete editable test parameters and make Coze/Dify workflow tests succeed through the backend runtime without breaking existing user-facing AI flows.
|
||||||
|
|
||||||
|
**Architecture:** Keep backend as the only place that knows provider credentials and provider-specific request formats. Add backend test sample generation for endpoint and scene tests, then update web-admin to load those samples when opening the dialog. Patch Coze/Dify provider adapters to match documented and historically working request/stream formats while preserving the current `/ai/runtime/stream` and `/ai/endpoint/stream` contracts.
|
||||||
|
|
||||||
|
**Tech Stack:** Spring Boot, FastJSON2, RestTemplate streaming, Element Plus, Vue 3, TypeScript, MySQL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Backend Test Sample Contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `backend-single/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
|
||||||
|
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
|
||||||
|
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
|
||||||
|
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
|
||||||
|
|
||||||
|
- [x] Add a response DTO with `sceneCode`, `endpointId`, `endpointCode`, `providerType`, `inputs`, and `paramFields`.
|
||||||
|
- [x] Add service methods `buildEndpointTestTemplate(String endpointId)` and `buildSceneTestTemplate(String sceneCode)`.
|
||||||
|
- [x] Generate complete sample inputs even when `defaultInputs` is `{}` or empty.
|
||||||
|
- [x] Expose `GET /ai/endpoints/test-template?id=...` and `GET /ai/scenes/test-template?sceneCode=...`.
|
||||||
|
|
||||||
|
### Task 2: Provider Runtime Compatibility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend-single/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
|
||||||
|
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
|
||||||
|
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
|
||||||
|
|
||||||
|
- [x] Add `Accept: text/event-stream` for stream requests.
|
||||||
|
- [x] Make Coze workflow body match historical calls: `workflow_id`, `user_id`, `stream`, `parameters`, and optional `bot_id`.
|
||||||
|
- [x] Make Coze chat body include `type: question` in `additional_messages`.
|
||||||
|
- [x] Parse Coze workflow events from `output`, `content`, `answer`, `data.output`, `data.content`, and nested message fields.
|
||||||
|
- [x] Keep Dify chat body aligned with `docs/dify平台接口.md`: `query`, `inputs`, `response_mode`, `user`, optional `conversation_id`.
|
||||||
|
- [x] Emit provider error events when upstream SSE includes an error event.
|
||||||
|
|
||||||
|
### Task 3: web-admin Test Dialog Defaults
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/types/aiconfig.ts`
|
||||||
|
- Modify: `web-admin/src/api/aiconfig.ts`
|
||||||
|
- Modify: `web-admin/src/views/aiconfig/AiRoutingList.vue`
|
||||||
|
|
||||||
|
- [x] Add `AiTestTemplateResponse` TypeScript type.
|
||||||
|
- [x] Add API helpers for endpoint and scene test templates.
|
||||||
|
- [x] When opening endpoint test dialog, request backend template and populate form fields plus JSON editor.
|
||||||
|
- [x] When opening scene test dialog, request backend template and populate JSON editor.
|
||||||
|
- [x] Validate required fields before running stream/non-stream tests.
|
||||||
|
- [x] Keep the existing editable JSON advanced area so testers can modify payloads.
|
||||||
|
|
||||||
|
### Task 4: Database Default Input Backfill
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `sql/2026-05-22-ai-scene-routing.sql`
|
||||||
|
|
||||||
|
- [x] Backfill `default_inputs` for existing Coze and Dify endpoints with realistic examples.
|
||||||
|
- [x] Apply the SQL to the test database.
|
||||||
|
- [x] Verify all enabled endpoints have non-empty `default_inputs`.
|
||||||
|
|
||||||
|
### Task 5: Verification
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `mvn test`
|
||||||
|
- `mvn -DskipTests clean package`
|
||||||
|
- `npm run build` in `web-admin`
|
||||||
|
- MySQL query for endpoint default inputs.
|
||||||
|
|
||||||
|
- [x] Backend tests pass.
|
||||||
|
- [x] Backend package succeeds.
|
||||||
|
- [x] web-admin build succeeds.
|
||||||
|
- [x] Coze endpoint test returns stream output or a provider business error with full request/log context.
|
||||||
|
- [x] Dify endpoint test returns stream output or a provider business error with full request/log context.
|
||||||
|
- [x] Existing scene runtime endpoints remain unchanged for web, life-script, and mini-program clients.
|
||||||
@@ -144,6 +144,34 @@ WHERE c.`is_deleted` = 0
|
|||||||
ORDER BY c.`create_time` DESC
|
ORDER BY c.`create_time` DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
INSERT INTO `t_ai_provider`
|
||||||
|
(`id`, `provider_code`, `provider_name`, `provider_type`, `base_url`, `api_key`, `auth_type`, `default_headers`, `timeout_ms`, `is_enabled`, `description`)
|
||||||
|
SELECT UUID(),
|
||||||
|
CONCAT('coze_', REPLACE(c.`config_key`, '.', '_')),
|
||||||
|
CONCAT(c.`config_name`, '服务商'),
|
||||||
|
'coze',
|
||||||
|
CASE
|
||||||
|
WHEN c.`api_base_url` LIKE '%/v1/workflow/stream_run' THEN REPLACE(c.`api_base_url`, '/v1/workflow/stream_run', '')
|
||||||
|
WHEN c.`api_base_url` LIKE '%/v3/chat' THEN REPLACE(c.`api_base_url`, '/v3/chat', '')
|
||||||
|
ELSE c.`api_base_url`
|
||||||
|
END,
|
||||||
|
c.`api_token`,
|
||||||
|
'bearer',
|
||||||
|
NULL,
|
||||||
|
COALESCE(c.`timeout_ms`, 60000),
|
||||||
|
c.`is_enabled`,
|
||||||
|
CONCAT('由旧 AI 配置迁移生成,配置键:', c.`config_key`)
|
||||||
|
FROM `t_ai_config` c
|
||||||
|
WHERE c.`is_deleted` = 0
|
||||||
|
AND c.`provider` = 'coze'
|
||||||
|
AND c.`api_token` IS NOT NULL
|
||||||
|
AND c.`api_token` <> ''
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `t_ai_provider` p
|
||||||
|
WHERE p.`provider_code` COLLATE utf8mb4_unicode_ci = CONCAT('coze_', REPLACE(c.`config_key`, '.', '_')) COLLATE utf8mb4_unicode_ci
|
||||||
|
AND p.`is_deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO `t_ai_provider`
|
INSERT INTO `t_ai_provider`
|
||||||
(`id`, `provider_code`, `provider_name`, `provider_type`, `base_url`, `api_key`, `auth_type`, `default_headers`, `timeout_ms`, `is_enabled`, `description`)
|
(`id`, `provider_code`, `provider_name`, `provider_type`, `base_url`, `api_key`, `auth_type`, `default_headers`, `timeout_ms`, `is_enabled`, `description`)
|
||||||
SELECT UUID(), 'dify_default', 'Dify 默认服务商', 'dify', 'http://49.232.138.53/v1', 'app-MqQOx09gCu9zzlKMpeLqHQHv',
|
SELECT UUID(), 'dify_default', 'Dify 默认服务商', 'dify', 'http://49.232.138.53/v1', 'app-MqQOx09gCu9zzlKMpeLqHQHv',
|
||||||
@@ -189,6 +217,12 @@ WHERE NOT EXISTS (
|
|||||||
SELECT 1 FROM `t_ai_endpoint_config` e WHERE e.`endpoint_code` = seed.endpoint_code AND e.`is_deleted` = 0
|
SELECT 1 FROM `t_ai_endpoint_config` e WHERE e.`endpoint_code` = seed.endpoint_code AND e.`is_deleted` = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config` e
|
||||||
|
JOIN `t_ai_provider` p ON p.`provider_code` COLLATE utf8mb4_unicode_ci = CONCAT('coze_', REPLACE(e.`endpoint_code`, '.', '_')) COLLATE utf8mb4_unicode_ci AND p.`is_deleted` = 0
|
||||||
|
SET e.`provider_id` = p.`id`
|
||||||
|
WHERE e.`is_deleted` = 0
|
||||||
|
AND e.`endpoint_code` LIKE 'coze.%';
|
||||||
|
|
||||||
UPDATE `t_ai_scene_binding` s
|
UPDATE `t_ai_scene_binding` s
|
||||||
JOIN `t_ai_endpoint_config` e ON e.`endpoint_code` = 'coze.chat.default' AND e.`is_deleted` = 0
|
JOIN `t_ai_endpoint_config` e ON e.`endpoint_code` = 'coze.chat.default' AND e.`is_deleted` = 0
|
||||||
SET s.`endpoint_id` = e.`id`, s.`is_enabled` = 1, s.`required_stream` = 1, s.`description` = '已绑定现有 Coze 聊天工作流'
|
SET s.`endpoint_id` = e.`id`, s.`is_enabled` = 1, s.`required_stream` = 1, s.`description` = '已绑定现有 Coze 聊天工作流'
|
||||||
@@ -230,3 +264,39 @@ UPDATE `t_ai_scene_binding` s
|
|||||||
JOIN `t_ai_endpoint_config` e ON e.`endpoint_code` = 'coze.user.dairy.summary' AND e.`is_deleted` = 0
|
JOIN `t_ai_endpoint_config` e ON e.`endpoint_code` = 'coze.user.dairy.summary' AND e.`is_deleted` = 0
|
||||||
SET s.`endpoint_id` = e.`id`, s.`is_enabled` = 1, s.`required_stream` = 1, s.`description` = '已绑定现有 Coze 人生事件疗愈工作流'
|
SET s.`endpoint_id` = e.`id`, s.`is_enabled` = 1, s.`required_stream` = 1, s.`description` = '已绑定现有 Coze 人生事件疗愈工作流'
|
||||||
WHERE s.`scene_code` = 'life_healing' AND s.`is_deleted` = 0;
|
WHERE s.`scene_code` = 'life_healing' AND s.`is_deleted` = 0;
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config`
|
||||||
|
SET `default_inputs` = '{"input":{"_meta":{"label":"输入内容","type":"textarea","required":true},"value":"你好,请用一句中文回复:AI 接口测试成功。"},"user_id":{"_meta":{"label":"用户标识","type":"string","required":true},"value":"admin-test-user"}}'
|
||||||
|
WHERE `endpoint_code` = 'coze.chat.default'
|
||||||
|
AND `is_deleted` = 0
|
||||||
|
AND (`default_inputs` IS NULL OR `default_inputs` = '' OR `default_inputs` = '{}');
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config`
|
||||||
|
SET `default_inputs` = '{"input":{"_meta":{"label":"剧本需求","type":"textarea","required":true},"value":"请生成一个关于普通人重新找回生活热情的短剧本,要求结构清晰、中文输出。"},"user_id":{"_meta":{"label":"用户标识","type":"string","required":true},"value":"admin-test-user"}}'
|
||||||
|
WHERE `endpoint_code` = 'coze.course.life.generate'
|
||||||
|
AND `is_deleted` = 0
|
||||||
|
AND (`default_inputs` IS NULL OR `default_inputs` = '' OR `default_inputs` = '{}');
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config`
|
||||||
|
SET `default_inputs` = '{"input":{"_meta":{"label":"短篇小说主题","type":"textarea","required":true},"value":"请生成一篇 300 字以内的治愈短篇小说,主题是雨后的新开始。"},"user_id":{"_meta":{"label":"用户标识","type":"string","required":true},"value":"admin-test-user"}}'
|
||||||
|
WHERE `endpoint_code` = 'coze.user.chose.story'
|
||||||
|
AND `is_deleted` = 0
|
||||||
|
AND (`default_inputs` IS NULL OR `default_inputs` = '' OR `default_inputs` = '{}');
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config`
|
||||||
|
SET `default_inputs` = '{"query":{"_meta":{"label":"用户问题","type":"textarea","required":true},"value":"请生成一个关于普通人重新找回生活热情的短剧本,要求结构清晰、中文输出。"},"inputs":{"_meta":{"label":"Dify 变量","type":"json","required":false},"value":{"input":"请生成一个关于普通人重新找回生活热情的短剧本,要求结构清晰、中文输出。","user_id":"admin-test-user"}},"response_mode":{"_meta":{"label":"响应模式","type":"string","required":true},"value":"streaming"},"conversation_id":{"_meta":{"label":"会话 ID","type":"string","required":false},"value":""},"user":{"_meta":{"label":"用户标识","type":"string","required":true},"value":"admin-test-user"},"user_id":{"_meta":{"label":"Dify 变量用户标识","type":"string","required":true},"value":"admin-test-user"}}'
|
||||||
|
WHERE `endpoint_code` = 'dify.script_generate.chat_messages'
|
||||||
|
AND `is_deleted` = 0
|
||||||
|
AND (`default_inputs` IS NULL OR `default_inputs` = '' OR `default_inputs` = '{}' OR `default_inputs` NOT LIKE '%"user_id"%');
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config`
|
||||||
|
SET `default_inputs` = '{"query":{"_meta":{"label":"用户问题","type":"textarea","required":true},"value":"请生成一篇 300 字以内的治愈短篇小说,主题是雨后的新开始。"},"inputs":{"_meta":{"label":"Dify 变量","type":"json","required":false},"value":{"input":"请生成一篇 300 字以内的治愈短篇小说,主题是雨后的新开始。","user_id":"admin-test-user"}},"response_mode":{"_meta":{"label":"响应模式","type":"string","required":true},"value":"streaming"},"conversation_id":{"_meta":{"label":"会话 ID","type":"string","required":false},"value":""},"user":{"_meta":{"label":"用户标识","type":"string","required":true},"value":"admin-test-user"},"user_id":{"_meta":{"label":"Dify 变量用户标识","type":"string","required":true},"value":"admin-test-user"}}'
|
||||||
|
WHERE `endpoint_code` = 'dify.short_story_generate.chat_messages'
|
||||||
|
AND `is_deleted` = 0
|
||||||
|
AND (`default_inputs` IS NULL OR `default_inputs` = '' OR `default_inputs` = '{}' OR `default_inputs` NOT LIKE '%"user_id"%');
|
||||||
|
|
||||||
|
UPDATE `t_ai_endpoint_config`
|
||||||
|
SET `default_inputs` = '{"input":{"_meta":{"label":"事件或日记内容","type":"textarea","required":true},"value":"今天工作压力很大,但傍晚散步时感觉心情慢慢平静下来,请给我一段温和的回应。"},"user_id":{"_meta":{"label":"用户标识","type":"string","required":true},"value":"admin-test-user"}}'
|
||||||
|
WHERE `endpoint_code` IN ('coze.user.dairy.summary', 'coze.user.life.state', 'coze.user.query.polish')
|
||||||
|
AND `is_deleted` = 0
|
||||||
|
AND (`default_inputs` IS NULL OR `default_inputs` = '' OR `default_inputs` = '{}');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
VITE_APP_TITLE=情绪博物馆管理后台
|
VITE_APP_TITLE=情绪博物馆管理后台
|
||||||
VITE_APP_BASE_API=http://localhost:19089/api
|
VITE_APP_BASE_API=https://lifescript.happylifeos.com/api
|
||||||
VITE_APP_PORT=5174
|
VITE_APP_PORT=5174
|
||||||
|
|||||||
@@ -237,6 +237,10 @@ export function listAiEndpoints() {
|
|||||||
return request({ url: '/ai/endpoints', method: 'get' })
|
return request({ url: '/ai/endpoints', method: 'get' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEndpointTestTemplate(id: string) {
|
||||||
|
return request({ url: '/ai/endpoints/test-template', method: 'get', params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
export function saveAiEndpoint(data: AiEndpointConfig) {
|
export function saveAiEndpoint(data: AiEndpointConfig) {
|
||||||
return request({ url: '/ai/endpoints', method: data.id ? 'put' : 'post', data })
|
return request({ url: '/ai/endpoints', method: data.id ? 'put' : 'post', data })
|
||||||
}
|
}
|
||||||
@@ -249,6 +253,10 @@ export function listAiScenes() {
|
|||||||
return request({ url: '/ai/scenes', method: 'get' })
|
return request({ url: '/ai/scenes', method: 'get' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSceneTestTemplate(sceneCode: string) {
|
||||||
|
return request({ url: '/ai/scenes/test-template', method: 'get', params: { sceneCode } })
|
||||||
|
}
|
||||||
|
|
||||||
export function saveAiScene(data: AiSceneBinding) {
|
export function saveAiScene(data: AiSceneBinding) {
|
||||||
return request({ url: '/ai/scenes', method: data.id ? 'put' : 'post', data })
|
return request({ url: '/ai/scenes', method: data.id ? 'put' : 'post', data })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,16 +273,17 @@ export interface AiEndpointRuntimeRequest {
|
|||||||
export interface TestParamField {
|
export interface TestParamField {
|
||||||
name: string
|
name: string
|
||||||
label: string
|
label: string
|
||||||
type: 'string' | 'textarea' | 'number' | 'boolean'
|
type: 'string' | 'textarea' | 'number' | 'boolean' | 'json'
|
||||||
value: any
|
value: any
|
||||||
defaultValue: any
|
defaultValue: any
|
||||||
required: boolean
|
required: boolean
|
||||||
|
placeholder?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParamDefinition {
|
export interface ParamDefinition {
|
||||||
name: string
|
name: string
|
||||||
label: string
|
label: string
|
||||||
type: 'string' | 'textarea' | 'number' | 'boolean'
|
type: 'string' | 'textarea' | 'number' | 'boolean' | 'json'
|
||||||
defaultValue: any
|
defaultValue: any
|
||||||
required: boolean
|
required: boolean
|
||||||
}
|
}
|
||||||
@@ -296,3 +297,13 @@ export interface AiRuntimeTestResponse {
|
|||||||
errorCode?: string
|
errorCode?: string
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AiTestTemplateResponse {
|
||||||
|
sceneCode?: string
|
||||||
|
endpointId?: string
|
||||||
|
endpointCode?: string
|
||||||
|
endpointName?: string
|
||||||
|
providerType?: string
|
||||||
|
inputs: Record<string, any>
|
||||||
|
paramFields: TestParamField[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -218,6 +218,7 @@
|
|||||||
<el-option label="多行" value="textarea" />
|
<el-option label="多行" value="textarea" />
|
||||||
<el-option label="数字" value="number" />
|
<el-option label="数字" value="number" />
|
||||||
<el-option label="开关" value="boolean" />
|
<el-option label="开关" value="boolean" />
|
||||||
|
<el-option label="JSON" value="json" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="param.defaultValue" placeholder="默认值" style="width: 100px" />
|
<el-input v-model="param.defaultValue" placeholder="默认值" style="width: 100px" />
|
||||||
<el-checkbox v-model="param.required">必填</el-checkbox>
|
<el-checkbox v-model="param.required">必填</el-checkbox>
|
||||||
@@ -298,6 +299,7 @@
|
|||||||
<el-input v-if="field.type === 'textarea'" v-model="field.value" type="textarea" :rows="3" :placeholder="field.name" @input="syncEndpointJsonFromFields" />
|
<el-input v-if="field.type === 'textarea'" v-model="field.value" type="textarea" :rows="3" :placeholder="field.name" @input="syncEndpointJsonFromFields" />
|
||||||
<el-input-number v-if="field.type === 'number'" v-model="field.value" :placeholder="field.name" @change="syncEndpointJsonFromFields" style="width: 100%" />
|
<el-input-number v-if="field.type === 'number'" v-model="field.value" :placeholder="field.name" @change="syncEndpointJsonFromFields" style="width: 100%" />
|
||||||
<el-switch v-if="field.type === 'boolean'" v-model="field.value" @change="syncEndpointJsonFromFields" />
|
<el-switch v-if="field.type === 'boolean'" v-model="field.value" @change="syncEndpointJsonFromFields" />
|
||||||
|
<el-input v-if="field.type === 'json'" v-model="field.value" type="textarea" :rows="4" :placeholder="field.placeholder || field.name" @input="syncEndpointJsonFromFields" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-collapse style="margin-top: 12px">
|
<el-collapse style="margin-top: 12px">
|
||||||
@@ -336,6 +338,8 @@ import {
|
|||||||
deleteAiEndpoint,
|
deleteAiEndpoint,
|
||||||
deleteAiProvider,
|
deleteAiProvider,
|
||||||
deleteAiScene,
|
deleteAiScene,
|
||||||
|
getEndpointTestTemplate,
|
||||||
|
getSceneTestTemplate,
|
||||||
listAiCallLogs,
|
listAiCallLogs,
|
||||||
listAiEndpoints,
|
listAiEndpoints,
|
||||||
listAiProviders,
|
listAiProviders,
|
||||||
@@ -348,7 +352,7 @@ import {
|
|||||||
testAiRuntime,
|
testAiRuntime,
|
||||||
testEndpointRuntime
|
testEndpointRuntime
|
||||||
} from '@/api/aiconfig'
|
} from '@/api/aiconfig'
|
||||||
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding, TestParamField, ParamDefinition } from '@/types/aiconfig'
|
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding, TestParamField, ParamDefinition, AiTestTemplateResponse } from '@/types/aiconfig'
|
||||||
|
|
||||||
const activeTab = ref('providers')
|
const activeTab = ref('providers')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -478,27 +482,67 @@ function openScene(row?: AiSceneBinding) {
|
|||||||
sceneDialog.value = true
|
sceneDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeTest() {
|
async function openRuntimeTest() {
|
||||||
testForm.sceneCode = scenes.value.find(item => item.isEnabled === 1 && item.endpointId)?.sceneCode || scenes.value[0]?.sceneCode || ''
|
testForm.sceneCode = scenes.value.find(item => item.isEnabled === 1 && item.endpointId)?.sceneCode || scenes.value[0]?.sceneCode || ''
|
||||||
testResult.value = null
|
testResult.value = null
|
||||||
nonStreamResult.value = null
|
nonStreamResult.value = null
|
||||||
|
await loadSceneTemplate(testForm.sceneCode)
|
||||||
testDialog.value = true
|
testDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSceneRuntimeTest(row: AiSceneBinding) {
|
async function openSceneRuntimeTest(row: AiSceneBinding) {
|
||||||
testForm.sceneCode = row.sceneCode
|
testForm.sceneCode = row.sceneCode
|
||||||
testResult.value = null
|
testResult.value = null
|
||||||
nonStreamResult.value = null
|
nonStreamResult.value = null
|
||||||
|
await loadSceneTemplate(row.sceneCode)
|
||||||
testDialog.value = true
|
testDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEndpointTest(row: AiEndpointConfig) {
|
async function openEndpointTest(row: AiEndpointConfig) {
|
||||||
|
if (!row.id) return
|
||||||
endpointTestRow.value = row
|
endpointTestRow.value = row
|
||||||
endpointParamFields.value = parseParamFields(row.defaultInputs)
|
endpointParamFields.value = parseParamFields(row.defaultInputs)
|
||||||
endpointInputsJson.value = buildInputsJson(endpointParamFields.value)
|
endpointInputsJson.value = buildInputsJson(endpointParamFields.value)
|
||||||
endpointTestResult.value = null
|
endpointTestResult.value = null
|
||||||
endpointNonStreamResult.value = null
|
endpointNonStreamResult.value = null
|
||||||
endpointTestDialog.value = true
|
endpointTestDialog.value = true
|
||||||
|
endpointTesting.value = true
|
||||||
|
try {
|
||||||
|
const res = await getEndpointTestTemplate(row.id!)
|
||||||
|
applyEndpointTemplate(res.data as AiTestTemplateResponse)
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.warning(error?.message || '测试样例加载失败,已使用本地默认参数')
|
||||||
|
} finally {
|
||||||
|
endpointTesting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSceneTemplate(sceneCode: string) {
|
||||||
|
if (!sceneCode) {
|
||||||
|
testInputsJson.value = '{\n "prompt": "请用一句中文回复测试成功。"\n}'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getSceneTestTemplate(sceneCode)
|
||||||
|
const template = res.data as AiTestTemplateResponse
|
||||||
|
testInputsJson.value = JSON.stringify(template.inputs || {}, null, 2)
|
||||||
|
} catch (error: any) {
|
||||||
|
testInputsJson.value = '{\n "prompt": "请用一句中文回复测试成功。"\n}'
|
||||||
|
ElMessage.warning(error?.message || '场景测试样例加载失败,已使用通用测试参数')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEndpointTemplate(template: AiTestTemplateResponse) {
|
||||||
|
const inputs = template.inputs || {}
|
||||||
|
endpointInputsJson.value = JSON.stringify(inputs, null, 2)
|
||||||
|
endpointParamFields.value = (template.paramFields || []).map(field => ({
|
||||||
|
...field,
|
||||||
|
value: field.type === 'json'
|
||||||
|
? JSON.stringify(field.value ?? {}, null, 2)
|
||||||
|
: field.value,
|
||||||
|
defaultValue: field.value,
|
||||||
|
required: Boolean(field.required)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParamFields(defaultInputs?: string): TestParamField[] {
|
function parseParamFields(defaultInputs?: string): TestParamField[] {
|
||||||
@@ -533,16 +577,28 @@ function parseParamFields(defaultInputs?: string): TestParamField[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferParamType(val: any): 'string' | 'textarea' | 'number' | 'boolean' {
|
function inferParamType(val: any): 'string' | 'textarea' | 'number' | 'boolean' | 'json' {
|
||||||
if (typeof val === 'number') return 'number'
|
if (typeof val === 'number') return 'number'
|
||||||
if (typeof val === 'boolean') return 'boolean'
|
if (typeof val === 'boolean') return 'boolean'
|
||||||
|
if (val && typeof val === 'object') return 'json'
|
||||||
if (typeof val === 'string' && val.length > 80) return 'textarea'
|
if (typeof val === 'string' && val.length > 80) return 'textarea'
|
||||||
return 'string'
|
return 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInputsJson(fields: TestParamField[]): string {
|
function buildInputsJson(fields: TestParamField[]): string {
|
||||||
const obj: Record<string, any> = {}
|
const obj: Record<string, any> = {}
|
||||||
fields.forEach(f => { obj[f.name] = f.value })
|
fields.forEach(f => {
|
||||||
|
if (!f.name) return
|
||||||
|
if (f.type === 'json') {
|
||||||
|
try {
|
||||||
|
obj[f.name] = typeof f.value === 'string' ? JSON.parse(f.value || '{}') : f.value
|
||||||
|
} catch {
|
||||||
|
obj[f.name] = f.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj[f.name] = f.value
|
||||||
|
}
|
||||||
|
})
|
||||||
return JSON.stringify(obj, null, 2)
|
return JSON.stringify(obj, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +611,9 @@ function syncEndpointFieldsFromJson() {
|
|||||||
const parsed = JSON.parse(endpointInputsJson.value || '{}')
|
const parsed = JSON.parse(endpointInputsJson.value || '{}')
|
||||||
endpointParamFields.value.forEach(field => {
|
endpointParamFields.value.forEach(field => {
|
||||||
if (field.name in parsed) {
|
if (field.name in parsed) {
|
||||||
field.value = parsed[field.name]
|
field.value = field.type === 'json'
|
||||||
|
? JSON.stringify(parsed[field.name] ?? {}, null, 2)
|
||||||
|
: parsed[field.name]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch { /* ignore parse errors */ }
|
} catch { /* ignore parse errors */ }
|
||||||
@@ -726,6 +784,7 @@ async function submitRuntimeTest() {
|
|||||||
|
|
||||||
async function submitEndpointNonStreamTest() {
|
async function submitEndpointNonStreamTest() {
|
||||||
if (!endpointTestRow.value) return
|
if (!endpointTestRow.value) return
|
||||||
|
if (!validateEndpointFields()) return
|
||||||
let inputs: Record<string, any>
|
let inputs: Record<string, any>
|
||||||
try {
|
try {
|
||||||
inputs = JSON.parse(endpointInputsJson.value || '{}')
|
inputs = JSON.parse(endpointInputsJson.value || '{}')
|
||||||
@@ -757,6 +816,7 @@ async function submitEndpointNonStreamTest() {
|
|||||||
|
|
||||||
async function submitEndpointStreamTest() {
|
async function submitEndpointStreamTest() {
|
||||||
if (!endpointTestRow.value) return
|
if (!endpointTestRow.value) return
|
||||||
|
if (!validateEndpointFields()) return
|
||||||
let inputs: Record<string, any>
|
let inputs: Record<string, any>
|
||||||
try {
|
try {
|
||||||
inputs = JSON.parse(endpointInputsJson.value || '{}')
|
inputs = JSON.parse(endpointInputsJson.value || '{}')
|
||||||
@@ -796,6 +856,26 @@ async function submitEndpointStreamTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateEndpointFields() {
|
||||||
|
for (const field of endpointParamFields.value) {
|
||||||
|
if (!field.required) continue
|
||||||
|
const value = field.value
|
||||||
|
if (value === null || value === undefined || String(value).trim() === '') {
|
||||||
|
ElMessage.error(`请填写必填参数:${field.label || field.name}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const field of endpointParamFields.value.filter(item => item.type === 'json')) {
|
||||||
|
try {
|
||||||
|
JSON.parse(field.value || '{}')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(`参数 ${field.label || field.name} 不是有效 JSON`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadAll)
|
onMounted(loadAll)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
|||||||
port: 5174,
|
port: 5174,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:19089',
|
target: 'https://lifescript.happylifeos.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path
|
rewrite: (path) => path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ VITE_APP_TITLE=情绪博物馆 - 开发
|
|||||||
VITE_APP_VERSION=1.0.0
|
VITE_APP_VERSION=1.0.0
|
||||||
|
|
||||||
# API配置
|
# API配置
|
||||||
VITE_API_BASE_URL=http://localhost:19089/api
|
VITE_API_BASE_URL=https://lifescript.happylifeos.com/api
|
||||||
VITE_WS_BASE_URL=ws://localhost:19089/api
|
VITE_WS_BASE_URL=wss://lifescript.happylifeos.com/ws
|
||||||
VITE_UPLOAD_URL=http://localhost:19089/api/upload
|
VITE_UPLOAD_URL=https://lifescript.happylifeos.com/api/upload
|
||||||
|
|
||||||
# 调试配置
|
# 调试配置
|
||||||
VITE_DEBUG=true
|
VITE_DEBUG=true
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ export const getEnvConfig = (): EnvConfig => {
|
|||||||
case 'local':
|
case 'local':
|
||||||
return {
|
return {
|
||||||
name: '本地环境',
|
name: '本地环境',
|
||||||
apiBaseUrl: 'http://localhost:19089/api',
|
apiBaseUrl: 'https://lifescript.happylifeos.com/api',
|
||||||
wsBaseUrl: 'ws://localhost:19089/api',
|
wsBaseUrl: 'wss://lifescript.happylifeos.com/ws',
|
||||||
uploadUrl: 'http://localhost:19089/api/upload',
|
uploadUrl: 'https://lifescript.happylifeos.com/api/upload',
|
||||||
debug: true,
|
debug: true,
|
||||||
mock: false,
|
mock: false,
|
||||||
appTitle: '情绪博物馆 - 本地',
|
appTitle: '情绪博物馆 - 本地',
|
||||||
@@ -91,9 +91,9 @@ export const getEnvConfig = (): EnvConfig => {
|
|||||||
case 'dev':
|
case 'dev':
|
||||||
return {
|
return {
|
||||||
name: '开发环境',
|
name: '开发环境',
|
||||||
apiBaseUrl: 'http://localhost:19089/api',
|
apiBaseUrl: 'https://lifescript.happylifeos.com/api',
|
||||||
wsBaseUrl: 'ws://localhost:19089/api',
|
wsBaseUrl: 'wss://lifescript.happylifeos.com/ws',
|
||||||
uploadUrl: 'http://localhost:19089/api/upload',
|
uploadUrl: 'https://lifescript.happylifeos.com/api/upload',
|
||||||
debug: true,
|
debug: true,
|
||||||
mock: false,
|
mock: false,
|
||||||
appTitle: '情绪博物馆 - 开发',
|
appTitle: '情绪博物馆 - 开发',
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ export default defineConfig({
|
|||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:19089',
|
target: 'https://lifescript.happylifeos.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user