luckytri 1.0.0 → 1.0.1

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 (133) 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 +192 -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 +192 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +109 -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-B9Odvzfz.js +3 -0
  19. package/public/app/assets/Empty-SkA8PyMo.js +2 -0
  20. package/public/app/assets/Field-BjLhi8pL.js +1 -0
  21. package/public/app/assets/HeartPage-CscKqMsr.js +1 -0
  22. package/public/app/assets/LifePage-BHCmBHlH.js +1 -0
  23. package/public/app/assets/MemoryPage-Bhs2xa1k.js +1 -0
  24. package/public/app/assets/NaturePage-CaPIar_5.js +3 -0
  25. package/public/app/assets/PeoplePage-CuwSSVeX.js +1 -0
  26. package/public/app/assets/PluginsPage-BhVm1SUi.js +2 -0
  27. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-5yYqyUgR.css} +1 -1
  29. package/public/app/assets/SystemPage-Chm5fiGP.js +1 -0
  30. package/public/app/assets/TimePage-BUm4eUzB.js +3 -0
  31. package/public/app/assets/index-BIAdfObg.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-DNDLyN9m.js} +1 -1
  34. package/public/app/assets/media-CKVAvML2.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +32 -99
  37. package/public/guide.html +21 -86
  38. package/public/tutorial.en.md +15 -39
  39. package/public/tutorial.md +15 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/launch.js +101 -94
  43. package/scripts/plugin.js +150 -150
  44. package/scripts/reset-password.js +22 -0
  45. package/scripts/runtime.js +25 -25
  46. package/scripts/service.js +56 -47
  47. package/scripts/setup.js +50 -50
  48. package/scripts/start-server.js +41 -41
  49. package/scripts/stop.js +36 -36
  50. package/server/app.js +110 -96
  51. package/server/auth.js +217 -0
  52. package/server/channels/contract.js +57 -57
  53. package/server/channels/hub.js +165 -165
  54. package/server/channels/index.js +28 -25
  55. package/server/channels/onebot/index.js +335 -336
  56. package/server/channels/onebot/token.js +22 -3
  57. package/server/core/capabilities.js +208 -208
  58. package/server/core/events.js +64 -60
  59. package/server/core/image-preprocessor.js +208 -208
  60. package/server/core/model-manager.js +1213 -1213
  61. package/server/core/net-guard.js +49 -49
  62. package/server/core/orchestrator.js +569 -569
  63. package/server/core/perceivers.js +91 -91
  64. package/server/core/persona-manager.js +243 -243
  65. package/server/core/repository.js +293 -293
  66. package/server/core/response-validator.js +508 -508
  67. package/server/core/turn-processor.js +630 -630
  68. package/server/core/turn.js +236 -236
  69. package/server/core/vision-manager.js +633 -633
  70. package/server/i18n/errors.js +512 -497
  71. package/server/i18n/labels.js +649 -653
  72. package/server/index.js +107 -102
  73. package/server/knowledge/manager.js +386 -386
  74. package/server/local-access.js +32 -0
  75. package/server/mind/anticipations.js +507 -507
  76. package/server/mind/api.js +703 -703
  77. package/server/mind/budget.js +152 -152
  78. package/server/mind/context-selection.js +61 -61
  79. package/server/mind/day-planner.js +324 -324
  80. package/server/mind/evidence.js +148 -148
  81. package/server/mind/index.js +445 -445
  82. package/server/mind/life/diary.js +786 -786
  83. package/server/mind/life/index.js +1049 -1049
  84. package/server/mind/observations.js +84 -84
  85. package/server/mind/own-day.js +324 -324
  86. package/server/mind/own-voice.js +318 -318
  87. package/server/mind/senses.js +53 -53
  88. package/server/mind/time/activity-clock.js +196 -196
  89. package/server/mind/time/availability.js +69 -69
  90. package/server/mind/time/commitment-review.js +198 -198
  91. package/server/mind/time/executor.js +512 -512
  92. package/server/mind/time/experiences.js +128 -128
  93. package/server/mind/time/games.js +562 -562
  94. package/server/mind/time/index.js +739 -739
  95. package/server/mind/time/intent.js +166 -166
  96. package/server/mind/time/kinds.js +171 -171
  97. package/server/mind/time/plugin-activity.js +207 -207
  98. package/server/mind/time/tasks.js +587 -587
  99. package/server/mind/util.js +110 -110
  100. package/server/mind/view.js +273 -273
  101. package/server/paths.js +102 -102
  102. package/server/plugins/api.js +204 -204
  103. package/server/plugins/host.js +967 -967
  104. package/server/plugins/install.js +295 -295
  105. package/server/plugins/manifest.js +201 -201
  106. package/server/plugins/permissions.js +42 -42
  107. package/server/plugins/runner/guard.mjs +36 -36
  108. package/server/plugins/runner/worker.mjs +250 -250
  109. package/server/plugins/schema.js +91 -91
  110. package/server/plugins/zip.js +112 -112
  111. package/server/startup-lock.js +118 -118
  112. package/server/storage/archive.js +241 -241
  113. package/server/storage/backup-cleanup-worker.js +8 -8
  114. package/server/storage/backup-cleanup.js +414 -414
  115. package/server/storage/backup-lock.js +76 -76
  116. package/server/storage/backup-scheduler.js +131 -131
  117. package/server/storage/store.js +199 -199
  118. package/server/studio/readiness.js +181 -190
  119. package/server/studio/settings.js +236 -236
  120. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  121. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  122. package/public/app/assets/Field-D4gY-INU.js +0 -1
  123. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  124. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  125. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  126. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  127. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  128. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  129. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  130. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  131. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  132. package/public/app/assets/index-BheRW-1t.js +0 -4
  133. 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
+ }