feat: AI 打字机流式输出、小程序脚本主页布局及灵感卡片优化

- life-script: 新增 aiRuntime 打字机流式服务,PathView/ScriptView/TimelineView 接入打字机效果
- mini-program: ScriptView 重构为打字机输出 + 卡片式灵感列表,主页布局优化
- web: aiRuntime 服务新增流式输出支持
- chat store: 消息状态管理和打字机流式渲染支持

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:35:33 +08:00
parent c900f56174
commit 64476eee6d
21 changed files with 1474 additions and 205 deletions
+73 -3
View File
@@ -49,6 +49,70 @@ export const useChatStore = defineStore('chat', () => {
const messageQueue = ref<QueuedMessage[]>([])
const isProcessingQueue = ref(false)
const queueProcessingInterval = ref<number | null>(null)
const streamBuffers = new Map<string, string>()
const streamDoneIds = new Set<string>()
const streamTimers = new Map<string, number>()
const TYPEWRITER_INTERVAL = 18
const TYPEWRITER_STEP = 1
const stopStreamTimer = (messageId: string) => {
const timer = streamTimers.get(messageId)
if (timer) {
window.clearInterval(timer)
streamTimers.delete(messageId)
}
}
const stopStreamTypewriter = (messageId: string) => {
stopStreamTimer(messageId)
streamBuffers.delete(messageId)
streamDoneIds.delete(messageId)
}
const flushStreamBuffer = (messageId: string) => {
const message = messages.value.find(m => m.id === messageId)
if (!message) {
stopStreamTypewriter(messageId)
return
}
const buffer = streamBuffers.get(messageId) || ''
if (buffer) {
const nextText = buffer.slice(0, TYPEWRITER_STEP)
message.content += nextText
streamBuffers.set(messageId, buffer.slice(TYPEWRITER_STEP))
message.status = 'sending'
return
}
if (streamDoneIds.has(messageId)) {
message.status = 'sent'
streamDoneIds.delete(messageId)
}
stopStreamTimer(messageId)
}
const ensureStreamTimer = (messageId: string) => {
if (streamTimers.has(messageId)) return
const timer = window.setInterval(() => flushStreamBuffer(messageId), TYPEWRITER_INTERVAL)
streamTimers.set(messageId, timer)
}
const appendStreamText = (messageId: string, text: string) => {
if (!text) return
streamBuffers.set(messageId, `${streamBuffers.get(messageId) || ''}${text}`)
ensureStreamTimer(messageId)
}
const finishStreamText = (messageId: string) => {
streamDoneIds.add(messageId)
ensureStreamTimer(messageId)
}
const stopAllStreamTypewriters = () => {
streamTimers.forEach(timer => window.clearInterval(timer))
streamTimers.clear()
streamBuffers.clear()
streamDoneIds.clear()
}
// 计算属性
const currentMessages = computed(() => {
@@ -457,7 +521,7 @@ export const useChatStore = defineStore('chat', () => {
// 添加AI消息到队列,使用后端的messageId
const aiMessage = addMessage({
id: messageId,
content: wsMessage.content.trim(),
content: '',
type: 'ai',
conversationId: wsMessage.conversationId || currentSession.value?.id,
timestamp: wsMessage.createTime || new Date().toISOString()
@@ -467,6 +531,8 @@ export const useChatStore = defineStore('chat', () => {
// 立即处理队列
await processMessageQueue()
appendStreamText(aiMessage.id, wsMessage.content?.trim() || '')
finishStreamText(aiMessage.id)
}
const ensureStreamingAiMessage = async (wsMessage: WebSocketMessage) => {
@@ -496,25 +562,28 @@ export const useChatStore = defineStore('chat', () => {
if (wsMessage.type === 'AI_STREAM_START') {
isTyping.value = true
stopStreamTypewriter(message.id)
message.content = ''
message.status = 'sending'
return
}
if (wsMessage.type === 'AI_STREAM_DELTA') {
isTyping.value = false
message.content += wsMessage.content || ''
appendStreamText(message.id, wsMessage.content || '')
message.status = 'sending'
return
}
if (wsMessage.type === 'AI_STREAM_DONE') {
isTyping.value = false
message.status = 'sent'
finishStreamText(message.id)
return
}
if (wsMessage.type === 'AI_STREAM_ERROR') {
isTyping.value = false
stopStreamTypewriter(message.id)
message.status = 'failed'
message.error = wsMessage.data?.message || wsMessage.content || 'AI服务暂时不可用'
if (!message.content) {
@@ -653,6 +722,7 @@ export const useChatStore = defineStore('chat', () => {
const disconnectWebSocket = () => {
stompWebSocketService.disconnect()
stopAllStreamTypewriters()
wsConnected.value = false
isConnected.value = false