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,739 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Attention } from "./attention.js";
3
+ import { Tasks, ACTIVITY_LABELS } from "./tasks.js";
4
+ import { activityLabel, holdsClock } from "./kinds.js";
5
+ import { dayKey, evidence, parse, text, zonedTime } from "../util.js";
6
+ import { Works } from "./works.js";
7
+ import { Sharing } from "./sharing.js";
8
+ import { evidenceRoots } from "../evidence.js";
9
+ import { Search } from "./search.js";
10
+ import { Games } from "./games.js";
11
+ import { ActivityClock } from "./activity-clock.js";
12
+ import { activityPresentation, gameText } from "./presentation.js";
13
+ import { Agenda } from "./agenda.js";
14
+ import { localClock } from "../../core/conversation-cues.js";
15
+
16
+ export const TIME_DEFAULTS = {
17
+ focusMinutes: 25,
18
+ breakMinutes: 5,
19
+ stepMinutes: 5,
20
+ paceSpeed: 1.25,
21
+ ownPlanMinutes: 45,
22
+ commitmentReviewMinutes: 30,
23
+ };
24
+ export class TimeSystem {
25
+ constructor(mind) {
26
+ this.mind = mind;
27
+ this.db = mind.db;
28
+ this.now = Date.now;
29
+ this.clock = new ActivityClock(this);
30
+ this.attention = new Attention();
31
+ this.tasks = new Tasks(this);
32
+ this.works = new Works(this);
33
+ this.sharing = new Sharing(this);
34
+ this.search = new Search(this);
35
+ this.games = new Games(this);
36
+ this.agenda = new Agenda(this);
37
+ this.tasks.delivery.reconcile();
38
+ // A stopped process cannot keep reading, creating or playing in the gap.
39
+ this.db.exec(
40
+ "UPDATE mind_time_spans SET ended=updated WHERE ended IS NULL; UPDATE mind_time_tasks SET state='paused',lease=NULL,lease_at=NULL,wait_reason='实例中断,进度已保留',checkpoint=CASE WHEN json_extract(checkpoint,'$.schedule.chosenAt') IS NOT NULL THEN json_set(checkpoint,'$.reschedule',json('true')) ELSE checkpoint END,revision=revision+1 WHERE state='doing';",
41
+ );
42
+ }
43
+ settings() {
44
+ return { ...TIME_DEFAULTS, ...this.mind.repo.config("time-system", {}) };
45
+ }
46
+ save(input) {
47
+ const value = { ...this.settings(), ...input };
48
+ for (const key of [
49
+ "focusMinutes",
50
+ "breakMinutes",
51
+ "stepMinutes",
52
+ "ownPlanMinutes",
53
+ "commitmentReviewMinutes",
54
+ ])
55
+ if (!Number.isInteger(value[key]) || value[key] < 1 || value[key] > 120)
56
+ throw Error("时间设置超出范围");
57
+ if (
58
+ !Number.isFinite(value.paceSpeed) ||
59
+ value.paceSpeed < 1 ||
60
+ value.paceSpeed > 2
61
+ )
62
+ throw Error("活动速度应在 1 至 2 之间");
63
+ this.mind.repo.saveConfig(
64
+ "time-system",
65
+ Object.fromEntries(
66
+ Object.keys(TIME_DEFAULTS).map((key) => [key, value[key]]),
67
+ ),
68
+ );
69
+ return this.settings();
70
+ }
71
+ event(task, kind, reason = "", data = {}, now = this.now()) {
72
+ const id = this.db
73
+ .prepare(
74
+ "INSERT INTO mind_time_events(created,task_id,kind,reason,data) VALUES (?,?,?,?,?)",
75
+ )
76
+ .run(
77
+ now,
78
+ task,
79
+ kind,
80
+ text(reason, 300),
81
+ JSON.stringify(data),
82
+ ).lastInsertRowid;
83
+ this.mind.store.revision++;
84
+ return Number(id);
85
+ }
86
+ primary() {
87
+ return this.tasks.get(
88
+ this.db
89
+ .prepare("SELECT id FROM mind_time_tasks WHERE state='doing' LIMIT 1")
90
+ .get()?.id || "",
91
+ );
92
+ }
93
+ focusMs(task) {
94
+ const chosen =
95
+ task.checkpoint.schedule?.chosenAt &&
96
+ task.checkpoint.schedule.durationMinutes;
97
+ return (
98
+ (Number.isInteger(chosen) && chosen >= 5 && chosen <= 240
99
+ ? chosen
100
+ : this.settings().focusMinutes) * 60000
101
+ );
102
+ }
103
+ planView(task) {
104
+ const slot = task.checkpoint.schedule;
105
+ return slot?.chosenAt && !task.checkpoint.reschedule
106
+ ? {
107
+ localStart: localClock(slot.proposedAt, this.mind.timeZone()).local,
108
+ expectedMinutes: slot.durationMinutes,
109
+ reason: slot.reason,
110
+ chosenBy: slot.chosenBy,
111
+ }
112
+ : null;
113
+ }
114
+ tick(now = this.now()) {
115
+ const task = this.primary();
116
+ if (!task) return;
117
+ const span = this.db
118
+ .prepare(
119
+ "SELECT * FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
120
+ )
121
+ .get(task.id);
122
+ if (!span) return;
123
+ if (this.mind.affect.state(now).phase === "asleep") {
124
+ this.tasks.control(
125
+ task.id,
126
+ {
127
+ action: "pause",
128
+ readyAt: now + 3600000,
129
+ reason: "睡眠期间不推进自己的活动",
130
+ },
131
+ span.updated,
132
+ );
133
+ return;
134
+ }
135
+ if (now < span.updated) return;
136
+ if (now - span.updated > 90000) {
137
+ this.tasks.control(
138
+ task.id,
139
+ { action: "pause", reason: "运行间隔中断,未补算停机时间" },
140
+ span.updated,
141
+ );
142
+ return;
143
+ }
144
+ const credit = this.clock.credit(task, now - span.updated);
145
+ this.db.exec("SAVEPOINT activity_tick");
146
+ try {
147
+ this.db
148
+ .prepare(
149
+ "UPDATE mind_time_spans SET active_ms=active_ms+?,engaged_ms=engaged_ms+?,clocked=?,updated=? WHERE id=?",
150
+ )
151
+ .run(
152
+ now - span.updated,
153
+ credit,
154
+ task.checkpoint.activityClock ? 1 : span.clocked,
155
+ now,
156
+ span.id,
157
+ );
158
+ this.clock.record(span, credit);
159
+ } catch (error) {
160
+ this.db.exec("ROLLBACK TO activity_tick");
161
+ throw error;
162
+ } finally {
163
+ this.db.exec("RELEASE activity_tick");
164
+ }
165
+ const recorded = this.db
166
+ .prepare("SELECT engaged_ms FROM mind_time_spans WHERE id=?")
167
+ .get(span.id);
168
+ if (
169
+ !task.lease &&
170
+ task.checkpoint.activityClock?.phase === "engaged" &&
171
+ recorded.engaged_ms >= this.focusMs(task) &&
172
+ this.clock.remaining(task, now) > 0
173
+ )
174
+ this.tasks.control(
175
+ task.id,
176
+ {
177
+ action: "pause",
178
+ readyAt: now + this.settings().breakMinutes * 60000,
179
+ reason: "专注段结束,歇一会再接着做",
180
+ },
181
+ now,
182
+ );
183
+ }
184
+ stopSpan(task, now = this.now()) {
185
+ const span = this.db
186
+ .prepare(
187
+ "SELECT * FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
188
+ )
189
+ .get(task.id);
190
+ if (!span) return;
191
+ const delta = Math.max(0, Math.min(60000, now - span.updated));
192
+ const credit = this.clock.credit(this.tasks.get(task.id), delta);
193
+ this.db.exec("SAVEPOINT stop_activity_span");
194
+ try {
195
+ this.db
196
+ .prepare(
197
+ "UPDATE mind_time_spans SET active_ms=active_ms+?,engaged_ms=engaged_ms+?,clocked=?,updated=?,ended=? WHERE id=?",
198
+ )
199
+ .run(
200
+ delta,
201
+ credit,
202
+ task.checkpoint.activityClock ? 1 : span.clocked,
203
+ span.updated + delta,
204
+ span.updated + delta,
205
+ span.id,
206
+ );
207
+ this.clock.record(span, credit);
208
+ } catch (error) {
209
+ this.db.exec("ROLLBACK TO stop_activity_span");
210
+ throw error;
211
+ } finally {
212
+ this.db.exec("RELEASE stop_activity_span");
213
+ }
214
+ }
215
+ start(task, now = this.now()) {
216
+ if (this.primary() && this.primary().id !== task.id) return null;
217
+ if (this.primary()) this.tick(now);
218
+ const refreshed = this.tasks.get(task.id);
219
+ if (
220
+ !refreshed ||
221
+ ["done", "abandoned"].includes(refreshed.state) ||
222
+ refreshed.lease
223
+ )
224
+ return null;
225
+ if (refreshed.state === "paused" && refreshed.ready_at > now) return null;
226
+ if (
227
+ !this.fixtureImmediate &&
228
+ !refreshed.checkpoint.pendingStep &&
229
+ !(
230
+ holdsClock(refreshed.activity) && refreshed.checkpoint.sourceIds?.length
231
+ )
232
+ ) {
233
+ this.db.prepare("UPDATE mind_time_tasks SET checkpoint=? WHERE id=?").run(
234
+ JSON.stringify({
235
+ ...refreshed.checkpoint,
236
+ activityClock: {
237
+ ...(refreshed.checkpoint.activityClock || {}),
238
+ version: 1,
239
+ phase: "preparing",
240
+ baselineMs: this.clock.committed(task.id),
241
+ plannedMs: 0,
242
+ },
243
+ }),
244
+ task.id,
245
+ );
246
+ }
247
+ const lease = randomUUID();
248
+ const result = this.db
249
+ .prepare(
250
+ "UPDATE mind_time_tasks SET state='doing',updated=?,wait_reason='',lease=?,lease_at=?,revision=revision+1 WHERE id=? AND state NOT IN ('done','abandoned') AND lease IS NULL",
251
+ )
252
+ .run(now, lease, now, task.id);
253
+ if (!result.changes) return null;
254
+ if (
255
+ !this.db
256
+ .prepare(
257
+ "SELECT 1 FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
258
+ )
259
+ .get(task.id)
260
+ )
261
+ this.db
262
+ .prepare(
263
+ "INSERT INTO mind_time_spans(id,task_id,started,updated) VALUES (?,?,?,?)",
264
+ )
265
+ .run(randomUUID(), task.id, now, now);
266
+ if (refreshed.state !== "doing")
267
+ this.event(task.id, "doing", "开始接着做", {}, now);
268
+ return this.tasks.get(task.id);
269
+ }
270
+ valid(task) {
271
+ const row = this.tasks.get(task.id);
272
+ return (
273
+ row?.state === "doing" &&
274
+ row.lease === task.lease &&
275
+ row.revision === task.revision
276
+ );
277
+ }
278
+ elapsed(id, now = this.now()) {
279
+ const task = this.tasks.get(id);
280
+ if (task?.checkpoint.activityClock) return this.clock.elapsed(task, now);
281
+ if (task && !this.fixtureImmediate) return 0;
282
+ return this.operationElapsed(id, now);
283
+ }
284
+ operationElapsed(id, now = this.now()) {
285
+ const row = this.db
286
+ .prepare(
287
+ "SELECT COALESCE(SUM(active_ms),0) n FROM mind_time_spans WHERE task_id=?",
288
+ )
289
+ .get(id);
290
+ const live = this.db
291
+ .prepare(
292
+ "SELECT updated FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
293
+ )
294
+ .get(id);
295
+ return (
296
+ row.n + (live ? Math.max(0, Math.min(60000, now - live.updated)) : 0)
297
+ );
298
+ }
299
+ visible(row, session, now = this.now()) {
300
+ if (session === undefined) return true;
301
+ if (!this.allowed(row, now)) return false;
302
+ if (row.discretion !== "open" && row.session_id !== session) return false;
303
+ return this.mind.meetings.stays(
304
+ { ...row, sources: evidence(row.sources) },
305
+ session,
306
+ now,
307
+ );
308
+ }
309
+ allowed(row, now = this.now()) {
310
+ const refs = evidenceRoots(this.db, row.sources || [], now, {
311
+ includeDerived: true,
312
+ });
313
+ for (const ref of refs) {
314
+ const kind = { s: "self", g: "meeting", t: "thought", a: "anticipation" }[
315
+ ref[0]
316
+ ],
317
+ id = ref.slice(2);
318
+ if (
319
+ kind &&
320
+ this.db
321
+ .prepare(
322
+ "SELECT 1 FROM mind_revocations WHERE target_kind=? AND target_id=?",
323
+ )
324
+ .get(kind, id)
325
+ )
326
+ return false;
327
+ if (
328
+ ref[0] === "m" &&
329
+ this.db
330
+ .prepare("SELECT 1 FROM mind_unlived WHERE seq=?")
331
+ .get(Number(id))
332
+ )
333
+ return false;
334
+ if (
335
+ ref[0] === "a" &&
336
+ this.mind.anticipations.get(id)?.status === "revoked"
337
+ )
338
+ return false;
339
+ }
340
+ return true;
341
+ }
342
+ progress(task, work, result, now = this.now()) {
343
+ const focus =
344
+ (this.db
345
+ .prepare(
346
+ `SELECT ${task.checkpoint.activityClock ? "engaged_ms" : "active_ms"} active_ms,updated FROM mind_time_spans WHERE task_id=? AND ended IS NULL`,
347
+ )
348
+ .get(task.id)?.active_ms || 0) >= this.focusMs(task);
349
+ const state = focus ? "paused" : "doing",
350
+ next =
351
+ now +
352
+ (focus ? this.settings().breakMinutes : this.settings().stepMinutes) *
353
+ 60000;
354
+ if (focus) this.stopSpan(task, now);
355
+ this.db
356
+ .prepare(
357
+ "UPDATE mind_time_tasks SET state=?,updated=?,next_step=?,wait_reason=?,lease=NULL,lease_at=NULL,revision=revision+1 WHERE id=?",
358
+ )
359
+ .run(
360
+ state,
361
+ now,
362
+ next,
363
+ focus ? "专注段结束,歇一会再接着做" : "",
364
+ task.id,
365
+ );
366
+ this.event(
367
+ task.id,
368
+ "checkpoint",
369
+ result.next || "保存位置,下次接着做",
370
+ { workId: work.id, version: work.version },
371
+ now,
372
+ );
373
+ }
374
+ reconcile(response, session, now = this.now(), prior = null) {
375
+ const works = this.works
376
+ .fragments({ session, now, cue: response.bubbles })
377
+ .filter((w) => w.kind === "write");
378
+ return {
379
+ ...response,
380
+ bubbles: response.bubbles.map((line) => {
381
+ if (prior?.id && this.tasks.get(prior.id)?.state !== "doing")
382
+ line = line.replace(
383
+ /(?:我)?(?:现在)?正在(写|读|阅读|玩)/g,
384
+ "我刚才在$1",
385
+ );
386
+ if (
387
+ !/(?:我|那篇|小说|短篇|故事).{0,20}(?:写好了|写完了|已经完成|已经发给)/.test(
388
+ line,
389
+ )
390
+ )
391
+ return line;
392
+ const title = line.match(/《([^》]+)》/)?.[1],
393
+ primary = this.primary();
394
+ const related = title
395
+ ? works.filter((w) => w.title === title)
396
+ : primary?.activity === "write"
397
+ ? works.filter((w) => w.id === primary.work_id)
398
+ : works;
399
+ const completed = related.some((w) => w.state === "complete");
400
+ if (!completed)
401
+ line = line.replace(
402
+ /(?:已经)?(?:写好了|写完了|已经完成)/g,
403
+ related.length
404
+ ? "写下了一部分,还没完成"
405
+ : "还没完成,得真正动笔写下来",
406
+ );
407
+ if (
408
+ /已经发给/.test(line) &&
409
+ !this.sharing
410
+ .list()
411
+ .some(
412
+ (s) =>
413
+ s.session_id === session &&
414
+ s.state === "sent" &&
415
+ related.some(
416
+ (w) => w.id === s.work_id && w.version === s.version,
417
+ ),
418
+ )
419
+ )
420
+ line = line.replace(/已经发给(?:你|大家)?(?:了)?/g, "还没有完整交付");
421
+ return line;
422
+ }),
423
+ };
424
+ }
425
+ view({ session, now = this.now(), cue = [] } = {}) {
426
+ const primary = this.primary(),
427
+ task = primary?.created <= now ? primary : null;
428
+ let current =
429
+ task && this.visible(task, session, now)
430
+ ? {
431
+ id: task.id,
432
+ activity: task.activity,
433
+ label: activityLabel(
434
+ task.activity,
435
+ ACTIVITY_LABELS[task.activity] || "在做自己的事",
436
+ ),
437
+ title: activityPresentation(task).title,
438
+ why: activityPresentation(task).why,
439
+ state: task.state,
440
+ checkpoint: {
441
+ summary: text(task.checkpoint.summary, 300),
442
+ next: text(
443
+ task.activity === "game"
444
+ ? gameText(task.checkpoint.next)
445
+ : task.checkpoint.next,
446
+ 160,
447
+ ),
448
+ version: task.checkpoint.version,
449
+ stage: task.checkpoint.stage,
450
+ segments:
451
+ task.checkpoint.segments || task.checkpoint.segment || 0,
452
+ },
453
+ elapsedMs: this.elapsed(task.id, now),
454
+ sessionElapsedMs: Math.max(
455
+ 0,
456
+ this.elapsed(task.id, now) -
457
+ (task.checkpoint.schedule?.baselineMs || 0),
458
+ ),
459
+ activityKind: task.activity === "game" ? "gaming" : task.activity,
460
+ timing: this.clock.view(task, now),
461
+ schedule: task.checkpoint.schedule || null,
462
+ plannedFor: this.planView(task),
463
+ moment:
464
+ task.activity === "game" ? this.games.moment(task, now) : null,
465
+ }
466
+ : task
467
+ ? { activity: task.activity, label: "在做自己的事", state: "doing" }
468
+ : null;
469
+ const review = this.sharing.reviewing;
470
+ if (!current && review && this.visible(review.work, session, now))
471
+ current = {
472
+ id: "share:" + review.share.id,
473
+ activity: "report",
474
+ label: "整理想分享的话",
475
+ title: review.work.title,
476
+ why: "回看自己的成果,挑想聊的内容",
477
+ state: "doing",
478
+ checkpoint: {
479
+ next:
480
+ review.phase === "reading" ? "回看这一段" : "整理成几句自己的话",
481
+ },
482
+ elapsedMs: 0,
483
+ timing: { phase: "preparing", plannedMs: null },
484
+ };
485
+ const planner = this.search.life?.planner?.working;
486
+ const care = planner ? "plan" : this.search.life?.ownDay?.working;
487
+ if (!current && care)
488
+ current = {
489
+ id: "own-day",
490
+ activity: "think",
491
+ state: "doing",
492
+ label: care === "review" ? "回看说过的话" : "想自己的安排",
493
+ title:
494
+ care === "review" ? "整理有没有漏下的约定" : "想想接下来愿意做点什么",
495
+ elapsedMs: 0,
496
+ timing: { phase: "preparing", plannedMs: null },
497
+ checkpoint: {},
498
+ };
499
+ const pending = this.tasks
500
+ .list({ limit: 100 })
501
+ .filter(
502
+ (row) =>
503
+ row.created <= now &&
504
+ row.state !== "abandoned" &&
505
+ (row.state !== "done" ||
506
+ !["none", "sent", "reported"].includes(row.share_state)) &&
507
+ this.visible(row, session, now),
508
+ )
509
+ .slice(0, 3)
510
+ .map((row) => ({
511
+ id: row.id,
512
+ title: activityPresentation(row).title,
513
+ state: row.state,
514
+ why: activityPresentation(row).why,
515
+ wait: row.wait_reason,
516
+ priority: row.priority,
517
+ priorityLabel: row.priorityLabel,
518
+ kind: row.kind,
519
+ share: row.share_state,
520
+ plannedFor: this.planView(row),
521
+ }));
522
+ return {
523
+ current,
524
+ pending,
525
+ intentions: this.db
526
+ .prepare(
527
+ "SELECT id FROM mind_time_tasks WHERE kind='plan' AND created<=? AND state NOT IN ('done','abandoned') AND json_extract(checkpoint,'$.mergedInto') IS NULL ORDER BY priority DESC,created LIMIT 100",
528
+ )
529
+ .all(now)
530
+ .map((row) => this.tasks.get(row.id))
531
+ .filter(
532
+ (row) =>
533
+ row.created <= now &&
534
+ row.kind === "plan" &&
535
+ !["done", "abandoned"].includes(row.state) &&
536
+ this.visible(row, session, now),
537
+ )
538
+ .slice(0, 3)
539
+ .map((row) => ({
540
+ title: activityPresentation(row).title,
541
+ why: activityPresentation(row).why,
542
+ state: row.state,
543
+ earliestAt: row.ready_at,
544
+ wait: row.wait_reason,
545
+ plannedFor: this.planView(row),
546
+ })),
547
+ works: this.works.fragments({ session, now, cue }),
548
+ ...(() => {
549
+ const actions = this.db
550
+ .prepare(
551
+ "SELECT id,label,state,result,session_id,created FROM mind_actions WHERE created>? AND created<=? AND state IN ('done','failed','awaiting','uncertain') ORDER BY created DESC LIMIT 8",
552
+ )
553
+ .all(now - 86400000, now)
554
+ .filter(
555
+ (row) => !session || !row.session_id || row.session_id === session,
556
+ )
557
+ .slice(0, 3)
558
+ .map((row) => ({
559
+ ref: `e:${row.id}`,
560
+ label: row.label,
561
+ state: row.state,
562
+ result: text(row.result, 160),
563
+ }));
564
+ return actions.length ? { actions } : {};
565
+ })(),
566
+ };
567
+ }
568
+ interaction(session, sources, now = this.now()) {
569
+ const task = this.primary();
570
+ if (task)
571
+ this.event(
572
+ task.id,
573
+ "interaction",
574
+ "一边做自己的事,一边交流",
575
+ { session, sources },
576
+ now,
577
+ );
578
+ }
579
+ adjust(turn, now = this.now()) {
580
+ const task = this.primary();
581
+ if (
582
+ task &&
583
+ (turn.crisis?.clear || ["chat", "rest"].includes(turn.attention?.action))
584
+ )
585
+ this.tasks.control(
586
+ task.id,
587
+ {
588
+ action: "pause",
589
+ readyAt: now + this.settings().breakMinutes * 60000,
590
+ reason: turn.crisis?.clear
591
+ ? "先认真回应眼前的紧急情况"
592
+ : turn.attention.reason || "自己选择换一下注意力",
593
+ },
594
+ now,
595
+ );
596
+ }
597
+ complete(
598
+ task,
599
+ { workId = null, creation = null, sources = [] } = {},
600
+ now = this.now(),
601
+ ) {
602
+ if (!this.valid(task)) throw Error("任务已变化");
603
+ if (!workId && !creation) throw Error("完成需要实际成果");
604
+ const actualWork =
605
+ workId &&
606
+ this.db
607
+ .prepare(
608
+ "SELECT 1 FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id AND v.version=w.version WHERE w.id=? AND w.state='complete' AND length(v.content)>0 AND EXISTS(SELECT 1 FROM mind_time_tasks t WHERE t.id=? AND t.work_id=w.id)",
609
+ )
610
+ .get(workId, task.id);
611
+ const actualCreation =
612
+ creation &&
613
+ this.db
614
+ .prepare(
615
+ "SELECT 1 FROM mind_creations WHERE id=? AND plan_id IN (?,?) AND length(content)>0",
616
+ )
617
+ .get(creation, task.id, task.anticipation_id || task.id);
618
+ if (!actualWork && !actualCreation) throw Error("完成需要已提交的实际成果");
619
+ this.stopSpan(task, now);
620
+ const share =
621
+ (task.kind === "promise" || task.checkpoint.contract?.delivery) &&
622
+ task.subject
623
+ ? "waiting"
624
+ : "none";
625
+ this.db
626
+ .prepare(
627
+ "UPDATE mind_time_tasks SET state='done',completed=?,updated=?,work_id=?,share_state=?,lease=NULL,lease_at=NULL,revision=revision+1 WHERE id=?",
628
+ )
629
+ .run(now, now, workId, share, task.id);
630
+ this.event(
631
+ task.id,
632
+ "done",
633
+ "留下了实际成果",
634
+ { workId, creation, sources },
635
+ now,
636
+ );
637
+ if (
638
+ task.anticipation_id &&
639
+ share === "none" &&
640
+ !task.checkpoint.contract?.originalGoal
641
+ )
642
+ this.mind.anticipations.close(task.anticipation_id, {
643
+ status: "done",
644
+ note: "实际活动已完成",
645
+ sources,
646
+ time: now,
647
+ });
648
+ }
649
+ events({ limit = 40, offset = 0 } = {}) {
650
+ return this.db
651
+ .prepare(
652
+ "SELECT * FROM mind_time_events ORDER BY created DESC,id DESC LIMIT ? OFFSET ?",
653
+ )
654
+ .all(Math.max(1, Math.min(100, limit)), Math.max(0, offset))
655
+ .map((row) => ({ ...row, data: parse(row.data, {}) }));
656
+ }
657
+ today(now = this.now()) {
658
+ const start = zonedTime(
659
+ dayKey(now, this.mind.timeZone()),
660
+ this.mind.timeZone(),
661
+ );
662
+ const spans = this.db
663
+ .prepare(
664
+ "SELECT s.*,t.title,t.activity FROM mind_time_spans s JOIN mind_time_tasks t ON t.id=s.task_id WHERE s.started<=? AND COALESCE(s.ended,s.updated)>=? ORDER BY started",
665
+ )
666
+ .all(now, start)
667
+ .map((row) => ({
668
+ ...row,
669
+ title: row.activity === "game" ? gameText(row.title) : row.title,
670
+ todayMs: Math.min(
671
+ row.clocked
672
+ ? this.clock.today(row.id, start, now)
673
+ : this.fixtureImmediate
674
+ ? row.active_ms
675
+ : 0,
676
+ Math.max(
677
+ 0,
678
+ Math.min(row.updated, now) - Math.max(row.started, start),
679
+ ),
680
+ ),
681
+ }));
682
+ const interactions = this.db
683
+ .prepare(
684
+ "SELECT * FROM mind_time_events WHERE kind='interaction' AND created>=? AND created<=? ORDER BY created DESC LIMIT 100",
685
+ )
686
+ .all(start, now)
687
+ .map((row) => ({ ...row, data: parse(row.data, {}) }));
688
+ return {
689
+ now,
690
+ start,
691
+ spans,
692
+ interactions,
693
+ activeMs: spans.reduce((n, s) => n + s.todayMs, 0),
694
+ };
695
+ }
696
+ lived({ since = 0, before = this.now(), session, limit = 6 } = {}) {
697
+ const tasks = this.db
698
+ .prepare(
699
+ "SELECT t.id FROM mind_time_tasks t WHERE t.created<=? AND EXISTS(SELECT 1 FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id WHERE w.task_id=t.id AND v.created>=? AND v.created<=?) ORDER BY t.updated DESC LIMIT 60",
700
+ )
701
+ .all(before, since, before)
702
+ .map((row) => this.tasks.get(row.id));
703
+ return tasks
704
+ .filter((task) => this.visible(task, session, before))
705
+ .slice(0, limit)
706
+ .map((task) => {
707
+ const event = this.db
708
+ .prepare(
709
+ "SELECT kind,data FROM mind_time_events WHERE task_id=? AND created<=? AND kind IN ('doing','paused','waiting','done','abandoned','checkpoint') ORDER BY id DESC LIMIT 1",
710
+ )
711
+ .get(task.id, before);
712
+ const work = this.db
713
+ .prepare(
714
+ "SELECT w.id,v.title,length(v.content) characters FROM mind_time_works w JOIN mind_time_versions v ON v.work_id=w.id WHERE w.task_id=? AND v.created<=? ORDER BY v.created DESC,v.version DESC LIMIT 1",
715
+ )
716
+ .get(task.id, before);
717
+ const state =
718
+ event?.kind === "checkpoint" ? "doing" : event?.kind || "doing";
719
+ return {
720
+ ref: `x:${task.id}`,
721
+ title: activityPresentation(task).title,
722
+ why: activityPresentation(task).why,
723
+ activity: task.activity,
724
+ state,
725
+ share:
726
+ task.completed && task.completed <= before
727
+ ? task.share_state
728
+ : "none",
729
+ activityKind: task.activity === "game" ? "gaming" : task.activity,
730
+ progress:
731
+ task.activity === "game"
732
+ ? `玩过第 ${task.checkpoint.segment || 0} 段,留下自己的游玩记录`
733
+ : `${work?.title || ""},已保存 ${work?.characters || 0} 字${state === "done" ? "完成稿" : "正文"}`,
734
+ project: task.project_id,
735
+ work: work?.id || null,
736
+ };
737
+ });
738
+ }
739
+ }