@senguoyun/dsh-arkme 0.1.46 → 0.1.48

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 (163) 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/plugin-release.md +9 -3
  5. package/docs/shared-conversation-members.md +61 -0
  6. package/docs/sidebar-local-first-review.md +104 -0
  7. package/docs/sidebar-local-first.md +61 -0
  8. package/lib/bundle-runtime.js +1 -1
  9. package/lib/client.js +68477 -66624
  10. package/lib/client.js.map +1 -1
  11. package/lib/extension-profile-restart-helper.js +5 -5
  12. package/lib/harness-model-client.js +36 -2
  13. package/lib/harness-session-client.js +124 -0
  14. package/lib/index.js +2837 -1532
  15. package/lib/{install-store-C1tAM346.js → install-store-bnyQDtYt.js} +1 -1
  16. package/lib/persistent-extension.js +1 -1
  17. package/lib/{persistent-runtime-DpNP9UKr.js → persistent-runtime-BOm3JEDw.js} +1573 -277
  18. package/lib/{plugin-updater-helper-BByro9U_.js → plugin-updater-helper-B0yVav4f.js} +1 -1
  19. package/lib/{plugin-updater-helper-BByw0ZyG.js → plugin-updater-helper-BbjKUV9T.js} +3 -3
  20. package/lib/plugin-updater-helper.js +1 -1
  21. package/lib/sdk.js +36 -2
  22. package/lib/types/arkme-service.d.ts +14 -0
  23. package/lib/types/arkme-service.d.ts.map +1 -1
  24. package/lib/types/chat-realtime.d.ts +6 -3
  25. package/lib/types/chat-realtime.d.ts.map +1 -1
  26. package/lib/types/client/ArkmeAvatar.d.ts +2 -1
  27. package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
  28. package/lib/types/client/ArkmeContactAddSurface.d.ts +2 -1
  29. package/lib/types/client/ArkmeContactAddSurface.d.ts.map +1 -1
  30. package/lib/types/client/ArkmeDSHBetaCommunityEntry.d.ts.map +1 -1
  31. package/lib/types/client/ArkmeDirectoryWindow.d.ts +6 -0
  32. package/lib/types/client/ArkmeDirectoryWindow.d.ts.map +1 -0
  33. package/lib/types/client/ArkmeGroupChatControls.d.ts +1 -1
  34. package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
  35. package/lib/types/client/ArkmeMessageReadReceipt.d.ts.map +1 -1
  36. package/lib/types/client/ArkmePersistentShell.d.ts +1 -1
  37. package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
  38. package/lib/types/client/ArkmeRecordingSurface.d.ts +3 -1
  39. package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
  40. package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
  41. package/lib/types/client/ArkmeVirtualWorkspace.d.ts +2 -1
  42. package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
  43. package/lib/types/client/DeepSeekHarnessSurface.d.ts +3 -1
  44. package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -1
  45. package/lib/types/client/api.d.ts +1 -1
  46. package/lib/types/client/api.d.ts.map +1 -1
  47. package/lib/types/client/avatar-image-store.d.ts +3 -2
  48. package/lib/types/client/avatar-image-store.d.ts.map +1 -1
  49. package/lib/types/client/bot-chat-directory-projection.d.ts +2 -0
  50. package/lib/types/client/bot-chat-directory-projection.d.ts.map +1 -1
  51. package/lib/types/client/bot-directory-preferences.d.ts +2 -0
  52. package/lib/types/client/bot-directory-preferences.d.ts.map +1 -1
  53. package/lib/types/client/chat-directory-store.d.ts +4 -0
  54. package/lib/types/client/chat-directory-store.d.ts.map +1 -1
  55. package/lib/types/client/conversation-members-store.d.ts +46 -0
  56. package/lib/types/client/conversation-members-store.d.ts.map +1 -0
  57. package/lib/types/client/harness-session-client.d.ts +5 -0
  58. package/lib/types/client/harness-session-client.d.ts.map +1 -0
  59. package/lib/types/client/interwoven-moments.d.ts +9 -0
  60. package/lib/types/client/interwoven-moments.d.ts.map +1 -1
  61. package/lib/types/client/message-read-receipt-store.d.ts +7 -1
  62. package/lib/types/client/message-read-receipt-store.d.ts.map +1 -1
  63. package/lib/types/client/realtime-client-events.d.ts.map +1 -1
  64. package/lib/types/client/recordings/ArkmeRecordingImportDialog.d.ts +4 -1
  65. package/lib/types/client/recordings/ArkmeRecordingImportDialog.d.ts.map +1 -1
  66. package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts +3 -2
  67. package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts.map +1 -1
  68. package/lib/types/client/redesign/contacts/ContactDirectoryAddDialog.d.ts +8 -0
  69. package/lib/types/client/redesign/contacts/ContactDirectoryAddDialog.d.ts.map +1 -0
  70. package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts +13 -4
  71. package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts.map +1 -1
  72. package/lib/types/client/redesign/contacts/ContactDirectoryToolbar.d.ts +7 -0
  73. package/lib/types/client/redesign/contacts/ContactDirectoryToolbar.d.ts.map +1 -0
  74. package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts +12 -2
  75. package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts.map +1 -1
  76. package/lib/types/client/redesign/contacts/ContactRemarkDialog.d.ts +9 -0
  77. package/lib/types/client/redesign/contacts/ContactRemarkDialog.d.ts.map +1 -0
  78. package/lib/types/client/redesign/contacts/ContactWorldList.d.ts +2 -0
  79. package/lib/types/client/redesign/contacts/ContactWorldList.d.ts.map +1 -1
  80. package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts +3 -2
  81. package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts.map +1 -1
  82. package/lib/types/client/redesign/contacts/contact-date.d.ts +3 -0
  83. package/lib/types/client/redesign/contacts/contact-date.d.ts.map +1 -0
  84. package/lib/types/client/redesign/contacts/contact-directory-search.d.ts +10 -0
  85. package/lib/types/client/redesign/contacts/contact-directory-search.d.ts.map +1 -0
  86. package/lib/types/client/redesign/contacts/contact-directory-state.d.ts +4 -1
  87. package/lib/types/client/redesign/contacts/contact-directory-state.d.ts.map +1 -1
  88. package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts +6 -2
  89. package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts.map +1 -1
  90. package/lib/types/client/use-arkme-avatar-image.d.ts +2 -1
  91. package/lib/types/client/use-arkme-avatar-image.d.ts.map +1 -1
  92. package/lib/types/client/use-conversation-members.d.ts +3 -0
  93. package/lib/types/client/use-conversation-members.d.ts.map +1 -0
  94. package/lib/types/dsh-remote/api-proxy-adapter.d.ts +1 -0
  95. package/lib/types/dsh-remote/api-proxy-adapter.d.ts.map +1 -1
  96. package/lib/types/dsh-remote/channel-manager.d.ts +9 -2
  97. package/lib/types/dsh-remote/channel-manager.d.ts.map +1 -1
  98. package/lib/types/dsh-remote/connection-diagnostics.d.ts +35 -0
  99. package/lib/types/dsh-remote/connection-diagnostics.d.ts.map +1 -0
  100. package/lib/types/dsh-remote/current-session-tool.d.ts +3 -0
  101. package/lib/types/dsh-remote/current-session-tool.d.ts.map +1 -0
  102. package/lib/types/dsh-remote/default-socket-factory.d.ts.map +1 -1
  103. package/lib/types/dsh-remote/desktop-session-presence.d.ts +21 -0
  104. package/lib/types/dsh-remote/desktop-session-presence.d.ts.map +1 -0
  105. package/lib/types/dsh-remote/dsh-event-contract.d.ts +2 -0
  106. package/lib/types/dsh-remote/dsh-event-contract.d.ts.map +1 -1
  107. package/lib/types/dsh-remote/host.d.ts +40 -6
  108. package/lib/types/dsh-remote/host.d.ts.map +1 -1
  109. package/lib/types/dsh-remote/live-event-batcher.d.ts +46 -0
  110. package/lib/types/dsh-remote/live-event-batcher.d.ts.map +1 -0
  111. package/lib/types/dsh-remote/protocol-v1.d.ts.map +1 -1
  112. package/lib/types/dsh-remote/realtime-transport.d.ts +19 -1
  113. package/lib/types/dsh-remote/realtime-transport.d.ts.map +1 -1
  114. package/lib/types/dsh-remote/types.d.ts +18 -2
  115. package/lib/types/dsh-remote/types.d.ts.map +1 -1
  116. package/lib/types/harness-embed-contract.d.ts +2 -0
  117. package/lib/types/harness-embed-contract.d.ts.map +1 -1
  118. package/lib/types/harness-embed-route.d.ts +4 -0
  119. package/lib/types/harness-embed-route.d.ts.map +1 -1
  120. package/lib/types/host-api.d.ts.map +1 -1
  121. package/lib/types/index.d.ts +1 -0
  122. package/lib/types/index.d.ts.map +1 -1
  123. package/lib/types/local-database.d.ts +11 -1
  124. package/lib/types/local-database.d.ts.map +1 -1
  125. package/lib/types/member-directory.d.ts +11 -0
  126. package/lib/types/member-directory.d.ts.map +1 -0
  127. package/lib/types/sdk/index.d.ts +24 -3
  128. package/lib/types/sdk/index.d.ts.map +1 -1
  129. package/lib/types/services/bot-service.d.ts +3 -0
  130. package/lib/types/services/bot-service.d.ts.map +1 -1
  131. package/lib/types/services/chat-realtime-service.d.ts +1 -0
  132. package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
  133. package/lib/types/services/chat-service.d.ts +14 -1
  134. package/lib/types/services/chat-service.d.ts.map +1 -1
  135. package/lib/types/services/contact-directory-service.d.ts +3 -0
  136. package/lib/types/services/contact-directory-service.d.ts.map +1 -1
  137. package/lib/types/services/conversation-directory-service.d.ts +72 -0
  138. package/lib/types/services/conversation-directory-service.d.ts.map +1 -0
  139. package/lib/types/services/desktop-attention-bridge.d.ts +6 -1
  140. package/lib/types/services/desktop-attention-bridge.d.ts.map +1 -1
  141. package/lib/types/services/group-service.d.ts.map +1 -1
  142. package/lib/types/services/media-service.d.ts +1 -0
  143. package/lib/types/services/media-service.d.ts.map +1 -1
  144. package/lib/types/services/service.d.ts +11 -0
  145. package/lib/types/services/service.d.ts.map +1 -1
  146. package/lib/types/services/source-service.d.ts +3 -0
  147. package/lib/types/services/source-service.d.ts.map +1 -1
  148. package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
  149. package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
  150. package/lib/types/tools/business/conversation/list-sources.d.ts +1 -0
  151. package/lib/types/tools/business/conversation/list-sources.d.ts.map +1 -1
  152. package/lib/types/tools/business/conversation/member-pages.d.ts +2 -0
  153. package/lib/types/tools/business/conversation/member-pages.d.ts.map +1 -0
  154. package/lib/types/tools/business/conversation/read-receipts.d.ts.map +1 -1
  155. package/lib/types/tools/business/index.d.ts.map +1 -1
  156. package/lib/types/tools/ports/conversations.d.ts +13 -1
  157. package/lib/types/tools/ports/conversations.d.ts.map +1 -1
  158. package/lib/types/tools/registry/registrar.d.ts.map +1 -1
  159. package/lib/types/types.d.ts +66 -2
  160. package/lib/types/types.d.ts.map +1 -1
  161. package/package.json +7 -6
  162. package/lib/{bundle-runtime-BTzRlc6V.js → bundle-runtime-CE73h3vS.js} +1 -1
  163. package/lib/{profile-bundle-policy-CvojZuTD.js → profile-bundle-policy-DLXDyohJ.js} +1 -1
@@ -1,9 +1,11 @@
1
+ import { defineTool } from "@deepseek-ai/dsh-tools";
1
2
  import { chmodSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import { chmod as chmod$1, copyFile, link, mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
2
5
  import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicKey, randomBytes, randomUUID, timingSafeEqual, verify } from "node:crypto";
3
6
  import minpath, { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
7
  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";
8
+ import { gunzipSync, gzipSync } from "node:zlib";
7
9
  import minproc from "node:process";
8
10
  import { Resolver, lookup } from "node:dns/promises";
9
11
  import { request } from "node:http";
@@ -11,10 +13,8 @@ import { request as request$1 } from "node:https";
11
13
  import { BlockList, isIP } from "node:net";
12
14
  import OSS from "ali-oss";
13
15
  import { Readable } from "node:stream";
14
- import { gunzipSync, gzipSync } from "node:zlib";
15
16
  import { createContext, runInContext } from "node:vm";
16
17
  import { Context } from "@deepseek-ai/cordis";
17
- import { defineTool } from "@deepseek-ai/dsh-tools";
18
18
  import { parseDocument } from "yaml";
19
19
  //#region \0rolldown/runtime.js
20
20
  var __create = Object.create;
@@ -48,14 +48,255 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
48
48
  enumerable: true
49
49
  }) : target, mod));
50
50
  //#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 "你们已互相拒收消息";
51
+ //#region src/services/desktop-attention-bridge.ts
52
+ const DESKTOP_BRIDGE_TIMEOUT_MS = 2e3;
53
+ const DESKTOP_NOTIFICATION_RETRY_MS = 250;
54
+ const MAX_NOTIFICATION_TITLE_LENGTH = 128;
55
+ const MAX_NOTIFICATION_BODY_LENGTH = 512;
56
+ const MAX_NATIVE_BADGE_COUNT = 999999;
57
+ function nonEmptyBounded(value, maxLength) {
58
+ const normalized = value?.trim() ?? "";
59
+ return normalized !== "" && normalized.length <= maxLength ? normalized : void 0;
60
+ }
61
+ function isLoopbackHostname(hostname) {
62
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
63
+ }
64
+ /**
65
+ * Resolve the native bridge only when the complete, client-injected contract is
66
+ * present. Partial or non-loopback configuration is rejected as unavailable;
67
+ * no credential is ever sent to a caller-controlled origin.
68
+ */
69
+ function arkmeDesktopBridgeConfigFromEnv(env) {
70
+ const rawEndpoint = env.ARKME_DESKTOP_BRIDGE_URL?.trim() ?? "";
71
+ const token = nonEmptyBounded(env.ARKME_DESKTOP_BRIDGE_TOKEN, 512);
72
+ const sessionId = nonEmptyBounded(env.ARKME_DESKTOP_BRIDGE_SESSION_ID, 160);
73
+ if (rawEndpoint === "" && token === void 0 && sessionId === void 0) return void 0;
74
+ 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;
75
+ let endpoint;
76
+ try {
77
+ endpoint = new URL(rawEndpoint);
78
+ } catch {
79
+ return;
58
80
  }
81
+ if (endpoint.protocol !== "http:" || !isLoopbackHostname(endpoint.hostname) || endpoint.port === "" || endpoint.username !== "" || endpoint.password !== "" || endpoint.pathname !== "/v1/actions" || endpoint.search !== "" || endpoint.hash !== "") return void 0;
82
+ return {
83
+ endpoint: endpoint.toString(),
84
+ token,
85
+ sessionId
86
+ };
87
+ }
88
+ function responseObject(value) {
89
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
90
+ }
91
+ async function requestArkmeDesktopBridge(config, fetchImpl, action, payload) {
92
+ const response = await fetchImpl(config.endpoint, {
93
+ method: "POST",
94
+ headers: {
95
+ authorization: `Bearer ${config.token}`,
96
+ "content-type": "application/json"
97
+ },
98
+ body: JSON.stringify({
99
+ schemaVersion: 1,
100
+ sessionId: config.sessionId,
101
+ action,
102
+ payload
103
+ }),
104
+ signal: AbortSignal.timeout(DESKTOP_BRIDGE_TIMEOUT_MS)
105
+ });
106
+ if (!response.ok) throw new Error(`desktop bridge returned HTTP ${String(response.status)}`);
107
+ const body = responseObject(await response.json());
108
+ const value = responseObject(body?.value);
109
+ if (body?.ok !== true || value === void 0) throw new Error("desktop bridge returned an invalid response");
110
+ return value;
111
+ }
112
+ function outcomeFromValue(value) {
113
+ if (typeof value.accepted !== "boolean") return void 0;
114
+ 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;
115
+ }
116
+ var ArkmeDesktopAttentionBridge = class {
117
+ config;
118
+ fetchImpl;
119
+ capabilities;
120
+ badgeRevision = 0;
121
+ badgeSourceRevision = 0;
122
+ badgeGeneration;
123
+ lastBadgeCount;
124
+ badgeMutationTail = Promise.resolve();
125
+ constructor(config = arkmeDesktopBridgeConfigFromEnv(process.env), fetchImpl = fetch, badgeGeneration = Number(process.hrtime.bigint() / 1000n)) {
126
+ this.config = config;
127
+ this.fetchImpl = fetchImpl;
128
+ this.badgeGeneration = Number.isSafeInteger(badgeGeneration) && badgeGeneration > 0 ? badgeGeneration : Math.max(1, Date.now() * 1e3);
129
+ }
130
+ available() {
131
+ return this.config !== void 0;
132
+ }
133
+ async showNotification(payload) {
134
+ const capabilities = await this.readCapabilities(true);
135
+ if (capabilities?.notificationShow !== true) return {
136
+ fallbackToBrowser: true,
137
+ outcome: capabilities === void 0 ? "unavailable" : "unsupported"
138
+ };
139
+ const boundedPayload = {
140
+ ...payload,
141
+ presentation: {
142
+ title: payload.presentation.title.trim().slice(0, MAX_NOTIFICATION_TITLE_LENGTH),
143
+ body: payload.presentation.body.trim().slice(0, MAX_NOTIFICATION_BODY_LENGTH)
144
+ }
145
+ };
146
+ for (let attempt = 0; attempt < 2; attempt += 1) {
147
+ try {
148
+ const outcome = outcomeFromValue(await this.request("notification.show", boundedPayload)) ?? "native-failed";
149
+ if (outcome !== "native-failed" || attempt > 0) return {
150
+ fallbackToBrowser: false,
151
+ outcome
152
+ };
153
+ } catch {
154
+ if (attempt > 0) return {
155
+ fallbackToBrowser: false,
156
+ outcome: "native-failed"
157
+ };
158
+ }
159
+ await new Promise((resolve) => {
160
+ setTimeout(resolve, DESKTOP_NOTIFICATION_RETRY_MS);
161
+ });
162
+ }
163
+ return {
164
+ fallbackToBrowser: false,
165
+ outcome: "native-failed"
166
+ };
167
+ }
168
+ async applyBadgeSummary(summary) {
169
+ const count = Math.min(MAX_NATIVE_BADGE_COUNT, Math.max(0, Math.trunc(Number.isFinite(summary.count) ? summary.count : 0)));
170
+ const revision = Math.trunc(summary.revision);
171
+ if (!Number.isSafeInteger(revision) || revision <= 0) return false;
172
+ const pending = this.badgeMutationTail.then(async () => await this.applyBadgeSnapshotSerial(count, revision));
173
+ this.badgeMutationTail = pending.then(() => void 0, () => void 0);
174
+ return await pending;
175
+ }
176
+ async resetBadgeCount() {
177
+ const pending = this.badgeMutationTail.then(async () => {
178
+ this.badgeGeneration += 1;
179
+ this.badgeRevision = 0;
180
+ this.badgeSourceRevision = 0;
181
+ this.lastBadgeCount = void 0;
182
+ for (let attempt = 0; attempt < 3; attempt += 1) if (await this.applyBadgeSnapshotSerial(0, 1)) return true;
183
+ return false;
184
+ });
185
+ this.badgeMutationTail = pending.then(() => void 0, () => void 0);
186
+ return await pending;
187
+ }
188
+ async applyBadgeSnapshotSerial(count, sourceRevision) {
189
+ if (sourceRevision < this.badgeSourceRevision || sourceRevision === this.badgeSourceRevision && count === this.lastBadgeCount) return true;
190
+ const capabilities = await this.readCapabilities();
191
+ if (capabilities === void 0) return this.config === void 0;
192
+ if (capabilities.badgeApplySnapshot.mode === "unsupported") return true;
193
+ const revision = Math.max(sourceRevision, this.badgeRevision + 1);
194
+ try {
195
+ const outcome = outcomeFromValue(await this.request("badge.applySnapshot", {
196
+ generation: this.badgeGeneration,
197
+ revision,
198
+ count
199
+ }));
200
+ if (outcome !== "accepted" && outcome !== "duplicate") return false;
201
+ this.badgeRevision = revision;
202
+ this.badgeSourceRevision = sourceRevision;
203
+ this.lastBadgeCount = count;
204
+ return true;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+ async readCapabilities(refresh = false) {
210
+ if (this.config === void 0) return void 0;
211
+ if (!refresh && this.capabilities !== void 0) return this.capabilities;
212
+ try {
213
+ const value = await this.request("capabilities.get", {});
214
+ if (value.schemaVersion !== 1 || value.sessionId !== this.config.sessionId) return void 0;
215
+ const capabilities = responseObject(value.capabilities);
216
+ const mode = responseObject(capabilities?.badgeApplySnapshot)?.mode;
217
+ if (capabilities?.notificationShow !== true && capabilities?.notificationShow !== false) return void 0;
218
+ if (mode !== "count" && mode !== "dot" && mode !== "unsupported") return void 0;
219
+ this.capabilities = {
220
+ notificationShow: capabilities.notificationShow,
221
+ badgeApplySnapshot: { mode }
222
+ };
223
+ return this.capabilities;
224
+ } catch {
225
+ return;
226
+ }
227
+ }
228
+ async request(action, payload) {
229
+ const config = this.config;
230
+ if (config === void 0) throw new Error("desktop bridge unavailable");
231
+ return await requestArkmeDesktopBridge(config, this.fetchImpl, action, payload);
232
+ }
233
+ };
234
+ /** Samples optional native lifecycle facts without delaying the Host session poll. */
235
+ function createDesktopLifecycleReader(fetchImpl) {
236
+ const config = arkmeDesktopBridgeConfigFromEnv(process.env);
237
+ let available;
238
+ let latest;
239
+ let flight;
240
+ let nextRead = 0;
241
+ return async () => {
242
+ if (config === void 0 || available === false || flight !== void 0 || performance.now() < nextRead) return latest;
243
+ nextRead = performance.now() + 2e3;
244
+ flight = (async () => {
245
+ if (available === void 0) {
246
+ available = responseObject(responseObject((await requestArkmeDesktopBridge(config, fetchImpl, "capabilities.get", {})).capabilities)?.lifecycle)?.version === 1;
247
+ if (!available) return;
248
+ }
249
+ const value = await requestArkmeDesktopBridge(config, fetchImpl, "lifecycle.get", {});
250
+ if (Number.isSafeInteger(value.resumeGeneration) && Number(value.resumeGeneration) >= 0 && typeof value.suspended === "boolean") latest = {
251
+ resumeGeneration: Number(value.resumeGeneration),
252
+ suspended: value.suspended
253
+ };
254
+ })().catch(() => void 0).finally(() => {
255
+ flight = void 0;
256
+ });
257
+ return latest;
258
+ };
259
+ }
260
+ //#endregion
261
+ //#region src/chat-attention.ts
262
+ function integerLikeValue$5(value) {
263
+ if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
264
+ if (typeof value !== "string" || value.trim() === "") return void 0;
265
+ const parsed = Number(value.trim());
266
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : void 0;
267
+ }
268
+ function arkmeChatPolicyNotificationDisabled(policy) {
269
+ if (policy === null || typeof policy !== "object" || Array.isArray(policy)) return void 0;
270
+ const value = policy;
271
+ const muteState = integerLikeValue$5(value.mute_state ?? value.muteState);
272
+ const notifyState = integerLikeValue$5(value.notify_state ?? value.notifyState);
273
+ if (muteState === 2 || notifyState === 2) return true;
274
+ if (muteState === 1 || notifyState === 1) return false;
275
+ }
276
+ function projectArkmeChatAttention(rawUnreadCount, policy, fallbackMuted = false) {
277
+ const unreadCount = Math.max(0, integerLikeValue$5(rawUnreadCount) ?? 0);
278
+ const isMuted = arkmeChatPolicyNotificationDisabled(policy) ?? fallbackMuted;
279
+ const notificationAllowed = !isMuted;
280
+ return {
281
+ unreadCount,
282
+ badgeUnreadCount: notificationAllowed ? unreadCount : 0,
283
+ notificationAllowed,
284
+ isMuted
285
+ };
286
+ }
287
+ function projectArkmeChatAttentionFromMuted(rawUnreadCount, isMuted) {
288
+ return projectArkmeChatAttention(rawUnreadCount, void 0, isMuted);
289
+ }
290
+ //#endregion
291
+ //#region src/chat-pin-projection.ts
292
+ /** Policy freshness is independent of message sequence and event delivery order. */
293
+ function retainNewerArkmeChatPin(current, incoming) {
294
+ 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;
295
+ return {
296
+ ...incoming,
297
+ isPinned: current.isPinned,
298
+ chatPolicyUpdatedAtMillis: current.chatPolicyUpdatedAtMillis
299
+ };
59
300
  }
60
301
  //#endregion
61
302
  //#region src/request-coordinator.ts
@@ -485,190 +726,6 @@ var ArkmeRequestCoordinator = class {
485
726
  }
486
727
  };
487
728
  //#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
729
  //#region src/account-session-owner.ts
673
730
  var ArkmeAccountSessionOwner = class {
674
731
  store;
@@ -911,6 +968,13 @@ var ServiceRuntime = class {
911
968
  stateStore;
912
969
  fetchImpl;
913
970
  pendingSessionStore;
971
+ memberCacheRevision = 0;
972
+ memberCacheEpoch() {
973
+ return this.memberCacheRevision;
974
+ }
975
+ invalidateMemberCache() {
976
+ this.memberCacheRevision += 1;
977
+ }
914
978
  requestCoordinator = new ArkmeRequestCoordinator();
915
979
  refreshInFlightByUserId = /* @__PURE__ */ new Map();
916
980
  accountSessions;
@@ -1435,6 +1499,666 @@ var ServiceRuntime = class {
1435
1499
  }
1436
1500
  };
1437
1501
  //#endregion
1502
+ //#region src/services/conversation-directory-service.ts
1503
+ const PAGE_SIZE = 20;
1504
+ const MAX_ROWS = 2e4;
1505
+ const keyOf = (source) => source.sourceKey ?? source.sourceRef;
1506
+ /** Merge pages and live changes without interpreting page absence as removal. */
1507
+ function mergeDirectorySource(previous, incoming, keepLive = false) {
1508
+ if (previous === void 0) return incoming;
1509
+ const stale = (incoming.latestSequence ?? 0) < (previous.latestSequence ?? 0) || keepLive && (incoming.latestSequence ?? 0) <= (previous.latestSequence ?? 0) && incoming.activeAtMillis <= previous.activeAtMillis;
1510
+ const merged = {
1511
+ ...previous,
1512
+ ...incoming
1513
+ };
1514
+ if (stale) {
1515
+ merged.sourceRef = previous.sourceRef;
1516
+ for (const field of [
1517
+ "latestSequence",
1518
+ "latestPreview",
1519
+ "activeAtMillis",
1520
+ "unreadCount",
1521
+ "badgeUnreadCount",
1522
+ "hasUnreadMention"
1523
+ ]) if (previous[field] !== void 0) Object.assign(merged, { [field]: previous[field] });
1524
+ }
1525
+ if (!stale && incoming.latestPreview === void 0) delete merged.latestPreview;
1526
+ const readSequence = Math.max(previous.readSequence ?? 0, incoming.readSequence ?? 0);
1527
+ if (readSequence > 0) {
1528
+ merged.readSequence = readSequence;
1529
+ if ((merged.latestSequence ?? 0) <= readSequence) merged.unreadCount = 0;
1530
+ else if ((incoming.readSequence ?? 0) < (previous.readSequence ?? 0) && (incoming.latestSequence ?? 0) <= (previous.latestSequence ?? 0)) merged.unreadCount = previous.unreadCount;
1531
+ }
1532
+ Object.assign(merged, projectArkmeChatAttentionFromMuted(merged.unreadCount, merged.isMuted === true));
1533
+ if (merged.unreadCount === 0) merged.hasUnreadMention = false;
1534
+ if (!incoming.avatarRef && previous.avatarRef !== void 0) merged.avatarRef = previous.avatarRef;
1535
+ if (!incoming.avatarRefs?.length && previous.avatarRefs !== void 0) merged.avatarRefs = previous.avatarRefs;
1536
+ if (!incoming.groupAvatar?.slots.length && previous.groupAvatar !== void 0) merged.groupAvatar = previous.groupAvatar;
1537
+ return retainNewerArkmeChatPin(previous, merged);
1538
+ }
1539
+ /** One account lifecycle owns cache restoration, a twenty-row scan, and directory deltas. */
1540
+ var ConversationDirectoryService = class {
1541
+ runtime;
1542
+ source;
1543
+ preferences;
1544
+ readBots;
1545
+ warmAvatar;
1546
+ emit;
1547
+ restoreBots;
1548
+ userId;
1549
+ generation = 0;
1550
+ revision = 0;
1551
+ cachedAtMillis = 0;
1552
+ sourceRemovals = /* @__PURE__ */ new Map();
1553
+ pendingReadAcks = /* @__PURE__ */ new Map();
1554
+ visibilityMutations = /* @__PURE__ */ new Map();
1555
+ sources = /* @__PURE__ */ new Map();
1556
+ mutations = /* @__PURE__ */ new Map();
1557
+ visibility = /* @__PURE__ */ new Map();
1558
+ bots = [];
1559
+ cachedBotKeys = /* @__PURE__ */ new Map();
1560
+ botPinnedKeys = /* @__PURE__ */ new Set();
1561
+ deletedBotRefs = /* @__PURE__ */ new Set();
1562
+ special = {};
1563
+ phase = "cached";
1564
+ error;
1565
+ restore;
1566
+ scan;
1567
+ firstPage;
1568
+ rescanRequested = false;
1569
+ rawBaselineComplete = false;
1570
+ rawBaseline;
1571
+ controller = new AbortController();
1572
+ persistence = Promise.resolve();
1573
+ diskPending = /* @__PURE__ */ new Map();
1574
+ diskWriting = false;
1575
+ cacheFailure;
1576
+ lastPublished = "";
1577
+ avatars = /* @__PURE__ */ new Map();
1578
+ avatarWork;
1579
+ constructor(runtime, source, preferences, readBots, warmAvatar, emit, restoreBots = async (items) => items) {
1580
+ this.runtime = runtime;
1581
+ this.source = source;
1582
+ this.preferences = preferences;
1583
+ this.readBots = readBots;
1584
+ this.warmAvatar = warmAvatar;
1585
+ this.emit = emit;
1586
+ this.restoreBots = restoreBots;
1587
+ }
1588
+ reset() {
1589
+ this.controller.abort();
1590
+ this.controller = new AbortController();
1591
+ this.generation++;
1592
+ this.userId = void 0;
1593
+ this.sourceRemovals.clear();
1594
+ this.pendingReadAcks.clear();
1595
+ this.sources.clear();
1596
+ this.mutations.clear();
1597
+ this.visibility.clear();
1598
+ this.visibilityMutations.clear();
1599
+ this.avatars.clear();
1600
+ this.bots = [];
1601
+ this.cachedBotKeys.clear();
1602
+ this.botPinnedKeys.clear();
1603
+ this.deletedBotRefs.clear();
1604
+ this.special = {};
1605
+ this.restore = void 0;
1606
+ this.scan = void 0;
1607
+ this.firstPage = void 0;
1608
+ this.rawBaseline = void 0;
1609
+ this.avatarWork = void 0;
1610
+ this.phase = "cached";
1611
+ this.error = void 0;
1612
+ this.lastPublished = "";
1613
+ this.cachedAtMillis = 0;
1614
+ this.rescanRequested = false;
1615
+ this.rawBaselineComplete = false;
1616
+ }
1617
+ async activate() {
1618
+ const { userId } = await this.runtime.requireSession();
1619
+ if (userId !== this.userId) {
1620
+ this.reset();
1621
+ this.userId = userId;
1622
+ const generation = this.generation;
1623
+ this.restore = (async () => {
1624
+ const cached = await this.runtime.stateStore.readDirectoryCache?.(userId);
1625
+ if (generation !== this.generation || cached === void 0) return;
1626
+ if (cached.items[0] !== void 0) await this.source.openSourceRef(cached.items[0].sourceRef, userId);
1627
+ if (generation !== this.generation) return;
1628
+ for (const item of cached.items) this.sources.set(keyOf(item), item);
1629
+ this.cachedAtMillis = cached.projection?.cachedAtMillis ?? 0;
1630
+ for (const item of cached.projection?.visibility ?? []) this.visibility.set(`${item.entryKind}:${item.entryRef}`, item);
1631
+ const cachedBots = cached.projection?.bots ?? [];
1632
+ const bots = await this.restoreBots(cachedBots, userId);
1633
+ if (generation !== this.generation) return;
1634
+ this.bots = bots;
1635
+ for (const bot of bots) {
1636
+ if (bot.directoryKey !== void 0) this.cachedBotKeys.set(bot.botRef, bot.directoryKey);
1637
+ const previous = cachedBots.find((item) => item.directoryKey === bot.directoryKey);
1638
+ if (previous === void 0 || previous.botRef === bot.botRef) continue;
1639
+ const visibility = this.visibility.get(`bot:${previous.botRef}`);
1640
+ this.visibility.delete(`bot:${previous.botRef}`);
1641
+ if (visibility !== void 0) this.visibility.set(`bot:${bot.botRef}`, {
1642
+ ...visibility,
1643
+ entryRef: bot.botRef
1644
+ });
1645
+ }
1646
+ this.botPinnedKeys = new Set(cached.projection?.botPinnedKeys ?? []);
1647
+ this.deletedBotRefs = new Set(cached.projection?.removedBotRefs ?? []);
1648
+ this.special = {
1649
+ ...cached.projection?.sendToSelf === void 0 ? {} : { sendToSelf: cached.projection.sendToSelf },
1650
+ ...cached.projection?.arkoProfile === void 0 ? {} : { arkoProfile: cached.projection.arkoProfile },
1651
+ ...cached.projection?.arkoPreview === void 0 ? {} : { arkoPreview: cached.projection.arkoPreview }
1652
+ };
1653
+ this.revision = Math.max(this.revision, cached.projection?.revision ?? 0);
1654
+ for (const key of cached.projection?.removedSourceKeys ?? []) this.sourceRemovals.set(key, this.revision);
1655
+ })().catch(() => {});
1656
+ }
1657
+ await this.restore;
1658
+ }
1659
+ async read(force = false, signal) {
1660
+ signal?.throwIfAborted();
1661
+ const pending = this.readSnapshot(force);
1662
+ if (signal === void 0) return await pending;
1663
+ return await new Promise((resolve, reject) => {
1664
+ const abort = () => {
1665
+ reject(signal.reason);
1666
+ };
1667
+ signal.addEventListener("abort", abort, { once: true });
1668
+ pending.then(resolve, reject).finally(() => {
1669
+ signal.removeEventListener("abort", abort);
1670
+ });
1671
+ });
1672
+ }
1673
+ async readSnapshot(force) {
1674
+ await this.activate();
1675
+ const generation = this.generation;
1676
+ const cached = this.snapshot();
1677
+ const hasCache = cached.items.length > 0 || cached.projection.bots.length > 0;
1678
+ if (this.scan === void 0 && (force || this.phase === "cached" || this.phase === "failed")) this.startScan();
1679
+ if (force && this.scan !== void 0 && cached.projection?.phase === "syncing") this.rescanRequested = true;
1680
+ if (hasCache) return structuredClone(cached);
1681
+ await this.firstPage;
1682
+ if (generation !== this.generation) throw new ArkmePluginError("login-context-changed", "账号已切换", false, 409);
1683
+ return structuredClone(this.snapshot());
1684
+ }
1685
+ /** Notification callers join the same scan, but only accept a complete current connection baseline. */
1686
+ async complete() {
1687
+ await this.activate();
1688
+ if (this.rawBaselineComplete && this.scan !== void 0) await this.scan.catch(() => void 0);
1689
+ await this.activate();
1690
+ if (this.scan === void 0) this.startScan();
1691
+ return structuredClone(await this.rawBaseline);
1692
+ }
1693
+ async settled() {
1694
+ while (this.scan !== void 0) await this.scan;
1695
+ while (this.avatarWork !== void 0) await this.avatarWork;
1696
+ while (this.diskWriting) await this.persistence;
1697
+ }
1698
+ startScan() {
1699
+ const generation = this.generation;
1700
+ const userId = this.userId;
1701
+ const signal = this.controller.signal;
1702
+ this.phase = "loading";
1703
+ this.error = void 0;
1704
+ this.rawBaselineComplete = false;
1705
+ let ready;
1706
+ let failed;
1707
+ this.firstPage = new Promise((resolve, reject) => {
1708
+ ready = resolve;
1709
+ failed = reject;
1710
+ });
1711
+ let baselineReady;
1712
+ let baselineFailed;
1713
+ this.rawBaseline = new Promise((resolve, reject) => {
1714
+ baselineReady = resolve;
1715
+ baselineFailed = reject;
1716
+ });
1717
+ this.rawBaseline.catch(() => void 0);
1718
+ const rawSources = /* @__PURE__ */ new Map();
1719
+ this.firstPage.catch(() => void 0);
1720
+ const pending = (async () => {
1721
+ let cursor;
1722
+ const visited = /* @__PURE__ */ new Set();
1723
+ let first = true;
1724
+ do {
1725
+ signal.throwIfAborted();
1726
+ if (visited.size >= MAX_ROWS / PAGE_SIZE) throw new Error("Directory page budget exceeded; synchronization incomplete");
1727
+ const atRevision = this.revision;
1728
+ const page = await this.source.listSources("root", {
1729
+ limit: PAGE_SIZE,
1730
+ refresh: true,
1731
+ firstPaint: true,
1732
+ ...cursor === void 0 ? {} : { cursor },
1733
+ signal
1734
+ });
1735
+ if (generation !== this.generation || (await this.runtime.requireSession()).userId !== userId) throw new DOMException("Account changed", "AbortError");
1736
+ if (page.hasMore && (page.nextCursor === void 0 || visited.has(page.nextCursor))) throw new ArkmePluginError("directory-cursor-invalid", "会话目录分页未完成:游标无效", true, 502);
1737
+ for (const item of page.items) rawSources.set(keyOf(item), item);
1738
+ if (!page.hasMore) {
1739
+ this.rawBaselineComplete = true;
1740
+ baselineReady({
1741
+ directory: "root",
1742
+ items: [...rawSources.values()],
1743
+ hasMore: false
1744
+ });
1745
+ }
1746
+ const visibleCandidates = page.items.map((item) => mergeDirectorySource(this.sources.get(keyOf(item)), item, (this.mutations.get(keyOf(item)) ?? 0) > atRevision));
1747
+ const visibility = await this.preferences.query(visibleCandidates.map((item) => item.sourceRef), [], signal).catch((error) => {
1748
+ if (signal.aborted || error instanceof ArkmePluginError && !error.retryable && [
1749
+ 401,
1750
+ 403,
1751
+ 409
1752
+ ].includes(error.httpStatus)) throw error;
1753
+ return { items: visibleCandidates.map((item) => ({
1754
+ entryKind: "source",
1755
+ entryRef: item.sourceRef,
1756
+ hidden: this.visibility.get(`source:${item.sourceRef}`)?.hidden ?? false
1757
+ })) };
1758
+ });
1759
+ signal.throwIfAborted();
1760
+ this.phase = "syncing";
1761
+ this.apply(page.items, visibility.items, atRevision);
1762
+ if (first) {
1763
+ first = false;
1764
+ ready();
1765
+ }
1766
+ for (const item of page.items) this.avatars.set(keyOf(item), item);
1767
+ this.startAvatars(generation);
1768
+ if (!page.hasMore) break;
1769
+ cursor = page.nextCursor;
1770
+ visited.add(cursor);
1771
+ await new Promise((resolve) => {
1772
+ setTimeout(resolve, 0);
1773
+ });
1774
+ } while (true);
1775
+ const bots = await this.readBots(signal);
1776
+ if (generation !== this.generation) return;
1777
+ await this.rememberBots(bots.items, userId);
1778
+ if (generation !== this.generation) return;
1779
+ this.phase = "complete";
1780
+ this.apply([]);
1781
+ const refs = new Set(this.bots.flatMap((bot) => bot.avatarRef ? [bot.avatarRef] : []));
1782
+ const previousAvatarWork = this.avatarWork;
1783
+ const avatarWork = (async () => {
1784
+ await previousAvatarWork;
1785
+ for (const ref of refs) {
1786
+ signal.throwIfAborted();
1787
+ await this.warmAvatar(ref, signal).catch(() => void 0);
1788
+ }
1789
+ })().catch(() => void 0).finally(() => {
1790
+ if (this.avatarWork !== avatarWork) return;
1791
+ this.avatarWork = void 0;
1792
+ if (generation === this.generation && this.avatars.size > 0) this.startAvatars(generation);
1793
+ });
1794
+ this.avatarWork = avatarWork;
1795
+ })().catch((error) => {
1796
+ if (generation === this.generation) {
1797
+ this.phase = "failed";
1798
+ this.error = error instanceof Error ? error.message : "目录同步失败";
1799
+ this.publish([]);
1800
+ }
1801
+ failed(error);
1802
+ baselineFailed(error);
1803
+ throw error;
1804
+ }).finally(() => {
1805
+ if (this.scan !== pending) return;
1806
+ this.scan = void 0;
1807
+ if (this.rescanRequested) {
1808
+ this.rescanRequested = false;
1809
+ this.startScan();
1810
+ }
1811
+ });
1812
+ this.scan = pending;
1813
+ pending.catch(() => void 0);
1814
+ }
1815
+ apply(items, visibility = [], atRevision = this.revision, authoritativeAvatars = false) {
1816
+ const changed = [];
1817
+ for (const item of items) {
1818
+ const key = keyOf(item);
1819
+ if ((this.sourceRemovals.get(key) ?? 0) > atRevision) continue;
1820
+ this.sourceRemovals.delete(key);
1821
+ const previous = this.sources.get(key);
1822
+ const ack = this.pendingReadAcks.get(key) ?? this.pendingReadAcks.get(item.sourceRef);
1823
+ const merged = mergeDirectorySource(previous, ack === void 0 ? item : {
1824
+ ...item,
1825
+ readSequence: Math.max(item.readSequence ?? 0, ack.effectiveReadSequence),
1826
+ ...(item.latestSequence ?? 0) <= ack.effectiveReadSequence ? { unreadCount: ack.unreadCount } : {}
1827
+ }, (this.mutations.get(key) ?? 0) > atRevision);
1828
+ if (authoritativeAvatars) {
1829
+ merged.avatarRef = item.avatarRef ?? "";
1830
+ merged.avatarRefs = item.avatarRefs ?? [];
1831
+ if (item.groupAvatar === void 0) delete merged.groupAvatar;
1832
+ else merged.groupAvatar = item.groupAvatar;
1833
+ }
1834
+ this.pendingReadAcks.delete(key);
1835
+ this.pendingReadAcks.delete(item.sourceRef);
1836
+ if (JSON.stringify(previous) === JSON.stringify(merged)) continue;
1837
+ if (previous === void 0 && this.sources.size >= MAX_ROWS) throw new Error("Conversation directory capacity exceeded; scan incomplete");
1838
+ if (previous !== void 0 && previous.sourceRef !== merged.sourceRef) {
1839
+ this.visibility.delete(`source:${previous.sourceRef}`);
1840
+ this.visibilityMutations.delete(`source:${previous.sourceRef}`);
1841
+ }
1842
+ this.sources.set(key, merged);
1843
+ this.mutations.set(key, this.revision + 1);
1844
+ changed.push(merged);
1845
+ }
1846
+ const currentRefs = new Set(items.flatMap((item) => {
1847
+ const source = this.sources.get(keyOf(item));
1848
+ return source === void 0 ? [] : [source.sourceRef];
1849
+ }));
1850
+ const acceptedVisibility = visibility.filter((entry) => (items.length === 0 || entry.entryKind !== "source" || currentRefs.has(entry.entryRef)) && (this.visibilityMutations.get(`${entry.entryKind}:${entry.entryRef}`) ?? 0) <= atRevision);
1851
+ for (const entry of acceptedVisibility) {
1852
+ const key = `${entry.entryKind}:${entry.entryRef}`;
1853
+ this.visibility.set(key, entry);
1854
+ this.visibilityMutations.set(key, this.revision + 1);
1855
+ }
1856
+ this.publish(changed, acceptedVisibility);
1857
+ }
1858
+ snapshot(items = [...this.sources.values()], visibility = [...this.visibility.values()]) {
1859
+ return {
1860
+ directory: "root",
1861
+ items,
1862
+ hasMore: this.phase !== "complete",
1863
+ projection: {
1864
+ ...this.special,
1865
+ removedSourceKeys: [...this.sourceRemovals.keys()],
1866
+ removedBotRefs: [...this.deletedBotRefs],
1867
+ botPinnedKeys: [...this.botPinnedKeys],
1868
+ revision: this.revision,
1869
+ phase: this.phase,
1870
+ cachedAtMillis: this.cachedAtMillis,
1871
+ visibility,
1872
+ bots: this.bots,
1873
+ ...this.error === void 0 ? {} : { error: this.error }
1874
+ }
1875
+ };
1876
+ }
1877
+ publish(items, visibility = []) {
1878
+ const fingerprint = JSON.stringify({
1879
+ items,
1880
+ visibility,
1881
+ phase: this.phase,
1882
+ error: this.error,
1883
+ bots: this.bots,
1884
+ special: this.special,
1885
+ pins: [...this.botPinnedKeys],
1886
+ removedBots: [...this.deletedBotRefs],
1887
+ removedSources: [...this.sourceRemovals.keys()]
1888
+ });
1889
+ if (fingerprint === this.lastPublished) return this.cacheFailure === void 0;
1890
+ this.lastPublished = fingerprint;
1891
+ this.revision++;
1892
+ this.cachedAtMillis = Date.now();
1893
+ const page = this.snapshot(items, visibility);
1894
+ const userId = this.userId;
1895
+ this.emit(structuredClone(page));
1896
+ if (!this.diskPending.has(userId) && this.diskPending.size >= 8) {
1897
+ this.cacheFailure = /* @__PURE__ */ new Error("Directory cache pending account capacity exceeded");
1898
+ console.warn("dsh-arkme: directory_cache_queue_full");
1899
+ return false;
1900
+ }
1901
+ const queued = this.diskPending.get(userId);
1902
+ const rows = new Map((queued?.items ?? []).map((item) => [keyOf(item), item]));
1903
+ for (const item of page.items) rows.set(keyOf(item), item);
1904
+ const hidden = new Map((queued?.projection?.visibility ?? []).map((item) => [`${item.entryKind}:${item.entryRef}`, item]));
1905
+ for (const item of page.projection.visibility) hidden.set(`${item.entryKind}:${item.entryRef}`, item);
1906
+ this.diskPending.set(userId, {
1907
+ ...page,
1908
+ items: [...rows.values()],
1909
+ projection: {
1910
+ ...page.projection,
1911
+ visibility: [...hidden.values()]
1912
+ }
1913
+ });
1914
+ this.flushDisk();
1915
+ return true;
1916
+ }
1917
+ flushDisk() {
1918
+ if (this.diskWriting || this.diskPending.size === 0) return;
1919
+ this.diskWriting = true;
1920
+ this.persistence = (async () => {
1921
+ while (this.diskPending.size > 0) {
1922
+ await new Promise((resolve) => {
1923
+ setTimeout(resolve, 0);
1924
+ });
1925
+ const [userId, page] = this.diskPending.entries().next().value;
1926
+ this.diskPending.delete(userId);
1927
+ try {
1928
+ await this.runtime.stateStore.writeDirectoryCache?.(userId, page);
1929
+ this.cacheFailure = void 0;
1930
+ } catch (error) {
1931
+ this.cacheFailure = error;
1932
+ console.warn("dsh-arkme: directory_cache_write_failed", error instanceof Error ? error.message : "cache failed");
1933
+ }
1934
+ }
1935
+ })().finally(() => {
1936
+ this.diskWriting = false;
1937
+ this.flushDisk();
1938
+ });
1939
+ }
1940
+ startAvatars(generation) {
1941
+ if (this.avatarWork !== void 0) return;
1942
+ const signal = this.controller.signal;
1943
+ const pending = (async () => {
1944
+ while (this.avatars.size > 0 && generation === this.generation) {
1945
+ const items = [...this.avatars.values()].slice(0, PAGE_SIZE);
1946
+ for (const item of items) this.avatars.delete(keyOf(item));
1947
+ const hydrated = await this.source.hydrateDirectoryPage(items.map((item) => this.sources.get(keyOf(item)) ?? item), signal);
1948
+ if (generation !== this.generation) return;
1949
+ this.apply(hydrated.filter((item) => this.sources.has(keyOf(item))).map((item) => {
1950
+ const updated = {
1951
+ ...this.sources.get(keyOf(item)),
1952
+ avatarRef: item.avatarRef ?? "",
1953
+ avatarRefs: item.avatarRefs ?? []
1954
+ };
1955
+ if (item.groupAvatar === void 0) delete updated.groupAvatar;
1956
+ else updated.groupAvatar = item.groupAvatar;
1957
+ return updated;
1958
+ }), [], this.revision, true);
1959
+ const refs = new Set(hydrated.flatMap((item) => [item.avatarRef, ...item.avatarRefs ?? []]).filter((ref) => ref !== void 0 && ref.trim() !== ""));
1960
+ for (const ref of refs) {
1961
+ signal.throwIfAborted();
1962
+ await this.warmAvatar(ref, signal).catch(() => void 0);
1963
+ }
1964
+ if (refs.size > 0 && generation === this.generation) {
1965
+ this.revision++;
1966
+ const page = this.snapshot([], []);
1967
+ this.emit({
1968
+ ...page,
1969
+ projection: {
1970
+ ...page.projection,
1971
+ avatarRefs: [...refs]
1972
+ }
1973
+ });
1974
+ }
1975
+ }
1976
+ })().catch(() => {}).finally(() => {
1977
+ if (this.avatarWork === pending) this.avatarWork = void 0;
1978
+ });
1979
+ this.avatarWork = pending;
1980
+ }
1981
+ async rememberBots(items, expectedUserId) {
1982
+ if (this.userId !== expectedUserId) return;
1983
+ await this.activate();
1984
+ if (this.userId !== expectedUserId) return;
1985
+ const generation = this.generation;
1986
+ const atRevision = this.revision;
1987
+ const visible = await this.preferences.query([], items.map((bot) => bot.botRef), this.controller.signal);
1988
+ if (generation !== this.generation) return;
1989
+ const merged = new Map(this.bots.map((bot) => [bot.directoryKey ?? bot.botRef, bot]));
1990
+ for (const bot of items) {
1991
+ if (this.deletedBotRefs.has(bot.botRef)) continue;
1992
+ const key = bot.directoryKey ?? bot.botRef;
1993
+ const previous = merged.get(key);
1994
+ const next = {
1995
+ ...previous,
1996
+ ...bot
1997
+ };
1998
+ if ((previous?.latestMessageAtMillis ?? 0) > (bot.latestMessageAtMillis ?? 0)) {
1999
+ next.latestMessageAtMillis = previous.latestMessageAtMillis;
2000
+ if (previous.latestMessagePreview !== void 0) next.latestMessagePreview = previous.latestMessagePreview;
2001
+ else delete next.latestMessagePreview;
2002
+ } else if (bot.latestMessageAtMillis !== void 0 && bot.latestMessagePreview === void 0) delete next.latestMessagePreview;
2003
+ next.conversationListActivityAtMillis = Math.max(previous?.conversationListActivityAtMillis ?? 0, bot.conversationListActivityAtMillis ?? 0);
2004
+ merged.set(key, next);
2005
+ }
2006
+ for (const bot of this.bots) if (bot.directoryKey !== void 0) this.cachedBotKeys.set(bot.botRef, bot.directoryKey);
2007
+ while (this.cachedBotKeys.size > MAX_ROWS) this.cachedBotKeys.delete(this.cachedBotKeys.keys().next().value);
2008
+ this.bots = [...merged.values()];
2009
+ const currentRefs = new Set(this.bots.map((bot) => bot.botRef));
2010
+ for (const [key, entry] of this.visibility) if (entry.entryKind === "bot" && !currentRefs.has(entry.entryRef)) {
2011
+ this.visibility.delete(key);
2012
+ this.visibilityMutations.delete(key);
2013
+ }
2014
+ this.apply([], visible.items.filter((item) => !this.deletedBotRefs.has(item.entryRef)), atRevision);
2015
+ }
2016
+ async forgetSource(sourceRef, expectedUserId) {
2017
+ await this.activate();
2018
+ if (this.userId !== expectedUserId) return;
2019
+ const generation = this.generation;
2020
+ const source = [...this.sources.values()].find((item) => item.sourceRef === sourceRef);
2021
+ const key = source === void 0 ? await this.source.chatDirectorySourceKey(expectedUserId, (await this.source.openSourceRef(sourceRef, expectedUserId)).ownerRef) : keyOf(source);
2022
+ if (generation !== this.generation) return;
2023
+ this.sources.delete(key);
2024
+ this.mutations.delete(key);
2025
+ this.avatars.delete(key);
2026
+ this.visibility.delete(`source:${sourceRef}`);
2027
+ this.visibilityMutations.delete(`source:${sourceRef}`);
2028
+ this.sourceRemovals.set(key, this.revision + 1);
2029
+ if (this.sourceRemovals.size > MAX_ROWS) this.sourceRemovals.delete(this.sourceRemovals.keys().next().value);
2030
+ this.publish([]);
2031
+ }
2032
+ async forgetBot(botRef, expectedUserId) {
2033
+ await this.activate();
2034
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2035
+ this.deletedBotRefs.add(botRef);
2036
+ const deleted = this.bots.find((bot) => bot.botRef === botRef || bot.directoryKey !== void 0 && bot.directoryKey === this.cachedBotKeys.get(botRef));
2037
+ if (deleted !== void 0) this.deletedBotRefs.add(deleted.botRef);
2038
+ while (this.deletedBotRefs.size > MAX_ROWS) this.deletedBotRefs.delete(this.deletedBotRefs.values().next().value);
2039
+ if (deleted !== void 0) this.botPinnedKeys.delete(deleted.directoryKey ?? deleted.botRef);
2040
+ this.visibility.delete(`bot:${botRef}`);
2041
+ this.visibilityMutations.delete(`bot:${botRef}`);
2042
+ this.bots = this.bots.filter((bot) => bot !== deleted);
2043
+ this.publish([]);
2044
+ }
2045
+ async pinBot(botRef, pinned) {
2046
+ await this.activate();
2047
+ const generation = this.generation;
2048
+ let bot = this.bots.find((item) => item.botRef === botRef || item.directoryKey !== void 0 && item.directoryKey === this.cachedBotKeys.get(botRef));
2049
+ if (bot === void 0) {
2050
+ const fresh = await this.readBots(this.controller.signal);
2051
+ if (generation !== this.generation) throw new ArkmePluginError("login-context-changed", "账号已切换", false, 409);
2052
+ bot = fresh.items.find((item) => item.botRef === botRef && !this.deletedBotRefs.has(item.botRef));
2053
+ if (bot !== void 0) this.bots.push(bot);
2054
+ }
2055
+ if (bot === void 0) throw new ArkmePluginError("bot-directory-entry-unavailable", "请先加载当前账号的 Bot 目录", false, 404);
2056
+ const key = bot.directoryKey ?? bot.botRef;
2057
+ const previous = this.botPinnedKeys.has(key);
2058
+ if (pinned) this.botPinnedKeys.add(key);
2059
+ else this.botPinnedKeys.delete(key);
2060
+ const admitted = this.publish([]);
2061
+ while (this.diskWriting) await this.persistence;
2062
+ if (generation !== this.generation) throw new ArkmePluginError("login-context-changed", "账号已切换", false, 409);
2063
+ if (!admitted || this.cacheFailure !== void 0) {
2064
+ if (previous) this.botPinnedKeys.add(key);
2065
+ else this.botPinnedKeys.delete(key);
2066
+ this.publish([]);
2067
+ throw new ArkmePluginError("directory-cache-write-failed", "本地置顶状态保存失败,请重试", true, 500);
2068
+ }
2069
+ }
2070
+ async rememberSpecial(patch, expectedUserId) {
2071
+ await this.activate();
2072
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2073
+ if (patch.arkoPreview !== void 0 && this.special.arkoPreview !== void 0 && patch.arkoPreview.createdAtMillis < this.special.arkoPreview.createdAtMillis) delete patch.arkoPreview;
2074
+ const next = {
2075
+ ...this.special,
2076
+ ...patch
2077
+ };
2078
+ if (JSON.stringify(next) === JSON.stringify(this.special)) return;
2079
+ this.special = next;
2080
+ this.publish([]);
2081
+ }
2082
+ async confirmVisibility(entryKind, entryRef, hidden, expectedUserId) {
2083
+ await this.activate();
2084
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2085
+ if (entryKind === "bot") {
2086
+ const current = this.bots.find((bot) => bot.botRef === entryRef || bot.directoryKey !== void 0 && bot.directoryKey === this.cachedBotKeys.get(entryRef));
2087
+ if (current !== void 0) {
2088
+ const generation = this.generation;
2089
+ const atRevision = this.revision;
2090
+ const visible = await this.preferences.query([], [current.botRef], this.controller.signal);
2091
+ if (generation === this.generation) this.apply([], visible.items, atRevision);
2092
+ return;
2093
+ }
2094
+ }
2095
+ this.apply([], [{
2096
+ entryKind,
2097
+ entryRef,
2098
+ hidden
2099
+ }]);
2100
+ }
2101
+ async confirmPin(sourceRef, pinned, policyUpdatedAtMillis, expectedUserId) {
2102
+ await this.activate();
2103
+ if (expectedUserId !== void 0 && this.userId !== expectedUserId) return;
2104
+ const source = [...this.sources.values()].find((item) => item.sourceRef === sourceRef);
2105
+ if (source !== void 0) this.apply([{
2106
+ ...source,
2107
+ isPinned: pinned,
2108
+ chatPolicyUpdatedAtMillis: policyUpdatedAtMillis
2109
+ }]);
2110
+ }
2111
+ async accept(event) {
2112
+ const generation = this.generation;
2113
+ const atRevision = this.revision;
2114
+ if (this.userId === void 0 || (await this.runtime.accountScopedSession())?.userId !== this.userId || generation !== this.generation) return;
2115
+ if (event.type === "sessions-delta") {
2116
+ if (event.updates.some((item) => this.sourceRemovals.has(keyOf(item.source)))) if (this.scan === void 0) this.startScan();
2117
+ else this.rescanRequested = true;
2118
+ const sources = event.updates.filter((item) => !this.sourceRemovals.has(keyOf(item.source))).map((item) => mergeDirectorySource(this.sources.get(keyOf(item.source)), item.source));
2119
+ const visibility = await this.preferences.query(sources.map((item) => item.sourceRef), [], this.controller.signal);
2120
+ if (generation !== this.generation) return;
2121
+ this.apply(sources, visibility.items, atRevision);
2122
+ } else if (event.type === "read-ack") {
2123
+ const source = [...this.sources.values()].find((item) => item.sourceKey === event.sourceKey || item.sourceRef === event.sourceRef);
2124
+ if (source !== void 0) this.apply([{
2125
+ ...source,
2126
+ readSequence: Math.max(source.readSequence ?? 0, event.effectiveReadSequence),
2127
+ ...(source.latestSequence ?? 0) <= event.effectiveReadSequence ? {
2128
+ unreadCount: event.unreadCount,
2129
+ badgeUnreadCount: source.isMuted ? 0 : event.unreadCount,
2130
+ ...event.unreadCount === 0 ? { hasUnreadMention: false } : {}
2131
+ } : {}
2132
+ }]);
2133
+ else {
2134
+ const key = event.sourceKey ?? event.sourceRef;
2135
+ const previous = this.pendingReadAcks.get(key);
2136
+ if (previous === void 0 || previous.effectiveReadSequence < event.effectiveReadSequence) this.pendingReadAcks.set(key, event);
2137
+ if (this.pendingReadAcks.size > MAX_ROWS) this.pendingReadAcks.delete(this.pendingReadAcks.keys().next().value);
2138
+ }
2139
+ } else if (event.type === "chat-pins-reconciled") this.apply([...this.sources.values()].flatMap((source) => {
2140
+ const pin = event.pins.find((item) => item.sourceKey === source.sourceKey);
2141
+ return pin === void 0 ? [] : [{
2142
+ ...source,
2143
+ isPinned: pin.pinned,
2144
+ chatPolicyUpdatedAtMillis: pin.policyUpdatedAtMillis
2145
+ }];
2146
+ }));
2147
+ else if (event.type === "conversation-list-preference-invalidated" || event.type === "chat-policy-invalidated") if (this.scan === void 0) this.startScan();
2148
+ else this.rescanRequested = true;
2149
+ }
2150
+ };
2151
+ //#endregion
2152
+ //#region src/direct-message-admission.ts
2153
+ function directMessageAdmissionMessage(admission) {
2154
+ switch (admission.state) {
2155
+ case "allowed": return "";
2156
+ case "refused_by_self": return "你已拒收对方的消息";
2157
+ case "refused_by_counterpart": return "对方已拒收你的消息";
2158
+ case "mutually_refused": return "你们已互相拒收消息";
2159
+ }
2160
+ }
2161
+ //#endregion
1438
2162
  //#region src/services/direct-message-admission-service.ts
1439
2163
  function projectDirectMessageAdmission(value, chatSessionUid) {
1440
2164
  const data = objectValue$2(value);
@@ -32377,7 +33101,7 @@ const TOPIC_PRIVACY_PRIVATE = 2;
32377
33101
  const PRIVACY_SNAPSHOT_LIMIT = 100;
32378
33102
  const PRIVACY_SNAPSHOT_MAX_PAGES = 100;
32379
33103
  const PRIVACY_SNAPSHOT_TTL_MS = 3e4;
32380
- function integerLikeValue$5(value) {
33104
+ function integerLikeValue$4(value) {
32381
33105
  if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
32382
33106
  if (typeof value === "string" && value.trim() !== "") {
32383
33107
  const parsed = Number(value.trim());
@@ -32390,13 +33114,13 @@ function arkmePrivacyLockedRecord(raw) {
32390
33114
  const item = objectValue$2(raw);
32391
33115
  const core = objectValue$2(item.record_core);
32392
33116
  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;
33117
+ 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
33118
  }
32395
33119
  /** Topic list requests are public-only; retain this guard for stale or malformed responses. */
32396
33120
  function arkmePrivacyLockedTopic(raw) {
32397
33121
  const item = objectValue$2(raw);
32398
33122
  const core = objectValue$2(item.topic_core ?? item);
32399
- return integerLikeValue$5(core.privacy_state ?? core.privacyState) === TOPIC_PRIVACY_PRIVATE;
33123
+ return integerLikeValue$4(core.privacy_state ?? core.privacyState) === TOPIC_PRIVACY_PRIVATE;
32400
33124
  }
32401
33125
  /** Mirrors the mobile visibility snapshot: it returns only privacy metadata, never record content. */
32402
33126
  var ArkmePrivacyVisibilityService = class {
@@ -32436,13 +33160,13 @@ var ArkmePrivacyVisibilityService = class {
32436
33160
  const item = objectValue$2(raw);
32437
33161
  const recordUid = stringValue$2(item.record_uid).trim();
32438
33162
  if (recordUid === "") continue;
32439
- if (integerLikeValue$5(item.lock_state) === 1 || integerLikeValue$5(item.content_access_state) === CONTENT_ACCESS_PROTECTED) locked.add(recordUid);
33163
+ if (integerLikeValue$4(item.lock_state) === 1 || integerLikeValue$4(item.content_access_state) === CONTENT_ACCESS_PROTECTED) locked.add(recordUid);
32440
33164
  }
32441
33165
  if (data.has_more !== true) {
32442
33166
  complete = true;
32443
33167
  break;
32444
33168
  }
32445
- const nextUpdateAt = integerLikeValue$5(data.next_update_at);
33169
+ const nextUpdateAt = integerLikeValue$4(data.next_update_at);
32446
33170
  const nextRecordUid = stringValue$2(data.next_record_uid).trim();
32447
33171
  if (nextUpdateAt <= 0 || nextRecordUid === "") throw new ArkmePluginError("privacy-visibility-snapshot-invalid", "隐私锁快记状态不完整,已停止显示快记", true, 502);
32448
33172
  cursorUpdateAt = nextUpdateAt;
@@ -32650,7 +33374,7 @@ function contractError$1(field) {
32650
33374
  function listValue$24(value) {
32651
33375
  return Array.isArray(value) ? value : [];
32652
33376
  }
32653
- function integerLikeValue$4(value) {
33377
+ function integerLikeValue$3(value) {
32654
33378
  if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
32655
33379
  if (typeof value === "string" && value.trim() !== "") {
32656
33380
  const parsed = Number(value.trim());
@@ -32670,7 +33394,7 @@ function arkmeMentionMetadataMentionsViewer(record, payload, viewerUserId) {
32670
33394
  const metadata = arkmeMentionMetadataFromRecord(record, payload);
32671
33395
  for (const rawMention of listValue$24(metadata.human_mentions ?? metadata.humanMentions)) {
32672
33396
  const mention = objectValue$2(rawMention);
32673
- const mentionedUserId = integerLikeValue$4(mention.user_id ?? mention.userId);
33397
+ const mentionedUserId = integerLikeValue$3(mention.user_id ?? mention.userId);
32674
33398
  if (mentionedUserId === 0 || mentionedUserId === viewerUserId) return true;
32675
33399
  }
32676
33400
  return false;
@@ -32928,47 +33652,6 @@ function arkmePickedFileKind(mimeType, fileName = "") {
32928
33652
  return mediaKind === "image" ? 1 : mediaKind === "video" ? 3 : 4;
32929
33653
  }
32930
33654
  //#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
33655
  //#region src/arkme-emoji-text.ts
32973
33656
  /** Asset-free emoji catalog shared by Browser UI and Host notification text. */
32974
33657
  const arkmeDefaultEmojiSeeds = Object.freeze([
@@ -34144,7 +34827,7 @@ var SourceService = class {
34144
34827
  const session = await this.runtime.requireSession();
34145
34828
  const limit = Math.min(directory === "send_to_self" ? 100 : 50, Math.max(1, Math.trunc(options.limit ?? 30)));
34146
34829
  const cursor = options.cursor?.trim() ?? "";
34147
- const cacheKey = `${String(session.userId)}:${directory}:${String(limit)}:${cursor}`;
34830
+ const cacheKey = `${String(session.userId)}:${directory}:${String(limit)}:${cursor}:${options.firstPaint === true ? "first" : "full"}`;
34148
34831
  this.pruneSourceListCache();
34149
34832
  const cached = this.sourceListCache.get(cacheKey);
34150
34833
  if (options.refresh !== true && cached !== void 0 && cached.expiresAtMillis > Date.now()) return cloneSourceList(cached.value);
@@ -34457,6 +35140,7 @@ var SourceService = class {
34457
35140
  activeAtMillis: arkmeChatSortActiveAt(bundle, chatSession),
34458
35141
  ...attention,
34459
35142
  ...hasUnreadMention === void 0 ? {} : { hasUnreadMention },
35143
+ readSequence: numberValue$26(unread.read_seq),
34460
35144
  isPinned,
34461
35145
  chatPolicyUpdatedAtMillis: numberValue$26(currentPolicy.update_at),
34462
35146
  ...numberValue$26(unread.session_last_seq ?? chatSession.last_seq) > 0 ? { latestSequence: numberValue$26(unread.session_last_seq ?? chatSession.last_seq) } : {}
@@ -34472,7 +35156,7 @@ var SourceService = class {
34472
35156
  } else groupSessionUidByIndex.set(itemIndex, uid);
34473
35157
  }
34474
35158
  try {
34475
- await this.hydrateSourceAvatars(items, privateUserIdByIndex, groupSessionUidByIndex, session, options.signal);
35159
+ if (options.firstPaint !== true) await this.hydrateSourceAvatars(items, privateUserIdByIndex, groupSessionUidByIndex, session, options.signal);
34476
35160
  } catch (error) {
34477
35161
  console.warn("dsh-arkme: Chat avatar hydration failed:", safeFailureMessage$6(error));
34478
35162
  }
@@ -34508,6 +35192,19 @@ var SourceService = class {
34508
35192
  this.sourceListCache.delete(oldestKey);
34509
35193
  }
34510
35194
  }
35195
+ async hydrateDirectoryPage(items, signal) {
35196
+ const session = await this.runtime.requireSession();
35197
+ const hydrated = items.map(cloneSourceItem);
35198
+ const privateUsers = /* @__PURE__ */ new Map();
35199
+ const groups = /* @__PURE__ */ new Map();
35200
+ for (let index = 0; index < items.length; index++) {
35201
+ const item = items[index];
35202
+ if (item.kind === "private_chat" && item.peerUserId !== void 0) privateUsers.set(index, item.peerUserId);
35203
+ if (item.kind === "group_chat") groups.set(index, (await this.openSourceRef(item.sourceRef, session.userId)).ownerRef);
35204
+ }
35205
+ await this.hydrateSourceAvatars(hydrated, privateUsers, groups, session, signal);
35206
+ return hydrated;
35207
+ }
34511
35208
  async hydrateSourceAvatars(items, privateUserIdByIndex, groupSessionUidByIndex, session, signal) {
34512
35209
  const groupSnapshotsByIndex = /* @__PURE__ */ new Map();
34513
35210
  const authoritativeGroupIndices = /* @__PURE__ */ new Set();
@@ -35291,8 +35988,42 @@ var BotService = class {
35291
35988
  async botDirectoryKey(userId, botId) {
35292
35989
  return `arkme-bot-directory-v1.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`arkme-bot-directory-v1:${String(userId)}:${botId}`).digest("base64url")}`;
35293
35990
  }
35991
+ /** A cached directory entry is a lookup, never a persisted live target/capability. */
35992
+ async restoreDirectoryBots(items, userId) {
35993
+ const secret = await this.runtime.stateStore.uniqueCode();
35994
+ return items.flatMap((bot) => {
35995
+ if (bot.directoryKey === void 0) return [];
35996
+ const payload = Buffer.from(JSON.stringify({ directoryKey: bot.directoryKey })).toString("base64url");
35997
+ const signature = createHmac("sha256", secret).update(`bot-directory-entry-v1:${userId}:${payload}`).digest("base64url");
35998
+ return [{
35999
+ ...bot,
36000
+ botRef: `arkme-bot-directory-entry-v1.${payload}.${signature}`
36001
+ }];
36002
+ });
36003
+ }
36004
+ async openDirectoryBotRef(reference, userId) {
36005
+ const parts = reference.split(".");
36006
+ if (parts.length !== 3 || reference.length > 1024) throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36007
+ const payload = parts[1];
36008
+ const supplied = Buffer.from(parts[2], "base64url");
36009
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(`bot-directory-entry-v1:${userId}:${payload}`).digest();
36010
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36011
+ let parsed;
36012
+ try {
36013
+ parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
36014
+ } catch {
36015
+ throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36016
+ }
36017
+ if ((await this.runtime.requireSession()).userId !== userId) throw new ArkmePluginError("bot-ref-account-mismatch", "Bot 目录引用与当前账号不匹配", false, 403);
36018
+ if (typeof parsed?.directoryKey !== "string") throw new ArkmePluginError("bot-ref-invalid", "Bot 目录引用无效", false, 400);
36019
+ const current = (await this.listBots()).items.find((bot) => bot.directoryKey === parsed.directoryKey);
36020
+ if ((await this.runtime.requireSession()).userId !== userId) throw new ArkmePluginError("bot-ref-account-mismatch", "账号已切换", false, 403);
36021
+ if (current === void 0) throw new ArkmePluginError("bot-ref-expired", "Bot 已不可用,请刷新列表", false, 410);
36022
+ return await this.openBotRef(current.botRef, userId);
36023
+ }
35294
36024
  async openBotRef(botRef, expectedUserId) {
35295
36025
  const normalized = botRef.trim();
36026
+ if (normalized.startsWith("arkme-bot-directory-entry-v1.")) return await this.openDirectoryBotRef(normalized, expectedUserId);
35296
36027
  if (!BOT_REF_PATTERN.test(normalized)) throw new ArkmePluginError("bot-ref-invalid", "Bot 引用无效", false);
35297
36028
  const entry = this.botRefs.get(normalized);
35298
36029
  if (entry === void 0) throw new ArkmePluginError("bot-ref-expired", "Bot 引用已过期,请刷新 Bot 列表", false, 410);
@@ -36675,6 +37406,29 @@ function decodeDataLine(line) {
36675
37406
  }
36676
37407
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
36677
37408
  }
37409
+ function decodeArkmeMemberJoinedDataLine(line) {
37410
+ const source = decodeDataLine(line);
37411
+ if (source === void 0 || positiveInteger(source.t) !== 24) return void 0;
37412
+ if (Object.keys(source).some((key) => ![
37413
+ "t",
37414
+ "event_uid",
37415
+ "chat_session_uid",
37416
+ "actor_user_id",
37417
+ "member_user_id",
37418
+ "join_at",
37419
+ "event_at",
37420
+ "source_client_id"
37421
+ ].includes(key))) return void 0;
37422
+ const eventUid = nonEmptyString(source.event_uid);
37423
+ const chatSessionUid = nonEmptyString(source.chat_session_uid);
37424
+ const eventAtMillis = positiveInteger(source.event_at);
37425
+ 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;
37426
+ return {
37427
+ eventUid,
37428
+ chatSessionUid,
37429
+ eventAtMillis
37430
+ };
37431
+ }
36678
37432
  function decodeArkmeMemberEventDataLine(line) {
36679
37433
  const source = decodeDataLine(line);
36680
37434
  if (source === void 0 || positiveInteger(source.t) !== 27) return void 0;
@@ -37243,6 +37997,7 @@ var ArkmeChatRealtimeRuntime = class {
37243
37997
  if (connectionSignal.aborted) return;
37244
37998
  const policyUpdated = decodeArkmeChatPolicyUpdatedDataLine(line);
37245
37999
  if (policyUpdated !== void 0 && policyUpdated.userId !== connectionUserId) return;
38000
+ const memberJoined = decodeArkmeMemberJoinedDataLine(line);
37246
38001
  const memberEvent = decodeArkmeMemberEventDataLine(line);
37247
38002
  const conversationListPreferenceUpdated = decodeArkmeConversationListPreferenceUpdatedDataLine(line);
37248
38003
  const projectionInvalidation = conversationListPreferenceUpdated === void 0 ? decodeArkmeProjectionInvalidatedDataLine(line) : void 0;
@@ -37250,20 +38005,29 @@ var ArkmeChatRealtimeRuntime = class {
37250
38005
  const timelineChanged = conversationListPreferenceUpdated === void 0 && projectionInvalidation === void 0 && readCursorAdvanced === void 0 ? decodeArkmeChatTimelineChangedDataLine(line) : void 0;
37251
38006
  const messagePreparing = decodeArkmeChatMessagePreparingDataLine(line);
37252
38007
  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;
38008
+ const eventUid = policyUpdated?.eventUid ?? memberJoined?.eventUid ?? memberEvent?.eventUid ?? conversationListPreferenceUpdated?.eventUid ?? projectionInvalidation?.eventUid ?? readCursorAdvanced?.eventUid ?? timelineChanged?.eventUid ?? messagePreparing?.eventUid ?? hint?.eventUid;
37254
38009
  if (eventUid === void 0 || this.seenEventUids.has(eventUid)) return;
37255
38010
  this.seenEventUids.add(eventUid);
37256
38011
  if (this.seenEventUids.size > MAX_SEEN_EVENTS) {
37257
38012
  const oldest = this.seenEventUids.values().next().value;
37258
38013
  if (oldest !== void 0) this.seenEventUids.delete(oldest);
37259
38014
  }
37260
- this.lastEventAtMillis = policyUpdated?.eventAtMillis ?? memberEvent?.eventAtMillis ?? conversationListPreferenceUpdated?.acceptedAtMillis ?? projectionInvalidation?.eventAtMillis ?? readCursorAdvanced?.eventAtMillis ?? timelineChanged?.eventAtMillis ?? messagePreparing?.eventAtMillis ?? hint?.eventAtMillis;
38015
+ this.lastEventAtMillis = policyUpdated?.eventAtMillis ?? memberJoined?.eventAtMillis ?? memberEvent?.eventAtMillis ?? conversationListPreferenceUpdated?.acceptedAtMillis ?? projectionInvalidation?.eventAtMillis ?? readCursorAdvanced?.eventAtMillis ?? timelineChanged?.eventAtMillis ?? messagePreparing?.eventAtMillis ?? hint?.eventAtMillis;
37261
38016
  if (policyUpdated !== void 0) this.advanceRevision("chat-policy-invalidation", {
37262
38017
  policyUpdated,
37263
38018
  connectionUserId,
37264
38019
  connectionSignal
37265
38020
  });
37266
- else if (memberEvent !== void 0) this.advanceRevision("chat-hint", { memberEvent });
38021
+ else if (memberJoined !== void 0) this.advanceRevision("chat-hint", {
38022
+ memberJoined,
38023
+ connectionUserId,
38024
+ connectionSignal
38025
+ });
38026
+ else if (memberEvent !== void 0) this.advanceRevision("chat-hint", {
38027
+ memberEvent,
38028
+ connectionUserId,
38029
+ connectionSignal
38030
+ });
37267
38031
  else if (conversationListPreferenceUpdated !== void 0) this.advanceRevision("conversation-list-preference-invalidation", { conversationListPreferenceUpdated });
37268
38032
  else if (projectionInvalidation !== void 0) this.advanceRevision("projection-invalidation", { projectionInvalidation });
37269
38033
  else if (timelineChanged !== void 0) this.advanceRevision("chat-hint", {
@@ -37287,11 +38051,12 @@ var ArkmeChatRealtimeRuntime = class {
37287
38051
  });
37288
38052
  }
37289
38053
  advanceRevision(cause, evidence = {}) {
37290
- const { hint, readCursorAdvanced, policyUpdated, projectionInvalidation, timelineChanged, messagePreparing, connectionUserId, connectionSignal, connectionStartedAtMillis, conversationListPreferenceUpdated, memberEvent } = evidence;
38054
+ const { hint, readCursorAdvanced, policyUpdated, projectionInvalidation, timelineChanged, messagePreparing, connectionUserId, connectionSignal, connectionStartedAtMillis, conversationListPreferenceUpdated, memberEvent, memberJoined } = evidence;
37291
38055
  this.revision += 1;
37292
38056
  const notice = {
37293
38057
  state: this.state(),
37294
38058
  cause,
38059
+ ...memberJoined === void 0 ? {} : { memberJoined },
37295
38060
  ...memberEvent === void 0 ? {} : { memberEvent },
37296
38061
  ...timelineChanged === void 0 ? {} : { timelineChanged },
37297
38062
  ...messagePreparing === void 0 ? {} : { messagePreparing },
@@ -37360,6 +38125,7 @@ var ChatRealtimeService = class {
37360
38125
  source;
37361
38126
  projectionReader;
37362
38127
  nativeAttention;
38128
+ directoryBaseline;
37363
38129
  disposed = false;
37364
38130
  chatRealtime;
37365
38131
  chatClientListeners = /* @__PURE__ */ new Set();
@@ -37480,8 +38246,8 @@ var ChatRealtimeService = class {
37480
38246
  this.invalidateChatPolicyForCurrentSession(notice);
37481
38247
  return;
37482
38248
  }
37483
- if (notice.cause === "chat-hint" && notice.memberEvent !== void 0) {
37484
- this.handleMemberEvent(notice.memberEvent);
38249
+ if (notice.cause === "chat-hint" && (notice.memberEvent !== void 0 || notice.memberJoined !== void 0)) {
38250
+ this.handleMemberEvent(notice);
37485
38251
  return;
37486
38252
  }
37487
38253
  if (notice.cause === "reconcile") {
@@ -37635,13 +38401,21 @@ var ChatRealtimeService = class {
37635
38401
  console.warn("dsh-arkme: Chat timeline invalidation failed:", safeFailureMessage$5(error));
37636
38402
  }
37637
38403
  }
37638
- async handleMemberEvent(hint) {
38404
+ async handleMemberEvent(notice) {
38405
+ const hint = notice.memberJoined ?? notice.memberEvent;
38406
+ if (hint === void 0 || notice.connectionSignal?.aborted) return;
37639
38407
  try {
37640
38408
  const session = await this.runtime.sessionStore.read();
37641
- if (session === void 0) return;
38409
+ if (session === void 0 || notice.connectionUserId !== void 0 && notice.connectionUserId !== session.userId) return;
38410
+ this.runtime.invalidateMemberCache?.();
37642
38411
  const sourceKey = await this.source.chatDirectorySourceKey(session.userId, hint.chatSessionUid);
37643
- if ((await this.runtime.sessionStore.read())?.userId !== session.userId) return;
37644
- this.emitChatClientEvent({
38412
+ if (notice.connectionSignal?.aborted || (await this.runtime.sessionStore.read())?.userId !== session.userId) return;
38413
+ if (notice.memberJoined !== void 0) this.emitChatClientEvent({
38414
+ type: "members-invalidated",
38415
+ revision: this.nextChatClientRevision(),
38416
+ sourceKey
38417
+ });
38418
+ else this.emitChatClientEvent({
37645
38419
  type: "member-events-invalidated",
37646
38420
  revision: this.nextChatClientRevision(),
37647
38421
  sourceKey,
@@ -37866,11 +38640,12 @@ var ChatRealtimeService = class {
37866
38640
  this.notificationBaselineSequences.clear();
37867
38641
  const sequences = /* @__PURE__ */ new Map();
37868
38642
  const pins = [];
37869
- this.source.invalidateSourceListCache(session.userId, "root");
38643
+ const shared = this.directoryBaseline === void 0 ? void 0 : await this.directoryBaseline();
37870
38644
  let cursor;
37871
- for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
37872
- const page = await this.source.listSources("root", {
37873
- limit: 50,
38645
+ const visited = /* @__PURE__ */ new Set();
38646
+ while (true) {
38647
+ const page = shared ?? await this.source.listSources("root", {
38648
+ limit: 20,
37874
38649
  refresh: true,
37875
38650
  ...cursor === void 0 ? {} : { cursor }
37876
38651
  });
@@ -37887,8 +38662,10 @@ var ChatRealtimeService = class {
37887
38662
  policyUpdatedAtMillis: item.chatPolicyUpdatedAtMillis
37888
38663
  });
37889
38664
  }
37890
- if (!page.hasMore || page.nextCursor === void 0) break;
38665
+ if (shared !== void 0 || !page.hasMore) break;
38666
+ if (page.nextCursor === void 0 || visited.has(page.nextCursor)) throw new Error("Notification directory cursor is invalid");
37891
38667
  cursor = page.nextCursor;
38668
+ visited.add(cursor);
37892
38669
  }
37893
38670
  const activeSession = await this.runtime.sessionStore.read();
37894
38671
  const state = this.chatRealtime.state();
@@ -38377,6 +39154,160 @@ var ChatRealtimeService = class {
38377
39154
  }
38378
39155
  };
38379
39156
  //#endregion
39157
+ //#region src/member-directory.ts
39158
+ /** Only for the members/page and members/by-user-ids read contracts: 2001/2002 mean unreadable membership/session. */
39159
+ function invalidatesMemberSnapshot(error) {
39160
+ const failure = error;
39161
+ const code = failure?.body?.code ?? failure?.code;
39162
+ return code !== void 0 && [
39163
+ "auth-http-401",
39164
+ "auth-http-403",
39165
+ "login-required",
39166
+ "login-expired",
39167
+ "source-ref-invalid",
39168
+ "arkme-code-1000",
39169
+ "arkme-code-403",
39170
+ "arkme-code-1004",
39171
+ "arkme-code-2001",
39172
+ "arkme-code-2002",
39173
+ "chat-members-source-invalid"
39174
+ ].includes(code);
39175
+ }
39176
+ function mergeMemberFacts(previous, facts) {
39177
+ const { memberName: _oldName, ...base } = previous ?? {
39178
+ displayName: facts.memberName || "群成员",
39179
+ recordCount: 0,
39180
+ mentionCount: 0,
39181
+ statsKnown: false
39182
+ };
39183
+ return {
39184
+ ...base,
39185
+ memberRef: facts.memberRef,
39186
+ role: facts.role,
39187
+ status: facts.status,
39188
+ isSelf: facts.isSelf,
39189
+ isOwner: facts.isOwner,
39190
+ joinedAtMillis: facts.joinedAtMillis,
39191
+ ...facts.memberName === void 0 ? {} : { memberName: facts.memberName }
39192
+ };
39193
+ }
39194
+ function mergeMemberPresentation(previous, incoming, profileUnavailable) {
39195
+ if (!profileUnavailable) return {
39196
+ ...incoming,
39197
+ statsKnown: true
39198
+ };
39199
+ return {
39200
+ ...incoming,
39201
+ statsKnown: true,
39202
+ ...[
39203
+ "",
39204
+ "群成员",
39205
+ "成员"
39206
+ ].includes(incoming.displayName) && previous !== void 0 ? { displayName: previous.displayName } : {},
39207
+ ...previous?.avatarRef === void 0 ? {} : { avatarRef: previous.avatarRef }
39208
+ };
39209
+ }
39210
+ /** Membership creates entities; details can only enrich or explicitly remove known entities. */
39211
+ function applyMemberUpdate(members, update) {
39212
+ let changed = false;
39213
+ const put = (item) => {
39214
+ const old = members.get(item.memberRef);
39215
+ const fields = /* @__PURE__ */ new Set([...Object.keys(old ?? {}), ...Object.keys(item)]);
39216
+ if (old !== void 0 && [...fields].every((field) => old[field] === item[field])) return;
39217
+ members.set(item.memberRef, item);
39218
+ changed = true;
39219
+ };
39220
+ if (update.kind === "membership") for (const facts of update.items) put(mergeMemberFacts(members.get(facts.memberRef), facts));
39221
+ else {
39222
+ const unavailable = new Set(update.unavailableProfileMemberRefs);
39223
+ for (const item of update.items) {
39224
+ const old = members.get(item.memberRef);
39225
+ if (old !== void 0) put(mergeMemberPresentation(old, item, unavailable.has(item.memberRef)));
39226
+ }
39227
+ }
39228
+ for (const ref of update.removedMemberRefs) if (members.delete(ref)) changed = true;
39229
+ return changed;
39230
+ }
39231
+ function validFacts(value) {
39232
+ if (!value || typeof value !== "object") return false;
39233
+ const item = value;
39234
+ return typeof item.memberRef === "string" && item.memberRef.trim() !== "" && item.status === "active" && [
39235
+ "owner",
39236
+ "admin",
39237
+ "member",
39238
+ "unknown"
39239
+ ].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");
39240
+ }
39241
+ function cachedMemberItem(value) {
39242
+ if (!validFacts(value)) return void 0;
39243
+ const item = value;
39244
+ 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;
39245
+ const result = {
39246
+ memberRef: item.memberRef,
39247
+ role: item.role,
39248
+ status: item.status,
39249
+ isSelf: item.isSelf,
39250
+ isOwner: item.isOwner,
39251
+ joinedAtMillis: item.joinedAtMillis,
39252
+ displayName: item.displayName,
39253
+ recordCount: item.recordCount,
39254
+ mentionCount: item.mentionCount,
39255
+ ...item.statsKnown === void 0 ? {} : { statsKnown: item.statsKnown }
39256
+ };
39257
+ for (const field of [
39258
+ "memberName",
39259
+ "secondaryName",
39260
+ "avatarRef",
39261
+ "mentionRef",
39262
+ "mentionDisplayName",
39263
+ "mentionSecondaryName"
39264
+ ]) {
39265
+ if (item[field] === void 0) continue;
39266
+ if (typeof item[field] !== "string") return void 0;
39267
+ result[field] = item[field];
39268
+ }
39269
+ return result;
39270
+ }
39271
+ function validateMemberUpdate(update) {
39272
+ 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("成员响应无效");
39273
+ if (update.kind === "presentation") {
39274
+ if (update.items.length + update.removedMemberRefs.length > 50) throw new Error("成员资料超出批次上限");
39275
+ 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("成员资料响应无效");
39276
+ } else if (update.kind !== "membership" || ![
39277
+ "owner",
39278
+ "admin",
39279
+ "member"
39280
+ ].includes(update.selfRole) || typeof update.hasMore !== "boolean" || update.items.length + update.removedMemberRefs.length > 100) throw new Error("成员分页响应无效");
39281
+ }
39282
+ function joinPerson(value) {
39283
+ if (!value || typeof value.displayName !== "string" || typeof value.isSelf !== "boolean" || value.memberRef !== void 0 && (typeof value.memberRef !== "string" || !value.memberRef.trim())) throw new Error("入群事件成员无效");
39284
+ return {
39285
+ displayName: value.displayName,
39286
+ isSelf: value.isSelf,
39287
+ ...value.memberRef === void 0 ? {} : { memberRef: value.memberRef }
39288
+ };
39289
+ }
39290
+ function mergeMemberJoinEvents(previous, incoming) {
39291
+ const events = new Map(previous.map((event) => [event.eventId, event]));
39292
+ for (const event of incoming) {
39293
+ 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("入群事件无效");
39294
+ const inviter = joinPerson(event.inviter);
39295
+ const incomingPeople = event.invitees.map(joinPerson);
39296
+ if (incomingPeople.some((person) => !person.memberRef)) throw new Error("入群事件缺少成员身份");
39297
+ const old = events.get(event.eventId);
39298
+ const invitees = new Map((old?.invitees ?? []).map((person) => [person.memberRef, person]));
39299
+ for (const person of incomingPeople) invitees.set(person.memberRef, person);
39300
+ events.set(event.eventId, {
39301
+ eventId: event.eventId,
39302
+ action: event.action,
39303
+ occurredAtMillis: event.occurredAtMillis,
39304
+ inviter,
39305
+ invitees: [...invitees.values()]
39306
+ });
39307
+ }
39308
+ return [...events.values()].slice(-2e3);
39309
+ }
39310
+ //#endregion
38380
39311
  //#region src/services/member-event-service.ts
38381
39312
  const CURSOR_PREFIX = "arkme-member-event-cursor-v1";
38382
39313
  var MemberEventService = class {
@@ -41310,7 +42241,7 @@ async function projectArkmeConversationMemberJoinEvents(rawItems, options) {
41310
42241
  "manual_add",
41311
42242
  "added_by_member"
41312
42243
  ])).has(sourceType) ? "direct_add" : "invite";
41313
- const inviterKey = inviterUserId > 0 ? `id:${String(inviterUserId)}` : `name:${inviterDisplayName}`;
42244
+ const inviterKey = inviterUserId > 0 ? `id:${String(inviterUserId)}` : `unknown-inviter:${String(inviteeUserId)}`;
41314
42245
  const groupKey = `${String(occurredAtMillis)}|${action}|${inviterKey}`;
41315
42246
  const group = groups.get(groupKey) ?? {
41316
42247
  action,
@@ -41675,6 +42606,156 @@ var ChatService = class {
41675
42606
  this.favoriteStickerMutationTail = pending.then(() => void 0, () => void 0);
41676
42607
  return await pending;
41677
42608
  }
42609
+ async cachedSourceMembers(sourceRef, signal) {
42610
+ signal?.throwIfAborted();
42611
+ const session = await this.runtime.requireSession();
42612
+ const source = await this.requireReadReceiptChatSource(sourceRef, session.userId);
42613
+ const cached = await this.runtime.stateStore.cachedConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
42614
+ signal?.throwIfAborted();
42615
+ if (cached === void 0) return void 0;
42616
+ const invalid = [];
42617
+ for (const member of cached.items) {
42618
+ signal?.throwIfAborted();
42619
+ try {
42620
+ await this.openChatMemberRef(member.memberRef, session.userId, source.ownerRef);
42621
+ } catch (error) {
42622
+ if (!(error instanceof ArkmePluginError)) return void 0;
42623
+ invalid.push(member.memberRef);
42624
+ }
42625
+ }
42626
+ signal?.throwIfAborted();
42627
+ if (invalid.length === 0) return cached;
42628
+ await this.runtime.stateStore.forgetCachedMembers?.(session.userId, source.ownerRef, invalid).catch(() => void 0);
42629
+ }
42630
+ async pageSourceMembers(sourceRef, options = {}) {
42631
+ options.signal?.throwIfAborted();
42632
+ const session = await this.runtime.requireSession();
42633
+ const cacheEpoch = this.runtime.memberCacheEpoch?.() ?? 0;
42634
+ const source = await this.requireReadReceiptChatSource(sourceRef, session.userId);
42635
+ const limit = options.limit ?? 50;
42636
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new ArkmePluginError("member-page-invalid", "成员分页大小必须为 1 至 100", false, 400);
42637
+ const after = options.cursor === void 0 ? 0 : await this.openMemberPageCursor(options.cursor, session.userId, source.ownerRef);
42638
+ const data = await this.memberRead(session, source.ownerRef, "/api/v1/chats/members/page", {
42639
+ chat_session_uid: source.ownerRef,
42640
+ after_user_id: after,
42641
+ limit,
42642
+ active_only: false
42643
+ }, options.signal);
42644
+ options.signal?.throwIfAborted();
42645
+ const raw = listValue$16(data.items).map(objectValue$2);
42646
+ const ids = raw.map((item) => numberValue$19(item.user_id));
42647
+ const next = numberValue$19(data.next_user_id);
42648
+ 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);
42649
+ const active = raw.filter((item) => chatMemberStatus$1(item.status) === "active");
42650
+ const items = await this.projectChatMembers(source.ownerRef, active, session, {
42651
+ includeViewerLabels: false,
42652
+ includeHumanMentionRefs: false,
42653
+ basicOnly: true,
42654
+ ...options.signal === void 0 ? {} : { signal: options.signal }
42655
+ });
42656
+ 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))));
42657
+ const signingKey = await this.runtime.stateStore.uniqueCode();
42658
+ const joinEvents = source.kind === "group_chat" && this.runtime.config.chatMemberJoinEventsEnabled !== false ? await projectArkmeConversationMemberJoinEvents(raw, {
42659
+ viewerUserId: session.userId,
42660
+ memberRefForUserId: async (id) => await this.sealChatMemberRef(session.userId, source.ownerRef, id),
42661
+ eventIdForStableKey: async (stableKey) => `arkme-chat-join-v1.${createHmac("sha256", signingKey).update(`${session.userId}|${source.ownerRef}|${stableKey}`).digest("base64url")}`
42662
+ }) : [];
42663
+ const page = {
42664
+ kind: "membership",
42665
+ selfRole: chatMemberRole$1(data.self_role),
42666
+ source: await this.source.sourceItem(source),
42667
+ items: items.map(({ memberRef, role, status, isSelf, isOwner, joinedAtMillis, memberName }) => ({
42668
+ memberRef,
42669
+ role,
42670
+ status,
42671
+ isSelf,
42672
+ isOwner,
42673
+ joinedAtMillis,
42674
+ ...memberName === void 0 ? {} : { memberName }
42675
+ })),
42676
+ removedMemberRefs,
42677
+ hasMore: data.has_more,
42678
+ joinEvents,
42679
+ ...data.has_more ? { nextCursor: await this.sealMemberPageCursor(session.userId, source.ownerRef, next) } : {}
42680
+ };
42681
+ if (!options.signal?.aborted && cacheEpoch === (this.runtime.memberCacheEpoch?.() ?? 0)) await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, page).catch(() => void 0);
42682
+ return page;
42683
+ }
42684
+ async sourceMembersPresentation(sourceRef, memberRefs, options = {}) {
42685
+ options.signal?.throwIfAborted();
42686
+ const session = await this.runtime.requireSession();
42687
+ const cacheEpoch = this.runtime.memberCacheEpoch?.() ?? 0;
42688
+ const source = await this.requireReadReceiptChatSource(sourceRef, session.userId);
42689
+ if (memberRefs.length < 1 || memberRefs.length > 50 || new Set(memberRefs).size !== memberRefs.length) throw new ArkmePluginError("member-presentation-invalid", "每次需要 1 至 50 个不同成员引用", false, 400);
42690
+ const ids = (await Promise.all(memberRefs.map((ref) => this.openChatMemberRef(ref, session.userId, source.ownerRef)))).map((ref) => ref.targetUserId);
42691
+ const data = await this.memberRead(session, source.ownerRef, "/api/v1/chats/members/by-user-ids", {
42692
+ chat_session_uid: source.ownerRef,
42693
+ user_ids: ids,
42694
+ active_only: true
42695
+ }, options.signal);
42696
+ options.signal?.throwIfAborted();
42697
+ const raw = listValue$16(data.items).map(objectValue$2);
42698
+ const returnedIds = raw.map((item) => numberValue$19(item.user_id));
42699
+ 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);
42700
+ const profiles = await this.profile.publicProfileSummariesByUserIds(returnedIds, session, options.signal).catch((error) => {
42701
+ if (options.signal?.aborted) throw error;
42702
+ return /* @__PURE__ */ new Map();
42703
+ });
42704
+ const items = await this.projectChatMembers(source.ownerRef, raw, session, {
42705
+ includeViewerLabels: false,
42706
+ includeHumanMentionRefs: source.kind === "group_chat",
42707
+ profiles,
42708
+ ...options.signal === void 0 ? {} : { signal: options.signal }
42709
+ });
42710
+ options.signal?.throwIfAborted();
42711
+ const unavailableProfileMemberRefs = await Promise.all(returnedIds.filter((id) => !profiles.has(id)).map((id) => this.sealChatMemberRef(session.userId, source.ownerRef, id)));
42712
+ const result = {
42713
+ kind: "presentation",
42714
+ source: await this.source.sourceItem(source),
42715
+ items,
42716
+ unavailableProfileMemberRefs,
42717
+ removedMemberRefs: memberRefs.filter((_ref, index) => !returnedIds.includes(ids[index]))
42718
+ };
42719
+ if (!options.signal?.aborted && cacheEpoch === (this.runtime.memberCacheEpoch?.() ?? 0)) await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, result).catch(() => void 0);
42720
+ return result;
42721
+ }
42722
+ async memberRead(session, group, path, body, signal) {
42723
+ try {
42724
+ return await this.runtime.authenticatedChatPost(path, body, session, signal, {
42725
+ lane: path.endsWith("/members/page") ? "interactive-read" : "background-read",
42726
+ key: `members:${this.runtime.memberCacheEpoch?.() ?? 0}:${path}:${JSON.stringify(body)}`,
42727
+ failureCooldownMs: 2e3
42728
+ });
42729
+ } catch (error) {
42730
+ if (invalidatesMemberSnapshot(error)) {
42731
+ this.runtime.invalidateMemberCache?.();
42732
+ await this.runtime.stateStore.clearConversationMembers?.(session.userId, group).catch(() => void 0);
42733
+ }
42734
+ throw error;
42735
+ }
42736
+ }
42737
+ async sealMemberPageCursor(userId, group, after) {
42738
+ const encoded = encodeOpaqueJson$6({
42739
+ userId,
42740
+ group,
42741
+ after
42742
+ });
42743
+ return `arkme-member-page-v1.${encoded}.${createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url")}`;
42744
+ }
42745
+ async openMemberPageCursor(cursor, userId, group) {
42746
+ const [prefix, encoded, signature, extra] = cursor.split(".");
42747
+ if (cursor.length > 4096 || prefix !== "arkme-member-page-v1" || !encoded || !signature || extra !== void 0) throw new ArkmePluginError("member-cursor-invalid", "成员分页游标无效", false, 400);
42748
+ const expected = createHmac("sha256", await this.runtime.stateStore.uniqueCode()).update(encoded).digest("base64url");
42749
+ if (Buffer.byteLength(signature) !== expected.length || !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) throw new ArkmePluginError("member-cursor-invalid", "成员分页游标无效", false, 400);
42750
+ let value;
42751
+ try {
42752
+ value = objectValue$2(decodeOpaqueJson$5(encoded));
42753
+ } catch {
42754
+ throw new ArkmePluginError("member-cursor-invalid", "成员分页游标无效", false, 400);
42755
+ }
42756
+ if (value.userId !== userId || value.group !== group || !Number.isSafeInteger(value.after) || numberValue$19(value.after) <= 0) throw new ArkmePluginError("member-cursor-invalid", "成员分页游标与当前账号或群不匹配", false, 403);
42757
+ return numberValue$19(value.after);
42758
+ }
41678
42759
  async listSourceMembers(sourceRef, options = {}) {
41679
42760
  const session = await this.runtime.requireSession();
41680
42761
  const source = await this.source.openSourceRef(sourceRef, session.userId);
@@ -42084,6 +43165,14 @@ var ChatService = class {
42084
43165
  }, session, options.signal);
42085
43166
  const item = objectValue$2(data.item);
42086
43167
  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);
43168
+ this.runtime.invalidateMemberCache?.();
43169
+ await this.runtime.stateStore.mergeConversationMembers?.(session.userId, source.ownerRef, {
43170
+ kind: "presentation",
43171
+ source: await this.source.sourceItem(source),
43172
+ items: [],
43173
+ removedMemberRefs: [normalizedMemberRef],
43174
+ unavailableProfileMemberRefs: []
43175
+ }).catch(() => void 0);
42087
43176
  return {
42088
43177
  sourceRef: normalizedSourceRef,
42089
43178
  memberRef: normalizedMemberRef,
@@ -42263,6 +43352,41 @@ var ChatService = class {
42263
43352
  userName: firstJoinDisplayName(item, ["display_name", "displayName"])
42264
43353
  };
42265
43354
  });
43355
+ if (options.basicOnly === true) {
43356
+ const cache = await this.runtime.stateStore.cachedConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
43357
+ const cached = new Map(cache?.items.map((member) => [member.memberRef, member]));
43358
+ const items = [];
43359
+ for (const member of receiptMembers) {
43360
+ const memberRef = await this.sealChatMemberRef(session.userId, source.ownerRef, member.userId);
43361
+ const known = cached.get(memberRef);
43362
+ const names = resolveChatMemberDisplayNames({
43363
+ userId: member.userId,
43364
+ remarkCandidates: [member.remarkName],
43365
+ memberNameCandidates: [member.memberName],
43366
+ userNameCandidates: [member.userName]
43367
+ });
43368
+ const currentName = names.displayName !== "群成员";
43369
+ items.push({
43370
+ memberRef,
43371
+ displayName: currentName ? names.displayName : known?.displayName ?? names.displayName,
43372
+ displayNameIsCurrent: currentName,
43373
+ ...known?.avatarRef === void 0 ? {} : { avatarRef: known.avatarRef },
43374
+ readStatus: member.readStatus,
43375
+ ...member.readStatus === "read" && member.readAtMillis > 0 ? { readAtMillis: member.readAtMillis } : {}
43376
+ });
43377
+ }
43378
+ const readCount = items.filter((member) => member.readStatus === "read").length;
43379
+ return {
43380
+ sourceRef,
43381
+ itemUid: message.itemUid,
43382
+ sequence: message.sequence,
43383
+ items,
43384
+ readCount,
43385
+ unreadCount: items.length - readCount,
43386
+ totalMemberCount: items.length,
43387
+ presentationComplete: false
43388
+ };
43389
+ }
42266
43390
  const userIds = receiptMembers.map((item) => item.userId);
42267
43391
  const missingRemarkUserIds = receiptMembers.filter((item) => item.remarkName === "").map((item) => item.userId);
42268
43392
  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 +45394,7 @@ var ChatService = class {
44270
45394
  }
44271
45395
  async projectChatMembers(chatSessionUid, rawItems, session, options) {
44272
45396
  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())]);
45397
+ 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
45398
  const members = [];
44275
45399
  for (const item of rawItems) {
44276
45400
  const userId = Math.trunc(numberValue$19(item.user_id));
@@ -73689,6 +74813,8 @@ var ContactDirectoryService = class {
73689
74813
  world;
73690
74814
  chat;
73691
74815
  contactRefs = /* @__PURE__ */ new Map();
74816
+ remarkRevision = 0;
74817
+ recentRemarks = /* @__PURE__ */ new Map();
73692
74818
  constructor(runtime, source, bot, profile, world, chat) {
73693
74819
  this.runtime = runtime;
73694
74820
  this.source = source;
@@ -73699,6 +74825,7 @@ var ContactDirectoryService = class {
73699
74825
  }
73700
74826
  dispose() {
73701
74827
  this.contactRefs.clear();
74828
+ this.recentRemarks.clear();
73702
74829
  }
73703
74830
  async listRecordingSpeakerUsers(session, signal) {
73704
74831
  const descriptors = await this.loadMergedContactDescriptors(session, signal === void 0 ? {} : { signal });
@@ -73755,9 +74882,12 @@ var ContactDirectoryService = class {
73755
74882
  const { session, entry } = await this.resolveContactRef(contactRef);
73756
74883
  const publicProfile = (await this.profile.publicProfileSummariesByUserIds([entry.targetUserId], session, signal)).get(entry.targetUserId);
73757
74884
  const identity = await this.contactIdentity(entry.targetUserId, entry.remark, publicProfile, session, entry.displayNameSnapshot);
74885
+ const remark = this.contactRefs.get(contactRef.trim())?.remark ?? entry.remark;
73758
74886
  const updated = {
73759
74887
  ...entry,
73760
- ...identity
74888
+ ...identity,
74889
+ remark,
74890
+ displayName: remark || identity.nickname || identity.accountName || entry.displayNameSnapshot || "联系人"
73761
74891
  };
73762
74892
  this.contactRefs.set(contactRef.trim(), updated);
73763
74893
  return {
@@ -73765,9 +74895,49 @@ var ContactDirectoryService = class {
73765
74895
  displayName: updated.displayName,
73766
74896
  nickname: updated.nickname,
73767
74897
  remark: updated.remark,
74898
+ ...updated.accountName === void 0 ? {} : { accountName: updated.accountName },
73768
74899
  ...updated.avatarRef === void 0 ? {} : { avatarRef: updated.avatarRef }
73769
74900
  };
73770
74901
  }
74902
+ async updateContactRemark(contactRef, remarkValue, signal) {
74903
+ if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
74904
+ const { session, entry } = await this.resolveContactRef(contactRef);
74905
+ const remark = remarkValue.trim();
74906
+ if (Array.from(remark).length > 100) throw new ArkmePluginError("directory-contact-remark-too-long", "备注名最多 100 个字符", false, 400);
74907
+ if (entry.chatSessionUid === void 0) throw new ArkmePluginError("directory-contact-session-unavailable", "暂时无法修改此联系人的备注,请刷新联系人后重试", true, 409);
74908
+ if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
74909
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/contacts/update-remark", {
74910
+ chat_session_uid: entry.chatSessionUid,
74911
+ remark,
74912
+ update_at: Date.now()
74913
+ }, session, signal);
74914
+ this.runtime.invalidateKey(this.runtime.requestScope(session.userId), "directory:contacts:");
74915
+ const contact = objectValue$2(data.contact);
74916
+ 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);
74917
+ const savedRemark = stringValue$2(contact.remark).trim();
74918
+ this.remarkRevision += 1;
74919
+ this.recentRemarks.set(`${session.userId}:${entry.targetUserId}`, {
74920
+ revision: this.remarkRevision,
74921
+ remark: savedRemark
74922
+ });
74923
+ for (const [ref, current] of this.contactRefs) {
74924
+ if (current.viewerUserId !== session.userId || current.targetUserId !== entry.targetUserId) continue;
74925
+ this.contactRefs.set(ref, {
74926
+ ...current,
74927
+ remark: savedRemark,
74928
+ displayName: savedRemark || current.nickname || current.accountName || current.displayNameSnapshot || "联系人"
74929
+ });
74930
+ }
74931
+ const current = this.contactRefs.get(contactRef.trim()) ?? entry;
74932
+ return {
74933
+ contactRef: contactRef.trim(),
74934
+ remark: savedRemark,
74935
+ displayName: savedRemark || current.nickname || current.accountName || current.displayNameSnapshot || "联系人",
74936
+ nickname: current.nickname,
74937
+ ...current.accountName === void 0 ? {} : { accountName: current.accountName },
74938
+ ...current.avatarRef === void 0 ? {} : { avatarRef: current.avatarRef }
74939
+ };
74940
+ }
73771
74941
  async contactWorld(contactRef, options = {}) {
73772
74942
  const { entry } = await this.resolveContactRef(contactRef);
73773
74943
  return await this.world.listUserWorldFeed(entry.targetUserId, cloneWorldOptions(options));
@@ -73830,6 +75000,7 @@ var ContactDirectoryService = class {
73830
75000
  const session = await this.runtime.requireSession();
73831
75001
  const limit = boundedLimit$1(options.limit);
73832
75002
  const offset = await this.openOffsetCursor("contacts", options.cursor, session.userId);
75003
+ const revisionAtStart = this.remarkRevision;
73833
75004
  const descriptors = await this.loadMergedContactDescriptors(session, options);
73834
75005
  const pageDescriptors = descriptors.slice(offset, offset + limit);
73835
75006
  const profiles = await this.profile.publicProfileSummariesByUserIds(pageDescriptors.map((item) => item.targetUserId), session, options.signal);
@@ -73837,7 +75008,17 @@ var ContactDirectoryService = class {
73837
75008
  this.pruneContactRefs();
73838
75009
  for (const descriptor of pageDescriptors) {
73839
75010
  const identity = await this.contactIdentity(descriptor.targetUserId, descriptor.remark, profiles.get(descriptor.targetUserId), session, descriptor.displayNameSnapshot);
73840
- const contactRef = `arkme-directory-contact-v1.${randomUUID()}`;
75011
+ const recent = this.recentRemarks.get(`${session.userId}:${descriptor.targetUserId}`);
75012
+ if (recent !== void 0 && recent.revision > revisionAtStart) {
75013
+ identity.remark = recent.remark;
75014
+ identity.displayName = recent.remark || identity.nickname || identity.accountName || descriptor.displayNameSnapshot || "联系人";
75015
+ }
75016
+ let contactRef;
75017
+ for (const [ref, current] of this.contactRefs) if (current.viewerUserId === session.userId && current.targetUserId === descriptor.targetUserId && current.expiresAtMillis > Date.now()) {
75018
+ contactRef = ref;
75019
+ break;
75020
+ }
75021
+ contactRef ??= `arkme-directory-contact-v1.${randomUUID()}`;
73841
75022
  const entry = {
73842
75023
  viewerUserId: session.userId,
73843
75024
  targetUserId: descriptor.targetUserId,
@@ -73846,6 +75027,7 @@ var ContactDirectoryService = class {
73846
75027
  ...identity,
73847
75028
  expiresAtMillis: Date.now() + CONTACT_DIRECTORY_REF_TTL_MS
73848
75029
  };
75030
+ this.contactRefs.delete(contactRef);
73849
75031
  this.contactRefs.set(contactRef, entry);
73850
75032
  const item = {
73851
75033
  kind: "contact",
@@ -73862,12 +75044,21 @@ var ContactDirectoryService = class {
73862
75044
  }
73863
75045
  this.pruneContactRefs();
73864
75046
  const hasMore = offset + pageDescriptors.length < descriptors.length;
75047
+ const nextCursor = hasMore ? await this.sealOffsetCursor("contacts", offset + pageDescriptors.length, session.userId) : void 0;
75048
+ for (const item of items) {
75049
+ if (item.kind !== "contact") continue;
75050
+ const current = this.contactRefs.get(item.contactRef);
75051
+ if (current === void 0) continue;
75052
+ item.remark = current.remark;
75053
+ item.displayName = current.displayName;
75054
+ item.letter = contactDirectoryLetter(item);
75055
+ }
73865
75056
  return {
73866
75057
  section: "contacts",
73867
75058
  items,
73868
75059
  total: descriptors.length,
73869
75060
  hasMore,
73870
- ...hasMore ? { nextCursor: await this.sealOffsetCursor("contacts", offset + pageDescriptors.length, session.userId) } : {}
75061
+ ...nextCursor === void 0 ? {} : { nextCursor }
73871
75062
  };
73872
75063
  }
73873
75064
  async loadMergedContactDescriptors(session, options) {
@@ -75190,13 +76381,15 @@ var GroupService = class {
75190
76381
  });
75191
76382
  continue;
75192
76383
  }
75193
- const outcome = stringValue$2(objectValue$2((await this.runtime.authenticatedChatPost("/api/v1/chats/members/add", {
76384
+ const data = await this.runtime.authenticatedChatPost("/api/v1/chats/members/add", {
75194
76385
  chat_session_uid: source.ownerRef,
75195
76386
  target_user_id: candidate.targetUserId,
75196
76387
  join_at: Date.now(),
75197
76388
  display_name_snapshot: candidate.displayName,
75198
76389
  extra: { source: "dsh_arkme_candidate" }
75199
- }, session, signal)).item).outcome);
76390
+ }, session, signal);
76391
+ this.runtime.invalidateMemberCache?.();
76392
+ const outcome = stringValue$2(objectValue$2(data.item).outcome);
75200
76393
  results.push({
75201
76394
  candidateRef,
75202
76395
  displayName: candidate.displayName,
@@ -75412,6 +76605,8 @@ var GroupService = class {
75412
76605
  target_user_id: session.userId,
75413
76606
  action: 1
75414
76607
  }, session, signal);
76608
+ this.runtime.invalidateMemberCache?.();
76609
+ await this.runtime.stateStore.clearConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
75415
76610
  this.source.invalidateSourceListCache(session.userId, "root");
75416
76611
  return { status: "ok" };
75417
76612
  }
@@ -75423,6 +76618,8 @@ var GroupService = class {
75423
76618
  chat_session_uid: source.ownerRef,
75424
76619
  update_at: Date.now()
75425
76620
  }, session, signal);
76621
+ this.runtime.invalidateMemberCache?.();
76622
+ await this.runtime.stateStore.clearConversationMembers?.(session.userId, source.ownerRef).catch(() => void 0);
75426
76623
  this.source.invalidateSourceListCache(session.userId, "root");
75427
76624
  return { status: "ok" };
75428
76625
  }
@@ -76612,12 +77809,28 @@ var MediaService = class {
76612
77809
  }, void 0, 11e4);
76613
77810
  }
76614
77811
  async readImage(imageRef, options = {}) {
77812
+ options.signal?.throwIfAborted();
76615
77813
  const session = await this.runtime.requireSession();
76616
77814
  const isProfileImage = imageRef.trim().startsWith("arkme-profile-image-v1.");
77815
+ const isBotImage = imageRef.trim().startsWith("arkme-bot-image-v1.");
77816
+ const isAvatar = isProfileImage || isBotImage;
76617
77817
  const maximumBytes = isProfileImage ? MAX_ARKME_PROFILE_IMAGE_BYTES : MAX_ARKME_IMAGE_BYTES;
76618
77818
  const byteLimit = Math.min(maximumBytes, Math.max(1, Math.trunc(options.maxBytes ?? maximumBytes)));
76619
77819
  const cacheKey = `${String(session.userId)}:${String(byteLimit)}:${imageRef.trim()}`;
76620
- const cached = this.cachedImage(cacheKey);
77820
+ if (isProfileImage) await this.profile.openProfileImageRef(imageRef, session.userId);
77821
+ const persisted = isAvatar ? await this.runtime.stateStore.readAvatarCache?.(session.userId, imageRef).catch(() => void 0) : void 0;
77822
+ if (persisted !== void 0 && options.refresh !== true && persisted.bytes <= byteLimit) {
77823
+ this.cacheImage(cacheKey, persisted);
77824
+ return cloneImageBytes(persisted);
77825
+ }
77826
+ if (isBotImage) try {
77827
+ if (this.botImages === void 0) throw new ArkmePluginError("bot-image-ref-invalid", "Bot 头像引用不可用", false, 403);
77828
+ await this.botImages.openBotImageRef(imageRef, session.userId);
77829
+ } catch (error) {
77830
+ if (persisted !== void 0 && persisted.bytes <= byteLimit) return cloneImageBytes(persisted);
77831
+ throw error;
77832
+ }
77833
+ const cached = options.refresh === true ? void 0 : this.cachedImage(cacheKey);
76621
77834
  if (cached !== void 0) return cached;
76622
77835
  const existing = this.imageInFlight.get(cacheKey);
76623
77836
  if (existing !== void 0) return cloneImageBytes(await existing);
@@ -76627,8 +77840,13 @@ var MediaService = class {
76627
77840
  try {
76628
77841
  const value = await pending;
76629
77842
  this.cacheImage(cacheKey, value);
77843
+ if (isAvatar) await this.runtime.stateStore.writeAvatarCache?.(session.userId, imageRef, value).catch(() => {
77844
+ console.warn("dsh-arkme: avatar_cache_write_failed");
77845
+ });
76630
77846
  return cloneImageBytes(value);
76631
77847
  } catch (error) {
77848
+ options.signal?.throwIfAborted();
77849
+ if (persisted !== void 0 && persisted.bytes <= byteLimit) return cloneImageBytes(persisted);
76632
77850
  if (isProfileImage) logArkmeAvatarDiagnostic("image_read_failed", {
76633
77851
  environment: this.runtime.config.environment,
76634
77852
  viewerUserId: session.userId,
@@ -83017,6 +84235,9 @@ var UnmarkedSpeakerService = class {
83017
84235
  return ref;
83018
84236
  }
83019
84237
  sealCursor(kind, upstreamCursor, viewerUserId, candidateRef) {
84238
+ if (kind === "candidate") {
84239
+ for (const [ref, entry] of this.cursorRefs) if (entry.kind === kind && entry.viewerUserId === viewerUserId && entry.upstreamCursor === upstreamCursor && entry.expiresAtMillis > Date.now()) return ref;
84240
+ }
83020
84241
  const ref = `${kind === "candidate" ? "arkme-unmarked-candidate-cursor-v1" : "arkme-unmarked-segment-cursor-v1"}.${randomUUID()}`;
83021
84242
  this.cursorRefs.set(ref, {
83022
84243
  viewerUserId,
@@ -85356,6 +86577,7 @@ var ArkmeService = class {
85356
86577
  extensionReview;
85357
86578
  media;
85358
86579
  privacy;
86580
+ directory;
85359
86581
  source;
85360
86582
  conversationDirectoryVisibility;
85361
86583
  record;
@@ -85440,6 +86662,20 @@ var ArkmeService = class {
85440
86662
  this.aiPolish = new GroupAiPolishService(this.runtime, this.source, { sendChatSourceTextRaw: async (...args) => await this.chat.sendChatSourceTextRaw(...args) });
85441
86663
  this.realtime = new ChatRealtimeService(this.runtime, this.source, { chatTimelineItems: async (data, session, chatSessionUid, sourceKind) => await this.chat.chatTimelineItems(data, session, chatSessionUid, sourceKind) });
85442
86664
  this.conversationDirectoryVisibility = new ConversationDirectoryVisibilityService(new ConversationListPreferenceService(this.runtime), this.source, this.bot, this.realtime);
86665
+ 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, {
86666
+ signal,
86667
+ refresh: true
86668
+ }), (page) => {
86669
+ this.realtime.emitChatClientEvent({
86670
+ type: "directory-update",
86671
+ revision: this.realtime.nextChatClientRevision(),
86672
+ page
86673
+ });
86674
+ }, async (bots, userId) => await this.bot.restoreDirectoryBots(bots, userId));
86675
+ this.realtime.directoryBaseline = async () => await this.directory.complete();
86676
+ this.realtime.subscribeChatRealtime((event) => {
86677
+ if (event.type !== "directory-update") this.directory.accept(event).catch(() => void 0);
86678
+ });
85443
86679
  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
86680
  this.userBan = new UserBanService(this.runtime, this.chat);
85445
86681
  this.directMessageAdmissionOwner = new DirectMessageAdmissionService(this.runtime, this.source);
@@ -85543,6 +86779,7 @@ var ArkmeService = class {
85543
86779
  return await this.backgroundSoundPreferenceOwner.update(enabled, signal, expectedUserId);
85544
86780
  }
85545
86781
  clearAccountState(userIds) {
86782
+ this.directory.reset();
85546
86783
  this.realtime.resetAttentionSummary();
85547
86784
  for (const userId of userIds) this.privacy.clear(userId);
85548
86785
  this.fileTransfers?.cancelActive();
@@ -85590,13 +86827,19 @@ var ArkmeService = class {
85590
86827
  return await this.bot.connectOpenClawBot(botRef, options);
85591
86828
  }
85592
86829
  async listBots(options = {}) {
85593
- return await this.bot.listBots(options);
86830
+ const { userId } = await this.runtime.requireSession();
86831
+ const result = await this.bot.listBots(options);
86832
+ this.directory.rememberBots(result.items, userId).catch(() => void 0);
86833
+ return result;
85594
86834
  }
85595
86835
  async createBot(input, options = {}) {
85596
86836
  return await this.bot.createBot(input, options);
85597
86837
  }
85598
86838
  async createBotSummary(input, options = {}) {
85599
- return await this.bot.createBotSummary(input, options);
86839
+ const { userId } = await this.runtime.requireSession();
86840
+ const bot = await this.bot.createBotSummary(input, options);
86841
+ this.directory.rememberBots([bot], userId).catch(() => void 0);
86842
+ return bot;
85600
86843
  }
85601
86844
  async revealBotSecret(botRef, options = {}) {
85602
86845
  return await this.bot.revealBotSecret(botRef, options);
@@ -85611,7 +86854,9 @@ var ArkmeService = class {
85611
86854
  return await this.bot.revealManagedBotToken(botRef, options);
85612
86855
  }
85613
86856
  async deleteManagedBot(botRef, confirmationName, options = {}) {
86857
+ const { userId } = await this.runtime.requireSession();
85614
86858
  await this.bot.deleteManagedBot(botRef, confirmationName, options);
86859
+ await this.directory.forgetBot(botRef, userId).catch(() => void 0);
85615
86860
  }
85616
86861
  async botNotificationPreference(botRef, options = {}) {
85617
86862
  return await this.botConversation.notificationPreference(botRef, options);
@@ -85712,6 +86957,7 @@ var ArkmeService = class {
85712
86957
  recordCalendar: true,
85713
86958
  imageLibrary: true,
85714
86959
  sourceDirectory: true,
86960
+ localFirstDirectory: true,
85715
86961
  sourceTimeline: true,
85716
86962
  forwardContent: true,
85717
86963
  sourceTextSend: true,
@@ -85728,6 +86974,7 @@ var ArkmeService = class {
85728
86974
  outgoingCall: true,
85729
86975
  callHistory: true,
85730
86976
  groupMembers: true,
86977
+ memberDirectoryPaging: true,
85731
86978
  groupMemberAdd: true,
85732
86979
  userCard: true,
85733
86980
  openPrivateChat: true,
@@ -85804,6 +87051,7 @@ var ArkmeService = class {
85804
87051
  return await this.callHistory.retryCallSummary(callRef, signal);
85805
87052
  }
85806
87053
  dispose() {
87054
+ this.directory.reset();
85807
87055
  this.record.dispose();
85808
87056
  this.realtime.resetAttentionSummary();
85809
87057
  this.fileTransfers?.cancelActive();
@@ -85849,6 +87097,9 @@ var ArkmeService = class {
85849
87097
  async directoryContactProfile(contactRef, signal) {
85850
87098
  return await this.contactDirectory.contactProfile(contactRef, signal);
85851
87099
  }
87100
+ async updateDirectoryContactRemark(contactRef, remark, signal) {
87101
+ return await this.contactDirectory.updateContactRemark(contactRef, remark, signal);
87102
+ }
85852
87103
  async directoryContactWorld(contactRef, options = {}) {
85853
87104
  return await this.contactDirectory.contactWorld(contactRef, options);
85854
87105
  }
@@ -85974,7 +87225,10 @@ var ArkmeService = class {
85974
87225
  return await this.profile.refreshProfile();
85975
87226
  }
85976
87227
  async arkoProfile(signal) {
85977
- return await this.arko.arkoProfile(signal);
87228
+ const { userId } = await this.runtime.requireSession();
87229
+ const profile = await this.arko.arkoProfile(signal);
87230
+ this.directory.rememberSpecial({ arkoProfile: profile }, userId).catch(() => void 0);
87231
+ return profile;
85978
87232
  }
85979
87233
  async arkoEnsureSession(signal) {
85980
87234
  return await this.arko.arkoEnsureSession(signal);
@@ -85989,7 +87243,16 @@ var ArkmeService = class {
85989
87243
  return await this.arko.arkoActivateModel(routeKey, signal);
85990
87244
  }
85991
87245
  async arkoHistoryPage(limit = 50, offset = 0, signal) {
85992
- return await this.arko.arkoHistoryPage(limit, offset, signal);
87246
+ const { userId } = await this.runtime.requireSession();
87247
+ const page = await this.arko.arkoHistoryPage(limit, offset, signal);
87248
+ if (offset === 0) {
87249
+ const latest = [...page.items].filter((item) => item.text.trim() !== "").sort((a, b) => b.createdAtMillis - a.createdAtMillis || b.messageId - a.messageId)[0];
87250
+ if (latest !== void 0) this.directory.rememberSpecial({ arkoPreview: {
87251
+ text: latest.text,
87252
+ createdAtMillis: latest.createdAtMillis
87253
+ } }, userId).catch(() => void 0);
87254
+ }
87255
+ return page;
85993
87256
  }
85994
87257
  async arkoAsk(text, options = {}) {
85995
87258
  return await this.arko.arkoAsk(text, options);
@@ -86046,16 +87309,34 @@ var ArkmeService = class {
86046
87309
  return await this.source.moveTopicHierarchy(sourceRef, currentParentSourceRef, nextParentSourceRef, insertBeforeSourceRef);
86047
87310
  }
86048
87311
  async listSources(directory, options = {}) {
86049
- return await this.source.listSources(directory, options);
87312
+ if (options.localFirst === true) {
87313
+ if (directory !== "root") throw new ArkmePluginError("source-directory-invalid", "本地目录仅支持会话列表", false);
87314
+ return await this.directory.read(options.refresh === true, options.signal);
87315
+ }
87316
+ const { userId } = await this.runtime.requireSession();
87317
+ const page = await this.source.listSources(directory, options);
87318
+ if (directory === "send_to_self") {
87319
+ const sendToSelf = page.items.find((item) => item.kind === "send_to_self");
87320
+ if (sendToSelf !== void 0) this.directory.rememberSpecial({ sendToSelf }, userId).catch(() => void 0);
87321
+ }
87322
+ return page;
86050
87323
  }
86051
87324
  async setChatDirectoryPin(sourceRef, pinned, signal) {
86052
- return await this.source.setChatDirectoryPin(sourceRef, pinned, signal);
87325
+ const { userId } = await this.runtime.requireSession();
87326
+ const result = await this.source.setChatDirectoryPin(sourceRef, pinned, signal);
87327
+ await this.directory.confirmPin(sourceRef, result.pinned, result.policyUpdatedAtMillis, userId).catch(() => void 0);
87328
+ return result;
87329
+ }
87330
+ async setBotDirectoryPin(botRef, pinned) {
87331
+ await this.directory.pinBot(botRef, pinned);
86053
87332
  }
86054
87333
  async conversationDirectoryVisibilitySnapshot(sourceRefs, botRefs, signal) {
86055
87334
  return await this.conversationDirectoryVisibility.query(sourceRefs, botRefs, signal);
86056
87335
  }
86057
87336
  async setConversationDirectoryVisibility(entryKind, entryRef, hidden, signal) {
87337
+ const { userId } = await this.runtime.requireSession();
86058
87338
  await this.conversationDirectoryVisibility.setVisibility(entryKind, entryRef, hidden, signal);
87339
+ await this.directory.confirmVisibility(entryKind, entryRef, hidden, userId).catch(() => void 0);
86059
87340
  }
86060
87341
  async dshBetaCommunityEntryState(signal) {
86061
87342
  return await this.community.dshBetaCommunityEntryState(signal);
@@ -86116,6 +87397,15 @@ var ArkmeService = class {
86116
87397
  async listGroupMembers(sourceRef, options = {}) {
86117
87398
  return await this.group.listGroupMembers(sourceRef, options);
86118
87399
  }
87400
+ async cachedSourceMembers(sourceRef, signal) {
87401
+ return await this.chat.cachedSourceMembers(sourceRef, signal);
87402
+ }
87403
+ async pageSourceMembers(sourceRef, options = {}) {
87404
+ return await this.chat.pageSourceMembers(sourceRef, options);
87405
+ }
87406
+ async sourceMembersPresentation(sourceRef, memberRefs, options = {}) {
87407
+ return await this.chat.sourceMembersPresentation(sourceRef, memberRefs, options);
87408
+ }
86119
87409
  async listSourceMembers(sourceRef, options = {}) {
86120
87410
  return await this.chat.listSourceMembers(sourceRef, options);
86121
87411
  }
@@ -86154,10 +87444,16 @@ var ArkmeService = class {
86154
87444
  return result;
86155
87445
  }
86156
87446
  async leaveGroup(sourceRef, signal) {
86157
- return await this.group.leaveGroup(sourceRef, signal);
87447
+ const { userId } = await this.runtime.requireSession();
87448
+ const result = await this.group.leaveGroup(sourceRef, signal);
87449
+ await this.directory.forgetSource(sourceRef, userId).catch(() => void 0);
87450
+ return result;
86158
87451
  }
86159
87452
  async dissolveGroup(sourceRef, signal) {
86160
- return await this.group.dissolveGroup(sourceRef, signal);
87453
+ const { userId } = await this.runtime.requireSession();
87454
+ const result = await this.group.dissolveGroup(sourceRef, signal);
87455
+ await this.directory.forgetSource(sourceRef, userId).catch(() => void 0);
87456
+ return result;
86161
87457
  }
86162
87458
  async reportGroup(sourceRef, reason, signal) {
86163
87459
  return await this.group.reportGroup(sourceRef, reason, signal);
@@ -88230,4 +89526,4 @@ async function invokePersistentArkmeExtension(extensionId, method, args) {
88230
89526
  return jsonValue(await handler(jsonValue(args)));
88231
89527
  }
88232
89528
  //#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 };
89529
+ export { parseArkmeRecordReeditAttachments as $, inspectNativeBundleArtifactV3 as A, ARKME_EXTENSION_ICON_MAX_BYTES as B, ARKME_BUNDLE_MAX_BYTES as C, ArkmeDirectMessageAdmissionError as Ct, 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, 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, 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 };