luckytri 1.0.3 → 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 +7 -0
- package/docs/en/guide.md +3 -1
- package/docs/en/webui.md +3 -1
- package/docs/zh/CHANGELOG.md +7 -0
- package/docs/zh/guide.md +3 -1
- package/docs/zh/webui.md +3 -1
- package/package.json +4 -3
- package/public/app/assets/{ChatsPage-B83Evc79.js → ChatsPage-CshdtTzV.js} +1 -1
- package/public/app/assets/{Empty-ddEfu0w8.js → Empty-C7yzOBzy.js} +1 -1
- package/public/app/assets/{Field-mIhV5YBA.js → Field-BjFv4Rfn.js} +1 -1
- package/public/app/assets/{HeartPage-BGEn24rc.js → HeartPage-eqo6ru16.js} +1 -1
- package/public/app/assets/{LifePage-BleqPj4O.js → LifePage-CN8GE5NK.js} +1 -1
- package/public/app/assets/{MemoryPage-BM3-0_lF.js → MemoryPage-CCpMN4Wg.js} +1 -1
- package/public/app/assets/{NaturePage-CxxMnQIc.js → NaturePage-C0JqdBTR.js} +2 -2
- package/public/app/assets/{PeoplePage-DDDHESQp.css → PeoplePage-BTU0H1pM.css} +1 -1
- package/public/app/assets/PeoplePage-BXGBbO4u.js +1 -0
- package/public/app/assets/{PluginsPage-DE_-Q3ra.js → PluginsPage-Dksp5AV5.js} +1 -1
- package/public/app/assets/{SystemPage-Be1a6UDP.css → SystemPage-BenIyTvk.css} +1 -1
- package/public/app/assets/{SystemPage-BWQKEQRU.js → SystemPage-Cd7aa8eE.js} +1 -1
- package/public/app/assets/{TimePage-Dzt8t8kL.js → TimePage-Bs_zImb4.js} +1 -1
- package/public/app/assets/{index-DY4e15Kh.css → index-BHZcHUMh.css} +1 -1
- package/public/app/assets/{index-oLIXWxsn.js → index-CyRtYxP3.js} +3 -3
- package/public/app/assets/{knowledge-Dvtmpd45.js → knowledge-BJOIyuJo.js} +1 -1
- package/public/app/assets/{media-keE52cIX.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 +23 -8
- package/public/guide.html +11 -5
- package/public/tutorial.en.md +3 -1
- package/public/tutorial.md +3 -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 +1 -1
- 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 +10 -0
- package/server/core/reply-delivery.js +10 -0
- package/server/core/response-validator.js +20 -0
- package/server/i18n/errors.js +25 -0
- package/server/index.js +45 -2
- package/server/mind/api.js +25 -0
- package/server/mind/bonds.js +9 -2
- package/server/mind/index.js +2 -0
- package/server/mind/initiative.js +6 -1
- package/server/mind/life/context.js +14 -3
- package/server/mind/life/growth.js +2 -1
- package/server/mind/nature.js +23 -2
- package/server/mind/relationships.js +279 -0
- package/server/mind/schema.js +2 -0
- package/server/mind/time/tasks.js +2 -1
- package/server/mind/view.js +2 -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 +14 -3
- package/public/app/assets/PeoplePage-D12cvuy7.js +0 -1
- package/public/app/assets/mind-DYRfFMwh.js +0 -1
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/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 -
|
|
@@ -213,6 +213,10 @@ export class LifeContext {
|
|
|
213
213
|
"SELECT seq,session_id,payload FROM core_events WHERE seq=?",
|
|
214
214
|
);
|
|
215
215
|
return list.map((p) => {
|
|
216
|
+
p =
|
|
217
|
+
this.owner.mind.bonds.person(p.userId, now, {
|
|
218
|
+
room: "group:__open__",
|
|
219
|
+
}) || p;
|
|
216
220
|
const seq = evidence(parse(lastSaid.get(p.userId, now)?.sources, []))
|
|
217
221
|
.filter((s) => s.startsWith("m:"))
|
|
218
222
|
.map((s) => Number(s.slice(2)))
|
|
@@ -223,6 +227,7 @@ export class LifeContext {
|
|
|
223
227
|
userId: p.userId,
|
|
224
228
|
name: p.name,
|
|
225
229
|
feel: p.feel,
|
|
230
|
+
...(p.relationship ? { relationship: p.relationship } : {}),
|
|
226
231
|
lastSeen: agoLabel(now - p.seenAt),
|
|
227
232
|
...(talk && p.lastTalkedAt
|
|
228
233
|
? { lastTalked: agoLabel(now - p.lastTalkedAt) }
|
|
@@ -286,9 +291,14 @@ export class LifeContext {
|
|
|
286
291
|
peopleIn(experiences, now, { open = false } = {}) {
|
|
287
292
|
const ids = [
|
|
288
293
|
...new Set(
|
|
289
|
-
experiences
|
|
290
|
-
e.messages.map((m) => m.userId).filter(Boolean)
|
|
291
|
-
|
|
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
|
+
),
|
|
292
302
|
),
|
|
293
303
|
].slice(0, 8);
|
|
294
304
|
// A diary page can be quoted in other rooms, so it only receives notes
|
|
@@ -307,6 +317,7 @@ export class LifeContext {
|
|
|
307
317
|
name: p.name,
|
|
308
318
|
feel: p.feel,
|
|
309
319
|
...(p.impression ? { impression: p.impression } : {}),
|
|
320
|
+
...(p.relationship ? { relationship: p.relationship } : {}),
|
|
310
321
|
}));
|
|
311
322
|
}
|
|
312
323
|
}
|
|
@@ -4,7 +4,7 @@ import { PLACE_THOUGHT_RULES } from "../faces.js";
|
|
|
4
4
|
import { HOUR, evidence, hasCredential, parse, text } from "../util.js";
|
|
5
5
|
const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
|
|
6
6
|
const IDENTITY_PROMPT =
|
|
7
|
-
'
|
|
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":[]}。';
|
|
8
8
|
const IDENTITY_BOOTSTRAP =
|
|
9
9
|
"如果 livedPersona 仍为空,而 verifiedMeetings 已有多次真实互动,请先从自己反复做出的选择中留下一版简短的整体自述;不必假装突然改变了性格,也不要只重复天性。之后的版本才需要比较哪里真正改变。自述只写自己的倾向,不写对方名字、群名或具体私事。";
|
|
10
10
|
const PERSONA_REPHRASE_PROMPT =
|
|
@@ -143,6 +143,7 @@ export class LifeGrowth {
|
|
|
143
143
|
livedPersona:
|
|
144
144
|
this.owner.mind.traits.persona(nature, now)?.content || "",
|
|
145
145
|
self: this.owner.selfView(now).slice(0, 10),
|
|
146
|
+
people: this.owner.peopleIn(experiences, now),
|
|
146
147
|
faces: this.owner.faceView(experiences, now),
|
|
147
148
|
verifiedMeetings: this.owner.identityMeetings(
|
|
148
149
|
now,
|
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))
|
|
@@ -266,7 +277,17 @@ export class Nature {
|
|
|
266
277
|
);
|
|
267
278
|
const settings = this.repo.store.settings();
|
|
268
279
|
if (settings.name !== value.name || settings.persona !== value.base)
|
|
269
|
-
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
|
+
});
|
|
270
291
|
this.repo.store.revision++;
|
|
271
292
|
return this.current();
|
|
272
293
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { parseSessionKey } from "../channels/session-key.js";
|
|
3
|
+
import { hasCredential, text } from "./util.js";
|
|
4
|
+
import { normalized } from "./util.js";
|
|
5
|
+
import { leaks } from "./guard.js";
|
|
6
|
+
|
|
7
|
+
export const RELATIONSHIP_ROLES = [
|
|
8
|
+
"妹妹",
|
|
9
|
+
"姐姐",
|
|
10
|
+
"弟弟",
|
|
11
|
+
"哥哥",
|
|
12
|
+
"朋友",
|
|
13
|
+
"伙伴",
|
|
14
|
+
];
|
|
15
|
+
const FAMILY = new Set(RELATIONSHIP_ROLES.slice(0, 4));
|
|
16
|
+
const scope = (room) => {
|
|
17
|
+
try {
|
|
18
|
+
return room ? parseSessionKey(room) : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Account identity and an explicit arrangement, never invented encounters or
|
|
25
|
+
// earned trust. Changes append history; nickname equality does not bind peers.
|
|
26
|
+
export class Relationships {
|
|
27
|
+
constructor(mind) {
|
|
28
|
+
this.mind = mind;
|
|
29
|
+
this.db = mind.db;
|
|
30
|
+
}
|
|
31
|
+
version() {
|
|
32
|
+
return this.db
|
|
33
|
+
.prepare("SELECT COALESCE(MAX(rowid),0) n FROM mind_relationships")
|
|
34
|
+
.get().n;
|
|
35
|
+
}
|
|
36
|
+
current(subject, { now = Date.now(), room = "" } = {}) {
|
|
37
|
+
const place = scope(room);
|
|
38
|
+
const row = this.db
|
|
39
|
+
.prepare(
|
|
40
|
+
"SELECT * FROM mind_relationships WHERE subject_id=? AND created<=? ORDER BY created DESC,rowid DESC",
|
|
41
|
+
)
|
|
42
|
+
.all(String(subject), now)
|
|
43
|
+
.find(
|
|
44
|
+
(r) =>
|
|
45
|
+
!place ||
|
|
46
|
+
(r.channel === place.channel &&
|
|
47
|
+
(r.account_id === "_" || r.account_id === place.accountId)),
|
|
48
|
+
);
|
|
49
|
+
return row?.status === "active" ? row : null;
|
|
50
|
+
}
|
|
51
|
+
visible(row, room) {
|
|
52
|
+
if (!row || !room || row.discretion === "open") return !!row;
|
|
53
|
+
const place = scope(room);
|
|
54
|
+
return (
|
|
55
|
+
place?.kind === "private" &&
|
|
56
|
+
place.nativeId === row.subject_id &&
|
|
57
|
+
place.channel === row.channel &&
|
|
58
|
+
(row.account_id === "_" || place.accountId === row.account_id)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
discloses(bubbles, room, now = Date.now()) {
|
|
62
|
+
const hidden = this.list(now).filter((row) => !this.visible(row, room));
|
|
63
|
+
if (
|
|
64
|
+
leaks(
|
|
65
|
+
bubbles,
|
|
66
|
+
hidden.filter((r) => r.note).map((r) => ({ content: r.note })),
|
|
67
|
+
).length
|
|
68
|
+
)
|
|
69
|
+
return true;
|
|
70
|
+
const said = normalized(bubbles.join(" "));
|
|
71
|
+
const english = {
|
|
72
|
+
妹妹: "youngersister",
|
|
73
|
+
姐姐: "oldersister",
|
|
74
|
+
弟弟: "youngerbrother",
|
|
75
|
+
哥哥: "olderbrother",
|
|
76
|
+
朋友: "friend",
|
|
77
|
+
伙伴: "companion",
|
|
78
|
+
};
|
|
79
|
+
return hidden.some((row) => {
|
|
80
|
+
const names = [
|
|
81
|
+
row.name,
|
|
82
|
+
this.mind.bonds.name(row.subject_id),
|
|
83
|
+
row.subject_id,
|
|
84
|
+
]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.map(normalized);
|
|
87
|
+
const role = normalized(row.peer_role),
|
|
88
|
+
en = english[row.peer_role] || role;
|
|
89
|
+
return names.some((name) =>
|
|
90
|
+
[
|
|
91
|
+
"我的" + role + name,
|
|
92
|
+
"我" + role + name,
|
|
93
|
+
name + "是我的" + role,
|
|
94
|
+
name + "是我" + role,
|
|
95
|
+
name + role,
|
|
96
|
+
"my" + en + name,
|
|
97
|
+
name + "ismy" + en,
|
|
98
|
+
].some((line) => said.includes(line)),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
weight(row) {
|
|
103
|
+
return row ? (FAMILY.has(row.peer_role) ? 0.15 : 0.05) : 0;
|
|
104
|
+
}
|
|
105
|
+
view(row, room = "") {
|
|
106
|
+
if (!this.visible(row, room)) return null;
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
subjectId: row.subject_id,
|
|
110
|
+
channel: row.channel,
|
|
111
|
+
accountId: row.account_id,
|
|
112
|
+
kind: row.kind,
|
|
113
|
+
name: row.name,
|
|
114
|
+
peerRole: row.peer_role,
|
|
115
|
+
selfRole: row.self_role,
|
|
116
|
+
discretion: row.discretion,
|
|
117
|
+
note: row.note,
|
|
118
|
+
created: row.created,
|
|
119
|
+
origin: "owner",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
list(now = Date.now()) {
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
return this.db
|
|
125
|
+
.prepare(
|
|
126
|
+
"SELECT * FROM mind_relationships WHERE created<=? ORDER BY created DESC,rowid DESC",
|
|
127
|
+
)
|
|
128
|
+
.all(now)
|
|
129
|
+
.filter((r) => {
|
|
130
|
+
const key = JSON.stringify([r.subject_id, r.channel, r.account_id]);
|
|
131
|
+
if (seen.has(key)) return false;
|
|
132
|
+
seen.add(key);
|
|
133
|
+
return r.status === "active";
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
history(subject, limit = 30) {
|
|
137
|
+
return this.db
|
|
138
|
+
.prepare(
|
|
139
|
+
"SELECT * FROM mind_relationships WHERE subject_id=? ORDER BY created DESC,rowid DESC LIMIT ?",
|
|
140
|
+
)
|
|
141
|
+
.all(String(subject), limit);
|
|
142
|
+
}
|
|
143
|
+
save(input, now = Date.now()) {
|
|
144
|
+
const subject = text(input?.subjectId, 128),
|
|
145
|
+
channel = String(input?.channel || "onebot"),
|
|
146
|
+
account = text(input?.accountId || "_", 128);
|
|
147
|
+
if (
|
|
148
|
+
String(input?.subjectId || "").length > 128 ||
|
|
149
|
+
String(input?.accountId || "").length > 128 ||
|
|
150
|
+
!/^[\w.-]{1,128}$/.test(subject) ||
|
|
151
|
+
["bot", "self", "_"].includes(subject) ||
|
|
152
|
+
!["onebot", "qqbot"].includes(channel) ||
|
|
153
|
+
!/^[\w.-]{1,128}$/.test(account) ||
|
|
154
|
+
(channel === "onebot" && !/^\d+$/.test(subject))
|
|
155
|
+
)
|
|
156
|
+
throw Error("关系对象账号无效");
|
|
157
|
+
if (channel === "qqbot" && account === "_")
|
|
158
|
+
throw Error("官方机器人关系需要指定应用账号");
|
|
159
|
+
if (
|
|
160
|
+
this.db
|
|
161
|
+
.prepare("SELECT 1 FROM core_events WHERE account_id=? LIMIT 1")
|
|
162
|
+
.get(subject)
|
|
163
|
+
)
|
|
164
|
+
throw Error("不能把当前实例绑定为自己的关系对象");
|
|
165
|
+
const kind = String(input.kind || "human"),
|
|
166
|
+
role = text(input.peerRole, 24),
|
|
167
|
+
name = text(input.name, 40),
|
|
168
|
+
note = text(input.note, 300),
|
|
169
|
+
discretion = input.discretion || "open";
|
|
170
|
+
if (
|
|
171
|
+
!["human", "bot"].includes(kind) ||
|
|
172
|
+
!role ||
|
|
173
|
+
String(input.peerRole).length > 24 ||
|
|
174
|
+
String(input.name || "").length > 40 ||
|
|
175
|
+
String(input.note || "").length > 300 ||
|
|
176
|
+
!["open", "private", "secret"].includes(discretion)
|
|
177
|
+
)
|
|
178
|
+
throw Error("关系设置无效");
|
|
179
|
+
const gender = this.mind.nature.current(now).gender;
|
|
180
|
+
const older =
|
|
181
|
+
gender === "female" ? "姐姐" : gender === "male" ? "哥哥" : "年长手足";
|
|
182
|
+
const younger =
|
|
183
|
+
gender === "female" ? "妹妹" : gender === "male" ? "弟弟" : "年幼手足";
|
|
184
|
+
const opposite = {
|
|
185
|
+
妹妹: older,
|
|
186
|
+
弟弟: older,
|
|
187
|
+
姐姐: younger,
|
|
188
|
+
哥哥: younger,
|
|
189
|
+
朋友: "朋友",
|
|
190
|
+
伙伴: "伙伴",
|
|
191
|
+
};
|
|
192
|
+
const selfRole = text(input.selfRole || opposite[role] || "", 24);
|
|
193
|
+
if (
|
|
194
|
+
!selfRole ||
|
|
195
|
+
String(input.selfRole || "").length > 24 ||
|
|
196
|
+
hasCredential([role, selfRole, name, note].join(" "))
|
|
197
|
+
)
|
|
198
|
+
throw Error("关系称呼或备注无效");
|
|
199
|
+
const room = `${channel}:${account}:private:${subject}`,
|
|
200
|
+
previous = this.current(subject, { now, room });
|
|
201
|
+
if (
|
|
202
|
+
input.expectedId !== undefined &&
|
|
203
|
+
input.expectedId !== (previous?.id || null)
|
|
204
|
+
)
|
|
205
|
+
throw Error("关系已经变化,请刷新后再修改");
|
|
206
|
+
if (
|
|
207
|
+
previous &&
|
|
208
|
+
previous.kind === kind &&
|
|
209
|
+
previous.peer_role === role &&
|
|
210
|
+
previous.self_role === selfRole &&
|
|
211
|
+
previous.name === name &&
|
|
212
|
+
previous.note === note &&
|
|
213
|
+
previous.discretion === discretion
|
|
214
|
+
)
|
|
215
|
+
return this.view(previous);
|
|
216
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
217
|
+
try {
|
|
218
|
+
this.db
|
|
219
|
+
.prepare(
|
|
220
|
+
"INSERT INTO mind_relationships(id,created,subject_id,channel,account_id,kind,name,peer_role,self_role,discretion,note,status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'active')",
|
|
221
|
+
)
|
|
222
|
+
.run(
|
|
223
|
+
randomUUID(),
|
|
224
|
+
now,
|
|
225
|
+
subject,
|
|
226
|
+
channel,
|
|
227
|
+
account,
|
|
228
|
+
kind,
|
|
229
|
+
name,
|
|
230
|
+
role,
|
|
231
|
+
selfRole,
|
|
232
|
+
discretion,
|
|
233
|
+
note,
|
|
234
|
+
);
|
|
235
|
+
this.db
|
|
236
|
+
.prepare(
|
|
237
|
+
"INSERT INTO mind_people(user_id,name,first_seen,last_seen,sessions) VALUES (?,NULL,NULL,NULL,'[]') ON CONFLICT(user_id) DO NOTHING",
|
|
238
|
+
)
|
|
239
|
+
.run(subject);
|
|
240
|
+
this.db.exec("COMMIT");
|
|
241
|
+
this.mind.repo.store.revision++;
|
|
242
|
+
return this.view(this.current(subject, { now, room }));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.db.exec("ROLLBACK");
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
end(subject, input = {}, now = Date.now()) {
|
|
249
|
+
if (
|
|
250
|
+
String(input.note || "").length > 300 ||
|
|
251
|
+
hasCredential(String(input.note || ""))
|
|
252
|
+
)
|
|
253
|
+
throw Error("关系称呼或备注无效");
|
|
254
|
+
const room = `${input.channel || "onebot"}:${input.accountId || "_"}:private:${subject}`,
|
|
255
|
+
row = this.current(subject, { now, room });
|
|
256
|
+
if (!row) throw Error("关系不存在或已解除");
|
|
257
|
+
if (input.expectedId !== undefined && input.expectedId !== row.id)
|
|
258
|
+
throw Error("关系已经变化,请刷新后再修改");
|
|
259
|
+
this.db
|
|
260
|
+
.prepare(
|
|
261
|
+
"INSERT INTO mind_relationships(id,created,subject_id,channel,account_id,kind,name,peer_role,self_role,discretion,note,status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'ended')",
|
|
262
|
+
)
|
|
263
|
+
.run(
|
|
264
|
+
randomUUID(),
|
|
265
|
+
now,
|
|
266
|
+
row.subject_id,
|
|
267
|
+
row.channel,
|
|
268
|
+
row.account_id,
|
|
269
|
+
row.kind,
|
|
270
|
+
row.name,
|
|
271
|
+
row.peer_role,
|
|
272
|
+
row.self_role,
|
|
273
|
+
row.discretion,
|
|
274
|
+
text(input.note || "在管理台解除关系", 300),
|
|
275
|
+
);
|
|
276
|
+
this.mind.repo.store.revision++;
|
|
277
|
+
return { ended: true };
|
|
278
|
+
}
|
|
279
|
+
}
|
package/server/mind/schema.js
CHANGED
|
@@ -26,6 +26,8 @@ export function migrateMind(db, store) {
|
|
|
26
26
|
CREATE INDEX IF NOT EXISTS mind_self_thread ON mind_self(thread,created);
|
|
27
27
|
CREATE TABLE IF NOT EXISTS mind_faces (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, created INTEGER NOT NULL, role TEXT NOT NULL DEFAULT '', tone TEXT NOT NULL DEFAULT '', aspiration TEXT NOT NULL DEFAULT '', content TEXT NOT NULL DEFAULT '', sources TEXT NOT NULL DEFAULT '[]', origin TEXT NOT NULL);
|
|
28
28
|
CREATE INDEX IF NOT EXISTS mind_faces_session ON mind_faces(session_id,created);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS mind_relationships(id TEXT PRIMARY KEY,created INTEGER NOT NULL,subject_id TEXT NOT NULL,channel TEXT NOT NULL,account_id TEXT NOT NULL,kind TEXT NOT NULL,name TEXT NOT NULL DEFAULT '',peer_role TEXT NOT NULL,self_role TEXT NOT NULL,discretion TEXT NOT NULL DEFAULT 'open',note TEXT NOT NULL DEFAULT '',status TEXT NOT NULL);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS mind_relationships_subject ON mind_relationships(subject_id,channel,account_id,created);
|
|
29
31
|
CREATE TABLE IF NOT EXISTS mind_thoughts (id TEXT PRIMARY KEY, created INTEGER NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, sessions TEXT NOT NULL DEFAULT '[]', sources TEXT NOT NULL DEFAULT '[]', parent_id TEXT, importance REAL NOT NULL DEFAULT 0.5, revisit_at INTEGER, status TEXT NOT NULL DEFAULT 'open', hidden INTEGER NOT NULL DEFAULT 0, outreach TEXT NOT NULL DEFAULT '', outreach_session TEXT, outreach_status TEXT NOT NULL DEFAULT 'none', run_id TEXT);
|
|
30
32
|
CREATE INDEX IF NOT EXISTS mind_thoughts_time ON mind_thoughts(created);
|
|
31
33
|
CREATE TABLE IF NOT EXISTS mind_diary (id TEXT PRIMARY KEY, day TEXT NOT NULL, created INTEGER NOT NULL, content TEXT NOT NULL, mood TEXT, compare TEXT, sources TEXT NOT NULL DEFAULT '[]', run_id TEXT);
|
|
@@ -367,7 +367,8 @@ export class Tasks {
|
|
|
367
367
|
? 2
|
|
368
368
|
: task.priority) *
|
|
369
369
|
100 +
|
|
370
|
-
Math.min(8, Number(person?.closeness || 0) * 8)
|
|
370
|
+
Math.min(8, Number(person?.closeness || 0) * 8) +
|
|
371
|
+
Math.min(2, Number(person?.relationalWeight || 0) * 8)
|
|
371
372
|
);
|
|
372
373
|
}
|
|
373
374
|
arrange(
|
package/server/mind/view.js
CHANGED
|
@@ -179,6 +179,7 @@ export function innerView(
|
|
|
179
179
|
(!bond.interactions &&
|
|
180
180
|
!bond.impression &&
|
|
181
181
|
!bond.lastChange &&
|
|
182
|
+
!bond.relationship &&
|
|
182
183
|
bond.tension < 0.15)
|
|
183
184
|
)
|
|
184
185
|
continue;
|
|
@@ -186,6 +187,7 @@ export function innerView(
|
|
|
186
187
|
id,
|
|
187
188
|
name: bond.name,
|
|
188
189
|
feel: bond.feel,
|
|
190
|
+
...(bond.relationship ? { relationship: bond.relationship } : {}),
|
|
189
191
|
...(bond.impression && mind.meetings.sayable(bond.impression, session)
|
|
190
192
|
? { impression: bond.impression }
|
|
191
193
|
: {}),
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmdirSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join, relative, isAbsolute } from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { compareVersions } from "../studio/updates.js";
|
|
16
|
+
export const UPDATE_ID =
|
|
17
|
+
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
18
|
+
export const updateRoot = (home) => join(home, "runtime");
|
|
19
|
+
export function updatePath(home, id) {
|
|
20
|
+
if (!UPDATE_ID.test(String(id))) throw Error("更新任务无效");
|
|
21
|
+
const root = updateRoot(home),
|
|
22
|
+
path = join(root, "releases", id);
|
|
23
|
+
// Managed paths cannot be symlinked into an unrelated installation.
|
|
24
|
+
for (const p of [root, join(root, "releases"), path])
|
|
25
|
+
if (existsSync(p) && lstatSync(p).isSymbolicLink())
|
|
26
|
+
throw Error("更新目录无效");
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
export function readActive(home) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(
|
|
32
|
+
readFileSync(join(updateRoot(home), "active.json"), "utf8"),
|
|
33
|
+
);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function atomicJson(file, value) {
|
|
39
|
+
const tmp = `${file}.${randomUUID()}.partial`;
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(tmp, JSON.stringify(value), {
|
|
42
|
+
mode: 0o600,
|
|
43
|
+
flag: "wx",
|
|
44
|
+
flush: true,
|
|
45
|
+
});
|
|
46
|
+
renameSync(tmp, file);
|
|
47
|
+
} finally {
|
|
48
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function writeActive(home, value) {
|
|
52
|
+
const root = updateRoot(home);
|
|
53
|
+
if (value) updatePath(home, value.id);
|
|
54
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
55
|
+
if (value) atomicJson(join(root, "active.json"), value);
|
|
56
|
+
else if (existsSync(join(root, "active.json")))
|
|
57
|
+
unlinkSync(join(root, "active.json"));
|
|
58
|
+
}
|
|
59
|
+
export function installedRelease(home, id, version) {
|
|
60
|
+
const slot = updatePath(home, id),
|
|
61
|
+
root = join(slot, "node_modules", "luckytri");
|
|
62
|
+
const part = relative(realpathSync(slot), realpathSync(root));
|
|
63
|
+
if (part.startsWith("..") || isAbsolute(part)) throw Error("更新目录无效");
|
|
64
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
65
|
+
if (
|
|
66
|
+
pkg.name !== "luckytri" ||
|
|
67
|
+
pkg.version !== version ||
|
|
68
|
+
!existsSync(join(root, "server", "index.js")) ||
|
|
69
|
+
!existsSync(join(root, "public", "app", "index.html"))
|
|
70
|
+
)
|
|
71
|
+
throw Error("更新程序校验失败");
|
|
72
|
+
return root;
|
|
73
|
+
}
|
|
74
|
+
export function resolveInstallation(home, basePackage, baseVersion) {
|
|
75
|
+
const active = readActive(home);
|
|
76
|
+
if (active) {
|
|
77
|
+
try {
|
|
78
|
+
// An explicit newer/equal global npm or source install takes precedence.
|
|
79
|
+
if (compareVersions(active.version, baseVersion) > 0)
|
|
80
|
+
return {
|
|
81
|
+
package: installedRelease(home, active.id, active.version),
|
|
82
|
+
version: active.version,
|
|
83
|
+
};
|
|
84
|
+
} catch {
|
|
85
|
+
/* Invalid receipts never route the launcher to arbitrary code. */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { package: basePackage, version: baseVersion };
|
|
89
|
+
}
|
|
90
|
+
export function pruneInstallations(home, keep) {
|
|
91
|
+
const root = join(updateRoot(home), "releases");
|
|
92
|
+
if (!existsSync(root)) return;
|
|
93
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
94
|
+
if (
|
|
95
|
+
!entry.isDirectory() ||
|
|
96
|
+
!UPDATE_ID.test(entry.name) ||
|
|
97
|
+
keep.includes(entry.name)
|
|
98
|
+
)
|
|
99
|
+
continue;
|
|
100
|
+
const path = updatePath(home, entry.name),
|
|
101
|
+
part = relative(realpathSync(root), realpathSync(path));
|
|
102
|
+
if (!part || part.startsWith("..") || isAbsolute(part))
|
|
103
|
+
throw Error("更新目录无效");
|
|
104
|
+
// File removal uses unlink, including Unicode/short Windows paths where
|
|
105
|
+
// recursive rm can return without actually removing a lock or directory.
|
|
106
|
+
const remove = (directory) => {
|
|
107
|
+
for (const child of readdirSync(directory, { withFileTypes: true })) {
|
|
108
|
+
const target = join(directory, child.name);
|
|
109
|
+
if (child.isDirectory() && !child.isSymbolicLink()) remove(target);
|
|
110
|
+
else unlinkSync(target);
|
|
111
|
+
}
|
|
112
|
+
rmdirSync(directory);
|
|
113
|
+
};
|
|
114
|
+
remove(path);
|
|
115
|
+
}
|
|
116
|
+
}
|