mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-03-29 23:14:57 +00:00
209 lines
6.1 KiB
JavaScript
209 lines
6.1 KiB
JavaScript
import { ref, computed, onUnmounted } from 'vue'
|
|
|
|
export const useTestState = () => {
|
|
// ========== 状态 ==========
|
|
const testStatus = ref('idle') // idle, testing, success, error
|
|
const responseText = ref('')
|
|
const errorMessage = ref('')
|
|
const testDuration = ref(0)
|
|
const testStartTime = ref(null)
|
|
const abortController = ref(null)
|
|
|
|
// ========== 状态样式计算属性 ==========
|
|
const statusStyleMap = {
|
|
idle: {
|
|
title: '准备就绪',
|
|
card: 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50',
|
|
iconBg: 'bg-gray-200 dark:bg-gray-700',
|
|
icon: 'fa-hourglass-start',
|
|
iconColor: 'text-gray-500 dark:text-gray-400',
|
|
text: 'text-gray-700 dark:text-gray-300'
|
|
},
|
|
testing: {
|
|
title: '正在测试...',
|
|
card: 'border-blue-200 bg-blue-50 dark:border-blue-500/30 dark:bg-blue-900/20',
|
|
iconBg: 'bg-blue-100 dark:bg-blue-500/30',
|
|
icon: 'fa-spinner fa-spin',
|
|
iconColor: 'text-blue-500 dark:text-blue-400',
|
|
text: 'text-blue-700 dark:text-blue-300'
|
|
},
|
|
success: {
|
|
title: '测试成功',
|
|
card: 'border-green-200 bg-green-50 dark:border-green-500/30 dark:bg-green-900/20',
|
|
iconBg: 'bg-green-100 dark:bg-green-500/30',
|
|
icon: 'fa-check-circle',
|
|
iconColor: 'text-green-500 dark:text-green-400',
|
|
text: 'text-green-700 dark:text-green-300'
|
|
},
|
|
error: {
|
|
title: '测试失败',
|
|
card: 'border-red-200 bg-red-50 dark:border-red-500/30 dark:bg-red-900/20',
|
|
iconBg: 'bg-red-100 dark:bg-red-500/30',
|
|
icon: 'fa-exclamation-circle',
|
|
iconColor: 'text-red-500 dark:text-red-400',
|
|
text: 'text-red-700 dark:text-red-300'
|
|
}
|
|
}
|
|
|
|
const currentStyle = computed(() => statusStyleMap[testStatus.value] || statusStyleMap.idle)
|
|
const statusTitle = computed(() => currentStyle.value.title)
|
|
const statusCardClass = computed(() => currentStyle.value.card)
|
|
const statusIconBgClass = computed(() => currentStyle.value.iconBg)
|
|
const statusIcon = computed(() => currentStyle.value.icon)
|
|
const statusIconClass = computed(() => currentStyle.value.iconColor)
|
|
const statusTextClass = computed(() => currentStyle.value.text)
|
|
|
|
// ========== SSE 事件处理 ==========
|
|
const handleSSEEvent = (data) => {
|
|
switch (data.type) {
|
|
case 'test_start':
|
|
break
|
|
case 'content':
|
|
responseText.value += data.text
|
|
break
|
|
case 'message_stop':
|
|
break
|
|
case 'test_complete':
|
|
testDuration.value = Date.now() - testStartTime.value
|
|
if (data.success) {
|
|
testStatus.value = 'success'
|
|
} else {
|
|
testStatus.value = 'error'
|
|
errorMessage.value = data.error || '测试失败'
|
|
}
|
|
break
|
|
case 'error':
|
|
testStatus.value = 'error'
|
|
errorMessage.value = data.error || '未知错误'
|
|
testDuration.value = Date.now() - testStartTime.value
|
|
break
|
|
}
|
|
}
|
|
|
|
// ========== SSE 流读取 ==========
|
|
const readSSEStream = async (response) => {
|
|
const reader = response.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let streamDone = false
|
|
let buffer = ''
|
|
|
|
while (!streamDone) {
|
|
const { done, value } = await reader.read()
|
|
if (done) {
|
|
streamDone = true
|
|
// 处理缓冲区中剩余的数据
|
|
if (buffer.trim()) {
|
|
processSSELine(buffer)
|
|
}
|
|
continue
|
|
}
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
const lines = buffer.split('\n')
|
|
// 最后一行可能不完整,保留在缓冲区
|
|
buffer = lines.pop() || ''
|
|
|
|
for (const line of lines) {
|
|
processSSELine(line)
|
|
}
|
|
}
|
|
}
|
|
|
|
const processSSELine = (line) => {
|
|
if (line.startsWith('data: ')) {
|
|
try {
|
|
const data = JSON.parse(line.substring(6))
|
|
handleSSEEvent(data)
|
|
} catch {
|
|
// 忽略解析错误
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========== 通用测试请求 ==========
|
|
const sendTestRequest = async (endpoint, payload, options = {}) => {
|
|
const { useSSE = true, headers = {} } = options
|
|
|
|
// 重置状态
|
|
testStatus.value = 'testing'
|
|
responseText.value = ''
|
|
errorMessage.value = ''
|
|
testDuration.value = 0
|
|
testStartTime.value = Date.now()
|
|
|
|
// 取消之前的请求
|
|
if (abortController.value) {
|
|
abortController.value.abort()
|
|
}
|
|
abortController.value = new AbortController()
|
|
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify(payload),
|
|
signal: abortController.value.signal
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}))
|
|
throw new Error(errorData.message || errorData.error || `HTTP ${response.status}`)
|
|
}
|
|
|
|
if (useSSE) {
|
|
await readSSEStream(response)
|
|
} else {
|
|
// JSON 响应
|
|
const data = await response.json()
|
|
testDuration.value = Date.now() - testStartTime.value
|
|
if (data.success) {
|
|
testStatus.value = 'success'
|
|
responseText.value = data.data?.responseText || 'Test passed'
|
|
} else {
|
|
testStatus.value = 'error'
|
|
errorMessage.value = data.message || 'Test failed'
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (err.name === 'AbortError') return
|
|
testStatus.value = 'error'
|
|
errorMessage.value = err.message || '连接失败'
|
|
testDuration.value = Date.now() - testStartTime.value
|
|
}
|
|
}
|
|
|
|
// ========== 重置 + 清理 ==========
|
|
const resetState = () => {
|
|
testStatus.value = 'idle'
|
|
responseText.value = ''
|
|
errorMessage.value = ''
|
|
testDuration.value = 0
|
|
testStartTime.value = null
|
|
}
|
|
|
|
const cleanup = () => {
|
|
if (abortController.value) {
|
|
abortController.value.abort()
|
|
abortController.value = null
|
|
}
|
|
}
|
|
|
|
onUnmounted(cleanup)
|
|
|
|
return {
|
|
testStatus,
|
|
responseText,
|
|
errorMessage,
|
|
testDuration,
|
|
statusTitle,
|
|
statusCardClass,
|
|
statusIconBgClass,
|
|
statusIcon,
|
|
statusIconClass,
|
|
statusTextClass,
|
|
sendTestRequest,
|
|
resetState,
|
|
cleanup
|
|
}
|
|
}
|