tgo-widget-miniprogram 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +81 -0
  2. package/miniprogram_dist/adapters/request.js +38 -0
  3. package/miniprogram_dist/adapters/storage.js +42 -0
  4. package/miniprogram_dist/adapters/systemInfo.js +22 -0
  5. package/miniprogram_dist/chat/index.js +164 -0
  6. package/miniprogram_dist/chat/index.json +7 -0
  7. package/miniprogram_dist/chat/index.wxml +48 -0
  8. package/miniprogram_dist/chat/index.wxss +92 -0
  9. package/miniprogram_dist/components/json-render-element/index.js +374 -0
  10. package/miniprogram_dist/components/json-render-element/index.json +6 -0
  11. package/miniprogram_dist/components/json-render-element/index.wxml +218 -0
  12. package/miniprogram_dist/components/json-render-element/index.wxss +450 -0
  13. package/miniprogram_dist/components/json-render-message/index.js +89 -0
  14. package/miniprogram_dist/components/json-render-message/index.json +7 -0
  15. package/miniprogram_dist/components/json-render-message/index.wxml +25 -0
  16. package/miniprogram_dist/components/json-render-message/index.wxss +26 -0
  17. package/miniprogram_dist/components/json-render-surface/index.js +116 -0
  18. package/miniprogram_dist/components/json-render-surface/index.json +6 -0
  19. package/miniprogram_dist/components/json-render-surface/index.wxml +10 -0
  20. package/miniprogram_dist/components/json-render-surface/index.wxss +6 -0
  21. package/miniprogram_dist/components/markdown-text/index.js +23 -0
  22. package/miniprogram_dist/components/markdown-text/index.json +3 -0
  23. package/miniprogram_dist/components/markdown-text/index.wxml +1 -0
  24. package/miniprogram_dist/components/markdown-text/index.wxss +6 -0
  25. package/miniprogram_dist/components/message-bubble/index.js +12 -0
  26. package/miniprogram_dist/components/message-bubble/index.json +3 -0
  27. package/miniprogram_dist/components/message-bubble/index.wxml +3 -0
  28. package/miniprogram_dist/components/message-bubble/index.wxss +17 -0
  29. package/miniprogram_dist/components/message-input/index.js +76 -0
  30. package/miniprogram_dist/components/message-input/index.json +3 -0
  31. package/miniprogram_dist/components/message-input/index.wxml +28 -0
  32. package/miniprogram_dist/components/message-input/index.wxss +56 -0
  33. package/miniprogram_dist/components/message-list/index.js +113 -0
  34. package/miniprogram_dist/components/message-list/index.json +9 -0
  35. package/miniprogram_dist/components/message-list/index.wxml +108 -0
  36. package/miniprogram_dist/components/message-list/index.wxss +113 -0
  37. package/miniprogram_dist/components/system-message/index.js +8 -0
  38. package/miniprogram_dist/components/system-message/index.json +3 -0
  39. package/miniprogram_dist/components/system-message/index.wxml +3 -0
  40. package/miniprogram_dist/components/system-message/index.wxss +15 -0
  41. package/miniprogram_dist/core/chatStore.js +758 -0
  42. package/miniprogram_dist/core/i18n.js +66 -0
  43. package/miniprogram_dist/core/platformStore.js +86 -0
  44. package/miniprogram_dist/core/types.js +192 -0
  45. package/miniprogram_dist/services/chat.js +67 -0
  46. package/miniprogram_dist/services/messageHistory.js +46 -0
  47. package/miniprogram_dist/services/platform.js +27 -0
  48. package/miniprogram_dist/services/upload.js +74 -0
  49. package/miniprogram_dist/services/visitor.js +67 -0
  50. package/miniprogram_dist/services/wukongim.js +183 -0
  51. package/miniprogram_dist/utils/jsonRender.js +158 -0
  52. package/miniprogram_dist/utils/markdown.js +31 -0
  53. package/miniprogram_dist/utils/time.js +85 -0
  54. package/miniprogram_dist/utils/uid.js +11 -0
  55. package/package.json +37 -0
@@ -0,0 +1,758 @@
1
+ /**
2
+ * Chat state management (Observable singleton)
3
+ * Ported from tgo-widget-app chatStore.ts
4
+ */
5
+ var IMService = require('../services/wukongim')
6
+ var visitorService = require('../services/visitor')
7
+ var historyService = require('../services/messageHistory')
8
+ var chatService = require('../services/chat')
9
+ var uploadService = require('../services/upload')
10
+ var systemInfoAdapter = require('../adapters/systemInfo')
11
+ var types = require('./types')
12
+ var platformStore = require('./platformStore')
13
+ var uidUtil = require('../utils/uid')
14
+ var jsonRenderUtils = require('../utils/jsonRender')
15
+
16
+ // Module-level unsub guards
17
+ var offMsg = null
18
+ var offStatus = null
19
+ var offCustom = null
20
+
21
+ // Stream timeout
22
+ var streamTimer = null
23
+ var STREAM_TIMEOUT_MS = 60000
24
+
25
+ // Per-clientMsgNo MixedStreamParser instances
26
+ var activeParsers = {}
27
+
28
+ function ChatStore() {
29
+ this._state = {
30
+ messages: [],
31
+ online: false,
32
+ initializing: false,
33
+ error: null,
34
+ // history
35
+ historyLoading: false,
36
+ historyHasMore: true,
37
+ historyError: null,
38
+ earliestSeq: null,
39
+ // identity
40
+ apiBase: '',
41
+ platformApiKey: '',
42
+ myUid: '',
43
+ channelId: '',
44
+ channelType: 251,
45
+ // streaming
46
+ isStreaming: false,
47
+ streamCanceling: false,
48
+ streamingClientMsgNo: ''
49
+ }
50
+ this._listeners = []
51
+ this._throttleTimer = null
52
+ this._pendingState = null
53
+ }
54
+
55
+ ChatStore.prototype.getState = function () {
56
+ return this._state
57
+ }
58
+
59
+ ChatStore.prototype._setState = function (partial) {
60
+ Object.assign(this._state, partial)
61
+ var state = this._state
62
+ this._listeners.forEach(function (fn) { try { fn(state) } catch (e) {} })
63
+ }
64
+
65
+ /**
66
+ * Throttled setState for streaming updates (50ms batch).
67
+ * Mutates this._state immediately (so subsequent reads see latest data),
68
+ * but defers listener notification to avoid excessive setData calls.
69
+ */
70
+ ChatStore.prototype._setStateThrottled = function (partial) {
71
+ var self = this
72
+ // Apply state mutation immediately so next read sees latest data
73
+ Object.assign(this._state, partial)
74
+ // Throttle listener notification
75
+ if (!this._throttleTimer) {
76
+ this._throttleTimer = setTimeout(function () {
77
+ self._throttleTimer = null
78
+ var state = self._state
79
+ self._listeners.forEach(function (fn) { try { fn(state) } catch (e) {} })
80
+ }, 50)
81
+ }
82
+ }
83
+
84
+ ChatStore.prototype.subscribe = function (fn) {
85
+ this._listeners.push(fn)
86
+ var self = this
87
+ return function () {
88
+ var idx = self._listeners.indexOf(fn)
89
+ if (idx >= 0) self._listeners.splice(idx, 1)
90
+ }
91
+ }
92
+
93
+ // ========== Initialization ==========
94
+
95
+ ChatStore.prototype.initIM = function (cfg) {
96
+ if (!cfg || !cfg.apiBase || !cfg.platformApiKey) return Promise.resolve()
97
+ var self = this
98
+ var st = this._state
99
+ if (st.initializing || IMService.isReady()) return Promise.resolve()
100
+
101
+ this._setState({ initializing: true, error: null, apiBase: cfg.apiBase, platformApiKey: cfg.platformApiKey })
102
+
103
+ var apiBase = cfg.apiBase
104
+ var platformApiKey = cfg.platformApiKey
105
+
106
+ // Load or register visitor
107
+ var cached = visitorService.loadCachedVisitor(apiBase, platformApiKey)
108
+ var visitorPromise
109
+ if (cached) {
110
+ visitorPromise = Promise.resolve(cached)
111
+ } else {
112
+ var sysInfo = systemInfoAdapter.collectSystemInfo()
113
+ visitorPromise = visitorService.registerVisitor({
114
+ apiBase: apiBase,
115
+ platformApiKey: platformApiKey,
116
+ extra: sysInfo ? { system_info: sysInfo } : {}
117
+ }).then(function (res) {
118
+ visitorService.saveCachedVisitor(apiBase, platformApiKey, res)
119
+ return visitorService.loadCachedVisitor(apiBase, platformApiKey)
120
+ })
121
+ }
122
+
123
+ return visitorPromise.then(function (visitor) {
124
+ var uid = String(visitor.visitor_id || '')
125
+ var uidForIM = uid.endsWith('-vtr') ? uid : uid + '-vtr'
126
+ var target = visitor.channel_id
127
+ var token = visitor.im_token
128
+
129
+ self._setState({
130
+ myUid: uidForIM,
131
+ channelId: target,
132
+ channelType: visitor.channel_type || 251
133
+ })
134
+
135
+ if (!token) {
136
+ throw new Error('[Chat] Missing im_token from visitor registration')
137
+ }
138
+
139
+ return IMService.init({
140
+ apiBase: apiBase,
141
+ uid: uidForIM,
142
+ token: token,
143
+ target: target,
144
+ channelType: 'group'
145
+ })
146
+ }).then(function () {
147
+ // Subscribe to IM events
148
+ self._bindIMEvents()
149
+ return IMService.connect()
150
+ }).then(function () {
151
+ // Load initial history
152
+ return self.loadInitialHistory(20)
153
+ }).catch(function (e) {
154
+ var errMsg = e && e.message ? e.message : String(e)
155
+ console.error('[Chat] IM initialization failed:', errMsg)
156
+ self._setState({ error: errMsg, online: false })
157
+ }).then(function () {
158
+ self._setState({ initializing: false })
159
+ })
160
+ }
161
+
162
+ ChatStore.prototype._bindIMEvents = function () {
163
+ var self = this
164
+ var uidForIM = this._state.myUid
165
+
166
+ // Status events
167
+ if (offStatus) { try { offStatus() } catch (e) {} ; offStatus = null }
168
+ offStatus = IMService.onStatus(function (s) {
169
+ self._setState({ online: s === 'connected' })
170
+ })
171
+
172
+ // Message events
173
+ if (offMsg) { try { offMsg() } catch (e) {} ; offMsg = null }
174
+ offMsg = IMService.onMessage(function (m) {
175
+ if (!m.fromUid || m.fromUid === uidForIM) return
176
+
177
+ var chat = {
178
+ id: String(m.messageId),
179
+ role: 'agent',
180
+ payload: types.toPayloadFromAny(m && m.payload),
181
+ time: new Date(m.timestamp * 1000),
182
+ messageSeq: typeof m.messageSeq === 'number' ? m.messageSeq : undefined,
183
+ clientMsgNo: m.clientMsgNo,
184
+ fromUid: m.fromUid,
185
+ channelId: m.channelId,
186
+ channelType: m.channelType
187
+ }
188
+
189
+ var currentMessages = self._state.messages
190
+ for (var i = 0; i < currentMessages.length; i++) {
191
+ if (currentMessages[i].id === chat.id) return
192
+ }
193
+
194
+ // Merge into streaming placeholder if exists
195
+ if (chat.clientMsgNo) {
196
+ var idx = -1
197
+ for (var j = 0; j < currentMessages.length; j++) {
198
+ if (currentMessages[j].clientMsgNo && currentMessages[j].clientMsgNo === chat.clientMsgNo) {
199
+ idx = j
200
+ break
201
+ }
202
+ }
203
+ if (idx >= 0) {
204
+ var next = currentMessages.slice()
205
+ next[idx] = Object.assign({}, currentMessages[idx], chat, { streamData: undefined })
206
+ self._setState({ messages: next })
207
+ return
208
+ }
209
+ }
210
+
211
+ self._setState({ messages: currentMessages.concat([chat]) })
212
+ })
213
+
214
+ // Custom stream events
215
+ if (offCustom) { try { offCustom() } catch (e) {} ; offCustom = null }
216
+ offCustom = IMService.onCustom(function (ev) {
217
+ try {
218
+ if (!ev) return
219
+ var customEvent = ev
220
+
221
+ // Parse event data
222
+ var eventData = null
223
+ if (customEvent.dataJson && typeof customEvent.dataJson === 'object') {
224
+ eventData = customEvent.dataJson
225
+ } else if (typeof customEvent.data === 'string') {
226
+ try { eventData = JSON.parse(customEvent.data) } catch (e) {}
227
+ } else if (customEvent.data && typeof customEvent.data === 'object') {
228
+ eventData = customEvent.data
229
+ }
230
+
231
+ var newEventType = (eventData && eventData.event_type) || customEvent.type || ''
232
+ var clientMsgNo = (eventData && eventData.client_msg_no) ? String(eventData.client_msg_no) : (customEvent.id ? String(customEvent.id) : '')
233
+
234
+ // --- Stream API v2 ---
235
+ if (newEventType === 'stream.delta') {
236
+ if (!clientMsgNo) return
237
+ var payload = eventData && eventData.payload
238
+ var delta = payload && payload.delta
239
+ if (delta) {
240
+ var parser = activeParsers[clientMsgNo]
241
+ if (!parser) {
242
+ parser = jsonRenderUtils.createMixedStreamParser({
243
+ onText: function (t) { self.appendMixedPart(clientMsgNo, { type: 'text', text: t + '\n' }) },
244
+ onPatch: function (p) { self.appendMixedPart(clientMsgNo, { type: 'data-spec', data: { type: 'patch', patch: p } }) }
245
+ })
246
+ activeParsers[clientMsgNo] = parser
247
+ }
248
+ parser.push(String(delta).replace(/([^\n])```spec/g, '$1\n```spec'))
249
+ }
250
+ return
251
+ }
252
+
253
+ if (newEventType === 'stream.close') {
254
+ if (!clientMsgNo) return
255
+ var closeParser = activeParsers[clientMsgNo]
256
+ if (closeParser) { closeParser.flush(); delete activeParsers[clientMsgNo] }
257
+ var errMsg = (eventData && eventData.payload && eventData.payload.end_reason > 0) ? '流异常结束' : undefined
258
+ self.finalizeStreamMessage(clientMsgNo, errMsg)
259
+ self.markStreamingEnd()
260
+ return
261
+ }
262
+
263
+ if (newEventType === 'stream.error') {
264
+ if (!clientMsgNo) return
265
+ var errParser = activeParsers[clientMsgNo]
266
+ if (errParser) { errParser.flush(); delete activeParsers[clientMsgNo] }
267
+ var errMessage = (eventData && eventData.payload && eventData.payload.error) || '未知错误'
268
+ self.finalizeStreamMessage(clientMsgNo, errMessage)
269
+ self.markStreamingEnd()
270
+ return
271
+ }
272
+
273
+ if (newEventType === 'stream.cancel') {
274
+ if (!clientMsgNo) return
275
+ var cancelParser = activeParsers[clientMsgNo]
276
+ if (cancelParser) { cancelParser.flush(); delete activeParsers[clientMsgNo] }
277
+ self.finalizeStreamMessage(clientMsgNo)
278
+ self.markStreamingEnd()
279
+ return
280
+ }
281
+
282
+ if (newEventType === 'stream.finish') {
283
+ return
284
+ }
285
+
286
+ // --- Legacy event format ---
287
+ if (customEvent.type === '___TextMessageStart') {
288
+ var startId = customEvent.id ? String(customEvent.id) : ''
289
+ if (startId) self.markStreamingStart(startId)
290
+ return
291
+ }
292
+
293
+ if (customEvent.type === '___TextMessageContent') {
294
+ var contentId = customEvent.id ? String(customEvent.id) : ''
295
+ if (!contentId) return
296
+ var chunk = typeof customEvent.data === 'string' ? customEvent.data : (customEvent.data != null ? String(customEvent.data) : '')
297
+ if (chunk) self.appendStreamData(contentId, chunk)
298
+ return
299
+ }
300
+
301
+ if (customEvent.type === '___TextMessageEnd') {
302
+ var endId = customEvent.id ? String(customEvent.id) : ''
303
+ if (!endId) return
304
+ var endError = customEvent.data ? String(customEvent.data) : undefined
305
+ self.finalizeStreamMessage(endId, endError)
306
+ self.markStreamingEnd()
307
+ return
308
+ }
309
+ } catch (err) {
310
+ console.error('[Chat] Custom event handler error:', err)
311
+ }
312
+ })
313
+ }
314
+
315
+ // ========== Send Message ==========
316
+
317
+ ChatStore.prototype.sendMessage = function (text) {
318
+ var v = (text || '').trim()
319
+ if (!v) return Promise.resolve()
320
+
321
+ var self = this
322
+ var clientMsgNo = uidUtil.generateClientMsgNo('cmn')
323
+ var id = 'u-' + Date.now()
324
+ var msg = {
325
+ id: id,
326
+ role: 'user',
327
+ payload: { type: 1, content: v },
328
+ time: new Date(),
329
+ status: 'sending',
330
+ clientMsgNo: clientMsgNo
331
+ }
332
+
333
+ // Add to messages immediately
334
+ this._setState({ messages: this._state.messages.concat([msg]) })
335
+
336
+ var st = this._state
337
+ if (!st.apiBase || !st.platformApiKey || !st.myUid) {
338
+ this._updateMessage(id, { status: undefined, errorMessage: 'Not initialized' })
339
+ return Promise.resolve()
340
+ }
341
+
342
+ // Auto-cancel previous streaming
343
+ var cancelPromise = st.isStreaming ? this.cancelStreaming('auto_cancel_on_new_send') : Promise.resolve()
344
+
345
+ return cancelPromise.then(function () {
346
+ // Wait for IM ready
347
+ if (!IMService.isReady()) {
348
+ return self._waitForIMReady(10000)
349
+ }
350
+ }).then(function () {
351
+ if (!IMService.isReady()) {
352
+ throw new Error('IM service is not ready')
353
+ }
354
+ return IMService.sendText(v, { clientMsgNo: clientMsgNo })
355
+ }).then(function (result) {
356
+ var ReasonCode = require('easyjssdk').ReasonCode
357
+ if (result.reasonCode !== ReasonCode.Success) {
358
+ self._updateMessage(id, { status: undefined, reasonCode: result.reasonCode })
359
+ return
360
+ }
361
+
362
+ // Call chat completion API
363
+ return chatService.sendChatCompletion({
364
+ apiBase: self._state.apiBase,
365
+ platformApiKey: self._state.platformApiKey,
366
+ message: v,
367
+ fromUid: self._state.myUid,
368
+ channelId: self._state.channelId,
369
+ channelType: self._state.channelType
370
+ }).then(function () {
371
+ self._updateMessage(id, { status: undefined, reasonCode: result.reasonCode })
372
+ })
373
+ }).catch(function (e) {
374
+ console.error('[Chat] Send failed:', e)
375
+ self.markStreamingEnd()
376
+ var ReasonCode = require('easyjssdk').ReasonCode
377
+ self._updateMessage(id, { status: undefined, reasonCode: ReasonCode.Unknown })
378
+ self._setState({ error: e && e.message ? e.message : String(e) })
379
+ })
380
+ }
381
+
382
+ ChatStore.prototype._waitForIMReady = function (timeoutMs) {
383
+ var start = Date.now()
384
+ return new Promise(function (resolve) {
385
+ var check = function () {
386
+ if (IMService.isReady() || (Date.now() - start) >= timeoutMs) {
387
+ resolve()
388
+ } else {
389
+ setTimeout(check, 120)
390
+ }
391
+ }
392
+ check()
393
+ })
394
+ }
395
+
396
+ ChatStore.prototype._updateMessage = function (id, partial) {
397
+ var messages = this._state.messages.map(function (m) {
398
+ if (m.id === id) return Object.assign({}, m, partial)
399
+ return m
400
+ })
401
+ this._setState({ messages: messages })
402
+ }
403
+
404
+ // ========== Upload ==========
405
+
406
+ ChatStore.prototype.uploadImage = function (tempFilePath) {
407
+ var self = this
408
+ var st = this._state
409
+ if (!st.apiBase || !st.channelId) {
410
+ this._setState({ error: '[Upload] Not initialized' })
411
+ return Promise.resolve()
412
+ }
413
+
414
+ var clientMsgNo = uidUtil.generateClientMsgNo('um')
415
+ var id = 'u-up-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6)
416
+ var placeholder = {
417
+ id: id,
418
+ role: 'user',
419
+ payload: { type: 1, content: '图片上传中…' },
420
+ time: new Date(),
421
+ status: 'uploading',
422
+ uploadProgress: 0,
423
+ clientMsgNo: clientMsgNo
424
+ }
425
+ this._setState({ messages: this._state.messages.concat([placeholder]) })
426
+
427
+ return uploadService.getImageInfo(tempFilePath).then(function (dims) {
428
+ return uploadService.uploadChatFile({
429
+ apiBase: st.apiBase,
430
+ apiKey: st.platformApiKey,
431
+ channelId: st.channelId,
432
+ channelType: st.channelType,
433
+ filePath: tempFilePath,
434
+ onProgress: function (p) {
435
+ self._updateMessage(id, { uploadProgress: p })
436
+ }
437
+ }).then(function (res) {
438
+ var w = dims ? Math.max(1, dims.width) : 1
439
+ var h = dims ? Math.max(1, dims.height) : 1
440
+ var payload = { type: 2, url: res.file_url, width: w, height: h }
441
+ self._updateMessage(id, { payload: payload, status: 'sending', uploadProgress: undefined })
442
+ return IMService.sendPayload(payload, { clientMsgNo: clientMsgNo })
443
+ }).then(function (result) {
444
+ self._updateMessage(id, { status: undefined, reasonCode: result && result.reasonCode })
445
+ })
446
+ }).catch(function (err) {
447
+ console.error('[Chat] Upload failed:', err)
448
+ self._updateMessage(id, { status: undefined, uploadError: err && err.message ? err.message : '上传失败' })
449
+ })
450
+ }
451
+
452
+ // ========== History ==========
453
+
454
+ ChatStore.prototype.loadInitialHistory = function (limit) {
455
+ limit = limit || 20
456
+ var self = this
457
+ var st = this._state
458
+ if (!st.channelId || !st.channelType) return Promise.resolve()
459
+ if (st.historyLoading) return Promise.resolve()
460
+
461
+ this._setState({ historyLoading: true, historyError: null })
462
+
463
+ return historyService.syncVisitorMessages({
464
+ apiBase: st.apiBase,
465
+ platformApiKey: st.platformApiKey,
466
+ channelId: st.channelId,
467
+ channelType: st.channelType,
468
+ startSeq: 0,
469
+ endSeq: 0,
470
+ limit: limit,
471
+ pullMode: 1
472
+ }).then(function (res) {
473
+ var myUid = self._state.myUid
474
+ var list = res.messages.slice().sort(function (a, b) {
475
+ return (a.message_seq || 0) - (b.message_seq || 0)
476
+ }).map(function (m) {
477
+ return types.mapHistoryToChatMessage(m, myUid)
478
+ })
479
+
480
+ // Dedup and prepend
481
+ var existingSeqs = {}
482
+ var existingIds = {}
483
+ self._state.messages.forEach(function (m) {
484
+ if (typeof m.messageSeq === 'number') existingSeqs[m.messageSeq] = true
485
+ existingIds[m.id] = true
486
+ })
487
+
488
+ var mergedHead = list.filter(function (m) {
489
+ if (m.messageSeq != null) return !existingSeqs[m.messageSeq]
490
+ return !existingIds[m.id]
491
+ })
492
+
493
+ var earliest = self._state.earliestSeq
494
+ mergedHead.forEach(function (m) {
495
+ if (m.messageSeq != null) {
496
+ if (earliest === null || m.messageSeq < earliest) earliest = m.messageSeq
497
+ }
498
+ })
499
+
500
+ self._setState({
501
+ messages: mergedHead.concat(self._state.messages),
502
+ earliestSeq: earliest,
503
+ historyHasMore: res.more === 1
504
+ })
505
+ }).catch(function (e) {
506
+ self._setState({ historyError: e && e.message ? e.message : String(e) })
507
+ }).then(function () {
508
+ self._setState({ historyLoading: false })
509
+ })
510
+ }
511
+
512
+ ChatStore.prototype.loadMoreHistory = function (limit) {
513
+ limit = limit || 20
514
+ var self = this
515
+ var st = this._state
516
+ if (!st.channelId || !st.channelType) return Promise.resolve()
517
+ if (st.historyLoading) return Promise.resolve()
518
+
519
+ var start = st.earliestSeq || 0
520
+ this._setState({ historyLoading: true, historyError: null })
521
+
522
+ return historyService.syncVisitorMessages({
523
+ apiBase: st.apiBase,
524
+ platformApiKey: st.platformApiKey,
525
+ channelId: st.channelId,
526
+ channelType: st.channelType,
527
+ startSeq: start,
528
+ endSeq: 0,
529
+ limit: limit,
530
+ pullMode: 0
531
+ }).then(function (res) {
532
+ var myUid = self._state.myUid
533
+ var listAsc = res.messages.slice().sort(function (a, b) {
534
+ return (a.message_seq || 0) - (b.message_seq || 0)
535
+ }).map(function (m) {
536
+ return types.mapHistoryToChatMessage(m, myUid)
537
+ })
538
+
539
+ var existingSeqs = {}
540
+ var existingIds = {}
541
+ self._state.messages.forEach(function (m) {
542
+ if (typeof m.messageSeq === 'number') existingSeqs[m.messageSeq] = true
543
+ existingIds[m.id] = true
544
+ })
545
+
546
+ var prepend = listAsc.filter(function (m) {
547
+ if (m.messageSeq != null) return !existingSeqs[m.messageSeq]
548
+ return !existingIds[m.id]
549
+ })
550
+
551
+ var earliest = self._state.earliestSeq
552
+ prepend.forEach(function (m) {
553
+ if (m.messageSeq != null) {
554
+ if (earliest === null || m.messageSeq < earliest) earliest = m.messageSeq
555
+ }
556
+ })
557
+
558
+ self._setState({
559
+ messages: prepend.concat(self._state.messages),
560
+ earliestSeq: earliest,
561
+ historyHasMore: res.more === 1
562
+ })
563
+ }).catch(function (e) {
564
+ self._setState({ historyError: e && e.message ? e.message : String(e) })
565
+ }).then(function () {
566
+ self._setState({ historyLoading: false })
567
+ })
568
+ }
569
+
570
+ // ========== Streaming ==========
571
+
572
+ ChatStore.prototype.markStreamingStart = function (clientMsgNo) {
573
+ if (!clientMsgNo) return
574
+ if (streamTimer) { try { clearTimeout(streamTimer) } catch (e) {} ; streamTimer = null }
575
+ var self = this
576
+ this._setState({ isStreaming: true, streamCanceling: false, streamingClientMsgNo: clientMsgNo })
577
+ streamTimer = setTimeout(function () {
578
+ var s = self._state
579
+ if (s.isStreaming && s.streamingClientMsgNo === clientMsgNo) {
580
+ self._setState({ isStreaming: false, streamingClientMsgNo: '', streamCanceling: false })
581
+ }
582
+ }, STREAM_TIMEOUT_MS)
583
+ }
584
+
585
+ ChatStore.prototype.markStreamingEnd = function () {
586
+ if (streamTimer) { try { clearTimeout(streamTimer) } catch (e) {} ; streamTimer = null }
587
+ this._setState({ isStreaming: false, streamCanceling: false, streamingClientMsgNo: '' })
588
+ }
589
+
590
+ ChatStore.prototype.cancelStreaming = function (reason) {
591
+ var self = this
592
+ var st = this._state
593
+ if (st.streamCanceling) return Promise.resolve()
594
+
595
+ this._setState({ streamCanceling: true })
596
+
597
+ if (!st.apiBase || !st.streamingClientMsgNo || !st.platformApiKey) {
598
+ this.markStreamingEnd()
599
+ return Promise.resolve()
600
+ }
601
+
602
+ return chatService.cancelStreaming({
603
+ apiBase: st.apiBase,
604
+ platformApiKey: st.platformApiKey,
605
+ clientMsgNo: st.streamingClientMsgNo,
606
+ reason: reason || 'user_cancel'
607
+ }).catch(function (e) {
608
+ console.warn('[Chat] Cancel streaming error:', e)
609
+ }).then(function () {
610
+ self.markStreamingEnd()
611
+ })
612
+ }
613
+
614
+ ChatStore.prototype.appendStreamData = function (clientMsgNo, data) {
615
+ if (!clientMsgNo || !data) return
616
+
617
+ var messages = this._state.messages
618
+ var found = false
619
+ var updated = messages.map(function (m) {
620
+ if (m.clientMsgNo && m.clientMsgNo === clientMsgNo) {
621
+ found = true
622
+ return Object.assign({}, m, { streamData: (m.streamData || '') + data })
623
+ }
624
+ return m
625
+ })
626
+
627
+ if (!found) {
628
+ var placeholder = {
629
+ id: 'stream-' + clientMsgNo,
630
+ role: 'agent',
631
+ payload: { type: 1, content: '' },
632
+ time: new Date(),
633
+ clientMsgNo: clientMsgNo,
634
+ streamData: data
635
+ }
636
+ updated = messages.concat([placeholder])
637
+ }
638
+
639
+ this._setStateThrottled({ messages: updated })
640
+
641
+ if (!this._state.isStreaming) {
642
+ this.markStreamingStart(clientMsgNo)
643
+ }
644
+ }
645
+
646
+ ChatStore.prototype.appendMixedPart = function (clientMsgNo, part) {
647
+ if (!clientMsgNo) return
648
+
649
+ var messages = this._state.messages
650
+ var found = false
651
+ var updated = messages.map(function (m) {
652
+ if (m.clientMsgNo && m.clientMsgNo === clientMsgNo) {
653
+ found = true
654
+ var parts = (m.uiParts || []).slice()
655
+ // Merge consecutive text parts
656
+ if (part.type === 'text' && parts.length > 0) {
657
+ var last = parts[parts.length - 1]
658
+ if (last.type === 'text') {
659
+ parts[parts.length - 1] = { type: 'text', text: (last.text || '') + (part.text || '') }
660
+ var textContent = ''
661
+ for (var i = 0; i < parts.length; i++) {
662
+ if (parts[i].type === 'text') textContent += parts[i].text || ''
663
+ }
664
+ return Object.assign({}, m, { uiParts: parts, streamData: textContent })
665
+ }
666
+ }
667
+ parts.push(part)
668
+ var tc = ''
669
+ for (var j = 0; j < parts.length; j++) {
670
+ if (parts[j].type === 'text') tc += parts[j].text || ''
671
+ }
672
+ return Object.assign({}, m, { uiParts: parts, streamData: tc })
673
+ }
674
+ return m
675
+ })
676
+
677
+ if (!found) {
678
+ var initParts = [part]
679
+ var placeholder = {
680
+ id: 'stream-' + clientMsgNo,
681
+ role: 'agent',
682
+ payload: { type: 1, content: '' },
683
+ time: new Date(),
684
+ clientMsgNo: clientMsgNo,
685
+ uiParts: initParts,
686
+ streamData: part.type === 'text' ? (part.text || '') : ''
687
+ }
688
+ updated = messages.concat([placeholder])
689
+ }
690
+
691
+ this._setStateThrottled({ messages: updated })
692
+
693
+ if (!this._state.isStreaming) {
694
+ this.markStreamingStart(clientMsgNo)
695
+ }
696
+ }
697
+
698
+ ChatStore.prototype.finalizeStreamMessage = function (clientMsgNo, errorMessage) {
699
+ if (!clientMsgNo) return
700
+
701
+ var messages = this._state.messages.map(function (m) {
702
+ if (m.clientMsgNo && m.clientMsgNo === clientMsgNo) {
703
+ var finalPayload = m.streamData
704
+ ? { type: 1, content: m.streamData }
705
+ : m.payload
706
+ var result = Object.assign({}, m, {
707
+ payload: finalPayload,
708
+ streamData: undefined,
709
+ errorMessage: errorMessage || undefined
710
+ })
711
+ // Preserve uiParts for json-render
712
+ if (m.uiParts) result.uiParts = m.uiParts
713
+ return result
714
+ }
715
+ return m
716
+ })
717
+
718
+ this._setState({ messages: messages })
719
+
720
+ if (this._state.streamingClientMsgNo === clientMsgNo) {
721
+ this.markStreamingEnd()
722
+ }
723
+ }
724
+
725
+ ChatStore.prototype.ensureWelcomeMessage = function (text) {
726
+ var t = (text || '').trim()
727
+ if (!t) return
728
+
729
+ var messages = this._state.messages
730
+ var idx = -1
731
+ for (var i = 0; i < messages.length; i++) {
732
+ if (messages[i].id === 'welcome') { idx = i; break }
733
+ }
734
+
735
+ if (idx >= 0) {
736
+ var next = messages.slice()
737
+ next[idx] = Object.assign({}, messages[idx], { payload: { type: 1, content: t } })
738
+ this._setState({ messages: next })
739
+ } else {
740
+ var welcome = {
741
+ id: 'welcome',
742
+ role: 'agent',
743
+ payload: { type: 1, content: t },
744
+ time: new Date()
745
+ }
746
+ this._setState({ messages: [welcome].concat(messages) })
747
+ }
748
+ }
749
+
750
+ ChatStore.prototype.disconnect = function () {
751
+ if (offMsg) { try { offMsg() } catch (e) {} ; offMsg = null }
752
+ if (offStatus) { try { offStatus() } catch (e) {} ; offStatus = null }
753
+ if (offCustom) { try { offCustom() } catch (e) {} ; offCustom = null }
754
+ this.markStreamingEnd()
755
+ IMService.disconnect()
756
+ }
757
+
758
+ module.exports = new ChatStore()