project-node-memory 1.0.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # project-node-memory
2
2
 
3
- > 中文 · [English](README_EN.md)
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` 可覆盖「工作区」,默认取会话工作区、再退回进程 cwd。
131
+ `config.workspace` 可覆盖「工作区」;优先级是 **exec 上下文(面板点的目录 / 会话工作区)→ config.workspace → 环境变量 `DSH_NODEKB_WORKSPACE` → 最近一次工具用过的目录 → 进程 cwd**。所以在面板里换目录总能生效,配置只当默认值。
132
132
 
133
- 面板拿不到会话上下文,所以它默认跟随**最近一次 `kb_*` 工具调用所在的工作区**(Agent 读过哪个项目,面板就显示哪个项目);顶栏的目录下拉里直接列出 DSH 用过的项目目录(标了各自发现几个库),点右上「目录」也可以手填路径。选择存在浏览器 localStorage,只影响面板,不影响工具。
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 级回退:config → env → exec → 上次工具用过的 → 进程 cwd |
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
@@ -1,6 +1,6 @@
1
1
  # project-node-memory
2
2
 
3
- > English · [中文](README.md)
3
+ > [简体中文](README.md) · English
4
4
 
5
5
  A DSH plugin that treats **a directory of Markdown as a database**: nine `kb_*` tools for the agent, and a center-column panel for humans (sidebar entry: 「项目节点知识库」).
6
6
 
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 (r) {
1035
- var rows = r.libs || []
1036
- setLibs(rows)
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 i = 0; i < rows.length; i++) {
1039
- if (rows[i].origin === 'workspace' && rows[i].exists) { pick = rows[i].name; break }
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 j = 0; j < rows.length; j++) { if (rows[j].exists) { pick = rows[j].name; break } }
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 (!rows.length) {
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
- ? libs.map(function (l) {
1225
- return h('option', { key: l.name, value: l.name }, l.name + (l.exists ? '' : '(路径不存在)'))
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.map(function (w) {
1246
- return h('option', { key: w.id || w.path, value: w.path }, (w.title || w.path) + (w.nodekb ? '' : '(无 .nodekb)'))
1247
- }),
1248
- WS && !wsList.some(function (w) { return w.path === WS }) ? h('option', { key: 'manual', value: WS }, WS) : null,
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' }, '选一个 DSH 项目目录:'),
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) + (w.nodekb ? ' · 有 .nodekb' : ' · 无 .nodekb')),
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
  }),
@@ -219,8 +219,11 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
219
219
  )
220
220
  }
221
221
 
222
- /** 列出可用库(不解析内容,避免一次拖动整库) */
223
- export function listLibraries({ workspace, config } = {}) {
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
@@ -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
- send(res, 200, { libs: knownLibraries(config, exec) })
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
 
@@ -58,19 +58,34 @@ function signature(nodekb) {
58
58
  return `${count}:${size}:${maxM}`
59
59
  }
60
60
 
61
- /** 取会话工作区:配置优先 → exec 上下文 → 进程 cwd */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-node-memory",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Project node knowledge base for DSH agents: markdown-as-database, stable TIDs, transclusion projection, structure self-check.",
5
5
  "type": "module",
6
6
  "license": "MIT",