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.
- package/README.md +81 -0
- package/miniprogram_dist/adapters/request.js +38 -0
- package/miniprogram_dist/adapters/storage.js +42 -0
- package/miniprogram_dist/adapters/systemInfo.js +22 -0
- package/miniprogram_dist/chat/index.js +164 -0
- package/miniprogram_dist/chat/index.json +7 -0
- package/miniprogram_dist/chat/index.wxml +48 -0
- package/miniprogram_dist/chat/index.wxss +92 -0
- package/miniprogram_dist/components/json-render-element/index.js +374 -0
- package/miniprogram_dist/components/json-render-element/index.json +6 -0
- package/miniprogram_dist/components/json-render-element/index.wxml +218 -0
- package/miniprogram_dist/components/json-render-element/index.wxss +450 -0
- package/miniprogram_dist/components/json-render-message/index.js +89 -0
- package/miniprogram_dist/components/json-render-message/index.json +7 -0
- package/miniprogram_dist/components/json-render-message/index.wxml +25 -0
- package/miniprogram_dist/components/json-render-message/index.wxss +26 -0
- package/miniprogram_dist/components/json-render-surface/index.js +116 -0
- package/miniprogram_dist/components/json-render-surface/index.json +6 -0
- package/miniprogram_dist/components/json-render-surface/index.wxml +10 -0
- package/miniprogram_dist/components/json-render-surface/index.wxss +6 -0
- package/miniprogram_dist/components/markdown-text/index.js +23 -0
- package/miniprogram_dist/components/markdown-text/index.json +3 -0
- package/miniprogram_dist/components/markdown-text/index.wxml +1 -0
- package/miniprogram_dist/components/markdown-text/index.wxss +6 -0
- package/miniprogram_dist/components/message-bubble/index.js +12 -0
- package/miniprogram_dist/components/message-bubble/index.json +3 -0
- package/miniprogram_dist/components/message-bubble/index.wxml +3 -0
- package/miniprogram_dist/components/message-bubble/index.wxss +17 -0
- package/miniprogram_dist/components/message-input/index.js +76 -0
- package/miniprogram_dist/components/message-input/index.json +3 -0
- package/miniprogram_dist/components/message-input/index.wxml +28 -0
- package/miniprogram_dist/components/message-input/index.wxss +56 -0
- package/miniprogram_dist/components/message-list/index.js +113 -0
- package/miniprogram_dist/components/message-list/index.json +9 -0
- package/miniprogram_dist/components/message-list/index.wxml +108 -0
- package/miniprogram_dist/components/message-list/index.wxss +113 -0
- package/miniprogram_dist/components/system-message/index.js +8 -0
- package/miniprogram_dist/components/system-message/index.json +3 -0
- package/miniprogram_dist/components/system-message/index.wxml +3 -0
- package/miniprogram_dist/components/system-message/index.wxss +15 -0
- package/miniprogram_dist/core/chatStore.js +758 -0
- package/miniprogram_dist/core/i18n.js +66 -0
- package/miniprogram_dist/core/platformStore.js +86 -0
- package/miniprogram_dist/core/types.js +192 -0
- package/miniprogram_dist/services/chat.js +67 -0
- package/miniprogram_dist/services/messageHistory.js +46 -0
- package/miniprogram_dist/services/platform.js +27 -0
- package/miniprogram_dist/services/upload.js +74 -0
- package/miniprogram_dist/services/visitor.js +67 -0
- package/miniprogram_dist/services/wukongim.js +183 -0
- package/miniprogram_dist/utils/jsonRender.js +158 -0
- package/miniprogram_dist/utils/markdown.js +31 -0
- package/miniprogram_dist/utils/time.js +85 -0
- package/miniprogram_dist/utils/uid.js +11 -0
- 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()
|