luckytri 1.0.7 → 1.0.9

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 (49) hide show
  1. package/README.en.md +7 -3
  2. package/README.md +7 -3
  3. package/docs/en/CHANGELOG.md +45 -24
  4. package/docs/en/guide.md +3 -1
  5. package/docs/zh/CHANGELOG.md +45 -24
  6. package/docs/zh/guide.md +3 -1
  7. package/package.json +1 -1
  8. package/public/guide.en.html +13 -5
  9. package/public/guide.html +12 -5
  10. package/public/tutorial.en.md +3 -1
  11. package/public/tutorial.md +3 -1
  12. package/server/core/context-builder.js +22 -3
  13. package/server/core/contribution-check.js +50 -0
  14. package/server/core/conversation-cues.js +29 -17
  15. package/server/core/conversation-grounding.js +377 -8
  16. package/server/core/dialogue-context.js +197 -2
  17. package/server/core/dialogue-contract.js +59 -0
  18. package/server/core/initiative-context.js +40 -18
  19. package/server/core/model-json.js +36 -3
  20. package/server/core/model-manager.js +7 -10
  21. package/server/core/model-presets.js +169 -24
  22. package/server/core/persona-manager.js +52 -12
  23. package/server/core/reader-check.js +166 -0
  24. package/server/core/reply-delivery.js +216 -10
  25. package/server/core/repository.js +33 -0
  26. package/server/core/response-generator.js +53 -10
  27. package/server/core/response-validator.js +42 -9
  28. package/server/core/turn-processor.js +26 -4
  29. package/server/core/turn.js +298 -51
  30. package/server/i18n/decisions.js +16 -0
  31. package/server/i18n/errors.js +4 -0
  32. package/server/i18n/labels.js +6 -5
  33. package/server/i18n/models.js +11 -2
  34. package/server/mind/expression-grounding.js +24 -0
  35. package/server/mind/own-voice.js +35 -0
  36. package/server/mind/reading.js +15 -3
  37. package/server/mind/time/activity-recall.js +65 -0
  38. package/server/mind/time/availability.js +23 -1
  39. package/server/mind/time/delivery-links.js +130 -130
  40. package/server/mind/time/executor.js +4 -2
  41. package/server/mind/time/games.js +13 -8
  42. package/server/mind/time/index.js +11 -3
  43. package/server/mind/time/kinds.js +29 -2
  44. package/server/mind/time/material-relevance.js +19 -0
  45. package/server/mind/time/reading-input.js +31 -0
  46. package/server/mind/time/sharing.js +10 -0
  47. package/server/mind/time/tasks.js +39 -3
  48. package/server/mind/time/works.js +105 -15
  49. package/server/mind/view.js +29 -9
@@ -1,10 +1,37 @@
1
1
  import { replyFocus } from "./conversation-cues.js";
2
2
  import { INITIATIVE_PROMPT } from "../mind/initiative.js";
3
3
  import { initiativeContext } from "./initiative-context.js";
4
- import { conversationGrounding } from "./conversation-grounding.js";
5
- import { currentExchange, explicitStop } from "./dialogue-context.js";
4
+ import {
5
+ conversationGrounding,
6
+ prefersSingleBubble,
7
+ } from "./conversation-grounding.js";
8
+ import {
9
+ currentExchange,
10
+ dialogueContext,
11
+ explicitStop,
12
+ } from "./dialogue-context.js";
13
+ import { readerQuestions } from "./reader-check.js";
14
+ import {
15
+ botExchange,
16
+ CONTRIBUTION_CHECK,
17
+ CONTRIBUTION_REPAIR,
18
+ } from "./contribution-check.js";
19
+ import {
20
+ claimsLivedAction,
21
+ EXPRESSION_GROUNDING,
22
+ } from "../mind/expression-grounding.js";
6
23
 
7
24
  export const CHOICES = ["speak", "react", "decline", "silent"];
25
+ const INTENT_KINDS = [
26
+ "question",
27
+ "proposal",
28
+ "feeling",
29
+ "sharing",
30
+ "correction",
31
+ "greeting",
32
+ "closing",
33
+ "other",
34
+ ];
8
35
  const LEGACY = {
9
36
  REPLY: "speak",
10
37
  MULTI_MESSAGE: "speak",
@@ -12,8 +39,22 @@ const LEGACY = {
12
39
  SILENT: "silent",
13
40
  };
14
41
 
15
- export function maxBubbles(choice) {
16
- return choice === "speak" ? 3 : 1;
42
+ export function maxBubbles(choice, snapshot) {
43
+ if (choice !== "speak") return 1;
44
+ return prefersSingleBubble(snapshot || {}) ? 1 : 3;
45
+ }
46
+
47
+ export function joinBubbleParts(parts) {
48
+ return parts.reduce((joined, part) => {
49
+ if (!joined) return part;
50
+ if (/^(?:[-*•]|\d+[.)、])\s/u.test(part)) return `${joined}\n${part}`;
51
+ if (/[A-Za-z0-9]$/u.test(joined) && /^[A-Za-z0-9]/u.test(part))
52
+ return `${joined} ${part}`;
53
+ if (/[,、::]$/u.test(joined) || /^[,、;;]/u.test(part))
54
+ return `${joined}${part}`;
55
+ if (/[。!?!?;;…)」』】》]$/u.test(joined)) return `${joined}${part}`;
56
+ return `${joined},${part}`;
57
+ }, "");
17
58
  }
18
59
 
19
60
  const ACTION_NAME = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]{0,40}$/;
@@ -42,7 +83,7 @@ export function normalizeAct(raw, trace) {
42
83
 
43
84
  export function recallWording(snapshot) {
44
85
  if (snapshot.initiative) return null;
45
- const continuity = snapshot.inner?.continuity;
86
+ const continuity = dialogueContext(snapshot).inner?.continuity;
46
87
  const notes = [];
47
88
  if (
48
89
  continuity?.people?.some(
@@ -68,7 +109,8 @@ export function recallWording(snapshot) {
68
109
  }
69
110
 
70
111
  // One look at the conversation: what it means to her, how it moves her, and
71
- // whether and how she answers. Comes back as a single model call.
112
+ // whether and how she answers. A bot exchange gets at most one repair pass
113
+ // when its first draft repeats old promises instead of answering new content.
72
114
  export async function takeTurn(
73
115
  models,
74
116
  profile,
@@ -77,12 +119,6 @@ export async function takeTurn(
77
119
  trace,
78
120
  extra = {},
79
121
  ) {
80
- const {
81
- sourceRows: _rows,
82
- batch: _batch,
83
- persona: _persona,
84
- ...context
85
- } = snapshot;
86
122
  const images = extra.images || [];
87
123
  const initiating =
88
124
  extra.occasion && ["presence", "outreach"].includes(extra.occasion.type);
@@ -93,35 +129,179 @@ export async function takeTurn(
93
129
  : snapshot.unavailableImages?.length
94
130
  ? "本轮图片没有读取成功,不要编造画面内容。"
95
131
  : undefined;
96
- return models.call(
97
- profile,
98
- "turn",
132
+ const turnSystem =
99
133
  extra.occasion && ["presence", "outreach"].includes(extra.occasion.type)
100
134
  ? `${system}\n${INITIATIVE_PROMPT}`
101
- : system,
102
- {
103
- context: initiating
104
- ? initiativeContext(snapshot, extra.occasion)
105
- : { ...context, grounding: conversationGrounding(snapshot) },
106
- ...(extra.occasion
107
- ? {
108
- occasion: initiating
109
- ? {
110
- ...extra.occasion,
111
- initiative: initiativeContext(snapshot, extra.occasion)
112
- .initiative,
113
- }
114
- : extra.occasion,
115
- }
116
- : {}),
117
- ...(imageGuide ? { imageGuide } : {}),
118
- ...(extra.pressure ? { pressure: extra.pressure } : {}),
119
- ...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
120
- ...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
121
- },
135
+ : system;
136
+ const turnInput = {
137
+ context: initiating
138
+ ? initiativeContext(snapshot, extra.occasion)
139
+ : dialogueContext(snapshot),
140
+ ...(extra.occasion
141
+ ? {
142
+ occasion: initiating
143
+ ? {
144
+ ...extra.occasion,
145
+ initiative: initiativeContext(snapshot, extra.occasion)
146
+ .initiative,
147
+ }
148
+ : extra.occasion,
149
+ }
150
+ : {}),
151
+ ...(imageGuide ? { imageGuide } : {}),
152
+ ...(extra.pressure ? { pressure: extra.pressure } : {}),
153
+ ...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
154
+ ...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
155
+ };
156
+ let raw = await models.call(
157
+ profile,
158
+ "turn",
159
+ turnSystem,
160
+ turnInput,
122
161
  trace,
123
162
  images,
124
163
  );
164
+ const words = (
165
+ Array.isArray(raw?.bubbles) ? raw.bubbles : [raw?.bubbles]
166
+ ).filter((word) => typeof word === "string");
167
+ if (
168
+ snapshot.initiative &&
169
+ raw?.choice === "speak" &&
170
+ claimsLivedAction(words.join("\n"))
171
+ ) {
172
+ // Reject a fictional action before experience() can turn its appraisal
173
+ // into a new memory. A self-initiated message has no unanswered request
174
+ // that would justify filling in a different story through rewrites.
175
+ let grounded;
176
+ try {
177
+ grounded = await models.call(
178
+ profile,
179
+ "validation",
180
+ EXPRESSION_GROUNDING,
181
+ {
182
+ candidate: { words },
183
+ currentLife: snapshot.inner?.currentLife || {},
184
+ notes: [],
185
+ self: [],
186
+ },
187
+ trace,
188
+ );
189
+ } catch (error) {
190
+ trace?.steps?.push(`主动经历核对暂不可用,先不分享:${error.message}`);
191
+ }
192
+ if (trace)
193
+ trace.experienceReview = {
194
+ words: [...words],
195
+ ok: grounded?.ok === true,
196
+ reason: grounded?.reason || "实际经历尚未核实",
197
+ };
198
+ if (grounded?.ok !== true)
199
+ return {
200
+ ...raw,
201
+ choice: "silent",
202
+ bubbles: [],
203
+ appraisal: "",
204
+ feelings: [],
205
+ bonds: [],
206
+ contribution: {
207
+ kind: "none",
208
+ point: "旧草稿中的新行动尚无实际记录支持",
209
+ },
210
+ reason: "这段念头的经历依据还没有核实,先留在自己这里",
211
+ };
212
+ }
213
+ const exchange = botExchange(snapshot, raw);
214
+ if (
215
+ exchange &&
216
+ (raw.choice === "speak" || raw.choice === "react") &&
217
+ raw.contribution?.kind !== "none"
218
+ ) {
219
+ try {
220
+ let review = await models.call(
221
+ profile,
222
+ "validation",
223
+ CONTRIBUTION_CHECK,
224
+ {
225
+ botExchange: exchange,
226
+ reply: raw.bubbles || [],
227
+ plannedPoint: raw.contribution?.point || "",
228
+ },
229
+ trace,
230
+ );
231
+ if (trace && typeof review.hasContribution === "boolean")
232
+ trace.contributionReview = review;
233
+ if (review.hasContribution === false && review.shouldRepair === true) {
234
+ trace?.steps?.push("新问题或新段落被上一稿漏接,按当前内容重新想一次");
235
+ try {
236
+ const repaired = await models.call(
237
+ profile,
238
+ "turn",
239
+ `${turnSystem}\n${CONTRIBUTION_REPAIR}`,
240
+ {
241
+ ...turnInput,
242
+ contributionRepair: {
243
+ previousReply: raw.bubbles || [],
244
+ },
245
+ },
246
+ trace,
247
+ images,
248
+ );
249
+ const repairedExchange = botExchange(snapshot, repaired);
250
+ if (
251
+ repairedExchange &&
252
+ ["speak", "react"].includes(repaired.choice) &&
253
+ repaired.contribution?.kind !== "none"
254
+ ) {
255
+ const revisedReview = await models.call(
256
+ profile,
257
+ "validation",
258
+ CONTRIBUTION_CHECK,
259
+ {
260
+ botExchange: repairedExchange,
261
+ reply: repaired.bubbles || [],
262
+ plannedPoint: repaired.contribution?.point || "",
263
+ repairAttempt: true,
264
+ },
265
+ trace,
266
+ );
267
+ review = revisedReview;
268
+ }
269
+ if (trace) {
270
+ trace.contributionReview = review;
271
+ trace.contributionRepair = {
272
+ attempted: true,
273
+ succeeded: review.hasContribution === true,
274
+ };
275
+ }
276
+ if (review.hasContribution === true) return repaired;
277
+ } catch (error) {
278
+ trace?.steps?.push(`按新内容重答失败,本轮先不发:${error.message}`);
279
+ }
280
+ return {
281
+ ...raw,
282
+ choice: "silent",
283
+ bubbles: [],
284
+ feelings: [],
285
+ bonds: [],
286
+ contribution: { kind: "none", point: "新内容没有形成可发送的回复" },
287
+ };
288
+ }
289
+ if (review.hasContribution === false)
290
+ return {
291
+ ...raw,
292
+ choice: "silent",
293
+ bubbles: [],
294
+ feelings: [],
295
+ bonds: [],
296
+ contribution: { kind: "none", point: "没有新的内容,不再换词确认" },
297
+ };
298
+ } catch (error) {
299
+ trace?.steps?.push(
300
+ `接话内容核对暂不可用,继续原有校验:${error.message}`,
301
+ );
302
+ }
303
+ }
304
+ return raw;
125
305
  }
126
306
 
127
307
  const ids = (value) =>
@@ -148,6 +328,17 @@ export function normalizeTurn(raw, snapshot, trace) {
148
328
  .slice(-1)
149
329
  .map((m) => m.id);
150
330
  }
331
+ if (
332
+ choice === "react" &&
333
+ readerQuestions(snapshot, {
334
+ choice,
335
+ targetMessageIds: targets,
336
+ understanding: raw.understanding,
337
+ }).some((q) => targets.includes(q.id))
338
+ ) {
339
+ choice = "speak";
340
+ trace?.steps?.push("对方在要求解释,保留完整回答空间,不按极短反应截断");
341
+ }
151
342
  let bubbles = [];
152
343
  if (Array.isArray(raw.bubbles)) bubbles = raw.bubbles;
153
344
  else if (typeof raw.bubbles === "string") bubbles = [raw.bubbles];
@@ -161,6 +352,26 @@ export function normalizeTurn(raw, snapshot, trace) {
161
352
  .filter((x) => typeof x === "string")
162
353
  .map((x) => x.trim())
163
354
  .filter(Boolean);
355
+ if (choice === "react" && bubbles.length === 1 && bubbles[0].length > 20) {
356
+ choice = "speak";
357
+ trace?.steps?.push(
358
+ "这是一句完整回应,按正常发言保留,不为短反应标签改写原意",
359
+ );
360
+ }
361
+ const bubbleLimit = maxBubbles(choice, snapshot);
362
+ if (bubbles.length > bubbleLimit) {
363
+ if (choice === "speak" && bubbleLimit === 1) {
364
+ bubbles = [joinBubbleParts(bubbles)];
365
+ trace?.steps?.push("根据对方的偏好,把连续补充合成一条完整消息");
366
+ } else {
367
+ bubbles = bubbles.slice(0, bubbleLimit);
368
+ trace?.steps?.push(
369
+ choice === "react"
370
+ ? "简短反应只留第一条气泡"
371
+ : "回复超过当前对方的分段偏好,已收束到允许的条数",
372
+ );
373
+ }
374
+ }
164
375
  if (choice === "silent") bubbles = [];
165
376
  const crisisIds = ids(raw.crisis?.messageIds).filter((id) =>
166
377
  batchIds.has(id),
@@ -177,12 +388,33 @@ export function normalizeTurn(raw, snapshot, trace) {
177
388
  bubbles = [];
178
389
  trace?.steps?.push("有人可能处在危机里,底线要求认真回应");
179
390
  }
391
+ const grounding = conversationGrounding(snapshot);
392
+ const botFollowupLimit =
393
+ !crisis && !snapshot.initiative && grounding.botFollowupLimit;
180
394
  const botLoop =
181
- !crisis && !snapshot.initiative && conversationGrounding(snapshot).botLoop;
182
- if (botLoop) {
395
+ !crisis &&
396
+ !snapshot.initiative &&
397
+ (grounding.botLoop ||
398
+ (raw.contribution?.kind === "none" &&
399
+ !raw.act &&
400
+ !raw.share &&
401
+ !["chat", "rest"].includes(raw.attention?.action) &&
402
+ batch.length > 0 &&
403
+ batch.filter((m) => m.role === "user").length > 0 &&
404
+ batch
405
+ .filter((m) => m.role === "user")
406
+ .every((m) =>
407
+ conversationGrounding(snapshot).botMessageIds.includes(m.id),
408
+ )));
409
+ const botYield = botLoop || botFollowupLimit;
410
+ if (botYield) {
183
411
  choice = "silent";
184
412
  bubbles = [];
185
- trace?.steps?.push("机器人连续确认没有新内容,这段对话在此收住");
413
+ trace?.steps?.push(
414
+ botFollowupLimit
415
+ ? "机器人之间已经连续说过两轮,这次没有新的直接提问或纠正,先留出空间"
416
+ : "机器人连续确认没有新内容,这段对话在此收住",
417
+ );
186
418
  }
187
419
  const stopped = !crisis && explicitStop(snapshot);
188
420
  if (stopped) {
@@ -197,7 +429,7 @@ export function normalizeTurn(raw, snapshot, trace) {
197
429
  return {
198
430
  choice,
199
431
  share:
200
- !botLoop &&
432
+ !botYield &&
201
433
  !stopped &&
202
434
  raw.share &&
203
435
  ["send", "later", "decline"].includes(raw.share.choice)
@@ -208,7 +440,7 @@ export function normalizeTurn(raw, snapshot, trace) {
208
440
  }
209
441
  : null,
210
442
  attention:
211
- !botLoop &&
443
+ !botYield &&
212
444
  raw.attention &&
213
445
  ["continue", "chat", "rest"].includes(raw.attention.action)
214
446
  ? {
@@ -216,16 +448,23 @@ export function normalizeTurn(raw, snapshot, trace) {
216
448
  reason: text(raw.attention.reason, 120),
217
449
  }
218
450
  : { action: "continue" },
219
- appraisal: botLoop
220
- ? "对方在确认先前的话,没有新的内容。"
451
+ appraisal: botYield
452
+ ? botFollowupLimit
453
+ ? "这段互答已经有来有回,先给聊天留点空间。"
454
+ : "对方在确认先前的话,没有新的内容。"
221
455
  : text(raw.appraisal, 200),
222
- reason: botLoop
223
- ? "这段交流已经结束,先收住。"
456
+ reason: botYield
457
+ ? botFollowupLimit
458
+ ? "连续互答两轮,眼下没有直接问题或纠正,先给群里留点空间。"
459
+ : "这段交流已经结束,先收住。"
224
460
  : text(raw.reason || raw.appraisal, 300) || "此刻的判断",
225
- topic: botLoop ? "交流结束" : text(raw.topic, 40),
461
+ topic: botYield ? "暂时停一下" : text(raw.topic, 40),
226
462
  ...(raw.understanding && typeof raw.understanding === "object"
227
463
  ? {
228
464
  understanding: {
465
+ ...(INTENT_KINDS.includes(raw.understanding.kind)
466
+ ? { kind: raw.understanding.kind }
467
+ : {}),
229
468
  messageIds: ids(raw.understanding.messageIds).filter((id) =>
230
469
  batchIds.has(id),
231
470
  ),
@@ -233,6 +472,14 @@ export function normalizeTurn(raw, snapshot, trace) {
233
472
  },
234
473
  }
235
474
  : {}),
475
+ ...(raw.contribution && typeof raw.contribution === "object"
476
+ ? {
477
+ contribution: {
478
+ kind: text(raw.contribution.kind, 20),
479
+ point: text(raw.contribution.point, 180),
480
+ },
481
+ }
482
+ : {}),
236
483
  targetMessageIds: targets,
237
484
  targetUserIds: [
238
485
  ...new Set(
@@ -242,17 +489,17 @@ export function normalizeTurn(raw, snapshot, trace) {
242
489
  ),
243
490
  ],
244
491
  evidenceIds: ids(raw.evidenceIds).filter((id) => known.has(id)),
245
- feelings: (Array.isArray(raw.feelings) && !botLoop ? raw.feelings : [])
492
+ feelings: (Array.isArray(raw.feelings) && !botYield ? raw.feelings : [])
246
493
  .filter((f) => f && typeof f.feeling === "string")
247
494
  .slice(0, 3),
248
- bonds: (Array.isArray(raw.bonds) && !botLoop ? raw.bonds : [])
495
+ bonds: (Array.isArray(raw.bonds) && !botYield ? raw.bonds : [])
249
496
  .filter((b) => b && b.userId != null && typeof b.change === "string")
250
497
  .map((b) => ({ ...b, evidence: ids(b.evidence) }))
251
498
  .slice(0, 4),
252
499
  crisis: crisis ? { clear: true, messageIds: crisisIds } : { clear: false },
253
- act: botLoop || stopped ? null : normalizeAct(raw.act, trace),
254
- bubbles: bubbles.slice(0, maxBubbles(choice)),
255
- maxBubbles: maxBubbles(choice),
500
+ act: botYield || stopped ? null : normalizeAct(raw.act, trace),
501
+ bubbles: bubbles.slice(0, bubbleLimit),
502
+ maxBubbles: bubbleLimit,
256
503
  };
257
504
  }
258
505
 
@@ -27,6 +27,10 @@ export const DECISIONS = {
27
27
  "Context compression failed; it will retry automatically",
28
28
 
29
29
  // orchestrator, delivery
30
+ "删去未通过检查的多余末尾气泡,保留已通过全部校验的原答复":
31
+ "Removed the rejected trailing bubbles and kept the original answer that passed every check",
32
+ "这段念头的经历依据还没有核实,先留在自己这里":
33
+ "The experience behind this thought is not verified yet; keeping it private for now",
30
34
  "有一个备用模型档案不存在,已跳过":
31
35
  "A backup model profile does not exist; skipped",
32
36
  "引用消息未能恢复,保留 unknown,不认领对象":
@@ -173,6 +177,18 @@ export const DECISIONS = {
173
177
  };
174
178
 
175
179
  export const DECISION_PATTERNS = [
180
+ [
181
+ "主动经历核对暂不可用,先不分享:{0}",
182
+ "The proactive experience check is unavailable; holding the message: {0}",
183
+ ],
184
+ [
185
+ "接话内容核对暂不可用,继续原有校验:{0}",
186
+ "The contribution check is unavailable; continuing the existing checks: {0}",
187
+ ],
188
+ [
189
+ "主动消息声称的新行动尚无实际活动记录支持:{0}",
190
+ "The new action claimed by this proactive message lacks activity records: {0}",
191
+ ],
176
192
  ["整理了 {0} 段语境摘要", "Sorted {0} context summaries"],
177
193
  ["QQ 用户·{0}", "QQ user·{0}"],
178
194
  ["读取 QQ {0} 失败:{1}", "Reading QQ {0} failed: {1}"],
@@ -2,6 +2,8 @@
2
2
  // key, exactly as the code writes it. `{0}` stands for a value the code puts
3
3
  // into the message; patterns are used when a message carries one.
4
4
  export const ERRORS = {
5
+ "JSON 同一对象含重复字段,不能确定哪份内容有效":
6
+ "A JSON object contains duplicate fields; the intended value is ambiguous",
5
7
  "写日记期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
6
8
  "Relationships changed while writing the diary; the old draft was not saved and the next attempt will use the new knowledge",
7
9
  "写回顾期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
@@ -457,6 +459,8 @@ export const ERRORS = {
457
459
  "正在推进活动步骤,请稍后测试":
458
460
  "An activity step is in progress. Test again shortly.",
459
461
  只有真实完成稿可以交付: "Only a genuinely finished piece can be delivered",
462
+ "这篇记录引用了与作品无关的资料,不能作为游戏阅读进度分享":
463
+ "This note uses material unrelated to the work and cannot be shared as reading progress",
460
464
  正文包含其他会话的私下内容:
461
465
  "The text contains private content from another session",
462
466
  作品来源不允许在此会话分享:
@@ -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
  [
@@ -23,6 +23,7 @@ export const MODEL_LABELS = {
23
23
 
24
24
  // providers and labels
25
25
  火山方舟: "Volcengine Ark",
26
+ "火山方舟 Agent Plan": "Volcengine Ark Agent Plan",
26
27
  "火山方舟 Coding Plan": "Volcengine Ark Coding Plan",
27
28
  "方舟自动路由(ark-code-latest)": "Ark auto-routing (ark-code-latest)",
28
29
  "GLM 最新别名(glm-latest)": "GLM latest alias (glm-latest)",
@@ -37,6 +38,8 @@ export const MODEL_LABELS = {
37
38
  // notes
38
39
  "控制台管理的动态模型入口;实际使用哪一个模型由 Coding Plan 控制台选择,切换通常数分钟后生效。支持图片理解。":
39
40
  "A dynamic model entry managed in the console; which model is actually used is chosen in the Coding Plan console, and a switch usually takes a few minutes. Supports image understanding.",
41
+ "Agent Plan 自动路由入口;由套餐配置动态选择可用语言模型,支持图片理解。":
42
+ "Agent Plan auto-routing entry; the plan dynamically selects an available language model. Supports image understanding.",
40
43
  [`当前主力。思考默认开启(high),可关闭;支持看图。${FLAT_TAIL}`]: `The current mainstay. Thinking is on by default (high) and can be turned off; supports images. ${FLAT_TAIL_EN}`,
41
44
  [`V4-Pro-0813。思考默认开启(high),可关闭;仅文本。${FLAT_TAIL}`]: `V4-Pro-0813. Thinking is on by default (high) and can be turned off; text only. ${FLAT_TAIL_EN}`,
42
45
  [`旗舰。思考默认开启,可关闭;支持图片、视频、音频输入。${FLAT_TAIL}`]: `Flagship. Thinking is on by default and can be turned off; accepts image, video and audio input. ${FLAT_TAIL_EN}`,
@@ -87,8 +90,6 @@ export const MODEL_LABELS = {
87
90
  "OpenCode Zen · Qwen lightweight multimodal · Anthropic Messages API.",
88
91
  "OpenCode Zen · MiniMax 主力模型 · Chat Completions 接口。":
89
92
  "OpenCode Zen · MiniMax main model · Chat Completions API.",
90
- "OpenCode Zen · 当前限时免费 · Chat Completions 接口。参考 OpenRouter Space Bunny Alpha 规格:1M 上下文、最高 524K 输出,支持图像/视频、推理、工具和 JSON;其匿名第三方提供方可能保留提示和回答。LuckyTri 默认输入 128K、输出 8K、低推理;当前图片链路可用,视频链路未支持。Zen 路由能力与数据政策可能不同。":
91
- "OpenCode Zen · free for a limited time · Chat Completions API. Modeled on the OpenRouter Space Bunny Alpha spec: 1M context, up to 524K output, image/video, reasoning, tools and JSON; its anonymous third-party providers may keep prompts and replies. LuckyTri defaults to 128K input, 8K output and low reasoning; images work today, video is not supported. Zen routes may differ in ability and data policy.",
92
93
  "OpenAI 兼容接口。上下文先按 128K 填,思考档可按供应商再改。":
93
94
  "OpenAI-compatible API. Context is prefilled at 128K; the thinking level can be changed per provider.",
94
95
  "已填好 OpenRouter API 地址。请自行填写模型 ID、API Key,并按所选模型设置上下文、输出和能力参数。":
@@ -100,6 +101,14 @@ export const MODEL_PATTERNS = [
100
101
  "OpenCode Go · {0} · {1} 接口。默认上下文和输出预算为保守起始值,可按账户实际模型上限调整。",
101
102
  "OpenCode Go · {0} · {1} API. Default context and output budgets are conservative starting values; adjust them to your account's real model limits.",
102
103
  ],
104
+ [
105
+ "Agent Plan 专属 Responses 接口 · 上下文 {0} · LuckyTri 输出预算 {1} · 支持图片理解。默认高强度推理;采样参数由模型服务端处理。",
106
+ "Agent Plan Responses API · context {0} · LuckyTri output budget {1} · supports image understanding. High reasoning by default; sampling parameters are handled by the model service.",
107
+ ],
108
+ [
109
+ "Agent Plan 专属 Responses 接口 · 上下文 {0} · LuckyTri 输出预算 {1} · 文本模型。默认高强度推理;采样参数由模型服务端处理。",
110
+ "Agent Plan Responses API · context {0} · LuckyTri output budget {1} · text model. High reasoning by default; sampling parameters are handled by the model service.",
111
+ ],
103
112
  [
104
113
  "Coding Plan 专属 Responses 接口 · 上下文 {0} · 官方最大输出 {1} · 支持图片理解。默认中等推理;Kimi K2.8 Preview 在 LuckyTri 中预留 64K 输出预算以保留长上下文。",
105
114
  "Coding Plan–only Responses API · context {0} · vendor max output {1} · supports image understanding. Medium reasoning by default; Kimi K2.8 Preview reserves a 64K output budget in LuckyTri to keep the long context.",
@@ -0,0 +1,24 @@
1
+ export function claimsLivedAction(content) {
2
+ return String(content || "")
3
+ .split(/[。!?\n]/)
4
+ .some(
5
+ (clause) =>
6
+ !/想|打算|准备|希望|假如|设想|如果/.test(clause) &&
7
+ (/(?:刚才|刚刚|今天|这次|然后|真的|已经)[^。!?]{0,25}(?:读了|读上|读完|读[一二两三几\d]+[段页章节遍]|坐下来读|翻开|翻出来|写完|完成了|打开了|开始读|动手了)/.test(
8
+ clause,
9
+ ) ||
10
+ /我(?:自己)?[^。!?]{0,10}(?:读上了|开始读了|翻开了|完成了|写完了)/.test(
11
+ clause,
12
+ )),
13
+ );
14
+ }
15
+
16
+ export function claimsSharedHistory(content) {
17
+ return /(?:之前|上次|早些时候)[^。!?\n]{0,25}(?:他|她|对方|那个人|你)[^。!?\n]{0,20}(?:说过|聊过|看过|读过|回话|私下)|(?:他|她|对方|那个人|你)[^。!?\n]{0,15}(?:之前|上次|读过|看过|说过)/.test(
18
+ String(content || ""),
19
+ );
20
+ }
21
+
22
+ export const EXPRESSION_GROUNDING = `核对一个准备保存的自我念头是否把愿望写成行动,或把别人的话题误认成共同经历。只输出JSON。
23
+ candidate只是草稿,不是事实。notes/self是过去的想法、愿望或理解;它们可以支持新的感受和看法,不能单独证明这次真的开始、读完或做完。actions/currentLife是实际活动记录。sources只表示引用了哪里,不自动证明动作。按实际记录核对这次声称新做过的具体动作和对象;不能用无关作品或一条待办证明完成。声称共同经历时,还须核对实际参与者和各自说过的话;在别处读到另一人的讨论不证明当前对象参与过。自己的新想法、偏好、假设、打算可以没有执行记录,也不需要已采取外部行动。只指出确实无依据的新经历与归属,不评文风。
24
+ 输出 {"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 =