@senguoyun/dsh-arkme 0.1.85 → 0.1.87

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 (77) hide show
  1. package/docs/recognized-speaker-directory-backend-handoff-2026-09-28.md +2 -0
  2. package/docs/recognized-speaker-entry-and-visits.md +2 -0
  3. package/docs/recognized-speaker-sorting.md +2 -0
  4. package/docs/self-role-avatar-cache.md +59 -0
  5. package/docs/speaker-directory-integration-2026-09-30.md +53 -0
  6. package/lib/client.js +1336 -1168
  7. package/lib/client.js.map +1 -1
  8. package/lib/harness-model-client.js +14 -0
  9. package/lib/harness-native-selection-client.js +14 -0
  10. package/lib/harness-sidebar-client.js +14 -0
  11. package/lib/index.js +28 -9
  12. package/lib/persistent-extension.js +1 -1
  13. package/lib/{persistent-runtime-adGFmkIa.js → persistent-runtime-0qn1BZ6S.js} +733 -136
  14. package/lib/sdk.js +14 -0
  15. package/lib/types/arkme-service.d.ts +7 -0
  16. package/lib/types/arkme-service.d.ts.map +1 -1
  17. package/lib/types/client/ArkmeAvatar.d.ts +2 -1
  18. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  19. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts +3 -30
  20. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts.map +1 -1
  21. package/lib/types/client/ArkmeSelfRolePicker.d.ts.map +1 -1
  22. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  23. package/lib/types/client/api.d.ts +1 -1
  24. package/lib/types/client/api.d.ts.map +1 -1
  25. package/lib/types/client/avatar-image-runtime.d.ts.map +1 -1
  26. package/lib/types/client/avatar-image-store.d.ts +10 -0
  27. package/lib/types/client/avatar-image-store.d.ts.map +1 -1
  28. package/lib/types/client/avatar-persistent-cache.d.ts +16 -0
  29. package/lib/types/client/avatar-persistent-cache.d.ts.map +1 -0
  30. package/lib/types/client/locales/actions-en.d.ts.map +1 -1
  31. package/lib/types/client/reaction-notifications.d.ts.map +1 -1
  32. package/lib/types/client/reaction-preview-store.d.ts +4 -1
  33. package/lib/types/client/reaction-preview-store.d.ts.map +1 -1
  34. package/lib/types/client/reaction-transport.d.ts +4 -0
  35. package/lib/types/client/reaction-transport.d.ts.map +1 -0
  36. package/lib/types/client/realtime-client-events.d.ts.map +1 -1
  37. package/lib/types/client/recognized-speaker-directory.d.ts +51 -22
  38. package/lib/types/client/recognized-speaker-directory.d.ts.map +1 -1
  39. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts +2 -4
  40. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts.map +1 -1
  41. package/lib/types/client/recordings/recording-speaker-options-store.d.ts.map +1 -1
  42. package/lib/types/host-api.d.ts.map +1 -1
  43. package/lib/types/local-database.d.ts +1 -0
  44. package/lib/types/local-database.d.ts.map +1 -1
  45. package/lib/types/reaction-contract.d.ts +4 -0
  46. package/lib/types/reaction-contract.d.ts.map +1 -1
  47. package/lib/types/reaction-diagnostics.d.ts +4 -0
  48. package/lib/types/reaction-diagnostics.d.ts.map +1 -0
  49. package/lib/types/reaction-host-diagnostics.d.ts +6 -0
  50. package/lib/types/reaction-host-diagnostics.d.ts.map +1 -0
  51. package/lib/types/recording-presence.d.ts +1 -1
  52. package/lib/types/request-coordinator.d.ts +2 -0
  53. package/lib/types/request-coordinator.d.ts.map +1 -1
  54. package/lib/types/sdk/index.d.ts +2 -0
  55. package/lib/types/sdk/index.d.ts.map +1 -1
  56. package/lib/types/services/chat-service.d.ts +1 -0
  57. package/lib/types/services/chat-service.d.ts.map +1 -1
  58. package/lib/types/services/media-service.d.ts +14 -0
  59. package/lib/types/services/media-service.d.ts.map +1 -1
  60. package/lib/types/services/profile-service.d.ts.map +1 -1
  61. package/lib/types/services/reaction-service.d.ts.map +1 -1
  62. package/lib/types/services/recording-service.d.ts +4 -0
  63. package/lib/types/services/recording-service.d.ts.map +1 -1
  64. package/lib/types/services/service.d.ts +1 -0
  65. package/lib/types/services/service.d.ts.map +1 -1
  66. package/lib/types/services/speaker-directory-service.d.ts +19 -0
  67. package/lib/types/services/speaker-directory-service.d.ts.map +1 -0
  68. package/lib/types/services/unmarked-speaker-service.d.ts +2 -0
  69. package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
  70. package/lib/types/speaker-directory-contract.d.ts +71 -0
  71. package/lib/types/speaker-directory-contract.d.ts.map +1 -0
  72. package/lib/types/tools/business/media/read-image.d.ts.map +1 -1
  73. package/lib/types/tools/ports/media.d.ts +1 -0
  74. package/lib/types/tools/ports/media.d.ts.map +1 -1
  75. package/lib/types/types.d.ts +2 -1
  76. package/lib/types/types.d.ts.map +1 -1
  77. package/package.json +5 -5
package/lib/client.js CHANGED
@@ -800,6 +800,20 @@ window.__ModuleLoader__.load({
800
800
  if (imageRef.trim() === "") throw new TypeError("Arkme image reference must not be empty");
801
801
  return await this.call("image.read", { imageRef }, signal);
802
802
  }
803
+ /** Probe immutable image bytes without starting an upstream request. */
804
+ async readCachedImage(imageRef, signal) {
805
+ if (imageRef.trim() === "") throw new TypeError("Arkme image reference must not be empty");
806
+ if ((await this.capabilities(signal)).features.imageCacheRead !== true) throw new Error("当前 Arkme Provider 不支持本地头像探测,请升级");
807
+ try {
808
+ return await this.call("image.read", {
809
+ imageRef,
810
+ cacheOnly: true
811
+ }, signal);
812
+ } catch (error) {
813
+ if (error instanceof ArkmeClientError && error.body.code === "image-cache-miss") return void 0;
814
+ throw error;
815
+ }
816
+ }
803
817
  /** Convert a Provider image payload into a browser-renderable data URL. */
804
818
  imageDataUrl(image) {
805
819
  return `data:${image.mediaType};base64,${image.dataBase64}`;
@@ -35173,6 +35187,23 @@ Bot 列表加载失败,点击重试|Could not load Bots. Click to retry
35173
35187
  //#endregion
35174
35188
  //#region src/client/locales/actions-en.ts
35175
35189
  const actionEnglish = {
35190
+ "加载更多说话人": "Load more speakers",
35191
+ "该分类暂无说话人": "No speakers in this category",
35192
+ "没有匹配的说话人": "No matching speakers",
35193
+ "目录有更新,刷新后查看最新人物。": "The directory has updates. Refresh to see the latest people.",
35194
+ "搜索内容过长,请缩短至 256 字节以内。": "Search text is too long. Keep it within 256 bytes.",
35195
+ "搜索名称或编号": "Search by name or number",
35196
+ "查看状态暂未同步,联网后自动重试。": "View status has not synced. It will retry when online.",
35197
+ "正在加载人物详情…": "Loading person details…",
35198
+ "部分录音仍在整理,人物可能继续更新。": "Some recordings are still being processed. People may continue to update.",
35199
+ "正在更新,当前显示已整理的目录。": "Updating. Showing the available directory.",
35200
+ "更新暂未完成,当前显示上次整理的目录。": "The update has not completed. Showing the previous directory.",
35201
+ "正在整理说话人目录…": "Preparing the speaker directory…",
35202
+ "说话人目录暂不可用,请稍后刷新。": "The speaker directory is unavailable. Please refresh later.",
35203
+ "说话人目录暂不可用,请稍后再试。": "The speaker directory is unavailable. Please try again later.",
35204
+ "声纹状态暂不可用": "Voiceprint status unavailable",
35205
+ "暂时无法更新": "Unable to update",
35206
+ "目录暂不可用": "Directory unavailable",
35176
35207
  "发现未发布的草稿": "Unpublished draft found",
35177
35208
  "草稿预览": "Draft preview",
35178
35209
  "预览草稿": "Preview draft",
@@ -54565,84 +54596,15 @@ body[data-ds-dark-theme] .arkme-bot-identity {
54565
54596
  });
54566
54597
  }
54567
54598
  //#endregion
54568
- //#region src/avatar-diagnostics.ts
54569
- function record(value) {
54570
- return value !== null && typeof value === "object" ? value : {};
54571
- }
54572
- function positiveId(value) {
54573
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
54574
- }
54575
- /** Decoded IDs are diagnostic hints only, never authentication or authorization inputs. */
54576
- function avatarReferenceDiagnostic(imageRef) {
54577
- const ref = imageRef.trim();
54578
- if (!ref.startsWith("arkme-profile-image-v1.")) return { referenceKind: ref.startsWith("arkme-media-v1.") ? "media" : "other" };
54579
- const context = { referenceKind: "profile" };
54580
- try {
54581
- const parts = ref.split(".");
54582
- const encoded = parts[1] ?? "";
54583
- if (parts.length !== 3 || encoded.length > 1024 || !/^[A-Za-z0-9_-]+$/.test(encoded)) return context;
54584
- const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
54585
- const payload = record(JSON.parse(globalThis.atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="))));
54586
- if (payload.version !== 1) return context;
54587
- const viewer = positiveId(payload.viewerUserId);
54588
- const target = positiveId(payload.targetUserId);
54589
- if (viewer !== void 0) context.referenceViewerUserId = viewer;
54590
- if (target !== void 0) context.referenceTargetUserId = target;
54591
- } catch {}
54592
- return context;
54593
- }
54594
- function avatarScopeDiagnostic(scopeKey) {
54595
- const match = /^(prod|test):([1-9][0-9]*)$/.exec(scopeKey ?? "");
54596
- const viewerUserId = positiveId(Number(match?.[2]));
54597
- return match === null || viewerUserId === void 0 ? {} : {
54598
- environment: match[1],
54599
- viewerUserId
54600
- };
54601
- }
54602
- function safeErrorCode(value) {
54603
- return typeof value === "string" && /^(?:(?:arkme|image|auth-http|local|profile)-[A-Za-z0-9_-]{1,80}|UND_ERR_[A-Z_]{1,64}|E[A-Z_]{1,64})$/.test(value) ? value : void 0;
54604
- }
54605
- function httpStatus(value) {
54606
- return typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599 ? value : void 0;
54607
- }
54608
- function imageDownloadStatus(error) {
54609
- if (error.name !== "ArkmePluginError" || error.code !== "image-download-failed" || typeof error.message !== "string") return void 0;
54610
- const match = /^Arkme 图片读取返回 HTTP ([1-5][0-9]{2})$/.exec(error.message);
54611
- return match === null ? void 0 : httpStatus(Number(match[1]));
54612
- }
54613
- function logArkmeAvatarDiagnostic(event, context, error) {
54614
- try {
54615
- const source = record(error);
54616
- const body = record(source.body);
54617
- const detail = source.name === "ArkmeClientError" ? body : source;
54618
- const errorName = [
54619
- "Error",
54620
- "TypeError",
54621
- "AbortError",
54622
- "TimeoutError",
54623
- "ArkmePluginError",
54624
- "ArkmeClientError"
54625
- ].includes(String(source.name)) ? String(source.name) : "unknown";
54626
- console.warn(`[ArkmeAvatarDiag] ${JSON.stringify({
54627
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
54628
- event,
54629
- ...context,
54630
- ...error === void 0 ? {} : { error: {
54631
- name: errorName,
54632
- code: safeErrorCode(detail.code),
54633
- retryable: typeof detail.retryable === "boolean" ? detail.retryable : void 0,
54634
- httpStatus: httpStatus(source.httpStatus),
54635
- upstreamStatus: httpStatus(source.upstreamStatus) ?? imageDownloadStatus(source),
54636
- causeCode: safeErrorCode(record(source.cause).code)
54637
- } }
54638
- })}`);
54639
- } catch {}
54640
- }
54641
- //#endregion
54642
54599
  //#region src/client/avatar-image-store.ts
54600
+ /** These references identify immutable bytes, unlike mutable profile references. */
54601
+ function isImmutableAvatarRef(ref) {
54602
+ return /^file_asset:\/\/[A-Za-z0-9_-]{8,128}$/.test(ref) || /^arkme-self-role-image-v1\.[A-Za-z0-9_-]+$/.test(ref);
54603
+ }
54643
54604
  const DEFAULT_TTL_MILLIS = 600 * 1e3;
54644
54605
  const DEFAULT_JITTER_MILLIS$1 = 120 * 1e3;
54645
- const DEFAULT_CONCURRENCY = 6;
54606
+ const DEFAULT_CONCURRENCY = 3;
54607
+ const IMMUTABLE_CONCURRENCY = 2;
54646
54608
  const MAX_RETAINED_AVATARS = 256;
54647
54609
  const MAX_QUEUED_AVATARS = 512;
54648
54610
  const AVATAR_IMAGE_SCOPE_CHANGED = "Avatar image scope changed";
@@ -54656,6 +54618,8 @@ body[data-ds-dark-theme] .arkme-bot-identity {
54656
54618
  jitterMillis;
54657
54619
  concurrency;
54658
54620
  activeDownloads = 0;
54621
+ activeImmutableReads = 0;
54622
+ activeCacheProbes = 0;
54659
54623
  generation = 0;
54660
54624
  scopeKey;
54661
54625
  constructor(options) {
@@ -54709,10 +54673,26 @@ body[data-ds-dark-theme] .arkme-bot-identity {
54709
54673
  expiresAtMillis: 0,
54710
54674
  pending: void 0
54711
54675
  };
54712
- const pending = this.schedule(async () => {
54676
+ const readRemote = () => this.schedule(isImmutableAvatarRef(imageRef) ? "immutable" : "remote", async () => {
54713
54677
  if (generation !== this.generation) throw new Error(AVATAR_IMAGE_SCOPE_CHANGED);
54714
54678
  return await this.options.reader(imageRef);
54715
- }).then((payload) => {
54679
+ });
54680
+ const persistent = scopeKey !== void 0 && isImmutableAvatarRef(imageRef) ? this.options.persistentCache : void 0;
54681
+ const probe = isImmutableAvatarRef(imageRef) ? this.options.cachedReader : void 0;
54682
+ const pending = (persistent === void 0 && probe === void 0 ? readRemote() : (async () => {
54683
+ const cached = await persistent?.read(scopeKey, imageRef).catch(() => void 0);
54684
+ if (generation !== this.generation) throw new Error(AVATAR_IMAGE_SCOPE_CHANGED);
54685
+ if (cached !== void 0) return cached;
54686
+ const local = probe === void 0 ? void 0 : await this.schedule("probe", async () => {
54687
+ if (generation !== this.generation) throw new Error(AVATAR_IMAGE_SCOPE_CHANGED);
54688
+ return await probe(imageRef);
54689
+ });
54690
+ if (generation !== this.generation) throw new Error(AVATAR_IMAGE_SCOPE_CHANGED);
54691
+ const payload = local ?? await readRemote();
54692
+ if (generation !== this.generation) throw new Error(AVATAR_IMAGE_SCOPE_CHANGED);
54693
+ persistent?.write(scopeKey, imageRef, payload).catch(() => void 0);
54694
+ return payload;
54695
+ })()).then((payload) => {
54716
54696
  if (generation !== this.generation) throw new Error(AVATAR_IMAGE_SCOPE_CHANGED);
54717
54697
  const value = `data:${payload.mediaType};base64,${payload.dataBase64}`;
54718
54698
  const previousValue = entry.value;
@@ -54751,27 +54731,35 @@ body[data-ds-dark-theme] .arkme-bot-identity {
54751
54731
  if (entry.pending === void 0 && !this.listeners.has(ref)) this.entries.delete(ref);
54752
54732
  }
54753
54733
  }
54754
- schedule(load) {
54734
+ schedule(lane, load) {
54755
54735
  return new Promise((resolve, reject) => {
54756
54736
  if (this.queue.length >= MAX_QUEUED_AVATARS) {
54757
54737
  reject(/* @__PURE__ */ new Error("Avatar loading queue is full"));
54758
54738
  return;
54759
54739
  }
54760
- this.queue.push(() => {
54761
- load().then(resolve, reject).finally(() => {
54762
- this.activeDownloads -= 1;
54763
- this.drainQueue();
54764
- });
54740
+ this.queue.push({
54741
+ lane,
54742
+ start: () => {
54743
+ load().then(resolve, reject).finally(() => {
54744
+ if (lane === "probe") this.activeCacheProbes -= 1;
54745
+ else if (lane === "immutable") this.activeImmutableReads -= 1;
54746
+ else this.activeDownloads -= 1;
54747
+ this.drainQueue();
54748
+ });
54749
+ }
54765
54750
  });
54766
54751
  this.drainQueue();
54767
54752
  });
54768
54753
  }
54769
54754
  drainQueue() {
54770
- while (this.activeDownloads < this.concurrency) {
54771
- const start = this.queue.shift();
54772
- if (start === void 0) return;
54773
- this.activeDownloads += 1;
54774
- start();
54755
+ for (;;) {
54756
+ const index = this.queue.findIndex((job) => job.lane === "probe" ? this.activeCacheProbes < 1 : job.lane === "immutable" ? this.activeImmutableReads < IMMUTABLE_CONCURRENCY : this.activeDownloads < this.concurrency);
54757
+ if (index === -1) return;
54758
+ const job = this.queue.splice(index, 1)[0];
54759
+ if (job.lane === "probe") this.activeCacheProbes += 1;
54760
+ else if (job.lane === "immutable") this.activeImmutableReads += 1;
54761
+ else this.activeDownloads += 1;
54762
+ job.start();
54775
54763
  }
54776
54764
  }
54777
54765
  notify(imageRef, value) {
@@ -54779,8 +54767,185 @@ body[data-ds-dark-theme] .arkme-bot-identity {
54779
54767
  }
54780
54768
  };
54781
54769
  //#endregion
54770
+ //#region src/client/avatar-persistent-cache.ts
54771
+ const CACHE_NAME = "arkme-immutable-avatars-v1";
54772
+ const MAX_ENTRIES$1 = 64;
54773
+ const MAX_BYTES$1 = 64 * 1024 * 1024;
54774
+ const MAX_BASE64_LENGTH = Math.ceil(8 * 1024 * 1024 / 3) * 4;
54775
+ function validPayload(value) {
54776
+ if (typeof value !== "object" || value === null) return false;
54777
+ const payload = value;
54778
+ return typeof payload.mediaType === "string" && /^image\/(png|jpeg|webp|gif|avif)$/.test(payload.mediaType) && typeof payload.dataBase64 === "string" && payload.dataBase64.length > 0 && payload.dataBase64.length <= MAX_BASE64_LENGTH && payload.dataBase64.length % 4 === 0 && !/[^A-Za-z0-9+/]/.test(payload.dataBase64.replace(/={1,2}$/, ""));
54779
+ }
54780
+ /** Validate browser-persisted bytes too: base64 syntax is not image validity. */
54781
+ async function decodableAvatar(payload) {
54782
+ try {
54783
+ const bytes = Uint8Array.from(atob(payload.dataBase64), (character) => character.charCodeAt(0));
54784
+ const blob = new Blob([bytes], { type: payload.mediaType });
54785
+ if (typeof createImageBitmap === "function") {
54786
+ (await createImageBitmap(blob)).close();
54787
+ return true;
54788
+ }
54789
+ if (typeof Image === "undefined") return false;
54790
+ const url = URL.createObjectURL(blob);
54791
+ try {
54792
+ return await new Promise((resolve) => {
54793
+ const image = new Image();
54794
+ image.onload = () => resolve(image.naturalWidth > 0 && image.naturalHeight > 0);
54795
+ image.onerror = () => resolve(false);
54796
+ image.src = url;
54797
+ });
54798
+ } finally {
54799
+ URL.revokeObjectURL(url);
54800
+ }
54801
+ } catch {
54802
+ return false;
54803
+ }
54804
+ }
54805
+ /** Browser bytes survive reloads. Host remains the authenticated source on misses.
54806
+ * No TTL for immutable refs; quota eviction and clearing site data are cache misses.
54807
+ * Synthetic keys are never fetched and include the environment/account scope.
54808
+ */
54809
+ var BrowserAvatarPersistentCache = class {
54810
+ decode;
54811
+ constructor(decode = decodableAvatar) {
54812
+ this.decode = decode;
54813
+ }
54814
+ writes = Promise.resolve();
54815
+ key(scope, ref) {
54816
+ try {
54817
+ if (!scope.trim() || !isImmutableAvatarRef(ref) || typeof globalThis.caches === "undefined" || typeof globalThis.location === "undefined") return void 0;
54818
+ return new URL(`/arkme-self/avatar-cache/v1/${encodeURIComponent(scope)}/${encodeURIComponent(ref)}`, globalThis.location.origin).href;
54819
+ } catch {
54820
+ return;
54821
+ }
54822
+ }
54823
+ async read(scope, ref) {
54824
+ const key = this.key(scope, ref);
54825
+ if (key === void 0) return void 0;
54826
+ try {
54827
+ const cache = await caches.open(CACHE_NAME);
54828
+ const response = await cache.match(key);
54829
+ if (response === void 0) return void 0;
54830
+ const payload = await response.json().catch(() => void 0);
54831
+ if (validPayload(payload) && await this.decode(payload)) return payload;
54832
+ await cache.delete(key);
54833
+ } catch {}
54834
+ }
54835
+ write(scope, ref, payload) {
54836
+ const key = this.key(scope, ref);
54837
+ if (key === void 0 || !validPayload(payload)) return Promise.resolve();
54838
+ const operation = this.writes.then(async () => {
54839
+ if (!await this.decode(payload)) return;
54840
+ const cache = await caches.open(CACHE_NAME);
54841
+ const body = JSON.stringify(payload);
54842
+ const keys = (await cache.keys()).filter((request) => request.url !== key);
54843
+ const sizes = await Promise.all(keys.map(async (request) => Number((await cache.match(request))?.headers.get("Content-Length")) || 0));
54844
+ let bytes = sizes.reduce((sum, size) => sum + size, 0) + body.length;
54845
+ let count = keys.length + 1;
54846
+ for (let index = 0; index < keys.length && (count > MAX_ENTRIES$1 || bytes > MAX_BYTES$1); index++) {
54847
+ await cache.delete(keys[index]);
54848
+ bytes -= sizes[index];
54849
+ count -= 1;
54850
+ }
54851
+ await cache.put(key, new Response(body, { headers: {
54852
+ "Content-Type": "application/json",
54853
+ "Content-Length": String(body.length)
54854
+ } }));
54855
+ });
54856
+ this.writes = operation.catch(() => void 0);
54857
+ return this.writes;
54858
+ }
54859
+ };
54860
+ //#endregion
54861
+ //#region src/avatar-diagnostics.ts
54862
+ function record(value) {
54863
+ return value !== null && typeof value === "object" ? value : {};
54864
+ }
54865
+ function positiveId(value) {
54866
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
54867
+ }
54868
+ /** Decoded IDs are diagnostic hints only, never authentication or authorization inputs. */
54869
+ function avatarReferenceDiagnostic(imageRef) {
54870
+ const ref = imageRef.trim();
54871
+ if (!ref.startsWith("arkme-profile-image-v1.")) return { referenceKind: ref.startsWith("arkme-media-v1.") ? "media" : "other" };
54872
+ const context = { referenceKind: "profile" };
54873
+ try {
54874
+ const parts = ref.split(".");
54875
+ const encoded = parts[1] ?? "";
54876
+ if (parts.length !== 3 || encoded.length > 1024 || !/^[A-Za-z0-9_-]+$/.test(encoded)) return context;
54877
+ const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
54878
+ const payload = record(JSON.parse(globalThis.atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="))));
54879
+ if (payload.version !== 1) return context;
54880
+ const viewer = positiveId(payload.viewerUserId);
54881
+ const target = positiveId(payload.targetUserId);
54882
+ if (viewer !== void 0) context.referenceViewerUserId = viewer;
54883
+ if (target !== void 0) context.referenceTargetUserId = target;
54884
+ } catch {}
54885
+ return context;
54886
+ }
54887
+ function avatarScopeDiagnostic(scopeKey) {
54888
+ const match = /^(prod|test):([1-9][0-9]*)$/.exec(scopeKey ?? "");
54889
+ const viewerUserId = positiveId(Number(match?.[2]));
54890
+ return match === null || viewerUserId === void 0 ? {} : {
54891
+ environment: match[1],
54892
+ viewerUserId
54893
+ };
54894
+ }
54895
+ function safeErrorCode(value) {
54896
+ return typeof value === "string" && /^(?:(?:arkme|image|auth-http|local|profile)-[A-Za-z0-9_-]{1,80}|UND_ERR_[A-Z_]{1,64}|E[A-Z_]{1,64})$/.test(value) ? value : void 0;
54897
+ }
54898
+ function httpStatus(value) {
54899
+ return typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599 ? value : void 0;
54900
+ }
54901
+ function imageDownloadStatus(error) {
54902
+ if (error.name !== "ArkmePluginError" || error.code !== "image-download-failed" || typeof error.message !== "string") return void 0;
54903
+ const match = /^Arkme 图片读取返回 HTTP ([1-5][0-9]{2})$/.exec(error.message);
54904
+ return match === null ? void 0 : httpStatus(Number(match[1]));
54905
+ }
54906
+ function logArkmeAvatarDiagnostic(event, context, error) {
54907
+ try {
54908
+ const source = record(error);
54909
+ const body = record(source.body);
54910
+ const detail = source.name === "ArkmeClientError" ? body : source;
54911
+ const errorName = [
54912
+ "Error",
54913
+ "TypeError",
54914
+ "AbortError",
54915
+ "TimeoutError",
54916
+ "ArkmePluginError",
54917
+ "ArkmeClientError"
54918
+ ].includes(String(source.name)) ? String(source.name) : "unknown";
54919
+ console.warn(`[ArkmeAvatarDiag] ${JSON.stringify({
54920
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
54921
+ event,
54922
+ ...context,
54923
+ ...error === void 0 ? {} : { error: {
54924
+ name: errorName,
54925
+ code: safeErrorCode(detail.code),
54926
+ retryable: typeof detail.retryable === "boolean" ? detail.retryable : void 0,
54927
+ httpStatus: httpStatus(source.httpStatus),
54928
+ upstreamStatus: httpStatus(source.upstreamStatus) ?? imageDownloadStatus(source),
54929
+ causeCode: safeErrorCode(record(source.cause).code)
54930
+ } }
54931
+ })}`);
54932
+ } catch {}
54933
+ }
54934
+ //#endregion
54782
54935
  //#region src/client/avatar-image-runtime.ts
54783
54936
  const arkmeAvatarImages = new InMemoryArkmeAvatarImageStore({
54937
+ persistentCache: new BrowserAvatarPersistentCache(),
54938
+ cachedReader: async (imageRef) => {
54939
+ try {
54940
+ return await callArkme("image.read", {
54941
+ imageRef,
54942
+ cacheOnly: true
54943
+ });
54944
+ } catch (error) {
54945
+ if (error instanceof ArkmeClientError && error.body.code === "image-cache-miss") return void 0;
54946
+ throw error;
54947
+ }
54948
+ },
54784
54949
  reader: async (imageRef) => await callArkme("image.read", { imageRef }),
54785
54950
  onLoadFailure: ({ imageRef, scopeKey, error, ...context }) => {
54786
54951
  logArkmeAvatarDiagnostic("image_load_failed", {
@@ -55011,7 +55176,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55011
55176
  children: fallback.label || "--"
55012
55177
  });
55013
55178
  }
55014
- function ArkmeUserAvatar({ avatarRef, lazy = false, fallback, size = 44, label = "用户头像", senderKind }) {
55179
+ function ArkmeUserAvatar({ avatarRef, lazy = false, fallback, fallbackContent, size = 44, label = "用户头像", senderKind }) {
55015
55180
  const normalizedRef = avatarRef?.trim() ?? "";
55016
55181
  const container = (0, react$1.useRef)(null);
55017
55182
  const imageUrl = useArkmeAvatarImage(normalizedRef, lazy ? container : void 0);
@@ -55029,7 +55194,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55029
55194
  }) : fallback?.kind === "phone_default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneDefaultAvatar, {
55030
55195
  fallback,
55031
55196
  size
55032
- }) : senderKind === "bot" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeBotAvatarFallback, { size }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultUserAvatar, { size })
55197
+ }) : fallbackContent ?? (senderKind === "bot" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeBotAvatarFallback, { size }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultUserAvatar, { size }))
55033
55198
  });
55034
55199
  }
55035
55200
  function GroupAvatarMember({ slot, size }) {
@@ -55526,6 +55691,54 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55526
55691
  return stop;
55527
55692
  }
55528
55693
  //#endregion
55694
+ //#region src/reaction-diagnostics.ts
55695
+ /** Timing metadata only: no credentials, message refs, names or message bodies. */
55696
+ function reactionTraceId(value) {
55697
+ return value && /^[a-f0-9-]{36}$/.test(value) ? value : globalThis.crypto.randomUUID();
55698
+ }
55699
+ function logReactionTiming(traceId, stage, started, detail = {}) {
55700
+ console.info(`[ArkmeReactionTiming] ${JSON.stringify({
55701
+ traceId,
55702
+ stage,
55703
+ at: Date.now(),
55704
+ durationMs: Math.round((performance.now() - started) * 100) / 100,
55705
+ ...detail
55706
+ })}`);
55707
+ }
55708
+ //#endregion
55709
+ //#region src/client/reaction-transport.ts
55710
+ const timings = /* @__PURE__ */ new WeakMap();
55711
+ function traceReactionApplied(value, stage) {
55712
+ const timing = timings.get(value);
55713
+ if (timing) logReactionTiming(timing.traceId, stage, timing.started);
55714
+ }
55715
+ async function callReaction(input, signal) {
55716
+ let diagnostics = false;
55717
+ try {
55718
+ diagnostics = globalThis.sessionStorage?.getItem("arkme.reaction.diagnostics") === "1";
55719
+ } catch {}
55720
+ if (!diagnostics) return await callArkme("reactions", input, signal);
55721
+ const traceId = reactionTraceId(input.traceId), started = performance.now();
55722
+ let ok = false;
55723
+ try {
55724
+ const value = await callArkme("reactions", {
55725
+ ...input,
55726
+ traceId
55727
+ }, signal);
55728
+ ok = true;
55729
+ if (diagnostics && value && typeof value === "object") timings.set(value, {
55730
+ traceId,
55731
+ started
55732
+ });
55733
+ return value;
55734
+ } finally {
55735
+ if (diagnostics) logReactionTiming(traceId, "browser-request", started, {
55736
+ action: input.action,
55737
+ ok
55738
+ });
55739
+ }
55740
+ }
55741
+ //#endregion
55529
55742
  //#region src/client/reaction-label-content.ts
55530
55743
  const reactionHandIds = [
55531
55744
  "ok_hand",
@@ -55778,10 +55991,12 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55778
55991
  targets = /* @__PURE__ */ new Map();
55779
55992
  states = /* @__PURE__ */ new Map();
55780
55993
  inactive = /* @__PURE__ */ new Set();
55994
+ actorRefreshAt = /* @__PURE__ */ new Map();
55781
55995
  knownTargets = /* @__PURE__ */ new Map();
55782
55996
  failures = /* @__PURE__ */ new Map();
55783
55997
  listeners = /* @__PURE__ */ new Set();
55784
55998
  revision = 0;
55999
+ reading;
55785
56000
  refreshing;
55786
56001
  timer;
55787
56002
  timerDue = 0;
@@ -55789,7 +56004,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55789
56004
  retryMillis = 2e3;
55790
56005
  pending;
55791
56006
  writing = false;
55792
- constructor(transport = (input, signal) => callArkme("reactions", input, signal)) {
56007
+ constructor(transport = callReaction) {
55793
56008
  this.transport = transport;
55794
56009
  }
55795
56010
  subscribe = (listener) => {
@@ -55811,6 +56026,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55811
56026
  this.targets.clear();
55812
56027
  this.states.clear();
55813
56028
  this.inactive.clear();
56029
+ this.actorRefreshAt.clear();
55814
56030
  this.knownTargets.clear();
55815
56031
  this.failures.clear();
55816
56032
  this.pending = void 0;
@@ -55823,6 +56039,11 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55823
56039
  }
55824
56040
  watch(scope, target) {
55825
56041
  if (scope !== this.scope) return () => {};
56042
+ const resolver = target.resolveActor ?? this.knownTargets.get(target.id)?.resolveActor;
56043
+ if (resolver) target = {
56044
+ ...target,
56045
+ resolveActor: resolver
56046
+ };
55826
56047
  const existing = this.targets.get(target.id);
55827
56048
  if (!existing && this.targets.size >= 200) {
55828
56049
  this.failures.set(target.id, "请打开消息后刷新表态");
@@ -55851,6 +56072,8 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55851
56072
  this.failures.delete(target.id);
55852
56073
  this.retainInactive(target.id);
55853
56074
  }
56075
+ const reading = this.reading;
56076
+ if (reading && ![...reading.entries].some(([id, entry]) => this.targets.get(id) === entry)) reading.controller.abort();
55854
56077
  if (!this.targets.size) this.stop();
55855
56078
  };
55856
56079
  }
@@ -55862,6 +56085,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55862
56085
  const oldest = this.inactive.values().next().value;
55863
56086
  this.inactive.delete(oldest);
55864
56087
  this.states.delete(oldest);
56088
+ this.actorRefreshAt.delete(oldest);
55865
56089
  this.knownTargets.delete(oldest);
55866
56090
  this.failures.delete(oldest);
55867
56091
  }
@@ -55884,9 +56108,20 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55884
56108
  await this.refresh();
55885
56109
  if (active && !signal.aborted && scope === this.scope && (alreadyReading || missing.some((target) => !this.states.has(target.id) && !this.failures.has(target.id)))) await this.refresh();
55886
56110
  };
56111
+ let unsubscribe = () => {};
56112
+ const factsReady = new Promise((resolve) => {
56113
+ unsubscribe = this.subscribe(() => {
56114
+ if (missing.every((target) => this.states.has(target.id) || this.failures.has(target.id))) resolve();
56115
+ });
56116
+ });
55887
56117
  try {
55888
- await Promise.race([load(), deadline]);
56118
+ await Promise.race([
56119
+ load(),
56120
+ deadline,
56121
+ factsReady
56122
+ ]);
55889
56123
  } finally {
56124
+ unsubscribe();
55890
56125
  active = false;
55891
56126
  clearTimeout(timer);
55892
56127
  signal.removeEventListener("abort", finish);
@@ -55947,11 +56182,17 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55947
56182
  }
55948
56183
  async refresh() {
55949
56184
  if (this.refreshing) return this.refreshing;
55950
- const scope = this.scope, signal = this.controller.signal;
56185
+ const scope = this.scope;
55951
56186
  if (!scope) return;
56187
+ const controller = new AbortController(), entries = new Map(this.targets);
56188
+ const reading = {
56189
+ controller,
56190
+ entries
56191
+ };
56192
+ this.reading = reading;
56193
+ const signal = AbortSignal.any([this.controller.signal, controller.signal]);
55952
56194
  const operation = (async () => {
55953
56195
  let failed = false;
55954
- const entries = new Map(this.targets);
55955
56196
  const targets = [...entries.values()].map((item) => item.target);
55956
56197
  for (let offset = 0; offset < targets.length; offset += 50) {
55957
56198
  const batch = targets.slice(offset, offset + 50);
@@ -55960,7 +56201,8 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55960
56201
  const page = await this.transport({
55961
56202
  action: "query",
55962
56203
  accountKey: scope,
55963
- targets: batch
56204
+ targets: batch,
56205
+ actorPresentation: "deferred"
55964
56206
  }, signal);
55965
56207
  if (signal.aborted) return;
55966
56208
  const returned = new Set(page.items.map((item) => item.target_id));
@@ -55973,8 +56215,25 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55973
56215
  }
55974
56216
  for (const item of page.items) if (entries.has(item.target_id) && this.targets.get(item.target_id) === entries.get(item.target_id)) {
55975
56217
  const previous = this.states.get(item.target_id);
55976
- if (!previous || previous.mine.revision <= item.mine.revision) this.states.set(item.target_id, item);
56218
+ if (!previous || previous.mine.revision <= item.mine.revision) {
56219
+ const known = new Map((Date.now() - (this.actorRefreshAt.get(item.target_id) ?? 0) < 6e4 ? previous?.groups : void 0)?.flatMap((group) => group.actors ?? []).filter((actor) => !actor.presentationPending).map((actor) => [actor.userId, actor]));
56220
+ const resolve = this.targets.get(item.target_id)?.target.resolveActor;
56221
+ this.states.set(item.target_id, {
56222
+ ...item,
56223
+ groups: item.groups.map((group) => ({
56224
+ ...group,
56225
+ actors: (group.actors ?? []).map((actor) => resolve?.(actor) ?? (actor.presentationPending && known.has(actor.userId) ? {
56226
+ ...actor,
56227
+ ...known.get(actor.userId),
56228
+ ...actor.memberRef ? { memberRef: actor.memberRef } : {},
56229
+ presentationPending: false
56230
+ } : actor))
56231
+ }))
56232
+ });
56233
+ }
55977
56234
  }
56235
+ if (before !== JSON.stringify(batch.map((t) => [this.states.get(t.id), this.failures.get(t.id)]))) this.publish();
56236
+ traceReactionApplied(page, "browser-facts-published");
55978
56237
  } catch (error) {
55979
56238
  if (signal.aborted) return;
55980
56239
  failed = true;
@@ -55983,7 +56242,52 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55983
56242
  this.failures.set(target.id, error instanceof Error ? error.message : "表态加载失败");
55984
56243
  }
55985
56244
  }
55986
- if (!signal.aborted && before !== JSON.stringify(batch.map((t) => [this.states.get(t.id), this.failures.get(t.id)]))) this.publish();
56245
+ if (failed && !signal.aborted && before !== JSON.stringify(batch.map((t) => [this.states.get(t.id), this.failures.get(t.id)]))) this.publish();
56246
+ const missing = batch.filter((target) => this.states.get(target.id)?.groups.some((group) => group.actors?.some((actor) => actor.presentationPending)));
56247
+ if (missing.length && !signal.aborted) {
56248
+ const initial = new Map(missing.map((target) => [target.id, this.states.get(target.id)]));
56249
+ try {
56250
+ const enriched = await this.transport({
56251
+ action: "query",
56252
+ accountKey: scope,
56253
+ targets: missing
56254
+ }, signal);
56255
+ if (signal.aborted) return;
56256
+ for (const item of enriched.items) {
56257
+ const current = this.states.get(item.target_id);
56258
+ if (!current || current !== initial.get(item.target_id) || this.targets.get(item.target_id) !== entries.get(item.target_id)) continue;
56259
+ this.actorRefreshAt.set(item.target_id, Date.now());
56260
+ if (!item.actors_visible) {
56261
+ this.states.set(item.target_id, {
56262
+ ...current,
56263
+ actors_visible: false,
56264
+ private: item.private,
56265
+ groups: current.groups.map((group) => ({
56266
+ ...group,
56267
+ actors: []
56268
+ }))
56269
+ });
56270
+ continue;
56271
+ }
56272
+ const actors = new Map(item.groups.flatMap((group) => group.actors ?? []).map((actor) => [actor.userId, actor]));
56273
+ const resolve = this.targets.get(item.target_id)?.target.resolveActor;
56274
+ this.states.set(item.target_id, {
56275
+ ...current,
56276
+ groups: current.groups.map((group) => ({
56277
+ ...group,
56278
+ actors: (group.actors ?? []).map((actor) => resolve?.(actor) ?? (actor.presentationPending && actors.has(actor.userId) ? {
56279
+ ...actor,
56280
+ ...actors.get(actor.userId),
56281
+ ...actor.memberRef ? { memberRef: actor.memberRef } : {},
56282
+ presentationPending: false
56283
+ } : actor))
56284
+ }))
56285
+ });
56286
+ }
56287
+ this.publish();
56288
+ traceReactionApplied(enriched, "browser-actors-published");
56289
+ } catch {}
56290
+ }
55987
56291
  }
55988
56292
  if (!signal.aborted) this.retryMillis = failed ? Math.min(12e4, this.retryMillis * 2) : 2e3;
55989
56293
  })();
@@ -55991,6 +56295,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
55991
56295
  try {
55992
56296
  await operation;
55993
56297
  } finally {
56298
+ if (this.reading === reading) this.reading = void 0;
55994
56299
  if (this.refreshing === operation) {
55995
56300
  this.refreshing = void 0;
55996
56301
  if (this.refreshAgain) {
@@ -56158,7 +56463,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
56158
56463
  users = 0;
56159
56464
  error = "";
56160
56465
  hasMore = false;
56161
- constructor(transport = (input, signal) => callArkme("reactions", input, signal)) {
56466
+ constructor(transport = callReaction) {
56162
56467
  this.transport = transport;
56163
56468
  }
56164
56469
  subscribe = (fn) => {
@@ -56279,6 +56584,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
56279
56584
  limit: 50
56280
56585
  }, signal);
56281
56586
  if (signal.aborted) return;
56587
+ traceReactionApplied(page, "browser-notifications-received");
56282
56588
  rows.push(...page.items);
56283
56589
  hasMore = page.has_more;
56284
56590
  if (!page.has_more) break;
@@ -56940,30 +57246,51 @@ body[data-ds-dark-theme] .arkme-bot-identity {
56940
57246
  }
56941
57247
  };
56942
57248
  //#endregion
56943
- //#region src/client/recognized-speaker-tracker.ts
56944
- const empty$4 = { checkedAt: 0 };
56945
- const storageKey$4 = (account) => `dsh-arkme:speaker-visits:v1:${account}`;
56946
- const storage$1 = () => {
56947
- try {
56948
- return window.localStorage;
56949
- } catch {
56950
- return;
57249
+ //#region src/client/recognized-speaker-directory.ts
57250
+ const loaders = {
57251
+ summary: (input, signal) => callArkme("speaker-directory.summary", input, signal),
57252
+ list: (input, signal) => callArkme("speaker-directory.list", { ...input }, signal),
57253
+ seen: (throughCursor, signal) => callArkme("speaker-directory.seen", { throughCursor }, signal),
57254
+ open: (detailRef, signal) => callArkme("speaker-directory.open", { detailRef }, signal),
57255
+ avatars: (detailRefs, signal) => callArkme("speaker-directory.avatars", { detailRefs }, signal)
57256
+ };
57257
+ var SpeakerReadDeferred = class extends Error {
57258
+ retryAt;
57259
+ constructor(retryAt, cause) {
57260
+ super(cause instanceof Error ? cause.message : "说话人目录暂不可用", { cause });
57261
+ this.retryAt = retryAt;
56951
57262
  }
56952
57263
  };
56953
- function markedSpeakerKeys(options) {
56954
- return [...new Set(options.filter((option) => option.kind === "speaker").map((option) => `m:${option.personKey || option.optionKey}`))];
57264
+ function directoryErrorCode(error) {
57265
+ const value = error;
57266
+ return value?.body?.code ?? value?.code ?? "";
56955
57267
  }
56956
- /** Only complete identity snapshots can establish a baseline or produce a precise delta.
56957
- * A removed identity may be a merge/recluster; without lineage we suppress ambiguous
56958
- * additions for that transition rather than presenting a false "new person".
56959
- */
56960
- var RecognizedSpeakerTracker = class {
56961
- getStorage;
56962
- records = /* @__PURE__ */ new Map();
56963
- snapshots = /* @__PURE__ */ new Map();
57268
+ function speakerRetryDelay(error) {
57269
+ if (error instanceof SpeakerReadDeferred) return Math.max(1e3, error.retryAt - Date.now());
57270
+ const value = error;
57271
+ if (directoryErrorCode(error) === "arkme-code-1001" || value?.body?.retryable === false || value?.retryable === false) return void 0;
57272
+ return Math.max(1e3, value?.body?.retryAfterMillis ?? value?.retryAfterMillis ?? 15e3);
57273
+ }
57274
+ const directoryVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
57275
+ const normalizeDirectoryQuery = (query) => query.trim().toLowerCase();
57276
+ function directoryQueryValid(query) {
57277
+ return new TextEncoder().encode(normalizeDirectoryQuery(query)).length <= 256;
57278
+ }
57279
+ const queryKey = (query) => JSON.stringify([
57280
+ query.filter,
57281
+ query.sort,
57282
+ normalizeDirectoryQuery(query.query)
57283
+ ]);
57284
+ const empty$4 = Object.freeze({});
57285
+ /** Shared summary and immutable same-version pages. No traversal or local new-person inference. */
57286
+ var RecognizedSpeakerDirectory = class {
57287
+ api;
57288
+ now;
57289
+ accounts = /* @__PURE__ */ new Map();
56964
57290
  listeners = /* @__PURE__ */ new Set();
56965
- constructor(getStorage = storage$1) {
56966
- this.getStorage = getStorage;
57291
+ constructor(api = loaders, now = Date.now) {
57292
+ this.api = api;
57293
+ this.now = now;
56967
57294
  }
56968
57295
  subscribe = (listener) => {
56969
57296
  this.listeners.add(listener);
@@ -56971,439 +57298,347 @@ body[data-ds-dark-theme] .arkme-bot-identity {
56971
57298
  this.listeners.delete(listener);
56972
57299
  };
56973
57300
  };
56974
- get = (account) => account === void 0 ? empty$4 : this.snapshots.get(account) ?? empty$4;
56975
- emit(account, snapshot) {
56976
- this.snapshots.set(account, snapshot);
56977
- for (const listener of this.listeners) listener();
56978
- }
56979
- read(account) {
56980
- if (this.records.has(account)) return this.records.get(account);
56981
- try {
56982
- const value = JSON.parse(this.getStorage()?.getItem(storageKey$4(account)) ?? "null");
56983
- if (value !== null && [
56984
- "known",
56985
- "current",
56986
- "pending"
56987
- ].every((key) => {
56988
- const list = value[key];
56989
- return Array.isArray(list) && list.length <= 2e4 && list.every((item) => typeof item === "string" && item.length <= 256);
56990
- })) {
56991
- this.records.set(account, value);
56992
- return value;
56993
- }
56994
- } catch {}
56995
- }
56996
- save(account, value) {
56997
- this.records.set(account, value);
56998
- try {
56999
- this.getStorage()?.setItem(storageKey$4(account), JSON.stringify(value));
57000
- } catch {}
57001
- }
57002
- setTotal(account, total) {
57003
- if (!Number.isSafeInteger(total) || total < 0) return;
57004
- const previous = this.get(account);
57005
- this.emit(account, previous.total === total ? {
57006
- ...previous,
57007
- total
57008
- } : {
57009
- total,
57010
- checkedAt: 0
57011
- });
57301
+ get = (account) => account === void 0 ? empty$4 : this.accounts.get(account)?.snapshot ?? empty$4;
57302
+ emit() {
57303
+ this.listeners.forEach((listener) => listener());
57012
57304
  }
57013
- uncertain(account) {
57014
- const previous = this.get(account);
57015
- this.emit(account, {
57016
- ...previous.total === void 0 ? {} : { total: previous.total },
57017
- checkedAt: 0
57018
- });
57305
+ account(key) {
57306
+ let found = this.accounts.get(key);
57307
+ if (!found) {
57308
+ found = {
57309
+ controller: new AbortController(),
57310
+ snapshot: empty$4,
57311
+ summaryAt: 0,
57312
+ retryAt: 0,
57313
+ failures: 0,
57314
+ flights: /* @__PURE__ */ new Map(),
57315
+ lists: /* @__PURE__ */ new Map(),
57316
+ pendingSeen: /* @__PURE__ */ new Set(),
57317
+ confirmedSeen: /* @__PURE__ */ new Set(),
57318
+ expiredSeen: /* @__PURE__ */ new Set(),
57319
+ revision: 0
57320
+ };
57321
+ this.accounts.set(key, found);
57322
+ }
57323
+ return found;
57019
57324
  }
57020
- acknowledge(account) {
57021
- const previous = this.read(account);
57022
- if (previous === void 0) return;
57023
- this.save(account, {
57024
- ...previous,
57025
- pending: []
57026
- });
57027
- this.emit(account, {
57028
- ...this.get(account),
57029
- newCount: 0
57325
+ current(account, value) {
57326
+ value.controller.signal.throwIfAborted();
57327
+ if (this.accounts.get(account) !== value) throw new DOMException("账号已切换", "AbortError");
57328
+ }
57329
+ async request(account, key, load) {
57330
+ const value = this.account(account);
57331
+ this.current(account, value);
57332
+ key = `${value.revision}:${key}`;
57333
+ const pending = value.flights.get(key);
57334
+ if (pending) return pending;
57335
+ if (this.now() < value.retryAt) throw new SpeakerReadDeferred(value.retryAt, /* @__PURE__ */ new Error("请求较频繁或网络暂不可用,请稍后重试"));
57336
+ const promise = Promise.resolve().then(() => load(value.controller.signal)).then((result) => {
57337
+ this.current(account, value);
57338
+ value.failures = 0;
57339
+ return result;
57340
+ }).catch((error) => {
57341
+ this.current(account, value);
57342
+ const delay = speakerRetryDelay(error);
57343
+ if (delay !== void 0) {
57344
+ value.failures += 1;
57345
+ value.retryAt = this.now() + Math.max(delay, Math.min(3e5, 1e3 * 2 ** Math.min(8, value.failures)));
57346
+ throw new SpeakerReadDeferred(value.retryAt, error);
57347
+ }
57348
+ throw error;
57349
+ }).finally(() => {
57350
+ if (value.flights.get(key) === promise) value.flights.delete(key);
57030
57351
  });
57352
+ value.flights.set(key, promise);
57353
+ return promise;
57031
57354
  }
57032
- observe(account, options, candidates, viewed = false) {
57033
- const marked = markedSpeakerKeys(options);
57034
- if (candidates.some((candidate) => !candidate.identityKey)) {
57035
- this.uncertain(account);
57036
- return;
57355
+ async summary(account, force = false) {
57356
+ const state = this.account(account), cached = state.snapshot.summary, revision = state.revision;
57357
+ if (!force && cached && (cached.state === "disabled" || this.now() < state.summaryAt)) return cached;
57358
+ try {
57359
+ const result = await this.request(account, "summary", (signal) => this.api.summary(cached && cached.snapshotVersion ? {
57360
+ snapshotVersion: cached.snapshotVersion,
57361
+ seenVersion: cached.seenVersion
57362
+ } : {}, signal));
57363
+ this.current(account, state);
57364
+ if (revision !== state.revision) return this.summary(account, force);
57365
+ state.snapshot = { summary: result };
57366
+ state.summaryAt = this.now() + (result.retryAfterMs > 0 ? result.retryAfterMs : 3e4);
57367
+ this.emit();
57368
+ return result;
57369
+ } catch (error) {
57370
+ this.current(account, state);
57371
+ state.snapshot = {
57372
+ ...state.snapshot,
57373
+ error: error instanceof Error ? error.message : "目录暂不可用"
57374
+ };
57375
+ this.emit();
57376
+ throw error;
57037
57377
  }
57038
- const current = [.../* @__PURE__ */ new Set([...marked, ...candidates.map((candidate) => `u:${candidate.identityKey}`)])];
57039
- const previous = this.read(account);
57040
- const known = new Set(previous?.known ?? []);
57041
- const currentSet = new Set(current);
57042
- const removed = previous?.current.some((key) => !currentSet.has(key)) ?? false;
57043
- const additions = previous === void 0 || removed ? [] : current.filter((key) => !known.has(key));
57044
- const pending = viewed ? [] : [.../* @__PURE__ */ new Set([...(previous?.pending ?? []).filter((key) => currentSet.has(key)), ...additions])];
57045
- current.forEach((key) => known.add(key));
57046
- const record = known.size > 2e4 ? {
57047
- known: current,
57048
- current,
57049
- pending: []
57050
- } : {
57051
- known: [...known],
57052
- current,
57053
- pending
57054
- };
57055
- this.save(account, record);
57056
- this.emit(account, {
57057
- total: current.length,
57058
- newCount: record.pending.length,
57059
- checkedAt: Date.now()
57060
- });
57061
57378
  }
57062
- clearSession() {
57063
- this.snapshots.clear();
57064
- this.records.clear();
57065
- for (const listener of this.listeners) listener();
57379
+ peek(account, query) {
57380
+ return this.accounts.get(account)?.lists.get(queryKey(query));
57066
57381
  }
57067
- };
57068
- const recognizedSpeakerTracker = new RecognizedSpeakerTracker();
57069
- let activeAccount$1 = "";
57070
- arkmeAuthStore.subscribe(() => {
57071
- const auth = arkmeAuthStore.getSnapshot().auth;
57072
- const next = auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : "";
57073
- if (next !== activeAccount$1) {
57074
- activeAccount$1 = next;
57075
- recognizedSpeakerTracker.clearSession();
57076
- }
57077
- });
57078
- //#endregion
57079
- //#region src/client/recognized-speaker-order.ts
57080
- const preferenceKey$3 = (accountKey) => `dsh-arkme:recognized-speaker-order:v1:${accountKey}`;
57081
- function browserStorage$1() {
57082
- try {
57083
- return typeof window === "undefined" ? void 0 : window.localStorage;
57084
- } catch {
57085
- return;
57086
- }
57087
- }
57088
- function readRecognizedSpeakerOrder(accountKey, storage = browserStorage$1()) {
57089
- try {
57090
- return storage?.getItem(preferenceKey$3(accountKey)) === "recent" ? "recent" : "frequent";
57091
- } catch {
57092
- return "frequent";
57093
- }
57094
- }
57095
- function writeRecognizedSpeakerOrder(accountKey, order, storage = browserStorage$1()) {
57096
- try {
57097
- storage?.setItem(preferenceKey$3(accountKey), order);
57098
- } catch {}
57099
- }
57100
- function positiveNumber(value) {
57101
- return value !== void 0 && Number.isFinite(value) && value > 0 ? value : 0;
57102
- }
57103
- /** Missing statistics sort last. Ties remain stable across refreshes and filters. */
57104
- function compareRecognizedSpeakers(left, right, order) {
57105
- const leftDays = positiveNumber(left.dayCount), rightDays = positiveNumber(right.dayCount);
57106
- const leftTime = positiveNumber(left.lastSeenAt), rightTime = positiveNumber(right.lastSeenAt);
57107
- return (order === "frequent" ? rightDays - leftDays || rightTime - leftTime : rightTime - leftTime || rightDays - leftDays) || left.name.localeCompare(right.name, void 0, { numeric: true }) || left.key.localeCompare(right.key);
57108
- }
57109
- /** The upstream cursor has a fixed order. Read metadata pages before claiming global order.
57110
- * Bound reads to the host's 2,000-candidate reference budget; never silently call a partial
57111
- * traversal complete. No audio or per-speaker detail is loaded here.
57112
- */
57113
- async function loadSpeakerCandidateSnapshot(loadPage, signal, onProgress) {
57114
- for (let attempt = 0; attempt < 2; attempt += 1) {
57115
- const rows = /* @__PURE__ */ new Map();
57116
- const cursors = /* @__PURE__ */ new Set();
57117
- let cursor = "";
57118
- let snapshot = {
57382
+ async first(account, query, signal, force = false) {
57383
+ if (!directoryQueryValid(query.query)) throw new Error("搜索内容过长,请缩短至 256 字节以内");
57384
+ signal.throwIfAborted();
57385
+ const state = this.account(account), revision = state.revision;
57386
+ const summary = await this.summary(account, force);
57387
+ signal.throwIfAborted();
57388
+ const key = queryKey(query), cached = state.lists.get(key);
57389
+ if (!force && cached && cached.snapshotVersion === summary.snapshotVersion && !state.expiredSeen.has(cached.throughCursor)) return cached;
57390
+ const input = {
57391
+ ...query,
57392
+ query: normalizeDirectoryQuery(query.query),
57393
+ cursor: "",
57394
+ snapshotVersion: summary.snapshotVersion,
57395
+ limit: 50
57396
+ };
57397
+ const page = summary.state === "disabled" || summary.coverage !== "complete" ? {
57398
+ ...summary,
57119
57399
  items: [],
57120
- complete: false,
57121
- projectionState: void 0
57400
+ hasMore: false,
57401
+ nextCursor: "",
57402
+ throughCursor: ""
57403
+ } : await this.request(account, `list:${JSON.stringify(input)}`, (inner) => this.api.list(input, inner));
57404
+ signal.throwIfAborted();
57405
+ this.current(account, state);
57406
+ if (revision !== state.revision) throw new DOMException("目录已更新", "AbortError");
57407
+ if (page.state === "snapshot_expired") {
57408
+ state.summaryAt = 0;
57409
+ state.lists.delete(key);
57410
+ if (page.retryAfterMs > 0) {
57411
+ state.retryAt = Math.max(state.retryAt, this.now() + page.retryAfterMs);
57412
+ throw new SpeakerReadDeferred(state.retryAt, /* @__PURE__ */ new Error("目录正在更新,请稍后重试"));
57413
+ }
57414
+ if (force) throw new Error("目录正在更新,请稍后刷新");
57415
+ return this.first(account, query, signal, true);
57416
+ }
57417
+ if (page.state === "disabled") {
57418
+ state.snapshot = { summary: {
57419
+ ...summary,
57420
+ state: "disabled"
57421
+ } };
57422
+ this.emit();
57423
+ }
57424
+ const result = {
57425
+ ...page,
57426
+ query: {
57427
+ ...query,
57428
+ query: input.query
57429
+ },
57430
+ requestedCursors: [""]
57122
57431
  };
57123
- for (let pageIndex = 0; pageIndex < 40; pageIndex += 1) {
57124
- signal.throwIfAborted();
57125
- const page = await loadPage(cursor, signal);
57126
- signal.throwIfAborted();
57127
- if (page.section !== "unmarked-speakers") throw new Error("未标记说话人列表响应无效");
57128
- if (page.cursorStale) {
57129
- snapshot = {
57130
- items: [],
57131
- complete: false,
57132
- projectionState: "stale"
57133
- };
57134
- onProgress(snapshot);
57135
- if (attempt === 0) break;
57136
- return snapshot;
57137
- }
57138
- for (const item of page.items) if (item.kind === "unmarked-speaker" && rows.size < 2e3) rows.set(item.candidateRef, item);
57139
- snapshot = {
57140
- items: [...rows.values()],
57141
- complete: !page.hasMore && page.coverage !== "partial" && (page.projectionState === void 0 || page.projectionState === "fresh"),
57142
- projectionState: page.projectionState
57143
- };
57144
- onProgress(snapshot);
57145
- if (!page.hasMore || rows.size >= 2e3 || page.projectionState !== void 0 && page.projectionState !== "fresh") return snapshot;
57146
- const next = page.nextCursor ?? "";
57147
- if (next === "" || cursors.has(next)) return snapshot;
57148
- cursors.add(next);
57149
- cursor = next;
57150
- if (pageIndex === 39) return snapshot;
57432
+ if (page.coverage === "complete" && page.state !== "disabled") {
57433
+ state.lists.delete(key);
57434
+ state.lists.set(key, result);
57435
+ while (state.lists.size > 12) state.lists.delete(state.lists.keys().next().value);
57151
57436
  }
57437
+ return result;
57152
57438
  }
57153
- return {
57154
- items: [],
57155
- complete: false,
57156
- projectionState: "stale"
57157
- };
57158
- }
57159
- //#endregion
57160
- //#region src/client/recognized-speaker-directory.ts
57161
- const readSpeakerOptions = (signal) => callArkme("recordings.speaker.options", {}, signal);
57162
- const readSpeakerPresence = (signal) => callArkme("recordings.speaker.presence", {}, signal);
57163
- const readSpeakerPage = (cursor, signal) => callArkme("directory.list", {
57164
- section: "unmarked-speakers",
57165
- limit: 50,
57166
- ...cursor === "" ? {} : { cursor }
57167
- }, signal);
57168
- var SpeakerReadDeferred = class extends Error {
57169
- retryAt;
57170
- constructor(retryAt, cause) {
57171
- super(cause instanceof Error ? cause.message : "说话人列表暂时无法加载", { cause });
57172
- this.retryAt = retryAt;
57173
- }
57174
- };
57175
- function speakerRetryDelay(error) {
57176
- return error instanceof SpeakerReadDeferred ? Math.max(1e3, error.retryAt - Date.now()) : void 0;
57177
- }
57178
- /** Per-account shared read. A short release grace lets entry -> list hand off the same
57179
- * request; leaving both cancels it. A late/aborted response can never update the cache.
57180
- */
57181
- var SharedRead = class {
57182
- ttl;
57183
- load;
57184
- now;
57185
- value;
57186
- freshUntil = 0;
57187
- retryAt = 0;
57188
- failures = 0;
57189
- error;
57190
- pending;
57191
- listeners = /* @__PURE__ */ new Set();
57192
- releaseTimer;
57193
- constructor(ttl, load, now) {
57194
- this.ttl = ttl;
57195
- this.load = load;
57196
- this.now = now;
57197
- }
57198
- read(signal, progress = () => {}) {
57439
+ async more(account, previous, signal) {
57199
57440
  signal.throwIfAborted();
57200
- if (this.value !== void 0) progress(this.value);
57201
- if (this.now() < this.retryAt) return Promise.reject(new SpeakerReadDeferred(this.retryAt, this.error));
57202
- if (this.value !== void 0 && this.now() < this.freshUntil) return Promise.resolve(this.value);
57203
- clearTimeout(this.releaseTimer);
57204
- if (this.pending?.controller.signal.aborted) this.pending = void 0;
57205
- if (this.pending === void 0) {
57206
- const controller = new AbortController();
57207
- const emit = (value) => {
57208
- controller.signal.throwIfAborted();
57209
- this.value = value;
57210
- for (const listener of this.listeners) listener(value);
57211
- };
57212
- const promise = Promise.resolve().then(async () => {
57213
- controller.signal.throwIfAborted();
57214
- const value = await this.load(controller.signal, emit);
57215
- controller.signal.throwIfAborted();
57216
- emit(value);
57217
- this.freshUntil = this.now() + this.ttl;
57218
- this.failures = 0;
57219
- this.retryAt = 0;
57220
- this.error = void 0;
57221
- return value;
57222
- }).catch((error) => {
57223
- if (controller.signal.aborted) throw error;
57224
- this.failures += 1;
57225
- const limited = /(?:\b429\b|rate.?limit|too many requests)/i.test(error instanceof Error ? error.message : String(error));
57226
- this.retryAt = this.now() + Math.min(5 * 6e4, (limited ? 6e4 : 15e3) * 2 ** Math.min(4, this.failures - 1));
57227
- this.error = error;
57228
- throw new SpeakerReadDeferred(this.retryAt, error);
57229
- }).finally(() => {
57230
- if (this.pending?.controller === controller) this.pending = void 0;
57231
- });
57232
- this.pending = {
57233
- controller,
57234
- promise
57441
+ if (!previous.hasMore) return previous;
57442
+ const state = this.account(account), revision = state.revision;
57443
+ if (!state.lists.has(queryKey(previous.query))) return this.first(account, previous.query, signal, true);
57444
+ const input = {
57445
+ ...previous.query,
57446
+ limit: 50,
57447
+ cursor: previous.nextCursor,
57448
+ snapshotVersion: previous.snapshotVersion
57449
+ };
57450
+ const page = await this.request(account, `list:${JSON.stringify(input)}`, (inner) => this.api.list(input, inner));
57451
+ signal.throwIfAborted();
57452
+ this.current(account, state);
57453
+ if (revision !== state.revision) throw new DOMException("目录已更新", "AbortError");
57454
+ if (page.state === "snapshot_expired") {
57455
+ state.lists.delete(queryKey(previous.query));
57456
+ state.summaryAt = 0;
57457
+ if (page.retryAfterMs > 0) {
57458
+ state.retryAt = Math.max(state.retryAt, this.now() + page.retryAfterMs);
57459
+ throw new SpeakerReadDeferred(state.retryAt, /* @__PURE__ */ new Error("目录正在更新,请稍后刷新"));
57460
+ }
57461
+ return this.first(account, previous.query, signal, true);
57462
+ }
57463
+ if (page.state === "disabled") {
57464
+ const summary = state.snapshot.summary;
57465
+ if (summary) {
57466
+ state.snapshot = { summary: {
57467
+ ...summary,
57468
+ state: "disabled"
57469
+ } };
57470
+ this.emit();
57471
+ }
57472
+ return {
57473
+ ...previous,
57474
+ state: "disabled",
57475
+ hasMore: false
57235
57476
  };
57236
57477
  }
57237
- const pending = this.pending;
57238
- return new Promise((resolve, reject) => {
57239
- const listener = (value) => {
57240
- if (!signal.aborted) progress(value);
57241
- };
57242
- this.listeners.add(listener);
57243
- const cleanup = () => {
57244
- signal.removeEventListener("abort", abort);
57245
- this.listeners.delete(listener);
57246
- if (this.listeners.size === 0 && this.pending === pending) this.releaseTimer = setTimeout(() => {
57247
- if (this.listeners.size === 0 && this.pending === pending) pending.controller.abort();
57248
- }, 250);
57249
- };
57250
- const abort = () => {
57251
- cleanup();
57252
- reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
57253
- };
57254
- signal.addEventListener("abort", abort, { once: true });
57255
- pending.promise.then((value) => {
57256
- cleanup();
57257
- if (!signal.aborted) resolve(value);
57258
- }, (error) => {
57259
- cleanup();
57260
- if (!signal.aborted) reject(error);
57261
- });
57262
- });
57263
- }
57264
- invalidate() {
57265
- clearTimeout(this.releaseTimer);
57266
- this.pending?.controller.abort();
57267
- this.pending = void 0;
57268
- this.freshUntil = 0;
57269
- }
57270
- dispose() {
57271
- this.invalidate();
57272
- this.listeners.clear();
57273
- this.value = void 0;
57274
- }
57275
- };
57276
- function pause(milliseconds, signal) {
57277
- if (milliseconds <= 0) return Promise.resolve();
57278
- return new Promise((resolve, reject) => {
57279
- const abort = () => {
57280
- clearTimeout(timer);
57281
- reject(signal.reason);
57478
+ if (page.snapshotVersion !== previous.snapshotVersion || page.coverage !== "complete" || page.hasMore && (page.nextCursor === previous.nextCursor || previous.requestedCursors.includes(page.nextCursor))) throw new Error("目录版本已变化,请刷新列表");
57479
+ const keys = new Set(previous.items.map((item) => item.personKey));
57480
+ const result = {
57481
+ ...page,
57482
+ query: previous.query,
57483
+ requestedCursors: [...previous.requestedCursors, previous.nextCursor],
57484
+ items: [...previous.items, ...page.items.filter((item) => {
57485
+ if (keys.has(item.personKey)) return false;
57486
+ keys.add(item.personKey);
57487
+ return true;
57488
+ })]
57282
57489
  };
57283
- const timer = setTimeout(() => {
57284
- signal.removeEventListener("abort", abort);
57285
- resolve();
57286
- }, milliseconds);
57287
- signal.addEventListener("abort", abort, { once: true });
57288
- if (signal.aborted) abort();
57289
- });
57290
- }
57291
- var RecognizedSpeakerDirectory = class {
57292
- loaders;
57293
- pageGapMs;
57294
- now;
57295
- accounts = /* @__PURE__ */ new Map();
57296
- constructor(loaders = {
57297
- marked: readSpeakerOptions,
57298
- page: readSpeakerPage,
57299
- presence: readSpeakerPresence
57300
- }, pageGapMs = 750, now = Date.now) {
57301
- this.loaders = loaders;
57302
- this.pageGapMs = pageGapMs;
57303
- this.now = now;
57490
+ state.lists.set(queryKey(previous.query), result);
57491
+ return result;
57304
57492
  }
57305
- account(account) {
57306
- const found = this.accounts.get(account);
57307
- if (found !== void 0) return found;
57308
- const pages = /* @__PURE__ */ new Map();
57309
- let startedAt = 0, lastRequestAt = 0, completed = false;
57310
- let state;
57311
- state = {
57312
- pages,
57313
- marked: new SharedRead(6e4, (signal) => this.loaders.marked(signal), this.now),
57314
- presence: new SharedRead(6e4, (signal) => this.loaders.presence(signal), this.now),
57315
- candidates: new SharedRead(5 * 6e4, async (signal, emit) => {
57316
- if (completed || this.now() - startedAt >= 5 * 6e4) pages.clear();
57317
- if (pages.size === 0) startedAt = this.now();
57318
- completed = false;
57319
- let total;
57320
- const result = await loadSpeakerCandidateSnapshot(async (cursor) => {
57321
- const cached = pages.get(cursor);
57322
- if (cached !== void 0) return cached;
57323
- await pause(Math.max(0, lastRequestAt + this.pageGapMs - this.now()), signal);
57324
- signal.throwIfAborted();
57325
- lastRequestAt = this.now();
57326
- const page = await this.loaders.page(cursor, signal);
57327
- signal.throwIfAborted();
57328
- if (page.cursorStale) {
57329
- pages.clear();
57330
- startedAt = this.now();
57331
- } else if (page.projectionState === void 0 || page.projectionState === "fresh") pages.set(cursor, page);
57332
- return page;
57333
- }, signal, (snapshot) => {
57334
- const first = pages.get("");
57335
- total = first?.coverage !== "partial" ? first?.total : void 0;
57336
- if (!state.candidates.value?.complete || snapshot.complete) emit({
57337
- ...snapshot,
57338
- total
57339
- });
57340
- });
57341
- completed = result.complete;
57342
- if (!result.complete) {
57343
- if (!state.candidates.value?.complete) emit({
57344
- ...result,
57345
- total
57346
- });
57347
- throw new Error("说话人列表尚未完整,请稍后重试");
57493
+ async confirmDisplayed(account, list) {
57494
+ if (!directoryVisible() || list.query.filter !== "all" || list.query.query !== "" || list.coverage !== "complete" || ![
57495
+ "fresh",
57496
+ "stale",
57497
+ "failed"
57498
+ ].includes(list.state) || !list.throughCursor) return;
57499
+ const state = this.account(account);
57500
+ if (state.confirmedSeen.has(list.throughCursor) || state.expiredSeen.has(list.throughCursor) || state.snapshot.summary?.state === "disabled") return;
57501
+ state.pendingSeen.add(list.throughCursor);
57502
+ await this.flushSeen(account);
57503
+ }
57504
+ async flushSeen(account) {
57505
+ const state = this.account(account);
57506
+ if (!directoryVisible() || state.snapshot.summary?.state === "disabled") return;
57507
+ for (const cursor of state.pendingSeen) {
57508
+ let result;
57509
+ try {
57510
+ result = await this.request(account, `seen:${cursor}`, (signal) => this.api.seen(cursor, signal));
57511
+ } catch (error) {
57512
+ if (directoryErrorCode(error) === "arkme-code-1001") {
57513
+ state.pendingSeen.delete(cursor);
57514
+ state.expiredSeen.add(cursor);
57515
+ state.lists.clear();
57516
+ state.summaryAt = 0;
57517
+ this.emit();
57518
+ continue;
57348
57519
  }
57349
- return {
57350
- ...result,
57351
- total
57352
- };
57353
- }, this.now)
57354
- };
57355
- this.accounts.set(account, state);
57356
- return state;
57357
- }
57358
- peekMarked(account) {
57359
- return this.accounts.get(account)?.marked.value;
57360
- }
57361
- peekPresence(account) {
57362
- return this.accounts.get(account)?.presence.value;
57520
+ throw error;
57521
+ }
57522
+ this.current(account, state);
57523
+ if (result.success) {
57524
+ state.pendingSeen.delete(cursor);
57525
+ state.confirmedSeen.add(cursor);
57526
+ await Promise.allSettled([...state.flights.entries()].filter(([key]) => key.endsWith(":summary")).map(([, promise]) => promise));
57527
+ await this.summary(account, true);
57528
+ } else if (result.state === "snapshot_expired") {
57529
+ state.pendingSeen.delete(cursor);
57530
+ state.expiredSeen.add(cursor);
57531
+ state.lists.clear();
57532
+ state.summaryAt = 0;
57533
+ if (result.retryAfterMs > 0) state.retryAt = Math.max(state.retryAt, this.now() + result.retryAfterMs);
57534
+ this.emit();
57535
+ } else {
57536
+ state.pendingSeen.clear();
57537
+ if (state.snapshot.summary) state.snapshot = { summary: {
57538
+ ...state.snapshot.summary,
57539
+ state: "disabled"
57540
+ } };
57541
+ this.emit();
57542
+ return;
57543
+ }
57544
+ }
57363
57545
  }
57364
- peekCandidates(account) {
57365
- return this.accounts.get(account)?.candidates.value;
57546
+ hasPendingSeen(account) {
57547
+ return (this.accounts.get(account)?.pendingSeen.size ?? 0) > 0;
57366
57548
  }
57367
- readMarked(account, signal) {
57368
- return this.account(account).marked.read(signal);
57549
+ needsReload(account, list) {
57550
+ return this.accounts.get(account)?.expiredSeen.has(list.throughCursor) === true;
57369
57551
  }
57370
- readPresence(account, signal) {
57371
- return this.account(account).presence.read(signal);
57552
+ open(account, ref, signal) {
57553
+ signal.throwIfAborted();
57554
+ return this.request(account, `open:${ref}`, (inner) => this.api.open(ref, inner)).then((value) => {
57555
+ signal.throwIfAborted();
57556
+ return value;
57557
+ });
57372
57558
  }
57373
- refreshPresence(account, signal) {
57374
- this.account(account).presence.invalidate();
57375
- return this.readPresence(account, signal);
57559
+ async avatars(account, detailRefs, signal) {
57560
+ const state = this.account(account);
57561
+ const result = await this.api.avatars(detailRefs, AbortSignal.any([state.controller.signal, signal]));
57562
+ signal.throwIfAborted();
57563
+ this.current(account, state);
57564
+ return result;
57376
57565
  }
57377
- readCandidates(account, signal, progress) {
57378
- return this.account(account).candidates.read(signal, progress);
57566
+ /** Visible consumers share one lightweight summary request, including online/focus recovery. */
57567
+ watch(account, changed = () => {}) {
57568
+ let disposed = false, busy = false, timer;
57569
+ const run = async () => {
57570
+ if (disposed || busy || !directoryVisible()) return;
57571
+ busy = true;
57572
+ let delay = 3e4;
57573
+ try {
57574
+ const summary = await this.summary(account);
57575
+ if (summary.state === "disabled") {
57576
+ changed();
57577
+ return;
57578
+ }
57579
+ await this.flushSeen(account);
57580
+ changed();
57581
+ delay = summary.retryAfterMs > 0 ? summary.retryAfterMs : 3e4;
57582
+ } catch (error) {
57583
+ delay = speakerRetryDelay(error) ?? 0;
57584
+ changed();
57585
+ } finally {
57586
+ busy = false;
57587
+ if (!disposed && delay > 0 && this.get(account).summary?.state !== "disabled") {
57588
+ clearTimeout(timer);
57589
+ timer = setTimeout(() => {
57590
+ run();
57591
+ }, Math.max(1e3, delay));
57592
+ }
57593
+ }
57594
+ };
57595
+ const wake = () => {
57596
+ if (directoryVisible()) {
57597
+ const value = this.account(account);
57598
+ value.summaryAt = 0;
57599
+ run();
57600
+ }
57601
+ };
57602
+ run();
57603
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", wake);
57604
+ if (typeof window !== "undefined") {
57605
+ window.addEventListener("online", wake);
57606
+ window.addEventListener("focus", wake);
57607
+ }
57608
+ return () => {
57609
+ disposed = true;
57610
+ clearTimeout(timer);
57611
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", wake);
57612
+ if (typeof window !== "undefined") {
57613
+ window.removeEventListener("online", wake);
57614
+ window.removeEventListener("focus", wake);
57615
+ }
57616
+ };
57379
57617
  }
57380
57618
  invalidate(account) {
57381
57619
  const state = this.accounts.get(account);
57382
- if (state === void 0) return;
57383
- state.pages.clear();
57384
- state.marked.invalidate();
57385
- state.presence.invalidate();
57386
- state.candidates.invalidate();
57620
+ if (!state) return;
57621
+ state.revision += 1;
57622
+ state.summaryAt = 0;
57623
+ state.lists.clear();
57624
+ this.emit();
57387
57625
  }
57388
57626
  clear() {
57389
- for (const state of this.accounts.values()) {
57390
- state.marked.dispose();
57391
- state.presence.dispose();
57392
- state.candidates.dispose();
57393
- }
57627
+ this.accounts.forEach((state) => state.controller.abort());
57394
57628
  this.accounts.clear();
57629
+ this.emit();
57395
57630
  }
57396
57631
  };
57397
57632
  const recognizedSpeakerDirectory = new RecognizedSpeakerDirectory();
57398
- const accountKey = () => {
57633
+ const currentAccount$1 = () => {
57399
57634
  const auth = arkmeAuthStore.getSnapshot().auth;
57400
57635
  return auth?.status === "authenticated" ? `${auth.environment}:${auth.userId}` : "";
57401
57636
  };
57402
- let activeAccount = accountKey();
57637
+ let account$1 = currentAccount$1();
57403
57638
  arkmeAuthStore.subscribe(() => {
57404
- const next = accountKey();
57405
- if (next !== activeAccount) {
57406
- activeAccount = next;
57639
+ const next = currentAccount$1();
57640
+ if (next !== account$1) {
57641
+ account$1 = next;
57407
57642
  recognizedSpeakerDirectory.clear();
57408
57643
  }
57409
57644
  });
@@ -57463,10 +57698,7 @@ body[data-ds-dark-theme] .arkme-bot-identity {
57463
57698
  throw error;
57464
57699
  } finally {
57465
57700
  if (context.current()) {
57466
- if (binding.account !== void 0) {
57467
- recognizedSpeakerTracker.uncertain(binding.account);
57468
- recognizedSpeakerDirectory.invalidate(binding.account);
57469
- }
57701
+ if (binding.account !== void 0) recognizedSpeakerDirectory.invalidate(binding.account);
57470
57702
  recordingSpeakerOptions.reset();
57471
57703
  recordingSpeakerOptions.invalidate();
57472
57704
  recordingSpeakerItemContexts.reset((candidateKey) => candidateKey === key || !recordingSpeakerItemContexts.get(candidateKey).mutating);
@@ -57947,29 +58179,29 @@ body[data-ds-dark-theme] .arkme-bot-identity {
57947
58179
  this.timer = void 0;
57948
58180
  }
57949
58181
  };
57950
- const caches = /* @__PURE__ */ new Map();
58182
+ const caches$1 = /* @__PURE__ */ new Map();
57951
58183
  function activateSelfTopicDirectoryAccount(account) {
57952
- for (const [key, cache] of caches) {
58184
+ for (const [key, cache] of caches$1) {
57953
58185
  if (key === account) continue;
57954
58186
  cache.dispose();
57955
- caches.delete(key);
58187
+ caches$1.delete(key);
57956
58188
  }
57957
58189
  }
57958
58190
  function selfTopicDirectory(userId, environment) {
57959
58191
  const key = `${environment}:${String(userId)}`;
57960
- let cache = caches.get(key);
58192
+ let cache = caches$1.get(key);
57961
58193
  if (!cache) {
57962
58194
  cache = new SelfTopicDirectoryCache(userId, environment);
57963
- caches.set(key, cache);
58195
+ caches$1.set(key, cache);
57964
58196
  }
57965
58197
  return cache;
57966
58198
  }
57967
58199
  function invalidateSelfTopicDirectories(hard = false) {
57968
- for (const cache of caches.values()) cache.invalidate(hard);
58200
+ for (const cache of caches$1.values()) cache.invalidate(hard);
57969
58201
  }
57970
58202
  function resetSelfTopicDirectories() {
57971
- for (const cache of caches.values()) cache.dispose();
57972
- caches.clear();
58203
+ for (const cache of caches$1.values()) cache.dispose();
58204
+ caches$1.clear();
57973
58205
  }
57974
58206
  //#endregion
57975
58207
  //#region src/client/provider-instance-runtime.ts
@@ -118596,7 +118828,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
118596
118828
  displayName: actorGroupNickname?.trim() || actorName,
118597
118829
  ...actorGroupNickname ? { groupNickname: actorGroupNickname } : {},
118598
118830
  ...actorAvatarRef ? { avatarRef: actorAvatarRef } : {}
118599
- } : actor;
118831
+ } : target.resolveActor?.(actor) ?? actor;
118600
118832
  const name = identity.displayName;
118601
118833
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [index > 0 && "、", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
118602
118834
  type: "button",
@@ -120961,14 +121193,17 @@ Please report this to https://github.com/markedjs/marked.`, e) {
120961
121193
  scope,
120962
121194
  roles: [],
120963
121195
  loading: true,
121196
+ loaded: false,
120964
121197
  error: ""
120965
121198
  });
120966
121199
  const visibleState = roleState.scope === scope ? roleState : {
120967
121200
  scope,
120968
121201
  roles: [],
120969
121202
  loading: true,
121203
+ loaded: false,
120970
121204
  error: ""
120971
121205
  };
121206
+ const initialLoading = visibleState.loading && !visibleState.loaded;
120972
121207
  const [refreshRevision, setRefreshRevision] = (0, react$1.useState)(0);
120973
121208
  (0, react$1.useEffect)(() => {
120974
121209
  if (typeof window === "undefined") return;
@@ -120986,6 +121221,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
120986
121221
  const [saving, setSaving] = (0, react$1.useState)(false);
120987
121222
  const [formError, setFormError] = (0, react$1.useState)("");
120988
121223
  const fileInputRef = (0, react$1.useRef)(null);
121224
+ const listControllerRef = (0, react$1.useRef)();
120989
121225
  const saveControllerRef = (0, react$1.useRef)();
120990
121226
  (0, react$1.useEffect)(() => {
120991
121227
  setMenuOpen(false);
@@ -121004,31 +121240,37 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121004
121240
  }, [scope]);
121005
121241
  (0, react$1.useEffect)(() => {
121006
121242
  const controller = new AbortController();
121007
- setRoleState({
121243
+ listControllerRef.current = controller;
121244
+ setRoleState((current) => ({
121008
121245
  scope,
121009
- roles: roleState.scope === scope ? roleState.roles : [],
121246
+ roles: current.scope === scope ? current.roles : [],
121247
+ loaded: current.scope === scope && current.loaded,
121010
121248
  loading: true,
121011
121249
  error: ""
121012
- });
121250
+ }));
121013
121251
  callArkme("self-roles.list", { expectedUserId: userId }, controller.signal).then((roles) => {
121014
121252
  if (controller.signal.aborted || scopeRef.current !== scope) return;
121015
121253
  setRoleState({
121016
121254
  scope,
121017
121255
  roles,
121018
121256
  loading: false,
121257
+ loaded: true,
121019
121258
  error: ""
121020
121259
  });
121260
+ for (const ref of new Set(roles.map((role) => role.avatarRef?.trim()).filter((ref) => !!ref))) arkmeAvatarImages.load(ref).catch(() => void 0);
121021
121261
  }).catch((caught) => {
121022
121262
  if (controller.signal.aborted || scopeRef.current !== scope) return;
121023
121263
  setRoleState((current) => ({
121024
121264
  scope,
121025
121265
  roles: current.scope === scope ? current.roles : [],
121266
+ loaded: current.scope === scope && current.loaded,
121026
121267
  loading: false,
121027
121268
  error: caught instanceof Error ? caught.message : "角色加载失败,请重试"
121028
121269
  }));
121029
121270
  });
121030
121271
  return () => {
121031
121272
  controller.abort();
121273
+ if (listControllerRef.current === controller) listControllerRef.current = void 0;
121032
121274
  };
121033
121275
  }, [
121034
121276
  scope,
@@ -121071,7 +121313,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121071
121313
  const createRole = async (event) => {
121072
121314
  event.preventDefault();
121073
121315
  const roleName = name.trim();
121074
- if (disabled || saveControllerRef.current !== void 0 || visibleState.loading || visibleState.error !== "" || editingRole === void 0 && visibleState.roles.length >= MAX_ROLES || !roleName || Array.from(roleName).length > MAX_NAME_LENGTH) return;
121316
+ if (disabled || saveControllerRef.current !== void 0 || initialLoading || visibleState.error !== "" || editingRole === void 0 && visibleState.roles.length >= MAX_ROLES || !roleName || Array.from(roleName).length > MAX_NAME_LENGTH) return;
121075
121317
  const controller = new AbortController();
121076
121318
  saveControllerRef.current = controller;
121077
121319
  const submittingScope = scope;
@@ -121087,8 +121329,12 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121087
121329
  ...avatarRef === void 0 ? {} : { avatarRef }
121088
121330
  }, controller.signal);
121089
121331
  if (controller.signal.aborted || scopeRef.current !== submittingScope) return;
121332
+ listControllerRef.current?.abort();
121090
121333
  setRoleState((current) => current.scope === submittingScope ? {
121091
121334
  ...current,
121335
+ loading: false,
121336
+ loaded: true,
121337
+ error: "",
121092
121338
  roles: [...current.roles.filter((role) => role.roleId !== created.roleId), created]
121093
121339
  } : current);
121094
121340
  setCreateOpen(false);
@@ -121120,7 +121366,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121120
121366
  event.stopPropagation();
121121
121367
  },
121122
121368
  onClick: () => {
121123
- if (!menuOpen) setRefreshRevision((value) => value + 1);
121369
+ if (!menuOpen && !visibleState.loading) setRefreshRevision((value) => value + 1);
121124
121370
  setMenuOpen((value) => !value);
121125
121371
  },
121126
121372
  children: [
@@ -121168,8 +121414,12 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121168
121414
  roleId: role.roleId
121169
121415
  }).then(() => {
121170
121416
  if (scopeRef.current !== scope) return;
121417
+ listControllerRef.current?.abort();
121171
121418
  setRoleState((current) => current.scope === scope ? {
121172
121419
  ...current,
121420
+ loading: false,
121421
+ loaded: true,
121422
+ error: "",
121173
121423
  roles: current.roles.filter((item) => item.roleId !== role.roleId)
121174
121424
  } : current);
121175
121425
  setRefreshRevision((value) => value + 1);
@@ -121219,7 +121469,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121219
121469
  onSelect: () => deleteRole(role)
121220
121470
  }]
121221
121471
  })),
121222
- ...visibleState.loading ? [{
121472
+ ...initialLoading ? [{
121223
121473
  type: "label",
121224
121474
  id: "role-loading",
121225
121475
  text: "正在加载角色…"
@@ -121242,7 +121492,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
121242
121492
  {
121243
121493
  id: "role-create",
121244
121494
  label: "+ 创建角色",
121245
- disabled: visibleState.loading || visibleState.error !== "" || visibleState.roles.length >= MAX_ROLES,
121495
+ disabled: initialLoading || visibleState.error !== "" || visibleState.roles.length >= MAX_ROLES,
121246
121496
  onSelect: () => {
121247
121497
  setMenuOpen(false);
121248
121498
  setEditingRole(void 0);
@@ -126598,11 +126848,11 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
126598
126848
  }
126599
126849
  const MAX_PREFERENCE = 4096;
126600
126850
  const PADDING = 12;
126601
- const preferenceKey$2 = (account) => `dsh-arkme:calendar-height:v1:${encodeURIComponent(account)}`;
126851
+ const preferenceKey$3 = (account) => `dsh-arkme:calendar-height:v1:${encodeURIComponent(account)}`;
126602
126852
  function readCalendarHeight(account) {
126603
126853
  if (!account) return void 0;
126604
126854
  try {
126605
- const value = Number(localStorage.getItem(preferenceKey$2(account)));
126855
+ const value = Number(localStorage.getItem(preferenceKey$3(account)));
126606
126856
  return Number.isFinite(value) && value >= 360 && value <= MAX_PREFERENCE ? value : void 0;
126607
126857
  } catch {
126608
126858
  return;
@@ -126611,7 +126861,7 @@ body[data-ds-dark-theme] .arkme-mention-all-avatar [data-arkme-theme-image="dark
126611
126861
  function writeCalendarHeight(account, height) {
126612
126862
  if (!account || !Number.isFinite(height)) return;
126613
126863
  try {
126614
- localStorage.setItem(preferenceKey$2(account), String(Math.round(Math.max(360, Math.min(MAX_PREFERENCE, height)))));
126864
+ localStorage.setItem(preferenceKey$3(account), String(Math.round(Math.max(360, Math.min(MAX_PREFERENCE, height)))));
126615
126865
  } catch {}
126616
126866
  }
126617
126867
  function calendarPopoverLayout(anchor, width, height, preferred) {
@@ -138133,14 +138383,14 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
138133
138383
  return;
138134
138384
  }
138135
138385
  }
138136
- function preferenceKey$1(userId) {
138386
+ function preferenceKey$2(userId) {
138137
138387
  return Number.isSafeInteger(userId) && (userId ?? 0) > 0 ? `${STORAGE_KEY_PREFIX$3}${String(userId)}` : void 0;
138138
138388
  }
138139
138389
  function isSelfTopicSort(value) {
138140
138390
  return value === "latest" || value === "most" || value === "custom";
138141
138391
  }
138142
138392
  function readSelfTopicSortPreference(userId, storage) {
138143
- const key = preferenceKey$1(userId);
138393
+ const key = preferenceKey$2(userId);
138144
138394
  const target = preferenceStorage$1(storage);
138145
138395
  if (key === void 0 || target === void 0) return DEFAULT_SELF_TOPIC_SORT;
138146
138396
  try {
@@ -138151,7 +138401,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
138151
138401
  }
138152
138402
  }
138153
138403
  function writeSelfTopicSortPreference(userId, sort, storage) {
138154
- const key = preferenceKey$1(userId);
138404
+ const key = preferenceKey$2(userId);
138155
138405
  const target = preferenceStorage$1(storage);
138156
138406
  if (key === void 0 || target === void 0) return;
138157
138407
  try {
@@ -143575,7 +143825,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
143575
143825
  return;
143576
143826
  }
143577
143827
  }
143578
- function preferenceKey(userId) {
143828
+ function preferenceKey$1(userId) {
143579
143829
  return Number.isSafeInteger(userId) && (userId ?? 0) > 0 ? `${STORAGE_KEY_PREFIX$2}user:${String(userId)}` : `${STORAGE_KEY_PREFIX$2}anonymous`;
143580
143830
  }
143581
143831
  function isMarketplaceSort(value) {
@@ -143585,7 +143835,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
143585
143835
  const target = preferenceStorage(storage);
143586
143836
  if (target === void 0) return DEFAULT_MARKETPLACE_SORT;
143587
143837
  try {
143588
- const value = target.getItem(preferenceKey(userId));
143838
+ const value = target.getItem(preferenceKey$1(userId));
143589
143839
  return isMarketplaceSort(value) ? value : DEFAULT_MARKETPLACE_SORT;
143590
143840
  } catch {
143591
143841
  return DEFAULT_MARKETPLACE_SORT;
@@ -143595,7 +143845,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
143595
143845
  const target = preferenceStorage(storage);
143596
143846
  if (target === void 0) return;
143597
143847
  try {
143598
- target.setItem(preferenceKey(userId), sort);
143848
+ target.setItem(preferenceKey$1(userId), sort);
143599
143849
  } catch {}
143600
143850
  }
143601
143851
  //#endregion
@@ -155138,7 +155388,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155138
155388
  * Wrap `React.useLayoutEffect` which will not throw warning message in test env
155139
155389
  */
155140
155390
  const useInternalLayoutEffect = canUseDom() ? react.useLayoutEffect : react.useEffect;
155141
- const useLayoutEffect$24 = (callback, deps) => {
155391
+ const useLayoutEffect$25 = (callback, deps) => {
155142
155392
  const firstMountRef = react.useRef(true);
155143
155393
  useInternalLayoutEffect(() => {
155144
155394
  return callback(firstMountRef.current);
@@ -155182,7 +155432,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155182
155432
  function useControlledState(defaultStateValue, value) {
155183
155433
  const [innerValue, setInnerValue] = (0, react.useState)(defaultStateValue);
155184
155434
  const mergedValue = value !== void 0 ? value : innerValue;
155185
- useLayoutEffect$24((mount) => {
155435
+ useLayoutEffect$25((mount) => {
155186
155436
  if (!mount) setInnerValue(value);
155187
155437
  }, [value]);
155188
155438
  return [mergedValue, setInnerValue];
@@ -155245,7 +155495,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155245
155495
  };
155246
155496
  //#endregion
155247
155497
  //#region node_modules/@rc-component/util/es/hooks/useMemo.js
155248
- function useMemo$37(getValue, condition, shouldUpdate) {
155498
+ function useMemo$36(getValue, condition, shouldUpdate) {
155249
155499
  const cacheRef = react.useRef({});
155250
155500
  if (!("value" in cacheRef.current) || shouldUpdate(cacheRef.current.condition, condition)) {
155251
155501
  cacheRef.current.value = getValue();
@@ -155358,7 +155608,7 @@ body:has([data-arkme-self-topic-menu]) > [role="menu"] { z-index: 10030; }
155358
155608
  };
155359
155609
  };
155360
155610
  const useComposeRef = (...refs) => {
155361
- return useMemo$37(() => composeRef(...refs), refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
155611
+ return useMemo$36(() => composeRef(...refs), refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
155362
155612
  };
155363
155613
  const supportRef = (nodeOrComponent) => {
155364
155614
  if (!nodeOrComponent) return false;
@@ -156248,13 +156498,13 @@ ${height ? `height: ${webkitScrollbarStyle.height};` : ""}
156248
156498
  ele.parentElement?.removeChild(ele);
156249
156499
  appendedRef.current = false;
156250
156500
  }
156251
- useLayoutEffect$24(() => {
156501
+ useLayoutEffect$25(() => {
156252
156502
  if (render) if (queueCreate) queueCreate(append);
156253
156503
  else append();
156254
156504
  else cleanup();
156255
156505
  return cleanup;
156256
156506
  }, [render]);
156257
- useLayoutEffect$24(() => {
156507
+ useLayoutEffect$25(() => {
156258
156508
  if (queue.length) {
156259
156509
  queue.forEach((appendFn) => appendFn());
156260
156510
  setQueue(EMPTY_LIST);
@@ -156280,7 +156530,7 @@ ${height ? `height: ${webkitScrollbarStyle.height};` : ""}
156280
156530
  uuid$1 += 1;
156281
156531
  return `${UNIQUE_ID}_${uuid$1}`;
156282
156532
  });
156283
- useLayoutEffect$24(() => {
156533
+ useLayoutEffect$25(() => {
156284
156534
  if (mergedLock) {
156285
156535
  const scrollbarSize = getTargetScrollBarSize(document.body).width;
156286
156536
  updateCSS(`
@@ -157223,7 +157473,7 @@ html body {
157223
157473
  ]);
157224
157474
  const getPopupContainerNeedParams = getPopupContainer?.length > 0;
157225
157475
  const [show, setShow] = react.useState(!getPopupContainer || !getPopupContainerNeedParams);
157226
- useLayoutEffect$24(() => {
157476
+ useLayoutEffect$25(() => {
157227
157477
  if (!show && getPopupContainerNeedParams && target) setShow(true);
157228
157478
  }, [
157229
157479
  show,
@@ -157775,8 +158025,8 @@ html body {
157775
158025
  ready: false
157776
158026
  }));
157777
158027
  };
157778
- useLayoutEffect$24(resetReady, [placement]);
157779
- useLayoutEffect$24(() => {
158028
+ useLayoutEffect$25(resetReady, [placement]);
158029
+ useLayoutEffect$25(() => {
157780
158030
  if (!open) resetReady();
157781
158031
  }, [open]);
157782
158032
  return [
@@ -157820,7 +158070,7 @@ html body {
157820
158070
  //#endregion
157821
158071
  //#region node_modules/@rc-component/trigger/es/hooks/useWatch.js
157822
158072
  function useWatch(open, target, popup, onAlign, onScroll) {
157823
- useLayoutEffect$24(() => {
158073
+ useLayoutEffect$25(() => {
157824
158074
  if (open && target && popup) {
157825
158075
  const targetElement = target;
157826
158076
  const popupElement = popup;
@@ -157975,7 +158225,7 @@ html body {
157975
158225
  id,
157976
158226
  onEsc
157977
158227
  }));
157978
- useLayoutEffect$24(() => {
158228
+ useLayoutEffect$25(() => {
157979
158229
  if (uniqueContext && unique && targetEle && !openUncontrolled && !parentContext) if (mergedOpen) uniqueContext.show(getUniqueOptions(mouseEnterDelay), isOpen);
157980
158230
  else uniqueContext.hide(mouseLeaveDelay);
157981
158231
  }, [mergedOpen, targetEle]);
@@ -158011,7 +158261,7 @@ html body {
158011
158261
  if (top) triggerOpen(false);
158012
158262
  }
158013
158263
  const [inMotion, setInMotion] = react.useState(false);
158014
- useLayoutEffect$24((firstMount) => {
158264
+ useLayoutEffect$25((firstMount) => {
158015
158265
  if (!firstMount || mergedOpen) setInMotion(true);
158016
158266
  }, [mergedOpen]);
158017
158267
  const [motionPrepareResolve, setMotionPrepareResolve] = react.useState(null);
@@ -158030,10 +158280,10 @@ html body {
158030
158280
  if (openRef.current && alignPoint && clickToHide) triggerOpen(false);
158031
158281
  };
158032
158282
  useWatch(mergedOpen, targetEle, popupEle, triggerAlign, onScroll);
158033
- useLayoutEffect$24(() => {
158283
+ useLayoutEffect$25(() => {
158034
158284
  triggerAlign();
158035
158285
  }, [mousePos, popupPlacement]);
158036
- useLayoutEffect$24(() => {
158286
+ useLayoutEffect$25(() => {
158037
158287
  if (mergedOpen && !builtinPlacements?.[popupPlacement]) triggerAlign();
158038
158288
  }, [JSON.stringify(popupAlign)]);
158039
158289
  const alignedClassName = react.useMemo(() => {
@@ -158073,7 +158323,7 @@ html body {
158073
158323
  syncTargetSize();
158074
158324
  setMotionPrepareResolve(() => resolve);
158075
158325
  });
158076
- useLayoutEffect$24(() => {
158326
+ useLayoutEffect$25(() => {
158077
158327
  if (motionPrepareResolve) {
158078
158328
  onAlign();
158079
158329
  motionPrepareResolve();
@@ -158280,7 +158530,7 @@ html body {
158280
158530
  }
158281
158531
  function useTarget(target, open, gap, scrollIntoViewOptions, inlineMode, placeholderRef) {
158282
158532
  const [targetElement, setTargetElement] = (0, react.useState)(void 0);
158283
- useLayoutEffect$24(() => {
158533
+ useLayoutEffect$25(() => {
158284
158534
  const nextElement = typeof target === "function" ? target() : target;
158285
158535
  setTargetElement(nextElement || null);
158286
158536
  });
@@ -158310,7 +158560,7 @@ html body {
158310
158560
  } else setPosInfo(null);
158311
158561
  });
158312
158562
  const getGapOffset = (index) => (Array.isArray(gap?.offset) ? gap?.offset[index] : gap?.offset) ?? 6;
158313
- useLayoutEffect$24(() => {
158563
+ useLayoutEffect$25(() => {
158314
158564
  updatePos();
158315
158565
  window.addEventListener("resize", updatePos);
158316
158566
  window.addEventListener("scroll", updatePos);
@@ -158600,7 +158850,7 @@ html body {
158600
158850
  const mergedOpen = mergedCurrent < 0 || mergedCurrent >= steps.length ? false : internalOpen ?? true;
158601
158851
  const [hasOpened, setHasOpened] = react.useState(mergedOpen);
158602
158852
  const openRef = react.useRef(mergedOpen);
158603
- useLayoutEffect$24(() => {
158853
+ useLayoutEffect$25(() => {
158604
158854
  if (mergedOpen) {
158605
158855
  if (!openRef.current) setMergedCurrent(0);
158606
158856
  setHasOpened(true);
@@ -158617,7 +158867,7 @@ html body {
158617
158867
  const mergedPlacement = getPlacement(targetElement, placement, stepPlacement);
158618
158868
  const mergedArrow = targetElement ? typeof stepArrow === "undefined" ? arrow : stepArrow : false;
158619
158869
  const arrowPointAtCenter = typeof mergedArrow === "object" ? mergedArrow.pointAtCenter : false;
158620
- useLayoutEffect$24(() => {
158870
+ useLayoutEffect$25(() => {
158621
158871
  triggerRef.current?.forceAlign();
158622
158872
  }, [arrowPointAtCenter, mergedCurrent]);
158623
158873
  const onInternalChange = (nextCurrent) => {
@@ -158655,7 +158905,7 @@ html body {
158655
158905
  return;
158656
158906
  }
158657
158907
  });
158658
- useLayoutEffect$24(() => {
158908
+ useLayoutEffect$25(() => {
158659
158909
  if (!mergedOpen) return;
158660
158910
  window.addEventListener("keydown", keyboardHandler);
158661
158911
  return () => {
@@ -166140,7 +166390,7 @@ html body {
166140
166390
  //#endregion
166141
166391
  //#region src/client/self-topic-expansion-preference.ts
166142
166392
  const EMPTY$1 = /* @__PURE__ */ new Set();
166143
- function browserStorage() {
166393
+ function browserStorage$1() {
166144
166394
  try {
166145
166395
  return typeof window === "undefined" ? void 0 : window.localStorage;
166146
166396
  } catch {
@@ -166159,7 +166409,7 @@ html body {
166159
166409
  storage;
166160
166410
  value;
166161
166411
  listeners = /* @__PURE__ */ new Set();
166162
- constructor(key, storage = browserStorage()) {
166412
+ constructor(key, storage = browserStorage$1()) {
166163
166413
  this.key = key;
166164
166414
  this.storage = storage;
166165
166415
  this.value = this.read();
@@ -178696,73 +178946,19 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
178696
178946
  const p$2 = e$36;
178697
178947
  //#endregion
178698
178948
  //#region src/client/recordings/RecognizedSpeakerEntry.tsx
178699
- function RecognizedSpeakerEntry({ accountKey, active, onOpen, style, readMarked = readSpeakerOptions, readPage = readSpeakerPage, directory: providedDirectory }) {
178949
+ function RecognizedSpeakerEntry({ accountKey, active, onOpen, style, directory = recognizedSpeakerDirectory }) {
178700
178950
  useArkmeLocale();
178701
- const directory = (0, react$1.useMemo)(() => providedDirectory ?? (readMarked === readSpeakerOptions && readPage === readSpeakerPage ? recognizedSpeakerDirectory : new RecognizedSpeakerDirectory({
178702
- marked: readMarked,
178703
- page: readPage,
178704
- presence: readSpeakerPresence
178705
- }, 0)), [
178706
- providedDirectory,
178707
- readMarked,
178708
- readPage
178709
- ]);
178710
- const snapshot = (0, react$1.useSyncExternalStore)(recognizedSpeakerTracker.subscribe, () => recognizedSpeakerTracker.get(accountKey), () => recognizedSpeakerTracker.get(accountKey));
178951
+ const snapshot = (0, react$1.useSyncExternalStore)(directory.subscribe, () => directory.get(accountKey), () => directory.get(accountKey));
178711
178952
  (0, react$1.useEffect)(() => {
178712
- if (!active || accountKey === void 0) return;
178713
- const controller = new AbortController();
178714
- let busy = false;
178715
- let retryTimer;
178716
- const refresh = async () => {
178717
- if (busy || controller.signal.aborted || typeof document !== "undefined" && document.visibilityState === "hidden") return;
178718
- busy = true;
178719
- try {
178720
- let marked;
178721
- const updateTotal = () => {
178722
- const value = directory.peekCandidates(accountKey);
178723
- if (marked !== void 0 && value?.total !== void 0 && !controller.signal.aborted) recognizedSpeakerTracker.setTotal(accountKey, markedSpeakerKeys(marked).length + value.total);
178724
- };
178725
- const [, result] = await Promise.all([directory.readMarked(accountKey, controller.signal).then((value) => {
178726
- marked = value;
178727
- updateTotal();
178728
- }), directory.readCandidates(accountKey, controller.signal, updateTotal)]);
178729
- if (controller.signal.aborted) return;
178730
- if (result.complete && marked !== void 0) recognizedSpeakerTracker.observe(accountKey, marked, result.items);
178731
- else recognizedSpeakerTracker.uncertain(accountKey);
178732
- } catch (error) {
178733
- if (!controller.signal.aborted) {
178734
- recognizedSpeakerTracker.uncertain(accountKey);
178735
- const delay = speakerRetryDelay(error);
178736
- if (delay !== void 0) {
178737
- clearTimeout(retryTimer);
178738
- retryTimer = setTimeout(() => {
178739
- refresh();
178740
- }, delay);
178741
- }
178742
- }
178743
- } finally {
178744
- busy = false;
178745
- }
178746
- };
178747
- refresh();
178748
- const timer = setInterval(() => {
178749
- refresh();
178750
- }, 6e4);
178751
- const onVisible = () => {
178752
- refresh();
178753
- };
178754
- if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
178755
- return () => {
178756
- controller.abort();
178757
- clearInterval(timer);
178758
- clearTimeout(retryTimer);
178759
- if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
178760
- };
178953
+ if (active && accountKey !== void 0) return directory.watch(accountKey);
178761
178954
  }, [
178762
178955
  accountKey,
178763
178956
  active,
178764
178957
  directory
178765
178958
  ]);
178959
+ const summary = snapshot.summary;
178960
+ const count = summary?.state === "disabled" ? null : summary?.totalCount;
178961
+ const hint = summary?.state === "disabled" ? "目录暂不可用" : count == null ? summary?.state === "building" ? "整理中…" : snapshot.error || summary?.state === "failed" ? "暂时无法更新" : "" : (summary?.unseenCount ?? 0) > 0 ? tr$1("新识别 {v0} 个", { v0: summary.unseenCount }) : "";
178766
178962
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
178767
178963
  "data-arkme-feedback": "recording-action",
178768
178964
  type: "button",
@@ -178796,12 +178992,12 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
178796
178992
  },
178797
178993
  children: tr$1("已识别说话人")
178798
178994
  }),
178799
- snapshot.total !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178995
+ count != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178800
178996
  style: {
178801
178997
  flexShrink: 0,
178802
178998
  fontVariantNumeric: "tabular-nums"
178803
178999
  },
178804
- children: snapshot.total
179000
+ children: count
178805
179001
  }),
178806
179002
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(s$21, {
178807
179003
  size: 12,
@@ -178809,14 +179005,14 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
178809
179005
  "aria-hidden": true
178810
179006
  })
178811
179007
  ]
178812
- }), (snapshot.newCount ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
179008
+ }), hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178813
179009
  style: {
178814
179010
  fontSize: 11,
178815
179011
  lineHeight: "15px",
178816
179012
  fontWeight: 400
178817
179013
  },
178818
179014
  "aria-live": "polite",
178819
- children: tr$1("新识别 {v0} 个", { v0: snapshot.newCount })
179015
+ children: tr$1(hint)
178820
179016
  })]
178821
179017
  });
178822
179018
  }
@@ -191153,6 +191349,28 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191153
191349
  });
191154
191350
  }
191155
191351
  //#endregion
191352
+ //#region src/client/recognized-speaker-order.ts
191353
+ const preferenceKey = (accountKey) => `dsh-arkme:recognized-speaker-order:v1:${accountKey}`;
191354
+ function browserStorage() {
191355
+ try {
191356
+ return typeof window === "undefined" ? void 0 : window.localStorage;
191357
+ } catch {
191358
+ return;
191359
+ }
191360
+ }
191361
+ function readRecognizedSpeakerOrder(accountKey, storage = browserStorage()) {
191362
+ try {
191363
+ return storage?.getItem(preferenceKey(accountKey)) === "recent" ? "recent" : "frequent";
191364
+ } catch {
191365
+ return "frequent";
191366
+ }
191367
+ }
191368
+ function writeRecognizedSpeakerOrder(accountKey, order, storage = browserStorage()) {
191369
+ try {
191370
+ storage?.setItem(preferenceKey(accountKey), order);
191371
+ } catch {}
191372
+ }
191373
+ //#endregion
191156
191374
  //#region src/client/recordings/SpeakerSelfGuide.tsx
191157
191375
  const loadVoiceprint = (signal) => callArkme("voiceprint.status", {}, signal);
191158
191376
  function SpeakerSelfGuide({ onOpenRecordings, readVoiceprint = loadVoiceprint }) {
@@ -191175,7 +191393,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191175
191393
  if (state.loading) return null;
191176
191394
  const pending = state.value?.enrollmentPending === true;
191177
191395
  const hasVoiceprint = state.value?.hasVoiceprint === true;
191178
- const title = pending ? "你的声纹正在处理中" : hasVoiceprint ? "已录入声纹,暂未在录音中匹配到你" : "还没有标记你的声音";
191396
+ const title = state.value === void 0 ? "声纹状态暂不可用" : pending ? "你的声纹正在处理中" : hasVoiceprint ? "已录入声纹,暂未在录音中匹配到你" : "还没有标记你的声音";
191179
191397
  const actionStyle = {
191180
191398
  border: 0,
191181
191399
  padding: 0,
@@ -191230,39 +191448,6 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191230
191448
  }
191231
191449
  //#endregion
191232
191450
  //#region src/client/ArkmeRecognizedSpeakersSurface.tsx
191233
- /** Group only by a stable owner identity, never by a shared name or speaker number. */
191234
- function identifiedSpeakerRows(options, candidates) {
191235
- const marked = /* @__PURE__ */ new Map();
191236
- for (const option of options) {
191237
- if (option.kind !== "speaker") continue;
191238
- const key = option.personKey || option.optionKey;
191239
- if (marked.has(key)) continue;
191240
- marked.set(key, {
191241
- kind: "marked",
191242
- key,
191243
- optionKey: option.optionKey,
191244
- speakerRef: option.speakerRef,
191245
- name: option.label.trim() || "未命名说话人",
191246
- ...option.avatarRef === void 0 ? {} : { avatarRef: option.avatarRef },
191247
- isCurrentUser: option.isCurrentUser
191248
- });
191249
- }
191250
- const unmarked = /* @__PURE__ */ new Map();
191251
- for (const candidate of candidates) {
191252
- if (unmarked.has(candidate.candidateRef)) continue;
191253
- unmarked.set(candidate.candidateRef, {
191254
- kind: "unmarked",
191255
- key: candidate.candidateRef,
191256
- name: tr$1("说话人 {v0}", { v0: candidate.speakerToken?.trim() || candidate.displayName }),
191257
- token: candidate.speakerToken?.trim() || "",
191258
- subtitle: candidate.subtitle,
191259
- candidateRef: candidate.candidateRef,
191260
- dayCount: candidate.appearanceDays,
191261
- lastSeenAt: candidate.latestAtMillis
191262
- });
191263
- }
191264
- return [...[...marked.values()].sort((left, right) => left.name.localeCompare(right.name)), ...unmarked.values()];
191265
- }
191266
191451
  const styles$20 = {
191267
191452
  root: {
191268
191453
  flex: 1,
@@ -191450,87 +191635,133 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191450
191635
  borderBottom: `1px solid ${arkmeTheme.borderSoft}`
191451
191636
  }
191452
191637
  };
191453
- const defaultLoadMarkedMembers = async (speakerRef, signal, expectedVersion) => await callArkme("recordings.speaker.members", {
191638
+ const defaultLoadMarkedMembers = (speakerRef, signal, expectedVersion) => callArkme("recordings.speaker.members", {
191454
191639
  speakerRef,
191455
191640
  ...expectedVersion === void 0 ? {} : { expectedVersion }
191456
191641
  }, signal);
191457
- function failureMessage(error) {
191458
- return error instanceof Error && error.message.trim() !== "" ? error.message : tr$1("说话人列表暂时无法加载");
191459
- }
191460
- function markedPresenceLabel(stat, result, loading, error) {
191461
- if (loading || result?.state === "building") return tr$1("出现统计整理中");
191462
- if (error !== "" || result?.state === "failed" || result === void 0) return tr$1("出现统计暂不可用");
191463
- if (result.state === "stale") return tr$1("出现统计更新中");
191464
- if (stat !== void 0) {
191465
- const date = new Date(stat.lastSeenAt);
191466
- if (!Number.isNaN(date.getTime())) {
191467
- const two = (value) => String(value).padStart(2, "0");
191468
- const recent = `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
191469
- return tr$1("出现 {v0} 天 · 最近 {v1}", {
191470
- v0: stat.dayCount,
191471
- v1: recent
191472
- });
191473
- }
191474
- }
191475
- return tr$1("暂无可统计的录音片段");
191476
- }
191477
- function MarkedSpeakerDetail({ accountKey, speaker, loadMembers, version, onVersionChanged }) {
191478
- const [state, setState] = (0, react$1.useState)({
191479
- loading: true,
191480
- error: ""
191642
+ const failureMessage = (error) => error instanceof Error ? error.message : tr$1("说话人列表暂时无法加载");
191643
+ const nameSegmenter = new Intl.Segmenter("zh-CN", { granularity: "grapheme" });
191644
+ function speakerNameAvatarFallback(row) {
191645
+ const initial = nameSegmenter.segment(row.displayName.trim())[Symbol.iterator]().next().value?.segment;
191646
+ if (!initial) return void 0;
191647
+ let hash = 0;
191648
+ for (const character of row.personKey) hash = Math.imul(hash, 31) + character.codePointAt(0) >>> 0;
191649
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
191650
+ "aria-hidden": true,
191651
+ style: {
191652
+ width: "100%",
191653
+ height: "100%",
191654
+ display: "grid",
191655
+ placeItems: "center",
191656
+ borderRadius: "50%",
191657
+ background: `color-mix(in srgb, ${recordingSpeakerColor(hash % 15)} 30%, ${arkmeTheme.base})`,
191658
+ color: arkmeTheme.text,
191659
+ fontSize: 17,
191660
+ fontWeight: 600,
191661
+ lineHeight: 1
191662
+ },
191663
+ children: initial
191481
191664
  });
191665
+ }
191666
+ function personStats(row) {
191667
+ const recent = row.lastSeenAt > 0 ? new Intl.DateTimeFormat(arkmeIntlLocale(), {
191668
+ year: "numeric",
191669
+ month: "2-digit",
191670
+ day: "2-digit",
191671
+ hour: "2-digit",
191672
+ minute: "2-digit",
191673
+ hour12: false
191674
+ }).format(row.lastSeenAt) : tr$1("时间未知");
191675
+ return tr$1("出现 {v0} 天 · 最近 {v1}", {
191676
+ v0: row.dayCount,
191677
+ v1: recent
191678
+ });
191679
+ }
191680
+ function statusText(status) {
191681
+ if (status?.state === "disabled") return tr$1("说话人目录暂不可用,请稍后再试。");
191682
+ if (status?.coverage === "unknown") return status.state === "failed" ? tr$1("说话人目录暂不可用,请稍后刷新。") : tr$1("正在整理说话人目录…");
191683
+ if (status?.state === "failed") return tr$1("更新暂未完成,当前显示上次整理的目录。");
191684
+ if (status?.state === "stale") return tr$1("正在更新,当前显示已整理的目录。");
191685
+ if (status && (status.processingSessionCount > 0 || status.pendingIdentityAggregationCount > 0 || status.insufficientEvidenceCount > 0 || status.scanTruncated)) return tr$1("部分录音仍在整理,人物可能继续更新。");
191686
+ return "";
191687
+ }
191688
+ function PersonDetail({ row, accountKey, directory, loadMembers, onChanged }) {
191689
+ const [detail, setDetail] = (0, react$1.useState)();
191690
+ const [members, setMembers] = (0, react$1.useState)();
191691
+ const [error, setError] = (0, react$1.useState)("");
191692
+ const [revision, setRevision] = (0, react$1.useState)(0);
191482
191693
  (0, react$1.useEffect)(() => {
191483
191694
  const controller = new AbortController();
191484
- setState({
191485
- loading: true,
191486
- error: ""
191487
- });
191488
- let timer;
191489
- const load = async () => {
191490
- try {
191491
- const result = await loadMembers(speaker.speakerRef, controller.signal, version);
191695
+ let retry;
191696
+ setDetail(void 0);
191697
+ setMembers(void 0);
191698
+ setError("");
191699
+ directory.open(accountKey, row.detailRef, controller.signal).then(async (value) => {
191700
+ if (controller.signal.aborted) return;
191701
+ setDetail(value);
191702
+ if (value.type === "speaker") {
191703
+ const result = await loadMembers(value.speakerRef, controller.signal, value.expectedVersion);
191492
191704
  if (controller.signal.aborted) return;
191493
- setState({
191494
- loading: false,
191495
- result,
191496
- error: ""
191497
- });
191498
- if (result.state === "stale" && result.version !== void 0 && result.version !== version) onVersionChanged();
191499
- if (result.state !== "fresh") timer = setTimeout(() => {
191500
- load();
191705
+ setMembers(result);
191706
+ if (result.state === "stale") onChanged();
191707
+ else if (result.state !== "fresh") retry = setTimeout(() => {
191708
+ if (directoryVisible()) setRevision((value) => value + 1);
191501
191709
  }, Math.max(5e3, result.retryAfterMs ?? 1e4));
191502
- } catch (error) {
191503
- if (!controller.signal.aborted) setState({
191504
- loading: false,
191505
- error: failureMessage(error)
191506
- });
191507
191710
  }
191508
- };
191509
- load();
191711
+ }).catch((error) => {
191712
+ if (controller.signal.aborted) return;
191713
+ if (directoryErrorCode(error) === "unmarked-candidate-not-found") onChanged();
191714
+ else setError(failureMessage(error));
191715
+ });
191510
191716
  return () => {
191511
- clearTimeout(timer);
191512
191717
  controller.abort();
191718
+ clearTimeout(retry);
191513
191719
  };
191514
191720
  }, [
191721
+ row.detailRef,
191515
191722
  accountKey,
191516
- speaker.speakerRef,
191723
+ directory,
191517
191724
  loadMembers,
191518
- version,
191519
- onVersionChanged
191725
+ onChanged,
191726
+ revision
191520
191727
  ]);
191728
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
191729
+ role: "alert",
191730
+ style: styles$20.error,
191731
+ children: [
191732
+ error,
191733
+ " ",
191734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
191735
+ style: styles$20.button,
191736
+ onClick: () => setRevision((value) => value + 1),
191737
+ children: tr$1("重试")
191738
+ })
191739
+ ]
191740
+ });
191741
+ if (!detail) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191742
+ role: "status",
191743
+ style: styles$20.state,
191744
+ children: tr$1("正在加载人物详情…")
191745
+ });
191746
+ if (detail.type === "candidate") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerDetail, {
191747
+ accountKey,
191748
+ candidateRef: detail.candidateRef,
191749
+ onDirectoryRefresh: onChanged,
191750
+ onCandidateCleared: onChanged
191751
+ });
191521
191752
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
191522
191753
  "aria-label": tr$1("已标记说话人详情"),
191523
191754
  children: [
191524
191755
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
191525
191756
  style: styles$20.detailTitle,
191526
- children: [speaker.name, speaker.isCurrentUser ? ` · ${tr$1("我")}` : ""]
191757
+ children: [row.displayName, row.isSelf ? ` · ${tr$1("我")}` : ""]
191527
191758
  }),
191528
191759
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
191529
191760
  style: {
191530
191761
  ...styles$20.meta,
191531
191762
  margin: 0
191532
191763
  },
191533
- children: tr$1("查看已确认归属的原始识别身份")
191764
+ children: personStats(row)
191534
191765
  }),
191535
191766
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
191536
191767
  style: styles$20.detailSection,
@@ -191539,30 +191770,20 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191539
191770
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
191540
191771
  style: {
191541
191772
  ...styles$20.meta,
191542
- margin: "0 0 8px",
191543
191773
  whiteSpace: "normal"
191544
191774
  },
191545
191775
  children: tr$1("展示全部历史已转写片段中可核实的关联。")
191546
191776
  }),
191547
- state.loading || state.result?.state === "building" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191777
+ !members || members.state !== "fresh" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191548
191778
  role: "status",
191549
191779
  style: styles$20.state,
191550
- children: tr$1("正在查找关联说话人…")
191551
- }) : state.error !== "" || state.result?.state === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191552
- role: "alert",
191553
- style: styles$20.error,
191554
- children: state.error || tr$1("出现统计暂不可用")
191555
- }) : state.result?.state === "stale" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191556
- role: "status",
191557
- style: styles$20.state,
191558
- children: tr$1("出现统计更新中")
191559
- }) : state.result?.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191560
- role: "status",
191780
+ children: members?.state === "failed" ? tr$1("出现统计暂不可用") : tr$1("出现统计整理中")
191781
+ }) : members.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191561
191782
  style: styles$20.state,
191562
- children: state.result.dayCount > 0 ? tr$1("有发声,但未找到稳定的原始识别身份。") : tr$1("暂无可核实的关联说话人。")
191783
+ children: tr$1("暂无可核实的关联说话人。")
191563
191784
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
191564
191785
  style: styles$20.list,
191565
- children: state.result?.items.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
191786
+ children: members.items.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
191566
191787
  style: styles$20.memberRow,
191567
191788
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
191568
191789
  token: member.token,
@@ -191583,274 +191804,251 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191583
191804
  ]
191584
191805
  });
191585
191806
  }
191586
- function ArkmeRecognizedSpeakersSurface({ accountKey, onBack, loadMarked = readSpeakerOptions, loadPresence = readSpeakerPresence, loadMarkedMembers = defaultLoadMarkedMembers, loadUnmarked = readSpeakerPage, directory: providedDirectory }) {
191807
+ function ArkmeRecognizedSpeakersSurface(props) {
191808
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DirectorySurface, { ...props }, props.accountKey);
191809
+ }
191810
+ function DirectorySurface({ accountKey, onBack, directory = recognizedSpeakerDirectory, loadMarkedMembers = defaultLoadMarkedMembers }) {
191587
191811
  useArkmeLocale();
191588
- const directory = (0, react$1.useMemo)(() => providedDirectory ?? (loadMarked === readSpeakerOptions && loadUnmarked === readSpeakerPage && loadPresence === readSpeakerPresence ? recognizedSpeakerDirectory : new RecognizedSpeakerDirectory({
191589
- marked: loadMarked,
191590
- page: loadUnmarked,
191591
- presence: loadPresence
191592
- }, 0)), [
191593
- providedDirectory,
191594
- loadMarked,
191595
- loadUnmarked,
191596
- loadPresence
191597
- ]);
191598
191812
  const [filter, setFilter] = (0, react$1.useState)("all");
191599
- const [query, setQuery] = (0, react$1.useState)("");
191600
- const [orderPreference, setOrderPreference] = (0, react$1.useState)(() => ({
191601
- accountKey,
191602
- value: readRecognizedSpeakerOrder(accountKey)
191603
- }));
191604
- const order = orderPreference.accountKey === accountKey ? orderPreference.value : readRecognizedSpeakerOrder(accountKey);
191605
- const [visibleCount, setVisibleCount] = (0, react$1.useState)(100);
191606
- const [refreshRevision, setRefreshRevision] = (0, react$1.useState)(0);
191607
- const [marked, setMarked] = (0, react$1.useState)({
191608
- loading: true,
191609
- items: [],
191610
- error: ""
191611
- });
191612
- const [presence, setPresence] = (0, react$1.useState)({
191613
- loading: true,
191614
- error: ""
191615
- });
191616
- const [unmarked, setUnmarked] = (0, react$1.useState)({
191617
- loading: true,
191618
- complete: false,
191619
- items: [],
191620
- projectionState: void 0,
191621
- error: ""
191622
- });
191623
- const [selectedCandidate, setSelectedCandidate] = (0, react$1.useState)();
191624
- const [selectedMarked, setSelectedMarked] = (0, react$1.useState)();
191625
- const lastAccountKey = (0, react$1.useRef)(accountKey);
191626
- const readAccount = (0, react$1.useRef)();
191627
- const refresh = (0, react$1.useCallback)(() => {
191628
- directory.invalidate(accountKey);
191629
- setSelectedMarked(void 0);
191630
- setRefreshRevision((value) => value + 1);
191631
- }, [directory, accountKey]);
191813
+ const [input, setInput] = (0, react$1.useState)(""), [queryText, setQueryText] = (0, react$1.useState)("");
191814
+ const [sort, setSort] = (0, react$1.useState)(() => readRecognizedSpeakerOrder(accountKey));
191815
+ const query = (0, react$1.useMemo)(() => ({
191816
+ filter,
191817
+ sort,
191818
+ query: queryText
191819
+ }), [
191820
+ filter,
191821
+ sort,
191822
+ queryText
191823
+ ]);
191824
+ const [list, setList] = (0, react$1.useState)();
191825
+ const listRef = (0, react$1.useRef)(list);
191826
+ listRef.current = list;
191827
+ const [loading, setLoading] = (0, react$1.useState)(true), [moreLoading, setMoreLoading] = (0, react$1.useState)(false);
191828
+ const [error, setError] = (0, react$1.useState)(""), [seenError, setSeenError] = (0, react$1.useState)("");
191829
+ const [selected, setSelected] = (0, react$1.useState)();
191830
+ const [avatars, setAvatars] = (0, react$1.useState)({});
191831
+ const rootRef = (0, react$1.useRef)(null), listScroll = (0, react$1.useRef)(0), hadSelection = (0, react$1.useRef)(false);
191832
+ (0, react$1.useLayoutEffect)(() => {
191833
+ const root = rootRef.current;
191834
+ if (root && root.clientWidth <= 680) {
191835
+ if (selected && !hadSelection.current) {
191836
+ listScroll.current = root.scrollTop;
191837
+ root.scrollTop = 0;
191838
+ } else if (!selected && hadSelection.current) root.scrollTop = listScroll.current;
191839
+ }
191840
+ hadSelection.current = selected !== void 0;
191841
+ }, [selected]);
191842
+ const [revision, setRevision] = (0, react$1.useState)(0);
191843
+ const force = (0, react$1.useRef)(false), recoveryAttempts = (0, react$1.useRef)(0);
191844
+ const controllerRef = (0, react$1.useRef)();
191845
+ const snapshot = (0, react$1.useSyncExternalStore)(directory.subscribe, () => directory.get(accountKey), () => directory.get(accountKey));
191846
+ const summary = snapshot.summary;
191847
+ const queryValid = directoryQueryValid(input);
191848
+ const queryPending = normalizeDirectoryQuery(input) !== queryText;
191849
+ const listMatches = list?.query.filter === filter && list.query.sort === sort && list.query.query === queryText;
191632
191850
  (0, react$1.useEffect)(() => {
191633
- setVisibleCount(100);
191851
+ if (!directory.hasPendingSeen(accountKey)) setSeenError("");
191634
191852
  }, [
191853
+ directory,
191635
191854
  accountKey,
191636
- filter,
191637
- query,
191638
- order
191855
+ snapshot
191639
191856
  ]);
191857
+ (0, react$1.useEffect)(() => {
191858
+ const timer = setTimeout(() => setQueryText(normalizeDirectoryQuery(input)), 300);
191859
+ return () => clearTimeout(timer);
191860
+ }, [input]);
191861
+ const refresh = (0, react$1.useCallback)(() => {
191862
+ force.current = true;
191863
+ directory.invalidate(accountKey);
191864
+ setRevision((value) => value + 1);
191865
+ }, [directory, accountKey]);
191640
191866
  (0, react$1.useEffect)(() => {
191641
191867
  const controller = new AbortController();
191642
- const retryTimers = [];
191643
- const retry = (error, read) => {
191644
- const delay = speakerRetryDelay(error);
191645
- if (delay !== void 0) retryTimers.push(setTimeout(() => {
191646
- if (!controller.signal.aborted) read();
191647
- }, delay));
191648
- };
191649
- readAccount.current = void 0;
191650
- if (lastAccountKey.current !== accountKey) {
191651
- lastAccountKey.current = accountKey;
191652
- setMarked({
191653
- loading: true,
191654
- items: [],
191655
- error: ""
191656
- });
191657
- setPresence({
191658
- loading: true,
191659
- error: ""
191660
- });
191661
- setUnmarked({
191662
- loading: true,
191663
- complete: false,
191664
- items: [],
191665
- projectionState: void 0,
191666
- error: ""
191667
- });
191668
- setOrderPreference({
191669
- accountKey,
191670
- value: readRecognizedSpeakerOrder(accountKey)
191671
- });
191672
- setQuery("");
191673
- setSelectedMarked(void 0);
191674
- setSelectedCandidate(void 0);
191675
- }
191676
- setMarked((previous) => ({
191677
- ...previous,
191678
- loading: true,
191679
- error: ""
191680
- }));
191681
- setPresence((previous) => ({
191682
- ...previous,
191683
- loading: true,
191684
- error: ""
191685
- }));
191686
- setUnmarked((previous) => ({
191687
- ...previous,
191688
- loading: true,
191689
- complete: false,
191690
- error: ""
191691
- }));
191692
- const cachedMarked = directory.peekMarked(accountKey);
191693
- if (cachedMarked !== void 0) setMarked({
191694
- loading: false,
191695
- items: cachedMarked,
191696
- error: ""
191697
- });
191698
- const readMarked = () => {
191699
- directory.readMarked(accountKey, controller.signal).then((items) => {
191700
- if (!controller.signal.aborted) setMarked({
191701
- loading: false,
191702
- items,
191703
- error: ""
191704
- });
191705
- }).catch((error) => {
191868
+ controllerRef.current = controller;
191869
+ let retry;
191870
+ let busy = false;
191871
+ const cached = directory.peek(accountKey, query);
191872
+ setList(cached);
191873
+ setSelected(void 0);
191874
+ setError("");
191875
+ setMoreLoading(false);
191876
+ const load = async () => {
191877
+ if (busy || !directoryVisible() || controller.signal.aborted) return;
191878
+ if (!directoryQueryValid(query.query)) {
191879
+ setLoading(false);
191880
+ return;
191881
+ }
191882
+ busy = true;
191883
+ setLoading(true);
191884
+ try {
191885
+ const result = await directory.first(accountKey, query, controller.signal, force.current);
191706
191886
  if (!controller.signal.aborted) {
191707
- setMarked((previous) => ({
191708
- ...previous,
191709
- loading: false,
191710
- error: failureMessage(error)
191711
- }));
191712
- retry(error, readMarked);
191887
+ force.current = false;
191888
+ setList(result);
191889
+ setError("");
191713
191890
  }
191714
- });
191891
+ } catch (error) {
191892
+ if (controller.signal.aborted) return;
191893
+ setError(failureMessage(error));
191894
+ const delay = speakerRetryDelay(error);
191895
+ if (delay !== void 0) retry = setTimeout(() => {
191896
+ load();
191897
+ }, delay);
191898
+ } finally {
191899
+ busy = false;
191900
+ if (!controller.signal.aborted) setLoading(false);
191901
+ }
191715
191902
  };
191716
- readMarked();
191717
- const cachedPresence = directory.peekPresence(accountKey);
191718
- if (cachedPresence !== void 0) setPresence({
191719
- loading: false,
191720
- result: cachedPresence,
191721
- error: ""
191903
+ load();
191904
+ const unwatch = directory.watch(accountKey, () => {
191905
+ if (controller.signal.aborted) return;
191906
+ const current = listRef.current;
191907
+ if (!current || current.coverage === "unknown" || directory.needsReload(accountKey, current)) load();
191722
191908
  });
191723
- const readPresence = () => {
191724
- directory.readPresence(accountKey, controller.signal).then((result) => {
191725
- if (!controller.signal.aborted) setPresence({
191726
- loading: false,
191727
- result,
191728
- error: ""
191729
- });
191730
- }).catch((error) => {
191731
- if (!controller.signal.aborted) {
191732
- setPresence((previous) => ({
191733
- ...previous,
191734
- loading: false,
191735
- error: failureMessage(error)
191736
- }));
191737
- retry(error, readPresence);
191738
- }
191739
- });
191909
+ return () => {
191910
+ controller.abort();
191911
+ clearTimeout(retry);
191912
+ unwatch();
191740
191913
  };
191741
- readPresence();
191742
- const readCandidates = () => {
191743
- directory.readCandidates(accountKey, controller.signal, (snapshot) => {
191744
- if (!controller.signal.aborted) {
191745
- readAccount.current = accountKey;
191746
- setUnmarked({
191747
- ...snapshot,
191748
- loading: true,
191749
- error: ""
191750
- });
191751
- }
191752
- }).then((snapshot) => {
191753
- if (!controller.signal.aborted) setUnmarked({
191754
- ...snapshot,
191755
- loading: false,
191756
- error: ""
191757
- });
191758
- }).catch((error) => {
191759
- if (!controller.signal.aborted) {
191760
- setUnmarked((previous) => ({
191761
- ...previous,
191762
- loading: false,
191763
- complete: false,
191764
- error: failureMessage(error)
191765
- }));
191766
- retry(error, readCandidates);
191914
+ }, [
191915
+ accountKey,
191916
+ directory,
191917
+ query,
191918
+ revision
191919
+ ]);
191920
+ (0, react$1.useEffect)(() => {
191921
+ if (!list || loading || queryPending || list.query.filter !== filter || list.query.query !== queryText) return;
191922
+ let disposed = false;
191923
+ const confirm = () => {
191924
+ if (directoryVisible()) directory.confirmDisplayed(accountKey, list).then(() => {
191925
+ if (!disposed) {
191926
+ setSeenError("");
191927
+ if (directory.needsReload(accountKey, list)) refresh();
191767
191928
  }
191929
+ }).catch(() => {
191930
+ if (!disposed) setSeenError(tr$1("查看状态暂未同步,联网后自动重试。"));
191768
191931
  });
191769
191932
  };
191770
- readCandidates();
191933
+ confirm();
191934
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", confirm);
191771
191935
  return () => {
191772
- controller.abort();
191773
- retryTimers.forEach(clearTimeout);
191936
+ disposed = true;
191937
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", confirm);
191774
191938
  };
191775
191939
  }, [
191776
191940
  accountKey,
191777
- refreshRevision,
191778
- directory
191941
+ directory,
191942
+ list,
191943
+ loading,
191944
+ queryPending,
191945
+ filter,
191946
+ queryText,
191947
+ refresh
191779
191948
  ]);
191949
+ const recoverDetail = (0, react$1.useCallback)(() => {
191950
+ if (!selected || recoveryAttempts.current >= 1) {
191951
+ setSelected(void 0);
191952
+ refresh();
191953
+ return;
191954
+ }
191955
+ recoveryAttempts.current += 1;
191956
+ const controller = controllerRef.current;
191957
+ if (!controller) return;
191958
+ directory.first(accountKey, query, controller.signal, true).then((next) => {
191959
+ if (controller.signal.aborted) return;
191960
+ setList(next);
191961
+ setSelected(next.items.find((item) => item.personKey === selected.personKey));
191962
+ }).catch((error) => {
191963
+ if (!controller.signal.aborted) {
191964
+ setSelected(void 0);
191965
+ setError(failureMessage(error));
191966
+ }
191967
+ });
191968
+ }, [
191969
+ accountKey,
191970
+ directory,
191971
+ selected,
191972
+ query,
191973
+ refresh
191974
+ ]);
191975
+ const loadMore = async () => {
191976
+ const controller = controllerRef.current;
191977
+ if (!list || !controller || moreLoading || queryPending) return;
191978
+ setMoreLoading(true);
191979
+ setError("");
191980
+ try {
191981
+ const next = await directory.more(accountKey, list, controller.signal);
191982
+ if (!controller.signal.aborted) setList(next);
191983
+ } catch (error) {
191984
+ if (!controller.signal.aborted) setError(failureMessage(error));
191985
+ } finally {
191986
+ if (!controller.signal.aborted) setMoreLoading(false);
191987
+ }
191988
+ };
191989
+ const disabled = summary?.state === "disabled" || list?.state === "disabled";
191990
+ const rows = disabled || queryPending || !listMatches ? [] : list?.items ?? [];
191991
+ const avatarBatch = JSON.stringify(rows.filter((row) => row.type === "marked" && avatars[row.detailRef] === void 0).slice(0, 50).map((row) => row.detailRef));
191780
191992
  (0, react$1.useEffect)(() => {
191781
- const state = presence.result?.state;
191782
- if (presence.loading || presence.error !== "" || state !== "building" && state !== "stale" && state !== "failed") return;
191993
+ const refs = JSON.parse(avatarBatch);
191994
+ if (refs.length === 0) return;
191783
191995
  const controller = new AbortController();
191784
- const timer = setTimeout(() => {
191785
- directory.refreshPresence(accountKey, controller.signal).then((result) => {
191786
- if (!controller.signal.aborted) setPresence({
191787
- loading: false,
191788
- result,
191789
- error: ""
191790
- });
191791
- }).catch((error) => {
191792
- if (!controller.signal.aborted) setPresence((previous) => ({
191996
+ let retry;
191997
+ let busy = false, retryAt = 0;
191998
+ const load = async () => {
191999
+ if (!directoryVisible() || controller.signal.aborted || busy) return;
192000
+ if (retryAt > Date.now()) {
192001
+ clearTimeout(retry);
192002
+ retry = setTimeout(() => {
192003
+ load();
192004
+ }, retryAt - Date.now());
192005
+ return;
192006
+ }
192007
+ busy = true;
192008
+ try {
192009
+ const result = await directory.avatars(accountKey, refs, controller.signal);
192010
+ if (controller.signal.aborted) return;
192011
+ const resolved = new Map(result.map((item) => [item.detailRef, item.avatarRef]));
192012
+ setAvatars((previous) => ({
191793
192013
  ...previous,
191794
- loading: false,
191795
- error: failureMessage(error)
192014
+ ...Object.fromEntries(refs.map((ref) => [ref, resolved.get(ref) || null]))
191796
192015
  }));
191797
- });
191798
- }, Math.max(5e3, presence.result?.retryAfterMs ?? 1e4));
192016
+ } catch (error) {
192017
+ if (controller.signal.aborted) return;
192018
+ const delay = speakerRetryDelay(error);
192019
+ if (delay !== void 0) {
192020
+ retryAt = Date.now() + Math.max(15e3, delay);
192021
+ retry = setTimeout(() => {
192022
+ load();
192023
+ }, retryAt - Date.now());
192024
+ }
192025
+ } finally {
192026
+ busy = false;
192027
+ }
192028
+ };
192029
+ const wake = () => {
192030
+ clearTimeout(retry);
192031
+ load();
192032
+ };
192033
+ load();
192034
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", wake);
192035
+ if (typeof window !== "undefined") window.addEventListener("online", wake);
191799
192036
  return () => {
191800
- clearTimeout(timer);
191801
192037
  controller.abort();
192038
+ clearTimeout(retry);
192039
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", wake);
192040
+ if (typeof window !== "undefined") window.removeEventListener("online", wake);
191802
192041
  };
191803
192042
  }, [
191804
192043
  accountKey,
191805
- refreshRevision,
191806
192044
  directory,
191807
- presence.loading,
191808
- presence.error,
191809
- presence.result
191810
- ]);
191811
- const refreshPresenceVersion = (0, react$1.useCallback)(() => {
191812
- setPresence({
191813
- loading: false,
191814
- result: {
191815
- state: "stale",
191816
- scope: "all-history",
191817
- items: [],
191818
- retryAfterMs: 1e3
191819
- },
191820
- error: ""
191821
- });
191822
- }, []);
191823
- const rows = (0, react$1.useMemo)(() => identifiedSpeakerRows(marked.items, unmarked.items), [marked.items, unmarked.items]);
191824
- const hasSelf = rows.some((row) => row.kind === "marked" && row.isCurrentUser);
191825
- (0, react$1.useEffect)(() => {
191826
- if (readAccount.current !== accountKey || marked.loading || marked.error !== "") return;
191827
- if (rows.length > 0 || !unmarked.loading && unmarked.complete) recognizedSpeakerTracker.acknowledge(accountKey);
191828
- if (!unmarked.loading && unmarked.complete && unmarked.error === "") recognizedSpeakerTracker.observe(accountKey, marked.items, unmarked.items, true);
191829
- }, [
191830
- accountKey,
191831
- marked.loading,
191832
- marked.error,
191833
- marked.items,
191834
- unmarked.loading,
191835
- unmarked.complete,
191836
- unmarked.error,
191837
- unmarked.items,
191838
- rows.length
192045
+ avatarBatch
191839
192046
  ]);
191840
- const presenceByOption = (0, react$1.useMemo)(() => new Map(presence.error === "" && presence.result?.state === "fresh" ? presence.result.items.map((item) => [item.optionKey, item]) : []), [presence.result, presence.error]);
191841
- const normalizedQuery = query.trim().toLocaleLowerCase();
191842
- const orderValue = (row) => {
191843
- const stat = row.kind === "marked" ? presenceByOption.get(row.optionKey) : unmarked.projectionState === void 0 || unmarked.projectionState === "fresh" ? row : void 0;
191844
- return {
191845
- key: `${row.kind}:${row.key}`,
191846
- name: row.name,
191847
- dayCount: stat?.dayCount,
191848
- lastSeenAt: stat?.lastSeenAt
191849
- };
191850
- };
191851
- const visible = rows.filter((row) => (filter === "all" || row.kind === filter) && (normalizedQuery === "" || row.name.toLocaleLowerCase().includes(normalizedQuery) || row.kind === "unmarked" && row.subtitle.toLocaleLowerCase().includes(normalizedQuery))).sort((left, right) => Number(right.kind === "marked" && right.isCurrentUser) - Number(left.kind === "marked" && left.isCurrentUser) || compareRecognizedSpeakers(orderValue(left), orderValue(right), order));
191852
- const loading = marked.loading || unmarked.loading;
192047
+ const state = disabled ? tr$1("说话人目录暂不可用,请稍后再试。") : statusText(summary?.snapshotVersion === list?.snapshotVersion ? summary : list?.coverage === "complete" ? list : summary);
192048
+ const hasNewVersion = list?.coverage === "complete" && summary?.snapshotVersion && summary.snapshotVersion !== list.snapshotVersion;
192049
+ const selfMissing = listMatches && filter === "all" && queryText === "" && !queryPending && list?.coverage === "complete" && list.state === "fresh" && !list.items.some((row) => row.isSelf);
191853
192050
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192051
+ ref: rootRef,
191854
192052
  style: styles$20.root,
191855
192053
  "data-arkme-owned": "recognized-speakers-surface",
191856
192054
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
@@ -191890,6 +192088,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191890
192088
  marginLeft: "auto"
191891
192089
  },
191892
192090
  onClick: refresh,
192091
+ disabled: loading,
191893
192092
  children: tr$1("刷新")
191894
192093
  })
191895
192094
  ]
@@ -191898,41 +192097,44 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191898
192097
  style: styles$20.filters,
191899
192098
  children: [
191900
192099
  [
191901
- ["all", "全部"],
191902
- ["marked", "已标记"],
191903
- ["unmarked", "未标记"]
191904
- ].map(([kind, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192100
+ [
192101
+ "all",
192102
+ "全部",
192103
+ summary?.totalCount
192104
+ ],
192105
+ [
192106
+ "marked",
192107
+ "已标记",
192108
+ summary?.markedCount
192109
+ ],
192110
+ [
192111
+ "unmarked",
192112
+ "未标记",
192113
+ summary?.unmarkedCount
192114
+ ]
192115
+ ].map(([kind, label, count]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
191905
192116
  type: "button",
191906
192117
  "aria-pressed": filter === kind,
191907
192118
  style: {
191908
192119
  ...styles$20.filter,
191909
192120
  ...filter === kind ? styles$20.selectedFilter : {}
191910
192121
  },
191911
- onClick: () => {
191912
- setFilter(kind);
191913
- setSelectedCandidate(void 0);
191914
- setSelectedMarked(void 0);
191915
- },
191916
- children: tr$1(label)
192122
+ onClick: () => setFilter(kind),
192123
+ children: [tr$1(label), !disabled && count != null ? ` ${count}` : ""]
191917
192124
  }, kind)),
191918
192125
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
191919
192126
  "aria-label": tr$1("搜索说话人"),
191920
- placeholder: tr$1("搜索说话人"),
191921
- value: query,
191922
- onChange: (event) => {
191923
- setQuery(event.target.value);
191924
- },
192127
+ placeholder: tr$1("搜索名称或编号"),
192128
+ value: input,
192129
+ onChange: (event) => setInput(event.target.value),
191925
192130
  style: styles$20.search
191926
192131
  }),
191927
192132
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
191928
192133
  "aria-label": tr$1("说话人排序"),
191929
- value: order,
192134
+ value: sort,
191930
192135
  onChange: (event) => {
191931
192136
  const next = event.target.value === "recent" ? "recent" : "frequent";
191932
- setOrderPreference({
191933
- accountKey,
191934
- value: next
191935
- });
192137
+ setSort(next);
191936
192138
  writeRecognizedSpeakerOrder(accountKey, next);
191937
192139
  },
191938
192140
  style: styles$20.sort,
@@ -191946,186 +192148,115 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
191946
192148
  })
191947
192149
  ]
191948
192150
  }),
191949
- !marked.loading && marked.error === "" && !hasSelf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerSelfGuide, { onOpenRecordings: onBack }, accountKey),
192151
+ !queryValid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192152
+ role: "alert",
192153
+ style: styles$20.error,
192154
+ children: tr$1("搜索内容过长,请缩短至 256 字节以内。")
192155
+ }),
192156
+ selfMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerSelfGuide, { onOpenRecordings: onBack }, accountKey),
192157
+ hasNewVersion && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192158
+ role: "status",
192159
+ style: styles$20.note,
192160
+ children: tr$1("目录有更新,刷新后查看最新人物。")
192161
+ }),
192162
+ state && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192163
+ role: "status",
192164
+ style: styles$20.note,
192165
+ children: state
192166
+ }),
192167
+ seenError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
192168
+ role: "status",
192169
+ style: styles$20.note,
192170
+ children: seenError
192171
+ }),
192172
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192173
+ role: "alert",
192174
+ style: styles$20.error,
192175
+ children: error
192176
+ }),
191950
192177
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
191951
192178
  className: "arkme-recognized-speakers-grid",
191952
- "data-detail-open": selectedCandidate === void 0 && selectedMarked === void 0 ? "false" : "true",
191953
- children: [
191954
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
191955
- className: "arkme-recognized-speakers-list",
191956
- children: [
191957
- filter !== "marked" && unmarked.loading && !unmarked.complete && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
191958
- role: "status",
191959
- style: styles$20.note,
191960
- children: tr$1("正在补齐说话人列表,排序仍在更新…")
191961
- }),
191962
- filter !== "marked" && !unmarked.loading && !unmarked.complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
191963
- role: "status",
191964
- style: styles$20.note,
191965
- children: tr$1("列表尚未完整,当前仅对已加载的说话人排序和搜索。")
191966
- }),
191967
- marked.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
191968
- role: "alert",
191969
- style: styles$20.error,
191970
- children: [
191971
- tr$1("已标记说话人读取失败:"),
191972
- " ",
191973
- marked.error
191974
- ]
191975
- }),
191976
- unmarked.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
191977
- role: "alert",
191978
- style: styles$20.error,
191979
- children: [
191980
- tr$1("未标记说话人读取失败:"),
191981
- " ",
191982
- unmarked.error
191983
- ]
191984
- }),
191985
- unmarked.projectionState === "building" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191986
- role: "status",
191987
- style: styles$20.state,
191988
- children: tr$1("未标记说话人正在整理,结果可能不完整。")
191989
- }),
191990
- unmarked.projectionState === "stale" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191991
- role: "status",
191992
- style: styles$20.state,
191993
- children: tr$1("未标记说话人正在更新,结果可能不完整。")
191994
- }),
191995
- unmarked.projectionState === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191996
- role: "alert",
191997
- style: styles$20.error,
191998
- children: tr$1("未标记说话人整理失败,请稍后刷新。")
191999
- }),
192000
- loading && rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192001
- role: "status",
192002
- style: styles$20.state,
192003
- children: tr$1("正在加载说话人…")
192004
- }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192005
- role: "status",
192006
- style: styles$20.state,
192007
- children: loading ? tr$1("正在更新说话人…") : rows.length === 0 && marked.error === "" && unmarked.error === "" && (unmarked.projectionState === void 0 || unmarked.projectionState === "fresh") ? tr$1("暂无已识别说话人") : tr$1("暂无可显示的说话人")
192008
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
192009
- style: styles$20.list,
192010
- children: visible.slice(0, visibleCount).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: row.kind === "unmarked" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192011
- type: "button",
192012
- className: "arkme-recognized-speakers-row",
192013
- "aria-current": selectedCandidate === row.candidateRef ? "true" : void 0,
192014
- style: {
192015
- ...styles$20.row,
192016
- cursor: "pointer"
192017
- },
192018
- onClick: () => {
192019
- setSelectedCandidate(row.candidateRef);
192020
- setSelectedMarked(void 0);
192021
- },
192022
- children: [
192023
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
192024
- token: row.token,
192025
- size: 38,
192026
- label: row.name
192027
- }),
192028
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192029
- style: styles$20.copy,
192030
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192031
- style: styles$20.name,
192032
- children: row.name
192033
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192034
- style: styles$20.meta,
192035
- children: row.subtitle
192036
- })]
192037
- }),
192038
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192039
- style: styles$20.badge,
192040
- children: [tr$1("未标记"), " ›"]
192041
- })
192042
- ]
192043
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192044
- type: "button",
192045
- className: "arkme-recognized-speakers-row",
192046
- "aria-current": selectedMarked?.key === row.key ? "true" : void 0,
192047
- style: {
192048
- ...styles$20.row,
192049
- cursor: "pointer"
192050
- },
192051
- onClick: () => {
192052
- setSelectedMarked(row);
192053
- setSelectedCandidate(void 0);
192054
- },
192055
- children: [
192056
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeUserAvatar, {
192057
- ...row.avatarRef === void 0 ? {} : { avatarRef: row.avatarRef },
192058
- size: 38,
192059
- label: row.name
192060
- }),
192061
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192062
- style: styles$20.copy,
192063
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192064
- style: styles$20.name,
192065
- children: [row.name, row.isCurrentUser ? ` · ${tr$1("我")}` : ""]
192066
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192067
- style: styles$20.meta,
192068
- children: markedPresenceLabel(presenceByOption.get(row.optionKey), presence.result, presence.loading, presence.error)
192069
- })]
192070
- }),
192071
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192072
- style: styles$20.badge,
192073
- children: [tr$1("已标记"), " ›"]
192074
- })
192075
- ]
192076
- }) }, `${row.kind}:${row.key}`))
192077
- }),
192078
- visible.length > visibleCount && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192079
- type: "button",
192080
- style: {
192081
- ...styles$20.button,
192082
- marginTop: 14
192083
- },
192084
- onClick: () => {
192085
- setVisibleCount((count) => count + 100);
192086
- },
192087
- children: tr$1("显示更多说话人")
192088
- })
192089
- ]
192090
- }),
192091
- selectedCandidate !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192092
- style: styles$20.detail,
192093
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192179
+ "data-detail-open": selected !== void 0 ? "true" : "false",
192180
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192181
+ className: "arkme-recognized-speakers-list",
192182
+ "aria-busy": loading || queryPending,
192183
+ children: [!disabled && (loading || queryPending) && rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192184
+ role: "status",
192185
+ style: styles$20.state,
192186
+ children: tr$1("正在加载说话人…")
192187
+ }) : !disabled && !error && rows.length === 0 && list?.coverage === "complete" && !queryPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
192188
+ role: "status",
192189
+ style: styles$20.state,
192190
+ children: tr$1(queryText ? "没有匹配的说话人" : filter === "all" ? "暂无已识别说话人" : "该分类暂无说话人")
192191
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
192192
+ style: styles$20.list,
192193
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
192094
192194
  type: "button",
192095
- className: "arkme-recognized-speakers-mobile-back",
192096
- style: styles$20.button,
192097
- onClick: () => {
192098
- setSelectedCandidate(void 0);
192195
+ className: "arkme-recognized-speakers-row",
192196
+ "aria-current": selected?.personKey === row.personKey ? "true" : void 0,
192197
+ style: {
192198
+ ...styles$20.row,
192199
+ cursor: "pointer"
192099
192200
  },
192100
- children: tr$1("‹ 返回列表")
192101
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerDetail, {
192102
- accountKey,
192103
- candidateRef: selectedCandidate,
192104
- onDirectoryRefresh: refresh,
192105
- onCandidateCleared: () => {
192106
- setSelectedCandidate(void 0);
192107
- }
192108
- }, `${accountKey}:${selectedCandidate}`)]
192109
- }),
192110
- selectedMarked !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192111
- style: styles$20.detail,
192112
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192113
- type: "button",
192114
- className: "arkme-recognized-speakers-mobile-back",
192115
- style: styles$20.button,
192116
192201
  onClick: () => {
192117
- setSelectedMarked(void 0);
192202
+ recoveryAttempts.current = 0;
192203
+ setSelected(row);
192118
192204
  },
192119
- children: tr$1("‹ 返回列表")
192120
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkedSpeakerDetail, {
192121
- accountKey,
192122
- speaker: selectedMarked,
192123
- loadMembers: loadMarkedMembers,
192124
- version: presence.result?.version,
192125
- onVersionChanged: refreshPresenceVersion
192126
- }, `${accountKey}:${selectedMarked.speakerRef}`)]
192127
- })
192128
- ]
192205
+ children: [
192206
+ row.type === "unmarked" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnmarkedSpeakerTokenAvatar, {
192207
+ token: row.displayNumber > 0 ? String(row.displayNumber) : "",
192208
+ size: 38,
192209
+ label: row.displayName
192210
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArkmeUserAvatar, {
192211
+ ...avatars[row.detailRef] ? { avatarRef: avatars[row.detailRef] } : {},
192212
+ fallbackContent: speakerNameAvatarFallback(row),
192213
+ size: 38,
192214
+ label: row.displayName
192215
+ }),
192216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192217
+ style: styles$20.copy,
192218
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192219
+ style: styles$20.name,
192220
+ children: [row.displayName, row.isSelf ? ` · ${tr$1("我")}` : ""]
192221
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
192222
+ style: styles$20.meta,
192223
+ children: personStats(row)
192224
+ })]
192225
+ }),
192226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
192227
+ style: styles$20.badge,
192228
+ children: [tr$1(row.type === "marked" ? "已标记" : "未标记"), " ›"]
192229
+ })
192230
+ ]
192231
+ }) }, row.personKey))
192232
+ }), !disabled && !queryPending && list?.hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192233
+ type: "button",
192234
+ disabled: moreLoading || loading,
192235
+ style: {
192236
+ ...styles$20.button,
192237
+ marginTop: 14
192238
+ },
192239
+ onClick: () => {
192240
+ loadMore();
192241
+ },
192242
+ children: tr$1(moreLoading ? "正在加载…" : "加载更多说话人")
192243
+ })]
192244
+ }), selected && !disabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192245
+ style: styles$20.detail,
192246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
192247
+ type: "button",
192248
+ className: "arkme-recognized-speakers-mobile-back",
192249
+ style: styles$20.button,
192250
+ onClick: () => setSelected(void 0),
192251
+ children: tr$1("‹ 返回列表")
192252
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonDetail, {
192253
+ row: selected,
192254
+ accountKey,
192255
+ directory,
192256
+ loadMembers: loadMarkedMembers,
192257
+ onChanged: recoverDetail
192258
+ }, selected.detailRef)]
192259
+ })]
192129
192260
  })
192130
192261
  ]
192131
192262
  })]
@@ -212533,7 +212664,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
212533
212664
  const c$3 = e$20;
212534
212665
  //#endregion
212535
212666
  //#region package.json
212536
- var version = "0.1.85";
212667
+ var version = "0.1.87";
212537
212668
  //#endregion
212538
212669
  //#region src/client/arkme-membership.ts
212539
212670
  function membershipLabel(state) {
@@ -234417,7 +234548,6 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
234417
234548
  return result;
234418
234549
  }, { signal }), controller.signal);
234419
234550
  page = refreshWindow === void 0 ? await readPage(cursor) : await readConversationTimelineWindow(refreshWindow, readPage, controller.signal);
234420
- if (refreshWindow === void 0 && intent !== "return-to-latest") await prepareTimelineReactions(page.items, controller.signal);
234421
234551
  } catch (caught) {
234422
234552
  if (isArkmeRequestAbort(caught, controller.signal)) return;
234423
234553
  throw caught;
@@ -234510,7 +234640,6 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
234510
234640
  acknowledgeRead,
234511
234641
  confirmedSendRetention,
234512
234642
  interwovenMoments,
234513
- prepareTimelineReactions,
234514
234643
  source,
234515
234644
  sourceIsChat,
234516
234645
  sourceProjectionRevision
@@ -236422,6 +236551,43 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
236422
236551
  return projected.isMuted === true;
236423
236552
  }, [updateSourceProjection]);
236424
236553
  const conversationMemberByRef = (0, react$1.useMemo)(() => new Map(conversationMembers.map((member) => [member.memberRef, member])), [conversationMembers]);
236554
+ const localReactionActors = (0, react$1.useMemo)(() => {
236555
+ const names = /* @__PURE__ */ new Map();
236556
+ for (const item of items) if (item.memberRef && !item.selfRole && item.senderKind !== "bot") names.set(item.memberRef, {
236557
+ displayName: arkmeTimelineSenderName(item, selfProfile),
236558
+ ...item.avatarRef ? { avatarRef: item.avatarRef } : {}
236559
+ });
236560
+ for (const member of conversationMembers) if (member.status === "active" && member.displayName.trim() && member.displayName !== "群成员") names.set(member.memberRef, {
236561
+ displayName: member.displayName,
236562
+ ...member.avatarRef ? { avatarRef: member.avatarRef } : {}
236563
+ });
236564
+ return names;
236565
+ }, [
236566
+ items,
236567
+ selfProfile,
236568
+ conversationMembers
236569
+ ]);
236570
+ const resolveReactionActor = (0, react$1.useCallback)((actor) => {
236571
+ const self = conversationMembers.find((member) => member.isSelf);
236572
+ const local = actor.userId === authenticatedUserId ? {
236573
+ displayName: (source?.kind === "group_chat" ? self?.memberName : void 0) || selfProfile?.displayName || selfProfile?.nickname || "我",
236574
+ ...selfProfile?.avatarRef ? { avatarRef: selfProfile.avatarRef } : {}
236575
+ } : source?.kind === "private_chat" && actor.userId === source.peerUserId ? {
236576
+ displayName: source.displayName,
236577
+ ...source.avatarRef ? { avatarRef: source.avatarRef } : {}
236578
+ } : actor.memberRef ? localReactionActors.get(actor.memberRef) : void 0;
236579
+ return local ? {
236580
+ ...actor,
236581
+ ...local,
236582
+ presentationPending: false
236583
+ } : void 0;
236584
+ }, [
236585
+ authenticatedUserId,
236586
+ source,
236587
+ selfProfile,
236588
+ conversationMembers,
236589
+ localReactionActors
236590
+ ]);
236425
236591
  const composerMentionsEnabled = source?.kind === "group_chat" || source?.kind === "private_chat";
236426
236592
  const mentionCandidates = (0, react$1.useMemo)(() => {
236427
236593
  if (!composerMentionsEnabled || mentionTrigger === void 0) return [];
@@ -238971,6 +239137,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
238971
239137
  },
238972
239138
  children: [!isSharedRecordingCard && !isExtensionMessage && messageHeader, (() => {
238973
239139
  const reactionTarget = {
239140
+ resolveActor: resolveReactionActor,
238974
239141
  sourceKey: source.sourceKey,
238975
239142
  itemUid: item.itemUid,
238976
239143
  id: `${conversationKey}:${arkmeTimelineOccurrenceKey(item)}`,
@@ -239215,6 +239382,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
239215
239382
  selfProfile,
239216
239383
  selfConversationMember,
239217
239384
  conversationMemberByRef,
239385
+ resolveReactionActor,
239218
239386
  activeSelectMode,
239219
239387
  archiveReadOnly,
239220
239388
  authenticatedAccountKey,
@@ -241885,7 +242053,7 @@ registerProcessor('arkme-pcm-journal', ArkmePcmJournal);
241885
242053
  };
241886
242054
  });
241887
242055
  const foreground = arkmeRealtimeTimelineDeliveryAllowed(browserDocument?.visibilityState, browserDocument?.hasFocus?.() ?? true);
241888
- if (foreground && timelineUpdates.length > 0) arkmeChatTimelineDelta.publish(timelineUpdates);
242056
+ if (timelineUpdates.length > 0) arkmeChatTimelineDelta.publish(timelineUpdates);
241889
242057
  for (const dateStamp of arkmeChatDeltaCalendarDateStamps(update)) arkmeCalendarInvalidations.publish({ dateStamp });
241890
242058
  if (foreground && arkmeSelectedBotAffectedByChatDelta(arkmeUi.getSnapshot().selectedBot, update)) arkmeUi.chatChanged();
241891
242059
  for (const sourceKey of arkmeChatDeltaSourceKeys(update)) arkmeInterwovenInvalidation.invalidate(sourceKey);