@senguoyun/dsh-arkme 0.1.86 → 0.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/docs/recognized-speaker-directory-backend-handoff-2026-09-28.md +2 -0
  2. package/docs/recognized-speaker-entry-and-visits.md +2 -0
  3. package/docs/recognized-speaker-sorting.md +2 -0
  4. package/docs/speaker-directory-integration-2026-09-30.md +53 -0
  5. package/lib/client.js +877 -1053
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +7 -1
  8. package/lib/persistent-extension.js +1 -1
  9. package/lib/{persistent-runtime-DfmCJcxH.js → persistent-runtime-0qn1BZ6S.js} +321 -15
  10. package/lib/types/arkme-service.d.ts +6 -0
  11. package/lib/types/arkme-service.d.ts.map +1 -1
  12. package/lib/types/client/ArkmeAvatar.d.ts +2 -1
  13. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  14. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts +3 -30
  15. package/lib/types/client/ArkmeRecognizedSpeakersSurface.d.ts.map +1 -1
  16. package/lib/types/client/api.d.ts +1 -1
  17. package/lib/types/client/api.d.ts.map +1 -1
  18. package/lib/types/client/locales/actions-en.d.ts.map +1 -1
  19. package/lib/types/client/recognized-speaker-directory.d.ts +51 -22
  20. package/lib/types/client/recognized-speaker-directory.d.ts.map +1 -1
  21. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts +2 -4
  22. package/lib/types/client/recordings/RecognizedSpeakerEntry.d.ts.map +1 -1
  23. package/lib/types/client/recordings/recording-speaker-options-store.d.ts.map +1 -1
  24. package/lib/types/host-api.d.ts.map +1 -1
  25. package/lib/types/recording-presence.d.ts +1 -1
  26. package/lib/types/services/recording-service.d.ts +4 -0
  27. package/lib/types/services/recording-service.d.ts.map +1 -1
  28. package/lib/types/services/service.d.ts.map +1 -1
  29. package/lib/types/services/speaker-directory-service.d.ts +19 -0
  30. package/lib/types/services/speaker-directory-service.d.ts.map +1 -0
  31. package/lib/types/services/unmarked-speaker-service.d.ts +2 -0
  32. package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
  33. package/lib/types/speaker-directory-contract.d.ts +71 -0
  34. package/lib/types/speaker-directory-contract.d.ts.map +1 -0
  35. package/lib/types/types.d.ts +1 -1
  36. package/lib/types/types.d.ts.map +1 -1
  37. package/package.json +5 -5
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as inspectNativeBundleArtifactV3, At as recordOwnerId, Bt as __exportAll, Ct as recordingImportFileNameKey, D as bundleSha256, E as arkmeSandboxEntryId, Et as registerManagedAiProvider, F as assertExtensionArtifactSize, Ft as parseArrangementBoardCachePages, G as validateVoiceprintPcm16Wav, Ht as __toCommonJS, I as canonicalExtensionJson, It as registerDSHAgentInputRecordSync, J as arkmeRequiredLinkMetadataFallback, K as arkmeExtensionShareRefFromLink, L as sha256Hex, Lt as createDesktopLifecycleReader, M as packLocalNativeBundleDirectoryV3, Mt as parseArkmeRecordReeditAttachments, N as readNativeBundleTarballV3, Nt as parseArkmeRecordReeditMentions, O as canonicalBundleJson, Ot as ArkmePluginError, P as renderArkmeSandboxHostEntry, Q as ARKME_PROVIDER_CONTRACT_VERSION, R as unpackArkmeExtension, Rt as __commonJSMin, S as ARKME_BUNDLE_ARTIFACT_KIND, T as ArkmeBundleArtifactError, Tt as normalizeRecentEmojiIds, U as ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE, Vt as __require, W as ArkmeService, X as arkmeRichBackgroundSound, Y as arkmeFileBackgroundSound, Z as ARKME_DEFAULT_SHARE_WEBSITE, _ as arkmeClientContentDigest, _t as ArkmeDirectMessageAdmissionError, at as cachedMemberItem, b as verifyExtensionResolutionSignature, bt as recordingImportFileKind, c as persistentExtensionBundleDirectory, ct as SelfRoleSyncStore, d as readPersistentExtensionActivation, dt as arkmeCanInlineLocalFile, f as refreshPersistentClientWrapper, ft as arkmeNormalizedFileMimeType, g as activeProfileExtensionOwnerConflicts, gt as SecretValue, h as renderArkmeBundleClientBundle, ht as ArkmeOutgoingCallError, i as invokePersistentArkmeExtension, it as applyMemberUpdate, j as packBundleFiles, jt as ArkmeRecordReeditDraftConflict, k as inspectBundleArtifact, kt as retryAfterMillis, l as persistentExtensionPackageIdentity, lt as ARKME_RUNTIME_INSTANCE_ID, m as writePersistentExtensionActivation, mt as isArkmeBotAvatarRef, nt as ARKME_WORLD_PUBLISH_MAX_IMAGES, o as persistentArkmeExtensionRuntimeState, ot as mergeMemberJoinEvents, p as repairPersistentExtensionInstallation, pt as arkmePickedFileKind, q as arkmeIsGenericLinkMetadataTitle, r as deactivatePersistentArkmeExtension, rt as ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, s as materializePersistentExtensionBundle, st as validateMemberUpdate, t as activatePersistentArkmeExtension, u as quarantinePersistentExtension, ut as ARKME_TOOL_FILE_MAX_BYTES, v as arkmeClientContentDigestFromSource, vt as isRecordingInstantOnOrAfterUnixEpoch, w as ARKME_NATIVE_BUNDLE_ARTIFACT_KIND, wt as isRecentEmojiId, x as verifyNativeBundleResolutionSignature, xt as sameRecordingImportIdentity, y as verifyBundleResolutionSignature, yt as isUnresolvedRecordingImportJob, z as ARKME_EXTENSION_FORMAT, zt as __esmMin } from "./persistent-runtime-DfmCJcxH.js";
1
+ import { A as inspectNativeBundleArtifactV3, At as recordOwnerId, Bt as __exportAll, Ct as recordingImportFileNameKey, D as bundleSha256, E as arkmeSandboxEntryId, Et as registerManagedAiProvider, F as assertExtensionArtifactSize, Ft as parseArrangementBoardCachePages, G as validateVoiceprintPcm16Wav, Ht as __toCommonJS, I as canonicalExtensionJson, It as registerDSHAgentInputRecordSync, J as arkmeRequiredLinkMetadataFallback, K as arkmeExtensionShareRefFromLink, L as sha256Hex, Lt as createDesktopLifecycleReader, M as packLocalNativeBundleDirectoryV3, Mt as parseArkmeRecordReeditAttachments, N as readNativeBundleTarballV3, Nt as parseArkmeRecordReeditMentions, O as canonicalBundleJson, Ot as ArkmePluginError, P as renderArkmeSandboxHostEntry, Q as ARKME_PROVIDER_CONTRACT_VERSION, R as unpackArkmeExtension, Rt as __commonJSMin, S as ARKME_BUNDLE_ARTIFACT_KIND, T as ArkmeBundleArtifactError, Tt as normalizeRecentEmojiIds, U as ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE, Vt as __require, W as ArkmeService, X as arkmeRichBackgroundSound, Y as arkmeFileBackgroundSound, Z as ARKME_DEFAULT_SHARE_WEBSITE, _ as arkmeClientContentDigest, _t as ArkmeDirectMessageAdmissionError, at as cachedMemberItem, b as verifyExtensionResolutionSignature, bt as recordingImportFileKind, c as persistentExtensionBundleDirectory, ct as SelfRoleSyncStore, d as readPersistentExtensionActivation, dt as arkmeCanInlineLocalFile, f as refreshPersistentClientWrapper, ft as arkmeNormalizedFileMimeType, g as activeProfileExtensionOwnerConflicts, gt as SecretValue, h as renderArkmeBundleClientBundle, ht as ArkmeOutgoingCallError, i as invokePersistentArkmeExtension, it as applyMemberUpdate, j as packBundleFiles, jt as ArkmeRecordReeditDraftConflict, k as inspectBundleArtifact, kt as retryAfterMillis, l as persistentExtensionPackageIdentity, lt as ARKME_RUNTIME_INSTANCE_ID, m as writePersistentExtensionActivation, mt as isArkmeBotAvatarRef, nt as ARKME_WORLD_PUBLISH_MAX_IMAGES, o as persistentArkmeExtensionRuntimeState, ot as mergeMemberJoinEvents, p as repairPersistentExtensionInstallation, pt as arkmePickedFileKind, q as arkmeIsGenericLinkMetadataTitle, r as deactivatePersistentArkmeExtension, rt as ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, s as materializePersistentExtensionBundle, st as validateMemberUpdate, t as activatePersistentArkmeExtension, u as quarantinePersistentExtension, ut as ARKME_TOOL_FILE_MAX_BYTES, v as arkmeClientContentDigestFromSource, vt as isRecordingInstantOnOrAfterUnixEpoch, w as ARKME_NATIVE_BUNDLE_ARTIFACT_KIND, wt as isRecentEmojiId, x as verifyNativeBundleResolutionSignature, xt as sameRecordingImportIdentity, y as verifyBundleResolutionSignature, yt as isUnresolvedRecordingImportJob, z as ARKME_EXTENSION_FORMAT, zt as __esmMin } from "./persistent-runtime-0qn1BZ6S.js";
2
2
  import { i as securePrivateFileSync, n as securePrivateDirectorySync, r as securePrivateFile, t as securePrivateDirectory } from "./private-filesystem-CGh4FtyA.js";
3
3
  import { _ as parsePluginUpdateManifest, b as validatePluginUpdateServiceOrigin, d as writePluginUpdateLifecycleLog, f as PluginUpdateInstallStateStore, g as existingCachedPluginArtifactPath, h as downloadAndCachePluginArtifact, m as PluginUpdateArtifactError, n as buildTargetInstallArgs, o as reconcilePluginUpdaterProfilePolicy, p as ARKME_PLUGIN_PACKAGE_NAME$1, t as assertTargetArtifactIntegrity, u as detachManagedProfilePluginLink, v as pluginUpdateEndpointUrl, y as validatePluginUpdateArtifactOrigin } from "./plugin-updater-helper-dNxHpiVg.js";
4
4
  import { i as prepareProfilePackageManager, n as isArkmeProfilePackageName, r as localExtensionPnpmArgs, t as applyArkmeProfileBundlePolicy } from "./profile-bundle-policy-8_y3QfHe.js";
@@ -14277,6 +14277,7 @@ function createArkmeHostApi(service, options) {
14277
14277
  "recordings.import.session.update-ownership",
14278
14278
  "recordings.import.session.delete",
14279
14279
  "recordings.speaker.assign-item",
14280
+ "speaker-directory.seen",
14280
14281
  "openapi.mcp.retry",
14281
14282
  "team.create",
14282
14283
  "team.join-by-jotmo-id"
@@ -14643,6 +14644,11 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
14643
14644
  case "recordings.import.session.delete": return await service.deleteRecordingImportSession(stringParam(params, "sessionRef").trim(), requestSignal);
14644
14645
  case "recordings.playback.open": return await service.recordingPlayback(stringParam(params, "itemRef").trim(), requestSignal);
14645
14646
  case "recordings.speaker.cached-options": return await service.cachedRecordingSpeakerOptions(requestSignal);
14647
+ case "speaker-directory.summary": return await service.speakerDirectorySummary(params, requestSignal);
14648
+ case "speaker-directory.list": return await service.speakerDirectoryList(params, requestSignal);
14649
+ case "speaker-directory.seen": return await service.speakerDirectorySeen(params, requestSignal);
14650
+ case "speaker-directory.open": return await service.speakerDirectoryOpen(params, requestSignal);
14651
+ case "speaker-directory.avatars": return await service.speakerDirectoryAvatars(params, requestSignal);
14646
14652
  case "recordings.speaker.options": return await service.recordingSpeakerOptions(requestSignal);
14647
14653
  case "recordings.speaker.presence": return await service.recordingSpeakerPresence(requestSignal);
14648
14654
  case "recordings.speaker.members": return await service.recordingSpeakerMembers(stringParam(params, "speakerRef").trim(), requestSignal, stringParam(params, "expectedVersion").trim() || void 0);
@@ -1,2 +1,2 @@
1
- import { a as persistentArkmeExtensionActive, i as invokePersistentArkmeExtension, n as applyPersistentArkmeHostExtension, o as persistentArkmeExtensionRuntimeState, r as deactivatePersistentArkmeExtension, t as activatePersistentArkmeExtension } from "./persistent-runtime-DfmCJcxH.js";
1
+ import { a as persistentArkmeExtensionActive, i as invokePersistentArkmeExtension, n as applyPersistentArkmeHostExtension, o as persistentArkmeExtensionRuntimeState, r as deactivatePersistentArkmeExtension, t as activatePersistentArkmeExtension } from "./persistent-runtime-0qn1BZ6S.js";
2
2
  export { activatePersistentArkmeExtension, applyPersistentArkmeHostExtension, deactivatePersistentArkmeExtension, invokePersistentArkmeExtension, persistentArkmeExtensionActive, persistentArkmeExtensionRuntimeState };
@@ -393,7 +393,7 @@ function keys(value, allowed) {
393
393
  function text$12(value, max) {
394
394
  if (typeof value !== "string" || value.length > max) fail$1();
395
395
  }
396
- function integer$6(value) {
396
+ function integer$7(value) {
397
397
  if (!Number.isSafeInteger(value) || value < 0) fail$1();
398
398
  }
399
399
  /** Validate the browser projection only; owner IDs and arbitrary payload fields are never persisted. */
@@ -409,9 +409,9 @@ function parseArrangementBoardCachePages(value) {
409
409
  "nextOffset",
410
410
  "board"
411
411
  ]);
412
- integer$6(page.total);
412
+ integer$7(page.total);
413
413
  if (typeof page.hasMore !== "boolean" || !Array.isArray(page.items) || page.items.length > 50 || page.items.length > page.total) fail$1();
414
- if (page.nextOffset !== void 0) integer$6(page.nextOffset);
414
+ if (page.nextOffset !== void 0) integer$7(page.nextOffset);
415
415
  if (page.board !== void 0) {
416
416
  const board = object$8(page.board);
417
417
  keys(board, ["supported", "version"]);
@@ -440,9 +440,9 @@ function parseArrangementBoardCachePages(value) {
440
440
  text$12(item.title, 2e4);
441
441
  text$12(item.description, 2e5);
442
442
  text$12(item.reminderState, 256);
443
- integer$6(item.createdAtMillis);
444
- integer$6(item.updatedAtMillis);
445
- for (const key of ["dueAtMillis", "remindAtMillis"]) if (item[key] !== void 0) integer$6(item[key]);
443
+ integer$7(item.createdAtMillis);
444
+ integer$7(item.updatedAtMillis);
445
+ for (const key of ["dueAtMillis", "remindAtMillis"]) if (item[key] !== void 0) integer$7(item[key]);
446
446
  if (item.recognitionState !== void 0) text$12(item.recognitionState, 256);
447
447
  if (item.creationSource !== void 0) {
448
448
  const source = object$8(item.creationSource);
@@ -456,12 +456,12 @@ function parseArrangementBoardCachePages(value) {
456
456
  "input",
457
457
  "none"
458
458
  ].includes(source.kind) || !Array.isArray(source.items) || source.items.length > 100) fail$1();
459
- integer$6(source.unavailableCount);
459
+ integer$7(source.unavailableCount);
460
460
  for (const rawSource of source.items) {
461
461
  const entry = object$8(rawSource);
462
462
  keys(entry, ["text", "createdAtMillis"]);
463
463
  text$12(entry.text, 2e5);
464
- if (entry.createdAtMillis !== void 0) integer$6(entry.createdAtMillis);
464
+ if (entry.createdAtMillis !== void 0) integer$7(entry.createdAtMillis);
465
465
  }
466
466
  }
467
467
  }
@@ -1306,7 +1306,7 @@ var HttpArkmeAccountScopeBridge = class {
1306
1306
  this.fetchImpl = fetchImpl;
1307
1307
  }
1308
1308
  async attest(identity) {
1309
- return status$1(await this.request("account.scope.attest", identity));
1309
+ return status$2(await this.request("account.scope.attest", identity));
1310
1310
  }
1311
1311
  async prepare(identity) {
1312
1312
  const value = await this.request("account.scope.prepare", identity);
@@ -1315,17 +1315,17 @@ var HttpArkmeAccountScopeBridge = class {
1315
1315
  return { transitionRef };
1316
1316
  }
1317
1317
  async commit(transitionRef) {
1318
- return status$1(await this.request("account.scope.commit", { transitionRef }));
1318
+ return status$2(await this.request("account.scope.commit", { transitionRef }));
1319
1319
  }
1320
1320
  async abort(transitionRef) {
1321
- if (status$1(await this.request("account.scope.abort", { transitionRef })).status !== "ready") throw new Error("Desktop account scope abort response is invalid");
1321
+ if (status$2(await this.request("account.scope.abort", { transitionRef })).status !== "ready") throw new Error("Desktop account scope abort response is invalid");
1322
1322
  return { status: "ready" };
1323
1323
  }
1324
1324
  async request(action, payload) {
1325
1325
  return await requestArkmeDesktopBridge(this.config, this.fetchImpl, action, payload);
1326
1326
  }
1327
1327
  };
1328
- function status$1(value) {
1328
+ function status$2(value) {
1329
1329
  if (value.status !== "ready" && value.status !== "relaunch") throw new Error("Desktop account scope response is invalid");
1330
1330
  return { status: value.status };
1331
1331
  }
@@ -1799,7 +1799,11 @@ var ServiceRuntime = class {
1799
1799
  });
1800
1800
  if (response.status === 401 || response.status === 403 && !preserveForbiddenError) throw new ArkmePluginError(`auth-http-${response.status}`, "Arkme 登录凭据已失效", false, response.status);
1801
1801
  if (!response.ok) {
1802
- const retryAfter = retryAfterMillis(response.headers.get("retry-after"));
1802
+ let retryAfter = retryAfterMillis(response.headers.get("retry-after"));
1803
+ if (path.startsWith("/api/v1/audio/speaker-directory/") && response.status === 429) try {
1804
+ const milliseconds = objectValue$2(objectValue$2(await response.clone().json()).data).retry_after_ms;
1805
+ if (typeof milliseconds === "number" && Number.isFinite(milliseconds) && milliseconds >= 0) retryAfter = Math.max(retryAfter ?? 0, milliseconds);
1806
+ } catch {}
1803
1807
  if (preserveHttpError) {
1804
1808
  let errorEnvelope;
1805
1809
  try {
@@ -2160,6 +2164,228 @@ var ServiceRuntime = class {
2160
2164
  }
2161
2165
  };
2162
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
2163
2389
  //#region src/services/self-role-service.ts
2164
2390
  /** Account-scoped role recovery. Existing message senders only request a frozen
2165
2391
  * snapshot; role CRUD does not enqueue or resend message content. */
@@ -92396,6 +92622,46 @@ var RecordingService = class {
92396
92622
  ...numberValue$5(data.retry_after_ms) > 0 ? { retryAfterMs: numberValue$5(data.retry_after_ms) } : {}
92397
92623
  };
92398
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
+ }
92399
92665
  /** All-history source identities, authorized by the same account-bound speaker reference as the UI. */
92400
92666
  async recordingSpeakerMembers(speakerRef, signal, expectedVersion) {
92401
92667
  this.assertWorkbenchEnabled();
@@ -93462,7 +93728,7 @@ var RecordingService = class {
93462
93728
  async recordingRefKey(prefix) {
93463
93729
  return createHash("sha256").update(await this.runtime.stateStore.uniqueCode()).update(`\0${prefix}`).digest();
93464
93730
  }
93465
- async recordingSpeakerProfiles(userIds, session, signal) {
93731
+ async recordingSpeakerProfiles(userIds, session, signal, propagateFailure = false) {
93466
93732
  if (this.profile === void 0) return /* @__PURE__ */ new Map();
93467
93733
  if (userIds.length === 0) return /* @__PURE__ */ new Map();
93468
93734
  try {
@@ -93476,7 +93742,8 @@ var RecordingService = class {
93476
93742
  }];
93477
93743
  }));
93478
93744
  return new Map(entries);
93479
- } catch {
93745
+ } catch (error) {
93746
+ if (propagateFailure) throw error;
93480
93747
  return /* @__PURE__ */ new Map();
93481
93748
  }
93482
93749
  }
@@ -96046,6 +96313,28 @@ var UnmarkedSpeakerService = class {
96046
96313
  ...typeof data.cursor_stale === "boolean" ? { cursorStale: data.cursor_stale } : {}
96047
96314
  };
96048
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
+ }
96049
96338
  async markOptions(candidateRef, signal) {
96050
96339
  const { session, entry, normalizedRef } = await this.resolveCandidateRef(candidateRef);
96051
96340
  const data = await this.runtime.authenticatedAudioPost("/api/v1/audio/unmarked-speakers/mark-options", { candidate_id: entry.candidateId }, session, signal, {
@@ -98731,6 +99020,7 @@ var ArkmeService = class {
98731
99020
  relatedQuickNote;
98732
99021
  contact;
98733
99022
  contactDirectory;
99023
+ speakerDirectory;
98734
99024
  unmarkedSpeaker;
98735
99025
  voiceprint;
98736
99026
  userBan;
@@ -98860,6 +99150,7 @@ var ArkmeService = class {
98860
99150
  forwardGateway: new OwnerRecordingForwardGateway(this.runtime, this.source, this.realtime, this.chat)
98861
99151
  });
98862
99152
  this.unmarkedSpeaker = new UnmarkedSpeakerService(this.runtime, this.media);
99153
+ this.speakerDirectory = new SpeakerDirectoryService(this.runtime, this.recording, this.unmarkedSpeaker);
98863
99154
  this.contact = new ContactService(this.runtime, this.source, this.profile, this.realtime);
98864
99155
  this.voiceprint = new VoiceprintService(this.runtime, this.profile, { resolveRegisteredContactUserId: async (contactRef, session, signal) => await this.contact.resolveRegisteredContactUserId(contactRef, session, signal) });
98865
99156
  this.auth = new AuthService(this.runtime, this.profile, {
@@ -99482,6 +99773,21 @@ var ArkmeService = class {
99482
99773
  async cachedRecordingSpeakerOptions(signal) {
99483
99774
  return await this.recording.cachedRecordingSpeakerOptions(signal);
99484
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
+ }
99485
99791
  async recordingSpeakerOptions(signal) {
99486
99792
  return await this.recording.recordingSpeakerOptions(signal);
99487
99793
  }
@@ -83,6 +83,7 @@ export declare class ArkmeService {
83
83
  private readonly relatedQuickNote;
84
84
  private readonly contact;
85
85
  private readonly contactDirectory;
86
+ private readonly speakerDirectory;
86
87
  private readonly unmarkedSpeaker;
87
88
  private readonly voiceprint;
88
89
  private readonly userBan;
@@ -332,6 +333,11 @@ export declare class ArkmeService {
332
333
  recordingDay(dateStamp: number, signal?: AbortSignal): Promise<ArkmeRecordingDay>;
333
334
  recordingPlayback(itemRef: string, signal?: AbortSignal): Promise<ArkmeRecordingPlayback>;
334
335
  cachedRecordingSpeakerOptions(signal?: AbortSignal): Promise<ArkmeRecordingSpeakerCandidate[] | null>;
336
+ speakerDirectorySummary(input: Record<string, unknown>, signal?: AbortSignal): Promise<import("./speaker-directory-contract.js").SpeakerDirectorySummary>;
337
+ speakerDirectoryAvatars(input: Record<string, unknown>, signal?: AbortSignal): Promise<import("./speaker-directory-contract.js").SpeakerDirectoryAvatar[]>;
338
+ speakerDirectoryList(input: Record<string, unknown>, signal?: AbortSignal): Promise<import("./speaker-directory-contract.js").SpeakerDirectoryPage>;
339
+ speakerDirectorySeen(input: Record<string, unknown>, signal?: AbortSignal): Promise<import("./speaker-directory-contract.js").SpeakerDirectorySeen>;
340
+ speakerDirectoryOpen(input: Record<string, unknown>, signal?: AbortSignal): Promise<import("./speaker-directory-contract.js").SpeakerDirectoryDetail>;
335
341
  recordingSpeakerOptions(signal?: AbortSignal): Promise<ArkmeRecordingSpeakerCandidate[]>;
336
342
  recordingSpeakerPresence(signal?: AbortSignal): Promise<ArkmeRecordingSpeakerPresence>;
337
343
  recordingSpeakerMembers(speakerRef: string, signal?: AbortSignal, expectedVersion?: string): Promise<import("./types.js").ArkmeRecordingSpeakerMembers>;