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,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
+ }