luckytri 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/en/CHANGELOG.md +20 -13
  5. package/docs/en/guide.md +7 -5
  6. package/docs/en/her-life.md +2 -2
  7. package/docs/en/webui.md +1 -1
  8. package/docs/zh/CHANGELOG.md +20 -13
  9. package/docs/zh/guide.md +7 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +2 -1
  13. package/public/app/assets/ChatsPage-CFMVozoG.css +1 -0
  14. package/public/app/assets/{ChatsPage-CshdtTzV.js → ChatsPage-DCYMkJ91.js} +2 -2
  15. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-CLyZkxR5.js} +1 -1
  16. package/public/app/assets/{Empty-DmUB2-Bk.css → Empty-DIobYG3x.css} +1 -1
  17. package/public/app/assets/{Field-BjFv4Rfn.js → Field-BpaRwLI_.js} +1 -1
  18. package/public/app/assets/{HeartPage-eqo6ru16.js → HeartPage-BIOMTiwr.js} +1 -1
  19. package/public/app/assets/HeartPage-C-2jvgzF.css +1 -0
  20. package/public/app/assets/{LifePage-CN8GE5NK.js → LifePage-BCgM4JkR.js} +1 -1
  21. package/public/app/assets/LifePage-CG1OHbRK.css +1 -0
  22. package/public/app/assets/MemoryPage-C_Bu4rfb.css +1 -0
  23. package/public/app/assets/{MemoryPage-CCpMN4Wg.js → MemoryPage-vShvRqMt.js} +1 -1
  24. package/public/app/assets/NaturePage-CPZlImT0.css +1 -0
  25. package/public/app/assets/{NaturePage-C0JqdBTR.js → NaturePage-Cy9ptr4G.js} +2 -2
  26. package/public/app/assets/PeoplePage-3RRUITDz.css +1 -0
  27. package/public/app/assets/{PeoplePage-BXGBbO4u.js → PeoplePage-CUZekB2E.js} +1 -1
  28. package/public/app/assets/{PluginsPage-Dksp5AV5.js → PluginsPage-1RM4rmJP.js} +1 -1
  29. package/public/app/assets/{SystemPage-BenIyTvk.css → SystemPage-BI2CB5Ig.css} +1 -1
  30. package/public/app/assets/{SystemPage-Cd7aa8eE.js → SystemPage-BeT5TmIV.js} +1 -1
  31. package/public/app/assets/TimePage-C-guwPk2.css +1 -0
  32. package/public/app/assets/{TimePage-Bs_zImb4.js → TimePage-taTmPMM8.js} +2 -2
  33. package/public/app/assets/{index-BHZcHUMh.css → index-DY5AM0Hz.css} +1 -1
  34. package/public/app/assets/{index-CyRtYxP3.js → index-DdYwC5ZU.js} +3 -3
  35. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-DJmfu2KN.js} +1 -1
  36. package/public/app/assets/{media-Bwtx8blz.js → media-BEtnE0Gv.js} +1 -1
  37. package/public/app/assets/{mind-DtTVrW6Z.js → mind-Dtiw0-QK.js} +1 -1
  38. package/public/app/index.html +6 -5
  39. package/public/guide.en.html +11 -3
  40. package/public/guide.html +6 -3
  41. package/public/tutorial.en.md +2 -0
  42. package/public/tutorial.md +2 -0
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/service-supervisor.js +50 -0
  46. package/scripts/start-server.js +71 -16
  47. package/scripts/update-instance.js +201 -201
  48. package/server/channels/adapters.js +27 -27
  49. package/server/channels/onebot/adapter.js +288 -288
  50. package/server/channels/qqbot/adapter.js +253 -253
  51. package/server/channels/qqbot/api.js +199 -199
  52. package/server/channels/qqbot/credentials.js +11 -11
  53. package/server/channels/qqbot/gateway.js +274 -274
  54. package/server/channels/qqbot/index.js +389 -389
  55. package/server/channels/segments.js +40 -40
  56. package/server/channels/session-key.js +153 -153
  57. package/server/channels/state.js +42 -42
  58. package/server/core/api.js +564 -564
  59. package/server/core/context-builder.js +288 -288
  60. package/server/core/context-compactor.js +454 -454
  61. package/server/core/conversation-cues.js +325 -325
  62. package/server/core/conversation-manager.js +87 -87
  63. package/server/core/feedback.js +8 -8
  64. package/server/core/group-style.js +72 -72
  65. package/server/core/initiative-context.js +66 -66
  66. package/server/core/input-budget.js +32 -32
  67. package/server/core/local-demo.js +98 -98
  68. package/server/core/message-manager.js +118 -118
  69. package/server/core/message-scheduler.js +78 -78
  70. package/server/core/model-json.js +190 -190
  71. package/server/core/model-presets.js +900 -900
  72. package/server/core/name-call.js +29 -29
  73. package/server/core/network.js +157 -157
  74. package/server/core/person-name.js +22 -22
  75. package/server/core/persona-manager.js +2 -1
  76. package/server/core/reply-delivery.js +404 -403
  77. package/server/core/reply-target-resolver.js +50 -50
  78. package/server/core/response-generator.js +50 -50
  79. package/server/core/response-validator.js +14 -2
  80. package/server/core/retired-prompts.js +51 -51
  81. package/server/core/sessions.js +109 -109
  82. package/server/core/speaker-names.js +92 -92
  83. package/server/core/topic-tracker.js +65 -65
  84. package/server/http.js +18 -18
  85. package/server/i18n/decisions.js +209 -206
  86. package/server/i18n/errors.js +4 -0
  87. package/server/i18n/index.js +144 -144
  88. package/server/i18n/models.js +111 -111
  89. package/server/knowledge/api.js +342 -342
  90. package/server/knowledge/embedding-profile.js +114 -114
  91. package/server/knowledge/index.js +13 -13
  92. package/server/knowledge/retrieval.js +93 -93
  93. package/server/knowledge/schema.js +158 -158
  94. package/server/knowledge/vector-index.js +90 -90
  95. package/server/mind/affect.js +265 -265
  96. package/server/mind/attention.js +200 -200
  97. package/server/mind/bonds.js +565 -565
  98. package/server/mind/clock.js +83 -83
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +60 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -323
  108. package/server/mind/life/diary.js +20 -0
  109. package/server/mind/life/growth.js +261 -254
  110. package/server/mind/life/index.js +10 -2
  111. package/server/mind/life/presence.js +305 -305
  112. package/server/mind/meetings.js +1069 -1069
  113. package/server/mind/memory-policy.js +48 -48
  114. package/server/mind/memory.js +888 -888
  115. package/server/mind/nature.js +358 -358
  116. package/server/mind/novelty.js +19 -19
  117. package/server/mind/periods.js +118 -118
  118. package/server/mind/reading.js +120 -120
  119. package/server/mind/relationship-context.js +71 -0
  120. package/server/mind/relationships.js +390 -279
  121. package/server/mind/salience.js +201 -201
  122. package/server/mind/schema.js +567 -567
  123. package/server/mind/self.js +484 -484
  124. package/server/mind/thoughts.js +274 -274
  125. package/server/mind/time/agenda.js +242 -242
  126. package/server/mind/time/api.js +200 -200
  127. package/server/mind/time/attention.js +34 -34
  128. package/server/mind/time/delivery-links.js +130 -130
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +51 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +432 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +9 -1
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-execution.js +2 -1
  141. package/server/runtime/update-manager.js +140 -140
  142. package/server/storage/data-package.js +298 -298
  143. package/server/storage/data-transfer-api.js +50 -50
  144. package/server/storage/data-transfer-worker.js +18 -18
  145. package/server/storage/data-transfer.js +416 -416
  146. package/server/studio/display-names.js +139 -139
  147. package/server/studio/management.js +102 -102
  148. package/server/studio/updates.js +144 -144
  149. package/server/version.js +10 -10
  150. package/public/app/assets/ChatsPage-DfyPCS9N.css +0 -1
  151. package/public/app/assets/HeartPage-Bft9wZp5.css +0 -1
  152. package/public/app/assets/LifePage-BUi5euBF.css +0 -1
  153. package/public/app/assets/MemoryPage-cZvXfkXb.css +0 -1
  154. package/public/app/assets/NaturePage-CHkpb5NK.css +0 -1
  155. package/public/app/assets/PeoplePage-BTU0H1pM.css +0 -1
  156. package/public/app/assets/TimePage-BOms_n4I.css +0 -1
@@ -1,373 +1,373 @@
1
- import { randomUUID } from "node:crypto";
2
- import { evidenceRoots } from "./evidence.js";
3
- import {
4
- evidence,
5
- hasCredential,
6
- normalized,
7
- parse,
8
- similar,
9
- text,
10
- } from "./util.js";
11
-
12
- export const PLACE_NOTE_KINDS = new Set([
13
- "impression",
14
- "wish",
15
- "preference",
16
- "question",
17
- "boundary",
18
- ]);
19
- export const PLACE_THOUGHT_RULES =
20
- '【在这里留下的想法】faces 是同一个你对这个群或会话的独特想法,不是群人格。每个会话的 notes 是已经留下的想法,可以保持不变;仅在你自己有新体会或打算时选择性更新。可以记下印象、愿望、偏好、疑问或分寸,不套用固定角色,不抄天性或整体自述,不为了填空生成内容,也不把别人的要求直接当作自己的愿望。只引用该会话真实相处的来源,或适合留在此处的自己的想法;不带入其他群或私聊的细节。修改一条不会改掉其他条,放下时保留理由与历史。输出 faces:[{"session":"会话ID","changes":[{"action":"add|revise|remove","id":"修改或放下时填已有 note.id","kind":"impression|wish|preference|question|boundary","content":"第一人称,不超过280字","why":"我为什么选择这样更新","sources":[消息 seq 或 g:ID 或 t:ID 或 s:ID]}]}];没有想改的就留空。这些想法是可以重新考虑的体会,不是全局人格指令,也不会单独提升人格刻度。';
21
-
22
- // One person's thoughts about a place. Rows are versioned note sets, rather
23
- // than copies of her personality. The old fields remain readable in history.
24
- export class Faces {
25
- constructor(mind) {
26
- this.mind = mind;
27
- this.db = mind.db;
28
- }
29
- revoked() {
30
- return new Set(
31
- this.db
32
- .prepare(
33
- "SELECT target_id FROM mind_revocations WHERE target_kind='face'",
34
- )
35
- .all()
36
- .map((row) => row.target_id),
37
- );
38
- }
39
- copied(content) {
40
- const value = normalized(content);
41
- if (value.length < 8) return false;
42
- return this.mind.nature.versions().some(({ version }) => {
43
- const row = this.db
44
- .prepare("SELECT value FROM mind_nature WHERE version=?")
45
- .get(version);
46
- const base = normalized(parse(row?.value, {}).base);
47
- return (
48
- base &&
49
- (base === value ||
50
- (value.length >= 30 && base.includes(value)) ||
51
- (value.length >= 80 && similar(value, base, 0.9)))
52
- );
53
- });
54
- }
55
- localEvidence(sources, session, time) {
56
- const roots = evidenceRoots(this.db, sources, time);
57
- let local = false,
58
- own = false;
59
- for (const root of roots) {
60
- if (root.startsWith("m:")) {
61
- const event = this.db
62
- .prepare(
63
- "SELECT session_id,time,payload FROM core_events WHERE seq=? AND seq NOT IN (SELECT seq FROM mind_unlived)",
64
- )
65
- .get(Number(root.slice(2)));
66
- if (
67
- !event ||
68
- event.time > time ||
69
- parse(event.payload, {}).simulated ||
70
- event.session_id !== session
71
- )
72
- return false;
73
- local = true;
74
- } else if (root.startsWith("s:")) own = true;
75
- else return false;
76
- }
77
- return (
78
- local ||
79
- (own &&
80
- !!this.db
81
- .prepare(
82
- "SELECT 1 FROM core_events WHERE session_id=? AND time<=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND seq NOT IN (SELECT seq FROM mind_unlived) LIMIT 1",
83
- )
84
- .get(session, time))
85
- );
86
- }
87
- read(row) {
88
- if (!row) return null;
89
- const sources = evidence(parse(row.sources, []));
90
- const legacy = row.notes == null;
91
- const notes = legacy
92
- ? row.origin === "migration" ||
93
- !sources.length ||
94
- !this.localEvidence(sources, row.session_id, row.created)
95
- ? []
96
- : Object.entries({
97
- role: row.role,
98
- tone: row.tone,
99
- aspiration: row.aspiration,
100
- content: row.content,
101
- })
102
- .filter(
103
- ([, content]) =>
104
- content && content.length <= 300 && !this.copied(content),
105
- )
106
- .map(([key, content]) => ({
107
- id: `legacy:${key}`,
108
- kind:
109
- key === "aspiration"
110
- ? "wish"
111
- : key === "tone"
112
- ? "preference"
113
- : "impression",
114
- content,
115
- why: "",
116
- sources,
117
- }))
118
- : parse(row.notes, []);
119
- return {
120
- ...row,
121
- sources,
122
- notes: Array.isArray(notes)
123
- ? notes.filter(
124
- (note) =>
125
- note &&
126
- typeof note.id === "string" &&
127
- typeof note.content === "string",
128
- )
129
- : [],
130
- changes: parse(row.changes, []),
131
- legacy,
132
- };
133
- }
134
- history(session, limit = 30) {
135
- const revoked = this.revoked();
136
- return this.db
137
- .prepare(
138
- "SELECT * FROM mind_faces WHERE session_id=? ORDER BY created DESC, rowid DESC LIMIT ?",
139
- )
140
- .all(session, limit)
141
- .map((row) => ({
142
- ...this.read(row),
143
- revoked: revoked.has(row.id),
144
- }));
145
- }
146
- current(session, before = Number.MAX_SAFE_INTEGER) {
147
- const revoked = this.revoked();
148
- for (const row of this.db
149
- .prepare(
150
- "SELECT * FROM mind_faces WHERE session_id=? AND created<=? AND (notes IS NOT NULL OR origin!='migration') ORDER BY created DESC,rowid DESC",
151
- )
152
- .iterate(session, before)) {
153
- if (revoked.has(row.id)) continue;
154
- const face = this.read(row);
155
- if (!face.legacy || face.notes.length) return face;
156
- }
157
- return null;
158
- }
159
- all(before = Number.MAX_SAFE_INTEGER) {
160
- return this.db
161
- .prepare("SELECT DISTINCT session_id FROM mind_faces WHERE created<=?")
162
- .all(before)
163
- .map((row) => this.current(row.session_id, before))
164
- .filter(Boolean);
165
- }
166
- propose(
167
- input,
168
- { valid = null, origin = "solitude", time = Date.now() } = {},
169
- ) {
170
- const session = String(input?.session || "");
171
- if (!this.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
172
- return { rejected: "会话不存在" };
173
- if (Array.isArray(input.changes))
174
- return this.changeNotes(input, { valid, origin, time });
175
- const next = {
176
- role: text(input.role, 40),
177
- tone: text(input.tone, 60),
178
- aspiration: text(input.aspiration, 80),
179
- content: text(input.content, 300),
180
- };
181
- if (!Object.values(next).some(Boolean)) return { rejected: "空内容" };
182
- if (Object.values(next).some((content) => this.copied(content)))
183
- return { rejected: "群里的想法不能复制整体人格" };
184
- if (hasCredential(Object.values(next).join(" ")))
185
- return { rejected: "疑似凭据" };
186
- if (session.includes(":group:") || session.startsWith("group:"))
187
- if (!this.mind.meetings.sayable(Object.values(next).join(" "), session))
188
- return { rejected: "不能把私下说法写进群里的面貌" };
189
- const cited = evidence(input.sources);
190
- const sources = valid ? cited.filter((s) => valid.has(s)) : cited;
191
- if (!sources.length && origin !== "migration")
192
- return { rejected: "缺少来源" };
193
- const roots = this.mind.meetings.privateRoots(sources);
194
- if (roots.length && !roots.includes(session))
195
- return { rejected: "来源不能带到这个会话" };
196
- if (origin !== "migration" && !this.localEvidence(sources, session, time))
197
- return { rejected: "需要这个会话的相处或自己的打算" };
198
- const spent = new Set();
199
- for (const row of this.db
200
- .prepare("SELECT sources FROM mind_faces WHERE session_id=?")
201
- .all(session))
202
- for (const source of evidenceRoots(
203
- this.db,
204
- JSON.parse(row.sources || "[]"),
205
- time,
206
- ))
207
- spent.add(source);
208
- if (
209
- sources.length &&
210
- !evidenceRoots(this.db, sources, time).some(
211
- (source) => !spent.has(source),
212
- )
213
- )
214
- return { rejected: "没有新的经历" };
215
- const current = this.current(session, time);
216
- if (current) {
217
- for (const key of Object.keys(next))
218
- if (!next[key]) next[key] = current[key];
219
- if (
220
- Object.keys(next).every((key) =>
221
- next[key] ? similar(next[key], current[key], 0.85) : !current[key],
222
- )
223
- )
224
- return { rejected: "没有变化" };
225
- }
226
- const mergedNotes = [
227
- ...(current?.notes || []).filter(
228
- (note) => !note.id.startsWith("legacy:"),
229
- ),
230
- ...this.read({
231
- ...next,
232
- sources,
233
- origin,
234
- session_id: session,
235
- created: time,
236
- }).notes,
237
- ];
238
- if (mergedNotes.length > 12) return { rejected: "先整理已有想法" };
239
- const id = randomUUID();
240
- this.db
241
- .prepare(
242
- "INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes) VALUES (?,?,?,?,?,?,?,?,?,?)",
243
- )
244
- .run(
245
- id,
246
- session,
247
- time,
248
- next.role,
249
- next.tone,
250
- next.aspiration,
251
- next.content,
252
- JSON.stringify(sources),
253
- origin,
254
- JSON.stringify(mergedNotes),
255
- );
256
- return { id };
257
- }
258
- changeNotes(input, { valid, origin, time }) {
259
- const session = String(input.session);
260
- const current = this.current(session, time);
261
- const notes = structuredClone(current?.notes || []);
262
- const spentRoots = new Set(),
263
- spentRefs = new Set();
264
- for (const row of this.db
265
- .prepare("SELECT sources FROM mind_faces WHERE session_id=?")
266
- .all(session)) {
267
- const refs = evidence(parse(row.sources, []));
268
- refs.forEach((ref) => spentRefs.add(ref));
269
- evidenceRoots(this.db, refs, time).forEach((ref) => spentRoots.add(ref));
270
- }
271
- const changes = [],
272
- rejected = [];
273
- for (const item of input.changes.slice(0, 8)) {
274
- const action = String(item?.action || "");
275
- const index = notes.findIndex((note) => note.id === item?.id);
276
- const content = text(item?.content, 280),
277
- why = text(item?.why, 200);
278
- const kind = String(item?.kind || notes[index]?.kind || "impression");
279
- const sources = evidence(item?.sources || input.sources).filter(
280
- (s) => !valid || valid.has(s),
281
- );
282
- const roots = evidenceRoots(this.db, sources, time);
283
- let reason = "";
284
- if (
285
- !["add", "revise", "remove"].includes(action) ||
286
- !PLACE_NOTE_KINDS.has(kind)
287
- )
288
- reason = "想法更新格式无效";
289
- else if (action !== "add" && index < 0) reason = "要修改的想法不存在";
290
- else if (!why || (action !== "remove" && !content))
291
- reason = "需要想法与选择理由";
292
- else if (hasCredential(`${content} ${why}`)) reason = "疑似凭据";
293
- else if (this.copied(content)) reason = "群里的想法不能复制整体人格";
294
- else if (!sources.length || !roots.length) reason = "缺少来源";
295
- else if (
296
- !this.mind.meetings.sayable(`${content} ${why}`, session) ||
297
- this.mind.meetings
298
- .privateRoots(sources)
299
- .some((room) => room !== session)
300
- )
301
- reason = "来源不能带到这个会话";
302
- else {
303
- if (!this.localEvidence(sources, session, time))
304
- reason = "需要这个会话的相处或自己的打算";
305
- else if (
306
- !roots.some((root) => !spentRoots.has(root)) &&
307
- !sources.some((ref) => /^[ts]:/.test(ref) && !spentRefs.has(ref))
308
- )
309
- reason = "没有新的经历或想法";
310
- }
311
- if (
312
- !reason &&
313
- action !== "remove" &&
314
- notes.some((n, i) => i !== index && similar(n.content, content, 0.9))
315
- )
316
- reason = "已经有这个想法";
317
- if (
318
- !reason &&
319
- action === "revise" &&
320
- normalized(notes[index].content) === normalized(content) &&
321
- notes[index].kind === kind
322
- )
323
- reason = "没有变化";
324
- if (!reason && action === "add" && notes.length >= 12)
325
- reason = "先整理已有想法";
326
- if (reason) {
327
- rejected.push({ id: item?.id || "", reason });
328
- continue;
329
- }
330
- const note = {
331
- id: action === "add" ? randomUUID() : item.id,
332
- kind,
333
- content: action === "remove" ? notes[index].content : content,
334
- why,
335
- sources,
336
- };
337
- if (action === "remove") notes.splice(index, 1);
338
- else if (action === "revise") notes[index] = note;
339
- else notes.push(note);
340
- changes.push({ action, ...note });
341
- }
342
- if (!changes.length)
343
- return {
344
- rejected: rejected[0]?.reason || "没有变化",
345
- rejectedChanges: rejected,
346
- };
347
- const id = randomUUID();
348
- const sources = evidence(changes.flatMap((c) => c.sources));
349
- this.db
350
- .prepare(
351
- "INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes,changes) VALUES (?,?,?,'','','','',?,?,?,?)",
352
- )
353
- .run(
354
- id,
355
- session,
356
- time,
357
- JSON.stringify(sources),
358
- origin,
359
- JSON.stringify(notes),
360
- JSON.stringify(changes),
361
- );
362
- return { id, changed: changes.length, rejectedChanges: rejected };
363
- }
364
- }
365
-
366
- export function describeFace(face) {
367
- if (!face) return "";
368
- return (face.notes || [])
369
- .slice(0, 5)
370
- .map((note) => note.content)
371
- .join(";")
372
- .slice(0, 600);
373
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { evidenceRoots } from "./evidence.js";
3
+ import {
4
+ evidence,
5
+ hasCredential,
6
+ normalized,
7
+ parse,
8
+ similar,
9
+ text,
10
+ } from "./util.js";
11
+
12
+ export const PLACE_NOTE_KINDS = new Set([
13
+ "impression",
14
+ "wish",
15
+ "preference",
16
+ "question",
17
+ "boundary",
18
+ ]);
19
+ export const PLACE_THOUGHT_RULES =
20
+ '【在这里留下的想法】faces 是同一个你对这个群或会话的独特想法,不是群人格。每个会话的 notes 是已经留下的想法,可以保持不变;仅在你自己有新体会或打算时选择性更新。可以记下印象、愿望、偏好、疑问或分寸,不套用固定角色,不抄天性或整体自述,不为了填空生成内容,也不把别人的要求直接当作自己的愿望。只引用该会话真实相处的来源,或适合留在此处的自己的想法;不带入其他群或私聊的细节。修改一条不会改掉其他条,放下时保留理由与历史。输出 faces:[{"session":"会话ID","changes":[{"action":"add|revise|remove","id":"修改或放下时填已有 note.id","kind":"impression|wish|preference|question|boundary","content":"第一人称,不超过280字","why":"我为什么选择这样更新","sources":[消息 seq 或 g:ID 或 t:ID 或 s:ID]}]}];没有想改的就留空。这些想法是可以重新考虑的体会,不是全局人格指令,也不会单独提升人格刻度。';
21
+
22
+ // One person's thoughts about a place. Rows are versioned note sets, rather
23
+ // than copies of her personality. The old fields remain readable in history.
24
+ export class Faces {
25
+ constructor(mind) {
26
+ this.mind = mind;
27
+ this.db = mind.db;
28
+ }
29
+ revoked() {
30
+ return new Set(
31
+ this.db
32
+ .prepare(
33
+ "SELECT target_id FROM mind_revocations WHERE target_kind='face'",
34
+ )
35
+ .all()
36
+ .map((row) => row.target_id),
37
+ );
38
+ }
39
+ copied(content) {
40
+ const value = normalized(content);
41
+ if (value.length < 8) return false;
42
+ return this.mind.nature.versions().some(({ version }) => {
43
+ const row = this.db
44
+ .prepare("SELECT value FROM mind_nature WHERE version=?")
45
+ .get(version);
46
+ const base = normalized(parse(row?.value, {}).base);
47
+ return (
48
+ base &&
49
+ (base === value ||
50
+ (value.length >= 30 && base.includes(value)) ||
51
+ (value.length >= 80 && similar(value, base, 0.9)))
52
+ );
53
+ });
54
+ }
55
+ localEvidence(sources, session, time) {
56
+ const roots = evidenceRoots(this.db, sources, time);
57
+ let local = false,
58
+ own = false;
59
+ for (const root of roots) {
60
+ if (root.startsWith("m:")) {
61
+ const event = this.db
62
+ .prepare(
63
+ "SELECT session_id,time,payload FROM core_events WHERE seq=? AND seq NOT IN (SELECT seq FROM mind_unlived)",
64
+ )
65
+ .get(Number(root.slice(2)));
66
+ if (
67
+ !event ||
68
+ event.time > time ||
69
+ parse(event.payload, {}).simulated ||
70
+ event.session_id !== session
71
+ )
72
+ return false;
73
+ local = true;
74
+ } else if (root.startsWith("s:")) own = true;
75
+ else return false;
76
+ }
77
+ return (
78
+ local ||
79
+ (own &&
80
+ !!this.db
81
+ .prepare(
82
+ "SELECT 1 FROM core_events WHERE session_id=? AND time<=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND seq NOT IN (SELECT seq FROM mind_unlived) LIMIT 1",
83
+ )
84
+ .get(session, time))
85
+ );
86
+ }
87
+ read(row) {
88
+ if (!row) return null;
89
+ const sources = evidence(parse(row.sources, []));
90
+ const legacy = row.notes == null;
91
+ const notes = legacy
92
+ ? row.origin === "migration" ||
93
+ !sources.length ||
94
+ !this.localEvidence(sources, row.session_id, row.created)
95
+ ? []
96
+ : Object.entries({
97
+ role: row.role,
98
+ tone: row.tone,
99
+ aspiration: row.aspiration,
100
+ content: row.content,
101
+ })
102
+ .filter(
103
+ ([, content]) =>
104
+ content && content.length <= 300 && !this.copied(content),
105
+ )
106
+ .map(([key, content]) => ({
107
+ id: `legacy:${key}`,
108
+ kind:
109
+ key === "aspiration"
110
+ ? "wish"
111
+ : key === "tone"
112
+ ? "preference"
113
+ : "impression",
114
+ content,
115
+ why: "",
116
+ sources,
117
+ }))
118
+ : parse(row.notes, []);
119
+ return {
120
+ ...row,
121
+ sources,
122
+ notes: Array.isArray(notes)
123
+ ? notes.filter(
124
+ (note) =>
125
+ note &&
126
+ typeof note.id === "string" &&
127
+ typeof note.content === "string",
128
+ )
129
+ : [],
130
+ changes: parse(row.changes, []),
131
+ legacy,
132
+ };
133
+ }
134
+ history(session, limit = 30) {
135
+ const revoked = this.revoked();
136
+ return this.db
137
+ .prepare(
138
+ "SELECT * FROM mind_faces WHERE session_id=? ORDER BY created DESC, rowid DESC LIMIT ?",
139
+ )
140
+ .all(session, limit)
141
+ .map((row) => ({
142
+ ...this.read(row),
143
+ revoked: revoked.has(row.id),
144
+ }));
145
+ }
146
+ current(session, before = Number.MAX_SAFE_INTEGER) {
147
+ const revoked = this.revoked();
148
+ for (const row of this.db
149
+ .prepare(
150
+ "SELECT * FROM mind_faces WHERE session_id=? AND created<=? AND (notes IS NOT NULL OR origin!='migration') ORDER BY created DESC,rowid DESC",
151
+ )
152
+ .iterate(session, before)) {
153
+ if (revoked.has(row.id)) continue;
154
+ const face = this.read(row);
155
+ if (!face.legacy || face.notes.length) return face;
156
+ }
157
+ return null;
158
+ }
159
+ all(before = Number.MAX_SAFE_INTEGER) {
160
+ return this.db
161
+ .prepare("SELECT DISTINCT session_id FROM mind_faces WHERE created<=?")
162
+ .all(before)
163
+ .map((row) => this.current(row.session_id, before))
164
+ .filter(Boolean);
165
+ }
166
+ propose(
167
+ input,
168
+ { valid = null, origin = "solitude", time = Date.now() } = {},
169
+ ) {
170
+ const session = String(input?.session || "");
171
+ if (!this.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
172
+ return { rejected: "会话不存在" };
173
+ if (Array.isArray(input.changes))
174
+ return this.changeNotes(input, { valid, origin, time });
175
+ const next = {
176
+ role: text(input.role, 40),
177
+ tone: text(input.tone, 60),
178
+ aspiration: text(input.aspiration, 80),
179
+ content: text(input.content, 300),
180
+ };
181
+ if (!Object.values(next).some(Boolean)) return { rejected: "空内容" };
182
+ if (Object.values(next).some((content) => this.copied(content)))
183
+ return { rejected: "群里的想法不能复制整体人格" };
184
+ if (hasCredential(Object.values(next).join(" ")))
185
+ return { rejected: "疑似凭据" };
186
+ if (session.includes(":group:") || session.startsWith("group:"))
187
+ if (!this.mind.meetings.sayable(Object.values(next).join(" "), session))
188
+ return { rejected: "不能把私下说法写进群里的面貌" };
189
+ const cited = evidence(input.sources);
190
+ const sources = valid ? cited.filter((s) => valid.has(s)) : cited;
191
+ if (!sources.length && origin !== "migration")
192
+ return { rejected: "缺少来源" };
193
+ const roots = this.mind.meetings.privateRoots(sources);
194
+ if (roots.length && !roots.includes(session))
195
+ return { rejected: "来源不能带到这个会话" };
196
+ if (origin !== "migration" && !this.localEvidence(sources, session, time))
197
+ return { rejected: "需要这个会话的相处或自己的打算" };
198
+ const spent = new Set();
199
+ for (const row of this.db
200
+ .prepare("SELECT sources FROM mind_faces WHERE session_id=?")
201
+ .all(session))
202
+ for (const source of evidenceRoots(
203
+ this.db,
204
+ JSON.parse(row.sources || "[]"),
205
+ time,
206
+ ))
207
+ spent.add(source);
208
+ if (
209
+ sources.length &&
210
+ !evidenceRoots(this.db, sources, time).some(
211
+ (source) => !spent.has(source),
212
+ )
213
+ )
214
+ return { rejected: "没有新的经历" };
215
+ const current = this.current(session, time);
216
+ if (current) {
217
+ for (const key of Object.keys(next))
218
+ if (!next[key]) next[key] = current[key];
219
+ if (
220
+ Object.keys(next).every((key) =>
221
+ next[key] ? similar(next[key], current[key], 0.85) : !current[key],
222
+ )
223
+ )
224
+ return { rejected: "没有变化" };
225
+ }
226
+ const mergedNotes = [
227
+ ...(current?.notes || []).filter(
228
+ (note) => !note.id.startsWith("legacy:"),
229
+ ),
230
+ ...this.read({
231
+ ...next,
232
+ sources,
233
+ origin,
234
+ session_id: session,
235
+ created: time,
236
+ }).notes,
237
+ ];
238
+ if (mergedNotes.length > 12) return { rejected: "先整理已有想法" };
239
+ const id = randomUUID();
240
+ this.db
241
+ .prepare(
242
+ "INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes) VALUES (?,?,?,?,?,?,?,?,?,?)",
243
+ )
244
+ .run(
245
+ id,
246
+ session,
247
+ time,
248
+ next.role,
249
+ next.tone,
250
+ next.aspiration,
251
+ next.content,
252
+ JSON.stringify(sources),
253
+ origin,
254
+ JSON.stringify(mergedNotes),
255
+ );
256
+ return { id };
257
+ }
258
+ changeNotes(input, { valid, origin, time }) {
259
+ const session = String(input.session);
260
+ const current = this.current(session, time);
261
+ const notes = structuredClone(current?.notes || []);
262
+ const spentRoots = new Set(),
263
+ spentRefs = new Set();
264
+ for (const row of this.db
265
+ .prepare("SELECT sources FROM mind_faces WHERE session_id=?")
266
+ .all(session)) {
267
+ const refs = evidence(parse(row.sources, []));
268
+ refs.forEach((ref) => spentRefs.add(ref));
269
+ evidenceRoots(this.db, refs, time).forEach((ref) => spentRoots.add(ref));
270
+ }
271
+ const changes = [],
272
+ rejected = [];
273
+ for (const item of input.changes.slice(0, 8)) {
274
+ const action = String(item?.action || "");
275
+ const index = notes.findIndex((note) => note.id === item?.id);
276
+ const content = text(item?.content, 280),
277
+ why = text(item?.why, 200);
278
+ const kind = String(item?.kind || notes[index]?.kind || "impression");
279
+ const sources = evidence(item?.sources || input.sources).filter(
280
+ (s) => !valid || valid.has(s),
281
+ );
282
+ const roots = evidenceRoots(this.db, sources, time);
283
+ let reason = "";
284
+ if (
285
+ !["add", "revise", "remove"].includes(action) ||
286
+ !PLACE_NOTE_KINDS.has(kind)
287
+ )
288
+ reason = "想法更新格式无效";
289
+ else if (action !== "add" && index < 0) reason = "要修改的想法不存在";
290
+ else if (!why || (action !== "remove" && !content))
291
+ reason = "需要想法与选择理由";
292
+ else if (hasCredential(`${content} ${why}`)) reason = "疑似凭据";
293
+ else if (this.copied(content)) reason = "群里的想法不能复制整体人格";
294
+ else if (!sources.length || !roots.length) reason = "缺少来源";
295
+ else if (
296
+ !this.mind.meetings.sayable(`${content} ${why}`, session) ||
297
+ this.mind.meetings
298
+ .privateRoots(sources)
299
+ .some((room) => room !== session)
300
+ )
301
+ reason = "来源不能带到这个会话";
302
+ else {
303
+ if (!this.localEvidence(sources, session, time))
304
+ reason = "需要这个会话的相处或自己的打算";
305
+ else if (
306
+ !roots.some((root) => !spentRoots.has(root)) &&
307
+ !sources.some((ref) => /^[ts]:/.test(ref) && !spentRefs.has(ref))
308
+ )
309
+ reason = "没有新的经历或想法";
310
+ }
311
+ if (
312
+ !reason &&
313
+ action !== "remove" &&
314
+ notes.some((n, i) => i !== index && similar(n.content, content, 0.9))
315
+ )
316
+ reason = "已经有这个想法";
317
+ if (
318
+ !reason &&
319
+ action === "revise" &&
320
+ normalized(notes[index].content) === normalized(content) &&
321
+ notes[index].kind === kind
322
+ )
323
+ reason = "没有变化";
324
+ if (!reason && action === "add" && notes.length >= 12)
325
+ reason = "先整理已有想法";
326
+ if (reason) {
327
+ rejected.push({ id: item?.id || "", reason });
328
+ continue;
329
+ }
330
+ const note = {
331
+ id: action === "add" ? randomUUID() : item.id,
332
+ kind,
333
+ content: action === "remove" ? notes[index].content : content,
334
+ why,
335
+ sources,
336
+ };
337
+ if (action === "remove") notes.splice(index, 1);
338
+ else if (action === "revise") notes[index] = note;
339
+ else notes.push(note);
340
+ changes.push({ action, ...note });
341
+ }
342
+ if (!changes.length)
343
+ return {
344
+ rejected: rejected[0]?.reason || "没有变化",
345
+ rejectedChanges: rejected,
346
+ };
347
+ const id = randomUUID();
348
+ const sources = evidence(changes.flatMap((c) => c.sources));
349
+ this.db
350
+ .prepare(
351
+ "INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes,changes) VALUES (?,?,?,'','','','',?,?,?,?)",
352
+ )
353
+ .run(
354
+ id,
355
+ session,
356
+ time,
357
+ JSON.stringify(sources),
358
+ origin,
359
+ JSON.stringify(notes),
360
+ JSON.stringify(changes),
361
+ );
362
+ return { id, changed: changes.length, rejectedChanges: rejected };
363
+ }
364
+ }
365
+
366
+ export function describeFace(face) {
367
+ if (!face) return "";
368
+ return (face.notes || [])
369
+ .slice(0, 5)
370
+ .map((note) => note.content)
371
+ .join(";")
372
+ .slice(0, 600);
373
+ }