@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.
- package/docs/dsh-remote-liveness.md +46 -0
- package/docs/member-directory-premerge-review.md +99 -0
- package/docs/member-directory-review.md +53 -0
- package/docs/plugin-release.md +9 -3
- package/docs/shared-conversation-members.md +61 -0
- package/docs/sidebar-local-first-review.md +104 -0
- package/docs/sidebar-local-first.md +61 -0
- package/lib/bundle-runtime.js +1 -1
- package/lib/client.js +68477 -66624
- package/lib/client.js.map +1 -1
- package/lib/extension-profile-restart-helper.js +5 -5
- package/lib/harness-model-client.js +36 -2
- package/lib/harness-session-client.js +124 -0
- package/lib/index.js +2837 -1532
- package/lib/{install-store-C1tAM346.js → install-store-bnyQDtYt.js} +1 -1
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-DpNP9UKr.js → persistent-runtime-BOm3JEDw.js} +1573 -277
- package/lib/{plugin-updater-helper-BByro9U_.js → plugin-updater-helper-B0yVav4f.js} +1 -1
- package/lib/{plugin-updater-helper-BByw0ZyG.js → plugin-updater-helper-BbjKUV9T.js} +3 -3
- package/lib/plugin-updater-helper.js +1 -1
- package/lib/sdk.js +36 -2
- package/lib/types/arkme-service.d.ts +14 -0
- package/lib/types/arkme-service.d.ts.map +1 -1
- package/lib/types/chat-realtime.d.ts +6 -3
- package/lib/types/chat-realtime.d.ts.map +1 -1
- package/lib/types/client/ArkmeAvatar.d.ts +2 -1
- package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
- package/lib/types/client/ArkmeContactAddSurface.d.ts +2 -1
- package/lib/types/client/ArkmeContactAddSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeDSHBetaCommunityEntry.d.ts.map +1 -1
- package/lib/types/client/ArkmeDirectoryWindow.d.ts +6 -0
- package/lib/types/client/ArkmeDirectoryWindow.d.ts.map +1 -0
- package/lib/types/client/ArkmeGroupChatControls.d.ts +1 -1
- package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
- package/lib/types/client/ArkmeMessageReadReceipt.d.ts.map +1 -1
- package/lib/types/client/ArkmePersistentShell.d.ts +1 -1
- package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
- package/lib/types/client/ArkmeRecordingSurface.d.ts +3 -1
- package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts +2 -1
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
- package/lib/types/client/DeepSeekHarnessSurface.d.ts +3 -1
- package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/avatar-image-store.d.ts +3 -2
- package/lib/types/client/avatar-image-store.d.ts.map +1 -1
- package/lib/types/client/bot-chat-directory-projection.d.ts +2 -0
- package/lib/types/client/bot-chat-directory-projection.d.ts.map +1 -1
- package/lib/types/client/bot-directory-preferences.d.ts +2 -0
- package/lib/types/client/bot-directory-preferences.d.ts.map +1 -1
- package/lib/types/client/chat-directory-store.d.ts +4 -0
- package/lib/types/client/chat-directory-store.d.ts.map +1 -1
- package/lib/types/client/conversation-members-store.d.ts +46 -0
- package/lib/types/client/conversation-members-store.d.ts.map +1 -0
- package/lib/types/client/harness-session-client.d.ts +5 -0
- package/lib/types/client/harness-session-client.d.ts.map +1 -0
- package/lib/types/client/interwoven-moments.d.ts +9 -0
- package/lib/types/client/interwoven-moments.d.ts.map +1 -1
- package/lib/types/client/message-read-receipt-store.d.ts +7 -1
- package/lib/types/client/message-read-receipt-store.d.ts.map +1 -1
- package/lib/types/client/realtime-client-events.d.ts.map +1 -1
- package/lib/types/client/recordings/ArkmeRecordingImportDialog.d.ts +4 -1
- package/lib/types/client/recordings/ArkmeRecordingImportDialog.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts +3 -2
- package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/ContactDirectoryAddDialog.d.ts +8 -0
- package/lib/types/client/redesign/contacts/ContactDirectoryAddDialog.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts +13 -4
- package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/ContactDirectoryToolbar.d.ts +7 -0
- package/lib/types/client/redesign/contacts/ContactDirectoryToolbar.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts +12 -2
- package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/ContactRemarkDialog.d.ts +9 -0
- package/lib/types/client/redesign/contacts/ContactRemarkDialog.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/ContactWorldList.d.ts +2 -0
- package/lib/types/client/redesign/contacts/ContactWorldList.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts +3 -2
- package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/contact-date.d.ts +3 -0
- package/lib/types/client/redesign/contacts/contact-date.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/contact-directory-search.d.ts +10 -0
- package/lib/types/client/redesign/contacts/contact-directory-search.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/contact-directory-state.d.ts +4 -1
- package/lib/types/client/redesign/contacts/contact-directory-state.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts +6 -2
- package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts.map +1 -1
- package/lib/types/client/use-arkme-avatar-image.d.ts +2 -1
- package/lib/types/client/use-arkme-avatar-image.d.ts.map +1 -1
- package/lib/types/client/use-conversation-members.d.ts +3 -0
- package/lib/types/client/use-conversation-members.d.ts.map +1 -0
- package/lib/types/dsh-remote/api-proxy-adapter.d.ts +1 -0
- package/lib/types/dsh-remote/api-proxy-adapter.d.ts.map +1 -1
- package/lib/types/dsh-remote/channel-manager.d.ts +9 -2
- package/lib/types/dsh-remote/channel-manager.d.ts.map +1 -1
- package/lib/types/dsh-remote/connection-diagnostics.d.ts +35 -0
- package/lib/types/dsh-remote/connection-diagnostics.d.ts.map +1 -0
- package/lib/types/dsh-remote/current-session-tool.d.ts +3 -0
- package/lib/types/dsh-remote/current-session-tool.d.ts.map +1 -0
- package/lib/types/dsh-remote/default-socket-factory.d.ts.map +1 -1
- package/lib/types/dsh-remote/desktop-session-presence.d.ts +21 -0
- package/lib/types/dsh-remote/desktop-session-presence.d.ts.map +1 -0
- package/lib/types/dsh-remote/dsh-event-contract.d.ts +2 -0
- package/lib/types/dsh-remote/dsh-event-contract.d.ts.map +1 -1
- package/lib/types/dsh-remote/host.d.ts +40 -6
- package/lib/types/dsh-remote/host.d.ts.map +1 -1
- package/lib/types/dsh-remote/live-event-batcher.d.ts +46 -0
- package/lib/types/dsh-remote/live-event-batcher.d.ts.map +1 -0
- package/lib/types/dsh-remote/protocol-v1.d.ts.map +1 -1
- package/lib/types/dsh-remote/realtime-transport.d.ts +19 -1
- package/lib/types/dsh-remote/realtime-transport.d.ts.map +1 -1
- package/lib/types/dsh-remote/types.d.ts +18 -2
- package/lib/types/dsh-remote/types.d.ts.map +1 -1
- package/lib/types/harness-embed-contract.d.ts +2 -0
- package/lib/types/harness-embed-contract.d.ts.map +1 -1
- package/lib/types/harness-embed-route.d.ts +4 -0
- package/lib/types/harness-embed-route.d.ts.map +1 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/local-database.d.ts +11 -1
- package/lib/types/local-database.d.ts.map +1 -1
- package/lib/types/member-directory.d.ts +11 -0
- package/lib/types/member-directory.d.ts.map +1 -0
- package/lib/types/sdk/index.d.ts +24 -3
- package/lib/types/sdk/index.d.ts.map +1 -1
- package/lib/types/services/bot-service.d.ts +3 -0
- package/lib/types/services/bot-service.d.ts.map +1 -1
- package/lib/types/services/chat-realtime-service.d.ts +1 -0
- package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
- package/lib/types/services/chat-service.d.ts +14 -1
- package/lib/types/services/chat-service.d.ts.map +1 -1
- package/lib/types/services/contact-directory-service.d.ts +3 -0
- package/lib/types/services/contact-directory-service.d.ts.map +1 -1
- package/lib/types/services/conversation-directory-service.d.ts +72 -0
- package/lib/types/services/conversation-directory-service.d.ts.map +1 -0
- package/lib/types/services/desktop-attention-bridge.d.ts +6 -1
- package/lib/types/services/desktop-attention-bridge.d.ts.map +1 -1
- package/lib/types/services/group-service.d.ts.map +1 -1
- package/lib/types/services/media-service.d.ts +1 -0
- package/lib/types/services/media-service.d.ts.map +1 -1
- package/lib/types/services/service.d.ts +11 -0
- package/lib/types/services/service.d.ts.map +1 -1
- package/lib/types/services/source-service.d.ts +3 -0
- package/lib/types/services/source-service.d.ts.map +1 -1
- package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -1
- package/lib/types/tools/business/conversation/index.d.ts.map +1 -1
- package/lib/types/tools/business/conversation/list-sources.d.ts +1 -0
- package/lib/types/tools/business/conversation/list-sources.d.ts.map +1 -1
- package/lib/types/tools/business/conversation/member-pages.d.ts +2 -0
- package/lib/types/tools/business/conversation/member-pages.d.ts.map +1 -0
- package/lib/types/tools/business/conversation/read-receipts.d.ts.map +1 -1
- package/lib/types/tools/business/index.d.ts.map +1 -1
- package/lib/types/tools/ports/conversations.d.ts +13 -1
- package/lib/types/tools/ports/conversations.d.ts.map +1 -1
- package/lib/types/tools/registry/registrar.d.ts.map +1 -1
- package/lib/types/types.d.ts +66 -2
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +7 -6
- package/lib/{bundle-runtime-BTzRlc6V.js → bundle-runtime-CE73h3vS.js} +1 -1
- 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
|
|
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/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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 (
|
|
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
|
|
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(
|
|
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.
|
|
38643
|
+
const shared = this.directoryBaseline === void 0 ? void 0 : await this.directoryBaseline();
|
|
37870
38644
|
let cursor;
|
|
37871
|
-
|
|
37872
|
-
|
|
37873
|
-
|
|
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 (
|
|
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)}` : `
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 _,
|
|
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 };
|