2026-02-10 13:05:24 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="agent-container flex flex-col">
|
|
|
|
|
|
<div class="agent-header flex align-center space-between">
|
|
|
|
|
|
<div class="header-title-wrapper">
|
|
|
|
|
|
<div class="agent-title">{{ props.title }}</div>
|
|
|
|
|
|
<div class="agent-name">AI Assistant 1.0</div>
|
|
|
|
|
|
</div>
|
2026-03-03 11:23:25 +08:00
|
|
|
|
<!-- <SvgIcon name="equal" color="#0d0d0d" size="24" /> -->
|
2026-02-10 13:05:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="agent-body flex-1 flex flex-col">
|
2026-04-02 14:50:20 +08:00
|
|
|
|
<List ref="listRef" :message-list="messageList" />
|
2026-02-25 11:00:31 +08:00
|
|
|
|
<Input
|
2026-02-26 16:48:08 +08:00
|
|
|
|
ref="inputRef"
|
2026-02-25 11:00:31 +08:00
|
|
|
|
is-agent-mode
|
|
|
|
|
|
:generating="isGenerating"
|
|
|
|
|
|
@send="handleSendMessage"
|
|
|
|
|
|
@pause="handlePause"
|
|
|
|
|
|
/>
|
2026-02-10 13:05:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-02 17:04:35 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ref,
|
|
|
|
|
|
reactive,
|
|
|
|
|
|
computed,
|
|
|
|
|
|
onUnmounted,
|
|
|
|
|
|
onMounted,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
watch,
|
|
|
|
|
|
onActivated
|
|
|
|
|
|
} from 'vue'
|
2026-02-10 13:05:24 +08:00
|
|
|
|
import List from './List.vue'
|
|
|
|
|
|
import Input from '../../components/Input.vue'
|
2026-03-10 16:28:11 +08:00
|
|
|
|
import { chatUrl } from '@/api/agent'
|
2026-02-11 16:32:38 +08:00
|
|
|
|
import type { AgentParamsType } from '@/api/agent'
|
2026-03-10 16:28:11 +08:00
|
|
|
|
import { useUserInfoStore, useProjectStore, useAgentStore } from '@/stores'
|
2026-03-11 14:32:13 +08:00
|
|
|
|
import MyEvent from '@/utils/myEvent'
|
2026-03-25 16:58:06 +08:00
|
|
|
|
import { useI18n } from 'vue-i18n'
|
2026-04-02 17:04:35 +08:00
|
|
|
|
import { useRoute } from 'vue-router'
|
2026-03-25 16:58:06 +08:00
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
2026-04-02 17:04:35 +08:00
|
|
|
|
const route = useRoute()
|
2026-02-10 13:05:24 +08:00
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
const userStore = useUserInfoStore()
|
2026-02-24 13:53:01 +08:00
|
|
|
|
const agentStore = useAgentStore()
|
2026-02-24 14:28:25 +08:00
|
|
|
|
const projectStore = useProjectStore()
|
2026-02-25 15:10:03 +08:00
|
|
|
|
|
2026-03-13 17:23:56 +08:00
|
|
|
|
const reportsContent = ref('')
|
|
|
|
|
|
const isGeneratingReport = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const emits = defineEmits(['update:sketchList', 'setTitle'])
|
2026-02-10 13:05:24 +08:00
|
|
|
|
const props = withDefaults(
|
|
|
|
|
|
defineProps<{
|
|
|
|
|
|
title: string
|
|
|
|
|
|
}>(),
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'Retro Sofa Sketch'
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
const messageList = ref([])
|
2026-02-25 15:10:03 +08:00
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
const listRef = ref()
|
2026-02-26 16:48:08 +08:00
|
|
|
|
const inputRef = ref()
|
2026-02-25 11:00:31 +08:00
|
|
|
|
const isGenerating = ref(false)
|
|
|
|
|
|
const isPaused = ref(false) // 标记是否为主动暂停
|
2026-02-11 16:32:38 +08:00
|
|
|
|
const params = reactive<AgentParamsType>({
|
2026-04-02 17:04:35 +08:00
|
|
|
|
projectID: route.params.id as string,
|
2026-02-11 16:32:38 +08:00
|
|
|
|
message: '',
|
2026-02-24 13:53:01 +08:00
|
|
|
|
token: userStore.state.token,
|
|
|
|
|
|
versionID: '',
|
2026-03-10 16:28:11 +08:00
|
|
|
|
needSuggestion: false,
|
|
|
|
|
|
useReport: false,
|
2026-02-11 16:32:38 +08:00
|
|
|
|
configParams: {
|
2026-03-02 14:03:59 +08:00
|
|
|
|
type: '',
|
|
|
|
|
|
region: '',
|
|
|
|
|
|
style: ''
|
2026-02-10 17:22:40 +08:00
|
|
|
|
},
|
2026-03-24 16:57:40 +08:00
|
|
|
|
imageUrlList: [],
|
|
|
|
|
|
quotaUrl: []
|
2026-02-11 16:32:38 +08:00
|
|
|
|
})
|
2026-02-10 17:22:40 +08:00
|
|
|
|
|
2026-02-25 15:10:03 +08:00
|
|
|
|
const sketchList = ref([])
|
2026-02-26 16:48:08 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
sketchList,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
emits('update:sketchList', newVal)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
2026-02-25 15:10:03 +08:00
|
|
|
|
|
2026-04-02 14:50:20 +08:00
|
|
|
|
const handleReset = () => {
|
2026-04-01 17:16:25 +08:00
|
|
|
|
messageList.value = []
|
|
|
|
|
|
sketchList.value = []
|
|
|
|
|
|
params.versionID = ''
|
|
|
|
|
|
params.imageUrlList = []
|
|
|
|
|
|
params.quotaUrl = []
|
|
|
|
|
|
params.needSuggestion = false
|
|
|
|
|
|
params.useReport = false
|
|
|
|
|
|
params.configParams = {
|
|
|
|
|
|
type: '',
|
|
|
|
|
|
region: '',
|
|
|
|
|
|
style: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
isGenerating.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 11:00:31 +08:00
|
|
|
|
// 每次请求时创建新的 AbortController
|
|
|
|
|
|
let abort: AbortController
|
|
|
|
|
|
|
|
|
|
|
|
const createAbortController = () => {
|
|
|
|
|
|
// 如果已有未完成的中止,先中止它
|
|
|
|
|
|
if (abort) {
|
|
|
|
|
|
abort.abort()
|
|
|
|
|
|
}
|
|
|
|
|
|
abort = new AbortController()
|
|
|
|
|
|
return abort
|
|
|
|
|
|
}
|
2026-02-11 16:32:38 +08:00
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-04-02 14:50:20 +08:00
|
|
|
|
// abort?.abort()
|
2026-04-01 17:16:25 +08:00
|
|
|
|
MyEvent.remove('resetAgent', handleReset)
|
2026-02-11 16:32:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-24 13:53:01 +08:00
|
|
|
|
onMounted(() => {
|
2026-04-01 17:16:25 +08:00
|
|
|
|
MyEvent.add('resetAgent', handleReset)
|
2026-02-24 13:53:01 +08:00
|
|
|
|
// 检查 store 中是否有初始项目数据
|
|
|
|
|
|
const initialData = agentStore.getInitialProjectData
|
|
|
|
|
|
if (initialData) {
|
|
|
|
|
|
// 等待页面渲染完成后自动发送初始消息
|
|
|
|
|
|
params.configParams = {
|
2026-02-25 15:10:03 +08:00
|
|
|
|
type: initialData.type,
|
2026-02-25 11:00:31 +08:00
|
|
|
|
region: initialData.area,
|
2026-02-25 15:10:03 +08:00
|
|
|
|
style: initialData.style,
|
2026-02-24 13:53:01 +08:00
|
|
|
|
temperature: 0.7
|
|
|
|
|
|
}
|
2026-03-10 16:28:11 +08:00
|
|
|
|
params.needSuggestion = initialData.needSuggestion || false
|
|
|
|
|
|
params.useReport = initialData.useReport
|
2026-02-24 13:53:01 +08:00
|
|
|
|
handleSendMessage({
|
|
|
|
|
|
text: initialData.text,
|
2026-03-03 14:40:49 +08:00
|
|
|
|
images: initialData.images,
|
2026-03-18 17:24:52 +08:00
|
|
|
|
useReport: initialData.useReport,
|
2026-03-24 16:57:40 +08:00
|
|
|
|
tempImages: initialData.tempImages,
|
|
|
|
|
|
quoteList: initialData.quoteList
|
2026-02-24 13:53:01 +08:00
|
|
|
|
})
|
|
|
|
|
|
// 更新 configParams
|
|
|
|
|
|
|
|
|
|
|
|
// 清空初始数据
|
|
|
|
|
|
agentStore.clearInitialProjectData()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-02 17:04:35 +08:00
|
|
|
|
// 用于存储不在页面显示但是仍进行中的对话信息
|
|
|
|
|
|
const newQueue = ref<{
|
|
|
|
|
|
nodeId?: string
|
|
|
|
|
|
name?: string
|
|
|
|
|
|
}>({})
|
|
|
|
|
|
onActivated(() => {
|
|
|
|
|
|
if (newQueue.value.nodeId) {
|
|
|
|
|
|
projectStore.setProject({ nodeId: newQueue.value.nodeId })
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newQueue.value.name) {
|
|
|
|
|
|
MyEvent.emit('newTitle', {
|
|
|
|
|
|
title: newQueue.value.name,
|
|
|
|
|
|
id: params.projectID
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newQueue.value.newSketch) {
|
|
|
|
|
|
mergeUniqueKeys(sketchList.value, newQueue.value.newSketch)
|
|
|
|
|
|
}
|
|
|
|
|
|
newQueue.value = {}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-25 15:10:03 +08:00
|
|
|
|
const handleSendMessage = async (
|
|
|
|
|
|
message: {
|
|
|
|
|
|
text: string
|
2026-03-02 16:40:40 +08:00
|
|
|
|
images: Array<{ url: string; name: string }>
|
2026-03-02 15:08:03 +08:00
|
|
|
|
tempImages: any[]
|
2026-03-17 14:16:14 +08:00
|
|
|
|
useReport: boolean
|
2026-03-24 16:57:40 +08:00
|
|
|
|
quoteList: Array<string>
|
2026-02-25 15:10:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
skipUserMessage = false
|
|
|
|
|
|
) => {
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isPaused.value = false
|
|
|
|
|
|
isGenerating.value = true
|
2026-02-11 16:32:38 +08:00
|
|
|
|
params.message = message.text
|
2026-03-18 17:24:52 +08:00
|
|
|
|
if (message.hasOwnProperty('useReport')) {
|
|
|
|
|
|
params.useReport = message.useReport
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
params.imageUrlList = message.images || []
|
2026-03-24 16:57:40 +08:00
|
|
|
|
params.quotaUrl = message.quoteList || []
|
2026-02-25 11:00:31 +08:00
|
|
|
|
// 如果不是重新生成模式,则添加用户消息到列表
|
|
|
|
|
|
if (!skipUserMessage) {
|
|
|
|
|
|
messageList.value.push({
|
|
|
|
|
|
id: messageList.value.length + 1,
|
|
|
|
|
|
text: message.text,
|
2026-03-02 15:08:03 +08:00
|
|
|
|
isUser: true,
|
2026-03-24 16:57:40 +08:00
|
|
|
|
imageUrls: message.tempImages.concat(message.quoteList)
|
2026-02-25 11:00:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-02-10 17:22:40 +08:00
|
|
|
|
|
|
|
|
|
|
// Add AI loading message
|
|
|
|
|
|
const aiMessage = reactive({
|
|
|
|
|
|
id: messageList.value.length + 1,
|
|
|
|
|
|
text: '',
|
|
|
|
|
|
isUser: false,
|
2026-04-02 17:04:35 +08:00
|
|
|
|
sessionId: route.params.id as string,
|
2026-02-10 17:22:40 +08:00
|
|
|
|
loading: true,
|
|
|
|
|
|
thinking: false,
|
|
|
|
|
|
thinkingText: '',
|
2026-02-11 16:32:38 +08:00
|
|
|
|
streaming: true
|
2026-02-10 17:22:40 +08:00
|
|
|
|
})
|
|
|
|
|
|
messageList.value.push(aiMessage)
|
|
|
|
|
|
|
2026-02-25 11:00:31 +08:00
|
|
|
|
// 创建新的 AbortController
|
|
|
|
|
|
const abortController = createAbortController()
|
|
|
|
|
|
|
2026-02-24 11:17:01 +08:00
|
|
|
|
// console.log('token---', params.token, '参数---', params)
|
2026-04-02 17:04:35 +08:00
|
|
|
|
params.projectID = route.params.id as string
|
2026-02-11 16:32:38 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const urlParams = new URLSearchParams<AgentParamsType>({
|
|
|
|
|
|
...params,
|
|
|
|
|
|
configParams: JSON.stringify(params.configParams)
|
|
|
|
|
|
})
|
2026-03-02 15:41:24 +08:00
|
|
|
|
const BASEURL = import.meta.env.VITE_APP_URL
|
2026-03-27 09:57:41 +08:00
|
|
|
|
// console.log('params', params)
|
|
|
|
|
|
|
|
|
|
|
|
// debugger
|
2026-03-10 16:28:11 +08:00
|
|
|
|
const response = await fetch(`${BASEURL}${chatUrl}?${urlParams.toString()}`, {
|
2026-02-11 16:32:38 +08:00
|
|
|
|
method: 'GET',
|
2026-02-25 11:00:31 +08:00
|
|
|
|
signal: abortController.signal
|
2026-02-11 16:32:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 检查响应内容类型,判断是否为流式响应
|
|
|
|
|
|
const contentType = response.headers.get('content-type') || ''
|
|
|
|
|
|
const isStreamResponse =
|
|
|
|
|
|
contentType.includes('text/event-stream') || contentType.includes('stream')
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
// 非流式错误响应,使用 text() 读取错误信息
|
|
|
|
|
|
const errorText = await response.text()
|
|
|
|
|
|
console.error('请求错误:', errorText)
|
2026-02-25 11:00:31 +08:00
|
|
|
|
if (!isPaused.value) {
|
|
|
|
|
|
aiMessage.text = '发送失败,请重试'
|
|
|
|
|
|
}
|
2026-02-23 14:53:29 +08:00
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isGenerating.value = false
|
2026-02-23 14:53:29 +08:00
|
|
|
|
return
|
2026-02-11 16:32:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 不是流式响应,使用 text()读取错误信息
|
|
|
|
|
|
if (!isStreamResponse) {
|
|
|
|
|
|
const text = await response.text()
|
|
|
|
|
|
try {
|
|
|
|
|
|
const errorData = JSON.parse(text)
|
|
|
|
|
|
console.error('非流式响应:', errorData)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('非流式响应文本:', text)
|
|
|
|
|
|
}
|
2026-02-25 11:00:31 +08:00
|
|
|
|
if (!isPaused.value) {
|
|
|
|
|
|
aiMessage.text = '发送失败,请重试'
|
|
|
|
|
|
}
|
2026-02-11 16:32:38 +08:00
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isGenerating.value = false
|
2026-02-11 16:32:38 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 流式响应处理
|
|
|
|
|
|
let contentBody = ''
|
|
|
|
|
|
let buffer = ''
|
|
|
|
|
|
const reader = response.body?.getReader()
|
|
|
|
|
|
if (!reader) throw new Error('无法获取流读取器')
|
|
|
|
|
|
|
|
|
|
|
|
const decoder = new TextDecoder()
|
2026-03-13 17:36:49 +08:00
|
|
|
|
let previousEventName = '' // 记录上一个事件名称
|
|
|
|
|
|
let hasReportStarted = false // 标记 report 是否已经开始
|
2026-03-24 15:45:28 +08:00
|
|
|
|
|
|
|
|
|
|
let hasSketchEvent = false
|
2026-03-24 16:57:40 +08:00
|
|
|
|
let hasReportEvent = false
|
|
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
try {
|
|
|
|
|
|
let flag = true
|
|
|
|
|
|
while (flag) {
|
|
|
|
|
|
const { done, value } = await reader.read()
|
|
|
|
|
|
if (done) {
|
2026-03-24 15:45:28 +08:00
|
|
|
|
if (hasSketchEvent) {
|
|
|
|
|
|
aiMessage.text += `<slot slot-name="sketch"></slot>`
|
|
|
|
|
|
}
|
2026-03-24 16:57:40 +08:00
|
|
|
|
if (hasReportEvent) {
|
|
|
|
|
|
aiMessage.text += `<slot slot-name="card" title="Report" content="Report"></slot>`
|
|
|
|
|
|
}
|
2026-03-24 15:45:28 +08:00
|
|
|
|
|
2026-02-10 17:22:40 +08:00
|
|
|
|
aiMessage.streaming = false
|
2026-02-11 16:32:38 +08:00
|
|
|
|
aiMessage.loading = false
|
2026-02-25 15:10:03 +08:00
|
|
|
|
isGenerating.value = false
|
2026-02-11 16:32:38 +08:00
|
|
|
|
flag = false
|
|
|
|
|
|
break
|
2026-02-10 17:22:40 +08:00
|
|
|
|
}
|
2026-02-11 16:32:38 +08:00
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
2026-03-10 16:28:11 +08:00
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
// 优先按空行拆分事件块(SSE标准)
|
|
|
|
|
|
let events = buffer.split(/\n\n/)
|
|
|
|
|
|
buffer = events.pop() // 保留不完整块
|
|
|
|
|
|
|
|
|
|
|
|
for (let event of events) {
|
|
|
|
|
|
if (!event.trim()) continue
|
|
|
|
|
|
|
2026-03-18 17:24:52 +08:00
|
|
|
|
const eventName = event
|
|
|
|
|
|
.split(/\n/)
|
|
|
|
|
|
.find((line) => line.startsWith('event:'))
|
|
|
|
|
|
?.replace(/^event:\s*/, '')
|
|
|
|
|
|
?.trim()
|
2026-03-13 17:23:56 +08:00
|
|
|
|
|
|
|
|
|
|
if (!hasReportStarted && eventName === 'report') {
|
|
|
|
|
|
isGeneratingReport.value = true
|
2026-03-24 16:57:40 +08:00
|
|
|
|
hasReportEvent = true
|
|
|
|
|
|
// contentBody += `<slot slot-name="card" title="Report" content="Report"></slot>`
|
2026-03-13 17:23:56 +08:00
|
|
|
|
hasReportStarted = true
|
2026-03-25 16:58:06 +08:00
|
|
|
|
ElMessage.success(t('agent.generatingReport'))
|
2026-03-13 17:23:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
previousEventName === 'report' &&
|
|
|
|
|
|
eventName !== 'report' &&
|
|
|
|
|
|
reportsContent.value
|
|
|
|
|
|
) {
|
|
|
|
|
|
isGeneratingReport.value = false
|
2026-03-16 14:11:01 +08:00
|
|
|
|
sessionStorage.setItem(
|
2026-04-02 17:04:35 +08:00
|
|
|
|
'reportsContent_' + params.projectID,
|
2026-03-13 17:36:49 +08:00
|
|
|
|
reportsContent.value
|
|
|
|
|
|
)
|
2026-02-24 14:24:45 +08:00
|
|
|
|
}
|
2026-03-13 17:23:56 +08:00
|
|
|
|
|
|
|
|
|
|
previousEventName = eventName
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('eventName:', eventName, 'event:', event)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据事件名称精确判断,而不是用 includes
|
|
|
|
|
|
if (eventName === 'error') {
|
2026-02-25 15:10:03 +08:00
|
|
|
|
aiMessage.text = '出现错误,请重试'
|
|
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
|
|
|
|
|
isGenerating.value = false
|
|
|
|
|
|
flag = false
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2026-03-13 17:23:56 +08:00
|
|
|
|
if (eventName === 'todo') {
|
2026-03-18 17:24:52 +08:00
|
|
|
|
continue
|
2026-03-11 14:32:13 +08:00
|
|
|
|
}
|
2026-03-13 17:23:56 +08:00
|
|
|
|
|
|
|
|
|
|
let isNodeIdEvent = eventName === 'nodeId'
|
|
|
|
|
|
|
2026-03-16 13:07:31 +08:00
|
|
|
|
let hasSketch = eventName === 'sketchIDAndUrl'
|
2026-02-25 11:00:31 +08:00
|
|
|
|
|
2026-03-12 16:54:54 +08:00
|
|
|
|
const dataLines = event
|
|
|
|
|
|
.split(/\n/)
|
|
|
|
|
|
.filter((line) => line.startsWith('data:'))
|
2026-03-13 17:23:56 +08:00
|
|
|
|
.map((line) => line.replace(/^data:\s*/, ''))
|
2026-03-12 16:54:54 +08:00
|
|
|
|
.filter((content) => content.startsWith('{') || content.startsWith('['))
|
2026-03-13 17:23:56 +08:00
|
|
|
|
// console.log('dataLInes', dataLines)
|
2026-02-25 11:00:31 +08:00
|
|
|
|
if (isNodeIdEvent) {
|
2026-03-18 17:24:52 +08:00
|
|
|
|
const versionID = event
|
|
|
|
|
|
.split(/\n/)
|
|
|
|
|
|
.filter((line) => line.startsWith('data:'))
|
|
|
|
|
|
.map((line) => line.replace(/^data:\s*/, ''))[0]
|
2026-03-16 16:46:43 +08:00
|
|
|
|
params.versionID = versionID
|
2026-04-02 17:04:35 +08:00
|
|
|
|
if (aiMessage.sessionId === projectStore.state.id) {
|
|
|
|
|
|
projectStore.setProject({ nodeId: versionID })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newQueue.value.nodeId = versionID
|
|
|
|
|
|
}
|
2026-02-24 14:24:45 +08:00
|
|
|
|
}
|
2026-03-12 16:54:54 +08:00
|
|
|
|
|
2026-03-13 17:23:56 +08:00
|
|
|
|
if (eventName === 'tool') {
|
2026-03-20 17:07:02 +08:00
|
|
|
|
MyEvent.emit('loading-sketch', sketchList.value.length)
|
2026-03-11 14:32:13 +08:00
|
|
|
|
}
|
2026-02-25 11:00:31 +08:00
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
if (dataLines.length === 0) continue
|
|
|
|
|
|
const jsonText = dataLines.join('\n')
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const jsonData = JSON.parse(jsonText)
|
2026-03-13 17:23:56 +08:00
|
|
|
|
// console.log('jsonData', jsonData)
|
|
|
|
|
|
if (jsonData.webAddress) {
|
2026-03-16 11:43:19 +08:00
|
|
|
|
aiMessage.webAddress = JSON.parse(jsonData.webAddress)
|
2026-03-24 15:45:28 +08:00
|
|
|
|
contentBody += `<slot slot-name="url"></slot>`
|
2026-03-13 17:23:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (jsonData.title) {
|
2026-04-02 17:04:35 +08:00
|
|
|
|
if (aiMessage.sessionId === projectStore.state.id) {
|
|
|
|
|
|
emits('setTitle', jsonData.title)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newQueue.value.name = jsonData.title
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 14:50:20 +08:00
|
|
|
|
MyEvent.emit('newTitle', {
|
|
|
|
|
|
title: jsonData.title,
|
|
|
|
|
|
id: params.projectID
|
|
|
|
|
|
})
|
2026-03-13 17:23:56 +08:00
|
|
|
|
}
|
2026-02-26 16:48:08 +08:00
|
|
|
|
|
2026-03-12 16:54:54 +08:00
|
|
|
|
if (hasSketch) {
|
2026-03-24 15:45:28 +08:00
|
|
|
|
hasSketchEvent = true
|
2026-04-02 17:04:35 +08:00
|
|
|
|
let tempArr = []
|
|
|
|
|
|
if (params.projectID === projectStore.state.id) {
|
|
|
|
|
|
mergeUniqueKeys(sketchList.value, jsonData)
|
|
|
|
|
|
MyEvent.emit('OpenSketch', params.projectID)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mergeUniqueKeys(tempArr, jsonData)
|
|
|
|
|
|
newQueue.value.newSketch = tempArr
|
|
|
|
|
|
}
|
2026-02-25 15:10:03 +08:00
|
|
|
|
}
|
2026-03-27 16:49:29 +08:00
|
|
|
|
if (eventName === 'reportName' || eventName === 'reportTitle') {
|
|
|
|
|
|
aiMessage.reportName = jsonData.reportName || jsonData.reportTitle
|
|
|
|
|
|
}
|
2026-03-13 17:23:56 +08:00
|
|
|
|
if (eventName === 'report') {
|
2026-03-13 17:36:49 +08:00
|
|
|
|
reportsContent.value += jsonData.report
|
2026-03-13 17:23:56 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if (jsonData.reasoning) {
|
|
|
|
|
|
aiMessage.thinking = true
|
|
|
|
|
|
aiMessage.loading = false
|
|
|
|
|
|
aiMessage.thinkingText += jsonData.reasoning
|
|
|
|
|
|
} else {
|
|
|
|
|
|
aiMessage.thinking = false
|
|
|
|
|
|
if (
|
|
|
|
|
|
jsonData.content &&
|
|
|
|
|
|
jsonData.content.length > 0 &&
|
|
|
|
|
|
jsonData.type !== 'end'
|
|
|
|
|
|
) {
|
|
|
|
|
|
contentBody += jsonData.content
|
|
|
|
|
|
aiMessage.text = contentBody
|
|
|
|
|
|
aiMessage.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 16:32:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (jsonData.type === 'end') {
|
|
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isGenerating.value = false
|
2026-02-11 16:32:38 +08:00
|
|
|
|
flag = false
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 检查是否为纯文本 [DONE]
|
|
|
|
|
|
if (jsonText.trim() === '[DONE]') {
|
|
|
|
|
|
console.log('结束-----------------------')
|
2026-03-24 15:45:28 +08:00
|
|
|
|
aiMessage.text = contentBody
|
2026-02-11 16:32:38 +08:00
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isGenerating.value = false
|
2026-02-11 16:32:38 +08:00
|
|
|
|
flag = false
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
// JSON 不完整:保留到下一次循环
|
|
|
|
|
|
if (!jsonText.trim().endsWith('}')) {
|
|
|
|
|
|
buffer = 'data: ' + jsonText // 重新放回缓存
|
|
|
|
|
|
continue
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('⚠️ JSON 格式错误,跳过:', jsonText)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-13 17:23:56 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新上一个事件名称
|
|
|
|
|
|
previousEventName = eventName
|
2026-02-11 16:32:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('流式传输错误:', error)
|
2026-02-25 11:00:31 +08:00
|
|
|
|
if (!isPaused.value) {
|
|
|
|
|
|
aiMessage.text = '发送失败,请重试'
|
|
|
|
|
|
}
|
2026-02-11 16:32:38 +08:00
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isGenerating.value = false
|
2026-02-11 16:32:38 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
reader.releaseLock()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('fetch请求失败:', error)
|
2026-02-25 11:00:31 +08:00
|
|
|
|
if (!isPaused.value) {
|
|
|
|
|
|
aiMessage.text = '发送失败,请重试'
|
|
|
|
|
|
}
|
2026-02-11 16:32:38 +08:00
|
|
|
|
aiMessage.streaming = false
|
|
|
|
|
|
aiMessage.loading = false
|
2026-02-25 11:00:31 +08:00
|
|
|
|
isGenerating.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePause = () => {
|
|
|
|
|
|
isPaused.value = true
|
|
|
|
|
|
isGenerating.value = false
|
|
|
|
|
|
abort?.abort()
|
2026-03-25 17:28:30 +08:00
|
|
|
|
MyEvent.emit('stopChat')
|
2026-02-25 11:00:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 14:50:20 +08:00
|
|
|
|
// const handleRegenerate = async (aiMessage: any) => {
|
|
|
|
|
|
// // 找到当前 AI 消息在列表中的索引
|
|
|
|
|
|
// const aiIndex = messageList.value.findIndex((msg) => msg.id === aiMessage.id)
|
|
|
|
|
|
// if (aiIndex === -1) return
|
|
|
|
|
|
|
|
|
|
|
|
// // 找到对应的用户消息(AI 消息前面的最近一条用户消息)
|
|
|
|
|
|
// let userMessage = null
|
|
|
|
|
|
// for (let i = aiIndex - 1; i >= 0; i--) {
|
|
|
|
|
|
// if (messageList.value[i].isUser) {
|
|
|
|
|
|
// userMessage = messageList.value[i]
|
|
|
|
|
|
// break
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
// if (!userMessage) return
|
|
|
|
|
|
|
|
|
|
|
|
// // 删除当前的 AI 回复消息
|
|
|
|
|
|
// messageList.value.splice(aiIndex, 1)
|
|
|
|
|
|
|
|
|
|
|
|
// // 重新调用 API(跳过用户消息添加,因为用户消息已存在)
|
|
|
|
|
|
// await handleSendMessage(
|
|
|
|
|
|
// {
|
|
|
|
|
|
// text: userMessage.text,
|
|
|
|
|
|
// images: userMessage.images || []
|
|
|
|
|
|
// },
|
|
|
|
|
|
// true
|
|
|
|
|
|
// )
|
|
|
|
|
|
// }
|
2026-02-26 16:48:08 +08:00
|
|
|
|
|
2026-03-04 14:02:11 +08:00
|
|
|
|
// 处理对话列表,将连续的 assistant 消息合并为一条
|
2026-04-01 16:59:22 +08:00
|
|
|
|
const processDialogue = (dialogue, startIndex, existingImgList, sessionId) => {
|
2026-03-04 14:02:11 +08:00
|
|
|
|
if (!dialogue || dialogue.length === 0) return []
|
|
|
|
|
|
|
|
|
|
|
|
const result = []
|
|
|
|
|
|
let i = startIndex
|
|
|
|
|
|
|
|
|
|
|
|
while (i < dialogue.length) {
|
|
|
|
|
|
const item = dialogue[i]
|
|
|
|
|
|
if (item.role === 'user') {
|
|
|
|
|
|
result.push({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
text: item.content,
|
|
|
|
|
|
isUser: true,
|
2026-03-13 17:23:56 +08:00
|
|
|
|
id: result.length + 1,
|
|
|
|
|
|
sessionId: sessionId
|
2026-03-04 14:02:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
i++
|
|
|
|
|
|
} else if (item.role === 'assistant') {
|
|
|
|
|
|
let combinedContent = item.content || ''
|
2026-03-13 17:23:56 +08:00
|
|
|
|
let combinedThinkingText = item.reasoning || ''
|
2026-03-24 15:45:28 +08:00
|
|
|
|
let combinedImageUrl = item.image_url || null
|
2026-03-27 16:49:29 +08:00
|
|
|
|
let reportName = item.reportName || null
|
2026-03-26 16:11:09 +08:00
|
|
|
|
let webAddress = item.webAddress || null
|
2026-04-01 16:59:22 +08:00
|
|
|
|
let combinedReport = item.report || ''
|
|
|
|
|
|
let hasReportSlot = !!item.report
|
|
|
|
|
|
let hasUrlSlot = !!item.webAddress
|
|
|
|
|
|
|
2026-03-04 14:02:11 +08:00
|
|
|
|
let j = i + 1
|
|
|
|
|
|
while (j < dialogue.length && dialogue[j].role === 'assistant') {
|
2026-04-01 16:59:22 +08:00
|
|
|
|
const next = dialogue[j]
|
|
|
|
|
|
combinedContent += next.content || ''
|
|
|
|
|
|
combinedThinkingText += next.reasoning || ''
|
|
|
|
|
|
combinedReport += next.report || ''
|
|
|
|
|
|
if (next.image_url) combinedImageUrl = next.image_url
|
|
|
|
|
|
if (next.reportName) reportName = next.reportName
|
|
|
|
|
|
if (next.report) hasReportSlot = true
|
|
|
|
|
|
if (next.webAddress) {
|
|
|
|
|
|
webAddress = next.webAddress
|
|
|
|
|
|
hasUrlSlot = true
|
2026-03-26 16:11:09 +08:00
|
|
|
|
}
|
2026-03-04 14:02:11 +08:00
|
|
|
|
j++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 16:59:22 +08:00
|
|
|
|
if (hasUrlSlot) {
|
|
|
|
|
|
combinedContent += `<slot slot-name="url"></slot>`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hasReportSlot) {
|
2026-03-25 16:58:06 +08:00
|
|
|
|
combinedContent += `<slot slot-name="card" title="Report" content="Report"></slot>`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 14:02:11 +08:00
|
|
|
|
result.push({
|
|
|
|
|
|
...item,
|
2026-03-27 16:49:29 +08:00
|
|
|
|
reportName,
|
2026-04-01 16:59:22 +08:00
|
|
|
|
report: combinedReport,
|
2026-03-04 14:02:11 +08:00
|
|
|
|
content: combinedContent,
|
2026-03-13 17:23:56 +08:00
|
|
|
|
thinkingText: combinedThinkingText,
|
2026-03-04 14:02:11 +08:00
|
|
|
|
text: combinedContent,
|
2026-03-24 15:45:28 +08:00
|
|
|
|
image_url: combinedImageUrl,
|
2026-04-02 14:50:20 +08:00
|
|
|
|
webAddress: !!webAddress ? JSON.parse(webAddress) : null,
|
2026-03-04 14:02:11 +08:00
|
|
|
|
isUser: false,
|
2026-03-13 17:23:56 +08:00
|
|
|
|
id: result.length + 1,
|
|
|
|
|
|
sessionId: sessionId
|
2026-03-04 14:02:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
i = j
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.push({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
text: item.content,
|
|
|
|
|
|
isUser: item.role === 'user',
|
2026-03-13 17:23:56 +08:00
|
|
|
|
id: result.length + 1,
|
|
|
|
|
|
sessionId: sessionId
|
2026-03-04 14:02:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
i++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
2026-03-13 17:23:56 +08:00
|
|
|
|
const processSession = (session, imgList, ancestorsList, idCounterRef) => {
|
|
|
|
|
|
if (!session) return
|
|
|
|
|
|
|
|
|
|
|
|
if (session.sketchIDAndUrl) {
|
2026-03-16 11:43:19 +08:00
|
|
|
|
imgList.push(session.sketchIDAndUrl)
|
2026-03-13 17:23:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 16:59:22 +08:00
|
|
|
|
const list = processDialogue(session.dialogue, 0, imgList, session.id)
|
2026-03-13 17:23:56 +08:00
|
|
|
|
list.forEach((el) => {
|
|
|
|
|
|
el.id = idCounterRef.value++
|
|
|
|
|
|
})
|
2026-03-25 16:58:06 +08:00
|
|
|
|
|
2026-03-13 17:23:56 +08:00
|
|
|
|
ancestorsList.push(...list)
|
|
|
|
|
|
}
|
2026-03-04 14:02:11 +08:00
|
|
|
|
|
2026-03-24 15:45:28 +08:00
|
|
|
|
const setChatInfo = (info) => {
|
2026-03-03 09:59:19 +08:00
|
|
|
|
const initialData = agentStore.getInitialProjectData
|
|
|
|
|
|
if (isGenerating.value || initialData) return
|
|
|
|
|
|
|
2026-03-02 16:40:40 +08:00
|
|
|
|
const data = info.conversation
|
2026-03-02 17:35:07 +08:00
|
|
|
|
let project = info.project
|
|
|
|
|
|
if (info.id) {
|
|
|
|
|
|
project = info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 16:40:40 +08:00
|
|
|
|
params.versionID = ''
|
|
|
|
|
|
sketchList.value = []
|
2026-03-02 17:35:07 +08:00
|
|
|
|
if (project) {
|
|
|
|
|
|
params.configParams.type = project.type
|
|
|
|
|
|
params.configParams.region = project.area
|
|
|
|
|
|
params.configParams.style = project.style
|
|
|
|
|
|
params.configParams.temperature = project.temperature
|
|
|
|
|
|
}
|
2026-03-03 09:59:19 +08:00
|
|
|
|
if (!data) {
|
|
|
|
|
|
messageList.value = []
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-02 11:29:07 +08:00
|
|
|
|
const { ancestors, current } = data
|
2026-03-03 10:28:16 +08:00
|
|
|
|
|
2026-03-13 17:23:56 +08:00
|
|
|
|
const imgList = []
|
2026-03-02 11:29:07 +08:00
|
|
|
|
const ancestorsList = []
|
2026-03-13 17:23:56 +08:00
|
|
|
|
const idCounterRef = { value: 1 }
|
2026-03-11 10:53:25 +08:00
|
|
|
|
|
2026-03-13 17:23:56 +08:00
|
|
|
|
ancestors?.forEach((item) => {
|
|
|
|
|
|
processSession(item, imgList, ancestorsList, idCounterRef)
|
2026-03-04 14:02:11 +08:00
|
|
|
|
})
|
2026-03-02 13:54:52 +08:00
|
|
|
|
|
2026-03-13 17:23:56 +08:00
|
|
|
|
processSession(current, imgList, ancestorsList, idCounterRef)
|
|
|
|
|
|
|
2026-03-02 16:40:40 +08:00
|
|
|
|
nextTick(() => {
|
2026-03-24 15:45:28 +08:00
|
|
|
|
ancestorsList.forEach((item) => {
|
2026-03-24 16:57:40 +08:00
|
|
|
|
if (item.image_url && item.role !== 'user') {
|
2026-03-24 15:45:28 +08:00
|
|
|
|
item.text += `<slot slot-name="sketch"></slot>`
|
|
|
|
|
|
}
|
2026-03-20 17:07:02 +08:00
|
|
|
|
})
|
2026-04-02 14:50:20 +08:00
|
|
|
|
// console.log('ancestorslist', ancestorsList)
|
2026-03-13 17:23:56 +08:00
|
|
|
|
messageList.value = [...ancestorsList]
|
2026-03-02 16:40:40 +08:00
|
|
|
|
params.versionID = current?.id
|
|
|
|
|
|
sketchList.value = imgList
|
|
|
|
|
|
})
|
2026-03-02 11:29:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:04:35 +08:00
|
|
|
|
const mergeUniqueKeys = (targetArr, newData) => {
|
|
|
|
|
|
// 提取现有数组中所有的 key,存入 Set 以实现 O(1) 查询
|
|
|
|
|
|
const existingKeys = new Set(targetArr.flatMap((item) => Object.keys(item)))
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(newData).forEach(([key, value]) => {
|
|
|
|
|
|
if (!existingKeys.has(key)) {
|
|
|
|
|
|
targetArr.push({ [key]: value })
|
|
|
|
|
|
existingKeys.add(key) // 防止 newData 内部有重复 key 时重复插入
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:48:08 +08:00
|
|
|
|
defineExpose({
|
2026-04-02 14:50:20 +08:00
|
|
|
|
setChatInfo
|
2026-02-26 16:48:08 +08:00
|
|
|
|
})
|
2026-02-10 13:05:24 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.c-svg {
|
|
|
|
|
|
width: initial;
|
|
|
|
|
|
}
|
|
|
|
|
|
.agent-container {
|
|
|
|
|
|
// width: 39%;
|
2026-02-25 15:12:04 +08:00
|
|
|
|
// width: 63.4rem;
|
2026-03-03 13:11:05 +08:00
|
|
|
|
// flex: 1;
|
2026-03-06 13:26:14 +08:00
|
|
|
|
width: 67.4rem;
|
2026-02-25 15:12:04 +08:00
|
|
|
|
margin-right: 2.7rem;
|
2026-02-10 13:05:24 +08:00
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 2rem;
|
|
|
|
|
|
box-shadow: 0px 15px 21px 0px #0000000d;
|
|
|
|
|
|
|
|
|
|
|
|
.agent-header {
|
|
|
|
|
|
height: 7.4rem;
|
|
|
|
|
|
border-bottom: 0.1rem solid #c9c9c9;
|
|
|
|
|
|
padding: 1.4rem 3.4rem 1.4rem 3.1rem;
|
|
|
|
|
|
|
|
|
|
|
|
.agent-title {
|
|
|
|
|
|
font-size: 1.8rem;
|
|
|
|
|
|
margin-bottom: 0.6rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.agent-name {
|
|
|
|
|
|
font-size: 1.4rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.agent-body {
|
|
|
|
|
|
padding: 3.2rem;
|
2026-02-11 16:32:38 +08:00
|
|
|
|
overflow: hidden;
|
2026-02-24 13:53:01 +08:00
|
|
|
|
row-gap: 2.4rem;
|
2026-02-10 13:05:24 +08:00
|
|
|
|
.assist-input-wrapper {
|
|
|
|
|
|
width: 100%;
|
2026-03-06 13:26:14 +08:00
|
|
|
|
// height: 14.4rem;
|
2026-02-10 13:05:24 +08:00
|
|
|
|
min-height: 14.4rem !important;
|
2026-03-06 13:26:14 +08:00
|
|
|
|
max-height: 18.1rem !important;
|
2026-02-10 13:05:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.input-wrapper {
|
|
|
|
|
|
height: 14.4rem;
|
|
|
|
|
|
padding: 0 2rem 3rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|