project-node-memory 1.4.0 → 1.4.1
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/lib/engine/commands.js +1 -1
- package/lib/engine/defs.js +8 -4
- package/lib/engine/rename.js +38 -19
- package/lib/engine/render.js +8 -4
- package/lib/host/routes.js +1 -1
- package/lib/host/runtime.js +16 -2
- package/lib/host/tools.js +4 -1
- package/package.json +1 -1
package/lib/engine/commands.js
CHANGED
|
@@ -33,7 +33,7 @@ export function cmdRead(nodes, target, { node = false, raw = false, toc = false,
|
|
|
33
33
|
// 不是节点:再看是不是一个 tscl 定义 id(值的老家可能只是一句话)
|
|
34
34
|
const defsMap = defs || collectDefs(nodes).defs
|
|
35
35
|
const def = defsMap.get(target)
|
|
36
|
-
if (def) return { text: formatDefRead(def, refsToDef(nodes, target)), code: 0, def: target }
|
|
36
|
+
if (def) return { text: formatDefRead(def, refsToDef(nodes, target, def.ownerTid)), code: 0, def: target }
|
|
37
37
|
const cands = [...nodes.keys()]
|
|
38
38
|
.filter((t) => t.startsWith(target) || t.startsWith(target + ':'))
|
|
39
39
|
.sort()
|
package/lib/engine/defs.js
CHANGED
|
@@ -59,10 +59,13 @@ export function isMultiline(body) {
|
|
|
59
59
|
return String(body || '').replace(/\s+$/, '').includes('\n')
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** 谁在正文里引用了这个定义([[tscl="id"]],或指向它的旧语法裸引用)→ [{tid,title,source,line,legacy}]
|
|
63
|
-
|
|
62
|
+
/** 谁在正文里引用了这个定义([[tscl="id"]],或指向它的旧语法裸引用)→ [{tid,title,source,line,legacy}]
|
|
63
|
+
* `ownerTid` 传进来的话,定义所在节点自己不算「引用者」——定义就写在它里面,
|
|
64
|
+
* 再把它列成一处引用会让计数虚高。 */
|
|
65
|
+
export function refsToDef(nodes, id, ownerTid = '') {
|
|
64
66
|
const out = []
|
|
65
67
|
for (const node of nodes.values()) {
|
|
68
|
+
if (ownerTid && node.tid === ownerTid) continue
|
|
66
69
|
const content = String(node.content || '')
|
|
67
70
|
if (!content.includes(id)) continue
|
|
68
71
|
for (const ref of extractRefs(content)) {
|
|
@@ -82,13 +85,14 @@ export function refsToDef(nodes, id) {
|
|
|
82
85
|
|
|
83
86
|
/** 定义 → 人读文本(kb_read 按 tscl id 读、面板点宏时跳转用) */
|
|
84
87
|
export function formatDefRead(def, refs = []) {
|
|
85
|
-
const
|
|
88
|
+
const fileOf = (source) => String(source || '').split('#')[0].split(/[\\/]/).filter(Boolean).slice(-2).join('/')
|
|
89
|
+
const src = fileOf(def.source)
|
|
86
90
|
const lines = [`定义体 <tscl id="${def.id}"> · 定义在 ${def.ownerTid}${src ? `(${src})` : ''}`, '', String(def.body || '').replace(/\s+$/, '')]
|
|
87
91
|
if (def.count > 1) lines.push('', `重复定义 ${def.count} 处(必须只剩一处): ${def.owners.join('、')}`)
|
|
88
92
|
if (refs.length) {
|
|
89
93
|
lines.push('', `被引用 ${refs.length} 处:`)
|
|
90
94
|
for (const r of refs) {
|
|
91
|
-
const at =
|
|
95
|
+
const at = fileOf(r.source)
|
|
92
96
|
lines.push(`- ${r.tid}${r.title ? ` · ${r.title}` : ''}${at ? `(${at}#L${r.line})` : ''}${r.legacy ? ` ← 旧语法,建议 [[tscl="${def.id}"]]` : ''}`)
|
|
93
97
|
}
|
|
94
98
|
} else {
|
package/lib/engine/rename.js
CHANGED
|
@@ -15,10 +15,27 @@ import fs from 'node:fs'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { slugify } from './parse.js'
|
|
17
17
|
import { ROOT_TID } from './mdlib.js'
|
|
18
|
+
import { resolve_path } from './render.js'
|
|
18
19
|
|
|
19
20
|
const FENCE = '```'
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
+
/** 把引用体(`[[…]]` 中间那段)换成新 TID;不是这个 TID 的返回 null。 */
|
|
23
|
+
function rewriteInner(inner, oldTid, newTid) {
|
|
24
|
+
// 新语法:[[node="TID"]] / [[tscl="TID"]]
|
|
25
|
+
const quoted = /^(node|tscl)\s*=\s*"([^"]*)"$/.exec(inner)
|
|
26
|
+
if (quoted) {
|
|
27
|
+
const [, kind, id] = quoted
|
|
28
|
+
if (id === oldTid) return `[[${kind}="${newTid}"]]`
|
|
29
|
+
if (id.startsWith(oldTid + ':')) return `[[${kind}="${newTid}${id.slice(oldTid.length)}"]]`
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
// 旧语法:[[TID]] / [[TID:子节点]]
|
|
33
|
+
if (inner === oldTid) return `[[${newTid}]]`
|
|
34
|
+
if (inner.startsWith(oldTid + ':')) return `[[${newTid}${inner.slice(oldTid.length)}]]`
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 把一段文本里的 oldTid 引用改写成 newTid(三种写法都覆盖,含 `oldTid:…` 子节点引用)。 */
|
|
22
39
|
export function rewriteRefsInText(text, oldTid, newTid) {
|
|
23
40
|
const lines = String(text).split('\n')
|
|
24
41
|
const out = []
|
|
@@ -36,15 +53,10 @@ export function rewriteRefsInText(text, oldTid, newTid) {
|
|
|
36
53
|
}
|
|
37
54
|
out.push(
|
|
38
55
|
line.replace(/\[\[([^[\]]+)\]\]/g, (whole, inner) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (inner.startsWith(oldTid + ':')) {
|
|
44
|
-
count += 1
|
|
45
|
-
return `[[${newTid}${inner.slice(oldTid.length)}]]`
|
|
46
|
-
}
|
|
47
|
-
return whole
|
|
56
|
+
const next = rewriteInner(inner, oldTid, newTid)
|
|
57
|
+
if (next === null) return whole
|
|
58
|
+
count += 1
|
|
59
|
+
return next
|
|
48
60
|
}),
|
|
49
61
|
)
|
|
50
62
|
}
|
|
@@ -97,11 +109,22 @@ export function renameFileNode(lib, target, newName, opts = {}) {
|
|
|
97
109
|
|
|
98
110
|
let node = nodes.get(wanted)
|
|
99
111
|
if (!node) {
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
// 允许直接给相对路径(doc/03-x.md、lessons/004-…):走与 kb_read 相同的三级路径解析
|
|
113
|
+
let tid = null
|
|
114
|
+
try {
|
|
115
|
+
tid = resolve_path(nodes, wanted)
|
|
116
|
+
} catch {
|
|
117
|
+
tid = null
|
|
118
|
+
}
|
|
119
|
+
if (tid) node = nodes.get(tid)
|
|
120
|
+
}
|
|
121
|
+
if (!node) {
|
|
122
|
+
// source 存的是绝对路径,按「库根 + 相对路径」比较(大小写与分隔符都归一)
|
|
123
|
+
const rel = wanted.split(path.sep).join('/').replace(/^\.?\/+/, '')
|
|
124
|
+
const want = path.normalize(path.join(lib.nodekb, rel))
|
|
125
|
+
node = [...nodes.values()].find((n) => n.kind === 'file' && absOf(lib, n) === want)
|
|
103
126
|
}
|
|
104
|
-
if (!node) throw new Error(`未找到节点: ${wanted}(可先 kb_read --toc 看 TID
|
|
127
|
+
if (!node) throw new Error(`未找到节点: ${wanted}(可先 kb_read --toc 看 TID 清单;也可以给相对路径 doc/03-x.md)`)
|
|
105
128
|
if (node.kind !== 'file') {
|
|
106
129
|
throw new Error(`只能重命名文件节点;${node.tid} 是 ${node.kind} 节点(目录改名请直接改文件夹)`)
|
|
107
130
|
}
|
|
@@ -145,7 +168,7 @@ export function renameFileNode(lib, target, newName, opts = {}) {
|
|
|
145
168
|
report.skipped.push({ path: relOf(lib, file), why: `读失败: ${e.message}` })
|
|
146
169
|
continue
|
|
147
170
|
}
|
|
148
|
-
if (!text.includes(
|
|
171
|
+
if (!text.includes(node.tid)) continue
|
|
149
172
|
const { text: next, count } = rewriteRefsInText(text, node.tid, newTid)
|
|
150
173
|
if (!count) continue
|
|
151
174
|
edits.push({ file, st1, next, count })
|
|
@@ -177,10 +200,6 @@ export function renameFileNode(lib, target, newName, opts = {}) {
|
|
|
177
200
|
return report
|
|
178
201
|
}
|
|
179
202
|
|
|
180
|
-
function oldRefPrefix(tid) {
|
|
181
|
-
return `[[${tid}`
|
|
182
|
-
}
|
|
183
|
-
|
|
184
203
|
/** 人读的回执 */
|
|
185
204
|
export function formatRename(report) {
|
|
186
205
|
const lines = []
|
package/lib/engine/render.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { anyRefRegex, strip_tags } from './parse.js'
|
|
17
|
+
import { ROOT_TID } from './mdlib.js'
|
|
17
18
|
|
|
18
19
|
const MAX_EMBED = 64 // 单次渲染最多展开的宏个数(防病态文件自引用爆炸)
|
|
19
20
|
|
|
@@ -169,15 +170,18 @@ export function node_info(nodes, tid) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
/** 路径解析(三级):精确 TID → 段边界前缀唯一 →
|
|
173
|
+
/** 路径解析(三级):精确 TID → 段边界前缀唯一 → 裸前缀(模糊)唯一。
|
|
174
|
+
* 空串与 "." 都当库根(README 一直这么写,代码以前只认空串)。 */
|
|
173
175
|
export function resolve_path(nodes, p) {
|
|
174
|
-
|
|
176
|
+
const raw = String(p ?? '').trim()
|
|
177
|
+
const target = raw === '' || raw === '.' || raw === './' ? ROOT_TID : raw
|
|
178
|
+
if (nodes.has(target)) return target
|
|
175
179
|
const keys = [...nodes.keys()]
|
|
176
|
-
let hits = keys.filter((t) => t ===
|
|
180
|
+
let hits = keys.filter((t) => t === target || t.startsWith(target + ':')).sort()
|
|
177
181
|
if (hits.length === 1) return hits[0]
|
|
178
182
|
if (!hits.length) {
|
|
179
183
|
// 段边界无命中 → 裸前缀模糊(fact:mod → fact:modid / fact:modid-places / fact:modid-trap)
|
|
180
|
-
hits = keys.filter((t) => t.startsWith(
|
|
184
|
+
hits = keys.filter((t) => t.startsWith(target)).sort()
|
|
181
185
|
if (hits.length === 1) return hits[0]
|
|
182
186
|
}
|
|
183
187
|
return null
|
package/lib/host/routes.js
CHANGED
|
@@ -197,7 +197,7 @@ export function registerRoutes(ctx, config) {
|
|
|
197
197
|
send(res, 404, { error: `未找到定义: ${id}`, kb: lib.name })
|
|
198
198
|
return
|
|
199
199
|
}
|
|
200
|
-
const refs = refsToDef(lib.nodes, id)
|
|
200
|
+
const refs = refsToDef(lib.nodes, id, def.ownerTid)
|
|
201
201
|
send(res, 200, {
|
|
202
202
|
kb: lib.name,
|
|
203
203
|
id,
|
package/lib/host/runtime.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from 'node:fs'
|
|
14
14
|
import path from 'node:path'
|
|
15
|
+
import crypto from 'node:crypto'
|
|
15
16
|
import { parse_library } from '../engine/mdlib.js'
|
|
16
17
|
import { collectDefs } from '../engine/defs.js'
|
|
17
18
|
import { resolveLibrary, listLibraries } from '../engine/registry.js'
|
|
@@ -90,6 +91,7 @@ function signature(nodekb) {
|
|
|
90
91
|
let count = 0
|
|
91
92
|
let maxM = 0
|
|
92
93
|
let size = 0
|
|
94
|
+
const names = []
|
|
93
95
|
const walk = (d) => {
|
|
94
96
|
let entries
|
|
95
97
|
try {
|
|
@@ -108,6 +110,9 @@ function signature(nodekb) {
|
|
|
108
110
|
count += 1
|
|
109
111
|
size += st.size
|
|
110
112
|
if (st.mtimeMs > maxM) maxM = st.mtimeMs
|
|
113
|
+
// 文件名也算签名:改名/移动既不改 mtime 也不改大小,
|
|
114
|
+
// 只按内容比签名会让「改名后的旧索引」一直命中缓存(库看着正常、实际裂开)。
|
|
115
|
+
names.push(path.relative(nodekb, p).split(path.sep).join('/'))
|
|
111
116
|
} catch {
|
|
112
117
|
/* 竞态删除:忽略 */
|
|
113
118
|
}
|
|
@@ -115,7 +120,14 @@ function signature(nodekb) {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
walk(nodekb)
|
|
118
|
-
|
|
123
|
+
names.sort()
|
|
124
|
+
const digest = crypto.createHash('sha1').update(names.join('\n')).digest('hex').slice(0, 12)
|
|
125
|
+
return `${count}:${size}:${maxM}:${digest}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 让某个库的缓存立刻失效(改名/移动之后必须调,别指望下一次签名比对) */
|
|
129
|
+
export function forgetLibrary(nodekb) {
|
|
130
|
+
if (nodekb) CACHE.delete(path.resolve(nodekb))
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
/** 判定工作区,并说明它是怎么定下来的。
|
|
@@ -128,11 +140,13 @@ export function workspaceSource(config, exec) {
|
|
|
128
140
|
if (cwd) return { path: cwd, from: 'exec.cwd' }
|
|
129
141
|
const scwd = sessionCwd(e)
|
|
130
142
|
if (scwd) return { path: scwd, from: 'session.header.cwd' }
|
|
143
|
+
// 面板没有会话:优先「最近一次工具用过的目录」,配置只当最后的默认值。
|
|
144
|
+
// 这样配置里留着的旧路径不会把面板钉在某个老项目上。
|
|
145
|
+
if (!e && LAST_WORKSPACE) return { path: LAST_WORKSPACE, from: 'last-workspace(面板)' }
|
|
131
146
|
const configured = config && typeof config.workspace === 'string' ? config.workspace : ''
|
|
132
147
|
if (configured) return { path: path.resolve(configured), from: 'config.workspace' }
|
|
133
148
|
const env = process.env.DSH_NODEKB_WORKSPACE
|
|
134
149
|
if (env) return { path: path.resolve(env), from: 'env.DSH_NODEKB_WORKSPACE' }
|
|
135
|
-
if (!e && LAST_WORKSPACE) return { path: LAST_WORKSPACE, from: 'last-workspace(面板)' }
|
|
136
150
|
return { path: process.cwd(), from: 'process.cwd' }
|
|
137
151
|
}
|
|
138
152
|
|
package/lib/host/tools.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
workspaceRoots,
|
|
29
29
|
sessionIdOf,
|
|
30
30
|
bindServices,
|
|
31
|
+
forgetLibrary,
|
|
31
32
|
} from './runtime.js'
|
|
32
33
|
|
|
33
34
|
const DEFAULT_MAXLEN = 50000
|
|
@@ -167,7 +168,7 @@ export function buildTools(ctx, config) {
|
|
|
167
168
|
kb: lib.name,
|
|
168
169
|
mode,
|
|
169
170
|
chars: String(def.body || '').length,
|
|
170
|
-
text: warnPrefix(lib) + formatDefRead(def, refsToDef(lib.nodes, target)),
|
|
171
|
+
text: warnPrefix(lib) + formatDefRead(def, refsToDef(lib.nodes, target, def.ownerTid)),
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
const cands = [...lib.nodes.keys()].filter((t) => t.startsWith(target) || t.startsWith(target + ':')).sort().slice(0, 10)
|
|
@@ -335,6 +336,8 @@ export function buildTools(ctx, config) {
|
|
|
335
336
|
const report = renameFileNode(lib, String(args.target ?? ''), String(args.new_name ?? ''), {
|
|
336
337
|
dryRun: Boolean(args.dry_run),
|
|
337
338
|
})
|
|
339
|
+
// 改名只动文件名:缓存签名里的 mtime/size 都不变,不主动失效就会继续用旧索引
|
|
340
|
+
if (!report.dry_run && report.renamed) forgetLibrary(lib.nodekb)
|
|
338
341
|
return { ...report, kb: lib.name, text: warnPrefix(lib) + formatRename(report) }
|
|
339
342
|
},
|
|
340
343
|
})
|
package/package.json
CHANGED