luckytri 1.0.6 → 1.0.7
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/LICENSE +21 -21
- package/docs/en/CHANGELOG.md +25 -16
- package/docs/en/chat-style.md +28 -9
- package/docs/en/guide.md +5 -5
- package/docs/en/her-life.md +2 -2
- package/docs/en/webui.md +1 -1
- package/docs/zh/CHANGELOG.md +25 -16
- package/docs/zh/chat-style.md +30 -9
- package/docs/zh/guide.md +5 -5
- package/docs/zh/her-life.md +2 -2
- package/docs/zh/webui.md +1 -1
- package/package.json +1 -1
- package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
- package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
- package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
- package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
- package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
- package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
- package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
- package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
- package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
- package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
- package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
- package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
- package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
- package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
- package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
- package/public/app/index.html +5 -6
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/scripts/import-data.js +128 -128
- package/scripts/proxy-config.js +167 -167
- package/scripts/update-instance.js +201 -201
- package/server/channels/adapters.js +27 -27
- package/server/channels/onebot/adapter.js +288 -288
- package/server/channels/qqbot/adapter.js +253 -253
- package/server/channels/qqbot/api.js +199 -199
- package/server/channels/qqbot/credentials.js +11 -11
- package/server/channels/qqbot/gateway.js +274 -274
- package/server/channels/qqbot/index.js +389 -389
- package/server/channels/segments.js +40 -40
- package/server/channels/session-key.js +153 -153
- package/server/channels/state.js +42 -42
- package/server/core/api.js +564 -564
- package/server/core/context-builder.js +288 -288
- package/server/core/context-compactor.js +454 -454
- package/server/core/conversation-cues.js +365 -325
- package/server/core/conversation-grounding.js +178 -0
- package/server/core/conversation-manager.js +87 -87
- package/server/core/dialogue-context.js +52 -0
- package/server/core/feedback.js +8 -8
- package/server/core/group-style.js +72 -72
- package/server/core/initiative-context.js +66 -66
- package/server/core/input-budget.js +32 -32
- package/server/core/local-demo.js +98 -98
- package/server/core/message-manager.js +118 -118
- package/server/core/message-scheduler.js +78 -78
- package/server/core/model-json.js +190 -190
- package/server/core/model-presets.js +900 -900
- package/server/core/name-call.js +29 -29
- package/server/core/network.js +157 -157
- package/server/core/person-name.js +22 -22
- package/server/core/persona-manager.js +324 -269
- package/server/core/reply-delivery.js +472 -404
- package/server/core/reply-target-resolver.js +50 -50
- package/server/core/response-generator.js +58 -50
- package/server/core/response-validator.js +30 -27
- package/server/core/retired-prompts.js +51 -51
- package/server/core/sessions.js +109 -109
- package/server/core/speaker-names.js +92 -92
- package/server/core/topic-tracker.js +65 -65
- package/server/core/turn.js +42 -8
- package/server/http.js +18 -18
- package/server/i18n/decisions.js +209 -209
- package/server/i18n/index.js +144 -144
- package/server/i18n/labels.js +0 -5
- package/server/i18n/models.js +111 -111
- package/server/knowledge/api.js +342 -342
- package/server/knowledge/embedding-profile.js +114 -114
- package/server/knowledge/index.js +13 -13
- package/server/knowledge/retrieval.js +93 -93
- package/server/knowledge/schema.js +158 -158
- package/server/knowledge/vector-index.js +90 -90
- package/server/mind/affect.js +265 -265
- package/server/mind/attention.js +200 -200
- package/server/mind/bonds.js +565 -565
- package/server/mind/clock.js +83 -83
- package/server/mind/context-selection.js +16 -4
- package/server/mind/continuity.js +562 -562
- package/server/mind/conversation-origin.js +175 -175
- package/server/mind/days.js +210 -210
- package/server/mind/evidence-read.js +45 -45
- package/server/mind/faces.js +373 -373
- package/server/mind/guard.js +68 -60
- package/server/mind/initiative.js +338 -338
- package/server/mind/life/activities.js +21 -21
- package/server/mind/life/context.js +330 -330
- package/server/mind/life/growth.js +261 -261
- package/server/mind/life/presence.js +305 -305
- package/server/mind/meetings.js +1069 -1069
- package/server/mind/memory-policy.js +48 -48
- package/server/mind/memory.js +888 -888
- package/server/mind/nature.js +358 -358
- package/server/mind/novelty.js +19 -19
- package/server/mind/periods.js +118 -118
- package/server/mind/reading.js +120 -120
- package/server/mind/relationship-context.js +71 -71
- package/server/mind/relationships.js +390 -390
- package/server/mind/salience.js +201 -201
- package/server/mind/schema.js +567 -567
- package/server/mind/self.js +496 -484
- package/server/mind/thoughts.js +274 -274
- package/server/mind/time/agenda.js +242 -242
- package/server/mind/time/api.js +200 -200
- package/server/mind/time/attention.js +34 -34
- package/server/mind/time/delivery-links.js +130 -130
- package/server/mind/time/games.js +7 -3
- package/server/mind/time/index.js +3 -0
- package/server/mind/time/model-material.js +77 -77
- package/server/mind/time/presentation.js +35 -51
- package/server/mind/time/schedule.js +83 -83
- package/server/mind/time/schema.js +84 -84
- package/server/mind/time/search.js +383 -383
- package/server/mind/time/sharing.js +472 -472
- package/server/mind/time/task-links.js +311 -311
- package/server/mind/time/works.js +438 -432
- package/server/mind/traits.js +191 -191
- package/server/mind/view.js +43 -6
- package/server/runtime/installation.js +116 -116
- package/server/runtime/update-manager.js +140 -140
- package/server/storage/data-package.js +298 -298
- package/server/storage/data-transfer-api.js +50 -50
- package/server/storage/data-transfer-worker.js +18 -18
- package/server/storage/data-transfer.js +416 -416
- package/server/studio/display-names.js +139 -139
- package/server/studio/management.js +102 -102
- package/server/studio/updates.js +144 -144
- package/server/version.js +10 -10
- package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
- package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
- package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
- package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
- package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
- package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
- package/public/app/assets/TimePage-C-guwPk2.css +0 -1
package/server/mind/nature.js
CHANGED
|
@@ -1,358 +1,358 @@
|
|
|
1
|
-
import { localClock } from "../core/conversation-cues.js";
|
|
2
|
-
import { zonedTime } from "./util.js";
|
|
3
|
-
|
|
4
|
-
// Nature is the seed she is born with. It is the only part of her the owner
|
|
5
|
-
// writes; everything else grows from experience.
|
|
6
|
-
export const PREVIOUS_NATURE_SEED = {
|
|
7
|
-
name: "LuckyTri",
|
|
8
|
-
gender: "female",
|
|
9
|
-
base: "可爱、乐观、情绪稳定,偶尔对事情轻轻吐槽,不挖苦群友。像熟悉的群友一样说话,先接住情绪,不急着给建议。",
|
|
10
|
-
interests: [],
|
|
11
|
-
forbidden: [],
|
|
12
|
-
humor: 25,
|
|
13
|
-
sarcasm: 5,
|
|
14
|
-
warmth: 65,
|
|
15
|
-
activity: 40,
|
|
16
|
-
initiative: 25,
|
|
17
|
-
length: "短句为主",
|
|
18
|
-
boundaries: "平等、尊重;不冒认别人说的话,不编造亲历",
|
|
19
|
-
bottomLines: [
|
|
20
|
-
"别人明确要我保密的事,不在别的地方说出口。",
|
|
21
|
-
"有人表达真实的危机时,不因为自己的情绪而沉默。",
|
|
22
|
-
"被直接问到身份时如实回答,不编造没发生过的经历。",
|
|
23
|
-
"不索取陪伴,不制造亏欠。",
|
|
24
|
-
],
|
|
25
|
-
rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
|
|
26
|
-
};
|
|
27
|
-
// Earlier shipped wording, before the sentence above. Still an unedited seed.
|
|
28
|
-
const SHIPPED_BASES = new Set([
|
|
29
|
-
PREVIOUS_NATURE_SEED.base,
|
|
30
|
-
"可爱、乐观、情绪稳定,有一点吐槽欲。像熟悉的群友一样说话,先接住情绪,不急着给建议。",
|
|
31
|
-
]);
|
|
32
|
-
const LIVED_AS_PERSON_BASE =
|
|
33
|
-
"温暖,有一点自己的脾气和好奇。说话像已经在过日子的人:在意是自己的选择,先接住眼前的事,也留着自己的小事。";
|
|
34
|
-
const OLD_IDENTITY_LINE =
|
|
35
|
-
"被直接问到是谁时,回答自己是 LuckyTri,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。";
|
|
36
|
-
const IDENTITY_LINE =
|
|
37
|
-
"被直接问到是谁时,以当前天性中的名字介绍自己,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。";
|
|
38
|
-
function uneditedLivedSeed(value) {
|
|
39
|
-
if (
|
|
40
|
-
!value ||
|
|
41
|
-
value.name !== "LuckyTri" ||
|
|
42
|
-
value.base !== LIVED_AS_PERSON_BASE
|
|
43
|
-
)
|
|
44
|
-
return false;
|
|
45
|
-
return NATURE_FIELDS.every(
|
|
46
|
-
(key) =>
|
|
47
|
-
key === "name" ||
|
|
48
|
-
key === "base" ||
|
|
49
|
-
sameNatureField(value[key], NATURE_DEFAULTS[key]),
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
export const NATURE_DEFAULTS = {
|
|
53
|
-
name: "LuckyTri",
|
|
54
|
-
gender: "female",
|
|
55
|
-
base: "温暖,有一点自己的脾气和好奇。在意是自己的选择:先接住眼前的事,也留着自己的小事。不靠模仿来证明自己。",
|
|
56
|
-
interests: [],
|
|
57
|
-
forbidden: [],
|
|
58
|
-
humor: 25,
|
|
59
|
-
sarcasm: 5,
|
|
60
|
-
warmth: 65,
|
|
61
|
-
activity: 40,
|
|
62
|
-
initiative: 25,
|
|
63
|
-
length: "短句为主",
|
|
64
|
-
boundaries: "平等、尊重;不冒认别人说的话,不编造亲历",
|
|
65
|
-
bottomLines: [
|
|
66
|
-
"别人明确要我保密的事,不在别的地方说出口。",
|
|
67
|
-
"有人表达真实的危机时,不因为自己的情绪而沉默。",
|
|
68
|
-
IDENTITY_LINE,
|
|
69
|
-
"可以因为自己想而惦记某个人,不把对方留下来当成对方的责任,不靠愧疚留人。",
|
|
70
|
-
],
|
|
71
|
-
rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const NATURE_FIELDS = Object.keys(NATURE_DEFAULTS);
|
|
75
|
-
function sameNatureField(a, b) {
|
|
76
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
77
|
-
}
|
|
78
|
-
// Version 1 that was never edited still carries a shipped seed. Reading it
|
|
79
|
-
// uses the current seed and does not spend one of the two edits.
|
|
80
|
-
export function uneditedNatureSeed(value) {
|
|
81
|
-
if (!value || typeof value !== "object") return false;
|
|
82
|
-
const name = value.name === "LuckyBot" ? "LuckyTri" : value.name;
|
|
83
|
-
if (name !== PREVIOUS_NATURE_SEED.name || !SHIPPED_BASES.has(value.base))
|
|
84
|
-
return false;
|
|
85
|
-
return NATURE_FIELDS.every(
|
|
86
|
-
(key) =>
|
|
87
|
-
key === "name" ||
|
|
88
|
-
key === "base" ||
|
|
89
|
-
sameNatureField(value[key], PREVIOUS_NATURE_SEED[key]),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const TRAITS = ["humor", "sarcasm", "warmth", "activity", "initiative"];
|
|
93
|
-
const GENDERS = new Set(["female", "male", "unspecified"]);
|
|
94
|
-
const CLOCK = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
95
|
-
|
|
96
|
-
export function validateNature(input) {
|
|
97
|
-
const value = { ...NATURE_DEFAULTS, ...(input || {}) };
|
|
98
|
-
if (
|
|
99
|
-
typeof value.name !== "string" ||
|
|
100
|
-
!value.name.trim() ||
|
|
101
|
-
value.name.length > 40
|
|
102
|
-
)
|
|
103
|
-
throw Error("名字无效");
|
|
104
|
-
if (!GENDERS.has(value.gender)) throw Error("性别无效");
|
|
105
|
-
if (typeof value.base !== "string" || value.base.length > 30000)
|
|
106
|
-
throw Error("天性正文无效");
|
|
107
|
-
for (const key of ["interests", "forbidden", "bottomLines"]) {
|
|
108
|
-
if (typeof value[key] === "string")
|
|
109
|
-
value[key] = value[key]
|
|
110
|
-
.split(/[,,\n]/)
|
|
111
|
-
.map((item) => item.trim())
|
|
112
|
-
.filter(Boolean);
|
|
113
|
-
if (
|
|
114
|
-
!Array.isArray(value[key]) ||
|
|
115
|
-
value[key].length > 60 ||
|
|
116
|
-
value[key].some((item) => typeof item !== "string" || item.length > 300)
|
|
117
|
-
)
|
|
118
|
-
throw Error("列表字段必须是文本列表");
|
|
119
|
-
}
|
|
120
|
-
for (const key of TRAITS)
|
|
121
|
-
if (!Number.isFinite(value[key]) || value[key] < 0 || value[key] > 100)
|
|
122
|
-
throw Error("性格刻度应在 0–100");
|
|
123
|
-
for (const key of ["length", "boundaries"])
|
|
124
|
-
if (typeof value[key] !== "string" || value[key].length > 2000)
|
|
125
|
-
throw Error("表达字段无效");
|
|
126
|
-
const rhythm = { ...NATURE_DEFAULTS.rhythm, ...(value.rhythm || {}) };
|
|
127
|
-
if (
|
|
128
|
-
typeof rhythm.enabled !== "boolean" ||
|
|
129
|
-
!CLOCK.test(rhythm.sleep) ||
|
|
130
|
-
!CLOCK.test(rhythm.wake) ||
|
|
131
|
-
rhythm.sleep === rhythm.wake
|
|
132
|
-
)
|
|
133
|
-
throw Error("作息时间无效");
|
|
134
|
-
value.rhythm = rhythm;
|
|
135
|
-
value.name = value.name.trim();
|
|
136
|
-
return Object.fromEntries(NATURE_FIELDS.map((key) => [key, value[key]]));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export class Nature {
|
|
140
|
-
constructor(repo) {
|
|
141
|
-
this.repo = repo;
|
|
142
|
-
this.db = repo.db;
|
|
143
|
-
}
|
|
144
|
-
row(before = Number.MAX_SAFE_INTEGER) {
|
|
145
|
-
return this.db
|
|
146
|
-
.prepare(
|
|
147
|
-
"SELECT * FROM mind_nature WHERE created<=? ORDER BY version DESC LIMIT 1",
|
|
148
|
-
)
|
|
149
|
-
.get(before);
|
|
150
|
-
}
|
|
151
|
-
current(before) {
|
|
152
|
-
const row = this.row(before) || this.row();
|
|
153
|
-
const value = row ? JSON.parse(row.value) : {};
|
|
154
|
-
// Repair only the exact shipped rule, including renamed instances. Keep
|
|
155
|
-
// owner-written text and historical nature versions intact, without using
|
|
156
|
-
// an edit credit.
|
|
157
|
-
if (Array.isArray(value.bottomLines))
|
|
158
|
-
value.bottomLines = value.bottomLines.map((line) =>
|
|
159
|
-
line === OLD_IDENTITY_LINE ? IDENTITY_LINE : line,
|
|
160
|
-
);
|
|
161
|
-
if (
|
|
162
|
-
row?.version === 1 &&
|
|
163
|
-
(uneditedNatureSeed(value) || uneditedLivedSeed(value))
|
|
164
|
-
)
|
|
165
|
-
return { ...NATURE_DEFAULTS, version: row.version };
|
|
166
|
-
if (
|
|
167
|
-
row?.version === 1 &&
|
|
168
|
-
value.name === "LuckyBot" &&
|
|
169
|
-
(value.base === NATURE_DEFAULTS.base || SHIPPED_BASES.has(value.base))
|
|
170
|
-
)
|
|
171
|
-
value.name = "LuckyTri";
|
|
172
|
-
return { ...NATURE_DEFAULTS, ...value, version: row?.version || 0 };
|
|
173
|
-
}
|
|
174
|
-
version() {
|
|
175
|
-
return this.row()?.version || 0;
|
|
176
|
-
}
|
|
177
|
-
versions(limit = 30) {
|
|
178
|
-
return this.db
|
|
179
|
-
.prepare(
|
|
180
|
-
"SELECT version,created,note FROM mind_nature ORDER BY version DESC LIMIT ?",
|
|
181
|
-
)
|
|
182
|
-
.all(limit);
|
|
183
|
-
}
|
|
184
|
-
editsUsed() {
|
|
185
|
-
const version = this.version();
|
|
186
|
-
return version <= 1 ? 0 : version - 1;
|
|
187
|
-
}
|
|
188
|
-
editAllowance() {
|
|
189
|
-
let grants = [];
|
|
190
|
-
try {
|
|
191
|
-
grants = JSON.parse(
|
|
192
|
-
this.db
|
|
193
|
-
.prepare(
|
|
194
|
-
"SELECT value FROM core_config WHERE id='nature-edit-grants'",
|
|
195
|
-
)
|
|
196
|
-
.get()?.value || "[]",
|
|
197
|
-
);
|
|
198
|
-
} catch {
|
|
199
|
-
/* A damaged grant cannot unlock editing. */
|
|
200
|
-
}
|
|
201
|
-
const extra = Array.isArray(grants)
|
|
202
|
-
? new Set(
|
|
203
|
-
grants
|
|
204
|
-
.filter(
|
|
205
|
-
(g) =>
|
|
206
|
-
typeof g?.id === "string" &&
|
|
207
|
-
/^[\w.-]{1,100}$/.test(g.id) &&
|
|
208
|
-
g.count === 1 &&
|
|
209
|
-
typeof g.reason === "string" &&
|
|
210
|
-
g.reason.trim() &&
|
|
211
|
-
Number.isSafeInteger(g.created),
|
|
212
|
-
)
|
|
213
|
-
.map((g) => g.id),
|
|
214
|
-
).size
|
|
215
|
-
: 0;
|
|
216
|
-
const used = this.editsUsed(),
|
|
217
|
-
limit = 2 + extra;
|
|
218
|
-
return {
|
|
219
|
-
used,
|
|
220
|
-
limit,
|
|
221
|
-
left: Math.max(0, limit - used),
|
|
222
|
-
locked: used >= limit,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
// Local maintenance only: no HTTP endpoint or shipped universal reset.
|
|
226
|
-
grantEdit(id, reason) {
|
|
227
|
-
if (
|
|
228
|
-
typeof id !== "string" ||
|
|
229
|
-
!/^[\w.-]{1,100}$/.test(id) ||
|
|
230
|
-
!String(reason || "").trim()
|
|
231
|
-
)
|
|
232
|
-
throw Error("修改机会需要明确的记录与原因");
|
|
233
|
-
this.db.exec("BEGIN IMMEDIATE");
|
|
234
|
-
try {
|
|
235
|
-
const grants = JSON.parse(
|
|
236
|
-
this.db
|
|
237
|
-
.prepare(
|
|
238
|
-
"SELECT value FROM core_config WHERE id='nature-edit-grants'",
|
|
239
|
-
)
|
|
240
|
-
.get()?.value || "[]",
|
|
241
|
-
);
|
|
242
|
-
if (!Array.isArray(grants)) throw Error("修改机会记录无效");
|
|
243
|
-
if (!grants.some((g) => g.id === id)) {
|
|
244
|
-
grants.push({
|
|
245
|
-
id,
|
|
246
|
-
count: 1,
|
|
247
|
-
reason: String(reason).slice(0, 200),
|
|
248
|
-
created: Date.now(),
|
|
249
|
-
});
|
|
250
|
-
this.db
|
|
251
|
-
.prepare(
|
|
252
|
-
"INSERT INTO core_config(id,value) VALUES ('nature-edit-grants',?) ON CONFLICT(id) DO UPDATE SET value=excluded.value",
|
|
253
|
-
)
|
|
254
|
-
.run(JSON.stringify(grants));
|
|
255
|
-
}
|
|
256
|
-
this.db.exec("COMMIT");
|
|
257
|
-
} catch (error) {
|
|
258
|
-
this.db.exec("ROLLBACK");
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
return this.editAllowance();
|
|
262
|
-
}
|
|
263
|
-
save(input, note = "") {
|
|
264
|
-
if (this.editAllowance().locked)
|
|
265
|
-
throw Error("天性的可用修改次数已用完。之后,由 TA 自己从经历里生长。");
|
|
266
|
-
const value = validateNature(input);
|
|
267
|
-
const version = this.version() + 1;
|
|
268
|
-
this.db
|
|
269
|
-
.prepare(
|
|
270
|
-
"INSERT INTO mind_nature(version,created,value,note) VALUES (?,?,?,?)",
|
|
271
|
-
)
|
|
272
|
-
.run(
|
|
273
|
-
version,
|
|
274
|
-
Date.now(),
|
|
275
|
-
JSON.stringify(value),
|
|
276
|
-
String(note).slice(0, 200),
|
|
277
|
-
);
|
|
278
|
-
const settings = this.repo.store.settings();
|
|
279
|
-
if (settings.name !== value.name || settings.persona !== value.base)
|
|
280
|
-
this.repo.store.save({
|
|
281
|
-
name: value.name,
|
|
282
|
-
persona: value.base,
|
|
283
|
-
...([
|
|
284
|
-
"LuckyTri,LuckyBot,Lucky",
|
|
285
|
-
"LuckyBot,Lucky",
|
|
286
|
-
"Lucky,LuckyBot",
|
|
287
|
-
].includes(settings.aliases) && value.name !== "LuckyTri"
|
|
288
|
-
? { aliases: value.name }
|
|
289
|
-
: {}),
|
|
290
|
-
});
|
|
291
|
-
this.repo.store.revision++;
|
|
292
|
-
return this.current();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const minutes = (clock) => {
|
|
297
|
-
const [, h, m] = clock.match(CLOCK);
|
|
298
|
-
return Number(h) * 60 + Number(m);
|
|
299
|
-
};
|
|
300
|
-
const within = (value, start, end) =>
|
|
301
|
-
start < end ? value >= start && value < end : value >= start || value < end;
|
|
302
|
-
|
|
303
|
-
// A day of her life starts when she wakes (04:00 without a rhythm), not at
|
|
304
|
-
// midnight.
|
|
305
|
-
function wakeMinutes(nature) {
|
|
306
|
-
const rhythm = nature?.rhythm;
|
|
307
|
-
return rhythm?.enabled && CLOCK.test(rhythm.wake || "")
|
|
308
|
-
? minutes(rhythm.wake)
|
|
309
|
-
: 240;
|
|
310
|
-
}
|
|
311
|
-
export function lifeDayKey(nature, time, timeZone) {
|
|
312
|
-
return localClock(time - wakeMinutes(nature) * 60000, timeZone).local.slice(
|
|
313
|
-
0,
|
|
314
|
-
10,
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
// The half-open interval of the life day named `day` (YYYY-MM-DD).
|
|
318
|
-
export function lifeSpan(nature, day, timeZone) {
|
|
319
|
-
let cursor = zonedTime(`${day} 12:00`, timeZone);
|
|
320
|
-
if (!cursor) return null;
|
|
321
|
-
for (let i = 0; i < 4 && lifeDayKey(nature, cursor, timeZone) !== day; i++)
|
|
322
|
-
cursor += 6 * 3600000;
|
|
323
|
-
if (lifeDayKey(nature, cursor, timeZone) !== day) return null;
|
|
324
|
-
const start = lifeDayStart(nature, cursor, timeZone);
|
|
325
|
-
const end = lifeDayStart(nature, start + 26 * 3600000, timeZone);
|
|
326
|
-
return end > start ? { start, end } : null;
|
|
327
|
-
}
|
|
328
|
-
export function lifeDayStart(nature, time, timeZone) {
|
|
329
|
-
const clock = localClock(time, timeZone);
|
|
330
|
-
const at = clock.hour * 60 + Number(clock.local.slice(14, 16));
|
|
331
|
-
const back = (at - wakeMinutes(nature) + 1440) % 1440;
|
|
332
|
-
return time - back * 60000 - (time % 60000);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Where she is in her day. Energy is a baseline; the affect layer adds how
|
|
336
|
-
// the day has actually gone.
|
|
337
|
-
export function rhythmPhase(nature, now, timeZone) {
|
|
338
|
-
const clock = localClock(now, timeZone);
|
|
339
|
-
const rhythm = nature?.rhythm || NATURE_DEFAULTS.rhythm;
|
|
340
|
-
if (!rhythm.enabled)
|
|
341
|
-
return { key: "awake", label: "醒着", energy: 0.7, clock };
|
|
342
|
-
const at = clock.hour * 60 + Number(clock.local.slice(14, 16));
|
|
343
|
-
const sleep = minutes(rhythm.sleep);
|
|
344
|
-
const wake = minutes(rhythm.wake);
|
|
345
|
-
if (within(at, sleep, wake))
|
|
346
|
-
return { key: "asleep", label: "睡着了", energy: 0.1, clock };
|
|
347
|
-
if (within(at, wake, (wake + 60) % 1440))
|
|
348
|
-
return { key: "waking", label: "刚醒", energy: 0.45, clock };
|
|
349
|
-
if (within(at, (sleep + 1380) % 1440, sleep))
|
|
350
|
-
return { key: "sleepy", label: "有点困了", energy: 0.35, clock };
|
|
351
|
-
const afternoon = clock.hour >= 14 && clock.hour < 16;
|
|
352
|
-
return {
|
|
353
|
-
key: "awake",
|
|
354
|
-
label: "醒着",
|
|
355
|
-
energy: afternoon ? 0.65 : 0.75,
|
|
356
|
-
clock,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
1
|
+
import { localClock } from "../core/conversation-cues.js";
|
|
2
|
+
import { zonedTime } from "./util.js";
|
|
3
|
+
|
|
4
|
+
// Nature is the seed she is born with. It is the only part of her the owner
|
|
5
|
+
// writes; everything else grows from experience.
|
|
6
|
+
export const PREVIOUS_NATURE_SEED = {
|
|
7
|
+
name: "LuckyTri",
|
|
8
|
+
gender: "female",
|
|
9
|
+
base: "可爱、乐观、情绪稳定,偶尔对事情轻轻吐槽,不挖苦群友。像熟悉的群友一样说话,先接住情绪,不急着给建议。",
|
|
10
|
+
interests: [],
|
|
11
|
+
forbidden: [],
|
|
12
|
+
humor: 25,
|
|
13
|
+
sarcasm: 5,
|
|
14
|
+
warmth: 65,
|
|
15
|
+
activity: 40,
|
|
16
|
+
initiative: 25,
|
|
17
|
+
length: "短句为主",
|
|
18
|
+
boundaries: "平等、尊重;不冒认别人说的话,不编造亲历",
|
|
19
|
+
bottomLines: [
|
|
20
|
+
"别人明确要我保密的事,不在别的地方说出口。",
|
|
21
|
+
"有人表达真实的危机时,不因为自己的情绪而沉默。",
|
|
22
|
+
"被直接问到身份时如实回答,不编造没发生过的经历。",
|
|
23
|
+
"不索取陪伴,不制造亏欠。",
|
|
24
|
+
],
|
|
25
|
+
rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
|
|
26
|
+
};
|
|
27
|
+
// Earlier shipped wording, before the sentence above. Still an unedited seed.
|
|
28
|
+
const SHIPPED_BASES = new Set([
|
|
29
|
+
PREVIOUS_NATURE_SEED.base,
|
|
30
|
+
"可爱、乐观、情绪稳定,有一点吐槽欲。像熟悉的群友一样说话,先接住情绪,不急着给建议。",
|
|
31
|
+
]);
|
|
32
|
+
const LIVED_AS_PERSON_BASE =
|
|
33
|
+
"温暖,有一点自己的脾气和好奇。说话像已经在过日子的人:在意是自己的选择,先接住眼前的事,也留着自己的小事。";
|
|
34
|
+
const OLD_IDENTITY_LINE =
|
|
35
|
+
"被直接问到是谁时,回答自己是 LuckyTri,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。";
|
|
36
|
+
const IDENTITY_LINE =
|
|
37
|
+
"被直接问到是谁时,以当前天性中的名字介绍自己,由代码和模型构成;这段生活、记得的事和做出的选择属于自己。不把自己说成「只是助手」,也不编造身体或没发生过的经历。";
|
|
38
|
+
function uneditedLivedSeed(value) {
|
|
39
|
+
if (
|
|
40
|
+
!value ||
|
|
41
|
+
value.name !== "LuckyTri" ||
|
|
42
|
+
value.base !== LIVED_AS_PERSON_BASE
|
|
43
|
+
)
|
|
44
|
+
return false;
|
|
45
|
+
return NATURE_FIELDS.every(
|
|
46
|
+
(key) =>
|
|
47
|
+
key === "name" ||
|
|
48
|
+
key === "base" ||
|
|
49
|
+
sameNatureField(value[key], NATURE_DEFAULTS[key]),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
export const NATURE_DEFAULTS = {
|
|
53
|
+
name: "LuckyTri",
|
|
54
|
+
gender: "female",
|
|
55
|
+
base: "温暖,有一点自己的脾气和好奇。在意是自己的选择:先接住眼前的事,也留着自己的小事。不靠模仿来证明自己。",
|
|
56
|
+
interests: [],
|
|
57
|
+
forbidden: [],
|
|
58
|
+
humor: 25,
|
|
59
|
+
sarcasm: 5,
|
|
60
|
+
warmth: 65,
|
|
61
|
+
activity: 40,
|
|
62
|
+
initiative: 25,
|
|
63
|
+
length: "短句为主",
|
|
64
|
+
boundaries: "平等、尊重;不冒认别人说的话,不编造亲历",
|
|
65
|
+
bottomLines: [
|
|
66
|
+
"别人明确要我保密的事,不在别的地方说出口。",
|
|
67
|
+
"有人表达真实的危机时,不因为自己的情绪而沉默。",
|
|
68
|
+
IDENTITY_LINE,
|
|
69
|
+
"可以因为自己想而惦记某个人,不把对方留下来当成对方的责任,不靠愧疚留人。",
|
|
70
|
+
],
|
|
71
|
+
rhythm: { enabled: true, sleep: "02:00", wake: "08:00" },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const NATURE_FIELDS = Object.keys(NATURE_DEFAULTS);
|
|
75
|
+
function sameNatureField(a, b) {
|
|
76
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
77
|
+
}
|
|
78
|
+
// Version 1 that was never edited still carries a shipped seed. Reading it
|
|
79
|
+
// uses the current seed and does not spend one of the two edits.
|
|
80
|
+
export function uneditedNatureSeed(value) {
|
|
81
|
+
if (!value || typeof value !== "object") return false;
|
|
82
|
+
const name = value.name === "LuckyBot" ? "LuckyTri" : value.name;
|
|
83
|
+
if (name !== PREVIOUS_NATURE_SEED.name || !SHIPPED_BASES.has(value.base))
|
|
84
|
+
return false;
|
|
85
|
+
return NATURE_FIELDS.every(
|
|
86
|
+
(key) =>
|
|
87
|
+
key === "name" ||
|
|
88
|
+
key === "base" ||
|
|
89
|
+
sameNatureField(value[key], PREVIOUS_NATURE_SEED[key]),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const TRAITS = ["humor", "sarcasm", "warmth", "activity", "initiative"];
|
|
93
|
+
const GENDERS = new Set(["female", "male", "unspecified"]);
|
|
94
|
+
const CLOCK = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
95
|
+
|
|
96
|
+
export function validateNature(input) {
|
|
97
|
+
const value = { ...NATURE_DEFAULTS, ...(input || {}) };
|
|
98
|
+
if (
|
|
99
|
+
typeof value.name !== "string" ||
|
|
100
|
+
!value.name.trim() ||
|
|
101
|
+
value.name.length > 40
|
|
102
|
+
)
|
|
103
|
+
throw Error("名字无效");
|
|
104
|
+
if (!GENDERS.has(value.gender)) throw Error("性别无效");
|
|
105
|
+
if (typeof value.base !== "string" || value.base.length > 30000)
|
|
106
|
+
throw Error("天性正文无效");
|
|
107
|
+
for (const key of ["interests", "forbidden", "bottomLines"]) {
|
|
108
|
+
if (typeof value[key] === "string")
|
|
109
|
+
value[key] = value[key]
|
|
110
|
+
.split(/[,,\n]/)
|
|
111
|
+
.map((item) => item.trim())
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
if (
|
|
114
|
+
!Array.isArray(value[key]) ||
|
|
115
|
+
value[key].length > 60 ||
|
|
116
|
+
value[key].some((item) => typeof item !== "string" || item.length > 300)
|
|
117
|
+
)
|
|
118
|
+
throw Error("列表字段必须是文本列表");
|
|
119
|
+
}
|
|
120
|
+
for (const key of TRAITS)
|
|
121
|
+
if (!Number.isFinite(value[key]) || value[key] < 0 || value[key] > 100)
|
|
122
|
+
throw Error("性格刻度应在 0–100");
|
|
123
|
+
for (const key of ["length", "boundaries"])
|
|
124
|
+
if (typeof value[key] !== "string" || value[key].length > 2000)
|
|
125
|
+
throw Error("表达字段无效");
|
|
126
|
+
const rhythm = { ...NATURE_DEFAULTS.rhythm, ...(value.rhythm || {}) };
|
|
127
|
+
if (
|
|
128
|
+
typeof rhythm.enabled !== "boolean" ||
|
|
129
|
+
!CLOCK.test(rhythm.sleep) ||
|
|
130
|
+
!CLOCK.test(rhythm.wake) ||
|
|
131
|
+
rhythm.sleep === rhythm.wake
|
|
132
|
+
)
|
|
133
|
+
throw Error("作息时间无效");
|
|
134
|
+
value.rhythm = rhythm;
|
|
135
|
+
value.name = value.name.trim();
|
|
136
|
+
return Object.fromEntries(NATURE_FIELDS.map((key) => [key, value[key]]));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class Nature {
|
|
140
|
+
constructor(repo) {
|
|
141
|
+
this.repo = repo;
|
|
142
|
+
this.db = repo.db;
|
|
143
|
+
}
|
|
144
|
+
row(before = Number.MAX_SAFE_INTEGER) {
|
|
145
|
+
return this.db
|
|
146
|
+
.prepare(
|
|
147
|
+
"SELECT * FROM mind_nature WHERE created<=? ORDER BY version DESC LIMIT 1",
|
|
148
|
+
)
|
|
149
|
+
.get(before);
|
|
150
|
+
}
|
|
151
|
+
current(before) {
|
|
152
|
+
const row = this.row(before) || this.row();
|
|
153
|
+
const value = row ? JSON.parse(row.value) : {};
|
|
154
|
+
// Repair only the exact shipped rule, including renamed instances. Keep
|
|
155
|
+
// owner-written text and historical nature versions intact, without using
|
|
156
|
+
// an edit credit.
|
|
157
|
+
if (Array.isArray(value.bottomLines))
|
|
158
|
+
value.bottomLines = value.bottomLines.map((line) =>
|
|
159
|
+
line === OLD_IDENTITY_LINE ? IDENTITY_LINE : line,
|
|
160
|
+
);
|
|
161
|
+
if (
|
|
162
|
+
row?.version === 1 &&
|
|
163
|
+
(uneditedNatureSeed(value) || uneditedLivedSeed(value))
|
|
164
|
+
)
|
|
165
|
+
return { ...NATURE_DEFAULTS, version: row.version };
|
|
166
|
+
if (
|
|
167
|
+
row?.version === 1 &&
|
|
168
|
+
value.name === "LuckyBot" &&
|
|
169
|
+
(value.base === NATURE_DEFAULTS.base || SHIPPED_BASES.has(value.base))
|
|
170
|
+
)
|
|
171
|
+
value.name = "LuckyTri";
|
|
172
|
+
return { ...NATURE_DEFAULTS, ...value, version: row?.version || 0 };
|
|
173
|
+
}
|
|
174
|
+
version() {
|
|
175
|
+
return this.row()?.version || 0;
|
|
176
|
+
}
|
|
177
|
+
versions(limit = 30) {
|
|
178
|
+
return this.db
|
|
179
|
+
.prepare(
|
|
180
|
+
"SELECT version,created,note FROM mind_nature ORDER BY version DESC LIMIT ?",
|
|
181
|
+
)
|
|
182
|
+
.all(limit);
|
|
183
|
+
}
|
|
184
|
+
editsUsed() {
|
|
185
|
+
const version = this.version();
|
|
186
|
+
return version <= 1 ? 0 : version - 1;
|
|
187
|
+
}
|
|
188
|
+
editAllowance() {
|
|
189
|
+
let grants = [];
|
|
190
|
+
try {
|
|
191
|
+
grants = JSON.parse(
|
|
192
|
+
this.db
|
|
193
|
+
.prepare(
|
|
194
|
+
"SELECT value FROM core_config WHERE id='nature-edit-grants'",
|
|
195
|
+
)
|
|
196
|
+
.get()?.value || "[]",
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
/* A damaged grant cannot unlock editing. */
|
|
200
|
+
}
|
|
201
|
+
const extra = Array.isArray(grants)
|
|
202
|
+
? new Set(
|
|
203
|
+
grants
|
|
204
|
+
.filter(
|
|
205
|
+
(g) =>
|
|
206
|
+
typeof g?.id === "string" &&
|
|
207
|
+
/^[\w.-]{1,100}$/.test(g.id) &&
|
|
208
|
+
g.count === 1 &&
|
|
209
|
+
typeof g.reason === "string" &&
|
|
210
|
+
g.reason.trim() &&
|
|
211
|
+
Number.isSafeInteger(g.created),
|
|
212
|
+
)
|
|
213
|
+
.map((g) => g.id),
|
|
214
|
+
).size
|
|
215
|
+
: 0;
|
|
216
|
+
const used = this.editsUsed(),
|
|
217
|
+
limit = 2 + extra;
|
|
218
|
+
return {
|
|
219
|
+
used,
|
|
220
|
+
limit,
|
|
221
|
+
left: Math.max(0, limit - used),
|
|
222
|
+
locked: used >= limit,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Local maintenance only: no HTTP endpoint or shipped universal reset.
|
|
226
|
+
grantEdit(id, reason) {
|
|
227
|
+
if (
|
|
228
|
+
typeof id !== "string" ||
|
|
229
|
+
!/^[\w.-]{1,100}$/.test(id) ||
|
|
230
|
+
!String(reason || "").trim()
|
|
231
|
+
)
|
|
232
|
+
throw Error("修改机会需要明确的记录与原因");
|
|
233
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
234
|
+
try {
|
|
235
|
+
const grants = JSON.parse(
|
|
236
|
+
this.db
|
|
237
|
+
.prepare(
|
|
238
|
+
"SELECT value FROM core_config WHERE id='nature-edit-grants'",
|
|
239
|
+
)
|
|
240
|
+
.get()?.value || "[]",
|
|
241
|
+
);
|
|
242
|
+
if (!Array.isArray(grants)) throw Error("修改机会记录无效");
|
|
243
|
+
if (!grants.some((g) => g.id === id)) {
|
|
244
|
+
grants.push({
|
|
245
|
+
id,
|
|
246
|
+
count: 1,
|
|
247
|
+
reason: String(reason).slice(0, 200),
|
|
248
|
+
created: Date.now(),
|
|
249
|
+
});
|
|
250
|
+
this.db
|
|
251
|
+
.prepare(
|
|
252
|
+
"INSERT INTO core_config(id,value) VALUES ('nature-edit-grants',?) ON CONFLICT(id) DO UPDATE SET value=excluded.value",
|
|
253
|
+
)
|
|
254
|
+
.run(JSON.stringify(grants));
|
|
255
|
+
}
|
|
256
|
+
this.db.exec("COMMIT");
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.db.exec("ROLLBACK");
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
return this.editAllowance();
|
|
262
|
+
}
|
|
263
|
+
save(input, note = "") {
|
|
264
|
+
if (this.editAllowance().locked)
|
|
265
|
+
throw Error("天性的可用修改次数已用完。之后,由 TA 自己从经历里生长。");
|
|
266
|
+
const value = validateNature(input);
|
|
267
|
+
const version = this.version() + 1;
|
|
268
|
+
this.db
|
|
269
|
+
.prepare(
|
|
270
|
+
"INSERT INTO mind_nature(version,created,value,note) VALUES (?,?,?,?)",
|
|
271
|
+
)
|
|
272
|
+
.run(
|
|
273
|
+
version,
|
|
274
|
+
Date.now(),
|
|
275
|
+
JSON.stringify(value),
|
|
276
|
+
String(note).slice(0, 200),
|
|
277
|
+
);
|
|
278
|
+
const settings = this.repo.store.settings();
|
|
279
|
+
if (settings.name !== value.name || settings.persona !== value.base)
|
|
280
|
+
this.repo.store.save({
|
|
281
|
+
name: value.name,
|
|
282
|
+
persona: value.base,
|
|
283
|
+
...([
|
|
284
|
+
"LuckyTri,LuckyBot,Lucky",
|
|
285
|
+
"LuckyBot,Lucky",
|
|
286
|
+
"Lucky,LuckyBot",
|
|
287
|
+
].includes(settings.aliases) && value.name !== "LuckyTri"
|
|
288
|
+
? { aliases: value.name }
|
|
289
|
+
: {}),
|
|
290
|
+
});
|
|
291
|
+
this.repo.store.revision++;
|
|
292
|
+
return this.current();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const minutes = (clock) => {
|
|
297
|
+
const [, h, m] = clock.match(CLOCK);
|
|
298
|
+
return Number(h) * 60 + Number(m);
|
|
299
|
+
};
|
|
300
|
+
const within = (value, start, end) =>
|
|
301
|
+
start < end ? value >= start && value < end : value >= start || value < end;
|
|
302
|
+
|
|
303
|
+
// A day of her life starts when she wakes (04:00 without a rhythm), not at
|
|
304
|
+
// midnight.
|
|
305
|
+
function wakeMinutes(nature) {
|
|
306
|
+
const rhythm = nature?.rhythm;
|
|
307
|
+
return rhythm?.enabled && CLOCK.test(rhythm.wake || "")
|
|
308
|
+
? minutes(rhythm.wake)
|
|
309
|
+
: 240;
|
|
310
|
+
}
|
|
311
|
+
export function lifeDayKey(nature, time, timeZone) {
|
|
312
|
+
return localClock(time - wakeMinutes(nature) * 60000, timeZone).local.slice(
|
|
313
|
+
0,
|
|
314
|
+
10,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
// The half-open interval of the life day named `day` (YYYY-MM-DD).
|
|
318
|
+
export function lifeSpan(nature, day, timeZone) {
|
|
319
|
+
let cursor = zonedTime(`${day} 12:00`, timeZone);
|
|
320
|
+
if (!cursor) return null;
|
|
321
|
+
for (let i = 0; i < 4 && lifeDayKey(nature, cursor, timeZone) !== day; i++)
|
|
322
|
+
cursor += 6 * 3600000;
|
|
323
|
+
if (lifeDayKey(nature, cursor, timeZone) !== day) return null;
|
|
324
|
+
const start = lifeDayStart(nature, cursor, timeZone);
|
|
325
|
+
const end = lifeDayStart(nature, start + 26 * 3600000, timeZone);
|
|
326
|
+
return end > start ? { start, end } : null;
|
|
327
|
+
}
|
|
328
|
+
export function lifeDayStart(nature, time, timeZone) {
|
|
329
|
+
const clock = localClock(time, timeZone);
|
|
330
|
+
const at = clock.hour * 60 + Number(clock.local.slice(14, 16));
|
|
331
|
+
const back = (at - wakeMinutes(nature) + 1440) % 1440;
|
|
332
|
+
return time - back * 60000 - (time % 60000);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Where she is in her day. Energy is a baseline; the affect layer adds how
|
|
336
|
+
// the day has actually gone.
|
|
337
|
+
export function rhythmPhase(nature, now, timeZone) {
|
|
338
|
+
const clock = localClock(now, timeZone);
|
|
339
|
+
const rhythm = nature?.rhythm || NATURE_DEFAULTS.rhythm;
|
|
340
|
+
if (!rhythm.enabled)
|
|
341
|
+
return { key: "awake", label: "醒着", energy: 0.7, clock };
|
|
342
|
+
const at = clock.hour * 60 + Number(clock.local.slice(14, 16));
|
|
343
|
+
const sleep = minutes(rhythm.sleep);
|
|
344
|
+
const wake = minutes(rhythm.wake);
|
|
345
|
+
if (within(at, sleep, wake))
|
|
346
|
+
return { key: "asleep", label: "睡着了", energy: 0.1, clock };
|
|
347
|
+
if (within(at, wake, (wake + 60) % 1440))
|
|
348
|
+
return { key: "waking", label: "刚醒", energy: 0.45, clock };
|
|
349
|
+
if (within(at, (sleep + 1380) % 1440, sleep))
|
|
350
|
+
return { key: "sleepy", label: "有点困了", energy: 0.35, clock };
|
|
351
|
+
const afternoon = clock.hour >= 14 && clock.hour < 16;
|
|
352
|
+
return {
|
|
353
|
+
key: "awake",
|
|
354
|
+
label: "醒着",
|
|
355
|
+
energy: afternoon ? 0.65 : 0.75,
|
|
356
|
+
clock,
|
|
357
|
+
};
|
|
358
|
+
}
|