Open source iOS Coverflow

本文提供了三个链接资源,分别涉及OpenFlow项目源码、Chaos in Motion的FlowCover示例代码及CookbookSamples项目的下载列表。这些资源对于理解网络编程中的OpenFlow协议及如何使用FlowCover进行数据流覆盖分析具有一定的参考价值。
我是用的uniapp开发的app项目,代码如下:<route lang="json5" type="home"> { layout: 'default', style: { navigationBarTitleText: 'AI聊天', navigationStyle: 'custom', disableScroll: true, 'app-plus': { bounce: 'none', }, }, } </route> <template> <PageLayout :navTitle="navTitle" type="popup" :navBgTransparent="true" :navLeftArrow="false" :enableDropdown="true" :arrowIcon="arrowIcon" @navBack="drawerShow = true" @navRight="handleNewConversation" @dropdownClick="handleDropdownClick"> <template #navLeft> <image :src="moreList" style="width: 24px; height: 24px;" /> </template> <template #navRight> <image :src="addConv" style="width: 24px; height: 24px;" /> </template> <!-- 自定义下拉框内容 --> <!-- <template #dropdown> <view class="dropdown-content"> <text>知识库</text> </view> </template> --> <!-- 页面内容 --> <view class="wrap"> <view v-if="isAIAnswering" class="stop-answer-btn" @click="handleStopAnswer"> <text>停止回答</text> </view> <z-paging ref="paging" v-model="dataList" :fixed="false" :auto="false" :paging-enabled="false" use-chat-record-mode use-virtual-list cell-height-mode="dynamic" safe-area-inset-bottom bottom-bg-color="#e5e5e5"> <!-- 自定义空数据视图 --> <template #empty> <view class="empty-container"> <view> <image :src="logo" class="empty-logo" mode="aspectFit" /> </view> <view class="empty-title">欢迎使用水利安全生产知识库</view> <view class="empty-subtitle">我能理解人类语言、生成内容,是你的问答智能助手。</view> </view> </template> <template #cell="{ item }"> <view style="transform: scaleY(-1)"> <ai-chat-item :item="item" :isAnswering="isAIAnswering && item.sendTimeId === currentAIMessageId" @copy="handleCopy" @regenerate="handleRegenerate" /> </view> </template> <template #bottom> <ai-chat-input-bar ref="inputBar" @send="doSend" @sendImage="handleImage" /> </template> </z-paging> </view> <!-- 左侧弹出框组件 --> <ai-chat-history v-model="drawerShow" :conversationList="conversationList" :currentConversationId="conversationId" @select="handleSelectConversation" /> </PageLayout> </template> <script lang="ts" setup> import { ref, onMounted, onBeforeUnmount } from 'vue' import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app' import { useUserStore } from '@/store/user' import { http } from '@/utils/http' import { useToast } from 'wot-design-uni' import { useParamsStore } from '@/store/page-params' import { formatDate } from '@/common/uitls' import aiChatInputBar from './components/AiChatInputBar.vue' import aiChatItem from './components/AiChatItem.vue' import aiChatHistory from './components/AiChatHistory.vue' import logo from '@/static/logo.png' import moreList from '@/static/more-list.png'; import addConv from '@/static/add-conv.png'; import arrowIcon from '@/static/arrow-right.png' import qs from 'qs' import signMd5Utils from '@/utils/signMd5Utils' // #ifdef APP-PLUS import { EventSourcePolyfill } from 'event-source-polyfill' // #endif defineOptions({ name: 'aiChat', options: { styleIsolation: 'apply-shared', }, }) // API 接口定义 const api = { sendChat: '/airag/chat/send', getConversations: '/airag/chat/conversations', getMessages: '/airag/chat/messages', stopChat: '/airag/chat/stop', uploadUrl: '/sys/common/upload', } const toast = useToast() const userStore = useUserStore() const paramsStore = useParamsStore() const paging = ref(null) const inputBar = ref(null) const navTitle = ref('新对话') const dataList = ref([]) const conversationId = ref('') const appId = ref('1980557694009438210') const requestId = ref('') const drawerShow = ref(false) const conversationList = ref([]) let eventSource = null // EventSource 实例 let currentAIMessageId = ref('') // 当前 AI 消息的 ID let currentAIMessage = ref('') // 累积的 AI 消息内容 // 添加状态变量 const isAIAnswering = ref(false) const handleDropdownClick = () => { console.log('下拉框被点击') // 处理下拉框点击事件 } // 页面初始化 const init = async () => { const localData = paramsStore.getPageParams('aiChat') const params = localData?.data if (params?.appId) { appId.value = params.appId } if (params?.title) { navTitle.value = params.title } await loadConversation() // 如果没有会话ID,手动触发空状态 if (!conversationId.value) { paging.value?.complete([]) } } // 加载会话列表 const loadConversation = async () => { try { // 添加您需要的参数 const params = { appId: appId.value, // 其他参数... } const res = await http.get(api.getConversations, params) if (res.success && res.result?.length) { conversationList.value = res.result || [ { "id": "2c9180839a09dce2019a0ef293460037", "title": "消防安全的", "messages": null, "app": null, "createTime": "2025-10-24 09:43:16" }, { "id": "237A8A0151", "title": "啥是电影蒙发更广泛的", "messages": null, "app": null, "createTime": "2025-10-23 14:53:00" } ] conversationId.value = res.result[0].id } } catch (e) { console.error('加载会话失败', e) toast.error('加载会话失败') } } // 加载全部消息(不分页) const loadAllMessages = async () => { if (!conversationId.value) { dataList.value = [] return } try { const res = await http.get(`${api.getMessages}/${conversationId.value}`) if (res.success) { dataList.value = analysis(res.result || []) } else { dataList.value = [] toast.error(res.message || '加载消息失败') } } catch (e) { console.error('加载消息失败', e) dataList.value = [] toast.error('加载消息失败') } } // 数据分析处理 const analysis = (data: any[]) => { return data.map((item) => { const time = formatDate(item.sendTime || new Date(), 'yyyy-MM-dd hh:mm:ss') const timeStr = String(time || '') const id = timeStr.replace(/:/g, '').replace(/-/g, '').replace(' ', '') return { ...item, sendTimeId: id, msgFrom: item.role === 'user' ? userStore.userInfo.userid : 'ai', fromUserName: item.role === 'user' ? userStore.userInfo.realname : 'AI助手', fromAvatar: item.role === 'user' ? userStore.userInfo.avatar : '', msgData: item.content, msgType: item.images?.length ? 'image' : 'text', sendTime: timeStr, } }) } // 发送消息到界面 const send = ({ msgData, msgType, isAI = false, messageId = '' }) => { const time = formatDate(new Date(), 'yyyy-MM-dd hh:mm:ss') const timeStr = String(time || '') const id = messageId || timeStr.replace(/:/g, '').replace(/-/g, '').replace(' ', '') paging.value?.addChatRecordData([ { fromUserName: isAI ? 'AI助手' : userStore.userInfo.realname, msgFrom: isAI ? 'ai' : userStore.userInfo.userid, msgData, fromAvatar: isAI ? '' : userStore.userInfo.avatar, sendTime: timeStr, sendTimeId: id, msgType, }, ]) return id } // 处理新建会话 const handleNewConversation = () => { // 1. 检查是否已经是新会话(没有 conversationId 或 dataList 为空) if (!conversationId.value || dataList.value.length === 0) { toast.info('已经在新会话中') return } // 2. 如果正在回答中,先停止回答 if (isAIAnswering.value) { handleStopAnswer() } // 3. 清空所有相关数据 clearConversationData() // 4. 显示空状态页面 paging.value?.complete([]) } // 清空会话数据 const clearConversationData = () => { // 清空会话 ID conversationId.value = '' // 清空消息列表 dataList.value = [] // 关闭 EventSource 连接 closeEventSource() // 重置 AI 回答状态 isAIAnswering.value = false currentAIMessageId.value = '' currentAIMessage.value = '' requestId.value = '' // 重置标题为"新对话" navTitle.value = '新对话' } // 更新 AI 消息(用于流式显示) const updateAIMessage = (messageId: string, content: string) => { // 查找并更新对应的消息 const index = dataList.value.findIndex(item => item.sendTimeId === messageId) if (index !== -1) { dataList.value[index].msgData = content } } // 关闭 EventSource 连接 const closeEventSource = () => { if (eventSource) { eventSource.close() eventSource = null } } // 发送文本消息(使用 EventSource) const doSend = async (textMsg: string) => { const content = textMsg.trim() if (!content) return send({ msgData: content, msgType: 'text', isAI: false }) // 关闭之前的连接 closeEventSource() try { const params = { appId: appId.value, content, conversationId: conversationId.value || '2c9180839a09dce2019a0ef293460037', images: JSON.stringify([]), responseMode: 'streaming', topicId: '2c9180839a09dce2019a0ef293440036', } // ✅ 在条件编译之前声明 url 变量 let url = '' // #ifdef H5 // H5 端使用原生 EventSource const queryParams = new URLSearchParams(params) url = `/jeecgboot${api.sendChat}?${queryParams.toString()}` eventSource = new EventSource(url) // #endif // #ifdef APP-PLUS // APP 端使用 EventSourcePolyfill const queryString = qs.stringify(params) url = `/jeecgboot${api.sendChat}?${queryString}` console.log('app端>>>', url) let sign = signMd5Utils.getSign(url, params) let vSign = signMd5Utils.getVSign(params, sign) eventSource = new EventSourcePolyfill(url, { headers: { 'X-Access-Token': userStore.userInfo.token, 'X-Tenant-Id': userStore.userInfo.tenantId, // 'X-Sign': sign, // 'V-Sign': vSign, // 'X-TIMESTAMP': signMd5Utils.getTimestamp(), }, heartbeatTimeout: 120000, }) // #endif isAIAnswering.value = true currentAIMessage.value = '' currentAIMessageId.value = '' // 监听消息事件 eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data) console.log('收到 SSE 消息:', data) handleSSEMessage(data) } catch (e) { console.error('解析 SSE 消息失败', e) } } // 监听错误事件 eventSource.onerror = (error) => { console.error('SSE 连接错误', error) closeEventSource() isAIAnswering.value = false if (!currentAIMessage.value) { toast.error('连接失败,请重试') } } } catch (e) { console.error('发送消息失败', e) toast.error('发送失败') isAIAnswering.value = false } } const doSend2 = async (textMsg: string) => { // #ifdef H5 await doSendForH5(textMsg) // #endif // #ifdef APP-PLUS await doSendForApp(textMsg) // #endif } // APP 端使用 uni.request 解析 SSE 流 const doSendForApp = async (textMsg: string) => { const content = textMsg.trim() if (!content) return send({ msgData: content, msgType: 'text', isAI: false }) try { const params = { appId: appId.value, content, conversationId: conversationId.value || '2c9180839a09dce2019a0ef293460037', images: JSON.stringify([]), responseMode: 'streaming', topicId: '2c9180839a09dce2019a0ef293440036', } // 使用 qs.stringify 构建查询字符串 const queryString = qs.stringify(params) const url = `/jeecgboot${api.sendChat}?${queryString}`; // let params = options.query // if (options.data && Object.keys(options.data).length > 0) { // params = Object.assign({}, options?.query, options?.data) // } let sign = signMd5Utils.getSign(url, params) let vSign = signMd5Utils.getVSign(params, sign) isAIAnswering.value = true currentAIMessage.value = '' currentAIMessageId.value = '' console.log('请求地址>>>', url) const downloadTask = uni.downloadFile({ url: `/jeecgboot${api.sendChat}?${queryString}`, header: { 'X-Access-Token': userStore.userInfo.token, 'X-Tenant-Id': userStore.userInfo.tenantId, 'Accept': 'text/event-stream', 'X-Sign': sign, 'V-Sign': vSign, 'X-TIMESTAMP': signMd5Utils.getTimestamp(), }, success: (res) => { console.log('下载完成', res) if (res.statusCode === 200) { uni.getFileSystemManager().readFile({ filePath: res.tempFilePath, encoding: 'utf8', success: (fileRes) => { const content = fileRes.data parseSSEChunk(content) } }) } }, fail: (err) => { console.error('请求失败', err) toast.error('连接失败,请重试') isAIAnswering.value = false } }) downloadTask.onProgressUpdate((res) => { console.log('下载进度', res.progress) }) } catch (e) { console.error('发送消息失败', e) toast.error('发送失败') isAIAnswering.value = false } } // 解析 SSE 数据块 let sseBuffer = '' const parseSSEChunk = (chunk: string) => { sseBuffer += chunk // 按行分割 const lines = sseBuffer.split('\n') sseBuffer = lines.pop() || '' // 保留最后一个不完整的行 for (const line of lines) { if (line.startsWith('data:')) { try { const jsonStr = line.substring(5).trim() if (jsonStr) { const data = JSON.parse(jsonStr) handleSSEMessage(data) } } catch (e) { console.error('解析 SSE 消息失败', e, line) } } } } const doSendForH5 = async (textMsg: string) => { const content = textMsg.trim() if (!content) return // 先在界面显示用户消息 send({ msgData: content, msgType: 'text', isAI: false }) // 关闭之前的连接 closeEventSource() try { const token = uni.getStorageSync('token') // 构建 URL 参数 const params = new URLSearchParams({ appId: appId.value, content, conversationId: conversationId.value || '2c9180839a09dce2019a0ef293460037', images: JSON.stringify([]), responseMode: 'streaming', topicId: '2c9180839a09dce2019a0ef293440036', }) const url = `/jeecgboot${api.sendChat}?${params.toString()}` // 创建 EventSource 连接 eventSource = new EventSource(url) // 重置当前消息 currentAIMessage.value = '' currentAIMessageId.value = '' // 监听消息事件 eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data) console.log('收到 SSE 消息:', data) handleSSEMessage(data) } catch (e) { console.error('解析 SSE 消息失败', e) } } // 监听错误事件 eventSource.onerror = (error) => { console.error('SSE 连接错误', error) closeEventSource() // 如果还没有收到任何消息,显示错误提示 if (!currentAIMessage.value) { toast.error('连接失败,请重试') } } } catch (e) { console.error('发送消息失败', e) toast.error('发送失败') } } // 处理 SSE 消息 const handleSSEMessage2 = (data: any) => { switch (data.event) { case 'INIT_REQUEST_ID': // 初始化请求 ID requestId.value = data.requestId if (data.conversationId) { conversationId.value = data.conversationId } console.log('初始化请求:', data) break case 'MESSAGE': // 流式消息块 const message = data.data?.message || '' currentAIMessage.value += message // 如果是第一次收到消息,创建 AI 消息项 if (!currentAIMessageId.value) { currentAIMessageId.value = send({ msgData: currentAIMessage.value, msgType: 'text', isAI: true, }) } else { // 更新现有消息 updateAIMessage(currentAIMessageId.value, currentAIMessage.value) } break case 'MESSAGE_END': // 消息结束 console.log('消息接收完成') closeEventSource() // 重置状态 currentAIMessage.value = '' currentAIMessageId.value = '' break case 'ERROR': case 'FLOW_ERROR': // 错误处理 const errorMsg = data.data?.message || '发送失败' toast.error(errorMsg) closeEventSource() // 重置状态 currentAIMessage.value = '' currentAIMessageId.value = '' break default: console.warn('未知的消息类型:', data.event) } } // 修改 handleSSEMessage 函数 const handleSSEMessage = (data: any) => { switch (data.event) { case 'INIT_REQUEST_ID': requestId.value = data.requestId if (data.conversationId) { conversationId.value = data.conversationId } isAIAnswering.value = true // 开始回答 break case 'MESSAGE': const message = data.data?.message || '' currentAIMessage.value += message if (!currentAIMessageId.value) { currentAIMessageId.value = send({ msgData: currentAIMessage.value, msgType: 'text', isAI: true, }) } else { updateAIMessage(currentAIMessageId.value, currentAIMessage.value) } break case 'MESSAGE_END': console.log('消息接收完成') closeEventSource() isAIAnswering.value = false // 回答结束 currentAIMessage.value = '' currentAIMessageId.value = '' break case 'ERROR': case 'FLOW_ERROR': const errorMsg = data.data?.message || '发送失败' toast.error(errorMsg) closeEventSource() isAIAnswering.value = false // 回答结束 currentAIMessage.value = '' currentAIMessageId.value = '' break } } // 停止回答 const handleStopAnswer = async () => { if (!requestId.value) return try { await http.post(api.stopChat, { requestId: requestId.value }) closeEventSource() isAIAnswering.value = false toast.success('已停止回答') } catch (e) { console.error('停止回答失败', e) toast.error('停止失败') } } // 复制消息 const handleCopy = (item: any) => { uni.setClipboardData({ data: item.msgData.replace(/<[^>]+>/g, ''), success: () => { // toast.success('复制成功') } }) } // 重新生成 const handleRegenerate = (item: any) => { // 找到该消息之前的用户消息 const index = dataList.value.findIndex(i => i.sendTimeId === item.sendTimeId) if (index > 0) { const userMsg = dataList.value[index - 1] if (userMsg.msgFrom === userStore.userInfo.userid) { doSend(userMsg.msgData) } } } // 处理图片上传 const handleImage = async (type: string) => { uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: [type], success: async (res) => { uni.showLoading({ title: '上传中...' }) try { const uploadRes = await uni.uploadFile({ url: http.baseUrl + api.uploadUrl, filePath: res.tempFilePaths[0], name: 'file', header: { 'X-Access-Token': uni.getStorageSync('token'), }, }) const data = JSON.parse(uploadRes.data) if (data.success) { send({ msgData: data.message, msgType: 'image' }) } else { toast.error('上传失败') } } catch (e) { toast.error('上传失败') } finally { uni.hideLoading() } }, }) } // 选择会话 const handleSelectConversation = (item) => { conversationId.value = item.id navTitle.value = item.title || 'AI助手' loadAllMessages() // 改为调用新的加载函数 } init() onLoad(() => { uni.hideTabBar() }) onShow(() => { uni.hideTabBar() }) onMounted(() => { }) onBeforeUnmount(() => { // 页面卸载时关闭 EventSource 连接 closeEventSource() }) </script> <style lang="scss" scoped> :deep(.pageContent) { background: transparent !important; } .wrap { height: 100%; background-image: url('@/static/bgImg.png'); background-size: cover; background-position: center; background-repeat: no-repeat; .z-paging-content { background: transparent; // 移除原有的渐变背景 } .stop-answer-btn { position: fixed; bottom: 400rpx; // 输入框上方 left: 50%; transform: translateX(-50%); width: 208rpx; height: 64rpx; border-radius: 40rpx; background: #FFFFFF; border: 2rpx solid #E2E2E2; display: flex; justify-content: center; align-items: center; padding: 12rpx 24rpx; box-sizing: border-box; z-index: 100; text { font-size: 28rpx; color: #333; } &:active { background: #f5f5f5; } } .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; .empty-logo { width: 67.74upx; height: 80upx; margin-bottom: 24upx; } .empty-title { font-family: Alibaba PuHuiTi 3.0; font-size: 40upx; font-weight: 600; line-height: normal; letter-spacing: 0px; color: rgba(0, 0, 0, 0.9); margin-bottom: 24upx; } .empty-subtitle { font-family: Alibaba PuHuiTi 3.0; font-size: 28upx; font-weight: normal; line-height: 48upx; text-align: center; color: rgba(0, 0, 0, 0.6); } } } :deep(.zp-page-bottom-container) { background: transparent !important; margin: 24upx; } </style> 现在在H5端正常,但是app端报错:发送消息失败, TypeError: XMLHttpRequest2 is not a constructor at pages/knowledge/AiKnowledgeManagerList.vue
10-28
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值