luckytri 1.0.4 → 1.0.6

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 (156) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/en/CHANGELOG.md +20 -13
  5. package/docs/en/guide.md +7 -5
  6. package/docs/en/her-life.md +2 -2
  7. package/docs/en/webui.md +1 -1
  8. package/docs/zh/CHANGELOG.md +20 -13
  9. package/docs/zh/guide.md +7 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +2 -1
  13. package/public/app/assets/ChatsPage-CFMVozoG.css +1 -0
  14. package/public/app/assets/{ChatsPage-CshdtTzV.js → ChatsPage-DCYMkJ91.js} +2 -2
  15. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-CLyZkxR5.js} +1 -1
  16. package/public/app/assets/{Empty-DmUB2-Bk.css → Empty-DIobYG3x.css} +1 -1
  17. package/public/app/assets/{Field-BjFv4Rfn.js → Field-BpaRwLI_.js} +1 -1
  18. package/public/app/assets/{HeartPage-eqo6ru16.js → HeartPage-BIOMTiwr.js} +1 -1
  19. package/public/app/assets/HeartPage-C-2jvgzF.css +1 -0
  20. package/public/app/assets/{LifePage-CN8GE5NK.js → LifePage-BCgM4JkR.js} +1 -1
  21. package/public/app/assets/LifePage-CG1OHbRK.css +1 -0
  22. package/public/app/assets/MemoryPage-C_Bu4rfb.css +1 -0
  23. package/public/app/assets/{MemoryPage-CCpMN4Wg.js → MemoryPage-vShvRqMt.js} +1 -1
  24. package/public/app/assets/NaturePage-CPZlImT0.css +1 -0
  25. package/public/app/assets/{NaturePage-C0JqdBTR.js → NaturePage-Cy9ptr4G.js} +2 -2
  26. package/public/app/assets/PeoplePage-3RRUITDz.css +1 -0
  27. package/public/app/assets/{PeoplePage-BXGBbO4u.js → PeoplePage-CUZekB2E.js} +1 -1
  28. package/public/app/assets/{PluginsPage-Dksp5AV5.js → PluginsPage-1RM4rmJP.js} +1 -1
  29. package/public/app/assets/{SystemPage-BenIyTvk.css → SystemPage-BI2CB5Ig.css} +1 -1
  30. package/public/app/assets/{SystemPage-Cd7aa8eE.js → SystemPage-BeT5TmIV.js} +1 -1
  31. package/public/app/assets/TimePage-C-guwPk2.css +1 -0
  32. package/public/app/assets/{TimePage-Bs_zImb4.js → TimePage-taTmPMM8.js} +2 -2
  33. package/public/app/assets/{index-BHZcHUMh.css → index-DY5AM0Hz.css} +1 -1
  34. package/public/app/assets/{index-CyRtYxP3.js → index-DdYwC5ZU.js} +3 -3
  35. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-DJmfu2KN.js} +1 -1
  36. package/public/app/assets/{media-Bwtx8blz.js → media-BEtnE0Gv.js} +1 -1
  37. package/public/app/assets/{mind-DtTVrW6Z.js → mind-Dtiw0-QK.js} +1 -1
  38. package/public/app/index.html +6 -5
  39. package/public/guide.en.html +11 -3
  40. package/public/guide.html +6 -3
  41. package/public/tutorial.en.md +2 -0
  42. package/public/tutorial.md +2 -0
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/service-supervisor.js +50 -0
  46. package/scripts/start-server.js +71 -16
  47. package/scripts/update-instance.js +201 -201
  48. package/server/channels/adapters.js +27 -27
  49. package/server/channels/onebot/adapter.js +288 -288
  50. package/server/channels/qqbot/adapter.js +253 -253
  51. package/server/channels/qqbot/api.js +199 -199
  52. package/server/channels/qqbot/credentials.js +11 -11
  53. package/server/channels/qqbot/gateway.js +274 -274
  54. package/server/channels/qqbot/index.js +389 -389
  55. package/server/channels/segments.js +40 -40
  56. package/server/channels/session-key.js +153 -153
  57. package/server/channels/state.js +42 -42
  58. package/server/core/api.js +564 -564
  59. package/server/core/context-builder.js +288 -288
  60. package/server/core/context-compactor.js +454 -454
  61. package/server/core/conversation-cues.js +325 -325
  62. package/server/core/conversation-manager.js +87 -87
  63. package/server/core/feedback.js +8 -8
  64. package/server/core/group-style.js +72 -72
  65. package/server/core/initiative-context.js +66 -66
  66. package/server/core/input-budget.js +32 -32
  67. package/server/core/local-demo.js +98 -98
  68. package/server/core/message-manager.js +118 -118
  69. package/server/core/message-scheduler.js +78 -78
  70. package/server/core/model-json.js +190 -190
  71. package/server/core/model-presets.js +900 -900
  72. package/server/core/name-call.js +29 -29
  73. package/server/core/network.js +157 -157
  74. package/server/core/person-name.js +22 -22
  75. package/server/core/persona-manager.js +2 -1
  76. package/server/core/reply-delivery.js +404 -403
  77. package/server/core/reply-target-resolver.js +50 -50
  78. package/server/core/response-generator.js +50 -50
  79. package/server/core/response-validator.js +14 -2
  80. package/server/core/retired-prompts.js +51 -51
  81. package/server/core/sessions.js +109 -109
  82. package/server/core/speaker-names.js +92 -92
  83. package/server/core/topic-tracker.js +65 -65
  84. package/server/http.js +18 -18
  85. package/server/i18n/decisions.js +209 -206
  86. package/server/i18n/errors.js +4 -0
  87. package/server/i18n/index.js +144 -144
  88. package/server/i18n/models.js +111 -111
  89. package/server/knowledge/api.js +342 -342
  90. package/server/knowledge/embedding-profile.js +114 -114
  91. package/server/knowledge/index.js +13 -13
  92. package/server/knowledge/retrieval.js +93 -93
  93. package/server/knowledge/schema.js +158 -158
  94. package/server/knowledge/vector-index.js +90 -90
  95. package/server/mind/affect.js +265 -265
  96. package/server/mind/attention.js +200 -200
  97. package/server/mind/bonds.js +565 -565
  98. package/server/mind/clock.js +83 -83
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +60 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -323
  108. package/server/mind/life/diary.js +20 -0
  109. package/server/mind/life/growth.js +261 -254
  110. package/server/mind/life/index.js +10 -2
  111. package/server/mind/life/presence.js +305 -305
  112. package/server/mind/meetings.js +1069 -1069
  113. package/server/mind/memory-policy.js +48 -48
  114. package/server/mind/memory.js +888 -888
  115. package/server/mind/nature.js +358 -358
  116. package/server/mind/novelty.js +19 -19
  117. package/server/mind/periods.js +118 -118
  118. package/server/mind/reading.js +120 -120
  119. package/server/mind/relationship-context.js +71 -0
  120. package/server/mind/relationships.js +390 -279
  121. package/server/mind/salience.js +201 -201
  122. package/server/mind/schema.js +567 -567
  123. package/server/mind/self.js +484 -484
  124. package/server/mind/thoughts.js +274 -274
  125. package/server/mind/time/agenda.js +242 -242
  126. package/server/mind/time/api.js +200 -200
  127. package/server/mind/time/attention.js +34 -34
  128. package/server/mind/time/delivery-links.js +130 -130
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +51 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +432 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +9 -1
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-execution.js +2 -1
  141. package/server/runtime/update-manager.js +140 -140
  142. package/server/storage/data-package.js +298 -298
  143. package/server/storage/data-transfer-api.js +50 -50
  144. package/server/storage/data-transfer-worker.js +18 -18
  145. package/server/storage/data-transfer.js +416 -416
  146. package/server/studio/display-names.js +139 -139
  147. package/server/studio/management.js +102 -102
  148. package/server/studio/updates.js +144 -144
  149. package/server/version.js +10 -10
  150. package/public/app/assets/ChatsPage-DfyPCS9N.css +0 -1
  151. package/public/app/assets/HeartPage-Bft9wZp5.css +0 -1
  152. package/public/app/assets/LifePage-BUi5euBF.css +0 -1
  153. package/public/app/assets/MemoryPage-cZvXfkXb.css +0 -1
  154. package/public/app/assets/NaturePage-CHkpb5NK.css +0 -1
  155. package/public/app/assets/PeoplePage-BTU0H1pM.css +0 -1
  156. package/public/app/assets/TimePage-BOms_n4I.css +0 -1
@@ -1,265 +1,265 @@
1
- import { randomUUID } from "node:crypto";
2
- import { evidenceRoots } from "./evidence.js";
3
- import { isPrivateSession } from "./memory.js";
4
- import { rhythmPhase } from "./nature.js";
5
- import { DAY, HOUR, clamp, evidence, parse, relax, text } from "./util.js";
6
-
7
- const HALF_LIFE = 3 * HOUR;
8
- const BASELINE = { valence: 0.15, arousal: 0.35 };
9
- // A single experience can move her, but not overturn her.
10
- const MAX_SHIFT = 0.35;
11
- // How the last couple of weeks went moves where she settles back to, a
12
- // little: a good stretch lifts it, a hard one lowers it.
13
- const LATELY_DAYS = 14;
14
- const LATELY_HALF_LIFE = 7;
15
- const LATELY_MAX = 0.12;
16
- const LATELY_NOTE = 0.06;
17
-
18
- // A mood is a few words ("挂心", "有点累"), not a sentence. Models sometimes
19
- // answer with the whole thought; take the first clause when it is short
20
- // enough to be a mood, otherwise there is no word to keep.
21
- const MOOD_MAX = 8;
22
-
23
- export function moodWord(raw) {
24
- const clause =
25
- String(raw ?? "")
26
- .split(/[,,。.!!??;;、::\s…]+/)
27
- .find(Boolean) || "";
28
- return Array.from(clause).length <= MOOD_MAX ? clause : "";
29
- }
30
-
31
- function moodLabel(valence, arousal) {
32
- if (valence > 0.45) return arousal > 0.5 ? "很开心" : "心情不错";
33
- if (valence > 0.22) return "挺好";
34
- if (valence > -0.12) return arousal < 0.25 ? "安静" : "平静";
35
- if (valence > -0.4) return arousal > 0.5 ? "有点烦" : "有点低落";
36
- return arousal > 0.5 ? "很烦躁" : "不太好";
37
- }
38
-
39
- export function energyLabel(energy) {
40
- if (energy >= 0.68) return "有精神";
41
- if (energy >= 0.45) return "还行";
42
- if (energy >= 0.25) return "有点累";
43
- return "很困";
44
- }
45
-
46
- export class Affect {
47
- constructor(mind) {
48
- this.mind = mind;
49
- this.db = mind.db;
50
- }
51
- feel({
52
- feeling,
53
- intensity = 0.3,
54
- valence = 0,
55
- arousal,
56
- cause = "",
57
- sources = [],
58
- session = null,
59
- origin = "turn",
60
- time = Date.now(),
61
- }) {
62
- if (!text(feeling, 120)) return null;
63
- const cited = evidence(sources);
64
- // The same messages cannot move her again after they already have.
65
- if (cited.length) {
66
- const spent = new Set();
67
- for (const row of this.db
68
- .prepare(
69
- "SELECT sources FROM mind_affect WHERE created<=? AND created>? AND sources!='[]'",
70
- )
71
- .all(time, time - 2 * DAY))
72
- for (const source of evidenceRoots(
73
- this.db,
74
- parse(row.sources, []),
75
- time,
76
- ))
77
- spent.add(source);
78
- if (
79
- evidenceRoots(this.db, cited, time).every((source) => spent.has(source))
80
- )
81
- return null;
82
- }
83
- const i = clamp(intensity, 0, 1);
84
- const v = clamp(valence, -1, 1);
85
- const a = clamp(arousal ?? i * 0.6, 0, 1);
86
- // A sentence in place of a mood keeps its pull on her, with a plain word.
87
- const label = moodWord(feeling) || moodLabel(v, a);
88
- const id = randomUUID();
89
- this.db
90
- .prepare(
91
- "INSERT INTO mind_affect(id,created,feeling,intensity,valence,arousal,cause,sources,session_id,origin) VALUES (?,?,?,?,?,?,?,?,?,?)",
92
- )
93
- .run(
94
- id,
95
- time,
96
- label,
97
- i,
98
- v,
99
- a,
100
- text(cause, 120),
101
- JSON.stringify(cited),
102
- session,
103
- origin,
104
- );
105
- return id;
106
- }
107
- // Where her mood settles these days, from the finished days she lived. A
108
- // lived day that stirred nothing counts as an ordinary one.
109
- lately(now = Date.now()) {
110
- const days = this.db
111
- .prepare(
112
- "SELECT valence FROM mind_days WHERE created<=? AND lived=1 ORDER BY day DESC LIMIT ?",
113
- )
114
- .all(now, LATELY_DAYS);
115
- if (!days.length) return { drift: 0, label: "" };
116
- let sum = 0;
117
- let weight = 0;
118
- days.forEach((day, i) => {
119
- const w = 0.5 ** (i / LATELY_HALF_LIFE);
120
- sum += (day.valence ?? BASELINE.valence) * w;
121
- weight += w;
122
- });
123
- const settled = Math.min(1, days.length / 5);
124
- const drift = clamp(
125
- (sum / weight - BASELINE.valence) * 0.3 * settled,
126
- -LATELY_MAX,
127
- LATELY_MAX,
128
- );
129
- return {
130
- drift: Math.round(drift * 100) / 100,
131
- label:
132
- drift >= LATELY_NOTE
133
- ? "这阵子挺开心"
134
- : drift <= -LATELY_NOTE
135
- ? "这阵子有点低落"
136
- : "",
137
- };
138
- }
139
- // The mood can follow her into another room. The words of a private
140
- // cause cannot.
141
- #causeHere(row, room) {
142
- if (!room) return true;
143
- if (
144
- row.session_id &&
145
- isPrivateSession(row.session_id) &&
146
- row.session_id !== room
147
- )
148
- return false;
149
- const roots = this.mind.meetings.privateRoots(parse(row.sources, []));
150
- return !roots.length || roots.includes(room);
151
- }
152
- // Folded from events at or before `now`, so a replay never sees later moods.
153
- state(
154
- now = Date.now(),
155
- { nature = this.mind.nature.current(now), room = "" } = {},
156
- ) {
157
- const timeZone = this.mind.timeZone();
158
- const lately = this.lately(now);
159
- const base = {
160
- valence: BASELINE.valence + lately.drift,
161
- arousal: BASELINE.arousal,
162
- };
163
- const rows = this.db
164
- .prepare(
165
- "SELECT * FROM mind_affect WHERE created<=? AND created>? ORDER BY created",
166
- )
167
- .all(now, now - 2 * DAY);
168
- // A feeling with no message behind it is one mood, not a stack.
169
- // Only the latest of each origin counts; earlier ones stay in the record.
170
- const latestBare = new Map();
171
- const bare = (row) => parse(row.sources, []).length === 0;
172
- for (const row of rows) if (bare(row)) latestBare.set(row.origin, row);
173
- const felt = rows.filter(
174
- (row) => !bare(row) || latestBare.get(row.origin) === row,
175
- );
176
- let valence = base.valence;
177
- let arousal = base.arousal;
178
- let at = now - 2 * DAY;
179
- let last = null;
180
- // Several feelings from one moment are one experience. Together they
181
- // still cannot move her further than a single feeling can.
182
- let batchAt = null;
183
- let batchV = 0;
184
- let batchA = 0;
185
- const apply = (when) => {
186
- if (when === null) return;
187
- valence = relax(valence, base.valence, when - at, HALF_LIFE);
188
- arousal = relax(arousal, base.arousal, when - at, HALF_LIFE);
189
- valence = clamp(valence + clamp(batchV, -MAX_SHIFT, MAX_SHIFT), -1, 1);
190
- arousal = clamp(arousal + batchA, 0, 1);
191
- at = when;
192
- batchV = 0;
193
- batchA = 0;
194
- };
195
- for (const row of felt) {
196
- if (batchAt !== null && row.created !== batchAt) apply(batchAt);
197
- batchAt = row.created;
198
- batchV += row.valence * row.intensity;
199
- batchA += (row.arousal - BASELINE.arousal) * row.intensity * 0.5;
200
- last = row;
201
- }
202
- apply(batchAt);
203
- valence = relax(valence, base.valence, now - at, HALF_LIFE);
204
- arousal = relax(arousal, base.arousal, now - at, HALF_LIFE);
205
- const residual = last
206
- ? Math.pow(0.5, (now - last.created) / HALF_LIFE) * last.intensity
207
- : 0;
208
- const phase = rhythmPhase(nature, now, timeZone);
209
- const talked = this.db
210
- .prepare(
211
- "SELECT COUNT(*) n FROM core_events WHERE role='assistant' AND time>? AND time<=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0",
212
- )
213
- .get(now - 2 * HOUR, now).n;
214
- const energy = clamp(
215
- phase.energy +
216
- (arousal - BASELINE.arousal) * 0.3 -
217
- Math.min(0.2, talked * 0.006) -
218
- Math.min(
219
- 0.12,
220
- (this.db
221
- .prepare(
222
- "SELECT COALESCE(SUM(active_ms),0) n FROM mind_time_spans WHERE updated>? AND started<=?",
223
- )
224
- .get(now - 2 * HOUR, now).n /
225
- HOUR) *
226
- 0.035,
227
- ),
228
- 0.05,
229
- 1,
230
- );
231
- const lingering = residual > 0.15;
232
- const cause =
233
- lingering && last && this.#causeHere(last, room) ? last.cause || "" : "";
234
- return {
235
- // Rows saved before the label was kept short are shortened here too.
236
- mood: lingering
237
- ? moodWord(last.feeling) || moodLabel(last.valence, last.arousal)
238
- : moodLabel(valence, arousal),
239
- cause,
240
- valence: Math.round(valence * 100) / 100,
241
- arousal: Math.round(arousal * 100) / 100,
242
- energy: Math.round(energy * 100) / 100,
243
- energyLabel: energyLabel(energy),
244
- phase: phase.key,
245
- phaseLabel: phase.label,
246
- clock: phase.clock,
247
- since: last?.created || null,
248
- talkedRecently: talked,
249
- baseline: Math.round(base.valence * 100) / 100,
250
- lately: lately.label,
251
- };
252
- }
253
- history({ before = Number.MAX_SAFE_INTEGER, limit = 60 } = {}) {
254
- return this.db
255
- .prepare(
256
- "SELECT * FROM mind_affect WHERE created<=? ORDER BY created DESC LIMIT ?",
257
- )
258
- .all(before, limit)
259
- .map((row) => ({
260
- ...row,
261
- feeling: moodWord(row.feeling) || moodLabel(row.valence, row.arousal),
262
- sources: JSON.parse(row.sources),
263
- }));
264
- }
265
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { evidenceRoots } from "./evidence.js";
3
+ import { isPrivateSession } from "./memory.js";
4
+ import { rhythmPhase } from "./nature.js";
5
+ import { DAY, HOUR, clamp, evidence, parse, relax, text } from "./util.js";
6
+
7
+ const HALF_LIFE = 3 * HOUR;
8
+ const BASELINE = { valence: 0.15, arousal: 0.35 };
9
+ // A single experience can move her, but not overturn her.
10
+ const MAX_SHIFT = 0.35;
11
+ // How the last couple of weeks went moves where she settles back to, a
12
+ // little: a good stretch lifts it, a hard one lowers it.
13
+ const LATELY_DAYS = 14;
14
+ const LATELY_HALF_LIFE = 7;
15
+ const LATELY_MAX = 0.12;
16
+ const LATELY_NOTE = 0.06;
17
+
18
+ // A mood is a few words ("挂心", "有点累"), not a sentence. Models sometimes
19
+ // answer with the whole thought; take the first clause when it is short
20
+ // enough to be a mood, otherwise there is no word to keep.
21
+ const MOOD_MAX = 8;
22
+
23
+ export function moodWord(raw) {
24
+ const clause =
25
+ String(raw ?? "")
26
+ .split(/[,,。.!!??;;、::\s…]+/)
27
+ .find(Boolean) || "";
28
+ return Array.from(clause).length <= MOOD_MAX ? clause : "";
29
+ }
30
+
31
+ function moodLabel(valence, arousal) {
32
+ if (valence > 0.45) return arousal > 0.5 ? "很开心" : "心情不错";
33
+ if (valence > 0.22) return "挺好";
34
+ if (valence > -0.12) return arousal < 0.25 ? "安静" : "平静";
35
+ if (valence > -0.4) return arousal > 0.5 ? "有点烦" : "有点低落";
36
+ return arousal > 0.5 ? "很烦躁" : "不太好";
37
+ }
38
+
39
+ export function energyLabel(energy) {
40
+ if (energy >= 0.68) return "有精神";
41
+ if (energy >= 0.45) return "还行";
42
+ if (energy >= 0.25) return "有点累";
43
+ return "很困";
44
+ }
45
+
46
+ export class Affect {
47
+ constructor(mind) {
48
+ this.mind = mind;
49
+ this.db = mind.db;
50
+ }
51
+ feel({
52
+ feeling,
53
+ intensity = 0.3,
54
+ valence = 0,
55
+ arousal,
56
+ cause = "",
57
+ sources = [],
58
+ session = null,
59
+ origin = "turn",
60
+ time = Date.now(),
61
+ }) {
62
+ if (!text(feeling, 120)) return null;
63
+ const cited = evidence(sources);
64
+ // The same messages cannot move her again after they already have.
65
+ if (cited.length) {
66
+ const spent = new Set();
67
+ for (const row of this.db
68
+ .prepare(
69
+ "SELECT sources FROM mind_affect WHERE created<=? AND created>? AND sources!='[]'",
70
+ )
71
+ .all(time, time - 2 * DAY))
72
+ for (const source of evidenceRoots(
73
+ this.db,
74
+ parse(row.sources, []),
75
+ time,
76
+ ))
77
+ spent.add(source);
78
+ if (
79
+ evidenceRoots(this.db, cited, time).every((source) => spent.has(source))
80
+ )
81
+ return null;
82
+ }
83
+ const i = clamp(intensity, 0, 1);
84
+ const v = clamp(valence, -1, 1);
85
+ const a = clamp(arousal ?? i * 0.6, 0, 1);
86
+ // A sentence in place of a mood keeps its pull on her, with a plain word.
87
+ const label = moodWord(feeling) || moodLabel(v, a);
88
+ const id = randomUUID();
89
+ this.db
90
+ .prepare(
91
+ "INSERT INTO mind_affect(id,created,feeling,intensity,valence,arousal,cause,sources,session_id,origin) VALUES (?,?,?,?,?,?,?,?,?,?)",
92
+ )
93
+ .run(
94
+ id,
95
+ time,
96
+ label,
97
+ i,
98
+ v,
99
+ a,
100
+ text(cause, 120),
101
+ JSON.stringify(cited),
102
+ session,
103
+ origin,
104
+ );
105
+ return id;
106
+ }
107
+ // Where her mood settles these days, from the finished days she lived. A
108
+ // lived day that stirred nothing counts as an ordinary one.
109
+ lately(now = Date.now()) {
110
+ const days = this.db
111
+ .prepare(
112
+ "SELECT valence FROM mind_days WHERE created<=? AND lived=1 ORDER BY day DESC LIMIT ?",
113
+ )
114
+ .all(now, LATELY_DAYS);
115
+ if (!days.length) return { drift: 0, label: "" };
116
+ let sum = 0;
117
+ let weight = 0;
118
+ days.forEach((day, i) => {
119
+ const w = 0.5 ** (i / LATELY_HALF_LIFE);
120
+ sum += (day.valence ?? BASELINE.valence) * w;
121
+ weight += w;
122
+ });
123
+ const settled = Math.min(1, days.length / 5);
124
+ const drift = clamp(
125
+ (sum / weight - BASELINE.valence) * 0.3 * settled,
126
+ -LATELY_MAX,
127
+ LATELY_MAX,
128
+ );
129
+ return {
130
+ drift: Math.round(drift * 100) / 100,
131
+ label:
132
+ drift >= LATELY_NOTE
133
+ ? "这阵子挺开心"
134
+ : drift <= -LATELY_NOTE
135
+ ? "这阵子有点低落"
136
+ : "",
137
+ };
138
+ }
139
+ // The mood can follow her into another room. The words of a private
140
+ // cause cannot.
141
+ #causeHere(row, room) {
142
+ if (!room) return true;
143
+ if (
144
+ row.session_id &&
145
+ isPrivateSession(row.session_id) &&
146
+ row.session_id !== room
147
+ )
148
+ return false;
149
+ const roots = this.mind.meetings.privateRoots(parse(row.sources, []));
150
+ return !roots.length || roots.includes(room);
151
+ }
152
+ // Folded from events at or before `now`, so a replay never sees later moods.
153
+ state(
154
+ now = Date.now(),
155
+ { nature = this.mind.nature.current(now), room = "" } = {},
156
+ ) {
157
+ const timeZone = this.mind.timeZone();
158
+ const lately = this.lately(now);
159
+ const base = {
160
+ valence: BASELINE.valence + lately.drift,
161
+ arousal: BASELINE.arousal,
162
+ };
163
+ const rows = this.db
164
+ .prepare(
165
+ "SELECT * FROM mind_affect WHERE created<=? AND created>? ORDER BY created",
166
+ )
167
+ .all(now, now - 2 * DAY);
168
+ // A feeling with no message behind it is one mood, not a stack.
169
+ // Only the latest of each origin counts; earlier ones stay in the record.
170
+ const latestBare = new Map();
171
+ const bare = (row) => parse(row.sources, []).length === 0;
172
+ for (const row of rows) if (bare(row)) latestBare.set(row.origin, row);
173
+ const felt = rows.filter(
174
+ (row) => !bare(row) || latestBare.get(row.origin) === row,
175
+ );
176
+ let valence = base.valence;
177
+ let arousal = base.arousal;
178
+ let at = now - 2 * DAY;
179
+ let last = null;
180
+ // Several feelings from one moment are one experience. Together they
181
+ // still cannot move her further than a single feeling can.
182
+ let batchAt = null;
183
+ let batchV = 0;
184
+ let batchA = 0;
185
+ const apply = (when) => {
186
+ if (when === null) return;
187
+ valence = relax(valence, base.valence, when - at, HALF_LIFE);
188
+ arousal = relax(arousal, base.arousal, when - at, HALF_LIFE);
189
+ valence = clamp(valence + clamp(batchV, -MAX_SHIFT, MAX_SHIFT), -1, 1);
190
+ arousal = clamp(arousal + batchA, 0, 1);
191
+ at = when;
192
+ batchV = 0;
193
+ batchA = 0;
194
+ };
195
+ for (const row of felt) {
196
+ if (batchAt !== null && row.created !== batchAt) apply(batchAt);
197
+ batchAt = row.created;
198
+ batchV += row.valence * row.intensity;
199
+ batchA += (row.arousal - BASELINE.arousal) * row.intensity * 0.5;
200
+ last = row;
201
+ }
202
+ apply(batchAt);
203
+ valence = relax(valence, base.valence, now - at, HALF_LIFE);
204
+ arousal = relax(arousal, base.arousal, now - at, HALF_LIFE);
205
+ const residual = last
206
+ ? Math.pow(0.5, (now - last.created) / HALF_LIFE) * last.intensity
207
+ : 0;
208
+ const phase = rhythmPhase(nature, now, timeZone);
209
+ const talked = this.db
210
+ .prepare(
211
+ "SELECT COUNT(*) n FROM core_events WHERE role='assistant' AND time>? AND time<=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0",
212
+ )
213
+ .get(now - 2 * HOUR, now).n;
214
+ const energy = clamp(
215
+ phase.energy +
216
+ (arousal - BASELINE.arousal) * 0.3 -
217
+ Math.min(0.2, talked * 0.006) -
218
+ Math.min(
219
+ 0.12,
220
+ (this.db
221
+ .prepare(
222
+ "SELECT COALESCE(SUM(active_ms),0) n FROM mind_time_spans WHERE updated>? AND started<=?",
223
+ )
224
+ .get(now - 2 * HOUR, now).n /
225
+ HOUR) *
226
+ 0.035,
227
+ ),
228
+ 0.05,
229
+ 1,
230
+ );
231
+ const lingering = residual > 0.15;
232
+ const cause =
233
+ lingering && last && this.#causeHere(last, room) ? last.cause || "" : "";
234
+ return {
235
+ // Rows saved before the label was kept short are shortened here too.
236
+ mood: lingering
237
+ ? moodWord(last.feeling) || moodLabel(last.valence, last.arousal)
238
+ : moodLabel(valence, arousal),
239
+ cause,
240
+ valence: Math.round(valence * 100) / 100,
241
+ arousal: Math.round(arousal * 100) / 100,
242
+ energy: Math.round(energy * 100) / 100,
243
+ energyLabel: energyLabel(energy),
244
+ phase: phase.key,
245
+ phaseLabel: phase.label,
246
+ clock: phase.clock,
247
+ since: last?.created || null,
248
+ talkedRecently: talked,
249
+ baseline: Math.round(base.valence * 100) / 100,
250
+ lately: lately.label,
251
+ };
252
+ }
253
+ history({ before = Number.MAX_SAFE_INTEGER, limit = 60 } = {}) {
254
+ return this.db
255
+ .prepare(
256
+ "SELECT * FROM mind_affect WHERE created<=? ORDER BY created DESC LIMIT ?",
257
+ )
258
+ .all(before, limit)
259
+ .map((row) => ({
260
+ ...row,
261
+ feeling: moodWord(row.feeling) || moodLabel(row.valence, row.arousal),
262
+ sources: JSON.parse(row.sources),
263
+ }));
264
+ }
265
+ }