luckytri 1.0.6 → 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 (168) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +7 -3
  3. package/README.md +7 -3
  4. package/docs/en/CHANGELOG.md +35 -16
  5. package/docs/en/chat-style.md +28 -9
  6. package/docs/en/guide.md +5 -5
  7. package/docs/en/her-life.md +2 -2
  8. package/docs/en/webui.md +1 -1
  9. package/docs/zh/CHANGELOG.md +35 -16
  10. package/docs/zh/chat-style.md +30 -9
  11. package/docs/zh/guide.md +5 -5
  12. package/docs/zh/her-life.md +2 -2
  13. package/docs/zh/webui.md +1 -1
  14. package/package.json +1 -1
  15. package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
  16. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  17. package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
  18. package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
  19. package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
  20. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  21. package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
  22. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  23. package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
  24. package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
  25. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  26. package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
  27. package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
  28. package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
  29. package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
  30. package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
  31. package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
  32. package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
  33. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  34. package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
  35. package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
  36. package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
  37. package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
  38. package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
  39. package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
  40. package/public/app/index.html +5 -6
  41. package/public/guide.en.html +3 -3
  42. package/public/guide.html +3 -3
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/update-instance.js +201 -201
  46. package/server/channels/adapters.js +27 -27
  47. package/server/channels/onebot/adapter.js +288 -288
  48. package/server/channels/qqbot/adapter.js +253 -253
  49. package/server/channels/qqbot/api.js +199 -199
  50. package/server/channels/qqbot/credentials.js +11 -11
  51. package/server/channels/qqbot/gateway.js +274 -274
  52. package/server/channels/qqbot/index.js +389 -389
  53. package/server/channels/segments.js +40 -40
  54. package/server/channels/session-key.js +153 -153
  55. package/server/channels/state.js +42 -42
  56. package/server/core/api.js +564 -564
  57. package/server/core/context-builder.js +288 -288
  58. package/server/core/context-compactor.js +454 -454
  59. package/server/core/conversation-cues.js +385 -325
  60. package/server/core/conversation-grounding.js +304 -0
  61. package/server/core/conversation-manager.js +87 -87
  62. package/server/core/dialogue-context.js +244 -0
  63. package/server/core/dialogue-contract.js +35 -0
  64. package/server/core/feedback.js +8 -8
  65. package/server/core/group-style.js +72 -72
  66. package/server/core/initiative-context.js +68 -66
  67. package/server/core/input-budget.js +32 -32
  68. package/server/core/local-demo.js +98 -98
  69. package/server/core/message-manager.js +118 -118
  70. package/server/core/message-scheduler.js +78 -78
  71. package/server/core/model-json.js +190 -190
  72. package/server/core/model-manager.js +5 -8
  73. package/server/core/model-presets.js +900 -900
  74. package/server/core/name-call.js +29 -29
  75. package/server/core/network.js +157 -157
  76. package/server/core/person-name.js +22 -22
  77. package/server/core/persona-manager.js +359 -269
  78. package/server/core/reader-check.js +62 -0
  79. package/server/core/reply-delivery.js +537 -404
  80. package/server/core/reply-target-resolver.js +50 -50
  81. package/server/core/response-generator.js +92 -50
  82. package/server/core/response-validator.js +53 -32
  83. package/server/core/retired-prompts.js +51 -51
  84. package/server/core/sessions.js +109 -109
  85. package/server/core/speaker-names.js +92 -92
  86. package/server/core/topic-tracker.js +65 -65
  87. package/server/core/turn.js +82 -15
  88. package/server/http.js +18 -18
  89. package/server/i18n/decisions.js +209 -209
  90. package/server/i18n/errors.js +2 -0
  91. package/server/i18n/index.js +144 -144
  92. package/server/i18n/labels.js +6 -10
  93. package/server/i18n/models.js +111 -111
  94. package/server/knowledge/api.js +342 -342
  95. package/server/knowledge/embedding-profile.js +114 -114
  96. package/server/knowledge/index.js +13 -13
  97. package/server/knowledge/retrieval.js +93 -93
  98. package/server/knowledge/schema.js +158 -158
  99. package/server/knowledge/vector-index.js +90 -90
  100. package/server/mind/affect.js +265 -265
  101. package/server/mind/attention.js +200 -200
  102. package/server/mind/bonds.js +565 -565
  103. package/server/mind/clock.js +83 -83
  104. package/server/mind/context-selection.js +16 -4
  105. package/server/mind/continuity.js +562 -562
  106. package/server/mind/conversation-origin.js +175 -175
  107. package/server/mind/days.js +210 -210
  108. package/server/mind/evidence-read.js +45 -45
  109. package/server/mind/expression-grounding.js +21 -0
  110. package/server/mind/faces.js +373 -373
  111. package/server/mind/guard.js +68 -60
  112. package/server/mind/initiative.js +338 -338
  113. package/server/mind/life/activities.js +21 -21
  114. package/server/mind/life/context.js +330 -330
  115. package/server/mind/life/growth.js +261 -261
  116. package/server/mind/life/presence.js +305 -305
  117. package/server/mind/meetings.js +1069 -1069
  118. package/server/mind/memory-policy.js +48 -48
  119. package/server/mind/memory.js +888 -888
  120. package/server/mind/nature.js +358 -358
  121. package/server/mind/novelty.js +19 -19
  122. package/server/mind/own-voice.js +35 -0
  123. package/server/mind/periods.js +118 -118
  124. package/server/mind/reading.js +132 -120
  125. package/server/mind/relationship-context.js +71 -71
  126. package/server/mind/relationships.js +390 -390
  127. package/server/mind/salience.js +201 -201
  128. package/server/mind/schema.js +567 -567
  129. package/server/mind/self.js +496 -484
  130. package/server/mind/thoughts.js +274 -274
  131. package/server/mind/time/agenda.js +242 -242
  132. package/server/mind/time/api.js +200 -200
  133. package/server/mind/time/attention.js +34 -34
  134. package/server/mind/time/availability.js +23 -1
  135. package/server/mind/time/executor.js +4 -2
  136. package/server/mind/time/games.js +20 -11
  137. package/server/mind/time/index.js +9 -2
  138. package/server/mind/time/kinds.js +29 -2
  139. package/server/mind/time/material-relevance.js +19 -0
  140. package/server/mind/time/model-material.js +77 -77
  141. package/server/mind/time/presentation.js +35 -51
  142. package/server/mind/time/reading-input.js +31 -0
  143. package/server/mind/time/schedule.js +83 -83
  144. package/server/mind/time/schema.js +84 -84
  145. package/server/mind/time/search.js +383 -383
  146. package/server/mind/time/sharing.js +482 -472
  147. package/server/mind/time/task-links.js +311 -311
  148. package/server/mind/time/tasks.js +41 -3
  149. package/server/mind/time/works.js +527 -432
  150. package/server/mind/traits.js +191 -191
  151. package/server/mind/view.js +62 -12
  152. package/server/runtime/installation.js +116 -116
  153. package/server/runtime/update-manager.js +140 -140
  154. package/server/storage/data-package.js +298 -298
  155. package/server/storage/data-transfer-api.js +50 -50
  156. package/server/storage/data-transfer-worker.js +18 -18
  157. package/server/storage/data-transfer.js +416 -416
  158. package/server/studio/display-names.js +139 -139
  159. package/server/studio/management.js +102 -102
  160. package/server/studio/updates.js +144 -144
  161. package/server/version.js +10 -10
  162. package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
  163. package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
  164. package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
  165. package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
  166. package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
  167. package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
  168. package/public/app/assets/TimePage-C-guwPk2.css +0 -1
@@ -1,404 +1,537 @@
1
- import { replyFocus } from "./conversation-cues.js";
2
- import { replyPrompt } from "./persona-manager.js";
3
- import { generate } from "./response-generator.js";
4
- import {
5
- contradictoryOwnWords,
6
- normalizeResponse,
7
- reviewContext,
8
- validateResponse,
9
- } from "./response-validator.js";
10
- import { deliver, sleep } from "./message-scheduler.js";
11
- import { leaks } from "../mind/guard.js";
12
- import { initiativeAudience } from "../mind/conversation-origin.js";
13
- function isFormatError(error) {
14
- return (
15
- error instanceof SyntaxError ||
16
- /JSON|Unexpected token|格式|气泡|bubbles|choice|输出被截断/i.test(
17
- error?.message || "",
18
- )
19
- );
20
- }
21
-
22
- // Owns this part of the lifecycle; the facade keeps the shared runtime state.
23
- export class ReplyDelivery {
24
- constructor(owner) {
25
- this.owner = owner;
26
- }
27
- async speak(c) {
28
- const {
29
- session,
30
- batch,
31
- turn,
32
- snapshot,
33
- trace,
34
- finish,
35
- models,
36
- model,
37
- nature,
38
- prompt,
39
- policy,
40
- } = c;
41
- const targetUsers = new Set(
42
- snapshot.messages
43
- .filter((m) => turn.targetMessageIds.includes(m.id))
44
- .map((m) => m.speaker),
45
- );
46
- const newerUserMessages = () =>
47
- this.owner.repo
48
- .eventsAfter(session, c.watermark, { simulated: c.simulatedTurn })
49
- .filter((m) => m.role === "user");
50
- const hasRelevantUpdate = () =>
51
- newerUserMessages().some(
52
- (m) =>
53
- c.privateChat ||
54
- targetUsers.has(m.userId) ||
55
- (m.replyId && snapshot.batch.some((b) => b.platformId === m.replyId)),
56
- );
57
- const audienceCurrent = () =>
58
- !snapshot.initiative ||
59
- initiativeAudience(
60
- this.owner.mind,
61
- {
62
- sources: snapshot.initiative.expression?.sources || [],
63
- created: snapshot.initiative.expression?.formedAt,
64
- },
65
- session,
66
- this.owner.now(),
67
- ).allowed;
68
- const isCurrent = () =>
69
- !this.owner.queue.closed &&
70
- (c.preview ||
71
- this.owner.enabled(session, { simulated: c.simulatedTurn })) &&
72
- this.owner.sessionState(session) === c.state &&
73
- !hasRelevantUpdate() &&
74
- audienceCurrent();
75
- const staleExit = () => {
76
- if (!audienceCurrent())
77
- trace.steps.push("话题来源或参与者已改变,这份旧稿未继续发送");
78
- if (newerUserMessages().length)
79
- trace.steps.push("新消息已进入下一批,取消旧稿");
80
- if (
81
- hasRelevantUpdate() &&
82
- c.clearEpoch === (this.owner.clearEpoch.get(session) || 0)
83
- )
84
- this.owner.queue.retain(session, batch);
85
- return finish("stale", "生成期间语境已更新,旧稿未发送");
86
- };
87
- const outdated = () => !c.replay && !c.preview && !isCurrent();
88
- const focus = replyFocus(snapshot, turn).kind;
89
- const fallbackOptions = [
90
- ...(focus === "promise_check"
91
- ? ["你问的约定我得认真核对,刚才我没接住。", "我先认真核对一下。"]
92
- : []),
93
- "嗯",
94
- "好",
95
- "行",
96
- "收到",
97
- ];
98
- const fallbackText = turn.crisis?.clear
99
- ? "你现在还好吗?身边有人能陪着你吗?"
100
- : snapshot.initiative
101
- ? ""
102
- : fallbackOptions.find(
103
- (text) =>
104
- !(nature.forbidden || []).some((word) => text.includes(word)) &&
105
- !snapshot.messages
106
- .filter((m) => m.role === "assistant")
107
- .slice(-12)
108
- .some((m) => m.text === text),
109
- ) || "嗯";
110
- const generationPrompt = replyPrompt(
111
- nature,
112
- prompt,
113
- snapshot.initiative ? "initiative" : "generation",
114
- );
115
- const makeResponse = async (issues = []) => {
116
- try {
117
- const raw = await generate(
118
- models,
119
- model,
120
- generationPrompt,
121
- snapshot,
122
- turn,
123
- trace,
124
- c.generationImages,
125
- issues,
126
- );
127
- return normalizeResponse(raw, turn, fallbackText);
128
- } catch (error) {
129
- if (!isFormatError(error)) throw error;
130
- if (snapshot.initiative) throw error;
131
- trace.steps.push("模型回复格式异常,已使用本地短句兜底");
132
- return { bubbles: [fallbackText], reason: "本地短句兜底" };
133
- }
134
- };
135
- const secrets = this.owner.mind.memory.secretsOutside(session);
136
- const privateFacts = this.owner.mind.meetings.privateSayings(session);
137
- const check = (response) => {
138
- const issues = validateResponse(
139
- response,
140
- snapshot,
141
- turn,
142
- policy.maxReply,
143
- );
144
- if (leaks(response.bubbles, secrets).length)
145
- issues.push("这句话说出了别人要求保密的事,不能在这里说");
146
- if (leaks(response.bubbles, privateFacts).length)
147
- issues.push("这句话把私下知道的事说出来了,不能在这里说");
148
- if (
149
- this.owner.mind.relationships.discloses(
150
- response.bubbles,
151
- session,
152
- this.owner.now(),
153
- )
154
- )
155
- issues.push(
156
- "这句话说出了私下绑定的关系或备注,不能在这里说;把它放进自己的想法也不会扩大公开范围",
157
- );
158
- return issues;
159
- };
160
- const recentPrivateContinuity =
161
- c.direct &&
162
- !c.privateChat &&
163
- snapshot.inner?.continuity?.people?.some(
164
- (person) =>
165
- person.recentShared?.length || person.myPrivateIntentions?.length,
166
- );
167
- const needsDeepCheck = (response) =>
168
- !!snapshot.initiative ||
169
- !!snapshot.inner?.relationships?.requested ||
170
- !!snapshot.inner?.continuity?.requested ||
171
- focus === "clarify_claim" ||
172
- focus === "basis_check" ||
173
- !!(
174
- c.direct &&
175
- snapshot.inner?.continuity?.people?.some(
176
- (person) => person.myElsewhereWords?.length,
177
- )
178
- ) ||
179
- !!recentPrivateContinuity ||
180
- (policy.deepCheck &&
181
- c.pressure < 0.85 &&
182
- (turn.crisis?.clear ||
183
- [
184
- "feeling",
185
- "vent",
186
- "repair",
187
- "promise_check",
188
- "clarify_claim",
189
- "basis_check",
190
- ].includes(focus) ||
191
- (!c.direct &&
192
- (response.bubbles.join("").length > 60 ||
193
- snapshot.batch.some((m) => m.relation === "unresolved")))));
194
- const deepCheck = async (response) => {
195
- try {
196
- const checked = await models.call(
197
- model,
198
- "validation",
199
- replyPrompt(nature, prompt, "validation"),
200
- {
201
- context: reviewContext(snapshot, turn),
202
- decision: {
203
- choice: turn.choice,
204
- reason: turn.reason,
205
- targetMessageIds: turn.targetMessageIds,
206
- },
207
- response,
208
- replyFocus: replyFocus(snapshot, turn),
209
- imageEvidence: c.generationImages.length
210
- ? "复审已附上回合模型看到的同一张画面;据图核对具体描述,不凭空判定为编造。"
211
- : snapshot.vision
212
- ? "本轮有图片观察结果,回复可以依据 context.vision,不要当成编造。"
213
- : snapshot.unavailableImages?.length
214
- ? "本轮图片没有读取成功,回复不应包含具体画面细节。"
215
- : undefined,
216
- ...(snapshot.initiative
217
- ? {
218
- task: "本轮没有收到新消息,是自己先形成念头再分享。逐项检查:有没有捏造对方刚说过或发过消息;有没有把旧消息当成当前提问而补答;有没有把自己的念头换成另一件事;有没有捏造亲历;有没有把其他会话的参与者、项目、约定误认成当前对象或咱们的事。依据 expression.audience.origin 核对真实来源与参与者。未参与者也可以聊:她认为有分享价值并自然给背景,不应仅因shared:false或话题来自别处而否决。公开可知不代表当前对象参与过、知道上下文或有相同需求。expression.words 也可能误认对象,不可因它是已有草稿而免检;可以改成向当前对象介绍新想法。只有具体错误才给 issues。",
219
- }
220
- : {}),
221
- },
222
- trace,
223
- c.generationImages,
224
- );
225
- if (typeof checked.ok !== "boolean" || !Array.isArray(checked.issues)) {
226
- trace.steps.push("回复复审结果格式异常,已按本地校验继续");
227
- return snapshot.initiative
228
- ? ["主动消息的语境核对结果无效,草稿保留"]
229
- : [];
230
- }
231
- return checked.ok
232
- ? []
233
- : checked.issues.length
234
- ? checked.issues
235
- : ["与天性或语境不一致"];
236
- } catch (error) {
237
- trace.steps.push(
238
- `回复复审暂不可用,已按本地校验继续:${error.message}`,
239
- );
240
- return snapshot.initiative
241
- ? ["主动消息的事实与内容核对暂未完成,草稿保留,稍后重新决定"]
242
- : [];
243
- }
244
- };
245
- if (outdated()) return staleExit();
246
- let response = turn.bubbles.length
247
- ? normalizeResponse(
248
- { bubbles: turn.bubbles, reason: turn.reason },
249
- turn,
250
- fallbackText,
251
- )
252
- : await makeResponse();
253
- let issues = check(response);
254
- if (!issues.length && needsDeepCheck(response)) {
255
- if (outdated()) return staleExit();
256
- issues = await deepCheck(response);
257
- }
258
- const rewriteLimit = snapshot.inner?.continuity?.people?.some(
259
- (person) => person.myElsewhereWords?.length,
260
- )
261
- ? 2
262
- : 1;
263
- for (let attempt = 0; issues.length && attempt < rewriteLimit; attempt++) {
264
- trace.validation = issues;
265
- if (outdated()) return staleExit();
266
- response = await makeResponse(issues);
267
- issues = check(response);
268
- if (!issues.length && needsDeepCheck(response)) {
269
- if (outdated()) return staleExit();
270
- issues = await deepCheck(response);
271
- }
272
- }
273
- if (issues.length && contradictoryOwnWords(snapshot, turn)) {
274
- const honest = {
275
- bubbles: ["我前面确实说过,后来解释得前后不一致,是我说乱了。"],
276
- reason: "已发出的原话互相矛盾,先承认自己说乱了",
277
- };
278
- const checked = check(honest);
279
- if (!checked.length) {
280
- trace.steps.push("本人旧话互相矛盾,使用核实后的简短更正");
281
- response = honest;
282
- issues = [];
283
- }
284
- }
285
- if (
286
- issues.length &&
287
- focus === "clarify_claim" &&
288
- issues.some((issue) =>
289
- /第三人的主人|倒置时间|追问者承担误解责任/.test(issue),
290
- )
291
- ) {
292
- const honest = {
293
- bubbles: [
294
- "那句‘你主人’是我回他时说错了,我没有依据说他有主人。后来又解释乱了,是我的问题。",
295
- ],
296
- reason: "旧话错误且原回复对象明确,承认没有依据",
297
- };
298
- if (!check(honest).length) {
299
- trace.steps.push("旧话将第三人关系说错,使用核实后的简短更正");
300
- response = honest;
301
- issues = [];
302
- }
303
- }
304
- if (
305
- issues.length &&
306
- focus === "basis_check" &&
307
- issues.some((issue) => /自己先开口的/.test(issue))
308
- ) {
309
- const honest = {
310
- bubbles: [
311
- "那句是我自己先开口的,你之前没发消息。我只是猜的,没有别的依据。",
312
- ],
313
- reason: "自己先开口的话没有他发来的消息可依,承认是猜的",
314
- };
315
- if (!check(honest).length) {
316
- trace.steps.push("先开口的话被追问依据,使用核实后的简短更正");
317
- response = honest;
318
- issues = [];
319
- }
320
- }
321
- trace.response = response;
322
- if (issues.length) {
323
- if (snapshot.initiative) {
324
- trace.validation = issues;
325
- return finish("error", "想说的话还没整理好,愿望保留,稍后重新决定");
326
- }
327
- trace.validation = issues;
328
- // Nobody called her here: a filler word says nothing to the room, and
329
- // often repeats the last one. Not saying it is the plain answer.
330
- if (
331
- !c.direct &&
332
- !c.privateChat &&
333
- !turn.crisis?.clear &&
334
- !c.replay &&
335
- !c.preview
336
- ) {
337
- trace.steps.push(
338
- "没有人在叫她,回复两次仍未通过校验,不用空话顶替,不说了",
339
- );
340
- return finish("silent", "没被叫到,话没整理好,就不说了");
341
- }
342
- // Checks protect the words; when someone did call her, they must not
343
- // erase the decision to answer.
344
- trace.steps.push("回复两次生成仍未通过校验,使用本地安全短句");
345
- response = { bubbles: [fallbackText], reason: "本地安全短句" };
346
- trace.response = response;
347
- }
348
- if (c.replay) return finish("replayed", "隔离回放完成,未发送或写入记忆");
349
- if (c.preview) return finish("previewed", turn.reason);
350
- // Give a sender who is adding one last line a moment to finish. The
351
- // queue already aggregates incoming messages; this catches the tail of
352
- // a model call before its first bubble is committed to the platform.
353
- if (this.owner.queue.lanes.has(session) && !turn.crisis?.clear)
354
- await sleep(250);
355
- if (!isCurrent()) return staleExit();
356
- response = this.owner.mind.time.reconcile(
357
- response,
358
- session,
359
- this.owner.now(),
360
- snapshot.inner?.currentLife?.current,
361
- );
362
- trace.response = response;
363
- trace.sent = await deliver(
364
- this.owner.repo,
365
- batch.at(-1) || c.anchor,
366
- response.bubbles,
367
- trace,
368
- this.owner.send,
369
- isCurrent,
370
- {
371
- now: this.owner.now,
372
- prepareBubble: (line) =>
373
- this.owner.mind.time.reconcile(
374
- { bubbles: [line] },
375
- session,
376
- this.owner.now(),
377
- snapshot.inner?.currentLife?.current,
378
- ).bubbles[0],
379
- },
380
- );
381
- if (!trace.sent.length && hasRelevantUpdate()) return staleExit();
382
- if (trace.sent.length && !c.simulatedTurn) {
383
- if (turn.share?.workId) {
384
- try {
385
- this.owner.mind.time.sharing.choose(
386
- turn.share.workId,
387
- session,
388
- turn.share,
389
- this.owner.now(),
390
- );
391
- } catch (error) {
392
- trace.steps.push("分享选择未执行:" + error.message);
393
- }
394
- }
395
- this.owner.mind.time.tasks.capture(trace, turn, this.owner.now());
396
- this.owner.mind.time.interaction(
397
- session,
398
- turn.targetMessageIds,
399
- this.owner.now(),
400
- );
401
- }
402
- return finish(trace.sent.length ? "sent" : "cancelled", turn.reason);
403
- }
404
- }
1
+ import { replyFocus } from "./conversation-cues.js";
2
+ import { replyPrompt } from "./persona-manager.js";
3
+ import { generate } from "./response-generator.js";
4
+ import {
5
+ contradictoryOwnWords,
6
+ normalizeResponse,
7
+ reviewContext,
8
+ validateResponse,
9
+ } from "./response-validator.js";
10
+ import { deliver, sleep } from "./message-scheduler.js";
11
+ import { leaks } from "../mind/guard.js";
12
+ import { conversationGrounding } from "./conversation-grounding.js";
13
+ import { initiativeAudience } from "../mind/conversation-origin.js";
14
+ import { readerQuestions, READER_CHECK, INTENT_CHECK } from "./reader-check.js";
15
+ import { currentExchange } from "./dialogue-context.js";
16
+ function isFormatError(error) {
17
+ return (
18
+ error instanceof SyntaxError ||
19
+ /JSON|Unexpected token|格式|气泡|bubbles|choice|输出被截断/i.test(
20
+ error?.message || "",
21
+ )
22
+ );
23
+ }
24
+
25
+ // Owns this part of the lifecycle; the facade keeps the shared runtime state.
26
+ export class ReplyDelivery {
27
+ constructor(owner) {
28
+ this.owner = owner;
29
+ }
30
+ async speak(c) {
31
+ const {
32
+ session,
33
+ batch,
34
+ turn,
35
+ snapshot,
36
+ trace,
37
+ finish,
38
+ models,
39
+ model,
40
+ nature,
41
+ prompt,
42
+ policy,
43
+ } = c;
44
+ const targetUsers = new Set(
45
+ snapshot.messages
46
+ .filter((m) => turn.targetMessageIds.includes(m.id))
47
+ .map((m) => m.speaker),
48
+ );
49
+ const newerUserMessages = () =>
50
+ this.owner.repo
51
+ .eventsAfter(session, c.watermark, { simulated: c.simulatedTurn })
52
+ .filter((m) => m.role === "user");
53
+ const hasRelevantUpdate = () =>
54
+ newerUserMessages().some(
55
+ (m) =>
56
+ c.privateChat ||
57
+ targetUsers.has(m.userId) ||
58
+ (m.replyId && snapshot.batch.some((b) => b.platformId === m.replyId)),
59
+ );
60
+ const audienceCurrent = () =>
61
+ !snapshot.initiative ||
62
+ initiativeAudience(
63
+ this.owner.mind,
64
+ {
65
+ sources: snapshot.initiative.expression?.sources || [],
66
+ created: snapshot.initiative.expression?.formedAt,
67
+ },
68
+ session,
69
+ this.owner.now(),
70
+ ).allowed;
71
+ const isCurrent = () =>
72
+ !this.owner.queue.closed &&
73
+ (c.preview ||
74
+ this.owner.enabled(session, { simulated: c.simulatedTurn })) &&
75
+ this.owner.sessionState(session) === c.state &&
76
+ !hasRelevantUpdate() &&
77
+ audienceCurrent();
78
+ const staleExit = () => {
79
+ if (!audienceCurrent())
80
+ trace.steps.push("话题来源或参与者已改变,这份旧稿未继续发送");
81
+ if (newerUserMessages().length)
82
+ trace.steps.push("新消息已进入下一批,取消旧稿");
83
+ if (
84
+ hasRelevantUpdate() &&
85
+ c.clearEpoch === (this.owner.clearEpoch.get(session) || 0)
86
+ )
87
+ this.owner.queue.retain(session, batch);
88
+ return finish("stale", "生成期间语境已更新,旧稿未发送");
89
+ };
90
+ const outdated = () => !c.replay && !c.preview && !isCurrent();
91
+ const focus = replyFocus(snapshot, turn).kind;
92
+ const fallbackOptions = [
93
+ ...(focus === "acknowledge" ? ["嗯。", "好。"] : []),
94
+ ...(focus === "readability_repair"
95
+ ? ["刚才那句我没说清楚,先不继续绕了。"]
96
+ : []),
97
+ ...(focus === "promise_check"
98
+ ? ["你问的约定我得认真核对,刚才我没接住。", "我先认真核对一下。"]
99
+ : []),
100
+ ...(snapshot.batch.some((m) =>
101
+ /^(?:你)?(?:还)?在(?:吗|不在)[??\s]*$/.test(m.text || ""),
102
+ )
103
+ ? ["在。"]
104
+ : []),
105
+ "这句我还没理清楚,先不乱说。",
106
+ "我还没想明白。",
107
+ ];
108
+ const fallbackText = turn.crisis?.clear
109
+ ? "你现在还好吗?身边有人能陪着你吗?"
110
+ : snapshot.initiative
111
+ ? ""
112
+ : fallbackOptions.find(
113
+ (text) =>
114
+ !(nature.forbidden || []).some((word) => text.includes(word)) &&
115
+ !snapshot.messages
116
+ .filter((m) => m.role === "assistant")
117
+ .slice(-12)
118
+ .some((m) => m.text === text),
119
+ ) || "我还没想明白。";
120
+ const generationPrompt = replyPrompt(
121
+ nature,
122
+ prompt,
123
+ snapshot.initiative ? "initiative" : "generation",
124
+ );
125
+ const makeResponse = async (issues = [], draft = null, plain = false) => {
126
+ try {
127
+ const raw = await generate(
128
+ models,
129
+ model,
130
+ generationPrompt,
131
+ snapshot,
132
+ draft ? { ...turn, bubbles: draft.bubbles } : turn,
133
+ trace,
134
+ c.generationImages,
135
+ issues,
136
+ { plain },
137
+ );
138
+ return normalizeResponse(raw, turn, fallbackText);
139
+ } catch (error) {
140
+ if (!isFormatError(error)) throw error;
141
+ if (snapshot.initiative) throw error;
142
+ trace.steps.push("模型回复格式异常,已使用本地短句兜底");
143
+ return { bubbles: [fallbackText], reason: "本地短句兜底" };
144
+ }
145
+ };
146
+ const secrets = this.owner.mind.memory.secretsOutside(session);
147
+ const privateFacts = this.owner.mind.meetings.privateSayings(session);
148
+ // A title or phrase the user has already said in this room is not a
149
+ // disclosure by itself. Explicit secrets retain the stricter check above.
150
+ const alreadySaid = snapshot.messages
151
+ .filter((m) => m.role === "user")
152
+ .slice(-12)
153
+ .map((m) => m.text || "");
154
+ const check = (response) => {
155
+ const issues = validateResponse(
156
+ response,
157
+ snapshot,
158
+ turn,
159
+ policy.maxReply,
160
+ );
161
+ if (leaks(response.bubbles, secrets).length)
162
+ issues.push("这句话说出了别人要求保密的事,不能在这里说");
163
+ if (leaks(response.bubbles, privateFacts, alreadySaid).length)
164
+ issues.push("这句话把私下知道的事说出来了,不能在这里说");
165
+ if (
166
+ this.owner.mind.relationships.discloses(
167
+ response.bubbles,
168
+ session,
169
+ this.owner.now(),
170
+ )
171
+ )
172
+ issues.push(
173
+ "这句话说出了私下绑定的关系或备注,不能在这里说;把它放进自己的想法也不会扩大公开范围",
174
+ );
175
+ return issues;
176
+ };
177
+ const recentPrivateContinuity =
178
+ c.direct &&
179
+ !c.privateChat &&
180
+ snapshot.inner?.continuity?.people?.some(
181
+ (person) =>
182
+ person.recentShared?.length || person.myPrivateIntentions?.length,
183
+ );
184
+ const needsDeepCheck = (response) =>
185
+ readerQuestions(snapshot, turn).length > 0 ||
186
+ currentExchange(snapshot).thirdPartyClaims.length > 0 ||
187
+ response.bubbles.some((line) =>
188
+ /(?:我|刚才|刚刚|刚还|今天|昨天|之前).{0,25}(?:读过|读了|读完|翻开|在翻|写完|完成了|看完了)/.test(
189
+ line,
190
+ ),
191
+ ) ||
192
+ response.bubbles.some((line) =>
193
+ /食堂|外卖|吃了|吃完|点好|点完|拍照|实拍|出门|上课/.test(line),
194
+ ) ||
195
+ (snapshot.inner?.relationships?.known || []).some(
196
+ (person) =>
197
+ person.name &&
198
+ !snapshot.batch.some(
199
+ (m) => String(m.speaker) === String(person.subjectId),
200
+ ) &&
201
+ response.bubbles.some((line) => line.includes(person.name)),
202
+ ) ||
203
+ response.bubbles.some((line) =>
204
+ /名下|落款|拎着|挂.{0,8}栏|这半|那半|落笔/.test(line),
205
+ ) ||
206
+ response.bubbles.some((line) =>
207
+ /从.{1,16}(?:学来|听来|知道|学的)|(?:教|告诉)我的/.test(line),
208
+ ) ||
209
+ conversationGrounding(snapshot).botOnlyTail >= 6 ||
210
+ snapshot.batch.some((m) =>
211
+ /看不懂|没看懂|听不懂|太抽象|人机|复读|说人话|好好说话|冷淡|冷漠|你觉得我|我的人格|性格怎么样/.test(
212
+ m.text || "",
213
+ ),
214
+ ) ||
215
+ response.bubbles.some((line) =>
216
+ /没重看|我.{0,8}(?:重看|重玩|通关|看完了|玩完了)/.test(line),
217
+ ) ||
218
+ (policy.deepCheck &&
219
+ c.pressure < 0.85 &&
220
+ snapshot.batch.some((m) =>
221
+ /想你|喜欢你|爱你|你知道|是谁|谁是|是不是|不太清|不敢说/.test(
222
+ m.text || "",
223
+ ),
224
+ )) ||
225
+ !!snapshot.initiative ||
226
+ !!snapshot.inner?.relationships?.requested ||
227
+ !!snapshot.inner?.continuity?.requested ||
228
+ focus === "clarify_claim" ||
229
+ focus === "basis_check" ||
230
+ !!(
231
+ c.direct &&
232
+ snapshot.inner?.continuity?.people?.some(
233
+ (person) => person.myElsewhereWords?.length,
234
+ )
235
+ ) ||
236
+ !!recentPrivateContinuity ||
237
+ (policy.deepCheck &&
238
+ c.pressure < 0.85 &&
239
+ (turn.crisis?.clear ||
240
+ [
241
+ "feeling",
242
+ "vent",
243
+ "repair",
244
+ "promise_check",
245
+ "clarify_claim",
246
+ "basis_check",
247
+ ].includes(focus) ||
248
+ (!c.direct &&
249
+ (response.bubbles.join("").length > 60 ||
250
+ snapshot.batch.some((m) => m.relation === "unresolved")))));
251
+ const deepCheck = async (response) => {
252
+ try {
253
+ const checked = await models.call(
254
+ model,
255
+ "validation",
256
+ replyPrompt(nature, prompt, "validation"),
257
+ {
258
+ context: reviewContext(snapshot, turn, response),
259
+ decision: {
260
+ choice: turn.choice,
261
+ reason: turn.reason,
262
+ targetMessageIds: turn.targetMessageIds,
263
+ },
264
+ response,
265
+ experienceEvidence:
266
+ "自己的旧回复可能误报经历,不单独证明实际读过、做完或正在做。声称具体活动须核对currentLife的真实作品、阅读与动作证据;待办和愿望不能证明执行。自己此刻的想法与感受不需要外部动作证明。普通问候不用补答已经换了话题的旧任务。",
267
+ replyFocus: replyFocus(snapshot, turn),
268
+ thirdPartyClaims: currentExchange(snapshot).thirdPartyClaims,
269
+ imageEvidence: c.generationImages.length
270
+ ? "复审已附上回合模型看到的同一张画面;据图核对具体描述,不凭空判定为编造。"
271
+ : snapshot.vision
272
+ ? "本轮有图片观察结果,回复可以依据 context.vision,不要当成编造。"
273
+ : snapshot.unavailableImages?.length
274
+ ? "本轮图片没有读取成功,回复不应包含具体画面细节。"
275
+ : undefined,
276
+ ...(snapshot.initiative
277
+ ? {
278
+ task: "本轮没有收到新消息,是自己先形成念头再分享。逐项检查:有没有捏造对方刚说过或发过消息;有没有把旧消息当成当前提问而补答;有没有把自己的念头换成另一件事;有没有捏造亲历;有没有把其他会话的参与者、项目、约定误认成当前对象或咱们的事。依据 expression.audience.origin 核对真实来源与参与者。未参与者也可以聊:她认为有分享价值并自然给背景,不应仅因shared:false或话题来自别处而否决。公开可知不代表当前对象参与过、知道上下文或有相同需求。expression.words 也可能误认对象,不可因它是已有草稿而免检;可以改成向当前对象介绍新想法。只有具体错误才给 issues。",
279
+ }
280
+ : {}),
281
+ },
282
+ trace,
283
+ c.generationImages,
284
+ );
285
+ if (typeof checked.ok !== "boolean" || !Array.isArray(checked.issues)) {
286
+ trace.steps.push("回复复审结果格式异常,已按本地校验继续");
287
+ return snapshot.initiative
288
+ ? ["主动消息的语境核对结果无效,草稿保留"]
289
+ : [];
290
+ }
291
+ const claims = currentExchange(snapshot).thirdPartyClaims;
292
+ if (checked.ok && claims.length) {
293
+ const claimingSpeakers = new Set(
294
+ snapshot.messages
295
+ .filter((m) => claims.some((claim) => claim.id === m.id))
296
+ .map((m) => m.speaker),
297
+ );
298
+ const intent = await models.call(
299
+ model,
300
+ "validation",
301
+ INTENT_CHECK,
302
+ {
303
+ claims,
304
+ humanWords: snapshot.messages
305
+ .filter(
306
+ (m) => m.role === "user" && !claimingSpeakers.has(m.speaker),
307
+ )
308
+ .slice(-8)
309
+ .map(({ id, speaker, text }) => ({ id, speaker, text })),
310
+ reply: response.bubbles,
311
+ },
312
+ trace,
313
+ );
314
+ if (intent.ok === false && Array.isArray(intent.issues))
315
+ return intent.issues.map(
316
+ (issue) => `第三方解释尚未被本人确认:${issue}`,
317
+ );
318
+ }
319
+ if (checked.ok && readerQuestions(snapshot, turn).length) {
320
+ const reader = await models.call(
321
+ model,
322
+ "validation",
323
+ READER_CHECK,
324
+ {
325
+ questions: readerQuestions(snapshot, turn),
326
+ reply: response.bubbles,
327
+ },
328
+ trace,
329
+ );
330
+ if (reader.ok === false && Array.isArray(reader.issues))
331
+ return reader.issues.map(
332
+ (issue) => `给提问者的解释仍没说清:${issue}`,
333
+ );
334
+ }
335
+ return checked.ok
336
+ ? []
337
+ : checked.issues.length
338
+ ? checked.issues
339
+ : ["与天性或语境不一致"];
340
+ } catch (error) {
341
+ trace.steps.push(
342
+ `回复复审暂不可用,已按本地校验继续:${error.message}`,
343
+ );
344
+ return snapshot.initiative
345
+ ? ["主动消息的事实与内容核对暂未完成,草稿保留,稍后重新决定"]
346
+ : [];
347
+ }
348
+ };
349
+ if (outdated()) return staleExit();
350
+ let response = turn.bubbles.length
351
+ ? normalizeResponse(
352
+ { bubbles: turn.bubbles, reason: turn.reason },
353
+ turn,
354
+ fallbackText,
355
+ )
356
+ : await makeResponse();
357
+ let issues = check(response);
358
+ if (!issues.length && needsDeepCheck(response)) {
359
+ if (outdated()) return staleExit();
360
+ issues = await deepCheck(response);
361
+ }
362
+ const rewriteLimit = snapshot.initiative || c.pressure >= 0.85 ? 1 : 2;
363
+ for (let attempt = 0; issues.length && attempt < rewriteLimit; attempt++) {
364
+ (trace.revisions ||= []).push({
365
+ draft: response.bubbles,
366
+ issues: [...issues],
367
+ });
368
+ trace.validation = issues;
369
+ if (outdated()) return staleExit();
370
+ response = await makeResponse(
371
+ issues,
372
+ response,
373
+ !snapshot.initiative && attempt === rewriteLimit - 1,
374
+ );
375
+ issues = check(response);
376
+ if (!issues.length && needsDeepCheck(response)) {
377
+ if (outdated()) return staleExit();
378
+ issues = await deepCheck(response);
379
+ }
380
+ }
381
+ // Removing a rejected suffix is a mechanical repair with no new factual
382
+ // claims. Preserve the answer, then run every check again.
383
+ if (issues.some((issue) => issue.startsWith("不要把hh反复当句尾装饰"))) {
384
+ const plain = {
385
+ ...response,
386
+ bubbles: response.bubbles.map((line) =>
387
+ line.replace(/[hH]{2,}[。!!~~]*$/, "").trim(),
388
+ ),
389
+ };
390
+ const remaining = check(plain);
391
+ if (!remaining.length) {
392
+ const reviewed = needsDeepCheck(plain) ? await deepCheck(plain) : [];
393
+ if (!reviewed.length) {
394
+ response = plain;
395
+ issues = [];
396
+ trace.steps.push("去掉被退回的hh句尾装饰,原有内容经完整校验后保留");
397
+ }
398
+ }
399
+ }
400
+ if (
401
+ issues.length &&
402
+ ["clarify_claim", "promise_check", "repair", "basis_check"].includes(
403
+ focus,
404
+ ) &&
405
+ contradictoryOwnWords(snapshot, turn)
406
+ ) {
407
+ const honest = {
408
+ bubbles: ["我前面确实说过,后来解释得前后不一致,是我说乱了。"],
409
+ reason: "已发出的原话互相矛盾,先承认自己说乱了",
410
+ };
411
+ const checked = check(honest);
412
+ if (!checked.length) {
413
+ trace.steps.push("本人旧话互相矛盾,使用核实后的简短更正");
414
+ response = honest;
415
+ issues = [];
416
+ }
417
+ }
418
+ if (
419
+ issues.length &&
420
+ focus === "clarify_claim" &&
421
+ issues.some((issue) =>
422
+ /第三人的主人|倒置时间|追问者承担误解责任/.test(issue),
423
+ )
424
+ ) {
425
+ const honest = {
426
+ bubbles: [
427
+ "那句‘你主人’是我回他时说错了,我没有依据说他有主人。后来又解释乱了,是我的问题。",
428
+ ],
429
+ reason: "旧话错误且原回复对象明确,承认没有依据",
430
+ };
431
+ if (!check(honest).length) {
432
+ trace.steps.push("旧话将第三人关系说错,使用核实后的简短更正");
433
+ response = honest;
434
+ issues = [];
435
+ }
436
+ }
437
+ if (
438
+ issues.length &&
439
+ focus === "basis_check" &&
440
+ issues.some((issue) => /自己先开口的/.test(issue))
441
+ ) {
442
+ const honest = {
443
+ bubbles: [
444
+ "那句是我自己先开口的,你之前没发消息。我只是猜的,没有别的依据。",
445
+ ],
446
+ reason: "自己先开口的话没有他发来的消息可依,承认是猜的",
447
+ };
448
+ if (!check(honest).length) {
449
+ trace.steps.push("先开口的话被追问依据,使用核实后的简短更正");
450
+ response = honest;
451
+ issues = [];
452
+ }
453
+ }
454
+ trace.response = response;
455
+ if (issues.length) {
456
+ if (snapshot.initiative) {
457
+ trace.validation = issues;
458
+ return finish("error", "想说的话还没整理好,愿望保留,稍后重新决定");
459
+ }
460
+ trace.validation = issues;
461
+ // Nobody called her here: a filler word says nothing to the room, and
462
+ // often repeats the last one. Not saying it is the plain answer.
463
+ if (
464
+ !c.direct &&
465
+ !c.privateChat &&
466
+ !turn.crisis?.clear &&
467
+ !c.replay &&
468
+ !c.preview
469
+ ) {
470
+ trace.steps.push(
471
+ "没有人在叫她,回复两次仍未通过校验,不用空话顶替,不说了",
472
+ );
473
+ return finish("silent", "没被叫到,话没整理好,就不说了");
474
+ }
475
+ // Checks protect the words; when someone did call her, they must not
476
+ // erase the decision to answer.
477
+ trace.steps.push("回复两次生成仍未通过校验,使用本地安全短句");
478
+ response = { bubbles: [fallbackText], reason: "本地安全短句" };
479
+ trace.response = response;
480
+ }
481
+ if (c.replay) return finish("replayed", "隔离回放完成,未发送或写入记忆");
482
+ if (c.preview) return finish("previewed", turn.reason);
483
+ // Give a sender who is adding one last line a moment to finish. The
484
+ // queue already aggregates incoming messages; this catches the tail of
485
+ // a model call before its first bubble is committed to the platform.
486
+ if (this.owner.queue.lanes.has(session) && !turn.crisis?.clear)
487
+ await sleep(250);
488
+ if (!isCurrent()) return staleExit();
489
+ response = this.owner.mind.time.reconcile(
490
+ response,
491
+ session,
492
+ this.owner.now(),
493
+ snapshot.inner?.currentLife?.current,
494
+ );
495
+ trace.response = response;
496
+ trace.sent = await deliver(
497
+ this.owner.repo,
498
+ batch.at(-1) || c.anchor,
499
+ response.bubbles,
500
+ trace,
501
+ this.owner.send,
502
+ isCurrent,
503
+ {
504
+ now: this.owner.now,
505
+ prepareBubble: (line) =>
506
+ this.owner.mind.time.reconcile(
507
+ { bubbles: [line] },
508
+ session,
509
+ this.owner.now(),
510
+ snapshot.inner?.currentLife?.current,
511
+ ).bubbles[0],
512
+ },
513
+ );
514
+ if (!trace.sent.length && hasRelevantUpdate()) return staleExit();
515
+ if (trace.sent.length && !c.simulatedTurn) {
516
+ if (turn.share?.workId) {
517
+ try {
518
+ this.owner.mind.time.sharing.choose(
519
+ turn.share.workId,
520
+ session,
521
+ turn.share,
522
+ this.owner.now(),
523
+ );
524
+ } catch (error) {
525
+ trace.steps.push("分享选择未执行:" + error.message);
526
+ }
527
+ }
528
+ this.owner.mind.time.tasks.capture(trace, turn, this.owner.now());
529
+ this.owner.mind.time.interaction(
530
+ session,
531
+ turn.targetMessageIds,
532
+ this.owner.now(),
533
+ );
534
+ }
535
+ return finish(trace.sent.length ? "sent" : "cancelled", turn.reason);
536
+ }
537
+ }