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,454 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { localClock } from "./conversation-cues.js";
3
+ import { readableName, speakerNames } from "./speaker-names.js";
4
+ import { grounded } from "../mind/salience.js";
5
+
6
+ export const DEFAULT_CONTEXT_MESSAGES = 40;
7
+ export const COMPACTION_BLOCK = 30;
8
+ // Each level covers fanIn times more chat than the one below but gets only a
9
+ // little more room, so recent turns keep detail and old ones keep the thread.
10
+ export const SUMMARY_LEVELS = [
11
+ { capacity: 4, fanIn: 3, summaryChars: 300, keyPoints: 4 },
12
+ { capacity: 3, fanIn: 3, summaryChars: 450, keyPoints: 5 },
13
+ { capacity: 3, fanIn: 3, summaryChars: 600, keyPoints: 6 },
14
+ { capacity: 1, fanIn: 0, summaryChars: 900, keyPoints: 8 },
15
+ ];
16
+ const TOP_LEVEL = SUMMARY_LEVELS.length - 1;
17
+ const FIRST_RUN_LOOKBACK = 120;
18
+ const MAX_CALLS_PER_RUN = 3;
19
+ const MAX_RUNNING = 2;
20
+ const BACKOFF_MS = [60000, 300000, 900000, 1800000];
21
+ const SENSITIVE_POINT =
22
+ /密码|验证码|密钥|身份证|银行卡|api.?key|sk-[a-z0-9]{8,}/i;
23
+ const LIVE = " AND COALESCE(json_extract(payload,'$.simulated'),0)=0";
24
+
25
+ export function contextKeep(policy = {}) {
26
+ const value = Number(policy?.contextMessages);
27
+ return Number.isInteger(value) && value > 0
28
+ ? value
29
+ : DEFAULT_CONTEXT_MESSAGES;
30
+ }
31
+
32
+ export function summaryPeriod(first, last = first, timeZone) {
33
+ const start = localClock(first, timeZone).local;
34
+ const end = localClock(last ?? first, timeZone).local;
35
+ if (start.slice(0, 4) !== end.slice(0, 4)) return `${start}–${end}`;
36
+ return `${start.slice(5)}–${start.slice(0, 10) === end.slice(0, 10) ? end.slice(11) : end.slice(5)}`;
37
+ }
38
+
39
+ function redact(text) {
40
+ return String(text)
41
+ .replace(/\bsk-[A-Za-z0-9_-]{8,}/g, "[已隐藏]")
42
+ .replace(/Bearer\s+\S+/gi, "Bearer [已隐藏]");
43
+ }
44
+
45
+ function cleanSummary(value, level) {
46
+ const spec = SUMMARY_LEVELS[level];
47
+ if (!value || typeof value.summary !== "string" || !value.summary.trim())
48
+ throw Error("语境摘要格式无效");
49
+ const keyPoints = (Array.isArray(value.keyPoints) ? value.keyPoints : [])
50
+ .map((point) => (typeof point === "string" ? { text: point } : point))
51
+ .filter(
52
+ (point) =>
53
+ point &&
54
+ typeof point.text === "string" &&
55
+ point.text.trim() &&
56
+ !SENSITIVE_POINT.test(point.text),
57
+ )
58
+ .map((point) => {
59
+ const importance = Number(point.importance);
60
+ return {
61
+ text: redact(point.text.trim()).slice(0, 80),
62
+ importance: Number.isFinite(importance)
63
+ ? Math.min(1, Math.max(0, importance))
64
+ : 0.5,
65
+ open: point.open === true,
66
+ };
67
+ })
68
+ .sort(
69
+ (a, b) => Number(b.open) - Number(a.open) || b.importance - a.importance,
70
+ )
71
+ .slice(0, spec.keyPoints);
72
+ return {
73
+ summary: redact(value.summary.trim()).slice(
74
+ 0,
75
+ Math.round(spec.summaryChars * 1.5),
76
+ ),
77
+ keyPoints,
78
+ };
79
+ }
80
+
81
+ function keptPoints(points, said) {
82
+ return points.filter((point) => grounded(point.text, said));
83
+ }
84
+
85
+ function sourceRow(m, names, timeZone) {
86
+ const media = (m.attachments || [])
87
+ .map((a) => a.summary || `[${a.type}]`)
88
+ .filter(Boolean)
89
+ .join(" ");
90
+ return {
91
+ id: m.seq,
92
+ name:
93
+ readableName(m.name, m.userId) ||
94
+ names.get(String(m.userId)) ||
95
+ (m.role === "assistant" ? "self" : String(m.userId || "")),
96
+ role: m.role,
97
+ ...(m.artifact ? { artifact: m.artifact } : {}),
98
+ time: localClock(m.time, timeZone).local.slice(5),
99
+ text:
100
+ (m.artifact
101
+ ? "[作品正文 · " +
102
+ m.artifact.domain +
103
+ ",其中人物/剧情不是现实人物事实] "
104
+ : "") + String(m.text || media).slice(0, 500),
105
+ };
106
+ }
107
+
108
+ // Compaction is bookkeeping; it runs at the lightest reasoning level the
109
+ // profile accepts instead of the level chosen for replies.
110
+ function lighter(profile) {
111
+ const efforts = Array.isArray(profile?.reasoningEfforts)
112
+ ? profile.reasoningEfforts
113
+ : [];
114
+ if (!efforts.length) return profile;
115
+ return {
116
+ ...profile,
117
+ reasoningEffort: efforts.includes("none") ? "none" : efforts[0],
118
+ };
119
+ }
120
+
121
+ export class ContextCompactor {
122
+ constructor(repo) {
123
+ this.repo = repo;
124
+ this.busy = new Set();
125
+ this.running = 0;
126
+ this.failures = new Map();
127
+ this.closed = false;
128
+ }
129
+ rows(session) {
130
+ return this.repo.db
131
+ .prepare(
132
+ "SELECT * FROM core_context_summaries WHERE session_id=? ORDER BY first_seq",
133
+ )
134
+ .all(session)
135
+ .map((row) => ({ ...row, data: JSON.parse(row.data) }));
136
+ }
137
+ list(session, { before = Number.MAX_SAFE_INTEGER, timeZone } = {}) {
138
+ return this.rows(session)
139
+ .filter((row) => row.last_seq < before)
140
+ .map((row) => ({
141
+ id: row.id,
142
+ level: row.level,
143
+ firstSeq: row.first_seq,
144
+ lastSeq: row.last_seq,
145
+ period: summaryPeriod(row.first_time, row.last_time, timeZone),
146
+ summary: row.data.summary,
147
+ keyPoints: row.data.keyPoints || [],
148
+ messages: row.data.messages || 0,
149
+ created: row.created,
150
+ }));
151
+ }
152
+ forPrompt(session, options = {}) {
153
+ const rows = this.list(session, options);
154
+ const shown = rows.filter(
155
+ (row) => row.summary || (row.keyPoints && row.keyPoints.length),
156
+ );
157
+ return {
158
+ summaries: shown.map((row) => ({
159
+ level: row.level,
160
+ period: row.period,
161
+ summary: row.summary,
162
+ ...(row.keyPoints.length
163
+ ? {
164
+ keyPoints: row.keyPoints.map(
165
+ (point) => (point.open ? "未完:" : "") + point.text,
166
+ ),
167
+ }
168
+ : {}),
169
+ })),
170
+ coverage: rows.at(-1)?.lastSeq || 0,
171
+ start: shown[0]?.firstSeq || rows[0]?.firstSeq || 0,
172
+ };
173
+ }
174
+ clear(session) {
175
+ this.failures.delete(session);
176
+ return Number(
177
+ this.repo.db
178
+ .prepare("DELETE FROM core_context_summaries WHERE session_id=?")
179
+ .run(session).changes,
180
+ );
181
+ }
182
+ close() {
183
+ this.closed = true;
184
+ }
185
+ nextBlock(session, keep) {
186
+ const db = this.repo.db;
187
+ const covered = db
188
+ .prepare(
189
+ "SELECT MAX(last_seq) n FROM core_context_summaries WHERE session_id=?",
190
+ )
191
+ .get(session).n;
192
+ let after = covered ?? 0;
193
+ if (covered == null) {
194
+ // History that predates compaction starts a short way back; older turns
195
+ // stay reachable through stage summaries, memories and recall.
196
+ const total = db
197
+ .prepare(`SELECT COUNT(*) n FROM core_events WHERE session_id=?${LIVE}`)
198
+ .get(session).n;
199
+ const skip = total - (keep + COMPACTION_BLOCK + FIRST_RUN_LOOKBACK);
200
+ if (skip > 0)
201
+ after = db
202
+ .prepare(
203
+ `SELECT seq FROM core_events WHERE session_id=?${LIVE} ORDER BY seq LIMIT 1 OFFSET ?`,
204
+ )
205
+ .get(session, skip - 1).seq;
206
+ }
207
+ const pending = db
208
+ .prepare(
209
+ `SELECT COUNT(*) n FROM core_events WHERE session_id=? AND seq>?${LIVE}`,
210
+ )
211
+ .get(session, after).n;
212
+ if (pending < keep + COMPACTION_BLOCK) return null;
213
+ return this.repo.eventsAfter(session, after, {
214
+ simulated: false,
215
+ limit: COMPACTION_BLOCK,
216
+ });
217
+ }
218
+ nextMerge(session) {
219
+ const rows = this.rows(session);
220
+ for (let level = 0; level < TOP_LEVEL; level++) {
221
+ const spec = SUMMARY_LEVELS[level];
222
+ const same = rows.filter(
223
+ (row) =>
224
+ row.level === level &&
225
+ (row.data.summary || (row.data.keyPoints || []).length),
226
+ );
227
+ if (same.length <= spec.capacity) continue;
228
+ const children = same.slice(0, spec.fanIn);
229
+ const target = level + 1;
230
+ if (target !== TOP_LEVEL) return { target, children };
231
+ const rolling = rows.find((row) => row.level === TOP_LEVEL);
232
+ return { target, children: rolling ? [rolling, ...children] : children };
233
+ }
234
+ return null;
235
+ }
236
+ save(session, level, first, last, data, replaces = []) {
237
+ const db = this.repo.db;
238
+ db.exec("BEGIN IMMEDIATE");
239
+ try {
240
+ if (replaces.length) {
241
+ const marks = replaces.map(() => "?").join(",");
242
+ const present = db
243
+ .prepare(
244
+ `SELECT COUNT(*) n FROM core_context_summaries WHERE id IN (${marks})`,
245
+ )
246
+ .get(...replaces).n;
247
+ if (present !== replaces.length) {
248
+ db.exec("ROLLBACK");
249
+ return false;
250
+ }
251
+ db.prepare(
252
+ `DELETE FROM core_context_summaries WHERE id IN (${marks})`,
253
+ ).run(...replaces);
254
+ } else {
255
+ // A cleared session or an overlapping run must not leave an orphan.
256
+ const exists = db
257
+ .prepare(
258
+ "SELECT COUNT(*) n FROM core_events WHERE session_id=? AND seq=?",
259
+ )
260
+ .get(session, last.seq).n;
261
+ const overlap = db
262
+ .prepare(
263
+ "SELECT COUNT(*) n FROM core_context_summaries WHERE session_id=? AND last_seq>=?",
264
+ )
265
+ .get(session, first.seq).n;
266
+ if (!exists || overlap) {
267
+ db.exec("ROLLBACK");
268
+ return false;
269
+ }
270
+ }
271
+ db.prepare(
272
+ "INSERT INTO core_context_summaries VALUES (?,?,?,?,?,?,?,?,?)",
273
+ ).run(
274
+ randomUUID(),
275
+ session,
276
+ level,
277
+ first.seq,
278
+ last.seq,
279
+ first.time ?? null,
280
+ last.time ?? null,
281
+ Date.now(),
282
+ JSON.stringify(data),
283
+ );
284
+ db.exec("COMMIT");
285
+ return true;
286
+ } catch (error) {
287
+ db.exec("ROLLBACK");
288
+ throw error;
289
+ }
290
+ }
291
+ async summarizeBlock(session, rows, options) {
292
+ const { models, profile, system, trace, timeZone, self } = options;
293
+ const aside = new Set(
294
+ this.repo.db
295
+ .prepare("SELECT seq FROM mind_unlived")
296
+ .all()
297
+ .map((row) => row.seq),
298
+ );
299
+ const lived = rows.filter((m) => !aside.has(m.seq));
300
+ const span = {
301
+ first: { seq: rows[0].seq, time: rows[0].time },
302
+ last: { seq: rows.at(-1).seq, time: rows.at(-1).time },
303
+ };
304
+ // She was not in the room for these lines. Cover them so they are not
305
+ // summarized later, and do not put the words into the summary.
306
+ if (!lived.length)
307
+ return this.save(session, 0, span.first, span.last, {
308
+ summary: "",
309
+ keyPoints: [],
310
+ messages: 0,
311
+ });
312
+ const names = speakerNames(this.repo.db, [session]);
313
+ const previous = this.rows(session)
314
+ .filter((row) => row.data.summary)
315
+ .at(-1);
316
+ const spec = SUMMARY_LEVELS[0];
317
+ const result = await models.call(
318
+ lighter(profile),
319
+ "summary",
320
+ system,
321
+ {
322
+ sessionId: session,
323
+ self: self || "LuckyTri",
324
+ ...(previous
325
+ ? {
326
+ previous: {
327
+ period: summaryPeriod(
328
+ previous.first_time,
329
+ previous.last_time,
330
+ timeZone,
331
+ ),
332
+ summary: previous.data.summary,
333
+ },
334
+ }
335
+ : {}),
336
+ messages: lived.map((m) => sourceRow(m, names, timeZone)),
337
+ limits: { summaryChars: spec.summaryChars, keyPoints: spec.keyPoints },
338
+ },
339
+ trace,
340
+ );
341
+ const cleaned = cleanSummary(result, 0);
342
+ cleaned.keyPoints = keptPoints(
343
+ cleaned.keyPoints,
344
+ lived.map((m) => String(m.text || "")),
345
+ );
346
+ return this.save(session, 0, span.first, span.last, {
347
+ ...cleaned,
348
+ messages: lived.length,
349
+ });
350
+ }
351
+ async mergeSummaries(session, { target, children }, options) {
352
+ const { models, profile, mergeSystem, trace, timeZone, self } = options;
353
+ const spec = SUMMARY_LEVELS[target];
354
+ const result = await models.call(
355
+ lighter(profile),
356
+ "summary",
357
+ mergeSystem,
358
+ {
359
+ sessionId: session,
360
+ self: self || "LuckyTri",
361
+ children: children.map((row) => ({
362
+ level: row.level,
363
+ period: summaryPeriod(row.first_time, row.last_time, timeZone),
364
+ summary: row.data.summary,
365
+ keyPoints: row.data.keyPoints || [],
366
+ })),
367
+ limits: { summaryChars: spec.summaryChars, keyPoints: spec.keyPoints },
368
+ },
369
+ trace,
370
+ );
371
+ const first = children[0];
372
+ const last = children.at(-1);
373
+ const cleaned = cleanSummary(result, target);
374
+ cleaned.keyPoints = keptPoints(cleaned.keyPoints, [
375
+ ...children.map((row) => row.data.summary || ""),
376
+ ...children.flatMap((row) =>
377
+ (row.data.keyPoints || []).map((point) => point.text || ""),
378
+ ),
379
+ ]);
380
+ return this.save(
381
+ session,
382
+ target,
383
+ { seq: first.first_seq, time: first.first_time },
384
+ { seq: last.last_seq, time: last.last_time },
385
+ {
386
+ ...cleaned,
387
+ messages: children.reduce((n, row) => n + (row.data.messages || 0), 0),
388
+ },
389
+ children.map((row) => row.id),
390
+ );
391
+ }
392
+ async compact(session, options) {
393
+ let saved = 0;
394
+ while (saved < MAX_CALLS_PER_RUN && !this.closed) {
395
+ const merge = this.nextMerge(session);
396
+ const block = merge ? null : this.nextBlock(session, options.keep);
397
+ if (!merge && !block?.length) break;
398
+ const stored = merge
399
+ ? await this.mergeSummaries(session, merge, options)
400
+ : await this.summarizeBlock(session, block, options);
401
+ // The session changed underneath (cleared or compacted elsewhere).
402
+ if (!stored) break;
403
+ saved++;
404
+ }
405
+ return saved;
406
+ }
407
+ ready(session) {
408
+ if (this.closed || this.busy.has(session) || this.running >= MAX_RUNNING)
409
+ return false;
410
+ const failure = this.failures.get(session);
411
+ return !failure || failure.until <= Date.now();
412
+ }
413
+ // Background only: callers never await this on the conversational lane.
414
+ schedule(session, options) {
415
+ if (!this.ready(session)) return null;
416
+ if (!this.nextMerge(session) && !this.nextBlock(session, options.keep))
417
+ return null;
418
+ this.busy.add(session);
419
+ this.running++;
420
+ const trace = this.repo.trace(session, "summary");
421
+ return this.compact(session, { ...options, trace })
422
+ .then((calls) => {
423
+ this.failures.delete(session);
424
+ trace.reason = calls
425
+ ? `整理了 ${calls} 段语境摘要`
426
+ : "没有需要压缩的内容";
427
+ this.finishTrace(trace, "complete");
428
+ return calls;
429
+ })
430
+ .catch((error) => {
431
+ const count = (this.failures.get(session)?.count || 0) + 1;
432
+ this.failures.set(session, {
433
+ count,
434
+ until:
435
+ Date.now() + BACKOFF_MS[Math.min(count, BACKOFF_MS.length) - 1],
436
+ });
437
+ trace.error = error.message;
438
+ trace.reason = "语境压缩失败,稍后自动重试";
439
+ this.finishTrace(trace, "error");
440
+ return 0;
441
+ })
442
+ .finally(() => {
443
+ this.busy.delete(session);
444
+ this.running--;
445
+ });
446
+ }
447
+ finishTrace(trace, status) {
448
+ try {
449
+ this.repo.finish(trace, status);
450
+ } catch {
451
+ // The database may already be closed during shutdown.
452
+ }
453
+ }
454
+ }