feat: 优化
This commit is contained in:
@@ -16,9 +16,12 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/anthropic": "^3.0.15",
|
||||||
|
"@ai-sdk/openai": "^3.0.12",
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"ai": "^6.0.39",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
|||||||
142
frontend/app/web-gold/src/components/ChatMessageRendererV2.vue
Normal file
142
frontend/app/web-gold/src/components/ChatMessageRendererV2.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<!-- 主渲染区域 -->
|
||||||
|
<div
|
||||||
|
:class="{ 'streaming': isStreaming }"
|
||||||
|
>
|
||||||
|
<!-- 消息渲染器 - GitHub 风格 -->
|
||||||
|
<div
|
||||||
|
class="markdown-body"
|
||||||
|
:class="{ 'streaming': isStreaming }"
|
||||||
|
v-html="renderedContent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onUnmounted, onMounted } from 'vue'
|
||||||
|
import { renderMarkdown } from '@/utils/markdown'
|
||||||
|
import type { ChatMessageRendererV2Props } from '@/services/ai-bridge'
|
||||||
|
import 'github-markdown-css/github-markdown.css'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ChatMessageRendererV2Props>(), {
|
||||||
|
config: () => ({
|
||||||
|
enableTypewriter: true,
|
||||||
|
typewriterSpeed: 10,
|
||||||
|
enableMarkdown: true,
|
||||||
|
enableHighlighting: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'stream-start': []
|
||||||
|
'stream-chunk': [chunk: string]
|
||||||
|
'stream-end': [fullContent: string]
|
||||||
|
'error': [error: Error]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const internalContent = ref('')
|
||||||
|
const renderedContent = ref('')
|
||||||
|
const isPageVisible = ref(true)
|
||||||
|
|
||||||
|
watch(() => props.content, (newContent) => {
|
||||||
|
if (newContent !== undefined && newContent !== null) {
|
||||||
|
updateContent(newContent)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(() => props.isStreaming, (newValue, oldValue) => {
|
||||||
|
if (newValue && !oldValue) {
|
||||||
|
handleStreamStart()
|
||||||
|
} else if (!newValue && oldValue) {
|
||||||
|
handleStreamEnd()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function updateContent(newContent = '') {
|
||||||
|
if (props.isStreaming && internalContent.value) {
|
||||||
|
await updateStreamingContent(newContent)
|
||||||
|
} else {
|
||||||
|
await updateStaticContent(newContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStreamingContent(newFullContent: string) {
|
||||||
|
const prev = internalContent.value
|
||||||
|
|
||||||
|
let delta: string
|
||||||
|
if (newFullContent.startsWith(prev)) {
|
||||||
|
delta = newFullContent.slice(prev.length)
|
||||||
|
} else {
|
||||||
|
console.warn('[ChatMessageRendererV2] 流式内容乱序,使用完整内容')
|
||||||
|
delta = newFullContent
|
||||||
|
internalContent.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
internalContent.value += delta
|
||||||
|
emit('stream-chunk', delta)
|
||||||
|
await renderContent(internalContent.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStaticContent(newContent: string) {
|
||||||
|
internalContent.value = newContent
|
||||||
|
await renderContent(newContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderContent(content: string) {
|
||||||
|
if (!content) {
|
||||||
|
renderedContent.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (props.config?.enableMarkdown !== false) {
|
||||||
|
renderedContent.value = await renderMarkdown(content)
|
||||||
|
} else {
|
||||||
|
renderedContent.value = content.replace(/\n/g, '<br>')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ChatMessageRendererV2] 渲染错误:', error)
|
||||||
|
renderedContent.value = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStreamStart() {
|
||||||
|
internalContent.value = ''
|
||||||
|
renderedContent.value = ''
|
||||||
|
emit('stream-start')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStreamEnd() {
|
||||||
|
emit('stream-end', internalContent.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
const wasVisible = isPageVisible.value
|
||||||
|
isPageVisible.value = !document.hidden
|
||||||
|
|
||||||
|
if (!wasVisible && isPageVisible.value && props.content) {
|
||||||
|
updateContent(props.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
handleVisibilityChange()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reset: () => {
|
||||||
|
internalContent.value = ''
|
||||||
|
renderedContent.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
</style>
|
||||||
345
frontend/app/web-gold/src/services/ai-bridge/index.ts
Normal file
345
frontend/app/web-gold/src/services/ai-bridge/index.ts
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
/**
|
||||||
|
* AI 桥接服务 - 主入口
|
||||||
|
* 提供统一的 AI 服务接口,基于现有系统架构
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导入核心类和类型
|
||||||
|
export { StreamAdapter, createStreamAdapter, DEFAULT_STREAM_CONFIG } from './stream-adapter'
|
||||||
|
export * from './type-definitions'
|
||||||
|
|
||||||
|
// 导入现有聊天 API
|
||||||
|
import { ChatMessageApi } from '@/api/chat'
|
||||||
|
|
||||||
|
// 导入适配器类
|
||||||
|
import { StreamAdapter } from './stream-adapter'
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 配置和常量
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 服务配置
|
||||||
|
*/
|
||||||
|
const AI_SERVICE_CONFIG = {
|
||||||
|
// 默认配置
|
||||||
|
defaults: {
|
||||||
|
model: 'default',
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2000,
|
||||||
|
apiUrl: '/admin-api'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 功能开关
|
||||||
|
features: {
|
||||||
|
useCustomAI: import.meta.env.VITE_USE_CUSTOM_AI === 'true',
|
||||||
|
useBridgeService: !!import.meta.env.VITE_AI_BRIDGE_URL,
|
||||||
|
enableStreaming: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 核心服务类
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 聊天服务类
|
||||||
|
* 基于现有系统,提供流式渲染优化
|
||||||
|
*/
|
||||||
|
export class AIChatService {
|
||||||
|
private streamAdapter: StreamAdapter
|
||||||
|
private performanceMonitor: PerformanceMonitor
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.streamAdapter = new StreamAdapter()
|
||||||
|
this.performanceMonitor = new PerformanceMonitor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送流式聊天消息
|
||||||
|
*/
|
||||||
|
async sendStreamMessage(options: {
|
||||||
|
conversationId?: string
|
||||||
|
content: string
|
||||||
|
roleId?: number
|
||||||
|
useContext?: boolean
|
||||||
|
useSearch?: boolean
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
onUpdate?: (content: string) => void
|
||||||
|
onComplete?: (fullContent: string) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
conversationId,
|
||||||
|
content,
|
||||||
|
roleId = 20,
|
||||||
|
useContext = true,
|
||||||
|
useSearch = false,
|
||||||
|
attachmentUrls = [],
|
||||||
|
onUpdate,
|
||||||
|
onComplete,
|
||||||
|
onError
|
||||||
|
} = options
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.performanceMonitor.track('chat-start')
|
||||||
|
|
||||||
|
// 使用现有的流式 API
|
||||||
|
return await this.sendLegacyStreamMessage({
|
||||||
|
conversationId,
|
||||||
|
content,
|
||||||
|
roleId,
|
||||||
|
useContext,
|
||||||
|
useSearch,
|
||||||
|
attachmentUrls,
|
||||||
|
onUpdate,
|
||||||
|
onComplete,
|
||||||
|
onError
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.performanceMonitor.track('chat-error')
|
||||||
|
if (onError) {
|
||||||
|
onError(error as Error)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用现有系统发送流式消息
|
||||||
|
*/
|
||||||
|
private async sendLegacyStreamMessage(options: {
|
||||||
|
conversationId?: string
|
||||||
|
content: string
|
||||||
|
roleId?: number
|
||||||
|
useContext?: boolean
|
||||||
|
useSearch?: boolean
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
onUpdate?: (content: string) => void
|
||||||
|
onComplete?: (fullContent: string) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
conversationId,
|
||||||
|
content,
|
||||||
|
roleId,
|
||||||
|
useContext,
|
||||||
|
useSearch,
|
||||||
|
attachmentUrls,
|
||||||
|
onUpdate,
|
||||||
|
onComplete,
|
||||||
|
onError
|
||||||
|
} = options
|
||||||
|
|
||||||
|
// 如果没有 conversationId,先创建一个
|
||||||
|
let currentConversationId = conversationId
|
||||||
|
if (!currentConversationId) {
|
||||||
|
const createResp = await ChatMessageApi.createChatConversationMy({
|
||||||
|
roleId: roleId || 20
|
||||||
|
})
|
||||||
|
currentConversationId = typeof createResp.data === 'object'
|
||||||
|
? createResp.data.id
|
||||||
|
: createResp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentConversationId) {
|
||||||
|
throw new Error('创建对话失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用现有的流式 API
|
||||||
|
return await ChatMessageApi.sendChatMessageStream({
|
||||||
|
conversationId: currentConversationId,
|
||||||
|
content,
|
||||||
|
useContext,
|
||||||
|
useSearch,
|
||||||
|
attachmentUrls,
|
||||||
|
onMessage: (event: any) => {
|
||||||
|
try {
|
||||||
|
const dataStr = event?.data || ''
|
||||||
|
if (!dataStr) return
|
||||||
|
|
||||||
|
const { code, data: responseData, msg } = JSON.parse(dataStr)
|
||||||
|
if (code !== 0) {
|
||||||
|
console.warn('[AI Service] 对话异常:', msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 AI 回复内容
|
||||||
|
const piece = responseData?.receive?.content || ''
|
||||||
|
if (piece && onUpdate) {
|
||||||
|
onUpdate(piece)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[AI Service] 解析流数据异常:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
console.error('[AI Service] 流式请求错误:', err)
|
||||||
|
if (onError) {
|
||||||
|
onError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
console.log('[AI Service] 流式请求完成')
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete('完成')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取性能指标
|
||||||
|
*/
|
||||||
|
getPerformanceMetrics() {
|
||||||
|
return this.performanceMonitor.getMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置服务状态
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.streamAdapter.reset()
|
||||||
|
this.performanceMonitor.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能监控类
|
||||||
|
*/
|
||||||
|
class PerformanceMonitor {
|
||||||
|
private metrics = {
|
||||||
|
chatCount: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
totalLatency: 0,
|
||||||
|
averageLatency: 0,
|
||||||
|
startTime: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
track(event: string) {
|
||||||
|
if (event === 'chat-start') {
|
||||||
|
this.metrics.chatCount++
|
||||||
|
} else if (event === 'chat-error') {
|
||||||
|
this.metrics.errorCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackLatency(latency: number) {
|
||||||
|
this.metrics.totalLatency += latency
|
||||||
|
this.metrics.averageLatency = this.metrics.totalLatency / this.metrics.chatCount
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetrics() {
|
||||||
|
return {
|
||||||
|
...this.metrics,
|
||||||
|
uptime: Date.now() - this.metrics.startTime,
|
||||||
|
errorRate: this.metrics.chatCount > 0
|
||||||
|
? this.metrics.errorCount / this.metrics.chatCount
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.metrics = {
|
||||||
|
chatCount: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
totalLatency: 0,
|
||||||
|
averageLatency: 0,
|
||||||
|
startTime: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 工具函数
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 AI 聊天服务实例
|
||||||
|
*/
|
||||||
|
export function createAIChatService(): AIChatService {
|
||||||
|
return new AIChatService()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 AI 服务是否可用
|
||||||
|
*/
|
||||||
|
export function isAIServiceEnabled(): boolean {
|
||||||
|
return AI_SERVICE_CONFIG.features.enableStreaming
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 AI 服务状态
|
||||||
|
*/
|
||||||
|
export function getAIServiceStatus() {
|
||||||
|
return {
|
||||||
|
customAIEnabled: AI_SERVICE_CONFIG.features.useCustomAI,
|
||||||
|
bridgeServiceAvailable: AI_SERVICE_CONFIG.features.useBridgeService,
|
||||||
|
streamingEnabled: AI_SERVICE_CONFIG.features.enableStreaming,
|
||||||
|
defaultModel: AI_SERVICE_CONFIG.defaults.model,
|
||||||
|
uptime: Date.now() - (globalThis as any).__AI_SERVICE_START_TIME || Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Vue 组合式函数
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue 组合式函数:使用 AI 聊天服务
|
||||||
|
*/
|
||||||
|
export function useAIChat() {
|
||||||
|
const service = createAIChatService()
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
isEnabled: isAIServiceEnabled(),
|
||||||
|
status: getAIServiceStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue 组合式函数:流式消息处理
|
||||||
|
*/
|
||||||
|
export function useStreamMessage() {
|
||||||
|
const { service, isEnabled } = useAIChat()
|
||||||
|
|
||||||
|
const sendMessage = async (options: {
|
||||||
|
conversationId?: string
|
||||||
|
content: string
|
||||||
|
roleId?: number
|
||||||
|
onUpdate?: (content: string) => void
|
||||||
|
onComplete?: (fullContent: string) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}) => {
|
||||||
|
return await service.sendStreamMessage(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendMessage,
|
||||||
|
isEnabled,
|
||||||
|
service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 全局实例
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// 创建全局 AI 服务实例
|
||||||
|
export const aiChatService = createAIChatService()
|
||||||
|
|
||||||
|
// 设置启动时间
|
||||||
|
;(globalThis as any).__AI_SERVICE_START_TIME = Date.now()
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 导出默认实例和工具函数
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export default {
|
||||||
|
service: aiChatService,
|
||||||
|
createService: createAIChatService,
|
||||||
|
isEnabled: isAIServiceEnabled,
|
||||||
|
status: getAIServiceStatus,
|
||||||
|
useChat: useAIChat,
|
||||||
|
useStreamMessage
|
||||||
|
}
|
||||||
285
frontend/app/web-gold/src/services/ai-bridge/stream-adapter.ts
Normal file
285
frontend/app/web-gold/src/services/ai-bridge/stream-adapter.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* AI 桥接服务 - 流式适配器
|
||||||
|
* 负责 SSE 到 AI SDK 协议转换,保持与现有系统的兼容性
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ReadableStream } from 'stream/web'
|
||||||
|
|
||||||
|
// SSE 事件类型
|
||||||
|
interface SSEEvent {
|
||||||
|
data: string
|
||||||
|
event?: string
|
||||||
|
id?: string
|
||||||
|
retry?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流式数据处理配置
|
||||||
|
interface StreamAdapterConfig {
|
||||||
|
enableVisibilityOptimization?: boolean
|
||||||
|
enablePerformanceTracking?: boolean
|
||||||
|
bufferSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式适配器类
|
||||||
|
* 将 SSE 响应转换为 AI SDK 兼容的格式
|
||||||
|
*/
|
||||||
|
export class StreamAdapter {
|
||||||
|
private buffer: string = ''
|
||||||
|
private isVisible: boolean = true
|
||||||
|
private config: StreamAdapterConfig
|
||||||
|
private performanceMetrics: {
|
||||||
|
chunkCount: number
|
||||||
|
totalBytes: number
|
||||||
|
startTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(config: StreamAdapterConfig = {}) {
|
||||||
|
this.config = {
|
||||||
|
enableVisibilityOptimization: true,
|
||||||
|
enablePerformanceTracking: true,
|
||||||
|
bufferSize: 1024,
|
||||||
|
...config
|
||||||
|
}
|
||||||
|
this.performanceMetrics = {
|
||||||
|
chunkCount: 0,
|
||||||
|
totalBytes: 0,
|
||||||
|
startTime: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听页面可见性变化
|
||||||
|
if (this.config.enableVisibilityOptimization) {
|
||||||
|
this.setupVisibilityListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置页面可见性监听器
|
||||||
|
*/
|
||||||
|
private setupVisibilityListener(): void {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
const wasVisible = this.isVisible
|
||||||
|
this.isVisible = !document.hidden
|
||||||
|
|
||||||
|
if (!wasVisible && this.isVisible) {
|
||||||
|
console.log('[AI Bridge] 页面重新可见,重新连接流')
|
||||||
|
} else if (wasVisible && !this.isVisible) {
|
||||||
|
console.log('[AI Bridge] 页面进入后台,优化性能')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 SSE ReadableStream 转换为 AI SDK 可消费的异步迭代器
|
||||||
|
*/
|
||||||
|
async *convertSSEResponse(sseStream: ReadableStream): AsyncIterable<string> {
|
||||||
|
console.log('[AI Bridge] 开始转换 SSE 响应')
|
||||||
|
|
||||||
|
const reader = sseStream.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let isFirstChunk = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true })
|
||||||
|
this.buffer += chunk
|
||||||
|
this.performanceMetrics.totalBytes += chunk.length
|
||||||
|
|
||||||
|
// 提取完整的 SSE 事件(以 \n\n 分隔)
|
||||||
|
const events = this.buffer.split('\n\n')
|
||||||
|
this.buffer = events.pop() || '' // 保存不完整的事件
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.trim()) {
|
||||||
|
const parsedEvent = this.parseSSEEvent(event)
|
||||||
|
if (parsedEvent && this.shouldProcessEvent(parsedEvent)) {
|
||||||
|
const content = this.extractContent(parsedEvent)
|
||||||
|
if (content) {
|
||||||
|
yield content
|
||||||
|
this.performanceMetrics.chunkCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一块数据特殊处理
|
||||||
|
if (isFirstChunk) {
|
||||||
|
console.log('[AI Bridge] 接收到第一块数据')
|
||||||
|
isFirstChunk = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理缓冲区中剩余的数据
|
||||||
|
if (this.buffer.trim()) {
|
||||||
|
const lastEvent = this.parseSSEEvent(this.buffer)
|
||||||
|
if (lastEvent) {
|
||||||
|
const content = this.extractContent(lastEvent)
|
||||||
|
if (content) {
|
||||||
|
yield content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AI Bridge] SSE 转换错误:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock()
|
||||||
|
this.logPerformanceMetrics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 SSE 事件
|
||||||
|
*/
|
||||||
|
private parseSSEEvent(event: string): SSEEvent | null {
|
||||||
|
const lines = event.split('\n')
|
||||||
|
const parsedEvent: SSEEvent = {
|
||||||
|
data: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
parsedEvent.data = line.substring(6)
|
||||||
|
} else if (line.startsWith('event: ')) {
|
||||||
|
parsedEvent.event = line.substring(7)
|
||||||
|
} else if (line.startsWith('id: ')) {
|
||||||
|
parsedEvent.id = line.substring(4)
|
||||||
|
} else if (line.startsWith('retry: ')) {
|
||||||
|
parsedEvent.retry = parseInt(line.substring(8), 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedEvent.data ? parsedEvent : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否应该处理此事件
|
||||||
|
*/
|
||||||
|
private shouldProcessEvent(event: SSEEvent): boolean {
|
||||||
|
// 页面不可见时跳过非关键事件
|
||||||
|
if (!this.isVisible && this.config.enableVisibilityOptimization) {
|
||||||
|
return event.event !== 'ping'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 SSE 事件中提取内容
|
||||||
|
*/
|
||||||
|
private extractContent(event: SSEEvent): string | null {
|
||||||
|
try {
|
||||||
|
// 尝试解析 JSON 格式
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (data.content) {
|
||||||
|
return data.content
|
||||||
|
} else if (data.text) {
|
||||||
|
return data.text
|
||||||
|
} else if (data.message?.content) {
|
||||||
|
return data.message.content
|
||||||
|
} else if (data.receive?.content) {
|
||||||
|
return data.receive.content
|
||||||
|
} else if (data.delta) {
|
||||||
|
return data.delta
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是纯文本数据
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch {
|
||||||
|
// 非 JSON 数据直接返回
|
||||||
|
return event.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理流式增量数据
|
||||||
|
*/
|
||||||
|
processStreamingDelta(newFullContent: string, previousContent: string = ''): string {
|
||||||
|
if (newFullContent.startsWith(previousContent)) {
|
||||||
|
// 正常情况:增量更新
|
||||||
|
return newFullContent.slice(previousContent.length)
|
||||||
|
} else {
|
||||||
|
// 异常情况:内容乱序,返回完整内容
|
||||||
|
console.warn('[AI Bridge] 流式内容乱序,使用完整内容')
|
||||||
|
return newFullContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取性能指标
|
||||||
|
*/
|
||||||
|
getPerformanceMetrics() {
|
||||||
|
const duration = Date.now() - this.performanceMetrics.startTime
|
||||||
|
return {
|
||||||
|
...this.performanceMetrics,
|
||||||
|
duration,
|
||||||
|
averageChunkSize: this.performanceMetrics.chunkCount > 0
|
||||||
|
? this.performanceMetrics.totalBytes / this.performanceMetrics.chunkCount
|
||||||
|
: 0,
|
||||||
|
chunksPerSecond: this.performanceMetrics.chunkCount / (duration / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录性能指标
|
||||||
|
*/
|
||||||
|
private logPerformanceMetrics(): void {
|
||||||
|
if (this.config.enablePerformanceTracking) {
|
||||||
|
const metrics = this.getPerformanceMetrics()
|
||||||
|
console.log('[AI Bridge] 性能指标:', {
|
||||||
|
总字节数: metrics.totalBytes,
|
||||||
|
数据块数: metrics.chunkCount,
|
||||||
|
持续时间: `${metrics.duration}ms`,
|
||||||
|
平均块大小: `${Math.round(metrics.averageChunkSize)} bytes`,
|
||||||
|
处理速度: `${metrics.chunksPerSecond.toFixed(2)} chunks/s`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置适配器状态
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.buffer = ''
|
||||||
|
this.performanceMetrics = {
|
||||||
|
chunkCount: 0,
|
||||||
|
totalBytes: 0,
|
||||||
|
startTime: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
dispose(): void {
|
||||||
|
this.buffer = ''
|
||||||
|
this.reset()
|
||||||
|
// 移除事件监听器(在实际实现中需要保存引用以便移除)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建流式适配器实例的工厂函数
|
||||||
|
*/
|
||||||
|
export function createStreamAdapter(config?: StreamAdapterConfig): StreamAdapter {
|
||||||
|
return new StreamAdapter(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_STREAM_CONFIG: StreamAdapterConfig = {
|
||||||
|
enableVisibilityOptimization: true,
|
||||||
|
enablePerformanceTracking: true,
|
||||||
|
bufferSize: 1024
|
||||||
|
}
|
||||||
497
frontend/app/web-gold/src/services/ai-bridge/type-definitions.ts
Normal file
497
frontend/app/web-gold/src/services/ai-bridge/type-definitions.ts
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
/**
|
||||||
|
* AI 桥接服务 - 类型定义系统
|
||||||
|
* 提供完整的 TypeScript 类型定义,确保端到端类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 基础类型定义
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天消息基础类型
|
||||||
|
*/
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
role: 'user' | 'assistant' | 'system'
|
||||||
|
timestamp: Date
|
||||||
|
metadata?: {
|
||||||
|
isStreaming?: boolean
|
||||||
|
tokens?: number
|
||||||
|
model?: string
|
||||||
|
provider?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式数据块
|
||||||
|
*/
|
||||||
|
export interface StreamChunk {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
delta?: string
|
||||||
|
isComplete: boolean
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 事件格式
|
||||||
|
*/
|
||||||
|
export interface SSEEvent {
|
||||||
|
data: string
|
||||||
|
event?: string
|
||||||
|
id?: string
|
||||||
|
retry?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// AI SDK 集成类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 提供商枚举
|
||||||
|
*/
|
||||||
|
export type AIProvider = 'openai' | 'anthropic' | 'custom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 模型配置
|
||||||
|
*/
|
||||||
|
export interface AIModelConfig {
|
||||||
|
provider: AIProvider
|
||||||
|
model: string
|
||||||
|
apiKey?: string
|
||||||
|
baseURL?: string
|
||||||
|
parameters?: {
|
||||||
|
temperature?: number
|
||||||
|
maxTokens?: number
|
||||||
|
topP?: number
|
||||||
|
frequencyPenalty?: number
|
||||||
|
presencePenalty?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI SDK 流式配置
|
||||||
|
*/
|
||||||
|
export interface AIStreamConfig {
|
||||||
|
model: AIModelConfig
|
||||||
|
streamMode: 'text' | 'full' | 'object'
|
||||||
|
enableStreaming?: boolean
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI SDK 响应格式
|
||||||
|
*/
|
||||||
|
export interface AIResponse {
|
||||||
|
id: string
|
||||||
|
type: 'message' | 'tool-call' | 'error' | 'done'
|
||||||
|
content?: string
|
||||||
|
toolCalls?: ToolCall[]
|
||||||
|
error?: {
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
usage?: {
|
||||||
|
promptTokens: number
|
||||||
|
completionTokens: number
|
||||||
|
totalTokens: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具调用类型
|
||||||
|
*/
|
||||||
|
export interface ToolCall {
|
||||||
|
id: string
|
||||||
|
type: 'function'
|
||||||
|
function: {
|
||||||
|
name: string
|
||||||
|
arguments: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 流式处理类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式处理器配置
|
||||||
|
*/
|
||||||
|
export interface StreamProcessorConfig {
|
||||||
|
enableVisibilityOptimization: boolean
|
||||||
|
enablePerformanceTracking: boolean
|
||||||
|
bufferSize: number
|
||||||
|
chunkSize: number
|
||||||
|
enableRetry: boolean
|
||||||
|
retryAttempts: number
|
||||||
|
retryDelay: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式处理器状态
|
||||||
|
*/
|
||||||
|
export interface StreamProcessorState {
|
||||||
|
status: 'idle' | 'connecting' | 'streaming' | 'paused' | 'completed' | 'error'
|
||||||
|
bytesReceived: number
|
||||||
|
chunksReceived: number
|
||||||
|
startTime: Date
|
||||||
|
lastChunkTime: Date
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式事件类型
|
||||||
|
*/
|
||||||
|
export type StreamEventType =
|
||||||
|
| 'stream-start'
|
||||||
|
| 'stream-chunk'
|
||||||
|
| 'stream-end'
|
||||||
|
| 'stream-error'
|
||||||
|
| 'stream-pause'
|
||||||
|
| 'stream-resume'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式事件
|
||||||
|
*/
|
||||||
|
export interface StreamEvent<T = any> {
|
||||||
|
type: StreamEventType
|
||||||
|
data: T
|
||||||
|
timestamp: Date
|
||||||
|
requestId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 性能监控类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能指标
|
||||||
|
*/
|
||||||
|
export interface PerformanceMetrics {
|
||||||
|
// 渲染性能
|
||||||
|
renderTime: number
|
||||||
|
firstRenderTime: number
|
||||||
|
incrementalRenderTime: number
|
||||||
|
|
||||||
|
// 流式性能
|
||||||
|
streamLatency: number
|
||||||
|
chunkProcessingTime: number
|
||||||
|
bytesPerSecond: number
|
||||||
|
|
||||||
|
// 内存使用
|
||||||
|
memoryUsage: number
|
||||||
|
memoryPeak: number
|
||||||
|
|
||||||
|
// 错误统计
|
||||||
|
errorCount: number
|
||||||
|
retryCount: number
|
||||||
|
|
||||||
|
// 成功率
|
||||||
|
successRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能报告
|
||||||
|
*/
|
||||||
|
export interface PerformanceReport {
|
||||||
|
metrics: PerformanceMetrics
|
||||||
|
timestamp: Date
|
||||||
|
sessionId: string
|
||||||
|
duration: number
|
||||||
|
summary: {
|
||||||
|
performanceScore: number
|
||||||
|
issues: string[]
|
||||||
|
recommendations: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 适配器类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 适配器接口
|
||||||
|
*/
|
||||||
|
export interface StreamAdapterInterface {
|
||||||
|
convertSSEResponse(sseStream: ReadableStream): AsyncIterable<string>
|
||||||
|
processStreamingDelta(newContent: string, previousContent?: string): string
|
||||||
|
getPerformanceMetrics(): any
|
||||||
|
reset(): void
|
||||||
|
dispose(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 适配器配置
|
||||||
|
*/
|
||||||
|
export interface AdapterConfig {
|
||||||
|
enableVisibilityOptimization: boolean
|
||||||
|
enablePerformanceTracking: boolean
|
||||||
|
bufferSize: number
|
||||||
|
chunkSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 组件 Props 类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatMessageRenderer 组件 Props
|
||||||
|
*/
|
||||||
|
export interface ChatMessageRendererProps {
|
||||||
|
content: string
|
||||||
|
isStreaming: boolean
|
||||||
|
config?: {
|
||||||
|
enableTypewriter?: boolean
|
||||||
|
typewriterSpeed?: number
|
||||||
|
enableMarkdown?: boolean
|
||||||
|
enableHighlighting?: boolean
|
||||||
|
}
|
||||||
|
onStreamStart?: () => void
|
||||||
|
onStreamChunk?: (chunk: string) => void
|
||||||
|
onStreamEnd?: (fullContent: string) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatMessageRendererV2 组件 Props
|
||||||
|
*/
|
||||||
|
export interface ChatMessageRendererV2Props extends ChatMessageRendererProps {
|
||||||
|
aiConfig?: AIStreamConfig
|
||||||
|
adapterConfig?: AdapterConfig
|
||||||
|
enableAIIntegration?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// API 类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天请求类型
|
||||||
|
*/
|
||||||
|
export interface ChatRequest {
|
||||||
|
conversationId?: string
|
||||||
|
content: string
|
||||||
|
roleId?: number
|
||||||
|
useContext?: boolean
|
||||||
|
useSearch?: boolean
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
config?: {
|
||||||
|
enableStreaming?: boolean
|
||||||
|
enableAI?: boolean
|
||||||
|
model?: AIModelConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天响应类型
|
||||||
|
*/
|
||||||
|
export interface ChatResponse {
|
||||||
|
id: string
|
||||||
|
conversationId: string
|
||||||
|
send?: {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
receive?: {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
reasoningContent?: string
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
metadata?: {
|
||||||
|
model?: string
|
||||||
|
provider?: string
|
||||||
|
tokens?: number
|
||||||
|
processingTime?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 配置类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局 AI 配置
|
||||||
|
*/
|
||||||
|
export interface AIGlobalConfig {
|
||||||
|
enabled: boolean
|
||||||
|
defaultProvider: AIProvider
|
||||||
|
defaultModel: string
|
||||||
|
streamConfig: StreamProcessorConfig
|
||||||
|
performanceConfig: {
|
||||||
|
enabled: boolean
|
||||||
|
samplingRate: number
|
||||||
|
reportInterval: number
|
||||||
|
}
|
||||||
|
featureFlags: {
|
||||||
|
enableVercelSDK: boolean
|
||||||
|
enableStreamingV2: boolean
|
||||||
|
enablePerformanceMonitoring: boolean
|
||||||
|
enableAdvancedFeatures: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境变量类型
|
||||||
|
*/
|
||||||
|
export interface EnvironmentConfig {
|
||||||
|
VITE_AI_SDK_ENABLED: string
|
||||||
|
VITE_AI_BRIDGE_URL: string
|
||||||
|
VITE_AI_DEFAULT_PROVIDER: AIProvider
|
||||||
|
VITE_AI_DEFAULT_MODEL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 工具类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具调用结果
|
||||||
|
*/
|
||||||
|
export interface ToolResult {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
result: any
|
||||||
|
error?: string
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具定义
|
||||||
|
*/
|
||||||
|
export interface ToolDefinition {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
parameters: {
|
||||||
|
type: 'object'
|
||||||
|
properties: Record<string, any>
|
||||||
|
required?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 错误类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 桥接服务错误
|
||||||
|
*/
|
||||||
|
export class AIBridgeError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public cause?: Error,
|
||||||
|
public context?: Record<string, any>
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'AIBridgeError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流处理错误
|
||||||
|
*/
|
||||||
|
export class StreamProcessingError extends AIBridgeError {
|
||||||
|
constructor(message: string, public streamId: string, cause?: Error) {
|
||||||
|
super(message, 'STREAM_PROCESSING_ERROR', cause, { streamId })
|
||||||
|
this.name = 'StreamProcessingError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置错误
|
||||||
|
*/
|
||||||
|
export class ConfigError extends AIBridgeError {
|
||||||
|
constructor(message: string, public configKey: string, public configValue: any) {
|
||||||
|
super(message, 'CONFIG_ERROR', undefined, { configKey, configValue })
|
||||||
|
this.name = 'ConfigError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 工具函数类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型守卫函数
|
||||||
|
*/
|
||||||
|
export type TypeGuard<T> = (value: any) => value is T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步处理器
|
||||||
|
*/
|
||||||
|
export type AsyncHandler<T = void> = (data?: T) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步处理器
|
||||||
|
*/
|
||||||
|
export type Handler<T = void> = (data?: T) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件监听器
|
||||||
|
*/
|
||||||
|
export type EventListener<T = any> = (event: StreamEvent<T>) => void
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 导出工具类型
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从联合类型中排除指定类型
|
||||||
|
*/
|
||||||
|
export type ExcludeType<T, U> = T extends U ? never : T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 必需属性类型
|
||||||
|
*/
|
||||||
|
export type RequiredKeys<T> = {
|
||||||
|
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可选属性类型
|
||||||
|
*/
|
||||||
|
export type OptionalKeys<T> = {
|
||||||
|
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 默认值导出
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认 AI 配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_AI_CONFIG: AIModelConfig = {
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认流处理配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_STREAM_CONFIG: StreamProcessorConfig = {
|
||||||
|
enableVisibilityOptimization: true,
|
||||||
|
enablePerformanceTracking: true,
|
||||||
|
bufferSize: 1024,
|
||||||
|
chunkSize: 64,
|
||||||
|
enableRetry: true,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认性能配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_PERFORMANCE_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
samplingRate: 0.1, // 10% 采样率
|
||||||
|
reportInterval: 5000 // 5秒报告一次
|
||||||
|
}
|
||||||
@@ -15,21 +15,19 @@ import BenchmarkTable from './components/BenchmarkTable.vue'
|
|||||||
import BatchAnalyzeModal from './components/BatchAnalyzeModal.vue'
|
import BatchAnalyzeModal from './components/BatchAnalyzeModal.vue'
|
||||||
import SavePromptModal from './components/SavePromptModal.vue'
|
import SavePromptModal from './components/SavePromptModal.vue'
|
||||||
|
|
||||||
// ==================== 初始化 ====================
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const promptStore = usePromptStore()
|
const promptStore = usePromptStore()
|
||||||
|
|
||||||
// ==================== 数据管理 ====================
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
|
expandedRowKeys,
|
||||||
saveTableDataToSession,
|
saveTableDataToSession,
|
||||||
loadTableDataFromSession,
|
loadTableDataFromSession,
|
||||||
processApiResponse,
|
processApiResponse,
|
||||||
clearData,
|
clearData,
|
||||||
} = useBenchmarkData()
|
} = useBenchmarkData()
|
||||||
|
|
||||||
// ==================== 分析功能 ====================
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
batchAnalyzeLoading,
|
batchAnalyzeLoading,
|
||||||
@@ -37,9 +35,8 @@ const {
|
|||||||
globalLoadingText,
|
globalLoadingText,
|
||||||
batchAnalyze,
|
batchAnalyze,
|
||||||
getVoiceText,
|
getVoiceText,
|
||||||
} = useBenchmarkAnalysis(data, saveTableDataToSession)
|
} = useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSession)
|
||||||
|
|
||||||
// ==================== 表单状态 ====================
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
platform: '抖音',
|
platform: '抖音',
|
||||||
url: '',
|
url: '',
|
||||||
@@ -47,7 +44,6 @@ const form = ref({
|
|||||||
sort_type: 0,
|
sort_type: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// ==================== 弹窗状态 ====================
|
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const batchPromptMergedText = ref('')
|
const batchPromptMergedText = ref('')
|
||||||
const batchPromptTextCount = ref(0)
|
const batchPromptTextCount = ref(0)
|
||||||
@@ -55,10 +51,6 @@ const batchPromptTextCount = ref(0)
|
|||||||
const savePromptModalVisible = ref(false)
|
const savePromptModalVisible = ref(false)
|
||||||
const savePromptContent = ref('')
|
const savePromptContent = ref('')
|
||||||
|
|
||||||
// ==================== API 调用函数 ====================
|
|
||||||
/**
|
|
||||||
* 分析用户主页,获取视频列表
|
|
||||||
*/
|
|
||||||
async function handleAnalyzeUser() {
|
async function handleAnalyzeUser() {
|
||||||
const sec_user_id = resolveId(form.value.url, {
|
const sec_user_id = resolveId(form.value.url, {
|
||||||
queryKeys: ['user'],
|
queryKeys: ['user'],
|
||||||
@@ -94,16 +86,13 @@ async function handleAnalyzeUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出数据到 Excel
|
|
||||||
*/
|
|
||||||
async function handleExportToExcel() {
|
async function handleExportToExcel() {
|
||||||
if (!data.value || data.value.length === 0) {
|
if (!data.value?.length) {
|
||||||
message.warning('暂无数据可导出')
|
message.warning('暂无数据可导出')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedRowKeys.value.length === 0) {
|
if (!selectedRowKeys.value.length) {
|
||||||
message.warning('请先选择要导出的行')
|
message.warning('请先选择要导出的行')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -116,15 +105,13 @@ async function handleExportToExcel() {
|
|||||||
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
|
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
|
||||||
const rowsNeedTranscription = selectedRows.filter(row => !row.transcriptions)
|
const rowsNeedTranscription = selectedRows.filter(row => !row.transcriptions)
|
||||||
|
|
||||||
// 导出时只获取语音转写,不进行 AI 对话分析
|
if (rowsNeedTranscription.length) {
|
||||||
if (rowsNeedTranscription.length > 0) {
|
|
||||||
globalLoading.value = true
|
globalLoading.value = true
|
||||||
globalLoadingText.value = `正在分析中...`
|
globalLoadingText.value = '正在分析中...'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transcriptions = await getVoiceText(rowsNeedTranscription)
|
const transcriptions = await getVoiceText(rowsNeedTranscription)
|
||||||
|
|
||||||
// 更新转写数据
|
|
||||||
for (const row of rowsNeedTranscription) {
|
for (const row of rowsNeedTranscription) {
|
||||||
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
|
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
|
||||||
if (transcription) {
|
if (transcription) {
|
||||||
@@ -163,9 +150,6 @@ async function handleExportToExcel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量分析处理
|
|
||||||
*/
|
|
||||||
async function handleBatchAnalyze() {
|
async function handleBatchAnalyze() {
|
||||||
try {
|
try {
|
||||||
await batchAnalyze(selectedRowKeys, async (mergedText, textCount) => {
|
await batchAnalyze(selectedRowKeys, async (mergedText, textCount) => {
|
||||||
@@ -174,22 +158,17 @@ async function handleBatchAnalyze() {
|
|||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
// 批量分析完成后清空选中项(无论成功还是失败)
|
|
||||||
selectedRowKeys.value = []
|
selectedRowKeys.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置表单
|
|
||||||
*/
|
|
||||||
async function handleResetForm() {
|
async function handleResetForm() {
|
||||||
form.value = { platform: '抖音', url: '', count: 20, sort_type: 0 }
|
form.value = { platform: '抖音', url: '', count: 20, sort_type: 0 }
|
||||||
await clearData()
|
await clearData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 批量提示词操作函数 ====================
|
|
||||||
function handleCopyBatchPrompt(prompt) {
|
function handleCopyBatchPrompt(prompt) {
|
||||||
if (!prompt || !prompt.trim()) {
|
if (!prompt?.trim()) {
|
||||||
message.warning('没有提示词可复制')
|
message.warning('没有提示词可复制')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,7 +181,7 @@ function handleCopyBatchPrompt(prompt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleUseBatchPrompt(prompt) {
|
function handleUseBatchPrompt(prompt) {
|
||||||
if (!prompt || !prompt.trim()) {
|
if (!prompt?.trim()) {
|
||||||
message.warning('暂无批量生成的提示词')
|
message.warning('暂无批量生成的提示词')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -211,11 +190,9 @@ function handleUseBatchPrompt(prompt) {
|
|||||||
router.push('/content-style/copywriting')
|
router.push('/content-style/copywriting')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 保存提示词到服务器 ====================
|
|
||||||
function handleOpenSavePromptModal(batchPrompt = null) {
|
function handleOpenSavePromptModal(batchPrompt = null) {
|
||||||
// 批量提示词:使用传入的 batchPrompt(AI 生成的内容),而不是原始的 mergedText
|
|
||||||
const promptToSave = batchPrompt || batchPromptMergedText.value
|
const promptToSave = batchPrompt || batchPromptMergedText.value
|
||||||
if (!promptToSave || !promptToSave.trim()) {
|
if (!promptToSave?.trim()) {
|
||||||
message.warning('没有提示词可保存')
|
message.warning('没有提示词可保存')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,7 +200,6 @@ function handleOpenSavePromptModal(batchPrompt = null) {
|
|||||||
savePromptModalVisible.value = true
|
savePromptModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 生命周期 ====================
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadTableDataFromSession()
|
await loadTableDataFromSession()
|
||||||
})
|
})
|
||||||
@@ -252,8 +228,7 @@ defineOptions({ name: 'ContentStyleBenchmark' })
|
|||||||
@batch-analyze="handleBatchAnalyze"
|
@batch-analyze="handleBatchAnalyze"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 空态显示 -->
|
<section v-if="!data.length && !loading" class="card results-card empty-state">
|
||||||
<section class="card results-card empty-state" v-if="data.length === 0 && !loading">
|
|
||||||
<a-empty description="暂无数据,请点击开始分析">
|
<a-empty description="暂无数据,请点击开始分析">
|
||||||
<template #image>
|
<template #image>
|
||||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@@ -1,41 +1,44 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { EditOutlined, CopyOutlined } from '@ant-design/icons-vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import ChatMessageRenderer from '@/components/ChatMessageRenderer.vue'
|
import ChatMessageRendererV2 from '@/components/ChatMessageRendererV2.vue'
|
||||||
import { ChatMessageApi } from '@/api/chat'
|
import { ChatMessageApi } from '@/api/chat'
|
||||||
import { streamChat } from '@/utils/streamChat'
|
import { streamChat } from '@/utils/streamChat'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(defineProps<{
|
||||||
visible: {
|
visible: boolean
|
||||||
type: Boolean,
|
mergedText: string
|
||||||
default: false,
|
textCount: number
|
||||||
},
|
}>(), {
|
||||||
mergedText: {
|
visible: false,
|
||||||
type: String,
|
mergedText: '',
|
||||||
default: '',
|
textCount: 0,
|
||||||
},
|
|
||||||
textCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:visible', 'copy', 'save', 'use'])
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean]
|
||||||
|
'copy': [text: string]
|
||||||
|
'save': [text: string]
|
||||||
|
'use': [text: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
const batchPrompt = ref('')
|
const batchPrompt = ref('')
|
||||||
const batchPromptEditMode = ref(false)
|
const batchPromptEditMode = ref(false)
|
||||||
const batchPromptGenerating = ref(false)
|
const batchPromptGenerating = ref(false)
|
||||||
const hasGenerated = ref(false)
|
const hasGenerated = ref(false)
|
||||||
|
|
||||||
|
function resetModal() {
|
||||||
|
batchPrompt.value = ''
|
||||||
|
batchPromptEditMode.value = false
|
||||||
|
batchPromptGenerating.value = false
|
||||||
|
hasGenerated.value = false
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (newVal && props.mergedText && !hasGenerated.value) {
|
if (newVal && props.mergedText && !hasGenerated.value) {
|
||||||
generateBatchPrompt()
|
generateBatchPrompt()
|
||||||
} else if (!newVal) {
|
} else if (!newVal) {
|
||||||
batchPrompt.value = ''
|
resetModal()
|
||||||
batchPromptEditMode.value = false
|
|
||||||
batchPromptGenerating.value = false
|
|
||||||
hasGenerated.value = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,47 +50,61 @@ watch(() => props.mergedText, (newVal) => {
|
|||||||
|
|
||||||
async function generateBatchPrompt() {
|
async function generateBatchPrompt() {
|
||||||
if (!props.mergedText || hasGenerated.value) return
|
if (!props.mergedText || hasGenerated.value) return
|
||||||
|
|
||||||
hasGenerated.value = true
|
hasGenerated.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
batchPromptGenerating.value = true
|
batchPromptGenerating.value = true
|
||||||
const createPayload = { roleId: 20 }
|
const conversationId = await createConversation()
|
||||||
console.debug('createChatConversationMy payload(batch):', createPayload)
|
|
||||||
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
|
|
||||||
|
|
||||||
let conversationId = null
|
|
||||||
if (conversationResp?.data) {
|
|
||||||
conversationId = typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conversationId) {
|
|
||||||
throw new Error('创建对话失败:未获取到 conversationId')
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiContent = await streamChat({
|
const aiContent = await streamChat({
|
||||||
conversationId,
|
conversationId,
|
||||||
content: props.mergedText,
|
content: props.mergedText,
|
||||||
onUpdate: (fullText) => {
|
onUpdate: (fullText: string) => {
|
||||||
batchPrompt.value = fullText
|
batchPrompt.value = fullText
|
||||||
},
|
},
|
||||||
enableTypewriter: true,
|
enableTypewriter: true,
|
||||||
typewriterSpeed: 10,
|
typewriterSpeed: 10,
|
||||||
typewriterBatchSize: 2
|
typewriterBatchSize: 2,
|
||||||
|
onComplete: () => {},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
console.error('流式聊天错误:', error)
|
||||||
|
},
|
||||||
|
enableContext: false,
|
||||||
|
enableWebSearch: false,
|
||||||
|
timeout: 180000,
|
||||||
|
attachmentUrls: []
|
||||||
})
|
})
|
||||||
|
|
||||||
if (aiContent && aiContent !== batchPrompt.value) {
|
if (aiContent && aiContent !== batchPrompt.value) {
|
||||||
batchPrompt.value = aiContent
|
batchPrompt.value = aiContent
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success(`批量分析完成:已基于 ${props.textCount} 个视频的文案生成综合提示词`)
|
message.success(`批量分析完成:已基于 ${props.textCount} 个视频的文案生成综合提示词`)
|
||||||
} catch (aiError) {
|
} catch (error) {
|
||||||
console.error('AI生成失败:', aiError)
|
console.error('AI生成失败:', error)
|
||||||
message.error('AI生成失败,请稍后重试')
|
message.error('AI生成失败,请稍后重试')
|
||||||
} finally {
|
} finally {
|
||||||
batchPromptGenerating.value = false
|
batchPromptGenerating.value = false
|
||||||
|
hasGenerated.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createConversation() {
|
||||||
|
const createPayload = { roleId: 20 }
|
||||||
|
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
|
||||||
|
|
||||||
|
const conversationId = conversationResp?.data
|
||||||
|
? (typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!conversationId) {
|
||||||
|
throw new Error('创建对话失败:未获取到 conversationId')
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversationId
|
||||||
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}
|
}
|
||||||
@@ -112,131 +129,86 @@ function handleUse() {
|
|||||||
:width="800"
|
:width="800"
|
||||||
:maskClosable="false"
|
:maskClosable="false"
|
||||||
:keyboard="false"
|
:keyboard="false"
|
||||||
@cancel="handleClose">
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
<div class="batch-prompt-modal">
|
<div class="batch-prompt-modal">
|
||||||
|
<!-- 内容显示模式 -->
|
||||||
<div v-if="!batchPromptEditMode" class="batch-prompt-display">
|
<div v-if="!batchPromptEditMode" class="batch-prompt-display">
|
||||||
<ChatMessageRenderer
|
<ChatMessageRendererV2
|
||||||
:content="batchPrompt"
|
:content="batchPrompt"
|
||||||
:is-streaming="batchPromptGenerating"
|
:is-streaming="batchPromptGenerating"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a-textarea
|
|
||||||
|
<!-- 编辑模式 -->
|
||||||
|
<a-textarea
|
||||||
v-else
|
v-else
|
||||||
v-model:value="batchPrompt"
|
v-model:value="batchPrompt"
|
||||||
:rows="15"
|
:rows="15"
|
||||||
placeholder="内容将在这里显示..." />
|
placeholder="内容将在这里显示..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-space>
|
<div class="footer-actions">
|
||||||
<a-button size="small" :title="batchPromptEditMode ? '取消编辑' : '编辑'" @click="batchPromptEditMode = !batchPromptEditMode">
|
<div class="left-actions">
|
||||||
<template #icon>
|
<a-button type="text" @click="batchPromptEditMode = !batchPromptEditMode">
|
||||||
<EditOutlined />
|
{{ batchPromptEditMode ? '取消编辑' : '编辑' }}
|
||||||
</template>
|
</a-button>
|
||||||
</a-button>
|
<a-button type="text" @click="handleCopy">复制</a-button>
|
||||||
<a-button size="small" title="复制" @click="handleCopy">
|
<a-button
|
||||||
<template #icon>
|
type="text"
|
||||||
<CopyOutlined />
|
@click="handleSave"
|
||||||
</template>
|
:disabled="!batchPrompt.trim()"
|
||||||
</a-button>
|
>
|
||||||
<a-button size="small" title="保存提示词" @click="handleSave" :disabled="!batchPrompt.trim()">
|
保存提示词
|
||||||
保存提示词
|
</a-button>
|
||||||
</a-button>
|
</div>
|
||||||
<a-button @click="handleClose">取消</a-button>
|
<div class="right-actions">
|
||||||
<a-button
|
<a-button @click="handleClose">取消</a-button>
|
||||||
type="primary"
|
<a-button
|
||||||
:disabled="batchPromptGenerating || !batchPrompt.trim()"
|
type="primary"
|
||||||
@click="handleUse">去创作</a-button>
|
:disabled="batchPromptGenerating || !batchPrompt.trim()"
|
||||||
</a-space>
|
@click="handleUse"
|
||||||
|
>
|
||||||
|
去创作
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.batch-prompt-modal {
|
.batch-prompt-modal {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
|
||||||
|
.batch-prompt-display {
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-prompt-display {
|
.footer-actions {
|
||||||
min-height: 300px;
|
display: flex;
|
||||||
max-height: 500px;
|
justify-content: space-between;
|
||||||
overflow-y: auto;
|
align-items: center;
|
||||||
padding: 12px;
|
width: 100%;
|
||||||
background: #0d0d0d;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(h1) {
|
.left-actions {
|
||||||
font-size: 18px;
|
display: flex;
|
||||||
font-weight: 600;
|
gap: 8px;
|
||||||
margin: 12px 0;
|
}
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(h2) {
|
.right-actions {
|
||||||
font-size: 16px;
|
display: flex;
|
||||||
font-weight: 600;
|
gap: 8px;
|
||||||
margin: 16px 0 8px 0;
|
}
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(h3) {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 12px 0 6px 0;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(p) {
|
|
||||||
margin: 8px 0;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(ul),
|
|
||||||
.batch-prompt-display :deep(ol) {
|
|
||||||
margin: 8px 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(li) {
|
|
||||||
margin: 4px 0;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(strong) {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(code) {
|
|
||||||
background: #1a1a1a;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #e11d48;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(pre) {
|
|
||||||
background: #1a1a1a;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(pre code) {
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-prompt-display :deep(blockquote) {
|
|
||||||
border-left: 3px solid var(--color-primary);
|
|
||||||
padding-left: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { CopyOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
import { CopyOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
||||||
import ChatMessageRenderer from '@/components/ChatMessageRenderer.vue'
|
import ChatMessageRendererV2 from '@/components/ChatMessageRendererV2.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
record: {
|
record: {
|
||||||
@@ -26,7 +26,6 @@ function handleCreateContent() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="expanded-content">
|
<div class="expanded-content">
|
||||||
<!-- 未分析的行显示提示 -->
|
|
||||||
<div v-if="!record.transcriptions && !record.prompt" class="no-analysis-tip">
|
<div v-if="!record.transcriptions && !record.prompt" class="no-analysis-tip">
|
||||||
<a-empty description="该视频尚未分析">
|
<a-empty description="该视频尚未分析">
|
||||||
<template #image>
|
<template #image>
|
||||||
@@ -42,10 +41,8 @@ function handleCreateContent() {
|
|||||||
</a-button>
|
</a-button>
|
||||||
</a-empty>
|
</a-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已分析的行显示内容 -->
|
|
||||||
<div v-else class="two-col">
|
<div v-else class="two-col">
|
||||||
<!-- 左侧:原配音内容 -->
|
|
||||||
<section class="col left-col">
|
<section class="col left-col">
|
||||||
<div class="sub-title">原配音</div>
|
<div class="sub-title">原配音</div>
|
||||||
<div class="transcript-box" v-if="record.transcriptions">
|
<div class="transcript-box" v-if="record.transcriptions">
|
||||||
@@ -54,44 +51,43 @@ function handleCreateContent() {
|
|||||||
<div v-else class="no-transcript">暂无转写文本,请先点击"分析"获取</div>
|
<div v-else class="no-transcript">暂无转写文本,请先点击"分析"获取</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 右侧:提示词 -->
|
|
||||||
<section class="col right-col">
|
<section class="col right-col">
|
||||||
<div class="sub-title">提示词</div>
|
<div class="sub-title">提示词</div>
|
||||||
|
|
||||||
<div class="prompt-display-wrapper">
|
<div class="prompt-display-wrapper">
|
||||||
<ChatMessageRenderer
|
<ChatMessageRendererV2
|
||||||
:content="record.prompt || ''"
|
:content="record.prompt || ''"
|
||||||
:is-streaming="record._analyzing || false"
|
:is-streaming="record._analyzing || false"
|
||||||
/>
|
/>
|
||||||
<div v-if="!record.prompt" class="no-prompt">暂无提示词</div>
|
<div v-if="!record.prompt" class="no-prompt">暂无提示词</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-actions">
|
<div class="right-actions">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button
|
<a-button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
class="copy-btn"
|
class="copy-btn"
|
||||||
:title="'复制'"
|
title="复制"
|
||||||
@click="handleCopy">
|
@click="handleCopy">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<CopyOutlined />
|
<CopyOutlined />
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="record.prompt"
|
v-if="record.prompt"
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
class="save-server-btn"
|
class="save-server-btn"
|
||||||
:title="'保存'"
|
title="保存"
|
||||||
@click="handleSaveToServer">
|
@click="handleSaveToServer">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SaveOutlined />
|
<SaveOutlined />
|
||||||
</template>
|
</template>
|
||||||
保存
|
保存
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
:disabled="!record.prompt || record._analyzing"
|
:disabled="!record.prompt || record._analyzing"
|
||||||
@click="handleCreateContent">基于提示词去创作</a-button>
|
@click="handleCreateContent">基于提示词去创作</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -177,7 +173,6 @@ function handleCreateContent() {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.no-analysis-tip {
|
.no-analysis-tip {
|
||||||
padding: var(--space-8) var(--space-5);
|
padding: var(--space-8) var(--space-5);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -12,43 +12,35 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
|
|||||||
const globalLoadingText = ref('')
|
const globalLoadingText = ref('')
|
||||||
const { getVoiceText } = useVoiceText()
|
const { getVoiceText } = useVoiceText()
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析单个视频,获取提示词
|
|
||||||
*/
|
|
||||||
async function analyzeVideo(row) {
|
async function analyzeVideo(row) {
|
||||||
try {
|
try {
|
||||||
if (row._analyzing) return
|
if (row._analyzing) return
|
||||||
|
|
||||||
row._analyzing = true
|
row._analyzing = true
|
||||||
|
|
||||||
// 1) 获取音频转写
|
|
||||||
message.info('正在获取音频转写...')
|
message.info('正在获取音频转写...')
|
||||||
const transcriptions = await getVoiceText([row])
|
const transcriptions = await getVoiceText([row])
|
||||||
row.transcriptions = transcriptions.find(item => item.audio_url === row.audio_url)?.value
|
row.transcriptions = transcriptions.find(item => item.audio_url === row.audio_url)?.value
|
||||||
|
|
||||||
// 2) 检查是否有语音文案
|
if (!row.transcriptions?.trim()) {
|
||||||
if (!row.transcriptions || !row.transcriptions.trim()) {
|
|
||||||
message.warning('未提取到语音内容,请检查音频文件或稍后重试')
|
message.warning('未提取到语音内容,请检查音频文件或稍后重试')
|
||||||
row._analyzing = false
|
row._analyzing = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) 创建对话
|
|
||||||
message.info('正在创建对话...')
|
message.info('正在创建对话...')
|
||||||
const createPayload = { roleId: 20, role_id: 20 }
|
const createPayload = { roleId: 20, role_id: 20 }
|
||||||
console.debug('createChatConversationMy payload:', createPayload)
|
console.debug('createChatConversationMy payload:', createPayload)
|
||||||
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
|
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
|
||||||
|
|
||||||
let conversationId = null
|
const conversationId = conversationResp?.data
|
||||||
if (conversationResp?.data) {
|
? (typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data)
|
||||||
conversationId = typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data
|
: null
|
||||||
}
|
|
||||||
|
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
throw new Error('创建对话失败:未获取到 conversationId')
|
throw new Error('创建对话失败:未获取到 conversationId')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) 基于转写构建提示,流式生成并实时写入 UI
|
|
||||||
message.info('正在生成提示词...')
|
message.info('正在生成提示词...')
|
||||||
const content = buildPromptFromTranscription(row.transcriptions)
|
const content = buildPromptFromTranscription(row.transcriptions)
|
||||||
const index = data.value.findIndex(item => item.id === row.id)
|
const index = data.value.findIndex(item => item.id === row.id)
|
||||||
@@ -63,19 +55,16 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
|
|||||||
typewriterBatchSize: 2
|
typewriterBatchSize: 2
|
||||||
})
|
})
|
||||||
|
|
||||||
// 5) 兜底处理
|
|
||||||
const finalPrompt = aiContent || row.transcriptions || ''
|
const finalPrompt = aiContent || row.transcriptions || ''
|
||||||
if (index !== -1) data.value[index].prompt = finalPrompt
|
if (index !== -1) data.value[index].prompt = finalPrompt
|
||||||
|
|
||||||
// 6) 分析完成后自动展开该行
|
const rowId = String(row.id)
|
||||||
const rowId = String(row.id) // 确保类型一致
|
|
||||||
if (!expandedRowKeys.value.includes(rowId)) {
|
if (!expandedRowKeys.value.includes(rowId)) {
|
||||||
expandedRowKeys.value.push(rowId)
|
expandedRowKeys.value.push(rowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) 保存数据到 session
|
|
||||||
await saveTableDataToSession()
|
await saveTableDataToSession()
|
||||||
|
|
||||||
message.success('分析完成')
|
message.success('分析完成')
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -87,11 +76,8 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量分析选中的视频
|
|
||||||
*/
|
|
||||||
async function batchAnalyze(selectedRowKeys, onBatchComplete) {
|
async function batchAnalyze(selectedRowKeys, onBatchComplete) {
|
||||||
if (selectedRowKeys.value.length === 0) {
|
if (!selectedRowKeys.value.length) {
|
||||||
message.warning('请先选择要分析的视频')
|
message.warning('请先选择要分析的视频')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -101,26 +87,23 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
|
|||||||
globalLoadingText.value = `正在批量分析 ${selectedRowKeys.value.length} 个视频...`
|
globalLoadingText.value = `正在批量分析 ${selectedRowKeys.value.length} 个视频...`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 获取所有选中视频的语音转写
|
|
||||||
globalLoadingText.value = '正在获取中...'
|
globalLoadingText.value = '正在获取中...'
|
||||||
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
|
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
|
||||||
const transcriptions = await getVoiceText(selectedRows)
|
const transcriptions = await getVoiceText(selectedRows)
|
||||||
|
|
||||||
// 2. 收集所有转写内容
|
|
||||||
const allTexts = []
|
const allTexts = []
|
||||||
for (const id of selectedRowKeys.value) {
|
for (const id of selectedRowKeys.value) {
|
||||||
const row = data.value.find(item => item.id === id)
|
const row = data.value.find(item => item.id === id)
|
||||||
if (row && row.audio_url) {
|
if (row?.audio_url) {
|
||||||
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
|
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
|
||||||
if (transcription && transcription.value && transcription.value.trim()) {
|
if (transcription?.value?.trim()) {
|
||||||
allTexts.push({ id: row.id, url: row.audio_url, text: transcription.value })
|
allTexts.push({ id: row.id, url: row.audio_url, text: transcription.value })
|
||||||
row.transcriptions = transcription.value
|
row.transcriptions = transcription.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查是否有可用的语音内容
|
if (!allTexts.length) {
|
||||||
if (allTexts.length === 0) {
|
|
||||||
message.warning('未提取到任何语音内容,请检查音频文件或稍后重试')
|
message.warning('未提取到任何语音内容,请检查音频文件或稍后重试')
|
||||||
batchAnalyzeLoading.value = false
|
batchAnalyzeLoading.value = false
|
||||||
globalLoading.value = false
|
globalLoading.value = false
|
||||||
@@ -130,8 +113,7 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
|
|||||||
|
|
||||||
await saveTableDataToSession()
|
await saveTableDataToSession()
|
||||||
const mergedText = allTexts.map(item => item.text).join('\n\n---\n\n')
|
const mergedText = allTexts.map(item => item.text).join('\n\n---\n\n')
|
||||||
|
|
||||||
// 4. 通知父组件打开弹窗并开始生成
|
|
||||||
if (onBatchComplete) {
|
if (onBatchComplete) {
|
||||||
await onBatchComplete(mergedText, allTexts.length)
|
await onBatchComplete(mergedText, allTexts.length)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,8 @@ export function useBenchmarkData() {
|
|||||||
const selectedRowKeys = ref([])
|
const selectedRowKeys = ref([])
|
||||||
const expandedRowKeys = ref([])
|
const expandedRowKeys = ref([])
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存表格数据到 session
|
|
||||||
*/
|
|
||||||
async function saveTableDataToSession() {
|
async function saveTableDataToSession() {
|
||||||
try {
|
try {
|
||||||
// 过滤掉不需要持久化的临时字段(如 _analyzing)
|
|
||||||
const persistData = (data.value || []).map((item) => {
|
const persistData = (data.value || []).map((item) => {
|
||||||
const rest = { ...item }
|
const rest = { ...item }
|
||||||
delete rest._analyzing
|
delete rest._analyzing
|
||||||
@@ -26,14 +22,10 @@ export function useBenchmarkData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 session 加载表格数据
|
|
||||||
*/
|
|
||||||
async function loadTableDataFromSession() {
|
async function loadTableDataFromSession() {
|
||||||
try {
|
try {
|
||||||
const savedData = await storage.getJSON(TABLE_DATA_STORAGE_KEY)
|
const savedData = await storage.getJSON(TABLE_DATA_STORAGE_KEY)
|
||||||
if (savedData && Array.isArray(savedData) && savedData.length > 0) {
|
if (savedData?.length) {
|
||||||
// 强制恢复临时字段的初始状态
|
|
||||||
data.value = savedData.map((item) => ({ ...item, _analyzing: false }))
|
data.value = savedData.map((item) => ({ ...item, _analyzing: false }))
|
||||||
console.log('从session加载了表格数据:', savedData.length, '条')
|
console.log('从session加载了表格数据:', savedData.length, '条')
|
||||||
}
|
}
|
||||||
@@ -42,9 +34,6 @@ export function useBenchmarkData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 API 响应数据
|
|
||||||
*/
|
|
||||||
function processApiResponse(resp, platform) {
|
function processApiResponse(resp, platform) {
|
||||||
if (platform === '抖音') {
|
if (platform === '抖音') {
|
||||||
const awemeList = resp?.data?.aweme_list || []
|
const awemeList = resp?.data?.aweme_list || []
|
||||||
@@ -57,9 +46,6 @@ export function useBenchmarkData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空数据
|
|
||||||
*/
|
|
||||||
async function clearData() {
|
async function clearData() {
|
||||||
data.value = []
|
data.value = []
|
||||||
selectedRowKeys.value = []
|
selectedRowKeys.value = []
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^25.0.6",
|
"@types/node": "^25.0.6",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"unocss": "^66.5.4",
|
"unocss": "^66.5.4",
|
||||||
"web-storage-cache": "^1.1.1"
|
"web-storage-cache": "^1.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user