@senguoyun/dsh-arkme 0.1.11 → 0.1.13

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 (218) hide show
  1. package/README.md +6 -2
  2. package/assets/desktop_call/avatar-lin-xiaoman.jpeg +0 -0
  3. package/assets/desktop_call/avatar-mother.jpg +0 -0
  4. package/assets/desktop_call/avatar-self.png +0 -0
  5. package/assets/desktop_call/call-add-linear.svg +13 -0
  6. package/assets/desktop_call/icon-scan-add-contact.svg +4 -0
  7. package/assets/desktop_call/profile-circle-linear.svg +11 -0
  8. package/assets/desktop_call/user-add-linear.svg +15 -0
  9. package/cordis.patch.yml +4 -1
  10. package/docs/consumer-plugin-contract.md +13 -4
  11. package/docs/extension-market-controls.md +1 -1
  12. package/docs/gui-production-integration.md +46 -0
  13. package/docs/record-calendar-ui-flow.md +88 -0
  14. package/docs/tool-registry.md +4 -0
  15. package/lib/client.js +43275 -15385
  16. package/lib/client.js.map +1 -1
  17. package/lib/extension-profile-restart-helper.js +19 -3
  18. package/lib/index.js +2293 -420
  19. package/lib/{install-store-D6P-dH2i.js → install-store-Sn3Ccxfu.js} +32 -5
  20. package/lib/persistent-extension.js +2 -2
  21. package/lib/{persistent-runtime-DInn3Zb1.js → persistent-runtime-Cki0XC3L.js} +1941 -288
  22. package/lib/plugin-updater-helper-CiPlNqBc.js +346 -0
  23. package/lib/plugin-updater-helper.js +2 -250
  24. package/lib/sdk.js +202 -3
  25. package/lib/types/arkme-service.d.ts +67 -2
  26. package/lib/types/arkme-service.d.ts.map +1 -1
  27. package/lib/types/chat-realtime.d.ts +8 -0
  28. package/lib/types/chat-realtime.d.ts.map +1 -1
  29. package/lib/types/client/ArkmeAccountMenu.d.ts +4 -0
  30. package/lib/types/client/ArkmeAccountMenu.d.ts.map +1 -0
  31. package/lib/types/client/ArkmeAppUpdateDialog.d.ts +5 -0
  32. package/lib/types/client/ArkmeAppUpdateDialog.d.ts.map +1 -0
  33. package/lib/types/client/ArkmeArkoSurface.d.ts.map +1 -1
  34. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  35. package/lib/types/client/ArkmeBrand.d.ts +35 -0
  36. package/lib/types/client/ArkmeBrand.d.ts.map +1 -0
  37. package/lib/types/client/ArkmeCalendarSurface.d.ts +10 -0
  38. package/lib/types/client/ArkmeCalendarSurface.d.ts.map +1 -0
  39. package/lib/types/client/ArkmeCallHistorySurface.d.ts +57 -0
  40. package/lib/types/client/ArkmeCallHistorySurface.d.ts.map +1 -0
  41. package/lib/types/client/ArkmeContactAddSurface.d.ts +9 -0
  42. package/lib/types/client/ArkmeContactAddSurface.d.ts.map +1 -0
  43. package/lib/types/client/ArkmeConversationSurface.d.ts +5 -3
  44. package/lib/types/client/ArkmeConversationSurface.d.ts.map +1 -1
  45. package/lib/types/client/ArkmeExtensionAvatar.d.ts.map +1 -1
  46. package/lib/types/client/ArkmeExtensionPublishDialog.d.ts.map +1 -1
  47. package/lib/types/client/ArkmeExtensionReviews.d.ts +10 -0
  48. package/lib/types/client/ArkmeExtensionReviews.d.ts.map +1 -1
  49. package/lib/types/client/ArkmeFooterDropdown.d.ts +1 -1
  50. package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
  51. package/lib/types/client/ArkmeGroupChatControls.d.ts +1 -0
  52. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  53. package/lib/types/client/ArkmeLogin.d.ts.map +1 -1
  54. package/lib/types/client/ArkmeMarketplace.d.ts +153 -0
  55. package/lib/types/client/ArkmeMarketplace.d.ts.map +1 -0
  56. package/lib/types/client/ArkmePrivateCallMenu.d.ts.map +1 -1
  57. package/lib/types/client/ArkmeProductNavigation.d.ts +8 -0
  58. package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -0
  59. package/lib/types/client/ArkmeQuickAdd.d.ts +12 -0
  60. package/lib/types/client/ArkmeQuickAdd.d.ts.map +1 -0
  61. package/lib/types/client/ArkmeRecordingSurface.d.ts +109 -3
  62. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  63. package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
  64. package/lib/types/client/ArkmeSendToSelfIcon.d.ts +5 -0
  65. package/lib/types/client/ArkmeSendToSelfIcon.d.ts.map +1 -0
  66. package/lib/types/client/ArkmeSettingsRow.d.ts.map +1 -1
  67. package/lib/types/client/ArkmeSettingsSurface.d.ts +22 -0
  68. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -0
  69. package/lib/types/client/ArkmeSharedExtensionDetail.d.ts +7 -0
  70. package/lib/types/client/ArkmeSharedExtensionDetail.d.ts.map +1 -0
  71. package/lib/types/client/ArkmeSidebar.d.ts +11 -2
  72. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  73. package/lib/types/client/ArkmeTopicCreateDialog.d.ts +1 -1
  74. package/lib/types/client/ArkmeTopicCreateDialog.d.ts.map +1 -1
  75. package/lib/types/client/ArkmeTopicDirectoryPopover.d.ts +1 -0
  76. package/lib/types/client/ArkmeTopicDirectoryPopover.d.ts.map +1 -1
  77. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +18 -1
  78. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  79. package/lib/types/client/ArkmeWorldSurface.d.ts +30 -0
  80. package/lib/types/client/ArkmeWorldSurface.d.ts.map +1 -0
  81. package/lib/types/client/api.d.ts +1 -1
  82. package/lib/types/client/api.d.ts.map +1 -1
  83. package/lib/types/client/app-update-store.d.ts +35 -0
  84. package/lib/types/client/app-update-store.d.ts.map +1 -0
  85. package/lib/types/client/arkme-assets.d.ts +2 -2
  86. package/lib/types/client/arkme-assets.d.ts.map +1 -1
  87. package/lib/types/client/arkme-theme.d.ts +5 -3
  88. package/lib/types/client/arkme-theme.d.ts.map +1 -1
  89. package/lib/types/client/arkme-wordmark.d.ts +3 -0
  90. package/lib/types/client/arkme-wordmark.d.ts.map +1 -0
  91. package/lib/types/client/demo-audio.d.ts +2 -0
  92. package/lib/types/client/demo-audio.d.ts.map +1 -0
  93. package/lib/types/client/desktop-notification-runtime.d.ts +35 -0
  94. package/lib/types/client/desktop-notification-runtime.d.ts.map +1 -0
  95. package/lib/types/client/extension-market-model.d.ts +1 -0
  96. package/lib/types/client/extension-market-model.d.ts.map +1 -1
  97. package/lib/types/client/extension-share-deeplink.d.ts +5 -0
  98. package/lib/types/client/extension-share-deeplink.d.ts.map +1 -0
  99. package/lib/types/client/index.d.ts +15 -2
  100. package/lib/types/client/index.d.ts.map +1 -1
  101. package/lib/types/client/interwoven-moments.d.ts +1 -1
  102. package/lib/types/client/interwoven-moments.d.ts.map +1 -1
  103. package/lib/types/client/notification-activation-store.d.ts +16 -0
  104. package/lib/types/client/notification-activation-store.d.ts.map +1 -0
  105. package/lib/types/client/personal-test-edition.d.ts +17 -0
  106. package/lib/types/client/personal-test-edition.d.ts.map +1 -0
  107. package/lib/types/client/related-recordings.d.ts +1 -0
  108. package/lib/types/client/related-recordings.d.ts.map +1 -1
  109. package/lib/types/client/ui-controller.d.ts +13 -1
  110. package/lib/types/client/ui-controller.d.ts.map +1 -1
  111. package/lib/types/extensions/bundle-artifact.d.ts +22 -0
  112. package/lib/types/extensions/bundle-artifact.d.ts.map +1 -1
  113. package/lib/types/extensions/install-store.d.ts +1 -0
  114. package/lib/types/extensions/install-store.d.ts.map +1 -1
  115. package/lib/types/extensions/manager.d.ts +52 -2
  116. package/lib/types/extensions/manager.d.ts.map +1 -1
  117. package/lib/types/extensions/owned-inventory.d.ts +6 -3
  118. package/lib/types/extensions/owned-inventory.d.ts.map +1 -1
  119. package/lib/types/extensions/owned-refs.d.ts +1 -1
  120. package/lib/types/extensions/owned-refs.d.ts.map +1 -1
  121. package/lib/types/extensions/owned-types.d.ts +19 -5
  122. package/lib/types/extensions/owned-types.d.ts.map +1 -1
  123. package/lib/types/extensions/persistent-bundle.d.ts +7 -0
  124. package/lib/types/extensions/persistent-bundle.d.ts.map +1 -1
  125. package/lib/types/extensions/persistent-client-bundle.d.ts.map +1 -1
  126. package/lib/types/extensions/persistent-runtime.d.ts +6 -0
  127. package/lib/types/extensions/persistent-runtime.d.ts.map +1 -1
  128. package/lib/types/extensions/profile-owned-inventory.d.ts +1 -0
  129. package/lib/types/extensions/profile-owned-inventory.d.ts.map +1 -1
  130. package/lib/types/extensions/profile-restart-helper.d.ts.map +1 -1
  131. package/lib/types/extensions/publish-client.d.ts +51 -2
  132. package/lib/types/extensions/publish-client.d.ts.map +1 -1
  133. package/lib/types/extensions/share-discovery-relay.d.ts +33 -0
  134. package/lib/types/extensions/share-discovery-relay.d.ts.map +1 -0
  135. package/lib/types/extensions/signature.d.ts +31 -1
  136. package/lib/types/extensions/signature.d.ts.map +1 -1
  137. package/lib/types/extensions/types.d.ts +118 -5
  138. package/lib/types/extensions/types.d.ts.map +1 -1
  139. package/lib/types/host-api.d.ts.map +1 -1
  140. package/lib/types/index.d.ts +12 -4
  141. package/lib/types/index.d.ts.map +1 -1
  142. package/lib/types/outgoing-call-assets.d.ts.map +1 -1
  143. package/lib/types/plugin-update-artifact.d.ts +46 -0
  144. package/lib/types/plugin-update-artifact.d.ts.map +1 -0
  145. package/lib/types/plugin-update.d.ts +20 -11
  146. package/lib/types/plugin-update.d.ts.map +1 -1
  147. package/lib/types/plugin-updater-helper.d.ts +11 -0
  148. package/lib/types/plugin-updater-helper.d.ts.map +1 -1
  149. package/lib/types/recording-presentation.d.ts +2 -2
  150. package/lib/types/recording-presentation.d.ts.map +1 -1
  151. package/lib/types/rich-media-routes.d.ts.map +1 -1
  152. package/lib/types/sdk/index.d.ts +103 -7
  153. package/lib/types/sdk/index.d.ts.map +1 -1
  154. package/lib/types/services/bot-service.d.ts +5 -1
  155. package/lib/types/services/bot-service.d.ts.map +1 -1
  156. package/lib/types/services/calendar-service.d.ts +25 -0
  157. package/lib/types/services/calendar-service.d.ts.map +1 -0
  158. package/lib/types/services/chat-realtime-service.d.ts +19 -4
  159. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  160. package/lib/types/services/contact-service.d.ts +25 -0
  161. package/lib/types/services/contact-service.d.ts.map +1 -0
  162. package/lib/types/services/group-service.d.ts +20 -2
  163. package/lib/types/services/group-service.d.ts.map +1 -1
  164. package/lib/types/services/media-service.d.ts +9 -1
  165. package/lib/types/services/media-service.d.ts.map +1 -1
  166. package/lib/types/services/recording-service.d.ts +4 -2
  167. package/lib/types/services/recording-service.d.ts.map +1 -1
  168. package/lib/types/services/search-service.d.ts +10 -2
  169. package/lib/types/services/search-service.d.ts.map +1 -1
  170. package/lib/types/services/service.d.ts +2 -0
  171. package/lib/types/services/service.d.ts.map +1 -1
  172. package/lib/types/services/world-service.d.ts +20 -1
  173. package/lib/types/services/world-service.d.ts.map +1 -1
  174. package/lib/types/tools/business/calendar/index.d.ts +4 -0
  175. package/lib/types/tools/business/calendar/index.d.ts.map +1 -0
  176. package/lib/types/tools/business/contacts/index.d.ts +4 -0
  177. package/lib/types/tools/business/contacts/index.d.ts.map +1 -0
  178. package/lib/types/tools/business/groups/create.d.ts +2 -0
  179. package/lib/types/tools/business/groups/create.d.ts.map +1 -0
  180. package/lib/types/tools/business/groups/index.d.ts +4 -0
  181. package/lib/types/tools/business/groups/index.d.ts.map +1 -0
  182. package/lib/types/tools/business/index.d.ts.map +1 -1
  183. package/lib/types/tools/business/recordings/index.d.ts +2 -1
  184. package/lib/types/tools/business/recordings/index.d.ts.map +1 -1
  185. package/lib/types/tools/business/records/images.d.ts +2 -0
  186. package/lib/types/tools/business/records/images.d.ts.map +1 -0
  187. package/lib/types/tools/business/records/index.d.ts.map +1 -1
  188. package/lib/types/tools/business/world/index.d.ts +2 -0
  189. package/lib/types/tools/business/world/index.d.ts.map +1 -1
  190. package/lib/types/tools/business/world/mine.d.ts +2 -0
  191. package/lib/types/tools/business/world/mine.d.ts.map +1 -0
  192. package/lib/types/tools/business/world/voiceprint-invite.d.ts +2 -0
  193. package/lib/types/tools/business/world/voiceprint-invite.d.ts.map +1 -0
  194. package/lib/types/tools/extensions/index.d.ts.map +1 -1
  195. package/lib/types/tools/extensions/publish-conversation.d.ts +9 -1
  196. package/lib/types/tools/extensions/publish-conversation.d.ts.map +1 -1
  197. package/lib/types/tools/ports/calendar.d.ts +17 -0
  198. package/lib/types/tools/ports/calendar.d.ts.map +1 -0
  199. package/lib/types/tools/ports/contacts.d.ts +12 -0
  200. package/lib/types/tools/ports/contacts.d.ts.map +1 -0
  201. package/lib/types/tools/ports/groups.d.ts +14 -0
  202. package/lib/types/tools/ports/groups.d.ts.map +1 -0
  203. package/lib/types/tools/ports/index.d.ts +5 -2
  204. package/lib/types/tools/ports/index.d.ts.map +1 -1
  205. package/lib/types/tools/ports/recordings.d.ts +3 -2
  206. package/lib/types/tools/ports/recordings.d.ts.map +1 -1
  207. package/lib/types/tools/ports/records.d.ts +6 -1
  208. package/lib/types/tools/ports/records.d.ts.map +1 -1
  209. package/lib/types/tools/ports/world.d.ts +12 -1
  210. package/lib/types/tools/ports/world.d.ts.map +1 -1
  211. package/lib/types/tools/prompts/business.d.ts.map +1 -1
  212. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  213. package/lib/types/types.d.ts +218 -3
  214. package/lib/types/types.d.ts.map +1 -1
  215. package/package.json +6 -4
  216. package/lib/plugin-update-install-state-CIsOCjWK.js +0 -67
  217. package/lib/types/client/ArkmeExtensionCenter.d.ts +0 -61
  218. package/lib/types/client/ArkmeExtensionCenter.d.ts.map +0 -1
@@ -1,12 +1,13 @@
1
1
  import { chmodSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { createHash, createHmac, createPublicKey, randomUUID, timingSafeEqual, verify } from "node:crypto";
3
3
  import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { open } from "node:fs/promises";
5
6
  import OSS from "ali-oss";
7
+ import { gunzipSync, gzipSync } from "node:zlib";
6
8
  import { createContext, runInContext } from "node:vm";
7
9
  import { Context } from "@deepseek-ai/cordis";
8
10
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
- import { gunzipSync, gzipSync } from "node:zlib";
10
11
  import { parseDocument } from "yaml";
11
12
  //#region src/outgoing-call-contract.ts
12
13
  var ArkmeOutgoingCallError = class extends Error {
@@ -957,6 +958,17 @@ var ServiceRuntime = class {
957
958
  return await this.post(this.config.recordBaseUrl, path, body, session.accessToken, [0], signal, false, requestOptions());
958
959
  }
959
960
  }
961
+ async authenticatedCalendarPost(path, body, initialSession, signal, options = {}) {
962
+ let session = initialSession ?? await this.requireSession();
963
+ const requestOptions = () => this.authenticatedRequestOptions(session, "record", "interactive-read", options);
964
+ try {
965
+ return await this.post(this.config.recordBaseUrl, path, body, session.accessToken, [0, 200], signal, false, requestOptions());
966
+ } catch (error) {
967
+ if (!(error instanceof ArkmePluginError) || !["auth-http-401", "auth-http-403"].includes(error.code)) throw error;
968
+ session = await this.refreshAccessToken(session);
969
+ return await this.post(this.config.recordBaseUrl, path, body, session.accessToken, [0, 200], signal, false, requestOptions());
970
+ }
971
+ }
960
972
  async authenticatedAuthPost(path, body, initialSession, signal, options = {}) {
961
973
  let session = initialSession ?? await this.requireSession();
962
974
  const requestOptions = () => this.authenticatedRequestOptions(session, "auth", "write", options);
@@ -1060,7 +1072,7 @@ var ServiceRuntime = class {
1060
1072
  }
1061
1073
  async extensionPost(path, body, signal, options = {}) {
1062
1074
  const baseUrl = this.config.extensionPublishBaseUrl?.trim() ?? "";
1063
- if (baseUrl === "") throw new ArkmePluginError("extension-service-disabled", "扩展市场服务尚未配置", false, 503);
1075
+ if (baseUrl === "") throw new ArkmePluginError("extension-service-disabled", "市集服务尚未配置", false, 503);
1064
1076
  let session = await this.requireSession();
1065
1077
  const requestOptions = () => ({
1066
1078
  ...options,
@@ -1079,13 +1091,13 @@ var ServiceRuntime = class {
1079
1091
  };
1080
1092
  //#endregion
1081
1093
  //#region src/services/ai-video-service.ts
1082
- function numberValue$20(value) {
1094
+ function numberValue$22(value) {
1083
1095
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1084
1096
  }
1085
- function booleanValue$11(value) {
1097
+ function booleanValue$13(value) {
1086
1098
  return value === true;
1087
1099
  }
1088
- function listValue$20(value) {
1100
+ function listValue$21(value) {
1089
1101
  return Array.isArray(value) ? value : [];
1090
1102
  }
1091
1103
  var AiVideoService = class {
@@ -1097,12 +1109,12 @@ var AiVideoService = class {
1097
1109
  const session = await this.runtime.requireSession();
1098
1110
  const data = await this.runtime.authenticatedIntelligentPost("/api/v1/ai-comic-video/preflight", this.aiVideoSelectionBody(sessionId, segments), session, signal);
1099
1111
  return {
1100
- allowed: booleanValue$11(data.allowed),
1112
+ allowed: booleanValue$13(data.allowed),
1101
1113
  message: stringValue$1(data.message).trim() || "AI 视频内容检查已完成",
1102
- selectedDurationMillis: Math.max(0, numberValue$20(data.selected_duration_millis)),
1103
- minimumDurationMillis: Math.max(0, numberValue$20(data.minimum_duration_millis)),
1104
- selectedSegmentCount: Math.max(0, numberValue$20(data.selected_segment_count)),
1105
- retryable: booleanValue$11(data.retryable),
1114
+ selectedDurationMillis: Math.max(0, numberValue$22(data.selected_duration_millis)),
1115
+ minimumDurationMillis: Math.max(0, numberValue$22(data.minimum_duration_millis)),
1116
+ selectedSegmentCount: Math.max(0, numberValue$22(data.selected_segment_count)),
1117
+ retryable: booleanValue$13(data.retryable),
1106
1118
  ...stringValue$1(data.reason_code).trim() === "" ? {} : { reasonCode: stringValue$1(data.reason_code).trim() },
1107
1119
  ...stringValue$1(data.proof).trim() === "" ? {} : { proof: stringValue$1(data.proof).trim() }
1108
1120
  };
@@ -1137,8 +1149,8 @@ var AiVideoService = class {
1137
1149
  ...options.statuses === void 0 || options.statuses.length === 0 ? {} : { statuses: options.statuses }
1138
1150
  }, session, options.signal);
1139
1151
  return {
1140
- items: listValue$20(data.items).map((raw) => this.aiVideoListItem(raw)).filter((item) => item !== void 0),
1141
- hasMore: booleanValue$11(data.has_more),
1152
+ items: listValue$21(data.items).map((raw) => this.aiVideoListItem(raw)).filter((item) => item !== void 0),
1153
+ hasMore: booleanValue$13(data.has_more),
1142
1154
  ...stringValue$1(data.next_cursor).trim() === "" ? {} : { nextCursor: stringValue$1(data.next_cursor).trim() }
1143
1155
  };
1144
1156
  }
@@ -1149,13 +1161,13 @@ var AiVideoService = class {
1149
1161
  texts
1150
1162
  }, session, signal);
1151
1163
  return {
1152
- allowed: booleanValue$11(data.allowed),
1164
+ allowed: booleanValue$13(data.allowed),
1153
1165
  message: stringValue$1(data.message).trim() || "AI 视频内容检查已完成",
1154
- selectedDurationMillis: Math.max(0, numberValue$20(data.selected_duration_millis)),
1155
- minimumDurationMillis: Math.max(0, numberValue$20(data.minimum_duration_millis)),
1156
- selectedSegmentCount: Math.max(0, numberValue$20(data.selected_segment_count)),
1157
- selectedTextCount: Math.max(0, numberValue$20(data.selected_text_count)),
1158
- retryable: booleanValue$11(data.retryable),
1166
+ selectedDurationMillis: Math.max(0, numberValue$22(data.selected_duration_millis)),
1167
+ minimumDurationMillis: Math.max(0, numberValue$22(data.minimum_duration_millis)),
1168
+ selectedSegmentCount: Math.max(0, numberValue$22(data.selected_segment_count)),
1169
+ selectedTextCount: Math.max(0, numberValue$22(data.selected_text_count)),
1170
+ retryable: booleanValue$13(data.retryable),
1159
1171
  ...stringValue$1(data.reason_code).trim() === "" ? {} : { reasonCode: stringValue$1(data.reason_code).trim() },
1160
1172
  ...stringValue$1(data.proof).trim() === "" ? {} : { proof: stringValue$1(data.proof).trim() }
1161
1173
  };
@@ -1194,19 +1206,19 @@ var AiVideoService = class {
1194
1206
  "canceled"
1195
1207
  ])).has(status)) throw new ArkmePluginError("ai-video-contract-invalid", "AI 视频服务返回了无效任务信息", true, 502);
1196
1208
  const selection = objectValue$1(data.selection);
1197
- const segmentCount = listValue$20(selection.segments).length;
1198
- const textCount = listValue$20(selection.texts).length;
1209
+ const segmentCount = listValue$21(selection.segments).length;
1210
+ const textCount = listValue$21(selection.texts).length;
1199
1211
  return {
1200
1212
  jobId,
1201
1213
  status,
1202
1214
  stage: stringValue$1(data.stage).trim() || status,
1203
- progress: Math.min(100, Math.max(0, Math.trunc(numberValue$20(data.progress)))),
1215
+ progress: Math.min(100, Math.max(0, Math.trunc(numberValue$22(data.progress)))),
1204
1216
  selectedSegmentCount: segmentCount,
1205
1217
  ...textCount === 0 ? {} : { selectedTextCount: textCount },
1206
- retryable: booleanValue$11(data.retryable),
1218
+ retryable: booleanValue$13(data.retryable),
1207
1219
  ...stringValue$1(data.video_asset_uid).trim() === "" ? {} : { videoAssetUid: stringValue$1(data.video_asset_uid).trim() },
1208
1220
  ...stringValue$1(data.cover_asset_uid).trim() === "" ? {} : { coverAssetUid: stringValue$1(data.cover_asset_uid).trim() },
1209
- ...numberValue$20(data.video_duration_millis) <= 0 ? {} : { videoDurationMillis: numberValue$20(data.video_duration_millis) },
1221
+ ...numberValue$22(data.video_duration_millis) <= 0 ? {} : { videoDurationMillis: numberValue$22(data.video_duration_millis) },
1210
1222
  ...stringValue$1(data.error_code).trim() === "" ? {} : { errorCode: stringValue$1(data.error_code).trim() },
1211
1223
  ...stringValue$1(data.error_message).trim() === "" ? {} : { errorMessage: stringValue$1(data.error_message).trim() },
1212
1224
  ...stringValue$1(data.failure_stage).trim() === "" ? {} : { failureStage: stringValue$1(data.failure_stage).trim() }
@@ -1229,17 +1241,17 @@ var AiVideoService = class {
1229
1241
  sessionId: stringValue$1(item.session_id).trim(),
1230
1242
  status,
1231
1243
  stage: stringValue$1(item.stage).trim() || status,
1232
- progress: Math.min(100, Math.max(0, Math.trunc(numberValue$20(item.progress)))),
1244
+ progress: Math.min(100, Math.max(0, Math.trunc(numberValue$22(item.progress)))),
1233
1245
  title: stringValue$1(source.title).trim() || "长录音 AI 视频",
1234
- sourceStartedAtMillis: numberValue$20(source.started_at),
1235
- selectedDurationMillis: Math.max(0, numberValue$20(source.selected_duration_millis)),
1236
- selectedSegmentCount: Math.max(0, Math.trunc(numberValue$20(item.selected_segment_count))),
1237
- retryable: booleanValue$11(item.retryable),
1238
- createdAtMillis: numberValue$20(item.created_at),
1239
- updatedAtMillis: numberValue$20(item.updated_at),
1246
+ sourceStartedAtMillis: numberValue$22(source.started_at),
1247
+ selectedDurationMillis: Math.max(0, numberValue$22(source.selected_duration_millis)),
1248
+ selectedSegmentCount: Math.max(0, Math.trunc(numberValue$22(item.selected_segment_count))),
1249
+ retryable: booleanValue$13(item.retryable),
1250
+ createdAtMillis: numberValue$22(item.created_at),
1251
+ updatedAtMillis: numberValue$22(item.updated_at),
1240
1252
  ...stringValue$1(item.cover_asset_uid).trim() === "" ? {} : { coverAssetUid: stringValue$1(item.cover_asset_uid).trim() },
1241
1253
  ...stringValue$1(item.video_asset_uid).trim() === "" ? {} : { videoAssetUid: stringValue$1(item.video_asset_uid).trim() },
1242
- ...numberValue$20(item.video_duration_millis) <= 0 ? {} : { videoDurationMillis: numberValue$20(item.video_duration_millis) },
1254
+ ...numberValue$22(item.video_duration_millis) <= 0 ? {} : { videoDurationMillis: numberValue$22(item.video_duration_millis) },
1243
1255
  ...stringValue$1(item.error_code).trim() === "" ? {} : { errorCode: stringValue$1(item.error_code).trim() },
1244
1256
  ...stringValue$1(item.error_message).trim() === "" ? {} : { errorMessage: clippedText(item.error_message, 500) }
1245
1257
  };
@@ -1265,13 +1277,13 @@ const ARKO_RUN_STATUSES = /* @__PURE__ */ new Set([
1265
1277
  "expired"
1266
1278
  ]);
1267
1279
  const ARKO_PROFILE_DISPLAY_NAME_CACHE_TTL_MS = 10 * 6e4;
1268
- function numberValue$19(value) {
1280
+ function numberValue$21(value) {
1269
1281
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1270
1282
  }
1271
- function booleanValue$10(value) {
1283
+ function booleanValue$12(value) {
1272
1284
  return value === true;
1273
1285
  }
1274
- function listValue$19(value) {
1286
+ function listValue$20(value) {
1275
1287
  return Array.isArray(value) ? value : [];
1276
1288
  }
1277
1289
  function optionalString$3(value) {
@@ -1300,7 +1312,7 @@ function joinUrlWithQuery(baseUrl, path, query) {
1300
1312
  }
1301
1313
  function arkoProfileFromData(data) {
1302
1314
  const displayName = stringValue$1(data.display_name).trim();
1303
- const version = numberValue$19(data.version);
1315
+ const version = numberValue$21(data.version);
1304
1316
  if (displayName === "" || !Number.isSafeInteger(version) || version < 0) throw new ArkmePluginError("arko-profile-contract-invalid", "Arko 资料响应不完整", true, 502);
1305
1317
  return {
1306
1318
  displayName,
@@ -1324,15 +1336,15 @@ function arkoModelOptionFromData(value) {
1324
1336
  displayName,
1325
1337
  provider,
1326
1338
  description,
1327
- recommended: booleanValue$10(data.recommended),
1328
- selected: booleanValue$10(data.selected)
1339
+ recommended: booleanValue$12(data.recommended),
1340
+ selected: booleanValue$12(data.selected)
1329
1341
  };
1330
1342
  }
1331
1343
  function arkoModelCatalogFromData(data) {
1332
1344
  const defaultRouteKey = arkoModelRouteKey(data.default_route_key);
1333
1345
  const effectiveRouteKey = arkoModelRouteKey(data.effective_route_key);
1334
1346
  const selectionSource = stringValue$1(data.selection_source);
1335
- const options = listValue$19(data.items).map(arkoModelOptionFromData);
1347
+ const options = listValue$20(data.items).map(arkoModelOptionFromData);
1336
1348
  const routeKeys = new Set(options.map((option) => option.routeKey));
1337
1349
  const selected = options.filter((option) => option.selected);
1338
1350
  if (selectionSource !== "default" && selectionSource !== "personal" || options.length === 0 || options.length > 16 || routeKeys.size !== options.length || !routeKeys.has(defaultRouteKey) || !routeKeys.has(effectiveRouteKey) || selected.length !== 1 || selected[0]?.routeKey !== effectiveRouteKey) throw new ArkmePluginError("arko-model-contract-invalid", "Arko 模型目录响应无效", true, 502);
@@ -1353,12 +1365,12 @@ function parsedJsonRecord(value) {
1353
1365
  }
1354
1366
  function arkoHistoryItemFromData(value) {
1355
1367
  const data = objectValue$1(value);
1356
- const messageId = numberValue$19(data.id ?? data.msg_id ?? data.msgId);
1357
- const sessionId = numberValue$19(data.session_id ?? data.sessionId);
1358
- const roleCode = numberValue$19(data.role ?? data.sender_role ?? data.senderRole);
1368
+ const messageId = numberValue$21(data.id ?? data.msg_id ?? data.msgId);
1369
+ const sessionId = numberValue$21(data.session_id ?? data.sessionId);
1370
+ const roleCode = numberValue$21(data.role ?? data.sender_role ?? data.senderRole);
1359
1371
  if (!Number.isSafeInteger(messageId) || messageId <= 0 || !Number.isSafeInteger(sessionId) || sessionId <= 0 || roleCode !== 2 && roleCode !== 3) return void 0;
1360
1372
  const extra = parsedJsonRecord(data.extra ?? data.metadata ?? data.meta);
1361
- const createdAt = numberValue$19(data.created_at ?? data.create_at ?? data.createdAt ?? data.createAt);
1373
+ const createdAt = numberValue$21(data.created_at ?? data.create_at ?? data.createdAt ?? data.createAt);
1362
1374
  const runUid = optionalString$3(extra.agent_run_uid);
1363
1375
  const runStatus = optionalString$3(extra.agent_run_status);
1364
1376
  const retryable = optionalBooleanValue$1(extra.agent_run_retryable);
@@ -1371,7 +1383,7 @@ function arkoHistoryItemFromData(value) {
1371
1383
  text: stringValue$1(data.content ?? data.text_content ?? data.textContent),
1372
1384
  reasoning: stringValue$1(data.reason_content ?? data.reason_text ?? data.reasonText),
1373
1385
  createdAtMillis: createdAt > 0 && createdAt < 1e11 ? createdAt * 1e3 : createdAt,
1374
- status: numberValue$19(data.status ?? data.msg_status),
1386
+ status: numberValue$21(data.status ?? data.msg_status),
1375
1387
  ...runUid === void 0 ? {} : { runUid },
1376
1388
  ...runStatus === void 0 ? {} : { runStatus },
1377
1389
  ...retryable === void 0 ? {} : { retryable },
@@ -1390,14 +1402,14 @@ function arkoRunProjectionFromData(data) {
1390
1402
  return {
1391
1403
  runUid,
1392
1404
  status,
1393
- retryable: booleanValue$10(data.agent_run_retryable ?? data.retryable),
1405
+ retryable: booleanValue$12(data.agent_run_retryable ?? data.retryable),
1394
1406
  ...errorCode === "" ? {} : { errorCode },
1395
1407
  ...retryOfRunUid === "" ? {} : { retryOfRunUid },
1396
1408
  ...clientAction === void 0 ? {} : { clientAction }
1397
1409
  };
1398
1410
  }
1399
1411
  function arkoCreatedRecordUids(value) {
1400
- return listValue$19(value).map((item) => stringValue$1(item).trim()).filter((item) => item !== "");
1412
+ return listValue$20(value).map((item) => stringValue$1(item).trim()).filter((item) => item !== "");
1401
1413
  }
1402
1414
  function arkoStreamFrame(raw) {
1403
1415
  try {
@@ -1426,7 +1438,7 @@ var ArkoService = class {
1426
1438
  async arkoEnsureSession(signal) {
1427
1439
  const session = await this.runtime.requireSession();
1428
1440
  const latest = await this.runtime.authenticatedIntelligentPost("/api/v1/qa/latest-session", { type: ARKO_AGENT_DIRECT_SESSION_TYPE }, session, signal);
1429
- const latestSessionId = numberValue$19(latest.id);
1441
+ const latestSessionId = numberValue$21(latest.id);
1430
1442
  if (Number.isSafeInteger(latestSessionId) && latestSessionId > 0) return {
1431
1443
  sessionId: latestSessionId,
1432
1444
  created: false,
@@ -1436,7 +1448,7 @@ var ArkoService = class {
1436
1448
  }
1437
1449
  async arkoCreateSession(signal) {
1438
1450
  const session = await this.runtime.requireSession();
1439
- const createdSessionId = numberValue$19((await this.runtime.authenticatedIntelligentPost("/api/v1/qa/new-session", {
1451
+ const createdSessionId = numberValue$21((await this.runtime.authenticatedIntelligentPost("/api/v1/qa/new-session", {
1440
1452
  name: ARKO_DEFAULT_SESSION_NAME,
1441
1453
  type: ARKO_AGENT_DIRECT_SESSION_TYPE
1442
1454
  }, session, signal)).session_id);
@@ -1461,7 +1473,7 @@ var ArkoService = class {
1461
1473
  const normalizedLimit = Math.min(ARKO_HISTORY_PAGE_LIMIT, Math.max(1, Math.trunc(limit)));
1462
1474
  const normalizedOffset = Math.max(0, Math.trunc(offset));
1463
1475
  const session = await this.runtime.requireSession();
1464
- const rawItems = listValue$19((await this.runtime.authenticatedIntelligentPost("/api/v1/qa/message-list", {
1476
+ const rawItems = listValue$20((await this.runtime.authenticatedIntelligentPost("/api/v1/qa/message-list", {
1465
1477
  limit: normalizedLimit,
1466
1478
  offset: normalizedOffset,
1467
1479
  session_type: ARKO_AGENT_DIRECT_SESSION_TYPE
@@ -1504,9 +1516,9 @@ var ArkoService = class {
1504
1516
  };
1505
1517
  if (modelRouteKey !== "") body.model_route_key = modelRouteKey;
1506
1518
  const accepted = await this.runtime.authenticatedIntelligentPost("/api/v1/qa/new-msg-v2", body, session, options.signal);
1507
- const userMsgId = numberValue$19(accepted.user_msg_id);
1508
- const assistantMsgId = numberValue$19(accepted.assistant_msg_id);
1509
- const acceptedSessionId = numberValue$19(accepted.session_id) || sessionId;
1519
+ const userMsgId = numberValue$21(accepted.user_msg_id);
1520
+ const assistantMsgId = numberValue$21(accepted.assistant_msg_id);
1521
+ const acceptedSessionId = numberValue$21(accepted.session_id) || sessionId;
1510
1522
  if (!Number.isSafeInteger(acceptedSessionId) || acceptedSessionId <= 0 || !Number.isSafeInteger(userMsgId) || userMsgId <= 0 || !Number.isSafeInteger(assistantMsgId) || assistantMsgId <= 0) throw new ArkmePluginError("arko-ask-contract-invalid", "Arko 消息响应不完整", true, 502);
1511
1523
  const runUid = stringValue$1(accepted.run_uid).trim();
1512
1524
  if (runUid === "") throw new ArkmePluginError("arko-ask-contract-invalid", "Arko 消息响应缺少运行标识", true, 502);
@@ -1542,8 +1554,8 @@ var ArkoService = class {
1542
1554
  }, session, signal);
1543
1555
  const status = stringValue$1(data.status).trim();
1544
1556
  const normalizedRunUid = stringValue$1(data.run_uid).trim() || runUid.trim();
1545
- const sequence = numberValue$19(data.sequence);
1546
- const surfaceAssistantMsgId = numberValue$19(data.surface_assistant_msg_id);
1557
+ const sequence = numberValue$21(data.sequence);
1558
+ const surfaceAssistantMsgId = numberValue$21(data.surface_assistant_msg_id);
1547
1559
  const clientAction = optionalRecord(data.client_action);
1548
1560
  if (normalizedRunUid !== runUid.trim() || !ARKO_RUN_STATUSES.has(status) || !Number.isSafeInteger(sequence) || sequence < 0 || !Number.isSafeInteger(surfaceAssistantMsgId) || surfaceAssistantMsgId <= 0) throw new ArkmePluginError("arko-run-status-contract-invalid", "Arko 运行状态响应不完整", true, 502);
1549
1561
  return {
@@ -1552,7 +1564,7 @@ var ArkoService = class {
1552
1564
  status,
1553
1565
  sequence,
1554
1566
  surfaceAssistantMsgId,
1555
- retryable: booleanValue$10(data.retryable),
1567
+ retryable: booleanValue$12(data.retryable),
1556
1568
  ...stringValue$1(data.error_code).trim() === "" ? {} : { errorCode: stringValue$1(data.error_code).trim() },
1557
1569
  ...stringValue$1(data.retry_of_run_uid).trim() === "" ? {} : { retryOfRunUid: stringValue$1(data.retry_of_run_uid).trim() },
1558
1570
  ...clientAction === void 0 ? {} : { clientAction }
@@ -1727,12 +1739,12 @@ var ArkoService = class {
1727
1739
  if (tailRun !== void 0) {
1728
1740
  run = tailRun;
1729
1741
  status = tailRun.status;
1730
- } else if (booleanValue$10(tail.error) || stringValue$1(tail.status).trim() === "error") status = "failed";
1742
+ } else if (booleanValue$12(tail.error) || stringValue$1(tail.status).trim() === "error") status = "failed";
1731
1743
  else status = ARKO_COMPLETED_STATUS;
1732
1744
  createdRecordUids = arkoCreatedRecordUids(tail.created_record_uids);
1733
1745
  const nextProfile = optionalRecord(tail.agent_profile);
1734
1746
  if (nextProfile !== void 0) profile = arkoProfileFromData(nextProfile);
1735
- if (booleanValue$10(tail.error) || stringValue$1(tail.status).trim() === "error") errorMessage = stringValue$1(tail.error_message).trim() || "Arko 执行失败";
1747
+ if (booleanValue$12(tail.error) || stringValue$1(tail.status).trim() === "error") errorMessage = stringValue$1(tail.error_message).trim() || "Arko 执行失败";
1736
1748
  }
1737
1749
  const type = stringValue$1(frame.type ?? frame.event).trim();
1738
1750
  const payload = optionalRecord(frame.payload) ?? frame;
@@ -1781,13 +1793,13 @@ const ARKME_ARRANGEMENT_REF_TTL_MILLIS = 900 * 1e3;
1781
1793
  const MAX_ARKME_ARRANGEMENT_REFS = 4096;
1782
1794
  const ARKME_ARRANGEMENT_REMINDER_REF_TTL_MILLIS = 900 * 1e3;
1783
1795
  const MAX_ARKME_ARRANGEMENT_REMINDER_REFS = 4096;
1784
- function numberValue$18(value) {
1796
+ function numberValue$20(value) {
1785
1797
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1786
1798
  }
1787
- function booleanValue$9(value) {
1799
+ function booleanValue$11(value) {
1788
1800
  return value === true;
1789
1801
  }
1790
- function listValue$18(value) {
1802
+ function listValue$19(value) {
1791
1803
  return Array.isArray(value) ? value : [];
1792
1804
  }
1793
1805
  function arrangementStatusCode(status) {
@@ -1797,7 +1809,7 @@ function arrangementStatusCode(status) {
1797
1809
  return -1;
1798
1810
  }
1799
1811
  function arrangementStatus(value) {
1800
- const code = Math.trunc(numberValue$18(value));
1812
+ const code = Math.trunc(numberValue$20(value));
1801
1813
  if (code === 1) return "identified";
1802
1814
  if (code === 2) return "following";
1803
1815
  if (code === 3) return "completed";
@@ -1846,9 +1858,9 @@ var ArrangementService = class {
1846
1858
  limit,
1847
1859
  offset
1848
1860
  }, session, options.signal, { lane: "interactive-read" });
1849
- const rawItems = listValue$18(data.list);
1861
+ const rawItems = listValue$19(data.list);
1850
1862
  const items = await Promise.all(rawItems.map(async (raw) => await this.arrangementItem(raw, session.userId)));
1851
- const total = Math.max(0, Math.trunc(numberValue$18(data.total)));
1863
+ const total = Math.max(0, Math.trunc(numberValue$20(data.total)));
1852
1864
  const nextOffset = offset + rawItems.length;
1853
1865
  const hasMore = rawItems.length > 0 && nextOffset < total;
1854
1866
  return {
@@ -1872,9 +1884,9 @@ var ArrangementService = class {
1872
1884
  offset,
1873
1885
  unread_only: options.unreadOnly === true
1874
1886
  }, session, options.signal, { lane: "interactive-read" });
1875
- const rawItems = listValue$18(data.list);
1887
+ const rawItems = listValue$19(data.list);
1876
1888
  const items = await Promise.all(rawItems.map(async (raw) => await this.arrangementReminderEvent(raw, session.userId)));
1877
- const total = Math.max(0, Math.trunc(numberValue$18(data.total)));
1889
+ const total = Math.max(0, Math.trunc(numberValue$20(data.total)));
1878
1890
  const nextOffset = offset + rawItems.length;
1879
1891
  const hasMore = rawItems.length > 0 && nextOffset < total;
1880
1892
  return {
@@ -1897,7 +1909,7 @@ var ArrangementService = class {
1897
1909
  project(data.next_reminder)
1898
1910
  ]);
1899
1911
  return {
1900
- unreadCount: Math.max(0, Math.trunc(numberValue$18(data.unread_count))),
1912
+ unreadCount: Math.max(0, Math.trunc(numberValue$20(data.unread_count))),
1901
1913
  ...latestUnread === void 0 ? {} : { latestUnread },
1902
1914
  ...latestEvent === void 0 ? {} : { latestEvent },
1903
1915
  ...nextReminder === void 0 ? {} : { nextReminder }
@@ -2001,7 +2013,7 @@ var ArrangementService = class {
2001
2013
  }, session, signal);
2002
2014
  return {
2003
2015
  outcome: "confirmed",
2004
- updatedCount: Math.max(0, Math.trunc(numberValue$18(data.updated_count)))
2016
+ updatedCount: Math.max(0, Math.trunc(numberValue$20(data.updated_count)))
2005
2017
  };
2006
2018
  } catch (error) {
2007
2019
  if (!isAmbiguousArrangementWriteError(error)) throw error;
@@ -2019,7 +2031,7 @@ var ArrangementService = class {
2019
2031
  }, session, signal);
2020
2032
  return {
2021
2033
  outcome: "confirmed",
2022
- updatedCount: Math.max(0, Math.trunc(numberValue$18(data.updated_count)))
2034
+ updatedCount: Math.max(0, Math.trunc(numberValue$20(data.updated_count)))
2023
2035
  };
2024
2036
  } catch (error) {
2025
2037
  if (!isAmbiguousArrangementWriteError(error)) throw error;
@@ -2034,7 +2046,7 @@ var ArrangementService = class {
2034
2046
  const data = await this.runtime.authenticatedIntelligentPost("/api/v1/arrangements/reminders/clear", {}, session, signal);
2035
2047
  return {
2036
2048
  outcome: "confirmed",
2037
- updatedCount: Math.max(0, Math.trunc(numberValue$18(data.updated_count)))
2049
+ updatedCount: Math.max(0, Math.trunc(numberValue$20(data.updated_count)))
2038
2050
  };
2039
2051
  } catch (error) {
2040
2052
  if (!isAmbiguousArrangementWriteError(error)) throw error;
@@ -2067,7 +2079,7 @@ var ArrangementService = class {
2067
2079
  }
2068
2080
  async reconcileArrangementDelete(arrangementRef, arrangementUid, session) {
2069
2081
  try {
2070
- const visible = listValue$18((await this.runtime.authenticatedIntelligentPost("/api/v1/arrangements/list", {
2082
+ const visible = listValue$19((await this.runtime.authenticatedIntelligentPost("/api/v1/arrangements/list", {
2071
2083
  status: -1,
2072
2084
  uids: [arrangementUid],
2073
2085
  limit: 1,
@@ -2098,17 +2110,17 @@ var ArrangementService = class {
2098
2110
  const item = objectValue$1(raw);
2099
2111
  const arrangementUid = stringValue$1(item.uid).trim();
2100
2112
  if (arrangementUid === "") throw new ArkmePluginError("arrangement-contract-invalid", "安排响应缺少业务身份", true, 502);
2101
- const dueAtMillis = Math.trunc(numberValue$18(item.due_at));
2102
- const remindAtMillis = Math.trunc(numberValue$18(item.remind_at));
2113
+ const dueAtMillis = Math.trunc(numberValue$20(item.due_at));
2114
+ const remindAtMillis = Math.trunc(numberValue$20(item.remind_at));
2103
2115
  return {
2104
2116
  arrangementRef: await this.arrangementRef(viewerUserId, arrangementUid),
2105
2117
  title: stringValue$1(item.title),
2106
2118
  description: stringValue$1(item.description),
2107
2119
  status: arrangementStatus(item.status),
2108
- reminderEnabled: booleanValue$9(item.reminder_enabled),
2120
+ reminderEnabled: booleanValue$11(item.reminder_enabled),
2109
2121
  reminderState: stringValue$1(item.reminder_state),
2110
- createdAtMillis: Math.trunc(numberValue$18(item.create_at)),
2111
- updatedAtMillis: Math.trunc(numberValue$18(item.update_at)),
2122
+ createdAtMillis: Math.trunc(numberValue$20(item.create_at)),
2123
+ updatedAtMillis: Math.trunc(numberValue$20(item.update_at)),
2112
2124
  ...dueAtMillis > 0 ? { dueAtMillis } : {},
2113
2125
  ...remindAtMillis > 0 ? { remindAtMillis } : {}
2114
2126
  };
@@ -2129,20 +2141,20 @@ var ArrangementService = class {
2129
2141
  const eventUid = stringValue$1(item.uid).trim();
2130
2142
  const arrangementUid = stringValue$1(item.arrangement_uid).trim();
2131
2143
  if (eventUid === "" || arrangementUid === "") throw new ArkmePluginError("arrangement-reminder-contract-invalid", "安排提醒响应缺少有效身份", true, 502);
2132
- const dueAtMillis = Math.trunc(numberValue$18(item.due_at));
2133
- const remindAtMillis = Math.trunc(numberValue$18(item.remind_at));
2134
- const readAtMillis = Math.trunc(numberValue$18(item.read_at));
2144
+ const dueAtMillis = Math.trunc(numberValue$20(item.due_at));
2145
+ const remindAtMillis = Math.trunc(numberValue$20(item.remind_at));
2146
+ const readAtMillis = Math.trunc(numberValue$20(item.read_at));
2135
2147
  return {
2136
2148
  eventRef: await this.arrangementReminderRef(viewerUserId, eventUid),
2137
2149
  arrangementRef: await this.arrangementRef(viewerUserId, arrangementUid),
2138
2150
  title: stringValue$1(item.title),
2139
2151
  description: stringValue$1(item.description),
2140
2152
  eventKind: stringValue$1(item.event_kind),
2141
- eventAtMillis: Math.trunc(numberValue$18(item.event_at)),
2153
+ eventAtMillis: Math.trunc(numberValue$20(item.event_at)),
2142
2154
  read: readAtMillis > 0,
2143
2155
  reminderState: stringValue$1(item.reminder_state),
2144
- createdAtMillis: Math.trunc(numberValue$18(item.create_at)),
2145
- updatedAtMillis: Math.trunc(numberValue$18(item.update_at)),
2156
+ createdAtMillis: Math.trunc(numberValue$20(item.create_at)),
2157
+ updatedAtMillis: Math.trunc(numberValue$20(item.update_at)),
2146
2158
  ...dueAtMillis > 0 ? { dueAtMillis } : {},
2147
2159
  ...remindAtMillis > 0 ? { remindAtMillis } : {},
2148
2160
  ...readAtMillis > 0 ? { readAtMillis } : {}
@@ -2199,7 +2211,7 @@ var ArrangementService = class {
2199
2211
  const ARKME_PHONE_BIND_SUCCESS = 1;
2200
2212
  const ARKME_PHONE_BIND_REPEAT = 2;
2201
2213
  const ARKME_PHONE_BIND_CODE_ERR = 3;
2202
- function numberValue$17(value) {
2214
+ function numberValue$19(value) {
2203
2215
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
2204
2216
  }
2205
2217
  var AuthService = class {
@@ -2264,7 +2276,7 @@ var AuthService = class {
2264
2276
  const data = await this.runtime.post(this.runtime.config.authBaseUrl, "/api/public/v1/auth/wechat-login-qrcode", {}, void 0, [200]);
2265
2277
  const sceneStr = stringValue$1(data.scene_str).trim();
2266
2278
  const qrContent = stringValue$1(data.url).trim();
2267
- const expireSeconds = Math.max(30, numberValue$17(data.expire_seconds) || 300);
2279
+ const expireSeconds = Math.max(30, numberValue$19(data.expire_seconds) || 300);
2268
2280
  if (qrContent === "" && sceneStr !== "") throw new ArkmePluginError("wechat-qr-unavailable", "测试环境当前未返回可用微信二维码,请使用手机号登录", true, 503);
2269
2281
  if (sceneStr === "" || qrContent === "") throw new ArkmePluginError("login-contract-invalid", "Arkme 登录二维码响应不完整", true, 502);
2270
2282
  const attemptId = crypto.randomUUID();
@@ -2300,7 +2312,7 @@ var AuthService = class {
2300
2312
  ref: 0,
2301
2313
  keep_cancel: true
2302
2314
  }, void 0, [200]);
2303
- const userId = numberValue$17(data.user_id);
2315
+ const userId = numberValue$19(data.user_id);
2304
2316
  if (userId <= 0) return {
2305
2317
  status: "pending",
2306
2318
  environment: this.runtime.config.environment,
@@ -2344,7 +2356,7 @@ var AuthService = class {
2344
2356
  const normalizedCaptcha = this.normalizedCaptcha(captcha);
2345
2357
  const session = await this.runtime.sessionStore.read() ?? await this.runtime.readPendingBindingSession();
2346
2358
  if (session !== void 0) {
2347
- if (numberValue$17((await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/bind-phone-send-code", {
2359
+ if (numberValue$19((await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/bind-phone-send-code", {
2348
2360
  phone: normalizedPhone,
2349
2361
  pre: "86",
2350
2362
  is_test: this.runtime.config.environment === "test",
@@ -2366,7 +2378,7 @@ var AuthService = class {
2366
2378
  if (!/^[0-9]{6}$/.test(normalizedCode)) throw new ArkmePluginError("phone-code-invalid", "请输入有效的短信验证码", false);
2367
2379
  const session = await this.runtime.sessionStore.read() ?? await this.runtime.readPendingBindingSession();
2368
2380
  if (session !== void 0) {
2369
- const result = numberValue$17((await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/verify-bind-phone", {
2381
+ const result = numberValue$19((await this.runtime.post(this.runtime.config.authBaseUrl, "/api/v1/auth/verify-bind-phone", {
2370
2382
  phone: normalizedPhone,
2371
2383
  pre: "86",
2372
2384
  code: normalizedCode,
@@ -2387,7 +2399,7 @@ var AuthService = class {
2387
2399
  keep_cancel: true
2388
2400
  }, void 0, [200]);
2389
2401
  if (data.ok === false) throw new ArkmePluginError("phone-code-rejected", "手机号或验证码错误", false, 401);
2390
- const userId = numberValue$17(data.user_id);
2402
+ const userId = numberValue$19(data.user_id);
2391
2403
  const accessToken = stringValue$1(data.access_token);
2392
2404
  const refreshToken = stringValue$1(data.refresh_token);
2393
2405
  if (userId <= 0 || accessToken === "" || refreshToken === "") throw new ArkmePluginError("login-contract-invalid", "Arkme手机号登录响应不完整", false, 502);
@@ -2454,19 +2466,19 @@ var SecretValue = class {
2454
2466
  };
2455
2467
  //#endregion
2456
2468
  //#region src/services/bot-service.ts
2457
- function numberValue$16(value) {
2469
+ function numberValue$18(value) {
2458
2470
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
2459
2471
  }
2460
- function booleanValue$8(value) {
2472
+ function booleanValue$10(value) {
2461
2473
  return value === true;
2462
2474
  }
2463
- function listValue$17(value) {
2475
+ function listValue$18(value) {
2464
2476
  return Array.isArray(value) ? value : [];
2465
2477
  }
2466
- function encodeOpaqueJson$5(value) {
2478
+ function encodeOpaqueJson$6(value) {
2467
2479
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
2468
2480
  }
2469
- function decodeOpaqueJson$5(value) {
2481
+ function decodeOpaqueJson$6(value) {
2470
2482
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
2471
2483
  }
2472
2484
  var BotService = class {
@@ -2501,7 +2513,7 @@ var BotService = class {
2501
2513
  const session = await this.runtime.requireSession();
2502
2514
  const data = await this.runtime.authenticatedBotPost("/api/v1/bot/list", {}, session, options.signal);
2503
2515
  const items = [];
2504
- for (const value of listValue$17(data.bots)) {
2516
+ for (const value of listValue$18(data.bots)) {
2505
2517
  const raw = objectValue$1(value);
2506
2518
  const provider = stringValue$1(raw.provider).trim();
2507
2519
  if (provider !== "openclaw" && provider !== "webhook") continue;
@@ -2540,6 +2552,10 @@ var BotService = class {
2540
2552
  throw new ArkmePluginError("bot-create-outcome-unknown", "Bot 可能已创建,但响应不完整;请刷新 Bot 列表确认", false, 409, { cause: error });
2541
2553
  }
2542
2554
  }
2555
+ /** Browser-safe result: the one-time credential never leaves the Host owner. */
2556
+ async createBotSummary(input, options = {}) {
2557
+ return (await this.createBot(input, options)).bot;
2558
+ }
2543
2559
  async revealBotSecret(botRef, options = {}) {
2544
2560
  const session = await this.runtime.requireSession();
2545
2561
  const reference = await this.openBotRef(botRef, session.userId);
@@ -2588,19 +2604,19 @@ var BotService = class {
2588
2604
  const group = await this.openGroupSourceRef(groupSourceRef, session.userId);
2589
2605
  const data = await this.runtime.authenticatedBotPost("/api/v1/bot/group/list", { subject_uid: group.ownerRef }, session, options.signal);
2590
2606
  const items = [];
2591
- for (const value of listValue$17(data.bots)) {
2607
+ for (const value of listValue$18(data.bots)) {
2592
2608
  const raw = objectValue$1(value);
2593
2609
  if (stringValue$1(raw.provider).trim() !== "openclaw") continue;
2594
2610
  const { directChatAvailable: _directChatAvailable, ...summary } = await this.botSummaryFromData(raw, session.userId);
2595
2611
  items.push({
2596
2612
  ...summary,
2597
- installed: booleanValue$8(raw.installed)
2613
+ installed: booleanValue$10(raw.installed)
2598
2614
  });
2599
2615
  }
2600
2616
  return {
2601
2617
  groupSourceRef,
2602
2618
  displayName: stringValue$1(data.subject_title).trim() || group.displayName,
2603
- canAddBots: booleanValue$8(data.can_current_user_add_bots),
2619
+ canAddBots: booleanValue$10(data.can_current_user_add_bots),
2604
2620
  items
2605
2621
  };
2606
2622
  }
@@ -2653,7 +2669,7 @@ var BotService = class {
2653
2669
  };
2654
2670
  }
2655
2671
  async sealBotRef(userId, botId) {
2656
- const payload = encodeOpaqueJson$5({
2672
+ const payload = encodeOpaqueJson$6({
2657
2673
  version: 1,
2658
2674
  userId,
2659
2675
  botId
@@ -2669,13 +2685,13 @@ var BotService = class {
2669
2685
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
2670
2686
  let raw;
2671
2687
  try {
2672
- raw = objectValue$1(decodeOpaqueJson$5(payload));
2688
+ raw = objectValue$1(decodeOpaqueJson$6(payload));
2673
2689
  } catch (error) {
2674
2690
  throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false, 400, { cause: error });
2675
2691
  }
2676
2692
  const reference = {
2677
2693
  version: 1,
2678
- userId: numberValue$16(raw.userId),
2694
+ userId: numberValue$18(raw.userId),
2679
2695
  botId: stringValue$1(raw.botId).trim()
2680
2696
  };
2681
2697
  if (raw.version !== 1 || reference.userId !== expectedUserId || reference.botId === "") throw new ArkmePluginError("bot-ref-invalid", "Bot 引用与当前账号不匹配", false, 403);
@@ -2683,6 +2699,181 @@ var BotService = class {
2683
2699
  }
2684
2700
  };
2685
2701
  //#endregion
2702
+ //#region src/services/calendar-service.ts
2703
+ const MAX_CALENDAR_RANGE_DAYS = 62;
2704
+ const MAX_DAY_RECORD_LIMIT = 50;
2705
+ function numberValue$17(value) {
2706
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2707
+ }
2708
+ function booleanValue$9(value) {
2709
+ if (typeof value === "boolean") return value;
2710
+ if (typeof value === "number") return value !== 0;
2711
+ }
2712
+ function listValue$17(value) {
2713
+ return Array.isArray(value) ? value : [];
2714
+ }
2715
+ function readCalendarDate(value, field) {
2716
+ const normalized = value.trim();
2717
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalized);
2718
+ if (match === null) throw new ArkmePluginError("calendar-date-invalid", `${field} 必须是 YYYY-MM-DD`, false);
2719
+ const year = Number(match[1]);
2720
+ const month = Number(match[2]);
2721
+ const day = Number(match[3]);
2722
+ const date = new Date(Date.UTC(year, month - 1, day));
2723
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) throw new ArkmePluginError("calendar-date-invalid", `${field} 必须是真实日期`, false);
2724
+ return normalized;
2725
+ }
2726
+ function calendarDayNumber(date) {
2727
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
2728
+ if (match === null) throw new ArkmePluginError("calendar-date-invalid", "日期格式无效", false);
2729
+ const year = Number(match[1]);
2730
+ const month = Number(match[2]);
2731
+ const day = Number(match[3]);
2732
+ return Math.trunc(Date.UTC(year, month - 1, day) / 864e5);
2733
+ }
2734
+ function readTimezone(value) {
2735
+ const normalized = value?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
2736
+ if (normalized.length > 80) throw new ArkmePluginError("calendar-timezone-invalid", "timezone 无效", false);
2737
+ return normalized;
2738
+ }
2739
+ function boundedLimit(value) {
2740
+ const limit = Math.trunc(value ?? 20);
2741
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_DAY_RECORD_LIMIT) throw new ArkmePluginError("calendar-limit-invalid", `limit 必须是 1-${MAX_DAY_RECORD_LIMIT} 的整数`, false);
2742
+ return limit;
2743
+ }
2744
+ function contentAccessState(value) {
2745
+ const code = Math.trunc(numberValue$17(value));
2746
+ if (code === 1) return "available";
2747
+ if (code === 2) return "protected";
2748
+ return "unknown";
2749
+ }
2750
+ function sourceKind(raw) {
2751
+ const topic = objectValue$1(raw.topic_core);
2752
+ const chat = objectValue$1(raw.chat_core);
2753
+ if (stringValue$1(topic.topic_uid).trim() !== "") return "topic";
2754
+ if (stringValue$1(chat.chat_session_uid).trim() !== "") return "chat";
2755
+ if (booleanValue$9(raw.is_uncategorized) === true) return "self";
2756
+ return "unknown";
2757
+ }
2758
+ var CalendarService = class {
2759
+ runtime;
2760
+ constructor(runtime) {
2761
+ this.runtime = runtime;
2762
+ }
2763
+ async bucketPage(options) {
2764
+ const startDate = readCalendarDate(options.startDate, "start_date");
2765
+ const endDate = readCalendarDate(options.endDate, "end_date");
2766
+ const startDay = calendarDayNumber(startDate);
2767
+ const endDay = calendarDayNumber(endDate);
2768
+ if (startDay > endDay) throw new ArkmePluginError("calendar-range-invalid", "start_date 不能晚于 end_date", false);
2769
+ if (endDay - startDay + 1 > MAX_CALENDAR_RANGE_DAYS) throw new ArkmePluginError("calendar-range-too-wide", `日历范围最多查询 ${MAX_CALENDAR_RANGE_DAYS} 天`, false);
2770
+ const timezone = readTimezone(options.timezone);
2771
+ const session = await this.runtime.requireSession();
2772
+ const data = await this.runtime.authenticatedCalendarPost("/api/v1/calendar/buckets/query", {
2773
+ bucket_scope_kind: 1,
2774
+ bucket_scope_uid: "",
2775
+ start_date: startDate,
2776
+ end_date: endDate,
2777
+ timezone
2778
+ }, session, options.signal, {
2779
+ key: `calendar:buckets:self:${startDate}:${endDate}:${timezone}`,
2780
+ cacheMs: 3e4,
2781
+ failureCooldownMs: 2e3
2782
+ });
2783
+ return {
2784
+ scope: "self",
2785
+ startDate,
2786
+ endDate,
2787
+ timezone: stringValue$1(data.timezone).trim() || timezone,
2788
+ refreshedAtMillis: Date.now(),
2789
+ days: listValue$17(data.daily_data).map((raw) => this.bucketDay(raw)).filter((day) => day !== void 0)
2790
+ };
2791
+ }
2792
+ async dayRecords(options) {
2793
+ const bucketDate = readCalendarDate(options.bucketDate, "bucket_date");
2794
+ const timezone = readTimezone(options.timezone);
2795
+ const limit = boundedLimit(options.limit);
2796
+ const cursorSendAt = Math.trunc(options.cursor?.sendAtMillis ?? 0);
2797
+ const cursorRecordUid = options.cursor?.recordUid.trim() ?? "";
2798
+ const session = await this.runtime.requireSession();
2799
+ const data = await this.runtime.authenticatedCalendarPost("/api/v1/calendar/records/query", {
2800
+ bucket_scope_kind: 1,
2801
+ bucket_scope_uid: "",
2802
+ bucket_date: bucketDate,
2803
+ timezone,
2804
+ limit,
2805
+ ...cursorSendAt > 0 && cursorRecordUid !== "" ? {
2806
+ cursor_send_at: cursorSendAt,
2807
+ cursor_record_uid: cursorRecordUid
2808
+ } : {}
2809
+ }, session, options.signal, {
2810
+ key: `calendar:records:self:${bucketDate}:${timezone}:${String(limit)}:${String(cursorSendAt)}:${cursorRecordUid}`,
2811
+ cacheMs: 1e4,
2812
+ failureCooldownMs: 2e3
2813
+ });
2814
+ const nextSendAt = Math.trunc(numberValue$17(data.next_cursor_send_at));
2815
+ const nextUid = stringValue$1(data.next_cursor_record_uid).trim();
2816
+ return {
2817
+ scope: "self",
2818
+ bucketDate,
2819
+ timezone: stringValue$1(data.timezone).trim() || timezone,
2820
+ refreshedAtMillis: Date.now(),
2821
+ items: listValue$17(data.items).map((raw) => this.dayRecord(raw)).filter((item) => item !== void 0),
2822
+ hasMore: data.has_more === true,
2823
+ ...nextSendAt > 0 && nextUid !== "" ? { nextCursor: {
2824
+ sendAtMillis: nextSendAt,
2825
+ recordUid: nextUid
2826
+ } } : {}
2827
+ };
2828
+ }
2829
+ bucketDay(raw) {
2830
+ const item = objectValue$1(raw);
2831
+ const bucketDate = stringValue$1(item.bucket_date).trim();
2832
+ if (bucketDate === "") return void 0;
2833
+ const count = Math.max(0, Math.trunc(numberValue$17(item.count)));
2834
+ return {
2835
+ bucketDate,
2836
+ count,
2837
+ protectedCount: Math.max(0, Math.trunc(numberValue$17(item.protected_count))),
2838
+ hasRecords: count > 0,
2839
+ ...numberValue$17(item.first_send_at) > 0 ? { firstSendAtMillis: Math.trunc(numberValue$17(item.first_send_at)) } : {}
2840
+ };
2841
+ }
2842
+ dayRecord(raw) {
2843
+ const item = objectValue$1(raw);
2844
+ const core = objectValue$1(item.record_core);
2845
+ const recordUid = stringValue$1(item.record_uid ?? core.record_uid).trim();
2846
+ const sendAtMillis = Math.trunc(numberValue$17(item.send_at ?? core.send_at));
2847
+ if (recordUid === "" || sendAtMillis <= 0) return void 0;
2848
+ const accessState = contentAccessState(core.content_access_state);
2849
+ const available = accessState === "available";
2850
+ const title = available ? clippedText(core.title, 500) : "";
2851
+ const textContent = available ? clippedText(core.text_content, 4e3) : "";
2852
+ const preview = title || clippedText(textContent, 160) || (available ? "无文字内容" : "受保护内容");
2853
+ const topic = objectValue$1(item.topic_core);
2854
+ const isUncategorized = booleanValue$9(item.is_uncategorized);
2855
+ const hasManualEdit = booleanValue$9(core.has_manual_edit);
2856
+ const hasPolish = booleanValue$9(core.has_polish);
2857
+ return {
2858
+ recordUid,
2859
+ sendAtMillis,
2860
+ accessState,
2861
+ title,
2862
+ textContent,
2863
+ preview,
2864
+ ...stringValue$1(topic.title).trim() === "" ? {} : { topicTitle: stringValue$1(topic.title).trim() },
2865
+ sourceKind: sourceKind(item),
2866
+ creationSource: Math.trunc(numberValue$17(core.creation_source)),
2867
+ templateKind: Math.trunc(numberValue$17(core.template_kind)),
2868
+ displayKind: Math.trunc(numberValue$17(core.display_kind)),
2869
+ protected: accessState === "protected",
2870
+ ...isUncategorized === void 0 ? {} : { isUncategorized },
2871
+ ...hasManualEdit === void 0 ? {} : { hasManualEdit },
2872
+ ...hasPolish === void 0 ? {} : { hasPolish }
2873
+ };
2874
+ }
2875
+ };
2876
+ //#endregion
2686
2877
  //#region src/chat-realtime.ts
2687
2878
  const ARKME_CHAT_SSE_PATH = "/api/v1/sse/chat/noty";
2688
2879
  const DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
@@ -2690,7 +2881,7 @@ const DEFAULT_INACTIVITY_TIMEOUT_MS = 75e3;
2690
2881
  const DEFAULT_LEASE_DURATION_MS = 57e4;
2691
2882
  const DEFAULT_IDLE_POLL_MS = 2e3;
2692
2883
  const DEFAULT_RETRY_BASE_MS = 1e3;
2693
- const DEFAULT_MAX_RETRY_MS = 6e4;
2884
+ const DEFAULT_MAX_RETRY_MS = 15e3;
2694
2885
  const DEFAULT_STABLE_CONNECTION_MS = 3e4;
2695
2886
  const MAX_SEEN_EVENTS = 256;
2696
2887
  var ArkmeChatRealtimeAuthError = class extends Error {};
@@ -2771,11 +2962,14 @@ var ArkmeChatRealtimeRuntime = class {
2771
2962
  now;
2772
2963
  rootController;
2773
2964
  connectionController;
2965
+ waitController;
2774
2966
  loop;
2775
2967
  revision = 0;
2776
2968
  connected = false;
2777
2969
  lastAcceptedAccessToken;
2778
2970
  lastConnectionLifetimeMs = 0;
2971
+ connectionGeneration = 0;
2972
+ forceReconnectRequested = false;
2779
2973
  lastEventAtMillis;
2780
2974
  seenEventUids = /* @__PURE__ */ new Set();
2781
2975
  listeners = /* @__PURE__ */ new Set();
@@ -2812,14 +3006,19 @@ var ArkmeChatRealtimeRuntime = class {
2812
3006
  stop() {
2813
3007
  this.rootController?.abort();
2814
3008
  this.connectionController?.abort();
3009
+ this.waitController?.abort();
2815
3010
  }
2816
3011
  reconnect() {
3012
+ if (this.loop === void 0) return;
3013
+ this.forceReconnectRequested = true;
2817
3014
  this.connectionController?.abort();
3015
+ this.waitController?.abort();
2818
3016
  }
2819
3017
  state() {
2820
3018
  return {
2821
3019
  revision: this.revision,
2822
3020
  connected: this.connected,
3021
+ connectionGeneration: this.connectionGeneration,
2823
3022
  ...this.lastEventAtMillis === void 0 ? {} : { lastEventAtMillis: this.lastEventAtMillis }
2824
3023
  };
2825
3024
  }
@@ -2841,16 +3040,17 @@ var ArkmeChatRealtimeRuntime = class {
2841
3040
  if (session === void 0) {
2842
3041
  this.connected = false;
2843
3042
  blockedAccessToken = void 0;
2844
- await waitFor(this.idlePollMs, signal);
3043
+ await this.wait(this.idlePollMs, signal);
2845
3044
  continue;
2846
3045
  }
2847
3046
  if (blockedAccessToken === session.accessToken) {
2848
3047
  this.connected = false;
2849
- await waitFor(this.idlePollMs, signal);
3048
+ await this.wait(this.idlePollMs, signal);
2850
3049
  continue;
2851
3050
  }
2852
3051
  blockedAccessToken = void 0;
2853
3052
  let stableConnection = false;
3053
+ this.options.diagnostic?.("reconnect_attempt", { connectionGeneration: this.connectionGeneration + 1 });
2854
3054
  try {
2855
3055
  await this.consume(session, signal);
2856
3056
  stableConnection = this.lastConnectionLifetimeMs >= this.stableConnectionMs;
@@ -2865,7 +3065,7 @@ var ArkmeChatRealtimeRuntime = class {
2865
3065
  if (refreshedButUnacceptedToken === session.accessToken) {
2866
3066
  blockedAccessToken = session.accessToken;
2867
3067
  refreshedButUnacceptedToken = void 0;
2868
- await waitFor(this.idlePollMs, signal);
3068
+ await this.wait(this.idlePollMs, signal);
2869
3069
  continue;
2870
3070
  }
2871
3071
  let refreshed;
@@ -2876,17 +3076,24 @@ var ArkmeChatRealtimeRuntime = class {
2876
3076
  }
2877
3077
  if (refreshed !== void 0 && refreshed.accessToken !== session.accessToken) {
2878
3078
  refreshedButUnacceptedToken = refreshed.accessToken;
3079
+ this.forceReconnectRequested = false;
2879
3080
  retryMs = this.retryBaseMs;
2880
3081
  continue;
2881
3082
  }
2882
3083
  blockedAccessToken = session.accessToken;
2883
- await waitFor(this.idlePollMs, signal);
3084
+ await this.wait(this.idlePollMs, signal);
2884
3085
  continue;
2885
3086
  }
2886
3087
  } finally {
3088
+ if (this.connected && !signal.aborted) this.options.diagnostic?.("sse_disconnected", { connectionGeneration: this.connectionGeneration });
2887
3089
  this.connected = false;
2888
3090
  }
2889
- await waitFor(Math.max(1, Math.round(retryMs * (.8 + this.random() * .4))), signal);
3091
+ if (this.forceReconnectRequested) {
3092
+ this.forceReconnectRequested = false;
3093
+ retryMs = this.retryBaseMs;
3094
+ continue;
3095
+ }
3096
+ await this.wait(this.jitteredDelay(retryMs), signal);
2890
3097
  if (!stableConnection) retryMs = Math.min(this.maxRetryMs, Math.max(this.retryBaseMs, retryMs * 2));
2891
3098
  }
2892
3099
  }
@@ -2922,6 +3129,7 @@ var ArkmeChatRealtimeRuntime = class {
2922
3129
  acceptedAtMillis = this.now();
2923
3130
  this.connected = true;
2924
3131
  this.lastAcceptedAccessToken = session.accessToken;
3132
+ this.connectionGeneration += 1;
2925
3133
  this.advanceRevision("reconcile");
2926
3134
  leaseTimer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("chat SSE lease rotation")), this.leaseDurationMs);
2927
3135
  const reader = response.body.getReader();
@@ -2946,6 +3154,25 @@ var ArkmeChatRealtimeRuntime = class {
2946
3154
  if (this.connectionController === controller) this.connectionController = void 0;
2947
3155
  }
2948
3156
  }
3157
+ jitteredDelay(milliseconds) {
3158
+ const random = Math.min(1, Math.max(0, this.random()));
3159
+ return Math.max(0, Math.round(milliseconds * (.8 + random * .4)));
3160
+ }
3161
+ async wait(milliseconds, rootSignal) {
3162
+ if (rootSignal.aborted) return;
3163
+ const controller = new AbortController();
3164
+ const abortFromRoot = () => {
3165
+ controller.abort(rootSignal.reason);
3166
+ };
3167
+ rootSignal.addEventListener("abort", abortFromRoot, { once: true });
3168
+ this.waitController = controller;
3169
+ try {
3170
+ await waitFor(milliseconds, controller.signal);
3171
+ } finally {
3172
+ rootSignal.removeEventListener("abort", abortFromRoot);
3173
+ if (this.waitController === controller) this.waitController = void 0;
3174
+ }
3175
+ }
2949
3176
  acceptLine(line) {
2950
3177
  const event = decodeArkmeChatReceiveDataLine(line);
2951
3178
  if (event === void 0 || this.seenEventUids.has(event.eventUid)) return;
@@ -2984,11 +3211,15 @@ var ChatRealtimeService = class {
2984
3211
  projectionReader;
2985
3212
  chatRealtime;
2986
3213
  chatClientListeners = /* @__PURE__ */ new Set();
2987
- pendingProjectionSequences = /* @__PURE__ */ new Map();
3214
+ pendingChatProjections = /* @__PURE__ */ new Map();
2988
3215
  projectionRetryCounts = /* @__PURE__ */ new Map();
2989
3216
  projectionTimer;
2990
3217
  projectionInFlight = false;
2991
3218
  projectionFailureCount = 0;
3219
+ notificationBaselineGeneration = 0;
3220
+ notificationBaselineSequences = /* @__PURE__ */ new Map();
3221
+ notificationBaselineRetryTimer;
3222
+ notificationBaselineRetryCount = 0;
2992
3223
  chatClientRevision = 0;
2993
3224
  constructor(runtime, source, projectionReader) {
2994
3225
  this.runtime = runtime;
@@ -3005,7 +3236,10 @@ var ChatRealtimeService = class {
3005
3236
  return;
3006
3237
  }
3007
3238
  },
3008
- fetchImpl: runtime.fetchImpl
3239
+ fetchImpl: runtime.fetchImpl,
3240
+ diagnostic: (event, details) => {
3241
+ console.info(`dsh-arkme: ${event}`, details);
3242
+ }
3009
3243
  });
3010
3244
  }
3011
3245
  reconnect() {
@@ -3013,9 +3247,13 @@ var ChatRealtimeService = class {
3013
3247
  }
3014
3248
  dispose() {
3015
3249
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
3250
+ if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
3016
3251
  this.projectionTimer = void 0;
3017
- this.pendingProjectionSequences.clear();
3252
+ this.notificationBaselineRetryTimer = void 0;
3253
+ this.pendingChatProjections.clear();
3018
3254
  this.projectionRetryCounts.clear();
3255
+ this.notificationBaselineSequences.clear();
3256
+ this.notificationBaselineGeneration = 0;
3019
3257
  this.chatClientListeners.clear();
3020
3258
  }
3021
3259
  startChatRealtime() {
@@ -3027,9 +3265,13 @@ var ChatRealtimeService = class {
3027
3265
  unsubscribe();
3028
3266
  stop();
3029
3267
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
3268
+ if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
3030
3269
  this.projectionTimer = void 0;
3031
- this.pendingProjectionSequences.clear();
3270
+ this.notificationBaselineRetryTimer = void 0;
3271
+ this.pendingChatProjections.clear();
3032
3272
  this.projectionRetryCounts.clear();
3273
+ this.notificationBaselineSequences.clear();
3274
+ this.notificationBaselineGeneration = 0;
3033
3275
  };
3034
3276
  }
3035
3277
  chatRealtimeState() {
@@ -3047,26 +3289,93 @@ var ChatRealtimeService = class {
3047
3289
  type: "reconcile",
3048
3290
  revision: this.chatClientRevision,
3049
3291
  connected: state.connected,
3292
+ connectionGeneration: state.connectionGeneration,
3050
3293
  refresh: "if-stale"
3051
3294
  };
3052
3295
  }
3053
3296
  handleChatRealtimeNotice(notice) {
3054
3297
  if (notice.cause === "reconcile") {
3298
+ const generation = notice.state.connectionGeneration;
3299
+ this.notificationBaselineGeneration = 0;
3300
+ this.notificationBaselineSequences.clear();
3301
+ this.notificationBaselineRetryCount = 0;
3302
+ if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
3303
+ this.notificationBaselineRetryTimer = void 0;
3055
3304
  this.emitChatClientEvent({
3056
3305
  type: "reconcile",
3057
3306
  revision: this.nextChatClientRevision(),
3058
3307
  connected: notice.state.connected,
3308
+ connectionGeneration: generation,
3059
3309
  refresh: "none"
3060
3310
  });
3311
+ this.reconcileChatNotificationBaseline(generation);
3061
3312
  return;
3062
3313
  }
3063
- if (notice.cause === "hint" && notice.hint !== void 0) this.scheduleChatSessionProjection(notice.hint.chatSessionUid, notice.hint.latestSequence);
3314
+ if (notice.cause === "hint" && notice.hint !== void 0) this.scheduleChatSessionProjection(notice.hint.chatSessionUid, notice.hint.latestSequence, {
3315
+ hint: notice.hint,
3316
+ connectionGeneration: notice.state.connectionGeneration,
3317
+ attempts: 0
3318
+ });
3064
3319
  }
3065
- scheduleChatSessionProjection(chatSessionUid, latestSequence) {
3320
+ async reconcileChatNotificationBaseline(connectionGeneration) {
3321
+ try {
3322
+ const session = await this.runtime.requireSession();
3323
+ const sequences = /* @__PURE__ */ new Map();
3324
+ let cursor;
3325
+ for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
3326
+ const page = await this.source.listSources("root", {
3327
+ limit: 50,
3328
+ refresh: true,
3329
+ ...cursor === void 0 ? {} : { cursor }
3330
+ });
3331
+ for (const item of page.items) {
3332
+ if (item.kind !== "private_chat" && item.kind !== "group_chat") continue;
3333
+ const source = await this.source.openSourceRef(item.sourceRef, session.userId);
3334
+ sequences.set(source.ownerRef, item.latestSequence ?? 0);
3335
+ }
3336
+ if (!page.hasMore || page.nextCursor === void 0) break;
3337
+ cursor = page.nextCursor;
3338
+ }
3339
+ const state = this.chatRealtime.state();
3340
+ if (!state.connected || state.connectionGeneration !== connectionGeneration) return;
3341
+ this.notificationBaselineSequences.clear();
3342
+ for (const [uid, sequence] of sequences) this.notificationBaselineSequences.set(uid, sequence);
3343
+ this.notificationBaselineGeneration = connectionGeneration;
3344
+ this.notificationBaselineRetryCount = 0;
3345
+ console.info("dsh-arkme: reconcile_completed", {
3346
+ connectionGeneration,
3347
+ sessionCount: sequences.size
3348
+ });
3349
+ if (this.pendingChatProjections.size > 0 && this.projectionTimer === void 0 && !this.projectionInFlight) this.projectionTimer = setTimeout(() => {
3350
+ this.projectionTimer = void 0;
3351
+ this.flushChatSessionProjections();
3352
+ }, 0);
3353
+ } catch (error) {
3354
+ const state = this.chatRealtime.state();
3355
+ if (!state.connected || state.connectionGeneration !== connectionGeneration) return;
3356
+ this.notificationBaselineRetryCount += 1;
3357
+ const delay = Math.min(15e3, 1e3 * 2 ** Math.min(4, this.notificationBaselineRetryCount - 1));
3358
+ console.warn("dsh-arkme: Chat reconnect reconciliation failed:", safeFailureMessage$6(error));
3359
+ this.notificationBaselineRetryTimer = setTimeout(() => {
3360
+ this.notificationBaselineRetryTimer = void 0;
3361
+ this.reconcileChatNotificationBaseline(connectionGeneration);
3362
+ }, delay);
3363
+ }
3364
+ }
3365
+ scheduleChatSessionProjection(chatSessionUid, latestSequence, notificationHint) {
3066
3366
  const uid = chatSessionUid.trim();
3067
3367
  if (uid === "") return;
3068
- if (latestSequence > (this.pendingProjectionSequences.get(uid) ?? 0)) this.projectionRetryCounts.delete(uid);
3069
- this.pendingProjectionSequences.set(uid, Math.max(latestSequence, this.pendingProjectionSequences.get(uid) ?? 0));
3368
+ const current = this.pendingChatProjections.get(uid);
3369
+ if (latestSequence > (current?.latestSequence ?? 0)) this.projectionRetryCounts.delete(uid);
3370
+ const notificationHints = current === void 0 ? [] : [...current.notificationHints];
3371
+ if (notificationHint !== void 0 && !notificationHints.some((item) => item.hint.eventUid === notificationHint.hint.eventUid)) {
3372
+ notificationHints.push(notificationHint);
3373
+ if (notificationHints.length > 256) notificationHints.splice(0, notificationHints.length - 256);
3374
+ }
3375
+ this.pendingChatProjections.set(uid, {
3376
+ latestSequence: Math.max(latestSequence, current?.latestSequence ?? 0),
3377
+ notificationHints
3378
+ });
3070
3379
  if (this.projectionTimer !== void 0 || this.projectionInFlight) return;
3071
3380
  this.projectionTimer = setTimeout(() => {
3072
3381
  this.projectionTimer = void 0;
@@ -3074,10 +3383,10 @@ var ChatRealtimeService = class {
3074
3383
  }, 200);
3075
3384
  }
3076
3385
  async flushChatSessionProjections() {
3077
- if (this.projectionInFlight || this.pendingProjectionSequences.size === 0) return;
3386
+ if (this.projectionInFlight || this.pendingChatProjections.size === 0) return;
3078
3387
  this.projectionInFlight = true;
3079
- const pending = [...this.pendingProjectionSequences.entries()].slice(0, 50);
3080
- for (const [uid] of pending) this.pendingProjectionSequences.delete(uid);
3388
+ const pending = [...this.pendingChatProjections.entries()].slice(0, 50);
3389
+ for (const [uid] of pending) this.pendingChatProjections.delete(uid);
3081
3390
  try {
3082
3391
  const failed = await this.refreshChatSessionProjectionBatch(pending);
3083
3392
  for (const [uid] of pending) if (!failed.some(([failedUid]) => failedUid === uid)) this.projectionRetryCounts.delete(uid);
@@ -3092,7 +3401,7 @@ var ChatRealtimeService = class {
3092
3401
  this.requeueProjectionFailures(pending);
3093
3402
  } finally {
3094
3403
  this.projectionInFlight = false;
3095
- if (this.pendingProjectionSequences.size > 0 && this.projectionTimer === void 0) {
3404
+ if (this.pendingChatProjections.size > 0 && this.projectionTimer === void 0) {
3096
3405
  const retryDelayBase = this.projectionFailureCount === 0 ? 200 : Math.min(5e3, 500 * 2 ** Math.min(3, this.projectionFailureCount - 1));
3097
3406
  const retryDelay = Math.max(100, Math.round(retryDelayBase * (.8 + Math.random() * .4)));
3098
3407
  this.projectionTimer = setTimeout(() => {
@@ -3103,7 +3412,7 @@ var ChatRealtimeService = class {
3103
3412
  }
3104
3413
  }
3105
3414
  requeueProjectionFailures(failed) {
3106
- for (const [uid, sequence] of failed) {
3415
+ for (const [uid, projection] of failed) {
3107
3416
  const retries = (this.projectionRetryCounts.get(uid) ?? 0) + 1;
3108
3417
  if (retries > MAX_PROJECTION_RETRIES) {
3109
3418
  this.projectionRetryCounts.delete(uid);
@@ -3111,8 +3420,24 @@ var ChatRealtimeService = class {
3111
3420
  continue;
3112
3421
  }
3113
3422
  this.projectionRetryCounts.set(uid, retries);
3114
- this.pendingProjectionSequences.set(uid, Math.max(sequence, this.pendingProjectionSequences.get(uid) ?? 0));
3423
+ this.mergePendingChatProjection(uid, projection);
3424
+ }
3425
+ }
3426
+ mergePendingChatProjection(uid, incoming) {
3427
+ const current = this.pendingChatProjections.get(uid);
3428
+ if (current === void 0) {
3429
+ this.pendingChatProjections.set(uid, {
3430
+ latestSequence: incoming.latestSequence,
3431
+ notificationHints: [...incoming.notificationHints]
3432
+ });
3433
+ return;
3115
3434
  }
3435
+ const byEventUid = new Map(current.notificationHints.map((item) => [item.hint.eventUid, item]));
3436
+ for (const item of incoming.notificationHints) byEventUid.set(item.hint.eventUid, item);
3437
+ this.pendingChatProjections.set(uid, {
3438
+ latestSequence: Math.max(current.latestSequence, incoming.latestSequence),
3439
+ notificationHints: [...byEventUid.values()].slice(-256)
3440
+ });
3116
3441
  }
3117
3442
  async refreshChatSessionProjectionBatch(pending) {
3118
3443
  const session = await this.runtime.requireSession();
@@ -3130,16 +3455,19 @@ var ChatRealtimeService = class {
3130
3455
  const failedUids = /* @__PURE__ */ new Set();
3131
3456
  for (let offset = 0; offset < pending.length; offset += 3) {
3132
3457
  const chunk = pending.slice(offset, offset + 3);
3133
- (await Promise.allSettled(chunk.map(async ([uid, hintedSequence]) => {
3458
+ (await Promise.allSettled(chunk.map(async ([uid, projection]) => {
3134
3459
  const cached = this.source.cachedChatSourceByKey(`${String(session.userId)}:${uid}`);
3135
- const afterSequence = Math.max(0, cached?.latestSequence ?? hintedSequence - 1);
3460
+ const firstHintSequence = projection.notificationHints.reduce((minimum, item) => Math.min(minimum, item.hint.latestSequence), projection.latestSequence);
3461
+ const requiredAfterSequence = Math.max(0, firstHintSequence - 1);
3462
+ const afterSequence = Math.max(0, Math.min(cached?.latestSequence ?? requiredAfterSequence, requiredAfterSequence));
3463
+ const notificationAttempt = projection.notificationHints.reduce((maximum, item) => Math.max(maximum, item.attempts), 0);
3136
3464
  const data = await this.runtime.authenticatedChatPost("/api/v1/chat/timeline/tail", {
3137
3465
  chat_session_uid: uid,
3138
3466
  after_seq: afterSequence,
3139
3467
  limit: 50
3140
3468
  }, session, void 0, {
3141
3469
  lane: "background-read",
3142
- key: `projection:tail:${uid}:${String(afterSequence)}`
3470
+ key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
3143
3471
  });
3144
3472
  return [uid, await this.projectionReader.chatTimelineItems(data, session)];
3145
3473
  }))).forEach((result, index) => {
@@ -3150,7 +3478,8 @@ var ChatRealtimeService = class {
3150
3478
  });
3151
3479
  }
3152
3480
  const updates = [];
3153
- for (const [uid] of pending) {
3481
+ const notifications = [];
3482
+ for (const [uid, projection] of pending) {
3154
3483
  const bundle = bundles.get(uid);
3155
3484
  if (bundle === void 0 || failedUids.has(uid)) {
3156
3485
  failedUids.add(uid);
@@ -3161,11 +3490,47 @@ var ChatRealtimeService = class {
3161
3490
  try {
3162
3491
  const source = await this.source.chatSourceFromBundle(bundle, session, this.source.cachedChatSourceByKey(cacheKey), timelineItems);
3163
3492
  this.source.setChatSourceByKey(cacheKey, source);
3493
+ const sourceKey = source.sourceKey ?? await this.source.chatDirectorySourceKey(session.userId, uid);
3164
3494
  updates.push({
3165
- sourceKey: source.sourceKey ?? await this.source.chatDirectorySourceKey(session.userId, uid),
3495
+ sourceKey,
3166
3496
  source,
3167
3497
  timelineItems
3168
3498
  });
3499
+ if (source.kind === "private_chat" || source.kind === "group_chat") for (const candidate of projection.notificationHints.sort((left, right) => left.hint.latestSequence - right.hint.latestSequence)) {
3500
+ const state = this.chatRealtime.state();
3501
+ if (candidate.connectionGeneration !== state.connectionGeneration) continue;
3502
+ if (this.notificationBaselineGeneration !== candidate.connectionGeneration) {
3503
+ this.mergePendingChatProjection(uid, {
3504
+ latestSequence: projection.latestSequence,
3505
+ notificationHints: [candidate]
3506
+ });
3507
+ continue;
3508
+ }
3509
+ const baselineSequence = this.notificationBaselineSequences.get(uid) ?? 0;
3510
+ if (candidate.hint.latestSequence <= baselineSequence || candidate.hint.senderUserId === session.userId || source.isMuted === true) continue;
3511
+ const message = timelineItems.find((item) => item.sequence === candidate.hint.latestSequence && !item.isMe);
3512
+ if (message === void 0) {
3513
+ if (candidate.attempts < 4) this.mergePendingChatProjection(uid, {
3514
+ latestSequence: projection.latestSequence,
3515
+ notificationHints: [{
3516
+ ...candidate,
3517
+ attempts: candidate.attempts + 1
3518
+ }]
3519
+ });
3520
+ else console.warn("dsh-arkme: Chat notification message was not projected:", candidate.hint.eventUid);
3521
+ continue;
3522
+ }
3523
+ const preview = message.textContent.trim() || "非文本内容";
3524
+ notifications.push({
3525
+ eventUid: candidate.hint.eventUid,
3526
+ sourceRef: source.sourceRef,
3527
+ sourceKey,
3528
+ sourceKind: source.kind,
3529
+ title: source.displayName,
3530
+ body: source.kind === "group_chat" ? `${message.senderName}:${preview}` : preview,
3531
+ eventAtMillis: candidate.hint.eventAtMillis
3532
+ });
3533
+ }
3169
3534
  } catch {
3170
3535
  failedUids.add(uid);
3171
3536
  }
@@ -3178,6 +3543,11 @@ var ChatRealtimeService = class {
3178
3543
  updates
3179
3544
  });
3180
3545
  }
3546
+ for (const notification of notifications) this.emitChatClientEvent({
3547
+ type: "message-notification",
3548
+ revision: this.nextChatClientRevision(),
3549
+ notification
3550
+ });
3181
3551
  return pending.filter(([uid]) => failedUids.has(uid));
3182
3552
  }
3183
3553
  emitChatClientEvent(event) {
@@ -3190,10 +3560,10 @@ var ChatRealtimeService = class {
3190
3560
  };
3191
3561
  //#endregion
3192
3562
  //#region src/services/chat-service.ts
3193
- function numberValue$15(value) {
3563
+ function numberValue$16(value) {
3194
3564
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
3195
3565
  }
3196
- function booleanValue$7(value) {
3566
+ function booleanValue$8(value) {
3197
3567
  return value === true;
3198
3568
  }
3199
3569
  function listValue$15(value) {
@@ -3242,10 +3612,10 @@ function timelineAgentSource(relation, record, payload) {
3242
3612
  label: agentSourceLabel(displayName)
3243
3613
  };
3244
3614
  }
3245
- function encodeOpaqueJson$4(value) {
3615
+ function encodeOpaqueJson$5(value) {
3246
3616
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
3247
3617
  }
3248
- function decodeOpaqueJson$4(value) {
3618
+ function decodeOpaqueJson$5(value) {
3249
3619
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
3250
3620
  }
3251
3621
  var ChatService = class {
@@ -3292,15 +3662,15 @@ var ChatService = class {
3292
3662
  const uid = stringValue$1(chatSession.chat_session_uid).trim();
3293
3663
  if (uid === "") throw new ArkmePluginError("private-chat-contract-invalid", "私聊会话响应不完整", true, 502);
3294
3664
  const unread = objectValue$1(data.unread_snapshot);
3295
- const latestSequence = numberValue$15(unread.session_last_seq ?? chatSession.last_seq);
3665
+ const latestSequence = numberValue$16(unread.session_last_seq ?? chatSession.last_seq);
3296
3666
  const source = {
3297
3667
  sourceRef: await this.source.sealSourceRef(session.userId, "private_chat", uid, displayName),
3298
3668
  sourceKey: await this.source.chatDirectorySourceKey(session.userId, uid),
3299
3669
  kind: "private_chat",
3300
3670
  displayName,
3301
3671
  ...profile?.avatarUrl === void 0 ? {} : { avatarRef: await this.profile.sealProfileImageRef(session.userId, peerUserId) },
3302
- activeAtMillis: numberValue$15(chatSession.last_active_at) || Date.now(),
3303
- unreadCount: numberValue$15(unread.unread_count),
3672
+ activeAtMillis: numberValue$16(chatSession.last_active_at) || Date.now(),
3673
+ unreadCount: numberValue$16(unread.unread_count),
3304
3674
  ...latestSequence > 0 ? { latestSequence } : {}
3305
3675
  };
3306
3676
  this.source.setChatSourceByKey(`${String(session.userId)}:${uid}`, source);
@@ -3327,7 +3697,7 @@ var ChatService = class {
3327
3697
  mediaUnavailable: media.unavailableRecordUids.has(recordUid)
3328
3698
  });
3329
3699
  }).filter((item) => item.itemUid !== "");
3330
- const nextSendAt = numberValue$15(data.next_cursor_send_at);
3700
+ const nextSendAt = numberValue$16(data.next_cursor_send_at);
3331
3701
  const nextUid = stringValue$1(data.next_cursor_record_uid).trim();
3332
3702
  return {
3333
3703
  source: await this.source.sourceItem(source),
@@ -3371,7 +3741,7 @@ var ChatService = class {
3371
3741
  mediaUnavailable: media.unavailableRecordUids.has(recordUid)
3372
3742
  });
3373
3743
  });
3374
- const nextSendAt = numberValue$15(data.next_cursor_send_at);
3744
+ const nextSendAt = numberValue$16(data.next_cursor_send_at);
3375
3745
  const nextUid = stringValue$1(data.next_cursor_record_uid).trim();
3376
3746
  return {
3377
3747
  source: await this.source.sourceItem(source),
@@ -3401,14 +3771,14 @@ var ChatService = class {
3401
3771
  const relation = objectValue$1(item.relation);
3402
3772
  const record = objectValue$1(item.record);
3403
3773
  const payload = objectValue$1(record.payload);
3404
- const recordStatus = numberValue$15(record.status);
3774
+ const recordStatus = numberValue$16(record.status);
3405
3775
  if (recordStatus !== 1) continue;
3406
3776
  const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
3407
3777
  if (uid === "") continue;
3408
3778
  const relationUid = stringValue$1(relation.rel_uid).trim();
3409
- const senderUserId = numberValue$15(relation.sender_user_id);
3779
+ const senderUserId = numberValue$16(relation.sender_user_id);
3410
3780
  const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
3411
- const sendAtMillis = numberValue$15(relation.attach_at ?? payload.send_at);
3781
+ const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
3412
3782
  const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
3413
3783
  const rawAgentSource = timelineAgentSource(relation, record, payload);
3414
3784
  const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
@@ -3422,16 +3792,16 @@ var ChatService = class {
3422
3792
  title: stringValue$1(payload.title),
3423
3793
  textContent: stringValue$1(payload.text_content),
3424
3794
  status: recordStatus,
3425
- sequence: numberValue$15(relation.seq),
3426
- ...numberValue$15(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$15(record.version ?? payload.version) } : {},
3795
+ sequence: numberValue$16(relation.seq),
3796
+ ...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
3427
3797
  ...aiPolish === void 0 ? {} : { aiPolish },
3428
3798
  ...forwardRecords === void 0 ? {} : { forwardRecords },
3429
- templateKind: numberValue$15(payload.template_kind),
3430
- displayKind: numberValue$15(payload.display_kind),
3431
- version: numberValue$15(payload.version ?? record.version),
3432
- updateAtMillis: numberValue$15(payload.update_at ?? record.update_at),
3433
- recordDurationMillis: numberValue$15(payload.record_duration_millis),
3434
- editDurationMillis: numberValue$15(payload.edit_duration_millis),
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),
3435
3805
  contentBlocks: this.media.richContentBlocks(item, session.userId)
3436
3806
  }) - 1;
3437
3807
  if (Number.isSafeInteger(senderUserId) && senderUserId > 0) senderUserIdByIndex.set(itemIndex, senderUserId);
@@ -3443,7 +3813,7 @@ var ChatService = class {
3443
3813
  items[index].avatarRef = await this.profile.sealProfileImageRef(session.userId, senderUserId);
3444
3814
  }
3445
3815
  } catch {}
3446
- const beforeSequence = numberValue$15(data.next_before_seq);
3816
+ const beforeSequence = numberValue$16(data.next_before_seq);
3447
3817
  let aiPolishSettings;
3448
3818
  let aiPolishNotices;
3449
3819
  if (aiPolishDecorations !== void 0) {
@@ -3482,7 +3852,7 @@ var ChatService = class {
3482
3852
  return {
3483
3853
  messageRef,
3484
3854
  reportUid,
3485
- status: numberValue$15(report.status)
3855
+ status: numberValue$16(report.status)
3486
3856
  };
3487
3857
  }
3488
3858
  async sendSourceText(sourceRef, textContent, options = {}) {
@@ -3513,7 +3883,7 @@ var ChatService = class {
3513
3883
  return {
3514
3884
  sourceRef,
3515
3885
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
3516
- status: numberValue$15(result.status),
3886
+ status: numberValue$16(result.status),
3517
3887
  localState: "synced"
3518
3888
  };
3519
3889
  }
@@ -3546,7 +3916,7 @@ var ChatService = class {
3546
3916
  const raw = objectValue$1(value);
3547
3917
  const botId = stringValue$1(raw.bot_id).trim();
3548
3918
  if (!requestedById.has(botId)) continue;
3549
- if (stringValue$1(raw.provider).trim() !== "openclaw" || !booleanValue$7(raw.installed)) throw new ArkmePluginError("bot-mention-not-installed", "所选 Bot 未安装到该群聊", false, 409);
3919
+ if (stringValue$1(raw.provider).trim() !== "openclaw" || !booleanValue$8(raw.installed)) throw new ArkmePluginError("bot-mention-not-installed", "所选 Bot 未安装到该群聊", false, 409);
3550
3920
  const name = stringValue$1(raw.name).trim();
3551
3921
  if (name === "") throw new ArkmePluginError("bot-contract-invalid", "Bot 响应不完整", true, 502);
3552
3922
  const display = `@${name}`;
@@ -3598,8 +3968,8 @@ var ChatService = class {
3598
3968
  return {
3599
3969
  sourceRef,
3600
3970
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
3601
- status: numberValue$15(result.audit_status),
3602
- sequence: numberValue$15(result.seq),
3971
+ status: numberValue$16(result.audit_status),
3972
+ sequence: numberValue$16(result.seq),
3603
3973
  localState: "synced"
3604
3974
  };
3605
3975
  }
@@ -3655,7 +4025,7 @@ var ChatService = class {
3655
4025
  return {
3656
4026
  sourceRef,
3657
4027
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
3658
- status: numberValue$15(result.status),
4028
+ status: numberValue$16(result.status),
3659
4029
  localState: "synced"
3660
4030
  };
3661
4031
  }
@@ -3667,7 +4037,7 @@ var ChatService = class {
3667
4037
  return {
3668
4038
  sourceRef,
3669
4039
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
3670
- status: numberValue$15(result.status),
4040
+ status: numberValue$16(result.status),
3671
4041
  localState: "synced"
3672
4042
  };
3673
4043
  }
@@ -3679,8 +4049,8 @@ var ChatService = class {
3679
4049
  return {
3680
4050
  sourceRef,
3681
4051
  itemUid: stringValue$1(result.record_uid).trim() || recordUid,
3682
- status: numberValue$15(result.audit_status ?? result.status),
3683
- sequence: numberValue$15(result.seq),
4052
+ status: numberValue$16(result.audit_status ?? result.status),
4053
+ sequence: numberValue$16(result.seq),
3684
4054
  localState: "synced"
3685
4055
  };
3686
4056
  }
@@ -3730,7 +4100,7 @@ var ChatService = class {
3730
4100
  send_at: sendAtMillis
3731
4101
  }, session, options.signal);
3732
4102
  const chatSessionUid = stringValue$1(result.chat_session_uid).trim();
3733
- const sequence = numberValue$15(result.seq);
4103
+ const sequence = numberValue$16(result.seq);
3734
4104
  const targetKind = stringValue$1(result.target_kind).trim();
3735
4105
  if (chatSessionUid === "" || !Number.isSafeInteger(sequence) || sequence <= 0 || targetKind !== "direct") throw new ArkmePluginError("direct-send-response-invalid", "Chat Agent 发送返回了无效响应", true, 502);
3736
4106
  const responseRecordUid = stringValue$1(result.record_uid).trim() || recordUid;
@@ -3758,10 +4128,10 @@ var ChatService = class {
3758
4128
  reason: "arkme_dsh_open_chat"
3759
4129
  }, session);
3760
4130
  const responseSessionUid = stringValue$1(data.chat_session_uid).trim();
3761
- const effectiveReadSequence = numberValue$15(data.effective_read_seq);
3762
- const readAt = numberValue$15(data.read_at);
3763
- const sessionLastSequence = numberValue$15(data.session_last_seq);
3764
- const unreadCount = numberValue$15(data.unread_count);
4131
+ const effectiveReadSequence = numberValue$16(data.effective_read_seq);
4132
+ const readAt = numberValue$16(data.read_at);
4133
+ const sessionLastSequence = numberValue$16(data.session_last_seq);
4134
+ const unreadCount = numberValue$16(data.unread_count);
3765
4135
  if (responseSessionUid !== source.ownerRef || !Number.isSafeInteger(effectiveReadSequence) || effectiveReadSequence < readSequence || readAt <= 0 || sessionLastSequence < effectiveReadSequence || !Number.isSafeInteger(unreadCount) || unreadCount < 0) throw new ArkmePluginError("source-read-ack-invalid", "聊天已读响应不完整", true, 502);
3766
4136
  const cacheKey = `${String(session.userId)}:${source.ownerRef}`;
3767
4137
  const cached = this.source.cachedChatSourceByKey(cacheKey);
@@ -3796,9 +4166,9 @@ var ChatService = class {
3796
4166
  const payload = objectValue$1(record.payload);
3797
4167
  const uid = stringValue$1(relation.record_uid ?? payload.record_uid).trim();
3798
4168
  if (uid === "") continue;
3799
- const senderUserId = numberValue$15(relation.sender_user_id);
4169
+ const senderUserId = numberValue$16(relation.sender_user_id);
3800
4170
  const aiPolish = this.aiPolish.timelineAiPolish(record, payload);
3801
- const sendAtMillis = numberValue$15(relation.attach_at ?? payload.send_at);
4171
+ const sendAtMillis = numberValue$16(relation.attach_at ?? payload.send_at);
3802
4172
  const forwardRecords = await this.chatForwardRecordsPreview(item, session.userId, sendAtMillis);
3803
4173
  const rawAgentSource = timelineAgentSource(relation, record, payload);
3804
4174
  const agentSource = senderUserId === session.userId ? this.arko.currentUserAgentSourceFallback(session.userId, rawAgentSource) : rawAgentSource;
@@ -3811,12 +4181,12 @@ var ChatService = class {
3811
4181
  sendAtMillis,
3812
4182
  title: stringValue$1(payload.title),
3813
4183
  textContent: stringValue$1(payload.text_content),
3814
- status: numberValue$15(record.status),
3815
- sequence: numberValue$15(relation.seq),
3816
- ...numberValue$15(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$15(record.version ?? payload.version) } : {},
4184
+ status: numberValue$16(record.status),
4185
+ sequence: numberValue$16(relation.seq),
4186
+ ...numberValue$16(record.version ?? payload.version) > 0 ? { recordVersion: numberValue$16(record.version ?? payload.version) } : {},
3817
4187
  ...aiPolish === void 0 ? {} : { aiPolish },
3818
4188
  ...forwardRecords === void 0 ? {} : { forwardRecords },
3819
- displayKind: numberValue$15(payload.display_kind),
4189
+ displayKind: numberValue$16(payload.display_kind),
3820
4190
  contentBlocks: this.media.richContentBlocks(item, session.userId)
3821
4191
  });
3822
4192
  }
@@ -3832,7 +4202,7 @@ var ChatService = class {
3832
4202
  const sorted = values.map((value, index) => ({
3833
4203
  value,
3834
4204
  index,
3835
- order: numberValue$15(objectValue$1(value).item_order)
4205
+ order: numberValue$16(objectValue$1(value).item_order)
3836
4206
  })).sort((left, right) => (left.order || left.index) - (right.order || right.index));
3837
4207
  for (const entry of sorted) {
3838
4208
  if (projectedItems.length >= 100) return;
@@ -3842,19 +4212,19 @@ var ChatService = class {
3842
4212
  await appendItems(listValue$15(nestedForward.items), depth + 1);
3843
4213
  continue;
3844
4214
  }
3845
- const senderUserId = numberValue$15(item.source_sender_user_id ?? item.sourceSenderUserId ?? item.owner_id ?? item.ownerId);
4215
+ const senderUserId = numberValue$16(item.source_sender_user_id ?? item.sourceSenderUserId ?? item.owner_id ?? item.ownerId);
3846
4216
  const senderName = stringValue$1(item.owner_name ?? item.ownerName ?? item.source_display_name ?? item.sourceDisplayName).trim() || "Arkme用户";
3847
4217
  const textContent = stringValue$1(item.text ?? item.text_preview ?? item.textPreview).trim();
3848
4218
  const title = stringValue$1(item.title).trim();
3849
- const imageCount = Math.max(0, Math.trunc(numberValue$15(item.image_count ?? item.imageCount)));
3850
- const voiceCount = Math.max(0, Math.trunc(numberValue$15(item.voice_count ?? item.voiceCount)));
3851
- const fileCount = Math.max(0, Math.trunc(numberValue$15(item.file_count ?? item.fileCount)));
4219
+ const imageCount = Math.max(0, Math.trunc(numberValue$16(item.image_count ?? item.imageCount)));
4220
+ const voiceCount = Math.max(0, Math.trunc(numberValue$16(item.voice_count ?? item.voiceCount)));
4221
+ const fileCount = Math.max(0, Math.trunc(numberValue$16(item.file_count ?? item.fileCount)));
3852
4222
  const fileName = listValue$15(item.file_names ?? item.fileNames).map((value) => stringValue$1(value).trim()).find((value) => value !== "");
3853
4223
  const contentLabel = imageCount > 0 ? imageCount > 1 ? `[${String(imageCount)}张图片]` : "[图片]" : voiceCount > 0 ? "[语音]" : fileCount > 0 ? fileName === void 0 ? "[文件]" : `[文件] ${fileName}` : stringValue$1(item.availability).trim() !== "" ? "原快记暂不可查看" : void 0;
3854
4224
  projectedItems.push({
3855
4225
  senderName,
3856
4226
  ...senderUserId > 0 ? { avatarRef: await this.profile.sealProfileImageRef(viewerUserId, senderUserId) } : {},
3857
- sendAtMillis: numberValue$15(item.send_at ?? item.sendAt),
4227
+ sendAtMillis: numberValue$16(item.send_at ?? item.sendAt),
3858
4228
  title,
3859
4229
  textContent,
3860
4230
  ...contentLabel === void 0 ? {} : { contentLabel }
@@ -3863,7 +4233,7 @@ var ChatService = class {
3863
4233
  };
3864
4234
  await appendItems(listValue$15(payload.items), 0);
3865
4235
  const summaryLines = listValue$15(payload.summary_lines ?? payload.summaryLines).map((value) => stringValue$1(value).trim()).filter((value) => value !== "");
3866
- const createdAtMillis = numberValue$15(payload.created_at ?? payload.createdAt) || fallbackCreatedAtMillis;
4236
+ const createdAtMillis = numberValue$16(payload.created_at ?? payload.createdAt) || fallbackCreatedAtMillis;
3867
4237
  return {
3868
4238
  title: stringValue$1(payload.title).trim() || "转发快记",
3869
4239
  createdAtMillis,
@@ -3872,7 +4242,7 @@ var ChatService = class {
3872
4242
  };
3873
4243
  }
3874
4244
  sealMessageRef(userId, chatSessionUid, relationUid, signingKey) {
3875
- const payload = encodeOpaqueJson$4({
4245
+ const payload = encodeOpaqueJson$5({
3876
4246
  version: 1,
3877
4247
  userId,
3878
4248
  chatSessionUid,
@@ -3889,13 +4259,13 @@ var ChatService = class {
3889
4259
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false);
3890
4260
  let parsed;
3891
4261
  try {
3892
- parsed = objectValue$1(decodeOpaqueJson$4(payload));
4262
+ parsed = objectValue$1(decodeOpaqueJson$5(payload));
3893
4263
  } catch (error) {
3894
4264
  throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false, 400, { cause: error });
3895
4265
  }
3896
4266
  const result = {
3897
4267
  version: 1,
3898
- userId: numberValue$15(parsed.userId),
4268
+ userId: numberValue$16(parsed.userId),
3899
4269
  chatSessionUid: stringValue$1(parsed.chatSessionUid).trim(),
3900
4270
  relationUid: stringValue$1(parsed.relationUid).trim()
3901
4271
  };
@@ -3904,6 +4274,167 @@ var ChatService = class {
3904
4274
  }
3905
4275
  };
3906
4276
  //#endregion
4277
+ //#region src/services/contact-service.ts
4278
+ const CONTACT_REF_TTL_MS = 10 * 6e4;
4279
+ const MAX_CONTACT_REFS = 512;
4280
+ const CONTACT_REF_PATTERN = /^arkme-contact-v1\.[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4281
+ function numberValue$15(value) {
4282
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
4283
+ return Number.isFinite(parsed) ? parsed : 0;
4284
+ }
4285
+ function booleanValue$7(value) {
4286
+ return value === true;
4287
+ }
4288
+ function normalizeIdentifier(value) {
4289
+ const trimmed = value.trim().replace(/^@/, "");
4290
+ if (trimmed === "" || trimmed.length > 64) throw new ArkmePluginError("contact-identifier-invalid", "请输入手机号或即我号", false);
4291
+ const compactPhone = trimmed.replace(/[\s()-]/g, "").replace(/^\+86/, "");
4292
+ if (/^\d+$/.test(compactPhone)) {
4293
+ if (!/^1[3-9]\d{9}$/.test(compactPhone)) throw new ArkmePluginError("contact-phone-invalid", "请输入有效的中国大陆手机号", false);
4294
+ return {
4295
+ kind: "phone",
4296
+ identifierType: 1,
4297
+ identifier: compactPhone,
4298
+ phonePre: "86"
4299
+ };
4300
+ }
4301
+ if (!/^[A-Za-z][A-Za-z0-9_-]{1,63}$/.test(trimmed)) throw new ArkmePluginError("contact-arkme-id-invalid", "即我号格式无效", false);
4302
+ return {
4303
+ kind: "arkme_id",
4304
+ identifierType: 2,
4305
+ identifier: trimmed,
4306
+ phonePre: "86"
4307
+ };
4308
+ }
4309
+ var ContactService = class {
4310
+ runtime;
4311
+ source;
4312
+ profile;
4313
+ realtime;
4314
+ candidates = /* @__PURE__ */ new Map();
4315
+ constructor(runtime, source, profile, realtime) {
4316
+ this.runtime = runtime;
4317
+ this.source = source;
4318
+ this.profile = profile;
4319
+ this.realtime = realtime;
4320
+ }
4321
+ dispose() {
4322
+ this.candidates.clear();
4323
+ }
4324
+ async search(identifier, options = {}) {
4325
+ const session = await this.runtime.requireSession();
4326
+ const normalized = normalizeIdentifier(identifier);
4327
+ const candidate = await this.lookup(normalized, session, options.signal);
4328
+ this.prune();
4329
+ this.candidates.set(candidate.result.contactRef, candidate);
4330
+ this.prune();
4331
+ return candidate.result;
4332
+ }
4333
+ async add(contactRef, options = {}) {
4334
+ const session = await this.runtime.requireSession();
4335
+ this.prune();
4336
+ const normalizedContactRef = contactRef.trim();
4337
+ if (!CONTACT_REF_PATTERN.test(normalizedContactRef)) throw new ArkmePluginError("contact-ref-invalid", "联系人搜索引用无效,请重新搜索", false);
4338
+ const cached = this.candidates.get(normalizedContactRef);
4339
+ if (cached === void 0 || cached.userId !== session.userId || cached.expiresAtMillis <= Date.now()) throw new ArkmePluginError("contact-ref-expired", "联系人搜索结果已过期,请重新搜索", false, 410);
4340
+ const candidate = await this.lookup(cached.normalized, session, options.signal, normalizedContactRef);
4341
+ if (candidate.targetUserId !== cached.targetUserId || candidate.result.registered !== cached.result.registered) {
4342
+ this.candidates.delete(normalizedContactRef);
4343
+ throw new ArkmePluginError("contact-candidate-changed", "联系人身份已发生变化,请重新搜索后确认", false, 409);
4344
+ }
4345
+ if (candidate.result.isSelf) throw new ArkmePluginError("contact-self-invalid", "不能添加自己为联系人", false);
4346
+ if (!candidate.result.canAdd) throw new ArkmePluginError("contact-add-unavailable", "该账号当前无法添加", false, 409);
4347
+ const remark = options.remark?.trim() ?? "";
4348
+ if (Array.from(remark).length > 100) throw new ArkmePluginError("contact-remark-invalid", "备注不能超过 100 个字符", false);
4349
+ const requestUid = options.requestUid?.trim() || randomUUID();
4350
+ 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(requestUid)) throw new ArkmePluginError("contact-request-uid-invalid", "联系人添加请求标识无效", false);
4351
+ const chatSessionUid = `chat_session_${requestUid}`;
4352
+ const displayName = candidate.result.displayName;
4353
+ const common = {
4354
+ chat_session_uid: chatSessionUid,
4355
+ create_at: Date.now(),
4356
+ ...remark === "" ? {} : { remark }
4357
+ };
4358
+ const registered = candidate.result.registered && candidate.targetUserId !== void 0;
4359
+ const bundle = registered ? await this.runtime.authenticatedChatPost("/api/v1/chats/contacts/add-and-open-private", {
4360
+ ...common,
4361
+ target_user_id: candidate.targetUserId,
4362
+ target_display_name_snapshot: displayName
4363
+ }, session, options.signal, { key: `contact:add:${requestUid}` }) : await this.runtime.authenticatedChatPost("/api/v1/chats/contacts/add-and-open-pending-phone", {
4364
+ ...common,
4365
+ phone_pre: candidate.normalized.phonePre,
4366
+ phone: candidate.phone ?? candidate.normalized.identifier,
4367
+ display_name_snapshot: displayName
4368
+ }, session, options.signal, { key: `contact:add:${requestUid}` });
4369
+ let source = await this.source.chatSourceFromBundle(bundle, session, void 0, []);
4370
+ if (registered && candidate.targetUserId !== void 0) source = {
4371
+ ...source,
4372
+ avatarRef: await this.profile.sealProfileImageRef(session.userId, candidate.targetUserId)
4373
+ };
4374
+ const returnedUid = stringValue$1(objectValue$1(bundle.session).chat_session_uid).trim();
4375
+ if (returnedUid === "") throw new ArkmePluginError("contact-add-contract-invalid", "联系人添加响应不完整", true, 502);
4376
+ this.source.setChatSource(session.userId, returnedUid, source);
4377
+ this.source.invalidateSourceListCache(session.userId, "root");
4378
+ this.realtime.emitChatClientEvent({
4379
+ type: "sessions-delta",
4380
+ revision: this.realtime.nextChatClientRevision(),
4381
+ updates: [{
4382
+ sourceKey: source.sourceKey ?? await this.source.chatDirectorySourceKey(session.userId, returnedUid),
4383
+ source,
4384
+ timelineItems: []
4385
+ }]
4386
+ });
4387
+ this.candidates.delete(normalizedContactRef);
4388
+ return {
4389
+ state: registered ? "ready" : "pending",
4390
+ source
4391
+ };
4392
+ }
4393
+ async lookup(normalized, session, signal, contactRef = `arkme-contact-v1.${randomUUID()}`) {
4394
+ const data = await this.runtime.authenticatedAuthPost("/api/v1/auth/search-contact-account", {
4395
+ identifier_type: normalized.identifierType,
4396
+ identifier: normalized.identifier,
4397
+ phone_pre: normalized.phonePre
4398
+ }, session, signal, {
4399
+ key: `contact:search:${normalized.identifierType}:${normalized.identifier}`,
4400
+ bypassCache: true
4401
+ });
4402
+ const targetUserId = Math.trunc(numberValue$15(data.user_id));
4403
+ const registered = booleanValue$7(data.is_registered) && targetUserId > 0;
4404
+ const isSelf = booleanValue$7(data.is_self);
4405
+ const canAdd = booleanValue$7(data.can_add) && !isSelf && (normalized.kind === "phone" || registered);
4406
+ const displayName = stringValue$1(data.nick_name).trim() || stringValue$1(data.jotmo_id).trim() || (normalized.kind === "phone" ? `+${normalized.phonePre} ${normalized.identifier}` : normalized.identifier);
4407
+ const result = {
4408
+ contactRef,
4409
+ identifierKind: normalized.kind,
4410
+ displayName,
4411
+ ...stringValue$1(data.jotmo_id).trim() === "" ? {} : { arkmeId: stringValue$1(data.jotmo_id).trim() },
4412
+ ...registered ? { avatarRef: await this.profile.sealProfileImageRef(session.userId, targetUserId) } : {},
4413
+ registered,
4414
+ inviteBySms: booleanValue$7(data.invite_by_sms),
4415
+ canAdd,
4416
+ isSelf
4417
+ };
4418
+ return {
4419
+ userId: session.userId,
4420
+ expiresAtMillis: Date.now() + CONTACT_REF_TTL_MS,
4421
+ normalized,
4422
+ result,
4423
+ ...targetUserId > 0 ? { targetUserId } : {},
4424
+ ...normalized.kind === "phone" ? { phone: stringValue$1(data.phone).trim() || normalized.identifier } : {}
4425
+ };
4426
+ }
4427
+ prune() {
4428
+ const now = Date.now();
4429
+ for (const [ref, candidate] of this.candidates) if (candidate.expiresAtMillis <= now) this.candidates.delete(ref);
4430
+ while (this.candidates.size >= MAX_CONTACT_REFS) {
4431
+ const oldest = this.candidates.keys().next().value;
4432
+ if (oldest === void 0) break;
4433
+ this.candidates.delete(oldest);
4434
+ }
4435
+ }
4436
+ };
4437
+ //#endregion
3907
4438
  //#region src/services/community-service.ts
3908
4439
  function numberValue$14(value) {
3909
4440
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -4119,7 +4650,7 @@ var ExtensionReviewService = class {
4119
4650
  422
4120
4651
  ].includes(error.httpStatus)) {
4121
4652
  await this.runtime.stateStore.removeExtensionReviewOperation(userId, operation.clientMutationId);
4122
- throw new ArkmePluginError("extension-review-registry-rejected", `评论已写入首页,但扩展市场未接受:${error.message}`, false, error.httpStatus, { cause: error });
4653
+ throw new ArkmePluginError("extension-review-registry-rejected", `评论已写入首页,但市集未接受:${error.message}`, false, error.httpStatus, { cause: error });
4123
4654
  }
4124
4655
  await this.runtime.stateStore.markExtensionReviewOperation(userId, operation.clientMutationId, "failed", safeFailureMessage$5(error));
4125
4656
  throw new ArkmePluginError("extension-review-registry-pending", "评论已写入首页,但同步到扩展详情失败;请保留当前内容并重试", true, 503, { cause: error });
@@ -4658,12 +5189,19 @@ var GroupAiPolishService = class {
4658
5189
  };
4659
5190
  //#endregion
4660
5191
  //#region src/services/group-service.ts
5192
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4661
5193
  function numberValue$12(value) {
4662
5194
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
4663
5195
  }
4664
5196
  function listValue$12(value) {
4665
5197
  return Array.isArray(value) ? value : [];
4666
5198
  }
5199
+ function encodeOpaqueJson$4(value) {
5200
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
5201
+ }
5202
+ function decodeOpaqueJson$4(value) {
5203
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
5204
+ }
4667
5205
  function chatMessageDnd$1(value) {
4668
5206
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
4669
5207
  const policy = value;
@@ -4685,10 +5223,325 @@ var GroupService = class {
4685
5223
  runtime;
4686
5224
  source;
4687
5225
  profile;
4688
- constructor(runtime, source, profile) {
5226
+ inviteSender;
5227
+ constructor(runtime, source, profile, inviteSender) {
4689
5228
  this.runtime = runtime;
4690
5229
  this.source = source;
4691
5230
  this.profile = profile;
5231
+ this.inviteSender = inviteSender;
5232
+ }
5233
+ async groupInvitePreview(sourceRef, signal) {
5234
+ const session = await this.runtime.requireSession();
5235
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
5236
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-source-invalid", "仅支持邀请群聊协作者", false);
5237
+ const preview = objectValue$1((await this.runtime.authenticatedChatPost("/api/v1/chats/governance/invite-preview", { chat_session_uid: source.ownerRef }, session, signal)).preview);
5238
+ const inviteLink = stringValue$1(preview.invite_link).trim();
5239
+ if (inviteLink === "") throw new ArkmePluginError("group-invite-unavailable", "邀请链接生成失败,请重试", true);
5240
+ return {
5241
+ source: await this.source.sourceItem(source),
5242
+ title: stringValue$1(preview.title).trim() || source.displayName,
5243
+ inviterDisplayName: stringValue$1(preview.creator_display_name).trim() || "Arkme",
5244
+ inviteLink,
5245
+ expireAtMillis: numberValue$12(preview.expire_at),
5246
+ mode: numberValue$12(preview.join_mode) === 2 ? "approval_invite" : "direct_add"
5247
+ };
5248
+ }
5249
+ async listGroupMemberCandidates(sourceRef, options = {}) {
5250
+ const session = await this.runtime.requireSession();
5251
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
5252
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-source-invalid", "仅支持向群聊添加成员", false);
5253
+ const limit = Math.min(50, Math.max(1, Math.trunc(options.limit ?? 20)));
5254
+ const query = options.query?.trim().toLocaleLowerCase() ?? "";
5255
+ const requestedGroupSourceRefs = [...new Set((options.groupSourceRefs ?? []).map((value) => value.trim()).filter((value) => value !== ""))].slice(0, 20);
5256
+ const [membersData, directoryData, inviteData] = await Promise.all([
5257
+ this.runtime.authenticatedChatPost("/api/v1/chats/members/list", {
5258
+ chat_session_uid: source.ownerRef,
5259
+ active_only: true
5260
+ }, session, options.signal).catch(() => ({})),
5261
+ this.runtime.authenticatedChatPost("/api/v1/chats/list", { limit: 50 }, session, options.signal),
5262
+ this.runtime.authenticatedChatPost("/api/v1/chats/governance/invite-preview", { chat_session_uid: source.ownerRef }, session, options.signal).catch(() => ({}))
5263
+ ]);
5264
+ const directoryBundles = listValue$12(directoryData.items);
5265
+ const privateBundles = directoryBundles.filter((raw) => {
5266
+ const kind = numberValue$12(objectValue$1(objectValue$1(raw).session).session_kind);
5267
+ return kind === 1 || kind === 3;
5268
+ });
5269
+ const privateTopicIds = privateBundles.map((raw) => {
5270
+ const chatSession = objectValue$1(objectValue$1(raw).session);
5271
+ return numberValue$12(chatSession.shared_topic_id ?? chatSession.subject_id);
5272
+ }).filter((value) => Number.isSafeInteger(value) && value > 0);
5273
+ const partnerData = privateTopicIds.length === 0 ? {} : await this.runtime.authenticatedSubjectPost("/api/v1/private/get-partner-info-v2", { rm_subject_ids: [...new Set(privateTopicIds)] }, session, options.signal).catch(() => ({}));
5274
+ const contactByTopicId = /* @__PURE__ */ new Map();
5275
+ for (const raw of listValue$12(partnerData.item_ls)) {
5276
+ const item = objectValue$1(raw);
5277
+ const topicId = numberValue$12(item.rm_subject_id ?? item.shared_topic_id);
5278
+ if (topicId > 0 && typeof item.is_contact === "boolean") contactByTopicId.set(topicId, item.is_contact);
5279
+ }
5280
+ const existingMemberUserIds = new Set(listValue$12(objectValue$1(membersData).items).map((item) => numberValue$12(objectValue$1(item).user_id)));
5281
+ const rawCandidates = /* @__PURE__ */ new Map();
5282
+ for (const raw of privateBundles) {
5283
+ const bundle = objectValue$1(raw);
5284
+ const chatSession = objectValue$1(bundle.session);
5285
+ if (numberValue$12(chatSession.session_kind) !== 1 && numberValue$12(chatSession.session_kind) !== 3) continue;
5286
+ const counterpart = objectValue$1(bundle.private_counterpart);
5287
+ const supplement = objectValue$1(bundle.private_supplement);
5288
+ const targetUserId = numberValue$12(counterpart.user_id);
5289
+ const privateChatSessionUid = stringValue$1(chatSession.chat_session_uid).trim();
5290
+ const displayName = stringValue$1(supplement.remark ?? supplement.counterpart_name_snapshot ?? counterpart.display_name_snapshot ?? supplement.pending_name ?? counterpart.visible_phone).trim();
5291
+ const privateTopicId = numberValue$12(chatSession.shared_topic_id ?? chatSession.subject_id);
5292
+ const contactState = numberValue$12(supplement.contact_state);
5293
+ const relation = contactByTopicId.get(privateTopicId) === true || contactState === 1 || contactState === 3 ? "contact" : "stranger";
5294
+ if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0 || targetUserId === session.userId || privateChatSessionUid === "" || displayName === "" || query !== "" && !displayName.toLocaleLowerCase().includes(query)) continue;
5295
+ rawCandidates.set(targetUserId, {
5296
+ targetUserId,
5297
+ displayName,
5298
+ privateChatSessionUid,
5299
+ relation,
5300
+ origin: "private_chat"
5301
+ });
5302
+ }
5303
+ const selected = [...rawCandidates.entries()].slice(0, limit);
5304
+ const sourceByGroupUid = /* @__PURE__ */ new Map();
5305
+ const groupSessionUidByIndex = /* @__PURE__ */ new Map();
5306
+ const groups = [];
5307
+ for (const raw of directoryBundles) {
5308
+ const bundle = objectValue$1(raw);
5309
+ const chatSession = objectValue$1(bundle.session);
5310
+ if (numberValue$12(chatSession.session_kind) !== 2) continue;
5311
+ const groupSessionUid = stringValue$1(chatSession.chat_session_uid).trim();
5312
+ if (groupSessionUid === "" || groupSessionUid === source.ownerRef) continue;
5313
+ const displayName = stringValue$1(chatSession.title ?? chatSession.subject_title ?? objectValue$1(bundle.participant_summary).display_name).trim();
5314
+ const groupItem = {
5315
+ sourceRef: await this.source.sealSourceRef(session.userId, "group_chat", groupSessionUid, displayName || "群聊"),
5316
+ sourceKey: await this.source.chatDirectorySourceKey(session.userId, groupSessionUid),
5317
+ kind: "group_chat",
5318
+ displayName: displayName || "群聊",
5319
+ activeAtMillis: numberValue$12(chatSession.updated_at),
5320
+ unreadCount: 0
5321
+ };
5322
+ const groupIndex = groups.push(groupItem) - 1;
5323
+ groupSessionUidByIndex.set(groupIndex, groupSessionUid);
5324
+ sourceByGroupUid.set(groupSessionUid, groupItem);
5325
+ this.source.setChatSource(session.userId, groupSessionUid, groupItem);
5326
+ }
5327
+ try {
5328
+ await this.source.hydrateSourceAvatars(groups, /* @__PURE__ */ new Map(), groupSessionUidByIndex, session, options.signal);
5329
+ } catch {}
5330
+ const requestedGroupSources = [];
5331
+ for (const groupSourceRef of requestedGroupSourceRefs) {
5332
+ const groupSource = await this.source.openSourceRef(groupSourceRef, session.userId);
5333
+ if (groupSource.kind !== "group_chat" || groupSource.ownerRef === source.ownerRef) continue;
5334
+ requestedGroupSources.push(groupSource);
5335
+ }
5336
+ const groupRawByUid = /* @__PURE__ */ new Map();
5337
+ const groupTargetUserIds = /* @__PURE__ */ new Set();
5338
+ for (const groupSource of requestedGroupSources) {
5339
+ const group = sourceByGroupUid.get(groupSource.ownerRef) ?? await this.source.sourceItem(groupSource);
5340
+ try {
5341
+ const groupMembersData = await this.runtime.authenticatedChatPost("/api/v1/chats/members/list", {
5342
+ chat_session_uid: groupSource.ownerRef,
5343
+ active_only: true
5344
+ }, session, options.signal);
5345
+ const raws = [];
5346
+ const seenUserIds = /* @__PURE__ */ new Set();
5347
+ for (const rawMember of listValue$12(groupMembersData.items)) {
5348
+ const member = objectValue$1(rawMember);
5349
+ const targetUserId = numberValue$12(member.user_id);
5350
+ if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0 || targetUserId === session.userId || seenUserIds.has(targetUserId)) continue;
5351
+ if (chatMemberStatus(member.status) !== "active") continue;
5352
+ const privateCandidate = rawCandidates.get(targetUserId);
5353
+ if (privateCandidate !== void 0) {
5354
+ if (query === "" || privateCandidate.displayName.toLocaleLowerCase().includes(query)) {
5355
+ raws.push(privateCandidate);
5356
+ groupTargetUserIds.add(targetUserId);
5357
+ seenUserIds.add(targetUserId);
5358
+ }
5359
+ continue;
5360
+ }
5361
+ const profileName = stringValue$1(member.profile_display_name).trim();
5362
+ const displayName = [
5363
+ stringValue$1(member.remark).trim(),
5364
+ stringValue$1(member.display_name_snapshot).trim(),
5365
+ profileName === `用户 ${String(targetUserId)}` ? "" : profileName,
5366
+ stringValue$1(objectValue$1(member.user_info).nick_name).trim(),
5367
+ String(targetUserId)
5368
+ ].find((value) => value !== "" && value !== "成员" && value !== "群成员") ?? "群成员";
5369
+ if (query !== "" && !displayName.toLocaleLowerCase().includes(query)) continue;
5370
+ raws.push({
5371
+ targetUserId,
5372
+ displayName,
5373
+ relation: "group",
5374
+ origin: "group_chat"
5375
+ });
5376
+ groupTargetUserIds.add(targetUserId);
5377
+ seenUserIds.add(targetUserId);
5378
+ }
5379
+ groupRawByUid.set(groupSource.ownerRef, {
5380
+ group,
5381
+ raws: raws.slice(0, limit),
5382
+ total: raws.length
5383
+ });
5384
+ } catch (error) {
5385
+ groupRawByUid.set(groupSource.ownerRef, {
5386
+ group,
5387
+ raws: [],
5388
+ total: 0,
5389
+ error: error instanceof Error && error.message.trim() !== "" ? error.message : "群成员加载失败,请重试"
5390
+ });
5391
+ }
5392
+ }
5393
+ const profiles = await this.profile.publicProfileSummariesByUserIds([.../* @__PURE__ */ new Set([...selected.map(([userId]) => userId), ...groupTargetUserIds])], session, options.signal).catch(() => /* @__PURE__ */ new Map());
5394
+ const preview = objectValue$1(objectValue$1(inviteData).preview);
5395
+ const approvalRequired = numberValue$12(preview.join_mode) === 2;
5396
+ const candidateItem = async (targetUserId, candidate) => {
5397
+ const alreadyMember = existingMemberUserIds.has(targetUserId);
5398
+ const cannotInvite = approvalRequired && candidate.privateChatSessionUid === void 0;
5399
+ return {
5400
+ candidateRef: await this.sealCandidateRef({
5401
+ version: 1,
5402
+ userId: session.userId,
5403
+ groupChatSessionUid: source.ownerRef,
5404
+ targetUserId,
5405
+ origin: candidate.origin,
5406
+ displayName: candidate.displayName,
5407
+ ...candidate.privateChatSessionUid === void 0 ? {} : { privateChatSessionUid: candidate.privateChatSessionUid }
5408
+ }),
5409
+ displayName: candidate.displayName,
5410
+ ...profiles.get(targetUserId)?.avatarUrl === void 0 ? {} : { avatarRef: await this.profile.sealProfileImageRef(session.userId, targetUserId) },
5411
+ origin: candidate.origin,
5412
+ relation: candidate.relation,
5413
+ ...alreadyMember ? {
5414
+ disabled: true,
5415
+ alreadyMember: true
5416
+ } : {},
5417
+ ...!alreadyMember && cannotInvite ? {
5418
+ disabled: true,
5419
+ statusText: "无法邀请"
5420
+ } : {}
5421
+ };
5422
+ };
5423
+ const items = await Promise.all(selected.map(async ([targetUserId, candidate]) => candidateItem(targetUserId, candidate)));
5424
+ const groupCandidates = await Promise.all([...groupRawByUid.values()].map(async (value) => ({
5425
+ group: value.group,
5426
+ items: await Promise.all(value.raws.map((raw) => candidateItem(raw.targetUserId, raw))),
5427
+ total: value.total,
5428
+ ...value.error === void 0 ? {} : { error: value.error }
5429
+ })));
5430
+ return {
5431
+ source: await this.source.sourceItem(source),
5432
+ items,
5433
+ total: rawCandidates.size,
5434
+ hasMore: rawCandidates.size > items.length,
5435
+ mode: numberValue$12(preview.join_mode) === 2 ? "approval_invite" : "direct_add",
5436
+ groups,
5437
+ groupCandidates,
5438
+ contactCount: [...rawCandidates.values()].filter((item) => item.relation === "contact").length,
5439
+ strangerCount: [...rawCandidates.values()].filter((item) => item.relation === "stranger").length
5440
+ };
5441
+ }
5442
+ async addGroupMembers(sourceRef, candidateRefs, signal) {
5443
+ const session = await this.runtime.requireSession();
5444
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
5445
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-source-invalid", "仅支持向群聊添加成员", false);
5446
+ const refs = candidateRefs.map((value) => value.trim());
5447
+ if (refs.length === 0 || refs.length > 20 || refs.some((value) => value === "") || new Set(refs).size !== refs.length) throw new ArkmePluginError("group-member-candidates-invalid", "请选择 1-20 个不重复的成员候选项", false);
5448
+ const preview = objectValue$1(objectValue$1(await this.runtime.authenticatedChatPost("/api/v1/chats/governance/invite-preview", { chat_session_uid: source.ownerRef }, session, signal)).preview);
5449
+ const approvalRequired = numberValue$12(preview.join_mode) === 2;
5450
+ const inviteLink = stringValue$1(preview.invite_link).trim();
5451
+ const results = [];
5452
+ for (const candidateRef of refs) {
5453
+ let candidate;
5454
+ try {
5455
+ candidate = await this.openCandidateRef(candidateRef, session.userId, source.ownerRef);
5456
+ if (approvalRequired) {
5457
+ if (candidate.privateChatSessionUid === void 0) throw new ArkmePluginError("group-candidate-invite-unavailable", "该对象暂时无法发送群聊邀请", false);
5458
+ if (this.inviteSender === void 0 || inviteLink === "") throw new ArkmePluginError("group-invite-unavailable", "该群需要发送邀请,但邀请链接暂不可用", true);
5459
+ const privateSourceRef = await this.source.sealSourceRef(session.userId, "private_chat", candidate.privateChatSessionUid, candidate.displayName);
5460
+ await this.inviteSender.sendPrivateText(privateSourceRef, candidate.privateChatSessionUid, `邀请你加入群聊“${source.displayName}”:${inviteLink}`, `chat_record_${randomUUID()}`, `chat_rel_${randomUUID()}`, session, signal);
5461
+ results.push({
5462
+ candidateRef,
5463
+ displayName: candidate.displayName,
5464
+ status: "invite_sent"
5465
+ });
5466
+ continue;
5467
+ }
5468
+ const outcome = stringValue$1(objectValue$1((await this.runtime.authenticatedChatPost("/api/v1/chats/members/add", {
5469
+ chat_session_uid: source.ownerRef,
5470
+ target_user_id: candidate.targetUserId,
5471
+ join_at: Date.now(),
5472
+ display_name_snapshot: candidate.displayName,
5473
+ extra: { source: "dsh_arkme_candidate" }
5474
+ }, session, signal)).item).outcome);
5475
+ results.push({
5476
+ candidateRef,
5477
+ displayName: candidate.displayName,
5478
+ status: outcome === "idempotent_hit" ? "already_member" : outcome === "updated" ? "reactivated" : "added"
5479
+ });
5480
+ } catch (error) {
5481
+ results.push({
5482
+ candidateRef,
5483
+ displayName: candidate?.displayName ?? "成员",
5484
+ status: "failed",
5485
+ error: error instanceof Error && error.message.trim() !== "" ? error.message : "添加成员失败"
5486
+ });
5487
+ }
5488
+ }
5489
+ return {
5490
+ source: await this.source.sourceItem(source),
5491
+ results,
5492
+ succeededCount: results.filter((item) => item.status !== "failed").length,
5493
+ failedCount: results.filter((item) => item.status === "failed").length
5494
+ };
5495
+ }
5496
+ async sealCandidateRef(payload) {
5497
+ const encoded = encodeOpaqueJson$4(payload);
5498
+ return `arkme-group-candidate-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
5499
+ }
5500
+ async openCandidateRef(candidateRef, expectedUserId, expectedGroupChatSessionUid) {
5501
+ const parts = candidateRef.split(".");
5502
+ if (parts.length !== 3 || parts[0] !== "arkme-group-candidate-v1") throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
5503
+ const encoded = parts[1] ?? "";
5504
+ const supplied = Buffer.from(parts[2] ?? "", "base64url");
5505
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
5506
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
5507
+ let raw;
5508
+ try {
5509
+ raw = objectValue$1(decodeOpaqueJson$4(encoded));
5510
+ } catch (error) {
5511
+ throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false, 400, { cause: error });
5512
+ }
5513
+ const result = {
5514
+ version: 1,
5515
+ userId: numberValue$12(raw.userId),
5516
+ groupChatSessionUid: stringValue$1(raw.groupChatSessionUid).trim(),
5517
+ targetUserId: numberValue$12(raw.targetUserId),
5518
+ displayName: stringValue$1(raw.displayName).trim(),
5519
+ ...stringValue$1(raw.privateChatSessionUid).trim() === "" ? {} : { privateChatSessionUid: stringValue$1(raw.privateChatSessionUid).trim() },
5520
+ origin: raw.origin === "group_chat" ? "group_chat" : "private_chat"
5521
+ };
5522
+ if (raw.version !== 1 || result.userId !== expectedUserId || result.groupChatSessionUid !== expectedGroupChatSessionUid || !Number.isSafeInteger(result.targetUserId) || result.targetUserId <= 0 || result.targetUserId === expectedUserId || result.displayName === "") throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用与当前账号不匹配", false, 403);
5523
+ return result;
5524
+ }
5525
+ async createGroup(title, clientMutationId, options = {}) {
5526
+ const normalizedTitle = title.trim();
5527
+ if (normalizedTitle === "" || Array.from(normalizedTitle).length > 80) throw new ArkmePluginError("group-title-invalid", "群聊名称需为 1-80 个字符", false);
5528
+ if (!UUID.test(clientMutationId)) throw new ArkmePluginError("client-mutation-id-invalid", "群聊操作标识无效", false);
5529
+ const session = await this.runtime.requireSession();
5530
+ let data;
5531
+ try {
5532
+ data = await this.runtime.authenticatedChatPost("/api/v1/chats/create-group", {
5533
+ chat_session_uid: clientMutationId,
5534
+ title: normalizedTitle,
5535
+ member_user_ids: [],
5536
+ create_at: Date.now()
5537
+ }, session, options.signal);
5538
+ } catch (error) {
5539
+ if (error instanceof ArkmePluginError && ["arkme-network-error", "arkme-timeout"].includes(error.code)) throw new ArkmePluginError("group-create-outcome-unknown", "群聊创建结果未知,请刷新会话列表确认;不会自动重试", false, 409, { cause: error });
5540
+ throw error;
5541
+ }
5542
+ const source = await this.source.chatSourceFromBundle(data, session, void 0, []);
5543
+ this.source.setChatSourceByKey(`${String(session.userId)}:${clientMutationId}`, source);
5544
+ return source;
4692
5545
  }
4693
5546
  async listGroupMembers(sourceRef, options = {}) {
4694
5547
  const session = await this.runtime.requireSession();
@@ -4731,7 +5584,8 @@ var GroupService = class {
4731
5584
  status,
4732
5585
  isSelf: userId === session.userId,
4733
5586
  isOwner: role === "owner",
4734
- joinedAtMillis: numberValue$12(item.join_at)
5587
+ joinedAtMillis: numberValue$12(item.join_at),
5588
+ recordCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).record_count)))
4735
5589
  });
4736
5590
  }
4737
5591
  const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
@@ -5174,6 +6028,33 @@ function md5Text(value) {
5174
6028
  function allowedSignedImageHost$2(environment, hostname) {
5175
6029
  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());
5176
6030
  }
6031
+ function allowedSignedAudioHost(environment, hostname) {
6032
+ return (environment === "prod" ? ["jotmo-useraudio.oss-cn-hangzhou.aliyuncs.com"] : ["jotmo-useraudio-test.oss-cn-hangzhou.aliyuncs.com"]).includes(hostname.toLowerCase());
6033
+ }
6034
+ function trustedWorldVoiceprintAudioUrl(environment, raw) {
6035
+ let parsed;
6036
+ try {
6037
+ parsed = new URL(raw.trim());
6038
+ } catch (error) {
6039
+ throw new ArkmePluginError("world-voiceprint-audio-invalid", "世界声纹音频地址无效", true, 502, { cause: error });
6040
+ }
6041
+ const signature = parsed.searchParams.get("x-oss-signature")?.trim() ?? "";
6042
+ if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.hash !== "" || !allowedSignedAudioHost(environment, parsed.hostname) || parsed.pathname.replace(/^\/+/, "") === "" || signature === "") throw new ArkmePluginError("world-voiceprint-audio-rejected", "世界声纹音频来源不受信任", false, 502);
6043
+ return parsed;
6044
+ }
6045
+ function worldVoiceprintFileName(mimeType) {
6046
+ const extension = {
6047
+ wav: "wav",
6048
+ "x-wav": "wav",
6049
+ mpeg: "mp3",
6050
+ mp4: "m4a",
6051
+ aac: "aac",
6052
+ ogg: "ogg",
6053
+ webm: "webm",
6054
+ flac: "flac"
6055
+ }[mimeType.split(";", 1)[0].trim().toLowerCase().replace(/^audio\//, "")];
6056
+ return extension === void 0 ? "世界声纹" : `世界声纹.${extension}`;
6057
+ }
5177
6058
  function trustedSignedImageUrl$1(environment, raw) {
5178
6059
  let parsed;
5179
6060
  try {
@@ -5234,6 +6115,7 @@ var MediaService = class {
5234
6115
  worldImages;
5235
6116
  recordIdentity;
5236
6117
  mediaRefs = /* @__PURE__ */ new Map();
6118
+ stableMediaRefs = /* @__PURE__ */ new Map();
5237
6119
  imageCache = /* @__PURE__ */ new Map();
5238
6120
  imageInFlight = /* @__PURE__ */ new Map();
5239
6121
  imageCacheBytes = 0;
@@ -5247,6 +6129,7 @@ var MediaService = class {
5247
6129
  }
5248
6130
  dispose() {
5249
6131
  this.mediaRefs.clear();
6132
+ this.stableMediaRefs.clear();
5250
6133
  this.imageCache.clear();
5251
6134
  this.imageInFlight.clear();
5252
6135
  this.imageCacheBytes = 0;
@@ -5353,18 +6236,20 @@ var MediaService = class {
5353
6236
  throw error;
5354
6237
  }
5355
6238
  }
5356
- async fetchMedia(mediaRef, range) {
6239
+ async fetchMedia(mediaRef, range, signal) {
5357
6240
  const session = await this.runtime.requireSession();
5358
6241
  const descriptor = this.mediaRefs.get(mediaRef);
5359
6242
  if (descriptor === void 0 || descriptor.viewerUserId !== session.userId || descriptor.expiresAtMillis <= Date.now()) {
5360
6243
  this.mediaRefs.delete(mediaRef);
6244
+ if (descriptor?.stableKey !== void 0) this.stableMediaRefs.delete(descriptor.stableKey);
5361
6245
  throw new ArkmePluginError("media-ref-invalid", "媒体引用已失效,请刷新对话后重试", false, 404);
5362
6246
  }
5363
6247
  const url = new URL(descriptor.remoteUrl);
5364
- if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || !allowedSignedImageHost$2(this.runtime.config.environment, url.hostname)) throw new ArkmePluginError("media-host-rejected", "媒体来源不受信任", false, 403);
6248
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || !(allowedSignedImageHost$2(this.runtime.config.environment, url.hostname) || allowedSignedAudioHost(this.runtime.config.environment, url.hostname))) throw new ArkmePluginError("media-host-rejected", "媒体来源不受信任", false, 403);
5365
6249
  const response = await this.runtime.fetchImpl(url, {
5366
6250
  headers: range === void 0 ? {} : { Range: range },
5367
- redirect: "error"
6251
+ redirect: "error",
6252
+ ...signal === void 0 ? {} : { signal }
5368
6253
  });
5369
6254
  if (!response.ok && response.status !== 206) throw new ArkmePluginError("media-fetch-failed", `媒体读取失败(${String(response.status)})`, true, 502);
5370
6255
  return {
@@ -5372,6 +6257,17 @@ var MediaService = class {
5372
6257
  descriptor
5373
6258
  };
5374
6259
  }
6260
+ issueWorldVoiceprintMediaRef(viewerUserId, input) {
6261
+ const remoteUrl = trustedWorldVoiceprintAudioUrl(this.runtime.config.environment, input.remoteUrl).toString();
6262
+ const mimeType = input.mimeType.trim() || "audio/wav";
6263
+ if (!mimeType.startsWith("audio/")) throw new ArkmePluginError("world-voiceprint-mime-invalid", "世界声纹音频格式无效", true, 502);
6264
+ return this.issueMediaRef(viewerUserId, {
6265
+ remoteUrl,
6266
+ mimeType,
6267
+ fileName: worldVoiceprintFileName(mimeType),
6268
+ size: 0
6269
+ });
6270
+ }
5375
6271
  async readImage(imageRef, options = {}) {
5376
6272
  const session = await this.runtime.requireSession();
5377
6273
  const maximumBytes = imageRef.trim().startsWith("arkme-profile-image-v1.") ? MAX_ARKME_PROFILE_IMAGE_BYTES : MAX_ARKME_IMAGE_BYTES;
@@ -5434,6 +6330,11 @@ var MediaService = class {
5434
6330
  }
5435
6331
  }
5436
6332
  async readImageUncached(session, imageRef, byteLimit, signal) {
6333
+ if (imageRef.trim().startsWith("arkme-media-v1.")) {
6334
+ const { response, descriptor } = await this.fetchMedia(imageRef.trim(), void 0, signal);
6335
+ if (!descriptor.mimeType.trim().toLowerCase().startsWith("image/")) throw new ArkmePluginError("image-type-unsupported", "该媒体引用不是图片", false, 415);
6336
+ return await this.imageBytesFromResponse(response, byteLimit);
6337
+ }
5437
6338
  if (imageRef.trim().startsWith("arkme-profile-image-v1.")) {
5438
6339
  const reference = await this.profile.openProfileImageRef(imageRef, session.userId);
5439
6340
  if (reference.targetUserId === session.userId) {
@@ -5563,20 +6464,33 @@ var MediaService = class {
5563
6464
  unavailableRecordUids
5564
6465
  };
5565
6466
  }
5566
- issueMediaRef(viewerUserId, descriptor) {
6467
+ issueImageMediaRef(viewerUserId, descriptor, stableIdentity) {
6468
+ return this.issueMediaRef(viewerUserId, descriptor, stableIdentity);
6469
+ }
6470
+ issueMediaRef(viewerUserId, descriptor, stableIdentity) {
5567
6471
  const now = Date.now();
5568
- for (const [key, value] of this.mediaRefs) if (value.expiresAtMillis <= now) this.mediaRefs.delete(key);
6472
+ for (const [key, value] of this.mediaRefs) if (value.expiresAtMillis <= now) {
6473
+ this.mediaRefs.delete(key);
6474
+ if (value.stableKey !== void 0) this.stableMediaRefs.delete(value.stableKey);
6475
+ }
5569
6476
  while (this.mediaRefs.size >= 2e3) {
5570
6477
  const oldest = this.mediaRefs.keys().next().value;
5571
6478
  if (oldest === void 0) break;
6479
+ const oldestDescriptor = this.mediaRefs.get(oldest);
5572
6480
  this.mediaRefs.delete(oldest);
6481
+ if (oldestDescriptor?.stableKey !== void 0) this.stableMediaRefs.delete(oldestDescriptor.stableKey);
5573
6482
  }
5574
- const ref = `arkme-media-v1.${randomUUID()}`;
6483
+ const stableKey = stableIdentity === void 0 ? void 0 : createHash("sha256").update(`${String(viewerUserId)}\0${stableIdentity}`).digest("base64url");
6484
+ const ref = (stableKey === void 0 ? void 0 : this.stableMediaRefs.get(stableKey)) ?? `arkme-media-v1.${randomUUID()}`;
6485
+ this.mediaRefs.delete(ref);
6486
+ const lifetimeMillis = stableKey === void 0 ? 30 * 6e4 : 1440 * 6e4;
5575
6487
  this.mediaRefs.set(ref, {
5576
6488
  ...descriptor,
5577
6489
  viewerUserId,
5578
- expiresAtMillis: now + 30 * 6e4
6490
+ expiresAtMillis: now + lifetimeMillis,
6491
+ ...stableKey === void 0 ? {} : { stableKey }
5579
6492
  });
6493
+ if (stableKey !== void 0) this.stableMediaRefs.set(stableKey, ref);
5580
6494
  return ref;
5581
6495
  }
5582
6496
  richContentBlocks(raw, viewerUserId, hydratedDisplayItems = []) {
@@ -5619,7 +6533,7 @@ var MediaService = class {
5619
6533
  mimeType,
5620
6534
  fileName,
5621
6535
  size: Math.max(0, Math.trunc(numberValue$10(item.size)))
5622
- }),
6536
+ }, kind === "image" && fileAssetUid !== "" ? fileAssetUid : void 0),
5623
6537
  ...fileAssetUid === "" ? {} : { fileAssetUid },
5624
6538
  fileName,
5625
6539
  mimeType,
@@ -5663,36 +6577,7 @@ var MediaService = class {
5663
6577
  signal: controller.signal
5664
6578
  });
5665
6579
  if (!response.ok) throw new ArkmePluginError("image-download-failed", `Arkme 图片读取返回 HTTP ${response.status}`, true, 502);
5666
- const declaredLength = Number(response.headers.get("content-length") ?? 0);
5667
- if (Number.isFinite(declaredLength) && declaredLength > byteLimit) throw new ArkmePluginError("image-too-large", "Arkme 图片超过读取大小限制", false, 413);
5668
- if (response.body === null) throw new ArkmePluginError("image-response-empty", "Arkme 图片响应为空", true, 502);
5669
- const chunks = [];
5670
- let bytes = 0;
5671
- const reader = response.body.getReader();
5672
- while (true) {
5673
- const next = await reader.read();
5674
- if (next.done) break;
5675
- bytes += next.value.byteLength;
5676
- if (bytes > byteLimit) {
5677
- await reader.cancel();
5678
- throw new ArkmePluginError("image-too-large", "Arkme 图片超过读取大小限制", false, 413);
5679
- }
5680
- chunks.push(next.value);
5681
- }
5682
- if (bytes === 0) throw new ArkmePluginError("image-response-empty", "Arkme 图片响应为空", true, 502);
5683
- const data = new Uint8Array(bytes);
5684
- let offset = 0;
5685
- for (const chunk of chunks) {
5686
- data.set(chunk, offset);
5687
- offset += chunk.byteLength;
5688
- }
5689
- const mediaType = imageMediaType(data);
5690
- if (mediaType === void 0) throw new ArkmePluginError("image-type-unsupported", "Arkme 图片不是受支持的 PNG、JPEG、WebP 或 GIF", false, 415);
5691
- return {
5692
- mediaType,
5693
- bytes,
5694
- data
5695
- };
6580
+ return await this.imageBytesFromResponse(response, byteLimit);
5696
6581
  } catch (error) {
5697
6582
  if (error instanceof ArkmePluginError) throw error;
5698
6583
  if (error.name === "AbortError") throw new ArkmePluginError("image-download-timeout", "Arkme 图片读取超时或已取消", true, 504, { cause: error });
@@ -5702,6 +6587,38 @@ var MediaService = class {
5702
6587
  signal?.removeEventListener("abort", abort);
5703
6588
  }
5704
6589
  }
6590
+ async imageBytesFromResponse(response, byteLimit) {
6591
+ const declaredLength = Number(response.headers.get("content-length") ?? 0);
6592
+ if (Number.isFinite(declaredLength) && declaredLength > byteLimit) throw new ArkmePluginError("image-too-large", "Arkme 图片超过读取大小限制", false, 413);
6593
+ if (response.body === null) throw new ArkmePluginError("image-response-empty", "Arkme 图片响应为空", true, 502);
6594
+ const chunks = [];
6595
+ let bytes = 0;
6596
+ const reader = response.body.getReader();
6597
+ while (true) {
6598
+ const next = await reader.read();
6599
+ if (next.done) break;
6600
+ bytes += next.value.byteLength;
6601
+ if (bytes > byteLimit) {
6602
+ await reader.cancel();
6603
+ throw new ArkmePluginError("image-too-large", "Arkme 图片超过读取大小限制", false, 413);
6604
+ }
6605
+ chunks.push(next.value);
6606
+ }
6607
+ if (bytes === 0) throw new ArkmePluginError("image-response-empty", "Arkme 图片响应为空", true, 502);
6608
+ const data = new Uint8Array(bytes);
6609
+ let offset = 0;
6610
+ for (const chunk of chunks) {
6611
+ data.set(chunk, offset);
6612
+ offset += chunk.byteLength;
6613
+ }
6614
+ const mediaType = imageMediaType(data);
6615
+ if (mediaType === void 0) throw new ArkmePluginError("image-type-unsupported", "Arkme 图片不是受支持的 PNG、JPEG、WebP 或 GIF", false, 415);
6616
+ return {
6617
+ mediaType,
6618
+ bytes,
6619
+ data
6620
+ };
6621
+ }
5705
6622
  };
5706
6623
  //#endregion
5707
6624
  //#region src/services/outgoing-call-service.ts
@@ -6615,7 +7532,7 @@ function displayTimestamp(value) {
6615
7532
  const parsed = Date.parse(stringValue(value));
6616
7533
  return Number.isFinite(parsed) ? parsed : 0;
6617
7534
  }
6618
- function projectRecordingTranscripts(response, speakerResponse) {
7535
+ function projectRecordingTranscripts(response, speakerResponse, source = "system") {
6619
7536
  const data = objectValue(response);
6620
7537
  const sessions = /* @__PURE__ */ new Map();
6621
7538
  const sessionSpeakerColorIndexes = /* @__PURE__ */ new Map();
@@ -6659,7 +7576,32 @@ function projectRecordingTranscripts(response, speakerResponse) {
6659
7576
  const sessionSpeakers = listValue$6(session.spk_ls ?? session.speakers).map(objectValue);
6660
7577
  const childOffset = numberValue$6(child.start_at);
6661
7578
  const childStart = childOffset >= 1e11 ? childOffset : numberValue$6(session.start_at) + childOffset;
6662
- const rows = listValue$6(child.asr);
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
+ }
6663
7605
  for (let index = 0; index < rows.length; index += 1) {
6664
7606
  const row = objectValue(rows[index]);
6665
7607
  const isBackground = numberValue$6(row.b ?? row.background) === 1 || row.background === true;
@@ -6673,15 +7615,15 @@ function projectRecordingTranscripts(response, speakerResponse) {
6673
7615
  const persistentSpeakerNumber = optionalNumberValue(sessionSpeaker.speaker_display_number ?? sessionSpeaker.speakerDisplayNumber);
6674
7616
  const speakerNumber = persistentSpeakerNumber !== void 0 && persistentSpeakerNumber > 0 ? persistentSpeakerNumber : rawSpeakerNumber;
6675
7617
  const speakerColorIndex = sessionSpeakerColorIndexes.get(`${sessionId}:${rawSpeakerNumber}`) ?? Math.max(0, rawSpeakerNumber);
6676
- const speakerLabel = speakerNumber >= 0 ? `说话人 ${speakerNumber}` : "未知说话人";
7618
+ const speakerLabel = source === "doubao" ? rawSpeakerNumber >= 0 ? `豆包说话人 ${rawSpeakerNumber + 1}` : "豆包说话人" : speakerNumber >= 0 ? `说话人 ${speakerNumber}` : "未知说话人";
6677
7619
  const startOffset = numberValue$6(row.s ?? row.start_at);
6678
7620
  const endOffset = Math.max(startOffset, numberValue$6(row.e ?? row.end_at));
6679
7621
  projected.push({
6680
- itemId: `${childId || sessionId}:${index}`,
7622
+ itemId: source === "system" ? `${childId || sessionId}:${index}` : `${childId || sessionId}:doubao:${index}`,
6681
7623
  sessionId,
6682
7624
  childId,
6683
7625
  asrItemIndex: index,
6684
- transcriptSource: "system",
7626
+ transcriptSource: source,
6685
7627
  startAtMillis: childStart + startOffset,
6686
7628
  endAtMillis: childStart + endOffset,
6687
7629
  speakerNumber,
@@ -6690,6 +7632,7 @@ function projectRecordingTranscripts(response, speakerResponse) {
6690
7632
  isSelf,
6691
7633
  isBackground,
6692
7634
  text,
7635
+ ...source === "doubao" ? { transcriptStatus: "ready" } : {},
6693
7636
  sourceIndex
6694
7637
  });
6695
7638
  sourceIndex += 1;
@@ -6936,7 +7879,21 @@ var RecordingService = class {
6936
7879
  days
6937
7880
  };
6938
7881
  }
6939
- async recordingTranscript(dateStamp, signal) {
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) {
6940
7897
  const dayStart = this.recordingDayStart(dateStamp);
6941
7898
  const date = dayStart.getTime();
6942
7899
  const session = await this.runtime.requireSession();
@@ -6948,14 +7905,29 @@ var RecordingService = class {
6948
7905
  let totalDurationMillis = 0;
6949
7906
  for (const rawSession of listValue$5(transcriptResult.value.session_ls)) totalDurationMillis += Math.max(0, numberValue$5(objectValue$1(rawSession).duration));
6950
7907
  const speakerData = speakerResult.status === "fulfilled" ? listValue$5(speakerResult.value.spk_ls) : [];
6951
- const items = projectRecordingTranscripts(transcriptResult.value, speakerData);
6952
- return {
6953
- state: items.length > 0 ? "ready" : "empty",
6954
- items,
6955
- message: items.length > 0 ? "" : "当天无录音",
6956
- identityCoverage: speakerResult.status === "fulfilled" ? "complete" : "partial",
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,
6957
7916
  totalDurationMillis
6958
7917
  };
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
+ };
6959
7931
  }
6960
7932
  async recordingProjection(dateStamp, kind, signal) {
6961
7933
  const dayStart = this.recordingDayStart(dateStamp);
@@ -6993,6 +7965,7 @@ var RecordingService = class {
6993
7965
  version: 1,
6994
7966
  dateStamp: numberValue$5(raw.dateStamp),
6995
7967
  content: content === "summary" || content === "timeline" ? content : "transcript",
7968
+ ...content === "transcript" && (raw.transcriptSource === "system" || raw.transcriptSource === "doubao") ? { transcriptSource: raw.transcriptSource } : {},
6996
7969
  itemOffset: numberValue$5(raw.itemOffset),
6997
7970
  textOffset: numberValue$5(raw.textOffset),
6998
7971
  fingerprint: stringValue$1(raw.fingerprint),
@@ -7002,25 +7975,31 @@ var RecordingService = class {
7002
7975
  "transcript",
7003
7976
  "summary",
7004
7977
  "timeline"
7005
- ].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);
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);
7006
7979
  return payload;
7007
7980
  }
7008
7981
  async recordingDay(dateStamp) {
7009
7982
  const date = this.recordingDayStart(dateStamp).getTime();
7010
7983
  const [transcriptResult, summaryResult, timelineResult] = await Promise.allSettled([
7011
- this.recordingTranscript(date),
7984
+ this.recordingTranscriptSections(date),
7012
7985
  this.recordingProjection(date, "summary"),
7013
7986
  this.recordingProjection(date, "timeline")
7014
7987
  ]);
7015
- const transcript = transcriptResult.status === "fulfilled" ? transcriptResult.value : {
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 : {
7016
7994
  state: "error",
7017
7995
  items: [],
7018
7996
  message: safeFailureMessage$2(transcriptResult.reason)
7019
7997
  };
7020
7998
  return {
7021
7999
  dateStamp: date,
7022
- totalDurationMillis: transcriptResult.status === "fulfilled" ? transcriptResult.value.totalDurationMillis : 0,
8000
+ totalDurationMillis: transcriptResult.status === "fulfilled" ? transcriptResult.value.system.totalDurationMillis : 0,
7023
8001
  transcript,
8002
+ doubaoTranscript,
7024
8003
  summary: summaryResult.status === "fulfilled" ? summaryResult.value : {
7025
8004
  state: "error",
7026
8005
  items: [],
@@ -7248,9 +8227,11 @@ function listValue$3(value) {
7248
8227
  var SearchService = class {
7249
8228
  runtime;
7250
8229
  record;
7251
- constructor(runtime, record) {
8230
+ media;
8231
+ constructor(runtime, record, media) {
7252
8232
  this.runtime = runtime;
7253
8233
  this.record = record;
8234
+ this.media = media;
7254
8235
  }
7255
8236
  async searchRecords(options) {
7256
8237
  const query = options.query.trim();
@@ -7326,6 +8307,85 @@ var SearchService = class {
7326
8307
  }, session, options.signal);
7327
8308
  return this.recordSearchResult(data);
7328
8309
  }
8310
+ /** Build the desktop image library from the mixed image/video scene. */
8311
+ async searchImages(options) {
8312
+ const session = await this.runtime.requireSession();
8313
+ const pageLimit = Math.min(50, Math.max(1, Math.trunc(options.limit)));
8314
+ const seenCursors = /* @__PURE__ */ new Set();
8315
+ let cursor = options.cursor?.trim() ?? "";
8316
+ if (cursor !== "") seenCursors.add(cursor);
8317
+ let lastPage;
8318
+ for (let pageIndex = 0; pageIndex < 8; pageIndex += 1) {
8319
+ const page = await this.searchScene({
8320
+ scene: "image_video",
8321
+ limit: pageLimit,
8322
+ ...cursor === "" ? {} : { cursor },
8323
+ ...options.signal === void 0 ? {} : { signal: options.signal }
8324
+ });
8325
+ lastPage = page;
8326
+ const candidates = page.items.flatMap((record) => record.media.flatMap((asset) => {
8327
+ const mimeType = asset.mimeType?.trim().toLowerCase() ?? "";
8328
+ return mimeType.startsWith("image/") || mimeType === "" && (asset.fileKind === void 0 || asset.fileKind === 1) ? [{
8329
+ record,
8330
+ asset
8331
+ }] : [];
8332
+ }));
8333
+ const uniqueAssetUids = [...new Set(candidates.map((candidate) => candidate.asset.fileAssetUid))];
8334
+ const displayItems = [];
8335
+ for (let offset = 0; offset < uniqueAssetUids.length; offset += 50) displayItems.push(...await this.media.queryFileAssets(uniqueAssetUids.slice(offset, offset + 50), options.signal));
8336
+ const displayByUid = new Map(displayItems.map((item) => [item.fileAssetUid, item]));
8337
+ const emitted = /* @__PURE__ */ new Set();
8338
+ const items = candidates.flatMap(({ record, asset }) => {
8339
+ const itemIdentity = `${record.recordUid}\0${asset.fileAssetUid}`;
8340
+ if (emitted.has(itemIdentity)) return [];
8341
+ const display = displayByUid.get(asset.fileAssetUid);
8342
+ if (display === void 0) return [];
8343
+ const mimeType = (display.mimeType ?? asset.mimeType ?? "").trim().toLowerCase();
8344
+ if (!mimeType.startsWith("image/")) return [];
8345
+ const remoteUrl = display.previewUrl ?? display.downloadUrl;
8346
+ if (remoteUrl === void 0) return [];
8347
+ emitted.add(itemIdentity);
8348
+ const fileName = display.fileName ?? asset.fileName ?? "图片";
8349
+ return [{
8350
+ itemKey: createHash("sha256").update(itemIdentity).digest("base64url"),
8351
+ mediaRef: this.media.issueImageMediaRef(session.userId, {
8352
+ remoteUrl,
8353
+ mimeType: mimeType || "application/octet-stream",
8354
+ fileName,
8355
+ size: Math.max(0, Math.trunc(asset.size ?? 0))
8356
+ }, asset.fileAssetUid),
8357
+ recordUid: record.recordUid,
8358
+ sendAtMillis: record.sendAtMillis,
8359
+ fileName,
8360
+ mimeType: mimeType || "application/octet-stream",
8361
+ size: Math.max(0, Math.trunc(asset.size ?? 0)),
8362
+ recordTitle: record.title || record.nickname || "快记",
8363
+ ...record.sourceTitle === void 0 ? {} : { sourceTitle: record.sourceTitle }
8364
+ }];
8365
+ });
8366
+ const nextCursor = page.nextCursor?.trim() ?? "";
8367
+ const canContinue = page.hasMore && nextCursor !== "" && !seenCursors.has(nextCursor);
8368
+ if (items.length > 0 || !canContinue) return {
8369
+ items,
8370
+ hasMore: canContinue,
8371
+ ...canContinue ? { nextCursor } : {},
8372
+ queryGuard: page.queryGuard
8373
+ };
8374
+ if (pageIndex === 7) return {
8375
+ items: [],
8376
+ hasMore: true,
8377
+ nextCursor,
8378
+ queryGuard: page.queryGuard
8379
+ };
8380
+ seenCursors.add(nextCursor);
8381
+ cursor = nextCursor;
8382
+ }
8383
+ return {
8384
+ items: [],
8385
+ hasMore: false,
8386
+ queryGuard: lastPage?.queryGuard ?? { state: "complete" }
8387
+ };
8388
+ }
7329
8389
  async searchRecordings(options) {
7330
8390
  const query = options.query.trim();
7331
8391
  if (query === "") throw new ArkmePluginError("recording-query-empty", "搜索关键词不能为空", false);
@@ -8372,6 +9432,8 @@ const ARKME_WORLD_IMAGE_REF_TTL_MILLIS = 900 * 1e3;
8372
9432
  const MAX_ARKME_WORLD_IMAGE_REFS = 2048;
8373
9433
  const ARKME_WORLD_RECORD_REF_TTL_MILLIS = 900 * 1e3;
8374
9434
  const MAX_ARKME_WORLD_RECORD_REFS = 4096;
9435
+ const ARKME_VOICEPRINT_PLAY_SCOPE = 2;
9436
+ const ARKME_VOICEPRINT_INVITE_TOKEN_MAX_LENGTH = 2048;
8375
9437
  function numberValue(value) {
8376
9438
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
8377
9439
  }
@@ -8383,6 +9445,27 @@ function safeFailureMessage(error) {
8383
9445
  if (error instanceof Error && error.message.trim() !== "") return error.message;
8384
9446
  return "未知错误";
8385
9447
  }
9448
+ function optionalTrimmedText(value, limit) {
9449
+ const normalized = value?.replace(/\s+/g, " ").trim();
9450
+ if (normalized === void 0 || normalized === "") return void 0;
9451
+ return normalized.length <= limit ? normalized : `${normalized.slice(0, limit).trimEnd()}...`;
9452
+ }
9453
+ function buildVoiceprintInviteShareUrl(environment, token) {
9454
+ const url = new URL("/app/voiceprint/invite", environment === "prod" ? "https://jiwo.cc" : "https://jotmo-app.senguo.me");
9455
+ url.hash = `t=${encodeURIComponent(token)}`;
9456
+ return url.toString();
9457
+ }
9458
+ function validVoiceprintInviteToken(token) {
9459
+ if (token === "" || token.length > ARKME_VOICEPRINT_INVITE_TOKEN_MAX_LENGTH) return false;
9460
+ let byteLength = 0;
9461
+ for (const char of token) {
9462
+ const codePoint = char.codePointAt(0) ?? 0;
9463
+ if (codePoint <= 31 || codePoint === 127) return false;
9464
+ byteLength += codePoint <= 127 ? 1 : codePoint <= 2047 ? 2 : codePoint <= 65535 ? 3 : 4;
9465
+ if (byteLength > ARKME_VOICEPRINT_INVITE_TOKEN_MAX_LENGTH) return false;
9466
+ }
9467
+ return true;
9468
+ }
8386
9469
  function worldVisibility(checkStatus) {
8387
9470
  if (checkStatus === 1) return "pending_review";
8388
9471
  if (checkStatus === 4) return "rejected";
@@ -8485,6 +9568,110 @@ var WorldService = class {
8485
9568
  ...hasMore ? { nextOffset } : {}
8486
9569
  };
8487
9570
  }
9571
+ async listMyWorldFeed(options = {}) {
9572
+ const session = await this.runtime.requireSession();
9573
+ 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;
9585
+ return {
9586
+ items,
9587
+ total,
9588
+ hasMore,
9589
+ ...hasMore ? { nextOffset } : {}
9590
+ };
9591
+ }
9592
+ async worldVoiceprintPlaybackAvailability(recordRefs, signal) {
9593
+ const session = await this.runtime.requireSession();
9594
+ const normalizedRefs = [...new Set(recordRefs.map((value) => value.trim()).filter((value) => value !== ""))].slice(0, 20);
9595
+ if (normalizedRefs.length === 0) return { items: [] };
9596
+ const entries = normalizedRefs.map((recordRef) => ({
9597
+ recordRef,
9598
+ entry: this.openWorldRecordRef(recordRef, session.userId)
9599
+ }));
9600
+ const ownerUserIds = [...new Set(entries.map((value) => value.entry.ownerUserId ?? 0).filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
9601
+ if (ownerUserIds.length === 0) return { items: entries.map(({ recordRef }) => ({
9602
+ recordRef,
9603
+ playable: false
9604
+ })) };
9605
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/world-playback-availability", { user_ids: ownerUserIds }, session, signal, {
9606
+ key: ownerUserIds.join(","),
9607
+ cacheMs: 5e3
9608
+ });
9609
+ const playableByOwner = /* @__PURE__ */ new Map();
9610
+ for (const raw of listValue(data.items)) {
9611
+ const item = objectValue$1(raw);
9612
+ const userId = Math.trunc(numberValue(item.user_id));
9613
+ if (userId > 0) playableByOwner.set(userId, item.playable === true);
9614
+ }
9615
+ return { items: entries.map(({ recordRef, entry }) => ({
9616
+ recordRef,
9617
+ playable: playableByOwner.get(entry.ownerUserId ?? 0) === true
9618
+ })) };
9619
+ }
9620
+ async generateWorldVoiceprintPlayback(input) {
9621
+ const session = await this.runtime.requireSession();
9622
+ const entry = this.openWorldRecordRef(input.recordRef, session.userId);
9623
+ const chunkIndex = Math.trunc(input.chunkIndex ?? 0);
9624
+ if (!Number.isSafeInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= 334) throw new ArkmePluginError("world-voiceprint-playback-input-invalid", "世界声纹播放参数无效,请刷新后重试", false);
9625
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/generate-playback", {
9626
+ source_scene: 4,
9627
+ source_id: entry.recordUid,
9628
+ source_chunk_index: chunkIndex
9629
+ }, session, input.signal, {
9630
+ lane: "write",
9631
+ bypassCache: true
9632
+ });
9633
+ const mimeType = stringValue$1(data.mime_type).trim() || "audio/wav";
9634
+ const responseChunkIndex = Math.trunc(numberValue(data.source_chunk_index));
9635
+ const chunkCount = Math.trunc(numberValue(data.source_chunk_count));
9636
+ const chunkStartRune = Math.trunc(numberValue(data.source_chunk_start_rune));
9637
+ const chunkEndRune = Math.trunc(numberValue(data.source_chunk_end_rune));
9638
+ if (responseChunkIndex !== chunkIndex || chunkCount <= 0 || chunkCount > 334 || chunkIndex >= chunkCount || chunkStartRune < 0 || chunkEndRune <= chunkStartRune) throw new ArkmePluginError("world-voiceprint-playback-response-invalid", "世界声纹播放响应无效,请重试", true, 502);
9639
+ return {
9640
+ mediaRef: this.media.issueWorldVoiceprintMediaRef(session.userId, {
9641
+ remoteUrl: stringValue$1(data.audio_url),
9642
+ mimeType
9643
+ }),
9644
+ mimeType,
9645
+ durationMillis: Math.max(0, Math.trunc(numberValue(data.duration_ms))),
9646
+ cacheHit: data.cache_hit === true,
9647
+ chunkIndex: responseChunkIndex,
9648
+ chunkCount,
9649
+ chunkStartRune,
9650
+ chunkEndRune
9651
+ };
9652
+ }
9653
+ async createWorldVoiceprintInviteIntent(recordRef, signal) {
9654
+ const session = await this.runtime.requireSession();
9655
+ const entry = this.openWorldRecordRef(recordRef, session.userId);
9656
+ const peerUserId = entry.ownerUserId ?? 0;
9657
+ if (!Number.isSafeInteger(peerUserId) || peerUserId <= 0) throw new ArkmePluginError("world-voiceprint-invite-target-missing", "无法确认这条世界动态的作者,请刷新后重试", false);
9658
+ if (peerUserId === session.userId) throw new ArkmePluginError("world-voiceprint-invite-self-invalid", "这是你自己的动态,不需要提醒自己开启声纹", false, 409);
9659
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/invites/create", { scope: ARKME_VOICEPRINT_PLAY_SCOPE }, session, signal, {
9660
+ lane: "write",
9661
+ bypassCache: true
9662
+ });
9663
+ const token = stringValue$1(data.invite_token).trim();
9664
+ const expiresAtMillis = Math.trunc(numberValue(data.expires_at));
9665
+ const scope = Math.trunc(numberValue(data.scope));
9666
+ if (!validVoiceprintInviteToken(token) || expiresAtMillis <= 0 || scope !== ARKME_VOICEPRINT_PLAY_SCOPE) throw new ArkmePluginError("world-voiceprint-invite-contract-invalid", "声纹邀请响应不完整,请稍后重试", true, 502);
9667
+ return {
9668
+ peerUserId,
9669
+ peerDisplayName: optionalTrimmedText(entry.authorName, 64) ?? "这位用户",
9670
+ inviteUrl: buildVoiceprintInviteShareUrl(this.runtime.config.environment, token),
9671
+ expiresAtMillis,
9672
+ ...entry.textPreview === void 0 ? {} : { textPreview: entry.textPreview }
9673
+ };
9674
+ }
8488
9675
  async listWorldInteractions(recordRef, options = {}) {
8489
9676
  const session = await this.runtime.requireSession();
8490
9677
  const root = this.openWorldRecordRef(recordRef, session.userId);
@@ -8695,7 +9882,14 @@ var WorldService = class {
8695
9882
  }
8696
9883
  const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
8697
9884
  return {
8698
- recordRef: await this.worldRecordRef(viewerUserId, recordUid),
9885
+ recordRef: await this.worldRecordRef(viewerUserId, recordUid, {
9886
+ ownerUserId,
9887
+ authorName: stringValue$1(item.nick_name ?? item.nickname).trim() || "Arkme用户",
9888
+ ...(() => {
9889
+ const textPreview = optionalTrimmedText(headline || textContent, 80);
9890
+ return textPreview === void 0 ? {} : { textPreview };
9891
+ })()
9892
+ }),
8699
9893
  authorName: stringValue$1(item.nick_name ?? item.nickname).trim() || "Arkme用户",
8700
9894
  ...avatarRef === void 0 ? {} : { avatarRef },
8701
9895
  ...avatarRef === void 0 && avatarFallback !== void 0 ? { avatarFallback } : {},
@@ -8765,7 +9959,14 @@ var WorldService = class {
8765
9959
  const avatarFallback = worldPhoneDefaultAvatar(rawAvatar);
8766
9960
  const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
8767
9961
  return {
8768
- interactionRef: await this.worldRecordRef(viewerUserId, recordUid),
9962
+ interactionRef: await this.worldRecordRef(viewerUserId, recordUid, {
9963
+ ownerUserId,
9964
+ authorName: stringValue$1(item.nick_name ?? item.nickname).trim() || "Arkme用户",
9965
+ ...(() => {
9966
+ const textPreview = optionalTrimmedText(textContent, 80);
9967
+ return textPreview === void 0 ? {} : { textPreview };
9968
+ })()
9969
+ }),
8769
9970
  parentRef: await this.worldRecordRef(viewerUserId, parentRecordUid),
8770
9971
  authorName: stringValue$1(item.nick_name ?? item.nickname).trim() || "Arkme用户",
8771
9972
  ...avatarRef === void 0 ? {} : { avatarRef },
@@ -8778,13 +9979,20 @@ var WorldService = class {
8778
9979
  voiceCount
8779
9980
  };
8780
9981
  }
8781
- async worldRecordRef(viewerUserId, recordUid) {
9982
+ async worldRecordRef(viewerUserId, recordUid, metadata = {}) {
8782
9983
  const recordRef = `arkme-world-record-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`world-record-v1:${String(viewerUserId)}:${recordUid}`).digest("base64url")}`;
8783
9984
  const now = Date.now();
8784
9985
  this.pruneWorldRecordRefs(now);
9986
+ const previous = this.worldRecordRefs.get(recordRef);
9987
+ const resolvedOwnerUserId = metadata.ownerUserId ?? previous?.ownerUserId;
9988
+ const authorName = optionalTrimmedText(metadata.authorName, 64) ?? previous?.authorName;
9989
+ const textPreview = optionalTrimmedText(metadata.textPreview, 80) ?? previous?.textPreview;
8785
9990
  this.worldRecordRefs.set(recordRef, {
8786
9991
  viewerUserId,
8787
9992
  recordUid,
9993
+ ...resolvedOwnerUserId === void 0 || resolvedOwnerUserId <= 0 ? {} : { ownerUserId: resolvedOwnerUserId },
9994
+ ...authorName === void 0 ? {} : { authorName },
9995
+ ...textPreview === void 0 ? {} : { textPreview },
8788
9996
  expiresAtMillis: now + ARKME_WORLD_RECORD_REF_TTL_MILLIS
8789
9997
  });
8790
9998
  return recordRef;
@@ -8866,8 +10074,18 @@ var WorldService = class {
8866
10074
  //#endregion
8867
10075
  //#region src/types.ts
8868
10076
  const ARKME_PROVIDER_CONTRACT_VERSION = 1;
10077
+ const ARKME_DEFAULT_SHARE_WEBSITE = "https://app.arkme.ai";
8869
10078
  //#endregion
8870
10079
  //#region src/arkme-service.ts
10080
+ function worldVoiceprintInviteMessage(input) {
10081
+ return `${input.textPreview === void 0 ? "我想邀请你开启声纹。" : `我看到你在世界里发的「${input.textPreview}」,想邀请你开启声纹。`}\n以后看到你的世界动态时,可以直接听到你的声音。\n${input.inviteUrl}`;
10082
+ }
10083
+ function voiceprintInviteRateLimitMessage(error) {
10084
+ if (!(error instanceof ArkmePluginError)) return void 0;
10085
+ if (error.upstreamStatus !== 429 && error.httpStatus !== 429 && !/\bHTTP\s*429\b/.test(error.message)) return void 0;
10086
+ const seconds = error.retryAfterMillis === void 0 ? 0 : Math.ceil(error.retryAfterMillis / 1e3);
10087
+ return seconds > 0 ? `提醒发送太频繁了,请 ${String(seconds)} 秒后再试。` : "提醒发送太频繁了,稍后再试。";
10088
+ }
8871
10089
  var ArkmeService = class {
8872
10090
  config;
8873
10091
  sessionStore;
@@ -8877,6 +10095,7 @@ var ArkmeService = class {
8877
10095
  runtime;
8878
10096
  aiVideo;
8879
10097
  arrangement;
10098
+ calendar;
8880
10099
  wechat;
8881
10100
  recording;
8882
10101
  profile;
@@ -8897,6 +10116,7 @@ var ArkmeService = class {
8897
10116
  interwoven;
8898
10117
  aiPolish;
8899
10118
  chat;
10119
+ contact;
8900
10120
  constructor(config, sessionStore, stateStore, fetchImpl = fetch, pendingSessionStore, outgoingCallBroker = new ArkmeOutgoingCallBroker()) {
8901
10121
  this.config = config;
8902
10122
  this.sessionStore = sessionStore;
@@ -8906,6 +10126,7 @@ var ArkmeService = class {
8906
10126
  this.runtime = new ServiceRuntime(config, sessionStore, stateStore, fetchImpl, pendingSessionStore);
8907
10127
  this.aiVideo = new AiVideoService(this.runtime);
8908
10128
  this.arrangement = new ArrangementService(this.runtime);
10129
+ this.calendar = new CalendarService(this.runtime);
8909
10130
  this.wechat = new WechatService(this.runtime);
8910
10131
  this.recording = new RecordingService(this.runtime);
8911
10132
  this.profile = new ProfileService(this.runtime);
@@ -8918,18 +10139,21 @@ var ArkmeService = class {
8918
10139
  recordItem: (raw) => this.recordItem(raw)
8919
10140
  });
8920
10141
  this.record = new RecordService(this.runtime, this.media, this.source);
8921
- this.search = new SearchService(this.runtime, this.record);
10142
+ this.search = new SearchService(this.runtime, this.record, this.media);
8922
10143
  this.bot = new BotService(this.runtime, this.source);
8923
10144
  this.outgoingCall = new OutgoingCallService(this.runtime, this.source, this.profile, outgoingCallBroker);
8924
10145
  this.world = new WorldService(this.runtime, { refreshProfile: async () => await this.refreshProfile() }, this.media, this.record);
8925
10146
  this.arko = new ArkoService(this.runtime, this.profile);
8926
- this.group = new GroupService(this.runtime, this.source, this.profile);
10147
+ this.group = new GroupService(this.runtime, this.source, this.profile, { sendPrivateText: async (sourceRef, chatSessionUid, text, recordUid, relationUid, session, signal) => {
10148
+ await this.chat.sendChatSourceTextRaw(sourceRef, chatSessionUid, text, recordUid, relationUid, session, void 0, void 0, signal);
10149
+ } });
8927
10150
  this.relatedRecording = new RelatedRecordingService(this.runtime, this.source);
8928
10151
  this.community = new CommunityService(this.runtime, this.source, this.profile);
8929
10152
  this.interwoven = new InterwovenService(this.runtime, this.source, this.profile);
8930
10153
  this.aiPolish = new GroupAiPolishService(this.runtime, this.source, { sendChatSourceTextRaw: async (...args) => await this.chat.sendChatSourceTextRaw(...args) });
8931
10154
  this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session) => await this.chat.chatTimelineItems(data, session) });
8932
10155
  this.chat = new ChatService(this.runtime, this.source, this.profile, this.media, this.record, this.bot, this.arko, this.aiPolish, this.realtime);
10156
+ this.contact = new ContactService(this.runtime, this.source, this.profile, this.realtime);
8933
10157
  this.auth = new AuthService(this.runtime, this.profile, {
8934
10158
  reconnectChatRealtime: () => {
8935
10159
  this.realtime.reconnect();
@@ -8947,6 +10171,7 @@ var ArkmeService = class {
8947
10171
  this.interwoven.dispose();
8948
10172
  this.world.dispose();
8949
10173
  this.arrangement.dispose();
10174
+ this.contact.dispose();
8950
10175
  }
8951
10176
  startChatRealtime() {
8952
10177
  return this.realtime.startChatRealtime();
@@ -8964,7 +10189,10 @@ var ArkmeService = class {
8964
10189
  this.realtime.handleChatRealtimeNotice(notice);
8965
10190
  }
8966
10191
  async refreshChatSessionProjectionBatch(pending) {
8967
- return await this.realtime.refreshChatSessionProjectionBatch(pending);
10192
+ return (await this.realtime.refreshChatSessionProjectionBatch(pending.map(([uid, latestSequence]) => [uid, {
10193
+ latestSequence,
10194
+ notificationHints: []
10195
+ }]))).map(([uid, projection]) => [uid, projection.latestSequence]);
8968
10196
  }
8969
10197
  attachOpenClawProvisioner(provisioner) {
8970
10198
  this.bot.attachOpenClawProvisioner(provisioner);
@@ -8978,6 +10206,9 @@ var ArkmeService = class {
8978
10206
  async createBot(input, options = {}) {
8979
10207
  return await this.bot.createBot(input, options);
8980
10208
  }
10209
+ async createBotSummary(input, options = {}) {
10210
+ return await this.bot.createBotSummary(input, options);
10211
+ }
8981
10212
  async revealBotSecret(botRef, options = {}) {
8982
10213
  return await this.bot.revealBotSecret(botRef, options);
8983
10214
  }
@@ -9004,7 +10235,8 @@ var ArkmeService = class {
9004
10235
  captchaId: this.config.geetestCaptchaId,
9005
10236
  environment: this.config.environment,
9006
10237
  testLoginEnabled: this.config.environment === "test",
9007
- callAssetBasePath: `${this.config.routePath}/call`
10238
+ callAssetBasePath: `${this.config.routePath}/call`,
10239
+ shareWebsite: this.config.shareWebsite ?? "https://app.arkme.ai"
9008
10240
  };
9009
10241
  }
9010
10242
  providerCapabilities() {
@@ -9023,6 +10255,8 @@ var ArkmeService = class {
9023
10255
  revisionPolling: true,
9024
10256
  userProfile: true,
9025
10257
  imageRead: true,
10258
+ recordCalendar: true,
10259
+ imageLibrary: true,
9026
10260
  sourceDirectory: true,
9027
10261
  sourceTimeline: true,
9028
10262
  sourceTextSend: true,
@@ -9031,8 +10265,11 @@ var ArkmeService = class {
9031
10265
  fileUpload: this.config.richMediaSendEnabled !== false,
9032
10266
  outgoingCall: true,
9033
10267
  groupMembers: true,
10268
+ groupMemberAdd: true,
9034
10269
  userCard: true,
9035
10270
  openPrivateChat: true,
10271
+ contactAdd: true,
10272
+ conversationQuickAdd: true,
9036
10273
  groupSettings: true,
9037
10274
  extensionManagement: true,
9038
10275
  extensionMetadataEdit: true,
@@ -9040,9 +10277,13 @@ var ArkmeService = class {
9040
10277
  extensionPreviews: true,
9041
10278
  worldFeed: true,
9042
10279
  worldInteractions: true,
10280
+ worldVoiceprintPlayback: true,
10281
+ worldVoiceprintInvite: true,
9043
10282
  arrangements: true,
9044
10283
  myExtensions: true,
9045
10284
  extensionPublish: true,
10285
+ recordingTranscripts: true,
10286
+ recordingDoubaoBackfill: true,
9046
10287
  extensionReviews: true,
9047
10288
  ...this.relatedRecording.isEnabled() ? { relatedRecordings: true } : {}
9048
10289
  },
@@ -9088,6 +10329,7 @@ var ArkmeService = class {
9088
10329
  return await this.outgoingCall.releaseOutgoingCall(callRequestId);
9089
10330
  }
9090
10331
  dispose() {
10332
+ this.contact.dispose();
9091
10333
  this.realtime.dispose();
9092
10334
  this.arko.dispose();
9093
10335
  this.auth.dispose();
@@ -9108,6 +10350,12 @@ var ArkmeService = class {
9108
10350
  async cachedProfile() {
9109
10351
  return await this.profile.cachedProfile();
9110
10352
  }
10353
+ async searchContact(identifier, options = {}) {
10354
+ return await this.contact.search(identifier, options);
10355
+ }
10356
+ async addContact(contactRef, options = {}) {
10357
+ return await this.contact.add(contactRef, options);
10358
+ }
9111
10359
  async extensionAuthors(userIds, signal) {
9112
10360
  return await this.extensionReview.extensionAuthors(userIds, signal);
9113
10361
  }
@@ -9122,8 +10370,12 @@ var ArkmeService = class {
9122
10370
  return await this.recording.recordingCalendar(fromStamp, toStamp, signal);
9123
10371
  }
9124
10372
  /** Read-only Audio capability shared by the built-in UI and Arkme recording tools. */
9125
- async recordingTranscript(dateStamp, signal) {
9126
- return await this.recording.recordingTranscript(dateStamp, signal);
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);
9127
10379
  }
9128
10380
  /** Read-only Audio capability shared by the built-in UI and Arkme recording tools. */
9129
10381
  async recordingProjection(dateStamp, kind, signal) {
@@ -9246,6 +10498,18 @@ var ArkmeService = class {
9246
10498
  async listGroupMembers(sourceRef, options = {}) {
9247
10499
  return await this.group.listGroupMembers(sourceRef, options);
9248
10500
  }
10501
+ async listGroupMemberCandidates(sourceRef, options = {}) {
10502
+ return await this.group.listGroupMemberCandidates(sourceRef, options);
10503
+ }
10504
+ async groupInvitePreview(sourceRef, signal) {
10505
+ return await this.group.groupInvitePreview(sourceRef, signal);
10506
+ }
10507
+ async addGroupMembers(sourceRef, candidateRefs, signal) {
10508
+ return await this.group.addGroupMembers(sourceRef, candidateRefs, signal);
10509
+ }
10510
+ async createGroup(title, clientMutationId, options = {}) {
10511
+ return await this.group.createGroup(title, clientMutationId, options);
10512
+ }
9249
10513
  async groupSettings(sourceRef, signal) {
9250
10514
  return await this.group.groupSettings(sourceRef, signal);
9251
10515
  }
@@ -9394,6 +10658,13 @@ var ArkmeService = class {
9394
10658
  async searchScene(options) {
9395
10659
  return await this.search.searchScene(options);
9396
10660
  }
10661
+ /**
10662
+ * Build the desktop image library from the owner's mixed image/video scene.
10663
+ * Signed storage URLs stay inside the Provider and are replaced by account-bound media refs.
10664
+ */
10665
+ async searchImages(options) {
10666
+ return await this.search.searchImages(options);
10667
+ }
9397
10668
  async searchRecordings(options) {
9398
10669
  return await this.search.searchRecordings(options);
9399
10670
  }
@@ -9406,6 +10677,12 @@ var ArkmeService = class {
9406
10677
  async list(limit, cursor) {
9407
10678
  return await this.record.list(limit, cursor);
9408
10679
  }
10680
+ async calendarBuckets(options) {
10681
+ return await this.calendar.bucketPage(options);
10682
+ }
10683
+ async calendarRecords(options) {
10684
+ return await this.calendar.dayRecords(options);
10685
+ }
9409
10686
  async listWorldRecords(options = {}) {
9410
10687
  return await this.world.listWorldRecords(options);
9411
10688
  }
@@ -9445,6 +10722,41 @@ var ArkmeService = class {
9445
10722
  async listWorldFeed(options = {}) {
9446
10723
  return await this.world.listWorldFeed(options);
9447
10724
  }
10725
+ /** Build the signed-in account's World projection without exposing owner IDs. */
10726
+ async listMyWorldFeed(options = {}) {
10727
+ return await this.world.listMyWorldFeed(options);
10728
+ }
10729
+ async worldVoiceprintPlaybackAvailability(recordRefs, signal) {
10730
+ return await this.world.worldVoiceprintPlaybackAvailability(recordRefs, signal);
10731
+ }
10732
+ async generateWorldVoiceprintPlayback(input) {
10733
+ return await this.world.generateWorldVoiceprintPlayback(input);
10734
+ }
10735
+ async inviteWorldVoiceprint(recordRef, signal) {
10736
+ try {
10737
+ const intent = await this.world.createWorldVoiceprintInviteIntent(recordRef, signal);
10738
+ const privateChat = await this.chat.openPrivateChatFromUser(intent.peerUserId, {
10739
+ displayName: intent.peerDisplayName,
10740
+ ...signal === void 0 ? {} : { signal }
10741
+ });
10742
+ const sent = await this.chat.sendSourceText(privateChat.source.sourceRef, worldVoiceprintInviteMessage(intent), signal === void 0 ? {} : { signal });
10743
+ if (sent.localState !== "synced") throw new ArkmePluginError("world-voiceprint-invite-send-pending", sent.error ?? "声纹邀请已进入发送队列,请稍后在私聊中确认", true);
10744
+ return {
10745
+ sent: true,
10746
+ peerDisplayName: intent.peerDisplayName,
10747
+ messageItemUid: sent.itemUid,
10748
+ expiresAtMillis: intent.expiresAtMillis
10749
+ };
10750
+ } catch (error) {
10751
+ const message = voiceprintInviteRateLimitMessage(error);
10752
+ if (message !== void 0) throw new ArkmePluginError("world-voiceprint-invite-rate-limited", message, true, 429, {
10753
+ cause: error,
10754
+ upstreamStatus: 429,
10755
+ ...!(error instanceof ArkmePluginError) || error.retryAfterMillis === void 0 ? {} : { retryAfterMillis: error.retryAfterMillis }
10756
+ });
10757
+ throw error;
10758
+ }
10759
+ }
9448
10760
  /** Read the authenticated comment/reply tree behind one account-bound World reference. */
9449
10761
  async listWorldInteractions(recordRef, options = {}) {
9450
10762
  return await this.world.listWorldInteractions(recordRef, options);
@@ -9495,6 +10807,8 @@ const ARKME_EXTENSION_FORMAT = "arkme-cordis-extension";
9495
10807
  const ARKME_EXTENSION_MAX_BYTES = 100 * 1024 * 1024;
9496
10808
  const ARKME_EXTENSION_ICON_MAX_BYTES = 2 * 1024 * 1024;
9497
10809
  const ARKME_EXTENSION_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
10810
+ /** Browser/model-safe projection. Host filesystem paths and Dynamic Cordis IDs stay private. */
10811
+ const ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE = "插件运行失败,已自动停用。";
9498
10812
  //#endregion
9499
10813
  //#region src/extensions/artifact.ts
9500
10814
  const TAR_BLOCK_BYTES$1 = 512;
@@ -9611,6 +10925,7 @@ function canonicalExtensionSignatureMessage(input) {
9611
10925
  }), "utf8");
9612
10926
  }
9613
10927
  const ARKME_BUNDLE_ARTIFACT_KIND = "dsh-bundle-tgz";
10928
+ const ARKME_NATIVE_BUNDLE_ARTIFACT_KIND = "dsh-native-package-tgz";
9614
10929
  const ARKME_BUNDLE_MAX_BYTES = 100 * 1024 * 1024;
9615
10930
  const TAR_BLOCK_BYTES = 512;
9616
10931
  const MAX_ARCHIVE_FILES = 1024;
@@ -9657,9 +10972,22 @@ function writeOctal(header, offset, length, value) {
9657
10972
  header.write(octal.slice(-(length - 1)), offset, length - 1, "ascii");
9658
10973
  header[offset + length - 1] = 0;
9659
10974
  }
10975
+ function splitTarPath(path) {
10976
+ const direct = Buffer.from(path, "utf8");
10977
+ if (direct.byteLength <= 100) return { name: direct };
10978
+ const separators = [...path.matchAll(/\//g)].map((match) => match.index).reverse();
10979
+ for (const separator of separators) {
10980
+ const prefix = Buffer.from(path.slice(0, separator), "utf8");
10981
+ const name = Buffer.from(path.slice(separator + 1), "utf8");
10982
+ if (prefix.byteLength <= 155 && name.byteLength <= 100) return {
10983
+ name,
10984
+ prefix
10985
+ };
10986
+ }
10987
+ throw new ArkmeBundleArtifactError("bundle-path-too-long", `Bundle 路径过长:${path}`);
10988
+ }
9660
10989
  function tarHeader(path, size) {
9661
- const name = Buffer.from(path, "utf8");
9662
- if (name.byteLength > 100) throw new ArkmeBundleArtifactError("bundle-path-too-long", `Bundle 路径过长:${path}`);
10990
+ const { name, prefix } = splitTarPath(path);
9663
10991
  const header = Buffer.alloc(TAR_BLOCK_BYTES);
9664
10992
  name.copy(header, 0);
9665
10993
  writeOctal(header, 100, 8, 420);
@@ -9671,6 +10999,7 @@ function tarHeader(path, size) {
9671
10999
  header[156] = "0".charCodeAt(0);
9672
11000
  header.write("ustar\0", 257, 6, "ascii");
9673
11001
  header.write("00", 263, 2, "ascii");
11002
+ prefix?.copy(header, 345);
9674
11003
  writeOctal(header, 148, 8, [...header].reduce((sum, byte) => sum + byte, 0));
9675
11004
  return header;
9676
11005
  }
@@ -9705,7 +11034,9 @@ function readTar(tar) {
9705
11034
  while (offset + TAR_BLOCK_BYTES <= tar.byteLength) {
9706
11035
  const header = tar.subarray(offset, offset + TAR_BLOCK_BYTES);
9707
11036
  if (header.every((byte) => byte === 0)) break;
9708
- const path = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
11037
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
11038
+ const prefix = header.subarray(345, 500).toString("utf8").replace(/\0.*$/, "");
11039
+ const path = prefix === "" ? name : `${prefix}/${name}`;
9709
11040
  validateArchivePath(path);
9710
11041
  if (files.has(path)) throw new ArkmeBundleArtifactError("bundle-duplicate-path", `Bundle 包含重复路径:${path}`);
9711
11042
  const type = header[156];
@@ -9790,6 +11121,105 @@ function validatePatch(raw, packageName, executionModel) {
9790
11121
  if (ids.size === 0) throw new ArkmeBundleArtifactError("bundle-patch-empty", "Bundle patch 至少需要一条 row");
9791
11122
  return [...ids].sort();
9792
11123
  }
11124
+ const NATIVE_LIFECYCLE_SCRIPTS = /* @__PURE__ */ new Set([
11125
+ "preinstall",
11126
+ "install",
11127
+ "postinstall",
11128
+ "prepare",
11129
+ "prepublish",
11130
+ "prepublishOnly",
11131
+ "prepack",
11132
+ "postpack"
11133
+ ]);
11134
+ function nonEmptyManifestValue(value) {
11135
+ if (value === void 0 || value === null) return false;
11136
+ if (typeof value === "string") return value.trim() !== "";
11137
+ if (Array.isArray(value)) return value.length > 0;
11138
+ if (typeof value === "object") return Object.keys(value).length > 0;
11139
+ if (typeof value === "boolean") return value;
11140
+ return true;
11141
+ }
11142
+ function inspectNativePatch(raw, packageName) {
11143
+ const document = parseDocument(raw.toString("utf8"), { uniqueKeys: true });
11144
+ if (document.errors.length > 0) throw new ArkmeBundleArtifactError("bundle-patch-invalid", "Bundle patch YAML 格式无效");
11145
+ const value = document.toJS();
11146
+ if (!Array.isArray(value)) throw new ArkmeBundleArtifactError("bundle-patch-invalid", "Bundle patch 必须是操作数组");
11147
+ const patchIds = /* @__PURE__ */ new Set();
11148
+ const capabilities = /* @__PURE__ */ new Set();
11149
+ for (const operation of value) {
11150
+ if (operation === null || typeof operation !== "object" || Array.isArray(operation)) {
11151
+ capabilities.add("profile_patch_override");
11152
+ continue;
11153
+ }
11154
+ const record = operation;
11155
+ if (!(Object.keys(record).length === 1 && Array.isArray(record.insert))) {
11156
+ capabilities.add("profile_patch_override");
11157
+ if (typeof record.id === "string" && record.id.trim() !== "") patchIds.add(record.id.trim());
11158
+ continue;
11159
+ }
11160
+ for (const row of record.insert) {
11161
+ if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
11162
+ const item = row;
11163
+ if (typeof item.id === "string" && item.id.trim() !== "") patchIds.add(item.id.trim());
11164
+ if (typeof item.name === "string" && item.name !== packageName && !item.name.startsWith(`${packageName}/`)) capabilities.add("external_package_reference");
11165
+ }
11166
+ }
11167
+ return {
11168
+ patchIds: [...patchIds].sort(),
11169
+ capabilities: [...capabilities].sort()
11170
+ };
11171
+ }
11172
+ function inspectNativeManifest(manifest, files) {
11173
+ if (typeof manifest.name !== "string" || manifest.name.length > 214 || !PACKAGE_NAME.test(manifest.name) || manifest.name.startsWith("@deepseek-ai/") || manifest.name === "@senguoyun/dsh-arkme" || manifest.name.startsWith("@arkme-local/")) throw new ArkmeBundleArtifactError("bundle-package-name-invalid", "Bundle package name 无效或属于保留集合");
11174
+ if (typeof manifest.version !== "string" || !SEMVER.test(manifest.version)) throw new ArkmeBundleArtifactError("bundle-version-invalid", "Bundle version 必须是严格 SemVer");
11175
+ if (manifest.dsh?.arkme !== void 0 && manifest.dsh.arkme !== null) throw new ArkmeBundleArtifactError("bundle-execution-model-invalid", "原生 DSH V3 Bundle 不允许声明 Arkme sandbox marker");
11176
+ const patch = manifest.dsh?.bundle?.patch;
11177
+ if (typeof patch !== "string" || patch.trim() === "" || patch.includes("\\") || patch.startsWith("/")) throw new ArkmeBundleArtifactError("bundle-patch-missing", "Bundle 必须声明包内 dsh.bundle.patch");
11178
+ const patchPath = `package/${posix.normalize(patch.replace(/^\.\//, ""))}`;
11179
+ if (!files.has(patchPath) || patchPath.includes("/../")) throw new ArkmeBundleArtifactError("bundle-patch-missing", "Bundle patch 不在包内");
11180
+ const capabilities = /* @__PURE__ */ new Set();
11181
+ if (nonEmptyManifestValue(manifest.dependencies)) capabilities.add("runtime_dependencies");
11182
+ if (nonEmptyManifestValue(manifest.optionalDependencies)) capabilities.add("optional_dependencies");
11183
+ if (nonEmptyManifestValue(manifest.bundledDependencies) || nonEmptyManifestValue(manifest.bundleDependencies)) capabilities.add("bundled_dependencies");
11184
+ for (const name of Object.keys(manifest.peerDependencies ?? {})) if (!name.startsWith("@deepseek-ai/") && name !== "@senguoyun/dsh-arkme" && name !== "react" && name !== "react-dom") {
11185
+ capabilities.add("peer_dependencies");
11186
+ break;
11187
+ }
11188
+ if (Object.entries(manifest.scripts ?? {}).some(([name, value]) => NATIVE_LIFECYCLE_SCRIPTS.has(name) && nonEmptyManifestValue(value))) capabilities.add("lifecycle_scripts");
11189
+ if (nonEmptyManifestValue(manifest.bin)) capabilities.add("bin");
11190
+ if ([...files.keys()].some((name) => name.toLowerCase().endsWith(".node"))) capabilities.add("native_addon");
11191
+ return {
11192
+ patchPath,
11193
+ capabilities: [...capabilities].sort()
11194
+ };
11195
+ }
11196
+ function inspectNativeBundleArtifactV3(bytes) {
11197
+ assertArchiveSize(bytes.byteLength, "Bundle");
11198
+ let tar;
11199
+ try {
11200
+ tar = gunzipSync(bytes, { maxOutputLength: 105907200 });
11201
+ } catch {
11202
+ throw new ArkmeBundleArtifactError("bundle-gzip-invalid", "Bundle 不是有效 gzip");
11203
+ }
11204
+ const files = readTar(tar);
11205
+ const packageJSON = files.get("package/package.json");
11206
+ if (packageJSON === void 0) throw new ArkmeBundleArtifactError("bundle-package-json-missing", "Bundle 缺少 package.json");
11207
+ const manifest = readManifest(packageJSON);
11208
+ const inspected = inspectNativeManifest(manifest, files);
11209
+ const patch = inspectNativePatch(files.get(inspected.patchPath), manifest.name);
11210
+ const nativeCapabilities = [.../* @__PURE__ */ new Set([...inspected.capabilities, ...patch.capabilities])].sort();
11211
+ return {
11212
+ bytes: Buffer.from(bytes),
11213
+ bundleSha256: bundleSha256(bytes),
11214
+ packageJsonSha256: bundleSha256(packageJSON),
11215
+ packageName: manifest.name,
11216
+ version: manifest.version,
11217
+ executionModel: "dsh-native",
11218
+ nativeCapabilities,
11219
+ files,
11220
+ patchIds: patch.patchIds
11221
+ };
11222
+ }
9793
11223
  function inspectBundleArtifact(bytes) {
9794
11224
  assertArchiveSize(bytes.byteLength, "Bundle");
9795
11225
  let tar;
@@ -9820,29 +11250,114 @@ function pathInside(root, target) {
9820
11250
  const fromRoot = relative(root, target);
9821
11251
  return fromRoot === "" || !fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot);
9822
11252
  }
9823
- function collectPackageFiles(root, manifest) {
11253
+ function globPattern(pattern) {
11254
+ let expression = "^";
11255
+ for (let index = 0; index < pattern.length; index += 1) {
11256
+ const character = pattern[index];
11257
+ if (character === "*") if (pattern[index + 1] === "*") {
11258
+ index += 1;
11259
+ if (pattern[index + 1] === "/") {
11260
+ index += 1;
11261
+ expression += "(?:.*/)?";
11262
+ } else expression += ".*";
11263
+ } else expression += "[^/]*";
11264
+ else if (character === "?") expression += "[^/]";
11265
+ else expression += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
11266
+ }
11267
+ return new RegExp(`${expression}$`);
11268
+ }
11269
+ function collectPackageFiles(root, manifest, nativeV3 = false) {
9824
11270
  const canonicalRoot = realpathSync(root);
9825
11271
  const selected = /* @__PURE__ */ new Map();
9826
- const add = (absolute) => {
11272
+ const ignoredNativeRootEntries = /* @__PURE__ */ new Set([
11273
+ ".git",
11274
+ ".hg",
11275
+ ".svn",
11276
+ "node_modules"
11277
+ ]);
11278
+ const ignoredNativeFiles = /* @__PURE__ */ new Set([
11279
+ ".npmrc",
11280
+ ".yarnrc",
11281
+ ".yarnrc.yml",
11282
+ ".pnpmfile.cjs"
11283
+ ]);
11284
+ const add = (absolute, allowIgnore = false) => {
9827
11285
  const stat = lstatSync(absolute);
9828
- if (stat.isSymbolicLink()) throw new ArkmeBundleArtifactError("bundle-link-forbidden", "Bundle source 不允许符号链接");
11286
+ const rel = relative(canonicalRoot, absolute).split(sep).join("/");
11287
+ const first = rel.split("/")[0] ?? "";
11288
+ const basename = rel.split("/").at(-1) ?? "";
11289
+ if (allowIgnore && (ignoredNativeRootEntries.has(first) || ignoredNativeFiles.has(basename) || basename === ".env" || basename.startsWith(".env."))) return;
11290
+ if (stat.isSymbolicLink()) {
11291
+ if (allowIgnore) return;
11292
+ throw new ArkmeBundleArtifactError("bundle-link-forbidden", "Bundle source 不允许符号链接");
11293
+ }
9829
11294
  const canonical = realpathSync(absolute);
9830
11295
  if (!pathInside(canonicalRoot, canonical)) throw new ArkmeBundleArtifactError("bundle-path-invalid", "Bundle source 逃逸 package root");
9831
11296
  if (stat.isDirectory()) {
9832
- for (const child of readdirSync(canonical).sort()) add(join(canonical, child));
11297
+ for (const child of readdirSync(canonical).sort()) add(join(canonical, child), allowIgnore);
9833
11298
  return;
9834
11299
  }
9835
11300
  if (!stat.isFile()) throw new ArkmeBundleArtifactError("bundle-file-type-forbidden", "Bundle source 只允许普通文件");
9836
- const rel = relative(canonicalRoot, canonical).split(sep).join("/");
9837
- selected.set(`package/${rel}`, readFileSync(canonical));
11301
+ const canonicalRel = relative(canonicalRoot, canonical).split(sep).join("/");
11302
+ selected.set(`package/${canonicalRel}`, readFileSync(canonical));
11303
+ };
11304
+ const addNativePackageExtras = () => {
11305
+ if (!nativeV3) return;
11306
+ for (const child of readdirSync(canonicalRoot).sort()) if (/^(?:readme|licen[cs]e|copying|notice)(?:\.|$)/i.test(child)) add(join(canonicalRoot, child), true);
11307
+ const paths = /* @__PURE__ */ new Set();
11308
+ if (typeof manifest.main === "string") paths.add(manifest.main);
11309
+ const bin = manifest.bin;
11310
+ if (typeof bin === "string") paths.add(bin);
11311
+ else if (bin !== null && typeof bin === "object" && !Array.isArray(bin)) {
11312
+ for (const value of Object.values(bin)) if (typeof value === "string") paths.add(value);
11313
+ }
11314
+ const patch = manifest.dsh?.bundle?.patch;
11315
+ if (typeof patch === "string") paths.add(patch);
11316
+ for (const entry of paths) {
11317
+ const target = resolve(canonicalRoot, entry);
11318
+ if (pathInside(canonicalRoot, target) && existsSync(target)) add(target, true);
11319
+ }
9838
11320
  };
9839
11321
  add(join(canonicalRoot, "package.json"));
9840
- for (const entry of manifest.files) {
9841
- if (entry.includes("*") || entry.includes("?") || entry.includes("[") || entry.includes("\\")) throw new ArkmeBundleArtifactError("bundle-files-invalid", "Bundle files 暂不接受 glob 或反斜杠");
11322
+ const entries = manifest.files ?? [];
11323
+ if (entries.length === 0) {
11324
+ if (!nativeV3) throw new ArkmeBundleArtifactError("bundle-files-missing", "Bundle 必须显式声明 files");
11325
+ for (const child of readdirSync(canonicalRoot).sort()) if (child !== "package.json") add(join(canonicalRoot, child), true);
11326
+ addNativePackageExtras();
11327
+ return selected;
11328
+ }
11329
+ let candidates;
11330
+ const listCandidates = () => {
11331
+ if (candidates !== void 0) return candidates;
11332
+ candidates = [];
11333
+ const walk = (directory) => {
11334
+ for (const child of readdirSync(directory).sort()) {
11335
+ const absolute = join(directory, child);
11336
+ const stat = lstatSync(absolute);
11337
+ if (stat.isSymbolicLink()) continue;
11338
+ if (stat.isDirectory()) walk(absolute);
11339
+ else if (stat.isFile()) candidates.push(relative(canonicalRoot, absolute).split(sep).join("/"));
11340
+ }
11341
+ };
11342
+ walk(canonicalRoot);
11343
+ return candidates;
11344
+ };
11345
+ for (const entry of entries) {
11346
+ if (entry.includes("\\") || entry.startsWith("/") || entry.split("/").includes("..")) throw new ArkmeBundleArtifactError("bundle-files-invalid", `Bundle files 路径无效:${entry}`);
11347
+ const hasGlob = entry.includes("*") || entry.includes("?") || entry.includes("[");
11348
+ if (hasGlob && !nativeV3) throw new ArkmeBundleArtifactError("bundle-files-invalid", "Bundle files 暂不接受 glob 或反斜杠");
11349
+ if (hasGlob) {
11350
+ const matcher = globPattern(entry.replace(/^\.\//, ""));
11351
+ const matches = listCandidates().filter((candidate) => matcher.test(candidate));
11352
+ if (matches.length === 0) throw new ArkmeBundleArtifactError("bundle-files-invalid", `Bundle files 路径无效:${entry}`);
11353
+ for (const match of matches) add(resolve(canonicalRoot, match), true);
11354
+ continue;
11355
+ }
9842
11356
  const target = resolve(canonicalRoot, entry);
9843
- if (!pathInside(canonicalRoot, target) || !statSync(target).isFile() && !statSync(target).isDirectory()) throw new ArkmeBundleArtifactError("bundle-files-invalid", `Bundle files 路径无效:${entry}`);
9844
- add(target);
11357
+ if (!pathInside(canonicalRoot, target) || !existsSync(target) || !statSync(target).isFile() && !statSync(target).isDirectory()) throw new ArkmeBundleArtifactError("bundle-files-invalid", `Bundle files 路径无效:${entry}`);
11358
+ add(target, nativeV3);
9845
11359
  }
11360
+ addNativePackageExtras();
9846
11361
  return selected;
9847
11362
  }
9848
11363
  function packBundleFiles(files) {
@@ -9856,14 +11371,25 @@ function packBundleFiles(files) {
9856
11371
  }
9857
11372
  };
9858
11373
  }
9859
- function packLocalBundleDirectory(directory) {
9860
- return packBundleFiles(collectPackageFiles(directory, readManifest(readFileSync(join(directory, "package.json")))));
11374
+ function packNativeBundleFiles(files) {
11375
+ const bytes = gzipSync(createTar(files), { level: 9 });
11376
+ assertArchiveSize(bytes.byteLength, "Bundle");
11377
+ return {
11378
+ bundle: inspectNativeBundleArtifactV3(bytes),
11379
+ source: {
11380
+ bytes: Buffer.from(bytes),
11381
+ sourceSha256: bundleSha256(bytes)
11382
+ }
11383
+ };
11384
+ }
11385
+ function packLocalNativeBundleDirectoryV3(directory) {
11386
+ return packNativeBundleFiles(collectPackageFiles(directory, readManifest(readFileSync(join(directory, "package.json"))), true));
9861
11387
  }
9862
- function readLocalBundleTarball(path) {
11388
+ function readNativeBundleTarballV3(path) {
9863
11389
  if (!lstatSync(path).isFile()) throw new ArkmeBundleArtifactError("bundle-tarball-invalid", "本地 Bundle tgz 必须是普通文件");
9864
11390
  const bytes = readFileSync(path);
9865
11391
  return {
9866
- bundle: inspectBundleArtifact(bytes),
11392
+ bundle: inspectNativeBundleArtifactV3(bytes),
9867
11393
  source: {
9868
11394
  bytes: Buffer.from(bytes),
9869
11395
  sourceSha256: bundleSha256(bytes)
@@ -9938,6 +11464,39 @@ function verifyBundleResolutionSignature(resolution, trustedKeys) {
9938
11464
  }
9939
11465
  if (!valid) throw new ArkmePluginError("extension-signature-invalid", "扩展 Bundle 平台签名验证失败", false, 403);
9940
11466
  }
11467
+ function canonicalNativeBundleSignatureMessage(input) {
11468
+ return Buffer.from(JSON.stringify({
11469
+ format_version: input.artifact_contract_version,
11470
+ artifact_kind: input.artifact_kind,
11471
+ extension_id: input.extension_id,
11472
+ package_name: input.package_name,
11473
+ version: input.version,
11474
+ execution_model: input.execution_model,
11475
+ bundle_sha256: input.bundle_sha256,
11476
+ package_json_sha256: input.package_json_sha256,
11477
+ source_sha256: input.source_sha256,
11478
+ ...input.native_capabilities.length === 0 ? {} : { native_capabilities: input.native_capabilities },
11479
+ published_at: input.published_at,
11480
+ signing_key_id: input.signing_key_id
11481
+ }), "utf8");
11482
+ }
11483
+ function verifyNativeBundleResolutionSignature(resolution, trustedKeys) {
11484
+ const capabilities = resolution.native_capabilities;
11485
+ if (resolution.artifact_contract_version !== 3 || resolution.artifact_kind !== "dsh-native-package-tgz" || resolution.package_name.trim() === "" || resolution.execution_model !== "dsh-native" || ![
11486
+ resolution.bundle_sha256,
11487
+ resolution.package_json_sha256,
11488
+ resolution.source_sha256
11489
+ ].every((value) => /^[a-f0-9]{64}$/.test(value)) || !Array.isArray(capabilities) || capabilities.some((value) => typeof value !== "string" || value.trim() === "") || capabilities.some((value, index) => index > 0 && capabilities[index - 1] >= value) || !Number.isSafeInteger(resolution.published_at) || resolution.published_at <= 0 || resolution.signature.trim() === "") throw new ArkmePluginError("extension-signature-envelope-invalid", "原生 DSH V3 签名 envelope 字段无效", false, 502);
11490
+ const key = trustedKeys.get(resolution.signing_key_id);
11491
+ if (key === void 0) throw new ArkmePluginError("extension-signing-key-untrusted", `不信任扩展签名密钥 ${resolution.signing_key_id}`, false, 403);
11492
+ let valid = false;
11493
+ try {
11494
+ valid = verify(null, canonicalNativeBundleSignatureMessage(resolution), publicKey(key), Buffer.from(resolution.signature, "base64"));
11495
+ } catch (error) {
11496
+ throw new ArkmePluginError("extension-signature-invalid", "原生 DSH V3 签名格式无效", false, 403, { cause: error });
11497
+ }
11498
+ if (!valid) throw new ArkmePluginError("extension-signature-invalid", "原生 DSH V3 平台签名验证失败", false, 403);
11499
+ }
9941
11500
  //#endregion
9942
11501
  //#region src/extensions/persistent-client-bundle.ts
9943
11502
  /** This function is serialized into each generated browser bundle. It must stay closure-free. */
@@ -9977,14 +11536,29 @@ function persistentClientFactory(requireModule, spec) {
9977
11536
  })
9978
11537
  });
9979
11538
  const envelope = await response.json();
9980
- if (!response.ok || envelope.ok !== true) throw new Error(envelope.error?.message ?? `Arkme Host returned HTTP ${String(response.status)}`);
11539
+ if (!response.ok || envelope.ok !== true) {
11540
+ const error = new Error(envelope.error?.message ?? `Arkme Host returned HTTP ${String(response.status)}`);
11541
+ if (envelope.error?.code !== void 0) error.code = envelope.error.code;
11542
+ throw error;
11543
+ }
9981
11544
  return envelope.value;
9982
11545
  };
9983
- const callHost = async (method, args = null) => await callOperation(spec.operation ?? "extensions.persistent.invoke", {
9984
- ...spec.identityKey === "packageName" ? { packageName: spec.extensionId } : { extensionId: spec.extensionId },
9985
- method,
9986
- args: jsonValue(args)
9987
- });
11546
+ let deactivateClient;
11547
+ const callHost = async (method, args = null) => {
11548
+ try {
11549
+ return await callOperation(spec.operation ?? "extensions.persistent.invoke", {
11550
+ ...spec.identityKey === "packageName" ? { packageName: spec.extensionId } : {
11551
+ extensionId: spec.extensionId,
11552
+ version: spec.version
11553
+ },
11554
+ method,
11555
+ args: jsonValue(args)
11556
+ });
11557
+ } catch (error) {
11558
+ if (error.code === "extension-runtime-unavailable") deactivateClient?.();
11559
+ throw error;
11560
+ }
11561
+ };
9988
11562
  const guardedService = (service, name) => new Proxy(service, { get(target, property) {
9989
11563
  const member = Reflect.get(target, property, target);
9990
11564
  if (typeof member !== "function") return member;
@@ -10044,8 +11618,10 @@ function persistentClientFactory(requireModule, spec) {
10044
11618
  name: `arkme-extension-client:${spec.extensionId}`,
10045
11619
  async apply(ctx) {
10046
11620
  if (spec.identityKey !== "packageName") {
10047
- const state = await callOperation("extensions.enabled-state", { extensionId: spec.extensionId });
10048
- if (state.installed !== true || state.enabled !== true) return;
11621
+ if ((await callOperation("extensions.persistent.client-state", {
11622
+ extensionId: spec.extensionId,
11623
+ version: spec.version
11624
+ })).mount !== true) return;
10049
11625
  }
10050
11626
  const styles = new Styles();
10051
11627
  const traps = {
@@ -10089,7 +11665,16 @@ function persistentClientFactory(requireModule, spec) {
10089
11665
  }
10090
11666
  };
10091
11667
  ctx.effect(() => () => styles.dispose(), `arkme-extension:${spec.extensionId}:styles`);
10092
- await ctx.plugin(guarded);
11668
+ const fiber = ctx.plugin(guarded);
11669
+ const deactivate = () => {
11670
+ styles.dispose();
11671
+ Promise.resolve(fiber.dispose?.()).catch(() => void 0);
11672
+ };
11673
+ deactivateClient = deactivate;
11674
+ ctx.effect(() => () => {
11675
+ if (deactivateClient === deactivate) deactivateClient = void 0;
11676
+ }, `arkme-extension:${spec.extensionId}:client-fiber`);
11677
+ await fiber;
10093
11678
  }
10094
11679
  };
10095
11680
  }
@@ -10125,18 +11710,18 @@ function writeSecure(path, content) {
10125
11710
  });
10126
11711
  chmodSync(path, 384);
10127
11712
  }
10128
- function activationText(extensionId, enabled) {
11713
+ function activationText(extensionId, enabled, quarantine) {
10129
11714
  return `${JSON.stringify({
10130
11715
  schema_version: 1,
10131
11716
  extension_id: extensionId,
10132
- enabled
11717
+ enabled,
11718
+ ...quarantine === void 0 ? {} : { quarantine }
10133
11719
  }, void 0, 2)}\n`;
10134
11720
  }
10135
- function writePersistentExtensionActivation(bundleDirectory, extensionId, enabled) {
10136
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(extensionId)) throw new Error("扩展身份无效");
11721
+ function writeActivationState(bundleDirectory, state) {
10137
11722
  const target = join(bundleDirectory, "activation.json");
10138
11723
  const temporary = join(bundleDirectory, `.activation.${randomUUID()}.tmp`);
10139
- writeSecure(temporary, activationText(extensionId, enabled));
11724
+ writeSecure(temporary, activationText(state.extension_id, state.enabled, state.quarantine));
10140
11725
  try {
10141
11726
  renameSync(temporary, target);
10142
11727
  chmodSync(target, 384);
@@ -10145,6 +11730,27 @@ function writePersistentExtensionActivation(bundleDirectory, extensionId, enable
10145
11730
  }
10146
11731
  return target;
10147
11732
  }
11733
+ function writePersistentExtensionActivation(bundleDirectory, extensionId, enabled) {
11734
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(extensionId)) throw new Error("扩展身份无效");
11735
+ return writeActivationState(bundleDirectory, {
11736
+ schema_version: 1,
11737
+ extension_id: extensionId,
11738
+ enabled
11739
+ });
11740
+ }
11741
+ function quarantinePersistentExtension(bundleDirectory, extensionId, error, failedAtMillis = Date.now()) {
11742
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(extensionId)) throw new Error("扩展身份无效");
11743
+ return writeActivationState(bundleDirectory, {
11744
+ schema_version: 1,
11745
+ extension_id: extensionId,
11746
+ enabled: false,
11747
+ quarantine: {
11748
+ code: "runtime-load-failed",
11749
+ failed_at_millis: failedAtMillis,
11750
+ message: ((error instanceof Error ? error.message : String(error)).trim() || "persistent extension failed to load").slice(0, 2e3)
11751
+ }
11752
+ });
11753
+ }
10148
11754
  function readPersistentExtensionActivation(installationUrl) {
10149
11755
  const activationUrl = new URL("./activation.json", installationUrl);
10150
11756
  let value;
@@ -10164,6 +11770,10 @@ function readPersistentExtensionActivation(installationUrl) {
10164
11770
  if (value === null || typeof value !== "object") throw new Error("Arkme persistent extension activation state is invalid");
10165
11771
  const state = value;
10166
11772
  if (state.schema_version !== 1 || typeof state.extension_id !== "string" || typeof state.enabled !== "boolean") throw new Error("Arkme persistent extension activation state is invalid");
11773
+ if (state.quarantine !== void 0) {
11774
+ const quarantine = state.quarantine;
11775
+ if (state.enabled || quarantine.code !== "runtime-load-failed" || !Number.isSafeInteger(quarantine.failed_at_millis) || (quarantine.failed_at_millis ?? 0) < 0 || typeof quarantine.message !== "string" || quarantine.message.trim() === "" || quarantine.message.length > 2e3) throw new Error("Arkme persistent extension quarantine state is invalid");
11776
+ }
10167
11777
  return state;
10168
11778
  }
10169
11779
  function materializePersistentExtensionBundle(input) {
@@ -10192,7 +11802,8 @@ function materializePersistentExtensionBundle(input) {
10192
11802
  if (existing !== void 0) if (existing.artifact_sha256 === installation.artifact_sha256 && existing.version === installation.version) {
10193
11803
  try {
10194
11804
  const manifest = JSON.parse(readFileSync(join(bundleDirectory, "package.json"), "utf8"));
10195
- const clientReady = input.clientCode === void 0 || manifest.exports?.["./client"] === "./lib/client.js" && manifest.dsh?.client?.inject?.length === 0 && readFileSync(join(bundleDirectory, "lib", "client.js"), "utf8").includes("extensions.persistent.invoke");
11805
+ const clientBundle = input.clientCode === void 0 ? void 0 : readFileSync(join(bundleDirectory, "lib", "client.js"), "utf8");
11806
+ const clientReady = input.clientCode === void 0 || manifest.exports?.["./client"] === "./lib/client.js" && manifest.dsh?.client?.inject?.length === 0 && clientBundle?.includes("extensions.persistent.invoke") === true && clientBundle.includes("extensions.persistent.client-state");
10196
11807
  if (manifest.exports?.["."] === "./lib/index.js" && manifest.exports?.["./package.json"] === "./package.json" && clientReady) {
10197
11808
  if (!existsSync(activationPath)) writeSecure(activationPath, activationText(input.resolution.extension_id, true));
10198
11809
  return {
@@ -10453,8 +12064,22 @@ async function mountPersistentArkmeHostExtension(installation, registration) {
10453
12064
  throw error;
10454
12065
  }
10455
12066
  }
12067
+ function quarantineRuntimeFailure(installation, installationUrl, error) {
12068
+ try {
12069
+ quarantinePersistentExtension(dirname(fileURLToPath(installationUrl)), installation.extension_id, error);
12070
+ } catch (quarantineError) {
12071
+ console.error(`[arkme-extension:${installation.extension_id}] failed to persist quarantine state`, quarantineError);
12072
+ }
12073
+ console.error(`[arkme-extension:${installation.extension_id}] disabled after runtime load failure`, error);
12074
+ }
10456
12075
  async function applyPersistentArkmeHostExtension(ctx, installationUrl) {
10457
- const installation = readInstallation(installationUrl);
12076
+ let installation;
12077
+ try {
12078
+ installation = readInstallation(installationUrl);
12079
+ } catch (error) {
12080
+ console.error("[arkme-extension:unknown] ignored invalid persistent installation metadata", error);
12081
+ return;
12082
+ }
10458
12083
  const previous = persistentRegistrations.get(installation.extension_id);
10459
12084
  if (previous?.ctx === ctx && previous.installationUrl.href === installationUrl.href) return;
10460
12085
  if (previous !== void 0) await deactivatePersistentArkmeExtension(installation.extension_id);
@@ -10463,27 +12088,55 @@ async function applyPersistentArkmeHostExtension(ctx, installationUrl) {
10463
12088
  installationUrl
10464
12089
  };
10465
12090
  persistentRegistrations.set(installation.extension_id, registration);
10466
- ctx.effect(() => () => {
10467
- if (persistentRegistrations.get(installation.extension_id) !== registration) return;
10468
- persistentRegistrations.delete(installation.extension_id);
10469
- persistentHandlers.delete(installation.extension_id);
10470
- }, `arkme-extension:${installation.extension_id}:registration`);
10471
- ctx.effect(() => () => {
10472
- if (persistentHandlers.get(installation.extension_id) === registration.handlers) persistentHandlers.delete(installation.extension_id);
10473
- }, `arkme-extension:${installation.extension_id}:handlers`);
10474
- const activation = readPersistentExtensionActivation(installationUrl);
10475
- if (activation.extension_id !== installation.extension_id) throw new Error("Arkme persistent extension activation identity mismatch");
10476
- if (!activation.enabled) return;
10477
- await mountPersistentArkmeHostExtension(installation, registration);
12091
+ try {
12092
+ ctx.effect(() => () => {
12093
+ if (persistentRegistrations.get(installation.extension_id) !== registration) return;
12094
+ persistentRegistrations.delete(installation.extension_id);
12095
+ persistentHandlers.delete(installation.extension_id);
12096
+ }, `arkme-extension:${installation.extension_id}:registration`);
12097
+ ctx.effect(() => () => {
12098
+ if (persistentHandlers.get(installation.extension_id) === registration.handlers) persistentHandlers.delete(installation.extension_id);
12099
+ }, `arkme-extension:${installation.extension_id}:handlers`);
12100
+ const activation = readPersistentExtensionActivation(installationUrl);
12101
+ if (activation.extension_id !== installation.extension_id) throw new Error("Arkme persistent extension activation identity mismatch");
12102
+ if (!activation.enabled) return;
12103
+ await mountPersistentArkmeHostExtension(installation, registration);
12104
+ } catch (error) {
12105
+ if (persistentRegistrations.get(installation.extension_id) === registration) {
12106
+ persistentRegistrations.delete(installation.extension_id);
12107
+ if (persistentHandlers.get(installation.extension_id) === registration.handlers) persistentHandlers.delete(installation.extension_id);
12108
+ if (persistentClientActivations.get(installation.extension_id) === registration.clientActivation) persistentClientActivations.delete(installation.extension_id);
12109
+ }
12110
+ quarantineRuntimeFailure(installation, installationUrl, error);
12111
+ }
10478
12112
  }
10479
12113
  function persistentArkmeExtensionActive(extensionId) {
10480
12114
  return persistentRegistrations.get(extensionId)?.fiber !== void 0 || persistentClientActivations.has(extensionId);
10481
12115
  }
12116
+ function persistentArkmeExtensionRuntimeState(extensionId) {
12117
+ const registration = persistentRegistrations.get(extensionId);
12118
+ if (registration === void 0) return void 0;
12119
+ try {
12120
+ return {
12121
+ version: readInstallation(registration.installationUrl).version,
12122
+ installationUrl: registration.installationUrl.href,
12123
+ active: registration.fiber !== void 0 || registration.clientActivation !== void 0
12124
+ };
12125
+ } catch {
12126
+ return;
12127
+ }
12128
+ }
10482
12129
  /** Re-mount a verified Host half when its wrapper is already present in the current DSH process. */
10483
12130
  async function activatePersistentArkmeExtension(extensionId) {
10484
12131
  const registration = persistentRegistrations.get(extensionId);
10485
12132
  if (registration === void 0) return false;
10486
- return await mountPersistentArkmeHostExtension(readInstallation(registration.installationUrl), registration);
12133
+ const installation = readInstallation(registration.installationUrl);
12134
+ try {
12135
+ return await mountPersistentArkmeHostExtension(installation, registration);
12136
+ } catch (error) {
12137
+ quarantineRuntimeFailure(installation, registration.installationUrl, error);
12138
+ throw error;
12139
+ }
10487
12140
  }
10488
12141
  async function deactivatePersistentArkmeExtension(extensionId) {
10489
12142
  const registration = persistentRegistrations.get(extensionId);
@@ -10503,4 +12156,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
10503
12156
  return jsonValue(await handler(jsonValue(args)));
10504
12157
  }
10505
12158
  //#endregion
10506
- export { ArkmeService as A, canonicalExtensionJson as C, ARKME_EXTENSION_ICON_MAX_BYTES as D, ARKME_EXTENSION_FORMAT as E, ArkmePluginError as M, ArkmeOutgoingCallError as N, ARKME_EXTENSION_MAX_BYTES as O, assertExtensionArtifactSize as S, unpackArkmeExtension as T, inspectBundleArtifact as _, persistentArkmeExtensionActive as a, readLocalBundleTarball as b, renderArkmeBundleClientBundle as c, ARKME_BUNDLE_ARTIFACT_KIND as d, ARKME_BUNDLE_MAX_BYTES as f, canonicalBundleJson as g, bundleSha256 as h, invokePersistentArkmeExtension as i, ARKME_PROVIDER_CONTRACT_VERSION as j, ARKME_EXTENSION_PREVIEW_MAX_BYTES as k, verifyBundleResolutionSignature as l, arkmeSandboxEntryId as m, applyPersistentArkmeHostExtension as n, materializePersistentExtensionBundle as o, ArkmeBundleArtifactError as p, deactivatePersistentArkmeExtension as r, writePersistentExtensionActivation as s, activatePersistentArkmeExtension as t, verifyExtensionResolutionSignature as u, packBundleFiles as v, sha256Hex as w, renderArkmeSandboxHostEntry as x, packLocalBundleDirectory as y };
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 };