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,507 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { localClock } from "../core/conversation-cues.js";
|
|
3
|
+
import { hasCredential } from "./guard.js";
|
|
4
|
+
import { isPrivateSession } from "./memory.js";
|
|
5
|
+
import { lifeSpan } from "./nature.js";
|
|
6
|
+
import { classifyActivity, isExecutable } from "./time/tasks.js";
|
|
7
|
+
import { taskIntent } from "./time/intent.js";
|
|
8
|
+
import { DAY, evidence, parse, similar, text, zonedTime } from "./util.js";
|
|
9
|
+
|
|
10
|
+
// What she is looking ahead to: promises she made, things she means to do,
|
|
11
|
+
// what others told her is coming, and the dates that come back every year.
|
|
12
|
+
export const ANTICIPATION_KINDS = {
|
|
13
|
+
promise: "答应的事",
|
|
14
|
+
plan: "想做的事",
|
|
15
|
+
event: "别人的安排",
|
|
16
|
+
date: "每年的日子",
|
|
17
|
+
};
|
|
18
|
+
// How long after its day something still waits to be asked about or done
|
|
19
|
+
// before it quietly counts as missed.
|
|
20
|
+
const GRACE = { event: 3 * DAY, promise: 7 * DAY, plan: 7 * DAY, date: 0 };
|
|
21
|
+
const HORIZON = 400 * DAY;
|
|
22
|
+
const CLOSE = new Set(["done", "missed", "let_go"]);
|
|
23
|
+
|
|
24
|
+
function row(value) {
|
|
25
|
+
return value
|
|
26
|
+
? {
|
|
27
|
+
...value,
|
|
28
|
+
sources: parse(value.sources, []),
|
|
29
|
+
closed_sources: parse(value.closed_sources, []),
|
|
30
|
+
}
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Anticipations {
|
|
35
|
+
constructor(mind) {
|
|
36
|
+
this.mind = mind;
|
|
37
|
+
this.db = mind.db;
|
|
38
|
+
}
|
|
39
|
+
day(time) {
|
|
40
|
+
return localClock(time, this.mind.timeZone()).local.slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
// A yearly date's next return (or today's), otherwise the one due moment.
|
|
43
|
+
occurrence(a, now) {
|
|
44
|
+
if (a.recurrence !== "yearly") return a.due_at;
|
|
45
|
+
const monthDay = this.day(a.due_at).slice(5);
|
|
46
|
+
const year = Number(this.day(now).slice(0, 4));
|
|
47
|
+
for (const y of [year - 1, year, year + 1]) {
|
|
48
|
+
const at = zonedTime(`${y}-${monthDay}`, this.mind.timeZone());
|
|
49
|
+
if (at !== null && at + DAY > now) return at;
|
|
50
|
+
}
|
|
51
|
+
return a.due_at;
|
|
52
|
+
}
|
|
53
|
+
ends(a, due) {
|
|
54
|
+
return due + (a.due_precision === "time" ? 0 : DAY);
|
|
55
|
+
}
|
|
56
|
+
// From the start of the day before.
|
|
57
|
+
opens(due) {
|
|
58
|
+
const midnight = zonedTime(this.day(due), this.mind.timeZone());
|
|
59
|
+
return (midnight ?? due) - DAY;
|
|
60
|
+
}
|
|
61
|
+
// Her name for someone, or the name they last used where it was said.
|
|
62
|
+
who(a) {
|
|
63
|
+
if (!a.subject) return "";
|
|
64
|
+
const known = this.mind.bonds.name(a.subject);
|
|
65
|
+
if (known) return known;
|
|
66
|
+
const said = parse(
|
|
67
|
+
this.db
|
|
68
|
+
.prepare(
|
|
69
|
+
"SELECT payload FROM core_events WHERE session_id=? AND role='user' AND json_extract(payload,'$.userId')=? ORDER BY seq DESC LIMIT 1",
|
|
70
|
+
)
|
|
71
|
+
.get(a.session_id, String(a.subject))?.payload,
|
|
72
|
+
{},
|
|
73
|
+
);
|
|
74
|
+
return said.name || a.subject;
|
|
75
|
+
}
|
|
76
|
+
lapsed(a, now) {
|
|
77
|
+
return (
|
|
78
|
+
a.recurrence !== "yearly" &&
|
|
79
|
+
now > this.ends(a, a.due_at) + (GRACE[a.kind] ?? GRACE.event)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
// A private promise can be written in her diary. It cannot be carried out
|
|
83
|
+
// of that room, or into the story she retells everywhere.
|
|
84
|
+
concealed(a) {
|
|
85
|
+
return a.discretion !== "open" || isPrivateSession(a.session_id);
|
|
86
|
+
}
|
|
87
|
+
// Private rooms whose plans, promises, or dates the diary of `day` would see.
|
|
88
|
+
privateRooms(day, before = Date.now()) {
|
|
89
|
+
const nature = this.mind.nature.current(before);
|
|
90
|
+
const zone = this.mind.timeZone();
|
|
91
|
+
const span = lifeSpan(nature, day, zone);
|
|
92
|
+
if (!span || span.start > before) return [];
|
|
93
|
+
const end = Math.min(before, span.end);
|
|
94
|
+
const rooms = new Set();
|
|
95
|
+
for (const a of this.db
|
|
96
|
+
.prepare(
|
|
97
|
+
"SELECT * FROM mind_anticipations WHERE created<=? AND status!='revoked'",
|
|
98
|
+
)
|
|
99
|
+
.all(before)
|
|
100
|
+
.map(row)) {
|
|
101
|
+
if (!this.concealed(a) || !this.#onDay(a, span, end)) continue;
|
|
102
|
+
// The room where it was said may still see that day's diary. A marker
|
|
103
|
+
// with no room would hide the diary everywhere, including there.
|
|
104
|
+
rooms.add(a.session_id || `a:${a.id}`);
|
|
105
|
+
}
|
|
106
|
+
return [...rooms];
|
|
107
|
+
}
|
|
108
|
+
#onDay(a, span, before) {
|
|
109
|
+
const pending =
|
|
110
|
+
a.status === "pending" || (a.closed_at != null && a.closed_at > before);
|
|
111
|
+
if (
|
|
112
|
+
!pending &&
|
|
113
|
+
a.closed_at >= span.start &&
|
|
114
|
+
a.closed_at < span.end &&
|
|
115
|
+
a.closed_at <= before
|
|
116
|
+
)
|
|
117
|
+
return true;
|
|
118
|
+
if (a.recurrence === "yearly")
|
|
119
|
+
return this.day(this.occurrence(a, span.start)) === this.day(span.start);
|
|
120
|
+
if (a.due_at >= span.start && a.due_at < span.end) return true;
|
|
121
|
+
const lapse = this.ends(a, a.due_at) + (GRACE[a.kind] ?? GRACE.event);
|
|
122
|
+
return pending && lapse >= span.start && lapse < span.end;
|
|
123
|
+
}
|
|
124
|
+
revokedContents() {
|
|
125
|
+
return this.db
|
|
126
|
+
.prepare(
|
|
127
|
+
"SELECT content FROM mind_revocations WHERE target_kind='anticipation'",
|
|
128
|
+
)
|
|
129
|
+
.all()
|
|
130
|
+
.map((r) => r.content)
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
add({
|
|
134
|
+
kind,
|
|
135
|
+
subject = null,
|
|
136
|
+
session = null,
|
|
137
|
+
content,
|
|
138
|
+
due,
|
|
139
|
+
recurrence = "none",
|
|
140
|
+
discretion = "open",
|
|
141
|
+
sources = [],
|
|
142
|
+
origin = "memory",
|
|
143
|
+
activity = "",
|
|
144
|
+
time = Date.now(),
|
|
145
|
+
}) {
|
|
146
|
+
if (!ANTICIPATION_KINDS[kind]) return { rejected: "类型无效" };
|
|
147
|
+
const words = text(content, 120);
|
|
148
|
+
if (!words || hasCredential(words)) return { rejected: "内容无效" };
|
|
149
|
+
const refs = evidence(sources);
|
|
150
|
+
if (!refs.length) return { rejected: "缺少来源" };
|
|
151
|
+
const when = String(due ?? "").trim();
|
|
152
|
+
const at = zonedTime(when, this.mind.timeZone());
|
|
153
|
+
if (at === null) return { rejected: "日期无效" };
|
|
154
|
+
const yearly = kind === "date" || recurrence === "yearly";
|
|
155
|
+
const precision = /\d{2}:\d{2}$/.test(when) ? "time" : "day";
|
|
156
|
+
if (
|
|
157
|
+
!yearly &&
|
|
158
|
+
(this.ends({ due_precision: precision }, at) <= time - DAY ||
|
|
159
|
+
at > time + HORIZON)
|
|
160
|
+
)
|
|
161
|
+
return { rejected: "日期不在合理范围" };
|
|
162
|
+
const who = subject ? String(subject) : "";
|
|
163
|
+
if (
|
|
164
|
+
this.revokedContents().some((old) =>
|
|
165
|
+
similar(old, `${who}\n${words}`, 0.75),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
return { rejected: "与撤销过的内容相同" };
|
|
169
|
+
if (!yearly && ["plan", "promise"].includes(kind)) {
|
|
170
|
+
const input = {
|
|
171
|
+
title: words,
|
|
172
|
+
activity: activity || classifyActivity(words),
|
|
173
|
+
subject,
|
|
174
|
+
session,
|
|
175
|
+
sources: refs,
|
|
176
|
+
},
|
|
177
|
+
intent = taskIntent(this.db, input);
|
|
178
|
+
if (intent.key) {
|
|
179
|
+
const tracked = this.mind.time?.tasks.links.find(input);
|
|
180
|
+
const prior = tracked
|
|
181
|
+
? this.mind.time.tasks.links
|
|
182
|
+
.anticipations(tracked)
|
|
183
|
+
.map((id) => this.get(id))
|
|
184
|
+
.find((a) => a && a.status !== "revoked")
|
|
185
|
+
: this.db
|
|
186
|
+
.prepare(
|
|
187
|
+
"SELECT * FROM mind_anticipations WHERE kind IN ('plan','promise') AND status='pending' ORDER BY created",
|
|
188
|
+
)
|
|
189
|
+
.all()
|
|
190
|
+
.find(
|
|
191
|
+
(a) =>
|
|
192
|
+
taskIntent(this.db, {
|
|
193
|
+
title: a.content,
|
|
194
|
+
activity: a.activity || classifyActivity(a.content),
|
|
195
|
+
subject: a.subject,
|
|
196
|
+
session: a.session_id,
|
|
197
|
+
sources: parse(a.sources, []),
|
|
198
|
+
}).key === intent.key,
|
|
199
|
+
);
|
|
200
|
+
if (tracked || prior) {
|
|
201
|
+
if (tracked) {
|
|
202
|
+
this.mind.time.tasks.links.mergeSource(
|
|
203
|
+
tracked,
|
|
204
|
+
{
|
|
205
|
+
sources: refs,
|
|
206
|
+
kind,
|
|
207
|
+
subject: who || intent.recipient,
|
|
208
|
+
dueAt: at,
|
|
209
|
+
duePrecision: precision,
|
|
210
|
+
origin,
|
|
211
|
+
},
|
|
212
|
+
time,
|
|
213
|
+
);
|
|
214
|
+
if (tracked.state === "abandoned")
|
|
215
|
+
this.mind.time.tasks.links.close(
|
|
216
|
+
tracked,
|
|
217
|
+
"原事项已放下,重复整理不能恢复",
|
|
218
|
+
time,
|
|
219
|
+
);
|
|
220
|
+
} else
|
|
221
|
+
this.db
|
|
222
|
+
.prepare("UPDATE mind_anticipations SET sources=? WHERE id=?")
|
|
223
|
+
.run(
|
|
224
|
+
JSON.stringify(
|
|
225
|
+
evidence([...parse(prior.sources, []), ...refs]),
|
|
226
|
+
),
|
|
227
|
+
prior.id,
|
|
228
|
+
);
|
|
229
|
+
return {
|
|
230
|
+
duplicate: prior?.id || tracked.id,
|
|
231
|
+
task: tracked?.id,
|
|
232
|
+
suppressed: tracked?.state === "abandoned",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const key = yearly ? this.day(at).slice(5) : this.day(at);
|
|
238
|
+
const same = this.db
|
|
239
|
+
.prepare(
|
|
240
|
+
"SELECT * FROM mind_anticipations WHERE status='pending' AND COALESCE(subject,'')=? AND kind=?",
|
|
241
|
+
)
|
|
242
|
+
.all(who, kind)
|
|
243
|
+
.find(
|
|
244
|
+
(r) =>
|
|
245
|
+
(yearly ? this.day(r.due_at).slice(5) : this.day(r.due_at)) === key &&
|
|
246
|
+
similar(r.content, words, 0.6),
|
|
247
|
+
);
|
|
248
|
+
if (same) return { duplicate: same.id };
|
|
249
|
+
const id = randomUUID();
|
|
250
|
+
this.db
|
|
251
|
+
.prepare(
|
|
252
|
+
"INSERT INTO mind_anticipations(id,created,kind,subject,session_id,content,due_at,due_precision,recurrence,discretion,sources,origin,activity) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
253
|
+
)
|
|
254
|
+
.run(
|
|
255
|
+
id,
|
|
256
|
+
time,
|
|
257
|
+
kind,
|
|
258
|
+
who || null,
|
|
259
|
+
session,
|
|
260
|
+
words,
|
|
261
|
+
at,
|
|
262
|
+
precision,
|
|
263
|
+
yearly ? "yearly" : "none",
|
|
264
|
+
["open", "private", "secret"].includes(discretion)
|
|
265
|
+
? discretion
|
|
266
|
+
: "open",
|
|
267
|
+
JSON.stringify(refs),
|
|
268
|
+
origin,
|
|
269
|
+
kind === "plan" && isExecutable(activity) ? activity : "",
|
|
270
|
+
);
|
|
271
|
+
return { id };
|
|
272
|
+
}
|
|
273
|
+
get(id) {
|
|
274
|
+
return row(
|
|
275
|
+
this.db.prepare("SELECT * FROM mind_anticipations WHERE id=?").get(id),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
// Still open at `now`; something closed later still counts when looking back.
|
|
279
|
+
pending(now = Date.now()) {
|
|
280
|
+
return this.db
|
|
281
|
+
.prepare(
|
|
282
|
+
"SELECT * FROM mind_anticipations WHERE created<=? AND (status='pending' OR (closed_at IS NOT NULL AND closed_at>? AND status!='revoked')) ORDER BY due_at",
|
|
283
|
+
)
|
|
284
|
+
.all(now, now)
|
|
285
|
+
.map(row);
|
|
286
|
+
}
|
|
287
|
+
close(id, { status, note = "", sources = [], time = Date.now() } = {}) {
|
|
288
|
+
if (!CLOSE.has(status)) return false;
|
|
289
|
+
const a = this.get(id);
|
|
290
|
+
if (!a || a.status !== "pending" || a.recurrence === "yearly") return false;
|
|
291
|
+
if (status === "done") {
|
|
292
|
+
if (
|
|
293
|
+
["plan", "promise"].includes(a.kind) &&
|
|
294
|
+
(a.activity || classifyActivity(a.content) !== "unknown")
|
|
295
|
+
)
|
|
296
|
+
this.mind.time?.tasks.sync(time);
|
|
297
|
+
const task = this.db
|
|
298
|
+
.prepare(
|
|
299
|
+
"SELECT state,share_state FROM mind_time_tasks WHERE anticipation_id=?",
|
|
300
|
+
)
|
|
301
|
+
.get(id);
|
|
302
|
+
if (
|
|
303
|
+
task &&
|
|
304
|
+
(task.state !== "done" || !["none", "sent"].includes(task.share_state))
|
|
305
|
+
)
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return (
|
|
309
|
+
this.db
|
|
310
|
+
.prepare(
|
|
311
|
+
"UPDATE mind_anticipations SET status=?,closed_at=?,closed_note=?,closed_sources=? WHERE id=? AND status='pending'",
|
|
312
|
+
)
|
|
313
|
+
.run(
|
|
314
|
+
status,
|
|
315
|
+
time,
|
|
316
|
+
text(note, 120),
|
|
317
|
+
JSON.stringify(evidence(sources)),
|
|
318
|
+
id,
|
|
319
|
+
).changes > 0
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
revoke(id, reason = "") {
|
|
323
|
+
const a = this.get(id);
|
|
324
|
+
if (!a) throw Error("约定不存在");
|
|
325
|
+
this.db
|
|
326
|
+
.prepare(
|
|
327
|
+
"UPDATE mind_anticipations SET status='revoked',closed_at=?,closed_note=? WHERE id=?",
|
|
328
|
+
)
|
|
329
|
+
.run(Date.now(), text(reason, 120), id);
|
|
330
|
+
return `${a.subject || ""}\n${a.content}`;
|
|
331
|
+
}
|
|
332
|
+
relative(due, now, precision = "day") {
|
|
333
|
+
const zone = this.mind.timeZone();
|
|
334
|
+
const at = (t) =>
|
|
335
|
+
Date.parse(`${localClock(t, zone).local.slice(0, 10)}T00:00:00Z`);
|
|
336
|
+
const days = Math.round((at(due) - at(now)) / DAY);
|
|
337
|
+
const clock =
|
|
338
|
+
precision === "time" ? ` ${localClock(due, zone).local.slice(11)}` : "";
|
|
339
|
+
const named = {
|
|
340
|
+
0: "今天",
|
|
341
|
+
1: "明天",
|
|
342
|
+
2: "后天",
|
|
343
|
+
[-1]: "昨天",
|
|
344
|
+
[-2]: "前天",
|
|
345
|
+
};
|
|
346
|
+
if (named[days] !== undefined) return named[days] + clock;
|
|
347
|
+
return days > 0 ? `${days} 天后` : `${-days} 天前`;
|
|
348
|
+
}
|
|
349
|
+
label(a, due, now) {
|
|
350
|
+
const who = this.who(a);
|
|
351
|
+
const when = this.relative(due, now, a.due_precision);
|
|
352
|
+
if (a.kind === "promise") {
|
|
353
|
+
const task = this.db
|
|
354
|
+
.prepare(
|
|
355
|
+
"SELECT state,share_state,checkpoint FROM mind_time_tasks WHERE anticipation_id=?",
|
|
356
|
+
)
|
|
357
|
+
.get(a.id);
|
|
358
|
+
const progress =
|
|
359
|
+
task?.state === "done" && task.share_state !== "sent"
|
|
360
|
+
? "作品完成,尚未交付"
|
|
361
|
+
: task?.state === "doing"
|
|
362
|
+
? "正在做"
|
|
363
|
+
: parse(task?.checkpoint, {}).workId
|
|
364
|
+
? "已有草稿,尚未完成"
|
|
365
|
+
: "还没做";
|
|
366
|
+
return `我答应${who ? `${who}` : ""}:${a.content}(${when}${a.status === "pending" ? "," + progress : ""})`;
|
|
367
|
+
}
|
|
368
|
+
if (a.kind === "plan") return `我打算:${a.content}(${when})`;
|
|
369
|
+
if (a.kind === "event")
|
|
370
|
+
return `${who ? `${who}计划:` : "有人计划:"}${a.content}(${when},结果未确认)`;
|
|
371
|
+
return `${who ? `${who}:` : ""}${a.content}(${when})`;
|
|
372
|
+
}
|
|
373
|
+
// Near enough to matter in a conversation: from the day before until a few
|
|
374
|
+
// days after, and only where it belongs.
|
|
375
|
+
upcoming({ now = Date.now(), people = [], session = "", limit = 3 } = {}) {
|
|
376
|
+
const here = new Set(session ? this.mind.memory.scopes(session) : []);
|
|
377
|
+
const present = new Set(people.map(String));
|
|
378
|
+
const out = [];
|
|
379
|
+
for (const a of this.pending(now)) {
|
|
380
|
+
const due = this.occurrence(a, now);
|
|
381
|
+
const after =
|
|
382
|
+
a.recurrence === "yearly" ? 0 : (GRACE[a.kind] ?? GRACE.event);
|
|
383
|
+
if (now < this.opens(due) || now >= this.ends(a, due) + after) continue;
|
|
384
|
+
// A plan by someone else in this room belongs to that person. Its
|
|
385
|
+
// presence in the same group must not make it A's plan when B said it.
|
|
386
|
+
const relevant = a.subject
|
|
387
|
+
? present.has(String(a.subject))
|
|
388
|
+
: a.session_id === session || a.kind === "plan";
|
|
389
|
+
if (!relevant) continue;
|
|
390
|
+
const local = here.has(a.session_id);
|
|
391
|
+
if (a.discretion !== "open" && !local) continue;
|
|
392
|
+
out.push({ id: a.id, due, text: this.label(a, due, now) });
|
|
393
|
+
if (out.length >= limit) break;
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
// What she might think about when alone: due soon, just past, or quietly
|
|
398
|
+
// missed within the last week.
|
|
399
|
+
// What is still ahead comes first, soonest first. Then what was due but still
|
|
400
|
+
// waits (inside its grace), then what already lapsed; both the most recent
|
|
401
|
+
// first, so a week-old plan of someone else does not take the places from
|
|
402
|
+
// what is near now.
|
|
403
|
+
due({ now = Date.now(), limit = 5, shareable = false } = {}) {
|
|
404
|
+
const found = [];
|
|
405
|
+
for (const a of this.pending(now)) {
|
|
406
|
+
if (shareable && this.concealed(a)) continue;
|
|
407
|
+
const due = this.occurrence(a, now);
|
|
408
|
+
const after =
|
|
409
|
+
a.recurrence === "yearly"
|
|
410
|
+
? 0
|
|
411
|
+
: (GRACE[a.kind] ?? GRACE.event) + 7 * DAY;
|
|
412
|
+
if (now < this.opens(due) || now >= this.ends(a, due) + after) continue;
|
|
413
|
+
const lapsed = this.lapsed(a, now);
|
|
414
|
+
// 0: its day has not passed yet; 1: due, still inside the grace;
|
|
415
|
+
// 2: already lapsed.
|
|
416
|
+
const rank = lapsed ? 2 : this.ends(a, due) <= now ? 1 : 0;
|
|
417
|
+
found.push({
|
|
418
|
+
due,
|
|
419
|
+
rank,
|
|
420
|
+
item: {
|
|
421
|
+
ref: `a:${a.id}`,
|
|
422
|
+
kind: ANTICIPATION_KINDS[a.kind],
|
|
423
|
+
...(a.subject ? { who: this.who(a), userId: a.subject } : {}),
|
|
424
|
+
content: a.content,
|
|
425
|
+
when: this.relative(due, now, a.due_precision),
|
|
426
|
+
...(lapsed ? { lapsed: true } : {}),
|
|
427
|
+
...(a.recurrence === "yearly" ? { yearly: true } : {}),
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return found
|
|
432
|
+
.sort(
|
|
433
|
+
(a, b) => a.rank - b.rank || (a.rank ? b.due - a.due : a.due - b.due),
|
|
434
|
+
)
|
|
435
|
+
.slice(0, limit)
|
|
436
|
+
.map((entry) => entry.item);
|
|
437
|
+
}
|
|
438
|
+
// Something she was looking ahead to has come close, or just passed.
|
|
439
|
+
newlyDue(since, now = Date.now()) {
|
|
440
|
+
return this.pending(now).some((a) => {
|
|
441
|
+
const due = this.occurrence(a, now);
|
|
442
|
+
const edges = [this.opens(due), this.ends(a, due)];
|
|
443
|
+
return edges.some((edge) => edge > since && edge <= now);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// The day's promises kept and missed, and the dates that fall on it.
|
|
447
|
+
today({ start, end, shareable = false }) {
|
|
448
|
+
const keep = (a) => !shareable || !this.concealed(a);
|
|
449
|
+
const closed = this.db
|
|
450
|
+
.prepare(
|
|
451
|
+
"SELECT * FROM mind_anticipations WHERE closed_at>=? AND closed_at<? AND status IN ('done','missed','let_go') ORDER BY closed_at",
|
|
452
|
+
)
|
|
453
|
+
.all(start, end)
|
|
454
|
+
.map(row)
|
|
455
|
+
.filter(keep);
|
|
456
|
+
const lapsedToday = this.pending(end).filter((a) => {
|
|
457
|
+
if (!keep(a) || a.recurrence === "yearly") return false;
|
|
458
|
+
const at = this.ends(a, a.due_at) + (GRACE[a.kind] ?? GRACE.event);
|
|
459
|
+
return at >= start && at < end;
|
|
460
|
+
});
|
|
461
|
+
const line = (a) => this.label(a, this.occurrence(a, start), start);
|
|
462
|
+
const byStatus = (status) =>
|
|
463
|
+
closed.filter((a) => a.status === status).map(line);
|
|
464
|
+
return {
|
|
465
|
+
kept: byStatus("done"),
|
|
466
|
+
missed: [...byStatus("missed"), ...lapsedToday.map(line)],
|
|
467
|
+
letGo: byStatus("let_go"),
|
|
468
|
+
dates: this.pending(end)
|
|
469
|
+
.filter(
|
|
470
|
+
(a) =>
|
|
471
|
+
keep(a) &&
|
|
472
|
+
a.recurrence === "yearly" &&
|
|
473
|
+
this.day(this.occurrence(a, start)) === this.day(start),
|
|
474
|
+
)
|
|
475
|
+
.map(line),
|
|
476
|
+
dueToday: this.pending(end)
|
|
477
|
+
.filter(
|
|
478
|
+
(a) =>
|
|
479
|
+
keep(a) &&
|
|
480
|
+
a.recurrence !== "yearly" &&
|
|
481
|
+
a.due_at >= start &&
|
|
482
|
+
a.due_at < end,
|
|
483
|
+
)
|
|
484
|
+
.map(line),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
list({ now = Date.now(), limit = 100 } = {}) {
|
|
488
|
+
return this.db
|
|
489
|
+
.prepare(
|
|
490
|
+
"SELECT * FROM mind_anticipations ORDER BY CASE status WHEN 'pending' THEN 0 ELSE 1 END, due_at LIMIT ?",
|
|
491
|
+
)
|
|
492
|
+
.all(limit)
|
|
493
|
+
.map(row)
|
|
494
|
+
.map((a) => {
|
|
495
|
+
const due = this.occurrence(a, now);
|
|
496
|
+
return {
|
|
497
|
+
...a,
|
|
498
|
+
name: this.who(a),
|
|
499
|
+
occurrence: due,
|
|
500
|
+
when: this.relative(due, now, a.due_precision),
|
|
501
|
+
state:
|
|
502
|
+
a.status === "pending" && this.lapsed(a, now) ? "lapsed" : a.status,
|
|
503
|
+
private: a.discretion !== "open" || isPrivateSession(a.session_id),
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|