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.
Files changed (133) 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 +192 -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 +192 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +109 -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-B9Odvzfz.js +3 -0
  19. package/public/app/assets/Empty-SkA8PyMo.js +2 -0
  20. package/public/app/assets/Field-BjLhi8pL.js +1 -0
  21. package/public/app/assets/HeartPage-CscKqMsr.js +1 -0
  22. package/public/app/assets/LifePage-BHCmBHlH.js +1 -0
  23. package/public/app/assets/MemoryPage-Bhs2xa1k.js +1 -0
  24. package/public/app/assets/NaturePage-CaPIar_5.js +3 -0
  25. package/public/app/assets/PeoplePage-CuwSSVeX.js +1 -0
  26. package/public/app/assets/PluginsPage-BhVm1SUi.js +2 -0
  27. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-5yYqyUgR.css} +1 -1
  29. package/public/app/assets/SystemPage-Chm5fiGP.js +1 -0
  30. package/public/app/assets/TimePage-BUm4eUzB.js +3 -0
  31. package/public/app/assets/index-BIAdfObg.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-DNDLyN9m.js} +1 -1
  34. package/public/app/assets/media-CKVAvML2.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +32 -99
  37. package/public/guide.html +21 -86
  38. package/public/tutorial.en.md +15 -39
  39. package/public/tutorial.md +15 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/launch.js +101 -94
  43. package/scripts/plugin.js +150 -150
  44. package/scripts/reset-password.js +22 -0
  45. package/scripts/runtime.js +25 -25
  46. package/scripts/service.js +56 -47
  47. package/scripts/setup.js +50 -50
  48. package/scripts/start-server.js +41 -41
  49. package/scripts/stop.js +36 -36
  50. package/server/app.js +110 -96
  51. package/server/auth.js +217 -0
  52. package/server/channels/contract.js +57 -57
  53. package/server/channels/hub.js +165 -165
  54. package/server/channels/index.js +28 -25
  55. package/server/channels/onebot/index.js +335 -336
  56. package/server/channels/onebot/token.js +22 -3
  57. package/server/core/capabilities.js +208 -208
  58. package/server/core/events.js +64 -60
  59. package/server/core/image-preprocessor.js +208 -208
  60. package/server/core/model-manager.js +1213 -1213
  61. package/server/core/net-guard.js +49 -49
  62. package/server/core/orchestrator.js +569 -569
  63. package/server/core/perceivers.js +91 -91
  64. package/server/core/persona-manager.js +243 -243
  65. package/server/core/repository.js +293 -293
  66. package/server/core/response-validator.js +508 -508
  67. package/server/core/turn-processor.js +630 -630
  68. package/server/core/turn.js +236 -236
  69. package/server/core/vision-manager.js +633 -633
  70. package/server/i18n/errors.js +512 -497
  71. package/server/i18n/labels.js +649 -653
  72. package/server/index.js +107 -102
  73. package/server/knowledge/manager.js +386 -386
  74. package/server/local-access.js +32 -0
  75. package/server/mind/anticipations.js +507 -507
  76. package/server/mind/api.js +703 -703
  77. package/server/mind/budget.js +152 -152
  78. package/server/mind/context-selection.js +61 -61
  79. package/server/mind/day-planner.js +324 -324
  80. package/server/mind/evidence.js +148 -148
  81. package/server/mind/index.js +445 -445
  82. package/server/mind/life/diary.js +786 -786
  83. package/server/mind/life/index.js +1049 -1049
  84. package/server/mind/observations.js +84 -84
  85. package/server/mind/own-day.js +324 -324
  86. package/server/mind/own-voice.js +318 -318
  87. package/server/mind/senses.js +53 -53
  88. package/server/mind/time/activity-clock.js +196 -196
  89. package/server/mind/time/availability.js +69 -69
  90. package/server/mind/time/commitment-review.js +198 -198
  91. package/server/mind/time/executor.js +512 -512
  92. package/server/mind/time/experiences.js +128 -128
  93. package/server/mind/time/games.js +562 -562
  94. package/server/mind/time/index.js +739 -739
  95. package/server/mind/time/intent.js +166 -166
  96. package/server/mind/time/kinds.js +171 -171
  97. package/server/mind/time/plugin-activity.js +207 -207
  98. package/server/mind/time/tasks.js +587 -587
  99. package/server/mind/util.js +110 -110
  100. package/server/mind/view.js +273 -273
  101. package/server/paths.js +102 -102
  102. package/server/plugins/api.js +204 -204
  103. package/server/plugins/host.js +967 -967
  104. package/server/plugins/install.js +295 -295
  105. package/server/plugins/manifest.js +201 -201
  106. package/server/plugins/permissions.js +42 -42
  107. package/server/plugins/runner/guard.mjs +36 -36
  108. package/server/plugins/runner/worker.mjs +250 -250
  109. package/server/plugins/schema.js +91 -91
  110. package/server/plugins/zip.js +112 -112
  111. package/server/startup-lock.js +118 -118
  112. package/server/storage/archive.js +241 -241
  113. package/server/storage/backup-cleanup-worker.js +8 -8
  114. package/server/storage/backup-cleanup.js +414 -414
  115. package/server/storage/backup-lock.js +76 -76
  116. package/server/storage/backup-scheduler.js +131 -131
  117. package/server/storage/store.js +199 -199
  118. package/server/studio/readiness.js +181 -190
  119. package/server/studio/settings.js +236 -236
  120. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  121. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  122. package/public/app/assets/Field-D4gY-INU.js +0 -1
  123. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  124. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  125. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  126. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  127. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  128. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  129. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  130. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  131. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  132. package/public/app/assets/index-BheRW-1t.js +0 -4
  133. 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
+ }