@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.
- package/docs/recognized-speaker-directory-backend-handoff-2026-09-28.md +2 -0
- package/docs/recognized-speaker-entry-and-visits.md +2 -0
- package/docs/recognized-speaker-sorting.md +2 -0
- package/docs/self-role-avatar-cache.md +59 -0
- package/docs/speaker-directory-integration-2026-09-30.md +53 -0
- package/lib/client.js +1336 -1168
- package/lib/client.js.map +1 -1
- package/lib/harness-model-client.js +14 -0
- package/lib/harness-native-selection-client.js +14 -0
- package/lib/harness-sidebar-client.js +14 -0
- package/lib/index.js +28 -9
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-adGFmkIa.js → persistent-runtime-0qn1BZ6S.js} +733 -136
- package/lib/sdk.js +14 -0
- package/lib/types/arkme-service.d.ts +7 -0
- package/lib/types/arkme-service.d.ts.map +1 -1
- package/lib/types/client/ArkmeAvatar.d.ts +2 -1
- package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
- package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts +3 -30
- package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSelfRolePicker.d.ts.map +1 -1
- package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/avatar-image-runtime.d.ts.map +1 -1
- package/lib/types/client/avatar-image-store.d.ts +10 -0
- package/lib/types/client/avatar-image-store.d.ts.map +1 -1
- package/lib/types/client/avatar-persistent-cache.d.ts +16 -0
- package/lib/types/client/avatar-persistent-cache.d.ts.map +1 -0
- package/lib/types/client/locales/actions-en.d.ts.map +1 -1
- package/lib/types/client/reaction-notifications.d.ts.map +1 -1
- package/lib/types/client/reaction-preview-store.d.ts +4 -1
- package/lib/types/client/reaction-preview-store.d.ts.map +1 -1
- package/lib/types/client/reaction-transport.d.ts +4 -0
- package/lib/types/client/reaction-transport.d.ts.map +1 -0
- package/lib/types/client/realtime-client-events.d.ts.map +1 -1
- package/lib/types/client/recognized-speaker-directory.d.ts +51 -22
- package/lib/types/client/recognized-speaker-directory.d.ts.map +1 -1
- package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts +2 -4
- package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts.map +1 -1
- package/lib/types/client/recordings/recording-speaker-options-store.d.ts.map +1 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/local-database.d.ts +1 -0
- package/lib/types/local-database.d.ts.map +1 -1
- package/lib/types/reaction-contract.d.ts +4 -0
- package/lib/types/reaction-contract.d.ts.map +1 -1
- package/lib/types/reaction-diagnostics.d.ts +4 -0
- package/lib/types/reaction-diagnostics.d.ts.map +1 -0
- package/lib/types/reaction-host-diagnostics.d.ts +6 -0
- package/lib/types/reaction-host-diagnostics.d.ts.map +1 -0
- package/lib/types/recording-presence.d.ts +1 -1
- package/lib/types/request-coordinator.d.ts +2 -0
- package/lib/types/request-coordinator.d.ts.map +1 -1
- package/lib/types/sdk/index.d.ts +2 -0
- package/lib/types/sdk/index.d.ts.map +1 -1
- package/lib/types/services/chat-service.d.ts +1 -0
- package/lib/types/services/chat-service.d.ts.map +1 -1
- package/lib/types/services/media-service.d.ts +14 -0
- package/lib/types/services/media-service.d.ts.map +1 -1
- package/lib/types/services/profile-service.d.ts.map +1 -1
- package/lib/types/services/reaction-service.d.ts.map +1 -1
- package/lib/types/services/recording-service.d.ts +4 -0
- package/lib/types/services/recording-service.d.ts.map +1 -1
- package/lib/types/services/service.d.ts +1 -0
- package/lib/types/services/service.d.ts.map +1 -1
- package/lib/types/services/speaker-directory-service.d.ts +19 -0
- package/lib/types/services/speaker-directory-service.d.ts.map +1 -0
- package/lib/types/services/unmarked-speaker-service.d.ts +2 -0
- package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
- package/lib/types/speaker-directory-contract.d.ts +71 -0
- package/lib/types/speaker-directory-contract.d.ts.map +1 -0
- package/lib/types/tools/business/media/read-image.d.ts.map +1 -1
- package/lib/types/tools/ports/media.d.ts +1 -0
- package/lib/types/tools/ports/media.d.ts.map +1 -1
- package/lib/types/types.d.ts +2 -1
- package/lib/types/types.d.ts.map +1 -1
- 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$
|
|
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$
|
|
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$
|
|
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$
|
|
443
|
-
integer$
|
|
444
|
-
for (const key of ["dueAtMillis", "remindAtMillis"]) if (item[key] !== void 0) integer$
|
|
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$
|
|
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$
|
|
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.
|
|
997
|
-
this.
|
|
998
|
-
const
|
|
999
|
-
if (
|
|
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$
|
|
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$
|
|
1318
|
+
return status$2(await this.request("account.scope.commit", { transitionRef }));
|
|
1231
1319
|
}
|
|
1232
1320
|
async abort(transitionRef) {
|
|
1233
|
-
if (status$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99937
|
-
const
|
|
99938
|
-
|
|
99939
|
-
|
|
99940
|
-
|
|
99941
|
-
|
|
99942
|
-
|
|
99943
|
-
|
|
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
|
-
}
|
|
99946
|
-
|
|
99947
|
-
|
|
99948
|
-
|
|
99949
|
-
|
|
99950
|
-
|
|
99951
|
-
|
|
99952
|
-
|
|
99953
|
-
|
|
99954
|
-
|
|
99955
|
-
|
|
99956
|
-
|
|
99957
|
-
|
|
99958
|
-
|
|
99959
|
-
|
|
99960
|
-
|
|
99961
|
-
|
|
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
|
-
|
|
99974
|
-
|
|
99975
|
-
|
|
99976
|
-
|
|
99977
|
-
|
|
99978
|
-
|
|
99979
|
-
|
|
99980
|
-
|
|
99981
|
-
|
|
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
|
|
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
|
-
|
|
100016
|
-
|
|
100017
|
-
|
|
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);
|