@modusensus/dsh-mneme 0.12.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 +48 -19
- package/docs/CONFIGURATION.md +43 -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 +581 -29
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3537 -1205
- package/lib/config.js +139 -1
- 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/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 +92 -2
- package/lib/inject.js +189 -18
- 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 +87 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +193 -66
- package/lib/settings.js +112 -14
- package/lib/store.js +347 -21
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +116 -2
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +581 -29
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +139 -1
- 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/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 +92 -2
- package/src/inject.js +189 -18
- 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 +87 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +193 -66
- package/src/settings.js +112 -14
- package/src/store.js +347 -21
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +116 -2
- 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 +375 -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 +779 -154
- 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/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-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/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 +7 -7
- 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/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/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +355 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +7 -0
- package/test/wiki-t1-jobs.test.js +125 -0
package/src/tools.js
CHANGED
|
@@ -9,8 +9,16 @@ import { TOOL_GUIDE, CONTINUITY_TOOL_RULE } from "./guide.js";
|
|
|
9
9
|
import { injectChildEnabled } from "./config.js";
|
|
10
10
|
import { normalizeScopeStateDimension, normalizeScopeStateValue, normalizeScopeStateState } from "./scope-state.js";
|
|
11
11
|
import { lastUserQuery } from "./inject.js";
|
|
12
|
+
// issue #607:工具报错里的 CLI 命令同样要给绝对路径入口——profile 装的插件没有
|
|
13
|
+
// 全局 shim,光写 `dsh-mneme-choro` 用户照抄只会 command not found。
|
|
14
|
+
import { choroCliCommandFrom } from "./wiki/cli-hint.js";
|
|
15
|
+
// 知识库三工具(契约 §3.4):定义住在 kb/tools.js,本文件只决定注册不注册。
|
|
16
|
+
import { kbToolDefinitions } from "./kb/tools.js";
|
|
12
17
|
|
|
13
18
|
const TEXT_OUTPUT = (text) => [{ type: "text", text }];
|
|
19
|
+
// 工具报错里的命令串:本文件在 src/ 与 lib/ 同一层(bin/ 的父级),模块加载时推导一次。
|
|
20
|
+
// 推导不出(源码树被裁剪)就返回 null,文案退回通用说法。
|
|
21
|
+
const CHORO_CLI = choroCliCommandFrom(import.meta.url, "<repo>");
|
|
14
22
|
const incompleteChoroSearch = (pages) =>
|
|
15
23
|
`Search was incomplete because these pages could not be read (${pages.join(", ")}). Check the local checkout and page permissions.`;
|
|
16
24
|
// Per-registry tool-name registry: guards against duplicate registration on
|
|
@@ -81,7 +89,8 @@ const MEMORY_PROVENANCE = (m) => {
|
|
|
81
89
|
// choro(M1):chorography 只读查询对象(createChoroQueries 产物)。缺省 null =
|
|
82
90
|
// 装配层没接(headless/测试),三个 choro_* 工具走 hiddenTools 闸根本不注册。
|
|
83
91
|
// 它与 service 刻意分开传——chorography 的所有权模型与记忆域相反(spec §3)。
|
|
84
|
-
|
|
92
|
+
// kb:知识库装配对象(createKb 产物);缺省 null = 宿主没装它,三个 kb 工具不注册。
|
|
93
|
+
export function createTools(ctx, service, config, embedder, sessionGate = null, choro = null, kb = null) {
|
|
85
94
|
const toolsRegistry = ctx.tools;
|
|
86
95
|
let registration = REGISTERED_TOOLS.get(toolsRegistry);
|
|
87
96
|
if (!registration) {
|
|
@@ -99,7 +108,7 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
99
108
|
const row = choro.resolveRepo(ref);
|
|
100
109
|
if (!row) {
|
|
101
110
|
const reason = ref ? `Repository ${JSON.stringify(ref)} is not registered.` : "No repository is registered for chorography.";
|
|
102
|
-
throw new Error(`${reason} Run
|
|
111
|
+
throw new Error(`${reason} ${CHORO_CLI ? `Run \`${CHORO_CLI}\`` : "Run `dsh-mneme-choro <repo> --apply`"} to register a repository.`);
|
|
103
112
|
}
|
|
104
113
|
if (row.ambiguous) {
|
|
105
114
|
const choices = row.ambiguous.map(({ id, name }) => `${id} (${name})`).join(", ");
|
|
@@ -453,7 +462,11 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
453
462
|
name: "memory_forget",
|
|
454
463
|
description:
|
|
455
464
|
"Stop a memory from being auto-injected and from appearing in searches and lists without deleting it. " +
|
|
456
|
-
"The entry stays in storage; pass forgotten: false to restore it."
|
|
465
|
+
"The entry stays in storage; pass forgotten: false to restore it. " +
|
|
466
|
+
"#532: suppression is per ROW - facts fused into the same entry leave visibility together. " +
|
|
467
|
+
"When the request targets one fact among several held by a row, inspect the row first (memory_get), " +
|
|
468
|
+
"split it with memory_update if needed, or say in your reply what else the suppression took down " +
|
|
469
|
+
"(docs/FORGET.md records the supersession gap behind this).",
|
|
457
470
|
parameters: {
|
|
458
471
|
id: { type: "string", required: true },
|
|
459
472
|
forgotten: { type: "boolean", description: "Suppress (true, default) or restore (false) the entry's visibility" }
|
|
@@ -1220,7 +1233,7 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
1220
1233
|
throw new Error(`Page ${JSON.stringify(args?.slug ?? "")} was not found. Use choro_module_map to list page slugs.`);
|
|
1221
1234
|
}
|
|
1222
1235
|
if (page.error === "page-missing") {
|
|
1223
|
-
throw new Error(`Page ${JSON.stringify(page.slug)} is missing from disk (${page.relPath}). Check the local checkout or run
|
|
1236
|
+
throw new Error(`Page ${JSON.stringify(page.slug)} is missing from disk (${page.relPath}). Check the local checkout or run ${CHORO_CLI ? `\`${CHORO_CLI}\`` : "`dsh-mneme-choro <repo> --apply`"} to rebuild it.`);
|
|
1224
1237
|
}
|
|
1225
1238
|
if (page.error) {
|
|
1226
1239
|
throw new Error(`Page ${JSON.stringify(page.slug)} could not be read (${page.relPath}). Check the local file and its permissions.`);
|
|
@@ -1289,6 +1302,24 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
1289
1302
|
hiddenTools.add("choro_read_page");
|
|
1290
1303
|
hiddenTools.add("choro_search");
|
|
1291
1304
|
}
|
|
1305
|
+
// 知识库三工具(契约 §3.4):与 choro 同款反向语义。装配缺件(kb 为 null)与没开
|
|
1306
|
+
// 对模型同样不可见——可区分性归路由层(503 / 409),模型侧不需要看见半个知识库。
|
|
1307
|
+
if (config?.kbEnabled !== true || !kb) {
|
|
1308
|
+
hiddenTools.add("kb_list_collections");
|
|
1309
|
+
hiddenTools.add("kb_search");
|
|
1310
|
+
hiddenTools.add("kb_read_doc");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 知识库工具定义在 kb/tools.js。**必须过 defineTool**:那里返回的是「describe 形状」
|
|
1314
|
+
// (属性上的 required:true、output.schema 用 spec 写法),直接 ctx.tools.register
|
|
1315
|
+
// 会被宿主的 JSON Schema 校验拒("required is not supported on type string")——
|
|
1316
|
+
// 这是接线时踩到的真故障,不是可选加工。判定按 parameters.type==="object" 分:
|
|
1317
|
+
// 已经是 JSON Schema 的说明调用方自己包过,再包一次会把 schema 转坏。
|
|
1318
|
+
if (typeof kbToolDefinitions === "function") {
|
|
1319
|
+
for (const def of kbToolDefinitions({ kb, cfg: config })) {
|
|
1320
|
+
tools.push(def?.parameters?.type === "object" ? def : defineTool(def));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1292
1323
|
|
|
1293
1324
|
for (const tool of tools) {
|
|
1294
1325
|
if (hiddenTools.has(tool.name)) {
|
package/src/user-settings.js
CHANGED
|
@@ -36,6 +36,14 @@ export function escapePromptVars(config, text) {
|
|
|
36
36
|
* 按会话身份取人格卡。返回 `{ card, source }`,`source` 是给面板解释「用的哪张卡」的
|
|
37
37
|
* 读数,不参与判定。
|
|
38
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
|
+
*
|
|
39
47
|
* `strict`(config.personaStrictAddressing,默认关)决定「有身份、没这张身份的卡」
|
|
40
48
|
* 与「这张卡被显式关掉」这两种情况:
|
|
41
49
|
* - 关(现状):落 default 卡。default 于是变成「所有没建卡的 agent 都能拿到它」,
|
|
@@ -44,20 +52,45 @@ export function escapePromptVars(config, text) {
|
|
|
44
52
|
* 身份」的会话(无名会话本来就是 default 卡的受众)。
|
|
45
53
|
* 默认关是有意的:这是一次口径变更,不能让老用户的注入文本在升级后静默变化。
|
|
46
54
|
*
|
|
47
|
-
* 严格模式有一格例外(#587
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 于是必然落进后一种处境:开了严格寻址就把唯一那张卡静默关死。
|
|
55
|
+
* 严格模式有一格例外(#587):两维**都没建过卡**时仍回落 default。两种处境必须
|
|
56
|
+
* 分开——「建了卡、只是没这一张的卡」是真该按 none 处理(否则关掉一张卡等于没关),
|
|
57
|
+
* 而「一张卡都没建」时 default 是用户手里唯一的一张卡,按 none 处理等于把它永久
|
|
58
|
+
* 废掉,且面板不报错。判据看两维合起来的键数(#606 起):只建了 bySession 卡时,
|
|
59
|
+
* 一个按 byAgent 寻址的会话同样不该被 default 兜底——用户已经做过分区了。
|
|
53
60
|
*/
|
|
54
|
-
export function resolvePersonaCard(
|
|
61
|
+
export function resolvePersonaCard(
|
|
62
|
+
settings,
|
|
63
|
+
agentId,
|
|
64
|
+
{ strict = false, sessionId = null, parentSessionId = null } = {}
|
|
65
|
+
) {
|
|
55
66
|
const store = settings.getPersona();
|
|
56
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
|
+
// 第三维:按预设/身份建的卡
|
|
57
87
|
const card = id ? store.byAgent[id] : null;
|
|
58
88
|
if (card && card.enabled === true) return { card, source: "byAgent" };
|
|
59
|
-
//
|
|
60
|
-
|
|
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" };
|
|
61
94
|
return { card: store.default, source: "default" };
|
|
62
95
|
}
|
|
63
96
|
|
|
@@ -78,57 +111,120 @@ export function resolveFactsCard(factsStore, agentId, { strict = false } = {}) {
|
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
/**
|
|
81
|
-
* 渲染整块「用户设置」。`agentId` 是会话身份(解析不到传 null → 走 default
|
|
114
|
+
* 渲染整块「用户设置」。`agentId` 是会话身份(解析不到传 null → 走 default 兜底),
|
|
115
|
+
* `sessionId` 是绑会话卡的键(#606;解析不到传 null)。
|
|
82
116
|
* `factsProvider` 是 profile-facts 域的读口(返回 `{ default: [...], byAgent: {...} }`),
|
|
83
117
|
* 可选;**开关与 provider 双闸**——profileFactsEnabled 关着时连 provider 都不调,
|
|
84
|
-
* 「关」必须是零读数而不是「读了不渲染」。返回 `{ text, source, agent, configGate }`:
|
|
118
|
+
* 「关」必须是零读数而不是「读了不渲染」。返回 `{ text, source, agent, session, configGate }`:
|
|
85
119
|
* `text` 为最终注入文本(全空时 ""),`configGate` 是 personaEnabled 的现值——面板
|
|
86
120
|
* 要靠它解释「卡开着为什么没生效」。
|
|
87
121
|
*/
|
|
88
|
-
export function renderUserSettingsBlock({
|
|
122
|
+
export function renderUserSettingsBlock({
|
|
123
|
+
settings,
|
|
124
|
+
config,
|
|
125
|
+
agentId = null,
|
|
126
|
+
sessionId = null,
|
|
127
|
+
parentSessionId = null,
|
|
128
|
+
factsProvider = null
|
|
129
|
+
}) {
|
|
89
130
|
const language = langOf(config);
|
|
90
131
|
const profile = settings.getProfile().trim();
|
|
91
132
|
const rules = settings.getRules();
|
|
92
133
|
const enabled = config?.personaEnabled === true;
|
|
134
|
+
const strict = config?.personaStrictAddressing === true;
|
|
93
135
|
const resolved = enabled
|
|
94
|
-
? resolvePersonaCard(settings, agentId, { strict
|
|
136
|
+
? resolvePersonaCard(settings, agentId, { strict, sessionId, parentSessionId })
|
|
95
137
|
: { card: {}, source: "default" };
|
|
96
138
|
const persona = resolved.card;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|
|
104
170
|
// 事实段:开关关 / 无 provider / 该卡没事实三种情况都不渲染,注入文本与
|
|
105
171
|
// 未启用事实时逐字节一致(老用户零变化)。
|
|
106
172
|
const factsResolved = config?.profileFactsEnabled === true && typeof factsProvider === "function"
|
|
107
|
-
? resolveFactsCard(factsProvider(), agentId, { strict
|
|
173
|
+
? resolveFactsCard(factsProvider(), agentId, { strict })
|
|
108
174
|
: { facts: [], source: "none" };
|
|
109
175
|
const factLines = [];
|
|
110
176
|
if (factsResolved.facts.length) {
|
|
111
177
|
let chars = 0;
|
|
112
178
|
let shown = 0;
|
|
113
179
|
for (const fact of factsResolved.facts) {
|
|
114
|
-
const
|
|
180
|
+
const value = String(fact);
|
|
181
|
+
const line = STR.profileFactLine[language](value);
|
|
115
182
|
if (shown >= PROFILE_FACTS_MAX_LINES || chars + line.length > PROFILE_FACTS_MAX_CHARS) break;
|
|
116
|
-
factLines.push(line);
|
|
183
|
+
factLines.push({ kind: "fact", index: shown, value, text: line });
|
|
117
184
|
chars += line.length;
|
|
118
185
|
shown += 1;
|
|
119
186
|
}
|
|
120
187
|
const remaining = factsResolved.facts.length - shown;
|
|
121
|
-
if (remaining > 0) factLines.push(STR.profileFactsTruncated[language](remaining));
|
|
122
|
-
if (factLines.length)
|
|
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
|
+
}
|
|
123
198
|
}
|
|
124
199
|
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : null;
|
|
200
|
+
const sid = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : null;
|
|
201
|
+
const pid =
|
|
202
|
+
typeof parentSessionId === "string" && parentSessionId.trim() ? parentSessionId.trim() : null;
|
|
125
203
|
if (!profile && !rules.length && !personaLines.length && !factLines.length) {
|
|
126
|
-
|
|
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
|
+
};
|
|
127
214
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (profile)
|
|
131
|
-
|
|
132
|
-
for (const rule of rules)
|
|
133
|
-
|
|
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
|
+
};
|
|
134
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
|
+
}
|
package/src/wiki/queries.js
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
// 输入——面板与工具传进来的 slug 查不到模块行就返回 null,绝不用它拼路径。
|
|
10
10
|
import { readFileSync, existsSync, realpathSync } from "node:fs";
|
|
11
11
|
import { join, resolve, sep } from "node:path";
|
|
12
|
+
// 过期判定复用既有三件真源(t0 的 gitHead、plan 的指纹、render 的模板版本),
|
|
13
|
+
// 面板侧不另造一套判据——两套判据迟早会对同一仓库给出两个答案。
|
|
14
|
+
import { gitHead } from "./t0.js";
|
|
15
|
+
import { loadPlanFromRepo } from "./plan.js";
|
|
16
|
+
import { TEMPLATE_VERSION } from "./render.js";
|
|
12
17
|
|
|
13
18
|
// 页正文去 frontmatter / CHORO 标记行后作为嵌入文本(P40:嵌页级全文)。
|
|
14
19
|
function pageEmbedText(content) {
|
|
@@ -49,12 +54,114 @@ export function createChoroQueries({ store }) {
|
|
|
49
54
|
`SELECT r.id, r.root_path, r.name, r.plan_fingerprint, r.git_baseline, r.updated_at,
|
|
50
55
|
(SELECT COUNT(*) FROM choro_modules m WHERE m.repo_id = r.id) AS module_count,
|
|
51
56
|
(SELECT COUNT(*) FROM choro_pages p JOIN choro_modules m2 ON p.module_id = m2.id
|
|
52
|
-
WHERE m2.repo_id = r.id) AS page_count
|
|
57
|
+
WHERE m2.repo_id = r.id) AS page_count,
|
|
58
|
+
(SELECT COUNT(*) FROM choro_claims c
|
|
59
|
+
JOIN choro_pages p2 ON c.page_id = p2.id
|
|
60
|
+
JOIN choro_modules m3 ON p2.module_id = m3.id
|
|
61
|
+
WHERE m3.repo_id = r.id) AS claim_count
|
|
53
62
|
FROM choro_repos r ORDER BY r.id`
|
|
54
63
|
).all();
|
|
55
64
|
return rows;
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 面板仓库列表(契约 §3.2):在 listRepos 的行上补契约要求的字段。
|
|
69
|
+
*
|
|
70
|
+
* 为什么另开一个函数而不是改 listRepos 的返回形状:listRepos 的
|
|
71
|
+
* module_count / page_count 被工具层与查询层测试锁着(下划线命名),改名就是
|
|
72
|
+
* 破坏既有调用方;而契约要的是 modules / pages 这样的对外字段。两套名字并存
|
|
73
|
+
* 难看,但改名的连带面比难看贵得多——装饰层把它包一层,工具层照旧读原始行。
|
|
74
|
+
*
|
|
75
|
+
* 每个仓库都要 fork 一次 git 判过期:这是用户点开面板才走的路(不是每轮注入的
|
|
76
|
+
* 热路径),一次 rev-parse 几十毫秒在交互里不可感知。
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* 契约字段的仓库对象(stale 由调用方决定是否重算)。
|
|
80
|
+
*
|
|
81
|
+
* 注意区分两个入口:repoFacts 每次都会 fork git 判过期;repoFromRow 是纯行投影,
|
|
82
|
+
* 给「已经知道过期原因、手上有判定结果」的调用方用(例如自动重跑之后要紧着按
|
|
83
|
+
* 新基线说话,不能再拿旧行算一遍)。混用会得到自相矛盾的回执。
|
|
84
|
+
*/
|
|
85
|
+
function repoFromRow(row) {
|
|
86
|
+
const rootPath = String(row.root_path ?? "");
|
|
87
|
+
return {
|
|
88
|
+
id: row.id,
|
|
89
|
+
name: row.name,
|
|
90
|
+
rootPath,
|
|
91
|
+
modules: Number(row.module_count ?? 0),
|
|
92
|
+
pages: Number(row.page_count ?? 0),
|
|
93
|
+
claims: Number(row.claim_count ?? 0),
|
|
94
|
+
// legacy = 这个仓库里有页面是用旧模板(或 M0 的 NULL)渲染的:重跑一次 T0 就
|
|
95
|
+
// 会把它们升级成当前模板,面板据此提示「建议重建」而不是让用户自己比对版本。
|
|
96
|
+
legacy: legacyPages(row.id) > 0,
|
|
97
|
+
lastRunAt: row.updated_at ?? null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function legacyPages(repoId) {
|
|
102
|
+
const hit = db.prepare(
|
|
103
|
+
`SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
|
|
104
|
+
WHERE m.repo_id = ? AND (p.template_version IS NULL OR p.template_version <> ?)`
|
|
105
|
+
).get(repoId, TEMPLATE_VERSION);
|
|
106
|
+
return Number(hit?.n ?? 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function repoFacts(row) {
|
|
110
|
+
const rootPath = String(row.root_path ?? "");
|
|
111
|
+
// git 优先:代码变了是更硬的「该重跑」信号,plan 变了才退而求其次——两者同时
|
|
112
|
+
// 成立时面板只说一句话,说「git」比说「plan」更贴近用户看到的现象(他刚改了码)。
|
|
113
|
+
const head = rootPath === "" ? null : gitHead(rootPath);
|
|
114
|
+
const baseline = String(row.git_baseline ?? "").trim();
|
|
115
|
+
let staleReason = null;
|
|
116
|
+
if (head && head !== baseline) {
|
|
117
|
+
staleReason = "git";
|
|
118
|
+
} else if (rootPath !== "") {
|
|
119
|
+
// 读不到 git(head 为 null)时不直接判「没过期」:plan.yaml 可能已经被改过,
|
|
120
|
+
// 那条判据不依赖 git,仍能给出答案。
|
|
121
|
+
let planFingerprintNow = null;
|
|
122
|
+
try { planFingerprintNow = loadPlanFromRepo(rootPath).fingerprint; } catch { planFingerprintNow = null; }
|
|
123
|
+
// plan.yaml 校验不过时 loadPlanFromRepo 会抛——那是「现在读不出结论」,不是
|
|
124
|
+
// 「过期了」,所以落 null 保持 stale=false,让重建按钮自己去报那条 YAML 错误。
|
|
125
|
+
if (planFingerprintNow && planFingerprintNow !== String(row.plan_fingerprint ?? "")) {
|
|
126
|
+
staleReason = "plan";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...repoFromRow(row),
|
|
131
|
+
stale: staleReason !== null,
|
|
132
|
+
staleReason
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 单仓的原始 listRepos 行(自动重跑之后要拿推进过的基线,不能再用旧行)。 */
|
|
137
|
+
function listRepoRow(repoId) {
|
|
138
|
+
return listRepos().find((r) => Number(r.id) === Number(repoId)) ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 契约形状的仓库列表:listRepos 的原始行 → 对外字段。 */
|
|
142
|
+
function listRepoFacts() {
|
|
143
|
+
return listRepos().map((row) => repoFacts(row));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 仓库的契约字段(按 id/name,给 /choro/page 这类已知单仓的读路径用)。 */
|
|
147
|
+
function repoFactsById(repoId) {
|
|
148
|
+
const row = listRepoRow(repoId);
|
|
149
|
+
return row ? repoFacts(row) : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 按绝对路径找已登记仓库(面板「选目录 → 生成」必需):T0 apply 落库时存的是
|
|
154
|
+
* realpath,所以这里也必须先 realpath 再比,否则 Windows 上盘符大小写/短名/符号
|
|
155
|
+
* 链接会让同一个目录被当成新仓库重复登记一次。
|
|
156
|
+
*/
|
|
157
|
+
function repoByRoot(rootPath) {
|
|
158
|
+
const raw = String(rootPath ?? "").trim();
|
|
159
|
+
if (raw === "") return null;
|
|
160
|
+
let normalized;
|
|
161
|
+
try { normalized = realpathSync(resolve(raw)); } catch { return null; }
|
|
162
|
+
return db.prepare("SELECT * FROM choro_repos WHERE root_path = ?").get(normalized) ?? null;
|
|
163
|
+
}
|
|
164
|
+
|
|
58
165
|
/** repo 解析:数字 id 或显示名(root_path 是本机私有路径,不作为对外键)。 */
|
|
59
166
|
function resolveRepo(ref) {
|
|
60
167
|
if (ref === undefined || ref === null || ref === "") {
|
|
@@ -82,6 +189,12 @@ export function createChoroQueries({ store }) {
|
|
|
82
189
|
`SELECT from_slug, to_slug, weight FROM choro_edges WHERE repo_id = ?
|
|
83
190
|
ORDER BY from_slug, to_slug`
|
|
84
191
|
).all(repoId);
|
|
192
|
+
// 纯加法补一个 camelCase 的 relPath:面板从地志页导入知识点要拼「根路径 + 页相对
|
|
193
|
+
// 路径」,而这里历史上直出 SQL 行的 snake_case(page_rel_path)。C 线按契约读的是
|
|
194
|
+
// mod.relPath,字段一缺就恒 undefined,用户选完模块点导入只会看到「请先选模块」。
|
|
195
|
+
// 原字段一律保留——src/tools.js 的 render 与 choro-command 都还在吃 file_count /
|
|
196
|
+
// page_status,改形状会波及它们;outline() 里的同名映射是这件事的正确写法。
|
|
197
|
+
for (const m of modules) m.relPath = m.page_rel_path ?? null;
|
|
85
198
|
return { modules, edges };
|
|
86
199
|
}
|
|
87
200
|
|
|
@@ -354,7 +467,8 @@ export function createChoroQueries({ store }) {
|
|
|
354
467
|
}
|
|
355
468
|
|
|
356
469
|
return {
|
|
357
|
-
listRepos,
|
|
470
|
+
listRepos, listRepoFacts, repoFacts, repoFactsById, repoFromRow, listRepoRow, repoByRoot,
|
|
471
|
+
resolveRepo, moduleMap, readPage, search, repoByCwd, outline,
|
|
358
472
|
embedPageVectors, searchSemantic, embedQueryText, semanticTopN,
|
|
359
473
|
};
|
|
360
474
|
}
|