luckytri 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +2 -0
  3. package/README.md +2 -0
  4. package/docs/en/CHANGELOG.md +20 -13
  5. package/docs/en/guide.md +7 -5
  6. package/docs/en/her-life.md +2 -2
  7. package/docs/en/webui.md +1 -1
  8. package/docs/zh/CHANGELOG.md +20 -13
  9. package/docs/zh/guide.md +7 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +2 -1
  13. package/public/app/assets/ChatsPage-CFMVozoG.css +1 -0
  14. package/public/app/assets/{ChatsPage-CshdtTzV.js → ChatsPage-DCYMkJ91.js} +2 -2
  15. package/public/app/assets/{Empty-C7yzOBzy.js → Empty-CLyZkxR5.js} +1 -1
  16. package/public/app/assets/{Empty-DmUB2-Bk.css → Empty-DIobYG3x.css} +1 -1
  17. package/public/app/assets/{Field-BjFv4Rfn.js → Field-BpaRwLI_.js} +1 -1
  18. package/public/app/assets/{HeartPage-eqo6ru16.js → HeartPage-BIOMTiwr.js} +1 -1
  19. package/public/app/assets/HeartPage-C-2jvgzF.css +1 -0
  20. package/public/app/assets/{LifePage-CN8GE5NK.js → LifePage-BCgM4JkR.js} +1 -1
  21. package/public/app/assets/LifePage-CG1OHbRK.css +1 -0
  22. package/public/app/assets/MemoryPage-C_Bu4rfb.css +1 -0
  23. package/public/app/assets/{MemoryPage-CCpMN4Wg.js → MemoryPage-vShvRqMt.js} +1 -1
  24. package/public/app/assets/NaturePage-CPZlImT0.css +1 -0
  25. package/public/app/assets/{NaturePage-C0JqdBTR.js → NaturePage-Cy9ptr4G.js} +2 -2
  26. package/public/app/assets/PeoplePage-3RRUITDz.css +1 -0
  27. package/public/app/assets/{PeoplePage-BXGBbO4u.js → PeoplePage-CUZekB2E.js} +1 -1
  28. package/public/app/assets/{PluginsPage-Dksp5AV5.js → PluginsPage-1RM4rmJP.js} +1 -1
  29. package/public/app/assets/{SystemPage-BenIyTvk.css → SystemPage-BI2CB5Ig.css} +1 -1
  30. package/public/app/assets/{SystemPage-Cd7aa8eE.js → SystemPage-BeT5TmIV.js} +1 -1
  31. package/public/app/assets/TimePage-C-guwPk2.css +1 -0
  32. package/public/app/assets/{TimePage-Bs_zImb4.js → TimePage-taTmPMM8.js} +2 -2
  33. package/public/app/assets/{index-BHZcHUMh.css → index-DY5AM0Hz.css} +1 -1
  34. package/public/app/assets/{index-CyRtYxP3.js → index-DdYwC5ZU.js} +3 -3
  35. package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-DJmfu2KN.js} +1 -1
  36. package/public/app/assets/{media-Bwtx8blz.js → media-BEtnE0Gv.js} +1 -1
  37. package/public/app/assets/{mind-DtTVrW6Z.js → mind-Dtiw0-QK.js} +1 -1
  38. package/public/app/index.html +6 -5
  39. package/public/guide.en.html +11 -3
  40. package/public/guide.html +6 -3
  41. package/public/tutorial.en.md +2 -0
  42. package/public/tutorial.md +2 -0
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/service-supervisor.js +50 -0
  46. package/scripts/start-server.js +71 -16
  47. package/scripts/update-instance.js +201 -201
  48. package/server/channels/adapters.js +27 -27
  49. package/server/channels/onebot/adapter.js +288 -288
  50. package/server/channels/qqbot/adapter.js +253 -253
  51. package/server/channels/qqbot/api.js +199 -199
  52. package/server/channels/qqbot/credentials.js +11 -11
  53. package/server/channels/qqbot/gateway.js +274 -274
  54. package/server/channels/qqbot/index.js +389 -389
  55. package/server/channels/segments.js +40 -40
  56. package/server/channels/session-key.js +153 -153
  57. package/server/channels/state.js +42 -42
  58. package/server/core/api.js +564 -564
  59. package/server/core/context-builder.js +288 -288
  60. package/server/core/context-compactor.js +454 -454
  61. package/server/core/conversation-cues.js +325 -325
  62. package/server/core/conversation-manager.js +87 -87
  63. package/server/core/feedback.js +8 -8
  64. package/server/core/group-style.js +72 -72
  65. package/server/core/initiative-context.js +66 -66
  66. package/server/core/input-budget.js +32 -32
  67. package/server/core/local-demo.js +98 -98
  68. package/server/core/message-manager.js +118 -118
  69. package/server/core/message-scheduler.js +78 -78
  70. package/server/core/model-json.js +190 -190
  71. package/server/core/model-presets.js +900 -900
  72. package/server/core/name-call.js +29 -29
  73. package/server/core/network.js +157 -157
  74. package/server/core/person-name.js +22 -22
  75. package/server/core/persona-manager.js +2 -1
  76. package/server/core/reply-delivery.js +404 -403
  77. package/server/core/reply-target-resolver.js +50 -50
  78. package/server/core/response-generator.js +50 -50
  79. package/server/core/response-validator.js +14 -2
  80. package/server/core/retired-prompts.js +51 -51
  81. package/server/core/sessions.js +109 -109
  82. package/server/core/speaker-names.js +92 -92
  83. package/server/core/topic-tracker.js +65 -65
  84. package/server/http.js +18 -18
  85. package/server/i18n/decisions.js +209 -206
  86. package/server/i18n/errors.js +4 -0
  87. package/server/i18n/index.js +144 -144
  88. package/server/i18n/models.js +111 -111
  89. package/server/knowledge/api.js +342 -342
  90. package/server/knowledge/embedding-profile.js +114 -114
  91. package/server/knowledge/index.js +13 -13
  92. package/server/knowledge/retrieval.js +93 -93
  93. package/server/knowledge/schema.js +158 -158
  94. package/server/knowledge/vector-index.js +90 -90
  95. package/server/mind/affect.js +265 -265
  96. package/server/mind/attention.js +200 -200
  97. package/server/mind/bonds.js +565 -565
  98. package/server/mind/clock.js +83 -83
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +60 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -323
  108. package/server/mind/life/diary.js +20 -0
  109. package/server/mind/life/growth.js +261 -254
  110. package/server/mind/life/index.js +10 -2
  111. package/server/mind/life/presence.js +305 -305
  112. package/server/mind/meetings.js +1069 -1069
  113. package/server/mind/memory-policy.js +48 -48
  114. package/server/mind/memory.js +888 -888
  115. package/server/mind/nature.js +358 -358
  116. package/server/mind/novelty.js +19 -19
  117. package/server/mind/periods.js +118 -118
  118. package/server/mind/reading.js +120 -120
  119. package/server/mind/relationship-context.js +71 -0
  120. package/server/mind/relationships.js +390 -279
  121. package/server/mind/salience.js +201 -201
  122. package/server/mind/schema.js +567 -567
  123. package/server/mind/self.js +484 -484
  124. package/server/mind/thoughts.js +274 -274
  125. package/server/mind/time/agenda.js +242 -242
  126. package/server/mind/time/api.js +200 -200
  127. package/server/mind/time/attention.js +34 -34
  128. package/server/mind/time/delivery-links.js +130 -130
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +51 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +432 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +9 -1
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-execution.js +2 -1
  141. package/server/runtime/update-manager.js +140 -140
  142. package/server/storage/data-package.js +298 -298
  143. package/server/storage/data-transfer-api.js +50 -50
  144. package/server/storage/data-transfer-worker.js +18 -18
  145. package/server/storage/data-transfer.js +416 -416
  146. package/server/studio/display-names.js +139 -139
  147. package/server/studio/management.js +102 -102
  148. package/server/studio/updates.js +144 -144
  149. package/server/version.js +10 -10
  150. package/public/app/assets/ChatsPage-DfyPCS9N.css +0 -1
  151. package/public/app/assets/HeartPage-Bft9wZp5.css +0 -1
  152. package/public/app/assets/LifePage-BUi5euBF.css +0 -1
  153. package/public/app/assets/MemoryPage-cZvXfkXb.css +0 -1
  154. package/public/app/assets/NaturePage-CHkpb5NK.css +0 -1
  155. package/public/app/assets/PeoplePage-BTU0H1pM.css +0 -1
  156. package/public/app/assets/TimePage-BOms_n4I.css +0 -1
@@ -1,279 +1,390 @@
1
- import { randomUUID } from "node:crypto";
2
- import { parseSessionKey } from "../channels/session-key.js";
3
- import { hasCredential, text } from "./util.js";
4
- import { normalized } from "./util.js";
5
- import { leaks } from "./guard.js";
6
-
7
- export const RELATIONSHIP_ROLES = [
8
- "妹妹",
9
- "姐姐",
10
- "弟弟",
11
- "哥哥",
12
- "朋友",
13
- "伙伴",
14
- ];
15
- const FAMILY = new Set(RELATIONSHIP_ROLES.slice(0, 4));
16
- const scope = (room) => {
17
- try {
18
- return room ? parseSessionKey(room) : null;
19
- } catch {
20
- return null;
21
- }
22
- };
23
-
24
- // Account identity and an explicit arrangement, never invented encounters or
25
- // earned trust. Changes append history; nickname equality does not bind peers.
26
- export class Relationships {
27
- constructor(mind) {
28
- this.mind = mind;
29
- this.db = mind.db;
30
- }
31
- version() {
32
- return this.db
33
- .prepare("SELECT COALESCE(MAX(rowid),0) n FROM mind_relationships")
34
- .get().n;
35
- }
36
- current(subject, { now = Date.now(), room = "" } = {}) {
37
- const place = scope(room);
38
- const row = this.db
39
- .prepare(
40
- "SELECT * FROM mind_relationships WHERE subject_id=? AND created<=? ORDER BY created DESC,rowid DESC",
41
- )
42
- .all(String(subject), now)
43
- .find(
44
- (r) =>
45
- !place ||
46
- (r.channel === place.channel &&
47
- (r.account_id === "_" || r.account_id === place.accountId)),
48
- );
49
- return row?.status === "active" ? row : null;
50
- }
51
- visible(row, room) {
52
- if (!row || !room || row.discretion === "open") return !!row;
53
- const place = scope(room);
54
- return (
55
- place?.kind === "private" &&
56
- place.nativeId === row.subject_id &&
57
- place.channel === row.channel &&
58
- (row.account_id === "_" || place.accountId === row.account_id)
59
- );
60
- }
61
- discloses(bubbles, room, now = Date.now()) {
62
- const hidden = this.list(now).filter((row) => !this.visible(row, room));
63
- if (
64
- leaks(
65
- bubbles,
66
- hidden.filter((r) => r.note).map((r) => ({ content: r.note })),
67
- ).length
68
- )
69
- return true;
70
- const said = normalized(bubbles.join(" "));
71
- const english = {
72
- 妹妹: "youngersister",
73
- 姐姐: "oldersister",
74
- 弟弟: "youngerbrother",
75
- 哥哥: "olderbrother",
76
- 朋友: "friend",
77
- 伙伴: "companion",
78
- };
79
- return hidden.some((row) => {
80
- const names = [
81
- row.name,
82
- this.mind.bonds.name(row.subject_id),
83
- row.subject_id,
84
- ]
85
- .filter(Boolean)
86
- .map(normalized);
87
- const role = normalized(row.peer_role),
88
- en = english[row.peer_role] || role;
89
- return names.some((name) =>
90
- [
91
- "我的" + role + name,
92
- "我" + role + name,
93
- name + "是我的" + role,
94
- name + "是我" + role,
95
- name + role,
96
- "my" + en + name,
97
- name + "ismy" + en,
98
- ].some((line) => said.includes(line)),
99
- );
100
- });
101
- }
102
- weight(row) {
103
- return row ? (FAMILY.has(row.peer_role) ? 0.15 : 0.05) : 0;
104
- }
105
- view(row, room = "") {
106
- if (!this.visible(row, room)) return null;
107
- return {
108
- id: row.id,
109
- subjectId: row.subject_id,
110
- channel: row.channel,
111
- accountId: row.account_id,
112
- kind: row.kind,
113
- name: row.name,
114
- peerRole: row.peer_role,
115
- selfRole: row.self_role,
116
- discretion: row.discretion,
117
- note: row.note,
118
- created: row.created,
119
- origin: "owner",
120
- };
121
- }
122
- list(now = Date.now()) {
123
- const seen = new Set();
124
- return this.db
125
- .prepare(
126
- "SELECT * FROM mind_relationships WHERE created<=? ORDER BY created DESC,rowid DESC",
127
- )
128
- .all(now)
129
- .filter((r) => {
130
- const key = JSON.stringify([r.subject_id, r.channel, r.account_id]);
131
- if (seen.has(key)) return false;
132
- seen.add(key);
133
- return r.status === "active";
134
- });
135
- }
136
- history(subject, limit = 30) {
137
- return this.db
138
- .prepare(
139
- "SELECT * FROM mind_relationships WHERE subject_id=? ORDER BY created DESC,rowid DESC LIMIT ?",
140
- )
141
- .all(String(subject), limit);
142
- }
143
- save(input, now = Date.now()) {
144
- const subject = text(input?.subjectId, 128),
145
- channel = String(input?.channel || "onebot"),
146
- account = text(input?.accountId || "_", 128);
147
- if (
148
- String(input?.subjectId || "").length > 128 ||
149
- String(input?.accountId || "").length > 128 ||
150
- !/^[\w.-]{1,128}$/.test(subject) ||
151
- ["bot", "self", "_"].includes(subject) ||
152
- !["onebot", "qqbot"].includes(channel) ||
153
- !/^[\w.-]{1,128}$/.test(account) ||
154
- (channel === "onebot" && !/^\d+$/.test(subject))
155
- )
156
- throw Error("关系对象账号无效");
157
- if (channel === "qqbot" && account === "_")
158
- throw Error("官方机器人关系需要指定应用账号");
159
- if (
160
- this.db
161
- .prepare("SELECT 1 FROM core_events WHERE account_id=? LIMIT 1")
162
- .get(subject)
163
- )
164
- throw Error("不能把当前实例绑定为自己的关系对象");
165
- const kind = String(input.kind || "human"),
166
- role = text(input.peerRole, 24),
167
- name = text(input.name, 40),
168
- note = text(input.note, 300),
169
- discretion = input.discretion || "open";
170
- if (
171
- !["human", "bot"].includes(kind) ||
172
- !role ||
173
- String(input.peerRole).length > 24 ||
174
- String(input.name || "").length > 40 ||
175
- String(input.note || "").length > 300 ||
176
- !["open", "private", "secret"].includes(discretion)
177
- )
178
- throw Error("关系设置无效");
179
- const gender = this.mind.nature.current(now).gender;
180
- const older =
181
- gender === "female" ? "姐姐" : gender === "male" ? "哥哥" : "年长手足";
182
- const younger =
183
- gender === "female" ? "妹妹" : gender === "male" ? "弟弟" : "年幼手足";
184
- const opposite = {
185
- 妹妹: older,
186
- 弟弟: older,
187
- 姐姐: younger,
188
- 哥哥: younger,
189
- 朋友: "朋友",
190
- 伙伴: "伙伴",
191
- };
192
- const selfRole = text(input.selfRole || opposite[role] || "", 24);
193
- if (
194
- !selfRole ||
195
- String(input.selfRole || "").length > 24 ||
196
- hasCredential([role, selfRole, name, note].join(" "))
197
- )
198
- throw Error("关系称呼或备注无效");
199
- const room = `${channel}:${account}:private:${subject}`,
200
- previous = this.current(subject, { now, room });
201
- if (
202
- input.expectedId !== undefined &&
203
- input.expectedId !== (previous?.id || null)
204
- )
205
- throw Error("关系已经变化,请刷新后再修改");
206
- if (
207
- previous &&
208
- previous.kind === kind &&
209
- previous.peer_role === role &&
210
- previous.self_role === selfRole &&
211
- previous.name === name &&
212
- previous.note === note &&
213
- previous.discretion === discretion
214
- )
215
- return this.view(previous);
216
- this.db.exec("BEGIN IMMEDIATE");
217
- try {
218
- this.db
219
- .prepare(
220
- "INSERT INTO mind_relationships(id,created,subject_id,channel,account_id,kind,name,peer_role,self_role,discretion,note,status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'active')",
221
- )
222
- .run(
223
- randomUUID(),
224
- now,
225
- subject,
226
- channel,
227
- account,
228
- kind,
229
- name,
230
- role,
231
- selfRole,
232
- discretion,
233
- note,
234
- );
235
- this.db
236
- .prepare(
237
- "INSERT INTO mind_people(user_id,name,first_seen,last_seen,sessions) VALUES (?,NULL,NULL,NULL,'[]') ON CONFLICT(user_id) DO NOTHING",
238
- )
239
- .run(subject);
240
- this.db.exec("COMMIT");
241
- this.mind.repo.store.revision++;
242
- return this.view(this.current(subject, { now, room }));
243
- } catch (error) {
244
- this.db.exec("ROLLBACK");
245
- throw error;
246
- }
247
- }
248
- end(subject, input = {}, now = Date.now()) {
249
- if (
250
- String(input.note || "").length > 300 ||
251
- hasCredential(String(input.note || ""))
252
- )
253
- throw Error("关系称呼或备注无效");
254
- const room = `${input.channel || "onebot"}:${input.accountId || "_"}:private:${subject}`,
255
- row = this.current(subject, { now, room });
256
- if (!row) throw Error("关系不存在或已解除");
257
- if (input.expectedId !== undefined && input.expectedId !== row.id)
258
- throw Error("关系已经变化,请刷新后再修改");
259
- this.db
260
- .prepare(
261
- "INSERT INTO mind_relationships(id,created,subject_id,channel,account_id,kind,name,peer_role,self_role,discretion,note,status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'ended')",
262
- )
263
- .run(
264
- randomUUID(),
265
- now,
266
- row.subject_id,
267
- row.channel,
268
- row.account_id,
269
- row.kind,
270
- row.name,
271
- row.peer_role,
272
- row.self_role,
273
- row.discretion,
274
- text(input.note || "在管理台解除关系", 300),
275
- );
276
- this.mind.repo.store.revision++;
277
- return { ended: true };
278
- }
279
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { parseSessionKey } from "../channels/session-key.js";
3
+ import { hasCredential, text } from "./util.js";
4
+ import { normalized } from "./util.js";
5
+ import { leaks } from "./guard.js";
6
+ import { mentionsRelationship } from "./relationship-context.js";
7
+
8
+ export const RELATIONSHIP_ROLES = [
9
+ "妹妹",
10
+ "姐姐",
11
+ "弟弟",
12
+ "哥哥",
13
+ "朋友",
14
+ "伙伴",
15
+ ];
16
+ const FAMILY = new Set(RELATIONSHIP_ROLES.slice(0, 4));
17
+ const scope = (room) => {
18
+ try {
19
+ return room ? parseSessionKey(room) : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ };
24
+
25
+ // Account identity and an explicit arrangement, never invented encounters or
26
+ // earned trust. Changes append history; nickname equality does not bind peers.
27
+ export class Relationships {
28
+ constructor(mind) {
29
+ this.mind = mind;
30
+ this.db = mind.db;
31
+ }
32
+ version() {
33
+ return this.db
34
+ .prepare("SELECT COALESCE(MAX(rowid),0) n FROM mind_relationships")
35
+ .get().n;
36
+ }
37
+ current(subject, { now = Date.now(), room = "" } = {}) {
38
+ const place = scope(room);
39
+ const row = this.db
40
+ .prepare(
41
+ "SELECT * FROM mind_relationships WHERE subject_id=? AND created<=? ORDER BY created DESC,rowid DESC",
42
+ )
43
+ .all(String(subject), now)
44
+ .find(
45
+ (r) =>
46
+ !place ||
47
+ (r.channel === place.channel &&
48
+ (r.account_id === "_" || r.account_id === place.accountId)),
49
+ );
50
+ return row?.status === "active" ? row : null;
51
+ }
52
+ visible(row, room) {
53
+ if (!row || !room || row.discretion === "open") return !!row;
54
+ const place = scope(room);
55
+ return (
56
+ place?.kind === "private" &&
57
+ place.nativeId === row.subject_id &&
58
+ place.channel === row.channel &&
59
+ (row.account_id === "_" || place.accountId === row.account_id)
60
+ );
61
+ }
62
+ discloses(bubbles, room, now = Date.now()) {
63
+ const hidden = this.list(now).filter((row) => !this.visible(row, room));
64
+ if (
65
+ leaks(
66
+ bubbles,
67
+ hidden.filter((r) => r.note).map((r) => ({ content: r.note })),
68
+ ).length
69
+ )
70
+ return true;
71
+ const said = normalized(bubbles.join(" "));
72
+ const english = {
73
+ 妹妹: "youngersister",
74
+ 姐姐: "oldersister",
75
+ 弟弟: "youngerbrother",
76
+ 哥哥: "olderbrother",
77
+ 朋友: "friend",
78
+ 伙伴: "companion",
79
+ };
80
+ return hidden.some((row) => {
81
+ const names = [
82
+ row.name,
83
+ this.mind.bonds.name(row.subject_id),
84
+ row.subject_id,
85
+ ]
86
+ .filter(Boolean)
87
+ .map(normalized);
88
+ const role = normalized(row.peer_role),
89
+ en = english[row.peer_role] || role;
90
+ return names.some((name) =>
91
+ [
92
+ "我的" + role + name,
93
+ "我" + role + name,
94
+ name + "是我的" + role,
95
+ name + "是我" + role,
96
+ name + role,
97
+ "my" + en + name,
98
+ name + "ismy" + en,
99
+ ].some((line) => said.includes(line)),
100
+ );
101
+ });
102
+ }
103
+ weight(row) {
104
+ return row ? (FAMILY.has(row.peer_role) ? 0.15 : 0.05) : 0;
105
+ }
106
+ view(row, room = "") {
107
+ if (!this.visible(row, room)) return null;
108
+ return {
109
+ id: row.id,
110
+ subjectId: row.subject_id,
111
+ channel: row.channel,
112
+ accountId: row.account_id,
113
+ kind: row.kind,
114
+ name: row.name,
115
+ peerRole: row.peer_role,
116
+ selfRole: row.self_role,
117
+ discretion: row.discretion,
118
+ note: row.note,
119
+ created: row.created,
120
+ origin: "owner",
121
+ };
122
+ }
123
+ list(now = Date.now()) {
124
+ const seen = new Set();
125
+ return this.db
126
+ .prepare(
127
+ "SELECT * FROM mind_relationships WHERE created<=? ORDER BY created DESC,rowid DESC",
128
+ )
129
+ .all(now)
130
+ .filter((r) => {
131
+ const key = JSON.stringify([r.subject_id, r.channel, r.account_id]);
132
+ if (seen.has(key)) return false;
133
+ seen.add(key);
134
+ return r.status === "active";
135
+ });
136
+ }
137
+ // Knowing someone does not require them to be speaking in this room.
138
+ // Select from the same current ledger used by the management UI, while
139
+ // keeping account identity and disclosure boundaries at the read boundary.
140
+ context({
141
+ room = "",
142
+ people = [],
143
+ cue = [],
144
+ now = Date.now(),
145
+ open = false,
146
+ limit = 6,
147
+ } = {}) {
148
+ const place = scope(room);
149
+ const present = new Set(people.map(String));
150
+ const words = (cue || [])
151
+ .filter((c) => typeof c !== "object" || c?.role !== "assistant")
152
+ .map((c) => (typeof c === "object" ? c?.text || "" : String(c)));
153
+ const rows = this.list(now)
154
+ .filter(
155
+ (r) => this.visible(r, room) && (!open || r.discretion === "open"),
156
+ )
157
+ .filter(
158
+ (r) =>
159
+ !place ||
160
+ (r.channel === place.channel &&
161
+ (r.account_id === "_" || r.account_id === place.accountId)),
162
+ )
163
+ .map((r) => {
164
+ const view = this.view(r, room);
165
+ const accountName = this.mind.bonds.name(r.subject_id);
166
+ view.name ||= accountName;
167
+ if (
168
+ accountName &&
169
+ accountName !== view.name &&
170
+ accountName !== r.subject_id
171
+ )
172
+ view.knownAs = [accountName];
173
+ const mentioned = words.some((word) =>
174
+ mentionsRelationship(word, view),
175
+ );
176
+ const speaker = present.has(r.subject_id);
177
+ return {
178
+ view,
179
+ mentioned,
180
+ score: Number(mentioned) * 2 + Number(speaker),
181
+ };
182
+ })
183
+ .sort((a, b) => b.score - a.score || b.view.created - a.view.created);
184
+ const selected = rows.slice(0, limit);
185
+ // Bond changes are intentionally capped; that counter is not the number
186
+ // of conversations. Count actual addressed replies without scanning the
187
+ // much larger model traces or assuming adjacent messages were exchanges.
188
+ const replies = selected.length
189
+ ? this.db
190
+ .prepare(
191
+ `
192
+ SELECT json_extract(t.payload,'$.userId') subject,
193
+ COALESCE(json_extract(t.payload,'$.channel'),'onebot') channel,
194
+ t.account_id account,COUNT(DISTINCT a.seq) replies,MAX(a.time) last
195
+ FROM core_events a JOIN json_each(a.payload,'$.replyTargetIds') refs
196
+ JOIN core_events t ON t.seq=refs.value
197
+ WHERE a.role='assistant' AND t.role='user' AND a.time<=? AND t.time<=?
198
+ AND COALESCE(json_extract(a.payload,'$.simulated'),0)=0
199
+ AND COALESCE(json_extract(t.payload,'$.simulated'),0)=0
200
+ AND t.seq NOT IN (SELECT seq FROM mind_unlived)
201
+ AND json_extract(t.payload,'$.userId') IN (${selected.map(() => "?").join(",")})
202
+ GROUP BY subject,channel,account
203
+ `,
204
+ )
205
+ .all(now, now, ...selected.map(({ view }) => view.subjectId))
206
+ : [];
207
+ return {
208
+ known: selected.map(({ view }) => {
209
+ const person = this.mind.bonds.person(view.subjectId, now, {
210
+ room: open ? "group:__open__" : room,
211
+ });
212
+ const history = replies.filter(
213
+ (r) =>
214
+ String(r.subject) === view.subjectId &&
215
+ r.channel === view.channel &&
216
+ (view.accountId === "_" || r.account === view.accountId),
217
+ );
218
+ return {
219
+ ...view,
220
+ ...(person
221
+ ? {
222
+ withMe: {
223
+ feel: person.feel,
224
+ spokenReplies: history.reduce((n, r) => n + r.replies, 0),
225
+ firstMetAt: person.firstMetAt,
226
+ lastTalkedAt:
227
+ Math.max(
228
+ person.lastTalkedAt || 0,
229
+ ...history.map((r) => r.last),
230
+ ) || null,
231
+ ...(person.impression
232
+ ? { impression: text(person.impression, 120) }
233
+ : {}),
234
+ },
235
+ }
236
+ : {}),
237
+ };
238
+ }),
239
+ requested:
240
+ rows.some((r) => r.mentioned) ||
241
+ (rows.some((r) => r.score) &&
242
+ words.some((w) =>
243
+ /(?:我|你).*(?:是谁|谁|关系|认识|认得)|who|relationship/i.test(w),
244
+ )),
245
+ };
246
+ }
247
+ history(subject, limit = 30) {
248
+ return this.db
249
+ .prepare(
250
+ "SELECT * FROM mind_relationships WHERE subject_id=? ORDER BY created DESC,rowid DESC LIMIT ?",
251
+ )
252
+ .all(String(subject), limit);
253
+ }
254
+ save(input, now = Date.now()) {
255
+ const subject = text(input?.subjectId, 128),
256
+ channel = String(input?.channel || "onebot"),
257
+ account = text(input?.accountId || "_", 128);
258
+ if (
259
+ String(input?.subjectId || "").length > 128 ||
260
+ String(input?.accountId || "").length > 128 ||
261
+ !/^[\w.-]{1,128}$/.test(subject) ||
262
+ ["bot", "self", "_"].includes(subject) ||
263
+ !["onebot", "qqbot"].includes(channel) ||
264
+ !/^[\w.-]{1,128}$/.test(account) ||
265
+ (channel === "onebot" && !/^\d+$/.test(subject))
266
+ )
267
+ throw Error("关系对象账号无效");
268
+ if (channel === "qqbot" && account === "_")
269
+ throw Error("官方机器人关系需要指定应用账号");
270
+ if (
271
+ this.db
272
+ .prepare("SELECT 1 FROM core_events WHERE account_id=? LIMIT 1")
273
+ .get(subject)
274
+ )
275
+ throw Error("不能把当前实例绑定为自己的关系对象");
276
+ const kind = String(input.kind || "human"),
277
+ role = text(input.peerRole, 24),
278
+ name = text(input.name, 40),
279
+ note = text(input.note, 300),
280
+ discretion = input.discretion || "open";
281
+ if (
282
+ !["human", "bot"].includes(kind) ||
283
+ !role ||
284
+ String(input.peerRole).length > 24 ||
285
+ String(input.name || "").length > 40 ||
286
+ String(input.note || "").length > 300 ||
287
+ !["open", "private", "secret"].includes(discretion)
288
+ )
289
+ throw Error("关系设置无效");
290
+ const gender = this.mind.nature.current(now).gender;
291
+ const older =
292
+ gender === "female" ? "姐姐" : gender === "male" ? "哥哥" : "年长手足";
293
+ const younger =
294
+ gender === "female" ? "妹妹" : gender === "male" ? "弟弟" : "年幼手足";
295
+ const opposite = {
296
+ 妹妹: older,
297
+ 弟弟: older,
298
+ 姐姐: younger,
299
+ 哥哥: younger,
300
+ 朋友: "朋友",
301
+ 伙伴: "伙伴",
302
+ };
303
+ const selfRole = text(input.selfRole || opposite[role] || "", 24);
304
+ if (
305
+ !selfRole ||
306
+ String(input.selfRole || "").length > 24 ||
307
+ hasCredential([role, selfRole, name, note].join(" "))
308
+ )
309
+ throw Error("关系称呼或备注无效");
310
+ const room = `${channel}:${account}:private:${subject}`,
311
+ previous = this.current(subject, { now, room });
312
+ if (
313
+ input.expectedId !== undefined &&
314
+ input.expectedId !== (previous?.id || null)
315
+ )
316
+ throw Error("关系已经变化,请刷新后再修改");
317
+ if (
318
+ previous &&
319
+ previous.kind === kind &&
320
+ previous.peer_role === role &&
321
+ previous.self_role === selfRole &&
322
+ previous.name === name &&
323
+ previous.note === note &&
324
+ previous.discretion === discretion
325
+ )
326
+ return this.view(previous);
327
+ this.db.exec("BEGIN IMMEDIATE");
328
+ try {
329
+ this.db
330
+ .prepare(
331
+ "INSERT INTO mind_relationships(id,created,subject_id,channel,account_id,kind,name,peer_role,self_role,discretion,note,status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'active')",
332
+ )
333
+ .run(
334
+ randomUUID(),
335
+ now,
336
+ subject,
337
+ channel,
338
+ account,
339
+ kind,
340
+ name,
341
+ role,
342
+ selfRole,
343
+ discretion,
344
+ note,
345
+ );
346
+ this.db
347
+ .prepare(
348
+ "INSERT INTO mind_people(user_id,name,first_seen,last_seen,sessions) VALUES (?,NULL,NULL,NULL,'[]') ON CONFLICT(user_id) DO NOTHING",
349
+ )
350
+ .run(subject);
351
+ this.db.exec("COMMIT");
352
+ this.mind.repo.store.revision++;
353
+ return this.view(this.current(subject, { now, room }));
354
+ } catch (error) {
355
+ this.db.exec("ROLLBACK");
356
+ throw error;
357
+ }
358
+ }
359
+ end(subject, input = {}, now = Date.now()) {
360
+ if (
361
+ String(input.note || "").length > 300 ||
362
+ hasCredential(String(input.note || ""))
363
+ )
364
+ throw Error("关系称呼或备注无效");
365
+ const room = `${input.channel || "onebot"}:${input.accountId || "_"}:private:${subject}`,
366
+ row = this.current(subject, { now, room });
367
+ if (!row) throw Error("关系不存在或已解除");
368
+ if (input.expectedId !== undefined && input.expectedId !== row.id)
369
+ throw Error("关系已经变化,请刷新后再修改");
370
+ this.db
371
+ .prepare(
372
+ "INSERT INTO mind_relationships(id,created,subject_id,channel,account_id,kind,name,peer_role,self_role,discretion,note,status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'ended')",
373
+ )
374
+ .run(
375
+ randomUUID(),
376
+ now,
377
+ row.subject_id,
378
+ row.channel,
379
+ row.account_id,
380
+ row.kind,
381
+ row.name,
382
+ row.peer_role,
383
+ row.self_role,
384
+ row.discretion,
385
+ text(input.note || "在管理台解除关系", 300),
386
+ );
387
+ this.mind.repo.store.revision++;
388
+ return { ended: true };
389
+ }
390
+ }