koishi-plugin-chatluna-affinity 0.3.0-beta.2 → 0.3.0-beta.3

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.
Files changed (2) hide show
  1. package/lib/index.js +198 -185
  2. package/package.json +6 -8
package/lib/index.js CHANGED
@@ -1284,6 +1284,190 @@ async function fetchGroupMemberIds(session, log) {
1284
1284
  }
1285
1285
  }
1286
1286
 
1287
+ // src/services/affinity/calculator.ts
1288
+ function normalizeAction(action) {
1289
+ const text = typeof action === "string" ? action.toLowerCase() : "";
1290
+ if (text === "increase" || text === "decrease") return text;
1291
+ return "increase";
1292
+ }
1293
+ function resolveShortTermConfig(config) {
1294
+ const cfg = config?.affinityDynamics?.shortTerm || {};
1295
+ const defaults = AFFINITY_DYNAMICS_DEFAULTS.shortTerm;
1296
+ const promoteRaw = Number(cfg.promoteThreshold);
1297
+ const demoteRaw = Number(cfg.demoteThreshold);
1298
+ let promoteThreshold = Number.isFinite(promoteRaw) ? Math.round(promoteRaw) : defaults.promoteThreshold;
1299
+ let demoteThreshold = Number.isFinite(demoteRaw) ? Math.round(demoteRaw) : defaults.demoteThreshold;
1300
+ if (promoteThreshold <= demoteThreshold) {
1301
+ const midpoint = Math.round((promoteThreshold + demoteThreshold) / 2) || 0;
1302
+ promoteThreshold = midpoint + 15;
1303
+ demoteThreshold = midpoint - 15;
1304
+ }
1305
+ const promoteStepRaw = Number(cfg.longTermPromoteStep);
1306
+ const demoteStepRaw = Number(cfg.longTermDemoteStep);
1307
+ const longTermPromoteStep = Math.max(
1308
+ 1,
1309
+ Math.round(
1310
+ Math.abs(
1311
+ Number.isFinite(promoteStepRaw) ? promoteStepRaw : Number.isFinite(cfg.longTermStep) ? cfg.longTermStep : defaults.longTermPromoteStep
1312
+ )
1313
+ )
1314
+ );
1315
+ const longTermDemoteStep = Math.max(
1316
+ 1,
1317
+ Math.round(
1318
+ Math.abs(
1319
+ Number.isFinite(demoteStepRaw) ? demoteStepRaw : Number.isFinite(cfg.longTermStep) ? cfg.longTermStep : defaults.longTermDemoteStep
1320
+ )
1321
+ )
1322
+ );
1323
+ return {
1324
+ promoteThreshold,
1325
+ demoteThreshold,
1326
+ longTermPromoteStep,
1327
+ longTermDemoteStep
1328
+ };
1329
+ }
1330
+ function resolveActionWindowConfig(config) {
1331
+ const cfg = config?.affinityDynamics?.actionWindow || {};
1332
+ const defaults = AFFINITY_DYNAMICS_DEFAULTS.actionWindow;
1333
+ const windowHoursRaw = Number(cfg.windowHours);
1334
+ const windowHours = Math.max(
1335
+ 1,
1336
+ Number.isFinite(windowHoursRaw) ? windowHoursRaw : defaults.windowHours
1337
+ );
1338
+ const increaseBonus = Number.isFinite(cfg.increaseBonus) ? cfg.increaseBonus : defaults.increaseBonus;
1339
+ const decreaseBonus = Number.isFinite(cfg.decreaseBonus) ? cfg.decreaseBonus : defaults.decreaseBonus;
1340
+ const bonusChatThresholdRaw = Number(cfg.bonusChatThreshold);
1341
+ const bonusChatThreshold = Math.max(
1342
+ 0,
1343
+ Number.isFinite(bonusChatThresholdRaw) ? Math.round(bonusChatThresholdRaw) : defaults.bonusChatThreshold
1344
+ );
1345
+ const maxEntriesRaw = Number(cfg.maxEntries);
1346
+ const maxEntries = Math.max(
1347
+ 10,
1348
+ Number.isFinite(maxEntriesRaw) ? Math.round(maxEntriesRaw) : defaults.maxEntries
1349
+ );
1350
+ return {
1351
+ windowHours,
1352
+ windowMs: windowHours * 3600 * 1e3,
1353
+ increaseBonus,
1354
+ decreaseBonus,
1355
+ bonusChatThreshold,
1356
+ maxEntries
1357
+ };
1358
+ }
1359
+ function resolveCoefficientConfig(config) {
1360
+ const cfg = config?.affinityDynamics?.coefficient || {};
1361
+ const defaults = AFFINITY_DYNAMICS_DEFAULTS.coefficient;
1362
+ const base = Number.isFinite(cfg.base) ? cfg.base : defaults.base;
1363
+ const maxDrop = Math.max(
1364
+ 0,
1365
+ Number.isFinite(cfg.maxDrop) ? cfg.maxDrop : defaults.maxDrop
1366
+ );
1367
+ const maxBoost = Math.max(
1368
+ 0,
1369
+ Number.isFinite(cfg.maxBoost) ? cfg.maxBoost : defaults.maxBoost
1370
+ );
1371
+ const decayPerDay = Math.max(
1372
+ 0,
1373
+ Number.isFinite(cfg.decayPerDay) ? cfg.decayPerDay : defaults.decayPerDay
1374
+ );
1375
+ const boostPerDay = Math.max(
1376
+ 0,
1377
+ Number.isFinite(cfg.boostPerDay) ? cfg.boostPerDay : defaults.boostPerDay
1378
+ );
1379
+ const min = base - maxDrop;
1380
+ const max = base + maxBoost;
1381
+ return { base, maxDrop, maxBoost, decayPerDay, boostPerDay, min, max };
1382
+ }
1383
+ function summarizeActionEntries(rawEntries, windowMs, nowMs) {
1384
+ const fallback = {
1385
+ entries: [],
1386
+ counts: { increase: 0, decrease: 0 },
1387
+ total: 0
1388
+ };
1389
+ if (!Array.isArray(rawEntries)) return fallback;
1390
+ const cutoff = nowMs - windowMs;
1391
+ const entries = [];
1392
+ const counts = { increase: 0, decrease: 0 };
1393
+ for (const entry of rawEntries) {
1394
+ if (!entry) continue;
1395
+ const ts = Number(entry.timestamp);
1396
+ if (!Number.isFinite(ts) || ts < cutoff) continue;
1397
+ const normalizedAction = normalizeAction(entry.action);
1398
+ entries.push({ action: normalizedAction, timestamp: ts });
1399
+ counts[normalizedAction] += 1;
1400
+ }
1401
+ return { entries, counts, total: counts.increase + counts.decrease };
1402
+ }
1403
+ function appendActionEntry(entries, action, nowMs, maxEntries) {
1404
+ const next = Array.isArray(entries) ? [...entries] : [];
1405
+ next.push({ action: normalizeAction(action), timestamp: nowMs });
1406
+ if (next.length > maxEntries) next.splice(0, next.length - maxEntries);
1407
+ return next;
1408
+ }
1409
+ function computeShortTermReset() {
1410
+ return 0;
1411
+ }
1412
+ function dayNumber2(date) {
1413
+ return Math.floor(date.getTime() / 864e5);
1414
+ }
1415
+ function computeDailyStreak(previousStreak, lastInteractionAt, now) {
1416
+ const last = lastInteractionAt instanceof Date ? lastInteractionAt : null;
1417
+ const currentDay = dayNumber2(now);
1418
+ const previousDay = last ? dayNumber2(last) : null;
1419
+ if (previousDay === null) return 1;
1420
+ if (previousDay === currentDay) return Math.max(1, previousStreak || 1);
1421
+ if (previousDay === currentDay - 1)
1422
+ return Math.max(1, (previousStreak || 0) + 1);
1423
+ return 1;
1424
+ }
1425
+ function computeCoefficientValue(coefConfig, streak, lastInteractionAt, now, todayIncreaseCount = 0, todayDecreaseCount = 0) {
1426
+ const lastMs = lastInteractionAt instanceof Date ? lastInteractionAt.getTime() : null;
1427
+ const nowMs = now.getTime();
1428
+ const inactivityDays = lastMs ? Math.max(0, Math.floor((nowMs - lastMs) / 864e5)) : 0;
1429
+ const hasInteractedToday = inactivityDays === 0;
1430
+ const isPositiveDay = todayIncreaseCount > todayDecreaseCount;
1431
+ const isNegativeDay = todayDecreaseCount > todayIncreaseCount;
1432
+ let decayPenalty = 0;
1433
+ let streakBoost = 0;
1434
+ if (!hasInteractedToday || isNegativeDay) {
1435
+ if (!hasInteractedToday) {
1436
+ decayPenalty = Math.min(
1437
+ coefConfig.maxDrop,
1438
+ inactivityDays * coefConfig.decayPerDay
1439
+ );
1440
+ } else if (isNegativeDay) {
1441
+ decayPenalty = Math.min(coefConfig.maxDrop, coefConfig.decayPerDay);
1442
+ }
1443
+ }
1444
+ if (hasInteractedToday && isPositiveDay && streak > 0) {
1445
+ streakBoost = Math.min(
1446
+ coefConfig.maxBoost,
1447
+ Math.max(0, (Math.max(1, streak) - 1) * coefConfig.boostPerDay)
1448
+ );
1449
+ }
1450
+ const coefficient = clampFloat(
1451
+ coefConfig.base - decayPenalty + streakBoost,
1452
+ coefConfig.min,
1453
+ coefConfig.max
1454
+ );
1455
+ return { coefficient, decayPenalty, streakBoost, inactivityDays };
1456
+ }
1457
+ function composeState(longTerm, shortTerm, clampFn) {
1458
+ return {
1459
+ affinity: clampFn(longTerm),
1460
+ longTermAffinity: clampFn(longTerm),
1461
+ shortTermAffinity: Math.round(shortTerm)
1462
+ };
1463
+ }
1464
+ function formatActionCounts(counts) {
1465
+ const safe = counts || {};
1466
+ const increase = Number(safe.increase) || 0;
1467
+ const decrease = Number(safe.decrease) || 0;
1468
+ return `\u63D0\u5347 ${increase} / \u964D\u4F4E ${decrease}`;
1469
+ }
1470
+
1287
1471
  // src/services/affinity/store.ts
1288
1472
  function createAffinityStore(options) {
1289
1473
  const { ctx, config } = options;
@@ -1513,6 +1697,18 @@ function createAffinityStore(options) {
1513
1697
  );
1514
1698
  const existing = await load(scopeId, normalizedUserId);
1515
1699
  if (!existing) return null;
1700
+ const parsedCoefficientState = extractState(existing).coefficientState;
1701
+ const now = /* @__PURE__ */ new Date();
1702
+ const nextStreak = computeDailyStreak(
1703
+ parsedCoefficientState?.streak,
1704
+ parsedCoefficientState?.lastInteractionAt || existing.lastInteractionAt,
1705
+ now
1706
+ );
1707
+ const nextCoefficientState = {
1708
+ ...parsedCoefficientState,
1709
+ streak: nextStreak,
1710
+ lastInteractionAt: now
1711
+ };
1516
1712
  return save(
1517
1713
  { ...seed, scopeId, userId: normalizedUserId },
1518
1714
  Number.NaN,
@@ -1521,7 +1717,8 @@ function createAffinityStore(options) {
1521
1717
  longTermAffinity: existing.longTermAffinity ?? existing.affinity,
1522
1718
  shortTermAffinity: existing.shortTermAffinity ?? 0,
1523
1719
  chatCount: Math.max(0, Number(existing.chatCount || 0)) + 1,
1524
- lastInteractionAt: /* @__PURE__ */ new Date()
1720
+ coefficientState: nextCoefficientState,
1721
+ lastInteractionAt: now
1525
1722
  }
1526
1723
  );
1527
1724
  };
@@ -1562,190 +1759,6 @@ function createAffinityCache() {
1562
1759
  };
1563
1760
  }
1564
1761
 
1565
- // src/services/affinity/calculator.ts
1566
- function normalizeAction(action) {
1567
- const text = typeof action === "string" ? action.toLowerCase() : "";
1568
- if (text === "increase" || text === "decrease") return text;
1569
- return "increase";
1570
- }
1571
- function resolveShortTermConfig(config) {
1572
- const cfg = config?.affinityDynamics?.shortTerm || {};
1573
- const defaults = AFFINITY_DYNAMICS_DEFAULTS.shortTerm;
1574
- const promoteRaw = Number(cfg.promoteThreshold);
1575
- const demoteRaw = Number(cfg.demoteThreshold);
1576
- let promoteThreshold = Number.isFinite(promoteRaw) ? Math.round(promoteRaw) : defaults.promoteThreshold;
1577
- let demoteThreshold = Number.isFinite(demoteRaw) ? Math.round(demoteRaw) : defaults.demoteThreshold;
1578
- if (promoteThreshold <= demoteThreshold) {
1579
- const midpoint = Math.round((promoteThreshold + demoteThreshold) / 2) || 0;
1580
- promoteThreshold = midpoint + 15;
1581
- demoteThreshold = midpoint - 15;
1582
- }
1583
- const promoteStepRaw = Number(cfg.longTermPromoteStep);
1584
- const demoteStepRaw = Number(cfg.longTermDemoteStep);
1585
- const longTermPromoteStep = Math.max(
1586
- 1,
1587
- Math.round(
1588
- Math.abs(
1589
- Number.isFinite(promoteStepRaw) ? promoteStepRaw : Number.isFinite(cfg.longTermStep) ? cfg.longTermStep : defaults.longTermPromoteStep
1590
- )
1591
- )
1592
- );
1593
- const longTermDemoteStep = Math.max(
1594
- 1,
1595
- Math.round(
1596
- Math.abs(
1597
- Number.isFinite(demoteStepRaw) ? demoteStepRaw : Number.isFinite(cfg.longTermStep) ? cfg.longTermStep : defaults.longTermDemoteStep
1598
- )
1599
- )
1600
- );
1601
- return {
1602
- promoteThreshold,
1603
- demoteThreshold,
1604
- longTermPromoteStep,
1605
- longTermDemoteStep
1606
- };
1607
- }
1608
- function resolveActionWindowConfig(config) {
1609
- const cfg = config?.affinityDynamics?.actionWindow || {};
1610
- const defaults = AFFINITY_DYNAMICS_DEFAULTS.actionWindow;
1611
- const windowHoursRaw = Number(cfg.windowHours);
1612
- const windowHours = Math.max(
1613
- 1,
1614
- Number.isFinite(windowHoursRaw) ? windowHoursRaw : defaults.windowHours
1615
- );
1616
- const increaseBonus = Number.isFinite(cfg.increaseBonus) ? cfg.increaseBonus : defaults.increaseBonus;
1617
- const decreaseBonus = Number.isFinite(cfg.decreaseBonus) ? cfg.decreaseBonus : defaults.decreaseBonus;
1618
- const bonusChatThresholdRaw = Number(cfg.bonusChatThreshold);
1619
- const bonusChatThreshold = Math.max(
1620
- 0,
1621
- Number.isFinite(bonusChatThresholdRaw) ? Math.round(bonusChatThresholdRaw) : defaults.bonusChatThreshold
1622
- );
1623
- const maxEntriesRaw = Number(cfg.maxEntries);
1624
- const maxEntries = Math.max(
1625
- 10,
1626
- Number.isFinite(maxEntriesRaw) ? Math.round(maxEntriesRaw) : defaults.maxEntries
1627
- );
1628
- return {
1629
- windowHours,
1630
- windowMs: windowHours * 3600 * 1e3,
1631
- increaseBonus,
1632
- decreaseBonus,
1633
- bonusChatThreshold,
1634
- maxEntries
1635
- };
1636
- }
1637
- function resolveCoefficientConfig(config) {
1638
- const cfg = config?.affinityDynamics?.coefficient || {};
1639
- const defaults = AFFINITY_DYNAMICS_DEFAULTS.coefficient;
1640
- const base = Number.isFinite(cfg.base) ? cfg.base : defaults.base;
1641
- const maxDrop = Math.max(
1642
- 0,
1643
- Number.isFinite(cfg.maxDrop) ? cfg.maxDrop : defaults.maxDrop
1644
- );
1645
- const maxBoost = Math.max(
1646
- 0,
1647
- Number.isFinite(cfg.maxBoost) ? cfg.maxBoost : defaults.maxBoost
1648
- );
1649
- const decayPerDay = Math.max(
1650
- 0,
1651
- Number.isFinite(cfg.decayPerDay) ? cfg.decayPerDay : defaults.decayPerDay
1652
- );
1653
- const boostPerDay = Math.max(
1654
- 0,
1655
- Number.isFinite(cfg.boostPerDay) ? cfg.boostPerDay : defaults.boostPerDay
1656
- );
1657
- const min = base - maxDrop;
1658
- const max = base + maxBoost;
1659
- return { base, maxDrop, maxBoost, decayPerDay, boostPerDay, min, max };
1660
- }
1661
- function summarizeActionEntries(rawEntries, windowMs, nowMs) {
1662
- const fallback = {
1663
- entries: [],
1664
- counts: { increase: 0, decrease: 0 },
1665
- total: 0
1666
- };
1667
- if (!Array.isArray(rawEntries)) return fallback;
1668
- const cutoff = nowMs - windowMs;
1669
- const entries = [];
1670
- const counts = { increase: 0, decrease: 0 };
1671
- for (const entry of rawEntries) {
1672
- if (!entry) continue;
1673
- const ts = Number(entry.timestamp);
1674
- if (!Number.isFinite(ts) || ts < cutoff) continue;
1675
- const normalizedAction = normalizeAction(entry.action);
1676
- entries.push({ action: normalizedAction, timestamp: ts });
1677
- counts[normalizedAction] += 1;
1678
- }
1679
- return { entries, counts, total: counts.increase + counts.decrease };
1680
- }
1681
- function appendActionEntry(entries, action, nowMs, maxEntries) {
1682
- const next = Array.isArray(entries) ? [...entries] : [];
1683
- next.push({ action: normalizeAction(action), timestamp: nowMs });
1684
- if (next.length > maxEntries) next.splice(0, next.length - maxEntries);
1685
- return next;
1686
- }
1687
- function computeShortTermReset() {
1688
- return 0;
1689
- }
1690
- function dayNumber2(date) {
1691
- return Math.floor(date.getTime() / 864e5);
1692
- }
1693
- function computeDailyStreak(previousStreak, lastInteractionAt, now) {
1694
- const last = lastInteractionAt instanceof Date ? lastInteractionAt : null;
1695
- const currentDay = dayNumber2(now);
1696
- const previousDay = last ? dayNumber2(last) : null;
1697
- if (previousDay === null) return 1;
1698
- if (previousDay === currentDay) return Math.max(1, previousStreak || 1);
1699
- if (previousDay === currentDay - 1)
1700
- return Math.max(1, (previousStreak || 0) + 1);
1701
- return 1;
1702
- }
1703
- function computeCoefficientValue(coefConfig, streak, lastInteractionAt, now, todayIncreaseCount = 0, todayDecreaseCount = 0) {
1704
- const lastMs = lastInteractionAt instanceof Date ? lastInteractionAt.getTime() : null;
1705
- const nowMs = now.getTime();
1706
- const inactivityDays = lastMs ? Math.max(0, Math.floor((nowMs - lastMs) / 864e5)) : 0;
1707
- const hasInteractedToday = inactivityDays === 0;
1708
- const isPositiveDay = todayIncreaseCount > todayDecreaseCount;
1709
- const isNegativeDay = todayDecreaseCount > todayIncreaseCount;
1710
- let decayPenalty = 0;
1711
- let streakBoost = 0;
1712
- if (!hasInteractedToday || isNegativeDay) {
1713
- if (!hasInteractedToday) {
1714
- decayPenalty = Math.min(
1715
- coefConfig.maxDrop,
1716
- inactivityDays * coefConfig.decayPerDay
1717
- );
1718
- } else if (isNegativeDay) {
1719
- decayPenalty = Math.min(coefConfig.maxDrop, coefConfig.decayPerDay);
1720
- }
1721
- }
1722
- if (hasInteractedToday && isPositiveDay && streak > 0) {
1723
- streakBoost = Math.min(
1724
- coefConfig.maxBoost,
1725
- Math.max(0, (Math.max(1, streak) - 1) * coefConfig.boostPerDay)
1726
- );
1727
- }
1728
- const coefficient = clampFloat(
1729
- coefConfig.base - decayPenalty + streakBoost,
1730
- coefConfig.min,
1731
- coefConfig.max
1732
- );
1733
- return { coefficient, decayPenalty, streakBoost, inactivityDays };
1734
- }
1735
- function composeState(longTerm, shortTerm, clampFn) {
1736
- return {
1737
- affinity: clampFn(longTerm),
1738
- longTermAffinity: clampFn(longTerm),
1739
- shortTermAffinity: Math.round(shortTerm)
1740
- };
1741
- }
1742
- function formatActionCounts(counts) {
1743
- const safe = counts || {};
1744
- const increase = Number(safe.increase) || 0;
1745
- const decrease = Number(safe.decrease) || 0;
1746
- return `\u63D0\u5347 ${increase} / \u964D\u4F4E ${decrease}`;
1747
- }
1748
-
1749
1762
  // src/services/message/history.ts
1750
1763
  function createMessageHistory(options) {
1751
1764
  const { ctx, config, log } = options;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "koishi-plugin-chatluna-affinity",
3
3
  "description": "ChatLuna Character 好感度系统,提供 {好感度}、{关系}、{黑名单} 变量与工具,并支持 XML 工具调用,仅支持 onebot 平台。",
4
- "version": "0.3.0-beta.2",
4
+ "version": "0.3.0-beta.3",
5
5
  "main": "lib/index.js",
6
- "typings": "lib/index.d.ts",
6
+ "types": "lib/index.d.ts",
7
7
  "files": [
8
8
  "lib",
9
9
  "dist",
@@ -43,20 +43,18 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@koishijs/client": "^5.30.0",
46
- "@types/node": "^20.11.0",
46
+ "@types/node": "^22.10.1",
47
47
  "@vitejs/plugin-vue": "^6.0.2",
48
48
  "rimraf": "^5.0.5",
49
49
  "sass": "^1.77.0",
50
50
  "tsup": "^8.0.0",
51
- "typescript": "^5.3.3",
51
+ "typescript": "^5.7.2",
52
52
  "vite": "^6.0.0"
53
53
  },
54
- "dependencies": {
55
- "koishi-plugin-chatluna": "*"
56
- },
57
54
  "scripts": {
58
55
  "build": "tsup && vite build",
59
56
  "watch": "tsup --watch",
60
- "clean": "rimraf lib dist"
57
+ "clean": "rimraf lib dist",
58
+ "test": "node --test --test-name-pattern='.*' test/*.test.js"
61
59
  }
62
60
  }