@csntgao/ai-employee-client 0.14.3
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/dist/index.d.ts +680 -0
- package/dist/index.js +2207 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2207 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
function createAuthenticateFrame(accessToken) {
|
|
3
|
+
return {
|
|
4
|
+
type: "authenticate",
|
|
5
|
+
protocol_version: 1,
|
|
6
|
+
access_token: accessToken,
|
|
7
|
+
requested_channels: ["ai_employee"],
|
|
8
|
+
cursors: {}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function createSubscriptionFrame(sessionId, action = "subscribe", replayHistory = false) {
|
|
12
|
+
const frame = {
|
|
13
|
+
action,
|
|
14
|
+
channel: "ai_employee",
|
|
15
|
+
resource_type: "session",
|
|
16
|
+
resource_id: sessionId
|
|
17
|
+
};
|
|
18
|
+
if (action === "subscribe") frame.replay_history = replayHistory;
|
|
19
|
+
return frame;
|
|
20
|
+
}
|
|
21
|
+
function createPingFrame(now = /* @__PURE__ */ new Date()) {
|
|
22
|
+
return { action: "ping", timestamp: now.toISOString() };
|
|
23
|
+
}
|
|
24
|
+
function parseJsonRecord(raw) {
|
|
25
|
+
try {
|
|
26
|
+
return asRecord(JSON.parse(raw));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function parseAiEmployeeEvent(raw) {
|
|
32
|
+
const event = asRecord(raw);
|
|
33
|
+
const type = stringValue(event.type);
|
|
34
|
+
const sessionId = stringValue(event.session_id).trim();
|
|
35
|
+
if (type === "session_title_updated") {
|
|
36
|
+
const title = stringValue(asRecord(event.payload).title).trim();
|
|
37
|
+
return sessionId && title ? { kind: "session-title", sessionId, title } : { kind: "ignored" };
|
|
38
|
+
}
|
|
39
|
+
if (type !== "session_message" || !sessionId) return { kind: "ignored" };
|
|
40
|
+
return {
|
|
41
|
+
kind: "session-message",
|
|
42
|
+
sessionId,
|
|
43
|
+
result: parseSessionPayload(asRecord(event.payload), sessionId)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function parseSessionPayload(payload, sessionId) {
|
|
47
|
+
const messages = [];
|
|
48
|
+
const patches = Array.isArray(payload.JsonPatch) ? payload.JsonPatch : [];
|
|
49
|
+
for (const rawPatch of patches) {
|
|
50
|
+
const patch = asRecord(rawPatch);
|
|
51
|
+
const value = asRecord(patch.value);
|
|
52
|
+
const parsed = messageFromEntry(value, sessionId);
|
|
53
|
+
if (parsed) messages.push(parsed);
|
|
54
|
+
}
|
|
55
|
+
const result = stringValue(payload.result).trim();
|
|
56
|
+
const fallbackResult = result ? createMessage(
|
|
57
|
+
sessionId,
|
|
58
|
+
"assistant",
|
|
59
|
+
result,
|
|
60
|
+
`result:${sessionId}:${hashText(result)}`,
|
|
61
|
+
payload
|
|
62
|
+
) : void 0;
|
|
63
|
+
if (payload.error) {
|
|
64
|
+
messages.push(
|
|
65
|
+
createMessage(
|
|
66
|
+
sessionId,
|
|
67
|
+
"error",
|
|
68
|
+
"AI \u56DE\u590D\u51FA\u73B0\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
69
|
+
`error:${sessionId}:${hashText(stableString(payload))}`,
|
|
70
|
+
payload
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
let processing;
|
|
75
|
+
if (payload.Ready === true) processing = true;
|
|
76
|
+
if (payload.finished === true || payload.stopped === true) processing = false;
|
|
77
|
+
return { messages, fallbackResult, processing };
|
|
78
|
+
}
|
|
79
|
+
function messageFromEntry(entry, sessionId) {
|
|
80
|
+
const entryTypeValue = entry.entry_type;
|
|
81
|
+
const entryType = typeof entryTypeValue === "string" ? entryTypeValue : stringValue(asRecord(entryTypeValue).type);
|
|
82
|
+
let role;
|
|
83
|
+
switch (entryType) {
|
|
84
|
+
case "assistant_message":
|
|
85
|
+
case "result":
|
|
86
|
+
role = "assistant";
|
|
87
|
+
break;
|
|
88
|
+
case "user_message":
|
|
89
|
+
role = "user";
|
|
90
|
+
break;
|
|
91
|
+
case "system_message":
|
|
92
|
+
case "system_push":
|
|
93
|
+
role = "system";
|
|
94
|
+
break;
|
|
95
|
+
case "error":
|
|
96
|
+
case "error_message":
|
|
97
|
+
role = "error";
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const content = role === "error" ? "AI \u56DE\u590D\u51FA\u73B0\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : extractText(entry.content);
|
|
103
|
+
if (!content) return null;
|
|
104
|
+
const messageId = stringValue(entry.message_id);
|
|
105
|
+
const webSocketMessageId = stringValue(entry.ws_message_id);
|
|
106
|
+
const explicitId = role === "user" ? messageId || webSocketMessageId || stringValue(entry.id) : webSocketMessageId || messageId || stringValue(entry.id);
|
|
107
|
+
const fingerprint = stableString(entry);
|
|
108
|
+
const id = explicitId || `realtime:${sessionId}:${hashText(fingerprint)}`;
|
|
109
|
+
return createMessage(sessionId, role, content, id, entry);
|
|
110
|
+
}
|
|
111
|
+
function createMessage(sessionId, role, content, id, source) {
|
|
112
|
+
const createdAt = stringValue(source.timestamp) || stringValue(source.create_time) || stringValue(source.created_at) || (/* @__PURE__ */ new Date()).toISOString();
|
|
113
|
+
return { id, sessionId, role, content, createdAt };
|
|
114
|
+
}
|
|
115
|
+
function extractText(raw) {
|
|
116
|
+
if (typeof raw !== "string") return "";
|
|
117
|
+
const value = raw.trim();
|
|
118
|
+
if (!value) return "";
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(value);
|
|
121
|
+
if (typeof parsed === "string") return parsed.trim();
|
|
122
|
+
const record3 = asRecord(parsed);
|
|
123
|
+
for (const key of ["text", "result", "message", "content"]) {
|
|
124
|
+
const candidate = stringValue(record3[key]).trim();
|
|
125
|
+
if (candidate) return candidate;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
const messageMatch = value.match(/<message>([\s\S]*?)<\/message>/i);
|
|
130
|
+
return (messageMatch?.[1] ?? value).trim();
|
|
131
|
+
}
|
|
132
|
+
function asRecord(value) {
|
|
133
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
134
|
+
}
|
|
135
|
+
function stringValue(value) {
|
|
136
|
+
return typeof value === "string" ? value : "";
|
|
137
|
+
}
|
|
138
|
+
function stableString(value) {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.stringify(value) ?? "";
|
|
141
|
+
} catch {
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function hashText(value) {
|
|
146
|
+
let hash = 2166136261;
|
|
147
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
148
|
+
hash ^= value.charCodeAt(index);
|
|
149
|
+
hash = Math.imul(hash, 16777619);
|
|
150
|
+
}
|
|
151
|
+
return (hash >>> 0).toString(36);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/client.ts
|
|
155
|
+
var DEFAULT_RECONNECT = {
|
|
156
|
+
initialDelayMs: 1e3,
|
|
157
|
+
maximumDelayMs: 3e4,
|
|
158
|
+
factor: 2,
|
|
159
|
+
jitter: 0.2
|
|
160
|
+
};
|
|
161
|
+
var MAX_SEEN_MESSAGE_IDS = 2e3;
|
|
162
|
+
var LOCAL_MESSAGE_MATCH_WINDOW_MS = 3e4;
|
|
163
|
+
var MAX_AVAILABILITY_RECONNECTS = 3;
|
|
164
|
+
var FIRST_MESSAGE_STORAGE_PREFIX = "uniplat.ai-employee.first-message";
|
|
165
|
+
var AiEmployeeClientError = class extends Error {
|
|
166
|
+
code;
|
|
167
|
+
constructor(code, message) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "AiEmployeeClientError";
|
|
170
|
+
this.code = code;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var AiEmployeeTransportError = class extends Error {
|
|
174
|
+
code;
|
|
175
|
+
constructor(code) {
|
|
176
|
+
super("AI Employee transport request failed.");
|
|
177
|
+
this.name = "AiEmployeeTransportError";
|
|
178
|
+
this.code = code;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var AiEmployeeClient = class {
|
|
182
|
+
aeCode;
|
|
183
|
+
agentCode;
|
|
184
|
+
realtimeUrl;
|
|
185
|
+
transport;
|
|
186
|
+
webSocketFactory;
|
|
187
|
+
reconnectOptions;
|
|
188
|
+
minimumHeartbeatMs;
|
|
189
|
+
requestIdFactory;
|
|
190
|
+
firstMessageRequestStore;
|
|
191
|
+
firstMessageRequestScope;
|
|
192
|
+
listeners = /* @__PURE__ */ new Map();
|
|
193
|
+
seenMessageIds = /* @__PURE__ */ new Set();
|
|
194
|
+
pendingLocalMessages = /* @__PURE__ */ new Map();
|
|
195
|
+
assistantReceivedInCurrentTurn = false;
|
|
196
|
+
socket = null;
|
|
197
|
+
reconnectTimer = null;
|
|
198
|
+
heartbeatTimer = null;
|
|
199
|
+
initializePromise = null;
|
|
200
|
+
currentMessages = [];
|
|
201
|
+
currentSessionId;
|
|
202
|
+
currentState = "idle";
|
|
203
|
+
reconnectAttempt = 0;
|
|
204
|
+
socketGeneration = 0;
|
|
205
|
+
lifecycleGeneration = 0;
|
|
206
|
+
shouldReconnect = false;
|
|
207
|
+
destroyed = false;
|
|
208
|
+
sending = false;
|
|
209
|
+
processing = false;
|
|
210
|
+
localMessageSequence = 0;
|
|
211
|
+
replayHistoryOnNextSubscription = false;
|
|
212
|
+
socketAuthenticationFailure = null;
|
|
213
|
+
availabilityReconnectAttempt = 0;
|
|
214
|
+
pendingFirstMessage = null;
|
|
215
|
+
constructor(options) {
|
|
216
|
+
this.realtimeUrl = validateRealtimeUrl(options.realtimeUrl);
|
|
217
|
+
this.aeCode = requiredText(options.aeCode, "aeCode");
|
|
218
|
+
this.agentCode = requiredText(options.agentCode, "agentCode");
|
|
219
|
+
if (!options.transport) {
|
|
220
|
+
throw new AiEmployeeClientError("INVALID_CONFIGURATION", "\u7F3A\u5C11 transport \u914D\u7F6E\u3002");
|
|
221
|
+
}
|
|
222
|
+
if (typeof options.transport.createSessionWithFirstMessage !== "function") {
|
|
223
|
+
throw new AiEmployeeClientError(
|
|
224
|
+
"INVALID_CONFIGURATION",
|
|
225
|
+
"transport \u7F3A\u5C11\u9996\u6761\u6D88\u606F\u539F\u5B50\u521B\u5EFA\u80FD\u529B\u3002"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
this.transport = options.transport;
|
|
229
|
+
this.currentSessionId = normalizeSessionId(options.sessionId);
|
|
230
|
+
this.reconnectOptions = normalizeReconnectOptions(options.reconnect);
|
|
231
|
+
this.minimumHeartbeatMs = Math.max(1, options.minimumHeartbeatMs ?? 5e3);
|
|
232
|
+
this.requestIdFactory = options.requestIdFactory ?? createRequestId;
|
|
233
|
+
this.firstMessageRequestStore = options.firstMessageRequestStore === void 0 ? createSessionFirstMessageRequestStore() : options.firstMessageRequestStore;
|
|
234
|
+
this.firstMessageRequestScope = `${this.aeCode}:${this.agentCode}`;
|
|
235
|
+
this.webSocketFactory = options.webSocketFactory ?? ((url) => {
|
|
236
|
+
if (typeof WebSocket === "undefined") {
|
|
237
|
+
throw new AiEmployeeClientError("WEBSOCKET_UNAVAILABLE", "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301 WebSocket\u3002");
|
|
238
|
+
}
|
|
239
|
+
return new WebSocket(url);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
get connectionState() {
|
|
243
|
+
return this.currentState;
|
|
244
|
+
}
|
|
245
|
+
get sessionId() {
|
|
246
|
+
return this.currentSessionId;
|
|
247
|
+
}
|
|
248
|
+
get messages() {
|
|
249
|
+
return [...this.currentMessages];
|
|
250
|
+
}
|
|
251
|
+
get isProcessing() {
|
|
252
|
+
return this.processing;
|
|
253
|
+
}
|
|
254
|
+
get isDestroyed() {
|
|
255
|
+
return this.destroyed;
|
|
256
|
+
}
|
|
257
|
+
on(type, listener) {
|
|
258
|
+
this.assertUsable();
|
|
259
|
+
const listeners = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
|
|
260
|
+
const untypedListener = listener;
|
|
261
|
+
listeners.add(untypedListener);
|
|
262
|
+
this.listeners.set(type, listeners);
|
|
263
|
+
return () => listeners.delete(untypedListener);
|
|
264
|
+
}
|
|
265
|
+
connect() {
|
|
266
|
+
return this.start();
|
|
267
|
+
}
|
|
268
|
+
start() {
|
|
269
|
+
this.assertUsable();
|
|
270
|
+
if (this.initializePromise) return this.initializePromise;
|
|
271
|
+
const lifecycle = this.lifecycleGeneration;
|
|
272
|
+
const pending = this.initialize(lifecycle);
|
|
273
|
+
this.initializePromise = pending;
|
|
274
|
+
const clearPending = () => {
|
|
275
|
+
if (this.initializePromise === pending) this.initializePromise = null;
|
|
276
|
+
};
|
|
277
|
+
void pending.then(clearPending, clearPending);
|
|
278
|
+
return pending;
|
|
279
|
+
}
|
|
280
|
+
async bindSession(sessionId) {
|
|
281
|
+
this.assertUsable();
|
|
282
|
+
const target = requiredText(sessionId, "sessionId");
|
|
283
|
+
const lifecycle = ++this.lifecycleGeneration;
|
|
284
|
+
this.stopConnection(false);
|
|
285
|
+
this.currentSessionId = target;
|
|
286
|
+
this.currentMessages = [];
|
|
287
|
+
this.seenMessageIds.clear();
|
|
288
|
+
this.pendingLocalMessages.clear();
|
|
289
|
+
this.assistantReceivedInCurrentTurn = false;
|
|
290
|
+
this.pendingFirstMessage = null;
|
|
291
|
+
await this.clearStoredFirstMessageRequest();
|
|
292
|
+
this.replayHistoryOnNextSubscription = false;
|
|
293
|
+
this.shouldReconnect = true;
|
|
294
|
+
await this.refreshHistory(lifecycle);
|
|
295
|
+
this.assertUsable();
|
|
296
|
+
if (!this.isCurrentLifecycle(lifecycle)) {
|
|
297
|
+
throw new AiEmployeeClientError("SESSION_UNAVAILABLE", "\u4F1A\u8BDD\u5207\u6362\u5DF2\u88AB\u65B0\u7684\u64CD\u4F5C\u66FF\u4EE3\u3002");
|
|
298
|
+
}
|
|
299
|
+
if (this.isCurrentLifecycle(lifecycle)) this.openSocket(false);
|
|
300
|
+
return { sessionId: target, messages: this.messages };
|
|
301
|
+
}
|
|
302
|
+
async startNewConversation() {
|
|
303
|
+
this.assertUsable();
|
|
304
|
+
this.initializePromise = null;
|
|
305
|
+
this.lifecycleGeneration += 1;
|
|
306
|
+
this.stopConnection(false);
|
|
307
|
+
this.currentSessionId = null;
|
|
308
|
+
this.currentMessages = [];
|
|
309
|
+
this.seenMessageIds.clear();
|
|
310
|
+
this.pendingLocalMessages.clear();
|
|
311
|
+
this.assistantReceivedInCurrentTurn = false;
|
|
312
|
+
this.pendingFirstMessage = null;
|
|
313
|
+
await this.clearStoredFirstMessageRequest();
|
|
314
|
+
this.replayHistoryOnNextSubscription = false;
|
|
315
|
+
this.shouldReconnect = true;
|
|
316
|
+
this.openSocket(false);
|
|
317
|
+
return { sessionId: null, messages: [] };
|
|
318
|
+
}
|
|
319
|
+
async refreshMessages() {
|
|
320
|
+
this.assertUsable();
|
|
321
|
+
if (!this.currentSessionId) {
|
|
322
|
+
throw new AiEmployeeClientError("SESSION_UNAVAILABLE", "\u5C1A\u672A\u7ED1\u5B9A\u6570\u5B57\u5458\u5DE5\u4F1A\u8BDD\u3002");
|
|
323
|
+
}
|
|
324
|
+
await this.refreshHistory(this.lifecycleGeneration);
|
|
325
|
+
return this.messages;
|
|
326
|
+
}
|
|
327
|
+
async sendMessage(content, options = {}) {
|
|
328
|
+
this.assertUsable();
|
|
329
|
+
const normalizedContent = content.trim();
|
|
330
|
+
if (!normalizedContent) {
|
|
331
|
+
throw new AiEmployeeClientError("EMPTY_MESSAGE", "\u6D88\u606F\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
332
|
+
}
|
|
333
|
+
if (this.sending) {
|
|
334
|
+
throw new AiEmployeeClientError("SEND_IN_PROGRESS", "\u4E0A\u4E00\u6761\u6D88\u606F\u4ECD\u5728\u53D1\u9001\u4E2D\u3002");
|
|
335
|
+
}
|
|
336
|
+
this.sending = true;
|
|
337
|
+
try {
|
|
338
|
+
const sessionId = this.currentSessionId;
|
|
339
|
+
return sessionId ? await this.sendFollowUp(sessionId, normalizedContent) : await this.sendFirstMessage(normalizedContent, options);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof AiEmployeeClientError) throw error;
|
|
342
|
+
if (transportErrorCode(error) === "IDEMPOTENCY_CONFLICT") {
|
|
343
|
+
const detail2 = this.emitError(
|
|
344
|
+
"FIRST_MESSAGE_CONFLICT",
|
|
345
|
+
"\u9996\u6761\u6D88\u606F\u5185\u5BB9\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5F00\u542F\u65B0\u5BF9\u8BDD\u540E\u91CD\u8BD5\u3002",
|
|
346
|
+
false
|
|
347
|
+
);
|
|
348
|
+
throw new AiEmployeeClientError(detail2.code, detail2.message);
|
|
349
|
+
}
|
|
350
|
+
if (transportErrorCode(error) === "TOO_MANY_REQUESTS") {
|
|
351
|
+
const detail2 = this.emitError("RATE_LIMITED", "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002", false);
|
|
352
|
+
throw new AiEmployeeClientError(detail2.code, detail2.message);
|
|
353
|
+
}
|
|
354
|
+
const detail = this.emitError("SEND_FAILED", "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
355
|
+
throw new AiEmployeeClientError(detail.code, detail.message);
|
|
356
|
+
} finally {
|
|
357
|
+
this.sending = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
disconnect() {
|
|
361
|
+
if (this.destroyed) return;
|
|
362
|
+
this.lifecycleGeneration += 1;
|
|
363
|
+
this.stopConnection(true);
|
|
364
|
+
}
|
|
365
|
+
destroy() {
|
|
366
|
+
if (this.destroyed) return;
|
|
367
|
+
this.lifecycleGeneration += 1;
|
|
368
|
+
this.stopConnection(false);
|
|
369
|
+
this.destroyed = true;
|
|
370
|
+
this.initializePromise = null;
|
|
371
|
+
this.setState("destroyed");
|
|
372
|
+
this.listeners.clear();
|
|
373
|
+
this.currentMessages = [];
|
|
374
|
+
this.seenMessageIds.clear();
|
|
375
|
+
this.pendingLocalMessages.clear();
|
|
376
|
+
this.assistantReceivedInCurrentTurn = false;
|
|
377
|
+
this.pendingFirstMessage = null;
|
|
378
|
+
}
|
|
379
|
+
async sendFollowUp(sessionId, content) {
|
|
380
|
+
this.assistantReceivedInCurrentTurn = false;
|
|
381
|
+
await this.transport.sendMessage(sessionId, content);
|
|
382
|
+
this.assertUsable();
|
|
383
|
+
const now = /* @__PURE__ */ new Date();
|
|
384
|
+
const message = {
|
|
385
|
+
id: `local:${now.getTime()}:${++this.localMessageSequence}`,
|
|
386
|
+
sessionId,
|
|
387
|
+
role: "user",
|
|
388
|
+
content,
|
|
389
|
+
createdAt: now.toISOString()
|
|
390
|
+
};
|
|
391
|
+
this.rememberPendingLocalMessage(content);
|
|
392
|
+
this.appendMessage(message, "local");
|
|
393
|
+
this.setProcessing(true);
|
|
394
|
+
return message;
|
|
395
|
+
}
|
|
396
|
+
async sendFirstMessage(content, options) {
|
|
397
|
+
this.assistantReceivedInCurrentTurn = false;
|
|
398
|
+
const createSessionWithFirstMessage = this.transport.createSessionWithFirstMessage;
|
|
399
|
+
const lifecycle = this.lifecycleGeneration;
|
|
400
|
+
const normalizedOptions = normalizeFirstMessageOptions(options);
|
|
401
|
+
const fingerprint = firstMessageFingerprint(content, normalizedOptions);
|
|
402
|
+
if (!this.pendingFirstMessage || this.pendingFirstMessage.fingerprint !== fingerprint) {
|
|
403
|
+
const stored = await this.loadStoredFirstMessageRequest();
|
|
404
|
+
this.pendingFirstMessage = stored?.fingerprint === fingerprint ? stored : { fingerprint, requestId: requiredRequestId(this.requestIdFactory()) };
|
|
405
|
+
await this.saveStoredFirstMessageRequest(this.pendingFirstMessage);
|
|
406
|
+
}
|
|
407
|
+
this.assertUsable();
|
|
408
|
+
if (!this.isCurrentLifecycle(lifecycle) || this.currentSessionId) {
|
|
409
|
+
this.pendingFirstMessage = null;
|
|
410
|
+
await this.clearStoredFirstMessageRequest();
|
|
411
|
+
throw new AiEmployeeClientError("SESSION_UNAVAILABLE", "\u9996\u6761\u6D88\u606F\u53D7\u7406\u5DF2\u88AB\u65B0\u7684\u64CD\u4F5C\u66FF\u4EE3\u3002");
|
|
412
|
+
}
|
|
413
|
+
const request = {
|
|
414
|
+
requestId: this.pendingFirstMessage.requestId,
|
|
415
|
+
aeCode: this.aeCode,
|
|
416
|
+
agentCode: this.agentCode,
|
|
417
|
+
content,
|
|
418
|
+
attachments: normalizedOptions.attachments,
|
|
419
|
+
trustedContext: normalizedOptions.trustedContext
|
|
420
|
+
};
|
|
421
|
+
const rawResult = await createSessionWithFirstMessage.call(this.transport, request);
|
|
422
|
+
const result = normalizeFirstMessageResult(rawResult, this.aeCode, this.agentCode);
|
|
423
|
+
this.assertUsable();
|
|
424
|
+
if (!this.isCurrentLifecycle(lifecycle) || this.currentSessionId) {
|
|
425
|
+
this.pendingFirstMessage = null;
|
|
426
|
+
await this.clearStoredFirstMessageRequest();
|
|
427
|
+
throw new AiEmployeeClientError("SESSION_UNAVAILABLE", "\u9996\u6761\u6D88\u606F\u53D7\u7406\u5DF2\u88AB\u65B0\u7684\u64CD\u4F5C\u66FF\u4EE3\u3002");
|
|
428
|
+
}
|
|
429
|
+
this.currentSessionId = result.sessionId;
|
|
430
|
+
this.pendingFirstMessage = null;
|
|
431
|
+
await this.clearStoredFirstMessageRequest();
|
|
432
|
+
const message = {
|
|
433
|
+
id: result.messageId,
|
|
434
|
+
sessionId: result.sessionId,
|
|
435
|
+
role: "user",
|
|
436
|
+
content,
|
|
437
|
+
createdAt: result.createdAt ?? result.startedAt
|
|
438
|
+
};
|
|
439
|
+
this.rememberPendingLocalMessage(content);
|
|
440
|
+
this.appendMessage(message, "local");
|
|
441
|
+
this.setProcessing(true);
|
|
442
|
+
this.replayHistoryOnNextSubscription = true;
|
|
443
|
+
this.subscribeCurrentSession(true);
|
|
444
|
+
return message;
|
|
445
|
+
}
|
|
446
|
+
async initialize(lifecycle) {
|
|
447
|
+
if (!this.currentSessionId) {
|
|
448
|
+
this.shouldReconnect = true;
|
|
449
|
+
this.openSocket(false);
|
|
450
|
+
return { sessionId: null, messages: [] };
|
|
451
|
+
}
|
|
452
|
+
await this.refreshHistory(lifecycle);
|
|
453
|
+
this.assertUsable();
|
|
454
|
+
if (!this.isCurrentLifecycle(lifecycle)) {
|
|
455
|
+
throw new AiEmployeeClientError("SESSION_UNAVAILABLE", "\u4F1A\u8BDD\u521D\u59CB\u5316\u5DF2\u88AB\u65B0\u7684\u64CD\u4F5C\u66FF\u4EE3\u3002");
|
|
456
|
+
}
|
|
457
|
+
this.shouldReconnect = true;
|
|
458
|
+
this.openSocket(false);
|
|
459
|
+
return { sessionId: this.currentSessionId, messages: this.messages };
|
|
460
|
+
}
|
|
461
|
+
async loadStoredFirstMessageRequest() {
|
|
462
|
+
if (!this.firstMessageRequestStore) return null;
|
|
463
|
+
try {
|
|
464
|
+
const state = await this.firstMessageRequestStore.load(this.firstMessageRequestScope);
|
|
465
|
+
if (!state || typeof state.fingerprint !== "string") return null;
|
|
466
|
+
return {
|
|
467
|
+
fingerprint: state.fingerprint,
|
|
468
|
+
requestId: requiredRequestId(state.requestId)
|
|
469
|
+
};
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async saveStoredFirstMessageRequest(state) {
|
|
475
|
+
if (!this.firstMessageRequestStore) return;
|
|
476
|
+
try {
|
|
477
|
+
await this.firstMessageRequestStore.save(this.firstMessageRequestScope, state);
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async clearStoredFirstMessageRequest() {
|
|
482
|
+
if (!this.firstMessageRequestStore) return;
|
|
483
|
+
try {
|
|
484
|
+
await this.firstMessageRequestStore.clear(this.firstMessageRequestScope);
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async refreshHistory(lifecycle) {
|
|
489
|
+
const sessionId = this.currentSessionId;
|
|
490
|
+
if (!sessionId) return false;
|
|
491
|
+
try {
|
|
492
|
+
const history = await this.transport.listMessages(sessionId);
|
|
493
|
+
if (!this.isCurrentLifecycle(lifecycle) || this.currentSessionId !== sessionId) return false;
|
|
494
|
+
this.replaceHistory(history, sessionId);
|
|
495
|
+
return true;
|
|
496
|
+
} catch {
|
|
497
|
+
if (this.isCurrentLifecycle(lifecycle)) {
|
|
498
|
+
this.emitError("HISTORY_LOAD_FAILED", "\u5386\u53F2\u6D88\u606F\u52A0\u8F7D\u5931\u8D25\uFF0C\u5B9E\u65F6\u8FDE\u63A5\u4ECD\u4F1A\u7EE7\u7EED\u3002", true);
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
replaceHistory(history, sessionId) {
|
|
504
|
+
const unique = /* @__PURE__ */ new Map();
|
|
505
|
+
for (const candidate of history) {
|
|
506
|
+
const normalized = normalizeHistoryMessage(candidate, sessionId);
|
|
507
|
+
if (normalized) unique.set(normalized.id, normalized);
|
|
508
|
+
}
|
|
509
|
+
this.currentMessages = [...unique.values()];
|
|
510
|
+
this.seenMessageIds.clear();
|
|
511
|
+
for (const message of this.currentMessages) this.rememberMessageId(message.id);
|
|
512
|
+
this.emit("history", { sessionId, messages: this.messages });
|
|
513
|
+
}
|
|
514
|
+
openSocket(isReconnect) {
|
|
515
|
+
if (this.destroyed || !this.shouldReconnect) return;
|
|
516
|
+
this.clearReconnectTimer();
|
|
517
|
+
this.closeCurrentSocket();
|
|
518
|
+
this.setState(isReconnect ? "reconnecting" : "connecting");
|
|
519
|
+
let socket;
|
|
520
|
+
try {
|
|
521
|
+
socket = this.webSocketFactory(this.realtimeUrl);
|
|
522
|
+
} catch {
|
|
523
|
+
this.emitError("WEBSOCKET_UNAVAILABLE", "\u65E0\u6CD5\u5EFA\u7ACB\u5B9E\u65F6\u8FDE\u63A5\u3002", true);
|
|
524
|
+
this.scheduleReconnect();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const generation = ++this.socketGeneration;
|
|
528
|
+
this.socket = socket;
|
|
529
|
+
this.socketAuthenticationFailure = null;
|
|
530
|
+
socket.onopen = () => void this.authenticateSocket(socket, generation);
|
|
531
|
+
socket.onmessage = (event) => void this.handleSocketData(event.data, socket, generation);
|
|
532
|
+
socket.onerror = () => {
|
|
533
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
534
|
+
this.emitError("WEBSOCKET_ERROR", "\u5B9E\u65F6\u8FDE\u63A5\u53D1\u751F\u5F02\u5E38\u3002", true);
|
|
535
|
+
};
|
|
536
|
+
socket.onclose = (event) => this.handleSocketClose(socket, generation, event.code);
|
|
537
|
+
}
|
|
538
|
+
async authenticateSocket(socket, generation) {
|
|
539
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
540
|
+
this.setState("authenticating");
|
|
541
|
+
let accessToken = "";
|
|
542
|
+
try {
|
|
543
|
+
accessToken = (await this.transport.getAccessToken()).trim();
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
547
|
+
if (!accessToken) {
|
|
548
|
+
this.emitError("TOKEN_UNAVAILABLE", "\u65E0\u6CD5\u83B7\u53D6\u5B9E\u65F6\u8FDE\u63A5\u51ED\u8BC1\u3002", true);
|
|
549
|
+
socket.close();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
socket.send(JSON.stringify(createAuthenticateFrame(accessToken)));
|
|
553
|
+
accessToken = "";
|
|
554
|
+
}
|
|
555
|
+
async handleSocketData(data, socket, generation) {
|
|
556
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
557
|
+
let raw = "";
|
|
558
|
+
if (typeof data === "string") raw = data;
|
|
559
|
+
else if (data instanceof Blob) raw = await data.text();
|
|
560
|
+
else if (data instanceof ArrayBuffer) raw = new TextDecoder().decode(data);
|
|
561
|
+
else {
|
|
562
|
+
this.emitError("PROTOCOL_ERROR", "\u6536\u5230\u65E0\u6CD5\u8BC6\u522B\u7684\u5B9E\u65F6\u6D88\u606F\u3002", true);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
566
|
+
const message = parseJsonRecord(raw);
|
|
567
|
+
if (!message) {
|
|
568
|
+
this.emitError("PROTOCOL_ERROR", "\u6536\u5230\u65E0\u6CD5\u89E3\u6790\u7684\u5B9E\u65F6\u6D88\u606F\u3002", true);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.handleServerMessage(message, socket, generation);
|
|
572
|
+
}
|
|
573
|
+
handleServerMessage(message, socket, generation) {
|
|
574
|
+
const type = text(message.type);
|
|
575
|
+
if (type === "realtime_ready") {
|
|
576
|
+
const channels = record(message.channels);
|
|
577
|
+
const aiChannel = record(channels.ai_employee);
|
|
578
|
+
if (aiChannel.available !== true) {
|
|
579
|
+
this.emitError("CHANNEL_UNAVAILABLE", "\u6570\u5B57\u5458\u5DE5\u5B9E\u65F6\u901A\u9053\u4E0D\u53EF\u7528\u3002", false);
|
|
580
|
+
this.shouldReconnect = false;
|
|
581
|
+
socket.close();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.reconnectAttempt = 0;
|
|
585
|
+
this.availabilityReconnectAttempt = 0;
|
|
586
|
+
this.startHeartbeat(numberValue(message.heartbeat_interval_seconds), socket, generation);
|
|
587
|
+
if (this.currentSessionId) {
|
|
588
|
+
this.subscribeCurrentSession(this.replayHistoryOnNextSubscription, true);
|
|
589
|
+
} else {
|
|
590
|
+
this.setState("connected");
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (type === "realtime_event" && message.channel === "ai_employee") {
|
|
595
|
+
this.handleAiEmployeeEvent(message.event);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (type === "channel_error" && message.channel === "ai_employee") {
|
|
599
|
+
this.handleChannelError(text(message.code));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (type === "authentication_error") {
|
|
603
|
+
const failure = authenticationFailure(text(message.code));
|
|
604
|
+
this.socketAuthenticationFailure = { generation, retryMode: failure.retryMode };
|
|
605
|
+
this.emitError(failure.code, failure.message, failure.retryMode !== "never");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (type === "protocol_error") {
|
|
609
|
+
this.emitError("PROTOCOL_ERROR", "\u5B9E\u65F6\u8FDE\u63A5\u534F\u8BAE\u5F02\u5E38\u3002", false);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
handleAiEmployeeEvent(rawEvent) {
|
|
613
|
+
const event = parseAiEmployeeEvent(rawEvent);
|
|
614
|
+
if (event.kind === "ignored" || event.sessionId !== this.currentSessionId) return;
|
|
615
|
+
if (event.kind === "session-title") {
|
|
616
|
+
this.emit("title", { sessionId: event.sessionId, title: event.title });
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (event.result.processing === true) this.assistantReceivedInCurrentTurn = false;
|
|
620
|
+
if (event.result.processing !== void 0) this.setProcessing(event.result.processing);
|
|
621
|
+
for (const message of event.result.messages) {
|
|
622
|
+
if (message.role === "user") this.assistantReceivedInCurrentTurn = false;
|
|
623
|
+
if (message.role === "user" && this.matchesPendingLocalMessage(message)) continue;
|
|
624
|
+
this.appendMessage(message, "realtime");
|
|
625
|
+
if (message.role === "assistant") this.assistantReceivedInCurrentTurn = true;
|
|
626
|
+
}
|
|
627
|
+
if (event.result.fallbackResult && !this.assistantReceivedInCurrentTurn) {
|
|
628
|
+
this.appendMessage(event.result.fallbackResult, "realtime");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
subscribeCurrentSession(replayHistory, channelReady = false) {
|
|
632
|
+
const sessionId = this.currentSessionId;
|
|
633
|
+
const socket = this.socket;
|
|
634
|
+
if (!sessionId || !socket || socket.readyState !== 1 || !channelReady && this.currentState !== "connected")
|
|
635
|
+
return;
|
|
636
|
+
this.setState("subscribing");
|
|
637
|
+
socket.send(JSON.stringify(createSubscriptionFrame(sessionId, "subscribe", replayHistory)));
|
|
638
|
+
this.replayHistoryOnNextSubscription = false;
|
|
639
|
+
this.setState("connected");
|
|
640
|
+
this.emit("ready", { sessionId });
|
|
641
|
+
}
|
|
642
|
+
rememberPendingLocalMessage(content) {
|
|
643
|
+
const now = Date.now();
|
|
644
|
+
for (const [pendingContent, sentAt] of this.pendingLocalMessages) {
|
|
645
|
+
if (now - sentAt > LOCAL_MESSAGE_MATCH_WINDOW_MS) {
|
|
646
|
+
this.pendingLocalMessages.delete(pendingContent);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
this.pendingLocalMessages.set(content, now);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* 本地已渲染的用户消息会被服务端以 replay 和实时事件多次回送,且不保证复用
|
|
653
|
+
* 同一个消息 ID。窗口期内不消费该条目,多次回声都要挡住;本地发送本身每次都
|
|
654
|
+
* 会追加气泡,所以重复发送同样内容不会因此丢消息。
|
|
655
|
+
*/
|
|
656
|
+
matchesPendingLocalMessage(message) {
|
|
657
|
+
const sentAt = this.pendingLocalMessages.get(message.content);
|
|
658
|
+
if (sentAt === void 0) return false;
|
|
659
|
+
if (Date.now() - sentAt <= LOCAL_MESSAGE_MATCH_WINDOW_MS) return true;
|
|
660
|
+
this.pendingLocalMessages.delete(message.content);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
appendMessage(message, source) {
|
|
664
|
+
if (this.seenMessageIds.has(message.id)) return;
|
|
665
|
+
this.rememberMessageId(message.id);
|
|
666
|
+
this.currentMessages = [...this.currentMessages, message];
|
|
667
|
+
this.emit("message", { sessionId: message.sessionId, message, source });
|
|
668
|
+
}
|
|
669
|
+
rememberMessageId(id) {
|
|
670
|
+
this.seenMessageIds.add(id);
|
|
671
|
+
if (this.seenMessageIds.size <= MAX_SEEN_MESSAGE_IDS) return;
|
|
672
|
+
const oldest = this.seenMessageIds.values().next().value;
|
|
673
|
+
if (typeof oldest === "string") this.seenMessageIds.delete(oldest);
|
|
674
|
+
}
|
|
675
|
+
handleChannelError(code) {
|
|
676
|
+
if (code === "SESSION_NOT_FOUND") {
|
|
677
|
+
this.emitError("SESSION_NOT_FOUND", "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5F53\u524D\u7528\u6237\u65E0\u6743\u8BBF\u95EE\u3002", false);
|
|
678
|
+
} else {
|
|
679
|
+
this.emitError("CHANNEL_UNAVAILABLE", "\u6570\u5B57\u5458\u5DE5\u5B9E\u65F6\u901A\u9053\u6682\u4E0D\u53EF\u7528\u3002", true);
|
|
680
|
+
}
|
|
681
|
+
this.setProcessing(false);
|
|
682
|
+
}
|
|
683
|
+
startHeartbeat(intervalSeconds, socket, generation) {
|
|
684
|
+
this.clearHeartbeatTimer();
|
|
685
|
+
const requestedMs = Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds * 1e3 : 25e3;
|
|
686
|
+
const intervalMs = Math.max(this.minimumHeartbeatMs, requestedMs);
|
|
687
|
+
this.heartbeatTimer = setInterval(() => {
|
|
688
|
+
if (!this.isCurrentSocket(socket, generation) || socket.readyState !== 1) return;
|
|
689
|
+
socket.send(JSON.stringify(createPingFrame()));
|
|
690
|
+
}, intervalMs);
|
|
691
|
+
}
|
|
692
|
+
handleSocketClose(socket, generation, code) {
|
|
693
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
694
|
+
this.socket = null;
|
|
695
|
+
this.clearHeartbeatTimer();
|
|
696
|
+
const authenticationFailure3 = this.socketAuthenticationFailure?.generation === generation ? this.socketAuthenticationFailure : null;
|
|
697
|
+
this.socketAuthenticationFailure = null;
|
|
698
|
+
if (!this.shouldReconnect || this.destroyed) {
|
|
699
|
+
if (!this.destroyed) this.setState("disconnected");
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (authenticationFailure3?.retryMode === "never") {
|
|
703
|
+
this.shouldReconnect = false;
|
|
704
|
+
this.setState("disconnected");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (code === 4400 || code === 4403) {
|
|
708
|
+
this.shouldReconnect = false;
|
|
709
|
+
if (!authenticationFailure3) {
|
|
710
|
+
const errorCode = code === 4403 ? "AUTHENTICATION_FAILED" : "PROTOCOL_ERROR";
|
|
711
|
+
const message = code === 4403 ? "\u5F53\u524D\u8EAB\u4EFD\u65E0\u6743\u5EFA\u7ACB\u5B9E\u65F6\u8FDE\u63A5\u3002" : "\u5B9E\u65F6\u8FDE\u63A5\u534F\u8BAE\u4E0D\u53D7\u652F\u6301\u3002";
|
|
712
|
+
this.emitError(errorCode, message, false);
|
|
713
|
+
}
|
|
714
|
+
this.setState("disconnected");
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (authenticationFailure3?.retryMode === "availability" || code === 1013) {
|
|
718
|
+
if (!authenticationFailure3) {
|
|
719
|
+
this.emitError("REALTIME_UNAVAILABLE", "\u5B9E\u65F6\u9274\u6743\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u6B63\u5728\u91CD\u8BD5\u3002", true);
|
|
720
|
+
}
|
|
721
|
+
this.scheduleAvailabilityReconnect();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (code === 4401) {
|
|
725
|
+
if (!authenticationFailure3) {
|
|
726
|
+
this.emitError("AUTHENTICATION_FAILED", "\u8FDE\u63A5\u51ED\u8BC1\u5DF2\u5931\u6548\uFF0C\u6B63\u5728\u91CD\u65B0\u8FDE\u63A5\u3002", true);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
this.scheduleReconnect();
|
|
730
|
+
}
|
|
731
|
+
scheduleAvailabilityReconnect() {
|
|
732
|
+
if (this.availabilityReconnectAttempt >= MAX_AVAILABILITY_RECONNECTS) {
|
|
733
|
+
this.shouldReconnect = false;
|
|
734
|
+
this.emitError("REALTIME_UNAVAILABLE", "\u5B9E\u65F6\u9274\u6743\u670D\u52A1\u6301\u7EED\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", false);
|
|
735
|
+
this.setState("disconnected");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
this.availabilityReconnectAttempt += 1;
|
|
739
|
+
this.scheduleReconnect();
|
|
740
|
+
}
|
|
741
|
+
scheduleReconnect() {
|
|
742
|
+
if (this.destroyed || !this.shouldReconnect || this.reconnectTimer) return;
|
|
743
|
+
this.setState("reconnecting");
|
|
744
|
+
const baseDelay = Math.min(
|
|
745
|
+
this.reconnectOptions.initialDelayMs * this.reconnectOptions.factor ** this.reconnectAttempt,
|
|
746
|
+
this.reconnectOptions.maximumDelayMs
|
|
747
|
+
);
|
|
748
|
+
const jitterRange = baseDelay * this.reconnectOptions.jitter;
|
|
749
|
+
const delay = Math.max(0, baseDelay - jitterRange + Math.random() * jitterRange * 2);
|
|
750
|
+
this.reconnectAttempt += 1;
|
|
751
|
+
const lifecycle = this.lifecycleGeneration;
|
|
752
|
+
this.reconnectTimer = setTimeout(() => {
|
|
753
|
+
this.reconnectTimer = null;
|
|
754
|
+
void this.recoverAndReconnect(lifecycle);
|
|
755
|
+
}, delay);
|
|
756
|
+
}
|
|
757
|
+
async recoverAndReconnect(lifecycle) {
|
|
758
|
+
if (!this.isCurrentLifecycle(lifecycle) || !this.shouldReconnect) return;
|
|
759
|
+
const historyRecovered = await this.refreshHistory(lifecycle);
|
|
760
|
+
if (!this.isCurrentLifecycle(lifecycle) || !this.shouldReconnect) return;
|
|
761
|
+
this.replayHistoryOnNextSubscription = !historyRecovered;
|
|
762
|
+
this.openSocket(true);
|
|
763
|
+
}
|
|
764
|
+
stopConnection(setDisconnectedState) {
|
|
765
|
+
this.shouldReconnect = false;
|
|
766
|
+
this.clearReconnectTimer();
|
|
767
|
+
this.clearHeartbeatTimer();
|
|
768
|
+
this.socketAuthenticationFailure = null;
|
|
769
|
+
this.closeCurrentSocket();
|
|
770
|
+
this.setProcessing(false);
|
|
771
|
+
if (setDisconnectedState) this.setState("disconnected");
|
|
772
|
+
}
|
|
773
|
+
closeCurrentSocket() {
|
|
774
|
+
const socket = this.socket;
|
|
775
|
+
this.socket = null;
|
|
776
|
+
this.socketGeneration += 1;
|
|
777
|
+
this.socketAuthenticationFailure = null;
|
|
778
|
+
if (socket && socket.readyState < 2) socket.close();
|
|
779
|
+
}
|
|
780
|
+
clearReconnectTimer() {
|
|
781
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
782
|
+
this.reconnectTimer = null;
|
|
783
|
+
}
|
|
784
|
+
clearHeartbeatTimer() {
|
|
785
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
786
|
+
this.heartbeatTimer = null;
|
|
787
|
+
}
|
|
788
|
+
setState(next) {
|
|
789
|
+
if (this.currentState === next) return;
|
|
790
|
+
const previous = this.currentState;
|
|
791
|
+
this.currentState = next;
|
|
792
|
+
this.emit("connection-state-change", { previous, current: next });
|
|
793
|
+
}
|
|
794
|
+
setProcessing(processing) {
|
|
795
|
+
if (this.processing === processing) return;
|
|
796
|
+
this.processing = processing;
|
|
797
|
+
this.emit("processing-state-change", { processing });
|
|
798
|
+
}
|
|
799
|
+
emitError(code, message, recoverable) {
|
|
800
|
+
const detail = { code, message, recoverable };
|
|
801
|
+
this.emit("error", detail);
|
|
802
|
+
return detail;
|
|
803
|
+
}
|
|
804
|
+
emit(type, detail) {
|
|
805
|
+
for (const listener of this.listeners.get(type) ?? []) listener(detail);
|
|
806
|
+
}
|
|
807
|
+
isCurrentSocket(socket, generation) {
|
|
808
|
+
return !this.destroyed && this.socket === socket && this.socketGeneration === generation;
|
|
809
|
+
}
|
|
810
|
+
isCurrentLifecycle(generation) {
|
|
811
|
+
return !this.destroyed && this.lifecycleGeneration === generation;
|
|
812
|
+
}
|
|
813
|
+
assertUsable() {
|
|
814
|
+
if (this.destroyed) {
|
|
815
|
+
throw new AiEmployeeClientError("CLIENT_DESTROYED", "\u5BA2\u6237\u7AEF\u5DF2\u9500\u6BC1\u3002");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
function validateRealtimeUrl(rawUrl) {
|
|
820
|
+
let url;
|
|
821
|
+
try {
|
|
822
|
+
url = new URL(rawUrl);
|
|
823
|
+
} catch {
|
|
824
|
+
throw new AiEmployeeClientError("INVALID_CONFIGURATION", "realtimeUrl \u4E0D\u662F\u6709\u6548\u5730\u5740\u3002");
|
|
825
|
+
}
|
|
826
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
|
827
|
+
throw new AiEmployeeClientError("INVALID_CONFIGURATION", "realtimeUrl \u53EA\u5141\u8BB8 ws: \u6216 wss:\u3002");
|
|
828
|
+
}
|
|
829
|
+
if (url.search || url.hash || url.username || url.password) {
|
|
830
|
+
throw new AiEmployeeClientError(
|
|
831
|
+
"INVALID_CONFIGURATION",
|
|
832
|
+
"realtimeUrl \u4E0D\u5141\u8BB8\u5305\u542B\u67E5\u8BE2\u53C2\u6570\u3001\u7247\u6BB5\u6216\u7528\u6237\u51ED\u636E\u3002"
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return url.toString();
|
|
836
|
+
}
|
|
837
|
+
function requiredText(value, field) {
|
|
838
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
839
|
+
if (!normalized) {
|
|
840
|
+
throw new AiEmployeeClientError("INVALID_CONFIGURATION", `${field} \u4E0D\u80FD\u4E3A\u7A7A\u3002`);
|
|
841
|
+
}
|
|
842
|
+
return normalized;
|
|
843
|
+
}
|
|
844
|
+
function normalizeSessionId(value) {
|
|
845
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
846
|
+
}
|
|
847
|
+
function normalizeFirstMessageOptions(options) {
|
|
848
|
+
const attachments = (options.attachments ?? []).map((attachment) => {
|
|
849
|
+
const transactionId = typeof attachment.transactionId === "string" ? attachment.transactionId.trim() : "";
|
|
850
|
+
const fspath = typeof attachment.fspath === "string" ? attachment.fspath.trim() : "";
|
|
851
|
+
if (!transactionId || !fspath.startsWith("fs/") || fspath.includes("\\") || fspath.split("/").some((segment) => segment === "..")) {
|
|
852
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u9644\u4EF6\u5F15\u7528\u65E0\u6548\u3002");
|
|
853
|
+
}
|
|
854
|
+
return { transactionId, fspath };
|
|
855
|
+
});
|
|
856
|
+
const trustedContext = options.trustedContext ?? null;
|
|
857
|
+
if (trustedContext !== null && (typeof trustedContext !== "object" || Array.isArray(trustedContext))) {
|
|
858
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u4E0A\u4E0B\u6587\u65E0\u6548\u3002");
|
|
859
|
+
}
|
|
860
|
+
return { attachments, trustedContext };
|
|
861
|
+
}
|
|
862
|
+
function firstMessageFingerprint(content, options) {
|
|
863
|
+
const serialized = stableFirstMessageString([
|
|
864
|
+
content,
|
|
865
|
+
options.attachments,
|
|
866
|
+
options.trustedContext
|
|
867
|
+
]);
|
|
868
|
+
return `v1:${hashText(serialized)}:${hashText(`first-message:${serialized}`)}:${serialized.length}`;
|
|
869
|
+
}
|
|
870
|
+
function stableFirstMessageString(value) {
|
|
871
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
872
|
+
const normalize = (candidate) => {
|
|
873
|
+
if (candidate === null || typeof candidate !== "object") return candidate;
|
|
874
|
+
if (seen.has(candidate)) {
|
|
875
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u4E0A\u4E0B\u6587\u65E0\u6548\u3002");
|
|
876
|
+
}
|
|
877
|
+
seen.add(candidate);
|
|
878
|
+
if (Array.isArray(candidate)) {
|
|
879
|
+
const result2 = candidate.map((item) => normalize(item));
|
|
880
|
+
seen.delete(candidate);
|
|
881
|
+
return result2;
|
|
882
|
+
}
|
|
883
|
+
const source = candidate;
|
|
884
|
+
const result = {};
|
|
885
|
+
for (const key of Object.keys(source).sort()) result[key] = normalize(source[key]);
|
|
886
|
+
seen.delete(candidate);
|
|
887
|
+
return result;
|
|
888
|
+
};
|
|
889
|
+
return JSON.stringify(normalize(value));
|
|
890
|
+
}
|
|
891
|
+
function requiredRequestId(value) {
|
|
892
|
+
const requestId = typeof value === "string" ? value.trim() : "";
|
|
893
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(requestId)) {
|
|
894
|
+
throw new AiEmployeeClientError("INVALID_CONFIGURATION", "requestIdFactory \u5FC5\u987B\u8FD4\u56DE UUID\u3002");
|
|
895
|
+
}
|
|
896
|
+
return requestId;
|
|
897
|
+
}
|
|
898
|
+
function createRequestId() {
|
|
899
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
900
|
+
return crypto.randomUUID();
|
|
901
|
+
}
|
|
902
|
+
if (typeof crypto === "undefined" || typeof crypto.getRandomValues !== "function") {
|
|
903
|
+
throw new AiEmployeeClientError("INVALID_CONFIGURATION", "\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u5B89\u5168\u751F\u6210 request_id\u3002");
|
|
904
|
+
}
|
|
905
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
906
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
907
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
908
|
+
const value = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
909
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
910
|
+
}
|
|
911
|
+
function normalizeFirstMessageResult(value, expectedAeCode, expectedAgentCode) {
|
|
912
|
+
if (!value || typeof value !== "object") {
|
|
913
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u53D7\u7406\u54CD\u5E94\u65E0\u6548\u3002");
|
|
914
|
+
}
|
|
915
|
+
const sessionId = requiredFirstMessageResultText(value.sessionId);
|
|
916
|
+
const messageId = requiredFirstMessageResultText(value.messageId);
|
|
917
|
+
const executionId = requiredFirstMessageResultText(value.executionId);
|
|
918
|
+
const aeCode = requiredFirstMessageResultText(value.aeCode);
|
|
919
|
+
const agentCode = requiredFirstMessageResultText(value.agentCode);
|
|
920
|
+
const conversationType = requiredFirstMessageResultText(value.conversationType);
|
|
921
|
+
if (aeCode !== expectedAeCode || agentCode !== expectedAgentCode) {
|
|
922
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u53D7\u7406\u54CD\u5E94\u4E0E\u5F53\u524D\u6570\u5B57\u5458\u5DE5\u4E0D\u5339\u914D\u3002");
|
|
923
|
+
}
|
|
924
|
+
if (value.status !== "running" && value.status !== "queued") {
|
|
925
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u53D7\u7406\u72B6\u6001\u65E0\u6548\u3002");
|
|
926
|
+
}
|
|
927
|
+
const startedAt = requiredIsoDate(value.startedAt, "transport startedAt");
|
|
928
|
+
const result = {
|
|
929
|
+
sessionId,
|
|
930
|
+
messageId,
|
|
931
|
+
executionId,
|
|
932
|
+
status: value.status,
|
|
933
|
+
startedAt,
|
|
934
|
+
aeCode,
|
|
935
|
+
agentCode,
|
|
936
|
+
conversationType
|
|
937
|
+
};
|
|
938
|
+
if (typeof value.title === "string" && value.title.trim()) result.title = value.title.trim();
|
|
939
|
+
if (value.createdAt) result.createdAt = requiredIsoDate(value.createdAt, "transport createdAt");
|
|
940
|
+
return result;
|
|
941
|
+
}
|
|
942
|
+
function requiredIsoDate(value, field) {
|
|
943
|
+
const normalized = requiredFirstMessageResultText(value);
|
|
944
|
+
if (!Number.isFinite(Date.parse(normalized))) {
|
|
945
|
+
throw new AiEmployeeClientError("SEND_FAILED", `${field} \u4E0D\u662F\u6709\u6548\u65F6\u95F4\u3002`);
|
|
946
|
+
}
|
|
947
|
+
return normalized;
|
|
948
|
+
}
|
|
949
|
+
function requiredFirstMessageResultText(value) {
|
|
950
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
951
|
+
throw new AiEmployeeClientError("SEND_FAILED", "\u9996\u6761\u6D88\u606F\u53D7\u7406\u54CD\u5E94\u65E0\u6548\u3002");
|
|
952
|
+
}
|
|
953
|
+
return value.trim();
|
|
954
|
+
}
|
|
955
|
+
function authenticationFailure(code) {
|
|
956
|
+
if (code === "UNAUTHORIZED") {
|
|
957
|
+
return {
|
|
958
|
+
code: "AUTHENTICATION_FAILED",
|
|
959
|
+
message: "\u8FDE\u63A5\u51ED\u8BC1\u5DF2\u5931\u6548\uFF0C\u6B63\u5728\u91CD\u65B0\u8FDE\u63A5\u3002",
|
|
960
|
+
retryMode: "token"
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
if (code === "ORGANIZATION_CONTEXT_REQUIRED") {
|
|
964
|
+
return {
|
|
965
|
+
code: "ORGANIZATION_CONTEXT_REQUIRED",
|
|
966
|
+
message: "\u8BF7\u9009\u62E9\u4F01\u4E1A/\u56E2\u961F\u540E\u518D\u5F00\u59CB\u54A8\u8BE2\u3002",
|
|
967
|
+
retryMode: "never"
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
if (code === "INVALID_ARGUMENT") {
|
|
971
|
+
return {
|
|
972
|
+
code: "PROTOCOL_ERROR",
|
|
973
|
+
message: "\u5B9E\u65F6\u8FDE\u63A5\u534F\u8BAE\u4E0D\u53D7\u652F\u6301\u3002",
|
|
974
|
+
retryMode: "never"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
if (isRealtimeAvailabilityFailure(code)) {
|
|
978
|
+
return {
|
|
979
|
+
code: "REALTIME_UNAVAILABLE",
|
|
980
|
+
message: "\u5B9E\u65F6\u9274\u6743\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u6B63\u5728\u91CD\u8BD5\u3002",
|
|
981
|
+
retryMode: "availability"
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
if (code === "AI_EMPLOYEE_NOT_AVAILABLE" || code === "APPLICATION_NOT_SUPPORTED") {
|
|
985
|
+
return {
|
|
986
|
+
code: "CHANNEL_UNAVAILABLE",
|
|
987
|
+
message: "\u5F53\u524D\u6570\u5B57\u5458\u5DE5\u6682\u4E0D\u53EF\u7528\u3002",
|
|
988
|
+
retryMode: "never"
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
if (code === "ACCESS_DENIED") {
|
|
992
|
+
return {
|
|
993
|
+
code: "AUTHENTICATION_FAILED",
|
|
994
|
+
message: "\u5F53\u524D\u8EAB\u4EFD\u65E0\u6743\u5EFA\u7ACB\u5B9E\u65F6\u8FDE\u63A5\u3002",
|
|
995
|
+
retryMode: "never"
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
code: "AUTHENTICATION_FAILED",
|
|
1000
|
+
message: "\u5B9E\u65F6\u8FDE\u63A5\u8BA4\u8BC1\u5931\u8D25\u3002",
|
|
1001
|
+
retryMode: "never"
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function transportErrorCode(error) {
|
|
1005
|
+
if (error instanceof AiEmployeeTransportError) return error.code;
|
|
1006
|
+
if (!error || typeof error !== "object" || !("code" in error)) return null;
|
|
1007
|
+
switch (error.code) {
|
|
1008
|
+
case "UNAUTHORIZED":
|
|
1009
|
+
case "ACCESS_DENIED":
|
|
1010
|
+
case "AI_EMPLOYEE_NOT_AVAILABLE":
|
|
1011
|
+
case "APPLICATION_NOT_SUPPORTED":
|
|
1012
|
+
case "ORGANIZATION_CONTEXT_REQUIRED":
|
|
1013
|
+
case "INVALID_ARGUMENT":
|
|
1014
|
+
case "IDEMPOTENCY_CONFLICT":
|
|
1015
|
+
case "REALTIME_UNAVAILABLE":
|
|
1016
|
+
case "TOO_MANY_REQUESTS":
|
|
1017
|
+
return error.code;
|
|
1018
|
+
default:
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function createSessionFirstMessageRequestStore() {
|
|
1023
|
+
try {
|
|
1024
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
1025
|
+
} catch {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const key = (scope) => `${FIRST_MESSAGE_STORAGE_PREFIX}:${hashText(scope)}`;
|
|
1029
|
+
return {
|
|
1030
|
+
load(scope) {
|
|
1031
|
+
const raw = sessionStorage.getItem(key(scope));
|
|
1032
|
+
if (!raw) return null;
|
|
1033
|
+
const parsed = JSON.parse(raw);
|
|
1034
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
1035
|
+
const state = parsed;
|
|
1036
|
+
return typeof state.fingerprint === "string" && typeof state.requestId === "string" ? { fingerprint: state.fingerprint, requestId: state.requestId } : null;
|
|
1037
|
+
},
|
|
1038
|
+
save(scope, state) {
|
|
1039
|
+
sessionStorage.setItem(
|
|
1040
|
+
key(scope),
|
|
1041
|
+
JSON.stringify({ fingerprint: state.fingerprint, requestId: state.requestId })
|
|
1042
|
+
);
|
|
1043
|
+
},
|
|
1044
|
+
clear(scope) {
|
|
1045
|
+
sessionStorage.removeItem(key(scope));
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function isRealtimeAvailabilityFailure(code) {
|
|
1050
|
+
return code === "REALTIME_UNAVAILABLE" || code === "KANBAN_AUTH_FAILED" || code.startsWith("SERVICE_");
|
|
1051
|
+
}
|
|
1052
|
+
function normalizeReconnectOptions(options) {
|
|
1053
|
+
return {
|
|
1054
|
+
initialDelayMs: positiveNumber(options?.initialDelayMs, DEFAULT_RECONNECT.initialDelayMs),
|
|
1055
|
+
maximumDelayMs: positiveNumber(options?.maximumDelayMs, DEFAULT_RECONNECT.maximumDelayMs),
|
|
1056
|
+
factor: Math.max(1, positiveNumber(options?.factor, DEFAULT_RECONNECT.factor)),
|
|
1057
|
+
jitter: Math.min(1, Math.max(0, options?.jitter ?? DEFAULT_RECONNECT.jitter))
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function positiveNumber(value, fallback) {
|
|
1061
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1062
|
+
}
|
|
1063
|
+
function normalizeHistoryMessage(candidate, expectedSessionId) {
|
|
1064
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
1065
|
+
const id = text(candidate.id).trim();
|
|
1066
|
+
const content = text(candidate.content).trim();
|
|
1067
|
+
const role = candidate.role;
|
|
1068
|
+
if (!id || !content || !["user", "assistant", "system", "error"].includes(role)) return null;
|
|
1069
|
+
return {
|
|
1070
|
+
id,
|
|
1071
|
+
sessionId: expectedSessionId,
|
|
1072
|
+
role,
|
|
1073
|
+
content: role === "error" ? "\u6D88\u606F\u5904\u7406\u51FA\u73B0\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : content,
|
|
1074
|
+
createdAt: text(candidate.createdAt) || (/* @__PURE__ */ new Date()).toISOString()
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function text(value) {
|
|
1078
|
+
return typeof value === "string" ? value : "";
|
|
1079
|
+
}
|
|
1080
|
+
function record(value) {
|
|
1081
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1082
|
+
}
|
|
1083
|
+
function numberValue(value) {
|
|
1084
|
+
return typeof value === "number" ? value : Number.NaN;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/assistant-actions.ts
|
|
1088
|
+
var ACTION_KEYS = ["action", "entry", "transaction-id", "trigger", "type"];
|
|
1089
|
+
var OPEN_CUSTOMER_SUPPORT_MARKER = /"action"\s*:\s*"open_customer_support"/g;
|
|
1090
|
+
var UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1091
|
+
function parseAiEmployeeAssistantContent(content) {
|
|
1092
|
+
const source = typeof content === "string" ? content : "";
|
|
1093
|
+
const candidate = trailingStructuredJson(source);
|
|
1094
|
+
if (!candidate || markerCount(source) !== 1) return unchanged(source);
|
|
1095
|
+
let value;
|
|
1096
|
+
try {
|
|
1097
|
+
value = JSON.parse(candidate.json);
|
|
1098
|
+
} catch {
|
|
1099
|
+
return unchanged(source);
|
|
1100
|
+
}
|
|
1101
|
+
const action = parseOpenCustomerSupportAction(value);
|
|
1102
|
+
if (!action) return unchanged(source);
|
|
1103
|
+
return Object.freeze({
|
|
1104
|
+
content: candidate.content,
|
|
1105
|
+
actions: Object.freeze([action])
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
function isUuidV4(value) {
|
|
1109
|
+
return typeof value === "string" && UUID_V4_PATTERN.test(value);
|
|
1110
|
+
}
|
|
1111
|
+
function trailingStructuredJson(content) {
|
|
1112
|
+
const withoutTrailingWhitespace = content.trimEnd();
|
|
1113
|
+
const fenced = withoutTrailingWhitespace.match(
|
|
1114
|
+
/(?:^|\r?\n)[\t ]*```json[\t ]*\r?\n([\s\S]*?)\r?\n[\t ]*```$/i
|
|
1115
|
+
);
|
|
1116
|
+
if (fenced?.index !== void 0) {
|
|
1117
|
+
return {
|
|
1118
|
+
content: withoutTrailingWhitespace.slice(0, fenced.index).trimEnd(),
|
|
1119
|
+
json: fenced[1]?.trim() ?? ""
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
const starts = [...withoutTrailingWhitespace.matchAll(/(?:^|\r?\n)[\t ]*(\{)/g)];
|
|
1123
|
+
for (let index = starts.length - 1; index >= 0; index -= 1) {
|
|
1124
|
+
const match = starts[index];
|
|
1125
|
+
const objectOffset = match?.index === void 0 ? -1 : match.index + (match[0]?.indexOf("{") ?? -1);
|
|
1126
|
+
if (objectOffset < 0) continue;
|
|
1127
|
+
const json = withoutTrailingWhitespace.slice(objectOffset).trim();
|
|
1128
|
+
try {
|
|
1129
|
+
JSON.parse(json);
|
|
1130
|
+
return {
|
|
1131
|
+
content: withoutTrailingWhitespace.slice(0, match?.index ?? 0).trimEnd(),
|
|
1132
|
+
json
|
|
1133
|
+
};
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
function parseOpenCustomerSupportAction(value) {
|
|
1140
|
+
if (!isRecord(value)) return null;
|
|
1141
|
+
const keys = Object.keys(value).sort();
|
|
1142
|
+
if (keys.length !== ACTION_KEYS.length || keys.some((key, index) => key !== ACTION_KEYS[index])) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
if (value.type !== "assistant_action" || value.action !== "open_customer_support" || !isUuidV4(value["transaction-id"]) || value.trigger !== "out_of_scope" || value.entry !== "help_and_support") {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
return Object.freeze({
|
|
1149
|
+
type: "assistant_action",
|
|
1150
|
+
action: "open_customer_support",
|
|
1151
|
+
transactionId: value["transaction-id"],
|
|
1152
|
+
trigger: "out_of_scope",
|
|
1153
|
+
entry: "help_and_support"
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function markerCount(value) {
|
|
1157
|
+
return [...value.matchAll(OPEN_CUSTOMER_SUPPORT_MARKER)].length;
|
|
1158
|
+
}
|
|
1159
|
+
function unchanged(content) {
|
|
1160
|
+
return Object.freeze({ content, actions: Object.freeze([]) });
|
|
1161
|
+
}
|
|
1162
|
+
function isRecord(value) {
|
|
1163
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/customer-support-client.ts
|
|
1167
|
+
var DEFAULT_RECONNECT2 = {
|
|
1168
|
+
initialDelayMs: 1e3,
|
|
1169
|
+
maximumDelayMs: 3e4,
|
|
1170
|
+
factor: 2,
|
|
1171
|
+
jitter: 0.2
|
|
1172
|
+
};
|
|
1173
|
+
var MAX_TOKEN_RECONNECTS = 1;
|
|
1174
|
+
var MAX_AVAILABILITY_RECONNECTS2 = 3;
|
|
1175
|
+
var CustomerSupportClientError = class extends Error {
|
|
1176
|
+
code;
|
|
1177
|
+
constructor(code, message) {
|
|
1178
|
+
super(message);
|
|
1179
|
+
this.name = "CustomerSupportClientError";
|
|
1180
|
+
this.code = code;
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
var CustomerSupportClient = class {
|
|
1184
|
+
realtimeUrl;
|
|
1185
|
+
transport;
|
|
1186
|
+
webSocketFactory;
|
|
1187
|
+
requestIdFactory;
|
|
1188
|
+
reconnectOptions;
|
|
1189
|
+
minimumHeartbeatMs;
|
|
1190
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1191
|
+
currentSession = null;
|
|
1192
|
+
currentSessions = [];
|
|
1193
|
+
currentMessages = [];
|
|
1194
|
+
currentState = "idle";
|
|
1195
|
+
socket = null;
|
|
1196
|
+
heartbeatTimer = null;
|
|
1197
|
+
reconnectTimer = null;
|
|
1198
|
+
reconnectAttempt = 0;
|
|
1199
|
+
tokenReconnectAttempt = 0;
|
|
1200
|
+
availabilityReconnectAttempt = 0;
|
|
1201
|
+
socketGeneration = 0;
|
|
1202
|
+
socketAuthenticationFailure = null;
|
|
1203
|
+
destroyed = false;
|
|
1204
|
+
shouldReconnect = false;
|
|
1205
|
+
sending = false;
|
|
1206
|
+
historyRefresh = null;
|
|
1207
|
+
sessionsRefresh = null;
|
|
1208
|
+
readCursorStates = /* @__PURE__ */ new Map();
|
|
1209
|
+
readLifecycleGeneration = 0;
|
|
1210
|
+
requestedSessionId;
|
|
1211
|
+
constructor(options) {
|
|
1212
|
+
this.realtimeUrl = validateRealtimeUrl2(options.realtimeUrl);
|
|
1213
|
+
if (!options.transport) {
|
|
1214
|
+
throw new CustomerSupportClientError("INVALID_CONFIGURATION", "\u7F3A\u5C11\u5BA2\u670D transport \u914D\u7F6E\u3002");
|
|
1215
|
+
}
|
|
1216
|
+
this.transport = options.transport;
|
|
1217
|
+
this.requestedSessionId = normalizedText(options.sessionId) || null;
|
|
1218
|
+
this.requestIdFactory = options.requestIdFactory ?? createRequestId2;
|
|
1219
|
+
this.reconnectOptions = normalizeReconnect(options.reconnect);
|
|
1220
|
+
this.minimumHeartbeatMs = Math.max(1, options.minimumHeartbeatMs ?? 5e3);
|
|
1221
|
+
this.webSocketFactory = options.webSocketFactory ?? ((url) => {
|
|
1222
|
+
if (typeof WebSocket === "undefined") {
|
|
1223
|
+
throw new CustomerSupportClientError(
|
|
1224
|
+
"WEBSOCKET_UNAVAILABLE",
|
|
1225
|
+
"\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301 WebSocket\u3002"
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
return new WebSocket(url);
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
get session() {
|
|
1232
|
+
return this.currentSession;
|
|
1233
|
+
}
|
|
1234
|
+
get messages() {
|
|
1235
|
+
return [...this.currentMessages];
|
|
1236
|
+
}
|
|
1237
|
+
get connectionState() {
|
|
1238
|
+
return this.currentState;
|
|
1239
|
+
}
|
|
1240
|
+
on(type, listener) {
|
|
1241
|
+
this.assertUsable();
|
|
1242
|
+
const listeners = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
|
|
1243
|
+
const untyped = listener;
|
|
1244
|
+
listeners.add(untyped);
|
|
1245
|
+
this.listeners.set(type, listeners);
|
|
1246
|
+
return () => listeners.delete(untyped);
|
|
1247
|
+
}
|
|
1248
|
+
async start() {
|
|
1249
|
+
this.assertUsable();
|
|
1250
|
+
try {
|
|
1251
|
+
const sessions = await this.transport.listSessions();
|
|
1252
|
+
this.assertUsable();
|
|
1253
|
+
this.replaceSessions(sessions);
|
|
1254
|
+
const requested = this.requestedSessionId;
|
|
1255
|
+
this.currentSession = requested ? sessions.find((session) => session.id === requested) ?? null : sessions.find(
|
|
1256
|
+
(session) => session.status !== "closed" && hasCustomerSupportMessages(session)
|
|
1257
|
+
) ?? sessions.find(hasCustomerSupportMessages) ?? null;
|
|
1258
|
+
if (this.currentSession) {
|
|
1259
|
+
this.emit("session", { session: this.currentSession });
|
|
1260
|
+
await this.refreshHistory();
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1263
|
+
this.emitError("HISTORY_LOAD_FAILED", "\u5BA2\u670D\u4F1A\u8BDD\u52A0\u8F7D\u5931\u8D25\uFF0C\u5B9E\u65F6\u8FDE\u63A5\u4ECD\u4F1A\u7EE7\u7EED\u3002", true);
|
|
1264
|
+
}
|
|
1265
|
+
this.shouldReconnect = true;
|
|
1266
|
+
this.openSocket(false);
|
|
1267
|
+
this.emit("ready", { sessionId: this.currentSession?.id ?? null });
|
|
1268
|
+
}
|
|
1269
|
+
prepareNewConversation() {
|
|
1270
|
+
this.assertUsable();
|
|
1271
|
+
this.currentSession = null;
|
|
1272
|
+
this.requestedSessionId = null;
|
|
1273
|
+
this.currentMessages = [];
|
|
1274
|
+
}
|
|
1275
|
+
async bindSession(sessionId) {
|
|
1276
|
+
this.assertUsable();
|
|
1277
|
+
const normalized = requiredText2(sessionId, "sessionId");
|
|
1278
|
+
const sessions = await this.transport.listSessions();
|
|
1279
|
+
this.replaceSessions(sessions);
|
|
1280
|
+
const session = sessions.find((candidate) => candidate.id === normalized);
|
|
1281
|
+
if (!session) {
|
|
1282
|
+
throw new CustomerSupportClientError("SESSION_UNAVAILABLE", "\u5BA2\u670D\u4F1A\u8BDD\u4E0D\u5B58\u5728\u3002");
|
|
1283
|
+
}
|
|
1284
|
+
this.currentSession = session;
|
|
1285
|
+
this.requestedSessionId = session.id;
|
|
1286
|
+
this.emit("session", { session });
|
|
1287
|
+
await this.refreshHistory();
|
|
1288
|
+
}
|
|
1289
|
+
async sendMessage(content) {
|
|
1290
|
+
this.assertUsable();
|
|
1291
|
+
const normalized = content.trim();
|
|
1292
|
+
if (!normalized) throw new CustomerSupportClientError("EMPTY_MESSAGE", "\u6D88\u606F\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
1293
|
+
if (this.sending) {
|
|
1294
|
+
throw new CustomerSupportClientError("SEND_IN_PROGRESS", "\u4E0A\u4E00\u6761\u6D88\u606F\u4ECD\u5728\u53D1\u9001\u4E2D\u3002");
|
|
1295
|
+
}
|
|
1296
|
+
this.sending = true;
|
|
1297
|
+
try {
|
|
1298
|
+
if (!this.currentSession) {
|
|
1299
|
+
const result = await this.transport.createSession({
|
|
1300
|
+
clientRequestId: requiredRequestId2(this.requestIdFactory()),
|
|
1301
|
+
title: normalized.slice(0, 30),
|
|
1302
|
+
initialMessage: normalized
|
|
1303
|
+
});
|
|
1304
|
+
this.assertUsable();
|
|
1305
|
+
this.currentSession = result.session;
|
|
1306
|
+
this.upsertSession(result.session);
|
|
1307
|
+
this.requestedSessionId = result.session.id;
|
|
1308
|
+
this.emit("session", { session: result.session });
|
|
1309
|
+
let message2 = result.initialMessage;
|
|
1310
|
+
if (!message2) {
|
|
1311
|
+
const page = await this.transport.listMessages(result.session.id);
|
|
1312
|
+
message2 = [...page.messages].reverse().find(
|
|
1313
|
+
(candidate) => candidate.senderType === "customer" && candidate.content.trim() === normalized
|
|
1314
|
+
);
|
|
1315
|
+
this.currentMessages = page.messages.filter((candidate) => candidate.id !== message2?.id);
|
|
1316
|
+
}
|
|
1317
|
+
if (!message2) throw new Error("Missing initial message");
|
|
1318
|
+
this.appendMessage(message2, "local");
|
|
1319
|
+
return message2;
|
|
1320
|
+
}
|
|
1321
|
+
const message = await this.transport.sendMessage(
|
|
1322
|
+
this.currentSession.id,
|
|
1323
|
+
requiredRequestId2(this.requestIdFactory()),
|
|
1324
|
+
normalized
|
|
1325
|
+
);
|
|
1326
|
+
this.assertUsable();
|
|
1327
|
+
this.appendMessage(message, "local");
|
|
1328
|
+
return message;
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (error instanceof CustomerSupportClientError) throw error;
|
|
1331
|
+
const detail = this.emitError("SEND_FAILED", "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
1332
|
+
throw new CustomerSupportClientError(detail.code, detail.message);
|
|
1333
|
+
} finally {
|
|
1334
|
+
this.sending = false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async listSessions() {
|
|
1338
|
+
this.assertUsable();
|
|
1339
|
+
return this.refreshSessions();
|
|
1340
|
+
}
|
|
1341
|
+
destroy() {
|
|
1342
|
+
if (this.destroyed) return;
|
|
1343
|
+
this.destroyed = true;
|
|
1344
|
+
this.shouldReconnect = false;
|
|
1345
|
+
this.clearTimers();
|
|
1346
|
+
this.socketGeneration += 1;
|
|
1347
|
+
this.socket?.close();
|
|
1348
|
+
this.socket = null;
|
|
1349
|
+
this.setState("destroyed");
|
|
1350
|
+
this.listeners.clear();
|
|
1351
|
+
this.currentMessages = [];
|
|
1352
|
+
this.currentSessions = [];
|
|
1353
|
+
this.historyRefresh = null;
|
|
1354
|
+
this.sessionsRefresh = null;
|
|
1355
|
+
this.readLifecycleGeneration += 1;
|
|
1356
|
+
this.readCursorStates.clear();
|
|
1357
|
+
}
|
|
1358
|
+
async refreshHistory(realtime = false) {
|
|
1359
|
+
const session = this.currentSession;
|
|
1360
|
+
if (!session) return;
|
|
1361
|
+
if (this.historyRefresh?.sessionId === session.id) return this.historyRefresh.promise;
|
|
1362
|
+
const pending = (async () => {
|
|
1363
|
+
try {
|
|
1364
|
+
const page = await this.transport.listMessages(session.id);
|
|
1365
|
+
if (this.destroyed || this.currentSession?.id !== session.id) return;
|
|
1366
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1367
|
+
for (const message of page.messages) {
|
|
1368
|
+
if (message.sessionId === session.id && message.id) unique.set(message.id, message);
|
|
1369
|
+
}
|
|
1370
|
+
const previousIds = new Set(this.currentMessages.map((message) => message.id));
|
|
1371
|
+
this.currentMessages = [...unique.values()].sort(compareMessages);
|
|
1372
|
+
this.emit("history", { sessionId: session.id, messages: this.messages });
|
|
1373
|
+
if (realtime) {
|
|
1374
|
+
for (const message of this.currentMessages) {
|
|
1375
|
+
if (!previousIds.has(message.id)) {
|
|
1376
|
+
this.emit("message", { sessionId: session.id, message, source: "realtime" });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
this.markReadIfNeeded(session.id, page.latestSequence);
|
|
1381
|
+
} catch {
|
|
1382
|
+
this.emitError("HISTORY_LOAD_FAILED", "\u5386\u53F2\u6D88\u606F\u52A0\u8F7D\u5931\u8D25\uFF0C\u5B9E\u65F6\u8FDE\u63A5\u4ECD\u4F1A\u7EE7\u7EED\u3002", true);
|
|
1383
|
+
}
|
|
1384
|
+
})();
|
|
1385
|
+
const refresh = { sessionId: session.id, promise: pending };
|
|
1386
|
+
this.historyRefresh = refresh;
|
|
1387
|
+
await pending.finally(() => {
|
|
1388
|
+
if (this.historyRefresh === refresh) this.historyRefresh = null;
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
markReadIfNeeded(sessionId, latestSequence) {
|
|
1392
|
+
const markRead = this.transport.markRead;
|
|
1393
|
+
const cursor = normalizeReadCursor(latestSequence);
|
|
1394
|
+
if (!markRead || !cursor || compareReadCursors(cursor, "0") <= 0 || this.destroyed) return;
|
|
1395
|
+
const state = this.readCursorState(sessionId);
|
|
1396
|
+
if (compareReadCursors(cursor, state.acknowledged) <= 0) return;
|
|
1397
|
+
if (state.inFlight) {
|
|
1398
|
+
if (compareReadCursors(cursor, state.inFlight) > 0) {
|
|
1399
|
+
state.pending = maxReadCursor(state.pending, cursor);
|
|
1400
|
+
}
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
this.submitReadCursor(sessionId, cursor, state, markRead);
|
|
1404
|
+
}
|
|
1405
|
+
submitReadCursor(sessionId, cursor, state, markRead) {
|
|
1406
|
+
const lifecycleGeneration = this.readLifecycleGeneration;
|
|
1407
|
+
const requestGeneration = ++state.generation;
|
|
1408
|
+
state.inFlight = cursor;
|
|
1409
|
+
let request;
|
|
1410
|
+
try {
|
|
1411
|
+
request = markRead.call(this.transport, sessionId, cursor);
|
|
1412
|
+
} catch {
|
|
1413
|
+
state.inFlight = null;
|
|
1414
|
+
state.pending = null;
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
void request.then(() => {
|
|
1418
|
+
if (!this.isCurrentReadRequest(state, lifecycleGeneration, requestGeneration, cursor))
|
|
1419
|
+
return;
|
|
1420
|
+
state.acknowledged = maxReadCursor(state.acknowledged, cursor);
|
|
1421
|
+
const next = state.pending;
|
|
1422
|
+
state.pending = null;
|
|
1423
|
+
state.inFlight = null;
|
|
1424
|
+
if (next && compareReadCursors(next, state.acknowledged) > 0) {
|
|
1425
|
+
this.submitReadCursor(sessionId, next, state, markRead);
|
|
1426
|
+
}
|
|
1427
|
+
}).catch(() => {
|
|
1428
|
+
if (!this.isCurrentReadRequest(state, lifecycleGeneration, requestGeneration, cursor))
|
|
1429
|
+
return;
|
|
1430
|
+
state.inFlight = null;
|
|
1431
|
+
state.pending = null;
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
readCursorState(sessionId) {
|
|
1435
|
+
const current = this.readCursorStates.get(sessionId);
|
|
1436
|
+
if (current) return current;
|
|
1437
|
+
const created = {
|
|
1438
|
+
acknowledged: "0",
|
|
1439
|
+
inFlight: null,
|
|
1440
|
+
pending: null,
|
|
1441
|
+
generation: 0
|
|
1442
|
+
};
|
|
1443
|
+
this.readCursorStates.set(sessionId, created);
|
|
1444
|
+
return created;
|
|
1445
|
+
}
|
|
1446
|
+
isCurrentReadRequest(state, lifecycleGeneration, requestGeneration, cursor) {
|
|
1447
|
+
return !this.destroyed && this.readLifecycleGeneration === lifecycleGeneration && state.generation === requestGeneration && state.inFlight === cursor;
|
|
1448
|
+
}
|
|
1449
|
+
openSocket(reconnecting) {
|
|
1450
|
+
if (this.destroyed || !this.shouldReconnect) return;
|
|
1451
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
1452
|
+
this.socket?.close();
|
|
1453
|
+
this.setState(reconnecting ? "reconnecting" : "connecting");
|
|
1454
|
+
let socket;
|
|
1455
|
+
try {
|
|
1456
|
+
socket = this.webSocketFactory(this.realtimeUrl);
|
|
1457
|
+
} catch {
|
|
1458
|
+
this.emitError("WEBSOCKET_UNAVAILABLE", "\u65E0\u6CD5\u5EFA\u7ACB\u5BA2\u670D\u5B9E\u65F6\u8FDE\u63A5\u3002", true);
|
|
1459
|
+
this.scheduleReconnect();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const generation = ++this.socketGeneration;
|
|
1463
|
+
this.socket = socket;
|
|
1464
|
+
socket.onopen = () => void this.authenticate(socket, generation);
|
|
1465
|
+
socket.onmessage = (event) => void this.handleSocketData(event.data, socket, generation);
|
|
1466
|
+
socket.onerror = () => {
|
|
1467
|
+
if (this.isCurrentSocket(socket, generation)) {
|
|
1468
|
+
this.emitError("WEBSOCKET_ERROR", "\u5BA2\u670D\u5B9E\u65F6\u8FDE\u63A5\u53D1\u751F\u5F02\u5E38\u3002", true);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
socket.onclose = (event) => this.handleSocketClose(socket, generation, event.code);
|
|
1472
|
+
}
|
|
1473
|
+
async authenticate(socket, generation) {
|
|
1474
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
1475
|
+
this.setState("authenticating");
|
|
1476
|
+
let accessToken = "";
|
|
1477
|
+
try {
|
|
1478
|
+
accessToken = (await this.transport.getAccessToken()).trim();
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
1482
|
+
if (!accessToken) {
|
|
1483
|
+
this.emitError("TOKEN_UNAVAILABLE", "\u65E0\u6CD5\u83B7\u53D6\u5BA2\u670D\u5B9E\u65F6\u8FDE\u63A5\u51ED\u8BC1\u3002", true);
|
|
1484
|
+
socket.close();
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
socket.send(
|
|
1488
|
+
JSON.stringify({
|
|
1489
|
+
type: "authenticate",
|
|
1490
|
+
protocol_version: 1,
|
|
1491
|
+
access_token: accessToken,
|
|
1492
|
+
requested_channels: ["customer_support"],
|
|
1493
|
+
cursors: {}
|
|
1494
|
+
})
|
|
1495
|
+
);
|
|
1496
|
+
accessToken = "";
|
|
1497
|
+
}
|
|
1498
|
+
async handleSocketData(data, socket, generation) {
|
|
1499
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
1500
|
+
let raw = "";
|
|
1501
|
+
if (typeof data === "string") raw = data;
|
|
1502
|
+
else if (data instanceof Blob) raw = await data.text();
|
|
1503
|
+
else if (data instanceof ArrayBuffer) raw = new TextDecoder().decode(data);
|
|
1504
|
+
const message = raw ? parseJsonRecord(raw) : null;
|
|
1505
|
+
if (!message) {
|
|
1506
|
+
this.emitError("PROTOCOL_ERROR", "\u6536\u5230\u65E0\u6CD5\u89E3\u6790\u7684\u5BA2\u670D\u5B9E\u65F6\u6D88\u606F\u3002", true);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
this.handleServerMessage(message, socket, generation);
|
|
1510
|
+
}
|
|
1511
|
+
handleServerMessage(message, socket, generation) {
|
|
1512
|
+
const type = normalizedText(message.type);
|
|
1513
|
+
if (type === "realtime_ready") {
|
|
1514
|
+
const channels = record2(message.channels);
|
|
1515
|
+
if (record2(channels.customer_support).available !== true) {
|
|
1516
|
+
this.shouldReconnect = false;
|
|
1517
|
+
this.emitError("CHANNEL_UNAVAILABLE", "\u4EBA\u5DE5\u5BA2\u670D\u5B9E\u65F6\u901A\u9053\u4E0D\u53EF\u7528\u3002", false);
|
|
1518
|
+
socket.close();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
this.reconnectAttempt = 0;
|
|
1522
|
+
this.tokenReconnectAttempt = 0;
|
|
1523
|
+
this.availabilityReconnectAttempt = 0;
|
|
1524
|
+
this.startHeartbeat(numberValue2(message.heartbeat_interval_seconds), socket, generation);
|
|
1525
|
+
this.setState("connected");
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (type === "realtime_event" && message.channel === "customer_support") {
|
|
1529
|
+
const event = record2(message.event);
|
|
1530
|
+
this.handleCustomerSupportEvent(event);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (type === "channel_resync_required" && message.channel === "customer_support") {
|
|
1534
|
+
void this.refreshFromRealtime();
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (type === "authentication_error") {
|
|
1538
|
+
const failure = authenticationFailure2(normalizedText(message.code));
|
|
1539
|
+
this.socketAuthenticationFailure = { generation, retryMode: failure.retryMode };
|
|
1540
|
+
this.emitError(failure.code, failure.message, failure.retryMode !== "never");
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
handleCustomerSupportEvent(event) {
|
|
1544
|
+
const eventType = normalizedText(event.event_type);
|
|
1545
|
+
const sessionId = normalizedText(event.session_id);
|
|
1546
|
+
if (!eventType || !sessionId) return;
|
|
1547
|
+
this.refreshSessionsFromRealtime();
|
|
1548
|
+
if (sessionId !== this.currentSession?.id) return;
|
|
1549
|
+
if (eventType === "support_read_state_changed") return;
|
|
1550
|
+
if (eventType === "support_message_created") {
|
|
1551
|
+
void this.refreshHistory(true);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
void this.refreshSessionFromRealtime();
|
|
1555
|
+
}
|
|
1556
|
+
async refreshFromRealtime() {
|
|
1557
|
+
await this.refreshSessions().catch(() => [...this.currentSessions]);
|
|
1558
|
+
await this.refreshSessionFromRealtime();
|
|
1559
|
+
await this.refreshHistory(true);
|
|
1560
|
+
}
|
|
1561
|
+
async refreshSessions() {
|
|
1562
|
+
if (this.sessionsRefresh) return this.sessionsRefresh.promise;
|
|
1563
|
+
const pending = (async () => {
|
|
1564
|
+
const sessions = await this.transport.listSessions();
|
|
1565
|
+
if (!this.destroyed) this.replaceSessions(sessions);
|
|
1566
|
+
return [...this.currentSessions];
|
|
1567
|
+
})();
|
|
1568
|
+
const refresh = { promise: pending };
|
|
1569
|
+
this.sessionsRefresh = refresh;
|
|
1570
|
+
return pending.finally(() => {
|
|
1571
|
+
if (this.sessionsRefresh === refresh) this.sessionsRefresh = null;
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
refreshSessionsFromRealtime() {
|
|
1575
|
+
void this.refreshSessions().catch(() => {
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
replaceSessions(sessions) {
|
|
1579
|
+
this.currentSessions = [...sessions];
|
|
1580
|
+
this.emit("sessions", { sessions: [...this.currentSessions] });
|
|
1581
|
+
}
|
|
1582
|
+
upsertSession(session) {
|
|
1583
|
+
const currentIndex = this.currentSessions.findIndex((candidate) => candidate.id === session.id);
|
|
1584
|
+
if (currentIndex < 0) this.currentSessions = [session, ...this.currentSessions];
|
|
1585
|
+
else {
|
|
1586
|
+
this.currentSessions = this.currentSessions.map(
|
|
1587
|
+
(candidate, index) => index === currentIndex ? session : candidate
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
this.emit("sessions", { sessions: [...this.currentSessions] });
|
|
1591
|
+
}
|
|
1592
|
+
async refreshSessionFromRealtime() {
|
|
1593
|
+
const sessionId = this.currentSession?.id;
|
|
1594
|
+
const getSession = this.transport.getSession;
|
|
1595
|
+
if (!sessionId || !getSession) return;
|
|
1596
|
+
try {
|
|
1597
|
+
const session = await getSession.call(this.transport, sessionId);
|
|
1598
|
+
if (!this.destroyed && this.currentSession?.id === sessionId) {
|
|
1599
|
+
this.currentSession = session;
|
|
1600
|
+
this.emit("session", { session });
|
|
1601
|
+
}
|
|
1602
|
+
} catch {
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
appendMessage(message, source) {
|
|
1606
|
+
if (this.currentMessages.some((candidate) => candidate.id === message.id)) return;
|
|
1607
|
+
this.currentMessages = [...this.currentMessages, message].sort(compareMessages);
|
|
1608
|
+
this.emit("message", { sessionId: message.sessionId, message, source });
|
|
1609
|
+
}
|
|
1610
|
+
startHeartbeat(intervalSeconds, socket, generation) {
|
|
1611
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1612
|
+
const requested = intervalSeconds > 0 ? intervalSeconds * 1e3 : 25e3;
|
|
1613
|
+
this.heartbeatTimer = setInterval(
|
|
1614
|
+
() => {
|
|
1615
|
+
if (this.isCurrentSocket(socket, generation) && socket.readyState === 1) {
|
|
1616
|
+
socket.send(JSON.stringify(createPingFrame()));
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
Math.max(this.minimumHeartbeatMs, requested)
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
handleSocketClose(socket, generation, code) {
|
|
1623
|
+
if (!this.isCurrentSocket(socket, generation)) return;
|
|
1624
|
+
this.socket = null;
|
|
1625
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1626
|
+
this.heartbeatTimer = null;
|
|
1627
|
+
const authenticationFailure3 = this.socketAuthenticationFailure?.generation === generation ? this.socketAuthenticationFailure : null;
|
|
1628
|
+
this.socketAuthenticationFailure = null;
|
|
1629
|
+
if (!this.shouldReconnect || this.destroyed) {
|
|
1630
|
+
if (!this.destroyed) this.setState("disconnected");
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (authenticationFailure3?.retryMode === "never") {
|
|
1634
|
+
this.shouldReconnect = false;
|
|
1635
|
+
this.setState("disconnected");
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
if (code === 4400 || code === 4403) {
|
|
1639
|
+
this.shouldReconnect = false;
|
|
1640
|
+
if (!authenticationFailure3) {
|
|
1641
|
+
this.emitError(
|
|
1642
|
+
code === 4400 ? "PROTOCOL_ERROR" : "AUTHENTICATION_FAILED",
|
|
1643
|
+
code === 4400 ? "\u5BA2\u670D\u5B9E\u65F6\u8FDE\u63A5\u534F\u8BAE\u4E0D\u53D7\u652F\u6301\u3002" : "\u5F53\u524D\u8EAB\u4EFD\u65E0\u6743\u4F7F\u7528\u4EBA\u5DE5\u5BA2\u670D\u3002",
|
|
1644
|
+
false
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
this.setState("disconnected");
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
if (authenticationFailure3?.retryMode === "availability" || code === 1013) {
|
|
1651
|
+
if (!authenticationFailure3) {
|
|
1652
|
+
this.emitError("REALTIME_UNAVAILABLE", "\u5BA2\u670D\u5B9E\u65F6\u9274\u6743\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u6B63\u5728\u91CD\u8BD5\u3002", true);
|
|
1653
|
+
}
|
|
1654
|
+
this.scheduleAvailabilityReconnect();
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (authenticationFailure3?.retryMode === "token" || code === 4401) {
|
|
1658
|
+
if (!authenticationFailure3) {
|
|
1659
|
+
this.emitError("AUTHENTICATION_FAILED", "\u5BA2\u670D\u8FDE\u63A5\u51ED\u8BC1\u5DF2\u5931\u6548\uFF0C\u6B63\u5728\u91CD\u65B0\u8FDE\u63A5\u3002", true);
|
|
1660
|
+
}
|
|
1661
|
+
if (this.tokenReconnectAttempt >= MAX_TOKEN_RECONNECTS) {
|
|
1662
|
+
this.shouldReconnect = false;
|
|
1663
|
+
this.emitError("AUTHENTICATION_FAILED", "\u4EBA\u5DE5\u5BA2\u670D\u8EAB\u4EFD\u9A8C\u8BC1\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u8BD5\u3002", false);
|
|
1664
|
+
this.setState("disconnected");
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
this.tokenReconnectAttempt += 1;
|
|
1668
|
+
}
|
|
1669
|
+
this.scheduleReconnect();
|
|
1670
|
+
}
|
|
1671
|
+
scheduleAvailabilityReconnect() {
|
|
1672
|
+
if (this.availabilityReconnectAttempt >= MAX_AVAILABILITY_RECONNECTS2) {
|
|
1673
|
+
this.shouldReconnect = false;
|
|
1674
|
+
this.emitError("REALTIME_UNAVAILABLE", "\u5BA2\u670D\u5B9E\u65F6\u9274\u6743\u670D\u52A1\u6301\u7EED\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", false);
|
|
1675
|
+
this.setState("disconnected");
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
this.availabilityReconnectAttempt += 1;
|
|
1679
|
+
this.scheduleReconnect();
|
|
1680
|
+
}
|
|
1681
|
+
scheduleReconnect() {
|
|
1682
|
+
if (!this.shouldReconnect || this.destroyed || this.reconnectTimer) return;
|
|
1683
|
+
const base = Math.min(
|
|
1684
|
+
this.reconnectOptions.maximumDelayMs,
|
|
1685
|
+
this.reconnectOptions.initialDelayMs * this.reconnectOptions.factor ** this.reconnectAttempt++
|
|
1686
|
+
);
|
|
1687
|
+
const jitter = base * this.reconnectOptions.jitter * (Math.random() * 2 - 1);
|
|
1688
|
+
this.reconnectTimer = setTimeout(
|
|
1689
|
+
() => {
|
|
1690
|
+
this.reconnectTimer = null;
|
|
1691
|
+
this.openSocket(true);
|
|
1692
|
+
},
|
|
1693
|
+
Math.max(0, base + jitter)
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
clearTimers() {
|
|
1697
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1698
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
1699
|
+
this.heartbeatTimer = null;
|
|
1700
|
+
this.reconnectTimer = null;
|
|
1701
|
+
}
|
|
1702
|
+
isCurrentSocket(socket, generation) {
|
|
1703
|
+
return !this.destroyed && this.socket === socket && this.socketGeneration === generation;
|
|
1704
|
+
}
|
|
1705
|
+
setState(current) {
|
|
1706
|
+
if (this.currentState === current) return;
|
|
1707
|
+
const previous = this.currentState;
|
|
1708
|
+
this.currentState = current;
|
|
1709
|
+
this.emit("connection-state-change", { previous, current });
|
|
1710
|
+
}
|
|
1711
|
+
emitError(code, message, recoverable) {
|
|
1712
|
+
const detail = { code, message, recoverable };
|
|
1713
|
+
this.emit("error", detail);
|
|
1714
|
+
return detail;
|
|
1715
|
+
}
|
|
1716
|
+
emit(type, detail) {
|
|
1717
|
+
for (const listener of this.listeners.get(type) ?? []) listener(detail);
|
|
1718
|
+
}
|
|
1719
|
+
assertUsable() {
|
|
1720
|
+
if (this.destroyed) {
|
|
1721
|
+
throw new CustomerSupportClientError("CLIENT_DESTROYED", "\u5BA2\u670D\u5BA2\u6237\u7AEF\u5DF2\u7ECF\u9500\u6BC1\u3002");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
function validateRealtimeUrl2(value) {
|
|
1726
|
+
let url;
|
|
1727
|
+
try {
|
|
1728
|
+
url = new URL(value);
|
|
1729
|
+
} catch {
|
|
1730
|
+
throw new CustomerSupportClientError("INVALID_CONFIGURATION", "\u5BA2\u670D\u5B9E\u65F6\u5730\u5740\u65E0\u6548\u3002");
|
|
1731
|
+
}
|
|
1732
|
+
if (!["ws:", "wss:"].includes(url.protocol) || url.search || url.hash || url.username) {
|
|
1733
|
+
throw new CustomerSupportClientError("INVALID_CONFIGURATION", "\u5BA2\u670D\u5B9E\u65F6\u5730\u5740\u4E0D\u5B89\u5168\u3002");
|
|
1734
|
+
}
|
|
1735
|
+
return url.href;
|
|
1736
|
+
}
|
|
1737
|
+
function normalizeReconnect(value) {
|
|
1738
|
+
return {
|
|
1739
|
+
initialDelayMs: Math.max(0, value?.initialDelayMs ?? DEFAULT_RECONNECT2.initialDelayMs),
|
|
1740
|
+
maximumDelayMs: Math.max(1, value?.maximumDelayMs ?? DEFAULT_RECONNECT2.maximumDelayMs),
|
|
1741
|
+
factor: Math.max(1, value?.factor ?? DEFAULT_RECONNECT2.factor),
|
|
1742
|
+
jitter: Math.min(1, Math.max(0, value?.jitter ?? DEFAULT_RECONNECT2.jitter))
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
function compareMessages(left, right) {
|
|
1746
|
+
const leftSequence = Number(left.sequence);
|
|
1747
|
+
const rightSequence = Number(right.sequence);
|
|
1748
|
+
if (Number.isFinite(leftSequence) && Number.isFinite(rightSequence)) {
|
|
1749
|
+
return leftSequence - rightSequence;
|
|
1750
|
+
}
|
|
1751
|
+
return left.createdAt.localeCompare(right.createdAt);
|
|
1752
|
+
}
|
|
1753
|
+
function normalizeReadCursor(value) {
|
|
1754
|
+
const cursor = normalizedText(value);
|
|
1755
|
+
if (!/^\d+$/.test(cursor)) return null;
|
|
1756
|
+
return cursor.replace(/^0+(?=\d)/, "");
|
|
1757
|
+
}
|
|
1758
|
+
function compareReadCursors(left, right) {
|
|
1759
|
+
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
1760
|
+
if (left === right) return 0;
|
|
1761
|
+
return left < right ? -1 : 1;
|
|
1762
|
+
}
|
|
1763
|
+
function maxReadCursor(left, right) {
|
|
1764
|
+
return left && compareReadCursors(left, right) >= 0 ? left : right;
|
|
1765
|
+
}
|
|
1766
|
+
function hasCustomerSupportMessages(session) {
|
|
1767
|
+
return session.lastMessage.trim().length > 0;
|
|
1768
|
+
}
|
|
1769
|
+
function requiredText2(value, field) {
|
|
1770
|
+
const normalized = normalizedText(value);
|
|
1771
|
+
if (!normalized) {
|
|
1772
|
+
throw new CustomerSupportClientError("INVALID_CONFIGURATION", `${field} \u4E0D\u80FD\u4E3A\u7A7A\u3002`);
|
|
1773
|
+
}
|
|
1774
|
+
return normalized;
|
|
1775
|
+
}
|
|
1776
|
+
function requiredRequestId2(value) {
|
|
1777
|
+
return requiredText2(value, "requestId");
|
|
1778
|
+
}
|
|
1779
|
+
function normalizedText(value) {
|
|
1780
|
+
return typeof value === "string" ? value.trim() : "";
|
|
1781
|
+
}
|
|
1782
|
+
function record2(value) {
|
|
1783
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1784
|
+
}
|
|
1785
|
+
function numberValue2(value) {
|
|
1786
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1787
|
+
}
|
|
1788
|
+
function createRequestId2() {
|
|
1789
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `request-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1790
|
+
}
|
|
1791
|
+
function authenticationFailure2(code) {
|
|
1792
|
+
if (code === "UNAUTHORIZED") {
|
|
1793
|
+
return {
|
|
1794
|
+
code: "AUTHENTICATION_FAILED",
|
|
1795
|
+
message: "\u5BA2\u670D\u8FDE\u63A5\u51ED\u8BC1\u5DF2\u5931\u6548\uFF0C\u6B63\u5728\u91CD\u65B0\u8FDE\u63A5\u3002",
|
|
1796
|
+
retryMode: "token"
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
if (code === "ORGANIZATION_CONTEXT_REQUIRED") {
|
|
1800
|
+
return {
|
|
1801
|
+
code: "ORGANIZATION_CONTEXT_REQUIRED",
|
|
1802
|
+
message: "\u8BF7\u9009\u62E9\u4F01\u4E1A/\u56E2\u961F\u540E\u518D\u8054\u7CFB\u4EBA\u5DE5\u5BA2\u670D\u3002",
|
|
1803
|
+
retryMode: "never"
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (code === "INVALID_ARGUMENT") {
|
|
1807
|
+
return {
|
|
1808
|
+
code: "PROTOCOL_ERROR",
|
|
1809
|
+
message: "\u5BA2\u670D\u5B9E\u65F6\u8FDE\u63A5\u534F\u8BAE\u4E0D\u53D7\u652F\u6301\u3002",
|
|
1810
|
+
retryMode: "never"
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
if (code === "REALTIME_UNAVAILABLE" || code === "KANBAN_AUTH_FAILED" || code.startsWith("SERVICE_")) {
|
|
1814
|
+
return {
|
|
1815
|
+
code: "REALTIME_UNAVAILABLE",
|
|
1816
|
+
message: "\u5BA2\u670D\u5B9E\u65F6\u9274\u6743\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u6B63\u5728\u91CD\u8BD5\u3002",
|
|
1817
|
+
retryMode: "availability"
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
if (code === "ACCESS_DENIED" || code === "ORIGIN_NOT_ALLOWED" || code === "CHANNEL_ACCESS_DENIED") {
|
|
1821
|
+
return {
|
|
1822
|
+
code: "AUTHENTICATION_FAILED",
|
|
1823
|
+
message: "\u5F53\u524D\u8EAB\u4EFD\u65E0\u6743\u4F7F\u7528\u4EBA\u5DE5\u5BA2\u670D\u3002",
|
|
1824
|
+
retryMode: "never"
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
return {
|
|
1828
|
+
code: "AUTHENTICATION_FAILED",
|
|
1829
|
+
message: "\u4EBA\u5DE5\u5BA2\u670D\u5B9E\u65F6\u8FDE\u63A5\u8BA4\u8BC1\u5931\u8D25\u3002",
|
|
1830
|
+
retryMode: "never"
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/notification-center-client.ts
|
|
1835
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
1836
|
+
var NotificationCenterClientError = class extends Error {
|
|
1837
|
+
code;
|
|
1838
|
+
constructor(code, message) {
|
|
1839
|
+
super(message);
|
|
1840
|
+
this.name = "NotificationCenterClientError";
|
|
1841
|
+
this.code = code;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
var NotificationCenterClient = class {
|
|
1845
|
+
transport;
|
|
1846
|
+
applicationName;
|
|
1847
|
+
pageSize;
|
|
1848
|
+
readStatus;
|
|
1849
|
+
preferredTypeOrder;
|
|
1850
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1851
|
+
items = [];
|
|
1852
|
+
hasMore = false;
|
|
1853
|
+
nextId = null;
|
|
1854
|
+
unreadCount = 0;
|
|
1855
|
+
notificationTypes = [];
|
|
1856
|
+
availableTypes = [];
|
|
1857
|
+
typeCatalogFromBackend = false;
|
|
1858
|
+
inboxGeneration = 0;
|
|
1859
|
+
destroyed = false;
|
|
1860
|
+
loadingInbox = false;
|
|
1861
|
+
pendingResync = false;
|
|
1862
|
+
constructor(options) {
|
|
1863
|
+
if (!options?.transport) {
|
|
1864
|
+
throw new NotificationCenterClientError(
|
|
1865
|
+
"INVALID_CONFIGURATION",
|
|
1866
|
+
"\u7F3A\u5C11\u901A\u77E5\u4E2D\u5FC3 transport \u914D\u7F6E\u3002"
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
this.transport = options.transport;
|
|
1870
|
+
this.applicationName = normalizedText2(options.applicationName);
|
|
1871
|
+
this.pageSize = Math.max(1, Math.floor(options.pageSize ?? DEFAULT_PAGE_SIZE));
|
|
1872
|
+
this.readStatus = ["all", "unread", "read"].includes(options.readStatus ?? "") ? options.readStatus : "all";
|
|
1873
|
+
this.preferredTypeOrder = (options.preferredTypeOrder ?? []).map((value) => normalizedText2(value)).filter(Boolean);
|
|
1874
|
+
}
|
|
1875
|
+
get notifications() {
|
|
1876
|
+
return [...this.items];
|
|
1877
|
+
}
|
|
1878
|
+
get unread() {
|
|
1879
|
+
return this.unreadCount;
|
|
1880
|
+
}
|
|
1881
|
+
/** 数据中实际存在的通知分类,供 UI 渲染页签;不是写死的枚举。 */
|
|
1882
|
+
get types() {
|
|
1883
|
+
return this.availableTypes.map((entry) => ({ ...entry }));
|
|
1884
|
+
}
|
|
1885
|
+
on(type, listener) {
|
|
1886
|
+
this.assertUsable();
|
|
1887
|
+
const listeners = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
|
|
1888
|
+
const untyped = listener;
|
|
1889
|
+
listeners.add(untyped);
|
|
1890
|
+
this.listeners.set(type, listeners);
|
|
1891
|
+
return () => listeners.delete(untyped);
|
|
1892
|
+
}
|
|
1893
|
+
async start() {
|
|
1894
|
+
this.assertUsable();
|
|
1895
|
+
await Promise.all([this.reloadInbox(), this.refreshUnreadCount()]);
|
|
1896
|
+
this.emit("ready", {});
|
|
1897
|
+
}
|
|
1898
|
+
async setTypeFilter(types) {
|
|
1899
|
+
this.assertUsable();
|
|
1900
|
+
this.notificationTypes = [...types].map((value) => normalizedText2(value)).filter(Boolean);
|
|
1901
|
+
await this.reloadInbox();
|
|
1902
|
+
}
|
|
1903
|
+
getTypeFilter() {
|
|
1904
|
+
return [...this.notificationTypes];
|
|
1905
|
+
}
|
|
1906
|
+
async loadMore() {
|
|
1907
|
+
this.assertUsable();
|
|
1908
|
+
if (!this.hasMore || this.loadingInbox || !this.nextId) return;
|
|
1909
|
+
const generation = this.inboxGeneration;
|
|
1910
|
+
this.loadingInbox = true;
|
|
1911
|
+
try {
|
|
1912
|
+
const result = await this.transport.listInbox({
|
|
1913
|
+
pageSize: this.pageSize,
|
|
1914
|
+
beforeId: this.nextId,
|
|
1915
|
+
notificationTypes: this.notificationTypes,
|
|
1916
|
+
readStatus: this.readStatus,
|
|
1917
|
+
applicationName: this.applicationName
|
|
1918
|
+
});
|
|
1919
|
+
if (this.destroyed || generation !== this.inboxGeneration) return;
|
|
1920
|
+
const existing = new Set(this.items.map((item) => item.id));
|
|
1921
|
+
const merged = [...this.items];
|
|
1922
|
+
for (const item of result.items) {
|
|
1923
|
+
if (!existing.has(item.id)) {
|
|
1924
|
+
existing.add(item.id);
|
|
1925
|
+
merged.push(item);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
this.items = merged;
|
|
1929
|
+
this.hasMore = result.hasMore;
|
|
1930
|
+
this.nextId = result.nextId;
|
|
1931
|
+
this.rememberTypes(result.items);
|
|
1932
|
+
this.emitInbox();
|
|
1933
|
+
} catch {
|
|
1934
|
+
this.emitError("INBOX_LOAD_FAILED", "\u901A\u77E5\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
1935
|
+
} finally {
|
|
1936
|
+
if (generation === this.inboxGeneration) this.finishInboxLoad();
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async refresh() {
|
|
1940
|
+
this.assertUsable();
|
|
1941
|
+
await Promise.all([this.reloadInbox(), this.refreshUnreadCount()]);
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Consume a notification realtime event captured by the host socket.
|
|
1945
|
+
*
|
|
1946
|
+
* The raw event is mapped through the transport so the DTO contract stays in
|
|
1947
|
+
* this SDK; anything that cannot be mapped degrades to a full refresh. Events
|
|
1948
|
+
* that arrive while an inbox request is in flight are replaced by a resync so a
|
|
1949
|
+
* late server page cannot drop them.
|
|
1950
|
+
*/
|
|
1951
|
+
applyRealtimeEvent(event) {
|
|
1952
|
+
if (this.destroyed) return;
|
|
1953
|
+
if (this.loadingInbox) {
|
|
1954
|
+
this.pendingResync = true;
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const parse = this.transport.parseRealtimeEvent;
|
|
1958
|
+
let change = null;
|
|
1959
|
+
if (parse) {
|
|
1960
|
+
try {
|
|
1961
|
+
change = parse.call(this.transport, event);
|
|
1962
|
+
} catch {
|
|
1963
|
+
change = null;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (!change) {
|
|
1967
|
+
void this.refreshFromRealtime();
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
this.applyRealtimeChange(change);
|
|
1971
|
+
}
|
|
1972
|
+
async markRead(notificationId) {
|
|
1973
|
+
this.assertUsable();
|
|
1974
|
+
const id = requiredText3(notificationId, "notificationId");
|
|
1975
|
+
const target = this.items.find((item) => item.id === id);
|
|
1976
|
+
if (target && target.readStatus === "read") return;
|
|
1977
|
+
try {
|
|
1978
|
+
const result = await this.transport.markRead({
|
|
1979
|
+
notificationId: id,
|
|
1980
|
+
applicationName: this.applicationName
|
|
1981
|
+
});
|
|
1982
|
+
if (this.destroyed) return;
|
|
1983
|
+
this.applyRead([id], result.readAt);
|
|
1984
|
+
this.setUnreadCount(result.unreadCount);
|
|
1985
|
+
this.emit("notification-read", { notificationId: id });
|
|
1986
|
+
} catch {
|
|
1987
|
+
this.emitError("MARK_READ_FAILED", "\u901A\u77E5\u6807\u8BB0\u5DF2\u8BFB\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
async markAllRead() {
|
|
1991
|
+
this.assertUsable();
|
|
1992
|
+
try {
|
|
1993
|
+
const result = await this.transport.markAllRead({
|
|
1994
|
+
applicationName: this.applicationName
|
|
1995
|
+
});
|
|
1996
|
+
if (this.destroyed) return;
|
|
1997
|
+
const readAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1998
|
+
this.items = this.items.map(
|
|
1999
|
+
(item) => item.readStatus === "read" ? item : { ...item, readStatus: "read", readAt }
|
|
2000
|
+
);
|
|
2001
|
+
this.emitInbox();
|
|
2002
|
+
this.setUnreadCount(result.unreadCount);
|
|
2003
|
+
} catch {
|
|
2004
|
+
this.emitError("MARK_READ_FAILED", "\u901A\u77E5\u6807\u8BB0\u5DF2\u8BFB\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
async downloadAttachment(fileId, name) {
|
|
2008
|
+
this.assertUsable();
|
|
2009
|
+
const id = requiredText3(fileId, "fileId");
|
|
2010
|
+
try {
|
|
2011
|
+
return await this.transport.downloadAttachment({ fileId: id, name });
|
|
2012
|
+
} catch {
|
|
2013
|
+
const detail = this.emitError("DOWNLOAD_FAILED", "\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
2014
|
+
throw new NotificationCenterClientError(detail.code, detail.message);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
destroy() {
|
|
2018
|
+
if (this.destroyed) return;
|
|
2019
|
+
this.destroyed = true;
|
|
2020
|
+
this.pendingResync = false;
|
|
2021
|
+
this.inboxGeneration += 1;
|
|
2022
|
+
this.listeners.clear();
|
|
2023
|
+
this.items = [];
|
|
2024
|
+
}
|
|
2025
|
+
async reloadInbox() {
|
|
2026
|
+
const generation = ++this.inboxGeneration;
|
|
2027
|
+
this.loadingInbox = true;
|
|
2028
|
+
try {
|
|
2029
|
+
const result = await this.transport.listInbox({
|
|
2030
|
+
pageSize: this.pageSize,
|
|
2031
|
+
beforeId: null,
|
|
2032
|
+
// 分类目录随首屏/刷新/切换页签的请求一并取回,不额外发请求。
|
|
2033
|
+
includeTypeFacets: true,
|
|
2034
|
+
notificationTypes: this.notificationTypes,
|
|
2035
|
+
readStatus: this.readStatus,
|
|
2036
|
+
applicationName: this.applicationName
|
|
2037
|
+
});
|
|
2038
|
+
if (this.destroyed || generation !== this.inboxGeneration) return;
|
|
2039
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2040
|
+
for (const item of result.items) {
|
|
2041
|
+
if (item.id) unique.set(item.id, item);
|
|
2042
|
+
}
|
|
2043
|
+
this.items = [...unique.values()];
|
|
2044
|
+
this.hasMore = result.hasMore;
|
|
2045
|
+
this.nextId = result.nextId;
|
|
2046
|
+
this.applyTypeCatalog(result.types);
|
|
2047
|
+
this.rememberTypes(result.items);
|
|
2048
|
+
this.emitInbox();
|
|
2049
|
+
} catch {
|
|
2050
|
+
if (generation === this.inboxGeneration) {
|
|
2051
|
+
this.emitError("INBOX_LOAD_FAILED", "\u901A\u77E5\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", true);
|
|
2052
|
+
}
|
|
2053
|
+
} finally {
|
|
2054
|
+
if (generation === this.inboxGeneration) this.finishInboxLoad();
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
finishInboxLoad() {
|
|
2058
|
+
this.loadingInbox = false;
|
|
2059
|
+
if (!this.pendingResync || this.destroyed) return;
|
|
2060
|
+
this.pendingResync = false;
|
|
2061
|
+
void this.refreshFromRealtime();
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* 分类页签的数据来源。
|
|
2065
|
+
*
|
|
2066
|
+
* `notification_type` 是后端自由文本列,展示名来自后端字典(`notification_type_name`),
|
|
2067
|
+
* 前端不维护 code 到中文名的映射。权威目录是 `inbox` 在 `includeTypeFacets` 下返回的
|
|
2068
|
+
* `types`(后端按收件人可见范围统计,忽略类型筛选与分页);后端未提供时退回累积已加载
|
|
2069
|
+
* 页面里出现过的分类——这在分页深处才出现的分类上会暂时缺页签,属已知取舍。
|
|
2070
|
+
*/
|
|
2071
|
+
applyTypeCatalog(types) {
|
|
2072
|
+
if (!Array.isArray(types)) return;
|
|
2073
|
+
const normalized = types.map((entry) => ({
|
|
2074
|
+
type: normalizedText2(entry?.type),
|
|
2075
|
+
name: normalizedText2(entry?.name)
|
|
2076
|
+
})).filter((entry) => entry.type);
|
|
2077
|
+
this.typeCatalogFromBackend = true;
|
|
2078
|
+
this.setAvailableTypes(normalized, true);
|
|
2079
|
+
}
|
|
2080
|
+
rememberTypes(items) {
|
|
2081
|
+
const discovered = items.map((item) => ({ type: normalizedText2(item.type), name: normalizedText2(item.typeName) })).filter((entry) => entry.type);
|
|
2082
|
+
if (!discovered.length) return;
|
|
2083
|
+
this.setAvailableTypes(discovered, false, this.typeCatalogFromBackend);
|
|
2084
|
+
}
|
|
2085
|
+
setAvailableTypes(candidates, replace, namesOnly = false) {
|
|
2086
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2087
|
+
if (!replace) {
|
|
2088
|
+
for (const entry of this.availableTypes) merged.set(entry.type, entry.name);
|
|
2089
|
+
}
|
|
2090
|
+
for (const entry of candidates) {
|
|
2091
|
+
if (namesOnly && !merged.has(entry.type)) continue;
|
|
2092
|
+
const existing = merged.get(entry.type);
|
|
2093
|
+
merged.set(entry.type, existing || entry.name);
|
|
2094
|
+
}
|
|
2095
|
+
const codes = [...merged.keys()];
|
|
2096
|
+
const preferred = this.preferredTypeOrder.filter((value) => merged.has(value));
|
|
2097
|
+
const ordered = [...preferred, ...codes.filter((value) => !preferred.includes(value))].map(
|
|
2098
|
+
(type) => ({ type, name: merged.get(type) ?? "" })
|
|
2099
|
+
);
|
|
2100
|
+
if (ordered.length === this.availableTypes.length && ordered.every(
|
|
2101
|
+
(entry, index) => entry.type === this.availableTypes[index]?.type && entry.name === this.availableTypes[index]?.name
|
|
2102
|
+
)) {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
this.availableTypes = ordered;
|
|
2106
|
+
this.emit("types-change", { types: this.types });
|
|
2107
|
+
}
|
|
2108
|
+
async refreshUnreadCount() {
|
|
2109
|
+
try {
|
|
2110
|
+
const result = await this.transport.getUnreadCount({ applicationName: this.applicationName });
|
|
2111
|
+
if (this.destroyed) return;
|
|
2112
|
+
this.setUnreadCount(result.unreadCount);
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
applyRealtimeChange(change) {
|
|
2117
|
+
if (change.kind === "created") {
|
|
2118
|
+
const incoming = change.notification;
|
|
2119
|
+
if (!incoming.id) return;
|
|
2120
|
+
if (!this.items.some((item) => item.id === incoming.id)) {
|
|
2121
|
+
this.items = [incoming, ...this.items];
|
|
2122
|
+
this.rememberTypes([incoming]);
|
|
2123
|
+
this.emitInbox();
|
|
2124
|
+
}
|
|
2125
|
+
this.syncRealtimeUnreadCount(change.unreadCount);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (change.kind === "read") {
|
|
2129
|
+
this.applyRead(change.notificationIds, change.readAt ?? null);
|
|
2130
|
+
this.syncRealtimeUnreadCount(change.unreadCount);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const removed = new Set(change.notificationIds);
|
|
2134
|
+
if (removed.size) {
|
|
2135
|
+
const next = this.items.filter((item) => !removed.has(item.id));
|
|
2136
|
+
if (next.length !== this.items.length) {
|
|
2137
|
+
this.items = next;
|
|
2138
|
+
this.emitInbox();
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
this.syncRealtimeUnreadCount(change.unreadCount);
|
|
2142
|
+
}
|
|
2143
|
+
syncRealtimeUnreadCount(value) {
|
|
2144
|
+
if (this.applicationName && typeof value === "number") this.setUnreadCount(value);
|
|
2145
|
+
else void this.refreshUnreadCount();
|
|
2146
|
+
}
|
|
2147
|
+
applyRead(ids, readAt) {
|
|
2148
|
+
const target = new Set(ids);
|
|
2149
|
+
if (!target.size) return;
|
|
2150
|
+
let changed = false;
|
|
2151
|
+
const next = this.items.map((item) => {
|
|
2152
|
+
if (target.has(item.id) && item.readStatus !== "read") {
|
|
2153
|
+
changed = true;
|
|
2154
|
+
return { ...item, readStatus: "read", readAt: readAt ?? item.readAt };
|
|
2155
|
+
}
|
|
2156
|
+
return item;
|
|
2157
|
+
});
|
|
2158
|
+
if (changed) {
|
|
2159
|
+
this.items = next;
|
|
2160
|
+
this.emitInbox();
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
async refreshFromRealtime() {
|
|
2164
|
+
await this.reloadInbox();
|
|
2165
|
+
await this.refreshUnreadCount();
|
|
2166
|
+
}
|
|
2167
|
+
emitInbox() {
|
|
2168
|
+
this.emit("inbox", {
|
|
2169
|
+
items: [...this.items],
|
|
2170
|
+
hasMore: this.hasMore,
|
|
2171
|
+
nextId: this.nextId
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
setUnreadCount(value) {
|
|
2175
|
+
const normalized = Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
2176
|
+
if (normalized === this.unreadCount) return;
|
|
2177
|
+
this.unreadCount = normalized;
|
|
2178
|
+
this.emit("unread-count-change", { unreadCount: normalized });
|
|
2179
|
+
}
|
|
2180
|
+
emitError(code, message, recoverable) {
|
|
2181
|
+
const detail = { code, message, recoverable };
|
|
2182
|
+
this.emit("error", detail);
|
|
2183
|
+
return detail;
|
|
2184
|
+
}
|
|
2185
|
+
emit(type, detail) {
|
|
2186
|
+
for (const listener of this.listeners.get(type) ?? []) listener(detail);
|
|
2187
|
+
}
|
|
2188
|
+
assertUsable() {
|
|
2189
|
+
if (this.destroyed) {
|
|
2190
|
+
throw new NotificationCenterClientError("CLIENT_DESTROYED", "\u901A\u77E5\u4E2D\u5FC3\u5BA2\u6237\u7AEF\u5DF2\u7ECF\u9500\u6BC1\u3002");
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
function requiredText3(value, field) {
|
|
2195
|
+
const normalized = normalizedText2(value);
|
|
2196
|
+
if (!normalized) {
|
|
2197
|
+
throw new NotificationCenterClientError("INVALID_CONFIGURATION", `${field} \u4E0D\u80FD\u4E3A\u7A7A\u3002`);
|
|
2198
|
+
}
|
|
2199
|
+
return normalized;
|
|
2200
|
+
}
|
|
2201
|
+
function normalizedText2(value) {
|
|
2202
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
export { AiEmployeeClient, AiEmployeeClientError, AiEmployeeTransportError, CustomerSupportClient, CustomerSupportClientError, NotificationCenterClient, NotificationCenterClientError, createAuthenticateFrame, createPingFrame, createSubscriptionFrame, hashText, isUuidV4, parseAiEmployeeAssistantContent, parseAiEmployeeEvent, parseJsonRecord };
|
|
2206
|
+
//# sourceMappingURL=index.js.map
|
|
2207
|
+
//# sourceMappingURL=index.js.map
|