@modusensus/dsh-mneme 0.11.0 → 0.13.0
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/README.md +54 -21
- package/docs/CONFIGURATION.md +45 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +744 -26
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3752 -819
- package/lib/config.js +171 -2
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +131 -5
- package/lib/inject.js +192 -19
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +115 -4
- package/lib/peak-hours.js +57 -2
- package/lib/profile-facts.js +356 -0
- package/lib/semantic.js +347 -60
- package/lib/service.js +302 -70
- package/lib/settings.js +117 -14
- package/lib/store.js +830 -55
- package/lib/summarize.js +38 -11
- package/lib/tools.js +173 -25
- package/lib/user-settings.js +175 -23
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +264 -10
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +4 -1
- package/src/api.js +744 -26
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +171 -2
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +131 -5
- package/src/inject.js +192 -19
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +115 -4
- package/src/peak-hours.js +57 -2
- package/src/profile-facts.js +356 -0
- package/src/semantic.js +347 -60
- package/src/service.js +302 -70
- package/src/settings.js +117 -14
- package/src/store.js +830 -55
- package/src/summarize.js +38 -11
- package/src/tools.js +173 -25
- package/src/user-settings.js +175 -23
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +264 -10
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +377 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +908 -138
- package/test/config.test.js +79 -0
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/document.test.js +32 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +38 -7
- package/test/profile-facts.test.js +492 -0
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/session-gate.test.js +1 -1
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +476 -1
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +91 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-tools.test.js +131 -76
package/src/user-settings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// dsh-mneme/src/user-settings.js
|
|
2
|
-
// 「用户设置」注入块(人格 / 画像 / 规则)的唯一渲染处。
|
|
2
|
+
// 「用户设置」注入块(人格 / 画像 / 自动画像事实 / 规则)的唯一渲染处。
|
|
3
3
|
//
|
|
4
4
|
// 为什么要单独成模块:面板「Agent 眼中的画像」那张卡原本自己在前端拼了一份 profile +
|
|
5
5
|
// rules 的列表,注释写着「注入生效文本」——但它没有表头、没有「- 用户画像:」前缀,
|
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
// 「看着像」。现在注入器与预览路由调同一个函数,前端只显示后端给的文本。
|
|
8
8
|
//
|
|
9
9
|
// 顺序是设计口径(人格设计稿⑥):特化指令(人格)先出,画像与规则后出、后写覆盖占
|
|
10
|
-
// 上风,rules
|
|
10
|
+
// 上风,rules 恒最硬。改这个顺序等于改提示词权重,别顺手调。自动画像事实(2026-11-06,
|
|
11
|
+
// src/profile-facts.js)插在画像之后、规则之前——它是「画像的 agent 侧补充」,与
|
|
12
|
+
// 手填画像物理分离(模型对 user_settings.profile 永远只有读权)。
|
|
11
13
|
|
|
12
14
|
import { STR, langOf } from "./lang.js";
|
|
13
15
|
|
|
16
|
+
// 事实段的注入预算(本块此前无任何截断机制——它是「身份文本」,从不轮换)。
|
|
17
|
+
// 不设上限的话,一次失控的维护轮能把每帧 systemPrompt 撑爆;行数与总字符双帽,
|
|
18
|
+
// 截断不静默(尾注如实说还剩几条,与 #164① 同一纪律)。
|
|
19
|
+
const PROFILE_FACTS_MAX_LINES = 40;
|
|
20
|
+
const PROFILE_FACTS_MAX_CHARS = 1200;
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* 提示词变量转义:宿主模板插值对 `{{`/`}}` 会按变量名解析并在缺失时抛错,整轮崩掉。
|
|
16
24
|
* 所以把 2 个以上连续花括号逐字符插 `\`,让任何 `{{`/`}}` 子串都不残留;转换幂等,
|
|
@@ -28,6 +36,14 @@ export function escapePromptVars(config, text) {
|
|
|
28
36
|
* 按会话身份取人格卡。返回 `{ card, source }`,`source` 是给面板解释「用的哪张卡」的
|
|
29
37
|
* 读数,不参与判定。
|
|
30
38
|
*
|
|
39
|
+
* 寻址维度(#606 起两维,子 agent 继承再加一维共三维):`sessionId`(绑会话卡,
|
|
40
|
+
* 最具体)优先于 `parentSessionId`(子 agent 继承父会话卡),最后是 `agentId`(按预设建卡)。
|
|
41
|
+
* 理由是具体性——会话比预设更具体,而 DS Bot 这类宿主里 `agentPreset` 恒为
|
|
42
|
+
* `"standard"`(`ds-bot/src/host/roster.js:72-80` 强制写入,实测 50 个会话里 43 个是
|
|
43
|
+
* 它),若让 byAgent 优先,则谁给 `standard` 建一张卡就会盖掉所有按会话建的卡。
|
|
44
|
+
* 父会话卡排在 byAgent 之前:子 agent 自己没卡时,它属于哪段对话比它属于哪个预设
|
|
45
|
+
* 更能说明「该用谁的人格」。
|
|
46
|
+
*
|
|
31
47
|
* `strict`(config.personaStrictAddressing,默认关)决定「有身份、没这张身份的卡」
|
|
32
48
|
* 与「这张卡被显式关掉」这两种情况:
|
|
33
49
|
* - 关(现状):落 default 卡。default 于是变成「所有没建卡的 agent 都能拿到它」,
|
|
@@ -35,44 +51,180 @@ export function escapePromptVars(config, text) {
|
|
|
35
51
|
* - 开:不注入人格(`source: "none"`)。default 退回它的本意:只兜「这一帧解析不到
|
|
36
52
|
* 身份」的会话(无名会话本来就是 default 卡的受众)。
|
|
37
53
|
* 默认关是有意的:这是一次口径变更,不能让老用户的注入文本在升级后静默变化。
|
|
54
|
+
*
|
|
55
|
+
* 严格模式有一格例外(#587):两维**都没建过卡**时仍回落 default。两种处境必须
|
|
56
|
+
* 分开——「建了卡、只是没这一张的卡」是真该按 none 处理(否则关掉一张卡等于没关),
|
|
57
|
+
* 而「一张卡都没建」时 default 是用户手里唯一的一张卡,按 none 处理等于把它永久
|
|
58
|
+
* 废掉,且面板不报错。判据看两维合起来的键数(#606 起):只建了 bySession 卡时,
|
|
59
|
+
* 一个按 byAgent 寻址的会话同样不该被 default 兜底——用户已经做过分区了。
|
|
38
60
|
*/
|
|
39
|
-
export function resolvePersonaCard(
|
|
61
|
+
export function resolvePersonaCard(
|
|
62
|
+
settings,
|
|
63
|
+
agentId,
|
|
64
|
+
{ strict = false, sessionId = null, parentSessionId = null } = {}
|
|
65
|
+
) {
|
|
40
66
|
const store = settings.getPersona();
|
|
41
67
|
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
|
|
68
|
+
const sid = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
|
|
69
|
+
const pid =
|
|
70
|
+
typeof parentSessionId === "string" && parentSessionId.trim() ? parentSessionId.trim() : "";
|
|
71
|
+
// 第一维:绑会话卡(最具体)
|
|
72
|
+
const sessionCard = sid ? store.bySession?.[sid] : null;
|
|
73
|
+
if (sessionCard && sessionCard.enabled === true) return { card: sessionCard, source: "bySession" };
|
|
74
|
+
// 第二维(子 agent 继承):自己没卡就取父会话的卡。
|
|
75
|
+
// 为什么必须有这一维:子 agent 是新会话,用户不可能给每个派生会话都建一张卡;而
|
|
76
|
+
// strict 一旦生效,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从「自称父
|
|
77
|
+
// 会话的人格」变成「不知道自己是谁」——比串味更糟。继承父会话卡才是用户能预期的
|
|
78
|
+
// 语义:同一个 agent 派生出去的手脚,仍是同一个 agent。
|
|
79
|
+
// 注意与 `src/session-gate.js` 的口径区别:那里**刻意不**用 parentSession 判「是不是
|
|
80
|
+
// 子代理」(宿主 fork 出来的主会话同样带 parentSession,用它判会把主会话误关)。
|
|
81
|
+
// 这里要问的不是「你是不是子代理」而是「你自己有没有卡」,所以 fork 出的分支继承
|
|
82
|
+
// 父卡也是正确的(它就是同一段对话的副本)。父 id 与自身 id 相同(脏数据)时跳过,
|
|
83
|
+
// 免得一次寻址查两遍同一个键。
|
|
84
|
+
const parentCard = pid && pid !== sid ? store.bySession?.[pid] : null;
|
|
85
|
+
if (parentCard && parentCard.enabled === true) return { card: parentCard, source: "parentSession" };
|
|
86
|
+
// 第三维:按预设/身份建的卡
|
|
42
87
|
const card = id ? store.byAgent[id] : null;
|
|
43
88
|
if (card && card.enabled === true) return { card, source: "byAgent" };
|
|
44
|
-
|
|
89
|
+
// 严格模式只在「确实建过卡」时才拒绝兜底:两维都空时兜底是唯一的出路。
|
|
90
|
+
const anyCard = Object.keys(store.byAgent ?? {}).length > 0 || Object.keys(store.bySession ?? {}).length > 0;
|
|
91
|
+
// 身份判定要把父会话 id 算进来:子 agent 只有父会话 id、没有自己的卡时,也是
|
|
92
|
+
// 「确实有身份」,不许被 default 兜底。
|
|
93
|
+
if ((id || sid || pid) && strict && anyCard) return { card: {}, source: "none" };
|
|
45
94
|
return { card: store.default, source: "default" };
|
|
46
95
|
}
|
|
47
96
|
|
|
48
97
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
98
|
+
* 按会话身份取自动事实卡(resolvePersonaCard 的镜像,形状不同:facts 没有
|
|
99
|
+
* enabled 位,「卡存在」就是非空数组)。strict 语义与人格一致:有身份但没这张
|
|
100
|
+
* 身份的卡时不落 default——两个 bot 对用户的认知不互串是 store 层分区(输入行)
|
|
101
|
+
* 与本层寻址(输出注入)共同守住的边界。与人格同一条例外(#587):`byAgent` 整体
|
|
102
|
+
* 为空时仍落 default,「一张都没建」不等于「这一张没建」。
|
|
103
|
+
*/
|
|
104
|
+
export function resolveFactsCard(factsStore, agentId, { strict = false } = {}) {
|
|
105
|
+
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
|
|
106
|
+
const byAgent = factsStore?.byAgent ?? {};
|
|
107
|
+
const card = id ? (byAgent[id] ?? null) : null;
|
|
108
|
+
if (card && card.length) return { facts: card, source: "byAgent" };
|
|
109
|
+
if (id && strict && Object.keys(byAgent).length > 0) return { facts: [], source: "none" };
|
|
110
|
+
return { facts: factsStore?.default ?? [], source: "default" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 渲染整块「用户设置」。`agentId` 是会话身份(解析不到传 null → 走 default 兜底),
|
|
115
|
+
* `sessionId` 是绑会话卡的键(#606;解析不到传 null)。
|
|
116
|
+
* `factsProvider` 是 profile-facts 域的读口(返回 `{ default: [...], byAgent: {...} }`),
|
|
117
|
+
* 可选;**开关与 provider 双闸**——profileFactsEnabled 关着时连 provider 都不调,
|
|
118
|
+
* 「关」必须是零读数而不是「读了不渲染」。返回 `{ text, source, agent, session, configGate }`:
|
|
119
|
+
* `text` 为最终注入文本(全空时 ""),`configGate` 是 personaEnabled 的现值——面板
|
|
120
|
+
* 要靠它解释「卡开着为什么没生效」。
|
|
52
121
|
*/
|
|
53
|
-
export function renderUserSettingsBlock({
|
|
122
|
+
export function renderUserSettingsBlock({
|
|
123
|
+
settings,
|
|
124
|
+
config,
|
|
125
|
+
agentId = null,
|
|
126
|
+
sessionId = null,
|
|
127
|
+
parentSessionId = null,
|
|
128
|
+
factsProvider = null
|
|
129
|
+
}) {
|
|
54
130
|
const language = langOf(config);
|
|
55
131
|
const profile = settings.getProfile().trim();
|
|
56
132
|
const rules = settings.getRules();
|
|
57
133
|
const enabled = config?.personaEnabled === true;
|
|
134
|
+
const strict = config?.personaStrictAddressing === true;
|
|
58
135
|
const resolved = enabled
|
|
59
|
-
? resolvePersonaCard(settings, agentId, { strict
|
|
136
|
+
? resolvePersonaCard(settings, agentId, { strict, sessionId, parentSessionId })
|
|
60
137
|
: { card: {}, source: "default" };
|
|
61
138
|
const persona = resolved.card;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
139
|
+
// 段(#600):每一行都带上它的 kind 与写回所需的定位信息,供面板按段读、按段改。
|
|
140
|
+
// 段文本是**未转义**的原样行;整串 `text` 仍走原来的「先 join 再统一转义」,
|
|
141
|
+
// 所以注入字节不变(转义只动连续花括号,换行不参与,逐段转义与整串转义等价)。
|
|
142
|
+
//
|
|
143
|
+
// `text` 是**带模板前缀的整行**(如 `- 用户画像:xxx`),不能拿去当新值回写,
|
|
144
|
+
// 否则每写一次叠一层前缀。可回写的段另带 `value`=原始值(无前缀、未转义);
|
|
145
|
+
// 没有原始值的段(表头、事实表头、截断尾注)不带 `value` 键。
|
|
146
|
+
const personaLines = [];
|
|
147
|
+
if (persona?.enabled === true) {
|
|
148
|
+
if (typeof persona.name === "string" && persona.name.trim()) {
|
|
149
|
+
const value = persona.name.trim();
|
|
150
|
+
personaLines.push({ kind: "personaName", field: "name", value, text: STR.personaNameLine[language](value) });
|
|
151
|
+
}
|
|
152
|
+
if (typeof persona.personality === "string" && persona.personality.trim()) {
|
|
153
|
+
const value = persona.personality.trim();
|
|
154
|
+
personaLines.push({ kind: "personaPersonality", field: "personality", value, text: STR.personaPersonalityLine[language](value) });
|
|
155
|
+
}
|
|
156
|
+
if (typeof persona.replyStyle === "string" && persona.replyStyle.trim()) {
|
|
157
|
+
const value = persona.replyStyle.trim();
|
|
158
|
+
personaLines.push({ kind: "personaReplyStyle", field: "replyStyle", value, text: STR.personaReplyStyleLine[language](value) });
|
|
159
|
+
}
|
|
160
|
+
// scenario / mesExample(#606):只有设置了才输出,未设置时本块逐字节不变。
|
|
161
|
+
if (typeof persona.scenario === "string" && persona.scenario.trim()) {
|
|
162
|
+
const value = persona.scenario.trim();
|
|
163
|
+
personaLines.push({ kind: "personaScenario", field: "scenario", value, text: STR.personaScenarioLine[language](value) });
|
|
164
|
+
}
|
|
165
|
+
if (typeof persona.mesExample === "string" && persona.mesExample.trim()) {
|
|
166
|
+
const value = persona.mesExample.trim();
|
|
167
|
+
personaLines.push({ kind: "personaMesExample", field: "mesExample", value, text: STR.personaMesExampleLine[language](value) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// 事实段:开关关 / 无 provider / 该卡没事实三种情况都不渲染,注入文本与
|
|
171
|
+
// 未启用事实时逐字节一致(老用户零变化)。
|
|
172
|
+
const factsResolved = config?.profileFactsEnabled === true && typeof factsProvider === "function"
|
|
173
|
+
? resolveFactsCard(factsProvider(), agentId, { strict })
|
|
174
|
+
: { facts: [], source: "none" };
|
|
175
|
+
const factLines = [];
|
|
176
|
+
if (factsResolved.facts.length) {
|
|
177
|
+
let chars = 0;
|
|
178
|
+
let shown = 0;
|
|
179
|
+
for (const fact of factsResolved.facts) {
|
|
180
|
+
const value = String(fact);
|
|
181
|
+
const line = STR.profileFactLine[language](value);
|
|
182
|
+
if (shown >= PROFILE_FACTS_MAX_LINES || chars + line.length > PROFILE_FACTS_MAX_CHARS) break;
|
|
183
|
+
factLines.push({ kind: "fact", index: shown, value, text: line });
|
|
184
|
+
chars += line.length;
|
|
185
|
+
shown += 1;
|
|
186
|
+
}
|
|
187
|
+
const remaining = factsResolved.facts.length - shown;
|
|
188
|
+
if (remaining > 0) factLines.push({ kind: "factsTruncated", remaining, text: STR.profileFactsTruncated[language](remaining) });
|
|
189
|
+
if (factLines.length) {
|
|
190
|
+
// 表头的 n 是**总条数**(含没注入的),与「另有 N 条未注入」互补,别改成 shown。
|
|
191
|
+
factLines.unshift({
|
|
192
|
+
kind: "factsHeader",
|
|
193
|
+
shown,
|
|
194
|
+
total: factsResolved.facts.length,
|
|
195
|
+
text: STR.profileFactsHeader[language](factsResolved.facts.length)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
69
199
|
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : null;
|
|
70
|
-
|
|
71
|
-
|
|
200
|
+
const sid = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : null;
|
|
201
|
+
const pid =
|
|
202
|
+
typeof parentSessionId === "string" && parentSessionId.trim() ? parentSessionId.trim() : null;
|
|
203
|
+
if (!profile && !rules.length && !personaLines.length && !factLines.length) {
|
|
204
|
+
// `parent` 是读数(面板要能说清「这张卡是从父会话继承来的」),不参与判定。
|
|
205
|
+
return {
|
|
206
|
+
text: "",
|
|
207
|
+
segments: [],
|
|
208
|
+
source: resolved.source,
|
|
209
|
+
agent: id,
|
|
210
|
+
session: sid,
|
|
211
|
+
parent: pid,
|
|
212
|
+
configGate: enabled
|
|
213
|
+
};
|
|
72
214
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
if (profile)
|
|
76
|
-
|
|
77
|
-
|
|
215
|
+
const segments = [{ kind: "header", text: STR.userSettingsHeader[language] }];
|
|
216
|
+
segments.push(...personaLines);
|
|
217
|
+
if (profile) segments.push({ kind: "profile", value: profile, text: STR.profileLine[language](profile) });
|
|
218
|
+
segments.push(...factLines);
|
|
219
|
+
for (const rule of rules) segments.push({ kind: "rule", value: rule, text: STR.ruleLine[language](rule) });
|
|
220
|
+
const lines = segments.map((s) => s.text);
|
|
221
|
+
return {
|
|
222
|
+
text: escapePromptVars(config, lines.join("\n")),
|
|
223
|
+
segments,
|
|
224
|
+
source: resolved.source,
|
|
225
|
+
agent: id,
|
|
226
|
+
session: sid,
|
|
227
|
+
parent: pid,
|
|
228
|
+
configGate: enabled
|
|
229
|
+
};
|
|
78
230
|
}
|
package/src/vector-index.js
CHANGED
|
@@ -65,35 +65,105 @@ export function createVectorIndex({ store, logger }) {
|
|
|
65
65
|
return store.searchVector(queryVector, { limit, threshold });
|
|
66
66
|
},
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Re-embed every row whose vector is missing OR came from another model.
|
|
70
|
+
*
|
|
71
|
+
* Returns { indexed, failed, skipped, remaining, error? } — three distinct
|
|
72
|
+
* outcomes instead of one `skipped` bucket that meant "failed", "threw" and
|
|
73
|
+
* "did not fit in the limit" all at once. #604.
|
|
74
|
+
*/
|
|
69
75
|
async rebuildIndex(embedder, { limit = 1000 } = {}) {
|
|
70
76
|
// The loop needs a single-text embedder. Native embedSingle is preferred;
|
|
71
77
|
// embed-only OpenAI-compatible clients (issue #10) are accepted too via
|
|
72
78
|
// their `embed` single-text interface, so /vector-reindex no longer
|
|
73
79
|
// silently returns 0 for them. An embedder exposing neither is ignored.
|
|
80
|
+
// #604: prefer the detailed entry point when the embedder has one. The
|
|
81
|
+
// legacy OpenAI-compatible client drops the failure reason on its normal
|
|
82
|
+
// path (embedText returns only the vector), so a provider outage and an
|
|
83
|
+
// unembeddable row both surfaced here as a bare null. The reason is what
|
|
84
|
+
// tells them apart, and the operator needs it to know whether pressing
|
|
85
|
+
// the button again is worth anything.
|
|
74
86
|
let embedOne = null;
|
|
87
|
+
let embedOneDetailed = null;
|
|
88
|
+
if (embedder && typeof embedder.embedSingleDetailed === "function") {
|
|
89
|
+
embedOneDetailed = (text) => embedder.embedSingleDetailed(text);
|
|
90
|
+
}
|
|
75
91
|
if (embedder && typeof embedder.embedSingle === "function") {
|
|
76
92
|
embedOne = (text) => embedder.embedSingle(text);
|
|
77
93
|
} else if (embedder && typeof embedder.embed === "function") {
|
|
78
94
|
embedOne = (text) => Promise.resolve(embedder.embed(text));
|
|
79
95
|
}
|
|
80
|
-
if (!embedOne)
|
|
81
|
-
|
|
96
|
+
if (!embedOne && !embedOneDetailed) {
|
|
97
|
+
return { indexed: 0, failed: 0, skipped: 0, remaining: 0, error: "no-embedder" };
|
|
98
|
+
}
|
|
99
|
+
const model = embedder?.modelHash ?? null;
|
|
100
|
+
// #604: this used to call needsEmbedding(), which only matches empty
|
|
101
|
+
// vectors. After a model switch every row already has a vector — from the
|
|
102
|
+
// previous model — so "reindex" selected nothing, embedded nothing, and
|
|
103
|
+
// reported {indexed: 0, skipped: 0} as if the index were already current.
|
|
104
|
+
// The boot sweep has always used the model-aware selector; using the same
|
|
105
|
+
// one here is what makes the button mean what it says.
|
|
106
|
+
const rows = model && typeof store.needsEmbeddingFor === "function"
|
|
107
|
+
? store.needsEmbeddingFor(model, limit)
|
|
108
|
+
: store.needsEmbedding(limit);
|
|
82
109
|
let indexed = 0;
|
|
110
|
+
let failed = 0;
|
|
111
|
+
const failureReasons = {};
|
|
112
|
+
const noteFailure = (reason) => {
|
|
113
|
+
failureReasons[reason] = (failureReasons[reason] ?? 0) + 1;
|
|
114
|
+
};
|
|
83
115
|
for (const row of rows) {
|
|
84
116
|
try {
|
|
85
117
|
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
86
|
-
|
|
118
|
+
let vector;
|
|
119
|
+
if (embedOneDetailed) {
|
|
120
|
+
const detailed = await embedOneDetailed(text);
|
|
121
|
+
vector = detailed ? detailed.vector : null;
|
|
122
|
+
if (!vector && detailed?.reason) noteFailure(`error:${detailed.reason}`);
|
|
123
|
+
} else {
|
|
124
|
+
vector = await embedOne(text);
|
|
125
|
+
}
|
|
87
126
|
if (vector && vector.length) {
|
|
88
|
-
|
|
127
|
+
// #604: the model fingerprint was omitted here, so every rebuilt
|
|
128
|
+
// row kept embedding_model NULL. needsEmbeddingFor() then treated
|
|
129
|
+
// it as stale forever: the boot sweep re-embedded it on each start,
|
|
130
|
+
// the panel counted it as unsearchable, and pressing reindex again
|
|
131
|
+
// found nothing (the vector was no longer empty). Passing the
|
|
132
|
+
// fingerprint closes the loop between the write and the selector.
|
|
133
|
+
store.setEmbedding(row.id, vector, model ?? undefined);
|
|
89
134
|
indexed++;
|
|
135
|
+
} else {
|
|
136
|
+
failed++;
|
|
137
|
+
// A detailed adapter already recorded why; the plain path has no
|
|
138
|
+
// reason to record, so name the only thing it can mean.
|
|
139
|
+
if (!embedOneDetailed) noteFailure("empty-vector");
|
|
140
|
+
logger?.warn?.(`[dsh-mneme] rebuild produced no vector for ${row.id}`);
|
|
90
141
|
}
|
|
91
142
|
} catch (error) {
|
|
143
|
+
failed++;
|
|
144
|
+
noteFailure(`error:${String(error)}`);
|
|
92
145
|
logger?.warn?.(`[dsh-mneme] rebuild embed failed for ${row.id}: ${String(error)}`);
|
|
93
146
|
}
|
|
94
147
|
}
|
|
95
148
|
if (indexed > 0 && embedder.modelHash) this.markModel(embedder.modelHash, embedder.dimension);
|
|
96
|
-
|
|
149
|
+
// remaining = still selected by the same predicate after this run. Rows
|
|
150
|
+
// that failed stay in it (they were never written), which is exactly the
|
|
151
|
+
// number the caller needs to decide whether to run again.
|
|
152
|
+
const remaining = typeof store.countNeedsEmbeddingFor === "function"
|
|
153
|
+
? store.countNeedsEmbeddingFor(model)
|
|
154
|
+
: null;
|
|
155
|
+
// `skipped` is 0 by definition here, not as a placeholder: every row the
|
|
156
|
+
// selector returned is attempted in the loop above, so nothing is left
|
|
157
|
+
// untouched. Rows that produced no vector are `failed`. The remaining
|
|
158
|
+
// bucket is the selector's own count, which is why a row can be both
|
|
159
|
+
// failed and remaining — it was tried and it is still stale.
|
|
160
|
+
//
|
|
161
|
+
// `failureReasons` mirrors the boot sweep's bucket names so the two paths
|
|
162
|
+
// read the same: `empty-vector` means this row will never embed, while an
|
|
163
|
+
// `error:` bucket means the provider or the store failed and the same
|
|
164
|
+
// press may well succeed next time. A run that only ever reports
|
|
165
|
+
// empty-vector is not a retry candidate.
|
|
166
|
+
return { indexed, failed, skipped: 0, remaining, failureReasons };
|
|
97
167
|
},
|
|
98
168
|
|
|
99
169
|
/** Index stats for the Web panel / debugging. */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// dsh-mneme/src/wiki/cli-hint.js
|
|
2
|
+
// issue #607:面板空态让用户「在仓库目录里跑 `dsh-mneme-choro <仓库路径> --apply`」,
|
|
3
|
+
// 但插件是装在 profile 依赖里的,npm 只为**全局**安装把 shim 放进 PATH。profile 的
|
|
4
|
+
// shim 落在 `<profile>/node_modules/.bin`,那里不在 PATH 上 —— 用户照着抄必然
|
|
5
|
+
// command not found。issue 作者实测后的原话是「完全就是 AI 自己编的」。
|
|
6
|
+
//
|
|
7
|
+
// 所以提示里的命令必须**能直接粘贴就跑**:给绝对路径的入口,而不是一个只对全局
|
|
8
|
+
// 安装成立的光名字。入口从插件自身位置推导(不信 PATH、不信 cwd)。
|
|
9
|
+
//
|
|
10
|
+
// 做法是逐级向上找「同时存在 bin/dsh-mneme-choro.mjs 与 package.json」的目录,
|
|
11
|
+
// 而不是固定 dirname 几次:本仓库的 src 与 lib 是同一层,但 wiki/ 是第二层,
|
|
12
|
+
// bin/ 里的脚本又在第一层 —— 写死层数的推导只要有一处调用方挪个位置就悄悄算错,
|
|
13
|
+
// 而算错的表现正是「又给出一条跑不起来的命令」,与这条 issue 的病因同源。
|
|
14
|
+
//
|
|
15
|
+
// 这条对 hoisted 与 pnpm isolated 两种 linker 都成立:后者的真实路径在
|
|
16
|
+
// `.pnpm/<pkg>/node_modules/@modusensus/dsh-mneme/`,包根照样是它的父目录。
|
|
17
|
+
//
|
|
18
|
+
// 刻意**只给 `node <entry>` 这一种形态**:`.bin` 的 shim 在 hoisted 布局下位于
|
|
19
|
+
// 包根的 `../../../node_modules/.bin`,isolated 布局下却在 profile 根 —— 猜错就是
|
|
20
|
+
// 再给一条跑不起来的命令。宁可只给一条一定对的,也不并列两条一条错的。
|
|
21
|
+
//
|
|
22
|
+
// 找不到入口就返回 null(源码树被裁剪、或调用方在包外),调用方退回通用说法,
|
|
23
|
+
// 不硬编一个假路径。
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const ENTRY_BASENAME = "dsh-mneme-choro.mjs";
|
|
29
|
+
// 从 `src/wiki/` 到包根是 2 层,从 `bin/` 是 1 层;留到 6 层是为了 pnpm 的
|
|
30
|
+
// `.pnpm/<pkg>/node_modules/@modusensus/<name>/` 这种深路径,同时不至于一路走到盘符。
|
|
31
|
+
const MAX_LEVELS = 6;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* choro CLI 入口的绝对路径;找不到(源码树被裁剪、调用方在包外)返回 null。
|
|
35
|
+
*
|
|
36
|
+
* @param {string} fromModuleUrl - 调用方自己的 `import.meta.url`。
|
|
37
|
+
* @returns {string|null}
|
|
38
|
+
*/
|
|
39
|
+
export function choroCliEntry(fromModuleUrl) {
|
|
40
|
+
let dir;
|
|
41
|
+
try {
|
|
42
|
+
dir = dirname(fileURLToPath(fromModuleUrl));
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
for (let level = 0; level <= MAX_LEVELS; level += 1) {
|
|
47
|
+
const entry = join(dir, "bin", ENTRY_BASENAME);
|
|
48
|
+
// 两个条件都要:只有 bin/ 没有 package.json 的目录可能是别人的同名 bin;
|
|
49
|
+
// 只有 package.json 没有 bin/ 的是源码树被裁剪(`files` 少了 bin)。
|
|
50
|
+
if (existsSync(entry) && existsSync(join(dir, "package.json"))) return entry;
|
|
51
|
+
const up = dirname(dir);
|
|
52
|
+
if (up === dir) break;
|
|
53
|
+
dir = up;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 给一句可直接粘贴的命令。只负责引号与形态,仓库路径由调用方给(面板给
|
|
60
|
+
* `<仓库路径>` 占位符,工具报错给 `<repo>`)。
|
|
61
|
+
*
|
|
62
|
+
* 引号按平台分:Windows 的 cmd / PowerShell 用双引号;POSIX 用单引号并把内嵌
|
|
63
|
+
* 单引号转成 `'\''` —— 安装路径里带空格是常态(`C:\Program Files\`),不带引号的
|
|
64
|
+
* 示例等于又给人一条跑不起来的命令。
|
|
65
|
+
*
|
|
66
|
+
* @param {string|null} entry - `choroCliEntry()` 的产物。
|
|
67
|
+
* @param {string} repoPlaceholder - 仓库路径占位符的展示形态。
|
|
68
|
+
* @param {string} [platform] - 覆盖 `process.platform`(测试用)。
|
|
69
|
+
* @returns {string|null}
|
|
70
|
+
*/
|
|
71
|
+
export function choroCliCommand(entry, repoPlaceholder, platform = process.platform) {
|
|
72
|
+
if (!entry) return null;
|
|
73
|
+
const quoted = platform === "win32"
|
|
74
|
+
? `"${entry}"`
|
|
75
|
+
: `'${entry.replace(/'/g, "'\\''")}'`;
|
|
76
|
+
return `node ${quoted} ${repoPlaceholder} --apply`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 一站式的便捷口:从调用方的 `import.meta.url` 直接拿到命令串。
|
|
81
|
+
* 找不到入口返回 null,调用方据此选通用说法。
|
|
82
|
+
*
|
|
83
|
+
* @param {string} fromModuleUrl
|
|
84
|
+
* @param {string} repoPlaceholder
|
|
85
|
+
* @param {string} [platform]
|
|
86
|
+
* @returns {string|null}
|
|
87
|
+
*/
|
|
88
|
+
export function choroCliCommandFrom(fromModuleUrl, repoPlaceholder, platform = process.platform) {
|
|
89
|
+
return choroCliCommand(choroCliEntry(fromModuleUrl), repoPlaceholder, platform);
|
|
90
|
+
}
|