forum.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import { defineStore } from 'pinia'
  2. import request from '@/utils/request'
  3. import { message } from 'ant-design-vue'
  4. export interface Message {
  5. id: number
  6. forum_id: number
  7. persona_id: number
  8. moderator_id?: number | null
  9. speaker_name: string
  10. content: string
  11. thought?: string | null // Added thought field
  12. timestamp: string
  13. }
  14. export interface Moderator {
  15. id: number
  16. name: string
  17. title: string
  18. bio: string
  19. system_prompt?: string
  20. greeting_template?: string
  21. closing_template?: string
  22. summary_template?: string
  23. creator_id: number
  24. created_at: string
  25. }
  26. export interface Forum {
  27. id: number
  28. topic: string
  29. creator_id: number
  30. moderator_id?: number | null
  31. moderator?: Moderator | null
  32. status: string
  33. start_time?: string | null
  34. summary_history: string[]
  35. participants?: any[]
  36. duration_minutes?: number
  37. }
  38. export interface SystemLog {
  39. id?: number
  40. timestamp: string
  41. level: 'info' | 'warning' | 'error' | 'thought' | 'speech'
  42. content: string
  43. source?: string
  44. }
  45. export const useForumStore = defineStore('forum', {
  46. state: () => ({
  47. forums: [] as Forum[],
  48. currentForum: null as Forum | null,
  49. messages: [] as Message[],
  50. moderators: [] as Moderator[],
  51. systemLogs: [] as SystemLog[],
  52. loading: false,
  53. thinking: false,
  54. // WebSocket Global State
  55. ws: null as WebSocket | null,
  56. isConnected: false,
  57. wsForumId: null as number | null,
  58. heartbeatInterval: null as any,
  59. reconnectTimeout: null as any,
  60. reconnectAttempts: 0,
  61. isManuallyClosed: false
  62. }),
  63. actions: {
  64. systemLogKey(log: SystemLog) {
  65. const timestamp = new Date(log.timestamp).getTime()
  66. return `${timestamp}|${log.level}|${log.source || ''}|${log.content}`
  67. },
  68. mergeSystemLogs(...collections: SystemLog[][]) {
  69. const unique = new Map<string, SystemLog>()
  70. for (const log of collections.flat()) {
  71. const key = this.systemLogKey(log)
  72. const existing = unique.get(key)
  73. if (!existing || (existing.id == null && log.id != null)) {
  74. unique.set(key, log)
  75. }
  76. }
  77. return [...unique.values()].sort(
  78. (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
  79. )
  80. },
  81. // --- Persistence ---
  82. saveToStorage() {
  83. if (!this.currentForum) return
  84. const data = {
  85. forum: this.currentForum,
  86. messages: this.messages,
  87. logs: this.systemLogs,
  88. thinking: this.thinking,
  89. timestamp: Date.now()
  90. }
  91. try {
  92. localStorage.setItem(`forum_data_${this.currentForum.id}`, JSON.stringify(data))
  93. } catch (e) {
  94. console.error('Failed to save to storage', e)
  95. }
  96. },
  97. loadFromStorage(forumId: number): boolean {
  98. try {
  99. const raw = localStorage.getItem(`forum_data_${forumId}`)
  100. if (!raw) return false
  101. const data = JSON.parse(raw)
  102. // Validate data integrity
  103. // Must have a valid forum object with ID matching request
  104. if (!data.forum || data.forum.id !== forumId) {
  105. return false
  106. }
  107. this.currentForum = data.forum
  108. // Ensure messages are valid (must have speaker_name)
  109. this.messages = Array.isArray(data.messages)
  110. ? data.messages.filter((m: any) => m && typeof m.speaker_name === 'string')
  111. : []
  112. this.systemLogs = Array.isArray(data.logs) ? data.logs : []
  113. this.thinking = !!data.thinking
  114. return true
  115. } catch (e) {
  116. console.error('Failed to load from storage', e)
  117. return false
  118. }
  119. },
  120. // --- WebSocket Actions ---
  121. resolveWsBase() {
  122. const raw = (import.meta.env.VITE_WS_BASE_URL as string | undefined)?.trim()
  123. if (!raw) {
  124. const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
  125. return `${protocol}//${window.location.host}`
  126. }
  127. if (raw.startsWith('ws://') || raw.startsWith('wss://')) {
  128. return raw.replace(/\/$/, '')
  129. }
  130. if (raw.startsWith('http://') || raw.startsWith('https://')) {
  131. return raw.replace(/^http/, 'ws').replace(/\/$/, '')
  132. }
  133. return raw.replace(/\/$/, '')
  134. },
  135. clearTimers() {
  136. if (this.heartbeatInterval) clearInterval(this.heartbeatInterval)
  137. if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout)
  138. this.heartbeatInterval = null
  139. this.reconnectTimeout = null
  140. },
  141. disconnectWebSocket() {
  142. this.isManuallyClosed = true
  143. this.clearTimers()
  144. if (this.ws) {
  145. try {
  146. // Remove listeners to prevent reconnect loops on manual close
  147. this.ws.onclose = null
  148. this.ws.onerror = null
  149. this.ws.onmessage = null
  150. this.ws.onopen = null
  151. this.ws.close(1000, "Client initiated disconnect")
  152. } catch (e) { /* ignore */ }
  153. this.ws = null
  154. this.isConnected = false
  155. this.wsForumId = null
  156. }
  157. },
  158. connectWebSocket(forumId: number) {
  159. // If already connected to this forum, just return
  160. if (this.ws && this.isConnected && this.wsForumId === forumId && this.ws.readyState === WebSocket.OPEN) {
  161. return
  162. }
  163. // If connected to a DIFFERENT forum, disconnect first
  164. if (this.ws && (this.wsForumId !== forumId || this.ws.readyState !== WebSocket.OPEN)) {
  165. this.disconnectWebSocket()
  166. }
  167. // Start new connection
  168. this.isManuallyClosed = false
  169. this.wsForumId = forumId
  170. const wsBase = this.resolveWsBase()
  171. const token = localStorage.getItem('token')
  172. const wsUrl = `${wsBase}/api/v1/forums/${forumId}/ws${token ? `?token=${encodeURIComponent(token)}` : ''}`
  173. const maxReconnectAttempts = 10
  174. console.log(`[WS Global] Connecting to forum ${forumId}`)
  175. try {
  176. this.ws = new WebSocket(wsUrl)
  177. this.ws.onopen = () => {
  178. console.log('[WS Global] Connected successfully')
  179. this.isConnected = true
  180. this.reconnectAttempts = 0
  181. // Sync data on connect
  182. this.fetchMessages(forumId)
  183. this.clearTimers()
  184. // Heartbeat
  185. this.heartbeatInterval = setInterval(() => {
  186. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  187. this.ws.send('ping')
  188. }
  189. }, 30000)
  190. }
  191. this.ws.onmessage = (event) => {
  192. try {
  193. if (event.data === 'pong') return
  194. const data = JSON.parse(event.data)
  195. if (data.type === 'new_message' && data.data) {
  196. this.addMessage(data.data)
  197. } else if (data.type === 'message_chunk' && data.data) {
  198. this.updateStreamingMessage(data.data)
  199. } else if (data.type === 'system_log' && data.data) {
  200. this.addSystemLog(data.data)
  201. } else if (data.type === 'system' && data.content) {
  202. this.addSystemLog({
  203. timestamp: new Date().toISOString(),
  204. level: 'info',
  205. content: data.content,
  206. source: 'System'
  207. })
  208. } else if (data.type === 'status_update' && data.status && this.currentForum) {
  209. this.currentForum.status = data.status
  210. this.thinking = data.status === 'running'
  211. }
  212. } catch (e) {
  213. console.error('[WS Global] Parse Error', e)
  214. }
  215. }
  216. this.ws.onclose = (e) => {
  217. console.log(`[WS Global] Closed (Code: ${e.code})`)
  218. this.isConnected = false
  219. this.clearTimers()
  220. if (!this.isManuallyClosed && this.reconnectAttempts < maxReconnectAttempts) {
  221. const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)
  222. console.log(`[WS Global] Reconnecting in ${delay}ms...`)
  223. this.reconnectTimeout = setTimeout(() => {
  224. this.reconnectAttempts++
  225. // Recursive call via store instance?
  226. // We are inside action, so 'this' is store.
  227. // But setTimeout changes context. Need to capture 'this' or use arrow.
  228. this.connectWebSocket(forumId)
  229. }, delay)
  230. }
  231. }
  232. this.ws.onerror = () => {
  233. console.warn('[WS Global] Connection error')
  234. }
  235. } catch (e) {
  236. console.error('[WS Global] Connection Failed', e)
  237. }
  238. },
  239. async fetchSystemLogs(forumId: number) {
  240. try {
  241. const res = await request.get(`/forums/${forumId}/logs`)
  242. if (Array.isArray(res.data)) {
  243. const backendLogs = res.data as SystemLog[]
  244. // Smart Merge Strategy:
  245. // 1. Trust Backend Logs as base history.
  246. // 2. Keep Local Logs that are NOT present in Backend Logs (likely pending persistence).
  247. // Create signature set for O(1) lookup
  248. // Signature = timestamp + content (source/level might vary slightly but usually consistent)
  249. this.systemLogs = this.mergeSystemLogs(backendLogs, this.systemLogs)
  250. } else {
  251. // If response is invalid, keep local logs
  252. console.warn('Invalid logs response', res.data)
  253. }
  254. // Restore "thinking" or "speaking" state based on last log
  255. if (this.systemLogs.length > 0) {
  256. const lastLog = this.systemLogs[this.systemLogs.length - 1]
  257. if (lastLog.level === 'thought' || lastLog.content.includes('正在思考')) {
  258. this.thinking = true
  259. } else {
  260. this.thinking = false
  261. }
  262. }
  263. } catch (error) {
  264. console.error('Failed to fetch system logs:', error)
  265. }
  266. },
  267. addSystemLog(log: SystemLog) {
  268. this.systemLogs = this.mergeSystemLogs(this.systemLogs, [log])
  269. },
  270. updateStreamingMessage(chunk: {
  271. speaker_name: string,
  272. content: string,
  273. persona_id: number | null,
  274. moderator_id?: number | null,
  275. stream_id?: string,
  276. thought?: string | null,
  277. timestamp: string
  278. }) {
  279. if (!this.currentForum) return // Guard against updates when no forum loaded
  280. // Robust logic: Use stream_id if available to find the message
  281. // If stream_id is missing, fallback to last message match (legacy behavior)
  282. let targetMsg: Message | undefined
  283. if (chunk.stream_id) {
  284. targetMsg = this.messages.find(m => (m as any).stream_id === chunk.stream_id)
  285. } else {
  286. // Fallback: Check last message
  287. const lastMsg = this.messages[this.messages.length - 1]
  288. if (lastMsg && lastMsg.speaker_name === chunk.speaker_name && (lastMsg as any).isStreaming) {
  289. targetMsg = lastMsg
  290. }
  291. }
  292. if (targetMsg) {
  293. targetMsg.content += (chunk.content || '')
  294. // Thought usually comes with the first chunk or separately, update if present
  295. if (chunk.thought && !targetMsg.thought) {
  296. targetMsg.thought = chunk.thought
  297. }
  298. } else {
  299. // Start new streaming message
  300. const newMsg: Message = {
  301. id: Date.now(), // Temp ID, will be replaced by final message
  302. forum_id: this.currentForum?.id || 0,
  303. persona_id: chunk.persona_id || 0,
  304. moderator_id: chunk.moderator_id || null,
  305. speaker_name: chunk.speaker_name || 'Unknown',
  306. content: chunk.content || '',
  307. thought: chunk.thought, // Initialize thought
  308. timestamp: chunk.timestamp || new Date().toISOString(),
  309. }
  310. ;(newMsg as any).isStreaming = true
  311. ;(newMsg as any).stream_id = chunk.stream_id // Store stream_id for future chunks
  312. this.messages.push(newMsg)
  313. }
  314. // Throttle save for streaming (save every ~50 chars or just rely on manual save on exit?)
  315. // To be safe and meet "long time retention", let's save occasionally
  316. if (Math.random() < 0.1) { // 10% chance to save on chunk update
  317. this.saveToStorage()
  318. }
  319. },
  320. addMessage(msg: Message & { stream_id?: string }) {
  321. if (!this.currentForum) return // Guard against updates when no forum loaded
  322. // When the full message arrives (type: 'new_message'), replace the streaming one
  323. // Match by stream_id if available, otherwise fallback
  324. let streamingMsgIndex = -1
  325. if (msg.stream_id) {
  326. streamingMsgIndex = this.messages.findIndex(m => (m as any).stream_id === msg.stream_id)
  327. }
  328. // Fallback match if stream_id not found or not provided
  329. if (streamingMsgIndex === -1) {
  330. streamingMsgIndex = this.messages.findIndex(m => m.speaker_name === msg.speaker_name && (m as any).isStreaming)
  331. }
  332. if (streamingMsgIndex !== -1) {
  333. // Replace streaming message with the final one
  334. this.messages.splice(streamingMsgIndex, 1, msg)
  335. } else {
  336. // Check if message already exists by ID to prevent duplicates
  337. const exists = this.messages.find(m => m.id === msg.id)
  338. if (!exists) {
  339. this.messages.push(msg)
  340. }
  341. }
  342. this.saveToStorage() // Always save on full message
  343. // Auto-scroll logic could be triggered here or in component watcher
  344. },
  345. async fetchForums() {
  346. // Background update if data exists
  347. const isBackground = this.forums.length > 0
  348. if (!isBackground) {
  349. this.loading = true
  350. }
  351. try {
  352. const res = await request.get('/forums/', { params: { limit: 500 } })
  353. this.forums = res.data
  354. } catch (error) {
  355. console.error('Failed to fetch forums:', error)
  356. } finally {
  357. this.loading = false
  358. }
  359. },
  360. async fetchForum(id: number) {
  361. // 1. Check if ID is valid
  362. if (!id || isNaN(id)) {
  363. console.error('Invalid forum ID', id)
  364. this.currentForum = null
  365. return
  366. }
  367. // 2. Memory Cache: If we already have THIS forum loaded, just refresh it
  368. if (this.currentForum && this.currentForum.id === id) {
  369. this.refreshForumData(id)
  370. return
  371. }
  372. // 3. Switching or Initial Load: ALWAYS clear old data first to prevent ghosting
  373. this.clearForumData()
  374. // 4. Storage Cache: Try to load from localStorage
  375. if (this.loadFromStorage(id)) {
  376. // If loaded from storage, we can show it immediately
  377. // But if it's 'pending', we don't even need to refresh messages/logs
  378. if (this.currentForum?.status !== 'pending') {
  379. this.refreshForumData(id)
  380. }
  381. return
  382. }
  383. // 5. Fresh Load from Network
  384. this.loading = true
  385. try {
  386. // First, get the forum metadata to check status
  387. const forumRes = await request.get(`/forums/${id}`)
  388. if (!forumRes.data) throw new Error('Empty response')
  389. this.currentForum = forumRes.data
  390. // If forum is 'pending', it's brand new or hasn't started, no need to fetch messages/logs
  391. if (this.currentForum?.status !== 'pending') {
  392. const [messagesRes, logsRes] = await Promise.all([
  393. request.get(`/forums/${id}/messages`).catch(e => ({ data: [] })),
  394. request.get(`/forums/${id}/logs`).catch(e => ({ data: [] }))
  395. ])
  396. // Process messages
  397. if (Array.isArray(messagesRes.data)) {
  398. this.messages = messagesRes.data.filter((m: any) => m && typeof m.speaker_name === 'string')
  399. }
  400. // Process logs
  401. if (Array.isArray(logsRes.data)) {
  402. this.systemLogs = logsRes.data
  403. }
  404. // Restore thinking state
  405. this.updateThinkingState()
  406. } else {
  407. // Brand new or pending forum - ensure clean state
  408. this.messages = []
  409. this.systemLogs = []
  410. this.thinking = false
  411. }
  412. this.saveToStorage()
  413. } catch (error) {
  414. const status = (error as { response?: { status?: number } })?.response?.status
  415. if (status !== 401 && status !== 403 && status !== 404) {
  416. console.error(`Failed to fetch forum ${id}:`, error)
  417. }
  418. this.currentForum = null
  419. } finally {
  420. this.loading = false
  421. }
  422. },
  423. updateThinkingState() {
  424. if (this.systemLogs.length > 0) {
  425. const lastLog = this.systemLogs[this.systemLogs.length - 1]
  426. if (lastLog.level === 'thought' || lastLog.content.includes('正在思考')) {
  427. this.thinking = true
  428. } else {
  429. this.thinking = false
  430. }
  431. } else {
  432. this.thinking = false
  433. }
  434. },
  435. async refreshForumData(id: number) {
  436. // Background refresh logic
  437. try {
  438. const forumRes = await request.get(`/forums/${id}`).catch(e => null)
  439. if (!forumRes || !forumRes.data) return
  440. // Only update if we are still on the same forum
  441. if (!this.currentForum || this.currentForum.id !== id) return
  442. this.currentForum = { ...this.currentForum, ...forumRes.data }
  443. // Only fetch messages/logs if NOT pending
  444. if (this.currentForum.status !== 'pending') {
  445. const [messagesRes, logsRes] = await Promise.all([
  446. request.get(`/forums/${id}/messages`).catch(e => null),
  447. request.get(`/forums/${id}/logs`).catch(e => null)
  448. ])
  449. if (messagesRes && Array.isArray(messagesRes.data)) {
  450. this.messages = messagesRes.data.filter((m: any) => m && typeof m.speaker_name === 'string')
  451. }
  452. if (logsRes && Array.isArray(logsRes.data)) {
  453. this.systemLogs = logsRes.data
  454. this.updateThinkingState()
  455. }
  456. }
  457. this.saveToStorage()
  458. } catch (e) {
  459. console.error('Background fetch failed', e)
  460. }
  461. },
  462. async fetchMessages(forumId: number) {
  463. try {
  464. const res = await request.get(`/forums/${forumId}/messages`)
  465. // Validate array
  466. if (Array.isArray(res.data)) {
  467. // Filter invalid messages
  468. this.messages = res.data.filter((m: any) => m && typeof m.speaker_name === 'string')
  469. } else {
  470. console.warn('Invalid messages format', res.data)
  471. this.messages = []
  472. }
  473. await this.fetchSystemLogs(forumId)
  474. // Save after successful fetch to keep storage fresh
  475. if (this.currentForum && this.currentForum.id === forumId) {
  476. this.saveToStorage()
  477. }
  478. } catch (error) {
  479. console.error(`Failed to fetch messages for forum ${forumId}:`, error)
  480. // Do not clear messages on error to keep cache displayed
  481. }
  482. },
  483. async fetchModerators() {
  484. try {
  485. const res = await request.get('/moderators/')
  486. this.moderators = res.data
  487. } catch (error) {
  488. console.error('Failed to fetch moderators:', error)
  489. this.moderators = []
  490. }
  491. },
  492. async createForum(topic: string, participantIds: number[], duration: number, moderatorId?: number) {
  493. this.loading = true
  494. try {
  495. const normalizedParticipantIds = Array.from(
  496. new Set(
  497. participantIds
  498. .map(id => Number(id))
  499. .filter(id => Number.isInteger(id) && id > 0)
  500. )
  501. )
  502. const res = await request.post('/forums/', {
  503. topic,
  504. participant_ids: normalizedParticipantIds,
  505. moderator_id: moderatorId,
  506. duration_minutes: duration
  507. })
  508. message.success('论坛创建成功')
  509. // Optimistic update: Add to list immediately
  510. this.forums.unshift(res.data)
  511. return res.data
  512. } catch (error) {
  513. console.error('Failed to create forum:', error)
  514. throw error
  515. } finally {
  516. this.loading = false
  517. }
  518. },
  519. async startForum(id: number) {
  520. try {
  521. const res = await request.post(`/forums/${id}/start`)
  522. message.success('论坛已开始')
  523. if (this.currentForum && this.currentForum.id === id) {
  524. this.currentForum.status = 'running'
  525. if (res.data?.start_time) this.currentForum.start_time = res.data.start_time
  526. if (res.data?.duration_minutes) this.currentForum.duration_minutes = res.data.duration_minutes
  527. }
  528. } catch (error) {
  529. console.error('Failed to start forum:', error)
  530. message.error('启动失败')
  531. }
  532. },
  533. async deleteForum(id: number) {
  534. // Optimistic update: Remove locally first
  535. const previousForums = [...this.forums]
  536. const previousCurrentForum = this.currentForum
  537. this.forums = this.forums.filter(f => f.id !== id)
  538. // Clear memory if deleting current
  539. if (this.currentForum && this.currentForum.id === id) {
  540. this.clearForumData()
  541. }
  542. try {
  543. await request.delete(`/forums/${id}`)
  544. // Clean storage
  545. localStorage.removeItem(`forum_data_${id}`)
  546. } catch (error) {
  547. console.error('Failed to delete forum:', error)
  548. // Rollback on failure
  549. this.forums = previousForums
  550. this.currentForum = previousCurrentForum
  551. // Also restore storage if needed? (Too complex, assume delete failure implies data still exists)
  552. throw error
  553. }
  554. },
  555. // New Action: Stop Forum
  556. async stopForum(id: number) {
  557. try {
  558. await request.post(`/forums/${id}/stop`)
  559. // Update local status if applicable
  560. const f = this.forums.find(f => f.id === id)
  561. if (f) f.status = 'closed'
  562. if (this.currentForum && this.currentForum.id === id) {
  563. this.currentForum.status = 'closed'
  564. }
  565. message.success('论坛已停止')
  566. } catch (error) {
  567. console.error('Failed to stop forum:', error)
  568. message.error('停止失败')
  569. }
  570. },
  571. leaveForum() {
  572. // Save current state before leaving
  573. this.saveToStorage()
  574. // Don't clear messages immediately to prevent flicker when switching
  575. // But clearing currentForum is fine
  576. // Actually, clearing messages is safer to avoid showing wrong forum data
  577. // MODIFIED: Don't clear if we are just navigating back but might return (keep cache)
  578. // But user asked "即使用户点击返回,页面也不会卸载,以便不重复读取"
  579. // So we should NOT clear messages here.
  580. // this.messages = [] // Keep messages in store
  581. // this.systemLogs = [] // Keep logs
  582. // But if we enter ANOTHER forum, we must clear.
  583. // fetchForum() handles clearing: `this.currentForum = null` and re-fetching.
  584. // However, we should stop thinking state?
  585. this.thinking = false
  586. this.loading = false
  587. // We only clear currentForum ref but keep data until overwritten?
  588. // No, if we clear currentForum, UI might break if it relies on it.
  589. // Let's keep currentForum too, but maybe mark as "inactive"?
  590. // The requirement says: "user current executing forum, page won't unload".
  591. // This implies keeping the state.
  592. // So leaveForum should be minimal.
  593. },
  594. // New Action: Clear Forum Data (explicitly called when needed, e.g. entering NEW forum)
  595. clearForumData() {
  596. this.messages = []
  597. this.systemLogs = []
  598. this.currentForum = null
  599. this.thinking = false
  600. }
  601. }
  602. })