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