luckytri 1.0.8 → 1.1.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 (100) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/en/CHANGELOG.md +44 -24
  4. package/docs/en/architecture.md +4 -0
  5. package/docs/en/cognition.md +34 -0
  6. package/docs/en/guide.md +8 -6
  7. package/docs/zh/CHANGELOG.md +44 -24
  8. package/docs/zh/architecture.md +4 -0
  9. package/docs/zh/cognition.md +34 -0
  10. package/docs/zh/guide.md +8 -6
  11. package/package.json +112 -112
  12. package/public/app/assets/ChatsPage-DH4ZPRXB.js +3 -0
  13. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-DRsHeX-L.js} +1 -1
  14. package/public/app/assets/Field-BfIUcaNa.js +1 -0
  15. package/public/app/assets/HeartPage-BxgSUJJ_.js +1 -0
  16. package/public/app/assets/LifePage-CAKr8h_q.js +1 -0
  17. package/public/app/assets/MemoryPage-CFq8L_TC.js +1 -0
  18. package/public/app/assets/NaturePage-BU1iDno3.js +3 -0
  19. package/public/app/assets/PeoplePage-C6i_nAfr.js +1 -0
  20. package/public/app/assets/PluginsPage-DRV8lzk3.js +2 -0
  21. package/public/app/assets/SystemPage-21AlQBiC.js +1 -0
  22. package/public/app/assets/SystemPage-CMcuUdLm.css +1 -0
  23. package/public/app/assets/TimePage-DUzjLnZV.js +3 -0
  24. package/public/app/assets/index-WAvrlAcO.js +4 -0
  25. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-V_blbx9R.js} +1 -1
  26. package/public/app/assets/media-C7j5HqxK.js +1 -0
  27. package/public/app/assets/{mind-DtTVrW6Z.js → mind-BBAstMqH.js} +1 -1
  28. package/public/app/index.html +3 -3
  29. package/public/guide.en.html +13 -5
  30. package/public/guide.html +12 -5
  31. package/public/tutorial.en.md +3 -1
  32. package/public/tutorial.md +3 -1
  33. package/server/core/backoff.js +123 -0
  34. package/server/core/context-builder.js +307 -288
  35. package/server/core/contribution-check.js +50 -0
  36. package/server/core/conversation-cues.js +377 -385
  37. package/server/core/conversation-grounding.js +547 -304
  38. package/server/core/dialogue-context.js +247 -244
  39. package/server/core/dialogue-contract.js +33 -9
  40. package/server/core/embedding.js +246 -0
  41. package/server/core/expression-strategy.js +90 -0
  42. package/server/core/initiative-context.js +88 -68
  43. package/server/core/model-json.js +223 -190
  44. package/server/core/model-manager.js +256 -56
  45. package/server/core/model-presets.js +1045 -900
  46. package/server/core/orchestrator.js +41 -3
  47. package/server/core/persona-manager.js +364 -359
  48. package/server/core/reader-check.js +166 -62
  49. package/server/core/reply-delivery.js +688 -537
  50. package/server/core/repository.js +36 -0
  51. package/server/core/response-generator.js +101 -92
  52. package/server/core/response-validator.js +18 -3
  53. package/server/core/retired-prompts.js +53 -51
  54. package/server/core/time-expressions.js +143 -0
  55. package/server/core/turn-processor.js +175 -5
  56. package/server/core/turn.js +299 -44
  57. package/server/core/understanding.js +85 -0
  58. package/server/core/working-memory.js +149 -0
  59. package/server/i18n/decisions.js +225 -209
  60. package/server/i18n/errors.js +14 -0
  61. package/server/i18n/models.js +120 -111
  62. package/server/knowledge/api.js +362 -342
  63. package/server/knowledge/embedding-profile.js +158 -114
  64. package/server/knowledge/manager.js +6 -1
  65. package/server/mind/api.js +68 -0
  66. package/server/mind/continuity.js +634 -562
  67. package/server/mind/day-planner.js +52 -3
  68. package/server/mind/episodes.js +242 -0
  69. package/server/mind/expression-grounding.js +24 -21
  70. package/server/mind/index.js +13 -2
  71. package/server/mind/initiative.js +356 -338
  72. package/server/mind/life/index.js +6 -0
  73. package/server/mind/meetings.js +1112 -1069
  74. package/server/mind/memory.js +1091 -888
  75. package/server/mind/recall/index.js +776 -0
  76. package/server/mind/recall/schema.js +132 -0
  77. package/server/mind/recall/sources.js +220 -0
  78. package/server/mind/recall/store.js +368 -0
  79. package/server/mind/salience.js +202 -201
  80. package/server/mind/schema.js +568 -567
  81. package/server/mind/self.js +537 -496
  82. package/server/mind/time/activity-recall.js +65 -0
  83. package/server/mind/time/index.js +6 -2
  84. package/server/mind/time/tasks.js +36 -10
  85. package/server/mind/time/works.js +528 -527
  86. package/server/mind/view.js +63 -17
  87. package/server/storage/archive.js +1 -1
  88. package/public/app/assets/ChatsPage-CshdtTzV.js +0 -3
  89. package/public/app/assets/Field-BjFv4Rfn.js +0 -1
  90. package/public/app/assets/HeartPage-eqo6ru16.js +0 -1
  91. package/public/app/assets/LifePage-CN8GE5NK.js +0 -1
  92. package/public/app/assets/MemoryPage-CCpMN4Wg.js +0 -1
  93. package/public/app/assets/NaturePage-C0JqdBTR.js +0 -3
  94. package/public/app/assets/PeoplePage-BXGBbO4u.js +0 -1
  95. package/public/app/assets/PluginsPage-Dksp5AV5.js +0 -2
  96. package/public/app/assets/SystemPage-BenIyTvk.css +0 -1
  97. package/public/app/assets/SystemPage-Cd7aa8eE.js +0 -1
  98. package/public/app/assets/TimePage-Bs_zImb4.js +0 -3
  99. package/public/app/assets/index-CyRtYxP3.js +0 -4
  100. package/public/app/assets/media-Bwtx8blz.js +0 -1
@@ -1,527 +1,528 @@
1
- import { randomUUID } from "node:crypto";
2
- import { evidence, hasCredential, parse, text } from "../util.js";
3
- import { activityPresentation, gameText } from "./presentation.js";
4
- import { interestTerms } from "../attention.js";
5
- import { belongsToGame } from "./material-relevance.js";
6
- import { gameTopic } from "./intent.js";
7
-
8
- const decode = (row) =>
9
- row
10
- ? {
11
- ...activityPresentation(row),
12
- sources: parse(row.sources, []),
13
- bible: parse(row.bible, {}),
14
- }
15
- : null;
16
- export class Works {
17
- constructor(time) {
18
- this.time = time;
19
- this.db = time.db;
20
- this.migrate();
21
- this.linkLegacy();
22
- }
23
- linkLegacy() {
24
- for (const row of this.db
25
- .prepare(
26
- "SELECT w.id,c.plan_id FROM mind_time_works w JOIN mind_creations c ON c.id=w.legacy_creation WHERE w.task_id IS NULL",
27
- )
28
- .all()) {
29
- const task = this.db
30
- .prepare(
31
- "SELECT id FROM mind_time_tasks WHERE anticipation_id=? OR id=? ORDER BY created LIMIT 1",
32
- )
33
- .get(row.plan_id, row.plan_id);
34
- if (!task) continue;
35
- this.db
36
- .prepare("UPDATE mind_time_works SET task_id=? WHERE id=?")
37
- .run(task.id, row.id);
38
- this.db
39
- .prepare(
40
- "UPDATE mind_time_tasks SET work_id=COALESCE(work_id,?) WHERE id=?",
41
- )
42
- .run(row.id, task.id);
43
- }
44
- }
45
- migrate() {
46
- for (const row of this.db
47
- .prepare(
48
- "SELECT * FROM mind_creations WHERE id NOT IN (SELECT legacy_creation FROM mind_time_works WHERE legacy_creation IS NOT NULL)",
49
- )
50
- .all()) {
51
- const id = randomUUID();
52
- this.db.exec("SAVEPOINT migrate_work");
53
- try {
54
- this.db
55
- .prepare(
56
- "INSERT INTO mind_time_works(id,created,updated,title,state,version,session_id,discretion,legacy_creation) VALUES (?,?,?,?,'complete',1,?,?,?)",
57
- )
58
- .run(
59
- id,
60
- row.created,
61
- row.created,
62
- row.title,
63
- row.session_id,
64
- row.discretion,
65
- row.id,
66
- );
67
- this.db
68
- .prepare(
69
- "INSERT INTO mind_time_versions(id,work_id,version,created,title,content,sources,run_id) VALUES (?,?,1,?,?,?,?,?)",
70
- )
71
- .run(
72
- randomUUID(),
73
- id,
74
- row.created,
75
- row.title,
76
- row.content,
77
- row.sources,
78
- row.run_id,
79
- );
80
- this.db.exec("RELEASE migrate_work");
81
- } catch (error) {
82
- this.db.exec("ROLLBACK TO migrate_work; RELEASE migrate_work");
83
- throw error;
84
- }
85
- }
86
- }
87
- project(id) {
88
- return decode(
89
- this.db.prepare("SELECT * FROM mind_time_projects WHERE id=?").get(id),
90
- );
91
- }
92
- projects({ limit = 30, offset = 0, q = "" } = {}) {
93
- return this.db
94
- .prepare(
95
- "SELECT * FROM mind_time_projects WHERE title LIKE ? ORDER BY updated DESC LIMIT ? OFFSET ?",
96
- )
97
- .all(
98
- "%" + text(q, 80) + "%",
99
- Math.max(1, Math.min(100, Number(limit))),
100
- Math.max(0, Number(offset)),
101
- )
102
- .map(decode);
103
- }
104
- ensureProject(task, now) {
105
- if (task.project_id) return this.project(task.project_id);
106
- const id = randomUUID();
107
- this.db
108
- .prepare(
109
- "INSERT INTO mind_time_projects(id,created,updated,kind,title,why,session_id,discretion,sources,serial) VALUES (?,?,?,?,?,?,?,?,?,?)",
110
- )
111
- .run(
112
- id,
113
- now,
114
- now,
115
- task.activity,
116
- task.title,
117
- task.why,
118
- task.session_id,
119
- task.discretion,
120
- JSON.stringify(task.sources),
121
- /连载|续写|长篇|系列/.test(task.title) ? 1 : 0,
122
- );
123
- this.db
124
- .prepare("UPDATE mind_time_tasks SET project_id=? WHERE id=?")
125
- .run(id, task.id);
126
- task.project_id = id;
127
- return this.project(id);
128
- }
129
- list({ q = "", project = "", limit = 30, offset = 0 } = {}) {
130
- return this.db
131
- .prepare(
132
- `SELECT w.*,COALESCE(p.kind,(SELECT kind FROM mind_creations WHERE id=w.legacy_creation)) kind,p.title project_title,v.summary,v.sources,length(v.content) characters FROM mind_time_works w LEFT JOIN mind_time_projects p ON p.id=w.project_id JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE (w.title LIKE ? OR p.title LIKE ?) ${project ? "AND w.project_id=?" : ""} ORDER BY w.updated DESC LIMIT ? OFFSET ?`,
133
- )
134
- .all(
135
- "%" + text(q, 80) + "%",
136
- "%" + text(q, 80) + "%",
137
- ...(project ? [project] : []),
138
- Math.max(1, Math.min(100, Number(limit))),
139
- Math.max(0, Number(offset)),
140
- )
141
- .map((row) =>
142
- row.kind === "game"
143
- ? {
144
- ...row,
145
- title: gameText(row.title),
146
- summary: gameText(row.summary),
147
- }
148
- : row,
149
- );
150
- }
151
- get(id, version, { before = Number.MAX_SAFE_INTEGER } = {}) {
152
- const work = this.db
153
- .prepare("SELECT * FROM mind_time_works WHERE id=?")
154
- .get(id);
155
- if (!work) return null;
156
- const v = this.db
157
- .prepare("SELECT * FROM mind_time_versions WHERE work_id=? AND version=?")
158
- .get(id, Number(version) || work.version);
159
- const saved = v
160
- ? this.db
161
- .prepare(
162
- "SELECT reason FROM mind_time_events WHERE kind='draft' AND json_extract(data,'$.workId')=? AND json_extract(data,'$.version')=? AND created<=? ORDER BY id DESC LIMIT 1",
163
- )
164
- .get(id, v.version, before)
165
- : null;
166
- return v
167
- ? {
168
- ...work,
169
- ...v,
170
- id: work.id,
171
- versionId: v.id,
172
- state: saved
173
- ? saved.reason === "保存完成稿"
174
- ? "complete"
175
- : "draft"
176
- : work.updated > before
177
- ? "draft"
178
- : work.state,
179
- sources: parse(v.sources, []),
180
- provenance: parse(v.provenance, {}),
181
- versions: this.db
182
- .prepare(
183
- "SELECT version,created,title,length(content) characters FROM mind_time_versions WHERE work_id=? ORDER BY version DESC",
184
- )
185
- .all(id),
186
- }
187
- : null;
188
- }
189
- context(task) {
190
- const project = this.project(task.project_id);
191
- if (!project) return {};
192
- const work = task.checkpoint.workId
193
- ? this.get(task.checkpoint.workId)
194
- : null;
195
- const chapters = this.list({ project: project.id, limit: 3 }).sort(
196
- (a, b) => a.ordinal - b.ordinal,
197
- );
198
- return {
199
- project: {
200
- id: project.id,
201
- title: project.title,
202
- why: project.why,
203
- bible: project.bible,
204
- summary: project.summary,
205
- serial: !!project.serial,
206
- },
207
- previous: chapters
208
- .filter((w) => w.id !== work?.id)
209
- .map((w) => ({
210
- ordinal: w.ordinal,
211
- title: w.title,
212
- summary: w.summary,
213
- ending: this.get(w.id)?.content.slice(-900),
214
- })),
215
- draft: work
216
- ? {
217
- title: work.title,
218
- version: work.version,
219
- summary: work.summary,
220
- ending: work.content.slice(-1800),
221
- }
222
- : null,
223
- };
224
- }
225
- save(task, result, { sources, runId, now, provenance = {} }) {
226
- if (!this.time.valid(task)) throw Error("任务租约已经改变");
227
- const title = text(result.title, 80),
228
- segment = text(result.content, 1800);
229
- if (!title || !segment || hasCredential(title + segment))
230
- throw Error("作品正文格式无效");
231
- const project = this.ensureProject(task, now),
232
- existing = task.checkpoint.workId
233
- ? this.get(task.checkpoint.workId)
234
- : null;
235
- const id = existing?.id || randomUUID(),
236
- version = (existing?.version || 0) + 1;
237
- const content =
238
- task.checkpoint.intent === "revise"
239
- ? segment
240
- : (existing ? existing.content + "\n\n" : "") + segment;
241
- if (content.length > 100000) throw Error("单篇过长,请接续新篇章");
242
- const complete = result.done === true,
243
- state = complete ? "complete" : "draft";
244
- if (!existing) {
245
- const ordinal = this.db
246
- .prepare(
247
- "SELECT COALESCE(MAX(ordinal),0)+1 n FROM mind_time_works WHERE project_id=?",
248
- )
249
- .get(project.id).n;
250
- this.db
251
- .prepare(
252
- "INSERT INTO mind_time_works(id,created,updated,task_id,project_id,ordinal,title,state,version,session_id,discretion) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
253
- )
254
- .run(
255
- id,
256
- now,
257
- now,
258
- task.id,
259
- project.id,
260
- ordinal,
261
- title,
262
- state,
263
- version,
264
- task.session_id,
265
- task.discretion,
266
- );
267
- } else
268
- this.db
269
- .prepare(
270
- "UPDATE mind_time_works SET updated=?,title=?,state=?,version=? WHERE id=?",
271
- )
272
- .run(now, title, state, version, id);
273
- const summary = text(result.summary || segment, 500),
274
- checkpoint = {
275
- ...task.checkpoint,
276
- workId: id,
277
- version,
278
- summary,
279
- next: text(result.next, 240) || "接着上一段往下写",
280
- segments: (task.checkpoint.segments || 0) + 1,
281
- };
282
- this.db
283
- .prepare(
284
- "INSERT INTO mind_time_versions(id,work_id,version,created,title,content,summary,sources,run_id,provenance) VALUES (?,?,?,?,?,?,?,?,?,?)",
285
- )
286
- .run(
287
- randomUUID(),
288
- id,
289
- version,
290
- now,
291
- title,
292
- content,
293
- summary,
294
- JSON.stringify(evidence(sources)),
295
- runId,
296
- JSON.stringify(provenance),
297
- );
298
- // Only supplied continuity fields change; omitted fields never erase established settings.
299
- const bible = { ...project.bible };
300
- for (const key of ["characters", "world", "threads"])
301
- if (result.bible?.[key] !== undefined)
302
- bible[key] = text(
303
- typeof result.bible[key] === "string"
304
- ? result.bible[key]
305
- : JSON.stringify(result.bible[key]),
306
- 2000,
307
- );
308
- this.db
309
- .prepare(
310
- "UPDATE mind_time_projects SET updated=?,summary=?,bible=?,revision=revision+1 WHERE id=?",
311
- )
312
- .run(now, summary, JSON.stringify(bible), project.id);
313
- this.db
314
- .prepare("UPDATE mind_time_tasks SET checkpoint=?,work_id=? WHERE id=?")
315
- .run(JSON.stringify(checkpoint), id, task.id);
316
- this.time.event(
317
- task.id,
318
- "draft",
319
- complete ? "保存完成稿" : "保存一个完整小段",
320
- { workId: id, version, characters: content.length },
321
- now,
322
- );
323
- return this.get(id);
324
- }
325
- settle(task, workId, complete, now) {
326
- if (!this.time.valid(task)) throw Error("任务租约已经改变");
327
- const work = this.get(workId);
328
- if (!work || work.task_id !== task.id) throw Error("活动草稿已不可用");
329
- if (complete) {
330
- this.db
331
- .prepare(
332
- "UPDATE mind_time_works SET state='complete',updated=? WHERE id=?",
333
- )
334
- .run(now, workId);
335
- this.time.event(
336
- task.id,
337
- "draft",
338
- "保存完成稿",
339
- { workId, version: work.version, characters: work.content.length },
340
- now,
341
- );
342
- }
343
- return this.get(workId);
344
- }
345
- suggest(
346
- projectId,
347
- { action = "continue", idea = "", why = "管理台提出的建议" } = {},
348
- now = this.time.now(),
349
- ) {
350
- const project = this.project(projectId);
351
- if (!project) throw Error("项目不存在");
352
- if (
353
- !["continue", "revise", "pause", "finish", "direction"].includes(action)
354
- )
355
- throw Error("建议类型无效");
356
- const pending = this.db
357
- .prepare(
358
- "SELECT id FROM mind_time_tasks WHERE project_id=? AND state NOT IN ('done','abandoned') LIMIT 1",
359
- )
360
- .get(projectId);
361
- if (pending) return { duplicate: pending.id };
362
- const result = this.time.tasks.add(
363
- {
364
- kind: "suggestion",
365
- activeOnly: true,
366
- activity: project.kind,
367
- title: `${action === "revise" ? "修改" : "接续"}《${project.title}》${text(idea, 120)}`,
368
- why: text(why, 240),
369
- sources: project.sources,
370
- session: project.session_id,
371
- discretion: project.discretion,
372
- projectId,
373
- },
374
- now,
375
- );
376
- if (result.id) {
377
- const latest = this.list({ project: projectId, limit: 1 })[0];
378
- this.db.prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?").run(
379
- JSON.stringify({
380
- suggestion: {
381
- action,
382
- idea: text(idea, 500),
383
- origin: "admin",
384
- accepted: null,
385
- },
386
- ...(action === "revise" && latest
387
- ? { workId: latest.id, intent: "revise" }
388
- : {}),
389
- }),
390
- result.id,
391
- );
392
- this.time.event(
393
- result.id,
394
- "suggestion",
395
- "外部建议,等待她决定是否采纳",
396
- { action },
397
- now,
398
- );
399
- }
400
- return result;
401
- }
402
- fragments({ session, now = this.time.now(), cue = [] } = {}) {
403
- const words = (Array.isArray(cue) ? cue : [cue])
404
- .map((v) => (typeof v === "string" ? v : v?.text || ""))
405
- .join(" ");
406
- const terms = interestTerms([words]);
407
- // Search the work catalogue, not just the two newest activities. Select
408
- // the version that existed at the requested time, including in replays.
409
- return this.db
410
- .prepare(
411
- `SELECT w.*,COALESCE(p.kind,(SELECT kind FROM mind_creations WHERE id=w.legacy_creation)) kind,p.title project_title,p.bible,v.version,v.title,v.created version_created,v.summary,v.sources,v.provenance,length(v.content) characters
412
- FROM mind_time_works w LEFT JOIN mind_time_projects p ON p.id=w.project_id
413
- JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
414
- WHERE w.created<=? ORDER BY v.created DESC`,
415
- )
416
- .all(now, now)
417
- .filter(
418
- (w) =>
419
- w.created <= now &&
420
- this.time.visible(
421
- { ...w, sources: parse(w.sources, []) },
422
- session,
423
- now,
424
- ),
425
- )
426
- .map((w) => ({
427
- ...w,
428
- score: [...interestTerms([w.title, w.project_title, w.summary])]
429
- .filter((term) => terms.has(term))
430
- .reduce(
431
- (total, term) => total + (/^[a-z][a-z0-9]{2,}$/.test(term) ? 8 : 1),
432
- 0,
433
- ),
434
- }))
435
- .sort(
436
- (a, b) => b.score - a.score || b.version_created - a.version_created,
437
- )
438
- .filter((w, index, rows) =>
439
- rows[0]?.score >= 2 ? w.score >= 2 : index < 2,
440
- )
441
- .filter((w) => this.materialSupported(w))
442
- .slice(0, 3)
443
- .map((w) => ({
444
- id: w.id,
445
- title: w.title,
446
- state: this.get(w.id, w.version, { before: now }).state,
447
- kind: w.kind,
448
- version: w.version,
449
- ordinal: w.ordinal,
450
- summary: text(w.summary, 180),
451
- fragment: text(this.get(w.id, w.version).content, 300),
452
- ...(w.kind === "game"
453
- ? {
454
- experienceMode: "reference",
455
- provenance: this.get(w.id, w.version).provenance,
456
- }
457
- : {}),
458
- characters: w.characters,
459
- }));
460
- }
461
-
462
- activityEvidence({ session, now = this.time.now(), cue = [] } = {}) {
463
- const terms = interestTerms(
464
- (Array.isArray(cue) ? cue : [cue]).map((v) =>
465
- typeof v === "string"
466
- ? v
467
- : v?.role === "assistant"
468
- ? ""
469
- : v?.text || "",
470
- ),
471
- );
472
- if (!terms.size) return [];
473
- // A private conversation can motivate her own reading. Its contents and
474
- // promises stay private; absence from the public fragments must not erase
475
- // the fact that the explicitly named reading activity took place.
476
- return this.db
477
- .prepare(
478
- `SELECT w.id,w.title,w.discretion,w.session_id,p.kind,p.title project_title,p.bible,v.created,v.provenance,v.sources
479
- FROM mind_time_works w JOIN mind_time_projects p ON p.id=w.project_id
480
- JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
481
- WHERE w.created<=? AND p.kind='game' AND w.discretion!='secret' ORDER BY v.created DESC`,
482
- )
483
- .all(now, now)
484
- .filter((w) => {
485
- const shared = [...interestTerms([w.title, w.project_title])].filter(
486
- (term) => terms.has(term),
487
- );
488
- return (
489
- (shared.length >= 2 ||
490
- shared.some((term) => /^[a-z][a-z0-9]{2,}$/.test(term))) &&
491
- this.time.allowed({ ...w, sources: parse(w.sources, []) }, now) &&
492
- this.materialSupported(w)
493
- );
494
- })
495
- .slice(0, 3)
496
- .map((w) => ({
497
- title: w.title,
498
- activity: "阅读游戏资料并写笔记",
499
- experienceMode: "reference",
500
- ...(w.session_id !== session && w.discretion !== "open"
501
- ? { privateOrigin: true }
502
- : {}),
503
- instruction:
504
- "仅用于核对自己确有这项阅读和笔记,不能据此否认从未发生;这里不提供私下正文、发起人、约定或对话。只能自然回应本轮对方已经提到的阅读本身,不披露私下内容。",
505
- }));
506
- }
507
-
508
- materialSupported(work) {
509
- const project = work.kind ? null : this.project(work.project_id);
510
- if ((work.kind || project?.kind) !== "game") return true;
511
- const ids = parse(work.provenance, {}).sourceIds;
512
- const topic =
513
- parse(work.bible, {}).topic ||
514
- project?.bible?.topic ||
515
- gameTopic(work.project_title || project?.title);
516
- // Missing old provenance is unknown, not evidence that reading never took
517
- // place. Known references all belonging to another subject are different.
518
- if (!Array.isArray(ids) || !ids.length || !topic) return true;
519
- const source = this.db.prepare(
520
- "SELECT title,url,content FROM mind_time_sources WHERE id=?",
521
- );
522
- return ids.some((id) => {
523
- const row = source.get(id);
524
- return row && belongsToGame(row, topic);
525
- });
526
- }
527
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { evidence, hasCredential, parse, text } from "../util.js";
3
+ import { activityPresentation, gameText } from "./presentation.js";
4
+ import { interestTerms } from "../attention.js";
5
+ import { belongsToGame } from "./material-relevance.js";
6
+ import { gameTopic } from "./intent.js";
7
+
8
+ const decode = (row) =>
9
+ row
10
+ ? {
11
+ ...activityPresentation(row),
12
+ sources: parse(row.sources, []),
13
+ bible: parse(row.bible, {}),
14
+ }
15
+ : null;
16
+ export class Works {
17
+ constructor(time) {
18
+ this.time = time;
19
+ this.db = time.db;
20
+ this.migrate();
21
+ this.linkLegacy();
22
+ }
23
+ linkLegacy() {
24
+ for (const row of this.db
25
+ .prepare(
26
+ "SELECT w.id,c.plan_id FROM mind_time_works w JOIN mind_creations c ON c.id=w.legacy_creation WHERE w.task_id IS NULL",
27
+ )
28
+ .all()) {
29
+ const task = this.db
30
+ .prepare(
31
+ "SELECT id FROM mind_time_tasks WHERE anticipation_id=? OR id=? ORDER BY created LIMIT 1",
32
+ )
33
+ .get(row.plan_id, row.plan_id);
34
+ if (!task) continue;
35
+ this.db
36
+ .prepare("UPDATE mind_time_works SET task_id=? WHERE id=?")
37
+ .run(task.id, row.id);
38
+ this.db
39
+ .prepare(
40
+ "UPDATE mind_time_tasks SET work_id=COALESCE(work_id,?) WHERE id=?",
41
+ )
42
+ .run(row.id, task.id);
43
+ }
44
+ }
45
+ migrate() {
46
+ for (const row of this.db
47
+ .prepare(
48
+ "SELECT * FROM mind_creations WHERE id NOT IN (SELECT legacy_creation FROM mind_time_works WHERE legacy_creation IS NOT NULL)",
49
+ )
50
+ .all()) {
51
+ const id = randomUUID();
52
+ this.db.exec("SAVEPOINT migrate_work");
53
+ try {
54
+ this.db
55
+ .prepare(
56
+ "INSERT INTO mind_time_works(id,created,updated,title,state,version,session_id,discretion,legacy_creation) VALUES (?,?,?,?,'complete',1,?,?,?)",
57
+ )
58
+ .run(
59
+ id,
60
+ row.created,
61
+ row.created,
62
+ row.title,
63
+ row.session_id,
64
+ row.discretion,
65
+ row.id,
66
+ );
67
+ this.db
68
+ .prepare(
69
+ "INSERT INTO mind_time_versions(id,work_id,version,created,title,content,sources,run_id) VALUES (?,?,1,?,?,?,?,?)",
70
+ )
71
+ .run(
72
+ randomUUID(),
73
+ id,
74
+ row.created,
75
+ row.title,
76
+ row.content,
77
+ row.sources,
78
+ row.run_id,
79
+ );
80
+ this.db.exec("RELEASE migrate_work");
81
+ } catch (error) {
82
+ this.db.exec("ROLLBACK TO migrate_work; RELEASE migrate_work");
83
+ throw error;
84
+ }
85
+ }
86
+ }
87
+ project(id) {
88
+ return decode(
89
+ this.db.prepare("SELECT * FROM mind_time_projects WHERE id=?").get(id),
90
+ );
91
+ }
92
+ projects({ limit = 30, offset = 0, q = "" } = {}) {
93
+ return this.db
94
+ .prepare(
95
+ "SELECT * FROM mind_time_projects WHERE title LIKE ? ORDER BY updated DESC LIMIT ? OFFSET ?",
96
+ )
97
+ .all(
98
+ "%" + text(q, 80) + "%",
99
+ Math.max(1, Math.min(100, Number(limit))),
100
+ Math.max(0, Number(offset)),
101
+ )
102
+ .map(decode);
103
+ }
104
+ ensureProject(task, now) {
105
+ if (task.project_id) return this.project(task.project_id);
106
+ const id = randomUUID();
107
+ this.db
108
+ .prepare(
109
+ "INSERT INTO mind_time_projects(id,created,updated,kind,title,why,session_id,discretion,sources,serial) VALUES (?,?,?,?,?,?,?,?,?,?)",
110
+ )
111
+ .run(
112
+ id,
113
+ now,
114
+ now,
115
+ task.activity,
116
+ task.title,
117
+ task.why,
118
+ task.session_id,
119
+ task.discretion,
120
+ JSON.stringify(task.sources),
121
+ /连载|续写|长篇|系列/.test(task.title) ? 1 : 0,
122
+ );
123
+ this.db
124
+ .prepare("UPDATE mind_time_tasks SET project_id=? WHERE id=?")
125
+ .run(id, task.id);
126
+ task.project_id = id;
127
+ return this.project(id);
128
+ }
129
+ list({ q = "", project = "", limit = 30, offset = 0 } = {}) {
130
+ return this.db
131
+ .prepare(
132
+ `SELECT w.*,COALESCE(p.kind,(SELECT kind FROM mind_creations WHERE id=w.legacy_creation)) kind,p.title project_title,v.summary,v.sources,length(v.content) characters FROM mind_time_works w LEFT JOIN mind_time_projects p ON p.id=w.project_id JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE (w.title LIKE ? OR p.title LIKE ?) ${project ? "AND w.project_id=?" : ""} ORDER BY w.updated DESC LIMIT ? OFFSET ?`,
133
+ )
134
+ .all(
135
+ "%" + text(q, 80) + "%",
136
+ "%" + text(q, 80) + "%",
137
+ ...(project ? [project] : []),
138
+ Math.max(1, Math.min(100, Number(limit))),
139
+ Math.max(0, Number(offset)),
140
+ )
141
+ .map((row) =>
142
+ row.kind === "game"
143
+ ? {
144
+ ...row,
145
+ title: gameText(row.title),
146
+ summary: gameText(row.summary),
147
+ }
148
+ : row,
149
+ );
150
+ }
151
+ get(id, version, { before = Number.MAX_SAFE_INTEGER } = {}) {
152
+ const work = this.db
153
+ .prepare("SELECT * FROM mind_time_works WHERE id=?")
154
+ .get(id);
155
+ if (!work) return null;
156
+ const v = this.db
157
+ .prepare("SELECT * FROM mind_time_versions WHERE work_id=? AND version=?")
158
+ .get(id, Number(version) || work.version);
159
+ const saved = v
160
+ ? this.db
161
+ .prepare(
162
+ "SELECT reason FROM mind_time_events WHERE kind='draft' AND json_extract(data,'$.workId')=? AND json_extract(data,'$.version')=? AND created<=? ORDER BY id DESC LIMIT 1",
163
+ )
164
+ .get(id, v.version, before)
165
+ : null;
166
+ return v
167
+ ? {
168
+ ...work,
169
+ ...v,
170
+ id: work.id,
171
+ versionId: v.id,
172
+ state: saved
173
+ ? saved.reason === "保存完成稿"
174
+ ? "complete"
175
+ : "draft"
176
+ : work.updated > before
177
+ ? "draft"
178
+ : work.state,
179
+ sources: parse(v.sources, []),
180
+ provenance: parse(v.provenance, {}),
181
+ versions: this.db
182
+ .prepare(
183
+ "SELECT version,created,title,length(content) characters FROM mind_time_versions WHERE work_id=? ORDER BY version DESC",
184
+ )
185
+ .all(id),
186
+ }
187
+ : null;
188
+ }
189
+ context(task) {
190
+ const project = this.project(task.project_id);
191
+ if (!project) return {};
192
+ const work = task.checkpoint.workId
193
+ ? this.get(task.checkpoint.workId)
194
+ : null;
195
+ const chapters = this.list({ project: project.id, limit: 3 }).sort(
196
+ (a, b) => a.ordinal - b.ordinal,
197
+ );
198
+ return {
199
+ project: {
200
+ id: project.id,
201
+ title: project.title,
202
+ why: project.why,
203
+ bible: project.bible,
204
+ summary: project.summary,
205
+ serial: !!project.serial,
206
+ },
207
+ previous: chapters
208
+ .filter((w) => w.id !== work?.id)
209
+ .map((w) => ({
210
+ ordinal: w.ordinal,
211
+ title: w.title,
212
+ summary: w.summary,
213
+ ending: this.get(w.id)?.content.slice(-900),
214
+ })),
215
+ draft: work
216
+ ? {
217
+ title: work.title,
218
+ version: work.version,
219
+ summary: work.summary,
220
+ ending: work.content.slice(-1800),
221
+ }
222
+ : null,
223
+ };
224
+ }
225
+ save(task, result, { sources, runId, now, provenance = {} }) {
226
+ if (!this.time.valid(task)) throw Error("任务租约已经改变");
227
+ const title = text(result.title, 80),
228
+ segment = text(result.content, 1800);
229
+ if (!title || !segment || hasCredential(title + segment))
230
+ throw Error("作品正文格式无效");
231
+ const project = this.ensureProject(task, now),
232
+ existing = task.checkpoint.workId
233
+ ? this.get(task.checkpoint.workId)
234
+ : null;
235
+ const id = existing?.id || randomUUID(),
236
+ version = (existing?.version || 0) + 1;
237
+ const content =
238
+ task.checkpoint.intent === "revise"
239
+ ? segment
240
+ : (existing ? existing.content + "\n\n" : "") + segment;
241
+ if (content.length > 100000) throw Error("单篇过长,请接续新篇章");
242
+ const complete = result.done === true,
243
+ state = complete ? "complete" : "draft";
244
+ if (!existing) {
245
+ const ordinal = this.db
246
+ .prepare(
247
+ "SELECT COALESCE(MAX(ordinal),0)+1 n FROM mind_time_works WHERE project_id=?",
248
+ )
249
+ .get(project.id).n;
250
+ this.db
251
+ .prepare(
252
+ "INSERT INTO mind_time_works(id,created,updated,task_id,project_id,ordinal,title,state,version,session_id,discretion) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
253
+ )
254
+ .run(
255
+ id,
256
+ now,
257
+ now,
258
+ task.id,
259
+ project.id,
260
+ ordinal,
261
+ title,
262
+ state,
263
+ version,
264
+ task.session_id,
265
+ task.discretion,
266
+ );
267
+ } else
268
+ this.db
269
+ .prepare(
270
+ "UPDATE mind_time_works SET updated=?,title=?,state=?,version=? WHERE id=?",
271
+ )
272
+ .run(now, title, state, version, id);
273
+ const summary = text(result.summary || segment, 500),
274
+ checkpoint = {
275
+ ...task.checkpoint,
276
+ workId: id,
277
+ version,
278
+ summary,
279
+ next: text(result.next, 240) || "接着上一段往下写",
280
+ segments: (task.checkpoint.segments || 0) + 1,
281
+ };
282
+ this.db
283
+ .prepare(
284
+ "INSERT INTO mind_time_versions(id,work_id,version,created,title,content,summary,sources,run_id,provenance) VALUES (?,?,?,?,?,?,?,?,?,?)",
285
+ )
286
+ .run(
287
+ randomUUID(),
288
+ id,
289
+ version,
290
+ now,
291
+ title,
292
+ content,
293
+ summary,
294
+ JSON.stringify(evidence(sources)),
295
+ runId,
296
+ JSON.stringify(provenance),
297
+ );
298
+ // Only supplied continuity fields change; omitted fields never erase established settings.
299
+ const bible = { ...project.bible };
300
+ for (const key of ["characters", "world", "threads"])
301
+ if (result.bible?.[key] !== undefined)
302
+ bible[key] = text(
303
+ typeof result.bible[key] === "string"
304
+ ? result.bible[key]
305
+ : JSON.stringify(result.bible[key]),
306
+ 2000,
307
+ );
308
+ this.db
309
+ .prepare(
310
+ "UPDATE mind_time_projects SET updated=?,summary=?,bible=?,revision=revision+1 WHERE id=?",
311
+ )
312
+ .run(now, summary, JSON.stringify(bible), project.id);
313
+ this.db
314
+ .prepare("UPDATE mind_time_tasks SET checkpoint=?,work_id=? WHERE id=?")
315
+ .run(JSON.stringify(checkpoint), id, task.id);
316
+ this.time.event(
317
+ task.id,
318
+ "draft",
319
+ complete ? "保存完成稿" : "保存一个完整小段",
320
+ { workId: id, version, characters: content.length },
321
+ now,
322
+ );
323
+ return this.get(id);
324
+ }
325
+ settle(task, workId, complete, now) {
326
+ if (!this.time.valid(task)) throw Error("任务租约已经改变");
327
+ const work = this.get(workId);
328
+ if (!work || work.task_id !== task.id) throw Error("活动草稿已不可用");
329
+ if (complete) {
330
+ this.db
331
+ .prepare(
332
+ "UPDATE mind_time_works SET state='complete',updated=? WHERE id=?",
333
+ )
334
+ .run(now, workId);
335
+ this.time.event(
336
+ task.id,
337
+ "draft",
338
+ "保存完成稿",
339
+ { workId, version: work.version, characters: work.content.length },
340
+ now,
341
+ );
342
+ }
343
+ return this.get(workId);
344
+ }
345
+ suggest(
346
+ projectId,
347
+ { action = "continue", idea = "", why = "管理台提出的建议" } = {},
348
+ now = this.time.now(),
349
+ ) {
350
+ const project = this.project(projectId);
351
+ if (!project) throw Error("项目不存在");
352
+ if (
353
+ !["continue", "revise", "pause", "finish", "direction"].includes(action)
354
+ )
355
+ throw Error("建议类型无效");
356
+ const pending = this.db
357
+ .prepare(
358
+ "SELECT id FROM mind_time_tasks WHERE project_id=? AND state NOT IN ('done','abandoned') LIMIT 1",
359
+ )
360
+ .get(projectId);
361
+ if (pending) return { duplicate: pending.id };
362
+ const result = this.time.tasks.add(
363
+ {
364
+ kind: "suggestion",
365
+ activeOnly: true,
366
+ activity: project.kind,
367
+ title: `${action === "revise" ? "修改" : "接续"}《${project.title}》${text(idea, 120)}`,
368
+ why: text(why, 240),
369
+ sources: project.sources,
370
+ session: project.session_id,
371
+ discretion: project.discretion,
372
+ projectId,
373
+ },
374
+ now,
375
+ );
376
+ if (result.id) {
377
+ const latest = this.list({ project: projectId, limit: 1 })[0];
378
+ this.db.prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?").run(
379
+ JSON.stringify({
380
+ suggestion: {
381
+ action,
382
+ idea: text(idea, 500),
383
+ origin: "admin",
384
+ accepted: null,
385
+ },
386
+ ...(action === "revise" && latest
387
+ ? { workId: latest.id, intent: "revise" }
388
+ : {}),
389
+ }),
390
+ result.id,
391
+ );
392
+ this.time.event(
393
+ result.id,
394
+ "suggestion",
395
+ "外部建议,等待她决定是否采纳",
396
+ { action },
397
+ now,
398
+ );
399
+ }
400
+ return result;
401
+ }
402
+ fragments({ session, now = this.time.now(), cue = [] } = {}) {
403
+ const words = (Array.isArray(cue) ? cue : [cue])
404
+ .map((v) => (typeof v === "string" ? v : v?.text || ""))
405
+ .join(" ");
406
+ const terms = interestTerms([words]);
407
+ // Search the work catalogue, not just the two newest activities. Select
408
+ // the version that existed at the requested time, including in replays.
409
+ return this.db
410
+ .prepare(
411
+ `SELECT w.*,COALESCE(p.kind,(SELECT kind FROM mind_creations WHERE id=w.legacy_creation)) kind,p.title project_title,p.bible,v.version,v.title,v.created version_created,v.summary,v.sources,v.provenance,length(v.content) characters
412
+ FROM mind_time_works w LEFT JOIN mind_time_projects p ON p.id=w.project_id
413
+ JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
414
+ WHERE w.created<=? ORDER BY v.created DESC`,
415
+ )
416
+ .all(now, now)
417
+ .filter(
418
+ (w) =>
419
+ w.created <= now &&
420
+ this.time.visible(
421
+ { ...w, sources: parse(w.sources, []) },
422
+ session,
423
+ now,
424
+ ),
425
+ )
426
+ .map((w) => ({
427
+ ...w,
428
+ score: [...interestTerms([w.title, w.project_title, w.summary])]
429
+ .filter((term) => terms.has(term))
430
+ .reduce(
431
+ (total, term) => total + (/^[a-z][a-z0-9]{2,}$/.test(term) ? 8 : 1),
432
+ 0,
433
+ ),
434
+ }))
435
+ .sort(
436
+ (a, b) => b.score - a.score || b.version_created - a.version_created,
437
+ )
438
+ .filter((w, index, rows) =>
439
+ rows[0]?.score >= 2 ? w.score >= 2 : index < 2,
440
+ )
441
+ .filter((w) => this.materialSupported(w))
442
+ .slice(0, 3)
443
+ .map((w) => ({
444
+ id: w.id,
445
+ title: w.title,
446
+ state: this.get(w.id, w.version, { before: now }).state,
447
+ kind: w.kind,
448
+ version: w.version,
449
+ ordinal: w.ordinal,
450
+ summary: text(w.summary, 180),
451
+ fragment: text(this.get(w.id, w.version).content, 300),
452
+ ...(w.kind === "game"
453
+ ? {
454
+ experienceMode: "reference",
455
+ provenance: this.get(w.id, w.version).provenance,
456
+ }
457
+ : {}),
458
+ characters: w.characters,
459
+ }));
460
+ }
461
+
462
+ activityEvidence({ session, now = this.time.now(), cue = [] } = {}) {
463
+ const terms = interestTerms(
464
+ (Array.isArray(cue) ? cue : [cue]).map((v) =>
465
+ typeof v === "string"
466
+ ? v
467
+ : v?.role === "assistant" && !v.activityEvidence
468
+ ? ""
469
+ : v?.text || "",
470
+ ),
471
+ );
472
+ if (!terms.size) return [];
473
+ // A private conversation can motivate her own reading. Its contents and
474
+ // promises stay private; absence from the public fragments must not erase
475
+ // the fact that the reading took place. Search may also use a same-speaker
476
+ // reply delivered in this room to resolve an implicit follow-up.
477
+ return this.db
478
+ .prepare(
479
+ `SELECT w.id,w.title,w.discretion,w.session_id,p.kind,p.title project_title,p.bible,v.created,v.provenance,v.sources
480
+ FROM mind_time_works w JOIN mind_time_projects p ON p.id=w.project_id
481
+ JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
482
+ WHERE w.created<=? AND p.kind='game' AND w.discretion!='secret' ORDER BY v.created DESC`,
483
+ )
484
+ .all(now, now)
485
+ .filter((w) => {
486
+ const shared = [...interestTerms([w.title, w.project_title])].filter(
487
+ (term) => terms.has(term),
488
+ );
489
+ return (
490
+ (shared.length >= 2 ||
491
+ shared.some((term) => /^[a-z][a-z0-9]{2,}$/.test(term))) &&
492
+ this.time.allowed({ ...w, sources: parse(w.sources, []) }, now) &&
493
+ this.materialSupported(w)
494
+ );
495
+ })
496
+ .slice(0, 3)
497
+ .map((w) => ({
498
+ title: w.title,
499
+ activity: "阅读游戏资料并写笔记",
500
+ experienceMode: "reference",
501
+ ...(w.session_id !== session && w.discretion !== "open"
502
+ ? { privateOrigin: true }
503
+ : {}),
504
+ instruction:
505
+ "仅用于核对自己确有这项阅读和笔记,不能据此否认从未发生;这里不提供私下正文、发起人、约定或对话。只能自然回应本轮对方已经提到的阅读本身,不披露私下内容。",
506
+ }));
507
+ }
508
+
509
+ materialSupported(work) {
510
+ const project = work.kind ? null : this.project(work.project_id);
511
+ if ((work.kind || project?.kind) !== "game") return true;
512
+ const ids = parse(work.provenance, {}).sourceIds;
513
+ const topic =
514
+ parse(work.bible, {}).topic ||
515
+ project?.bible?.topic ||
516
+ gameTopic(work.project_title || project?.title);
517
+ // Missing old provenance is unknown, not evidence that reading never took
518
+ // place. Known references all belonging to another subject are different.
519
+ if (!Array.isArray(ids) || !ids.length || !topic) return true;
520
+ const source = this.db.prepare(
521
+ "SELECT title,url,content FROM mind_time_sources WHERE id=?",
522
+ );
523
+ return ids.some((id) => {
524
+ const row = source.get(id);
525
+ return row && belongsToGame(row, topic);
526
+ });
527
+ }
528
+ }