project-node-memory 1.0.0 → 1.0.3
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 +18 -4
- package/README_EN.md +1 -1
- package/lib/client.js +51 -21
- package/lib/engine/registry.js +7 -2
- package/lib/host/routes.js +10 -2
- package/lib/host/runtime.js +21 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# project-node-memory
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 简体中文 · [English](README_EN.md)
|
|
4
4
|
|
|
5
5
|
把 **Markdown 目录当成知识库** 的 DSH 插件:为 Agent 提供 9 个 `kb_*` 工具,为人类提供知识节点面板(侧栏入口名「项目节点知识库」)。
|
|
6
6
|
|
|
@@ -128,9 +128,23 @@ dsh plugin add ./project-node-memory-<版本>.tgz
|
|
|
128
128
|
- D:\shared\kb_registry.json
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
`config.workspace`
|
|
131
|
+
`config.workspace` 可覆盖「工作区」;优先级是 **exec 上下文(面板点的目录 / 会话工作区)→ config.workspace → 环境变量 `DSH_NODEKB_WORKSPACE` → 最近一次工具用过的目录 → 进程 cwd**。所以在面板里换目录总能生效,配置只当默认值。
|
|
132
132
|
|
|
133
|
-
面板拿不到会话上下文,所以它默认跟随**最近一次 `kb_*` 工具调用所在的工作区**(Agent
|
|
133
|
+
面板拿不到会话上下文,所以它默认跟随**最近一次 `kb_*` 工具调用所在的工作区**(Agent 读过哪个项目,面板就显示哪个项目);顶栏的目录下拉只列出 DSH 用过的、且**真的有 `.nodekb`** 的项目目录(标了各自发现几个库),点右上「目录」也可以手填路径。选择存在浏览器 localStorage,只影响面板,不影响工具。
|
|
134
|
+
|
|
135
|
+
面板的两个下拉是联动的:**工作区 → 该工作区里的库**(`GET /nodekb/api/libs?scope=workspace`),全局注册表里的库单独归到「全局注册」分组,路径已不存在的注册项不显示(`exists:false`)。
|
|
136
|
+
|
|
137
|
+
想知道某个请求最后解析成了哪个工作区,直接看 `GET /nodekb/api/where`:它把 config / env / exec / 上次记忆 / 进程 cwd 与最终结论一起吐出来。
|
|
138
|
+
|
|
139
|
+
### 「注册」到底指什么
|
|
140
|
+
|
|
141
|
+
没有注册这一步也能用 —— 只要库落在工作区里的约定位置(见上),它就会出现在列表里。`kb_registry.json` 只是给**不在工作区里**的库用的(比如 F 盘另一个项目、共享笔记目录):
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{ "libraries": { "my-notes": { "path": "D:\\notes" } } }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
读取顺序:`config.libraries` → `config.registries` 指定的文件 → `<ws>/.nodekb/registry.json` → `<ws>/kb_registry.json` → 用户目录的全局注册表(`~/.dsh/nodekb/registry.json`)。注册表里登记的名字优先级高于自动发现的名字,路径重复的以先出现的为准。
|
|
134
148
|
|
|
135
149
|
## 语法
|
|
136
150
|
|
|
@@ -185,7 +199,7 @@ node test/parity.mjs # 与 Python 原实现逐条对拍(需要 python
|
|
|
185
199
|
| `ctx.inject([...], cb)` | `prompt.js` / `skill.js` | 动态注入不可用 → 段落/技能不注册 | 退回同步取值;仍失败只告警 |
|
|
186
200
|
| `ctx.systemPrompt.section({name,order,text})` | `lib/host/prompt.js` | 段落不出现 → Agent 不优先用它(工具仍在) | 失败被吞,`/api/prompt` 自证 |
|
|
187
201
|
| `ctx.skills.register({…})` | `lib/host/skill.js` | 写法规范不进技能目录 | 失败被吞,`/api/skill` 自证 |
|
|
188
|
-
| `exec.workspace / cwd / session` | `lib/host/runtime.js` | 定位不到工作区 → 默认库位置错 | 4 级回退:
|
|
202
|
+
| `exec.workspace / cwd / session` | `lib/host/runtime.js` | 定位不到工作区 → 默认库位置错 | 4 级回退:exec → config → env → 上次工具用过的 → 进程 cwd |
|
|
189
203
|
| `window.__ModuleLoader__.load({id,factory})` | `lib/client.js` | id 不匹配 → `bundle loaded without registering` | 自检断言 id === 包名 |
|
|
190
204
|
| `ctx.slots.inject` / `slots.register` | `lib/client.js` | 槽名或 props 改名 → 图标不出现 | 走 official `inject`,失败写 `/api/client-report` 并在面板内自述 |
|
|
191
205
|
|
package/README_EN.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -940,6 +940,11 @@
|
|
|
940
940
|
var libs = libsState[0]
|
|
941
941
|
var setLibs = libsState[1]
|
|
942
942
|
|
|
943
|
+
// 全局注册表里的库(不在当前工作区内),与 libs 分组显示
|
|
944
|
+
var otherState = useState([])
|
|
945
|
+
var otherLibs = otherState[0]
|
|
946
|
+
var setOtherLibs = otherState[1]
|
|
947
|
+
|
|
943
948
|
var kbState = useState('')
|
|
944
949
|
var kb = kbState[0]
|
|
945
950
|
var setKb = kbState[1]
|
|
@@ -1028,21 +1033,35 @@
|
|
|
1028
1033
|
})
|
|
1029
1034
|
}, [])
|
|
1030
1035
|
|
|
1031
|
-
//
|
|
1036
|
+
// 库列表:分两组
|
|
1037
|
+
// mine = 当前工作区里的库(/libs?scope=workspace,只含真的在这个目录下的)
|
|
1038
|
+
// other = 全局注册表里登记的库(路径不在本工作区内,且必须真的存在)
|
|
1039
|
+
// 「t(路径不存在)」这类失效注册项直接不进下拉。
|
|
1032
1040
|
var loadLibs = useCallback(function () {
|
|
1033
|
-
return get('/libs')
|
|
1034
|
-
.then(function (
|
|
1035
|
-
var
|
|
1036
|
-
|
|
1041
|
+
return Promise.all([get('/libs', { scope: 'workspace' }), get('/libs')])
|
|
1042
|
+
.then(function (rs) {
|
|
1043
|
+
var mine = (rs[0] && rs[0].libs) || []
|
|
1044
|
+
var all = (rs[1] && rs[1].libs) || []
|
|
1045
|
+
var seen = {}
|
|
1046
|
+
for (var i = 0; i < mine.length; i++) seen[mine[i].name + '|' + mine[i].path] = 1
|
|
1047
|
+
var other = []
|
|
1048
|
+
for (var j = 0; j < all.length; j++) {
|
|
1049
|
+
var l = all[j]
|
|
1050
|
+
if (!l.exists) continue
|
|
1051
|
+
if (seen[l.name + '|' + l.path]) continue
|
|
1052
|
+
other.push(l)
|
|
1053
|
+
}
|
|
1054
|
+
setLibs(mine)
|
|
1055
|
+
setOtherLibs(other)
|
|
1037
1056
|
var pick = null
|
|
1038
|
-
for (var
|
|
1039
|
-
if (
|
|
1057
|
+
for (var a = 0; a < mine.length; a++) {
|
|
1058
|
+
if (mine[a].exists) { pick = mine[a].name; break }
|
|
1040
1059
|
}
|
|
1041
1060
|
if (!pick) {
|
|
1042
|
-
for (var
|
|
1061
|
+
for (var b = 0; b < all.length; b++) { if (all[b].exists) { pick = all[b].name; break } }
|
|
1043
1062
|
}
|
|
1044
1063
|
setKb(pick || '')
|
|
1045
|
-
if (!
|
|
1064
|
+
if (!mine.length && !other.length) {
|
|
1046
1065
|
setErr('这个工作区里没找到 .nodekb 库。换一个目录(右上「目录」),或在项目里建 .nodekb/ 放 md 文件;放在 .dsh/skills/<技能>/.nodekb/ 也会被自动发现。')
|
|
1047
1066
|
} else {
|
|
1048
1067
|
setErr('')
|
|
@@ -1220,10 +1239,19 @@
|
|
|
1220
1239
|
onChange: function (e) { setKb(e.target.value); setSel('root'); setView('subtree') },
|
|
1221
1240
|
title: '切换知识库',
|
|
1222
1241
|
},
|
|
1223
|
-
libs.length
|
|
1224
|
-
?
|
|
1225
|
-
|
|
1226
|
-
|
|
1242
|
+
libs.length || otherLibs.length
|
|
1243
|
+
? [
|
|
1244
|
+
libs.length
|
|
1245
|
+
? h('optgroup', { key: 'ws', label: '当前工作区' }, libs.map(function (l) {
|
|
1246
|
+
return h('option', { key: 'w-' + l.name, value: l.name }, l.name + (l.exists ? '' : '(路径不存在)'))
|
|
1247
|
+
}))
|
|
1248
|
+
: null,
|
|
1249
|
+
otherLibs.length
|
|
1250
|
+
? h('optgroup', { key: 'reg', label: '全局注册' }, otherLibs.map(function (l) {
|
|
1251
|
+
return h('option', { key: 'r-' + l.name, value: l.name }, l.name + ' — ' + l.path)
|
|
1252
|
+
}))
|
|
1253
|
+
: null,
|
|
1254
|
+
]
|
|
1227
1255
|
: h('option', { value: '' }, '(无库)'),
|
|
1228
1256
|
),
|
|
1229
1257
|
tree && typeof tree.nodeCount === 'number' ? h('span', { className: 'nkb-muted' }, tree.nodeCount + ' 节点') : null,
|
|
@@ -1242,10 +1270,12 @@
|
|
|
1242
1270
|
style: { maxWidth: 220 },
|
|
1243
1271
|
},
|
|
1244
1272
|
h('option', { value: '' }, '(默认目录)'),
|
|
1245
|
-
wsList
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1273
|
+
wsList
|
|
1274
|
+
.filter(function (w) { return w.nodekb })
|
|
1275
|
+
.map(function (w) {
|
|
1276
|
+
return h('option', { key: w.id || w.path, value: w.path }, (w.title || w.path) + '(' + (w.libs || 0) + ' 个库)')
|
|
1277
|
+
}),
|
|
1278
|
+
WS && !wsList.some(function (w) { return w.path === WS && w.nodekb }) ? h('option', { key: 'manual', value: WS }, WS + '(当前)') : null,
|
|
1249
1279
|
),
|
|
1250
1280
|
h('button', { onClick: runSearch, disabled: busy }, '搜索'),
|
|
1251
1281
|
h('button', { onClick: runDiag, disabled: busy }, '诊断'),
|
|
@@ -1381,16 +1411,16 @@
|
|
|
1381
1411
|
'div',
|
|
1382
1412
|
{ className: 'nkb-diag nkb-err' },
|
|
1383
1413
|
h('div', null, err),
|
|
1384
|
-
wsList.length
|
|
1414
|
+
wsList.filter(function (w) { return w.nodekb }).length
|
|
1385
1415
|
? h(
|
|
1386
1416
|
'div',
|
|
1387
1417
|
{ style: { marginTop: 10 } },
|
|
1388
|
-
h('div', { className: 'nkb-muted' }, '
|
|
1389
|
-
wsList.map(function (w) {
|
|
1418
|
+
h('div', { className: 'nkb-muted' }, '选一个有 .nodekb 的 DSH 项目目录:'),
|
|
1419
|
+
wsList.filter(function (w) { return w.nodekb }).map(function (w) {
|
|
1390
1420
|
return h(
|
|
1391
1421
|
'div',
|
|
1392
1422
|
{ key: w.id || w.path, style: { marginTop: 6 } },
|
|
1393
|
-
h('button', { onClick: function () { useWs(w.path) } }, (w.title || w.path) +
|
|
1423
|
+
h('button', { onClick: function () { useWs(w.path) } }, (w.title || w.path) + ' · ' + (w.libs || 0) + ' 个库'),
|
|
1394
1424
|
h('span', { className: 'nkb-muted', style: { marginLeft: 8 } }, w.path),
|
|
1395
1425
|
)
|
|
1396
1426
|
}),
|
package/lib/engine/registry.js
CHANGED
|
@@ -219,8 +219,11 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
219
219
|
)
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/** 列出可用库(不解析内容,避免一次拖动整库)
|
|
223
|
-
|
|
222
|
+
/** 列出可用库(不解析内容,避免一次拖动整库)
|
|
223
|
+
* scope='workspace' 时只回「真的在当前工作区里」的库(发现的 + 注册表里路径落在工作区内的),
|
|
224
|
+
* 面板用它做「知识库选择只显示当前工作区的库」;全局注册表项不再混进来。
|
|
225
|
+
*/
|
|
226
|
+
export function listLibraries({ workspace, config, scope } = {}) {
|
|
224
227
|
const ws = path.resolve(workspace || process.cwd())
|
|
225
228
|
const registries = loadRegistries(
|
|
226
229
|
config && config.registries ? config.registries : defaultRegistryPaths(ws),
|
|
@@ -235,6 +238,8 @@ export function listLibraries({ workspace, config } = {}) {
|
|
|
235
238
|
for (const [name, entry] of registries) {
|
|
236
239
|
const nodekb = path.join(entry.path, '.nodekb')
|
|
237
240
|
if (seen.has(nodekb)) continue
|
|
241
|
+
// 工作区视图只保留「路径位于工作区内」的注册项,否则一个全局注册表会污染所有工作区
|
|
242
|
+
if (scope === 'workspace' && !isInside(entry.path, ws)) continue
|
|
238
243
|
rows.push({ name, path: entry.path, exists: isDir(nodekb), origin: entry.registry })
|
|
239
244
|
}
|
|
240
245
|
return rows
|
package/lib/host/routes.js
CHANGED
|
@@ -12,7 +12,7 @@ import { search, formatSearch } from '../engine/search.js'
|
|
|
12
12
|
import { diag } from '../engine/diag.js'
|
|
13
13
|
import { extract_refs } from '../engine/parse.js'
|
|
14
14
|
import { renameFileNode, formatRename } from '../engine/rename.js'
|
|
15
|
-
import { openLibrary, knownLibraries } from './runtime.js'
|
|
15
|
+
import { openLibrary, knownLibraries, whereInfo, workspaceOf } from './runtime.js'
|
|
16
16
|
import { promptState } from './prompt.js'
|
|
17
17
|
import { skillState } from './skill.js'
|
|
18
18
|
import { toolState } from './tools.js'
|
|
@@ -86,7 +86,15 @@ export function registerRoutes(ctx, config) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (url.pathname === `${PREFIX}/libs`) {
|
|
89
|
-
|
|
89
|
+
// scope=workspace:只要当前工作区里的库(面板的「知识库」下拉用它)
|
|
90
|
+
const scope = q.get('scope') === 'workspace' ? 'workspace' : undefined
|
|
91
|
+
send(res, 200, { workspace: workspaceOf(config, exec), libs: knownLibraries(config, exec, { scope }) })
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (url.pathname === `${PREFIX}/where`) {
|
|
96
|
+
// 诊断:工作区到底是怎么定下来的(面板 / 工具对不上时先看这个)
|
|
97
|
+
send(res, 200, whereInfo(config, exec))
|
|
90
98
|
return
|
|
91
99
|
}
|
|
92
100
|
|
package/lib/host/runtime.js
CHANGED
|
@@ -58,19 +58,34 @@ function signature(nodekb) {
|
|
|
58
58
|
return `${count}:${size}:${maxM}`
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
61
|
+
/** 取会话工作区:exec 上下文(面板显式 ws / 会话工作区) → 配置 → 环境变量 → 上次记住的 → 进程 cwd
|
|
62
|
+
* exec 放最前面:面板点了哪个工作区、Agent 在哪个会话里,都必须压过配置里的默认值。 */
|
|
62
63
|
export function workspaceOf(config, exec) {
|
|
64
|
+
const e = exec && typeof exec === 'object' ? exec : {}
|
|
65
|
+
const candidates = [e.workspace, e.cwd, e.session && e.session.workspace, e.session && e.session.cwd]
|
|
66
|
+
for (const c of candidates) if (typeof c === 'string' && c) return path.resolve(c)
|
|
63
67
|
const configured = config && typeof config.workspace === 'string' ? config.workspace : ''
|
|
64
68
|
if (configured) return path.resolve(configured)
|
|
65
69
|
const env = process.env.DSH_NODEKB_WORKSPACE
|
|
66
70
|
if (env) return path.resolve(env)
|
|
67
|
-
const e = exec && typeof exec === 'object' ? exec : {}
|
|
68
|
-
const candidates = [e.workspace, e.cwd, e.session && e.session.workspace, e.session && e.session.cwd]
|
|
69
|
-
for (const c of candidates) if (typeof c === 'string' && c) return path.resolve(c)
|
|
70
71
|
if (LAST_WORKSPACE) return LAST_WORKSPACE
|
|
71
72
|
return process.cwd()
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/** 诊断用:把「工作区是怎么定下来的」原样吐出来(面板/工具都对不上时靠它) */
|
|
76
|
+
export function whereInfo(config, exec) {
|
|
77
|
+
const e = exec && typeof exec === 'object' ? exec : {}
|
|
78
|
+
return {
|
|
79
|
+
configured: (config && config.workspace) || '',
|
|
80
|
+
env: process.env.DSH_NODEKB_WORKSPACE || '',
|
|
81
|
+
execWorkspace: (e.workspace && String(e.workspace)) || '',
|
|
82
|
+
execCwd: (e.cwd && String(e.cwd)) || '',
|
|
83
|
+
lastWorkspace: LAST_WORKSPACE || '',
|
|
84
|
+
processCwd: process.cwd(),
|
|
85
|
+
resolved: workspaceOf(config, exec),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
/**
|
|
75
90
|
* 打开一个库(带缓存)。
|
|
76
91
|
* @returns {{name:string, base:string, nodekb:string, nodes:Map, problems:string[], cached:boolean}}
|
|
@@ -93,8 +108,8 @@ export function openLibrary(config, exec, { kb, lib, remember = true } = {}) {
|
|
|
93
108
|
return { ...resolved, nodes, problems, cached: false }
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
export function knownLibraries(config, exec) {
|
|
97
|
-
return listLibraries({ workspace: workspaceOf(config, exec), config })
|
|
111
|
+
export function knownLibraries(config, exec, opts = {}) {
|
|
112
|
+
return listLibraries({ workspace: workspaceOf(config, exec), config, scope: opts.scope })
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
export function libraryHint(config, exec) {
|
package/package.json
CHANGED