luckytri 1.0.4 → 1.0.6

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 (156) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/en/CHANGELOG.md +20 -13
  5. package/docs/en/guide.md +7 -5
  6. package/docs/en/her-life.md +2 -2
  7. package/docs/en/webui.md +1 -1
  8. package/docs/zh/CHANGELOG.md +20 -13
  9. package/docs/zh/guide.md +7 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +2 -1
  13. package/public/app/assets/ChatsPage-CFMVozoG.css +1 -0
  14. package/public/app/assets/{ChatsPage-CshdtTzV.js → ChatsPage-DCYMkJ91.js} +2 -2
  15. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-CLyZkxR5.js} +1 -1
  16. package/public/app/assets/{Empty-DmUB2-Bk.css → Empty-DIobYG3x.css} +1 -1
  17. package/public/app/assets/{Field-BjFv4Rfn.js → Field-BpaRwLI_.js} +1 -1
  18. package/public/app/assets/{HeartPage-eqo6ru16.js → HeartPage-BIOMTiwr.js} +1 -1
  19. package/public/app/assets/HeartPage-C-2jvgzF.css +1 -0
  20. package/public/app/assets/{LifePage-CN8GE5NK.js → LifePage-BCgM4JkR.js} +1 -1
  21. package/public/app/assets/LifePage-CG1OHbRK.css +1 -0
  22. package/public/app/assets/MemoryPage-C_Bu4rfb.css +1 -0
  23. package/public/app/assets/{MemoryPage-CCpMN4Wg.js → MemoryPage-vShvRqMt.js} +1 -1
  24. package/public/app/assets/NaturePage-CPZlImT0.css +1 -0
  25. package/public/app/assets/{NaturePage-C0JqdBTR.js → NaturePage-Cy9ptr4G.js} +2 -2
  26. package/public/app/assets/PeoplePage-3RRUITDz.css +1 -0
  27. package/public/app/assets/{PeoplePage-BXGBbO4u.js → PeoplePage-CUZekB2E.js} +1 -1
  28. package/public/app/assets/{PluginsPage-Dksp5AV5.js → PluginsPage-1RM4rmJP.js} +1 -1
  29. package/public/app/assets/{SystemPage-BenIyTvk.css → SystemPage-BI2CB5Ig.css} +1 -1
  30. package/public/app/assets/{SystemPage-Cd7aa8eE.js → SystemPage-BeT5TmIV.js} +1 -1
  31. package/public/app/assets/TimePage-C-guwPk2.css +1 -0
  32. package/public/app/assets/{TimePage-Bs_zImb4.js → TimePage-taTmPMM8.js} +2 -2
  33. package/public/app/assets/{index-BHZcHUMh.css → index-DY5AM0Hz.css} +1 -1
  34. package/public/app/assets/{index-CyRtYxP3.js → index-DdYwC5ZU.js} +3 -3
  35. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-DJmfu2KN.js} +1 -1
  36. package/public/app/assets/{media-Bwtx8blz.js → media-BEtnE0Gv.js} +1 -1
  37. package/public/app/assets/{mind-DtTVrW6Z.js → mind-Dtiw0-QK.js} +1 -1
  38. package/public/app/index.html +6 -5
  39. package/public/guide.en.html +11 -3
  40. package/public/guide.html +6 -3
  41. package/public/tutorial.en.md +2 -0
  42. package/public/tutorial.md +2 -0
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/service-supervisor.js +50 -0
  46. package/scripts/start-server.js +71 -16
  47. package/scripts/update-instance.js +201 -201
  48. package/server/channels/adapters.js +27 -27
  49. package/server/channels/onebot/adapter.js +288 -288
  50. package/server/channels/qqbot/adapter.js +253 -253
  51. package/server/channels/qqbot/api.js +199 -199
  52. package/server/channels/qqbot/credentials.js +11 -11
  53. package/server/channels/qqbot/gateway.js +274 -274
  54. package/server/channels/qqbot/index.js +389 -389
  55. package/server/channels/segments.js +40 -40
  56. package/server/channels/session-key.js +153 -153
  57. package/server/channels/state.js +42 -42
  58. package/server/core/api.js +564 -564
  59. package/server/core/context-builder.js +288 -288
  60. package/server/core/context-compactor.js +454 -454
  61. package/server/core/conversation-cues.js +325 -325
  62. package/server/core/conversation-manager.js +87 -87
  63. package/server/core/feedback.js +8 -8
  64. package/server/core/group-style.js +72 -72
  65. package/server/core/initiative-context.js +66 -66
  66. package/server/core/input-budget.js +32 -32
  67. package/server/core/local-demo.js +98 -98
  68. package/server/core/message-manager.js +118 -118
  69. package/server/core/message-scheduler.js +78 -78
  70. package/server/core/model-json.js +190 -190
  71. package/server/core/model-presets.js +900 -900
  72. package/server/core/name-call.js +29 -29
  73. package/server/core/network.js +157 -157
  74. package/server/core/person-name.js +22 -22
  75. package/server/core/persona-manager.js +2 -1
  76. package/server/core/reply-delivery.js +404 -403
  77. package/server/core/reply-target-resolver.js +50 -50
  78. package/server/core/response-generator.js +50 -50
  79. package/server/core/response-validator.js +14 -2
  80. package/server/core/retired-prompts.js +51 -51
  81. package/server/core/sessions.js +109 -109
  82. package/server/core/speaker-names.js +92 -92
  83. package/server/core/topic-tracker.js +65 -65
  84. package/server/http.js +18 -18
  85. package/server/i18n/decisions.js +209 -206
  86. package/server/i18n/errors.js +4 -0
  87. package/server/i18n/index.js +144 -144
  88. package/server/i18n/models.js +111 -111
  89. package/server/knowledge/api.js +342 -342
  90. package/server/knowledge/embedding-profile.js +114 -114
  91. package/server/knowledge/index.js +13 -13
  92. package/server/knowledge/retrieval.js +93 -93
  93. package/server/knowledge/schema.js +158 -158
  94. package/server/knowledge/vector-index.js +90 -90
  95. package/server/mind/affect.js +265 -265
  96. package/server/mind/attention.js +200 -200
  97. package/server/mind/bonds.js +565 -565
  98. package/server/mind/clock.js +83 -83
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +60 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -323
  108. package/server/mind/life/diary.js +20 -0
  109. package/server/mind/life/growth.js +261 -254
  110. package/server/mind/life/index.js +10 -2
  111. package/server/mind/life/presence.js +305 -305
  112. package/server/mind/meetings.js +1069 -1069
  113. package/server/mind/memory-policy.js +48 -48
  114. package/server/mind/memory.js +888 -888
  115. package/server/mind/nature.js +358 -358
  116. package/server/mind/novelty.js +19 -19
  117. package/server/mind/periods.js +118 -118
  118. package/server/mind/reading.js +120 -120
  119. package/server/mind/relationship-context.js +71 -0
  120. package/server/mind/relationships.js +390 -279
  121. package/server/mind/salience.js +201 -201
  122. package/server/mind/schema.js +567 -567
  123. package/server/mind/self.js +484 -484
  124. package/server/mind/thoughts.js +274 -274
  125. package/server/mind/time/agenda.js +242 -242
  126. package/server/mind/time/api.js +200 -200
  127. package/server/mind/time/attention.js +34 -34
  128. package/server/mind/time/delivery-links.js +130 -130
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +51 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +432 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +9 -1
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-execution.js +2 -1
  141. package/server/runtime/update-manager.js +140 -140
  142. package/server/storage/data-package.js +298 -298
  143. package/server/storage/data-transfer-api.js +50 -50
  144. package/server/storage/data-transfer-worker.js +18 -18
  145. package/server/storage/data-transfer.js +416 -416
  146. package/server/studio/display-names.js +139 -139
  147. package/server/studio/management.js +102 -102
  148. package/server/studio/updates.js +144 -144
  149. package/server/version.js +10 -10
  150. package/public/app/assets/ChatsPage-DfyPCS9N.css +0 -1
  151. package/public/app/assets/HeartPage-Bft9wZp5.css +0 -1
  152. package/public/app/assets/LifePage-BUi5euBF.css +0 -1
  153. package/public/app/assets/MemoryPage-cZvXfkXb.css +0 -1
  154. package/public/app/assets/NaturePage-CHkpb5NK.css +0 -1
  155. package/public/app/assets/PeoplePage-BTU0H1pM.css +0 -1
  156. package/public/app/assets/TimePage-BOms_n4I.css +0 -1
@@ -1,342 +1,342 @@
1
- import { randomUUID } from "node:crypto";
2
- import { wrap } from "../http.js";
3
- import { prompts } from "../core/persona-manager.js";
4
- import { indexMemory } from "./schema.js";
5
- import {
6
- publicEmbeddingProfile,
7
- saveEmbeddingProfile,
8
- resolveEmbeddingProfile,
9
- embedBatches,
10
- } from "./embedding-profile.js";
11
- import {
12
- applySpeakerNames,
13
- presentMemory,
14
- speakerNames,
15
- } from "../core/speaker-names.js";
16
-
17
- export function mountKnowledge(app, system) {
18
- const { repo } = system;
19
- app.get(
20
- "/api/core/memories/:id/permissions",
21
- wrap((req, res) => res.json(system.memory.permissions(req.params.id))),
22
- );
23
- app.patch(
24
- "/api/core/memories/:id/permissions",
25
- wrap((req, res) =>
26
- res.json(system.memory.setPermission(req.params.id, req.body)),
27
- ),
28
- );
29
- app.get("/api/core/knowledge/embedding", (req, res) =>
30
- res.json(publicEmbeddingProfile(repo)),
31
- );
32
- app.patch(
33
- "/api/core/knowledge/embedding",
34
- wrap((req, res) => res.json(saveEmbeddingProfile(repo, req.body))),
35
- );
36
- app.post(
37
- "/api/core/knowledge/embedding/test",
38
- wrap(async (req, res) => {
39
- const { profile } = resolveEmbeddingProfile(repo, system.models);
40
- if (!profile.embedding) throw Error("知识向量未启用");
41
- const vectors = await embedBatches(system.models, profile, [
42
- "星空与时间",
43
- ]);
44
- res.json({ ok: true, dimensions: vectors[0].length });
45
- }),
46
- );
47
- app.post(
48
- "/api/core/knowledge/documents/:id/reembed",
49
- wrap(async (req, res) =>
50
- res.json(await system.knowledge.reembedDocument(req.params.id)),
51
- ),
52
- );
53
- app.get("/api/core/memories", (req, res) => {
54
- const session = String(req.query.session || "");
55
- if (!session) {
56
- const rows = repo.db
57
- .prepare("SELECT * FROM core_memories ORDER BY updated DESC LIMIT 500")
58
- .all();
59
- const names = speakerNames(
60
- repo.db,
61
- rows.map((row) => row.session_id),
62
- );
63
- return res.json(rows.map((row) => presentMemory(row, names)));
64
- }
65
- const scopes = system.memory.scopes(session);
66
- const placeholders = scopes.map(() => "?").join(",");
67
- const rows = repo.db
68
- .prepare(
69
- `SELECT * FROM core_memories WHERE session_id IN (${placeholders}) ORDER BY updated DESC LIMIT 500`,
70
- )
71
- .all(...scopes);
72
- const names = speakerNames(repo.db, [session, ...scopes]);
73
- res.json(rows.map((row) => presentMemory(row, names)));
74
- });
75
- app.post(
76
- "/api/core/memories",
77
- wrap((req, res) => {
78
- const { session, subject, content } = req.body;
79
- if (
80
- typeof subject !== "string" ||
81
- !/^[0-9A-Za-z_-]{1,64}$/.test(subject) ||
82
- typeof content !== "string" ||
83
- !content.trim() ||
84
- content.length > 4000 ||
85
- !repo.db.prepare("SELECT id FROM sessions WHERE id=?").get(session)
86
- )
87
- throw Error("请选择会话、用户 ID 并填写记忆");
88
- const discretion = ["open", "private", "secret"].includes(
89
- req.body.discretion,
90
- )
91
- ? req.body.discretion
92
- : /private/.test(session)
93
- ? "private"
94
- : "open";
95
- const id = randomUUID(),
96
- now = Date.now();
97
- repo.db
98
- .prepare(
99
- "INSERT INTO core_memories(id,session_id,subject,content,type,confidence,importance,status,sources,created,updated,discretion) VALUES (?,?,?,?,'manual',1,1,'confirmed','[]',?,?,?)",
100
- )
101
- .run(id, session, subject, content, now, now, discretion);
102
- indexMemory(repo.db, id, content);
103
- repo.store.revision++;
104
- res.json({ id });
105
- }),
106
- );
107
- app.post(
108
- "/api/core/memories/batch-delete",
109
- wrap((req, res) => {
110
- const { session, ids, all } = req.body || {};
111
- const result = system.memory.deleteBatch(session, ids, {
112
- all: all === true,
113
- });
114
- res.json({ ok: true, ...result });
115
- }),
116
- );
117
- app.get(
118
- "/api/core/memory-summary",
119
- wrap((req, res) => {
120
- const session = String(req.query.session || "");
121
- if (!session) throw Error("请选择会话");
122
- const rows = repo.db
123
- .prepare(
124
- "SELECT id,first_seq,last_seq,time,data FROM core_stages WHERE session_id=? ORDER BY last_seq DESC LIMIT 3",
125
- )
126
- .all(session);
127
- const stages = rows
128
- .map((row) => {
129
- try {
130
- const data = JSON.parse(row.data);
131
- return {
132
- id: row.id,
133
- firstSeq: row.first_seq,
134
- lastSeq: row.last_seq,
135
- time: row.time,
136
- summary:
137
- typeof data.summary === "string" ? data.summary.trim() : "",
138
- };
139
- } catch {
140
- return null;
141
- }
142
- })
143
- .filter((row) => row && row.summary);
144
- const names = speakerNames(repo.db, [session]);
145
- const summary = applySpeakerNames(
146
- stages.length
147
- ? stages
148
- .map((stage) => stage.summary)
149
- .join(" ")
150
- .slice(0, 5000)
151
- : "这个会话还没有阶段性记忆总结。消息积累后会自动整理,也可以点击“立即整理”。",
152
- names,
153
- );
154
- res.json({
155
- session,
156
- summary,
157
- updated: stages[0]?.time || null,
158
- source: stages.length ? "stage" : "empty",
159
- stages: stages.map((stage) => ({
160
- ...stage,
161
- summary: applySpeakerNames(stage.summary, names),
162
- })),
163
- participants: [...names.entries()].map(([id, name]) => ({ id, name })),
164
- });
165
- }),
166
- );
167
- app.get("/api/core/stages", (req, res) =>
168
- res.json(
169
- repo.db
170
- .prepare(
171
- "SELECT * FROM core_stages WHERE session_id=? ORDER BY last_seq DESC LIMIT 100",
172
- )
173
- .all(String(req.query.session || ""))
174
- .map((r) => ({ ...r, data: JSON.parse(r.data) })),
175
- ),
176
- );
177
- app.get("/api/core/memories/:id/versions", (req, res) =>
178
- res.json(
179
- repo.db
180
- .prepare(
181
- "SELECT * FROM core_memory_versions WHERE memory_id=? ORDER BY id DESC",
182
- )
183
- .all(req.params.id),
184
- ),
185
- );
186
- app.patch(
187
- "/api/core/memories/:id",
188
- wrap((req, res) => {
189
- const allowed = {};
190
- for (const k of [
191
- "content",
192
- "status",
193
- "locked",
194
- "confidence",
195
- "importance",
196
- "expires",
197
- "discretion",
198
- ])
199
- if (k in req.body) allowed[k] = req.body[k];
200
- if (
201
- "content" in allowed &&
202
- (typeof allowed.content !== "string" ||
203
- !allowed.content.trim() ||
204
- allowed.content.length > 4000)
205
- )
206
- throw Error("记忆内容无效");
207
- if (
208
- "status" in allowed &&
209
- !["candidate", "confirmed", "disputed", "deleted", "expired"].includes(
210
- allowed.status,
211
- )
212
- )
213
- throw Error("记忆状态无效");
214
- for (const k of ["confidence", "importance"])
215
- if (
216
- k in allowed &&
217
- (!Number.isFinite(allowed[k]) || allowed[k] < 0 || allowed[k] > 1)
218
- )
219
- throw Error("置信度与重要性应在0–1");
220
- if (
221
- "expires" in allowed &&
222
- allowed.expires !== null &&
223
- (!Number.isSafeInteger(allowed.expires) || allowed.expires < 0)
224
- )
225
- throw Error("过期时间无效");
226
- if ("locked" in allowed && typeof allowed.locked !== "boolean")
227
- throw Error("锁定状态无效");
228
- system.memory.update(req.params.id, allowed);
229
- res.json({ ok: true });
230
- }),
231
- );
232
- app.post(
233
- "/api/core/memories/merge",
234
- wrap((req, res) => {
235
- const { ids, content } = req.body;
236
- if (
237
- !Array.isArray(ids) ||
238
- ids.length !== 2 ||
239
- ids[0] === ids[1] ||
240
- typeof content !== "string" ||
241
- !content.trim()
242
- )
243
- throw Error("选择两条记忆并输入合并内容");
244
- const rows = ids.map((id) =>
245
- repo.db.prepare("SELECT * FROM core_memories WHERE id=?").get(id),
246
- );
247
- if (
248
- rows.some((r) => !r || r.locked) ||
249
- rows[0].session_id !== rows[1].session_id ||
250
- rows[0].subject !== rows[1].subject
251
- )
252
- throw Error("只能合并同范围同用户的未锁定记忆");
253
- repo.db.exec("BEGIN IMMEDIATE");
254
- try {
255
- system.memory.update(ids[0], {
256
- content,
257
- sources: JSON.stringify(rows.flatMap((r) => JSON.parse(r.sources))),
258
- });
259
- system.memory.update(ids[1], { status: "deleted" });
260
- repo.db.exec("COMMIT");
261
- } catch (e) {
262
- repo.db.exec("ROLLBACK");
263
- throw e;
264
- }
265
- res.json({ ok: true });
266
- }),
267
- );
268
- app.post(
269
- "/api/core/memory/consolidate",
270
- wrap(async (req, res) => {
271
- const id = req.body.session;
272
- const t = repo.trace(id, "memory");
273
- try {
274
- await system.memory.consolidate(
275
- id,
276
- system.models.profile(),
277
- prompts(repo).memory,
278
- t,
279
- { force: true },
280
- );
281
- repo.finish(t, "complete");
282
- res.json({ ok: true });
283
- } catch (e) {
284
- t.error = e.message;
285
- repo.finish(t, "error");
286
- throw e;
287
- }
288
- }),
289
- );
290
- app.get("/api/core/knowledge/collections", (req, res) =>
291
- res.json(system.knowledge.collections(String(req.query.session || ""))),
292
- );
293
- app.post(
294
- "/api/core/knowledge/collections",
295
- wrap((req, res) => {
296
- res.json(system.knowledge.createCollection(req.body || {}));
297
- }),
298
- );
299
- app.get("/api/core/knowledge/documents", (req, res) =>
300
- res.json(system.knowledge.documents(String(req.query.collection || ""))),
301
- );
302
- app.post(
303
- "/api/core/knowledge/documents",
304
- wrap(async (req, res) => {
305
- const { collectionId, title, text, source, embed } = req.body || {};
306
- res.json(
307
- await system.knowledge.ingest({
308
- collectionId,
309
- title,
310
- text,
311
- source,
312
- embed: embed !== false,
313
- }),
314
- );
315
- }),
316
- );
317
- app.delete(
318
- "/api/core/knowledge/documents/:id",
319
- wrap((req, res) => {
320
- system.knowledge.removeDocument(req.params.id);
321
- res.json({ ok: true });
322
- }),
323
- );
324
- app.post(
325
- "/api/core/knowledge/search",
326
- wrap(async (req, res) => {
327
- const { session, text } = req.body || {};
328
- if (typeof text !== "string" || !text.trim() || text.length > 4000)
329
- throw Error("请输入 1–4000 字检索内容");
330
- if (
331
- session &&
332
- !repo.db.prepare("SELECT id FROM sessions WHERE id=?").get(session)
333
- )
334
- throw Error("会话不存在");
335
- res.json(
336
- await system.knowledge.retrieveWithEmbed(session || "", [
337
- { text: text.trim(), userId: "probe" },
338
- ]),
339
- );
340
- }),
341
- );
342
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { wrap } from "../http.js";
3
+ import { prompts } from "../core/persona-manager.js";
4
+ import { indexMemory } from "./schema.js";
5
+ import {
6
+ publicEmbeddingProfile,
7
+ saveEmbeddingProfile,
8
+ resolveEmbeddingProfile,
9
+ embedBatches,
10
+ } from "./embedding-profile.js";
11
+ import {
12
+ applySpeakerNames,
13
+ presentMemory,
14
+ speakerNames,
15
+ } from "../core/speaker-names.js";
16
+
17
+ export function mountKnowledge(app, system) {
18
+ const { repo } = system;
19
+ app.get(
20
+ "/api/core/memories/:id/permissions",
21
+ wrap((req, res) => res.json(system.memory.permissions(req.params.id))),
22
+ );
23
+ app.patch(
24
+ "/api/core/memories/:id/permissions",
25
+ wrap((req, res) =>
26
+ res.json(system.memory.setPermission(req.params.id, req.body)),
27
+ ),
28
+ );
29
+ app.get("/api/core/knowledge/embedding", (req, res) =>
30
+ res.json(publicEmbeddingProfile(repo)),
31
+ );
32
+ app.patch(
33
+ "/api/core/knowledge/embedding",
34
+ wrap((req, res) => res.json(saveEmbeddingProfile(repo, req.body))),
35
+ );
36
+ app.post(
37
+ "/api/core/knowledge/embedding/test",
38
+ wrap(async (req, res) => {
39
+ const { profile } = resolveEmbeddingProfile(repo, system.models);
40
+ if (!profile.embedding) throw Error("知识向量未启用");
41
+ const vectors = await embedBatches(system.models, profile, [
42
+ "星空与时间",
43
+ ]);
44
+ res.json({ ok: true, dimensions: vectors[0].length });
45
+ }),
46
+ );
47
+ app.post(
48
+ "/api/core/knowledge/documents/:id/reembed",
49
+ wrap(async (req, res) =>
50
+ res.json(await system.knowledge.reembedDocument(req.params.id)),
51
+ ),
52
+ );
53
+ app.get("/api/core/memories", (req, res) => {
54
+ const session = String(req.query.session || "");
55
+ if (!session) {
56
+ const rows = repo.db
57
+ .prepare("SELECT * FROM core_memories ORDER BY updated DESC LIMIT 500")
58
+ .all();
59
+ const names = speakerNames(
60
+ repo.db,
61
+ rows.map((row) => row.session_id),
62
+ );
63
+ return res.json(rows.map((row) => presentMemory(row, names)));
64
+ }
65
+ const scopes = system.memory.scopes(session);
66
+ const placeholders = scopes.map(() => "?").join(",");
67
+ const rows = repo.db
68
+ .prepare(
69
+ `SELECT * FROM core_memories WHERE session_id IN (${placeholders}) ORDER BY updated DESC LIMIT 500`,
70
+ )
71
+ .all(...scopes);
72
+ const names = speakerNames(repo.db, [session, ...scopes]);
73
+ res.json(rows.map((row) => presentMemory(row, names)));
74
+ });
75
+ app.post(
76
+ "/api/core/memories",
77
+ wrap((req, res) => {
78
+ const { session, subject, content } = req.body;
79
+ if (
80
+ typeof subject !== "string" ||
81
+ !/^[0-9A-Za-z_-]{1,64}$/.test(subject) ||
82
+ typeof content !== "string" ||
83
+ !content.trim() ||
84
+ content.length > 4000 ||
85
+ !repo.db.prepare("SELECT id FROM sessions WHERE id=?").get(session)
86
+ )
87
+ throw Error("请选择会话、用户 ID 并填写记忆");
88
+ const discretion = ["open", "private", "secret"].includes(
89
+ req.body.discretion,
90
+ )
91
+ ? req.body.discretion
92
+ : /private/.test(session)
93
+ ? "private"
94
+ : "open";
95
+ const id = randomUUID(),
96
+ now = Date.now();
97
+ repo.db
98
+ .prepare(
99
+ "INSERT INTO core_memories(id,session_id,subject,content,type,confidence,importance,status,sources,created,updated,discretion) VALUES (?,?,?,?,'manual',1,1,'confirmed','[]',?,?,?)",
100
+ )
101
+ .run(id, session, subject, content, now, now, discretion);
102
+ indexMemory(repo.db, id, content);
103
+ repo.store.revision++;
104
+ res.json({ id });
105
+ }),
106
+ );
107
+ app.post(
108
+ "/api/core/memories/batch-delete",
109
+ wrap((req, res) => {
110
+ const { session, ids, all } = req.body || {};
111
+ const result = system.memory.deleteBatch(session, ids, {
112
+ all: all === true,
113
+ });
114
+ res.json({ ok: true, ...result });
115
+ }),
116
+ );
117
+ app.get(
118
+ "/api/core/memory-summary",
119
+ wrap((req, res) => {
120
+ const session = String(req.query.session || "");
121
+ if (!session) throw Error("请选择会话");
122
+ const rows = repo.db
123
+ .prepare(
124
+ "SELECT id,first_seq,last_seq,time,data FROM core_stages WHERE session_id=? ORDER BY last_seq DESC LIMIT 3",
125
+ )
126
+ .all(session);
127
+ const stages = rows
128
+ .map((row) => {
129
+ try {
130
+ const data = JSON.parse(row.data);
131
+ return {
132
+ id: row.id,
133
+ firstSeq: row.first_seq,
134
+ lastSeq: row.last_seq,
135
+ time: row.time,
136
+ summary:
137
+ typeof data.summary === "string" ? data.summary.trim() : "",
138
+ };
139
+ } catch {
140
+ return null;
141
+ }
142
+ })
143
+ .filter((row) => row && row.summary);
144
+ const names = speakerNames(repo.db, [session]);
145
+ const summary = applySpeakerNames(
146
+ stages.length
147
+ ? stages
148
+ .map((stage) => stage.summary)
149
+ .join(" ")
150
+ .slice(0, 5000)
151
+ : "这个会话还没有阶段性记忆总结。消息积累后会自动整理,也可以点击“立即整理”。",
152
+ names,
153
+ );
154
+ res.json({
155
+ session,
156
+ summary,
157
+ updated: stages[0]?.time || null,
158
+ source: stages.length ? "stage" : "empty",
159
+ stages: stages.map((stage) => ({
160
+ ...stage,
161
+ summary: applySpeakerNames(stage.summary, names),
162
+ })),
163
+ participants: [...names.entries()].map(([id, name]) => ({ id, name })),
164
+ });
165
+ }),
166
+ );
167
+ app.get("/api/core/stages", (req, res) =>
168
+ res.json(
169
+ repo.db
170
+ .prepare(
171
+ "SELECT * FROM core_stages WHERE session_id=? ORDER BY last_seq DESC LIMIT 100",
172
+ )
173
+ .all(String(req.query.session || ""))
174
+ .map((r) => ({ ...r, data: JSON.parse(r.data) })),
175
+ ),
176
+ );
177
+ app.get("/api/core/memories/:id/versions", (req, res) =>
178
+ res.json(
179
+ repo.db
180
+ .prepare(
181
+ "SELECT * FROM core_memory_versions WHERE memory_id=? ORDER BY id DESC",
182
+ )
183
+ .all(req.params.id),
184
+ ),
185
+ );
186
+ app.patch(
187
+ "/api/core/memories/:id",
188
+ wrap((req, res) => {
189
+ const allowed = {};
190
+ for (const k of [
191
+ "content",
192
+ "status",
193
+ "locked",
194
+ "confidence",
195
+ "importance",
196
+ "expires",
197
+ "discretion",
198
+ ])
199
+ if (k in req.body) allowed[k] = req.body[k];
200
+ if (
201
+ "content" in allowed &&
202
+ (typeof allowed.content !== "string" ||
203
+ !allowed.content.trim() ||
204
+ allowed.content.length > 4000)
205
+ )
206
+ throw Error("记忆内容无效");
207
+ if (
208
+ "status" in allowed &&
209
+ !["candidate", "confirmed", "disputed", "deleted", "expired"].includes(
210
+ allowed.status,
211
+ )
212
+ )
213
+ throw Error("记忆状态无效");
214
+ for (const k of ["confidence", "importance"])
215
+ if (
216
+ k in allowed &&
217
+ (!Number.isFinite(allowed[k]) || allowed[k] < 0 || allowed[k] > 1)
218
+ )
219
+ throw Error("置信度与重要性应在0–1");
220
+ if (
221
+ "expires" in allowed &&
222
+ allowed.expires !== null &&
223
+ (!Number.isSafeInteger(allowed.expires) || allowed.expires < 0)
224
+ )
225
+ throw Error("过期时间无效");
226
+ if ("locked" in allowed && typeof allowed.locked !== "boolean")
227
+ throw Error("锁定状态无效");
228
+ system.memory.update(req.params.id, allowed);
229
+ res.json({ ok: true });
230
+ }),
231
+ );
232
+ app.post(
233
+ "/api/core/memories/merge",
234
+ wrap((req, res) => {
235
+ const { ids, content } = req.body;
236
+ if (
237
+ !Array.isArray(ids) ||
238
+ ids.length !== 2 ||
239
+ ids[0] === ids[1] ||
240
+ typeof content !== "string" ||
241
+ !content.trim()
242
+ )
243
+ throw Error("选择两条记忆并输入合并内容");
244
+ const rows = ids.map((id) =>
245
+ repo.db.prepare("SELECT * FROM core_memories WHERE id=?").get(id),
246
+ );
247
+ if (
248
+ rows.some((r) => !r || r.locked) ||
249
+ rows[0].session_id !== rows[1].session_id ||
250
+ rows[0].subject !== rows[1].subject
251
+ )
252
+ throw Error("只能合并同范围同用户的未锁定记忆");
253
+ repo.db.exec("BEGIN IMMEDIATE");
254
+ try {
255
+ system.memory.update(ids[0], {
256
+ content,
257
+ sources: JSON.stringify(rows.flatMap((r) => JSON.parse(r.sources))),
258
+ });
259
+ system.memory.update(ids[1], { status: "deleted" });
260
+ repo.db.exec("COMMIT");
261
+ } catch (e) {
262
+ repo.db.exec("ROLLBACK");
263
+ throw e;
264
+ }
265
+ res.json({ ok: true });
266
+ }),
267
+ );
268
+ app.post(
269
+ "/api/core/memory/consolidate",
270
+ wrap(async (req, res) => {
271
+ const id = req.body.session;
272
+ const t = repo.trace(id, "memory");
273
+ try {
274
+ await system.memory.consolidate(
275
+ id,
276
+ system.models.profile(),
277
+ prompts(repo).memory,
278
+ t,
279
+ { force: true },
280
+ );
281
+ repo.finish(t, "complete");
282
+ res.json({ ok: true });
283
+ } catch (e) {
284
+ t.error = e.message;
285
+ repo.finish(t, "error");
286
+ throw e;
287
+ }
288
+ }),
289
+ );
290
+ app.get("/api/core/knowledge/collections", (req, res) =>
291
+ res.json(system.knowledge.collections(String(req.query.session || ""))),
292
+ );
293
+ app.post(
294
+ "/api/core/knowledge/collections",
295
+ wrap((req, res) => {
296
+ res.json(system.knowledge.createCollection(req.body || {}));
297
+ }),
298
+ );
299
+ app.get("/api/core/knowledge/documents", (req, res) =>
300
+ res.json(system.knowledge.documents(String(req.query.collection || ""))),
301
+ );
302
+ app.post(
303
+ "/api/core/knowledge/documents",
304
+ wrap(async (req, res) => {
305
+ const { collectionId, title, text, source, embed } = req.body || {};
306
+ res.json(
307
+ await system.knowledge.ingest({
308
+ collectionId,
309
+ title,
310
+ text,
311
+ source,
312
+ embed: embed !== false,
313
+ }),
314
+ );
315
+ }),
316
+ );
317
+ app.delete(
318
+ "/api/core/knowledge/documents/:id",
319
+ wrap((req, res) => {
320
+ system.knowledge.removeDocument(req.params.id);
321
+ res.json({ ok: true });
322
+ }),
323
+ );
324
+ app.post(
325
+ "/api/core/knowledge/search",
326
+ wrap(async (req, res) => {
327
+ const { session, text } = req.body || {};
328
+ if (typeof text !== "string" || !text.trim() || text.length > 4000)
329
+ throw Error("请输入 1–4000 字检索内容");
330
+ if (
331
+ session &&
332
+ !repo.db.prepare("SELECT id FROM sessions WHERE id=?").get(session)
333
+ )
334
+ throw Error("会话不存在");
335
+ res.json(
336
+ await system.knowledge.retrieveWithEmbed(session || "", [
337
+ { text: text.trim(), userId: "probe" },
338
+ ]),
339
+ );
340
+ }),
341
+ );
342
+ }