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

29 KiB
Raw Permalink Blame History

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 SUCCESSendpointId 字段新增不破坏任何编译,因为这是新增字段)

  • 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: 验证场景绑定表行内测试

  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
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 中使用一致
  • testEndpointRuntimestreamEndpointRuntime 签名一致
  • endpointId 字段名在后端和前端一致