From 9fbeb408fc0739523531b3ba95d09cf8370716d2 Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Wed, 25 Feb 2026 11:00:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=20=E5=88=9B=E5=BB=BA=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 3 +- src/api/agent.ts | 16 +- src/assets/images/sender.png | Bin 1670 -> 295 bytes src/stores/index.ts | 1 + src/utils/request.ts | 241 +++++++++++----------- src/views/home/agent/components/Agent.vue | 134 +++++++++--- src/views/home/agent/components/Item.vue | 4 +- src/views/home/agent/components/List.vue | 4 +- src/views/home/components/Input.vue | 58 ++++-- 9 files changed, 288 insertions(+), 173 deletions(-) diff --git a/.env.development b/.env.development index 4214b50..2cde04e 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,5 @@ # VITE_APP_URL = http://192.168.31.82:8771 -VITE_APP_URL = http://18.167.251.121:10015 +# VITE_APP_URL = http://18.167.251.121:10015 # VITE_APP_URL = http://192.168.31.118:8080 +VITE_APP_URL = http://192.168.31.82:8755 VITE_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com diff --git a/src/api/agent.ts b/src/api/agent.ts index d233a5f..ac409b4 100644 --- a/src/api/agent.ts +++ b/src/api/agent.ts @@ -3,8 +3,8 @@ import request from '@/utils/request' // 对话 export interface AgentParamsType { message: string // 消息 - projectID: string // - versionID?: string // + projectID: string // + versionID?: string // imageUrlList?: string[] // 图片URL列表 configParams: Record // 其他配置参数 token: string @@ -13,7 +13,17 @@ export const fetchAgentReply = (data: AgentParamsType): Promise = return request({ url: '/api/ai-design/chat', method: 'get', - data, + data, meta: { responseAll: true } }) } + +export interface CreateProjectParamsType { + type: string + region: string + style: string + temperature: number | string +} +export const createProject = (data: CreateProjectParamsType): Promise => { + return request({ url: '/api/project/init', method: 'post', data }) +} diff --git a/src/assets/images/sender.png b/src/assets/images/sender.png index 0579e400ae739803cf4b5dcb22a50fbf521867d2..5bc468f615d2802deb36fbeef63bb13d028a8f06 100644 GIT binary patch delta 274 zcmZqUUCw0I8Q|y6%O%Cdz`(%k>ERLtr1?RZgAGVpUz_R$q&N#aB8wRqxP?KOkzv*x ziHTjgdeaOTFNhE+m@5ukd=$anMkwb;n#UmVYHZ)^6d)dVaOunJ%(qUq$vD_Z3{{ zZp=#eGuJR|qWM|1>zmb8%*%f3#9r^V1$z--qRE zIy#@ut)8{kDXH=~!+wiUU2#5v(#ttJm)y&-T>P%#$fi{j-2FZ^*!8pry^=k@e?E81 V{@dI(>OlW7c)I$ztaD0e0sudGXuJRb delta 1660 zcmV-?27~#h0)`DXiBL{Q4GJ0x0000DNk~Le0000$0000$2nGNE0IF$m-T(jq32;bR za{vGqB>(^xB>_oNB=C_oAAbfLNklp}zhM!A1=PafDgK+|%jY_T_I)uW0a=5hA}#&+8zLsM z0H}N}g$c|fL3w_ef(;}XpaTDowWypCIsmZ<@1(}TC?b(q4jYJ20Z?ah^ZJg&C%o+g z7)Nl^xPk#0C;;koE`QDMTm?}U!+-=Lk;o3>h6MUV-}MBo4~=+%D9u4At5f30KHu8U z$!a0@CI7QLhMBr5(8bcZpr;280NZ|*@BA!A0#&5N((xc?1_}V*d9i#3qXahzS}N`L zJI%Kb(06{+#0&hHxxZTVJFRB`Sp3JMV|y=XR`*-I&ggcaF@MG{V;p#_^7L70#p{q~ z0Q5L7L)}&-IMGv+CgGJQcsBu}8yFw)LJIYI(d$4*0C*-h6M67(U-3c;+tK2JO$X?^zl}RSHDql+wF}tuve~|00V;nY zBrPsTZtf6jz5yO?U_cxf==nm@b)bN)q?c$nFHN)f z|2YW0NnQ{1giZ%QHvzqdC+lu;6GXIFe)7f6{pXK`ae*wtW@U}hIDpiwrS;KHf{MO( z7r4#o$f38q1Mu8r$o@bUZP5sT;>8V{`ui1K``7GyV}Fj{H{Co(tUs{>kw7E?I7yAP zw}Pk{RJ238_-owpz4ngT{*TZ7{9S~GSCW!E^zGyJ5DgRCfmdH2T(m>G`0p;Yw$OW_ zhr$Lbo8*OqOeF1JggbYrogkteyu`2b``$j)+_!!mkx*9|?gJ7TgtfqdgPr305$(`T zytMm0uYZ_*V*g514gfa+)YG9Iu;2BeZ~hG2x)xltLpSk)lL-ou*&iZX0zl!jzh`%v z=LZ8OD80lByIlc@%KjQk$z&*vu!wNs=^fr^i72>r)qGIN&IHNV<>7RK?V^Mr;(rBe_BTZ10Ljq4dLQ`ylCi0S-LAt1~~xrm{)?_Vx)iY zA`bg&H~@69RD~G{;z9ZZXQ=Gvf;j9CRh6&GOdde0ggCkF>ioQ15QqI?oNoe9r+5P( znd8I8{6Soh+Sxo7m;DpCVxAaXfk~(#E`LP)&8yAVZWqL5|4&P6veit5m5R3(!0iIL z)4<#p%}mjr{U!5Vb6#u)fC{DBmbl=)$AYv3 z=kiCef>K3yp+xJ~|5RF=N2EyQd22;!M-Hq$%+2$tFn=Dk zLG2J`=|XE=v3HHoLhi;;mBK~>_xy{c$@ab83nB`~W9<-aBU;${UWYsbpwnYg1&j+y z6}nZDcU=5={SUpi { - removePending(config) - // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求 - !config.repeatRequest && addPending(config) - // 打开loading - if (config.loading) { - LoadingInstance._count++ - if (LoadingInstance._count === 1) { - openLoading(config.loadingDom) - } - } - // 如果登录了,有token,则请求携带token - // Do something before request is sent - const token = useUserInfoStore().state.token - if (token) { - config.headers.Authorization = token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 - // config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 - } - return config - }, - (error) => { - // Do something with request error - console.log(error) // for debug - Promise.reject(error) - } + (config: any) => { + removePending(config) + // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求 + !config.repeatRequest && addPending(config) + // 打开loading + if (config.loading) { + LoadingInstance._count++ + if (LoadingInstance._count === 1) { + openLoading(config.loadingDom) + } + } + // 如果登录了,有token,则请求携带token + // Do something before request is sent + const token = useUserInfoStore().state.token + if (token) { + config.headers.Authorization = 'Bearer ' + token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 + // config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 + } + return config + }, + (error) => { + // Do something with request error + console.log(error) // for debug + Promise.reject(error) + } ) // respone拦截器 service.interceptors.response.use( - // response => response, - /** - * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 - * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 - */ - (response: any) => { - // 如果是llm/streamChat这样的流式接口,不走这样的处理 - if (response.config.url.includes('llm/streamChat')) { - return response - } - - // 已完成请求的删除请求中数组 - removePending(response.config) - // 关闭loading - if (response.config.loading) { - closeLoading() - } - const res = response.data - // 处理异常的情况 - // console.log(res) - if (res.code != 200) { - ElMessage.error(res.message) - return Promise.reject(new Error(res.errMsg || res.message || 'error')) - } else { - // 默认只返回data,不返回状态码和message - // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data) - const isbackAll = response.config.meta && response.config.meta.responseAll - if (isbackAll) { - return res - } else { - return res.data - } - } - }, - (error) => { - if(error?.response){ - if (error.config?.loading) closeLoading() // 关闭loading - if(error?.response?.status === 401){//如果是记录浏览器页面就不跳转login - // showConfirmDialog({ - // title: '确定登出', - // message: '你已被登出,可以取消继续留在该页面,或者重新登录', - // confirmButtonText: '重新登录', - // cancelButtonText: '取消' - // }).then(() => { - // store.loginOut().then(() => { - // location.reload() // 为了重新实例化vue-router对象 避免bug - // }) - // }) - // showToast({ - // message: 'Please log in and try again.', - // duration: 5000 - // }) - // router.push('/login') - // useGenerateStore().clearGenerateData() - return Promise.reject(false) + // response => response, + /** + * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 + * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 + */ + (response: any) => { + // 如果是llm/streamChat这样的流式接口,不走这样的处理 + if (response.config.url.includes('llm/streamChat')) { + return response } - error.config && removePending(error.config) - console.log('err' + error) // for debug - ElMessage.error(error.message) + + // 已完成请求的删除请求中数组 + removePending(response.config) + // 关闭loading + if (response.config.loading) { + closeLoading() + } + const res = response.data + // 处理异常的情况 + // console.log(res) + if (res.code != 200) { + ElMessage.error(res.message) + return Promise.reject(new Error(res.errMsg || res.message || 'error')) + } else { + // 默认只返回data,不返回状态码和message + // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data) + const isbackAll = response.config.meta && response.config.meta.responseAll + if (isbackAll) { + return res + } else { + return res.data + } + } + }, + (error) => { + if (error?.response) { + if (error.config?.loading) closeLoading() // 关闭loading + if (error?.response?.status === 401) { + //如果是记录浏览器页面就不跳转login + // showConfirmDialog({ + // title: '确定登出', + // message: '你已被登出,可以取消继续留在该页面,或者重新登录', + // confirmButtonText: '重新登录', + // cancelButtonText: '取消' + // }).then(() => { + // store.loginOut().then(() => { + // location.reload() // 为了重新实例化vue-router对象 避免bug + // }) + // }) + // showToast({ + // message: 'Please log in and try again.', + // duration: 5000 + // }) + // router.push('/login') + // useGenerateStore().clearGenerateData() + return Promise.reject(false) + } + error.config && removePending(error.config) + console.log('err' + error) // for debug + ElMessage.error(error.message) + } + return Promise.reject(error) } - return Promise.reject(error) - } ) // --------------------------------取消接口重复请求的函数----------------------------------- @@ -133,10 +134,10 @@ const pendingMap = new Map() * @returns string */ function getPendingKey(config: any) { - const { url, method, params } = config - let { data } = config - if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 - return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&') + const { url, method, params } = config + let { data } = config + if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 + return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&') } /** @@ -144,39 +145,39 @@ function getPendingKey(config: any) { * @param {*} config */ function addPending(config: any) { - const pendingKey = getPendingKey(config) - config.cancelToken = - config.cancelToken || - new axios.CancelToken((cancel) => { - if (!pendingMap.has(pendingKey)) { - pendingMap.set(pendingKey, cancel) - } - }) + const pendingKey = getPendingKey(config) + config.cancelToken = + config.cancelToken || + new axios.CancelToken((cancel) => { + if (!pendingMap.has(pendingKey)) { + pendingMap.set(pendingKey, cancel) + } + }) } /** * 删除重复的请求 * @param {*} config */ function removePending(config: any) { - const pendingKey = getPendingKey(config) - if (pendingMap.has(pendingKey)) { - const cancelToken = pendingMap.get(pendingKey) - cancelToken(pendingKey) - pendingMap.delete(pendingKey) - } + const pendingKey = getPendingKey(config) + if (pendingMap.has(pendingKey)) { + const cancelToken = pendingMap.get(pendingKey) + cancelToken(pendingKey) + pendingMap.delete(pendingKey) + } } // ----------------------------------loading的函数------------------------------- const LoadingInstance: { _count: number } = { - _count: 0 + _count: 0 } function openLoading(loadingDom: any) { - useGlobalStore().setLoading(true) + useGlobalStore().setLoading(true) } function closeLoading() { - if (LoadingInstance._count > 0) LoadingInstance._count-- - if (LoadingInstance._count === 0) { - useGlobalStore().setLoading(false) - } + if (LoadingInstance._count > 0) LoadingInstance._count-- + if (LoadingInstance._count === 0) { + useGlobalStore().setLoading(false) + } } export default service diff --git a/src/views/home/agent/components/Agent.vue b/src/views/home/agent/components/Agent.vue index 55518a4..32a7f7b 100644 --- a/src/views/home/agent/components/Agent.vue +++ b/src/views/home/agent/components/Agent.vue @@ -8,8 +8,13 @@
- - + +
@@ -20,7 +25,7 @@ import Input from '../../components/Input.vue' import { fetchAgentReply } from '@/api/agent' import type { AgentParamsType } from '@/api/agent' - import { useUserInfoStore,useProjectStore } from '@/stores' + import { useUserInfoStore, useProjectStore } from '@/stores' import { useAgentStore } from '@/stores/agent' const userStore = useUserInfoStore() @@ -37,8 +42,10 @@ const messageList = ref([]) const listRef = ref() + const isGenerating = ref(false) + const isPaused = ref(false) // 标记是否为主动暂停 const params = reactive({ - projectID: '1', + projectID: projectStore.state.id, message: '', token: userStore.state.token, versionID: '', @@ -51,22 +58,32 @@ imageUrlList: [] }) - const abort = new AbortController() + // 每次请求时创建新的 AbortController + let abort: AbortController + + const createAbortController = () => { + // 如果已有未完成的中止,先中止它 + if (abort) { + abort.abort() + } + abort = new AbortController() + return abort + } onUnmounted(() => { - abort.abort() + abort?.abort() }) onMounted(() => { // 检查 store 中是否有初始项目数据 - projectStore.setId('1') // 临时设置项目ID为1,实际应用中应根据上下文动态设置 + // projectStore.setId('1') // 临时设置项目ID为1,实际应用中应根据上下文动态设置 const initialData = agentStore.getInitialProjectData if (initialData) { // 等待页面渲染完成后自动发送初始消息 params.configParams = { - type: initialData.type || 'Chair', - region: initialData.area || 'China', - style: initialData.style || 'Transitional', + type: initialData.type , + region: initialData.area, + style: initialData.style , temperature: 0.7 } handleSendMessage({ @@ -83,15 +100,21 @@ const handleSendMessage = async (message: { text: string images: Array<{ url: string; name: string }> - }) => { + }, skipUserMessage = false) => { console.log('Message sent:', message) + isPaused.value = false + isGenerating.value = true params.message = message.text params.imageUrlList = message.images || [] - messageList.value.push({ - id: messageList.value.length + 1, - text: message.text, - isUser: true - }) + + // 如果不是重新生成模式,则添加用户消息到列表 + if (!skipUserMessage) { + messageList.value.push({ + id: messageList.value.length + 1, + text: message.text, + isUser: true + }) + } // Add AI loading message const aiMessage = reactive({ @@ -106,6 +129,9 @@ }) messageList.value.push(aiMessage) + // 创建新的 AbortController + const abortController = createAbortController() + // console.log('token---', params.token, '参数---', params) try { @@ -116,7 +142,7 @@ const response = await fetch(`/api/ai-design/chat?${urlParams.toString()}`, { method: 'GET', - signal: abort.signal + signal: abortController.signal }) // 检查响应内容类型,判断是否为流式响应 @@ -128,9 +154,12 @@ // 非流式错误响应,使用 text() 读取错误信息 const errorText = await response.text() console.error('请求错误:', errorText) - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false return } @@ -143,9 +172,12 @@ } catch (e) { console.error('非流式响应文本:', text) } - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false return } @@ -181,26 +213,26 @@ // 过滤掉 id: 等字段,只取 data: let isNodeIdEvent = false - if(event.startsWith('event:')){ + if (event.startsWith('event:')) { isNodeIdEvent = true // continue } - + const dataLines = event - .split(/\n/) - .filter((line) => line.startsWith('data:')) - .map((line) => line.replace(/^data:\s*/, '').trim()) - console.log('dataLInes',dataLines); - if(isNodeIdEvent){ + .split(/\n/) + .filter((line) => line.startsWith('data:')) + .map((line) => line.replace(/^data:\s*/, '').trim()) + console.log('dataLInes', dataLines) + if (isNodeIdEvent) { params.versionID = dataLines[0] } - + if (dataLines.length === 0) continue const jsonText = dataLines.join('\n') try { const jsonData = JSON.parse(jsonText) - + // 赋值 project_id 和 version_id if (jsonData.project_id) params.projectID = jsonData.project_id if (jsonData.version_id) params.versionID = jsonData.version_id @@ -215,6 +247,7 @@ if (jsonData.type === 'end') { aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false flag = false break } @@ -224,6 +257,7 @@ console.log('结束-----------------------') aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false flag = false break } @@ -239,19 +273,59 @@ } } catch (error) { console.error('流式传输错误:', error) - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false } finally { reader.releaseLock() } } catch (error) { console.error('fetch请求失败:', error) - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false } } + + const handlePause = () => { + isPaused.value = true + isGenerating.value = false + abort?.abort() + } + + 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 + ) + }