luckytri 1.0.4 → 1.0.6

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 (156) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/en/CHANGELOG.md +20 -13
  5. package/docs/en/guide.md +7 -5
  6. package/docs/en/her-life.md +2 -2
  7. package/docs/en/webui.md +1 -1
  8. package/docs/zh/CHANGELOG.md +20 -13
  9. package/docs/zh/guide.md +7 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +2 -1
  13. package/public/app/assets/ChatsPage-CFMVozoG.css +1 -0
  14. package/public/app/assets/{ChatsPage-CshdtTzV.js → ChatsPage-DCYMkJ91.js} +2 -2
  15. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-CLyZkxR5.js} +1 -1
  16. package/public/app/assets/{Empty-DmUB2-Bk.css → Empty-DIobYG3x.css} +1 -1
  17. package/public/app/assets/{Field-BjFv4Rfn.js → Field-BpaRwLI_.js} +1 -1
  18. package/public/app/assets/{HeartPage-eqo6ru16.js → HeartPage-BIOMTiwr.js} +1 -1
  19. package/public/app/assets/HeartPage-C-2jvgzF.css +1 -0
  20. package/public/app/assets/{LifePage-CN8GE5NK.js → LifePage-BCgM4JkR.js} +1 -1
  21. package/public/app/assets/LifePage-CG1OHbRK.css +1 -0
  22. package/public/app/assets/MemoryPage-C_Bu4rfb.css +1 -0
  23. package/public/app/assets/{MemoryPage-CCpMN4Wg.js → MemoryPage-vShvRqMt.js} +1 -1
  24. package/public/app/assets/NaturePage-CPZlImT0.css +1 -0
  25. package/public/app/assets/{NaturePage-C0JqdBTR.js → NaturePage-Cy9ptr4G.js} +2 -2
  26. package/public/app/assets/PeoplePage-3RRUITDz.css +1 -0
  27. package/public/app/assets/{PeoplePage-BXGBbO4u.js → PeoplePage-CUZekB2E.js} +1 -1
  28. package/public/app/assets/{PluginsPage-Dksp5AV5.js → PluginsPage-1RM4rmJP.js} +1 -1
  29. package/public/app/assets/{SystemPage-BenIyTvk.css → SystemPage-BI2CB5Ig.css} +1 -1
  30. package/public/app/assets/{SystemPage-Cd7aa8eE.js → SystemPage-BeT5TmIV.js} +1 -1
  31. package/public/app/assets/TimePage-C-guwPk2.css +1 -0
  32. package/public/app/assets/{TimePage-Bs_zImb4.js → TimePage-taTmPMM8.js} +2 -2
  33. package/public/app/assets/{index-BHZcHUMh.css → index-DY5AM0Hz.css} +1 -1
  34. package/public/app/assets/{index-CyRtYxP3.js → index-DdYwC5ZU.js} +3 -3
  35. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-DJmfu2KN.js} +1 -1
  36. package/public/app/assets/{media-Bwtx8blz.js → media-BEtnE0Gv.js} +1 -1
  37. package/public/app/assets/{mind-DtTVrW6Z.js → mind-Dtiw0-QK.js} +1 -1
  38. package/public/app/index.html +6 -5
  39. package/public/guide.en.html +11 -3
  40. package/public/guide.html +6 -3
  41. package/public/tutorial.en.md +2 -0
  42. package/public/tutorial.md +2 -0
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/service-supervisor.js +50 -0
  46. package/scripts/start-server.js +71 -16
  47. package/scripts/update-instance.js +201 -201
  48. package/server/channels/adapters.js +27 -27
  49. package/server/channels/onebot/adapter.js +288 -288
  50. package/server/channels/qqbot/adapter.js +253 -253
  51. package/server/channels/qqbot/api.js +199 -199
  52. package/server/channels/qqbot/credentials.js +11 -11
  53. package/server/channels/qqbot/gateway.js +274 -274
  54. package/server/channels/qqbot/index.js +389 -389
  55. package/server/channels/segments.js +40 -40
  56. package/server/channels/session-key.js +153 -153
  57. package/server/channels/state.js +42 -42
  58. package/server/core/api.js +564 -564
  59. package/server/core/context-builder.js +288 -288
  60. package/server/core/context-compactor.js +454 -454
  61. package/server/core/conversation-cues.js +325 -325
  62. package/server/core/conversation-manager.js +87 -87
  63. package/server/core/feedback.js +8 -8
  64. package/server/core/group-style.js +72 -72
  65. package/server/core/initiative-context.js +66 -66
  66. package/server/core/input-budget.js +32 -32
  67. package/server/core/local-demo.js +98 -98
  68. package/server/core/message-manager.js +118 -118
  69. package/server/core/message-scheduler.js +78 -78
  70. package/server/core/model-json.js +190 -190
  71. package/server/core/model-presets.js +900 -900
  72. package/server/core/name-call.js +29 -29
  73. package/server/core/network.js +157 -157
  74. package/server/core/person-name.js +22 -22
  75. package/server/core/persona-manager.js +2 -1
  76. package/server/core/reply-delivery.js +404 -403
  77. package/server/core/reply-target-resolver.js +50 -50
  78. package/server/core/response-generator.js +50 -50
  79. package/server/core/response-validator.js +14 -2
  80. package/server/core/retired-prompts.js +51 -51
  81. package/server/core/sessions.js +109 -109
  82. package/server/core/speaker-names.js +92 -92
  83. package/server/core/topic-tracker.js +65 -65
  84. package/server/http.js +18 -18
  85. package/server/i18n/decisions.js +209 -206
  86. package/server/i18n/errors.js +4 -0
  87. package/server/i18n/index.js +144 -144
  88. package/server/i18n/models.js +111 -111
  89. package/server/knowledge/api.js +342 -342
  90. package/server/knowledge/embedding-profile.js +114 -114
  91. package/server/knowledge/index.js +13 -13
  92. package/server/knowledge/retrieval.js +93 -93
  93. package/server/knowledge/schema.js +158 -158
  94. package/server/knowledge/vector-index.js +90 -90
  95. package/server/mind/affect.js +265 -265
  96. package/server/mind/attention.js +200 -200
  97. package/server/mind/bonds.js +565 -565
  98. package/server/mind/clock.js +83 -83
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +60 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -323
  108. package/server/mind/life/diary.js +20 -0
  109. package/server/mind/life/growth.js +261 -254
  110. package/server/mind/life/index.js +10 -2
  111. package/server/mind/life/presence.js +305 -305
  112. package/server/mind/meetings.js +1069 -1069
  113. package/server/mind/memory-policy.js +48 -48
  114. package/server/mind/memory.js +888 -888
  115. package/server/mind/nature.js +358 -358
  116. package/server/mind/novelty.js +19 -19
  117. package/server/mind/periods.js +118 -118
  118. package/server/mind/reading.js +120 -120
  119. package/server/mind/relationship-context.js +71 -0
  120. package/server/mind/relationships.js +390 -279
  121. package/server/mind/salience.js +201 -201
  122. package/server/mind/schema.js +567 -567
  123. package/server/mind/self.js +484 -484
  124. package/server/mind/thoughts.js +274 -274
  125. package/server/mind/time/agenda.js +242 -242
  126. package/server/mind/time/api.js +200 -200
  127. package/server/mind/time/attention.js +34 -34
  128. package/server/mind/time/delivery-links.js +130 -130
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +51 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +432 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +9 -1
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-execution.js +2 -1
  141. package/server/runtime/update-manager.js +140 -140
  142. package/server/storage/data-package.js +298 -298
  143. package/server/storage/data-transfer-api.js +50 -50
  144. package/server/storage/data-transfer-worker.js +18 -18
  145. package/server/storage/data-transfer.js +416 -416
  146. package/server/studio/display-names.js +139 -139
  147. package/server/studio/management.js +102 -102
  148. package/server/studio/updates.js +144 -144
  149. package/server/version.js +10 -10
  150. package/public/app/assets/ChatsPage-DfyPCS9N.css +0 -1
  151. package/public/app/assets/HeartPage-Bft9wZp5.css +0 -1
  152. package/public/app/assets/LifePage-BUi5euBF.css +0 -1
  153. package/public/app/assets/MemoryPage-cZvXfkXb.css +0 -1
  154. package/public/app/assets/NaturePage-CHkpb5NK.css +0 -1
  155. package/public/app/assets/PeoplePage-BTU0H1pM.css +0 -1
  156. package/public/app/assets/TimePage-BOms_n4I.css +0 -1
@@ -1,403 +1,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 { 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?.continuity?.requested ||
170
- focus === "clarify_claim" ||
171
- focus === "basis_check" ||
172
- !!(
173
- c.direct &&
174
- snapshot.inner?.continuity?.people?.some(
175
- (person) => person.myElsewhereWords?.length,
176
- )
177
- ) ||
178
- !!recentPrivateContinuity ||
179
- (policy.deepCheck &&
180
- c.pressure < 0.85 &&
181
- (turn.crisis?.clear ||
182
- [
183
- "feeling",
184
- "vent",
185
- "repair",
186
- "promise_check",
187
- "clarify_claim",
188
- "basis_check",
189
- ].includes(focus) ||
190
- (!c.direct &&
191
- (response.bubbles.join("").length > 60 ||
192
- snapshot.batch.some((m) => m.relation === "unresolved")))));
193
- const deepCheck = async (response) => {
194
- try {
195
- const checked = await models.call(
196
- model,
197
- "validation",
198
- replyPrompt(nature, prompt, "validation"),
199
- {
200
- context: reviewContext(snapshot, turn),
201
- decision: {
202
- choice: turn.choice,
203
- reason: turn.reason,
204
- targetMessageIds: turn.targetMessageIds,
205
- },
206
- response,
207
- replyFocus: replyFocus(snapshot, turn),
208
- imageEvidence: c.generationImages.length
209
- ? "复审已附上回合模型看到的同一张画面;据图核对具体描述,不凭空判定为编造。"
210
- : snapshot.vision
211
- ? "本轮有图片观察结果,回复可以依据 context.vision,不要当成编造。"
212
- : snapshot.unavailableImages?.length
213
- ? "本轮图片没有读取成功,回复不应包含具体画面细节。"
214
- : undefined,
215
- ...(snapshot.initiative
216
- ? {
217
- task: "本轮没有收到新消息,是自己先形成念头再分享。逐项检查:有没有捏造对方刚说过或发过消息;有没有把旧消息当成当前提问而补答;有没有把自己的念头换成另一件事;有没有捏造亲历;有没有把其他会话的参与者、项目、约定误认成当前对象或咱们的事。依据 expression.audience.origin 核对真实来源与参与者。未参与者也可以聊:她认为有分享价值并自然给背景,不应仅因shared:false或话题来自别处而否决。公开可知不代表当前对象参与过、知道上下文或有相同需求。expression.words 也可能误认对象,不可因它是已有草稿而免检;可以改成向当前对象介绍新想法。只有具体错误才给 issues。",
218
- }
219
- : {}),
220
- },
221
- trace,
222
- c.generationImages,
223
- );
224
- if (typeof checked.ok !== "boolean" || !Array.isArray(checked.issues)) {
225
- trace.steps.push("回复复审结果格式异常,已按本地校验继续");
226
- return snapshot.initiative
227
- ? ["主动消息的语境核对结果无效,草稿保留"]
228
- : [];
229
- }
230
- return checked.ok
231
- ? []
232
- : checked.issues.length
233
- ? checked.issues
234
- : ["与天性或语境不一致"];
235
- } catch (error) {
236
- trace.steps.push(
237
- `回复复审暂不可用,已按本地校验继续:${error.message}`,
238
- );
239
- return snapshot.initiative
240
- ? ["主动消息的事实与内容核对暂未完成,草稿保留,稍后重新决定"]
241
- : [];
242
- }
243
- };
244
- if (outdated()) return staleExit();
245
- let response = turn.bubbles.length
246
- ? normalizeResponse(
247
- { bubbles: turn.bubbles, reason: turn.reason },
248
- turn,
249
- fallbackText,
250
- )
251
- : await makeResponse();
252
- let issues = check(response);
253
- if (!issues.length && needsDeepCheck(response)) {
254
- if (outdated()) return staleExit();
255
- issues = await deepCheck(response);
256
- }
257
- const rewriteLimit = snapshot.inner?.continuity?.people?.some(
258
- (person) => person.myElsewhereWords?.length,
259
- )
260
- ? 2
261
- : 1;
262
- for (let attempt = 0; issues.length && attempt < rewriteLimit; attempt++) {
263
- trace.validation = issues;
264
- if (outdated()) return staleExit();
265
- response = await makeResponse(issues);
266
- issues = check(response);
267
- if (!issues.length && needsDeepCheck(response)) {
268
- if (outdated()) return staleExit();
269
- issues = await deepCheck(response);
270
- }
271
- }
272
- if (issues.length && contradictoryOwnWords(snapshot, turn)) {
273
- const honest = {
274
- bubbles: ["我前面确实说过,后来解释得前后不一致,是我说乱了。"],
275
- reason: "已发出的原话互相矛盾,先承认自己说乱了",
276
- };
277
- const checked = check(honest);
278
- if (!checked.length) {
279
- trace.steps.push("本人旧话互相矛盾,使用核实后的简短更正");
280
- response = honest;
281
- issues = [];
282
- }
283
- }
284
- if (
285
- issues.length &&
286
- focus === "clarify_claim" &&
287
- issues.some((issue) =>
288
- /第三人的主人|倒置时间|追问者承担误解责任/.test(issue),
289
- )
290
- ) {
291
- const honest = {
292
- bubbles: [
293
- "那句‘你主人’是我回他时说错了,我没有依据说他有主人。后来又解释乱了,是我的问题。",
294
- ],
295
- reason: "旧话错误且原回复对象明确,承认没有依据",
296
- };
297
- if (!check(honest).length) {
298
- trace.steps.push("旧话将第三人关系说错,使用核实后的简短更正");
299
- response = honest;
300
- issues = [];
301
- }
302
- }
303
- if (
304
- issues.length &&
305
- focus === "basis_check" &&
306
- issues.some((issue) => /自己先开口的/.test(issue))
307
- ) {
308
- const honest = {
309
- bubbles: [
310
- "那句是我自己先开口的,你之前没发消息。我只是猜的,没有别的依据。",
311
- ],
312
- reason: "自己先开口的话没有他发来的消息可依,承认是猜的",
313
- };
314
- if (!check(honest).length) {
315
- trace.steps.push("先开口的话被追问依据,使用核实后的简短更正");
316
- response = honest;
317
- issues = [];
318
- }
319
- }
320
- trace.response = response;
321
- if (issues.length) {
322
- if (snapshot.initiative) {
323
- trace.validation = issues;
324
- return finish("error", "想说的话还没整理好,愿望保留,稍后重新决定");
325
- }
326
- trace.validation = issues;
327
- // Nobody called her here: a filler word says nothing to the room, and
328
- // often repeats the last one. Not saying it is the plain answer.
329
- if (
330
- !c.direct &&
331
- !c.privateChat &&
332
- !turn.crisis?.clear &&
333
- !c.replay &&
334
- !c.preview
335
- ) {
336
- trace.steps.push(
337
- "没有人在叫她,回复两次仍未通过校验,不用空话顶替,不说了",
338
- );
339
- return finish("silent", "没被叫到,话没整理好,就不说了");
340
- }
341
- // Checks protect the words; when someone did call her, they must not
342
- // erase the decision to answer.
343
- trace.steps.push("回复两次生成仍未通过校验,使用本地安全短句");
344
- response = { bubbles: [fallbackText], reason: "本地安全短句" };
345
- trace.response = response;
346
- }
347
- if (c.replay) return finish("replayed", "隔离回放完成,未发送或写入记忆");
348
- if (c.preview) return finish("previewed", turn.reason);
349
- // Give a sender who is adding one last line a moment to finish. The
350
- // queue already aggregates incoming messages; this catches the tail of
351
- // a model call before its first bubble is committed to the platform.
352
- if (this.owner.queue.lanes.has(session) && !turn.crisis?.clear)
353
- await sleep(250);
354
- if (!isCurrent()) return staleExit();
355
- response = this.owner.mind.time.reconcile(
356
- response,
357
- session,
358
- this.owner.now(),
359
- snapshot.inner?.currentLife?.current,
360
- );
361
- trace.response = response;
362
- trace.sent = await deliver(
363
- this.owner.repo,
364
- batch.at(-1) || c.anchor,
365
- response.bubbles,
366
- trace,
367
- this.owner.send,
368
- isCurrent,
369
- {
370
- now: this.owner.now,
371
- prepareBubble: (line) =>
372
- this.owner.mind.time.reconcile(
373
- { bubbles: [line] },
374
- session,
375
- this.owner.now(),
376
- snapshot.inner?.currentLife?.current,
377
- ).bubbles[0],
378
- },
379
- );
380
- if (!trace.sent.length && hasRelevantUpdate()) return staleExit();
381
- if (trace.sent.length && !c.simulatedTurn) {
382
- if (turn.share?.workId) {
383
- try {
384
- this.owner.mind.time.sharing.choose(
385
- turn.share.workId,
386
- session,
387
- turn.share,
388
- this.owner.now(),
389
- );
390
- } catch (error) {
391
- trace.steps.push("分享选择未执行:" + error.message);
392
- }
393
- }
394
- this.owner.mind.time.tasks.capture(trace, turn, this.owner.now());
395
- this.owner.mind.time.interaction(
396
- session,
397
- turn.targetMessageIds,
398
- this.owner.now(),
399
- );
400
- }
401
- return finish(trace.sent.length ? "sent" : "cancelled", turn.reason);
402
- }
403
- }
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
+ }