@senguoyun/dsh-arkme 0.1.85 → 0.1.86

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 (52) hide show
  1. package/docs/self-role-avatar-cache.md +59 -0
  2. package/lib/client.js +460 -116
  3. package/lib/client.js.map +1 -1
  4. package/lib/harness-model-client.js +14 -0
  5. package/lib/harness-native-selection-client.js +14 -0
  6. package/lib/harness-sidebar-client.js +14 -0
  7. package/lib/index.js +22 -9
  8. package/lib/persistent-extension.js +1 -1
  9. package/lib/{persistent-runtime-adGFmkIa.js → persistent-runtime-DfmCJcxH.js} +412 -121
  10. package/lib/sdk.js +14 -0
  11. package/lib/types/arkme-service.d.ts +1 -0
  12. package/lib/types/arkme-service.d.ts.map +1 -1
  13. package/lib/types/client/ArkmeSelfRolePicker.d.ts.map +1 -1
  14. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  15. package/lib/types/client/avatar-image-runtime.d.ts.map +1 -1
  16. package/lib/types/client/avatar-image-store.d.ts +10 -0
  17. package/lib/types/client/avatar-image-store.d.ts.map +1 -1
  18. package/lib/types/client/avatar-persistent-cache.d.ts +16 -0
  19. package/lib/types/client/avatar-persistent-cache.d.ts.map +1 -0
  20. package/lib/types/client/reaction-notifications.d.ts.map +1 -1
  21. package/lib/types/client/reaction-preview-store.d.ts +4 -1
  22. package/lib/types/client/reaction-preview-store.d.ts.map +1 -1
  23. package/lib/types/client/reaction-transport.d.ts +4 -0
  24. package/lib/types/client/reaction-transport.d.ts.map +1 -0
  25. package/lib/types/client/realtime-client-events.d.ts.map +1 -1
  26. package/lib/types/host-api.d.ts.map +1 -1
  27. package/lib/types/local-database.d.ts +1 -0
  28. package/lib/types/local-database.d.ts.map +1 -1
  29. package/lib/types/reaction-contract.d.ts +4 -0
  30. package/lib/types/reaction-contract.d.ts.map +1 -1
  31. package/lib/types/reaction-diagnostics.d.ts +4 -0
  32. package/lib/types/reaction-diagnostics.d.ts.map +1 -0
  33. package/lib/types/reaction-host-diagnostics.d.ts +6 -0
  34. package/lib/types/reaction-host-diagnostics.d.ts.map +1 -0
  35. package/lib/types/request-coordinator.d.ts +2 -0
  36. package/lib/types/request-coordinator.d.ts.map +1 -1
  37. package/lib/types/sdk/index.d.ts +2 -0
  38. package/lib/types/sdk/index.d.ts.map +1 -1
  39. package/lib/types/services/chat-service.d.ts +1 -0
  40. package/lib/types/services/chat-service.d.ts.map +1 -1
  41. package/lib/types/services/media-service.d.ts +14 -0
  42. package/lib/types/services/media-service.d.ts.map +1 -1
  43. package/lib/types/services/profile-service.d.ts.map +1 -1
  44. package/lib/types/services/reaction-service.d.ts.map +1 -1
  45. package/lib/types/services/service.d.ts +1 -0
  46. package/lib/types/services/service.d.ts.map +1 -1
  47. package/lib/types/tools/business/media/read-image.d.ts.map +1 -1
  48. package/lib/types/tools/ports/media.d.ts +1 -0
  49. package/lib/types/tools/ports/media.d.ts.map +1 -1
  50. package/lib/types/types.d.ts +1 -0
  51. package/lib/types/types.d.ts.map +1 -1
  52. 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";
@@ -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;
@@ -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);
@@ -1855,6 +1955,13 @@ var ServiceRuntime = class {
1855
1955
  if (!(error instanceof ArkmePluginError) || !["auth-http-401", "auth-http-403"].includes(error.code)) throw error;
1856
1956
  session = await this.refreshAccessToken(session);
1857
1957
  return await this.post(this.config.recordBaseUrl, path, body, session.accessToken, [0], signal, false, requestOptions());
1958
+ } finally {
1959
+ if ([
1960
+ "/api/v1/reactions/set",
1961
+ "/api/v1/reactions/notifications/read",
1962
+ "/api/v1/reactions/history-policy/set",
1963
+ "/api/v1/reactions/library/set"
1964
+ ].includes(path)) this.requestCoordinator.invalidateKey(this.requestScope(session.userId), `owner-read:${this.config.recordBaseUrl.replace(/\/+$/, "")}/api/v1/reactions/`);
1858
1965
  }
1859
1966
  }
1860
1967
  async authenticatedCalendarPost(path, body, initialSession, signal, options = {}) {
@@ -2946,7 +3053,7 @@ var ReactionService = class {
2946
3053
  const ids = /* @__PURE__ */ new Map();
2947
3054
  const resolve = async (target) => {
2948
3055
  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);
3056
+ return await measureReaction("target-resolve", {}, () => this.resolve(target));
2950
3057
  };
2951
3058
  switch (input.action) {
2952
3059
  case "notifications":
@@ -2965,6 +3072,7 @@ var ReactionService = class {
2965
3072
  })) };
2966
3073
  break;
2967
3074
  case "query": {
3075
+ if (input.actorPresentation !== void 0 && input.actorPresentation !== "deferred") throw new ArkmePluginError("reaction-query-invalid", "表态展示选项无效", false);
2968
3076
  if (!Array.isArray(input.targets) || input.targets.length < 1 || input.targets.length > 50) throw new ArkmePluginError("reaction-query-invalid", "表态查询数量无效", false);
2969
3077
  const targets = [], seenIDs = /* @__PURE__ */ new Set();
2970
3078
  for (const target of input.targets) {
@@ -2978,6 +3086,7 @@ var ReactionService = class {
2978
3086
  targets.push(wire);
2979
3087
  }
2980
3088
  }
3089
+ targets.sort((left, right) => targetKey(left).localeCompare(targetKey(right)));
2981
3090
  path = "query";
2982
3091
  body = { targets };
2983
3092
  break;
@@ -3060,7 +3169,7 @@ var ReactionService = class {
3060
3169
  }
3061
3170
  const before = await this.runtime.requireSession();
3062
3171
  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));
3172
+ const result = objectValue$2(await measureReaction("reaction-owner", { action: input.action }, () => this.runtime.authenticatedPost(`/api/v1/reactions/${path}`, body, session, signal)));
3064
3173
  const current = await this.runtime.requireSession();
3065
3174
  if (signal?.aborted || `${this.runtime.config.environment}:${current.userId}` !== accountKey) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
3066
3175
  if (input.action === "query") {
@@ -52490,6 +52599,12 @@ var ChatService = class {
52490
52599
  rel_uid: reference.relationUid
52491
52600
  };
52492
52601
  }
52602
+ async reactionActorReferences(sourceRef, userIds) {
52603
+ const session = await this.runtime.requireSession();
52604
+ const source = await this.source.openSourceRef(sourceRef, session.userId);
52605
+ if (source.kind !== "private_chat" && source.kind !== "group_chat") return /* @__PURE__ */ new Map();
52606
+ return new Map(await Promise.all([...new Set(userIds)].map(async (id) => [id, await this.sealChatMemberRef(session.userId, source.ownerRef, id)])));
52607
+ }
52493
52608
  /** Keep viewer-owned remarks separate from group nicknames and public profiles. */
52494
52609
  async reactionActorLabels(sourceRef, userIds, signal) {
52495
52610
  const session = await this.runtime.requireSession();
@@ -88088,6 +88203,10 @@ var MediaService = class {
88088
88203
  imageCacheBytes = 0;
88089
88204
  activeImageDownloads = 0;
88090
88205
  imageDownloadWaiters = [];
88206
+ avatarAssets = /* @__PURE__ */ new Map();
88207
+ avatarAssetTimer;
88208
+ imageGeneration = 0;
88209
+ localAvatarReads = /* @__PURE__ */ new Map();
88091
88210
  constructor(runtime, profile, worldImages, recordIdentity, botImages, selfRoleAvatars) {
88092
88211
  this.runtime = runtime;
88093
88212
  this.profile = profile;
@@ -88097,19 +88216,27 @@ var MediaService = class {
88097
88216
  this.selfRoleAvatars = selfRoleAvatars;
88098
88217
  }
88099
88218
  dispose() {
88219
+ this.imageGeneration += 1;
88220
+ clearTimeout(this.avatarAssetTimer);
88221
+ this.avatarAssetTimer = void 0;
88222
+ for (const pending of this.avatarAssets.values()) pending.reject(/* @__PURE__ */ new Error("Avatar image scope changed"));
88223
+ this.avatarAssets.clear();
88100
88224
  this.mediaRefs.clear();
88101
88225
  this.stableMediaRefs.clear();
88102
88226
  this.imageCache.clear();
88103
88227
  this.imageInFlight.clear();
88228
+ this.localAvatarReads.clear();
88104
88229
  this.imageCacheBytes = 0;
88105
88230
  this.imageDownloadWaiters.splice(0).forEach((resolve) => {
88106
88231
  resolve();
88107
88232
  });
88108
88233
  }
88109
88234
  async queryFileAssets(fileAssetUids, signal) {
88235
+ return await this.queryFileAssetsForSession(await this.runtime.requireSession(), fileAssetUids, signal);
88236
+ }
88237
+ async queryFileAssetsForSession(session, fileAssetUids, signal) {
88110
88238
  const unique = [...new Set(fileAssetUids.map((uid) => uid.trim()).filter((uid) => uid !== ""))].slice(0, 50);
88111
88239
  if (unique.length === 0) return [];
88112
- const session = await this.runtime.requireSession();
88113
88240
  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
88241
  const item = objectValue$2(raw);
88115
88242
  const previewUrl = safeHttpsUrl(item.preview_url);
@@ -88371,7 +88498,11 @@ var MediaService = class {
88371
88498
  }
88372
88499
  async readImage(imageRef, options = {}) {
88373
88500
  options.signal?.throwIfAborted();
88501
+ const generation = this.imageGeneration;
88374
88502
  const session = await this.runtime.requireSession();
88503
+ const assetMatch = /^file_asset:\/\/([A-Za-z0-9_-]{8,128})$/.exec(imageRef.trim());
88504
+ if (assetMatch !== null) return await this.readFileAssetImage(session, assetMatch[1], options, generation);
88505
+ if (options.cacheOnly) throw new ArkmePluginError("image-cache-miss", "本地没有可用的资产头像", false, 404);
88375
88506
  const isProfileImage = imageRef.trim().startsWith("arkme-profile-image-v1.");
88376
88507
  const isBotImage = imageRef.trim().startsWith("arkme-bot-image-v1.");
88377
88508
  const isSelfRoleImage = imageRef.trim().startsWith("arkme-self-role-image-v1.");
@@ -88420,6 +88551,145 @@ var MediaService = class {
88420
88551
  if (this.imageInFlight.get(cacheKey) === pending) this.imageInFlight.delete(cacheKey);
88421
88552
  }
88422
88553
  }
88554
+ /** Immutable asset bytes survive URL expiry and Host restarts in the existing avatar store. */
88555
+ async readFileAssetImage(session, uid, options, generation) {
88556
+ await this.assertImageAccount(session, generation);
88557
+ options.signal?.throwIfAborted();
88558
+ const byteLimit = Math.min(MAX_SELF_ROLE_AVATAR_BYTES, Math.max(1, Math.trunc(options.maxBytes ?? 8388608)));
88559
+ const diskKey = `asset:${this.runtime.config.environment}:${uid}`;
88560
+ const cacheKey = `${session.userId}:${byteLimit}:${diskKey}`;
88561
+ const cached = this.cachedImage(cacheKey);
88562
+ if (cached !== void 0) return cached;
88563
+ if (options.cacheOnly) {
88564
+ const value = await this.readLocalFileAssetImage(session, uid, diskKey, byteLimit, generation);
88565
+ options.signal?.throwIfAborted();
88566
+ await this.assertImageAccount(session, generation);
88567
+ if (value === void 0) throw new ArkmePluginError("image-cache-miss", "本地没有可用的资产头像", false, 404);
88568
+ this.cacheImage(cacheKey, value);
88569
+ return cloneImageBytes(value);
88570
+ }
88571
+ let pending = this.imageInFlight.get(cacheKey);
88572
+ if (pending === void 0) {
88573
+ pending = this.loadFileAssetImage(session, uid, diskKey, byteLimit, generation);
88574
+ this.imageInFlight.set(cacheKey, pending);
88575
+ }
88576
+ try {
88577
+ const value = await pending;
88578
+ options.signal?.throwIfAborted();
88579
+ await this.assertImageAccount(session, generation);
88580
+ this.cacheImage(cacheKey, value);
88581
+ return cloneImageBytes(value);
88582
+ } finally {
88583
+ if (this.imageInFlight.get(cacheKey) === pending) this.imageInFlight.delete(cacheKey);
88584
+ }
88585
+ }
88586
+ async assertImageAccount(session, generation) {
88587
+ const current = await this.runtime.requireSession();
88588
+ if (generation !== this.imageGeneration || current.userId !== session.userId) throw new ArkmePluginError("image-account-changed", "账号已切换,请重试", false, 409);
88589
+ }
88590
+ async validAvatar(image, byteLimit) {
88591
+ if (image.bytes <= 0 || image.bytes !== image.data.byteLength || image.bytes > byteLimit || imageMediaType(image.data) !== image.mediaType) return false;
88592
+ try {
88593
+ const { default: sharp } = await import("sharp");
88594
+ await sharp(image.data, {
88595
+ failOn: "warning",
88596
+ limitInputPixels: 64 * 1024 * 1024
88597
+ }).resize(1, 1).raw().toBuffer();
88598
+ return true;
88599
+ } catch {
88600
+ return false;
88601
+ }
88602
+ }
88603
+ async readLocalFileAssetImage(session, uid, diskKey, byteLimit, generation) {
88604
+ const key = `${generation}:${session.userId}:${byteLimit}:${diskKey}`;
88605
+ let pending = this.localAvatarReads.get(key);
88606
+ if (pending === void 0) {
88607
+ pending = (async () => {
88608
+ const persisted = await this.runtime.stateStore.readAvatarCache?.(session.userId, diskKey).catch(() => void 0);
88609
+ if (persisted !== void 0 && await this.validAvatar(persisted, byteLimit)) return persisted;
88610
+ await this.assertImageAccount(session, generation);
88611
+ const localRef = await this.runtime.stateStore.selfRoleAvatarLocalRef?.(session.userId, uid).catch(() => void 0);
88612
+ const original = localRef === void 0 ? void 0 : await this.selfRoleAvatars?.read(session.userId, localRef, byteLimit).catch(() => void 0);
88613
+ return original !== void 0 && await this.validAvatar(original, byteLimit) ? original : void 0;
88614
+ })();
88615
+ this.localAvatarReads.set(key, pending);
88616
+ }
88617
+ try {
88618
+ const value = await pending;
88619
+ await this.assertImageAccount(session, generation);
88620
+ return value;
88621
+ } finally {
88622
+ if (this.localAvatarReads.get(key) === pending) this.localAvatarReads.delete(key);
88623
+ }
88624
+ }
88625
+ async loadFileAssetImage(session, uid, diskKey, byteLimit, generation) {
88626
+ let value = await this.readLocalFileAssetImage(session, uid, diskKey, byteLimit, generation);
88627
+ if (value === void 0) {
88628
+ await this.assertImageAccount(session, generation);
88629
+ const asset = await this.queueAvatarAsset(session, uid, generation);
88630
+ const url = asset?.previewUrl ?? asset?.downloadUrl;
88631
+ if (url === void 0) throw new ArkmePluginError("image-ref-unavailable", "历史头像当前不可用", true, 404);
88632
+ await this.assertImageAccount(session, generation);
88633
+ value = await this.withImageDownloadPermit(async () => {
88634
+ await this.assertImageAccount(session, generation);
88635
+ return await this.downloadSignedImage(trustedSignedImageUrl$1(this.runtime.config.environment, url), byteLimit, void 0, this.runtime.requestScope(session.userId));
88636
+ });
88637
+ if (!await this.validAvatar(value, byteLimit)) throw new ArkmePluginError("image-bytes-invalid", "头像数据损坏,请重试", true, 502);
88638
+ }
88639
+ await this.assertImageAccount(session, generation);
88640
+ await this.runtime.stateStore.writeAvatarCache?.(session.userId, diskKey, value).catch(() => {
88641
+ console.warn("dsh-arkme: avatar_cache_write_failed");
88642
+ });
88643
+ return value;
88644
+ }
88645
+ queueAvatarAsset(session, uid, generation) {
88646
+ const key = `${session.userId}:${uid}`;
88647
+ const existing = this.avatarAssets.get(key);
88648
+ if (existing !== void 0) return existing.promise;
88649
+ let resolve;
88650
+ let reject;
88651
+ const promise = new Promise((done, fail) => {
88652
+ resolve = done;
88653
+ reject = fail;
88654
+ });
88655
+ this.avatarAssets.set(key, {
88656
+ session,
88657
+ uid,
88658
+ generation,
88659
+ started: false,
88660
+ promise,
88661
+ resolve,
88662
+ reject
88663
+ });
88664
+ this.avatarAssetTimer ??= setTimeout(() => {
88665
+ this.avatarAssetTimer = void 0;
88666
+ this.flushAvatarAssets();
88667
+ }, 8);
88668
+ return promise;
88669
+ }
88670
+ async flushAvatarAssets() {
88671
+ const groups = /* @__PURE__ */ new Map();
88672
+ for (const entry of this.avatarAssets.entries()) {
88673
+ if (entry[1].started) continue;
88674
+ entry[1].started = true;
88675
+ const group = groups.get(entry[1].session.userId) ?? [];
88676
+ group.push(entry);
88677
+ groups.set(entry[1].session.userId, group);
88678
+ }
88679
+ for (const group of groups.values()) for (let start = 0; start < group.length; start += 50) {
88680
+ const batch = group.slice(start, start + 50);
88681
+ try {
88682
+ await this.assertImageAccount(batch[0][1].session, batch[0][1].generation);
88683
+ const items = await this.queryFileAssetsForSession(batch[0][1].session, batch.map(([, pending]) => pending.uid));
88684
+ const byUid = new Map(items.map((item) => [item.fileAssetUid, item]));
88685
+ for (const [, pending] of batch) pending.resolve(byUid.get(pending.uid));
88686
+ } catch (error) {
88687
+ for (const [, pending] of batch) pending.reject(error);
88688
+ } finally {
88689
+ for (const [key, pending] of batch) if (this.avatarAssets.get(key) === pending) this.avatarAssets.delete(key);
88690
+ }
88691
+ }
88692
+ }
88423
88693
  cachedImage(cacheKey) {
88424
88694
  const cached = this.imageCache.get(cacheKey);
88425
88695
  if (cached === void 0) return void 0;
@@ -88467,13 +88737,6 @@ var MediaService = class {
88467
88737
  if (this.selfRoleAvatars === void 0) throw new ArkmePluginError("self-role-avatar-missing", "本地角色头像不可用", false, 404);
88468
88738
  return await this.selfRoleAvatars.read(session.userId, imageRef.trim(), byteLimit);
88469
88739
  }
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
88740
  if (imageRef.trim().startsWith("arkme-bot-image-v1.")) {
88478
88741
  if (this.botImages === void 0) throw new ArkmePluginError("bot-image-ref-invalid", "Bot 头像引用不可用", false, 403);
88479
88742
  const reference = await this.botImages.openBotImageRef(imageRef, session.userId);
@@ -90302,6 +90565,11 @@ var ProfileService = class {
90302
90565
  }
90303
90566
  if (cached.value !== null) profiles.set(userId, cached.value);
90304
90567
  }
90568
+ await measureReaction("profile-cache", {
90569
+ requested: normalized.length,
90570
+ hits: normalized.length - missing.length,
90571
+ misses: missing.length
90572
+ }, async () => void 0);
90305
90573
  for (const batch of chunksOf(missing, 50)) {
90306
90574
  if (batch.length === 0) continue;
90307
90575
  const startedAtMillis = Date.now();
@@ -98960,6 +99228,7 @@ var ArkmeService = class {
98960
99228
  messageReport: true,
98961
99229
  userBanManagement: true,
98962
99230
  selfRoles: true,
99231
+ imageCacheRead: true,
98963
99232
  directMessageAdmission: true,
98964
99233
  reactionsV1: true,
98965
99234
  groupOwnerGovernance: true,
@@ -99933,110 +100202,132 @@ var ArkmeService = class {
99933
100202
  return await this.chat.favoriteStickers(signal);
99934
100203
  }
99935
100204
  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]);
100205
+ return await withReactionTrace(input, async () => {
100206
+ 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) => {
100207
+ const session = await this.runtime.requireSession();
100208
+ 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)]);
100209
+ const missingAvatars = [...sources].filter(([, source]) => !source.avatarRef && !source.avatarRefs?.length && !source.groupAvatar);
100210
+ if (missingAvatars.length > 0) try {
100211
+ const hydrated = await this.source.hydrateDirectoryPage(missingAvatars.map(([, source]) => source), signal ?? new AbortController().signal);
100212
+ missingAvatars.forEach(([uid], index) => {
100213
+ if (hydrated[index]) sources.set(uid, hydrated[index]);
100214
+ });
100215
+ } catch {
100216
+ signal?.throwIfAborted();
100217
+ }
100218
+ const selfSource = items.some((item) => item.sourceKind === "send_to_self" && item.message) ? {
100219
+ sourceRef: await this.source.sealSourceRef(session.userId, "send_to_self", "all", "发给自己"),
100220
+ kind: "send_to_self",
100221
+ displayName: "发给自己",
100222
+ activeAtMillis: 0,
100223
+ unreadCount: 0
100224
+ } : void 0;
100225
+ return items.map((item) => {
100226
+ const source = item.sourceKind === "send_to_self" ? selfSource : sources.get(item.sessionUID);
100227
+ return {
100228
+ sourceName: item.sourceKind === "send_to_self" ? "发给自己" : source?.displayName,
100229
+ authorName: item.senderID === session.userId ? "我" : profiles.get(item.senderID)?.displayName,
100230
+ ...source && item.message ? { originalMessage: {
100231
+ source,
100232
+ itemUid: item.message.recordUID,
100233
+ recordOwnerUserId: item.message.ownerUserID,
100234
+ sendAtMillis: item.message.sendAtMillis
100235
+ } } : {},
100236
+ ...source ? { avatar: {
100237
+ ...source.avatarRef ? { avatarRef: source.avatarRef } : {},
100238
+ ...source.avatarRefs ? { avatarRefs: source.avatarRefs } : {},
100239
+ ...source.groupAvatar ? { groupAvatar: source.groupAvatar } : {}
100240
+ } } : {}
100241
+ };
99944
100242
  });
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
- };
100243
+ }).request(input, signal);
100244
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100245
+ const actorLabels = async (sourceRef, ids) => {
100246
+ if (!sourceRef || !ids.length) return /* @__PURE__ */ new Map();
100247
+ try {
100248
+ return await this.chat.reactionActorLabels(sourceRef, ids, signal);
100249
+ } catch (error) {
100250
+ if (signal?.aborted) throw error;
100251
+ return /* @__PURE__ */ new Map();
100252
+ }
100253
+ };
100254
+ 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)])));
100255
+ const presentActor = (userId, profileName, label, avatarRef) => ({
100256
+ userId,
100257
+ displayName: label?.remark || label?.groupNickname || profileName?.trim() || "用户",
100258
+ ...label?.groupNickname ? { groupNickname: label.groupNickname } : {},
100259
+ ...avatarRef ? { avatarRef } : {}
99972
100260
  });
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();
100261
+ if (input.action === "query" || input.action === "groups") {
100262
+ const page = result;
100263
+ const groupPage = result;
100264
+ const groups = input.action === "query" ? page.items.flatMap((item) => item.actors_visible ? item.groups : []) : groupPage.items;
100265
+ const ids = [...new Set(groups.flatMap((group) => group.actorIds))];
100266
+ const session = await this.runtime.requireSession();
100267
+ const sourceByTarget = new Map(input.action === "query" ? input.targets.map((target) => [target.id, target.sourceRef]) : []);
100268
+ const sourceIds = /* @__PURE__ */ new Map();
100269
+ const include = (sourceRef, actorIds) => {
100270
+ if (!sourceRef || !actorIds.length) return;
100271
+ const users = sourceIds.get(sourceRef) ?? /* @__PURE__ */ new Set();
100272
+ actorIds.forEach((id) => users.add(id));
100273
+ sourceIds.set(sourceRef, users);
100274
+ };
100275
+ if (input.action === "query") {
100276
+ for (const item of page.items) if (item.actors_visible) include(sourceByTarget.get(item.target_id), item.groups.flatMap((group) => group.actorIds));
100277
+ } else include(input.target.sourceRef, ids);
100278
+ if (input.action === "query" && input.actorPresentation === "deferred") {
100279
+ const references = /* @__PURE__ */ new Map();
100280
+ for (const [sourceRef, users] of sourceIds) references.set(sourceRef, await this.chat.reactionActorReferences(sourceRef, [...users]));
100281
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100282
+ return {
100283
+ ...page,
100284
+ items: page.items.map((item) => ({
100285
+ ...item,
100286
+ groups: item.groups.map(({ actorIds, ...group }) => ({
100287
+ ...group,
100288
+ actors: item.actors_visible ? actorIds.map((userId) => ({
100289
+ userId,
100290
+ displayName: "用户",
100291
+ presentationPending: true,
100292
+ ...references.get(sourceByTarget.get(item.target_id) ?? "")?.get(userId) ? { memberRef: references.get(sourceByTarget.get(item.target_id)).get(userId) } : {}
100293
+ })) : []
100294
+ }))
100295
+ }))
100296
+ };
100297
+ }
100298
+ const [profiles, labels] = await Promise.all([measureReaction("actor-profiles", { count: ids.length }, () => this.profile.publicProfileSummariesByUserIds(ids, session, signal, 5e3)), (async () => {
100299
+ const labels = /* @__PURE__ */ new Map();
100300
+ for (const [sourceRef, users] of sourceIds) labels.set(sourceRef, await measureReaction("actor-labels", { count: users.size }, () => actorLabels(sourceRef, [...users])));
100301
+ return labels;
100302
+ })()]);
100303
+ const avatars = await actorAvatars(session.userId, profiles);
100304
+ if (`${this.config.environment}:${(await this.runtime.requireSession()).userId}` !== input.accountKey || signal?.aborted) throw new ArkmePluginError("reaction-account-changed", "账号已变化", false);
100305
+ const present = ({ actorIds, ...group }, sourceRef, visible = true) => ({
100306
+ ...group,
100307
+ actors: visible ? actorIds.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(sourceRef ?? "")?.get(userId), avatars.get(userId))) : []
100308
+ });
100309
+ return input.action === "query" ? {
100310
+ ...page,
100311
+ items: page.items.map((item) => ({
100312
+ ...item,
100313
+ groups: item.groups.map((group) => present(group, sourceByTarget.get(item.target_id), item.actors_visible))
100314
+ }))
100315
+ } : {
100316
+ ...groupPage,
100317
+ items: groupPage.items.map((group) => present(group, input.target.sourceRef))
100318
+ };
99982
100319
  }
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") {
100320
+ if (input.action !== "actors") return result;
99992
100321
  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
100322
  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
- })()]);
100323
+ const [profiles, labels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(page.user_ids, session, signal, 5e3), actorLabels(input.target.sourceRef, page.user_ids)]);
100013
100324
  const avatars = await actorAvatars(session.userId, profiles);
100014
100325
  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))
100326
+ return {
100327
+ items: page.user_ids.map((userId) => presentActor(userId, profiles.get(userId)?.displayName, labels.get(userId), avatars.get(userId))),
100328
+ has_more: page.has_more
100028
100329
  };
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
- };
100330
+ });
100040
100331
  }
100041
100332
  async addFavoriteSticker(item, signal) {
100042
100333
  return await this.chat.addFavoriteSticker(item, signal);