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.
Files changed (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -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
+ }