<template>
<view class="ai-chat-container">
<view class="header">
<text class="title">多轮对话</text>
</view>
<view class="input-group">
<view class="input-row">
<input type="text" v-model="userId" placeholder="用户ID (必填)" />
<input type="text" v-model="userName" placeholder="用户名称" />
<input type="text" v-model="sessionid" placeholder="会话ID" />
</view>
<view class="input-row">
<input type="text" v-model="collection" placeholder="知识库名称(可选)" />
</view>
<view class="input-row">
<textarea v-model="messageInput" placeholder="输入您的问题(支持多行输入)" class="message-input" />
</view>
<button @click="handleSend" :disabled="isProcessing" class="send-btn">发送请求</button>
</view>
<view class="response-container">
<text>{{ responseBuffer }}</text>
</view>
<view class="status-bar">
<text>连接状态:{{ connectionStatus }}</text>
</view>
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
</view>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import StompWx from '@/utils/stomp-wx.js';
export default {
name: 'AIChat',
setup() {
// 状态变量
const userId = ref('Q1100325');
const userName = ref('macroswang');
const sessionid = ref('6624663456435635463456435');
const collection = ref('');
const messageInput = ref('你好');
const responseBuffer = ref('');
const connectionStatus = ref('未连接');
const errorMessage = ref('');
const isProcessing = ref(false);
// STOMP客户端
let stompClient = null;
let currentSubscription = null;
let currentUserId = null;
// 添加心跳检测机制
let heartbeatInterval = null;
// 连接WebSocket
const connectWebSocket = async (userIdValue) => {
return new Promise((resolve, reject) => {
// 如果已连接且用户ID相同,直接复用连接
if (stompClient && stompClient.connected && currentUserId === userIdValue) {
resolve();
return;
}
// 断开旧连接(如果存在)
if (stompClient && stompClient.connected) {
stompClient.disconnect();
}
// 创建新连接
const wsUrl = import.meta.env.VITE_APP_BASE_WEBSOCKET_HOST;
console.log("WebSocket URL:", wsUrl);
// 确保URL使用wss协议(企业微信要求)
const secureWsUrl = wsUrl.replace('ws://', 'wss://');
// 检测是否在企业微信环境
let isEnterpriseWx = false;
let cropId = '';
// 基础请求头
const headers = {
'userId': userIdValue,
'crop_id':'ww40f48251ec0cd3b2',
'Authorization': 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJcImFIWkJoYUJGaWNkR0NkRVhYbVU3UUtNMTZUVFwiIiwiZXhwIjoxNzQyMDE3OTA5fQ.yx4vkjbZEDt_den6V04CXSPAPD9qiyix4apJlzM4SXE'
};
// 添加其他企业微信可能需要的头信息
headers['x-client-type'] = 'wxwork';
headers['x-client-version'] = '1.2';
console.log("创建连接,使用头信息:", headers);
// 创建STOMP客户端,针对企业微信环境优化配置
stompClient = new StompWx(secureWsUrl, {
headers,
// 企业微信环境下的特殊配置
heartbeat: {
outgoing: isEnterpriseWx ? 5000 : 10000, // 企业微信环境下更频繁的心跳
incoming: isEnterpriseWx ? 5000 : 10000
},
debug: true,
// 添加重连配置
reconnectDelay: isEnterpriseWx ? 2000 : 5000,
maxReconnectAttempts: isEnterpriseWx ? 10 : 5
});
console.log("STOMP客户端创建完成,准备连接");
// 减少连接超时时间
let connectionTimeout = setTimeout(() => {
if (!stompClient.connected) {
console.error("WebSocket连接超时");
showError("连接超时,正在重试...");
// 超时后自动重试一次
clearTimeout(connectionTimeout);
stompClient.disconnect();
// 短暂延迟后重试
setTimeout(() => {
console.log("尝试重新连接...");
// 使用新的客户端重试
stompClient = new StompWx(secureWsUrl, {
headers,
heartbeat: {
outgoing: 5000, // 进一步减少心跳间隔
incoming: 5000
},
debug: true
});
stompClient.connect(() => {
console.log("重试连接成功");
currentUserId = userIdValue;
updateStatus('已连接(重试成功)');
setupSubscription(userIdValue);
resolve();
}, (retryError) => {
console.error("重试连接失败:", retryError);
showError(`连接失败,请检查网络环境或刷新页面`);
reject(new Error("重试连接失败"));
});
}, 2000);
}
}, isEnterpriseWx ? 8000 : 10000); // 企业微信环境下超时时间更短
console.log("开始连接WebSocket...");
stompClient.connect(() => {
console.log("WebSocket连接成功");
clearTimeout(connectionTimeout);
currentUserId = userIdValue;
updateStatus('已连接');
setupSubscription(userIdValue);
resolve();
}, (error) => { // 添加错误回调
console.error("连接错误:", error);
clearTimeout(connectionTimeout);
showError(`连接失败: ${error?.headers?.message || '未知错误'}`);
setTimeout(() => {
updateStatus('正在重连...');
connectWebSocket(userIdValue).catch(err => {
console.error("重连失败:", err);
});
}, 3000);
reject(error);
});
});
};
// 设置消息订阅
const setupSubscription = (userIdValue) => {
if (currentSubscription) {
try {
stompClient.unsubscribe(currentSubscription);
} catch (e) {
console.warn("取消订阅失败:", e);
}
currentSubscription = null;
}
const sessionidValue = sessionid.value.trim();
console.log("检查订阅" + sessionidValue);
const destination = '/user/' + sessionidValue + '/topic/data';
console.log("准备订阅:", destination);
try {
currentSubscription = stompClient.subscribe(destination, (message) => {
console.log("收到消息:", message);
// 添加消息接收检测
if (!message) {
console.warn("接收到空消息");
return;
}
if (!message.content) {
console.warn("消息内容为空:", message);
// 尝试解析消息体
try {
const body = message.body || message.data;
if (body) {
handleStreamMessage(body);
return;
}
} catch (e) {
console.error("解析消息失败:", e);
}
return;
}
handleStreamMessage(message.content);
});
// 添加订阅确认日志
console.log("已订阅目标:", destination, "订阅ID:", currentSubscription);
// 发送一个测试消息确认连接状态
setTimeout(() => {
if (stompClient && stompClient.connected) {
console.log("发送测试消息");
try {
stompClient.send("/app/ping", JSON.stringify({
userId: userIdValue,
timestamp: Date.now()
}), {});
} catch (e) {
console.error("发送测试消息失败:", e);
}
} else {
console.warn("无法发送测试消息,连接未就绪");
}
}, 1000);
} catch (e) {
console.error("订阅失败:", e);
showError("订阅失败,请重试");
}
};
// 处理发送请求
const handleSend = async () => {
const requestData = validateInputs();
if (!requestData) return;
try {
// 确保连接的是当前用户
if (currentUserId !== requestData.userId) {
await connectWebSocket(requestData.userId);
}
isProcessing.value = true;
updateStatus('连接中...');
if (!stompClient || !stompClient.connected) {
await connectWebSocket(requestData.userId);
}
sendRequest(requestData);
clearResponse();
} catch (error) {
showError(`请求失败: ${error.message}`);
resetState();
}
};
// 验证输入
const validateInputs = () => {
const userIdValue = userId.value.trim();
const messageValue = messageInput.value.trim();
if (!userIdValue) {
showError("用户ID不能为空");
return null;
}
if (!messageValue) {
showError("消息内容不能为空");
return null;
}
return {
userId: userIdValue,
userName: userName.value.trim() || '匿名用户',
messages: [{
role: "user",
content: messageValue
}],
sessionid: sessionid.value.trim(),
collection: collection.value.trim()
};
};
// 发送请求体
const sendRequest = (data) => {
const headers = {
'userId': data.userId,
};
stompClient.send("/app/streamData", JSON.stringify(data), headers);
updateStatus('等待响应...');
};
// 处理流式响应
const handleStreamMessage = (chunk) => {
console.log("收到数据块:", chunk);
if (!chunk) {
console.warn("收到空数据块");
return;
}
if (chunk === '流数据发送完毕') {
finalizeResponse();
console.log("发送完毕");
return;
}
// 尝试解析JSON格式的消息
try {
const jsonData = JSON.parse(chunk);
if (jsonData.content) {
responseBuffer.value += jsonData.content;
return;
}
} catch (e) {
// 不是JSON格式,按普通文本处理
}
responseBuffer.value += chunk;
};
// 完成处理
const finalizeResponse = () => {
updateStatus('响应完成');
console.log('完整响应内容:', responseBuffer.value);
resetState();
};
// 重置状态
const resetState = () => {
isProcessing.value = false;
};
// 清空响应区
const clearResponse = () => {
responseBuffer.value = '';
};
// 更新状态显示
const updateStatus = (text) => {
connectionStatus.value = text;
};
// 显示错误信息
const showError = (message) => {
errorMessage.value = message;
setTimeout(() => {
errorMessage.value = "";
}, 5000);
};
onMounted(() => {
// 设置心跳检测,保持连接活跃
heartbeatInterval = setInterval(() => {
if (stompClient && stompClient.connected) {
try {
stompClient.send("/app/heartbeat", JSON.stringify({
timestamp: Date.now(),
userId: userId.value
}), {});
console.log("发送心跳");
} catch (e) {
console.error("发送心跳失败:", e);
// 心跳失败,尝试重连
if (currentUserId) {
console.log("心跳失败,尝试重连");
connectWebSocket(currentUserId).catch(err => {
console.error("心跳重连失败:", err);
});
}
}
}
}, 15000); // 减少到15秒
});
// 组件卸载时断开连接
onUnmounted(() => {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
if (stompClient && stompClient.connected) {
stompClient.disconnect();
currentUserId = null;
}
});
return {
userId,
userName,
sessionid,
collection,
messageInput,
responseBuffer,
connectionStatus,
errorMessage,
isProcessing,
handleSend
};
}
};
</script>
<style>
.ai-chat-container {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
max-width: 750rpx;
margin: 20rpx auto;
padding: 20rpx;
background-color: #f5f5f5;
}
.header {
margin-bottom: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
}
.input-group {
margin-bottom: 20rpx;
background: white;
padding: 20rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.input-row {
margin: 10rpx 0;
display: flex;
gap: 10rpx;
}
input {
flex: 1;
padding: 16rpx;
border: 1rpx solid #ddd;
border-radius: 4rpx;
min-width: 120rpx;
}
.message-input {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 1rpx solid #ddd;
border-radius: 4rpx;
}
.send-btn {
padding: 20rpx 40rpx;
background: #2196F3;
color: white;
border: none;
border-radius: 4rpx;
margin-top: 10rpx;
}
.send-btn:active {
background: #1976D2
}
.response-container {
background: white;
padding: 20rpx;
border-radius: 8rpx;
min-height: 400rpx;
max-height: 1000rpx;
overflow-y: auto;
white-space: pre-wrap;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
line-height: 1.6;
}
.status-bar {
margin-top: 15rpx;
color: #666;
font-size: 28rpx;
}
.error-message {
color: #f44336;
margin: 10rpx 0;
font-size: 28rpx;
}
</style>
// 新版导入方式
import { Client } from '@stomp/stompjs'
import '@/utils/text-encoder-polyfill.js'
// 微信小程序 WebSocket 适配器
class WxWebSocket {
constructor(url) {
this.url = url
this.socketTask = null
this.onopen = null
this.onmessage = null
this.onclose = null
this.onerror = null
}
connect() {
try {
console.log('正在连接到 WebSocket:', this.url)
this.socketTask = uni.connectSocket({
url: this.url,
header: {
'content-type': 'application/json'
},
success: () => console.log('WebSocket 连接请求已发送'),
fail: err => {
console.error('WebSocket 连接失败', err)
if (this.onerror) this.onerror(err)
}
})
if (this.socketTask) {
this.socketTask.onOpen(() => {
console.log('WebSocket 已打开,准备触发 onopen 事件')
if (this.onopen) {
console.log('调用 onopen 回调')
this.onopen({ target: this })
}
})
this.socketTask.onMessage(res => {
console.log('WebSocket 收到消息:', typeof res.data, res.data.length > 100 ? res.data.substring(0, 100) + '...' : res.data)
// 确保消息数据是字符串
let messageData = res.data
if (typeof messageData !== 'string') {
try {
// 如果是对象,尝试转换为字符串
messageData = JSON.stringify(messageData)
} catch (e) {
console.error('消息数据转换失败:', e)
}
}
if (this.onmessage) {
this.onmessage({
data: messageData,
target: this
})
}
})
this.socketTask.onClose(() => {
console.log('WebSocket 已关闭')
if (this.onclose) {
this.onclose({ target: this })
}
})
this.socketTask.onError(err => {
console.error('WebSocket 错误:', err)
if (this.onerror) {
this.onerror({ error: err, target: this })
}
})
} else {
console.error('WebSocket 任务创建失败')
if (this.onerror) {
this.onerror({ error: new Error('WebSocket 任务创建失败'), target: this })
}
}
} catch (error) {
console.error('WebSocket 连接过程中发生异常:', error)
if (this.onerror) {
this.onerror({ error, target: this })
}
}
}
send(data) {
console.log('发送数据:', typeof data, data.length > 100 ? data.substring(0, 100) + '...' : data)
if (this.socketTask) {
this.socketTask.send({
data,
success: () => console.log('数据发送成功'),
fail: err => {
console.error('发送消息失败', err)
if (this.onerror) {
this.onerror({ error: err, target: this })
}
}
})
} else {
console.error('无法发送数据:socketTask 不存在')
}
}
close() {
if (this.socketTask) {
this.socketTask.close({
success: () => console.log('WebSocket 关闭成功'),
fail: err => console.error('WebSocket 关闭失败', err)
})
}
}
addEventListener(event, callback) {
console.log('添加事件监听器:', event)
if (event === 'open') {
this.onopen = callback
} else if (event === 'message') {
this.onmessage = callback
} else if (event === 'close') {
this.onclose = callback
} else if (event === 'error') {
this.onerror = callback
}
}
removeEventListener(event, callback) {
console.log('移除事件监听器:', event)
if (event === 'open' && this.onopen === callback) {
this.onopen = null
} else if (event === 'message' && this.onmessage === callback) {
this.onmessage = null
} else if (event === 'close' && this.onclose === callback) {
this.onclose = null
} else if (event === 'error' && this.onerror === callback) {
this.onerror = null
}
}
}
// StompWx 类 - 包装 STOMP 客户端
class StompWx {
constructor(url, options = {}) {
this.url = url
this.options = options
this.client = null
this.connected = false
this.callbacks = {
connect: [],
error: [],
disconnect: []
}
this.subscriptions = {}
this._init()
}
_init() {
// 创建自定义的 WebSocket 适配器
const webSocketFactory = () => {
console.log('创建 WebSocket 连接...')
const socket = new WxWebSocket(this.url)
socket.connect()
return socket
}
this.client = new Client({
webSocketFactory,
connectHeaders: this.options.headers || {},
debug: str => console.log('[STOMP]', str),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
})
this.client.onConnect = frame => {
console.log('✅ STOMP 连接成功', frame)
this.connected = true
this.callbacks.connect.forEach(callback => callback(frame))
}
this.client.onStompError = frame => {
console.error('❌ STOMP 连接错误', frame)
this.callbacks.error.forEach(callback => callback(frame))
}
this.client.onDisconnect = () => {
console.log('STOMP 连接已断开')
this.connected = false
this.callbacks.disconnect.forEach(callback => callback())
}
}
connect(callback) {
if (callback) {
this.callbacks.connect.push(callback)
}
if (!this.client.active) {
console.log('激活 STOMP 客户端...')
this.client.activate()
} else if (this.connected) {
// 如果已经连接,直接调用回调
callback && callback()
}
return this
}
disconnect(callback) {
if (callback) {
this.callbacks.disconnect.push(callback)
}
if (this.client.active) {
this.client.deactivate()
}
return this
}
subscribe(destination, callback, headers = {}) {
const id = Math.random().toString(36).substring(2, 15)
const doSubscribe = () => {
if (!this.connected) {
console.warn('尚未连接,订阅将在连接后进行')
return
}
console.log(`订阅主题: ${destination}`)
const subscription = this.client.subscribe(
destination,
message => {
console.log('收到订阅消息:', message)
try {
// 尝试解析 JSON
const body = message.body
let data
try {
data = JSON.parse(body)
} catch (e) {
data = body
}
callback({ id: message.headers.id, content: data })
} catch (error) {
console.error('处理订阅消息时出错:', error)
callback({ error: error.message })
}
},
headers
)
this.subscriptions[id] = subscription
console.log('订阅成功,ID:', id)
}
if (this.connected) {
doSubscribe()
} else {
// 如果尚未连接,添加连接回调
this.callbacks.connect.push(doSubscribe)
}
return id
}
unsubscribe(id) {
if (this.subscriptions[id]) {
this.subscriptions[id].unsubscribe()
delete this.subscriptions[id]
console.log('取消订阅成功,ID:', id)
}
}
send(destination, body, headers = {}) {
if (!this.connected) {
console.error('未连接,无法发送消息')
return false
}
try {
console.log('发送消息到:', destination)
// 确保 body 是字符串
let messageBody = body
if (typeof body === 'object') {
messageBody = JSON.stringify(body)
}
this.client.publish({
destination: destination,
headers: headers,
body: messageBody
})
console.log('消息发送成功')
return true
} catch (error) {
console.error('发送消息失败:', error)
return false
}
}
onError(callback) {
if (callback) {
this.callbacks.error.push(callback)
}
return this
}
}
export default StompWx
你好,请移步企微官方讨论区:https://developer.work.weixin.qq.com/community/question