luckytri 1.0.0
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/.github/SECURITY.md +71 -0
- package/LICENSE +21 -0
- package/README.en.md +250 -0
- package/README.md +250 -0
- package/bin/luckytri.js +107 -0
- package/docs/brand/banner.png +0 -0
- package/docs/brand/palette.png +0 -0
- package/docs/brand/portrait.png +0 -0
- package/docs/en/CHANGELOG.md +113 -0
- package/docs/en/architecture.md +152 -0
- package/docs/en/chat-style.md +62 -0
- package/docs/en/connect.md +63 -0
- package/docs/en/guide.md +216 -0
- package/docs/en/her-life.md +189 -0
- package/docs/en/npm-release.md +25 -0
- package/docs/en/plugins.md +82 -0
- package/docs/en/recovery.md +28 -0
- package/docs/en/time.md +71 -0
- package/docs/en/webui.md +73 -0
- package/docs/zh/CHANGELOG.md +113 -0
- package/docs/zh/architecture.md +152 -0
- package/docs/zh/chat-style.md +62 -0
- package/docs/zh/connect.md +63 -0
- package/docs/zh/guide.md +216 -0
- package/docs/zh/her-life.md +189 -0
- package/docs/zh/npm-release.md +33 -0
- package/docs/zh/plugins.md +82 -0
- package/docs/zh/recovery.md +28 -0
- package/docs/zh/time.md +71 -0
- package/docs/zh/webui.md +73 -0
- package/package.json +108 -0
- package/plugins/sdk/index.d.ts +155 -0
- package/plugins/sdk/testing.js +58 -0
- package/plugins/weather/index.js +82 -0
- package/plugins/weather/luckytri-plugin.json +29 -0
- package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/Empty-CpP5kQHH.js +2 -0
- package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
- package/public/app/assets/Field-D4gY-INU.js +1 -0
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
- package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
- package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
- package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
- package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
- package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
- package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
- package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
- package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/TimePage-Cw44inM4.js +3 -0
- package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
- package/public/app/assets/index-BheRW-1t.js +4 -0
- package/public/app/assets/index-YDz1ree1.css +1 -0
- package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
- package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
- package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
- package/public/app/assets/media-BzPDBm7e.js +1 -0
- package/public/app/index.html +27 -0
- package/public/guide.en.html +1053 -0
- package/public/guide.html +893 -0
- package/public/plugin-kit/v1/bridge.js +30 -0
- package/public/plugin-kit/v1/kit.css +14 -0
- package/public/tutorial.en.md +214 -0
- package/public/tutorial.md +214 -0
- package/scripts/backup.js +311 -0
- package/scripts/browser.js +41 -0
- package/scripts/launch.js +94 -0
- package/scripts/plugin.js +150 -0
- package/scripts/proxy-config.js +167 -0
- package/scripts/recover.js +26 -0
- package/scripts/runtime.js +25 -0
- package/scripts/service.js +47 -0
- package/scripts/setup.js +50 -0
- package/scripts/start-server.js +41 -0
- package/scripts/stop.js +36 -0
- package/server/app.js +96 -0
- package/server/channels/adapters.js +27 -0
- package/server/channels/contract.js +57 -0
- package/server/channels/hub.js +165 -0
- package/server/channels/index.js +25 -0
- package/server/channels/onebot/adapter.js +288 -0
- package/server/channels/onebot/index.js +336 -0
- package/server/channels/onebot/token.js +3 -0
- package/server/channels/qqbot/adapter.js +253 -0
- package/server/channels/qqbot/api.js +199 -0
- package/server/channels/qqbot/credentials.js +11 -0
- package/server/channels/qqbot/gateway.js +274 -0
- package/server/channels/qqbot/index.js +389 -0
- package/server/channels/segments.js +40 -0
- package/server/channels/session-key.js +153 -0
- package/server/channels/state.js +42 -0
- package/server/core/api.js +564 -0
- package/server/core/capabilities.js +208 -0
- package/server/core/context-builder.js +288 -0
- package/server/core/context-compactor.js +454 -0
- package/server/core/conversation-cues.js +325 -0
- package/server/core/conversation-manager.js +87 -0
- package/server/core/events.js +60 -0
- package/server/core/feedback.js +8 -0
- package/server/core/group-style.js +72 -0
- package/server/core/image-preprocessor.js +208 -0
- package/server/core/initiative-context.js +66 -0
- package/server/core/input-budget.js +32 -0
- package/server/core/local-demo.js +98 -0
- package/server/core/message-manager.js +118 -0
- package/server/core/message-scheduler.js +78 -0
- package/server/core/model-json.js +190 -0
- package/server/core/model-manager.js +1213 -0
- package/server/core/model-presets.js +900 -0
- package/server/core/name-call.js +29 -0
- package/server/core/net-guard.js +49 -0
- package/server/core/network.js +157 -0
- package/server/core/orchestrator.js +569 -0
- package/server/core/perceivers.js +91 -0
- package/server/core/person-name.js +22 -0
- package/server/core/persona-manager.js +243 -0
- package/server/core/reply-delivery.js +393 -0
- package/server/core/reply-target-resolver.js +50 -0
- package/server/core/repository.js +293 -0
- package/server/core/response-generator.js +50 -0
- package/server/core/response-validator.js +508 -0
- package/server/core/retired-prompts.js +51 -0
- package/server/core/sessions.js +109 -0
- package/server/core/speaker-names.js +92 -0
- package/server/core/topic-tracker.js +65 -0
- package/server/core/turn-processor.js +630 -0
- package/server/core/turn.js +236 -0
- package/server/core/vision-manager.js +633 -0
- package/server/http.js +18 -0
- package/server/i18n/decisions.js +206 -0
- package/server/i18n/errors.js +497 -0
- package/server/i18n/index.js +144 -0
- package/server/i18n/labels.js +653 -0
- package/server/i18n/models.js +111 -0
- package/server/index.js +102 -0
- package/server/knowledge/api.js +342 -0
- package/server/knowledge/embedding-profile.js +114 -0
- package/server/knowledge/index.js +13 -0
- package/server/knowledge/manager.js +386 -0
- package/server/knowledge/retrieval.js +93 -0
- package/server/knowledge/schema.js +154 -0
- package/server/knowledge/vector-index.js +90 -0
- package/server/mind/affect.js +265 -0
- package/server/mind/anticipations.js +507 -0
- package/server/mind/api.js +703 -0
- package/server/mind/attention.js +200 -0
- package/server/mind/bonds.js +558 -0
- package/server/mind/budget.js +152 -0
- package/server/mind/clock.js +83 -0
- package/server/mind/context-selection.js +61 -0
- package/server/mind/continuity.js +562 -0
- package/server/mind/conversation-origin.js +175 -0
- package/server/mind/day-planner.js +324 -0
- package/server/mind/days.js +210 -0
- package/server/mind/evidence-read.js +45 -0
- package/server/mind/evidence.js +148 -0
- package/server/mind/faces.js +138 -0
- package/server/mind/guard.js +60 -0
- package/server/mind/index.js +445 -0
- package/server/mind/initiative.js +333 -0
- package/server/mind/life/activities.js +21 -0
- package/server/mind/life/context.js +309 -0
- package/server/mind/life/diary.js +786 -0
- package/server/mind/life/growth.js +252 -0
- package/server/mind/life/index.js +1049 -0
- package/server/mind/life/presence.js +305 -0
- package/server/mind/meetings.js +1069 -0
- package/server/mind/memory-policy.js +48 -0
- package/server/mind/memory.js +888 -0
- package/server/mind/nature.js +262 -0
- package/server/mind/novelty.js +19 -0
- package/server/mind/observations.js +84 -0
- package/server/mind/own-day.js +324 -0
- package/server/mind/own-voice.js +318 -0
- package/server/mind/periods.js +118 -0
- package/server/mind/reading.js +120 -0
- package/server/mind/salience.js +201 -0
- package/server/mind/schema.js +562 -0
- package/server/mind/self.js +484 -0
- package/server/mind/senses.js +53 -0
- package/server/mind/thoughts.js +274 -0
- package/server/mind/time/activity-clock.js +196 -0
- package/server/mind/time/agenda.js +242 -0
- package/server/mind/time/api.js +200 -0
- package/server/mind/time/attention.js +34 -0
- package/server/mind/time/availability.js +69 -0
- package/server/mind/time/commitment-review.js +198 -0
- package/server/mind/time/delivery-links.js +130 -0
- package/server/mind/time/executor.js +512 -0
- package/server/mind/time/experiences.js +128 -0
- package/server/mind/time/games.js +562 -0
- package/server/mind/time/index.js +739 -0
- package/server/mind/time/intent.js +166 -0
- package/server/mind/time/kinds.js +171 -0
- package/server/mind/time/model-material.js +77 -0
- package/server/mind/time/plugin-activity.js +207 -0
- package/server/mind/time/presentation.js +51 -0
- package/server/mind/time/schedule.js +83 -0
- package/server/mind/time/schema.js +84 -0
- package/server/mind/time/search.js +383 -0
- package/server/mind/time/sharing.js +472 -0
- package/server/mind/time/task-links.js +311 -0
- package/server/mind/time/tasks.js +587 -0
- package/server/mind/time/works.js +432 -0
- package/server/mind/traits.js +191 -0
- package/server/mind/util.js +110 -0
- package/server/mind/view.js +273 -0
- package/server/paths.js +102 -0
- package/server/plugins/api.js +204 -0
- package/server/plugins/host.js +967 -0
- package/server/plugins/install.js +295 -0
- package/server/plugins/manifest.js +201 -0
- package/server/plugins/permissions.js +42 -0
- package/server/plugins/runner/guard.mjs +36 -0
- package/server/plugins/runner/worker.mjs +250 -0
- package/server/plugins/schema.js +91 -0
- package/server/plugins/zip.js +112 -0
- package/server/startup-lock.js +118 -0
- package/server/storage/archive.js +241 -0
- package/server/storage/backup-cleanup-worker.js +8 -0
- package/server/storage/backup-cleanup.js +414 -0
- package/server/storage/backup-lock.js +76 -0
- package/server/storage/backup-scheduler.js +131 -0
- package/server/storage/store.js +199 -0
- package/server/studio/display-names.js +138 -0
- package/server/studio/management.js +102 -0
- package/server/studio/readiness.js +190 -0
- package/server/studio/settings.js +236 -0
- package/server/version.js +10 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { localClock } from "../core/conversation-cues.js";
|
|
2
|
+
import { lifeDayKey, lifeDayStart, lifeSpan } from "./nature.js";
|
|
3
|
+
import { DAY } from "./util.js";
|
|
4
|
+
|
|
5
|
+
const MINUTE = 60000;
|
|
6
|
+
const LIVE = "COALESCE(json_extract(payload,'$.simulated'),0)=0";
|
|
7
|
+
const BACKFILL_DAYS = 14;
|
|
8
|
+
const MILESTONES = [100, 200, 300, 500, 1000];
|
|
9
|
+
|
|
10
|
+
// The days she has actually lived. Things fade by how much life has passed
|
|
11
|
+
// since, not by the calendar, so a silent month does not wear her away.
|
|
12
|
+
export class Days {
|
|
13
|
+
constructor(mind) {
|
|
14
|
+
this.mind = mind;
|
|
15
|
+
this.db = mind.db;
|
|
16
|
+
this.rolled = null;
|
|
17
|
+
}
|
|
18
|
+
key(time, nature = this.mind.nature.current(time)) {
|
|
19
|
+
return lifeDayKey(nature, time, this.mind.timeZone());
|
|
20
|
+
}
|
|
21
|
+
start(time, nature = this.mind.nature.current(time)) {
|
|
22
|
+
return lifeDayStart(nature, time, this.mind.timeZone());
|
|
23
|
+
}
|
|
24
|
+
// Each finished day is written once; cheap enough to call every minute.
|
|
25
|
+
rollup(now = Date.now()) {
|
|
26
|
+
const nature = this.mind.nature.current(now);
|
|
27
|
+
const today = this.key(now, nature);
|
|
28
|
+
if (this.rolled === today) return 0;
|
|
29
|
+
const has = this.db.prepare("SELECT 1 FROM mind_days WHERE day=?");
|
|
30
|
+
let end = this.start(now, nature);
|
|
31
|
+
let written = 0;
|
|
32
|
+
for (let i = 0; i < BACKFILL_DAYS; i++) {
|
|
33
|
+
const start = this.start(end - MINUTE, nature);
|
|
34
|
+
const day = this.key(start, nature);
|
|
35
|
+
if (!has.get(day) && this.write(day, start, end, now)) written++;
|
|
36
|
+
end = start;
|
|
37
|
+
}
|
|
38
|
+
this.rolled = today;
|
|
39
|
+
return written;
|
|
40
|
+
}
|
|
41
|
+
write(day, start, end, now) {
|
|
42
|
+
const events = this.db
|
|
43
|
+
.prepare(
|
|
44
|
+
`SELECT COUNT(*) n, COUNT(DISTINCT CASE WHEN role='user' THEN json_extract(payload,'$.userId') END) people FROM core_events WHERE time>=? AND time<? AND ${LIVE} AND seq NOT IN (SELECT seq FROM mind_unlived)`,
|
|
45
|
+
)
|
|
46
|
+
.get(start, end);
|
|
47
|
+
const felt = this.db
|
|
48
|
+
.prepare(
|
|
49
|
+
"SELECT SUM(valence*intensity) v, SUM(intensity) i, AVG(arousal) a, COUNT(*) n FROM mind_affect WHERE created>=? AND created<?",
|
|
50
|
+
)
|
|
51
|
+
.get(start, end);
|
|
52
|
+
const choices = this.db
|
|
53
|
+
.prepare(
|
|
54
|
+
"SELECT COUNT(*) n, COALESCE(SUM(choice!='silent'),0) spoke FROM mind_choices WHERE created>=? AND created<?",
|
|
55
|
+
)
|
|
56
|
+
.get(start, end);
|
|
57
|
+
const present = this.participated(start, end);
|
|
58
|
+
if (!events.n && !felt.n && !present) return false;
|
|
59
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
60
|
+
this.db
|
|
61
|
+
.prepare(
|
|
62
|
+
"INSERT OR IGNORE INTO mind_days(day,created,lived,events,valence,arousal,looked,spoke,people) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
63
|
+
)
|
|
64
|
+
.run(
|
|
65
|
+
day,
|
|
66
|
+
now,
|
|
67
|
+
Number(present),
|
|
68
|
+
events.n,
|
|
69
|
+
felt.i > 0 ? round(felt.v / felt.i) : null,
|
|
70
|
+
felt.n ? round(felt.a) : null,
|
|
71
|
+
choices.n,
|
|
72
|
+
choices.spoke,
|
|
73
|
+
events.people,
|
|
74
|
+
);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
// Days written when any messages counted as a life are put back, once she
|
|
78
|
+
// did not look, speak, or sit with them.
|
|
79
|
+
reconcile(now = Date.now()) {
|
|
80
|
+
const nature = this.mind.nature.current(now);
|
|
81
|
+
const zone = this.mind.timeZone();
|
|
82
|
+
const clear = this.db.prepare("UPDATE mind_days SET lived=0 WHERE day=?");
|
|
83
|
+
let changed = 0;
|
|
84
|
+
for (const row of this.db
|
|
85
|
+
.prepare(
|
|
86
|
+
"SELECT day FROM mind_days WHERE lived=1 AND looked=0 AND spoke=0",
|
|
87
|
+
)
|
|
88
|
+
.all()) {
|
|
89
|
+
const span = lifeSpan(nature, row.day, zone);
|
|
90
|
+
if (!span || this.participated(span.start, span.end)) continue;
|
|
91
|
+
clear.run(row.day);
|
|
92
|
+
changed++;
|
|
93
|
+
}
|
|
94
|
+
return changed;
|
|
95
|
+
}
|
|
96
|
+
// She looked, spoke, or sat with the day. Other people's messages do not count.
|
|
97
|
+
participated(start, end, { inclusive = false } = {}) {
|
|
98
|
+
const endOp = inclusive ? "<=" : "<";
|
|
99
|
+
return !!(
|
|
100
|
+
this.db
|
|
101
|
+
.prepare(
|
|
102
|
+
`SELECT 1 FROM mind_choices WHERE created>=? AND created${endOp}? AND (choice!='silent' OR COALESCE(occasion,'') NOT IN ('presence','outreach'))`,
|
|
103
|
+
)
|
|
104
|
+
.get(start, end) ||
|
|
105
|
+
this.db
|
|
106
|
+
.prepare(
|
|
107
|
+
`SELECT 1 FROM core_events WHERE role='assistant' AND time>=? AND time${endOp}? AND ${LIVE}`,
|
|
108
|
+
)
|
|
109
|
+
.get(start, end) ||
|
|
110
|
+
this.db
|
|
111
|
+
.prepare(
|
|
112
|
+
`SELECT 1 FROM mind_runs WHERE started>=? AND started${endOp}? AND (status IN ('written','complete','draft','experienced') OR (status='empty' AND (summary IS NULL OR COALESCE(json_extract(summary,'$.freshMessages'),0)>0)))`,
|
|
113
|
+
)
|
|
114
|
+
.get(start, end)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
// Counts lived days after the day of a given moment, as known at `now`.
|
|
118
|
+
lived(now = Date.now()) {
|
|
119
|
+
const nature = this.mind.nature.current(now);
|
|
120
|
+
const days = this.db
|
|
121
|
+
.prepare(
|
|
122
|
+
"SELECT day FROM mind_days WHERE lived=1 AND created<=? ORDER BY day",
|
|
123
|
+
)
|
|
124
|
+
.all(now)
|
|
125
|
+
.map((row) => row.day);
|
|
126
|
+
const since = (time) => {
|
|
127
|
+
if (!days.length) return 0;
|
|
128
|
+
if (!time) return days.length;
|
|
129
|
+
const key = this.key(time, nature);
|
|
130
|
+
let lo = 0;
|
|
131
|
+
let hi = days.length;
|
|
132
|
+
while (lo < hi) {
|
|
133
|
+
const mid = (lo + hi) >> 1;
|
|
134
|
+
if (days[mid] <= key) lo = mid + 1;
|
|
135
|
+
else hi = mid;
|
|
136
|
+
}
|
|
137
|
+
return days.length - lo;
|
|
138
|
+
};
|
|
139
|
+
return { count: days.length, since };
|
|
140
|
+
}
|
|
141
|
+
recent({ now = Date.now(), limit = 14 } = {}) {
|
|
142
|
+
return this.db
|
|
143
|
+
.prepare(
|
|
144
|
+
"SELECT * FROM mind_days WHERE created<=? ORDER BY day DESC LIMIT ?",
|
|
145
|
+
)
|
|
146
|
+
.all(now, limit);
|
|
147
|
+
}
|
|
148
|
+
// Her first day: the first thing that happened to her, or her first nature.
|
|
149
|
+
born() {
|
|
150
|
+
const event = this.db
|
|
151
|
+
.prepare(
|
|
152
|
+
`SELECT MIN(time) t FROM core_events WHERE ${LIVE} AND seq NOT IN (SELECT seq FROM mind_unlived)`,
|
|
153
|
+
)
|
|
154
|
+
.get().t;
|
|
155
|
+
const nature = this.db
|
|
156
|
+
.prepare("SELECT MIN(created) t FROM mind_nature")
|
|
157
|
+
.get().t;
|
|
158
|
+
const times = [event, nature].filter((t) => Number.isFinite(t));
|
|
159
|
+
return times.length ? Math.min(...times) : null;
|
|
160
|
+
}
|
|
161
|
+
dayOfLife(now = Date.now()) {
|
|
162
|
+
const born = this.born();
|
|
163
|
+
if (!born || born > now) return 1;
|
|
164
|
+
const zone = this.mind.timeZone();
|
|
165
|
+
const from = Date.parse(
|
|
166
|
+
`${localClock(born, zone).local.slice(0, 10)}T00:00:00Z`,
|
|
167
|
+
);
|
|
168
|
+
const to = Date.parse(
|
|
169
|
+
`${localClock(now, zone).local.slice(0, 10)}T00:00:00Z`,
|
|
170
|
+
);
|
|
171
|
+
return Math.round((to - from) / DAY) + 1;
|
|
172
|
+
}
|
|
173
|
+
// Round numbers and yearly returns: of her own days, and of meeting people
|
|
174
|
+
// she has come to know.
|
|
175
|
+
anniversaries(now = Date.now(), { limit = 3 } = {}) {
|
|
176
|
+
const zone = this.mind.timeZone();
|
|
177
|
+
const today = localClock(now, zone).local.slice(0, 10);
|
|
178
|
+
const out = [];
|
|
179
|
+
const mark = (first, label) => {
|
|
180
|
+
const day = localClock(first, zone).local.slice(0, 10);
|
|
181
|
+
const days =
|
|
182
|
+
Math.round(
|
|
183
|
+
(Date.parse(`${today}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) /
|
|
184
|
+
DAY,
|
|
185
|
+
) + 1;
|
|
186
|
+
const years = Number(today.slice(0, 4)) - Number(day.slice(0, 4));
|
|
187
|
+
if (years >= 1 && day.slice(5) === today.slice(5))
|
|
188
|
+
return `${label}满 ${years} 年`;
|
|
189
|
+
if (MILESTONES.includes(days)) return `${label}的第 ${days} 天`;
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
const born = this.born();
|
|
193
|
+
if (born && born <= now) {
|
|
194
|
+
const own = mark(born, "我来到这里");
|
|
195
|
+
if (own) out.push(own);
|
|
196
|
+
}
|
|
197
|
+
for (const row of this.db
|
|
198
|
+
.prepare(
|
|
199
|
+
"SELECT user_id,first_seen FROM mind_people WHERE first_seen<=? ORDER BY first_seen LIMIT 500",
|
|
200
|
+
)
|
|
201
|
+
.all(now)) {
|
|
202
|
+
if (out.length >= limit) break;
|
|
203
|
+
const person = this.mind.bonds.person(row.user_id, now);
|
|
204
|
+
if (!person || person.familiarity < 0.3) continue;
|
|
205
|
+
const label = mark(row.first_seen, `认识${person.name}`);
|
|
206
|
+
if (label) out.push(label);
|
|
207
|
+
}
|
|
208
|
+
return out.slice(0, limit);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const states = new WeakMap();
|
|
2
|
+
const LIMIT = 4096;
|
|
3
|
+
|
|
4
|
+
// Provenance walks often visit the same ancestors from many threads. Reuse
|
|
5
|
+
// their reads, while physical DB changes and the exact cutoff keep history,
|
|
6
|
+
// revocations and other-process updates isolated.
|
|
7
|
+
export function evidenceReader(db) {
|
|
8
|
+
let state = states.get(db);
|
|
9
|
+
if (!state) {
|
|
10
|
+
state = {
|
|
11
|
+
mark: db.prepare(
|
|
12
|
+
"SELECT total_changes() changes,(SELECT data_version FROM pragma_data_version) external",
|
|
13
|
+
),
|
|
14
|
+
stamp: "",
|
|
15
|
+
statements: new Map(),
|
|
16
|
+
rows: new Map(),
|
|
17
|
+
};
|
|
18
|
+
states.set(db, state);
|
|
19
|
+
}
|
|
20
|
+
const mark = state.mark.get(),
|
|
21
|
+
stamp = `${mark.changes}:${mark.external}`,
|
|
22
|
+
transaction = db.isTransaction === true;
|
|
23
|
+
// total_changes includes rolled-back writes. Never retain reads from an
|
|
24
|
+
// explicit transaction, otherwise its abandoned data could survive rollback.
|
|
25
|
+
if (transaction || stamp !== state.stamp) {
|
|
26
|
+
state.rows.clear();
|
|
27
|
+
state.stamp = stamp;
|
|
28
|
+
}
|
|
29
|
+
return (sql, args = [], many = false) => {
|
|
30
|
+
let query = state.statements.get(sql);
|
|
31
|
+
if (!query) {
|
|
32
|
+
query = { id: state.statements.size, statement: db.prepare(sql) };
|
|
33
|
+
state.statements.set(sql, query);
|
|
34
|
+
}
|
|
35
|
+
const key = JSON.stringify([query.id, many, args]);
|
|
36
|
+
if (!transaction && state.rows.has(key)) return state.rows.get(key);
|
|
37
|
+
const row = query.statement[many ? "all" : "get"](...args);
|
|
38
|
+
if (!transaction) {
|
|
39
|
+
state.rows.set(key, row);
|
|
40
|
+
if (state.rows.size > LIMIT)
|
|
41
|
+
state.rows.delete(state.rows.keys().next().value);
|
|
42
|
+
}
|
|
43
|
+
return row;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { evidence, parse } from "./util.js";
|
|
2
|
+
import { evidenceReader } from "./evidence-read.js";
|
|
3
|
+
|
|
4
|
+
// A retelling is an interpretation of an experience, not another experience.
|
|
5
|
+
// Resolve all derivative citations to the same roots, without writing history.
|
|
6
|
+
export function evidenceRoots(
|
|
7
|
+
db,
|
|
8
|
+
sources,
|
|
9
|
+
before = Number.MAX_SAFE_INTEGER,
|
|
10
|
+
{ includeDerived = false } = {},
|
|
11
|
+
) {
|
|
12
|
+
const queue = [...evidence(sources)];
|
|
13
|
+
const visited = new Set();
|
|
14
|
+
const roots = new Set();
|
|
15
|
+
const read = evidenceReader(db);
|
|
16
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
17
|
+
if (visited.size >= 2048) return []; // Fail closed on damaged/unbounded graphs.
|
|
18
|
+
const ref = queue[cursor];
|
|
19
|
+
if (visited.has(ref)) continue;
|
|
20
|
+
visited.add(ref);
|
|
21
|
+
const kind = ref[0],
|
|
22
|
+
id = ref.slice(2);
|
|
23
|
+
if (["m", "r", "f"].includes(kind)) {
|
|
24
|
+
if (kind === "m") {
|
|
25
|
+
const event = read(
|
|
26
|
+
"SELECT time,CASE WHEN json_valid(payload) THEN json_extract(payload,'$.artifact.workId') END work_id FROM core_events WHERE seq=?",
|
|
27
|
+
[Number(id)],
|
|
28
|
+
),
|
|
29
|
+
workId = event?.time <= before ? event.work_id : null;
|
|
30
|
+
if (workId) {
|
|
31
|
+
const work = read("SELECT task_id FROM mind_time_works WHERE id=?", [
|
|
32
|
+
workId,
|
|
33
|
+
]);
|
|
34
|
+
if (work?.task_id) {
|
|
35
|
+
queue.push(`x:${work.task_id}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
roots.add(ref);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (kind === "o" || kind === "e") {
|
|
44
|
+
const row =
|
|
45
|
+
kind === "o"
|
|
46
|
+
? read(
|
|
47
|
+
"SELECT created FROM mind_observations WHERE id=? AND revoked=0",
|
|
48
|
+
[id],
|
|
49
|
+
)
|
|
50
|
+
: read(
|
|
51
|
+
"SELECT finished AS created FROM mind_actions WHERE id=? AND state='done'",
|
|
52
|
+
[id],
|
|
53
|
+
);
|
|
54
|
+
if (row && row.created <= before) roots.add(ref);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
let rows = [];
|
|
58
|
+
if (kind === "x") {
|
|
59
|
+
rows = read(
|
|
60
|
+
"SELECT sources,created,(SELECT MIN(v.created) FROM mind_time_versions v JOIN mind_time_works w ON w.id=v.work_id WHERE w.task_id=mind_time_tasks.id) first_version FROM mind_time_tasks WHERE id=?",
|
|
61
|
+
[id],
|
|
62
|
+
true,
|
|
63
|
+
).filter(
|
|
64
|
+
(row) =>
|
|
65
|
+
row.created <= before &&
|
|
66
|
+
row.first_version !== null &&
|
|
67
|
+
row.first_version <= before,
|
|
68
|
+
);
|
|
69
|
+
if (rows.length) roots.add(ref);
|
|
70
|
+
} else if (kind === "t")
|
|
71
|
+
rows = read(
|
|
72
|
+
"SELECT sources,parent_id,created FROM mind_thoughts WHERE id=? AND hidden=0",
|
|
73
|
+
[id],
|
|
74
|
+
true,
|
|
75
|
+
);
|
|
76
|
+
else if (kind === "d")
|
|
77
|
+
rows = read(
|
|
78
|
+
"SELECT sources,created FROM mind_diary WHERE day=?",
|
|
79
|
+
[id],
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
82
|
+
else if (kind === "s")
|
|
83
|
+
rows = read(
|
|
84
|
+
"SELECT sources,created FROM mind_self WHERE thread=? AND thread NOT IN (SELECT target_id FROM mind_revocations WHERE target_kind='self')",
|
|
85
|
+
[id],
|
|
86
|
+
true,
|
|
87
|
+
);
|
|
88
|
+
else if (kind === "g")
|
|
89
|
+
rows = read(
|
|
90
|
+
"SELECT sources,created FROM mind_meetings WHERE id=? AND id NOT IN (SELECT target_id FROM mind_revocations WHERE target_kind='meeting')",
|
|
91
|
+
[id],
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
94
|
+
else if (kind === "a")
|
|
95
|
+
rows = read(
|
|
96
|
+
"SELECT sources,created FROM mind_anticipations WHERE id=?",
|
|
97
|
+
[id],
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
rows = rows.filter((row) => row.created <= before);
|
|
101
|
+
// A self-originated wish is one stable internal source. Retelling it
|
|
102
|
+
// must not mint another one; it is distinguishable from external roots.
|
|
103
|
+
if (
|
|
104
|
+
kind === "s" &&
|
|
105
|
+
rows.length &&
|
|
106
|
+
rows.some((row) => !evidence(parse(row.sources, [])).length)
|
|
107
|
+
)
|
|
108
|
+
roots.add(ref);
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
queue.push(...evidence(parse(row.sources, [])));
|
|
111
|
+
if (row.parent_id) queue.push(`t:${row.parent_id}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...new Set([...roots, ...(includeDerived ? visited : [])])];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Evidence outside the model's own wording. Unsent drafts, simulations,
|
|
118
|
+
// unknown citations and new names for an internal wish do not qualify.
|
|
119
|
+
export function externalEvidence(db, sources, before) {
|
|
120
|
+
return evidenceRoots(db, sources, before).filter((ref) => {
|
|
121
|
+
const id = ref.slice(2);
|
|
122
|
+
if (ref.startsWith("x:"))
|
|
123
|
+
return !!db
|
|
124
|
+
.prepare(
|
|
125
|
+
"SELECT 1 FROM mind_time_tasks WHERE id=? AND state!='abandoned' AND created<=? AND EXISTS(SELECT 1 FROM mind_time_versions v JOIN mind_time_works w ON w.id=v.work_id WHERE w.task_id=mind_time_tasks.id AND v.created<=? AND length(v.content)>0) AND (SELECT COALESCE(SUM(active_ms),0) FROM mind_time_spans WHERE task_id=mind_time_tasks.id AND updated<=?)>=300000",
|
|
126
|
+
)
|
|
127
|
+
.get(id, before, before, before);
|
|
128
|
+
if (ref.startsWith("m:"))
|
|
129
|
+
return !!db
|
|
130
|
+
.prepare(
|
|
131
|
+
"SELECT 1 FROM core_events WHERE seq=? AND time<=? AND role='user' AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND seq NOT IN (SELECT seq FROM mind_unlived)",
|
|
132
|
+
)
|
|
133
|
+
.get(Number(id), before);
|
|
134
|
+
if (ref.startsWith("f:"))
|
|
135
|
+
return !!db
|
|
136
|
+
.prepare(
|
|
137
|
+
"SELECT 1 FROM reply_feedback f JOIN decisions d ON d.id=f.decision_id WHERE f.decision_id=? AND f.time<=? AND d.is_demo=0",
|
|
138
|
+
)
|
|
139
|
+
.get(id, before);
|
|
140
|
+
if (ref.startsWith("r:"))
|
|
141
|
+
return !!db
|
|
142
|
+
.prepare(
|
|
143
|
+
"SELECT 1 FROM mind_readings WHERE chunk_id=? AND created<=? LIMIT 1",
|
|
144
|
+
)
|
|
145
|
+
.get(id, before);
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { evidenceRoots } from "./evidence.js";
|
|
3
|
+
import { evidence, hasCredential, similar, text } from "./util.js";
|
|
4
|
+
|
|
5
|
+
// A face is who she has become in one place: the role she plays there, how
|
|
6
|
+
// she talks there and what she would like to be there.
|
|
7
|
+
export class Faces {
|
|
8
|
+
constructor(mind) {
|
|
9
|
+
this.mind = mind;
|
|
10
|
+
this.db = mind.db;
|
|
11
|
+
}
|
|
12
|
+
revoked() {
|
|
13
|
+
return new Set(
|
|
14
|
+
this.db
|
|
15
|
+
.prepare(
|
|
16
|
+
"SELECT target_id FROM mind_revocations WHERE target_kind='face'",
|
|
17
|
+
)
|
|
18
|
+
.all()
|
|
19
|
+
.map((row) => row.target_id),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
history(session, limit = 30) {
|
|
23
|
+
const revoked = this.revoked();
|
|
24
|
+
return this.db
|
|
25
|
+
.prepare(
|
|
26
|
+
"SELECT * FROM mind_faces WHERE session_id=? ORDER BY created DESC, rowid DESC LIMIT ?",
|
|
27
|
+
)
|
|
28
|
+
.all(session, limit)
|
|
29
|
+
.map((row) => ({
|
|
30
|
+
...row,
|
|
31
|
+
sources: JSON.parse(row.sources),
|
|
32
|
+
revoked: revoked.has(row.id),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
current(session, before = Number.MAX_SAFE_INTEGER) {
|
|
36
|
+
const revoked = this.revoked();
|
|
37
|
+
return (
|
|
38
|
+
this.db
|
|
39
|
+
.prepare(
|
|
40
|
+
"SELECT * FROM mind_faces WHERE session_id=? AND created<=? ORDER BY created DESC, rowid DESC LIMIT 20",
|
|
41
|
+
)
|
|
42
|
+
.all(session, before)
|
|
43
|
+
.find((row) => !revoked.has(row.id)) || null
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
all(before = Number.MAX_SAFE_INTEGER) {
|
|
47
|
+
return this.db
|
|
48
|
+
.prepare("SELECT DISTINCT session_id FROM mind_faces WHERE created<=?")
|
|
49
|
+
.all(before)
|
|
50
|
+
.map((row) => this.current(row.session_id, before))
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
propose(
|
|
54
|
+
input,
|
|
55
|
+
{ valid = null, origin = "solitude", time = Date.now() } = {},
|
|
56
|
+
) {
|
|
57
|
+
const session = String(input?.session || "");
|
|
58
|
+
if (!this.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
|
|
59
|
+
return { rejected: "会话不存在" };
|
|
60
|
+
const next = {
|
|
61
|
+
role: text(input.role, 40),
|
|
62
|
+
tone: text(input.tone, 60),
|
|
63
|
+
aspiration: text(input.aspiration, 80),
|
|
64
|
+
content: text(input.content, 300),
|
|
65
|
+
};
|
|
66
|
+
if (!Object.values(next).some(Boolean)) return { rejected: "空内容" };
|
|
67
|
+
if (hasCredential(Object.values(next).join(" ")))
|
|
68
|
+
return { rejected: "疑似凭据" };
|
|
69
|
+
if (session.includes(":group:") || session.startsWith("group:"))
|
|
70
|
+
if (!this.mind.meetings.sayable(Object.values(next).join(" "), session))
|
|
71
|
+
return { rejected: "不能把私下说法写进群里的面貌" };
|
|
72
|
+
const cited = evidence(input.sources);
|
|
73
|
+
const sources = valid ? cited.filter((s) => valid.has(s)) : cited;
|
|
74
|
+
if (!sources.length && origin !== "migration")
|
|
75
|
+
return { rejected: "缺少来源" };
|
|
76
|
+
const roots = this.mind.meetings.privateRoots(sources);
|
|
77
|
+
if (roots.length && !roots.includes(session))
|
|
78
|
+
return { rejected: "来源不能带到这个会话" };
|
|
79
|
+
const spent = new Set();
|
|
80
|
+
for (const row of this.db
|
|
81
|
+
.prepare("SELECT sources FROM mind_faces WHERE session_id=?")
|
|
82
|
+
.all(session))
|
|
83
|
+
for (const source of evidenceRoots(
|
|
84
|
+
this.db,
|
|
85
|
+
JSON.parse(row.sources || "[]"),
|
|
86
|
+
time,
|
|
87
|
+
))
|
|
88
|
+
spent.add(source);
|
|
89
|
+
if (
|
|
90
|
+
sources.length &&
|
|
91
|
+
!evidenceRoots(this.db, sources, time).some(
|
|
92
|
+
(source) => !spent.has(source),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return { rejected: "没有新的经历" };
|
|
96
|
+
const current = this.current(session, time);
|
|
97
|
+
if (current) {
|
|
98
|
+
for (const key of Object.keys(next))
|
|
99
|
+
if (!next[key]) next[key] = current[key];
|
|
100
|
+
if (
|
|
101
|
+
Object.keys(next).every((key) =>
|
|
102
|
+
next[key] ? similar(next[key], current[key], 0.85) : !current[key],
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return { rejected: "没有变化" };
|
|
106
|
+
}
|
|
107
|
+
const id = randomUUID();
|
|
108
|
+
this.db
|
|
109
|
+
.prepare(
|
|
110
|
+
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
111
|
+
)
|
|
112
|
+
.run(
|
|
113
|
+
id,
|
|
114
|
+
session,
|
|
115
|
+
time,
|
|
116
|
+
next.role,
|
|
117
|
+
next.tone,
|
|
118
|
+
next.aspiration,
|
|
119
|
+
next.content,
|
|
120
|
+
JSON.stringify(sources),
|
|
121
|
+
origin,
|
|
122
|
+
);
|
|
123
|
+
return { id };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function describeFace(face) {
|
|
128
|
+
if (!face) return "";
|
|
129
|
+
return [
|
|
130
|
+
face.role && `在这里我是${face.role}`,
|
|
131
|
+
face.tone && `说话${face.tone}`,
|
|
132
|
+
face.aspiration && `想${face.aspiration.replace(/^想/, "")}`,
|
|
133
|
+
face.content,
|
|
134
|
+
]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join(";")
|
|
137
|
+
.slice(0, 360);
|
|
138
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { hasCredential, normalized } from "./util.js";
|
|
2
|
+
|
|
3
|
+
export { hasCredential };
|
|
4
|
+
|
|
5
|
+
const SECRET_REQUEST =
|
|
6
|
+
/(?:帮我|替我|给我|你要|记得)?保密|别(?:告诉|跟|和|对)(?:别人|其他人|任何人|他们|大家)|不要(?:告诉|跟|和|说给)(?:别人|其他人|任何人|大家)|(?:只|就)(?:告诉|跟|和)你(?:一个人)?说|(?:不要|别)(?:说|传)出去|别外传|别声张/;
|
|
7
|
+
|
|
8
|
+
export function secretRequest(text) {
|
|
9
|
+
return SECRET_REQUEST.test(String(text || ""));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CRISIS =
|
|
13
|
+
/自杀|不想活|活不下去|活着没意思|结束(?:自己的)?生命|伤害自己|割腕|跳楼|轻生|想死(?!你)|撑不下去了/;
|
|
14
|
+
const CASUAL_DEATH = /(?:笑|社|饿|累|困|热|冷|烦|气|丑|尬|馋)死|想死你/;
|
|
15
|
+
const EXPLICIT =
|
|
16
|
+
/自杀|不想活|活不下去|结束(?:自己的)?生命|伤害自己|割腕|跳楼|轻生/;
|
|
17
|
+
|
|
18
|
+
// A cheap signal that makes sure she actually reads the message. Whether it
|
|
19
|
+
// is a real crisis is still her understanding of the whole conversation.
|
|
20
|
+
export function crisisSignal(text) {
|
|
21
|
+
const value = String(text || "");
|
|
22
|
+
if (!CRISIS.test(value)) return false;
|
|
23
|
+
return EXPLICIT.test(value) || !CASUAL_DEATH.test(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// "某作玩过""都玩过一些""通关了""玩得比较杂": a claim to have played
|
|
27
|
+
// something. She has no record of playing anything, so it is not a fact she
|
|
28
|
+
// can give. Wishes ("想通关"), questions ("玩过吗"), other people ("他玩过")
|
|
29
|
+
// and "没玩过" are not claims.
|
|
30
|
+
const NOT_A_CLAIM = "没未不还想要会能可等算你他她谁人";
|
|
31
|
+
const CLAIMED_PLAY = new RegExp(
|
|
32
|
+
`(?<![${NOT_A_CLAIM}][^,。!?,.!?\\s]{0,4})(?:也|都|早就|之前|以前|已经|全)?(?:玩过|通关|二周目|全结局|全成就)(?![吗么没??]|不(?![少多])|的?吧)|(?<![你他她谁人][^,。!?,.!?\\s]{0,6})玩得(?:比较|挺|还)?(?:杂|多)(?![吗么??]|的?吧)`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export function claimedPlay(text) {
|
|
36
|
+
return CLAIMED_PLAY.test(String(text || ""));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Secrets never enter another conversation's context, but a reply is still
|
|
40
|
+
// checked against them before it leaves.
|
|
41
|
+
export function leaks(bubbles, secrets) {
|
|
42
|
+
const said = normalized((bubbles || []).join(""));
|
|
43
|
+
if (said.length < 4) return [];
|
|
44
|
+
const pairs = (s) =>
|
|
45
|
+
new Set(Array.from({ length: s.length - 1 }, (_, i) => s.slice(i, i + 2)));
|
|
46
|
+
const spoken = pairs(said);
|
|
47
|
+
const found = [];
|
|
48
|
+
for (const secret of secrets || []) {
|
|
49
|
+
const key = normalized(secret.content);
|
|
50
|
+
if (key.length < 4) continue;
|
|
51
|
+
const grams = pairs(key);
|
|
52
|
+
let hit =
|
|
53
|
+
said.includes(key) ||
|
|
54
|
+
[...grams].filter((g) => spoken.has(g)).length / grams.size >= 0.6;
|
|
55
|
+
for (let i = 0; !hit && i + 6 <= key.length; i += 2)
|
|
56
|
+
if (said.includes(key.slice(i, i + 6))) hit = true;
|
|
57
|
+
if (hit) found.push(secret);
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|