Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
29 KiB
AI 配置行内测试功能实施计划
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: 在 AI 配置管理页面的场景绑定表和接口工作流表分别增加行内测试按钮,工作流测试独立走 endpoint 直调链路
Architecture: 后端复用现有 adapter/stream 基础设施但绕过场景解析层,前端复用测试对话框 UI 但增加独立的 endpoint 测试对话框。所有修改通过现有 Axios/fetch + SSE 模式完成。
Tech Stack: Vue 3 + Element Plus + TypeScript, Spring Boot 2.7 + MyBatis-Plus
File Structure
| 文件 | 职责 | 操作 |
|---|---|---|
backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java |
DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
backend-single/src/main/java/com/emotion/service/AiRuntimeService.java |
Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java |
Service 实现:新增两个方法的完整实现 | 修改 |
backend-single/src/main/java/com/emotion/controller/AiRoutingController.java |
Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
web-admin/src/types/aiconfig.ts |
TypeScript 类型:新增 AiEndpointRuntimeRequest | 修改 |
web-admin/src/api/aiconfig.ts |
API 层:新增 testEndpointRuntime、streamEndpointRuntime、提取 fetchSseStream | 修改 |
web-admin/src/views/aiconfig/AiRoutingList.vue |
Vue 组件:增加行内测试按钮 + endpoint 测试对话框 | 修改 |
Task 1: DTO 增强 — AiRuntimeRequest 新增 endpointId
Files:
-
Modify:
backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java -
Step 1: 新增 endpointId 字段和 RESERVED_KEYS
// 第 12 行:RESERVED_KEYS 增加 "endpointId"
private static final Set<String> RESERVED_KEYS = Set.of("sceneCode", "scene", "inputs", "userId", "userName", "username", "userType", "requestId", "endpointId");
// 第 16 行:sceneCode 下方新增 endpointId
private String sceneCode;
private String endpointId; // ← 新增
private String userId;
- Step 2: fromPayload() 中增加 endpointId 提取
在 fromPayload() 方法中,在 request.setSceneCode(sceneCode) 下方添加:
request.setEndpointId(payload.getString("endpointId"));
完整 fromPayload() 片段:
public static AiRuntimeRequest fromPayload(JSONObject payload) {
AiRuntimeRequest request = new AiRuntimeRequest();
if (payload == null) {
return request;
}
String sceneCode = payload.getString("sceneCode");
if (!StringUtils.hasText(sceneCode)) {
sceneCode = payload.getString("scene");
}
request.setSceneCode(sceneCode);
request.setEndpointId(payload.getString("endpointId")); // ← 新增
JSONObject inputs = payload.getJSONObject("inputs");
if (inputs == null) {
inputs = new JSONObject();
JSONObject runtimeInputs = inputs;
payload.forEach((key, value) -> {
if (!RESERVED_KEYS.contains(key)) {
runtimeInputs.put(key, value);
}
});
}
request.setInputs(inputs);
return request;
}
- Step 3: 编译验证
Run: cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests
Expected: BUILD SUCCESS(endpointId 字段新增不破坏任何编译,因为这是新增字段)
- Step 4: Commit
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git commit -m "feat: AiRuntimeRequest DTO 新增 endpointId 字段
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 2: Service 接口层 — 新增 testEndpoint + invokeEndpointStream
Files:
-
Modify:
backend-single/src/main/java/com/emotion/service/AiRuntimeService.java -
Step 1: 新增两个接口方法
package com.emotion.service;
import com.emotion.dto.request.ai.AiRuntimeRequest;
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
import com.emotion.dto.response.ai.AiStreamEvent;
import java.util.Map;
import java.util.function.Consumer;
public interface AiRuntimeService {
void invokeStream(AiRuntimeRequest request, Consumer<AiStreamEvent> consumer);
AiRuntimeTestResponse test(AiRuntimeRequest request);
// 新增 ↓
AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs);
void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer);
}
- Step 2: 编译验证
Run: cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests
Expected: BUILD SUCCESS(接口新增方法,但实现类尚未修改,此时不会编译失败 — 因为 ServiceImpl 编译是单独的任务)
- Step 3: Commit
git add backend-single/src/main/java/com/emotion/service/AiRuntimeService.java
git commit -m "feat: AiRuntimeService 接口新增 endpoint 测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 3: Service 实现层 — testEndpoint + invokeEndpointStream 实现
Files:
-
Modify:
backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java -
Step 1: 新增 testEndpoint 方法
在现有 test() 方法(第 115-141 行)之后,添加:
@Override
public AiRuntimeTestResponse testEndpoint(String endpointId, Map<String, Object> inputs) {
long startedAt = System.currentTimeMillis();
StringBuilder output = new StringBuilder();
AtomicInteger chunks = new AtomicInteger(0);
final String[] errorCode = new String[1];
final String[] errorMessage = new String[1];
invokeEndpointStream(endpointId, inputs, event -> {
if ("delta".equals(event.getType()) && event.getContent() != null) {
chunks.incrementAndGet();
output.append(event.getContent());
} else if ("error".equals(event.getType())) {
errorCode[0] = event.getCode();
errorMessage[0] = event.getMessage();
}
});
return AiRuntimeTestResponse.builder()
.sceneCode("")
.status(errorCode[0] == null ? "success" : "failed")
.output(output.toString())
.streamChunks(chunks.get())
.durationMs(System.currentTimeMillis() - startedAt)
.errorCode(errorCode[0])
.errorMessage(errorMessage[0])
.build();
}
- Step 2: 新增 invokeEndpointStream 方法
在 testEndpoint 方法之后,添加:
@Override
public void invokeEndpointStream(String endpointId, Map<String, Object> inputs, Consumer<AiStreamEvent> consumer) {
long startedAt = System.currentTimeMillis();
AtomicLong firstTokenAt = new AtomicLong(0);
AtomicInteger chunks = new AtomicInteger(0);
String requestId = UUID.randomUUID().toString();
StringBuilder output = new StringBuilder();
AiEndpointConfig endpoint = endpointConfigService.getEnabledById(endpointId);
if (endpoint == null) {
throw new IllegalStateException("AI_ENDPOINT_DISABLED");
}
AiProvider provider = providerService.getEnabledById(endpoint.getProviderId());
if (provider == null) {
throw new IllegalStateException("AI_PROVIDER_DISABLED");
}
AiProviderAdapter adapter = adapters.stream()
.filter(item -> item.supports(provider.getProviderType()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("AI_PROVIDER_ADAPTER_NOT_FOUND"));
// 构造请求,显式从 UserContextHolder 设置用户信息
AiRuntimeRequest request = new AiRuntimeRequest();
request.setInputs(inputs == null ? new com.alibaba.fastjson2.JSONObject() : new com.alibaba.fastjson2.JSONObject(inputs));
request.setEndpointId(endpointId);
request.setUserId(resolveUserId(request));
request.setUserName(UserContextHolder.getCurrentUsername());
request.setUserType(UserContextHolder.getCurrentUserType());
request.setRequestId(UserContextHolder.getRequestId());
// 注入 userId / userName / userType / requestId 到 inputs(场景注入因 sceneCode 为空自动跳过)
enrichInputs(request);
AiCallLog callLog = new AiCallLog();
callLog.setRequestId(requestId);
callLog.setEndpointCode(endpoint.getEndpointCode());
callLog.setProviderCode(provider.getProviderCode());
callLog.setUserId(request.getUserId());
callLog.setInputText(JSON.toJSONString(request.getInputs()));
callLog.setStatus("running");
callLogService.save(callLog);
consumer.accept(AiStreamEvent.start(endpoint.getEndpointCode()));
try {
adapter.stream(provider, endpoint, request, event -> {
if ("delta".equals(event.getType())) {
chunks.incrementAndGet();
if (firstTokenAt.compareAndSet(0, System.currentTimeMillis())) {
log.debug("AI first token emitted, endpoint={}, requestId={}", endpoint.getEndpointCode(), requestId);
}
if (event.getContent() != null) {
output.append(event.getContent());
}
}
consumer.accept(event);
});
callLog.setStatus("success");
callLog.setOutputText(output.toString());
callLog.setStreamChunks(chunks.get());
callLog.setFirstTokenMs(firstTokenAt.get() == 0 ? null : firstTokenAt.get() - startedAt);
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
callLogService.updateById(callLog);
consumer.accept(AiStreamEvent.done(Map.of(
"requestId", requestId,
"streamChunks", chunks.get(),
"durationMs", callLog.getDurationMs()
)));
} catch (Exception e) {
String code = normalizeErrorCode(e);
callLog.setStatus("failed");
callLog.setErrorCode(code);
callLog.setErrorMessage(e.getMessage());
callLog.setOutputText(output.toString());
callLog.setStreamChunks(chunks.get());
callLog.setDurationMs(System.currentTimeMillis() - startedAt);
saveOrUpdateLog(callLog);
consumer.accept(AiStreamEvent.error(code, e.getMessage()));
}
}
- Step 3: 编译验证
Run: cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests
Expected: BUILD SUCCESS
- Step 4: Commit
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git commit -m "feat: AiRuntimeServiceImpl 实现 endpoint 直接测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 4: Controller 层 — 新增 /endpoint/test 和 /endpoint/stream
Files:
-
Modify:
backend-single/src/main/java/com/emotion/controller/AiRoutingController.java -
Step 1: 在 runtimeStream 方法之后新增两个 endpoint 方法
在 runtimeStream 方法(第 138-151 行)的 } 之后、withCurrentUser 方法之前,添加:
@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));
}
@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;
}
- Step 2: 编译并验证完整后端
Run: cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests
Expected: BUILD SUCCESS
- Step 3: Commit
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: AiRoutingController 新增 /endpoint/test 和 /endpoint/stream 接口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 5: 前端类型定义 — 新增 AiEndpointRuntimeRequest
Files:
-
Modify:
web-admin/src/types/aiconfig.ts -
Step 1: 在 AiRuntimeRequest 之后新增类型
在 AiRuntimeRequest 接口定义(第 262-266 行)之后、AiRuntimeTestResponse 之前,添加:
export interface AiEndpointRuntimeRequest {
endpointId: string
inputs: Record<string, any>
}
- Step 2: Commit
git add web-admin/src/types/aiconfig.ts
git commit -m "feat: 前端类型定义新增 AiEndpointRuntimeRequest
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 6: 前端 API 层 — 提取 fetchSseStream 并新增 endpoint 测试函数
Files:
-
Modify:
web-admin/src/api/aiconfig.ts -
Step 1: 在 parseSseFrame 之后新增共享辅助函数 fetchSseStream
在 parseSseFrame 函数(第 276-288 行)之后、streamAiRuntime 之前,添加:
async function fetchSseStream(
url: string,
body: Record<string, any>,
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
) {
const token = localStorage.getItem('adminToken')
const response = await fetch(`${import.meta.env.VITE_APP_BASE_API}${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: JSON.stringify(body)
})
if (!response.ok || !response.body) {
throw new Error(`流式测试请求失败(${response.status})`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
let output = ''
const consumeText = (text: string) => {
buffer += text
const frames = buffer.split(/\r?\n\r?\n/)
buffer = frames.pop() || ''
frames.forEach((frame) => {
const event = parseSseFrame(frame)
if (!event) return
if (event.type === 'delta') {
output += event.content || ''
}
onEvent(event, output)
if (event.type === 'error') {
throw new Error(event.message || event.code || '流式测试失败')
}
})
}
while (true) {
const { value, done } = await reader.read()
if (done) break
consumeText(decoder.decode(value, { stream: true }))
}
consumeText(decoder.decode())
if (buffer.trim()) consumeText('\n\n')
return output
}
- Step 2: 修改 streamAiRuntime 委托给 fetchSseStream
将现有 streamAiRuntime(第 290-337 行)替换为:
export async function streamAiRuntime(
data: AiRuntimeRequest,
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
) {
return fetchSseStream('/ai/runtime/stream', data, onEvent)
}
- Step 3: 新增 streamEndpointRuntime 和 testEndpointRuntime
在修改后的 streamAiRuntime 之后,添加:
export async function streamEndpointRuntime(
data: AiEndpointRuntimeRequest,
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
) {
return fetchSseStream('/ai/endpoint/stream', data, onEvent)
}
export function testEndpointRuntime(data: AiEndpointRuntimeRequest) {
return request({ url: '/ai/endpoint/test', method: 'post', data, timeout: 60000 })
}
- Step 4: 验证 TypeScript 编译
Run: cd web-admin && npm run type-check
Expected: 无类型错误
- Step 5: Commit
git add web-admin/src/api/aiconfig.ts
git commit -m "feat: API 层提取 fetchSseStream 并新增 endpoint 测试函数
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 7: Vue 组件 — 场景绑定表增加行内测试按钮
Files:
-
Modify:
web-admin/src/views/aiconfig/AiRoutingList.vue -
Step 1: 修改场景绑定表操作列宽度和新增测试按钮
将第 137-142 行(场景绑定表操作列)从:
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openScene(row)">编辑</el-button>
<el-button link type="danger" @click="removeScene(row)">删除</el-button>
</template>
</el-table-column>
替换为:
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openScene(row)">编辑</el-button>
<el-button link type="danger" @click="removeScene(row)">删除</el-button>
<el-button link type="success" @click="openSceneRuntimeTest(row)">测试</el-button>
</template>
</el-table-column>
- Step 2: 新增 openSceneRuntimeTest 函数
在 openRuntimeTest() 函数之后、submitProvider() 函数之前,添加:
function openSceneRuntimeTest(row: AiSceneBinding) {
testForm.sceneCode = row.sceneCode
testResult.value = null
nonStreamResult.value = null
testDialog.value = true
}
- Step 3: Commit
git add web-admin/src/views/aiconfig/AiRoutingList.vue
git commit -m "feat: 场景绑定表增加行内测试按钮
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 8: Vue 组件 — 接口工作流表增加行内测试按钮
Files:
-
Modify:
web-admin/src/views/aiconfig/AiRoutingList.vue -
Step 1: 修改接口工作流表操作列宽度和新增测试按钮
将第 100-105 行(接口工作流表操作列)从:
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openEndpoint(row)">编辑</el-button>
<el-button link type="danger" @click="removeEndpoint(row)">删除</el-button>
</template>
</el-table-column>
替换为:
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openEndpoint(row)">编辑</el-button>
<el-button link type="danger" @click="removeEndpoint(row)">删除</el-button>
<el-button link type="success" :disabled="row.isEnabled !== 1" @click="openEndpointTest(row)">测试</el-button>
</template>
</el-table-column>
- Step 2: Commit
git add web-admin/src/views/aiconfig/AiRoutingList.vue
git commit -m "feat: 接口工作流表增加行内测试按钮
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 9: Vue 组件 — 新增接口工作流测试对话框
Files:
-
Modify:
web-admin/src/views/aiconfig/AiRoutingList.vue -
Step 1: 新增 API import
在第 288 行(testAiRuntime import)之后添加:
testAiRuntime,
testEndpointRuntime,
streamEndpointRuntime
} from '@/api/aiconfig'
- Step 2: 新增响应式状态变量
在 const testInputsJson 声明(第 307 行)之后,添加:
const endpointTestDialog = ref(false)
const endpointTesting = ref(false)
const endpointTestRow = ref<AiEndpointConfig | null>(null)
const endpointTestResult = ref<AiRuntimeTestResponse | null>(null)
const endpointNonStreamResult = ref<AiRuntimeTestResponse | null>(null)
const endpointInputsJson = ref('{}')
- Step 3: 新增 endpoint 测试对话框 template
在现有测试对话框 </el-dialog> 标签(第 269 行)之后,添加新的 endpoint 测试对话框:
<el-dialog v-model="endpointTestDialog" title="接口工作流测试" width="760px">
<div class="endpoint-info">
<span class="endpoint-label">接口名称:</span>
<span>{{ endpointTestRow?.endpointName }}({{ endpointTestRow?.endpointCode }})</span>
</div>
<el-form label-width="110px" style="margin-top: 16px">
<el-form-item label="入参 JSON">
<el-input v-model="endpointInputsJson" type="textarea" :rows="6" placeholder="请输入 JSON 入参" />
</el-form-item>
</el-form>
<el-alert
v-if="endpointNonStreamResult"
:type="endpointNonStreamResult.status === 'success' ? 'success' : 'error'"
:title="endpointNonStreamResult.status === 'success' ? '非流式测试成功' : '非流式测试失败'"
show-icon
/>
<pre v-if="endpointNonStreamResult" class="test-output">{{ endpointNonStreamResult.output || endpointNonStreamResult.errorMessage || '暂无输出' }}</pre>
<el-alert
v-if="endpointTestResult"
:type="endpointTestResult.status === 'success' ? 'success' : 'error'"
:title="endpointTestResult.status === 'success' ? '流式测试成功' : '流式测试失败'"
show-icon
/>
<pre v-if="endpointTestResult" class="test-output">{{ endpointTestResult.output || endpointTestResult.errorMessage || '暂无输出' }}</pre>
<template #footer>
<el-button @click="endpointTestDialog = false">关闭</el-button>
<el-button :loading="endpointTesting" @click="submitEndpointNonStreamTest">非流式测试</el-button>
<el-button type="primary" :loading="endpointTesting" @click="submitEndpointStreamTest">流式测试</el-button>
</template>
</el-dialog>
- Step 4: 新增 openEndpointTest 函数
在 openSceneRuntimeTest() 函数之后,添加:
function openEndpointTest(row: AiEndpointConfig) {
endpointTestRow.value = row
if (row.defaultInputs) {
try {
const parsed = JSON.parse(row.defaultInputs)
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
endpointInputsJson.value = JSON.stringify(parsed, null, 2)
} else {
endpointInputsJson.value = '{}'
}
} catch {
endpointInputsJson.value = '{}'
}
} else {
endpointInputsJson.value = '{}'
}
endpointTestResult.value = null
endpointNonStreamResult.value = null
endpointTestDialog.value = true
}
- Step 5: 新增 submitEndpointNonStreamTest 函数
在 submitNonStreamTest() 函数(第 462-490 行)之后,添加:
async function submitEndpointNonStreamTest() {
if (!endpointTestRow.value) return
let inputs: Record<string, any>
try {
inputs = JSON.parse(endpointInputsJson.value || '{}')
} catch {
ElMessage.error('入参 JSON 格式不正确')
return
}
endpointTesting.value = true
try {
const res = await testEndpointRuntime({ endpointId: endpointTestRow.value.id!, inputs })
endpointNonStreamResult.value = res.data as AiRuntimeTestResponse
if (endpointNonStreamResult.value.status === 'success') {
ElMessage.success('非流式测试成功')
} else {
ElMessage.error(`测试失败: ${endpointNonStreamResult.value.errorMessage || endpointNonStreamResult.value.errorCode}`)
}
await loadAll()
} catch (error: any) {
endpointNonStreamResult.value = {
sceneCode: '',
status: 'failed',
errorMessage: error?.message || '非流式测试失败'
} as AiRuntimeTestResponse
ElMessage.error(error?.message || '非流式测试失败')
} finally {
endpointTesting.value = false
}
}
- Step 6: 新增 submitEndpointStreamTest 函数
在 submitEndpointNonStreamTest() 之后,添加:
async function submitEndpointStreamTest() {
if (!endpointTestRow.value) return
let inputs: Record<string, any>
try {
inputs = JSON.parse(endpointInputsJson.value || '{}')
} catch {
ElMessage.error('入参 JSON 格式不正确')
return
}
endpointTesting.value = true
endpointTestResult.value = { sceneCode: '', status: 'success', output: '', streamChunks: 0, durationMs: 0 }
const startedAt = Date.now()
try {
await streamEndpointRuntime({ endpointId: endpointTestRow.value.id!, inputs }, (event, output) => {
if (!endpointTestResult.value) return
endpointTestResult.value.output = output
if (event.type === 'delta') {
endpointTestResult.value.streamChunks = (endpointTestResult.value.streamChunks || 0) + 1
}
if (event.type === 'done') {
endpointTestResult.value.status = 'success'
}
if (event.type === 'error') {
endpointTestResult.value.status = 'failed'
endpointTestResult.value.errorCode = event.code
endpointTestResult.value.errorMessage = event.message
}
endpointTestResult.value.durationMs = Date.now() - startedAt
})
await loadAll()
} catch (error: any) {
if (endpointTestResult.value) {
endpointTestResult.value.status = 'failed'
endpointTestResult.value.errorMessage = error?.message || '流式测试失败'
endpointTestResult.value.durationMs = Date.now() - startedAt
}
} finally {
endpointTesting.value = false
}
}
- Step 7: 新增 endpoint-info 样式
在 <style scoped> 部分末尾(.test-output 样式之后),添加:
.endpoint-info {
padding: 10px 14px;
color: rgba(226, 232, 240, 0.85);
background: rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--ls-radius-md);
font-size: 14px;
}
.endpoint-label {
color: var(--ls-text-muted);
margin-right: 6px;
}
- Step 8: 编译验证前端
Run: cd web-admin && npm run type-check
Expected: 无类型错误
- Step 9: Commit
git add web-admin/src/views/aiconfig/AiRoutingList.vue
git commit -m "feat: 新增接口工作流测试对话框(流式+非流式)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Task 10: 浏览器验证
- Step 1: 启动 web-admin 开发服务器
cd web-admin && npm run dev
- Step 2: 登录管理后台
打开浏览器访问 http://localhost:5174/emotion-museum-admin/
使用账号登录:
-
手机号:19928748688
-
短信验证码:123456
-
Step 3: 验证场景绑定表行内测试
- 导航到「AI 配置管理」页面
- 点击「场景绑定」Tab
- 确认每行操作列有「编辑」「删除」「测试」三个按钮
- 点击某行的「测试」按钮,确认测试对话框打开且自动填入该行的 sceneCode
- 输入入参
{"prompt": "请用一句中文回复测试成功。"} - 点击「非流式测试」,确认返回成功结果
- 点击「流式测试」,确认流式输出正常
- 关闭对话框,点击另一行的「测试」按钮,确认结果已清空且 sceneCode 已更新
- Step 4: 验证接口工作流表行内测试
- 点击「接口工作流」Tab
- 确认每行操作列有「编辑」「删除」「测试」三个按钮
- 确认禁用的 endpoint 的「测试」按钮是禁用状态
- 点击某行(如
dify.script.generate)的「测试」按钮 - 确认新对话框打开,标题「接口工作流测试」
- 确认对话框顶部显示接口名称和编码
- 确认入参 JSON 框已自动填入该 endpoint 的 defaultInputs(或
{}) - 输入入参,点击「非流式测试」,确认返回成功结果
- 点击「流式测试」,确认流式输出正常
- 关闭对话框,打开另一行的测试,确认结果已清空
- Step 5: 验证控制台无报错
确认浏览器 Console 没有任何错误
- Step 6: 最终 Commit
git add .
git commit -m "feat: AI 配置管理页面增加行内测试功能(场景绑定+接口工作流)
- 场景绑定表每行增加「测试」按钮,自动填入 sceneCode
- 接口工作流表每行增加「测试」按钮,支持流式和非流式测试
- 后端新增 /ai/endpoint/test 和 /ai/endpoint/stream 接口
- 前端提取 fetchSseStream 共享辅助函数,避免代码重复
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
Self-Review Checklist
1. Spec coverage:
- ✅ 场景绑定表每行增加「测试」按钮 → Task 7
- ✅ 接口工作流表每行增加「测试」按钮 → Task 8
- ✅ 接口工作流测试对话框(流式+非流式)→ Task 9
- ✅ 后端 DTO 增强(endpointId)→ Task 1
- ✅ 后端 Service 接口新增方法 → Task 2
- ✅ 后端 Service 实现 → Task 3
- ✅ 后端 Controller 新增接口 → Task 4
- ✅ 前端类型定义 → Task 5
- ✅ 前端 API 层(fetchSseStream 提取 + endpoint 函数)→ Task 6
- ✅ 浏览器验证 → Task 10
2. Placeholder scan:
- ✅ 无 "TBD", "TODO", "implement later"
- ✅ 所有函数都有完整代码
- ✅ 无 "similar to Task N" 引用
3. Type consistency:
- ✅
AiEndpointRuntimeRequest类型在 Task 5 定义,Task 6 和 Task 9 中使用一致 - ✅
testEndpointRuntime和streamEndpointRuntime签名一致 - ✅
endpointId字段名在后端和前端一致