chatlab-cli 0.28.0 → 0.29.0
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/dist/{chat-command-7WL5NMZ6.mjs → chat-command-JKG3FY5B.mjs} +8 -5
- package/dist/{chat-command-7WL5NMZ6.mjs.map → chat-command-JKG3FY5B.mjs.map} +1 -1
- package/dist/chunk-2MZ6G4AC.mjs +1339 -0
- package/dist/chunk-2MZ6G4AC.mjs.map +1 -0
- package/dist/chunk-5FGB42EQ.mjs +7 -0
- package/dist/{chunk-WGS33EWH.mjs → chunk-6FPEP2ZX.mjs} +2 -2
- package/dist/chunk-7RW4CQAO.mjs +1310 -0
- package/dist/chunk-7RW4CQAO.mjs.map +1 -0
- package/dist/{chunk-GFRG7TZQ.mjs → chunk-ARZM6HBX.mjs} +17 -15
- package/dist/{chunk-GFRG7TZQ.mjs.map → chunk-ARZM6HBX.mjs.map} +1 -1
- package/dist/{chunk-VASK54KT.mjs → chunk-CIB23U32.mjs} +2 -2
- package/dist/chunk-D4WTE7LV.mjs +526 -0
- package/dist/chunk-D4WTE7LV.mjs.map +1 -0
- package/dist/{chunk-ZSFVX6FM.mjs → chunk-I35WHWA6.mjs} +7 -21
- package/dist/chunk-I35WHWA6.mjs.map +1 -0
- package/dist/{chunk-YQ2FJGBT.mjs → chunk-KL2NGIMQ.mjs} +241 -80
- package/dist/chunk-KL2NGIMQ.mjs.map +1 -0
- package/dist/{chunk-55WR6WKW.mjs → chunk-QQK7MLCR.mjs} +10044 -8354
- package/dist/chunk-QQK7MLCR.mjs.map +1 -0
- package/dist/{chunk-XNU5X7EV.mjs → chunk-SKUG5XVD.mjs} +10 -8
- package/dist/{chunk-XNU5X7EV.mjs.map → chunk-SKUG5XVD.mjs.map} +1 -1
- package/dist/chunk-WPI6YSAR.mjs +8813 -0
- package/dist/chunk-WPI6YSAR.mjs.map +1 -0
- package/dist/cli.mjs +7 -4
- package/dist/contacts-worker.mjs +48 -0
- package/dist/contacts-worker.mjs.map +1 -0
- package/dist/{http-C2FBHNZG.mjs → http-5E7ARYFW.mjs} +12 -9
- package/dist/{import-ED6JCPSJ.mjs → import-3KG6VK6O.mjs} +8 -12
- package/dist/index.mjs +12 -9
- package/dist/{mcp-5JIBER4M.mjs → mcp-JBI4CUQT.mjs} +8 -5
- package/dist/{mcp-5JIBER4M.mjs.map → mcp-JBI4CUQT.mjs.map} +1 -1
- package/dist/people-relationships-worker.mjs +48 -0
- package/dist/people-relationships-worker.mjs.map +1 -0
- package/dist/semantic-index-worker.mjs +1 -1
- package/dist/{src-WBNM42SL.mjs → src-UTBQAOJ3.mjs} +86 -66
- package/dist/{stream-import-37U3SROO.mjs → stream-import-7HZ4IRYJ.mjs} +7 -4
- package/dist/stream-import-7HZ4IRYJ.mjs.map +1 -0
- package/dist/undici-ZSXBT7EA.mjs +18389 -0
- package/dist/undici-ZSXBT7EA.mjs.map +1 -0
- package/dist/{worker-runtime-6DGYL4TF.mjs → worker-runtime-ALS4MR2A.mjs} +13 -5
- package/dist/worker-runtime-ALS4MR2A.mjs.map +1 -0
- package/dist-web/assets/BufferResource-MsqP5sq3.js +185 -0
- package/dist-web/assets/CanvasRenderer-BKCvcc2p.js +1 -0
- package/dist-web/assets/DebugToolsPanel-D_lFIW6j.css +1 -0
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-HGJb0Et1.js +110 -0
- package/dist-web/assets/{EChartWordcloud-B_YisOW5.js → EChartWordcloud-Clg60FMo.js} +1 -1
- package/dist-web/assets/Filter-wRJtdXx9.js +1 -0
- package/dist-web/assets/MessageExportModal-BCrMf5Kw.js +1 -0
- package/dist-web/assets/PageHeader-Bbn87QZS.css +1 -0
- package/dist-web/assets/PageHeader-C_yw54le.js +1 -0
- package/dist-web/assets/RelationshipGalaxyCanvas-DTzJq7q7.js +244 -0
- package/dist-web/assets/RelationshipGalaxyThreeCanvas-BluPHjCR.css +1 -0
- package/dist-web/assets/RelationshipGalaxyThreeCanvas-DIE-0_iM.js +4108 -0
- package/dist-web/assets/RenderTargetSystem-BSAQFamI.js +184 -0
- package/dist-web/assets/WebGLRenderer-lfzmCZNT.js +156 -0
- package/dist-web/assets/WebGPURenderer-BnEdfdr_.js +41 -0
- package/dist-web/assets/browserAll-gRNfDyPu.js +14 -0
- package/dist-web/assets/{electron-DjminAKK.js → electron-BHaEaaZP.js} +1 -1
- package/dist-web/assets/{fetch-B_kLRm6-.js → fetch-BW3aO7PS.js} +1 -1
- package/dist-web/assets/fetch-BwF5hNuF.js +1 -0
- package/dist-web/assets/{fetch-DV8A1dGB.js → fetch-C1fI_KOX.js} +1 -1
- package/dist-web/assets/{fetch-uM3erGZm.js → fetch-CBZbgZqk.js} +1 -1
- package/dist-web/assets/{fetch-Wad5mWVH.js → fetch-CFRoSIAn.js} +1 -1
- package/dist-web/assets/fetch-CN91TIwV.js +1 -0
- package/dist-web/assets/{fetch-DOhCQ5J8.js → fetch-CQt-gb-f.js} +1 -1
- package/dist-web/assets/{fetch-Cle6QKmL.js → fetch-DPCJ-1l-.js} +1 -1
- package/dist-web/assets/{fetch-r83wdzHy.js → fetch-gMB4wuBd.js} +1 -1
- package/dist-web/assets/{fetch-srCJzcWc.js → fetch-mFlEBZyB.js} +1 -1
- package/dist-web/assets/index---rXG1hZ.css +1 -0
- package/dist-web/assets/index--6C0sK6a.css +1 -0
- package/dist-web/assets/index-B6wEGata.js +173 -0
- package/dist-web/assets/index-Bg4GGPXv.js +1 -0
- package/dist-web/assets/index-BgVbj5qX.js +12 -0
- package/dist-web/assets/index-BkoXaBV_.js +64 -0
- package/dist-web/assets/index-C56IuV5U.css +1 -0
- package/dist-web/assets/index-CLRaKwpT.js +2 -0
- package/dist-web/assets/index-Dj1BSD7O.js +1 -0
- package/dist-web/assets/index-DosG7LmJ.css +1 -0
- package/dist-web/assets/index-DveHdPfK.js +1 -0
- package/dist-web/assets/{index-BX3tqfLz.css → index-MBRD3wPd.css} +1 -1
- package/dist-web/assets/index-RoDgJcHU.js +2 -0
- package/dist-web/assets/{index-tkbEq8av.js → index-ZdCB7xcf.js} +4 -4
- package/dist-web/assets/index-aljYO5ms.js +1 -0
- package/dist-web/assets/people-page-header-Mupn-U_b.js +1 -0
- package/dist-web/assets/relationship-galaxy-viewport-Cwfa51ER.js +1 -0
- package/dist-web/assets/{vendor-echarts-CITQeskI.js → vendor-echarts-hJVnODyU.js} +2 -2
- package/dist-web/assets/{vendor-echarts-wordcloud-B1kQMklB.js → vendor-echarts-wordcloud-k5ih2Jl4.js} +1 -1
- package/dist-web/assets/{vendor-nuxt-ui-Zy8txNdC.js → vendor-nuxt-ui-Dr2T8_OZ.js} +2 -2
- package/dist-web/assets/{vendor-reka-ui-mMvma1EP.js → vendor-reka-ui-CzM4kDQ7.js} +1 -1
- package/dist-web/assets/{vendor-zrender-DFl4U2Oe.js → vendor-zrender-5DcYjSZk.js} +1 -1
- package/dist-web/assets/{web-D27K6zSX.js → web-BakeV58u.js} +1 -1
- package/dist-web/assets/webworkerAll-D_6PgURS.js +83 -0
- package/dist-web/index.html +4 -4
- package/package.json +1 -1
- package/dist/chunk-55WR6WKW.mjs.map +0 -1
- package/dist/chunk-GPF34WXV.mjs +0 -9573
- package/dist/chunk-GPF34WXV.mjs.map +0 -1
- package/dist/chunk-YQ2FJGBT.mjs.map +0 -1
- package/dist/chunk-YZDEZ6UI.mjs +0 -389
- package/dist/chunk-YZDEZ6UI.mjs.map +0 -1
- package/dist/chunk-ZSFVX6FM.mjs.map +0 -1
- package/dist/worker-runtime-6DGYL4TF.mjs.map +0 -1
- package/dist-web/assets/DebugToolsPanel-DFw6_G5V.css +0 -1
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-CCz0Syxo.js +0 -110
- package/dist-web/assets/MessageExportModal-Dd6R-cX_.js +0 -1
- package/dist-web/assets/fetch-CURbMNJV.js +0 -1
- package/dist-web/assets/fetch-DFCYOQe5.js +0 -1
- package/dist-web/assets/index-7E5GSvR2.css +0 -1
- package/dist-web/assets/index-BCRHSc1k.js +0 -173
- package/dist-web/assets/index-BIvLzdwg.js +0 -1
- package/dist-web/assets/index-BpszRZ8V.js +0 -64
- package/dist-web/assets/index-D5UgZUpS.js +0 -12
- package/dist-web/assets/index-I3SWZjnW.css +0 -1
- package/dist-web/assets/index-hOsKRsp6.js +0 -2
- /package/dist/{http-C2FBHNZG.mjs.map → chunk-5FGB42EQ.mjs.map} +0 -0
- /package/dist/{chunk-WGS33EWH.mjs.map → chunk-6FPEP2ZX.mjs.map} +0 -0
- /package/dist/{chunk-VASK54KT.mjs.map → chunk-CIB23U32.mjs.map} +0 -0
- /package/dist/{import-ED6JCPSJ.mjs.map → http-5E7ARYFW.mjs.map} +0 -0
- /package/dist/{src-WBNM42SL.mjs.map → import-3KG6VK6O.mjs.map} +0 -0
- /package/dist/{stream-import-37U3SROO.mjs.map → src-UTBQAOJ3.mjs.map} +0 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import {
|
|
8
|
+
CONTACTS_TIME_RANGE_PRESETS,
|
|
9
|
+
MIN_PRIVATE_SESSIONS_FOR_CONTACTS,
|
|
10
|
+
appLogger,
|
|
11
|
+
computeFriendScores,
|
|
12
|
+
computeNonFriendScores,
|
|
13
|
+
getCache,
|
|
14
|
+
getDbFileVersion,
|
|
15
|
+
getGroupContactFacts,
|
|
16
|
+
getLatestContactMessageTs,
|
|
17
|
+
getPrivateContactFacts,
|
|
18
|
+
getSessionMeta,
|
|
19
|
+
isChatSessionDb,
|
|
20
|
+
isNameMatchPlatform,
|
|
21
|
+
resolveOwnerMember,
|
|
22
|
+
setCache
|
|
23
|
+
} from "./chunk-WPI6YSAR.mjs";
|
|
24
|
+
|
|
25
|
+
// ../../packages/node-runtime/src/services/contacts/facts-cache.ts
|
|
26
|
+
var CONTACTS_FACTS_FORMAT_VERSION = 1;
|
|
27
|
+
function createEmptyContactsFactsCacheStats() {
|
|
28
|
+
return {
|
|
29
|
+
latestHits: 0,
|
|
30
|
+
latestMisses: 0,
|
|
31
|
+
factsHits: 0,
|
|
32
|
+
factsMisses: 0,
|
|
33
|
+
writes: 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildContactsSessionLatestCacheKey(algorithmVersion) {
|
|
37
|
+
return `contacts:latest:v${CONTACTS_FACTS_FORMAT_VERSION}:${algorithmVersion}`;
|
|
38
|
+
}
|
|
39
|
+
function buildContactsSessionFactsCacheKey(algorithmVersion, timeRange) {
|
|
40
|
+
return [
|
|
41
|
+
"contacts:facts",
|
|
42
|
+
`v${CONTACTS_FACTS_FORMAT_VERSION}`,
|
|
43
|
+
algorithmVersion,
|
|
44
|
+
`preset:${timeRange.preset}`,
|
|
45
|
+
`start:${timeRange.startTs ?? "all"}`
|
|
46
|
+
].join(":");
|
|
47
|
+
}
|
|
48
|
+
function readCachedContactsSessionLatest(sessionId, cacheDir, key, dbVersion) {
|
|
49
|
+
const cached = getCache(sessionId, key, cacheDir);
|
|
50
|
+
if (!cached || cached.v !== dbVersion || !isContactsSessionLatestFacts(cached.data)) return { hit: false };
|
|
51
|
+
return { hit: true, data: cached.data };
|
|
52
|
+
}
|
|
53
|
+
function writeCachedContactsSessionLatest(sessionId, cacheDir, key, dbVersion, data) {
|
|
54
|
+
setCache(sessionId, key, { v: dbVersion, data }, cacheDir);
|
|
55
|
+
}
|
|
56
|
+
function readCachedContactsSessionFacts(sessionId, cacheDir, key, dbVersion) {
|
|
57
|
+
const cached = getCache(sessionId, key, cacheDir);
|
|
58
|
+
if (!cached || cached.v !== dbVersion || !isContactsSessionFacts(cached.data)) return { hit: false };
|
|
59
|
+
return { hit: true, data: cached.data };
|
|
60
|
+
}
|
|
61
|
+
function writeCachedContactsSessionFacts(sessionId, cacheDir, key, dbVersion, data) {
|
|
62
|
+
setCache(sessionId, key, { v: dbVersion, data }, cacheDir);
|
|
63
|
+
}
|
|
64
|
+
function isContactsSessionLatestFacts(value) {
|
|
65
|
+
return isObject(value) && isNullableNumber(value.latestMessageTs);
|
|
66
|
+
}
|
|
67
|
+
function isContactsSessionFacts(value) {
|
|
68
|
+
if (!isObject(value) || typeof value.kind !== "string" || !isNullableNumber(value.latestMessageTs)) return false;
|
|
69
|
+
switch (value.kind) {
|
|
70
|
+
case "not_chat_db":
|
|
71
|
+
case "missing_meta":
|
|
72
|
+
case "unsupported_type":
|
|
73
|
+
return true;
|
|
74
|
+
case "missing_owner":
|
|
75
|
+
case "unresolved_owner":
|
|
76
|
+
case "private_missing":
|
|
77
|
+
case "private_ambiguous":
|
|
78
|
+
return isContactsSessionMetaFacts(value.meta);
|
|
79
|
+
case "private":
|
|
80
|
+
return isContactsSessionMetaFacts(value.meta) && isContactsCachedPrivateFacts(value.facts);
|
|
81
|
+
case "group":
|
|
82
|
+
return isContactsSessionMetaFacts(value.meta) && Array.isArray(value.facts) && value.facts.every(isContactsCachedGroupFacts);
|
|
83
|
+
default:
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function isContactsSessionMetaFacts(value) {
|
|
88
|
+
return isObject(value) && typeof value.name === "string" && typeof value.platform === "string" && (value.type === "private" /* PRIVATE */ || value.type === "group" /* GROUP */) && (typeof value.ownerId === "string" || value.ownerId === null);
|
|
89
|
+
}
|
|
90
|
+
function isContactsCachedPrivateFacts(value) {
|
|
91
|
+
return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.privateMessageCount) && Array.isArray(value.activeMonths) && value.activeMonths.every((month) => typeof month === "string") && isNullableNumber(value.lastMessageTs);
|
|
92
|
+
}
|
|
93
|
+
function isContactsCachedGroupFacts(value) {
|
|
94
|
+
return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.messageCount) && isFiniteNumber(value.coOccurrenceCount) && isFiniteNumber(value.coOccurrenceRawScore) && isFiniteNumber(value.replyInteractionCount) && isFiniteNumber(value.repliesFromOwnerToContact) && isFiniteNumber(value.repliesFromContactToOwner) && isNullableNumber(value.lastInteractionTs);
|
|
95
|
+
}
|
|
96
|
+
function isContactMemberRef(value) {
|
|
97
|
+
return isObject(value) && isFiniteNumber(value.id) && typeof value.platformId === "string" && typeof value.name === "string" && Array.isArray(value.aliases) && value.aliases.every((alias) => typeof alias === "string") && (typeof value.avatar === "string" || value.avatar === null);
|
|
98
|
+
}
|
|
99
|
+
function isNullableNumber(value) {
|
|
100
|
+
return value === null || isFiniteNumber(value);
|
|
101
|
+
}
|
|
102
|
+
function isFiniteNumber(value) {
|
|
103
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
104
|
+
}
|
|
105
|
+
function isObject(value) {
|
|
106
|
+
return typeof value === "object" && value !== null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ../../packages/node-runtime/src/services/contacts/time-range.ts
|
|
110
|
+
var SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
|
|
111
|
+
var DEFAULT_CONTACTS_TIME_RANGE_PRESET = "1y";
|
|
112
|
+
var YEARS_BY_PRESET = {
|
|
113
|
+
"1y": 1,
|
|
114
|
+
"2y": 2,
|
|
115
|
+
"3y": 3,
|
|
116
|
+
"5y": 5
|
|
117
|
+
};
|
|
118
|
+
function normalizeContactsTimeRangePreset(value) {
|
|
119
|
+
return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : DEFAULT_CONTACTS_TIME_RANGE_PRESET;
|
|
120
|
+
}
|
|
121
|
+
function resolveContactsTimeRange(presetInput, anchorTs) {
|
|
122
|
+
const preset = normalizeContactsTimeRangePreset(presetInput);
|
|
123
|
+
const normalizedAnchor = typeof anchorTs === "number" ? anchorTs : null;
|
|
124
|
+
const years = YEARS_BY_PRESET[preset];
|
|
125
|
+
return {
|
|
126
|
+
preset,
|
|
127
|
+
anchorTs: normalizedAnchor,
|
|
128
|
+
startTs: normalizedAnchor !== null && years ? normalizedAnchor - years * SECONDS_PER_YEAR : null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ../../packages/node-runtime/src/services/contacts/compute.ts
|
|
133
|
+
var CONTACTS_ALGORITHM_VERSION = "contacts-v2";
|
|
134
|
+
function computeContactsSnapshot(options) {
|
|
135
|
+
const startedAt = options.now?.() ?? Date.now();
|
|
136
|
+
const sessionIds = options.adapter.listSessionIds();
|
|
137
|
+
const factsCache = options.factsCacheDir ? createFactsCacheContext(options.factsCacheDir) : null;
|
|
138
|
+
const timeRange = resolveContactsTimeRange(
|
|
139
|
+
options.timeRangePreset,
|
|
140
|
+
findGlobalLatestMessageTs(options.adapter, sessionIds, factsCache)
|
|
141
|
+
);
|
|
142
|
+
const result = computeContacts({
|
|
143
|
+
adapter: options.adapter,
|
|
144
|
+
sessionIds,
|
|
145
|
+
timeRange,
|
|
146
|
+
factsCache,
|
|
147
|
+
onProgress: options.onProgress
|
|
148
|
+
});
|
|
149
|
+
const finishedAt = options.now?.() ?? Date.now();
|
|
150
|
+
if (factsCache) {
|
|
151
|
+
appLogger.info("contacts", "contacts session facts cache summary", factsCache.stats);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...result,
|
|
155
|
+
algorithmVersion: CONTACTS_ALGORITHM_VERSION,
|
|
156
|
+
signature: options.signature,
|
|
157
|
+
timeRange,
|
|
158
|
+
computedAt: finishedAt,
|
|
159
|
+
workerStats: {
|
|
160
|
+
durationMs: Math.max(0, finishedAt - startedAt),
|
|
161
|
+
totalSessions: sessionIds.length,
|
|
162
|
+
processedSessions: sessionIds.length,
|
|
163
|
+
skippedFailedSessions: result.diagnostics.skippedFailedSessions
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function computeContacts(options) {
|
|
168
|
+
const diagnostics = createEmptyDiagnostics();
|
|
169
|
+
const accumulators = /* @__PURE__ */ new Map();
|
|
170
|
+
let processedSessions = 0;
|
|
171
|
+
for (const sessionId of options.sessionIds) {
|
|
172
|
+
options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId });
|
|
173
|
+
try {
|
|
174
|
+
const facts = getSessionFacts(options.adapter, sessionId, options.timeRange, options.factsCache);
|
|
175
|
+
applySessionFacts(accumulators, diagnostics, sessionId, facts);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
diagnostics.skippedFailedSessions++;
|
|
178
|
+
appLogger.error("contacts", `failed to process contact session: ${sessionId}`, error);
|
|
179
|
+
} finally {
|
|
180
|
+
processedSessions++;
|
|
181
|
+
options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
diagnostics.contactsEnabled = diagnostics.activePrivateSessionCount > MIN_PRIVATE_SESSIONS_FOR_CONTACTS;
|
|
185
|
+
const contacts = buildContactItems([...accumulators.values()]);
|
|
186
|
+
return { contacts, diagnostics };
|
|
187
|
+
}
|
|
188
|
+
function findGlobalLatestMessageTs(adapter, sessionIds, factsCache) {
|
|
189
|
+
let latest = null;
|
|
190
|
+
for (const sessionId of sessionIds) {
|
|
191
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
192
|
+
const cached = readLatestFacts(sessionId, factsCache, dbVersion);
|
|
193
|
+
if (cached) {
|
|
194
|
+
if (cached.latestMessageTs !== null) latest = Math.max(latest ?? 0, cached.latestMessageTs);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const db = adapter.openReadonly(sessionId);
|
|
199
|
+
const ts = db && isChatSessionDb(db) ? getLatestContactMessageTs(db) : null;
|
|
200
|
+
writeLatestFacts(adapter, sessionId, factsCache, { latestMessageTs: ts }, dbVersion);
|
|
201
|
+
if (ts !== null) latest = Math.max(latest ?? 0, ts);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
appLogger.error("contacts", `failed to inspect contact session range: ${sessionId}`, error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return latest;
|
|
207
|
+
}
|
|
208
|
+
function getSessionFacts(adapter, sessionId, timeRange, factsCache) {
|
|
209
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
210
|
+
const cached = readSessionFacts(sessionId, timeRange, factsCache, dbVersion);
|
|
211
|
+
if (cached) return cached;
|
|
212
|
+
const facts = computeSessionFacts(adapter, sessionId, timeRange);
|
|
213
|
+
writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, dbVersion);
|
|
214
|
+
return facts;
|
|
215
|
+
}
|
|
216
|
+
function computeSessionFacts(adapter, sessionId, timeRange) {
|
|
217
|
+
const db = adapter.openReadonly(sessionId);
|
|
218
|
+
if (!db || !isChatSessionDb(db)) return { kind: "not_chat_db", latestMessageTs: null };
|
|
219
|
+
const latestMessageTs = getLatestContactMessageTs(db);
|
|
220
|
+
const meta = getSessionMeta(db);
|
|
221
|
+
if (!meta) return { kind: "missing_meta", latestMessageTs };
|
|
222
|
+
if (meta.type !== "private" /* PRIVATE */ && meta.type !== "group" /* GROUP */)
|
|
223
|
+
return { kind: "unsupported_type", latestMessageTs };
|
|
224
|
+
const cachedMeta = toContactsSessionMetaFacts(meta);
|
|
225
|
+
if (!meta.ownerId?.trim()) return { kind: "missing_owner", meta: cachedMeta, latestMessageTs };
|
|
226
|
+
const owner = resolveOwnerMember(db);
|
|
227
|
+
if (!owner) return { kind: "unresolved_owner", meta: cachedMeta, latestMessageTs };
|
|
228
|
+
if (meta.type === "private" /* PRIVATE */) {
|
|
229
|
+
const facts = getPrivateContactFacts(db, owner.id, { startTs: timeRange.startTs });
|
|
230
|
+
if (facts.type === "missing") return { kind: "private_missing", meta: cachedMeta, latestMessageTs };
|
|
231
|
+
if (facts.type === "ambiguous") return { kind: "private_ambiguous", meta: cachedMeta, latestMessageTs };
|
|
232
|
+
return { kind: "private", meta: cachedMeta, latestMessageTs, facts };
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
kind: "group",
|
|
236
|
+
meta: cachedMeta,
|
|
237
|
+
latestMessageTs,
|
|
238
|
+
facts: getGroupContactFacts(db, owner.id, { startTs: timeRange.startTs })
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function applySessionFacts(accumulators, diagnostics, sessionId, sessionFacts) {
|
|
242
|
+
if ("meta" in sessionFacts && sessionFacts.meta.type === "private" /* PRIVATE */) diagnostics.privateSessionCount++;
|
|
243
|
+
switch (sessionFacts.kind) {
|
|
244
|
+
case "missing_owner":
|
|
245
|
+
diagnostics.skippedMissingOwnerSessions++;
|
|
246
|
+
return;
|
|
247
|
+
case "unresolved_owner":
|
|
248
|
+
diagnostics.skippedUnresolvedOwnerSessions++;
|
|
249
|
+
return;
|
|
250
|
+
case "private_ambiguous":
|
|
251
|
+
diagnostics.skippedAmbiguousPrivateSessions++;
|
|
252
|
+
return;
|
|
253
|
+
case "private":
|
|
254
|
+
applyPrivateFacts(accumulators, diagnostics, sessionId, sessionFacts.meta, sessionFacts.facts);
|
|
255
|
+
return;
|
|
256
|
+
case "group":
|
|
257
|
+
applyGroupFacts(accumulators, sessionId, sessionFacts.meta, sessionFacts.facts);
|
|
258
|
+
return;
|
|
259
|
+
default:
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function applyPrivateFacts(accumulators, diagnostics, sessionId, meta, facts) {
|
|
264
|
+
if (facts.privateMessageCount <= 0) return;
|
|
265
|
+
diagnostics.activePrivateSessionCount++;
|
|
266
|
+
const acc = getOrCreateAccumulator(accumulators, sessionId, meta, facts.contact);
|
|
267
|
+
acc.isFriend = true;
|
|
268
|
+
acc.privateMessageCount += facts.privateMessageCount;
|
|
269
|
+
for (const month of facts.activeMonths) acc.activePrivateMonths.add(month);
|
|
270
|
+
updateLastInteraction(acc, facts.lastMessageTs);
|
|
271
|
+
acc.sourceSessions.push({
|
|
272
|
+
id: sessionId,
|
|
273
|
+
name: meta.name,
|
|
274
|
+
platform: meta.platform,
|
|
275
|
+
type: "private" /* PRIVATE */,
|
|
276
|
+
messageCount: facts.privateMessageCount,
|
|
277
|
+
privateMessageCount: facts.privateMessageCount,
|
|
278
|
+
lastMessageTs: facts.lastMessageTs
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function applyGroupFacts(accumulators, sessionId, meta, sessionFacts) {
|
|
282
|
+
for (const facts of sessionFacts) {
|
|
283
|
+
if (!hasGroupContactSignal(facts)) continue;
|
|
284
|
+
const acc = getOrCreateAccumulator(accumulators, sessionId, meta, facts.contact);
|
|
285
|
+
acc.commonGroupSessionIds.add(sessionId);
|
|
286
|
+
acc.coOccurrenceCount += facts.coOccurrenceCount;
|
|
287
|
+
acc.coOccurrenceRawScore += facts.coOccurrenceRawScore;
|
|
288
|
+
acc.replyInteractionCount += facts.replyInteractionCount;
|
|
289
|
+
acc.repliesFromOwnerToContact += facts.repliesFromOwnerToContact;
|
|
290
|
+
acc.repliesFromContactToOwner += facts.repliesFromContactToOwner;
|
|
291
|
+
updateLastInteraction(acc, facts.lastInteractionTs);
|
|
292
|
+
acc.sourceSessions.push({
|
|
293
|
+
id: sessionId,
|
|
294
|
+
name: meta.name,
|
|
295
|
+
platform: meta.platform,
|
|
296
|
+
type: "group" /* GROUP */,
|
|
297
|
+
messageCount: facts.messageCount,
|
|
298
|
+
coOccurrenceCount: facts.coOccurrenceCount,
|
|
299
|
+
coOccurrenceRawScore: facts.coOccurrenceRawScore,
|
|
300
|
+
replyInteractionCount: facts.replyInteractionCount,
|
|
301
|
+
repliesFromOwnerToContact: facts.repliesFromOwnerToContact,
|
|
302
|
+
repliesFromContactToOwner: facts.repliesFromContactToOwner,
|
|
303
|
+
lastInteractionTs: facts.lastInteractionTs
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function hasGroupContactSignal(facts) {
|
|
308
|
+
return facts.messageCount > 0 || facts.coOccurrenceCount > 0 || facts.replyInteractionCount > 0;
|
|
309
|
+
}
|
|
310
|
+
function createFactsCacheContext(dir) {
|
|
311
|
+
return {
|
|
312
|
+
dir,
|
|
313
|
+
latestKey: buildContactsSessionLatestCacheKey(CONTACTS_ALGORITHM_VERSION),
|
|
314
|
+
stats: createEmptyContactsFactsCacheStats()
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function readLatestFacts(sessionId, factsCache, dbVersion) {
|
|
318
|
+
if (!factsCache) return null;
|
|
319
|
+
const cached = readCachedContactsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion);
|
|
320
|
+
if (!cached.hit) {
|
|
321
|
+
factsCache.stats.latestMisses++;
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
factsCache.stats.latestHits++;
|
|
325
|
+
return cached.data;
|
|
326
|
+
}
|
|
327
|
+
function writeLatestFacts(adapter, sessionId, factsCache, data, expectedDbVersion) {
|
|
328
|
+
if (!factsCache) return;
|
|
329
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
330
|
+
if (dbVersion !== expectedDbVersion) {
|
|
331
|
+
appLogger.debug("contacts", "skipped contacts latest facts cache write because db version changed", {
|
|
332
|
+
sessionId
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
writeCachedContactsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, data);
|
|
337
|
+
factsCache.stats.writes++;
|
|
338
|
+
}
|
|
339
|
+
function readSessionFacts(sessionId, timeRange, factsCache, dbVersion) {
|
|
340
|
+
if (!factsCache) return null;
|
|
341
|
+
const cached = readCachedContactsSessionFacts(
|
|
342
|
+
sessionId,
|
|
343
|
+
factsCache.dir,
|
|
344
|
+
buildContactsSessionFactsCacheKey(CONTACTS_ALGORITHM_VERSION, timeRange),
|
|
345
|
+
dbVersion
|
|
346
|
+
);
|
|
347
|
+
if (!cached.hit) {
|
|
348
|
+
factsCache.stats.factsMisses++;
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
factsCache.stats.factsHits++;
|
|
352
|
+
return cached.data;
|
|
353
|
+
}
|
|
354
|
+
function writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, expectedDbVersion) {
|
|
355
|
+
if (!factsCache) return;
|
|
356
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
357
|
+
if (dbVersion !== expectedDbVersion) {
|
|
358
|
+
appLogger.debug("contacts", "skipped contacts session facts cache write because db version changed", {
|
|
359
|
+
sessionId
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
writeCachedContactsSessionFacts(
|
|
364
|
+
sessionId,
|
|
365
|
+
factsCache.dir,
|
|
366
|
+
buildContactsSessionFactsCacheKey(CONTACTS_ALGORITHM_VERSION, timeRange),
|
|
367
|
+
dbVersion,
|
|
368
|
+
facts
|
|
369
|
+
);
|
|
370
|
+
writeCachedContactsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, {
|
|
371
|
+
latestMessageTs: facts.latestMessageTs
|
|
372
|
+
});
|
|
373
|
+
factsCache.stats.writes += 2;
|
|
374
|
+
}
|
|
375
|
+
function getSessionDbVersion(adapter, sessionId) {
|
|
376
|
+
return getDbFileVersion(adapter.getDbPath(sessionId));
|
|
377
|
+
}
|
|
378
|
+
function toContactsSessionMetaFacts(meta) {
|
|
379
|
+
return {
|
|
380
|
+
name: meta.name,
|
|
381
|
+
platform: meta.platform,
|
|
382
|
+
type: meta.type,
|
|
383
|
+
ownerId: meta.ownerId
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function buildContactItems(accumulators) {
|
|
387
|
+
const friendInputs = accumulators.filter((acc) => acc.isFriend).map((acc) => ({
|
|
388
|
+
acc,
|
|
389
|
+
privateMessageCount: acc.privateMessageCount,
|
|
390
|
+
activeMonths: [...acc.activePrivateMonths],
|
|
391
|
+
commonGroupCount: acc.commonGroupSessionIds.size
|
|
392
|
+
}));
|
|
393
|
+
const nonFriendInputs = accumulators.filter((acc) => !acc.isFriend).map((acc) => ({
|
|
394
|
+
acc,
|
|
395
|
+
coOccurrenceRawScore: acc.coOccurrenceRawScore,
|
|
396
|
+
commonGroupCount: acc.commonGroupSessionIds.size,
|
|
397
|
+
replyInteractionCount: acc.replyInteractionCount,
|
|
398
|
+
coOccurrenceCount: acc.coOccurrenceCount
|
|
399
|
+
}));
|
|
400
|
+
const friendScores = computeFriendScores(friendInputs);
|
|
401
|
+
const nonFriendScores = computeNonFriendScores(nonFriendInputs);
|
|
402
|
+
const contacts = [];
|
|
403
|
+
for (const input of friendInputs) {
|
|
404
|
+
const score = friendScores.get(input) ?? { score: 0, scoreBreakdown: {} };
|
|
405
|
+
contacts.push(toContactItem(input.acc, "friend", score));
|
|
406
|
+
}
|
|
407
|
+
for (const input of nonFriendInputs) {
|
|
408
|
+
const score = nonFriendScores.get(input) ?? { score: 0, scoreBreakdown: {} };
|
|
409
|
+
contacts.push(toContactItem(input.acc, "non_friend", score));
|
|
410
|
+
}
|
|
411
|
+
return contacts.sort((a, b) => b.score - a.score || a.displayName.localeCompare(b.displayName));
|
|
412
|
+
}
|
|
413
|
+
function toContactItem(acc, pool, scoring) {
|
|
414
|
+
const aliases = [...acc.aliases].filter((alias) => alias !== acc.displayName);
|
|
415
|
+
const searchText = [acc.displayName, acc.platformId, ...aliases].join(" ").toLowerCase();
|
|
416
|
+
const item = {
|
|
417
|
+
key: acc.key,
|
|
418
|
+
platform: acc.platform,
|
|
419
|
+
platformId: acc.platformId,
|
|
420
|
+
sessionScoped: acc.sessionScoped,
|
|
421
|
+
sessionId: acc.sessionId,
|
|
422
|
+
displayName: acc.displayName,
|
|
423
|
+
aliases,
|
|
424
|
+
avatar: acc.avatar,
|
|
425
|
+
isFriend: acc.isFriend,
|
|
426
|
+
pool,
|
|
427
|
+
score: scoring.score,
|
|
428
|
+
scoreBreakdown: {
|
|
429
|
+
...scoring.scoreBreakdown,
|
|
430
|
+
privateMessageCount: acc.privateMessageCount || scoring.scoreBreakdown.privateMessageCount,
|
|
431
|
+
activePrivateMonths: acc.activePrivateMonths.size || scoring.scoreBreakdown.activePrivateMonths,
|
|
432
|
+
commonGroupCount: acc.commonGroupSessionIds.size,
|
|
433
|
+
coOccurrenceCount: acc.coOccurrenceCount,
|
|
434
|
+
coOccurrenceRawScore: acc.coOccurrenceRawScore,
|
|
435
|
+
replyInteractionCount: acc.replyInteractionCount,
|
|
436
|
+
repliesFromOwnerToContact: acc.repliesFromOwnerToContact,
|
|
437
|
+
repliesFromContactToOwner: acc.repliesFromContactToOwner
|
|
438
|
+
},
|
|
439
|
+
sourceSessions: acc.sourceSessions,
|
|
440
|
+
searchText,
|
|
441
|
+
lastInteractionTs: acc.lastInteractionTs
|
|
442
|
+
};
|
|
443
|
+
if (pool === "friend") item.friendSource = "private";
|
|
444
|
+
return item;
|
|
445
|
+
}
|
|
446
|
+
function createEmptyContactsDiagnostics() {
|
|
447
|
+
return createEmptyDiagnostics();
|
|
448
|
+
}
|
|
449
|
+
function createEmptyDiagnostics() {
|
|
450
|
+
return {
|
|
451
|
+
privateSessionCount: 0,
|
|
452
|
+
activePrivateSessionCount: 0,
|
|
453
|
+
contactsEnabled: false,
|
|
454
|
+
skippedMissingOwnerSessions: 0,
|
|
455
|
+
skippedUnresolvedOwnerSessions: 0,
|
|
456
|
+
skippedAmbiguousPrivateSessions: 0,
|
|
457
|
+
skippedInvalidPlatformIdMembers: 0,
|
|
458
|
+
skippedFailedSessions: 0,
|
|
459
|
+
warnings: []
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function getOrCreateAccumulator(accumulators, sessionId, meta, contact) {
|
|
463
|
+
const sessionScoped = shouldScopeContactToSession(meta.platform, contact);
|
|
464
|
+
const key = buildContactKey(meta.platform, contact.platformId, sessionScoped ? sessionId : void 0);
|
|
465
|
+
const existing = accumulators.get(key);
|
|
466
|
+
if (existing) {
|
|
467
|
+
mergeContactIdentity(existing, contact);
|
|
468
|
+
return existing;
|
|
469
|
+
}
|
|
470
|
+
const created = {
|
|
471
|
+
key,
|
|
472
|
+
platform: meta.platform,
|
|
473
|
+
platformId: contact.platformId,
|
|
474
|
+
sessionScoped,
|
|
475
|
+
sessionId: sessionScoped ? sessionId : void 0,
|
|
476
|
+
displayName: contact.name || contact.platformId,
|
|
477
|
+
aliases: new Set([contact.platformId, contact.name, ...contact.aliases].filter(Boolean)),
|
|
478
|
+
avatar: contact.avatar,
|
|
479
|
+
isFriend: false,
|
|
480
|
+
privateMessageCount: 0,
|
|
481
|
+
activePrivateMonths: /* @__PURE__ */ new Set(),
|
|
482
|
+
commonGroupSessionIds: /* @__PURE__ */ new Set(),
|
|
483
|
+
coOccurrenceCount: 0,
|
|
484
|
+
coOccurrenceRawScore: 0,
|
|
485
|
+
replyInteractionCount: 0,
|
|
486
|
+
repliesFromOwnerToContact: 0,
|
|
487
|
+
repliesFromContactToOwner: 0,
|
|
488
|
+
sourceSessions: [],
|
|
489
|
+
lastInteractionTs: null
|
|
490
|
+
};
|
|
491
|
+
accumulators.set(key, created);
|
|
492
|
+
return created;
|
|
493
|
+
}
|
|
494
|
+
function shouldScopeContactToSession(platform, contact) {
|
|
495
|
+
if (isNameMatchPlatform(platform)) return true;
|
|
496
|
+
return platform.trim().toLowerCase() === "qq" && contact.platformId.trim() === contact.name.trim();
|
|
497
|
+
}
|
|
498
|
+
function buildContactKey(platform, platformId, sessionId) {
|
|
499
|
+
const normalizedPlatform = platform.trim();
|
|
500
|
+
const normalizedPlatformId = platformId.trim();
|
|
501
|
+
if (!normalizedPlatform) throw new Error("platform is required");
|
|
502
|
+
if (!normalizedPlatformId) throw new Error("platformId is required");
|
|
503
|
+
return sessionId?.trim() ? `${normalizedPlatform}:${sessionId.trim()}:${normalizedPlatformId}` : `${normalizedPlatform}:${normalizedPlatformId}`;
|
|
504
|
+
}
|
|
505
|
+
function mergeContactIdentity(acc, contact) {
|
|
506
|
+
if (contact.name) acc.aliases.add(contact.name);
|
|
507
|
+
acc.aliases.add(contact.platformId);
|
|
508
|
+
for (const alias of contact.aliases) acc.aliases.add(alias);
|
|
509
|
+
if ((!acc.displayName || acc.displayName === acc.platformId) && contact.name) {
|
|
510
|
+
acc.displayName = contact.name;
|
|
511
|
+
}
|
|
512
|
+
if (!acc.avatar && contact.avatar) acc.avatar = contact.avatar;
|
|
513
|
+
}
|
|
514
|
+
function updateLastInteraction(acc, ts) {
|
|
515
|
+
if (ts === null) return;
|
|
516
|
+
acc.lastInteractionTs = Math.max(acc.lastInteractionTs ?? 0, ts);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export {
|
|
520
|
+
normalizeContactsTimeRangePreset,
|
|
521
|
+
resolveContactsTimeRange,
|
|
522
|
+
CONTACTS_ALGORITHM_VERSION,
|
|
523
|
+
computeContactsSnapshot,
|
|
524
|
+
createEmptyContactsDiagnostics
|
|
525
|
+
};
|
|
526
|
+
//# sourceMappingURL=chunk-D4WTE7LV.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../packages/node-runtime/src/services/contacts/facts-cache.ts","../../../packages/node-runtime/src/services/contacts/time-range.ts","../../../packages/node-runtime/src/services/contacts/compute.ts"],"sourcesContent":["import { ChatType, type ChatPlatform, type ContactsTimeRangeState } from '@openchatlab/shared-types'\nimport type { ContactMemberRef } from '@openchatlab/core'\nimport { getCache, setCache, deleteSessionCache } from '../../cache/session-cache'\n\nexport const CONTACTS_FACTS_FORMAT_VERSION = 1\n\nexport interface ContactsFactsCacheStats {\n latestHits: number\n latestMisses: number\n factsHits: number\n factsMisses: number\n writes: number\n}\n\nexport interface ContactsSessionMetaFacts {\n name: string\n platform: ChatPlatform\n type: ChatType.PRIVATE | ChatType.GROUP\n ownerId: string | null\n}\n\nexport interface ContactsCachedPrivateFacts {\n contact: ContactMemberRef\n privateMessageCount: number\n activeMonths: string[]\n lastMessageTs: number | null\n}\n\nexport interface ContactsCachedGroupFacts {\n contact: ContactMemberRef\n messageCount: number\n coOccurrenceCount: number\n coOccurrenceRawScore: number\n replyInteractionCount: number\n repliesFromOwnerToContact: number\n repliesFromContactToOwner: number\n lastInteractionTs: number | null\n}\n\nexport type ContactsSessionFacts =\n | { kind: 'not_chat_db'; latestMessageTs: null }\n | { kind: 'missing_meta'; latestMessageTs: number | null }\n | { kind: 'unsupported_type'; latestMessageTs: number | null }\n | { kind: 'missing_owner'; meta: ContactsSessionMetaFacts; latestMessageTs: number | null }\n | { kind: 'unresolved_owner'; meta: ContactsSessionMetaFacts; latestMessageTs: number | null }\n | { kind: 'private_missing'; meta: ContactsSessionMetaFacts; latestMessageTs: number | null }\n | { kind: 'private_ambiguous'; meta: ContactsSessionMetaFacts; latestMessageTs: number | null }\n | {\n kind: 'private'\n meta: ContactsSessionMetaFacts\n latestMessageTs: number | null\n facts: ContactsCachedPrivateFacts\n }\n | {\n kind: 'group'\n meta: ContactsSessionMetaFacts\n latestMessageTs: number | null\n facts: ContactsCachedGroupFacts[]\n }\n\nexport interface ContactsSessionLatestFacts {\n latestMessageTs: number | null\n}\n\nexport type ContactsCacheReadResult<T> = { hit: true; data: T } | { hit: false }\n\ninterface VersionedContactsCacheEntry<T> {\n v: string\n data: T\n}\n\nexport function createEmptyContactsFactsCacheStats(): ContactsFactsCacheStats {\n return {\n latestHits: 0,\n latestMisses: 0,\n factsHits: 0,\n factsMisses: 0,\n writes: 0,\n }\n}\n\nexport function buildContactsSessionLatestCacheKey(algorithmVersion: string): string {\n return `contacts:latest:v${CONTACTS_FACTS_FORMAT_VERSION}:${algorithmVersion}`\n}\n\nexport function buildContactsSessionFactsCacheKey(algorithmVersion: string, timeRange: ContactsTimeRangeState): string {\n return [\n 'contacts:facts',\n `v${CONTACTS_FACTS_FORMAT_VERSION}`,\n algorithmVersion,\n `preset:${timeRange.preset}`,\n `start:${timeRange.startTs ?? 'all'}`,\n ].join(':')\n}\n\nexport function readCachedContactsSessionLatest(\n sessionId: string,\n cacheDir: string,\n key: string,\n dbVersion: string\n): ContactsCacheReadResult<ContactsSessionLatestFacts> {\n const cached = getCache<VersionedContactsCacheEntry<ContactsSessionLatestFacts>>(sessionId, key, cacheDir)\n if (!cached || cached.v !== dbVersion || !isContactsSessionLatestFacts(cached.data)) return { hit: false }\n return { hit: true, data: cached.data }\n}\n\nexport function writeCachedContactsSessionLatest(\n sessionId: string,\n cacheDir: string,\n key: string,\n dbVersion: string,\n data: ContactsSessionLatestFacts\n): void {\n setCache<VersionedContactsCacheEntry<ContactsSessionLatestFacts>>(sessionId, key, { v: dbVersion, data }, cacheDir)\n}\n\nexport function readCachedContactsSessionFacts(\n sessionId: string,\n cacheDir: string,\n key: string,\n dbVersion: string\n): ContactsCacheReadResult<ContactsSessionFacts> {\n const cached = getCache<VersionedContactsCacheEntry<ContactsSessionFacts>>(sessionId, key, cacheDir)\n if (!cached || cached.v !== dbVersion || !isContactsSessionFacts(cached.data)) return { hit: false }\n return { hit: true, data: cached.data }\n}\n\nexport function writeCachedContactsSessionFacts(\n sessionId: string,\n cacheDir: string,\n key: string,\n dbVersion: string,\n data: ContactsSessionFacts\n): void {\n setCache<VersionedContactsCacheEntry<ContactsSessionFacts>>(sessionId, key, { v: dbVersion, data }, cacheDir)\n}\n\nexport function deleteContactsSessionFactsCache(sessionId: string, cacheDir: string): void {\n deleteSessionCache(sessionId, cacheDir)\n}\n\nfunction isContactsSessionLatestFacts(value: unknown): value is ContactsSessionLatestFacts {\n return isObject(value) && isNullableNumber(value.latestMessageTs)\n}\n\nfunction isContactsSessionFacts(value: unknown): value is ContactsSessionFacts {\n if (!isObject(value) || typeof value.kind !== 'string' || !isNullableNumber(value.latestMessageTs)) return false\n switch (value.kind) {\n case 'not_chat_db':\n case 'missing_meta':\n case 'unsupported_type':\n return true\n case 'missing_owner':\n case 'unresolved_owner':\n case 'private_missing':\n case 'private_ambiguous':\n return isContactsSessionMetaFacts(value.meta)\n case 'private':\n return isContactsSessionMetaFacts(value.meta) && isContactsCachedPrivateFacts(value.facts)\n case 'group':\n return (\n isContactsSessionMetaFacts(value.meta) &&\n Array.isArray(value.facts) &&\n value.facts.every(isContactsCachedGroupFacts)\n )\n default:\n return false\n }\n}\n\nfunction isContactsSessionMetaFacts(value: unknown): value is ContactsSessionMetaFacts {\n return (\n isObject(value) &&\n typeof value.name === 'string' &&\n typeof value.platform === 'string' &&\n (value.type === ChatType.PRIVATE || value.type === ChatType.GROUP) &&\n (typeof value.ownerId === 'string' || value.ownerId === null)\n )\n}\n\nfunction isContactsCachedPrivateFacts(value: unknown): value is ContactsCachedPrivateFacts {\n return (\n isObject(value) &&\n isContactMemberRef(value.contact) &&\n isFiniteNumber(value.privateMessageCount) &&\n Array.isArray(value.activeMonths) &&\n value.activeMonths.every((month) => typeof month === 'string') &&\n isNullableNumber(value.lastMessageTs)\n )\n}\n\nfunction isContactsCachedGroupFacts(value: unknown): value is ContactsCachedGroupFacts {\n return (\n isObject(value) &&\n isContactMemberRef(value.contact) &&\n isFiniteNumber(value.messageCount) &&\n isFiniteNumber(value.coOccurrenceCount) &&\n isFiniteNumber(value.coOccurrenceRawScore) &&\n isFiniteNumber(value.replyInteractionCount) &&\n isFiniteNumber(value.repliesFromOwnerToContact) &&\n isFiniteNumber(value.repliesFromContactToOwner) &&\n isNullableNumber(value.lastInteractionTs)\n )\n}\n\nfunction isContactMemberRef(value: unknown): value is ContactMemberRef {\n return (\n isObject(value) &&\n isFiniteNumber(value.id) &&\n typeof value.platformId === 'string' &&\n typeof value.name === 'string' &&\n Array.isArray(value.aliases) &&\n value.aliases.every((alias) => typeof alias === 'string') &&\n (typeof value.avatar === 'string' || value.avatar === null)\n )\n}\n\nfunction isNullableNumber(value: unknown): value is number | null {\n return value === null || isFiniteNumber(value)\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value)\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n","import {\n CONTACTS_TIME_RANGE_PRESETS,\n type ContactsTimeRangePreset,\n type ContactsTimeRangeState,\n} from '@openchatlab/shared-types'\n\nconst SECONDS_PER_YEAR = 365 * 24 * 60 * 60\nconst DEFAULT_CONTACTS_TIME_RANGE_PRESET: ContactsTimeRangePreset = '1y'\n\nconst YEARS_BY_PRESET: Partial<Record<ContactsTimeRangePreset, number>> = {\n '1y': 1,\n '2y': 2,\n '3y': 3,\n '5y': 5,\n}\n\nexport function normalizeContactsTimeRangePreset(value: unknown): ContactsTimeRangePreset {\n return CONTACTS_TIME_RANGE_PRESETS.includes(value as ContactsTimeRangePreset)\n ? (value as ContactsTimeRangePreset)\n : DEFAULT_CONTACTS_TIME_RANGE_PRESET\n}\n\nexport function resolveContactsTimeRange(\n presetInput: unknown,\n anchorTs: number | null | undefined\n): ContactsTimeRangeState {\n const preset = normalizeContactsTimeRangePreset(presetInput)\n const normalizedAnchor = typeof anchorTs === 'number' ? anchorTs : null\n const years = YEARS_BY_PRESET[preset]\n\n return {\n preset,\n anchorTs: normalizedAnchor,\n startTs: normalizedAnchor !== null && years ? normalizedAnchor - years * SECONDS_PER_YEAR : null,\n }\n}\n","import { ChatType } from '@openchatlab/shared-types'\nimport type {\n ChatPlatform,\n ContactItem,\n ContactsDiagnostics,\n ContactsTimeRangePreset,\n ContactsTimeRangeState,\n ContactSourceSession,\n} from '@openchatlab/shared-types'\nimport {\n MIN_PRIVATE_SESSIONS_FOR_CONTACTS,\n computeFriendScores,\n computeNonFriendScores,\n getGroupContactFacts,\n getLatestContactMessageTs,\n getPrivateContactFacts,\n getSessionMeta,\n isChatSessionDb,\n isNameMatchPlatform,\n resolveOwnerMember,\n} from '@openchatlab/core'\nimport type { ContactMemberRef, SessionMeta } from '@openchatlab/core'\nimport { getDbFileVersion } from '../../cache/analytics-cache'\nimport { appLogger } from '../../logging/app-logger'\nimport type { SessionRuntimeAdapter } from '../adapters'\nimport {\n buildContactsSessionFactsCacheKey,\n buildContactsSessionLatestCacheKey,\n createEmptyContactsFactsCacheStats,\n readCachedContactsSessionFacts,\n readCachedContactsSessionLatest,\n writeCachedContactsSessionFacts,\n writeCachedContactsSessionLatest,\n type ContactsCachedGroupFacts,\n type ContactsCachedPrivateFacts,\n type ContactsFactsCacheStats,\n type ContactsSessionFacts,\n type ContactsSessionLatestFacts,\n type ContactsSessionMetaFacts,\n} from './facts-cache'\nimport { resolveContactsTimeRange } from './time-range'\n\nexport const CONTACTS_ALGORITHM_VERSION = 'contacts-v2'\n\nexport interface ContactsWorkerStats {\n durationMs: number\n totalSessions: number\n processedSessions: number\n skippedFailedSessions: number\n}\n\nexport interface ContactsSnapshot {\n contacts: ContactItem[]\n diagnostics: ContactsDiagnostics\n algorithmVersion: string\n signature: string\n timeRange: ContactsTimeRangeState\n computedAt: number\n workerStats: ContactsWorkerStats\n}\n\nexport interface ContactsComputeProgress {\n processedSessions: number\n totalSessions: number\n currentSessionId?: string\n}\n\nexport interface ComputeContactsSnapshotOptions {\n adapter: SessionRuntimeAdapter\n signature: string\n timeRangePreset?: ContactsTimeRangePreset\n factsCacheDir?: string\n now?: () => number\n onProgress?: (progress: ContactsComputeProgress) => void\n}\n\ninterface ContactAccumulator {\n key: string\n platform: ChatPlatform\n platformId: string\n sessionScoped: boolean\n sessionId?: string\n displayName: string\n aliases: Set<string>\n avatar: string | null\n isFriend: boolean\n privateMessageCount: number\n activePrivateMonths: Set<string>\n commonGroupSessionIds: Set<string>\n coOccurrenceCount: number\n coOccurrenceRawScore: number\n replyInteractionCount: number\n repliesFromOwnerToContact: number\n repliesFromContactToOwner: number\n sourceSessions: ContactSourceSession[]\n lastInteractionTs: number | null\n}\n\ninterface BuildContactsResult {\n contacts: ContactItem[]\n diagnostics: ContactsDiagnostics\n}\n\ninterface ContactsFactsCacheContext {\n dir: string\n latestKey: string\n stats: ContactsFactsCacheStats\n}\n\nexport function computeContactsSnapshot(options: ComputeContactsSnapshotOptions): ContactsSnapshot {\n const startedAt = options.now?.() ?? Date.now()\n const sessionIds = options.adapter.listSessionIds()\n const factsCache = options.factsCacheDir ? createFactsCacheContext(options.factsCacheDir) : null\n const timeRange = resolveContactsTimeRange(\n options.timeRangePreset,\n findGlobalLatestMessageTs(options.adapter, sessionIds, factsCache)\n )\n const result = computeContacts({\n adapter: options.adapter,\n sessionIds,\n timeRange,\n factsCache,\n onProgress: options.onProgress,\n })\n const finishedAt = options.now?.() ?? Date.now()\n if (factsCache) {\n appLogger.info('contacts', 'contacts session facts cache summary', factsCache.stats)\n }\n return {\n ...result,\n algorithmVersion: CONTACTS_ALGORITHM_VERSION,\n signature: options.signature,\n timeRange,\n computedAt: finishedAt,\n workerStats: {\n durationMs: Math.max(0, finishedAt - startedAt),\n totalSessions: sessionIds.length,\n processedSessions: sessionIds.length,\n skippedFailedSessions: result.diagnostics.skippedFailedSessions,\n },\n }\n}\n\nfunction computeContacts(options: {\n adapter: SessionRuntimeAdapter\n sessionIds: string[]\n timeRange: ContactsTimeRangeState\n factsCache: ContactsFactsCacheContext | null\n onProgress?: (progress: ContactsComputeProgress) => void\n}): BuildContactsResult {\n const diagnostics = createEmptyDiagnostics()\n const accumulators = new Map<string, ContactAccumulator>()\n let processedSessions = 0\n\n for (const sessionId of options.sessionIds) {\n options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId })\n try {\n const facts = getSessionFacts(options.adapter, sessionId, options.timeRange, options.factsCache)\n applySessionFacts(accumulators, diagnostics, sessionId, facts)\n } catch (error) {\n diagnostics.skippedFailedSessions++\n appLogger.error('contacts', `failed to process contact session: ${sessionId}`, error)\n } finally {\n processedSessions++\n options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId })\n }\n }\n\n diagnostics.contactsEnabled = diagnostics.activePrivateSessionCount > MIN_PRIVATE_SESSIONS_FOR_CONTACTS\n const contacts = buildContactItems([...accumulators.values()])\n return { contacts, diagnostics }\n}\n\nfunction findGlobalLatestMessageTs(\n adapter: SessionRuntimeAdapter,\n sessionIds: string[],\n factsCache: ContactsFactsCacheContext | null\n): number | null {\n let latest: number | null = null\n for (const sessionId of sessionIds) {\n const dbVersion = getSessionDbVersion(adapter, sessionId)\n const cached = readLatestFacts(sessionId, factsCache, dbVersion)\n if (cached) {\n if (cached.latestMessageTs !== null) latest = Math.max(latest ?? 0, cached.latestMessageTs)\n continue\n }\n try {\n const db = adapter.openReadonly(sessionId)\n const ts = db && isChatSessionDb(db) ? getLatestContactMessageTs(db) : null\n writeLatestFacts(adapter, sessionId, factsCache, { latestMessageTs: ts }, dbVersion)\n if (ts !== null) latest = Math.max(latest ?? 0, ts)\n } catch (error) {\n appLogger.error('contacts', `failed to inspect contact session range: ${sessionId}`, error)\n }\n }\n return latest\n}\n\nfunction getSessionFacts(\n adapter: SessionRuntimeAdapter,\n sessionId: string,\n timeRange: ContactsTimeRangeState,\n factsCache: ContactsFactsCacheContext | null\n): ContactsSessionFacts {\n const dbVersion = getSessionDbVersion(adapter, sessionId)\n const cached = readSessionFacts(sessionId, timeRange, factsCache, dbVersion)\n if (cached) return cached\n\n const facts = computeSessionFacts(adapter, sessionId, timeRange)\n writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, dbVersion)\n return facts\n}\n\nfunction computeSessionFacts(\n adapter: SessionRuntimeAdapter,\n sessionId: string,\n timeRange: ContactsTimeRangeState\n): ContactsSessionFacts {\n const db = adapter.openReadonly(sessionId)\n if (!db || !isChatSessionDb(db)) return { kind: 'not_chat_db', latestMessageTs: null }\n\n const latestMessageTs = getLatestContactMessageTs(db)\n const meta = getSessionMeta(db)\n if (!meta) return { kind: 'missing_meta', latestMessageTs }\n if (meta.type !== ChatType.PRIVATE && meta.type !== ChatType.GROUP)\n return { kind: 'unsupported_type', latestMessageTs }\n\n const cachedMeta = toContactsSessionMetaFacts(meta)\n if (!meta.ownerId?.trim()) return { kind: 'missing_owner', meta: cachedMeta, latestMessageTs }\n\n const owner = resolveOwnerMember(db)\n if (!owner) return { kind: 'unresolved_owner', meta: cachedMeta, latestMessageTs }\n\n if (meta.type === ChatType.PRIVATE) {\n const facts = getPrivateContactFacts(db, owner.id, { startTs: timeRange.startTs })\n if (facts.type === 'missing') return { kind: 'private_missing', meta: cachedMeta, latestMessageTs }\n if (facts.type === 'ambiguous') return { kind: 'private_ambiguous', meta: cachedMeta, latestMessageTs }\n return { kind: 'private', meta: cachedMeta, latestMessageTs, facts }\n }\n\n return {\n kind: 'group',\n meta: cachedMeta,\n latestMessageTs,\n facts: getGroupContactFacts(db, owner.id, { startTs: timeRange.startTs }),\n }\n}\n\nfunction applySessionFacts(\n accumulators: Map<string, ContactAccumulator>,\n diagnostics: ContactsDiagnostics,\n sessionId: string,\n sessionFacts: ContactsSessionFacts\n): void {\n if ('meta' in sessionFacts && sessionFacts.meta.type === ChatType.PRIVATE) diagnostics.privateSessionCount++\n\n switch (sessionFacts.kind) {\n case 'missing_owner':\n diagnostics.skippedMissingOwnerSessions++\n return\n case 'unresolved_owner':\n diagnostics.skippedUnresolvedOwnerSessions++\n return\n case 'private_ambiguous':\n diagnostics.skippedAmbiguousPrivateSessions++\n return\n case 'private':\n applyPrivateFacts(accumulators, diagnostics, sessionId, sessionFacts.meta, sessionFacts.facts)\n return\n case 'group':\n applyGroupFacts(accumulators, sessionId, sessionFacts.meta, sessionFacts.facts)\n return\n default:\n return\n }\n}\n\nfunction applyPrivateFacts(\n accumulators: Map<string, ContactAccumulator>,\n diagnostics: ContactsDiagnostics,\n sessionId: string,\n meta: ContactsSessionMetaFacts,\n facts: ContactsCachedPrivateFacts\n): void {\n if (facts.privateMessageCount <= 0) return\n diagnostics.activePrivateSessionCount++\n\n const acc = getOrCreateAccumulator(accumulators, sessionId, meta, facts.contact)\n acc.isFriend = true\n acc.privateMessageCount += facts.privateMessageCount\n for (const month of facts.activeMonths) acc.activePrivateMonths.add(month)\n updateLastInteraction(acc, facts.lastMessageTs)\n acc.sourceSessions.push({\n id: sessionId,\n name: meta.name,\n platform: meta.platform,\n type: ChatType.PRIVATE,\n messageCount: facts.privateMessageCount,\n privateMessageCount: facts.privateMessageCount,\n lastMessageTs: facts.lastMessageTs,\n })\n}\n\nfunction applyGroupFacts(\n accumulators: Map<string, ContactAccumulator>,\n sessionId: string,\n meta: ContactsSessionMetaFacts,\n sessionFacts: ContactsCachedGroupFacts[]\n): void {\n for (const facts of sessionFacts) {\n if (!hasGroupContactSignal(facts)) continue\n const acc = getOrCreateAccumulator(accumulators, sessionId, meta, facts.contact)\n acc.commonGroupSessionIds.add(sessionId)\n acc.coOccurrenceCount += facts.coOccurrenceCount\n acc.coOccurrenceRawScore += facts.coOccurrenceRawScore\n acc.replyInteractionCount += facts.replyInteractionCount\n acc.repliesFromOwnerToContact += facts.repliesFromOwnerToContact\n acc.repliesFromContactToOwner += facts.repliesFromContactToOwner\n updateLastInteraction(acc, facts.lastInteractionTs)\n acc.sourceSessions.push({\n id: sessionId,\n name: meta.name,\n platform: meta.platform,\n type: ChatType.GROUP,\n messageCount: facts.messageCount,\n coOccurrenceCount: facts.coOccurrenceCount,\n coOccurrenceRawScore: facts.coOccurrenceRawScore,\n replyInteractionCount: facts.replyInteractionCount,\n repliesFromOwnerToContact: facts.repliesFromOwnerToContact,\n repliesFromContactToOwner: facts.repliesFromContactToOwner,\n lastInteractionTs: facts.lastInteractionTs,\n })\n }\n}\n\nfunction hasGroupContactSignal(facts: ContactsCachedGroupFacts): boolean {\n return facts.messageCount > 0 || facts.coOccurrenceCount > 0 || facts.replyInteractionCount > 0\n}\n\nfunction createFactsCacheContext(dir: string): ContactsFactsCacheContext {\n return {\n dir,\n latestKey: buildContactsSessionLatestCacheKey(CONTACTS_ALGORITHM_VERSION),\n stats: createEmptyContactsFactsCacheStats(),\n }\n}\n\nfunction readLatestFacts(\n sessionId: string,\n factsCache: ContactsFactsCacheContext | null,\n dbVersion: string\n): ContactsSessionLatestFacts | null {\n if (!factsCache) return null\n const cached = readCachedContactsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion)\n if (!cached.hit) {\n factsCache.stats.latestMisses++\n return null\n }\n factsCache.stats.latestHits++\n return cached.data\n}\n\nfunction writeLatestFacts(\n adapter: SessionRuntimeAdapter,\n sessionId: string,\n factsCache: ContactsFactsCacheContext | null,\n data: ContactsSessionLatestFacts,\n expectedDbVersion: string\n): void {\n if (!factsCache) return\n const dbVersion = getSessionDbVersion(adapter, sessionId)\n if (dbVersion !== expectedDbVersion) {\n appLogger.debug('contacts', 'skipped contacts latest facts cache write because db version changed', {\n sessionId,\n })\n return\n }\n writeCachedContactsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, data)\n factsCache.stats.writes++\n}\n\nfunction readSessionFacts(\n sessionId: string,\n timeRange: ContactsTimeRangeState,\n factsCache: ContactsFactsCacheContext | null,\n dbVersion: string\n): ContactsSessionFacts | null {\n if (!factsCache) return null\n const cached = readCachedContactsSessionFacts(\n sessionId,\n factsCache.dir,\n buildContactsSessionFactsCacheKey(CONTACTS_ALGORITHM_VERSION, timeRange),\n dbVersion\n )\n if (!cached.hit) {\n factsCache.stats.factsMisses++\n return null\n }\n factsCache.stats.factsHits++\n return cached.data\n}\n\nfunction writeSessionFacts(\n adapter: SessionRuntimeAdapter,\n sessionId: string,\n timeRange: ContactsTimeRangeState,\n factsCache: ContactsFactsCacheContext | null,\n facts: ContactsSessionFacts,\n expectedDbVersion: string\n): void {\n if (!factsCache) return\n const dbVersion = getSessionDbVersion(adapter, sessionId)\n if (dbVersion !== expectedDbVersion) {\n appLogger.debug('contacts', 'skipped contacts session facts cache write because db version changed', {\n sessionId,\n })\n return\n }\n writeCachedContactsSessionFacts(\n sessionId,\n factsCache.dir,\n buildContactsSessionFactsCacheKey(CONTACTS_ALGORITHM_VERSION, timeRange),\n dbVersion,\n facts\n )\n writeCachedContactsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, {\n latestMessageTs: facts.latestMessageTs,\n })\n factsCache.stats.writes += 2\n}\n\nfunction getSessionDbVersion(adapter: SessionRuntimeAdapter, sessionId: string): string {\n return getDbFileVersion(adapter.getDbPath(sessionId))\n}\n\nfunction toContactsSessionMetaFacts(meta: SessionMeta): ContactsSessionMetaFacts {\n return {\n name: meta.name,\n platform: meta.platform,\n type: meta.type as ChatType.PRIVATE | ChatType.GROUP,\n ownerId: meta.ownerId,\n }\n}\n\nfunction buildContactItems(accumulators: ContactAccumulator[]): ContactItem[] {\n const friendInputs = accumulators\n .filter((acc) => acc.isFriend)\n .map((acc) => ({\n acc,\n privateMessageCount: acc.privateMessageCount,\n activeMonths: [...acc.activePrivateMonths],\n commonGroupCount: acc.commonGroupSessionIds.size,\n }))\n const nonFriendInputs = accumulators\n .filter((acc) => !acc.isFriend)\n .map((acc) => ({\n acc,\n coOccurrenceRawScore: acc.coOccurrenceRawScore,\n commonGroupCount: acc.commonGroupSessionIds.size,\n replyInteractionCount: acc.replyInteractionCount,\n coOccurrenceCount: acc.coOccurrenceCount,\n }))\n\n const friendScores = computeFriendScores(friendInputs)\n const nonFriendScores = computeNonFriendScores(nonFriendInputs)\n const contacts: ContactItem[] = []\n\n for (const input of friendInputs) {\n const score = friendScores.get(input) ?? { score: 0, scoreBreakdown: {} }\n contacts.push(toContactItem(input.acc, 'friend', score))\n }\n\n for (const input of nonFriendInputs) {\n const score = nonFriendScores.get(input) ?? { score: 0, scoreBreakdown: {} }\n contacts.push(toContactItem(input.acc, 'non_friend', score))\n }\n\n return contacts.sort((a, b) => b.score - a.score || a.displayName.localeCompare(b.displayName))\n}\n\nfunction toContactItem(\n acc: ContactAccumulator,\n pool: 'friend' | 'non_friend',\n scoring: { score: number; scoreBreakdown: ContactItem['scoreBreakdown'] }\n): ContactItem {\n const aliases = [...acc.aliases].filter((alias) => alias !== acc.displayName)\n const searchText = [acc.displayName, acc.platformId, ...aliases].join(' ').toLowerCase()\n\n const item: ContactItem = {\n key: acc.key,\n platform: acc.platform,\n platformId: acc.platformId,\n sessionScoped: acc.sessionScoped,\n sessionId: acc.sessionId,\n displayName: acc.displayName,\n aliases,\n avatar: acc.avatar,\n isFriend: acc.isFriend,\n pool,\n score: scoring.score,\n scoreBreakdown: {\n ...scoring.scoreBreakdown,\n privateMessageCount: acc.privateMessageCount || scoring.scoreBreakdown.privateMessageCount,\n activePrivateMonths: acc.activePrivateMonths.size || scoring.scoreBreakdown.activePrivateMonths,\n commonGroupCount: acc.commonGroupSessionIds.size,\n coOccurrenceCount: acc.coOccurrenceCount,\n coOccurrenceRawScore: acc.coOccurrenceRawScore,\n replyInteractionCount: acc.replyInteractionCount,\n repliesFromOwnerToContact: acc.repliesFromOwnerToContact,\n repliesFromContactToOwner: acc.repliesFromContactToOwner,\n },\n sourceSessions: acc.sourceSessions,\n searchText,\n lastInteractionTs: acc.lastInteractionTs,\n }\n if (pool === 'friend') item.friendSource = 'private'\n return item\n}\n\nexport function createEmptyContactsDiagnostics(): ContactsDiagnostics {\n return createEmptyDiagnostics()\n}\n\nfunction createEmptyDiagnostics(): ContactsDiagnostics {\n return {\n privateSessionCount: 0,\n activePrivateSessionCount: 0,\n contactsEnabled: false,\n skippedMissingOwnerSessions: 0,\n skippedUnresolvedOwnerSessions: 0,\n skippedAmbiguousPrivateSessions: 0,\n skippedInvalidPlatformIdMembers: 0,\n skippedFailedSessions: 0,\n warnings: [],\n }\n}\n\nfunction getOrCreateAccumulator(\n accumulators: Map<string, ContactAccumulator>,\n sessionId: string,\n meta: ContactsSessionMetaFacts,\n contact: ContactMemberRef\n): ContactAccumulator {\n const sessionScoped = shouldScopeContactToSession(meta.platform, contact)\n const key = buildContactKey(meta.platform, contact.platformId, sessionScoped ? sessionId : undefined)\n const existing = accumulators.get(key)\n if (existing) {\n mergeContactIdentity(existing, contact)\n return existing\n }\n\n const created: ContactAccumulator = {\n key,\n platform: meta.platform,\n platformId: contact.platformId,\n sessionScoped,\n sessionId: sessionScoped ? sessionId : undefined,\n displayName: contact.name || contact.platformId,\n aliases: new Set([contact.platformId, contact.name, ...contact.aliases].filter(Boolean)),\n avatar: contact.avatar,\n isFriend: false,\n privateMessageCount: 0,\n activePrivateMonths: new Set(),\n commonGroupSessionIds: new Set(),\n coOccurrenceCount: 0,\n coOccurrenceRawScore: 0,\n replyInteractionCount: 0,\n repliesFromOwnerToContact: 0,\n repliesFromContactToOwner: 0,\n sourceSessions: [],\n lastInteractionTs: null,\n }\n accumulators.set(key, created)\n return created\n}\n\nfunction shouldScopeContactToSession(platform: ChatPlatform, contact: ContactMemberRef): boolean {\n if (isNameMatchPlatform(platform)) return true\n return platform.trim().toLowerCase() === 'qq' && contact.platformId.trim() === contact.name.trim()\n}\n\nfunction buildContactKey(platform: ChatPlatform, platformId: string, sessionId?: string): string {\n const normalizedPlatform = platform.trim()\n const normalizedPlatformId = platformId.trim()\n if (!normalizedPlatform) throw new Error('platform is required')\n if (!normalizedPlatformId) throw new Error('platformId is required')\n return sessionId?.trim()\n ? `${normalizedPlatform}:${sessionId.trim()}:${normalizedPlatformId}`\n : `${normalizedPlatform}:${normalizedPlatformId}`\n}\n\nfunction mergeContactIdentity(acc: ContactAccumulator, contact: ContactMemberRef): void {\n if (contact.name) acc.aliases.add(contact.name)\n acc.aliases.add(contact.platformId)\n for (const alias of contact.aliases) acc.aliases.add(alias)\n if ((!acc.displayName || acc.displayName === acc.platformId) && contact.name) {\n acc.displayName = contact.name\n }\n if (!acc.avatar && contact.avatar) acc.avatar = contact.avatar\n}\n\nfunction updateLastInteraction(acc: ContactAccumulator, ts: number | null): void {\n if (ts === null) return\n acc.lastInteractionTs = Math.max(acc.lastInteractionTs ?? 0, ts)\n}\n"],"mappings":"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","names":[]}
|