luckytri 1.3.0 → 1.3.3

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