luckytri 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/.github/SECURITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +250 -0
  4. package/README.md +250 -0
  5. package/bin/luckytri.js +107 -0
  6. package/docs/brand/banner.png +0 -0
  7. package/docs/brand/palette.png +0 -0
  8. package/docs/brand/portrait.png +0 -0
  9. package/docs/en/CHANGELOG.md +113 -0
  10. package/docs/en/architecture.md +152 -0
  11. package/docs/en/chat-style.md +62 -0
  12. package/docs/en/connect.md +63 -0
  13. package/docs/en/guide.md +216 -0
  14. package/docs/en/her-life.md +189 -0
  15. package/docs/en/npm-release.md +25 -0
  16. package/docs/en/plugins.md +82 -0
  17. package/docs/en/recovery.md +28 -0
  18. package/docs/en/time.md +71 -0
  19. package/docs/en/webui.md +73 -0
  20. package/docs/zh/CHANGELOG.md +113 -0
  21. package/docs/zh/architecture.md +152 -0
  22. package/docs/zh/chat-style.md +62 -0
  23. package/docs/zh/connect.md +63 -0
  24. package/docs/zh/guide.md +216 -0
  25. package/docs/zh/her-life.md +189 -0
  26. package/docs/zh/npm-release.md +33 -0
  27. package/docs/zh/plugins.md +82 -0
  28. package/docs/zh/recovery.md +28 -0
  29. package/docs/zh/time.md +71 -0
  30. package/docs/zh/webui.md +73 -0
  31. package/package.json +108 -0
  32. package/plugins/sdk/index.d.ts +155 -0
  33. package/plugins/sdk/testing.js +58 -0
  34. package/plugins/weather/index.js +82 -0
  35. package/plugins/weather/luckytri-plugin.json +29 -0
  36. package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
  37. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  38. package/public/app/assets/Empty-CpP5kQHH.js +2 -0
  39. package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
  40. package/public/app/assets/Field-D4gY-INU.js +1 -0
  41. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  42. package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
  43. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  44. package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
  45. package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
  46. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  47. package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
  48. package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
  49. package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
  50. package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
  51. package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
  52. package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
  53. package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
  54. package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
  55. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  56. package/public/app/assets/TimePage-Cw44inM4.js +3 -0
  57. package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
  58. package/public/app/assets/index-BheRW-1t.js +4 -0
  59. package/public/app/assets/index-YDz1ree1.css +1 -0
  60. package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
  61. package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
  62. package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
  63. package/public/app/assets/media-BzPDBm7e.js +1 -0
  64. package/public/app/index.html +27 -0
  65. package/public/guide.en.html +1053 -0
  66. package/public/guide.html +893 -0
  67. package/public/plugin-kit/v1/bridge.js +30 -0
  68. package/public/plugin-kit/v1/kit.css +14 -0
  69. package/public/tutorial.en.md +214 -0
  70. package/public/tutorial.md +214 -0
  71. package/scripts/backup.js +311 -0
  72. package/scripts/browser.js +41 -0
  73. package/scripts/launch.js +94 -0
  74. package/scripts/plugin.js +150 -0
  75. package/scripts/proxy-config.js +167 -0
  76. package/scripts/recover.js +26 -0
  77. package/scripts/runtime.js +25 -0
  78. package/scripts/service.js +47 -0
  79. package/scripts/setup.js +50 -0
  80. package/scripts/start-server.js +41 -0
  81. package/scripts/stop.js +36 -0
  82. package/server/app.js +96 -0
  83. package/server/channels/adapters.js +27 -0
  84. package/server/channels/contract.js +57 -0
  85. package/server/channels/hub.js +165 -0
  86. package/server/channels/index.js +25 -0
  87. package/server/channels/onebot/adapter.js +288 -0
  88. package/server/channels/onebot/index.js +336 -0
  89. package/server/channels/onebot/token.js +3 -0
  90. package/server/channels/qqbot/adapter.js +253 -0
  91. package/server/channels/qqbot/api.js +199 -0
  92. package/server/channels/qqbot/credentials.js +11 -0
  93. package/server/channels/qqbot/gateway.js +274 -0
  94. package/server/channels/qqbot/index.js +389 -0
  95. package/server/channels/segments.js +40 -0
  96. package/server/channels/session-key.js +153 -0
  97. package/server/channels/state.js +42 -0
  98. package/server/core/api.js +564 -0
  99. package/server/core/capabilities.js +208 -0
  100. package/server/core/context-builder.js +288 -0
  101. package/server/core/context-compactor.js +454 -0
  102. package/server/core/conversation-cues.js +325 -0
  103. package/server/core/conversation-manager.js +87 -0
  104. package/server/core/events.js +60 -0
  105. package/server/core/feedback.js +8 -0
  106. package/server/core/group-style.js +72 -0
  107. package/server/core/image-preprocessor.js +208 -0
  108. package/server/core/initiative-context.js +66 -0
  109. package/server/core/input-budget.js +32 -0
  110. package/server/core/local-demo.js +98 -0
  111. package/server/core/message-manager.js +118 -0
  112. package/server/core/message-scheduler.js +78 -0
  113. package/server/core/model-json.js +190 -0
  114. package/server/core/model-manager.js +1213 -0
  115. package/server/core/model-presets.js +900 -0
  116. package/server/core/name-call.js +29 -0
  117. package/server/core/net-guard.js +49 -0
  118. package/server/core/network.js +157 -0
  119. package/server/core/orchestrator.js +569 -0
  120. package/server/core/perceivers.js +91 -0
  121. package/server/core/person-name.js +22 -0
  122. package/server/core/persona-manager.js +243 -0
  123. package/server/core/reply-delivery.js +393 -0
  124. package/server/core/reply-target-resolver.js +50 -0
  125. package/server/core/repository.js +293 -0
  126. package/server/core/response-generator.js +50 -0
  127. package/server/core/response-validator.js +508 -0
  128. package/server/core/retired-prompts.js +51 -0
  129. package/server/core/sessions.js +109 -0
  130. package/server/core/speaker-names.js +92 -0
  131. package/server/core/topic-tracker.js +65 -0
  132. package/server/core/turn-processor.js +630 -0
  133. package/server/core/turn.js +236 -0
  134. package/server/core/vision-manager.js +633 -0
  135. package/server/http.js +18 -0
  136. package/server/i18n/decisions.js +206 -0
  137. package/server/i18n/errors.js +497 -0
  138. package/server/i18n/index.js +144 -0
  139. package/server/i18n/labels.js +653 -0
  140. package/server/i18n/models.js +111 -0
  141. package/server/index.js +102 -0
  142. package/server/knowledge/api.js +342 -0
  143. package/server/knowledge/embedding-profile.js +114 -0
  144. package/server/knowledge/index.js +13 -0
  145. package/server/knowledge/manager.js +386 -0
  146. package/server/knowledge/retrieval.js +93 -0
  147. package/server/knowledge/schema.js +154 -0
  148. package/server/knowledge/vector-index.js +90 -0
  149. package/server/mind/affect.js +265 -0
  150. package/server/mind/anticipations.js +507 -0
  151. package/server/mind/api.js +703 -0
  152. package/server/mind/attention.js +200 -0
  153. package/server/mind/bonds.js +558 -0
  154. package/server/mind/budget.js +152 -0
  155. package/server/mind/clock.js +83 -0
  156. package/server/mind/context-selection.js +61 -0
  157. package/server/mind/continuity.js +562 -0
  158. package/server/mind/conversation-origin.js +175 -0
  159. package/server/mind/day-planner.js +324 -0
  160. package/server/mind/days.js +210 -0
  161. package/server/mind/evidence-read.js +45 -0
  162. package/server/mind/evidence.js +148 -0
  163. package/server/mind/faces.js +138 -0
  164. package/server/mind/guard.js +60 -0
  165. package/server/mind/index.js +445 -0
  166. package/server/mind/initiative.js +333 -0
  167. package/server/mind/life/activities.js +21 -0
  168. package/server/mind/life/context.js +309 -0
  169. package/server/mind/life/diary.js +786 -0
  170. package/server/mind/life/growth.js +252 -0
  171. package/server/mind/life/index.js +1049 -0
  172. package/server/mind/life/presence.js +305 -0
  173. package/server/mind/meetings.js +1069 -0
  174. package/server/mind/memory-policy.js +48 -0
  175. package/server/mind/memory.js +888 -0
  176. package/server/mind/nature.js +262 -0
  177. package/server/mind/novelty.js +19 -0
  178. package/server/mind/observations.js +84 -0
  179. package/server/mind/own-day.js +324 -0
  180. package/server/mind/own-voice.js +318 -0
  181. package/server/mind/periods.js +118 -0
  182. package/server/mind/reading.js +120 -0
  183. package/server/mind/salience.js +201 -0
  184. package/server/mind/schema.js +562 -0
  185. package/server/mind/self.js +484 -0
  186. package/server/mind/senses.js +53 -0
  187. package/server/mind/thoughts.js +274 -0
  188. package/server/mind/time/activity-clock.js +196 -0
  189. package/server/mind/time/agenda.js +242 -0
  190. package/server/mind/time/api.js +200 -0
  191. package/server/mind/time/attention.js +34 -0
  192. package/server/mind/time/availability.js +69 -0
  193. package/server/mind/time/commitment-review.js +198 -0
  194. package/server/mind/time/delivery-links.js +130 -0
  195. package/server/mind/time/executor.js +512 -0
  196. package/server/mind/time/experiences.js +128 -0
  197. package/server/mind/time/games.js +562 -0
  198. package/server/mind/time/index.js +739 -0
  199. package/server/mind/time/intent.js +166 -0
  200. package/server/mind/time/kinds.js +171 -0
  201. package/server/mind/time/model-material.js +77 -0
  202. package/server/mind/time/plugin-activity.js +207 -0
  203. package/server/mind/time/presentation.js +51 -0
  204. package/server/mind/time/schedule.js +83 -0
  205. package/server/mind/time/schema.js +84 -0
  206. package/server/mind/time/search.js +383 -0
  207. package/server/mind/time/sharing.js +472 -0
  208. package/server/mind/time/task-links.js +311 -0
  209. package/server/mind/time/tasks.js +587 -0
  210. package/server/mind/time/works.js +432 -0
  211. package/server/mind/traits.js +191 -0
  212. package/server/mind/util.js +110 -0
  213. package/server/mind/view.js +273 -0
  214. package/server/paths.js +102 -0
  215. package/server/plugins/api.js +204 -0
  216. package/server/plugins/host.js +967 -0
  217. package/server/plugins/install.js +295 -0
  218. package/server/plugins/manifest.js +201 -0
  219. package/server/plugins/permissions.js +42 -0
  220. package/server/plugins/runner/guard.mjs +36 -0
  221. package/server/plugins/runner/worker.mjs +250 -0
  222. package/server/plugins/schema.js +91 -0
  223. package/server/plugins/zip.js +112 -0
  224. package/server/startup-lock.js +118 -0
  225. package/server/storage/archive.js +241 -0
  226. package/server/storage/backup-cleanup-worker.js +8 -0
  227. package/server/storage/backup-cleanup.js +414 -0
  228. package/server/storage/backup-lock.js +76 -0
  229. package/server/storage/backup-scheduler.js +131 -0
  230. package/server/storage/store.js +199 -0
  231. package/server/studio/display-names.js +138 -0
  232. package/server/studio/management.js +102 -0
  233. package/server/studio/readiness.js +190 -0
  234. package/server/studio/settings.js +236 -0
  235. package/server/version.js +10 -0
@@ -0,0 +1,262 @@
1
+ import { localClock } from "../core/conversation-cues.js";
2
+ import { zonedTime } from "./util.js";
3
+
4
+ // Nature is the seed she is born with. It is the only part of her the owner
5
+ // writes; everything else grows from experience.
6
+ export const PREVIOUS_NATURE_SEED = {
7
+ name: "LuckyTri",
8
+ gender: "female",
9
+ base: "可爱、乐观、情绪稳定,偶尔对事情轻轻吐槽,不挖苦群友。像熟悉的群友一样说话,先接住情绪,不急着给建议。",
10
+ interests: [],
11
+ forbidden: [],
12
+ humor: 25,
13
+ sarcasm: 5,
14
+ warmth: 65,
15
+ activity: 40,
16
+ initiative: 25,
17
+ length: "短句为主",
18
+ boundaries: "平等、尊重;不冒认别人说的话,不编造亲历",
19
+ bottomLines: [
20
+ "别人明确要我保密的事,不在别的地方说出口。",
21
+ "有人表达真实的危机时,不因为自己的情绪而沉默。",
22
+ "被直接问到身份时如实回答,不编造没发生过的经历。",
23
+ "不索取陪伴,不制造亏欠。",
24
+ ],
25
+ rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
26
+ };
27
+ // Earlier shipped wording, before the sentence above. Still an unedited seed.
28
+ const SHIPPED_BASES = new Set([
29
+ PREVIOUS_NATURE_SEED.base,
30
+ "可爱、乐观、情绪稳定,有一点吐槽欲。像熟悉的群友一样说话,先接住情绪,不急着给建议。",
31
+ ]);
32
+ const LIVED_AS_PERSON_BASE =
33
+ "温暖,有一点自己的脾气和好奇。说话像已经在过日子的人:在意是自己的选择,先接住眼前的事,也留着自己的小事。";
34
+ function uneditedLivedSeed(value) {
35
+ if (
36
+ !value ||
37
+ value.name !== "LuckyTri" ||
38
+ value.base !== LIVED_AS_PERSON_BASE
39
+ )
40
+ return false;
41
+ return NATURE_FIELDS.every(
42
+ (key) =>
43
+ key === "name" ||
44
+ key === "base" ||
45
+ sameNatureField(value[key], NATURE_DEFAULTS[key]),
46
+ );
47
+ }
48
+ export const NATURE_DEFAULTS = {
49
+ name: "LuckyTri",
50
+ gender: "female",
51
+ base: "温暖,有一点自己的脾气和好奇。在意是自己的选择:先接住眼前的事,也留着自己的小事。不靠模仿来证明自己。",
52
+ interests: [],
53
+ forbidden: [],
54
+ humor: 25,
55
+ sarcasm: 5,
56
+ warmth: 65,
57
+ activity: 40,
58
+ initiative: 25,
59
+ length: "短句为主",
60
+ boundaries: "平等、尊重;不冒认别人说的话,不编造亲历",
61
+ bottomLines: [
62
+ "别人明确要我保密的事,不在别的地方说出口。",
63
+ "有人表达真实的危机时,不因为自己的情绪而沉默。",
64
+ "被直接问到是谁时,回答自己是 LuckyTri,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。",
65
+ "可以因为自己想而惦记某个人,不把对方留下来当成对方的责任,不靠愧疚留人。",
66
+ ],
67
+ rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
68
+ };
69
+
70
+ export const NATURE_FIELDS = Object.keys(NATURE_DEFAULTS);
71
+ function sameNatureField(a, b) {
72
+ return JSON.stringify(a) === JSON.stringify(b);
73
+ }
74
+ // Version 1 that was never edited still carries a shipped seed. Reading it
75
+ // uses the current seed and does not spend one of the two edits.
76
+ export function uneditedNatureSeed(value) {
77
+ if (!value || typeof value !== "object") return false;
78
+ const name = value.name === "LuckyBot" ? "LuckyTri" : value.name;
79
+ if (name !== PREVIOUS_NATURE_SEED.name || !SHIPPED_BASES.has(value.base))
80
+ return false;
81
+ return NATURE_FIELDS.every(
82
+ (key) =>
83
+ key === "name" ||
84
+ key === "base" ||
85
+ sameNatureField(value[key], PREVIOUS_NATURE_SEED[key]),
86
+ );
87
+ }
88
+ const TRAITS = ["humor", "sarcasm", "warmth", "activity", "initiative"];
89
+ const GENDERS = new Set(["female", "male", "unspecified"]);
90
+ const CLOCK = /^([01]\d|2[0-3]):([0-5]\d)$/;
91
+
92
+ export function validateNature(input) {
93
+ const value = { ...NATURE_DEFAULTS, ...(input || {}) };
94
+ if (
95
+ typeof value.name !== "string" ||
96
+ !value.name.trim() ||
97
+ value.name.length > 40
98
+ )
99
+ throw Error("名字无效");
100
+ if (!GENDERS.has(value.gender)) throw Error("性别无效");
101
+ if (typeof value.base !== "string" || value.base.length > 30000)
102
+ throw Error("天性正文无效");
103
+ for (const key of ["interests", "forbidden", "bottomLines"]) {
104
+ if (typeof value[key] === "string")
105
+ value[key] = value[key]
106
+ .split(/[,,\n]/)
107
+ .map((item) => item.trim())
108
+ .filter(Boolean);
109
+ if (
110
+ !Array.isArray(value[key]) ||
111
+ value[key].length > 60 ||
112
+ value[key].some((item) => typeof item !== "string" || item.length > 300)
113
+ )
114
+ throw Error("列表字段必须是文本列表");
115
+ }
116
+ for (const key of TRAITS)
117
+ if (!Number.isFinite(value[key]) || value[key] < 0 || value[key] > 100)
118
+ throw Error("性格刻度应在 0–100");
119
+ for (const key of ["length", "boundaries"])
120
+ if (typeof value[key] !== "string" || value[key].length > 2000)
121
+ throw Error("表达字段无效");
122
+ const rhythm = { ...NATURE_DEFAULTS.rhythm, ...(value.rhythm || {}) };
123
+ if (
124
+ typeof rhythm.enabled !== "boolean" ||
125
+ !CLOCK.test(rhythm.sleep) ||
126
+ !CLOCK.test(rhythm.wake) ||
127
+ rhythm.sleep === rhythm.wake
128
+ )
129
+ throw Error("作息时间无效");
130
+ value.rhythm = rhythm;
131
+ value.name = value.name.trim();
132
+ return Object.fromEntries(NATURE_FIELDS.map((key) => [key, value[key]]));
133
+ }
134
+
135
+ export class Nature {
136
+ constructor(repo) {
137
+ this.repo = repo;
138
+ this.db = repo.db;
139
+ }
140
+ row(before = Number.MAX_SAFE_INTEGER) {
141
+ return this.db
142
+ .prepare(
143
+ "SELECT * FROM mind_nature WHERE created<=? ORDER BY version DESC LIMIT 1",
144
+ )
145
+ .get(before);
146
+ }
147
+ current(before) {
148
+ const row = this.row(before) || this.row();
149
+ const value = row ? JSON.parse(row.value) : {};
150
+ if (
151
+ row?.version === 1 &&
152
+ (uneditedNatureSeed(value) || uneditedLivedSeed(value))
153
+ )
154
+ return { ...NATURE_DEFAULTS, version: row.version };
155
+ if (
156
+ row?.version === 1 &&
157
+ value.name === "LuckyBot" &&
158
+ (value.base === NATURE_DEFAULTS.base || SHIPPED_BASES.has(value.base))
159
+ )
160
+ value.name = "LuckyTri";
161
+ return { ...NATURE_DEFAULTS, ...value, version: row?.version || 0 };
162
+ }
163
+ version() {
164
+ return this.row()?.version || 0;
165
+ }
166
+ versions(limit = 30) {
167
+ return this.db
168
+ .prepare(
169
+ "SELECT version,created,note FROM mind_nature ORDER BY version DESC LIMIT ?",
170
+ )
171
+ .all(limit);
172
+ }
173
+ editsUsed() {
174
+ const version = this.version();
175
+ return version <= 1 ? 0 : version - 1;
176
+ }
177
+ save(input, note = "") {
178
+ if (this.editsUsed() >= 2)
179
+ throw Error("天性只能改两次。用完之后,由 TA 自己从经历里生长。");
180
+ const value = validateNature(input);
181
+ const version = this.version() + 1;
182
+ this.db
183
+ .prepare(
184
+ "INSERT INTO mind_nature(version,created,value,note) VALUES (?,?,?,?)",
185
+ )
186
+ .run(
187
+ version,
188
+ Date.now(),
189
+ JSON.stringify(value),
190
+ String(note).slice(0, 200),
191
+ );
192
+ const settings = this.repo.store.settings();
193
+ if (settings.name !== value.name || settings.persona !== value.base)
194
+ this.repo.store.save({ name: value.name, persona: value.base });
195
+ this.repo.store.revision++;
196
+ return this.current();
197
+ }
198
+ }
199
+
200
+ const minutes = (clock) => {
201
+ const [, h, m] = clock.match(CLOCK);
202
+ return Number(h) * 60 + Number(m);
203
+ };
204
+ const within = (value, start, end) =>
205
+ start < end ? value >= start && value < end : value >= start || value < end;
206
+
207
+ // A day of her life starts when she wakes (04:00 without a rhythm), not at
208
+ // midnight.
209
+ function wakeMinutes(nature) {
210
+ const rhythm = nature?.rhythm;
211
+ return rhythm?.enabled && CLOCK.test(rhythm.wake || "")
212
+ ? minutes(rhythm.wake)
213
+ : 240;
214
+ }
215
+ export function lifeDayKey(nature, time, timeZone) {
216
+ return localClock(time - wakeMinutes(nature) * 60000, timeZone).local.slice(
217
+ 0,
218
+ 10,
219
+ );
220
+ }
221
+ // The half-open interval of the life day named `day` (YYYY-MM-DD).
222
+ export function lifeSpan(nature, day, timeZone) {
223
+ let cursor = zonedTime(`${day} 12:00`, timeZone);
224
+ if (!cursor) return null;
225
+ for (let i = 0; i < 4 && lifeDayKey(nature, cursor, timeZone) !== day; i++)
226
+ cursor += 6 * 3600000;
227
+ if (lifeDayKey(nature, cursor, timeZone) !== day) return null;
228
+ const start = lifeDayStart(nature, cursor, timeZone);
229
+ const end = lifeDayStart(nature, start + 26 * 3600000, timeZone);
230
+ return end > start ? { start, end } : null;
231
+ }
232
+ export function lifeDayStart(nature, time, timeZone) {
233
+ const clock = localClock(time, timeZone);
234
+ const at = clock.hour * 60 + Number(clock.local.slice(14, 16));
235
+ const back = (at - wakeMinutes(nature) + 1440) % 1440;
236
+ return time - back * 60000 - (time % 60000);
237
+ }
238
+
239
+ // Where she is in her day. Energy is a baseline; the affect layer adds how
240
+ // the day has actually gone.
241
+ export function rhythmPhase(nature, now, timeZone) {
242
+ const clock = localClock(now, timeZone);
243
+ const rhythm = nature?.rhythm || NATURE_DEFAULTS.rhythm;
244
+ if (!rhythm.enabled)
245
+ return { key: "awake", label: "醒着", energy: 0.7, clock };
246
+ const at = clock.hour * 60 + Number(clock.local.slice(14, 16));
247
+ const sleep = minutes(rhythm.sleep);
248
+ const wake = minutes(rhythm.wake);
249
+ if (within(at, sleep, wake))
250
+ return { key: "asleep", label: "睡着了", energy: 0.1, clock };
251
+ if (within(at, wake, (wake + 60) % 1440))
252
+ return { key: "waking", label: "刚醒", energy: 0.45, clock };
253
+ if (within(at, (sleep + 1380) % 1440, sleep))
254
+ return { key: "sleepy", label: "有点困了", energy: 0.35, clock };
255
+ const afternoon = clock.hour >= 14 && clock.hour < 16;
256
+ return {
257
+ key: "awake",
258
+ label: "醒着",
259
+ energy: afternoon ? 0.65 : 0.75,
260
+ clock,
261
+ };
262
+ }
@@ -0,0 +1,19 @@
1
+ import { normalized, similar } from "./util.js";
2
+
3
+ // Surface wording can change while the idea remains the same. This modest
4
+ // overlap check is used only for recent self-initiated speech; it is not a
5
+ // general claim that two conversations have the same meaning.
6
+ export function sameRecentTheme(a, b) {
7
+ if (similar(a, b)) return true;
8
+ const left = normalized(a);
9
+ const right = normalized(b);
10
+ if (left.length < 10 || right.length < 10) return false;
11
+ const grams = (value) =>
12
+ new Set(
13
+ Array.from({ length: value.length - 1 }, (_, i) => value.slice(i, i + 2)),
14
+ );
15
+ const x = grams(left);
16
+ const y = grams(right);
17
+ const shared = [...x].filter((gram) => y.has(gram)).length;
18
+ return shared >= 5 && shared / Math.min(x.size, y.size) >= 0.32;
19
+ }
@@ -0,0 +1,84 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { hasCredential, text } from "./util.js";
3
+
4
+ const DAY = 86400000;
5
+ const DEDUPE = 6 * 3600000;
6
+ const DAILY = 24;
7
+
8
+ // Something a plugin says happened, cited as "o:<id>". Append-only: undoing
9
+ // it leaves a tombstone, and it never counts as the kind of outside evidence
10
+ // that moves her traits.
11
+ export class Observations {
12
+ constructor(mind) {
13
+ this.mind = mind;
14
+ this.db = mind.db;
15
+ }
16
+ add(
17
+ { pluginId, summary, detail = "", discretion = "open" },
18
+ now = Date.now(),
19
+ ) {
20
+ const words = text(summary, 300);
21
+ const more = text(detail, 1200);
22
+ if (!words || hasCredential(words + more))
23
+ return { rejected: "这段经历没有可保留的内容" };
24
+ const hash = createHash("sha256")
25
+ .update(`${pluginId}\n${words}`)
26
+ .digest("hex");
27
+ const again = this.db
28
+ .prepare(
29
+ "SELECT id FROM mind_observations WHERE plugin_id=? AND hash=? AND revoked=0 AND created>?",
30
+ )
31
+ .get(pluginId, hash, now - DEDUPE);
32
+ if (again) return { ref: `o:${again.id}`, duplicate: true };
33
+ const used = this.db
34
+ .prepare(
35
+ "SELECT COUNT(*) n FROM mind_observations WHERE plugin_id=? AND created>? AND created<=?",
36
+ )
37
+ .get(pluginId, now - DAY, now).n;
38
+ if (used >= DAILY) return { rejected: "今天从这里记下的经历已经够多了" };
39
+ const id = randomUUID();
40
+ this.db
41
+ .prepare(
42
+ "INSERT INTO mind_observations(id,created,plugin_id,summary,detail,discretion,hash,revoked) VALUES (?,?,?,?,?,?,?,0)",
43
+ )
44
+ .run(
45
+ id,
46
+ now,
47
+ pluginId,
48
+ words,
49
+ more,
50
+ discretion === "private" ? "private" : "open",
51
+ hash,
52
+ );
53
+ this.mind.store.revision++;
54
+ return { ref: `o:${id}` };
55
+ }
56
+ recent({ since = 0, before = Date.now(), limit = 6, session = "" } = {}) {
57
+ return this.db
58
+ .prepare(
59
+ "SELECT * FROM mind_observations WHERE revoked=0 AND created>? AND created<=? ORDER BY created DESC LIMIT ?",
60
+ )
61
+ .all(since, before, Math.max(1, Math.min(24, limit)))
62
+ .filter((row) => session === "" || row.discretion !== "private")
63
+ .map((row) => ({
64
+ ref: `o:${row.id}`,
65
+ created: row.created,
66
+ summary: row.summary,
67
+ ...(row.detail ? { detail: row.detail } : {}),
68
+ ...(row.discretion === "private" ? { private: true } : {}),
69
+ }));
70
+ }
71
+ get(id) {
72
+ return this.db
73
+ .prepare("SELECT * FROM mind_observations WHERE id=?")
74
+ .get(id);
75
+ }
76
+ revoke(id) {
77
+ const row = this.get(id);
78
+ if (!row || row.revoked) throw Error("这段经历不存在");
79
+ this.db
80
+ .prepare("UPDATE mind_observations SET revoked=1 WHERE id=?")
81
+ .run(id);
82
+ return row.summary;
83
+ }
84
+ }
@@ -0,0 +1,324 @@
1
+ import { withFallback } from "../core/model-manager.js";
2
+ import { prompts, replyPrompt } from "../core/persona-manager.js";
3
+ import { localClock } from "../core/conversation-cues.js";
4
+ import { evidence, text, hasCredential } from "./util.js";
5
+ import { CommitmentReview } from "./time/commitment-review.js";
6
+ import { chosenSlot } from "./day-planner.js";
7
+ import { activityEstimate } from "./time/activity-clock.js";
8
+ import { gameTopic } from "./time/intent.js";
9
+ import {
10
+ isExecutable,
11
+ isPluginActivity,
12
+ pluginAvailable,
13
+ withActivities,
14
+ } from "./time/kinds.js";
15
+
16
+ export class OwnDay {
17
+ constructor(life) {
18
+ this.life = life;
19
+ this.db = life.db;
20
+ this.review = new CommitmentReview(life);
21
+ }
22
+ state() {
23
+ return this.life.repo.config("own-day", {});
24
+ }
25
+ save(patch) {
26
+ this.life.repo.saveConfig("own-day", { ...this.state(), ...patch });
27
+ }
28
+ request() {
29
+ this.save({ reviewRequested: true });
30
+ return { queued: true };
31
+ }
32
+ status(now = this.life.now()) {
33
+ const state = this.state();
34
+ const pending = this.db
35
+ .prepare(
36
+ "SELECT COUNT(*) n FROM core_events WHERE role='assistant' AND seq>? AND time<=? AND seq NOT IN (SELECT seq FROM mind_unlived) AND seq NOT IN (SELECT seq FROM mind_time_commitment_reviews) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND json_extract(payload,'$.artifact') IS NULL",
37
+ )
38
+ .get(state.reviewSeq || 0, now).n;
39
+ const last = this.db
40
+ .prepare(
41
+ "SELECT finished,status,reason FROM mind_runs WHERE kind='day-care' AND started<=? ORDER BY started DESC LIMIT 1",
42
+ )
43
+ .get(now);
44
+ return {
45
+ queued: !!state.reviewRequested,
46
+ working: this.working || null,
47
+ reviewAt: state.reviewAt || null,
48
+ planAt: state.planAt || null,
49
+ pendingReplies: pending,
50
+ last: last || null,
51
+ };
52
+ }
53
+ async run(now = this.life.now()) {
54
+ const life = this.life,
55
+ time = life.mind.time;
56
+ if (
57
+ this.disabledForTest ||
58
+ life.closed ||
59
+ life.busy ||
60
+ !life.settings().solitude ||
61
+ life.repo.store.settings().demo ||
62
+ life.phase(now).key === "asleep" ||
63
+ time.primary() ||
64
+ !life.mind.budget.allows("inner", now)
65
+ )
66
+ return null;
67
+ let profile;
68
+ try {
69
+ profile = life.profile();
70
+ } catch {
71
+ return null;
72
+ }
73
+ const state = this.state(),
74
+ settings = time.settings(),
75
+ reviewDue =
76
+ state.reviewRequested ||
77
+ now - (state.reviewAt || 0) >= settings.commitmentReviewMinutes * 60000;
78
+ const ownOpen = this.db
79
+ .prepare(
80
+ "SELECT count(*) n FROM mind_time_tasks WHERE kind='plan' AND state IN ('todo','scheduled','doing','paused') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
81
+ )
82
+ .get().n;
83
+ const planDue =
84
+ ownOpen < 3 &&
85
+ now - (state.planAt || 0) >= settings.ownPlanMinutes * 60000;
86
+ if (!reviewDue && !planDue) return null;
87
+ return time.attention.activity(async () => {
88
+ let batch = null;
89
+ if (reviewDue) {
90
+ for (let page = 0; page < 5; page++) {
91
+ batch = this.review.batch(now);
92
+ if (!batch || batch.rows.length) break;
93
+ this.save({ reviewSeq: batch.through });
94
+ }
95
+ if (!batch || !batch.rows.length) {
96
+ this.save({ reviewAt: now, reviewRequested: false });
97
+ batch = null;
98
+ }
99
+ }
100
+ if (!batch && !planDue) return null;
101
+ const reviewing = !!batch,
102
+ trace = life.repo.trace("__mind__", "activity"),
103
+ version = life.mind.nature.version(),
104
+ runId = life.run(
105
+ "day-care",
106
+ reviewing ? "独处回看有没有漏下约定" : "想想接下来愿意做点什么",
107
+ );
108
+ this.working = reviewing ? "review" : "plan";
109
+ this.save(
110
+ reviewing ? { reviewAt: now, reviewRequested: false } : { planAt: now },
111
+ );
112
+ let status = "error",
113
+ reason = "";
114
+ try {
115
+ const projects = time.works
116
+ .projects({ limit: 12 })
117
+ .filter((p) => p.state === "active" && time.visible(p, "", now))
118
+ .slice(0, 4);
119
+ const self = life.selfView(now, { open: true }).slice(0, 8);
120
+ const input = reviewing
121
+ ? {
122
+ review: batch.messages,
123
+ existing: time.tasks
124
+ .list({ limit: 80 })
125
+ .filter((t) => time.visible(t, batch.session, now))
126
+ .map((t) => ({
127
+ id: t.id,
128
+ title: t.title,
129
+ state: t.state,
130
+ share: t.share_state,
131
+ }))
132
+ .slice(0, 12),
133
+ }
134
+ : {
135
+ clock: localClock(now, life.mind.timeZone()),
136
+ self,
137
+ interests: life.mind.nature.current(now).interests,
138
+ affect: life.mind.affect.state(now),
139
+ projects: projects.map((p) => ({
140
+ id: p.id,
141
+ title: p.title,
142
+ kind: p.kind,
143
+ summary: text(p.summary, 240),
144
+ })),
145
+ available: {
146
+ read: life.mind.reading.unreadCount() > 0,
147
+ write: true,
148
+ think: true,
149
+ game: !time.search.ready(now),
150
+ ...pluginAvailable(),
151
+ },
152
+ busy: time.tasks
153
+ .slots(now)
154
+ .map(({ startAt, endAt, priority }) => ({
155
+ startAt,
156
+ endAt,
157
+ priority,
158
+ })),
159
+ pending: time.tasks
160
+ .list({ limit: 20 })
161
+ .filter(
162
+ (t) =>
163
+ time.visible(t, "", now) &&
164
+ !["done", "abandoned"].includes(t.state),
165
+ )
166
+ .map((t) => ({ title: t.title, state: t.state }))
167
+ .slice(0, 5),
168
+ };
169
+ const instruction = reviewing
170
+ ? '独处回看自己确实送达的话有没有漏下承诺。review.said是自己说的话,earlier是前面的真实语境。只提取明确接受的未来行动;玩笑、引用、别人的计划、作品情节、愿望和过去完成的事不算承诺。没有日期也可以有约定。已放下或已完成的事项不要复活,已经整理的事项不要重建。输出JSON {"commitments":[{"source":"m:消息seq","quote":"said中的原文片段","accepted":true,"kind":"promise|plan","activity":"write|read|think|game|unknown","title":"具体行动"}]},没有遗漏则空数组。只整理,不发送、不声称兑现。'
171
+ : '这是留给自己的空隙,不需要有人先约你,也不需要新消息才有生活。根据自己的兴趣、好奇、已有作品与精力,决定想不想随手做一件有意思的小事。可以阅读、写个小场景、玩一段游戏、整理一个想法,也可以选择歇着。一次最多一件,不为填满时间而列清单,不把别人的建议冒充自己的愿望,不编造外部经历或无法执行的动作。自己决定几点开始、这次想投入多少分钟;durationMinutes是实际安排分钟,不是API耗时,不再除以加速倍数。游戏必须选定明确名字,已有项目可以接着做,不要只说“玩一会galgame”。输出JSON {"skip":false,"plan":{"activity":"write|read|think|game","title":"具体小行动","topic":"游戏名称或空","why":"自己的动机","startAt":"本地YYYY-MM-DD HH:mm","durationMinutes":20,"sources":["s:本次self中thread"],"projectId":"本次项目ID或null"}}。没有想做的事就skip:true。';
172
+ const answer = await withFallback(
173
+ life.chat.models,
174
+ life.chat.fallbackFor(null, profile, trace),
175
+ ).call(
176
+ profile,
177
+ "reflection",
178
+ replyPrompt(
179
+ life.mind.nature.current(now),
180
+ { ...prompts(life.repo), activity: withActivities(instruction) },
181
+ "activity",
182
+ ),
183
+ input,
184
+ trace,
185
+ );
186
+ if (
187
+ life.closed ||
188
+ version !== life.mind.nature.version() ||
189
+ time.primary()
190
+ ) {
191
+ status = "cancelled";
192
+ return { status };
193
+ }
194
+ if (reviewing) {
195
+ this.db.exec("SAVEPOINT commitment_care");
196
+ try {
197
+ const ids = this.review.apply(batch, answer, life.now());
198
+ this.save({ reviewSeq: batch.through });
199
+ status = "reviewed";
200
+ reason = ids.length
201
+ ? "把说过的约定重新理清了"
202
+ : "回看过这一段,没有漏下的新约定";
203
+ return { status, reason, tasks: ids };
204
+ } catch (error) {
205
+ this.db.exec("ROLLBACK TO commitment_care");
206
+ throw error;
207
+ } finally {
208
+ this.db.exec("RELEASE commitment_care");
209
+ }
210
+ }
211
+ if (answer?.skip === true) {
212
+ status = "resting";
213
+ reason = "这会儿想留一点空白";
214
+ return { status, reason };
215
+ }
216
+ const plan = answer?.plan;
217
+ if (
218
+ !plan ||
219
+ !isExecutable(plan.activity) ||
220
+ !text(plan.title, 160) ||
221
+ !text(plan.why, 200) ||
222
+ hasCredential(plan.title + plan.why)
223
+ )
224
+ throw Error("自己的安排格式无效");
225
+ if (
226
+ (plan.activity === "read" && !input.available.read) ||
227
+ (plan.activity === "game" && !input.available.game) ||
228
+ (isPluginActivity(plan.activity) && !input.available[plan.activity])
229
+ ) {
230
+ status = "waiting";
231
+ reason = "想做的事情还缺少条件";
232
+ return { status, reason };
233
+ }
234
+ const valid = new Set(self.map((s) => `s:${s.thread}`)),
235
+ sources = evidence(plan.sources).filter((s) => valid.has(s)),
236
+ project = projects.find(
237
+ (p) => p.id === plan.projectId && p.kind === plan.activity,
238
+ );
239
+ const topic =
240
+ plan.activity === "game"
241
+ ? text(
242
+ plan.topic || gameTopic(plan.title) || project?.bible?.topic,
243
+ 80,
244
+ )
245
+ : "";
246
+ if (plan.activity === "game" && !topic)
247
+ throw Error("需要自己选定要玩的游戏");
248
+ const slot = chosenSlot(
249
+ {
250
+ ...plan,
251
+ durationMinutes:
252
+ plan.durationMinutes ??
253
+ Math.ceil(
254
+ activityEstimate(plan.activity, null, time.settings().paceSpeed)
255
+ .plannedMs / 60000,
256
+ ),
257
+ reason: plan.why,
258
+ },
259
+ life,
260
+ life.now(),
261
+ now,
262
+ );
263
+ this.db.exec("SAVEPOINT own_choice");
264
+ try {
265
+ const wish = life.mind.self.propose(
266
+ {
267
+ kind: "intention",
268
+ content: `我想${text(plan.title, 100)}`,
269
+ sources,
270
+ strength: 0.25,
271
+ },
272
+ { valid, origin: "solitude", time: life.now() },
273
+ );
274
+ if (!wish.thread) throw Error(wish.rejected || "愿望未保存");
275
+ const result = time.tasks.add(
276
+ {
277
+ kind: "plan",
278
+ activity: plan.activity,
279
+ title:
280
+ topic && !gameTopic(plan.title)
281
+ ? `${plan.title} ·《${topic}》`
282
+ : plan.title,
283
+ why: plan.why,
284
+ sources: [`s:${wish.thread}`, ...(project?.sources || [])],
285
+ projectId: project?.id || null,
286
+ origin: "own-day",
287
+ },
288
+ life.now(),
289
+ );
290
+ const id = result.id || result.duplicate;
291
+ if (result.id)
292
+ time.tasks.arrange(result.id, { ...slot, topic }, life.now());
293
+ if (result.id)
294
+ time.event(
295
+ id,
296
+ "own-choice",
297
+ "空下来时自己想做的事",
298
+ { wish: wish.thread },
299
+ life.now(),
300
+ );
301
+ status = id ? "planned" : "empty";
302
+ reason = id ? "给自己留下一件想做的事" : "这件事已经有所安排";
303
+ return { status, reason, task: id };
304
+ } catch (error) {
305
+ this.db.exec("ROLLBACK TO own_choice");
306
+ throw error;
307
+ } finally {
308
+ this.db.exec("RELEASE own_choice");
309
+ }
310
+ } catch (error) {
311
+ reason = text(error.message, 160);
312
+ trace.error = reason;
313
+ return { status: "error", reason };
314
+ } finally {
315
+ this.working = null;
316
+ life.chat.finishQuietly(
317
+ trace,
318
+ status === "error" ? "error" : "complete",
319
+ );
320
+ life.end(runId, status, reason, trace);
321
+ }
322
+ });
323
+ }
324
+ }