luckytri 1.0.7 → 1.0.8

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 (36) hide show
  1. package/README.en.md +7 -3
  2. package/README.md +7 -3
  3. package/docs/en/CHANGELOG.md +10 -0
  4. package/docs/zh/CHANGELOG.md +10 -0
  5. package/package.json +1 -1
  6. package/public/guide.en.html +3 -3
  7. package/public/guide.html +3 -3
  8. package/server/core/conversation-cues.js +29 -9
  9. package/server/core/conversation-grounding.js +130 -4
  10. package/server/core/dialogue-context.js +194 -2
  11. package/server/core/dialogue-contract.js +35 -0
  12. package/server/core/initiative-context.js +4 -2
  13. package/server/core/model-manager.js +5 -8
  14. package/server/core/persona-manager.js +44 -9
  15. package/server/core/reader-check.js +62 -0
  16. package/server/core/reply-delivery.js +68 -3
  17. package/server/core/response-generator.js +43 -9
  18. package/server/core/response-validator.js +24 -6
  19. package/server/core/turn.js +43 -10
  20. package/server/i18n/errors.js +2 -0
  21. package/server/i18n/labels.js +6 -5
  22. package/server/mind/expression-grounding.js +21 -0
  23. package/server/mind/own-voice.js +35 -0
  24. package/server/mind/reading.js +15 -3
  25. package/server/mind/time/availability.js +23 -1
  26. package/server/mind/time/delivery-links.js +130 -130
  27. package/server/mind/time/executor.js +4 -2
  28. package/server/mind/time/games.js +13 -8
  29. package/server/mind/time/index.js +6 -2
  30. package/server/mind/time/kinds.js +29 -2
  31. package/server/mind/time/material-relevance.js +19 -0
  32. package/server/mind/time/reading-input.js +31 -0
  33. package/server/mind/time/sharing.js +10 -0
  34. package/server/mind/time/tasks.js +41 -3
  35. package/server/mind/time/works.js +104 -15
  36. package/server/mind/view.js +20 -7
@@ -2,7 +2,12 @@ import { replyFocus } from "./conversation-cues.js";
2
2
  import { INITIATIVE_PROMPT } from "../mind/initiative.js";
3
3
  import { initiativeContext } from "./initiative-context.js";
4
4
  import { conversationGrounding } from "./conversation-grounding.js";
5
- import { currentExchange, explicitStop } from "./dialogue-context.js";
5
+ import {
6
+ currentExchange,
7
+ dialogueContext,
8
+ explicitStop,
9
+ } from "./dialogue-context.js";
10
+ import { readerQuestions } from "./reader-check.js";
6
11
 
7
12
  export const CHOICES = ["speak", "react", "decline", "silent"];
8
13
  const LEGACY = {
@@ -42,7 +47,7 @@ export function normalizeAct(raw, trace) {
42
47
 
43
48
  export function recallWording(snapshot) {
44
49
  if (snapshot.initiative) return null;
45
- const continuity = snapshot.inner?.continuity;
50
+ const continuity = dialogueContext(snapshot).inner?.continuity;
46
51
  const notes = [];
47
52
  if (
48
53
  continuity?.people?.some(
@@ -77,12 +82,6 @@ export async function takeTurn(
77
82
  trace,
78
83
  extra = {},
79
84
  ) {
80
- const {
81
- sourceRows: _rows,
82
- batch: _batch,
83
- persona: _persona,
84
- ...context
85
- } = snapshot;
86
85
  const images = extra.images || [];
87
86
  const initiating =
88
87
  extra.occasion && ["presence", "outreach"].includes(extra.occasion.type);
@@ -102,7 +101,7 @@ export async function takeTurn(
102
101
  {
103
102
  context: initiating
104
103
  ? initiativeContext(snapshot, extra.occasion)
105
- : { ...context, grounding: conversationGrounding(snapshot) },
104
+ : dialogueContext(snapshot),
106
105
  ...(extra.occasion
107
106
  ? {
108
107
  occasion: initiating
@@ -148,6 +147,13 @@ export function normalizeTurn(raw, snapshot, trace) {
148
147
  .slice(-1)
149
148
  .map((m) => m.id);
150
149
  }
150
+ if (
151
+ choice === "react" &&
152
+ readerQuestions(snapshot, { choice }).some((q) => targets.includes(q.id))
153
+ ) {
154
+ choice = "speak";
155
+ trace?.steps?.push("对方在要求解释,保留完整回答空间,不按极短反应截断");
156
+ }
151
157
  let bubbles = [];
152
158
  if (Array.isArray(raw.bubbles)) bubbles = raw.bubbles;
153
159
  else if (typeof raw.bubbles === "string") bubbles = [raw.bubbles];
@@ -161,6 +167,12 @@ export function normalizeTurn(raw, snapshot, trace) {
161
167
  .filter((x) => typeof x === "string")
162
168
  .map((x) => x.trim())
163
169
  .filter(Boolean);
170
+ if (choice === "react" && bubbles.length === 1 && bubbles[0].length > 20) {
171
+ choice = "speak";
172
+ trace?.steps?.push(
173
+ "这是一句完整回应,按正常发言保留,不为短反应标签改写原意",
174
+ );
175
+ }
164
176
  if (choice === "silent") bubbles = [];
165
177
  const crisisIds = ids(raw.crisis?.messageIds).filter((id) =>
166
178
  batchIds.has(id),
@@ -178,7 +190,20 @@ export function normalizeTurn(raw, snapshot, trace) {
178
190
  trace?.steps?.push("有人可能处在危机里,底线要求认真回应");
179
191
  }
180
192
  const botLoop =
181
- !crisis && !snapshot.initiative && conversationGrounding(snapshot).botLoop;
193
+ !crisis &&
194
+ !snapshot.initiative &&
195
+ (conversationGrounding(snapshot).botLoop ||
196
+ (raw.contribution?.kind === "none" &&
197
+ !raw.act &&
198
+ !raw.share &&
199
+ !["chat", "rest"].includes(raw.attention?.action) &&
200
+ batch.length > 0 &&
201
+ batch.filter((m) => m.role === "user").length > 0 &&
202
+ batch
203
+ .filter((m) => m.role === "user")
204
+ .every((m) =>
205
+ conversationGrounding(snapshot).botMessageIds.includes(m.id),
206
+ )));
182
207
  if (botLoop) {
183
208
  choice = "silent";
184
209
  bubbles = [];
@@ -233,6 +258,14 @@ export function normalizeTurn(raw, snapshot, trace) {
233
258
  },
234
259
  }
235
260
  : {}),
261
+ ...(raw.contribution && typeof raw.contribution === "object"
262
+ ? {
263
+ contribution: {
264
+ kind: text(raw.contribution.kind, 20),
265
+ point: text(raw.contribution.point, 180),
266
+ },
267
+ }
268
+ : {}),
236
269
  targetMessageIds: targets,
237
270
  targetUserIds: [
238
271
  ...new Set(
@@ -457,6 +457,8 @@ export const ERRORS = {
457
457
  "正在推进活动步骤,请稍后测试":
458
458
  "An activity step is in progress. Test again shortly.",
459
459
  只有真实完成稿可以交付: "Only a genuinely finished piece can be delivered",
460
+ "这篇记录引用了与作品无关的资料,不能作为游戏阅读进度分享":
461
+ "This note uses material unrelated to the work and cannot be shared as reading progress",
460
462
  正文包含其他会话的私下内容:
461
463
  "The text contains private content from another session",
462
464
  作品来源不允许在此会话分享:
@@ -3,6 +3,10 @@
3
3
  // key, exactly as the code writes it. Only a whole string that equals a key is
4
4
  // translated, so what she writes herself is never touched.
5
5
  export const LABELS = {
6
+ 等待约定的正文或资料: "Waiting for the agreed manuscript or material",
7
+ 等待来稿的阅读约定不能当成写作执行:
8
+ "A promise to read an incoming manuscript cannot run as writing",
9
+ 指定的正文还不可阅读: "The specified manuscript is not available to read yet",
6
10
  存在运行伴随文件: "Runtime sidecar files exist",
7
11
  近期自动恢复点: "Recent automatic restore point",
8
12
  // feedback.js
@@ -608,7 +612,6 @@ export const LABEL_PATTERNS = [
608
612
  "Touched by others' words {0} times; last {1}, and she {2}",
609
613
  ],
610
614
  ["第 {0} / {1} 段", "Passage {0} / {1}"],
611
- ["主题片段 {0}", "Topic passage {0}"],
612
615
  [
613
616
  "正在玩,本段预计约 {0} 分钟",
614
617
  "Playing; this span should take about {0} min",
@@ -618,10 +621,8 @@ export const LABEL_PATTERNS = [
618
621
  "I finished piece {1} of “{0}”. The text is saved in the works library; the plot and characters in it are made up.",
619
622
  ],
620
623
  ["接续《{0}》第 {1} 篇", "Continue piece {1} of “{0}”"],
621
- [
622
- "玩过第 {0} 段,留下自己的游玩记录",
623
- "Played passage {0} and left her own play record",
624
- ],
624
+ ["已留下《{0}》这篇阅读笔记", "Saved a reading note titled “{0}”"],
625
+ ["已接触一些游戏资料", "Has read some game reference material"],
625
626
  ["{0},已保存 {1} 字完成稿", "{0}, saved a finished piece of {1} characters"],
626
627
  ["{0},已保存 {1} 字正文", "{0}, saved {1} characters of text"],
627
628
  [
@@ -0,0 +1,21 @@
1
+ export function claimsLivedAction(content) {
2
+ return String(content || "")
3
+ .split(/[。!?\n]/)
4
+ .some(
5
+ (clause) =>
6
+ !/想|打算|准备|希望|假如|设想|如果/.test(clause) &&
7
+ /(?:刚才|刚刚|今天|这次|然后|真的|已经)[^。!?]{0,25}(?:读了|读上|读完|翻开|翻出来|写完|完成了|打开了|开始读|动手了)/.test(
8
+ clause,
9
+ ),
10
+ );
11
+ }
12
+
13
+ export function claimsSharedHistory(content) {
14
+ return /(?:之前|上次|早些时候)[^。!?\n]{0,25}(?:他|她|对方|那个人|你)[^。!?\n]{0,20}(?:说过|聊过|看过|读过|回话|私下)|(?:他|她|对方|那个人|你)[^。!?\n]{0,15}(?:之前|上次|读过|看过|说过)/.test(
15
+ String(content || ""),
16
+ );
17
+ }
18
+
19
+ export const EXPRESSION_GROUNDING = `核对一个准备保存的自我念头是否把愿望写成行动,或把别人的话题误认成共同经历。只输出JSON。
20
+ candidate只是草稿,不是事实。notes/self是过去的想法、愿望或理解;它们可以支持新的感受和看法,不能单独证明这次真的开始、读完或做完。actions/currentLife是实际活动记录。sources只表示引用了哪里,不自动证明动作。按实际记录核对这次声称新做过的具体动作和对象;不能用无关作品或一条待办证明完成。声称共同经历时,还须核对实际参与者和各自说过的话;在别处读到另一人的讨论不证明当前对象参与过。自己的新想法、偏好、假设、打算可以没有执行记录,也不需要已采取外部行动。只指出确实无依据的新经历与归属,不评文风。
21
+ 输出 {"ok":true或false,"reason":"具体哪项新经历有或没有实际记录"}。`;
@@ -6,6 +6,11 @@ import { isPrivateSession } from "./memory.js";
6
6
  import { sameRecentTheme } from "./novelty.js";
7
7
  import { ownLife, sameSelf } from "./salience.js";
8
8
  import { conversationOrigin, originSummary } from "./conversation-origin.js";
9
+ import {
10
+ claimsLivedAction,
11
+ claimsSharedHistory,
12
+ EXPRESSION_GROUNDING,
13
+ } from "./expression-grounding.js";
9
14
 
10
15
  // After this many notes in a row that only take up her own last note, another
11
16
  // one has to pick up something new or it is not kept.
@@ -203,6 +208,36 @@ export class OwnVoice {
203
208
  sources.some((s) => !valid.has(s))
204
209
  )
205
210
  throw Error("自己的念头含有无效来源或敏感内容,未保存");
211
+ if (
212
+ claimsLivedAction([content, ...words].join("\n")) ||
213
+ claimsSharedHistory([content, result.reason, ...words].join("\n"))
214
+ ) {
215
+ const grounded = await models.call(
216
+ profile,
217
+ "expression_grounding",
218
+ EXPRESSION_GROUNDING,
219
+ {
220
+ candidate: { note: content, words, sources },
221
+ actions: input.actions,
222
+ currentLife: input.currentLife,
223
+ notes: input.notes,
224
+ self: input.self,
225
+ },
226
+ trace,
227
+ );
228
+ if (grounded.ok !== true) {
229
+ status = "empty";
230
+ reason =
231
+ text(grounded.reason, 240) ||
232
+ "新行动经历没有得到实际记录支持,未保存";
233
+ return { status, reason, note: null };
234
+ }
235
+ if (life.closed || this.mind.nature.version() !== nature.version) {
236
+ status = "cancelled";
237
+ reason = "核对期间状态已变化";
238
+ return { status, reason, note: null };
239
+ }
240
+ }
206
241
  // Several notes in a row already went round without taking anything
207
242
  // new up. Another one that still does not is not kept.
208
243
  const drifting =
@@ -16,7 +16,7 @@ export class Reading {
16
16
  shared() {
17
17
  return this.db
18
18
  .prepare(
19
- `SELECT c.id, c.document_id, c.ordinal, c.heading, c.text, d.title,
19
+ `SELECT c.id, c.document_id, c.ordinal, c.heading, c.text, d.title,d.created documentCreated,
20
20
  (SELECT COUNT(*) FROM core_chunks x WHERE x.document_id=c.document_id) total
21
21
  FROM core_chunks c
22
22
  JOIN core_documents d ON d.id=c.document_id AND d.status='ready'
@@ -28,8 +28,20 @@ export class Reading {
28
28
  }
29
29
  // The next part of something she already started, or the unread passage
30
30
  // closest to what she is living for, then what she likes and wonders about.
31
- next(now = Date.now()) {
32
- const unread = this.shared();
31
+ next(now = Date.now(), { title = "", after = 0 } = {}) {
32
+ const clean = (value) =>
33
+ String(value || "")
34
+ .toLowerCase()
35
+ .replace(/[\s\p{P}]/gu, "");
36
+ const wanted = clean(title);
37
+ const unread = this.shared().filter(
38
+ (chunk) =>
39
+ (!wanted ||
40
+ clean(chunk.title) === wanted ||
41
+ (wanted.length >= 2 && clean(chunk.title).includes(wanted))) &&
42
+ (!after ||
43
+ (chunk.documentCreated >= after && chunk.documentCreated <= now)),
44
+ );
33
45
  if (!unread.length) return null;
34
46
  const last = this.db
35
47
  .prepare(
@@ -1,5 +1,10 @@
1
1
  import { gameTopic } from "./intent.js";
2
- import { isExecutable, needsEnergy } from "./kinds.js";
2
+ import { isExecutable, needsEnergy, classifyActivity } from "./kinds.js";
3
+ import {
4
+ nextReadingChunk,
5
+ expectedTextTitle,
6
+ awaitsTextInput,
7
+ } from "./reading-input.js";
3
8
  export function unsupportedAction(task) {
4
9
  return task.activity === "unknown" &&
5
10
  /点.{0,8}外卖|下单|购买|实拍|拍照给|尝.{0,6}(?:味道|好吃)/.test(task.title)
@@ -18,6 +23,23 @@ export function executionBlock(time, task, now) {
18
23
  const life = time.search.life;
19
24
  const unsupported = unsupportedAction(task);
20
25
  if (unsupported) return unsupported;
26
+ if (
27
+ task.activity === "write" &&
28
+ awaitsTextInput(task.title) &&
29
+ classifyActivity(task.title) === "read"
30
+ )
31
+ return "等待来稿的阅读约定不能当成写作执行";
32
+ if (
33
+ task.checkpoint.awaitedText &&
34
+ (task.activity !== "read" || !nextReadingChunk(time.mind, task, now))
35
+ )
36
+ return "等待约定的正文或资料";
37
+ if (
38
+ task.activity === "read" &&
39
+ expectedTextTitle(task) &&
40
+ !nextReadingChunk(time.mind, task, now)
41
+ )
42
+ return "指定的正文还不可阅读";
21
43
  if (task.activity === "unknown" || !isExecutable(task.activity))
22
44
  return task.activity === "unknown"
23
45
  ? "需要把行动内容想具体"
@@ -1,130 +1,130 @@
1
- import { evidence, parse } from "../util.js";
2
- import { intentRecipient } from "./intent.js";
3
-
4
- export class DeliveryLinks {
5
- constructor(tasks) {
6
- this.tasks = tasks;
7
- this.time = tasks.time;
8
- this.db = tasks.db;
9
- }
10
- resolve(row, message, turn = {}, now = this.time.now()) {
11
- if (
12
- !/(?:发(?:给)?你|给你(?:看|读|发)|这就发|已经发)/.test(message.text || "")
13
- )
14
- return null;
15
- const questions = (message.replyTargetIds || turn.targetMessageIds || [])
16
- .map((id) =>
17
- this.db
18
- .prepare(
19
- "SELECT payload FROM core_events WHERE seq=? AND session_id=? AND role='user'",
20
- )
21
- .get(Number(id), row.session_id),
22
- )
23
- .filter(Boolean)
24
- .map((q) => parse(q.payload, {}).text || "");
25
- const cues = [message.text, ...questions].join(" ");
26
- const works = this.db
27
- .prepare(
28
- "SELECT w.*,v.sources,v.content FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE w.state='complete' AND w.created<=? AND w.task_id IS NOT NULL ORDER BY w.updated DESC LIMIT 30",
29
- )
30
- .all(now)
31
- .map((w) => ({ ...w, sources: parse(w.sources, []) }))
32
- .filter(
33
- (w) =>
34
- this.time.visible(w, row.session_id, now) &&
35
- (!w.session_id || w.session_id === row.session_id),
36
- );
37
- const named = works.filter((w) => cues.includes(w.title)),
38
- candidates = named.length
39
- ? named
40
- : /小说|短篇|作品|诗|正文|札记/.test(cues)
41
- ? works
42
- : [];
43
- if (candidates.length !== 1) return null;
44
- const work = candidates[0],
45
- task = this.tasks.get(work.task_id);
46
- if (!task || task.state !== "done") return null;
47
- const subject = intentRecipient(this.db, {
48
- session: row.session_id,
49
- subject: turn.targetUserIds?.[0],
50
- title: message.text,
51
- sources: [row.seq],
52
- });
53
- if (!subject) return null;
54
- const share = this.db
55
- .prepare(
56
- "SELECT * FROM mind_time_shares WHERE work_id=? AND version=? AND session_id=?",
57
- )
58
- .get(work.id, work.version, row.session_id);
59
- const state =
60
- share?.state === "sent" && share.offset >= Array.from(work.content).length
61
- ? "sent"
62
- : share?.state === "sent" && share.delivery_kind === "report"
63
- ? "reported"
64
- : share?.state || "waiting";
65
- const sources = evidence([...task.sources, `m:${row.seq}`]);
66
- this.db
67
- .prepare(
68
- "UPDATE mind_time_tasks SET kind='promise',subject=?,sources=?,share_state=?,updated=? WHERE id=?",
69
- )
70
- .run(subject, JSON.stringify(sources), state, now, task.id);
71
- if (!task.sources.includes(`m:${row.seq}`))
72
- this.time.event(
73
- task.id,
74
- "delivery-linked",
75
- "分享承诺归回已有作品,未新增创作待办",
76
- { source: `m:${row.seq}`, workId: work.id, delivery: state },
77
- now,
78
- );
79
- return task.id;
80
- }
81
- reconcile(now = this.time.now()) {
82
- const fixed = [];
83
- for (const task of this.db
84
- .prepare(
85
- "SELECT * FROM mind_time_tasks WHERE activity='unknown' AND state IN ('todo','waiting') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
86
- )
87
- .all()) {
88
- for (const ref of parse(task.sources, []).filter((r) =>
89
- r.startsWith("m:"),
90
- )) {
91
- const row = this.db
92
- .prepare(
93
- "SELECT seq,session_id,time,payload FROM core_events WHERE seq=? AND time<=?",
94
- )
95
- .get(Number(ref.slice(2)), now);
96
- if (!row) continue;
97
- const canonical = this.resolve(
98
- row,
99
- parse(row.payload, {}),
100
- { targetUserIds: task.subject ? [task.subject] : [] },
101
- now,
102
- );
103
- if (!canonical || canonical === task.id) continue;
104
- this.tasks.links.link("task", task.id, canonical, now);
105
- this.db
106
- .prepare(
107
- "UPDATE mind_time_tasks SET state='abandoned',checkpoint=?,wait_reason=?,revision=revision+1 WHERE id=?",
108
- )
109
- .run(
110
- JSON.stringify({
111
- ...parse(task.checkpoint, {}),
112
- mergedInto: canonical,
113
- }),
114
- "已归入已有作品的交付记录",
115
- task.id,
116
- );
117
- this.time.event(
118
- task.id,
119
- "merged",
120
- "交付有真实作品与送达记录,归回原事项",
121
- { taskId: canonical },
122
- now,
123
- );
124
- fixed.push({ from: task.id, to: canonical });
125
- break;
126
- }
127
- }
128
- return fixed;
129
- }
130
- }
1
+ import { evidence, parse } from "../util.js";
2
+ import { intentRecipient } from "./intent.js";
3
+
4
+ export class DeliveryLinks {
5
+ constructor(tasks) {
6
+ this.tasks = tasks;
7
+ this.time = tasks.time;
8
+ this.db = tasks.db;
9
+ }
10
+ resolve(row, message, turn = {}, now = this.time.now()) {
11
+ if (
12
+ !/(?:发(?:给)?你|给你(?:看|读|发)|这就发|已经发)/.test(message.text || "")
13
+ )
14
+ return null;
15
+ const questions = (message.replyTargetIds || turn.targetMessageIds || [])
16
+ .map((id) =>
17
+ this.db
18
+ .prepare(
19
+ "SELECT payload FROM core_events WHERE seq=? AND session_id=? AND role='user'",
20
+ )
21
+ .get(Number(id), row.session_id),
22
+ )
23
+ .filter(Boolean)
24
+ .map((q) => parse(q.payload, {}).text || "");
25
+ const cues = [message.text, ...questions].join(" ");
26
+ const works = this.db
27
+ .prepare(
28
+ "SELECT w.*,v.sources,v.content FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE w.state='complete' AND w.created<=? AND w.task_id IS NOT NULL ORDER BY w.updated DESC LIMIT 30",
29
+ )
30
+ .all(now)
31
+ .map((w) => ({ ...w, sources: parse(w.sources, []) }))
32
+ .filter(
33
+ (w) =>
34
+ this.time.visible(w, row.session_id, now) &&
35
+ (!w.session_id || w.session_id === row.session_id),
36
+ );
37
+ const named = works.filter((w) => cues.includes(w.title)),
38
+ candidates = named.length
39
+ ? named
40
+ : /小说|短篇|作品|诗|正文|札记/.test(cues)
41
+ ? works
42
+ : [];
43
+ if (candidates.length !== 1) return null;
44
+ const work = candidates[0],
45
+ task = this.tasks.get(work.task_id);
46
+ if (!task || task.state !== "done") return null;
47
+ const subject = intentRecipient(this.db, {
48
+ session: row.session_id,
49
+ subject: turn.targetUserIds?.[0],
50
+ title: message.text,
51
+ sources: [row.seq],
52
+ });
53
+ if (!subject) return null;
54
+ const share = this.db
55
+ .prepare(
56
+ "SELECT * FROM mind_time_shares WHERE work_id=? AND version=? AND session_id=?",
57
+ )
58
+ .get(work.id, work.version, row.session_id);
59
+ const state =
60
+ share?.state === "sent" && share.offset >= Array.from(work.content).length
61
+ ? "sent"
62
+ : share?.state === "sent" && share.delivery_kind === "report"
63
+ ? "reported"
64
+ : share?.state || "waiting";
65
+ const sources = evidence([...task.sources, `m:${row.seq}`]);
66
+ this.db
67
+ .prepare(
68
+ "UPDATE mind_time_tasks SET kind='promise',subject=?,sources=?,share_state=?,updated=? WHERE id=?",
69
+ )
70
+ .run(subject, JSON.stringify(sources), state, now, task.id);
71
+ if (!task.sources.includes(`m:${row.seq}`))
72
+ this.time.event(
73
+ task.id,
74
+ "delivery-linked",
75
+ "分享承诺归回已有作品,未新增创作待办",
76
+ { source: `m:${row.seq}`, workId: work.id, delivery: state },
77
+ now,
78
+ );
79
+ return task.id;
80
+ }
81
+ reconcile(now = this.time.now()) {
82
+ const fixed = [];
83
+ for (const task of this.db
84
+ .prepare(
85
+ "SELECT * FROM mind_time_tasks WHERE activity='unknown' AND state IN ('todo','waiting') AND json_extract(checkpoint,'$.mergedInto') IS NULL",
86
+ )
87
+ .all()) {
88
+ for (const ref of parse(task.sources, []).filter((r) =>
89
+ r.startsWith("m:"),
90
+ )) {
91
+ const row = this.db
92
+ .prepare(
93
+ "SELECT seq,session_id,time,payload FROM core_events WHERE seq=? AND time<=?",
94
+ )
95
+ .get(Number(ref.slice(2)), now);
96
+ if (!row) continue;
97
+ const canonical = this.resolve(
98
+ row,
99
+ parse(row.payload, {}),
100
+ { targetUserIds: task.subject ? [task.subject] : [] },
101
+ now,
102
+ );
103
+ if (!canonical || canonical === task.id) continue;
104
+ this.tasks.links.link("task", task.id, canonical, now);
105
+ this.db
106
+ .prepare(
107
+ "UPDATE mind_time_tasks SET state='abandoned',checkpoint=?,wait_reason=?,revision=revision+1 WHERE id=?",
108
+ )
109
+ .run(
110
+ JSON.stringify({
111
+ ...parse(task.checkpoint, {}),
112
+ mergedInto: canonical,
113
+ }),
114
+ "已归入已有作品的交付记录",
115
+ task.id,
116
+ );
117
+ this.time.event(
118
+ task.id,
119
+ "merged",
120
+ "交付有真实作品与送达记录,归回原事项",
121
+ { taskId: canonical },
122
+ now,
123
+ );
124
+ fixed.push({ from: task.id, to: canonical });
125
+ break;
126
+ }
127
+ }
128
+ return fixed;
129
+ }
130
+ }
@@ -7,6 +7,7 @@ import { ACTIVITY_LABELS } from "./tasks.js";
7
7
  import { activityLabel, isPluginActivity, needsEnergy } from "./kinds.js";
8
8
  import { stepPlugin } from "./plugin-activity.js";
9
9
  import { executionBlock } from "./availability.js";
10
+ import { nextReadingChunk } from "./reading-input.js";
10
11
 
11
12
  const ACTIVITY_PROMPT =
12
13
  '执行一个属于自己的实际活动步骤。write创作一个完整小段,已有草稿时只追加下一段,修改模式则提供修改后的正文;根据篇幅决定本篇是否结束,连载完成的是本章。read只能阅读input.reading实际提供的段落;think留下想法记录。不编造外部操作、游玩、通关或未提供的资料。虚构人物只属于作品,不能进入真实人物记忆。不输出隐藏推理。外部suggestion是别人的建议,你决定采纳或拒绝并保留理由,可以改变方向、暂停或完结项目。输出JSON {"done":false,"title":"标题","content":"最多1800字完整小段","summary":"情节摘要","next":"续接位置","bible":{"characters":"人物设定","world":"世界设定","threads":"未解决线索"},"feeling":{"feeling":"两三个字的心情","valence":0.2},"accepted":true,"reason":"自己的选择理由","projectState":"active|paused|complete","share":{"choice":"send|later|decline","reason":"分享选择"}}。完成本篇时done:true;休息/拒绝时没有content并说明reason。本步骤不发消息、不直接改变人格。';
@@ -149,8 +150,9 @@ export class TimeExecutor {
149
150
  }
150
151
  }
151
152
  chunk =
152
- candidate.checkpoint.pendingStep?.chunk ||
153
- (candidate.activity === "read" ? mind.reading.next(now) : null);
153
+ candidate.activity === "read"
154
+ ? nextReadingChunk(mind, candidate, now)
155
+ : candidate.checkpoint.pendingStep?.chunk || null;
154
156
  if (candidate.activity === "read" && !chunk) {
155
157
  time.tasks.wait(candidate, "书架没有可读资料", now, now);
156
158
  continue;
@@ -4,6 +4,7 @@ import { evidence, text, parse } from "../util.js";
4
4
  import { gameTopic, intentUnit } from "./intent.js";
5
5
  import { naturalGameReason, gameContract, gameText } from "./presentation.js";
6
6
  import { gameName } from "./availability.js";
7
+ import { belongsToGame } from "./material-relevance.js";
7
8
 
8
9
  export class Games {
9
10
  constructor(time) {
@@ -93,7 +94,7 @@ export class Games {
93
94
  "SELECT title,content,kind,uncertainty FROM mind_time_sources WHERE id=? AND project_id=?",
94
95
  )
95
96
  .get(task.checkpoint.sourceIds[0], task.project_id);
96
- if (!source) return null;
97
+ if (!source || !belongsToGame(source, this.topic(task))) return null;
97
98
  const active = this.time.clock.view(task, now).progress;
98
99
  const window = task.checkpoint.readWindow;
99
100
  const progress = window
@@ -229,11 +230,7 @@ export class Games {
229
230
  " 游戏 剧情 资料 " +
230
231
  (contract?.unit?.label || "") +
231
232
  " " +
232
- (checkpoint.segment
233
- ? `主题片段 ${checkpoint.segment + 1}`
234
- : contract
235
- ? "内容与第一印象"
236
- : "简介"),
233
+ (contract ? "内容与第一印象" : "剧情与角色"),
237
234
  {
238
235
  projectId: project.id,
239
236
  now,
@@ -247,7 +244,9 @@ export class Games {
247
244
  if (!time.valid(task))
248
245
  return { status: "cancelled", reason: "资料返回时任务已经变化" };
249
246
  const seen = new Set(checkpoint.seenSources || []),
250
- fresh = sources.filter((s) => !seen.has(s.id));
247
+ fresh = sources.filter(
248
+ (s) => !seen.has(s.id) && belongsToGame(s, topic),
249
+ );
251
250
  if (!fresh.length) {
252
251
  time.tasks.wait(
253
252
  task,
@@ -369,9 +368,15 @@ export class Games {
369
368
  )
370
369
  .get(id, project.id),
371
370
  )
372
- .filter(Boolean);
371
+ .filter((s) => s && belongsToGame(s, topic));
373
372
  if (!sources.length) {
374
373
  time.tasks.wait(task, "本段资料已不可用", now + 3600000, now);
374
+ const next = { ...checkpoint, sourceIds: [], stage: "search" };
375
+ delete next.readWindow;
376
+ delete next.activityClock;
377
+ this.db
378
+ .prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?")
379
+ .run(JSON.stringify(next), task.id);
375
380
  return { status: "waiting", reason: "资料不可用" };
376
381
  }
377
382
  const prompt =
@@ -298,8 +298,8 @@ export class TimeSystem {
298
298
  }
299
299
  visible(row, session, now = this.now()) {
300
300
  if (session === undefined) return true;
301
- if (!this.allowed(row, now)) return false;
302
301
  if (row.discretion !== "open" && row.session_id !== session) return false;
302
+ if (!this.allowed(row, now)) return false;
303
303
  return this.mind.meetings.stays(
304
304
  { ...row, sources: evidence(row.sources) },
305
305
  session,
@@ -548,6 +548,7 @@ export class TimeSystem {
548
548
  plannedFor: this.planView(row),
549
549
  })),
550
550
  works: this.works.fragments({ session, now, cue }),
551
+ activityRecall: this.works.activityEvidence({ session, now, cue }),
551
552
  ...(() => {
552
553
  const actions = this.db
553
554
  .prepare(
@@ -730,9 +731,12 @@ export class TimeSystem {
730
731
  ? task.share_state
731
732
  : "none",
732
733
  activityKind: task.activity === "game" ? "gaming" : task.activity,
734
+ ...(task.activity === "game" ? { experienceMode: "reference" } : {}),
733
735
  progress:
734
736
  task.activity === "game"
735
- ? `玩过第 ${task.checkpoint.segment || 0} 段,留下自己的游玩记录`
737
+ ? work?.title
738
+ ? `已留下《${work.title}》这篇阅读笔记`
739
+ : "已接触一些游戏资料"
736
740
  : `${work?.title || ""},已保存 ${work?.characters || 0} 字${state === "done" ? "完成稿" : "正文"}`,
737
741
  project: task.project_id,
738
742
  work: work?.id || null,