25 Commits

Author SHA1 Message Date
685c6d0910 feat: 根据需要处理H5传递过来的消息 2026-02-04 23:35:33 +08:00
03231e611c feat: 根据需要处理H5传递过来的消息 2026-02-04 23:20:17 +08:00
e917befe02 feat: 发送的话题字段更改 2026-02-04 22:46:45 +08:00
4e92ec2000 feat: 问卷字段调整和样式调整 2026-02-04 15:01:11 +08:00
2da15786c8 feat: webview 的导航栏样式处理 2026-02-04 14:33:35 +08:00
8e27c27abf feat: 兼容处理toolResult字段 2026-02-04 12:00:55 +08:00
76ead609d5 feat: 字段的调整和问卷跳转的处理 2026-02-04 11:59:12 +08:00
521a0bf4b3 feat: 问卷调查组件的字段调式 2026-02-03 22:47:43 +08:00
DEV_DSW
2d3cd6796e feat: 问卷组件新增跳转逻辑 2026-01-30 16:46:27 +08:00
e383bbbcf0 feat: 增加问卷调查组件 2026-01-27 17:05:24 +08:00
6f879a0bc8 feat: 导航栏的ip形象调整 2026-01-21 11:43:15 +08:00
d78368b4ec feat: IP形象的调整 2026-01-21 11:33:26 +08:00
226ddf8305 feat: 雪碧图组件调整 2026-01-20 22:52:43 +08:00
98c1d50ca5 feat: 实现多行多列雪碧图的动画加载 2026-01-20 22:46:55 +08:00
8211e0b91b feat: 改变了欢迎ip 的动态配置 2026-01-16 11:28:55 +08:00
17a05aab4c feat: 入住信息的样式优化 2026-01-16 10:26:22 +08:00
ff9227c3b8 feat: 环境地址配置 2026-01-16 10:25:55 +08:00
5aba9afdd5 feat: 调整商品的跳转问题 2026-01-16 09:36:44 +08:00
0351bd8dc6 feat: 更新版本号 2026-01-16 09:36:21 +08:00
3b8e604853 feat: 去掉无用的消息类型 2026-01-15 15:33:29 +08:00
fbb19c4603 feat: 智念的图替换 2026-01-13 16:38:58 +08:00
06ec029555 feat:重连调整 2026-01-08 17:01:47 +08:00
af615f666c feat: 消息与socket的优化调整 2026-01-08 16:26:10 +08:00
fb15b8aec1 feat: 发送消息的优化 2026-01-08 15:03:43 +08:00
7239313f0f feat: 优化发送消息的中断重连问题 2026-01-08 14:46:44 +08:00
23 changed files with 645 additions and 334 deletions

View File

@@ -6,14 +6,16 @@
"placeholder": "快告诉智念您在想什么~",
"loginDesc": "您好,欢迎来到智念科技",
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/dh_logo.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_large.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/zn/zn_large.png",
"ipLargeImageWidth": 395,
"ipLargeImageHeight": 335,
"ipLargeTotalFrames": 71,
"ipLargeColumns": 1,
"ipSmallImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_small.png",
"ipLargeImageHeight": 19687,
"ipSmallImageHeight": 3744,
"ipLargeImageStep": 147,
"ipSmallImageStep": 117,
"ipLargeTime": 4,
"ipSmallTime": 4
"ipSmallImageWidth": 128,
"ipSmallImageHeight": 128,
"ipSmallTotalFrames": 117,
"ipSmallColumns": 1
},
"duohua": {
"clientId": "2",
@@ -23,13 +25,15 @@
"loginDesc": "您好,欢迎来到朵花温泉",
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/dh_logo.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_large.png",
"ipLargeImageWidth": 263,
"ipLargeImageHeight": 210,
"ipLargeTotalFrames": 147,
"ipLargeColumns": 1,
"ipSmallImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_small.png",
"ipLargeImageHeight": 19687,
"ipSmallImageHeight": 3744,
"ipLargeImageStep": 147,
"ipSmallImageStep": 117,
"ipLargeTime": 4,
"ipSmallTime": 4
"ipSmallImageWidth": 128,
"ipSmallImageHeight": 128,
"ipSmallTotalFrames": 117,
"ipSmallColumns": 1
},
"tianmu": {
"clientId": "4",
@@ -39,12 +43,14 @@
"loginDesc": "您好,欢迎来到天沐温泉",
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/tm_logo.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/tm/tm_large.png",
"ipLargeImageWidth": 395,
"ipLargeImageHeight": 335,
"ipLargeTotalFrames": 71,
"ipLargeColumns": 1,
"ipSmallImage": "https://oss.nianxx.cn/mp/static/version_101/tm/tm_small.png",
"ipLargeImageHeight": 9514,
"ipSmallImageHeight": 4736,
"ipLargeImageStep": 71,
"ipSmallImageStep": 148,
"ipLargeTime": 4,
"ipSmallTime": 6
"ipSmallImageWidth": 128,
"ipSmallImageHeight": 128,
"ipSmallTotalFrames": 148,
"ipSmallColumns": 1
}
}

View File

@@ -1,6 +1,6 @@
<script setup>
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { getEvnUrl } from "@/request/api/config";
import { getEvnUrl } from "@/request/base/config";
import { refreshToken } from "@/hooks/useGoLogin";
import { getAccessToken } from "@/constant/token";

View File

@@ -0,0 +1,112 @@
<template>
<view class="sprite-animator" :style="spriteStyle" />
</template>
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue'
const props = withDefaults(defineProps<{
src: string
frameWidth: number
frameHeight: number
totalFrames: number
columns: number
displayWidth: number
fps?: number
autoplay?: boolean
loop?: boolean
}>(), {
fps: 12,
autoplay: true,
loop: true,
})
/* JS 精确逐帧方案(使用 setInterval确保整数偏移、无插值 */
const currentFrame = ref(0)
let intervalId: number | null = null
const play = () => {
stop()
const safeFps = Math.max(1, Math.floor(props.fps || 12))
const durationPerFrame = 1000 / safeFps
// 强制整数像素偏移,使用 setInterval 在各运行时更可靠
const setter = (globalThis as any).setInterval || setInterval
intervalId = setter(() => {
const safeTotal = Math.max(1, Math.floor(props.totalFrames || 0))
if (currentFrame.value >= safeTotal - 1) {
if (props.loop) {
currentFrame.value = 0
} else {
stop()
return
}
} else {
currentFrame.value++
}
}, durationPerFrame) as unknown as number
}
const stop = () => {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
onMounted(() => {
console.log('[SpriteAnimator] mounted', { autoplay: props.autoplay, fps: props.fps })
if (props.autoplay) play()
})
onUnmounted(() => stop())
const spriteStyle = computed(() => {
const {
src,
frameWidth: rawFrameWidth,
frameHeight: rawFrameHeight,
totalFrames: rawTotalFrames,
columns: rawColumns,
} = props
const frameWidth = Math.max(1, Math.floor(rawFrameWidth || 0))
const frameHeight = Math.max(1, Math.floor(rawFrameHeight || 0))
const totalFrames = Math.max(1, Math.floor(rawTotalFrames || 0))
let columns = Math.floor(Number(rawColumns) || 0)
if (!columns || columns <= 0) columns = Math.min(1, totalFrames)
columns = Math.min(columns, totalFrames)
const displayWidth = props.displayWidth || frameWidth
const rows = Math.ceil(totalFrames / columns)
const imageWidth = columns * frameWidth
const imageHeight = rows * frameHeight
const scale = displayWidth / frameWidth
const displayHeight = Math.round(frameHeight * scale)
const scaledImageWidth = Math.round(imageWidth * scale)
const scaledImageHeight = Math.round(imageHeight * scale)
if (currentFrame.value < 0) currentFrame.value = 0
if (currentFrame.value >= totalFrames) currentFrame.value = totalFrames - 1
const col = currentFrame.value % columns
const row = Math.floor(currentFrame.value / columns)
const offsetX = Math.round(col * frameWidth * scale)
const offsetY = Math.round(row * frameHeight * scale)
const styleStr = `width: ${Math.round(displayWidth)}px; height: ${displayHeight}px; background-image: url("${src}"); background-repeat: no-repeat; background-size: ${scaledImageWidth}px ${scaledImageHeight}px; background-position: -${offsetX}px -${offsetY}px;`
return styleStr as any
})
// 对外暴露
defineExpose({ play, stop, currentFrame })
</script>
<style scoped>
.sprite-animator {
display: block;
will-change: background-position;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<view class="survey-questionnaire w-vw-24">
<view class="bg-white border-box border-ff overflow-hidden rounded-20">
<view class="border-box flex flex-items-center flex-justify-between bg-EEF8FF">
<text class="font-size-18 font-500 color-171717 text-left ml-12">
调查问卷
</text>
<image class="w-102 h-72" :src="surveyData.logoUrl" mode="widthFix" />
</view>
<image class="w-full" :src="surveyData.bannerUrl" mode="widthFix" />
<view class="h-44 m-12 rounded-50 bg-button color-white flex flex-items-center flex-justify-center"
@click="handleCall">
前往填写
</view>
</view>
</view>
</template>
<script setup>
import { getAccessToken } from "@/constant/token";
import { defineProps, computed } from "vue";
import { navigateTo } from "../../router";
const props = defineProps({
toolCall: {
type: Object,
default: {},
},
});
const surveyData = computed(() => {
if (props.toolCall?.data) {
return JSON.parse(props.toolCall?.data);
} else {
return {};
}
});
const handleCall = () => {
const token = getAccessToken();
navigateTo(surveyData.value.jumpUrl, { token: token });
};
</script>
<style lang="scss" scoped></style>

View File

@@ -7,13 +7,6 @@ export const MessageRole = {
OTHER: "OTHER",
};
export const MessageType = {
// 文本消息
TEXT: "TEXT",
// 图片消息
IMAGE: "IMAGE",
};
/// 组件的名称
export const CompName = {
// 快速预定卡片
@@ -28,6 +21,8 @@ export const CompName = {
pictureAndCommodityCard: "pictureAndCommodityCard",
// 输入车牌卡片
enterLicensePlateCard: "enterLicensePlateCard",
// 调查问卷卡片
callSurveyQuestionnaire: "callSurveyQuestionnaire",
};
/// 发送的指令类型

View File

@@ -1,40 +1,27 @@
<template>
<view class="bg-white rounded-12 overflow-hidden mb-12">
<view class="flex flex-items-center border-box p-12 border-bottom">
<view class="font-size-16 font-500 color-000 line-height-24 flex-full"
>入住信息</view
>
<view class="font-size-16 font-500 color-000 line-height-24 flex-full">入住信息</view>
<view class="right">
<Stepper v-model="count" unit="间" />
</view>
</view>
<view class="border-box pl-12 pr-12">
<view
class="border-box border-bottom pt-12 pb-12 flex flex-items-center"
v-for="(item, index) in userFormList"
:key="index"
>
<view class="border-box border-bottom pt-12 pb-12 flex flex-items-center" v-for="(item, index) in userFormList"
:key="index">
<view class="font-size-14 font-500 color-525866 mr-12"> 住客姓名 </view>
<view class="right">
<input
class="border-box rounded-8 px-4 py-2 font-size-15 color-000 line-height-20"
v-model.trim="item.visitorName"
placeholder="请输入姓名"
maxlength="11"
/>
<input class="border-box px-4 py-2 font-size-15 color-000 line-height-20" v-model.trim="item.visitorName"
placeholder="请输入姓名" maxlength="11" />
</view>
</view>
<view class="flex flex-items-center border-box pt-12 pb-12">
<view class="font-size-14 font-500 color-525866 mr-12"> 联系手机 </view>
<view class="right">
<input
class="border-box rounded-8 px-4 py-2 font-size-15 color-000 line-height-20"
v-model.trim="userFormList[0].contactPhone"
placeholder="请输入联系手机"
maxlength="11"
/>
<input class="border-box px-4 py-2 font-size-15 color-000 line-height-20"
v-model.trim="userFormList[0].contactPhone" placeholder="请输入联系手机" maxlength="11" />
</view>
</view>
</view>

View File

@@ -19,7 +19,7 @@
<text class="font-size-12 line-height-16 color-99A0AE">
/{{ item.stockUnitLabel }}
</text>
<text class="btn border-box rounded-10 color-white ml-16" @click.stop="handleBooking(item)"></text>
<text class="btn border-box rounded-10 color-white ml-16"></text>
</view>
</view>
</view>
@@ -63,7 +63,6 @@ const navigateToPage = (commodityId, path) => {
const handleClick = ({ commodityId }) => navigateToPage(commodityId, "/pages/goods/index")
const handleBooking = ({ commodityId }) => navigateToPage(commodityId, "/pages-booking/index")
</script>
<style scoped lang="scss">

View File

@@ -23,6 +23,15 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/webview/index",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#FFFFFF",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black"
}
}
],
"subPackages": [

View File

@@ -33,6 +33,9 @@
<AddCarCrad v-else-if="
item.toolCall.componentName === CompName.enterLicensePlateCard
" :toolCall="item.toolCall" />
<SurveyQuestionnaire v-else-if="
item.toolCall.componentName === CompName.callSurveyQuestionnaire
" :toolCall="item.toolCall" />
</template>
<template #footer>
@@ -82,7 +85,7 @@ import {
NOTICE_EVENT_LOGOUT,
NOTICE_EVENT_LOGIN_SUCCESS,
} from "@/constant/constant";
import { MessageRole, MessageType, CompName } from "@/model/ChatModel";
import { MessageRole, CompName } from "@/model/ChatModel";
import ChatTopWelcome from "../ChatTopWelcome/index.vue";
import ChatTopNavBar from "../ChatTopNavBar/index.vue";
import ChatCardAI from "../ChatCardAi/index.vue";
@@ -99,6 +102,7 @@ import DetailCardCompontent from "../../module/DetailCardCompontent/index.vue";
import CreateServiceOrder from "@/components/CreateServiceOrder/index.vue";
import Feedback from "@/components/Feedback/index.vue";
import AddCarCrad from "@/components/AddCarCrad/index.vue";
import SurveyQuestionnaire from "@/components/SurveyQuestionnaire/index.vue";
import { mainPageData } from "@/request/api/MainPageDataApi";
import {
conversationMsgList,
@@ -144,12 +148,25 @@ const mainPageDataModel = ref({});
// 会话进行中标志
const isSessionActive = ref(false);
/// 指令
let commonType = "";
let messageCommonType = "";
// WebSocket 相关
let webSocketManager = null;
/// WebSocket 连接状态
let webSocketConnectStatus = false;
/// 使用统一的连接状态判断函数,避免状态不同步
const isWsConnected = () => !!(webSocketManager && typeof webSocketManager.isConnected === "function" && webSocketManager.isConnected());
// pendingMap: messageId -> msgIndex
const pendingMap = new Map();
// pendingTimeouts: messageId -> timeoutId
const pendingTimeouts = new Map();
// 超时时间ms
const MESSAGE_TIMEOUT = 30000;
// 防止并发初始化 websocket
let isInitializing = false;
let pendingInitPromise = null;
// sleep helper
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
// 当前会话的消息ID用于保持发送和终止的messageId一致
let currentSessionMessageId = null;
@@ -221,7 +238,7 @@ const handleReplyText = (text) => {
const handleReplyInstruct = async (item) => {
await checkToken();
commonType = item.type;
messageCommonType = item.type;
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(item.title, true);
@@ -251,7 +268,7 @@ const sendMessageAction = (inputText) => {
/// 添加通知
const addNoticeListener = () => {
uni.$on(NOTICE_EVENT_LOGIN_SUCCESS, () => {
if (!webSocketConnectStatus) {
if (!isWsConnected()) {
initHandler();
}
});
@@ -355,90 +372,136 @@ const getMainPageData = async () => {
/// =============对话↓================
// 初始化WebSocket
const initWebSocket = async () => {
// 清理旧实例
if (webSocketManager) {
webSocketManager.destroy();
// 防止并发初始化
if (isInitializing) {
return pendingInitPromise;
}
// 使用配置的WebSocket服务器地址
const token = getAccessToken();
const wsUrl = `${appStore.serverConfig.wssUrl}?access_token=${token}`;
isInitializing = true;
pendingInitPromise = (async () => {
// 清理旧实例
if (webSocketManager) {
try {
webSocketManager.destroy();
} catch (e) {
console.warn("destroy old webSocketManager failed:", e);
}
webSocketManager = null;
}
// 初始化WebSocket管理器
webSocketManager = new WebSocketManager({
wsUrl: wsUrl,
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 5, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
// 使用配置的WebSocket服务器地址
const token = getAccessToken();
const wsUrl = `${appStore.serverConfig.wssUrl}?access_token=${token}`;
// 连接成功回调
onOpen: (event) => {
console.log("WebSocket连接成功");
// 重置会话状态
webSocketConnectStatus = true;
isSessionActive.value = false; // 连接成功时重置会话状态,避免影响新消息发送
},
// 初始化WebSocket管理器
webSocketManager = new WebSocketManager({
wsUrl: wsUrl,
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 5, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
// 连接断开回调
onClose: (event) => {
console.error("WebSocket连接断开:", event);
webSocketConnectStatus = false;
// 停止当前会话
isSessionActive.value = false;
},
// 连接成功回调
onOpen: (event) => {
console.log("WebSocket连接成功");
// 重置会话状态
isSessionActive.value = false; // 连接成功时重置会话状态,避免影响新消息发送
},
// 错误回调
onError: (error) => {
console.error("WebSocket错误:", error);
webSocketConnectStatus = false;
isSessionActive.value = false;
},
// 连接断开回调
onClose: (event) => {
console.error("WebSocket连接断开:", event);
// 停止当前会话
isSessionActive.value = false;
},
// 消息回调
onMessage: (data) => {
handleWebSocketMessage(data);
},
// 错误回调
onError: (error) => {
console.error("WebSocket错误:", error);
isSessionActive.value = false;
},
// 获取会话ID回调 (用于心跳检测)
getConversationId: () => conversationId.value,
// 消息回调
onMessage: (data) => {
handleWebSocketMessage(data);
},
// 获取代理ID回调 (用于心跳检测)
getAgentId: () => agentId.value,
});
// 获取会话ID回调 (用于心跳检测)
getConversationId: () => conversationId.value,
try {
// 初始化连接
await webSocketManager.connect();
console.log("WebSocket连接初始化成功");
webSocketConnectStatus = true;
return true;
} catch (error) {
console.error("WebSocket连接失败:", error);
webSocketConnectStatus = false;
return false;
}
// 获取代理ID回调 (用于心跳检测)
getAgentId: () => agentId.value,
});
try {
// 初始化连接
await webSocketManager.connect();
console.log("WebSocket连接初始化成功");
return true;
} catch (error) {
console.error("WebSocket连接失败:", error);
return false;
} finally {
isInitializing = false;
pendingInitPromise = null;
}
})();
return pendingInitPromise;
};
// 处理WebSocket消息
const handleWebSocketMessage = (data) => {
const aiMsgIndex = chatMsgList.value.length - 1;
if (!chatMsgList.value[aiMsgIndex] || aiMsgIndex < 0) {
console.error("处理WebSocket消息时找不到对应的AI消息项");
// 验证关键字段(若服务端传回 conversationId/agentId则校验是否属于当前会话
if (data.conversationId && data.conversationId !== conversationId.value) {
console.warn("收到不属于当前会话的消息,忽略", data.conversationId);
return;
}
if (data.agentId && data.agentId !== agentId.value) {
console.warn("收到不属于当前 agent 的消息,忽略", data.agentId);
return;
}
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
data.content = String(data.content);
try {
data.content = JSON.stringify(data.content);
} catch (e) {
data.content = String(data.content);
}
}
// 直接拼接内容到AI消息
// 优先使用 messageId 进行匹配
const msgId = data.messageId || data.id || data.msgId;
let aiMsgIndex = -1;
if (msgId && pendingMap.has(msgId)) {
aiMsgIndex = pendingMap.get(msgId);
} else if (!msgId && currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
// 服务端未返回 messageId 的场景:优先使用当前会话的 messageId 映射
aiMsgIndex = pendingMap.get(currentSessionMessageId);
} else {
// 向后搜索最近的 AI 消息
for (let i = chatMsgList.value.length - 1; i >= 0; i--) {
if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) {
aiMsgIndex = i;
break;
}
}
if (aiMsgIndex === -1) {
console.error("处理WebSocket消息时找不到对应的AI消息项");
return;
}
}
// 直接拼接内容到对应 AI 消息
if (data.content) {
if (chatMsgList.value[aiMsgIndex].isLoading) {
chatMsgList.value[aiMsgIndex].msg = "";
// 首次收到内容:替换“加载中”文案并取消 loading 状态(恢复原始渲染逻辑)
chatMsgList.value[aiMsgIndex].msg = data.content;
chatMsgList.value[aiMsgIndex].isLoading = false;
} else {
// 后续流式内容追加
chatMsgList.value[aiMsgIndex].msg += data.content;
}
chatMsgList.value[aiMsgIndex].msg += data.content;
chatMsgList.value[aiMsgIndex].isLoading = false;
nextTick(() => scrollToBottom());
}
@@ -463,8 +526,20 @@ const handleWebSocketMessage = (data) => {
chatMsgList.value[aiMsgIndex].question = data.question;
}
// 清理 pendingMap / timeout
const ownedMessageId = chatMsgList.value[aiMsgIndex].messageId || msgId;
if (ownedMessageId) {
if (pendingTimeouts.has(ownedMessageId)) {
clearTimeout(pendingTimeouts.get(ownedMessageId));
pendingTimeouts.delete(ownedMessageId);
}
pendingMap.delete(ownedMessageId);
}
// 重置会话状态
isSessionActive.value = false;
// 清理当前会话的 messageId避免保留陈旧 id
resetMessageState();
}
};
@@ -491,7 +566,7 @@ const sendMessage = async (message, isInstruct = false) => {
await checkToken();
// 检查WebSocket连接状态如果未连接尝试重新连接
if (!webSocketConnectStatus) {
if (!isWsConnected()) {
console.log("WebSocket未连接尝试重新连接...");
// 显示加载提示
uni.showLoading({
@@ -505,7 +580,7 @@ const sendMessage = async (message, isInstruct = false) => {
await new Promise(resolve => setTimeout(resolve, 1000));
// 检查连接是否成功建立
if (!webSocketConnectStatus) {
if (!isWsConnected()) {
uni.hideLoading();
uni.showToast({
title: "连接服务器失败,请稍后重试",
@@ -537,10 +612,6 @@ const sendMessage = async (message, isInstruct = false) => {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.ME,
msg: message,
msgContent: {
type: MessageType.TEXT,
text: message,
},
};
chatMsgList.value.push(newMsg);
inputMessage.value = "";
@@ -550,111 +621,211 @@ const sendMessage = async (message, isInstruct = false) => {
console.log("发送的新消息:", JSON.stringify(newMsg));
};
// 通用WebSocket消息发送函数
const sendWebSocketMessage = (messageType, messageContent, options = {}) => {
// 通用WebSocket消息发送函数 -> 返回 Promise<boolean>
const sendWebSocketMessage = async (messageType, messageContent, options = {}) => {
const args = {
conversationId: conversationId.value,
agentId: agentId.value,
messageType: String(messageType), // 消息类型 0-对话 1-指令 2-中断停止 3-心跳检测
messageContent: messageContent,
messageId: currentSessionMessageId,
messageId: options.messageId || currentSessionMessageId,
};
try {
// 直接调用webSocketManager的sendMessage方法利用其内部的消息队列机制
// 即使当前连接断开,消息也会被加入队列,等待连接恢复后发送
const result = webSocketManager.sendMessage(args);
console.log(`WebSocket消息已发送 [类型:${messageType}]:`, args);
return result;
} catch (error) {
console.error("发送WebSocket消息失败:", error);
const maxRetries = typeof options.retries === 'number' ? options.retries : 3;
const baseDelay = typeof options.baseDelay === 'number' ? options.baseDelay : 300; // ms
const maxDelay = typeof options.maxDelay === 'number' ? options.maxDelay : 5000; // ms
for (let attempt = 0; attempt <= maxRetries; attempt++) {
// 确保连接
if (!isWsConnected()) {
if (options.tryReconnect) {
try {
await initWebSocket();
} catch (e) {
console.error('reconnect failed in sendWebSocketMessage:', e);
}
}
}
if (!isWsConnected()) {
if (!options.silent) console.warn('WebSocket 未连接,无法发送消息', args);
// 如果还有重试机会,进行等待后重试
if (attempt < maxRetries) {
const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
await sleep(delay);
continue;
}
isSessionActive.value = false;
return false;
}
try {
const raw = webSocketManager.sendMessage(args);
// 兼容可能返回同步布尔或 Promise 的实现
const result = await Promise.resolve(raw);
if (result) {
console.log(`WebSocket消息已发送 [类型:${messageType}]:`, args);
return true;
}
// 若返回 false消息可能已经被 manager 入队并触发连接流程。
// 在这种情况下避免立即当作失败处理,而是等待短暂时间以观察连接是否建立并由 manager 发送队列。
console.warn('webSocketManager.sendMessage 返回 false等待连接或队列发送...', { attempt, args });
const waitForConnectMs = typeof options.waitForConnectMs === 'number' ? options.waitForConnectMs : 5000;
if (webSocketManager && typeof webSocketManager.isConnected === 'function' && !webSocketManager.isConnected()) {
const startTs = Date.now();
while (Date.now() - startTs < waitForConnectMs) {
await sleep(200);
if (webSocketManager.isConnected()) {
// 给 manager 一点时间处理队列并发送
await sleep(150);
console.log('检测到 manager 已连接,假定队列消息已发送', args);
return true;
}
}
console.warn('等待 manager 建连超时,进入重试逻辑', { waitForConnectMs, args });
} else {
console.warn('sendMessage 返回 false 但 manager 看起来已连接或不可用,继续重试', { args });
}
} catch (error) {
console.error('发送WebSocket消息异常:', error, args);
}
// 失败且还有重试机会,等待指数退避
if (attempt < maxRetries) {
const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
await sleep(delay + Math.floor(Math.random() * 100));
continue;
}
// 最后一次失败
isSessionActive.value = false;
return false;
}
// 不可达,但为了类型安全
isSessionActive.value = false;
return false;
};
// 发送获取AI聊天消息
const sendChat = (message, isInstruct = false) => {
const sendChat = async (message, isInstruct = false) => {
// 检查WebSocket管理器是否存在如果不存在尝试重新初始化
if (!webSocketManager) {
console.error("WebSocket管理器不存在尝试重新初始化...");
initWebSocket();
// 短暂延迟后再次检查连接状态
setTimeout(() => {
if (webSocketManager && webSocketManager.isConnected()) {
const connected = webSocketManager && webSocketManager.isConnected();
isSessionActive.value = connected;
// 更新AI消息状态
const aiMsgIndex = chatMsgList.value.length - 1;
if (aiMsgIndex >= 0 && chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) {
chatMsgList.value[aiMsgIndex].msg = connected ? "" : "发送消息失败,请重试";
chatMsgList.value[aiMsgIndex].isLoading = connected;
}
if (connected) {
// 连接成功后重新发送消息
sendChat(message, isInstruct);
} else {
console.error("WebSocket重新初始化失败");
isSessionActive.value = false;
// 更新AI消息状态为失败
const aiMsgIndex = chatMsgList.value.length - 1;
if (aiMsgIndex >= 0 && chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) {
chatMsgList.value[aiMsgIndex].msg = "发送消息失败,请重试";
chatMsgList.value[aiMsgIndex].isLoading = false;
}
}
}, 1000);
return;
}
const messageType = isInstruct ? 1 : 0;
const messageContent = isInstruct ? commonType : message;
const messageContent = isInstruct ? messageCommonType : message;
// 生成 messageId 并保存到当前会话变量stopRequest 可能使用)
currentSessionMessageId = IdUtils.generateMessageId();
// 插入AI消息
// 插入AI消息,并在 pendingMap 中记录
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.AI,
msg: "加载中",
isLoading: true,
msgContent: {
type: MessageType.TEXT,
url: "",
},
messageId: currentSessionMessageId,
};
chatMsgList.value.push(aiMsg);
// 添加AI消息后滚动到底部
setTimeoutScrollToBottom();
const aiMsgIndex = chatMsgList.value.length - 1;
// 发送消息
const success = sendWebSocketMessage(messageType, messageContent);
// 记录 pendingMap
pendingMap.set(currentSessionMessageId, aiMsgIndex);
// 启动超时回退
const timeoutId = setTimeout(() => {
const idx = pendingMap.get(currentSessionMessageId);
if (idx != null && chatMsgList.value[idx] && chatMsgList.value[idx].isLoading) {
chatMsgList.value[idx].msg = "请求超时,请重试";
chatMsgList.value[idx].isLoading = false;
pendingMap.delete(currentSessionMessageId);
pendingTimeouts.delete(currentSessionMessageId);
isSessionActive.value = false;
setTimeoutScrollToBottom();
}
}, MESSAGE_TIMEOUT);
pendingTimeouts.set(currentSessionMessageId, timeoutId);
// 发送消息,支持重连尝试
const success = await sendWebSocketMessage(messageType, messageContent, { messageId: currentSessionMessageId, tryReconnect: true });
if (!success) {
chatMsgList.value[aiMsgIndex].msg = "发送消息失败,请重试";
chatMsgList.value[aiMsgIndex].isLoading = false;
const idx = pendingMap.get(currentSessionMessageId);
if (idx != null && chatMsgList.value[idx]) {
chatMsgList.value[idx].msg = "发送消息失败,请重试";
chatMsgList.value[idx].isLoading = false;
}
// 清理 pending
if (pendingTimeouts.has(currentSessionMessageId)) {
clearTimeout(pendingTimeouts.get(currentSessionMessageId));
pendingTimeouts.delete(currentSessionMessageId);
}
pendingMap.delete(currentSessionMessageId);
isSessionActive.value = false;
}
// 重置消息状态,为新消息做准备
resetMessageState();
};
// 停止请求函数
const stopRequest = () => {
const stopRequest = async () => {
console.log("停止请求");
// 发送中断消息给服务器 (messageType=2)
sendWebSocketMessage(2, "stop_request", { silent: true });
// 发送中断消息给服务器 (messageType=2),带上当前 messageId
try {
await sendWebSocketMessage(2, "stop_request", { messageId: currentSessionMessageId, silent: true });
} catch (e) {
console.warn("stopRequest send failed:", e);
}
// 直接将AI消息状态设为停止
const aiMsgIndex = chatMsgList.value.length - 1;
if (
chatMsgList.value[aiMsgIndex] &&
chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI
) {
// 直接将AI消息状态设为停止(优先使用 pendingMap 映射)
let aiMsgIndex = -1;
if (currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
aiMsgIndex = pendingMap.get(currentSessionMessageId);
} else {
aiMsgIndex = chatMsgList.value.length - 1;
}
if (chatMsgList.value[aiMsgIndex] &&
chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) {
chatMsgList.value[aiMsgIndex].isLoading = false;
if (
chatMsgList.value[aiMsgIndex].msg &&
if (chatMsgList.value[aiMsgIndex].msg &&
chatMsgList.value[aiMsgIndex].msg.trim() &&
!chatMsgList.value[aiMsgIndex].msg.startsWith("加载中")
) {
!chatMsgList.value[aiMsgIndex].msg.startsWith("加载中")) {
// 保留已显示内容
} else {
chatMsgList.value[aiMsgIndex].msg = "请求已停止";
}
}
// 清理 pending
if (currentSessionMessageId) {
if (pendingTimeouts.has(currentSessionMessageId)) {
clearTimeout(pendingTimeouts.get(currentSessionMessageId));
pendingTimeouts.delete(currentSessionMessageId);
}
pendingMap.delete(currentSessionMessageId);
}
// 重置会话状态
isSessionActive.value = false;
setTimeoutScrollToBottom();
@@ -676,13 +847,23 @@ const resetConfig = () => {
if (webSocketManager) {
webSocketManager.destroy();
webSocketManager = null;
webSocketConnectStatus = false;
}
// 重置消息状态
resetMessageState();
isSessionActive.value = false;
// 清理 pendingMap / pendingTimeouts
try {
for (const t of pendingTimeouts.values()) {
clearTimeout(t);
}
} catch (e) {
// ignore
}
pendingTimeouts.clear();
pendingMap.clear();
// 清理定时器
if (holdKeyboardTimer.value) {
clearTimeout(holdKeyboardTimer.value);

View File

@@ -4,19 +4,15 @@
<!-- 隐藏 -->
<view class="flex-full h-full flex flex-items-center flex-justify-center">
<!-- ChatTopWelcome不在可视区显示并添加动画在可视区隐藏 -->
<view
v-show="show"
:class="['w-32 h-32', { 'image-animated': show }]"
:style="getStyle"
></view>
<text
v-show="show"
:class="[
'font-size-14 font-500 color-171717 ml-10',
{ 'text-animated': show },
]"
>
<!-- ChatTopWelcome不在可视区显示并添加动画在可视区隐藏 -->
<SpriteAnimator v-show="show" class="image-animated" :src="spriteStyle.ipSmallImage"
:frameWidth="spriteStyle.frameWidth" :frameHeight="spriteStyle.frameHeight"
:totalFrames="spriteStyle.totalFrames" :columns="spriteStyle.columns" :displayWidth="spriteStyle.displayWidth"
:fps="16" />
<text v-show="show" :class="[
'font-size-14 font-500 color-171717 ml-10',
{ 'text-animated': show },
]">
{{ config.name }}
</text>
</view>
@@ -28,25 +24,33 @@
<script setup>
import { ref, defineProps, computed, defineExpose } from "vue";
import { getCurrentConfig } from "@/constant/base";
import SpriteAnimator from "@/components/Sprite/SpriteAnimator.vue";
const props = defineProps({
mainPageDataModel: {
type: Object,
default: () => ({}),
default: () => ({
initPageImages: {},
}),
},
});
const initPageImages = computed(() => {
return props.mainPageDataModel?.initPageImages || {};
});
const show = ref(false);
const config = getCurrentConfig();
const getStyle = computed(() => {
const spriteStyle = computed(() => {
const images = initPageImages.value;
return {
"--ipSmallImageStep": config.ipSmallImageStep,
"--ipSmallImageHeight": config.ipSmallImageHeight,
"--ipSmallTime": config.ipSmallTime,
backgroundImage: `url(${config.ipSmallImage})`,
backgroundRepeat: "no-repeat",
backgroundSize: "32px auto",
backgroundPosition: "0 0",
ipSmallImage: images.ipSmallImage ?? config.ipSmallImage,
frameWidth: images.ipSmallImageWidth ?? config.ipSmallImageWidth,
frameHeight: images.ipSmallImageHeight ?? config.ipSmallImageHeight,
totalFrames: images.ipSmallTotalFrames ?? config.ipSmallTotalFrames,
columns: images.ipSmallColumns ?? config.ipSmallColumns,
displayWidth: 32,
};
});

View File

@@ -1,30 +1,18 @@
// 图片从0%到100%动画
.image-animated {
animation: logo-scale 0.3s ease-in-out,
sprite-play calc(var(--ipSmallTime) * 1s) steps(var(--ipSmallImageStep))
infinite;
animation: logo-scale 0.3s ease-in-out;
}
@keyframes logo-scale {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes sprite-play {
0% {
background-position: 0 0;
}
100% {
/* 117 帧 × 每帧高度约 32px终点应为 -(117-1)*32 = -5421px */
background-position: 0 calc(var(--ipSmallImageHeight) * -1px);
}
}
// 文字从0%到100%动画,从左到右
.text-animated {
animation: text-fade-in 0.3s ease-in-out;
@@ -35,6 +23,7 @@
opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);

View File

@@ -2,13 +2,11 @@ v
<template>
<view class="welcome-content border-box p-12">
<view class="wrap rounded-20">
<view
class="flex flex-items-center flex-justify-between border-box pl-12 pr-12"
>
<view class="ip" :style="getStyle"></view>
<view
class="welcome-text font-size-14 font-500 font-family-misans-vf color-171717 line-height-24"
>
<view class="flex flex-items-center flex-justify-between border-box pl-12 pr-12">
<SpriteAnimator :src="spriteStyle.ipLargeImage" :frameWidth="spriteStyle.frameWidth"
:frameHeight="spriteStyle.frameHeight" :totalFrames="spriteStyle.totalFrames" :columns="spriteStyle.columns"
:displayWidth="spriteStyle.displayWidth" :fps="16" />
<view class="welcome-text font-size-14 font-500 font-family-misans-vf color-171717 line-height-24">
{{ welcomeContent }}
</view>
</view>
@@ -22,6 +20,7 @@ v
import { defineProps, computed, getCurrentInstance, defineExpose } from "vue";
import { getCurrentConfig } from "@/constant/base";
import ChatMoreTips from "../ChatMoreTips/index.vue";
import SpriteAnimator from "@/components/Sprite/SpriteAnimator.vue";
const props = defineProps({
mainPageDataModel: {
@@ -49,19 +48,24 @@ const props = defineProps({
},
});
const getStyle = computed(() => {
const config = getCurrentConfig();
const initPageImages = computed(() => {
return props.mainPageDataModel?.initPageImages || {};
});
const config = getCurrentConfig();
const spriteStyle = computed(() => {
const images = initPageImages.value;
return {
"--ipLargeImageStep": config.ipLargeImageStep,
"--ipLargeImageHeight": config.ipLargeImageHeight,
"--ipLargeTime": config.ipLargeTime,
backgroundImage: `url(${config.ipLargeImage})`,
backgroundRepeat: "no-repeat",
backgroundSize: "158px auto",
backgroundPosition: "0 0",
ipLargeImage: images.ipLargeImage ?? config.ipLargeImage,
frameWidth: images.ipLargeImageWidth ?? config.ipLargeImageWidth,
frameHeight: images.ipLargeImageHeight ?? config.ipLargeImageHeight,
totalFrames: images.ipLargeTotalFrames ?? config.ipLargeTotalFrames,
columns: images.ipLargeColumns ?? config.ipLargeColumns,
displayWidth: 158,
};
});
const welcomeContent = computed(() => props.mainPageDataModel.welcomeContent);
const guideWords = computed(() => props.mainPageDataModel.guideWords);

View File

@@ -1,32 +1,3 @@
.wrap {
background-color: rgba(255, 255, 255, 0.5);
}
.ip {
position: relative;
flex: 0 0 158px;
width: 158px;
height: 134px;
animation: sprite-play calc(var(--ipLargeTime) * 1s)
steps(var(--ipLargeImageStep)) infinite;
&::before {
content: "";
position: absolute;
background-color: #f9fcfd;
top: 0;
left: 0;
right: 0;
height: 3px;
}
}
@keyframes sprite-play {
0% {
background-position: 0 0;
}
100% {
/* 40 帧 × 每帧高度约 139px终点应为 -(40-1)*139 = -5421px */
background-position: 0 calc(var(--ipLargeImageHeight) * -1px);
}
}

View File

@@ -2,16 +2,9 @@
<view class="container">
<ModuleTitle :title="recommendTheme.themeName" />
<view class="container-scroll">
<view
v-for="(item, index) in recommendTheme.recommendPostsList"
:key="index"
>
<view v-for="(item, index) in recommendTheme.recommendPostsList" :key="index">
<view class="mk-card-item" @click="sendReply(item)">
<image
class="card-img"
:src="item.coverPhoto"
mode="widthFix"
></image>
<image class="card-img" :src="item.coverPhoto" mode="widthFix"></image>
<text class="card-text">{{ item.topic }}</text>
</view>
</view>
@@ -20,7 +13,7 @@
</template>
<script setup>
import { SEND_MESSAGE_CONTENT_TEXT } from "@/constant/constant";
import { SEND_MESSAGE_CONTENT_TEXT, SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
import { defineProps } from "vue";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
@@ -32,8 +25,12 @@ const props = defineProps({
});
const sendReply = (item) => {
const topic = item.userInputContent || item.topic.replace(/^#/, "");
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, topic);
if (item.userInputContentType && item.userInputContentType === '1') {
const commonItem = { type: item.userInputContent, title: item.topic }
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, commonItem);
return;
}
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item.userInputContent);
};
</script>

View File

@@ -3,29 +3,18 @@
<ModuleTitle :title="recommendTheme.themeName" />
<view class="container-scroll font-size-0 scroll-x whitespace-nowrap">
<view
class="card-item bg-white inline-block rounded-20 mr-8"
v-for="(item, index) in recommendTheme.recommendPostsList"
:key="index"
@click="sendReply(item)"
>
<view class="card-item bg-white inline-block rounded-20 mr-8"
v-for="(item, index) in recommendTheme.recommendPostsList" :key="index" @click="sendReply(item)">
<view class="m-4 relative">
<image
class="card-img rounded-16 relative z-10"
:src="item.coverPhoto"
mode="aspectFill"
/>
<image class="card-img rounded-16 relative z-10" :src="item.coverPhoto" mode="aspectFill" />
<view class="shadow absolute rounded-16"></view>
</view>
<view class="card-text border-box">
<view class="color-171717 font-size-14 line-height-20 ellipsis-1">
<view class="font-size-11 color-99A0AE ellipsis-1">
{{ item.topic }}
</view>
<view class="font-size-11 color-99A0AE">
{{ item.userInputContent }}
</view>
</view>
</view>
</view>
@@ -34,7 +23,7 @@
<script setup>
import { defineProps } from "vue";
import { SEND_MESSAGE_CONTENT_TEXT } from "@/constant/constant";
import { SEND_MESSAGE_CONTENT_TEXT, SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
const props = defineProps({
@@ -45,8 +34,12 @@ const props = defineProps({
});
const sendReply = (item) => {
const topic = item.userInputContent || item.topic.replace(/^#/, "");
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, topic);
if (item.userInputContentType && item.userInputContentType === '1') {
const commonItem = { type: item.userInputContent, title: item.topic }
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, commonItem);
return;
}
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item.userInputContent);
};
</script>

View File

@@ -1,26 +1,14 @@
<template>
<view class="webview">
<!-- 使用 NavBar 组件 -->
<TopNavBar title="网页浏览" @back="goBack" />
<!-- WebView 内容区域 -->
<view class="webview-content">
<web-view :src="webviewUrl"></web-view>
</view>
<view>
<web-view :src="webviewUrl" @message="handleH5Message"></web-view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import TopNavBar from "@/components/TopNavBar/index.vue";
const webviewUrl = ref("");
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
onMounted(() => {
// 获取页面参数
const pages = getCurrentPages();
@@ -33,24 +21,26 @@ onMounted(() => {
webviewUrl.value = decodeURIComponent(options.url);
}
});
const handleH5Message = (event) => {
const messageData = event.detail.data[0];
console.log("Received message from H5:", messageData);
// 根据需要处理H5传递过来的消息
const action = messageData.action;
switch (action) {
case "navigateBack":
uni.navigateBack();
break;
case "navigateTo":
break
default:
console.log("Unknown action:", action);
}
};
</script>
<style lang="scss" scoped>
.webview {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fff;
}
.webview-content {
flex: 1;
margin-top: calc(44px + var(--status-bar-height));
}
.webview-content {
width: 100%;
height: 100%;
}
</style>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,13 @@
import request from "../base/request";
import { proUrl } from "../base/baseUrl";
import { useAppStore } from "@/store";
export const getServiceUrl = async (versionValue) => {
const apiUrl = proUrl + "/hotelBiz/mainScene/getServiceUrl";
const res = await request.post(apiUrl, { versionValue: versionValue });
if (res && res.code == 0 && res.data) {
const appStore = useAppStore();
appStore.setServerConfig(res.data);
}
};

View File

@@ -1,15 +1,18 @@
import request from "../base/request";
import { isZhiNian } from "@/constant/base";
import { useAppStore } from "@/store";
import { devUrl, proUrl, wssDevUrl } from "../base/baseUrl";
import { devUrl, wssDevUrl } from "./baseUrl";
import { getServiceUrl } from "../api/GetServiceUrlApi";
/// 版本号, 每次发版本前增加
const versionValue = "1.0.3";
const versionValue = "1.0.4";
/// 是否是测试版本, 测试版本为true 发布版本为false
const developVersion = false;
// 获取服务地址
const getEvnUrl = async () => {
/// 智念客户端不需要获取环境地址
if (isZhiNian) {
if (isZhiNian || developVersion) {
const appStore = useAppStore();
appStore.setServerConfig({
baseUrl: devUrl, // 服务器基础地址
@@ -18,12 +21,8 @@ const getEvnUrl = async () => {
return;
}
const apiUrl = proUrl + "/hotelBiz/mainScene/getServiceUrl";
const res = await request.post(apiUrl, { versionValue: versionValue });
if (res && res.code == 0 && res.data) {
const appStore = useAppStore();
appStore.setServerConfig(res.data);
}
/// 正式环境获取服务地址
await getServiceUrl(versionValue);
};
export { getEvnUrl };

View File

@@ -1,4 +1,3 @@
import { goLogin } from "../../hooks/useGoLogin";
import { getCurrentConfig } from "@/constant/base";
import { useAppStore } from "@/store";
import { NOTICE_EVENT_LOGOUT } from "@/constant/constant";

View File

@@ -6,7 +6,7 @@ export function navigateTo(url, args) {
// 如果有额外参数拼接到URL后面
const paramString = objectToUrlParams(args);
if (paramString) {
if (targetUrl.contains("?")) {
if (typeof targetUrl === "string" && targetUrl.includes("?")) {
targetUrl += "&";
} else {
targetUrl += "?";
@@ -14,6 +14,7 @@ export function navigateTo(url, args) {
targetUrl += paramString;
}
}
console.log("Navigating to URL:", targetUrl);
uni.navigateTo({
url: "/pages/webview/index?url=" + encodeURIComponent(targetUrl),
});

View File

@@ -21,3 +21,7 @@
.h-24 {
height: 24px;
}
.h72 {
height: 72px;
}

View File

@@ -6,6 +6,10 @@
width: 100vw;
}
.w-vw-24 {
width: calc(100vw - 24px);
}
.w-24 {
width: 24px;
}
@@ -22,6 +26,12 @@
width: 60px;
}
.w-80 {
width: 80px;
}
.w-102 {
width: 102px;
}

View File

@@ -28,10 +28,11 @@ export class WebSocketManager {
// 回调函数
this.callbacks = {
onConnect: options.onConnect || (() => {}),
onDisconnect: options.onDisconnect || (() => {}),
onError: options.onError || (() => {}),
onMessage: options.onMessage || (() => {}),
// 支持两套回调命名onConnect/onDisconnect 与 onOpen/onClose兼容调用方
onConnect: options.onConnect || options.onOpen || (() => { }),
onDisconnect: options.onDisconnect || options.onClose || (() => { }),
onError: options.onError || (() => { }),
onMessage: options.onMessage || (() => { }),
getConversationId: options.getConversationId || (() => ""),
getAgentId: options.getAgentId || (() => ""),
};
@@ -327,7 +328,7 @@ export class WebSocketManager {
const messageData = {
...message,
timestamp: Date.now(),
retryCount: 0,
retryCount: typeof message.retryCount === 'number' ? message.retryCount : 0,
};
if (this.connectionState) {
@@ -392,7 +393,7 @@ export class WebSocketManager {
agentId: this.callbacks.getAgentId
? this.callbacks.getAgentId()
: "",
messageType: 3, // 心跳检测
messageType: '3', // 心跳检测
messageContent: "heartbeat",
messageId: IdUtils.generateMessageId(),
};