luckytri 1.0.6 → 1.0.7
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/LICENSE +21 -21
- package/docs/en/CHANGELOG.md +25 -16
- package/docs/en/chat-style.md +28 -9
- package/docs/en/guide.md +5 -5
- package/docs/en/her-life.md +2 -2
- package/docs/en/webui.md +1 -1
- package/docs/zh/CHANGELOG.md +25 -16
- package/docs/zh/chat-style.md +30 -9
- package/docs/zh/guide.md +5 -5
- package/docs/zh/her-life.md +2 -2
- package/docs/zh/webui.md +1 -1
- package/package.json +1 -1
- package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
- package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
- package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
- package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
- package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
- package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
- package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
- package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
- package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
- package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
- package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
- package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
- package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
- package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
- package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
- package/public/app/index.html +5 -6
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/scripts/import-data.js +128 -128
- package/scripts/proxy-config.js +167 -167
- package/scripts/update-instance.js +201 -201
- package/server/channels/adapters.js +27 -27
- package/server/channels/onebot/adapter.js +288 -288
- package/server/channels/qqbot/adapter.js +253 -253
- package/server/channels/qqbot/api.js +199 -199
- package/server/channels/qqbot/credentials.js +11 -11
- package/server/channels/qqbot/gateway.js +274 -274
- package/server/channels/qqbot/index.js +389 -389
- package/server/channels/segments.js +40 -40
- package/server/channels/session-key.js +153 -153
- package/server/channels/state.js +42 -42
- package/server/core/api.js +564 -564
- package/server/core/context-builder.js +288 -288
- package/server/core/context-compactor.js +454 -454
- package/server/core/conversation-cues.js +365 -325
- package/server/core/conversation-grounding.js +178 -0
- package/server/core/conversation-manager.js +87 -87
- package/server/core/dialogue-context.js +52 -0
- package/server/core/feedback.js +8 -8
- package/server/core/group-style.js +72 -72
- package/server/core/initiative-context.js +66 -66
- package/server/core/input-budget.js +32 -32
- package/server/core/local-demo.js +98 -98
- package/server/core/message-manager.js +118 -118
- package/server/core/message-scheduler.js +78 -78
- package/server/core/model-json.js +190 -190
- package/server/core/model-presets.js +900 -900
- package/server/core/name-call.js +29 -29
- package/server/core/network.js +157 -157
- package/server/core/person-name.js +22 -22
- package/server/core/persona-manager.js +324 -269
- package/server/core/reply-delivery.js +472 -404
- package/server/core/reply-target-resolver.js +50 -50
- package/server/core/response-generator.js +58 -50
- package/server/core/response-validator.js +30 -27
- package/server/core/retired-prompts.js +51 -51
- package/server/core/sessions.js +109 -109
- package/server/core/speaker-names.js +92 -92
- package/server/core/topic-tracker.js +65 -65
- package/server/core/turn.js +42 -8
- package/server/http.js +18 -18
- package/server/i18n/decisions.js +209 -209
- package/server/i18n/index.js +144 -144
- package/server/i18n/labels.js +0 -5
- package/server/i18n/models.js +111 -111
- package/server/knowledge/api.js +342 -342
- package/server/knowledge/embedding-profile.js +114 -114
- package/server/knowledge/index.js +13 -13
- package/server/knowledge/retrieval.js +93 -93
- package/server/knowledge/schema.js +158 -158
- package/server/knowledge/vector-index.js +90 -90
- package/server/mind/affect.js +265 -265
- package/server/mind/attention.js +200 -200
- package/server/mind/bonds.js +565 -565
- package/server/mind/clock.js +83 -83
- package/server/mind/context-selection.js +16 -4
- package/server/mind/continuity.js +562 -562
- package/server/mind/conversation-origin.js +175 -175
- package/server/mind/days.js +210 -210
- package/server/mind/evidence-read.js +45 -45
- package/server/mind/faces.js +373 -373
- package/server/mind/guard.js +68 -60
- package/server/mind/initiative.js +338 -338
- package/server/mind/life/activities.js +21 -21
- package/server/mind/life/context.js +330 -330
- package/server/mind/life/growth.js +261 -261
- package/server/mind/life/presence.js +305 -305
- package/server/mind/meetings.js +1069 -1069
- package/server/mind/memory-policy.js +48 -48
- package/server/mind/memory.js +888 -888
- package/server/mind/nature.js +358 -358
- package/server/mind/novelty.js +19 -19
- package/server/mind/periods.js +118 -118
- package/server/mind/reading.js +120 -120
- package/server/mind/relationship-context.js +71 -71
- package/server/mind/relationships.js +390 -390
- package/server/mind/salience.js +201 -201
- package/server/mind/schema.js +567 -567
- package/server/mind/self.js +496 -484
- package/server/mind/thoughts.js +274 -274
- package/server/mind/time/agenda.js +242 -242
- package/server/mind/time/api.js +200 -200
- package/server/mind/time/attention.js +34 -34
- package/server/mind/time/delivery-links.js +130 -130
- package/server/mind/time/games.js +7 -3
- package/server/mind/time/index.js +3 -0
- package/server/mind/time/model-material.js +77 -77
- package/server/mind/time/presentation.js +35 -51
- package/server/mind/time/schedule.js +83 -83
- package/server/mind/time/schema.js +84 -84
- package/server/mind/time/search.js +383 -383
- package/server/mind/time/sharing.js +472 -472
- package/server/mind/time/task-links.js +311 -311
- package/server/mind/time/works.js +438 -432
- package/server/mind/traits.js +191 -191
- package/server/mind/view.js +43 -6
- package/server/runtime/installation.js +116 -116
- package/server/runtime/update-manager.js +140 -140
- package/server/storage/data-package.js +298 -298
- package/server/storage/data-transfer-api.js +50 -50
- package/server/storage/data-transfer-worker.js +18 -18
- package/server/storage/data-transfer.js +416 -416
- package/server/studio/display-names.js +139 -139
- package/server/studio/management.js +102 -102
- package/server/studio/updates.js +144 -144
- package/server/version.js +10 -10
- package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
- package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
- package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
- package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
- package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
- package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
- package/public/app/assets/TimePage-C-guwPk2.css +0 -1
package/server/mind/affect.js
CHANGED
|
@@ -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
|
+
}
|