@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 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): Promise<ConversationRecord[]>;
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
- const messages = await this.listMessages({
513
- conversationId: options.conversationId,
514
- limit,
515
- afterSequence,
516
- });
517
- const maxSequence = messages.reduce((current, message) => {
518
- const sequence = typeof message.sequence === 'number' && Number.isFinite(message.sequence)
519
- ? Math.floor(message.sequence)
520
- : current;
521
- return Math.max(current, sequence);
522
- }, lastSequence);
523
- lastSequence = Math.max(lastSequence, maxSequence);
524
- await options.onMessages({ reason, messages, afterSequence });
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
  }
@@ -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): Promise<ConversationRecord[]>;
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
- 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 });
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthen/sdk-core",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "AppThen Conversation Hub SDK - platform-agnostic core",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",