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,512 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { localClock } from "../../core/conversation-cues.js";
3
+ import { withFallback } from "../../core/model-manager.js";
4
+ import { prompts, replyPrompt } from "../../core/persona-manager.js";
5
+ import { evidence, hasCredential, text } from "../util.js";
6
+ import { ACTIVITY_LABELS } from "./tasks.js";
7
+ import { activityLabel, isPluginActivity, needsEnergy } from "./kinds.js";
8
+ import { stepPlugin } from "./plugin-activity.js";
9
+ import { executionBlock } from "./availability.js";
10
+
11
+ const ACTIVITY_PROMPT =
12
+ '执行一个属于自己的实际活动步骤。write创作一个完整小段,已有草稿时只追加下一段,修改模式则提供修改后的正文;根据篇幅决定本篇是否结束,连载完成的是本章。read只能阅读input.reading实际提供的段落;think留下想法记录。不编造外部操作、游玩、通关或未提供的资料。虚构人物只属于作品,不能进入真实人物记忆。不输出隐藏推理。外部suggestion是别人的建议,你决定采纳或拒绝并保留理由,可以改变方向、暂停或完结项目。输出JSON {"done":false,"title":"标题","content":"最多1800字完整小段","summary":"情节摘要","next":"续接位置","bible":{"characters":"人物设定","world":"世界设定","threads":"未解决线索"},"feeling":{"feeling":"两三个字的心情","valence":0.2},"accepted":true,"reason":"自己的选择理由","projectState":"active|paused|complete","share":{"choice":"send|later|decline","reason":"分享选择"}}。完成本篇时done:true;休息/拒绝时没有content并说明reason。本步骤不发消息、不直接改变人格。';
13
+
14
+ export class TimeExecutor {
15
+ constructor(life) {
16
+ this.life = life;
17
+ this.time = life.mind.time;
18
+ this.db = life.db;
19
+ }
20
+ run(now = this.life.now()) {
21
+ return this.time.attention.activity(() => this.step(now));
22
+ }
23
+ async step(now) {
24
+ const life = this.life,
25
+ mind = life.mind,
26
+ time = this.time;
27
+ time.tasks.sync(now);
28
+ if (
29
+ life.closed ||
30
+ life.busy ||
31
+ life.repo.store.settings().demo ||
32
+ !life.settings().solitude ||
33
+ life.phase(now).key === "asleep"
34
+ )
35
+ return null;
36
+ let task, chunk;
37
+ let primary = time.primary();
38
+ if (primary && !primary.lease) {
39
+ const urgent = time.tasks
40
+ .ready(now)
41
+ .find(
42
+ (t) =>
43
+ t.priority > primary.priority &&
44
+ !executionBlock(time, t, now) &&
45
+ (time.fixtureImmediate ||
46
+ life.planner.disabledForTest ||
47
+ (t.checkpoint.schedule?.chosenAt && !t.checkpoint.reschedule)),
48
+ );
49
+ if (urgent) {
50
+ time.tasks.control(
51
+ primary.id,
52
+ {
53
+ action: "pause",
54
+ readyAt: now + 60000,
55
+ reason: "先处理一件优先级更高的事,保留续接位置",
56
+ },
57
+ now,
58
+ );
59
+ time.event(
60
+ primary.id,
61
+ "preempted",
62
+ "更高优先级事项先做",
63
+ { forTask: urgent.id, checkpoint: primary.checkpoint.next || "" },
64
+ now,
65
+ );
66
+ primary = null;
67
+ }
68
+ }
69
+ for (const candidate of primary ? [primary] : time.tasks.ready(now)) {
70
+ if (candidate.next_step > now) continue;
71
+ if (
72
+ !time.fixtureImmediate &&
73
+ !life.planner.disabledForTest &&
74
+ (!candidate.checkpoint.schedule?.chosenAt ||
75
+ candidate.checkpoint.reschedule)
76
+ )
77
+ continue;
78
+ const block = executionBlock(time, candidate, now);
79
+ if (
80
+ block &&
81
+ block !== "来源已经撤销" &&
82
+ !(candidate.activity === "read" && block === "书架没有可读资料")
83
+ ) {
84
+ time.tasks.wait(
85
+ candidate,
86
+ block,
87
+ /选定|想具体|执行能力/.test(block)
88
+ ? Number.MAX_SAFE_INTEGER
89
+ : now + 10 * 60000,
90
+ now,
91
+ );
92
+ continue;
93
+ }
94
+ if (
95
+ candidate.checkpoint.pendingStep?.chunk &&
96
+ !this.db
97
+ .prepare("SELECT 1 FROM core_chunks WHERE id=?")
98
+ .get(candidate.checkpoint.pendingStep.chunk.id)
99
+ ) {
100
+ time.tasks.wait(
101
+ candidate,
102
+ "书架这段内容已不可用",
103
+ Number.MAX_SAFE_INTEGER,
104
+ now,
105
+ );
106
+ continue;
107
+ }
108
+ if (!time.allowed(candidate, now)) {
109
+ time.tasks.control(
110
+ candidate.id,
111
+ { action: "abandon", reason: "来源已经撤销" },
112
+ now,
113
+ );
114
+ continue;
115
+ }
116
+ if (
117
+ candidate.session_id &&
118
+ !life.chat.enabled(candidate.session_id, { simulated: false })
119
+ ) {
120
+ time.tasks.wait(candidate, "来源会话目前未启用", now + 60000, now);
121
+ continue;
122
+ }
123
+ if (
124
+ !candidate.checkpoint.pendingStep &&
125
+ !mind.budget.allows("inner", now)
126
+ ) {
127
+ time.tasks.wait(candidate, "今日独处预算不足", now + 3600000, now);
128
+ continue;
129
+ }
130
+ if (
131
+ needsEnergy(candidate.activity) &&
132
+ mind.affect.state(now).energy < 0.18
133
+ ) {
134
+ time.tasks.wait(
135
+ candidate,
136
+ "精力较低,先恢复一会儿再投入",
137
+ now + 10 * 60000,
138
+ now,
139
+ );
140
+ continue;
141
+ }
142
+ if (candidate.activity === "game") {
143
+ const blocked = candidate.checkpoint.sourceIds?.length
144
+ ? ""
145
+ : time.search.ready(now);
146
+ if (blocked) {
147
+ time.tasks.wait(candidate, blocked, now + 60000, now);
148
+ continue;
149
+ }
150
+ }
151
+ chunk =
152
+ candidate.checkpoint.pendingStep?.chunk ||
153
+ (candidate.activity === "read" ? mind.reading.next(now) : null);
154
+ if (candidate.activity === "read" && !chunk) {
155
+ time.tasks.wait(candidate, "书架没有可读资料", now, now);
156
+ continue;
157
+ }
158
+ task = candidate;
159
+ break;
160
+ }
161
+ if (!task) return time.sharing.run(life, now);
162
+ let profile;
163
+ try {
164
+ if (!task.checkpoint.pendingStep) profile = life.profile();
165
+ } catch {
166
+ time.tasks.wait(task, "等待可用模型", now + 60000, now);
167
+ return null;
168
+ }
169
+ task = time.start(task, now);
170
+ if (!task) return null;
171
+ const runId = life.run(
172
+ "activity",
173
+ `${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "活动")}:${task.title}`,
174
+ ),
175
+ trace = life.repo.trace("__mind__", "activity"),
176
+ version = mind.nature.version();
177
+ let status = "empty",
178
+ reason = "这次还不想做",
179
+ creation = null;
180
+ try {
181
+ if (task.activity === "game") {
182
+ const outcome = await time.games.step(task, life, trace, runId);
183
+ status = outcome.status;
184
+ reason = outcome.reason;
185
+ return { ...outcome, runId };
186
+ }
187
+ if (isPluginActivity(task.activity)) {
188
+ const outcome = await stepPlugin(task, life, trace, runId);
189
+ status = outcome.status;
190
+ reason = outcome.reason;
191
+ return { ...outcome, runId };
192
+ }
193
+ const nature = mind.traits.effective(mind.nature.current(now), now);
194
+ const pending = task.checkpoint.pendingStep;
195
+ if (pending && time.clock.remaining(task, now) > 0) {
196
+ status = "engaged";
197
+ reason = "接着做这一段";
198
+ time.clock.release(task, task.checkpoint, now);
199
+ return {
200
+ status: "engaged",
201
+ reason: "接着做这一段",
202
+ task: task.id,
203
+ runId,
204
+ };
205
+ }
206
+ const result =
207
+ pending?.result ||
208
+ (await withFallback(
209
+ life.chat.models,
210
+ life.chat.fallbackFor(null, profile, trace),
211
+ ).call(
212
+ profile,
213
+ "reflection",
214
+ replyPrompt(
215
+ nature,
216
+ {
217
+ ...prompts(life.repo),
218
+ activity:
219
+ ACTIVITY_PROMPT +
220
+ '\n为本段行动估计通常需要的投入时长,输出duration:{minutes:15,basis:"按本段内容与行动估计的理由"}。只估计这一段阅读、写作或思考需要的时间,不用API处理秒数;她的实际节奏会按统一速度稍作加快。',
221
+ },
222
+ "activity",
223
+ ),
224
+ {
225
+ clock: localClock(now, mind.timeZone()),
226
+ activity: task.activity,
227
+ affect: mind.affect.state(now),
228
+ task: {
229
+ id: task.id,
230
+ title: task.title,
231
+ why: task.why,
232
+ checkpoint: task.checkpoint,
233
+ },
234
+ plan: {
235
+ ref: task.anticipation_id
236
+ ? `a:${task.anticipation_id}`
237
+ : task.sources[0],
238
+ content: task.title,
239
+ },
240
+ self: life
241
+ .selfView(now, { room: task.session_id || "" })
242
+ .slice(0, 8),
243
+ ...(chunk ? { reading: mind.reading.passage(chunk) } : {}),
244
+ creation: time.works.context(task),
245
+ currentLife: time.view({ session: task.session_id, now }),
246
+ },
247
+ trace,
248
+ ));
249
+ const finished = life.now();
250
+ if (
251
+ task.activity === "write" &&
252
+ result?.done === true &&
253
+ /(?:未完待续|[((]待续[))]|尚未完成|只是开头草稿)\s*[。.!!]?\s*$/.test(
254
+ String(result.content || ""),
255
+ )
256
+ )
257
+ result.done = false;
258
+ if (
259
+ life.closed ||
260
+ mind.nature.version() !== version ||
261
+ !time.valid(task)
262
+ ) {
263
+ status = "cancelled";
264
+ reason = "任务或服务状态已经变化";
265
+ } else if (
266
+ (result?.done === false && !result.content) ||
267
+ result?.skip ||
268
+ (task.kind === "suggestion" && result?.accepted === false)
269
+ ) {
270
+ reason = text(result.reason, 120) || reason;
271
+ if (task.kind === "suggestion" && result?.accepted === false) {
272
+ time.event(task.id, "suggestion-declined", reason, {}, finished);
273
+ time.tasks.control(task.id, { action: "abandon", reason }, finished);
274
+ } else
275
+ time.tasks.control(
276
+ task.id,
277
+ {
278
+ action: "pause",
279
+ readyAt: finished + time.settings().breakMinutes * 60000,
280
+ reason,
281
+ },
282
+ finished,
283
+ );
284
+ } else {
285
+ const title = text(result?.title, 80),
286
+ content = text(result?.content, 1800);
287
+ if (
288
+ typeof result?.done !== "boolean" ||
289
+ !title ||
290
+ !content ||
291
+ hasCredential(title + content)
292
+ )
293
+ throw Error("活动成果格式无效");
294
+ const sources = evidence([
295
+ ...(task.anticipation_id ? [`a:${task.anticipation_id}`] : []),
296
+ ...(chunk ? [`r:${chunk.id}`] : []),
297
+ ...task.sources,
298
+ ]);
299
+ if (!time.fixtureImmediate && !pending) {
300
+ this.db.exec("SAVEPOINT paced_draft");
301
+ try {
302
+ if (task.kind === "suggestion")
303
+ time.event(
304
+ task.id,
305
+ "suggestion-accepted",
306
+ text(result.reason, 240),
307
+ {},
308
+ finished,
309
+ );
310
+ const work = time.works.save(
311
+ task,
312
+ { ...result, done: false },
313
+ { sources, runId, now: finished },
314
+ );
315
+ const checkpoint = time.tasks.get(task.id).checkpoint;
316
+ time.clock.release(
317
+ task,
318
+ {
319
+ ...checkpoint,
320
+ activityClock: time.clock.plan(task, result.duration, finished),
321
+ pendingStep: {
322
+ result: { ...result, title, content },
323
+ workId: work.id,
324
+ chunk,
325
+ sources,
326
+ runId,
327
+ },
328
+ next: result.next || "接着推敲与整理这一段",
329
+ },
330
+ finished,
331
+ );
332
+ status = "draft";
333
+ reason = "草稿已保存,接着完成这一段";
334
+ return { status, reason, task: task.id, runId, workId: work.id };
335
+ } catch (error) {
336
+ this.db.exec("ROLLBACK TO paced_draft");
337
+ throw error;
338
+ } finally {
339
+ this.db.exec("RELEASE paced_draft");
340
+ }
341
+ }
342
+ this.db.exec("SAVEPOINT time_execution");
343
+ try {
344
+ if (task.kind === "suggestion" && !pending)
345
+ time.event(
346
+ task.id,
347
+ "suggestion-accepted",
348
+ text(result.reason, 240),
349
+ {},
350
+ finished,
351
+ );
352
+ if (pending) {
353
+ const checkpoint = {
354
+ ...task.checkpoint,
355
+ activityClock: {
356
+ ...task.checkpoint.activityClock,
357
+ phase: "finished",
358
+ },
359
+ };
360
+ delete checkpoint.pendingStep;
361
+ this.db
362
+ .prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?")
363
+ .run(JSON.stringify(checkpoint), task.id);
364
+ }
365
+ const work = pending
366
+ ? time.works.settle(task, pending.workId, result.done, finished)
367
+ : time.works.save(task, result, {
368
+ sources,
369
+ runId,
370
+ now: finished,
371
+ });
372
+ if (chunk)
373
+ mind.reading.record(chunk, text(content, 120), runId, finished);
374
+ if (result.done) {
375
+ creation = randomUUID();
376
+ this.db
377
+ .prepare(
378
+ "INSERT INTO mind_creations VALUES (?,?,?,?,?,?,?,?,?,?)",
379
+ )
380
+ .run(
381
+ creation,
382
+ finished,
383
+ task.anticipation_id || task.id,
384
+ task.activity,
385
+ title,
386
+ work.content,
387
+ JSON.stringify(sources),
388
+ task.session_id,
389
+ task.discretion,
390
+ runId,
391
+ );
392
+ mind.thoughts.add({
393
+ // Narrative remains in the work. Only the act becomes an experience.
394
+ kind: "reflection",
395
+ content:
396
+ task.activity === "write"
397
+ ? `我完成了《${title}》的第 ${work.ordinal} 篇,正文保存在作品库;其中情节和人物是创作。`
398
+ : text(`${title}:${content}`, 600),
399
+ sources: evidence([`x:${task.id}`, ...sources]),
400
+ sessions: task.session_id ? [task.session_id] : [],
401
+ runId,
402
+ time: finished,
403
+ importance: 0.4,
404
+ });
405
+ time.complete(
406
+ task,
407
+ { creation, workId: work.id, sources },
408
+ finished,
409
+ );
410
+ this.db
411
+ .prepare(
412
+ "UPDATE mind_time_works SET legacy_creation=? WHERE id=?",
413
+ )
414
+ .run(creation, work.id);
415
+ const project = time.works.project(task.project_id);
416
+ if (
417
+ project.serial &&
418
+ result.next &&
419
+ (!result.projectState || result.projectState === "active")
420
+ )
421
+ time.tasks.add(
422
+ {
423
+ kind: "plan",
424
+ activity: task.activity,
425
+ title: `接续《${project.title}》第 ${work.ordinal + 1} 篇`,
426
+ why: text(result.reason || result.next, 240),
427
+ sources: [`x:${task.id}`],
428
+ session: task.session_id,
429
+ discretion: task.discretion,
430
+ projectId: project.id,
431
+ readyAt: finished + time.settings().breakMinutes * 60000,
432
+ activeOnly: true,
433
+ },
434
+ finished,
435
+ );
436
+ if (result.share?.choice && task.session_id)
437
+ time.sharing.choose(
438
+ work.id,
439
+ task.session_id,
440
+ result.share,
441
+ finished,
442
+ );
443
+ } else time.progress(task, work, result, finished);
444
+ if (result.feeling?.feeling)
445
+ mind.affect.feel({
446
+ ...result.feeling,
447
+ intensity: 0.2,
448
+ cause: `做自己的${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "事")}时的感受`,
449
+ sources: [`x:${task.id}`],
450
+ session: task.session_id,
451
+ origin: "activity",
452
+ time: finished,
453
+ });
454
+ if (["paused", "complete"].includes(result.projectState))
455
+ this.db
456
+ .prepare("UPDATE mind_time_projects SET state=? WHERE id=?")
457
+ .run(result.projectState, task.project_id);
458
+ if (result.projectState === "paused" && !result.done)
459
+ time.tasks.control(
460
+ task.id,
461
+ {
462
+ action: "pause",
463
+ reason: text(result.reason, 240) || "自己选择暂时放下这个项目",
464
+ },
465
+ finished,
466
+ );
467
+ this.db.exec("RELEASE time_execution");
468
+ } catch (error) {
469
+ this.db.exec("ROLLBACK TO time_execution");
470
+ this.db.exec("RELEASE time_execution");
471
+ creation = null;
472
+ throw error;
473
+ }
474
+ status = result.done ? "written" : "draft";
475
+ reason = `${activityLabel(task.activity, ACTIVITY_LABELS[task.activity] || "活动")},保存了《${title}》${result.done ? "完成稿" : "草稿"}`;
476
+ }
477
+ } catch (error) {
478
+ status = "error";
479
+ reason = text(error.message, 200);
480
+ trace.error = reason;
481
+ if (time.valid(task))
482
+ time.tasks.wait(task, reason, life.now() + 15 * 60000, life.now());
483
+ } finally {
484
+ if (time.valid(task))
485
+ time.tasks.control(
486
+ task.id,
487
+ {
488
+ action: "pause",
489
+ readyAt: life.now() + time.settings().breakMinutes * 60000,
490
+ reason,
491
+ },
492
+ life.now(),
493
+ );
494
+ trace.reason = reason;
495
+ life.chat.finishQuietly(trace, status === "error" ? "error" : "complete");
496
+ life.end(runId, status, reason, trace, {
497
+ plan: task.anticipation_id,
498
+ task: task.id,
499
+ activity: task.activity,
500
+ creation,
501
+ });
502
+ }
503
+ return {
504
+ status,
505
+ reason,
506
+ runId,
507
+ activity: task.activity,
508
+ creation,
509
+ task: task.id,
510
+ };
511
+ }
512
+ }
@@ -0,0 +1,128 @@
1
+ import { parse, text } from "../util.js";
2
+ import { gameText } from "./presentation.js";
3
+ import { activityLabel } from "./kinds.js";
4
+
5
+ const labels = {
6
+ game: "游玩",
7
+ write: "创作",
8
+ read: "阅读",
9
+ think: "独处思考",
10
+ research: "探索",
11
+ };
12
+ const generic =
13
+ /^(?:留下了实际成果|玩过这一段,留下自己的感受|保存位置,下次接着做)$/;
14
+
15
+ // Events remain the audit trail. The library shows each saved outcome once,
16
+ // using its historical version rather than today's task or latest manuscript.
17
+ export function experiences(db, { limit = 40, offset = 0, q = "" } = {}) {
18
+ const rows = db
19
+ .prepare(
20
+ `
21
+ WITH drafts AS MATERIALIZED (
22
+ SELECT id,created,reason,json_extract(data,'$.workId') work_id,
23
+ json_extract(data,'$.version') version FROM mind_time_events WHERE kind='draft'
24
+ ), linked AS (
25
+ SELECT e.*,t.activity,t.title task_title,w.id work_id,w.project_id,
26
+ p.title project_title,p.kind project_kind,v.version,v.title work_title,
27
+ v.summary,substr(v.content,1,300) excerpt,length(v.content) characters,
28
+ v.provenance,
29
+ (SELECT reason FROM drafts d WHERE d.work_id=w.id AND d.version=v.version
30
+ AND d.created<=e.created AND d.id<e.id
31
+ ORDER BY d.created DESC,d.id DESC LIMIT 1) saved_reason
32
+ FROM mind_time_events e
33
+ LEFT JOIN mind_time_tasks t ON t.id=e.task_id
34
+ LEFT JOIN mind_time_works w ON w.id=COALESCE(json_extract(e.data,'$.workId'),
35
+ (SELECT id FROM mind_time_works WHERE legacy_creation=json_extract(e.data,'$.creation')))
36
+ LEFT JOIN mind_time_projects p ON p.id=w.project_id
37
+ LEFT JOIN mind_time_versions v ON v.work_id=w.id AND v.created<=e.created
38
+ AND v.version=COALESCE(json_extract(e.data,'$.version'),
39
+ (SELECT version FROM drafts d WHERE d.work_id=w.id AND d.created<=e.created AND d.id<e.id
40
+ ORDER BY d.created DESC,d.id DESC LIMIT 1),
41
+ CASE WHEN w.legacy_creation IS NOT NULL THEN
42
+ (SELECT MAX(version) FROM mind_time_versions WHERE work_id=w.id AND created<=e.created) END)
43
+ WHERE e.kind IN ('reference-experience','done','checkpoint')
44
+ ), grouped AS (
45
+ SELECT *,ROW_NUMBER() OVER (PARTITION BY
46
+ CASE WHEN version IS NOT NULL THEN work_id||':'||version ELSE 'event:'||id END
47
+ ORDER BY CASE kind WHEN 'reference-experience' THEN 3 WHEN 'done' THEN 2 ELSE 1 END DESC,
48
+ created DESC,id DESC) position,
49
+ MAX(kind='done') OVER (PARTITION BY
50
+ CASE WHEN version IS NOT NULL THEN work_id||':'||version ELSE 'event:'||id END) completed
51
+ FROM linked
52
+ )
53
+ SELECT * FROM grouped WHERE position=1
54
+ AND (COALESCE(work_title,'') LIKE ? OR COALESCE(task_title,'') LIKE ?
55
+ OR COALESCE(project_title,'') LIKE ? OR COALESCE(summary,'') LIKE ?)
56
+ ORDER BY created DESC,id DESC LIMIT ? OFFSET ?
57
+ `,
58
+ )
59
+ .all(
60
+ ...Array(4).fill("%" + text(q, 80) + "%"),
61
+ Math.max(1, Math.min(100, Number(limit) || 40)),
62
+ Math.max(0, Number(offset) || 0),
63
+ );
64
+ const sources = db.prepare(
65
+ "SELECT id,title,kind,url FROM mind_time_sources WHERE id=?",
66
+ );
67
+ return rows.map((row) => {
68
+ const data = parse(row.data, {}),
69
+ provenance = parse(row.provenance, {});
70
+ const activity =
71
+ row.activity ||
72
+ row.project_kind ||
73
+ (row.kind === "reference-experience" ? "game" : "other");
74
+ const clean = (value) =>
75
+ activity === "game" ? gameText(value) : String(value || "").trim();
76
+ const title =
77
+ clean(row.work_title || row.task_title || row.project_title) ||
78
+ "这次活动的记录";
79
+ const summary = text(
80
+ clean(
81
+ row.summary ||
82
+ row.excerpt ||
83
+ (!generic.test(row.reason) ? row.reason : ""),
84
+ ),
85
+ 260,
86
+ );
87
+ const sourceIds = [
88
+ ...new Set([
89
+ ...(Array.isArray(data.sourceIds) ? data.sourceIds : []),
90
+ ...(Array.isArray(provenance.sourceIds) ? provenance.sourceIds : []),
91
+ ]),
92
+ ];
93
+ return {
94
+ id: row.id,
95
+ created: row.created,
96
+ task_id: row.task_id,
97
+ kind: row.kind,
98
+ title,
99
+ summary,
100
+ activity,
101
+ label: labels[activity] || activityLabel(activity),
102
+ project: row.project_id
103
+ ? { id: row.project_id, title: clean(row.project_title) }
104
+ : null,
105
+ work: row.version
106
+ ? {
107
+ id: row.work_id,
108
+ version: row.version,
109
+ characters: row.characters,
110
+ state:
111
+ row.completed ||
112
+ row.kind === "reference-experience" ||
113
+ row.saved_reason === "保存完成稿"
114
+ ? "complete"
115
+ : "draft",
116
+ }
117
+ : null,
118
+ materials: sourceIds.map((id) => sources.get(id)).filter(Boolean),
119
+ // Compatibility for older clients; never substitute a future manuscript.
120
+ reason: row.reason,
121
+ data: {
122
+ ...data,
123
+ workId: row.version ? row.work_id : null,
124
+ version: row.version,
125
+ },
126
+ };
127
+ });
128
+ }