chatlab-cli 0.28.1 → 0.30.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-ISJ2DC7R.mjs → chat-command-UZ23GKO7.mjs} +8 -5
- package/dist/{chat-command-ISJ2DC7R.mjs.map → chat-command-UZ23GKO7.mjs.map} +1 -1
- package/dist/{chunk-AB6TAXTQ.mjs → chunk-4IZ3WKYU.mjs} +591 -310
- package/dist/chunk-4IZ3WKYU.mjs.map +1 -0
- package/dist/{chunk-3QBVO4J7.mjs → chunk-5UBHNQFZ.mjs} +101 -41
- package/dist/chunk-5UBHNQFZ.mjs.map +1 -0
- package/dist/chunk-7BZLS2KW.mjs +8965 -0
- package/dist/chunk-7BZLS2KW.mjs.map +1 -0
- package/dist/{chunk-ZVDOFWL7.mjs → chunk-A2SANKG4.mjs} +4043 -10895
- package/dist/chunk-A2SANKG4.mjs.map +1 -0
- package/dist/chunk-CNSYXRJP.mjs +526 -0
- package/dist/chunk-CNSYXRJP.mjs.map +1 -0
- package/dist/{chunk-T7IETWHN.mjs → chunk-FCCL5Z4W.mjs} +5 -3
- package/dist/{chunk-T7IETWHN.mjs.map → chunk-FCCL5Z4W.mjs.map} +1 -1
- package/dist/chunk-M55KW2HA.mjs +2323 -0
- package/dist/chunk-M55KW2HA.mjs.map +1 -0
- package/dist/chunk-MUPILWAZ.mjs +1339 -0
- package/dist/chunk-MUPILWAZ.mjs.map +1 -0
- package/dist/{chunk-YBS3YQB6.mjs → chunk-P3ZOLTGS.mjs} +31 -57
- package/dist/chunk-P3ZOLTGS.mjs.map +1 -0
- package/dist/{chunk-NJB2NNB2.mjs → chunk-RDR6PS2R.mjs} +2 -2
- package/dist/{chunk-ERAAWLEF.mjs → chunk-ZFU2CHW3.mjs} +11 -8
- package/dist/{chunk-ERAAWLEF.mjs.map → chunk-ZFU2CHW3.mjs.map} +1 -1
- package/dist/cli.mjs +7 -4
- package/dist/contacts-worker.mjs +48 -0
- package/dist/contacts-worker.mjs.map +1 -0
- package/dist/{http-MBILV37U.mjs → http-UWRA5FLB.mjs} +11 -8
- package/dist/{import-EYV3GXHE.mjs → import-745KQL7N.mjs} +7 -4
- package/dist/index.mjs +11 -8
- package/dist/{mcp-H55AEXU5.mjs → mcp-UHN5JOHC.mjs} +8 -5
- package/dist/{mcp-H55AEXU5.mjs.map → mcp-UHN5JOHC.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-36ZIR65K.mjs → src-Y7MDRXLJ.mjs} +68 -44
- package/dist/{stream-import-YZMZ56B5.mjs → stream-import-3JMXQD7P.mjs} +7 -4
- package/dist/{worker-runtime-YDYS2C6T.mjs → worker-runtime-63RYMGZI.mjs} +7 -3
- package/dist/{worker-runtime-YDYS2C6T.mjs.map → worker-runtime-63RYMGZI.mjs.map} +1 -1
- package/dist-web/assets/BufferResource-BVdJ_I5X.js +185 -0
- package/dist-web/assets/CanvasRenderer-BaRuT54R.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-N5CoiJzN.js +110 -0
- package/dist-web/assets/{EChartWordcloud-B_YisOW5.js → EChartWordcloud-Clg60FMo.js} +1 -1
- package/dist-web/assets/Filter-B7cNFtuE.js +1 -0
- package/dist-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-D_dJuLWJ.js +1 -0
- package/dist-web/assets/MessageExportModal-B2Qm6VVj.js +1 -0
- package/dist-web/assets/PageHeader-Bbn87QZS.css +1 -0
- package/dist-web/assets/PageHeader-PjlWy9fC.js +1 -0
- package/dist-web/assets/RelationshipGalaxyCanvas-11MafciW.js +244 -0
- package/dist-web/assets/RelationshipGalaxyThreeCanvas-BluPHjCR.css +1 -0
- package/dist-web/assets/RelationshipGalaxyThreeCanvas-C1I4B8NX.js +4108 -0
- package/dist-web/assets/RenderTargetSystem-Chkz1WlI.js +184 -0
- package/dist-web/assets/WebGLRenderer-xK19ru58.js +156 -0
- package/dist-web/assets/WebGPURenderer-CL5ZaKsn.js +41 -0
- package/dist-web/assets/browserAll-CyyNzRD_.js +14 -0
- package/dist-web/assets/electron-MXaTKaMu.js +1 -0
- package/dist-web/assets/{fetch-29ZBtGV0.js → fetch-D1dDiFO4.js} +1 -1
- package/dist-web/assets/{fetch-TCeykOkO.js → fetch-DDnVXGte.js} +1 -1
- package/dist-web/assets/{fetch-Bh1fo3Df.js → fetch-DTwdRTXM.js} +1 -1
- package/dist-web/assets/{fetch-BcxrU2mr.js → fetch-DaaJwT-5.js} +1 -1
- package/dist-web/assets/{fetch-tIIVdXSv.js → fetch-DeA0ztwT.js} +1 -1
- package/dist-web/assets/fetch-DpnIGZSN.js +17 -0
- package/dist-web/assets/{fetch-C3yhWO2S.js → fetch-DsE-xcP2.js} +1 -1
- package/dist-web/assets/{fetch-BYczbT4C.js → fetch-DsTgYu_X.js} +1 -1
- package/dist-web/assets/fetch-hKE53UO4.js +1 -0
- package/dist-web/assets/fetch-u9GTMhMF.js +1 -0
- package/dist-web/assets/index---rXG1hZ.css +1 -0
- package/dist-web/assets/index-BVkE4qis.js +22 -0
- package/dist-web/assets/index-BcXLcFnV.css +1 -0
- package/dist-web/assets/index-BnHKyESZ.js +1 -0
- package/dist-web/assets/index-Btr5H7d-.js +2 -0
- package/dist-web/assets/{index-BX3tqfLz.css → index-C1p9CbmN.css} +1 -1
- package/dist-web/assets/index-COpza1A4.css +1 -0
- package/dist-web/assets/index-D04LWJop.js +173 -0
- package/dist-web/assets/index-DFCYmzGE.js +2 -0
- package/dist-web/assets/index-DosG7LmJ.css +1 -0
- package/dist-web/assets/index-DqXHRIhI.js +1 -0
- package/dist-web/assets/{index-fbWrQpgH.js → index-DqtXt3wl.js} +1 -1
- package/dist-web/assets/index-Dszv-QFp.js +64 -0
- package/dist-web/assets/index-DzLrWGXu.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-DaLxYaC4.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-H3XLx159.js → web-DPVWfPS-.js} +1 -1
- package/dist-web/assets/webworkerAll-DF5QV_vN.js +83 -0
- package/dist-web/index.html +4 -4
- package/package.json +1 -1
- package/dist/chunk-3QBVO4J7.mjs.map +0 -1
- package/dist/chunk-AB6TAXTQ.mjs.map +0 -1
- package/dist/chunk-GHTGFM6O.mjs +0 -540
- package/dist/chunk-GHTGFM6O.mjs.map +0 -1
- package/dist/chunk-YBS3YQB6.mjs.map +0 -1
- package/dist/chunk-ZVDOFWL7.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-Do3x4aL3.js +0 -110
- package/dist-web/assets/MessageExportModal-rijqUusq.js +0 -1
- package/dist-web/assets/electron-DWDCqx_d.js +0 -1
- package/dist-web/assets/fetch-BAxKmbKF.js +0 -1
- package/dist-web/assets/fetch-Da_TBxrX.js +0 -1
- package/dist-web/assets/fetch-DyD8wDhb.js +0 -17
- package/dist-web/assets/index-BzjLqlW_.js +0 -173
- package/dist-web/assets/index-C74qEDFh.js +0 -64
- package/dist-web/assets/index-CM6xNPFp.css +0 -1
- package/dist-web/assets/index-CgK2LLIF.js +0 -12
- package/dist-web/assets/index-CjkOvM3N.js +0 -2
- package/dist-web/assets/index-Dno7CADA.js +0 -1
- package/dist-web/assets/index-I3SWZjnW.css +0 -1
- /package/dist/{chunk-NJB2NNB2.mjs.map → chunk-RDR6PS2R.mjs.map} +0 -0
- /package/dist/{http-MBILV37U.mjs.map → http-UWRA5FLB.mjs.map} +0 -0
- /package/dist/{import-EYV3GXHE.mjs.map → import-745KQL7N.mjs.map} +0 -0
- /package/dist/{src-36ZIR65K.mjs.map → src-Y7MDRXLJ.mjs.map} +0 -0
- /package/dist/{stream-import-YZMZ56B5.mjs.map → stream-import-3JMXQD7P.mjs.map} +0 -0
|
@@ -0,0 +1,1339 @@
|
|
|
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
|
+
appLogger,
|
|
10
|
+
computeFriendScores,
|
|
11
|
+
computeNonFriendScores,
|
|
12
|
+
getCache,
|
|
13
|
+
getDbFileVersion,
|
|
14
|
+
getGroupContactFacts,
|
|
15
|
+
getGroupRelationshipGraphFacts,
|
|
16
|
+
getLatestContactMessageTs,
|
|
17
|
+
getPrivateContactFacts,
|
|
18
|
+
getSessionMeta,
|
|
19
|
+
isChatSessionDb,
|
|
20
|
+
isNameMatchPlatform,
|
|
21
|
+
resolveOwnerMember,
|
|
22
|
+
setCache
|
|
23
|
+
} from "./chunk-7BZLS2KW.mjs";
|
|
24
|
+
|
|
25
|
+
// ../../packages/node-runtime/src/services/people/relationships/facts-cache.ts
|
|
26
|
+
var PEOPLE_RELATIONSHIPS_FACTS_FORMAT_VERSION = 3;
|
|
27
|
+
function createEmptyPeopleRelationshipsFactsCacheStats() {
|
|
28
|
+
return {
|
|
29
|
+
latestHits: 0,
|
|
30
|
+
latestMisses: 0,
|
|
31
|
+
factsHits: 0,
|
|
32
|
+
factsMisses: 0,
|
|
33
|
+
writes: 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildPeopleRelationshipsSessionLatestCacheKey(algorithmVersion) {
|
|
37
|
+
return `people-relationships:latest:v${PEOPLE_RELATIONSHIPS_FACTS_FORMAT_VERSION}:${algorithmVersion}`;
|
|
38
|
+
}
|
|
39
|
+
function buildPeopleRelationshipsSessionFactsCacheKey(algorithmVersion, timeRange) {
|
|
40
|
+
return [
|
|
41
|
+
"people-relationships:facts",
|
|
42
|
+
`v${PEOPLE_RELATIONSHIPS_FACTS_FORMAT_VERSION}`,
|
|
43
|
+
algorithmVersion,
|
|
44
|
+
`preset:${timeRange.preset}`,
|
|
45
|
+
`start:${timeRange.startTs ?? "all"}`
|
|
46
|
+
].join(":");
|
|
47
|
+
}
|
|
48
|
+
function readCachedPeopleRelationshipsSessionLatest(sessionId, cacheDir, key, dbVersion) {
|
|
49
|
+
const cached = getCache(
|
|
50
|
+
sessionId,
|
|
51
|
+
key,
|
|
52
|
+
cacheDir
|
|
53
|
+
);
|
|
54
|
+
if (!cached || cached.v !== dbVersion || !isPeopleRelationshipsSessionLatestFacts(cached.data)) return { hit: false };
|
|
55
|
+
return { hit: true, data: cached.data };
|
|
56
|
+
}
|
|
57
|
+
function writeCachedPeopleRelationshipsSessionLatest(sessionId, cacheDir, key, dbVersion, data) {
|
|
58
|
+
setCache(
|
|
59
|
+
sessionId,
|
|
60
|
+
key,
|
|
61
|
+
{ v: dbVersion, data },
|
|
62
|
+
cacheDir
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function readCachedPeopleRelationshipsSessionFacts(sessionId, cacheDir, key, dbVersion) {
|
|
66
|
+
const cached = getCache(
|
|
67
|
+
sessionId,
|
|
68
|
+
key,
|
|
69
|
+
cacheDir
|
|
70
|
+
);
|
|
71
|
+
if (!cached || cached.v !== dbVersion || !isPeopleRelationshipsSessionFacts(cached.data)) return { hit: false };
|
|
72
|
+
return { hit: true, data: cached.data };
|
|
73
|
+
}
|
|
74
|
+
function writeCachedPeopleRelationshipsSessionFacts(sessionId, cacheDir, key, dbVersion, data) {
|
|
75
|
+
setCache(
|
|
76
|
+
sessionId,
|
|
77
|
+
key,
|
|
78
|
+
{ v: dbVersion, data },
|
|
79
|
+
cacheDir
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function isPeopleRelationshipsSessionLatestFacts(value) {
|
|
83
|
+
return isObject(value) && isNullableNumber(value.latestMessageTs);
|
|
84
|
+
}
|
|
85
|
+
function isPeopleRelationshipsSessionFacts(value) {
|
|
86
|
+
if (!isObject(value) || typeof value.kind !== "string" || !isNullableNumber(value.latestMessageTs)) return false;
|
|
87
|
+
switch (value.kind) {
|
|
88
|
+
case "not_chat_db":
|
|
89
|
+
case "missing_meta":
|
|
90
|
+
case "unsupported_type":
|
|
91
|
+
return true;
|
|
92
|
+
case "missing_owner":
|
|
93
|
+
case "unresolved_owner":
|
|
94
|
+
case "private_missing":
|
|
95
|
+
case "private_ambiguous":
|
|
96
|
+
return isPeopleRelationshipsSessionMetaFacts(value.meta);
|
|
97
|
+
case "private":
|
|
98
|
+
return isPeopleRelationshipsSessionMetaFacts(value.meta) && isPeopleRelationshipsCachedPrivateFacts(value.facts);
|
|
99
|
+
case "group":
|
|
100
|
+
return isPeopleRelationshipsSessionMetaFacts(value.meta) && isPeopleRelationshipsCachedGroupFacts(value.facts);
|
|
101
|
+
default:
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isPeopleRelationshipsSessionMetaFacts(value) {
|
|
106
|
+
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) && (value.owner === void 0 || isContactMemberRef(value.owner));
|
|
107
|
+
}
|
|
108
|
+
function isPeopleRelationshipsCachedPrivateFacts(value) {
|
|
109
|
+
return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.privateMessageCount) && Array.isArray(value.activeMonths) && value.activeMonths.every((month) => typeof month === "string") && isNullableNumber(value.lastMessageTs);
|
|
110
|
+
}
|
|
111
|
+
function isPeopleRelationshipsCachedGroupFacts(value) {
|
|
112
|
+
return isObject(value) && Array.isArray(value.members) && value.members.every(isRelationshipGraphMemberFact) && Array.isArray(value.edges) && value.edges.every(isRelationshipGraphEdgeFact) && Array.isArray(value.ownerEdges) && value.ownerEdges.every(isGroupContactFacts) && isFiniteNumber(value.ownerMessageCount);
|
|
113
|
+
}
|
|
114
|
+
function isGroupContactFacts(value) {
|
|
115
|
+
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);
|
|
116
|
+
}
|
|
117
|
+
function isRelationshipGraphMemberFact(value) {
|
|
118
|
+
return isObject(value) && isContactMemberRef(value.contact) && isFiniteNumber(value.messageCount) && isNullableNumber(value.lastMessageTs);
|
|
119
|
+
}
|
|
120
|
+
function isRelationshipGraphEdgeFact(value) {
|
|
121
|
+
return isObject(value) && isContactMemberRef(value.source) && isContactMemberRef(value.target) && isFiniteNumber(value.coOccurrenceCount) && isFiniteNumber(value.coOccurrenceRawScore) && isFiniteNumber(value.replyInteractionCount) && isFiniteNumber(value.repliesFromSourceToTarget) && isFiniteNumber(value.repliesFromTargetToSource) && isNullableNumber(value.lastInteractionTs);
|
|
122
|
+
}
|
|
123
|
+
function isContactMemberRef(value) {
|
|
124
|
+
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);
|
|
125
|
+
}
|
|
126
|
+
function isNullableNumber(value) {
|
|
127
|
+
return value === null || isFiniteNumber(value);
|
|
128
|
+
}
|
|
129
|
+
function isFiniteNumber(value) {
|
|
130
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
131
|
+
}
|
|
132
|
+
function isObject(value) {
|
|
133
|
+
return typeof value === "object" && value !== null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ../../packages/node-runtime/src/services/people/relationships/time-range.ts
|
|
137
|
+
var SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
|
|
138
|
+
var DEFAULT_PEOPLE_RELATIONSHIPS_TIME_RANGE_PRESET = "1y";
|
|
139
|
+
var YEARS_BY_PRESET = {
|
|
140
|
+
"1y": 1,
|
|
141
|
+
"2y": 2,
|
|
142
|
+
"3y": 3,
|
|
143
|
+
"5y": 5
|
|
144
|
+
};
|
|
145
|
+
function normalizePeopleRelationshipsTimeRangePreset(value) {
|
|
146
|
+
return CONTACTS_TIME_RANGE_PRESETS.includes(value) ? value : DEFAULT_PEOPLE_RELATIONSHIPS_TIME_RANGE_PRESET;
|
|
147
|
+
}
|
|
148
|
+
function resolvePeopleRelationshipsTimeRange(presetInput, anchorTs) {
|
|
149
|
+
const preset = normalizePeopleRelationshipsTimeRangePreset(presetInput);
|
|
150
|
+
const normalizedAnchor = typeof anchorTs === "number" ? anchorTs : null;
|
|
151
|
+
const years = YEARS_BY_PRESET[preset];
|
|
152
|
+
return {
|
|
153
|
+
preset,
|
|
154
|
+
anchorTs: normalizedAnchor,
|
|
155
|
+
startTs: normalizedAnchor !== null && years ? normalizedAnchor - years * SECONDS_PER_YEAR : null
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ../../packages/node-runtime/src/services/people/relationships/compute.ts
|
|
160
|
+
var PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION = "people-relationships-v2";
|
|
161
|
+
var PRIVATE_COMMUNITY_ID = "private";
|
|
162
|
+
var OWNER_KEY_PREFIX = "owner";
|
|
163
|
+
var REPLY_WEIGHT = 3;
|
|
164
|
+
var CO_OCCURRENCE_COUNT_WEIGHT = 0.05;
|
|
165
|
+
var EDGE_RECENCY_HALF_LIFE_SECONDS = 120 * 24 * 60 * 60;
|
|
166
|
+
var EDGE_RECENCY_FLOOR = 0.1;
|
|
167
|
+
var MIN_GROUP_FRIENDS_FOR_PANORAMA = 3;
|
|
168
|
+
var MIN_GROUP_FRIEND_RATIO_FOR_PANORAMA = 0.2;
|
|
169
|
+
var MIN_OWNER_CONNECTED_MEMBERS_FOR_PANORAMA = 3;
|
|
170
|
+
var MIN_OWNER_GROUP_MESSAGES_FOR_PANORAMA = 20;
|
|
171
|
+
var MIN_OWNER_GROUP_MESSAGE_RATIO_FOR_PANORAMA = 0.05;
|
|
172
|
+
var MIN_OWNER_GROUP_MESSAGES_FOR_RATIO = 8;
|
|
173
|
+
var SMALL_GROUP_MEMBER_LIMIT_FOR_FRIEND_EDGE = 12;
|
|
174
|
+
var MAX_OWNER_CONNECTED_NON_FRIENDS_PER_GROUP = 30;
|
|
175
|
+
var MAX_FRIEND_CONNECTED_NON_FRIENDS_PER_GROUP = 30;
|
|
176
|
+
var MAX_HIGH_SIGNAL_NON_FRIENDS_PER_GROUP = 10;
|
|
177
|
+
var MAX_MEMBERS_PER_GROUP_FOR_PANORAMA = 80;
|
|
178
|
+
var COMMUNITY_COLORS = [
|
|
179
|
+
"#7dd3fc",
|
|
180
|
+
"#f0abfc",
|
|
181
|
+
"#facc15",
|
|
182
|
+
"#34d399",
|
|
183
|
+
"#fb7185",
|
|
184
|
+
"#a78bfa",
|
|
185
|
+
"#2dd4bf",
|
|
186
|
+
"#f97316",
|
|
187
|
+
"#60a5fa",
|
|
188
|
+
"#e879f9"
|
|
189
|
+
];
|
|
190
|
+
function computePeopleRelationshipsSnapshot(options) {
|
|
191
|
+
const startedAt = options.now?.() ?? Date.now();
|
|
192
|
+
const sessionIds = options.adapter.listSessionIds();
|
|
193
|
+
const limits = normalizeLimits(options.limits);
|
|
194
|
+
const factsCache = options.factsCacheDir ? createFactsCacheContext(options.factsCacheDir) : null;
|
|
195
|
+
const timeRange = resolvePeopleRelationshipsTimeRange(
|
|
196
|
+
options.timeRangePreset,
|
|
197
|
+
findGlobalLatestMessageTs(options.adapter, sessionIds, factsCache)
|
|
198
|
+
);
|
|
199
|
+
const result = computePeopleRelationships({
|
|
200
|
+
adapter: options.adapter,
|
|
201
|
+
sessionIds,
|
|
202
|
+
timeRange,
|
|
203
|
+
factsCache,
|
|
204
|
+
limits,
|
|
205
|
+
onProgress: options.onProgress
|
|
206
|
+
});
|
|
207
|
+
const finishedAt = options.now?.() ?? Date.now();
|
|
208
|
+
if (factsCache) {
|
|
209
|
+
appLogger.info("people-relationships", "people relationships session facts cache summary", factsCache.stats);
|
|
210
|
+
}
|
|
211
|
+
appLogger.info("people-relationships", "people relationships panorama filter summary", {
|
|
212
|
+
includedGroupSessions: result.diagnostics.panoramaIncludedGroupSessions,
|
|
213
|
+
excludedLowValueGroupSessions: result.diagnostics.panoramaExcludedLowValueGroupSessions,
|
|
214
|
+
includedGroupMembers: result.diagnostics.panoramaIncludedGroupMembers,
|
|
215
|
+
excludedGroupMembers: result.diagnostics.panoramaExcludedGroupMembers,
|
|
216
|
+
candidateNodes: result.diagnostics.panoramaCandidateNodes,
|
|
217
|
+
reasons: result.diagnostics.panoramaGroupInclusionReasons
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
...result,
|
|
221
|
+
algorithmVersion: PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION,
|
|
222
|
+
signature: options.signature,
|
|
223
|
+
timeRange,
|
|
224
|
+
computedAt: finishedAt,
|
|
225
|
+
limits,
|
|
226
|
+
workerStats: {
|
|
227
|
+
durationMs: Math.max(0, finishedAt - startedAt),
|
|
228
|
+
totalSessions: sessionIds.length,
|
|
229
|
+
processedSessions: sessionIds.length,
|
|
230
|
+
skippedFailedSessions: result.diagnostics.skippedFailedSessions
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function buildPeopleRelationshipsNeighborhoodGraph(snapshot, contactKey, limits = {}) {
|
|
235
|
+
const normalizedLimits = normalizeLimits({ ...snapshot.limits, ...limits });
|
|
236
|
+
const center = snapshot.nodes.find((node) => node.key === contactKey);
|
|
237
|
+
if (!center) return { nodes: [], edges: [], communities: [] };
|
|
238
|
+
const relatedEdges = sortEdgesForDisplay(
|
|
239
|
+
snapshot.edges.filter((edge) => edge.sourceKey === contactKey || edge.targetKey === contactKey)
|
|
240
|
+
);
|
|
241
|
+
const { directEdges, nodeKeys } = selectNeighborhoodDirectEdges(relatedEdges, contactKey, normalizedLimits);
|
|
242
|
+
const directEdgeIds = new Set(relatedEdges.map((edge) => edge.id));
|
|
243
|
+
const secondaryEdgeLimit = Math.max(0, normalizedLimits.neighborhoodEdgeLimit - directEdges.length);
|
|
244
|
+
const secondaryEdges = sortEdgesForDisplay(
|
|
245
|
+
snapshot.edges.filter(
|
|
246
|
+
(edge) => !directEdgeIds.has(edge.id) && nodeKeys.has(edge.sourceKey) && nodeKeys.has(edge.targetKey)
|
|
247
|
+
)
|
|
248
|
+
).slice(0, secondaryEdgeLimit);
|
|
249
|
+
const edges = sortEdgesForDisplay([...directEdges, ...secondaryEdges]);
|
|
250
|
+
const nodes = layoutNeighborhoodNodes(
|
|
251
|
+
snapshot.nodes.filter((node) => nodeKeys.has(node.key)),
|
|
252
|
+
directEdges,
|
|
253
|
+
contactKey
|
|
254
|
+
).sort(compareNodes);
|
|
255
|
+
return {
|
|
256
|
+
nodes,
|
|
257
|
+
edges,
|
|
258
|
+
communities: filterNeighborhoodCommunities(snapshot.communities, directEdges)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function selectNeighborhoodDirectEdges(relatedEdges, contactKey, limits) {
|
|
262
|
+
const nodeKeys = /* @__PURE__ */ new Set([contactKey]);
|
|
263
|
+
const directEdges = [];
|
|
264
|
+
for (const edge of relatedEdges) {
|
|
265
|
+
if (directEdges.length >= limits.neighborhoodEdgeLimit) break;
|
|
266
|
+
const neighborKey = getNeighborhoodNeighborKey(edge, contactKey);
|
|
267
|
+
if (!neighborKey) continue;
|
|
268
|
+
if (!nodeKeys.has(neighborKey)) {
|
|
269
|
+
if (nodeKeys.size >= limits.neighborhoodNodeLimit) continue;
|
|
270
|
+
nodeKeys.add(neighborKey);
|
|
271
|
+
}
|
|
272
|
+
directEdges.push(edge);
|
|
273
|
+
}
|
|
274
|
+
return { directEdges, nodeKeys };
|
|
275
|
+
}
|
|
276
|
+
function getNeighborhoodNeighborKey(edge, contactKey) {
|
|
277
|
+
if (edge.sourceKey === contactKey) return edge.targetKey;
|
|
278
|
+
if (edge.targetKey === contactKey) return edge.sourceKey;
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function layoutNeighborhoodNodes(nodes, relatedEdges, contactKey) {
|
|
282
|
+
const center = nodes.find((node) => node.key === contactKey);
|
|
283
|
+
if (!center) return nodes.map((node) => ({ ...node }));
|
|
284
|
+
const directEdgeByNodeKey = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const edge of relatedEdges) {
|
|
286
|
+
const neighborKey = edge.sourceKey === contactKey ? edge.targetKey : edge.targetKey === contactKey ? edge.sourceKey : null;
|
|
287
|
+
if (neighborKey) directEdgeByNodeKey.set(neighborKey, edge);
|
|
288
|
+
}
|
|
289
|
+
const maxDirectWeight = Math.max(...[...directEdgeByNodeKey.values()].map((edge) => edge.weight), 1);
|
|
290
|
+
const communityGroups = /* @__PURE__ */ new Map();
|
|
291
|
+
for (const node of nodes) {
|
|
292
|
+
if (node.key === contactKey) continue;
|
|
293
|
+
const group = communityGroups.get(node.communityId) ?? [];
|
|
294
|
+
group.push(node);
|
|
295
|
+
communityGroups.set(node.communityId, group);
|
|
296
|
+
}
|
|
297
|
+
const sortedCommunityGroups = [...communityGroups.entries()].sort((a, b) => {
|
|
298
|
+
if (a[0] === center.communityId) return -1;
|
|
299
|
+
if (b[0] === center.communityId) return 1;
|
|
300
|
+
const weightA = sumNeighborhoodCommunityWeight(a[1], directEdgeByNodeKey);
|
|
301
|
+
const weightB = sumNeighborhoodCommunityWeight(b[1], directEdgeByNodeKey);
|
|
302
|
+
return weightB - weightA || a[0].localeCompare(b[0]);
|
|
303
|
+
});
|
|
304
|
+
const localNodes = [{ ...center, x: 0, y: 0 }];
|
|
305
|
+
for (const [communityIndex, [communityId, communityNodes]] of sortedCommunityGroups.entries()) {
|
|
306
|
+
const baseAngle = neighborhoodCommunityAngle(communityIndex, communityId, center.communityId);
|
|
307
|
+
communityNodes.sort((a, b) => {
|
|
308
|
+
const edgeA = directEdgeByNodeKey.get(a.key)?.weight ?? 0;
|
|
309
|
+
const edgeB = directEdgeByNodeKey.get(b.key)?.weight ?? 0;
|
|
310
|
+
return edgeB - edgeA || compareNodes(a, b);
|
|
311
|
+
});
|
|
312
|
+
for (const [index, node] of communityNodes.entries()) {
|
|
313
|
+
const angle = neighborhoodNodeAngle(baseAngle, communityId, node.key, index, communityNodes.length);
|
|
314
|
+
const radius = neighborhoodNodeRadius(
|
|
315
|
+
node,
|
|
316
|
+
center,
|
|
317
|
+
directEdgeByNodeKey.get(node.key),
|
|
318
|
+
maxDirectWeight,
|
|
319
|
+
communityNodes.length
|
|
320
|
+
);
|
|
321
|
+
localNodes.push({
|
|
322
|
+
...node,
|
|
323
|
+
x: roundNum(Math.cos(angle) * radius, 2),
|
|
324
|
+
y: roundNum(Math.sin(angle) * radius, 2)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return localNodes;
|
|
329
|
+
}
|
|
330
|
+
function sumNeighborhoodCommunityWeight(nodes, directEdgeByNodeKey) {
|
|
331
|
+
return nodes.reduce((sum, node) => sum + (directEdgeByNodeKey.get(node.key)?.weight ?? 0), 0);
|
|
332
|
+
}
|
|
333
|
+
function neighborhoodCommunityAngle(index, communityId, centerCommunityId) {
|
|
334
|
+
if (communityId === centerCommunityId) return -Math.PI / 2;
|
|
335
|
+
return index * 2.399963229728653;
|
|
336
|
+
}
|
|
337
|
+
function neighborhoodNodeAngle(baseAngle, communityId, nodeKey, index, communitySize) {
|
|
338
|
+
const seedOffset = (stableHash(`${communityId}:${nodeKey}:neighborhood-angle`) % 1e3 / 1e3 - 0.5) * 0.12;
|
|
339
|
+
const centeredIndex = index - (communitySize - 1) / 2;
|
|
340
|
+
const spacing = communitySize <= 4 ? 0.42 : communitySize <= 10 ? 0.24 : 0.14;
|
|
341
|
+
const maxSpread = communitySize <= 4 ? 0.72 : communitySize <= 20 ? 1.08 : 1.32;
|
|
342
|
+
return baseAngle + clamp(centeredIndex * spacing + seedOffset, -maxSpread, maxSpread);
|
|
343
|
+
}
|
|
344
|
+
function neighborhoodNodeRadius(node, center, directEdge, maxDirectWeight, communitySize) {
|
|
345
|
+
const directStrength = directEdge ? Math.min(1, directEdge.weight / maxDirectWeight) : 0;
|
|
346
|
+
const sameCommunityBonus = node.communityId === center.communityId ? 110 : 0;
|
|
347
|
+
const smallCommunityBonus = node.communityId === center.communityId && communitySize <= 6 ? 80 : 0;
|
|
348
|
+
const friendBonus = node.pool === "friend" ? 70 : 0;
|
|
349
|
+
const scoreBonus = Math.min(90, Math.sqrt(Math.max(0, Math.min(1, node.score))) * 90);
|
|
350
|
+
const rankPenalty = Math.sqrt(Math.max(0, node.rank - center.rank)) * 8;
|
|
351
|
+
return roundNum(
|
|
352
|
+
clamp(
|
|
353
|
+
260 + (1 - directStrength) * 560 + rankPenalty - sameCommunityBonus - smallCommunityBonus - friendBonus - scoreBonus,
|
|
354
|
+
120,
|
|
355
|
+
1100
|
|
356
|
+
),
|
|
357
|
+
2
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
function computePeopleRelationships(options) {
|
|
361
|
+
const diagnostics = createEmptyDiagnostics();
|
|
362
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
363
|
+
const edges = /* @__PURE__ */ new Map();
|
|
364
|
+
const groupContributions = [];
|
|
365
|
+
const communityLabels = /* @__PURE__ */ new Map([[PRIVATE_COMMUNITY_ID, "Private contacts"]]);
|
|
366
|
+
let processedSessions = 0;
|
|
367
|
+
for (const sessionId of options.sessionIds) {
|
|
368
|
+
options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId });
|
|
369
|
+
try {
|
|
370
|
+
const facts = getSessionFacts(options.adapter, sessionId, options.timeRange, options.factsCache);
|
|
371
|
+
applySessionFacts(nodes, edges, groupContributions, communityLabels, diagnostics, sessionId, facts);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
diagnostics.skippedFailedSessions++;
|
|
374
|
+
appLogger.error("people-relationships", `failed to process people relationship session: ${sessionId}`, error);
|
|
375
|
+
} finally {
|
|
376
|
+
processedSessions++;
|
|
377
|
+
options.onProgress?.({ processedSessions, totalSessions: options.sessionIds.length, currentSessionId: sessionId });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
applyPanoramaContributions([...nodes.values()], edges, groupContributions, diagnostics);
|
|
381
|
+
return buildGraph(
|
|
382
|
+
[...nodes.values()],
|
|
383
|
+
[...edges.values()],
|
|
384
|
+
communityLabels,
|
|
385
|
+
groupContributions,
|
|
386
|
+
options.limits,
|
|
387
|
+
diagnostics
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
function findGlobalLatestMessageTs(adapter, sessionIds, factsCache) {
|
|
391
|
+
let latest = null;
|
|
392
|
+
for (const sessionId of sessionIds) {
|
|
393
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
394
|
+
const cached = readLatestFacts(sessionId, factsCache, dbVersion);
|
|
395
|
+
if (cached) {
|
|
396
|
+
if (cached.latestMessageTs !== null) latest = Math.max(latest ?? 0, cached.latestMessageTs);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const db = adapter.openReadonly(sessionId);
|
|
401
|
+
const ts = db && isChatSessionDb(db) ? getLatestContactMessageTs(db) : null;
|
|
402
|
+
writeLatestFacts(adapter, sessionId, factsCache, { latestMessageTs: ts }, dbVersion);
|
|
403
|
+
if (ts !== null) latest = Math.max(latest ?? 0, ts);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
appLogger.error(
|
|
406
|
+
"people-relationships",
|
|
407
|
+
`failed to inspect people relationship session range: ${sessionId}`,
|
|
408
|
+
error
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return latest;
|
|
413
|
+
}
|
|
414
|
+
function getSessionFacts(adapter, sessionId, timeRange, factsCache) {
|
|
415
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
416
|
+
const cached = readSessionFacts(sessionId, timeRange, factsCache, dbVersion);
|
|
417
|
+
if (cached) return cached;
|
|
418
|
+
const facts = computeSessionFacts(adapter, sessionId, timeRange);
|
|
419
|
+
writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, dbVersion);
|
|
420
|
+
return facts;
|
|
421
|
+
}
|
|
422
|
+
function computeSessionFacts(adapter, sessionId, timeRange) {
|
|
423
|
+
const db = adapter.openReadonly(sessionId);
|
|
424
|
+
if (!db || !isChatSessionDb(db)) return { kind: "not_chat_db", latestMessageTs: null };
|
|
425
|
+
const latestMessageTs = getLatestContactMessageTs(db);
|
|
426
|
+
const meta = getSessionMeta(db);
|
|
427
|
+
if (!meta) return { kind: "missing_meta", latestMessageTs };
|
|
428
|
+
if (meta.type !== "private" /* PRIVATE */ && meta.type !== "group" /* GROUP */)
|
|
429
|
+
return { kind: "unsupported_type", latestMessageTs };
|
|
430
|
+
const cachedMeta = toPeopleRelationshipsSessionMetaFacts(meta);
|
|
431
|
+
if (!meta.ownerId?.trim()) return { kind: "missing_owner", meta: cachedMeta, latestMessageTs };
|
|
432
|
+
const owner = resolveOwnerMember(db);
|
|
433
|
+
if (!owner) return { kind: "unresolved_owner", meta: cachedMeta, latestMessageTs };
|
|
434
|
+
const cachedMetaWithOwner = toPeopleRelationshipsSessionMetaFacts(meta, owner);
|
|
435
|
+
if (meta.type === "private" /* PRIVATE */) {
|
|
436
|
+
const facts = getPrivateContactFacts(db, owner.id, { startTs: timeRange.startTs });
|
|
437
|
+
if (facts.type === "missing") return { kind: "private_missing", meta: cachedMetaWithOwner, latestMessageTs };
|
|
438
|
+
if (facts.type === "ambiguous") return { kind: "private_ambiguous", meta: cachedMetaWithOwner, latestMessageTs };
|
|
439
|
+
return { kind: "private", meta: cachedMetaWithOwner, latestMessageTs, facts };
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
kind: "group",
|
|
443
|
+
meta: cachedMetaWithOwner,
|
|
444
|
+
latestMessageTs,
|
|
445
|
+
facts: {
|
|
446
|
+
...getGroupRelationshipGraphFacts(db, owner.id, { startTs: timeRange.startTs }),
|
|
447
|
+
ownerEdges: getGroupContactFacts(db, owner.id, { startTs: timeRange.startTs })
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function applySessionFacts(nodes, edges, groupContributions, communityLabels, diagnostics, sessionId, sessionFacts) {
|
|
452
|
+
switch (sessionFacts.kind) {
|
|
453
|
+
case "missing_owner":
|
|
454
|
+
diagnostics.skippedMissingOwnerSessions++;
|
|
455
|
+
return;
|
|
456
|
+
case "unresolved_owner":
|
|
457
|
+
diagnostics.skippedUnresolvedOwnerSessions++;
|
|
458
|
+
return;
|
|
459
|
+
case "private_ambiguous":
|
|
460
|
+
diagnostics.skippedAmbiguousPrivateSessions++;
|
|
461
|
+
return;
|
|
462
|
+
case "private":
|
|
463
|
+
diagnostics.processedPrivateSessions++;
|
|
464
|
+
applyPrivateFacts(nodes, edges, sessionId, sessionFacts.meta, sessionFacts.facts);
|
|
465
|
+
return;
|
|
466
|
+
case "group":
|
|
467
|
+
diagnostics.processedGroupSessions++;
|
|
468
|
+
applyGroupFacts(
|
|
469
|
+
nodes,
|
|
470
|
+
edges,
|
|
471
|
+
groupContributions,
|
|
472
|
+
communityLabels,
|
|
473
|
+
sessionId,
|
|
474
|
+
sessionFacts.meta,
|
|
475
|
+
sessionFacts.facts
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
default:
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function applyPrivateFacts(nodes, edges, sessionId, meta, facts) {
|
|
483
|
+
if (facts.privateMessageCount <= 0) return;
|
|
484
|
+
const ownerNode = getOrCreateOwnerNode(nodes, meta);
|
|
485
|
+
const node = getOrCreateNode(nodes, sessionId, meta, facts.contact);
|
|
486
|
+
node.isFriend = true;
|
|
487
|
+
node.friendSource = "private";
|
|
488
|
+
node.panoramaCandidate = true;
|
|
489
|
+
node.privateMessageCount += facts.privateMessageCount;
|
|
490
|
+
for (const month of facts.activeMonths) node.activePrivateMonths.add(month);
|
|
491
|
+
node.communityWeights.set(PRIVATE_COMMUNITY_ID, (node.communityWeights.get(PRIVATE_COMMUNITY_ID) ?? 0) + 1);
|
|
492
|
+
updateLastInteraction(node, facts.lastMessageTs);
|
|
493
|
+
if (!ownerNode) return;
|
|
494
|
+
ownerNode.isFriend = true;
|
|
495
|
+
ownerNode.panoramaCandidate = true;
|
|
496
|
+
ownerNode.privateMessageCount += facts.privateMessageCount;
|
|
497
|
+
for (const month of facts.activeMonths) ownerNode.activePrivateMonths.add(month);
|
|
498
|
+
ownerNode.communityWeights.set(PRIVATE_COMMUNITY_ID, (ownerNode.communityWeights.get(PRIVATE_COMMUNITY_ID) ?? 0) + 1);
|
|
499
|
+
updateLastInteraction(ownerNode, facts.lastMessageTs);
|
|
500
|
+
applyOwnerContactEdge(edges, ownerNode, node, {
|
|
501
|
+
coOccurrenceCount: facts.privateMessageCount,
|
|
502
|
+
coOccurrenceRawScore: facts.privateMessageCount * 1.8,
|
|
503
|
+
replyInteractionCount: 0,
|
|
504
|
+
repliesFromOwnerToContact: 0,
|
|
505
|
+
repliesFromContactToOwner: 0,
|
|
506
|
+
lastInteractionTs: facts.lastMessageTs,
|
|
507
|
+
sessionId,
|
|
508
|
+
panoramaEligible: true
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function applyGroupFacts(nodes, edges, groupContributions, communityLabels, sessionId, meta, facts) {
|
|
512
|
+
const communityId = `group:${sessionId}`;
|
|
513
|
+
communityLabels.set(communityId, meta.name);
|
|
514
|
+
const ownerNode = getOrCreateOwnerNode(nodes, meta);
|
|
515
|
+
const contribution = {
|
|
516
|
+
sessionId,
|
|
517
|
+
memberKeys: /* @__PURE__ */ new Set(),
|
|
518
|
+
memberMessageCounts: /* @__PURE__ */ new Map(),
|
|
519
|
+
ownerConnectedKeys: /* @__PURE__ */ new Set(),
|
|
520
|
+
ownerEdgeScores: /* @__PURE__ */ new Map(),
|
|
521
|
+
relationshipEdgeIds: /* @__PURE__ */ new Set(),
|
|
522
|
+
relationshipEdgeEndpoints: /* @__PURE__ */ new Map(),
|
|
523
|
+
relationshipEdgeScores: /* @__PURE__ */ new Map(),
|
|
524
|
+
friendConnectedScores: /* @__PURE__ */ new Map(),
|
|
525
|
+
ownerMessageCount: facts.ownerMessageCount
|
|
526
|
+
};
|
|
527
|
+
for (const member of facts.members) {
|
|
528
|
+
const node = getOrCreateNode(nodes, sessionId, meta, member.contact);
|
|
529
|
+
contribution.memberKeys.add(node.key);
|
|
530
|
+
contribution.memberMessageCounts.set(node.key, member.messageCount);
|
|
531
|
+
node.groupMessageCount += member.messageCount;
|
|
532
|
+
node.commonGroupSessionIds.add(sessionId);
|
|
533
|
+
node.communityWeights.set(
|
|
534
|
+
communityId,
|
|
535
|
+
(node.communityWeights.get(communityId) ?? 0) + Math.max(1, member.messageCount)
|
|
536
|
+
);
|
|
537
|
+
updateLastInteraction(node, member.lastMessageTs);
|
|
538
|
+
}
|
|
539
|
+
if (ownerNode) {
|
|
540
|
+
ownerNode.groupMessageCount += facts.ownerMessageCount;
|
|
541
|
+
for (const fact of facts.ownerEdges) {
|
|
542
|
+
if (fact.coOccurrenceCount <= 0 && fact.replyInteractionCount <= 0) continue;
|
|
543
|
+
const contactNode = getOrCreateNode(nodes, sessionId, meta, fact.contact);
|
|
544
|
+
const ownerEdgeScore = computeRawEdgeWeight({
|
|
545
|
+
coOccurrenceCount: fact.coOccurrenceCount,
|
|
546
|
+
coOccurrenceRawScore: fact.coOccurrenceRawScore,
|
|
547
|
+
replyInteractionCount: fact.replyInteractionCount
|
|
548
|
+
});
|
|
549
|
+
contribution.memberKeys.add(contactNode.key);
|
|
550
|
+
contribution.ownerConnectedKeys.add(contactNode.key);
|
|
551
|
+
contribution.ownerEdgeScores.set(
|
|
552
|
+
contactNode.key,
|
|
553
|
+
(contribution.ownerEdgeScores.get(contactNode.key) ?? 0) + ownerEdgeScore
|
|
554
|
+
);
|
|
555
|
+
ownerNode.commonGroupSessionIds.add(sessionId);
|
|
556
|
+
ownerNode.communityWeights.set(communityId, (ownerNode.communityWeights.get(communityId) ?? 0) + 1);
|
|
557
|
+
updateLastInteraction(ownerNode, fact.lastInteractionTs);
|
|
558
|
+
applyOwnerContactEdge(edges, ownerNode, contactNode, {
|
|
559
|
+
coOccurrenceCount: fact.coOccurrenceCount,
|
|
560
|
+
coOccurrenceRawScore: fact.coOccurrenceRawScore,
|
|
561
|
+
replyInteractionCount: fact.replyInteractionCount,
|
|
562
|
+
repliesFromOwnerToContact: fact.repliesFromOwnerToContact,
|
|
563
|
+
repliesFromContactToOwner: fact.repliesFromContactToOwner,
|
|
564
|
+
lastInteractionTs: fact.lastInteractionTs,
|
|
565
|
+
sessionId
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
for (const fact of facts.edges) {
|
|
570
|
+
const sourceNode = getOrCreateNode(nodes, sessionId, meta, fact.source);
|
|
571
|
+
const targetNode = getOrCreateNode(nodes, sessionId, meta, fact.target);
|
|
572
|
+
const sourceKey = sourceNode.key < targetNode.key ? sourceNode.key : targetNode.key;
|
|
573
|
+
const targetKey = sourceNode.key < targetNode.key ? targetNode.key : sourceNode.key;
|
|
574
|
+
const edge = getOrCreateEdge(edges, sourceKey, targetKey);
|
|
575
|
+
const currentEdgeId = edgeId(sourceKey, targetKey);
|
|
576
|
+
const relationshipEdgeScore = computeRawEdgeWeight(fact);
|
|
577
|
+
contribution.memberKeys.add(sourceNode.key);
|
|
578
|
+
contribution.memberKeys.add(targetNode.key);
|
|
579
|
+
contribution.relationshipEdgeIds.add(currentEdgeId);
|
|
580
|
+
contribution.relationshipEdgeEndpoints.set(currentEdgeId, [sourceKey, targetKey]);
|
|
581
|
+
contribution.relationshipEdgeScores.set(
|
|
582
|
+
currentEdgeId,
|
|
583
|
+
(contribution.relationshipEdgeScores.get(currentEdgeId) ?? 0) + relationshipEdgeScore
|
|
584
|
+
);
|
|
585
|
+
edge.coOccurrenceCount += fact.coOccurrenceCount;
|
|
586
|
+
edge.coOccurrenceRawScore += fact.coOccurrenceRawScore;
|
|
587
|
+
edge.replyInteractionCount += fact.replyInteractionCount;
|
|
588
|
+
if (sourceNode.key === sourceKey) {
|
|
589
|
+
edge.repliesFromSourceToTarget += fact.repliesFromSourceToTarget;
|
|
590
|
+
edge.repliesFromTargetToSource += fact.repliesFromTargetToSource;
|
|
591
|
+
} else {
|
|
592
|
+
edge.repliesFromSourceToTarget += fact.repliesFromTargetToSource;
|
|
593
|
+
edge.repliesFromTargetToSource += fact.repliesFromSourceToTarget;
|
|
594
|
+
}
|
|
595
|
+
edge.sourceSessionIds.add(sessionId);
|
|
596
|
+
edge.lastInteractionTs = maxNullableTs(edge.lastInteractionTs, fact.lastInteractionTs);
|
|
597
|
+
sourceNode.commonGroupSessionIds.add(sessionId);
|
|
598
|
+
targetNode.commonGroupSessionIds.add(sessionId);
|
|
599
|
+
sourceNode.communityWeights.set(communityId, (sourceNode.communityWeights.get(communityId) ?? 0) + 1);
|
|
600
|
+
targetNode.communityWeights.set(communityId, (targetNode.communityWeights.get(communityId) ?? 0) + 1);
|
|
601
|
+
updateLastInteraction(sourceNode, fact.lastInteractionTs);
|
|
602
|
+
updateLastInteraction(targetNode, fact.lastInteractionTs);
|
|
603
|
+
}
|
|
604
|
+
groupContributions.push(contribution);
|
|
605
|
+
}
|
|
606
|
+
function buildGraph(nodeAccumulators, edgeAccumulators, communityLabels, groupContributions, limits, baseDiagnostics) {
|
|
607
|
+
const edgeWeights = /* @__PURE__ */ new Map();
|
|
608
|
+
for (const edge of edgeAccumulators) {
|
|
609
|
+
const weight = computeEdgeWeight(edge);
|
|
610
|
+
edgeWeights.set(edgeId(edge.sourceKey, edge.targetKey), weight);
|
|
611
|
+
}
|
|
612
|
+
for (const edge of edgeAccumulators) {
|
|
613
|
+
const weight = edgeWeights.get(edgeId(edge.sourceKey, edge.targetKey)) ?? 0;
|
|
614
|
+
const source = nodeAccumulators.find((node) => node.key === edge.sourceKey);
|
|
615
|
+
const target = nodeAccumulators.find((node) => node.key === edge.targetKey);
|
|
616
|
+
if (source) source.edgeWeight += weight;
|
|
617
|
+
if (target) target.edgeWeight += weight;
|
|
618
|
+
}
|
|
619
|
+
const nodeScores = computeContactPriorityScores(nodeAccumulators);
|
|
620
|
+
const rankedNodes = nodeAccumulators.map((node) => ({
|
|
621
|
+
node,
|
|
622
|
+
score: nodeScores.get(node) ?? 0,
|
|
623
|
+
relationshipActivityScore: computeRelationshipActivityScore(node)
|
|
624
|
+
})).sort(compareRankedNodes);
|
|
625
|
+
const panoramaCandidateKeys = new Set(
|
|
626
|
+
rankedNodes.filter((item) => item.node.panoramaCandidate).map((item) => item.node.key)
|
|
627
|
+
);
|
|
628
|
+
const panoramaEligibleEdgeIds = new Set(
|
|
629
|
+
edgeAccumulators.filter((edge) => edge.panoramaEligible).map((edge) => edgeId(edge.sourceKey, edge.targetKey))
|
|
630
|
+
);
|
|
631
|
+
const maxScore = Math.max(...rankedNodes.map((item) => item.score), 1);
|
|
632
|
+
const nodeByKey = /* @__PURE__ */ new Map();
|
|
633
|
+
rankedNodes.forEach((item, index) => {
|
|
634
|
+
const communityId = pickCommunityId(item.node);
|
|
635
|
+
const color = colorForCommunity(communityId);
|
|
636
|
+
nodeByKey.set(item.node.key, {
|
|
637
|
+
key: item.node.key,
|
|
638
|
+
kind: item.node.kind,
|
|
639
|
+
platform: item.node.platform,
|
|
640
|
+
platformId: item.node.platformId,
|
|
641
|
+
sessionScoped: item.node.sessionScoped,
|
|
642
|
+
sessionId: item.node.sessionId,
|
|
643
|
+
displayName: item.node.displayName,
|
|
644
|
+
aliases: [...item.node.aliases].filter((alias) => alias !== item.node.displayName),
|
|
645
|
+
avatar: item.node.avatar,
|
|
646
|
+
pool: item.node.isFriend ? "friend" : "non_friend",
|
|
647
|
+
friendSource: item.node.friendSource,
|
|
648
|
+
score: roundNum(item.score),
|
|
649
|
+
rank: index + 1,
|
|
650
|
+
communityId,
|
|
651
|
+
x: 0,
|
|
652
|
+
y: 0,
|
|
653
|
+
size: roundNum(4 + Math.sqrt(item.score / maxScore) * 18, 2),
|
|
654
|
+
color,
|
|
655
|
+
labelVisibility: item.node.kind === "owner" || index < 30 ? 2 : index < 160 ? 1 : 0,
|
|
656
|
+
lastInteractionTs: item.node.lastInteractionTs,
|
|
657
|
+
privateMessageCount: item.node.privateMessageCount,
|
|
658
|
+
groupMessageCount: item.node.groupMessageCount,
|
|
659
|
+
commonGroupCount: item.node.commonGroupSessionIds.size,
|
|
660
|
+
searchText: [
|
|
661
|
+
item.node.displayName,
|
|
662
|
+
item.node.platformId,
|
|
663
|
+
...item.node.aliases,
|
|
664
|
+
...item.node.kind === "owner" ? ["\u6211", "me", "myself", "owner"] : []
|
|
665
|
+
].join(" ").toLowerCase()
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
const allNodes = layoutNodes([...nodeByKey.values()], communityLabels);
|
|
669
|
+
const allEdges = edgeAccumulators.map((edge) => toGraphEdge(edge, edgeWeights.get(edgeId(edge.sourceKey, edge.targetKey)) ?? 0)).filter((edge) => nodeByKey.has(edge.sourceKey) && nodeByKey.has(edge.targetKey)).sort(compareEdges);
|
|
670
|
+
const communities = buildCommunities(allNodes, communityLabels, groupContributions);
|
|
671
|
+
const coreNodeKeys = new Set(
|
|
672
|
+
allNodes.filter((node) => panoramaCandidateKeys.has(node.key)).slice(0, limits.coreNodeLimit).map((node) => node.key)
|
|
673
|
+
);
|
|
674
|
+
const coreEdges = cropEdges(
|
|
675
|
+
allEdges.filter(
|
|
676
|
+
(edge) => coreNodeKeys.has(edge.sourceKey) && coreNodeKeys.has(edge.targetKey) && panoramaEligibleEdgeIds.has(edge.id)
|
|
677
|
+
),
|
|
678
|
+
limits
|
|
679
|
+
);
|
|
680
|
+
const graphNodes = allNodes.filter((node) => coreNodeKeys.has(node.key));
|
|
681
|
+
const graph = {
|
|
682
|
+
nodes: graphNodes,
|
|
683
|
+
edges: coreEdges,
|
|
684
|
+
communities: filterCommunities(communities, graphNodes)
|
|
685
|
+
};
|
|
686
|
+
const diagnostics = {
|
|
687
|
+
...baseDiagnostics,
|
|
688
|
+
totalNodes: allNodes.length,
|
|
689
|
+
totalEdges: allEdges.length,
|
|
690
|
+
panoramaCandidateNodes: panoramaCandidateKeys.size,
|
|
691
|
+
coreNodeCount: graph.nodes.length,
|
|
692
|
+
coreEdgeCount: graph.edges.length
|
|
693
|
+
};
|
|
694
|
+
return { nodes: allNodes, edges: allEdges, communities, graph, diagnostics };
|
|
695
|
+
}
|
|
696
|
+
function cropEdges(edges, limits) {
|
|
697
|
+
const counts = /* @__PURE__ */ new Map();
|
|
698
|
+
const kept = [];
|
|
699
|
+
for (const edge of sortEdgesForDisplay(edges)) {
|
|
700
|
+
if (kept.length >= limits.coreEdgeLimit) break;
|
|
701
|
+
const sourceCount = counts.get(edge.sourceKey) ?? 0;
|
|
702
|
+
const targetCount = counts.get(edge.targetKey) ?? 0;
|
|
703
|
+
if (sourceCount >= limits.perNodeEdgeLimit || targetCount >= limits.perNodeEdgeLimit) continue;
|
|
704
|
+
kept.push(edge);
|
|
705
|
+
counts.set(edge.sourceKey, sourceCount + 1);
|
|
706
|
+
counts.set(edge.targetKey, targetCount + 1);
|
|
707
|
+
}
|
|
708
|
+
return kept;
|
|
709
|
+
}
|
|
710
|
+
function layoutNodes(nodes, communityLabels) {
|
|
711
|
+
const byCommunity = /* @__PURE__ */ new Map();
|
|
712
|
+
for (const node of nodes) {
|
|
713
|
+
const group = byCommunity.get(node.communityId) ?? [];
|
|
714
|
+
group.push(node);
|
|
715
|
+
byCommunity.set(node.communityId, group);
|
|
716
|
+
}
|
|
717
|
+
const sortedCommunities = [...byCommunity.entries()].sort((a, b) => {
|
|
718
|
+
if (a[0] === PRIVATE_COMMUNITY_ID) return -1;
|
|
719
|
+
if (b[0] === PRIVATE_COMMUNITY_ID) return 1;
|
|
720
|
+
const scoreA = a[1].reduce((sum, node) => sum + node.score, 0);
|
|
721
|
+
const scoreB = b[1].reduce((sum, node) => sum + node.score, 0);
|
|
722
|
+
return scoreB - scoreA || (communityLabels.get(a[0]) ?? a[0]).localeCompare(communityLabels.get(b[0]) ?? b[0]);
|
|
723
|
+
});
|
|
724
|
+
for (const [communityIndex, [communityId, communityNodes]] of sortedCommunities.entries()) {
|
|
725
|
+
const baseAngle = communityAngle(communityIndex, communityId);
|
|
726
|
+
communityNodes.sort(compareNodes);
|
|
727
|
+
for (const [index, node] of communityNodes.entries()) {
|
|
728
|
+
if (node.kind === "owner") {
|
|
729
|
+
node.x = 0;
|
|
730
|
+
node.y = 0;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const angle = egoNodeAngle(baseAngle, communityId, node.key, index, communityNodes.length);
|
|
734
|
+
const radius = egoNodeRadius(node, communityId, communityNodes.length);
|
|
735
|
+
node.x = roundNum(Math.cos(angle) * radius, 2);
|
|
736
|
+
node.y = roundNum(Math.sin(angle) * radius, 2);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return nodes.sort(compareNodes);
|
|
740
|
+
}
|
|
741
|
+
function communityAngle(index, communityId) {
|
|
742
|
+
if (communityId === PRIVATE_COMMUNITY_ID) return -Math.PI / 2;
|
|
743
|
+
return index * 2.399963229728653;
|
|
744
|
+
}
|
|
745
|
+
function egoNodeAngle(baseAngle, communityId, nodeKey, index, communitySize) {
|
|
746
|
+
const seedOffset = (stableHash(`${communityId}:${nodeKey}:angle`) % 1e3 / 1e3 - 0.5) * 0.16;
|
|
747
|
+
const centeredIndex = index - (communitySize - 1) / 2;
|
|
748
|
+
const spacing = communitySize <= 4 ? 0.38 : communitySize <= 12 ? 0.24 : 0.12;
|
|
749
|
+
const maxSpread = communitySize <= 4 ? 0.75 : communitySize <= 20 ? 1.1 : 1.35;
|
|
750
|
+
return baseAngle + clamp(centeredIndex * spacing + seedOffset, -maxSpread, maxSpread);
|
|
751
|
+
}
|
|
752
|
+
function egoNodeRadius(node, communityId, communitySize) {
|
|
753
|
+
const rankPenalty = Math.sqrt(Math.max(0, node.rank - 1)) * 26;
|
|
754
|
+
const scoreBonus = Math.min(260, Math.sqrt(Math.max(0, node.score)) * 22);
|
|
755
|
+
const privateBonus = node.privateMessageCount > 0 ? Math.min(220, Math.log1p(node.privateMessageCount) * 48) : 0;
|
|
756
|
+
const friendBonus = node.pool === "friend" ? 280 : 0;
|
|
757
|
+
const commonGroupBonus = Math.min(180, node.commonGroupCount * 18);
|
|
758
|
+
const groupSizePenalty = communityId.startsWith("group:") ? Math.min(1400, Math.max(0, communitySize - 6) * 22 + Math.sqrt(communitySize) * 24) : 0;
|
|
759
|
+
const baseRadius = node.pool === "friend" ? 430 : 760;
|
|
760
|
+
const minRadius = node.pool === "friend" ? 160 : 340;
|
|
761
|
+
return roundNum(
|
|
762
|
+
clamp(
|
|
763
|
+
baseRadius + rankPenalty + groupSizePenalty - scoreBonus - privateBonus - friendBonus - commonGroupBonus,
|
|
764
|
+
minRadius,
|
|
765
|
+
2600
|
|
766
|
+
),
|
|
767
|
+
2
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
function buildCommunities(nodes, communityLabels, groupContributions) {
|
|
771
|
+
const groups = /* @__PURE__ */ new Map();
|
|
772
|
+
for (const node of nodes) {
|
|
773
|
+
const group = groups.get(node.communityId) ?? [];
|
|
774
|
+
group.push(node);
|
|
775
|
+
groups.set(node.communityId, group);
|
|
776
|
+
}
|
|
777
|
+
const groupMemberCounts = new Map(groupContributions.map((item) => [`group:${item.sessionId}`, item.memberKeys.size]));
|
|
778
|
+
const communitiesById = /* @__PURE__ */ new Map();
|
|
779
|
+
for (const [id, group] of groups) {
|
|
780
|
+
const x = group.reduce((sum, node) => sum + node.x, 0) / group.length;
|
|
781
|
+
const y = group.reduce((sum, node) => sum + node.y, 0) / group.length;
|
|
782
|
+
communitiesById.set(id, {
|
|
783
|
+
id,
|
|
784
|
+
label: communityLabels.get(id) ?? id,
|
|
785
|
+
size: groupMemberCounts.get(id) ?? group.length,
|
|
786
|
+
x: roundNum(x, 2),
|
|
787
|
+
y: roundNum(y, 2),
|
|
788
|
+
color: colorForCommunity(id)
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
for (const [id, label] of communityLabels) {
|
|
792
|
+
if (communitiesById.has(id) || !id.startsWith("group:")) continue;
|
|
793
|
+
communitiesById.set(id, {
|
|
794
|
+
id,
|
|
795
|
+
label,
|
|
796
|
+
size: groupMemberCounts.get(id) ?? 0,
|
|
797
|
+
x: 0,
|
|
798
|
+
y: 0,
|
|
799
|
+
color: colorForCommunity(id)
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
return [...communitiesById.values()].sort((a, b) => b.size - a.size || a.label.localeCompare(b.label));
|
|
803
|
+
}
|
|
804
|
+
function filterNeighborhoodCommunities(communities, relatedEdges) {
|
|
805
|
+
const communityById = new Map(communities.map((community) => [community.id, community]));
|
|
806
|
+
const sourceCommunityIds = /* @__PURE__ */ new Set();
|
|
807
|
+
for (const edge of relatedEdges) {
|
|
808
|
+
for (const sessionId of edge.sourceSessionIds) sourceCommunityIds.add(`group:${sessionId}`);
|
|
809
|
+
}
|
|
810
|
+
return [...sourceCommunityIds].map((id) => communityById.get(id)).filter((community) => Boolean(community)).sort((a, b) => b.size - a.size || a.label.localeCompare(b.label));
|
|
811
|
+
}
|
|
812
|
+
function filterCommunities(communities, nodes) {
|
|
813
|
+
const ids = new Set(nodes.map((node) => node.communityId));
|
|
814
|
+
return communities.filter((community) => ids.has(community.id));
|
|
815
|
+
}
|
|
816
|
+
function applyPanoramaContributions(nodes, edges, groupContributions, diagnostics) {
|
|
817
|
+
const nodeByKey = new Map(nodes.map((node) => [node.key, node]));
|
|
818
|
+
for (const contribution of groupContributions) {
|
|
819
|
+
const decision = evaluateGroupPanoramaContribution(contribution, nodeByKey);
|
|
820
|
+
if (!decision.included) {
|
|
821
|
+
diagnostics.panoramaExcludedLowValueGroupSessions++;
|
|
822
|
+
diagnostics.panoramaExcludedGroupMembers += contribution.memberKeys.size;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
diagnostics.panoramaIncludedGroupSessions++;
|
|
826
|
+
incrementPanoramaReason(diagnostics, decision.reason);
|
|
827
|
+
for (const key of decision.selectedMemberKeys) {
|
|
828
|
+
const node = nodeByKey.get(key);
|
|
829
|
+
if (node) node.panoramaCandidate = true;
|
|
830
|
+
}
|
|
831
|
+
for (const edgeId2 of contribution.relationshipEdgeIds) {
|
|
832
|
+
const endpoints = contribution.relationshipEdgeEndpoints.get(edgeId2);
|
|
833
|
+
if (!endpoints) continue;
|
|
834
|
+
if (!decision.selectedMemberKeys.has(endpoints[0]) || !decision.selectedMemberKeys.has(endpoints[1])) continue;
|
|
835
|
+
const edge = edges.get(edgeId2);
|
|
836
|
+
if (edge) edge.panoramaEligible = true;
|
|
837
|
+
}
|
|
838
|
+
for (const key of contribution.ownerConnectedKeys) {
|
|
839
|
+
if (!decision.selectedMemberKeys.has(key)) continue;
|
|
840
|
+
const ownerKey = buildOwnerKeyForContribution(nodeByKey, key);
|
|
841
|
+
if (!ownerKey) continue;
|
|
842
|
+
const ownerEdgeId = sortedEdgeId(ownerKey, key);
|
|
843
|
+
const edge = edges.get(ownerEdgeId);
|
|
844
|
+
if (edge) edge.panoramaEligible = true;
|
|
845
|
+
}
|
|
846
|
+
diagnostics.panoramaIncludedGroupMembers += decision.selectedMemberKeys.size;
|
|
847
|
+
diagnostics.panoramaExcludedGroupMembers += Math.max(
|
|
848
|
+
0,
|
|
849
|
+
contribution.memberKeys.size - decision.selectedMemberKeys.size
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function evaluateGroupPanoramaContribution(contribution, nodeByKey) {
|
|
854
|
+
const friendKeys = [...contribution.memberKeys].filter((key) => nodeByKey.get(key)?.isFriend === true);
|
|
855
|
+
const memberCount = contribution.memberKeys.size;
|
|
856
|
+
const friendCount = friendKeys.length;
|
|
857
|
+
const friendRatio = memberCount > 0 ? friendCount / memberCount : 0;
|
|
858
|
+
const ownerConnectedCount = contribution.ownerConnectedKeys.size;
|
|
859
|
+
const totalGroupMessages = contribution.ownerMessageCount + [...contribution.memberMessageCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
860
|
+
const ownerMessageRatio = totalGroupMessages > 0 ? contribution.ownerMessageCount / totalGroupMessages : 0;
|
|
861
|
+
const friendConnectedScores = computeFriendConnectedScores(contribution, new Set(friendKeys));
|
|
862
|
+
contribution.friendConnectedScores.clear();
|
|
863
|
+
for (const [key, score] of friendConnectedScores) contribution.friendConnectedScores.set(key, score);
|
|
864
|
+
let reason = null;
|
|
865
|
+
if (friendCount === 0) {
|
|
866
|
+
reason = null;
|
|
867
|
+
} else if (friendCount >= MIN_GROUP_FRIENDS_FOR_PANORAMA) {
|
|
868
|
+
reason = "friend_count";
|
|
869
|
+
} else if (friendCount >= 2 && friendRatio >= MIN_GROUP_FRIEND_RATIO_FOR_PANORAMA) {
|
|
870
|
+
reason = "friend_ratio";
|
|
871
|
+
} else if (ownerConnectedCount >= MIN_OWNER_CONNECTED_MEMBERS_FOR_PANORAMA) {
|
|
872
|
+
reason = "owner_connected";
|
|
873
|
+
} else if (contribution.ownerMessageCount >= MIN_OWNER_GROUP_MESSAGES_FOR_PANORAMA || contribution.ownerMessageCount >= MIN_OWNER_GROUP_MESSAGES_FOR_RATIO && ownerMessageRatio >= MIN_OWNER_GROUP_MESSAGE_RATIO_FOR_PANORAMA) {
|
|
874
|
+
reason = "owner_activity";
|
|
875
|
+
} else if (memberCount <= SMALL_GROUP_MEMBER_LIMIT_FOR_FRIEND_EDGE && friendConnectedScores.size > 0) {
|
|
876
|
+
reason = "friend_connected";
|
|
877
|
+
}
|
|
878
|
+
if (!reason) return { included: false, reason: "low_value", selectedMemberKeys: /* @__PURE__ */ new Set() };
|
|
879
|
+
return {
|
|
880
|
+
included: true,
|
|
881
|
+
reason,
|
|
882
|
+
selectedMemberKeys: selectPanoramaMembers(contribution, nodeByKey, new Set(friendKeys))
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function computeFriendConnectedScores(contribution, friendKeys) {
|
|
886
|
+
const scores = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const [edgeId2, endpoints] of contribution.relationshipEdgeEndpoints) {
|
|
888
|
+
const score = contribution.relationshipEdgeScores.get(edgeId2) ?? 0;
|
|
889
|
+
const [sourceKey, targetKey] = endpoints;
|
|
890
|
+
if (friendKeys.has(sourceKey) && !friendKeys.has(targetKey)) {
|
|
891
|
+
scores.set(targetKey, (scores.get(targetKey) ?? 0) + score);
|
|
892
|
+
} else if (friendKeys.has(targetKey) && !friendKeys.has(sourceKey)) {
|
|
893
|
+
scores.set(sourceKey, (scores.get(sourceKey) ?? 0) + score);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return scores;
|
|
897
|
+
}
|
|
898
|
+
function selectPanoramaMembers(contribution, nodeByKey, friendKeys) {
|
|
899
|
+
const selected = new Set(friendKeys);
|
|
900
|
+
const remainingLimit = () => Math.max(0, MAX_MEMBERS_PER_GROUP_FOR_PANORAMA - selected.size);
|
|
901
|
+
addTopScoredKeys(selected, contribution.ownerEdgeScores, MAX_OWNER_CONNECTED_NON_FRIENDS_PER_GROUP, remainingLimit());
|
|
902
|
+
addTopScoredKeys(
|
|
903
|
+
selected,
|
|
904
|
+
contribution.friendConnectedScores,
|
|
905
|
+
MAX_FRIEND_CONNECTED_NON_FRIENDS_PER_GROUP,
|
|
906
|
+
remainingLimit()
|
|
907
|
+
);
|
|
908
|
+
addTopScoredKeys(
|
|
909
|
+
selected,
|
|
910
|
+
computeHighSignalNonFriendScores(contribution, nodeByKey, friendKeys),
|
|
911
|
+
MAX_HIGH_SIGNAL_NON_FRIENDS_PER_GROUP,
|
|
912
|
+
remainingLimit()
|
|
913
|
+
);
|
|
914
|
+
return selected;
|
|
915
|
+
}
|
|
916
|
+
function computeHighSignalNonFriendScores(contribution, nodeByKey, friendKeys) {
|
|
917
|
+
const scores = /* @__PURE__ */ new Map();
|
|
918
|
+
for (const key of contribution.memberKeys) {
|
|
919
|
+
const node = nodeByKey.get(key);
|
|
920
|
+
if (!node || node.kind === "owner" || friendKeys.has(key)) continue;
|
|
921
|
+
const messageScore = Math.log1p(contribution.memberMessageCounts.get(key) ?? 0);
|
|
922
|
+
const ownerScore = contribution.ownerEdgeScores.get(key) ?? 0;
|
|
923
|
+
const friendScore = contribution.friendConnectedScores.get(key) ?? 0;
|
|
924
|
+
const relationshipScore = sumRelationshipScoresForKey(contribution, key);
|
|
925
|
+
const score = ownerScore * 2 + friendScore * 1.5 + relationshipScore + messageScore;
|
|
926
|
+
if (score > 0) scores.set(key, score);
|
|
927
|
+
}
|
|
928
|
+
return scores;
|
|
929
|
+
}
|
|
930
|
+
function sumRelationshipScoresForKey(contribution, key) {
|
|
931
|
+
let total = 0;
|
|
932
|
+
for (const [edgeId2, endpoints] of contribution.relationshipEdgeEndpoints) {
|
|
933
|
+
if (endpoints[0] === key || endpoints[1] === key) total += contribution.relationshipEdgeScores.get(edgeId2) ?? 0;
|
|
934
|
+
}
|
|
935
|
+
return total;
|
|
936
|
+
}
|
|
937
|
+
function addTopScoredKeys(selected, scores, limit, remainingLimit) {
|
|
938
|
+
if (limit <= 0 || remainingLimit <= 0) return;
|
|
939
|
+
const items = [...scores.entries()].filter(([key, score]) => !selected.has(key) && score > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
940
|
+
for (const [key] of items.slice(0, Math.min(limit, remainingLimit))) {
|
|
941
|
+
selected.add(key);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function incrementPanoramaReason(diagnostics, reason) {
|
|
945
|
+
diagnostics.panoramaGroupInclusionReasons[reason] = (diagnostics.panoramaGroupInclusionReasons[reason] ?? 0) + 1;
|
|
946
|
+
}
|
|
947
|
+
function buildOwnerKeyForContribution(nodeByKey, contactKey) {
|
|
948
|
+
const contact = nodeByKey.get(contactKey);
|
|
949
|
+
return contact ? buildOwnerKey() : "";
|
|
950
|
+
}
|
|
951
|
+
function toGraphEdge(edge, weight) {
|
|
952
|
+
return {
|
|
953
|
+
id: edgeId(edge.sourceKey, edge.targetKey),
|
|
954
|
+
sourceKey: edge.sourceKey,
|
|
955
|
+
targetKey: edge.targetKey,
|
|
956
|
+
weight: roundNum(weight),
|
|
957
|
+
coOccurrenceCount: edge.coOccurrenceCount,
|
|
958
|
+
coOccurrenceRawScore: roundNum(edge.coOccurrenceRawScore),
|
|
959
|
+
replyInteractionCount: edge.replyInteractionCount,
|
|
960
|
+
repliesFromSourceToTarget: edge.repliesFromSourceToTarget,
|
|
961
|
+
repliesFromTargetToSource: edge.repliesFromTargetToSource,
|
|
962
|
+
sourceGroupCount: edge.sourceSessionIds.size,
|
|
963
|
+
sourceSessionIds: [...edge.sourceSessionIds].sort(),
|
|
964
|
+
lastInteractionTs: edge.lastInteractionTs,
|
|
965
|
+
visibility: weight >= 8 ? 2 : 1
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function createFactsCacheContext(dir) {
|
|
969
|
+
return {
|
|
970
|
+
dir,
|
|
971
|
+
latestKey: buildPeopleRelationshipsSessionLatestCacheKey(PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION),
|
|
972
|
+
stats: createEmptyPeopleRelationshipsFactsCacheStats()
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function readLatestFacts(sessionId, factsCache, dbVersion) {
|
|
976
|
+
if (!factsCache) return null;
|
|
977
|
+
const cached = readCachedPeopleRelationshipsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion);
|
|
978
|
+
if (!cached.hit) {
|
|
979
|
+
factsCache.stats.latestMisses++;
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
factsCache.stats.latestHits++;
|
|
983
|
+
return cached.data;
|
|
984
|
+
}
|
|
985
|
+
function writeLatestFacts(adapter, sessionId, factsCache, data, expectedDbVersion) {
|
|
986
|
+
if (!factsCache) return;
|
|
987
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
988
|
+
if (dbVersion !== expectedDbVersion) {
|
|
989
|
+
appLogger.debug("people-relationships", "skipped latest facts cache write because db version changed", {
|
|
990
|
+
sessionId
|
|
991
|
+
});
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
writeCachedPeopleRelationshipsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, data);
|
|
995
|
+
factsCache.stats.writes++;
|
|
996
|
+
}
|
|
997
|
+
function readSessionFacts(sessionId, timeRange, factsCache, dbVersion) {
|
|
998
|
+
if (!factsCache) return null;
|
|
999
|
+
const cached = readCachedPeopleRelationshipsSessionFacts(
|
|
1000
|
+
sessionId,
|
|
1001
|
+
factsCache.dir,
|
|
1002
|
+
buildPeopleRelationshipsSessionFactsCacheKey(PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION, timeRange),
|
|
1003
|
+
dbVersion
|
|
1004
|
+
);
|
|
1005
|
+
if (!cached.hit) {
|
|
1006
|
+
factsCache.stats.factsMisses++;
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
factsCache.stats.factsHits++;
|
|
1010
|
+
return cached.data;
|
|
1011
|
+
}
|
|
1012
|
+
function writeSessionFacts(adapter, sessionId, timeRange, factsCache, facts, expectedDbVersion) {
|
|
1013
|
+
if (!factsCache) return;
|
|
1014
|
+
const dbVersion = getSessionDbVersion(adapter, sessionId);
|
|
1015
|
+
if (dbVersion !== expectedDbVersion) {
|
|
1016
|
+
appLogger.debug("people-relationships", "skipped session facts cache write because db version changed", {
|
|
1017
|
+
sessionId
|
|
1018
|
+
});
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
writeCachedPeopleRelationshipsSessionFacts(
|
|
1022
|
+
sessionId,
|
|
1023
|
+
factsCache.dir,
|
|
1024
|
+
buildPeopleRelationshipsSessionFactsCacheKey(PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION, timeRange),
|
|
1025
|
+
dbVersion,
|
|
1026
|
+
facts
|
|
1027
|
+
);
|
|
1028
|
+
writeCachedPeopleRelationshipsSessionLatest(sessionId, factsCache.dir, factsCache.latestKey, dbVersion, {
|
|
1029
|
+
latestMessageTs: facts.latestMessageTs
|
|
1030
|
+
});
|
|
1031
|
+
factsCache.stats.writes += 2;
|
|
1032
|
+
}
|
|
1033
|
+
function getSessionDbVersion(adapter, sessionId) {
|
|
1034
|
+
return getDbFileVersion(adapter.getDbPath(sessionId));
|
|
1035
|
+
}
|
|
1036
|
+
function getOrCreateNode(nodes, sessionId, meta, contact) {
|
|
1037
|
+
const sessionScoped = shouldScopeContactToSession(meta.platform, contact);
|
|
1038
|
+
const key = buildContactKey(meta.platform, contact.platformId, sessionScoped ? sessionId : void 0);
|
|
1039
|
+
const existing = nodes.get(key);
|
|
1040
|
+
if (existing) {
|
|
1041
|
+
mergeContactIdentity(existing, contact);
|
|
1042
|
+
return existing;
|
|
1043
|
+
}
|
|
1044
|
+
const created = {
|
|
1045
|
+
key,
|
|
1046
|
+
kind: "contact",
|
|
1047
|
+
platform: meta.platform,
|
|
1048
|
+
platformId: contact.platformId,
|
|
1049
|
+
sessionScoped,
|
|
1050
|
+
sessionId: sessionScoped ? sessionId : void 0,
|
|
1051
|
+
displayName: contact.name || contact.platformId,
|
|
1052
|
+
aliases: new Set([contact.platformId, contact.name, ...contact.aliases].filter(Boolean)),
|
|
1053
|
+
avatar: contact.avatar,
|
|
1054
|
+
isFriend: false,
|
|
1055
|
+
privateMessageCount: 0,
|
|
1056
|
+
activePrivateMonths: /* @__PURE__ */ new Set(),
|
|
1057
|
+
groupMessageCount: 0,
|
|
1058
|
+
commonGroupSessionIds: /* @__PURE__ */ new Set(),
|
|
1059
|
+
ownerCoOccurrenceCount: 0,
|
|
1060
|
+
ownerCoOccurrenceRawScore: 0,
|
|
1061
|
+
ownerReplyInteractionCount: 0,
|
|
1062
|
+
ownerRepliesFromOwnerToContact: 0,
|
|
1063
|
+
ownerRepliesFromContactToOwner: 0,
|
|
1064
|
+
communityWeights: /* @__PURE__ */ new Map(),
|
|
1065
|
+
edgeWeight: 0,
|
|
1066
|
+
panoramaCandidate: false,
|
|
1067
|
+
lastInteractionTs: null
|
|
1068
|
+
};
|
|
1069
|
+
nodes.set(key, created);
|
|
1070
|
+
return created;
|
|
1071
|
+
}
|
|
1072
|
+
function getOrCreateOwnerNode(nodes, meta) {
|
|
1073
|
+
if (!meta.owner) return null;
|
|
1074
|
+
const key = buildOwnerKey();
|
|
1075
|
+
const existing = nodes.get(key);
|
|
1076
|
+
if (existing) {
|
|
1077
|
+
mergeContactIdentity(existing, meta.owner);
|
|
1078
|
+
return existing;
|
|
1079
|
+
}
|
|
1080
|
+
const created = {
|
|
1081
|
+
key,
|
|
1082
|
+
kind: "owner",
|
|
1083
|
+
platform: meta.platform,
|
|
1084
|
+
platformId: meta.owner.platformId || meta.ownerId || OWNER_KEY_PREFIX,
|
|
1085
|
+
sessionScoped: false,
|
|
1086
|
+
displayName: meta.owner.name || meta.owner.platformId || "Me",
|
|
1087
|
+
aliases: new Set(
|
|
1088
|
+
[meta.owner.platformId, meta.owner.name, ...meta.owner.aliases, "\u6211", "me", "owner"].filter(Boolean)
|
|
1089
|
+
),
|
|
1090
|
+
avatar: meta.owner.avatar,
|
|
1091
|
+
isFriend: true,
|
|
1092
|
+
privateMessageCount: 0,
|
|
1093
|
+
activePrivateMonths: /* @__PURE__ */ new Set(),
|
|
1094
|
+
groupMessageCount: 0,
|
|
1095
|
+
commonGroupSessionIds: /* @__PURE__ */ new Set(),
|
|
1096
|
+
ownerCoOccurrenceCount: 0,
|
|
1097
|
+
ownerCoOccurrenceRawScore: 0,
|
|
1098
|
+
ownerReplyInteractionCount: 0,
|
|
1099
|
+
ownerRepliesFromOwnerToContact: 0,
|
|
1100
|
+
ownerRepliesFromContactToOwner: 0,
|
|
1101
|
+
communityWeights: /* @__PURE__ */ new Map(),
|
|
1102
|
+
edgeWeight: 0,
|
|
1103
|
+
panoramaCandidate: true,
|
|
1104
|
+
lastInteractionTs: null
|
|
1105
|
+
};
|
|
1106
|
+
nodes.set(key, created);
|
|
1107
|
+
return created;
|
|
1108
|
+
}
|
|
1109
|
+
function applyOwnerContactEdge(edges, ownerNode, contactNode, facts) {
|
|
1110
|
+
const sourceKey = ownerNode.key < contactNode.key ? ownerNode.key : contactNode.key;
|
|
1111
|
+
const targetKey = ownerNode.key < contactNode.key ? contactNode.key : ownerNode.key;
|
|
1112
|
+
const edge = getOrCreateEdge(edges, sourceKey, targetKey);
|
|
1113
|
+
edge.coOccurrenceCount += facts.coOccurrenceCount;
|
|
1114
|
+
edge.coOccurrenceRawScore += facts.coOccurrenceRawScore;
|
|
1115
|
+
edge.replyInteractionCount += facts.replyInteractionCount;
|
|
1116
|
+
if (sourceKey === ownerNode.key) {
|
|
1117
|
+
edge.repliesFromSourceToTarget += facts.repliesFromOwnerToContact;
|
|
1118
|
+
edge.repliesFromTargetToSource += facts.repliesFromContactToOwner;
|
|
1119
|
+
} else {
|
|
1120
|
+
edge.repliesFromSourceToTarget += facts.repliesFromContactToOwner;
|
|
1121
|
+
edge.repliesFromTargetToSource += facts.repliesFromOwnerToContact;
|
|
1122
|
+
}
|
|
1123
|
+
edge.sourceSessionIds.add(facts.sessionId);
|
|
1124
|
+
if (facts.panoramaEligible) edge.panoramaEligible = true;
|
|
1125
|
+
edge.lastInteractionTs = maxNullableTs(edge.lastInteractionTs, facts.lastInteractionTs);
|
|
1126
|
+
contactNode.ownerCoOccurrenceCount += facts.coOccurrenceCount;
|
|
1127
|
+
contactNode.ownerCoOccurrenceRawScore += facts.coOccurrenceRawScore;
|
|
1128
|
+
contactNode.ownerReplyInteractionCount += facts.replyInteractionCount;
|
|
1129
|
+
contactNode.ownerRepliesFromOwnerToContact += facts.repliesFromOwnerToContact;
|
|
1130
|
+
contactNode.ownerRepliesFromContactToOwner += facts.repliesFromContactToOwner;
|
|
1131
|
+
updateLastInteraction(contactNode, facts.lastInteractionTs);
|
|
1132
|
+
}
|
|
1133
|
+
function getOrCreateEdge(edges, sourceKey, targetKey) {
|
|
1134
|
+
const id = edgeId(sourceKey, targetKey);
|
|
1135
|
+
const existing = edges.get(id);
|
|
1136
|
+
if (existing) return existing;
|
|
1137
|
+
const created = {
|
|
1138
|
+
sourceKey,
|
|
1139
|
+
targetKey,
|
|
1140
|
+
coOccurrenceCount: 0,
|
|
1141
|
+
coOccurrenceRawScore: 0,
|
|
1142
|
+
replyInteractionCount: 0,
|
|
1143
|
+
repliesFromSourceToTarget: 0,
|
|
1144
|
+
repliesFromTargetToSource: 0,
|
|
1145
|
+
sourceSessionIds: /* @__PURE__ */ new Set(),
|
|
1146
|
+
panoramaEligible: false,
|
|
1147
|
+
lastInteractionTs: null
|
|
1148
|
+
};
|
|
1149
|
+
edges.set(id, created);
|
|
1150
|
+
return created;
|
|
1151
|
+
}
|
|
1152
|
+
function toPeopleRelationshipsSessionMetaFacts(meta, owner) {
|
|
1153
|
+
return {
|
|
1154
|
+
name: meta.name,
|
|
1155
|
+
platform: meta.platform,
|
|
1156
|
+
type: meta.type,
|
|
1157
|
+
ownerId: meta.ownerId,
|
|
1158
|
+
owner
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function shouldScopeContactToSession(platform, contact) {
|
|
1162
|
+
if (isNameMatchPlatform(platform)) return true;
|
|
1163
|
+
return platform.trim().toLowerCase() === "qq" && contact.platformId.trim() === contact.name.trim();
|
|
1164
|
+
}
|
|
1165
|
+
function buildContactKey(platform, platformId, sessionId) {
|
|
1166
|
+
const normalizedPlatform = platform.trim();
|
|
1167
|
+
const normalizedPlatformId = platformId.trim();
|
|
1168
|
+
if (!normalizedPlatform) throw new Error("platform is required");
|
|
1169
|
+
if (!normalizedPlatformId) throw new Error("platformId is required");
|
|
1170
|
+
return sessionId?.trim() ? `${normalizedPlatform}:${sessionId.trim()}:${normalizedPlatformId}` : `${normalizedPlatform}:${normalizedPlatformId}`;
|
|
1171
|
+
}
|
|
1172
|
+
function buildOwnerKey() {
|
|
1173
|
+
return OWNER_KEY_PREFIX;
|
|
1174
|
+
}
|
|
1175
|
+
function mergeContactIdentity(acc, contact) {
|
|
1176
|
+
if (contact.name) acc.aliases.add(contact.name);
|
|
1177
|
+
acc.aliases.add(contact.platformId);
|
|
1178
|
+
for (const alias of contact.aliases) acc.aliases.add(alias);
|
|
1179
|
+
if ((!acc.displayName || acc.displayName === acc.platformId) && contact.name) acc.displayName = contact.name;
|
|
1180
|
+
if (!acc.avatar && contact.avatar) acc.avatar = contact.avatar;
|
|
1181
|
+
}
|
|
1182
|
+
function pickCommunityId(node) {
|
|
1183
|
+
let bestId = node.isFriend ? PRIVATE_COMMUNITY_ID : "";
|
|
1184
|
+
let bestWeight = node.isFriend ? 1 : 0;
|
|
1185
|
+
for (const [id, weight] of node.communityWeights) {
|
|
1186
|
+
if (weight > bestWeight || weight === bestWeight && id < bestId) {
|
|
1187
|
+
bestId = id;
|
|
1188
|
+
bestWeight = weight;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return bestId || PRIVATE_COMMUNITY_ID;
|
|
1192
|
+
}
|
|
1193
|
+
function computeEdgeWeight(edge) {
|
|
1194
|
+
return computeRawEdgeWeight(edge);
|
|
1195
|
+
}
|
|
1196
|
+
function computeRawEdgeWeight(edge) {
|
|
1197
|
+
return edge.coOccurrenceRawScore + edge.replyInteractionCount * REPLY_WEIGHT + edge.coOccurrenceCount * CO_OCCURRENCE_COUNT_WEIGHT;
|
|
1198
|
+
}
|
|
1199
|
+
function computeContactPriorityScores(nodes) {
|
|
1200
|
+
const result = /* @__PURE__ */ new Map();
|
|
1201
|
+
for (const node of nodes) {
|
|
1202
|
+
if (node.kind === "owner") result.set(node, 1);
|
|
1203
|
+
}
|
|
1204
|
+
const friendInputs = nodes.filter((node) => node.kind !== "owner" && node.isFriend).map((node) => ({
|
|
1205
|
+
node,
|
|
1206
|
+
privateMessageCount: node.privateMessageCount,
|
|
1207
|
+
activeMonths: [...node.activePrivateMonths],
|
|
1208
|
+
commonGroupCount: node.commonGroupSessionIds.size
|
|
1209
|
+
}));
|
|
1210
|
+
const nonFriendInputs = nodes.filter((node) => node.kind !== "owner" && !node.isFriend).map((node) => ({
|
|
1211
|
+
node,
|
|
1212
|
+
coOccurrenceRawScore: node.ownerCoOccurrenceRawScore,
|
|
1213
|
+
commonGroupCount: node.commonGroupSessionIds.size,
|
|
1214
|
+
replyInteractionCount: node.ownerReplyInteractionCount,
|
|
1215
|
+
coOccurrenceCount: node.ownerCoOccurrenceCount,
|
|
1216
|
+
repliesFromOwnerToContact: node.ownerRepliesFromOwnerToContact,
|
|
1217
|
+
repliesFromContactToOwner: node.ownerRepliesFromContactToOwner
|
|
1218
|
+
}));
|
|
1219
|
+
const friendScores = computeFriendScores(friendInputs);
|
|
1220
|
+
const nonFriendScores = computeNonFriendScores(nonFriendInputs);
|
|
1221
|
+
for (const input of friendInputs) {
|
|
1222
|
+
result.set(input.node, friendScores.get(input)?.score ?? 0);
|
|
1223
|
+
}
|
|
1224
|
+
for (const input of nonFriendInputs) {
|
|
1225
|
+
result.set(input.node, nonFriendScores.get(input)?.score ?? 0);
|
|
1226
|
+
}
|
|
1227
|
+
return result;
|
|
1228
|
+
}
|
|
1229
|
+
function compareRankedNodes(a, b) {
|
|
1230
|
+
const poolPriorityDiff = getNodePoolPriority(a.node) - getNodePoolPriority(b.node);
|
|
1231
|
+
if (poolPriorityDiff !== 0) return poolPriorityDiff;
|
|
1232
|
+
return b.score - a.score || b.relationshipActivityScore - a.relationshipActivityScore || a.node.displayName.localeCompare(b.node.displayName);
|
|
1233
|
+
}
|
|
1234
|
+
function getNodePoolPriority(node) {
|
|
1235
|
+
if (node.kind === "owner") return 0;
|
|
1236
|
+
return node.isFriend ? 1 : 2;
|
|
1237
|
+
}
|
|
1238
|
+
function computeRelationshipActivityScore(node) {
|
|
1239
|
+
return (node.isFriend ? 60 : 0) + node.privateMessageCount * 1.8 + node.groupMessageCount * 0.35 + node.commonGroupSessionIds.size * 8 + node.edgeWeight * 6;
|
|
1240
|
+
}
|
|
1241
|
+
function normalizeLimits(limits = {}) {
|
|
1242
|
+
return {
|
|
1243
|
+
coreNodeLimit: normalizePositiveLimit(limits.coreNodeLimit, 1500),
|
|
1244
|
+
coreEdgeLimit: normalizePositiveLimit(limits.coreEdgeLimit, 6e3),
|
|
1245
|
+
perNodeEdgeLimit: normalizePositiveLimit(limits.perNodeEdgeLimit, 12),
|
|
1246
|
+
neighborhoodNodeLimit: normalizePositiveLimit(limits.neighborhoodNodeLimit, 80),
|
|
1247
|
+
neighborhoodEdgeLimit: normalizePositiveLimit(limits.neighborhoodEdgeLimit, 240),
|
|
1248
|
+
searchResultLimit: normalizePositiveLimit(limits.searchResultLimit, 20)
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function normalizePositiveLimit(value, fallback) {
|
|
1252
|
+
if (!Number.isFinite(value)) return fallback;
|
|
1253
|
+
return Math.max(1, Math.trunc(value));
|
|
1254
|
+
}
|
|
1255
|
+
function createEmptyDiagnostics() {
|
|
1256
|
+
return {
|
|
1257
|
+
processedPrivateSessions: 0,
|
|
1258
|
+
processedGroupSessions: 0,
|
|
1259
|
+
skippedMissingOwnerSessions: 0,
|
|
1260
|
+
skippedUnresolvedOwnerSessions: 0,
|
|
1261
|
+
skippedAmbiguousPrivateSessions: 0,
|
|
1262
|
+
skippedFailedSessions: 0,
|
|
1263
|
+
totalNodes: 0,
|
|
1264
|
+
totalEdges: 0,
|
|
1265
|
+
panoramaIncludedGroupSessions: 0,
|
|
1266
|
+
panoramaExcludedLowValueGroupSessions: 0,
|
|
1267
|
+
panoramaIncludedGroupMembers: 0,
|
|
1268
|
+
panoramaExcludedGroupMembers: 0,
|
|
1269
|
+
panoramaCandidateNodes: 0,
|
|
1270
|
+
panoramaGroupInclusionReasons: {},
|
|
1271
|
+
coreNodeCount: 0,
|
|
1272
|
+
coreEdgeCount: 0,
|
|
1273
|
+
warnings: []
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function updateLastInteraction(acc, ts) {
|
|
1277
|
+
acc.lastInteractionTs = maxNullableTs(acc.lastInteractionTs, ts);
|
|
1278
|
+
}
|
|
1279
|
+
function maxNullableTs(current, next) {
|
|
1280
|
+
if (next === null) return current;
|
|
1281
|
+
return Math.max(current ?? 0, next);
|
|
1282
|
+
}
|
|
1283
|
+
function edgeId(sourceKey, targetKey) {
|
|
1284
|
+
return `${sourceKey}__${targetKey}`;
|
|
1285
|
+
}
|
|
1286
|
+
function sortedEdgeId(aKey, bKey) {
|
|
1287
|
+
return aKey < bKey ? edgeId(aKey, bKey) : edgeId(bKey, aKey);
|
|
1288
|
+
}
|
|
1289
|
+
function colorForCommunity(id) {
|
|
1290
|
+
return COMMUNITY_COLORS[stableHash(id) % COMMUNITY_COLORS.length];
|
|
1291
|
+
}
|
|
1292
|
+
function stableHash(value) {
|
|
1293
|
+
let hash = 2166136261;
|
|
1294
|
+
for (let index = 0; index < value.length; index++) {
|
|
1295
|
+
hash ^= value.charCodeAt(index);
|
|
1296
|
+
hash = Math.imul(hash, 16777619);
|
|
1297
|
+
}
|
|
1298
|
+
return hash >>> 0;
|
|
1299
|
+
}
|
|
1300
|
+
function compareNodes(a, b) {
|
|
1301
|
+
return a.rank - b.rank || a.displayName.localeCompare(b.displayName);
|
|
1302
|
+
}
|
|
1303
|
+
function compareEdges(a, b) {
|
|
1304
|
+
return b.weight - a.weight || a.id.localeCompare(b.id);
|
|
1305
|
+
}
|
|
1306
|
+
function sortEdgesForDisplay(edges) {
|
|
1307
|
+
const anchorTs = edges.reduce((max, edge) => Math.max(max, edge.lastInteractionTs ?? 0), 0);
|
|
1308
|
+
return [...edges].sort((a, b) => compareEdgesForDisplay(a, b, anchorTs));
|
|
1309
|
+
}
|
|
1310
|
+
function compareEdgesForDisplay(a, b, anchorTs) {
|
|
1311
|
+
const bRecentWeight = getRecentEdgeWeight(b, anchorTs);
|
|
1312
|
+
const aRecentWeight = getRecentEdgeWeight(a, anchorTs);
|
|
1313
|
+
if (bRecentWeight !== aRecentWeight) return bRecentWeight - aRecentWeight;
|
|
1314
|
+
return compareEdges(a, b);
|
|
1315
|
+
}
|
|
1316
|
+
function getRecentEdgeWeight(edge, anchorTs) {
|
|
1317
|
+
if (!edge.lastInteractionTs || anchorTs <= 0) return edge.weight;
|
|
1318
|
+
const ageSeconds = Math.max(0, anchorTs - edge.lastInteractionTs);
|
|
1319
|
+
const recencyFactor = EDGE_RECENCY_FLOOR + (1 - EDGE_RECENCY_FLOOR) * Math.pow(0.5, ageSeconds / EDGE_RECENCY_HALF_LIFE_SECONDS);
|
|
1320
|
+
return edge.weight * recencyFactor;
|
|
1321
|
+
}
|
|
1322
|
+
function clamp(value, min, max) {
|
|
1323
|
+
return Math.min(max, Math.max(min, value));
|
|
1324
|
+
}
|
|
1325
|
+
function roundNum(value, digits = 4) {
|
|
1326
|
+
const factor = 10 ** digits;
|
|
1327
|
+
return Math.round(value * factor) / factor;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export {
|
|
1331
|
+
normalizePeopleRelationshipsTimeRangePreset,
|
|
1332
|
+
resolvePeopleRelationshipsTimeRange,
|
|
1333
|
+
PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION,
|
|
1334
|
+
computePeopleRelationshipsSnapshot,
|
|
1335
|
+
buildPeopleRelationshipsNeighborhoodGraph,
|
|
1336
|
+
compareNodes,
|
|
1337
|
+
compareEdges
|
|
1338
|
+
};
|
|
1339
|
+
//# sourceMappingURL=chunk-MUPILWAZ.mjs.map
|