koishi-plugin-chatluna-affinity 0.3.1 → 0.3.2

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/lib/index.js CHANGED
@@ -417,6 +417,7 @@ __export(index_exports, {
417
417
  COMMON_STYLE: () => COMMON_STYLE,
418
418
  Config: () => ConfigSchema,
419
419
  ConfigSchema: () => ConfigSchema,
420
+ DASHBOARD_SNAPSHOT_MODEL_NAME: () => DASHBOARD_SNAPSHOT_MODEL_NAME,
420
421
  DEFAULT_MEMBER_INFO_ITEMS: () => DEFAULT_MEMBER_INFO_ITEMS,
421
422
  FETCH_CONSTANTS: () => FETCH_CONSTANTS,
422
423
  MIGRATION_MODEL_NAME: () => MIGRATION_MODEL_NAME,
@@ -430,6 +431,7 @@ __export(index_exports, {
430
431
  THRESHOLDS: () => THRESHOLDS,
431
432
  TIME_CONSTANTS: () => TIME_CONSTANTS,
432
433
  TIMING_CONSTANTS: () => TIMING_CONSTANTS,
434
+ USER_AFFINITY_SNAPSHOT_MODEL_NAME: () => USER_AFFINITY_SNAPSHOT_MODEL_NAME,
433
435
  USER_ALIAS_MODEL_NAME: () => USER_ALIAS_MODEL_NAME,
434
436
  USER_ALIAS_MODEL_NAME_V2: () => USER_ALIAS_MODEL_NAME_V2,
435
437
  appendActionEntry: () => appendActionEntry,
@@ -474,6 +476,7 @@ __export(index_exports, {
474
476
  escapeHtmlForRender: () => escapeHtmlForRender,
475
477
  extendAffinityModel: () => extendAffinityModel,
476
478
  extendBlacklistModel: () => extendBlacklistModel,
479
+ extendDashboardSnapshotModel: () => extendDashboardSnapshotModel,
477
480
  extendMigrationModel: () => extendMigrationModel,
478
481
  extendUserAliasModel: () => extendUserAliasModel,
479
482
  fetchGroupMemberIds: () => fetchGroupMemberIds,
@@ -860,6 +863,7 @@ var VariableSettingsSchema = import_koishi4.Schema.object({
860
863
  )
861
864
  }).description("\u53D8\u91CF\u8BBE\u7F6E");
862
865
  var OtherSettingsSchema = import_koishi4.Schema.object({
866
+ enableDashboard: import_koishi4.Schema.boolean().default(true).description("\u5728 Koishi \u63A7\u5236\u53F0\u4FA7\u680F\u663E\u793A\u597D\u611F\u5EA6\u4EEA\u8868\u76D8"),
863
867
  rankRenderAsImage: import_koishi4.Schema.boolean().default(false).description("\u5C06\u597D\u611F\u5EA6\u6392\u884C\u6E32\u67D3\u4E3A\u56FE\u7247"),
864
868
  blacklistRenderAsImage: import_koishi4.Schema.boolean().default(false).description("\u5C06\u9ED1\u540D\u5355\u6E32\u67D3\u4E3A\u56FE\u7247"),
865
869
  shortTermBlacklistRenderAsImage: import_koishi4.Schema.boolean().default(false).description("\u5C06\u4E34\u65F6\u9ED1\u540D\u5355\u6E32\u67D3\u4E3A\u56FE\u7247"),
@@ -1012,12 +1016,57 @@ function extendMigrationModel(ctx) {
1012
1016
  );
1013
1017
  }
1014
1018
 
1019
+ // src/models/dashboard-snapshot.ts
1020
+ var DASHBOARD_SNAPSHOT_MODEL_NAME = "chatluna_affinity_dashboard_snapshot";
1021
+ var USER_AFFINITY_SNAPSHOT_MODEL_NAME = "chatluna_affinity_user_snapshot";
1022
+ function extendDashboardSnapshotModel(ctx) {
1023
+ ctx.model.extend(
1024
+ DASHBOARD_SNAPSHOT_MODEL_NAME,
1025
+ {
1026
+ scopeId: { type: "string", length: 32 },
1027
+ date: { type: "string", length: 10 },
1028
+ recordedAt: { type: "timestamp" },
1029
+ generatedBy: { type: "string", length: 32, nullable: true },
1030
+ users: { type: "integer", initial: 0 },
1031
+ affinityTotal: { type: "integer", initial: 0 },
1032
+ longTermAffinityTotal: { type: "integer", initial: 0 },
1033
+ shortTermAffinityTotal: { type: "integer", initial: 0 },
1034
+ chatCount: { type: "integer", initial: 0 },
1035
+ blacklisted: { type: "integer", initial: 0 },
1036
+ permanentBlacklisted: { type: "integer", initial: 0 },
1037
+ temporaryBlacklisted: { type: "integer", initial: 0 },
1038
+ aliases: { type: "integer", initial: 0 },
1039
+ latestInteractionAt: { type: "timestamp", nullable: true }
1040
+ },
1041
+ { primary: ["scopeId", "date"] }
1042
+ );
1043
+ ctx.model.extend(
1044
+ USER_AFFINITY_SNAPSHOT_MODEL_NAME,
1045
+ {
1046
+ scopeId: { type: "string", length: 32 },
1047
+ userId: { type: "string", length: 64 },
1048
+ date: { type: "string", length: 10 },
1049
+ recordedAt: { type: "timestamp" },
1050
+ nickname: { type: "string", length: 255, nullable: true },
1051
+ affinity: { type: "integer", initial: 0 },
1052
+ longTermAffinity: { type: "integer", initial: 0 },
1053
+ shortTermAffinity: { type: "integer", initial: 0 },
1054
+ chatCount: { type: "integer", initial: 0 },
1055
+ relation: { type: "string", length: 64, nullable: true },
1056
+ specialRelation: { type: "string", length: 64, nullable: true },
1057
+ lastInteractionAt: { type: "timestamp", nullable: true }
1058
+ },
1059
+ { primary: ["scopeId", "userId", "date"] }
1060
+ );
1061
+ }
1062
+
1015
1063
  // src/models/index.ts
1016
1064
  function registerModels(ctx) {
1017
1065
  extendAffinityModel(ctx);
1018
1066
  extendBlacklistModel(ctx);
1019
1067
  extendUserAliasModel(ctx);
1020
1068
  extendMigrationModel(ctx);
1069
+ extendDashboardSnapshotModel(ctx);
1021
1070
  }
1022
1071
 
1023
1072
  // src/helpers/logger.ts
@@ -3834,6 +3883,600 @@ function createMigrationService(options) {
3834
3883
  return { run };
3835
3884
  }
3836
3885
 
3886
+ // src/services/dashboard/snapshot.ts
3887
+ async function readRecordedDashboardSnapshots(ctx, scopeId) {
3888
+ const dashboardSnapshots = await ctx.database.get(
3889
+ DASHBOARD_SNAPSHOT_MODEL_NAME,
3890
+ { scopeId }
3891
+ );
3892
+ const userAffinitySnapshots = await ctx.database.get(
3893
+ USER_AFFINITY_SNAPSHOT_MODEL_NAME,
3894
+ { scopeId }
3895
+ );
3896
+ return { dashboardSnapshots, userAffinitySnapshots };
3897
+ }
3898
+ function formatSnapshotDate(value) {
3899
+ const year = value.getFullYear();
3900
+ const month = String(value.getMonth() + 1).padStart(2, "0");
3901
+ const day = String(value.getDate()).padStart(2, "0");
3902
+ return `${year}-${month}-${day}`;
3903
+ }
3904
+ function parseSnapshotDate(value) {
3905
+ const matched = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
3906
+ if (!matched) return null;
3907
+ const year = Number(matched[1]);
3908
+ const month = Number(matched[2]);
3909
+ const day = Number(matched[3]);
3910
+ const date = new Date(year, month - 1, day);
3911
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
3912
+ return null;
3913
+ }
3914
+ return date;
3915
+ }
3916
+ async function readDashboardSnapshotSource(ctx, scopeId) {
3917
+ const affinityRows = await ctx.database.get(MODEL_NAME_V2, { scopeId });
3918
+ const blacklistRows = await ctx.database.get(BLACKLIST_MODEL_NAME_V2, {
3919
+ scopeId
3920
+ });
3921
+ const aliasRows = await ctx.database.get(USER_ALIAS_MODEL_NAME_V2, {
3922
+ scopeId
3923
+ });
3924
+ return { affinityRows, blacklistRows, aliasRows };
3925
+ }
3926
+ function createDashboardSnapshot(scopeId, now, source, trigger = "backend") {
3927
+ const permanentBlacklisted = source.blacklistRows.filter(
3928
+ (row) => row.mode === "permanent"
3929
+ ).length;
3930
+ const temporaryBlacklisted = source.blacklistRows.filter(
3931
+ (row) => row.mode === "temporary"
3932
+ ).length;
3933
+ const latestInteractionAt = source.affinityRows.reduce(
3934
+ (latest, row) => {
3935
+ const value = row.lastInteractionAt;
3936
+ if (!value) return latest;
3937
+ if (!latest || value.getTime() > latest.getTime()) return value;
3938
+ return latest;
3939
+ },
3940
+ null
3941
+ );
3942
+ return {
3943
+ scopeId,
3944
+ date: formatSnapshotDate(now),
3945
+ recordedAt: now,
3946
+ generatedBy: trigger,
3947
+ users: source.affinityRows.length,
3948
+ affinityTotal: source.affinityRows.reduce(
3949
+ (total, row) => total + Number(row.affinity || 0),
3950
+ 0
3951
+ ),
3952
+ longTermAffinityTotal: source.affinityRows.reduce(
3953
+ (total, row) => total + Number(row.longTermAffinity ?? row.affinity ?? 0),
3954
+ 0
3955
+ ),
3956
+ shortTermAffinityTotal: source.affinityRows.reduce(
3957
+ (total, row) => total + Number(row.shortTermAffinity || 0),
3958
+ 0
3959
+ ),
3960
+ chatCount: source.affinityRows.reduce(
3961
+ (total, row) => total + Number(row.chatCount || 0),
3962
+ 0
3963
+ ),
3964
+ blacklisted: source.blacklistRows.length,
3965
+ permanentBlacklisted,
3966
+ temporaryBlacklisted,
3967
+ aliases: source.aliasRows.length,
3968
+ latestInteractionAt
3969
+ };
3970
+ }
3971
+ function createUserAffinitySnapshots(scopeId, now, source, existingSnapshots = []) {
3972
+ const date = formatSnapshotDate(now);
3973
+ const latestSnapshotsByUserId = /* @__PURE__ */ new Map();
3974
+ for (const snapshot of existingSnapshots) {
3975
+ if (snapshot.scopeId !== scopeId || !parseSnapshotDate(snapshot.date)) {
3976
+ continue;
3977
+ }
3978
+ const latest = latestSnapshotsByUserId.get(snapshot.userId);
3979
+ if (!latest || snapshot.date >= latest.date) {
3980
+ latestSnapshotsByUserId.set(snapshot.userId, snapshot);
3981
+ }
3982
+ }
3983
+ return source.affinityRows.filter((row) => {
3984
+ const latest = latestSnapshotsByUserId.get(row.userId);
3985
+ return !latest || Number(latest.affinity || 0) !== Number(row.affinity || 0) || Number(latest.longTermAffinity || 0) !== Number(row.longTermAffinity ?? row.affinity ?? 0) || Number(latest.chatCount || 0) !== Number(row.chatCount || 0);
3986
+ }).map((row) => ({
3987
+ scopeId,
3988
+ userId: row.userId,
3989
+ date,
3990
+ recordedAt: now,
3991
+ nickname: row.nickname || null,
3992
+ affinity: Number(row.affinity || 0),
3993
+ longTermAffinity: Number(row.longTermAffinity ?? row.affinity ?? 0),
3994
+ shortTermAffinity: Number(row.shortTermAffinity || 0),
3995
+ chatCount: Number(row.chatCount || 0),
3996
+ relation: row.relation || null,
3997
+ specialRelation: row.specialRelation || null,
3998
+ lastInteractionAt: row.lastInteractionAt || null
3999
+ }));
4000
+ }
4001
+ function hasSnapshotSourceData(source) {
4002
+ return source.affinityRows.length > 0 || source.blacklistRows.length > 0 || source.aliasRows.length > 0;
4003
+ }
4004
+ function mergeDashboardSnapshot(rows, snapshot) {
4005
+ return [
4006
+ ...rows.filter(
4007
+ (row) => row.scopeId !== snapshot.scopeId || row.date !== snapshot.date
4008
+ ),
4009
+ snapshot
4010
+ ];
4011
+ }
4012
+ function mergeUserAffinitySnapshots(rows, snapshots) {
4013
+ const snapshotKeys = new Set(
4014
+ snapshots.map(
4015
+ (snapshot) => `${snapshot.scopeId}:${snapshot.userId}:${snapshot.date}`
4016
+ )
4017
+ );
4018
+ return [
4019
+ ...rows.filter(
4020
+ (row) => !snapshotKeys.has(`${row.scopeId}:${row.userId}:${row.date}`)
4021
+ ),
4022
+ ...snapshots
4023
+ ];
4024
+ }
4025
+ async function recordDashboardSnapshot(ctx, options) {
4026
+ const scopeId = options.scopeId.trim();
4027
+ if (!scopeId) {
4028
+ return { dashboardSnapshots: [], userAffinitySnapshots: [] };
4029
+ }
4030
+ const now = options.now || /* @__PURE__ */ new Date();
4031
+ const source = options.source || await readDashboardSnapshotSource(ctx, scopeId);
4032
+ const existingSnapshots = options.existingSnapshots || await ctx.database.get(DASHBOARD_SNAPSHOT_MODEL_NAME, { scopeId });
4033
+ const existingUserAffinitySnapshots = options.existingUserAffinitySnapshots || await ctx.database.get(USER_AFFINITY_SNAPSHOT_MODEL_NAME, { scopeId });
4034
+ if (!hasSnapshotSourceData(source) && existingSnapshots.length === 0) {
4035
+ return {
4036
+ dashboardSnapshots: existingSnapshots,
4037
+ userAffinitySnapshots: existingUserAffinitySnapshots
4038
+ };
4039
+ }
4040
+ const snapshot = createDashboardSnapshot(
4041
+ scopeId,
4042
+ now,
4043
+ source,
4044
+ options.trigger
4045
+ );
4046
+ const userSnapshots = createUserAffinitySnapshots(
4047
+ scopeId,
4048
+ now,
4049
+ source,
4050
+ existingUserAffinitySnapshots
4051
+ );
4052
+ await ctx.database.upsert(DASHBOARD_SNAPSHOT_MODEL_NAME, [snapshot]);
4053
+ if (userSnapshots.length > 0) {
4054
+ await ctx.database.upsert(USER_AFFINITY_SNAPSHOT_MODEL_NAME, userSnapshots);
4055
+ }
4056
+ return {
4057
+ dashboardSnapshots: mergeDashboardSnapshot(existingSnapshots, snapshot),
4058
+ userAffinitySnapshots: mergeUserAffinitySnapshots(
4059
+ existingUserAffinitySnapshots,
4060
+ userSnapshots
4061
+ )
4062
+ };
4063
+ }
4064
+
4065
+ // src/services/dashboard/backend.ts
4066
+ var DEFAULT_SNAPSHOT_INTERVAL_MS = 60 * 60 * 1e3;
4067
+ function registerDashboardBackend(options) {
4068
+ const { config, ctx, log } = options;
4069
+ if (config.enableDashboard === false) return;
4070
+ const getNow = options.now || (() => /* @__PURE__ */ new Date());
4071
+ const intervalMs = options.sampleIntervalMs || DEFAULT_SNAPSHOT_INTERVAL_MS;
4072
+ const schedule = options.setInterval || setInterval;
4073
+ const clear = options.clearInterval || clearInterval;
4074
+ let timer = null;
4075
+ let running = false;
4076
+ const record = async () => {
4077
+ if (running) return;
4078
+ running = true;
4079
+ try {
4080
+ await recordDashboardSnapshot(ctx, {
4081
+ scopeId: config.scopeId,
4082
+ now: getNow()
4083
+ });
4084
+ } catch (error) {
4085
+ log("warn", "\u8BB0\u5F55\u4EEA\u8868\u76D8\u540E\u53F0\u5FEB\u7167\u5931\u8D25", error);
4086
+ } finally {
4087
+ running = false;
4088
+ }
4089
+ };
4090
+ ctx.on("ready", () => {
4091
+ if (timer !== null) return;
4092
+ void record();
4093
+ timer = schedule(() => {
4094
+ void record();
4095
+ }, intervalMs);
4096
+ });
4097
+ ctx.on("dispose", () => {
4098
+ if (timer === null) return;
4099
+ clear(timer);
4100
+ timer = null;
4101
+ });
4102
+ }
4103
+
4104
+ // src/services/dashboard/index.ts
4105
+ var DASHBOARD_EVENT = "chatluna-affinity/dashboard";
4106
+ var DAY_MS = 24 * 60 * 60 * 1e3;
4107
+ var WEEK_DAYS = 7;
4108
+ var MONTH_DAYS = 30;
4109
+ var HISTORY_POINT_LIMIT = 12;
4110
+ function toIsoString(value) {
4111
+ if (!value) return null;
4112
+ const date = value instanceof Date ? value : new Date(value);
4113
+ if (Number.isNaN(date.getTime())) return null;
4114
+ return date.toISOString();
4115
+ }
4116
+ function toCount(value) {
4117
+ return Number.isFinite(value) ? Number(value) : 0;
4118
+ }
4119
+ function roundAverage(total, count) {
4120
+ if (!count) return 0;
4121
+ return Math.round(total / count * 100) / 100;
4122
+ }
4123
+ function roundPercent(value) {
4124
+ return Math.round(value * 100) / 100;
4125
+ }
4126
+ function snapshotAffinityTotal(snapshot) {
4127
+ return toCount(snapshot.affinityTotal);
4128
+ }
4129
+ function snapshotUsers(snapshot) {
4130
+ return toCount(snapshot.users);
4131
+ }
4132
+ function snapshotChatCount(snapshot) {
4133
+ return toCount(snapshot.chatCount);
4134
+ }
4135
+ function snapshotBlacklisted(snapshot) {
4136
+ return toCount(snapshot.blacklisted);
4137
+ }
4138
+ function snapshotAliases(snapshot) {
4139
+ return toCount(snapshot.aliases);
4140
+ }
4141
+ function createMetricChange(current, previous) {
4142
+ return {
4143
+ current,
4144
+ previous,
4145
+ percent: previous === 0 ? current === 0 ? 0 : null : roundPercent((current - previous) / previous * 100)
4146
+ };
4147
+ }
4148
+ function getDisplayRelation(record) {
4149
+ return record.specialRelation || record.relation || "\u672A\u5206\u7EC4";
4150
+ }
4151
+ function getRelationKind(record) {
4152
+ return record.specialRelation ? "custom" : "preset";
4153
+ }
4154
+ function getRelationTone(record, levels) {
4155
+ if (record.specialRelation) return "custom";
4156
+ const relation = record.relation || "";
4157
+ const orderedLevels = [...levels || []].filter((level) => level.relation).sort((left, right) => left.min - right.min);
4158
+ const index = orderedLevels.findIndex((level) => level.relation === relation);
4159
+ if (index < 0) return "unknown";
4160
+ const ratio = orderedLevels.length > 1 ? index / (orderedLevels.length - 1) : 1;
4161
+ if (ratio <= 0.25) return "low";
4162
+ if (ratio < 0.75) return "medium";
4163
+ return "high";
4164
+ }
4165
+ function getDisplayName(record) {
4166
+ return record.nickname || record.userId;
4167
+ }
4168
+ function getOneBotAvatarUrl(userId) {
4169
+ const numericId = userId.match(/^\d+$/)?.[0];
4170
+ return numericId ? `https://q1.qlogo.cn/g?b=qq&nk=${numericId}&s=640` : null;
4171
+ }
4172
+ function getBlacklistDisplayName(record) {
4173
+ return record.nickname || record.userId;
4174
+ }
4175
+ function formatTrendLabel(date) {
4176
+ return `${date.getMonth() + 1}/${date.getDate()}`;
4177
+ }
4178
+ function startOfLocalDay(value) {
4179
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate());
4180
+ }
4181
+ function createEmptyTrendPoint(date) {
4182
+ return {
4183
+ label: formatTrendLabel(date),
4184
+ users: 0,
4185
+ averageAffinity: 0,
4186
+ chatCount: 0,
4187
+ blacklisted: 0
4188
+ };
4189
+ }
4190
+ function createSnapshotTrendPoint(snapshot) {
4191
+ const date = parseSnapshotDate(snapshot.date);
4192
+ if (!date) return null;
4193
+ return {
4194
+ label: formatTrendLabel(date),
4195
+ users: snapshotUsers(snapshot),
4196
+ averageAffinity: roundAverage(
4197
+ snapshotAffinityTotal(snapshot),
4198
+ snapshotUsers(snapshot)
4199
+ ),
4200
+ chatCount: snapshotChatCount(snapshot),
4201
+ blacklisted: snapshotBlacklisted(snapshot)
4202
+ };
4203
+ }
4204
+ function createDailyTrend(snapshots, anchor, days) {
4205
+ const snapshotsByDate = new Map(
4206
+ snapshots.map((snapshot) => [snapshot.date, snapshot])
4207
+ );
4208
+ const end = startOfLocalDay(anchor).getTime() + DAY_MS;
4209
+ const start = new Date(end - days * DAY_MS);
4210
+ return Array.from({ length: days }, (_, index) => {
4211
+ const date = new Date(start.getTime() + index * DAY_MS);
4212
+ const snapshot = snapshotsByDate.get(formatSnapshotDate(date));
4213
+ const point = snapshot ? createSnapshotTrendPoint(snapshot) : null;
4214
+ return point || createEmptyTrendPoint(date);
4215
+ });
4216
+ }
4217
+ function createAllTrend(snapshots) {
4218
+ const latestByMonth = /* @__PURE__ */ new Map();
4219
+ for (const snapshot of snapshots) {
4220
+ const date = parseSnapshotDate(snapshot.date);
4221
+ if (!date) continue;
4222
+ const key = `${date.getFullYear()}-${date.getMonth()}`;
4223
+ const current = latestByMonth.get(key);
4224
+ if (!current || snapshot.date > current.date) {
4225
+ latestByMonth.set(key, snapshot);
4226
+ }
4227
+ }
4228
+ return [...latestByMonth.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, snapshot]) => {
4229
+ const point = createSnapshotTrendPoint(snapshot);
4230
+ const date = parseSnapshotDate(snapshot.date);
4231
+ return {
4232
+ ...point || createEmptyTrendPoint(date || /* @__PURE__ */ new Date()),
4233
+ label: date ? `${date.getFullYear()}/${date.getMonth() + 1}` : snapshot.date
4234
+ };
4235
+ });
4236
+ }
4237
+ function getSnapshotTime(snapshot) {
4238
+ return parseSnapshotDate(snapshot.date)?.getTime() ?? null;
4239
+ }
4240
+ function getLatestSnapshotDate(snapshots) {
4241
+ const times = snapshots.map(getSnapshotTime).filter((value) => value !== null);
4242
+ if (!times.length) return null;
4243
+ return new Date(Math.max(...times));
4244
+ }
4245
+ function findLatestSnapshotInWindow(snapshots, start, end) {
4246
+ let latest = null;
4247
+ let latestTime = -Infinity;
4248
+ for (const snapshot of snapshots) {
4249
+ const time = getSnapshotTime(snapshot);
4250
+ if (time === null || time < start || time >= end || time < latestTime) {
4251
+ continue;
4252
+ }
4253
+ latest = snapshot;
4254
+ latestTime = time;
4255
+ }
4256
+ return latest;
4257
+ }
4258
+ function snapshotAverage(snapshot) {
4259
+ if (!snapshot) return 0;
4260
+ return roundAverage(snapshotAffinityTotal(snapshot), snapshotUsers(snapshot));
4261
+ }
4262
+ function createUserHistoryPoints(row, snapshots, trendAnchor) {
4263
+ const points = snapshots.map((snapshot) => {
4264
+ const date = parseSnapshotDate(snapshot.date);
4265
+ if (!date) return null;
4266
+ return {
4267
+ date,
4268
+ point: {
4269
+ label: formatTrendLabel(date),
4270
+ timestamp: toIsoString(snapshot.recordedAt) || toIsoString(date),
4271
+ affinity: toCount(snapshot.affinity),
4272
+ longTermAffinity: toCount(snapshot.longTermAffinity),
4273
+ chatCount: toCount(snapshot.chatCount)
4274
+ }
4275
+ };
4276
+ }).filter(
4277
+ (point) => point !== null
4278
+ ).sort((left, right) => left.date.getTime() - right.date.getTime());
4279
+ if (points.length) {
4280
+ const anchorDate = startOfLocalDay(trendAnchor);
4281
+ const latest = points.at(-1);
4282
+ if (latest && latest.date.getTime() < anchorDate.getTime()) {
4283
+ points.push({
4284
+ date: anchorDate,
4285
+ point: {
4286
+ label: formatTrendLabel(anchorDate),
4287
+ timestamp: toIsoString(anchorDate),
4288
+ affinity: latest.point.affinity,
4289
+ longTermAffinity: toCount(row.longTermAffinity ?? row.affinity),
4290
+ chatCount: toCount(row.chatCount)
4291
+ }
4292
+ });
4293
+ }
4294
+ return points.slice(-HISTORY_POINT_LIMIT).map(({ point }) => point);
4295
+ }
4296
+ return [
4297
+ {
4298
+ label: "\u5F53\u524D",
4299
+ timestamp: toIsoString(row.lastInteractionAt),
4300
+ affinity: toCount(row.affinity),
4301
+ longTermAffinity: toCount(row.longTermAffinity ?? row.affinity),
4302
+ chatCount: toCount(row.chatCount)
4303
+ }
4304
+ ];
4305
+ }
4306
+ function groupUserSnapshots(snapshots) {
4307
+ const grouped = /* @__PURE__ */ new Map();
4308
+ for (const snapshot of snapshots) {
4309
+ const rows = grouped.get(snapshot.userId) || [];
4310
+ rows.push(snapshot);
4311
+ grouped.set(snapshot.userId, rows);
4312
+ }
4313
+ return grouped;
4314
+ }
4315
+ async function getDashboardData(ctx, options) {
4316
+ const scopeId = options.scopeId;
4317
+ const now = options.now || /* @__PURE__ */ new Date();
4318
+ const relationshipAffinityLevels = options.relationshipAffinityLevels || [];
4319
+ const affinityRows = await ctx.database.get(MODEL_NAME_V2, {
4320
+ scopeId
4321
+ });
4322
+ const blacklistRows = await ctx.database.get(BLACKLIST_MODEL_NAME_V2, {
4323
+ scopeId
4324
+ });
4325
+ const aliasRows = await ctx.database.get(USER_ALIAS_MODEL_NAME_V2, {
4326
+ scopeId
4327
+ });
4328
+ const recordedSnapshots = await readRecordedDashboardSnapshots(ctx, scopeId);
4329
+ const snapshotRows = recordedSnapshots.dashboardSnapshots;
4330
+ const userSnapshotRows = recordedSnapshots.userAffinitySnapshots;
4331
+ const trendAnchor = getLatestSnapshotDate(snapshotRows) || now;
4332
+ let chatCount = 0;
4333
+ let affinityTotal = 0;
4334
+ let longTermTotal = 0;
4335
+ let shortTermTotal = 0;
4336
+ let latestInteractionAt = null;
4337
+ const relationCounts = /* @__PURE__ */ new Map();
4338
+ const affinityByUserId = /* @__PURE__ */ new Map();
4339
+ const userSnapshotsByUserId = groupUserSnapshots(userSnapshotRows);
4340
+ for (const row of affinityRows) {
4341
+ chatCount += toCount(row.chatCount);
4342
+ affinityTotal += toCount(row.affinity);
4343
+ longTermTotal += toCount(row.longTermAffinity ?? row.affinity);
4344
+ shortTermTotal += toCount(row.shortTermAffinity);
4345
+ const relation = getDisplayRelation(row);
4346
+ const kind = getRelationKind(row);
4347
+ const relationKey = `${kind}:${relation}`;
4348
+ const currentRelation = relationCounts.get(relationKey) || {
4349
+ relation,
4350
+ kind,
4351
+ count: 0
4352
+ };
4353
+ currentRelation.count += 1;
4354
+ relationCounts.set(relationKey, currentRelation);
4355
+ affinityByUserId.set(row.userId, toCount(row.affinity));
4356
+ const currentInteractionAt = toIsoString(row.lastInteractionAt);
4357
+ if (currentInteractionAt && (!latestInteractionAt || currentInteractionAt > latestInteractionAt)) {
4358
+ latestInteractionAt = currentInteractionAt;
4359
+ }
4360
+ }
4361
+ const topUsers = [...affinityRows].sort((left, right) => right.affinity - left.affinity).map((row) => ({
4362
+ userId: row.userId,
4363
+ name: getDisplayName(row),
4364
+ avatarUrl: getOneBotAvatarUrl(row.userId),
4365
+ affinity: toCount(row.affinity),
4366
+ longTermAffinity: toCount(row.longTermAffinity ?? row.affinity),
4367
+ relation: getDisplayRelation(row),
4368
+ relationTone: getRelationTone(row, relationshipAffinityLevels),
4369
+ chatCount: toCount(row.chatCount),
4370
+ lastInteractionAt: toIsoString(row.lastInteractionAt),
4371
+ historyPoints: createUserHistoryPoints(
4372
+ row,
4373
+ userSnapshotsByUserId.get(row.userId) || [],
4374
+ trendAnchor
4375
+ )
4376
+ }));
4377
+ const relationStats = [...relationCounts.values()].sort((left, right) => right.count - left.count);
4378
+ const currentWeekEnd = startOfLocalDay(trendAnchor).getTime() + DAY_MS;
4379
+ const currentWeekStart = currentWeekEnd - WEEK_DAYS * DAY_MS;
4380
+ const previousWeekStart = currentWeekStart - WEEK_DAYS * DAY_MS;
4381
+ const currentWeekSnapshot = findLatestSnapshotInWindow(
4382
+ snapshotRows,
4383
+ currentWeekStart,
4384
+ currentWeekEnd
4385
+ );
4386
+ const previousWeekSnapshot = findLatestSnapshotInWindow(
4387
+ snapshotRows,
4388
+ previousWeekStart,
4389
+ currentWeekStart
4390
+ );
4391
+ const blacklistItems = [...blacklistRows].sort((left, right) => {
4392
+ const leftTime = toIsoString(left.blockedAt) || "";
4393
+ const rightTime = toIsoString(right.blockedAt) || "";
4394
+ return rightTime.localeCompare(leftTime);
4395
+ }).map((row) => ({
4396
+ platform: row.platform,
4397
+ userId: row.userId,
4398
+ name: getBlacklistDisplayName(row),
4399
+ avatarUrl: getOneBotAvatarUrl(row.userId),
4400
+ affinity: affinityByUserId.get(row.userId) ?? null,
4401
+ mode: row.mode,
4402
+ blockedAt: toIsoString(row.blockedAt),
4403
+ expiresAt: toIsoString(row.expiresAt),
4404
+ note: row.note || ""
4405
+ }));
4406
+ const permanentBlacklisted = blacklistRows.filter(
4407
+ (row) => row.mode === "permanent"
4408
+ ).length;
4409
+ const temporaryBlacklisted = blacklistRows.filter(
4410
+ (row) => row.mode === "temporary"
4411
+ ).length;
4412
+ return {
4413
+ scopeId,
4414
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4415
+ totals: {
4416
+ users: affinityRows.length,
4417
+ blacklisted: blacklistRows.length,
4418
+ permanentBlacklisted,
4419
+ temporaryBlacklisted,
4420
+ aliases: aliasRows.length,
4421
+ chatCount
4422
+ },
4423
+ averages: {
4424
+ affinity: roundAverage(affinityTotal, affinityRows.length),
4425
+ longTermAffinity: roundAverage(longTermTotal, affinityRows.length),
4426
+ shortTermAffinity: roundAverage(shortTermTotal, affinityRows.length)
4427
+ },
4428
+ latestInteractionAt,
4429
+ weeklyChanges: {
4430
+ users: createMetricChange(
4431
+ currentWeekSnapshot ? snapshotUsers(currentWeekSnapshot) : 0,
4432
+ previousWeekSnapshot ? snapshotUsers(previousWeekSnapshot) : 0
4433
+ ),
4434
+ averageAffinity: createMetricChange(
4435
+ snapshotAverage(currentWeekSnapshot),
4436
+ snapshotAverage(previousWeekSnapshot)
4437
+ ),
4438
+ chatCount: createMetricChange(
4439
+ currentWeekSnapshot ? snapshotChatCount(currentWeekSnapshot) : 0,
4440
+ previousWeekSnapshot ? snapshotChatCount(previousWeekSnapshot) : 0
4441
+ ),
4442
+ aliases: createMetricChange(
4443
+ currentWeekSnapshot ? snapshotAliases(currentWeekSnapshot) : 0,
4444
+ previousWeekSnapshot ? snapshotAliases(previousWeekSnapshot) : 0
4445
+ )
4446
+ },
4447
+ trends: {
4448
+ week: createDailyTrend(snapshotRows, trendAnchor, WEEK_DAYS),
4449
+ month: createDailyTrend(snapshotRows, trendAnchor, MONTH_DAYS),
4450
+ all: createAllTrend(snapshotRows)
4451
+ },
4452
+ relationStats,
4453
+ blacklistItems,
4454
+ topUsers
4455
+ };
4456
+ }
4457
+ function registerDashboardWebui(options) {
4458
+ const { config, ctx, entry, log } = options;
4459
+ if (config.enableDashboard === false) return;
4460
+ ctx.inject(["console"], (innerCtx) => {
4461
+ const consoleService = innerCtx.console;
4462
+ consoleService.addEntry(entry);
4463
+ consoleService.addListener(
4464
+ DASHBOARD_EVENT,
4465
+ async () => getDashboardData(ctx, {
4466
+ scopeId: config.scopeId,
4467
+ relationshipAffinityLevels: config.relationshipAffinityLevels
4468
+ }),
4469
+ { authority: 1 }
4470
+ );
4471
+ if (config.debugLogging) {
4472
+ log("debug", "\u5DF2\u6CE8\u518C\u63A7\u5236\u53F0\u4EEA\u8868\u76D8\u9875\u9762\u4E0E\u6570\u636E\u63A5\u53E3", {
4473
+ event: DASHBOARD_EVENT,
4474
+ scopeId: config.scopeId
4475
+ });
4476
+ }
4477
+ });
4478
+ }
4479
+
3837
4480
  // src/renders/base.ts
3838
4481
  function getPuppeteer(ctx) {
3839
4482
  return ctx.puppeteer || null;
@@ -5211,20 +5854,26 @@ function registerClearAllCommand(deps) {
5211
5854
  await ctx.database.remove(USER_ALIAS_MODEL_NAME_V2, {
5212
5855
  scopeId: config.scopeId
5213
5856
  });
5857
+ await ctx.database.remove(DASHBOARD_SNAPSHOT_MODEL_NAME, {
5858
+ scopeId: config.scopeId
5859
+ });
5860
+ await ctx.database.remove(USER_AFFINITY_SNAPSHOT_MODEL_NAME, {
5861
+ scopeId: config.scopeId
5862
+ });
5214
5863
  cache.clearAll?.();
5215
5864
  log("info", "\u5F53\u524D\u4F5C\u7528\u57DF\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A", {
5216
5865
  scopeId: config.scopeId,
5217
5866
  operator: session.userId,
5218
5867
  platform: session.platform
5219
5868
  });
5220
- return `\u2705 \u5DF2\u6210\u529F\u6E05\u7A7A\u4F5C\u7528\u57DF ${config.scopeId} \u4E0B\u7684\u597D\u611F\u5EA6\u3001\u9ED1\u540D\u5355\u4E0E\u6635\u79F0\u6570\u636E\u3002`;
5869
+ return `\u2705 \u5DF2\u6210\u529F\u6E05\u7A7A\u4F5C\u7528\u57DF ${config.scopeId} \u4E0B\u7684\u597D\u611F\u5EA6\u3001\u9ED1\u540D\u5355\u3001\u6635\u79F0\u4E0E\u8D8B\u52BF\u5FEB\u7167\u6570\u636E\u3002`;
5221
5870
  } catch (error) {
5222
5871
  log("warn", "\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25", error);
5223
5872
  return "\u274C \u6E05\u7A7A\u6570\u636E\u5E93\u65F6\u53D1\u751F\u9519\u8BEF\uFF0C\u8BF7\u67E5\u770B\u65E5\u5FD7\u3002";
5224
5873
  }
5225
5874
  }
5226
5875
  pendingClearConfirmations.set(sessionKey, { expiresAt: now + 60 * 1e3 });
5227
- return `\u26A0\uFE0F \u8B66\u544A\uFF1A\u6B64\u64CD\u4F5C\u5C06\u6C38\u4E45\u5220\u9664\u4F5C\u7528\u57DF ${config.scopeId} \u4E0B\u7684\u597D\u611F\u5EA6\u3001\u9ED1\u540D\u5355\u4E0E\u6635\u79F0\u6570\u636E\uFF0C\u4E14\u65E0\u6CD5\u6062\u590D\uFF01
5876
+ return `\u26A0\uFE0F \u8B66\u544A\uFF1A\u6B64\u64CD\u4F5C\u5C06\u6C38\u4E45\u5220\u9664\u4F5C\u7528\u57DF ${config.scopeId} \u4E0B\u7684\u597D\u611F\u5EA6\u3001\u9ED1\u540D\u5355\u3001\u6635\u79F0\u4E0E\u8D8B\u52BF\u5FEB\u7167\u6570\u636E\uFF0C\u4E14\u65E0\u6CD5\u6062\u590D\uFF01
5228
5877
  \u8BF7\u5728 60 \u79D2\u5185\u4F7F\u7528 \`${buildScopedCommandName(config.scopeId, "clearAll")} -y\` \u6216 \`\u6E05\u7A7A\u597D\u611F\u5EA6 -y\` \u786E\u8BA4\u6267\u884C\u3002`;
5229
5878
  });
5230
5879
  }
@@ -5302,14 +5951,21 @@ function apply(ctx, config) {
5302
5951
  config.initialAffinity = Number.isFinite(config.initialAffinity) ? Number(config.initialAffinity) : BASE_AFFINITY_DEFAULTS.initialAffinity;
5303
5952
  normalizeToolSettings(config);
5304
5953
  registerModels(ctx);
5305
- ctx.inject(["console"], (innerCtx) => {
5306
- const consoleService = innerCtx.console;
5307
- consoleService?.addEntry?.({
5954
+ const log = createLogger(ctx, config);
5955
+ registerDashboardBackend({
5956
+ ctx,
5957
+ config,
5958
+ log
5959
+ });
5960
+ registerDashboardWebui({
5961
+ ctx,
5962
+ config,
5963
+ log,
5964
+ entry: {
5308
5965
  dev: path.resolve(__dirname, "../client/index.ts"),
5309
5966
  prod: path.resolve(__dirname, "../dist")
5310
- });
5967
+ }
5311
5968
  });
5312
- const log = createLogger(ctx, config);
5313
5969
  log("info", runtimeFingerprint);
5314
5970
  log(
5315
5971
  "warn",
@@ -5795,6 +6451,7 @@ var usage = `
5795
6451
  COMMON_STYLE,
5796
6452
  Config,
5797
6453
  ConfigSchema,
6454
+ DASHBOARD_SNAPSHOT_MODEL_NAME,
5798
6455
  DEFAULT_MEMBER_INFO_ITEMS,
5799
6456
  FETCH_CONSTANTS,
5800
6457
  MIGRATION_MODEL_NAME,
@@ -5808,6 +6465,7 @@ var usage = `
5808
6465
  THRESHOLDS,
5809
6466
  TIME_CONSTANTS,
5810
6467
  TIMING_CONSTANTS,
6468
+ USER_AFFINITY_SNAPSHOT_MODEL_NAME,
5811
6469
  USER_ALIAS_MODEL_NAME,
5812
6470
  USER_ALIAS_MODEL_NAME_V2,
5813
6471
  appendActionEntry,
@@ -5852,6 +6510,7 @@ var usage = `
5852
6510
  escapeHtmlForRender,
5853
6511
  extendAffinityModel,
5854
6512
  extendBlacklistModel,
6513
+ extendDashboardSnapshotModel,
5855
6514
  extendMigrationModel,
5856
6515
  extendUserAliasModel,
5857
6516
  fetchGroupMemberIds,