demo.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. <template>
  2. <div class="sse-container">
  3. <h2>SSE 实时数据连接</h2>
  4. <div class="status-indicator">
  5. <span class="status-dot" :class="{ connected: isConnected }"></span>
  6. <span>{{ connectionStatusText }}</span>
  7. </div>
  8. <div class="controls">
  9. <button @click="connect" :disabled="isConnected">连接</button>
  10. <button @click="disconnect" :disabled="!isConnected">断开</button>
  11. <button @click="clearMessages">清空消息</button>
  12. </div>
  13. <div class="message-list">
  14. <div v-if="messages.length === 0" class="empty-message">
  15. 暂无消息,请先连接服务器
  16. </div>
  17. <div v-for="(msg, index) in messages" :key="index" class="message-item" :class="msg.type">
  18. <div class="message-header">
  19. <span class="message-type">{{ msg.type.toUpperCase() }}</span>
  20. <span class="message-time">{{ formatTime(msg.timestamp) }}</span>
  21. </div>
  22. <div class="message-content">{{ msg.content }}</div>
  23. </div>
  24. </div>
  25. </div>
  26. <!-- WebSocket 连接部分 -->
  27. <div class="ws-container">
  28. <h2>WebSocket 实时数据连接</h2>
  29. <div class="status-indicator">
  30. <span class="status-dot" :class="{ connected: isWsConnected }"></span>
  31. <span>{{ wsConnectionStatusText }}</span>
  32. </div>
  33. <div class="controls">
  34. <button @click="connectWs" :disabled="isWsConnected">连接</button>
  35. <button @click="disconnectWs" :disabled="!isWsConnected">断开</button>
  36. <button @click="clearWsMessages">清空消息</button>
  37. </div>
  38. <div class="message-list">
  39. <div v-if="wsMessages.length === 0" class="empty-message">
  40. 暂无消息,请先连接WebSocket服务器
  41. </div>
  42. <div v-for="(msg, index) in wsMessages" :key="index" class="message-item" :class="msg.type">
  43. <div class="message-header">
  44. <span class="message-type">{{ msg.type.toUpperCase() }}</span>
  45. <span class="message-time">{{ formatTime(msg.timestamp) }}</span>
  46. </div>
  47. <div class="message-content">{{ msg.content }}</div>
  48. </div>
  49. </div>
  50. </div>
  51. </template>
  52. <script>
  53. export default {
  54. name: 'SSEConnection',
  55. data() {
  56. return {
  57. isConnected: false,
  58. eventSource: null,
  59. messages: [],
  60. retryCount: 0,
  61. maxRetries: 5,
  62. retryDelay: 3000,
  63. // WebSocket相关数据
  64. isWsConnected: false,
  65. wsConnection: null,
  66. wsMessages: [],
  67. wsRetryCount: 0,
  68. wsMaxRetries: 5,
  69. wsRetryDelay: 3000,
  70. };
  71. },
  72. computed: {
  73. connectionStatusText() {
  74. return this.isConnected ? '已连接' : '未连接';
  75. },
  76. wsConnectionStatusText() {
  77. return this.isWsConnected ? '已连接' : '未连接';
  78. }
  79. },
  80. methods: {
  81. async connect() {
  82. if (this.isConnected) return;
  83. try {
  84. // 获取JWT token
  85. const token = localStorage.getItem('token');
  86. // 请求参数
  87. const requestData = {
  88. message: "压缩空气系统中变频器的应用如何实现节能?",
  89. user_id: 13817302290,
  90. session_id: ""/* 35fe2491-dca8-4a1e-b927-43bba0c2b669 */
  91. };
  92. // 调用接口获取SSE URL
  93. const response = await fetch('http://192.168.100.187:8062/api/agents/chat/multimodal/ ', {
  94. method: 'POST',
  95. mode: 'cors',
  96. credentials: 'include',
  97. headers: {
  98. 'Authorization': `JWT ${token}`,
  99. 'Content-Type': 'application/json',
  100. },
  101. body: JSON.stringify(requestData)
  102. });
  103. if (!response.ok) {
  104. throw new Error('获取SSE URL失败');
  105. }
  106. // 尝试从响应头获取SSE URL
  107. console.log('Response Headers:', response.headers);
  108. console.log('All Headers:', Array.from(response.headers.entries()));
  109. let sseUrl = response.headers.get('X-Sse-Url');
  110. // 如果响应头中没有,尝试从响应体中获取
  111. if (!sseUrl) {
  112. const responseData = await response.json();
  113. console.log('Response Data:', responseData);
  114. sseUrl = responseData.data.sse_url || responseData.sse_url;
  115. if (!sseUrl) {
  116. throw new Error('未找到SSE URL');
  117. }
  118. }
  119. // 将token添加到SSE URL中
  120. const urlWithToken = `http://192.168.100.187:8062${sseUrl}${sseUrl.includes('?') ? '&' : '?'}token=JWT ${token}`;
  121. this.eventSource = new EventSource(urlWithToken);
  122. // 通用消息处理器
  123. this.eventSource.onmessage = (event) => {
  124. console.log('收到SSE消息:', event.data);
  125. try {
  126. const data = JSON.parse(event.data);
  127. let messageContent = '';
  128. let messageType = 'message';
  129. switch(data.type) {
  130. case 'connected':
  131. messageContent = `已连接 - 会话ID: ${data.session_id}`;
  132. messageType = 'system';
  133. break;
  134. case 'heartbeat':
  135. // 不显示心跳消息
  136. return;
  137. case 'thinking_step':
  138. messageContent = data.data.content;
  139. messageType = 'thinking';
  140. break;
  141. case 'completed':
  142. messageContent = '会话已完成';
  143. messageType = 'system';
  144. break;
  145. default:
  146. messageContent = JSON.stringify(data);
  147. }
  148. if (messageContent) {
  149. this.addMessage(messageContent, messageType);
  150. }
  151. } catch (error) {
  152. console.error('解析SSE消息失败:', error);
  153. this.addMessage(`消息解析错误: ${error.message}`, 'error');
  154. }
  155. };
  156. // 自定义事件处理器
  157. this.eventSource.addEventListener('status', (event) => {
  158. const data = JSON.parse(event.data);
  159. this.addMessage(`状态更新: ${data.status}`, 'status');
  160. });
  161. this.eventSource.addEventListener('error', (event) => {
  162. this.addMessage(`错误: ${event.message || '连接错误'}`, 'error');
  163. });
  164. // 错误处理
  165. this.eventSource.onerror = (error) => {
  166. if (this.eventSource.readyState === EventSource.CLOSED) {
  167. this.addMessage('连接已关闭', 'system');
  168. } else {
  169. this.addMessage(`连接错误: ${error.message}`, 'error');
  170. this.attemptReconnect();
  171. }
  172. };
  173. this.isConnected = true;
  174. this.retryCount = 0;
  175. this.addMessage('成功连接到SSE服务器', 'system');
  176. } catch (error) {
  177. this.addMessage(`连接失败: ${error.message}`, 'error');
  178. this.attemptReconnect();
  179. }
  180. },
  181. disconnect() {
  182. if (this.eventSource) {
  183. this.eventSource.close();
  184. this.eventSource = null;
  185. this.isConnected = false;
  186. this.addMessage('已手动断开连接', 'system');
  187. }
  188. },
  189. attemptReconnect() {
  190. if (this.retryCount < this.maxRetries) {
  191. this.retryCount++;
  192. this.addMessage(`尝试重新连接 (${this.retryCount}/${this.maxRetries})...`, 'system');
  193. setTimeout(() => {
  194. this.connect();
  195. }, this.retryDelay);
  196. } else {
  197. this.addMessage('达到最大重试次数,停止尝试连接', 'error');
  198. this.isConnected = false;
  199. }
  200. },
  201. addMessage(content, type = 'message') {
  202. this.messages.unshift({
  203. content,
  204. type,
  205. timestamp: new Date()
  206. });
  207. // 限制消息数量
  208. if (this.messages.length > 100) {
  209. this.messages.pop();
  210. }
  211. },
  212. clearMessages() {
  213. this.messages = [];
  214. },
  215. formatTime(date) {
  216. return date.toLocaleTimeString();
  217. },
  218. // WebSocket相关方法
  219. async connectWs() {
  220. if (this.isWsConnected) return;
  221. try {
  222. const token = localStorage.getItem('token');
  223. const wsUrl = `ws://192.168.100.187:8062/ws/thinking/13/35fe2491-dca8-4a1e-b927-43bba0c2b669/?token=JWT ${token}`;
  224. this.wsConnection = new WebSocket(wsUrl);
  225. this.wsConnection.onopen = () => {
  226. this.isWsConnected = true;
  227. this.wsRetryCount = 0;
  228. this.addWsMessage('成功连接到WebSocket服务器', 'system');
  229. };
  230. this.wsConnection.onmessage = (event) => {
  231. console.log('收到WebSocket消息:', event.data);
  232. try {
  233. const data = JSON.parse(event.data);
  234. let messageContent = '';
  235. let messageType = 'message';
  236. switch(data.type) {
  237. case 'connected':
  238. messageContent = `已连接 - 会话ID: ${data.session_id}`;
  239. messageType = 'system';
  240. break;
  241. case 'thinking_step':
  242. messageContent = data.data.content;
  243. messageType = 'thinking';
  244. break;
  245. case 'completed':
  246. messageContent = '会话已完成';
  247. messageType = 'system';
  248. break;
  249. default:
  250. messageContent = JSON.stringify(data);
  251. }
  252. if (messageContent) {
  253. this.addWsMessage(messageContent, messageType);
  254. }
  255. } catch (error) {
  256. console.error('解析WebSocket消息失败:', error);
  257. this.addWsMessage(`消息解析错误: ${error.message}`, 'error');
  258. }
  259. };
  260. this.wsConnection.onerror = (error) => {
  261. this.addWsMessage(`连接错误: ${error.message || '未知错误'}`, 'error');
  262. this.attemptWsReconnect();
  263. };
  264. this.wsConnection.onclose = () => {
  265. this.isWsConnected = false;
  266. this.addWsMessage('连接已关闭', 'system');
  267. };
  268. } catch (error) {
  269. this.addWsMessage(`连接失败: ${error.message}`, 'error');
  270. this.attemptWsReconnect();
  271. }
  272. },
  273. disconnectWs() {
  274. if (this.wsConnection) {
  275. this.wsConnection.close();
  276. this.wsConnection = null;
  277. this.isWsConnected = false;
  278. this.addWsMessage('已手动断开连接', 'system');
  279. }
  280. },
  281. attemptWsReconnect() {
  282. if (this.wsRetryCount < this.wsMaxRetries) {
  283. this.wsRetryCount++;
  284. this.addWsMessage(`尝试重新连接 (${this.wsRetryCount}/${this.wsMaxRetries})...`, 'system');
  285. setTimeout(() => {
  286. this.connectWs();
  287. }, this.wsRetryDelay);
  288. } else {
  289. this.addWsMessage('达到最大重试次数,停止尝试连接', 'error');
  290. this.isWsConnected = false;
  291. }
  292. },
  293. addWsMessage(content, type = 'message') {
  294. this.wsMessages.unshift({
  295. content,
  296. type,
  297. timestamp: new Date()
  298. });
  299. // 限制消息数量
  300. if (this.wsMessages.length > 100) {
  301. this.wsMessages.pop();
  302. }
  303. },
  304. clearWsMessages() {
  305. this.wsMessages = [];
  306. },
  307. },
  308. beforeUnmount() {
  309. // 组件卸载前关闭所有连接
  310. if (this.eventSource) {
  311. this.eventSource.close();
  312. }
  313. if (this.wsConnection) {
  314. this.wsConnection.close();
  315. }
  316. }
  317. };
  318. </script>
  319. <style scoped>
  320. .sse-container {
  321. max-width: 800px;
  322. margin: 0 auto;
  323. padding: 20px;
  324. font-family: Arial, sans-serif;
  325. }
  326. .status-indicator {
  327. display: flex;
  328. align-items: center;
  329. gap: 10px;
  330. margin: 15px 0;
  331. font-size: 1.1em;
  332. }
  333. .status-dot {
  334. width: 12px;
  335. height: 12px;
  336. border-radius: 50%;
  337. background-color: #ccc;
  338. }
  339. .status-dot.connected {
  340. background-color: #4CAF50;
  341. box-shadow: 0 0 8px #4CAF50;
  342. }
  343. .controls {
  344. margin: 20px 0;
  345. display: flex;
  346. gap: 10px;
  347. }
  348. .controls button {
  349. padding: 8px 16px;
  350. border: none;
  351. border-radius: 4px;
  352. cursor: pointer;
  353. background-color: #f0f0f0;
  354. transition: background-color 0.3s;
  355. }
  356. .controls button:hover {
  357. background-color: #e0e0e0;
  358. }
  359. .controls button:disabled {
  360. opacity: 0.6;
  361. cursor: not-allowed;
  362. }
  363. .message-list {
  364. border: 1px solid #eee;
  365. border-radius: 4px;
  366. max-height: 500px;
  367. overflow-y: auto;
  368. padding: 10px;
  369. }
  370. .empty-message {
  371. text-align: center;
  372. padding: 20px;
  373. color: #888;
  374. }
  375. .message-item {
  376. margin-bottom: 15px;
  377. padding: 10px;
  378. border-radius: 4px;
  379. background-color: #f9f9f9;
  380. border-left: 4px solid #ddd;
  381. }
  382. .message-item.system {
  383. border-left-color: #2196F3;
  384. }
  385. .message-item.status {
  386. border-left-color: #4CAF50;
  387. }
  388. .message-item.error {
  389. border-left-color: #f44336;
  390. }
  391. .message-item.thinking {
  392. border-left-color: #9c27b0;
  393. background-color: #f3e5f5;
  394. }
  395. .message-header {
  396. display: flex;
  397. justify-content: space-between;
  398. margin-bottom: 5px;
  399. font-size: 0.9em;
  400. color: #666;
  401. }
  402. .message-type {
  403. font-weight: bold;
  404. }
  405. .message-content {
  406. word-break: break-word;
  407. }
  408. .ws-container {
  409. max-width: 800px;
  410. margin: 20px auto;
  411. padding: 20px;
  412. font-family: Arial, sans-serif;
  413. border-top: 1px solid #eee;
  414. }
  415. </style>