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,472 +1,482 @@
1
- import { randomUUID } from "node:crypto";
2
- import { deliver } from "../../core/message-scheduler.js";
3
- import { withFallback } from "../../core/model-manager.js";
4
- import { prompts, replyPrompt } from "../../core/persona-manager.js";
5
- import { parse, text, hasCredential } from "../util.js";
6
- import { parseSessionKey } from "../../channels/session-key.js";
7
- import { leaks } from "../guard.js";
8
- import { displayNames } from "../../studio/display-names.js";
9
-
10
- export class Sharing {
11
- constructor(time) {
12
- this.time = time;
13
- this.db = time.db;
14
- this.db
15
- .prepare(
16
- "UPDATE mind_time_shares SET delivery_kind='report' WHERE state IN ('pending','deferred','declined') AND delivery_kind='body'",
17
- )
18
- .run();
19
- this.db
20
- .prepare(
21
- "UPDATE mind_time_shares SET state='uncertain',reason='实例中断,先核对送达情况' WHERE state='sending'",
22
- )
23
- .run();
24
- }
25
- list() {
26
- return this.db
27
- .prepare("SELECT * FROM mind_time_shares ORDER BY updated DESC LIMIT 50")
28
- .all();
29
- }
30
- choose(workId, session, { choice, reason = "" } = {}, now = this.time.now()) {
31
- const work = this.time.works.get(workId);
32
- if (!work || work.state !== "complete")
33
- throw Error("只有真实完成稿可以交付");
34
- if (
35
- leaks([work.content], this.time.mind.meetings.privateSayings(session))
36
- .length
37
- )
38
- throw Error("正文包含其他会话的私下内容");
39
- if (!this.time.visible(work, session, now))
40
- throw Error("作品来源不允许在此会话分享");
41
- if (!["send", "later", "decline"].includes(choice))
42
- throw Error("需要明确分享选择");
43
- const prior = this.db
44
- .prepare(
45
- "SELECT * FROM mind_time_shares WHERE work_id=? AND version=? AND session_id=?",
46
- )
47
- .get(workId, work.version, session);
48
- if (prior && ["uncertain", "sending", "sent"].includes(prior.state))
49
- return prior;
50
- const state = { send: "pending", later: "deferred", decline: "declined" }[
51
- choice
52
- ],
53
- id = prior?.id || randomUUID();
54
- this.db
55
- .prepare(
56
- "INSERT INTO mind_time_shares(id,created,updated,work_id,version,session_id,state,reason,delivery_kind) VALUES (?,?,?,?,?,?,?,?, 'report') ON CONFLICT(work_id,version,session_id) DO UPDATE SET state=excluded.state,reason=excluded.reason,updated=excluded.updated",
57
- )
58
- .run(
59
- id,
60
- now,
61
- now,
62
- workId,
63
- work.version,
64
- session,
65
- state,
66
- text(reason, 240),
67
- );
68
- if (work.task_id)
69
- this.db
70
- .prepare(
71
- "UPDATE mind_time_tasks SET share_state=?,share_reason=?,next_step=? WHERE id=?",
72
- )
73
- .run(state, text(reason, 240), now + 30 * 60000, work.task_id);
74
- this.time.event(
75
- work.task_id,
76
- "share-choice",
77
- reason,
78
- { choice, workId, session },
79
- now,
80
- );
81
- return this.db.prepare("SELECT * FROM mind_time_shares WHERE id=?").get(id);
82
- }
83
- async run(life, now) {
84
- if (!life.online() || life.repo.store.settings().demo || life.closed)
85
- return null;
86
- let share = this.db
87
- .prepare(
88
- "SELECT * FROM mind_time_shares WHERE state='pending' AND next_step<=? ORDER BY created LIMIT 1",
89
- )
90
- .get(now);
91
- if (!share) {
92
- const task = this.db
93
- .prepare(
94
- "SELECT id FROM mind_time_tasks WHERE state='done' AND share_state IN ('waiting','deferred') AND next_step<=? AND subject IS NOT NULL ORDER BY due_at,created LIMIT 1",
95
- )
96
- .get(now);
97
- if (!task || !life.mind.budget.allows("inner", now)) return null;
98
- const item = this.time.tasks.get(task.id),
99
- work = this.time.works.get(item.work_id);
100
- if (!work || !this.time.visible(work, item.session_id, now)) return null;
101
- this.db
102
- .prepare("UPDATE mind_time_tasks SET next_step=? WHERE id=?")
103
- .run(now + 30 * 60000, item.id);
104
- const trace = life.repo.trace("__mind__", "activity");
105
- try {
106
- const result = await withFallback(
107
- life.chat.models,
108
- life.chat.fallbackFor(null, life.profile(), trace),
109
- ).call(
110
- life.profile(),
111
- "reflection",
112
- replyPrompt(
113
- life.mind.nature.current(now),
114
- {
115
- ...prompts(life.repo),
116
- activity:
117
- '作品已经保存,是否现在履行给这个人的交付?这是你自己的选择。输出JSON {"choice":"send|later|decline","reason":"原因"}。不能说已经发送。',
118
- },
119
- "activity",
120
- ),
121
- {
122
- task: item.title,
123
- person:
124
- displayNames(this.db, now).get(String(item.subject)) || null,
125
- work: {
126
- id: work.id,
127
- title: work.title,
128
- summary: work.summary,
129
- state: work.state,
130
- },
131
- },
132
- trace,
133
- );
134
- share = this.choose(
135
- work.id,
136
- item.session_id,
137
- {
138
- choice: result.choice || "later",
139
- reason: result.reason || "暂缓,稍后再决定",
140
- },
141
- life.now(),
142
- );
143
- } finally {
144
- life.chat.finishQuietly(trace, "complete");
145
- }
146
- }
147
- if (share?.state !== "pending") return null;
148
- return this.time.attention.chat(() => this.send(life, share));
149
- }
150
- async send(life, share) {
151
- const current = this.db
152
- .prepare("SELECT * FROM mind_time_shares WHERE id=?")
153
- .get(share.id);
154
- if (!current || current.state !== "pending") return null;
155
- share = current;
156
- const work = this.time.works.get(share.work_id, share.version);
157
- if (
158
- !work ||
159
- !life.chat.enabled(share.session_id, { simulated: false }) ||
160
- !this.time.visible(work, share.session_id, life.now())
161
- )
162
- return null;
163
- // Where proactive messages are switched off she does not knock; the work
164
- // waits and she looks again later.
165
- if (life.canReach && !life.canReach(share.session_id)) {
166
- const reason = "对方没有开启主动消息,等对方来找她";
167
- this.db
168
- .prepare(
169
- "UPDATE mind_time_shares SET reason=?,next_step=? WHERE id=? AND state='pending'",
170
- )
171
- .run(reason, life.now() + 60 * 60000, share.id);
172
- return { status: "waiting", reason };
173
- }
174
- if (share.delivery_kind === "report" && !parse(share.report, []).length) {
175
- try {
176
- return await this.review(life, share, work);
177
- } catch (error) {
178
- const reason = text(error.message, 180);
179
- this.db
180
- .prepare(
181
- "UPDATE mind_time_shares SET reason=?,next_step=? WHERE id=? AND state='pending'",
182
- )
183
- .run(reason, life.now() + 15 * 60000, share.id);
184
- return { status: "waiting", reason };
185
- }
186
- }
187
- const task = this.time.tasks.get(work.task_id),
188
- trace = life.repo.trace(share.session_id, "live");
189
- trace.decision = { targetMessageIds: [] };
190
- const report = parse(share.report, []),
191
- chars = Array.from(work.content),
192
- bubbles = [];
193
- if (share.delivery_kind === "report")
194
- bubbles.push(...report.slice(share.send_index, share.send_index + 3));
195
- else
196
- for (
197
- let pos = share.offset;
198
- pos < chars.length && bubbles.length < 3;
199
- pos += 500
200
- )
201
- bubbles.push(chars.slice(pos, pos + 500).join(""));
202
- if (!bubbles.length) return null;
203
- const reserved = this.db
204
- .prepare(
205
- "UPDATE mind_time_shares SET state='sending',trace_id=?,updated=? WHERE id=? AND state='pending'",
206
- )
207
- .run(trace.id, life.now(), share.id);
208
- if (!reserved.changes) return null;
209
- const kind =
210
- this.time.works.project(work.project_id)?.kind ||
211
- this.db
212
- .prepare("SELECT kind FROM mind_creations WHERE id=?")
213
- .get(work.legacy_creation || "")?.kind;
214
- const route = parseSessionKey(share.session_id),
215
- anchor = parse(
216
- this.db
217
- .prepare(
218
- "SELECT payload FROM core_events WHERE session_id=? ORDER BY seq DESC LIMIT 1",
219
- )
220
- .get(share.session_id)?.payload,
221
- {},
222
- );
223
- const message = {
224
- sessionId: share.session_id,
225
- kind: route.kind,
226
- userId:
227
- route.kind === "private"
228
- ? route.nativeId
229
- : task?.subject || route.nativeId,
230
- accountId:
231
- route.accountId !== "_" ? route.accountId : anchor.accountId || "",
232
- nativeId: route.nativeId,
233
- channel: route.channel,
234
- artifact: {
235
- workId: work.id,
236
- version: work.version,
237
- domain:
238
- kind === "game"
239
- ? "reference"
240
- : kind === "write"
241
- ? "fiction"
242
- : "personal-notes",
243
- },
244
- name: life.mind.nature.current().name,
245
- attachments: [],
246
- };
247
- let status = "complete";
248
- try {
249
- await deliver(
250
- life.repo,
251
- message,
252
- bubbles,
253
- trace,
254
- life.chat.send,
255
- () =>
256
- !life.closed &&
257
- life.chat.enabled(share.session_id, { simulated: false }) &&
258
- this.time.visible(work, share.session_id, life.now()),
259
- { now: life.now },
260
- );
261
- } catch (error) {
262
- status = "error";
263
- trace.error = text(error.message, 200);
264
- }
265
- const confirmed = (trace.sent || []).reduce(
266
- (n, s) => n + Array.from(s).length,
267
- 0,
268
- ),
269
- offset =
270
- share.delivery_kind === "report"
271
- ? share.offset
272
- : share.offset + confirmed,
273
- sendIndex = share.send_index + (trace.sent || []).length;
274
- const state =
275
- status === "error"
276
- ? "uncertain"
277
- : (
278
- share.delivery_kind === "report"
279
- ? sendIndex >= report.length
280
- : offset >= chars.length
281
- )
282
- ? "sent"
283
- : "pending";
284
- this.db
285
- .prepare(
286
- "UPDATE mind_time_shares SET state=?,offset=?,send_index=?,updated=?,reason=? WHERE id=?",
287
- )
288
- .run(state, offset, sendIndex, life.now(), trace.error || "", share.id);
289
- if (task) {
290
- this.db
291
- .prepare(
292
- "UPDATE mind_time_tasks SET share_state=?,share_reason=? WHERE id=?",
293
- )
294
- .run(
295
- state === "sent" && share.delivery_kind === "report"
296
- ? "reported"
297
- : state,
298
- trace.error || "",
299
- task.id,
300
- );
301
- if (
302
- state === "sent" &&
303
- task.anticipation_id &&
304
- !task.checkpoint.contract?.originalGoal
305
- )
306
- life.mind.anticipations.close(task.anticipation_id, {
307
- status: "done",
308
- note:
309
- share.delivery_kind === "report"
310
- ? "回看成果后整理的汇报已确认送达"
311
- : "作品已全部确认交付",
312
- sources: task.sources,
313
- time: life.now(),
314
- });
315
- }
316
- this.time.event(
317
- task?.id,
318
- "delivery",
319
- state === "sent"
320
- ? share.delivery_kind === "report"
321
- ? "整理后的汇报已逐句送达"
322
- : "作品已完整确认交付"
323
- : state === "uncertain"
324
- ? "送达状态不确定,需要核对"
325
- : "保存部分交付位置",
326
- {
327
- workId: work.id,
328
- offset,
329
- state,
330
- deliveryKind: share.delivery_kind,
331
- sendIndex,
332
- },
333
- life.now(),
334
- );
335
- life.chat.finishQuietly(trace, status);
336
- return { status: "shared", state, workId: work.id, offset };
337
- }
338
- async review(life, share, work) {
339
- if (!life.mind.budget.allows("inner", life.now()))
340
- return { status: "waiting", reason: "今日独处预算不足" };
341
- const notes = parse(share.review_notes, []),
342
- chars = Array.from(work.content),
343
- reviewing = share.review_offset < chars.length,
344
- end = Math.min(chars.length, share.review_offset + 2400),
345
- trace = life.repo.trace(share.session_id, "activity"),
346
- version = life.mind.nature.version();
347
- let status = "error";
348
- this.reviewing = {
349
- share,
350
- work,
351
- phase: reviewing ? "reading" : "composing",
352
- };
353
- try {
354
- const result = await withFallback(
355
- life.chat.models,
356
- life.chat.fallbackFor(null, life.profile(), trace),
357
- ).call(
358
- life.profile(),
359
- "reflection",
360
- replyPrompt(
361
- life.mind.nature.current(),
362
- {
363
- ...prompts(life.repo),
364
- activity: reviewing
365
- ? '回看自己已经完成的成果中的这一段,抓住实际内容、感受和想分享的重点。只整理,不发消息,不复制全文。输出JSON {"note":"最多220字阅读整理"}。小说人物属于作品,游戏剧情属于游戏世界。'
366
- : '你已逐段回看自己的成果。现在根据notes整理想告诉这个人的话,带自己的感受与具体内容,像日常聊天一样一句一句说。不要粘贴正文,不写大段汇报或列表,不解释实现方式,不暴露后台地址。输出JSON {"bubbles":["一句自然的话","再接一句"]},2至6句,每句不超过80字,总计不超过350字。可以摘一句喜欢的台词,不复制整篇。不输出隐藏推理。',
367
- },
368
- "activity",
369
- ),
370
- reviewing
371
- ? {
372
- title: work.title,
373
- passage: chars.slice(share.review_offset, end).join(""),
374
- position: share.review_offset,
375
- total: chars.length,
376
- }
377
- : {
378
- title: work.title,
379
- notes,
380
- person:
381
- displayNames(this.db, life.now()).get(
382
- String(this.time.tasks.get(work.task_id)?.subject),
383
- ) || null,
384
- currentLife: this.time.view({ session: share.session_id }),
385
- },
386
- trace,
387
- );
388
- const current = this.db
389
- .prepare("SELECT * FROM mind_time_shares WHERE id=?")
390
- .get(share.id);
391
- if (
392
- life.closed ||
393
- version !== life.mind.nature.version() ||
394
- current?.state !== "pending" ||
395
- current.review_offset !== share.review_offset ||
396
- current.report !== share.report ||
397
- !this.time.visible(work, share.session_id, life.now())
398
- )
399
- return { status: "cancelled" };
400
- if (reviewing) {
401
- const note = text(result.note, 220);
402
- if (
403
- !note ||
404
- hasCredential(note) ||
405
- leaks(
406
- [note],
407
- this.time.mind.meetings.privateSayings(share.session_id),
408
- ).length
409
- )
410
- throw Error("成果整理格式无效");
411
- this.db
412
- .prepare(
413
- "UPDATE mind_time_shares SET review_offset=?,review_notes=?,updated=? WHERE id=?",
414
- )
415
- .run(end, JSON.stringify([...notes, note]), life.now(), share.id);
416
- status = "complete";
417
- return {
418
- status: "reviewing",
419
- reason: "正在回看自己的成果",
420
- read: end,
421
- total: chars.length,
422
- };
423
- }
424
- const bubbles = naturalReport(result.bubbles, work.content);
425
- if (
426
- leaks(bubbles, this.time.mind.meetings.privateSayings(share.session_id))
427
- .length
428
- )
429
- throw Error("汇报涉及私下内容");
430
- this.db
431
- .prepare("UPDATE mind_time_shares SET report=?,updated=? WHERE id=?")
432
- .run(JSON.stringify(bubbles), life.now(), share.id);
433
- status = "complete";
434
- return { status: "reviewed", reason: "已整理成几句想说的话" };
435
- } finally {
436
- this.reviewing = null;
437
- life.chat.finishQuietly(trace, status);
438
- }
439
- }
440
- }
441
- export function naturalReport(input, original) {
442
- if (!Array.isArray(input)) throw Error("需要几句自然汇报");
443
- const lines = input
444
- .flatMap((value) =>
445
- String(value)
446
- .split(/[\r\n]+/)
447
- .flatMap(
448
- (s) =>
449
- s.match(/[^。!?!?]+[。!?!?]+[”」』"']*|[^。!?!?]+$/gu) || [],
450
- ),
451
- )
452
- .map((s) => s.trim())
453
- .filter(Boolean);
454
- if (
455
- !lines.length ||
456
- lines.length > 6 ||
457
- lines.some((s) => Array.from(s).length > 80) ||
458
- lines.join("").length > 350
459
- )
460
- throw Error("汇报需要简短的一句一句说");
461
- if (
462
- hasCredential(lines.join(" ")) ||
463
- /https?:\/\/|127\.0\.0\.1|localhost|管理台地址/.test(lines.join(" "))
464
- )
465
- throw Error("汇报包含不适合公开的信息");
466
- if (
467
- lines.join("").replace(/\s/g, "") === String(original).replace(/\s/g, "") ||
468
- lines.filter((s) => s.length > 40 && original.includes(s)).length > 1
469
- )
470
- throw Error("请回看后整理,不要复制正文");
471
- return lines;
472
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { deliver } from "../../core/message-scheduler.js";
3
+ import { withFallback } from "../../core/model-manager.js";
4
+ import { prompts, replyPrompt } from "../../core/persona-manager.js";
5
+ import { parse, text, hasCredential } from "../util.js";
6
+ import { parseSessionKey } from "../../channels/session-key.js";
7
+ import { leaks } from "../guard.js";
8
+ import { displayNames } from "../../studio/display-names.js";
9
+
10
+ export class Sharing {
11
+ constructor(time) {
12
+ this.time = time;
13
+ this.db = time.db;
14
+ this.db
15
+ .prepare(
16
+ "UPDATE mind_time_shares SET delivery_kind='report' WHERE state IN ('pending','deferred','declined') AND delivery_kind='body'",
17
+ )
18
+ .run();
19
+ this.db
20
+ .prepare(
21
+ "UPDATE mind_time_shares SET state='uncertain',reason='实例中断,先核对送达情况' WHERE state='sending'",
22
+ )
23
+ .run();
24
+ }
25
+ list() {
26
+ return this.db
27
+ .prepare("SELECT * FROM mind_time_shares ORDER BY updated DESC LIMIT 50")
28
+ .all();
29
+ }
30
+ choose(workId, session, { choice, reason = "" } = {}, now = this.time.now()) {
31
+ const work = this.time.works.get(workId);
32
+ if (!work || work.state !== "complete")
33
+ throw Error("只有真实完成稿可以交付");
34
+ if (!this.time.works.materialSupported(work))
35
+ throw Error("这篇记录引用了与作品无关的资料,不能作为游戏阅读进度分享");
36
+ if (
37
+ leaks([work.content], this.time.mind.meetings.privateSayings(session))
38
+ .length
39
+ )
40
+ throw Error("正文包含其他会话的私下内容");
41
+ if (!this.time.visible(work, session, now))
42
+ throw Error("作品来源不允许在此会话分享");
43
+ if (!["send", "later", "decline"].includes(choice))
44
+ throw Error("需要明确分享选择");
45
+ const prior = this.db
46
+ .prepare(
47
+ "SELECT * FROM mind_time_shares WHERE work_id=? AND version=? AND session_id=?",
48
+ )
49
+ .get(workId, work.version, session);
50
+ if (prior && ["uncertain", "sending", "sent"].includes(prior.state))
51
+ return prior;
52
+ const state = { send: "pending", later: "deferred", decline: "declined" }[
53
+ choice
54
+ ],
55
+ id = prior?.id || randomUUID();
56
+ this.db
57
+ .prepare(
58
+ "INSERT INTO mind_time_shares(id,created,updated,work_id,version,session_id,state,reason,delivery_kind) VALUES (?,?,?,?,?,?,?,?, 'report') ON CONFLICT(work_id,version,session_id) DO UPDATE SET state=excluded.state,reason=excluded.reason,updated=excluded.updated",
59
+ )
60
+ .run(
61
+ id,
62
+ now,
63
+ now,
64
+ workId,
65
+ work.version,
66
+ session,
67
+ state,
68
+ text(reason, 240),
69
+ );
70
+ if (work.task_id)
71
+ this.db
72
+ .prepare(
73
+ "UPDATE mind_time_tasks SET share_state=?,share_reason=?,next_step=? WHERE id=?",
74
+ )
75
+ .run(state, text(reason, 240), now + 30 * 60000, work.task_id);
76
+ this.time.event(
77
+ work.task_id,
78
+ "share-choice",
79
+ reason,
80
+ { choice, workId, session },
81
+ now,
82
+ );
83
+ return this.db.prepare("SELECT * FROM mind_time_shares WHERE id=?").get(id);
84
+ }
85
+ async run(life, now) {
86
+ if (!life.online() || life.repo.store.settings().demo || life.closed)
87
+ return null;
88
+ let share = this.db
89
+ .prepare(
90
+ "SELECT * FROM mind_time_shares WHERE state='pending' AND next_step<=? ORDER BY created LIMIT 1",
91
+ )
92
+ .get(now);
93
+ if (!share) {
94
+ const task = this.db
95
+ .prepare(
96
+ "SELECT id FROM mind_time_tasks WHERE state='done' AND share_state IN ('waiting','deferred') AND next_step<=? AND subject IS NOT NULL ORDER BY due_at,created LIMIT 1",
97
+ )
98
+ .get(now);
99
+ if (!task || !life.mind.budget.allows("inner", now)) return null;
100
+ const item = this.time.tasks.get(task.id),
101
+ work = this.time.works.get(item.work_id);
102
+ if (!work || !this.time.visible(work, item.session_id, now)) return null;
103
+ this.db
104
+ .prepare("UPDATE mind_time_tasks SET next_step=? WHERE id=?")
105
+ .run(now + 30 * 60000, item.id);
106
+ const trace = life.repo.trace("__mind__", "activity");
107
+ try {
108
+ const result = await withFallback(
109
+ life.chat.models,
110
+ life.chat.fallbackFor(null, life.profile(), trace),
111
+ ).call(
112
+ life.profile(),
113
+ "reflection",
114
+ replyPrompt(
115
+ life.mind.nature.current(now),
116
+ {
117
+ ...prompts(life.repo),
118
+ activity:
119
+ '作品已经保存,是否现在履行给这个人的交付?这是你自己的选择。输出JSON {"choice":"send|later|decline","reason":"原因"}。不能说已经发送。',
120
+ },
121
+ "activity",
122
+ ),
123
+ {
124
+ task: item.title,
125
+ person:
126
+ displayNames(this.db, now).get(String(item.subject)) || null,
127
+ work: {
128
+ id: work.id,
129
+ title: work.title,
130
+ summary: work.summary,
131
+ state: work.state,
132
+ },
133
+ },
134
+ trace,
135
+ );
136
+ share = this.choose(
137
+ work.id,
138
+ item.session_id,
139
+ {
140
+ choice: result.choice || "later",
141
+ reason: result.reason || "暂缓,稍后再决定",
142
+ },
143
+ life.now(),
144
+ );
145
+ } finally {
146
+ life.chat.finishQuietly(trace, "complete");
147
+ }
148
+ }
149
+ if (share?.state !== "pending") return null;
150
+ return this.time.attention.chat(() => this.send(life, share));
151
+ }
152
+ async send(life, share) {
153
+ const current = this.db
154
+ .prepare("SELECT * FROM mind_time_shares WHERE id=?")
155
+ .get(share.id);
156
+ if (!current || current.state !== "pending") return null;
157
+ share = current;
158
+ const work = this.time.works.get(share.work_id, share.version);
159
+ if (work && !this.time.works.materialSupported(work)) {
160
+ this.db
161
+ .prepare(
162
+ "UPDATE mind_time_shares SET state='declined',reason=? WHERE id=?",
163
+ )
164
+ .run("引用资料与作品无关,保留记录但停止分享", share.id);
165
+ return { status: "declined", reason: "引用资料与作品无关" };
166
+ }
167
+ if (
168
+ !work ||
169
+ !life.chat.enabled(share.session_id, { simulated: false }) ||
170
+ !this.time.visible(work, share.session_id, life.now())
171
+ )
172
+ return null;
173
+ // Where proactive messages are switched off she does not knock; the work
174
+ // waits and she looks again later.
175
+ if (life.canReach && !life.canReach(share.session_id)) {
176
+ const reason = "对方没有开启主动消息,等对方来找她";
177
+ this.db
178
+ .prepare(
179
+ "UPDATE mind_time_shares SET reason=?,next_step=? WHERE id=? AND state='pending'",
180
+ )
181
+ .run(reason, life.now() + 60 * 60000, share.id);
182
+ return { status: "waiting", reason };
183
+ }
184
+ if (share.delivery_kind === "report" && !parse(share.report, []).length) {
185
+ try {
186
+ return await this.review(life, share, work);
187
+ } catch (error) {
188
+ const reason = text(error.message, 180);
189
+ this.db
190
+ .prepare(
191
+ "UPDATE mind_time_shares SET reason=?,next_step=? WHERE id=? AND state='pending'",
192
+ )
193
+ .run(reason, life.now() + 15 * 60000, share.id);
194
+ return { status: "waiting", reason };
195
+ }
196
+ }
197
+ const task = this.time.tasks.get(work.task_id),
198
+ trace = life.repo.trace(share.session_id, "live");
199
+ trace.decision = { targetMessageIds: [] };
200
+ const report = parse(share.report, []),
201
+ chars = Array.from(work.content),
202
+ bubbles = [];
203
+ if (share.delivery_kind === "report")
204
+ bubbles.push(...report.slice(share.send_index, share.send_index + 3));
205
+ else
206
+ for (
207
+ let pos = share.offset;
208
+ pos < chars.length && bubbles.length < 3;
209
+ pos += 500
210
+ )
211
+ bubbles.push(chars.slice(pos, pos + 500).join(""));
212
+ if (!bubbles.length) return null;
213
+ const reserved = this.db
214
+ .prepare(
215
+ "UPDATE mind_time_shares SET state='sending',trace_id=?,updated=? WHERE id=? AND state='pending'",
216
+ )
217
+ .run(trace.id, life.now(), share.id);
218
+ if (!reserved.changes) return null;
219
+ const kind =
220
+ this.time.works.project(work.project_id)?.kind ||
221
+ this.db
222
+ .prepare("SELECT kind FROM mind_creations WHERE id=?")
223
+ .get(work.legacy_creation || "")?.kind;
224
+ const route = parseSessionKey(share.session_id),
225
+ anchor = parse(
226
+ this.db
227
+ .prepare(
228
+ "SELECT payload FROM core_events WHERE session_id=? ORDER BY seq DESC LIMIT 1",
229
+ )
230
+ .get(share.session_id)?.payload,
231
+ {},
232
+ );
233
+ const message = {
234
+ sessionId: share.session_id,
235
+ kind: route.kind,
236
+ userId:
237
+ route.kind === "private"
238
+ ? route.nativeId
239
+ : task?.subject || route.nativeId,
240
+ accountId:
241
+ route.accountId !== "_" ? route.accountId : anchor.accountId || "",
242
+ nativeId: route.nativeId,
243
+ channel: route.channel,
244
+ artifact: {
245
+ workId: work.id,
246
+ version: work.version,
247
+ domain:
248
+ kind === "game"
249
+ ? "reference"
250
+ : kind === "write"
251
+ ? "fiction"
252
+ : "personal-notes",
253
+ },
254
+ name: life.mind.nature.current().name,
255
+ attachments: [],
256
+ };
257
+ let status = "complete";
258
+ try {
259
+ await deliver(
260
+ life.repo,
261
+ message,
262
+ bubbles,
263
+ trace,
264
+ life.chat.send,
265
+ () =>
266
+ !life.closed &&
267
+ life.chat.enabled(share.session_id, { simulated: false }) &&
268
+ this.time.visible(work, share.session_id, life.now()),
269
+ { now: life.now },
270
+ );
271
+ } catch (error) {
272
+ status = "error";
273
+ trace.error = text(error.message, 200);
274
+ }
275
+ const confirmed = (trace.sent || []).reduce(
276
+ (n, s) => n + Array.from(s).length,
277
+ 0,
278
+ ),
279
+ offset =
280
+ share.delivery_kind === "report"
281
+ ? share.offset
282
+ : share.offset + confirmed,
283
+ sendIndex = share.send_index + (trace.sent || []).length;
284
+ const state =
285
+ status === "error"
286
+ ? "uncertain"
287
+ : (
288
+ share.delivery_kind === "report"
289
+ ? sendIndex >= report.length
290
+ : offset >= chars.length
291
+ )
292
+ ? "sent"
293
+ : "pending";
294
+ this.db
295
+ .prepare(
296
+ "UPDATE mind_time_shares SET state=?,offset=?,send_index=?,updated=?,reason=? WHERE id=?",
297
+ )
298
+ .run(state, offset, sendIndex, life.now(), trace.error || "", share.id);
299
+ if (task) {
300
+ this.db
301
+ .prepare(
302
+ "UPDATE mind_time_tasks SET share_state=?,share_reason=? WHERE id=?",
303
+ )
304
+ .run(
305
+ state === "sent" && share.delivery_kind === "report"
306
+ ? "reported"
307
+ : state,
308
+ trace.error || "",
309
+ task.id,
310
+ );
311
+ if (
312
+ state === "sent" &&
313
+ task.anticipation_id &&
314
+ !task.checkpoint.contract?.originalGoal
315
+ )
316
+ life.mind.anticipations.close(task.anticipation_id, {
317
+ status: "done",
318
+ note:
319
+ share.delivery_kind === "report"
320
+ ? "回看成果后整理的汇报已确认送达"
321
+ : "作品已全部确认交付",
322
+ sources: task.sources,
323
+ time: life.now(),
324
+ });
325
+ }
326
+ this.time.event(
327
+ task?.id,
328
+ "delivery",
329
+ state === "sent"
330
+ ? share.delivery_kind === "report"
331
+ ? "整理后的汇报已逐句送达"
332
+ : "作品已完整确认交付"
333
+ : state === "uncertain"
334
+ ? "送达状态不确定,需要核对"
335
+ : "保存部分交付位置",
336
+ {
337
+ workId: work.id,
338
+ offset,
339
+ state,
340
+ deliveryKind: share.delivery_kind,
341
+ sendIndex,
342
+ },
343
+ life.now(),
344
+ );
345
+ life.chat.finishQuietly(trace, status);
346
+ return { status: "shared", state, workId: work.id, offset };
347
+ }
348
+ async review(life, share, work) {
349
+ if (!life.mind.budget.allows("inner", life.now()))
350
+ return { status: "waiting", reason: "今日独处预算不足" };
351
+ const notes = parse(share.review_notes, []),
352
+ chars = Array.from(work.content),
353
+ reviewing = share.review_offset < chars.length,
354
+ end = Math.min(chars.length, share.review_offset + 2400),
355
+ trace = life.repo.trace(share.session_id, "activity"),
356
+ version = life.mind.nature.version();
357
+ let status = "error";
358
+ this.reviewing = {
359
+ share,
360
+ work,
361
+ phase: reviewing ? "reading" : "composing",
362
+ };
363
+ try {
364
+ const result = await withFallback(
365
+ life.chat.models,
366
+ life.chat.fallbackFor(null, life.profile(), trace),
367
+ ).call(
368
+ life.profile(),
369
+ "reflection",
370
+ replyPrompt(
371
+ life.mind.nature.current(),
372
+ {
373
+ ...prompts(life.repo),
374
+ activity: reviewing
375
+ ? '回看自己已经完成的成果中的这一段,抓住实际内容、感受和想分享的重点。只整理,不发消息,不复制全文。输出JSON {"note":"最多220字阅读整理"}。小说人物属于作品,游戏剧情属于游戏世界。'
376
+ : '你已逐段回看自己的成果。现在根据notes整理想告诉这个人的话,带自己的感受与具体内容,像日常聊天一样一句一句说。不要粘贴正文,不写大段汇报或列表,不解释实现方式,不暴露后台地址。输出JSON {"bubbles":["一句自然的话","再接一句"]},2至6句,每句不超过80字,总计不超过350字。可以摘一句喜欢的台词,不复制整篇。不输出隐藏推理。',
377
+ },
378
+ "activity",
379
+ ),
380
+ reviewing
381
+ ? {
382
+ title: work.title,
383
+ passage: chars.slice(share.review_offset, end).join(""),
384
+ position: share.review_offset,
385
+ total: chars.length,
386
+ }
387
+ : {
388
+ title: work.title,
389
+ notes,
390
+ person:
391
+ displayNames(this.db, life.now()).get(
392
+ String(this.time.tasks.get(work.task_id)?.subject),
393
+ ) || null,
394
+ currentLife: this.time.view({ session: share.session_id }),
395
+ },
396
+ trace,
397
+ );
398
+ const current = this.db
399
+ .prepare("SELECT * FROM mind_time_shares WHERE id=?")
400
+ .get(share.id);
401
+ if (
402
+ life.closed ||
403
+ version !== life.mind.nature.version() ||
404
+ current?.state !== "pending" ||
405
+ current.review_offset !== share.review_offset ||
406
+ current.report !== share.report ||
407
+ !this.time.visible(work, share.session_id, life.now())
408
+ )
409
+ return { status: "cancelled" };
410
+ if (reviewing) {
411
+ const note = text(result.note, 220);
412
+ if (
413
+ !note ||
414
+ hasCredential(note) ||
415
+ leaks(
416
+ [note],
417
+ this.time.mind.meetings.privateSayings(share.session_id),
418
+ ).length
419
+ )
420
+ throw Error("成果整理格式无效");
421
+ this.db
422
+ .prepare(
423
+ "UPDATE mind_time_shares SET review_offset=?,review_notes=?,updated=? WHERE id=?",
424
+ )
425
+ .run(end, JSON.stringify([...notes, note]), life.now(), share.id);
426
+ status = "complete";
427
+ return {
428
+ status: "reviewing",
429
+ reason: "正在回看自己的成果",
430
+ read: end,
431
+ total: chars.length,
432
+ };
433
+ }
434
+ const bubbles = naturalReport(result.bubbles, work.content);
435
+ if (
436
+ leaks(bubbles, this.time.mind.meetings.privateSayings(share.session_id))
437
+ .length
438
+ )
439
+ throw Error("汇报涉及私下内容");
440
+ this.db
441
+ .prepare("UPDATE mind_time_shares SET report=?,updated=? WHERE id=?")
442
+ .run(JSON.stringify(bubbles), life.now(), share.id);
443
+ status = "complete";
444
+ return { status: "reviewed", reason: "已整理成几句想说的话" };
445
+ } finally {
446
+ this.reviewing = null;
447
+ life.chat.finishQuietly(trace, status);
448
+ }
449
+ }
450
+ }
451
+ export function naturalReport(input, original) {
452
+ if (!Array.isArray(input)) throw Error("需要几句自然汇报");
453
+ const lines = input
454
+ .flatMap((value) =>
455
+ String(value)
456
+ .split(/[\r\n]+/)
457
+ .flatMap(
458
+ (s) =>
459
+ s.match(/[^。!?!?]+[。!?!?]+[”」』"']*|[^。!?!?]+$/gu) || [],
460
+ ),
461
+ )
462
+ .map((s) => s.trim())
463
+ .filter(Boolean);
464
+ if (
465
+ !lines.length ||
466
+ lines.length > 6 ||
467
+ lines.some((s) => Array.from(s).length > 80) ||
468
+ lines.join("").length > 350
469
+ )
470
+ throw Error("汇报需要简短的一句一句说");
471
+ if (
472
+ hasCredential(lines.join(" ")) ||
473
+ /https?:\/\/|127\.0\.0\.1|localhost|管理台地址/.test(lines.join(" "))
474
+ )
475
+ throw Error("汇报包含不适合公开的信息");
476
+ if (
477
+ lines.join("").replace(/\s/g, "") === String(original).replace(/\s/g, "") ||
478
+ lines.filter((s) => s.length > 40 && original.includes(s)).length > 1
479
+ )
480
+ throw Error("请回看后整理,不要复制正文");
481
+ return lines;
482
+ }