luckytri 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -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
+ }