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.
- package/.github/SECURITY.md +71 -0
- package/LICENSE +21 -0
- package/README.en.md +250 -0
- package/README.md +250 -0
- package/bin/luckytri.js +107 -0
- package/docs/brand/banner.png +0 -0
- package/docs/brand/palette.png +0 -0
- package/docs/brand/portrait.png +0 -0
- package/docs/en/CHANGELOG.md +113 -0
- package/docs/en/architecture.md +152 -0
- package/docs/en/chat-style.md +62 -0
- package/docs/en/connect.md +63 -0
- package/docs/en/guide.md +216 -0
- package/docs/en/her-life.md +189 -0
- package/docs/en/npm-release.md +25 -0
- package/docs/en/plugins.md +82 -0
- package/docs/en/recovery.md +28 -0
- package/docs/en/time.md +71 -0
- package/docs/en/webui.md +73 -0
- package/docs/zh/CHANGELOG.md +113 -0
- package/docs/zh/architecture.md +152 -0
- package/docs/zh/chat-style.md +62 -0
- package/docs/zh/connect.md +63 -0
- package/docs/zh/guide.md +216 -0
- package/docs/zh/her-life.md +189 -0
- package/docs/zh/npm-release.md +33 -0
- package/docs/zh/plugins.md +82 -0
- package/docs/zh/recovery.md +28 -0
- package/docs/zh/time.md +71 -0
- package/docs/zh/webui.md +73 -0
- package/package.json +108 -0
- package/plugins/sdk/index.d.ts +155 -0
- package/plugins/sdk/testing.js +58 -0
- package/plugins/weather/index.js +82 -0
- package/plugins/weather/luckytri-plugin.json +29 -0
- package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/Empty-CpP5kQHH.js +2 -0
- package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
- package/public/app/assets/Field-D4gY-INU.js +1 -0
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
- package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
- package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
- package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
- package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
- package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
- package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
- package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
- package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/TimePage-Cw44inM4.js +3 -0
- package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
- package/public/app/assets/index-BheRW-1t.js +4 -0
- package/public/app/assets/index-YDz1ree1.css +1 -0
- package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
- package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
- package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
- package/public/app/assets/media-BzPDBm7e.js +1 -0
- package/public/app/index.html +27 -0
- package/public/guide.en.html +1053 -0
- package/public/guide.html +893 -0
- package/public/plugin-kit/v1/bridge.js +30 -0
- package/public/plugin-kit/v1/kit.css +14 -0
- package/public/tutorial.en.md +214 -0
- package/public/tutorial.md +214 -0
- package/scripts/backup.js +311 -0
- package/scripts/browser.js +41 -0
- package/scripts/launch.js +94 -0
- package/scripts/plugin.js +150 -0
- package/scripts/proxy-config.js +167 -0
- package/scripts/recover.js +26 -0
- package/scripts/runtime.js +25 -0
- package/scripts/service.js +47 -0
- package/scripts/setup.js +50 -0
- package/scripts/start-server.js +41 -0
- package/scripts/stop.js +36 -0
- package/server/app.js +96 -0
- package/server/channels/adapters.js +27 -0
- package/server/channels/contract.js +57 -0
- package/server/channels/hub.js +165 -0
- package/server/channels/index.js +25 -0
- package/server/channels/onebot/adapter.js +288 -0
- package/server/channels/onebot/index.js +336 -0
- package/server/channels/onebot/token.js +3 -0
- package/server/channels/qqbot/adapter.js +253 -0
- package/server/channels/qqbot/api.js +199 -0
- package/server/channels/qqbot/credentials.js +11 -0
- package/server/channels/qqbot/gateway.js +274 -0
- package/server/channels/qqbot/index.js +389 -0
- package/server/channels/segments.js +40 -0
- package/server/channels/session-key.js +153 -0
- package/server/channels/state.js +42 -0
- package/server/core/api.js +564 -0
- package/server/core/capabilities.js +208 -0
- package/server/core/context-builder.js +288 -0
- package/server/core/context-compactor.js +454 -0
- package/server/core/conversation-cues.js +325 -0
- package/server/core/conversation-manager.js +87 -0
- package/server/core/events.js +60 -0
- package/server/core/feedback.js +8 -0
- package/server/core/group-style.js +72 -0
- package/server/core/image-preprocessor.js +208 -0
- package/server/core/initiative-context.js +66 -0
- package/server/core/input-budget.js +32 -0
- package/server/core/local-demo.js +98 -0
- package/server/core/message-manager.js +118 -0
- package/server/core/message-scheduler.js +78 -0
- package/server/core/model-json.js +190 -0
- package/server/core/model-manager.js +1213 -0
- package/server/core/model-presets.js +900 -0
- package/server/core/name-call.js +29 -0
- package/server/core/net-guard.js +49 -0
- package/server/core/network.js +157 -0
- package/server/core/orchestrator.js +569 -0
- package/server/core/perceivers.js +91 -0
- package/server/core/person-name.js +22 -0
- package/server/core/persona-manager.js +243 -0
- package/server/core/reply-delivery.js +393 -0
- package/server/core/reply-target-resolver.js +50 -0
- package/server/core/repository.js +293 -0
- package/server/core/response-generator.js +50 -0
- package/server/core/response-validator.js +508 -0
- package/server/core/retired-prompts.js +51 -0
- package/server/core/sessions.js +109 -0
- package/server/core/speaker-names.js +92 -0
- package/server/core/topic-tracker.js +65 -0
- package/server/core/turn-processor.js +630 -0
- package/server/core/turn.js +236 -0
- package/server/core/vision-manager.js +633 -0
- package/server/http.js +18 -0
- package/server/i18n/decisions.js +206 -0
- package/server/i18n/errors.js +497 -0
- package/server/i18n/index.js +144 -0
- package/server/i18n/labels.js +653 -0
- package/server/i18n/models.js +111 -0
- package/server/index.js +102 -0
- package/server/knowledge/api.js +342 -0
- package/server/knowledge/embedding-profile.js +114 -0
- package/server/knowledge/index.js +13 -0
- package/server/knowledge/manager.js +386 -0
- package/server/knowledge/retrieval.js +93 -0
- package/server/knowledge/schema.js +154 -0
- package/server/knowledge/vector-index.js +90 -0
- package/server/mind/affect.js +265 -0
- package/server/mind/anticipations.js +507 -0
- package/server/mind/api.js +703 -0
- package/server/mind/attention.js +200 -0
- package/server/mind/bonds.js +558 -0
- package/server/mind/budget.js +152 -0
- package/server/mind/clock.js +83 -0
- package/server/mind/context-selection.js +61 -0
- package/server/mind/continuity.js +562 -0
- package/server/mind/conversation-origin.js +175 -0
- package/server/mind/day-planner.js +324 -0
- package/server/mind/days.js +210 -0
- package/server/mind/evidence-read.js +45 -0
- package/server/mind/evidence.js +148 -0
- package/server/mind/faces.js +138 -0
- package/server/mind/guard.js +60 -0
- package/server/mind/index.js +445 -0
- package/server/mind/initiative.js +333 -0
- package/server/mind/life/activities.js +21 -0
- package/server/mind/life/context.js +309 -0
- package/server/mind/life/diary.js +786 -0
- package/server/mind/life/growth.js +252 -0
- package/server/mind/life/index.js +1049 -0
- package/server/mind/life/presence.js +305 -0
- package/server/mind/meetings.js +1069 -0
- package/server/mind/memory-policy.js +48 -0
- package/server/mind/memory.js +888 -0
- package/server/mind/nature.js +262 -0
- package/server/mind/novelty.js +19 -0
- package/server/mind/observations.js +84 -0
- package/server/mind/own-day.js +324 -0
- package/server/mind/own-voice.js +318 -0
- package/server/mind/periods.js +118 -0
- package/server/mind/reading.js +120 -0
- package/server/mind/salience.js +201 -0
- package/server/mind/schema.js +562 -0
- package/server/mind/self.js +484 -0
- package/server/mind/senses.js +53 -0
- package/server/mind/thoughts.js +274 -0
- package/server/mind/time/activity-clock.js +196 -0
- package/server/mind/time/agenda.js +242 -0
- package/server/mind/time/api.js +200 -0
- package/server/mind/time/attention.js +34 -0
- package/server/mind/time/availability.js +69 -0
- package/server/mind/time/commitment-review.js +198 -0
- package/server/mind/time/delivery-links.js +130 -0
- package/server/mind/time/executor.js +512 -0
- package/server/mind/time/experiences.js +128 -0
- package/server/mind/time/games.js +562 -0
- package/server/mind/time/index.js +739 -0
- package/server/mind/time/intent.js +166 -0
- package/server/mind/time/kinds.js +171 -0
- package/server/mind/time/model-material.js +77 -0
- package/server/mind/time/plugin-activity.js +207 -0
- package/server/mind/time/presentation.js +51 -0
- package/server/mind/time/schedule.js +83 -0
- package/server/mind/time/schema.js +84 -0
- package/server/mind/time/search.js +383 -0
- package/server/mind/time/sharing.js +472 -0
- package/server/mind/time/task-links.js +311 -0
- package/server/mind/time/tasks.js +587 -0
- package/server/mind/time/works.js +432 -0
- package/server/mind/traits.js +191 -0
- package/server/mind/util.js +110 -0
- package/server/mind/view.js +273 -0
- package/server/paths.js +102 -0
- package/server/plugins/api.js +204 -0
- package/server/plugins/host.js +967 -0
- package/server/plugins/install.js +295 -0
- package/server/plugins/manifest.js +201 -0
- package/server/plugins/permissions.js +42 -0
- package/server/plugins/runner/guard.mjs +36 -0
- package/server/plugins/runner/worker.mjs +250 -0
- package/server/plugins/schema.js +91 -0
- package/server/plugins/zip.js +112 -0
- package/server/startup-lock.js +118 -0
- package/server/storage/archive.js +241 -0
- package/server/storage/backup-cleanup-worker.js +8 -0
- package/server/storage/backup-cleanup.js +414 -0
- package/server/storage/backup-lock.js +76 -0
- package/server/storage/backup-scheduler.js +131 -0
- package/server/storage/store.js +199 -0
- package/server/studio/display-names.js +138 -0
- package/server/studio/management.js +102 -0
- package/server/studio/readiness.js +190 -0
- package/server/studio/settings.js +236 -0
- package/server/version.js +10 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { evidenceRoots } from "./evidence.js";
|
|
3
|
+
import {
|
|
4
|
+
THOUGHT_FADED,
|
|
5
|
+
anyTouches,
|
|
6
|
+
cueList,
|
|
7
|
+
thoughtSalience,
|
|
8
|
+
} from "./salience.js";
|
|
9
|
+
import { HOUR, clamp, evidence, parse, text } from "./util.js";
|
|
10
|
+
|
|
11
|
+
export const THOUGHT_KINDS = {
|
|
12
|
+
reflection: "后来想到",
|
|
13
|
+
revision: "重新理解",
|
|
14
|
+
unfinished: "仍放在心上",
|
|
15
|
+
reconnection: "久别想起",
|
|
16
|
+
expression: "自己的念头",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function row(value) {
|
|
20
|
+
return value
|
|
21
|
+
? {
|
|
22
|
+
...value,
|
|
23
|
+
sessions: parse(value.sessions, []),
|
|
24
|
+
sources: parse(value.sources, []),
|
|
25
|
+
outreach_draft: parse(value.outreach_draft, []),
|
|
26
|
+
}
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Her journal between conversations. A later understanding is a new entry
|
|
31
|
+
// pointing at the old one; the old words stay as they were.
|
|
32
|
+
export class Thoughts {
|
|
33
|
+
constructor(mind) {
|
|
34
|
+
this.mind = mind;
|
|
35
|
+
this.db = mind.db;
|
|
36
|
+
}
|
|
37
|
+
add({
|
|
38
|
+
kind,
|
|
39
|
+
content,
|
|
40
|
+
sessions = [],
|
|
41
|
+
sources = [],
|
|
42
|
+
parentId = null,
|
|
43
|
+
importance = 0.5,
|
|
44
|
+
revisitHours = 0,
|
|
45
|
+
outreach = "",
|
|
46
|
+
outreachSession = null,
|
|
47
|
+
outreachAfterHours,
|
|
48
|
+
outreachReason = "",
|
|
49
|
+
runId = null,
|
|
50
|
+
time = Date.now(),
|
|
51
|
+
}) {
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
const hours = Number(revisitHours);
|
|
54
|
+
this.db
|
|
55
|
+
.prepare(
|
|
56
|
+
"INSERT INTO mind_thoughts(id,created,kind,content,sessions,sources,parent_id,importance,revisit_at,status,hidden,outreach,outreach_session,outreach_status,run_id,outreach_at,outreach_reason) VALUES (?,?,?,?,?,?,?,?,?,'open',0,?,?,?,?,?,?)",
|
|
57
|
+
)
|
|
58
|
+
.run(
|
|
59
|
+
id,
|
|
60
|
+
time,
|
|
61
|
+
THOUGHT_KINDS[kind] ? kind : "reflection",
|
|
62
|
+
text(content, 600),
|
|
63
|
+
JSON.stringify([...new Set(sessions)].slice(0, 12)),
|
|
64
|
+
JSON.stringify(evidence(sources)),
|
|
65
|
+
parentId || null,
|
|
66
|
+
clamp(importance),
|
|
67
|
+
Number.isFinite(hours) && hours > 0
|
|
68
|
+
? time + Math.min(8760, Math.max(1, hours)) * HOUR
|
|
69
|
+
: null,
|
|
70
|
+
text(outreach, 120),
|
|
71
|
+
outreach ? outreachSession : null,
|
|
72
|
+
outreach ? "planned" : "none",
|
|
73
|
+
runId,
|
|
74
|
+
outreach
|
|
75
|
+
? time +
|
|
76
|
+
Math.min(
|
|
77
|
+
8760,
|
|
78
|
+
Math.max(0, Number(outreachAfterHours ?? revisitHours) || 0),
|
|
79
|
+
) *
|
|
80
|
+
HOUR
|
|
81
|
+
: null,
|
|
82
|
+
text(outreachReason, 300),
|
|
83
|
+
);
|
|
84
|
+
return id;
|
|
85
|
+
}
|
|
86
|
+
get(id) {
|
|
87
|
+
return row(
|
|
88
|
+
this.db.prepare("SELECT * FROM mind_thoughts WHERE id=?").get(id),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
list({
|
|
92
|
+
before = Number.MAX_SAFE_INTEGER,
|
|
93
|
+
session = "",
|
|
94
|
+
q = "",
|
|
95
|
+
limit = 30,
|
|
96
|
+
hidden = true,
|
|
97
|
+
} = {}) {
|
|
98
|
+
return this.db
|
|
99
|
+
.prepare(
|
|
100
|
+
`SELECT * FROM mind_thoughts WHERE created<?${hidden ? "" : " AND hidden=0"}${session ? " AND sessions LIKE ?" : ""}${q ? " AND content LIKE ?" : ""} ORDER BY created DESC LIMIT ?`,
|
|
101
|
+
)
|
|
102
|
+
.all(
|
|
103
|
+
before,
|
|
104
|
+
...(session ? [`%${JSON.stringify(session).slice(1, -1)}%`] : []),
|
|
105
|
+
...(q ? [`%${q}%`] : []),
|
|
106
|
+
limit,
|
|
107
|
+
)
|
|
108
|
+
.map(row);
|
|
109
|
+
}
|
|
110
|
+
// Still on her mind at `now`, ranked by how present each one is; a thought
|
|
111
|
+
// let go of later still counts as open when looking back. A later note
|
|
112
|
+
// that only repeats sources already used does not start this clock over.
|
|
113
|
+
weighed({ now = Date.now(), session = "" } = {}) {
|
|
114
|
+
const lived = this.mind.days.lived(now);
|
|
115
|
+
const notes = this.db
|
|
116
|
+
.prepare(
|
|
117
|
+
"SELECT * FROM mind_thoughts WHERE hidden=0 AND created<=? AND (status='open' OR resolved_at>?) ORDER BY created DESC LIMIT 400",
|
|
118
|
+
)
|
|
119
|
+
.all(now, now)
|
|
120
|
+
.map(row)
|
|
121
|
+
.filter((t) => !session || t.sessions.includes(session));
|
|
122
|
+
const clock = sourceClock(notes, (sources) =>
|
|
123
|
+
evidenceRoots(this.db, sources, now),
|
|
124
|
+
);
|
|
125
|
+
return notes
|
|
126
|
+
.map((t) => {
|
|
127
|
+
const earned = clock.get(t.id) ?? t.created;
|
|
128
|
+
// A revisit brings back the note that actually lived the experience.
|
|
129
|
+
// Rewriting it later and setting another time does not.
|
|
130
|
+
const seen = earned < t.created ? { ...t, revisit_at: null } : t;
|
|
131
|
+
return {
|
|
132
|
+
...t,
|
|
133
|
+
salience: thoughtSalience({ ...seen, created: earned }, lived, now),
|
|
134
|
+
};
|
|
135
|
+
})
|
|
136
|
+
.sort((a, b) => b.salience - a.salience || b.created - a.created);
|
|
137
|
+
}
|
|
138
|
+
open({ now = Date.now(), limit = 12, session = "" } = {}) {
|
|
139
|
+
return this.weighed({ now, session })
|
|
140
|
+
.filter((t) => t.salience >= THOUGHT_FADED)
|
|
141
|
+
.slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
// The studio card shows the newest note still in view. Salience still
|
|
144
|
+
// decides which thoughts a conversation carries.
|
|
145
|
+
latest(now = Date.now()) {
|
|
146
|
+
return (
|
|
147
|
+
this.open({ now, limit: 40 }).sort((a, b) => b.created - a.created)[0] ||
|
|
148
|
+
null
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// Faded thoughts from this place that the conversation brings back.
|
|
152
|
+
reminded({ now = Date.now(), cue, cues, session = "", limit = 1 } = {}) {
|
|
153
|
+
const sets = cueList(cue, cues);
|
|
154
|
+
if (!sets.length) return [];
|
|
155
|
+
return this.weighed({ now, session })
|
|
156
|
+
.filter((t) => t.salience < THOUGHT_FADED && anyTouches(t.content, sets))
|
|
157
|
+
.slice(0, limit);
|
|
158
|
+
}
|
|
159
|
+
resolve(id, resolution = "", time = Date.now()) {
|
|
160
|
+
return (
|
|
161
|
+
this.db
|
|
162
|
+
.prepare(
|
|
163
|
+
"UPDATE mind_thoughts SET status='resolved',resolved_at=?,resolution=? WHERE id=? AND status='open'",
|
|
164
|
+
)
|
|
165
|
+
.run(time, text(resolution, 120), id).changes > 0
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
update(id, { status, hidden }) {
|
|
169
|
+
const current = this.get(id);
|
|
170
|
+
if (!current) throw Error("手记不存在");
|
|
171
|
+
if (status !== undefined && !["open", "resolved"].includes(status))
|
|
172
|
+
throw Error("状态无效");
|
|
173
|
+
if (hidden !== undefined && typeof hidden !== "boolean")
|
|
174
|
+
throw Error("开关无效");
|
|
175
|
+
const next = status ?? current.status;
|
|
176
|
+
const resolved =
|
|
177
|
+
next === current.status
|
|
178
|
+
? [current.resolved_at ?? null, current.resolution ?? ""]
|
|
179
|
+
: next === "resolved"
|
|
180
|
+
? [Date.now(), "在工作台里放下"]
|
|
181
|
+
: [null, ""];
|
|
182
|
+
this.db
|
|
183
|
+
.prepare(
|
|
184
|
+
"UPDATE mind_thoughts SET status=?,hidden=?,resolved_at=?,resolution=? WHERE id=?",
|
|
185
|
+
)
|
|
186
|
+
.run(
|
|
187
|
+
next,
|
|
188
|
+
hidden === undefined ? current.hidden : +hidden,
|
|
189
|
+
...resolved,
|
|
190
|
+
id,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
remove(id) {
|
|
194
|
+
if (
|
|
195
|
+
this.db.prepare("SELECT 1 FROM mind_thoughts WHERE parent_id=?").get(id)
|
|
196
|
+
)
|
|
197
|
+
throw Error("这条手记有后续修正,请隐藏以保留轨迹");
|
|
198
|
+
this.db.prepare("DELETE FROM mind_thoughts WHERE id=?").run(id);
|
|
199
|
+
}
|
|
200
|
+
setOutreach(id, status, reason = "") {
|
|
201
|
+
this.db
|
|
202
|
+
.prepare(
|
|
203
|
+
"UPDATE mind_thoughts SET outreach_status=?,outreach_wait_reason=?,outreach_retry_at=NULL WHERE id=?",
|
|
204
|
+
)
|
|
205
|
+
.run(status, text(reason, 300), id);
|
|
206
|
+
}
|
|
207
|
+
planOutreach(id, { session, words, reason, time }) {
|
|
208
|
+
const current = this.get(id);
|
|
209
|
+
if (!current || current.outreach_status !== "none") return false;
|
|
210
|
+
const draft = words
|
|
211
|
+
.filter((s) => typeof s === "string" && s.trim())
|
|
212
|
+
.slice(0, 3)
|
|
213
|
+
.map((s) => text(s, 180));
|
|
214
|
+
if (!session || !draft.length) return false;
|
|
215
|
+
this.db
|
|
216
|
+
.prepare(
|
|
217
|
+
"UPDATE mind_thoughts SET sessions=?,outreach=?,outreach_session=?,outreach_status='planned',outreach_at=?,outreach_reason=?,outreach_draft=? WHERE id=? AND outreach_status='none'",
|
|
218
|
+
)
|
|
219
|
+
.run(
|
|
220
|
+
// An independent thought is not owned by its first audience. Keep its
|
|
221
|
+
// origin scope; outreach_session records where she chose to share it.
|
|
222
|
+
JSON.stringify(
|
|
223
|
+
current.kind === "expression"
|
|
224
|
+
? current.sessions
|
|
225
|
+
: [...new Set([...current.sessions, session])],
|
|
226
|
+
),
|
|
227
|
+
draft.join("\n"),
|
|
228
|
+
session,
|
|
229
|
+
time,
|
|
230
|
+
text(reason, 300),
|
|
231
|
+
JSON.stringify(draft),
|
|
232
|
+
id,
|
|
233
|
+
);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
deferOutreach(id, reason, retryAt) {
|
|
237
|
+
this.db
|
|
238
|
+
.prepare(
|
|
239
|
+
"UPDATE mind_thoughts SET outreach_status='planned',outreach_wait_reason=?,outreach_retry_at=? WHERE id=?",
|
|
240
|
+
)
|
|
241
|
+
.run(text(reason, 300), retryAt, id);
|
|
242
|
+
}
|
|
243
|
+
dueOutreach(now = Date.now()) {
|
|
244
|
+
return this.db
|
|
245
|
+
.prepare(
|
|
246
|
+
"SELECT * FROM mind_thoughts WHERE outreach_status='planned' AND hidden=0 AND status='open' AND outreach!='' AND COALESCE(outreach_at,revisit_at,created)<=? AND COALESCE(outreach_retry_at,0)<=? ORDER BY importance DESC, created LIMIT 5",
|
|
247
|
+
)
|
|
248
|
+
.all(now, now)
|
|
249
|
+
.map(row);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The first time a source was lived. A later note that only repeats those
|
|
254
|
+
// sources keeps that time, so rewriting it does not make it newly present.
|
|
255
|
+
function sourceClock(notes, roots) {
|
|
256
|
+
const earned = new Map();
|
|
257
|
+
const clock = new Map();
|
|
258
|
+
for (const note of [...notes].sort((a, b) => a.created - b.created)) {
|
|
259
|
+
const resolved = roots(note.sources);
|
|
260
|
+
const sources = resolved.length ? resolved : [`t:${note.id}`];
|
|
261
|
+
const fresh = sources.some((source) => !earned.has(source));
|
|
262
|
+
if (fresh) {
|
|
263
|
+
clock.set(note.id, note.created);
|
|
264
|
+
for (const source of sources)
|
|
265
|
+
if (!earned.has(source)) earned.set(source, note.created);
|
|
266
|
+
} else {
|
|
267
|
+
clock.set(
|
|
268
|
+
note.id,
|
|
269
|
+
Math.min(...sources.map((source) => earned.get(source))),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return clock;
|
|
274
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { text } from "../util.js";
|
|
2
|
+
import { activityMinutes } from "./kinds.js";
|
|
3
|
+
const MINUTE = 60000;
|
|
4
|
+
export function activityEstimate(activity, hint, speed = 1.25) {
|
|
5
|
+
const fallback = activityMinutes(activity),
|
|
6
|
+
proposed = Number(hint?.minutes);
|
|
7
|
+
const referenceMinutes =
|
|
8
|
+
Number.isFinite(proposed) && proposed >= 5 && proposed <= 240
|
|
9
|
+
? proposed
|
|
10
|
+
: fallback[0];
|
|
11
|
+
const pace =
|
|
12
|
+
Number.isFinite(speed) && speed >= 1 && speed <= 2 ? speed : 1.25;
|
|
13
|
+
return {
|
|
14
|
+
referenceMinutes,
|
|
15
|
+
plannedMs: Math.ceil((referenceMinutes * MINUTE) / pace),
|
|
16
|
+
speed: pace,
|
|
17
|
+
basis: text(hint?.basis, 240) || fallback[1],
|
|
18
|
+
scope: "本段",
|
|
19
|
+
chapterMinutes:
|
|
20
|
+
Number.isFinite(hint?.chapterMinutes) &&
|
|
21
|
+
hint.chapterMinutes >= referenceMinutes &&
|
|
22
|
+
hint.chapterMinutes <= 6000
|
|
23
|
+
? hint.chapterMinutes
|
|
24
|
+
: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export class ActivityClock {
|
|
28
|
+
constructor(time) {
|
|
29
|
+
this.time = time;
|
|
30
|
+
this.db = time.db;
|
|
31
|
+
}
|
|
32
|
+
committed(id) {
|
|
33
|
+
return this.db
|
|
34
|
+
.prepare(
|
|
35
|
+
"SELECT COALESCE(SUM(engaged_ms),0) n FROM mind_time_spans WHERE task_id=?",
|
|
36
|
+
)
|
|
37
|
+
.get(id).n;
|
|
38
|
+
}
|
|
39
|
+
record(span, ms) {
|
|
40
|
+
if (ms > 0)
|
|
41
|
+
this.db
|
|
42
|
+
.prepare(
|
|
43
|
+
"INSERT OR IGNORE INTO mind_time_activity_ticks(span_id,started,ended) VALUES(?,?,?)",
|
|
44
|
+
)
|
|
45
|
+
.run(span.id, span.updated, span.updated + ms);
|
|
46
|
+
}
|
|
47
|
+
today(spanId, start, now) {
|
|
48
|
+
return this.db
|
|
49
|
+
.prepare(
|
|
50
|
+
"SELECT COALESCE(SUM(MAX(0,MIN(ended,?)-MAX(started,?))),0) n FROM mind_time_activity_ticks WHERE span_id=? AND ended>? AND started<?",
|
|
51
|
+
)
|
|
52
|
+
.get(now, start, spanId, start, now).n;
|
|
53
|
+
}
|
|
54
|
+
credit(task, delta) {
|
|
55
|
+
const clock = task.checkpoint?.activityClock;
|
|
56
|
+
if (
|
|
57
|
+
!clock ||
|
|
58
|
+
clock.phase !== "engaged" ||
|
|
59
|
+
task.lease ||
|
|
60
|
+
task.state !== "doing"
|
|
61
|
+
)
|
|
62
|
+
return 0;
|
|
63
|
+
return Math.max(
|
|
64
|
+
0,
|
|
65
|
+
Math.min(
|
|
66
|
+
delta,
|
|
67
|
+
Math.round(
|
|
68
|
+
clock.plannedMs - (this.committed(task.id) - clock.baselineMs),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
elapsed(task, now = this.time.now()) {
|
|
74
|
+
const total = this.committed(task.id),
|
|
75
|
+
span = this.db
|
|
76
|
+
.prepare(
|
|
77
|
+
"SELECT updated FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
|
|
78
|
+
)
|
|
79
|
+
.get(task.id);
|
|
80
|
+
return (
|
|
81
|
+
total +
|
|
82
|
+
(span
|
|
83
|
+
? this.credit(task, Math.max(0, Math.min(MINUTE, now - span.updated)))
|
|
84
|
+
: 0)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
plan(task, hint, now) {
|
|
88
|
+
const estimate = activityEstimate(
|
|
89
|
+
task.activity,
|
|
90
|
+
hint,
|
|
91
|
+
this.time.settings().paceSpeed,
|
|
92
|
+
);
|
|
93
|
+
if (task.checkpoint.schedule?.chosenAt) {
|
|
94
|
+
const used =
|
|
95
|
+
this.db
|
|
96
|
+
.prepare(
|
|
97
|
+
"SELECT engaged_ms FROM mind_time_spans WHERE task_id=? AND ended IS NULL",
|
|
98
|
+
)
|
|
99
|
+
.get(task.id)?.engaged_ms || 0;
|
|
100
|
+
estimate.plannedMs = Math.min(
|
|
101
|
+
estimate.plannedMs,
|
|
102
|
+
Math.max(MINUTE, this.time.focusMs(task) - used),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
version: 1,
|
|
107
|
+
phase: "engaged",
|
|
108
|
+
...estimate,
|
|
109
|
+
baselineMs: this.committed(task.id),
|
|
110
|
+
started: now,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
view(task, now = this.time.now()) {
|
|
114
|
+
const clock = task.checkpoint?.activityClock;
|
|
115
|
+
if (!clock)
|
|
116
|
+
return {
|
|
117
|
+
phase:
|
|
118
|
+
!this.time.fixtureImmediate &&
|
|
119
|
+
this.time.operationElapsed(task.id, now) > 0
|
|
120
|
+
? "legacy"
|
|
121
|
+
: "unstarted",
|
|
122
|
+
elapsedMs: this.time.fixtureImmediate
|
|
123
|
+
? this.time.operationElapsed(task.id, now)
|
|
124
|
+
: null,
|
|
125
|
+
plannedMs: null,
|
|
126
|
+
};
|
|
127
|
+
const elapsedMs = this.elapsed(task, now),
|
|
128
|
+
stepMs = Math.max(0, elapsedMs - clock.baselineMs);
|
|
129
|
+
return {
|
|
130
|
+
phase: clock.phase,
|
|
131
|
+
elapsedMs,
|
|
132
|
+
stepMs,
|
|
133
|
+
plannedMs: clock.plannedMs,
|
|
134
|
+
remainingMs: Math.max(0, Math.round(clock.plannedMs - stepMs)),
|
|
135
|
+
progress: clock.plannedMs > 0 ? Math.min(1, stepMs / clock.plannedMs) : 0,
|
|
136
|
+
referenceMinutes: clock.referenceMinutes,
|
|
137
|
+
speed: clock.speed,
|
|
138
|
+
basis: clock.basis,
|
|
139
|
+
chapterMinutes: clock.chapterMinutes,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
remaining(task, now) {
|
|
143
|
+
return this.view(task, now).remainingMs || 0;
|
|
144
|
+
}
|
|
145
|
+
release(task, checkpoint, now) {
|
|
146
|
+
const slot = checkpoint.schedule,
|
|
147
|
+
clock = checkpoint.activityClock;
|
|
148
|
+
if (
|
|
149
|
+
task.activity !== "game" &&
|
|
150
|
+
slot?.chosenAt &&
|
|
151
|
+
clock.phase === "engaged"
|
|
152
|
+
) {
|
|
153
|
+
const base = Math.max(clock.baselineMs, slot.baselineMs || 0);
|
|
154
|
+
const remainingAtStart = Math.max(
|
|
155
|
+
0,
|
|
156
|
+
clock.plannedMs - (base - clock.baselineMs),
|
|
157
|
+
);
|
|
158
|
+
const quota = Math.max(
|
|
159
|
+
MINUTE,
|
|
160
|
+
this.time.focusMs(task) - Math.max(0, base - (slot.baselineMs || 0)),
|
|
161
|
+
);
|
|
162
|
+
if (remainingAtStart > quota)
|
|
163
|
+
checkpoint = {
|
|
164
|
+
...checkpoint,
|
|
165
|
+
activityClock: {
|
|
166
|
+
...clock,
|
|
167
|
+
originalEstimateMs: clock.originalEstimateMs || clock.plannedMs,
|
|
168
|
+
baselineMs: base,
|
|
169
|
+
plannedMs: quota,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
this.db
|
|
174
|
+
.prepare(
|
|
175
|
+
"UPDATE mind_time_spans SET updated=? WHERE task_id=? AND ended IS NULL",
|
|
176
|
+
)
|
|
177
|
+
.run(now, task.id);
|
|
178
|
+
const rest = this.time.focusMs(task),
|
|
179
|
+
remaining = Math.max(
|
|
180
|
+
0,
|
|
181
|
+
Math.round(
|
|
182
|
+
checkpoint.activityClock.plannedMs -
|
|
183
|
+
(this.committed(task.id) - checkpoint.activityClock.baselineMs),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
this.db
|
|
187
|
+
.prepare(
|
|
188
|
+
"UPDATE mind_time_tasks SET checkpoint=?,next_step=?,lease=NULL,lease_at=NULL,revision=revision+1 WHERE id=?",
|
|
189
|
+
)
|
|
190
|
+
.run(
|
|
191
|
+
JSON.stringify(checkpoint),
|
|
192
|
+
now + Math.min(rest, remaining),
|
|
193
|
+
task.id,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { dayKey, zonedTime, parse } from "../util.js";
|
|
2
|
+
import { activityPresentation } from "./presentation.js";
|
|
3
|
+
import { executionBlock } from "./availability.js";
|
|
4
|
+
|
|
5
|
+
// A view of saved choices and actual spans. Reading it never makes a plan.
|
|
6
|
+
export class Agenda {
|
|
7
|
+
constructor(time) {
|
|
8
|
+
this.time = time;
|
|
9
|
+
this.db = time.db;
|
|
10
|
+
}
|
|
11
|
+
blocked(task, now) {
|
|
12
|
+
if (task.state === "paused" && task.next_step >= Number.MAX_SAFE_INTEGER)
|
|
13
|
+
return task.wait_reason || "自己选择暂停,等待重新安排";
|
|
14
|
+
return executionBlock(this.time, task, now);
|
|
15
|
+
}
|
|
16
|
+
view(now = this.time.now()) {
|
|
17
|
+
const time = this.time,
|
|
18
|
+
zone = time.mind.timeZone(),
|
|
19
|
+
date = dayKey(now, zone);
|
|
20
|
+
const nextDate = new Date(Date.parse(date + "T12:00:00Z") + 86400000)
|
|
21
|
+
.toISOString()
|
|
22
|
+
.slice(0, 10);
|
|
23
|
+
const start = zonedTime(date, zone),
|
|
24
|
+
end = zonedTime(nextDate, zone),
|
|
25
|
+
today = time.today(now),
|
|
26
|
+
primary = time.primary();
|
|
27
|
+
const actual = today.spans
|
|
28
|
+
.map((s) => {
|
|
29
|
+
const rename = this.db
|
|
30
|
+
.prepare(
|
|
31
|
+
"SELECT data FROM mind_time_events WHERE task_id=? AND kind='self-scheduled' AND created>=? ORDER BY created,id LIMIT 1",
|
|
32
|
+
)
|
|
33
|
+
.get(s.task_id, s.started);
|
|
34
|
+
const title = parse(rename?.data, {}).previousTitle || s.title;
|
|
35
|
+
return {
|
|
36
|
+
id: s.id,
|
|
37
|
+
taskId: s.task_id,
|
|
38
|
+
title,
|
|
39
|
+
activity: s.activity,
|
|
40
|
+
start: Math.max(start, s.started),
|
|
41
|
+
end: Math.min(now, s.ended ?? s.updated),
|
|
42
|
+
engagedMs: s.clocked || time.fixtureImmediate ? s.todayMs : null,
|
|
43
|
+
ongoing: !s.ended && primary?.id === s.task_id,
|
|
44
|
+
kind: "actual",
|
|
45
|
+
label:
|
|
46
|
+
!s.ended && primary?.id === s.task_id
|
|
47
|
+
? primary.checkpoint.activityClock?.phase === "preparing"
|
|
48
|
+
? "准备中"
|
|
49
|
+
: "正在做"
|
|
50
|
+
: s.clocked && !s.todayMs
|
|
51
|
+
? "准备时段"
|
|
52
|
+
: "已发生",
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
.filter((s) => s.end > s.start);
|
|
56
|
+
const planned = [],
|
|
57
|
+
waiting = [],
|
|
58
|
+
unarranged = [],
|
|
59
|
+
paused = [];
|
|
60
|
+
const rows = this.db
|
|
61
|
+
.prepare(
|
|
62
|
+
"SELECT id FROM mind_time_tasks WHERE created<=? AND state IN ('todo','scheduled','doing','paused','waiting') AND json_extract(checkpoint,'$.mergedInto') IS NULL ORDER BY priority DESC,created LIMIT 100",
|
|
63
|
+
)
|
|
64
|
+
.all(now);
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
const task = time.tasks.get(row.id),
|
|
67
|
+
view = activityPresentation(task),
|
|
68
|
+
slot = task.checkpoint.schedule;
|
|
69
|
+
const why = this.blocked(task, now);
|
|
70
|
+
if (task.state === "paused") {
|
|
71
|
+
if (paused.length < 12)
|
|
72
|
+
paused.push({
|
|
73
|
+
id: task.id,
|
|
74
|
+
title: view.title,
|
|
75
|
+
reason: task.wait_reason || "暂时停下来,进度已保存",
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (why || task.state === "waiting") {
|
|
80
|
+
if (waiting.length < 12)
|
|
81
|
+
waiting.push({
|
|
82
|
+
id: task.id,
|
|
83
|
+
title: view.title,
|
|
84
|
+
reason: why || task.wait_reason,
|
|
85
|
+
retryAt:
|
|
86
|
+
task.next_step < Number.MAX_SAFE_INTEGER ? task.next_step : null,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
!slot?.chosenAt ||
|
|
92
|
+
task.checkpoint.reschedule ||
|
|
93
|
+
(slot.endedAt <= now && task.state !== "doing")
|
|
94
|
+
) {
|
|
95
|
+
if (unarranged.length < 12)
|
|
96
|
+
unarranged.push({
|
|
97
|
+
id: task.id,
|
|
98
|
+
title: view.title,
|
|
99
|
+
reason: slot?.chosenAt
|
|
100
|
+
? "原安排已过去,等她重新选时间"
|
|
101
|
+
: "事情已经记下,等她选择几点做、做多久",
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (slot.proposedAt >= end || slot.endedAt <= start) {
|
|
106
|
+
if (unarranged.length < 12)
|
|
107
|
+
unarranged.push({
|
|
108
|
+
id: task.id,
|
|
109
|
+
title: view.title,
|
|
110
|
+
reason: "安排在今天之外",
|
|
111
|
+
startAt: slot.proposedAt,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
planned.push({
|
|
116
|
+
id: "slot:" + task.id,
|
|
117
|
+
kind: "activity",
|
|
118
|
+
taskId: task.id,
|
|
119
|
+
title: view.title,
|
|
120
|
+
activity: task.activity,
|
|
121
|
+
kindOfTask: task.kind,
|
|
122
|
+
priority: task.priority,
|
|
123
|
+
start: Math.max(start, slot.proposedAt),
|
|
124
|
+
end: Math.min(end, slot.endedAt),
|
|
125
|
+
chosenStart: slot.proposedAt,
|
|
126
|
+
chosenEnd: slot.endedAt,
|
|
127
|
+
durationMinutes: slot.durationMinutes,
|
|
128
|
+
why: slot.reason || view.why,
|
|
129
|
+
label:
|
|
130
|
+
task.state === "doing"
|
|
131
|
+
? "正在执行这份安排"
|
|
132
|
+
: slot.chosenBy === "admin"
|
|
133
|
+
? "管理台安排"
|
|
134
|
+
: "她选定的安排",
|
|
135
|
+
estimated: false,
|
|
136
|
+
scope: "这次投入",
|
|
137
|
+
state: task.state,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const rhythm = time.mind.nature.current(now).rhythm;
|
|
141
|
+
if (rhythm?.enabled) {
|
|
142
|
+
const bed = zonedTime(date + " " + rhythm.sleep, zone),
|
|
143
|
+
wake = zonedTime(date + " " + rhythm.wake, zone);
|
|
144
|
+
for (const [a, b] of bed < wake
|
|
145
|
+
? [[bed, wake]]
|
|
146
|
+
: [
|
|
147
|
+
[start, wake],
|
|
148
|
+
[bed, end],
|
|
149
|
+
])
|
|
150
|
+
if (b > now)
|
|
151
|
+
planned.push({
|
|
152
|
+
id: "sleep:" + a,
|
|
153
|
+
kind: "sleep",
|
|
154
|
+
title: "作息中的睡眠时间",
|
|
155
|
+
start: Math.max(now, a),
|
|
156
|
+
end: b,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const block of planned.filter((b) => b.taskId)) {
|
|
160
|
+
const urgent = planned
|
|
161
|
+
.filter(
|
|
162
|
+
(b) =>
|
|
163
|
+
b.taskId &&
|
|
164
|
+
b.taskId !== block.taskId &&
|
|
165
|
+
b.priority > block.priority &&
|
|
166
|
+
b.start < block.end &&
|
|
167
|
+
b.end > block.start,
|
|
168
|
+
)
|
|
169
|
+
.sort((a, b) => a.start - b.start)[0];
|
|
170
|
+
const bedtime = planned
|
|
171
|
+
.filter(
|
|
172
|
+
(b) =>
|
|
173
|
+
b.kind === "sleep" && b.start < block.end && b.end > block.start,
|
|
174
|
+
)
|
|
175
|
+
.sort((a, b) => a.start - b.start)[0];
|
|
176
|
+
const stop = Math.min(
|
|
177
|
+
urgent ? Math.max(block.start, urgent.start) : block.end,
|
|
178
|
+
bedtime ? Math.max(block.start, bedtime.start) : block.end,
|
|
179
|
+
);
|
|
180
|
+
block.chartEnd = stop;
|
|
181
|
+
if (stop < block.end) {
|
|
182
|
+
block.interruptedAt = stop;
|
|
183
|
+
block.interruption =
|
|
184
|
+
bedtime && stop === bedtime.start ? "sleep" : "priority";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
planned.sort(
|
|
188
|
+
(a, b) => a.start - b.start || (b.priority || 0) - (a.priority || 0),
|
|
189
|
+
);
|
|
190
|
+
const next =
|
|
191
|
+
planned
|
|
192
|
+
.filter((b) => b.taskId && b.state !== "doing" && b.chosenStart > now)
|
|
193
|
+
.sort((a, b) => a.chosenStart - b.chosenStart)[0] || null;
|
|
194
|
+
const life = time.search.life,
|
|
195
|
+
planning = life?.planner?.working || life?.ownDay?.working,
|
|
196
|
+
phase = life?.phase(now);
|
|
197
|
+
const idle = {
|
|
198
|
+
title: primary
|
|
199
|
+
? "有主活动在继续"
|
|
200
|
+
: planning
|
|
201
|
+
? "正在想自己的安排"
|
|
202
|
+
: phase?.key === "asleep"
|
|
203
|
+
? "在睡觉"
|
|
204
|
+
: next
|
|
205
|
+
? "这会儿还没有主活动"
|
|
206
|
+
: "这会儿没有正在推进的主活动",
|
|
207
|
+
reason: primary
|
|
208
|
+
? "普通交流可以伴随进行"
|
|
209
|
+
: planning
|
|
210
|
+
? "把想做的事、开始时间和投入时长想清楚"
|
|
211
|
+
: phase?.key === "asleep"
|
|
212
|
+
? "醒来后再接着自己的安排"
|
|
213
|
+
: next
|
|
214
|
+
? "下一项已经选好时间,之前可以聊天、歇着或随手折腾"
|
|
215
|
+
: paused.length
|
|
216
|
+
? "有活动保留了续接位置,之后由她重新安排"
|
|
217
|
+
: unarranged.length
|
|
218
|
+
? "待办已经记下,等她选择具体时间"
|
|
219
|
+
: "可以自己找件小事做,也可以选择歇着",
|
|
220
|
+
next,
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
date,
|
|
224
|
+
zone,
|
|
225
|
+
start,
|
|
226
|
+
end,
|
|
227
|
+
now,
|
|
228
|
+
actual,
|
|
229
|
+
planned,
|
|
230
|
+
waiting,
|
|
231
|
+
unarranged,
|
|
232
|
+
paused,
|
|
233
|
+
idle,
|
|
234
|
+
interactions: today.interactions.map((p) => ({
|
|
235
|
+
id: p.id,
|
|
236
|
+
at: p.created,
|
|
237
|
+
taskId: p.task_id,
|
|
238
|
+
})),
|
|
239
|
+
note: "下方是她已选定的开始时间和这次想投入的时长。实际经过单独记录;提前完成、暂停或优先级切换后会保留原安排并重新选择。空白不代表已经在做什么。",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|