@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
@@ -10,6 +10,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
10
10
  import { access, chmod as chmod$1, copyFile, lstat, mkdir, mkdtemp, open, opendir, readFile, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
11
11
  import { fileURLToPath, fileURLToPath as urlToPath } from "node:url";
12
12
  import { request } from "node:http";
13
+ import { AsyncLocalStorage } from "node:async_hooks";
13
14
  import { homedir, hostname, platform, tmpdir } from "node:os";
14
15
  import { request as request$1 } from "node:https";
15
16
  import { BlockList, isIP } from "node:net";
@@ -392,7 +393,7 @@ function keys(value, allowed) {
392
393
  function text$12(value, max) {
393
394
  if (typeof value !== "string" || value.length > max) fail$1();
394
395
  }
395
- function integer$6(value) {
396
+ function integer$7(value) {
396
397
  if (!Number.isSafeInteger(value) || value < 0) fail$1();
397
398
  }
398
399
  /** Validate the browser projection only; owner IDs and arbitrary payload fields are never persisted. */
@@ -408,9 +409,9 @@ function parseArrangementBoardCachePages(value) {
408
409
  "nextOffset",
409
410
  "board"
410
411
  ]);
411
- integer$6(page.total);
412
+ integer$7(page.total);
412
413
  if (typeof page.hasMore !== "boolean" || !Array.isArray(page.items) || page.items.length > 50 || page.items.length > page.total) fail$1();
413
- if (page.nextOffset !== void 0) integer$6(page.nextOffset);
414
+ if (page.nextOffset !== void 0) integer$7(page.nextOffset);
414
415
  if (page.board !== void 0) {
415
416
  const board = object$8(page.board);
416
417
  keys(board, ["supported", "version"]);
@@ -439,9 +440,9 @@ function parseArrangementBoardCachePages(value) {
439
440
  text$12(item.title, 2e4);
440
441
  text$12(item.description, 2e5);
441
442
  text$12(item.reminderState, 256);
442
- integer$6(item.createdAtMillis);
443
- integer$6(item.updatedAtMillis);
444
- for (const key of ["dueAtMillis", "remindAtMillis"]) if (item[key] !== void 0) integer$6(item[key]);
443
+ integer$7(item.createdAtMillis);
444
+ integer$7(item.updatedAtMillis);
445
+ for (const key of ["dueAtMillis", "remindAtMillis"]) if (item[key] !== void 0) integer$7(item[key]);
445
446
  if (item.recognitionState !== void 0) text$12(item.recognitionState, 256);
446
447
  if (item.creationSource !== void 0) {
447
448
  const source = object$8(item.creationSource);
@@ -455,12 +456,12 @@ function parseArrangementBoardCachePages(value) {
455
456
  "input",
456
457
  "none"
457
458
  ].includes(source.kind) || !Array.isArray(source.items) || source.items.length > 100) fail$1();
458
- integer$6(source.unavailableCount);
459
+ integer$7(source.unavailableCount);
459
460
  for (const rawSource of source.items) {
460
461
  const entry = object$8(rawSource);
461
462
  keys(entry, ["text", "createdAtMillis"]);
462
463
  text$12(entry.text, 2e5);
463
- if (entry.createdAtMillis !== void 0) integer$6(entry.createdAtMillis);
464
+ if (entry.createdAtMillis !== void 0) integer$7(entry.createdAtMillis);
464
465
  }
465
466
  }
466
467
  }
@@ -537,6 +538,56 @@ function stringifyOwnerJson(value) {
537
538
  });
538
539
  }
539
540
  //#endregion
541
+ //#region src/reaction-diagnostics.ts
542
+ /** Timing metadata only: no credentials, message refs, names or message bodies. */
543
+ function reactionTraceId(value) {
544
+ return value && /^[a-f0-9-]{36}$/.test(value) ? value : globalThis.crypto.randomUUID();
545
+ }
546
+ function logReactionTiming(traceId, stage, started, detail = {}) {
547
+ console.info(`[ArkmeReactionTiming] ${JSON.stringify({
548
+ traceId,
549
+ stage,
550
+ at: Date.now(),
551
+ durationMs: Math.round((performance.now() - started) * 100) / 100,
552
+ ...detail
553
+ })}`);
554
+ }
555
+ //#endregion
556
+ //#region src/reaction-host-diagnostics.ts
557
+ const context = new AsyncLocalStorage();
558
+ function bindReactionTrace(work) {
559
+ return AsyncLocalStorage.bind(work);
560
+ }
561
+ async function withReactionTrace(input, work) {
562
+ if (process.env.ARKME_REACTION_DIAGNOSTICS !== "1") return await work();
563
+ return await context.run(reactionTraceId(input.traceId), () => measureReaction("host-total", { action: input.action }, work));
564
+ }
565
+ async function measureReaction(stage, detail, work) {
566
+ const traceId = context.getStore();
567
+ if (!traceId) return await work();
568
+ const started = performance.now();
569
+ let ok = false;
570
+ try {
571
+ const result = await work();
572
+ ok = true;
573
+ return result;
574
+ } finally {
575
+ logReactionTiming(traceId, stage, started, {
576
+ ...detail,
577
+ ok
578
+ });
579
+ }
580
+ }
581
+ function reactionQueueObserver(route) {
582
+ const traceId = context.getStore();
583
+ if (!traceId) return void 0;
584
+ const started = performance.now();
585
+ return (event, detail) => logReactionTiming(traceId, `queue-${event}`, started, {
586
+ route,
587
+ ...detail
588
+ });
589
+ }
590
+ //#endregion
540
591
  //#region src/http-retry-after.ts
541
592
  /** HTTP delta-seconds and HTTP-date share one parser; never shorten a server hint. */
542
593
  function retryAfterMillis(value) {
@@ -723,6 +774,10 @@ var ArkmeRequestCoordinator = class {
723
774
  const existing = this.inFlight.get(fullKey);
724
775
  if (existing !== void 0 && existing.epoch === epoch && !existing.controller.signal.aborted) {
725
776
  this.bump(request, "joined");
777
+ request.onQueue?.("join", {
778
+ lane: request.lane,
779
+ service: request.service
780
+ });
726
781
  return await this.joinFlight(existing, request.signal).then(clone);
727
782
  }
728
783
  const controller = new AbortController();
@@ -825,7 +880,7 @@ var ArkmeRequestCoordinator = class {
825
880
  const deadline = recovery === void 0 ? void 0 : setTimeout(() => controller.abort(recovery.timeout()), recovery.deadlineMs);
826
881
  try {
827
882
  while (true) {
828
- release = await this.acquire(request.lane, request.service, controller.signal, route);
883
+ release = await this.acquire(request.lane, request.service, controller.signal, route, request.onQueue);
829
884
  this.bump(request, "started");
830
885
  if (controller.signal.aborted) throw abortError(controller.signal.reason);
831
886
  if (this.epoch(scope) !== epoch) throw new ArkmeStaleRequestError();
@@ -902,7 +957,7 @@ var ArkmeRequestCoordinator = class {
902
957
  });
903
958
  });
904
959
  }
905
- acquire(lane, service, signal, route) {
960
+ acquire(lane, service, signal, route, onQueue) {
906
961
  if (signal.aborted) return Promise.reject(abortError(signal.reason));
907
962
  const laneLimit = this.laneLimits.get(lane);
908
963
  const serviceLimit = this.serviceLimits.get(service);
@@ -928,6 +983,12 @@ var ArkmeRequestCoordinator = class {
928
983
  const index = this.queue.indexOf(queued);
929
984
  if (index >= 0) this.queue.splice(index, 1);
930
985
  signal.removeEventListener("abort", abort);
986
+ onQueue?.("cancel", {
987
+ lane,
988
+ service,
989
+ queueWaitMs: this.now() - queued.queuedAt,
990
+ blockedBy: [...queued.blockedBy ?? []].join(",")
991
+ });
931
992
  reject(abortError(signal.reason));
932
993
  };
933
994
  queued = {
@@ -938,10 +999,24 @@ var ArkmeRequestCoordinator = class {
938
999
  signal,
939
1000
  resolve,
940
1001
  reject,
941
- abort
1002
+ abort,
1003
+ queuedAt: this.now(),
1004
+ ...onQueue ? {
1005
+ onQueue,
1006
+ blockedBy: /* @__PURE__ */ new Set()
1007
+ } : {}
942
1008
  };
943
1009
  signal.addEventListener("abort", abort, { once: true });
944
1010
  this.queue.push(queued);
1011
+ onQueue?.("enter", {
1012
+ lane,
1013
+ service,
1014
+ queueLength: this.queue.length,
1015
+ laneQueued,
1016
+ serviceQueued,
1017
+ laneActive: laneLimit.active,
1018
+ serviceActive: serviceLimit.active
1019
+ });
945
1020
  this.queue.sort((left, right) => LANE_PRIORITY[left.lane] - LANE_PRIORITY[right.lane] || left.sequence - right.sequence);
946
1021
  this.drain();
947
1022
  });
@@ -969,6 +1044,13 @@ var ArkmeRequestCoordinator = class {
969
1044
  queued.route.tokens = Math.max(0, queued.route.tokens - 1);
970
1045
  queued.route.active += 1;
971
1046
  }
1047
+ queued.onQueue?.("start", {
1048
+ lane: queued.lane,
1049
+ service: queued.service,
1050
+ queueWaitMs: this.now() - queued.queuedAt,
1051
+ queueLength: this.queue.length,
1052
+ blockedBy: [...queued.blockedBy ?? []].join(",")
1053
+ });
972
1054
  let released = false;
973
1055
  queued.resolve(() => {
974
1056
  if (released) return;
@@ -993,17 +1075,23 @@ var ArkmeRequestCoordinator = class {
993
1075
  }
994
1076
  const lane = this.laneLimits.get(queued.lane);
995
1077
  const service = this.serviceLimits.get(queued.service);
996
- this.refill(lane);
997
- this.refill(service);
998
- const route = queued.route;
999
- if (route !== void 0) {
1000
- this.refill(route);
1001
- if (route.active >= route.maxConcurrent || route.tokens < 1 || this.now() < route.cooldownUntil) continue;
1002
- }
1003
- if (lane.active < lane.maxConcurrent && service.active < service.maxConcurrent && this.now() >= lane.cooldownUntil && this.now() >= service.cooldownUntil && lane.tokens >= 1 && service.tokens >= 1) return index;
1078
+ const laneBlocked = this.blocked(lane, "lane", queued.blockedBy);
1079
+ const serviceBlocked = this.blocked(service, "service", queued.blockedBy);
1080
+ const routeBlocked = queued.route !== void 0 && this.blocked(queued.route, "route", queued.blockedBy);
1081
+ if (!laneBlocked && !serviceBlocked && !routeBlocked) return index;
1004
1082
  }
1005
1083
  return -1;
1006
1084
  }
1085
+ blocked(limit, kind, reasons) {
1086
+ this.refill(limit);
1087
+ const concurrency = limit.active >= limit.maxConcurrent, rate = limit.tokens < 1, cooldown = this.now() < limit.cooldownUntil;
1088
+ if (reasons) {
1089
+ if (concurrency) reasons.add(`${kind}-concurrency`);
1090
+ if (rate) reasons.add(`${kind}-rate`);
1091
+ if (cooldown) reasons.add(`${kind}-cooldown`);
1092
+ }
1093
+ return concurrency || rate || cooldown;
1094
+ }
1007
1095
  refill(limit) {
1008
1096
  const now = this.now();
1009
1097
  if (now <= limit.lastRefillAt) return;
@@ -1218,7 +1306,7 @@ var HttpArkmeAccountScopeBridge = class {
1218
1306
  this.fetchImpl = fetchImpl;
1219
1307
  }
1220
1308
  async attest(identity) {
1221
- return status$1(await this.request("account.scope.attest", identity));
1309
+ return status$2(await this.request("account.scope.attest", identity));
1222
1310
  }
1223
1311
  async prepare(identity) {
1224
1312
  const value = await this.request("account.scope.prepare", identity);
@@ -1227,17 +1315,17 @@ var HttpArkmeAccountScopeBridge = class {
1227
1315
  return { transitionRef };
1228
1316
  }
1229
1317
  async commit(transitionRef) {
1230
- return status$1(await this.request("account.scope.commit", { transitionRef }));
1318
+ return status$2(await this.request("account.scope.commit", { transitionRef }));
1231
1319
  }
1232
1320
  async abort(transitionRef) {
1233
- if (status$1(await this.request("account.scope.abort", { transitionRef })).status !== "ready") throw new Error("Desktop account scope abort response is invalid");
1321
+ if (status$2(await this.request("account.scope.abort", { transitionRef })).status !== "ready") throw new Error("Desktop account scope abort response is invalid");
1234
1322
  return { status: "ready" };
1235
1323
  }
1236
1324
  async request(action, payload) {
1237
1325
  return await requestArkmeDesktopBridge(this.config, this.fetchImpl, action, payload);
1238
1326
  }
1239
1327
  };
1240
- function status$1(value) {
1328
+ function status$2(value) {
1241
1329
  if (value.status !== "ready" && value.status !== "relaunch") throw new Error("Desktop account scope response is invalid");
1242
1330
  return { status: value.status };
1243
1331
  }
@@ -1631,6 +1719,16 @@ var ServiceRuntime = class {
1631
1719
  "/api/v1/chats/contacts/list",
1632
1720
  "/api/v1/chats/common-group/query"
1633
1721
  ])).has(path)) return true;
1722
+ if (baseUrl === this.config.recordBaseUrl && (/* @__PURE__ */ new Set([
1723
+ "/api/v1/reactions/query",
1724
+ "/api/v1/reactions/notifications/query",
1725
+ "/api/v1/reactions/actors/query",
1726
+ "/api/v1/reactions/groups/query",
1727
+ "/api/v1/reactions/history/query",
1728
+ "/api/v1/reactions/received/query",
1729
+ "/api/v1/reactions/history-policy/query",
1730
+ "/api/v1/reactions/library/query"
1731
+ ])).has(path)) return true;
1634
1732
  if (baseUrl === this.config.botBaseUrl && path === "/api/v1/bot/list") return true;
1635
1733
  if (baseUrl === this.config.audioBaseUrl && path === "/api/v1/audio/unmarked-speakers/list") return true;
1636
1734
  return false;
@@ -1638,7 +1736,8 @@ var ServiceRuntime = class {
1638
1736
  async post(baseUrl, path, body, bearer, successCodes, signal, preferDataError = false, options = {}, preserveHttpError = false, preserveForbiddenError = false) {
1639
1737
  const read = this.registeredRead(baseUrl, path) && !(body instanceof FormData);
1640
1738
  const route = `${baseUrl.replace(/\/+$/, "")}${path}`;
1641
- return await this.requestCoordinator.run({
1739
+ const onQueue = reactionQueueObserver(path);
1740
+ return await measureReaction("upstream-total", { route: path }, () => this.requestCoordinator.run({
1642
1741
  scope: options.scope ?? "public",
1643
1742
  lane: options.lane ?? "write",
1644
1743
  service: options.service ?? this.requestService(baseUrl),
@@ -1649,7 +1748,7 @@ var ServiceRuntime = class {
1649
1748
  ...options.cancelWhenUnobserved === void 0 ? {} : { cancelWhenUnobserved: options.cancelWhenUnobserved },
1650
1749
  ...signal === void 0 ? {} : { signal },
1651
1750
  ...read ? {
1652
- lane: options.lane === "background-read" ? "background-read" : "interactive-read",
1751
+ lane: options.lane === "background-read" || path === "/api/v1/reactions/notifications/query" ? "background-read" : "interactive-read",
1653
1752
  route,
1654
1753
  key: `owner-read:${route}:${stableReadParameters(body)}`,
1655
1754
  cacheMs: 0,
@@ -1670,8 +1769,9 @@ var ServiceRuntime = class {
1670
1769
  "login-expired"
1671
1770
  ].includes(error.code),
1672
1771
  serviceCooldownMs: (error) => read || options.publishServiceCooldown === false ? 0 : this.remoteServiceCooldownMs(error),
1673
- operation: async (coordinatedSignal) => await this.postDirect(baseUrl, path, body, bearer, successCodes, coordinatedSignal, preferDataError, preserveHttpError, preserveForbiddenError)
1674
- }).catch((error) => {
1772
+ ...onQueue ? { onQueue } : {},
1773
+ operation: bindReactionTrace(async (coordinatedSignal) => await measureReaction("upstream-http", { route: path }, () => this.postDirect(baseUrl, path, body, bearer, successCodes, coordinatedSignal, preferDataError, preserveHttpError, preserveForbiddenError)))
1774
+ })).catch((error) => {
1675
1775
  if (options.trackWriteOutcome === true && error instanceof ArkmeRequestQueueOverflowError) throw new ArkmePluginError("arkme-request-queue-full", "发送请求较多,请稍后重试", true, 503, { cause: error });
1676
1776
  if (options.trackWriteOutcome === true && error instanceof Error && error.name === "AbortError") throw new ArkmePluginError("arkme-request-aborted", "发送请求已取消", true, 409, { cause: error });
1677
1777
  if (options.trackWriteOutcome === true && error instanceof ArkmePluginError && remoteWriteOutcomeUnknown(error)) throw withUnknownWriteOutcome(error);
@@ -1699,7 +1799,11 @@ var ServiceRuntime = class {
1699
1799
  });
1700
1800
  if (response.status === 401 || response.status === 403 && !preserveForbiddenError) throw new ArkmePluginError(`auth-http-${response.status}`, "Arkme 登录凭据已失效", false, response.status);
1701
1801
  if (!response.ok) {
1702
- const retryAfter = retryAfterMillis(response.headers.get("retry-after"));
1802
+ let retryAfter = retryAfterMillis(response.headers.get("retry-after"));
1803
+ if (path.startsWith("/api/v1/audio/speaker-directory/") && response.status === 429) try {
1804
+ const milliseconds = objectValue$2(objectValue$2(await response.clone().json()).data).retry_after_ms;
1805
+ if (typeof milliseconds === "number" && Number.isFinite(milliseconds) && milliseconds >= 0) retryAfter = Math.max(retryAfter ?? 0, milliseconds);
1806
+ } catch {}
1703
1807
  if (preserveHttpError) {
1704
1808
  let errorEnvelope;
1705
1809
  try {
@@ -1855,6 +1959,13 @@ var ServiceRuntime = class {
1855
1959
  if (!(error instanceof ArkmePluginError) || !["auth-http-401", "auth-http-403"].includes(error.code)) throw error;
1856
1960
  session = await this.refreshAccessToken(session);
1857
1961
  return await this.post(this.config.recordBaseUrl, path, body, session.accessToken, [0], signal, false, requestOptions());
1962
+ } finally {
1963
+ if ([
1964
+ "/api/v1/reactions/set",
1965
+ "/api/v1/reactions/notifications/read",
1966
+ "/api/v1/reactions/history-policy/set",
1967
+ "/api/v1/reactions/library/set"
1968
+ ].includes(path)) this.requestCoordinator.invalidateKey(this.requestScope(session.userId), `owner-read:${this.config.recordBaseUrl.replace(/\/+$/, "")}/api/v1/reactions/`);
1858
1969
  }
1859
1970
  }
1860
1971
  async authenticatedCalendarPost(path, body, initialSession, signal, options = {}) {
@@ -2053,6 +2164,228 @@ var ServiceRuntime = class {
2053
2164
  }
2054
2165
  };
2055
2166
  //#endregion
2167
+ //#region src/services/speaker-directory-service.ts
2168
+ const prefix$2 = "arkme-speaker-directory-detail-v1";
2169
+ const invalid$13 = () => new ArkmePluginError("speaker-directory-contract-invalid", "说话人目录响应无效,请稍后重试", true, 502);
2170
+ function integer$6(value) {
2171
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalid$13();
2172
+ return value;
2173
+ }
2174
+ function str(value) {
2175
+ if (typeof value !== "string") throw invalid$13();
2176
+ return value;
2177
+ }
2178
+ function status$1(data) {
2179
+ if (![
2180
+ "fresh",
2181
+ "stale",
2182
+ "building",
2183
+ "failed",
2184
+ "disabled",
2185
+ "snapshot_expired"
2186
+ ].includes(String(data.state)) || !["complete", "unknown"].includes(String(data.coverage))) throw invalid$13();
2187
+ return {
2188
+ state: data.state,
2189
+ coverage: data.coverage,
2190
+ retryAfterMs: integer$6(data.retry_after_ms ?? 0),
2191
+ processingSessionCount: integer$6(data.processing_session_count ?? 0),
2192
+ pendingIdentityAggregationCount: integer$6(data.pending_identity_aggregation_count ?? 0),
2193
+ insufficientEvidenceCount: integer$6(data.insufficient_evidence_count ?? 0),
2194
+ scanTruncated: data.scan_truncated === true
2195
+ };
2196
+ }
2197
+ function inputString(value, maxBytes, fallback = "") {
2198
+ if (value === void 0) return fallback;
2199
+ if (typeof value !== "string" || Buffer.byteLength(value) > maxBytes) throw new ArkmePluginError("speaker-directory-input-invalid", "说话人查询条件过长或无效", false);
2200
+ return value;
2201
+ }
2202
+ var SpeakerDirectoryService = class {
2203
+ runtime;
2204
+ recording;
2205
+ unmarked;
2206
+ constructor(runtime, recording, unmarked) {
2207
+ this.runtime = runtime;
2208
+ this.recording = recording;
2209
+ this.unmarked = unmarked;
2210
+ }
2211
+ async request(path, body, signal) {
2212
+ const session = await this.runtime.requireSession();
2213
+ const data = await this.runtime.authenticatedAudioPost(`/api/v1/audio/speaker-directory/${path}`, body, session, signal, {
2214
+ lane: path === "seen" ? "write" : "interactive-read",
2215
+ cacheMs: 0,
2216
+ cancelWhenUnobserved: true
2217
+ });
2218
+ signal?.throwIfAborted();
2219
+ const current = await this.runtime.requireSession();
2220
+ if (current.userId !== session.userId || current.refreshToken !== session.refreshToken) throw new ArkmePluginError("speaker-directory-account-changed", "账号已切换,请重新打开目录", false, 409);
2221
+ return {
2222
+ data: objectValue$2(data),
2223
+ session
2224
+ };
2225
+ }
2226
+ async summary(input, signal) {
2227
+ const body = {};
2228
+ if (input.snapshotVersion !== void 0) body.snapshot_version = inputString(input.snapshotVersion, 128);
2229
+ if (input.seenVersion !== void 0) {
2230
+ if (typeof input.seenVersion !== "number" || !Number.isSafeInteger(input.seenVersion) || input.seenVersion < 0) throw new ArkmePluginError("speaker-directory-input-invalid", "查看版本无效", false);
2231
+ body.seen_version = input.seenVersion;
2232
+ }
2233
+ const { data } = await this.request("summary", body, signal);
2234
+ const state = status$1(data);
2235
+ const count = (value) => value === null || value === void 0 ? null : integer$6(value);
2236
+ const result = {
2237
+ ...state,
2238
+ totalCount: count(data.total_count),
2239
+ markedCount: count(data.marked_count),
2240
+ unmarkedCount: count(data.unmarked_count),
2241
+ unseenCount: count(data.unseen_count),
2242
+ snapshotVersion: str(data.snapshot_version ?? ""),
2243
+ seenVersion: integer$6(data.seen_version ?? 0),
2244
+ updatedAt: integer$6(data.updated_at ?? 0),
2245
+ notModified: data.not_modified === true
2246
+ };
2247
+ if (state.coverage === "complete" && state.state !== "disabled" && state.state !== "snapshot_expired" && (result.totalCount === null || result.markedCount === null || result.unmarkedCount === null || result.unseenCount === null || result.totalCount !== result.markedCount + result.unmarkedCount || result.snapshotVersion === "")) throw invalid$13();
2248
+ return result;
2249
+ }
2250
+ async list(input, signal) {
2251
+ const filter = input.filter ?? "all", sort = input.sort ?? "frequent";
2252
+ const query = inputString(input.query, 4096).trim().toLowerCase();
2253
+ inputString(query, 256);
2254
+ const limit = input.limit === void 0 || input.limit === 0 ? 50 : input.limit;
2255
+ if (![
2256
+ "all",
2257
+ "marked",
2258
+ "unmarked"
2259
+ ].includes(String(filter)) || !["frequent", "recent"].includes(String(sort)) || typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 100) throw new ArkmePluginError("speaker-directory-input-invalid", "说话人查询条件无效", false);
2260
+ const { data, session } = await this.request("list", {
2261
+ filter,
2262
+ sort,
2263
+ query,
2264
+ limit,
2265
+ snapshot_version: inputString(input.snapshotVersion, 128),
2266
+ cursor: inputString(input.cursor, 4096)
2267
+ }, signal);
2268
+ const state = status$1(data);
2269
+ if (state.state === "disabled" || state.state === "snapshot_expired" || state.coverage === "unknown") return {
2270
+ ...state,
2271
+ items: [],
2272
+ hasMore: false,
2273
+ nextCursor: "",
2274
+ snapshotVersion: "",
2275
+ throughCursor: ""
2276
+ };
2277
+ if (!Array.isArray(data.items) || typeof data.has_more !== "boolean") throw invalid$13();
2278
+ const snapshotVersion = str(data.snapshot_version), throughCursor = str(data.through_cursor), nextCursor = str(data.next_cursor);
2279
+ if (!snapshotVersion || !throughCursor || data.has_more && !nextCursor) throw invalid$13();
2280
+ const key = await this.key();
2281
+ const items = data.items.map((value) => {
2282
+ const raw = objectValue$2(value), ref = objectValue$2(raw.detail_ref);
2283
+ if (!["marked", "unmarked"].includes(String(raw.type)) || typeof raw.is_self !== "boolean" || ref.type !== (raw.type === "marked" ? "speaker" : "candidate") || !str(ref.id) || !str(raw.person_key)) throw invalid$13();
2284
+ if (ref.expected_version !== void 0) str(ref.expected_version);
2285
+ const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv);
2286
+ const encrypted = Buffer.concat([cipher.update(JSON.stringify({
2287
+ userId: session.userId,
2288
+ environment: this.runtime.config.environment,
2289
+ type: ref.type,
2290
+ id: ref.id,
2291
+ expectedVersion: ref.expected_version
2292
+ })), cipher.final()]);
2293
+ return {
2294
+ personKey: str(raw.person_key),
2295
+ type: raw.type,
2296
+ displayName: str(raw.display_name),
2297
+ displayNumber: integer$6(raw.display_number),
2298
+ isSelf: raw.is_self,
2299
+ dayCount: integer$6(raw.day_count),
2300
+ lastSeenAt: integer$6(raw.last_seen_at),
2301
+ detailRef: [
2302
+ prefix$2,
2303
+ iv.toString("base64url"),
2304
+ encrypted.toString("base64url"),
2305
+ cipher.getAuthTag().toString("base64url")
2306
+ ].join(".")
2307
+ };
2308
+ });
2309
+ signal?.throwIfAborted();
2310
+ return {
2311
+ ...state,
2312
+ items,
2313
+ hasMore: data.has_more,
2314
+ nextCursor,
2315
+ snapshotVersion,
2316
+ throughCursor
2317
+ };
2318
+ }
2319
+ async seen(input, signal) {
2320
+ const cursor = inputString(input.throughCursor, 4096);
2321
+ if (!cursor) throw new ArkmePluginError("speaker-directory-input-invalid", "缺少目录查看凭据", false);
2322
+ const { data } = await this.request("seen", { through_cursor: cursor }, signal);
2323
+ if (data.success === true) return {
2324
+ success: true,
2325
+ seenVersion: integer$6(data.seen_version)
2326
+ };
2327
+ if (data.state === "disabled" || data.state === "snapshot_expired") return {
2328
+ success: false,
2329
+ state: data.state,
2330
+ retryAfterMs: integer$6(data.retry_after_ms ?? 0)
2331
+ };
2332
+ throw invalid$13();
2333
+ }
2334
+ async open(input, signal) {
2335
+ const session = await this.runtime.requireSession();
2336
+ const ref = await this.readDetailRef(input.detailRef, session.userId);
2337
+ signal?.throwIfAborted();
2338
+ if (ref.type === "candidate") return {
2339
+ type: "candidate",
2340
+ candidateRef: await this.unmarked.directoryCandidateRef(str(ref.id), signal)
2341
+ };
2342
+ if (ref.type !== "speaker") throw invalid$13();
2343
+ return {
2344
+ type: "speaker",
2345
+ speakerRef: await this.recording.directorySpeakerRef(str(ref.id), session.userId),
2346
+ ...ref.expectedVersion === void 0 ? {} : { expectedVersion: str(ref.expectedVersion) }
2347
+ };
2348
+ }
2349
+ async avatars(input, signal) {
2350
+ if (!Array.isArray(input.detailRefs) || input.detailRefs.length > 50) throw new ArkmePluginError("speaker-directory-input-invalid", "头像查询参数无效", false);
2351
+ const session = await this.runtime.requireSession();
2352
+ const requested = await Promise.all([...new Set(input.detailRefs)].map(async (detailRef) => {
2353
+ const ref = await this.readDetailRef(detailRef, session.userId);
2354
+ if (ref.type !== "speaker") throw new ArkmePluginError("speaker-directory-input-invalid", "仅查询已标记人物头像", false);
2355
+ return {
2356
+ detailRef: str(detailRef),
2357
+ id: str(ref.id)
2358
+ };
2359
+ }));
2360
+ signal?.throwIfAborted();
2361
+ const avatars = await this.recording.directorySpeakerAvatars(requested.map((item) => item.id), session.userId, signal);
2362
+ const current = await this.runtime.requireSession();
2363
+ if (current.userId !== session.userId || current.refreshToken !== session.refreshToken) throw new ArkmePluginError("speaker-directory-account-changed", "账号已切换", false, 409);
2364
+ signal?.throwIfAborted();
2365
+ return requested.map(({ detailRef, id }) => ({
2366
+ detailRef,
2367
+ ...avatars.has(id) ? { avatarRef: avatars.get(id) } : {}
2368
+ }));
2369
+ }
2370
+ async readDetailRef(value, viewerUserId) {
2371
+ const [kind, iv, bytes, tag, ...extra] = inputString(value, 16384).split(".");
2372
+ let ref;
2373
+ try {
2374
+ if (kind !== prefix$2 || !iv || !bytes || !tag || extra.length) throw new Error("invalid reference");
2375
+ const decipher = createDecipheriv("aes-256-gcm", await this.key(), Buffer.from(iv, "base64url"));
2376
+ decipher.setAuthTag(Buffer.from(tag, "base64url"));
2377
+ ref = objectValue$2(JSON.parse(Buffer.concat([decipher.update(Buffer.from(bytes, "base64url")), decipher.final()]).toString("utf8")));
2378
+ } catch {
2379
+ throw new ArkmePluginError("speaker-directory-detail-invalid", "人物引用已失效,请刷新目录", false, 400);
2380
+ }
2381
+ if (ref.userId !== viewerUserId || ref.environment !== this.runtime.config.environment) throw new ArkmePluginError("speaker-directory-account-mismatch", "人物引用与当前账号不匹配", false, 403);
2382
+ return ref;
2383
+ }
2384
+ async key() {
2385
+ return createHash("sha256").update(await this.runtime.stateStore.uniqueCode()).update(`\0${prefix$2}`).digest();
2386
+ }
2387
+ };
2388
+ //#endregion
2056
2389
  //#region src/services/self-role-service.ts
2057
2390
  /** Account-scoped role recovery. Existing message senders only request a frozen
2058
2391
  * snapshot; role CRUD does not enqueue or resend message content. */
@@ -2946,7 +3279,7 @@ var ReactionService = class {
2946
3279
  const ids = /* @__PURE__ */ new Map();
2947
3280
  const resolve = async (target) => {
2948
3281
  if (!target || typeof target.id !== "string" || !target.id || target.id.length > 1024 || !(typeof target.worldRecordRef === "string" && !target.sourceRef && !target.messageActionRef || !target.worldRecordRef && typeof target.sourceRef === "string" && typeof target.messageActionRef === "string")) throw new ArkmePluginError("reaction-target-invalid", "表态目标无效", false);
2949
- return await this.resolve(target);
3282
+ return await measureReaction("target-resolve", {}, () => this.resolve(target));
2950
3283
  };
2951
3284
  switch (input.action) {
2952
3285
  case "notifications":
@@ -2965,6 +3298,7 @@ var ReactionService = class {
2965
3298
  })) };
2966
3299
  break;
2967
3300
  case "query": {
3301
+ if (input.actorPresentation !== void 0 && input.actorPresentation !== "deferred") throw new ArkmePluginError("reaction-query-invalid", "表态展示选项无效", false);
2968
3302
  if (!Array.isArray(input.targets) || input.targets.length < 1 || input.targets.length > 50) throw new ArkmePluginError("reaction-query-invalid", "表态查询数量无效", false);
2969
3303
  const targets = [], seenIDs = /* @__PURE__ */ new Set();
2970
3304
  for (const target of input.targets) {
@@ -2978,6 +3312,7 @@ var ReactionService = class {
2978
3312
  targets.push(wire);
2979
3313
  }
2980
3314
  }
3315
+ targets.sort((left, right) => targetKey(left).localeCompare(targetKey(right)));
2981
3316
  path = "query";
2982
3317
  body = { targets };
2983
3318
  break;
@@ -3060,7 +3395,7 @@ var ReactionService = class {
3060
3395
  }
3061
3396
  const before = await this.runtime.requireSession();
3062
3397
  if (`${this.runtime.config.environment}:${before.userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
3063
- const result = objectValue$2(await this.runtime.authenticatedPost(`/api/v1/reactions/${path}`, body, session, signal));
3398
+ const result = objectValue$2(await measureReaction("reaction-owner", { action: input.action }, () => this.runtime.authenticatedPost(`/api/v1/reactions/${path}`, body, session, signal)));
3064
3399
  const current = await this.runtime.requireSession();
3065
3400
  if (signal?.aborted || `${this.runtime.config.environment}:${current.userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
3066
3401
  if (input.action === "query") {
@@ -52490,6 +52825,12 @@ var ChatService = class {
52490
52825
  rel_uid: reference.relationUid
52491
52826
  };
52492
52827
  }
52828
+ async reactionActorReferences(sourceRef, userIds) {
52829
+ const session = await this.runtime.requireSession();
52830
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
52831
+ if (source.kind !== "private_chat" && source.kind !== "group_chat") return /* @__PURE__ */ new Map();
52832
+ return new Map(await Promise.all([...new Set(userIds)].map(async (id) => [id, await this.sealChatMemberRef(session.userId, source.ownerRef, id)])));
52833
+ }
52493
52834
  /** Keep viewer-owned remarks separate from group nicknames and public profiles. */
52494
52835
  async reactionActorLabels(sourceRef, userIds, signal) {
52495
52836
  const session = await this.runtime.requireSession();
@@ -88088,6 +88429,10 @@ var MediaService = class {
88088
88429
  imageCacheBytes = 0;
88089
88430
  activeImageDownloads = 0;
88090
88431
  imageDownloadWaiters = [];
88432
+ avatarAssets = /* @__PURE__ */ new Map();
88433
+ avatarAssetTimer;
88434
+ imageGeneration = 0;
88435
+ localAvatarReads = /* @__PURE__ */ new Map();
88091
88436
  constructor(runtime, profile, worldImages, recordIdentity, botImages, selfRoleAvatars) {
88092
88437
  this.runtime = runtime;
88093
88438
  this.profile = profile;
@@ -88097,19 +88442,27 @@ var MediaService = class {
88097
88442
  this.selfRoleAvatars = selfRoleAvatars;
88098
88443
  }
88099
88444
  dispose() {
88445
+ this.imageGeneration += 1;
88446
+ clearTimeout(this.avatarAssetTimer);
88447
+ this.avatarAssetTimer = void 0;
88448
+ for (const pending of this.avatarAssets.values()) pending.reject(/* @__PURE__ */ new Error("Avatar image scope changed"));
88449
+ this.avatarAssets.clear();
88100
88450
  this.mediaRefs.clear();
88101
88451
  this.stableMediaRefs.clear();
88102
88452
  this.imageCache.clear();
88103
88453
  this.imageInFlight.clear();
88454
+ this.localAvatarReads.clear();
88104
88455
  this.imageCacheBytes = 0;
88105
88456
  this.imageDownloadWaiters.splice(0).forEach((resolve) => {
88106
88457
  resolve();
88107
88458
  });
88108
88459
  }
88109
88460
  async queryFileAssets(fileAssetUids, signal) {
88461
+ return await this.queryFileAssetsForSession(await this.runtime.requireSession(), fileAssetUids, signal);
88462
+ }
88463
+ async queryFileAssetsForSession(session, fileAssetUids, signal) {
88110
88464
  const unique = [...new Set(fileAssetUids.map((uid) => uid.trim()).filter((uid) => uid !== ""))].slice(0, 50);
88111
88465
  if (unique.length === 0) return [];
88112
- const session = await this.runtime.requireSession();
88113
88466
  return listValue$10((await this.runtime.authenticatedPost("/api/v1/files/assets/query", { file_asset_uids: unique }, session, signal, { lane: "interactive-read" })).items).map((raw) => {
88114
88467
  const item = objectValue$2(raw);
88115
88468
  const previewUrl = safeHttpsUrl(item.preview_url);
@@ -88371,7 +88724,11 @@ var MediaService = class {
88371
88724
  }
88372
88725
  async readImage(imageRef, options = {}) {
88373
88726
  options.signal?.throwIfAborted();
88727
+ const generation = this.imageGeneration;
88374
88728
  const session = await this.runtime.requireSession();
88729
+ const assetMatch = /^file_asset:\/\/([A-Za-z0-9_-]{8,128})$/.exec(imageRef.trim());
88730
+ if (assetMatch !== null) return await this.readFileAssetImage(session, assetMatch[1], options, generation);
88731
+ if (options.cacheOnly) throw new ArkmePluginError("image-cache-miss", "本地没有可用的资产头像", false, 404);
88375
88732
  const isProfileImage = imageRef.trim().startsWith("arkme-profile-image-v1.");
88376
88733
  const isBotImage = imageRef.trim().startsWith("arkme-bot-image-v1.");
88377
88734
  const isSelfRoleImage = imageRef.trim().startsWith("arkme-self-role-image-v1.");
@@ -88420,6 +88777,145 @@ var MediaService = class {
88420
88777
  if (this.imageInFlight.get(cacheKey) === pending) this.imageInFlight.delete(cacheKey);
88421
88778
  }
88422
88779
  }
88780
+ /** Immutable asset bytes survive URL expiry and Host restarts in the existing avatar store. */
88781
+ async readFileAssetImage(session, uid, options, generation) {
88782
+ await this.assertImageAccount(session, generation);
88783
+ options.signal?.throwIfAborted();
88784
+ const byteLimit = Math.min(MAX_SELF_ROLE_AVATAR_BYTES, Math.max(1, Math.trunc(options.maxBytes ?? 8388608)));
88785
+ const diskKey = `asset:${this.runtime.config.environment}:${uid}`;
88786
+ const cacheKey = `${session.userId}:${byteLimit}:${diskKey}`;
88787
+ const cached = this.cachedImage(cacheKey);
88788
+ if (cached !== void 0) return cached;
88789
+ if (options.cacheOnly) {
88790
+ const value = await this.readLocalFileAssetImage(session, uid, diskKey, byteLimit, generation);
88791
+ options.signal?.throwIfAborted();
88792
+ await this.assertImageAccount(session, generation);
88793
+ if (value === void 0) throw new ArkmePluginError("image-cache-miss", "本地没有可用的资产头像", false, 404);
88794
+ this.cacheImage(cacheKey, value);
88795
+ return cloneImageBytes(value);
88796
+ }
88797
+ let pending = this.imageInFlight.get(cacheKey);
88798
+ if (pending === void 0) {
88799
+ pending = this.loadFileAssetImage(session, uid, diskKey, byteLimit, generation);
88800
+ this.imageInFlight.set(cacheKey, pending);
88801
+ }
88802
+ try {
88803
+ const value = await pending;
88804
+ options.signal?.throwIfAborted();
88805
+ await this.assertImageAccount(session, generation);
88806
+ this.cacheImage(cacheKey, value);
88807
+ return cloneImageBytes(value);
88808
+ } finally {
88809
+ if (this.imageInFlight.get(cacheKey) === pending) this.imageInFlight.delete(cacheKey);
88810
+ }
88811
+ }
88812
+ async assertImageAccount(session, generation) {
88813
+ const current = await this.runtime.requireSession();
88814
+ if (generation !== this.imageGeneration || current.userId !== session.userId) throw new ArkmePluginError("image-account-changed", "账号已切换,请重试", false, 409);
88815
+ }
88816
+ async validAvatar(image, byteLimit) {
88817
+ if (image.bytes <= 0 || image.bytes !== image.data.byteLength || image.bytes > byteLimit || imageMediaType(image.data) !== image.mediaType) return false;
88818
+ try {
88819
+ const { default: sharp } = await import("sharp");
88820
+ await sharp(image.data, {
88821
+ failOn: "warning",
88822
+ limitInputPixels: 64 * 1024 * 1024
88823
+ }).resize(1, 1).raw().toBuffer();
88824
+ return true;
88825
+ } catch {
88826
+ return false;
88827
+ }
88828
+ }
88829
+ async readLocalFileAssetImage(session, uid, diskKey, byteLimit, generation) {
88830
+ const key = `${generation}:${session.userId}:${byteLimit}:${diskKey}`;
88831
+ let pending = this.localAvatarReads.get(key);
88832
+ if (pending === void 0) {
88833
+ pending = (async () => {
88834
+ const persisted = await this.runtime.stateStore.readAvatarCache?.(session.userId, diskKey).catch(() => void 0);
88835
+ if (persisted !== void 0 && await this.validAvatar(persisted, byteLimit)) return persisted;
88836
+ await this.assertImageAccount(session, generation);
88837
+ const localRef = await this.runtime.stateStore.selfRoleAvatarLocalRef?.(session.userId, uid).catch(() => void 0);
88838
+ const original = localRef === void 0 ? void 0 : await this.selfRoleAvatars?.read(session.userId, localRef, byteLimit).catch(() => void 0);
88839
+ return original !== void 0 && await this.validAvatar(original, byteLimit) ? original : void 0;
88840
+ })();
88841
+ this.localAvatarReads.set(key, pending);
88842
+ }
88843
+ try {
88844
+ const value = await pending;
88845
+ await this.assertImageAccount(session, generation);
88846
+ return value;
88847
+ } finally {
88848
+ if (this.localAvatarReads.get(key) === pending) this.localAvatarReads.delete(key);
88849
+ }
88850
+ }
88851
+ async loadFileAssetImage(session, uid, diskKey, byteLimit, generation) {
88852
+ let value = await this.readLocalFileAssetImage(session, uid, diskKey, byteLimit, generation);
88853
+ if (value === void 0) {
88854
+ await this.assertImageAccount(session, generation);
88855
+ const asset = await this.queueAvatarAsset(session, uid, generation);
88856
+ const url = asset?.previewUrl ?? asset?.downloadUrl;
88857
+ if (url === void 0) throw new ArkmePluginError("image-ref-unavailable", "历史头像当前不可用", true, 404);
88858
+ await this.assertImageAccount(session, generation);
88859
+ value = await this.withImageDownloadPermit(async () => {
88860
+ await this.assertImageAccount(session, generation);
88861
+ return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, url), byteLimit, void 0, this.runtime.requestScope(session.userId));
88862
+ });
88863
+ if (!await this.validAvatar(value, byteLimit)) throw new ArkmePluginError("image-bytes-invalid", "头像数据损坏,请重试", true, 502);
88864
+ }
88865
+ await this.assertImageAccount(session, generation);
88866
+ await this.runtime.stateStore.writeAvatarCache?.(session.userId, diskKey, value).catch(() => {
88867
+ console.warn("dsh-arkme: avatar_cache_write_failed");
88868
+ });
88869
+ return value;
88870
+ }
88871
+ queueAvatarAsset(session, uid, generation) {
88872
+ const key = `${session.userId}:${uid}`;
88873
+ const existing = this.avatarAssets.get(key);
88874
+ if (existing !== void 0) return existing.promise;
88875
+ let resolve;
88876
+ let reject;
88877
+ const promise = new Promise((done, fail) => {
88878
+ resolve = done;
88879
+ reject = fail;
88880
+ });
88881
+ this.avatarAssets.set(key, {
88882
+ session,
88883
+ uid,
88884
+ generation,
88885
+ started: false,
88886
+ promise,
88887
+ resolve,
88888
+ reject
88889
+ });
88890
+ this.avatarAssetTimer ??= setTimeout(() => {
88891
+ this.avatarAssetTimer = void 0;
88892
+ this.flushAvatarAssets();
88893
+ }, 8);
88894
+ return promise;
88895
+ }
88896
+ async flushAvatarAssets() {
88897
+ const groups = /* @__PURE__ */ new Map();
88898
+ for (const entry of this.avatarAssets.entries()) {
88899
+ if (entry[1].started) continue;
88900
+ entry[1].started = true;
88901
+ const group = groups.get(entry[1].session.userId) ?? [];
88902
+ group.push(entry);
88903
+ groups.set(entry[1].session.userId, group);
88904
+ }
88905
+ for (const group of groups.values()) for (let start = 0; start < group.length; start += 50) {
88906
+ const batch = group.slice(start, start + 50);
88907
+ try {
88908
+ await this.assertImageAccount(batch[0][1].session, batch[0][1].generation);
88909
+ const items = await this.queryFileAssetsForSession(batch[0][1].session, batch.map(([, pending]) => pending.uid));
88910
+ const byUid = new Map(items.map((item) => [item.fileAssetUid, item]));
88911
+ for (const [, pending] of batch) pending.resolve(byUid.get(pending.uid));
88912
+ } catch (error) {
88913
+ for (const [, pending] of batch) pending.reject(error);
88914
+ } finally {
88915
+ for (const [key, pending] of batch) if (this.avatarAssets.get(key) === pending) this.avatarAssets.delete(key);
88916
+ }
88917
+ }
88918
+ }
88423
88919
  cachedImage(cacheKey) {
88424
88920
  const cached = this.imageCache.get(cacheKey);
88425
88921
  if (cached === void 0) return void 0;
@@ -88467,13 +88963,6 @@ var MediaService = class {
88467
88963
  if (this.selfRoleAvatars === void 0) throw new ArkmePluginError("self-role-avatar-missing", "本地角色头像不可用", false, 404);
88468
88964
  return await this.selfRoleAvatars.read(session.userId, imageRef.trim(), byteLimit);
88469
88965
  }
88470
- const snapshotAsset = /^file_asset:\/\/([A-Za-z0-9_-]{8,128})$/.exec(imageRef.trim());
88471
- if (snapshotAsset !== null) {
88472
- const asset = (await this.queryFileAssets([snapshotAsset[1]], signal)).find((item) => item.fileAssetUid === snapshotAsset[1]);
88473
- const url = asset?.previewUrl ?? asset?.downloadUrl;
88474
- if (url === void 0) throw new ArkmePluginError("image-ref-unavailable", "历史头像当前不可用", true, 404);
88475
- return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, url), byteLimit, signal, this.runtime.requestScope(session.userId));
88476
- }
88477
88966
  if (imageRef.trim().startsWith("arkme-bot-image-v1.")) {
88478
88967
  if (this.botImages === void 0) throw new ArkmePluginError("bot-image-ref-invalid", "Bot 头像引用不可用", false, 403);
88479
88968
  const reference = await this.botImages.openBotImageRef(imageRef, session.userId);
@@ -90302,6 +90791,11 @@ var ProfileService = class {
90302
90791
  }
90303
90792
  if (cached.value !== null) profiles.set(userId, cached.value);
90304
90793
  }
90794
+ await measureReaction("profile-cache", {
90795
+ requested: normalized.length,
90796
+ hits: normalized.length - missing.length,
90797
+ misses: missing.length
90798
+ }, async () => void 0);
90305
90799
  for (const batch of chunksOf(missing, 50)) {
90306
90800
  if (batch.length === 0) continue;
90307
90801
  const startedAtMillis = Date.now();
@@ -92128,6 +92622,46 @@ var RecordingService = class {
92128
92622
  ...numberValue$5(data.retry_after_ms) > 0 ? { retryAfterMs: numberValue$5(data.retry_after_ms) } : {}
92129
92623
  };
92130
92624
  }
92625
+ /** Called only for an authenticated, Host-verified directory detail_ref. */
92626
+ async directorySpeakerRef(speakerId, viewerUserId) {
92627
+ return this.sealRecordingRefWithKey("arkme-recording-speaker-v1", {
92628
+ version: 1,
92629
+ viewerUserId,
92630
+ target: {
92631
+ kind: "speaker",
92632
+ speakerId
92633
+ }
92634
+ }, await this.recordingRefKey("arkme-recording-speaker-v1"));
92635
+ }
92636
+ /** Enrich only verified directory speakers; do not load contact suggestions or presence stats. */
92637
+ async directorySpeakerAvatars(speakerIds, viewerUserId, signal) {
92638
+ const session = await this.runtime.requireSession();
92639
+ const revision = this.speakerCacheRevision;
92640
+ if (session.userId !== viewerUserId) throw new ArkmePluginError("recording-ref-account-mismatch", "账号已切换", false, 403);
92641
+ if (speakerIds.length === 0) return /* @__PURE__ */ new Map();
92642
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/get-speaker-ls", {}, session, signal, {
92643
+ lane: "background-read",
92644
+ key: "directory-speaker-avatars",
92645
+ cacheMs: 1e4
92646
+ });
92647
+ if (!Array.isArray(data.spk_ls)) throw new ArkmePluginError("recording-speaker-contract-invalid", "说话人资料响应无效", true, 502);
92648
+ const wanted = new Set(speakerIds);
92649
+ const bindings = /* @__PURE__ */ new Map();
92650
+ for (const value of data.spk_ls) {
92651
+ const raw = objectValue$2(value);
92652
+ const id = stringValue$2(raw.speaker_id ?? raw.id ?? raw.spk_id).trim();
92653
+ const userId = positiveUserId(raw.ref_usr_id ?? raw.ref_user_id ?? raw.user_id);
92654
+ if (wanted.has(id) && userId !== void 0) bindings.set(id, userId);
92655
+ }
92656
+ const profiles = await this.recordingSpeakerProfiles([...new Set(bindings.values())], session, signal, true);
92657
+ await this.assertSpeakerReadCurrent(session, revision, signal);
92658
+ const result = /* @__PURE__ */ new Map();
92659
+ for (const [id, userId] of bindings) {
92660
+ const avatar = profiles.get(userId)?.avatarRef;
92661
+ if (avatar) result.set(id, avatar);
92662
+ }
92663
+ return result;
92664
+ }
92131
92665
  /** All-history source identities, authorized by the same account-bound speaker reference as the UI. */
92132
92666
  async recordingSpeakerMembers(speakerRef, signal, expectedVersion) {
92133
92667
  this.assertWorkbenchEnabled();
@@ -93194,7 +93728,7 @@ var RecordingService = class {
93194
93728
  async recordingRefKey(prefix) {
93195
93729
  return createHash("sha256").update(await this.runtime.stateStore.uniqueCode()).update(`\0${prefix}`).digest();
93196
93730
  }
93197
- async recordingSpeakerProfiles(userIds, session, signal) {
93731
+ async recordingSpeakerProfiles(userIds, session, signal, propagateFailure = false) {
93198
93732
  if (this.profile === void 0) return /* @__PURE__ */ new Map();
93199
93733
  if (userIds.length === 0) return /* @__PURE__ */ new Map();
93200
93734
  try {
@@ -93208,7 +93742,8 @@ var RecordingService = class {
93208
93742
  }];
93209
93743
  }));
93210
93744
  return new Map(entries);
93211
- } catch {
93745
+ } catch (error) {
93746
+ if (propagateFailure) throw error;
93212
93747
  return /* @__PURE__ */ new Map();
93213
93748
  }
93214
93749
  }
@@ -95778,6 +96313,28 @@ var UnmarkedSpeakerService = class {
95778
96313
  ...typeof data.cursor_stale === "boolean" ? { cursorStale: data.cursor_stale } : {}
95779
96314
  };
95780
96315
  }
96316
+ /** Hydrate on selection, so paginating a large directory never evicts selected references. */
96317
+ async directoryCandidateRef(candidateId, signal) {
96318
+ const session = await this.runtime.requireSession();
96319
+ const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/unmarked-speakers/detail", { candidate_id: candidateId }, session, signal);
96320
+ signal?.throwIfAborted();
96321
+ const current = await this.runtime.requireSession();
96322
+ if (current.userId !== session.userId || current.refreshToken !== session.refreshToken) throw new ArkmePluginError("unmarked-candidate-ref-account-mismatch", "账号已切换", false, 403);
96323
+ if (data.outcome === "candidate_not_found") throw new ArkmePluginError("unmarked-candidate-not-found", "该人物已变化,请刷新目录", false, 404);
96324
+ const raw = objectValue$2(data.candidate);
96325
+ if (stringValue$2(raw.candidate_id) !== candidateId || !["cross_day", "single_day"].includes(String(raw.status))) throw new ArkmePluginError("unmarked-detail-contract-invalid", "人物详情暂不可用", true, 502);
96326
+ this.pruneRefs();
96327
+ return this.sealCandidateRef(session.userId, {
96328
+ candidateId,
96329
+ status: raw.status,
96330
+ speakerToken: boundedInteger(raw.speaker_display_number) > 0 ? String(raw.speaker_display_number) : stringValue$2(raw.label),
96331
+ appearanceDays: boundedInteger(raw.day_count),
96332
+ validAudioDurationMillis: boundedInteger(raw.total_speech_duration_ms),
96333
+ segmentCount: boundedInteger(raw.segment_count),
96334
+ firstSeenAtMillis: positiveTimestamp(raw.first_seen_at),
96335
+ latestAtMillis: positiveTimestamp(raw.last_seen_at)
96336
+ });
96337
+ }
95781
96338
  async markOptions(candidateRef, signal) {
95782
96339
  const { session, entry, normalizedRef } = await this.resolveCandidateRef(candidateRef);
95783
96340
  const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/unmarked-speakers/mark-options", { candidate_id: entry.candidateId }, session, signal, {
@@ -98463,6 +99020,7 @@ var ArkmeService = class {
98463
99020
  relatedQuickNote;
98464
99021
  contact;
98465
99022
  contactDirectory;
99023
+ speakerDirectory;
98466
99024
  unmarkedSpeaker;
98467
99025
  voiceprint;
98468
99026
  userBan;
@@ -98592,6 +99150,7 @@ var ArkmeService = class {
98592
99150
  forwardGateway: new OwnerRecordingForwardGateway(this.runtime, this.source, this.realtime, this.chat)
98593
99151
  });
98594
99152
  this.unmarkedSpeaker = new UnmarkedSpeakerService(this.runtime, this.media);
99153
+ this.speakerDirectory = new SpeakerDirectoryService(this.runtime, this.recording, this.unmarkedSpeaker);
98595
99154
  this.contact = new ContactService(this.runtime, this.source, this.profile, this.realtime);
98596
99155
  this.voiceprint = new VoiceprintService(this.runtime, this.profile, { resolveRegisteredContactUserId: async (contactRef, session, signal) => await this.contact.resolveRegisteredContactUserId(contactRef, session, signal) });
98597
99156
  this.auth = new AuthService(this.runtime, this.profile, {
@@ -98960,6 +99519,7 @@ var ArkmeService = class {
98960
99519
  messageReport: true,
98961
99520
  userBanManagement: true,
98962
99521
  selfRoles: true,
99522
+ imageCacheRead: true,
98963
99523
  directMessageAdmission: true,
98964
99524
  reactionsV1: true,
98965
99525
  groupOwnerGovernance: true,
@@ -99213,6 +99773,21 @@ var ArkmeService = class {
99213
99773
  async cachedRecordingSpeakerOptions(signal) {
99214
99774
  return await this.recording.cachedRecordingSpeakerOptions(signal);
99215
99775
  }
99776
+ async speakerDirectorySummary(input, signal) {
99777
+ return this.speakerDirectory.summary(input, signal);
99778
+ }
99779
+ async speakerDirectoryAvatars(input, signal) {
99780
+ return this.speakerDirectory.avatars(input, signal);
99781
+ }
99782
+ async speakerDirectoryList(input, signal) {
99783
+ return this.speakerDirectory.list(input, signal);
99784
+ }
99785
+ async speakerDirectorySeen(input, signal) {
99786
+ return this.speakerDirectory.seen(input, signal);
99787
+ }
99788
+ async speakerDirectoryOpen(input, signal) {
99789
+ return this.speakerDirectory.open(input, signal);
99790
+ }
99216
99791
  async recordingSpeakerOptions(signal) {
99217
99792
  return await this.recording.recordingSpeakerOptions(signal);
99218
99793
  }
@@ -99933,110 +100508,132 @@ var ArkmeService = class {
99933
100508
  return await this.chat.favoriteStickers(signal);
99934
100509
  }
99935
100510
  async reactions(input, signal) {
99936
- const result = await new ReactionService(this.runtime, (target) => target.worldRecordRef !== void 0 ? this.world.reactionTarget(target.worldRecordRef) : this.chat.reactionTarget(target.sourceRef, target.messageActionRef), (uid) => this.world.reactionReference(uid), (viewer, uid) => this.source.chatDirectorySourceKey(viewer, uid), async (items, signal) => {
99937
- const session = await this.runtime.requireSession();
99938
- const [sources, profiles] = await Promise.all([this.source.chatSourcesBySessionUids([...new Set(items.map((item) => item.sessionUID).filter(Boolean))], signal), this.profile.publicProfileSummariesByUserIds([...new Set(items.map((item) => item.senderID).filter((id) => id > 0))], session, signal, 5e3)]);
99939
- const missingAvatars = [...sources].filter(([, source]) => !source.avatarRef && !source.avatarRefs?.length && !source.groupAvatar);
99940
- if (missingAvatars.length > 0) try {
99941
- const hydrated = await this.source.hydrateDirectoryPage(missingAvatars.map(([, source]) => source), signal ?? new AbortController().signal);
99942
- missingAvatars.forEach(([uid], index) => {
99943
- if (hydrated[index]) sources.set(uid, hydrated[index]);
100511
+ return await withReactionTrace(input, async () => {
100512
+ const result = await new ReactionService(this.runtime, (target) => target.worldRecordRef !== void 0 ? this.world.reactionTarget(target.worldRecordRef) : this.chat.reactionTarget(target.sourceRef, target.messageActionRef), (uid) => this.world.reactionReference(uid), (viewer, uid) => this.source.chatDirectorySourceKey(viewer, uid), async (items, signal) => {
100513
+ const session = await this.runtime.requireSession();
100514
+ const [sources, profiles] = await Promise.all([this.source.chatSourcesBySessionUids([...new Set(items.map((item) => item.sessionUID).filter(Boolean))], signal), this.profile.publicProfileSummariesByUserIds([...new Set(items.map((item) => item.senderID).filter((id) => id > 0))], session, signal, 5e3)]);
100515
+ const missingAvatars = [...sources].filter(([, source]) => !source.avatarRef && !source.avatarRefs?.length && !source.groupAvatar);
100516
+ if (missingAvatars.length > 0) try {
100517
+ const hydrated = await this.source.hydrateDirectoryPage(missingAvatars.map(([, source]) => source), signal ?? new AbortController().signal);
100518
+ missingAvatars.forEach(([uid], index) => {
100519
+ if (hydrated[index]) sources.set(uid, hydrated[index]);
100520
+ });
100521
+ } catch {
100522
+ signal?.throwIfAborted();
100523
+ }
100524
+ const selfSource = items.some((item) => item.sourceKind === "send_to_self" && item.message) ? {
100525
+ sourceRef: await this.source.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
100526
+ kind: "send_to_self",
100527
+ displayName: "发给自己",
100528
+ activeAtMillis: 0,
100529
+ unreadCount: 0
100530
+ } : void 0;
100531
+ return items.map((item) => {
100532
+ const source = item.sourceKind === "send_to_self" ? selfSource : sources.get(item.sessionUID);
100533
+ return {
100534
+ sourceName: item.sourceKind === "send_to_self" ? "发给自己" : source?.displayName,
100535
+ authorName: item.senderID === session.userId ? "我" : profiles.get(item.senderID)?.displayName,
100536
+ ...source && item.message ? { originalMessage: {
100537
+ source,
100538
+ itemUid: item.message.recordUID,
100539
+ recordOwnerUserId: item.message.ownerUserID,
100540
+ sendAtMillis: item.message.sendAtMillis
100541
+ } } : {},
100542
+ ...source ? { avatar: {
100543
+ ...source.avatarRef ? { avatarRef: source.avatarRef } : {},
100544
+ ...source.avatarRefs ? { avatarRefs: source.avatarRefs } : {},
100545
+ ...source.groupAvatar ? { groupAvatar: source.groupAvatar } : {}
100546
+ } } : {}
100547
+ };
99944
100548
  });
99945
- } catch {
99946
- signal?.throwIfAborted();
99947
- }
99948
- const selfSource = items.some((item) => item.sourceKind === "send_to_self" && item.message) ? {
99949
- sourceRef: await this.source.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
99950
- kind: "send_to_self",
99951
- displayName: "发给自己",
99952
- activeAtMillis: 0,
99953
- unreadCount: 0
99954
- } : void 0;
99955
- return items.map((item) => {
99956
- const source = item.sourceKind === "send_to_self" ? selfSource : sources.get(item.sessionUID);
99957
- return {
99958
- sourceName: item.sourceKind === "send_to_self" ? "发给自己" : source?.displayName,
99959
- authorName: item.senderID === session.userId ? "我" : profiles.get(item.senderID)?.displayName,
99960
- ...source && item.message ? { originalMessage: {
99961
- source,
99962
- itemUid: item.message.recordUID,
99963
- recordOwnerUserId: item.message.ownerUserID,
99964
- sendAtMillis: item.message.sendAtMillis
99965
- } } : {},
99966
- ...source ? { avatar: {
99967
- ...source.avatarRef ? { avatarRef: source.avatarRef } : {},
99968
- ...source.avatarRefs ? { avatarRefs: source.avatarRefs } : {},
99969
- ...source.groupAvatar ? { groupAvatar: source.groupAvatar } : {}
99970
- } } : {}
99971
- };
100549
+ }).request(input, signal);
100550
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100551
+ const actorLabels = async (sourceRef, ids) => {
100552
+ if (!sourceRef || !ids.length) return /* @__PURE__ */ new Map();
100553
+ try {
100554
+ return await this.chat.reactionActorLabels(sourceRef, ids, signal);
100555
+ } catch (error) {
100556
+ if (signal?.aborted) throw error;
100557
+ return /* @__PURE__ */ new Map();
100558
+ }
100559
+ };
100560
+ const actorAvatars = async (viewer, profiles) => new Map(await Promise.all([...profiles].filter(([, profile]) => profile.avatarUrl).map(async ([id]) => [id, await this.profile.sealProfileImageRef(viewer, id)])));
100561
+ const presentActor = (userId, profileName, label, avatarRef) => ({
100562
+ userId,
100563
+ displayName: label?.remark || label?.groupNickname || profileName?.trim() || "用户",
100564
+ ...label?.groupNickname ? { groupNickname: label.groupNickname } : {},
100565
+ ...avatarRef ? { avatarRef } : {}
99972
100566
  });
99973
- }).request(input, signal);
99974
- if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
99975
- const actorLabels = async (sourceRef, ids) => {
99976
- if (!sourceRef || !ids.length) return /* @__PURE__ */ new Map();
99977
- try {
99978
- return await this.chat.reactionActorLabels(sourceRef, ids, signal);
99979
- } catch (error) {
99980
- if (signal?.aborted) throw error;
99981
- return /* @__PURE__ */ new Map();
100567
+ if (input.action === "query" || input.action === "groups") {
100568
+ const page = result;
100569
+ const groupPage = result;
100570
+ const groups = input.action === "query" ? page.items.flatMap((item) => item.actors_visible ? item.groups : []) : groupPage.items;
100571
+ const ids = [...new Set(groups.flatMap((group) => group.actorIds))];
100572
+ const session = await this.runtime.requireSession();
100573
+ const sourceByTarget = new Map(input.action === "query" ? input.targets.map((target) => [target.id, target.sourceRef]) : []);
100574
+ const sourceIds = /* @__PURE__ */ new Map();
100575
+ const include = (sourceRef, actorIds) => {
100576
+ if (!sourceRef || !actorIds.length) return;
100577
+ const users = sourceIds.get(sourceRef) ?? /* @__PURE__ */ new Set();
100578
+ actorIds.forEach((id) => users.add(id));
100579
+ sourceIds.set(sourceRef, users);
100580
+ };
100581
+ if (input.action === "query") {
100582
+ for (const item of page.items) if (item.actors_visible) include(sourceByTarget.get(item.target_id), item.groups.flatMap((group) => group.actorIds));
100583
+ } else include(input.target.sourceRef, ids);
100584
+ if (input.action === "query" && input.actorPresentation === "deferred") {
100585
+ const references = /* @__PURE__ */ new Map();
100586
+ for (const [sourceRef, users] of sourceIds) references.set(sourceRef, await this.chat.reactionActorReferences(sourceRef, [...users]));
100587
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100588
+ return {
100589
+ ...page,
100590
+ items: page.items.map((item) => ({
100591
+ ...item,
100592
+ groups: item.groups.map(({ actorIds, ...group }) => ({
100593
+ ...group,
100594
+ actors: item.actors_visible ? actorIds.map((userId) => ({
100595
+ userId,
100596
+ displayName: "用户",
100597
+ presentationPending: true,
100598
+ ...references.get(sourceByTarget.get(item.target_id) ?? "")?.get(userId) ? { memberRef: references.get(sourceByTarget.get(item.target_id)).get(userId) } : {}
100599
+ })) : []
100600
+ }))
100601
+ }))
100602
+ };
100603
+ }
100604
+ const [profiles, labels] = await Promise.all([measureReaction("actor-profiles", { count: ids.length }, () => this.profile.publicProfileSummariesByUserIds(ids, session, signal, 5e3)), (async () => {
100605
+ const labels = /* @__PURE__ */ new Map();
100606
+ for (const [sourceRef, users] of sourceIds) labels.set(sourceRef, await measureReaction("actor-labels", { count: users.size }, () => actorLabels(sourceRef, [...users])));
100607
+ return labels;
100608
+ })()]);
100609
+ const avatars = await actorAvatars(session.userId, profiles);
100610
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100611
+ const present = ({ actorIds, ...group }, sourceRef, visible = true) => ({
100612
+ ...group,
100613
+ actors: visible ? actorIds.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(sourceRef ?? "")?.get(userId), avatars.get(userId))) : []
100614
+ });
100615
+ return input.action === "query" ? {
100616
+ ...page,
100617
+ items: page.items.map((item) => ({
100618
+ ...item,
100619
+ groups: item.groups.map((group) => present(group, sourceByTarget.get(item.target_id), item.actors_visible))
100620
+ }))
100621
+ } : {
100622
+ ...groupPage,
100623
+ items: groupPage.items.map((group) => present(group, input.target.sourceRef))
100624
+ };
99982
100625
  }
99983
- };
99984
- const actorAvatars = async (viewer, profiles) => new Map(await Promise.all([...profiles].filter(([, profile]) => profile.avatarUrl).map(async ([id]) => [id, await this.profile.sealProfileImageRef(viewer, id)])));
99985
- const presentActor = (userId, profileName, label, avatarRef) => ({
99986
- userId,
99987
- displayName: label?.remark || label?.groupNickname || profileName?.trim() || "用户",
99988
- ...label?.groupNickname ? { groupNickname: label.groupNickname } : {},
99989
- ...avatarRef ? { avatarRef } : {}
99990
- });
99991
- if (input.action === "query" || input.action === "groups") {
100626
+ if (input.action !== "actors") return result;
99992
100627
  const page = result;
99993
- const groupPage = result;
99994
- const groups = input.action === "query" ? page.items.flatMap((item) => item.actors_visible ? item.groups : []) : groupPage.items;
99995
- const ids = [...new Set(groups.flatMap((group) => group.actorIds))];
99996
100628
  const session = await this.runtime.requireSession();
99997
- const sourceByTarget = new Map(input.action === "query" ? input.targets.map((target) => [target.id, target.sourceRef]) : []);
99998
- const sourceIds = /* @__PURE__ */ new Map();
99999
- const include = (sourceRef, actorIds) => {
100000
- if (!sourceRef || !actorIds.length) return;
100001
- const users = sourceIds.get(sourceRef) ?? /* @__PURE__ */ new Set();
100002
- actorIds.forEach((id) => users.add(id));
100003
- sourceIds.set(sourceRef, users);
100004
- };
100005
- if (input.action === "query") {
100006
- for (const item of page.items) if (item.actors_visible) include(sourceByTarget.get(item.target_id), item.groups.flatMap((group) => group.actorIds));
100007
- } else include(input.target.sourceRef, ids);
100008
- const [profiles, labels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(ids, session, signal, 5e3), (async () => {
100009
- const labels = /* @__PURE__ */ new Map();
100010
- for (const [sourceRef, users] of sourceIds) labels.set(sourceRef, await actorLabels(sourceRef, [...users]));
100011
- return labels;
100012
- })()]);
100629
+ const [profiles, labels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(page.user_ids, session, signal, 5e3), actorLabels(input.target.sourceRef, page.user_ids)]);
100013
100630
  const avatars = await actorAvatars(session.userId, profiles);
100014
100631
  if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100015
- const present = ({ actorIds, ...group }, sourceRef, visible = true) => ({
100016
- ...group,
100017
- actors: visible ? actorIds.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(sourceRef ?? "")?.get(userId), avatars.get(userId))) : []
100018
- });
100019
- return input.action === "query" ? {
100020
- ...page,
100021
- items: page.items.map((item) => ({
100022
- ...item,
100023
- groups: item.groups.map((group) => present(group, sourceByTarget.get(item.target_id), item.actors_visible))
100024
- }))
100025
- } : {
100026
- ...groupPage,
100027
- items: groupPage.items.map((group) => present(group, input.target.sourceRef))
100632
+ return {
100633
+ items: page.user_ids.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(userId), avatars.get(userId))),
100634
+ has_more: page.has_more
100028
100635
  };
100029
- }
100030
- if (input.action !== "actors") return result;
100031
- const page = result;
100032
- const session = await this.runtime.requireSession();
100033
- const [profiles, labels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(page.user_ids, session, signal, 5e3), actorLabels(input.target.sourceRef, page.user_ids)]);
100034
- const avatars = await actorAvatars(session.userId, profiles);
100035
- if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100036
- return {
100037
- items: page.user_ids.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(userId), avatars.get(userId))),
100038
- has_more: page.has_more
100039
- };
100636
+ });
100040
100637
  }
100041
100638
  async addFavoriteSticker(item, signal) {
100042
100639
  return await this.chat.addFavoriteSticker(item, signal);