luckytri 1.0.0 → 1.0.2

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 (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -1,587 +1,587 @@
1
- import { randomUUID } from "node:crypto";
2
- import { evidenceRoots } from "../evidence.js";
3
- import { evidence, parse, similar, text } from "../util.js";
4
- import { hasCredential } from "../guard.js";
5
- import { taskIntent, intentRecipient, intentMessageSources } from "./intent.js";
6
- import { TaskLinks } from "./task-links.js";
7
- import { deadlineAt, taskSchedule, spokenDate } from "./schedule.js";
8
- import { DeliveryLinks } from "./delivery-links.js";
9
- import { activityPresentation } from "./presentation.js";
10
- import { activityLabel, classifyActivity, isExecutable } from "./kinds.js";
11
-
12
- export const TASK_STATES = [
13
- "todo",
14
- "scheduled",
15
- "doing",
16
- "paused",
17
- "waiting",
18
- "done",
19
- "abandoned",
20
- ];
21
- export const PRIORITIES = { 0: "低", 1: "普通", 2: "高", 3: "最高" };
22
- export const ACTIVITY_LABELS = {
23
- write: "写作",
24
- read: "阅读",
25
- think: "独处思考",
26
- game: "正在玩",
27
- unknown: "等待澄清",
28
- };
29
- export { classifyActivity, activityLabel, isExecutable };
30
- const decode = (row) =>
31
- row
32
- ? {
33
- ...row,
34
- sources: parse(row.sources, []),
35
- checkpoint: parse(row.checkpoint, {}),
36
- }
37
- : null;
38
-
39
- export class Tasks {
40
- constructor(time) {
41
- this.time = time;
42
- this.db = time.db;
43
- this.links = new TaskLinks(this);
44
- this.delivery = new DeliveryLinks(this);
45
- }
46
- get(id) {
47
- return decode(
48
- this.db.prepare("SELECT * FROM mind_time_tasks WHERE id=?").get(id),
49
- );
50
- }
51
- list({ state = "", limit = 40, offset = 0 } = {}) {
52
- const rows = this.db
53
- .prepare(
54
- `SELECT * FROM mind_time_tasks WHERE ${state ? "state=?" : "json_extract(checkpoint,'$.mergedInto') IS NULL"} ORDER BY CASE state WHEN 'doing' THEN 0 WHEN 'done' THEN 2 WHEN 'abandoned' THEN 3 ELSE 1 END,priority DESC,created LIMIT ? OFFSET ?`,
55
- )
56
- .all(
57
- ...(state ? [state] : []),
58
- Math.max(1, Math.min(100, limit)),
59
- Math.max(0, offset),
60
- );
61
- return rows.map(decode).map((row) => this.present(row));
62
- }
63
- present(row) {
64
- if (!row) return null;
65
- const checkpoint =
66
- row.activity === "game" && !row.checkpoint.reschedule
67
- ? this.time.games.contactWindow(row, row.checkpoint, this.time.now())
68
- : row.checkpoint;
69
- const display = { ...row, checkpoint };
70
- return {
71
- ...activityPresentation(display),
72
- priorityLabel: PRIORITIES[row.priority],
73
- schedule: taskSchedule(row, this.time),
74
- timing: this.time.clock.view(display),
75
- };
76
- }
77
- add(
78
- {
79
- kind = "plan",
80
- activity,
81
- title,
82
- why = "",
83
- sources = [],
84
- session = null,
85
- subject = null,
86
- discretion = "open",
87
- readyAt,
88
- dueAt = null,
89
- anticipationId = null,
90
- projectId = null,
91
- dependsOn = null,
92
- activeOnly = false,
93
- duePrecision = "time",
94
- priority,
95
- origin = "plan",
96
- },
97
- now = this.time.now(),
98
- ) {
99
- title = text(title, 240);
100
- if (!title || hasCredential(title + why))
101
- return { rejected: "任务内容无效" };
102
- const refs = evidence(sources),
103
- roots = new Set(evidenceRoots(this.db, refs, now));
104
- if (!refs.length) return { rejected: "缺少真实来源" };
105
- const privateRoots = this.time.mind.meetings.privateRoots(refs, now);
106
- session = privateRoots[0] || session;
107
- if (privateRoots.length && discretion !== "secret") discretion = "private";
108
- const type =
109
- activity && (ACTIVITY_LABELS[activity] || isExecutable(activity))
110
- ? activity
111
- : classifyActivity(title);
112
- subject =
113
- intentRecipient(this.db, { session, subject, title, sources: refs }) ||
114
- null;
115
- const intent = taskIntent(this.db, {
116
- title,
117
- activity: type,
118
- session,
119
- subject,
120
- sources: refs,
121
- projectId,
122
- });
123
- const prior = this.db
124
- .prepare(
125
- `SELECT * FROM mind_time_tasks WHERE json_extract(checkpoint,'$.mergedInto') IS NULL ${activeOnly ? "AND state NOT IN ('done','abandoned')" : ""} ${intent.key ? "AND intent_key=?" : ""} ORDER BY CASE state WHEN 'doing' THEN 0 WHEN 'done' THEN 1 WHEN 'abandoned' THEN 3 ELSE 2 END,created`,
126
- )
127
- .all(...(intent.key ? [intent.key] : []))
128
- .find(
129
- (row) =>
130
- (anticipationId && row.anticipation_id === anticipationId) ||
131
- (intent.key &&
132
- row.intent_key === intent.key &&
133
- (!projectId || !row.project_id || projectId === row.project_id)) ||
134
- (row.activity === type &&
135
- taskIntent(this.db, { ...row, sources: parse(row.sources, []) })
136
- .scope === intent.scope &&
137
- String(row.subject || "") === String(subject || "") &&
138
- !(intent.key && row.intent_key && intent.key !== row.intent_key) &&
139
- similar(row.title, title, 0.6) &&
140
- evidenceRoots(this.db, parse(row.sources, []), now).some((ref) =>
141
- roots.has(ref),
142
- )),
143
- );
144
- if (prior) {
145
- const existing = this.get(prior.id);
146
- this.links.mergeSource(
147
- existing,
148
- {
149
- sources: refs,
150
- kind,
151
- subject,
152
- dueAt,
153
- duePrecision,
154
- anticipationId,
155
- origin,
156
- },
157
- now,
158
- );
159
- if (existing.state === "abandoned") {
160
- this.links.close(
161
- this.get(existing.id),
162
- "事项已放下,不因重新整理来源而恢复",
163
- now,
164
- );
165
- return { suppressed: existing.id, duplicate: existing.id };
166
- }
167
- return { duplicate: existing.id };
168
- }
169
- const id = randomUUID();
170
- priority ??= kind === "promise" ? 2 : 1;
171
- if (!Number.isInteger(priority) || priority < 0 || priority > 3)
172
- throw Error("优先级无效");
173
- this.db
174
- .prepare(
175
- "INSERT INTO mind_time_tasks(id,created,updated,kind,activity,title,why,ready_at,due_at,session_id,subject,discretion,sources,anticipation_id,project_id,depends_on,state,wait_reason,intent_key,due_precision,priority) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
176
- )
177
- .run(
178
- id,
179
- now,
180
- now,
181
- kind,
182
- type,
183
- title,
184
- text(why, 300),
185
- Number.isFinite(readyAt) ? readyAt : now,
186
- deadlineAt(dueAt, duePrecision, this.time.mind.timeZone()),
187
- session,
188
- subject ? String(subject) : null,
189
- discretion,
190
- JSON.stringify(refs),
191
- anticipationId,
192
- projectId,
193
- dependsOn,
194
- type === "unknown" ? "waiting" : "todo",
195
- type === "unknown" ? "需要明确可以执行的内容" : "",
196
- intent.key,
197
- duePrecision,
198
- priority,
199
- );
200
- if (anticipationId)
201
- this.links.link("anticipation", anticipationId, id, now);
202
- for (const ref of refs) this.links.link("source", id + ":" + ref, id, now);
203
- this.time.event(
204
- id,
205
- "created",
206
- why || "留下一个可以接着做的安排",
207
- { sources: refs },
208
- now,
209
- );
210
- return { id };
211
- }
212
- sync(now = this.time.now()) {
213
- for (const a of this.db
214
- .prepare(
215
- "SELECT * FROM mind_anticipations WHERE kind IN ('plan','promise') AND created<=? ORDER BY created",
216
- )
217
- .all(now)) {
218
- const link = this.links.byAnticipation(a.id),
219
- old = link ? this.get(this.links.canonical(link.id)) : null;
220
- if (old) {
221
- if (
222
- a.status === "revoked" &&
223
- !["done", "abandoned"].includes(old.state)
224
- )
225
- this.control(
226
- old.id,
227
- { action: "abandon", reason: "原约定已撤销" },
228
- now,
229
- );
230
- if (old.state === "abandoned")
231
- this.links.close(old, "事项已放下,不自动重建待办", now);
232
- continue;
233
- }
234
- if (a.status !== "pending") continue;
235
- const added = this.add(
236
- {
237
- kind: a.kind,
238
- activity: a.activity,
239
- title: a.content,
240
- sources: parse(a.sources, []),
241
- session: a.session_id,
242
- subject: a.subject,
243
- discretion: a.discretion,
244
- dueAt: a.due_at,
245
- duePrecision: a.due_precision,
246
- origin: "sync",
247
- anticipationId: a.id,
248
- why: "从自己留下的约定继续做",
249
- },
250
- now,
251
- );
252
- if (added.duplicate)
253
- this.db
254
- .prepare(
255
- "UPDATE mind_time_tasks SET anticipation_id=COALESCE(anticipation_id,?) WHERE id=?",
256
- )
257
- .run(a.id, added.duplicate);
258
- }
259
- }
260
- capture(trace, turn, now = this.time.now()) {
261
- const rows = this.db
262
- .prepare(
263
- "SELECT seq,time,session_id,payload FROM core_events WHERE role='assistant' AND json_extract(payload,'$.traceId')=? ORDER BY seq",
264
- )
265
- .all(trace.id);
266
- const ids = [];
267
- for (const row of rows) {
268
- const m = parse(row.payload, {}),
269
- words = String(m.text || "");
270
- if (
271
- m.simulated ||
272
- m.artifact ||
273
- row.time > now ||
274
- /(?:不|没|别)(?:会|想|打算|准备)|开玩笑/.test(words)
275
- )
276
- continue;
277
- if (this.delivery.resolve(row, m, turn, now)) continue;
278
- if (/(?:小说里|故事里|角色台词|模拟对话|假如我是|假设我是)/.test(words))
279
- continue;
280
- if (
281
- /(?:已经|刚才|刚|昨天).{0,20}(?:写(?:了|完)|读(?:了|完)|玩(?:了|过)|做完)/.test(
282
- words,
283
- ) &&
284
- !/(?:明天|明晚|回头|等我|我会|我打算)/.test(words)
285
- )
286
- continue;
287
- if (
288
- !/(?:我(?:会|要|想|打算|准备|答应)|给你|帮你|等我|回头|明天|明晚|今晚).{0,45}(?:写|读|玩|看|整理|想一想|发给|送给|做)/.test(
289
- words,
290
- )
291
- )
292
- continue;
293
- const activity = classifyActivity(words);
294
- const promise = /给你|发给|答应|帮你|让你看/.test(words);
295
- const { dueAt, duePrecision } = spokenDate(
296
- words,
297
- row.time,
298
- this.time.mind.timeZone(),
299
- );
300
- const result = this.add(
301
- {
302
- kind: promise ? "promise" : "plan",
303
- activity,
304
- title: words,
305
- why: "这是我实际说出口的打算",
306
- sources: intentMessageSources(this.db, row, m),
307
- session: row.session_id,
308
- subject: promise ? turn.targetUserIds?.[0] : null,
309
- dueAt,
310
- duePrecision,
311
- origin: "capture",
312
- },
313
- now,
314
- );
315
- if (result.id) {
316
- ids.push(result.id);
317
- if (/(?:如果|等到|等你).{0,35}(?:资料|题目|要求|确认)/.test(words))
318
- this.wait(
319
- this.get(result.id),
320
- "等待约定的条件澄清",
321
- Number.MAX_SAFE_INTEGER,
322
- now,
323
- );
324
- }
325
- }
326
- return ids;
327
- }
328
- ready(now = this.time.now()) {
329
- return this.db
330
- .prepare(
331
- "SELECT * FROM mind_time_tasks WHERE state IN ('todo','scheduled','waiting','paused') AND ready_at<=? AND next_step<=? AND activity!='unknown' AND json_extract(checkpoint,'$.mergedInto') IS NULL ORDER BY priority DESC,created LIMIT 100",
332
- )
333
- .all(now, now)
334
- .map(decode)
335
- .filter(
336
- (task) =>
337
- !task.depends_on || this.get(task.depends_on)?.state === "done",
338
- )
339
- .sort((a, b) => this.score(b, now) - this.score(a, now));
340
- }
341
- score(task, now) {
342
- const age = Math.max(0, Math.min(20, (now - task.created) / 3600000));
343
- const recent = this.db
344
- .prepare(
345
- "SELECT t.kind FROM mind_time_events e JOIN mind_time_tasks t ON t.id=e.task_id WHERE e.kind='done' ORDER BY e.created DESC LIMIT 2",
346
- )
347
- .all();
348
- const own =
349
- task.kind === "plan"
350
- ? 20 +
351
- (recent.length === 2 && recent.every((t) => t.kind === "promise")
352
- ? 65
353
- : 0)
354
- : task.kind === "suggestion"
355
- ? 10
356
- : 30;
357
- const person = task.subject
358
- ? this.time.mind.bonds.person(task.subject, now)
359
- : null;
360
- return (
361
- own +
362
- age +
363
- (task.kind === "plan" &&
364
- task.priority === 1 &&
365
- recent.length === 2 &&
366
- recent.every((t) => t.kind === "promise")
367
- ? 2
368
- : task.priority) *
369
- 100 +
370
- Math.min(8, Number(person?.closeness || 0) * 8)
371
- );
372
- }
373
- arrange(
374
- id,
375
- { startAt, durationMinutes, reason, activity, title, topic, projectId },
376
- now = this.time.now(),
377
- ) {
378
- const task = this.get(id);
379
- if (
380
- !task ||
381
- ["doing", "done", "abandoned"].includes(task.state) ||
382
- task.lease
383
- )
384
- throw Error("事项已变化,不能覆盖安排");
385
- if (
386
- !Number.isFinite(startAt) ||
387
- startAt < now - 60000 ||
388
- !Number.isInteger(durationMinutes) ||
389
- durationMinutes < 5 ||
390
- durationMinutes > 240
391
- )
392
- throw Error("时间安排无效");
393
- startAt = Math.max(now, startAt);
394
- const conflicts = this.slots(now).filter(
395
- (slot) =>
396
- slot.id !== id &&
397
- slot.startAt < startAt + durationMinutes * 60000 &&
398
- slot.endAt > startAt,
399
- );
400
- if (conflicts.some((slot) => slot.priority >= task.priority))
401
- throw Error("所选时间与已有安排重叠,需要重新选定");
402
- let checkpoint = {
403
- ...task.checkpoint,
404
- reschedule: false,
405
- schedule: {
406
- proposedAt: startAt,
407
- endedAt: startAt + durationMinutes * 60000,
408
- durationMinutes,
409
- chosenAt: now,
410
- chosenBy: "self",
411
- baselineMs: this.time.clock.committed(id),
412
- reason: text(reason, 240),
413
- focusMinutes: durationMinutes,
414
- outcome: task.checkpoint.schedule?.outcome || "",
415
- },
416
- ...(topic ? { topic: text(topic, 80) } : {}),
417
- };
418
- if (task.checkpoint.activityClock?.phase === "preparing")
419
- delete checkpoint.activityClock;
420
- if ((activity || task.activity) === "game")
421
- checkpoint = this.time.games.contactWindow(
422
- { ...task, checkpoint },
423
- checkpoint,
424
- now,
425
- );
426
- this.db.exec("SAVEPOINT arrange_task");
427
- try {
428
- const updated = this.db
429
- .prepare(
430
- "UPDATE mind_time_tasks SET title=?,activity=?,state=?,ready_at=?,next_step=?,wait_reason='',updated=?,checkpoint=?,project_id=COALESCE(?,project_id),revision=revision+1 WHERE id=? AND revision=? AND lease IS NULL",
431
- )
432
- .run(
433
- text(title, 240) || task.title,
434
- activity || task.activity,
435
- startAt > now ? "scheduled" : "todo",
436
- startAt,
437
- startAt,
438
- now,
439
- JSON.stringify(checkpoint),
440
- projectId || null,
441
- id,
442
- task.revision,
443
- );
444
- if (!updated.changes) throw Error("事项已变化");
445
- const current = this.get(id);
446
- if (topic && current.project_id) {
447
- const project = this.time.works.project(current.project_id);
448
- this.db
449
- .prepare("UPDATE mind_time_projects SET bible=?,updated=? WHERE id=?")
450
- .run(
451
- JSON.stringify({ ...project.bible, topic: text(topic, 80) }),
452
- now,
453
- project.id,
454
- );
455
- }
456
- this.time.event(
457
- id,
458
- "self-scheduled",
459
- reason || "自己选定开始时间与投入时长",
460
- {
461
- previous: task.checkpoint.schedule || null,
462
- schedule: checkpoint.schedule,
463
- previousTitle: task.title,
464
- },
465
- now,
466
- );
467
- return current;
468
- } catch (error) {
469
- this.db.exec("ROLLBACK TO arrange_task");
470
- throw error;
471
- } finally {
472
- this.db.exec("RELEASE arrange_task");
473
- }
474
- }
475
- slots(now = this.time.now()) {
476
- return this.db
477
- .prepare(
478
- "SELECT id,priority,state,checkpoint FROM mind_time_tasks WHERE state IN ('todo','scheduled','doing') AND json_extract(checkpoint,'$.reschedule') IS NOT 1 AND json_extract(checkpoint,'$.schedule.chosenAt') IS NOT NULL AND json_extract(checkpoint,'$.schedule.endedAt')>? ORDER BY ready_at LIMIT 30",
479
- )
480
- .all(now)
481
- .map((row) => {
482
- const slot = parse(row.checkpoint, {}).schedule;
483
- return {
484
- id: row.id,
485
- priority: row.priority,
486
- startAt: slot.proposedAt,
487
- endAt: slot.endedAt,
488
- ongoing: row.state === "doing",
489
- };
490
- });
491
- }
492
- control(
493
- id,
494
- { action, reason = "", readyAt, dueAt, priority, durationMinutes },
495
- now = this.time.now(),
496
- ) {
497
- const task = this.get(id);
498
- if (!task) throw Error("任务不存在");
499
- if (task.checkpoint.mergedInto)
500
- throw Error("这条记录已合并,请调整唯一任务");
501
- if (action === "priority") {
502
- if (!Number.isInteger(priority) || priority < 0 || priority > 3)
503
- throw Error("优先级无效");
504
- this.db
505
- .prepare("UPDATE mind_time_tasks SET priority=?,updated=? WHERE id=?")
506
- .run(priority, now, id);
507
- this.time.event(
508
- id,
509
- "priority",
510
- reason || "调整优先级",
511
- { previous: task.priority, priority },
512
- now,
513
- );
514
- return this.get(id);
515
- }
516
- if (["done", "abandoned"].includes(task.state)) throw Error("任务已经结束");
517
- const state = {
518
- pause: "paused",
519
- resume: "todo",
520
- schedule: "scheduled",
521
- abandon: "abandoned",
522
- wait: "waiting",
523
- }[action];
524
- if (!state) throw Error("不能直接标记完成");
525
- if (
526
- action === "schedule" &&
527
- durationMinutes !== undefined &&
528
- (!Number.isInteger(durationMinutes) ||
529
- durationMinutes < 5 ||
530
- durationMinutes > 240)
531
- )
532
- throw Error("投入时间应为 5 至 240 分钟");
533
- this.time.stopSpan(task, now);
534
- const ready = Number.isFinite(readyAt) ? readyAt : now;
535
- const next =
536
- state === "paused" && !Number.isFinite(readyAt)
537
- ? Number.MAX_SAFE_INTEGER
538
- : ready;
539
- const checkpoint = { ...task.checkpoint };
540
- if (state === "paused" && checkpoint.schedule?.chosenAt)
541
- checkpoint.reschedule = true;
542
- if (action === "schedule") {
543
- const minutes =
544
- durationMinutes ||
545
- checkpoint.schedule?.durationMinutes ||
546
- this.time.settings().focusMinutes;
547
- checkpoint.reschedule = false;
548
- checkpoint.schedule = {
549
- ...checkpoint.schedule,
550
- proposedAt: Math.max(now, ready),
551
- endedAt: Math.max(now, ready) + minutes * 60000,
552
- durationMinutes: minutes,
553
- focusMinutes: minutes,
554
- chosenAt: now,
555
- chosenBy: "admin",
556
- baselineMs: this.time.clock.committed(id),
557
- reason: text(reason, 240) || "管理台安排",
558
- };
559
- }
560
- this.db
561
- .prepare(
562
- "UPDATE mind_time_tasks SET state=?,updated=?,ready_at=?,next_step=?,due_at=?,wait_reason=?,checkpoint=?,revision=revision+1,lease=NULL,lease_at=NULL WHERE id=?",
563
- )
564
- .run(
565
- state,
566
- now,
567
- ready,
568
- next,
569
- dueAt === undefined ? task.due_at : dueAt,
570
- text(reason, 300),
571
- JSON.stringify(checkpoint),
572
- id,
573
- );
574
- this.time.event(id, state, reason, {}, now);
575
- if (state === "abandoned")
576
- this.links.close(task, reason || "这件事被放下了", now);
577
- return this.get(id);
578
- }
579
- wait(task, reason, next = 0, now = this.time.now()) {
580
- this.time.stopSpan(task, now);
581
- this.db
582
- .prepare(
583
- "UPDATE mind_time_tasks SET state='waiting',wait_reason=?,next_step=?,updated=?,lease=NULL,lease_at=NULL,revision=revision+1 WHERE id=?",
584
- )
585
- .run(text(reason, 300), next, now, task.id);
586
- }
587
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { evidenceRoots } from "../evidence.js";
3
+ import { evidence, parse, similar, text } from "../util.js";
4
+ import { hasCredential } from "../guard.js";
5
+ import { taskIntent, intentRecipient, intentMessageSources } from "./intent.js";
6
+ import { TaskLinks } from "./task-links.js";
7
+ import { deadlineAt, taskSchedule, spokenDate } from "./schedule.js";
8
+ import { DeliveryLinks } from "./delivery-links.js";
9
+ import { activityPresentation } from "./presentation.js";
10
+ import { activityLabel, classifyActivity, isExecutable } from "./kinds.js";
11
+
12
+ export const TASK_STATES = [
13
+ "todo",
14
+ "scheduled",
15
+ "doing",
16
+ "paused",
17
+ "waiting",
18
+ "done",
19
+ "abandoned",
20
+ ];
21
+ export const PRIORITIES = { 0: "低", 1: "普通", 2: "高", 3: "最高" };
22
+ export const ACTIVITY_LABELS = {
23
+ write: "写作",
24
+ read: "阅读",
25
+ think: "独处思考",
26
+ game: "正在玩",
27
+ unknown: "等待澄清",
28
+ };
29
+ export { classifyActivity, activityLabel, isExecutable };
30
+ const decode = (row) =>
31
+ row
32
+ ? {
33
+ ...row,
34
+ sources: parse(row.sources, []),
35
+ checkpoint: parse(row.checkpoint, {}),
36
+ }
37
+ : null;
38
+
39
+ export class Tasks {
40
+ constructor(time) {
41
+ this.time = time;
42
+ this.db = time.db;
43
+ this.links = new TaskLinks(this);
44
+ this.delivery = new DeliveryLinks(this);
45
+ }
46
+ get(id) {
47
+ return decode(
48
+ this.db.prepare("SELECT * FROM mind_time_tasks WHERE id=?").get(id),
49
+ );
50
+ }
51
+ list({ state = "", limit = 40, offset = 0 } = {}) {
52
+ const rows = this.db
53
+ .prepare(
54
+ `SELECT * FROM mind_time_tasks WHERE ${state ? "state=?" : "json_extract(checkpoint,'$.mergedInto') IS NULL"} ORDER BY CASE state WHEN 'doing' THEN 0 WHEN 'done' THEN 2 WHEN 'abandoned' THEN 3 ELSE 1 END,priority DESC,created LIMIT ? OFFSET ?`,
55
+ )
56
+ .all(
57
+ ...(state ? [state] : []),
58
+ Math.max(1, Math.min(100, limit)),
59
+ Math.max(0, offset),
60
+ );
61
+ return rows.map(decode).map((row) => this.present(row));
62
+ }
63
+ present(row) {
64
+ if (!row) return null;
65
+ const checkpoint =
66
+ row.activity === "game" && !row.checkpoint.reschedule
67
+ ? this.time.games.contactWindow(row, row.checkpoint, this.time.now())
68
+ : row.checkpoint;
69
+ const display = { ...row, checkpoint };
70
+ return {
71
+ ...activityPresentation(display),
72
+ priorityLabel: PRIORITIES[row.priority],
73
+ schedule: taskSchedule(row, this.time),
74
+ timing: this.time.clock.view(display),
75
+ };
76
+ }
77
+ add(
78
+ {
79
+ kind = "plan",
80
+ activity,
81
+ title,
82
+ why = "",
83
+ sources = [],
84
+ session = null,
85
+ subject = null,
86
+ discretion = "open",
87
+ readyAt,
88
+ dueAt = null,
89
+ anticipationId = null,
90
+ projectId = null,
91
+ dependsOn = null,
92
+ activeOnly = false,
93
+ duePrecision = "time",
94
+ priority,
95
+ origin = "plan",
96
+ },
97
+ now = this.time.now(),
98
+ ) {
99
+ title = text(title, 240);
100
+ if (!title || hasCredential(title + why))
101
+ return { rejected: "任务内容无效" };
102
+ const refs = evidence(sources),
103
+ roots = new Set(evidenceRoots(this.db, refs, now));
104
+ if (!refs.length) return { rejected: "缺少真实来源" };
105
+ const privateRoots = this.time.mind.meetings.privateRoots(refs, now);
106
+ session = privateRoots[0] || session;
107
+ if (privateRoots.length && discretion !== "secret") discretion = "private";
108
+ const type =
109
+ activity && (ACTIVITY_LABELS[activity] || isExecutable(activity))
110
+ ? activity
111
+ : classifyActivity(title);
112
+ subject =
113
+ intentRecipient(this.db, { session, subject, title, sources: refs }) ||
114
+ null;
115
+ const intent = taskIntent(this.db, {
116
+ title,
117
+ activity: type,
118
+ session,
119
+ subject,
120
+ sources: refs,
121
+ projectId,
122
+ });
123
+ const prior = this.db
124
+ .prepare(
125
+ `SELECT * FROM mind_time_tasks WHERE json_extract(checkpoint,'$.mergedInto') IS NULL ${activeOnly ? "AND state NOT IN ('done','abandoned')" : ""} ${intent.key ? "AND intent_key=?" : ""} ORDER BY CASE state WHEN 'doing' THEN 0 WHEN 'done' THEN 1 WHEN 'abandoned' THEN 3 ELSE 2 END,created`,
126
+ )
127
+ .all(...(intent.key ? [intent.key] : []))
128
+ .find(
129
+ (row) =>
130
+ (anticipationId && row.anticipation_id === anticipationId) ||
131
+ (intent.key &&
132
+ row.intent_key === intent.key &&
133
+ (!projectId || !row.project_id || projectId === row.project_id)) ||
134
+ (row.activity === type &&
135
+ taskIntent(this.db, { ...row, sources: parse(row.sources, []) })
136
+ .scope === intent.scope &&
137
+ String(row.subject || "") === String(subject || "") &&
138
+ !(intent.key && row.intent_key && intent.key !== row.intent_key) &&
139
+ similar(row.title, title, 0.6) &&
140
+ evidenceRoots(this.db, parse(row.sources, []), now).some((ref) =>
141
+ roots.has(ref),
142
+ )),
143
+ );
144
+ if (prior) {
145
+ const existing = this.get(prior.id);
146
+ this.links.mergeSource(
147
+ existing,
148
+ {
149
+ sources: refs,
150
+ kind,
151
+ subject,
152
+ dueAt,
153
+ duePrecision,
154
+ anticipationId,
155
+ origin,
156
+ },
157
+ now,
158
+ );
159
+ if (existing.state === "abandoned") {
160
+ this.links.close(
161
+ this.get(existing.id),
162
+ "事项已放下,不因重新整理来源而恢复",
163
+ now,
164
+ );
165
+ return { suppressed: existing.id, duplicate: existing.id };
166
+ }
167
+ return { duplicate: existing.id };
168
+ }
169
+ const id = randomUUID();
170
+ priority ??= kind === "promise" ? 2 : 1;
171
+ if (!Number.isInteger(priority) || priority < 0 || priority > 3)
172
+ throw Error("优先级无效");
173
+ this.db
174
+ .prepare(
175
+ "INSERT INTO mind_time_tasks(id,created,updated,kind,activity,title,why,ready_at,due_at,session_id,subject,discretion,sources,anticipation_id,project_id,depends_on,state,wait_reason,intent_key,due_precision,priority) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
176
+ )
177
+ .run(
178
+ id,
179
+ now,
180
+ now,
181
+ kind,
182
+ type,
183
+ title,
184
+ text(why, 300),
185
+ Number.isFinite(readyAt) ? readyAt : now,
186
+ deadlineAt(dueAt, duePrecision, this.time.mind.timeZone()),
187
+ session,
188
+ subject ? String(subject) : null,
189
+ discretion,
190
+ JSON.stringify(refs),
191
+ anticipationId,
192
+ projectId,
193
+ dependsOn,
194
+ type === "unknown" ? "waiting" : "todo",
195
+ type === "unknown" ? "需要明确可以执行的内容" : "",
196
+ intent.key,
197
+ duePrecision,
198
+ priority,
199
+ );
200
+ if (anticipationId)
201
+ this.links.link("anticipation", anticipationId, id, now);
202
+ for (const ref of refs) this.links.link("source", id + ":" + ref, id, now);
203
+ this.time.event(
204
+ id,
205
+ "created",
206
+ why || "留下一个可以接着做的安排",
207
+ { sources: refs },
208
+ now,
209
+ );
210
+ return { id };
211
+ }
212
+ sync(now = this.time.now()) {
213
+ for (const a of this.db
214
+ .prepare(
215
+ "SELECT * FROM mind_anticipations WHERE kind IN ('plan','promise') AND created<=? ORDER BY created",
216
+ )
217
+ .all(now)) {
218
+ const link = this.links.byAnticipation(a.id),
219
+ old = link ? this.get(this.links.canonical(link.id)) : null;
220
+ if (old) {
221
+ if (
222
+ a.status === "revoked" &&
223
+ !["done", "abandoned"].includes(old.state)
224
+ )
225
+ this.control(
226
+ old.id,
227
+ { action: "abandon", reason: "原约定已撤销" },
228
+ now,
229
+ );
230
+ if (old.state === "abandoned")
231
+ this.links.close(old, "事项已放下,不自动重建待办", now);
232
+ continue;
233
+ }
234
+ if (a.status !== "pending") continue;
235
+ const added = this.add(
236
+ {
237
+ kind: a.kind,
238
+ activity: a.activity,
239
+ title: a.content,
240
+ sources: parse(a.sources, []),
241
+ session: a.session_id,
242
+ subject: a.subject,
243
+ discretion: a.discretion,
244
+ dueAt: a.due_at,
245
+ duePrecision: a.due_precision,
246
+ origin: "sync",
247
+ anticipationId: a.id,
248
+ why: "从自己留下的约定继续做",
249
+ },
250
+ now,
251
+ );
252
+ if (added.duplicate)
253
+ this.db
254
+ .prepare(
255
+ "UPDATE mind_time_tasks SET anticipation_id=COALESCE(anticipation_id,?) WHERE id=?",
256
+ )
257
+ .run(a.id, added.duplicate);
258
+ }
259
+ }
260
+ capture(trace, turn, now = this.time.now()) {
261
+ const rows = this.db
262
+ .prepare(
263
+ "SELECT seq,time,session_id,payload FROM core_events WHERE role='assistant' AND json_extract(payload,'$.traceId')=? ORDER BY seq",
264
+ )
265
+ .all(trace.id);
266
+ const ids = [];
267
+ for (const row of rows) {
268
+ const m = parse(row.payload, {}),
269
+ words = String(m.text || "");
270
+ if (
271
+ m.simulated ||
272
+ m.artifact ||
273
+ row.time > now ||
274
+ /(?:不|没|别)(?:会|想|打算|准备)|开玩笑/.test(words)
275
+ )
276
+ continue;
277
+ if (this.delivery.resolve(row, m, turn, now)) continue;
278
+ if (/(?:小说里|故事里|角色台词|模拟对话|假如我是|假设我是)/.test(words))
279
+ continue;
280
+ if (
281
+ /(?:已经|刚才|刚|昨天).{0,20}(?:写(?:了|完)|读(?:了|完)|玩(?:了|过)|做完)/.test(
282
+ words,
283
+ ) &&
284
+ !/(?:明天|明晚|回头|等我|我会|我打算)/.test(words)
285
+ )
286
+ continue;
287
+ if (
288
+ !/(?:我(?:会|要|想|打算|准备|答应)|给你|帮你|等我|回头|明天|明晚|今晚).{0,45}(?:写|读|玩|看|整理|想一想|发给|送给|做)/.test(
289
+ words,
290
+ )
291
+ )
292
+ continue;
293
+ const activity = classifyActivity(words);
294
+ const promise = /给你|发给|答应|帮你|让你看/.test(words);
295
+ const { dueAt, duePrecision } = spokenDate(
296
+ words,
297
+ row.time,
298
+ this.time.mind.timeZone(),
299
+ );
300
+ const result = this.add(
301
+ {
302
+ kind: promise ? "promise" : "plan",
303
+ activity,
304
+ title: words,
305
+ why: "这是我实际说出口的打算",
306
+ sources: intentMessageSources(this.db, row, m),
307
+ session: row.session_id,
308
+ subject: promise ? turn.targetUserIds?.[0] : null,
309
+ dueAt,
310
+ duePrecision,
311
+ origin: "capture",
312
+ },
313
+ now,
314
+ );
315
+ if (result.id) {
316
+ ids.push(result.id);
317
+ if (/(?:如果|等到|等你).{0,35}(?:资料|题目|要求|确认)/.test(words))
318
+ this.wait(
319
+ this.get(result.id),
320
+ "等待约定的条件澄清",
321
+ Number.MAX_SAFE_INTEGER,
322
+ now,
323
+ );
324
+ }
325
+ }
326
+ return ids;
327
+ }
328
+ ready(now = this.time.now()) {
329
+ return this.db
330
+ .prepare(
331
+ "SELECT * FROM mind_time_tasks WHERE state IN ('todo','scheduled','waiting','paused') AND ready_at<=? AND next_step<=? AND activity!='unknown' AND json_extract(checkpoint,'$.mergedInto') IS NULL ORDER BY priority DESC,created LIMIT 100",
332
+ )
333
+ .all(now, now)
334
+ .map(decode)
335
+ .filter(
336
+ (task) =>
337
+ !task.depends_on || this.get(task.depends_on)?.state === "done",
338
+ )
339
+ .sort((a, b) => this.score(b, now) - this.score(a, now));
340
+ }
341
+ score(task, now) {
342
+ const age = Math.max(0, Math.min(20, (now - task.created) / 3600000));
343
+ const recent = this.db
344
+ .prepare(
345
+ "SELECT t.kind FROM mind_time_events e JOIN mind_time_tasks t ON t.id=e.task_id WHERE e.kind='done' ORDER BY e.created DESC LIMIT 2",
346
+ )
347
+ .all();
348
+ const own =
349
+ task.kind === "plan"
350
+ ? 20 +
351
+ (recent.length === 2 && recent.every((t) => t.kind === "promise")
352
+ ? 65
353
+ : 0)
354
+ : task.kind === "suggestion"
355
+ ? 10
356
+ : 30;
357
+ const person = task.subject
358
+ ? this.time.mind.bonds.person(task.subject, now)
359
+ : null;
360
+ return (
361
+ own +
362
+ age +
363
+ (task.kind === "plan" &&
364
+ task.priority === 1 &&
365
+ recent.length === 2 &&
366
+ recent.every((t) => t.kind === "promise")
367
+ ? 2
368
+ : task.priority) *
369
+ 100 +
370
+ Math.min(8, Number(person?.closeness || 0) * 8)
371
+ );
372
+ }
373
+ arrange(
374
+ id,
375
+ { startAt, durationMinutes, reason, activity, title, topic, projectId },
376
+ now = this.time.now(),
377
+ ) {
378
+ const task = this.get(id);
379
+ if (
380
+ !task ||
381
+ ["doing", "done", "abandoned"].includes(task.state) ||
382
+ task.lease
383
+ )
384
+ throw Error("事项已变化,不能覆盖安排");
385
+ if (
386
+ !Number.isFinite(startAt) ||
387
+ startAt < now - 60000 ||
388
+ !Number.isInteger(durationMinutes) ||
389
+ durationMinutes < 5 ||
390
+ durationMinutes > 240
391
+ )
392
+ throw Error("时间安排无效");
393
+ startAt = Math.max(now, startAt);
394
+ const conflicts = this.slots(now).filter(
395
+ (slot) =>
396
+ slot.id !== id &&
397
+ slot.startAt < startAt + durationMinutes * 60000 &&
398
+ slot.endAt > startAt,
399
+ );
400
+ if (conflicts.some((slot) => slot.priority >= task.priority))
401
+ throw Error("所选时间与已有安排重叠,需要重新选定");
402
+ let checkpoint = {
403
+ ...task.checkpoint,
404
+ reschedule: false,
405
+ schedule: {
406
+ proposedAt: startAt,
407
+ endedAt: startAt + durationMinutes * 60000,
408
+ durationMinutes,
409
+ chosenAt: now,
410
+ chosenBy: "self",
411
+ baselineMs: this.time.clock.committed(id),
412
+ reason: text(reason, 240),
413
+ focusMinutes: durationMinutes,
414
+ outcome: task.checkpoint.schedule?.outcome || "",
415
+ },
416
+ ...(topic ? { topic: text(topic, 80) } : {}),
417
+ };
418
+ if (task.checkpoint.activityClock?.phase === "preparing")
419
+ delete checkpoint.activityClock;
420
+ if ((activity || task.activity) === "game")
421
+ checkpoint = this.time.games.contactWindow(
422
+ { ...task, checkpoint },
423
+ checkpoint,
424
+ now,
425
+ );
426
+ this.db.exec("SAVEPOINT arrange_task");
427
+ try {
428
+ const updated = this.db
429
+ .prepare(
430
+ "UPDATE mind_time_tasks SET title=?,activity=?,state=?,ready_at=?,next_step=?,wait_reason='',updated=?,checkpoint=?,project_id=COALESCE(?,project_id),revision=revision+1 WHERE id=? AND revision=? AND lease IS NULL",
431
+ )
432
+ .run(
433
+ text(title, 240) || task.title,
434
+ activity || task.activity,
435
+ startAt > now ? "scheduled" : "todo",
436
+ startAt,
437
+ startAt,
438
+ now,
439
+ JSON.stringify(checkpoint),
440
+ projectId || null,
441
+ id,
442
+ task.revision,
443
+ );
444
+ if (!updated.changes) throw Error("事项已变化");
445
+ const current = this.get(id);
446
+ if (topic && current.project_id) {
447
+ const project = this.time.works.project(current.project_id);
448
+ this.db
449
+ .prepare("UPDATE mind_time_projects SET bible=?,updated=? WHERE id=?")
450
+ .run(
451
+ JSON.stringify({ ...project.bible, topic: text(topic, 80) }),
452
+ now,
453
+ project.id,
454
+ );
455
+ }
456
+ this.time.event(
457
+ id,
458
+ "self-scheduled",
459
+ reason || "自己选定开始时间与投入时长",
460
+ {
461
+ previous: task.checkpoint.schedule || null,
462
+ schedule: checkpoint.schedule,
463
+ previousTitle: task.title,
464
+ },
465
+ now,
466
+ );
467
+ return current;
468
+ } catch (error) {
469
+ this.db.exec("ROLLBACK TO arrange_task");
470
+ throw error;
471
+ } finally {
472
+ this.db.exec("RELEASE arrange_task");
473
+ }
474
+ }
475
+ slots(now = this.time.now()) {
476
+ return this.db
477
+ .prepare(
478
+ "SELECT id,priority,state,checkpoint FROM mind_time_tasks WHERE state IN ('todo','scheduled','doing') AND json_extract(checkpoint,'$.reschedule') IS NOT 1 AND json_extract(checkpoint,'$.schedule.chosenAt') IS NOT NULL AND json_extract(checkpoint,'$.schedule.endedAt')>? ORDER BY ready_at LIMIT 30",
479
+ )
480
+ .all(now)
481
+ .map((row) => {
482
+ const slot = parse(row.checkpoint, {}).schedule;
483
+ return {
484
+ id: row.id,
485
+ priority: row.priority,
486
+ startAt: slot.proposedAt,
487
+ endAt: slot.endedAt,
488
+ ongoing: row.state === "doing",
489
+ };
490
+ });
491
+ }
492
+ control(
493
+ id,
494
+ { action, reason = "", readyAt, dueAt, priority, durationMinutes },
495
+ now = this.time.now(),
496
+ ) {
497
+ const task = this.get(id);
498
+ if (!task) throw Error("任务不存在");
499
+ if (task.checkpoint.mergedInto)
500
+ throw Error("这条记录已合并,请调整唯一任务");
501
+ if (action === "priority") {
502
+ if (!Number.isInteger(priority) || priority < 0 || priority > 3)
503
+ throw Error("优先级无效");
504
+ this.db
505
+ .prepare("UPDATE mind_time_tasks SET priority=?,updated=? WHERE id=?")
506
+ .run(priority, now, id);
507
+ this.time.event(
508
+ id,
509
+ "priority",
510
+ reason || "调整优先级",
511
+ { previous: task.priority, priority },
512
+ now,
513
+ );
514
+ return this.get(id);
515
+ }
516
+ if (["done", "abandoned"].includes(task.state)) throw Error("任务已经结束");
517
+ const state = {
518
+ pause: "paused",
519
+ resume: "todo",
520
+ schedule: "scheduled",
521
+ abandon: "abandoned",
522
+ wait: "waiting",
523
+ }[action];
524
+ if (!state) throw Error("不能直接标记完成");
525
+ if (
526
+ action === "schedule" &&
527
+ durationMinutes !== undefined &&
528
+ (!Number.isInteger(durationMinutes) ||
529
+ durationMinutes < 5 ||
530
+ durationMinutes > 240)
531
+ )
532
+ throw Error("投入时间应为 5 至 240 分钟");
533
+ this.time.stopSpan(task, now);
534
+ const ready = Number.isFinite(readyAt) ? readyAt : now;
535
+ const next =
536
+ state === "paused" && !Number.isFinite(readyAt)
537
+ ? Number.MAX_SAFE_INTEGER
538
+ : ready;
539
+ const checkpoint = { ...task.checkpoint };
540
+ if (state === "paused" && checkpoint.schedule?.chosenAt)
541
+ checkpoint.reschedule = true;
542
+ if (action === "schedule") {
543
+ const minutes =
544
+ durationMinutes ||
545
+ checkpoint.schedule?.durationMinutes ||
546
+ this.time.settings().focusMinutes;
547
+ checkpoint.reschedule = false;
548
+ checkpoint.schedule = {
549
+ ...checkpoint.schedule,
550
+ proposedAt: Math.max(now, ready),
551
+ endedAt: Math.max(now, ready) + minutes * 60000,
552
+ durationMinutes: minutes,
553
+ focusMinutes: minutes,
554
+ chosenAt: now,
555
+ chosenBy: "admin",
556
+ baselineMs: this.time.clock.committed(id),
557
+ reason: text(reason, 240) || "管理台安排",
558
+ };
559
+ }
560
+ this.db
561
+ .prepare(
562
+ "UPDATE mind_time_tasks SET state=?,updated=?,ready_at=?,next_step=?,due_at=?,wait_reason=?,checkpoint=?,revision=revision+1,lease=NULL,lease_at=NULL WHERE id=?",
563
+ )
564
+ .run(
565
+ state,
566
+ now,
567
+ ready,
568
+ next,
569
+ dueAt === undefined ? task.due_at : dueAt,
570
+ text(reason, 300),
571
+ JSON.stringify(checkpoint),
572
+ id,
573
+ );
574
+ this.time.event(id, state, reason, {}, now);
575
+ if (state === "abandoned")
576
+ this.links.close(task, reason || "这件事被放下了", now);
577
+ return this.get(id);
578
+ }
579
+ wait(task, reason, next = 0, now = this.time.now()) {
580
+ this.time.stopSpan(task, now);
581
+ this.db
582
+ .prepare(
583
+ "UPDATE mind_time_tasks SET state='waiting',wait_reason=?,next_step=?,updated=?,lease=NULL,lease_at=NULL,revision=revision+1 WHERE id=?",
584
+ )
585
+ .run(text(reason, 300), next, now, task.id);
586
+ }
587
+ }