AI接口支持流式调用

This commit is contained in:
2025-10-30 15:59:48 +08:00
parent 9ddc6887ff
commit 093d07ab76
2 changed files with 596 additions and 153 deletions
+225 -35
View File
@@ -549,6 +549,28 @@
<el-input v-model="testRequest.url" readonly />
</el-form-item>
<el-form-item label="测试选项">
<div class="test-options">
<el-checkbox
v-model="testOptions.useStream"
@change="updateTestRequestBody"
>
启用流式响应
</el-checkbox>
<el-tooltip content="启用后将测试流式返回,可以看到AI逐步生成的响应内容" placement="top">
<el-icon class="info-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="测试消息">
<el-input
v-model="testOptions.testMessage"
placeholder="输入测试消息内容"
@input="updateTestRequestBody"
/>
</el-form-item>
<el-form-item label="请求头">
<el-input
v-model="testRequest.headers"
@@ -562,14 +584,14 @@
<el-input
v-model="testRequest.body"
type="textarea"
:rows="12"
:rows="10"
placeholder="JSON格式的请求体"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleTestRequest" :loading="testLoading">
发送测试请求
{{ testOptions.useStream ? '发送流式测试' : '发送测试请求' }}
</el-button>
<el-button @click="handleFormatRequest">格式化请求</el-button>
<el-button @click="handleResetTest">重置</el-button>
@@ -625,6 +647,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { InfoFilled } from '@element-plus/icons-vue'
import {
getAiConfigPage,
createAiConfig,
@@ -746,6 +769,11 @@ const testResponse = reactive({
body: ''
})
const testOptions = reactive({
useStream: false,
testMessage: '你好,这是一个测试消息,请回复确认接口正常工作。'
})
// 获取配置类型标签类型
const getConfigTypeTagType = (type: string) => {
const typeMap: Record<string, string> = {
@@ -1078,11 +1106,11 @@ const initTestData = (config: AiConfig) => {
const requestBody: any = {
bot_id: config.botId || '',
user_id: 'test_user_' + Date.now(),
stream: false,
stream: testOptions.useStream,
additional_messages: [
{
role: 'user',
content: '你好,这是一个测试消息,请回复确认接口正常工作。',
content: testOptions.testMessage,
content_type: 'text',
type: 'question'
}
@@ -1113,6 +1141,20 @@ const initTestData = (config: AiConfig) => {
testResponse.body = ''
}
// 更新测试请求体
const updateTestRequestBody = () => {
if (!testConfig.value) return
try {
const body = JSON.parse(testRequest.body)
body.stream = testOptions.useStream
body.additional_messages[0].content = testOptions.testMessage
testRequest.body = JSON.stringify(body, null, 2)
} catch (e) {
console.warn('更新请求体失败:', e)
}
}
// 发送测试请求
const handleTestRequest = async () => {
if (!testConfig.value) return
@@ -1138,39 +1180,15 @@ const handleTestRequest = async () => {
return
}
// 发送请求
const response = await fetch(testRequest.url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 检查是否为流式请求
const isStreamRequest = body.stream === true
// 获取响应头
const responseHeaders: any = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
// 获取响应体
const responseBody = await response.text()
// 更新响应数据
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
testResponse.body = responseBody
// 尝试格式化响应体
try {
const jsonBody = JSON.parse(responseBody)
testResponse.body = JSON.stringify(jsonBody, null, 2)
} catch (e) {
// 如果不是JSON格式,保持原样
}
if (response.ok) {
ElMessage.success('测试请求发送成功')
if (isStreamRequest) {
// 处理流式请求
await handleStreamRequest(headers, body)
} else {
ElMessage.warning(`请求返回状态码: ${response.status}`)
// 处理普通请求
await handleNormalRequest(headers, body)
}
} catch (error: any) {
@@ -1190,6 +1208,165 @@ const handleTestRequest = async () => {
}
}
// 处理普通请求
const handleNormalRequest = async (headers: any, body: any) => {
const response = await fetch(testRequest.url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 获取响应头
const responseHeaders: any = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
// 获取响应体
const responseBody = await response.text()
// 更新响应数据
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
testResponse.body = responseBody
// 尝试格式化响应体
try {
const jsonBody = JSON.parse(responseBody)
testResponse.body = JSON.stringify(jsonBody, null, 2)
} catch (e) {
// 如果不是JSON格式,保持原样
}
if (response.ok) {
ElMessage.success('测试请求发送成功')
} else {
ElMessage.warning(`请求返回状态码: ${response.status}`)
}
}
// 处理流式请求
const handleStreamRequest = async (headers: any, body: any) => {
const response = await fetch(testRequest.url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 获取响应头
const responseHeaders: any = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
if (!response.ok) {
const errorBody = await response.text()
testResponse.body = errorBody
ElMessage.warning(`请求返回状态码: ${response.status}`)
return
}
if (!response.body) {
testResponse.body = 'Error: 响应体为空'
ElMessage.error('响应体为空')
return
}
// 处理流式响应
const reader = response.body.getReader()
const decoder = new TextDecoder()
let streamContent = ''
let chunks: string[] = []
// 清空响应体,准备接收流式数据
testResponse.body = '正在接收流式数据...\n\n'
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
// 解码数据块
const chunk = decoder.decode(value, { stream: true })
streamContent += chunk
chunks.push(chunk)
// 实时更新响应体显示
testResponse.body = `=== 流式响应数据 ===\n\n` +
`接收到 ${chunks.length} 个数据块,总长度: ${streamContent.length} 字符\n\n` +
`=== 原始数据流 ===\n${streamContent}\n\n` +
`=== 解析后的数据 ===\n${parseStreamData(streamContent)}`
}
ElMessage.success(`流式请求完成,共接收 ${chunks.length} 个数据块`)
} catch (streamError: any) {
console.error('流式数据读取失败:', streamError)
testResponse.body += `\n\n=== 流式读取错误 ===\n${streamError.message || streamError}`
ElMessage.error('流式数据读取失败: ' + (streamError.message || streamError))
} finally {
reader.releaseLock()
}
}
// 解析流式数据
const parseStreamData = (streamContent: string): string => {
try {
const lines = streamContent.split('\n')
const parsedData: any[] = []
let currentEvent = ''
let currentData = ''
for (const line of lines) {
if (line.startsWith('event:')) {
currentEvent = line.substring(6).trim()
} else if (line.startsWith('data:')) {
currentData = line.substring(5).trim()
if (currentData === '[DONE]') {
parsedData.push({
event: currentEvent || 'done',
data: '[DONE]',
timestamp: new Date().toISOString()
})
} else if (currentData) {
try {
const jsonData = JSON.parse(currentData)
parsedData.push({
event: currentEvent || 'data',
data: jsonData,
timestamp: new Date().toISOString()
})
} catch (e) {
parsedData.push({
event: currentEvent || 'raw',
data: currentData,
timestamp: new Date().toISOString()
})
}
}
currentEvent = ''
currentData = ''
} else if (line.trim() === '') {
// 空行,重置状态
currentEvent = ''
currentData = ''
}
}
return JSON.stringify(parsedData, null, 2)
} catch (e) {
return `解析失败: ${e}\n\n原始内容:\n${streamContent}`
}
}
// 格式化请求
const handleFormatRequest = () => {
try {
@@ -1260,6 +1437,8 @@ const handleTestDialogClose = () => {
testResponse.status = null
testResponse.headers = ''
testResponse.body = ''
testOptions.useStream = false
testOptions.testMessage = '你好,这是一个测试消息,请回复确认接口正常工作。'
}
onMounted(() => {
@@ -1321,6 +1500,17 @@ onMounted(() => {
padding-bottom: 8px;
}
.test-options {
display: flex;
align-items: center;
gap: 8px;
.info-icon {
color: #909399;
cursor: help;
}
}
.el-textarea {
:deep(.el-textarea__inner) {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;