@senguoyun/dsh-arkme 0.1.14 → 0.1.16

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.
Files changed (164) hide show
  1. package/assets/desktop_call/avatar-lin-xiaoman.jpeg +0 -0
  2. package/assets/desktop_call/avatar-mother.jpg +0 -0
  3. package/assets/desktop_call/avatar-self.png +0 -0
  4. package/assets/desktop_call/call-add-linear.svg +13 -0
  5. package/assets/desktop_call/icon-scan-add-contact.svg +4 -0
  6. package/assets/desktop_call/profile-circle-linear.svg +11 -0
  7. package/assets/desktop_call/user-add-linear.svg +15 -0
  8. package/lib/client.js +18343 -10390
  9. package/lib/client.js.map +1 -1
  10. package/lib/extension-profile-restart-helper.js +2 -2
  11. package/lib/index.js +713 -87
  12. package/lib/{install-store-Sn3Ccxfu.js → install-store-Beau1y-o.js} +12 -6
  13. package/lib/persistent-extension.js +1 -1
  14. package/lib/{persistent-runtime-BAArZI-P.js → persistent-runtime-D_hNgTX3.js} +1001 -142
  15. package/lib/{plugin-updater-helper-CiPlNqBc.js → plugin-updater-helper-BEYBTOld.js} +5 -5
  16. package/lib/plugin-updater-helper.js +1 -1
  17. package/lib/{profile-package-manager-DvzGgkqe.js → profile-package-manager-DwuQu8ZL.js} +91 -3
  18. package/lib/sdk.js +23 -2
  19. package/lib/types/arkme-service.d.ts +49 -1
  20. package/lib/types/arkme-service.d.ts.map +1 -1
  21. package/lib/types/client/ArkmeArkoSurface.d.ts.map +1 -1
  22. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  23. package/lib/types/client/ArkmeCalendarSurface.d.ts.map +1 -1
  24. package/lib/types/client/ArkmeCallHistorySurface.d.ts +57 -0
  25. package/lib/types/client/ArkmeCallHistorySurface.d.ts.map +1 -0
  26. package/lib/types/client/ArkmeChatMemberActions.d.ts +48 -0
  27. package/lib/types/client/ArkmeChatMemberActions.d.ts.map +1 -0
  28. package/lib/types/client/ArkmeExtensionReviews.d.ts +4 -2
  29. package/lib/types/client/ArkmeExtensionReviews.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
  31. package/lib/types/client/ArkmeGroupChatControls.d.ts +3 -1
  32. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  33. package/lib/types/client/ArkmeMarketplace.d.ts +18 -4
  34. package/lib/types/client/ArkmeMarketplace.d.ts.map +1 -1
  35. package/lib/types/client/ArkmeMentionTextarea.d.ts +13 -0
  36. package/lib/types/client/ArkmeMentionTextarea.d.ts.map +1 -0
  37. package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
  38. package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
  39. package/lib/types/client/ArkmeRichContent.d.ts +7 -1
  40. package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
  41. package/lib/types/client/ArkmeSidebar.d.ts +2 -0
  42. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  43. package/lib/types/client/ArkmeStartupAuthGate.d.ts +4 -2
  44. package/lib/types/client/ArkmeStartupAuthGate.d.ts.map +1 -1
  45. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +6 -1
  46. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  47. package/lib/types/client/ArkmeVoiceprintSurface.d.ts +61 -0
  48. package/lib/types/client/ArkmeVoiceprintSurface.d.ts.map +1 -0
  49. package/lib/types/client/ArkmeWorldSurface.d.ts +43 -2
  50. package/lib/types/client/ArkmeWorldSurface.d.ts.map +1 -1
  51. package/lib/types/client/api.d.ts +1 -1
  52. package/lib/types/client/api.d.ts.map +1 -1
  53. package/lib/types/client/composer-draft-store.d.ts +18 -0
  54. package/lib/types/client/composer-draft-store.d.ts.map +1 -1
  55. package/lib/types/client/composer-focus.d.ts +8 -0
  56. package/lib/types/client/composer-focus.d.ts.map +1 -0
  57. package/lib/types/client/conversation-composer-presentation.d.ts +47 -0
  58. package/lib/types/client/conversation-composer-presentation.d.ts.map +1 -0
  59. package/lib/types/client/conversation-memory-cache.d.ts +30 -1
  60. package/lib/types/client/conversation-memory-cache.d.ts.map +1 -1
  61. package/lib/types/client/demo-audio.d.ts +2 -0
  62. package/lib/types/client/demo-audio.d.ts.map +1 -0
  63. package/lib/types/client/index.d.ts +2 -1
  64. package/lib/types/client/index.d.ts.map +1 -1
  65. package/lib/types/client/interwoven-moments.d.ts +2 -1
  66. package/lib/types/client/interwoven-moments.d.ts.map +1 -1
  67. package/lib/types/client/marketplace-sort-preference.d.ts +5 -0
  68. package/lib/types/client/marketplace-sort-preference.d.ts.map +1 -0
  69. package/lib/types/client/navigation-cache.d.ts +4 -0
  70. package/lib/types/client/navigation-cache.d.ts.map +1 -1
  71. package/lib/types/client/provider-instance-runtime.d.ts +20 -0
  72. package/lib/types/client/provider-instance-runtime.d.ts.map +1 -0
  73. package/lib/types/client/redesign/ArkmeRootFrame.d.ts +1 -1
  74. package/lib/types/client/redesign/ArkmeRootFrame.d.ts.map +1 -1
  75. package/lib/types/client/redesign/styles.d.ts.map +1 -1
  76. package/lib/types/client/related-recordings.d.ts +1 -0
  77. package/lib/types/client/related-recordings.d.ts.map +1 -1
  78. package/lib/types/client/source-list.d.ts +4 -0
  79. package/lib/types/client/source-list.d.ts.map +1 -1
  80. package/lib/types/client/ui-controller.d.ts +3 -1
  81. package/lib/types/client/ui-controller.d.ts.map +1 -1
  82. package/lib/types/client/voiceprint-enrollment-client.d.ts +12 -0
  83. package/lib/types/client/voiceprint-enrollment-client.d.ts.map +1 -0
  84. package/lib/types/client/voiceprint-recorder.d.ts +23 -0
  85. package/lib/types/client/voiceprint-recorder.d.ts.map +1 -0
  86. package/lib/types/client/world-voiceprint-playback.d.ts +41 -0
  87. package/lib/types/client/world-voiceprint-playback.d.ts.map +1 -0
  88. package/lib/types/extensions/install-store.d.ts +1 -0
  89. package/lib/types/extensions/install-store.d.ts.map +1 -1
  90. package/lib/types/extensions/manager.d.ts +2 -0
  91. package/lib/types/extensions/manager.d.ts.map +1 -1
  92. package/lib/types/extensions/owned-inventory.d.ts +3 -1
  93. package/lib/types/extensions/owned-inventory.d.ts.map +1 -1
  94. package/lib/types/extensions/owned-store.d.ts +1 -0
  95. package/lib/types/extensions/owned-store.d.ts.map +1 -1
  96. package/lib/types/extensions/profile-installer.d.ts.map +1 -1
  97. package/lib/types/extensions/publish-client.d.ts +2 -0
  98. package/lib/types/extensions/publish-client.d.ts.map +1 -1
  99. package/lib/types/host-api.d.ts +1 -1
  100. package/lib/types/host-api.d.ts.map +1 -1
  101. package/lib/types/index.d.ts.map +1 -1
  102. package/lib/types/local-database.d.ts +1 -0
  103. package/lib/types/local-database.d.ts.map +1 -1
  104. package/lib/types/openclaw/secret-provider.d.ts.map +1 -1
  105. package/lib/types/outgoing-call-assets.d.ts.map +1 -1
  106. package/lib/types/plugin-update-install-state.d.ts.map +1 -1
  107. package/lib/types/plugin-update-state.d.ts.map +1 -1
  108. package/lib/types/plugin-update.d.ts +6 -0
  109. package/lib/types/plugin-update.d.ts.map +1 -1
  110. package/lib/types/private-filesystem.d.ts +5 -0
  111. package/lib/types/private-filesystem.d.ts.map +1 -0
  112. package/lib/types/sdk/index.d.ts +11 -2
  113. package/lib/types/sdk/index.d.ts.map +1 -1
  114. package/lib/types/services/chat-realtime-service.d.ts +1 -1
  115. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  116. package/lib/types/services/chat-service.d.ts +21 -2
  117. package/lib/types/services/chat-service.d.ts.map +1 -1
  118. package/lib/types/services/contact-service.d.ts +2 -0
  119. package/lib/types/services/contact-service.d.ts.map +1 -1
  120. package/lib/types/services/group-service.d.ts.map +1 -1
  121. package/lib/types/services/service.d.ts +5 -2
  122. package/lib/types/services/service.d.ts.map +1 -1
  123. package/lib/types/services/source-service.d.ts.map +1 -1
  124. package/lib/types/services/voiceprint-service.d.ts +79 -0
  125. package/lib/types/services/voiceprint-service.d.ts.map +1 -0
  126. package/lib/types/services/world-service.d.ts +2 -1
  127. package/lib/types/services/world-service.d.ts.map +1 -1
  128. package/lib/types/state-store.d.ts.map +1 -1
  129. package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
  130. package/lib/types/tools/business/conversation/member-records.d.ts +3 -0
  131. package/lib/types/tools/business/conversation/member-records.d.ts.map +1 -0
  132. package/lib/types/tools/business/index.d.ts.map +1 -1
  133. package/lib/types/tools/business/voiceprint/grants.d.ts +2 -0
  134. package/lib/types/tools/business/voiceprint/grants.d.ts.map +1 -0
  135. package/lib/types/tools/business/voiceprint/index.d.ts +3 -0
  136. package/lib/types/tools/business/voiceprint/index.d.ts.map +1 -0
  137. package/lib/types/tools/business/voiceprint/invite.d.ts +2 -0
  138. package/lib/types/tools/business/voiceprint/invite.d.ts.map +1 -0
  139. package/lib/types/tools/business/voiceprint/recognized-invite.d.ts +2 -0
  140. package/lib/types/tools/business/voiceprint/recognized-invite.d.ts.map +1 -0
  141. package/lib/types/tools/business/voiceprint/recognized-people.d.ts +2 -0
  142. package/lib/types/tools/business/voiceprint/recognized-people.d.ts.map +1 -0
  143. package/lib/types/tools/business/voiceprint/restore.d.ts +2 -0
  144. package/lib/types/tools/business/voiceprint/restore.d.ts.map +1 -0
  145. package/lib/types/tools/business/voiceprint/revoke.d.ts +2 -0
  146. package/lib/types/tools/business/voiceprint/revoke.d.ts.map +1 -0
  147. package/lib/types/tools/business/voiceprint/status.d.ts +2 -0
  148. package/lib/types/tools/business/voiceprint/status.d.ts.map +1 -0
  149. package/lib/types/tools/extensions/index.d.ts.map +1 -1
  150. package/lib/types/tools/ports/conversations.d.ts +10 -1
  151. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  152. package/lib/types/tools/ports/index.d.ts +3 -2
  153. package/lib/types/tools/ports/index.d.ts.map +1 -1
  154. package/lib/types/tools/ports/voiceprint.d.ts +37 -0
  155. package/lib/types/tools/ports/voiceprint.d.ts.map +1 -0
  156. package/lib/types/tools/prompts/business.d.ts.map +1 -1
  157. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  158. package/lib/types/types.d.ts +113 -2
  159. package/lib/types/types.d.ts.map +1 -1
  160. package/lib/types/voiceprint-routes.d.ts +11 -0
  161. package/lib/types/voiceprint-routes.d.ts.map +1 -0
  162. package/lib/types/voiceprint-wav.d.ts +6 -0
  163. package/lib/types/voiceprint-wav.d.ts.map +1 -0
  164. package/package.json +4 -4
@@ -815,15 +815,16 @@ var ServiceRuntime = class {
815
815
  else signal.addEventListener("abort", abort, { once: true });
816
816
  const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
817
817
  try {
818
+ const multipart = body instanceof FormData;
818
819
  const response = await this.fetchImpl(joinUrl(baseUrl, path), {
819
820
  method: "POST",
820
821
  headers: {
821
- "Content-Type": "application/json",
822
+ ...multipart ? {} : { "Content-Type": "application/json" },
822
823
  "Accept-Language": "zh-CN",
823
824
  Usersource: "3",
824
825
  ...bearer === void 0 ? {} : { Authorization: `Bearer ${bearer}` }
825
826
  },
826
- body: JSON.stringify(body),
827
+ body: multipart ? body : JSON.stringify(body),
827
828
  signal: controller.signal
828
829
  });
829
830
  if (response.status === 401 || response.status === 403 && !preserveForbiddenError) throw new ArkmePluginError(`auth-http-${response.status}`, "Arkme 登录凭据已失效", false, response.status);
@@ -1033,6 +1034,17 @@ var ServiceRuntime = class {
1033
1034
  return await this.post(this.config.audioBaseUrl, path, body, session.accessToken, [200], signal, false, requestOptions());
1034
1035
  }
1035
1036
  }
1037
+ async authenticatedAudioMultipartPost(path, body, initialSession, signal, options = {}) {
1038
+ let session = initialSession ?? await this.requireSession();
1039
+ const requestOptions = () => this.authenticatedRequestOptions(session, "audio", "write", options);
1040
+ try {
1041
+ return await this.post(this.config.audioBaseUrl, path, body, session.accessToken, [200], signal, false, requestOptions());
1042
+ } catch (error) {
1043
+ if (!(error instanceof ArkmePluginError) || !["auth-http-401", "auth-http-403"].includes(error.code)) throw error;
1044
+ session = await this.refreshAccessToken(session);
1045
+ return await this.post(this.config.audioBaseUrl, path, body, session.accessToken, [200], signal, false, requestOptions());
1046
+ }
1047
+ }
1036
1048
  async authenticatedRelationPost(path, body, initialSession, signal, options = {}) {
1037
1049
  let session = initialSession ?? await this.requireSession();
1038
1050
  const requestOptions = () => this.authenticatedRequestOptions(session, "relation", "interactive-read", options);
@@ -2482,10 +2494,10 @@ function booleanValue$10(value) {
2482
2494
  function listValue$18(value) {
2483
2495
  return Array.isArray(value) ? value : [];
2484
2496
  }
2485
- function encodeOpaqueJson$6(value) {
2497
+ function encodeOpaqueJson$7(value) {
2486
2498
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
2487
2499
  }
2488
- function decodeOpaqueJson$6(value) {
2500
+ function decodeOpaqueJson$7(value) {
2489
2501
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
2490
2502
  }
2491
2503
  var BotService = class {
@@ -2677,7 +2689,7 @@ var BotService = class {
2677
2689
  };
2678
2690
  }
2679
2691
  async sealBotRef(userId, botId) {
2680
- const payload = encodeOpaqueJson$6({
2692
+ const payload = encodeOpaqueJson$7({
2681
2693
  version: 1,
2682
2694
  userId,
2683
2695
  botId
@@ -2693,7 +2705,7 @@ var BotService = class {
2693
2705
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
2694
2706
  let raw;
2695
2707
  try {
2696
- raw = objectValue$1(decodeOpaqueJson$6(payload));
2708
+ raw = objectValue$1(decodeOpaqueJson$7(payload));
2697
2709
  } catch (error) {
2698
2710
  throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false, 400, { cause: error });
2699
2711
  }
@@ -3477,7 +3489,7 @@ var ChatRealtimeService = class {
3477
3489
  lane: "background-read",
3478
3490
  key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
3479
3491
  });
3480
- return [uid, await this.projectionReader.chatTimelineItems(data, session)];
3492
+ return [uid, await this.projectionReader.chatTimelineItems(data, session, uid)];
3481
3493
  }))).forEach((result, index) => {
3482
3494
  const uid = chunk[index]?.[0];
3483
3495
  if (uid === void 0) return;
@@ -3577,6 +3589,18 @@ function booleanValue$8(value) {
3577
3589
  function listValue$15(value) {
3578
3590
  return Array.isArray(value) ? value : [];
3579
3591
  }
3592
+ function chatMemberRole$1(value) {
3593
+ if (value === "owner" || value === 1) return "owner";
3594
+ if (value === "admin" || value === 2) return "admin";
3595
+ if (value === "member" || value === "participant" || value === 3) return "member";
3596
+ return "unknown";
3597
+ }
3598
+ function chatMemberStatus$1(value) {
3599
+ if (value === "active" || value === 1) return "active";
3600
+ if (value === "left" || value === 2) return "left";
3601
+ if (value === "removed" || value === 3) return "removed";
3602
+ return "unknown";
3603
+ }
3580
3604
  function integerLikeValue(value) {
3581
3605
  if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
3582
3606
  if (typeof value === "string" && value.trim() !== "") {
@@ -3620,10 +3644,10 @@ function timelineAgentSource(relation, record, payload) {
3620
3644
  label: agentSourceLabel(displayName)
3621
3645
  };
3622
3646
  }
3623
- function encodeOpaqueJson$5(value) {
3647
+ function encodeOpaqueJson$6(value) {
3624
3648
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
3625
3649
  }
3626
- function decodeOpaqueJson$5(value) {
3650
+ function decodeOpaqueJson$6(value) {
3627
3651
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
3628
3652
  }
3629
3653
  var ChatService = class {
@@ -3647,6 +3671,65 @@ var ChatService = class {
3647
3671
  this.aiPolish = aiPolish;
3648
3672
  this.realtime = realtime;
3649
3673
  }
3674
+ async listSourceMembers(sourceRef, options = {}) {
3675
+ const session = await this.runtime.requireSession();
3676
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
3677
+ if (source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("chat-members-source-invalid", "仅支持查看群聊或私聊成员", false);
3678
+ const rawItems = await this.rawChatMembers(source.ownerRef, options.activeOnly !== false, session, options.signal);
3679
+ const members = await this.projectChatMembers(source.ownerRef, rawItems, session, options.signal);
3680
+ return {
3681
+ source: await this.source.sourceItem(source),
3682
+ items: members,
3683
+ total: members.length,
3684
+ activeCount: members.filter((item) => item.status === "active").length
3685
+ };
3686
+ }
3687
+ async sourceMemberRecords(sourceRef, memberRef, mode, options = {}) {
3688
+ const session = await this.runtime.requireSession();
3689
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
3690
+ if (source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("chat-members-source-invalid", "仅支持查看群聊或私聊成员快记", false);
3691
+ if (mode !== "owner" && mode !== "mentioned") throw new ArkmePluginError("chat-member-record-mode-invalid", "成员快记模式无效", false);
3692
+ const reference = await this.openChatMemberRef(memberRef, session.userId, source.ownerRef);
3693
+ const rawMembers = await this.rawChatMembers(source.ownerRef, false, session, options.signal);
3694
+ const member = (await this.projectChatMembers(source.ownerRef, rawMembers, session, options.signal)).find((item) => item.memberRef === memberRef);
3695
+ if (member === void 0) throw new ArkmePluginError("chat-member-ref-stale", "该成员已不属于当前会话,请刷新后重试", false, 409);
3696
+ const limit = Math.min(50, Math.max(1, Math.trunc(options.limit ?? 30)));
3697
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/members/records/page", {
3698
+ chat_session_uid: source.ownerRef,
3699
+ member_user_id: reference.targetUserId,
3700
+ mode: mode === "owner" ? "sent" : "mentioned",
3701
+ before_seq: Math.max(0, Math.trunc(options.beforeSequence ?? 0)),
3702
+ limit
3703
+ }, session, options.signal, {
3704
+ lane: "interactive-read",
3705
+ key: `member-records:${source.ownerRef}:${String(reference.targetUserId)}:${mode}:${String(Math.max(0, Math.trunc(options.beforeSequence ?? 0)))}`,
3706
+ failureCooldownMs: 2e3
3707
+ });
3708
+ const items = await this.projectChatTimelineItems(listValue$15(data.items), source, session, options.signal);
3709
+ const nextBeforeSequence = Math.max(0, Math.trunc(numberValue$16(data.next_before_seq)));
3710
+ return {
3711
+ source: await this.source.sourceItem(source),
3712
+ member,
3713
+ mode,
3714
+ items,
3715
+ hasMore: data.has_more === true,
3716
+ ...nextBeforeSequence > 0 ? { nextCursor: { beforeSequence: nextBeforeSequence } } : {}
3717
+ };
3718
+ }
3719
+ async openPrivateChatFromMember(sourceRef, memberRef, options = {}) {
3720
+ const session = await this.runtime.requireSession();
3721
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
3722
+ if (source.kind !== "group_chat" && source.kind !== "private_chat") throw new ArkmePluginError("chat-members-source-invalid", "仅支持从聊天成员发起私聊", false);
3723
+ const reference = await this.openChatMemberRef(memberRef, session.userId, source.ownerRef);
3724
+ if (reference.targetUserId === session.userId) throw new ArkmePluginError("private-chat-self-invalid", "不能给自己发起私聊", false, 409);
3725
+ const rawMember = (await this.rawChatMembers(source.ownerRef, true, session, options.signal)).find((item) => Math.trunc(numberValue$16(item.user_id)) === reference.targetUserId);
3726
+ if (rawMember === void 0) throw new ArkmePluginError("chat-member-ref-stale", "该成员已不属于当前会话,请刷新后重试", false, 409);
3727
+ const displayName = stringValue$1(rawMember.remark).trim() || stringValue$1(rawMember.display_name_snapshot).trim() || "群成员";
3728
+ return await this.openPrivateChatFromUser(reference.targetUserId, {
3729
+ displayName,
3730
+ ...options.signal === void 0 ? {} : { signal: options.signal }
3731
+ });
3732
+ }
3650
3733
  async openPrivateChatFromUser(peerUserId, options = {}) {
3651
3734
  const session = await this.runtime.requireSession();
3652
3735
  if (!Number.isSafeInteger(peerUserId) || peerUserId <= 0) throw new ArkmePluginError("private-chat-peer-invalid", "私聊用户参数无效", false);
@@ -3771,56 +3854,7 @@ var ChatService = class {
3771
3854
  key: `timeline:${source.ownerRef}:${String(Math.max(0, Math.trunc(options.cursor?.beforeSequence ?? 0)))}:${String(limit)}`,
3772
3855
  failureCooldownMs: 2e3
3773
3856
  });
3774
- const items = [];
3775
- const senderUserIdByIndex = /* @__PURE__ */ new Map();
3776
- const messageRefSigningKey = source.kind === "group_chat" ? await this.runtime.stateStore.uniqueCode() : void 0;
3777
- for (const raw of listValue$15(data.items)) {
3778
- const item = objectValue$1(raw);
3779
- const relation = objectValue$1(item.relation);
3780
- const record = objectValue$1(item.record);
3781
- const payload = objectValue$1(record.payload);
3782
- const recordStatus = numberValue$16(record.status);
3783
- if (recordStatus !== 1) continue;
3784
- const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
3785
- if (uid === "") continue;
3786
- const relationUid = stringValue$1(relation.rel_uid).trim();
3787
- const senderUserId = numberValue$16(relation.sender_user_id);
3788
- const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
3789
- const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
3790
- const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
3791
- const rawAgentSource = timelineAgentSource(relation, record, payload);
3792
- const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
3793
- const itemIndex = items.push({
3794
- itemUid: uid,
3795
- ...source.kind !== "group_chat" || relationUid === "" || senderUserId === session.userId ? {} : { messageRef: this.sealMessageRef(session.userId, source.ownerRef, relationUid, messageRefSigningKey) },
3796
- senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
3797
- ...agentSource === void 0 ? {} : { agentSource },
3798
- isMe: senderUserId === session.userId,
3799
- sendAtMillis,
3800
- title: stringValue$1(payload.title),
3801
- textContent: stringValue$1(payload.text_content),
3802
- status: recordStatus,
3803
- sequence: numberValue$16(relation.seq),
3804
- ...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
3805
- ...aiPolish === void 0 ? {} : { aiPolish },
3806
- ...forwardRecords === void 0 ? {} : { forwardRecords },
3807
- templateKind: numberValue$16(payload.template_kind),
3808
- displayKind: numberValue$16(payload.display_kind),
3809
- version: numberValue$16(payload.version ?? record.version),
3810
- updateAtMillis: numberValue$16(payload.update_at ?? record.update_at),
3811
- recordDurationMillis: numberValue$16(payload.record_duration_millis),
3812
- editDurationMillis: numberValue$16(payload.edit_duration_millis),
3813
- contentBlocks: this.media.richContentBlocks(item, session.userId)
3814
- }) - 1;
3815
- if (Number.isSafeInteger(senderUserId) && senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
3816
- }
3817
- try {
3818
- const profiles = await this.profile.publicProfilesByUserIds([...new Set(senderUserIdByIndex.values())], session, options.signal);
3819
- for (const [index, senderUserId] of senderUserIdByIndex) {
3820
- if (!profiles.has(senderUserId) || items[index] === void 0) continue;
3821
- items[index].avatarRef = await this.profile.sealProfileImageRef(session.userId, senderUserId);
3822
- }
3823
- } catch {}
3857
+ const items = await this.projectChatTimelineItems(listValue$15(data.items), source, session, options.signal);
3824
3858
  const beforeSequence = numberValue$16(data.next_before_seq);
3825
3859
  let aiPolishSettings;
3826
3860
  let aiPolishNotices;
@@ -3871,6 +3905,7 @@ var ChatService = class {
3871
3905
  const recordUid = options.recordUid?.trim() || randomUUID();
3872
3906
  if (source.kind === "send_to_self" || source.kind === "default_category") {
3873
3907
  const result = await this.record.createTextForConversation(recordUid, text);
3908
+ if (result.localState !== "failed") this.source.invalidateSourceListCache(session.userId, "send_to_self");
3874
3909
  return {
3875
3910
  sourceRef,
3876
3911
  itemUid: result.recordUid,
@@ -3888,6 +3923,7 @@ var ChatService = class {
3888
3923
  text_content: text,
3889
3924
  send_at: Date.now()
3890
3925
  }, session);
3926
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
3891
3927
  return {
3892
3928
  sourceRef,
3893
3929
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
@@ -3898,16 +3934,68 @@ var ChatService = class {
3898
3934
  const relationUid = options.relationUid?.trim() || randomUUID();
3899
3935
  const agentAuthored = isAgentAuthoredChatSend(options);
3900
3936
  let sent;
3901
- if (source.kind === "group_chat") if (options.botRefs !== void 0 && options.botRefs.length > 0) sent = await this.groupBotMentionSend(sourceRef, source, text, options.botRefs, recordUid, relationUid, session, options.signal, { agentAuthored });
3902
- else sent = await this.aiPolish.sendGroupSourceTextWithAiPolish(sourceRef, source.ownerRef, text, recordUid, relationUid, session, {
3903
- agentAuthored,
3904
- ...options.signal === void 0 ? {} : { signal: options.signal }
3905
- });
3906
- else if (options.botRefs !== void 0 && options.botRefs.length > 0) throw new ArkmePluginError("bot-mention-group-required", "Bot mention 只能发送到群聊", false);
3937
+ if (source.kind === "group_chat") {
3938
+ if ((options.botRefs?.length ?? 0) > 0 && (options.humanMentions?.length ?? 0) > 0) throw new ArkmePluginError("mention-kind-conflict", "单条消息暂不支持同时新增真人和 Bot mention", false);
3939
+ if (options.botRefs !== void 0 && options.botRefs.length > 0) sent = await this.groupBotMentionSend(sourceRef, source, text, options.botRefs, recordUid, relationUid, session, options.signal, { agentAuthored });
3940
+ else if (options.humanMentions !== void 0 && options.humanMentions.length > 0) {
3941
+ const contentPayload = await this.humanMentionContentPayload(source, textContent, text, options.humanMentions, session, options.signal);
3942
+ sent = await this.sendChatSourceTextRaw(sourceRef, source.ownerRef, text, recordUid, relationUid, session, void 0, contentPayload, options.signal, { agentAuthored });
3943
+ } else sent = await this.aiPolish.sendGroupSourceTextWithAiPolish(sourceRef, source.ownerRef, text, recordUid, relationUid, session, {
3944
+ agentAuthored,
3945
+ ...options.signal === void 0 ? {} : { signal: options.signal }
3946
+ });
3947
+ } else if ((options.botRefs?.length ?? 0) > 0 || (options.humanMentions?.length ?? 0) > 0) throw new ArkmePluginError("mention-group-required", "Mention 只能发送到群聊", false);
3907
3948
  else sent = await this.sendChatSourceTextRaw(sourceRef, source.ownerRef, text, recordUid, relationUid, session, void 0, void 0, options.signal, { agentAuthored });
3908
3949
  if (agentAuthored && sent.localState === "synced") this.arko.agentSourceDisplayName(session);
3909
3950
  return sent;
3910
3951
  }
3952
+ async humanMentionContentPayload(source, rawText, normalizedText, inputs, session, signal) {
3953
+ if (inputs.length > 50) throw new ArkmePluginError("human-mention-invalid", "单条消息 mention 数量过多", false);
3954
+ const leadingTrim = rawText.length - rawText.trimStart().length;
3955
+ const rawMembers = await this.rawChatMembers(source.ownerRef, true, session, signal);
3956
+ const membersByUserId = new Map(rawMembers.map((item) => [Math.trunc(numberValue$16(item.user_id)), item]));
3957
+ const mentions = [];
3958
+ for (const input of [...inputs].sort((left, right) => left.startIndex - right.startIndex)) {
3959
+ const memberRef = input.memberRef.trim();
3960
+ const startIndex = Math.trunc(input.startIndex) - leadingTrim;
3961
+ const length = Math.trunc(input.length);
3962
+ if (memberRef === "" || startIndex < 0 || length < 2 || startIndex + length > normalizedText.length) throw new ArkmePluginError("human-mention-invalid", "真人 mention 引用或文本区间无效", false);
3963
+ const reference = await this.openChatMemberRef(memberRef, session.userId, source.ownerRef);
3964
+ if (reference.targetUserId === session.userId) throw new ArkmePluginError("human-mention-self-invalid", "不能 @ 自己", false);
3965
+ const rawMember = membersByUserId.get(reference.targetUserId);
3966
+ if (rawMember === void 0) throw new ArkmePluginError("chat-member-ref-stale", "被 @ 成员已不在当前群聊", false, 409);
3967
+ const visible = normalizedText.slice(startIndex, startIndex + length);
3968
+ const displayName = visible.startsWith("@") ? visible.slice(1).trim() : "";
3969
+ const expectedNames = [stringValue$1(rawMember.remark).trim(), stringValue$1(rawMember.display_name_snapshot).trim()].filter((value) => value !== "");
3970
+ if (displayName === "" || !expectedNames.includes(displayName)) throw new ArkmePluginError("human-mention-text-mismatch", "真人 mention 文本已变化,请重新选择成员", false, 409);
3971
+ const previous = mentions.at(-1);
3972
+ if (previous !== void 0 && previous.start_index + previous.length > startIndex) throw new ArkmePluginError("human-mention-overlap", "真人 mention 文本区间重叠", false);
3973
+ mentions.push({
3974
+ user_id: reference.targetUserId,
3975
+ display_name_snapshot: displayName,
3976
+ start_index: startIndex,
3977
+ length
3978
+ });
3979
+ }
3980
+ const checksumInput = {
3981
+ text_content: normalizedText,
3982
+ human_mentions: mentions.map((mention) => ({
3983
+ user_id: mention.user_id,
3984
+ start_index: mention.start_index,
3985
+ length: mention.length
3986
+ })),
3987
+ bot_mentions: []
3988
+ };
3989
+ return {
3990
+ payload_kind: 2,
3991
+ schema_version: 1,
3992
+ mention_metadata: {
3993
+ schema_version: 1,
3994
+ source_checksum: createHash("sha256").update(JSON.stringify(checksumInput)).digest("hex"),
3995
+ human_mentions: mentions
3996
+ }
3997
+ };
3998
+ }
3911
3999
  async groupBotMentionSend(sourceRef, source, text, botRefs, recordUid, relationUid, session, signal, options = {}) {
3912
4000
  const uniqueRefs = new Set(botRefs.map((ref) => ref.trim()));
3913
4001
  if (uniqueRefs.has("") || uniqueRefs.size !== botRefs.length) throw new ArkmePluginError("bot-mention-ref-invalid", "Bot mention 引用为空或重复", false);
@@ -4006,7 +4094,7 @@ var ChatService = class {
4006
4094
  const recordUid = options.recordUid?.trim() || randomUUID();
4007
4095
  const relationUid = options.relationUid?.trim() || randomUUID();
4008
4096
  const templateKind = longArticle ? 1 : assets.length === 0 ? 1 : 2;
4009
- const contentPayload = assets.length === 0 ? void 0 : {
4097
+ const mediaContentPayload = assets.length === 0 ? void 0 : {
4010
4098
  payload_kind: 2,
4011
4099
  schema_version: 1,
4012
4100
  text_state: textContent === "" ? 3 : 1,
@@ -4018,6 +4106,16 @@ var ChatService = class {
4018
4106
  file_name: asset.fileName
4019
4107
  }))
4020
4108
  };
4109
+ let contentPayload = mediaContentPayload;
4110
+ if ((input.humanMentions?.length ?? 0) > 0) {
4111
+ if (source.kind !== "group_chat") throw new ArkmePluginError("mention-group-required", "Mention 只能发送到群聊", false);
4112
+ if (longArticle) throw new ArkmePluginError("human-mention-rich-invalid", "长文暂不支持成员 mention", false);
4113
+ const mentionPayload = await this.humanMentionContentPayload(source, input.textContent ?? "", textContent, input.humanMentions, session);
4114
+ contentPayload = {
4115
+ ...mediaContentPayload ?? {},
4116
+ ...mentionPayload
4117
+ };
4118
+ }
4021
4119
  const commonBody = {
4022
4120
  record_uid: recordUid,
4023
4121
  template_kind: templateKind,
@@ -4030,6 +4128,7 @@ var ChatService = class {
4030
4128
  };
4031
4129
  if (source.kind === "send_to_self" || source.kind === "default_category") {
4032
4130
  const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session);
4131
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
4033
4132
  return {
4034
4133
  sourceRef,
4035
4134
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
@@ -4042,6 +4141,7 @@ var ChatService = class {
4042
4141
  topic_uid: source.ownerRef,
4043
4142
  ...commonBody
4044
4143
  }, session);
4144
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
4045
4145
  return {
4046
4146
  sourceRef,
4047
4147
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
@@ -4165,7 +4265,7 @@ var ChatService = class {
4165
4265
  async chatSourceFromBundle(bundle, session, cached, timelineItems) {
4166
4266
  return await this.source.chatSourceFromBundle(bundle, session, cached, timelineItems);
4167
4267
  }
4168
- async chatTimelineItems(data, session) {
4268
+ async chatTimelineItems(data, session, chatSessionUid) {
4169
4269
  const items = [];
4170
4270
  for (const raw of listValue$15(data.items)) {
4171
4271
  const item = objectValue$1(raw);
@@ -4182,6 +4282,7 @@ var ChatService = class {
4182
4282
  const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
4183
4283
  items.push({
4184
4284
  itemUid: uid,
4285
+ ...senderUserId > 0 ? { memberRef: await this.sealChatMemberRef(session.userId, chatSessionUid, senderUserId) } : {},
4185
4286
  senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
4186
4287
  ...agentSource === void 0 ? {} : { agentSource },
4187
4288
  ...senderUserId > 0 ? { avatarRef: await this.profile.sealProfileImageRef(session.userId, senderUserId) } : {},
@@ -4249,8 +4350,143 @@ var ChatService = class {
4249
4350
  items: projectedItems
4250
4351
  };
4251
4352
  }
4353
+ async rawChatMembers(chatSessionUid, activeOnly, session, signal) {
4354
+ return listValue$15((await this.runtime.authenticatedChatPost("/api/v1/chats/members/list", {
4355
+ chat_session_uid: chatSessionUid,
4356
+ active_only: activeOnly
4357
+ }, session, signal)).items).map(objectValue$1);
4358
+ }
4359
+ async projectChatMembers(chatSessionUid, rawItems, session, signal) {
4360
+ const userIds = rawItems.map((item) => Math.trunc(numberValue$16(item.user_id))).filter((userId) => Number.isSafeInteger(userId) && userId > 0);
4361
+ const profiles = await this.profile.publicProfileSummariesByUserIds(userIds, session, signal).catch(() => /* @__PURE__ */ new Map());
4362
+ const members = [];
4363
+ for (const item of rawItems) {
4364
+ const userId = Math.trunc(numberValue$16(item.user_id));
4365
+ if (!Number.isSafeInteger(userId) || userId <= 0) continue;
4366
+ const profile = profiles.get(userId);
4367
+ const remarkName = stringValue$1(item.remark).trim();
4368
+ const memberName = stringValue$1(item.display_name_snapshot).trim();
4369
+ const profileDisplayName = profile?.displayName.trim() ?? "";
4370
+ const publicDisplayName = profileDisplayName === `用户 ${String(userId)}` ? "" : profileDisplayName;
4371
+ const displayName = [
4372
+ remarkName,
4373
+ memberName,
4374
+ publicDisplayName
4375
+ ].find((value) => value !== "" && value !== "成员" && value !== "群成员") ?? "群成员";
4376
+ const secondaryName = [
4377
+ memberName,
4378
+ publicDisplayName,
4379
+ remarkName
4380
+ ].find((value) => value !== "" && value !== "成员" && value !== "群成员" && value !== displayName) ?? "";
4381
+ const role = chatMemberRole$1(item.role);
4382
+ const status = chatMemberStatus$1(item.status);
4383
+ const extra = objectValue$1(item.extra);
4384
+ members.push({
4385
+ memberRef: await this.sealChatMemberRef(session.userId, chatSessionUid, userId),
4386
+ displayName,
4387
+ ...memberName === "" ? {} : { memberName },
4388
+ ...secondaryName === "" ? {} : { secondaryName },
4389
+ ...profile?.avatarUrl === void 0 ? {} : { avatarRef: await this.profile.sealProfileImageRef(session.userId, userId) },
4390
+ role,
4391
+ status,
4392
+ isSelf: userId === session.userId,
4393
+ isOwner: role === "owner",
4394
+ joinedAtMillis: Math.max(0, Math.trunc(numberValue$16(item.join_at))),
4395
+ recordCount: Math.max(0, Math.trunc(numberValue$16(extra.record_count))),
4396
+ mentionCount: Math.max(0, Math.trunc(numberValue$16(extra.mention_count)))
4397
+ });
4398
+ }
4399
+ const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
4400
+ members.sort((left, right) => roleRank(left.role) - roleRank(right.role) || (right.status === "active" ? 1 : 0) - (left.status === "active" ? 1 : 0) || left.joinedAtMillis - right.joinedAtMillis || left.displayName.localeCompare(right.displayName));
4401
+ return members;
4402
+ }
4403
+ async projectChatTimelineItems(rawItems, source, session, signal) {
4404
+ const items = [];
4405
+ const senderUserIdByIndex = /* @__PURE__ */ new Map();
4406
+ const signingKey = await this.runtime.stateStore.uniqueCode();
4407
+ for (const raw of rawItems) {
4408
+ const item = objectValue$1(raw);
4409
+ const relation = objectValue$1(item.relation);
4410
+ const record = objectValue$1(item.record);
4411
+ const payload = objectValue$1(record.payload);
4412
+ const recordStatus = numberValue$16(record.status);
4413
+ if (recordStatus !== 1) continue;
4414
+ const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
4415
+ if (uid === "") continue;
4416
+ const relationUid = stringValue$1(relation.rel_uid).trim();
4417
+ const senderUserId = Math.trunc(numberValue$16(relation.sender_user_id));
4418
+ const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
4419
+ const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
4420
+ const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
4421
+ const rawAgentSource = timelineAgentSource(relation, record, payload);
4422
+ const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
4423
+ const itemIndex = items.push({
4424
+ itemUid: uid,
4425
+ ...source.kind !== "group_chat" || relationUid === "" || senderUserId === session.userId ? {} : { messageRef: this.sealMessageRef(session.userId, source.ownerRef, relationUid, signingKey) },
4426
+ ...senderUserId > 0 ? { memberRef: await this.sealChatMemberRef(session.userId, source.ownerRef, senderUserId) } : {},
4427
+ senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
4428
+ ...agentSource === void 0 ? {} : { agentSource },
4429
+ isMe: senderUserId === session.userId,
4430
+ sendAtMillis,
4431
+ title: stringValue$1(payload.title),
4432
+ textContent: stringValue$1(payload.text_content),
4433
+ status: recordStatus,
4434
+ sequence: numberValue$16(relation.seq),
4435
+ ...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
4436
+ ...aiPolish === void 0 ? {} : { aiPolish },
4437
+ templateKind: numberValue$16(payload.template_kind),
4438
+ displayKind: numberValue$16(payload.display_kind),
4439
+ version: numberValue$16(payload.version ?? record.version),
4440
+ updateAtMillis: numberValue$16(payload.update_at ?? record.update_at),
4441
+ recordDurationMillis: numberValue$16(payload.record_duration_millis),
4442
+ editDurationMillis: numberValue$16(payload.edit_duration_millis),
4443
+ contentBlocks: this.media.richContentBlocks(item, session.userId),
4444
+ ...forwardRecords === void 0 ? {} : { forwardRecords }
4445
+ }) - 1;
4446
+ if (senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
4447
+ }
4448
+ try {
4449
+ const profiles = await this.profile.publicProfilesByUserIds([...new Set(senderUserIdByIndex.values())], session, signal);
4450
+ for (const [index, senderUserId] of senderUserIdByIndex) {
4451
+ if (!profiles.has(senderUserId) || items[index] === void 0) continue;
4452
+ items[index].avatarRef = await this.profile.sealProfileImageRef(session.userId, senderUserId);
4453
+ }
4454
+ } catch {}
4455
+ return items;
4456
+ }
4457
+ async sealChatMemberRef(viewerUserId, chatSessionUid, targetUserId) {
4458
+ const encoded = encodeOpaqueJson$6({
4459
+ version: 1,
4460
+ viewerUserId,
4461
+ chatSessionUid,
4462
+ targetUserId
4463
+ });
4464
+ return `arkme-chat-member-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
4465
+ }
4466
+ async openChatMemberRef(memberRef, expectedViewerUserId, expectedChatSessionUid) {
4467
+ const parts = memberRef.trim().split(".");
4468
+ if (parts.length !== 3 || parts[0] !== "arkme-chat-member-v1") throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用无效", false);
4469
+ const encoded = parts[1] ?? "";
4470
+ const supplied = Buffer.from(parts[2] ?? "", "base64url");
4471
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
4472
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用无效", false);
4473
+ let raw;
4474
+ try {
4475
+ raw = objectValue$1(decodeOpaqueJson$6(encoded));
4476
+ } catch (error) {
4477
+ throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用无效", false, 400, { cause: error });
4478
+ }
4479
+ const result = {
4480
+ version: 1,
4481
+ viewerUserId: Math.trunc(numberValue$16(raw.viewerUserId)),
4482
+ chatSessionUid: stringValue$1(raw.chatSessionUid).trim(),
4483
+ targetUserId: Math.trunc(numberValue$16(raw.targetUserId))
4484
+ };
4485
+ if (raw.version !== 1 || result.viewerUserId !== expectedViewerUserId || result.chatSessionUid !== expectedChatSessionUid || !Number.isSafeInteger(result.targetUserId) || result.targetUserId <= 0) throw new ArkmePluginError("chat-member-ref-invalid", "聊天成员引用与当前账号或会话不匹配", false, 403);
4486
+ return result;
4487
+ }
4252
4488
  sealMessageRef(userId, chatSessionUid, relationUid, signingKey) {
4253
- const payload = encodeOpaqueJson$5({
4489
+ const payload = encodeOpaqueJson$6({
4254
4490
  version: 1,
4255
4491
  userId,
4256
4492
  chatSessionUid,
@@ -4267,7 +4503,7 @@ var ChatService = class {
4267
4503
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false);
4268
4504
  let parsed;
4269
4505
  try {
4270
- parsed = objectValue$1(decodeOpaqueJson$5(payload));
4506
+ parsed = objectValue$1(decodeOpaqueJson$6(payload));
4271
4507
  } catch (error) {
4272
4508
  throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false, 400, { cause: error });
4273
4509
  }
@@ -4398,6 +4634,19 @@ var ContactService = class {
4398
4634
  source
4399
4635
  };
4400
4636
  }
4637
+ async resolveRegisteredContactUserId(contactRef, session, signal) {
4638
+ this.prune();
4639
+ const normalizedContactRef = contactRef.trim();
4640
+ if (!CONTACT_REF_PATTERN.test(normalizedContactRef)) throw new ArkmePluginError("contact-ref-invalid", "联系人搜索引用无效,请重新搜索", false);
4641
+ const cached = this.candidates.get(normalizedContactRef);
4642
+ if (cached === void 0 || cached.userId !== session.userId || cached.expiresAtMillis <= Date.now()) throw new ArkmePluginError("contact-ref-expired", "联系人搜索结果已过期,请重新搜索", false, 410);
4643
+ const candidate = await this.lookup(cached.normalized, session, signal, normalizedContactRef);
4644
+ if (candidate.targetUserId === void 0 || !candidate.result.registered || candidate.result.isSelf || candidate.targetUserId !== cached.targetUserId) {
4645
+ this.candidates.delete(normalizedContactRef);
4646
+ throw new ArkmePluginError("contact-candidate-changed", "联系人身份已发生变化,请重新搜索后确认", false, 409);
4647
+ }
4648
+ return candidate.targetUserId;
4649
+ }
4401
4650
  async lookup(normalized, session, signal, contactRef = `arkme-contact-v1.${randomUUID()}`) {
4402
4651
  const data = await this.runtime.authenticatedAuthPost("/api/v1/auth/search-contact-account", {
4403
4652
  identifier_type: normalized.identifierType,
@@ -5204,10 +5453,10 @@ function numberValue$12(value) {
5204
5453
  function listValue$12(value) {
5205
5454
  return Array.isArray(value) ? value : [];
5206
5455
  }
5207
- function encodeOpaqueJson$4(value) {
5456
+ function encodeOpaqueJson$5(value) {
5208
5457
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
5209
5458
  }
5210
- function decodeOpaqueJson$4(value) {
5459
+ function decodeOpaqueJson$5(value) {
5211
5460
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
5212
5461
  }
5213
5462
  function chatMessageDnd$1(value) {
@@ -5502,7 +5751,7 @@ var GroupService = class {
5502
5751
  };
5503
5752
  }
5504
5753
  async sealCandidateRef(payload) {
5505
- const encoded = encodeOpaqueJson$4(payload);
5754
+ const encoded = encodeOpaqueJson$5(payload);
5506
5755
  return `arkme-group-candidate-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
5507
5756
  }
5508
5757
  async openCandidateRef(candidateRef, expectedUserId, expectedGroupChatSessionUid) {
@@ -5514,7 +5763,7 @@ var GroupService = class {
5514
5763
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
5515
5764
  let raw;
5516
5765
  try {
5517
- raw = objectValue$1(decodeOpaqueJson$4(encoded));
5766
+ raw = objectValue$1(decodeOpaqueJson$5(encoded));
5518
5767
  } catch (error) {
5519
5768
  throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false, 400, { cause: error });
5520
5769
  }
@@ -5593,7 +5842,8 @@ var GroupService = class {
5593
5842
  isSelf: userId === session.userId,
5594
5843
  isOwner: role === "owner",
5595
5844
  joinedAtMillis: numberValue$12(item.join_at),
5596
- recordCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).record_count)))
5845
+ recordCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).record_count))),
5846
+ mentionCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).mention_count)))
5597
5847
  });
5598
5848
  }
5599
5849
  const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
@@ -6799,10 +7049,10 @@ function chunksOf$1(values, size) {
6799
7049
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
6800
7050
  return chunks;
6801
7051
  }
6802
- function encodeOpaqueJson$3(value) {
7052
+ function encodeOpaqueJson$4(value) {
6803
7053
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
6804
7054
  }
6805
- function decodeOpaqueJson$3(value) {
7055
+ function decodeOpaqueJson$4(value) {
6806
7056
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
6807
7057
  }
6808
7058
  function arkmeIdAvailabilityReason(value) {
@@ -7145,7 +7395,7 @@ var ProfileService = class {
7145
7395
  return profiles;
7146
7396
  }
7147
7397
  async sealProfileImageRef(viewerUserId, targetUserId) {
7148
- const payload = encodeOpaqueJson$3({
7398
+ const payload = encodeOpaqueJson$4({
7149
7399
  version: 1,
7150
7400
  viewerUserId,
7151
7401
  targetUserId
@@ -7161,7 +7411,7 @@ var ProfileService = class {
7161
7411
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("image-ref-invalid", "Arkme头像引用无效", false);
7162
7412
  let parsed;
7163
7413
  try {
7164
- parsed = objectValue$1(decodeOpaqueJson$3(payload));
7414
+ parsed = objectValue$1(decodeOpaqueJson$4(payload));
7165
7415
  } catch (error) {
7166
7416
  throw new ArkmePluginError("image-ref-invalid", "Arkme头像引用无效", false, 400, { cause: error });
7167
7417
  }
@@ -7851,10 +8101,10 @@ function numberValue$5(value) {
7851
8101
  function listValue$5(value) {
7852
8102
  return Array.isArray(value) ? value : [];
7853
8103
  }
7854
- function encodeOpaqueJson$2(value) {
8104
+ function encodeOpaqueJson$3(value) {
7855
8105
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
7856
8106
  }
7857
- function decodeOpaqueJson$2(value) {
8107
+ function decodeOpaqueJson$3(value) {
7858
8108
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
7859
8109
  }
7860
8110
  function safeFailureMessage$2(error) {
@@ -7934,7 +8184,7 @@ var RecordingService = class {
7934
8184
  }
7935
8185
  async sealRecordingCursor(payload) {
7936
8186
  const session = await this.runtime.requireSession();
7937
- const encoded = encodeOpaqueJson$2(payload);
8187
+ const encoded = encodeOpaqueJson$3(payload);
7938
8188
  return `arkme-recording-cursor-v1.${encoded}.${createHmac("sha256", await this.recordingCursorKey(session.userId)).update(encoded).digest("base64url")}`;
7939
8189
  }
7940
8190
  async openRecordingCursor(cursor) {
@@ -7946,7 +8196,7 @@ var RecordingService = class {
7946
8196
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false);
7947
8197
  let raw;
7948
8198
  try {
7949
- raw = objectValue$1(decodeOpaqueJson$2(encoded));
8199
+ raw = objectValue$1(decodeOpaqueJson$3(encoded));
7950
8200
  } catch (error) {
7951
8201
  throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false, 400, { cause: error });
7952
8202
  }
@@ -8498,10 +8748,10 @@ function booleanValue$1(value) {
8498
8748
  function listValue$2(value) {
8499
8749
  return Array.isArray(value) ? value : [];
8500
8750
  }
8501
- function encodeOpaqueJson$1(value) {
8751
+ function encodeOpaqueJson$2(value) {
8502
8752
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
8503
8753
  }
8504
- function decodeOpaqueJson$1(value) {
8754
+ function decodeOpaqueJson$2(value) {
8505
8755
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
8506
8756
  }
8507
8757
  function isSourceKind(value) {
@@ -8683,13 +8933,6 @@ var SourceService = class {
8683
8933
  ...defaultLatestPreview === "" ? {} : { latestPreview: defaultLatestPreview },
8684
8934
  ...defaultRecordCount === void 0 ? {} : { recordCount: defaultRecordCount }
8685
8935
  };
8686
- const aggregateSource = {
8687
- sourceRef: await this.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
8688
- kind: "send_to_self",
8689
- displayName: "发给自己",
8690
- activeAtMillis: 0,
8691
- unreadCount: 0
8692
- };
8693
8936
  const topicDescriptors = [];
8694
8937
  const seenTopicUids = /* @__PURE__ */ new Set();
8695
8938
  const parentTopicUidByChild = /* @__PURE__ */ new Map();
@@ -8717,30 +8960,44 @@ var SourceService = class {
8717
8960
  ...parentTopicUid === "" || parentTopicUid === topicUid ? {} : { parentTopicUid },
8718
8961
  title,
8719
8962
  latestPreview: textPreview(latest),
8963
+ latestMessageAtMillis: numberValue$2(latest.send_at ?? summary.latest_send_at),
8720
8964
  activeAtMillis: numberValue$2(latest.send_at ?? summary.latest_send_at ?? core.update_at),
8721
8965
  recordCount: numberValue$2(summary.record_count)
8722
8966
  });
8723
8967
  }
8724
8968
  const sourceRefByTopicUid = /* @__PURE__ */ new Map();
8725
8969
  for (const topic of topicDescriptors) sourceRefByTopicUid.set(topic.topicUid, await this.sealSourceRef(session.userId, "topic", topic.topicUid, topic.title));
8970
+ const topics = topicDescriptors.map((topic) => {
8971
+ const parentSourceRef = topic.parentTopicUid === void 0 ? void 0 : sourceRefByTopicUid.get(topic.parentTopicUid);
8972
+ return {
8973
+ sourceRef: sourceRefByTopicUid.get(topic.topicUid),
8974
+ ...parentSourceRef === void 0 ? {} : { parentSourceRef },
8975
+ kind: "topic",
8976
+ displayName: topic.title,
8977
+ ...topic.latestPreview === "" ? {} : { latestPreview: topic.latestPreview },
8978
+ activeAtMillis: topic.activeAtMillis,
8979
+ unreadCount: 0,
8980
+ recordCount: topic.recordCount
8981
+ };
8982
+ });
8983
+ const latestAggregateItem = [defaultCategory, ...topics.map((source, index) => ({
8984
+ ...source,
8985
+ activeAtMillis: topicDescriptors[index]?.latestMessageAtMillis ?? 0
8986
+ }))].reduce((latest, source) => latest === void 0 || source.activeAtMillis > latest.activeAtMillis ? source : latest, void 0);
8987
+ const aggregateLatestPreview = latestAggregateItem === void 0 || latestAggregateItem.activeAtMillis <= 0 ? "" : latestAggregateItem.latestPreview?.trim() || "非文本内容";
8726
8988
  return {
8727
8989
  directory,
8728
8990
  items: [
8729
- aggregateSource,
8991
+ {
8992
+ sourceRef: await this.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
8993
+ kind: "send_to_self",
8994
+ displayName: "发给自己",
8995
+ ...aggregateLatestPreview === "" ? {} : { latestPreview: aggregateLatestPreview },
8996
+ activeAtMillis: latestAggregateItem?.activeAtMillis ?? 0,
8997
+ unreadCount: 0
8998
+ },
8730
8999
  defaultCategory,
8731
- ...topicDescriptors.map((topic) => {
8732
- const parentSourceRef = topic.parentTopicUid === void 0 ? void 0 : sourceRefByTopicUid.get(topic.parentTopicUid);
8733
- return {
8734
- sourceRef: sourceRefByTopicUid.get(topic.topicUid),
8735
- ...parentSourceRef === void 0 ? {} : { parentSourceRef },
8736
- kind: "topic",
8737
- displayName: topic.title,
8738
- ...topic.latestPreview === "" ? {} : { latestPreview: topic.latestPreview },
8739
- activeAtMillis: topic.activeAtMillis,
8740
- unreadCount: 0,
8741
- recordCount: topic.recordCount
8742
- };
8743
- })
9000
+ ...topics
8744
9001
  ],
8745
9002
  hasMore: false
8746
9003
  };
@@ -8906,7 +9163,7 @@ var SourceService = class {
8906
9163
  return `arkme-chat-source-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`chat-source-key-v1:${String(userId)}:${chatSessionUid.trim()}`).digest("base64url")}`;
8907
9164
  }
8908
9165
  async sealSourceRef(userId, kind, ownerRef, displayName) {
8909
- const payload = encodeOpaqueJson$1({
9166
+ const payload = encodeOpaqueJson$2({
8910
9167
  version: 1,
8911
9168
  userId,
8912
9169
  kind,
@@ -8924,7 +9181,7 @@ var SourceService = class {
8924
9181
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("source-ref-invalid", "Arkme 数据源引用无效", false);
8925
9182
  let parsed;
8926
9183
  try {
8927
- parsed = objectValue$1(decodeOpaqueJson$1(payload));
9184
+ parsed = objectValue$1(decodeOpaqueJson$2(payload));
8928
9185
  } catch (error) {
8929
9186
  throw new ArkmePluginError("source-ref-invalid", "Arkme 数据源引用无效", false, 400, { cause: error });
8930
9187
  }
@@ -8980,13 +9237,13 @@ var SourceService = class {
8980
9237
  };
8981
9238
  }
8982
9239
  encodeCursor(value) {
8983
- return `arkme-cursor-v1.${encodeOpaqueJson$1(value)}`;
9240
+ return `arkme-cursor-v1.${encodeOpaqueJson$2(value)}`;
8984
9241
  }
8985
9242
  decodeCursor(cursor) {
8986
9243
  const [prefix, payload, ...extra] = cursor.trim().split(".");
8987
9244
  if (prefix !== "arkme-cursor-v1" || payload === void 0 || extra.length > 0) throw new ArkmePluginError("source-cursor-invalid", "Arkme 数据源分页游标无效", false);
8988
9245
  try {
8989
- const decoded = objectValue$1(decodeOpaqueJson$1(payload));
9246
+ const decoded = objectValue$1(decodeOpaqueJson$2(payload));
8990
9247
  if (Object.keys(decoded).length === 0) throw new Error("empty cursor");
8991
9248
  return decoded;
8992
9249
  } catch (error) {
@@ -9037,10 +9294,10 @@ function optionalString(value) {
9037
9294
  const text = stringValue$1(value).trim();
9038
9295
  return text === "" ? void 0 : text;
9039
9296
  }
9040
- function encodeOpaqueJson(value) {
9297
+ function encodeOpaqueJson$1(value) {
9041
9298
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
9042
9299
  }
9043
- function decodeOpaqueJson(value) {
9300
+ function decodeOpaqueJson$1(value) {
9044
9301
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
9045
9302
  }
9046
9303
  var WechatService = class {
@@ -9348,7 +9605,7 @@ var WechatService = class {
9348
9605
  };
9349
9606
  }
9350
9607
  async sealWechatConversationRef(userId, importSessionKey) {
9351
- const payload = encodeOpaqueJson({
9608
+ const payload = encodeOpaqueJson$1({
9352
9609
  version: 1,
9353
9610
  userId,
9354
9611
  importSessionKey
@@ -9364,7 +9621,7 @@ var WechatService = class {
9364
9621
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("wechat-conversation-ref-invalid", "微信会话引用无效,请先重新查询微信会话列表", false);
9365
9622
  let parsed;
9366
9623
  try {
9367
- parsed = objectValue$1(decodeOpaqueJson(payload));
9624
+ parsed = objectValue$1(decodeOpaqueJson$1(payload));
9368
9625
  } catch (error) {
9369
9626
  throw new ArkmePluginError("wechat-conversation-ref-invalid", "微信会话引用无效,请先重新查询微信会话列表", false, 400, { cause: error });
9370
9627
  }
@@ -9377,7 +9634,7 @@ var WechatService = class {
9377
9634
  return result;
9378
9635
  }
9379
9636
  async sealWechatCursor(userId, scope, offset) {
9380
- const payload = encodeOpaqueJson({
9637
+ const payload = encodeOpaqueJson$1({
9381
9638
  version: 1,
9382
9639
  userId,
9383
9640
  scope,
@@ -9395,7 +9652,7 @@ var WechatService = class {
9395
9652
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("wechat-cursor-invalid", "微信数据分页游标无效,请从第一页重新查询", false);
9396
9653
  let parsed;
9397
9654
  try {
9398
- parsed = objectValue$1(decodeOpaqueJson(payload));
9655
+ parsed = objectValue$1(decodeOpaqueJson$1(payload));
9399
9656
  } catch (error) {
9400
9657
  throw new ArkmePluginError("wechat-cursor-invalid", "微信数据分页游标无效,请从第一页重新查询", false, 400, { cause: error });
9401
9658
  }
@@ -9413,12 +9670,527 @@ var WechatService = class {
9413
9670
  //#region src/types.ts
9414
9671
  const ARKME_PROVIDER_CONTRACT_VERSION = 1;
9415
9672
  const ARKME_DEFAULT_SHARE_WEBSITE = "https://app.arkme.ai";
9673
+ const ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS = 3e3;
9674
+ const ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS = 6e4;
9675
+ const ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES = 10 * 1024 * 1024;
9416
9676
  const ARKME_WORLD_PUBLISH_MAX_IMAGES = 9;
9417
9677
  const ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
9418
9678
  //#endregion
9679
+ //#region src/voiceprint-wav.ts
9680
+ function ascii(bytes, offset, length) {
9681
+ return String.fromCharCode(...bytes.subarray(offset, offset + length));
9682
+ }
9683
+ function validateVoiceprintPcm16Wav(wav, declaredDurationMs) {
9684
+ if (wav.byteLength < 46 || !Number.isSafeInteger(declaredDurationMs) || declaredDurationMs <= 0) throw new TypeError("声纹 WAV 元数据无效");
9685
+ const view = new DataView(wav.buffer, wav.byteOffset, wav.byteLength);
9686
+ const sampleRate = view.getUint32(24, true);
9687
+ const dataBytes = view.getUint32(40, true);
9688
+ if (!(ascii(wav, 0, 4) === "RIFF" && view.getUint32(4, true) === wav.byteLength - 8 && ascii(wav, 8, 4) === "WAVE" && ascii(wav, 12, 4) === "fmt " && view.getUint32(16, true) === 16 && view.getUint16(20, true) === 1 && view.getUint16(22, true) === 1 && sampleRate >= 8e3 && sampleRate <= 192e3 && view.getUint32(28, true) === sampleRate * 2 && view.getUint16(32, true) === 2 && view.getUint16(34, true) === 16 && ascii(wav, 36, 4) === "data" && dataBytes > 0 && dataBytes === wav.byteLength - 44)) throw new TypeError("声纹录音必须是单声道 PCM16 WAV");
9689
+ const durationMs = Math.round(dataBytes * 1e3 / (sampleRate * 2));
9690
+ if (Math.abs(durationMs - declaredDurationMs) > 250) throw new TypeError("声纹 WAV 时长与声明不一致");
9691
+ return {
9692
+ sampleRate,
9693
+ durationMs
9694
+ };
9695
+ }
9696
+ //#endregion
9697
+ //#region src/services/voiceprint-service.ts
9698
+ const GRANT_REF_PREFIX = "arkme-voiceprint-grant-v1";
9699
+ const PERSON_REF_PREFIX = "arkme-voiceprint-person-v1";
9700
+ const SPEAKER_ID_PATTERN = /^[0-9a-f]{24}$/;
9701
+ const SHORT_INVITE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
9702
+ function encodeOpaqueJson(value) {
9703
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
9704
+ }
9705
+ function decodeOpaqueJson(value) {
9706
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
9707
+ }
9708
+ function integerValue$1(value) {
9709
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : NaN;
9710
+ }
9711
+ function requiredBoolean(value, code) {
9712
+ if (typeof value !== "boolean") throw contractError(code);
9713
+ return value;
9714
+ }
9715
+ function requiredInteger(value, code, minimum = 0) {
9716
+ const parsed = integerValue$1(value);
9717
+ if (!Number.isSafeInteger(parsed) || parsed < minimum) throw contractError(code);
9718
+ return parsed;
9719
+ }
9720
+ function requiredString(value, code) {
9721
+ if (typeof value !== "string") throw contractError(code);
9722
+ return value;
9723
+ }
9724
+ function contractError(code) {
9725
+ return new ArkmePluginError(code, "声纹服务返回的数据不完整,请稍后重试", true, 502);
9726
+ }
9727
+ function validOpaqueText(value, maxBytes) {
9728
+ return value === value.trim() && Buffer.byteLength(value, "utf8") <= maxBytes && !/[\u0000-\u001f\u007f]/u.test(value);
9729
+ }
9730
+ function validSpeakerId(value) {
9731
+ return SPEAKER_ID_PATTERN.test(value) && value !== "000000000000000000000000";
9732
+ }
9733
+ var VoiceprintService = class {
9734
+ runtime;
9735
+ profile;
9736
+ inviteTarget;
9737
+ constructor(runtime, profile, inviteTarget) {
9738
+ this.runtime = runtime;
9739
+ this.profile = profile;
9740
+ this.inviteTarget = inviteTarget;
9741
+ }
9742
+ async myVoiceprint(options = {}) {
9743
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/my", {}, void 0, options.signal, {
9744
+ key: "voiceprint:self",
9745
+ cacheMs: 3e3,
9746
+ failureCooldownMs: 2e3
9747
+ });
9748
+ const hasVoiceprint = requiredBoolean(data.has_voiceprint, "voiceprint-self-contract-invalid");
9749
+ const canIdentify = requiredBoolean(data.can_identify, "voiceprint-self-contract-invalid");
9750
+ const canPlay = requiredBoolean(data.can_play, "voiceprint-self-contract-invalid");
9751
+ const canRestorePlayback = requiredBoolean(data.can_restore_playback, "voiceprint-self-contract-invalid");
9752
+ const rawStatus = requiredString(data.enrollment_status ?? "none", "voiceprint-self-contract-invalid").trim();
9753
+ if (![
9754
+ "none",
9755
+ "processing",
9756
+ "ready"
9757
+ ].includes(rawStatus)) throw contractError("voiceprint-self-contract-invalid");
9758
+ const enrollmentStatus = rawStatus;
9759
+ const pendingSessionId = requiredString(data.pending_session_id ?? "", "voiceprint-self-contract-invalid").trim();
9760
+ const nickname = requiredString(data.nick_name ?? "", "voiceprint-self-contract-invalid").trim();
9761
+ const updatedAtMillis = requiredInteger(data.updated_at ?? 0, "voiceprint-self-contract-invalid");
9762
+ if (!validOpaqueText(pendingSessionId, 2048) || !hasVoiceprint && (canIdentify || canPlay || canRestorePlayback) || canPlay && !hasVoiceprint) throw contractError("voiceprint-self-contract-invalid");
9763
+ return {
9764
+ hasVoiceprint,
9765
+ nickname,
9766
+ updatedAtMillis,
9767
+ canIdentify,
9768
+ canPlay,
9769
+ canRestorePlayback,
9770
+ enrollmentStatus,
9771
+ enrollmentPending: enrollmentStatus === "processing" || pendingSessionId !== ""
9772
+ };
9773
+ }
9774
+ async outboundGrants(input, options = {}) {
9775
+ if (!validOpaqueText(input.cursor, 512) || !Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new ArkmePluginError("voiceprint-grant-page-invalid", "声纹授权分页参数无效", false);
9776
+ const session = await this.runtime.requireSession();
9777
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/grants", {
9778
+ direction: "outbound",
9779
+ cursor: input.cursor,
9780
+ limit: input.limit
9781
+ }, session, options.signal, {
9782
+ key: `voiceprint:grants:${input.cursor}:${String(input.limit)}`,
9783
+ failureCooldownMs: 2e3
9784
+ });
9785
+ const rawItems = data.grant_ls;
9786
+ if (!Array.isArray(rawItems)) throw contractError("voiceprint-grant-contract-invalid");
9787
+ const grants = rawItems.map((raw) => this.parseOutboundGrant(objectValue$1(raw), session.userId));
9788
+ const granteeUserIds = [...new Set(grants.map((item) => item.granteeUserId))];
9789
+ const profiles = await this.loadPublicProfiles(granteeUserIds, session, options.signal);
9790
+ const items = [];
9791
+ for (const grant of grants) {
9792
+ const publicProfile = profiles.get(grant.granteeUserId);
9793
+ const avatarRef = publicProfile?.avatarUrl === void 0 ? void 0 : await this.optionalAvatarRef(session.userId, grant.granteeUserId);
9794
+ items.push({
9795
+ grantRef: await this.sealRef(GRANT_REF_PREFIX, {
9796
+ version: 1,
9797
+ viewerUserId: session.userId,
9798
+ granteeUserId: grant.granteeUserId
9799
+ }),
9800
+ displayName: publicProfile?.displayName.trim() || "用户资料不可用",
9801
+ ...avatarRef === void 0 ? {} : { avatarRef },
9802
+ identifyEnabled: grant.identifyEnabled,
9803
+ playEnabled: grant.playEnabled,
9804
+ grantedAtMillis: grant.grantedAtMillis,
9805
+ updatedAtMillis: grant.updatedAtMillis
9806
+ });
9807
+ }
9808
+ const nextCursor = requiredString(data.next_cursor, "voiceprint-grant-contract-invalid");
9809
+ const hasMore = requiredBoolean(data.has_more, "voiceprint-grant-contract-invalid");
9810
+ if (!validOpaqueText(nextCursor, 512) || hasMore !== (nextCursor !== "")) throw contractError("voiceprint-grant-contract-invalid");
9811
+ return {
9812
+ items,
9813
+ nextCursor,
9814
+ hasMore
9815
+ };
9816
+ }
9817
+ async recognizedPeople(input, options = {}) {
9818
+ if (!validOpaqueText(input.cursor, 2048) || !Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 50) throw new ArkmePluginError("voiceprint-person-page-invalid", "已识别人分页参数无效", false);
9819
+ const session = await this.runtime.requireSession();
9820
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/list", {
9821
+ cursor: input.cursor,
9822
+ limit: input.limit
9823
+ }, session, options.signal, {
9824
+ key: `voiceprint:people:${input.cursor}:${String(input.limit)}`,
9825
+ failureCooldownMs: 2e3
9826
+ });
9827
+ requiredBoolean(data.capability_enabled, "voiceprint-person-contract-invalid");
9828
+ const rawItems = data.items;
9829
+ if (!Array.isArray(rawItems)) throw contractError("voiceprint-person-contract-invalid");
9830
+ const projections = rawItems.map((raw) => this.parseRecognizedPerson(objectValue$1(raw)));
9831
+ if (projections.some((item) => item.targetUserId === session.userId)) throw contractError("voiceprint-person-contract-invalid");
9832
+ const targetUserIds = [...new Set(projections.map((item) => item.targetUserId).filter((userId) => userId > 0))];
9833
+ const profiles = await this.loadPublicProfiles(targetUserIds, session, options.signal);
9834
+ const items = await Promise.all(projections.map(async (item) => await this.projectRecognizedPerson(item, session, profiles)));
9835
+ if (new Set(items.map((item) => item.personRef)).size !== items.length) throw contractError("voiceprint-person-contract-invalid");
9836
+ let reachedUnauthorized = false;
9837
+ for (const item of items) if (!item.playGranted) reachedUnauthorized = true;
9838
+ else if (reachedUnauthorized) throw contractError("voiceprint-person-contract-invalid");
9839
+ const nextCursor = requiredString(data.next_cursor, "voiceprint-person-contract-invalid");
9840
+ const hasMore = requiredBoolean(data.has_more, "voiceprint-person-contract-invalid");
9841
+ if (!validOpaqueText(nextCursor, 2048) || hasMore !== (nextCursor !== "")) throw contractError("voiceprint-person-contract-invalid");
9842
+ return {
9843
+ items,
9844
+ nextCursor,
9845
+ hasMore
9846
+ };
9847
+ }
9848
+ async recognizedPerson(personRef, options = {}) {
9849
+ const session = await this.runtime.requireSession();
9850
+ const reference = await this.openPersonRef(personRef, session.userId);
9851
+ return (await this.loadRecognizedPerson(reference, personRef, session, options)).detail;
9852
+ }
9853
+ async loadRecognizedPerson(reference, personRef, session, options) {
9854
+ const selector = reference.identityKind === "speaker" ? { speaker_id: reference.speakerId } : { target_user_id: reference.targetUserId };
9855
+ let data;
9856
+ try {
9857
+ data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/detail", selector, session, options.signal, {
9858
+ key: `voiceprint:person:${personRef}`,
9859
+ failureCooldownMs: 2e3
9860
+ });
9861
+ } catch (error) {
9862
+ if (error instanceof ArkmePluginError && error.code === "arkme-code-1003") throw new ArkmePluginError("voiceprint-person-unavailable", "这条已识别人已不可访问,请刷新列表", false, 410, { cause: error });
9863
+ throw error;
9864
+ }
9865
+ const projection = this.parseRecognizedPerson(data);
9866
+ if (reference.identityKind === "speaker" && projection.speakerId !== reference.speakerId || reference.identityKind === "authorized_user" && projection.targetUserId !== reference.targetUserId || projection.targetUserId === session.userId) throw contractError("voiceprint-person-contract-invalid");
9867
+ const profiles = await this.loadPublicProfiles(projection.targetUserId > 0 ? [projection.targetUserId] : [], session, options.signal);
9868
+ if (requiredBoolean(data.claim_required, "voiceprint-person-contract-invalid") !== (projection.speakerId !== "" && projection.targetUserId === 0)) throw contractError("voiceprint-person-contract-invalid");
9869
+ return {
9870
+ detail: await this.projectRecognizedPerson(projection, session, profiles),
9871
+ targetUserId: projection.targetUserId
9872
+ };
9873
+ }
9874
+ async recognizedPersonVoiceprints(personRef, options = {}) {
9875
+ const session = await this.runtime.requireSession();
9876
+ const reference = await this.openPersonRef(personRef, session.userId);
9877
+ if (reference.identityKind !== "speaker") throw new ArkmePluginError("voiceprint-library-unavailable", "只有录音中识别到的声音可以查看声纹记录", false, 409);
9878
+ const selector = { speaker_id: reference.speakerId };
9879
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/voiceprints/list", selector, session, options.signal, {
9880
+ key: `voiceprint:library:${personRef}`,
9881
+ failureCooldownMs: 2e3
9882
+ });
9883
+ if (!Array.isArray(data.items)) throw contractError("voiceprint-library-contract-invalid");
9884
+ const items = [];
9885
+ const refs = /* @__PURE__ */ new Set();
9886
+ let reachedLocal = false;
9887
+ let previousHitCount;
9888
+ let previousAuthorized;
9889
+ for (const raw of data.items) {
9890
+ const item = objectValue$1(raw);
9891
+ const voiceprintRef = requiredString(item.voiceprint_id, "voiceprint-library-contract-invalid");
9892
+ const kindCode = requiredInteger(item.kind, "voiceprint-library-contract-invalid", 1);
9893
+ const authorized = requiredBoolean(item.is_authorized, "voiceprint-library-contract-invalid");
9894
+ const hitCount = requiredInteger(item.hit_count, "voiceprint-library-contract-invalid");
9895
+ const createdAtMillis = item.created_at === void 0 ? void 0 : requiredInteger(item.created_at, "voiceprint-library-contract-invalid", 1);
9896
+ if (!validOpaqueText(voiceprintRef, 2048) || voiceprintRef === "" || refs.has(voiceprintRef) || ![
9897
+ 1,
9898
+ 2,
9899
+ 3
9900
+ ].includes(kindCode) || authorized !== (kindCode === 3) || authorized && hitCount !== 0) throw contractError("voiceprint-library-contract-invalid");
9901
+ if (!authorized) reachedLocal = true;
9902
+ else if (reachedLocal) throw contractError("voiceprint-library-contract-invalid");
9903
+ if (previousAuthorized === authorized && previousHitCount !== void 0 && hitCount > previousHitCount) throw contractError("voiceprint-library-contract-invalid");
9904
+ refs.add(voiceprintRef);
9905
+ previousAuthorized = authorized;
9906
+ previousHitCount = hitCount;
9907
+ items.push({
9908
+ kind: kindCode === 3 ? "authorized" : kindCode === 2 ? "legacy" : "local",
9909
+ hitCount,
9910
+ ...createdAtMillis === void 0 ? {} : { createdAtMillis }
9911
+ });
9912
+ }
9913
+ return { items };
9914
+ }
9915
+ async createInvitation(options = {}) {
9916
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/invites/create", { scope: 2 }, void 0, options.signal, {
9917
+ lane: "write",
9918
+ key: `voiceprint:invite:${Date.now().toString(36)}`,
9919
+ bypassCache: true
9920
+ });
9921
+ const inviteToken = requiredString(data.invite_token, "voiceprint-invite-contract-invalid").trim();
9922
+ const previewToken = requiredString(data.preview_token, "voiceprint-invite-contract-invalid").trim();
9923
+ const shortInviteToken = stringValue$1(data.short_invite_token).trim();
9924
+ const shortPreviewToken = stringValue$1(data.short_preview_token).trim();
9925
+ const expiresAtMillis = requiredInteger(data.expires_at, "voiceprint-invite-contract-invalid", 1);
9926
+ if (requiredInteger(data.scope, "voiceprint-invite-contract-invalid", 1) !== 2 || !validOpaqueText(inviteToken, 2048) || inviteToken === "" || !validOpaqueText(previewToken, 2048) || previewToken === "") throw contractError("voiceprint-invite-contract-invalid");
9927
+ const useShortPair = SHORT_INVITE_TOKEN_PATTERN.test(shortInviteToken) && SHORT_INVITE_TOKEN_PATTERN.test(shortPreviewToken);
9928
+ const token = useShortPair ? shortInviteToken : inviteToken;
9929
+ const preview = useShortPair ? shortPreviewToken : previewToken;
9930
+ return {
9931
+ inviteUrl: this.invitationUrl(token, preview, useShortPair),
9932
+ expiresAtMillis
9933
+ };
9934
+ }
9935
+ async createRecognizedPersonInvitation(personRef, targetContactRef, options = {}) {
9936
+ const session = await this.runtime.requireSession();
9937
+ const reference = await this.openPersonRef(personRef, session.userId);
9938
+ if (reference.identityKind !== "speaker") throw new ArkmePluginError("voiceprint-person-invite-unavailable", "授权用户投影不能作为待认领声纹邀请", false, 409);
9939
+ const current = await this.loadRecognizedPerson(reference, personRef, session, options);
9940
+ if (!current.detail.canInvite || current.detail.playGranted) throw new ArkmePluginError("voiceprint-person-invite-unavailable", "这条已识别人当前不能生成邀请", false, 409);
9941
+ const normalizedContactRef = targetContactRef?.trim() ?? "";
9942
+ const targetSelectionRequired = current.targetUserId === 0;
9943
+ if (!targetSelectionRequired && normalizedContactRef !== "") throw new ArkmePluginError("voiceprint-invite-target-unexpected", "这条已识别人已绑定用户,无需重新选择邀请对象", false);
9944
+ let targetUserId = current.targetUserId;
9945
+ if (targetSelectionRequired) {
9946
+ if (this.inviteTarget === void 0) throw new ArkmePluginError("voiceprint-invite-target-unavailable", "当前运行时无法选择邀请对象", false, 503);
9947
+ if (!validOpaqueText(normalizedContactRef, 256) || normalizedContactRef === "") throw new ArkmePluginError("voiceprint-invite-target-invalid", "邀请对象引用无效,请重新搜索", false);
9948
+ targetUserId = await this.inviteTarget.resolveRegisteredContactUserId(normalizedContactRef, session, options.signal);
9949
+ if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0 || targetUserId === session.userId) throw new ArkmePluginError("voiceprint-invite-target-invalid", "请选择其他已注册的 Arkme 用户", false);
9950
+ }
9951
+ const request = targetSelectionRequired ? {
9952
+ speaker_id: reference.speakerId,
9953
+ target_user_id: targetUserId
9954
+ } : { speaker_id: reference.speakerId };
9955
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/recognized-speakers/invites/create", request, session, options.signal, {
9956
+ lane: "write",
9957
+ key: `voiceprint:person-invite:${personRef}:${normalizedContactRef || "bound"}`,
9958
+ bypassCache: true
9959
+ });
9960
+ const inviteToken = requiredString(data.invite_token, "voiceprint-person-invite-contract-invalid").trim();
9961
+ const previewToken = requiredString(data.preview_token, "voiceprint-person-invite-contract-invalid").trim();
9962
+ const shortInviteToken = stringValue$1(data.short_invite_token).trim();
9963
+ const shortPreviewToken = stringValue$1(data.short_preview_token).trim();
9964
+ const expiresAtMillis = requiredInteger(data.expires_at, "voiceprint-person-invite-contract-invalid", 1);
9965
+ if (requiredInteger(data.token_version, "voiceprint-person-invite-contract-invalid", 1) !== 2 || requiredString(data.speaker_id, "voiceprint-person-invite-contract-invalid").trim().toLowerCase() !== reference.speakerId || requiredInteger(data.target_user_id, "voiceprint-person-invite-contract-invalid", 1) !== targetUserId || requiredBoolean(data.claim_required, "voiceprint-person-invite-contract-invalid") !== targetSelectionRequired || requiredInteger(data.scope, "voiceprint-person-invite-contract-invalid", 1) !== 2 || !validOpaqueText(inviteToken, 2048) || inviteToken === "" || !validOpaqueText(previewToken, 2048) || previewToken === "") throw contractError("voiceprint-person-invite-contract-invalid");
9966
+ const useShortPair = SHORT_INVITE_TOKEN_PATTERN.test(shortInviteToken) && SHORT_INVITE_TOKEN_PATTERN.test(shortPreviewToken);
9967
+ return {
9968
+ inviteUrl: this.invitationUrl(useShortPair ? shortInviteToken : inviteToken, useShortPair ? shortPreviewToken : previewToken, useShortPair),
9969
+ expiresAtMillis
9970
+ };
9971
+ }
9972
+ async revokePlaybackGrant(grantRef, options = {}) {
9973
+ const session = await this.runtime.requireSession();
9974
+ const reference = await this.openGrantRef(grantRef, session.userId);
9975
+ if (requiredBoolean((await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/grants/set-scope", {
9976
+ grantee_user_id: reference.granteeUserId,
9977
+ scope: 2,
9978
+ enabled: false
9979
+ }, session, options.signal, {
9980
+ lane: "write",
9981
+ key: `voiceprint:revoke:${grantRef}`,
9982
+ bypassCache: true
9983
+ })).revoked, "voiceprint-revoke-contract-invalid") !== true) throw contractError("voiceprint-revoke-contract-invalid");
9984
+ return { revoked: true };
9985
+ }
9986
+ async restorePlayback(options = {}) {
9987
+ const session = await this.runtime.requireSession();
9988
+ this.runtime.invalidateKey(this.runtime.requestScope(session.userId), "voiceprint:self");
9989
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/restore-playback", {}, session, options.signal, {
9990
+ lane: "write",
9991
+ key: `voiceprint:restore:${Date.now().toString(36)}`,
9992
+ bypassCache: true
9993
+ });
9994
+ const result = {
9995
+ canPlay: requiredBoolean(data.can_play, "voiceprint-restore-contract-invalid"),
9996
+ restored: requiredBoolean(data.restored, "voiceprint-restore-contract-invalid"),
9997
+ updatedAtMillis: requiredInteger(data.updated_at ?? 0, "voiceprint-restore-contract-invalid")
9998
+ };
9999
+ if (result.restored && !result.canPlay) throw contractError("voiceprint-restore-contract-invalid");
10000
+ return result;
10001
+ }
10002
+ async bindEnrollment() {
10003
+ const viewerUserId = (await this.runtime.requireSession()).userId;
10004
+ return { enrollVoiceprintWav: async (input, options = {}) => await this.enrollWav(input, {
10005
+ ...options,
10006
+ expectedViewerUserId: viewerUserId
10007
+ }) };
10008
+ }
10009
+ async enrollWav(input, options = {}) {
10010
+ const { wav, durationMs } = input;
10011
+ if (!Number.isSafeInteger(durationMs) || durationMs < 3e3 || durationMs > 6e4 || wav.byteLength > 10485760 || wav.byteLength < 46) throw new ArkmePluginError("voiceprint-enrollment-input-invalid", "声纹录音需为 3 至 60 秒的 WAV 音频", false);
10012
+ try {
10013
+ validateVoiceprintPcm16Wav(wav, durationMs);
10014
+ } catch (error) {
10015
+ throw new ArkmePluginError("voiceprint-enrollment-input-invalid", "声纹录音需为 3 至 60 秒的单声道 PCM16 WAV 音频", false, 400, { cause: error });
10016
+ }
10017
+ const session = await this.runtime.requireSession();
10018
+ if (options.expectedViewerUserId !== void 0 && session.userId !== options.expectedViewerUserId) throw new ArkmePluginError("account-changed", "登录账号已切换,请在当前账号下重新录入", false, 409);
10019
+ const form = new FormData();
10020
+ const wavBuffer = new Uint8Array(wav).buffer;
10021
+ form.set("audio_file", new Blob([wavBuffer], { type: "audio/wav" }), "voiceprint.wav");
10022
+ form.set("duration_ms", String(durationMs));
10023
+ this.runtime.invalidateKey(this.runtime.requestScope(session.userId), "voiceprint:self");
10024
+ const data = await this.runtime.authenticatedAudioMultipartPost("/api/v1/audio/voiceprint/enroll-from-audio", form, session, options.signal, {
10025
+ key: `voiceprint:enroll:${Date.now().toString(36)}`,
10026
+ bypassCache: true
10027
+ });
10028
+ const status = requiredString(data.status, "voiceprint-enrollment-contract-invalid").trim();
10029
+ if (status !== "processing") throw contractError("voiceprint-enrollment-contract-invalid");
10030
+ return {
10031
+ status,
10032
+ cloneReady: requiredBoolean(data.clone_ok, "voiceprint-enrollment-contract-invalid"),
10033
+ updatedAtMillis: requiredInteger(data.updated_at, "voiceprint-enrollment-contract-invalid", 1)
10034
+ };
10035
+ }
10036
+ parseOutboundGrant(raw, viewerUserId) {
10037
+ const ownerUserId = requiredInteger(raw.owner_user_id, "voiceprint-grant-contract-invalid", 1);
10038
+ const granteeUserId = requiredInteger(raw.grantee_user_id, "voiceprint-grant-contract-invalid", 1);
10039
+ const status = requiredInteger(raw.status, "voiceprint-grant-contract-invalid");
10040
+ if (ownerUserId !== viewerUserId || granteeUserId === viewerUserId || status !== 1) throw contractError("voiceprint-grant-contract-invalid");
10041
+ return {
10042
+ granteeUserId,
10043
+ identifyEnabled: requiredBoolean(raw.identify_enabled, "voiceprint-grant-contract-invalid"),
10044
+ playEnabled: requiredBoolean(raw.play_enabled, "voiceprint-grant-contract-invalid"),
10045
+ grantedAtMillis: requiredInteger(raw.play_granted_at ?? 0, "voiceprint-grant-contract-invalid"),
10046
+ updatedAtMillis: requiredInteger(raw.update_at, "voiceprint-grant-contract-invalid")
10047
+ };
10048
+ }
10049
+ parseRecognizedPerson(raw) {
10050
+ const speakerId = requiredString(raw.speaker_id, "voiceprint-person-contract-invalid").trim().toLowerCase();
10051
+ const targetUserId = requiredInteger(raw.target_user_id, "voiceprint-person-contract-invalid");
10052
+ const displayName = requiredString(raw.display_name, "voiceprint-person-contract-invalid").trim();
10053
+ const playGranted = requiredBoolean(raw.play_granted, "voiceprint-person-contract-invalid");
10054
+ const previewAvailable = requiredBoolean(raw.preview_available, "voiceprint-person-contract-invalid");
10055
+ const canInvite = requiredBoolean(raw.can_invite, "voiceprint-person-contract-invalid");
10056
+ const hasSpeaker = validSpeakerId(speakerId);
10057
+ if (speakerId !== "" && !hasSpeaker || !hasSpeaker && targetUserId <= 0 || canInvite === playGranted || previewAvailable && !playGranted || playGranted && targetUserId <= 0 || !hasSpeaker && canInvite) throw contractError("voiceprint-person-contract-invalid");
10058
+ return {
10059
+ speakerId,
10060
+ targetUserId,
10061
+ displayName,
10062
+ playGranted,
10063
+ previewAvailable,
10064
+ canInvite
10065
+ };
10066
+ }
10067
+ async projectRecognizedPerson(item, session, profiles) {
10068
+ const identityKind = item.speakerId === "" ? "authorized_user" : "speaker";
10069
+ const payload = identityKind === "speaker" ? {
10070
+ version: 1,
10071
+ viewerUserId: session.userId,
10072
+ identityKind,
10073
+ speakerId: item.speakerId
10074
+ } : {
10075
+ version: 1,
10076
+ viewerUserId: session.userId,
10077
+ identityKind,
10078
+ targetUserId: item.targetUserId
10079
+ };
10080
+ const avatarRef = (item.targetUserId > 0 ? profiles.get(item.targetUserId) : void 0)?.avatarUrl === void 0 ? void 0 : await this.optionalAvatarRef(session.userId, item.targetUserId);
10081
+ return {
10082
+ personRef: await this.sealRef(PERSON_REF_PREFIX, payload),
10083
+ identityKind,
10084
+ displayName: item.displayName || "未命名声纹",
10085
+ ...avatarRef === void 0 ? {} : { avatarRef },
10086
+ playGranted: item.playGranted,
10087
+ previewAvailable: item.previewAvailable,
10088
+ canInvite: item.canInvite,
10089
+ inviteTargetSelectionRequired: item.canInvite && item.targetUserId === 0
10090
+ };
10091
+ }
10092
+ async loadPublicProfiles(userIds, session, signal) {
10093
+ try {
10094
+ return await this.profile.publicProfileSummariesByUserIds(userIds, session, signal);
10095
+ } catch (error) {
10096
+ if (signal?.aborted === true) throw error;
10097
+ return /* @__PURE__ */ new Map();
10098
+ }
10099
+ }
10100
+ async optionalAvatarRef(viewerUserId, targetUserId) {
10101
+ try {
10102
+ return await this.profile.sealProfileImageRef(viewerUserId, targetUserId);
10103
+ } catch {
10104
+ return;
10105
+ }
10106
+ }
10107
+ async sealRef(prefix, payload) {
10108
+ const encoded = encodeOpaqueJson(payload);
10109
+ return `${prefix}.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
10110
+ }
10111
+ async openPersonRef(personRef, expectedViewerUserId) {
10112
+ const normalized = personRef.trim();
10113
+ if (!validOpaqueText(normalized, 2048)) throw this.invalidPersonRef();
10114
+ const parts = normalized.split(".");
10115
+ if (parts.length !== 3 || parts[0] !== PERSON_REF_PREFIX) throw this.invalidPersonRef();
10116
+ const encoded = parts[1] ?? "";
10117
+ const supplied = Buffer.from(parts[2] ?? "", "base64url");
10118
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
10119
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw this.invalidPersonRef();
10120
+ let raw;
10121
+ try {
10122
+ raw = objectValue$1(decodeOpaqueJson(encoded));
10123
+ } catch (error) {
10124
+ throw new ArkmePluginError("voiceprint-person-ref-invalid", "已识别人引用无效", false, 400, { cause: error });
10125
+ }
10126
+ const viewerUserId = integerValue$1(raw.viewerUserId);
10127
+ if (raw.version !== 1 || viewerUserId !== expectedViewerUserId) throw this.invalidPersonRef(true);
10128
+ if (raw.identityKind === "speaker") {
10129
+ const speakerId = stringValue$1(raw.speakerId).trim().toLowerCase();
10130
+ if (!validSpeakerId(speakerId)) throw this.invalidPersonRef();
10131
+ return {
10132
+ version: 1,
10133
+ viewerUserId,
10134
+ identityKind: "speaker",
10135
+ speakerId
10136
+ };
10137
+ }
10138
+ if (raw.identityKind === "authorized_user") {
10139
+ const targetUserId = integerValue$1(raw.targetUserId);
10140
+ if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0) throw this.invalidPersonRef();
10141
+ return {
10142
+ version: 1,
10143
+ viewerUserId,
10144
+ identityKind: "authorized_user",
10145
+ targetUserId
10146
+ };
10147
+ }
10148
+ throw this.invalidPersonRef();
10149
+ }
10150
+ async openGrantRef(grantRef, expectedViewerUserId) {
10151
+ const normalized = grantRef.trim();
10152
+ if (!validOpaqueText(normalized, 2048)) throw this.invalidGrantRef();
10153
+ const parts = normalized.split(".");
10154
+ if (parts.length !== 3 || parts[0] !== GRANT_REF_PREFIX) throw this.invalidGrantRef();
10155
+ const encoded = parts[1] ?? "";
10156
+ const supplied = Buffer.from(parts[2] ?? "", "base64url");
10157
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
10158
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw this.invalidGrantRef();
10159
+ let raw;
10160
+ try {
10161
+ raw = objectValue$1(decodeOpaqueJson(encoded));
10162
+ } catch (error) {
10163
+ throw new ArkmePluginError("voiceprint-grant-ref-invalid", "声纹授权引用无效", false, 400, { cause: error });
10164
+ }
10165
+ const viewerUserId = integerValue$1(raw.viewerUserId);
10166
+ const granteeUserId = integerValue$1(raw.granteeUserId);
10167
+ if (raw.version !== 1 || viewerUserId !== expectedViewerUserId || !Number.isSafeInteger(granteeUserId) || granteeUserId <= 0 || granteeUserId === expectedViewerUserId) throw this.invalidGrantRef(viewerUserId !== expectedViewerUserId);
10168
+ return {
10169
+ version: 1,
10170
+ viewerUserId,
10171
+ granteeUserId
10172
+ };
10173
+ }
10174
+ invitationUrl(token, previewToken, compact) {
10175
+ const base = this.runtime.config.environment === "prod" ? "https://jiwo.cc" : "https://jotmo-app.senguo.me";
10176
+ const url = new URL(compact ? "/v" : "/app/voiceprint/invite", base);
10177
+ url.searchParams.set("p", previewToken);
10178
+ url.hash = `t=${token}`;
10179
+ return url.toString();
10180
+ }
10181
+ invalidPersonRef(accountMismatch = false) {
10182
+ return new ArkmePluginError("voiceprint-person-ref-invalid", accountMismatch ? "已识别人引用与当前账号不匹配" : "已识别人引用无效", false, accountMismatch ? 403 : 400);
10183
+ }
10184
+ invalidGrantRef(accountMismatch = false) {
10185
+ return new ArkmePluginError("voiceprint-grant-ref-invalid", accountMismatch ? "声纹授权引用与当前账号不匹配" : "声纹授权引用无效", false, accountMismatch ? 403 : 400);
10186
+ }
10187
+ };
10188
+ //#endregion
9419
10189
  //#region src/services/world-service.ts
9420
10190
  const ARKME_WORLD_IMAGE_REF_TTL_MILLIS = 900 * 1e3;
9421
10191
  const MAX_ARKME_WORLD_IMAGE_REFS = 2048;
10192
+ const ARKME_WORLD_AVATAR_RESOLUTION_CACHE_TTL_MILLIS = 300 * 1e3;
10193
+ const MAX_ARKME_WORLD_AVATAR_RESOLUTION_CACHE_ENTRIES = 2048;
9422
10194
  const ARKME_WORLD_RECORD_REF_TTL_MILLIS = 900 * 1e3;
9423
10195
  const MAX_ARKME_WORLD_RECORD_REFS = 4096;
9424
10196
  const ARKME_VOICEPRINT_PLAY_SCOPE = 2;
@@ -9470,6 +10242,9 @@ function integerValue(value) {
9470
10242
  function listValue(value) {
9471
10243
  return Array.isArray(value) ? value : [];
9472
10244
  }
10245
+ function isTopLevelWorldRecord(raw) {
10246
+ return stringValue$1(objectValue$1(raw).parent_record_uid).trim() === "";
10247
+ }
9473
10248
  function safeFailureMessage(error) {
9474
10249
  if (error instanceof ArkmePluginError) return error.message;
9475
10250
  if (error instanceof Error && error.message.trim() !== "") return error.message;
@@ -9565,12 +10340,21 @@ function worldAvatarResolutionKey(ownerUserId, avatarRef) {
9565
10340
  if (!Number.isSafeInteger(ownerUserId) || ownerUserId <= 0 || !normalized.startsWith("file_asset://")) return "";
9566
10341
  return `${String(ownerUserId)}|${normalized}`;
9567
10342
  }
10343
+ function worldImageAssetIdentity(raw) {
10344
+ try {
10345
+ const parsed = new URL(raw.trim());
10346
+ return `${parsed.hostname.toLowerCase()}${parsed.pathname}`;
10347
+ } catch {
10348
+ return raw.trim();
10349
+ }
10350
+ }
9568
10351
  var WorldService = class {
9569
10352
  runtime;
9570
10353
  profile;
9571
10354
  media;
9572
10355
  record;
9573
10356
  worldImageRefs = /* @__PURE__ */ new Map();
10357
+ worldAvatarResolutionCache = /* @__PURE__ */ new Map();
9574
10358
  worldRecordRefs = /* @__PURE__ */ new Map();
9575
10359
  voiceprintSocialCache = /* @__PURE__ */ new Map();
9576
10360
  voiceprintSocialInFlight = /* @__PURE__ */ new Map();
@@ -9582,6 +10366,7 @@ var WorldService = class {
9582
10366
  }
9583
10367
  dispose() {
9584
10368
  this.worldImageRefs.clear();
10369
+ this.worldAvatarResolutionCache.clear();
9585
10370
  this.worldRecordRefs.clear();
9586
10371
  this.voiceprintSocialCache.clear();
9587
10372
  this.voiceprintSocialInFlight.clear();
@@ -9629,22 +10414,30 @@ var WorldService = class {
9629
10414
  async listMyWorldFeed(options = {}) {
9630
10415
  const session = await this.runtime.requireSession();
9631
10416
  const limit = Math.min(20, Math.max(1, Math.trunc(options.limit ?? 20)));
9632
- const offset = Math.max(0, Math.trunc(options.offset ?? 0));
9633
- const data = await this.runtime.authenticatedWorldPost("/api/v1/public-record/my-list", {
9634
- limit,
9635
- offset
9636
- }, session, options.signal);
9637
- const rawItems = listValue(data.list);
9638
- const resolvedAvatars = await this.resolveWorldAvatarUrls(rawItems, session, options.signal);
9639
- const items = (await Promise.all(rawItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
9640
- const total = Math.max(0, Math.trunc(numberValue(data.total)));
9641
- const nextOffset = offset + rawItems.length;
9642
- const hasMore = rawItems.length > 0 && nextOffset < total;
10417
+ let offset = Math.max(0, Math.trunc(options.offset ?? 0));
10418
+ let total = 0;
10419
+ let items = [];
10420
+ let hasMore = false;
10421
+ let attempts = 0;
10422
+ do {
10423
+ const data = await this.runtime.authenticatedWorldPost("/api/v1/public-record/my-list", {
10424
+ limit,
10425
+ offset
10426
+ }, session, options.signal);
10427
+ const rawItems = listValue(data.list);
10428
+ const rootItems = rawItems.filter(isTopLevelWorldRecord);
10429
+ const resolvedAvatars = await this.resolveWorldAvatarUrls(rootItems, session, options.signal);
10430
+ items = (await Promise.all(rootItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
10431
+ total = Math.max(0, Math.trunc(numberValue(data.total)));
10432
+ offset += rawItems.length;
10433
+ hasMore = rawItems.length > 0 && offset < total;
10434
+ attempts += 1;
10435
+ } while (items.length === 0 && hasMore && attempts < 4);
9643
10436
  return {
9644
10437
  items,
9645
10438
  total,
9646
10439
  hasMore,
9647
- ...hasMore ? { nextOffset } : {}
10440
+ ...hasMore ? { nextOffset: offset } : {}
9648
10441
  };
9649
10442
  }
9650
10443
  async listUserWorldFeed(userId, options = {}) {
@@ -9664,7 +10457,7 @@ var WorldService = class {
9664
10457
  }, void 0, [200], options.signal);
9665
10458
  const rawItems = listValue(data.list);
9666
10459
  total = Math.max(0, Math.trunc(numberValue(data.total)));
9667
- const rootItems = rawItems.filter((raw) => stringValue$1(objectValue$1(raw).parent_record_uid).trim() === "");
10460
+ const rootItems = rawItems.filter(isTopLevelWorldRecord);
9668
10461
  const resolvedAvatars = await this.resolveWorldAvatarUrls(rootItems, session, options.signal);
9669
10462
  items = (await Promise.all(rootItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
9670
10463
  offset += rawItems.length;
@@ -9714,7 +10507,8 @@ var WorldService = class {
9714
10507
  const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/generate-playback", {
9715
10508
  source_scene: 4,
9716
10509
  source_id: entry.recordUid,
9717
- source_chunk_index: chunkIndex
10510
+ source_chunk_index: chunkIndex,
10511
+ source_chunk_max_runes: 120
9718
10512
  }, session, input.signal, {
9719
10513
  lane: "write",
9720
10514
  bypassCache: true
@@ -10053,7 +10847,7 @@ var WorldService = class {
10053
10847
  async readWorldImage(imageRef, options = {}) {
10054
10848
  const session = await this.runtime.requireSession();
10055
10849
  const entry = await this.openWorldImageRef(imageRef, session.userId);
10056
- const byteLimit = Math.min(MAX_ARKME_IMAGE_BYTES, Math.max(1, Math.trunc(options.maxBytes ?? 2097152)));
10850
+ const byteLimit = Math.min(ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, Math.max(1, Math.trunc(options.maxBytes ?? 20971520)));
10057
10851
  return await this.media.downloadSignedImage(trustedWorldImageUrl(this.runtime.config.environment, entry.sourceUrl), byteLimit, options.signal);
10058
10852
  }
10059
10853
  async publishWorldTextForConversation(recordUid, textContent, signal) {
@@ -10243,11 +11037,11 @@ var WorldService = class {
10243
11037
  const avatarUrl = rawAvatar.startsWith("file_asset://") ? resolvedAvatars.get(worldAvatarResolutionKey(ownerUserId, rawAvatar)) ?? "" : rawAvatar;
10244
11038
  const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
10245
11039
  const imageRefs = [];
10246
- for (const signedUrl of rawImages.slice(0, 9)) {
11040
+ for (const [index, signedUrl] of rawImages.slice(0, 9).entries()) {
10247
11041
  if (!this.isTrustedWorldImageUrl(signedUrl)) continue;
10248
- imageRefs.push(await this.sealWorldImageRef(viewerUserId, signedUrl));
11042
+ imageRefs.push(await this.sealWorldImageRef(viewerUserId, signedUrl, `record:${recordUid}:image:${String(index)}:${worldImageAssetIdentity(signedUrl)}`));
10249
11043
  }
10250
- const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
11044
+ const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl, `avatar:${String(ownerUserId)}:${rawAvatar.startsWith("file_asset://") ? rawAvatar : worldImageAssetIdentity(avatarUrl)}`) : void 0;
10251
11045
  return {
10252
11046
  recordRef: await this.worldRecordRef(viewerUserId, recordUid, {
10253
11047
  ownerUserId,
@@ -10295,19 +11089,40 @@ var WorldService = class {
10295
11089
  });
10296
11090
  }
10297
11091
  if (requested.size === 0) return /* @__PURE__ */ new Map();
11092
+ const now = Date.now();
11093
+ for (const [key, entry] of this.worldAvatarResolutionCache) if (entry.expiresAtMillis <= now) this.worldAvatarResolutionCache.delete(key);
11094
+ const resolved = /* @__PURE__ */ new Map();
11095
+ for (const [key] of requested) {
11096
+ const cached = this.worldAvatarResolutionCache.get(key);
11097
+ if (cached === void 0) continue;
11098
+ this.worldAvatarResolutionCache.delete(key);
11099
+ this.worldAvatarResolutionCache.set(key, cached);
11100
+ resolved.set(key, cached.sourceUrl);
11101
+ }
11102
+ const unresolved = [...requested].filter(([key]) => !resolved.has(key)).map(([, value]) => value);
11103
+ if (unresolved.length === 0) return resolved;
10298
11104
  let data;
10299
11105
  try {
10300
- data = await this.runtime.authenticatedAuthPost("/api/v1/auth/resolve-avatar-refs", { items: [...requested.values()] }, session, signal);
11106
+ data = await this.runtime.authenticatedAuthPost("/api/v1/auth/resolve-avatar-refs", { items: unresolved }, session, signal);
10301
11107
  } catch {
10302
- return /* @__PURE__ */ new Map();
11108
+ return resolved;
10303
11109
  }
10304
- const resolved = /* @__PURE__ */ new Map();
10305
11110
  for (const raw of listValue(data.items)) {
10306
11111
  const item = objectValue$1(raw);
10307
11112
  const key = worldAvatarResolutionKey(Math.trunc(numberValue(item.owner_user_id)), stringValue$1(item.avatar_ref).trim());
10308
11113
  const url = stringValue$1(item.url).trim();
10309
11114
  if (!requested.has(key) || !this.isTrustedWorldImageUrl(url)) continue;
10310
11115
  resolved.set(key, url);
11116
+ this.worldAvatarResolutionCache.delete(key);
11117
+ this.worldAvatarResolutionCache.set(key, {
11118
+ sourceUrl: url,
11119
+ expiresAtMillis: Date.now() + ARKME_WORLD_AVATAR_RESOLUTION_CACHE_TTL_MILLIS
11120
+ });
11121
+ }
11122
+ while (this.worldAvatarResolutionCache.size > MAX_ARKME_WORLD_AVATAR_RESOLUTION_CACHE_ENTRIES) {
11123
+ const oldest = this.worldAvatarResolutionCache.keys().next().value;
11124
+ if (oldest === void 0) break;
11125
+ this.worldAvatarResolutionCache.delete(oldest);
10311
11126
  }
10312
11127
  return resolved;
10313
11128
  }
@@ -10324,7 +11139,7 @@ var WorldService = class {
10324
11139
  const rawAvatar = stringValue$1(item.avatar ?? item.head_img).trim();
10325
11140
  const avatarUrl = rawAvatar.startsWith("file_asset://") ? resolvedAvatars.get(worldAvatarResolutionKey(ownerUserId, rawAvatar)) ?? "" : rawAvatar;
10326
11141
  const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
10327
- const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
11142
+ const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl, `avatar:${String(ownerUserId)}:${rawAvatar.startsWith("file_asset://") ? rawAvatar : worldImageAssetIdentity(avatarUrl)}`) : void 0;
10328
11143
  return {
10329
11144
  interactionRef: await this.worldRecordRef(viewerUserId, recordUid, {
10330
11145
  ownerUserId,
@@ -10389,11 +11204,12 @@ var WorldService = class {
10389
11204
  this.worldImageRefs.delete(oldest);
10390
11205
  }
10391
11206
  }
10392
- async sealWorldImageRef(viewerUserId, sourceUrl) {
11207
+ async sealWorldImageRef(viewerUserId, sourceUrl, stableIdentity) {
10393
11208
  const now = Date.now();
10394
11209
  this.pruneWorldImageRefs(now);
10395
- const token = randomUUID();
10396
- const signature = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`world-image-v1:${String(viewerUserId)}:${token}`).digest("base64url");
11210
+ const uniqueCode = await this.runtime.stateStore.uniqueCode();
11211
+ const token = createHmac("sha256", uniqueCode).update(`world-image-token-v1:${String(viewerUserId)}:${stableIdentity}`).digest("base64url");
11212
+ const signature = createHmac("sha256", uniqueCode).update(`world-image-v1:${String(viewerUserId)}:${token}`).digest("base64url");
10397
11213
  this.worldImageRefs.set(token, {
10398
11214
  viewerUserId,
10399
11215
  sourceUrl,
@@ -10480,6 +11296,7 @@ var ArkmeService = class {
10480
11296
  aiPolish;
10481
11297
  chat;
10482
11298
  contact;
11299
+ voiceprint;
10483
11300
  constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker()) {
10484
11301
  this.config = config;
10485
11302
  this.sessionStore = sessionStore;
@@ -10514,9 +11331,10 @@ var ArkmeService = class {
10514
11331
  this.community = new CommunityService(this.runtime, this.source, this.profile);
10515
11332
  this.interwoven = new InterwovenService(this.runtime, this.source, this.profile);
10516
11333
  this.aiPolish = new GroupAiPolishService(this.runtime, this.source, { sendChatSourceTextRaw: async (...args) => await this.chat.sendChatSourceTextRaw(...args) });
10517
- this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session) => await this.chat.chatTimelineItems(data, session) });
11334
+ this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session, chatSessionUid) => await this.chat.chatTimelineItems(data, session, chatSessionUid) });
10518
11335
  this.chat = new ChatService(this.runtime, this.source, this.profile, this.media, this.record, this.bot, this.arko, this.aiPolish, this.realtime);
10519
11336
  this.contact = new ContactService(this.runtime, this.source, this.profile, this.realtime);
11337
+ this.voiceprint = new VoiceprintService(this.runtime, this.profile, { resolveRegisteredContactUserId: async (contactRef, session, signal) => await this.contact.resolveRegisteredContactUserId(contactRef, session, signal) });
10520
11338
  this.auth = new AuthService(this.runtime, this.profile, {
10521
11339
  reconnectChatRealtime: () => {
10522
11340
  this.realtime.reconnect();
@@ -10599,6 +11417,7 @@ var ArkmeService = class {
10599
11417
  environment: this.config.environment,
10600
11418
  testLoginEnabled: this.config.environment === "test",
10601
11419
  callAssetBasePath: `${this.config.routePath}/call`,
11420
+ voiceprintEnrollmentPath: `${this.config.routePath}/voiceprint/enroll`,
10602
11421
  shareWebsite: this.config.shareWebsite ?? "https://app.arkme.ai"
10603
11422
  };
10604
11423
  }
@@ -10644,6 +11463,7 @@ var ArkmeService = class {
10644
11463
  worldVoiceprintPlayback: true,
10645
11464
  worldVoiceprintInvite: true,
10646
11465
  worldVoiceprintSocialContext: true,
11466
+ voiceprintManagement: true,
10647
11467
  arrangements: true,
10648
11468
  myExtensions: true,
10649
11469
  extensionPublish: true,
@@ -10857,6 +11677,12 @@ var ArkmeService = class {
10857
11677
  async listGroupMembers(sourceRef, options = {}) {
10858
11678
  return await this.group.listGroupMembers(sourceRef, options);
10859
11679
  }
11680
+ async listSourceMembers(sourceRef, options = {}) {
11681
+ return await this.chat.listSourceMembers(sourceRef, options);
11682
+ }
11683
+ async sourceMemberRecords(sourceRef, memberRef, mode, options = {}) {
11684
+ return await this.chat.sourceMemberRecords(sourceRef, memberRef, mode, options);
11685
+ }
10860
11686
  async listGroupMemberCandidates(sourceRef, options = {}) {
10861
11687
  return await this.group.listGroupMemberCandidates(sourceRef, options);
10862
11688
  }
@@ -10893,6 +11719,9 @@ var ArkmeService = class {
10893
11719
  async openPrivateChatFromUser(peerUserId, options = {}) {
10894
11720
  return await this.chat.openPrivateChatFromUser(peerUserId, options);
10895
11721
  }
11722
+ async openPrivateChatFromMember(sourceRef, memberRef, options = {}) {
11723
+ return await this.chat.openPrivateChatFromMember(sourceRef, memberRef, options);
11724
+ }
10896
11725
  async readSource(sourceRef, options = {}) {
10897
11726
  return await this.chat.readSource(sourceRef, options);
10898
11727
  }
@@ -11122,6 +11951,36 @@ var ArkmeService = class {
11122
11951
  throw error;
11123
11952
  }
11124
11953
  }
11954
+ async myVoiceprint(options = {}) {
11955
+ return await this.voiceprint.myVoiceprint(options);
11956
+ }
11957
+ async outboundVoiceprintGrants(input, options = {}) {
11958
+ return await this.voiceprint.outboundGrants(input, options);
11959
+ }
11960
+ async recognizedVoiceprintPeople(input, options = {}) {
11961
+ return await this.voiceprint.recognizedPeople(input, options);
11962
+ }
11963
+ async recognizedVoiceprintPerson(personRef, options = {}) {
11964
+ return await this.voiceprint.recognizedPerson(personRef, options);
11965
+ }
11966
+ async recognizedPersonVoiceprints(personRef, options = {}) {
11967
+ return await this.voiceprint.recognizedPersonVoiceprints(personRef, options);
11968
+ }
11969
+ async createVoiceprintInvitation(options = {}) {
11970
+ return await this.voiceprint.createInvitation(options);
11971
+ }
11972
+ async createRecognizedPersonVoiceprintInvitation(personRef, targetContactRef, options = {}) {
11973
+ return await this.voiceprint.createRecognizedPersonInvitation(personRef, targetContactRef, options);
11974
+ }
11975
+ async revokeVoiceprintPlaybackGrant(grantRef, options = {}) {
11976
+ return await this.voiceprint.revokePlaybackGrant(grantRef, options);
11977
+ }
11978
+ async restoreVoiceprintPlayback(options = {}) {
11979
+ return await this.voiceprint.restorePlayback(options);
11980
+ }
11981
+ async bindVoiceprintEnrollment() {
11982
+ return await this.voiceprint.bindEnrollment();
11983
+ }
11125
11984
  /** Read the authenticated comment/reply tree behind one account-bound World reference. */
11126
11985
  async listWorldInteractions(recordRef, options = {}) {
11127
11986
  return await this.world.listWorldInteractions(recordRef, options);
@@ -12527,4 +13386,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
12527
13386
  return jsonValue(await handler(jsonValue(args)));
12528
13387
  }
12529
13388
  //#endregion
12530
- export { unpackArkmeExtension as A, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as B, packBundleFiles as C, assertExtensionArtifactSize as D, renderArkmeSandboxHostEntry as E, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as F, ArkmePluginError as H, ArkmeService as I, ARKME_DEFAULT_SHARE_WEBSITE as L, ARKME_EXTENSION_ICON_MAX_BYTES as M, ARKME_EXTENSION_MAX_BYTES as N, canonicalExtensionJson as O, ARKME_EXTENSION_PREVIEW_MAX_BYTES as P, ARKME_PROVIDER_CONTRACT_VERSION as R, inspectNativeBundleArtifactV3 as S, readNativeBundleTarballV3 as T, ArkmeOutgoingCallError as U, isArkmeBotAvatarRef as V, ArkmeBundleArtifactError as _, persistentArkmeExtensionActive as a, canonicalBundleJson as b, readPersistentExtensionActivation as c, verifyBundleResolutionSignature as d, verifyExtensionResolutionSignature as f, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as g, ARKME_BUNDLE_MAX_BYTES as h, invokePersistentArkmeExtension as i, ARKME_EXTENSION_FORMAT as j, sha256Hex as k, writePersistentExtensionActivation as l, ARKME_BUNDLE_ARTIFACT_KIND as m, applyPersistentArkmeHostExtension as n, persistentArkmeExtensionRuntimeState as o, verifyNativeBundleResolutionSignature as p, deactivatePersistentArkmeExtension as r, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, renderArkmeBundleClientBundle as u, arkmeSandboxEntryId as v, packLocalNativeBundleDirectoryV3 as w, inspectBundleArtifact as x, bundleSha256 as y, ARKME_WORLD_PUBLISH_MAX_IMAGES as z };
13389
+ export { unpackArkmeExtension as A, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as B, packBundleFiles as C, assertExtensionArtifactSize as D, renderArkmeSandboxHostEntry as E, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as F, isArkmeBotAvatarRef as G, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as H, ArkmeService as I, ArkmePluginError as K, validateVoiceprintPcm16Wav as L, ARKME_EXTENSION_ICON_MAX_BYTES as M, ARKME_EXTENSION_MAX_BYTES as N, canonicalExtensionJson as O, ARKME_EXTENSION_PREVIEW_MAX_BYTES as P, ARKME_DEFAULT_SHARE_WEBSITE as R, inspectNativeBundleArtifactV3 as S, readNativeBundleTarballV3 as T, ARKME_WORLD_PUBLISH_MAX_IMAGES as U, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as V, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as W, ArkmeBundleArtifactError as _, persistentArkmeExtensionActive as a, canonicalBundleJson as b, readPersistentExtensionActivation as c, verifyBundleResolutionSignature as d, verifyExtensionResolutionSignature as f, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as g, ARKME_BUNDLE_MAX_BYTES as h, invokePersistentArkmeExtension as i, ARKME_EXTENSION_FORMAT as j, sha256Hex as k, writePersistentExtensionActivation as l, ARKME_BUNDLE_ARTIFACT_KIND as m, applyPersistentArkmeHostExtension as n, persistentArkmeExtensionRuntimeState as o, verifyNativeBundleResolutionSignature as p, ArkmeOutgoingCallError as q, deactivatePersistentArkmeExtension as r, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, renderArkmeBundleClientBundle as u, arkmeSandboxEntryId as v, packLocalNativeBundleDirectoryV3 as w, inspectBundleArtifact as x, bundleSha256 as y, ARKME_PROVIDER_CONTRACT_VERSION as z };