@senguoyun/dsh-arkme 0.1.11 → 0.1.12

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 (135) hide show
  1. package/README.md +4 -0
  2. package/docs/gui-production-integration.md +46 -0
  3. package/docs/record-calendar-ui-flow.md +88 -0
  4. package/docs/tool-registry.md +4 -0
  5. package/lib/client.js +24408 -18902
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +810 -55
  8. package/lib/persistent-extension.js +2 -2
  9. package/lib/{persistent-runtime-DInn3Zb1.js → persistent-runtime-kUpXXUZI.js} +1211 -201
  10. package/lib/sdk.js +88 -1
  11. package/lib/types/arkme-service.d.ts +38 -1
  12. package/lib/types/arkme-service.d.ts.map +1 -1
  13. package/lib/types/chat-realtime.d.ts +8 -0
  14. package/lib/types/chat-realtime.d.ts.map +1 -1
  15. package/lib/types/client/ArkmeAccountMenu.d.ts +4 -0
  16. package/lib/types/client/ArkmeAccountMenu.d.ts.map +1 -0
  17. package/lib/types/client/ArkmeArkoSurface.d.ts.map +1 -1
  18. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  19. package/lib/types/client/ArkmeBrand.d.ts +35 -0
  20. package/lib/types/client/ArkmeBrand.d.ts.map +1 -0
  21. package/lib/types/client/ArkmeCalendarSurface.d.ts +10 -0
  22. package/lib/types/client/ArkmeCalendarSurface.d.ts.map +1 -0
  23. package/lib/types/client/ArkmeConversationSurface.d.ts +5 -3
  24. package/lib/types/client/ArkmeConversationSurface.d.ts.map +1 -1
  25. package/lib/types/client/ArkmeExtensionAvatar.d.ts.map +1 -1
  26. package/lib/types/client/ArkmeExtensionCenter.d.ts +17 -4
  27. package/lib/types/client/ArkmeExtensionCenter.d.ts.map +1 -1
  28. package/lib/types/client/ArkmeFooterDropdown.d.ts +1 -1
  29. package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeGroupChatControls.d.ts +1 -0
  31. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  32. package/lib/types/client/ArkmeLogin.d.ts.map +1 -1
  33. package/lib/types/client/ArkmePrivateCallMenu.d.ts.map +1 -1
  34. package/lib/types/client/ArkmeProductNavigation.d.ts +8 -0
  35. package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -0
  36. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  37. package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
  38. package/lib/types/client/ArkmeSendToSelfIcon.d.ts +5 -0
  39. package/lib/types/client/ArkmeSendToSelfIcon.d.ts.map +1 -0
  40. package/lib/types/client/ArkmeSettingsRow.d.ts.map +1 -1
  41. package/lib/types/client/ArkmeSettingsSurface.d.ts +2 -0
  42. package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -0
  43. package/lib/types/client/ArkmeSharedExtensionDetail.d.ts +7 -0
  44. package/lib/types/client/ArkmeSharedExtensionDetail.d.ts.map +1 -0
  45. package/lib/types/client/ArkmeSidebar.d.ts +11 -2
  46. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  47. package/lib/types/client/ArkmeTopicCreateDialog.d.ts +1 -1
  48. package/lib/types/client/ArkmeTopicCreateDialog.d.ts.map +1 -1
  49. package/lib/types/client/ArkmeTopicDirectoryPopover.d.ts +1 -0
  50. package/lib/types/client/ArkmeTopicDirectoryPopover.d.ts.map +1 -1
  51. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +6 -1
  52. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  53. package/lib/types/client/api.d.ts +1 -1
  54. package/lib/types/client/api.d.ts.map +1 -1
  55. package/lib/types/client/arkme-assets.d.ts +2 -2
  56. package/lib/types/client/arkme-assets.d.ts.map +1 -1
  57. package/lib/types/client/arkme-theme.d.ts +5 -3
  58. package/lib/types/client/arkme-theme.d.ts.map +1 -1
  59. package/lib/types/client/arkme-wordmark.d.ts +3 -0
  60. package/lib/types/client/arkme-wordmark.d.ts.map +1 -0
  61. package/lib/types/client/desktop-notification-runtime.d.ts +35 -0
  62. package/lib/types/client/desktop-notification-runtime.d.ts.map +1 -0
  63. package/lib/types/client/extension-share-deeplink.d.ts +5 -0
  64. package/lib/types/client/extension-share-deeplink.d.ts.map +1 -0
  65. package/lib/types/client/index.d.ts +7 -0
  66. package/lib/types/client/index.d.ts.map +1 -1
  67. package/lib/types/client/interwoven-moments.d.ts +1 -1
  68. package/lib/types/client/interwoven-moments.d.ts.map +1 -1
  69. package/lib/types/client/notification-activation-store.d.ts +16 -0
  70. package/lib/types/client/notification-activation-store.d.ts.map +1 -0
  71. package/lib/types/client/related-recordings.d.ts +1 -0
  72. package/lib/types/client/related-recordings.d.ts.map +1 -1
  73. package/lib/types/client/ui-controller.d.ts +10 -1
  74. package/lib/types/client/ui-controller.d.ts.map +1 -1
  75. package/lib/types/extensions/manager.d.ts +18 -1
  76. package/lib/types/extensions/manager.d.ts.map +1 -1
  77. package/lib/types/extensions/owned-inventory.d.ts +4 -1
  78. package/lib/types/extensions/owned-inventory.d.ts.map +1 -1
  79. package/lib/types/extensions/owned-types.d.ts +1 -0
  80. package/lib/types/extensions/owned-types.d.ts.map +1 -1
  81. package/lib/types/extensions/persistent-bundle.d.ts +7 -0
  82. package/lib/types/extensions/persistent-bundle.d.ts.map +1 -1
  83. package/lib/types/extensions/persistent-client-bundle.d.ts.map +1 -1
  84. package/lib/types/extensions/persistent-runtime.d.ts +6 -0
  85. package/lib/types/extensions/persistent-runtime.d.ts.map +1 -1
  86. package/lib/types/extensions/publish-client.d.ts +5 -1
  87. package/lib/types/extensions/publish-client.d.ts.map +1 -1
  88. package/lib/types/extensions/share-discovery-relay.d.ts +33 -0
  89. package/lib/types/extensions/share-discovery-relay.d.ts.map +1 -0
  90. package/lib/types/extensions/types.d.ts +48 -1
  91. package/lib/types/extensions/types.d.ts.map +1 -1
  92. package/lib/types/host-api.d.ts.map +1 -1
  93. package/lib/types/index.d.ts +4 -2
  94. package/lib/types/index.d.ts.map +1 -1
  95. package/lib/types/rich-media-routes.d.ts.map +1 -1
  96. package/lib/types/sdk/index.d.ts +44 -4
  97. package/lib/types/sdk/index.d.ts.map +1 -1
  98. package/lib/types/services/calendar-service.d.ts +25 -0
  99. package/lib/types/services/calendar-service.d.ts.map +1 -0
  100. package/lib/types/services/chat-realtime-service.d.ts +19 -4
  101. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  102. package/lib/types/services/group-service.d.ts +17 -2
  103. package/lib/types/services/group-service.d.ts.map +1 -1
  104. package/lib/types/services/media-service.d.ts +9 -1
  105. package/lib/types/services/media-service.d.ts.map +1 -1
  106. package/lib/types/services/search-service.d.ts +10 -2
  107. package/lib/types/services/search-service.d.ts.map +1 -1
  108. package/lib/types/services/service.d.ts +1 -0
  109. package/lib/types/services/service.d.ts.map +1 -1
  110. package/lib/types/services/world-service.d.ts +7 -1
  111. package/lib/types/services/world-service.d.ts.map +1 -1
  112. package/lib/types/tools/business/calendar/index.d.ts +4 -0
  113. package/lib/types/tools/business/calendar/index.d.ts.map +1 -0
  114. package/lib/types/tools/business/groups/index.d.ts +3 -0
  115. package/lib/types/tools/business/groups/index.d.ts.map +1 -0
  116. package/lib/types/tools/business/index.d.ts.map +1 -1
  117. package/lib/types/tools/business/records/images.d.ts +2 -0
  118. package/lib/types/tools/business/records/images.d.ts.map +1 -0
  119. package/lib/types/tools/business/records/index.d.ts.map +1 -1
  120. package/lib/types/tools/extensions/index.d.ts.map +1 -1
  121. package/lib/types/tools/extensions/publish-conversation.d.ts +1 -0
  122. package/lib/types/tools/extensions/publish-conversation.d.ts.map +1 -1
  123. package/lib/types/tools/ports/calendar.d.ts +17 -0
  124. package/lib/types/tools/ports/calendar.d.ts.map +1 -0
  125. package/lib/types/tools/ports/groups.d.ts +11 -0
  126. package/lib/types/tools/ports/groups.d.ts.map +1 -0
  127. package/lib/types/tools/ports/index.d.ts +4 -2
  128. package/lib/types/tools/ports/index.d.ts.map +1 -1
  129. package/lib/types/tools/ports/records.d.ts +6 -1
  130. package/lib/types/tools/ports/records.d.ts.map +1 -1
  131. package/lib/types/tools/prompts/business.d.ts.map +1 -1
  132. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  133. package/lib/types/types.d.ts +172 -2
  134. package/lib/types/types.d.ts.map +1 -1
  135. package/package.json +5 -4
@@ -1,6 +1,7 @@
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";
6
7
  import { createContext, runInContext } from "node:vm";
@@ -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);
@@ -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$21(value) {
1083
1095
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1084
1096
  }
1085
- function booleanValue$11(value) {
1097
+ function booleanValue$12(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$12(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$21(data.selected_duration_millis)),
1115
+ minimumDurationMillis: Math.max(0, numberValue$21(data.minimum_duration_millis)),
1116
+ selectedSegmentCount: Math.max(0, numberValue$21(data.selected_segment_count)),
1117
+ retryable: booleanValue$12(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$12(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$12(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$21(data.selected_duration_millis)),
1167
+ minimumDurationMillis: Math.max(0, numberValue$21(data.minimum_duration_millis)),
1168
+ selectedSegmentCount: Math.max(0, numberValue$21(data.selected_segment_count)),
1169
+ selectedTextCount: Math.max(0, numberValue$21(data.selected_text_count)),
1170
+ retryable: booleanValue$12(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$21(data.progress)))),
1204
1216
  selectedSegmentCount: segmentCount,
1205
1217
  ...textCount === 0 ? {} : { selectedTextCount: textCount },
1206
- retryable: booleanValue$11(data.retryable),
1218
+ retryable: booleanValue$12(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$21(data.video_duration_millis) <= 0 ? {} : { videoDurationMillis: numberValue$21(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$21(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$21(source.started_at),
1247
+ selectedDurationMillis: Math.max(0, numberValue$21(source.selected_duration_millis)),
1248
+ selectedSegmentCount: Math.max(0, Math.trunc(numberValue$21(item.selected_segment_count))),
1249
+ retryable: booleanValue$12(item.retryable),
1250
+ createdAtMillis: numberValue$21(item.created_at),
1251
+ updatedAtMillis: numberValue$21(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$21(item.video_duration_millis) <= 0 ? {} : { videoDurationMillis: numberValue$21(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$20(value) {
1269
1281
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1270
1282
  }
1271
- function booleanValue$10(value) {
1283
+ function booleanValue$11(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$20(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$11(data.recommended),
1340
+ selected: booleanValue$11(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$20(data.id ?? data.msg_id ?? data.msgId);
1369
+ const sessionId = numberValue$20(data.session_id ?? data.sessionId);
1370
+ const roleCode = numberValue$20(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$20(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$20(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$11(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$20(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$20((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$20(accepted.user_msg_id);
1520
+ const assistantMsgId = numberValue$20(accepted.assistant_msg_id);
1521
+ const acceptedSessionId = numberValue$20(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$20(data.sequence);
1558
+ const surfaceAssistantMsgId = numberValue$20(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$11(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$11(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$11(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$19(value) {
1785
1797
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1786
1798
  }
1787
- function booleanValue$9(value) {
1799
+ function booleanValue$10(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$19(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$19(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$19(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$19(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$19(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$19(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$19(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$19(item.due_at));
2114
+ const remindAtMillis = Math.trunc(numberValue$19(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$10(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$19(item.create_at)),
2123
+ updatedAtMillis: Math.trunc(numberValue$19(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$19(item.due_at));
2145
+ const remindAtMillis = Math.trunc(numberValue$19(item.remind_at));
2146
+ const readAtMillis = Math.trunc(numberValue$19(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$19(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$19(item.create_at)),
2157
+ updatedAtMillis: Math.trunc(numberValue$19(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$18(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$18(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$18(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$18((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$18((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$18(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$17(value) {
2458
2470
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
2459
2471
  }
2460
- function booleanValue$8(value) {
2472
+ function booleanValue$9(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;
@@ -2588,19 +2600,19 @@ var BotService = class {
2588
2600
  const group = await this.openGroupSourceRef(groupSourceRef, session.userId);
2589
2601
  const data = await this.runtime.authenticatedBotPost("/api/v1/bot/group/list", { subject_uid: group.ownerRef }, session, options.signal);
2590
2602
  const items = [];
2591
- for (const value of listValue$17(data.bots)) {
2603
+ for (const value of listValue$18(data.bots)) {
2592
2604
  const raw = objectValue$1(value);
2593
2605
  if (stringValue$1(raw.provider).trim() !== "openclaw") continue;
2594
2606
  const { directChatAvailable: _directChatAvailable, ...summary } = await this.botSummaryFromData(raw, session.userId);
2595
2607
  items.push({
2596
2608
  ...summary,
2597
- installed: booleanValue$8(raw.installed)
2609
+ installed: booleanValue$9(raw.installed)
2598
2610
  });
2599
2611
  }
2600
2612
  return {
2601
2613
  groupSourceRef,
2602
2614
  displayName: stringValue$1(data.subject_title).trim() || group.displayName,
2603
- canAddBots: booleanValue$8(data.can_current_user_add_bots),
2615
+ canAddBots: booleanValue$9(data.can_current_user_add_bots),
2604
2616
  items
2605
2617
  };
2606
2618
  }
@@ -2653,7 +2665,7 @@ var BotService = class {
2653
2665
  };
2654
2666
  }
2655
2667
  async sealBotRef(userId, botId) {
2656
- const payload = encodeOpaqueJson$5({
2668
+ const payload = encodeOpaqueJson$6({
2657
2669
  version: 1,
2658
2670
  userId,
2659
2671
  botId
@@ -2669,13 +2681,13 @@ var BotService = class {
2669
2681
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
2670
2682
  let raw;
2671
2683
  try {
2672
- raw = objectValue$1(decodeOpaqueJson$5(payload));
2684
+ raw = objectValue$1(decodeOpaqueJson$6(payload));
2673
2685
  } catch (error) {
2674
2686
  throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false, 400, { cause: error });
2675
2687
  }
2676
2688
  const reference = {
2677
2689
  version: 1,
2678
- userId: numberValue$16(raw.userId),
2690
+ userId: numberValue$17(raw.userId),
2679
2691
  botId: stringValue$1(raw.botId).trim()
2680
2692
  };
2681
2693
  if (raw.version !== 1 || reference.userId !== expectedUserId || reference.botId === "") throw new ArkmePluginError("bot-ref-invalid", "Bot 引用与当前账号不匹配", false, 403);
@@ -2683,6 +2695,181 @@ var BotService = class {
2683
2695
  }
2684
2696
  };
2685
2697
  //#endregion
2698
+ //#region src/services/calendar-service.ts
2699
+ const MAX_CALENDAR_RANGE_DAYS = 62;
2700
+ const MAX_DAY_RECORD_LIMIT = 50;
2701
+ function numberValue$16(value) {
2702
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2703
+ }
2704
+ function booleanValue$8(value) {
2705
+ if (typeof value === "boolean") return value;
2706
+ if (typeof value === "number") return value !== 0;
2707
+ }
2708
+ function listValue$17(value) {
2709
+ return Array.isArray(value) ? value : [];
2710
+ }
2711
+ function readCalendarDate(value, field) {
2712
+ const normalized = value.trim();
2713
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalized);
2714
+ if (match === null) throw new ArkmePluginError("calendar-date-invalid", `${field} 必须是 YYYY-MM-DD`, false);
2715
+ const year = Number(match[1]);
2716
+ const month = Number(match[2]);
2717
+ const day = Number(match[3]);
2718
+ const date = new Date(Date.UTC(year, month - 1, day));
2719
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) throw new ArkmePluginError("calendar-date-invalid", `${field} 必须是真实日期`, false);
2720
+ return normalized;
2721
+ }
2722
+ function calendarDayNumber(date) {
2723
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
2724
+ if (match === null) throw new ArkmePluginError("calendar-date-invalid", "日期格式无效", false);
2725
+ const year = Number(match[1]);
2726
+ const month = Number(match[2]);
2727
+ const day = Number(match[3]);
2728
+ return Math.trunc(Date.UTC(year, month - 1, day) / 864e5);
2729
+ }
2730
+ function readTimezone(value) {
2731
+ const normalized = value?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
2732
+ if (normalized.length > 80) throw new ArkmePluginError("calendar-timezone-invalid", "timezone 无效", false);
2733
+ return normalized;
2734
+ }
2735
+ function boundedLimit(value) {
2736
+ const limit = Math.trunc(value ?? 20);
2737
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_DAY_RECORD_LIMIT) throw new ArkmePluginError("calendar-limit-invalid", `limit 必须是 1-${MAX_DAY_RECORD_LIMIT} 的整数`, false);
2738
+ return limit;
2739
+ }
2740
+ function contentAccessState(value) {
2741
+ const code = Math.trunc(numberValue$16(value));
2742
+ if (code === 1) return "available";
2743
+ if (code === 2) return "protected";
2744
+ return "unknown";
2745
+ }
2746
+ function sourceKind(raw) {
2747
+ const topic = objectValue$1(raw.topic_core);
2748
+ const chat = objectValue$1(raw.chat_core);
2749
+ if (stringValue$1(topic.topic_uid).trim() !== "") return "topic";
2750
+ if (stringValue$1(chat.chat_session_uid).trim() !== "") return "chat";
2751
+ if (booleanValue$8(raw.is_uncategorized) === true) return "self";
2752
+ return "unknown";
2753
+ }
2754
+ var CalendarService = class {
2755
+ runtime;
2756
+ constructor(runtime) {
2757
+ this.runtime = runtime;
2758
+ }
2759
+ async bucketPage(options) {
2760
+ const startDate = readCalendarDate(options.startDate, "start_date");
2761
+ const endDate = readCalendarDate(options.endDate, "end_date");
2762
+ const startDay = calendarDayNumber(startDate);
2763
+ const endDay = calendarDayNumber(endDate);
2764
+ if (startDay > endDay) throw new ArkmePluginError("calendar-range-invalid", "start_date 不能晚于 end_date", false);
2765
+ if (endDay - startDay + 1 > MAX_CALENDAR_RANGE_DAYS) throw new ArkmePluginError("calendar-range-too-wide", `日历范围最多查询 ${MAX_CALENDAR_RANGE_DAYS} 天`, false);
2766
+ const timezone = readTimezone(options.timezone);
2767
+ const session = await this.runtime.requireSession();
2768
+ const data = await this.runtime.authenticatedCalendarPost("/api/v1/calendar/buckets/query", {
2769
+ bucket_scope_kind: 1,
2770
+ bucket_scope_uid: "",
2771
+ start_date: startDate,
2772
+ end_date: endDate,
2773
+ timezone
2774
+ }, session, options.signal, {
2775
+ key: `calendar:buckets:self:${startDate}:${endDate}:${timezone}`,
2776
+ cacheMs: 3e4,
2777
+ failureCooldownMs: 2e3
2778
+ });
2779
+ return {
2780
+ scope: "self",
2781
+ startDate,
2782
+ endDate,
2783
+ timezone: stringValue$1(data.timezone).trim() || timezone,
2784
+ refreshedAtMillis: Date.now(),
2785
+ days: listValue$17(data.daily_data).map((raw) => this.bucketDay(raw)).filter((day) => day !== void 0)
2786
+ };
2787
+ }
2788
+ async dayRecords(options) {
2789
+ const bucketDate = readCalendarDate(options.bucketDate, "bucket_date");
2790
+ const timezone = readTimezone(options.timezone);
2791
+ const limit = boundedLimit(options.limit);
2792
+ const cursorSendAt = Math.trunc(options.cursor?.sendAtMillis ?? 0);
2793
+ const cursorRecordUid = options.cursor?.recordUid.trim() ?? "";
2794
+ const session = await this.runtime.requireSession();
2795
+ const data = await this.runtime.authenticatedCalendarPost("/api/v1/calendar/records/query", {
2796
+ bucket_scope_kind: 1,
2797
+ bucket_scope_uid: "",
2798
+ bucket_date: bucketDate,
2799
+ timezone,
2800
+ limit,
2801
+ ...cursorSendAt > 0 && cursorRecordUid !== "" ? {
2802
+ cursor_send_at: cursorSendAt,
2803
+ cursor_record_uid: cursorRecordUid
2804
+ } : {}
2805
+ }, session, options.signal, {
2806
+ key: `calendar:records:self:${bucketDate}:${timezone}:${String(limit)}:${String(cursorSendAt)}:${cursorRecordUid}`,
2807
+ cacheMs: 1e4,
2808
+ failureCooldownMs: 2e3
2809
+ });
2810
+ const nextSendAt = Math.trunc(numberValue$16(data.next_cursor_send_at));
2811
+ const nextUid = stringValue$1(data.next_cursor_record_uid).trim();
2812
+ return {
2813
+ scope: "self",
2814
+ bucketDate,
2815
+ timezone: stringValue$1(data.timezone).trim() || timezone,
2816
+ refreshedAtMillis: Date.now(),
2817
+ items: listValue$17(data.items).map((raw) => this.dayRecord(raw)).filter((item) => item !== void 0),
2818
+ hasMore: data.has_more === true,
2819
+ ...nextSendAt > 0 && nextUid !== "" ? { nextCursor: {
2820
+ sendAtMillis: nextSendAt,
2821
+ recordUid: nextUid
2822
+ } } : {}
2823
+ };
2824
+ }
2825
+ bucketDay(raw) {
2826
+ const item = objectValue$1(raw);
2827
+ const bucketDate = stringValue$1(item.bucket_date).trim();
2828
+ if (bucketDate === "") return void 0;
2829
+ const count = Math.max(0, Math.trunc(numberValue$16(item.count)));
2830
+ return {
2831
+ bucketDate,
2832
+ count,
2833
+ protectedCount: Math.max(0, Math.trunc(numberValue$16(item.protected_count))),
2834
+ hasRecords: count > 0,
2835
+ ...numberValue$16(item.first_send_at) > 0 ? { firstSendAtMillis: Math.trunc(numberValue$16(item.first_send_at)) } : {}
2836
+ };
2837
+ }
2838
+ dayRecord(raw) {
2839
+ const item = objectValue$1(raw);
2840
+ const core = objectValue$1(item.record_core);
2841
+ const recordUid = stringValue$1(item.record_uid ?? core.record_uid).trim();
2842
+ const sendAtMillis = Math.trunc(numberValue$16(item.send_at ?? core.send_at));
2843
+ if (recordUid === "" || sendAtMillis <= 0) return void 0;
2844
+ const accessState = contentAccessState(core.content_access_state);
2845
+ const available = accessState === "available";
2846
+ const title = available ? clippedText(core.title, 500) : "";
2847
+ const textContent = available ? clippedText(core.text_content, 4e3) : "";
2848
+ const preview = title || clippedText(textContent, 160) || (available ? "无文字内容" : "受保护内容");
2849
+ const topic = objectValue$1(item.topic_core);
2850
+ const isUncategorized = booleanValue$8(item.is_uncategorized);
2851
+ const hasManualEdit = booleanValue$8(core.has_manual_edit);
2852
+ const hasPolish = booleanValue$8(core.has_polish);
2853
+ return {
2854
+ recordUid,
2855
+ sendAtMillis,
2856
+ accessState,
2857
+ title,
2858
+ textContent,
2859
+ preview,
2860
+ ...stringValue$1(topic.title).trim() === "" ? {} : { topicTitle: stringValue$1(topic.title).trim() },
2861
+ sourceKind: sourceKind(item),
2862
+ creationSource: Math.trunc(numberValue$16(core.creation_source)),
2863
+ templateKind: Math.trunc(numberValue$16(core.template_kind)),
2864
+ displayKind: Math.trunc(numberValue$16(core.display_kind)),
2865
+ protected: accessState === "protected",
2866
+ ...isUncategorized === void 0 ? {} : { isUncategorized },
2867
+ ...hasManualEdit === void 0 ? {} : { hasManualEdit },
2868
+ ...hasPolish === void 0 ? {} : { hasPolish }
2869
+ };
2870
+ }
2871
+ };
2872
+ //#endregion
2686
2873
  //#region src/chat-realtime.ts
2687
2874
  const ARKME_CHAT_SSE_PATH = "/api/v1/sse/chat/noty";
2688
2875
  const DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
@@ -2690,7 +2877,7 @@ const DEFAULT_INACTIVITY_TIMEOUT_MS = 75e3;
2690
2877
  const DEFAULT_LEASE_DURATION_MS = 57e4;
2691
2878
  const DEFAULT_IDLE_POLL_MS = 2e3;
2692
2879
  const DEFAULT_RETRY_BASE_MS = 1e3;
2693
- const DEFAULT_MAX_RETRY_MS = 6e4;
2880
+ const DEFAULT_MAX_RETRY_MS = 15e3;
2694
2881
  const DEFAULT_STABLE_CONNECTION_MS = 3e4;
2695
2882
  const MAX_SEEN_EVENTS = 256;
2696
2883
  var ArkmeChatRealtimeAuthError = class extends Error {};
@@ -2771,11 +2958,14 @@ var ArkmeChatRealtimeRuntime = class {
2771
2958
  now;
2772
2959
  rootController;
2773
2960
  connectionController;
2961
+ waitController;
2774
2962
  loop;
2775
2963
  revision = 0;
2776
2964
  connected = false;
2777
2965
  lastAcceptedAccessToken;
2778
2966
  lastConnectionLifetimeMs = 0;
2967
+ connectionGeneration = 0;
2968
+ forceReconnectRequested = false;
2779
2969
  lastEventAtMillis;
2780
2970
  seenEventUids = /* @__PURE__ */ new Set();
2781
2971
  listeners = /* @__PURE__ */ new Set();
@@ -2812,14 +3002,19 @@ var ArkmeChatRealtimeRuntime = class {
2812
3002
  stop() {
2813
3003
  this.rootController?.abort();
2814
3004
  this.connectionController?.abort();
3005
+ this.waitController?.abort();
2815
3006
  }
2816
3007
  reconnect() {
3008
+ if (this.loop === void 0) return;
3009
+ this.forceReconnectRequested = true;
2817
3010
  this.connectionController?.abort();
3011
+ this.waitController?.abort();
2818
3012
  }
2819
3013
  state() {
2820
3014
  return {
2821
3015
  revision: this.revision,
2822
3016
  connected: this.connected,
3017
+ connectionGeneration: this.connectionGeneration,
2823
3018
  ...this.lastEventAtMillis === void 0 ? {} : { lastEventAtMillis: this.lastEventAtMillis }
2824
3019
  };
2825
3020
  }
@@ -2841,16 +3036,17 @@ var ArkmeChatRealtimeRuntime = class {
2841
3036
  if (session === void 0) {
2842
3037
  this.connected = false;
2843
3038
  blockedAccessToken = void 0;
2844
- await waitFor(this.idlePollMs, signal);
3039
+ await this.wait(this.idlePollMs, signal);
2845
3040
  continue;
2846
3041
  }
2847
3042
  if (blockedAccessToken === session.accessToken) {
2848
3043
  this.connected = false;
2849
- await waitFor(this.idlePollMs, signal);
3044
+ await this.wait(this.idlePollMs, signal);
2850
3045
  continue;
2851
3046
  }
2852
3047
  blockedAccessToken = void 0;
2853
3048
  let stableConnection = false;
3049
+ this.options.diagnostic?.("reconnect_attempt", { connectionGeneration: this.connectionGeneration + 1 });
2854
3050
  try {
2855
3051
  await this.consume(session, signal);
2856
3052
  stableConnection = this.lastConnectionLifetimeMs >= this.stableConnectionMs;
@@ -2865,7 +3061,7 @@ var ArkmeChatRealtimeRuntime = class {
2865
3061
  if (refreshedButUnacceptedToken === session.accessToken) {
2866
3062
  blockedAccessToken = session.accessToken;
2867
3063
  refreshedButUnacceptedToken = void 0;
2868
- await waitFor(this.idlePollMs, signal);
3064
+ await this.wait(this.idlePollMs, signal);
2869
3065
  continue;
2870
3066
  }
2871
3067
  let refreshed;
@@ -2876,17 +3072,24 @@ var ArkmeChatRealtimeRuntime = class {
2876
3072
  }
2877
3073
  if (refreshed !== void 0 && refreshed.accessToken !== session.accessToken) {
2878
3074
  refreshedButUnacceptedToken = refreshed.accessToken;
3075
+ this.forceReconnectRequested = false;
2879
3076
  retryMs = this.retryBaseMs;
2880
3077
  continue;
2881
3078
  }
2882
3079
  blockedAccessToken = session.accessToken;
2883
- await waitFor(this.idlePollMs, signal);
3080
+ await this.wait(this.idlePollMs, signal);
2884
3081
  continue;
2885
3082
  }
2886
3083
  } finally {
3084
+ if (this.connected && !signal.aborted) this.options.diagnostic?.("sse_disconnected", { connectionGeneration: this.connectionGeneration });
2887
3085
  this.connected = false;
2888
3086
  }
2889
- await waitFor(Math.max(1, Math.round(retryMs * (.8 + this.random() * .4))), signal);
3087
+ if (this.forceReconnectRequested) {
3088
+ this.forceReconnectRequested = false;
3089
+ retryMs = this.retryBaseMs;
3090
+ continue;
3091
+ }
3092
+ await this.wait(this.jitteredDelay(retryMs), signal);
2890
3093
  if (!stableConnection) retryMs = Math.min(this.maxRetryMs, Math.max(this.retryBaseMs, retryMs * 2));
2891
3094
  }
2892
3095
  }
@@ -2922,6 +3125,7 @@ var ArkmeChatRealtimeRuntime = class {
2922
3125
  acceptedAtMillis = this.now();
2923
3126
  this.connected = true;
2924
3127
  this.lastAcceptedAccessToken = session.accessToken;
3128
+ this.connectionGeneration += 1;
2925
3129
  this.advanceRevision("reconcile");
2926
3130
  leaseTimer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("chat SSE lease rotation")), this.leaseDurationMs);
2927
3131
  const reader = response.body.getReader();
@@ -2946,6 +3150,25 @@ var ArkmeChatRealtimeRuntime = class {
2946
3150
  if (this.connectionController === controller) this.connectionController = void 0;
2947
3151
  }
2948
3152
  }
3153
+ jitteredDelay(milliseconds) {
3154
+ const random = Math.min(1, Math.max(0, this.random()));
3155
+ return Math.max(0, Math.round(milliseconds * (.8 + random * .4)));
3156
+ }
3157
+ async wait(milliseconds, rootSignal) {
3158
+ if (rootSignal.aborted) return;
3159
+ const controller = new AbortController();
3160
+ const abortFromRoot = () => {
3161
+ controller.abort(rootSignal.reason);
3162
+ };
3163
+ rootSignal.addEventListener("abort", abortFromRoot, { once: true });
3164
+ this.waitController = controller;
3165
+ try {
3166
+ await waitFor(milliseconds, controller.signal);
3167
+ } finally {
3168
+ rootSignal.removeEventListener("abort", abortFromRoot);
3169
+ if (this.waitController === controller) this.waitController = void 0;
3170
+ }
3171
+ }
2949
3172
  acceptLine(line) {
2950
3173
  const event = decodeArkmeChatReceiveDataLine(line);
2951
3174
  if (event === void 0 || this.seenEventUids.has(event.eventUid)) return;
@@ -2984,11 +3207,15 @@ var ChatRealtimeService = class {
2984
3207
  projectionReader;
2985
3208
  chatRealtime;
2986
3209
  chatClientListeners = /* @__PURE__ */ new Set();
2987
- pendingProjectionSequences = /* @__PURE__ */ new Map();
3210
+ pendingChatProjections = /* @__PURE__ */ new Map();
2988
3211
  projectionRetryCounts = /* @__PURE__ */ new Map();
2989
3212
  projectionTimer;
2990
3213
  projectionInFlight = false;
2991
3214
  projectionFailureCount = 0;
3215
+ notificationBaselineGeneration = 0;
3216
+ notificationBaselineSequences = /* @__PURE__ */ new Map();
3217
+ notificationBaselineRetryTimer;
3218
+ notificationBaselineRetryCount = 0;
2992
3219
  chatClientRevision = 0;
2993
3220
  constructor(runtime, source, projectionReader) {
2994
3221
  this.runtime = runtime;
@@ -3005,7 +3232,10 @@ var ChatRealtimeService = class {
3005
3232
  return;
3006
3233
  }
3007
3234
  },
3008
- fetchImpl: runtime.fetchImpl
3235
+ fetchImpl: runtime.fetchImpl,
3236
+ diagnostic: (event, details) => {
3237
+ console.info(`dsh-arkme: ${event}`, details);
3238
+ }
3009
3239
  });
3010
3240
  }
3011
3241
  reconnect() {
@@ -3013,9 +3243,13 @@ var ChatRealtimeService = class {
3013
3243
  }
3014
3244
  dispose() {
3015
3245
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
3246
+ if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
3016
3247
  this.projectionTimer = void 0;
3017
- this.pendingProjectionSequences.clear();
3248
+ this.notificationBaselineRetryTimer = void 0;
3249
+ this.pendingChatProjections.clear();
3018
3250
  this.projectionRetryCounts.clear();
3251
+ this.notificationBaselineSequences.clear();
3252
+ this.notificationBaselineGeneration = 0;
3019
3253
  this.chatClientListeners.clear();
3020
3254
  }
3021
3255
  startChatRealtime() {
@@ -3027,9 +3261,13 @@ var ChatRealtimeService = class {
3027
3261
  unsubscribe();
3028
3262
  stop();
3029
3263
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
3264
+ if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
3030
3265
  this.projectionTimer = void 0;
3031
- this.pendingProjectionSequences.clear();
3266
+ this.notificationBaselineRetryTimer = void 0;
3267
+ this.pendingChatProjections.clear();
3032
3268
  this.projectionRetryCounts.clear();
3269
+ this.notificationBaselineSequences.clear();
3270
+ this.notificationBaselineGeneration = 0;
3033
3271
  };
3034
3272
  }
3035
3273
  chatRealtimeState() {
@@ -3047,26 +3285,93 @@ var ChatRealtimeService = class {
3047
3285
  type: "reconcile",
3048
3286
  revision: this.chatClientRevision,
3049
3287
  connected: state.connected,
3288
+ connectionGeneration: state.connectionGeneration,
3050
3289
  refresh: "if-stale"
3051
3290
  };
3052
3291
  }
3053
3292
  handleChatRealtimeNotice(notice) {
3054
3293
  if (notice.cause === "reconcile") {
3294
+ const generation = notice.state.connectionGeneration;
3295
+ this.notificationBaselineGeneration = 0;
3296
+ this.notificationBaselineSequences.clear();
3297
+ this.notificationBaselineRetryCount = 0;
3298
+ if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
3299
+ this.notificationBaselineRetryTimer = void 0;
3055
3300
  this.emitChatClientEvent({
3056
3301
  type: "reconcile",
3057
3302
  revision: this.nextChatClientRevision(),
3058
3303
  connected: notice.state.connected,
3304
+ connectionGeneration: generation,
3059
3305
  refresh: "none"
3060
3306
  });
3307
+ this.reconcileChatNotificationBaseline(generation);
3061
3308
  return;
3062
3309
  }
3063
- if (notice.cause === "hint" && notice.hint !== void 0) this.scheduleChatSessionProjection(notice.hint.chatSessionUid, notice.hint.latestSequence);
3310
+ if (notice.cause === "hint" && notice.hint !== void 0) this.scheduleChatSessionProjection(notice.hint.chatSessionUid, notice.hint.latestSequence, {
3311
+ hint: notice.hint,
3312
+ connectionGeneration: notice.state.connectionGeneration,
3313
+ attempts: 0
3314
+ });
3064
3315
  }
3065
- scheduleChatSessionProjection(chatSessionUid, latestSequence) {
3316
+ async reconcileChatNotificationBaseline(connectionGeneration) {
3317
+ try {
3318
+ const session = await this.runtime.requireSession();
3319
+ const sequences = /* @__PURE__ */ new Map();
3320
+ let cursor;
3321
+ for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
3322
+ const page = await this.source.listSources("root", {
3323
+ limit: 50,
3324
+ refresh: true,
3325
+ ...cursor === void 0 ? {} : { cursor }
3326
+ });
3327
+ for (const item of page.items) {
3328
+ if (item.kind !== "private_chat" && item.kind !== "group_chat") continue;
3329
+ const source = await this.source.openSourceRef(item.sourceRef, session.userId);
3330
+ sequences.set(source.ownerRef, item.latestSequence ?? 0);
3331
+ }
3332
+ if (!page.hasMore || page.nextCursor === void 0) break;
3333
+ cursor = page.nextCursor;
3334
+ }
3335
+ const state = this.chatRealtime.state();
3336
+ if (!state.connected || state.connectionGeneration !== connectionGeneration) return;
3337
+ this.notificationBaselineSequences.clear();
3338
+ for (const [uid, sequence] of sequences) this.notificationBaselineSequences.set(uid, sequence);
3339
+ this.notificationBaselineGeneration = connectionGeneration;
3340
+ this.notificationBaselineRetryCount = 0;
3341
+ console.info("dsh-arkme: reconcile_completed", {
3342
+ connectionGeneration,
3343
+ sessionCount: sequences.size
3344
+ });
3345
+ if (this.pendingChatProjections.size > 0 && this.projectionTimer === void 0 && !this.projectionInFlight) this.projectionTimer = setTimeout(() => {
3346
+ this.projectionTimer = void 0;
3347
+ this.flushChatSessionProjections();
3348
+ }, 0);
3349
+ } catch (error) {
3350
+ const state = this.chatRealtime.state();
3351
+ if (!state.connected || state.connectionGeneration !== connectionGeneration) return;
3352
+ this.notificationBaselineRetryCount += 1;
3353
+ const delay = Math.min(15e3, 1e3 * 2 ** Math.min(4, this.notificationBaselineRetryCount - 1));
3354
+ console.warn("dsh-arkme: Chat reconnect reconciliation failed:", safeFailureMessage$6(error));
3355
+ this.notificationBaselineRetryTimer = setTimeout(() => {
3356
+ this.notificationBaselineRetryTimer = void 0;
3357
+ this.reconcileChatNotificationBaseline(connectionGeneration);
3358
+ }, delay);
3359
+ }
3360
+ }
3361
+ scheduleChatSessionProjection(chatSessionUid, latestSequence, notificationHint) {
3066
3362
  const uid = chatSessionUid.trim();
3067
3363
  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));
3364
+ const current = this.pendingChatProjections.get(uid);
3365
+ if (latestSequence > (current?.latestSequence ?? 0)) this.projectionRetryCounts.delete(uid);
3366
+ const notificationHints = current === void 0 ? [] : [...current.notificationHints];
3367
+ if (notificationHint !== void 0 && !notificationHints.some((item) => item.hint.eventUid === notificationHint.hint.eventUid)) {
3368
+ notificationHints.push(notificationHint);
3369
+ if (notificationHints.length > 256) notificationHints.splice(0, notificationHints.length - 256);
3370
+ }
3371
+ this.pendingChatProjections.set(uid, {
3372
+ latestSequence: Math.max(latestSequence, current?.latestSequence ?? 0),
3373
+ notificationHints
3374
+ });
3070
3375
  if (this.projectionTimer !== void 0 || this.projectionInFlight) return;
3071
3376
  this.projectionTimer = setTimeout(() => {
3072
3377
  this.projectionTimer = void 0;
@@ -3074,10 +3379,10 @@ var ChatRealtimeService = class {
3074
3379
  }, 200);
3075
3380
  }
3076
3381
  async flushChatSessionProjections() {
3077
- if (this.projectionInFlight || this.pendingProjectionSequences.size === 0) return;
3382
+ if (this.projectionInFlight || this.pendingChatProjections.size === 0) return;
3078
3383
  this.projectionInFlight = true;
3079
- const pending = [...this.pendingProjectionSequences.entries()].slice(0, 50);
3080
- for (const [uid] of pending) this.pendingProjectionSequences.delete(uid);
3384
+ const pending = [...this.pendingChatProjections.entries()].slice(0, 50);
3385
+ for (const [uid] of pending) this.pendingChatProjections.delete(uid);
3081
3386
  try {
3082
3387
  const failed = await this.refreshChatSessionProjectionBatch(pending);
3083
3388
  for (const [uid] of pending) if (!failed.some(([failedUid]) => failedUid === uid)) this.projectionRetryCounts.delete(uid);
@@ -3092,7 +3397,7 @@ var ChatRealtimeService = class {
3092
3397
  this.requeueProjectionFailures(pending);
3093
3398
  } finally {
3094
3399
  this.projectionInFlight = false;
3095
- if (this.pendingProjectionSequences.size > 0 && this.projectionTimer === void 0) {
3400
+ if (this.pendingChatProjections.size > 0 && this.projectionTimer === void 0) {
3096
3401
  const retryDelayBase = this.projectionFailureCount === 0 ? 200 : Math.min(5e3, 500 * 2 ** Math.min(3, this.projectionFailureCount - 1));
3097
3402
  const retryDelay = Math.max(100, Math.round(retryDelayBase * (.8 + Math.random() * .4)));
3098
3403
  this.projectionTimer = setTimeout(() => {
@@ -3103,7 +3408,7 @@ var ChatRealtimeService = class {
3103
3408
  }
3104
3409
  }
3105
3410
  requeueProjectionFailures(failed) {
3106
- for (const [uid, sequence] of failed) {
3411
+ for (const [uid, projection] of failed) {
3107
3412
  const retries = (this.projectionRetryCounts.get(uid) ?? 0) + 1;
3108
3413
  if (retries > MAX_PROJECTION_RETRIES) {
3109
3414
  this.projectionRetryCounts.delete(uid);
@@ -3111,9 +3416,25 @@ var ChatRealtimeService = class {
3111
3416
  continue;
3112
3417
  }
3113
3418
  this.projectionRetryCounts.set(uid, retries);
3114
- this.pendingProjectionSequences.set(uid, Math.max(sequence, this.pendingProjectionSequences.get(uid) ?? 0));
3419
+ this.mergePendingChatProjection(uid, projection);
3115
3420
  }
3116
3421
  }
3422
+ mergePendingChatProjection(uid, incoming) {
3423
+ const current = this.pendingChatProjections.get(uid);
3424
+ if (current === void 0) {
3425
+ this.pendingChatProjections.set(uid, {
3426
+ latestSequence: incoming.latestSequence,
3427
+ notificationHints: [...incoming.notificationHints]
3428
+ });
3429
+ return;
3430
+ }
3431
+ const byEventUid = new Map(current.notificationHints.map((item) => [item.hint.eventUid, item]));
3432
+ for (const item of incoming.notificationHints) byEventUid.set(item.hint.eventUid, item);
3433
+ this.pendingChatProjections.set(uid, {
3434
+ latestSequence: Math.max(current.latestSequence, incoming.latestSequence),
3435
+ notificationHints: [...byEventUid.values()].slice(-256)
3436
+ });
3437
+ }
3117
3438
  async refreshChatSessionProjectionBatch(pending) {
3118
3439
  const session = await this.runtime.requireSession();
3119
3440
  const sessionUids = pending.map(([uid]) => uid).sort();
@@ -3130,16 +3451,19 @@ var ChatRealtimeService = class {
3130
3451
  const failedUids = /* @__PURE__ */ new Set();
3131
3452
  for (let offset = 0; offset < pending.length; offset += 3) {
3132
3453
  const chunk = pending.slice(offset, offset + 3);
3133
- (await Promise.allSettled(chunk.map(async ([uid, hintedSequence]) => {
3454
+ (await Promise.allSettled(chunk.map(async ([uid, projection]) => {
3134
3455
  const cached = this.source.cachedChatSourceByKey(`${String(session.userId)}:${uid}`);
3135
- const afterSequence = Math.max(0, cached?.latestSequence ?? hintedSequence - 1);
3456
+ const firstHintSequence = projection.notificationHints.reduce((minimum, item) => Math.min(minimum, item.hint.latestSequence), projection.latestSequence);
3457
+ const requiredAfterSequence = Math.max(0, firstHintSequence - 1);
3458
+ const afterSequence = Math.max(0, Math.min(cached?.latestSequence ?? requiredAfterSequence, requiredAfterSequence));
3459
+ const notificationAttempt = projection.notificationHints.reduce((maximum, item) => Math.max(maximum, item.attempts), 0);
3136
3460
  const data = await this.runtime.authenticatedChatPost("/api/v1/chat/timeline/tail", {
3137
3461
  chat_session_uid: uid,
3138
3462
  after_seq: afterSequence,
3139
3463
  limit: 50
3140
3464
  }, session, void 0, {
3141
3465
  lane: "background-read",
3142
- key: `projection:tail:${uid}:${String(afterSequence)}`
3466
+ key: `projection:tail:${uid}:${String(afterSequence)}:${String(notificationAttempt)}`
3143
3467
  });
3144
3468
  return [uid, await this.projectionReader.chatTimelineItems(data, session)];
3145
3469
  }))).forEach((result, index) => {
@@ -3150,7 +3474,8 @@ var ChatRealtimeService = class {
3150
3474
  });
3151
3475
  }
3152
3476
  const updates = [];
3153
- for (const [uid] of pending) {
3477
+ const notifications = [];
3478
+ for (const [uid, projection] of pending) {
3154
3479
  const bundle = bundles.get(uid);
3155
3480
  if (bundle === void 0 || failedUids.has(uid)) {
3156
3481
  failedUids.add(uid);
@@ -3161,11 +3486,47 @@ var ChatRealtimeService = class {
3161
3486
  try {
3162
3487
  const source = await this.source.chatSourceFromBundle(bundle, session, this.source.cachedChatSourceByKey(cacheKey), timelineItems);
3163
3488
  this.source.setChatSourceByKey(cacheKey, source);
3489
+ const sourceKey = source.sourceKey ?? await this.source.chatDirectorySourceKey(session.userId, uid);
3164
3490
  updates.push({
3165
- sourceKey: source.sourceKey ?? await this.source.chatDirectorySourceKey(session.userId, uid),
3491
+ sourceKey,
3166
3492
  source,
3167
3493
  timelineItems
3168
3494
  });
3495
+ if (source.kind === "private_chat" || source.kind === "group_chat") for (const candidate of projection.notificationHints.sort((left, right) => left.hint.latestSequence - right.hint.latestSequence)) {
3496
+ const state = this.chatRealtime.state();
3497
+ if (candidate.connectionGeneration !== state.connectionGeneration) continue;
3498
+ if (this.notificationBaselineGeneration !== candidate.connectionGeneration) {
3499
+ this.mergePendingChatProjection(uid, {
3500
+ latestSequence: projection.latestSequence,
3501
+ notificationHints: [candidate]
3502
+ });
3503
+ continue;
3504
+ }
3505
+ const baselineSequence = this.notificationBaselineSequences.get(uid) ?? 0;
3506
+ if (candidate.hint.latestSequence <= baselineSequence || candidate.hint.senderUserId === session.userId || source.isMuted === true) continue;
3507
+ const message = timelineItems.find((item) => item.sequence === candidate.hint.latestSequence && !item.isMe);
3508
+ if (message === void 0) {
3509
+ if (candidate.attempts < 4) this.mergePendingChatProjection(uid, {
3510
+ latestSequence: projection.latestSequence,
3511
+ notificationHints: [{
3512
+ ...candidate,
3513
+ attempts: candidate.attempts + 1
3514
+ }]
3515
+ });
3516
+ else console.warn("dsh-arkme: Chat notification message was not projected:", candidate.hint.eventUid);
3517
+ continue;
3518
+ }
3519
+ const preview = message.textContent.trim() || "非文本内容";
3520
+ notifications.push({
3521
+ eventUid: candidate.hint.eventUid,
3522
+ sourceRef: source.sourceRef,
3523
+ sourceKey,
3524
+ sourceKind: source.kind,
3525
+ title: source.displayName,
3526
+ body: source.kind === "group_chat" ? `${message.senderName}:${preview}` : preview,
3527
+ eventAtMillis: candidate.hint.eventAtMillis
3528
+ });
3529
+ }
3169
3530
  } catch {
3170
3531
  failedUids.add(uid);
3171
3532
  }
@@ -3178,6 +3539,11 @@ var ChatRealtimeService = class {
3178
3539
  updates
3179
3540
  });
3180
3541
  }
3542
+ for (const notification of notifications) this.emitChatClientEvent({
3543
+ type: "message-notification",
3544
+ revision: this.nextChatClientRevision(),
3545
+ notification
3546
+ });
3181
3547
  return pending.filter(([uid]) => failedUids.has(uid));
3182
3548
  }
3183
3549
  emitChatClientEvent(event) {
@@ -3242,10 +3608,10 @@ function timelineAgentSource(relation, record, payload) {
3242
3608
  label: agentSourceLabel(displayName)
3243
3609
  };
3244
3610
  }
3245
- function encodeOpaqueJson$4(value) {
3611
+ function encodeOpaqueJson$5(value) {
3246
3612
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
3247
3613
  }
3248
- function decodeOpaqueJson$4(value) {
3614
+ function decodeOpaqueJson$5(value) {
3249
3615
  return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
3250
3616
  }
3251
3617
  var ChatService = class {
@@ -3872,7 +4238,7 @@ var ChatService = class {
3872
4238
  };
3873
4239
  }
3874
4240
  sealMessageRef(userId, chatSessionUid, relationUid, signingKey) {
3875
- const payload = encodeOpaqueJson$4({
4241
+ const payload = encodeOpaqueJson$5({
3876
4242
  version: 1,
3877
4243
  userId,
3878
4244
  chatSessionUid,
@@ -3889,7 +4255,7 @@ var ChatService = class {
3889
4255
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false);
3890
4256
  let parsed;
3891
4257
  try {
3892
- parsed = objectValue$1(decodeOpaqueJson$4(payload));
4258
+ parsed = objectValue$1(decodeOpaqueJson$5(payload));
3893
4259
  } catch (error) {
3894
4260
  throw new ArkmePluginError("message-ref-invalid", "Arkme 消息引用无效", false, 400, { cause: error });
3895
4261
  }
@@ -4664,6 +5030,12 @@ function numberValue$12(value) {
4664
5030
  function listValue$12(value) {
4665
5031
  return Array.isArray(value) ? value : [];
4666
5032
  }
5033
+ function encodeOpaqueJson$4(value) {
5034
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
5035
+ }
5036
+ function decodeOpaqueJson$4(value) {
5037
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
5038
+ }
4667
5039
  function chatMessageDnd$1(value) {
4668
5040
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
4669
5041
  const policy = value;
@@ -4685,10 +5057,304 @@ var GroupService = class {
4685
5057
  runtime;
4686
5058
  source;
4687
5059
  profile;
4688
- constructor(runtime, source, profile) {
5060
+ inviteSender;
5061
+ constructor(runtime, source, profile, inviteSender) {
4689
5062
  this.runtime = runtime;
4690
5063
  this.source = source;
4691
5064
  this.profile = profile;
5065
+ this.inviteSender = inviteSender;
5066
+ }
5067
+ async groupInvitePreview(sourceRef, signal) {
5068
+ const session = await this.runtime.requireSession();
5069
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
5070
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-source-invalid", "仅支持邀请群聊协作者", false);
5071
+ const preview = objectValue$1((await this.runtime.authenticatedChatPost("/api/v1/chats/governance/invite-preview", { chat_session_uid: source.ownerRef }, session, signal)).preview);
5072
+ const inviteLink = stringValue$1(preview.invite_link).trim();
5073
+ if (inviteLink === "") throw new ArkmePluginError("group-invite-unavailable", "邀请链接生成失败,请重试", true);
5074
+ return {
5075
+ source: await this.source.sourceItem(source),
5076
+ title: stringValue$1(preview.title).trim() || source.displayName,
5077
+ inviterDisplayName: stringValue$1(preview.creator_display_name).trim() || "Arkme",
5078
+ inviteLink,
5079
+ expireAtMillis: numberValue$12(preview.expire_at),
5080
+ mode: numberValue$12(preview.join_mode) === 2 ? "approval_invite" : "direct_add"
5081
+ };
5082
+ }
5083
+ async listGroupMemberCandidates(sourceRef, options = {}) {
5084
+ const session = await this.runtime.requireSession();
5085
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
5086
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-source-invalid", "仅支持向群聊添加成员", false);
5087
+ const limit = Math.min(50, Math.max(1, Math.trunc(options.limit ?? 20)));
5088
+ const query = options.query?.trim().toLocaleLowerCase() ?? "";
5089
+ const requestedGroupSourceRefs = [...new Set((options.groupSourceRefs ?? []).map((value) => value.trim()).filter((value) => value !== ""))].slice(0, 20);
5090
+ const [membersData, directoryData, inviteData] = await Promise.all([
5091
+ this.runtime.authenticatedChatPost("/api/v1/chats/members/list", {
5092
+ chat_session_uid: source.ownerRef,
5093
+ active_only: true
5094
+ }, session, options.signal).catch(() => ({})),
5095
+ this.runtime.authenticatedChatPost("/api/v1/chats/list", { limit: 50 }, session, options.signal),
5096
+ this.runtime.authenticatedChatPost("/api/v1/chats/governance/invite-preview", { chat_session_uid: source.ownerRef }, session, options.signal).catch(() => ({}))
5097
+ ]);
5098
+ const directoryBundles = listValue$12(directoryData.items);
5099
+ const privateBundles = directoryBundles.filter((raw) => {
5100
+ const kind = numberValue$12(objectValue$1(objectValue$1(raw).session).session_kind);
5101
+ return kind === 1 || kind === 3;
5102
+ });
5103
+ const privateTopicIds = privateBundles.map((raw) => {
5104
+ const chatSession = objectValue$1(objectValue$1(raw).session);
5105
+ return numberValue$12(chatSession.shared_topic_id ?? chatSession.subject_id);
5106
+ }).filter((value) => Number.isSafeInteger(value) && value > 0);
5107
+ 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(() => ({}));
5108
+ const contactByTopicId = /* @__PURE__ */ new Map();
5109
+ for (const raw of listValue$12(partnerData.item_ls)) {
5110
+ const item = objectValue$1(raw);
5111
+ const topicId = numberValue$12(item.rm_subject_id ?? item.shared_topic_id);
5112
+ if (topicId > 0 && typeof item.is_contact === "boolean") contactByTopicId.set(topicId, item.is_contact);
5113
+ }
5114
+ const existingMemberUserIds = new Set(listValue$12(objectValue$1(membersData).items).map((item) => numberValue$12(objectValue$1(item).user_id)));
5115
+ const rawCandidates = /* @__PURE__ */ new Map();
5116
+ for (const raw of privateBundles) {
5117
+ const bundle = objectValue$1(raw);
5118
+ const chatSession = objectValue$1(bundle.session);
5119
+ if (numberValue$12(chatSession.session_kind) !== 1 && numberValue$12(chatSession.session_kind) !== 3) continue;
5120
+ const counterpart = objectValue$1(bundle.private_counterpart);
5121
+ const supplement = objectValue$1(bundle.private_supplement);
5122
+ const targetUserId = numberValue$12(counterpart.user_id);
5123
+ const privateChatSessionUid = stringValue$1(chatSession.chat_session_uid).trim();
5124
+ const displayName = stringValue$1(supplement.remark ?? supplement.counterpart_name_snapshot ?? counterpart.display_name_snapshot ?? supplement.pending_name ?? counterpart.visible_phone).trim();
5125
+ const privateTopicId = numberValue$12(chatSession.shared_topic_id ?? chatSession.subject_id);
5126
+ const contactState = numberValue$12(supplement.contact_state);
5127
+ const relation = contactByTopicId.get(privateTopicId) === true || contactState === 1 || contactState === 3 ? "contact" : "stranger";
5128
+ if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0 || targetUserId === session.userId || privateChatSessionUid === "" || displayName === "" || query !== "" && !displayName.toLocaleLowerCase().includes(query)) continue;
5129
+ rawCandidates.set(targetUserId, {
5130
+ targetUserId,
5131
+ displayName,
5132
+ privateChatSessionUid,
5133
+ relation,
5134
+ origin: "private_chat"
5135
+ });
5136
+ }
5137
+ const selected = [...rawCandidates.entries()].slice(0, limit);
5138
+ const sourceByGroupUid = /* @__PURE__ */ new Map();
5139
+ const groupSessionUidByIndex = /* @__PURE__ */ new Map();
5140
+ const groups = [];
5141
+ for (const raw of directoryBundles) {
5142
+ const bundle = objectValue$1(raw);
5143
+ const chatSession = objectValue$1(bundle.session);
5144
+ if (numberValue$12(chatSession.session_kind) !== 2) continue;
5145
+ const groupSessionUid = stringValue$1(chatSession.chat_session_uid).trim();
5146
+ if (groupSessionUid === "" || groupSessionUid === source.ownerRef) continue;
5147
+ const displayName = stringValue$1(chatSession.title ?? chatSession.subject_title ?? objectValue$1(bundle.participant_summary).display_name).trim();
5148
+ const groupItem = {
5149
+ sourceRef: await this.source.sealSourceRef(session.userId, "group_chat", groupSessionUid, displayName || "群聊"),
5150
+ sourceKey: await this.source.chatDirectorySourceKey(session.userId, groupSessionUid),
5151
+ kind: "group_chat",
5152
+ displayName: displayName || "群聊",
5153
+ activeAtMillis: numberValue$12(chatSession.updated_at),
5154
+ unreadCount: 0
5155
+ };
5156
+ const groupIndex = groups.push(groupItem) - 1;
5157
+ groupSessionUidByIndex.set(groupIndex, groupSessionUid);
5158
+ sourceByGroupUid.set(groupSessionUid, groupItem);
5159
+ this.source.setChatSource(session.userId, groupSessionUid, groupItem);
5160
+ }
5161
+ try {
5162
+ await this.source.hydrateSourceAvatars(groups, /* @__PURE__ */ new Map(), groupSessionUidByIndex, session, options.signal);
5163
+ } catch {}
5164
+ const requestedGroupSources = [];
5165
+ for (const groupSourceRef of requestedGroupSourceRefs) {
5166
+ const groupSource = await this.source.openSourceRef(groupSourceRef, session.userId);
5167
+ if (groupSource.kind !== "group_chat" || groupSource.ownerRef === source.ownerRef) continue;
5168
+ requestedGroupSources.push(groupSource);
5169
+ }
5170
+ const groupRawByUid = /* @__PURE__ */ new Map();
5171
+ const groupTargetUserIds = /* @__PURE__ */ new Set();
5172
+ for (const groupSource of requestedGroupSources) {
5173
+ const group = sourceByGroupUid.get(groupSource.ownerRef) ?? await this.source.sourceItem(groupSource);
5174
+ try {
5175
+ const groupMembersData = await this.runtime.authenticatedChatPost("/api/v1/chats/members/list", {
5176
+ chat_session_uid: groupSource.ownerRef,
5177
+ active_only: true
5178
+ }, session, options.signal);
5179
+ const raws = [];
5180
+ const seenUserIds = /* @__PURE__ */ new Set();
5181
+ for (const rawMember of listValue$12(groupMembersData.items)) {
5182
+ const member = objectValue$1(rawMember);
5183
+ const targetUserId = numberValue$12(member.user_id);
5184
+ if (!Number.isSafeInteger(targetUserId) || targetUserId <= 0 || targetUserId === session.userId || seenUserIds.has(targetUserId)) continue;
5185
+ if (chatMemberStatus(member.status) !== "active") continue;
5186
+ const privateCandidate = rawCandidates.get(targetUserId);
5187
+ if (privateCandidate !== void 0) {
5188
+ if (query === "" || privateCandidate.displayName.toLocaleLowerCase().includes(query)) {
5189
+ raws.push(privateCandidate);
5190
+ groupTargetUserIds.add(targetUserId);
5191
+ seenUserIds.add(targetUserId);
5192
+ }
5193
+ continue;
5194
+ }
5195
+ const profileName = stringValue$1(member.profile_display_name).trim();
5196
+ const displayName = [
5197
+ stringValue$1(member.remark).trim(),
5198
+ stringValue$1(member.display_name_snapshot).trim(),
5199
+ profileName === `用户 ${String(targetUserId)}` ? "" : profileName,
5200
+ stringValue$1(objectValue$1(member.user_info).nick_name).trim(),
5201
+ String(targetUserId)
5202
+ ].find((value) => value !== "" && value !== "成员" && value !== "群成员") ?? "群成员";
5203
+ if (query !== "" && !displayName.toLocaleLowerCase().includes(query)) continue;
5204
+ raws.push({
5205
+ targetUserId,
5206
+ displayName,
5207
+ relation: "group",
5208
+ origin: "group_chat"
5209
+ });
5210
+ groupTargetUserIds.add(targetUserId);
5211
+ seenUserIds.add(targetUserId);
5212
+ }
5213
+ groupRawByUid.set(groupSource.ownerRef, {
5214
+ group,
5215
+ raws: raws.slice(0, limit),
5216
+ total: raws.length
5217
+ });
5218
+ } catch (error) {
5219
+ groupRawByUid.set(groupSource.ownerRef, {
5220
+ group,
5221
+ raws: [],
5222
+ total: 0,
5223
+ error: error instanceof Error && error.message.trim() !== "" ? error.message : "群成员加载失败,请重试"
5224
+ });
5225
+ }
5226
+ }
5227
+ const profiles = await this.profile.publicProfileSummariesByUserIds([.../* @__PURE__ */ new Set([...selected.map(([userId]) => userId), ...groupTargetUserIds])], session, options.signal).catch(() => /* @__PURE__ */ new Map());
5228
+ const preview = objectValue$1(objectValue$1(inviteData).preview);
5229
+ const approvalRequired = numberValue$12(preview.join_mode) === 2;
5230
+ const candidateItem = async (targetUserId, candidate) => {
5231
+ const alreadyMember = existingMemberUserIds.has(targetUserId);
5232
+ const cannotInvite = approvalRequired && candidate.privateChatSessionUid === void 0;
5233
+ return {
5234
+ candidateRef: await this.sealCandidateRef({
5235
+ version: 1,
5236
+ userId: session.userId,
5237
+ groupChatSessionUid: source.ownerRef,
5238
+ targetUserId,
5239
+ origin: candidate.origin,
5240
+ displayName: candidate.displayName,
5241
+ ...candidate.privateChatSessionUid === void 0 ? {} : { privateChatSessionUid: candidate.privateChatSessionUid }
5242
+ }),
5243
+ displayName: candidate.displayName,
5244
+ ...profiles.get(targetUserId)?.avatarUrl === void 0 ? {} : { avatarRef: await this.profile.sealProfileImageRef(session.userId, targetUserId) },
5245
+ origin: candidate.origin,
5246
+ relation: candidate.relation,
5247
+ ...alreadyMember ? {
5248
+ disabled: true,
5249
+ alreadyMember: true
5250
+ } : {},
5251
+ ...!alreadyMember && cannotInvite ? {
5252
+ disabled: true,
5253
+ statusText: "无法邀请"
5254
+ } : {}
5255
+ };
5256
+ };
5257
+ const items = await Promise.all(selected.map(async ([targetUserId, candidate]) => candidateItem(targetUserId, candidate)));
5258
+ const groupCandidates = await Promise.all([...groupRawByUid.values()].map(async (value) => ({
5259
+ group: value.group,
5260
+ items: await Promise.all(value.raws.map((raw) => candidateItem(raw.targetUserId, raw))),
5261
+ total: value.total,
5262
+ ...value.error === void 0 ? {} : { error: value.error }
5263
+ })));
5264
+ return {
5265
+ source: await this.source.sourceItem(source),
5266
+ items,
5267
+ total: rawCandidates.size,
5268
+ hasMore: rawCandidates.size > items.length,
5269
+ mode: numberValue$12(preview.join_mode) === 2 ? "approval_invite" : "direct_add",
5270
+ groups,
5271
+ groupCandidates,
5272
+ contactCount: [...rawCandidates.values()].filter((item) => item.relation === "contact").length,
5273
+ strangerCount: [...rawCandidates.values()].filter((item) => item.relation === "stranger").length
5274
+ };
5275
+ }
5276
+ async addGroupMembers(sourceRef, candidateRefs, signal) {
5277
+ const session = await this.runtime.requireSession();
5278
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
5279
+ if (source.kind !== "group_chat") throw new ArkmePluginError("group-source-invalid", "仅支持向群聊添加成员", false);
5280
+ const refs = candidateRefs.map((value) => value.trim());
5281
+ 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);
5282
+ const preview = objectValue$1(objectValue$1(await this.runtime.authenticatedChatPost("/api/v1/chats/governance/invite-preview", { chat_session_uid: source.ownerRef }, session, signal)).preview);
5283
+ const approvalRequired = numberValue$12(preview.join_mode) === 2;
5284
+ const inviteLink = stringValue$1(preview.invite_link).trim();
5285
+ const results = [];
5286
+ for (const candidateRef of refs) {
5287
+ let candidate;
5288
+ try {
5289
+ candidate = await this.openCandidateRef(candidateRef, session.userId, source.ownerRef);
5290
+ if (approvalRequired) {
5291
+ if (candidate.privateChatSessionUid === void 0) throw new ArkmePluginError("group-candidate-invite-unavailable", "该对象暂时无法发送群聊邀请", false);
5292
+ if (this.inviteSender === void 0 || inviteLink === "") throw new ArkmePluginError("group-invite-unavailable", "该群需要发送邀请,但邀请链接暂不可用", true);
5293
+ const privateSourceRef = await this.source.sealSourceRef(session.userId, "private_chat", candidate.privateChatSessionUid, candidate.displayName);
5294
+ await this.inviteSender.sendPrivateText(privateSourceRef, candidate.privateChatSessionUid, `邀请你加入群聊“${source.displayName}”:${inviteLink}`, `chat_record_${randomUUID()}`, `chat_rel_${randomUUID()}`, session, signal);
5295
+ results.push({
5296
+ candidateRef,
5297
+ displayName: candidate.displayName,
5298
+ status: "invite_sent"
5299
+ });
5300
+ continue;
5301
+ }
5302
+ const outcome = stringValue$1(objectValue$1((await this.runtime.authenticatedChatPost("/api/v1/chats/members/add", {
5303
+ chat_session_uid: source.ownerRef,
5304
+ target_user_id: candidate.targetUserId,
5305
+ join_at: Date.now(),
5306
+ display_name_snapshot: candidate.displayName,
5307
+ extra: { source: "dsh_arkme_candidate" }
5308
+ }, session, signal)).item).outcome);
5309
+ results.push({
5310
+ candidateRef,
5311
+ displayName: candidate.displayName,
5312
+ status: outcome === "idempotent_hit" ? "already_member" : outcome === "updated" ? "reactivated" : "added"
5313
+ });
5314
+ } catch (error) {
5315
+ results.push({
5316
+ candidateRef,
5317
+ displayName: candidate?.displayName ?? "成员",
5318
+ status: "failed",
5319
+ error: error instanceof Error && error.message.trim() !== "" ? error.message : "添加成员失败"
5320
+ });
5321
+ }
5322
+ }
5323
+ return {
5324
+ source: await this.source.sourceItem(source),
5325
+ results,
5326
+ succeededCount: results.filter((item) => item.status !== "failed").length,
5327
+ failedCount: results.filter((item) => item.status === "failed").length
5328
+ };
5329
+ }
5330
+ async sealCandidateRef(payload) {
5331
+ const encoded = encodeOpaqueJson$4(payload);
5332
+ return `arkme-group-candidate-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
5333
+ }
5334
+ async openCandidateRef(candidateRef, expectedUserId, expectedGroupChatSessionUid) {
5335
+ const parts = candidateRef.split(".");
5336
+ if (parts.length !== 3 || parts[0] !== "arkme-group-candidate-v1") throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
5337
+ const encoded = parts[1] ?? "";
5338
+ const supplied = Buffer.from(parts[2] ?? "", "base64url");
5339
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest();
5340
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false);
5341
+ let raw;
5342
+ try {
5343
+ raw = objectValue$1(decodeOpaqueJson$4(encoded));
5344
+ } catch (error) {
5345
+ throw new ArkmePluginError("group-candidate-ref-invalid", "群成员候选引用无效", false, 400, { cause: error });
5346
+ }
5347
+ const result = {
5348
+ version: 1,
5349
+ userId: numberValue$12(raw.userId),
5350
+ groupChatSessionUid: stringValue$1(raw.groupChatSessionUid).trim(),
5351
+ targetUserId: numberValue$12(raw.targetUserId),
5352
+ displayName: stringValue$1(raw.displayName).trim(),
5353
+ ...stringValue$1(raw.privateChatSessionUid).trim() === "" ? {} : { privateChatSessionUid: stringValue$1(raw.privateChatSessionUid).trim() },
5354
+ origin: raw.origin === "group_chat" ? "group_chat" : "private_chat"
5355
+ };
5356
+ 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);
5357
+ return result;
4692
5358
  }
4693
5359
  async listGroupMembers(sourceRef, options = {}) {
4694
5360
  const session = await this.runtime.requireSession();
@@ -4731,7 +5397,8 @@ var GroupService = class {
4731
5397
  status,
4732
5398
  isSelf: userId === session.userId,
4733
5399
  isOwner: role === "owner",
4734
- joinedAtMillis: numberValue$12(item.join_at)
5400
+ joinedAtMillis: numberValue$12(item.join_at),
5401
+ recordCount: Math.max(0, Math.trunc(numberValue$12(objectValue$1(item.extra).record_count)))
4735
5402
  });
4736
5403
  }
4737
5404
  const roleRank = (role) => role === "owner" ? 0 : role === "admin" ? 1 : role === "member" ? 2 : 3;
@@ -5174,6 +5841,33 @@ function md5Text(value) {
5174
5841
  function allowedSignedImageHost$2(environment, hostname) {
5175
5842
  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
5843
  }
5844
+ function allowedSignedAudioHost(environment, hostname) {
5845
+ return (environment === "prod" ? ["jotmo-useraudio.oss-cn-hangzhou.aliyuncs.com"] : ["jotmo-useraudio-test.oss-cn-hangzhou.aliyuncs.com"]).includes(hostname.toLowerCase());
5846
+ }
5847
+ function trustedWorldVoiceprintAudioUrl(environment, raw) {
5848
+ let parsed;
5849
+ try {
5850
+ parsed = new URL(raw.trim());
5851
+ } catch (error) {
5852
+ throw new ArkmePluginError("world-voiceprint-audio-invalid", "世界声纹音频地址无效", true, 502, { cause: error });
5853
+ }
5854
+ const signature = parsed.searchParams.get("x-oss-signature")?.trim() ?? "";
5855
+ 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);
5856
+ return parsed;
5857
+ }
5858
+ function worldVoiceprintFileName(mimeType) {
5859
+ const extension = {
5860
+ wav: "wav",
5861
+ "x-wav": "wav",
5862
+ mpeg: "mp3",
5863
+ mp4: "m4a",
5864
+ aac: "aac",
5865
+ ogg: "ogg",
5866
+ webm: "webm",
5867
+ flac: "flac"
5868
+ }[mimeType.split(";", 1)[0].trim().toLowerCase().replace(/^audio\//, "")];
5869
+ return extension === void 0 ? "世界声纹" : `世界声纹.${extension}`;
5870
+ }
5177
5871
  function trustedSignedImageUrl$1(environment, raw) {
5178
5872
  let parsed;
5179
5873
  try {
@@ -5234,6 +5928,7 @@ var MediaService = class {
5234
5928
  worldImages;
5235
5929
  recordIdentity;
5236
5930
  mediaRefs = /* @__PURE__ */ new Map();
5931
+ stableMediaRefs = /* @__PURE__ */ new Map();
5237
5932
  imageCache = /* @__PURE__ */ new Map();
5238
5933
  imageInFlight = /* @__PURE__ */ new Map();
5239
5934
  imageCacheBytes = 0;
@@ -5247,6 +5942,7 @@ var MediaService = class {
5247
5942
  }
5248
5943
  dispose() {
5249
5944
  this.mediaRefs.clear();
5945
+ this.stableMediaRefs.clear();
5250
5946
  this.imageCache.clear();
5251
5947
  this.imageInFlight.clear();
5252
5948
  this.imageCacheBytes = 0;
@@ -5353,18 +6049,20 @@ var MediaService = class {
5353
6049
  throw error;
5354
6050
  }
5355
6051
  }
5356
- async fetchMedia(mediaRef, range) {
6052
+ async fetchMedia(mediaRef, range, signal) {
5357
6053
  const session = await this.runtime.requireSession();
5358
6054
  const descriptor = this.mediaRefs.get(mediaRef);
5359
6055
  if (descriptor === void 0 || descriptor.viewerUserId !== session.userId || descriptor.expiresAtMillis <= Date.now()) {
5360
6056
  this.mediaRefs.delete(mediaRef);
6057
+ if (descriptor?.stableKey !== void 0) this.stableMediaRefs.delete(descriptor.stableKey);
5361
6058
  throw new ArkmePluginError("media-ref-invalid", "媒体引用已失效,请刷新对话后重试", false, 404);
5362
6059
  }
5363
6060
  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);
6061
+ 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
6062
  const response = await this.runtime.fetchImpl(url, {
5366
6063
  headers: range === void 0 ? {} : { Range: range },
5367
- redirect: "error"
6064
+ redirect: "error",
6065
+ ...signal === void 0 ? {} : { signal }
5368
6066
  });
5369
6067
  if (!response.ok && response.status !== 206) throw new ArkmePluginError("media-fetch-failed", `媒体读取失败(${String(response.status)})`, true, 502);
5370
6068
  return {
@@ -5372,6 +6070,17 @@ var MediaService = class {
5372
6070
  descriptor
5373
6071
  };
5374
6072
  }
6073
+ issueWorldVoiceprintMediaRef(viewerUserId, input) {
6074
+ const remoteUrl = trustedWorldVoiceprintAudioUrl(this.runtime.config.environment, input.remoteUrl).toString();
6075
+ const mimeType = input.mimeType.trim() || "audio/wav";
6076
+ if (!mimeType.startsWith("audio/")) throw new ArkmePluginError("world-voiceprint-mime-invalid", "世界声纹音频格式无效", true, 502);
6077
+ return this.issueMediaRef(viewerUserId, {
6078
+ remoteUrl,
6079
+ mimeType,
6080
+ fileName: worldVoiceprintFileName(mimeType),
6081
+ size: 0
6082
+ });
6083
+ }
5375
6084
  async readImage(imageRef, options = {}) {
5376
6085
  const session = await this.runtime.requireSession();
5377
6086
  const maximumBytes = imageRef.trim().startsWith("arkme-profile-image-v1.") ? MAX_ARKME_PROFILE_IMAGE_BYTES : MAX_ARKME_IMAGE_BYTES;
@@ -5434,6 +6143,11 @@ var MediaService = class {
5434
6143
  }
5435
6144
  }
5436
6145
  async readImageUncached(session, imageRef, byteLimit, signal) {
6146
+ if (imageRef.trim().startsWith("arkme-media-v1.")) {
6147
+ const { response, descriptor } = await this.fetchMedia(imageRef.trim(), void 0, signal);
6148
+ if (!descriptor.mimeType.trim().toLowerCase().startsWith("image/")) throw new ArkmePluginError("image-type-unsupported", "该媒体引用不是图片", false, 415);
6149
+ return await this.imageBytesFromResponse(response, byteLimit);
6150
+ }
5437
6151
  if (imageRef.trim().startsWith("arkme-profile-image-v1.")) {
5438
6152
  const reference = await this.profile.openProfileImageRef(imageRef, session.userId);
5439
6153
  if (reference.targetUserId === session.userId) {
@@ -5563,20 +6277,33 @@ var MediaService = class {
5563
6277
  unavailableRecordUids
5564
6278
  };
5565
6279
  }
5566
- issueMediaRef(viewerUserId, descriptor) {
6280
+ issueImageMediaRef(viewerUserId, descriptor, stableIdentity) {
6281
+ return this.issueMediaRef(viewerUserId, descriptor, stableIdentity);
6282
+ }
6283
+ issueMediaRef(viewerUserId, descriptor, stableIdentity) {
5567
6284
  const now = Date.now();
5568
- for (const [key, value] of this.mediaRefs) if (value.expiresAtMillis <= now) this.mediaRefs.delete(key);
6285
+ for (const [key, value] of this.mediaRefs) if (value.expiresAtMillis <= now) {
6286
+ this.mediaRefs.delete(key);
6287
+ if (value.stableKey !== void 0) this.stableMediaRefs.delete(value.stableKey);
6288
+ }
5569
6289
  while (this.mediaRefs.size >= 2e3) {
5570
6290
  const oldest = this.mediaRefs.keys().next().value;
5571
6291
  if (oldest === void 0) break;
6292
+ const oldestDescriptor = this.mediaRefs.get(oldest);
5572
6293
  this.mediaRefs.delete(oldest);
6294
+ if (oldestDescriptor?.stableKey !== void 0) this.stableMediaRefs.delete(oldestDescriptor.stableKey);
5573
6295
  }
5574
- const ref = `arkme-media-v1.${randomUUID()}`;
6296
+ const stableKey = stableIdentity === void 0 ? void 0 : createHash("sha256").update(`${String(viewerUserId)}\0${stableIdentity}`).digest("base64url");
6297
+ const ref = (stableKey === void 0 ? void 0 : this.stableMediaRefs.get(stableKey)) ?? `arkme-media-v1.${randomUUID()}`;
6298
+ this.mediaRefs.delete(ref);
6299
+ const lifetimeMillis = stableKey === void 0 ? 30 * 6e4 : 1440 * 6e4;
5575
6300
  this.mediaRefs.set(ref, {
5576
6301
  ...descriptor,
5577
6302
  viewerUserId,
5578
- expiresAtMillis: now + 30 * 6e4
6303
+ expiresAtMillis: now + lifetimeMillis,
6304
+ ...stableKey === void 0 ? {} : { stableKey }
5579
6305
  });
6306
+ if (stableKey !== void 0) this.stableMediaRefs.set(stableKey, ref);
5580
6307
  return ref;
5581
6308
  }
5582
6309
  richContentBlocks(raw, viewerUserId, hydratedDisplayItems = []) {
@@ -5619,7 +6346,7 @@ var MediaService = class {
5619
6346
  mimeType,
5620
6347
  fileName,
5621
6348
  size: Math.max(0, Math.trunc(numberValue$10(item.size)))
5622
- }),
6349
+ }, kind === "image" && fileAssetUid !== "" ? fileAssetUid : void 0),
5623
6350
  ...fileAssetUid === "" ? {} : { fileAssetUid },
5624
6351
  fileName,
5625
6352
  mimeType,
@@ -5663,36 +6390,7 @@ var MediaService = class {
5663
6390
  signal: controller.signal
5664
6391
  });
5665
6392
  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
- };
6393
+ return await this.imageBytesFromResponse(response, byteLimit);
5696
6394
  } catch (error) {
5697
6395
  if (error instanceof ArkmePluginError) throw error;
5698
6396
  if (error.name === "AbortError") throw new ArkmePluginError("image-download-timeout", "Arkme 图片读取超时或已取消", true, 504, { cause: error });
@@ -5702,6 +6400,38 @@ var MediaService = class {
5702
6400
  signal?.removeEventListener("abort", abort);
5703
6401
  }
5704
6402
  }
6403
+ async imageBytesFromResponse(response, byteLimit) {
6404
+ const declaredLength = Number(response.headers.get("content-length") ?? 0);
6405
+ if (Number.isFinite(declaredLength) && declaredLength > byteLimit) throw new ArkmePluginError("image-too-large", "Arkme 图片超过读取大小限制", false, 413);
6406
+ if (response.body === null) throw new ArkmePluginError("image-response-empty", "Arkme 图片响应为空", true, 502);
6407
+ const chunks = [];
6408
+ let bytes = 0;
6409
+ const reader = response.body.getReader();
6410
+ while (true) {
6411
+ const next = await reader.read();
6412
+ if (next.done) break;
6413
+ bytes += next.value.byteLength;
6414
+ if (bytes > byteLimit) {
6415
+ await reader.cancel();
6416
+ throw new ArkmePluginError("image-too-large", "Arkme 图片超过读取大小限制", false, 413);
6417
+ }
6418
+ chunks.push(next.value);
6419
+ }
6420
+ if (bytes === 0) throw new ArkmePluginError("image-response-empty", "Arkme 图片响应为空", true, 502);
6421
+ const data = new Uint8Array(bytes);
6422
+ let offset = 0;
6423
+ for (const chunk of chunks) {
6424
+ data.set(chunk, offset);
6425
+ offset += chunk.byteLength;
6426
+ }
6427
+ const mediaType = imageMediaType(data);
6428
+ if (mediaType === void 0) throw new ArkmePluginError("image-type-unsupported", "Arkme 图片不是受支持的 PNG、JPEG、WebP 或 GIF", false, 415);
6429
+ return {
6430
+ mediaType,
6431
+ bytes,
6432
+ data
6433
+ };
6434
+ }
5705
6435
  };
5706
6436
  //#endregion
5707
6437
  //#region src/services/outgoing-call-service.ts
@@ -7248,9 +7978,11 @@ function listValue$3(value) {
7248
7978
  var SearchService = class {
7249
7979
  runtime;
7250
7980
  record;
7251
- constructor(runtime, record) {
7981
+ media;
7982
+ constructor(runtime, record, media) {
7252
7983
  this.runtime = runtime;
7253
7984
  this.record = record;
7985
+ this.media = media;
7254
7986
  }
7255
7987
  async searchRecords(options) {
7256
7988
  const query = options.query.trim();
@@ -7326,6 +8058,85 @@ var SearchService = class {
7326
8058
  }, session, options.signal);
7327
8059
  return this.recordSearchResult(data);
7328
8060
  }
8061
+ /** Build the desktop image library from the mixed image/video scene. */
8062
+ async searchImages(options) {
8063
+ const session = await this.runtime.requireSession();
8064
+ const pageLimit = Math.min(50, Math.max(1, Math.trunc(options.limit)));
8065
+ const seenCursors = /* @__PURE__ */ new Set();
8066
+ let cursor = options.cursor?.trim() ?? "";
8067
+ if (cursor !== "") seenCursors.add(cursor);
8068
+ let lastPage;
8069
+ for (let pageIndex = 0; pageIndex < 8; pageIndex += 1) {
8070
+ const page = await this.searchScene({
8071
+ scene: "image_video",
8072
+ limit: pageLimit,
8073
+ ...cursor === "" ? {} : { cursor },
8074
+ ...options.signal === void 0 ? {} : { signal: options.signal }
8075
+ });
8076
+ lastPage = page;
8077
+ const candidates = page.items.flatMap((record) => record.media.flatMap((asset) => {
8078
+ const mimeType = asset.mimeType?.trim().toLowerCase() ?? "";
8079
+ return mimeType.startsWith("image/") || mimeType === "" && (asset.fileKind === void 0 || asset.fileKind === 1) ? [{
8080
+ record,
8081
+ asset
8082
+ }] : [];
8083
+ }));
8084
+ const uniqueAssetUids = [...new Set(candidates.map((candidate) => candidate.asset.fileAssetUid))];
8085
+ const displayItems = [];
8086
+ for (let offset = 0; offset < uniqueAssetUids.length; offset += 50) displayItems.push(...await this.media.queryFileAssets(uniqueAssetUids.slice(offset, offset + 50), options.signal));
8087
+ const displayByUid = new Map(displayItems.map((item) => [item.fileAssetUid, item]));
8088
+ const emitted = /* @__PURE__ */ new Set();
8089
+ const items = candidates.flatMap(({ record, asset }) => {
8090
+ const itemIdentity = `${record.recordUid}\0${asset.fileAssetUid}`;
8091
+ if (emitted.has(itemIdentity)) return [];
8092
+ const display = displayByUid.get(asset.fileAssetUid);
8093
+ if (display === void 0) return [];
8094
+ const mimeType = (display.mimeType ?? asset.mimeType ?? "").trim().toLowerCase();
8095
+ if (!mimeType.startsWith("image/")) return [];
8096
+ const remoteUrl = display.previewUrl ?? display.downloadUrl;
8097
+ if (remoteUrl === void 0) return [];
8098
+ emitted.add(itemIdentity);
8099
+ const fileName = display.fileName ?? asset.fileName ?? "图片";
8100
+ return [{
8101
+ itemKey: createHash("sha256").update(itemIdentity).digest("base64url"),
8102
+ mediaRef: this.media.issueImageMediaRef(session.userId, {
8103
+ remoteUrl,
8104
+ mimeType: mimeType || "application/octet-stream",
8105
+ fileName,
8106
+ size: Math.max(0, Math.trunc(asset.size ?? 0))
8107
+ }, asset.fileAssetUid),
8108
+ recordUid: record.recordUid,
8109
+ sendAtMillis: record.sendAtMillis,
8110
+ fileName,
8111
+ mimeType: mimeType || "application/octet-stream",
8112
+ size: Math.max(0, Math.trunc(asset.size ?? 0)),
8113
+ recordTitle: record.title || record.nickname || "快记",
8114
+ ...record.sourceTitle === void 0 ? {} : { sourceTitle: record.sourceTitle }
8115
+ }];
8116
+ });
8117
+ const nextCursor = page.nextCursor?.trim() ?? "";
8118
+ const canContinue = page.hasMore && nextCursor !== "" && !seenCursors.has(nextCursor);
8119
+ if (items.length > 0 || !canContinue) return {
8120
+ items,
8121
+ hasMore: canContinue,
8122
+ ...canContinue ? { nextCursor } : {},
8123
+ queryGuard: page.queryGuard
8124
+ };
8125
+ if (pageIndex === 7) return {
8126
+ items: [],
8127
+ hasMore: true,
8128
+ nextCursor,
8129
+ queryGuard: page.queryGuard
8130
+ };
8131
+ seenCursors.add(nextCursor);
8132
+ cursor = nextCursor;
8133
+ }
8134
+ return {
8135
+ items: [],
8136
+ hasMore: false,
8137
+ queryGuard: lastPage?.queryGuard ?? { state: "complete" }
8138
+ };
8139
+ }
7329
8140
  async searchRecordings(options) {
7330
8141
  const query = options.query.trim();
7331
8142
  if (query === "") throw new ArkmePluginError("recording-query-empty", "搜索关键词不能为空", false);
@@ -8485,6 +9296,67 @@ var WorldService = class {
8485
9296
  ...hasMore ? { nextOffset } : {}
8486
9297
  };
8487
9298
  }
9299
+ async worldVoiceprintPlaybackAvailability(recordRefs, signal) {
9300
+ const session = await this.runtime.requireSession();
9301
+ const normalizedRefs = [...new Set(recordRefs.map((value) => value.trim()).filter((value) => value !== ""))].slice(0, 20);
9302
+ if (normalizedRefs.length === 0) return { items: [] };
9303
+ const entries = normalizedRefs.map((recordRef) => ({
9304
+ recordRef,
9305
+ entry: this.openWorldRecordRef(recordRef, session.userId)
9306
+ }));
9307
+ const ownerUserIds = [...new Set(entries.map((value) => value.entry.ownerUserId ?? 0).filter((userId) => Number.isSafeInteger(userId) && userId > 0))].sort((left, right) => left - right);
9308
+ if (ownerUserIds.length === 0) return { items: entries.map(({ recordRef }) => ({
9309
+ recordRef,
9310
+ playable: false
9311
+ })) };
9312
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/world-playback-availability", { user_ids: ownerUserIds }, session, signal, {
9313
+ key: ownerUserIds.join(","),
9314
+ cacheMs: 5e3
9315
+ });
9316
+ const playableByOwner = /* @__PURE__ */ new Map();
9317
+ for (const raw of listValue(data.items)) {
9318
+ const item = objectValue$1(raw);
9319
+ const userId = Math.trunc(numberValue(item.user_id));
9320
+ if (userId > 0) playableByOwner.set(userId, item.playable === true);
9321
+ }
9322
+ return { items: entries.map(({ recordRef, entry }) => ({
9323
+ recordRef,
9324
+ playable: playableByOwner.get(entry.ownerUserId ?? 0) === true
9325
+ })) };
9326
+ }
9327
+ async generateWorldVoiceprintPlayback(input) {
9328
+ const session = await this.runtime.requireSession();
9329
+ const entry = this.openWorldRecordRef(input.recordRef, session.userId);
9330
+ const chunkIndex = Math.trunc(input.chunkIndex ?? 0);
9331
+ if (!Number.isSafeInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= 334) throw new ArkmePluginError("world-voiceprint-playback-input-invalid", "世界声纹播放参数无效,请刷新后重试", false);
9332
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/voiceprint/generate-playback", {
9333
+ source_scene: 4,
9334
+ source_id: entry.recordUid,
9335
+ source_chunk_index: chunkIndex
9336
+ }, session, input.signal, {
9337
+ lane: "write",
9338
+ bypassCache: true
9339
+ });
9340
+ const mimeType = stringValue$1(data.mime_type).trim() || "audio/wav";
9341
+ const responseChunkIndex = Math.trunc(numberValue(data.source_chunk_index));
9342
+ const chunkCount = Math.trunc(numberValue(data.source_chunk_count));
9343
+ const chunkStartRune = Math.trunc(numberValue(data.source_chunk_start_rune));
9344
+ const chunkEndRune = Math.trunc(numberValue(data.source_chunk_end_rune));
9345
+ if (responseChunkIndex !== chunkIndex || chunkCount <= 0 || chunkCount > 334 || chunkIndex >= chunkCount || chunkStartRune < 0 || chunkEndRune <= chunkStartRune) throw new ArkmePluginError("world-voiceprint-playback-response-invalid", "世界声纹播放响应无效,请重试", true, 502);
9346
+ return {
9347
+ mediaRef: this.media.issueWorldVoiceprintMediaRef(session.userId, {
9348
+ remoteUrl: stringValue$1(data.audio_url),
9349
+ mimeType
9350
+ }),
9351
+ mimeType,
9352
+ durationMillis: Math.max(0, Math.trunc(numberValue(data.duration_ms))),
9353
+ cacheHit: data.cache_hit === true,
9354
+ chunkIndex: responseChunkIndex,
9355
+ chunkCount,
9356
+ chunkStartRune,
9357
+ chunkEndRune
9358
+ };
9359
+ }
8488
9360
  async listWorldInteractions(recordRef, options = {}) {
8489
9361
  const session = await this.runtime.requireSession();
8490
9362
  const root = this.openWorldRecordRef(recordRef, session.userId);
@@ -8695,7 +9567,7 @@ var WorldService = class {
8695
9567
  }
8696
9568
  const avatarRef = this.isTrustedWorldImageUrl(avatarUrl) ? await this.sealWorldImageRef(viewerUserId, avatarUrl) : void 0;
8697
9569
  return {
8698
- recordRef: await this.worldRecordRef(viewerUserId, recordUid),
9570
+ recordRef: await this.worldRecordRef(viewerUserId, recordUid, ownerUserId),
8699
9571
  authorName: stringValue$1(item.nick_name ?? item.nickname).trim() || "Arkme用户",
8700
9572
  ...avatarRef === void 0 ? {} : { avatarRef },
8701
9573
  ...avatarRef === void 0 && avatarFallback !== void 0 ? { avatarFallback } : {},
@@ -8778,13 +9650,16 @@ var WorldService = class {
8778
9650
  voiceCount
8779
9651
  };
8780
9652
  }
8781
- async worldRecordRef(viewerUserId, recordUid) {
9653
+ async worldRecordRef(viewerUserId, recordUid, ownerUserId) {
8782
9654
  const recordRef = `arkme-world-record-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`world-record-v1:${String(viewerUserId)}:${recordUid}`).digest("base64url")}`;
8783
9655
  const now = Date.now();
8784
9656
  this.pruneWorldRecordRefs(now);
9657
+ const previous = this.worldRecordRefs.get(recordRef);
9658
+ const resolvedOwnerUserId = ownerUserId ?? previous?.ownerUserId;
8785
9659
  this.worldRecordRefs.set(recordRef, {
8786
9660
  viewerUserId,
8787
9661
  recordUid,
9662
+ ...resolvedOwnerUserId === void 0 || resolvedOwnerUserId <= 0 ? {} : { ownerUserId: resolvedOwnerUserId },
8788
9663
  expiresAtMillis: now + ARKME_WORLD_RECORD_REF_TTL_MILLIS
8789
9664
  });
8790
9665
  return recordRef;
@@ -8877,6 +9752,7 @@ var ArkmeService = class {
8877
9752
  runtime;
8878
9753
  aiVideo;
8879
9754
  arrangement;
9755
+ calendar;
8880
9756
  wechat;
8881
9757
  recording;
8882
9758
  profile;
@@ -8906,6 +9782,7 @@ var ArkmeService = class {
8906
9782
  this.runtime = new ServiceRuntime(config, sessionStore, stateStore, fetchImpl, pendingSessionStore);
8907
9783
  this.aiVideo = new AiVideoService(this.runtime);
8908
9784
  this.arrangement = new ArrangementService(this.runtime);
9785
+ this.calendar = new CalendarService(this.runtime);
8909
9786
  this.wechat = new WechatService(this.runtime);
8910
9787
  this.recording = new RecordingService(this.runtime);
8911
9788
  this.profile = new ProfileService(this.runtime);
@@ -8918,12 +9795,14 @@ var ArkmeService = class {
8918
9795
  recordItem: (raw) => this.recordItem(raw)
8919
9796
  });
8920
9797
  this.record = new RecordService(this.runtime, this.media, this.source);
8921
- this.search = new SearchService(this.runtime, this.record);
9798
+ this.search = new SearchService(this.runtime, this.record, this.media);
8922
9799
  this.bot = new BotService(this.runtime, this.source);
8923
9800
  this.outgoingCall = new OutgoingCallService(this.runtime, this.source, this.profile, outgoingCallBroker);
8924
9801
  this.world = new WorldService(this.runtime, { refreshProfile: async () => await this.refreshProfile() }, this.media, this.record);
8925
9802
  this.arko = new ArkoService(this.runtime, this.profile);
8926
- this.group = new GroupService(this.runtime, this.source, this.profile);
9803
+ this.group = new GroupService(this.runtime, this.source, this.profile, { sendPrivateText: async (sourceRef, chatSessionUid, text, recordUid, relationUid, session, signal) => {
9804
+ await this.chat.sendChatSourceTextRaw(sourceRef, chatSessionUid, text, recordUid, relationUid, session, void 0, void 0, signal);
9805
+ } });
8927
9806
  this.relatedRecording = new RelatedRecordingService(this.runtime, this.source);
8928
9807
  this.community = new CommunityService(this.runtime, this.source, this.profile);
8929
9808
  this.interwoven = new InterwovenService(this.runtime, this.source, this.profile);
@@ -8964,7 +9843,10 @@ var ArkmeService = class {
8964
9843
  this.realtime.handleChatRealtimeNotice(notice);
8965
9844
  }
8966
9845
  async refreshChatSessionProjectionBatch(pending) {
8967
- return await this.realtime.refreshChatSessionProjectionBatch(pending);
9846
+ return (await this.realtime.refreshChatSessionProjectionBatch(pending.map(([uid, latestSequence]) => [uid, {
9847
+ latestSequence,
9848
+ notificationHints: []
9849
+ }]))).map(([uid, projection]) => [uid, projection.latestSequence]);
8968
9850
  }
8969
9851
  attachOpenClawProvisioner(provisioner) {
8970
9852
  this.bot.attachOpenClawProvisioner(provisioner);
@@ -9023,6 +9905,8 @@ var ArkmeService = class {
9023
9905
  revisionPolling: true,
9024
9906
  userProfile: true,
9025
9907
  imageRead: true,
9908
+ recordCalendar: true,
9909
+ imageLibrary: true,
9026
9910
  sourceDirectory: true,
9027
9911
  sourceTimeline: true,
9028
9912
  sourceTextSend: true,
@@ -9031,6 +9915,7 @@ var ArkmeService = class {
9031
9915
  fileUpload: this.config.richMediaSendEnabled !== false,
9032
9916
  outgoingCall: true,
9033
9917
  groupMembers: true,
9918
+ groupMemberAdd: true,
9034
9919
  userCard: true,
9035
9920
  openPrivateChat: true,
9036
9921
  groupSettings: true,
@@ -9040,6 +9925,7 @@ var ArkmeService = class {
9040
9925
  extensionPreviews: true,
9041
9926
  worldFeed: true,
9042
9927
  worldInteractions: true,
9928
+ worldVoiceprintPlayback: true,
9043
9929
  arrangements: true,
9044
9930
  myExtensions: true,
9045
9931
  extensionPublish: true,
@@ -9246,6 +10132,15 @@ var ArkmeService = class {
9246
10132
  async listGroupMembers(sourceRef, options = {}) {
9247
10133
  return await this.group.listGroupMembers(sourceRef, options);
9248
10134
  }
10135
+ async listGroupMemberCandidates(sourceRef, options = {}) {
10136
+ return await this.group.listGroupMemberCandidates(sourceRef, options);
10137
+ }
10138
+ async groupInvitePreview(sourceRef, signal) {
10139
+ return await this.group.groupInvitePreview(sourceRef, signal);
10140
+ }
10141
+ async addGroupMembers(sourceRef, candidateRefs, signal) {
10142
+ return await this.group.addGroupMembers(sourceRef, candidateRefs, signal);
10143
+ }
9249
10144
  async groupSettings(sourceRef, signal) {
9250
10145
  return await this.group.groupSettings(sourceRef, signal);
9251
10146
  }
@@ -9394,6 +10289,13 @@ var ArkmeService = class {
9394
10289
  async searchScene(options) {
9395
10290
  return await this.search.searchScene(options);
9396
10291
  }
10292
+ /**
10293
+ * Build the desktop image library from the owner's mixed image/video scene.
10294
+ * Signed storage URLs stay inside the Provider and are replaced by account-bound media refs.
10295
+ */
10296
+ async searchImages(options) {
10297
+ return await this.search.searchImages(options);
10298
+ }
9397
10299
  async searchRecordings(options) {
9398
10300
  return await this.search.searchRecordings(options);
9399
10301
  }
@@ -9406,6 +10308,12 @@ var ArkmeService = class {
9406
10308
  async list(limit, cursor) {
9407
10309
  return await this.record.list(limit, cursor);
9408
10310
  }
10311
+ async calendarBuckets(options) {
10312
+ return await this.calendar.bucketPage(options);
10313
+ }
10314
+ async calendarRecords(options) {
10315
+ return await this.calendar.dayRecords(options);
10316
+ }
9409
10317
  async listWorldRecords(options = {}) {
9410
10318
  return await this.world.listWorldRecords(options);
9411
10319
  }
@@ -9445,6 +10353,12 @@ var ArkmeService = class {
9445
10353
  async listWorldFeed(options = {}) {
9446
10354
  return await this.world.listWorldFeed(options);
9447
10355
  }
10356
+ async worldVoiceprintPlaybackAvailability(recordRefs, signal) {
10357
+ return await this.world.worldVoiceprintPlaybackAvailability(recordRefs, signal);
10358
+ }
10359
+ async generateWorldVoiceprintPlayback(input) {
10360
+ return await this.world.generateWorldVoiceprintPlayback(input);
10361
+ }
9448
10362
  /** Read the authenticated comment/reply tree behind one account-bound World reference. */
9449
10363
  async listWorldInteractions(recordRef, options = {}) {
9450
10364
  return await this.world.listWorldInteractions(recordRef, options);
@@ -9495,6 +10409,8 @@ const ARKME_EXTENSION_FORMAT = "arkme-cordis-extension";
9495
10409
  const ARKME_EXTENSION_MAX_BYTES = 100 * 1024 * 1024;
9496
10410
  const ARKME_EXTENSION_ICON_MAX_BYTES = 2 * 1024 * 1024;
9497
10411
  const ARKME_EXTENSION_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
10412
+ /** Browser/model-safe projection. Host filesystem paths and Dynamic Cordis IDs stay private. */
10413
+ const ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE = "插件运行失败,已自动停用。";
9498
10414
  //#endregion
9499
10415
  //#region src/extensions/artifact.ts
9500
10416
  const TAR_BLOCK_BYTES$1 = 512;
@@ -9977,14 +10893,29 @@ function persistentClientFactory(requireModule, spec) {
9977
10893
  })
9978
10894
  });
9979
10895
  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)}`);
10896
+ if (!response.ok || envelope.ok !== true) {
10897
+ const error = new Error(envelope.error?.message ?? `Arkme Host returned HTTP ${String(response.status)}`);
10898
+ if (envelope.error?.code !== void 0) error.code = envelope.error.code;
10899
+ throw error;
10900
+ }
9981
10901
  return envelope.value;
9982
10902
  };
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
- });
10903
+ let deactivateClient;
10904
+ const callHost = async (method, args = null) => {
10905
+ try {
10906
+ return await callOperation(spec.operation ?? "extensions.persistent.invoke", {
10907
+ ...spec.identityKey === "packageName" ? { packageName: spec.extensionId } : {
10908
+ extensionId: spec.extensionId,
10909
+ version: spec.version
10910
+ },
10911
+ method,
10912
+ args: jsonValue(args)
10913
+ });
10914
+ } catch (error) {
10915
+ if (error.code === "extension-runtime-unavailable") deactivateClient?.();
10916
+ throw error;
10917
+ }
10918
+ };
9988
10919
  const guardedService = (service, name) => new Proxy(service, { get(target, property) {
9989
10920
  const member = Reflect.get(target, property, target);
9990
10921
  if (typeof member !== "function") return member;
@@ -10044,8 +10975,10 @@ function persistentClientFactory(requireModule, spec) {
10044
10975
  name: `arkme-extension-client:${spec.extensionId}`,
10045
10976
  async apply(ctx) {
10046
10977
  if (spec.identityKey !== "packageName") {
10047
- const state = await callOperation("extensions.enabled-state", { extensionId: spec.extensionId });
10048
- if (state.installed !== true || state.enabled !== true) return;
10978
+ if ((await callOperation("extensions.persistent.client-state", {
10979
+ extensionId: spec.extensionId,
10980
+ version: spec.version
10981
+ })).mount !== true) return;
10049
10982
  }
10050
10983
  const styles = new Styles();
10051
10984
  const traps = {
@@ -10089,7 +11022,16 @@ function persistentClientFactory(requireModule, spec) {
10089
11022
  }
10090
11023
  };
10091
11024
  ctx.effect(() => () => styles.dispose(), `arkme-extension:${spec.extensionId}:styles`);
10092
- await ctx.plugin(guarded);
11025
+ const fiber = ctx.plugin(guarded);
11026
+ const deactivate = () => {
11027
+ styles.dispose();
11028
+ Promise.resolve(fiber.dispose?.()).catch(() => void 0);
11029
+ };
11030
+ deactivateClient = deactivate;
11031
+ ctx.effect(() => () => {
11032
+ if (deactivateClient === deactivate) deactivateClient = void 0;
11033
+ }, `arkme-extension:${spec.extensionId}:client-fiber`);
11034
+ await fiber;
10093
11035
  }
10094
11036
  };
10095
11037
  }
@@ -10125,18 +11067,18 @@ function writeSecure(path, content) {
10125
11067
  });
10126
11068
  chmodSync(path, 384);
10127
11069
  }
10128
- function activationText(extensionId, enabled) {
11070
+ function activationText(extensionId, enabled, quarantine) {
10129
11071
  return `${JSON.stringify({
10130
11072
  schema_version: 1,
10131
11073
  extension_id: extensionId,
10132
- enabled
11074
+ enabled,
11075
+ ...quarantine === void 0 ? {} : { quarantine }
10133
11076
  }, void 0, 2)}\n`;
10134
11077
  }
10135
- function writePersistentExtensionActivation(bundleDirectory, extensionId, enabled) {
10136
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(extensionId)) throw new Error("扩展身份无效");
11078
+ function writeActivationState(bundleDirectory, state) {
10137
11079
  const target = join(bundleDirectory, "activation.json");
10138
11080
  const temporary = join(bundleDirectory, `.activation.${randomUUID()}.tmp`);
10139
- writeSecure(temporary, activationText(extensionId, enabled));
11081
+ writeSecure(temporary, activationText(state.extension_id, state.enabled, state.quarantine));
10140
11082
  try {
10141
11083
  renameSync(temporary, target);
10142
11084
  chmodSync(target, 384);
@@ -10145,6 +11087,27 @@ function writePersistentExtensionActivation(bundleDirectory, extensionId, enable
10145
11087
  }
10146
11088
  return target;
10147
11089
  }
11090
+ function writePersistentExtensionActivation(bundleDirectory, extensionId, enabled) {
11091
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(extensionId)) throw new Error("扩展身份无效");
11092
+ return writeActivationState(bundleDirectory, {
11093
+ schema_version: 1,
11094
+ extension_id: extensionId,
11095
+ enabled
11096
+ });
11097
+ }
11098
+ function quarantinePersistentExtension(bundleDirectory, extensionId, error, failedAtMillis = Date.now()) {
11099
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(extensionId)) throw new Error("扩展身份无效");
11100
+ return writeActivationState(bundleDirectory, {
11101
+ schema_version: 1,
11102
+ extension_id: extensionId,
11103
+ enabled: false,
11104
+ quarantine: {
11105
+ code: "runtime-load-failed",
11106
+ failed_at_millis: failedAtMillis,
11107
+ message: ((error instanceof Error ? error.message : String(error)).trim() || "persistent extension failed to load").slice(0, 2e3)
11108
+ }
11109
+ });
11110
+ }
10148
11111
  function readPersistentExtensionActivation(installationUrl) {
10149
11112
  const activationUrl = new URL("./activation.json", installationUrl);
10150
11113
  let value;
@@ -10164,6 +11127,10 @@ function readPersistentExtensionActivation(installationUrl) {
10164
11127
  if (value === null || typeof value !== "object") throw new Error("Arkme persistent extension activation state is invalid");
10165
11128
  const state = value;
10166
11129
  if (state.schema_version !== 1 || typeof state.extension_id !== "string" || typeof state.enabled !== "boolean") throw new Error("Arkme persistent extension activation state is invalid");
11130
+ if (state.quarantine !== void 0) {
11131
+ const quarantine = state.quarantine;
11132
+ 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");
11133
+ }
10167
11134
  return state;
10168
11135
  }
10169
11136
  function materializePersistentExtensionBundle(input) {
@@ -10192,7 +11159,8 @@ function materializePersistentExtensionBundle(input) {
10192
11159
  if (existing !== void 0) if (existing.artifact_sha256 === installation.artifact_sha256 && existing.version === installation.version) {
10193
11160
  try {
10194
11161
  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");
11162
+ const clientBundle = input.clientCode === void 0 ? void 0 : readFileSync(join(bundleDirectory, "lib", "client.js"), "utf8");
11163
+ 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
11164
  if (manifest.exports?.["."] === "./lib/index.js" && manifest.exports?.["./package.json"] === "./package.json" && clientReady) {
10197
11165
  if (!existsSync(activationPath)) writeSecure(activationPath, activationText(input.resolution.extension_id, true));
10198
11166
  return {
@@ -10453,8 +11421,22 @@ async function mountPersistentArkmeHostExtension(installation, registration) {
10453
11421
  throw error;
10454
11422
  }
10455
11423
  }
11424
+ function quarantineRuntimeFailure(installation, installationUrl, error) {
11425
+ try {
11426
+ quarantinePersistentExtension(dirname(fileURLToPath(installationUrl)), installation.extension_id, error);
11427
+ } catch (quarantineError) {
11428
+ console.error(`[arkme-extension:${installation.extension_id}] failed to persist quarantine state`, quarantineError);
11429
+ }
11430
+ console.error(`[arkme-extension:${installation.extension_id}] disabled after runtime load failure`, error);
11431
+ }
10456
11432
  async function applyPersistentArkmeHostExtension(ctx, installationUrl) {
10457
- const installation = readInstallation(installationUrl);
11433
+ let installation;
11434
+ try {
11435
+ installation = readInstallation(installationUrl);
11436
+ } catch (error) {
11437
+ console.error("[arkme-extension:unknown] ignored invalid persistent installation metadata", error);
11438
+ return;
11439
+ }
10458
11440
  const previous = persistentRegistrations.get(installation.extension_id);
10459
11441
  if (previous?.ctx === ctx && previous.installationUrl.href === installationUrl.href) return;
10460
11442
  if (previous !== void 0) await deactivatePersistentArkmeExtension(installation.extension_id);
@@ -10463,27 +11445,55 @@ async function applyPersistentArkmeHostExtension(ctx, installationUrl) {
10463
11445
  installationUrl
10464
11446
  };
10465
11447
  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);
11448
+ try {
11449
+ ctx.effect(() => () => {
11450
+ if (persistentRegistrations.get(installation.extension_id) !== registration) return;
11451
+ persistentRegistrations.delete(installation.extension_id);
11452
+ persistentHandlers.delete(installation.extension_id);
11453
+ }, `arkme-extension:${installation.extension_id}:registration`);
11454
+ ctx.effect(() => () => {
11455
+ if (persistentHandlers.get(installation.extension_id) === registration.handlers) persistentHandlers.delete(installation.extension_id);
11456
+ }, `arkme-extension:${installation.extension_id}:handlers`);
11457
+ const activation = readPersistentExtensionActivation(installationUrl);
11458
+ if (activation.extension_id !== installation.extension_id) throw new Error("Arkme persistent extension activation identity mismatch");
11459
+ if (!activation.enabled) return;
11460
+ await mountPersistentArkmeHostExtension(installation, registration);
11461
+ } catch (error) {
11462
+ if (persistentRegistrations.get(installation.extension_id) === registration) {
11463
+ persistentRegistrations.delete(installation.extension_id);
11464
+ if (persistentHandlers.get(installation.extension_id) === registration.handlers) persistentHandlers.delete(installation.extension_id);
11465
+ if (persistentClientActivations.get(installation.extension_id) === registration.clientActivation) persistentClientActivations.delete(installation.extension_id);
11466
+ }
11467
+ quarantineRuntimeFailure(installation, installationUrl, error);
11468
+ }
10478
11469
  }
10479
11470
  function persistentArkmeExtensionActive(extensionId) {
10480
11471
  return persistentRegistrations.get(extensionId)?.fiber !== void 0 || persistentClientActivations.has(extensionId);
10481
11472
  }
11473
+ function persistentArkmeExtensionRuntimeState(extensionId) {
11474
+ const registration = persistentRegistrations.get(extensionId);
11475
+ if (registration === void 0) return void 0;
11476
+ try {
11477
+ return {
11478
+ version: readInstallation(registration.installationUrl).version,
11479
+ installationUrl: registration.installationUrl.href,
11480
+ active: registration.fiber !== void 0 || registration.clientActivation !== void 0
11481
+ };
11482
+ } catch {
11483
+ return;
11484
+ }
11485
+ }
10482
11486
  /** Re-mount a verified Host half when its wrapper is already present in the current DSH process. */
10483
11487
  async function activatePersistentArkmeExtension(extensionId) {
10484
11488
  const registration = persistentRegistrations.get(extensionId);
10485
11489
  if (registration === void 0) return false;
10486
- return await mountPersistentArkmeHostExtension(readInstallation(registration.installationUrl), registration);
11490
+ const installation = readInstallation(registration.installationUrl);
11491
+ try {
11492
+ return await mountPersistentArkmeHostExtension(installation, registration);
11493
+ } catch (error) {
11494
+ quarantineRuntimeFailure(installation, registration.installationUrl, error);
11495
+ throw error;
11496
+ }
10487
11497
  }
10488
11498
  async function deactivatePersistentArkmeExtension(extensionId) {
10489
11499
  const registration = persistentRegistrations.get(extensionId);
@@ -10503,4 +11513,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
10503
11513
  return jsonValue(await handler(jsonValue(args)));
10504
11514
  }
10505
11515
  //#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 };
11516
+ export { ARKME_EXTENSION_MAX_BYTES as A, renderArkmeSandboxHostEntry as C, unpackArkmeExtension as D, sha256Hex as E, ArkmePluginError as F, ArkmeOutgoingCallError as I, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as M, ArkmeService as N, ARKME_EXTENSION_FORMAT as O, ARKME_PROVIDER_CONTRACT_VERSION as P, readLocalBundleTarball as S, canonicalExtensionJson as T, bundleSha256 as _, persistentArkmeExtensionActive as a, packBundleFiles as b, readPersistentExtensionActivation as c, verifyBundleResolutionSignature as d, verifyExtensionResolutionSignature as f, arkmeSandboxEntryId as g, ArkmeBundleArtifactError as h, invokePersistentArkmeExtension as i, ARKME_EXTENSION_PREVIEW_MAX_BYTES as j, ARKME_EXTENSION_ICON_MAX_BYTES as k, writePersistentExtensionActivation as l, ARKME_BUNDLE_MAX_BYTES as m, applyPersistentArkmeHostExtension as n, persistentArkmeExtensionRuntimeState as o, ARKME_BUNDLE_ARTIFACT_KIND as p, deactivatePersistentArkmeExtension as r, materializePersistentExtensionBundle as s, activatePersistentArkmeExtension as t, renderArkmeBundleClientBundle as u, canonicalBundleJson as v, assertExtensionArtifactSize as w, packLocalBundleDirectory as x, inspectBundleArtifact as y };