@senguoyun/dsh-arkme 0.1.13 → 0.1.15

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 (233) hide show
  1. package/docs/consumer-plugin-contract.md +3 -1
  2. package/docs/extension-market-controls.md +46 -7
  3. package/docs/gui-production-integration.md +10 -9
  4. package/lib/client.js +45125 -43501
  5. package/lib/client.js.map +1 -1
  6. package/lib/extension-profile-restart-helper.js +2 -2
  7. package/lib/index.js +1180 -442
  8. package/lib/{install-store-Sn3Ccxfu.js → install-store-Beau1y-o.js} +12 -6
  9. package/lib/persistent-extension.js +1 -1
  10. package/lib/{persistent-runtime-Cki0XC3L.js → persistent-runtime-D_hNgTX3.js} +1479 -249
  11. package/lib/{plugin-updater-helper-CiPlNqBc.js → plugin-updater-helper-BEYBTOld.js} +5 -5
  12. package/lib/plugin-updater-helper.js +1 -1
  13. package/lib/{profile-package-manager-DvzGgkqe.js → profile-package-manager-DwuQu8ZL.js} +91 -3
  14. package/lib/sdk.js +74 -17
  15. package/lib/types/arkme-service.d.ts +62 -9
  16. package/lib/types/arkme-service.d.ts.map +1 -1
  17. package/lib/types/bot-avatar-ref.d.ts +3 -0
  18. package/lib/types/bot-avatar-ref.d.ts.map +1 -0
  19. package/lib/types/client/ArkmeArkoSurface.d.ts.map +1 -1
  20. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  21. package/lib/types/client/ArkmeBotCreateDialog.d.ts +6 -0
  22. package/lib/types/client/ArkmeBotCreateDialog.d.ts.map +1 -0
  23. package/lib/types/client/ArkmeCalendarSurface.d.ts +4 -1
  24. package/lib/types/client/ArkmeCalendarSurface.d.ts.map +1 -1
  25. package/lib/types/client/ArkmeChatMemberActions.d.ts +48 -0
  26. package/lib/types/client/ArkmeChatMemberActions.d.ts.map +1 -0
  27. package/lib/types/client/ArkmeConversationSurface.d.ts +8 -18
  28. package/lib/types/client/ArkmeConversationSurface.d.ts.map +1 -1
  29. package/lib/types/client/ArkmeExtensionPublishDialog.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeExtensionReviews.d.ts +4 -2
  31. package/lib/types/client/ArkmeExtensionReviews.d.ts.map +1 -1
  32. package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
  33. package/lib/types/client/ArkmeGroupChatControls.d.ts +3 -1
  34. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  35. package/lib/types/client/ArkmeLogin.d.ts.map +1 -1
  36. package/lib/types/client/ArkmeMarketplace.d.ts +31 -6
  37. package/lib/types/client/ArkmeMarketplace.d.ts.map +1 -1
  38. package/lib/types/client/ArkmeMentionTextarea.d.ts +13 -0
  39. package/lib/types/client/ArkmeMentionTextarea.d.ts.map +1 -0
  40. package/lib/types/client/ArkmePersistentShell.d.ts +20 -0
  41. package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -0
  42. package/lib/types/client/ArkmeProductNavigation.d.ts +4 -3
  43. package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
  44. package/lib/types/client/ArkmeQuickAdd.d.ts.map +1 -1
  45. package/lib/types/client/ArkmeRecordingSurface.d.ts +3 -109
  46. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  47. package/lib/types/client/ArkmeRichContent.d.ts +7 -1
  48. package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
  49. package/lib/types/client/ArkmeSettingsRow.d.ts +1 -1
  50. package/lib/types/client/ArkmeSettingsRow.d.ts.map +1 -1
  51. package/lib/types/client/ArkmeSettingsSurface.d.ts +4 -1
  52. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
  53. package/lib/types/client/ArkmeSidebar.d.ts +9 -1
  54. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  55. package/lib/types/client/ArkmeStartupAuthGate.d.ts +4 -2
  56. package/lib/types/client/ArkmeStartupAuthGate.d.ts.map +1 -1
  57. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +9 -9
  58. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  59. package/lib/types/client/ArkmeVoiceprintSurface.d.ts +61 -0
  60. package/lib/types/client/ArkmeVoiceprintSurface.d.ts.map +1 -0
  61. package/lib/types/client/ArkmeWorldSurface.d.ts +104 -4
  62. package/lib/types/client/ArkmeWorldSurface.d.ts.map +1 -1
  63. package/lib/types/client/DeepSeekHarnessSurface.d.ts +6 -0
  64. package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -0
  65. package/lib/types/client/api.d.ts +1 -1
  66. package/lib/types/client/api.d.ts.map +1 -1
  67. package/lib/types/client/arkme-emoji.d.ts +3 -0
  68. package/lib/types/client/arkme-emoji.d.ts.map +1 -0
  69. package/lib/types/client/composer-draft-store.d.ts +18 -0
  70. package/lib/types/client/composer-draft-store.d.ts.map +1 -1
  71. package/lib/types/client/composer-focus.d.ts +8 -0
  72. package/lib/types/client/composer-focus.d.ts.map +1 -0
  73. package/lib/types/client/conversation-composer-presentation.d.ts +47 -0
  74. package/lib/types/client/conversation-composer-presentation.d.ts.map +1 -0
  75. package/lib/types/client/conversation-memory-cache.d.ts +30 -1
  76. package/lib/types/client/conversation-memory-cache.d.ts.map +1 -1
  77. package/lib/types/client/index.d.ts +6 -4
  78. package/lib/types/client/index.d.ts.map +1 -1
  79. package/lib/types/client/interwoven-moments.d.ts +2 -1
  80. package/lib/types/client/interwoven-moments.d.ts.map +1 -1
  81. package/lib/types/client/marketplace-sort-preference.d.ts +5 -0
  82. package/lib/types/client/marketplace-sort-preference.d.ts.map +1 -0
  83. package/lib/types/client/navigation-cache.d.ts +4 -0
  84. package/lib/types/client/navigation-cache.d.ts.map +1 -1
  85. package/lib/types/client/provider-instance-runtime.d.ts +20 -0
  86. package/lib/types/client/provider-instance-runtime.d.ts.map +1 -0
  87. package/lib/types/client/redesign/ArkmeRootFrame.d.ts +19 -0
  88. package/lib/types/client/redesign/ArkmeRootFrame.d.ts.map +1 -0
  89. package/lib/types/client/redesign/ArkmeTaskSurface.d.ts +15 -0
  90. package/lib/types/client/redesign/ArkmeTaskSurface.d.ts.map +1 -0
  91. package/lib/types/client/redesign/ArkmeWorkspaceDialog.d.ts +9 -0
  92. package/lib/types/client/redesign/ArkmeWorkspaceDialog.d.ts.map +1 -0
  93. package/lib/types/client/redesign/layout-controller.d.ts +17 -0
  94. package/lib/types/client/redesign/layout-controller.d.ts.map +1 -0
  95. package/lib/types/client/redesign/styles.d.ts +3 -0
  96. package/lib/types/client/redesign/styles.d.ts.map +1 -0
  97. package/lib/types/client/redesign/task-session.d.ts +14 -0
  98. package/lib/types/client/redesign/task-session.d.ts.map +1 -0
  99. package/lib/types/client/redesign/theme-presenter.d.ts +10 -0
  100. package/lib/types/client/redesign/theme-presenter.d.ts.map +1 -0
  101. package/lib/types/client/related-recordings.d.ts +1 -0
  102. package/lib/types/client/related-recordings.d.ts.map +1 -1
  103. package/lib/types/client/slots-contract.d.ts +2 -2
  104. package/lib/types/client/slots-contract.d.ts.map +1 -1
  105. package/lib/types/client/source-list.d.ts +4 -0
  106. package/lib/types/client/source-list.d.ts.map +1 -1
  107. package/lib/types/client/ui-controller.d.ts +20 -9
  108. package/lib/types/client/ui-controller.d.ts.map +1 -1
  109. package/lib/types/client/voiceprint-enrollment-client.d.ts +12 -0
  110. package/lib/types/client/voiceprint-enrollment-client.d.ts.map +1 -0
  111. package/lib/types/client/voiceprint-recorder.d.ts +23 -0
  112. package/lib/types/client/voiceprint-recorder.d.ts.map +1 -0
  113. package/lib/types/client/world-voiceprint-expectation-copy.d.ts +7 -0
  114. package/lib/types/client/world-voiceprint-expectation-copy.d.ts.map +1 -0
  115. package/lib/types/client/world-voiceprint-playback.d.ts +41 -0
  116. package/lib/types/client/world-voiceprint-playback.d.ts.map +1 -0
  117. package/lib/types/extensions/install-store.d.ts +1 -0
  118. package/lib/types/extensions/install-store.d.ts.map +1 -1
  119. package/lib/types/extensions/manager.d.ts +5 -0
  120. package/lib/types/extensions/manager.d.ts.map +1 -1
  121. package/lib/types/extensions/owned-inventory.d.ts +30 -2
  122. package/lib/types/extensions/owned-inventory.d.ts.map +1 -1
  123. package/lib/types/extensions/owned-refs.d.ts +1 -0
  124. package/lib/types/extensions/owned-refs.d.ts.map +1 -1
  125. package/lib/types/extensions/owned-store.d.ts +9 -0
  126. package/lib/types/extensions/owned-store.d.ts.map +1 -1
  127. package/lib/types/extensions/profile-installer.d.ts.map +1 -1
  128. package/lib/types/extensions/publish-client.d.ts +2 -0
  129. package/lib/types/extensions/publish-client.d.ts.map +1 -1
  130. package/lib/types/extensions/publisher-role.d.ts +9 -0
  131. package/lib/types/extensions/publisher-role.d.ts.map +1 -0
  132. package/lib/types/extensions/types.d.ts +11 -0
  133. package/lib/types/extensions/types.d.ts.map +1 -1
  134. package/lib/types/host-api.d.ts +1 -1
  135. package/lib/types/host-api.d.ts.map +1 -1
  136. package/lib/types/index.d.ts +2 -2
  137. package/lib/types/index.d.ts.map +1 -1
  138. package/lib/types/local-database.d.ts +1 -0
  139. package/lib/types/local-database.d.ts.map +1 -1
  140. package/lib/types/openclaw/secret-provider.d.ts.map +1 -1
  141. package/lib/types/outgoing-call-assets.d.ts.map +1 -1
  142. package/lib/types/plugin-update-install-state.d.ts.map +1 -1
  143. package/lib/types/plugin-update-state.d.ts.map +1 -1
  144. package/lib/types/plugin-update.d.ts +6 -0
  145. package/lib/types/plugin-update.d.ts.map +1 -1
  146. package/lib/types/private-filesystem.d.ts +5 -0
  147. package/lib/types/private-filesystem.d.ts.map +1 -0
  148. package/lib/types/recording-presentation.d.ts +2 -2
  149. package/lib/types/recording-presentation.d.ts.map +1 -1
  150. package/lib/types/sdk/index.d.ts +31 -8
  151. package/lib/types/sdk/index.d.ts.map +1 -1
  152. package/lib/types/services/bot-service.d.ts.map +1 -1
  153. package/lib/types/services/chat-realtime-service.d.ts +1 -1
  154. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  155. package/lib/types/services/chat-service.d.ts +21 -2
  156. package/lib/types/services/chat-service.d.ts.map +1 -1
  157. package/lib/types/services/contact-service.d.ts +2 -0
  158. package/lib/types/services/contact-service.d.ts.map +1 -1
  159. package/lib/types/services/group-service.d.ts.map +1 -1
  160. package/lib/types/services/record-service.d.ts +2 -1
  161. package/lib/types/services/record-service.d.ts.map +1 -1
  162. package/lib/types/services/recording-service.d.ts +2 -4
  163. package/lib/types/services/recording-service.d.ts.map +1 -1
  164. package/lib/types/services/service.d.ts +5 -2
  165. package/lib/types/services/service.d.ts.map +1 -1
  166. package/lib/types/services/source-service.d.ts.map +1 -1
  167. package/lib/types/services/voiceprint-service.d.ts +79 -0
  168. package/lib/types/services/voiceprint-service.d.ts.map +1 -0
  169. package/lib/types/services/world-service.d.ts +41 -6
  170. package/lib/types/services/world-service.d.ts.map +1 -1
  171. package/lib/types/state-store.d.ts.map +1 -1
  172. package/lib/types/tools/business/bots/create.d.ts.map +1 -1
  173. package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
  174. package/lib/types/tools/business/conversation/member-records.d.ts +3 -0
  175. package/lib/types/tools/business/conversation/member-records.d.ts.map +1 -0
  176. package/lib/types/tools/business/index.d.ts.map +1 -1
  177. package/lib/types/tools/business/recordings/index.d.ts +1 -2
  178. package/lib/types/tools/business/recordings/index.d.ts.map +1 -1
  179. package/lib/types/tools/business/voiceprint/grants.d.ts +2 -0
  180. package/lib/types/tools/business/voiceprint/grants.d.ts.map +1 -0
  181. package/lib/types/tools/business/voiceprint/index.d.ts +3 -0
  182. package/lib/types/tools/business/voiceprint/index.d.ts.map +1 -0
  183. package/lib/types/tools/business/voiceprint/invite.d.ts +2 -0
  184. package/lib/types/tools/business/voiceprint/invite.d.ts.map +1 -0
  185. package/lib/types/tools/business/voiceprint/recognized-invite.d.ts +2 -0
  186. package/lib/types/tools/business/voiceprint/recognized-invite.d.ts.map +1 -0
  187. package/lib/types/tools/business/voiceprint/recognized-people.d.ts +2 -0
  188. package/lib/types/tools/business/voiceprint/recognized-people.d.ts.map +1 -0
  189. package/lib/types/tools/business/voiceprint/restore.d.ts +2 -0
  190. package/lib/types/tools/business/voiceprint/restore.d.ts.map +1 -0
  191. package/lib/types/tools/business/voiceprint/revoke.d.ts +2 -0
  192. package/lib/types/tools/business/voiceprint/revoke.d.ts.map +1 -0
  193. package/lib/types/tools/business/voiceprint/status.d.ts +2 -0
  194. package/lib/types/tools/business/voiceprint/status.d.ts.map +1 -0
  195. package/lib/types/tools/business/world/index.d.ts +2 -0
  196. package/lib/types/tools/business/world/index.d.ts.map +1 -1
  197. package/lib/types/tools/business/world/user.d.ts +2 -0
  198. package/lib/types/tools/business/world/user.d.ts.map +1 -0
  199. package/lib/types/tools/business/world/voiceprint-social-context.d.ts +2 -0
  200. package/lib/types/tools/business/world/voiceprint-social-context.d.ts.map +1 -0
  201. package/lib/types/tools/extensions/index.d.ts.map +1 -1
  202. package/lib/types/tools/ports/conversations.d.ts +10 -1
  203. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  204. package/lib/types/tools/ports/index.d.ts +3 -2
  205. package/lib/types/tools/ports/index.d.ts.map +1 -1
  206. package/lib/types/tools/ports/recordings.d.ts +2 -3
  207. package/lib/types/tools/ports/recordings.d.ts.map +1 -1
  208. package/lib/types/tools/ports/voiceprint.d.ts +37 -0
  209. package/lib/types/tools/ports/voiceprint.d.ts.map +1 -0
  210. package/lib/types/tools/ports/world.d.ts +10 -1
  211. package/lib/types/tools/ports/world.d.ts.map +1 -1
  212. package/lib/types/tools/prompts/business.d.ts.map +1 -1
  213. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  214. package/lib/types/types.d.ts +150 -16
  215. package/lib/types/types.d.ts.map +1 -1
  216. package/lib/types/voiceprint-routes.d.ts +11 -0
  217. package/lib/types/voiceprint-routes.d.ts.map +1 -0
  218. package/lib/types/voiceprint-wav.d.ts +6 -0
  219. package/lib/types/voiceprint-wav.d.ts.map +1 -0
  220. package/package.json +7 -4
  221. package/assets/desktop_call/avatar-lin-xiaoman.jpeg +0 -0
  222. package/assets/desktop_call/avatar-mother.jpg +0 -0
  223. package/assets/desktop_call/avatar-self.png +0 -0
  224. package/assets/desktop_call/call-add-linear.svg +0 -13
  225. package/assets/desktop_call/icon-scan-add-contact.svg +0 -4
  226. package/assets/desktop_call/profile-circle-linear.svg +0 -11
  227. package/assets/desktop_call/user-add-linear.svg +0 -15
  228. package/lib/types/client/ArkmeCallHistorySurface.d.ts +0 -57
  229. package/lib/types/client/ArkmeCallHistorySurface.d.ts.map +0 -1
  230. package/lib/types/client/demo-audio.d.ts +0 -2
  231. package/lib/types/client/demo-audio.d.ts.map +0 -1
  232. package/lib/types/client/personal-test-edition.d.ts +0 -17
  233. package/lib/types/client/personal-test-edition.d.ts.map +0 -1
@@ -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);
@@ -2454,6 +2466,13 @@ var AuthService = class {
2454
2466
  }
2455
2467
  };
2456
2468
  //#endregion
2469
+ //#region src/bot-avatar-ref.ts
2470
+ const ARKME_BOT_AVATAR_REF_PATTERN = /^file_asset:\/\/[A-Za-z0-9_-]{8,128}$/;
2471
+ /** Bot avatars cross process boundaries only as account-scoped file asset references. */
2472
+ function isArkmeBotAvatarRef(value) {
2473
+ return ARKME_BOT_AVATAR_REF_PATTERN.test(value.trim());
2474
+ }
2475
+ //#endregion
2457
2476
  //#region src/secret-value.ts
2458
2477
  var SecretValue = class {
2459
2478
  #value;
@@ -2475,10 +2494,10 @@ function booleanValue$10(value) {
2475
2494
  function listValue$18(value) {
2476
2495
  return Array.isArray(value) ? value : [];
2477
2496
  }
2478
- function encodeOpaqueJson$6(value) {
2497
+ function encodeOpaqueJson$7(value) {
2479
2498
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
2480
2499
  }
2481
- function decodeOpaqueJson$6(value) {
2500
+ function decodeOpaqueJson$7(value) {
2482
2501
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
2483
2502
  }
2484
2503
  var BotService = class {
@@ -2528,6 +2547,7 @@ var BotService = class {
2528
2547
  const avatar = input.avatar?.trim() ?? "";
2529
2548
  if (name === "") throw new ArkmePluginError("bot-name-invalid", "Bot 名称不能为空", false);
2530
2549
  if (provider !== "openclaw" && provider !== "webhook") throw new ArkmePluginError("bot-provider-unsupported", "Bot Provider 不受支持", false, 400);
2550
+ if (avatar !== "" && !isArkmeBotAvatarRef(avatar)) throw new ArkmePluginError("bot-avatar-invalid", "Bot 头像引用无效", false, 400);
2531
2551
  const session = await this.runtime.requireSession();
2532
2552
  let data;
2533
2553
  try {
@@ -2669,7 +2689,7 @@ var BotService = class {
2669
2689
  };
2670
2690
  }
2671
2691
  async sealBotRef(userId, botId) {
2672
- const payload = encodeOpaqueJson$6({
2692
+ const payload = encodeOpaqueJson$7({
2673
2693
  version: 1,
2674
2694
  userId,
2675
2695
  botId
@@ -2685,7 +2705,7 @@ var BotService = class {
2685
2705
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
2686
2706
  let raw;
2687
2707
  try {
2688
- raw = objectValue$1(decodeOpaqueJson$6(payload));
2708
+ raw = objectValue$1(decodeOpaqueJson$7(payload));
2689
2709
  } catch (error) {
2690
2710
  throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false, 400, { cause: error });
2691
2711
  }
@@ -3469,7 +3489,7 @@ var ChatRealtimeService = class {
3469
3489
  lane: "background-read",
3470
3490
  key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
3471
3491
  });
3472
- return [uid, await this.projectionReader.chatTimelineItems(data, session)];
3492
+ return [uid, await this.projectionReader.chatTimelineItems(data, session, uid)];
3473
3493
  }))).forEach((result, index) => {
3474
3494
  const uid = chunk[index]?.[0];
3475
3495
  if (uid === void 0) return;
@@ -3569,6 +3589,18 @@ function booleanValue$8(value) {
3569
3589
  function listValue$15(value) {
3570
3590
  return Array.isArray(value) ? value : [];
3571
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
+ }
3572
3604
  function integerLikeValue(value) {
3573
3605
  if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
3574
3606
  if (typeof value === "string" && value.trim() !== "") {
@@ -3612,10 +3644,10 @@ function timelineAgentSource(relation, record, payload) {
3612
3644
  label: agentSourceLabel(displayName)
3613
3645
  };
3614
3646
  }
3615
- function encodeOpaqueJson$5(value) {
3647
+ function encodeOpaqueJson$6(value) {
3616
3648
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
3617
3649
  }
3618
- function decodeOpaqueJson$5(value) {
3650
+ function decodeOpaqueJson$6(value) {
3619
3651
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
3620
3652
  }
3621
3653
  var ChatService = class {
@@ -3639,6 +3671,65 @@ var ChatService = class {
3639
3671
  this.aiPolish = aiPolish;
3640
3672
  this.realtime = realtime;
3641
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
+ }
3642
3733
  async openPrivateChatFromUser(peerUserId, options = {}) {
3643
3734
  const session = await this.runtime.requireSession();
3644
3735
  if (!Number.isSafeInteger(peerUserId) || peerUserId <= 0) throw new ArkmePluginError("private-chat-peer-invalid", "私聊用户参数无效", false);
@@ -3763,56 +3854,7 @@ var ChatService = class {
3763
3854
  key: `timeline:${source.ownerRef}:${String(Math.max(0, Math.trunc(options.cursor?.beforeSequence ?? 0)))}:${String(limit)}`,
3764
3855
  failureCooldownMs: 2e3
3765
3856
  });
3766
- const items = [];
3767
- const senderUserIdByIndex = /* @__PURE__ */ new Map();
3768
- const messageRefSigningKey = source.kind === "group_chat" ? await this.runtime.stateStore.uniqueCode() : void 0;
3769
- for (const raw of listValue$15(data.items)) {
3770
- const item = objectValue$1(raw);
3771
- const relation = objectValue$1(item.relation);
3772
- const record = objectValue$1(item.record);
3773
- const payload = objectValue$1(record.payload);
3774
- const recordStatus = numberValue$16(record.status);
3775
- if (recordStatus !== 1) continue;
3776
- const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
3777
- if (uid === "") continue;
3778
- const relationUid = stringValue$1(relation.rel_uid).trim();
3779
- const senderUserId = numberValue$16(relation.sender_user_id);
3780
- const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
3781
- const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
3782
- const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
3783
- const rawAgentSource = timelineAgentSource(relation, record, payload);
3784
- const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
3785
- const itemIndex = items.push({
3786
- itemUid: uid,
3787
- ...source.kind !== "group_chat" || relationUid === "" || senderUserId === session.userId ? {} : { messageRef: this.sealMessageRef(session.userId, source.ownerRef, relationUid, messageRefSigningKey) },
3788
- senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
3789
- ...agentSource === void 0 ? {} : { agentSource },
3790
- isMe: senderUserId === session.userId,
3791
- sendAtMillis,
3792
- title: stringValue$1(payload.title),
3793
- textContent: stringValue$1(payload.text_content),
3794
- status: recordStatus,
3795
- sequence: numberValue$16(relation.seq),
3796
- ...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
3797
- ...aiPolish === void 0 ? {} : { aiPolish },
3798
- ...forwardRecords === void 0 ? {} : { forwardRecords },
3799
- templateKind: numberValue$16(payload.template_kind),
3800
- displayKind: numberValue$16(payload.display_kind),
3801
- version: numberValue$16(payload.version ?? record.version),
3802
- updateAtMillis: numberValue$16(payload.update_at ?? record.update_at),
3803
- recordDurationMillis: numberValue$16(payload.record_duration_millis),
3804
- editDurationMillis: numberValue$16(payload.edit_duration_millis),
3805
- contentBlocks: this.media.richContentBlocks(item, session.userId)
3806
- }) - 1;
3807
- if (Number.isSafeInteger(senderUserId) && senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
3808
- }
3809
- try {
3810
- const profiles = await this.profile.publicProfilesByUserIds([...new Set(senderUserIdByIndex.values())], session, options.signal);
3811
- for (const [index, senderUserId] of senderUserIdByIndex) {
3812
- if (!profiles.has(senderUserId) || items[index] === void 0) continue;
3813
- items[index].avatarRef = await this.profile.sealProfileImageRef(session.userId, senderUserId);
3814
- }
3815
- } catch {}
3857
+ const items = await this.projectChatTimelineItems(listValue$15(data.items), source, session, options.signal);
3816
3858
  const beforeSequence = numberValue$16(data.next_before_seq);
3817
3859
  let aiPolishSettings;
3818
3860
  let aiPolishNotices;
@@ -3863,6 +3905,7 @@ var ChatService = class {
3863
3905
  const recordUid = options.recordUid?.trim() || randomUUID();
3864
3906
  if (source.kind === "send_to_self" || source.kind === "default_category") {
3865
3907
  const result = await this.record.createTextForConversation(recordUid, text);
3908
+ if (result.localState !== "failed") this.source.invalidateSourceListCache(session.userId, "send_to_self");
3866
3909
  return {
3867
3910
  sourceRef,
3868
3911
  itemUid: result.recordUid,
@@ -3880,6 +3923,7 @@ var ChatService = class {
3880
3923
  text_content: text,
3881
3924
  send_at: Date.now()
3882
3925
  }, session);
3926
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
3883
3927
  return {
3884
3928
  sourceRef,
3885
3929
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
@@ -3890,16 +3934,68 @@ var ChatService = class {
3890
3934
  const relationUid = options.relationUid?.trim() || randomUUID();
3891
3935
  const agentAuthored = isAgentAuthoredChatSend(options);
3892
3936
  let sent;
3893
- 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 });
3894
- else sent = await this.aiPolish.sendGroupSourceTextWithAiPolish(sourceRef, source.ownerRef, text, recordUid, relationUid, session, {
3895
- agentAuthored,
3896
- ...options.signal === void 0 ? {} : { signal: options.signal }
3897
- });
3898
- 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);
3899
3948
  else sent = await this.sendChatSourceTextRaw(sourceRef, source.ownerRef, text, recordUid, relationUid, session, void 0, void 0, options.signal, { agentAuthored });
3900
3949
  if (agentAuthored && sent.localState === "synced") this.arko.agentSourceDisplayName(session);
3901
3950
  return sent;
3902
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
+ }
3903
3999
  async groupBotMentionSend(sourceRef, source, text, botRefs, recordUid, relationUid, session, signal, options = {}) {
3904
4000
  const uniqueRefs = new Set(botRefs.map((ref) => ref.trim()));
3905
4001
  if (uniqueRefs.has("") || uniqueRefs.size !== botRefs.length) throw new ArkmePluginError("bot-mention-ref-invalid", "Bot mention 引用为空或重复", false);
@@ -3998,7 +4094,7 @@ var ChatService = class {
3998
4094
  const recordUid = options.recordUid?.trim() || randomUUID();
3999
4095
  const relationUid = options.relationUid?.trim() || randomUUID();
4000
4096
  const templateKind = longArticle ? 1 : assets.length === 0 ? 1 : 2;
4001
- const contentPayload = assets.length === 0 ? void 0 : {
4097
+ const mediaContentPayload = assets.length === 0 ? void 0 : {
4002
4098
  payload_kind: 2,
4003
4099
  schema_version: 1,
4004
4100
  text_state: textContent === "" ? 3 : 1,
@@ -4010,6 +4106,16 @@ var ChatService = class {
4010
4106
  file_name: asset.fileName
4011
4107
  }))
4012
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
+ }
4013
4119
  const commonBody = {
4014
4120
  record_uid: recordUid,
4015
4121
  template_kind: templateKind,
@@ -4022,6 +4128,7 @@ var ChatService = class {
4022
4128
  };
4023
4129
  if (source.kind === "send_to_self" || source.kind === "default_category") {
4024
4130
  const result = await this.runtime.authenticatedPost("/api/v1/records/create", commonBody, session);
4131
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
4025
4132
  return {
4026
4133
  sourceRef,
4027
4134
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
@@ -4034,6 +4141,7 @@ var ChatService = class {
4034
4141
  topic_uid: source.ownerRef,
4035
4142
  ...commonBody
4036
4143
  }, session);
4144
+ this.source.invalidateSourceListCache(session.userId, "send_to_self");
4037
4145
  return {
4038
4146
  sourceRef,
4039
4147
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
@@ -4157,7 +4265,7 @@ var ChatService = class {
4157
4265
  async chatSourceFromBundle(bundle, session, cached, timelineItems) {
4158
4266
  return await this.source.chatSourceFromBundle(bundle, session, cached, timelineItems);
4159
4267
  }
4160
- async chatTimelineItems(data, session) {
4268
+ async chatTimelineItems(data, session, chatSessionUid) {
4161
4269
  const items = [];
4162
4270
  for (const raw of listValue$15(data.items)) {
4163
4271
  const item = objectValue$1(raw);
@@ -4174,6 +4282,7 @@ var ChatService = class {
4174
4282
  const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
4175
4283
  items.push({
4176
4284
  itemUid: uid,
4285
+ ...senderUserId > 0 ? { memberRef: await this.sealChatMemberRef(session.userId, chatSessionUid, senderUserId) } : {},
4177
4286
  senderName: stringValue$1(relation.display_name_snapshot).trim() || "Arkme用户",
4178
4287
  ...agentSource === void 0 ? {} : { agentSource },
4179
4288
  ...senderUserId > 0 ? { avatarRef: await this.profile.sealProfileImageRef(session.userId, senderUserId) } : {},
@@ -4241,8 +4350,143 @@ var ChatService = class {
4241
4350
  items: projectedItems
4242
4351
  };
4243
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
+ }
4244
4488
  sealMessageRef(userId, chatSessionUid, relationUid, signingKey) {
4245
- const payload = encodeOpaqueJson$5({
4489
+ const payload = encodeOpaqueJson$6({
4246
4490
  version: 1,
4247
4491
  userId,
4248
4492
  chatSessionUid,
@@ -4259,7 +4503,7 @@ var ChatService = class {
4259
4503
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false);
4260
4504
  let parsed;
4261
4505
  try {
4262
- parsed = objectValue$1(decodeOpaqueJson$5(payload));
4506
+ parsed = objectValue$1(decodeOpaqueJson$6(payload));
4263
4507
  } catch (error) {
4264
4508
  throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false, 400, { cause: error });
4265
4509
  }
@@ -4390,6 +4634,19 @@ var ContactService = class {
4390
4634
  source
4391
4635
  };
4392
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
+ }
4393
4650
  async lookup(normalized, session, signal, contactRef = `arkme-contact-v1.${randomUUID()}`) {
4394
4651
  const data = await this.runtime.authenticatedAuthPost("/api/v1/auth/search-contact-account", {
4395
4652
  identifier_type: normalized.identifierType,
@@ -5196,10 +5453,10 @@ function numberValue$12(value) {
5196
5453
  function listValue$12(value) {
5197
5454
  return Array.isArray(value) ? value : [];
5198
5455
  }
5199
- function encodeOpaqueJson$4(value) {
5456
+ function encodeOpaqueJson$5(value) {
5200
5457
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
5201
5458
  }
5202
- function decodeOpaqueJson$4(value) {
5459
+ function decodeOpaqueJson$5(value) {
5203
5460
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
5204
5461
  }
5205
5462
  function chatMessageDnd$1(value) {
@@ -5494,7 +5751,7 @@ var GroupService = class {
5494
5751
  };
5495
5752
  }
5496
5753
  async sealCandidateRef(payload) {
5497
- const encoded = encodeOpaqueJson$4(payload);
5754
+ const encoded = encodeOpaqueJson$5(payload);
5498
5755
  return `arkme-group-candidate-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
5499
5756
  }
5500
5757
  async openCandidateRef(candidateRef, expectedUserId, expectedGroupChatSessionUid) {
@@ -5506,7 +5763,7 @@ var GroupService = class {
5506
5763
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
5507
5764
  let raw;
5508
5765
  try {
5509
- raw = objectValue$1(decodeOpaqueJson$4(encoded));
5766
+ raw = objectValue$1(decodeOpaqueJson$5(encoded));
5510
5767
  } catch (error) {
5511
5768
  throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false, 400, { cause: error });
5512
5769
  }
@@ -5585,7 +5842,8 @@ var GroupService = class {
5585
5842
  isSelf: userId === session.userId,
5586
5843
  isOwner: role === "owner",
5587
5844
  joinedAtMillis: numberValue$12(item.join_at),
5588
- 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)))
5589
5847
  });
5590
5848
  }
5591
5849
  const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
@@ -6791,10 +7049,10 @@ function chunksOf$1(values, size) {
6791
7049
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
6792
7050
  return chunks;
6793
7051
  }
6794
- function encodeOpaqueJson$3(value) {
7052
+ function encodeOpaqueJson$4(value) {
6795
7053
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
6796
7054
  }
6797
- function decodeOpaqueJson$3(value) {
7055
+ function decodeOpaqueJson$4(value) {
6798
7056
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
6799
7057
  }
6800
7058
  function arkmeIdAvailabilityReason(value) {
@@ -7137,7 +7395,7 @@ var ProfileService = class {
7137
7395
  return profiles;
7138
7396
  }
7139
7397
  async sealProfileImageRef(viewerUserId, targetUserId) {
7140
- const payload = encodeOpaqueJson$3({
7398
+ const payload = encodeOpaqueJson$4({
7141
7399
  version: 1,
7142
7400
  viewerUserId,
7143
7401
  targetUserId
@@ -7153,7 +7411,7 @@ var ProfileService = class {
7153
7411
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("image-ref-invalid", "Arkme头像引用无效", false);
7154
7412
  let parsed;
7155
7413
  try {
7156
- parsed = objectValue$1(decodeOpaqueJson$3(payload));
7414
+ parsed = objectValue$1(decodeOpaqueJson$4(payload));
7157
7415
  } catch (error) {
7158
7416
  throw new ArkmePluginError("image-ref-invalid", "Arkme头像引用无效", false, 400, { cause: error });
7159
7417
  }
@@ -7389,20 +7647,57 @@ var RecordService = class {
7389
7647
  };
7390
7648
  }
7391
7649
  }
7392
- async pendingWrites() {
7393
- const session = await this.runtime.requireSession();
7394
- return await this.runtime.stateStore.listPending(session.userId);
7395
- }
7396
- async retryPending(recordUid) {
7650
+ async createFileAssetsForConversation(recordUid, textContent, assets) {
7397
7651
  const session = await this.runtime.requireSession();
7398
- const pending = (await this.runtime.stateStore.listPending(session.userId)).find((item) => item.recordUid === recordUid);
7399
- if (pending === void 0) throw new ArkmePluginError("outbox-entry-not-found", "待重试内容不存在", false, 404);
7400
- return await this.sendPending(session, pending);
7401
- }
7402
- async sendPending(session, pending) {
7403
- try {
7404
- const data = await this.runtime.authenticatedPost("/api/v1/records/create", {
7405
- record_uid: pending.recordUid,
7652
+ const normalizedUid = recordUid.trim();
7653
+ const normalizedText = textContent.trim();
7654
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(normalizedUid)) throw new ArkmePluginError("record-uid-invalid", "写入标识无效,请重试", false);
7655
+ if (assets.length === 0 || assets.length > 20 || normalizedText.length > this.runtime.config.maxTextLength) throw new ArkmePluginError("record-file-assets-invalid", "附件内容为空、过长或数量超限", false);
7656
+ for (const asset of assets) if (!/^[A-Za-z0-9._:-]{8,256}$/.test(asset.fileAssetUid) || asset.fileName.trim() === "" || asset.fileName.length > 255 || !Number.isSafeInteger(asset.size) || asset.size <= 0 || ![
7657
+ 1,
7658
+ 2,
7659
+ 3,
7660
+ 4
7661
+ ].includes(asset.fileKind)) throw new ArkmePluginError("record-file-asset-invalid", "附件资产参数无效", false);
7662
+ const data = await this.runtime.authenticatedPost("/api/v1/records/create", {
7663
+ record_uid: normalizedUid,
7664
+ template_kind: 2,
7665
+ display_kind: 0,
7666
+ title: "",
7667
+ text_content: normalizedText,
7668
+ content_payload: {
7669
+ payload_kind: 2,
7670
+ schema_version: 1,
7671
+ text_state: normalizedText === "" ? 3 : 1,
7672
+ media_refs: assets.map((asset, index) => ({
7673
+ file_asset_uid: asset.fileAssetUid,
7674
+ content_file_role: 1,
7675
+ render_role: 1,
7676
+ sort_order: index,
7677
+ file_name: asset.fileName
7678
+ }))
7679
+ },
7680
+ send_at: Date.now()
7681
+ }, session);
7682
+ return {
7683
+ recordUid: stringValue$1(data.record_uid).trim() || normalizedUid,
7684
+ status: numberValue$7(data.status)
7685
+ };
7686
+ }
7687
+ async pendingWrites() {
7688
+ const session = await this.runtime.requireSession();
7689
+ return await this.runtime.stateStore.listPending(session.userId);
7690
+ }
7691
+ async retryPending(recordUid) {
7692
+ const session = await this.runtime.requireSession();
7693
+ const pending = (await this.runtime.stateStore.listPending(session.userId)).find((item) => item.recordUid === recordUid);
7694
+ if (pending === void 0) throw new ArkmePluginError("outbox-entry-not-found", "待重试内容不存在", false, 404);
7695
+ return await this.sendPending(session, pending);
7696
+ }
7697
+ async sendPending(session, pending) {
7698
+ try {
7699
+ const data = await this.runtime.authenticatedPost("/api/v1/records/create", {
7700
+ record_uid: pending.recordUid,
7406
7701
  template_kind: 1,
7407
7702
  title: "",
7408
7703
  text_content: pending.textContent,
@@ -7532,7 +7827,7 @@ function displayTimestamp(value) {
7532
7827
  const parsed = Date.parse(stringValue(value));
7533
7828
  return Number.isFinite(parsed) ? parsed : 0;
7534
7829
  }
7535
- function projectRecordingTranscripts(response, speakerResponse, source = "system") {
7830
+ function projectRecordingTranscripts(response, speakerResponse) {
7536
7831
  const data = objectValue(response);
7537
7832
  const sessions = /* @__PURE__ */ new Map();
7538
7833
  const sessionSpeakerColorIndexes = /* @__PURE__ */ new Map();
@@ -7576,32 +7871,7 @@ function projectRecordingTranscripts(response, speakerResponse, source = "system
7576
7871
  const sessionSpeakers = listValue$6(session.spk_ls ?? session.speakers).map(objectValue);
7577
7872
  const childOffset = numberValue$6(child.start_at);
7578
7873
  const childStart = childOffset >= 1e11 ? childOffset : numberValue$6(session.start_at) + childOffset;
7579
- const rows = listValue$6(source === "doubao" ? child.doubao_asr : child.asr);
7580
- const doubaoStatus = numberValue$6(child.doubao_asr_status);
7581
- if (source === "doubao" && (doubaoStatus === 1 || doubaoStatus === 2 || doubaoStatus === 4 || doubaoStatus === 5)) {
7582
- const transcriptStatus = doubaoStatus === 1 || doubaoStatus === 2 ? "processing" : doubaoStatus === 4 ? "silent" : "failed";
7583
- const text = transcriptStatus === "processing" ? "豆包转写中..." : transcriptStatus === "silent" ? "豆包未识别到人声" : "豆包转写失败";
7584
- const label = transcriptStatus === "processing" ? "豆包转写中" : transcriptStatus === "silent" ? "豆包静音" : "豆包失败";
7585
- projected.push({
7586
- itemId: `${childId || sessionId}:doubao:status`,
7587
- sessionId,
7588
- childId,
7589
- asrItemIndex: 0,
7590
- transcriptSource: "doubao",
7591
- startAtMillis: childStart,
7592
- endAtMillis: childStart + Math.max(0, numberValue$6(child.duration)),
7593
- speakerNumber: -1,
7594
- speakerColorIndex: -1,
7595
- speakerLabel: label,
7596
- isSelf: false,
7597
- isBackground: false,
7598
- text,
7599
- transcriptStatus,
7600
- sourceIndex
7601
- });
7602
- sourceIndex += 1;
7603
- continue;
7604
- }
7874
+ const rows = listValue$6(child.asr);
7605
7875
  for (let index = 0; index < rows.length; index += 1) {
7606
7876
  const row = objectValue(rows[index]);
7607
7877
  const isBackground = numberValue$6(row.b ?? row.background) === 1 || row.background === true;
@@ -7615,15 +7885,15 @@ function projectRecordingTranscripts(response, speakerResponse, source = "system
7615
7885
  const persistentSpeakerNumber = optionalNumberValue(sessionSpeaker.speaker_display_number ?? sessionSpeaker.speakerDisplayNumber);
7616
7886
  const speakerNumber = persistentSpeakerNumber !== void 0 && persistentSpeakerNumber > 0 ? persistentSpeakerNumber : rawSpeakerNumber;
7617
7887
  const speakerColorIndex = sessionSpeakerColorIndexes.get(`${sessionId}:${rawSpeakerNumber}`) ?? Math.max(0, rawSpeakerNumber);
7618
- const speakerLabel = source === "doubao" ? rawSpeakerNumber >= 0 ? `豆包说话人 ${rawSpeakerNumber + 1}` : "豆包说话人" : speakerNumber >= 0 ? `说话人 ${speakerNumber}` : "未知说话人";
7888
+ const speakerLabel = speakerNumber >= 0 ? `说话人 ${speakerNumber}` : "未知说话人";
7619
7889
  const startOffset = numberValue$6(row.s ?? row.start_at);
7620
7890
  const endOffset = Math.max(startOffset, numberValue$6(row.e ?? row.end_at));
7621
7891
  projected.push({
7622
- itemId: source === "system" ? `${childId || sessionId}:${index}` : `${childId || sessionId}:doubao:${index}`,
7892
+ itemId: `${childId || sessionId}:${index}`,
7623
7893
  sessionId,
7624
7894
  childId,
7625
7895
  asrItemIndex: index,
7626
- transcriptSource: source,
7896
+ transcriptSource: "system",
7627
7897
  startAtMillis: childStart + startOffset,
7628
7898
  endAtMillis: childStart + endOffset,
7629
7899
  speakerNumber,
@@ -7632,7 +7902,6 @@ function projectRecordingTranscripts(response, speakerResponse, source = "system
7632
7902
  isSelf,
7633
7903
  isBackground,
7634
7904
  text,
7635
- ...source === "doubao" ? { transcriptStatus: "ready" } : {},
7636
7905
  sourceIndex
7637
7906
  });
7638
7907
  sourceIndex += 1;
@@ -7832,10 +8101,10 @@ function numberValue$5(value) {
7832
8101
  function listValue$5(value) {
7833
8102
  return Array.isArray(value) ? value : [];
7834
8103
  }
7835
- function encodeOpaqueJson$2(value) {
8104
+ function encodeOpaqueJson$3(value) {
7836
8105
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
7837
8106
  }
7838
- function decodeOpaqueJson$2(value) {
8107
+ function decodeOpaqueJson$3(value) {
7839
8108
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
7840
8109
  }
7841
8110
  function safeFailureMessage$2(error) {
@@ -7879,21 +8148,7 @@ var RecordingService = class {
7879
8148
  days
7880
8149
  };
7881
8150
  }
7882
- async recordingTranscript(dateStamp, signal, source = "system") {
7883
- const sections = await this.recordingTranscriptSections(dateStamp, signal);
7884
- return source === "doubao" ? sections.doubao : sections.system;
7885
- }
7886
- async startRecordingDoubaoBackfill(dateStamp, signal) {
7887
- const date = this.recordingDayStart(dateStamp).getTime();
7888
- const session = await this.runtime.requireSession();
7889
- const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/doubao-asr/backfill-day", { start_at: date }, session, signal);
7890
- return {
7891
- queuedChildCount: Math.max(0, Math.trunc(numberValue$5(data.queued_child_count))),
7892
- inFlightChildCount: Math.max(0, Math.trunc(numberValue$5(data.in_flight_child_count))),
7893
- missingAudioChildCount: Math.max(0, Math.trunc(numberValue$5(data.missing_audio_child_count)))
7894
- };
7895
- }
7896
- async recordingTranscriptSections(dateStamp, signal) {
8151
+ async recordingTranscript(dateStamp, signal) {
7897
8152
  const dayStart = this.recordingDayStart(dateStamp);
7898
8153
  const date = dayStart.getTime();
7899
8154
  const session = await this.runtime.requireSession();
@@ -7905,29 +8160,14 @@ var RecordingService = class {
7905
8160
  let totalDurationMillis = 0;
7906
8161
  for (const rawSession of listValue$5(transcriptResult.value.session_ls)) totalDurationMillis += Math.max(0, numberValue$5(objectValue$1(rawSession).duration));
7907
8162
  const speakerData = speakerResult.status === "fulfilled" ? listValue$5(speakerResult.value.spk_ls) : [];
7908
- const identityCoverage = speakerResult.status === "fulfilled" ? "complete" : "partial";
7909
- const systemItems = projectRecordingTranscripts(transcriptResult.value, speakerData, "system");
7910
- const doubaoItems = projectRecordingTranscripts(transcriptResult.value, speakerData, "doubao");
7911
- const system = {
7912
- state: systemItems.length > 0 ? "ready" : "empty",
7913
- items: systemItems,
7914
- message: systemItems.length > 0 ? "" : "当天无录音",
7915
- identityCoverage,
8163
+ const items = projectRecordingTranscripts(transcriptResult.value, speakerData);
8164
+ return {
8165
+ state: items.length > 0 ? "ready" : "empty",
8166
+ items,
8167
+ message: items.length > 0 ? "" : "当天无录音",
8168
+ identityCoverage: speakerResult.status === "fulfilled" ? "complete" : "partial",
7916
8169
  totalDurationMillis
7917
8170
  };
7918
- const hasProcessing = doubaoItems.some((item) => item.transcriptStatus === "processing");
7919
- const hasReady = doubaoItems.some((item) => item.transcriptStatus === "ready" || item.transcriptStatus === "silent" || item.transcriptStatus === void 0);
7920
- const hasFailed = doubaoItems.some((item) => item.transcriptStatus === "failed");
7921
- return {
7922
- system,
7923
- doubao: {
7924
- state: hasProcessing ? "processing" : hasReady ? "ready" : hasFailed ? "failed" : "empty",
7925
- items: doubaoItems,
7926
- message: hasProcessing ? "豆包转写中" : hasReady ? "" : hasFailed ? "豆包转写失败" : "豆包转写尚未生成",
7927
- identityCoverage,
7928
- totalDurationMillis
7929
- }
7930
- };
7931
8171
  }
7932
8172
  async recordingProjection(dateStamp, kind, signal) {
7933
8173
  const dayStart = this.recordingDayStart(dateStamp);
@@ -7944,7 +8184,7 @@ var RecordingService = class {
7944
8184
  }
7945
8185
  async sealRecordingCursor(payload) {
7946
8186
  const session = await this.runtime.requireSession();
7947
- const encoded = encodeOpaqueJson$2(payload);
8187
+ const encoded = encodeOpaqueJson$3(payload);
7948
8188
  return `arkme-recording-cursor-v1.${encoded}.${createHmac("sha256", await this.recordingCursorKey(session.userId)).update(encoded).digest("base64url")}`;
7949
8189
  }
7950
8190
  async openRecordingCursor(cursor) {
@@ -7956,7 +8196,7 @@ var RecordingService = class {
7956
8196
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false);
7957
8197
  let raw;
7958
8198
  try {
7959
- raw = objectValue$1(decodeOpaqueJson$2(encoded));
8199
+ raw = objectValue$1(decodeOpaqueJson$3(encoded));
7960
8200
  } catch (error) {
7961
8201
  throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false, 400, { cause: error });
7962
8202
  }
@@ -7965,7 +8205,6 @@ var RecordingService = class {
7965
8205
  version: 1,
7966
8206
  dateStamp: numberValue$5(raw.dateStamp),
7967
8207
  content: content === "summary" || content === "timeline" ? content : "transcript",
7968
- ...content === "transcript" && (raw.transcriptSource === "system" || raw.transcriptSource === "doubao") ? { transcriptSource: raw.transcriptSource } : {},
7969
8208
  itemOffset: numberValue$5(raw.itemOffset),
7970
8209
  textOffset: numberValue$5(raw.textOffset),
7971
8210
  fingerprint: stringValue$1(raw.fingerprint),
@@ -7975,31 +8214,25 @@ var RecordingService = class {
7975
8214
  "transcript",
7976
8215
  "summary",
7977
8216
  "timeline"
7978
- ].includes(String(content)) || content === "transcript" && raw.transcriptSource !== void 0 && raw.transcriptSource !== "system" && raw.transcriptSource !== "doubao" || !Number.isSafeInteger(payload.dateStamp) || payload.dateStamp <= 0 || !Number.isSafeInteger(payload.itemOffset) || payload.itemOffset < 0 || !Number.isSafeInteger(payload.textOffset) || payload.textOffset < 0 || payload.fingerprint === "") throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false);
8217
+ ].includes(String(content)) || !Number.isSafeInteger(payload.dateStamp) || payload.dateStamp <= 0 || !Number.isSafeInteger(payload.itemOffset) || payload.itemOffset < 0 || !Number.isSafeInteger(payload.textOffset) || payload.textOffset < 0 || payload.fingerprint === "") throw new ArkmePluginError("recording-cursor-invalid", "录音分页游标无效", false);
7979
8218
  return payload;
7980
8219
  }
7981
8220
  async recordingDay(dateStamp) {
7982
8221
  const date = this.recordingDayStart(dateStamp).getTime();
7983
8222
  const [transcriptResult, summaryResult, timelineResult] = await Promise.allSettled([
7984
- this.recordingTranscriptSections(date),
8223
+ this.recordingTranscript(date),
7985
8224
  this.recordingProjection(date, "summary"),
7986
8225
  this.recordingProjection(date, "timeline")
7987
8226
  ]);
7988
- const transcript = transcriptResult.status === "fulfilled" ? transcriptResult.value.system : {
7989
- state: "error",
7990
- items: [],
7991
- message: safeFailureMessage$2(transcriptResult.reason)
7992
- };
7993
- const doubaoTranscript = transcriptResult.status === "fulfilled" ? transcriptResult.value.doubao : {
8227
+ const transcript = transcriptResult.status === "fulfilled" ? transcriptResult.value : {
7994
8228
  state: "error",
7995
8229
  items: [],
7996
8230
  message: safeFailureMessage$2(transcriptResult.reason)
7997
8231
  };
7998
8232
  return {
7999
8233
  dateStamp: date,
8000
- totalDurationMillis: transcriptResult.status === "fulfilled" ? transcriptResult.value.system.totalDurationMillis : 0,
8234
+ totalDurationMillis: transcriptResult.status === "fulfilled" ? transcriptResult.value.totalDurationMillis : 0,
8001
8235
  transcript,
8002
- doubaoTranscript,
8003
8236
  summary: summaryResult.status === "fulfilled" ? summaryResult.value : {
8004
8237
  state: "error",
8005
8238
  items: [],
@@ -8515,10 +8748,10 @@ function booleanValue$1(value) {
8515
8748
  function listValue$2(value) {
8516
8749
  return Array.isArray(value) ? value : [];
8517
8750
  }
8518
- function encodeOpaqueJson$1(value) {
8751
+ function encodeOpaqueJson$2(value) {
8519
8752
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
8520
8753
  }
8521
- function decodeOpaqueJson$1(value) {
8754
+ function decodeOpaqueJson$2(value) {
8522
8755
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
8523
8756
  }
8524
8757
  function isSourceKind(value) {
@@ -8700,13 +8933,6 @@ var SourceService = class {
8700
8933
  ...defaultLatestPreview === "" ? {} : { latestPreview: defaultLatestPreview },
8701
8934
  ...defaultRecordCount === void 0 ? {} : { recordCount: defaultRecordCount }
8702
8935
  };
8703
- const aggregateSource = {
8704
- sourceRef: await this.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
8705
- kind: "send_to_self",
8706
- displayName: "发给自己",
8707
- activeAtMillis: 0,
8708
- unreadCount: 0
8709
- };
8710
8936
  const topicDescriptors = [];
8711
8937
  const seenTopicUids = /* @__PURE__ */ new Set();
8712
8938
  const parentTopicUidByChild = /* @__PURE__ */ new Map();
@@ -8734,30 +8960,44 @@ var SourceService = class {
8734
8960
  ...parentTopicUid === "" || parentTopicUid === topicUid ? {} : { parentTopicUid },
8735
8961
  title,
8736
8962
  latestPreview: textPreview(latest),
8963
+ latestMessageAtMillis: numberValue$2(latest.send_at ?? summary.latest_send_at),
8737
8964
  activeAtMillis: numberValue$2(latest.send_at ?? summary.latest_send_at ?? core.update_at),
8738
8965
  recordCount: numberValue$2(summary.record_count)
8739
8966
  });
8740
8967
  }
8741
8968
  const sourceRefByTopicUid = /* @__PURE__ */ new Map();
8742
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() || "非文本内容";
8743
8988
  return {
8744
8989
  directory,
8745
8990
  items: [
8746
- 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
+ },
8747
8999
  defaultCategory,
8748
- ...topicDescriptors.map((topic) => {
8749
- const parentSourceRef = topic.parentTopicUid === void 0 ? void 0 : sourceRefByTopicUid.get(topic.parentTopicUid);
8750
- return {
8751
- sourceRef: sourceRefByTopicUid.get(topic.topicUid),
8752
- ...parentSourceRef === void 0 ? {} : { parentSourceRef },
8753
- kind: "topic",
8754
- displayName: topic.title,
8755
- ...topic.latestPreview === "" ? {} : { latestPreview: topic.latestPreview },
8756
- activeAtMillis: topic.activeAtMillis,
8757
- unreadCount: 0,
8758
- recordCount: topic.recordCount
8759
- };
8760
- })
9000
+ ...topics
8761
9001
  ],
8762
9002
  hasMore: false
8763
9003
  };
@@ -8923,7 +9163,7 @@ var SourceService = class {
8923
9163
  return `arkme-chat-source-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`chat-source-key-v1:${String(userId)}:${chatSessionUid.trim()}`).digest("base64url")}`;
8924
9164
  }
8925
9165
  async sealSourceRef(userId, kind, ownerRef, displayName) {
8926
- const payload = encodeOpaqueJson$1({
9166
+ const payload = encodeOpaqueJson$2({
8927
9167
  version: 1,
8928
9168
  userId,
8929
9169
  kind,
@@ -8941,7 +9181,7 @@ var SourceService = class {
8941
9181
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("source-ref-invalid", "Arkme 数据源引用无效", false);
8942
9182
  let parsed;
8943
9183
  try {
8944
- parsed = objectValue$1(decodeOpaqueJson$1(payload));
9184
+ parsed = objectValue$1(decodeOpaqueJson$2(payload));
8945
9185
  } catch (error) {
8946
9186
  throw new ArkmePluginError("source-ref-invalid", "Arkme 数据源引用无效", false, 400, { cause: error });
8947
9187
  }
@@ -8997,13 +9237,13 @@ var SourceService = class {
8997
9237
  };
8998
9238
  }
8999
9239
  encodeCursor(value) {
9000
- return `arkme-cursor-v1.${encodeOpaqueJson$1(value)}`;
9240
+ return `arkme-cursor-v1.${encodeOpaqueJson$2(value)}`;
9001
9241
  }
9002
9242
  decodeCursor(cursor) {
9003
9243
  const [prefix, payload, ...extra] = cursor.trim().split(".");
9004
9244
  if (prefix !== "arkme-cursor-v1" || payload === void 0 || extra.length > 0) throw new ArkmePluginError("source-cursor-invalid", "Arkme 数据源分页游标无效", false);
9005
9245
  try {
9006
- const decoded = objectValue$1(decodeOpaqueJson$1(payload));
9246
+ const decoded = objectValue$1(decodeOpaqueJson$2(payload));
9007
9247
  if (Object.keys(decoded).length === 0) throw new Error("empty cursor");
9008
9248
  return decoded;
9009
9249
  } catch (error) {
@@ -9054,10 +9294,10 @@ function optionalString(value) {
9054
9294
  const text = stringValue$1(value).trim();
9055
9295
  return text === "" ? void 0 : text;
9056
9296
  }
9057
- function encodeOpaqueJson(value) {
9297
+ function encodeOpaqueJson$1(value) {
9058
9298
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
9059
9299
  }
9060
- function decodeOpaqueJson(value) {
9300
+ function decodeOpaqueJson$1(value) {
9061
9301
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
9062
9302
  }
9063
9303
  var WechatService = class {
@@ -9365,7 +9605,7 @@ var WechatService = class {
9365
9605
  };
9366
9606
  }
9367
9607
  async sealWechatConversationRef(userId, importSessionKey) {
9368
- const payload = encodeOpaqueJson({
9608
+ const payload = encodeOpaqueJson$1({
9369
9609
  version: 1,
9370
9610
  userId,
9371
9611
  importSessionKey
@@ -9381,7 +9621,7 @@ var WechatService = class {
9381
9621
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("wechat-conversation-ref-invalid", "微信会话引用无效,请先重新查询微信会话列表", false);
9382
9622
  let parsed;
9383
9623
  try {
9384
- parsed = objectValue$1(decodeOpaqueJson(payload));
9624
+ parsed = objectValue$1(decodeOpaqueJson$1(payload));
9385
9625
  } catch (error) {
9386
9626
  throw new ArkmePluginError("wechat-conversation-ref-invalid", "微信会话引用无效,请先重新查询微信会话列表", false, 400, { cause: error });
9387
9627
  }
@@ -9394,7 +9634,7 @@ var WechatService = class {
9394
9634
  return result;
9395
9635
  }
9396
9636
  async sealWechatCursor(userId, scope, offset) {
9397
- const payload = encodeOpaqueJson({
9637
+ const payload = encodeOpaqueJson$1({
9398
9638
  version: 1,
9399
9639
  userId,
9400
9640
  scope,
@@ -9412,7 +9652,7 @@ var WechatService = class {
9412
9652
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("wechat-cursor-invalid", "微信数据分页游标无效,请从第一页重新查询", false);
9413
9653
  let parsed;
9414
9654
  try {
9415
- parsed = objectValue$1(decodeOpaqueJson(payload));
9655
+ parsed = objectValue$1(decodeOpaqueJson$1(payload));
9416
9656
  } catch (error) {
9417
9657
  throw new ArkmePluginError("wechat-cursor-invalid", "微信数据分页游标无效,请从第一页重新查询", false, 400, { cause: error });
9418
9658
  }
@@ -9427,19 +9667,584 @@ var WechatService = class {
9427
9667
  }
9428
9668
  };
9429
9669
  //#endregion
9670
+ //#region src/types.ts
9671
+ const ARKME_PROVIDER_CONTRACT_VERSION = 1;
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;
9676
+ const ARKME_WORLD_PUBLISH_MAX_IMAGES = 9;
9677
+ const ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
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
9430
10189
  //#region src/services/world-service.ts
9431
10190
  const ARKME_WORLD_IMAGE_REF_TTL_MILLIS = 900 * 1e3;
9432
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;
9433
10194
  const ARKME_WORLD_RECORD_REF_TTL_MILLIS = 900 * 1e3;
9434
10195
  const MAX_ARKME_WORLD_RECORD_REFS = 4096;
9435
10196
  const ARKME_VOICEPRINT_PLAY_SCOPE = 2;
9436
10197
  const ARKME_VOICEPRINT_INVITE_TOKEN_MAX_LENGTH = 2048;
10198
+ const ARKME_VOICEPRINT_SOCIAL_CACHE_MILLIS = 300 * 1e3;
10199
+ const ARKME_VOICEPRINT_SOCIAL_STALE_MILLIS = 1440 * 60 * 1e3;
10200
+ const ARKME_VOICEPRINT_SOCIAL_SOURCE_TIMEOUT_MILLIS = 4e3;
10201
+ const WORLD_VOICEPRINT_SOCIAL_RELATIONS = {
10202
+ reciprocal_expectation: {
10203
+ type: "reciprocal_expectation",
10204
+ displayLine: "TA也曾期待过你的声音",
10205
+ reasonCode: "relationship_reciprocal_expectation",
10206
+ reasonLabel: "因为TA也曾期待过我的声音"
10207
+ },
10208
+ call: {
10209
+ type: "call",
10210
+ displayLine: "你们曾经通过话",
10211
+ reasonCode: "relationship_call",
10212
+ reasonLabel: "因为我们曾经通过话"
10213
+ },
10214
+ world_interaction: {
10215
+ type: "world_interaction",
10216
+ displayLine: "你们曾在世界回应过彼此",
10217
+ reasonCode: "relationship_world",
10218
+ reasonLabel: "因为我们在世界里回应过彼此"
10219
+ },
10220
+ group_interaction: {
10221
+ type: "group_interaction",
10222
+ displayLine: "你们曾在同一个群里互动过",
10223
+ reasonCode: "relationship_group",
10224
+ reasonLabel: "因为我们在群里有过互动"
10225
+ },
10226
+ private_chat: {
10227
+ type: "private_chat",
10228
+ displayLine: "你们曾经聊过",
10229
+ reasonCode: "relationship_chat",
10230
+ reasonLabel: "因为我们以前聊过"
10231
+ }
10232
+ };
9437
10233
  function numberValue(value) {
9438
10234
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
9439
10235
  }
10236
+ function integerValue(value) {
10237
+ if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
10238
+ if (typeof value !== "string" || !/^-?\d+$/.test(value.trim())) return 0;
10239
+ const parsed = Number(value.trim());
10240
+ return Number.isSafeInteger(parsed) ? parsed : 0;
10241
+ }
9440
10242
  function listValue(value) {
9441
10243
  return Array.isArray(value) ? value : [];
9442
10244
  }
10245
+ function isTopLevelWorldRecord(raw) {
10246
+ return stringValue$1(objectValue$1(raw).parent_record_uid).trim() === "";
10247
+ }
9443
10248
  function safeFailureMessage(error) {
9444
10249
  if (error instanceof ArkmePluginError) return error.message;
9445
10250
  if (error instanceof Error && error.message.trim() !== "") return error.message;
@@ -9472,6 +10277,26 @@ function worldVisibility(checkStatus) {
9472
10277
  if (checkStatus === 0 || checkStatus === 2 || checkStatus === 3) return "visible";
9473
10278
  return "unknown";
9474
10279
  }
10280
+ function worldPublicationResult(checkStatus) {
10281
+ const visibility = worldVisibility(checkStatus);
10282
+ if (visibility === "rejected") return {
10283
+ recordSaved: true,
10284
+ recordState: "synced",
10285
+ worldPublished: false,
10286
+ visibility,
10287
+ checkStatus,
10288
+ retryable: false,
10289
+ error: "内容未通过审核,请调整后重试"
10290
+ };
10291
+ return {
10292
+ recordSaved: true,
10293
+ recordState: "synced",
10294
+ worldPublished: true,
10295
+ visibility,
10296
+ checkStatus,
10297
+ retryable: false
10298
+ };
10299
+ }
9475
10300
  function worldTags(text) {
9476
10301
  return [...text.matchAll(/#(\S+)/gu)].map((match) => match[1] ?? "").filter((tag) => tag !== "");
9477
10302
  }
@@ -9479,6 +10304,10 @@ function stableWorldInteractionRecordUid(userId, targetRecordUid, clientMutation
9479
10304
  const hex = createHash("sha256").update(`dsh-arkme:world-interaction:${String(userId)}:${targetRecordUid}:${clientMutationId}`).digest("hex");
9480
10305
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
9481
10306
  }
10307
+ function stableWorldPublishRecordUid(userId, clientMutationId) {
10308
+ const hex = createHash("sha256").update(`dsh-arkme:world-publish:${String(userId)}:${clientMutationId}`).digest("hex");
10309
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
10310
+ }
9482
10311
  function allowedSignedImageHost(environment, hostname) {
9483
10312
  return (environment === "prod" ? ["jotmo-userfiles.oss-cn-hangzhou.aliyuncs.com", "userfiles.jotmo.cc"] : ["jotmo-userfiles-test.oss-cn-hangzhou.aliyuncs.com", "jotmo-userfiles.senguo.me"]).includes(hostname.toLowerCase());
9484
10313
  }
@@ -9511,13 +10340,24 @@ function worldAvatarResolutionKey(ownerUserId, avatarRef) {
9511
10340
  if (!Number.isSafeInteger(ownerUserId) || ownerUserId <= 0 || !normalized.startsWith("file_asset://")) return "";
9512
10341
  return `${String(ownerUserId)}|${normalized}`;
9513
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
+ }
9514
10351
  var WorldService = class {
9515
10352
  runtime;
9516
10353
  profile;
9517
10354
  media;
9518
10355
  record;
9519
10356
  worldImageRefs = /* @__PURE__ */ new Map();
10357
+ worldAvatarResolutionCache = /* @__PURE__ */ new Map();
9520
10358
  worldRecordRefs = /* @__PURE__ */ new Map();
10359
+ voiceprintSocialCache = /* @__PURE__ */ new Map();
10360
+ voiceprintSocialInFlight = /* @__PURE__ */ new Map();
9521
10361
  constructor(runtime, profile, media, record) {
9522
10362
  this.runtime = runtime;
9523
10363
  this.profile = profile;
@@ -9526,7 +10366,10 @@ var WorldService = class {
9526
10366
  }
9527
10367
  dispose() {
9528
10368
  this.worldImageRefs.clear();
10369
+ this.worldAvatarResolutionCache.clear();
9529
10370
  this.worldRecordRefs.clear();
10371
+ this.voiceprintSocialCache.clear();
10372
+ this.voiceprintSocialInFlight.clear();
9530
10373
  }
9531
10374
  async listWorldRecords(options = {}) {
9532
10375
  const limit = Math.min(20, Math.max(1, Math.trunc(options.limit ?? 10)));
@@ -9571,22 +10414,61 @@ var WorldService = class {
9571
10414
  async listMyWorldFeed(options = {}) {
9572
10415
  const session = await this.runtime.requireSession();
9573
10416
  const limit = Math.min(20, Math.max(1, Math.trunc(options.limit ?? 20)));
9574
- const offset = Math.max(0, Math.trunc(options.offset ?? 0));
9575
- const data = await this.runtime.authenticatedWorldPost("/api/v1/public-record/my-list", {
9576
- limit,
9577
- offset
9578
- }, session, options.signal);
9579
- const rawItems = listValue(data.list);
9580
- const resolvedAvatars = await this.resolveWorldAvatarUrls(rawItems, session, options.signal);
9581
- const items = (await Promise.all(rawItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
9582
- const total = Math.max(0, Math.trunc(numberValue(data.total)));
9583
- const nextOffset = offset + rawItems.length;
9584
- 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);
9585
10436
  return {
9586
10437
  items,
9587
10438
  total,
9588
10439
  hasMore,
9589
- ...hasMore ? { nextOffset } : {}
10440
+ ...hasMore ? { nextOffset: offset } : {}
10441
+ };
10442
+ }
10443
+ async listUserWorldFeed(userId, options = {}) {
10444
+ if (!Number.isSafeInteger(userId) || userId <= 0) throw new ArkmePluginError("world-user-id-invalid", "世界用户 ID 无效,请刷新后重试", false, 400);
10445
+ const session = await this.runtime.requireSession();
10446
+ const limit = Math.min(20, Math.max(1, Math.trunc(options.limit ?? 20)));
10447
+ let offset = Math.max(0, Math.trunc(options.offset ?? 0));
10448
+ let total = 0;
10449
+ let items = [];
10450
+ let hasMore = false;
10451
+ let attempts = 0;
10452
+ do {
10453
+ const data = await this.runtime.post(this.runtime.config.worldBaseUrl, "/api/public/v1/public-record/user-list", {
10454
+ user_id: userId,
10455
+ limit,
10456
+ offset
10457
+ }, void 0, [200], options.signal);
10458
+ const rawItems = listValue(data.list);
10459
+ total = Math.max(0, Math.trunc(numberValue(data.total)));
10460
+ const rootItems = rawItems.filter(isTopLevelWorldRecord);
10461
+ const resolvedAvatars = await this.resolveWorldAvatarUrls(rootItems, session, options.signal);
10462
+ items = (await Promise.all(rootItems.map((raw) => this.worldFeedItem(raw, session.userId, resolvedAvatars)))).filter((item) => item !== void 0);
10463
+ offset += rawItems.length;
10464
+ hasMore = rawItems.length > 0 && offset < total;
10465
+ attempts += 1;
10466
+ } while (items.length === 0 && hasMore && attempts < 4);
10467
+ return {
10468
+ items,
10469
+ total,
10470
+ hasMore,
10471
+ ...hasMore ? { nextOffset: offset } : {}
9590
10472
  };
9591
10473
  }
9592
10474
  async worldVoiceprintPlaybackAvailability(recordRefs, signal) {
@@ -9625,7 +10507,8 @@ var WorldService = class {
9625
10507
  const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/generate-playback", {
9626
10508
  source_scene: 4,
9627
10509
  source_id: entry.recordUid,
9628
- source_chunk_index: chunkIndex
10510
+ source_chunk_index: chunkIndex,
10511
+ source_chunk_max_runes: 120
9629
10512
  }, session, input.signal, {
9630
10513
  lane: "write",
9631
10514
  bypassCache: true
@@ -9650,6 +10533,37 @@ var WorldService = class {
9650
10533
  chunkEndRune
9651
10534
  };
9652
10535
  }
10536
+ async worldVoiceprintSocialContext(recordRef, options = {}) {
10537
+ const session = await this.runtime.requireSession();
10538
+ const entry = this.openWorldRecordRef(recordRef, session.userId);
10539
+ const authorUserId = entry.ownerUserId ?? 0;
10540
+ if (!Number.isSafeInteger(authorUserId) || authorUserId <= 0 || authorUserId === session.userId) return { relations: [] };
10541
+ const cacheKey = `${String(session.userId)}:${String(authorUserId)}`;
10542
+ const cached = this.voiceprintSocialCache.get(cacheKey);
10543
+ const age = cached === void 0 ? Number.POSITIVE_INFINITY : Date.now() - cached.savedAtMillis;
10544
+ if (options.forceRefresh !== true && cached !== void 0 && cached.context.relations.length > 0 && age < ARKME_VOICEPRINT_SOCIAL_CACHE_MILLIS) return cached.context;
10545
+ if (options.forceRefresh !== true && cached !== void 0 && cached.context.relations.length > 0 && age < ARKME_VOICEPRINT_SOCIAL_STALE_MILLIS) {
10546
+ this.refreshWorldVoiceprintSocialContext({
10547
+ cacheKey,
10548
+ flightKey: `${cacheKey}:${entry.recordUid}`,
10549
+ viewerUserId: session.userId,
10550
+ authorUserId,
10551
+ recordUid: entry.recordUid,
10552
+ fallback: cached,
10553
+ ...options.signal === void 0 ? {} : { signal: options.signal }
10554
+ });
10555
+ return cached.context;
10556
+ }
10557
+ return await this.refreshWorldVoiceprintSocialContext({
10558
+ cacheKey,
10559
+ flightKey: `${cacheKey}:${entry.recordUid}`,
10560
+ viewerUserId: session.userId,
10561
+ authorUserId,
10562
+ recordUid: entry.recordUid,
10563
+ ...cached === void 0 ? {} : { fallback: cached },
10564
+ ...options.signal === void 0 ? {} : { signal: options.signal }
10565
+ });
10566
+ }
9653
10567
  async createWorldVoiceprintInviteIntent(recordRef, signal) {
9654
10568
  const session = await this.runtime.requireSession();
9655
10569
  const entry = this.openWorldRecordRef(recordRef, session.userId);
@@ -9672,6 +10586,183 @@ var WorldService = class {
9672
10586
  ...entry.textPreview === void 0 ? {} : { textPreview: entry.textPreview }
9673
10587
  };
9674
10588
  }
10589
+ refreshWorldVoiceprintSocialContext(input) {
10590
+ const existing = this.voiceprintSocialInFlight.get(input.flightKey);
10591
+ if (existing !== void 0) return existing;
10592
+ const refresh = this.loadWorldVoiceprintSocialContext(input).then((result) => {
10593
+ const context = this.mergeWorldVoiceprintSocialContexts(input.fallback?.context, result.context);
10594
+ if (context.relations.length > 0 || result.allSourcesSucceeded) this.voiceprintSocialCache.set(input.cacheKey, {
10595
+ context,
10596
+ savedAtMillis: Date.now()
10597
+ });
10598
+ return context;
10599
+ }).catch(() => input.fallback?.context.relations.length ? input.fallback.context : { relations: [] }).finally(() => {
10600
+ this.voiceprintSocialInFlight.delete(input.flightKey);
10601
+ });
10602
+ this.voiceprintSocialInFlight.set(input.flightKey, refresh);
10603
+ return refresh;
10604
+ }
10605
+ async loadWorldVoiceprintSocialContext(input) {
10606
+ const session = await this.runtime.requireSession();
10607
+ const directChat = this.loadWorldVoiceprintDirectChat(input.authorUserId, session, input.signal);
10608
+ const results = await Promise.all([
10609
+ this.guardWorldVoiceprintSocialSource(async () => await this.loadWorldVoiceprintReciprocalExpectation(input.authorUserId, session, input.signal)),
10610
+ this.guardWorldVoiceprintSocialSource(async () => await this.loadWorldVoiceprintInterwovenRelations(input.authorUserId, directChat, session, input.signal)),
10611
+ this.guardWorldVoiceprintSocialSource(async () => await this.loadWorldVoiceprintPrivateChatRelation(input.authorUserId, directChat)),
10612
+ this.guardWorldVoiceprintSocialSource(async () => await this.loadWorldVoiceprintCommentRelation(input.viewerUserId, input.recordUid, session, input.signal))
10613
+ ]);
10614
+ return {
10615
+ context: { relations: this.dedupeWorldVoiceprintRelations(results.flatMap((result) => result.relations)) },
10616
+ allSourcesSucceeded: results.every((result) => result.succeeded)
10617
+ };
10618
+ }
10619
+ async guardWorldVoiceprintSocialSource(loader) {
10620
+ let timeout;
10621
+ try {
10622
+ return await Promise.race([loader(), new Promise((resolve) => {
10623
+ timeout = setTimeout(() => {
10624
+ resolve({
10625
+ succeeded: false,
10626
+ relations: []
10627
+ });
10628
+ }, ARKME_VOICEPRINT_SOCIAL_SOURCE_TIMEOUT_MILLIS);
10629
+ })]);
10630
+ } catch {
10631
+ return {
10632
+ succeeded: false,
10633
+ relations: []
10634
+ };
10635
+ } finally {
10636
+ if (timeout !== void 0) clearTimeout(timeout);
10637
+ }
10638
+ }
10639
+ async loadWorldVoiceprintReciprocalExpectation(authorUserId, session, signal) {
10640
+ return {
10641
+ succeeded: true,
10642
+ relations: listValue(objectValue$1((await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/world-enrollment-interactions", {}, session, signal, {
10643
+ lane: "interactive-read",
10644
+ key: "world-enrollment-interactions",
10645
+ cacheMs: 5e3
10646
+ })).received).requesters).some((raw) => {
10647
+ const requester = objectValue$1(raw);
10648
+ return integerValue(requester.user_id ?? requester.userId) === authorUserId;
10649
+ }) ? [WORLD_VOICEPRINT_SOCIAL_RELATIONS.reciprocal_expectation] : []
10650
+ };
10651
+ }
10652
+ async loadWorldVoiceprintDirectChat(authorUserId, session, signal) {
10653
+ let pageCursor;
10654
+ const visited = /* @__PURE__ */ new Set();
10655
+ for (;;) {
10656
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/list", {
10657
+ limit: 100,
10658
+ session_kind: 1,
10659
+ ...pageCursor === void 0 ? {} : { page_cursor: pageCursor }
10660
+ }, session, signal, {
10661
+ lane: "interactive-read",
10662
+ bypassCache: true
10663
+ });
10664
+ for (const raw of listValue(data.items)) {
10665
+ const bundle = objectValue$1(raw);
10666
+ const chatSession = objectValue$1(bundle.session);
10667
+ const counterpart = objectValue$1(bundle.private_counterpart);
10668
+ const sessionKind = integerValue(chatSession.session_kind);
10669
+ if (sessionKind !== 1 && sessionKind !== 3 || integerValue(counterpart.user_id ?? counterpart.userId) !== authorUserId) continue;
10670
+ const latestPreview = objectValue$1(bundle.latest_preview);
10671
+ const latestRecord = objectValue$1(latestPreview.record);
10672
+ const latestRelation = objectValue$1(latestPreview.relation);
10673
+ const unread = objectValue$1(bundle.unread_snapshot);
10674
+ const sessionUid = stringValue$1(chatSession.chat_session_uid).trim();
10675
+ if (sessionUid === "") continue;
10676
+ const sharedTopicId = [
10677
+ chatSession,
10678
+ objectValue$1(chatSession.extra),
10679
+ objectValue$1(chatSession.policy_snapshot)
10680
+ ].map((source) => integerValue(source.subject_id ?? source.subjectId ?? source.shared_topic_id ?? source.sharedTopicId)).find((value) => value > 0) ?? 0;
10681
+ const hasPrivateMessageEvidence = Object.keys(latestRecord).length > 0 || Object.keys(objectValue$1(latestRecord.payload)).length > 0 || stringValue$1(latestRecord.record_uid).trim() !== "" || stringValue$1(latestRelation.record_uid).trim() !== "" || integerValue(unread.session_last_seq ?? chatSession.last_seq) > 0;
10682
+ return {
10683
+ sessionUid,
10684
+ sharedTopicId: Math.max(0, sharedTopicId),
10685
+ hasPrivateMessageEvidence
10686
+ };
10687
+ }
10688
+ if (data.has_more !== true) return void 0;
10689
+ const nextCursor = objectValue$1(data.next_page_cursor);
10690
+ if (Object.keys(nextCursor).length === 0) throw new Error("voiceprint direct-chat pagination cursor missing");
10691
+ const identity = JSON.stringify(nextCursor);
10692
+ if (visited.has(identity)) throw new Error("voiceprint direct-chat pagination cursor repeated");
10693
+ visited.add(identity);
10694
+ pageCursor = nextCursor;
10695
+ }
10696
+ }
10697
+ async loadWorldVoiceprintInterwovenRelations(authorUserId, directChatFuture, session, signal) {
10698
+ const directChat = await directChatFuture;
10699
+ if (directChat === void 0 || authorUserId <= 0) return {
10700
+ succeeded: true,
10701
+ relations: []
10702
+ };
10703
+ const data = directChat.sharedTopicId > 0 ? await this.runtime.authenticatedWorldPost("/api/v1/interwoven-moments/summary", { subject_id: directChat.sharedTopicId }, session, signal, {
10704
+ lane: "interactive-read",
10705
+ bypassCache: true
10706
+ }) : await this.runtime.authenticatedChatPost("/api/v1/chats/interwoven/summary", {
10707
+ subject_id: 0,
10708
+ chat_session_uid: directChat.sessionUid
10709
+ }, session, signal, {
10710
+ lane: "interactive-read",
10711
+ bypassCache: true
10712
+ });
10713
+ const stats = listValue(objectValue$1(data.recent_card).total_stats);
10714
+ const countFor = (momentType) => {
10715
+ return integerValue(objectValue$1(stats.find((value) => integerValue(objectValue$1(value).moment_type) === momentType)).count);
10716
+ };
10717
+ const relations = [];
10718
+ const callSummary = data.call_summary;
10719
+ if ((callSummary !== null && typeof callSummary === "object" && !Array.isArray(callSummary) ? integerValue(objectValue$1(callSummary).total_call_count) : countFor(3)) > 0) relations.push(WORLD_VOICEPRINT_SOCIAL_RELATIONS.call);
10720
+ if (countFor(2) > 0) relations.push(WORLD_VOICEPRINT_SOCIAL_RELATIONS.world_interaction);
10721
+ const groupFromStatus = listValue(data.source_status).some((raw) => {
10722
+ const status = objectValue$1(raw);
10723
+ return integerValue(status.moment_type) === 1 && integerValue(status.item_count) > 0;
10724
+ });
10725
+ if (countFor(1) > 0 || groupFromStatus) relations.push(WORLD_VOICEPRINT_SOCIAL_RELATIONS.group_interaction);
10726
+ return {
10727
+ succeeded: true,
10728
+ relations
10729
+ };
10730
+ }
10731
+ async loadWorldVoiceprintPrivateChatRelation(authorUserId, directChatFuture) {
10732
+ const directChat = await directChatFuture;
10733
+ return {
10734
+ succeeded: true,
10735
+ relations: directChat !== void 0 && authorUserId > 0 && directChat.hasPrivateMessageEvidence ? [WORLD_VOICEPRINT_SOCIAL_RELATIONS.private_chat] : []
10736
+ };
10737
+ }
10738
+ async loadWorldVoiceprintCommentRelation(viewerUserId, recordUid, session, signal) {
10739
+ if (recordUid.trim() === "") return {
10740
+ succeeded: true,
10741
+ relations: []
10742
+ };
10743
+ return {
10744
+ succeeded: true,
10745
+ relations: listValue((await this.runtime.authenticatedWorldPost("/api/v1/public-record/extend-list", {
10746
+ record_uid: recordUid,
10747
+ limit: 100,
10748
+ offset: 0
10749
+ }, session, signal, {
10750
+ lane: "interactive-read",
10751
+ bypassCache: true
10752
+ })).list).some((raw) => {
10753
+ const item = objectValue$1(raw);
10754
+ return integerValue(item.user_id ?? item.userId) === viewerUserId;
10755
+ }) ? [WORLD_VOICEPRINT_SOCIAL_RELATIONS.world_interaction] : []
10756
+ };
10757
+ }
10758
+ dedupeWorldVoiceprintRelations(relations) {
10759
+ const seen = /* @__PURE__ */ new Set();
10760
+ return relations.filter((relation) => !seen.has(relation.type) && seen.add(relation.type));
10761
+ }
10762
+ mergeWorldVoiceprintSocialContexts(previous, current) {
10763
+ if (previous === void 0 || previous.relations.length === 0) return current;
10764
+ return { relations: this.dedupeWorldVoiceprintRelations([...previous.relations, ...current.relations]) };
10765
+ }
9675
10766
  async listWorldInteractions(recordRef, options = {}) {
9676
10767
  const session = await this.runtime.requireSession();
9677
10768
  const root = this.openWorldRecordRef(recordRef, session.userId);
@@ -9756,7 +10847,7 @@ var WorldService = class {
9756
10847
  async readWorldImage(imageRef, options = {}) {
9757
10848
  const session = await this.runtime.requireSession();
9758
10849
  const entry = await this.openWorldImageRef(imageRef, session.userId);
9759
- 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)));
9760
10851
  return await this.media.downloadSignedImage(trustedWorldImageUrl(this.runtime.config.environment, entry.sourceUrl), byteLimit, options.signal);
9761
10852
  }
9762
10853
  async publishWorldTextForConversation(recordUid, textContent, signal) {
@@ -9817,18 +10908,88 @@ var WorldService = class {
9817
10908
  avatar: profile.avatarRef,
9818
10909
  template_kind: 1
9819
10910
  }, void 0, signal);
9820
- const checkStatus = Math.trunc(numberValue(published.check_status));
9821
- return {
10911
+ return worldPublicationResult(Math.trunc(numberValue(published.check_status)));
10912
+ } catch (error) {
10913
+ try {
10914
+ if (await this.worldRecordIsPublic(normalizedUid, signal)) return {
10915
+ recordSaved: true,
10916
+ recordState: "synced",
10917
+ worldPublished: true,
10918
+ visibility: "unknown",
10919
+ checkStatus: 0,
10920
+ retryable: false
10921
+ };
10922
+ } catch {}
10923
+ return this.worldPublishFailure(true, error, "synced");
10924
+ }
10925
+ }
10926
+ async publishWorldText(input) {
10927
+ const session = await this.runtime.requireSession();
10928
+ const clientMutationId = input.clientMutationId.trim();
10929
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(clientMutationId)) throw new ArkmePluginError("world-publish-mutation-invalid", "发布请求标识无效,请重试", false);
10930
+ return await this.publishWorldTextForConversation(stableWorldPublishRecordUid(session.userId, clientMutationId), input.textContent);
10931
+ }
10932
+ async publishWorldFileAssets(input) {
10933
+ const session = await this.runtime.requireSession();
10934
+ const clientMutationId = input.clientMutationId.trim();
10935
+ const textContent = input.textContent.trim();
10936
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(clientMutationId)) throw new ArkmePluginError("world-publish-mutation-invalid", "发布请求标识无效,请重试", false);
10937
+ if (textContent === "" || textContent.length > this.runtime.config.maxTextLength) throw new ArkmePluginError("world-text-invalid", "世界内容为空或过长", false);
10938
+ if (input.fileAssets.length === 0 || input.fileAssets.length > 9 || input.fileAssets.some((asset) => !/^[A-Za-z0-9._:-]{8,256}$/.test(asset.fileAssetUid) || asset.fileName.trim() === "" || !asset.mimeType.toLowerCase().startsWith("image/") || !Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > 20971520 || asset.fileKind !== 1)) throw new ArkmePluginError("world-publish-assets-invalid", "世界图片参数无效", false);
10939
+ let profile;
10940
+ try {
10941
+ const snapshot = await this.profile.refreshProfile();
10942
+ if (snapshot.profile === null) throw new ArkmePluginError("profile-unavailable", "无法读取当前 Arkme 账号资料", true);
10943
+ profile = snapshot.profile;
10944
+ } catch (error) {
10945
+ return this.worldPublishFailure(false, error);
10946
+ }
10947
+ if (profile.contact.phoneMasked === void 0) return {
10948
+ recordSaved: false,
10949
+ recordState: "not_saved",
10950
+ worldPublished: false,
10951
+ visibility: "not_published",
10952
+ checkStatus: 0,
10953
+ retryable: false,
10954
+ error: "请先在 Arkme 客户端绑定手机号,再发到世界"
10955
+ };
10956
+ const recordUid = stableWorldPublishRecordUid(session.userId, clientMutationId);
10957
+ try {
10958
+ if (await this.worldRecordIsPublic(recordUid)) return {
9822
10959
  recordSaved: true,
9823
10960
  recordState: "synced",
9824
10961
  worldPublished: true,
9825
- visibility: worldVisibility(checkStatus),
9826
- checkStatus,
10962
+ visibility: "unknown",
10963
+ checkStatus: 0,
9827
10964
  retryable: false
9828
10965
  };
10966
+ } catch {}
10967
+ try {
10968
+ await this.record.createFileAssetsForConversation(recordUid, textContent, input.fileAssets);
10969
+ } catch (error) {
10970
+ return this.worldPublishFailure(false, error);
10971
+ }
10972
+ try {
10973
+ const published = await this.runtime.authenticatedWorldPost("/api/v1/public-record/publish-with-file-assets", {
10974
+ record_uid: recordUid,
10975
+ content: textContent,
10976
+ text_content: textContent,
10977
+ file_assets: input.fileAssets.map((asset, index) => ({
10978
+ file_asset_uid: asset.fileAssetUid,
10979
+ media_type: "image",
10980
+ file_kind: 1,
10981
+ sort_order: index
10982
+ })),
10983
+ tags: worldTags(textContent),
10984
+ original_topic_id: 0,
10985
+ created_at: Date.now(),
10986
+ nick_name: profile.nickname || profile.displayName,
10987
+ avatar: profile.avatarRef
10988
+ }, session);
10989
+ return worldPublicationResult(Math.trunc(numberValue(published.check_status)));
9829
10990
  } catch (error) {
9830
10991
  try {
9831
- if (await this.worldRecordIsPublic(normalizedUid, signal)) return {
10992
+ if (await this.worldRecordIsPublic(recordUid)) return {
9832
10993
  recordSaved: true,
9833
10994
  recordState: "synced",
9834
10995
  worldPublished: true,
@@ -9876,11 +11037,11 @@ var WorldService = class {
9876
11037
  const avatarUrl = rawAvatar.startsWith("file_asset://") ? resolvedAvatars.get(worldAvatarResolutionKey(ownerUserId, rawAvatar)) ?? "" : rawAvatar;
9877
11038
  const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
9878
11039
  const imageRefs = [];
9879
- for (const signedUrl of rawImages.slice(0, 9)) {
11040
+ for (const [index, signedUrl] of rawImages.slice(0, 9).entries()) {
9880
11041
  if (!this.isTrustedWorldImageUrl(signedUrl)) continue;
9881
- imageRefs.push(await this.sealWorldImageRef(viewerUserId, signedUrl));
11042
+ imageRefs.push(await this.sealWorldImageRef(viewerUserId, signedUrl, `record:${recordUid}:image:${String(index)}:${worldImageAssetIdentity(signedUrl)}`));
9882
11043
  }
9883
- 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;
9884
11045
  return {
9885
11046
  recordRef: await this.worldRecordRef(viewerUserId, recordUid, {
9886
11047
  ownerUserId,
@@ -9928,19 +11089,40 @@ var WorldService = class {
9928
11089
  });
9929
11090
  }
9930
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;
9931
11104
  let data;
9932
11105
  try {
9933
- 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);
9934
11107
  } catch {
9935
- return /* @__PURE__ */ new Map();
11108
+ return resolved;
9936
11109
  }
9937
- const resolved = /* @__PURE__ */ new Map();
9938
11110
  for (const raw of listValue(data.items)) {
9939
11111
  const item = objectValue$1(raw);
9940
11112
  const key = worldAvatarResolutionKey(Math.trunc(numberValue(item.owner_user_id)), stringValue$1(item.avatar_ref).trim());
9941
11113
  const url = stringValue$1(item.url).trim();
9942
11114
  if (!requested.has(key) || !this.isTrustedWorldImageUrl(url)) continue;
9943
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);
9944
11126
  }
9945
11127
  return resolved;
9946
11128
  }
@@ -9957,7 +11139,7 @@ var WorldService = class {
9957
11139
  const rawAvatar = stringValue$1(item.avatar ?? item.head_img).trim();
9958
11140
  const avatarUrl = rawAvatar.startsWith("file_asset://") ? resolvedAvatars.get(worldAvatarResolutionKey(ownerUserId, rawAvatar)) ?? "" : rawAvatar;
9959
11141
  const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
9960
- 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;
9961
11143
  return {
9962
11144
  interactionRef: await this.worldRecordRef(viewerUserId, recordUid, {
9963
11145
  ownerUserId,
@@ -10022,11 +11204,12 @@ var WorldService = class {
10022
11204
  this.worldImageRefs.delete(oldest);
10023
11205
  }
10024
11206
  }
10025
- async sealWorldImageRef(viewerUserId, sourceUrl) {
11207
+ async sealWorldImageRef(viewerUserId, sourceUrl, stableIdentity) {
10026
11208
  const now = Date.now();
10027
11209
  this.pruneWorldImageRefs(now);
10028
- const token = randomUUID();
10029
- 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");
10030
11213
  this.worldImageRefs.set(token, {
10031
11214
  viewerUserId,
10032
11215
  sourceUrl,
@@ -10072,10 +11255,6 @@ var WorldService = class {
10072
11255
  }
10073
11256
  };
10074
11257
  //#endregion
10075
- //#region src/types.ts
10076
- const ARKME_PROVIDER_CONTRACT_VERSION = 1;
10077
- const ARKME_DEFAULT_SHARE_WEBSITE = "https://app.arkme.ai";
10078
- //#endregion
10079
11258
  //#region src/arkme-service.ts
10080
11259
  function worldVoiceprintInviteMessage(input) {
10081
11260
  return `${input.textPreview === void 0 ? "我想邀请你开启声纹。" : `我看到你在世界里发的「${input.textPreview}」,想邀请你开启声纹。`}\n以后看到你的世界动态时,可以直接听到你的声音。\n${input.inviteUrl}`;
@@ -10117,6 +11296,7 @@ var ArkmeService = class {
10117
11296
  aiPolish;
10118
11297
  chat;
10119
11298
  contact;
11299
+ voiceprint;
10120
11300
  constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker()) {
10121
11301
  this.config = config;
10122
11302
  this.sessionStore = sessionStore;
@@ -10151,9 +11331,10 @@ var ArkmeService = class {
10151
11331
  this.community = new CommunityService(this.runtime, this.source, this.profile);
10152
11332
  this.interwoven = new InterwovenService(this.runtime, this.source, this.profile);
10153
11333
  this.aiPolish = new GroupAiPolishService(this.runtime, this.source, { sendChatSourceTextRaw: async (...args) => await this.chat.sendChatSourceTextRaw(...args) });
10154
- 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) });
10155
11335
  this.chat = new ChatService(this.runtime, this.source, this.profile, this.media, this.record, this.bot, this.arko, this.aiPolish, this.realtime);
10156
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) });
10157
11338
  this.auth = new AuthService(this.runtime, this.profile, {
10158
11339
  reconnectChatRealtime: () => {
10159
11340
  this.realtime.reconnect();
@@ -10236,6 +11417,7 @@ var ArkmeService = class {
10236
11417
  environment: this.config.environment,
10237
11418
  testLoginEnabled: this.config.environment === "test",
10238
11419
  callAssetBasePath: `${this.config.routePath}/call`,
11420
+ voiceprintEnrollmentPath: `${this.config.routePath}/voiceprint/enroll`,
10239
11421
  shareWebsite: this.config.shareWebsite ?? "https://app.arkme.ai"
10240
11422
  };
10241
11423
  }
@@ -10277,13 +11459,14 @@ var ArkmeService = class {
10277
11459
  extensionPreviews: true,
10278
11460
  worldFeed: true,
10279
11461
  worldInteractions: true,
11462
+ worldPublish: true,
10280
11463
  worldVoiceprintPlayback: true,
10281
11464
  worldVoiceprintInvite: true,
11465
+ worldVoiceprintSocialContext: true,
11466
+ voiceprintManagement: true,
10282
11467
  arrangements: true,
10283
11468
  myExtensions: true,
10284
11469
  extensionPublish: true,
10285
- recordingTranscripts: true,
10286
- recordingDoubaoBackfill: true,
10287
11470
  extensionReviews: true,
10288
11471
  ...this.relatedRecording.isEnabled() ? { relatedRecordings: true } : {}
10289
11472
  },
@@ -10370,12 +11553,8 @@ var ArkmeService = class {
10370
11553
  return await this.recording.recordingCalendar(fromStamp, toStamp, signal);
10371
11554
  }
10372
11555
  /** Read-only Audio capability shared by the built-in UI and Arkme recording tools. */
10373
- async recordingTranscript(dateStamp, signal, source = "system") {
10374
- return await this.recording.recordingTranscript(dateStamp, signal, source);
10375
- }
10376
- /** Queue Doubao backfill only after an explicit UI, SDK, or Tool request. */
10377
- async startRecordingDoubaoBackfill(dateStamp, signal) {
10378
- return await this.recording.startRecordingDoubaoBackfill(dateStamp, signal);
11556
+ async recordingTranscript(dateStamp, signal) {
11557
+ return await this.recording.recordingTranscript(dateStamp, signal);
10379
11558
  }
10380
11559
  /** Read-only Audio capability shared by the built-in UI and Arkme recording tools. */
10381
11560
  async recordingProjection(dateStamp, kind, signal) {
@@ -10498,6 +11677,12 @@ var ArkmeService = class {
10498
11677
  async listGroupMembers(sourceRef, options = {}) {
10499
11678
  return await this.group.listGroupMembers(sourceRef, options);
10500
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
+ }
10501
11686
  async listGroupMemberCandidates(sourceRef, options = {}) {
10502
11687
  return await this.group.listGroupMemberCandidates(sourceRef, options);
10503
11688
  }
@@ -10534,6 +11719,9 @@ var ArkmeService = class {
10534
11719
  async openPrivateChatFromUser(peerUserId, options = {}) {
10535
11720
  return await this.chat.openPrivateChatFromUser(peerUserId, options);
10536
11721
  }
11722
+ async openPrivateChatFromMember(sourceRef, memberRef, options = {}) {
11723
+ return await this.chat.openPrivateChatFromMember(sourceRef, memberRef, options);
11724
+ }
10537
11725
  async readSource(sourceRef, options = {}) {
10538
11726
  return await this.chat.readSource(sourceRef, options);
10539
11727
  }
@@ -10726,12 +11914,18 @@ var ArkmeService = class {
10726
11914
  async listMyWorldFeed(options = {}) {
10727
11915
  return await this.world.listMyWorldFeed(options);
10728
11916
  }
11917
+ async listUserWorldFeed(userId, options = {}) {
11918
+ return await this.world.listUserWorldFeed(userId, options);
11919
+ }
10729
11920
  async worldVoiceprintPlaybackAvailability(recordRefs, signal) {
10730
11921
  return await this.world.worldVoiceprintPlaybackAvailability(recordRefs, signal);
10731
11922
  }
10732
11923
  async generateWorldVoiceprintPlayback(input) {
10733
11924
  return await this.world.generateWorldVoiceprintPlayback(input);
10734
11925
  }
11926
+ async worldVoiceprintSocialContext(recordRef, options = {}) {
11927
+ return await this.world.worldVoiceprintSocialContext(recordRef, options);
11928
+ }
10735
11929
  async inviteWorldVoiceprint(recordRef, signal) {
10736
11930
  try {
10737
11931
  const intent = await this.world.createWorldVoiceprintInviteIntent(recordRef, signal);
@@ -10757,6 +11951,36 @@ var ArkmeService = class {
10757
11951
  throw error;
10758
11952
  }
10759
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
+ }
10760
11984
  /** Read the authenticated comment/reply tree behind one account-bound World reference. */
10761
11985
  async listWorldInteractions(recordRef, options = {}) {
10762
11986
  return await this.world.listWorldInteractions(recordRef, options);
@@ -10772,6 +11996,12 @@ var ArkmeService = class {
10772
11996
  async publishWorldTextForConversation(recordUid, textContent, signal) {
10773
11997
  return await this.world.publishWorldTextForConversation(recordUid, textContent, signal);
10774
11998
  }
11999
+ async publishWorldText(input) {
12000
+ return await this.world.publishWorldText(input);
12001
+ }
12002
+ async publishWorldFileAssets(input) {
12003
+ return await this.world.publishWorldFileAssets(input);
12004
+ }
10775
12005
  async createText(recordUid, textContent) {
10776
12006
  return await this.record.createText(recordUid, textContent);
10777
12007
  }
@@ -12156,4 +13386,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
12156
13386
  return jsonValue(await handler(jsonValue(args)));
12157
13387
  }
12158
13388
  //#endregion
12159
- export { unpackArkmeExtension as A, ArkmeOutgoingCallError as B, packBundleFiles as C, assertExtensionArtifactSize as D, renderArkmeSandboxHostEntry as E, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as F, 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, 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, ArkmePluginError 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 };