luckytri 1.0.0 → 1.0.1

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