luckytri 1.0.0

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 (235) hide show
  1. package/.github/SECURITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +250 -0
  4. package/README.md +250 -0
  5. package/bin/luckytri.js +107 -0
  6. package/docs/brand/banner.png +0 -0
  7. package/docs/brand/palette.png +0 -0
  8. package/docs/brand/portrait.png +0 -0
  9. package/docs/en/CHANGELOG.md +113 -0
  10. package/docs/en/architecture.md +152 -0
  11. package/docs/en/chat-style.md +62 -0
  12. package/docs/en/connect.md +63 -0
  13. package/docs/en/guide.md +216 -0
  14. package/docs/en/her-life.md +189 -0
  15. package/docs/en/npm-release.md +25 -0
  16. package/docs/en/plugins.md +82 -0
  17. package/docs/en/recovery.md +28 -0
  18. package/docs/en/time.md +71 -0
  19. package/docs/en/webui.md +73 -0
  20. package/docs/zh/CHANGELOG.md +113 -0
  21. package/docs/zh/architecture.md +152 -0
  22. package/docs/zh/chat-style.md +62 -0
  23. package/docs/zh/connect.md +63 -0
  24. package/docs/zh/guide.md +216 -0
  25. package/docs/zh/her-life.md +189 -0
  26. package/docs/zh/npm-release.md +33 -0
  27. package/docs/zh/plugins.md +82 -0
  28. package/docs/zh/recovery.md +28 -0
  29. package/docs/zh/time.md +71 -0
  30. package/docs/zh/webui.md +73 -0
  31. package/package.json +108 -0
  32. package/plugins/sdk/index.d.ts +155 -0
  33. package/plugins/sdk/testing.js +58 -0
  34. package/plugins/weather/index.js +82 -0
  35. package/plugins/weather/luckytri-plugin.json +29 -0
  36. package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
  37. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  38. package/public/app/assets/Empty-CpP5kQHH.js +2 -0
  39. package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
  40. package/public/app/assets/Field-D4gY-INU.js +1 -0
  41. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  42. package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
  43. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  44. package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
  45. package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
  46. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  47. package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
  48. package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
  49. package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
  50. package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
  51. package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
  52. package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
  53. package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
  54. package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
  55. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  56. package/public/app/assets/TimePage-Cw44inM4.js +3 -0
  57. package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
  58. package/public/app/assets/index-BheRW-1t.js +4 -0
  59. package/public/app/assets/index-YDz1ree1.css +1 -0
  60. package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
  61. package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
  62. package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
  63. package/public/app/assets/media-BzPDBm7e.js +1 -0
  64. package/public/app/index.html +27 -0
  65. package/public/guide.en.html +1053 -0
  66. package/public/guide.html +893 -0
  67. package/public/plugin-kit/v1/bridge.js +30 -0
  68. package/public/plugin-kit/v1/kit.css +14 -0
  69. package/public/tutorial.en.md +214 -0
  70. package/public/tutorial.md +214 -0
  71. package/scripts/backup.js +311 -0
  72. package/scripts/browser.js +41 -0
  73. package/scripts/launch.js +94 -0
  74. package/scripts/plugin.js +150 -0
  75. package/scripts/proxy-config.js +167 -0
  76. package/scripts/recover.js +26 -0
  77. package/scripts/runtime.js +25 -0
  78. package/scripts/service.js +47 -0
  79. package/scripts/setup.js +50 -0
  80. package/scripts/start-server.js +41 -0
  81. package/scripts/stop.js +36 -0
  82. package/server/app.js +96 -0
  83. package/server/channels/adapters.js +27 -0
  84. package/server/channels/contract.js +57 -0
  85. package/server/channels/hub.js +165 -0
  86. package/server/channels/index.js +25 -0
  87. package/server/channels/onebot/adapter.js +288 -0
  88. package/server/channels/onebot/index.js +336 -0
  89. package/server/channels/onebot/token.js +3 -0
  90. package/server/channels/qqbot/adapter.js +253 -0
  91. package/server/channels/qqbot/api.js +199 -0
  92. package/server/channels/qqbot/credentials.js +11 -0
  93. package/server/channels/qqbot/gateway.js +274 -0
  94. package/server/channels/qqbot/index.js +389 -0
  95. package/server/channels/segments.js +40 -0
  96. package/server/channels/session-key.js +153 -0
  97. package/server/channels/state.js +42 -0
  98. package/server/core/api.js +564 -0
  99. package/server/core/capabilities.js +208 -0
  100. package/server/core/context-builder.js +288 -0
  101. package/server/core/context-compactor.js +454 -0
  102. package/server/core/conversation-cues.js +325 -0
  103. package/server/core/conversation-manager.js +87 -0
  104. package/server/core/events.js +60 -0
  105. package/server/core/feedback.js +8 -0
  106. package/server/core/group-style.js +72 -0
  107. package/server/core/image-preprocessor.js +208 -0
  108. package/server/core/initiative-context.js +66 -0
  109. package/server/core/input-budget.js +32 -0
  110. package/server/core/local-demo.js +98 -0
  111. package/server/core/message-manager.js +118 -0
  112. package/server/core/message-scheduler.js +78 -0
  113. package/server/core/model-json.js +190 -0
  114. package/server/core/model-manager.js +1213 -0
  115. package/server/core/model-presets.js +900 -0
  116. package/server/core/name-call.js +29 -0
  117. package/server/core/net-guard.js +49 -0
  118. package/server/core/network.js +157 -0
  119. package/server/core/orchestrator.js +569 -0
  120. package/server/core/perceivers.js +91 -0
  121. package/server/core/person-name.js +22 -0
  122. package/server/core/persona-manager.js +243 -0
  123. package/server/core/reply-delivery.js +393 -0
  124. package/server/core/reply-target-resolver.js +50 -0
  125. package/server/core/repository.js +293 -0
  126. package/server/core/response-generator.js +50 -0
  127. package/server/core/response-validator.js +508 -0
  128. package/server/core/retired-prompts.js +51 -0
  129. package/server/core/sessions.js +109 -0
  130. package/server/core/speaker-names.js +92 -0
  131. package/server/core/topic-tracker.js +65 -0
  132. package/server/core/turn-processor.js +630 -0
  133. package/server/core/turn.js +236 -0
  134. package/server/core/vision-manager.js +633 -0
  135. package/server/http.js +18 -0
  136. package/server/i18n/decisions.js +206 -0
  137. package/server/i18n/errors.js +497 -0
  138. package/server/i18n/index.js +144 -0
  139. package/server/i18n/labels.js +653 -0
  140. package/server/i18n/models.js +111 -0
  141. package/server/index.js +102 -0
  142. package/server/knowledge/api.js +342 -0
  143. package/server/knowledge/embedding-profile.js +114 -0
  144. package/server/knowledge/index.js +13 -0
  145. package/server/knowledge/manager.js +386 -0
  146. package/server/knowledge/retrieval.js +93 -0
  147. package/server/knowledge/schema.js +154 -0
  148. package/server/knowledge/vector-index.js +90 -0
  149. package/server/mind/affect.js +265 -0
  150. package/server/mind/anticipations.js +507 -0
  151. package/server/mind/api.js +703 -0
  152. package/server/mind/attention.js +200 -0
  153. package/server/mind/bonds.js +558 -0
  154. package/server/mind/budget.js +152 -0
  155. package/server/mind/clock.js +83 -0
  156. package/server/mind/context-selection.js +61 -0
  157. package/server/mind/continuity.js +562 -0
  158. package/server/mind/conversation-origin.js +175 -0
  159. package/server/mind/day-planner.js +324 -0
  160. package/server/mind/days.js +210 -0
  161. package/server/mind/evidence-read.js +45 -0
  162. package/server/mind/evidence.js +148 -0
  163. package/server/mind/faces.js +138 -0
  164. package/server/mind/guard.js +60 -0
  165. package/server/mind/index.js +445 -0
  166. package/server/mind/initiative.js +333 -0
  167. package/server/mind/life/activities.js +21 -0
  168. package/server/mind/life/context.js +309 -0
  169. package/server/mind/life/diary.js +786 -0
  170. package/server/mind/life/growth.js +252 -0
  171. package/server/mind/life/index.js +1049 -0
  172. package/server/mind/life/presence.js +305 -0
  173. package/server/mind/meetings.js +1069 -0
  174. package/server/mind/memory-policy.js +48 -0
  175. package/server/mind/memory.js +888 -0
  176. package/server/mind/nature.js +262 -0
  177. package/server/mind/novelty.js +19 -0
  178. package/server/mind/observations.js +84 -0
  179. package/server/mind/own-day.js +324 -0
  180. package/server/mind/own-voice.js +318 -0
  181. package/server/mind/periods.js +118 -0
  182. package/server/mind/reading.js +120 -0
  183. package/server/mind/salience.js +201 -0
  184. package/server/mind/schema.js +562 -0
  185. package/server/mind/self.js +484 -0
  186. package/server/mind/senses.js +53 -0
  187. package/server/mind/thoughts.js +274 -0
  188. package/server/mind/time/activity-clock.js +196 -0
  189. package/server/mind/time/agenda.js +242 -0
  190. package/server/mind/time/api.js +200 -0
  191. package/server/mind/time/attention.js +34 -0
  192. package/server/mind/time/availability.js +69 -0
  193. package/server/mind/time/commitment-review.js +198 -0
  194. package/server/mind/time/delivery-links.js +130 -0
  195. package/server/mind/time/executor.js +512 -0
  196. package/server/mind/time/experiences.js +128 -0
  197. package/server/mind/time/games.js +562 -0
  198. package/server/mind/time/index.js +739 -0
  199. package/server/mind/time/intent.js +166 -0
  200. package/server/mind/time/kinds.js +171 -0
  201. package/server/mind/time/model-material.js +77 -0
  202. package/server/mind/time/plugin-activity.js +207 -0
  203. package/server/mind/time/presentation.js +51 -0
  204. package/server/mind/time/schedule.js +83 -0
  205. package/server/mind/time/schema.js +84 -0
  206. package/server/mind/time/search.js +383 -0
  207. package/server/mind/time/sharing.js +472 -0
  208. package/server/mind/time/task-links.js +311 -0
  209. package/server/mind/time/tasks.js +587 -0
  210. package/server/mind/time/works.js +432 -0
  211. package/server/mind/traits.js +191 -0
  212. package/server/mind/util.js +110 -0
  213. package/server/mind/view.js +273 -0
  214. package/server/paths.js +102 -0
  215. package/server/plugins/api.js +204 -0
  216. package/server/plugins/host.js +967 -0
  217. package/server/plugins/install.js +295 -0
  218. package/server/plugins/manifest.js +201 -0
  219. package/server/plugins/permissions.js +42 -0
  220. package/server/plugins/runner/guard.mjs +36 -0
  221. package/server/plugins/runner/worker.mjs +250 -0
  222. package/server/plugins/schema.js +91 -0
  223. package/server/plugins/zip.js +112 -0
  224. package/server/startup-lock.js +118 -0
  225. package/server/storage/archive.js +241 -0
  226. package/server/storage/backup-cleanup-worker.js +8 -0
  227. package/server/storage/backup-cleanup.js +414 -0
  228. package/server/storage/backup-lock.js +76 -0
  229. package/server/storage/backup-scheduler.js +131 -0
  230. package/server/storage/store.js +199 -0
  231. package/server/studio/display-names.js +138 -0
  232. package/server/studio/management.js +102 -0
  233. package/server/studio/readiness.js +190 -0
  234. package/server/studio/settings.js +236 -0
  235. package/server/version.js +10 -0
@@ -0,0 +1,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 (
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
+ }