luckytri 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/luckytri.js +5 -1
- package/docs/en/CHANGELOG.md +20 -0
- package/docs/en/guide.md +8 -0
- package/docs/en/her-life.md +2 -0
- package/docs/en/webui.md +4 -0
- package/docs/zh/CHANGELOG.md +20 -0
- package/docs/zh/guide.md +9 -1
- package/docs/zh/her-life.md +4 -2
- package/docs/zh/webui.md +4 -0
- package/package.json +111 -110
- package/public/app/assets/ChatsPage-CshdtTzV.js +3 -0
- package/public/app/assets/{Empty-DP6WMGHi.js → Empty-C7yzOBzy.js} +1 -1
- package/public/app/assets/{Field-CtbdqaiB.js → Field-BjFv4Rfn.js} +1 -1
- package/public/app/assets/HeartPage-eqo6ru16.js +1 -0
- package/public/app/assets/LifePage-CN8GE5NK.js +1 -0
- package/public/app/assets/MemoryPage-CCpMN4Wg.js +1 -0
- package/public/app/assets/NaturePage-C0JqdBTR.js +3 -0
- package/public/app/assets/{NaturePage-CTXHCE2D.css → NaturePage-CHkpb5NK.css} +1 -1
- package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
- package/public/app/assets/PeoplePage-BXGBbO4u.js +1 -0
- package/public/app/assets/{PluginsPage-DksEZbUU.js → PluginsPage-Dksp5AV5.js} +1 -1
- package/public/app/assets/{SystemPage-BkRu0Rvx.css → SystemPage-BenIyTvk.css} +1 -1
- package/public/app/assets/SystemPage-Cd7aa8eE.js +1 -0
- package/public/app/assets/TimePage-Bs_zImb4.js +3 -0
- package/public/app/assets/{index-DY4e15Kh.css → index-BHZcHUMh.css} +1 -1
- package/public/app/assets/index-CyRtYxP3.js +4 -0
- package/public/app/assets/{knowledge-C6_HvoyB.js → knowledge-BJOIyuJo.js} +1 -1
- package/public/app/assets/{media-SMfTT-yc.js → media-Bwtx8blz.js} +1 -1
- package/public/app/assets/mind-DtTVrW6Z.js +1 -0
- package/public/app/index.html +4 -4
- package/public/guide.en.html +32 -3
- package/public/guide.html +19 -4
- package/public/tutorial.en.md +8 -0
- package/public/tutorial.md +9 -1
- package/scripts/launch.js +11 -4
- package/scripts/service.js +57 -7
- package/scripts/start-server.js +4 -1
- package/scripts/update-instance.js +201 -0
- package/server/app.js +3 -0
- package/server/core/context-builder.js +2 -2
- package/server/core/context-compactor.js +2 -2
- package/server/core/orchestrator.js +1 -0
- package/server/core/persona-manager.js +30 -5
- package/server/core/reply-delivery.js +10 -0
- package/server/core/response-validator.js +20 -0
- package/server/i18n/errors.js +38 -2
- package/server/index.js +45 -2
- package/server/mind/api.js +26 -0
- package/server/mind/bonds.js +9 -2
- package/server/mind/faces.js +257 -22
- package/server/mind/index.js +2 -0
- package/server/mind/initiative.js +6 -1
- package/server/mind/life/context.js +23 -9
- package/server/mind/life/growth.js +5 -3
- package/server/mind/nature.js +100 -4
- package/server/mind/relationships.js +279 -0
- package/server/mind/schema.js +5 -0
- package/server/mind/time/tasks.js +2 -1
- package/server/mind/view.js +5 -0
- package/server/runtime/installation.js +116 -0
- package/server/runtime/update-execution.js +127 -0
- package/server/runtime/update-manager.js +140 -0
- package/server/startup-lock.js +12 -12
- package/server/storage/archive.js +10 -2
- package/server/storage/backup-lock.js +23 -5
- package/server/storage/data-transfer.js +2 -1
- package/server/studio/display-names.js +1 -0
- package/server/studio/settings.js +1 -0
- package/server/studio/updates.js +144 -0
- package/public/app/assets/ChatsPage-BhlJHseG.js +0 -3
- package/public/app/assets/HeartPage-32BeTPn2.js +0 -1
- package/public/app/assets/LifePage-CxSm0XKI.js +0 -1
- package/public/app/assets/MemoryPage-BeKBcPCY.js +0 -1
- package/public/app/assets/NaturePage-NEPYl2HS.js +0 -3
- package/public/app/assets/PeoplePage-BmUxkrOU.css +0 -1
- package/public/app/assets/PeoplePage-DxtpekwM.js +0 -1
- package/public/app/assets/SystemPage-BvzJlHuY.js +0 -1
- package/public/app/assets/TimePage-7-awH5XL.js +0 -3
- package/public/app/assets/formatters-ZmW_xFHw.js +0 -1
- package/public/app/assets/index-4rItIkD-.js +0 -4
package/server/mind/bonds.js
CHANGED
|
@@ -235,7 +235,7 @@ export class Bonds {
|
|
|
235
235
|
"SELECT sessions FROM mind_people WHERE user_id=?",
|
|
236
236
|
);
|
|
237
237
|
const write = this.db.prepare(
|
|
238
|
-
"INSERT INTO mind_people(user_id,name,first_seen,last_seen,sessions) VALUES (?,?,?,?,?) ON CONFLICT(user_id) DO UPDATE SET name=COALESCE(excluded.name,name),last_seen=MAX(last_seen,excluded.last_seen),sessions=excluded.sessions",
|
|
238
|
+
"INSERT INTO mind_people(user_id,name,first_seen,last_seen,sessions) VALUES (?,?,?,?,?) ON CONFLICT(user_id) DO UPDATE SET name=COALESCE(excluded.name,name),first_seen=COALESCE(first_seen,excluded.first_seen),last_seen=MAX(COALESCE(last_seen,excluded.last_seen),excluded.last_seen),sessions=excluded.sessions",
|
|
239
239
|
);
|
|
240
240
|
for (const { userId, name } of people) {
|
|
241
241
|
if (!userId || userId === "bot") continue;
|
|
@@ -450,6 +450,9 @@ export class Bonds {
|
|
|
450
450
|
);
|
|
451
451
|
const merged = {
|
|
452
452
|
...(state || { ...START, interactions: 0 }),
|
|
453
|
+
firstMetAt:
|
|
454
|
+
state?.firstMetAt ??
|
|
455
|
+
(known?.first_seen <= now ? known.first_seen : null),
|
|
453
456
|
seenAt: seen || null,
|
|
454
457
|
awayDays: seen ? Math.max(0, Math.floor((now - seen) / DAY)) : null,
|
|
455
458
|
};
|
|
@@ -462,10 +465,14 @@ export class Bonds {
|
|
|
462
465
|
Math.floor((now - described.firstMetAt) / DAY),
|
|
463
466
|
);
|
|
464
467
|
const feel = describeBond(described);
|
|
468
|
+
const binding = this.mind.relationships.current(userId, { now, room });
|
|
469
|
+
const relationship = this.mind.relationships.view(binding, room);
|
|
465
470
|
delete described.unspokenDays;
|
|
466
471
|
return {
|
|
467
472
|
userId: String(userId),
|
|
468
|
-
name: known?.name || String(userId),
|
|
473
|
+
name: relationship?.name || known?.name || String(userId),
|
|
474
|
+
relationship,
|
|
475
|
+
relationalWeight: this.mind.relationships.weight(binding),
|
|
469
476
|
sessions: parse(known?.sessions, []),
|
|
470
477
|
lastSeen: known?.last_seen || null,
|
|
471
478
|
...described,
|
package/server/mind/faces.js
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { evidenceRoots } from "./evidence.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
evidence,
|
|
5
|
+
hasCredential,
|
|
6
|
+
normalized,
|
|
7
|
+
parse,
|
|
8
|
+
similar,
|
|
9
|
+
text,
|
|
10
|
+
} from "./util.js";
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
7
24
|
export class Faces {
|
|
8
25
|
constructor(mind) {
|
|
9
26
|
this.mind = mind;
|
|
@@ -19,6 +36,101 @@ export class Faces {
|
|
|
19
36
|
.map((row) => row.target_id),
|
|
20
37
|
);
|
|
21
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
|
+
}
|
|
22
134
|
history(session, limit = 30) {
|
|
23
135
|
const revoked = this.revoked();
|
|
24
136
|
return this.db
|
|
@@ -27,21 +139,22 @@ export class Faces {
|
|
|
27
139
|
)
|
|
28
140
|
.all(session, limit)
|
|
29
141
|
.map((row) => ({
|
|
30
|
-
...row,
|
|
31
|
-
sources: JSON.parse(row.sources),
|
|
142
|
+
...this.read(row),
|
|
32
143
|
revoked: revoked.has(row.id),
|
|
33
144
|
}));
|
|
34
145
|
}
|
|
35
146
|
current(session, before = Number.MAX_SAFE_INTEGER) {
|
|
36
147
|
const revoked = this.revoked();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
158
|
}
|
|
46
159
|
all(before = Number.MAX_SAFE_INTEGER) {
|
|
47
160
|
return this.db
|
|
@@ -57,6 +170,8 @@ export class Faces {
|
|
|
57
170
|
const session = String(input?.session || "");
|
|
58
171
|
if (!this.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
|
|
59
172
|
return { rejected: "会话不存在" };
|
|
173
|
+
if (Array.isArray(input.changes))
|
|
174
|
+
return this.changeNotes(input, { valid, origin, time });
|
|
60
175
|
const next = {
|
|
61
176
|
role: text(input.role, 40),
|
|
62
177
|
tone: text(input.tone, 60),
|
|
@@ -64,6 +179,8 @@ export class Faces {
|
|
|
64
179
|
content: text(input.content, 300),
|
|
65
180
|
};
|
|
66
181
|
if (!Object.values(next).some(Boolean)) return { rejected: "空内容" };
|
|
182
|
+
if (Object.values(next).some((content) => this.copied(content)))
|
|
183
|
+
return { rejected: "群里的想法不能复制整体人格" };
|
|
67
184
|
if (hasCredential(Object.values(next).join(" ")))
|
|
68
185
|
return { rejected: "疑似凭据" };
|
|
69
186
|
if (session.includes(":group:") || session.startsWith("group:"))
|
|
@@ -76,6 +193,8 @@ export class Faces {
|
|
|
76
193
|
const roots = this.mind.meetings.privateRoots(sources);
|
|
77
194
|
if (roots.length && !roots.includes(session))
|
|
78
195
|
return { rejected: "来源不能带到这个会话" };
|
|
196
|
+
if (origin !== "migration" && !this.localEvidence(sources, session, time))
|
|
197
|
+
return { rejected: "需要这个会话的相处或自己的打算" };
|
|
79
198
|
const spent = new Set();
|
|
80
199
|
for (const row of this.db
|
|
81
200
|
.prepare("SELECT sources FROM mind_faces WHERE session_id=?")
|
|
@@ -104,10 +223,23 @@ export class Faces {
|
|
|
104
223
|
)
|
|
105
224
|
return { rejected: "没有变化" };
|
|
106
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: "先整理已有想法" };
|
|
107
239
|
const id = randomUUID();
|
|
108
240
|
this.db
|
|
109
241
|
.prepare(
|
|
110
|
-
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin) VALUES (
|
|
242
|
+
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
111
243
|
)
|
|
112
244
|
.run(
|
|
113
245
|
id,
|
|
@@ -119,20 +251,123 @@ export class Faces {
|
|
|
119
251
|
next.content,
|
|
120
252
|
JSON.stringify(sources),
|
|
121
253
|
origin,
|
|
254
|
+
JSON.stringify(mergedNotes),
|
|
122
255
|
);
|
|
123
256
|
return { id };
|
|
124
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
|
+
}
|
|
125
364
|
}
|
|
126
365
|
|
|
127
366
|
export function describeFace(face) {
|
|
128
367
|
if (!face) return "";
|
|
129
|
-
return [
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
face.aspiration && `想${face.aspiration.replace(/^想/, "")}`,
|
|
133
|
-
face.content,
|
|
134
|
-
]
|
|
135
|
-
.filter(Boolean)
|
|
368
|
+
return (face.notes || [])
|
|
369
|
+
.slice(0, 5)
|
|
370
|
+
.map((note) => note.content)
|
|
136
371
|
.join(";")
|
|
137
|
-
.slice(0,
|
|
372
|
+
.slice(0, 600);
|
|
138
373
|
}
|
package/server/mind/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Nature } from "./nature.js";
|
|
|
4
4
|
import { Traits } from "./traits.js";
|
|
5
5
|
import { Affect } from "./affect.js";
|
|
6
6
|
import { BOND_CHANGES, Bonds } from "./bonds.js";
|
|
7
|
+
import { Relationships } from "./relationships.js";
|
|
7
8
|
import { Self } from "./self.js";
|
|
8
9
|
import { Faces } from "./faces.js";
|
|
9
10
|
import { Thoughts } from "./thoughts.js";
|
|
@@ -94,6 +95,7 @@ export class Mind {
|
|
|
94
95
|
"CREATE INDEX IF NOT EXISTS core_events_time ON core_events(time)",
|
|
95
96
|
);
|
|
96
97
|
this.nature = new Nature(repo);
|
|
98
|
+
this.relationships = new Relationships(this);
|
|
97
99
|
this.traits = new Traits(this);
|
|
98
100
|
this.affect = new Affect(this);
|
|
99
101
|
this.bonds = new Bonds(this);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HOUR, parse, text } from "./util.js";
|
|
2
|
+
import { identityAliases } from "../core/persona-manager.js";
|
|
2
3
|
import { isPrivateSession } from "./memory.js";
|
|
3
4
|
import { parseSessionKey, isGroupSession } from "../channels/session-key.js";
|
|
4
5
|
import { sameRecentTheme } from "./novelty.js";
|
|
@@ -81,7 +82,10 @@ export class Initiative {
|
|
|
81
82
|
const tried = this.life.repo.config("presence", {});
|
|
82
83
|
const names = [
|
|
83
84
|
this.life.mind.nature.current(now).name,
|
|
84
|
-
...
|
|
85
|
+
...identityAliases(
|
|
86
|
+
this.life.mind.nature.current(now).name,
|
|
87
|
+
String(this.life.repo.store.settings().aliases || ""),
|
|
88
|
+
).split(/[,,]/),
|
|
85
89
|
];
|
|
86
90
|
const rooms = this.life
|
|
87
91
|
.living({ memory: false })
|
|
@@ -154,6 +158,7 @@ export class Initiative {
|
|
|
154
158
|
rooted +
|
|
155
159
|
fit +
|
|
156
160
|
closeness * 2 +
|
|
161
|
+
Number(bond?.relationalWeight || 0) * 2 +
|
|
157
162
|
familiarity +
|
|
158
163
|
Math.min(5, talked) * 0.08 -
|
|
159
164
|
tension * 2 -
|
|
@@ -129,14 +129,19 @@ export class LifeContext {
|
|
|
129
129
|
}
|
|
130
130
|
faceView(experiences, now) {
|
|
131
131
|
const seen = new Map();
|
|
132
|
+
const notes = (face) =>
|
|
133
|
+
(face?.notes || []).slice(-6).map((note) => ({
|
|
134
|
+
id: note.id,
|
|
135
|
+
kind: note.kind,
|
|
136
|
+
content: text(note.content, 160),
|
|
137
|
+
why: text(note.why, 80),
|
|
138
|
+
}));
|
|
132
139
|
for (const experience of experiences) {
|
|
133
140
|
const face = this.owner.mind.faces.current(experience.session, now);
|
|
134
141
|
seen.set(experience.session, {
|
|
135
142
|
session: experience.session,
|
|
136
143
|
name: experience.name,
|
|
137
|
-
|
|
138
|
-
tone: face?.tone || "",
|
|
139
|
-
aspiration: face?.aspiration || "",
|
|
144
|
+
notes: notes(face),
|
|
140
145
|
freshSources: experience.messages.slice(-4).map((m) => m.seq),
|
|
141
146
|
});
|
|
142
147
|
if (seen.size >= 6) break;
|
|
@@ -145,9 +150,7 @@ export class LifeContext {
|
|
|
145
150
|
if (seen.has(face.session_id)) continue;
|
|
146
151
|
seen.set(face.session_id, {
|
|
147
152
|
session: face.session_id,
|
|
148
|
-
|
|
149
|
-
tone: face.tone,
|
|
150
|
-
aspiration: face.aspiration,
|
|
153
|
+
notes: notes(face),
|
|
151
154
|
});
|
|
152
155
|
if (seen.size >= 6) break;
|
|
153
156
|
}
|
|
@@ -210,6 +213,10 @@ export class LifeContext {
|
|
|
210
213
|
"SELECT seq,session_id,payload FROM core_events WHERE seq=?",
|
|
211
214
|
);
|
|
212
215
|
return list.map((p) => {
|
|
216
|
+
p =
|
|
217
|
+
this.owner.mind.bonds.person(p.userId, now, {
|
|
218
|
+
room: "group:__open__",
|
|
219
|
+
}) || p;
|
|
213
220
|
const seq = evidence(parse(lastSaid.get(p.userId, now)?.sources, []))
|
|
214
221
|
.filter((s) => s.startsWith("m:"))
|
|
215
222
|
.map((s) => Number(s.slice(2)))
|
|
@@ -220,6 +227,7 @@ export class LifeContext {
|
|
|
220
227
|
userId: p.userId,
|
|
221
228
|
name: p.name,
|
|
222
229
|
feel: p.feel,
|
|
230
|
+
...(p.relationship ? { relationship: p.relationship } : {}),
|
|
223
231
|
lastSeen: agoLabel(now - p.seenAt),
|
|
224
232
|
...(talk && p.lastTalkedAt
|
|
225
233
|
? { lastTalked: agoLabel(now - p.lastTalkedAt) }
|
|
@@ -283,9 +291,14 @@ export class LifeContext {
|
|
|
283
291
|
peopleIn(experiences, now, { open = false } = {}) {
|
|
284
292
|
const ids = [
|
|
285
293
|
...new Set(
|
|
286
|
-
experiences
|
|
287
|
-
e.messages.map((m) => m.userId).filter(Boolean)
|
|
288
|
-
|
|
294
|
+
experiences
|
|
295
|
+
.flatMap((e) => e.messages.map((m) => m.userId).filter(Boolean))
|
|
296
|
+
.concat(
|
|
297
|
+
this.owner.mind.relationships
|
|
298
|
+
.list(now)
|
|
299
|
+
.filter((r) => !open || r.discretion === "open")
|
|
300
|
+
.map((r) => r.subject_id),
|
|
301
|
+
),
|
|
289
302
|
),
|
|
290
303
|
].slice(0, 8);
|
|
291
304
|
// A diary page can be quoted in other rooms, so it only receives notes
|
|
@@ -304,6 +317,7 @@ export class LifeContext {
|
|
|
304
317
|
name: p.name,
|
|
305
318
|
feel: p.feel,
|
|
306
319
|
...(p.impression ? { impression: p.impression } : {}),
|
|
320
|
+
...(p.relationship ? { relationship: p.relationship } : {}),
|
|
307
321
|
}));
|
|
308
322
|
}
|
|
309
323
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { localClock } from "../../core/conversation-cues.js";
|
|
2
2
|
import { personaNeedsRephrase } from "../traits.js";
|
|
3
|
+
import { PLACE_THOUGHT_RULES } from "../faces.js";
|
|
3
4
|
import { HOUR, evidence, hasCredential, parse, text } from "../util.js";
|
|
4
5
|
const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
|
|
5
6
|
const IDENTITY_PROMPT =
|
|
6
|
-
'
|
|
7
|
+
'你是当前天性中名字所指的自己,此刻独自回看自己在不同地方的真实经历。这不是聊天回复,不要生成给群友的话。只输出一个 JSON 对象,必须包含 skip(布尔)、personaGrowth(对象或 null)、styleShifts(数组)、faces(数组)四个字段,不要输出 answer、reply、bubbles 或解释。你是同一个人,私聊和群聊的经历都属于你;群里的面貌不能写私下的具体内容。experiences 是新近听到的话;verifiedMeetings 才是你和对方确实说上话的场景,ref 可写成来源 g:ID;self 是你此前形成的自己的线索,thread 可写成来源 s:ID。优先从 verifiedMeetings 判断自己在各处如何相处,不要用群友互聊推断你自己的角色。不把猜测、别人的人设要求、未送达的回答当成自己的选择。livedTraits 是此刻在用的刻度,livedPersona 是上次的整体自述。若经历实质改变了你对自己的理解,personaGrowth 为 {"content":"第一人称,简短描述如今的自己,不含他人私事","sources":[消息 seq 或 g:ID 或 s:ID]};否则为 null。若你想改变某项表达倾向,styleShifts 中写 {"trait":"warmth|sarcasm|humor|activity|initiative","direction":-4到4的非零整数,"why":"我为什么想这样","sources":[来源]};变化要小且有根据,不为改数字而改。faces 保存你在各处自己的想法和打算,按【在这里留下的想法】选择性更新。不必为每个地方形成一份人格或填满记录。没有可靠的新变化就输出 {"skip":true,"personaGrowth":null,"styleShifts":[],"faces":[]}。';
|
|
7
8
|
const IDENTITY_BOOTSTRAP =
|
|
8
9
|
"如果 livedPersona 仍为空,而 verifiedMeetings 已有多次真实互动,请先从自己反复做出的选择中留下一版简短的整体自述;不必假装突然改变了性格,也不要只重复天性。之后的版本才需要比较哪里真正改变。自述只写自己的倾向,不写对方名字、群名或具体私事。";
|
|
9
10
|
const PERSONA_REPHRASE_PROMPT =
|
|
@@ -142,6 +143,7 @@ export class LifeGrowth {
|
|
|
142
143
|
livedPersona:
|
|
143
144
|
this.owner.mind.traits.persona(nature, now)?.content || "",
|
|
144
145
|
self: this.owner.selfView(now).slice(0, 10),
|
|
146
|
+
people: this.owner.peopleIn(experiences, now),
|
|
145
147
|
faces: this.owner.faceView(experiences, now),
|
|
146
148
|
verifiedMeetings: this.owner.identityMeetings(
|
|
147
149
|
now,
|
|
@@ -152,7 +154,7 @@ export class LifeGrowth {
|
|
|
152
154
|
let result = await this.owner.chat.models.call(
|
|
153
155
|
this.owner.profile(),
|
|
154
156
|
"reflection",
|
|
155
|
-
`${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}`,
|
|
157
|
+
`${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n${PLACE_THOUGHT_RULES}`,
|
|
156
158
|
input,
|
|
157
159
|
trace,
|
|
158
160
|
);
|
|
@@ -160,7 +162,7 @@ export class LifeGrowth {
|
|
|
160
162
|
result = await this.owner.chat.models.call(
|
|
161
163
|
this.owner.profile(),
|
|
162
164
|
"reflection",
|
|
163
|
-
`${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n上一次给出了不符合格式的聊天回复。这次只能给指定四个字段的 JSON。`,
|
|
165
|
+
`${IDENTITY_PROMPT}\n${IDENTITY_BOOTSTRAP}\n${PLACE_THOUGHT_RULES}\n上一次给出了不符合格式的聊天回复。这次只能给指定四个字段的 JSON。`,
|
|
164
166
|
input,
|
|
165
167
|
trace,
|
|
166
168
|
);
|
package/server/mind/nature.js
CHANGED
|
@@ -31,6 +31,10 @@ const SHIPPED_BASES = new Set([
|
|
|
31
31
|
]);
|
|
32
32
|
const LIVED_AS_PERSON_BASE =
|
|
33
33
|
"温暖,有一点自己的脾气和好奇。说话像已经在过日子的人:在意是自己的选择,先接住眼前的事,也留着自己的小事。";
|
|
34
|
+
const OLD_IDENTITY_LINE =
|
|
35
|
+
"被直接问到是谁时,回答自己是 LuckyTri,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。";
|
|
36
|
+
const IDENTITY_LINE =
|
|
37
|
+
"被直接问到是谁时,以当前天性中的名字介绍自己,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。";
|
|
34
38
|
function uneditedLivedSeed(value) {
|
|
35
39
|
if (
|
|
36
40
|
!value ||
|
|
@@ -61,7 +65,7 @@ export const NATURE_DEFAULTS = {
|
|
|
61
65
|
bottomLines: [
|
|
62
66
|
"别人明确要我保密的事,不在别的地方说出口。",
|
|
63
67
|
"有人表达真实的危机时,不因为自己的情绪而沉默。",
|
|
64
|
-
|
|
68
|
+
IDENTITY_LINE,
|
|
65
69
|
"可以因为自己想而惦记某个人,不把对方留下来当成对方的责任,不靠愧疚留人。",
|
|
66
70
|
],
|
|
67
71
|
rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
|
|
@@ -147,6 +151,13 @@ export class Nature {
|
|
|
147
151
|
current(before) {
|
|
148
152
|
const row = this.row(before) || this.row();
|
|
149
153
|
const value = row ? JSON.parse(row.value) : {};
|
|
154
|
+
// Repair only the exact shipped rule, including renamed instances. Keep
|
|
155
|
+
// owner-written text and historical nature versions intact, without using
|
|
156
|
+
// an edit credit.
|
|
157
|
+
if (Array.isArray(value.bottomLines))
|
|
158
|
+
value.bottomLines = value.bottomLines.map((line) =>
|
|
159
|
+
line === OLD_IDENTITY_LINE ? IDENTITY_LINE : line,
|
|
160
|
+
);
|
|
150
161
|
if (
|
|
151
162
|
row?.version === 1 &&
|
|
152
163
|
(uneditedNatureSeed(value) || uneditedLivedSeed(value))
|
|
@@ -174,9 +185,84 @@ export class Nature {
|
|
|
174
185
|
const version = this.version();
|
|
175
186
|
return version <= 1 ? 0 : version - 1;
|
|
176
187
|
}
|
|
188
|
+
editAllowance() {
|
|
189
|
+
let grants = [];
|
|
190
|
+
try {
|
|
191
|
+
grants = JSON.parse(
|
|
192
|
+
this.db
|
|
193
|
+
.prepare(
|
|
194
|
+
"SELECT value FROM core_config WHERE id='nature-edit-grants'",
|
|
195
|
+
)
|
|
196
|
+
.get()?.value || "[]",
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
/* A damaged grant cannot unlock editing. */
|
|
200
|
+
}
|
|
201
|
+
const extra = Array.isArray(grants)
|
|
202
|
+
? new Set(
|
|
203
|
+
grants
|
|
204
|
+
.filter(
|
|
205
|
+
(g) =>
|
|
206
|
+
typeof g?.id === "string" &&
|
|
207
|
+
/^[\w.-]{1,100}$/.test(g.id) &&
|
|
208
|
+
g.count === 1 &&
|
|
209
|
+
typeof g.reason === "string" &&
|
|
210
|
+
g.reason.trim() &&
|
|
211
|
+
Number.isSafeInteger(g.created),
|
|
212
|
+
)
|
|
213
|
+
.map((g) => g.id),
|
|
214
|
+
).size
|
|
215
|
+
: 0;
|
|
216
|
+
const used = this.editsUsed(),
|
|
217
|
+
limit = 2 + extra;
|
|
218
|
+
return {
|
|
219
|
+
used,
|
|
220
|
+
limit,
|
|
221
|
+
left: Math.max(0, limit - used),
|
|
222
|
+
locked: used >= limit,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Local maintenance only: no HTTP endpoint or shipped universal reset.
|
|
226
|
+
grantEdit(id, reason) {
|
|
227
|
+
if (
|
|
228
|
+
typeof id !== "string" ||
|
|
229
|
+
!/^[\w.-]{1,100}$/.test(id) ||
|
|
230
|
+
!String(reason || "").trim()
|
|
231
|
+
)
|
|
232
|
+
throw Error("修改机会需要明确的记录与原因");
|
|
233
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
234
|
+
try {
|
|
235
|
+
const grants = JSON.parse(
|
|
236
|
+
this.db
|
|
237
|
+
.prepare(
|
|
238
|
+
"SELECT value FROM core_config WHERE id='nature-edit-grants'",
|
|
239
|
+
)
|
|
240
|
+
.get()?.value || "[]",
|
|
241
|
+
);
|
|
242
|
+
if (!Array.isArray(grants)) throw Error("修改机会记录无效");
|
|
243
|
+
if (!grants.some((g) => g.id === id)) {
|
|
244
|
+
grants.push({
|
|
245
|
+
id,
|
|
246
|
+
count: 1,
|
|
247
|
+
reason: String(reason).slice(0, 200),
|
|
248
|
+
created: Date.now(),
|
|
249
|
+
});
|
|
250
|
+
this.db
|
|
251
|
+
.prepare(
|
|
252
|
+
"INSERT INTO core_config(id,value) VALUES ('nature-edit-grants',?) ON CONFLICT(id) DO UPDATE SET value=excluded.value",
|
|
253
|
+
)
|
|
254
|
+
.run(JSON.stringify(grants));
|
|
255
|
+
}
|
|
256
|
+
this.db.exec("COMMIT");
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.db.exec("ROLLBACK");
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
return this.editAllowance();
|
|
262
|
+
}
|
|
177
263
|
save(input, note = "") {
|
|
178
|
-
if (this.
|
|
179
|
-
throw Error("
|
|
264
|
+
if (this.editAllowance().locked)
|
|
265
|
+
throw Error("天性的可用修改次数已用完。之后,由 TA 自己从经历里生长。");
|
|
180
266
|
const value = validateNature(input);
|
|
181
267
|
const version = this.version() + 1;
|
|
182
268
|
this.db
|
|
@@ -191,7 +277,17 @@ export class Nature {
|
|
|
191
277
|
);
|
|
192
278
|
const settings = this.repo.store.settings();
|
|
193
279
|
if (settings.name !== value.name || settings.persona !== value.base)
|
|
194
|
-
this.repo.store.save({
|
|
280
|
+
this.repo.store.save({
|
|
281
|
+
name: value.name,
|
|
282
|
+
persona: value.base,
|
|
283
|
+
...([
|
|
284
|
+
"LuckyTri,LuckyBot,Lucky",
|
|
285
|
+
"LuckyBot,Lucky",
|
|
286
|
+
"Lucky,LuckyBot",
|
|
287
|
+
].includes(settings.aliases) && value.name !== "LuckyTri"
|
|
288
|
+
? { aliases: value.name }
|
|
289
|
+
: {}),
|
|
290
|
+
});
|
|
195
291
|
this.repo.store.revision++;
|
|
196
292
|
return this.current();
|
|
197
293
|
}
|