luckytri 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/docs/en/CHANGELOG.md +25 -16
- package/docs/en/chat-style.md +28 -9
- package/docs/en/guide.md +5 -5
- package/docs/en/her-life.md +2 -2
- package/docs/en/webui.md +1 -1
- package/docs/zh/CHANGELOG.md +25 -16
- package/docs/zh/chat-style.md +30 -9
- package/docs/zh/guide.md +5 -5
- package/docs/zh/her-life.md +2 -2
- package/docs/zh/webui.md +1 -1
- package/package.json +1 -1
- package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
- package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
- package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
- package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
- package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
- package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
- package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
- package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
- package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
- package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
- package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
- package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
- package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
- package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
- package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
- package/public/app/index.html +5 -6
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/scripts/import-data.js +128 -128
- package/scripts/proxy-config.js +167 -167
- package/scripts/update-instance.js +201 -201
- package/server/channels/adapters.js +27 -27
- package/server/channels/onebot/adapter.js +288 -288
- package/server/channels/qqbot/adapter.js +253 -253
- package/server/channels/qqbot/api.js +199 -199
- package/server/channels/qqbot/credentials.js +11 -11
- package/server/channels/qqbot/gateway.js +274 -274
- package/server/channels/qqbot/index.js +389 -389
- package/server/channels/segments.js +40 -40
- package/server/channels/session-key.js +153 -153
- package/server/channels/state.js +42 -42
- package/server/core/api.js +564 -564
- package/server/core/context-builder.js +288 -288
- package/server/core/context-compactor.js +454 -454
- package/server/core/conversation-cues.js +365 -325
- package/server/core/conversation-grounding.js +178 -0
- package/server/core/conversation-manager.js +87 -87
- package/server/core/dialogue-context.js +52 -0
- package/server/core/feedback.js +8 -8
- package/server/core/group-style.js +72 -72
- package/server/core/initiative-context.js +66 -66
- package/server/core/input-budget.js +32 -32
- package/server/core/local-demo.js +98 -98
- package/server/core/message-manager.js +118 -118
- package/server/core/message-scheduler.js +78 -78
- package/server/core/model-json.js +190 -190
- package/server/core/model-presets.js +900 -900
- package/server/core/name-call.js +29 -29
- package/server/core/network.js +157 -157
- package/server/core/person-name.js +22 -22
- package/server/core/persona-manager.js +324 -269
- package/server/core/reply-delivery.js +472 -404
- package/server/core/reply-target-resolver.js +50 -50
- package/server/core/response-generator.js +58 -50
- package/server/core/response-validator.js +30 -27
- package/server/core/retired-prompts.js +51 -51
- package/server/core/sessions.js +109 -109
- package/server/core/speaker-names.js +92 -92
- package/server/core/topic-tracker.js +65 -65
- package/server/core/turn.js +42 -8
- package/server/http.js +18 -18
- package/server/i18n/decisions.js +209 -209
- package/server/i18n/index.js +144 -144
- package/server/i18n/labels.js +0 -5
- package/server/i18n/models.js +111 -111
- package/server/knowledge/api.js +342 -342
- package/server/knowledge/embedding-profile.js +114 -114
- package/server/knowledge/index.js +13 -13
- package/server/knowledge/retrieval.js +93 -93
- package/server/knowledge/schema.js +158 -158
- package/server/knowledge/vector-index.js +90 -90
- package/server/mind/affect.js +265 -265
- package/server/mind/attention.js +200 -200
- package/server/mind/bonds.js +565 -565
- package/server/mind/clock.js +83 -83
- package/server/mind/context-selection.js +16 -4
- package/server/mind/continuity.js +562 -562
- package/server/mind/conversation-origin.js +175 -175
- package/server/mind/days.js +210 -210
- package/server/mind/evidence-read.js +45 -45
- package/server/mind/faces.js +373 -373
- package/server/mind/guard.js +68 -60
- package/server/mind/initiative.js +338 -338
- package/server/mind/life/activities.js +21 -21
- package/server/mind/life/context.js +330 -330
- package/server/mind/life/growth.js +261 -261
- package/server/mind/life/presence.js +305 -305
- package/server/mind/meetings.js +1069 -1069
- package/server/mind/memory-policy.js +48 -48
- package/server/mind/memory.js +888 -888
- package/server/mind/nature.js +358 -358
- package/server/mind/novelty.js +19 -19
- package/server/mind/periods.js +118 -118
- package/server/mind/reading.js +120 -120
- package/server/mind/relationship-context.js +71 -71
- package/server/mind/relationships.js +390 -390
- package/server/mind/salience.js +201 -201
- package/server/mind/schema.js +567 -567
- package/server/mind/self.js +496 -484
- package/server/mind/thoughts.js +274 -274
- package/server/mind/time/agenda.js +242 -242
- package/server/mind/time/api.js +200 -200
- package/server/mind/time/attention.js +34 -34
- package/server/mind/time/delivery-links.js +130 -130
- package/server/mind/time/games.js +7 -3
- package/server/mind/time/index.js +3 -0
- package/server/mind/time/model-material.js +77 -77
- package/server/mind/time/presentation.js +35 -51
- package/server/mind/time/schedule.js +83 -83
- package/server/mind/time/schema.js +84 -84
- package/server/mind/time/search.js +383 -383
- package/server/mind/time/sharing.js +472 -472
- package/server/mind/time/task-links.js +311 -311
- package/server/mind/time/works.js +438 -432
- package/server/mind/traits.js +191 -191
- package/server/mind/view.js +43 -6
- package/server/runtime/installation.js +116 -116
- package/server/runtime/update-manager.js +140 -140
- package/server/storage/data-package.js +298 -298
- package/server/storage/data-transfer-api.js +50 -50
- package/server/storage/data-transfer-worker.js +18 -18
- package/server/storage/data-transfer.js +416 -416
- package/server/studio/display-names.js +139 -139
- package/server/studio/management.js +102 -102
- package/server/studio/updates.js +144 -144
- package/server/version.js +10 -10
- package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
- package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
- package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
- package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
- package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
- package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
- package/public/app/assets/TimePage-C-guwPk2.css +0 -1
package/server/core/sessions.js
CHANGED
|
@@ -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
|
+
}
|
package/server/core/turn.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
200
|
-
|
|
201
|
-
|
|
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
|
};
|