luckytri 1.1.2 → 1.2.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/README.en.md +14 -12
- package/README.md +4 -2
- package/docs/en/CHANGELOG.md +24 -0
- package/docs/en/architecture.md +12 -10
- package/docs/en/chat-style.md +13 -13
- package/docs/en/cognition.md +12 -4
- package/docs/en/evolution.md +35 -0
- package/docs/en/her-life.md +5 -2
- package/docs/en/vision.md +50 -0
- package/docs/zh/CHANGELOG.md +24 -0
- package/docs/zh/architecture.md +12 -10
- package/docs/zh/chat-style.md +17 -17
- package/docs/zh/cognition.md +12 -4
- package/docs/zh/evolution.md +35 -0
- package/docs/zh/her-life.md +5 -2
- package/docs/zh/vision.md +50 -0
- package/package.json +4 -1
- package/public/app/assets/{ChatsPage-IfBgNdaD.js → ChatsPage-eDqFIg97.js} +1 -1
- package/public/app/assets/{Empty-DK9nQPCf.js → Empty-CAGOJsfD.js} +1 -1
- package/public/app/assets/{Field-BzKFhuFd.js → Field-CRz-JmN1.js} +1 -1
- package/public/app/assets/{HeartPage-CKxrbfFQ.js → HeartPage-BGWoeMrC.js} +1 -1
- package/public/app/assets/{LifePage-CtxPEi6E.js → LifePage-iao3o3wt.js} +1 -1
- package/public/app/assets/{MemoryPage-B8FLXzLi.js → MemoryPage-Dwi2yi7E.js} +1 -1
- package/public/app/assets/NaturePage-DniImo9U.js +3 -0
- package/public/app/assets/{NaturePage-CHkpb5NK.css → NaturePage-zguDpGWY.css} +1 -1
- package/public/app/assets/{PeoplePage-D3yn0HV0.js → PeoplePage-D-MC3JEt.js} +1 -1
- package/public/app/assets/{PluginsPage-ojv8zawj.js → PluginsPage-D7dJTf96.js} +1 -1
- package/public/app/assets/SystemPage-C5CUHjGh.js +1 -0
- package/public/app/assets/{SystemPage-CGVTvNZw.css → SystemPage-C_Y5M7JU.css} +1 -1
- package/public/app/assets/{TimePage-Dyd-FFbq.js → TimePage-Dwhr3-92.js} +1 -1
- package/public/app/assets/{index-BJfzfgnm.js → index-L0bHax8C.js} +3 -3
- package/public/app/assets/{knowledge-C-bND0Ub.js → knowledge-BTDNafzl.js} +1 -1
- package/public/app/assets/{media-D7G3Nco6.js → media-BBzOAfNG.js} +1 -1
- package/public/app/assets/{mind-ec-G6iNZ.js → mind-CBTkFje2.js} +1 -1
- package/public/app/index.html +3 -3
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/scripts/migrate-instance.js +27 -0
- package/scripts/update-instance.js +21 -2
- package/server/channels/contract.js +5 -1
- package/server/channels/hub.js +2 -2
- package/server/channels/onebot/adapter.js +291 -288
- package/server/channels/onebot/index.js +2 -2
- package/server/channels/qqbot/api.js +194 -199
- package/server/core/context-builder.js +20 -3
- package/server/core/conversation-cues.js +5 -4
- package/server/core/conversation-grounding.js +20 -17
- package/server/core/conversation-needs.js +32 -0
- package/server/core/dialogue-context.js +2 -5
- package/server/core/dialogue-contract.js +23 -16
- package/server/core/message-scheduler.js +85 -78
- package/server/core/model-manager.js +113 -16
- package/server/core/model-presets.js +1 -1
- package/server/core/orchestrator.js +18 -2
- package/server/core/persona-manager.js +14 -5
- package/server/core/reader-check.js +47 -42
- package/server/core/reply-delivery.js +125 -34
- package/server/core/response-validator.js +28 -1
- package/server/core/retired-prompts.js +5 -0
- package/server/core/speech-length.js +43 -0
- package/server/core/turn-processor.js +38 -5
- package/server/core/turn.js +70 -16
- package/server/i18n/errors.js +30 -0
- package/server/i18n/labels.js +5 -0
- package/server/index.js +3 -0
- package/server/knowledge/embedding-profile.js +126 -29
- package/server/knowledge/manager.js +103 -34
- package/server/knowledge/schema.js +197 -158
- package/server/mind/affect.js +267 -265
- package/server/mind/aftermath.js +169 -0
- package/server/mind/anticipations.js +2 -0
- package/server/mind/clock.js +91 -83
- package/server/mind/discretion.js +20 -0
- package/server/mind/episodes.js +2 -1
- package/server/mind/index.js +17 -2
- package/server/mind/initiative.js +29 -14
- package/server/mind/memory.js +47 -5
- package/server/mind/recall/cue.js +67 -0
- package/server/mind/recall/index.js +183 -29
- package/server/mind/recall/roots.js +231 -0
- package/server/mind/recall/schema.js +2 -0
- package/server/mind/recall/store.js +113 -30
- package/server/mind/salience.js +22 -1
- package/server/mind/schema.js +5 -0
- package/server/mind/thoughts.js +276 -274
- package/server/mind/view.js +100 -60
- package/server/plugins/install.js +1 -6
- package/server/plugins/zip.js +18 -4
- package/server/runtime/data-migration.js +193 -0
- package/server/runtime/update-execution.js +43 -4
- package/server/runtime/update-manager.js +4 -1
- package/server/storage/archive.js +1 -1
- package/server/storage/backup-scheduler.js +15 -3
- package/public/app/assets/NaturePage-C7i607ul.js +0 -3
- package/public/app/assets/SystemPage-Cv3rFvyT.js +0 -1
package/server/mind/thoughts.js
CHANGED
|
@@ -1,274 +1,276 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { evidenceRoots } from "./evidence.js";
|
|
3
|
-
import {
|
|
4
|
-
THOUGHT_FADED,
|
|
5
|
-
anyTouches,
|
|
6
|
-
cueList,
|
|
7
|
-
thoughtSalience,
|
|
8
|
-
} from "./salience.js";
|
|
9
|
-
import { HOUR, clamp, evidence, parse, text } from "./util.js";
|
|
10
|
-
|
|
11
|
-
export const THOUGHT_KINDS = {
|
|
12
|
-
reflection: "后来想到",
|
|
13
|
-
revision: "重新理解",
|
|
14
|
-
unfinished: "仍放在心上",
|
|
15
|
-
reconnection: "久别想起",
|
|
16
|
-
expression: "自己的念头",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function row(value) {
|
|
20
|
-
return value
|
|
21
|
-
? {
|
|
22
|
-
...value,
|
|
23
|
-
sessions: parse(value.sessions, []),
|
|
24
|
-
sources: parse(value.sources, []),
|
|
25
|
-
outreach_draft: parse(value.outreach_draft, []),
|
|
26
|
-
}
|
|
27
|
-
: null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Her journal between conversations. A later understanding is a new entry
|
|
31
|
-
// pointing at the old one; the old words stay as they were.
|
|
32
|
-
export class Thoughts {
|
|
33
|
-
constructor(mind) {
|
|
34
|
-
this.mind = mind;
|
|
35
|
-
this.db = mind.db;
|
|
36
|
-
}
|
|
37
|
-
add({
|
|
38
|
-
kind,
|
|
39
|
-
content,
|
|
40
|
-
sessions = [],
|
|
41
|
-
sources = [],
|
|
42
|
-
parentId = null,
|
|
43
|
-
importance = 0.5,
|
|
44
|
-
revisitHours = 0,
|
|
45
|
-
outreach = "",
|
|
46
|
-
outreachSession = null,
|
|
47
|
-
outreachAfterHours,
|
|
48
|
-
outreachReason = "",
|
|
49
|
-
runId = null,
|
|
50
|
-
time = Date.now(),
|
|
51
|
-
}) {
|
|
52
|
-
const id = randomUUID();
|
|
53
|
-
const hours = Number(revisitHours);
|
|
54
|
-
this.db
|
|
55
|
-
.prepare(
|
|
56
|
-
"INSERT INTO mind_thoughts(id,created,kind,content,sessions,sources,parent_id,importance,revisit_at,status,hidden,outreach,outreach_session,outreach_status,run_id,outreach_at,outreach_reason) VALUES (?,?,?,?,?,?,?,?,?,'open',0,?,?,?,?,?,?)",
|
|
57
|
-
)
|
|
58
|
-
.run(
|
|
59
|
-
id,
|
|
60
|
-
time,
|
|
61
|
-
THOUGHT_KINDS[kind] ? kind : "reflection",
|
|
62
|
-
text(content, 600),
|
|
63
|
-
JSON.stringify([...new Set(sessions)].slice(0, 12)),
|
|
64
|
-
JSON.stringify(evidence(sources)),
|
|
65
|
-
parentId || null,
|
|
66
|
-
clamp(importance),
|
|
67
|
-
Number.isFinite(hours) && hours > 0
|
|
68
|
-
? time + Math.min(8760, Math.max(1, hours)) * HOUR
|
|
69
|
-
: null,
|
|
70
|
-
text(outreach, 120),
|
|
71
|
-
outreach ? outreachSession : null,
|
|
72
|
-
outreach ? "planned" : "none",
|
|
73
|
-
runId,
|
|
74
|
-
outreach
|
|
75
|
-
? time +
|
|
76
|
-
Math.min(
|
|
77
|
-
8760,
|
|
78
|
-
Math.max(0, Number(outreachAfterHours ?? revisitHours) || 0),
|
|
79
|
-
) *
|
|
80
|
-
HOUR
|
|
81
|
-
: null,
|
|
82
|
-
text(outreachReason, 300),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
throw Error("
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { evidenceRoots } from "./evidence.js";
|
|
3
|
+
import {
|
|
4
|
+
THOUGHT_FADED,
|
|
5
|
+
anyTouches,
|
|
6
|
+
cueList,
|
|
7
|
+
thoughtSalience,
|
|
8
|
+
} from "./salience.js";
|
|
9
|
+
import { HOUR, clamp, evidence, parse, text } from "./util.js";
|
|
10
|
+
|
|
11
|
+
export const THOUGHT_KINDS = {
|
|
12
|
+
reflection: "后来想到",
|
|
13
|
+
revision: "重新理解",
|
|
14
|
+
unfinished: "仍放在心上",
|
|
15
|
+
reconnection: "久别想起",
|
|
16
|
+
expression: "自己的念头",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function row(value) {
|
|
20
|
+
return value
|
|
21
|
+
? {
|
|
22
|
+
...value,
|
|
23
|
+
sessions: parse(value.sessions, []),
|
|
24
|
+
sources: parse(value.sources, []),
|
|
25
|
+
outreach_draft: parse(value.outreach_draft, []),
|
|
26
|
+
}
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Her journal between conversations. A later understanding is a new entry
|
|
31
|
+
// pointing at the old one; the old words stay as they were.
|
|
32
|
+
export class Thoughts {
|
|
33
|
+
constructor(mind) {
|
|
34
|
+
this.mind = mind;
|
|
35
|
+
this.db = mind.db;
|
|
36
|
+
}
|
|
37
|
+
add({
|
|
38
|
+
kind,
|
|
39
|
+
content,
|
|
40
|
+
sessions = [],
|
|
41
|
+
sources = [],
|
|
42
|
+
parentId = null,
|
|
43
|
+
importance = 0.5,
|
|
44
|
+
revisitHours = 0,
|
|
45
|
+
outreach = "",
|
|
46
|
+
outreachSession = null,
|
|
47
|
+
outreachAfterHours,
|
|
48
|
+
outreachReason = "",
|
|
49
|
+
runId = null,
|
|
50
|
+
time = Date.now(),
|
|
51
|
+
}) {
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
const hours = Number(revisitHours);
|
|
54
|
+
this.db
|
|
55
|
+
.prepare(
|
|
56
|
+
"INSERT INTO mind_thoughts(id,created,kind,content,sessions,sources,parent_id,importance,revisit_at,status,hidden,outreach,outreach_session,outreach_status,run_id,outreach_at,outreach_reason) VALUES (?,?,?,?,?,?,?,?,?,'open',0,?,?,?,?,?,?)",
|
|
57
|
+
)
|
|
58
|
+
.run(
|
|
59
|
+
id,
|
|
60
|
+
time,
|
|
61
|
+
THOUGHT_KINDS[kind] ? kind : "reflection",
|
|
62
|
+
text(content, 600),
|
|
63
|
+
JSON.stringify([...new Set(sessions)].slice(0, 12)),
|
|
64
|
+
JSON.stringify(evidence(sources)),
|
|
65
|
+
parentId || null,
|
|
66
|
+
clamp(importance),
|
|
67
|
+
Number.isFinite(hours) && hours > 0
|
|
68
|
+
? time + Math.min(8760, Math.max(1, hours)) * HOUR
|
|
69
|
+
: null,
|
|
70
|
+
text(outreach, 120),
|
|
71
|
+
outreach ? outreachSession : null,
|
|
72
|
+
outreach ? "planned" : "none",
|
|
73
|
+
runId,
|
|
74
|
+
outreach
|
|
75
|
+
? time +
|
|
76
|
+
Math.min(
|
|
77
|
+
8760,
|
|
78
|
+
Math.max(0, Number(outreachAfterHours ?? revisitHours) || 0),
|
|
79
|
+
) *
|
|
80
|
+
HOUR
|
|
81
|
+
: null,
|
|
82
|
+
text(outreachReason, 300),
|
|
83
|
+
);
|
|
84
|
+
// Searchable, and dated in the audit, from the moment it is written.
|
|
85
|
+
this.mind.recall?.sync("thought", id);
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
get(id) {
|
|
89
|
+
return row(
|
|
90
|
+
this.db.prepare("SELECT * FROM mind_thoughts WHERE id=?").get(id),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
list({
|
|
94
|
+
before = Number.MAX_SAFE_INTEGER,
|
|
95
|
+
session = "",
|
|
96
|
+
q = "",
|
|
97
|
+
limit = 30,
|
|
98
|
+
hidden = true,
|
|
99
|
+
} = {}) {
|
|
100
|
+
return this.db
|
|
101
|
+
.prepare(
|
|
102
|
+
`SELECT * FROM mind_thoughts WHERE created<?${hidden ? "" : " AND hidden=0"}${session ? " AND sessions LIKE ?" : ""}${q ? " AND content LIKE ?" : ""} ORDER BY created DESC LIMIT ?`,
|
|
103
|
+
)
|
|
104
|
+
.all(
|
|
105
|
+
before,
|
|
106
|
+
...(session ? [`%${JSON.stringify(session).slice(1, -1)}%`] : []),
|
|
107
|
+
...(q ? [`%${q}%`] : []),
|
|
108
|
+
limit,
|
|
109
|
+
)
|
|
110
|
+
.map(row);
|
|
111
|
+
}
|
|
112
|
+
// Still on her mind at `now`, ranked by how present each one is; a thought
|
|
113
|
+
// let go of later still counts as open when looking back. A later note
|
|
114
|
+
// that only repeats sources already used does not start this clock over.
|
|
115
|
+
weighed({ now = Date.now(), session = "" } = {}) {
|
|
116
|
+
const lived = this.mind.days.lived(now);
|
|
117
|
+
const notes = this.db
|
|
118
|
+
.prepare(
|
|
119
|
+
"SELECT * FROM mind_thoughts WHERE hidden=0 AND created<=? AND (status='open' OR resolved_at>?) ORDER BY created DESC LIMIT 400",
|
|
120
|
+
)
|
|
121
|
+
.all(now, now)
|
|
122
|
+
.map(row)
|
|
123
|
+
.filter((t) => !session || t.sessions.includes(session));
|
|
124
|
+
const clock = sourceClock(notes, (sources) =>
|
|
125
|
+
evidenceRoots(this.db, sources, now),
|
|
126
|
+
);
|
|
127
|
+
return notes
|
|
128
|
+
.map((t) => {
|
|
129
|
+
const earned = clock.get(t.id) ?? t.created;
|
|
130
|
+
// A revisit brings back the note that actually lived the experience.
|
|
131
|
+
// Rewriting it later and setting another time does not.
|
|
132
|
+
const seen = earned < t.created ? { ...t, revisit_at: null } : t;
|
|
133
|
+
return {
|
|
134
|
+
...t,
|
|
135
|
+
salience: thoughtSalience({ ...seen, created: earned }, lived, now),
|
|
136
|
+
};
|
|
137
|
+
})
|
|
138
|
+
.sort((a, b) => b.salience - a.salience || b.created - a.created);
|
|
139
|
+
}
|
|
140
|
+
open({ now = Date.now(), limit = 12, session = "" } = {}) {
|
|
141
|
+
return this.weighed({ now, session })
|
|
142
|
+
.filter((t) => t.salience >= THOUGHT_FADED)
|
|
143
|
+
.slice(0, limit);
|
|
144
|
+
}
|
|
145
|
+
// The studio card shows the newest note still in view. Salience still
|
|
146
|
+
// decides which thoughts a conversation carries.
|
|
147
|
+
latest(now = Date.now()) {
|
|
148
|
+
return (
|
|
149
|
+
this.open({ now, limit: 40 }).sort((a, b) => b.created - a.created)[0] ||
|
|
150
|
+
null
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// Faded thoughts from this place that the conversation brings back.
|
|
154
|
+
reminded({ now = Date.now(), cue, cues, session = "", limit = 1 } = {}) {
|
|
155
|
+
const sets = cueList(cue, cues);
|
|
156
|
+
if (!sets.length) return [];
|
|
157
|
+
return this.weighed({ now, session })
|
|
158
|
+
.filter((t) => t.salience < THOUGHT_FADED && anyTouches(t.content, sets))
|
|
159
|
+
.slice(0, limit);
|
|
160
|
+
}
|
|
161
|
+
resolve(id, resolution = "", time = Date.now()) {
|
|
162
|
+
return (
|
|
163
|
+
this.db
|
|
164
|
+
.prepare(
|
|
165
|
+
"UPDATE mind_thoughts SET status='resolved',resolved_at=?,resolution=? WHERE id=? AND status='open'",
|
|
166
|
+
)
|
|
167
|
+
.run(time, text(resolution, 120), id).changes > 0
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
update(id, { status, hidden }) {
|
|
171
|
+
const current = this.get(id);
|
|
172
|
+
if (!current) throw Error("手记不存在");
|
|
173
|
+
if (status !== undefined && !["open", "resolved"].includes(status))
|
|
174
|
+
throw Error("状态无效");
|
|
175
|
+
if (hidden !== undefined && typeof hidden !== "boolean")
|
|
176
|
+
throw Error("开关无效");
|
|
177
|
+
const next = status ?? current.status;
|
|
178
|
+
const resolved =
|
|
179
|
+
next === current.status
|
|
180
|
+
? [current.resolved_at ?? null, current.resolution ?? ""]
|
|
181
|
+
: next === "resolved"
|
|
182
|
+
? [Date.now(), "在工作台里放下"]
|
|
183
|
+
: [null, ""];
|
|
184
|
+
this.db
|
|
185
|
+
.prepare(
|
|
186
|
+
"UPDATE mind_thoughts SET status=?,hidden=?,resolved_at=?,resolution=? WHERE id=?",
|
|
187
|
+
)
|
|
188
|
+
.run(
|
|
189
|
+
next,
|
|
190
|
+
hidden === undefined ? current.hidden : +hidden,
|
|
191
|
+
...resolved,
|
|
192
|
+
id,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
remove(id) {
|
|
196
|
+
if (
|
|
197
|
+
this.db.prepare("SELECT 1 FROM mind_thoughts WHERE parent_id=?").get(id)
|
|
198
|
+
)
|
|
199
|
+
throw Error("这条手记有后续修正,请隐藏以保留轨迹");
|
|
200
|
+
this.db.prepare("DELETE FROM mind_thoughts WHERE id=?").run(id);
|
|
201
|
+
}
|
|
202
|
+
setOutreach(id, status, reason = "") {
|
|
203
|
+
this.db
|
|
204
|
+
.prepare(
|
|
205
|
+
"UPDATE mind_thoughts SET outreach_status=?,outreach_wait_reason=?,outreach_retry_at=NULL WHERE id=?",
|
|
206
|
+
)
|
|
207
|
+
.run(status, text(reason, 300), id);
|
|
208
|
+
}
|
|
209
|
+
planOutreach(id, { session, words, reason, time }) {
|
|
210
|
+
const current = this.get(id);
|
|
211
|
+
if (!current || current.outreach_status !== "none") return false;
|
|
212
|
+
const draft = words
|
|
213
|
+
.filter((s) => typeof s === "string" && s.trim())
|
|
214
|
+
.slice(0, 3)
|
|
215
|
+
.map((s) => text(s, 180));
|
|
216
|
+
if (!session || !draft.length) return false;
|
|
217
|
+
this.db
|
|
218
|
+
.prepare(
|
|
219
|
+
"UPDATE mind_thoughts SET sessions=?,outreach=?,outreach_session=?,outreach_status='planned',outreach_at=?,outreach_reason=?,outreach_draft=? WHERE id=? AND outreach_status='none'",
|
|
220
|
+
)
|
|
221
|
+
.run(
|
|
222
|
+
// An independent thought is not owned by its first audience. Keep its
|
|
223
|
+
// origin scope; outreach_session records where she chose to share it.
|
|
224
|
+
JSON.stringify(
|
|
225
|
+
current.kind === "expression"
|
|
226
|
+
? current.sessions
|
|
227
|
+
: [...new Set([...current.sessions, session])],
|
|
228
|
+
),
|
|
229
|
+
draft.join("\n"),
|
|
230
|
+
session,
|
|
231
|
+
time,
|
|
232
|
+
text(reason, 300),
|
|
233
|
+
JSON.stringify(draft),
|
|
234
|
+
id,
|
|
235
|
+
);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
deferOutreach(id, reason, retryAt) {
|
|
239
|
+
this.db
|
|
240
|
+
.prepare(
|
|
241
|
+
"UPDATE mind_thoughts SET outreach_status='planned',outreach_wait_reason=?,outreach_retry_at=? WHERE id=?",
|
|
242
|
+
)
|
|
243
|
+
.run(text(reason, 300), retryAt, id);
|
|
244
|
+
}
|
|
245
|
+
dueOutreach(now = Date.now()) {
|
|
246
|
+
return this.db
|
|
247
|
+
.prepare(
|
|
248
|
+
"SELECT * FROM mind_thoughts WHERE outreach_status='planned' AND hidden=0 AND status='open' AND outreach!='' AND COALESCE(outreach_at,revisit_at,created)<=? AND COALESCE(outreach_retry_at,0)<=? ORDER BY importance DESC, created LIMIT 5",
|
|
249
|
+
)
|
|
250
|
+
.all(now, now)
|
|
251
|
+
.map(row);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The first time a source was lived. A later note that only repeats those
|
|
256
|
+
// sources keeps that time, so rewriting it does not make it newly present.
|
|
257
|
+
function sourceClock(notes, roots) {
|
|
258
|
+
const earned = new Map();
|
|
259
|
+
const clock = new Map();
|
|
260
|
+
for (const note of [...notes].sort((a, b) => a.created - b.created)) {
|
|
261
|
+
const resolved = roots(note.sources);
|
|
262
|
+
const sources = resolved.length ? resolved : [`t:${note.id}`];
|
|
263
|
+
const fresh = sources.some((source) => !earned.has(source));
|
|
264
|
+
if (fresh) {
|
|
265
|
+
clock.set(note.id, note.created);
|
|
266
|
+
for (const source of sources)
|
|
267
|
+
if (!earned.has(source)) earned.set(source, note.created);
|
|
268
|
+
} else {
|
|
269
|
+
clock.set(
|
|
270
|
+
note.id,
|
|
271
|
+
Math.min(...sources.map((source) => earned.get(source))),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return clock;
|
|
276
|
+
}
|