luckytri 1.0.0 → 1.0.1
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/.github/SECURITY.md +4 -4
- package/README.en.md +252 -250
- package/README.md +252 -250
- package/bin/luckytri.js +5 -1
- package/docs/en/CHANGELOG.md +8 -0
- package/docs/en/connect.md +18 -18
- package/docs/en/guide.md +192 -216
- package/docs/en/npm-release.md +25 -25
- package/docs/zh/CHANGELOG.md +8 -0
- package/docs/zh/connect.md +17 -17
- package/docs/zh/guide.md +192 -216
- package/docs/zh/npm-release.md +33 -33
- package/package.json +109 -108
- package/plugins/sdk/index.d.ts +155 -155
- package/plugins/sdk/testing.js +58 -58
- package/plugins/weather/index.js +82 -82
- package/plugins/weather/luckytri-plugin.json +29 -29
- package/public/app/assets/ChatsPage-B9Odvzfz.js +3 -0
- package/public/app/assets/Empty-SkA8PyMo.js +2 -0
- package/public/app/assets/Field-BjLhi8pL.js +1 -0
- package/public/app/assets/HeartPage-CscKqMsr.js +1 -0
- package/public/app/assets/LifePage-BHCmBHlH.js +1 -0
- package/public/app/assets/MemoryPage-Bhs2xa1k.js +1 -0
- package/public/app/assets/NaturePage-CaPIar_5.js +3 -0
- package/public/app/assets/PeoplePage-CuwSSVeX.js +1 -0
- package/public/app/assets/PluginsPage-BhVm1SUi.js +2 -0
- package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
- package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-5yYqyUgR.css} +1 -1
- package/public/app/assets/SystemPage-Chm5fiGP.js +1 -0
- package/public/app/assets/TimePage-BUm4eUzB.js +3 -0
- package/public/app/assets/index-BIAdfObg.js +4 -0
- package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
- package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-DNDLyN9m.js} +1 -1
- package/public/app/assets/media-CKVAvML2.js +1 -0
- package/public/app/index.html +3 -3
- package/public/guide.en.html +32 -99
- package/public/guide.html +21 -86
- package/public/tutorial.en.md +15 -39
- package/public/tutorial.md +15 -39
- package/scripts/backup.js +311 -311
- package/scripts/browser.js +41 -41
- package/scripts/launch.js +101 -94
- package/scripts/plugin.js +150 -150
- package/scripts/reset-password.js +22 -0
- package/scripts/runtime.js +25 -25
- package/scripts/service.js +56 -47
- package/scripts/setup.js +50 -50
- package/scripts/start-server.js +41 -41
- package/scripts/stop.js +36 -36
- package/server/app.js +110 -96
- package/server/auth.js +217 -0
- package/server/channels/contract.js +57 -57
- package/server/channels/hub.js +165 -165
- package/server/channels/index.js +28 -25
- package/server/channels/onebot/index.js +335 -336
- package/server/channels/onebot/token.js +22 -3
- package/server/core/capabilities.js +208 -208
- package/server/core/events.js +64 -60
- package/server/core/image-preprocessor.js +208 -208
- package/server/core/model-manager.js +1213 -1213
- package/server/core/net-guard.js +49 -49
- package/server/core/orchestrator.js +569 -569
- package/server/core/perceivers.js +91 -91
- package/server/core/persona-manager.js +243 -243
- package/server/core/repository.js +293 -293
- package/server/core/response-validator.js +508 -508
- package/server/core/turn-processor.js +630 -630
- package/server/core/turn.js +236 -236
- package/server/core/vision-manager.js +633 -633
- package/server/i18n/errors.js +512 -497
- package/server/i18n/labels.js +649 -653
- package/server/index.js +107 -102
- package/server/knowledge/manager.js +386 -386
- package/server/local-access.js +32 -0
- package/server/mind/anticipations.js +507 -507
- package/server/mind/api.js +703 -703
- package/server/mind/budget.js +152 -152
- package/server/mind/context-selection.js +61 -61
- package/server/mind/day-planner.js +324 -324
- package/server/mind/evidence.js +148 -148
- package/server/mind/index.js +445 -445
- package/server/mind/life/diary.js +786 -786
- package/server/mind/life/index.js +1049 -1049
- package/server/mind/observations.js +84 -84
- package/server/mind/own-day.js +324 -324
- package/server/mind/own-voice.js +318 -318
- package/server/mind/senses.js +53 -53
- package/server/mind/time/activity-clock.js +196 -196
- package/server/mind/time/availability.js +69 -69
- package/server/mind/time/commitment-review.js +198 -198
- package/server/mind/time/executor.js +512 -512
- package/server/mind/time/experiences.js +128 -128
- package/server/mind/time/games.js +562 -562
- package/server/mind/time/index.js +739 -739
- package/server/mind/time/intent.js +166 -166
- package/server/mind/time/kinds.js +171 -171
- package/server/mind/time/plugin-activity.js +207 -207
- package/server/mind/time/tasks.js +587 -587
- package/server/mind/util.js +110 -110
- package/server/mind/view.js +273 -273
- package/server/paths.js +102 -102
- package/server/plugins/api.js +204 -204
- package/server/plugins/host.js +967 -967
- package/server/plugins/install.js +295 -295
- package/server/plugins/manifest.js +201 -201
- package/server/plugins/permissions.js +42 -42
- package/server/plugins/runner/guard.mjs +36 -36
- package/server/plugins/runner/worker.mjs +250 -250
- package/server/plugins/schema.js +91 -91
- package/server/plugins/zip.js +112 -112
- package/server/startup-lock.js +118 -118
- package/server/storage/archive.js +241 -241
- package/server/storage/backup-cleanup-worker.js +8 -8
- package/server/storage/backup-cleanup.js +414 -414
- package/server/storage/backup-lock.js +76 -76
- package/server/storage/backup-scheduler.js +131 -131
- package/server/storage/store.js +199 -199
- package/server/studio/readiness.js +181 -190
- package/server/studio/settings.js +236 -236
- package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
- package/public/app/assets/Empty-CpP5kQHH.js +0 -2
- package/public/app/assets/Field-D4gY-INU.js +0 -1
- package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
- package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
- package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
- package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
- package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
- package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
- package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
- package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
- package/public/app/assets/TimePage-Cw44inM4.js +0 -3
- package/public/app/assets/index-BheRW-1t.js +0 -4
- package/public/app/assets/media-BzPDBm7e.js +0 -1
package/server/mind/index.js
CHANGED
|
@@ -1,445 +1,445 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { migrateMind } from "./schema.js";
|
|
3
|
-
import { Nature } from "./nature.js";
|
|
4
|
-
import { Traits } from "./traits.js";
|
|
5
|
-
import { Affect } from "./affect.js";
|
|
6
|
-
import { BOND_CHANGES, Bonds } from "./bonds.js";
|
|
7
|
-
import { Self } from "./self.js";
|
|
8
|
-
import { Faces } from "./faces.js";
|
|
9
|
-
import { Thoughts } from "./thoughts.js";
|
|
10
|
-
import { MemoryManager } from "./memory.js";
|
|
11
|
-
import { Budget } from "./budget.js";
|
|
12
|
-
import { Reading } from "./reading.js";
|
|
13
|
-
import { Days } from "./days.js";
|
|
14
|
-
import { Anticipations } from "./anticipations.js";
|
|
15
|
-
import { Periods } from "./periods.js";
|
|
16
|
-
import { Meetings } from "./meetings.js";
|
|
17
|
-
import { Continuity } from "./continuity.js";
|
|
18
|
-
import { TimeSystem } from "./time/index.js";
|
|
19
|
-
import { ownLife } from "./salience.js";
|
|
20
|
-
import { innerView } from "./view.js";
|
|
21
|
-
import { Senses } from "./senses.js";
|
|
22
|
-
import { Observations } from "./observations.js";
|
|
23
|
-
import { clamp, dayKey, parse, text } from "./util.js";
|
|
24
|
-
|
|
25
|
-
// The wish a day's snapshot says she was living. Older snapshots did not
|
|
26
|
-
// store it; those fall back to her own-life threads from that day.
|
|
27
|
-
export function snapshotLiving(snap) {
|
|
28
|
-
if (snap?.livingFor?.content && ownLife(snap.livingFor.content))
|
|
29
|
-
return snap.livingFor;
|
|
30
|
-
const own = (snap?.self || []).filter(
|
|
31
|
-
(row) => row.kind === "intention" && ownLife(row.content),
|
|
32
|
-
);
|
|
33
|
-
if (!own.length) return null;
|
|
34
|
-
return [...own].sort((a, b) => (b.strength || 0) - (a.strength || 0))[0];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// What changed in her between two saved days.
|
|
38
|
-
export function diffSnapshots(before, after) {
|
|
39
|
-
if (!before || !after) return null;
|
|
40
|
-
const key = (t) => t.thread;
|
|
41
|
-
const old = new Map((before.self || []).map((t) => [key(t), t]));
|
|
42
|
-
const now = new Map((after.self || []).map((t) => [key(t), t]));
|
|
43
|
-
const person = (p) => p.closeness + p.trust - p.tension;
|
|
44
|
-
const known = new Map((before.people || []).map((p) => [p.userId, p]));
|
|
45
|
-
const fromLife = snapshotLiving(before);
|
|
46
|
-
const toLife = snapshotLiving(after);
|
|
47
|
-
return {
|
|
48
|
-
appeared: [...now.values()].filter((t) => !old.has(key(t))),
|
|
49
|
-
faded: [...old.values()].filter((t) => !now.has(key(t))),
|
|
50
|
-
changed: [...now.values()]
|
|
51
|
-
.filter((t) => old.has(key(t)))
|
|
52
|
-
.map((t) => ({ ...t, before: old.get(key(t)) }))
|
|
53
|
-
.filter(
|
|
54
|
-
(t) =>
|
|
55
|
-
t.before.content !== t.content ||
|
|
56
|
-
Math.abs(t.before.strength - t.strength) >= 0.05 ||
|
|
57
|
-
t.before.status !== t.status,
|
|
58
|
-
),
|
|
59
|
-
people: (after.people || [])
|
|
60
|
-
.map((p) => ({
|
|
61
|
-
...p,
|
|
62
|
-
shift: known.has(p.userId)
|
|
63
|
-
? Math.round((person(p) - person(known.get(p.userId))) * 100) / 100
|
|
64
|
-
: null,
|
|
65
|
-
}))
|
|
66
|
-
.filter((p) => p.shift === null || Math.abs(p.shift) >= 0.05),
|
|
67
|
-
mood: { before: before.affect?.mood, after: after.affect?.mood },
|
|
68
|
-
...(fromLife?.content !== toLife?.content
|
|
69
|
-
? {
|
|
70
|
-
livingFor: {
|
|
71
|
-
from: fromLife?.content || "",
|
|
72
|
-
to: toLife?.content || "",
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
: {}),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const TURN_CHANGES = new Set(
|
|
80
|
-
Object.keys(BOND_CHANGES).filter(
|
|
81
|
-
(c) => !["interaction", "impression"].includes(c),
|
|
82
|
-
),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// One of her, across every conversation. Sessions only say where something
|
|
86
|
-
// happened; they are no longer walls between parts of her.
|
|
87
|
-
export class Mind {
|
|
88
|
-
constructor(repo, { models } = {}) {
|
|
89
|
-
this.repo = repo;
|
|
90
|
-
this.db = repo.db;
|
|
91
|
-
this.store = repo.store;
|
|
92
|
-
migrateMind(this.db, this.store);
|
|
93
|
-
this.db.exec(
|
|
94
|
-
"CREATE INDEX IF NOT EXISTS core_events_time ON core_events(time)",
|
|
95
|
-
);
|
|
96
|
-
this.nature = new Nature(repo);
|
|
97
|
-
this.traits = new Traits(this);
|
|
98
|
-
this.affect = new Affect(this);
|
|
99
|
-
this.bonds = new Bonds(this);
|
|
100
|
-
this.self = new Self(this);
|
|
101
|
-
this.faces = new Faces(this);
|
|
102
|
-
this.thoughts = new Thoughts(this);
|
|
103
|
-
this.memory = new MemoryManager(repo, models, this);
|
|
104
|
-
this.budget = new Budget(repo);
|
|
105
|
-
this.reading = new Reading(this);
|
|
106
|
-
this.days = new Days(this);
|
|
107
|
-
this.anticipations = new Anticipations(this);
|
|
108
|
-
this.periods = new Periods(this);
|
|
109
|
-
this.meetings = new Meetings(this);
|
|
110
|
-
this.continuity = new Continuity(this);
|
|
111
|
-
this.time = new TimeSystem(this);
|
|
112
|
-
this.senses = new Senses();
|
|
113
|
-
this.observations = new Observations(this);
|
|
114
|
-
this.body = () => [];
|
|
115
|
-
this.days.reconcile();
|
|
116
|
-
}
|
|
117
|
-
timeZone() {
|
|
118
|
-
return this.repo.config("life", {})?.timeZone || "Asia/Shanghai";
|
|
119
|
-
}
|
|
120
|
-
view(options) {
|
|
121
|
-
return innerView(this, options);
|
|
122
|
-
}
|
|
123
|
-
// The whole system is one body she can read: what is connected, and
|
|
124
|
-
// whether each plugin is on. The studio shows the same state.
|
|
125
|
-
embody(read) {
|
|
126
|
-
this.body = typeof read === "function" ? read : () => [];
|
|
127
|
-
}
|
|
128
|
-
attention(session) {
|
|
129
|
-
return (
|
|
130
|
-
this.db
|
|
131
|
-
.prepare("SELECT * FROM mind_attention WHERE session_id=?")
|
|
132
|
-
.get(session) || { session_id: session, looked_seq: 0, looked_at: null }
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
look(session, seq, time = Date.now()) {
|
|
136
|
-
this.db
|
|
137
|
-
.prepare(
|
|
138
|
-
"INSERT INTO mind_attention(session_id,looked_seq,looked_at,deferred) VALUES (?,?,?,0) ON CONFLICT(session_id) DO UPDATE SET looked_seq=MAX(looked_seq,excluded.looked_seq),looked_at=excluded.looked_at,deferred=0",
|
|
139
|
-
)
|
|
140
|
-
.run(session, seq, time);
|
|
141
|
-
}
|
|
142
|
-
// Messages that passed while she was not in the room. They are not unread,
|
|
143
|
-
// and they are not something she lived.
|
|
144
|
-
unlived(seqs) {
|
|
145
|
-
const mark = this.db.prepare(
|
|
146
|
-
"INSERT OR IGNORE INTO mind_unlived(seq) VALUES (?)",
|
|
147
|
-
);
|
|
148
|
-
for (const seq of seqs || []) if (seq) mark.run(seq);
|
|
149
|
-
}
|
|
150
|
-
// Someone called her while she slept; she will look when she wakes.
|
|
151
|
-
defer(session) {
|
|
152
|
-
this.db
|
|
153
|
-
.prepare(
|
|
154
|
-
"INSERT INTO mind_attention(session_id,looked_seq,deferred) VALUES (?,0,1) ON CONFLICT(session_id) DO UPDATE SET deferred=1",
|
|
155
|
-
)
|
|
156
|
-
.run(session);
|
|
157
|
-
}
|
|
158
|
-
deferred() {
|
|
159
|
-
return this.db
|
|
160
|
-
.prepare("SELECT session_id FROM mind_attention WHERE deferred=1")
|
|
161
|
-
.all()
|
|
162
|
-
.map((row) => row.session_id);
|
|
163
|
-
}
|
|
164
|
-
// Messages that arrived while she was not looking (busy, asleep, or only
|
|
165
|
-
// glancing). The next real look reads all of them together.
|
|
166
|
-
unread(session, { after, limit = 40, now = Date.now() } = {}) {
|
|
167
|
-
const since = after ?? this.attention(session).looked_seq;
|
|
168
|
-
// Anything older than half a day is history, not something to answer.
|
|
169
|
-
return this.repo.unreadEvents(session, since, now - 12 * 3600000, limit);
|
|
170
|
-
}
|
|
171
|
-
choose({
|
|
172
|
-
session,
|
|
173
|
-
traceId = null,
|
|
174
|
-
choice,
|
|
175
|
-
appraisal = "",
|
|
176
|
-
reason = "",
|
|
177
|
-
watermark = null,
|
|
178
|
-
occasion = null,
|
|
179
|
-
time = Date.now(),
|
|
180
|
-
}) {
|
|
181
|
-
this.db
|
|
182
|
-
.prepare(
|
|
183
|
-
"INSERT INTO mind_choices(id,created,session_id,trace_id,choice,appraisal,reason,watermark,occasion) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
184
|
-
)
|
|
185
|
-
.run(
|
|
186
|
-
randomUUID(),
|
|
187
|
-
time,
|
|
188
|
-
session,
|
|
189
|
-
traceId,
|
|
190
|
-
choice,
|
|
191
|
-
text(appraisal, 200),
|
|
192
|
-
text(reason, 300),
|
|
193
|
-
watermark,
|
|
194
|
-
occasion,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
choices({ session = "", limit = 30, before = Number.MAX_SAFE_INTEGER } = {}) {
|
|
198
|
-
return this.db
|
|
199
|
-
.prepare(
|
|
200
|
-
`SELECT * FROM mind_choices WHERE created<?${session ? " AND session_id=?" : ""} ORDER BY created DESC LIMIT ?`,
|
|
201
|
-
)
|
|
202
|
-
.all(before, ...(session ? [session] : []), limit);
|
|
203
|
-
}
|
|
204
|
-
// Living through a conversation changes her whether or not she speaks.
|
|
205
|
-
// `deferTalk` waits until words actually leave before counting a conversation.
|
|
206
|
-
experience(
|
|
207
|
-
turn,
|
|
208
|
-
{
|
|
209
|
-
session,
|
|
210
|
-
snapshot,
|
|
211
|
-
kind = "group",
|
|
212
|
-
spoke = false,
|
|
213
|
-
deferTalk = false,
|
|
214
|
-
time = Date.now(),
|
|
215
|
-
},
|
|
216
|
-
) {
|
|
217
|
-
const ids = new Set((snapshot.messages || []).map((m) => m.id));
|
|
218
|
-
const speakers = new Map(
|
|
219
|
-
(snapshot.messages || [])
|
|
220
|
-
.filter((m) => m.role === "user")
|
|
221
|
-
.map((m) => [String(m.speaker), m.name]),
|
|
222
|
-
);
|
|
223
|
-
const batch = (snapshot.messages || []).filter((m) =>
|
|
224
|
-
(snapshot.batchIds || []).includes(m.id),
|
|
225
|
-
);
|
|
226
|
-
this.bonds.meet(
|
|
227
|
-
batch
|
|
228
|
-
.filter((m) => m.role === "user")
|
|
229
|
-
.map((m) => ({ userId: String(m.speaker), name: m.name })),
|
|
230
|
-
session,
|
|
231
|
-
time,
|
|
232
|
-
);
|
|
233
|
-
for (const f of (turn.feelings || []).slice(0, 3)) {
|
|
234
|
-
const cited = (Array.isArray(f.cause) ? f.cause : []).filter((id) =>
|
|
235
|
-
ids.has(id),
|
|
236
|
-
);
|
|
237
|
-
if (Array.isArray(f.cause) && f.cause.length && !cited.length) continue;
|
|
238
|
-
this.affect.feel({
|
|
239
|
-
feeling: f.feeling,
|
|
240
|
-
intensity: clamp(f.intensity, 0, 1),
|
|
241
|
-
valence: clamp(f.valence, -1, 1),
|
|
242
|
-
arousal: f.arousal,
|
|
243
|
-
cause: turn.appraisal || "",
|
|
244
|
-
sources: cited,
|
|
245
|
-
session,
|
|
246
|
-
origin: "turn",
|
|
247
|
-
time,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
for (const b of (turn.bonds || []).slice(0, 4)) {
|
|
251
|
-
if (!TURN_CHANGES.has(b.change) || !speakers.has(String(b.userId)))
|
|
252
|
-
continue;
|
|
253
|
-
const cited = (Array.isArray(b.evidence) ? b.evidence : []).filter((id) =>
|
|
254
|
-
ids.has(id),
|
|
255
|
-
);
|
|
256
|
-
if (!cited.length) continue;
|
|
257
|
-
this.bonds.record({
|
|
258
|
-
id: String(b.userId),
|
|
259
|
-
change: b.change,
|
|
260
|
-
note: b.why,
|
|
261
|
-
sources: cited,
|
|
262
|
-
session,
|
|
263
|
-
origin: "turn",
|
|
264
|
-
time,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (!deferTalk)
|
|
268
|
-
this.settleTalk(turn, { session, snapshot, kind, spoke, time });
|
|
269
|
-
}
|
|
270
|
-
// Whether this turn became a conversation. A choice to speak that never
|
|
271
|
-
// leaves does not count as having spoken.
|
|
272
|
-
settleTalk(
|
|
273
|
-
turn,
|
|
274
|
-
{
|
|
275
|
-
session,
|
|
276
|
-
snapshot,
|
|
277
|
-
kind = "group",
|
|
278
|
-
spoke = false,
|
|
279
|
-
withheld = false,
|
|
280
|
-
sent = [],
|
|
281
|
-
time = Date.now(),
|
|
282
|
-
},
|
|
283
|
-
) {
|
|
284
|
-
const batch = (snapshot.messages || []).filter((m) =>
|
|
285
|
-
(snapshot.batchIds || []).includes(m.id),
|
|
286
|
-
);
|
|
287
|
-
// A draft that never left must not spend the hour's one conversation mark.
|
|
288
|
-
// Otherwise a later message that does go out would not count as talking.
|
|
289
|
-
if (!withheld) {
|
|
290
|
-
const targets = new Set(turn.targetMessageIds || []);
|
|
291
|
-
for (const m of batch) {
|
|
292
|
-
if (m.role !== "user") continue;
|
|
293
|
-
const addressed = m.relation === "direct";
|
|
294
|
-
if (!addressed && !(spoke && targets.has(m.id))) continue;
|
|
295
|
-
this.bonds.record({
|
|
296
|
-
id: String(m.speaker),
|
|
297
|
-
change: "interaction",
|
|
298
|
-
sources: [m.id],
|
|
299
|
-
session,
|
|
300
|
-
// Being called and staying quiet can make a person more familiar.
|
|
301
|
-
// It is not a conversation, and it does not end the time since she spoke.
|
|
302
|
-
origin: spoke ? (addressed ? "direct" : "group") : "noticed",
|
|
303
|
-
time,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
if (spoke && kind === "group")
|
|
307
|
-
this.bonds.record({
|
|
308
|
-
kind: "group",
|
|
309
|
-
id: session,
|
|
310
|
-
change: "interaction",
|
|
311
|
-
session,
|
|
312
|
-
origin: "group",
|
|
313
|
-
time,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
this.meetings.keep(turn, {
|
|
317
|
-
session,
|
|
318
|
-
snapshot,
|
|
319
|
-
sent: spoke ? sent : [],
|
|
320
|
-
time,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
revoke(kind, id, reason = "") {
|
|
324
|
-
const note = text(reason, 200);
|
|
325
|
-
const tomb = (content) =>
|
|
326
|
-
this.db
|
|
327
|
-
.prepare(
|
|
328
|
-
"INSERT INTO mind_revocations(id,created,target_kind,target_id,content,reason) VALUES (?,?,?,?,?,?)",
|
|
329
|
-
)
|
|
330
|
-
.run(randomUUID(), Date.now(), kind, id, content, note);
|
|
331
|
-
if (kind === "memory") return this.memory.revoke(id, note);
|
|
332
|
-
if (kind === "self") {
|
|
333
|
-
const rows = this.self.history(id);
|
|
334
|
-
if (!rows.length) throw Error("线索不存在");
|
|
335
|
-
tomb(rows.at(-1).content);
|
|
336
|
-
} else if (kind === "face") {
|
|
337
|
-
const row = this.db
|
|
338
|
-
.prepare("SELECT * FROM mind_faces WHERE id=?")
|
|
339
|
-
.get(id);
|
|
340
|
-
if (!row) throw Error("面貌版本不存在");
|
|
341
|
-
tomb(row.content || row.role);
|
|
342
|
-
} else if (kind === "bond") {
|
|
343
|
-
const row = this.db
|
|
344
|
-
.prepare("SELECT * FROM mind_bond_events WHERE id=?")
|
|
345
|
-
.get(id);
|
|
346
|
-
if (!row) throw Error("关系变化不存在");
|
|
347
|
-
tomb(row.note || row.change);
|
|
348
|
-
} else if (kind === "thought") {
|
|
349
|
-
this.thoughts.update(id, { hidden: true });
|
|
350
|
-
tomb(this.thoughts.get(id)?.content || "");
|
|
351
|
-
} else if (kind === "anticipation") {
|
|
352
|
-
tomb(this.anticipations.revoke(id, note));
|
|
353
|
-
} else if (kind === "meeting") {
|
|
354
|
-
const row = this.db
|
|
355
|
-
.prepare("SELECT * FROM mind_meetings WHERE id=?")
|
|
356
|
-
.get(id);
|
|
357
|
-
if (!row) throw Error("相遇不存在");
|
|
358
|
-
tomb(row.appraisal || row.topic);
|
|
359
|
-
} else if (kind === "observation") {
|
|
360
|
-
tomb(this.observations.revoke(id));
|
|
361
|
-
} else throw Error("不能撤销这类内容");
|
|
362
|
-
this.store.revision++;
|
|
363
|
-
}
|
|
364
|
-
revocations(limit = 100) {
|
|
365
|
-
return this.db
|
|
366
|
-
.prepare("SELECT * FROM mind_revocations ORDER BY created DESC LIMIT ?")
|
|
367
|
-
.all(limit);
|
|
368
|
-
}
|
|
369
|
-
// What she was living on a saved day. New snapshots store it; older ones
|
|
370
|
-
// may have dropped the wish from the day's bag, or written a duty there.
|
|
371
|
-
livedThen(snap) {
|
|
372
|
-
if (!snap) return null;
|
|
373
|
-
if (ownLife(snap.livingFor?.content)) return snap.livingFor;
|
|
374
|
-
if (snap.created) {
|
|
375
|
-
const lived = this.self.living({
|
|
376
|
-
before: snap.created,
|
|
377
|
-
now: snap.created,
|
|
378
|
-
});
|
|
379
|
-
if (lived) return { thread: lived.thread, content: lived.content };
|
|
380
|
-
}
|
|
381
|
-
return snapshotLiving(snap);
|
|
382
|
-
}
|
|
383
|
-
// A plain record of who she was at the end of a day, for comparing later.
|
|
384
|
-
snapshot(now = Date.now(), day = dayKey(now, this.timeZone())) {
|
|
385
|
-
const living = this.self.living({ before: now, now });
|
|
386
|
-
const self = this.self
|
|
387
|
-
.active({ before: now, limit: 30 })
|
|
388
|
-
.map(({ thread, kind, content, strength, status }) => ({
|
|
389
|
-
thread,
|
|
390
|
-
kind,
|
|
391
|
-
content,
|
|
392
|
-
strength,
|
|
393
|
-
status,
|
|
394
|
-
}));
|
|
395
|
-
if (living && !self.some((row) => row.thread === living.thread)) {
|
|
396
|
-
self.unshift({
|
|
397
|
-
thread: living.thread,
|
|
398
|
-
kind: living.kind,
|
|
399
|
-
content: living.content,
|
|
400
|
-
strength: living.strength,
|
|
401
|
-
status: living.status,
|
|
402
|
-
});
|
|
403
|
-
self.splice(30);
|
|
404
|
-
}
|
|
405
|
-
const value = {
|
|
406
|
-
affect: this.affect.state(now),
|
|
407
|
-
self,
|
|
408
|
-
...(living
|
|
409
|
-
? { livingFor: { thread: living.thread, content: living.content } }
|
|
410
|
-
: {}),
|
|
411
|
-
faces: this.faces
|
|
412
|
-
.all(now)
|
|
413
|
-
.map(({ session_id, role, tone, aspiration }) => ({
|
|
414
|
-
session: session_id,
|
|
415
|
-
role,
|
|
416
|
-
tone,
|
|
417
|
-
aspiration,
|
|
418
|
-
})),
|
|
419
|
-
people: this.bonds
|
|
420
|
-
.people({ now, limit: 30 })
|
|
421
|
-
.map(({ userId, name, familiarity, closeness, trust, tension }) => ({
|
|
422
|
-
userId,
|
|
423
|
-
name,
|
|
424
|
-
familiarity,
|
|
425
|
-
closeness,
|
|
426
|
-
trust,
|
|
427
|
-
tension,
|
|
428
|
-
})),
|
|
429
|
-
};
|
|
430
|
-
this.db
|
|
431
|
-
.prepare(
|
|
432
|
-
"INSERT INTO mind_snapshots(day,created,value) VALUES (?,?,?) ON CONFLICT(day) DO UPDATE SET created=excluded.created,value=excluded.value",
|
|
433
|
-
)
|
|
434
|
-
.run(day, now, JSON.stringify(value));
|
|
435
|
-
return { day, ...value };
|
|
436
|
-
}
|
|
437
|
-
snapshotOf(day) {
|
|
438
|
-
const row = this.db
|
|
439
|
-
.prepare("SELECT * FROM mind_snapshots WHERE day=?")
|
|
440
|
-
.get(day);
|
|
441
|
-
return row
|
|
442
|
-
? { day: row.day, created: row.created, ...parse(row.value, {}) }
|
|
443
|
-
: null;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { migrateMind } from "./schema.js";
|
|
3
|
+
import { Nature } from "./nature.js";
|
|
4
|
+
import { Traits } from "./traits.js";
|
|
5
|
+
import { Affect } from "./affect.js";
|
|
6
|
+
import { BOND_CHANGES, Bonds } from "./bonds.js";
|
|
7
|
+
import { Self } from "./self.js";
|
|
8
|
+
import { Faces } from "./faces.js";
|
|
9
|
+
import { Thoughts } from "./thoughts.js";
|
|
10
|
+
import { MemoryManager } from "./memory.js";
|
|
11
|
+
import { Budget } from "./budget.js";
|
|
12
|
+
import { Reading } from "./reading.js";
|
|
13
|
+
import { Days } from "./days.js";
|
|
14
|
+
import { Anticipations } from "./anticipations.js";
|
|
15
|
+
import { Periods } from "./periods.js";
|
|
16
|
+
import { Meetings } from "./meetings.js";
|
|
17
|
+
import { Continuity } from "./continuity.js";
|
|
18
|
+
import { TimeSystem } from "./time/index.js";
|
|
19
|
+
import { ownLife } from "./salience.js";
|
|
20
|
+
import { innerView } from "./view.js";
|
|
21
|
+
import { Senses } from "./senses.js";
|
|
22
|
+
import { Observations } from "./observations.js";
|
|
23
|
+
import { clamp, dayKey, parse, text } from "./util.js";
|
|
24
|
+
|
|
25
|
+
// The wish a day's snapshot says she was living. Older snapshots did not
|
|
26
|
+
// store it; those fall back to her own-life threads from that day.
|
|
27
|
+
export function snapshotLiving(snap) {
|
|
28
|
+
if (snap?.livingFor?.content && ownLife(snap.livingFor.content))
|
|
29
|
+
return snap.livingFor;
|
|
30
|
+
const own = (snap?.self || []).filter(
|
|
31
|
+
(row) => row.kind === "intention" && ownLife(row.content),
|
|
32
|
+
);
|
|
33
|
+
if (!own.length) return null;
|
|
34
|
+
return [...own].sort((a, b) => (b.strength || 0) - (a.strength || 0))[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// What changed in her between two saved days.
|
|
38
|
+
export function diffSnapshots(before, after) {
|
|
39
|
+
if (!before || !after) return null;
|
|
40
|
+
const key = (t) => t.thread;
|
|
41
|
+
const old = new Map((before.self || []).map((t) => [key(t), t]));
|
|
42
|
+
const now = new Map((after.self || []).map((t) => [key(t), t]));
|
|
43
|
+
const person = (p) => p.closeness + p.trust - p.tension;
|
|
44
|
+
const known = new Map((before.people || []).map((p) => [p.userId, p]));
|
|
45
|
+
const fromLife = snapshotLiving(before);
|
|
46
|
+
const toLife = snapshotLiving(after);
|
|
47
|
+
return {
|
|
48
|
+
appeared: [...now.values()].filter((t) => !old.has(key(t))),
|
|
49
|
+
faded: [...old.values()].filter((t) => !now.has(key(t))),
|
|
50
|
+
changed: [...now.values()]
|
|
51
|
+
.filter((t) => old.has(key(t)))
|
|
52
|
+
.map((t) => ({ ...t, before: old.get(key(t)) }))
|
|
53
|
+
.filter(
|
|
54
|
+
(t) =>
|
|
55
|
+
t.before.content !== t.content ||
|
|
56
|
+
Math.abs(t.before.strength - t.strength) >= 0.05 ||
|
|
57
|
+
t.before.status !== t.status,
|
|
58
|
+
),
|
|
59
|
+
people: (after.people || [])
|
|
60
|
+
.map((p) => ({
|
|
61
|
+
...p,
|
|
62
|
+
shift: known.has(p.userId)
|
|
63
|
+
? Math.round((person(p) - person(known.get(p.userId))) * 100) / 100
|
|
64
|
+
: null,
|
|
65
|
+
}))
|
|
66
|
+
.filter((p) => p.shift === null || Math.abs(p.shift) >= 0.05),
|
|
67
|
+
mood: { before: before.affect?.mood, after: after.affect?.mood },
|
|
68
|
+
...(fromLife?.content !== toLife?.content
|
|
69
|
+
? {
|
|
70
|
+
livingFor: {
|
|
71
|
+
from: fromLife?.content || "",
|
|
72
|
+
to: toLife?.content || "",
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
: {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const TURN_CHANGES = new Set(
|
|
80
|
+
Object.keys(BOND_CHANGES).filter(
|
|
81
|
+
(c) => !["interaction", "impression"].includes(c),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// One of her, across every conversation. Sessions only say where something
|
|
86
|
+
// happened; they are no longer walls between parts of her.
|
|
87
|
+
export class Mind {
|
|
88
|
+
constructor(repo, { models } = {}) {
|
|
89
|
+
this.repo = repo;
|
|
90
|
+
this.db = repo.db;
|
|
91
|
+
this.store = repo.store;
|
|
92
|
+
migrateMind(this.db, this.store);
|
|
93
|
+
this.db.exec(
|
|
94
|
+
"CREATE INDEX IF NOT EXISTS core_events_time ON core_events(time)",
|
|
95
|
+
);
|
|
96
|
+
this.nature = new Nature(repo);
|
|
97
|
+
this.traits = new Traits(this);
|
|
98
|
+
this.affect = new Affect(this);
|
|
99
|
+
this.bonds = new Bonds(this);
|
|
100
|
+
this.self = new Self(this);
|
|
101
|
+
this.faces = new Faces(this);
|
|
102
|
+
this.thoughts = new Thoughts(this);
|
|
103
|
+
this.memory = new MemoryManager(repo, models, this);
|
|
104
|
+
this.budget = new Budget(repo);
|
|
105
|
+
this.reading = new Reading(this);
|
|
106
|
+
this.days = new Days(this);
|
|
107
|
+
this.anticipations = new Anticipations(this);
|
|
108
|
+
this.periods = new Periods(this);
|
|
109
|
+
this.meetings = new Meetings(this);
|
|
110
|
+
this.continuity = new Continuity(this);
|
|
111
|
+
this.time = new TimeSystem(this);
|
|
112
|
+
this.senses = new Senses();
|
|
113
|
+
this.observations = new Observations(this);
|
|
114
|
+
this.body = () => [];
|
|
115
|
+
this.days.reconcile();
|
|
116
|
+
}
|
|
117
|
+
timeZone() {
|
|
118
|
+
return this.repo.config("life", {})?.timeZone || "Asia/Shanghai";
|
|
119
|
+
}
|
|
120
|
+
view(options) {
|
|
121
|
+
return innerView(this, options);
|
|
122
|
+
}
|
|
123
|
+
// The whole system is one body she can read: what is connected, and
|
|
124
|
+
// whether each plugin is on. The studio shows the same state.
|
|
125
|
+
embody(read) {
|
|
126
|
+
this.body = typeof read === "function" ? read : () => [];
|
|
127
|
+
}
|
|
128
|
+
attention(session) {
|
|
129
|
+
return (
|
|
130
|
+
this.db
|
|
131
|
+
.prepare("SELECT * FROM mind_attention WHERE session_id=?")
|
|
132
|
+
.get(session) || { session_id: session, looked_seq: 0, looked_at: null }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
look(session, seq, time = Date.now()) {
|
|
136
|
+
this.db
|
|
137
|
+
.prepare(
|
|
138
|
+
"INSERT INTO mind_attention(session_id,looked_seq,looked_at,deferred) VALUES (?,?,?,0) ON CONFLICT(session_id) DO UPDATE SET looked_seq=MAX(looked_seq,excluded.looked_seq),looked_at=excluded.looked_at,deferred=0",
|
|
139
|
+
)
|
|
140
|
+
.run(session, seq, time);
|
|
141
|
+
}
|
|
142
|
+
// Messages that passed while she was not in the room. They are not unread,
|
|
143
|
+
// and they are not something she lived.
|
|
144
|
+
unlived(seqs) {
|
|
145
|
+
const mark = this.db.prepare(
|
|
146
|
+
"INSERT OR IGNORE INTO mind_unlived(seq) VALUES (?)",
|
|
147
|
+
);
|
|
148
|
+
for (const seq of seqs || []) if (seq) mark.run(seq);
|
|
149
|
+
}
|
|
150
|
+
// Someone called her while she slept; she will look when she wakes.
|
|
151
|
+
defer(session) {
|
|
152
|
+
this.db
|
|
153
|
+
.prepare(
|
|
154
|
+
"INSERT INTO mind_attention(session_id,looked_seq,deferred) VALUES (?,0,1) ON CONFLICT(session_id) DO UPDATE SET deferred=1",
|
|
155
|
+
)
|
|
156
|
+
.run(session);
|
|
157
|
+
}
|
|
158
|
+
deferred() {
|
|
159
|
+
return this.db
|
|
160
|
+
.prepare("SELECT session_id FROM mind_attention WHERE deferred=1")
|
|
161
|
+
.all()
|
|
162
|
+
.map((row) => row.session_id);
|
|
163
|
+
}
|
|
164
|
+
// Messages that arrived while she was not looking (busy, asleep, or only
|
|
165
|
+
// glancing). The next real look reads all of them together.
|
|
166
|
+
unread(session, { after, limit = 40, now = Date.now() } = {}) {
|
|
167
|
+
const since = after ?? this.attention(session).looked_seq;
|
|
168
|
+
// Anything older than half a day is history, not something to answer.
|
|
169
|
+
return this.repo.unreadEvents(session, since, now - 12 * 3600000, limit);
|
|
170
|
+
}
|
|
171
|
+
choose({
|
|
172
|
+
session,
|
|
173
|
+
traceId = null,
|
|
174
|
+
choice,
|
|
175
|
+
appraisal = "",
|
|
176
|
+
reason = "",
|
|
177
|
+
watermark = null,
|
|
178
|
+
occasion = null,
|
|
179
|
+
time = Date.now(),
|
|
180
|
+
}) {
|
|
181
|
+
this.db
|
|
182
|
+
.prepare(
|
|
183
|
+
"INSERT INTO mind_choices(id,created,session_id,trace_id,choice,appraisal,reason,watermark,occasion) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
184
|
+
)
|
|
185
|
+
.run(
|
|
186
|
+
randomUUID(),
|
|
187
|
+
time,
|
|
188
|
+
session,
|
|
189
|
+
traceId,
|
|
190
|
+
choice,
|
|
191
|
+
text(appraisal, 200),
|
|
192
|
+
text(reason, 300),
|
|
193
|
+
watermark,
|
|
194
|
+
occasion,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
choices({ session = "", limit = 30, before = Number.MAX_SAFE_INTEGER } = {}) {
|
|
198
|
+
return this.db
|
|
199
|
+
.prepare(
|
|
200
|
+
`SELECT * FROM mind_choices WHERE created<?${session ? " AND session_id=?" : ""} ORDER BY created DESC LIMIT ?`,
|
|
201
|
+
)
|
|
202
|
+
.all(before, ...(session ? [session] : []), limit);
|
|
203
|
+
}
|
|
204
|
+
// Living through a conversation changes her whether or not she speaks.
|
|
205
|
+
// `deferTalk` waits until words actually leave before counting a conversation.
|
|
206
|
+
experience(
|
|
207
|
+
turn,
|
|
208
|
+
{
|
|
209
|
+
session,
|
|
210
|
+
snapshot,
|
|
211
|
+
kind = "group",
|
|
212
|
+
spoke = false,
|
|
213
|
+
deferTalk = false,
|
|
214
|
+
time = Date.now(),
|
|
215
|
+
},
|
|
216
|
+
) {
|
|
217
|
+
const ids = new Set((snapshot.messages || []).map((m) => m.id));
|
|
218
|
+
const speakers = new Map(
|
|
219
|
+
(snapshot.messages || [])
|
|
220
|
+
.filter((m) => m.role === "user")
|
|
221
|
+
.map((m) => [String(m.speaker), m.name]),
|
|
222
|
+
);
|
|
223
|
+
const batch = (snapshot.messages || []).filter((m) =>
|
|
224
|
+
(snapshot.batchIds || []).includes(m.id),
|
|
225
|
+
);
|
|
226
|
+
this.bonds.meet(
|
|
227
|
+
batch
|
|
228
|
+
.filter((m) => m.role === "user")
|
|
229
|
+
.map((m) => ({ userId: String(m.speaker), name: m.name })),
|
|
230
|
+
session,
|
|
231
|
+
time,
|
|
232
|
+
);
|
|
233
|
+
for (const f of (turn.feelings || []).slice(0, 3)) {
|
|
234
|
+
const cited = (Array.isArray(f.cause) ? f.cause : []).filter((id) =>
|
|
235
|
+
ids.has(id),
|
|
236
|
+
);
|
|
237
|
+
if (Array.isArray(f.cause) && f.cause.length && !cited.length) continue;
|
|
238
|
+
this.affect.feel({
|
|
239
|
+
feeling: f.feeling,
|
|
240
|
+
intensity: clamp(f.intensity, 0, 1),
|
|
241
|
+
valence: clamp(f.valence, -1, 1),
|
|
242
|
+
arousal: f.arousal,
|
|
243
|
+
cause: turn.appraisal || "",
|
|
244
|
+
sources: cited,
|
|
245
|
+
session,
|
|
246
|
+
origin: "turn",
|
|
247
|
+
time,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
for (const b of (turn.bonds || []).slice(0, 4)) {
|
|
251
|
+
if (!TURN_CHANGES.has(b.change) || !speakers.has(String(b.userId)))
|
|
252
|
+
continue;
|
|
253
|
+
const cited = (Array.isArray(b.evidence) ? b.evidence : []).filter((id) =>
|
|
254
|
+
ids.has(id),
|
|
255
|
+
);
|
|
256
|
+
if (!cited.length) continue;
|
|
257
|
+
this.bonds.record({
|
|
258
|
+
id: String(b.userId),
|
|
259
|
+
change: b.change,
|
|
260
|
+
note: b.why,
|
|
261
|
+
sources: cited,
|
|
262
|
+
session,
|
|
263
|
+
origin: "turn",
|
|
264
|
+
time,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (!deferTalk)
|
|
268
|
+
this.settleTalk(turn, { session, snapshot, kind, spoke, time });
|
|
269
|
+
}
|
|
270
|
+
// Whether this turn became a conversation. A choice to speak that never
|
|
271
|
+
// leaves does not count as having spoken.
|
|
272
|
+
settleTalk(
|
|
273
|
+
turn,
|
|
274
|
+
{
|
|
275
|
+
session,
|
|
276
|
+
snapshot,
|
|
277
|
+
kind = "group",
|
|
278
|
+
spoke = false,
|
|
279
|
+
withheld = false,
|
|
280
|
+
sent = [],
|
|
281
|
+
time = Date.now(),
|
|
282
|
+
},
|
|
283
|
+
) {
|
|
284
|
+
const batch = (snapshot.messages || []).filter((m) =>
|
|
285
|
+
(snapshot.batchIds || []).includes(m.id),
|
|
286
|
+
);
|
|
287
|
+
// A draft that never left must not spend the hour's one conversation mark.
|
|
288
|
+
// Otherwise a later message that does go out would not count as talking.
|
|
289
|
+
if (!withheld) {
|
|
290
|
+
const targets = new Set(turn.targetMessageIds || []);
|
|
291
|
+
for (const m of batch) {
|
|
292
|
+
if (m.role !== "user") continue;
|
|
293
|
+
const addressed = m.relation === "direct";
|
|
294
|
+
if (!addressed && !(spoke && targets.has(m.id))) continue;
|
|
295
|
+
this.bonds.record({
|
|
296
|
+
id: String(m.speaker),
|
|
297
|
+
change: "interaction",
|
|
298
|
+
sources: [m.id],
|
|
299
|
+
session,
|
|
300
|
+
// Being called and staying quiet can make a person more familiar.
|
|
301
|
+
// It is not a conversation, and it does not end the time since she spoke.
|
|
302
|
+
origin: spoke ? (addressed ? "direct" : "group") : "noticed",
|
|
303
|
+
time,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (spoke && kind === "group")
|
|
307
|
+
this.bonds.record({
|
|
308
|
+
kind: "group",
|
|
309
|
+
id: session,
|
|
310
|
+
change: "interaction",
|
|
311
|
+
session,
|
|
312
|
+
origin: "group",
|
|
313
|
+
time,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
this.meetings.keep(turn, {
|
|
317
|
+
session,
|
|
318
|
+
snapshot,
|
|
319
|
+
sent: spoke ? sent : [],
|
|
320
|
+
time,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
revoke(kind, id, reason = "") {
|
|
324
|
+
const note = text(reason, 200);
|
|
325
|
+
const tomb = (content) =>
|
|
326
|
+
this.db
|
|
327
|
+
.prepare(
|
|
328
|
+
"INSERT INTO mind_revocations(id,created,target_kind,target_id,content,reason) VALUES (?,?,?,?,?,?)",
|
|
329
|
+
)
|
|
330
|
+
.run(randomUUID(), Date.now(), kind, id, content, note);
|
|
331
|
+
if (kind === "memory") return this.memory.revoke(id, note);
|
|
332
|
+
if (kind === "self") {
|
|
333
|
+
const rows = this.self.history(id);
|
|
334
|
+
if (!rows.length) throw Error("线索不存在");
|
|
335
|
+
tomb(rows.at(-1).content);
|
|
336
|
+
} else if (kind === "face") {
|
|
337
|
+
const row = this.db
|
|
338
|
+
.prepare("SELECT * FROM mind_faces WHERE id=?")
|
|
339
|
+
.get(id);
|
|
340
|
+
if (!row) throw Error("面貌版本不存在");
|
|
341
|
+
tomb(row.content || row.role);
|
|
342
|
+
} else if (kind === "bond") {
|
|
343
|
+
const row = this.db
|
|
344
|
+
.prepare("SELECT * FROM mind_bond_events WHERE id=?")
|
|
345
|
+
.get(id);
|
|
346
|
+
if (!row) throw Error("关系变化不存在");
|
|
347
|
+
tomb(row.note || row.change);
|
|
348
|
+
} else if (kind === "thought") {
|
|
349
|
+
this.thoughts.update(id, { hidden: true });
|
|
350
|
+
tomb(this.thoughts.get(id)?.content || "");
|
|
351
|
+
} else if (kind === "anticipation") {
|
|
352
|
+
tomb(this.anticipations.revoke(id, note));
|
|
353
|
+
} else if (kind === "meeting") {
|
|
354
|
+
const row = this.db
|
|
355
|
+
.prepare("SELECT * FROM mind_meetings WHERE id=?")
|
|
356
|
+
.get(id);
|
|
357
|
+
if (!row) throw Error("相遇不存在");
|
|
358
|
+
tomb(row.appraisal || row.topic);
|
|
359
|
+
} else if (kind === "observation") {
|
|
360
|
+
tomb(this.observations.revoke(id));
|
|
361
|
+
} else throw Error("不能撤销这类内容");
|
|
362
|
+
this.store.revision++;
|
|
363
|
+
}
|
|
364
|
+
revocations(limit = 100) {
|
|
365
|
+
return this.db
|
|
366
|
+
.prepare("SELECT * FROM mind_revocations ORDER BY created DESC LIMIT ?")
|
|
367
|
+
.all(limit);
|
|
368
|
+
}
|
|
369
|
+
// What she was living on a saved day. New snapshots store it; older ones
|
|
370
|
+
// may have dropped the wish from the day's bag, or written a duty there.
|
|
371
|
+
livedThen(snap) {
|
|
372
|
+
if (!snap) return null;
|
|
373
|
+
if (ownLife(snap.livingFor?.content)) return snap.livingFor;
|
|
374
|
+
if (snap.created) {
|
|
375
|
+
const lived = this.self.living({
|
|
376
|
+
before: snap.created,
|
|
377
|
+
now: snap.created,
|
|
378
|
+
});
|
|
379
|
+
if (lived) return { thread: lived.thread, content: lived.content };
|
|
380
|
+
}
|
|
381
|
+
return snapshotLiving(snap);
|
|
382
|
+
}
|
|
383
|
+
// A plain record of who she was at the end of a day, for comparing later.
|
|
384
|
+
snapshot(now = Date.now(), day = dayKey(now, this.timeZone())) {
|
|
385
|
+
const living = this.self.living({ before: now, now });
|
|
386
|
+
const self = this.self
|
|
387
|
+
.active({ before: now, limit: 30 })
|
|
388
|
+
.map(({ thread, kind, content, strength, status }) => ({
|
|
389
|
+
thread,
|
|
390
|
+
kind,
|
|
391
|
+
content,
|
|
392
|
+
strength,
|
|
393
|
+
status,
|
|
394
|
+
}));
|
|
395
|
+
if (living && !self.some((row) => row.thread === living.thread)) {
|
|
396
|
+
self.unshift({
|
|
397
|
+
thread: living.thread,
|
|
398
|
+
kind: living.kind,
|
|
399
|
+
content: living.content,
|
|
400
|
+
strength: living.strength,
|
|
401
|
+
status: living.status,
|
|
402
|
+
});
|
|
403
|
+
self.splice(30);
|
|
404
|
+
}
|
|
405
|
+
const value = {
|
|
406
|
+
affect: this.affect.state(now),
|
|
407
|
+
self,
|
|
408
|
+
...(living
|
|
409
|
+
? { livingFor: { thread: living.thread, content: living.content } }
|
|
410
|
+
: {}),
|
|
411
|
+
faces: this.faces
|
|
412
|
+
.all(now)
|
|
413
|
+
.map(({ session_id, role, tone, aspiration }) => ({
|
|
414
|
+
session: session_id,
|
|
415
|
+
role,
|
|
416
|
+
tone,
|
|
417
|
+
aspiration,
|
|
418
|
+
})),
|
|
419
|
+
people: this.bonds
|
|
420
|
+
.people({ now, limit: 30 })
|
|
421
|
+
.map(({ userId, name, familiarity, closeness, trust, tension }) => ({
|
|
422
|
+
userId,
|
|
423
|
+
name,
|
|
424
|
+
familiarity,
|
|
425
|
+
closeness,
|
|
426
|
+
trust,
|
|
427
|
+
tension,
|
|
428
|
+
})),
|
|
429
|
+
};
|
|
430
|
+
this.db
|
|
431
|
+
.prepare(
|
|
432
|
+
"INSERT INTO mind_snapshots(day,created,value) VALUES (?,?,?) ON CONFLICT(day) DO UPDATE SET created=excluded.created,value=excluded.value",
|
|
433
|
+
)
|
|
434
|
+
.run(day, now, JSON.stringify(value));
|
|
435
|
+
return { day, ...value };
|
|
436
|
+
}
|
|
437
|
+
snapshotOf(day) {
|
|
438
|
+
const row = this.db
|
|
439
|
+
.prepare("SELECT * FROM mind_snapshots WHERE day=?")
|
|
440
|
+
.get(day);
|
|
441
|
+
return row
|
|
442
|
+
? { day: row.day, created: row.created, ...parse(row.value, {}) }
|
|
443
|
+
: null;
|
|
444
|
+
}
|
|
445
|
+
}
|