@senguoyun/dsh-arkme 0.1.47 → 0.1.49

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 (160) hide show
  1. package/docs/dsh-remote-liveness.md +46 -0
  2. package/docs/member-directory-premerge-review.md +99 -0
  3. package/docs/member-directory-review.md +53 -0
  4. package/docs/shared-conversation-members.md +61 -0
  5. package/docs/sidebar-local-first-review.md +104 -0
  6. package/docs/sidebar-local-first.md +61 -0
  7. package/lib/bundle-runtime.js +1 -1
  8. package/lib/client.js +68338 -66607
  9. package/lib/client.js.map +1 -1
  10. package/lib/extension-profile-restart-helper.js +5 -5
  11. package/lib/harness-model-client.js +36 -2
  12. package/lib/harness-session-client.js +124 -0
  13. package/lib/index.js +10940 -822
  14. package/lib/{install-store-C1tAM346.js → install-store-bnyQDtYt.js} +1 -1
  15. package/lib/persistent-extension.js +1 -1
  16. package/lib/{persistent-runtime-DpNP9UKr.js → persistent-runtime-D2pt8S-e.js} +1587 -280
  17. package/lib/{plugin-updater-helper-BByro9U_.js → plugin-updater-helper-B0yVav4f.js} +1 -1
  18. package/lib/{plugin-updater-helper-BByw0ZyG.js → plugin-updater-helper-BbjKUV9T.js} +3 -3
  19. package/lib/plugin-updater-helper.js +1 -1
  20. package/lib/sdk.js +36 -2
  21. package/lib/types/arkme-service.d.ts +14 -0
  22. package/lib/types/arkme-service.d.ts.map +1 -1
  23. package/lib/types/chat-realtime.d.ts +6 -3
  24. package/lib/types/chat-realtime.d.ts.map +1 -1
  25. package/lib/types/client/ArkmeAvatar.d.ts +2 -1
  26. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  27. package/lib/types/client/ArkmeContactAddSurface.d.ts +2 -1
  28. package/lib/types/client/ArkmeContactAddSurface.d.ts.map +1 -1
  29. package/lib/types/client/ArkmeDSHBetaCommunityEntry.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeDirectoryWindow.d.ts +6 -0
  31. package/lib/types/client/ArkmeDirectoryWindow.d.ts.map +1 -0
  32. package/lib/types/client/ArkmeGroupChatControls.d.ts +1 -1
  33. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  34. package/lib/types/client/ArkmeMessageReadReceipt.d.ts.map +1 -1
  35. package/lib/types/client/ArkmePersistentShell.d.ts +1 -1
  36. package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
  37. package/lib/types/client/ArkmeRecordingSurface.d.ts +3 -1
  38. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  39. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  40. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +2 -1
  41. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  42. package/lib/types/client/DeepSeekHarnessSurface.d.ts +3 -1
  43. package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -1
  44. package/lib/types/client/api.d.ts +1 -1
  45. package/lib/types/client/api.d.ts.map +1 -1
  46. package/lib/types/client/avatar-image-store.d.ts +3 -2
  47. package/lib/types/client/avatar-image-store.d.ts.map +1 -1
  48. package/lib/types/client/bot-chat-directory-projection.d.ts +2 -0
  49. package/lib/types/client/bot-chat-directory-projection.d.ts.map +1 -1
  50. package/lib/types/client/bot-directory-preferences.d.ts +2 -0
  51. package/lib/types/client/bot-directory-preferences.d.ts.map +1 -1
  52. package/lib/types/client/chat-directory-store.d.ts +4 -0
  53. package/lib/types/client/chat-directory-store.d.ts.map +1 -1
  54. package/lib/types/client/conversation-members-store.d.ts +46 -0
  55. package/lib/types/client/conversation-members-store.d.ts.map +1 -0
  56. package/lib/types/client/harness-session-client.d.ts +5 -0
  57. package/lib/types/client/harness-session-client.d.ts.map +1 -0
  58. package/lib/types/client/message-read-receipt-store.d.ts +7 -1
  59. package/lib/types/client/message-read-receipt-store.d.ts.map +1 -1
  60. package/lib/types/client/realtime-client-events.d.ts.map +1 -1
  61. package/lib/types/client/recordings/ArkmeRecordingImportDialog.d.ts +4 -1
  62. package/lib/types/client/recordings/ArkmeRecordingImportDialog.d.ts.map +1 -1
  63. package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts +3 -2
  64. package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts.map +1 -1
  65. package/lib/types/client/redesign/contacts/ContactDirectoryAddDialog.d.ts +8 -0
  66. package/lib/types/client/redesign/contacts/ContactDirectoryAddDialog.d.ts.map +1 -0
  67. package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts +13 -4
  68. package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts.map +1 -1
  69. package/lib/types/client/redesign/contacts/ContactDirectoryToolbar.d.ts +7 -0
  70. package/lib/types/client/redesign/contacts/ContactDirectoryToolbar.d.ts.map +1 -0
  71. package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts +12 -2
  72. package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts.map +1 -1
  73. package/lib/types/client/redesign/contacts/ContactRemarkDialog.d.ts +9 -0
  74. package/lib/types/client/redesign/contacts/ContactRemarkDialog.d.ts.map +1 -0
  75. package/lib/types/client/redesign/contacts/ContactWorldList.d.ts +2 -0
  76. package/lib/types/client/redesign/contacts/ContactWorldList.d.ts.map +1 -1
  77. package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts +3 -2
  78. package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts.map +1 -1
  79. package/lib/types/client/redesign/contacts/contact-date.d.ts +3 -0
  80. package/lib/types/client/redesign/contacts/contact-date.d.ts.map +1 -0
  81. package/lib/types/client/redesign/contacts/contact-directory-search.d.ts +10 -0
  82. package/lib/types/client/redesign/contacts/contact-directory-search.d.ts.map +1 -0
  83. package/lib/types/client/redesign/contacts/contact-directory-state.d.ts +4 -1
  84. package/lib/types/client/redesign/contacts/contact-directory-state.d.ts.map +1 -1
  85. package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts +6 -2
  86. package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts.map +1 -1
  87. package/lib/types/client/use-arkme-avatar-image.d.ts +2 -1
  88. package/lib/types/client/use-arkme-avatar-image.d.ts.map +1 -1
  89. package/lib/types/client/use-conversation-members.d.ts +3 -0
  90. package/lib/types/client/use-conversation-members.d.ts.map +1 -0
  91. package/lib/types/dsh-remote/api-proxy-adapter.d.ts +1 -0
  92. package/lib/types/dsh-remote/api-proxy-adapter.d.ts.map +1 -1
  93. package/lib/types/dsh-remote/channel-manager.d.ts +9 -2
  94. package/lib/types/dsh-remote/channel-manager.d.ts.map +1 -1
  95. package/lib/types/dsh-remote/connection-diagnostics.d.ts +35 -0
  96. package/lib/types/dsh-remote/connection-diagnostics.d.ts.map +1 -0
  97. package/lib/types/dsh-remote/current-session-tool.d.ts +3 -0
  98. package/lib/types/dsh-remote/current-session-tool.d.ts.map +1 -0
  99. package/lib/types/dsh-remote/default-socket-factory.d.ts.map +1 -1
  100. package/lib/types/dsh-remote/desktop-session-presence.d.ts +21 -0
  101. package/lib/types/dsh-remote/desktop-session-presence.d.ts.map +1 -0
  102. package/lib/types/dsh-remote/dsh-event-contract.d.ts +2 -0
  103. package/lib/types/dsh-remote/dsh-event-contract.d.ts.map +1 -1
  104. package/lib/types/dsh-remote/host.d.ts +40 -6
  105. package/lib/types/dsh-remote/host.d.ts.map +1 -1
  106. package/lib/types/dsh-remote/live-event-batcher.d.ts +46 -0
  107. package/lib/types/dsh-remote/live-event-batcher.d.ts.map +1 -0
  108. package/lib/types/dsh-remote/protocol-v1.d.ts.map +1 -1
  109. package/lib/types/dsh-remote/realtime-transport.d.ts +19 -1
  110. package/lib/types/dsh-remote/realtime-transport.d.ts.map +1 -1
  111. package/lib/types/dsh-remote/types.d.ts +18 -2
  112. package/lib/types/dsh-remote/types.d.ts.map +1 -1
  113. package/lib/types/harness-embed-contract.d.ts +2 -0
  114. package/lib/types/harness-embed-contract.d.ts.map +1 -1
  115. package/lib/types/harness-embed-route.d.ts +4 -0
  116. package/lib/types/harness-embed-route.d.ts.map +1 -1
  117. package/lib/types/host-api.d.ts.map +1 -1
  118. package/lib/types/index.d.ts +1 -0
  119. package/lib/types/index.d.ts.map +1 -1
  120. package/lib/types/local-database.d.ts +11 -1
  121. package/lib/types/local-database.d.ts.map +1 -1
  122. package/lib/types/member-directory.d.ts +11 -0
  123. package/lib/types/member-directory.d.ts.map +1 -0
  124. package/lib/types/sdk/index.d.ts +24 -3
  125. package/lib/types/sdk/index.d.ts.map +1 -1
  126. package/lib/types/services/bot-service.d.ts +3 -0
  127. package/lib/types/services/bot-service.d.ts.map +1 -1
  128. package/lib/types/services/chat-realtime-service.d.ts +1 -0
  129. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  130. package/lib/types/services/chat-service.d.ts +14 -1
  131. package/lib/types/services/chat-service.d.ts.map +1 -1
  132. package/lib/types/services/contact-directory-service.d.ts +3 -0
  133. package/lib/types/services/contact-directory-service.d.ts.map +1 -1
  134. package/lib/types/services/conversation-directory-service.d.ts +72 -0
  135. package/lib/types/services/conversation-directory-service.d.ts.map +1 -0
  136. package/lib/types/services/desktop-attention-bridge.d.ts +6 -1
  137. package/lib/types/services/desktop-attention-bridge.d.ts.map +1 -1
  138. package/lib/types/services/group-service.d.ts.map +1 -1
  139. package/lib/types/services/media-service.d.ts +1 -0
  140. package/lib/types/services/media-service.d.ts.map +1 -1
  141. package/lib/types/services/service.d.ts +11 -0
  142. package/lib/types/services/service.d.ts.map +1 -1
  143. package/lib/types/services/source-service.d.ts +3 -0
  144. package/lib/types/services/source-service.d.ts.map +1 -1
  145. package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
  146. package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
  147. package/lib/types/tools/business/conversation/list-sources.d.ts +1 -0
  148. package/lib/types/tools/business/conversation/list-sources.d.ts.map +1 -1
  149. package/lib/types/tools/business/conversation/member-pages.d.ts +2 -0
  150. package/lib/types/tools/business/conversation/member-pages.d.ts.map +1 -0
  151. package/lib/types/tools/business/conversation/read-receipts.d.ts.map +1 -1
  152. package/lib/types/tools/business/index.d.ts.map +1 -1
  153. package/lib/types/tools/ports/conversations.d.ts +13 -1
  154. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  155. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  156. package/lib/types/types.d.ts +66 -2
  157. package/lib/types/types.d.ts.map +1 -1
  158. package/package.json +7 -6
  159. package/lib/{bundle-runtime-BTzRlc6V.js → bundle-runtime-CE73h3vS.js} +1 -1
  160. package/lib/{profile-bundle-policy-CvojZuTD.js → profile-bundle-policy-DLXDyohJ.js} +1 -1
@@ -1,20 +1,21 @@
1
+ import { createRequire } from "node:module";
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
1
3
  import { chmodSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import * as fs from "node:fs/promises";
5
+ import { chmod as chmod$1, copyFile, link, mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
2
6
  import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from "node:crypto";
3
7
  import minpath, { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
8
  import { fileURLToPath, fileURLToPath as urlToPath } from "node:url";
5
- import * as fs from "node:fs/promises";
6
- import { chmod as chmod$1, copyFile, link, mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
7
- import minproc from "node:process";
8
- import { Resolver, lookup } from "node:dns/promises";
9
+ import { gunzipSync, gzipSync } from "node:zlib";
9
10
  import { request } from "node:http";
10
11
  import { request as request$1 } from "node:https";
12
+ import { Readable } from "node:stream";
11
13
  import { BlockList, isIP } from "node:net";
14
+ import minproc from "node:process";
15
+ import { Resolver, lookup } from "node:dns/promises";
12
16
  import OSS from "ali-oss";
13
- import { Readable } from "node:stream";
14
- import { gunzipSync, gzipSync } from "node:zlib";
15
17
  import { createContext, runInContext } from "node:vm";
16
18
  import { Context } from "@deepseek-ai/cordis";
17
- import { defineTool } from "@deepseek-ai/dsh-tools";
18
19
  import { parseDocument } from "yaml";
19
20
  //#region \0rolldown/runtime.js
20
21
  var __create = Object.create;
@@ -23,6 +24,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
23
24
  var __getOwnPropNames = Object.getOwnPropertyNames;
24
25
  var __getProtoOf = Object.getPrototypeOf;
25
26
  var __hasOwnProp = Object.prototype.hasOwnProperty;
27
+ var __esmMin = (fn, res, err) => () => {
28
+ if (err) throw err[0];
29
+ try {
30
+ return fn && (res = fn(fn = 0)), res;
31
+ } catch (e) {
32
+ throw err = [e], e;
33
+ }
34
+ };
26
35
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
27
36
  var __exportAll = (all, no_symbols) => {
28
37
  let target = {};
@@ -47,15 +56,258 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
47
56
  value: mod,
48
57
  enumerable: true
49
58
  }) : target, mod));
59
+ var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
60
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
50
61
  //#endregion
51
- //#region src/direct-message-admission.ts
52
- function directMessageAdmissionMessage(admission) {
53
- switch (admission.state) {
54
- case "allowed": return "";
55
- case "refused_by_self": return "你已拒收对方的消息";
56
- case "refused_by_counterpart": return "对方已拒收你的消息";
57
- case "mutually_refused": return "你们已互相拒收消息";
62
+ //#region src/services/desktop-attention-bridge.ts
63
+ const DESKTOP_BRIDGE_TIMEOUT_MS = 2e3;
64
+ const DESKTOP_NOTIFICATION_RETRY_MS = 250;
65
+ const MAX_NOTIFICATION_TITLE_LENGTH = 128;
66
+ const MAX_NOTIFICATION_BODY_LENGTH = 512;
67
+ const MAX_NATIVE_BADGE_COUNT = 999999;
68
+ function nonEmptyBounded(value, maxLength) {
69
+ const normalized = value?.trim() ?? "";
70
+ return normalized !== "" && normalized.length <= maxLength ? normalized : void 0;
71
+ }
72
+ function isLoopbackHostname(hostname) {
73
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
74
+ }
75
+ /**
76
+ * Resolve the native bridge only when the complete, client-injected contract is
77
+ * present. Partial or non-loopback configuration is rejected as unavailable;
78
+ * no credential is ever sent to a caller-controlled origin.
79
+ */
80
+ function arkmeDesktopBridgeConfigFromEnv(env) {
81
+ const rawEndpoint = env.ARKME_DESKTOP_BRIDGE_URL?.trim() ?? "";
82
+ const token = nonEmptyBounded(env.ARKME_DESKTOP_BRIDGE_TOKEN, 512);
83
+ const sessionId = nonEmptyBounded(env.ARKME_DESKTOP_BRIDGE_SESSION_ID, 160);
84
+ if (rawEndpoint === "" && token === void 0 && sessionId === void 0) return void 0;
85
+ if (rawEndpoint === "" || token === void 0 || token.length < 16 || /\s/.test(token) || sessionId === void 0 || sessionId.length < 8 || !/^[A-Za-z0-9._:-]+$/.test(sessionId)) return void 0;
86
+ let endpoint;
87
+ try {
88
+ endpoint = new URL(rawEndpoint);
89
+ } catch {
90
+ return;
58
91
  }
92
+ if (endpoint.protocol !== "http:" || !isLoopbackHostname(endpoint.hostname) || endpoint.port === "" || endpoint.username !== "" || endpoint.password !== "" || endpoint.pathname !== "/v1/actions" || endpoint.search !== "" || endpoint.hash !== "") return void 0;
93
+ return {
94
+ endpoint: endpoint.toString(),
95
+ token,
96
+ sessionId
97
+ };
98
+ }
99
+ function responseObject(value) {
100
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
101
+ }
102
+ async function requestArkmeDesktopBridge(config, fetchImpl, action, payload) {
103
+ const response = await fetchImpl(config.endpoint, {
104
+ method: "POST",
105
+ headers: {
106
+ authorization: `Bearer ${config.token}`,
107
+ "content-type": "application/json"
108
+ },
109
+ body: JSON.stringify({
110
+ schemaVersion: 1,
111
+ sessionId: config.sessionId,
112
+ action,
113
+ payload
114
+ }),
115
+ signal: AbortSignal.timeout(DESKTOP_BRIDGE_TIMEOUT_MS)
116
+ });
117
+ if (!response.ok) throw new Error(`desktop bridge returned HTTP ${String(response.status)}`);
118
+ const body = responseObject(await response.json());
119
+ const value = responseObject(body?.value);
120
+ if (body?.ok !== true || value === void 0) throw new Error("desktop bridge returned an invalid response");
121
+ return value;
122
+ }
123
+ function outcomeFromValue(value) {
124
+ if (typeof value.accepted !== "boolean") return void 0;
125
+ return value.outcome === "accepted" || value.outcome === "duplicate" || value.outcome === "unsupported" || value.outcome === "native-failed" || value.outcome === "expired" || value.outcome === "rate-limited" ? value.outcome : void 0;
126
+ }
127
+ var ArkmeDesktopAttentionBridge = class {
128
+ config;
129
+ fetchImpl;
130
+ capabilities;
131
+ badgeRevision = 0;
132
+ badgeSourceRevision = 0;
133
+ badgeGeneration;
134
+ lastBadgeCount;
135
+ badgeMutationTail = Promise.resolve();
136
+ constructor(config = arkmeDesktopBridgeConfigFromEnv(process.env), fetchImpl = fetch, badgeGeneration = Number(process.hrtime.bigint() / 1000n)) {
137
+ this.config = config;
138
+ this.fetchImpl = fetchImpl;
139
+ this.badgeGeneration = Number.isSafeInteger(badgeGeneration) && badgeGeneration > 0 ? badgeGeneration : Math.max(1, Date.now() * 1e3);
140
+ }
141
+ available() {
142
+ return this.config !== void 0;
143
+ }
144
+ async showNotification(payload) {
145
+ const capabilities = await this.readCapabilities(true);
146
+ if (capabilities?.notificationShow !== true) return {
147
+ fallbackToBrowser: true,
148
+ outcome: capabilities === void 0 ? "unavailable" : "unsupported"
149
+ };
150
+ const boundedPayload = {
151
+ ...payload,
152
+ presentation: {
153
+ title: payload.presentation.title.trim().slice(0, MAX_NOTIFICATION_TITLE_LENGTH),
154
+ body: payload.presentation.body.trim().slice(0, MAX_NOTIFICATION_BODY_LENGTH)
155
+ }
156
+ };
157
+ for (let attempt = 0; attempt < 2; attempt += 1) {
158
+ try {
159
+ const outcome = outcomeFromValue(await this.request("notification.show", boundedPayload)) ?? "native-failed";
160
+ if (outcome !== "native-failed" || attempt > 0) return {
161
+ fallbackToBrowser: false,
162
+ outcome
163
+ };
164
+ } catch {
165
+ if (attempt > 0) return {
166
+ fallbackToBrowser: false,
167
+ outcome: "native-failed"
168
+ };
169
+ }
170
+ await new Promise((resolve) => {
171
+ setTimeout(resolve, DESKTOP_NOTIFICATION_RETRY_MS);
172
+ });
173
+ }
174
+ return {
175
+ fallbackToBrowser: false,
176
+ outcome: "native-failed"
177
+ };
178
+ }
179
+ async applyBadgeSummary(summary) {
180
+ const count = Math.min(MAX_NATIVE_BADGE_COUNT, Math.max(0, Math.trunc(Number.isFinite(summary.count) ? summary.count : 0)));
181
+ const revision = Math.trunc(summary.revision);
182
+ if (!Number.isSafeInteger(revision) || revision <= 0) return false;
183
+ const pending = this.badgeMutationTail.then(async () => await this.applyBadgeSnapshotSerial(count, revision));
184
+ this.badgeMutationTail = pending.then(() => void 0, () => void 0);
185
+ return await pending;
186
+ }
187
+ async resetBadgeCount() {
188
+ const pending = this.badgeMutationTail.then(async () => {
189
+ this.badgeGeneration += 1;
190
+ this.badgeRevision = 0;
191
+ this.badgeSourceRevision = 0;
192
+ this.lastBadgeCount = void 0;
193
+ for (let attempt = 0; attempt < 3; attempt += 1) if (await this.applyBadgeSnapshotSerial(0, 1)) return true;
194
+ return false;
195
+ });
196
+ this.badgeMutationTail = pending.then(() => void 0, () => void 0);
197
+ return await pending;
198
+ }
199
+ async applyBadgeSnapshotSerial(count, sourceRevision) {
200
+ if (sourceRevision < this.badgeSourceRevision || sourceRevision === this.badgeSourceRevision && count === this.lastBadgeCount) return true;
201
+ const capabilities = await this.readCapabilities();
202
+ if (capabilities === void 0) return this.config === void 0;
203
+ if (capabilities.badgeApplySnapshot.mode === "unsupported") return true;
204
+ const revision = Math.max(sourceRevision, this.badgeRevision + 1);
205
+ try {
206
+ const outcome = outcomeFromValue(await this.request("badge.applySnapshot", {
207
+ generation: this.badgeGeneration,
208
+ revision,
209
+ count
210
+ }));
211
+ if (outcome !== "accepted" && outcome !== "duplicate") return false;
212
+ this.badgeRevision = revision;
213
+ this.badgeSourceRevision = sourceRevision;
214
+ this.lastBadgeCount = count;
215
+ return true;
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
220
+ async readCapabilities(refresh = false) {
221
+ if (this.config === void 0) return void 0;
222
+ if (!refresh && this.capabilities !== void 0) return this.capabilities;
223
+ try {
224
+ const value = await this.request("capabilities.get", {});
225
+ if (value.schemaVersion !== 1 || value.sessionId !== this.config.sessionId) return void 0;
226
+ const capabilities = responseObject(value.capabilities);
227
+ const mode = responseObject(capabilities?.badgeApplySnapshot)?.mode;
228
+ if (capabilities?.notificationShow !== true && capabilities?.notificationShow !== false) return void 0;
229
+ if (mode !== "count" && mode !== "dot" && mode !== "unsupported") return void 0;
230
+ this.capabilities = {
231
+ notificationShow: capabilities.notificationShow,
232
+ badgeApplySnapshot: { mode }
233
+ };
234
+ return this.capabilities;
235
+ } catch {
236
+ return;
237
+ }
238
+ }
239
+ async request(action, payload) {
240
+ const config = this.config;
241
+ if (config === void 0) throw new Error("desktop bridge unavailable");
242
+ return await requestArkmeDesktopBridge(config, this.fetchImpl, action, payload);
243
+ }
244
+ };
245
+ /** Samples optional native lifecycle facts without delaying the Host session poll. */
246
+ function createDesktopLifecycleReader(fetchImpl) {
247
+ const config = arkmeDesktopBridgeConfigFromEnv(process.env);
248
+ let available;
249
+ let latest;
250
+ let flight;
251
+ let nextRead = 0;
252
+ return async () => {
253
+ if (config === void 0 || available === false || flight !== void 0 || performance.now() < nextRead) return latest;
254
+ nextRead = performance.now() + 2e3;
255
+ flight = (async () => {
256
+ if (available === void 0) {
257
+ available = responseObject(responseObject((await requestArkmeDesktopBridge(config, fetchImpl, "capabilities.get", {})).capabilities)?.lifecycle)?.version === 1;
258
+ if (!available) return;
259
+ }
260
+ const value = await requestArkmeDesktopBridge(config, fetchImpl, "lifecycle.get", {});
261
+ if (Number.isSafeInteger(value.resumeGeneration) && Number(value.resumeGeneration) >= 0 && typeof value.suspended === "boolean") latest = {
262
+ resumeGeneration: Number(value.resumeGeneration),
263
+ suspended: value.suspended
264
+ };
265
+ })().catch(() => void 0).finally(() => {
266
+ flight = void 0;
267
+ });
268
+ return latest;
269
+ };
270
+ }
271
+ //#endregion
272
+ //#region src/chat-attention.ts
273
+ function integerLikeValue$5(value) {
274
+ if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
275
+ if (typeof value !== "string" || value.trim() === "") return void 0;
276
+ const parsed = Number(value.trim());
277
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : void 0;
278
+ }
279
+ function arkmeChatPolicyNotificationDisabled(policy) {
280
+ if (policy === null || typeof policy !== "object" || Array.isArray(policy)) return void 0;
281
+ const value = policy;
282
+ const muteState = integerLikeValue$5(value.mute_state ?? value.muteState);
283
+ const notifyState = integerLikeValue$5(value.notify_state ?? value.notifyState);
284
+ if (muteState === 2 || notifyState === 2) return true;
285
+ if (muteState === 1 || notifyState === 1) return false;
286
+ }
287
+ function projectArkmeChatAttention(rawUnreadCount, policy, fallbackMuted = false) {
288
+ const unreadCount = Math.max(0, integerLikeValue$5(rawUnreadCount) ?? 0);
289
+ const isMuted = arkmeChatPolicyNotificationDisabled(policy) ?? fallbackMuted;
290
+ const notificationAllowed = !isMuted;
291
+ return {
292
+ unreadCount,
293
+ badgeUnreadCount: notificationAllowed ? unreadCount : 0,
294
+ notificationAllowed,
295
+ isMuted
296
+ };
297
+ }
298
+ function projectArkmeChatAttentionFromMuted(rawUnreadCount, isMuted) {
299
+ return projectArkmeChatAttention(rawUnreadCount, void 0, isMuted);
300
+ }
301
+ //#endregion
302
+ //#region src/chat-pin-projection.ts
303
+ /** Policy freshness is independent of message sequence and event delivery order. */
304
+ function retainNewerArkmeChatPin(current, incoming) {
305
+ if (incoming.kind !== "private_chat" && incoming.kind !== "group_chat" || current?.kind !== incoming.kind || current.isPinned === void 0 || current.chatPolicyUpdatedAtMillis === void 0 || current.chatPolicyUpdatedAtMillis <= (incoming.chatPolicyUpdatedAtMillis ?? 0)) return incoming;
306
+ return {
307
+ ...incoming,
308
+ isPinned: current.isPinned,
309
+ chatPolicyUpdatedAtMillis: current.chatPolicyUpdatedAtMillis
310
+ };
59
311
  }
60
312
  //#endregion
61
313
  //#region src/request-coordinator.ts
@@ -485,190 +737,6 @@ var ArkmeRequestCoordinator = class {
485
737
  }
486
738
  };
487
739
  //#endregion
488
- //#region src/services/desktop-attention-bridge.ts
489
- const DESKTOP_BRIDGE_TIMEOUT_MS = 2e3;
490
- const DESKTOP_NOTIFICATION_RETRY_MS = 250;
491
- const MAX_NOTIFICATION_TITLE_LENGTH = 128;
492
- const MAX_NOTIFICATION_BODY_LENGTH = 512;
493
- const MAX_NATIVE_BADGE_COUNT = 999999;
494
- function nonEmptyBounded(value, maxLength) {
495
- const normalized = value?.trim() ?? "";
496
- return normalized !== "" && normalized.length <= maxLength ? normalized : void 0;
497
- }
498
- function isLoopbackHostname(hostname) {
499
- return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
500
- }
501
- /**
502
- * Resolve the native bridge only when the complete, client-injected contract is
503
- * present. Partial or non-loopback configuration is rejected as unavailable;
504
- * no credential is ever sent to a caller-controlled origin.
505
- */
506
- function arkmeDesktopBridgeConfigFromEnv(env) {
507
- const rawEndpoint = env.ARKME_DESKTOP_BRIDGE_URL?.trim() ?? "";
508
- const token = nonEmptyBounded(env.ARKME_DESKTOP_BRIDGE_TOKEN, 512);
509
- const sessionId = nonEmptyBounded(env.ARKME_DESKTOP_BRIDGE_SESSION_ID, 160);
510
- if (rawEndpoint === "" && token === void 0 && sessionId === void 0) return void 0;
511
- if (rawEndpoint === "" || token === void 0 || token.length < 16 || /\s/.test(token) || sessionId === void 0 || sessionId.length < 8 || !/^[A-Za-z0-9._:-]+$/.test(sessionId)) return void 0;
512
- let endpoint;
513
- try {
514
- endpoint = new URL(rawEndpoint);
515
- } catch {
516
- return;
517
- }
518
- if (endpoint.protocol !== "http:" || !isLoopbackHostname(endpoint.hostname) || endpoint.port === "" || endpoint.username !== "" || endpoint.password !== "" || endpoint.pathname !== "/v1/actions" || endpoint.search !== "" || endpoint.hash !== "") return void 0;
519
- return {
520
- endpoint: endpoint.toString(),
521
- token,
522
- sessionId
523
- };
524
- }
525
- function responseObject(value) {
526
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
527
- }
528
- async function requestArkmeDesktopBridge(config, fetchImpl, action, payload) {
529
- const response = await fetchImpl(config.endpoint, {
530
- method: "POST",
531
- headers: {
532
- authorization: `Bearer ${config.token}`,
533
- "content-type": "application/json"
534
- },
535
- body: JSON.stringify({
536
- schemaVersion: 1,
537
- sessionId: config.sessionId,
538
- action,
539
- payload
540
- }),
541
- signal: AbortSignal.timeout(DESKTOP_BRIDGE_TIMEOUT_MS)
542
- });
543
- if (!response.ok) throw new Error(`desktop bridge returned HTTP ${String(response.status)}`);
544
- const body = responseObject(await response.json());
545
- const value = responseObject(body?.value);
546
- if (body?.ok !== true || value === void 0) throw new Error("desktop bridge returned an invalid response");
547
- return value;
548
- }
549
- function outcomeFromValue(value) {
550
- if (typeof value.accepted !== "boolean") return void 0;
551
- return value.outcome === "accepted" || value.outcome === "duplicate" || value.outcome === "unsupported" || value.outcome === "native-failed" || value.outcome === "expired" || value.outcome === "rate-limited" ? value.outcome : void 0;
552
- }
553
- var ArkmeDesktopAttentionBridge = class {
554
- config;
555
- fetchImpl;
556
- capabilities;
557
- badgeRevision = 0;
558
- badgeSourceRevision = 0;
559
- badgeGeneration;
560
- lastBadgeCount;
561
- badgeMutationTail = Promise.resolve();
562
- constructor(config = arkmeDesktopBridgeConfigFromEnv(process.env), fetchImpl = fetch, badgeGeneration = Number(process.hrtime.bigint() / 1000n)) {
563
- this.config = config;
564
- this.fetchImpl = fetchImpl;
565
- this.badgeGeneration = Number.isSafeInteger(badgeGeneration) && badgeGeneration > 0 ? badgeGeneration : Math.max(1, Date.now() * 1e3);
566
- }
567
- available() {
568
- return this.config !== void 0;
569
- }
570
- async showNotification(payload) {
571
- const capabilities = await this.readCapabilities(true);
572
- if (capabilities?.notificationShow !== true) return {
573
- fallbackToBrowser: true,
574
- outcome: capabilities === void 0 ? "unavailable" : "unsupported"
575
- };
576
- const boundedPayload = {
577
- ...payload,
578
- presentation: {
579
- title: payload.presentation.title.trim().slice(0, MAX_NOTIFICATION_TITLE_LENGTH),
580
- body: payload.presentation.body.trim().slice(0, MAX_NOTIFICATION_BODY_LENGTH)
581
- }
582
- };
583
- for (let attempt = 0; attempt < 2; attempt += 1) {
584
- try {
585
- const outcome = outcomeFromValue(await this.request("notification.show", boundedPayload)) ?? "native-failed";
586
- if (outcome !== "native-failed" || attempt > 0) return {
587
- fallbackToBrowser: false,
588
- outcome
589
- };
590
- } catch {
591
- if (attempt > 0) return {
592
- fallbackToBrowser: false,
593
- outcome: "native-failed"
594
- };
595
- }
596
- await new Promise((resolve) => {
597
- setTimeout(resolve, DESKTOP_NOTIFICATION_RETRY_MS);
598
- });
599
- }
600
- return {
601
- fallbackToBrowser: false,
602
- outcome: "native-failed"
603
- };
604
- }
605
- async applyBadgeSummary(summary) {
606
- const count = Math.min(MAX_NATIVE_BADGE_COUNT, Math.max(0, Math.trunc(Number.isFinite(summary.count) ? summary.count : 0)));
607
- const revision = Math.trunc(summary.revision);
608
- if (!Number.isSafeInteger(revision) || revision <= 0) return false;
609
- const pending = this.badgeMutationTail.then(async () => await this.applyBadgeSnapshotSerial(count, revision));
610
- this.badgeMutationTail = pending.then(() => void 0, () => void 0);
611
- return await pending;
612
- }
613
- async resetBadgeCount() {
614
- const pending = this.badgeMutationTail.then(async () => {
615
- this.badgeGeneration += 1;
616
- this.badgeRevision = 0;
617
- this.badgeSourceRevision = 0;
618
- this.lastBadgeCount = void 0;
619
- for (let attempt = 0; attempt < 3; attempt += 1) if (await this.applyBadgeSnapshotSerial(0, 1)) return true;
620
- return false;
621
- });
622
- this.badgeMutationTail = pending.then(() => void 0, () => void 0);
623
- return await pending;
624
- }
625
- async applyBadgeSnapshotSerial(count, sourceRevision) {
626
- if (sourceRevision < this.badgeSourceRevision || sourceRevision === this.badgeSourceRevision && count === this.lastBadgeCount) return true;
627
- const capabilities = await this.readCapabilities();
628
- if (capabilities === void 0) return this.config === void 0;
629
- if (capabilities.badgeApplySnapshot.mode === "unsupported") return true;
630
- const revision = Math.max(sourceRevision, this.badgeRevision + 1);
631
- try {
632
- const outcome = outcomeFromValue(await this.request("badge.applySnapshot", {
633
- generation: this.badgeGeneration,
634
- revision,
635
- count
636
- }));
637
- if (outcome !== "accepted" && outcome !== "duplicate") return false;
638
- this.badgeRevision = revision;
639
- this.badgeSourceRevision = sourceRevision;
640
- this.lastBadgeCount = count;
641
- return true;
642
- } catch {
643
- return false;
644
- }
645
- }
646
- async readCapabilities(refresh = false) {
647
- if (this.config === void 0) return void 0;
648
- if (!refresh && this.capabilities !== void 0) return this.capabilities;
649
- try {
650
- const value = await this.request("capabilities.get", {});
651
- if (value.schemaVersion !== 1 || value.sessionId !== this.config.sessionId) return void 0;
652
- const capabilities = responseObject(value.capabilities);
653
- const mode = responseObject(capabilities?.badgeApplySnapshot)?.mode;
654
- if (capabilities?.notificationShow !== true && capabilities?.notificationShow !== false) return void 0;
655
- if (mode !== "count" && mode !== "dot" && mode !== "unsupported") return void 0;
656
- this.capabilities = {
657
- notificationShow: capabilities.notificationShow,
658
- badgeApplySnapshot: { mode }
659
- };
660
- return this.capabilities;
661
- } catch {
662
- return;
663
- }
664
- }
665
- async request(action, payload) {
666
- const config = this.config;
667
- if (config === void 0) throw new Error("desktop bridge unavailable");
668
- return await requestArkmeDesktopBridge(config, this.fetchImpl, action, payload);
669
- }
670
- };
671
- //#endregion
672
740
  //#region src/account-session-owner.ts
673
741
  var ArkmeAccountSessionOwner = class {
674
742
  store;
@@ -911,6 +979,13 @@ var ServiceRuntime = class {
911
979
  stateStore;
912
980
  fetchImpl;
913
981
  pendingSessionStore;
982
+ memberCacheRevision = 0;
983
+ memberCacheEpoch() {
984
+ return this.memberCacheRevision;
985
+ }
986
+ invalidateMemberCache() {
987
+ this.memberCacheRevision += 1;
988
+ }
914
989
  requestCoordinator = new ArkmeRequestCoordinator();
915
990
  refreshInFlightByUserId = /* @__PURE__ */ new Map();
916
991
  accountSessions;
@@ -1435,6 +1510,666 @@ var ServiceRuntime = class {
1435
1510
  }
1436
1511
  };
1437
1512
  //#endregion
1513
+ //#region src/services/conversation-directory-service.ts
1514
+ const PAGE_SIZE = 20;
1515
+ const MAX_ROWS = 2e4;
1516
+ const keyOf = (source) => source.sourceKey ?? source.sourceRef;
1517
+ /** Merge pages and live changes without interpreting page absence as removal. */
1518
+ function mergeDirectorySource(previous, incoming, keepLive = false) {
1519
+ if (previous === void 0) return incoming;
1520
+ const stale = (incoming.latestSequence ?? 0) < (previous.latestSequence ?? 0) || keepLive && (incoming.latestSequence ?? 0) <= (previous.latestSequence ?? 0) && incoming.activeAtMillis <= previous.activeAtMillis;
1521
+ const merged = {
1522
+ ...previous,
1523
+ ...incoming
1524
+ };
1525
+ if (stale) {
1526
+ merged.sourceRef = previous.sourceRef;
1527
+ for (const field of [
1528
+ "latestSequence",
1529
+ "latestPreview",
1530
+ "activeAtMillis",
1531
+ "unreadCount",
1532
+ "badgeUnreadCount",
1533
+ "hasUnreadMention"
1534
+ ]) if (previous[field] !== void 0) Object.assign(merged, { [field]: previous[field] });
1535
+ }
1536
+ if (!stale && incoming.latestPreview === void 0) delete merged.latestPreview;
1537
+ const readSequence = Math.max(previous.readSequence ?? 0, incoming.readSequence ?? 0);
1538
+ if (readSequence > 0) {
1539
+ merged.readSequence = readSequence;
1540
+ if ((merged.latestSequence ?? 0) <= readSequence) merged.unreadCount = 0;
1541
+ else if ((incoming.readSequence ?? 0) < (previous.readSequence ?? 0) && (incoming.latestSequence ?? 0) <= (previous.latestSequence ?? 0)) merged.unreadCount = previous.unreadCount;
1542
+ }
1543
+ Object.assign(merged, projectArkmeChatAttentionFromMuted(merged.unreadCount, merged.isMuted === true));
1544
+ if (merged.unreadCount === 0) merged.hasUnreadMention = false;
1545
+ if (!incoming.avatarRef && previous.avatarRef !== void 0) merged.avatarRef = previous.avatarRef;
1546
+ if (!incoming.avatarRefs?.length && previous.avatarRefs !== void 0) merged.avatarRefs = previous.avatarRefs;
1547
+ if (!incoming.groupAvatar?.slots.length && previous.groupAvatar !== void 0) merged.groupAvatar = previous.groupAvatar;
1548
+ return retainNewerArkmeChatPin(previous, merged);
1549
+ }
1550
+ /** One account lifecycle owns cache restoration, a twenty-row scan, and directory deltas. */
1551
+ var ConversationDirectoryService = class {
1552
+ runtime;
1553
+ source;
1554
+ preferences;
1555
+ readBots;
1556
+ warmAvatar;
1557
+ emit;
1558
+ restoreBots;
1559
+ userId;
1560
+ generation = 0;
1561
+ revision = 0;
1562
+ cachedAtMillis = 0;
1563
+ sourceRemovals = /* @__PURE__ */ new Map();
1564
+ pendingReadAcks = /* @__PURE__ */ new Map();
1565
+ visibilityMutations = /* @__PURE__ */ new Map();
1566
+ sources = /* @__PURE__ */ new Map();
1567
+ mutations = /* @__PURE__ */ new Map();
1568
+ visibility = /* @__PURE__ */ new Map();
1569
+ bots = [];
1570
+ cachedBotKeys = /* @__PURE__ */ new Map();
1571
+ botPinnedKeys = /* @__PURE__ */ new Set();
1572
+ deletedBotRefs = /* @__PURE__ */ new Set();
1573
+ special = {};
1574
+ phase = "cached";
1575
+ error;
1576
+ restore;
1577
+ scan;
1578
+ firstPage;
1579
+ rescanRequested = false;
1580
+ rawBaselineComplete = false;
1581
+ rawBaseline;
1582
+ controller = new AbortController();
1583
+ persistence = Promise.resolve();
1584
+ diskPending = /* @__PURE__ */ new Map();
1585
+ diskWriting = false;
1586
+ cacheFailure;
1587
+ lastPublished = "";
1588
+ avatars = /* @__PURE__ */ new Map();
1589
+ avatarWork;
1590
+ constructor(runtime, source, preferences, readBots, warmAvatar, emit, restoreBots = async (items) => items) {
1591
+ this.runtime = runtime;
1592
+ this.source = source;
1593
+ this.preferences = preferences;
1594
+ this.readBots = readBots;
1595
+ this.warmAvatar = warmAvatar;
1596
+ this.emit = emit;
1597
+ this.restoreBots = restoreBots;
1598
+ }
1599
+ reset() {
1600
+ this.controller.abort();
1601
+ this.controller = new AbortController();
1602
+ this.generation++;
1603
+ this.userId = void 0;
1604
+ this.sourceRemovals.clear();
1605
+ this.pendingReadAcks.clear();
1606
+ this.sources.clear();
1607
+ this.mutations.clear();
1608
+ this.visibility.clear();
1609
+ this.visibilityMutations.clear();
1610
+ this.avatars.clear();
1611
+ this.bots = [];
1612
+ this.cachedBotKeys.clear();
1613
+ this.botPinnedKeys.clear();
1614
+ this.deletedBotRefs.clear();
1615
+ this.special = {};
1616
+ this.restore = void 0;
1617
+ this.scan = void 0;
1618
+ this.firstPage = void 0;
1619
+ this.rawBaseline = void 0;
1620
+ this.avatarWork = void 0;
1621
+ this.phase = "cached";
1622
+ this.error = void 0;
1623
+ this.lastPublished = "";
1624
+ this.cachedAtMillis = 0;
1625
+ this.rescanRequested = false;
1626
+ this.rawBaselineComplete = false;
1627
+ }
1628
+ async activate() {
1629
+ const { userId } = await this.runtime.requireSession();
1630
+ if (userId !== this.userId) {
1631
+ this.reset();
1632
+ this.userId = userId;
1633
+ const generation = this.generation;
1634
+ this.restore = (async () => {
1635
+ const cached = await this.runtime.stateStore.readDirectoryCache?.(userId);
1636
+ if (generation !== this.generation || cached === void 0) return;
1637
+ if (cached.items[0] !== void 0) await this.source.openSourceRef(cached.items[0].sourceRef, userId);
1638
+ if (generation !== this.generation) return;
1639
+ for (const item of cached.items) this.sources.set(keyOf(item), item);
1640
+ this.cachedAtMillis = cached.projection?.cachedAtMillis ?? 0;
1641
+ for (const item of cached.projection?.visibility ?? []) this.visibility.set(`${item.entryKind}:${item.entryRef}`, item);
1642
+ const cachedBots = cached.projection?.bots ?? [];
1643
+ const bots = await this.restoreBots(cachedBots, userId);
1644
+ if (generation !== this.generation) return;
1645
+ this.bots = bots;
1646
+ for (const bot of bots) {
1647
+ if (bot.directoryKey !== void 0) this.cachedBotKeys.set(bot.botRef, bot.directoryKey);
1648
+ const previous = cachedBots.find((item) => item.directoryKey === bot.directoryKey);
1649
+ if (previous === void 0 || previous.botRef === bot.botRef) continue;
1650
+ const visibility = this.visibility.get(`bot:${previous.botRef}`);
1651
+ this.visibility.delete(`bot:${previous.botRef}`);
1652
+ if (visibility !== void 0) this.visibility.set(`bot:${bot.botRef}`, {
1653
+ ...visibility,
1654
+ entryRef: bot.botRef
1655
+ });
1656
+ }
1657
+ this.botPinnedKeys = new Set(cached.projection?.botPinnedKeys ?? []);
1658
+ this.deletedBotRefs = new Set(cached.projection?.removedBotRefs ?? []);
1659
+ this.special = {
1660
+ ...cached.projection?.sendToSelf === void 0 ? {} : { sendToSelf: cached.projection.sendToSelf },
1661
+ ...cached.projection?.arkoProfile === void 0 ? {} : { arkoProfile: cached.projection.arkoProfile },
1662
+ ...cached.projection?.arkoPreview === void 0 ? {} : { arkoPreview: cached.projection.arkoPreview }
1663
+ };
1664
+ this.revision = Math.max(this.revision, cached.projection?.revision ?? 0);
1665
+ for (const key of cached.projection?.removedSourceKeys ?? []) this.sourceRemovals.set(key, this.revision);
1666
+ })().catch(() => {});
1667
+ }
1668
+ await this.restore;
1669
+ }
1670
+ async read(force = false, signal) {
1671
+ signal?.throwIfAborted();
1672
+ const pending = this.readSnapshot(force);
1673
+ if (signal === void 0) return await pending;
1674
+ return await new Promise((resolve, reject) => {
1675
+ const abort = () => {
1676
+ reject(signal.reason);
1677
+ };
1678
+ signal.addEventListener("abort", abort, { once: true });
1679
+ pending.then(resolve, reject).finally(() => {
1680
+ signal.removeEventListener("abort", abort);
1681
+ });
1682
+ });
1683
+ }
1684
+ async readSnapshot(force) {
1685
+ await this.activate();
1686
+ const generation = this.generation;
1687
+ const cached = this.snapshot();
1688
+ const hasCache = cached.items.length > 0 || cached.projection.bots.length > 0;
1689
+ if (this.scan === void 0 && (force || this.phase === "cached" || this.phase === "failed")) this.startScan();
1690
+ if (force && this.scan !== void 0 && cached.projection?.phase === "syncing") this.rescanRequested = true;
1691
+ if (hasCache) return structuredClone(cached);
1692
+ await this.firstPage;
1693
+ if (generation !== this.generation) throw new ArkmePluginError("login-context-changed", "账号已切换", false, 409);
1694
+ return structuredClone(this.snapshot());
1695
+ }
1696
+ /** Notification callers join the same scan, but only accept a complete current connection baseline. */
1697
+ async complete() {
1698
+ await this.activate();
1699
+ if (this.rawBaselineComplete && this.scan !== void 0) await this.scan.catch(() => void 0);
1700
+ await this.activate();
1701
+ if (this.scan === void 0) this.startScan();
1702
+ return structuredClone(await this.rawBaseline);
1703
+ }
1704
+ async settled() {
1705
+ while (this.scan !== void 0) await this.scan;
1706
+ while (this.avatarWork !== void 0) await this.avatarWork;
1707
+ while (this.diskWriting) await this.persistence;
1708
+ }
1709
+ startScan() {
1710
+ const generation = this.generation;
1711
+ const userId = this.userId;
1712
+ const signal = this.controller.signal;
1713
+ this.phase = "loading";
1714
+ this.error = void 0;
1715
+ this.rawBaselineComplete = false;
1716
+ let ready;
1717
+ let failed;
1718
+ this.firstPage = new Promise((resolve, reject) => {
1719
+ ready = resolve;
1720
+ failed = reject;
1721
+ });
1722
+ let baselineReady;
1723
+ let baselineFailed;
1724
+ this.rawBaseline = new Promise((resolve, reject) => {
1725
+ baselineReady = resolve;
1726
+ baselineFailed = reject;
1727
+ });
1728
+ this.rawBaseline.catch(() => void 0);
1729
+ const rawSources = /* @__PURE__ */ new Map();
1730
+ this.firstPage.catch(() => void 0);
1731
+ const pending = (async () => {
1732
+ let cursor;
1733
+ const visited = /* @__PURE__ */ new Set();
1734
+ let first = true;
1735
+ do {
1736
+ signal.throwIfAborted();
1737
+ if (visited.size >= MAX_ROWS / PAGE_SIZE) throw new Error("Directory page budget exceeded; synchronization incomplete");
1738
+ const atRevision = this.revision;
1739
+ const page = await this.source.listSources("root", {
1740
+ limit: PAGE_SIZE,
1741
+ refresh: true,
1742
+ firstPaint: true,
1743
+ ...cursor === void 0 ? {} : { cursor },
1744
+ signal
1745
+ });
1746
+ if (generation !== this.generation || (await this.runtime.requireSession()).userId !== userId) throw new DOMException("Account changed", "AbortError");
1747
+ if (page.hasMore && (page.nextCursor === void 0 || visited.has(page.nextCursor))) throw new ArkmePluginError("directory-cursor-invalid", "会话目录分页未完成:游标无效", true, 502);
1748
+ for (const item of page.items) rawSources.set(keyOf(item), item);
1749
+ if (!page.hasMore) {
1750
+ this.rawBaselineComplete = true;
1751
+ baselineReady({
1752
+ directory: "root",
1753
+ items: [...rawSources.values()],
1754
+ hasMore: false
1755
+ });
1756
+ }
1757
+ const visibleCandidates = page.items.map((item) => mergeDirectorySource(this.sources.get(keyOf(item)), item, (this.mutations.get(keyOf(item)) ?? 0) > atRevision));
1758
+ const visibility = await this.preferences.query(visibleCandidates.map((item) => item.sourceRef), [], signal).catch((error) => {
1759
+ if (signal.aborted || error instanceof ArkmePluginError && !error.retryable && [
1760
+ 401,
1761
+ 403,
1762
+ 409
1763
+ ].includes(error.httpStatus)) throw error;
1764
+ return { items: visibleCandidates.map((item) => ({
1765
+ entryKind: "source",
1766
+ entryRef: item.sourceRef,
1767
+ hidden: this.visibility.get(`source:${item.sourceRef}`)?.hidden ?? false
1768
+ })) };
1769
+ });
1770
+ signal.throwIfAborted();
1771
+ this.phase = "syncing";
1772
+ this.apply(page.items, visibility.items, atRevision);
1773
+ if (first) {
1774
+ first = false;
1775
+ ready();
1776
+ }
1777
+ for (const item of page.items) this.avatars.set(keyOf(item), item);
1778
+ this.startAvatars(generation);
1779
+ if (!page.hasMore) break;
1780
+ cursor = page.nextCursor;
1781
+ visited.add(cursor);
1782
+ await new Promise((resolve) => {
1783
+ setTimeout(resolve, 0);
1784
+ });
1785
+ } while (true);
1786
+ const bots = await this.readBots(signal);
1787
+ if (generation !== this.generation) return;
1788
+ await this.rememberBots(bots.items, userId);
1789
+ if (generation !== this.generation) return;
1790
+ this.phase = "complete";
1791
+ this.apply([]);
1792
+ const refs = new Set(this.bots.flatMap((bot) => bot.avatarRef ? [bot.avatarRef] : []));
1793
+ const previousAvatarWork = this.avatarWork;
1794
+ const avatarWork = (async () => {
1795
+ await previousAvatarWork;
1796
+ for (const ref of refs) {
1797
+ signal.throwIfAborted();
1798
+ await this.warmAvatar(ref, signal).catch(() => void 0);
1799
+ }
1800
+ })().catch(() => void 0).finally(() => {
1801
+ if (this.avatarWork !== avatarWork) return;
1802
+ this.avatarWork = void 0;
1803
+ if (generation === this.generation && this.avatars.size > 0) this.startAvatars(generation);
1804
+ });
1805
+ this.avatarWork = avatarWork;
1806
+ })().catch((error) => {
1807
+ if (generation === this.generation) {
1808
+ this.phase = "failed";
1809
+ this.error = error instanceof Error ? error.message : "目录同步失败";
1810
+ this.publish([]);
1811
+ }
1812
+ failed(error);
1813
+ baselineFailed(error);
1814
+ throw error;
1815
+ }).finally(() => {
1816
+ if (this.scan !== pending) return;
1817
+ this.scan = void 0;
1818
+ if (this.rescanRequested) {
1819
+ this.rescanRequested = false;
1820
+ this.startScan();
1821
+ }
1822
+ });
1823
+ this.scan = pending;
1824
+ pending.catch(() => void 0);
1825
+ }
1826
+ apply(items, visibility = [], atRevision = this.revision, authoritativeAvatars = false) {
1827
+ const changed = [];
1828
+ for (const item of items) {
1829
+ const key = keyOf(item);
1830
+ if ((this.sourceRemovals.get(key) ?? 0) > atRevision) continue;
1831
+ this.sourceRemovals.delete(key);
1832
+ const previous = this.sources.get(key);
1833
+ const ack = this.pendingReadAcks.get(key) ?? this.pendingReadAcks.get(item.sourceRef);
1834
+ const merged = mergeDirectorySource(previous, ack === void 0 ? item : {
1835
+ ...item,
1836
+ readSequence: Math.max(item.readSequence ?? 0, ack.effectiveReadSequence),
1837
+ ...(item.latestSequence ?? 0) <= ack.effectiveReadSequence ? { unreadCount: ack.unreadCount } : {}
1838
+ }, (this.mutations.get(key) ?? 0) > atRevision);
1839
+ if (authoritativeAvatars) {
1840
+ merged.avatarRef = item.avatarRef ?? "";
1841
+ merged.avatarRefs = item.avatarRefs ?? [];
1842
+ if (item.groupAvatar === void 0) delete merged.groupAvatar;
1843
+ else merged.groupAvatar = item.groupAvatar;
1844
+ }
1845
+ this.pendingReadAcks.delete(key);
1846
+ this.pendingReadAcks.delete(item.sourceRef);
1847
+ if (JSON.stringify(previous) === JSON.stringify(merged)) continue;
1848
+ if (previous === void 0 && this.sources.size >= MAX_ROWS) throw new Error("Conversation directory capacity exceeded; scan incomplete");
1849
+ if (previous !== void 0 && previous.sourceRef !== merged.sourceRef) {
1850
+ this.visibility.delete(`source:${previous.sourceRef}`);
1851
+ this.visibilityMutations.delete(`source:${previous.sourceRef}`);
1852
+ }
1853
+ this.sources.set(key, merged);
1854
+ this.mutations.set(key, this.revision + 1);
1855
+ changed.push(merged);
1856
+ }
1857
+ const currentRefs = new Set(items.flatMap((item) => {
1858
+ const source = this.sources.get(keyOf(item));
1859
+ return source === void 0 ? [] : [source.sourceRef];
1860
+ }));
1861
+ const acceptedVisibility = visibility.filter((entry) => (items.length === 0 || entry.entryKind !== "source" || currentRefs.has(entry.entryRef)) && (this.visibilityMutations.get(`${entry.entryKind}:${entry.entryRef}`) ?? 0) <= atRevision);
1862
+ for (const entry of acceptedVisibility) {
1863
+ const key = `${entry.entryKind}:${entry.entryRef}`;
1864
+ this.visibility.set(key, entry);
1865
+ this.visibilityMutations.set(key, this.revision + 1);
1866
+ }
1867
+ this.publish(changed, acceptedVisibility);
1868
+ }
1869
+ snapshot(items = [...this.sources.values()], visibility = [...this.visibility.values()]) {
1870
+ return {
1871
+ directory: "root",
1872
+ items,
1873
+ hasMore: this.phase !== "complete",
1874
+ projection: {
1875
+ ...this.special,
1876
+ removedSourceKeys: [...this.sourceRemovals.keys()],
1877
+ removedBotRefs: [...this.deletedBotRefs],
1878
+ botPinnedKeys: [...this.botPinnedKeys],
1879
+ revision: this.revision,
1880
+ phase: this.phase,
1881
+ cachedAtMillis: this.cachedAtMillis,
1882
+ visibility,
1883
+ bots: this.bots,
1884
+ ...this.error === void 0 ? {} : { error: this.error }
1885
+ }
1886
+ };
1887
+ }
1888
+ publish(items, visibility = []) {
1889
+ const fingerprint = JSON.stringify({
1890
+ items,
1891
+ visibility,
1892
+ phase: this.phase,
1893
+ error: this.error,
1894
+ bots: this.bots,
1895
+ special: this.special,
1896
+ pins: [...this.botPinnedKeys],
1897
+ removedBots: [...this.deletedBotRefs],
1898
+ removedSources: [...this.sourceRemovals.keys()]
1899
+ });
1900
+ if (fingerprint === this.lastPublished) return this.cacheFailure === void 0;
1901
+ this.lastPublished = fingerprint;
1902
+ this.revision++;
1903
+ this.cachedAtMillis = Date.now();
1904
+ const page = this.snapshot(items, visibility);
1905
+ const userId = this.userId;
1906
+ this.emit(structuredClone(page));
1907
+ if (!this.diskPending.has(userId) && this.diskPending.size >= 8) {
1908
+ this.cacheFailure = /* @__PURE__ */ new Error("Directory cache pending account capacity exceeded");
1909
+ console.warn("dsh-arkme: directory_cache_queue_full");
1910
+ return false;
1911
+ }
1912
+ const queued = this.diskPending.get(userId);
1913
+ const rows = new Map((queued?.items ?? []).map((item) => [keyOf(item), item]));
1914
+ for (const item of page.items) rows.set(keyOf(item), item);
1915
+ const hidden = new Map((queued?.projection?.visibility ?? []).map((item) => [`${item.entryKind}:${item.entryRef}`, item]));
1916
+ for (const item of page.projection.visibility) hidden.set(`${item.entryKind}:${item.entryRef}`, item);
1917
+ this.diskPending.set(userId, {
1918
+ ...page,
1919
+ items: [...rows.values()],
1920
+ projection: {
1921
+ ...page.projection,
1922
+ visibility: [...hidden.values()]
1923
+ }
1924
+ });
1925
+ this.flushDisk();
1926
+ return true;
1927
+ }
1928
+ flushDisk() {
1929
+ if (this.diskWriting || this.diskPending.size === 0) return;
1930
+ this.diskWriting = true;
1931
+ this.persistence = (async () => {
1932
+ while (this.diskPending.size > 0) {
1933
+ await new Promise((resolve) => {
1934
+ setTimeout(resolve, 0);
1935
+ });
1936
+ const [userId, page] = this.diskPending.entries().next().value;
1937
+ this.diskPending.delete(userId);
1938
+ try {
1939
+ await this.runtime.stateStore.writeDirectoryCache?.(userId, page);
1940
+ this.cacheFailure = void 0;
1941
+ } catch (error) {
1942
+ this.cacheFailure = error;
1943
+ console.warn("dsh-arkme: directory_cache_write_failed", error instanceof Error ? error.message : "cache failed");
1944
+ }
1945
+ }
1946
+ })().finally(() => {
1947
+ this.diskWriting = false;
1948
+ this.flushDisk();
1949
+ });
1950
+ }
1951
+ startAvatars(generation) {
1952
+ if (this.avatarWork !== void 0) return;
1953
+ const signal = this.controller.signal;
1954
+ const pending = (async () => {
1955
+ while (this.avatars.size > 0 && generation === this.generation) {
1956
+ const items = [...this.avatars.values()].slice(0, PAGE_SIZE);
1957
+ for (const item of items) this.avatars.delete(keyOf(item));
1958
+ const hydrated = await this.source.hydrateDirectoryPage(items.map((item) => this.sources.get(keyOf(item)) ?? item), signal);
1959
+ if (generation !== this.generation) return;
1960
+ this.apply(hydrated.filter((item) => this.sources.has(keyOf(item))).map((item) => {
1961
+ const updated = {
1962
+ ...this.sources.get(keyOf(item)),
1963
+ avatarRef: item.avatarRef ?? "",
1964
+ avatarRefs: item.avatarRefs ?? []
1965
+ };
1966
+ if (item.groupAvatar === void 0) delete updated.groupAvatar;
1967
+ else updated.groupAvatar = item.groupAvatar;
1968
+ return updated;
1969
+ }), [], this.revision, true);
1970
+ const refs = new Set(hydrated.flatMap((item) => [item.avatarRef, ...item.avatarRefs ?? []]).filter((ref) => ref !== void 0 && ref.trim() !== ""));
1971
+ for (const ref of refs) {
1972
+ signal.throwIfAborted();
1973
+ await this.warmAvatar(ref, signal).catch(() => void 0);
1974
+ }
1975
+ if (refs.size > 0 && generation === this.generation) {
1976
+ this.revision++;
1977
+ const page = this.snapshot([], []);
1978
+ this.emit({
1979
+ ...page,
1980
+ projection: {
1981
+ ...page.projection,
1982
+ avatarRefs: [...refs]
1983
+ }
1984
+ });
1985
+ }
1986
+ }
1987
+ })().catch(() => {}).finally(() => {
1988
+ if (this.avatarWork === pending) this.avatarWork = void 0;
1989
+ });
1990
+ this.avatarWork = pending;
1991
+ }
1992
+ async rememberBots(items, expectedUserId) {
1993
+ if (this.userId !== expectedUserId) return;
1994
+ await this.activate();
1995
+ if (this.userId !== expectedUserId) return;
1996
+ const generation = this.generation;
1997
+ const atRevision = this.revision;
1998
+ const visible = await this.preferences.query([], items.map((bot) => bot.botRef), this.controller.signal);
1999
+ if (generation !== this.generation) return;
2000
+ const merged = new Map(this.bots.map((bot) => [bot.directoryKey ?? bot.botRef, bot]));
2001
+ for (const bot of items) {
2002
+ if (this.deletedBotRefs.has(bot.botRef)) continue;
2003
+ const key = bot.directoryKey ?? bot.botRef;
2004
+ const previous = merged.get(key);
2005
+ const next = {
2006
+ ...previous,
2007
+ ...bot
2008
+ };
2009
+ if ((previous?.latestMessageAtMillis ?? 0) > (bot.latestMessageAtMillis ?? 0)) {
2010
+ next.latestMessageAtMillis = previous.latestMessageAtMillis;
2011
+ if (previous.latestMessagePreview !== void 0) next.latestMessagePreview = previous.latestMessagePreview;
2012
+ else delete next.latestMessagePreview;
2013
+ } else if (bot.latestMessageAtMillis !== void 0 && bot.latestMessagePreview === void 0) delete next.latestMessagePreview;
2014
+ next.conversationListActivityAtMillis = Math.max(previous?.conversationListActivityAtMillis ?? 0, bot.conversationListActivityAtMillis ?? 0);
2015
+ merged.set(key, next);
2016
+ }
2017
+ for (const bot of this.bots) if (bot.directoryKey !== void 0) this.cachedBotKeys.set(bot.botRef, bot.directoryKey);
2018
+ while (this.cachedBotKeys.size > MAX_ROWS) this.cachedBotKeys.delete(this.cachedBotKeys.keys().next().value);
2019
+ this.bots = [...merged.values()];
2020
+ const currentRefs = new Set(this.bots.map((bot) => bot.botRef));
2021
+ for (const [key, entry] of this.visibility) if (entry.entryKind === "bot" && !currentRefs.has(entry.entryRef)) {
2022
+ this.visibility.delete(key);
2023
+ this.visibilityMutations.delete(key);
2024
+ }
2025
+ this.apply([], visible.items.filter((item) => !this.deletedBotRefs.has(item.entryRef)), atRevision);
2026
+ }
2027
+ async forgetSource(sourceRef, expectedUserId) {
2028
+ await this.activate();
2029
+ if (this.userId !== expectedUserId) return;
2030
+ const generation = this.generation;
2031
+ const source = [...this.sources.values()].find((item) => item.sourceRef === sourceRef);
2032
+ const key = source === void 0 ? await this.source.chatDirectorySourceKey(expectedUserId, (await this.source.openSourceRef(sourceRef, expectedUserId)).ownerRef) : keyOf(source);
2033
+ if (generation !== this.generation) return;
2034
+ this.sources.delete(key);
2035
+ this.mutations.delete(key);
2036
+ this.avatars.delete(key);
2037
+ this.visibility.delete(`source:${sourceRef}`);
2038
+ this.visibilityMutations.delete(`source:${sourceRef}`);
2039
+ this.sourceRemovals.set(key, this.revision + 1);
2040
+ if (this.sourceRemovals.size > MAX_ROWS) this.sourceRemovals.delete(this.sourceRemovals.keys().next().value);
2041
+ this.publish([]);
2042
+ }
2043
+ async forgetBot(botRef, expectedUserId) {
2044
+ await this.activate();
2045
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2046
+ this.deletedBotRefs.add(botRef);
2047
+ const deleted = this.bots.find((bot) => bot.botRef === botRef || bot.directoryKey !== void 0 && bot.directoryKey === this.cachedBotKeys.get(botRef));
2048
+ if (deleted !== void 0) this.deletedBotRefs.add(deleted.botRef);
2049
+ while (this.deletedBotRefs.size > MAX_ROWS) this.deletedBotRefs.delete(this.deletedBotRefs.values().next().value);
2050
+ if (deleted !== void 0) this.botPinnedKeys.delete(deleted.directoryKey ?? deleted.botRef);
2051
+ this.visibility.delete(`bot:${botRef}`);
2052
+ this.visibilityMutations.delete(`bot:${botRef}`);
2053
+ this.bots = this.bots.filter((bot) => bot !== deleted);
2054
+ this.publish([]);
2055
+ }
2056
+ async pinBot(botRef, pinned) {
2057
+ await this.activate();
2058
+ const generation = this.generation;
2059
+ let bot = this.bots.find((item) => item.botRef === botRef || item.directoryKey !== void 0 && item.directoryKey === this.cachedBotKeys.get(botRef));
2060
+ if (bot === void 0) {
2061
+ const fresh = await this.readBots(this.controller.signal);
2062
+ if (generation !== this.generation) throw new ArkmePluginError("login-context-changed", "账号已切换", false, 409);
2063
+ bot = fresh.items.find((item) => item.botRef === botRef && !this.deletedBotRefs.has(item.botRef));
2064
+ if (bot !== void 0) this.bots.push(bot);
2065
+ }
2066
+ if (bot === void 0) throw new ArkmePluginError("bot-directory-entry-unavailable", "请先加载当前账号的 Bot 目录", false, 404);
2067
+ const key = bot.directoryKey ?? bot.botRef;
2068
+ const previous = this.botPinnedKeys.has(key);
2069
+ if (pinned) this.botPinnedKeys.add(key);
2070
+ else this.botPinnedKeys.delete(key);
2071
+ const admitted = this.publish([]);
2072
+ while (this.diskWriting) await this.persistence;
2073
+ if (generation !== this.generation) throw new ArkmePluginError("login-context-changed", "账号已切换", false, 409);
2074
+ if (!admitted || this.cacheFailure !== void 0) {
2075
+ if (previous) this.botPinnedKeys.add(key);
2076
+ else this.botPinnedKeys.delete(key);
2077
+ this.publish([]);
2078
+ throw new ArkmePluginError("directory-cache-write-failed", "本地置顶状态保存失败,请重试", true, 500);
2079
+ }
2080
+ }
2081
+ async rememberSpecial(patch, expectedUserId) {
2082
+ await this.activate();
2083
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2084
+ if (patch.arkoPreview !== void 0 && this.special.arkoPreview !== void 0 && patch.arkoPreview.createdAtMillis < this.special.arkoPreview.createdAtMillis) delete patch.arkoPreview;
2085
+ const next = {
2086
+ ...this.special,
2087
+ ...patch
2088
+ };
2089
+ if (JSON.stringify(next) === JSON.stringify(this.special)) return;
2090
+ this.special = next;
2091
+ this.publish([]);
2092
+ }
2093
+ async confirmVisibility(entryKind, entryRef, hidden, expectedUserId) {
2094
+ await this.activate();
2095
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2096
+ if (entryKind === "bot") {
2097
+ const current = this.bots.find((bot) => bot.botRef === entryRef || bot.directoryKey !== void 0 && bot.directoryKey === this.cachedBotKeys.get(entryRef));
2098
+ if (current !== void 0) {
2099
+ const generation = this.generation;
2100
+ const atRevision = this.revision;
2101
+ const visible = await this.preferences.query([], [current.botRef], this.controller.signal);
2102
+ if (generation === this.generation) this.apply([], visible.items, atRevision);
2103
+ return;
2104
+ }
2105
+ }
2106
+ this.apply([], [{
2107
+ entryKind,
2108
+ entryRef,
2109
+ hidden
2110
+ }]);
2111
+ }
2112
+ async confirmPin(sourceRef, pinned, policyUpdatedAtMillis, expectedUserId) {
2113
+ await this.activate();
2114
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2115
+ const source = [...this.sources.values()].find((item) => item.sourceRef === sourceRef);
2116
+ if (source !== void 0) this.apply([{
2117
+ ...source,
2118
+ isPinned: pinned,
2119
+ chatPolicyUpdatedAtMillis: policyUpdatedAtMillis
2120
+ }]);
2121
+ }
2122
+ async accept(event) {
2123
+ const generation = this.generation;
2124
+ const atRevision = this.revision;
2125
+ if (this.userId === void 0 || (await this.runtime.accountScopedSession())?.userId !== this.userId || generation !== this.generation) return;
2126
+ if (event.type === "sessions-delta") {
2127
+ if (event.updates.some((item) => this.sourceRemovals.has(keyOf(item.source)))) if (this.scan === void 0) this.startScan();
2128
+ else this.rescanRequested = true;
2129
+ const sources = event.updates.filter((item) => !this.sourceRemovals.has(keyOf(item.source))).map((item) => mergeDirectorySource(this.sources.get(keyOf(item.source)), item.source));
2130
+ const visibility = await this.preferences.query(sources.map((item) => item.sourceRef), [], this.controller.signal);
2131
+ if (generation !== this.generation) return;
2132
+ this.apply(sources, visibility.items, atRevision);
2133
+ } else if (event.type === "read-ack") {
2134
+ const source = [...this.sources.values()].find((item) => item.sourceKey === event.sourceKey || item.sourceRef === event.sourceRef);
2135
+ if (source !== void 0) this.apply([{
2136
+ ...source,
2137
+ readSequence: Math.max(source.readSequence ?? 0, event.effectiveReadSequence),
2138
+ ...(source.latestSequence ?? 0) <= event.effectiveReadSequence ? {
2139
+ unreadCount: event.unreadCount,
2140
+ badgeUnreadCount: source.isMuted ? 0 : event.unreadCount,
2141
+ ...event.unreadCount === 0 ? { hasUnreadMention: false } : {}
2142
+ } : {}
2143
+ }]);
2144
+ else {
2145
+ const key = event.sourceKey ?? event.sourceRef;
2146
+ const previous = this.pendingReadAcks.get(key);
2147
+ if (previous === void 0 || previous.effectiveReadSequence < event.effectiveReadSequence) this.pendingReadAcks.set(key, event);
2148
+ if (this.pendingReadAcks.size > MAX_ROWS) this.pendingReadAcks.delete(this.pendingReadAcks.keys().next().value);
2149
+ }
2150
+ } else if (event.type === "chat-pins-reconciled") this.apply([...this.sources.values()].flatMap((source) => {
2151
+ const pin = event.pins.find((item) => item.sourceKey === source.sourceKey);
2152
+ return pin === void 0 ? [] : [{
2153
+ ...source,
2154
+ isPinned: pin.pinned,
2155
+ chatPolicyUpdatedAtMillis: pin.policyUpdatedAtMillis
2156
+ }];
2157
+ }));
2158
+ else if (event.type === "conversation-list-preference-invalidated" || event.type === "chat-policy-invalidated") if (this.scan === void 0) this.startScan();
2159
+ else this.rescanRequested = true;
2160
+ }
2161
+ };
2162
+ //#endregion
2163
+ //#region src/direct-message-admission.ts
2164
+ function directMessageAdmissionMessage(admission) {
2165
+ switch (admission.state) {
2166
+ case "allowed": return "";
2167
+ case "refused_by_self": return "你已拒收对方的消息";
2168
+ case "refused_by_counterpart": return "对方已拒收你的消息";
2169
+ case "mutually_refused": return "你们已互相拒收消息";
2170
+ }
2171
+ }
2172
+ //#endregion
1438
2173
  //#region src/services/direct-message-admission-service.ts
1439
2174
  function projectDirectMessageAdmission(value, chatSessionUid) {
1440
2175
  const data = objectValue$2(value);
@@ -32377,7 +33112,7 @@ const TOPIC_PRIVACY_PRIVATE = 2;
32377
33112
  const PRIVACY_SNAPSHOT_LIMIT = 100;
32378
33113
  const PRIVACY_SNAPSHOT_MAX_PAGES = 100;
32379
33114
  const PRIVACY_SNAPSHOT_TTL_MS = 3e4;
32380
- function integerLikeValue$5(value) {
33115
+ function integerLikeValue$4(value) {
32381
33116
  if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
32382
33117
  if (typeof value === "string" && value.trim() !== "") {
32383
33118
  const parsed = Number(value.trim());
@@ -32390,13 +33125,13 @@ function arkmePrivacyLockedRecord(raw) {
32390
33125
  const item = objectValue$2(raw);
32391
33126
  const core = objectValue$2(item.record_core);
32392
33127
  const extra = objectValue$2(core.extra ?? item.extra);
32393
- return integerLikeValue$5(core.content_access_state ?? core.contentAccessState ?? item.content_access_state ?? item.contentAccessState ?? extra.content_access_state ?? extra.contentAccessState) === CONTENT_ACCESS_PROTECTED;
33128
+ return integerLikeValue$4(core.content_access_state ?? core.contentAccessState ?? item.content_access_state ?? item.contentAccessState ?? extra.content_access_state ?? extra.contentAccessState) === CONTENT_ACCESS_PROTECTED;
32394
33129
  }
32395
33130
  /** Topic list requests are public-only; retain this guard for stale or malformed responses. */
32396
33131
  function arkmePrivacyLockedTopic(raw) {
32397
33132
  const item = objectValue$2(raw);
32398
33133
  const core = objectValue$2(item.topic_core ?? item);
32399
- return integerLikeValue$5(core.privacy_state ?? core.privacyState) === TOPIC_PRIVACY_PRIVATE;
33134
+ return integerLikeValue$4(core.privacy_state ?? core.privacyState) === TOPIC_PRIVACY_PRIVATE;
32400
33135
  }
32401
33136
  /** Mirrors the mobile visibility snapshot: it returns only privacy metadata, never record content. */
32402
33137
  var ArkmePrivacyVisibilityService = class {
@@ -32436,13 +33171,13 @@ var ArkmePrivacyVisibilityService = class {
32436
33171
  const item = objectValue$2(raw);
32437
33172
  const recordUid = stringValue$2(item.record_uid).trim();
32438
33173
  if (recordUid === "") continue;
32439
- if (integerLikeValue$5(item.lock_state) === 1 || integerLikeValue$5(item.content_access_state) === CONTENT_ACCESS_PROTECTED) locked.add(recordUid);
33174
+ if (integerLikeValue$4(item.lock_state) === 1 || integerLikeValue$4(item.content_access_state) === CONTENT_ACCESS_PROTECTED) locked.add(recordUid);
32440
33175
  }
32441
33176
  if (data.has_more !== true) {
32442
33177
  complete = true;
32443
33178
  break;
32444
33179
  }
32445
- const nextUpdateAt = integerLikeValue$5(data.next_update_at);
33180
+ const nextUpdateAt = integerLikeValue$4(data.next_update_at);
32446
33181
  const nextRecordUid = stringValue$2(data.next_record_uid).trim();
32447
33182
  if (nextUpdateAt <= 0 || nextRecordUid === "") throw new ArkmePluginError("privacy-visibility-snapshot-invalid", "隐私锁快记状态不完整,已停止显示快记", true, 502);
32448
33183
  cursorUpdateAt = nextUpdateAt;
@@ -32650,7 +33385,7 @@ function contractError$1(field) {
32650
33385
  function listValue$24(value) {
32651
33386
  return Array.isArray(value) ? value : [];
32652
33387
  }
32653
- function integerLikeValue$4(value) {
33388
+ function integerLikeValue$3(value) {
32654
33389
  if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
32655
33390
  if (typeof value === "string" && value.trim() !== "") {
32656
33391
  const parsed = Number(value.trim());
@@ -32670,7 +33405,7 @@ function arkmeMentionMetadataMentionsViewer(record, payload, viewerUserId) {
32670
33405
  const metadata = arkmeMentionMetadataFromRecord(record, payload);
32671
33406
  for (const rawMention of listValue$24(metadata.human_mentions ?? metadata.humanMentions)) {
32672
33407
  const mention = objectValue$2(rawMention);
32673
- const mentionedUserId = integerLikeValue$4(mention.user_id ?? mention.userId);
33408
+ const mentionedUserId = integerLikeValue$3(mention.user_id ?? mention.userId);
32674
33409
  if (mentionedUserId === 0 || mentionedUserId === viewerUserId) return true;
32675
33410
  }
32676
33411
  return false;
@@ -32928,47 +33663,6 @@ function arkmePickedFileKind(mimeType, fileName = "") {
32928
33663
  return mediaKind === "image" ? 1 : mediaKind === "video" ? 3 : 4;
32929
33664
  }
32930
33665
  //#endregion
32931
- //#region src/chat-attention.ts
32932
- function integerLikeValue$3(value) {
32933
- if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
32934
- if (typeof value !== "string" || value.trim() === "") return void 0;
32935
- const parsed = Number(value.trim());
32936
- return Number.isFinite(parsed) ? Math.trunc(parsed) : void 0;
32937
- }
32938
- function arkmeChatPolicyNotificationDisabled(policy) {
32939
- if (policy === null || typeof policy !== "object" || Array.isArray(policy)) return void 0;
32940
- const value = policy;
32941
- const muteState = integerLikeValue$3(value.mute_state ?? value.muteState);
32942
- const notifyState = integerLikeValue$3(value.notify_state ?? value.notifyState);
32943
- if (muteState === 2 || notifyState === 2) return true;
32944
- if (muteState === 1 || notifyState === 1) return false;
32945
- }
32946
- function projectArkmeChatAttention(rawUnreadCount, policy, fallbackMuted = false) {
32947
- const unreadCount = Math.max(0, integerLikeValue$3(rawUnreadCount) ?? 0);
32948
- const isMuted = arkmeChatPolicyNotificationDisabled(policy) ?? fallbackMuted;
32949
- const notificationAllowed = !isMuted;
32950
- return {
32951
- unreadCount,
32952
- badgeUnreadCount: notificationAllowed ? unreadCount : 0,
32953
- notificationAllowed,
32954
- isMuted
32955
- };
32956
- }
32957
- function projectArkmeChatAttentionFromMuted(rawUnreadCount, isMuted) {
32958
- return projectArkmeChatAttention(rawUnreadCount, void 0, isMuted);
32959
- }
32960
- //#endregion
32961
- //#region src/chat-pin-projection.ts
32962
- /** Policy freshness is independent of message sequence and event delivery order. */
32963
- function retainNewerArkmeChatPin(current, incoming) {
32964
- if (incoming.kind !== "private_chat" && incoming.kind !== "group_chat" || current?.kind !== incoming.kind || current.isPinned === void 0 || current.chatPolicyUpdatedAtMillis === void 0 || current.chatPolicyUpdatedAtMillis <= (incoming.chatPolicyUpdatedAtMillis ?? 0)) return incoming;
32965
- return {
32966
- ...incoming,
32967
- isPinned: current.isPinned,
32968
- chatPolicyUpdatedAtMillis: current.chatPolicyUpdatedAtMillis
32969
- };
32970
- }
32971
- //#endregion
32972
33666
  //#region src/arkme-emoji-text.ts
32973
33667
  /** Asset-free emoji catalog shared by Browser UI and Host notification text. */
32974
33668
  const arkmeDefaultEmojiSeeds = Object.freeze([
@@ -34144,7 +34838,7 @@ var SourceService = class {
34144
34838
  const session = await this.runtime.requireSession();
34145
34839
  const limit = Math.min(directory === "send_to_self" ? 100 : 50, Math.max(1, Math.trunc(options.limit ?? 30)));
34146
34840
  const cursor = options.cursor?.trim() ?? "";
34147
- const cacheKey = `${String(session.userId)}:${directory}:${String(limit)}:${cursor}`;
34841
+ const cacheKey = `${String(session.userId)}:${directory}:${String(limit)}:${cursor}:${options.firstPaint === true ? "first" : "full"}`;
34148
34842
  this.pruneSourceListCache();
34149
34843
  const cached = this.sourceListCache.get(cacheKey);
34150
34844
  if (options.refresh !== true && cached !== void 0 && cached.expiresAtMillis > Date.now()) return cloneSourceList(cached.value);
@@ -34457,6 +35151,7 @@ var SourceService = class {
34457
35151
  activeAtMillis: arkmeChatSortActiveAt(bundle, chatSession),
34458
35152
  ...attention,
34459
35153
  ...hasUnreadMention === void 0 ? {} : { hasUnreadMention },
35154
+ readSequence: numberValue$26(unread.read_seq),
34460
35155
  isPinned,
34461
35156
  chatPolicyUpdatedAtMillis: numberValue$26(currentPolicy.update_at),
34462
35157
  ...numberValue$26(unread.session_last_seq ?? chatSession.last_seq) > 0 ? { latestSequence: numberValue$26(unread.session_last_seq ?? chatSession.last_seq) } : {}
@@ -34472,7 +35167,7 @@ var SourceService = class {
34472
35167
  } else groupSessionUidByIndex.set(itemIndex, uid);
34473
35168
  }
34474
35169
  try {
34475
- await this.hydrateSourceAvatars(items, privateUserIdByIndex, groupSessionUidByIndex, session, options.signal);
35170
+ if (options.firstPaint !== true) await this.hydrateSourceAvatars(items, privateUserIdByIndex, groupSessionUidByIndex, session, options.signal);
34476
35171
  } catch (error) {
34477
35172
  console.warn("dsh-arkme: Chat avatar hydration failed:", safeFailureMessage$6(error));
34478
35173
  }
@@ -34508,6 +35203,19 @@ var SourceService = class {
34508
35203
  this.sourceListCache.delete(oldestKey);
34509
35204
  }
34510
35205
  }
35206
+ async hydrateDirectoryPage(items, signal) {
35207
+ const session = await this.runtime.requireSession();
35208
+ const hydrated = items.map(cloneSourceItem);
35209
+ const privateUsers = /* @__PURE__ */ new Map();
35210
+ const groups = /* @__PURE__ */ new Map();
35211
+ for (let index = 0; index < items.length; index++) {
35212
+ const item = items[index];
35213
+ if (item.kind === "private_chat" && item.peerUserId !== void 0) privateUsers.set(index, item.peerUserId);
35214
+ if (item.kind === "group_chat") groups.set(index, (await this.openSourceRef(item.sourceRef, session.userId)).ownerRef);
35215
+ }
35216
+ await this.hydrateSourceAvatars(hydrated, privateUsers, groups, session, signal);
35217
+ return hydrated;
35218
+ }
34511
35219
  async hydrateSourceAvatars(items, privateUserIdByIndex, groupSessionUidByIndex, session, signal) {
34512
35220
  const groupSnapshotsByIndex = /* @__PURE__ */ new Map();
34513
35221
  const authoritativeGroupIndices = /* @__PURE__ */ new Set();
@@ -35291,8 +35999,42 @@ var BotService = class {
35291
35999
  async botDirectoryKey(userId, botId) {
35292
36000
  return `arkme-bot-directory-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`arkme-bot-directory-v1:${String(userId)}:${botId}`).digest("base64url")}`;
35293
36001
  }
36002
+ /** A cached directory entry is a lookup, never a persisted live target/capability. */
36003
+ async restoreDirectoryBots(items, userId) {
36004
+ const secret = await this.runtime.stateStore.uniqueCode();
36005
+ return items.flatMap((bot) => {
36006
+ if (bot.directoryKey === void 0) return [];
36007
+ const payload = Buffer.from(JSON.stringify({ directoryKey: bot.directoryKey })).toString("base64url");
36008
+ const signature = createHmac("sha256", secret).update(`bot-directory-entry-v1:${userId}:${payload}`).digest("base64url");
36009
+ return [{
36010
+ ...bot,
36011
+ botRef: `arkme-bot-directory-entry-v1.${payload}.${signature}`
36012
+ }];
36013
+ });
36014
+ }
36015
+ async openDirectoryBotRef(reference, userId) {
36016
+ const parts = reference.split(".");
36017
+ if (parts.length !== 3 || reference.length > 1024) throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36018
+ const payload = parts[1];
36019
+ const supplied = Buffer.from(parts[2], "base64url");
36020
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`bot-directory-entry-v1:${userId}:${payload}`).digest();
36021
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36022
+ let parsed;
36023
+ try {
36024
+ parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
36025
+ } catch {
36026
+ throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36027
+ }
36028
+ if ((await this.runtime.requireSession()).userId !== userId) throw new ArkmePluginError("bot-ref-account-mismatch", "Bot 目录引用与当前账号不匹配", false, 403);
36029
+ if (typeof parsed?.directoryKey !== "string") throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36030
+ const current = (await this.listBots()).items.find((bot) => bot.directoryKey === parsed.directoryKey);
36031
+ if ((await this.runtime.requireSession()).userId !== userId) throw new ArkmePluginError("bot-ref-account-mismatch", "账号已切换", false, 403);
36032
+ if (current === void 0) throw new ArkmePluginError("bot-ref-expired", "Bot 已不可用,请刷新列表", false, 410);
36033
+ return await this.openBotRef(current.botRef, userId);
36034
+ }
35294
36035
  async openBotRef(botRef, expectedUserId) {
35295
36036
  const normalized = botRef.trim();
36037
+ if (normalized.startsWith("arkme-bot-directory-entry-v1.")) return await this.openDirectoryBotRef(normalized, expectedUserId);
35296
36038
  if (!BOT_REF_PATTERN.test(normalized)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
35297
36039
  const entry = this.botRefs.get(normalized);
35298
36040
  if (entry === void 0) throw new ArkmePluginError("bot-ref-expired", "Bot 引用已过期,请刷新 Bot 列表", false, 410);
@@ -36675,6 +37417,29 @@ function decodeDataLine(line) {
36675
37417
  }
36676
37418
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
36677
37419
  }
37420
+ function decodeArkmeMemberJoinedDataLine(line) {
37421
+ const source = decodeDataLine(line);
37422
+ if (source === void 0 || positiveInteger(source.t) !== 24) return void 0;
37423
+ if (Object.keys(source).some((key) => ![
37424
+ "t",
37425
+ "event_uid",
37426
+ "chat_session_uid",
37427
+ "actor_user_id",
37428
+ "member_user_id",
37429
+ "join_at",
37430
+ "event_at",
37431
+ "source_client_id"
37432
+ ].includes(key))) return void 0;
37433
+ const eventUid = nonEmptyString(source.event_uid);
37434
+ const chatSessionUid = nonEmptyString(source.chat_session_uid);
37435
+ const eventAtMillis = positiveInteger(source.event_at);
37436
+ if (eventUid === void 0 || chatSessionUid === void 0 || eventAtMillis === void 0 || positiveInteger(source.actor_user_id) === void 0 || positiveInteger(source.member_user_id) === void 0 || positiveInteger(source.join_at) === void 0 || nonNegativeInteger(source.source_client_id) === void 0) return void 0;
37437
+ return {
37438
+ eventUid,
37439
+ chatSessionUid,
37440
+ eventAtMillis
37441
+ };
37442
+ }
36678
37443
  function decodeArkmeMemberEventDataLine(line) {
36679
37444
  const source = decodeDataLine(line);
36680
37445
  if (source === void 0 || positiveInteger(source.t) !== 27) return void 0;
@@ -37243,6 +38008,7 @@ var ArkmeChatRealtimeRuntime = class {
37243
38008
  if (connectionSignal.aborted) return;
37244
38009
  const policyUpdated = decodeArkmeChatPolicyUpdatedDataLine(line);
37245
38010
  if (policyUpdated !== void 0 && policyUpdated.userId !== connectionUserId) return;
38011
+ const memberJoined = decodeArkmeMemberJoinedDataLine(line);
37246
38012
  const memberEvent = decodeArkmeMemberEventDataLine(line);
37247
38013
  const conversationListPreferenceUpdated = decodeArkmeConversationListPreferenceUpdatedDataLine(line);
37248
38014
  const projectionInvalidation = conversationListPreferenceUpdated === void 0 ? decodeArkmeProjectionInvalidatedDataLine(line) : void 0;
@@ -37250,20 +38016,29 @@ var ArkmeChatRealtimeRuntime = class {
37250
38016
  const timelineChanged = conversationListPreferenceUpdated === void 0 && projectionInvalidation === void 0 && readCursorAdvanced === void 0 ? decodeArkmeChatTimelineChangedDataLine(line) : void 0;
37251
38017
  const messagePreparing = decodeArkmeChatMessagePreparingDataLine(line);
37252
38018
  const hint = conversationListPreferenceUpdated === void 0 && projectionInvalidation === void 0 && readCursorAdvanced === void 0 && timelineChanged === void 0 ? decodeArkmeChatReceiveDataLine(line) : void 0;
37253
- const eventUid = policyUpdated?.eventUid ?? memberEvent?.eventUid ?? conversationListPreferenceUpdated?.eventUid ?? projectionInvalidation?.eventUid ?? readCursorAdvanced?.eventUid ?? timelineChanged?.eventUid ?? messagePreparing?.eventUid ?? hint?.eventUid;
38019
+ const eventUid = policyUpdated?.eventUid ?? memberJoined?.eventUid ?? memberEvent?.eventUid ?? conversationListPreferenceUpdated?.eventUid ?? projectionInvalidation?.eventUid ?? readCursorAdvanced?.eventUid ?? timelineChanged?.eventUid ?? messagePreparing?.eventUid ?? hint?.eventUid;
37254
38020
  if (eventUid === void 0 || this.seenEventUids.has(eventUid)) return;
37255
38021
  this.seenEventUids.add(eventUid);
37256
38022
  if (this.seenEventUids.size > MAX_SEEN_EVENTS) {
37257
38023
  const oldest = this.seenEventUids.values().next().value;
37258
38024
  if (oldest !== void 0) this.seenEventUids.delete(oldest);
37259
38025
  }
37260
- this.lastEventAtMillis = policyUpdated?.eventAtMillis ?? memberEvent?.eventAtMillis ?? conversationListPreferenceUpdated?.acceptedAtMillis ?? projectionInvalidation?.eventAtMillis ?? readCursorAdvanced?.eventAtMillis ?? timelineChanged?.eventAtMillis ?? messagePreparing?.eventAtMillis ?? hint?.eventAtMillis;
38026
+ this.lastEventAtMillis = policyUpdated?.eventAtMillis ?? memberJoined?.eventAtMillis ?? memberEvent?.eventAtMillis ?? conversationListPreferenceUpdated?.acceptedAtMillis ?? projectionInvalidation?.eventAtMillis ?? readCursorAdvanced?.eventAtMillis ?? timelineChanged?.eventAtMillis ?? messagePreparing?.eventAtMillis ?? hint?.eventAtMillis;
37261
38027
  if (policyUpdated !== void 0) this.advanceRevision("chat-policy-invalidation", {
37262
38028
  policyUpdated,
37263
38029
  connectionUserId,
37264
38030
  connectionSignal
37265
38031
  });
37266
- else if (memberEvent !== void 0) this.advanceRevision("chat-hint", { memberEvent });
38032
+ else if (memberJoined !== void 0) this.advanceRevision("chat-hint", {
38033
+ memberJoined,
38034
+ connectionUserId,
38035
+ connectionSignal
38036
+ });
38037
+ else if (memberEvent !== void 0) this.advanceRevision("chat-hint", {
38038
+ memberEvent,
38039
+ connectionUserId,
38040
+ connectionSignal
38041
+ });
37267
38042
  else if (conversationListPreferenceUpdated !== void 0) this.advanceRevision("conversation-list-preference-invalidation", { conversationListPreferenceUpdated });
37268
38043
  else if (projectionInvalidation !== void 0) this.advanceRevision("projection-invalidation", { projectionInvalidation });
37269
38044
  else if (timelineChanged !== void 0) this.advanceRevision("chat-hint", {
@@ -37287,11 +38062,12 @@ var ArkmeChatRealtimeRuntime = class {
37287
38062
  });
37288
38063
  }
37289
38064
  advanceRevision(cause, evidence = {}) {
37290
- const { hint, readCursorAdvanced, policyUpdated, projectionInvalidation, timelineChanged, messagePreparing, connectionUserId, connectionSignal, connectionStartedAtMillis, conversationListPreferenceUpdated, memberEvent } = evidence;
38065
+ const { hint, readCursorAdvanced, policyUpdated, projectionInvalidation, timelineChanged, messagePreparing, connectionUserId, connectionSignal, connectionStartedAtMillis, conversationListPreferenceUpdated, memberEvent, memberJoined } = evidence;
37291
38066
  this.revision += 1;
37292
38067
  const notice = {
37293
38068
  state: this.state(),
37294
38069
  cause,
38070
+ ...memberJoined === void 0 ? {} : { memberJoined },
37295
38071
  ...memberEvent === void 0 ? {} : { memberEvent },
37296
38072
  ...timelineChanged === void 0 ? {} : { timelineChanged },
37297
38073
  ...messagePreparing === void 0 ? {} : { messagePreparing },
@@ -37360,6 +38136,7 @@ var ChatRealtimeService = class {
37360
38136
  source;
37361
38137
  projectionReader;
37362
38138
  nativeAttention;
38139
+ directoryBaseline;
37363
38140
  disposed = false;
37364
38141
  chatRealtime;
37365
38142
  chatClientListeners = /* @__PURE__ */ new Set();
@@ -37480,8 +38257,8 @@ var ChatRealtimeService = class {
37480
38257
  this.invalidateChatPolicyForCurrentSession(notice);
37481
38258
  return;
37482
38259
  }
37483
- if (notice.cause === "chat-hint" && notice.memberEvent !== void 0) {
37484
- this.handleMemberEvent(notice.memberEvent);
38260
+ if (notice.cause === "chat-hint" && (notice.memberEvent !== void 0 || notice.memberJoined !== void 0)) {
38261
+ this.handleMemberEvent(notice);
37485
38262
  return;
37486
38263
  }
37487
38264
  if (notice.cause === "reconcile") {
@@ -37635,13 +38412,21 @@ var ChatRealtimeService = class {
37635
38412
  console.warn("dsh-arkme: Chat timeline invalidation failed:", safeFailureMessage$5(error));
37636
38413
  }
37637
38414
  }
37638
- async handleMemberEvent(hint) {
38415
+ async handleMemberEvent(notice) {
38416
+ const hint = notice.memberJoined ?? notice.memberEvent;
38417
+ if (hint === void 0 || notice.connectionSignal?.aborted) return;
37639
38418
  try {
37640
38419
  const session = await this.runtime.sessionStore.read();
37641
- if (session === void 0) return;
38420
+ if (session === void 0 || notice.connectionUserId !== void 0 && notice.connectionUserId !== session.userId) return;
38421
+ this.runtime.invalidateMemberCache?.();
37642
38422
  const sourceKey = await this.source.chatDirectorySourceKey(session.userId, hint.chatSessionUid);
37643
- if ((await this.runtime.sessionStore.read())?.userId !== session.userId) return;
37644
- this.emitChatClientEvent({
38423
+ if (notice.connectionSignal?.aborted || (await this.runtime.sessionStore.read())?.userId !== session.userId) return;
38424
+ if (notice.memberJoined !== void 0) this.emitChatClientEvent({
38425
+ type: "members-invalidated",
38426
+ revision: this.nextChatClientRevision(),
38427
+ sourceKey
38428
+ });
38429
+ else this.emitChatClientEvent({
37645
38430
  type: "member-events-invalidated",
37646
38431
  revision: this.nextChatClientRevision(),
37647
38432
  sourceKey,
@@ -37866,11 +38651,12 @@ var ChatRealtimeService = class {
37866
38651
  this.notificationBaselineSequences.clear();
37867
38652
  const sequences = /* @__PURE__ */ new Map();
37868
38653
  const pins = [];
37869
- this.source.invalidateSourceListCache(session.userId, "root");
38654
+ const shared = this.directoryBaseline === void 0 ? void 0 : await this.directoryBaseline();
37870
38655
  let cursor;
37871
- for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
37872
- const page = await this.source.listSources("root", {
37873
- limit: 50,
38656
+ const visited = /* @__PURE__ */ new Set();
38657
+ while (true) {
38658
+ const page = shared ?? await this.source.listSources("root", {
38659
+ limit: 20,
37874
38660
  refresh: true,
37875
38661
  ...cursor === void 0 ? {} : { cursor }
37876
38662
  });
@@ -37887,8 +38673,10 @@ var ChatRealtimeService = class {
37887
38673
  policyUpdatedAtMillis: item.chatPolicyUpdatedAtMillis
37888
38674
  });
37889
38675
  }
37890
- if (!page.hasMore || page.nextCursor === void 0) break;
38676
+ if (shared !== void 0 || !page.hasMore) break;
38677
+ if (page.nextCursor === void 0 || visited.has(page.nextCursor)) throw new Error("Notification directory cursor is invalid");
37891
38678
  cursor = page.nextCursor;
38679
+ visited.add(cursor);
37892
38680
  }
37893
38681
  const activeSession = await this.runtime.sessionStore.read();
37894
38682
  const state = this.chatRealtime.state();
@@ -38377,6 +39165,160 @@ var ChatRealtimeService = class {
38377
39165
  }
38378
39166
  };
38379
39167
  //#endregion
39168
+ //#region src/member-directory.ts
39169
+ /** Only for the members/page and members/by-user-ids read contracts: 2001/2002 mean unreadable membership/session. */
39170
+ function invalidatesMemberSnapshot(error) {
39171
+ const failure = error;
39172
+ const code = failure?.body?.code ?? failure?.code;
39173
+ return code !== void 0 && [
39174
+ "auth-http-401",
39175
+ "auth-http-403",
39176
+ "login-required",
39177
+ "login-expired",
39178
+ "source-ref-invalid",
39179
+ "arkme-code-1000",
39180
+ "arkme-code-403",
39181
+ "arkme-code-1004",
39182
+ "arkme-code-2001",
39183
+ "arkme-code-2002",
39184
+ "chat-members-source-invalid"
39185
+ ].includes(code);
39186
+ }
39187
+ function mergeMemberFacts(previous, facts) {
39188
+ const { memberName: _oldName, ...base } = previous ?? {
39189
+ displayName: facts.memberName || "群成员",
39190
+ recordCount: 0,
39191
+ mentionCount: 0,
39192
+ statsKnown: false
39193
+ };
39194
+ return {
39195
+ ...base,
39196
+ memberRef: facts.memberRef,
39197
+ role: facts.role,
39198
+ status: facts.status,
39199
+ isSelf: facts.isSelf,
39200
+ isOwner: facts.isOwner,
39201
+ joinedAtMillis: facts.joinedAtMillis,
39202
+ ...facts.memberName === void 0 ? {} : { memberName: facts.memberName }
39203
+ };
39204
+ }
39205
+ function mergeMemberPresentation(previous, incoming, profileUnavailable) {
39206
+ if (!profileUnavailable) return {
39207
+ ...incoming,
39208
+ statsKnown: true
39209
+ };
39210
+ return {
39211
+ ...incoming,
39212
+ statsKnown: true,
39213
+ ...[
39214
+ "",
39215
+ "群成员",
39216
+ "成员"
39217
+ ].includes(incoming.displayName) && previous !== void 0 ? { displayName: previous.displayName } : {},
39218
+ ...previous?.avatarRef === void 0 ? {} : { avatarRef: previous.avatarRef }
39219
+ };
39220
+ }
39221
+ /** Membership creates entities; details can only enrich or explicitly remove known entities. */
39222
+ function applyMemberUpdate(members, update) {
39223
+ let changed = false;
39224
+ const put = (item) => {
39225
+ const old = members.get(item.memberRef);
39226
+ const fields = /* @__PURE__ */ new Set([...Object.keys(old ?? {}), ...Object.keys(item)]);
39227
+ if (old !== void 0 && [...fields].every((field) => old[field] === item[field])) return;
39228
+ members.set(item.memberRef, item);
39229
+ changed = true;
39230
+ };
39231
+ if (update.kind === "membership") for (const facts of update.items) put(mergeMemberFacts(members.get(facts.memberRef), facts));
39232
+ else {
39233
+ const unavailable = new Set(update.unavailableProfileMemberRefs);
39234
+ for (const item of update.items) {
39235
+ const old = members.get(item.memberRef);
39236
+ if (old !== void 0) put(mergeMemberPresentation(old, item, unavailable.has(item.memberRef)));
39237
+ }
39238
+ }
39239
+ for (const ref of update.removedMemberRefs) if (members.delete(ref)) changed = true;
39240
+ return changed;
39241
+ }
39242
+ function validFacts(value) {
39243
+ if (!value || typeof value !== "object") return false;
39244
+ const item = value;
39245
+ return typeof item.memberRef === "string" && item.memberRef.trim() !== "" && item.status === "active" && [
39246
+ "owner",
39247
+ "admin",
39248
+ "member",
39249
+ "unknown"
39250
+ ].includes(item.role) && typeof item.isSelf === "boolean" && typeof item.isOwner === "boolean" && Number.isSafeInteger(item.joinedAtMillis) && item.joinedAtMillis >= 0 && (item.memberName === void 0 || typeof item.memberName === "string");
39251
+ }
39252
+ function cachedMemberItem(value) {
39253
+ if (!validFacts(value)) return void 0;
39254
+ const item = value;
39255
+ if (typeof item.displayName !== "string" || !Number.isSafeInteger(item.recordCount) || item.recordCount < 0 || !Number.isSafeInteger(item.mentionCount) || item.mentionCount < 0 || item.statsKnown !== void 0 && typeof item.statsKnown !== "boolean") return void 0;
39256
+ const result = {
39257
+ memberRef: item.memberRef,
39258
+ role: item.role,
39259
+ status: item.status,
39260
+ isSelf: item.isSelf,
39261
+ isOwner: item.isOwner,
39262
+ joinedAtMillis: item.joinedAtMillis,
39263
+ displayName: item.displayName,
39264
+ recordCount: item.recordCount,
39265
+ mentionCount: item.mentionCount,
39266
+ ...item.statsKnown === void 0 ? {} : { statsKnown: item.statsKnown }
39267
+ };
39268
+ for (const field of [
39269
+ "memberName",
39270
+ "secondaryName",
39271
+ "avatarRef",
39272
+ "mentionRef",
39273
+ "mentionDisplayName",
39274
+ "mentionSecondaryName"
39275
+ ]) {
39276
+ if (item[field] === void 0) continue;
39277
+ if (typeof item[field] !== "string") return void 0;
39278
+ result[field] = item[field];
39279
+ }
39280
+ return result;
39281
+ }
39282
+ function validateMemberUpdate(update) {
39283
+ if (!Array.isArray(update.items) || !Array.isArray(update.removedMemberRefs) || update.items.some((item) => !validFacts(item)) || new Set(update.items.map((item) => item.memberRef)).size !== update.items.length || update.removedMemberRefs.some((ref) => typeof ref !== "string" || !ref || update.items.some((item) => item.memberRef === ref))) throw new Error("成员响应无效");
39284
+ if (update.kind === "presentation") {
39285
+ if (update.items.length + update.removedMemberRefs.length > 50) throw new Error("成员资料超出批次上限");
39286
+ if (!Array.isArray(update.unavailableProfileMemberRefs) || update.unavailableProfileMemberRefs.some((ref) => !update.items.some((item) => item.memberRef === ref)) || update.items.some((item) => cachedMemberItem(item) === void 0)) throw new Error("成员资料响应无效");
39287
+ } else if (update.kind !== "membership" || ![
39288
+ "owner",
39289
+ "admin",
39290
+ "member"
39291
+ ].includes(update.selfRole) || typeof update.hasMore !== "boolean" || update.items.length + update.removedMemberRefs.length > 100) throw new Error("成员分页响应无效");
39292
+ }
39293
+ function joinPerson(value) {
39294
+ if (!value || typeof value.displayName !== "string" || typeof value.isSelf !== "boolean" || value.memberRef !== void 0 && (typeof value.memberRef !== "string" || !value.memberRef.trim())) throw new Error("入群事件成员无效");
39295
+ return {
39296
+ displayName: value.displayName,
39297
+ isSelf: value.isSelf,
39298
+ ...value.memberRef === void 0 ? {} : { memberRef: value.memberRef }
39299
+ };
39300
+ }
39301
+ function mergeMemberJoinEvents(previous, incoming) {
39302
+ const events = new Map(previous.map((event) => [event.eventId, event]));
39303
+ for (const event of incoming) {
39304
+ if (!event || typeof event.eventId !== "string" || !event.eventId || !["invite", "direct_add"].includes(event.action) || !Number.isSafeInteger(event.occurredAtMillis) || event.occurredAtMillis < 0 || !Array.isArray(event.invitees)) throw new Error("入群事件无效");
39305
+ const inviter = joinPerson(event.inviter);
39306
+ const incomingPeople = event.invitees.map(joinPerson);
39307
+ if (incomingPeople.some((person) => !person.memberRef)) throw new Error("入群事件缺少成员身份");
39308
+ const old = events.get(event.eventId);
39309
+ const invitees = new Map((old?.invitees ?? []).map((person) => [person.memberRef, person]));
39310
+ for (const person of incomingPeople) invitees.set(person.memberRef, person);
39311
+ events.set(event.eventId, {
39312
+ eventId: event.eventId,
39313
+ action: event.action,
39314
+ occurredAtMillis: event.occurredAtMillis,
39315
+ inviter,
39316
+ invitees: [...invitees.values()]
39317
+ });
39318
+ }
39319
+ return [...events.values()].slice(-2e3);
39320
+ }
39321
+ //#endregion
38380
39322
  //#region src/services/member-event-service.ts
38381
39323
  const CURSOR_PREFIX = "arkme-member-event-cursor-v1";
38382
39324
  var MemberEventService = class {
@@ -41310,7 +42252,7 @@ async function projectArkmeConversationMemberJoinEvents(rawItems, options) {
41310
42252
  "manual_add",
41311
42253
  "added_by_member"
41312
42254
  ])).has(sourceType) ? "direct_add" : "invite";
41313
- const inviterKey = inviterUserId > 0 ? `id:${String(inviterUserId)}` : `name:${inviterDisplayName}`;
42255
+ const inviterKey = inviterUserId > 0 ? `id:${String(inviterUserId)}` : `unknown-inviter:${String(inviteeUserId)}`;
41314
42256
  const groupKey = `${String(occurredAtMillis)}|${action}|${inviterKey}`;
41315
42257
  const group = groups.get(groupKey) ?? {
41316
42258
  action,
@@ -41675,6 +42617,156 @@ var ChatService = class {
41675
42617
  this.favoriteStickerMutationTail = pending.then(() => void 0, () => void 0);
41676
42618
  return await pending;
41677
42619
  }
42620
+ async cachedSourceMembers(sourceRef, signal) {
42621
+ signal?.throwIfAborted();
42622
+ const session = await this.runtime.requireSession();
42623
+ const source = await this.requireReadReceiptChatSource(sourceRef, session.userId);
42624
+ const cached = await this.runtime.stateStore.cachedConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
42625
+ signal?.throwIfAborted();
42626
+ if (cached === void 0) return void 0;
42627
+ const invalid = [];
42628
+ for (const member of cached.items) {
42629
+ signal?.throwIfAborted();
42630
+ try {
42631
+ await this.openChatMemberRef(member.memberRef, session.userId, source.ownerRef);
42632
+ } catch (error) {
42633
+ if (!(error instanceof ArkmePluginError)) return void 0;
42634
+ invalid.push(member.memberRef);
42635
+ }
42636
+ }
42637
+ signal?.throwIfAborted();
42638
+ if (invalid.length === 0) return cached;
42639
+ await this.runtime.stateStore.forgetCachedMembers?.(session.userId, source.ownerRef, invalid).catch(() => void 0);
42640
+ }
42641
+ async pageSourceMembers(sourceRef, options = {}) {
42642
+ options.signal?.throwIfAborted();
42643
+ const session = await this.runtime.requireSession();
42644
+ const cacheEpoch = this.runtime.memberCacheEpoch?.() ?? 0;
42645
+ const source = await this.requireReadReceiptChatSource(sourceRef, session.userId);
42646
+ const limit = options.limit ?? 50;
42647
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new ArkmePluginError("member-page-invalid", "成员分页大小必须为 1 至 100", false, 400);
42648
+ const after = options.cursor === void 0 ? 0 : await this.openMemberPageCursor(options.cursor, session.userId, source.ownerRef);
42649
+ const data = await this.memberRead(session, source.ownerRef, "/api/v1/chats/members/page", {
42650
+ chat_session_uid: source.ownerRef,
42651
+ after_user_id: after,
42652
+ limit,
42653
+ active_only: false
42654
+ }, options.signal);
42655
+ options.signal?.throwIfAborted();
42656
+ const raw = listValue$16(data.items).map(objectValue$2);
42657
+ const ids = raw.map((item) => numberValue$19(item.user_id));
42658
+ const next = numberValue$19(data.next_user_id);
42659
+ if (stringValue$2(data.chat_session_uid) !== source.ownerRef || !Array.isArray(data.items) || typeof data.has_more !== "boolean" || chatMemberRole$1(data.self_role) === "unknown" || raw.some((item) => chatMemberStatus$1(item.status) === "unknown") || raw.length > limit || ids.some((id, index) => !Number.isSafeInteger(id) || id <= (index === 0 ? after : ids[index - 1])) || data.has_more && (raw.length === 0 || next !== ids.at(-1))) throw new ArkmePluginError("member-page-invalid-response", "成员分页响应无效", true, 502);
42660
+ const active = raw.filter((item) => chatMemberStatus$1(item.status) === "active");
42661
+ const items = await this.projectChatMembers(source.ownerRef, active, session, {
42662
+ includeViewerLabels: false,
42663
+ includeHumanMentionRefs: false,
42664
+ basicOnly: true,
42665
+ ...options.signal === void 0 ? {} : { signal: options.signal }
42666
+ });
42667
+ const removedMemberRefs = await Promise.all(raw.filter((item) => chatMemberStatus$1(item.status) !== "active").map((item) => this.sealChatMemberRef(session.userId, source.ownerRef, numberValue$19(item.user_id))));
42668
+ const signingKey = await this.runtime.stateStore.uniqueCode();
42669
+ const joinEvents = source.kind === "group_chat" && this.runtime.config.chatMemberJoinEventsEnabled !== false ? await projectArkmeConversationMemberJoinEvents(raw, {
42670
+ viewerUserId: session.userId,
42671
+ memberRefForUserId: async (id) => await this.sealChatMemberRef(session.userId, source.ownerRef, id),
42672
+ eventIdForStableKey: async (stableKey) => `arkme-chat-join-v1.${createHmac("sha256", signingKey).update(`${session.userId}|${source.ownerRef}|${stableKey}`).digest("base64url")}`
42673
+ }) : [];
42674
+ const page = {
42675
+ kind: "membership",
42676
+ selfRole: chatMemberRole$1(data.self_role),
42677
+ source: await this.source.sourceItem(source),
42678
+ items: items.map(({ memberRef, role, status, isSelf, isOwner, joinedAtMillis, memberName }) => ({
42679
+ memberRef,
42680
+ role,
42681
+ status,
42682
+ isSelf,
42683
+ isOwner,
42684
+ joinedAtMillis,
42685
+ ...memberName === void 0 ? {} : { memberName }
42686
+ })),
42687
+ removedMemberRefs,
42688
+ hasMore: data.has_more,
42689
+ joinEvents,
42690
+ ...data.has_more ? { nextCursor: await this.sealMemberPageCursor(session.userId, source.ownerRef, next) } : {}
42691
+ };
42692
+ if (!options.signal?.aborted && cacheEpoch === (this.runtime.memberCacheEpoch?.() ?? 0)) await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, page).catch(() => void 0);
42693
+ return page;
42694
+ }
42695
+ async sourceMembersPresentation(sourceRef, memberRefs, options = {}) {
42696
+ options.signal?.throwIfAborted();
42697
+ const session = await this.runtime.requireSession();
42698
+ const cacheEpoch = this.runtime.memberCacheEpoch?.() ?? 0;
42699
+ const source = await this.requireReadReceiptChatSource(sourceRef, session.userId);
42700
+ if (memberRefs.length < 1 || memberRefs.length > 50 || new Set(memberRefs).size !== memberRefs.length) throw new ArkmePluginError("member-presentation-invalid", "每次需要 1 至 50 个不同成员引用", false, 400);
42701
+ const ids = (await Promise.all(memberRefs.map((ref) => this.openChatMemberRef(ref, session.userId, source.ownerRef)))).map((ref) => ref.targetUserId);
42702
+ const data = await this.memberRead(session, source.ownerRef, "/api/v1/chats/members/by-user-ids", {
42703
+ chat_session_uid: source.ownerRef,
42704
+ user_ids: ids,
42705
+ active_only: true
42706
+ }, options.signal);
42707
+ options.signal?.throwIfAborted();
42708
+ const raw = listValue$16(data.items).map(objectValue$2);
42709
+ const returnedIds = raw.map((item) => numberValue$19(item.user_id));
42710
+ if (stringValue$2(data.chat_session_uid) !== source.ownerRef || !Array.isArray(data.items) || raw.length > ids.length || new Set(returnedIds).size !== raw.length || returnedIds.some((id) => !ids.includes(id)) || raw.some((item) => chatMemberStatus$1(item.status) !== "active")) throw new ArkmePluginError("member-presentation-invalid-response", "成员资料响应无效", true, 502);
42711
+ const profiles = await this.profile.publicProfileSummariesByUserIds(returnedIds, session, options.signal).catch((error) => {
42712
+ if (options.signal?.aborted) throw error;
42713
+ return /* @__PURE__ */ new Map();
42714
+ });
42715
+ const items = await this.projectChatMembers(source.ownerRef, raw, session, {
42716
+ includeViewerLabels: false,
42717
+ includeHumanMentionRefs: source.kind === "group_chat",
42718
+ profiles,
42719
+ ...options.signal === void 0 ? {} : { signal: options.signal }
42720
+ });
42721
+ options.signal?.throwIfAborted();
42722
+ const unavailableProfileMemberRefs = await Promise.all(returnedIds.filter((id) => !profiles.has(id)).map((id) => this.sealChatMemberRef(session.userId, source.ownerRef, id)));
42723
+ const result = {
42724
+ kind: "presentation",
42725
+ source: await this.source.sourceItem(source),
42726
+ items,
42727
+ unavailableProfileMemberRefs,
42728
+ removedMemberRefs: memberRefs.filter((_ref, index) => !returnedIds.includes(ids[index]))
42729
+ };
42730
+ if (!options.signal?.aborted && cacheEpoch === (this.runtime.memberCacheEpoch?.() ?? 0)) await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, result).catch(() => void 0);
42731
+ return result;
42732
+ }
42733
+ async memberRead(session, group, path, body, signal) {
42734
+ try {
42735
+ return await this.runtime.authenticatedChatPost(path, body, session, signal, {
42736
+ lane: path.endsWith("/members/page") ? "interactive-read" : "background-read",
42737
+ key: `members:${this.runtime.memberCacheEpoch?.() ?? 0}:${path}:${JSON.stringify(body)}`,
42738
+ failureCooldownMs: 2e3
42739
+ });
42740
+ } catch (error) {
42741
+ if (invalidatesMemberSnapshot(error)) {
42742
+ this.runtime.invalidateMemberCache?.();
42743
+ await this.runtime.stateStore.clearConversationMembers?.(session.userId, group).catch(() => void 0);
42744
+ }
42745
+ throw error;
42746
+ }
42747
+ }
42748
+ async sealMemberPageCursor(userId, group, after) {
42749
+ const encoded = encodeOpaqueJson$6({
42750
+ userId,
42751
+ group,
42752
+ after
42753
+ });
42754
+ return `arkme-member-page-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
42755
+ }
42756
+ async openMemberPageCursor(cursor, userId, group) {
42757
+ const [prefix, encoded, signature, extra] = cursor.split(".");
42758
+ if (cursor.length > 4096 || prefix !== "arkme-member-page-v1" || !encoded || !signature || extra !== void 0) throw new ArkmePluginError("member-cursor-invalid", "成员分页游标无效", false, 400);
42759
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url");
42760
+ if (Buffer.byteLength(signature) !== expected.length || !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) throw new ArkmePluginError("member-cursor-invalid", "成员分页游标无效", false, 400);
42761
+ let value;
42762
+ try {
42763
+ value = objectValue$2(decodeOpaqueJson$5(encoded));
42764
+ } catch {
42765
+ throw new ArkmePluginError("member-cursor-invalid", "成员分页游标无效", false, 400);
42766
+ }
42767
+ if (value.userId !== userId || value.group !== group || !Number.isSafeInteger(value.after) || numberValue$19(value.after) <= 0) throw new ArkmePluginError("member-cursor-invalid", "成员分页游标与当前账号或群不匹配", false, 403);
42768
+ return numberValue$19(value.after);
42769
+ }
41678
42770
  async listSourceMembers(sourceRef, options = {}) {
41679
42771
  const session = await this.runtime.requireSession();
41680
42772
  const source = await this.source.openSourceRef(sourceRef, session.userId);
@@ -42084,6 +43176,14 @@ var ChatService = class {
42084
43176
  }, session, options.signal);
42085
43177
  const item = objectValue$2(data.item);
42086
43178
  if (stringValue$2(data.chat_session_uid).trim() !== source.ownerRef || !Number.isSafeInteger(numberValue$19(item.user_id)) || numberValue$19(item.user_id) !== member.targetUserId || !Number.isSafeInteger(numberValue$19(item.status)) || numberValue$19(item.status) !== 3 || typeof item.join_restricted !== "boolean" || options.preventRejoin === true && item.join_restricted !== true) throw new ArkmePluginError("group-member-remove-invalid-response", "移除成员服务返回无效", true, 502);
43179
+ this.runtime.invalidateMemberCache?.();
43180
+ await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, {
43181
+ kind: "presentation",
43182
+ source: await this.source.sourceItem(source),
43183
+ items: [],
43184
+ removedMemberRefs: [normalizedMemberRef],
43185
+ unavailableProfileMemberRefs: []
43186
+ }).catch(() => void 0);
42087
43187
  return {
42088
43188
  sourceRef: normalizedSourceRef,
42089
43189
  memberRef: normalizedMemberRef,
@@ -42263,6 +43363,41 @@ var ChatService = class {
42263
43363
  userName: firstJoinDisplayName(item, ["display_name", "displayName"])
42264
43364
  };
42265
43365
  });
43366
+ if (options.basicOnly === true) {
43367
+ const cache = await this.runtime.stateStore.cachedConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
43368
+ const cached = new Map(cache?.items.map((member) => [member.memberRef, member]));
43369
+ const items = [];
43370
+ for (const member of receiptMembers) {
43371
+ const memberRef = await this.sealChatMemberRef(session.userId, source.ownerRef, member.userId);
43372
+ const known = cached.get(memberRef);
43373
+ const names = resolveChatMemberDisplayNames({
43374
+ userId: member.userId,
43375
+ remarkCandidates: [member.remarkName],
43376
+ memberNameCandidates: [member.memberName],
43377
+ userNameCandidates: [member.userName]
43378
+ });
43379
+ const currentName = names.displayName !== "群成员";
43380
+ items.push({
43381
+ memberRef,
43382
+ displayName: currentName ? names.displayName : known?.displayName ?? names.displayName,
43383
+ displayNameIsCurrent: currentName,
43384
+ ...known?.avatarRef === void 0 ? {} : { avatarRef: known.avatarRef },
43385
+ readStatus: member.readStatus,
43386
+ ...member.readStatus === "read" && member.readAtMillis > 0 ? { readAtMillis: member.readAtMillis } : {}
43387
+ });
43388
+ }
43389
+ const readCount = items.filter((member) => member.readStatus === "read").length;
43390
+ return {
43391
+ sourceRef,
43392
+ itemUid: message.itemUid,
43393
+ sequence: message.sequence,
43394
+ items,
43395
+ readCount,
43396
+ unreadCount: items.length - readCount,
43397
+ totalMemberCount: items.length,
43398
+ presentationComplete: false
43399
+ };
43400
+ }
42266
43401
  const userIds = receiptMembers.map((item) => item.userId);
42267
43402
  const missingRemarkUserIds = receiptMembers.filter((item) => item.remarkName === "").map((item) => item.userId);
42268
43403
  const [profiles, privateRemarks] = await Promise.all([this.profile.publicProfileSummariesByUserIds(userIds, session, options.signal).catch(() => /* @__PURE__ */ new Map()), missingRemarkUserIds.length === 0 ? Promise.resolve(/* @__PURE__ */ new Map()) : this.source.privateRemarksByUserIds(missingRemarkUserIds, options.signal === void 0 ? {} : { signal: options.signal }).catch(() => /* @__PURE__ */ new Map())]);
@@ -44270,7 +45405,7 @@ var ChatService = class {
44270
45405
  }
44271
45406
  async projectChatMembers(chatSessionUid, rawItems, session, options) {
44272
45407
  const userIds = rawItems.map((item) => Math.trunc(numberValue$19(item.user_id))).filter((userId) => Number.isSafeInteger(userId) && userId > 0);
44273
- const [profiles, viewerLabels] = await Promise.all([this.profile.publicProfileSummariesByUserIds(userIds, session, options.signal).catch(() => /* @__PURE__ */ new Map()), options.includeViewerLabels ? this.source.privateChatViewerLabelsByUserIds(userIds, { ...options.signal === void 0 ? {} : { signal: options.signal } }).catch(() => /* @__PURE__ */ new Map()) : Promise.resolve(/* @__PURE__ */ new Map())]);
45408
+ const [profiles, viewerLabels] = await Promise.all([options.basicOnly ? Promise.resolve(/* @__PURE__ */ new Map()) : options.profiles !== void 0 ? Promise.resolve(options.profiles) : this.profile.publicProfileSummariesByUserIds(userIds, session, options.signal).catch(() => /* @__PURE__ */ new Map()), options.includeViewerLabels ? this.source.privateChatViewerLabelsByUserIds(userIds, { ...options.signal === void 0 ? {} : { signal: options.signal } }).catch(() => /* @__PURE__ */ new Map()) : Promise.resolve(/* @__PURE__ */ new Map())]);
44274
45409
  const members = [];
44275
45410
  for (const item of rawItems) {
44276
45411
  const userId = Math.trunc(numberValue$19(item.user_id));
@@ -73689,6 +74824,8 @@ var ContactDirectoryService = class {
73689
74824
  world;
73690
74825
  chat;
73691
74826
  contactRefs = /* @__PURE__ */ new Map();
74827
+ remarkRevision = 0;
74828
+ recentRemarks = /* @__PURE__ */ new Map();
73692
74829
  constructor(runtime, source, bot, profile, world, chat) {
73693
74830
  this.runtime = runtime;
73694
74831
  this.source = source;
@@ -73699,6 +74836,7 @@ var ContactDirectoryService = class {
73699
74836
  }
73700
74837
  dispose() {
73701
74838
  this.contactRefs.clear();
74839
+ this.recentRemarks.clear();
73702
74840
  }
73703
74841
  async listRecordingSpeakerUsers(session, signal) {
73704
74842
  const descriptors = await this.loadMergedContactDescriptors(session, signal === void 0 ? {} : { signal });
@@ -73755,9 +74893,12 @@ var ContactDirectoryService = class {
73755
74893
  const { session, entry } = await this.resolveContactRef(contactRef);
73756
74894
  const publicProfile = (await this.profile.publicProfileSummariesByUserIds([entry.targetUserId], session, signal)).get(entry.targetUserId);
73757
74895
  const identity = await this.contactIdentity(entry.targetUserId, entry.remark, publicProfile, session, entry.displayNameSnapshot);
74896
+ const remark = this.contactRefs.get(contactRef.trim())?.remark ?? entry.remark;
73758
74897
  const updated = {
73759
74898
  ...entry,
73760
- ...identity
74899
+ ...identity,
74900
+ remark,
74901
+ displayName: remark || identity.nickname || identity.accountName || entry.displayNameSnapshot || "联系人"
73761
74902
  };
73762
74903
  this.contactRefs.set(contactRef.trim(), updated);
73763
74904
  return {
@@ -73765,9 +74906,49 @@ var ContactDirectoryService = class {
73765
74906
  displayName: updated.displayName,
73766
74907
  nickname: updated.nickname,
73767
74908
  remark: updated.remark,
74909
+ ...updated.accountName === void 0 ? {} : { accountName: updated.accountName },
73768
74910
  ...updated.avatarRef === void 0 ? {} : { avatarRef: updated.avatarRef }
73769
74911
  };
73770
74912
  }
74913
+ async updateContactRemark(contactRef, remarkValue, signal) {
74914
+ if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
74915
+ const { session, entry } = await this.resolveContactRef(contactRef);
74916
+ const remark = remarkValue.trim();
74917
+ if (Array.from(remark).length > 100) throw new ArkmePluginError("directory-contact-remark-too-long", "备注名最多 100 个字符", false, 400);
74918
+ if (entry.chatSessionUid === void 0) throw new ArkmePluginError("directory-contact-session-unavailable", "暂时无法修改此联系人的备注,请刷新联系人后重试", true, 409);
74919
+ if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
74920
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/contacts/update-remark", {
74921
+ chat_session_uid: entry.chatSessionUid,
74922
+ remark,
74923
+ update_at: Date.now()
74924
+ }, session, signal);
74925
+ this.runtime.invalidateKey(this.runtime.requestScope(session.userId), "directory:contacts:");
74926
+ const contact = objectValue$2(data.contact);
74927
+ if (stringValue$2(contact.chat_session_uid) !== entry.chatSessionUid || numberValue$17(contact.user_id) !== entry.targetUserId || contact.remark !== void 0 && typeof contact.remark !== "string") throw new ArkmePluginError("directory-contact-remark-response-invalid", "备注保存响应不完整,请重试", true, 502);
74928
+ const savedRemark = stringValue$2(contact.remark).trim();
74929
+ this.remarkRevision += 1;
74930
+ this.recentRemarks.set(`${session.userId}:${entry.targetUserId}`, {
74931
+ revision: this.remarkRevision,
74932
+ remark: savedRemark
74933
+ });
74934
+ for (const [ref, current] of this.contactRefs) {
74935
+ if (current.viewerUserId !== session.userId || current.targetUserId !== entry.targetUserId) continue;
74936
+ this.contactRefs.set(ref, {
74937
+ ...current,
74938
+ remark: savedRemark,
74939
+ displayName: savedRemark || current.nickname || current.accountName || current.displayNameSnapshot || "联系人"
74940
+ });
74941
+ }
74942
+ const current = this.contactRefs.get(contactRef.trim()) ?? entry;
74943
+ return {
74944
+ contactRef: contactRef.trim(),
74945
+ remark: savedRemark,
74946
+ displayName: savedRemark || current.nickname || current.accountName || current.displayNameSnapshot || "联系人",
74947
+ nickname: current.nickname,
74948
+ ...current.accountName === void 0 ? {} : { accountName: current.accountName },
74949
+ ...current.avatarRef === void 0 ? {} : { avatarRef: current.avatarRef }
74950
+ };
74951
+ }
73771
74952
  async contactWorld(contactRef, options = {}) {
73772
74953
  const { entry } = await this.resolveContactRef(contactRef);
73773
74954
  return await this.world.listUserWorldFeed(entry.targetUserId, cloneWorldOptions(options));
@@ -73830,6 +75011,7 @@ var ContactDirectoryService = class {
73830
75011
  const session = await this.runtime.requireSession();
73831
75012
  const limit = boundedLimit$1(options.limit);
73832
75013
  const offset = await this.openOffsetCursor("contacts", options.cursor, session.userId);
75014
+ const revisionAtStart = this.remarkRevision;
73833
75015
  const descriptors = await this.loadMergedContactDescriptors(session, options);
73834
75016
  const pageDescriptors = descriptors.slice(offset, offset + limit);
73835
75017
  const profiles = await this.profile.publicProfileSummariesByUserIds(pageDescriptors.map((item) => item.targetUserId), session, options.signal);
@@ -73837,7 +75019,17 @@ var ContactDirectoryService = class {
73837
75019
  this.pruneContactRefs();
73838
75020
  for (const descriptor of pageDescriptors) {
73839
75021
  const identity = await this.contactIdentity(descriptor.targetUserId, descriptor.remark, profiles.get(descriptor.targetUserId), session, descriptor.displayNameSnapshot);
73840
- const contactRef = `arkme-directory-contact-v1.${randomUUID()}`;
75022
+ const recent = this.recentRemarks.get(`${session.userId}:${descriptor.targetUserId}`);
75023
+ if (recent !== void 0 && recent.revision > revisionAtStart) {
75024
+ identity.remark = recent.remark;
75025
+ identity.displayName = recent.remark || identity.nickname || identity.accountName || descriptor.displayNameSnapshot || "联系人";
75026
+ }
75027
+ let contactRef;
75028
+ for (const [ref, current] of this.contactRefs) if (current.viewerUserId === session.userId && current.targetUserId === descriptor.targetUserId && current.expiresAtMillis > Date.now()) {
75029
+ contactRef = ref;
75030
+ break;
75031
+ }
75032
+ contactRef ??= `arkme-directory-contact-v1.${randomUUID()}`;
73841
75033
  const entry = {
73842
75034
  viewerUserId: session.userId,
73843
75035
  targetUserId: descriptor.targetUserId,
@@ -73846,6 +75038,7 @@ var ContactDirectoryService = class {
73846
75038
  ...identity,
73847
75039
  expiresAtMillis: Date.now() + CONTACT_DIRECTORY_REF_TTL_MS
73848
75040
  };
75041
+ this.contactRefs.delete(contactRef);
73849
75042
  this.contactRefs.set(contactRef, entry);
73850
75043
  const item = {
73851
75044
  kind: "contact",
@@ -73862,12 +75055,21 @@ var ContactDirectoryService = class {
73862
75055
  }
73863
75056
  this.pruneContactRefs();
73864
75057
  const hasMore = offset + pageDescriptors.length < descriptors.length;
75058
+ const nextCursor = hasMore ? await this.sealOffsetCursor("contacts", offset + pageDescriptors.length, session.userId) : void 0;
75059
+ for (const item of items) {
75060
+ if (item.kind !== "contact") continue;
75061
+ const current = this.contactRefs.get(item.contactRef);
75062
+ if (current === void 0) continue;
75063
+ item.remark = current.remark;
75064
+ item.displayName = current.displayName;
75065
+ item.letter = contactDirectoryLetter(item);
75066
+ }
73865
75067
  return {
73866
75068
  section: "contacts",
73867
75069
  items,
73868
75070
  total: descriptors.length,
73869
75071
  hasMore,
73870
- ...hasMore ? { nextCursor: await this.sealOffsetCursor("contacts", offset + pageDescriptors.length, session.userId) } : {}
75072
+ ...nextCursor === void 0 ? {} : { nextCursor }
73871
75073
  };
73872
75074
  }
73873
75075
  async loadMergedContactDescriptors(session, options) {
@@ -75190,13 +76392,15 @@ var GroupService = class {
75190
76392
  });
75191
76393
  continue;
75192
76394
  }
75193
- const outcome = stringValue$2(objectValue$2((await this.runtime.authenticatedChatPost("/api/v1/chats/members/add", {
76395
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/members/add", {
75194
76396
  chat_session_uid: source.ownerRef,
75195
76397
  target_user_id: candidate.targetUserId,
75196
76398
  join_at: Date.now(),
75197
76399
  display_name_snapshot: candidate.displayName,
75198
76400
  extra: { source: "dsh_arkme_candidate" }
75199
- }, session, signal)).item).outcome);
76401
+ }, session, signal);
76402
+ this.runtime.invalidateMemberCache?.();
76403
+ const outcome = stringValue$2(objectValue$2(data.item).outcome);
75200
76404
  results.push({
75201
76405
  candidateRef,
75202
76406
  displayName: candidate.displayName,
@@ -75412,6 +76616,8 @@ var GroupService = class {
75412
76616
  target_user_id: session.userId,
75413
76617
  action: 1
75414
76618
  }, session, signal);
76619
+ this.runtime.invalidateMemberCache?.();
76620
+ await this.runtime.stateStore.clearConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
75415
76621
  this.source.invalidateSourceListCache(session.userId, "root");
75416
76622
  return { status: "ok" };
75417
76623
  }
@@ -75423,6 +76629,8 @@ var GroupService = class {
75423
76629
  chat_session_uid: source.ownerRef,
75424
76630
  update_at: Date.now()
75425
76631
  }, session, signal);
76632
+ this.runtime.invalidateMemberCache?.();
76633
+ await this.runtime.stateStore.clearConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
75426
76634
  this.source.invalidateSourceListCache(session.userId, "root");
75427
76635
  return { status: "ok" };
75428
76636
  }
@@ -76612,12 +77820,28 @@ var MediaService = class {
76612
77820
  }, void 0, 11e4);
76613
77821
  }
76614
77822
  async readImage(imageRef, options = {}) {
77823
+ options.signal?.throwIfAborted();
76615
77824
  const session = await this.runtime.requireSession();
76616
77825
  const isProfileImage = imageRef.trim().startsWith("arkme-profile-image-v1.");
77826
+ const isBotImage = imageRef.trim().startsWith("arkme-bot-image-v1.");
77827
+ const isAvatar = isProfileImage || isBotImage;
76617
77828
  const maximumBytes = isProfileImage ? MAX_ARKME_PROFILE_IMAGE_BYTES : MAX_ARKME_IMAGE_BYTES;
76618
77829
  const byteLimit = Math.min(maximumBytes, Math.max(1, Math.trunc(options.maxBytes ?? maximumBytes)));
76619
77830
  const cacheKey = `${String(session.userId)}:${String(byteLimit)}:${imageRef.trim()}`;
76620
- const cached = this.cachedImage(cacheKey);
77831
+ if (isProfileImage) await this.profile.openProfileImageRef(imageRef, session.userId);
77832
+ const persisted = isAvatar ? await this.runtime.stateStore.readAvatarCache?.(session.userId, imageRef).catch(() => void 0) : void 0;
77833
+ if (persisted !== void 0 && options.refresh !== true && persisted.bytes <= byteLimit) {
77834
+ this.cacheImage(cacheKey, persisted);
77835
+ return cloneImageBytes(persisted);
77836
+ }
77837
+ if (isBotImage) try {
77838
+ if (this.botImages === void 0) throw new ArkmePluginError("bot-image-ref-invalid", "Bot 头像引用不可用", false, 403);
77839
+ await this.botImages.openBotImageRef(imageRef, session.userId);
77840
+ } catch (error) {
77841
+ if (persisted !== void 0 && persisted.bytes <= byteLimit) return cloneImageBytes(persisted);
77842
+ throw error;
77843
+ }
77844
+ const cached = options.refresh === true ? void 0 : this.cachedImage(cacheKey);
76621
77845
  if (cached !== void 0) return cached;
76622
77846
  const existing = this.imageInFlight.get(cacheKey);
76623
77847
  if (existing !== void 0) return cloneImageBytes(await existing);
@@ -76627,8 +77851,13 @@ var MediaService = class {
76627
77851
  try {
76628
77852
  const value = await pending;
76629
77853
  this.cacheImage(cacheKey, value);
77854
+ if (isAvatar) await this.runtime.stateStore.writeAvatarCache?.(session.userId, imageRef, value).catch(() => {
77855
+ console.warn("dsh-arkme: avatar_cache_write_failed");
77856
+ });
76630
77857
  return cloneImageBytes(value);
76631
77858
  } catch (error) {
77859
+ options.signal?.throwIfAborted();
77860
+ if (persisted !== void 0 && persisted.bytes <= byteLimit) return cloneImageBytes(persisted);
76632
77861
  if (isProfileImage) logArkmeAvatarDiagnostic("image_read_failed", {
76633
77862
  environment: this.runtime.config.environment,
76634
77863
  viewerUserId: session.userId,
@@ -83017,6 +84246,9 @@ var UnmarkedSpeakerService = class {
83017
84246
  return ref;
83018
84247
  }
83019
84248
  sealCursor(kind, upstreamCursor, viewerUserId, candidateRef) {
84249
+ if (kind === "candidate") {
84250
+ for (const [ref, entry] of this.cursorRefs) if (entry.kind === kind && entry.viewerUserId === viewerUserId && entry.upstreamCursor === upstreamCursor && entry.expiresAtMillis > Date.now()) return ref;
84251
+ }
83020
84252
  const ref = `${kind === "candidate" ? "arkme-unmarked-candidate-cursor-v1" : "arkme-unmarked-segment-cursor-v1"}.${randomUUID()}`;
83021
84253
  this.cursorRefs.set(ref, {
83022
84254
  viewerUserId,
@@ -85356,6 +86588,7 @@ var ArkmeService = class {
85356
86588
  extensionReview;
85357
86589
  media;
85358
86590
  privacy;
86591
+ directory;
85359
86592
  source;
85360
86593
  conversationDirectoryVisibility;
85361
86594
  record;
@@ -85440,6 +86673,20 @@ var ArkmeService = class {
85440
86673
  this.aiPolish = new GroupAiPolishService(this.runtime, this.source, { sendChatSourceTextRaw: async (...args) => await this.chat.sendChatSourceTextRaw(...args) });
85441
86674
  this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session, chatSessionUid, sourceKind) => await this.chat.chatTimelineItems(data, session, chatSessionUid, sourceKind) });
85442
86675
  this.conversationDirectoryVisibility = new ConversationDirectoryVisibilityService(new ConversationListPreferenceService(this.runtime), this.source, this.bot, this.realtime);
86676
+ this.directory = new ConversationDirectoryService(this.runtime, this.source, this.conversationDirectoryVisibility, async (signal) => await this.botConversation.directory({ signal }), async (ref, signal) => await this.media.readImage(ref, {
86677
+ signal,
86678
+ refresh: true
86679
+ }), (page) => {
86680
+ this.realtime.emitChatClientEvent({
86681
+ type: "directory-update",
86682
+ revision: this.realtime.nextChatClientRevision(),
86683
+ page
86684
+ });
86685
+ }, async (bots, userId) => await this.bot.restoreDirectoryBots(bots, userId));
86686
+ this.realtime.directoryBaseline = async () => await this.directory.complete();
86687
+ this.realtime.subscribeChatRealtime((event) => {
86688
+ if (event.type !== "directory-update") this.directory.accept(event).catch(() => void 0);
86689
+ });
85443
86690
  this.chat = new ChatService(this.runtime, this.source, this.profile, this.media, this.record, this.bot, this.arko, this.aiPolish, this.realtime, this.privacy, this.messageActions);
85444
86691
  this.userBan = new UserBanService(this.runtime, this.chat);
85445
86692
  this.directMessageAdmissionOwner = new DirectMessageAdmissionService(this.runtime, this.source);
@@ -85543,6 +86790,7 @@ var ArkmeService = class {
85543
86790
  return await this.backgroundSoundPreferenceOwner.update(enabled, signal, expectedUserId);
85544
86791
  }
85545
86792
  clearAccountState(userIds) {
86793
+ this.directory.reset();
85546
86794
  this.realtime.resetAttentionSummary();
85547
86795
  for (const userId of userIds) this.privacy.clear(userId);
85548
86796
  this.fileTransfers?.cancelActive();
@@ -85590,13 +86838,19 @@ var ArkmeService = class {
85590
86838
  return await this.bot.connectOpenClawBot(botRef, options);
85591
86839
  }
85592
86840
  async listBots(options = {}) {
85593
- return await this.bot.listBots(options);
86841
+ const { userId } = await this.runtime.requireSession();
86842
+ const result = await this.bot.listBots(options);
86843
+ this.directory.rememberBots(result.items, userId).catch(() => void 0);
86844
+ return result;
85594
86845
  }
85595
86846
  async createBot(input, options = {}) {
85596
86847
  return await this.bot.createBot(input, options);
85597
86848
  }
85598
86849
  async createBotSummary(input, options = {}) {
85599
- return await this.bot.createBotSummary(input, options);
86850
+ const { userId } = await this.runtime.requireSession();
86851
+ const bot = await this.bot.createBotSummary(input, options);
86852
+ this.directory.rememberBots([bot], userId).catch(() => void 0);
86853
+ return bot;
85600
86854
  }
85601
86855
  async revealBotSecret(botRef, options = {}) {
85602
86856
  return await this.bot.revealBotSecret(botRef, options);
@@ -85611,7 +86865,9 @@ var ArkmeService = class {
85611
86865
  return await this.bot.revealManagedBotToken(botRef, options);
85612
86866
  }
85613
86867
  async deleteManagedBot(botRef, confirmationName, options = {}) {
86868
+ const { userId } = await this.runtime.requireSession();
85614
86869
  await this.bot.deleteManagedBot(botRef, confirmationName, options);
86870
+ await this.directory.forgetBot(botRef, userId).catch(() => void 0);
85615
86871
  }
85616
86872
  async botNotificationPreference(botRef, options = {}) {
85617
86873
  return await this.botConversation.notificationPreference(botRef, options);
@@ -85712,6 +86968,7 @@ var ArkmeService = class {
85712
86968
  recordCalendar: true,
85713
86969
  imageLibrary: true,
85714
86970
  sourceDirectory: true,
86971
+ localFirstDirectory: true,
85715
86972
  sourceTimeline: true,
85716
86973
  forwardContent: true,
85717
86974
  sourceTextSend: true,
@@ -85728,6 +86985,7 @@ var ArkmeService = class {
85728
86985
  outgoingCall: true,
85729
86986
  callHistory: true,
85730
86987
  groupMembers: true,
86988
+ memberDirectoryPaging: true,
85731
86989
  groupMemberAdd: true,
85732
86990
  userCard: true,
85733
86991
  openPrivateChat: true,
@@ -85804,6 +87062,7 @@ var ArkmeService = class {
85804
87062
  return await this.callHistory.retryCallSummary(callRef, signal);
85805
87063
  }
85806
87064
  dispose() {
87065
+ this.directory.reset();
85807
87066
  this.record.dispose();
85808
87067
  this.realtime.resetAttentionSummary();
85809
87068
  this.fileTransfers?.cancelActive();
@@ -85849,6 +87108,9 @@ var ArkmeService = class {
85849
87108
  async directoryContactProfile(contactRef, signal) {
85850
87109
  return await this.contactDirectory.contactProfile(contactRef, signal);
85851
87110
  }
87111
+ async updateDirectoryContactRemark(contactRef, remark, signal) {
87112
+ return await this.contactDirectory.updateContactRemark(contactRef, remark, signal);
87113
+ }
85852
87114
  async directoryContactWorld(contactRef, options = {}) {
85853
87115
  return await this.contactDirectory.contactWorld(contactRef, options);
85854
87116
  }
@@ -85974,7 +87236,10 @@ var ArkmeService = class {
85974
87236
  return await this.profile.refreshProfile();
85975
87237
  }
85976
87238
  async arkoProfile(signal) {
85977
- return await this.arko.arkoProfile(signal);
87239
+ const { userId } = await this.runtime.requireSession();
87240
+ const profile = await this.arko.arkoProfile(signal);
87241
+ this.directory.rememberSpecial({ arkoProfile: profile }, userId).catch(() => void 0);
87242
+ return profile;
85978
87243
  }
85979
87244
  async arkoEnsureSession(signal) {
85980
87245
  return await this.arko.arkoEnsureSession(signal);
@@ -85989,7 +87254,16 @@ var ArkmeService = class {
85989
87254
  return await this.arko.arkoActivateModel(routeKey, signal);
85990
87255
  }
85991
87256
  async arkoHistoryPage(limit = 50, offset = 0, signal) {
85992
- return await this.arko.arkoHistoryPage(limit, offset, signal);
87257
+ const { userId } = await this.runtime.requireSession();
87258
+ const page = await this.arko.arkoHistoryPage(limit, offset, signal);
87259
+ if (offset === 0) {
87260
+ const latest = [...page.items].filter((item) => item.text.trim() !== "").sort((a, b) => b.createdAtMillis - a.createdAtMillis || b.messageId - a.messageId)[0];
87261
+ if (latest !== void 0) this.directory.rememberSpecial({ arkoPreview: {
87262
+ text: latest.text,
87263
+ createdAtMillis: latest.createdAtMillis
87264
+ } }, userId).catch(() => void 0);
87265
+ }
87266
+ return page;
85993
87267
  }
85994
87268
  async arkoAsk(text, options = {}) {
85995
87269
  return await this.arko.arkoAsk(text, options);
@@ -86046,16 +87320,34 @@ var ArkmeService = class {
86046
87320
  return await this.source.moveTopicHierarchy(sourceRef, currentParentSourceRef, nextParentSourceRef, insertBeforeSourceRef);
86047
87321
  }
86048
87322
  async listSources(directory, options = {}) {
86049
- return await this.source.listSources(directory, options);
87323
+ if (options.localFirst === true) {
87324
+ if (directory !== "root") throw new ArkmePluginError("source-directory-invalid", "本地目录仅支持会话列表", false);
87325
+ return await this.directory.read(options.refresh === true, options.signal);
87326
+ }
87327
+ const { userId } = await this.runtime.requireSession();
87328
+ const page = await this.source.listSources(directory, options);
87329
+ if (directory === "send_to_self") {
87330
+ const sendToSelf = page.items.find((item) => item.kind === "send_to_self");
87331
+ if (sendToSelf !== void 0) this.directory.rememberSpecial({ sendToSelf }, userId).catch(() => void 0);
87332
+ }
87333
+ return page;
86050
87334
  }
86051
87335
  async setChatDirectoryPin(sourceRef, pinned, signal) {
86052
- return await this.source.setChatDirectoryPin(sourceRef, pinned, signal);
87336
+ const { userId } = await this.runtime.requireSession();
87337
+ const result = await this.source.setChatDirectoryPin(sourceRef, pinned, signal);
87338
+ await this.directory.confirmPin(sourceRef, result.pinned, result.policyUpdatedAtMillis, userId).catch(() => void 0);
87339
+ return result;
87340
+ }
87341
+ async setBotDirectoryPin(botRef, pinned) {
87342
+ await this.directory.pinBot(botRef, pinned);
86053
87343
  }
86054
87344
  async conversationDirectoryVisibilitySnapshot(sourceRefs, botRefs, signal) {
86055
87345
  return await this.conversationDirectoryVisibility.query(sourceRefs, botRefs, signal);
86056
87346
  }
86057
87347
  async setConversationDirectoryVisibility(entryKind, entryRef, hidden, signal) {
87348
+ const { userId } = await this.runtime.requireSession();
86058
87349
  await this.conversationDirectoryVisibility.setVisibility(entryKind, entryRef, hidden, signal);
87350
+ await this.directory.confirmVisibility(entryKind, entryRef, hidden, userId).catch(() => void 0);
86059
87351
  }
86060
87352
  async dshBetaCommunityEntryState(signal) {
86061
87353
  return await this.community.dshBetaCommunityEntryState(signal);
@@ -86116,6 +87408,15 @@ var ArkmeService = class {
86116
87408
  async listGroupMembers(sourceRef, options = {}) {
86117
87409
  return await this.group.listGroupMembers(sourceRef, options);
86118
87410
  }
87411
+ async cachedSourceMembers(sourceRef, signal) {
87412
+ return await this.chat.cachedSourceMembers(sourceRef, signal);
87413
+ }
87414
+ async pageSourceMembers(sourceRef, options = {}) {
87415
+ return await this.chat.pageSourceMembers(sourceRef, options);
87416
+ }
87417
+ async sourceMembersPresentation(sourceRef, memberRefs, options = {}) {
87418
+ return await this.chat.sourceMembersPresentation(sourceRef, memberRefs, options);
87419
+ }
86119
87420
  async listSourceMembers(sourceRef, options = {}) {
86120
87421
  return await this.chat.listSourceMembers(sourceRef, options);
86121
87422
  }
@@ -86154,10 +87455,16 @@ var ArkmeService = class {
86154
87455
  return result;
86155
87456
  }
86156
87457
  async leaveGroup(sourceRef, signal) {
86157
- return await this.group.leaveGroup(sourceRef, signal);
87458
+ const { userId } = await this.runtime.requireSession();
87459
+ const result = await this.group.leaveGroup(sourceRef, signal);
87460
+ await this.directory.forgetSource(sourceRef, userId).catch(() => void 0);
87461
+ return result;
86158
87462
  }
86159
87463
  async dissolveGroup(sourceRef, signal) {
86160
- return await this.group.dissolveGroup(sourceRef, signal);
87464
+ const { userId } = await this.runtime.requireSession();
87465
+ const result = await this.group.dissolveGroup(sourceRef, signal);
87466
+ await this.directory.forgetSource(sourceRef, userId).catch(() => void 0);
87467
+ return result;
86161
87468
  }
86162
87469
  async reportGroup(sourceRef, reason, signal) {
86163
87470
  return await this.group.reportGroup(sourceRef, reason, signal);
@@ -88230,4 +89537,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
88230
89537
  return jsonValue(await handler(jsonValue(args)));
88231
89538
  }
88232
89539
  //#endregion
88233
- export { parseArkmeRecordReeditAttachments as $, inspectNativeBundleArtifactV3 as A, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, bundleSha256 as D, arkmeSandboxEntryId as E, assertExtensionArtifactSize as F, validateVoiceprintPcm16Wav as G, ARKME_EXTENSION_PREVIEW_MAX_BYTES as H, canonicalExtensionJson as I, arkmeIsGenericLinkMetadataTitle as J, isRecordingInstantOnOrAfterUnixEpoch as K, sha256Hex as L, packLocalNativeBundleDirectoryV3 as M, readNativeBundleTarballV3 as N, canonicalBundleJson as O, renderArkmeSandboxHostEntry as P, ArkmeRecordReeditDraftConflict as Q, unpackArkmeExtension as R, ARKME_BUNDLE_ARTIFACT_KIND as S, ArkmeBundleArtifactError as T, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as U, ARKME_EXTENSION_MAX_BYTES as V, ArkmeService as W, arkmeFileBackgroundSound as X, arkmeRequiredLinkMetadataFallback as Y, arkmeRichBackgroundSound as Z, arkmeClientContentDigest as _, ArkmeOutgoingCallError as _t, persistentArkmeExtensionActive as a, ARKME_WORLD_PUBLISH_MAX_IMAGES as at, verifyExtensionResolutionSignature as b, ArkmePluginError as bt, persistentExtensionBundleDirectory as c, ARKME_TOOL_FILE_MAX_BYTES as ct, readPersistentExtensionActivation as d, arkmePickedFileKind as dt, ARKME_DEFAULT_SHARE_WEBSITE as et, refreshPersistentClientWrapper as f, isArkmeBotAvatarRef as ft, activeProfileExtensionOwnerConflicts as g, recordingImportFileNameKey as gt, renderArkmeBundleClientBundle as h, MAX_RECORDING_IMPORT_BYTES as ht, invokePersistentArkmeExtension as i, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as it, packBundleFiles as j, inspectBundleArtifact as k, persistentExtensionPackageIdentity as l, arkmeCanInlineLocalFile as lt, writePersistentExtensionActivation as m, sameRecordingImportIdentity as mt, applyPersistentArkmeHostExtension as n, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as nt, persistentArkmeExtensionRuntimeState as o, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as ot, repairPersistentExtensionInstallation as p, recordingImportFileKind as pt, arkmeExtensionShareRefFromLink as q, deactivatePersistentArkmeExtension as r, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as rt, materializePersistentExtensionBundle as s, ARKME_RUNTIME_INSTANCE_ID as st, activatePersistentArkmeExtension as t, ARKME_PROVIDER_CONTRACT_VERSION as tt, quarantinePersistentExtension as u, arkmeNormalizedFileMimeType as ut, arkmeClientContentDigestFromSource as v, SecretValue as vt, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as w, verifyNativeBundleResolutionSignature as x, verifyBundleResolutionSignature as y, ArkmeDirectMessageAdmissionError as yt, ARKME_EXTENSION_FORMAT as z };
89540
+ export { parseArkmeRecordReeditAttachments as $, inspectNativeBundleArtifactV3 as A, __toCommonJS as At, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, ArkmeDirectMessageAdmissionError as Ct, bundleSha256 as D, __esmMin as Dt, arkmeSandboxEntryId as E, __commonJSMin as Et, assertExtensionArtifactSize as F, validateVoiceprintPcm16Wav as G, ARKME_EXTENSION_PREVIEW_MAX_BYTES as H, canonicalExtensionJson as I, arkmeIsGenericLinkMetadataTitle as J, isRecordingInstantOnOrAfterUnixEpoch as K, sha256Hex as L, packLocalNativeBundleDirectoryV3 as M, readNativeBundleTarballV3 as N, canonicalBundleJson as O, __exportAll as Ot, renderArkmeSandboxHostEntry as P, ArkmeRecordReeditDraftConflict as Q, unpackArkmeExtension as R, ARKME_BUNDLE_ARTIFACT_KIND as S, SecretValue as St, ArkmeBundleArtifactError as T, createDesktopLifecycleReader as Tt, ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE as U, ARKME_EXTENSION_MAX_BYTES as V, ArkmeService as W, arkmeFileBackgroundSound as X, arkmeRequiredLinkMetadataFallback as Y, arkmeRichBackgroundSound as Z, arkmeClientContentDigest as _, recordingImportFileKind as _t, persistentArkmeExtensionActive as a, ARKME_WORLD_PUBLISH_MAX_IMAGES as at, verifyExtensionResolutionSignature as b, recordingImportFileNameKey as bt, persistentExtensionBundleDirectory as c, cachedMemberItem as ct, readPersistentExtensionActivation as d, ARKME_RUNTIME_INSTANCE_ID as dt, ARKME_DEFAULT_SHARE_WEBSITE as et, refreshPersistentClientWrapper as f, ARKME_TOOL_FILE_MAX_BYTES as ft, activeProfileExtensionOwnerConflicts as g, isArkmeBotAvatarRef as gt, renderArkmeBundleClientBundle as h, arkmePickedFileKind as ht, invokePersistentArkmeExtension as i, ARKME_VOICEPRINT_ENROLLMENT_MIN_DURATION_MS as it, packBundleFiles as j, inspectBundleArtifact as k, __require as kt, persistentExtensionPackageIdentity as l, mergeMemberJoinEvents as lt, writePersistentExtensionActivation as m, arkmeNormalizedFileMimeType as mt, applyPersistentArkmeHostExtension as n, ARKME_VOICEPRINT_ENROLLMENT_MAX_AUDIO_BYTES as nt, persistentArkmeExtensionRuntimeState as o, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES as ot, repairPersistentExtensionInstallation as p, arkmeCanInlineLocalFile as pt, arkmeExtensionShareRefFromLink as q, deactivatePersistentArkmeExtension as r, ARKME_VOICEPRINT_ENROLLMENT_MAX_DURATION_MS as rt, materializePersistentExtensionBundle as s, applyMemberUpdate as st, activatePersistentArkmeExtension as t, ARKME_PROVIDER_CONTRACT_VERSION as tt, quarantinePersistentExtension as u, validateMemberUpdate as ut, arkmeClientContentDigestFromSource as v, sameRecordingImportIdentity as vt, ARKME_NATIVE_BUNDLE_ARTIFACT_KIND as w, ArkmePluginError as wt, verifyNativeBundleResolutionSignature as x, ArkmeOutgoingCallError as xt, verifyBundleResolutionSignature as y, MAX_RECORDING_IMPORT_BYTES as yt, ARKME_EXTENSION_FORMAT as z };