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,15 +1,39 @@
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";
4
+ import {
5
+ conversationGrounding,
6
+ prefersSingleBubble,
7
+ } from "./conversation-grounding.js";
5
8
  import {
6
9
  currentExchange,
7
10
  dialogueContext,
8
11
  explicitStop,
9
12
  } from "./dialogue-context.js";
10
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";
11
23
 
12
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
+ ];
35
+ export const TONES = ["serious", "joking", "teasing", "ironic", "venting"];
36
+ export const STANCES = ["fact", "guess", "opinion", "joke", "hearsay"];
13
37
  const LEGACY = {
14
38
  REPLY: "speak",
15
39
  MULTI_MESSAGE: "speak",
@@ -17,8 +41,22 @@ const LEGACY = {
17
41
  SILENT: "silent",
18
42
  };
19
43
 
20
- export function maxBubbles(choice) {
21
- return choice === "speak" ? 3 : 1;
44
+ export function maxBubbles(choice, snapshot) {
45
+ if (choice !== "speak") return 1;
46
+ return prefersSingleBubble(snapshot || {}) ? 1 : 3;
47
+ }
48
+
49
+ export function joinBubbleParts(parts) {
50
+ return parts.reduce((joined, part) => {
51
+ if (!joined) return part;
52
+ if (/^(?:[-*•]|\d+[.)、])\s/u.test(part)) return `${joined}\n${part}`;
53
+ if (/[A-Za-z0-9]$/u.test(joined) && /^[A-Za-z0-9]/u.test(part))
54
+ return `${joined} ${part}`;
55
+ if (/[,、::]$/u.test(joined) || /^[,、;;]/u.test(part))
56
+ return `${joined}${part}`;
57
+ if (/[。!?!?;;…)」』】》]$/u.test(joined)) return `${joined}${part}`;
58
+ return `${joined},${part}`;
59
+ }, "");
22
60
  }
23
61
 
24
62
  const ACTION_NAME = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]{0,40}$/;
@@ -73,7 +111,8 @@ export function recallWording(snapshot) {
73
111
  }
74
112
 
75
113
  // One look at the conversation: what it means to her, how it moves her, and
76
- // whether and how she answers. Comes back as a single model call.
114
+ // whether and how she answers. A bot exchange gets at most one repair pass
115
+ // when its first draft repeats old promises instead of answering new content.
77
116
  export async function takeTurn(
78
117
  models,
79
118
  profile,
@@ -92,35 +131,179 @@ export async function takeTurn(
92
131
  : snapshot.unavailableImages?.length
93
132
  ? "本轮图片没有读取成功,不要编造画面内容。"
94
133
  : undefined;
95
- return models.call(
96
- profile,
97
- "turn",
134
+ const turnSystem =
98
135
  extra.occasion && ["presence", "outreach"].includes(extra.occasion.type)
99
136
  ? `${system}\n${INITIATIVE_PROMPT}`
100
- : system,
101
- {
102
- context: initiating
103
- ? initiativeContext(snapshot, extra.occasion)
104
- : dialogueContext(snapshot),
105
- ...(extra.occasion
106
- ? {
107
- occasion: initiating
108
- ? {
109
- ...extra.occasion,
110
- initiative: initiativeContext(snapshot, extra.occasion)
111
- .initiative,
112
- }
113
- : extra.occasion,
114
- }
115
- : {}),
116
- ...(imageGuide ? { imageGuide } : {}),
117
- ...(extra.pressure ? { pressure: extra.pressure } : {}),
118
- ...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
119
- ...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
120
- },
137
+ : system;
138
+ const turnInput = {
139
+ context: initiating
140
+ ? initiativeContext(snapshot, extra.occasion)
141
+ : dialogueContext(snapshot),
142
+ ...(extra.occasion
143
+ ? {
144
+ occasion: initiating
145
+ ? {
146
+ ...extra.occasion,
147
+ initiative: initiativeContext(snapshot, extra.occasion)
148
+ .initiative,
149
+ }
150
+ : extra.occasion,
151
+ }
152
+ : {}),
153
+ ...(imageGuide ? { imageGuide } : {}),
154
+ ...(extra.pressure ? { pressure: extra.pressure } : {}),
155
+ ...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
156
+ ...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
157
+ };
158
+ let raw = await models.call(
159
+ profile,
160
+ "turn",
161
+ turnSystem,
162
+ turnInput,
121
163
  trace,
122
164
  images,
123
165
  );
166
+ const words = (
167
+ Array.isArray(raw?.bubbles) ? raw.bubbles : [raw?.bubbles]
168
+ ).filter((word) => typeof word === "string");
169
+ if (
170
+ snapshot.initiative &&
171
+ raw?.choice === "speak" &&
172
+ claimsLivedAction(words.join("\n"))
173
+ ) {
174
+ // Reject a fictional action before experience() can turn its appraisal
175
+ // into a new memory. A self-initiated message has no unanswered request
176
+ // that would justify filling in a different story through rewrites.
177
+ let grounded;
178
+ try {
179
+ grounded = await models.call(
180
+ profile,
181
+ "validation",
182
+ EXPRESSION_GROUNDING,
183
+ {
184
+ candidate: { words },
185
+ currentLife: snapshot.inner?.currentLife || {},
186
+ notes: [],
187
+ self: [],
188
+ },
189
+ trace,
190
+ );
191
+ } catch (error) {
192
+ trace?.steps?.push(`主动经历核对暂不可用,先不分享:${error.message}`);
193
+ }
194
+ if (trace)
195
+ trace.experienceReview = {
196
+ words: [...words],
197
+ ok: grounded?.ok === true,
198
+ reason: grounded?.reason || "实际经历尚未核实",
199
+ };
200
+ if (grounded?.ok !== true)
201
+ return {
202
+ ...raw,
203
+ choice: "silent",
204
+ bubbles: [],
205
+ appraisal: "",
206
+ feelings: [],
207
+ bonds: [],
208
+ contribution: {
209
+ kind: "none",
210
+ point: "旧草稿中的新行动尚无实际记录支持",
211
+ },
212
+ reason: "这段念头的经历依据还没有核实,先留在自己这里",
213
+ };
214
+ }
215
+ const exchange = botExchange(snapshot, raw);
216
+ if (
217
+ exchange &&
218
+ (raw.choice === "speak" || raw.choice === "react") &&
219
+ raw.contribution?.kind !== "none"
220
+ ) {
221
+ try {
222
+ let review = await models.call(
223
+ profile,
224
+ "validation",
225
+ CONTRIBUTION_CHECK,
226
+ {
227
+ botExchange: exchange,
228
+ reply: raw.bubbles || [],
229
+ plannedPoint: raw.contribution?.point || "",
230
+ },
231
+ trace,
232
+ );
233
+ if (trace && typeof review.hasContribution === "boolean")
234
+ trace.contributionReview = review;
235
+ if (review.hasContribution === false && review.shouldRepair === true) {
236
+ trace?.steps?.push("新问题或新段落被上一稿漏接,按当前内容重新想一次");
237
+ try {
238
+ const repaired = await models.call(
239
+ profile,
240
+ "turn",
241
+ `${turnSystem}\n${CONTRIBUTION_REPAIR}`,
242
+ {
243
+ ...turnInput,
244
+ contributionRepair: {
245
+ previousReply: raw.bubbles || [],
246
+ },
247
+ },
248
+ trace,
249
+ images,
250
+ );
251
+ const repairedExchange = botExchange(snapshot, repaired);
252
+ if (
253
+ repairedExchange &&
254
+ ["speak", "react"].includes(repaired.choice) &&
255
+ repaired.contribution?.kind !== "none"
256
+ ) {
257
+ const revisedReview = await models.call(
258
+ profile,
259
+ "validation",
260
+ CONTRIBUTION_CHECK,
261
+ {
262
+ botExchange: repairedExchange,
263
+ reply: repaired.bubbles || [],
264
+ plannedPoint: repaired.contribution?.point || "",
265
+ repairAttempt: true,
266
+ },
267
+ trace,
268
+ );
269
+ review = revisedReview;
270
+ }
271
+ if (trace) {
272
+ trace.contributionReview = review;
273
+ trace.contributionRepair = {
274
+ attempted: true,
275
+ succeeded: review.hasContribution === true,
276
+ };
277
+ }
278
+ if (review.hasContribution === true) return repaired;
279
+ } catch (error) {
280
+ trace?.steps?.push(`按新内容重答失败,本轮先不发:${error.message}`);
281
+ }
282
+ return {
283
+ ...raw,
284
+ choice: "silent",
285
+ bubbles: [],
286
+ feelings: [],
287
+ bonds: [],
288
+ contribution: { kind: "none", point: "新内容没有形成可发送的回复" },
289
+ };
290
+ }
291
+ if (review.hasContribution === false)
292
+ return {
293
+ ...raw,
294
+ choice: "silent",
295
+ bubbles: [],
296
+ feelings: [],
297
+ bonds: [],
298
+ contribution: { kind: "none", point: "没有新的内容,不再换词确认" },
299
+ };
300
+ } catch (error) {
301
+ trace?.steps?.push(
302
+ `接话内容核对暂不可用,继续原有校验:${error.message}`,
303
+ );
304
+ }
305
+ }
306
+ return raw;
124
307
  }
125
308
 
126
309
  const ids = (value) =>
@@ -128,11 +311,44 @@ const ids = (value) =>
128
311
  .map((id) => (typeof id === "string" && /^\d+$/.test(id) ? Number(id) : id))
129
312
  .filter((id) => Number.isSafeInteger(id));
130
313
 
314
+ // A model sometimes closes an object too early or too late, leaving a
315
+ // top-level field inside understanding or contribution, or the reading
316
+ // fields outside. Put them back where the contract says they belong.
317
+ function liftMisplaced(raw, trace) {
318
+ const out = { ...raw };
319
+ let moved = false;
320
+ for (const key of ["understanding", "contribution"]) {
321
+ const inner = out[key];
322
+ if (!inner || typeof inner !== "object" || Array.isArray(inner)) continue;
323
+ for (const field of [
324
+ "choice",
325
+ "targetMessageIds",
326
+ "topic",
327
+ "bubbles",
328
+ "appraisal",
329
+ "reason",
330
+ ])
331
+ if (out[field] === undefined && inner[field] !== undefined) {
332
+ out[field] = inner[field];
333
+ moved = true;
334
+ }
335
+ }
336
+ if (out.understanding && typeof out.understanding === "object")
337
+ for (const field of ["tone", "stance"])
338
+ if (out[field] !== undefined && out.understanding[field] === undefined) {
339
+ out.understanding = { ...out.understanding, [field]: out[field] };
340
+ moved = true;
341
+ }
342
+ if (moved) trace?.steps?.push("回合输出有字段放错了层级,已按约定归位");
343
+ return out;
344
+ }
345
+
131
346
  export function normalizeTurn(raw, snapshot, trace) {
132
347
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
133
348
  throw SyntaxError("回合输出不是 JSON 对象");
134
349
  const known = new Set((snapshot.messages || []).map((m) => m.id));
135
350
  const batchIds = new Set(snapshot.batchIds || []);
351
+ raw = liftMisplaced(raw, trace);
136
352
  let choice = CHOICES.includes(raw.choice)
137
353
  ? raw.choice
138
354
  : LEGACY[raw.action] || null;
@@ -149,7 +365,11 @@ export function normalizeTurn(raw, snapshot, trace) {
149
365
  }
150
366
  if (
151
367
  choice === "react" &&
152
- readerQuestions(snapshot, { choice }).some((q) => targets.includes(q.id))
368
+ readerQuestions(snapshot, {
369
+ choice,
370
+ targetMessageIds: targets,
371
+ understanding: raw.understanding,
372
+ }).some((q) => targets.includes(q.id))
153
373
  ) {
154
374
  choice = "speak";
155
375
  trace?.steps?.push("对方在要求解释,保留完整回答空间,不按极短反应截断");
@@ -173,6 +393,20 @@ export function normalizeTurn(raw, snapshot, trace) {
173
393
  "这是一句完整回应,按正常发言保留,不为短反应标签改写原意",
174
394
  );
175
395
  }
396
+ const bubbleLimit = maxBubbles(choice, snapshot);
397
+ if (bubbles.length > bubbleLimit) {
398
+ if (choice === "speak" && bubbleLimit === 1) {
399
+ bubbles = [joinBubbleParts(bubbles)];
400
+ trace?.steps?.push("根据对方的偏好,把连续补充合成一条完整消息");
401
+ } else {
402
+ bubbles = bubbles.slice(0, bubbleLimit);
403
+ trace?.steps?.push(
404
+ choice === "react"
405
+ ? "简短反应只留第一条气泡"
406
+ : "回复超过当前对方的分段偏好,已收束到允许的条数",
407
+ );
408
+ }
409
+ }
176
410
  if (choice === "silent") bubbles = [];
177
411
  const crisisIds = ids(raw.crisis?.messageIds).filter((id) =>
178
412
  batchIds.has(id),
@@ -189,10 +423,13 @@ export function normalizeTurn(raw, snapshot, trace) {
189
423
  bubbles = [];
190
424
  trace?.steps?.push("有人可能处在危机里,底线要求认真回应");
191
425
  }
426
+ const grounding = conversationGrounding(snapshot);
427
+ const botFollowupLimit =
428
+ !crisis && !snapshot.initiative && grounding.botFollowupLimit;
192
429
  const botLoop =
193
430
  !crisis &&
194
431
  !snapshot.initiative &&
195
- (conversationGrounding(snapshot).botLoop ||
432
+ (grounding.botLoop ||
196
433
  (raw.contribution?.kind === "none" &&
197
434
  !raw.act &&
198
435
  !raw.share &&
@@ -204,10 +441,15 @@ export function normalizeTurn(raw, snapshot, trace) {
204
441
  .every((m) =>
205
442
  conversationGrounding(snapshot).botMessageIds.includes(m.id),
206
443
  )));
207
- if (botLoop) {
444
+ const botYield = botLoop || botFollowupLimit;
445
+ if (botYield) {
208
446
  choice = "silent";
209
447
  bubbles = [];
210
- trace?.steps?.push("机器人连续确认没有新内容,这段对话在此收住");
448
+ trace?.steps?.push(
449
+ botFollowupLimit
450
+ ? "机器人之间已经连续说过两轮,这次没有新的直接提问或纠正,先留出空间"
451
+ : "机器人连续确认没有新内容,这段对话在此收住",
452
+ );
211
453
  }
212
454
  const stopped = !crisis && explicitStop(snapshot);
213
455
  if (stopped) {
@@ -222,7 +464,7 @@ export function normalizeTurn(raw, snapshot, trace) {
222
464
  return {
223
465
  choice,
224
466
  share:
225
- !botLoop &&
467
+ !botYield &&
226
468
  !stopped &&
227
469
  raw.share &&
228
470
  ["send", "later", "decline"].includes(raw.share.choice)
@@ -233,7 +475,7 @@ export function normalizeTurn(raw, snapshot, trace) {
233
475
  }
234
476
  : null,
235
477
  attention:
236
- !botLoop &&
478
+ !botYield &&
237
479
  raw.attention &&
238
480
  ["continue", "chat", "rest"].includes(raw.attention.action)
239
481
  ? {
@@ -241,20 +483,33 @@ export function normalizeTurn(raw, snapshot, trace) {
241
483
  reason: text(raw.attention.reason, 120),
242
484
  }
243
485
  : { action: "continue" },
244
- appraisal: botLoop
245
- ? "对方在确认先前的话,没有新的内容。"
486
+ appraisal: botYield
487
+ ? botFollowupLimit
488
+ ? "这段互答已经有来有回,先给聊天留点空间。"
489
+ : "对方在确认先前的话,没有新的内容。"
246
490
  : text(raw.appraisal, 200),
247
- reason: botLoop
248
- ? "这段交流已经结束,先收住。"
491
+ reason: botYield
492
+ ? botFollowupLimit
493
+ ? "连续互答两轮,眼下没有直接问题或纠正,先给群里留点空间。"
494
+ : "这段交流已经结束,先收住。"
249
495
  : text(raw.reason || raw.appraisal, 300) || "此刻的判断",
250
- topic: botLoop ? "交流结束" : text(raw.topic, 40),
496
+ topic: botYield ? "暂时停一下" : text(raw.topic, 40),
251
497
  ...(raw.understanding && typeof raw.understanding === "object"
252
498
  ? {
253
499
  understanding: {
500
+ ...(INTENT_KINDS.includes(raw.understanding.kind)
501
+ ? { kind: raw.understanding.kind }
502
+ : {}),
254
503
  messageIds: ids(raw.understanding.messageIds).filter((id) =>
255
504
  batchIds.has(id),
256
505
  ),
257
506
  point: text(raw.understanding.point, 180),
507
+ ...(TONES.includes(raw.understanding.tone)
508
+ ? { tone: raw.understanding.tone }
509
+ : {}),
510
+ ...(STANCES.includes(raw.understanding.stance)
511
+ ? { stance: raw.understanding.stance }
512
+ : {}),
258
513
  },
259
514
  }
260
515
  : {}),
@@ -275,17 +530,17 @@ export function normalizeTurn(raw, snapshot, trace) {
275
530
  ),
276
531
  ],
277
532
  evidenceIds: ids(raw.evidenceIds).filter((id) => known.has(id)),
278
- feelings: (Array.isArray(raw.feelings) && !botLoop ? raw.feelings : [])
533
+ feelings: (Array.isArray(raw.feelings) && !botYield ? raw.feelings : [])
279
534
  .filter((f) => f && typeof f.feeling === "string")
280
535
  .slice(0, 3),
281
- bonds: (Array.isArray(raw.bonds) && !botLoop ? raw.bonds : [])
536
+ bonds: (Array.isArray(raw.bonds) && !botYield ? raw.bonds : [])
282
537
  .filter((b) => b && b.userId != null && typeof b.change === "string")
283
538
  .map((b) => ({ ...b, evidence: ids(b.evidence) }))
284
539
  .slice(0, 4),
285
540
  crisis: crisis ? { clear: true, messageIds: crisisIds } : { clear: false },
286
- act: botLoop || stopped ? null : normalizeAct(raw.act, trace),
287
- bubbles: bubbles.slice(0, maxBubbles(choice)),
288
- maxBubbles: maxBubbles(choice),
541
+ act: botYield || stopped ? null : normalizeAct(raw.act, trace),
542
+ bubbles: bubbles.slice(0, bubbleLimit),
543
+ maxBubbles: bubbleLimit,
289
544
  };
290
545
  }
291
546
 
@@ -0,0 +1,85 @@
1
+ // Cheap readings of the batch that a model would otherwise have to work out
2
+ // from scratch: one person's run of messages read as one utterance, and
3
+ // surface signs of tone and certainty. They are hints with their reason, not
4
+ // verdicts; the turn decides what was meant.
5
+ const LAUGH =
6
+ /哈{3,}|h{3,}|233+|笑死|绷不住|乐死|xswl|\[doge\]|(狗头)|\(狗头\)|🐶|😂|🤣/i;
7
+ const TEASE = /开玩笑|逗你|骗你的|假的啦|就随便说说|嘴硬|你行你上|鉴定为/;
8
+ const IRONY =
9
+ /呵呵|真行|可真(?:行|会|厉害)|好家伙|太(?:厉害|聪明)了吧|厉害了我的|谢谢你啊|那可太|笑死个人|真是谢谢/;
10
+ const GUESS = /可能|应该是|估计|大概|好像|也许|说不定|我猜|不确定|感觉是|似乎/;
11
+ const OPINION = /我觉得|我认为|我感觉|在我看来|个人觉得|我更喜欢|还是.{0,6}好/;
12
+ const HEARSAY = /听说|据说|他说|她说|有人说|别人说|朋友说/;
13
+ const COMPLAIN = /烦死|好烦|气死|无语|崩溃|受不了|服了|累死|心累|难受/;
14
+
15
+ export function messageCues(text) {
16
+ const value = String(text || "");
17
+ const tone = [];
18
+ if (LAUGH.test(value)) tone.push("laughing");
19
+ if (TEASE.test(value)) tone.push("teasing");
20
+ if (IRONY.test(value)) tone.push("maybe_ironic");
21
+ if (COMPLAIN.test(value)) tone.push("complaining");
22
+ const stance = HEARSAY.test(value)
23
+ ? "hearsay"
24
+ : GUESS.test(value)
25
+ ? "guess"
26
+ : OPINION.test(value)
27
+ ? "opinion"
28
+ : null;
29
+ return { tone, stance };
30
+ }
31
+
32
+ // Consecutive messages from one person in the batch, read as one turn of
33
+ // speech: "我明天" + "要去面试" + "好紧张" is one sentence typed in pieces.
34
+ export function utterances(messages = [], batchIds = []) {
35
+ const fresh = new Set(batchIds);
36
+ const rows = messages
37
+ .filter((m) => fresh.has(m.id) && m.role === "user")
38
+ .sort((a, b) => a.id - b.id);
39
+ const groups = [];
40
+ for (const m of rows) {
41
+ const last = groups.at(-1);
42
+ if (
43
+ last &&
44
+ last.speaker === String(m.speaker) &&
45
+ m.time - last.until < 90000
46
+ ) {
47
+ last.ids.push(m.id);
48
+ last.text += ` ${m.text || ""}`;
49
+ last.until = m.time;
50
+ } else
51
+ groups.push({
52
+ speaker: String(m.speaker),
53
+ name: m.name,
54
+ ids: [m.id],
55
+ text: m.text || "",
56
+ until: m.time,
57
+ });
58
+ }
59
+ return groups
60
+ .filter((g) => g.ids.length > 1)
61
+ .map(({ until: _until, ...g }) => ({
62
+ ...g,
63
+ text: g.text.trim().slice(0, 160),
64
+ }));
65
+ }
66
+
67
+ export function understandingHints(messages = [], batchIds = []) {
68
+ const fresh = new Set(batchIds);
69
+ const cues = messages
70
+ .filter((m) => fresh.has(m.id) && m.role === "user")
71
+ .map((m) => ({ messageId: m.id, ...messageCues(m.text) }))
72
+ .filter((c) => c.tone.length || c.stance)
73
+ .map((c) => ({
74
+ messageId: c.messageId,
75
+ ...(c.tone.length ? { tone: c.tone } : {}),
76
+ ...(c.stance ? { stance: c.stance } : {}),
77
+ }));
78
+ const groups = utterances(messages, batchIds);
79
+ if (!cues.length && !groups.length) return null;
80
+ return {
81
+ ...(groups.length ? { utterances: groups } : {}),
82
+ ...(cues.length ? { surfaceCues: cues } : {}),
83
+ note: "从字面看出的语气与把握程度提示,不是结论:反讽、玩笑要结合前后文判断;连续几条是同一个人分开打的一句话。",
84
+ };
85
+ }