@appthen/sdk-core 0.0.2 → 0.0.4
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 +107 -1
- package/dist/index.js +85 -15
- package/dist-cjs/index.d.ts +107 -1
- package/dist-cjs/index.js +87 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -273,8 +273,17 @@ export interface CreateMessageInput {
|
|
|
273
273
|
status?: string;
|
|
274
274
|
metadata?: Record<string, unknown>;
|
|
275
275
|
}
|
|
276
|
+
/** 入口客户端信号:网关只认白名单 kind 与短 deviceId,用于运行时路由。 */
|
|
277
|
+
export interface ChatHubEntryClientSignal {
|
|
278
|
+
kind: 'web' | 'desktop' | 'mobile';
|
|
279
|
+
deviceId?: string;
|
|
280
|
+
}
|
|
281
|
+
/** 运行偏好:auto = 服务端按入口策略决定;cloud = 用户显式选择云端执行。 */
|
|
282
|
+
export type ChatHubRuntimePreference = 'auto' | 'cloud';
|
|
276
283
|
export interface CreateInternalMessageInput extends CreateMessageInput {
|
|
277
284
|
tenantId: string;
|
|
285
|
+
client?: ChatHubEntryClientSignal;
|
|
286
|
+
runtimePreference?: ChatHubRuntimePreference;
|
|
278
287
|
}
|
|
279
288
|
export interface QueryMessagesInput {
|
|
280
289
|
tenantId?: string;
|
|
@@ -298,7 +307,26 @@ export interface QueryConversationsInput {
|
|
|
298
307
|
status?: string;
|
|
299
308
|
createdBySubjectId?: string;
|
|
300
309
|
subjectId?: string;
|
|
310
|
+
/** 分页(P1):不传即全量(保持历史行为)。 */
|
|
311
|
+
limit?: number;
|
|
312
|
+
offset?: number;
|
|
313
|
+
/** keyset 游标(上一页最后一条的 encode 结果,见 encodeConversationListCursor)。 */
|
|
314
|
+
cursor?: string;
|
|
315
|
+
/** 服务端搜索:标题 / conversationId 纯文本匹配(不区分大小写)。 */
|
|
316
|
+
search?: string;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 会话列表游标(keyset 分页,P1):与服务端 conversation-list-cursor.util 同构。
|
|
320
|
+
* 载荷 = base64url(JSON { u: updatedAt 毫秒, c: conversationId })。
|
|
321
|
+
* 客户端从「当前列表最后一条」构造;服务端按 (updatedAt desc, conversationId desc) 续页。
|
|
322
|
+
*/
|
|
323
|
+
export interface ConversationListCursorPayload {
|
|
324
|
+
updatedAt: number;
|
|
325
|
+
conversationId: string;
|
|
301
326
|
}
|
|
327
|
+
export declare function encodeConversationListCursor(payload: ConversationListCursorPayload): string;
|
|
328
|
+
/** 从会话记录构造游标(读取 updatedAt;缺失/非法返回 null)。 */
|
|
329
|
+
export declare function buildConversationListCursorFromRecord(record: Record<string, unknown> | null | undefined): string | null;
|
|
302
330
|
export interface CreateInternalConversationInput extends CreateConversationInput {
|
|
303
331
|
tenantId: string;
|
|
304
332
|
}
|
|
@@ -421,6 +449,10 @@ export interface SendTextMessageOptions {
|
|
|
421
449
|
mentionedSubjectIds?: string[];
|
|
422
450
|
metadata?: Record<string, unknown>;
|
|
423
451
|
status?: string;
|
|
452
|
+
/** 这条消息从哪种客户端发出(运行时路由信号)。 */
|
|
453
|
+
client?: ChatHubEntryClientSignal;
|
|
454
|
+
/** 仅显式选择云端执行时传 `cloud`;默认不传 = 服务端按 auto 处理。 */
|
|
455
|
+
runtimePreference?: ChatHubRuntimePreference;
|
|
424
456
|
}
|
|
425
457
|
export interface SendChatHubMessageOptions {
|
|
426
458
|
conversationId: string;
|
|
@@ -431,6 +463,8 @@ export interface SendChatHubMessageOptions {
|
|
|
431
463
|
mentionedSubjectIds?: string[];
|
|
432
464
|
metadata?: Record<string, unknown>;
|
|
433
465
|
status?: string;
|
|
466
|
+
client?: ChatHubEntryClientSignal;
|
|
467
|
+
runtimePreference?: ChatHubRuntimePreference;
|
|
434
468
|
}
|
|
435
469
|
export interface ListChatHubMessagesOptions {
|
|
436
470
|
conversationId: string;
|
|
@@ -560,7 +594,14 @@ export declare class ChatHubAppRuntime {
|
|
|
560
594
|
updateConversationRead(input: ConversationReadStateInput): Promise<void>;
|
|
561
595
|
ensureAuthenticated(): Promise<void>;
|
|
562
596
|
bootstrapConversation(): Promise<ChatHubBootstrapResult>;
|
|
563
|
-
listConversations(subjectId?: string
|
|
597
|
+
listConversations(subjectId?: string, options?: {
|
|
598
|
+
/** 分页(P1):不传 limit 即全量(历史行为)。 */
|
|
599
|
+
limit?: number;
|
|
600
|
+
/** keyset 游标(上一页最后一条的 encode 结果)。 */
|
|
601
|
+
cursor?: string;
|
|
602
|
+
/** 服务端搜索:标题 / conversationId 纯文本匹配。 */
|
|
603
|
+
search?: string;
|
|
604
|
+
}): Promise<ConversationRecord[]>;
|
|
564
605
|
listMessages(input: string | ListChatHubMessagesOptions): Promise<ConversationMessageRecord[]>;
|
|
565
606
|
listMessagesPage(input: string | ListChatHubMessagesOptions): Promise<ListChatHubMessagesPageResult>;
|
|
566
607
|
ensureParticipant(input: CreateParticipantInput): Promise<Record<string, unknown>>;
|
|
@@ -582,6 +623,12 @@ export interface PlatformProvisionTenantInput {
|
|
|
582
623
|
externalCorrelationId: string;
|
|
583
624
|
/** 该租户被授予的 platform scope,缺省 ['platform:subject:sync']。 */
|
|
584
625
|
grantScopes?: string[];
|
|
626
|
+
adminIdentifiers?: PlatformAdminIdentifier[];
|
|
627
|
+
adminDisplayName?: string;
|
|
628
|
+
}
|
|
629
|
+
export interface PlatformAdminIdentifier {
|
|
630
|
+
type: 'phone' | 'email';
|
|
631
|
+
value: string;
|
|
585
632
|
}
|
|
586
633
|
export interface PlatformProvisionTenantResult {
|
|
587
634
|
tenantId: string;
|
|
@@ -590,6 +637,8 @@ export interface PlatformProvisionTenantResult {
|
|
|
590
637
|
grantId: string;
|
|
591
638
|
/** 仅首次开通返回(7d 有效);认领激活时提交。 */
|
|
592
639
|
claimToken?: string;
|
|
640
|
+
inviteId?: string;
|
|
641
|
+
inviteUrl?: string;
|
|
593
642
|
/** false = 幂等命中(同 platformId + externalCorrelationId 已开通过)。 */
|
|
594
643
|
created: boolean;
|
|
595
644
|
[key: string]: unknown;
|
|
@@ -600,6 +649,11 @@ export interface PlatformExternalIdentityInput {
|
|
|
600
649
|
displayLabel?: string;
|
|
601
650
|
metadata?: Record<string, unknown>;
|
|
602
651
|
}
|
|
652
|
+
export interface PlatformLoginIdentifierInput {
|
|
653
|
+
/** 登录渠道。v1 仅支持 `phone`(邮箱/工号等预留,新增渠道不改契约形状)。 */
|
|
654
|
+
provider: 'phone' | string;
|
|
655
|
+
value: string;
|
|
656
|
+
}
|
|
603
657
|
export interface PlatformSyncSubjectInput {
|
|
604
658
|
classId: string;
|
|
605
659
|
displayName: string;
|
|
@@ -611,6 +665,32 @@ export interface PlatformSyncSubjectInput {
|
|
|
611
665
|
tags?: string[];
|
|
612
666
|
/** 命中已存在主体时是否更新其可写字段,默认 false(纯幂等)。 */
|
|
613
667
|
updateOnMatch?: boolean;
|
|
668
|
+
/**
|
|
669
|
+
* 登录标识(v1 = 手机号)。**带上才可登录**——平台据此把"这个成员"与
|
|
670
|
+
* "用该标识登录的人"对上;不带不算错,但该主体只作为通讯录/圈人对象存在,
|
|
671
|
+
* 登录时列不出来。
|
|
672
|
+
*
|
|
673
|
+
* 语义要点:
|
|
674
|
+
* - 与 `updateOnMatch` **正交**:传入即补绑(命中已存在主体时同样生效),
|
|
675
|
+
* 所以"先同步→后拿到手机号→再推一次"即可让身份变得可用;
|
|
676
|
+
* - **不传 ≠ 解绑**(缺席不代表移除);
|
|
677
|
+
* - 同一标识可同时挂在多个主体上(例如第三方系统里同一手机号对应
|
|
678
|
+
* 「用户前台」「员工后台」两个账号空间)——平台按声明如实挂载,不自行归并;
|
|
679
|
+
* - 系统内**首次**挂载该标识的主体为「主身份」,用于登录时的默认选择。
|
|
680
|
+
*/
|
|
681
|
+
loginIdentifiers?: PlatformLoginIdentifierInput[];
|
|
682
|
+
/**
|
|
683
|
+
* 身份标签:**由第三方声明的短标签**(如「用户前台」/「员工后台」),
|
|
684
|
+
* 用于在工作台的「切换身份」里区分同一个人的多个身份。
|
|
685
|
+
*
|
|
686
|
+
* 语义要点:
|
|
687
|
+
* - 平台**只提供字段,不发明语义**:值原样展示,不翻译、不判定、不造词;
|
|
688
|
+
* 不传时界面回退到角色(member / admin / owner)。
|
|
689
|
+
* - 属**内容字段**(与 `displayName` / `tags` 同规则):建时落库;命中已存在
|
|
690
|
+
* 主体时**仅在 `updateOnMatch: true` 下覆盖**,不传不会被清空。
|
|
691
|
+
* - 建议 ≤ 4 个字(行内展示位有限);接口上限 64 字符,超长返回 400。
|
|
692
|
+
*/
|
|
693
|
+
identityLabel?: string;
|
|
614
694
|
/** 幂等键:{source, externalKey}。重复推送只产生一个 Subject,可安全重试。 */
|
|
615
695
|
externalIdentity: PlatformExternalIdentityInput;
|
|
616
696
|
}
|
|
@@ -630,10 +710,21 @@ export interface PlatformResolveTenantResult {
|
|
|
630
710
|
status: string;
|
|
631
711
|
}>;
|
|
632
712
|
}
|
|
713
|
+
export interface PlatformAdminInviteInput {
|
|
714
|
+
adminIdentifiers: PlatformAdminIdentifier[];
|
|
715
|
+
adminDisplayName?: string;
|
|
716
|
+
}
|
|
717
|
+
export interface PlatformAdminInviteResult {
|
|
718
|
+
tenantId: string;
|
|
719
|
+
status: string;
|
|
720
|
+
inviteId: string;
|
|
721
|
+
inviteUrl: string;
|
|
722
|
+
}
|
|
633
723
|
export declare class PlatformApi extends BaseApi {
|
|
634
724
|
provisionTenant(input: PlatformProvisionTenantInput): Promise<PlatformProvisionTenantResult>;
|
|
635
725
|
syncSubject(targetTenantId: string, input: PlatformSyncSubjectInput): Promise<PlatformSyncSubjectResult>;
|
|
636
726
|
resolveTenant(externalCorrelationId: string): Promise<PlatformResolveTenantResult>;
|
|
727
|
+
createAdminInvite(tenantId: string, input: PlatformAdminInviteInput): Promise<PlatformAdminInviteResult>;
|
|
637
728
|
toggleGrant(grantId: string, status: 'active' | 'revoked'): Promise<Record<string, unknown>>;
|
|
638
729
|
/**
|
|
639
730
|
* 按标签/条件圈人并直接触达(声明式,调用方无 subjectId 概念):
|
|
@@ -642,6 +733,21 @@ export declare class PlatformApi extends BaseApi {
|
|
|
642
733
|
* 收件人二选一:标签圈人(selector)或单点声明(receive,如微信客服消息迁移)。
|
|
643
734
|
*/
|
|
644
735
|
notify(targetTenantId: string, request: PlatformNotifyRequest, notify: PlatformNotifyInput): Promise<PlatformNotifyResult>;
|
|
736
|
+
/**
|
|
737
|
+
* 重发认领凭证(平台级):租户 7 天内未认领时,服务商重新签发 claimToken。
|
|
738
|
+
* **重发即作废**——epoch 递增,旧 token 立即失效;仅 pending 租户可重发。
|
|
739
|
+
*/
|
|
740
|
+
reissueClaimToken(tenantId: string): Promise<PlatformReissueClaimTokenResult>;
|
|
741
|
+
}
|
|
742
|
+
/** 重发认领凭证的返回(claimToken 有效期 7 天;重发后旧 token 立即失效)。 */
|
|
743
|
+
export interface PlatformReissueClaimTokenResult {
|
|
744
|
+
tenantId: string;
|
|
745
|
+
status: string;
|
|
746
|
+
claimToken: string;
|
|
747
|
+
expiresIn: string;
|
|
748
|
+
/** 签发轮次:0 = 首次开通签发,>=1 = 第 N 次重发。 */
|
|
749
|
+
claimEpoch: number;
|
|
750
|
+
reissued: boolean;
|
|
645
751
|
}
|
|
646
752
|
/** 声明式主体引用:调用方只需本地维护 id/昵称/头像,服务端 lazily create-or-get。 */
|
|
647
753
|
export interface PlatformLazySubject {
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,27 @@
|
|
|
10
10
|
* - 内部 JWT 保护的查询接口(messages / conversations)
|
|
11
11
|
*/
|
|
12
12
|
export const SDK_VERSION = '0.1.0';
|
|
13
|
+
export function encodeConversationListCursor(payload) {
|
|
14
|
+
const json = JSON.stringify({ u: payload.updatedAt, c: payload.conversationId });
|
|
15
|
+
// 同时兼容浏览器(btoa,需先转 latin1)与 Node(Buffer):base64url 无 padding。
|
|
16
|
+
const base64 = typeof Buffer !== 'undefined'
|
|
17
|
+
? Buffer.from(json, 'utf8').toString('base64')
|
|
18
|
+
: btoa(unescape(encodeURIComponent(json)));
|
|
19
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
20
|
+
}
|
|
21
|
+
/** 从会话记录构造游标(读取 updatedAt;缺失/非法返回 null)。 */
|
|
22
|
+
export function buildConversationListCursorFromRecord(record) {
|
|
23
|
+
const conversationId = typeof record?.conversationId === 'string' ? record.conversationId : '';
|
|
24
|
+
const rawUpdatedAt = record?.updatedAt;
|
|
25
|
+
const updatedAt = typeof rawUpdatedAt === 'number'
|
|
26
|
+
? rawUpdatedAt
|
|
27
|
+
: typeof rawUpdatedAt === 'string'
|
|
28
|
+
? Date.parse(rawUpdatedAt)
|
|
29
|
+
: NaN;
|
|
30
|
+
if (!conversationId || !Number.isFinite(updatedAt) || updatedAt <= 0)
|
|
31
|
+
return null;
|
|
32
|
+
return encodeConversationListCursor({ updatedAt, conversationId });
|
|
33
|
+
}
|
|
13
34
|
export function resolveChatHubMessagePolicy(value) {
|
|
14
35
|
const policy = value && typeof value === 'object' ? value : {};
|
|
15
36
|
return {
|
|
@@ -22,6 +43,12 @@ export function resolveChatHubMessagePolicy(value) {
|
|
|
22
43
|
allowRevoke: policy.allowRevoke === true,
|
|
23
44
|
};
|
|
24
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* 断线补齐时一次 refresh 最多向后续拉的页数(防单次同步打爆服务端)。
|
|
48
|
+
* 每页按 watch 的 limit(上限 100),20 页 = 最多 2000 条,
|
|
49
|
+
* 覆盖任何合理的断线积压;真超过这个量级应走历史分页而不是实时补齐。
|
|
50
|
+
*/
|
|
51
|
+
const MAX_CATCHUP_PAGES = 20;
|
|
25
52
|
function withBearerToken(headers, token) {
|
|
26
53
|
if (!token) {
|
|
27
54
|
return headers || {};
|
|
@@ -384,13 +411,16 @@ export class ChatHubAppRuntime {
|
|
|
384
411
|
conversation,
|
|
385
412
|
};
|
|
386
413
|
}
|
|
387
|
-
async listConversations(subjectId) {
|
|
414
|
+
async listConversations(subjectId, options) {
|
|
388
415
|
await this.ensureAuthenticated();
|
|
389
416
|
const resolvedSubjectId = subjectId || this.config.subjects?.[0]?.subjectId || undefined;
|
|
390
417
|
const conversations = await this.client.conversations.list({
|
|
391
418
|
tenantId: this.tenantId,
|
|
392
419
|
typeId: this.config.conversationType?.typeId,
|
|
393
420
|
subjectId: resolvedSubjectId,
|
|
421
|
+
...(options?.limit ? { limit: options.limit } : {}),
|
|
422
|
+
...(options?.cursor ? { cursor: options.cursor } : {}),
|
|
423
|
+
...(options?.search ? { search: options.search } : {}),
|
|
394
424
|
});
|
|
395
425
|
// Participants are intentionally NOT fetched here: doing so would fan out
|
|
396
426
|
// one `/conversation-runtime/participants` HTTP request per conversation
|
|
@@ -506,22 +536,41 @@ export class ChatHubAppRuntime {
|
|
|
506
536
|
if (refreshPromise) {
|
|
507
537
|
return refreshPromise;
|
|
508
538
|
}
|
|
509
|
-
const afterSequence = lastSequence;
|
|
510
539
|
refreshPromise = (async () => {
|
|
511
540
|
try {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
541
|
+
let cursor = lastSequence;
|
|
542
|
+
for (let page = 0; page < MAX_CATCHUP_PAGES; page += 1) {
|
|
543
|
+
const pageAfterSequence = cursor;
|
|
544
|
+
const messages = await this.listMessages({
|
|
545
|
+
conversationId: options.conversationId,
|
|
546
|
+
limit,
|
|
547
|
+
afterSequence: pageAfterSequence,
|
|
548
|
+
});
|
|
549
|
+
const maxSequence = messages.reduce((current, message) => {
|
|
550
|
+
const sequence = typeof message.sequence === 'number' && Number.isFinite(message.sequence)
|
|
551
|
+
? Math.floor(message.sequence)
|
|
552
|
+
: current;
|
|
553
|
+
return Math.max(current, sequence);
|
|
554
|
+
}, cursor);
|
|
555
|
+
const advanced = maxSequence > cursor;
|
|
556
|
+
if (advanced) {
|
|
557
|
+
cursor = maxSequence;
|
|
558
|
+
}
|
|
559
|
+
lastSequence = Math.max(lastSequence, maxSequence);
|
|
560
|
+
await options.onMessages({
|
|
561
|
+
reason,
|
|
562
|
+
messages,
|
|
563
|
+
afterSequence: pageAfterSequence,
|
|
564
|
+
});
|
|
565
|
+
// 收敛条件:不满页说明已追平;本页没有推进说明游标打转,必须停下
|
|
566
|
+
// (否则服务端若回传不含 sequence 的消息会死循环)。
|
|
567
|
+
if (messages.length < limit || !advanced) {
|
|
568
|
+
if (page > 0) {
|
|
569
|
+
console.info('[chat-hub-sdk] watch caught up', options.conversationId, 'pages=', page + 1, 'lastSequence=', lastSequence);
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
525
574
|
}
|
|
526
575
|
catch (error) {
|
|
527
576
|
options.onError?.(error);
|
|
@@ -608,6 +657,8 @@ export class ChatHubAppRuntime {
|
|
|
608
657
|
mentionedSubjectIds: input.mentionedSubjectIds,
|
|
609
658
|
metadata: input.metadata,
|
|
610
659
|
status: input.status,
|
|
660
|
+
client: input.client,
|
|
661
|
+
runtimePreference: input.runtimePreference,
|
|
611
662
|
});
|
|
612
663
|
}
|
|
613
664
|
async sendMessage(input) {
|
|
@@ -622,6 +673,8 @@ export class ChatHubAppRuntime {
|
|
|
622
673
|
content: input.content,
|
|
623
674
|
mentionedSubjectIds: input.mentionedSubjectIds,
|
|
624
675
|
metadata: input.metadata,
|
|
676
|
+
client: input.client,
|
|
677
|
+
runtimePreference: input.runtimePreference,
|
|
625
678
|
});
|
|
626
679
|
}
|
|
627
680
|
async updateMessage(input) {
|
|
@@ -779,6 +832,13 @@ export class PlatformApi extends BaseApi {
|
|
|
779
832
|
query: { externalCorrelationId },
|
|
780
833
|
});
|
|
781
834
|
}
|
|
835
|
+
async createAdminInvite(tenantId, input) {
|
|
836
|
+
return this.request({
|
|
837
|
+
method: 'POST',
|
|
838
|
+
path: `/open-platform/platform/tenants/${encodeURIComponent(tenantId)}/admin-invites`,
|
|
839
|
+
body: input,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
782
842
|
async toggleGrant(grantId, status) {
|
|
783
843
|
return this.request({
|
|
784
844
|
method: 'POST',
|
|
@@ -799,4 +859,14 @@ export class PlatformApi extends BaseApi {
|
|
|
799
859
|
body: { ...notify, ...request, targetTenantId },
|
|
800
860
|
});
|
|
801
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* 重发认领凭证(平台级):租户 7 天内未认领时,服务商重新签发 claimToken。
|
|
864
|
+
* **重发即作废**——epoch 递增,旧 token 立即失效;仅 pending 租户可重发。
|
|
865
|
+
*/
|
|
866
|
+
async reissueClaimToken(tenantId) {
|
|
867
|
+
return this.request({
|
|
868
|
+
method: 'POST',
|
|
869
|
+
path: `/open-platform/platform/tenants/${encodeURIComponent(tenantId)}/claim-token`,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
802
872
|
}
|
package/dist-cjs/index.d.ts
CHANGED
|
@@ -273,8 +273,17 @@ export interface CreateMessageInput {
|
|
|
273
273
|
status?: string;
|
|
274
274
|
metadata?: Record<string, unknown>;
|
|
275
275
|
}
|
|
276
|
+
/** 入口客户端信号:网关只认白名单 kind 与短 deviceId,用于运行时路由。 */
|
|
277
|
+
export interface ChatHubEntryClientSignal {
|
|
278
|
+
kind: 'web' | 'desktop' | 'mobile';
|
|
279
|
+
deviceId?: string;
|
|
280
|
+
}
|
|
281
|
+
/** 运行偏好:auto = 服务端按入口策略决定;cloud = 用户显式选择云端执行。 */
|
|
282
|
+
export type ChatHubRuntimePreference = 'auto' | 'cloud';
|
|
276
283
|
export interface CreateInternalMessageInput extends CreateMessageInput {
|
|
277
284
|
tenantId: string;
|
|
285
|
+
client?: ChatHubEntryClientSignal;
|
|
286
|
+
runtimePreference?: ChatHubRuntimePreference;
|
|
278
287
|
}
|
|
279
288
|
export interface QueryMessagesInput {
|
|
280
289
|
tenantId?: string;
|
|
@@ -298,7 +307,26 @@ export interface QueryConversationsInput {
|
|
|
298
307
|
status?: string;
|
|
299
308
|
createdBySubjectId?: string;
|
|
300
309
|
subjectId?: string;
|
|
310
|
+
/** 分页(P1):不传即全量(保持历史行为)。 */
|
|
311
|
+
limit?: number;
|
|
312
|
+
offset?: number;
|
|
313
|
+
/** keyset 游标(上一页最后一条的 encode 结果,见 encodeConversationListCursor)。 */
|
|
314
|
+
cursor?: string;
|
|
315
|
+
/** 服务端搜索:标题 / conversationId 纯文本匹配(不区分大小写)。 */
|
|
316
|
+
search?: string;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 会话列表游标(keyset 分页,P1):与服务端 conversation-list-cursor.util 同构。
|
|
320
|
+
* 载荷 = base64url(JSON { u: updatedAt 毫秒, c: conversationId })。
|
|
321
|
+
* 客户端从「当前列表最后一条」构造;服务端按 (updatedAt desc, conversationId desc) 续页。
|
|
322
|
+
*/
|
|
323
|
+
export interface ConversationListCursorPayload {
|
|
324
|
+
updatedAt: number;
|
|
325
|
+
conversationId: string;
|
|
301
326
|
}
|
|
327
|
+
export declare function encodeConversationListCursor(payload: ConversationListCursorPayload): string;
|
|
328
|
+
/** 从会话记录构造游标(读取 updatedAt;缺失/非法返回 null)。 */
|
|
329
|
+
export declare function buildConversationListCursorFromRecord(record: Record<string, unknown> | null | undefined): string | null;
|
|
302
330
|
export interface CreateInternalConversationInput extends CreateConversationInput {
|
|
303
331
|
tenantId: string;
|
|
304
332
|
}
|
|
@@ -421,6 +449,10 @@ export interface SendTextMessageOptions {
|
|
|
421
449
|
mentionedSubjectIds?: string[];
|
|
422
450
|
metadata?: Record<string, unknown>;
|
|
423
451
|
status?: string;
|
|
452
|
+
/** 这条消息从哪种客户端发出(运行时路由信号)。 */
|
|
453
|
+
client?: ChatHubEntryClientSignal;
|
|
454
|
+
/** 仅显式选择云端执行时传 `cloud`;默认不传 = 服务端按 auto 处理。 */
|
|
455
|
+
runtimePreference?: ChatHubRuntimePreference;
|
|
424
456
|
}
|
|
425
457
|
export interface SendChatHubMessageOptions {
|
|
426
458
|
conversationId: string;
|
|
@@ -431,6 +463,8 @@ export interface SendChatHubMessageOptions {
|
|
|
431
463
|
mentionedSubjectIds?: string[];
|
|
432
464
|
metadata?: Record<string, unknown>;
|
|
433
465
|
status?: string;
|
|
466
|
+
client?: ChatHubEntryClientSignal;
|
|
467
|
+
runtimePreference?: ChatHubRuntimePreference;
|
|
434
468
|
}
|
|
435
469
|
export interface ListChatHubMessagesOptions {
|
|
436
470
|
conversationId: string;
|
|
@@ -560,7 +594,14 @@ export declare class ChatHubAppRuntime {
|
|
|
560
594
|
updateConversationRead(input: ConversationReadStateInput): Promise<void>;
|
|
561
595
|
ensureAuthenticated(): Promise<void>;
|
|
562
596
|
bootstrapConversation(): Promise<ChatHubBootstrapResult>;
|
|
563
|
-
listConversations(subjectId?: string
|
|
597
|
+
listConversations(subjectId?: string, options?: {
|
|
598
|
+
/** 分页(P1):不传 limit 即全量(历史行为)。 */
|
|
599
|
+
limit?: number;
|
|
600
|
+
/** keyset 游标(上一页最后一条的 encode 结果)。 */
|
|
601
|
+
cursor?: string;
|
|
602
|
+
/** 服务端搜索:标题 / conversationId 纯文本匹配。 */
|
|
603
|
+
search?: string;
|
|
604
|
+
}): Promise<ConversationRecord[]>;
|
|
564
605
|
listMessages(input: string | ListChatHubMessagesOptions): Promise<ConversationMessageRecord[]>;
|
|
565
606
|
listMessagesPage(input: string | ListChatHubMessagesOptions): Promise<ListChatHubMessagesPageResult>;
|
|
566
607
|
ensureParticipant(input: CreateParticipantInput): Promise<Record<string, unknown>>;
|
|
@@ -582,6 +623,12 @@ export interface PlatformProvisionTenantInput {
|
|
|
582
623
|
externalCorrelationId: string;
|
|
583
624
|
/** 该租户被授予的 platform scope,缺省 ['platform:subject:sync']。 */
|
|
584
625
|
grantScopes?: string[];
|
|
626
|
+
adminIdentifiers?: PlatformAdminIdentifier[];
|
|
627
|
+
adminDisplayName?: string;
|
|
628
|
+
}
|
|
629
|
+
export interface PlatformAdminIdentifier {
|
|
630
|
+
type: 'phone' | 'email';
|
|
631
|
+
value: string;
|
|
585
632
|
}
|
|
586
633
|
export interface PlatformProvisionTenantResult {
|
|
587
634
|
tenantId: string;
|
|
@@ -590,6 +637,8 @@ export interface PlatformProvisionTenantResult {
|
|
|
590
637
|
grantId: string;
|
|
591
638
|
/** 仅首次开通返回(7d 有效);认领激活时提交。 */
|
|
592
639
|
claimToken?: string;
|
|
640
|
+
inviteId?: string;
|
|
641
|
+
inviteUrl?: string;
|
|
593
642
|
/** false = 幂等命中(同 platformId + externalCorrelationId 已开通过)。 */
|
|
594
643
|
created: boolean;
|
|
595
644
|
[key: string]: unknown;
|
|
@@ -600,6 +649,11 @@ export interface PlatformExternalIdentityInput {
|
|
|
600
649
|
displayLabel?: string;
|
|
601
650
|
metadata?: Record<string, unknown>;
|
|
602
651
|
}
|
|
652
|
+
export interface PlatformLoginIdentifierInput {
|
|
653
|
+
/** 登录渠道。v1 仅支持 `phone`(邮箱/工号等预留,新增渠道不改契约形状)。 */
|
|
654
|
+
provider: 'phone' | string;
|
|
655
|
+
value: string;
|
|
656
|
+
}
|
|
603
657
|
export interface PlatformSyncSubjectInput {
|
|
604
658
|
classId: string;
|
|
605
659
|
displayName: string;
|
|
@@ -611,6 +665,32 @@ export interface PlatformSyncSubjectInput {
|
|
|
611
665
|
tags?: string[];
|
|
612
666
|
/** 命中已存在主体时是否更新其可写字段,默认 false(纯幂等)。 */
|
|
613
667
|
updateOnMatch?: boolean;
|
|
668
|
+
/**
|
|
669
|
+
* 登录标识(v1 = 手机号)。**带上才可登录**——平台据此把"这个成员"与
|
|
670
|
+
* "用该标识登录的人"对上;不带不算错,但该主体只作为通讯录/圈人对象存在,
|
|
671
|
+
* 登录时列不出来。
|
|
672
|
+
*
|
|
673
|
+
* 语义要点:
|
|
674
|
+
* - 与 `updateOnMatch` **正交**:传入即补绑(命中已存在主体时同样生效),
|
|
675
|
+
* 所以"先同步→后拿到手机号→再推一次"即可让身份变得可用;
|
|
676
|
+
* - **不传 ≠ 解绑**(缺席不代表移除);
|
|
677
|
+
* - 同一标识可同时挂在多个主体上(例如第三方系统里同一手机号对应
|
|
678
|
+
* 「用户前台」「员工后台」两个账号空间)——平台按声明如实挂载,不自行归并;
|
|
679
|
+
* - 系统内**首次**挂载该标识的主体为「主身份」,用于登录时的默认选择。
|
|
680
|
+
*/
|
|
681
|
+
loginIdentifiers?: PlatformLoginIdentifierInput[];
|
|
682
|
+
/**
|
|
683
|
+
* 身份标签:**由第三方声明的短标签**(如「用户前台」/「员工后台」),
|
|
684
|
+
* 用于在工作台的「切换身份」里区分同一个人的多个身份。
|
|
685
|
+
*
|
|
686
|
+
* 语义要点:
|
|
687
|
+
* - 平台**只提供字段,不发明语义**:值原样展示,不翻译、不判定、不造词;
|
|
688
|
+
* 不传时界面回退到角色(member / admin / owner)。
|
|
689
|
+
* - 属**内容字段**(与 `displayName` / `tags` 同规则):建时落库;命中已存在
|
|
690
|
+
* 主体时**仅在 `updateOnMatch: true` 下覆盖**,不传不会被清空。
|
|
691
|
+
* - 建议 ≤ 4 个字(行内展示位有限);接口上限 64 字符,超长返回 400。
|
|
692
|
+
*/
|
|
693
|
+
identityLabel?: string;
|
|
614
694
|
/** 幂等键:{source, externalKey}。重复推送只产生一个 Subject,可安全重试。 */
|
|
615
695
|
externalIdentity: PlatformExternalIdentityInput;
|
|
616
696
|
}
|
|
@@ -630,10 +710,21 @@ export interface PlatformResolveTenantResult {
|
|
|
630
710
|
status: string;
|
|
631
711
|
}>;
|
|
632
712
|
}
|
|
713
|
+
export interface PlatformAdminInviteInput {
|
|
714
|
+
adminIdentifiers: PlatformAdminIdentifier[];
|
|
715
|
+
adminDisplayName?: string;
|
|
716
|
+
}
|
|
717
|
+
export interface PlatformAdminInviteResult {
|
|
718
|
+
tenantId: string;
|
|
719
|
+
status: string;
|
|
720
|
+
inviteId: string;
|
|
721
|
+
inviteUrl: string;
|
|
722
|
+
}
|
|
633
723
|
export declare class PlatformApi extends BaseApi {
|
|
634
724
|
provisionTenant(input: PlatformProvisionTenantInput): Promise<PlatformProvisionTenantResult>;
|
|
635
725
|
syncSubject(targetTenantId: string, input: PlatformSyncSubjectInput): Promise<PlatformSyncSubjectResult>;
|
|
636
726
|
resolveTenant(externalCorrelationId: string): Promise<PlatformResolveTenantResult>;
|
|
727
|
+
createAdminInvite(tenantId: string, input: PlatformAdminInviteInput): Promise<PlatformAdminInviteResult>;
|
|
637
728
|
toggleGrant(grantId: string, status: 'active' | 'revoked'): Promise<Record<string, unknown>>;
|
|
638
729
|
/**
|
|
639
730
|
* 按标签/条件圈人并直接触达(声明式,调用方无 subjectId 概念):
|
|
@@ -642,6 +733,21 @@ export declare class PlatformApi extends BaseApi {
|
|
|
642
733
|
* 收件人二选一:标签圈人(selector)或单点声明(receive,如微信客服消息迁移)。
|
|
643
734
|
*/
|
|
644
735
|
notify(targetTenantId: string, request: PlatformNotifyRequest, notify: PlatformNotifyInput): Promise<PlatformNotifyResult>;
|
|
736
|
+
/**
|
|
737
|
+
* 重发认领凭证(平台级):租户 7 天内未认领时,服务商重新签发 claimToken。
|
|
738
|
+
* **重发即作废**——epoch 递增,旧 token 立即失效;仅 pending 租户可重发。
|
|
739
|
+
*/
|
|
740
|
+
reissueClaimToken(tenantId: string): Promise<PlatformReissueClaimTokenResult>;
|
|
741
|
+
}
|
|
742
|
+
/** 重发认领凭证的返回(claimToken 有效期 7 天;重发后旧 token 立即失效)。 */
|
|
743
|
+
export interface PlatformReissueClaimTokenResult {
|
|
744
|
+
tenantId: string;
|
|
745
|
+
status: string;
|
|
746
|
+
claimToken: string;
|
|
747
|
+
expiresIn: string;
|
|
748
|
+
/** 签发轮次:0 = 首次开通签发,>=1 = 第 N 次重发。 */
|
|
749
|
+
claimEpoch: number;
|
|
750
|
+
reissued: boolean;
|
|
645
751
|
}
|
|
646
752
|
/** 声明式主体引用:调用方只需本地维护 id/昵称/头像,服务端 lazily create-or-get。 */
|
|
647
753
|
export interface PlatformLazySubject {
|
package/dist-cjs/index.js
CHANGED
|
@@ -12,9 +12,32 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.PlatformApi = exports.ChatHubAppRuntime = exports.ChatHubClient = exports.MessagesApi = exports.ConversationsApi = exports.ConversationTypesApi = exports.SubjectDirectoryApi = exports.OpenPlatformApi = exports.SDK_VERSION = void 0;
|
|
15
|
+
exports.encodeConversationListCursor = encodeConversationListCursor;
|
|
16
|
+
exports.buildConversationListCursorFromRecord = buildConversationListCursorFromRecord;
|
|
15
17
|
exports.resolveChatHubMessagePolicy = resolveChatHubMessagePolicy;
|
|
16
18
|
exports.createChatHubAppRuntime = createChatHubAppRuntime;
|
|
17
19
|
exports.SDK_VERSION = '0.1.0';
|
|
20
|
+
function encodeConversationListCursor(payload) {
|
|
21
|
+
const json = JSON.stringify({ u: payload.updatedAt, c: payload.conversationId });
|
|
22
|
+
// 同时兼容浏览器(btoa,需先转 latin1)与 Node(Buffer):base64url 无 padding。
|
|
23
|
+
const base64 = typeof Buffer !== 'undefined'
|
|
24
|
+
? Buffer.from(json, 'utf8').toString('base64')
|
|
25
|
+
: btoa(unescape(encodeURIComponent(json)));
|
|
26
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
27
|
+
}
|
|
28
|
+
/** 从会话记录构造游标(读取 updatedAt;缺失/非法返回 null)。 */
|
|
29
|
+
function buildConversationListCursorFromRecord(record) {
|
|
30
|
+
const conversationId = typeof record?.conversationId === 'string' ? record.conversationId : '';
|
|
31
|
+
const rawUpdatedAt = record?.updatedAt;
|
|
32
|
+
const updatedAt = typeof rawUpdatedAt === 'number'
|
|
33
|
+
? rawUpdatedAt
|
|
34
|
+
: typeof rawUpdatedAt === 'string'
|
|
35
|
+
? Date.parse(rawUpdatedAt)
|
|
36
|
+
: NaN;
|
|
37
|
+
if (!conversationId || !Number.isFinite(updatedAt) || updatedAt <= 0)
|
|
38
|
+
return null;
|
|
39
|
+
return encodeConversationListCursor({ updatedAt, conversationId });
|
|
40
|
+
}
|
|
18
41
|
function resolveChatHubMessagePolicy(value) {
|
|
19
42
|
const policy = value && typeof value === 'object' ? value : {};
|
|
20
43
|
return {
|
|
@@ -27,6 +50,12 @@ function resolveChatHubMessagePolicy(value) {
|
|
|
27
50
|
allowRevoke: policy.allowRevoke === true,
|
|
28
51
|
};
|
|
29
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* 断线补齐时一次 refresh 最多向后续拉的页数(防单次同步打爆服务端)。
|
|
55
|
+
* 每页按 watch 的 limit(上限 100),20 页 = 最多 2000 条,
|
|
56
|
+
* 覆盖任何合理的断线积压;真超过这个量级应走历史分页而不是实时补齐。
|
|
57
|
+
*/
|
|
58
|
+
const MAX_CATCHUP_PAGES = 20;
|
|
30
59
|
function withBearerToken(headers, token) {
|
|
31
60
|
if (!token) {
|
|
32
61
|
return headers || {};
|
|
@@ -395,13 +424,16 @@ class ChatHubAppRuntime {
|
|
|
395
424
|
conversation,
|
|
396
425
|
};
|
|
397
426
|
}
|
|
398
|
-
async listConversations(subjectId) {
|
|
427
|
+
async listConversations(subjectId, options) {
|
|
399
428
|
await this.ensureAuthenticated();
|
|
400
429
|
const resolvedSubjectId = subjectId || this.config.subjects?.[0]?.subjectId || undefined;
|
|
401
430
|
const conversations = await this.client.conversations.list({
|
|
402
431
|
tenantId: this.tenantId,
|
|
403
432
|
typeId: this.config.conversationType?.typeId,
|
|
404
433
|
subjectId: resolvedSubjectId,
|
|
434
|
+
...(options?.limit ? { limit: options.limit } : {}),
|
|
435
|
+
...(options?.cursor ? { cursor: options.cursor } : {}),
|
|
436
|
+
...(options?.search ? { search: options.search } : {}),
|
|
405
437
|
});
|
|
406
438
|
// Participants are intentionally NOT fetched here: doing so would fan out
|
|
407
439
|
// one `/conversation-runtime/participants` HTTP request per conversation
|
|
@@ -517,22 +549,41 @@ class ChatHubAppRuntime {
|
|
|
517
549
|
if (refreshPromise) {
|
|
518
550
|
return refreshPromise;
|
|
519
551
|
}
|
|
520
|
-
const afterSequence = lastSequence;
|
|
521
552
|
refreshPromise = (async () => {
|
|
522
553
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
554
|
+
let cursor = lastSequence;
|
|
555
|
+
for (let page = 0; page < MAX_CATCHUP_PAGES; page += 1) {
|
|
556
|
+
const pageAfterSequence = cursor;
|
|
557
|
+
const messages = await this.listMessages({
|
|
558
|
+
conversationId: options.conversationId,
|
|
559
|
+
limit,
|
|
560
|
+
afterSequence: pageAfterSequence,
|
|
561
|
+
});
|
|
562
|
+
const maxSequence = messages.reduce((current, message) => {
|
|
563
|
+
const sequence = typeof message.sequence === 'number' && Number.isFinite(message.sequence)
|
|
564
|
+
? Math.floor(message.sequence)
|
|
565
|
+
: current;
|
|
566
|
+
return Math.max(current, sequence);
|
|
567
|
+
}, cursor);
|
|
568
|
+
const advanced = maxSequence > cursor;
|
|
569
|
+
if (advanced) {
|
|
570
|
+
cursor = maxSequence;
|
|
571
|
+
}
|
|
572
|
+
lastSequence = Math.max(lastSequence, maxSequence);
|
|
573
|
+
await options.onMessages({
|
|
574
|
+
reason,
|
|
575
|
+
messages,
|
|
576
|
+
afterSequence: pageAfterSequence,
|
|
577
|
+
});
|
|
578
|
+
// 收敛条件:不满页说明已追平;本页没有推进说明游标打转,必须停下
|
|
579
|
+
// (否则服务端若回传不含 sequence 的消息会死循环)。
|
|
580
|
+
if (messages.length < limit || !advanced) {
|
|
581
|
+
if (page > 0) {
|
|
582
|
+
console.info('[chat-hub-sdk] watch caught up', options.conversationId, 'pages=', page + 1, 'lastSequence=', lastSequence);
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
536
587
|
}
|
|
537
588
|
catch (error) {
|
|
538
589
|
options.onError?.(error);
|
|
@@ -619,6 +670,8 @@ class ChatHubAppRuntime {
|
|
|
619
670
|
mentionedSubjectIds: input.mentionedSubjectIds,
|
|
620
671
|
metadata: input.metadata,
|
|
621
672
|
status: input.status,
|
|
673
|
+
client: input.client,
|
|
674
|
+
runtimePreference: input.runtimePreference,
|
|
622
675
|
});
|
|
623
676
|
}
|
|
624
677
|
async sendMessage(input) {
|
|
@@ -633,6 +686,8 @@ class ChatHubAppRuntime {
|
|
|
633
686
|
content: input.content,
|
|
634
687
|
mentionedSubjectIds: input.mentionedSubjectIds,
|
|
635
688
|
metadata: input.metadata,
|
|
689
|
+
client: input.client,
|
|
690
|
+
runtimePreference: input.runtimePreference,
|
|
636
691
|
});
|
|
637
692
|
}
|
|
638
693
|
async updateMessage(input) {
|
|
@@ -791,6 +846,13 @@ class PlatformApi extends BaseApi {
|
|
|
791
846
|
query: { externalCorrelationId },
|
|
792
847
|
});
|
|
793
848
|
}
|
|
849
|
+
async createAdminInvite(tenantId, input) {
|
|
850
|
+
return this.request({
|
|
851
|
+
method: 'POST',
|
|
852
|
+
path: `/open-platform/platform/tenants/${encodeURIComponent(tenantId)}/admin-invites`,
|
|
853
|
+
body: input,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
794
856
|
async toggleGrant(grantId, status) {
|
|
795
857
|
return this.request({
|
|
796
858
|
method: 'POST',
|
|
@@ -811,5 +873,15 @@ class PlatformApi extends BaseApi {
|
|
|
811
873
|
body: { ...notify, ...request, targetTenantId },
|
|
812
874
|
});
|
|
813
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* 重发认领凭证(平台级):租户 7 天内未认领时,服务商重新签发 claimToken。
|
|
878
|
+
* **重发即作废**——epoch 递增,旧 token 立即失效;仅 pending 租户可重发。
|
|
879
|
+
*/
|
|
880
|
+
async reissueClaimToken(tenantId) {
|
|
881
|
+
return this.request({
|
|
882
|
+
method: 'POST',
|
|
883
|
+
path: `/open-platform/platform/tenants/${encodeURIComponent(tenantId)}/claim-token`,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
814
886
|
}
|
|
815
887
|
exports.PlatformApi = PlatformApi;
|