@senguoyun/dsh-arkme 0.1.45 → 0.1.46

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 (105) hide show
  1. package/docs/direct-message-refusal.md +39 -0
  2. package/docs/plugin-release.md +28 -3
  3. package/lib/client.js +1719 -562
  4. package/lib/client.js.map +1 -1
  5. package/lib/harness-model-client.js +4922 -0
  6. package/lib/index.js +1605 -110
  7. package/lib/persistent-extension.js +1 -1
  8. package/lib/{persistent-runtime-CEGGFpR7.js → persistent-runtime-DpNP9UKr.js} +1152 -155
  9. package/lib/sdk.js +25 -2
  10. package/lib/types/arkme-service.d.ts +10 -1
  11. package/lib/types/arkme-service.d.ts.map +1 -1
  12. package/lib/types/chat-pin-projection.d.ts +4 -0
  13. package/lib/types/chat-pin-projection.d.ts.map +1 -0
  14. package/lib/types/chat-realtime.d.ts +13 -1
  15. package/lib/types/chat-realtime.d.ts.map +1 -1
  16. package/lib/types/client/ArkmeAttachmentStrip.d.ts +2 -1
  17. package/lib/types/client/ArkmeAttachmentStrip.d.ts.map +1 -1
  18. package/lib/types/client/ArkmeBillingSettings.d.ts +7 -1
  19. package/lib/types/client/ArkmeBillingSettings.d.ts.map +1 -1
  20. package/lib/types/client/ArkmeMarkdownComposerInput.d.ts.map +1 -1
  21. package/lib/types/client/ArkmeModelSelect.d.ts +20 -0
  22. package/lib/types/client/ArkmeModelSelect.d.ts.map +1 -0
  23. package/lib/types/client/ArkmeNoteDetails.d.ts +3 -1
  24. package/lib/types/client/ArkmeNoteDetails.d.ts.map +1 -1
  25. package/lib/types/client/ArkmeRichContent.d.ts +3 -1
  26. package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
  27. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  28. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  29. package/lib/types/client/chat-directory-store.d.ts +7 -1
  30. package/lib/types/client/chat-directory-store.d.ts.map +1 -1
  31. package/lib/types/client/conversation-memory-cache.d.ts +3 -0
  32. package/lib/types/client/conversation-memory-cache.d.ts.map +1 -1
  33. package/lib/types/client/conversation-timeline-refresh.d.ts +5 -0
  34. package/lib/types/client/conversation-timeline-refresh.d.ts.map +1 -0
  35. package/lib/types/client/direct-message-admission.d.ts +15 -0
  36. package/lib/types/client/direct-message-admission.d.ts.map +1 -0
  37. package/lib/types/client/harness-model-client.d.ts +15 -0
  38. package/lib/types/client/harness-model-client.d.ts.map +1 -0
  39. package/lib/types/client/realtime-client-events.d.ts.map +1 -1
  40. package/lib/types/client/record-reedit-editors.d.ts +38 -0
  41. package/lib/types/client/record-reedit-editors.d.ts.map +1 -0
  42. package/lib/types/client/record-reedit-submissions.d.ts +12 -0
  43. package/lib/types/client/record-reedit-submissions.d.ts.map +1 -0
  44. package/lib/types/client/timeline-media.d.ts +4 -0
  45. package/lib/types/client/timeline-media.d.ts.map +1 -0
  46. package/lib/types/client/ui-controller.d.ts.map +1 -1
  47. package/lib/types/direct-message-admission.d.ts +17 -0
  48. package/lib/types/direct-message-admission.d.ts.map +1 -0
  49. package/lib/types/file-transfer-contract.d.ts +2 -0
  50. package/lib/types/file-transfer-contract.d.ts.map +1 -1
  51. package/lib/types/file-transfer-owner.d.ts.map +1 -1
  52. package/lib/types/harness-embed-contract.d.ts +1 -0
  53. package/lib/types/harness-embed-contract.d.ts.map +1 -1
  54. package/lib/types/harness-embed-route.d.ts +2 -1
  55. package/lib/types/harness-embed-route.d.ts.map +1 -1
  56. package/lib/types/host-api.d.ts.map +1 -1
  57. package/lib/types/index.d.ts.map +1 -1
  58. package/lib/types/local-database.d.ts +7 -2
  59. package/lib/types/local-database.d.ts.map +1 -1
  60. package/lib/types/managed-ai/adapter.d.ts +4 -1
  61. package/lib/types/managed-ai/adapter.d.ts.map +1 -1
  62. package/lib/types/managed-ai/image-preparer.d.ts +19 -0
  63. package/lib/types/managed-ai/image-preparer.d.ts.map +1 -0
  64. package/lib/types/managed-ai/multipart-upload.d.ts +8 -0
  65. package/lib/types/managed-ai/multipart-upload.d.ts.map +1 -0
  66. package/lib/types/managed-ai/transport.d.ts +59 -0
  67. package/lib/types/managed-ai/transport.d.ts.map +1 -0
  68. package/lib/types/record-reedit-contract.d.ts +84 -1
  69. package/lib/types/record-reedit-contract.d.ts.map +1 -1
  70. package/lib/types/rich-media-routes.d.ts.map +1 -1
  71. package/lib/types/sdk/index.d.ts +6 -1
  72. package/lib/types/sdk/index.d.ts.map +1 -1
  73. package/lib/types/services/chat-realtime-service.d.ts +4 -3
  74. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  75. package/lib/types/services/chat-service.d.ts.map +1 -1
  76. package/lib/types/services/direct-message-admission-service.d.ts +21 -0
  77. package/lib/types/services/direct-message-admission-service.d.ts.map +1 -0
  78. package/lib/types/services/file-transfers.d.ts +8 -1
  79. package/lib/types/services/file-transfers.d.ts.map +1 -1
  80. package/lib/types/services/media-service.d.ts +2 -0
  81. package/lib/types/services/media-service.d.ts.map +1 -1
  82. package/lib/types/services/message-action-infrastructure.d.ts.map +1 -1
  83. package/lib/types/services/record-reedit-attachments.d.ts +30 -0
  84. package/lib/types/services/record-reedit-attachments.d.ts.map +1 -0
  85. package/lib/types/services/record-reedit-submissions.d.ts +37 -0
  86. package/lib/types/services/record-reedit-submissions.d.ts.map +1 -0
  87. package/lib/types/services/record-service.d.ts +30 -2
  88. package/lib/types/services/record-service.d.ts.map +1 -1
  89. package/lib/types/services/recording-forward-gateway.d.ts.map +1 -1
  90. package/lib/types/services/service.d.ts +12 -2
  91. package/lib/types/services/service.d.ts.map +1 -1
  92. package/lib/types/services/source-service.d.ts +0 -1
  93. package/lib/types/services/source-service.d.ts.map +1 -1
  94. package/lib/types/state-store.d.ts +8 -2
  95. package/lib/types/state-store.d.ts.map +1 -1
  96. package/lib/types/tools/business/conversation/direct-message-refusal.d.ts +3 -0
  97. package/lib/types/tools/business/conversation/direct-message-refusal.d.ts.map +1 -0
  98. package/lib/types/tools/business/index.d.ts.map +1 -1
  99. package/lib/types/tools/business/records/reedit.d.ts.map +1 -1
  100. package/lib/types/tools/ports/index.d.ts +2 -1
  101. package/lib/types/tools/ports/index.d.ts.map +1 -1
  102. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  103. package/lib/types/types.d.ts +24 -2
  104. package/lib/types/types.d.ts.map +1 -1
  105. package/package.json +6 -6
@@ -48,6 +48,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
48
48
  enumerable: true
49
49
  }) : target, mod));
50
50
  //#endregion
51
+ //#region src/direct-message-admission.ts
52
+ function directMessageAdmissionMessage(admission) {
53
+ switch (admission.state) {
54
+ case "allowed": return "";
55
+ case "refused_by_self": return "你已拒收对方的消息";
56
+ case "refused_by_counterpart": return "对方已拒收你的消息";
57
+ case "mutually_refused": return "你们已互相拒收消息";
58
+ }
59
+ }
60
+ //#endregion
51
61
  //#region src/request-coordinator.ts
52
62
  const DEFAULT_LANE_LIMITS = {
53
63
  auth: {
@@ -846,6 +856,14 @@ var ArkmePluginError = class extends Error {
846
856
  if (options?.writeOutcomeUnknown === true) this.writeOutcomeUnknown = true;
847
857
  }
848
858
  };
859
+ /** Opaque upstream body, for the owning business adapter only. Never serialize this error wholesale. */
860
+ var ArkmeUpstreamResponseError = class extends ArkmePluginError {
861
+ responseData;
862
+ constructor(code, message, retryable, httpStatus, responseData) {
863
+ super(code, message, retryable, httpStatus);
864
+ this.responseData = responseData;
865
+ }
866
+ };
849
867
  function remoteWriteOutcomeUnknown(error) {
850
868
  if ([
851
869
  "arkme-network-error",
@@ -1110,7 +1128,7 @@ var ServiceRuntime = class {
1110
1128
  const errorData = objectValue$2(envelope.data);
1111
1129
  const serviceErrorCode = preferDataError ? stringValue$2(errorData.error_code).trim() : "";
1112
1130
  const serviceMessage = preferDataError ? stringValue$2(errorData.message).trim() : "";
1113
- throw new ArkmePluginError(serviceErrorCode || `arkme-code-${envelope.code}`, serviceMessage || envelope.message?.trim() || "Arkme 服务请求失败", serviceErrorCode === "" ? envelope.code >= 500 : serviceErrorCode === "ai_comic_video_rate_limited", 502);
1131
+ throw new ArkmeUpstreamResponseError(serviceErrorCode || `arkme-code-${envelope.code}`, serviceMessage || envelope.message?.trim() || "Arkme 服务请求失败", serviceErrorCode === "" ? envelope.code !== 1004 && envelope.code >= 500 : serviceErrorCode === "ai_comic_video_rate_limited", 502, envelope.data);
1114
1132
  }
1115
1133
  return envelope.data ?? {};
1116
1134
  } catch (error) {
@@ -1417,6 +1435,102 @@ var ServiceRuntime = class {
1417
1435
  }
1418
1436
  };
1419
1437
  //#endregion
1438
+ //#region src/services/direct-message-admission-service.ts
1439
+ function projectDirectMessageAdmission(value, chatSessionUid) {
1440
+ const data = objectValue$2(value);
1441
+ const validStatus = (value) => value === 1 || value === 2;
1442
+ const validRevision = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1443
+ if (chatSessionUid === "" || data.chat_session_uid !== chatSessionUid || !validStatus(data.own_refusal_status) || !validStatus(data.counterpart_refusal_status) || !validRevision(data.own_revision) || !validRevision(data.counterpart_revision) || data.own_refusal_status === 1 && data.own_revision === 0 || data.counterpart_refusal_status === 1 && data.counterpart_revision === 0 || typeof data.refusal_creation_enabled !== "boolean") throw new ArkmePluginError("arkme-response-invalid", "私聊发送状态无效,请刷新后重试", false, 502);
1444
+ const ownRefused = data.own_refusal_status === 1;
1445
+ const counterpartRefused = data.counterpart_refusal_status === 1;
1446
+ const state = ownRefused ? counterpartRefused ? 4 : 2 : counterpartRefused ? 3 : 1;
1447
+ if (data.admission_state !== state || data.can_send !== (state === 1)) throw new ArkmePluginError("arkme-response-invalid", "私聊发送状态不一致,请刷新后重试", false, 502);
1448
+ return {
1449
+ state: [
1450
+ "allowed",
1451
+ "refused_by_self",
1452
+ "refused_by_counterpart",
1453
+ "mutually_refused"
1454
+ ][state - 1],
1455
+ canSend: state === 1,
1456
+ ownRefused,
1457
+ counterpartRefused,
1458
+ ownRevision: data.own_revision,
1459
+ counterpartRevision: data.counterpart_revision,
1460
+ refusalCreationEnabled: data.refusal_creation_enabled
1461
+ };
1462
+ }
1463
+ var ArkmeDirectMessageAdmissionError = class extends ArkmePluginError {
1464
+ admission;
1465
+ constructor(code, admission, message) {
1466
+ super(code, message ?? (code === "arkme-code-2002" ? "拒收状态已变化,请确认最新状态后重试" : directMessageAdmissionMessage(admission)), false, 409);
1467
+ this.admission = admission;
1468
+ }
1469
+ };
1470
+ function admissionResponseError(error, chatSessionUid, expectedCode) {
1471
+ if (!(error instanceof ArkmeUpstreamResponseError) || error.code !== expectedCode) return error;
1472
+ try {
1473
+ const projection = projectDirectMessageAdmission(error.responseData, chatSessionUid);
1474
+ if (expectedCode === "arkme-code-1004" && projection.canSend) return error;
1475
+ return new ArkmeDirectMessageAdmissionError(error.code, projection);
1476
+ } catch {
1477
+ return error;
1478
+ }
1479
+ }
1480
+ /** Only new receiver-visible Chat messages use this adapter, never reads, reedit or historical mirroring. */
1481
+ async function postChatMessageCreation(runtime, path, body, session, signal, options) {
1482
+ try {
1483
+ return await runtime.authenticatedChatPost(path, body, session, signal, ...options === void 0 ? [] : [options]);
1484
+ } catch (error) {
1485
+ throw admissionResponseError(error, String(body.chat_session_uid ?? ""), "arkme-code-1004");
1486
+ }
1487
+ }
1488
+ /** Chat remains the fact and eligibility owner; source references enforce account binding on the Host. */
1489
+ var DirectMessageAdmissionService = class {
1490
+ runtime;
1491
+ source;
1492
+ constructor(runtime, source) {
1493
+ this.runtime = runtime;
1494
+ this.source = source;
1495
+ }
1496
+ async context(sourceRef) {
1497
+ const session = await this.runtime.requireSession();
1498
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
1499
+ if (source.kind !== "private_chat") throw new ArkmePluginError("source-invalid", "仅真人私聊支持拒收消息", false);
1500
+ return {
1501
+ session,
1502
+ source
1503
+ };
1504
+ }
1505
+ async directMessageAdmission(sourceRef, signal) {
1506
+ const { session, source } = await this.context(sourceRef);
1507
+ return projectDirectMessageAdmission(await this.runtime.authenticatedChatPost("/api/v1/chats/direct-message-admission/query", { chat_session_uid: source.ownerRef }, session, signal, {
1508
+ lane: "interactive-read",
1509
+ bypassCache: true
1510
+ }), source.ownerRef);
1511
+ }
1512
+ async setDirectMessageRefusal(sourceRef, refused, expectedRevision, signal) {
1513
+ if (typeof refused !== "boolean" || !Number.isSafeInteger(expectedRevision) || expectedRevision < 0) throw new ArkmePluginError("request-invalid", "拒收设置参数无效", false);
1514
+ const { session, source } = await this.context(sourceRef);
1515
+ try {
1516
+ return projectDirectMessageAdmission(await this.runtime.authenticatedChatPost("/api/v1/chats/direct-message-refusal/set", {
1517
+ chat_session_uid: source.ownerRef,
1518
+ status: refused ? 1 : 2,
1519
+ expected_revision: expectedRevision
1520
+ }, session, signal), source.ownerRef);
1521
+ } catch (error) {
1522
+ if (error instanceof ArkmeUpstreamResponseError && error.code === "arkme-code-1004") {
1523
+ let projection;
1524
+ try {
1525
+ projection = projectDirectMessageAdmission(error.responseData, source.ownerRef);
1526
+ } catch {}
1527
+ if (projection?.refusalCreationEnabled === false) throw new ArkmeDirectMessageAdmissionError(error.code, projection, "新增拒收暂未开放,已有拒收仍可解除");
1528
+ }
1529
+ throw admissionResponseError(error, source.ownerRef, "arkme-code-2002");
1530
+ }
1531
+ }
1532
+ };
1533
+ //#endregion
1420
1534
  //#region src/services/recording-forward-gateway.ts
1421
1535
  /** Deliver Audio selectors through each destination owner's native forward contract. */
1422
1536
  var OwnerRecordingForwardGateway = class {
@@ -1442,7 +1556,7 @@ var OwnerRecordingForwardGateway = class {
1442
1556
  transcript_source: item.transcriptSource
1443
1557
  }));
1444
1558
  if (target.kind === "private_chat" || target.kind === "group_chat") {
1445
- const data = await this.runtime.authenticatedChatPost("/api/v1/chats/records/forward", {
1559
+ const data = await postChatMessageCreation(this.runtime, "/api/v1/chats/records/forward", {
1446
1560
  chat_session_uid: target.ownerRef,
1447
1561
  client_request_id: input.requestId,
1448
1562
  source_items: [{
@@ -32844,6 +32958,17 @@ function projectArkmeChatAttentionFromMuted(rawUnreadCount, isMuted) {
32844
32958
  return projectArkmeChatAttention(rawUnreadCount, void 0, isMuted);
32845
32959
  }
32846
32960
  //#endregion
32961
+ //#region src/chat-pin-projection.ts
32962
+ /** Policy freshness is independent of message sequence and event delivery order. */
32963
+ function retainNewerArkmeChatPin(current, incoming) {
32964
+ if (incoming.kind !== "private_chat" && incoming.kind !== "group_chat" || current?.kind !== incoming.kind || current.isPinned === void 0 || current.chatPolicyUpdatedAtMillis === void 0 || current.chatPolicyUpdatedAtMillis <= (incoming.chatPolicyUpdatedAtMillis ?? 0)) return incoming;
32965
+ return {
32966
+ ...incoming,
32967
+ isPinned: current.isPinned,
32968
+ chatPolicyUpdatedAtMillis: current.chatPolicyUpdatedAtMillis
32969
+ };
32970
+ }
32971
+ //#endregion
32847
32972
  //#region src/arkme-emoji-text.ts
32848
32973
  /** Asset-free emoji catalog shared by Browser UI and Host notification text. */
32849
32974
  const arkmeDefaultEmojiSeeds = Object.freeze([
@@ -33498,7 +33623,8 @@ var SourceService = class {
33498
33623
  };
33499
33624
  }
33500
33625
  storeChatSourceByKey(cacheKey, source) {
33501
- this.chatSourceCache.set(cacheKey, cloneSourceItem(this.projectChatSourceAttention(source)));
33626
+ const projected = retainNewerArkmeChatPin(this.chatSourceCache.get(cacheKey), source);
33627
+ this.chatSourceCache.set(cacheKey, cloneSourceItem(this.projectChatSourceAttention(projected)));
33502
33628
  }
33503
33629
  /**
33504
33630
  * Server-owned tray summary. `summary` covers every visible conversation;
@@ -34026,7 +34152,8 @@ var SourceService = class {
34026
34152
  if (existing !== void 0) return cloneSourceList(await existing);
34027
34153
  const pending = this.listSourcesUncached(session, directory, {
34028
34154
  ...options,
34029
- ...cursor === "" ? {} : { cursor }
34155
+ ...cursor === "" ? {} : { cursor },
34156
+ isCurrent: () => this.sourceListInFlight.get(cacheKey) === pending
34030
34157
  }, limit);
34031
34158
  this.sourceListInFlight.set(cacheKey, pending);
34032
34159
  try {
@@ -34048,33 +34175,42 @@ var SourceService = class {
34048
34175
  const session = await this.runtime.requireSession();
34049
34176
  const source = await this.openSourceRef(sourceRef, session.userId);
34050
34177
  if (source.kind !== "private_chat" && source.kind !== "group_chat") throw new ArkmePluginError("chat-directory-policy-invalid", "仅支持更新私聊或群聊的会话列表状态", false);
34051
- const pinTarget = await this.resolveChatPinTarget(source, session, signal);
34178
+ if (signal?.aborted === true) throw new DOMException("The operation was aborted", "AbortError");
34052
34179
  const current = await this.runtime.authenticatedChatPost("/api/v1/chats/policy/get", { chat_session_uid: source.ownerRef }, session, signal);
34053
- const updatedAt = Date.now();
34054
- await Promise.all([this.runtime.authenticatedChatPost("/api/v1/chats/policy/update", {
34180
+ if ([
34181
+ "show_in_home_state",
34182
+ "privacy_state",
34183
+ "mute_state",
34184
+ "pin_state",
34185
+ "notify_state",
34186
+ "status"
34187
+ ].some((field) => !Number.isSafeInteger(current[field]) || Number(current[field]) <= 0)) throw new ArkmePluginError("chat-pin-policy-invalid", "会话设置读取不完整,请重试", true, 502);
34188
+ const updated = await this.runtime.authenticatedChatPost("/api/v1/chats/policy/update", {
34055
34189
  chat_session_uid: source.ownerRef,
34056
- show_in_home_state: numberValue$26(current.show_in_home_state) || 1,
34057
- privacy_state: numberValue$26(current.privacy_state) || 1,
34058
- mute_state: numberValue$26(current.mute_state) || 1,
34190
+ show_in_home_state: current.show_in_home_state,
34191
+ privacy_state: current.privacy_state,
34192
+ mute_state: current.mute_state,
34059
34193
  pin_state: pinned ? 2 : 1,
34060
- notify_state: numberValue$26(current.notify_state) || 1,
34061
- status: numberValue$26(current.status) || 1,
34062
- update_at: updatedAt
34063
- }, session, signal), this.runtime.authenticatedPost("/api/v1/topics/pin/set", {
34064
- topic_uid: pinTarget.subjectUid,
34065
- pin_state: pinned ? 1 : 2,
34066
- ...pinned ? { pinned_at: updatedAt } : {}
34067
- }, session, signal)]);
34194
+ notify_state: current.notify_state,
34195
+ status: current.status,
34196
+ update_at: Date.now()
34197
+ }, session, signal);
34198
+ if (updated.chat_session_uid !== source.ownerRef || updated.pin_state !== 1 && updated.pin_state !== 2 || !Number.isSafeInteger(updated.update_at) || Number(updated.update_at) <= 0) throw new ArkmePluginError("chat-pin-result-invalid", "无法确认置顶结果,请刷新后重试", true, 502);
34199
+ const effectivePinned = updated.pin_state === 2;
34200
+ const policyUpdatedAtMillis = Number(updated.update_at);
34068
34201
  const cacheKey = `${String(session.userId)}:${source.ownerRef}`;
34069
34202
  const cached = this.chatSourceCache.get(cacheKey);
34070
34203
  if (cached !== void 0) this.storeChatSourceByKey(cacheKey, {
34071
34204
  ...cached,
34072
- isPinned: pinned
34205
+ isPinned: effectivePinned,
34206
+ chatPolicyUpdatedAtMillis: policyUpdatedAtMillis
34073
34207
  });
34074
- this.sourceListCache.clear();
34208
+ this.invalidateSourceListCache(session.userId, "root");
34209
+ if (effectivePinned !== pinned) throw new ArkmePluginError("chat-pin-conflict", "会话置顶状态已变化,请刷新后重试", true, 409);
34075
34210
  return {
34076
34211
  sourceRef,
34077
- pinned
34212
+ pinned: effectivePinned,
34213
+ policyUpdatedAtMillis
34078
34214
  };
34079
34215
  }
34080
34216
  async chatConversationListPreferenceEntry(sourceRef, signal) {
@@ -34113,16 +34249,6 @@ var SourceService = class {
34113
34249
  }
34114
34250
  };
34115
34251
  }
34116
- async resolveChatPinTarget(source, session, signal) {
34117
- if (source.sidebarSubjectUid !== void 0 && source.sidebarSubjectUid.trim() !== "") return { subjectUid: source.sidebarSubjectUid.trim() };
34118
- const target = arkmeChatDirectoryMetadataFromBundle(await this.runtime.authenticatedChatPost("/api/v1/chats/detail", { chat_session_uid: source.ownerRef }, session, signal, {
34119
- lane: "interactive-read",
34120
- key: `chat-sidebar-target:${source.ownerRef}`,
34121
- failureCooldownMs: 2e3
34122
- }), source.kind === "group_chat" ? source.ownerRef : "");
34123
- if (target.subjectUid === void 0) throw new ArkmePluginError("chat-sidebar-target-unavailable", "未能定位该会话的跨端侧边栏数据,请刷新后重试", true, 502);
34124
- return { subjectUid: target.subjectUid };
34125
- }
34126
34252
  async listGroupSources(options = {}) {
34127
34253
  const session = await this.runtime.requireSession();
34128
34254
  const limit = Math.min(50, Math.max(1, Math.trunc(options.limit ?? 30)));
@@ -34315,13 +34441,14 @@ var SourceService = class {
34315
34441
  const botGroupTarget = kind === "group_chat" ? arkmeGroupBotBindingTargetFromBundle(bundle) : void 0;
34316
34442
  const cached = this.chatSourceCache.get(`${String(session.userId)}:${uid}`);
34317
34443
  const chatDirectoryMetadata = arkmeChatDirectoryMetadataFromBundle(bundle, kind === "group_chat" ? uid : "");
34318
- const item = {
34444
+ const item = retainNewerArkmeChatPin(cached, {
34319
34445
  sourceRef: await this.sealSourceRef(session.userId, kind, uid, displayName, {
34320
34446
  ...botGroupTarget === void 0 ? {} : { botGroupTarget },
34321
34447
  chatDirectoryMetadata
34322
34448
  }),
34323
34449
  sourceKey: await this.chatDirectorySourceKey(session.userId, uid),
34324
34450
  kind,
34451
+ directMessageAdmissionApplicable: sessionKind === 1 && numberValue$26(counterpart.user_id) > 0,
34325
34452
  displayName,
34326
34453
  ...kind === "private_chat" && cached?.avatarRef !== void 0 ? { avatarRef: cached.avatarRef } : {},
34327
34454
  ...kind === "group_chat" && cached?.avatarRefs !== void 0 ? { avatarRefs: [...cached.avatarRefs] } : {},
@@ -34331,8 +34458,9 @@ var SourceService = class {
34331
34458
  ...attention,
34332
34459
  ...hasUnreadMention === void 0 ? {} : { hasUnreadMention },
34333
34460
  isPinned,
34461
+ chatPolicyUpdatedAtMillis: numberValue$26(currentPolicy.update_at),
34334
34462
  ...numberValue$26(unread.session_last_seq ?? chatSession.last_seq) > 0 ? { latestSequence: numberValue$26(unread.session_last_seq ?? chatSession.last_seq) } : {}
34335
- };
34463
+ });
34336
34464
  const itemIndex = items.push(item) - 1;
34337
34465
  chatSessionUidByIndex.set(itemIndex, uid);
34338
34466
  if (kind === "private_chat") {
@@ -34348,7 +34476,7 @@ var SourceService = class {
34348
34476
  } catch (error) {
34349
34477
  console.warn("dsh-arkme: Chat avatar hydration failed:", safeFailureMessage$6(error));
34350
34478
  }
34351
- for (const [index, uid] of chatSessionUidByIndex) {
34479
+ if (options.isCurrent?.() !== false) for (const [index, uid] of chatSessionUidByIndex) {
34352
34480
  const item = items[index];
34353
34481
  if (item !== void 0) this.storeChatSourceByKey(`${String(session.userId)}:${uid}`, item);
34354
34482
  }
@@ -34365,6 +34493,7 @@ var SourceService = class {
34365
34493
  };
34366
34494
  }
34367
34495
  invalidateSourceListCache(userId, directory) {
34496
+ if (directory === void 0 || directory === "root") this.runtime.invalidateKey(this.runtime.requestScope(userId), "directory:root:");
34368
34497
  const prefix = `${String(userId)}:`;
34369
34498
  const matches = (key) => key.startsWith(prefix) && (directory === void 0 || key.startsWith(`${prefix}${directory}:`));
34370
34499
  for (const key of this.sourceListCache.keys()) if (matches(key)) this.sourceListCache.delete(key);
@@ -34607,13 +34736,14 @@ var SourceService = class {
34607
34736
  const hasUnreadMention = kind !== "group_chat" ? void 0 : unreadCount <= 0 ? false : backendMentionState === void 0 && !latestMentionsViewer ? cached?.hasUnreadMention : backendMentionState === true || latestMentionsViewer;
34608
34737
  const botGroupTarget = kind === "group_chat" ? arkmeGroupBotBindingTargetFromBundle(bundle) : void 0;
34609
34738
  const chatDirectoryMetadata = arkmeChatDirectoryMetadataFromBundle(bundle, kind === "group_chat" ? uid : "");
34610
- return {
34739
+ return retainNewerArkmeChatPin(cached, {
34611
34740
  sourceRef: await this.sealSourceRef(session.userId, kind, uid, displayName, {
34612
34741
  ...botGroupTarget === void 0 ? {} : { botGroupTarget },
34613
34742
  chatDirectoryMetadata
34614
34743
  }),
34615
34744
  sourceKey: await this.chatDirectorySourceKey(session.userId, uid),
34616
34745
  kind,
34746
+ directMessageAdmissionApplicable: sessionKind === 1 && numberValue$26(counterpart.user_id) > 0,
34617
34747
  displayName,
34618
34748
  ...cached?.avatarRef === void 0 ? {} : { avatarRef: cached.avatarRef },
34619
34749
  ...cached?.avatarRefs === void 0 ? {} : { avatarRefs: cached.avatarRefs },
@@ -34623,8 +34753,9 @@ var SourceService = class {
34623
34753
  ...attention,
34624
34754
  ...hasUnreadMention === void 0 ? {} : { hasUnreadMention },
34625
34755
  isPinned,
34756
+ chatPolicyUpdatedAtMillis: numberValue$26(currentPolicy.update_at),
34626
34757
  ...latestSequence > 0 ? { latestSequence } : {}
34627
- };
34758
+ });
34628
34759
  }
34629
34760
  encodeCursor(value) {
34630
34761
  return `arkme-cursor-v1.${encodeOpaqueJson$7(value)}`;
@@ -36618,6 +36749,41 @@ function decodeArkmeChatReadCursorAdvancedDataLine(line) {
36618
36749
  eventAtMillis
36619
36750
  };
36620
36751
  }
36752
+ const POLICY_UPDATED_FORBIDDEN_FIELDS = [
36753
+ "rel_uid",
36754
+ "latest_seq",
36755
+ "sender_user_id",
36756
+ "read_seq",
36757
+ "read_at",
36758
+ "unread_count",
36759
+ "record",
36760
+ "record_uid",
36761
+ "record_body",
36762
+ "text_content",
36763
+ "payload",
36764
+ "receiver_user_ids"
36765
+ ];
36766
+ /** Policy events invalidate a directory projection; Chat list remains the source of truth. */
36767
+ function decodeArkmeChatPolicyUpdatedDataLine(line) {
36768
+ const source = decodeDataLine(line);
36769
+ if (source === void 0 || positiveInteger(source.t) !== 19 || POLICY_UPDATED_FORBIDDEN_FIELDS.some((field) => Object.hasOwn(source, field))) return void 0;
36770
+ const eventUid = nonEmptyString(source.event_uid);
36771
+ const chatSessionUid = nonEmptyString(source.chat_session_uid);
36772
+ const userId = positiveInteger(source.user_id);
36773
+ const pinState = positiveInteger(source.pin_state);
36774
+ const policyUpdateAtMillis = positiveInteger(source.policy_update_at);
36775
+ const eventAtMillis = positiveInteger(source.event_at);
36776
+ const sourceClientId = source.source_client_id === void 0 ? 0 : nonNegativeInteger(source.source_client_id);
36777
+ if (eventUid === void 0 || chatSessionUid === void 0 || userId === void 0 || pinState !== 1 && pinState !== 2 || policyUpdateAtMillis === void 0 || eventAtMillis === void 0 || sourceClientId === void 0) return void 0;
36778
+ return {
36779
+ eventUid,
36780
+ chatSessionUid,
36781
+ userId,
36782
+ pinState,
36783
+ policyUpdateAtMillis,
36784
+ eventAtMillis
36785
+ };
36786
+ }
36621
36787
  const TIMELINE_CHANGED_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
36622
36788
  "t",
36623
36789
  "event_uid",
@@ -37075,6 +37241,8 @@ var ArkmeChatRealtimeRuntime = class {
37075
37241
  }
37076
37242
  acceptLine(line, connectionUserId, connectionSignal, connectionStartedAtMillis) {
37077
37243
  if (connectionSignal.aborted) return;
37244
+ const policyUpdated = decodeArkmeChatPolicyUpdatedDataLine(line);
37245
+ if (policyUpdated !== void 0 && policyUpdated.userId !== connectionUserId) return;
37078
37246
  const memberEvent = decodeArkmeMemberEventDataLine(line);
37079
37247
  const conversationListPreferenceUpdated = decodeArkmeConversationListPreferenceUpdatedDataLine(line);
37080
37248
  const projectionInvalidation = conversationListPreferenceUpdated === void 0 ? decodeArkmeProjectionInvalidatedDataLine(line) : void 0;
@@ -37082,15 +37250,20 @@ var ArkmeChatRealtimeRuntime = class {
37082
37250
  const timelineChanged = conversationListPreferenceUpdated === void 0 && projectionInvalidation === void 0 && readCursorAdvanced === void 0 ? decodeArkmeChatTimelineChangedDataLine(line) : void 0;
37083
37251
  const messagePreparing = decodeArkmeChatMessagePreparingDataLine(line);
37084
37252
  const hint = conversationListPreferenceUpdated === void 0 && projectionInvalidation === void 0 && readCursorAdvanced === void 0 && timelineChanged === void 0 ? decodeArkmeChatReceiveDataLine(line) : void 0;
37085
- const eventUid = memberEvent?.eventUid ?? conversationListPreferenceUpdated?.eventUid ?? projectionInvalidation?.eventUid ?? readCursorAdvanced?.eventUid ?? timelineChanged?.eventUid ?? messagePreparing?.eventUid ?? hint?.eventUid;
37253
+ const eventUid = policyUpdated?.eventUid ?? memberEvent?.eventUid ?? conversationListPreferenceUpdated?.eventUid ?? projectionInvalidation?.eventUid ?? readCursorAdvanced?.eventUid ?? timelineChanged?.eventUid ?? messagePreparing?.eventUid ?? hint?.eventUid;
37086
37254
  if (eventUid === void 0 || this.seenEventUids.has(eventUid)) return;
37087
37255
  this.seenEventUids.add(eventUid);
37088
37256
  if (this.seenEventUids.size > MAX_SEEN_EVENTS) {
37089
37257
  const oldest = this.seenEventUids.values().next().value;
37090
37258
  if (oldest !== void 0) this.seenEventUids.delete(oldest);
37091
37259
  }
37092
- this.lastEventAtMillis = memberEvent?.eventAtMillis ?? conversationListPreferenceUpdated?.acceptedAtMillis ?? projectionInvalidation?.eventAtMillis ?? readCursorAdvanced?.eventAtMillis ?? timelineChanged?.eventAtMillis ?? messagePreparing?.eventAtMillis ?? hint?.eventAtMillis;
37093
- if (memberEvent !== void 0) this.advanceRevision("chat-hint", { memberEvent });
37260
+ this.lastEventAtMillis = policyUpdated?.eventAtMillis ?? memberEvent?.eventAtMillis ?? conversationListPreferenceUpdated?.acceptedAtMillis ?? projectionInvalidation?.eventAtMillis ?? readCursorAdvanced?.eventAtMillis ?? timelineChanged?.eventAtMillis ?? messagePreparing?.eventAtMillis ?? hint?.eventAtMillis;
37261
+ if (policyUpdated !== void 0) this.advanceRevision("chat-policy-invalidation", {
37262
+ policyUpdated,
37263
+ connectionUserId,
37264
+ connectionSignal
37265
+ });
37266
+ else if (memberEvent !== void 0) this.advanceRevision("chat-hint", { memberEvent });
37094
37267
  else if (conversationListPreferenceUpdated !== void 0) this.advanceRevision("conversation-list-preference-invalidation", { conversationListPreferenceUpdated });
37095
37268
  else if (projectionInvalidation !== void 0) this.advanceRevision("projection-invalidation", { projectionInvalidation });
37096
37269
  else if (timelineChanged !== void 0) this.advanceRevision("chat-hint", {
@@ -37114,7 +37287,7 @@ var ArkmeChatRealtimeRuntime = class {
37114
37287
  });
37115
37288
  }
37116
37289
  advanceRevision(cause, evidence = {}) {
37117
- const { hint, readCursorAdvanced, projectionInvalidation, timelineChanged, messagePreparing, connectionUserId, connectionSignal, connectionStartedAtMillis, conversationListPreferenceUpdated, memberEvent } = evidence;
37290
+ const { hint, readCursorAdvanced, policyUpdated, projectionInvalidation, timelineChanged, messagePreparing, connectionUserId, connectionSignal, connectionStartedAtMillis, conversationListPreferenceUpdated, memberEvent } = evidence;
37118
37291
  this.revision += 1;
37119
37292
  const notice = {
37120
37293
  state: this.state(),
@@ -37125,6 +37298,7 @@ var ArkmeChatRealtimeRuntime = class {
37125
37298
  ...connectionUserId === void 0 ? {} : { connectionUserId },
37126
37299
  ...connectionSignal === void 0 ? {} : { connectionSignal },
37127
37300
  ...connectionStartedAtMillis === void 0 ? {} : { connectionStartedAtMillis },
37301
+ ...policyUpdated === void 0 ? {} : { policyUpdated },
37128
37302
  ...hint === void 0 ? {} : { hint },
37129
37303
  ...readCursorAdvanced === void 0 ? {} : { readCursorAdvanced },
37130
37304
  ...projectionInvalidation === void 0 ? {} : { projectionInvalidation },
@@ -37199,8 +37373,8 @@ var ChatRealtimeService = class {
37199
37373
  notificationBaselineUserId;
37200
37374
  notificationBaselineOwnerGeneration = 0;
37201
37375
  notificationBaselineSequences = /* @__PURE__ */ new Map();
37202
- notificationBaselineRetryTimer;
37203
- notificationBaselineRetryCount = 0;
37376
+ connectionBaselineRetryTimer;
37377
+ connectionBaselineRetryCount = 0;
37204
37378
  chatClientRevision = 0;
37205
37379
  attentionSummaryVersion = 0;
37206
37380
  attentionSummaryFingerprint = "";
@@ -37243,11 +37417,11 @@ var ChatRealtimeService = class {
37243
37417
  dispose() {
37244
37418
  this.disposed = true;
37245
37419
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
37246
- if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
37420
+ if (this.connectionBaselineRetryTimer !== void 0) clearTimeout(this.connectionBaselineRetryTimer);
37247
37421
  if (this.attentionRetryTimer !== void 0) clearTimeout(this.attentionRetryTimer);
37248
37422
  this.projectionTimer = void 0;
37249
37423
  this.projectionTimerDueAtMillis = 0;
37250
- this.notificationBaselineRetryTimer = void 0;
37424
+ this.connectionBaselineRetryTimer = void 0;
37251
37425
  this.attentionRetryTimer = void 0;
37252
37426
  this.pendingChatProjections.clear();
37253
37427
  this.projectionRetryCounts.clear();
@@ -37267,11 +37441,11 @@ var ChatRealtimeService = class {
37267
37441
  unsubscribe();
37268
37442
  stop();
37269
37443
  if (this.projectionTimer !== void 0) clearTimeout(this.projectionTimer);
37270
- if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
37444
+ if (this.connectionBaselineRetryTimer !== void 0) clearTimeout(this.connectionBaselineRetryTimer);
37271
37445
  if (this.attentionRetryTimer !== void 0) clearTimeout(this.attentionRetryTimer);
37272
37446
  this.projectionTimer = void 0;
37273
37447
  this.projectionTimerDueAtMillis = 0;
37274
- this.notificationBaselineRetryTimer = void 0;
37448
+ this.connectionBaselineRetryTimer = void 0;
37275
37449
  this.attentionRetryTimer = void 0;
37276
37450
  this.pendingChatProjections.clear();
37277
37451
  this.projectionRetryCounts.clear();
@@ -37302,6 +37476,10 @@ var ChatRealtimeService = class {
37302
37476
  };
37303
37477
  }
37304
37478
  handleChatRealtimeNotice(notice) {
37479
+ if (notice.cause === "chat-policy-invalidation" && notice.policyUpdated !== void 0) {
37480
+ this.invalidateChatPolicyForCurrentSession(notice);
37481
+ return;
37482
+ }
37305
37483
  if (notice.cause === "chat-hint" && notice.memberEvent !== void 0) {
37306
37484
  this.handleMemberEvent(notice.memberEvent);
37307
37485
  return;
@@ -37313,9 +37491,9 @@ var ChatRealtimeService = class {
37313
37491
  this.notificationBaselineUserId = void 0;
37314
37492
  this.notificationBaselineOwnerGeneration = 0;
37315
37493
  this.notificationBaselineSequences.clear();
37316
- this.notificationBaselineRetryCount = 0;
37317
- if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
37318
- this.notificationBaselineRetryTimer = void 0;
37494
+ this.connectionBaselineRetryCount = 0;
37495
+ if (this.connectionBaselineRetryTimer !== void 0) clearTimeout(this.connectionBaselineRetryTimer);
37496
+ this.connectionBaselineRetryTimer = void 0;
37319
37497
  this.emitChatClientEvent({
37320
37498
  type: "reconcile",
37321
37499
  revision: this.nextChatClientRevision(),
@@ -37323,11 +37501,19 @@ var ChatRealtimeService = class {
37323
37501
  connectionGeneration: generation,
37324
37502
  refresh: "none"
37325
37503
  });
37326
- this.reconcileChatNotificationBaseline(generation, notice.connectionUserId);
37504
+ this.reconcileChatConnectionBaseline(generation, notice.connectionUserId);
37327
37505
  this.refreshAttentionSummary();
37328
37506
  this.invalidateRecordProjection();
37329
37507
  return;
37330
37508
  }
37509
+ if (notice.cause === "projection-invalidation" && notice.projectionInvalidation?.projection === "chat.direct_message_admission") {
37510
+ this.emitChatClientEvent({
37511
+ type: "projection-invalidated",
37512
+ projection: "chat.direct_message_admission",
37513
+ revision: this.nextChatClientRevision()
37514
+ });
37515
+ return;
37516
+ }
37331
37517
  if (notice.cause === "projection-invalidation" && notice.projectionInvalidation?.projection === "record") {
37332
37518
  this.invalidateRecordProjection();
37333
37519
  return;
@@ -37619,9 +37805,9 @@ var ChatRealtimeService = class {
37619
37805
  this.notificationBaselineGeneration = 0;
37620
37806
  this.notificationBaselineUserId = void 0;
37621
37807
  this.notificationBaselineOwnerGeneration = 0;
37622
- this.notificationBaselineRetryCount = 0;
37623
- if (this.notificationBaselineRetryTimer !== void 0) clearTimeout(this.notificationBaselineRetryTimer);
37624
- this.notificationBaselineRetryTimer = void 0;
37808
+ this.connectionBaselineRetryCount = 0;
37809
+ if (this.connectionBaselineRetryTimer !== void 0) clearTimeout(this.connectionBaselineRetryTimer);
37810
+ this.connectionBaselineRetryTimer = void 0;
37625
37811
  const resetVersion = Math.max(Date.now(), this.attentionSummaryVersion + 1);
37626
37812
  this.emitChatClientEvent({
37627
37813
  type: "attention-summary",
@@ -37639,6 +37825,21 @@ var ChatRealtimeService = class {
37639
37825
  this.attentionSummaryFingerprint = "";
37640
37826
  this.latestAttentionSummary = void 0;
37641
37827
  }
37828
+ async invalidateChatPolicyForCurrentSession(notice) {
37829
+ const hint = notice.policyUpdated;
37830
+ if (this.disposed || hint === void 0 || notice.connectionSignal?.aborted || notice.connectionUserId !== hint.userId) return;
37831
+ try {
37832
+ const session = await this.runtime.sessionStore.read();
37833
+ if (this.disposed || notice.connectionSignal?.aborted || session?.userId !== hint.userId) return;
37834
+ this.source.invalidateSourceListCache(session.userId, "root");
37835
+ this.emitChatClientEvent({
37836
+ type: "chat-policy-invalidated",
37837
+ revision: this.nextChatClientRevision()
37838
+ });
37839
+ } catch (error) {
37840
+ console.warn("dsh-arkme: Chat policy invalidation failed:", safeFailureMessage$5(error));
37841
+ }
37842
+ }
37642
37843
  /** Browser invalidation only; raw source/Bot caches belong to different projections. */
37643
37844
  async invalidateConversationListPreferenceForCurrentSession(expectedUserId) {
37644
37845
  try {
@@ -37652,15 +37853,20 @@ var ChatRealtimeService = class {
37652
37853
  console.warn("dsh-arkme: Conversation-list preference invalidation failed:", safeFailureMessage$5(error));
37653
37854
  }
37654
37855
  }
37655
- async reconcileChatNotificationBaseline(connectionGeneration, expectedUserId) {
37856
+ async reconcileChatConnectionBaseline(connectionGeneration, expectedUserId) {
37857
+ if (this.disposed) return;
37656
37858
  try {
37657
37859
  const session = await this.runtime.requireSession();
37658
- if (expectedUserId !== void 0 && session.userId !== expectedUserId) return;
37860
+ if (this.disposed || expectedUserId !== void 0 && session.userId !== expectedUserId) return;
37861
+ const initialState = this.chatRealtime.state();
37862
+ if (!initialState.connected || initialState.connectionGeneration !== connectionGeneration) return;
37659
37863
  expectedUserId = session.userId;
37660
37864
  this.activateAttentionOwner(session.userId);
37661
37865
  const ownerGeneration = this.attentionOwnerGeneration;
37662
37866
  this.notificationBaselineSequences.clear();
37663
37867
  const sequences = /* @__PURE__ */ new Map();
37868
+ const pins = [];
37869
+ this.source.invalidateSourceListCache(session.userId, "root");
37664
37870
  let cursor;
37665
37871
  for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
37666
37872
  const page = await this.source.listSources("root", {
@@ -37668,37 +37874,51 @@ var ChatRealtimeService = class {
37668
37874
  refresh: true,
37669
37875
  ...cursor === void 0 ? {} : { cursor }
37670
37876
  });
37877
+ const activeSession = await this.runtime.sessionStore.read();
37878
+ const state = this.chatRealtime.state();
37879
+ if (this.disposed || !state.connected || state.connectionGeneration !== connectionGeneration || activeSession?.userId !== session.userId || ownerGeneration !== this.attentionOwnerGeneration) return;
37671
37880
  for (const item of page.items) {
37672
37881
  if (item.kind !== "private_chat" && item.kind !== "group_chat") continue;
37673
37882
  const source = await this.source.openSourceRef(item.sourceRef, session.userId);
37674
37883
  sequences.set(source.ownerRef, item.latestSequence ?? 0);
37884
+ if (item.sourceKey !== void 0 && item.isPinned !== void 0 && item.chatPolicyUpdatedAtMillis !== void 0) pins.push({
37885
+ sourceKey: item.sourceKey,
37886
+ pinned: item.isPinned,
37887
+ policyUpdatedAtMillis: item.chatPolicyUpdatedAtMillis
37888
+ });
37675
37889
  }
37676
37890
  if (!page.hasMore || page.nextCursor === void 0) break;
37677
37891
  cursor = page.nextCursor;
37678
37892
  }
37679
- const [state, activeSession] = await Promise.all([Promise.resolve(this.chatRealtime.state()), this.runtime.sessionStore.read()]);
37680
- if (!state.connected || state.connectionGeneration !== connectionGeneration || activeSession?.userId !== session.userId || ownerGeneration !== this.attentionOwnerGeneration) return;
37893
+ const activeSession = await this.runtime.sessionStore.read();
37894
+ const state = this.chatRealtime.state();
37895
+ if (this.disposed || !state.connected || state.connectionGeneration !== connectionGeneration || activeSession?.userId !== session.userId || ownerGeneration !== this.attentionOwnerGeneration) return;
37681
37896
  this.notificationBaselineSequences.clear();
37682
37897
  for (const [uid, sequence] of sequences) this.notificationBaselineSequences.set(uid, sequence);
37683
37898
  this.notificationBaselineGeneration = connectionGeneration;
37684
37899
  this.notificationBaselineUserId = session.userId;
37685
37900
  this.notificationBaselineOwnerGeneration = ownerGeneration;
37686
- this.notificationBaselineRetryCount = 0;
37901
+ this.connectionBaselineRetryCount = 0;
37902
+ if (pins.length > 0) this.emitChatClientEvent({
37903
+ type: "chat-pins-reconciled",
37904
+ revision: this.nextChatClientRevision(),
37905
+ pins
37906
+ });
37687
37907
  console.info("dsh-arkme: reconcile_completed", {
37688
37908
  connectionGeneration,
37689
37909
  sessionCount: sequences.size
37690
37910
  });
37691
37911
  if (this.pendingChatProjections.size > 0) this.scheduleProjectionFlush(0);
37692
37912
  } catch (error) {
37693
- const state = this.chatRealtime.state();
37694
37913
  const activeSession = await this.runtime.sessionStore.read().catch(() => void 0);
37695
- if (!state.connected || state.connectionGeneration !== connectionGeneration || expectedUserId === void 0 || activeSession?.userId !== expectedUserId) return;
37696
- this.notificationBaselineRetryCount += 1;
37697
- const delay = Math.min(15e3, 1e3 * 2 ** Math.min(4, this.notificationBaselineRetryCount - 1));
37914
+ const state = this.chatRealtime.state();
37915
+ if (this.disposed || !state.connected || state.connectionGeneration !== connectionGeneration || expectedUserId === void 0 || activeSession?.userId !== expectedUserId) return;
37916
+ this.connectionBaselineRetryCount += 1;
37917
+ const delay = Math.min(15e3, 1e3 * 2 ** Math.min(4, this.connectionBaselineRetryCount - 1));
37698
37918
  console.warn("dsh-arkme: Chat reconnect reconciliation failed:", safeFailureMessage$5(error));
37699
- this.notificationBaselineRetryTimer = setTimeout(() => {
37700
- this.notificationBaselineRetryTimer = void 0;
37701
- this.reconcileChatNotificationBaseline(connectionGeneration, expectedUserId);
37919
+ this.connectionBaselineRetryTimer = setTimeout(() => {
37920
+ this.connectionBaselineRetryTimer = void 0;
37921
+ this.reconcileChatConnectionBaseline(connectionGeneration, expectedUserId);
37702
37922
  }, delay);
37703
37923
  }
37704
37924
  }
@@ -38350,6 +38570,286 @@ const ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES = 10 * 1024 * 1024;
38350
38570
  const ARKME_WORLD_PUBLISH_MAX_IMAGES = 27;
38351
38571
  const ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
38352
38572
  //#endregion
38573
+ //#region src/record-reedit-contract.ts
38574
+ var ArkmeRecordReeditDraftConflict = class extends Error {};
38575
+ /** Only identities are persisted, never browser Files, URLs or host paths. */
38576
+ function parseArkmeRecordReeditAttachments(value) {
38577
+ if (!Array.isArray(value) || value.length > 9) throw new TypeError("请选择至多 9 个附件");
38578
+ const seen = /* @__PURE__ */ new Set();
38579
+ return value.map((raw) => {
38580
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new TypeError("附件引用无效");
38581
+ const item = raw;
38582
+ const asset = item.fileAssetUid;
38583
+ const file = item.fileRef;
38584
+ if (asset === void 0 === (file === void 0) || Object.keys(item).some((key) => key !== "fileAssetUid" && key !== "fileRef")) throw new TypeError("附件必须指定一个资产或本地文件引用");
38585
+ const id = asset ?? file;
38586
+ if (typeof id !== "string" || id.trim() === "" || id !== id.trim() || id.length > 200 || file !== void 0 && !/^arkme-file-v1\.[0-9a-f-]{36}$/.test(id)) throw new TypeError("附件引用无效");
38587
+ const key = `${asset === void 0 ? "file" : "asset"}:${id}`;
38588
+ if (seen.has(key)) throw new TypeError("附件不可重复");
38589
+ seen.add(key);
38590
+ return asset === void 0 ? { fileRef: id } : { fileAssetUid: id };
38591
+ });
38592
+ }
38593
+ //#endregion
38594
+ //#region src/services/record-reedit-submissions.ts
38595
+ function recordReeditSubmissionView(job) {
38596
+ const { submissionId, state, attachments, voiceFileAssetUid, voiceBlock, result, error } = job;
38597
+ const { itemUid, baseVersion } = job.context;
38598
+ const { title, textContent } = job.draft;
38599
+ return structuredClone({
38600
+ submissionId,
38601
+ baseVersion,
38602
+ itemUid,
38603
+ state,
38604
+ title,
38605
+ textContent,
38606
+ attachments,
38607
+ ...voiceFileAssetUid ? { voiceFileAssetUid } : {},
38608
+ ...voiceBlock ? { voiceBlock } : {},
38609
+ ...result ? { result } : {},
38610
+ ...error ? { error } : {}
38611
+ });
38612
+ }
38613
+ /** Owns only delivery of an existing Record edit, not new-message outbox semantics. */
38614
+ var RecordReeditSubmissions = class {
38615
+ ports;
38616
+ running = /* @__PURE__ */ new Map();
38617
+ knownCompletions = /* @__PURE__ */ new Map();
38618
+ closed = false;
38619
+ constructor(ports) {
38620
+ this.ports = ports;
38621
+ }
38622
+ dispose() {
38623
+ this.closed = true;
38624
+ }
38625
+ async accept(candidate) {
38626
+ if (this.closed) throw new ArkmePluginError("record-reedit-unavailable", "插件正在重启,请稍后重试", false, 409);
38627
+ const userId = candidate.context.expectedUserId;
38628
+ const previous = (await this.ports.list(userId)).find((job) => job.context.sourceIdentityKey === candidate.context.sourceIdentityKey && job.context.itemUid === candidate.context.itemUid);
38629
+ if (previous && [
38630
+ "pending",
38631
+ "committing",
38632
+ "uncertain"
38633
+ ].includes(previous.state)) {
38634
+ if (previous.draft.draftRevision === candidate.draft.draftRevision) return recordReeditSubmissionView(previous);
38635
+ throw new ArkmePluginError("record-reedit-in-progress", "这条快记仍在保存或核对,请稍后再编辑;其他消息不受影响", false, 409);
38636
+ }
38637
+ const job = {
38638
+ ...structuredClone(candidate),
38639
+ submissionId: randomUUID(),
38640
+ state: "pending"
38641
+ };
38642
+ await this.ports.put(userId, job, previous?.submissionId);
38643
+ const receipt = recordReeditSubmissionView(job);
38644
+ this.start(job, false);
38645
+ return receipt;
38646
+ }
38647
+ async list(userId, sourceIdentityKey) {
38648
+ return (await this.ports.list(userId)).filter((job) => job.context.sourceIdentityKey === sourceIdentityKey).map((job) => recordReeditSubmissionView(this.knownCompletions.get(job.submissionId) ?? job));
38649
+ }
38650
+ async settleKnownCompletions(userId, sourceIdentityKey, itemUid) {
38651
+ for (const job of this.knownCompletions.values()) {
38652
+ if (job.context.expectedUserId !== userId || job.context.sourceIdentityKey !== sourceIdentityKey || itemUid !== void 0 && job.context.itemUid !== itemUid) continue;
38653
+ try {
38654
+ await this.persistKnownCompletion(job);
38655
+ } catch (error) {
38656
+ if (itemUid !== void 0) throw error;
38657
+ }
38658
+ }
38659
+ }
38660
+ /** Tool waits for the remote result, but shares UI delivery's write-ahead evidence. */
38661
+ async writeConfirmed(candidate, fingerprint, write) {
38662
+ if (this.closed) throw new ArkmePluginError("record-reedit-unavailable", "插件正在重启,请稍后重试", false, 409);
38663
+ const userId = candidate.context.expectedUserId;
38664
+ const previous = (await this.ports.list(userId)).find((job) => job.context.sourceIdentityKey === candidate.context.sourceIdentityKey && job.context.itemUid === candidate.context.itemUid);
38665
+ if (previous && [
38666
+ "pending",
38667
+ "committing",
38668
+ "uncertain"
38669
+ ].includes(previous.state)) throw new ArkmePluginError("record-reedit-in-progress", "这条快记仍在保存或核对,请稍后再编辑", false, 409);
38670
+ const job = {
38671
+ ...structuredClone(candidate),
38672
+ submissionId: randomUUID(),
38673
+ state: "committing",
38674
+ expectedCommittedFingerprint: fingerprint
38675
+ };
38676
+ const work = (async () => {
38677
+ await this.ports.put(userId, job, previous?.submissionId);
38678
+ if (this.closed) throw new ArkmePluginError("record-reedit-unavailable", "插件已停止,提交等待核对", false, 409);
38679
+ const outcome = await write();
38680
+ await this.finish(job, outcome);
38681
+ return outcome;
38682
+ })();
38683
+ this.running.set(job.submissionId, work.then(() => void 0, () => void 0));
38684
+ try {
38685
+ return await work;
38686
+ } finally {
38687
+ this.running.delete(job.submissionId);
38688
+ }
38689
+ }
38690
+ async resume(userId, sourceIdentityKey, reconcile = false, sourceRef) {
38691
+ await this.settleKnownCompletions(userId, sourceIdentityKey);
38692
+ const jobs = (await this.ports.list(userId)).filter((job) => job.context.sourceIdentityKey === sourceIdentityKey);
38693
+ for (const job of jobs) {
38694
+ if (this.running.has(job.submissionId) || this.knownCompletions.has(job.submissionId)) continue;
38695
+ if (sourceRef) job.context.sourceRef = sourceRef;
38696
+ if (job.state === "pending") this.start(job, false);
38697
+ else if (job.state === "committing" || job.state === "uncertain" && reconcile) this.start(job, true);
38698
+ }
38699
+ }
38700
+ start(job, reconcile) {
38701
+ if (this.closed || this.running.has(job.submissionId)) return;
38702
+ const work = this.run(structuredClone(job), reconcile).catch(() => void 0).finally(() => this.running.delete(job.submissionId));
38703
+ this.running.set(job.submissionId, work);
38704
+ }
38705
+ async run(initial, reconcile) {
38706
+ let job = initial;
38707
+ const { context, draft, attachments, voiceFileAssetUid, voiceBlock, submissionId } = initial;
38708
+ const candidate = {
38709
+ context,
38710
+ draft,
38711
+ attachments,
38712
+ submissionId,
38713
+ ...voiceFileAssetUid ? { voiceFileAssetUid } : {},
38714
+ ...voiceBlock ? { voiceBlock } : {}
38715
+ };
38716
+ const save = () => this.ports.put(job.context.expectedUserId, job, job.submissionId);
38717
+ const outcome = reconcile ? await this.ports.reconcile(job) : await this.ports.commit(job, async (fingerprint) => {
38718
+ if (this.closed) throw new ArkmePluginError("record-reedit-unavailable", "插件已停止,提交等待恢复", false, 409);
38719
+ job = {
38720
+ ...candidate,
38721
+ state: "committing",
38722
+ expectedCommittedFingerprint: fingerprint
38723
+ };
38724
+ await save();
38725
+ if (this.closed) throw new ArkmePluginError("record-reedit-unavailable", "插件已停止,提交等待核对", false, 409);
38726
+ });
38727
+ await this.finish(job, outcome);
38728
+ }
38729
+ async finish(initial, outcome) {
38730
+ const { context, draft, attachments, voiceFileAssetUid, voiceBlock, submissionId } = initial;
38731
+ const candidate = {
38732
+ context,
38733
+ draft,
38734
+ attachments,
38735
+ submissionId,
38736
+ ...voiceFileAssetUid ? { voiceFileAssetUid } : {},
38737
+ ...voiceBlock ? { voiceBlock } : {}
38738
+ };
38739
+ const checkpoint = initial.expectedCommittedFingerprint;
38740
+ let job;
38741
+ if (outcome.kind === "committed") {
38742
+ job = {
38743
+ ...candidate,
38744
+ state: "committed",
38745
+ result: outcome.result,
38746
+ ...checkpoint ? { expectedCommittedFingerprint: checkpoint } : {}
38747
+ };
38748
+ this.knownCompletions.set(job.submissionId, job);
38749
+ } else if (outcome.kind === "uncertain") {
38750
+ if (!checkpoint) throw new Error("Unknown Record edit outcome requires a persisted write checkpoint");
38751
+ job = {
38752
+ ...candidate,
38753
+ state: "uncertain",
38754
+ expectedCommittedFingerprint: checkpoint,
38755
+ error: outcome.message
38756
+ };
38757
+ } else job = {
38758
+ ...candidate,
38759
+ state: "failed",
38760
+ error: outcome.message,
38761
+ ...checkpoint ? { expectedCommittedFingerprint: checkpoint } : {}
38762
+ };
38763
+ if (this.closed) return;
38764
+ if (job.state === "committed") {
38765
+ await this.persistKnownCompletion(job).catch(() => void 0);
38766
+ return;
38767
+ }
38768
+ await this.ports.put(job.context.expectedUserId, job, job.submissionId);
38769
+ }
38770
+ async persistKnownCompletion(job) {
38771
+ if (this.closed) return;
38772
+ await this.ports.put(job.context.expectedUserId, job, job.submissionId);
38773
+ if (this.closed || this.knownCompletions.get(job.submissionId) !== job) return;
38774
+ this.knownCompletions.delete(job.submissionId);
38775
+ await this.ports.committed?.(job.context.expectedUserId).catch(() => void 0);
38776
+ }
38777
+ };
38778
+ //#endregion
38779
+ //#region src/services/record-reedit-attachments.ts
38780
+ const invalid$1 = () => new ArkmePluginError("record-reedit-attachment-invalid", "附件引用无效或不属于这条快记,请重新读取后选择", false, 409);
38781
+ const refs = (payload) => Array.isArray(payload?.media_refs) ? payload.media_refs.map(objectValue$2) : [];
38782
+ /** A Live Photo is one editable item with its original companion resources. */
38783
+ function recordReeditMediaGroups(payload) {
38784
+ const media = refs(payload).sort((a, b) => Number(a.sort_order ?? 0) - Number(b.sort_order ?? 0));
38785
+ return media.filter((ref) => ref.render_role !== 4 && ref.render_role !== 2).map((primary) => {
38786
+ const logicalUid = stringValue$2(objectValue$2(primary.dynamic_photo).logical_uid);
38787
+ return {
38788
+ fileAssetUid: stringValue$2(primary.file_asset_uid),
38789
+ primary,
38790
+ refs: [primary, ...media.filter((ref) => ref !== primary && ref.render_role === 4 && logicalUid !== "" && stringValue$2(objectValue$2(ref.dynamic_photo).logical_uid) === logicalUid)]
38791
+ };
38792
+ });
38793
+ }
38794
+ function recordReeditAttachmentSelection(value, payload) {
38795
+ let selection;
38796
+ try {
38797
+ selection = parseArkmeRecordReeditAttachments(value);
38798
+ } catch {
38799
+ throw invalid$1();
38800
+ }
38801
+ const groups = recordReeditMediaGroups(payload);
38802
+ const supported = new Set(groups.map((group) => group.fileAssetUid));
38803
+ if (selection.some((item) => item.fileAssetUid !== void 0 && !supported.has(item.fileAssetUid))) throw invalid$1();
38804
+ const grouped = new Set(groups.flatMap((group) => group.refs));
38805
+ if (refs(payload).some((ref) => ref.render_role === 4 && !groups.some((group) => group.refs.some((item) => item.file_asset_uid === ref.file_asset_uid)))) throw invalid$1();
38806
+ if (groups.some((group) => group.fileAssetUid === "") || grouped.size !== groups.reduce((sum, group) => sum + group.refs.length, 0)) throw invalid$1();
38807
+ return selection;
38808
+ }
38809
+ function recordReeditAttachmentChanges(payload, selection) {
38810
+ const old = recordReeditMediaGroups(payload).map((group) => group.fileAssetUid);
38811
+ const kept = selection.flatMap((item) => item.fileAssetUid === void 0 ? [] : [item.fileAssetUid]);
38812
+ return {
38813
+ added: selection.length - kept.length,
38814
+ removed: old.filter((uid) => !kept.includes(uid)).length,
38815
+ retained: kept.length,
38816
+ reordered: old.filter((uid) => kept.includes(uid)).join("\0") !== kept.join("\0")
38817
+ };
38818
+ }
38819
+ function recordReeditWithAttachments(payload, selection, uploads) {
38820
+ const groups = recordReeditMediaGroups(payload);
38821
+ const output = structuredClone(payload ?? {
38822
+ schema_version: 1,
38823
+ text_state: 1
38824
+ });
38825
+ const media = selection.flatMap((item) => {
38826
+ if (item.fileAssetUid !== void 0) {
38827
+ const group = groups.find((value) => value.fileAssetUid === item.fileAssetUid);
38828
+ if (group === void 0) throw invalid$1();
38829
+ return group.refs.map((ref) => structuredClone(ref));
38830
+ }
38831
+ const asset = uploads.get(item.fileRef);
38832
+ if (asset === void 0 || asset.fileAssetUid.trim() === "") throw invalid$1();
38833
+ return [{
38834
+ file_asset_uid: asset.fileAssetUid,
38835
+ render_role: 1,
38836
+ file_name: asset.fileName
38837
+ }];
38838
+ });
38839
+ media.push(...refs(payload).filter((ref) => ref.render_role === 2).map((ref) => structuredClone(ref)));
38840
+ if (new Set(media.map((ref) => ref.file_asset_uid)).size !== media.length) throw new ArkmePluginError("record-reedit-attachment-invalid", "部分附件内容重复,请保留一份后重试;草稿已保留", false, 409);
38841
+ output.media_refs = media.map((ref, sortOrder) => ({
38842
+ ...ref,
38843
+ sort_order: sortOrder
38844
+ }));
38845
+ const templateKind = stringValue$2(objectValue$2(output.voice).source_file_asset_uid) !== "" ? media.length > 0 ? 4 : 3 : media.length > 0 ? 2 : 1;
38846
+ output.payload_kind = templateKind;
38847
+ return {
38848
+ templateKind,
38849
+ contentPayload: output
38850
+ };
38851
+ }
38852
+ //#endregion
38353
38853
  //#region src/services/record-service.ts
38354
38854
  function numberValue$20(value) {
38355
38855
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -38415,6 +38915,16 @@ function recordReeditDynamicPhoto(raw) {
38415
38915
  function recordReeditReadContentPayload(raw) {
38416
38916
  const source = objectValue$2(raw);
38417
38917
  if (Object.keys(source).length === 0) return void 0;
38918
+ if ([
38919
+ "forward_records",
38920
+ "shared_recording",
38921
+ "structured_anchor",
38922
+ "call_record"
38923
+ ].some((key) => Object.keys(objectValue$2(source[key])).length > 0)) throw new ArkmePluginError("record-reedit-shape-unsupported", "卡片类内容暂不支持普通重新编辑,原内容和草稿均保留", false, 409);
38924
+ if (source.background_sound_amplitudes !== void 0 || listValue$17(source.media_refs).some((rawRef) => {
38925
+ const ref = objectValue$2(rawRef);
38926
+ return numberValue$20(ref.content_file_role) === 4 || numberValue$20(ref.binding_type) === 4;
38927
+ })) throw new ArkmePluginError("record-reedit-shape-unsupported", "该快记包含背景音,当前接口无法安全保留,暂不支持重新编辑;已有草稿不会删除", false, 409);
38418
38928
  if (listValue$17(source.legacy_file_refs).length > 0) throw new ArkmePluginError("record-reedit-legacy-files-unsupported", "该快记包含尚未升级的历史附件,当前无法安全重新编辑", false, 409);
38419
38929
  const output = cloneKnownFields(source, [
38420
38930
  "payload_kind",
@@ -38480,13 +38990,24 @@ function recordReeditReadContentPayload(raw) {
38480
38990
  "length"
38481
38991
  ])).filter((mention) => Object.keys(mention).length > 0);
38482
38992
  if (locationMentions.length > 0) output.location_mentions = locationMentions;
38993
+ const hashTags = listValue$17(source.hash_tags).map((rawTag) => cloneKnownFields(objectValue$2(rawTag), [
38994
+ "tag",
38995
+ "start_index",
38996
+ "length"
38997
+ ]));
38998
+ if (hashTags.length > 0) output.hash_tags = hashTags;
38483
38999
  return output;
38484
39000
  }
38485
39001
  function recordReeditContentPayloadForWrite(contentPayload, currentText, nextText) {
38486
- if (contentPayload === void 0) return void 0;
39002
+ if (contentPayload === void 0) return arkmeHashTagContentPayload(nextText);
38487
39003
  if (nextText !== currentText && (Object.keys(objectValue$2(contentPayload.mention_metadata)).length > 0 || listValue$17(contentPayload.location_mentions).length > 0)) throw new ArkmePluginError("record-reedit-rich-text-unsupported", "该快记包含 @ 或位置内容,当前只能保持正文不变后修改标题", false, 409);
38488
39004
  const output = structuredClone(contentPayload);
38489
- if (Object.prototype.hasOwnProperty.call(output, "text_state")) output.text_state = 1;
39005
+ if (nextText !== currentText) {
39006
+ const hashTags = arkmeHashTagPayload(nextText);
39007
+ if (hashTags.length > 0) output.hash_tags = hashTags;
39008
+ else delete output.hash_tags;
39009
+ }
39010
+ if (nextText !== currentText && Object.prototype.hasOwnProperty.call(output, "text_state")) output.text_state = nextText === "" ? 3 : 1;
38490
39011
  return output;
38491
39012
  }
38492
39013
  function recordReeditHasAttachments(contentPayload) {
@@ -38528,11 +39049,160 @@ var RecordService = class {
38528
39049
  media;
38529
39050
  source;
38530
39051
  privacy;
38531
- constructor(runtime, media, source, privacy = new ArkmePrivacyVisibilityService(runtime)) {
39052
+ reeditFiles;
39053
+ reeditEditors = /* @__PURE__ */ new Map();
39054
+ submissions;
39055
+ reeditAdmissions = /* @__PURE__ */ new Map();
39056
+ activeRecordCommits = /* @__PURE__ */ new Set();
39057
+ closed = false;
39058
+ constructor(runtime, media, source, privacy = new ArkmePrivacyVisibilityService(runtime), reeditFiles, onReeditCommitted) {
38532
39059
  this.runtime = runtime;
38533
39060
  this.media = media;
38534
39061
  this.source = source;
38535
39062
  this.privacy = privacy;
39063
+ this.reeditFiles = reeditFiles;
39064
+ this.submissions = new RecordReeditSubmissions({
39065
+ list: (userId) => this.runtime.stateStore.listRecordReeditSubmissions(userId),
39066
+ put: (userId, job, expectedId) => {
39067
+ this.assertReeditActive();
39068
+ const persist = async () => {
39069
+ this.assertReeditActive();
39070
+ await this.runtime.stateStore.putRecordReeditSubmission(userId, job, expectedId);
39071
+ };
39072
+ const refs = job.draft.attachments?.flatMap((item) => item.fileRef === void 0 ? [] : [item.fileRef]) ?? [];
39073
+ return expectedId !== job.submissionId && refs.length > 0 ? this.recordReeditFiles().withReferences(refs, userId, persist) : persist();
39074
+ },
39075
+ committed: async (userId) => {
39076
+ if ((await this.runtime.requireSession()).userId === userId && !this.closed) await onReeditCommitted?.();
39077
+ },
39078
+ commit: async (job, beforeWrite) => {
39079
+ let writeStarted = false;
39080
+ try {
39081
+ return {
39082
+ kind: "committed",
39083
+ result: await this.executeRecordReeditCommand(job, async (fingerprint, _owner, write) => {
39084
+ await beforeWrite(fingerprint);
39085
+ writeStarted = true;
39086
+ return await write();
39087
+ })
39088
+ };
39089
+ } catch (error) {
39090
+ return this.recordReeditExecutionFailure(error, writeStarted);
39091
+ }
39092
+ },
39093
+ reconcile: async (job) => {
39094
+ try {
39095
+ const session = await this.runtime.requireSession();
39096
+ if (session.userId !== job.context.expectedUserId || !job.expectedCommittedFingerprint) return {
39097
+ kind: "uncertain",
39098
+ message: "请切回原账号后核对提交结果"
39099
+ };
39100
+ return {
39101
+ kind: "committed",
39102
+ result: await this.reconcileRecordReeditUnknownOutcome(session, job.context, job.expectedCommittedFingerprint)
39103
+ };
39104
+ } catch (error) {
39105
+ return this.recordReeditExecutionFailure(error, true, true);
39106
+ }
39107
+ }
39108
+ });
39109
+ }
39110
+ recordReeditExecutionFailure(error, writeStarted, reconciling = false) {
39111
+ const message = error instanceof Error ? error.message : "保存失败,请恢复编辑后重试";
39112
+ if (error instanceof ArkmePluginError && error.code === "record-reedit-conflict") return {
39113
+ kind: "conflict",
39114
+ message
39115
+ };
39116
+ if (reconciling || writeStarted && (!(error instanceof ArkmePluginError) || error.writeOutcomeUnknown || error.code === "record-reedit-outcome-unknown")) return {
39117
+ kind: "uncertain",
39118
+ message
39119
+ };
39120
+ return {
39121
+ kind: "failed",
39122
+ message
39123
+ };
39124
+ }
39125
+ reeditBaselineKey(userId, sourceRef, itemUid) {
39126
+ return JSON.stringify([
39127
+ userId,
39128
+ sourceRef,
39129
+ itemUid
39130
+ ]);
39131
+ }
39132
+ dispose() {
39133
+ this.closed = true;
39134
+ this.submissions.dispose();
39135
+ this.reeditEditors.clear();
39136
+ }
39137
+ assertReeditActive() {
39138
+ if (this.closed) throw new ArkmePluginError("record-reedit-unavailable", "插件已停止,请在新实例中恢复编辑或核对结果", false, 409);
39139
+ }
39140
+ async saveRecordReeditDraft(input) {
39141
+ return (await this.prepareLocalRecordReedit(input, true)).context;
39142
+ }
39143
+ async submitRecordReedit(input) {
39144
+ const session = await this.runtime.requireSession();
39145
+ const key = JSON.stringify([session.userId, input.itemUid.trim()]);
39146
+ const work = (this.reeditAdmissions.get(key) ?? Promise.resolve()).catch(() => void 0).then(() => this.acceptRecordReedit(input));
39147
+ this.reeditAdmissions.set(key, work);
39148
+ try {
39149
+ return await work;
39150
+ } finally {
39151
+ if (this.reeditAdmissions.get(key) === work) this.reeditAdmissions.delete(key);
39152
+ }
39153
+ }
39154
+ async acceptRecordReedit(input) {
39155
+ const session = await this.runtime.requireSession();
39156
+ const source = await this.source.openSourceRef(input.sourceRef, session.userId);
39157
+ const identity = await this.recordReeditSourceIdentityKey(source);
39158
+ await this.submissions.settleKnownCompletions(session.userId, identity, input.itemUid);
39159
+ const previous = (await this.runtime.stateStore.listRecordReeditSubmissions(session.userId)).find((job) => job.context.itemUid === input.itemUid && job.context.sourceIdentityKey === identity);
39160
+ if (previous && [
39161
+ "pending",
39162
+ "committing",
39163
+ "uncertain"
39164
+ ].includes(previous.state)) {
39165
+ if ((input.newText === void 0 || input.newText === previous.draft.textContent) && (input.newTitle === void 0 || input.newTitle.trim() === previous.draft.title) && input.expectedVersion === previous.context.baseVersion && (input.attachments === void 0 || JSON.stringify(input.attachments) === JSON.stringify(previous.draft.attachments))) return recordReeditSubmissionView(previous);
39166
+ throw new ArkmePluginError("record-reedit-in-progress", "这条快记仍在保存或核对,请稍后再编辑;其他消息不受影响", false, 409);
39167
+ }
39168
+ if (this.activeRecordCommits.has(JSON.stringify([
39169
+ session.userId,
39170
+ identity,
39171
+ input.itemUid
39172
+ ]))) throw new ArkmePluginError("record-reedit-in-progress", "该快记正在提交,请稍后重新确认", false, 409);
39173
+ const { context, owner, view } = await this.prepareLocalRecordReedit(input, false);
39174
+ const draft = await this.runtime.stateStore.getRecordReeditDraft(context.expectedUserId, context.sourceIdentityKey, context.itemUid);
39175
+ if (!draft || draft.draftRevision !== context.draftRevision) throw new ArkmeRecordReeditDraftConflict("草稿已变化,请重新读取");
39176
+ const selections = draft.attachments ?? view.attachments.map((item) => item.selection);
39177
+ const attachments = await this.recordReeditAttachmentViews(owner, selections, view.attachments.flatMap((item) => item.block ? [item.block] : []));
39178
+ if ((await this.runtime.requireSession()).userId !== context.expectedUserId) throw new ArkmePluginError("record-reedit-account-changed", "账号已切换,请重新打开", false, 409);
39179
+ const voiceFileAssetUid = stringValue$2(objectValue$2(owner.contentPayload?.voice).source_file_asset_uid);
39180
+ return await this.submissions.accept({
39181
+ context,
39182
+ draft,
39183
+ attachments,
39184
+ ...voiceFileAssetUid ? { voiceFileAssetUid } : {},
39185
+ ...view.voiceBlock ? { voiceBlock: view.voiceBlock } : {}
39186
+ });
39187
+ }
39188
+ async recordReeditSubmissions(sourceRef) {
39189
+ const session = await this.runtime.requireSession();
39190
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
39191
+ return await this.submissions.list(session.userId, await this.recordReeditSourceIdentityKey(source));
39192
+ }
39193
+ async resumeRecordReeditSubmissions(sourceRef, reconcile = false) {
39194
+ const session = await this.runtime.requireSession();
39195
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
39196
+ await this.submissions.resume(session.userId, await this.recordReeditSourceIdentityKey(source), reconcile, sourceRef);
39197
+ }
39198
+ async acknowledgeRecordReeditSubmission(sourceRef, submissionId, version) {
39199
+ const session = await this.runtime.requireSession();
39200
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
39201
+ if (!Number.isSafeInteger(version) || version <= 0) return;
39202
+ const identity = await this.recordReeditSourceIdentityKey(source);
39203
+ await this.submissions.settleKnownCompletions(session.userId, identity);
39204
+ this.assertReeditActive();
39205
+ await this.runtime.stateStore.acknowledgeRecordReeditSubmission(session.userId, identity, submissionId, version);
38536
39206
  }
38537
39207
  async prepareRecordReedit(input, options = {}) {
38538
39208
  const session = await this.runtime.requireSession();
@@ -38540,32 +39210,76 @@ var RecordService = class {
38540
39210
  const itemUid = input.itemUid.trim();
38541
39211
  if (sourceRef === "" || itemUid === "") throw new ArkmePluginError("record-reedit-target-invalid", "重新编辑目标无效", false);
38542
39212
  const owner = await this.recordReeditOwnerSnapshot(sourceRef, itemUid, session);
39213
+ await this.submissions.settleKnownCompletions(session.userId, owner.sourceIdentityKey, itemUid);
39214
+ const expectedVersion = input.expectedVersion ?? options.expectedBaseVersion;
39215
+ return await this.prepareRecordReeditCandidate({
39216
+ ...input,
39217
+ sourceRef,
39218
+ itemUid,
39219
+ ...expectedVersion === void 0 ? {} : { expectedVersion }
39220
+ }, owner, session, options.draftOnly === true);
39221
+ }
39222
+ async prepareLocalRecordReedit(input, draftOnly) {
39223
+ this.assertReeditActive();
39224
+ const session = await this.runtime.requireSession();
39225
+ const sourceRef = input.sourceRef.trim();
39226
+ const itemUid = input.itemUid.trim();
39227
+ let editor = this.reeditEditors.get(this.reeditBaselineKey(session.userId, sourceRef, itemUid));
39228
+ if (!editor) {
39229
+ if (!Number.isSafeInteger(input.expectedVersion) || (input.expectedVersion ?? 0) <= 0) throw new ArkmePluginError("record-reedit-version-invalid", "恢复编辑上下文需要原快记版本,请重新读取后确认", false, 409);
39230
+ editor = await this.loadRecordReeditEditor(sourceRef, itemUid, session);
39231
+ }
39232
+ if (input.expectedVersion !== void 0 && editor.owner.version !== input.expectedVersion) throw new ArkmePluginError("record-reedit-conflict", "快记已在其他位置更新,草稿已保留,请检查后重新确认", false, 409);
39233
+ await this.source.openSourceRef(sourceRef, session.userId);
39234
+ return {
39235
+ context: await this.prepareRecordReeditCandidate({
39236
+ ...input,
39237
+ sourceRef,
39238
+ itemUid
39239
+ }, editor.owner, session, draftOnly),
39240
+ ...editor
39241
+ };
39242
+ }
39243
+ async prepareRecordReeditCandidate(input, owner, session, draftOnly) {
39244
+ const { sourceRef, itemUid } = input;
38543
39245
  const previous = await this.runtime.stateStore.getRecordReeditDraft(session.userId, owner.sourceIdentityKey, itemUid);
38544
- const expectedBaseVersion = options.expectedBaseVersion;
39246
+ const expectedBaseVersion = input.expectedVersion;
38545
39247
  if (expectedBaseVersion !== void 0 && (!Number.isSafeInteger(expectedBaseVersion) || expectedBaseVersion <= 0)) throw new ArkmePluginError("record-reedit-version-invalid", "重新编辑版本无效,请重新打开后再试", false, 409);
38546
- const hasNewText = input.newText !== void 0;
38547
- if (!hasNewText && previous === void 0) throw new ArkmePluginError("record-reedit-draft-not-found", "该快记没有可恢复的重新编辑草稿,请提供新的正文", false, 404);
38548
- const candidateText = hasNewText ? input.newText : previous.textContent;
39248
+ if (!(input.newText !== void 0) && input.newTitle === void 0 && input.attachments === void 0 && previous === void 0) throw new ArkmePluginError("record-reedit-draft-not-found", "该快记没有可恢复的重新编辑草稿,请提供新的正文", false, 404);
39249
+ if (input.expectedDraftRevision !== void 0 && (!Number.isSafeInteger(input.expectedDraftRevision) || input.expectedDraftRevision < 0 || input.expectedDraftRevision !== (previous?.draftRevision ?? 0))) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新打开后确认", false, 409);
39250
+ if (input.attachments !== void 0 && expectedBaseVersion === void 0) throw new ArkmePluginError("record-reedit-version-invalid", "修改附件需要读取时的快记版本", false, 409);
39251
+ if (input.attachments !== void 0 && expectedBaseVersion !== owner.version) throw new ArkmePluginError("record-reedit-conflict", "快记已在其他位置更新,草稿已保留,请重新读取附件后确认", false, 409);
39252
+ const attachments = input.attachments === void 0 ? previous?.attachments : recordReeditAttachmentSelection(input.attachments, owner.contentPayload);
39253
+ if (owner.displayKind === 1 && (attachments?.length ?? 0) > 0) throw new ArkmePluginError("record-reedit-shape-unsupported", "长文只支持纯文本,不能添加附件;请使用普通快记", false, 409);
39254
+ const candidateText = input.newText ?? previous?.textContent ?? owner.textContent;
38549
39255
  const textContent = arkmeRecordTextFormat(owner.contentPayload) === "markdown" ? candidateText : candidateText.trim();
38550
- const title = (input.newTitle !== void 0 ? input.newTitle : hasNewText ? owner.title : previous.title).trim();
39256
+ const title = (input.newTitle ?? previous?.title ?? owner.title).trim();
38551
39257
  const maxTextLength = owner.displayKind === 1 ? 4e4 : this.runtime.config.maxTextLength;
38552
- if (textContent.trim() === "" || textContent.length > maxTextLength || title.length > 100) throw new ArkmePluginError("record-reedit-content-invalid", "重新编辑的标题或正文长度无效", false);
39258
+ const hasVoice = stringValue$2(objectValue$2(owner.contentPayload?.voice).source_file_asset_uid) !== "";
39259
+ const hasMedia = attachments === void 0 ? recordReeditHasAttachments(owner.contentPayload) : attachments.length > 0;
39260
+ if (!draftOnly && textContent.trim() === "" && !hasVoice && !hasMedia || textContent.length > maxTextLength || title.length > 100) throw new ArkmePluginError("record-reedit-content-invalid", "重新编辑的标题或正文长度无效", false);
38553
39261
  recordReeditContentPayloadForWrite(owner.contentPayload, owner.textContent, textContent);
39262
+ if (input.attachments !== void 0 && !draftOnly) {
39263
+ for (const item of attachments ?? []) if (item.fileRef !== void 0) await this.recordReeditFiles().readLocal(item.fileRef);
39264
+ }
39265
+ if ((await this.runtime.requireSession()).userId !== session.userId) throw new ArkmePluginError("record-reedit-account-changed", "当前账号已变化,请切回原账号后重新确认", false, 409);
38554
39266
  const preservesExpectedBaseline = expectedBaseVersion !== void 0 && previous?.baseVersion === expectedBaseVersion;
38555
- const baseVersion = expectedBaseVersion ?? owner.version;
38556
- const baseContentFingerprint = preservesExpectedBaseline ? previous.baseContentFingerprint : owner.fingerprint;
38557
- const editing = await this.runtime.stateStore.putRecordReeditDraft(session.userId, {
39267
+ const retainsAttachmentBaseline = input.attachments === void 0 && previous?.attachments !== void 0;
39268
+ const baseVersion = retainsAttachmentBaseline ? previous.baseVersion : expectedBaseVersion ?? owner.version;
39269
+ const baseContentFingerprint = preservesExpectedBaseline || retainsAttachmentBaseline ? previous.baseContentFingerprint : owner.fingerprint;
39270
+ const editing = await this.putRecordReeditDraft(session.userId, {
38558
39271
  schemaVersion: 1,
38559
39272
  sourceIdentityKey: owner.sourceIdentityKey,
38560
39273
  lastSourceRef: sourceRef,
38561
39274
  itemUid,
38562
39275
  title,
38563
39276
  textContent,
39277
+ ...attachments === void 0 ? {} : { attachments },
38564
39278
  baseVersion,
38565
39279
  baseContentFingerprint,
38566
39280
  editDurationMillis: previous?.editDurationMillis ?? owner.editDurationMillis,
38567
39281
  updatedAtMillis: Date.now()
38568
- });
39282
+ }, input.expectedDraftRevision ?? previous?.draftRevision ?? 0);
38569
39283
  return {
38570
39284
  expectedUserId: session.userId,
38571
39285
  sourceRef,
@@ -38581,17 +39295,36 @@ var RecordService = class {
38581
39295
  newTitle: title,
38582
39296
  newTextPreview: arkmeEmojiTokenSafePrefix(textContent, 160, "codeUnits"),
38583
39297
  sendAtMillis: owner.sendAtMillis,
38584
- preservesAttachments: recordReeditHasAttachments(owner.contentPayload)
39298
+ preservesAttachments: attachments === void 0 && recordReeditHasAttachments(owner.contentPayload),
39299
+ ...attachments === void 0 ? {} : { attachmentChanges: recordReeditAttachmentChanges(owner.contentPayload, attachments) }
38585
39300
  };
38586
39301
  }
38587
39302
  async recordReeditEditor(sourceRefInput, itemUidInput) {
38588
39303
  const session = await this.runtime.requireSession();
38589
- const sourceRef = sourceRefInput.trim();
38590
- const itemUid = itemUidInput.trim();
39304
+ return (await this.loadRecordReeditEditor(sourceRefInput.trim(), itemUidInput.trim(), session)).view;
39305
+ }
39306
+ async loadRecordReeditEditor(sourceRef, itemUid, session) {
39307
+ this.assertReeditActive();
38591
39308
  if (sourceRef === "" || itemUid === "") throw new ArkmePluginError("record-reedit-target-invalid", "重新编辑目标无效", false);
38592
39309
  const owner = await this.recordReeditOwnerSnapshot(sourceRef, itemUid, session);
38593
- const draft = await this.runtime.stateStore.getRecordReeditDraft(session.userId, owner.sourceIdentityKey, itemUid);
38594
- return {
39310
+ const baselineKey = this.reeditBaselineKey(session.userId, sourceRef, itemUid);
39311
+ await this.submissions.settleKnownCompletions(session.userId, owner.sourceIdentityKey, itemUid);
39312
+ let draft = await this.runtime.stateStore.getRecordReeditDraft(session.userId, owner.sourceIdentityKey, itemUid);
39313
+ if (!draft) {
39314
+ const failed = (await this.runtime.stateStore.listRecordReeditSubmissions(session.userId)).find((job) => job.context.itemUid === itemUid && job.context.sourceIdentityKey === owner.sourceIdentityKey && job.state === "failed");
39315
+ if (failed) draft = await this.putRecordReeditDraft(session.userId, {
39316
+ ...failed.draft,
39317
+ lastSourceRef: sourceRef
39318
+ }, 0);
39319
+ }
39320
+ const selections = recordReeditMediaGroups(owner.contentPayload).map((group) => ({ fileAssetUid: group.fileAssetUid }));
39321
+ const blocks = await this.recordReeditMediaBlocks(owner, session);
39322
+ const voiceFileAssetUid = stringValue$2(objectValue$2(owner.contentPayload?.voice).source_file_asset_uid);
39323
+ const voiceBlock = blocks.find((block) => block.kind === "audio" && block.fileAssetUid === voiceFileAssetUid);
39324
+ const attachments = await this.recordReeditAttachmentViews(owner, selections, blocks);
39325
+ const draftAttachments = draft?.attachments === void 0 ? void 0 : await this.recordReeditAttachmentViews(owner, draft.attachments, blocks);
39326
+ if ((await this.runtime.requireSession()).userId !== session.userId) throw new ArkmePluginError("record-reedit-account-changed", "当前账号已变化,请重新打开", false, 409);
39327
+ const snapshot = {
38595
39328
  sourceRef,
38596
39329
  itemUid,
38597
39330
  title: owner.title,
@@ -38603,40 +39336,131 @@ var RecordService = class {
38603
39336
  version: owner.version,
38604
39337
  maxTextLength: owner.displayKind === 1 ? 4e4 : this.runtime.config.maxTextLength,
38605
39338
  preservesAttachments: recordReeditHasAttachments(owner.contentPayload),
39339
+ attachments,
39340
+ hasVoice: voiceFileAssetUid !== "",
39341
+ ...voiceBlock ? { voiceBlock } : {},
39342
+ maxAttachments: owner.displayKind === 1 ? 0 : 9,
38606
39343
  ...draft === void 0 ? {} : { draft: {
38607
39344
  title: draft.title,
38608
39345
  textContent: draft.textContent,
38609
- updatedAtMillis: draft.updatedAtMillis
39346
+ updatedAtMillis: draft.updatedAtMillis,
39347
+ baseVersion: draft.baseVersion,
39348
+ draftRevision: draft.draftRevision,
39349
+ ...draftAttachments === void 0 ? {} : { attachments: draftAttachments }
38610
39350
  } }
38611
39351
  };
39352
+ this.assertReeditActive();
39353
+ const editor = {
39354
+ owner,
39355
+ view: snapshot
39356
+ };
39357
+ this.reeditEditors.delete(baselineKey);
39358
+ if (this.reeditEditors.size >= 100) this.reeditEditors.delete(this.reeditEditors.keys().next().value);
39359
+ this.reeditEditors.set(baselineKey, editor);
39360
+ return editor;
38612
39361
  }
38613
39362
  async commitRecordReedit(context) {
39363
+ const session = await this.runtime.requireSession();
39364
+ if (session.userId !== context.expectedUserId) throw new ArkmePluginError("record-reedit-account-changed", "账号已切换,请重新确认", false, 409);
39365
+ await this.submissions.settleKnownCompletions(session.userId, context.sourceIdentityKey, context.itemUid);
39366
+ if ((await this.runtime.stateStore.listRecordReeditSubmissions(session.userId)).some((job) => job.context.itemUid === context.itemUid && job.context.sourceIdentityKey === context.sourceIdentityKey && [
39367
+ "pending",
39368
+ "committing",
39369
+ "uncertain"
39370
+ ].includes(job.state))) throw new ArkmePluginError("record-reedit-in-progress", "该快记已有提交正在保存或核对,请稍后重新确认", false, 409);
39371
+ const draft = await this.runtime.stateStore.getRecordReeditDraft(session.userId, context.sourceIdentityKey, context.itemUid);
39372
+ if (!draft || draft.draftRevision !== context.draftRevision) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
39373
+ const command = structuredClone({
39374
+ context,
39375
+ draft
39376
+ });
39377
+ return await this.executeRecordReeditCommand(command, async (fingerprint, owner, write) => {
39378
+ const selections = draft.attachments ?? recordReeditMediaGroups(owner.contentPayload).map((group) => ({ fileAssetUid: group.fileAssetUid }));
39379
+ const blocks = await this.recordReeditMediaBlocks(owner, session);
39380
+ const attachments = await this.recordReeditAttachmentViews(owner, selections, blocks);
39381
+ const voiceFileAssetUid = stringValue$2(objectValue$2(owner.contentPayload?.voice).source_file_asset_uid);
39382
+ const voiceBlock = blocks.find((block) => block.kind === "audio" && block.fileAssetUid === voiceFileAssetUid);
39383
+ if ((await this.runtime.stateStore.getRecordReeditDraft(session.userId, context.sourceIdentityKey, context.itemUid))?.draftRevision !== draft.draftRevision) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
39384
+ let failure;
39385
+ const outcome = await this.submissions.writeConfirmed({
39386
+ ...command,
39387
+ attachments,
39388
+ ...voiceFileAssetUid ? { voiceFileAssetUid } : {},
39389
+ ...voiceBlock ? { voiceBlock } : {}
39390
+ }, fingerprint, async () => {
39391
+ try {
39392
+ if ((await this.runtime.stateStore.getRecordReeditDraft(session.userId, context.sourceIdentityKey, context.itemUid))?.draftRevision !== draft.draftRevision) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
39393
+ return {
39394
+ kind: "committed",
39395
+ result: await write()
39396
+ };
39397
+ } catch (error) {
39398
+ failure = error;
39399
+ return this.recordReeditExecutionFailure(error, true);
39400
+ }
39401
+ });
39402
+ if (outcome.kind === "committed") return outcome.result;
39403
+ throw failure;
39404
+ });
39405
+ }
39406
+ async executeRecordReeditCommand(command, atWrite) {
39407
+ const { context } = command;
39408
+ const key = JSON.stringify([
39409
+ context.expectedUserId,
39410
+ context.sourceIdentityKey,
39411
+ context.itemUid
39412
+ ]);
39413
+ if (this.activeRecordCommits.has(key)) throw new ArkmePluginError("record-reedit-in-progress", "该快记正在提交,请稍后重新确认", false, 409);
39414
+ this.activeRecordCommits.add(key);
39415
+ try {
39416
+ return await this.performRecordReeditCommit(command, atWrite);
39417
+ } finally {
39418
+ this.activeRecordCommits.delete(key);
39419
+ }
39420
+ }
39421
+ async performRecordReeditCommit(command, atWrite) {
39422
+ const { context, draft } = command;
38614
39423
  const session = await this.runtime.requireSession();
38615
39424
  if (session.userId !== context.expectedUserId) throw new ArkmePluginError("record-reedit-account-changed", "当前账号已变化,请切回原账号后重新确认", false, 409);
38616
39425
  const source = await this.source.openSourceRef(context.sourceRef, session.userId);
38617
- const sourceIdentityKey = await this.recordReeditSourceIdentityKey(source);
38618
- if (sourceIdentityKey !== context.sourceIdentityKey) throw new ArkmePluginError("record-reedit-source-changed", "重新编辑来源已变化,请重新发起", false, 409);
38619
- const draft = await this.runtime.stateStore.getRecordReeditDraft(session.userId, sourceIdentityKey, context.itemUid);
39426
+ if (await this.recordReeditSourceIdentityKey(source) !== context.sourceIdentityKey) throw new ArkmePluginError("record-reedit-source-changed", "重新编辑来源已变化,请重新发起", false, 409);
38620
39427
  if (draft === void 0 || draft.draftRevision !== context.draftRevision) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
38621
39428
  const owner = await this.recordReeditOwnerSnapshot(context.sourceRef, context.itemUid, session, source);
38622
39429
  if (owner.version !== context.baseVersion || owner.fingerprint !== context.baseContentFingerprint) throw new ArkmePluginError("record-reedit-conflict", "快记已在其他位置更新,草稿已保留,请检查后重新确认", false, 409);
39430
+ if (draft.textContent.trim() === "" && stringValue$2(objectValue$2(owner.contentPayload?.voice).source_file_asset_uid) === "" && (draft.attachments === void 0 ? !recordReeditHasAttachments(owner.contentPayload) : draft.attachments.length === 0)) throw new ArkmePluginError("record-reedit-content-invalid", "请保留正文或至少一个附件", false);
38623
39431
  const body = this.recordReeditUpdateBody(owner, draft.title, draft.textContent);
38624
- const committedContentPayload = objectValue$2(body.content_payload);
38625
- const { source: _source, sourceIdentityKey: _sourceIdentityKey, fingerprint: _fingerprint, ...baseFields } = owner;
39432
+ if (draft.attachments !== void 0) {
39433
+ const selection = recordReeditAttachmentSelection(draft.attachments, owner.contentPayload);
39434
+ const fileRefs = selection.flatMap((item) => item.fileRef === void 0 ? [] : [item.fileRef]);
39435
+ const assets = fileRefs.length === 0 ? [] : await this.recordReeditFiles().uploadRefs(fileRefs);
39436
+ if ((await this.runtime.requireSession()).userId !== session.userId) throw new ArkmePluginError("record-reedit-account-changed", "当前账号已变化,请切回原账号后重新确认", false, 409);
39437
+ const updated = recordReeditWithAttachments(recordReeditReadContentPayload(body.content_payload), selection, new Map(fileRefs.map((ref, index) => [ref, assets[index]])));
39438
+ body.template_kind = updated.templateKind;
39439
+ body.content_payload = updated.contentPayload;
39440
+ if (draft.textContent === "" && updated.contentPayload.text_state === 1) updated.contentPayload.text_state = 3;
39441
+ }
39442
+ const committedContentPayload = recordReeditReadContentPayload(body.content_payload);
39443
+ const { source: _source, sourceIdentityKey: _sourceIdentityKey, fingerprint: _fingerprint, attachmentData: _attachmentData, ...baseFields } = owner;
38626
39444
  const expectedCommittedFingerprint = recordReeditFingerprint({
38627
39445
  ...baseFields,
38628
39446
  title: draft.title,
38629
39447
  textContent: draft.textContent,
38630
- ...owner.contentPayload === void 0 ? {} : { contentPayload: committedContentPayload },
39448
+ templateKind: numberValue$20(body.template_kind),
39449
+ ...committedContentPayload === void 0 ? {} : { contentPayload: committedContentPayload },
38631
39450
  version: owner.version + 1
38632
39451
  });
38633
- try {
38634
- const data = await this.runtime.authenticatedPost("/api/v1/records/update", body, session, void 0, { trackWriteOutcome: true });
38635
- return await this.acceptRecordReeditCommit(session.userId, context, draft, data);
38636
- } catch (error) {
38637
- if (!(error instanceof ArkmePluginError) || error.writeOutcomeUnknown !== true) throw error;
38638
- return await this.reconcileRecordReeditUnknownOutcome(session, context, draft, expectedCommittedFingerprint);
38639
- }
39452
+ if ((await this.runtime.requireSession()).userId !== session.userId) throw new ArkmePluginError("record-reedit-account-changed", "当前账号已变化,请切回原账号后重新确认", false, 409);
39453
+ return await atWrite(expectedCommittedFingerprint, owner, async () => {
39454
+ if ((await this.runtime.requireSession()).userId !== session.userId) throw new ArkmePluginError("record-reedit-account-changed", "账号已切换,请恢复编辑后重新确认", false, 409);
39455
+ this.assertReeditActive();
39456
+ try {
39457
+ const data = await this.runtime.authenticatedPost("/api/v1/records/update", body, session, void 0, { trackWriteOutcome: true });
39458
+ return this.recordReeditCommitResult(context, data);
39459
+ } catch (error) {
39460
+ if (!(error instanceof ArkmePluginError) || error.writeOutcomeUnknown !== true) throw error;
39461
+ return await this.reconcileRecordReeditUnknownOutcome(session, context, expectedCommittedFingerprint);
39462
+ }
39463
+ });
38640
39464
  }
38641
39465
  async prepareDiscardRecordReeditDraft(sourceRefInput, itemUidInput) {
38642
39466
  const session = await this.runtime.requireSession();
@@ -38663,7 +39487,8 @@ var RecordService = class {
38663
39487
  const source = await this.source.openSourceRef(context.sourceRef, session.userId);
38664
39488
  const sourceIdentityKey = await this.recordReeditSourceIdentityKey(source);
38665
39489
  if (sourceIdentityKey !== context.sourceIdentityKey) throw new ArkmePluginError("record-reedit-source-changed", "重新编辑来源已变化,请重新发起", false, 409);
38666
- if (!await this.runtime.stateStore.removeRecordReeditDraft(session.userId, sourceIdentityKey, context.itemUid, context.draftRevision)) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
39490
+ this.assertReeditActive();
39491
+ if (!await this.runtime.stateStore.discardRecordReeditCandidate(session.userId, sourceIdentityKey, context.itemUid, context.draftRevision)) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
38667
39492
  return {
38668
39493
  status: "discarded",
38669
39494
  itemUid: context.itemUid
@@ -38707,6 +39532,9 @@ var RecordService = class {
38707
39532
  if (this.runtime.config.richMediaSendEnabled === false) throw new ArkmePluginError("rich-content-disabled", "长文编辑已被插件配置关闭", false, 403);
38708
39533
  const session = await this.runtime.requireSession();
38709
39534
  const detail = await this.longArticleDetail(sourceRef, itemUid);
39535
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
39536
+ const sourceIdentityKey = await this.recordReeditSourceIdentityKey(source);
39537
+ this.assertLongArticleDraftHasNoAttachmentChanges(await this.runtime.stateStore.getRecordReeditDraft(session.userId, sourceIdentityKey, itemUid));
38710
39538
  const title = input.title.trim();
38711
39539
  const textContent = detail.textFormat === "markdown" ? input.textContent : input.textContent.trim();
38712
39540
  const editDurationMillis = Math.max(0, Math.trunc(input.editDurationMillis));
@@ -38731,11 +39559,12 @@ var RecordService = class {
38731
39559
  if (uid === void 0) return await this.runtime.stateStore.getLongArticleDraft(session.userId, sourceRef);
38732
39560
  const sourceIdentityKey = await this.recordReeditSourceIdentityKey(source);
38733
39561
  const current = await this.runtime.stateStore.getRecordReeditDraft(session.userId, sourceIdentityKey, uid);
39562
+ this.assertLongArticleDraftHasNoAttachmentChanges(current);
38734
39563
  if (current !== void 0) return this.longArticleDraftFromRecordReedit(current, sourceRef);
38735
39564
  const legacy = await this.runtime.stateStore.getLongArticleDraft(session.userId, sourceRef, uid);
38736
39565
  if (legacy === void 0) return void 0;
38737
39566
  const detail = await this.longArticleDetail(sourceRef, uid);
38738
- const migrated = await this.runtime.stateStore.putRecordReeditDraft(session.userId, {
39567
+ const migrated = await this.putRecordReeditDraft(session.userId, {
38739
39568
  schemaVersion: 1,
38740
39569
  sourceIdentityKey,
38741
39570
  lastSourceRef: sourceRef,
@@ -38746,7 +39575,8 @@ var RecordService = class {
38746
39575
  baseContentFingerprint: this.longArticleContentFingerprint(detail),
38747
39576
  editDurationMillis: legacy.durationMillis,
38748
39577
  updatedAtMillis: legacy.updatedAtMillis
38749
- });
39578
+ }, 0);
39579
+ this.assertReeditActive();
38750
39580
  await this.runtime.stateStore.removeLongArticleDraft(session.userId, sourceRef, uid);
38751
39581
  return this.longArticleDraftFromRecordReedit(migrated, sourceRef);
38752
39582
  }
@@ -38767,8 +39597,9 @@ var RecordService = class {
38767
39597
  }
38768
39598
  const sourceIdentityKey = await this.recordReeditSourceIdentityKey(source);
38769
39599
  const current = await this.runtime.stateStore.getRecordReeditDraft(session.userId, sourceIdentityKey, itemUid);
39600
+ this.assertLongArticleDraftHasNoAttachmentChanges(current);
38770
39601
  const detail = current === void 0 ? await this.longArticleDetail(draft.sourceRef, itemUid) : void 0;
38771
- await this.runtime.stateStore.putRecordReeditDraft(session.userId, {
39602
+ await this.putRecordReeditDraft(session.userId, {
38772
39603
  schemaVersion: 1,
38773
39604
  sourceIdentityKey,
38774
39605
  lastSourceRef: draft.sourceRef,
@@ -38779,7 +39610,8 @@ var RecordService = class {
38779
39610
  baseContentFingerprint: current?.baseContentFingerprint ?? this.longArticleContentFingerprint(detail),
38780
39611
  editDurationMillis: Math.max(0, Math.trunc(draft.durationMillis)),
38781
39612
  updatedAtMillis: Date.now()
38782
- });
39613
+ }, current?.draftRevision ?? 0);
39614
+ this.assertReeditActive();
38783
39615
  await this.runtime.stateStore.removeLongArticleDraft(session.userId, draft.sourceRef, itemUid);
38784
39616
  }
38785
39617
  async removeLongArticleDraft(sourceRef, itemUid) {
@@ -38789,13 +39621,35 @@ var RecordService = class {
38789
39621
  if (uid !== void 0) {
38790
39622
  const sourceIdentityKey = await this.recordReeditSourceIdentityKey(source);
38791
39623
  const current = await this.runtime.stateStore.getRecordReeditDraft(session.userId, sourceIdentityKey, uid);
38792
- if (current !== void 0) await this.runtime.stateStore.removeRecordReeditDraft(session.userId, sourceIdentityKey, uid, current.draftRevision);
39624
+ this.assertLongArticleDraftHasNoAttachmentChanges(current);
39625
+ if (current !== void 0) {
39626
+ this.assertReeditActive();
39627
+ await this.runtime.stateStore.removeRecordReeditDraft(session.userId, sourceIdentityKey, uid, current.draftRevision);
39628
+ }
38793
39629
  }
39630
+ this.assertReeditActive();
38794
39631
  await this.runtime.stateStore.removeLongArticleDraft(session.userId, sourceRef, uid);
38795
39632
  }
38796
39633
  async recordReeditSourceIdentityKey(source) {
38797
39634
  return `arkme-record-reedit-source-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`record-reedit-source-v1:${String(source.userId)}:${source.kind}:${source.ownerRef}`).digest("base64url")}`;
38798
39635
  }
39636
+ async putRecordReeditDraft(userId, draft, expectedRevision) {
39637
+ try {
39638
+ this.assertReeditActive();
39639
+ const persist = async () => {
39640
+ this.assertReeditActive();
39641
+ return await this.runtime.stateStore.putRecordReeditDraft(userId, draft, expectedRevision);
39642
+ };
39643
+ const refs = draft.attachments?.flatMap((item) => item.fileRef === void 0 ? [] : [item.fileRef]) ?? [];
39644
+ return refs.length === 0 ? await persist() : await this.recordReeditFiles().withReferences(refs, userId, persist);
39645
+ } catch (error) {
39646
+ if (error instanceof ArkmeRecordReeditDraftConflict) throw new ArkmePluginError("record-reedit-draft-changed", "重新编辑草稿已变化,请重新确认", false, 409);
39647
+ throw error;
39648
+ }
39649
+ }
39650
+ assertLongArticleDraftHasNoAttachmentChanges(draft) {
39651
+ if (draft?.attachments !== void 0) throw new ArkmePluginError("record-reedit-attachments-editor-required", "此草稿包含附件编辑,请使用重新编辑入口或 Arkme 重新编辑工具继续,不能在纯文本长文编辑器中覆盖", false, 409);
39652
+ }
38799
39653
  async recordReeditOwnerSnapshot(sourceRef, itemUid, session, openedSource) {
38800
39654
  const source = openedSource ?? await this.source.openSourceRef(sourceRef, session.userId);
38801
39655
  const data = await this.runtime.authenticatedPost("/api/v1/records/detail", { record_uid: itemUid }, session);
@@ -38836,12 +39690,59 @@ var RecordService = class {
38836
39690
  version
38837
39691
  };
38838
39692
  return {
39693
+ attachmentData: data,
38839
39694
  source,
38840
39695
  sourceIdentityKey: await this.recordReeditSourceIdentityKey(source),
38841
39696
  ...snapshotFields,
38842
39697
  fingerprint: recordReeditFingerprint(snapshotFields)
38843
39698
  };
38844
39699
  }
39700
+ recordReeditFiles() {
39701
+ if (this.reeditFiles === void 0) throw new ArkmePluginError("file-flow-unavailable", "当前宿主不支持本地文件流程,请升级插件", false, 501);
39702
+ return this.reeditFiles;
39703
+ }
39704
+ async recordReeditMediaBlocks(owner, session) {
39705
+ if (!recordReeditHasAttachments(owner.contentPayload)) return [];
39706
+ const hydration = await this.media.hydrateRecordMediaPage([owner.attachmentData], session);
39707
+ return this.media.richContentBlocks(owner.attachmentData, session.userId, hydration.displayItemsByRecordUid.get(owner.itemUid) ?? []);
39708
+ }
39709
+ async recordReeditAttachmentViews(owner, selections, blocks) {
39710
+ if (selections.length === 0) return [];
39711
+ const localFiles = selections.some((item) => item.fileRef !== void 0) ? await this.recordReeditFiles().files() : [];
39712
+ const rawRefs = listValue$17(objectValue$2(objectValue$2(owner.attachmentData.record_core).content_payload).media_refs).map(objectValue$2);
39713
+ return selections.map((selection) => {
39714
+ if (selection.fileRef !== void 0) {
39715
+ const file = localFiles.find((value) => value.fileRef === selection.fileRef);
39716
+ return {
39717
+ selection,
39718
+ localFile: file ?? {
39719
+ fileRef: selection.fileRef,
39720
+ fileName: "附件已不可用,请移除或重新添加",
39721
+ mimeType: "application/octet-stream",
39722
+ size: 0,
39723
+ fileKind: 4
39724
+ },
39725
+ ...file === void 0 ? { unavailable: true } : {}
39726
+ };
39727
+ }
39728
+ const raw = rawRefs.find((item) => item.file_asset_uid === selection.fileAssetUid);
39729
+ const block = blocks.find((item) => item.fileAssetUid === selection.fileAssetUid);
39730
+ const fileName = block?.fileName ?? (stringValue$2(raw?.file_name) || "附件");
39731
+ const mimeType = block?.mimeType ?? arkmeNormalizedFileMimeType(stringValue$2(raw?.mime_type), fileName);
39732
+ return {
39733
+ selection,
39734
+ asset: {
39735
+ fileAssetUid: selection.fileAssetUid,
39736
+ fileName,
39737
+ mimeType,
39738
+ size: block?.size ?? numberValue$20(raw?.size),
39739
+ fileKind: arkmePickedFileKind(mimeType, fileName)
39740
+ },
39741
+ ...block === void 0 ? {} : { block },
39742
+ ...raw === void 0 ? { unavailable: true } : {}
39743
+ };
39744
+ });
39745
+ }
38845
39746
  recordReeditUpdateBody(owner, title, textContent) {
38846
39747
  const contentPayload = recordReeditContentPayloadForWrite(owner.contentPayload, owner.textContent, textContent);
38847
39748
  return {
@@ -38856,13 +39757,12 @@ var RecordService = class {
38856
39757
  version: owner.version
38857
39758
  };
38858
39759
  }
38859
- async acceptRecordReeditCommit(userId, context, draft, data) {
39760
+ recordReeditCommitResult(context, data) {
38860
39761
  const core = objectValue$2(data.record_core);
38861
39762
  const itemUid = stringValue$2(core.record_uid).trim();
38862
39763
  const version = Math.trunc(numberValue$20(core.version));
38863
39764
  const revisionUid = stringValue$2(data.revision_uid).trim();
38864
39765
  if (itemUid !== context.itemUid || version !== context.baseVersion + 1 || revisionUid === "") throw new ArkmePluginError("record-reedit-response-invalid", "快记更新响应不完整,正在等待状态确认", false, 502, { writeOutcomeUnknown: true });
38865
- if (!await this.runtime.stateStore.removeRecordReeditDraft(userId, context.sourceIdentityKey, context.itemUid, draft.draftRevision)) throw new ArkmePluginError("record-reedit-draft-changed", "快记已更新,但本机草稿发生变化,请勿重复提交", false, 409);
38866
39766
  return {
38867
39767
  status: "committed",
38868
39768
  itemUid,
@@ -38871,23 +39771,20 @@ var RecordService = class {
38871
39771
  projectionState: "pending"
38872
39772
  };
38873
39773
  }
38874
- async reconcileRecordReeditUnknownOutcome(session, context, draft, expectedCommittedFingerprint) {
39774
+ async reconcileRecordReeditUnknownOutcome(session, context, expectedCommittedFingerprint) {
38875
39775
  let current;
38876
39776
  try {
38877
39777
  current = await this.recordReeditOwnerSnapshot(context.sourceRef, context.itemUid, session);
38878
39778
  } catch (error) {
38879
39779
  throw new ArkmePluginError("record-reedit-outcome-unknown", "提交结果暂时无法确认,草稿已保留,请稍后查询,勿重复提交", false, 409, { cause: error });
38880
39780
  }
38881
- if (current.version === context.baseVersion + 1 && current.fingerprint === expectedCommittedFingerprint) {
38882
- if (!await this.runtime.stateStore.removeRecordReeditDraft(session.userId, context.sourceIdentityKey, context.itemUid, draft.draftRevision)) throw new ArkmePluginError("record-reedit-draft-changed", "快记已更新,但本机草稿发生变化,请勿重复提交", false, 409);
38883
- return {
38884
- status: "committed",
38885
- itemUid: current.itemUid,
38886
- version: current.version,
38887
- revisionUid: "",
38888
- projectionState: "pending"
38889
- };
38890
- }
39781
+ if (current.version === context.baseVersion + 1 && current.fingerprint === expectedCommittedFingerprint) return {
39782
+ status: "committed",
39783
+ itemUid: current.itemUid,
39784
+ version: current.version,
39785
+ revisionUid: "",
39786
+ projectionState: "pending"
39787
+ };
38891
39788
  if (current.version === context.baseVersion && current.fingerprint === context.baseContentFingerprint) throw new ArkmePluginError("record-reedit-outcome-unknown", "提交结果暂时无法确认,草稿已保留,请稍后查询,勿重复提交", false, 409);
38892
39789
  throw new ArkmePluginError("record-reedit-conflict", "快记内容已变化,草稿已保留,请检查后重新确认", false, 409);
38893
39790
  }
@@ -39290,6 +40187,7 @@ var RecordService = class {
39290
40187
  const core = objectValue$2(item.record_core);
39291
40188
  const extensionProjection = this.recordExtensionProjection(raw, userId);
39292
40189
  const forwardRecords = projectRecordRecordingForward(item.content_payload ?? core.content_payload);
40190
+ const contentBlocks = this.media.richContentBlocks(raw, userId, options.displayItems);
39293
40191
  return {
39294
40192
  itemUid: stringValue$2(item.record_uid ?? core.record_uid).trim(),
39295
40193
  senderName: stringValue$2(item.nickname).trim() || "我",
@@ -39305,11 +40203,11 @@ var RecordService = class {
39305
40203
  updateAtMillis: numberValue$20(item.update_at ?? core.update_at),
39306
40204
  recordDurationMillis: numberValue$20(item.record_duration_millis ?? core.record_duration_millis),
39307
40205
  editDurationMillis: numberValue$20(item.edit_duration_millis ?? core.edit_duration_millis),
39308
- contentBlocks: this.media.richContentBlocks(raw, userId, options.displayItems),
40206
+ contentBlocks,
39309
40207
  ...forwardRecords === void 0 ? {} : { forwardRecords },
39310
40208
  ...extensionProjection === void 0 ? {} : extensionProjection,
39311
40209
  ...options.selfTopic === void 0 ? {} : { selfTopic: options.selfTopic },
39312
- ...options.mediaUnavailable === true ? { mediaUnavailable: true } : {}
40210
+ ...options.mediaUnavailable === true || this.media.recordMediaUnavailable(raw, contentBlocks) ? { mediaUnavailable: true } : {}
39313
40211
  };
39314
40212
  }
39315
40213
  recordItem(raw, userId, options = {}) {
@@ -39319,6 +40217,7 @@ var RecordService = class {
39319
40217
  if (recordUid === "") return void 0;
39320
40218
  const extensionProjection = userId === void 0 ? void 0 : this.recordExtensionProjection(raw, userId);
39321
40219
  const forwardRecords = projectRecordRecordingForward(item.content_payload ?? core.content_payload);
40220
+ const contentBlocks = userId === void 0 ? void 0 : this.media.richContentBlocks(raw, userId, options.displayItems);
39322
40221
  return {
39323
40222
  recordUid,
39324
40223
  sendAtMillis: numberValue$20(item.send_at ?? core.send_at),
@@ -39331,9 +40230,9 @@ var RecordService = class {
39331
40230
  creationSource: recordCreationSource(raw),
39332
40231
  displayKind: numberValue$20(item.display_kind ?? core.display_kind),
39333
40232
  ...forwardRecords === void 0 ? {} : { forwardRecords },
39334
- ...userId === void 0 ? {} : { contentBlocks: this.media.richContentBlocks(raw, userId, options.displayItems) },
40233
+ ...contentBlocks === void 0 ? {} : { contentBlocks },
39335
40234
  ...extensionProjection === void 0 ? {} : extensionProjection,
39336
- ...options.mediaUnavailable === true ? { mediaUnavailable: true } : {}
40235
+ ...options.mediaUnavailable === true || contentBlocks !== void 0 && this.media.recordMediaUnavailable(raw, contentBlocks) ? { mediaUnavailable: true } : {}
39337
40236
  };
39338
40237
  }
39339
40238
  isDSHAgentInput(raw) {
@@ -41627,7 +42526,7 @@ var ChatService = class {
41627
42526
  const evidence = await this.mentionContentPayload(source, textContent, normalizedText, options.humanMentions ?? [], options.botMentions ?? [], session, options.signal, options.textFormat);
41628
42527
  Object.assign(contentPayload, { mention_metadata: evidence.mention_metadata });
41629
42528
  }
41630
- const data = await this.runtime.authenticatedChatPost("/api/v1/chats/extensions/children/create", {
42529
+ const data = await postChatMessageCreation(this.runtime, "/api/v1/chats/extensions/children/create", {
41631
42530
  chat_session_uid: reference.chatSessionUid,
41632
42531
  parent_record_owner_user_id: reference.recordOwnerUserId,
41633
42532
  parent_record_uid: reference.recordUid,
@@ -42001,7 +42900,7 @@ var ChatService = class {
42001
42900
  const sendAtMillis = Date.now();
42002
42901
  const clientRequestId = this.forwardClientRequestId(targetSource.ownerRef, references, sendAtMillis);
42003
42902
  const sourceRecordUids = references.some((reference) => reference.sourceKind === "chat_relation") ? [] : references.map((reference) => reference.recordUid).filter((recordUid) => recordUid.trim() !== "");
42004
- const data = await this.runtime.authenticatedChatPost("/api/v1/chats/records/forward", {
42903
+ const data = await postChatMessageCreation(this.runtime, "/api/v1/chats/records/forward", {
42005
42904
  chat_session_uid: targetSource.ownerRef,
42006
42905
  client_request_id: clientRequestId,
42007
42906
  ...sourceRecordUids.length === 0 ? {} : { source_record_uids: sourceRecordUids },
@@ -42375,7 +43274,7 @@ var ChatService = class {
42375
43274
  });
42376
43275
  }
42377
43276
  async postChatTextRecord(chatSessionUid, text, recordUid, relationUid, sendAtMillis, session, signal, options = {}) {
42378
- return await this.runtime.authenticatedChatPost("/api/v1/chats/records/send", {
43277
+ return await postChatMessageCreation(this.runtime, "/api/v1/chats/records/send", {
42379
43278
  chat_session_uid: chatSessionUid,
42380
43279
  record_uid: recordUid,
42381
43280
  rel_uid: relationUid,
@@ -42488,7 +43387,7 @@ var ChatService = class {
42488
43387
  assets
42489
43388
  });
42490
43389
  }
42491
- const result = await this.runtime.authenticatedChatPost("/api/v1/chats/records/send", {
43390
+ const result = await postChatMessageCreation(this.runtime, "/api/v1/chats/records/send", {
42492
43391
  chat_session_uid: source.ownerRef,
42493
43392
  rel_uid: relationUid,
42494
43393
  ...commonBody
@@ -42598,7 +43497,7 @@ var ChatService = class {
42598
43497
  if (sticker === void 0 || sticker.is_available === false) throw new ArkmePluginError("favorite-sticker-unavailable", "该收藏表情已不可用", false, 404);
42599
43498
  const recordUid = options.recordUid?.trim() || randomUUID();
42600
43499
  const relationUid = options.relationUid?.trim() || randomUUID();
42601
- const result = await this.runtime.authenticatedChatPost("/api/v1/chats/records/send", {
43500
+ const result = await postChatMessageCreation(this.runtime, "/api/v1/chats/records/send", {
42602
43501
  chat_session_uid: source.ownerRef,
42603
43502
  record_uid: recordUid,
42604
43503
  rel_uid: relationUid,
@@ -42854,8 +43753,10 @@ var ChatService = class {
42854
43753
  ...forwardRecords === void 0 ? {} : { forwardRecords },
42855
43754
  ...sharedRecording === void 0 ? {} : { sharedRecording },
42856
43755
  ...extensionProjection === void 0 ? {} : extensionProjection,
43756
+ templateKind: numberValue$19(payload.template_kind),
42857
43757
  displayKind: numberValue$19(payload.display_kind),
42858
- contentBlocks
43758
+ contentBlocks,
43759
+ ...this.media.recordMediaUnavailable(item, contentBlocks) ? { mediaUnavailable: true } : {}
42859
43760
  });
42860
43761
  }
42861
43762
  this.hydrateTimelineExtensionParents(items);
@@ -43493,6 +44394,7 @@ var ChatService = class {
43493
44394
  ...captureContext === void 0 ? {} : { captureContext },
43494
44395
  contentBlocks,
43495
44396
  ...forwardRecords === void 0 ? {} : { forwardRecords },
44397
+ ...this.media.recordMediaUnavailable(item, contentBlocks) ? { mediaUnavailable: true } : {},
43496
44398
  ...sharedRecording === void 0 ? {} : { sharedRecording },
43497
44399
  ...extensionProjection === void 0 ? {} : extensionProjection
43498
44400
  }) - 1;
@@ -76126,6 +77028,12 @@ var MediaService = class {
76126
77028
  if (stableKey !== void 0) this.stableMediaRefs.set(stableKey, ref);
76127
77029
  return ref;
76128
77030
  }
77031
+ /** A current Record version does not imply that every media URL was resolved. */
77032
+ recordMediaUnavailable(raw, blocks) {
77033
+ if (this.runtime.config.richMediaRenderEnabled === false) return false;
77034
+ const displayed = new Set(blocks.map((block) => block.fileAssetUid));
77035
+ return this.recordMediaRefs(raw).some((ref) => !displayed.has(stringValue$2(ref.file_asset_uid).trim()));
77036
+ }
76129
77037
  richContentBlocks(raw, viewerUserId, hydratedDisplayItems = []) {
76130
77038
  if (this.runtime.config.richMediaRenderEnabled === false) return [];
76131
77039
  const root = objectValue$2(raw);
@@ -76603,7 +77511,7 @@ var ArkmeMessageActionGateway = class {
76603
77511
  };
76604
77512
  }
76605
77513
  async forwardToChat(input) {
76606
- const data = await this.runtime.authenticatedChatPost("/api/v1/chats/records/forward", {
77514
+ const data = await postChatMessageCreation(this.runtime, "/api/v1/chats/records/forward", {
76607
77515
  chat_session_uid: input.target.ownerRef,
76608
77516
  client_request_id: input.requestId,
76609
77517
  source_items: input.references.map((reference) => this.chatForwardSourceItem(reference)),
@@ -80908,6 +81816,8 @@ var FileTransfers = class {
80908
81816
  states = /* @__PURE__ */ new Map();
80909
81817
  writes = Promise.resolve();
80910
81818
  mutations = Promise.resolve();
81819
+ directUploads = Promise.resolve();
81820
+ uploadingRefs = /* @__PURE__ */ new Set();
80911
81821
  queue = Promise.resolve();
80912
81822
  jobs = /* @__PURE__ */ new Set();
80913
81823
  controllers = /* @__PURE__ */ new Set();
@@ -80932,6 +81842,7 @@ var FileTransfers = class {
80932
81842
  }
80933
81843
  async settled() {
80934
81844
  await this.queue;
81845
+ await this.directUploads.catch(() => {});
80935
81846
  await Promise.all(this.jobs);
80936
81847
  await this.writes;
80937
81848
  }
@@ -81030,7 +81941,7 @@ var FileTransfers = class {
81030
81941
  const limit = metadata.mimeType.startsWith("image/") ? this.policy.maxImageBytes : this.policy.maxFileBytes;
81031
81942
  if (!metadata.fileName.trim() || metadata.fileName.length > 255 || /[\u0000-\u001f]/.test(metadata.fileName) || metadata.mimeType.length > 200 || !/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/.test(metadata.mimeType) || !Number.isSafeInteger(metadata.size) || metadata.size <= 0 || metadata.size > limit) throw fail("file-input-invalid", `文件为空或超出限制(${Math.floor(limit / 1024 / 1024)} MiB)`);
81032
81943
  }
81033
- async stage(temporaryPath, metadata, expectedUserId) {
81944
+ async stage(temporaryPath, metadata, expectedUserId, retention) {
81034
81945
  const userId = await this.ports.currentUser();
81035
81946
  if (expectedUserId !== void 0 && expectedUserId !== userId) throw fail("file-account-changed", "账号已切换,本次文件导入已取消");
81036
81947
  const normalizedMetadata = {
@@ -81053,7 +81964,8 @@ var FileTransfers = class {
81053
81964
  fileRef: ref,
81054
81965
  fileKind: arkmePickedFileKind(normalizedMetadata.mimeType, normalizedMetadata.fileName),
81055
81966
  sha256: hash.digest("hex"),
81056
- createdAtMillis: Date.now()
81967
+ createdAtMillis: Date.now(),
81968
+ ...retention ? { retention } : {}
81057
81969
  };
81058
81970
  await copyFile(temporaryPath, this.path(userId, ref));
81059
81971
  await chmod$1(this.path(userId, ref), 384);
@@ -81070,17 +81982,50 @@ var FileTransfers = class {
81070
81982
  }
81071
81983
  async prune(userId, state) {
81072
81984
  const retained = new Set(state.tasks.filter((task) => task.state !== "sent").flatMap((task) => task.fileRefs));
81985
+ let editRefs;
81986
+ try {
81987
+ editRefs = await this.ports.retainedFileRefs?.(userId) ?? [];
81988
+ } catch {
81989
+ return;
81990
+ }
81991
+ for (const ref of editRefs) retained.add(ref);
81073
81992
  const completed = new Set(state.tasks.filter((task) => task.state === "sent").flatMap((task) => task.fileRefs));
81074
- for (const file of Object.values(state.files)) {
81075
- if (Date.now() - file.createdAtMillis < 168 * 36e5 || retained.has(file.fileRef) || !completed.has(file.fileRef)) continue;
81076
- delete state.files[file.fileRef];
81993
+ const expired = Object.values(state.files).filter((file) => {
81994
+ if (Date.now() - file.createdAtMillis < 168 * 36e5 || retained.has(file.fileRef) || this.uploadingRefs.has(`${userId}:${file.fileRef}`)) return false;
81995
+ return file.retention === "references" ? this.ports.retainedFileRefs !== void 0 : completed.has(file.fileRef);
81996
+ });
81997
+ const expiredRefs = new Set(expired.map((file) => file.fileRef));
81998
+ const tasks = state.tasks.filter((task) => task.state !== "sent" || Date.now() - task.createdAtMillis < 168 * 36e5 || task.fileRefs.some((ref) => state.files[ref] !== void 0 && !expiredRefs.has(ref)));
81999
+ if (expired.length === 0 && tasks.length === state.tasks.length) return;
82000
+ const previous = {
82001
+ files: state.files,
82002
+ tasks: state.tasks
82003
+ };
82004
+ state.files = Object.fromEntries(Object.entries(state.files).filter(([ref]) => !expiredRefs.has(ref)));
82005
+ state.tasks = tasks;
82006
+ try {
82007
+ await this.save(userId, state);
82008
+ } catch (error) {
82009
+ Object.assign(state, previous);
82010
+ throw error;
82011
+ }
82012
+ for (const file of expired) {
81077
82013
  await unlink(this.path(userId, file.fileRef)).catch(() => {});
81078
82014
  await rm(this.openDirectory(userId, file.fileRef), {
81079
82015
  recursive: true,
81080
82016
  force: true
81081
82017
  });
81082
82018
  }
81083
- state.tasks = state.tasks.filter((task) => task.state !== "sent" || Date.now() - task.createdAtMillis < 168 * 36e5);
82019
+ }
82020
+ /** Validate new references and persist their owner without a cleanup/removal gap. Local I/O only. */
82021
+ async withReferences(refs, userId, persist) {
82022
+ return this.exclusive(async () => {
82023
+ await this.assertUser(userId);
82024
+ const retained = new Set(await this.ports.retainedFileRefs?.(userId) ?? []);
82025
+ for (const ref of new Set(refs)) if (!retained.has(ref)) await this.readLocal(ref);
82026
+ await this.assertUser(userId);
82027
+ return await persist();
82028
+ });
81084
82029
  }
81085
82030
  async readLocal(ref) {
81086
82031
  const userId = await this.ports.currentUser();
@@ -81128,6 +82073,8 @@ var FileTransfers = class {
81128
82073
  const state = await this.state(userId);
81129
82074
  await this.assertUser(userId);
81130
82075
  if (state.tasks.some((task) => task.fileRefs.includes(ref))) throw fail("file-in-use", "文件仍被本地发送任务引用,请先移除该任务");
82076
+ if (this.uploadingRefs.has(`${userId}:${ref}`)) throw fail("file-in-use", "文件正在上传,请稍后移除");
82077
+ if ((await this.ports.retainedFileRefs?.(userId))?.includes(ref)) throw fail("file-in-use", "文件仍被重新编辑草稿引用,请先从草稿移除");
81131
82078
  if (!state.files[ref]) return;
81132
82079
  delete state.files[ref];
81133
82080
  await this.save(userId, state);
@@ -81273,38 +82220,53 @@ var FileTransfers = class {
81273
82220
  async uploadRefs(fileRefs, signal) {
81274
82221
  const userId = await this.ports.currentUser();
81275
82222
  if (fileRefs.length < 1 || fileRefs.length > this.policy.maxAttachments || new Set(fileRefs).size !== fileRefs.length || fileRefs.some((ref) => !REF.test(ref))) throw fail("file-upload-invalid", "请选择 1 至 9 个有效附件");
81276
- return await this.exclusive(async () => {
81277
- await this.assertUser(userId, signal);
81278
- const state = await this.state(userId);
81279
- const controller = new AbortController();
81280
- const abort = () => {
81281
- controller.abort(signal?.reason);
81282
- };
81283
- signal?.addEventListener("abort", abort, { once: true });
81284
- this.controllers.add(controller);
82223
+ const controller = new AbortController();
82224
+ const abort = () => {
82225
+ controller.abort(signal?.reason);
82226
+ };
82227
+ signal?.addEventListener("abort", abort, { once: true });
82228
+ if (signal?.aborted) abort();
82229
+ this.controllers.add(controller);
82230
+ const work = this.directUploads.catch(() => {}).then(async () => {
82231
+ const state = await this.exclusive(async () => {
82232
+ await this.assertUser(userId, controller.signal);
82233
+ const current = await this.state(userId);
82234
+ if (fileRefs.some((ref) => !current.files[ref])) throw fail("file-local-missing", "本地附件已不存在");
82235
+ for (const ref of fileRefs) this.uploadingRefs.add(`${userId}:${ref}`);
82236
+ return current;
82237
+ });
81285
82238
  try {
81286
82239
  const assets = [];
81287
82240
  for (const fileRef of fileRefs) {
81288
82241
  await this.assertUser(userId, controller.signal);
81289
82242
  const stored = state.files[fileRef];
81290
82243
  if (stored === void 0) throw fail("file-local-missing", "本地附件已不存在");
81291
- let asset = stored.asset ?? Object.values(state.files).find((other) => other.sha256 === stored.sha256 && other.fileKind === stored.fileKind && other.asset)?.asset;
82244
+ let asset = stored.asset?.fileKind === stored.fileKind ? stored.asset : Object.values(state.files).find((other) => other.sha256 === stored.sha256 && other.asset?.fileKind === stored.fileKind)?.asset;
81292
82245
  if (asset === void 0) asset = await this.ports.upload(this.path(userId, fileRef), stored, () => {}, userId, controller.signal);
82246
+ await this.assertUser(userId, controller.signal);
81293
82247
  asset = {
81294
82248
  ...asset,
81295
82249
  fileName: stored.fileName
81296
82250
  };
81297
- stored.asset = asset;
81298
- await this.save(userId, state);
82251
+ await this.exclusive(async () => {
82252
+ stored.asset = asset;
82253
+ await this.save(userId, state);
82254
+ });
81299
82255
  assets.push(asset);
81300
82256
  }
81301
82257
  await this.assertUser(userId, controller.signal);
81302
82258
  return clone(assets);
81303
82259
  } finally {
81304
- signal?.removeEventListener("abort", abort);
81305
- this.controllers.delete(controller);
82260
+ for (const ref of fileRefs) this.uploadingRefs.delete(`${userId}:${ref}`);
81306
82261
  }
81307
82262
  });
82263
+ this.directUploads = work;
82264
+ try {
82265
+ return await work;
82266
+ } finally {
82267
+ signal?.removeEventListener("abort", abort);
82268
+ this.controllers.delete(controller);
82269
+ }
81308
82270
  }
81309
82271
  async retry(taskRef) {
81310
82272
  const userId = await this.ports.currentUser();
@@ -81315,6 +82277,7 @@ var FileTransfers = class {
81315
82277
  await this.assertUser(userId);
81316
82278
  if (task.state === "uncertain") throw fail("file-send-uncertain", "发送结果待确认,请先核对原会话,不能自动重复发送");
81317
82279
  if (task.state !== "failed") return clone(task);
82280
+ if (task.retryable === false) throw fail(task.errorCode ?? "file-send-rejected", task.error ?? "该发送已被拒绝,不能重试");
81318
82281
  task.state = "queued";
81319
82282
  delete task.error;
81320
82283
  delete task.errorCode;
@@ -81388,6 +82351,8 @@ var FileTransfers = class {
81388
82351
  } else if (ownerOutcome.kind === "owner_not_accepted") {
81389
82352
  task.state = "failed";
81390
82353
  task.error = ownerOutcome.message;
82354
+ if (ownerOutcome.retryable === false) task.retryable = false;
82355
+ else delete task.retryable;
81391
82356
  if (ownerOutcome.code === void 0) delete task.errorCode;
81392
82357
  else task.errorCode = ownerOutcome.code;
81393
82358
  } else {
@@ -81502,6 +82467,7 @@ function createArkmeFileTransfers(options) {
81502
82467
  if (options.directory === void 0) return void 0;
81503
82468
  return new FileTransfers(options.directory, {
81504
82469
  currentUser: async () => (await options.runtime.requireSession()).userId,
82470
+ retainedFileRefs: async (userId) => await options.runtime.stateStore.recordReeditFileRefs(userId),
81505
82471
  validateSource: async (sourceRef) => {
81506
82472
  await options.source.openSourceRef(sourceRef, (await options.runtime.requireSession()).userId);
81507
82473
  },
@@ -81546,7 +82512,8 @@ function createArkmeFileTransfers(options) {
81546
82512
  } : {
81547
82513
  kind: "owner_not_accepted",
81548
82514
  message: error.message,
81549
- code: error.code
82515
+ code: error.code,
82516
+ retryable: error.retryable
81550
82517
  };
81551
82518
  }
81552
82519
  },
@@ -84413,6 +85380,7 @@ var ArkmeService = class {
84413
85380
  unmarkedSpeaker;
84414
85381
  voiceprint;
84415
85382
  userBan;
85383
+ directMessageAdmissionOwner;
84416
85384
  backgroundSoundPreferenceOwner;
84417
85385
  fileTransfers;
84418
85386
  localFileOpener;
@@ -84443,7 +85411,14 @@ var ArkmeService = class {
84443
85411
  isDSHAgentInput: (raw) => this.record.isDSHAgentInput(raw),
84444
85412
  isPrivacyLocked: (raw) => this.record.isPrivacyLocked(raw)
84445
85413
  }, this.privacy);
84446
- this.record = new RecordService(this.runtime, this.media, this.source, this.privacy);
85414
+ this.record = new RecordService(this.runtime, this.media, this.source, this.privacy, {
85415
+ files: async () => await this.filesOwner().files(),
85416
+ readLocal: async (ref) => ({ file: (await this.filesOwner().readLocal(ref)).file }),
85417
+ uploadRefs: async (refs) => await this.filesOwner().uploadRefs(refs),
85418
+ withReferences: async (refs, userId, persist) => await this.filesOwner().withReferences(refs, userId, persist)
85419
+ }, async () => {
85420
+ await this.realtime.invalidateRecordProjection();
85421
+ });
84447
85422
  this.search = new SearchService(this.runtime, this.record, this.media, this.source, this.privacy);
84448
85423
  this.bot = new BotService(this.runtime, this.source);
84449
85424
  this.messageActions = new MessageActionService(new ArkmeMessageActionGateway(this.runtime, this.source, async (target, sequence) => {
@@ -84467,6 +85442,7 @@ var ArkmeService = class {
84467
85442
  this.conversationDirectoryVisibility = new ConversationDirectoryVisibilityService(new ConversationListPreferenceService(this.runtime), this.source, this.bot, this.realtime);
84468
85443
  this.chat = new ChatService(this.runtime, this.source, this.profile, this.media, this.record, this.bot, this.arko, this.aiPolish, this.realtime, this.privacy, this.messageActions);
84469
85444
  this.userBan = new UserBanService(this.runtime, this.chat);
85445
+ this.directMessageAdmissionOwner = new DirectMessageAdmissionService(this.runtime, this.source);
84470
85446
  this.botConversation = new BotConversationService(this.runtime, this.bot, this.chat, async () => {
84471
85447
  await this.realtime.invalidateRecordProjection();
84472
85448
  }, this.messageActions);
@@ -84520,8 +85496,8 @@ var ArkmeService = class {
84520
85496
  async fileSessionUser() {
84521
85497
  return (await this.runtime.requireSession()).userId;
84522
85498
  }
84523
- async fileStage(path, metadata, expectedUserId) {
84524
- return await this.filesOwner().stage(path, metadata, expectedUserId);
85499
+ async fileStage(path, metadata, expectedUserId, retention) {
85500
+ return await this.filesOwner().stage(path, metadata, expectedUserId, retention);
84525
85501
  }
84526
85502
  async fileList() {
84527
85503
  return await this.filesOwner().files();
@@ -84742,6 +85718,7 @@ var ArkmeService = class {
84742
85718
  messageReadReceipts: true,
84743
85719
  messageReport: true,
84744
85720
  userBanManagement: true,
85721
+ directMessageAdmission: true,
84745
85722
  groupOwnerGovernance: true,
84746
85723
  ...this.config.markdownQuickNotesEnabled === true ? { markdownQuickNotes: true } : {},
84747
85724
  richContentRead: this.config.richMediaRenderEnabled !== false,
@@ -84827,6 +85804,7 @@ var ArkmeService = class {
84827
85804
  return await this.callHistory.retryCallSummary(callRef, signal);
84828
85805
  }
84829
85806
  dispose() {
85807
+ this.record.dispose();
84830
85808
  this.realtime.resetAttentionSummary();
84831
85809
  this.fileTransfers?.cancelActive();
84832
85810
  this.contact.dispose();
@@ -85190,6 +86168,12 @@ var ArkmeService = class {
85190
86168
  async userBanStatus(sourceRef, signal) {
85191
86169
  return await this.userBan.status(sourceRef, signal);
85192
86170
  }
86171
+ directMessageAdmission(sourceRef, signal) {
86172
+ return this.directMessageAdmissionOwner.directMessageAdmission(sourceRef, signal);
86173
+ }
86174
+ setDirectMessageRefusal(sourceRef, refused, expectedRevision, signal) {
86175
+ return this.directMessageAdmissionOwner.setDirectMessageRefusal(sourceRef, refused, expectedRevision, signal);
86176
+ }
85193
86177
  async banPrivateChatUser(sourceRef, remark = "", signal) {
85194
86178
  return await this.userBan.ban(sourceRef, remark, signal);
85195
86179
  }
@@ -85347,13 +86331,26 @@ var ArkmeService = class {
85347
86331
  async prepareRecordReedit(input, options = {}) {
85348
86332
  return await this.record.prepareRecordReedit(input, options);
85349
86333
  }
86334
+ async saveRecordReeditDraft(input) {
86335
+ return await this.record.saveRecordReeditDraft(input);
86336
+ }
86337
+ async submitRecordReedit(input) {
86338
+ return await this.record.submitRecordReedit(input);
86339
+ }
86340
+ async recordReeditSubmissions(sourceRef) {
86341
+ return await this.record.recordReeditSubmissions(sourceRef);
86342
+ }
86343
+ async resumeRecordReeditSubmissions(sourceRef, reconcile = false) {
86344
+ return await this.record.resumeRecordReeditSubmissions(sourceRef, reconcile);
86345
+ }
86346
+ async acknowledgeRecordReeditSubmission(sourceRef, submissionId, version) {
86347
+ return await this.record.acknowledgeRecordReeditSubmission(sourceRef, submissionId, version);
86348
+ }
85350
86349
  async recordReeditEditor(sourceRef, itemUid) {
85351
86350
  return await this.record.recordReeditEditor(sourceRef, itemUid);
85352
86351
  }
85353
86352
  async commitRecordReedit(context) {
85354
- const result = await this.record.commitRecordReedit(context);
85355
- await this.realtime.invalidateRecordProjection().catch(() => void 0);
85356
- return result;
86353
+ return await this.record.commitRecordReedit(context);
85357
86354
  }
85358
86355
  async prepareDiscardRecordReeditDraft(sourceRef, itemUid) {
85359
86356
  return await this.record.prepareDiscardRecordReeditDraft(sourceRef, itemUid);
@@ -87233,4 +88230,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
87233
88230
  return jsonValue(await handler(jsonValue(args)));
87234
88231
  }
87235
88232
  //#endregion
87236
- export { ARKME_PROVIDER_CONTRACT_VERSION as $, inspectNativeBundleArtifactV3 as A, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, bundleSha256 as D, arkmeSandboxEntryId as E, assertExtensionArtifactSize as F, validateVoiceprintPcm16Wav as G, ARKME_EXTENSION_PREVIEW_MAX_BYTES as H, canonicalExtensionJson as I, arkmeIsGenericLinkMetadataTitle as J, isRecordingInstantOnOrAfterUnixEpoch as K, sha256Hex as L, packLocalNativeBundleDirectoryV3 as M, readNativeBundleTarballV3 as N, canonicalBundleJson as O, renderArkmeSandboxHostEntry as P, ARKME_DEFAULT_SHARE_WEBSITE as Q, unpackArkmeExtension as R, ARKME_BUNDLE_ARTIFACT_KIND as S, ArkmeBundleArtifactError as T, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as U, ARKME_EXTENSION_MAX_BYTES as V, ArkmeService as W, arkmeFileBackgroundSound as X, arkmeRequiredLinkMetadataFallback as Y, arkmeRichBackgroundSound as Z, arkmeClientContentDigest as _, ArkmePluginError as _t, persistentArkmeExtensionActive as a, ARKME_RUNTIME_INSTANCE_ID as at, verifyExtensionResolutionSignature as b, persistentExtensionBundleDirectory as c, arkmeNormalizedFileMimeType as ct, readPersistentExtensionActivation as d, recordingImportFileKind as dt, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as et, refreshPersistentClientWrapper as f, sameRecordingImportIdentity as ft, activeProfileExtensionOwnerConflicts as g, SecretValue as gt, renderArkmeBundleClientBundle as h, ArkmeOutgoingCallError as ht, invokePersistentArkmeExtension as i, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as it, packBundleFiles as j, inspectBundleArtifact as k, persistentExtensionPackageIdentity as l, arkmePickedFileKind as lt, writePersistentExtensionActivation as m, recordingImportFileNameKey as mt, applyPersistentArkmeHostExtension as n, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as nt, persistentArkmeExtensionRuntimeState as o, ARKME_TOOL_FILE_MAX_BYTES as ot, repairPersistentExtensionInstallation as p, MAX_RECORDING_IMPORT_BYTES as pt, arkmeExtensionShareRefFromLink as q, deactivatePersistentArkmeExtension as r, ARKME_WORLD_PUBLISH_MAX_IMAGES as rt, materializePersistentExtensionBundle as s, arkmeCanInlineLocalFile as st, activatePersistentArkmeExtension as t, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as tt, quarantinePersistentExtension as u, isArkmeBotAvatarRef as ut, arkmeClientContentDigestFromSource as v, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as w, verifyNativeBundleResolutionSignature as x, verifyBundleResolutionSignature as y, ARKME_EXTENSION_FORMAT as z };
88233
+ export { parseArkmeRecordReeditAttachments as $, inspectNativeBundleArtifactV3 as A, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, bundleSha256 as D, arkmeSandboxEntryId as E, assertExtensionArtifactSize as F, validateVoiceprintPcm16Wav as G, ARKME_EXTENSION_PREVIEW_MAX_BYTES as H, canonicalExtensionJson as I, arkmeIsGenericLinkMetadataTitle as J, isRecordingInstantOnOrAfterUnixEpoch as K, sha256Hex as L, packLocalNativeBundleDirectoryV3 as M, readNativeBundleTarballV3 as N, canonicalBundleJson as O, renderArkmeSandboxHostEntry as P, ArkmeRecordReeditDraftConflict as Q, unpackArkmeExtension as R, ARKME_BUNDLE_ARTIFACT_KIND as S, ArkmeBundleArtifactError as T, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as U, ARKME_EXTENSION_MAX_BYTES as V, ArkmeService as W, arkmeFileBackgroundSound as X, arkmeRequiredLinkMetadataFallback as Y, arkmeRichBackgroundSound as Z, arkmeClientContentDigest as _, ArkmeOutgoingCallError as _t, persistentArkmeExtensionActive as a, ARKME_WORLD_PUBLISH_MAX_IMAGES as at, verifyExtensionResolutionSignature as b, ArkmePluginError as bt, persistentExtensionBundleDirectory as c, ARKME_TOOL_FILE_MAX_BYTES as ct, readPersistentExtensionActivation as d, arkmePickedFileKind as dt, ARKME_DEFAULT_SHARE_WEBSITE as et, refreshPersistentClientWrapper as f, isArkmeBotAvatarRef as ft, activeProfileExtensionOwnerConflicts as g, recordingImportFileNameKey as gt, renderArkmeBundleClientBundle as h, MAX_RECORDING_IMPORT_BYTES as ht, invokePersistentArkmeExtension as i, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as it, packBundleFiles as j, inspectBundleArtifact as k, persistentExtensionPackageIdentity as l, arkmeCanInlineLocalFile as lt, writePersistentExtensionActivation as m, sameRecordingImportIdentity as mt, applyPersistentArkmeHostExtension as n, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as nt, persistentArkmeExtensionRuntimeState as o, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as ot, repairPersistentExtensionInstallation as p, recordingImportFileKind as pt, arkmeExtensionShareRefFromLink as q, deactivatePersistentArkmeExtension as r, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as rt, materializePersistentExtensionBundle as s, ARKME_RUNTIME_INSTANCE_ID as st, activatePersistentArkmeExtension as t, ARKME_PROVIDER_CONTRACT_VERSION as tt, quarantinePersistentExtension as u, arkmeNormalizedFileMimeType as ut, arkmeClientContentDigestFromSource as v, SecretValue as vt, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as w, verifyNativeBundleResolutionSignature as x, verifyBundleResolutionSignature as y, ArkmeDirectMessageAdmissionError as yt, ARKME_EXTENSION_FORMAT as z };