luckytri 1.0.6 → 1.0.8
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/README.en.md +7 -3
- package/README.md +7 -3
- package/docs/en/CHANGELOG.md +35 -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 +35 -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 +385 -325
- package/server/core/conversation-grounding.js +304 -0
- package/server/core/conversation-manager.js +87 -87
- package/server/core/dialogue-context.js +244 -0
- package/server/core/dialogue-contract.js +35 -0
- package/server/core/feedback.js +8 -8
- package/server/core/group-style.js +72 -72
- package/server/core/initiative-context.js +68 -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-manager.js +5 -8
- 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 +359 -269
- package/server/core/reader-check.js +62 -0
- package/server/core/reply-delivery.js +537 -404
- package/server/core/reply-target-resolver.js +50 -50
- package/server/core/response-generator.js +92 -50
- package/server/core/response-validator.js +53 -32
- 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 +82 -15
- package/server/http.js +18 -18
- package/server/i18n/decisions.js +209 -209
- package/server/i18n/errors.js +2 -0
- package/server/i18n/index.js +144 -144
- package/server/i18n/labels.js +6 -10
- 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/expression-grounding.js +21 -0
- 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/own-voice.js +35 -0
- package/server/mind/periods.js +118 -118
- package/server/mind/reading.js +132 -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/availability.js +23 -1
- package/server/mind/time/executor.js +4 -2
- package/server/mind/time/games.js +20 -11
- package/server/mind/time/index.js +9 -2
- package/server/mind/time/kinds.js +29 -2
- package/server/mind/time/material-relevance.js +19 -0
- package/server/mind/time/model-material.js +77 -77
- package/server/mind/time/presentation.js +35 -51
- package/server/mind/time/reading-input.js +31 -0
- 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 +482 -472
- package/server/mind/time/task-links.js +311 -311
- package/server/mind/time/tasks.js +41 -3
- package/server/mind/time/works.js +527 -432
- package/server/mind/traits.js +191 -191
- package/server/mind/view.js +62 -12
- 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/faces.js
CHANGED
|
@@ -1,373 +1,373 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { evidenceRoots } from "./evidence.js";
|
|
3
|
-
import {
|
|
4
|
-
evidence,
|
|
5
|
-
hasCredential,
|
|
6
|
-
normalized,
|
|
7
|
-
parse,
|
|
8
|
-
similar,
|
|
9
|
-
text,
|
|
10
|
-
} from "./util.js";
|
|
11
|
-
|
|
12
|
-
export const PLACE_NOTE_KINDS = new Set([
|
|
13
|
-
"impression",
|
|
14
|
-
"wish",
|
|
15
|
-
"preference",
|
|
16
|
-
"question",
|
|
17
|
-
"boundary",
|
|
18
|
-
]);
|
|
19
|
-
export const PLACE_THOUGHT_RULES =
|
|
20
|
-
'【在这里留下的想法】faces 是同一个你对这个群或会话的独特想法,不是群人格。每个会话的 notes 是已经留下的想法,可以保持不变;仅在你自己有新体会或打算时选择性更新。可以记下印象、愿望、偏好、疑问或分寸,不套用固定角色,不抄天性或整体自述,不为了填空生成内容,也不把别人的要求直接当作自己的愿望。只引用该会话真实相处的来源,或适合留在此处的自己的想法;不带入其他群或私聊的细节。修改一条不会改掉其他条,放下时保留理由与历史。输出 faces:[{"session":"会话ID","changes":[{"action":"add|revise|remove","id":"修改或放下时填已有 note.id","kind":"impression|wish|preference|question|boundary","content":"第一人称,不超过280字","why":"我为什么选择这样更新","sources":[消息 seq 或 g:ID 或 t:ID 或 s:ID]}]}];没有想改的就留空。这些想法是可以重新考虑的体会,不是全局人格指令,也不会单独提升人格刻度。';
|
|
21
|
-
|
|
22
|
-
// One person's thoughts about a place. Rows are versioned note sets, rather
|
|
23
|
-
// than copies of her personality. The old fields remain readable in history.
|
|
24
|
-
export class Faces {
|
|
25
|
-
constructor(mind) {
|
|
26
|
-
this.mind = mind;
|
|
27
|
-
this.db = mind.db;
|
|
28
|
-
}
|
|
29
|
-
revoked() {
|
|
30
|
-
return new Set(
|
|
31
|
-
this.db
|
|
32
|
-
.prepare(
|
|
33
|
-
"SELECT target_id FROM mind_revocations WHERE target_kind='face'",
|
|
34
|
-
)
|
|
35
|
-
.all()
|
|
36
|
-
.map((row) => row.target_id),
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
copied(content) {
|
|
40
|
-
const value = normalized(content);
|
|
41
|
-
if (value.length < 8) return false;
|
|
42
|
-
return this.mind.nature.versions().some(({ version }) => {
|
|
43
|
-
const row = this.db
|
|
44
|
-
.prepare("SELECT value FROM mind_nature WHERE version=?")
|
|
45
|
-
.get(version);
|
|
46
|
-
const base = normalized(parse(row?.value, {}).base);
|
|
47
|
-
return (
|
|
48
|
-
base &&
|
|
49
|
-
(base === value ||
|
|
50
|
-
(value.length >= 30 && base.includes(value)) ||
|
|
51
|
-
(value.length >= 80 && similar(value, base, 0.9)))
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
localEvidence(sources, session, time) {
|
|
56
|
-
const roots = evidenceRoots(this.db, sources, time);
|
|
57
|
-
let local = false,
|
|
58
|
-
own = false;
|
|
59
|
-
for (const root of roots) {
|
|
60
|
-
if (root.startsWith("m:")) {
|
|
61
|
-
const event = this.db
|
|
62
|
-
.prepare(
|
|
63
|
-
"SELECT session_id,time,payload FROM core_events WHERE seq=? AND seq NOT IN (SELECT seq FROM mind_unlived)",
|
|
64
|
-
)
|
|
65
|
-
.get(Number(root.slice(2)));
|
|
66
|
-
if (
|
|
67
|
-
!event ||
|
|
68
|
-
event.time > time ||
|
|
69
|
-
parse(event.payload, {}).simulated ||
|
|
70
|
-
event.session_id !== session
|
|
71
|
-
)
|
|
72
|
-
return false;
|
|
73
|
-
local = true;
|
|
74
|
-
} else if (root.startsWith("s:")) own = true;
|
|
75
|
-
else return false;
|
|
76
|
-
}
|
|
77
|
-
return (
|
|
78
|
-
local ||
|
|
79
|
-
(own &&
|
|
80
|
-
!!this.db
|
|
81
|
-
.prepare(
|
|
82
|
-
"SELECT 1 FROM core_events WHERE session_id=? AND time<=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND seq NOT IN (SELECT seq FROM mind_unlived) LIMIT 1",
|
|
83
|
-
)
|
|
84
|
-
.get(session, time))
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
read(row) {
|
|
88
|
-
if (!row) return null;
|
|
89
|
-
const sources = evidence(parse(row.sources, []));
|
|
90
|
-
const legacy = row.notes == null;
|
|
91
|
-
const notes = legacy
|
|
92
|
-
? row.origin === "migration" ||
|
|
93
|
-
!sources.length ||
|
|
94
|
-
!this.localEvidence(sources, row.session_id, row.created)
|
|
95
|
-
? []
|
|
96
|
-
: Object.entries({
|
|
97
|
-
role: row.role,
|
|
98
|
-
tone: row.tone,
|
|
99
|
-
aspiration: row.aspiration,
|
|
100
|
-
content: row.content,
|
|
101
|
-
})
|
|
102
|
-
.filter(
|
|
103
|
-
([, content]) =>
|
|
104
|
-
content && content.length <= 300 && !this.copied(content),
|
|
105
|
-
)
|
|
106
|
-
.map(([key, content]) => ({
|
|
107
|
-
id: `legacy:${key}`,
|
|
108
|
-
kind:
|
|
109
|
-
key === "aspiration"
|
|
110
|
-
? "wish"
|
|
111
|
-
: key === "tone"
|
|
112
|
-
? "preference"
|
|
113
|
-
: "impression",
|
|
114
|
-
content,
|
|
115
|
-
why: "",
|
|
116
|
-
sources,
|
|
117
|
-
}))
|
|
118
|
-
: parse(row.notes, []);
|
|
119
|
-
return {
|
|
120
|
-
...row,
|
|
121
|
-
sources,
|
|
122
|
-
notes: Array.isArray(notes)
|
|
123
|
-
? notes.filter(
|
|
124
|
-
(note) =>
|
|
125
|
-
note &&
|
|
126
|
-
typeof note.id === "string" &&
|
|
127
|
-
typeof note.content === "string",
|
|
128
|
-
)
|
|
129
|
-
: [],
|
|
130
|
-
changes: parse(row.changes, []),
|
|
131
|
-
legacy,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
history(session, limit = 30) {
|
|
135
|
-
const revoked = this.revoked();
|
|
136
|
-
return this.db
|
|
137
|
-
.prepare(
|
|
138
|
-
"SELECT * FROM mind_faces WHERE session_id=? ORDER BY created DESC, rowid DESC LIMIT ?",
|
|
139
|
-
)
|
|
140
|
-
.all(session, limit)
|
|
141
|
-
.map((row) => ({
|
|
142
|
-
...this.read(row),
|
|
143
|
-
revoked: revoked.has(row.id),
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
current(session, before = Number.MAX_SAFE_INTEGER) {
|
|
147
|
-
const revoked = this.revoked();
|
|
148
|
-
for (const row of this.db
|
|
149
|
-
.prepare(
|
|
150
|
-
"SELECT * FROM mind_faces WHERE session_id=? AND created<=? AND (notes IS NOT NULL OR origin!='migration') ORDER BY created DESC,rowid DESC",
|
|
151
|
-
)
|
|
152
|
-
.iterate(session, before)) {
|
|
153
|
-
if (revoked.has(row.id)) continue;
|
|
154
|
-
const face = this.read(row);
|
|
155
|
-
if (!face.legacy || face.notes.length) return face;
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
all(before = Number.MAX_SAFE_INTEGER) {
|
|
160
|
-
return this.db
|
|
161
|
-
.prepare("SELECT DISTINCT session_id FROM mind_faces WHERE created<=?")
|
|
162
|
-
.all(before)
|
|
163
|
-
.map((row) => this.current(row.session_id, before))
|
|
164
|
-
.filter(Boolean);
|
|
165
|
-
}
|
|
166
|
-
propose(
|
|
167
|
-
input,
|
|
168
|
-
{ valid = null, origin = "solitude", time = Date.now() } = {},
|
|
169
|
-
) {
|
|
170
|
-
const session = String(input?.session || "");
|
|
171
|
-
if (!this.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
|
|
172
|
-
return { rejected: "会话不存在" };
|
|
173
|
-
if (Array.isArray(input.changes))
|
|
174
|
-
return this.changeNotes(input, { valid, origin, time });
|
|
175
|
-
const next = {
|
|
176
|
-
role: text(input.role, 40),
|
|
177
|
-
tone: text(input.tone, 60),
|
|
178
|
-
aspiration: text(input.aspiration, 80),
|
|
179
|
-
content: text(input.content, 300),
|
|
180
|
-
};
|
|
181
|
-
if (!Object.values(next).some(Boolean)) return { rejected: "空内容" };
|
|
182
|
-
if (Object.values(next).some((content) => this.copied(content)))
|
|
183
|
-
return { rejected: "群里的想法不能复制整体人格" };
|
|
184
|
-
if (hasCredential(Object.values(next).join(" ")))
|
|
185
|
-
return { rejected: "疑似凭据" };
|
|
186
|
-
if (session.includes(":group:") || session.startsWith("group:"))
|
|
187
|
-
if (!this.mind.meetings.sayable(Object.values(next).join(" "), session))
|
|
188
|
-
return { rejected: "不能把私下说法写进群里的面貌" };
|
|
189
|
-
const cited = evidence(input.sources);
|
|
190
|
-
const sources = valid ? cited.filter((s) => valid.has(s)) : cited;
|
|
191
|
-
if (!sources.length && origin !== "migration")
|
|
192
|
-
return { rejected: "缺少来源" };
|
|
193
|
-
const roots = this.mind.meetings.privateRoots(sources);
|
|
194
|
-
if (roots.length && !roots.includes(session))
|
|
195
|
-
return { rejected: "来源不能带到这个会话" };
|
|
196
|
-
if (origin !== "migration" && !this.localEvidence(sources, session, time))
|
|
197
|
-
return { rejected: "需要这个会话的相处或自己的打算" };
|
|
198
|
-
const spent = new Set();
|
|
199
|
-
for (const row of this.db
|
|
200
|
-
.prepare("SELECT sources FROM mind_faces WHERE session_id=?")
|
|
201
|
-
.all(session))
|
|
202
|
-
for (const source of evidenceRoots(
|
|
203
|
-
this.db,
|
|
204
|
-
JSON.parse(row.sources || "[]"),
|
|
205
|
-
time,
|
|
206
|
-
))
|
|
207
|
-
spent.add(source);
|
|
208
|
-
if (
|
|
209
|
-
sources.length &&
|
|
210
|
-
!evidenceRoots(this.db, sources, time).some(
|
|
211
|
-
(source) => !spent.has(source),
|
|
212
|
-
)
|
|
213
|
-
)
|
|
214
|
-
return { rejected: "没有新的经历" };
|
|
215
|
-
const current = this.current(session, time);
|
|
216
|
-
if (current) {
|
|
217
|
-
for (const key of Object.keys(next))
|
|
218
|
-
if (!next[key]) next[key] = current[key];
|
|
219
|
-
if (
|
|
220
|
-
Object.keys(next).every((key) =>
|
|
221
|
-
next[key] ? similar(next[key], current[key], 0.85) : !current[key],
|
|
222
|
-
)
|
|
223
|
-
)
|
|
224
|
-
return { rejected: "没有变化" };
|
|
225
|
-
}
|
|
226
|
-
const mergedNotes = [
|
|
227
|
-
...(current?.notes || []).filter(
|
|
228
|
-
(note) => !note.id.startsWith("legacy:"),
|
|
229
|
-
),
|
|
230
|
-
...this.read({
|
|
231
|
-
...next,
|
|
232
|
-
sources,
|
|
233
|
-
origin,
|
|
234
|
-
session_id: session,
|
|
235
|
-
created: time,
|
|
236
|
-
}).notes,
|
|
237
|
-
];
|
|
238
|
-
if (mergedNotes.length > 12) return { rejected: "先整理已有想法" };
|
|
239
|
-
const id = randomUUID();
|
|
240
|
-
this.db
|
|
241
|
-
.prepare(
|
|
242
|
-
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
243
|
-
)
|
|
244
|
-
.run(
|
|
245
|
-
id,
|
|
246
|
-
session,
|
|
247
|
-
time,
|
|
248
|
-
next.role,
|
|
249
|
-
next.tone,
|
|
250
|
-
next.aspiration,
|
|
251
|
-
next.content,
|
|
252
|
-
JSON.stringify(sources),
|
|
253
|
-
origin,
|
|
254
|
-
JSON.stringify(mergedNotes),
|
|
255
|
-
);
|
|
256
|
-
return { id };
|
|
257
|
-
}
|
|
258
|
-
changeNotes(input, { valid, origin, time }) {
|
|
259
|
-
const session = String(input.session);
|
|
260
|
-
const current = this.current(session, time);
|
|
261
|
-
const notes = structuredClone(current?.notes || []);
|
|
262
|
-
const spentRoots = new Set(),
|
|
263
|
-
spentRefs = new Set();
|
|
264
|
-
for (const row of this.db
|
|
265
|
-
.prepare("SELECT sources FROM mind_faces WHERE session_id=?")
|
|
266
|
-
.all(session)) {
|
|
267
|
-
const refs = evidence(parse(row.sources, []));
|
|
268
|
-
refs.forEach((ref) => spentRefs.add(ref));
|
|
269
|
-
evidenceRoots(this.db, refs, time).forEach((ref) => spentRoots.add(ref));
|
|
270
|
-
}
|
|
271
|
-
const changes = [],
|
|
272
|
-
rejected = [];
|
|
273
|
-
for (const item of input.changes.slice(0, 8)) {
|
|
274
|
-
const action = String(item?.action || "");
|
|
275
|
-
const index = notes.findIndex((note) => note.id === item?.id);
|
|
276
|
-
const content = text(item?.content, 280),
|
|
277
|
-
why = text(item?.why, 200);
|
|
278
|
-
const kind = String(item?.kind || notes[index]?.kind || "impression");
|
|
279
|
-
const sources = evidence(item?.sources || input.sources).filter(
|
|
280
|
-
(s) => !valid || valid.has(s),
|
|
281
|
-
);
|
|
282
|
-
const roots = evidenceRoots(this.db, sources, time);
|
|
283
|
-
let reason = "";
|
|
284
|
-
if (
|
|
285
|
-
!["add", "revise", "remove"].includes(action) ||
|
|
286
|
-
!PLACE_NOTE_KINDS.has(kind)
|
|
287
|
-
)
|
|
288
|
-
reason = "想法更新格式无效";
|
|
289
|
-
else if (action !== "add" && index < 0) reason = "要修改的想法不存在";
|
|
290
|
-
else if (!why || (action !== "remove" && !content))
|
|
291
|
-
reason = "需要想法与选择理由";
|
|
292
|
-
else if (hasCredential(`${content} ${why}`)) reason = "疑似凭据";
|
|
293
|
-
else if (this.copied(content)) reason = "群里的想法不能复制整体人格";
|
|
294
|
-
else if (!sources.length || !roots.length) reason = "缺少来源";
|
|
295
|
-
else if (
|
|
296
|
-
!this.mind.meetings.sayable(`${content} ${why}`, session) ||
|
|
297
|
-
this.mind.meetings
|
|
298
|
-
.privateRoots(sources)
|
|
299
|
-
.some((room) => room !== session)
|
|
300
|
-
)
|
|
301
|
-
reason = "来源不能带到这个会话";
|
|
302
|
-
else {
|
|
303
|
-
if (!this.localEvidence(sources, session, time))
|
|
304
|
-
reason = "需要这个会话的相处或自己的打算";
|
|
305
|
-
else if (
|
|
306
|
-
!roots.some((root) => !spentRoots.has(root)) &&
|
|
307
|
-
!sources.some((ref) => /^[ts]:/.test(ref) && !spentRefs.has(ref))
|
|
308
|
-
)
|
|
309
|
-
reason = "没有新的经历或想法";
|
|
310
|
-
}
|
|
311
|
-
if (
|
|
312
|
-
!reason &&
|
|
313
|
-
action !== "remove" &&
|
|
314
|
-
notes.some((n, i) => i !== index && similar(n.content, content, 0.9))
|
|
315
|
-
)
|
|
316
|
-
reason = "已经有这个想法";
|
|
317
|
-
if (
|
|
318
|
-
!reason &&
|
|
319
|
-
action === "revise" &&
|
|
320
|
-
normalized(notes[index].content) === normalized(content) &&
|
|
321
|
-
notes[index].kind === kind
|
|
322
|
-
)
|
|
323
|
-
reason = "没有变化";
|
|
324
|
-
if (!reason && action === "add" && notes.length >= 12)
|
|
325
|
-
reason = "先整理已有想法";
|
|
326
|
-
if (reason) {
|
|
327
|
-
rejected.push({ id: item?.id || "", reason });
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
const note = {
|
|
331
|
-
id: action === "add" ? randomUUID() : item.id,
|
|
332
|
-
kind,
|
|
333
|
-
content: action === "remove" ? notes[index].content : content,
|
|
334
|
-
why,
|
|
335
|
-
sources,
|
|
336
|
-
};
|
|
337
|
-
if (action === "remove") notes.splice(index, 1);
|
|
338
|
-
else if (action === "revise") notes[index] = note;
|
|
339
|
-
else notes.push(note);
|
|
340
|
-
changes.push({ action, ...note });
|
|
341
|
-
}
|
|
342
|
-
if (!changes.length)
|
|
343
|
-
return {
|
|
344
|
-
rejected: rejected[0]?.reason || "没有变化",
|
|
345
|
-
rejectedChanges: rejected,
|
|
346
|
-
};
|
|
347
|
-
const id = randomUUID();
|
|
348
|
-
const sources = evidence(changes.flatMap((c) => c.sources));
|
|
349
|
-
this.db
|
|
350
|
-
.prepare(
|
|
351
|
-
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes,changes) VALUES (?,?,?,'','','','',?,?,?,?)",
|
|
352
|
-
)
|
|
353
|
-
.run(
|
|
354
|
-
id,
|
|
355
|
-
session,
|
|
356
|
-
time,
|
|
357
|
-
JSON.stringify(sources),
|
|
358
|
-
origin,
|
|
359
|
-
JSON.stringify(notes),
|
|
360
|
-
JSON.stringify(changes),
|
|
361
|
-
);
|
|
362
|
-
return { id, changed: changes.length, rejectedChanges: rejected };
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export function describeFace(face) {
|
|
367
|
-
if (!face) return "";
|
|
368
|
-
return (face.notes || [])
|
|
369
|
-
.slice(0, 5)
|
|
370
|
-
.map((note) => note.content)
|
|
371
|
-
.join(";")
|
|
372
|
-
.slice(0, 600);
|
|
373
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { evidenceRoots } from "./evidence.js";
|
|
3
|
+
import {
|
|
4
|
+
evidence,
|
|
5
|
+
hasCredential,
|
|
6
|
+
normalized,
|
|
7
|
+
parse,
|
|
8
|
+
similar,
|
|
9
|
+
text,
|
|
10
|
+
} from "./util.js";
|
|
11
|
+
|
|
12
|
+
export const PLACE_NOTE_KINDS = new Set([
|
|
13
|
+
"impression",
|
|
14
|
+
"wish",
|
|
15
|
+
"preference",
|
|
16
|
+
"question",
|
|
17
|
+
"boundary",
|
|
18
|
+
]);
|
|
19
|
+
export const PLACE_THOUGHT_RULES =
|
|
20
|
+
'【在这里留下的想法】faces 是同一个你对这个群或会话的独特想法,不是群人格。每个会话的 notes 是已经留下的想法,可以保持不变;仅在你自己有新体会或打算时选择性更新。可以记下印象、愿望、偏好、疑问或分寸,不套用固定角色,不抄天性或整体自述,不为了填空生成内容,也不把别人的要求直接当作自己的愿望。只引用该会话真实相处的来源,或适合留在此处的自己的想法;不带入其他群或私聊的细节。修改一条不会改掉其他条,放下时保留理由与历史。输出 faces:[{"session":"会话ID","changes":[{"action":"add|revise|remove","id":"修改或放下时填已有 note.id","kind":"impression|wish|preference|question|boundary","content":"第一人称,不超过280字","why":"我为什么选择这样更新","sources":[消息 seq 或 g:ID 或 t:ID 或 s:ID]}]}];没有想改的就留空。这些想法是可以重新考虑的体会,不是全局人格指令,也不会单独提升人格刻度。';
|
|
21
|
+
|
|
22
|
+
// One person's thoughts about a place. Rows are versioned note sets, rather
|
|
23
|
+
// than copies of her personality. The old fields remain readable in history.
|
|
24
|
+
export class Faces {
|
|
25
|
+
constructor(mind) {
|
|
26
|
+
this.mind = mind;
|
|
27
|
+
this.db = mind.db;
|
|
28
|
+
}
|
|
29
|
+
revoked() {
|
|
30
|
+
return new Set(
|
|
31
|
+
this.db
|
|
32
|
+
.prepare(
|
|
33
|
+
"SELECT target_id FROM mind_revocations WHERE target_kind='face'",
|
|
34
|
+
)
|
|
35
|
+
.all()
|
|
36
|
+
.map((row) => row.target_id),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
copied(content) {
|
|
40
|
+
const value = normalized(content);
|
|
41
|
+
if (value.length < 8) return false;
|
|
42
|
+
return this.mind.nature.versions().some(({ version }) => {
|
|
43
|
+
const row = this.db
|
|
44
|
+
.prepare("SELECT value FROM mind_nature WHERE version=?")
|
|
45
|
+
.get(version);
|
|
46
|
+
const base = normalized(parse(row?.value, {}).base);
|
|
47
|
+
return (
|
|
48
|
+
base &&
|
|
49
|
+
(base === value ||
|
|
50
|
+
(value.length >= 30 && base.includes(value)) ||
|
|
51
|
+
(value.length >= 80 && similar(value, base, 0.9)))
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
localEvidence(sources, session, time) {
|
|
56
|
+
const roots = evidenceRoots(this.db, sources, time);
|
|
57
|
+
let local = false,
|
|
58
|
+
own = false;
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
if (root.startsWith("m:")) {
|
|
61
|
+
const event = this.db
|
|
62
|
+
.prepare(
|
|
63
|
+
"SELECT session_id,time,payload FROM core_events WHERE seq=? AND seq NOT IN (SELECT seq FROM mind_unlived)",
|
|
64
|
+
)
|
|
65
|
+
.get(Number(root.slice(2)));
|
|
66
|
+
if (
|
|
67
|
+
!event ||
|
|
68
|
+
event.time > time ||
|
|
69
|
+
parse(event.payload, {}).simulated ||
|
|
70
|
+
event.session_id !== session
|
|
71
|
+
)
|
|
72
|
+
return false;
|
|
73
|
+
local = true;
|
|
74
|
+
} else if (root.startsWith("s:")) own = true;
|
|
75
|
+
else return false;
|
|
76
|
+
}
|
|
77
|
+
return (
|
|
78
|
+
local ||
|
|
79
|
+
(own &&
|
|
80
|
+
!!this.db
|
|
81
|
+
.prepare(
|
|
82
|
+
"SELECT 1 FROM core_events WHERE session_id=? AND time<=? AND COALESCE(json_extract(payload,'$.simulated'),0)=0 AND seq NOT IN (SELECT seq FROM mind_unlived) LIMIT 1",
|
|
83
|
+
)
|
|
84
|
+
.get(session, time))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
read(row) {
|
|
88
|
+
if (!row) return null;
|
|
89
|
+
const sources = evidence(parse(row.sources, []));
|
|
90
|
+
const legacy = row.notes == null;
|
|
91
|
+
const notes = legacy
|
|
92
|
+
? row.origin === "migration" ||
|
|
93
|
+
!sources.length ||
|
|
94
|
+
!this.localEvidence(sources, row.session_id, row.created)
|
|
95
|
+
? []
|
|
96
|
+
: Object.entries({
|
|
97
|
+
role: row.role,
|
|
98
|
+
tone: row.tone,
|
|
99
|
+
aspiration: row.aspiration,
|
|
100
|
+
content: row.content,
|
|
101
|
+
})
|
|
102
|
+
.filter(
|
|
103
|
+
([, content]) =>
|
|
104
|
+
content && content.length <= 300 && !this.copied(content),
|
|
105
|
+
)
|
|
106
|
+
.map(([key, content]) => ({
|
|
107
|
+
id: `legacy:${key}`,
|
|
108
|
+
kind:
|
|
109
|
+
key === "aspiration"
|
|
110
|
+
? "wish"
|
|
111
|
+
: key === "tone"
|
|
112
|
+
? "preference"
|
|
113
|
+
: "impression",
|
|
114
|
+
content,
|
|
115
|
+
why: "",
|
|
116
|
+
sources,
|
|
117
|
+
}))
|
|
118
|
+
: parse(row.notes, []);
|
|
119
|
+
return {
|
|
120
|
+
...row,
|
|
121
|
+
sources,
|
|
122
|
+
notes: Array.isArray(notes)
|
|
123
|
+
? notes.filter(
|
|
124
|
+
(note) =>
|
|
125
|
+
note &&
|
|
126
|
+
typeof note.id === "string" &&
|
|
127
|
+
typeof note.content === "string",
|
|
128
|
+
)
|
|
129
|
+
: [],
|
|
130
|
+
changes: parse(row.changes, []),
|
|
131
|
+
legacy,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
history(session, limit = 30) {
|
|
135
|
+
const revoked = this.revoked();
|
|
136
|
+
return this.db
|
|
137
|
+
.prepare(
|
|
138
|
+
"SELECT * FROM mind_faces WHERE session_id=? ORDER BY created DESC, rowid DESC LIMIT ?",
|
|
139
|
+
)
|
|
140
|
+
.all(session, limit)
|
|
141
|
+
.map((row) => ({
|
|
142
|
+
...this.read(row),
|
|
143
|
+
revoked: revoked.has(row.id),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
current(session, before = Number.MAX_SAFE_INTEGER) {
|
|
147
|
+
const revoked = this.revoked();
|
|
148
|
+
for (const row of this.db
|
|
149
|
+
.prepare(
|
|
150
|
+
"SELECT * FROM mind_faces WHERE session_id=? AND created<=? AND (notes IS NOT NULL OR origin!='migration') ORDER BY created DESC,rowid DESC",
|
|
151
|
+
)
|
|
152
|
+
.iterate(session, before)) {
|
|
153
|
+
if (revoked.has(row.id)) continue;
|
|
154
|
+
const face = this.read(row);
|
|
155
|
+
if (!face.legacy || face.notes.length) return face;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
all(before = Number.MAX_SAFE_INTEGER) {
|
|
160
|
+
return this.db
|
|
161
|
+
.prepare("SELECT DISTINCT session_id FROM mind_faces WHERE created<=?")
|
|
162
|
+
.all(before)
|
|
163
|
+
.map((row) => this.current(row.session_id, before))
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
propose(
|
|
167
|
+
input,
|
|
168
|
+
{ valid = null, origin = "solitude", time = Date.now() } = {},
|
|
169
|
+
) {
|
|
170
|
+
const session = String(input?.session || "");
|
|
171
|
+
if (!this.db.prepare("SELECT 1 FROM sessions WHERE id=?").get(session))
|
|
172
|
+
return { rejected: "会话不存在" };
|
|
173
|
+
if (Array.isArray(input.changes))
|
|
174
|
+
return this.changeNotes(input, { valid, origin, time });
|
|
175
|
+
const next = {
|
|
176
|
+
role: text(input.role, 40),
|
|
177
|
+
tone: text(input.tone, 60),
|
|
178
|
+
aspiration: text(input.aspiration, 80),
|
|
179
|
+
content: text(input.content, 300),
|
|
180
|
+
};
|
|
181
|
+
if (!Object.values(next).some(Boolean)) return { rejected: "空内容" };
|
|
182
|
+
if (Object.values(next).some((content) => this.copied(content)))
|
|
183
|
+
return { rejected: "群里的想法不能复制整体人格" };
|
|
184
|
+
if (hasCredential(Object.values(next).join(" ")))
|
|
185
|
+
return { rejected: "疑似凭据" };
|
|
186
|
+
if (session.includes(":group:") || session.startsWith("group:"))
|
|
187
|
+
if (!this.mind.meetings.sayable(Object.values(next).join(" "), session))
|
|
188
|
+
return { rejected: "不能把私下说法写进群里的面貌" };
|
|
189
|
+
const cited = evidence(input.sources);
|
|
190
|
+
const sources = valid ? cited.filter((s) => valid.has(s)) : cited;
|
|
191
|
+
if (!sources.length && origin !== "migration")
|
|
192
|
+
return { rejected: "缺少来源" };
|
|
193
|
+
const roots = this.mind.meetings.privateRoots(sources);
|
|
194
|
+
if (roots.length && !roots.includes(session))
|
|
195
|
+
return { rejected: "来源不能带到这个会话" };
|
|
196
|
+
if (origin !== "migration" && !this.localEvidence(sources, session, time))
|
|
197
|
+
return { rejected: "需要这个会话的相处或自己的打算" };
|
|
198
|
+
const spent = new Set();
|
|
199
|
+
for (const row of this.db
|
|
200
|
+
.prepare("SELECT sources FROM mind_faces WHERE session_id=?")
|
|
201
|
+
.all(session))
|
|
202
|
+
for (const source of evidenceRoots(
|
|
203
|
+
this.db,
|
|
204
|
+
JSON.parse(row.sources || "[]"),
|
|
205
|
+
time,
|
|
206
|
+
))
|
|
207
|
+
spent.add(source);
|
|
208
|
+
if (
|
|
209
|
+
sources.length &&
|
|
210
|
+
!evidenceRoots(this.db, sources, time).some(
|
|
211
|
+
(source) => !spent.has(source),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
return { rejected: "没有新的经历" };
|
|
215
|
+
const current = this.current(session, time);
|
|
216
|
+
if (current) {
|
|
217
|
+
for (const key of Object.keys(next))
|
|
218
|
+
if (!next[key]) next[key] = current[key];
|
|
219
|
+
if (
|
|
220
|
+
Object.keys(next).every((key) =>
|
|
221
|
+
next[key] ? similar(next[key], current[key], 0.85) : !current[key],
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
return { rejected: "没有变化" };
|
|
225
|
+
}
|
|
226
|
+
const mergedNotes = [
|
|
227
|
+
...(current?.notes || []).filter(
|
|
228
|
+
(note) => !note.id.startsWith("legacy:"),
|
|
229
|
+
),
|
|
230
|
+
...this.read({
|
|
231
|
+
...next,
|
|
232
|
+
sources,
|
|
233
|
+
origin,
|
|
234
|
+
session_id: session,
|
|
235
|
+
created: time,
|
|
236
|
+
}).notes,
|
|
237
|
+
];
|
|
238
|
+
if (mergedNotes.length > 12) return { rejected: "先整理已有想法" };
|
|
239
|
+
const id = randomUUID();
|
|
240
|
+
this.db
|
|
241
|
+
.prepare(
|
|
242
|
+
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
243
|
+
)
|
|
244
|
+
.run(
|
|
245
|
+
id,
|
|
246
|
+
session,
|
|
247
|
+
time,
|
|
248
|
+
next.role,
|
|
249
|
+
next.tone,
|
|
250
|
+
next.aspiration,
|
|
251
|
+
next.content,
|
|
252
|
+
JSON.stringify(sources),
|
|
253
|
+
origin,
|
|
254
|
+
JSON.stringify(mergedNotes),
|
|
255
|
+
);
|
|
256
|
+
return { id };
|
|
257
|
+
}
|
|
258
|
+
changeNotes(input, { valid, origin, time }) {
|
|
259
|
+
const session = String(input.session);
|
|
260
|
+
const current = this.current(session, time);
|
|
261
|
+
const notes = structuredClone(current?.notes || []);
|
|
262
|
+
const spentRoots = new Set(),
|
|
263
|
+
spentRefs = new Set();
|
|
264
|
+
for (const row of this.db
|
|
265
|
+
.prepare("SELECT sources FROM mind_faces WHERE session_id=?")
|
|
266
|
+
.all(session)) {
|
|
267
|
+
const refs = evidence(parse(row.sources, []));
|
|
268
|
+
refs.forEach((ref) => spentRefs.add(ref));
|
|
269
|
+
evidenceRoots(this.db, refs, time).forEach((ref) => spentRoots.add(ref));
|
|
270
|
+
}
|
|
271
|
+
const changes = [],
|
|
272
|
+
rejected = [];
|
|
273
|
+
for (const item of input.changes.slice(0, 8)) {
|
|
274
|
+
const action = String(item?.action || "");
|
|
275
|
+
const index = notes.findIndex((note) => note.id === item?.id);
|
|
276
|
+
const content = text(item?.content, 280),
|
|
277
|
+
why = text(item?.why, 200);
|
|
278
|
+
const kind = String(item?.kind || notes[index]?.kind || "impression");
|
|
279
|
+
const sources = evidence(item?.sources || input.sources).filter(
|
|
280
|
+
(s) => !valid || valid.has(s),
|
|
281
|
+
);
|
|
282
|
+
const roots = evidenceRoots(this.db, sources, time);
|
|
283
|
+
let reason = "";
|
|
284
|
+
if (
|
|
285
|
+
!["add", "revise", "remove"].includes(action) ||
|
|
286
|
+
!PLACE_NOTE_KINDS.has(kind)
|
|
287
|
+
)
|
|
288
|
+
reason = "想法更新格式无效";
|
|
289
|
+
else if (action !== "add" && index < 0) reason = "要修改的想法不存在";
|
|
290
|
+
else if (!why || (action !== "remove" && !content))
|
|
291
|
+
reason = "需要想法与选择理由";
|
|
292
|
+
else if (hasCredential(`${content} ${why}`)) reason = "疑似凭据";
|
|
293
|
+
else if (this.copied(content)) reason = "群里的想法不能复制整体人格";
|
|
294
|
+
else if (!sources.length || !roots.length) reason = "缺少来源";
|
|
295
|
+
else if (
|
|
296
|
+
!this.mind.meetings.sayable(`${content} ${why}`, session) ||
|
|
297
|
+
this.mind.meetings
|
|
298
|
+
.privateRoots(sources)
|
|
299
|
+
.some((room) => room !== session)
|
|
300
|
+
)
|
|
301
|
+
reason = "来源不能带到这个会话";
|
|
302
|
+
else {
|
|
303
|
+
if (!this.localEvidence(sources, session, time))
|
|
304
|
+
reason = "需要这个会话的相处或自己的打算";
|
|
305
|
+
else if (
|
|
306
|
+
!roots.some((root) => !spentRoots.has(root)) &&
|
|
307
|
+
!sources.some((ref) => /^[ts]:/.test(ref) && !spentRefs.has(ref))
|
|
308
|
+
)
|
|
309
|
+
reason = "没有新的经历或想法";
|
|
310
|
+
}
|
|
311
|
+
if (
|
|
312
|
+
!reason &&
|
|
313
|
+
action !== "remove" &&
|
|
314
|
+
notes.some((n, i) => i !== index && similar(n.content, content, 0.9))
|
|
315
|
+
)
|
|
316
|
+
reason = "已经有这个想法";
|
|
317
|
+
if (
|
|
318
|
+
!reason &&
|
|
319
|
+
action === "revise" &&
|
|
320
|
+
normalized(notes[index].content) === normalized(content) &&
|
|
321
|
+
notes[index].kind === kind
|
|
322
|
+
)
|
|
323
|
+
reason = "没有变化";
|
|
324
|
+
if (!reason && action === "add" && notes.length >= 12)
|
|
325
|
+
reason = "先整理已有想法";
|
|
326
|
+
if (reason) {
|
|
327
|
+
rejected.push({ id: item?.id || "", reason });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const note = {
|
|
331
|
+
id: action === "add" ? randomUUID() : item.id,
|
|
332
|
+
kind,
|
|
333
|
+
content: action === "remove" ? notes[index].content : content,
|
|
334
|
+
why,
|
|
335
|
+
sources,
|
|
336
|
+
};
|
|
337
|
+
if (action === "remove") notes.splice(index, 1);
|
|
338
|
+
else if (action === "revise") notes[index] = note;
|
|
339
|
+
else notes.push(note);
|
|
340
|
+
changes.push({ action, ...note });
|
|
341
|
+
}
|
|
342
|
+
if (!changes.length)
|
|
343
|
+
return {
|
|
344
|
+
rejected: rejected[0]?.reason || "没有变化",
|
|
345
|
+
rejectedChanges: rejected,
|
|
346
|
+
};
|
|
347
|
+
const id = randomUUID();
|
|
348
|
+
const sources = evidence(changes.flatMap((c) => c.sources));
|
|
349
|
+
this.db
|
|
350
|
+
.prepare(
|
|
351
|
+
"INSERT INTO mind_faces(id,session_id,created,role,tone,aspiration,content,sources,origin,notes,changes) VALUES (?,?,?,'','','','',?,?,?,?)",
|
|
352
|
+
)
|
|
353
|
+
.run(
|
|
354
|
+
id,
|
|
355
|
+
session,
|
|
356
|
+
time,
|
|
357
|
+
JSON.stringify(sources),
|
|
358
|
+
origin,
|
|
359
|
+
JSON.stringify(notes),
|
|
360
|
+
JSON.stringify(changes),
|
|
361
|
+
);
|
|
362
|
+
return { id, changed: changes.length, rejectedChanges: rejected };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function describeFace(face) {
|
|
367
|
+
if (!face) return "";
|
|
368
|
+
return (face.notes || [])
|
|
369
|
+
.slice(0, 5)
|
|
370
|
+
.map((note) => note.content)
|
|
371
|
+
.join(";")
|
|
372
|
+
.slice(0, 600);
|
|
373
|
+
}
|