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.
Files changed (155) hide show
  1. package/LICENSE +21 -21
  2. package/docs/en/CHANGELOG.md +25 -16
  3. package/docs/en/chat-style.md +28 -9
  4. package/docs/en/guide.md +5 -5
  5. package/docs/en/her-life.md +2 -2
  6. package/docs/en/webui.md +1 -1
  7. package/docs/zh/CHANGELOG.md +25 -16
  8. package/docs/zh/chat-style.md +30 -9
  9. package/docs/zh/guide.md +5 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +1 -1
  13. package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
  14. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  15. package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
  16. package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
  17. package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
  18. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  19. package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
  20. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  21. package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
  22. package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
  23. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  24. package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
  25. package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
  26. package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
  27. package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
  28. package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
  29. package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
  30. package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
  31. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  32. package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
  33. package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
  34. package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
  35. package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
  36. package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
  37. package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
  38. package/public/app/index.html +5 -6
  39. package/public/guide.en.html +3 -3
  40. package/public/guide.html +3 -3
  41. package/scripts/import-data.js +128 -128
  42. package/scripts/proxy-config.js +167 -167
  43. package/scripts/update-instance.js +201 -201
  44. package/server/channels/adapters.js +27 -27
  45. package/server/channels/onebot/adapter.js +288 -288
  46. package/server/channels/qqbot/adapter.js +253 -253
  47. package/server/channels/qqbot/api.js +199 -199
  48. package/server/channels/qqbot/credentials.js +11 -11
  49. package/server/channels/qqbot/gateway.js +274 -274
  50. package/server/channels/qqbot/index.js +389 -389
  51. package/server/channels/segments.js +40 -40
  52. package/server/channels/session-key.js +153 -153
  53. package/server/channels/state.js +42 -42
  54. package/server/core/api.js +564 -564
  55. package/server/core/context-builder.js +288 -288
  56. package/server/core/context-compactor.js +454 -454
  57. package/server/core/conversation-cues.js +365 -325
  58. package/server/core/conversation-grounding.js +178 -0
  59. package/server/core/conversation-manager.js +87 -87
  60. package/server/core/dialogue-context.js +52 -0
  61. package/server/core/feedback.js +8 -8
  62. package/server/core/group-style.js +72 -72
  63. package/server/core/initiative-context.js +66 -66
  64. package/server/core/input-budget.js +32 -32
  65. package/server/core/local-demo.js +98 -98
  66. package/server/core/message-manager.js +118 -118
  67. package/server/core/message-scheduler.js +78 -78
  68. package/server/core/model-json.js +190 -190
  69. package/server/core/model-presets.js +900 -900
  70. package/server/core/name-call.js +29 -29
  71. package/server/core/network.js +157 -157
  72. package/server/core/person-name.js +22 -22
  73. package/server/core/persona-manager.js +324 -269
  74. package/server/core/reply-delivery.js +472 -404
  75. package/server/core/reply-target-resolver.js +50 -50
  76. package/server/core/response-generator.js +58 -50
  77. package/server/core/response-validator.js +30 -27
  78. package/server/core/retired-prompts.js +51 -51
  79. package/server/core/sessions.js +109 -109
  80. package/server/core/speaker-names.js +92 -92
  81. package/server/core/topic-tracker.js +65 -65
  82. package/server/core/turn.js +42 -8
  83. package/server/http.js +18 -18
  84. package/server/i18n/decisions.js +209 -209
  85. package/server/i18n/index.js +144 -144
  86. package/server/i18n/labels.js +0 -5
  87. package/server/i18n/models.js +111 -111
  88. package/server/knowledge/api.js +342 -342
  89. package/server/knowledge/embedding-profile.js +114 -114
  90. package/server/knowledge/index.js +13 -13
  91. package/server/knowledge/retrieval.js +93 -93
  92. package/server/knowledge/schema.js +158 -158
  93. package/server/knowledge/vector-index.js +90 -90
  94. package/server/mind/affect.js +265 -265
  95. package/server/mind/attention.js +200 -200
  96. package/server/mind/bonds.js +565 -565
  97. package/server/mind/clock.js +83 -83
  98. package/server/mind/context-selection.js +16 -4
  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 +68 -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 -330
  108. package/server/mind/life/growth.js +261 -261
  109. package/server/mind/life/presence.js +305 -305
  110. package/server/mind/meetings.js +1069 -1069
  111. package/server/mind/memory-policy.js +48 -48
  112. package/server/mind/memory.js +888 -888
  113. package/server/mind/nature.js +358 -358
  114. package/server/mind/novelty.js +19 -19
  115. package/server/mind/periods.js +118 -118
  116. package/server/mind/reading.js +120 -120
  117. package/server/mind/relationship-context.js +71 -71
  118. package/server/mind/relationships.js +390 -390
  119. package/server/mind/salience.js +201 -201
  120. package/server/mind/schema.js +567 -567
  121. package/server/mind/self.js +496 -484
  122. package/server/mind/thoughts.js +274 -274
  123. package/server/mind/time/agenda.js +242 -242
  124. package/server/mind/time/api.js +200 -200
  125. package/server/mind/time/attention.js +34 -34
  126. package/server/mind/time/delivery-links.js +130 -130
  127. package/server/mind/time/games.js +7 -3
  128. package/server/mind/time/index.js +3 -0
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +35 -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 +438 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +43 -6
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-manager.js +140 -140
  141. package/server/storage/data-package.js +298 -298
  142. package/server/storage/data-transfer-api.js +50 -50
  143. package/server/storage/data-transfer-worker.js +18 -18
  144. package/server/storage/data-transfer.js +416 -416
  145. package/server/studio/display-names.js +139 -139
  146. package/server/studio/management.js +102 -102
  147. package/server/studio/updates.js +144 -144
  148. package/server/version.js +10 -10
  149. package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
  150. package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
  151. package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
  152. package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
  153. package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
  154. package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
  155. package/public/app/assets/TimePage-C-guwPk2.css +0 -1
@@ -1,109 +1,109 @@
1
- import { formatSessionKey } from "../channels/session-key.js";
2
-
3
- // The shape of the id a room has on each platform: a QQ number for OneBot, an
4
- // openid for the official bot.
5
- const NATIVE_ID = {
6
- onebot: /^\d{4,20}$/,
7
- qqbot: /^[0-9A-Za-z_-]{8,64}$/,
8
- };
9
-
10
- export function parseNewSession(body, { qqbotAppId = "" } = {}) {
11
- const { id, kind, name } = body || {};
12
- const channel = body?.channel === "qqbot" ? "qqbot" : "onebot";
13
- if (
14
- !["group", "private"].includes(kind) ||
15
- typeof id !== "string" ||
16
- !NATIVE_ID[channel].test(id) ||
17
- typeof name !== "string" ||
18
- !name.trim() ||
19
- name.length > 100
20
- )
21
- throw Error(
22
- channel === "qqbot"
23
- ? "请填写有效的群 openid / 用户 openid 和名称"
24
- : "请填写有效的群号/QQ号和名称",
25
- );
26
- if (channel === "qqbot") {
27
- if (!qqbotAppId) throw Error("请先在「系统 → 连接 QQ」填写 AppID");
28
- return {
29
- sessionId: formatSessionKey({
30
- channel,
31
- accountId: qqbotAppId,
32
- kind,
33
- nativeId: id,
34
- }),
35
- kind,
36
- name: name.trim(),
37
- };
38
- }
39
- return { sessionId: `${kind}:${id}`, kind, name: name.trim() };
40
- }
41
-
42
- export function upsertSession(db, { sessionId, kind, name }) {
43
- db.prepare(
44
- "INSERT INTO sessions(id,name,kind,enabled,archived) VALUES (?,?,?,1,0) ON CONFLICT(id) DO UPDATE SET name=excluded.name,kind=excluded.kind,archived=0",
45
- ).run(sessionId, name, kind);
46
- }
47
-
48
- export function setSessionEnabled(db, id, enabled) {
49
- if (typeof enabled !== "boolean") throw Error("开关无效");
50
- if (!db.prepare("SELECT id FROM sessions WHERE id=?").get(id))
51
- throw Error("会话不存在");
52
- db.prepare("UPDATE sessions SET enabled=? WHERE id=?").run(+enabled, id);
53
- }
54
-
55
- function nativeId(id) {
56
- const parts = String(id || "").split(":");
57
- return parts[parts.length - 1] || "";
58
- }
59
-
60
- export function isPlaceholderName(name, id = "") {
61
- const native = nativeId(id);
62
- const raw = String(name || "").trim();
63
- const stripped = raw.replace(/^(群聊|私聊)\s+/, "");
64
- return (
65
- !raw ||
66
- stripped === native ||
67
- /^\d{4,20}$/.test(stripped) ||
68
- raw === "未命名的群" ||
69
- raw === "未命名的人"
70
- );
71
- }
72
-
73
- // Fill in real group and friend names from QQ. A name the user already set stays.
74
- export function applyDirectoryNames(db, { groups = [], friends = [] } = {}) {
75
- const groupNames = new Map();
76
- for (const group of groups) {
77
- const id = String(group?.group_id ?? "");
78
- const name = String(group?.group_name || "").trim();
79
- if (id && name && !/^\d{4,20}$/.test(name))
80
- groupNames.set(id, name.slice(0, 100));
81
- }
82
- const friendNames = new Map();
83
- for (const friend of friends) {
84
- const id = String(friend?.user_id ?? "");
85
- const name = String(friend?.remark || friend?.nickname || "").trim();
86
- if (id && name && name !== id && !/^\d{4,20}$/.test(name))
87
- friendNames.set(id, name.slice(0, 100));
88
- }
89
- const update = db.prepare("UPDATE sessions SET name=? WHERE id=?");
90
- let changed = 0;
91
- for (const row of db.prepare("SELECT id,name,kind FROM sessions").all()) {
92
- const native = nativeId(row.id);
93
- const privateChat = row.kind === "private" || row.id.includes(":private:");
94
- const next = privateChat ? friendNames.get(native) : groupNames.get(native);
95
- if (!next || next === row.name || !isPlaceholderName(row.name, row.id))
96
- continue;
97
- update.run(next, row.id);
98
- changed += 1;
99
- }
100
- return changed;
101
- }
102
-
103
- export function renameSession(db, id, name) {
104
- if (!db.prepare("SELECT id FROM sessions WHERE id=?").get(id))
105
- throw Error("会话不存在");
106
- if (typeof name !== "string" || !name.trim() || name.length > 100)
107
- throw Error("会话名称无效");
108
- db.prepare("UPDATE sessions SET name=? WHERE id=?").run(name.trim(), id);
109
- }
1
+ import { formatSessionKey } from "../channels/session-key.js";
2
+
3
+ // The shape of the id a room has on each platform: a QQ number for OneBot, an
4
+ // openid for the official bot.
5
+ const NATIVE_ID = {
6
+ onebot: /^\d{4,20}$/,
7
+ qqbot: /^[0-9A-Za-z_-]{8,64}$/,
8
+ };
9
+
10
+ export function parseNewSession(body, { qqbotAppId = "" } = {}) {
11
+ const { id, kind, name } = body || {};
12
+ const channel = body?.channel === "qqbot" ? "qqbot" : "onebot";
13
+ if (
14
+ !["group", "private"].includes(kind) ||
15
+ typeof id !== "string" ||
16
+ !NATIVE_ID[channel].test(id) ||
17
+ typeof name !== "string" ||
18
+ !name.trim() ||
19
+ name.length > 100
20
+ )
21
+ throw Error(
22
+ channel === "qqbot"
23
+ ? "请填写有效的群 openid / 用户 openid 和名称"
24
+ : "请填写有效的群号/QQ号和名称",
25
+ );
26
+ if (channel === "qqbot") {
27
+ if (!qqbotAppId) throw Error("请先在「系统 → 连接 QQ」填写 AppID");
28
+ return {
29
+ sessionId: formatSessionKey({
30
+ channel,
31
+ accountId: qqbotAppId,
32
+ kind,
33
+ nativeId: id,
34
+ }),
35
+ kind,
36
+ name: name.trim(),
37
+ };
38
+ }
39
+ return { sessionId: `${kind}:${id}`, kind, name: name.trim() };
40
+ }
41
+
42
+ export function upsertSession(db, { sessionId, kind, name }) {
43
+ db.prepare(
44
+ "INSERT INTO sessions(id,name,kind,enabled,archived) VALUES (?,?,?,1,0) ON CONFLICT(id) DO UPDATE SET name=excluded.name,kind=excluded.kind,archived=0",
45
+ ).run(sessionId, name, kind);
46
+ }
47
+
48
+ export function setSessionEnabled(db, id, enabled) {
49
+ if (typeof enabled !== "boolean") throw Error("开关无效");
50
+ if (!db.prepare("SELECT id FROM sessions WHERE id=?").get(id))
51
+ throw Error("会话不存在");
52
+ db.prepare("UPDATE sessions SET enabled=? WHERE id=?").run(+enabled, id);
53
+ }
54
+
55
+ function nativeId(id) {
56
+ const parts = String(id || "").split(":");
57
+ return parts[parts.length - 1] || "";
58
+ }
59
+
60
+ export function isPlaceholderName(name, id = "") {
61
+ const native = nativeId(id);
62
+ const raw = String(name || "").trim();
63
+ const stripped = raw.replace(/^(群聊|私聊)\s+/, "");
64
+ return (
65
+ !raw ||
66
+ stripped === native ||
67
+ /^\d{4,20}$/.test(stripped) ||
68
+ raw === "未命名的群" ||
69
+ raw === "未命名的人"
70
+ );
71
+ }
72
+
73
+ // Fill in real group and friend names from QQ. A name the user already set stays.
74
+ export function applyDirectoryNames(db, { groups = [], friends = [] } = {}) {
75
+ const groupNames = new Map();
76
+ for (const group of groups) {
77
+ const id = String(group?.group_id ?? "");
78
+ const name = String(group?.group_name || "").trim();
79
+ if (id && name && !/^\d{4,20}$/.test(name))
80
+ groupNames.set(id, name.slice(0, 100));
81
+ }
82
+ const friendNames = new Map();
83
+ for (const friend of friends) {
84
+ const id = String(friend?.user_id ?? "");
85
+ const name = String(friend?.remark || friend?.nickname || "").trim();
86
+ if (id && name && name !== id && !/^\d{4,20}$/.test(name))
87
+ friendNames.set(id, name.slice(0, 100));
88
+ }
89
+ const update = db.prepare("UPDATE sessions SET name=? WHERE id=?");
90
+ let changed = 0;
91
+ for (const row of db.prepare("SELECT id,name,kind FROM sessions").all()) {
92
+ const native = nativeId(row.id);
93
+ const privateChat = row.kind === "private" || row.id.includes(":private:");
94
+ const next = privateChat ? friendNames.get(native) : groupNames.get(native);
95
+ if (!next || next === row.name || !isPlaceholderName(row.name, row.id))
96
+ continue;
97
+ update.run(next, row.id);
98
+ changed += 1;
99
+ }
100
+ return changed;
101
+ }
102
+
103
+ export function renameSession(db, id, name) {
104
+ if (!db.prepare("SELECT id FROM sessions WHERE id=?").get(id))
105
+ throw Error("会话不存在");
106
+ if (typeof name !== "string" || !name.trim() || name.length > 100)
107
+ throw Error("会话名称无效");
108
+ db.prepare("UPDATE sessions SET name=? WHERE id=?").run(name.trim(), id);
109
+ }
@@ -1,92 +1,92 @@
1
- const namesByDatabase = new WeakMap();
2
-
3
- export function readableName(name, userId) {
4
- const label = String(name || "").trim();
5
- const id = String(userId || "").trim();
6
- if (!label || label === id || label === "bot") return "";
7
- if (/^\d{4,20}$/.test(label)) return "";
8
- return label.slice(0, 100);
9
- }
10
-
11
- function sessionNames(db, session) {
12
- let cache = namesByDatabase.get(db);
13
- if (!cache) {
14
- cache = new Map();
15
- namesByDatabase.set(db, cache);
16
- }
17
-
18
- const watermark =
19
- db
20
- .prepare("SELECT MAX(seq) n FROM core_events WHERE session_id=?")
21
- .get(session).n || 0;
22
- const previous = cache.get(session);
23
- if (previous?.watermark === watermark) return previous;
24
-
25
- // New events only add or update a speaker's name. Rebuild after a clear or
26
- // when the sequence watermark moves backwards.
27
- const incremental = previous && watermark > previous.watermark;
28
- const entries = incremental ? new Map(previous.entries) : new Map();
29
- const after = incremental ? previous.watermark : 0;
30
- const rows = db
31
- .prepare(
32
- `SELECT seq, json_extract(payload,'$.userId') AS user_id,
33
- json_extract(payload,'$.name') AS name
34
- FROM core_events WHERE session_id=? AND seq>? AND seq NOT IN (SELECT seq FROM mind_unlived) ORDER BY seq`,
35
- )
36
- .all(session, after);
37
- for (const row of rows) {
38
- const label = readableName(row.name, row.user_id);
39
- if (label)
40
- entries.set(String(row.user_id), { label, seq: Number(row.seq) });
41
- }
42
- const next = { watermark, entries };
43
- cache.set(session, next);
44
- return next;
45
- }
46
-
47
- export function invalidateSpeakerNames(db, session) {
48
- namesByDatabase.get(db)?.delete(String(session));
49
- }
50
-
51
- export function speakerNames(db, sessionIds) {
52
- const ids = [...new Set((sessionIds || []).map(String).filter(Boolean))];
53
- const names = new Map();
54
- if (!ids.length) return names;
55
- const latest = new Map();
56
- for (const id of ids) {
57
- for (const [userId, entry] of sessionNames(db, id).entries) {
58
- const existing = latest.get(userId);
59
- if (!existing || entry.seq > existing.seq) latest.set(userId, entry);
60
- }
61
- }
62
- for (const [userId, entry] of latest) names.set(userId, entry.label);
63
- return names;
64
- }
65
-
66
- export function applySpeakerNames(text, names) {
67
- let out = String(text || "");
68
- // A QQ number inside a longer run of digits is not that person; an openid
69
- // (the official bot's ids) is only that person when it stands alone.
70
- const entries = [...names.entries()]
71
- .filter(
72
- ([id, name]) =>
73
- /^(?:\d{4,20}|[0-9A-Za-z_-]{16,64})$/.test(id) && name && name !== id,
74
- )
75
- .sort((a, b) => b[0].length - a[0].length);
76
- for (const [id, name] of entries) {
77
- const numeric = /^\d+$/.test(id);
78
- const before = numeric ? "(?<!\\d)" : "(?<![0-9A-Za-z_-])";
79
- const after = numeric ? "(?!\\d)" : "(?![0-9A-Za-z_-])";
80
- out = out.replace(new RegExp(`${before}${id}${after}`, "g"), name);
81
- }
82
- return out;
83
- }
84
-
85
- export function presentMemory(row, names) {
86
- const subject = String(row.subject || "");
87
- return {
88
- ...row,
89
- content: applySpeakerNames(row.content, names),
90
- subjectName: names.get(subject) || subject,
91
- };
92
- }
1
+ const namesByDatabase = new WeakMap();
2
+
3
+ export function readableName(name, userId) {
4
+ const label = String(name || "").trim();
5
+ const id = String(userId || "").trim();
6
+ if (!label || label === id || label === "bot") return "";
7
+ if (/^\d{4,20}$/.test(label)) return "";
8
+ return label.slice(0, 100);
9
+ }
10
+
11
+ function sessionNames(db, session) {
12
+ let cache = namesByDatabase.get(db);
13
+ if (!cache) {
14
+ cache = new Map();
15
+ namesByDatabase.set(db, cache);
16
+ }
17
+
18
+ const watermark =
19
+ db
20
+ .prepare("SELECT MAX(seq) n FROM core_events WHERE session_id=?")
21
+ .get(session).n || 0;
22
+ const previous = cache.get(session);
23
+ if (previous?.watermark === watermark) return previous;
24
+
25
+ // New events only add or update a speaker's name. Rebuild after a clear or
26
+ // when the sequence watermark moves backwards.
27
+ const incremental = previous && watermark > previous.watermark;
28
+ const entries = incremental ? new Map(previous.entries) : new Map();
29
+ const after = incremental ? previous.watermark : 0;
30
+ const rows = db
31
+ .prepare(
32
+ `SELECT seq, json_extract(payload,'$.userId') AS user_id,
33
+ json_extract(payload,'$.name') AS name
34
+ FROM core_events WHERE session_id=? AND seq>? AND seq NOT IN (SELECT seq FROM mind_unlived) ORDER BY seq`,
35
+ )
36
+ .all(session, after);
37
+ for (const row of rows) {
38
+ const label = readableName(row.name, row.user_id);
39
+ if (label)
40
+ entries.set(String(row.user_id), { label, seq: Number(row.seq) });
41
+ }
42
+ const next = { watermark, entries };
43
+ cache.set(session, next);
44
+ return next;
45
+ }
46
+
47
+ export function invalidateSpeakerNames(db, session) {
48
+ namesByDatabase.get(db)?.delete(String(session));
49
+ }
50
+
51
+ export function speakerNames(db, sessionIds) {
52
+ const ids = [...new Set((sessionIds || []).map(String).filter(Boolean))];
53
+ const names = new Map();
54
+ if (!ids.length) return names;
55
+ const latest = new Map();
56
+ for (const id of ids) {
57
+ for (const [userId, entry] of sessionNames(db, id).entries) {
58
+ const existing = latest.get(userId);
59
+ if (!existing || entry.seq > existing.seq) latest.set(userId, entry);
60
+ }
61
+ }
62
+ for (const [userId, entry] of latest) names.set(userId, entry.label);
63
+ return names;
64
+ }
65
+
66
+ export function applySpeakerNames(text, names) {
67
+ let out = String(text || "");
68
+ // A QQ number inside a longer run of digits is not that person; an openid
69
+ // (the official bot's ids) is only that person when it stands alone.
70
+ const entries = [...names.entries()]
71
+ .filter(
72
+ ([id, name]) =>
73
+ /^(?:\d{4,20}|[0-9A-Za-z_-]{16,64})$/.test(id) && name && name !== id,
74
+ )
75
+ .sort((a, b) => b[0].length - a[0].length);
76
+ for (const [id, name] of entries) {
77
+ const numeric = /^\d+$/.test(id);
78
+ const before = numeric ? "(?<!\\d)" : "(?<![0-9A-Za-z_-])";
79
+ const after = numeric ? "(?!\\d)" : "(?![0-9A-Za-z_-])";
80
+ out = out.replace(new RegExp(`${before}${id}${after}`, "g"), name);
81
+ }
82
+ return out;
83
+ }
84
+
85
+ export function presentMemory(row, names) {
86
+ const subject = String(row.subject || "");
87
+ return {
88
+ ...row,
89
+ content: applySpeakerNames(row.content, names),
90
+ subjectName: names.get(subject) || subject,
91
+ };
92
+ }
@@ -1,65 +1,65 @@
1
- import { topicWeight, elapsedLabel } from "../mind/clock.js";
2
- import { messageSeqs, parse } from "../mind/util.js";
3
-
4
- // Topics cool down with time; ones her journal still holds open stay warm.
5
- export class TopicTracker {
6
- constructor(repo) {
7
- this.repo = repo;
8
- repo.db.exec(
9
- "CREATE TABLE IF NOT EXISTS core_topics (session_id TEXT, trace_id TEXT, watermark INTEGER, time INTEGER, topic TEXT, evidence TEXT, PRIMARY KEY(session_id,trace_id))",
10
- );
11
- }
12
- recent(session, watermark, now = Date.now()) {
13
- const rows = this.repo.db
14
- .prepare(
15
- "SELECT topic,evidence,watermark,time FROM core_topics WHERE session_id=? AND watermark<? AND time<=? ORDER BY watermark DESC LIMIT 60",
16
- )
17
- .all(session, watermark, now);
18
- const thoughts = this.repo.db
19
- .prepare(
20
- "SELECT sources,importance,status,sessions FROM mind_thoughts WHERE hidden=0 AND created<=? ORDER BY created DESC LIMIT 200",
21
- )
22
- .all(now)
23
- .filter((t) => parse(t.sessions, []).includes(session))
24
- .map((t) => ({ ...t, seqs: messageSeqs(parse(t.sources, [])) }));
25
- const grouped = new Map();
26
- for (const row of rows) {
27
- if (grouped.has(row.topic)) {
28
- grouped.get(row.topic).mentions++;
29
- continue;
30
- }
31
- const evidence = JSON.parse(row.evidence || "[]");
32
- const related = thoughts.filter((t) =>
33
- t.seqs.some((seq) => evidence.includes(seq)),
34
- );
35
- grouped.set(row.topic, {
36
- ...row,
37
- evidence,
38
- mentions: 1,
39
- open: related.some((t) => t.status === "open"),
40
- importance: Math.max(0, ...related.map((t) => t.importance || 0)),
41
- });
42
- }
43
- return [...grouped.values()]
44
- .map((t) => ({
45
- ...t,
46
- weight: topicWeight(t.time, now, t),
47
- distance: elapsedLabel(t.time, now),
48
- }))
49
- .sort((a, b) => b.weight - a.weight)
50
- .slice(0, 6);
51
- }
52
- record(session, trace, watermark, decision) {
53
- if (typeof decision.topic !== "string" || !decision.topic) return;
54
- this.repo.db
55
- .prepare("INSERT OR IGNORE INTO core_topics VALUES (?,?,?,?,?,?)")
56
- .run(
57
- session,
58
- trace,
59
- watermark,
60
- Date.now(),
61
- decision.topic,
62
- JSON.stringify(decision.targetMessageIds || decision.evidenceIds || []),
63
- );
64
- }
65
- }
1
+ import { topicWeight, elapsedLabel } from "../mind/clock.js";
2
+ import { messageSeqs, parse } from "../mind/util.js";
3
+
4
+ // Topics cool down with time; ones her journal still holds open stay warm.
5
+ export class TopicTracker {
6
+ constructor(repo) {
7
+ this.repo = repo;
8
+ repo.db.exec(
9
+ "CREATE TABLE IF NOT EXISTS core_topics (session_id TEXT, trace_id TEXT, watermark INTEGER, time INTEGER, topic TEXT, evidence TEXT, PRIMARY KEY(session_id,trace_id))",
10
+ );
11
+ }
12
+ recent(session, watermark, now = Date.now()) {
13
+ const rows = this.repo.db
14
+ .prepare(
15
+ "SELECT topic,evidence,watermark,time FROM core_topics WHERE session_id=? AND watermark<? AND time<=? ORDER BY watermark DESC LIMIT 60",
16
+ )
17
+ .all(session, watermark, now);
18
+ const thoughts = this.repo.db
19
+ .prepare(
20
+ "SELECT sources,importance,status,sessions FROM mind_thoughts WHERE hidden=0 AND created<=? ORDER BY created DESC LIMIT 200",
21
+ )
22
+ .all(now)
23
+ .filter((t) => parse(t.sessions, []).includes(session))
24
+ .map((t) => ({ ...t, seqs: messageSeqs(parse(t.sources, [])) }));
25
+ const grouped = new Map();
26
+ for (const row of rows) {
27
+ if (grouped.has(row.topic)) {
28
+ grouped.get(row.topic).mentions++;
29
+ continue;
30
+ }
31
+ const evidence = JSON.parse(row.evidence || "[]");
32
+ const related = thoughts.filter((t) =>
33
+ t.seqs.some((seq) => evidence.includes(seq)),
34
+ );
35
+ grouped.set(row.topic, {
36
+ ...row,
37
+ evidence,
38
+ mentions: 1,
39
+ open: related.some((t) => t.status === "open"),
40
+ importance: Math.max(0, ...related.map((t) => t.importance || 0)),
41
+ });
42
+ }
43
+ return [...grouped.values()]
44
+ .map((t) => ({
45
+ ...t,
46
+ weight: topicWeight(t.time, now, t),
47
+ distance: elapsedLabel(t.time, now),
48
+ }))
49
+ .sort((a, b) => b.weight - a.weight)
50
+ .slice(0, 6);
51
+ }
52
+ record(session, trace, watermark, decision) {
53
+ if (typeof decision.topic !== "string" || !decision.topic) return;
54
+ this.repo.db
55
+ .prepare("INSERT OR IGNORE INTO core_topics VALUES (?,?,?,?,?,?)")
56
+ .run(
57
+ session,
58
+ trace,
59
+ watermark,
60
+ Date.now(),
61
+ decision.topic,
62
+ JSON.stringify(decision.targetMessageIds || decision.evidenceIds || []),
63
+ );
64
+ }
65
+ }
@@ -1,6 +1,8 @@
1
1
  import { replyFocus } from "./conversation-cues.js";
2
2
  import { INITIATIVE_PROMPT } from "../mind/initiative.js";
3
3
  import { initiativeContext } from "./initiative-context.js";
4
+ import { conversationGrounding } from "./conversation-grounding.js";
5
+ import { currentExchange, explicitStop } from "./dialogue-context.js";
4
6
 
5
7
  export const CHOICES = ["speak", "react", "decline", "silent"];
6
8
  const LEGACY = {
@@ -100,7 +102,7 @@ export async function takeTurn(
100
102
  {
101
103
  context: initiating
102
104
  ? initiativeContext(snapshot, extra.occasion)
103
- : context,
105
+ : { ...context, grounding: conversationGrounding(snapshot) },
104
106
  ...(extra.occasion
105
107
  ? {
106
108
  occasion: initiating
@@ -115,6 +117,7 @@ export async function takeTurn(
115
117
  ...(imageGuide ? { imageGuide } : {}),
116
118
  ...(extra.pressure ? { pressure: extra.pressure } : {}),
117
119
  ...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
120
+ ...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
118
121
  },
119
122
  trace,
120
123
  images,
@@ -174,6 +177,19 @@ export function normalizeTurn(raw, snapshot, trace) {
174
177
  bubbles = [];
175
178
  trace?.steps?.push("有人可能处在危机里,底线要求认真回应");
176
179
  }
180
+ const botLoop =
181
+ !crisis && !snapshot.initiative && conversationGrounding(snapshot).botLoop;
182
+ if (botLoop) {
183
+ choice = "silent";
184
+ bubbles = [];
185
+ trace?.steps?.push("机器人连续确认没有新内容,这段对话在此收住");
186
+ }
187
+ const stopped = !crisis && explicitStop(snapshot);
188
+ if (stopped) {
189
+ choice = "silent";
190
+ bubbles = [];
191
+ trace?.steps?.push("对方明确要求停止,这轮不再补建议或宣告退场");
192
+ }
177
193
  const text = (value, max) =>
178
194
  String(value ?? "")
179
195
  .trim()
@@ -181,7 +197,10 @@ export function normalizeTurn(raw, snapshot, trace) {
181
197
  return {
182
198
  choice,
183
199
  share:
184
- raw.share && ["send", "later", "decline"].includes(raw.share.choice)
200
+ !botLoop &&
201
+ !stopped &&
202
+ raw.share &&
203
+ ["send", "later", "decline"].includes(raw.share.choice)
185
204
  ? {
186
205
  workId: text(raw.share.workId, 100),
187
206
  choice: raw.share.choice,
@@ -189,6 +208,7 @@ export function normalizeTurn(raw, snapshot, trace) {
189
208
  }
190
209
  : null,
191
210
  attention:
211
+ !botLoop &&
192
212
  raw.attention &&
193
213
  ["continue", "chat", "rest"].includes(raw.attention.action)
194
214
  ? {
@@ -196,9 +216,23 @@ export function normalizeTurn(raw, snapshot, trace) {
196
216
  reason: text(raw.attention.reason, 120),
197
217
  }
198
218
  : { action: "continue" },
199
- appraisal: text(raw.appraisal, 200),
200
- reason: text(raw.reason || raw.appraisal, 300) || "此刻的判断",
201
- topic: text(raw.topic, 40),
219
+ appraisal: botLoop
220
+ ? "对方在确认先前的话,没有新的内容。"
221
+ : text(raw.appraisal, 200),
222
+ reason: botLoop
223
+ ? "这段交流已经结束,先收住。"
224
+ : text(raw.reason || raw.appraisal, 300) || "此刻的判断",
225
+ topic: botLoop ? "交流结束" : text(raw.topic, 40),
226
+ ...(raw.understanding && typeof raw.understanding === "object"
227
+ ? {
228
+ understanding: {
229
+ messageIds: ids(raw.understanding.messageIds).filter((id) =>
230
+ batchIds.has(id),
231
+ ),
232
+ point: text(raw.understanding.point, 180),
233
+ },
234
+ }
235
+ : {}),
202
236
  targetMessageIds: targets,
203
237
  targetUserIds: [
204
238
  ...new Set(
@@ -208,15 +242,15 @@ export function normalizeTurn(raw, snapshot, trace) {
208
242
  ),
209
243
  ],
210
244
  evidenceIds: ids(raw.evidenceIds).filter((id) => known.has(id)),
211
- feelings: (Array.isArray(raw.feelings) ? raw.feelings : [])
245
+ feelings: (Array.isArray(raw.feelings) && !botLoop ? raw.feelings : [])
212
246
  .filter((f) => f && typeof f.feeling === "string")
213
247
  .slice(0, 3),
214
- bonds: (Array.isArray(raw.bonds) ? raw.bonds : [])
248
+ bonds: (Array.isArray(raw.bonds) && !botLoop ? raw.bonds : [])
215
249
  .filter((b) => b && b.userId != null && typeof b.change === "string")
216
250
  .map((b) => ({ ...b, evidence: ids(b.evidence) }))
217
251
  .slice(0, 4),
218
252
  crisis: crisis ? { clear: true, messageIds: crisisIds } : { clear: false },
219
- act: normalizeAct(raw.act, trace),
253
+ act: botLoop || stopped ? null : normalizeAct(raw.act, trace),
220
254
  bubbles: bubbles.slice(0, maxBubbles(choice)),
221
255
  maxBubbles: maxBubbles(choice),
222
256
  };