luckytri 1.0.0 → 1.0.2

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 (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -1,630 +1,630 @@
1
- import { parseSessionKey } from "../channels/session-key.js";
2
- import { initiativeContext, initiativeSnapshot } from "./initiative-context.js";
3
- import { withFallback } from "./model-manager.js";
4
- import { replyPrompt, prompts } from "./persona-manager.js";
5
- import { buildContext, perceive } from "./context-builder.js";
6
- import {
7
- loadVisionImages,
8
- visionInputs,
9
- classifyVision,
10
- classifyLoaded,
11
- descriptionsFor,
12
- mergeVision,
13
- saveVision,
14
- markVisionSeen,
15
- visionWindow,
16
- } from "./vision-manager.js";
17
- import { normalizeTurn, takeTurn } from "./turn.js";
18
- import {
19
- initiativeAudience,
20
- originSummary,
21
- } from "../mind/conversation-origin.js";
22
- const OCCASIONS = {
23
- wake: "你刚睡醒,看到了睡着时收到的消息。它们是之前发的,按消息时间理解,可以自然地说刚看到。",
24
- backlog:
25
- "这些是你刚才没细看的消息,现在回头看了一眼,已经隔了一会儿,不必每条都回。",
26
- outreach:
27
- "这是你自己先前想说的一句(occasion.thought),可能来自你正在为自己而活的事。现在仍然合适就说这一句,不合适就 silent。occasion.returned 是你写下这句话之后又见到的人;人已经出现了,就按现在的情况决定,不要假装还没见过。不要改口说没人找你,不要把留下对方当成对方的责任,不要把「我只是助手」当成收尾。",
28
- presence:
29
- "你自己在这里待了一会儿。不是等待一个必须回答的问题;看看自己的兴趣、念头、心情和对人的感觉,此刻想不想说点什么,由你选择。",
30
- };
31
- function isPrivateSession(session) {
32
- if (String(session).startsWith("preview:")) return true;
33
- try {
34
- return parseSessionKey(session).kind === "private";
35
- } catch {
36
- return String(session).startsWith("private");
37
- }
38
- }
39
- function isFormatError(error) {
40
- return (
41
- error instanceof SyntaxError ||
42
- /JSON|Unexpected token|格式|气泡|bubbles|choice|输出被截断/i.test(
43
- error?.message || "",
44
- )
45
- );
46
- }
47
-
48
- // Owns this part of the lifecycle; the facade keeps the shared runtime state.
49
- export class TurnProcessor {
50
- constructor(owner) {
51
- this.owner = owner;
52
- }
53
- async process(
54
- session,
55
- batch,
56
- {
57
- replay = false,
58
- simulated = false,
59
- preview = null,
60
- occasion: planned = null,
61
- backlog = false,
62
- anchor = null,
63
- } = {},
64
- ) {
65
- let occasion = planned;
66
- const ownInitiative =
67
- !batch.length && ["presence", "outreach"].includes(planned?.type);
68
- const simulatedTurn =
69
- !replay && (simulated || !!preview || batch.some((m) => m.simulated));
70
- const live = !replay && !simulatedTurn;
71
- const clock = () =>
72
- replay || simulatedTurn
73
- ? (batch.at(-1)?.time ?? this.owner.now())
74
- : this.owner.now();
75
- const nature = preview?.nature
76
- ? { ...this.owner.mind.nature.current(), ...preview.nature }
77
- : this.owner.mind.traits.effective(
78
- this.owner.mind.nature.current(clock()),
79
- clock(),
80
- );
81
- let watermark = batch.at(-1)?.seq ?? this.owner.repo.latest(session);
82
- const eventMode = simulatedTurn;
83
- let resolved = null;
84
- let gate = null;
85
- if (live && !occasion) {
86
- if (!this.owner.enabled(session, { simulated: false })) {
87
- // She is not in this conversation; what passes here is not unread.
88
- this.owner.mind.look(session, watermark, this.owner.now());
89
- } else {
90
- resolved = perceive(
91
- this.owner.repo,
92
- session,
93
- watermark,
94
- eventMode,
95
- nature.name,
96
- );
97
- gate = this.owner.gate(session, batch, resolved, clock(), nature);
98
- if (!gate.look && backlog)
99
- return { status: "glanced", reason: gate.reason };
100
- if (backlog) occasion = { type: "backlog" };
101
- }
102
- }
103
- const trace = this.owner.repo.trace(
104
- session,
105
- replay ? "replay" : preview ? "preview" : simulatedTurn ? "demo" : "live",
106
- );
107
- const policy = this.owner.policy(preview?.viewSession || session);
108
- const state = this.owner.sessionState(session);
109
- const clearEpoch = this.owner.clearEpoch.get(session) || 0;
110
- const privateChat = isPrivateSession(session);
111
- if (!replay && !preview)
112
- for (const m of batch)
113
- this.owner.repo.db
114
- .prepare(
115
- "UPDATE core_jobs SET status='running',trace_id=? WHERE seq=?",
116
- )
117
- .run(trace.id, m.seq);
118
- const finish = (status, reason) => {
119
- trace.reason = reason;
120
- if (!replay && !preview)
121
- for (const m of batch)
122
- this.owner.repo.db
123
- .prepare("UPDATE core_jobs SET status=? WHERE seq=?")
124
- .run(status, m.seq);
125
- this.owner.repo.finish(trace, status);
126
- if (
127
- !replay &&
128
- !preview &&
129
- this.owner.repo.db
130
- .prepare("SELECT id FROM sessions WHERE id=?")
131
- .get(session)
132
- )
133
- this.owner.store.log(
134
- session,
135
- "语境决策",
136
- reason,
137
- trace.sent?.join("\n") || "",
138
- Number(!!simulatedTurn),
139
- );
140
- return trace;
141
- };
142
- try {
143
- if (
144
- !replay &&
145
- !preview &&
146
- !this.owner.enabled(session, { simulated: simulatedTurn })
147
- ) {
148
- this.owner.mind.unlived(batch.map((m) => m.seq));
149
- return finish("silent", "会话已暂停或处于模拟模式");
150
- }
151
- if (gate) {
152
- trace.attention = {
153
- look: gate.look,
154
- score: gate.score,
155
- threshold: gate.threshold,
156
- reason: gate.reason,
157
- };
158
- if (!gate.look) {
159
- if (gate.defer) this.owner.mind.defer(session);
160
- // A glance still counts as seeing who is around; a closer look
161
- // records it after the turn, once she has noticed any absence.
162
- else
163
- this.owner.mind.bonds.meet(
164
- batch
165
- .filter((m) => m.role === "user")
166
- .map((m) => ({ userId: String(m.userId), name: m.name })),
167
- session,
168
- this.owner.now(),
169
- );
170
- return finish(gate.defer ? "deferred" : "glanced", gate.reason);
171
- }
172
- batch = gate.rows.length ? gate.rows : batch;
173
- watermark = Math.max(watermark, batch.at(-1).seq);
174
- }
175
- if (!replay && !preview)
176
- await this.owner.restoreQuotes(session, batch, trace);
177
- const prompt = prompts(this.owner.repo);
178
- const now = clock();
179
- const hasKey =
180
- this.owner.store.settings().apiKey || process.env.LLM_API_KEY;
181
- let model;
182
- try {
183
- model = this.owner.models.profile();
184
- } catch (error) {
185
- // A fresh installation has no model yet; the offline sample keeps the
186
- // simulator usable. A live turn surfaces the configuration error.
187
- if (
188
- preview ||
189
- (simulatedTurn && this.owner.store.settings().demo && !hasKey)
190
- )
191
- return this.owner.offline(session, batch, trace, finish, [], preview);
192
- throw error;
193
- }
194
- const models = withFallback(
195
- this.owner.models,
196
- this.owner.fallbackFor(policy, model, trace),
197
- );
198
- resolved ||= perceive(
199
- this.owner.repo,
200
- session,
201
- watermark,
202
- eventMode,
203
- nature.name,
204
- );
205
- const batchIds = batch.map((m) => m.seq);
206
- const window = resolved.filter((m) => !m.referenceOnly).slice(-60);
207
- // The speakers in this batch are the people whose history matters now.
208
- // Pulling everyone from a long room window made unrelated plans compete
209
- // with the exchange actually taking place.
210
- const people = [
211
- ...new Set(
212
- batch.filter((m) => m.role === "user").map((m) => String(m.userId)),
213
- ),
214
- ];
215
- if (ownInitiative && privateChat && !people.length) {
216
- try {
217
- people.push(String(parseSessionKey(session).nativeId));
218
- } catch {
219
- /* An unparseable destination has no person to infer. */
220
- }
221
- }
222
- // Replays exclude mutable memory so later corrections cannot leak into
223
- // the past; her inner state is folded as of the replayed moment.
224
- const memories =
225
- policy.memory && !replay && !ownInitiative
226
- ? this.owner.mind.memory.retrieve(session, batch, now, {
227
- people,
228
- touch: live,
229
- forSpeech: true,
230
- })
231
- : [];
232
- const knowledge = ownInitiative
233
- ? []
234
- : replay
235
- ? this.owner.knowledge.retrieve(session, batch, now)
236
- : await this.owner.knowledge.retrieveWithEmbed(session, batch, now);
237
- const summaryView =
238
- !ownInitiative && !simulatedTurn && policy.compaction !== false
239
- ? this.owner.compactor.forPrompt(session, {
240
- before: replay ? batch[0].seq : Number.MAX_SAFE_INTEGER,
241
- timeZone: policy.timeZone,
242
- })
243
- : { summaries: [], coverage: 0, start: 0 };
244
- const stages =
245
- policy.memory && !ownInitiative
246
- ? this.owner.repo.db
247
- .prepare(
248
- "SELECT first_seq,last_seq,data FROM core_stages WHERE session_id=? AND last_seq<=? ORDER BY last_seq DESC LIMIT 5",
249
- )
250
- .all(
251
- session,
252
- summaryView.summaries.length
253
- ? summaryView.start - 1
254
- : watermark,
255
- )
256
- .map((s) => ({
257
- first_seq: s.first_seq,
258
- last_seq: s.last_seq,
259
- summary: JSON.parse(s.data).summary,
260
- reliability:
261
- "未核实的阶段线索,不是人物事实;冲突时以原文和记忆为准",
262
- }))
263
- : [];
264
- const view = this.owner.mind.view({
265
- session: preview?.viewSession || session,
266
- kind: privateChat ? "private" : "group",
267
- people,
268
- cue: batch.map((m) => ({
269
- role: m.role,
270
- userId: m.userId,
271
- text: m.text || "",
272
- relation: resolved.find((line) => line.id === m.seq)?.relation,
273
- mentioned: m.mentioned === true,
274
- })),
275
- now,
276
- });
277
- const snapshot = ownInitiative
278
- ? initiativeSnapshot(session, window, nature, now, policy.timeZone)
279
- : buildContext(
280
- this.owner.repo,
281
- session,
282
- watermark,
283
- model,
284
- policy,
285
- batchIds,
286
- memories,
287
- nature,
288
- now,
289
- {
290
- knowledge,
291
- stages,
292
- simulated: simulatedTurn,
293
- summaries: summaryView.summaries,
294
- coverage: summaryView.coverage,
295
- summaryStart: summaryView.start,
296
- self: view.self,
297
- inner: view.inner,
298
- nameOf: (id) => this.owner.mind.bonds.name(id),
299
- resolved,
300
- },
301
- );
302
- if (ownInitiative) {
303
- snapshot.self = view.self;
304
- snapshot.inner = view.inner;
305
- }
306
- const visionModel = model.vision
307
- ? model
308
- : this.owner.fallbackFor(null, model).find((item) => item.vision) ||
309
- model;
310
- const direct = snapshot.batch.some((m) => m.relation === "direct");
311
- const media = ownInitiative
312
- ? { images: [], unavailable: [] }
313
- : visionInputs(snapshot, visionModel, {
314
- selective: !!policy.selectiveVision,
315
- });
316
- const early = classifyVision(
317
- this.owner.repo.db,
318
- snapshot.sessionId,
319
- media.images,
320
- );
321
- const loaded = early.pending.length
322
- ? await this.owner.loadVisionImages(early.pending, {
323
- sessionId: snapshot.sessionId,
324
- fetchImage: this.owner.fetchImage,
325
- })
326
- : { images: [], unavailable: [] };
327
- const planned = classifyLoaded(
328
- this.owner.repo.db,
329
- snapshot.sessionId,
330
- loaded.images,
331
- {
332
- batchIds: snapshot.batchIds,
333
- direct,
334
- modelCanSee: !!model.vision,
335
- },
336
- );
337
- mergeVision(snapshot, [...early.cached, ...planned.cached]);
338
- if (early.cached.length || planned.cached.length)
339
- trace.steps.push("图片使用已保存的观察,不再提交画面");
340
- const describe = planned.describe;
341
- let show = planned.show.slice();
342
- if (!replay)
343
- snapshot.topics = this.owner.topics.recent(session, watermark, now);
344
- snapshot.unavailableImages = [
345
- ...media.unavailable,
346
- ...loaded.unavailable,
347
- ];
348
- trace.snapshot = { ...snapshot, sourceRows: undefined, batch: undefined };
349
- trace.config = {
350
- nature: { ...nature },
351
- policy,
352
- model: { ...model, apiKey: undefined },
353
- prompts: prompt,
354
- };
355
- trace.affect = view.affect;
356
- if (
357
- simulatedTurn &&
358
- !hasKey &&
359
- (preview || this.owner.store.settings().demo)
360
- )
361
- return this.owner.offline(
362
- session,
363
- batch,
364
- trace,
365
- finish,
366
- snapshot.messages,
367
- preview,
368
- state,
369
- );
370
- if (describe.length) {
371
- try {
372
- const observed = await models.call(
373
- visionModel,
374
- "vision",
375
- prompt.system +
376
- "\n" +
377
- prompt.vision +
378
- "\n每张图按 messageId 独立观察。description 只写可见元素、文字和表情语气;前后语境仅用来消歧,不把当前话题、发送意图或给谁看的猜测写进可缓存的描述。表情包台词不是事实,不从前一张图推断这一张。",
379
- {
380
- messages: visionWindow(snapshot.messages, [
381
- ...describe.map((image) => image.messageId),
382
- ...snapshot.batchIds,
383
- ]),
384
- batchIds: snapshot.batchIds,
385
- },
386
- trace,
387
- describe,
388
- );
389
- const paired = descriptionsFor(describe, observed);
390
- if (paired.length) {
391
- saveVision(this.owner.repo.db, snapshot.sessionId, paired);
392
- mergeVision(
393
- snapshot,
394
- paired.map(({ image, description }) => ({
395
- messageId: image.messageId,
396
- description,
397
- })),
398
- );
399
- } else if (
400
- observed &&
401
- typeof observed === "object" &&
402
- !snapshot.vision
403
- )
404
- snapshot.vision = observed;
405
- } catch (error) {
406
- trace.steps.push(
407
- `图片理解失败,本轮不根据画面编造:${error.message}`,
408
- );
409
- snapshot.unavailableImages.push(
410
- ...describe.map((image) => ({
411
- messageId: image.messageId,
412
- reason: "图片理解调用失败",
413
- })),
414
- );
415
- if (model.vision) show = show.concat(describe);
416
- }
417
- }
418
- if (snapshot.vision) trace.snapshot.vision = snapshot.vision;
419
- if (!ownInitiative) {
420
- await this.owner.perceivers.apply(snapshot, { replay, now });
421
- const offered = this.owner.capabilities.offered({
422
- kind: privateChat ? "private" : "group",
423
- now,
424
- });
425
- if (offered.length) snapshot.canDo = offered;
426
- }
427
- const generationImages = model.vision ? show : [];
428
- if (!replay && !preview) {
429
- const rounds = this.owner.repo.db
430
- .prepare(
431
- "SELECT COUNT(DISTINCT trace_id) n FROM core_outbox WHERE session_id=? AND time>? AND status IN ('confirmed','uncertain','sending')",
432
- )
433
- .get(session, this.owner.now() - 60000).n;
434
- const roundLimit = privateChat ? 30 : direct ? 20 : 6;
435
- if (rounds >= roundLimit)
436
- return finish("silent", `每分钟发言轮数限速(${roundLimit}轮)`);
437
- }
438
- const pressure = this.owner.mind.budget.pressure("conversation", now);
439
- const occasionData = occasion
440
- ? {
441
- type: occasion.type,
442
- note: OCCASIONS[occasion.type] || "",
443
- ...occasion.data,
444
- }
445
- : undefined;
446
- if (["presence", "outreach"].includes(occasion?.type)) {
447
- const expression = occasionData.expression;
448
- const audience = initiativeAudience(
449
- this.owner.mind,
450
- { sources: expression?.sources || [], created: expression?.formedAt },
451
- session,
452
- now,
453
- );
454
- if (!audience.allowed) return finish("silent", audience.reason);
455
- if (expression)
456
- expression.audience = {
457
- ...audience,
458
- origin: originSummary(audience.origin),
459
- };
460
- snapshot.initiative = occasionData;
461
- trace.snapshot = initiativeContext(snapshot);
462
- }
463
- if (
464
- live &&
465
- !occasion &&
466
- this.owner.batchMovedOn(session, batch, snapshot, privateChat)
467
- ) {
468
- trace.steps.push(
469
- "读房间的这几秒里,要回的人又说了话:先不花一次调用,并入下一批一起回",
470
- );
471
- return finish("stale", "读房间时又有新话,并入下一批");
472
- }
473
- let turn;
474
- try {
475
- turn = normalizeTurn(
476
- await takeTurn(
477
- models,
478
- model,
479
- replyPrompt(
480
- nature,
481
- prompt,
482
- snapshot.initiative ? "initiative" : "turn",
483
- ),
484
- snapshot,
485
- trace,
486
- {
487
- images: generationImages,
488
- occasion: occasionData,
489
- pressure:
490
- pressure >= 0.7 ? "今天已经说了很多话,能短就短" : undefined,
491
- },
492
- ),
493
- snapshot,
494
- trace,
495
- );
496
- if (generationImages.length)
497
- markVisionSeen(
498
- this.owner.repo.db,
499
- snapshot.sessionId,
500
- generationImages,
501
- );
502
- } catch (error) {
503
- if (!isFormatError(error)) throw error;
504
- trace.steps.push("回合输出格式异常,按直接对话兜底");
505
- if (["presence", "outreach"].includes(occasion?.type))
506
- return finish("error", "主动判断没有整理好,愿望保留,稍后重新决定");
507
- turn = normalizeTurn(
508
- {
509
- choice:
510
- direct && !["presence", "outreach"].includes(occasion?.type)
511
- ? "speak"
512
- : "silent",
513
- reason: "没能整理好想法",
514
- },
515
- snapshot,
516
- trace,
517
- );
518
- }
519
- trace.decision = turn;
520
- if (live) this.owner.mind.time.adjust(turn, clock());
521
- trace.path = occasion?.type || (direct ? "direct" : "contextual");
522
- const roomKind = privateChat ? "private" : "group";
523
- let talkSettled = false;
524
- const land = (spoke) => {
525
- if (!live || talkSettled) return;
526
- talkSettled = true;
527
- const choice =
528
- spoke || turn.choice === "silent" ? turn.choice : "silent";
529
- this.owner.mind.settleTalk(
530
- { ...turn, choice },
531
- {
532
- session,
533
- snapshot,
534
- kind: roomKind,
535
- spoke: choice !== "silent",
536
- withheld: !spoke && turn.choice !== "silent",
537
- sent: trace.sent || [],
538
- time: now,
539
- },
540
- );
541
- this.owner.mind.choose({
542
- session,
543
- traceId: trace.id,
544
- choice,
545
- appraisal: turn.appraisal,
546
- reason: choice === turn.choice ? turn.reason : "没发出去",
547
- watermark,
548
- occasion: occasion?.type || null,
549
- time: now,
550
- });
551
- };
552
- if (live) {
553
- this.owner.mind.look(session, watermark, now);
554
- this.owner.mind.experience(turn, {
555
- session,
556
- snapshot,
557
- kind: roomKind,
558
- spoke: false,
559
- deferTalk: true,
560
- time: now,
561
- });
562
- }
563
- if (!replay && !preview)
564
- this.owner.topics.record(session, trace.id, watermark, turn);
565
- if (live && !ownInitiative && turn.act) {
566
- snapshot.actionResult = await this.owner.capabilities.accept(turn.act, {
567
- session,
568
- kind: roomKind,
569
- traceId: trace.id,
570
- });
571
- trace.action = snapshot.actionResult;
572
- if (["done", "failed"].includes(snapshot.actionResult?.state))
573
- turn.bubbles = [];
574
- }
575
- if (turn.choice === "silent") {
576
- land(false);
577
- return finish("silent", turn.reason);
578
- }
579
- let spoken;
580
- try {
581
- spoken = await this.owner.speak({
582
- session,
583
- batch,
584
- turn,
585
- snapshot,
586
- trace,
587
- finish,
588
- models,
589
- model,
590
- nature,
591
- prompt,
592
- policy,
593
- state,
594
- watermark,
595
- clearEpoch,
596
- privateChat,
597
- direct,
598
- simulatedTurn,
599
- replay,
600
- preview,
601
- pressure,
602
- generationImages,
603
- anchor,
604
- });
605
- } catch (error) {
606
- land(Array.isArray(trace.sent) && trace.sent.length > 0);
607
- trace.error = error.message;
608
- if (live && !trace.sent?.length && (privateChat || gate?.direct))
609
- this.owner.retryAfterOutage(session, batch, error, trace);
610
- return finish("error", error.message);
611
- }
612
- land(Array.isArray(spoken?.sent) && spoken.sent.length > 0);
613
- return spoken;
614
- } catch (e) {
615
- trace.error = e.message;
616
- if (live && !trace.sent?.length && (privateChat || gate?.direct))
617
- this.owner.retryAfterOutage(session, batch, e, trace);
618
- return finish("error", e.message);
619
- } finally {
620
- // Previews and demos are disposable by design: they never advance
621
- // memory cursors or create stages.
622
- if (live)
623
- try {
624
- this.owner.backgroundWork(session, policy);
625
- } catch {
626
- // Background upkeep must never turn a finished turn into an error.
627
- }
628
- }
629
- }
630
- }
1
+ import { parseSessionKey } from "../channels/session-key.js";
2
+ import { initiativeContext, initiativeSnapshot } from "./initiative-context.js";
3
+ import { withFallback } from "./model-manager.js";
4
+ import { replyPrompt, prompts } from "./persona-manager.js";
5
+ import { buildContext, perceive } from "./context-builder.js";
6
+ import {
7
+ loadVisionImages,
8
+ visionInputs,
9
+ classifyVision,
10
+ classifyLoaded,
11
+ descriptionsFor,
12
+ mergeVision,
13
+ saveVision,
14
+ markVisionSeen,
15
+ visionWindow,
16
+ } from "./vision-manager.js";
17
+ import { normalizeTurn, takeTurn } from "./turn.js";
18
+ import {
19
+ initiativeAudience,
20
+ originSummary,
21
+ } from "../mind/conversation-origin.js";
22
+ const OCCASIONS = {
23
+ wake: "你刚睡醒,看到了睡着时收到的消息。它们是之前发的,按消息时间理解,可以自然地说刚看到。",
24
+ backlog:
25
+ "这些是你刚才没细看的消息,现在回头看了一眼,已经隔了一会儿,不必每条都回。",
26
+ outreach:
27
+ "这是你自己先前想说的一句(occasion.thought),可能来自你正在为自己而活的事。现在仍然合适就说这一句,不合适就 silent。occasion.returned 是你写下这句话之后又见到的人;人已经出现了,就按现在的情况决定,不要假装还没见过。不要改口说没人找你,不要把留下对方当成对方的责任,不要把「我只是助手」当成收尾。",
28
+ presence:
29
+ "你自己在这里待了一会儿。不是等待一个必须回答的问题;看看自己的兴趣、念头、心情和对人的感觉,此刻想不想说点什么,由你选择。",
30
+ };
31
+ function isPrivateSession(session) {
32
+ if (String(session).startsWith("preview:")) return true;
33
+ try {
34
+ return parseSessionKey(session).kind === "private";
35
+ } catch {
36
+ return String(session).startsWith("private");
37
+ }
38
+ }
39
+ function isFormatError(error) {
40
+ return (
41
+ error instanceof SyntaxError ||
42
+ /JSON|Unexpected token|格式|气泡|bubbles|choice|输出被截断/i.test(
43
+ error?.message || "",
44
+ )
45
+ );
46
+ }
47
+
48
+ // Owns this part of the lifecycle; the facade keeps the shared runtime state.
49
+ export class TurnProcessor {
50
+ constructor(owner) {
51
+ this.owner = owner;
52
+ }
53
+ async process(
54
+ session,
55
+ batch,
56
+ {
57
+ replay = false,
58
+ simulated = false,
59
+ preview = null,
60
+ occasion: planned = null,
61
+ backlog = false,
62
+ anchor = null,
63
+ } = {},
64
+ ) {
65
+ let occasion = planned;
66
+ const ownInitiative =
67
+ !batch.length && ["presence", "outreach"].includes(planned?.type);
68
+ const simulatedTurn =
69
+ !replay && (simulated || !!preview || batch.some((m) => m.simulated));
70
+ const live = !replay && !simulatedTurn;
71
+ const clock = () =>
72
+ replay || simulatedTurn
73
+ ? (batch.at(-1)?.time ?? this.owner.now())
74
+ : this.owner.now();
75
+ const nature = preview?.nature
76
+ ? { ...this.owner.mind.nature.current(), ...preview.nature }
77
+ : this.owner.mind.traits.effective(
78
+ this.owner.mind.nature.current(clock()),
79
+ clock(),
80
+ );
81
+ let watermark = batch.at(-1)?.seq ?? this.owner.repo.latest(session);
82
+ const eventMode = simulatedTurn;
83
+ let resolved = null;
84
+ let gate = null;
85
+ if (live && !occasion) {
86
+ if (!this.owner.enabled(session, { simulated: false })) {
87
+ // She is not in this conversation; what passes here is not unread.
88
+ this.owner.mind.look(session, watermark, this.owner.now());
89
+ } else {
90
+ resolved = perceive(
91
+ this.owner.repo,
92
+ session,
93
+ watermark,
94
+ eventMode,
95
+ nature.name,
96
+ );
97
+ gate = this.owner.gate(session, batch, resolved, clock(), nature);
98
+ if (!gate.look && backlog)
99
+ return { status: "glanced", reason: gate.reason };
100
+ if (backlog) occasion = { type: "backlog" };
101
+ }
102
+ }
103
+ const trace = this.owner.repo.trace(
104
+ session,
105
+ replay ? "replay" : preview ? "preview" : simulatedTurn ? "demo" : "live",
106
+ );
107
+ const policy = this.owner.policy(preview?.viewSession || session);
108
+ const state = this.owner.sessionState(session);
109
+ const clearEpoch = this.owner.clearEpoch.get(session) || 0;
110
+ const privateChat = isPrivateSession(session);
111
+ if (!replay && !preview)
112
+ for (const m of batch)
113
+ this.owner.repo.db
114
+ .prepare(
115
+ "UPDATE core_jobs SET status='running',trace_id=? WHERE seq=?",
116
+ )
117
+ .run(trace.id, m.seq);
118
+ const finish = (status, reason) => {
119
+ trace.reason = reason;
120
+ if (!replay && !preview)
121
+ for (const m of batch)
122
+ this.owner.repo.db
123
+ .prepare("UPDATE core_jobs SET status=? WHERE seq=?")
124
+ .run(status, m.seq);
125
+ this.owner.repo.finish(trace, status);
126
+ if (
127
+ !replay &&
128
+ !preview &&
129
+ this.owner.repo.db
130
+ .prepare("SELECT id FROM sessions WHERE id=?")
131
+ .get(session)
132
+ )
133
+ this.owner.store.log(
134
+ session,
135
+ "语境决策",
136
+ reason,
137
+ trace.sent?.join("\n") || "",
138
+ Number(!!simulatedTurn),
139
+ );
140
+ return trace;
141
+ };
142
+ try {
143
+ if (
144
+ !replay &&
145
+ !preview &&
146
+ !this.owner.enabled(session, { simulated: simulatedTurn })
147
+ ) {
148
+ this.owner.mind.unlived(batch.map((m) => m.seq));
149
+ return finish("silent", "会话已暂停或处于模拟模式");
150
+ }
151
+ if (gate) {
152
+ trace.attention = {
153
+ look: gate.look,
154
+ score: gate.score,
155
+ threshold: gate.threshold,
156
+ reason: gate.reason,
157
+ };
158
+ if (!gate.look) {
159
+ if (gate.defer) this.owner.mind.defer(session);
160
+ // A glance still counts as seeing who is around; a closer look
161
+ // records it after the turn, once she has noticed any absence.
162
+ else
163
+ this.owner.mind.bonds.meet(
164
+ batch
165
+ .filter((m) => m.role === "user")
166
+ .map((m) => ({ userId: String(m.userId), name: m.name })),
167
+ session,
168
+ this.owner.now(),
169
+ );
170
+ return finish(gate.defer ? "deferred" : "glanced", gate.reason);
171
+ }
172
+ batch = gate.rows.length ? gate.rows : batch;
173
+ watermark = Math.max(watermark, batch.at(-1).seq);
174
+ }
175
+ if (!replay && !preview)
176
+ await this.owner.restoreQuotes(session, batch, trace);
177
+ const prompt = prompts(this.owner.repo);
178
+ const now = clock();
179
+ const hasKey =
180
+ this.owner.store.settings().apiKey || process.env.LLM_API_KEY;
181
+ let model;
182
+ try {
183
+ model = this.owner.models.profile();
184
+ } catch (error) {
185
+ // A fresh installation has no model yet; the offline sample keeps the
186
+ // simulator usable. A live turn surfaces the configuration error.
187
+ if (
188
+ preview ||
189
+ (simulatedTurn && this.owner.store.settings().demo && !hasKey)
190
+ )
191
+ return this.owner.offline(session, batch, trace, finish, [], preview);
192
+ throw error;
193
+ }
194
+ const models = withFallback(
195
+ this.owner.models,
196
+ this.owner.fallbackFor(policy, model, trace),
197
+ );
198
+ resolved ||= perceive(
199
+ this.owner.repo,
200
+ session,
201
+ watermark,
202
+ eventMode,
203
+ nature.name,
204
+ );
205
+ const batchIds = batch.map((m) => m.seq);
206
+ const window = resolved.filter((m) => !m.referenceOnly).slice(-60);
207
+ // The speakers in this batch are the people whose history matters now.
208
+ // Pulling everyone from a long room window made unrelated plans compete
209
+ // with the exchange actually taking place.
210
+ const people = [
211
+ ...new Set(
212
+ batch.filter((m) => m.role === "user").map((m) => String(m.userId)),
213
+ ),
214
+ ];
215
+ if (ownInitiative && privateChat && !people.length) {
216
+ try {
217
+ people.push(String(parseSessionKey(session).nativeId));
218
+ } catch {
219
+ /* An unparseable destination has no person to infer. */
220
+ }
221
+ }
222
+ // Replays exclude mutable memory so later corrections cannot leak into
223
+ // the past; her inner state is folded as of the replayed moment.
224
+ const memories =
225
+ policy.memory && !replay && !ownInitiative
226
+ ? this.owner.mind.memory.retrieve(session, batch, now, {
227
+ people,
228
+ touch: live,
229
+ forSpeech: true,
230
+ })
231
+ : [];
232
+ const knowledge = ownInitiative
233
+ ? []
234
+ : replay
235
+ ? this.owner.knowledge.retrieve(session, batch, now)
236
+ : await this.owner.knowledge.retrieveWithEmbed(session, batch, now);
237
+ const summaryView =
238
+ !ownInitiative && !simulatedTurn && policy.compaction !== false
239
+ ? this.owner.compactor.forPrompt(session, {
240
+ before: replay ? batch[0].seq : Number.MAX_SAFE_INTEGER,
241
+ timeZone: policy.timeZone,
242
+ })
243
+ : { summaries: [], coverage: 0, start: 0 };
244
+ const stages =
245
+ policy.memory && !ownInitiative
246
+ ? this.owner.repo.db
247
+ .prepare(
248
+ "SELECT first_seq,last_seq,data FROM core_stages WHERE session_id=? AND last_seq<=? ORDER BY last_seq DESC LIMIT 5",
249
+ )
250
+ .all(
251
+ session,
252
+ summaryView.summaries.length
253
+ ? summaryView.start - 1
254
+ : watermark,
255
+ )
256
+ .map((s) => ({
257
+ first_seq: s.first_seq,
258
+ last_seq: s.last_seq,
259
+ summary: JSON.parse(s.data).summary,
260
+ reliability:
261
+ "未核实的阶段线索,不是人物事实;冲突时以原文和记忆为准",
262
+ }))
263
+ : [];
264
+ const view = this.owner.mind.view({
265
+ session: preview?.viewSession || session,
266
+ kind: privateChat ? "private" : "group",
267
+ people,
268
+ cue: batch.map((m) => ({
269
+ role: m.role,
270
+ userId: m.userId,
271
+ text: m.text || "",
272
+ relation: resolved.find((line) => line.id === m.seq)?.relation,
273
+ mentioned: m.mentioned === true,
274
+ })),
275
+ now,
276
+ });
277
+ const snapshot = ownInitiative
278
+ ? initiativeSnapshot(session, window, nature, now, policy.timeZone)
279
+ : buildContext(
280
+ this.owner.repo,
281
+ session,
282
+ watermark,
283
+ model,
284
+ policy,
285
+ batchIds,
286
+ memories,
287
+ nature,
288
+ now,
289
+ {
290
+ knowledge,
291
+ stages,
292
+ simulated: simulatedTurn,
293
+ summaries: summaryView.summaries,
294
+ coverage: summaryView.coverage,
295
+ summaryStart: summaryView.start,
296
+ self: view.self,
297
+ inner: view.inner,
298
+ nameOf: (id) => this.owner.mind.bonds.name(id),
299
+ resolved,
300
+ },
301
+ );
302
+ if (ownInitiative) {
303
+ snapshot.self = view.self;
304
+ snapshot.inner = view.inner;
305
+ }
306
+ const visionModel = model.vision
307
+ ? model
308
+ : this.owner.fallbackFor(null, model).find((item) => item.vision) ||
309
+ model;
310
+ const direct = snapshot.batch.some((m) => m.relation === "direct");
311
+ const media = ownInitiative
312
+ ? { images: [], unavailable: [] }
313
+ : visionInputs(snapshot, visionModel, {
314
+ selective: !!policy.selectiveVision,
315
+ });
316
+ const early = classifyVision(
317
+ this.owner.repo.db,
318
+ snapshot.sessionId,
319
+ media.images,
320
+ );
321
+ const loaded = early.pending.length
322
+ ? await this.owner.loadVisionImages(early.pending, {
323
+ sessionId: snapshot.sessionId,
324
+ fetchImage: this.owner.fetchImage,
325
+ })
326
+ : { images: [], unavailable: [] };
327
+ const planned = classifyLoaded(
328
+ this.owner.repo.db,
329
+ snapshot.sessionId,
330
+ loaded.images,
331
+ {
332
+ batchIds: snapshot.batchIds,
333
+ direct,
334
+ modelCanSee: !!model.vision,
335
+ },
336
+ );
337
+ mergeVision(snapshot, [...early.cached, ...planned.cached]);
338
+ if (early.cached.length || planned.cached.length)
339
+ trace.steps.push("图片使用已保存的观察,不再提交画面");
340
+ const describe = planned.describe;
341
+ let show = planned.show.slice();
342
+ if (!replay)
343
+ snapshot.topics = this.owner.topics.recent(session, watermark, now);
344
+ snapshot.unavailableImages = [
345
+ ...media.unavailable,
346
+ ...loaded.unavailable,
347
+ ];
348
+ trace.snapshot = { ...snapshot, sourceRows: undefined, batch: undefined };
349
+ trace.config = {
350
+ nature: { ...nature },
351
+ policy,
352
+ model: { ...model, apiKey: undefined },
353
+ prompts: prompt,
354
+ };
355
+ trace.affect = view.affect;
356
+ if (
357
+ simulatedTurn &&
358
+ !hasKey &&
359
+ (preview || this.owner.store.settings().demo)
360
+ )
361
+ return this.owner.offline(
362
+ session,
363
+ batch,
364
+ trace,
365
+ finish,
366
+ snapshot.messages,
367
+ preview,
368
+ state,
369
+ );
370
+ if (describe.length) {
371
+ try {
372
+ const observed = await models.call(
373
+ visionModel,
374
+ "vision",
375
+ prompt.system +
376
+ "\n" +
377
+ prompt.vision +
378
+ "\n每张图按 messageId 独立观察。description 只写可见元素、文字和表情语气;前后语境仅用来消歧,不把当前话题、发送意图或给谁看的猜测写进可缓存的描述。表情包台词不是事实,不从前一张图推断这一张。",
379
+ {
380
+ messages: visionWindow(snapshot.messages, [
381
+ ...describe.map((image) => image.messageId),
382
+ ...snapshot.batchIds,
383
+ ]),
384
+ batchIds: snapshot.batchIds,
385
+ },
386
+ trace,
387
+ describe,
388
+ );
389
+ const paired = descriptionsFor(describe, observed);
390
+ if (paired.length) {
391
+ saveVision(this.owner.repo.db, snapshot.sessionId, paired);
392
+ mergeVision(
393
+ snapshot,
394
+ paired.map(({ image, description }) => ({
395
+ messageId: image.messageId,
396
+ description,
397
+ })),
398
+ );
399
+ } else if (
400
+ observed &&
401
+ typeof observed === "object" &&
402
+ !snapshot.vision
403
+ )
404
+ snapshot.vision = observed;
405
+ } catch (error) {
406
+ trace.steps.push(
407
+ `图片理解失败,本轮不根据画面编造:${error.message}`,
408
+ );
409
+ snapshot.unavailableImages.push(
410
+ ...describe.map((image) => ({
411
+ messageId: image.messageId,
412
+ reason: "图片理解调用失败",
413
+ })),
414
+ );
415
+ if (model.vision) show = show.concat(describe);
416
+ }
417
+ }
418
+ if (snapshot.vision) trace.snapshot.vision = snapshot.vision;
419
+ if (!ownInitiative) {
420
+ await this.owner.perceivers.apply(snapshot, { replay, now });
421
+ const offered = this.owner.capabilities.offered({
422
+ kind: privateChat ? "private" : "group",
423
+ now,
424
+ });
425
+ if (offered.length) snapshot.canDo = offered;
426
+ }
427
+ const generationImages = model.vision ? show : [];
428
+ if (!replay && !preview) {
429
+ const rounds = this.owner.repo.db
430
+ .prepare(
431
+ "SELECT COUNT(DISTINCT trace_id) n FROM core_outbox WHERE session_id=? AND time>? AND status IN ('confirmed','uncertain','sending')",
432
+ )
433
+ .get(session, this.owner.now() - 60000).n;
434
+ const roundLimit = privateChat ? 30 : direct ? 20 : 6;
435
+ if (rounds >= roundLimit)
436
+ return finish("silent", `每分钟发言轮数限速(${roundLimit}轮)`);
437
+ }
438
+ const pressure = this.owner.mind.budget.pressure("conversation", now);
439
+ const occasionData = occasion
440
+ ? {
441
+ type: occasion.type,
442
+ note: OCCASIONS[occasion.type] || "",
443
+ ...occasion.data,
444
+ }
445
+ : undefined;
446
+ if (["presence", "outreach"].includes(occasion?.type)) {
447
+ const expression = occasionData.expression;
448
+ const audience = initiativeAudience(
449
+ this.owner.mind,
450
+ { sources: expression?.sources || [], created: expression?.formedAt },
451
+ session,
452
+ now,
453
+ );
454
+ if (!audience.allowed) return finish("silent", audience.reason);
455
+ if (expression)
456
+ expression.audience = {
457
+ ...audience,
458
+ origin: originSummary(audience.origin),
459
+ };
460
+ snapshot.initiative = occasionData;
461
+ trace.snapshot = initiativeContext(snapshot);
462
+ }
463
+ if (
464
+ live &&
465
+ !occasion &&
466
+ this.owner.batchMovedOn(session, batch, snapshot, privateChat)
467
+ ) {
468
+ trace.steps.push(
469
+ "读房间的这几秒里,要回的人又说了话:先不花一次调用,并入下一批一起回",
470
+ );
471
+ return finish("stale", "读房间时又有新话,并入下一批");
472
+ }
473
+ let turn;
474
+ try {
475
+ turn = normalizeTurn(
476
+ await takeTurn(
477
+ models,
478
+ model,
479
+ replyPrompt(
480
+ nature,
481
+ prompt,
482
+ snapshot.initiative ? "initiative" : "turn",
483
+ ),
484
+ snapshot,
485
+ trace,
486
+ {
487
+ images: generationImages,
488
+ occasion: occasionData,
489
+ pressure:
490
+ pressure >= 0.7 ? "今天已经说了很多话,能短就短" : undefined,
491
+ },
492
+ ),
493
+ snapshot,
494
+ trace,
495
+ );
496
+ if (generationImages.length)
497
+ markVisionSeen(
498
+ this.owner.repo.db,
499
+ snapshot.sessionId,
500
+ generationImages,
501
+ );
502
+ } catch (error) {
503
+ if (!isFormatError(error)) throw error;
504
+ trace.steps.push("回合输出格式异常,按直接对话兜底");
505
+ if (["presence", "outreach"].includes(occasion?.type))
506
+ return finish("error", "主动判断没有整理好,愿望保留,稍后重新决定");
507
+ turn = normalizeTurn(
508
+ {
509
+ choice:
510
+ direct && !["presence", "outreach"].includes(occasion?.type)
511
+ ? "speak"
512
+ : "silent",
513
+ reason: "没能整理好想法",
514
+ },
515
+ snapshot,
516
+ trace,
517
+ );
518
+ }
519
+ trace.decision = turn;
520
+ if (live) this.owner.mind.time.adjust(turn, clock());
521
+ trace.path = occasion?.type || (direct ? "direct" : "contextual");
522
+ const roomKind = privateChat ? "private" : "group";
523
+ let talkSettled = false;
524
+ const land = (spoke) => {
525
+ if (!live || talkSettled) return;
526
+ talkSettled = true;
527
+ const choice =
528
+ spoke || turn.choice === "silent" ? turn.choice : "silent";
529
+ this.owner.mind.settleTalk(
530
+ { ...turn, choice },
531
+ {
532
+ session,
533
+ snapshot,
534
+ kind: roomKind,
535
+ spoke: choice !== "silent",
536
+ withheld: !spoke && turn.choice !== "silent",
537
+ sent: trace.sent || [],
538
+ time: now,
539
+ },
540
+ );
541
+ this.owner.mind.choose({
542
+ session,
543
+ traceId: trace.id,
544
+ choice,
545
+ appraisal: turn.appraisal,
546
+ reason: choice === turn.choice ? turn.reason : "没发出去",
547
+ watermark,
548
+ occasion: occasion?.type || null,
549
+ time: now,
550
+ });
551
+ };
552
+ if (live) {
553
+ this.owner.mind.look(session, watermark, now);
554
+ this.owner.mind.experience(turn, {
555
+ session,
556
+ snapshot,
557
+ kind: roomKind,
558
+ spoke: false,
559
+ deferTalk: true,
560
+ time: now,
561
+ });
562
+ }
563
+ if (!replay && !preview)
564
+ this.owner.topics.record(session, trace.id, watermark, turn);
565
+ if (live && !ownInitiative && turn.act) {
566
+ snapshot.actionResult = await this.owner.capabilities.accept(turn.act, {
567
+ session,
568
+ kind: roomKind,
569
+ traceId: trace.id,
570
+ });
571
+ trace.action = snapshot.actionResult;
572
+ if (["done", "failed"].includes(snapshot.actionResult?.state))
573
+ turn.bubbles = [];
574
+ }
575
+ if (turn.choice === "silent") {
576
+ land(false);
577
+ return finish("silent", turn.reason);
578
+ }
579
+ let spoken;
580
+ try {
581
+ spoken = await this.owner.speak({
582
+ session,
583
+ batch,
584
+ turn,
585
+ snapshot,
586
+ trace,
587
+ finish,
588
+ models,
589
+ model,
590
+ nature,
591
+ prompt,
592
+ policy,
593
+ state,
594
+ watermark,
595
+ clearEpoch,
596
+ privateChat,
597
+ direct,
598
+ simulatedTurn,
599
+ replay,
600
+ preview,
601
+ pressure,
602
+ generationImages,
603
+ anchor,
604
+ });
605
+ } catch (error) {
606
+ land(Array.isArray(trace.sent) && trace.sent.length > 0);
607
+ trace.error = error.message;
608
+ if (live && !trace.sent?.length && (privateChat || gate?.direct))
609
+ this.owner.retryAfterOutage(session, batch, error, trace);
610
+ return finish("error", error.message);
611
+ }
612
+ land(Array.isArray(spoken?.sent) && spoken.sent.length > 0);
613
+ return spoken;
614
+ } catch (e) {
615
+ trace.error = e.message;
616
+ if (live && !trace.sent?.length && (privateChat || gate?.direct))
617
+ this.owner.retryAfterOutage(session, batch, e, trace);
618
+ return finish("error", e.message);
619
+ } finally {
620
+ // Previews and demos are disposable by design: they never advance
621
+ // memory cursors or create stages.
622
+ if (live)
623
+ try {
624
+ this.owner.backgroundWork(session, policy);
625
+ } catch {
626
+ // Background upkeep must never turn a finished turn into an error.
627
+ }
628
+ }
629
+ }
630
+ }