luckytri 1.0.0 → 1.0.2
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 +4 -4
- package/README.en.md +252 -250
- package/README.md +252 -250
- package/bin/luckytri.js +5 -1
- package/docs/en/CHANGELOG.md +8 -0
- package/docs/en/connect.md +18 -18
- package/docs/en/guide.md +202 -216
- package/docs/en/npm-release.md +25 -25
- package/docs/zh/CHANGELOG.md +8 -0
- package/docs/zh/connect.md +17 -17
- package/docs/zh/guide.md +202 -216
- package/docs/zh/npm-release.md +33 -33
- package/package.json +110 -108
- package/plugins/sdk/index.d.ts +155 -155
- package/plugins/sdk/testing.js +58 -58
- package/plugins/weather/index.js +82 -82
- package/plugins/weather/luckytri-plugin.json +29 -29
- package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
- package/public/app/assets/Empty-DP6WMGHi.js +2 -0
- package/public/app/assets/Field-CtbdqaiB.js +1 -0
- package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
- package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
- package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
- package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
- package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
- package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
- package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
- package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
- package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
- package/public/app/assets/TimePage-7-awH5XL.js +3 -0
- package/public/app/assets/index-4rItIkD-.js +4 -0
- package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
- package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
- package/public/app/assets/media-SMfTT-yc.js +1 -0
- package/public/app/index.html +3 -3
- package/public/guide.en.html +72 -99
- package/public/guide.html +44 -86
- package/public/tutorial.en.md +25 -39
- package/public/tutorial.md +25 -39
- package/scripts/backup.js +311 -311
- package/scripts/browser.js +41 -41
- package/scripts/import-data.js +128 -0
- package/scripts/launch.js +101 -94
- package/scripts/plugin.js +150 -150
- package/scripts/reset-password.js +22 -0
- package/scripts/runtime.js +25 -25
- package/scripts/service.js +56 -47
- package/scripts/setup.js +50 -50
- package/scripts/start-server.js +41 -41
- package/scripts/stop.js +36 -36
- package/server/app.js +112 -96
- package/server/auth.js +217 -0
- package/server/channels/contract.js +57 -57
- package/server/channels/hub.js +165 -165
- package/server/channels/index.js +28 -25
- package/server/channels/onebot/index.js +335 -336
- package/server/channels/onebot/token.js +22 -3
- package/server/core/capabilities.js +208 -208
- package/server/core/events.js +64 -60
- package/server/core/image-preprocessor.js +208 -208
- package/server/core/model-manager.js +1213 -1213
- package/server/core/net-guard.js +49 -49
- package/server/core/orchestrator.js +569 -569
- package/server/core/perceivers.js +91 -91
- package/server/core/persona-manager.js +243 -243
- package/server/core/repository.js +293 -293
- package/server/core/response-validator.js +508 -508
- package/server/core/turn-processor.js +630 -630
- package/server/core/turn.js +236 -236
- package/server/core/vision-manager.js +633 -633
- package/server/i18n/errors.js +548 -497
- package/server/i18n/labels.js +649 -653
- package/server/index.js +151 -102
- package/server/knowledge/manager.js +386 -386
- package/server/knowledge/schema.js +12 -8
- package/server/local-access.js +32 -0
- package/server/mind/anticipations.js +507 -507
- package/server/mind/api.js +703 -703
- package/server/mind/budget.js +152 -152
- package/server/mind/context-selection.js +61 -61
- package/server/mind/day-planner.js +324 -324
- package/server/mind/evidence.js +148 -148
- package/server/mind/index.js +445 -445
- package/server/mind/life/diary.js +786 -786
- package/server/mind/life/index.js +1049 -1049
- package/server/mind/observations.js +84 -84
- package/server/mind/own-day.js +324 -324
- package/server/mind/own-voice.js +318 -318
- package/server/mind/senses.js +53 -53
- package/server/mind/time/activity-clock.js +196 -196
- package/server/mind/time/availability.js +69 -69
- package/server/mind/time/commitment-review.js +198 -198
- package/server/mind/time/executor.js +512 -512
- package/server/mind/time/experiences.js +128 -128
- package/server/mind/time/games.js +562 -562
- package/server/mind/time/index.js +739 -739
- package/server/mind/time/intent.js +166 -166
- package/server/mind/time/kinds.js +171 -171
- package/server/mind/time/plugin-activity.js +207 -207
- package/server/mind/time/tasks.js +587 -587
- package/server/mind/util.js +110 -110
- package/server/mind/view.js +273 -273
- package/server/paths.js +102 -102
- package/server/plugins/api.js +204 -204
- package/server/plugins/host.js +967 -967
- package/server/plugins/install.js +295 -295
- package/server/plugins/manifest.js +201 -201
- package/server/plugins/permissions.js +42 -42
- package/server/plugins/runner/guard.mjs +36 -36
- package/server/plugins/runner/worker.mjs +250 -250
- package/server/plugins/schema.js +91 -91
- package/server/plugins/zip.js +112 -112
- package/server/startup-lock.js +118 -118
- package/server/storage/archive.js +242 -241
- package/server/storage/backup-cleanup-worker.js +8 -8
- package/server/storage/backup-cleanup.js +415 -414
- package/server/storage/backup-lock.js +76 -76
- package/server/storage/backup-scheduler.js +131 -131
- package/server/storage/data-package.js +298 -0
- package/server/storage/data-transfer-api.js +50 -0
- package/server/storage/data-transfer-worker.js +18 -0
- package/server/storage/data-transfer.js +415 -0
- package/server/storage/store.js +202 -199
- package/server/studio/readiness.js +181 -190
- package/server/studio/settings.js +236 -236
- package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
- package/public/app/assets/Empty-CpP5kQHH.js +0 -2
- package/public/app/assets/Field-D4gY-INU.js +0 -1
- package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
- package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
- package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
- package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
- package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
- package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
- package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
- package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
- package/public/app/assets/TimePage-Cw44inM4.js +0 -3
- package/public/app/assets/index-BheRW-1t.js +0 -4
- package/public/app/assets/media-BzPDBm7e.js +0 -1
package/server/mind/own-voice.js
CHANGED
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
import { localClock } from "../core/conversation-cues.js";
|
|
2
|
-
import { replyPrompt, prompts } from "../core/persona-manager.js";
|
|
3
|
-
import { withFallback } from "../core/model-manager.js";
|
|
4
|
-
import { HOUR, evidence, hasCredential, text } from "./util.js";
|
|
5
|
-
import { isPrivateSession } from "./memory.js";
|
|
6
|
-
import { sameRecentTheme } from "./novelty.js";
|
|
7
|
-
import { ownLife, sameSelf } from "./salience.js";
|
|
8
|
-
import { conversationOrigin, originSummary } from "./conversation-origin.js";
|
|
9
|
-
|
|
10
|
-
// After this many notes in a row that only take up her own last note, another
|
|
11
|
-
// one has to pick up something new or it is not kept.
|
|
12
|
-
const STREAK = 3;
|
|
13
|
-
|
|
14
|
-
// Form a thought before choosing an audience. No transcript, unanswered
|
|
15
|
-
// question or destination is supplied here: those belong to delivery, not
|
|
16
|
-
// to the origin of what she wants to say.
|
|
17
|
-
export class OwnVoice {
|
|
18
|
-
constructor(life) {
|
|
19
|
-
this.life = life;
|
|
20
|
-
this.mind = life.mind;
|
|
21
|
-
}
|
|
22
|
-
// What has come into her life since `after` that this loop did not write
|
|
23
|
-
// itself: thoughts from solitude, night memory or a review, and threads
|
|
24
|
-
// those revised. Her own expression write-backs are not new.
|
|
25
|
-
arrived(after, until) {
|
|
26
|
-
const thoughts = this.mind.db
|
|
27
|
-
.prepare(
|
|
28
|
-
"SELECT id FROM mind_thoughts WHERE created>? AND created<=? AND kind!='expression' AND hidden=0",
|
|
29
|
-
)
|
|
30
|
-
.all(after, until)
|
|
31
|
-
.map((row) => `t:${row.id}`);
|
|
32
|
-
const threads = this.mind.db
|
|
33
|
-
.prepare(
|
|
34
|
-
"SELECT DISTINCT thread FROM mind_self WHERE created>? AND created<=? AND origin!='expression'",
|
|
35
|
-
)
|
|
36
|
-
.all(after, until)
|
|
37
|
-
.map((row) => `s:${row.thread}`);
|
|
38
|
-
return new Set([...thoughts, ...threads]);
|
|
39
|
-
}
|
|
40
|
-
// How many of her newest notes only took up the one before. A note is
|
|
41
|
-
// anchored when it cites something that arrived after the previous note.
|
|
42
|
-
circling(now) {
|
|
43
|
-
const notes = this.mind.thoughts
|
|
44
|
-
.list({ before: now + 1, limit: 40, hidden: false })
|
|
45
|
-
.filter((t) => t.kind === "expression");
|
|
46
|
-
let run = 0;
|
|
47
|
-
for (let i = 0; i + 1 < notes.length; i++) {
|
|
48
|
-
const note = notes[i];
|
|
49
|
-
const previous = notes[i + 1];
|
|
50
|
-
if (note.created - previous.created > 24 * HOUR) break;
|
|
51
|
-
const arrived = this.arrived(previous.created, note.created);
|
|
52
|
-
if (note.sources.some((ref) => arrived.has(ref))) break;
|
|
53
|
-
run++;
|
|
54
|
-
}
|
|
55
|
-
return { run, latest: notes[0] || null };
|
|
56
|
-
}
|
|
57
|
-
// Going round again with nothing new is not a reason to ask her: she has
|
|
58
|
-
// written several notes that only continue her last one, and nothing she
|
|
59
|
-
// could take up has come in since.
|
|
60
|
-
resting(now) {
|
|
61
|
-
const { run } = this.circling(now);
|
|
62
|
-
if (run < STREAK) return null;
|
|
63
|
-
const { circling } = this.material(now);
|
|
64
|
-
if (circling?.fresh.length) return null;
|
|
65
|
-
return `连着 ${run} 条念头都只在接自己上一条,还没有新的东西可接`;
|
|
66
|
-
}
|
|
67
|
-
material(now) {
|
|
68
|
-
const nature = this.mind.nature.current(now);
|
|
69
|
-
const threads = this.mind.self
|
|
70
|
-
.active({ before: now, now, limit: 40 })
|
|
71
|
-
.filter((s) => this.mind.meetings.stays(s, ""))
|
|
72
|
-
.filter((s) =>
|
|
73
|
-
["interest", "curiosity", "view", "intention"].includes(s.kind),
|
|
74
|
-
)
|
|
75
|
-
// Older summaries sometimes called advice or a duty to someone else
|
|
76
|
-
// an intention. Keep the record, but do not treat it as a life she
|
|
77
|
-
// is living.
|
|
78
|
-
.filter((s) => s.kind !== "intention" || ownLife(s.content))
|
|
79
|
-
.slice(0, 12);
|
|
80
|
-
const notes = this.mind.thoughts
|
|
81
|
-
.open({ now, limit: 40 })
|
|
82
|
-
.filter((t) =>
|
|
83
|
-
this.mind.meetings.stays(
|
|
84
|
-
{
|
|
85
|
-
...t,
|
|
86
|
-
session_id:
|
|
87
|
-
t.kind === "expression" && !t.sources.length
|
|
88
|
-
? ""
|
|
89
|
-
: t.sessions.find(isPrivateSession) || "",
|
|
90
|
-
},
|
|
91
|
-
"",
|
|
92
|
-
),
|
|
93
|
-
)
|
|
94
|
-
.sort((a, b) => b.created - a.created)
|
|
95
|
-
.slice(0, 6);
|
|
96
|
-
const recent = this.mind.thoughts
|
|
97
|
-
.list({ before: now + 1, limit: 80 })
|
|
98
|
-
.filter((t) => t.kind === "expression")
|
|
99
|
-
.slice(0, 12);
|
|
100
|
-
const { mood, energyLabel, phaseLabel } = this.mind.affect.state(now, {
|
|
101
|
-
nature,
|
|
102
|
-
});
|
|
103
|
-
// Only what she can see counts as something she could take up. A note
|
|
104
|
-
// she was shown and did not take up is not offered again until more
|
|
105
|
-
// arrives.
|
|
106
|
-
const { run, latest } = this.circling(now);
|
|
107
|
-
const since = Math.max(
|
|
108
|
-
latest?.created ?? 0,
|
|
109
|
-
this.life.repo.config("own-voice", {}).circledAt ?? 0,
|
|
110
|
-
);
|
|
111
|
-
const arrived = run >= 2 ? this.arrived(since, now) : new Set();
|
|
112
|
-
const fresh = [
|
|
113
|
-
...threads.map((s) => `s:${s.thread}`),
|
|
114
|
-
...notes.map((t) => `t:${t.id}`),
|
|
115
|
-
].filter((ref) => arrived.has(ref));
|
|
116
|
-
return {
|
|
117
|
-
...(run >= 2 ? { circling: { notes: run, fresh } } : {}),
|
|
118
|
-
clock: localClock(now, this.mind.timeZone()),
|
|
119
|
-
currentLife: this.mind.time.view({ session: "", now }),
|
|
120
|
-
actions: this.mind.time.lived({
|
|
121
|
-
session: "",
|
|
122
|
-
before: now,
|
|
123
|
-
since: now - 86400000,
|
|
124
|
-
}),
|
|
125
|
-
mood: { feeling: mood, energy: energyLabel, phase: phaseLabel },
|
|
126
|
-
interests: nature.interests || [],
|
|
127
|
-
self: threads.map((s) => ({
|
|
128
|
-
ref: `s:${s.thread}`,
|
|
129
|
-
kind: s.kind,
|
|
130
|
-
content: s.content,
|
|
131
|
-
emerging: s.status === "emerging",
|
|
132
|
-
origin: originSummary(conversationOrigin(this.mind.db, s.sources, now)),
|
|
133
|
-
})),
|
|
134
|
-
notes: notes.map((t) => ({
|
|
135
|
-
ref: `t:${t.id}`,
|
|
136
|
-
created: t.created,
|
|
137
|
-
kind: t.kind,
|
|
138
|
-
content: t.content,
|
|
139
|
-
origin: originSummary(
|
|
140
|
-
conversationOrigin(this.mind.db, [`t:${t.id}`], t.created),
|
|
141
|
-
),
|
|
142
|
-
})),
|
|
143
|
-
...(() => {
|
|
144
|
-
const world = this.mind.observations.recent({
|
|
145
|
-
since: now - 86400000,
|
|
146
|
-
before: now,
|
|
147
|
-
limit: 4,
|
|
148
|
-
});
|
|
149
|
-
return world.length ? { world } : {};
|
|
150
|
-
})(),
|
|
151
|
-
recentExpressions: recent.map((t) => ({
|
|
152
|
-
ref: `t:${t.id}`,
|
|
153
|
-
created: t.created,
|
|
154
|
-
content: t.content,
|
|
155
|
-
said: t.outreach_status === "sent" ? t.outreach : "",
|
|
156
|
-
where: t.outreach_status === "sent" ? t.outreach_session : "",
|
|
157
|
-
})),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
async form(now = this.life.now()) {
|
|
161
|
-
const life = this.life;
|
|
162
|
-
const nature = this.mind.nature.current(now);
|
|
163
|
-
const input = this.material(now);
|
|
164
|
-
const id = life.run("expression", "先听听自己想说什么");
|
|
165
|
-
const trace = life.repo.trace("__mind__", "expression");
|
|
166
|
-
let status = "empty";
|
|
167
|
-
let reason = "此刻没有想表达的新内容";
|
|
168
|
-
let note = null;
|
|
169
|
-
try {
|
|
170
|
-
const profile = life.profile();
|
|
171
|
-
const models = withFallback(
|
|
172
|
-
life.chat.models,
|
|
173
|
-
life.chat.fallbackFor(null, profile, trace),
|
|
174
|
-
);
|
|
175
|
-
const result = await models.call(
|
|
176
|
-
profile,
|
|
177
|
-
"expression",
|
|
178
|
-
replyPrompt(nature, prompts(life.repo), "expression"),
|
|
179
|
-
input,
|
|
180
|
-
trace,
|
|
181
|
-
);
|
|
182
|
-
if (life.closed || this.mind.nature.version() !== nature.version) {
|
|
183
|
-
status = "cancelled";
|
|
184
|
-
reason = "状态已变化,这次想法未保存";
|
|
185
|
-
} else if (result?.note && result.skip !== true) {
|
|
186
|
-
const content = text(result.note, 600);
|
|
187
|
-
const words = (Array.isArray(result.words) ? result.words : [])
|
|
188
|
-
.filter((s) => typeof s === "string" && s.trim())
|
|
189
|
-
.slice(0, 3)
|
|
190
|
-
.map((s) => text(s, 180));
|
|
191
|
-
const sources = evidence(result.sources);
|
|
192
|
-
const valid = new Set(
|
|
193
|
-
[
|
|
194
|
-
...input.self,
|
|
195
|
-
...input.notes,
|
|
196
|
-
...input.actions,
|
|
197
|
-
...(input.world || []),
|
|
198
|
-
].map((s) => s.ref),
|
|
199
|
-
);
|
|
200
|
-
if (
|
|
201
|
-
!content ||
|
|
202
|
-
hasCredential([content, ...words].join("\n")) ||
|
|
203
|
-
sources.some((s) => !valid.has(s))
|
|
204
|
-
)
|
|
205
|
-
throw Error("自己的念头含有无效来源或敏感内容,未保存");
|
|
206
|
-
// Several notes in a row already went round without taking anything
|
|
207
|
-
// new up. Another one that still does not is not kept.
|
|
208
|
-
const drifting =
|
|
209
|
-
(input.circling?.notes ?? 0) >= STREAK &&
|
|
210
|
-
!sources.some((ref) => input.circling.fresh.includes(ref));
|
|
211
|
-
const repeated =
|
|
212
|
-
drifting ||
|
|
213
|
-
input.recentExpressions.some((t) =>
|
|
214
|
-
sameRecentTheme(t.content, content),
|
|
215
|
-
);
|
|
216
|
-
const surfaceEcho = input.recentExpressions.some(
|
|
217
|
-
(t) => t.said && sameRecentTheme(t.said, words.join("\n")),
|
|
218
|
-
);
|
|
219
|
-
const recentlySaid = input.recentExpressions.filter(
|
|
220
|
-
(t) => t.said && now - t.created < 12 * 3600000,
|
|
221
|
-
);
|
|
222
|
-
const linked = recentlySaid.some((t) => sources.includes(t.ref));
|
|
223
|
-
let echoed = surfaceEcho;
|
|
224
|
-
if (
|
|
225
|
-
!repeated &&
|
|
226
|
-
!echoed &&
|
|
227
|
-
result.share === true &&
|
|
228
|
-
recentlySaid.length
|
|
229
|
-
) {
|
|
230
|
-
try {
|
|
231
|
-
const verdict = await models.call(
|
|
232
|
-
profile,
|
|
233
|
-
"expression_novelty",
|
|
234
|
-
'比较一个人先前已分享的念头和现在想说的话。只判断核心话题与动机是否仍是同一件事;换词、补一个没有外部新经历的小步骤、换一个群分享,都算同一主题。真正换了关注对象或出现新的经历,才算新内容。不判断好坏。只输出 JSON:{"sameTheme":true或false,"reason":"一句依据"}。',
|
|
235
|
-
{
|
|
236
|
-
candidate: { note: content, words, sources },
|
|
237
|
-
recentlySaid: recentlySaid.slice(0, 6),
|
|
238
|
-
},
|
|
239
|
-
trace,
|
|
240
|
-
);
|
|
241
|
-
echoed =
|
|
242
|
-
verdict?.sameTheme === true ||
|
|
243
|
-
(linked && verdict?.sameTheme !== false);
|
|
244
|
-
} catch {
|
|
245
|
-
// If the related thought cannot be checked, keep its new turn of
|
|
246
|
-
// thought in her journal instead of broadcasting it again.
|
|
247
|
-
echoed = linked;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (drifting) {
|
|
251
|
-
life.repo.saveConfig("own-voice", {
|
|
252
|
-
...life.repo.config("own-voice", {}),
|
|
253
|
-
circledAt: now,
|
|
254
|
-
});
|
|
255
|
-
reason =
|
|
256
|
-
"连着几条念头都只在接自己上一条,这一条也没接住新的东西,不留";
|
|
257
|
-
} else if (repeated) reason = "这个念头已经留过,不重复制造新的愿望";
|
|
258
|
-
else {
|
|
259
|
-
const noteId = this.mind.thoughts.add({
|
|
260
|
-
kind: "expression",
|
|
261
|
-
content,
|
|
262
|
-
sources,
|
|
263
|
-
importance: 0.5,
|
|
264
|
-
runId: id,
|
|
265
|
-
time: now,
|
|
266
|
-
});
|
|
267
|
-
// A new wording of a wish she already holds writes back to that
|
|
268
|
-
// thread. It does not invent a second livingFor.
|
|
269
|
-
const match = this.mind.self
|
|
270
|
-
.latest(now)
|
|
271
|
-
.find(
|
|
272
|
-
(row) =>
|
|
273
|
-
row.status !== "closed" &&
|
|
274
|
-
!/^(?:我)?(?:会|愿意|想)?(?:建议|劝|提醒|鼓励)/.test(content) &&
|
|
275
|
-
sameSelf(row.content, content),
|
|
276
|
-
);
|
|
277
|
-
if (match)
|
|
278
|
-
this.mind.self.propose(
|
|
279
|
-
{
|
|
280
|
-
thread: match.thread,
|
|
281
|
-
content: text(content, 80),
|
|
282
|
-
sources: [`t:${noteId}`],
|
|
283
|
-
},
|
|
284
|
-
{ origin: "expression", time: now },
|
|
285
|
-
);
|
|
286
|
-
note = {
|
|
287
|
-
id: noteId,
|
|
288
|
-
// An idea may grow privately after it has been spoken. Without a
|
|
289
|
-
// new encounter, its next paraphrase should not tour other rooms.
|
|
290
|
-
share: result.share === true && words.length > 0 && !echoed,
|
|
291
|
-
words,
|
|
292
|
-
reason: text(result.reason, 300) || "想把这个念头说出来",
|
|
293
|
-
audience: ["private", "group"].includes(result.audience)
|
|
294
|
-
? result.audience
|
|
295
|
-
: "either",
|
|
296
|
-
};
|
|
297
|
-
status = "written";
|
|
298
|
-
reason = echoed
|
|
299
|
-
? "同一念头还在往前长,先留给自己,不换个地方重说"
|
|
300
|
-
: note.share
|
|
301
|
-
? "留下一个自己想分享的念头"
|
|
302
|
-
: "这句话先留给自己";
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} catch (error) {
|
|
306
|
-
status = "error";
|
|
307
|
-
reason = String(error.message).slice(0, 300);
|
|
308
|
-
trace.error = error.message;
|
|
309
|
-
} finally {
|
|
310
|
-
life.end(id, status, reason, trace, {
|
|
311
|
-
freshMessages: 0,
|
|
312
|
-
...(note ? { thought: note.id } : {}),
|
|
313
|
-
});
|
|
314
|
-
life.repo.finish(trace, status);
|
|
315
|
-
}
|
|
316
|
-
return { status, reason, note };
|
|
317
|
-
}
|
|
318
|
-
}
|
|
1
|
+
import { localClock } from "../core/conversation-cues.js";
|
|
2
|
+
import { replyPrompt, prompts } from "../core/persona-manager.js";
|
|
3
|
+
import { withFallback } from "../core/model-manager.js";
|
|
4
|
+
import { HOUR, evidence, hasCredential, text } from "./util.js";
|
|
5
|
+
import { isPrivateSession } from "./memory.js";
|
|
6
|
+
import { sameRecentTheme } from "./novelty.js";
|
|
7
|
+
import { ownLife, sameSelf } from "./salience.js";
|
|
8
|
+
import { conversationOrigin, originSummary } from "./conversation-origin.js";
|
|
9
|
+
|
|
10
|
+
// After this many notes in a row that only take up her own last note, another
|
|
11
|
+
// one has to pick up something new or it is not kept.
|
|
12
|
+
const STREAK = 3;
|
|
13
|
+
|
|
14
|
+
// Form a thought before choosing an audience. No transcript, unanswered
|
|
15
|
+
// question or destination is supplied here: those belong to delivery, not
|
|
16
|
+
// to the origin of what she wants to say.
|
|
17
|
+
export class OwnVoice {
|
|
18
|
+
constructor(life) {
|
|
19
|
+
this.life = life;
|
|
20
|
+
this.mind = life.mind;
|
|
21
|
+
}
|
|
22
|
+
// What has come into her life since `after` that this loop did not write
|
|
23
|
+
// itself: thoughts from solitude, night memory or a review, and threads
|
|
24
|
+
// those revised. Her own expression write-backs are not new.
|
|
25
|
+
arrived(after, until) {
|
|
26
|
+
const thoughts = this.mind.db
|
|
27
|
+
.prepare(
|
|
28
|
+
"SELECT id FROM mind_thoughts WHERE created>? AND created<=? AND kind!='expression' AND hidden=0",
|
|
29
|
+
)
|
|
30
|
+
.all(after, until)
|
|
31
|
+
.map((row) => `t:${row.id}`);
|
|
32
|
+
const threads = this.mind.db
|
|
33
|
+
.prepare(
|
|
34
|
+
"SELECT DISTINCT thread FROM mind_self WHERE created>? AND created<=? AND origin!='expression'",
|
|
35
|
+
)
|
|
36
|
+
.all(after, until)
|
|
37
|
+
.map((row) => `s:${row.thread}`);
|
|
38
|
+
return new Set([...thoughts, ...threads]);
|
|
39
|
+
}
|
|
40
|
+
// How many of her newest notes only took up the one before. A note is
|
|
41
|
+
// anchored when it cites something that arrived after the previous note.
|
|
42
|
+
circling(now) {
|
|
43
|
+
const notes = this.mind.thoughts
|
|
44
|
+
.list({ before: now + 1, limit: 40, hidden: false })
|
|
45
|
+
.filter((t) => t.kind === "expression");
|
|
46
|
+
let run = 0;
|
|
47
|
+
for (let i = 0; i + 1 < notes.length; i++) {
|
|
48
|
+
const note = notes[i];
|
|
49
|
+
const previous = notes[i + 1];
|
|
50
|
+
if (note.created - previous.created > 24 * HOUR) break;
|
|
51
|
+
const arrived = this.arrived(previous.created, note.created);
|
|
52
|
+
if (note.sources.some((ref) => arrived.has(ref))) break;
|
|
53
|
+
run++;
|
|
54
|
+
}
|
|
55
|
+
return { run, latest: notes[0] || null };
|
|
56
|
+
}
|
|
57
|
+
// Going round again with nothing new is not a reason to ask her: she has
|
|
58
|
+
// written several notes that only continue her last one, and nothing she
|
|
59
|
+
// could take up has come in since.
|
|
60
|
+
resting(now) {
|
|
61
|
+
const { run } = this.circling(now);
|
|
62
|
+
if (run < STREAK) return null;
|
|
63
|
+
const { circling } = this.material(now);
|
|
64
|
+
if (circling?.fresh.length) return null;
|
|
65
|
+
return `连着 ${run} 条念头都只在接自己上一条,还没有新的东西可接`;
|
|
66
|
+
}
|
|
67
|
+
material(now) {
|
|
68
|
+
const nature = this.mind.nature.current(now);
|
|
69
|
+
const threads = this.mind.self
|
|
70
|
+
.active({ before: now, now, limit: 40 })
|
|
71
|
+
.filter((s) => this.mind.meetings.stays(s, ""))
|
|
72
|
+
.filter((s) =>
|
|
73
|
+
["interest", "curiosity", "view", "intention"].includes(s.kind),
|
|
74
|
+
)
|
|
75
|
+
// Older summaries sometimes called advice or a duty to someone else
|
|
76
|
+
// an intention. Keep the record, but do not treat it as a life she
|
|
77
|
+
// is living.
|
|
78
|
+
.filter((s) => s.kind !== "intention" || ownLife(s.content))
|
|
79
|
+
.slice(0, 12);
|
|
80
|
+
const notes = this.mind.thoughts
|
|
81
|
+
.open({ now, limit: 40 })
|
|
82
|
+
.filter((t) =>
|
|
83
|
+
this.mind.meetings.stays(
|
|
84
|
+
{
|
|
85
|
+
...t,
|
|
86
|
+
session_id:
|
|
87
|
+
t.kind === "expression" && !t.sources.length
|
|
88
|
+
? ""
|
|
89
|
+
: t.sessions.find(isPrivateSession) || "",
|
|
90
|
+
},
|
|
91
|
+
"",
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
.sort((a, b) => b.created - a.created)
|
|
95
|
+
.slice(0, 6);
|
|
96
|
+
const recent = this.mind.thoughts
|
|
97
|
+
.list({ before: now + 1, limit: 80 })
|
|
98
|
+
.filter((t) => t.kind === "expression")
|
|
99
|
+
.slice(0, 12);
|
|
100
|
+
const { mood, energyLabel, phaseLabel } = this.mind.affect.state(now, {
|
|
101
|
+
nature,
|
|
102
|
+
});
|
|
103
|
+
// Only what she can see counts as something she could take up. A note
|
|
104
|
+
// she was shown and did not take up is not offered again until more
|
|
105
|
+
// arrives.
|
|
106
|
+
const { run, latest } = this.circling(now);
|
|
107
|
+
const since = Math.max(
|
|
108
|
+
latest?.created ?? 0,
|
|
109
|
+
this.life.repo.config("own-voice", {}).circledAt ?? 0,
|
|
110
|
+
);
|
|
111
|
+
const arrived = run >= 2 ? this.arrived(since, now) : new Set();
|
|
112
|
+
const fresh = [
|
|
113
|
+
...threads.map((s) => `s:${s.thread}`),
|
|
114
|
+
...notes.map((t) => `t:${t.id}`),
|
|
115
|
+
].filter((ref) => arrived.has(ref));
|
|
116
|
+
return {
|
|
117
|
+
...(run >= 2 ? { circling: { notes: run, fresh } } : {}),
|
|
118
|
+
clock: localClock(now, this.mind.timeZone()),
|
|
119
|
+
currentLife: this.mind.time.view({ session: "", now }),
|
|
120
|
+
actions: this.mind.time.lived({
|
|
121
|
+
session: "",
|
|
122
|
+
before: now,
|
|
123
|
+
since: now - 86400000,
|
|
124
|
+
}),
|
|
125
|
+
mood: { feeling: mood, energy: energyLabel, phase: phaseLabel },
|
|
126
|
+
interests: nature.interests || [],
|
|
127
|
+
self: threads.map((s) => ({
|
|
128
|
+
ref: `s:${s.thread}`,
|
|
129
|
+
kind: s.kind,
|
|
130
|
+
content: s.content,
|
|
131
|
+
emerging: s.status === "emerging",
|
|
132
|
+
origin: originSummary(conversationOrigin(this.mind.db, s.sources, now)),
|
|
133
|
+
})),
|
|
134
|
+
notes: notes.map((t) => ({
|
|
135
|
+
ref: `t:${t.id}`,
|
|
136
|
+
created: t.created,
|
|
137
|
+
kind: t.kind,
|
|
138
|
+
content: t.content,
|
|
139
|
+
origin: originSummary(
|
|
140
|
+
conversationOrigin(this.mind.db, [`t:${t.id}`], t.created),
|
|
141
|
+
),
|
|
142
|
+
})),
|
|
143
|
+
...(() => {
|
|
144
|
+
const world = this.mind.observations.recent({
|
|
145
|
+
since: now - 86400000,
|
|
146
|
+
before: now,
|
|
147
|
+
limit: 4,
|
|
148
|
+
});
|
|
149
|
+
return world.length ? { world } : {};
|
|
150
|
+
})(),
|
|
151
|
+
recentExpressions: recent.map((t) => ({
|
|
152
|
+
ref: `t:${t.id}`,
|
|
153
|
+
created: t.created,
|
|
154
|
+
content: t.content,
|
|
155
|
+
said: t.outreach_status === "sent" ? t.outreach : "",
|
|
156
|
+
where: t.outreach_status === "sent" ? t.outreach_session : "",
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async form(now = this.life.now()) {
|
|
161
|
+
const life = this.life;
|
|
162
|
+
const nature = this.mind.nature.current(now);
|
|
163
|
+
const input = this.material(now);
|
|
164
|
+
const id = life.run("expression", "先听听自己想说什么");
|
|
165
|
+
const trace = life.repo.trace("__mind__", "expression");
|
|
166
|
+
let status = "empty";
|
|
167
|
+
let reason = "此刻没有想表达的新内容";
|
|
168
|
+
let note = null;
|
|
169
|
+
try {
|
|
170
|
+
const profile = life.profile();
|
|
171
|
+
const models = withFallback(
|
|
172
|
+
life.chat.models,
|
|
173
|
+
life.chat.fallbackFor(null, profile, trace),
|
|
174
|
+
);
|
|
175
|
+
const result = await models.call(
|
|
176
|
+
profile,
|
|
177
|
+
"expression",
|
|
178
|
+
replyPrompt(nature, prompts(life.repo), "expression"),
|
|
179
|
+
input,
|
|
180
|
+
trace,
|
|
181
|
+
);
|
|
182
|
+
if (life.closed || this.mind.nature.version() !== nature.version) {
|
|
183
|
+
status = "cancelled";
|
|
184
|
+
reason = "状态已变化,这次想法未保存";
|
|
185
|
+
} else if (result?.note && result.skip !== true) {
|
|
186
|
+
const content = text(result.note, 600);
|
|
187
|
+
const words = (Array.isArray(result.words) ? result.words : [])
|
|
188
|
+
.filter((s) => typeof s === "string" && s.trim())
|
|
189
|
+
.slice(0, 3)
|
|
190
|
+
.map((s) => text(s, 180));
|
|
191
|
+
const sources = evidence(result.sources);
|
|
192
|
+
const valid = new Set(
|
|
193
|
+
[
|
|
194
|
+
...input.self,
|
|
195
|
+
...input.notes,
|
|
196
|
+
...input.actions,
|
|
197
|
+
...(input.world || []),
|
|
198
|
+
].map((s) => s.ref),
|
|
199
|
+
);
|
|
200
|
+
if (
|
|
201
|
+
!content ||
|
|
202
|
+
hasCredential([content, ...words].join("\n")) ||
|
|
203
|
+
sources.some((s) => !valid.has(s))
|
|
204
|
+
)
|
|
205
|
+
throw Error("自己的念头含有无效来源或敏感内容,未保存");
|
|
206
|
+
// Several notes in a row already went round without taking anything
|
|
207
|
+
// new up. Another one that still does not is not kept.
|
|
208
|
+
const drifting =
|
|
209
|
+
(input.circling?.notes ?? 0) >= STREAK &&
|
|
210
|
+
!sources.some((ref) => input.circling.fresh.includes(ref));
|
|
211
|
+
const repeated =
|
|
212
|
+
drifting ||
|
|
213
|
+
input.recentExpressions.some((t) =>
|
|
214
|
+
sameRecentTheme(t.content, content),
|
|
215
|
+
);
|
|
216
|
+
const surfaceEcho = input.recentExpressions.some(
|
|
217
|
+
(t) => t.said && sameRecentTheme(t.said, words.join("\n")),
|
|
218
|
+
);
|
|
219
|
+
const recentlySaid = input.recentExpressions.filter(
|
|
220
|
+
(t) => t.said && now - t.created < 12 * 3600000,
|
|
221
|
+
);
|
|
222
|
+
const linked = recentlySaid.some((t) => sources.includes(t.ref));
|
|
223
|
+
let echoed = surfaceEcho;
|
|
224
|
+
if (
|
|
225
|
+
!repeated &&
|
|
226
|
+
!echoed &&
|
|
227
|
+
result.share === true &&
|
|
228
|
+
recentlySaid.length
|
|
229
|
+
) {
|
|
230
|
+
try {
|
|
231
|
+
const verdict = await models.call(
|
|
232
|
+
profile,
|
|
233
|
+
"expression_novelty",
|
|
234
|
+
'比较一个人先前已分享的念头和现在想说的话。只判断核心话题与动机是否仍是同一件事;换词、补一个没有外部新经历的小步骤、换一个群分享,都算同一主题。真正换了关注对象或出现新的经历,才算新内容。不判断好坏。只输出 JSON:{"sameTheme":true或false,"reason":"一句依据"}。',
|
|
235
|
+
{
|
|
236
|
+
candidate: { note: content, words, sources },
|
|
237
|
+
recentlySaid: recentlySaid.slice(0, 6),
|
|
238
|
+
},
|
|
239
|
+
trace,
|
|
240
|
+
);
|
|
241
|
+
echoed =
|
|
242
|
+
verdict?.sameTheme === true ||
|
|
243
|
+
(linked && verdict?.sameTheme !== false);
|
|
244
|
+
} catch {
|
|
245
|
+
// If the related thought cannot be checked, keep its new turn of
|
|
246
|
+
// thought in her journal instead of broadcasting it again.
|
|
247
|
+
echoed = linked;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (drifting) {
|
|
251
|
+
life.repo.saveConfig("own-voice", {
|
|
252
|
+
...life.repo.config("own-voice", {}),
|
|
253
|
+
circledAt: now,
|
|
254
|
+
});
|
|
255
|
+
reason =
|
|
256
|
+
"连着几条念头都只在接自己上一条,这一条也没接住新的东西,不留";
|
|
257
|
+
} else if (repeated) reason = "这个念头已经留过,不重复制造新的愿望";
|
|
258
|
+
else {
|
|
259
|
+
const noteId = this.mind.thoughts.add({
|
|
260
|
+
kind: "expression",
|
|
261
|
+
content,
|
|
262
|
+
sources,
|
|
263
|
+
importance: 0.5,
|
|
264
|
+
runId: id,
|
|
265
|
+
time: now,
|
|
266
|
+
});
|
|
267
|
+
// A new wording of a wish she already holds writes back to that
|
|
268
|
+
// thread. It does not invent a second livingFor.
|
|
269
|
+
const match = this.mind.self
|
|
270
|
+
.latest(now)
|
|
271
|
+
.find(
|
|
272
|
+
(row) =>
|
|
273
|
+
row.status !== "closed" &&
|
|
274
|
+
!/^(?:我)?(?:会|愿意|想)?(?:建议|劝|提醒|鼓励)/.test(content) &&
|
|
275
|
+
sameSelf(row.content, content),
|
|
276
|
+
);
|
|
277
|
+
if (match)
|
|
278
|
+
this.mind.self.propose(
|
|
279
|
+
{
|
|
280
|
+
thread: match.thread,
|
|
281
|
+
content: text(content, 80),
|
|
282
|
+
sources: [`t:${noteId}`],
|
|
283
|
+
},
|
|
284
|
+
{ origin: "expression", time: now },
|
|
285
|
+
);
|
|
286
|
+
note = {
|
|
287
|
+
id: noteId,
|
|
288
|
+
// An idea may grow privately after it has been spoken. Without a
|
|
289
|
+
// new encounter, its next paraphrase should not tour other rooms.
|
|
290
|
+
share: result.share === true && words.length > 0 && !echoed,
|
|
291
|
+
words,
|
|
292
|
+
reason: text(result.reason, 300) || "想把这个念头说出来",
|
|
293
|
+
audience: ["private", "group"].includes(result.audience)
|
|
294
|
+
? result.audience
|
|
295
|
+
: "either",
|
|
296
|
+
};
|
|
297
|
+
status = "written";
|
|
298
|
+
reason = echoed
|
|
299
|
+
? "同一念头还在往前长,先留给自己,不换个地方重说"
|
|
300
|
+
: note.share
|
|
301
|
+
? "留下一个自己想分享的念头"
|
|
302
|
+
: "这句话先留给自己";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
status = "error";
|
|
307
|
+
reason = String(error.message).slice(0, 300);
|
|
308
|
+
trace.error = error.message;
|
|
309
|
+
} finally {
|
|
310
|
+
life.end(id, status, reason, trace, {
|
|
311
|
+
freshMessages: 0,
|
|
312
|
+
...(note ? { thought: note.id } : {}),
|
|
313
|
+
});
|
|
314
|
+
life.repo.finish(trace, status);
|
|
315
|
+
}
|
|
316
|
+
return { status, reason, note };
|
|
317
|
+
}
|
|
318
|
+
}
|