Files
happy-life-star/docs/superpowers/plans/2026-05-23-ai-config-row-test.md
T
2026-05-23 14:09:11 +08:00

865 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**
```java
// 第 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)` 下方添加:
```java
request.setEndpointId(payload.getString("endpointId"));
```
完整 fromPayload() 片段:
```java
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 SUCCESSendpointId 字段新增不破坏任何编译,因为这是新增字段)
- [ ] **Step 4: Commit**
```bash
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: 新增两个接口方法**
```java
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**
```bash
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 行)之后,添加:
```java
@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` 方法之后,添加:
```java
@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**
```bash
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` 方法之前,添加:
```java
@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**
```bash
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` 之前,添加:
```typescript
export interface AiEndpointRuntimeRequest {
endpointId: string
inputs: Record<string, any>
}
```
- [ ] **Step 2: Commit**
```bash
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` 之前,添加:
```typescript
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 行)替换为:
```typescript
export async function streamAiRuntime(
data: AiRuntimeRequest,
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
) {
return fetchSseStream('/ai/runtime/stream', data, onEvent)
}
```
- [ ] **Step 3: 新增 streamEndpointRuntime 和 testEndpointRuntime**
在修改后的 `streamAiRuntime` 之后,添加:
```typescript
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**
```bash
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 行(场景绑定表操作列)从:
```vue
<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>
```
替换为:
```vue
<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()` 函数之前,添加:
```typescript
function openSceneRuntimeTest(row: AiSceneBinding) {
testForm.sceneCode = row.sceneCode
testResult.value = null
nonStreamResult.value = null
testDialog.value = true
}
```
- [ ] **Step 3: Commit**
```bash
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 行(接口工作流表操作列)从:
```vue
<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>
```
替换为:
```vue
<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**
```bash
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)之后添加:
```typescript
testAiRuntime,
testEndpointRuntime,
streamEndpointRuntime
} from '@/api/aiconfig'
```
- [ ] **Step 2: 新增响应式状态变量**
`const testInputsJson` 声明(第 307 行)之后,添加:
```typescript
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 测试对话框:
```vue
<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()` 函数之后,添加:
```typescript
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 行)之后,添加:
```typescript
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()` 之后,添加:
```typescript
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` 样式之后),添加:
```css
.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**
```bash
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 开发服务器**
```bash
cd web-admin && npm run dev
```
- [ ] **Step 2: 登录管理后台**
打开浏览器访问 `http://localhost:5174/emotion-museum-admin/`
使用账号登录:
- 手机号:19928748688
- 短信验证码:123456
- [ ] **Step 3: 验证场景绑定表行内测试**
1. 导航到「AI 配置管理」页面
2. 点击「场景绑定」Tab
3. 确认每行操作列有「编辑」「删除」「测试」三个按钮
4. 点击某行的「测试」按钮,确认测试对话框打开且自动填入该行的 sceneCode
5. 输入入参 `{"prompt": "请用一句中文回复测试成功。"}`
6. 点击「非流式测试」,确认返回成功结果
7. 点击「流式测试」,确认流式输出正常
8. 关闭对话框,点击另一行的「测试」按钮,确认结果已清空且 sceneCode 已更新
- [ ] **Step 4: 验证接口工作流表行内测试**
1. 点击「接口工作流」Tab
2. 确认每行操作列有「编辑」「删除」「测试」三个按钮
3. 确认禁用的 endpoint 的「测试」按钮是禁用状态
4. 点击某行(如 `dify.script.generate`)的「测试」按钮
5. 确认新对话框打开,标题「接口工作流测试」
6. 确认对话框顶部显示接口名称和编码
7. 确认入参 JSON 框已自动填入该 endpoint 的 defaultInputs(或 `{}`
8. 输入入参,点击「非流式测试」,确认返回成功结果
9. 点击「流式测试」,确认流式输出正常
10. 关闭对话框,打开另一行的测试,确认结果已清空
- [ ] **Step 5: 验证控制台无报错**
确认浏览器 Console 没有任何错误
- [ ] **Step 6: 最终 Commit**
```bash
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` 字段名在后端和前端一致