460 lines
11 KiB
TypeScript
460 lines
11 KiB
TypeScript
import request from '@/utils/request'
|
|
import type {
|
|
AiConfigPageRequest,
|
|
AiConfigCreateRequest,
|
|
AiConfigUpdateRequest,
|
|
AiProvider,
|
|
AiEndpointConfig,
|
|
AiSceneBinding,
|
|
AiCallLog,
|
|
AiRuntimeRequest,
|
|
AiEndpointRuntimeRequest
|
|
} from '@/types/aiconfig'
|
|
import type { LogQueryParams, PageResult } from '@/types/common'
|
|
|
|
// 分页查询AI配置
|
|
export function getAiConfigPage(params: AiConfigPageRequest) {
|
|
return request({
|
|
url: '/aiConfig/page',
|
|
method: 'get',
|
|
params
|
|
})
|
|
}
|
|
|
|
// 根据ID获取AI配置
|
|
export function getAiConfigById(id: string) {
|
|
return request({
|
|
url: '/aiConfig/detail',
|
|
method: 'get',
|
|
params: { id }
|
|
})
|
|
}
|
|
|
|
// 创建AI配置
|
|
export function createAiConfig(data: AiConfigCreateRequest) {
|
|
return request({
|
|
url: '/aiConfig/create',
|
|
method: 'post',
|
|
data
|
|
})
|
|
}
|
|
|
|
// 更新AI配置
|
|
export function updateAiConfig(data: AiConfigUpdateRequest) {
|
|
return request({
|
|
url: '/aiConfig/update',
|
|
method: 'put',
|
|
data
|
|
})
|
|
}
|
|
|
|
// 删除AI配置
|
|
export function deleteAiConfig(id: string) {
|
|
return request({
|
|
url: '/aiConfig/delete',
|
|
method: 'delete',
|
|
params: { id }
|
|
})
|
|
}
|
|
|
|
// 根据配置类型查询AI配置
|
|
export function getAiConfigByType(configType: string) {
|
|
return request({
|
|
url: '/aiConfig/byConfigType',
|
|
method: 'get',
|
|
params: { configType }
|
|
})
|
|
}
|
|
|
|
// 根据服务提供商查询AI配置
|
|
export function getAiConfigByProvider(provider: string) {
|
|
return request({
|
|
url: '/aiConfig/byProvider',
|
|
method: 'get',
|
|
params: { provider }
|
|
})
|
|
}
|
|
|
|
// 根据使用场景查询AI配置
|
|
export function getAiConfigByUsageScenario(usageScenario: string) {
|
|
return request({
|
|
url: '/aiConfig/byUsageScenario',
|
|
method: 'get',
|
|
params: { usageScenario }
|
|
})
|
|
}
|
|
|
|
// 根据环境查询AI配置
|
|
export function getAiConfigByEnvironment(environment: string) {
|
|
return request({
|
|
url: '/aiConfig/byEnvironment',
|
|
method: 'get',
|
|
params: { environment }
|
|
})
|
|
}
|
|
|
|
// 查询已启用的AI配置
|
|
export function getEnabledAiConfigs() {
|
|
return request({
|
|
url: '/aiConfig/enabled',
|
|
method: 'get'
|
|
})
|
|
}
|
|
|
|
// 查询已禁用的AI配置
|
|
export function getDisabledAiConfigs() {
|
|
return request({
|
|
url: '/aiConfig/disabled',
|
|
method: 'get'
|
|
})
|
|
}
|
|
|
|
// 查询默认配置
|
|
export function getDefaultAiConfigs() {
|
|
return request({
|
|
url: '/aiConfig/default',
|
|
method: 'get'
|
|
})
|
|
}
|
|
|
|
// 根据配置键值查询AI配置
|
|
export function getAiConfigByKey(configKey: string) {
|
|
return request({
|
|
url: '/aiConfig/byConfigKey',
|
|
method: 'get',
|
|
params: { configKey }
|
|
})
|
|
}
|
|
|
|
// 启用AI配置
|
|
export function enableAiConfig(id: string) {
|
|
return request({
|
|
url: '/aiConfig/enable',
|
|
method: 'put',
|
|
params: { id }
|
|
})
|
|
}
|
|
|
|
// 禁用AI配置
|
|
export function disableAiConfig(id: string) {
|
|
return request({
|
|
url: '/aiConfig/disable',
|
|
method: 'put',
|
|
params: { id }
|
|
})
|
|
}
|
|
|
|
// 设置为默认配置
|
|
export function setAsDefaultConfig(id: string) {
|
|
return request({
|
|
url: '/aiConfig/setDefault',
|
|
method: 'put',
|
|
params: { id }
|
|
})
|
|
}
|
|
|
|
// 取消默认配置
|
|
export function unsetDefaultConfig(id: string) {
|
|
return request({
|
|
url: '/aiConfig/unsetDefault',
|
|
method: 'put',
|
|
params: { id }
|
|
})
|
|
}
|
|
|
|
// 查询最优配置
|
|
export function getBestAiConfig(usageScenario: string, environment: string) {
|
|
return request({
|
|
url: '/aiConfig/bestConfig',
|
|
method: 'get',
|
|
params: { usageScenario, environment }
|
|
})
|
|
}
|
|
|
|
// 统计已启用配置数量
|
|
export function countEnabledConfigs() {
|
|
return request({
|
|
url: '/aiConfig/countEnabled',
|
|
method: 'get'
|
|
})
|
|
}
|
|
|
|
// 统计已禁用配置数量
|
|
export function countDisabledConfigs() {
|
|
return request({
|
|
url: '/aiConfig/countDisabled',
|
|
method: 'get'
|
|
})
|
|
}
|
|
|
|
// 统计默认配置数量
|
|
export function countDefaultConfigs() {
|
|
return request({
|
|
url: '/aiConfig/countDefault',
|
|
method: 'get'
|
|
})
|
|
}
|
|
|
|
// 根据配置类型统计数量
|
|
export function countByConfigType(configType: string) {
|
|
return request({
|
|
url: '/aiConfig/countByConfigType',
|
|
method: 'get',
|
|
params: { configType }
|
|
})
|
|
}
|
|
|
|
// 根据服务提供商统计数量
|
|
export function countByProvider(provider: string) {
|
|
return request({
|
|
url: '/aiConfig/countByProvider',
|
|
method: 'get',
|
|
params: { provider }
|
|
})
|
|
}
|
|
|
|
|
|
// 测试后更新AI配置
|
|
export function updateAiConfigFromTest(data: any) {
|
|
return request({
|
|
url: '/aiConfig/updateFromTest',
|
|
method: 'put',
|
|
data
|
|
})
|
|
}
|
|
|
|
export function listAiProviders() {
|
|
return request({ url: '/ai/providers', method: 'get' })
|
|
}
|
|
|
|
export function saveAiProvider(data: AiProvider) {
|
|
return request({ url: '/ai/providers', method: data.id ? 'put' : 'post', data })
|
|
}
|
|
|
|
export function deleteAiProvider(id: string) {
|
|
return request({ url: '/ai/providers', method: 'delete', params: { id } })
|
|
}
|
|
|
|
export function listAiEndpoints() {
|
|
return request({ url: '/ai/endpoints', method: 'get' })
|
|
}
|
|
|
|
export function getEndpointTestTemplate(id: string) {
|
|
return request({ url: '/ai/endpoints/test-template', method: 'get', params: { id } })
|
|
}
|
|
|
|
export function saveAiEndpoint(data: AiEndpointConfig) {
|
|
return request({ url: '/ai/endpoints', method: data.id ? 'put' : 'post', data })
|
|
}
|
|
|
|
export function deleteAiEndpoint(id: string) {
|
|
return request({ url: '/ai/endpoints', method: 'delete', params: { id } })
|
|
}
|
|
|
|
export function listAiScenes() {
|
|
return request({ url: '/ai/scenes', method: 'get' })
|
|
}
|
|
|
|
export function getSceneTestTemplate(sceneCode: string) {
|
|
return request({ url: '/ai/scenes/test-template', method: 'get', params: { sceneCode } })
|
|
}
|
|
|
|
export function saveAiScene(data: AiSceneBinding) {
|
|
return request({ url: '/ai/scenes', method: data.id ? 'put' : 'post', data })
|
|
}
|
|
|
|
export function deleteAiScene(id: string) {
|
|
return request({ url: '/ai/scenes', method: 'delete', params: { id } })
|
|
}
|
|
|
|
export function listAiCallLogs(limit = 50) {
|
|
return request({ url: '/ai/call-logs', method: 'get', params: { limit } })
|
|
}
|
|
|
|
export function queryAiCallLogs(params: LogQueryParams) {
|
|
return request<PageResult<AiCallLog>>({
|
|
url: '/ai/call-logs',
|
|
method: 'post',
|
|
data: params
|
|
})
|
|
}
|
|
|
|
export function testAiRuntime(data: AiRuntimeRequest) {
|
|
return request({ url: '/ai/runtime/test', method: 'post', data, timeout: 60000 })
|
|
}
|
|
|
|
export interface AiRuntimeStreamEvent {
|
|
type: string
|
|
content?: string
|
|
code?: string
|
|
message?: string
|
|
seq?: number
|
|
metadata?: Record<string, any>
|
|
}
|
|
|
|
function parseSseFrame(frame: string): AiRuntimeStreamEvent | null {
|
|
const event = { type: 'message', data: '' }
|
|
frame.split(/\r?\n/).forEach((line) => {
|
|
if (line.startsWith('event:')) event.type = line.slice(6).trim()
|
|
if (line.startsWith('data:')) event.data += line.slice(5).trim()
|
|
})
|
|
if (!event.data) return null
|
|
try {
|
|
return JSON.parse(event.data)
|
|
} catch {
|
|
return { type: event.type, content: event.data }
|
|
}
|
|
}
|
|
|
|
export function normalizeAiText(value?: string): string {
|
|
if (!value) return ''
|
|
const trimmed = value.trim()
|
|
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return value
|
|
try {
|
|
const parsed = JSON.parse(trimmed)
|
|
const extracted = extractTextValue(parsed)
|
|
return extracted ? normalizeAiText(extracted) : value
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
|
|
function findOverlapLength(current: string, next: string) {
|
|
const max = Math.min(current.length, next.length)
|
|
for (let size = max; size > 0; size -= 1) {
|
|
if (current.slice(-size) === next.slice(0, size)) return size
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function mergeStreamOutput(current: string, chunk?: string) {
|
|
const next = normalizeAiText(chunk || '')
|
|
if (!next) return { output: current, delta: '' }
|
|
if (!current) return { output: next, delta: next }
|
|
if (next === current) return { output: current, delta: '' }
|
|
if (next.length >= 16 && current.endsWith(next)) return { output: current, delta: '' }
|
|
if (next.startsWith(current)) {
|
|
return { output: next, delta: next.slice(current.length) }
|
|
}
|
|
const currentIndex = next.length > current.length ? next.indexOf(current) : -1
|
|
if (currentIndex >= 0) {
|
|
return { output: next, delta: next.slice(currentIndex + current.length) }
|
|
}
|
|
const overlap = findOverlapLength(current, next)
|
|
if (overlap < 8) return { output: current + next, delta: next }
|
|
const delta = next.slice(overlap)
|
|
return { output: current + delta, delta }
|
|
}
|
|
|
|
function extractTextValue(value: any): string {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return ''
|
|
for (const key of ['output', 'answer', 'content', 'text', 'result']) {
|
|
const item = value[key]
|
|
if (typeof item === 'string' && item.trim()) return item
|
|
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
const nested = extractTextValue(item)
|
|
if (nested) return nested
|
|
}
|
|
}
|
|
for (const key of ['data', 'outputs', 'message']) {
|
|
const nested = extractTextValue(value[key])
|
|
if (nested) return nested
|
|
}
|
|
return ''
|
|
}
|
|
|
|
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 = ''
|
|
let recovered = false
|
|
|
|
const finishRecovered = (event: AiRuntimeStreamEvent, message?: string) => {
|
|
if (!output.trim()) return false
|
|
recovered = true
|
|
onEvent({
|
|
type: 'done',
|
|
metadata: {
|
|
...(event.metadata || {}),
|
|
recovered: true,
|
|
warningCode: event.code,
|
|
warningMessage: message || event.message
|
|
}
|
|
}, output)
|
|
return true
|
|
}
|
|
|
|
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 = mergeStreamOutput(output, event.content).output
|
|
}
|
|
onEvent(event, output)
|
|
if (event.type === 'error' && finishRecovered(event)) {
|
|
return
|
|
}
|
|
if (event.type === 'error') {
|
|
throw new Error(event.message || event.code || '流式测试失败')
|
|
}
|
|
})
|
|
}
|
|
|
|
try {
|
|
while (true) {
|
|
const { value, done } = await reader.read()
|
|
if (done) break
|
|
consumeText(decoder.decode(value, { stream: true }))
|
|
if (recovered) break
|
|
}
|
|
} catch (error: any) {
|
|
if (!finishRecovered({ type: 'error', message: error?.message })) {
|
|
throw error
|
|
}
|
|
}
|
|
if (recovered) return output
|
|
consumeText(decoder.decode())
|
|
if (buffer.trim()) consumeText('\n\n')
|
|
return output
|
|
}
|
|
|
|
export async function streamAiRuntime(
|
|
data: AiRuntimeRequest,
|
|
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
|
|
) {
|
|
return fetchSseStream('/ai/runtime/stream', data, onEvent)
|
|
}
|
|
|
|
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 })
|
|
}
|