@appthen/sdk-core 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +616 -0
- package/dist/index.js +789 -0
- package/dist-cjs/index.d.ts +616 -0
- package/dist-cjs/index.js +802 -0
- package/dist-cjs/package.json +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @appthen/sdk-core
|
|
4
|
+
*
|
|
5
|
+
* 平台无关的 Conversation Hub SDK 核心。
|
|
6
|
+
*
|
|
7
|
+
* V0 先对齐目前已经落地的 Hub API:
|
|
8
|
+
* - open-platform/token
|
|
9
|
+
* - subject-directory/open/*
|
|
10
|
+
* - conversation-runtime/open/*
|
|
11
|
+
* - 内部 JWT 保护的查询接口(messages / conversations)
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.PlatformApi = exports.ChatHubAppRuntime = exports.ChatHubClient = exports.MessagesApi = exports.ConversationsApi = exports.ConversationTypesApi = exports.SubjectDirectoryApi = exports.OpenPlatformApi = exports.SDK_VERSION = void 0;
|
|
15
|
+
exports.resolveChatHubMessagePolicy = resolveChatHubMessagePolicy;
|
|
16
|
+
exports.createChatHubAppRuntime = createChatHubAppRuntime;
|
|
17
|
+
exports.SDK_VERSION = '0.1.0';
|
|
18
|
+
function resolveChatHubMessagePolicy(value) {
|
|
19
|
+
const policy = value && typeof value === 'object' ? value : {};
|
|
20
|
+
return {
|
|
21
|
+
allowRichText: policy.allowRichText !== false,
|
|
22
|
+
allowCopy: policy.allowCopy !== false,
|
|
23
|
+
allowReply: policy.allowReply !== false,
|
|
24
|
+
allowForward: policy.allowForward !== false,
|
|
25
|
+
allowMultiForwardSelection: policy.allowMultiForwardSelection !== false,
|
|
26
|
+
allowDelete: policy.allowDelete === true,
|
|
27
|
+
allowRevoke: policy.allowRevoke === true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function withBearerToken(headers, token) {
|
|
31
|
+
if (!token) {
|
|
32
|
+
return headers || {};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...(headers || {}),
|
|
36
|
+
Authorization: `Bearer ${token}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
class BaseApi {
|
|
40
|
+
constructor(http, auth) {
|
|
41
|
+
this.http = http;
|
|
42
|
+
this.auth = auth;
|
|
43
|
+
}
|
|
44
|
+
async request(options) {
|
|
45
|
+
const token = await this.auth.getToken();
|
|
46
|
+
const response = await this.http.request({
|
|
47
|
+
...options,
|
|
48
|
+
headers: withBearerToken(options.headers, token),
|
|
49
|
+
});
|
|
50
|
+
return response.data;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class OpenPlatformApi extends BaseApi {
|
|
54
|
+
async issueToken(input) {
|
|
55
|
+
return this.request({
|
|
56
|
+
method: 'POST',
|
|
57
|
+
path: '/open-platform/token',
|
|
58
|
+
body: input,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async getCurrentTokenPayload() {
|
|
62
|
+
return this.request({
|
|
63
|
+
method: 'GET',
|
|
64
|
+
path: '/open-platform/token/current',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.OpenPlatformApi = OpenPlatformApi;
|
|
69
|
+
class SubjectDirectoryApi extends BaseApi {
|
|
70
|
+
async createSubject(input) {
|
|
71
|
+
return this.request({
|
|
72
|
+
method: 'POST',
|
|
73
|
+
path: '/subject-directory/open/subjects',
|
|
74
|
+
body: input,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async createSubjectClass(input) {
|
|
78
|
+
return this.request({
|
|
79
|
+
method: 'POST',
|
|
80
|
+
path: '/subject-directory/open/subject-classes',
|
|
81
|
+
body: input,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async createSubjectInternal(input) {
|
|
85
|
+
return this.request({
|
|
86
|
+
method: 'POST',
|
|
87
|
+
path: '/subject-directory/subjects',
|
|
88
|
+
body: input,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async createSubjectClassInternal(input) {
|
|
92
|
+
return this.request({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
path: '/subject-directory/subject-classes',
|
|
95
|
+
body: input,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async createExternalIdentity(input) {
|
|
99
|
+
return this.request({
|
|
100
|
+
method: 'POST',
|
|
101
|
+
path: '/subject-directory/open/external-identities',
|
|
102
|
+
body: input,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async findSubjectByExternalIdentity(query) {
|
|
106
|
+
return this.request({
|
|
107
|
+
method: 'GET',
|
|
108
|
+
path: '/subject-directory/open/subjects/by-external-identity',
|
|
109
|
+
query,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async getSubjectContext(subjectId) {
|
|
113
|
+
return this.request({
|
|
114
|
+
method: 'GET',
|
|
115
|
+
path: `/subject-directory/open/subjects/${encodeURIComponent(subjectId)}/context`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async listSubjects(query) {
|
|
119
|
+
return this.request({
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/subject-directory/subjects',
|
|
122
|
+
query: { ...query },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async getSubjectContextInternal(tenantId, subjectId) {
|
|
126
|
+
return this.request({
|
|
127
|
+
method: 'GET',
|
|
128
|
+
path: `/subject-directory/subjects/${encodeURIComponent(subjectId)}/context`,
|
|
129
|
+
query: { tenantId },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.SubjectDirectoryApi = SubjectDirectoryApi;
|
|
134
|
+
class ConversationTypesApi extends BaseApi {
|
|
135
|
+
async create(input) {
|
|
136
|
+
return this.request({
|
|
137
|
+
method: 'POST',
|
|
138
|
+
path: '/conversation-runtime/open/types',
|
|
139
|
+
body: input,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async resolveCapabilities(typeId, query = {}) {
|
|
143
|
+
return this.request({
|
|
144
|
+
method: 'GET',
|
|
145
|
+
path: `/conversation-runtime/open/types/${encodeURIComponent(typeId)}/capabilities`,
|
|
146
|
+
query,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async createInternal(input) {
|
|
150
|
+
return this.request({
|
|
151
|
+
method: 'POST',
|
|
152
|
+
path: '/conversation-runtime/types',
|
|
153
|
+
body: input,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async listInternal(query) {
|
|
157
|
+
return this.request({
|
|
158
|
+
method: 'GET',
|
|
159
|
+
path: '/conversation-runtime/types',
|
|
160
|
+
query: { ...query },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.ConversationTypesApi = ConversationTypesApi;
|
|
165
|
+
class ConversationsApi extends BaseApi {
|
|
166
|
+
async create(input) {
|
|
167
|
+
return this.request({
|
|
168
|
+
method: 'POST',
|
|
169
|
+
path: '/conversation-runtime/open/conversations',
|
|
170
|
+
body: input,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async createInternal(input) {
|
|
174
|
+
return this.request({
|
|
175
|
+
method: 'POST',
|
|
176
|
+
path: '/conversation-runtime/conversations',
|
|
177
|
+
body: input,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async list(query = {}) {
|
|
181
|
+
return this.request({
|
|
182
|
+
method: 'GET',
|
|
183
|
+
path: '/conversation-runtime/conversations',
|
|
184
|
+
query: query,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async addParticipant(input) {
|
|
188
|
+
return this.request({
|
|
189
|
+
method: 'POST',
|
|
190
|
+
path: '/conversation-runtime/participants',
|
|
191
|
+
body: input,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async addParticipantInternal(input) {
|
|
195
|
+
return this.request({
|
|
196
|
+
method: 'POST',
|
|
197
|
+
path: '/conversation-runtime/participants',
|
|
198
|
+
body: input,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async listParticipants(query) {
|
|
202
|
+
return this.request({
|
|
203
|
+
method: 'GET',
|
|
204
|
+
path: '/conversation-runtime/participants',
|
|
205
|
+
query: { ...query },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.ConversationsApi = ConversationsApi;
|
|
210
|
+
class MessagesApi extends BaseApi {
|
|
211
|
+
async create(input) {
|
|
212
|
+
return this.request({
|
|
213
|
+
method: 'POST',
|
|
214
|
+
path: '/conversation-runtime/open/messages',
|
|
215
|
+
body: input,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async createInternal(input) {
|
|
219
|
+
return this.request({
|
|
220
|
+
method: 'POST',
|
|
221
|
+
path: '/conversation-runtime/messages',
|
|
222
|
+
body: input,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async list(query) {
|
|
226
|
+
return this.request({
|
|
227
|
+
method: 'GET',
|
|
228
|
+
path: '/conversation-runtime/messages',
|
|
229
|
+
query: { ...query },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async updateInternal(messageId, input) {
|
|
233
|
+
return this.request({
|
|
234
|
+
method: 'PATCH',
|
|
235
|
+
path: `/conversation-runtime/messages/${messageId}`,
|
|
236
|
+
query: {
|
|
237
|
+
tenantId: input.tenantId,
|
|
238
|
+
},
|
|
239
|
+
body: {
|
|
240
|
+
status: input.status,
|
|
241
|
+
content: input.content,
|
|
242
|
+
metadata: input.metadata,
|
|
243
|
+
actorSubjectId: input.actorSubjectId,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.MessagesApi = MessagesApi;
|
|
249
|
+
function stableUnique(items) {
|
|
250
|
+
return Array.from(new Set(items));
|
|
251
|
+
}
|
|
252
|
+
function createRuntimeMessageId() {
|
|
253
|
+
return `message-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
254
|
+
}
|
|
255
|
+
function createRuntimeConversationId() {
|
|
256
|
+
return `conversation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
257
|
+
}
|
|
258
|
+
function resolveMessagePageCursor(message) {
|
|
259
|
+
if (!message) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
if (typeof message.createdAt === 'string') {
|
|
263
|
+
return message.createdAt;
|
|
264
|
+
}
|
|
265
|
+
if (typeof message.updatedAt === 'string') {
|
|
266
|
+
return message.updatedAt;
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
class ChatHubClient {
|
|
271
|
+
constructor(options) {
|
|
272
|
+
this.options = options;
|
|
273
|
+
this.openPlatform = new OpenPlatformApi(options.http, options.auth);
|
|
274
|
+
this.subjects = new SubjectDirectoryApi(options.http, options.auth);
|
|
275
|
+
this.conversationTypes = new ConversationTypesApi(options.http, options.auth);
|
|
276
|
+
this.conversations = new ConversationsApi(options.http, options.auth);
|
|
277
|
+
this.messages = new MessagesApi(options.http, options.auth);
|
|
278
|
+
this.realtime = options.realtime;
|
|
279
|
+
}
|
|
280
|
+
async setToken(token) {
|
|
281
|
+
await this.options.auth.setToken(token);
|
|
282
|
+
}
|
|
283
|
+
async getToken() {
|
|
284
|
+
return this.options.auth.getToken();
|
|
285
|
+
}
|
|
286
|
+
async issueAppToken(input) {
|
|
287
|
+
const result = await this.openPlatform.issueToken(input);
|
|
288
|
+
if (result.token) {
|
|
289
|
+
await this.setToken(result.token);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
async connectRealtime() {
|
|
294
|
+
if (!this.realtime?.connect) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await this.realtime.connect(await this.getToken());
|
|
298
|
+
}
|
|
299
|
+
async disconnectRealtime() {
|
|
300
|
+
if (!this.realtime?.disconnect) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await this.realtime.disconnect();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
exports.ChatHubClient = ChatHubClient;
|
|
307
|
+
class ChatHubAppRuntime {
|
|
308
|
+
constructor(client, config) {
|
|
309
|
+
this.client = client;
|
|
310
|
+
this.config = config;
|
|
311
|
+
}
|
|
312
|
+
get tenantId() {
|
|
313
|
+
return this.config.tenant.tenantId;
|
|
314
|
+
}
|
|
315
|
+
get appConfig() {
|
|
316
|
+
return this.config;
|
|
317
|
+
}
|
|
318
|
+
/** 暴露底层实时适配器,供宿主加入自定义房间 / 监听全局事件(跨设备同步等)。 */
|
|
319
|
+
get realtime() {
|
|
320
|
+
return this.client.realtime;
|
|
321
|
+
}
|
|
322
|
+
async connectRealtime() {
|
|
323
|
+
await this.client.connectRealtime();
|
|
324
|
+
}
|
|
325
|
+
async disconnectRealtime() {
|
|
326
|
+
await this.client.disconnectRealtime();
|
|
327
|
+
}
|
|
328
|
+
async subscribeConversation(conversationId) {
|
|
329
|
+
try {
|
|
330
|
+
await this.ensureAuthenticated();
|
|
331
|
+
await this.client.connectRealtime();
|
|
332
|
+
if (!this.client.realtime?.subscribeConversation) {
|
|
333
|
+
console.warn('[chat-hub-sdk] subscribeConversation: no realtime adapter');
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const sub = await this.client.realtime.subscribeConversation(conversationId);
|
|
337
|
+
console.info('[chat-hub-sdk] subscribeConversation ok', conversationId, Boolean(sub));
|
|
338
|
+
return sub;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.warn('[chat-hub-sdk] subscribeConversation failed', conversationId, error instanceof Error ? error.message : String(error));
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async publishConversationEvent(conversationId, event) {
|
|
346
|
+
await this.ensureAuthenticated();
|
|
347
|
+
await this.client.connectRealtime();
|
|
348
|
+
if (!this.client.realtime?.publishConversationEvent) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
await this.client.realtime.publishConversationEvent(conversationId, event);
|
|
352
|
+
}
|
|
353
|
+
async updateConversationTyping(input) {
|
|
354
|
+
await this.publishConversationEvent(input.conversationId, {
|
|
355
|
+
eventName: 'conversation.typing.updated',
|
|
356
|
+
conversationId: input.conversationId,
|
|
357
|
+
isTyping: input.isTyping,
|
|
358
|
+
senderSubjectId: input.senderSubjectId,
|
|
359
|
+
metadata: input.metadata,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async updateConversationRead(input) {
|
|
363
|
+
await this.publishConversationEvent(input.conversationId, {
|
|
364
|
+
eventName: 'conversation.read.updated',
|
|
365
|
+
conversationId: input.conversationId,
|
|
366
|
+
messageId: input.messageId,
|
|
367
|
+
readAt: new Date().toISOString(),
|
|
368
|
+
metadata: input.metadata,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async ensureAuthenticated() {
|
|
372
|
+
if (this.config.auth.mode !== 'tenant_app_token') {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!this.config.auth.appTokenInput) {
|
|
376
|
+
throw new Error('tenant_app_token mode requires appTokenInput');
|
|
377
|
+
}
|
|
378
|
+
const token = await this.client.getToken();
|
|
379
|
+
if (token) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
await this.client.issueAppToken(this.config.auth.appTokenInput);
|
|
383
|
+
}
|
|
384
|
+
async bootstrapConversation() {
|
|
385
|
+
await this.ensureAuthenticated();
|
|
386
|
+
await this.ensureSubjectClasses();
|
|
387
|
+
await this.ensureSubjects();
|
|
388
|
+
await this.ensureConversationType();
|
|
389
|
+
const conversation = await this.createConversationFromPreset();
|
|
390
|
+
return {
|
|
391
|
+
tenantId: this.tenantId,
|
|
392
|
+
subjectClassIds: stableUnique((this.config.subjectClasses || []).map((item) => item.classId)),
|
|
393
|
+
subjectIds: stableUnique((this.config.subjects || []).map((item) => item.subjectId)),
|
|
394
|
+
conversationTypeId: this.config.conversationType?.typeId,
|
|
395
|
+
conversation,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async listConversations(subjectId) {
|
|
399
|
+
await this.ensureAuthenticated();
|
|
400
|
+
const resolvedSubjectId = subjectId || this.config.subjects?.[0]?.subjectId || undefined;
|
|
401
|
+
const conversations = await this.client.conversations.list({
|
|
402
|
+
tenantId: this.tenantId,
|
|
403
|
+
typeId: this.config.conversationType?.typeId,
|
|
404
|
+
subjectId: resolvedSubjectId,
|
|
405
|
+
});
|
|
406
|
+
// Participants are intentionally NOT fetched here: doing so would fan out
|
|
407
|
+
// one `/conversation-runtime/participants` HTTP request per conversation
|
|
408
|
+
// (N+1) on the conversation-list path, which on open produced dozens of
|
|
409
|
+
// concurrent requests. The timeline view loads participants on demand for
|
|
410
|
+
// the selected conversation only (see ConversationHubView).
|
|
411
|
+
return conversations;
|
|
412
|
+
}
|
|
413
|
+
async listMessages(input) {
|
|
414
|
+
await this.ensureAuthenticated();
|
|
415
|
+
const query = typeof input === 'string'
|
|
416
|
+
? {
|
|
417
|
+
conversationId: input,
|
|
418
|
+
}
|
|
419
|
+
: input;
|
|
420
|
+
return this.client.messages.list({
|
|
421
|
+
tenantId: this.tenantId,
|
|
422
|
+
conversationId: query.conversationId,
|
|
423
|
+
limit: query.limit,
|
|
424
|
+
beforeCreatedAt: query.beforeCreatedAt,
|
|
425
|
+
afterSequence: query.afterSequence,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async listMessagesPage(input) {
|
|
429
|
+
await this.ensureAuthenticated();
|
|
430
|
+
const query = typeof input === 'string'
|
|
431
|
+
? {
|
|
432
|
+
conversationId: input,
|
|
433
|
+
}
|
|
434
|
+
: input;
|
|
435
|
+
const normalizedLimit = typeof query.limit === 'number' && Number.isFinite(query.limit)
|
|
436
|
+
? Math.min(Math.max(Math.floor(query.limit), 1), 100)
|
|
437
|
+
: undefined;
|
|
438
|
+
if (!normalizedLimit) {
|
|
439
|
+
const items = await this.client.messages.list({
|
|
440
|
+
tenantId: this.tenantId,
|
|
441
|
+
conversationId: query.conversationId,
|
|
442
|
+
beforeCreatedAt: query.beforeCreatedAt,
|
|
443
|
+
afterSequence: query.afterSequence,
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
items,
|
|
447
|
+
hasMore: false,
|
|
448
|
+
nextBeforeCreatedAt: resolveMessagePageCursor(items[0]),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const records = await this.client.messages.list({
|
|
452
|
+
tenantId: this.tenantId,
|
|
453
|
+
conversationId: query.conversationId,
|
|
454
|
+
limit: Math.min(normalizedLimit + 1, 100),
|
|
455
|
+
beforeCreatedAt: query.beforeCreatedAt,
|
|
456
|
+
afterSequence: query.afterSequence,
|
|
457
|
+
});
|
|
458
|
+
const hasMore = records.length > normalizedLimit;
|
|
459
|
+
const items = hasMore ? records.slice(records.length - normalizedLimit) : records;
|
|
460
|
+
return {
|
|
461
|
+
items,
|
|
462
|
+
hasMore,
|
|
463
|
+
nextBeforeCreatedAt: resolveMessagePageCursor(items[0]),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async ensureParticipant(input) {
|
|
467
|
+
await this.ensureAuthenticated();
|
|
468
|
+
await this.ensureSubjectClasses();
|
|
469
|
+
await this.ensureSubjects();
|
|
470
|
+
await this.ensureConversationType();
|
|
471
|
+
return this.client.conversations.addParticipantInternal({
|
|
472
|
+
tenantId: this.tenantId,
|
|
473
|
+
conversationId: input.conversationId,
|
|
474
|
+
subjectId: input.subjectId,
|
|
475
|
+
role: input.role,
|
|
476
|
+
status: input.status || 'active',
|
|
477
|
+
visibilityScope: input.visibilityScope,
|
|
478
|
+
capabilityScope: input.capabilityScope,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
watchConversationMessages(options) {
|
|
482
|
+
const limit = typeof options.limit === 'number' && Number.isFinite(options.limit)
|
|
483
|
+
? Math.min(Math.max(Math.floor(options.limit), 1), 100)
|
|
484
|
+
: 20;
|
|
485
|
+
const fallbackIntervalMs = Math.max(options.fallbackIntervalMs ?? 30000, 5000);
|
|
486
|
+
const healthcheckIntervalMs = Math.max(options.healthcheckIntervalMs ?? 120000, 15000);
|
|
487
|
+
const pauseWhenHidden = options.pauseWhenHidden !== false;
|
|
488
|
+
let closed = false;
|
|
489
|
+
let subscription = null;
|
|
490
|
+
let fallbackTimer = null;
|
|
491
|
+
let refreshPromise = null;
|
|
492
|
+
let lastSequence = typeof options.initialLastSequence === 'number' &&
|
|
493
|
+
Number.isFinite(options.initialLastSequence)
|
|
494
|
+
? Math.max(Math.floor(options.initialLastSequence), 0)
|
|
495
|
+
: 0;
|
|
496
|
+
const timerApi = globalThis;
|
|
497
|
+
const shouldPause = () => pauseWhenHidden &&
|
|
498
|
+
typeof globalThis !== 'undefined' &&
|
|
499
|
+
Boolean(globalThis.document
|
|
500
|
+
?.hidden);
|
|
501
|
+
const clearFallback = () => {
|
|
502
|
+
if (fallbackTimer !== null) {
|
|
503
|
+
timerApi.clearInterval?.(fallbackTimer);
|
|
504
|
+
fallbackTimer = null;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const ensureFallback = (intervalMs) => {
|
|
508
|
+
if (fallbackTimer !== null || closed || !timerApi.setInterval)
|
|
509
|
+
return;
|
|
510
|
+
fallbackTimer = timerApi.setInterval(() => {
|
|
511
|
+
void refresh('fallback');
|
|
512
|
+
}, intervalMs);
|
|
513
|
+
};
|
|
514
|
+
const refresh = async (reason = 'manual') => {
|
|
515
|
+
if (closed || shouldPause())
|
|
516
|
+
return;
|
|
517
|
+
if (refreshPromise) {
|
|
518
|
+
return refreshPromise;
|
|
519
|
+
}
|
|
520
|
+
const afterSequence = lastSequence;
|
|
521
|
+
refreshPromise = (async () => {
|
|
522
|
+
try {
|
|
523
|
+
const messages = await this.listMessages({
|
|
524
|
+
conversationId: options.conversationId,
|
|
525
|
+
limit,
|
|
526
|
+
afterSequence,
|
|
527
|
+
});
|
|
528
|
+
const maxSequence = messages.reduce((current, message) => {
|
|
529
|
+
const sequence = typeof message.sequence === 'number' && Number.isFinite(message.sequence)
|
|
530
|
+
? Math.floor(message.sequence)
|
|
531
|
+
: current;
|
|
532
|
+
return Math.max(current, sequence);
|
|
533
|
+
}, lastSequence);
|
|
534
|
+
lastSequence = Math.max(lastSequence, maxSequence);
|
|
535
|
+
await options.onMessages({ reason, messages, afterSequence });
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
options.onError?.(error);
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
refreshPromise = null;
|
|
542
|
+
}
|
|
543
|
+
})();
|
|
544
|
+
return refreshPromise;
|
|
545
|
+
};
|
|
546
|
+
const startRealtime = async () => {
|
|
547
|
+
try {
|
|
548
|
+
console.info('[chat-hub-sdk] watch startRealtime', options.conversationId);
|
|
549
|
+
subscription = await this.subscribeConversation(options.conversationId);
|
|
550
|
+
if (!subscription) {
|
|
551
|
+
console.warn('[chat-hub-sdk] watch no realtime subscription, fallback only');
|
|
552
|
+
ensureFallback(fallbackIntervalMs);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
console.info('[chat-hub-sdk] watch realtime subscription active');
|
|
556
|
+
ensureFallback(healthcheckIntervalMs);
|
|
557
|
+
for await (const event of subscription) {
|
|
558
|
+
if (closed)
|
|
559
|
+
break;
|
|
560
|
+
await options.onEvent?.(event);
|
|
561
|
+
const eventRecord = event && typeof event === 'object' ? event : {};
|
|
562
|
+
const eventName = eventRecord.eventName;
|
|
563
|
+
if (eventName === 'conversation.message.created' ||
|
|
564
|
+
eventName === 'conversation.message.updated') {
|
|
565
|
+
const eventConversationId = typeof eventRecord.conversationId === 'string'
|
|
566
|
+
? eventRecord.conversationId
|
|
567
|
+
: '';
|
|
568
|
+
const eventMessage = eventRecord.message;
|
|
569
|
+
if (eventConversationId === options.conversationId &&
|
|
570
|
+
eventMessage &&
|
|
571
|
+
typeof eventMessage === 'object') {
|
|
572
|
+
await options.onMessages({
|
|
573
|
+
reason: 'realtime_event',
|
|
574
|
+
messages: [eventMessage],
|
|
575
|
+
afterSequence: lastSequence,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
const sequence = typeof eventRecord.sequence === 'number' &&
|
|
579
|
+
Number.isFinite(eventRecord.sequence)
|
|
580
|
+
? Math.floor(eventRecord.sequence)
|
|
581
|
+
: null;
|
|
582
|
+
if (sequence !== null && sequence <= lastSequence) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
await refresh('realtime_event');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
options.onError?.(error);
|
|
591
|
+
ensureFallback(fallbackIntervalMs);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
void startRealtime();
|
|
595
|
+
if (options.immediate !== false) {
|
|
596
|
+
void refresh('initial');
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
refresh,
|
|
600
|
+
close: async () => {
|
|
601
|
+
closed = true;
|
|
602
|
+
clearFallback();
|
|
603
|
+
if (subscription) {
|
|
604
|
+
await subscription.close();
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async sendTextMessage(input) {
|
|
610
|
+
return this.sendMessage({
|
|
611
|
+
conversationId: input.conversationId,
|
|
612
|
+
senderSubjectId: input.senderSubjectId,
|
|
613
|
+
kind: 'text',
|
|
614
|
+
content: {
|
|
615
|
+
text: input.text,
|
|
616
|
+
attachments: input.attachments || [],
|
|
617
|
+
},
|
|
618
|
+
messageId: input.messageId,
|
|
619
|
+
mentionedSubjectIds: input.mentionedSubjectIds,
|
|
620
|
+
metadata: input.metadata,
|
|
621
|
+
status: input.status,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
async sendMessage(input) {
|
|
625
|
+
await this.ensureAuthenticated();
|
|
626
|
+
return this.client.messages.createInternal({
|
|
627
|
+
tenantId: this.tenantId,
|
|
628
|
+
conversationId: input.conversationId,
|
|
629
|
+
messageId: input.messageId || createRuntimeMessageId(),
|
|
630
|
+
senderSubjectId: input.senderSubjectId,
|
|
631
|
+
kind: input.kind,
|
|
632
|
+
status: input.status || 'sent',
|
|
633
|
+
content: input.content,
|
|
634
|
+
mentionedSubjectIds: input.mentionedSubjectIds,
|
|
635
|
+
metadata: input.metadata,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
async updateMessage(input) {
|
|
639
|
+
await this.ensureAuthenticated();
|
|
640
|
+
return this.client.messages.updateInternal(input.messageId, {
|
|
641
|
+
tenantId: this.tenantId,
|
|
642
|
+
status: input.status,
|
|
643
|
+
content: input.content,
|
|
644
|
+
metadata: input.metadata,
|
|
645
|
+
actorSubjectId: input.actorSubjectId,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async deleteMessage(input) {
|
|
649
|
+
return this.updateMessage({
|
|
650
|
+
...input,
|
|
651
|
+
status: 'deleted',
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async revokeMessage(input) {
|
|
655
|
+
return this.updateMessage({
|
|
656
|
+
...input,
|
|
657
|
+
status: 'revoked',
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async ensureSubjectClasses() {
|
|
661
|
+
for (const preset of this.config.subjectClasses || []) {
|
|
662
|
+
try {
|
|
663
|
+
await this.client.subjects.createSubjectClassInternal({
|
|
664
|
+
tenantId: this.tenantId,
|
|
665
|
+
classId: preset.classId,
|
|
666
|
+
label: preset.label,
|
|
667
|
+
ontology: preset.ontology,
|
|
668
|
+
scope: preset.scope,
|
|
669
|
+
lifecycle: preset.lifecycle,
|
|
670
|
+
status: preset.status || 'active',
|
|
671
|
+
instanceMatcher: preset.instanceMatcher,
|
|
672
|
+
openRegistration: preset.openRegistration,
|
|
673
|
+
requiredExternalIdentity: preset.requiredExternalIdentity,
|
|
674
|
+
tags: preset.tags,
|
|
675
|
+
notes: preset.notes,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// Reuse existing definitions when the class has already been created.
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async ensureSubjects() {
|
|
684
|
+
for (const preset of this.config.subjects || []) {
|
|
685
|
+
// 跳过隐式构造的占位 subject:前端 shell 会把隐式 human/agent 占位
|
|
686
|
+
// (subjectId 形如 "subject-*-hub-<tenant>-default" 或 "subject-human-*")
|
|
687
|
+
// 一并塞进 config.subjects,ensureSubjects 若持久化它们会在 subject_directory
|
|
688
|
+
// 产生脏记录,进而被后端当成真实实例反复加入新建会话参与者。
|
|
689
|
+
// 这类占位不属于用户手动创建的真实主体,不应落库。
|
|
690
|
+
const subjectId = String(preset?.subjectId || '');
|
|
691
|
+
if (/^subject-.+-hub-[0-9a-f]{24}-default$/.test(subjectId) ||
|
|
692
|
+
/^subject-(human|agent|system)-/.test(subjectId)) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
await this.client.subjects.createSubjectInternal({
|
|
697
|
+
tenantId: this.tenantId,
|
|
698
|
+
subjectId: preset.subjectId,
|
|
699
|
+
classId: preset.classId,
|
|
700
|
+
displayName: preset.displayName,
|
|
701
|
+
status: preset.status || 'active',
|
|
702
|
+
avatarUrl: preset.avatarUrl,
|
|
703
|
+
profile: preset.profile,
|
|
704
|
+
tags: preset.tags,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
// Reuse existing subject records when they already exist.
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async ensureConversationType() {
|
|
713
|
+
const preset = this.config.conversationType;
|
|
714
|
+
if (!preset) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
await this.client.conversationTypes.createInternal({
|
|
719
|
+
tenantId: this.tenantId,
|
|
720
|
+
typeId: preset.typeId,
|
|
721
|
+
name: preset.name,
|
|
722
|
+
label: preset.label,
|
|
723
|
+
conversationKind: preset.conversationKind,
|
|
724
|
+
status: preset.status || 'active',
|
|
725
|
+
welcomeMessage: preset.welcomeMessage,
|
|
726
|
+
creatorPolicy: preset.creatorPolicy,
|
|
727
|
+
audiencePolicy: preset.audiencePolicy,
|
|
728
|
+
messagePolicy: preset.messagePolicy,
|
|
729
|
+
defaultCapabilities: preset.defaultCapabilities,
|
|
730
|
+
optionalCapabilities: preset.optionalCapabilities,
|
|
731
|
+
restrictedCapabilities: preset.restrictedCapabilities,
|
|
732
|
+
triggeredCapabilities: preset.triggeredCapabilities,
|
|
733
|
+
capabilityTiers: preset.capabilityTiers,
|
|
734
|
+
metadata: preset.metadata,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Reuse existing type definitions when the type has already been created.
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async createConversationFromPreset() {
|
|
742
|
+
const preset = this.config.conversation;
|
|
743
|
+
if (!preset) {
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
const conversationId = createRuntimeConversationId();
|
|
747
|
+
const conversation = await this.client.conversations.createInternal({
|
|
748
|
+
tenantId: this.tenantId,
|
|
749
|
+
conversationId,
|
|
750
|
+
typeId: preset.typeId,
|
|
751
|
+
title: preset.title,
|
|
752
|
+
createdBySubjectId: preset.createdBySubjectId,
|
|
753
|
+
parentConversationId: preset.parentConversationId,
|
|
754
|
+
relation: preset.relation,
|
|
755
|
+
status: preset.status || 'active',
|
|
756
|
+
metadata: preset.metadata,
|
|
757
|
+
});
|
|
758
|
+
await Promise.all((preset.participants || []).map((participant) => this.client.conversations.addParticipantInternal({
|
|
759
|
+
tenantId: this.tenantId,
|
|
760
|
+
conversationId,
|
|
761
|
+
subjectId: participant.subjectId,
|
|
762
|
+
role: participant.role,
|
|
763
|
+
status: participant.status || 'active',
|
|
764
|
+
})));
|
|
765
|
+
return conversation;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
exports.ChatHubAppRuntime = ChatHubAppRuntime;
|
|
769
|
+
function createChatHubAppRuntime(client, config) {
|
|
770
|
+
return new ChatHubAppRuntime(client, config);
|
|
771
|
+
}
|
|
772
|
+
class PlatformApi extends BaseApi {
|
|
773
|
+
async provisionTenant(input) {
|
|
774
|
+
return this.request({
|
|
775
|
+
method: 'POST',
|
|
776
|
+
path: '/open-platform/platform/tenants',
|
|
777
|
+
body: input,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async syncSubject(targetTenantId, input) {
|
|
781
|
+
return this.request({
|
|
782
|
+
method: 'POST',
|
|
783
|
+
path: '/open-platform/platform/subjects:sync',
|
|
784
|
+
body: { ...input, targetTenantId },
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async resolveTenant(externalCorrelationId) {
|
|
788
|
+
return this.request({
|
|
789
|
+
method: 'GET',
|
|
790
|
+
path: '/open-platform/platform/tenants:resolve',
|
|
791
|
+
query: { externalCorrelationId },
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async toggleGrant(grantId, status) {
|
|
795
|
+
return this.request({
|
|
796
|
+
method: 'POST',
|
|
797
|
+
path: `/open-platform/platform/grants/${encodeURIComponent(grantId)}/toggle`,
|
|
798
|
+
body: { status },
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
exports.PlatformApi = PlatformApi;
|