project-node-memory 0.0.0-stage → 1.0.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/client.js ADDED
@@ -0,0 +1,1621 @@
1
+ /**
2
+ * project-node-memory —— 浏览器侧入口(非 ESM;由 DSH 模块加载器执行)。
3
+ *
4
+ * 结构:
5
+ * 1. 调 window.__ModuleLoader__.load({ id, factory })
6
+ * id 必须是 package.json 的 name(写成 cordis 行 id 会报 loaded without registering)
7
+ * 2. factory(require) 拿到 React,建出 { apply, inject }
8
+ * 3. apply(ctx) 注册两个槽位:
9
+ * - sidebar.panellist id=nodekb 左栏齿轮里的图标
10
+ * - main key=nodekb 图标点开后的中栏面板
11
+ *
12
+ * 面板数据全部来自宿主侧路由 /nodekb/api/*(读写都是 md,面板只读)。
13
+ * 刻意不用 JSX / TS:工厂体没有编译步骤,写 JSX 会在页面加载时直接语法错误。
14
+ */
15
+ ;(function () {
16
+ var g = typeof window !== 'undefined' ? window : globalThis
17
+ var PKG_ID = 'project-node-memory'
18
+ var SIDEBAR_LABEL = '项目节点知识库'
19
+ var loader = g && g.__ModuleLoader__
20
+ if (!loader || typeof loader.load !== 'function') {
21
+ throw new Error(PKG_ID + ' client: 缺少 window.__ModuleLoader__(本文件只能由 DSH 模块加载器执行)')
22
+ }
23
+ loader.load({ id: PKG_ID, factory: makePlugin })
24
+
25
+ function makePlugin(require) {
26
+ var React = null
27
+ try {
28
+ React = require('react')
29
+ } catch (e) {
30
+ /* 加载器没给 react 时退回全局 */
31
+ }
32
+ if (!React) React = g.React
33
+ if (!React) throw new Error('project-node-memory client: 取不到 react')
34
+
35
+ var h = React.createElement
36
+ var useState = React.useState
37
+ var useEffect = React.useEffect
38
+ var useMemo = React.useMemo
39
+ var useCallback = React.useCallback
40
+ var useRef = React.useRef
41
+
42
+ var API = '/nodekb/api'
43
+ var WS_KEY = 'project-node-memory.ws'
44
+ var WS_KEY_OLD = 'dsh-nodekb.ws'
45
+
46
+ // 客户端自证:把"加载到哪一步"报给宿主(GET /nodekb/api/client-report 可读),
47
+ // 这样排查"图标不见了"不用人肉开 DevTools。
48
+ function report(obj) {
49
+ try {
50
+ // 每条报告都带上「这个客户端构建是谁」,用来确认热载后跑的是哪一版
51
+ var payload = { id: PKG_ID, label: SIDEBAR_LABEL }
52
+ for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) payload[k] = obj[k]
53
+ fetch(API + '/client-report', {
54
+ method: 'POST',
55
+ headers: { 'content-type': 'application/json' },
56
+ body: JSON.stringify(payload),
57
+ }).catch(function () { /* 报告失败无所谓 */ })
58
+ } catch (e) {
59
+ /* 报告失败无所谓 */
60
+ }
61
+ }
62
+
63
+ report({ phase: 'factory', hasReact: !!React })
64
+
65
+ // 面板拿不到会话上下文,所以工作区由人指定一次、记在 localStorage
66
+ function readWs() {
67
+ try {
68
+ if (!g.localStorage) return ''
69
+ // 迁移:改名前的旧键还能读到
70
+ return g.localStorage.getItem(WS_KEY) || g.localStorage.getItem(WS_KEY_OLD) || ''
71
+ } catch (e) {
72
+ return ''
73
+ }
74
+ }
75
+ var WS = readWs()
76
+
77
+ function writeWs(v) {
78
+ WS = v || ''
79
+ try {
80
+ if (g.localStorage) {
81
+ if (WS) g.localStorage.setItem(WS_KEY, WS)
82
+ else g.localStorage.removeItem(WS_KEY)
83
+ }
84
+ } catch (e) {
85
+ /* 隐私模式:只在本页生效 */
86
+ }
87
+ }
88
+
89
+ // ── 数据层 ────────────────────────────────────────────────────────────
90
+ function qs(params) {
91
+ var parts = []
92
+ for (var k in params) {
93
+ var v = params[k]
94
+ if (v === undefined || v === null || v === '') continue
95
+ parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v)))
96
+ }
97
+ return parts.length ? '?' + parts.join('&') : ''
98
+ }
99
+
100
+ function get(path, params) {
101
+ var p = {}
102
+ for (var k in params || {}) p[k] = params[k]
103
+ if (WS && p.ws === undefined) p.ws = WS
104
+ return fetch(API + path + qs(p), { headers: { accept: 'application/json' } }).then(function (r) {
105
+ return r.json().then(function (body) {
106
+ if (!r.ok) throw new Error((body && body.error) || 'HTTP ' + r.status)
107
+ return body
108
+ })
109
+ })
110
+ }
111
+
112
+ // ── 样式(只注入一次)──────────────────────────────────────────────────
113
+ function ensureStyle() {
114
+ if (typeof document === 'undefined') return
115
+ if (document.getElementById('project-node-memory-style')) return
116
+ var css = [
117
+ '.nkb-root{display:flex;flex-direction:column;height:100%;min-height:0;font-size:13px;color:inherit}',
118
+ '.nkb-head{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid rgba(128,128,128,.25);flex:0 0 auto}',
119
+ '.nkb-title{font-weight:600;letter-spacing:.02em}',
120
+ '.nkb-head select,.nkb-head input,.nkb-head button{font:inherit;color:inherit;background:transparent;border:1px solid rgba(128,128,128,.35);border-radius:6px;padding:3px 7px}',
121
+ '.nkb-head input{min-width:180px}',
122
+ '.nkb-head button{cursor:pointer}',
123
+ '.nkb-head button:hover{background:rgba(128,128,128,.14)}',
124
+ '.nkb-body{display:flex;flex:1 1 auto;min-height:0}',
125
+ '.nkb-left{width:272px;flex:0 0 272px;border-right:1px solid rgba(128,128,128,.25);display:flex;flex-direction:column;min-height:0}',
126
+ '.nkb-tree{flex:1 1 auto;overflow:auto;padding:6px 4px;min-height:0}',
127
+ '.nkb-settings{flex:0 0 auto;border-top:1px solid rgba(128,128,128,.22);padding:8px 10px;font-size:12px;max-height:46%;overflow:auto}',
128
+ '.nkb-settings .lbl{opacity:.55;margin:8px 0 3px}',
129
+ '.nkb-settings .lbl:first-child{margin-top:0}',
130
+ '.nkb-chip{display:inline-block;font-size:11px;border:1px solid rgba(128,128,128,.4);border-radius:10px;padding:1px 8px;margin:0 4px 4px 0;cursor:pointer;opacity:.75;user-select:none}',
131
+ '.nkb-chip.on{background:rgba(80,140,255,.18);border-color:rgba(80,140,255,.6);opacity:1}',
132
+ '.nkb-main{flex:1 1 auto;display:flex;flex-direction:column;min-width:0;min-height:0;position:relative;overflow:hidden}',
133
+ '.nkb-main svg{display:block;width:100%;height:100%;cursor:grab;touch-action:none}',
134
+ '.nkb-main svg.drag{cursor:grabbing}',
135
+ '.nkb-gbar{position:absolute;right:10px;bottom:10px;display:flex;gap:4px;background:rgba(20,20,20,.55);border-radius:8px;padding:3px;backdrop-filter:blur(4px)}',
136
+ '.nkb-gbar button{font:inherit;font-size:12px;color:inherit;background:transparent;border:0;border-radius:6px;padding:2px 8px;cursor:pointer;opacity:.8}',
137
+ '.nkb-gbar button:hover{background:rgba(128,128,128,.2);opacity:1}',
138
+ '.nkb-node rect{fill:rgba(128,128,128,.10);stroke-width:1.3;rx:6}',
139
+ '.nkb-node:hover rect{fill:rgba(128,128,128,.20)}',
140
+ '.nkb-node.sel rect{fill:rgba(80,140,255,.22);stroke:#5b9dff;stroke-width:2}',
141
+ '.nkb-node text{fill:currentColor;font-size:11.5px;pointer-events:none}',
142
+ '.nkb-node.folder rect{stroke-dasharray:3 2}',
143
+ '.nkb-edge{stroke:rgba(128,128,128,.45);stroke-width:1;fill:none}',
144
+ '.nkb-edge.on{stroke:#5b9dff;stroke-width:2}',
145
+ '.nkb-right{width:460px;flex:0 0 460px;min-width:320px;max-width:52vw;border-left:1px solid rgba(128,128,128,.25);display:flex;flex-direction:column;min-height:0}',
146
+ '.nkb-content{flex:1 1 auto;display:flex;flex-direction:column;min-height:0;overflow:hidden}',
147
+ '.nkb-props{flex:0 0 auto;max-height:40%;overflow:auto;border-top:1px solid rgba(128,128,128,.25);padding:8px 12px;font-size:12px;line-height:1.65}',
148
+ '.nkb-meta{width:280px;flex:0 0 280px;overflow:auto;border-left:1px solid rgba(128,128,128,.25);padding:10px 12px;font-size:12px;line-height:1.7}',
149
+ '.nkb-tabs{display:flex;gap:4px;padding:6px 10px;border-bottom:1px solid rgba(128,128,128,.18);flex:0 0 auto}',
150
+ '.nkb-tabs button{font:inherit;font-size:12px;color:inherit;background:transparent;border:1px solid transparent;border-radius:6px;padding:2px 8px;cursor:pointer;opacity:.7}',
151
+ '.nkb-tabs button.on{background:rgba(128,128,128,.18);opacity:1}',
152
+ '.nkb-text{flex:1 1 auto;overflow:auto;margin:0;padding:12px 14px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;line-height:1.65}',
153
+ '.nkb-md{flex:1 1 auto;min-height:0;min-width:0;overflow:auto;padding:10px 16px 24px;font-size:13px;line-height:1.72;word-break:break-word}',
154
+ '.nkb-md h1,.nkb-md h2,.nkb-md h3,.nkb-md h4,.nkb-md h5,.nkb-md h6{margin:14px 0 6px;line-height:1.35;font-weight:650}',
155
+ '.nkb-md h1{font-size:19px;border-bottom:1px solid rgba(128,128,128,.25);padding-bottom:4px}',
156
+ '.nkb-md h2{font-size:16.5px;border-bottom:1px solid rgba(128,128,128,.16);padding-bottom:3px}',
157
+ '.nkb-md h3{font-size:14.5px}.nkb-md h4,.nkb-md h5,.nkb-md h6{font-size:13px}',
158
+ '.nkb-md p{margin:6px 0;white-space:pre-wrap}',
159
+ '.nkb-md ul,.nkb-md ol{margin:6px 0 6px 0;padding-left:22px}',
160
+ '.nkb-md li{margin:2px 0;white-space:pre-wrap}',
161
+ '.nkb-md blockquote{margin:8px 0;padding:2px 12px;border-left:3px solid rgba(128,128,128,.4);opacity:.85}',
162
+ '.nkb-md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;background:rgba(128,128,128,.16);border-radius:4px;padding:1px 4px}',
163
+ '.nkb-md pre{margin:8px 0;padding:10px 12px;background:rgba(128,128,128,.12);border-radius:8px;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.6}',
164
+ '.nkb-md pre code{background:none;padding:0;font-size:12px}',
165
+ '.nkb-md a{color:#5b9dff;text-decoration:none}.nkb-md a:hover{text-decoration:underline}',
166
+ '.nkb-md hr{border:none;border-top:1px solid rgba(128,128,128,.3);margin:14px 0}',
167
+ '.nkb-md table{border-collapse:collapse;margin:0;font-size:12.5px}',
168
+ '.nkb-tablewrap{overflow-x:auto;max-width:100%;margin:8px 0}',
169
+ '.nkb-cut{flex:0 0 auto;font-size:11.5px;opacity:.75;padding:5px 14px;border-bottom:1px solid rgba(128,128,128,.18);background:rgba(180,140,40,.10)}',
170
+ '.nkb-md th,.nkb-md td{border:1px solid rgba(128,128,128,.3);padding:3px 8px;text-align:left;vertical-align:top}',
171
+ '.nkb-md th{background:rgba(128,128,128,.12);font-weight:600}',
172
+ '.nkb-md math{font-size:1.06em;font-family:"Cambria Math","Latin Modern Math",Symbol,serif}',
173
+ '.nkb-md .nkb-math{white-space:normal;display:inline-block;vertical-align:middle;max-width:100%;overflow-x:auto;overflow-y:hidden}',
174
+ '.nkb-md .nkb-math-block{display:block;text-align:center;margin:10px 0}',
175
+ '.nkb-row{display:flex;align-items:center;gap:2px;border-radius:6px;cursor:pointer;padding:1px 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
176
+ '.nkb-row:hover{background:rgba(128,128,128,.14)}',
177
+ '.nkb-row.on{background:rgba(80,140,255,.20)}',
178
+ '.nkb-row .t{opacity:.85;overflow:hidden;text-overflow:ellipsis}',
179
+ '.nkb-row .n{opacity:.45;font-size:11px;margin-left:6px}',
180
+ '.nkb-caret{width:12px;flex:0 0 12px;opacity:.55;text-align:center;user-select:none}',
181
+ '.nkb-tag{font-size:10.5px;opacity:.6;border:1px solid rgba(128,128,128,.35);border-radius:4px;padding:0 4px;margin-left:6px}',
182
+ '.nkb-hit{padding:6px 6px;border-radius:6px;cursor:pointer;border-bottom:1px solid rgba(128,128,128,.12)}',
183
+ '.nkb-hit:hover{background:rgba(128,128,128,.12)}',
184
+ '.nkb-hit .tt{font-weight:600}',
185
+ '.nkb-hit .sn{opacity:.7;white-space:pre-wrap;margin-top:2px;font-size:11.5px;line-height:1.5;max-height:5.2em;overflow:hidden}',
186
+ '.nkb-muted{opacity:.55}',
187
+ '.nkb-err{color:#d9534f;white-space:pre-wrap}',
188
+ '.nkb-link{color:#3b82f6;cursor:pointer;text-decoration:underline dotted}',
189
+ '.nkb-kv{display:flex;gap:6px}',
190
+ '.nkb-kv .k{opacity:.5;flex:0 0 58px}',
191
+ '.nkb-kv .v{word-break:break-all}',
192
+ '.nkb-sec{margin:10px 0 2px;font-weight:600;opacity:.7}',
193
+ '.nkb-diag{padding:10px 14px;overflow:auto;flex:1 1 auto;white-space:pre-wrap;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}'
194
+ ].join('\n')
195
+ var el = document.createElement('style')
196
+ el.id = 'project-node-memory-style'
197
+ el.textContent = css
198
+ document.head.appendChild(el)
199
+ }
200
+
201
+ // ── 小组件 ────────────────────────────────────────────────────────────
202
+ /** 把 [[TID]] 变成可点的链接 */
203
+ function Rich(props) {
204
+ var text = props.text || ''
205
+ var parts = useMemo(function () {
206
+ var out = []
207
+ var re = /\[\[([a-zA-Z0-9\u4e00-\u9fff:._-]+)\]\]/g
208
+ var last = 0
209
+ var m
210
+ while ((m = re.exec(text))) {
211
+ if (m.index > last) out.push({ t: text.slice(last, m.index) })
212
+ out.push({ t: m[0], tid: m[1] })
213
+ last = m.index + m[0].length
214
+ }
215
+ if (last < text.length) out.push({ t: text.slice(last) })
216
+ return out
217
+ }, [text])
218
+ return h(
219
+ 'pre',
220
+ { className: 'nkb-text' },
221
+ parts.map(function (p, i) {
222
+ if (!p.tid) return p.t
223
+ return h(
224
+ 'span',
225
+ { key: i, className: 'nkb-link', onClick: function () { props.onOpen && props.onOpen(p.tid) } },
226
+ p.t,
227
+ )
228
+ }),
229
+ )
230
+ }
231
+
232
+ // ── 类型颜色:同一类型恒定取色 ─────────────────────────────────────────
233
+ var TYPE_HUE = { root: 210, folder: 38, file: 96, doc: 205, facts: 150, lessons: 32, todo: 8, handover: 282, index: 190, library: 320, section: 250 }
234
+ function typeColor(type) {
235
+ var s = String(type || '')
236
+ if (!s) return 'rgba(128,128,128,.55)'
237
+ var hue = TYPE_HUE[s]
238
+ if (hue === undefined) {
239
+ var acc = 0
240
+ for (var i = 0; i < s.length; i++) acc = (acc * 31 + s.charCodeAt(i)) % 360
241
+ hue = acc
242
+ }
243
+ return 'hsl(' + hue + ' 62% 58%)'
244
+ }
245
+
246
+ // ── LaTeX 子集 → MathML(浏览器原生排版,不引 KaTeX、不带字体)──────────
247
+ var TEX_GREEK = 'alpha beta gamma delta epsilon varepsilon zeta eta theta vartheta iota kappa lambda mu nu xi omicron pi varpi rho varrho sigma varsigma tau upsilon phi varphi chi psi omega Gamma Delta Theta Lambda Xi Pi Sigma Upsilon Phi Psi Omega'.split(' ')
248
+ var TEX_SYM = {
249
+ alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ', epsilon: 'ϵ', varepsilon: 'ε', zeta: 'ζ',
250
+ eta: 'η', theta: 'θ', vartheta: 'ϑ', iota: 'ι', kappa: 'κ', lambda: 'λ', mu: 'μ', nu: 'ν',
251
+ xi: 'ξ', omicron: 'ο', pi: 'π', varpi: 'ϖ', rho: 'ρ', varrho: 'ϱ', sigma: 'σ', varsigma: 'ς',
252
+ tau: 'τ', upsilon: 'υ', phi: 'ϕ', varphi: 'φ', chi: 'χ', psi: 'ψ', omega: 'ω',
253
+ Gamma: 'Γ', Delta: 'Δ', Theta: 'Θ', Lambda: 'Λ', Xi: 'Ξ', Pi: 'Π', Sigma: 'Σ',
254
+ Upsilon: 'Υ', Phi: 'Φ', Psi: 'Ψ', Omega: 'Ω',
255
+ times: '×', cdot: '⋅', pm: '±', mp: '∓', div: '÷', ast: '∗', star: '⋆', circ: '∘', bullet: '∙',
256
+ le: '≤', leq: '≤', ge: '≥', geq: '≥', ne: '≠', neq: '≠', approx: '≈', sim: '∼', simeq: '≃', cong: '≅',
257
+ equiv: '≡', propto: '∝', ll: '≪', gg: '≫', infty: '∞', partial: '∂', nabla: '∇',
258
+ sum: '∑', prod: '∏', coprod: '∐', int: '∫', oint: '∮', iint: '∬', iiint: '∭',
259
+ to: '→', rightarrow: '→', leftarrow: '←', gets: '←', Rightarrow: '⇒', Leftarrow: '⇐',
260
+ leftrightarrow: '↔', Leftrightarrow: '⇔', iff: '⇔', implies: '⇒', mapsto: '↦',
261
+ forall: '∀', exists: '∃', nexists: '∄', in: '∈', notin: '∉', ni: '∋',
262
+ subset: '⊂', subseteq: '⊆', supset: '⊃', supseteq: '⊇', cup: '∪', cap: '∩', setminus: '∖',
263
+ emptyset: '∅', varnothing: '∅', neg: '¬', lnot: '¬', land: '∧', wedge: '∧', lor: '∨', vee: '∨',
264
+ angle: '∠', perp: '⊥', parallel: '∥', cdots: '⋯', ldots: '…', dots: '…', vdots: '⋮', ddots: '⋱',
265
+ degree: '°', prime: '′', therefore: '∴', because: '∵', hbar: 'ℏ', ell: 'ℓ',
266
+ langle: '⟨', rangle: '⟩', lvert: '|', rvert: '|', vert: '|', Vert: '‖', backslash: '∖',
267
+ }
268
+ var TEX_LETTERLIKE = ['partial', 'nabla', 'ell', 'hbar', 'aleph', 'Re', 'Im']
269
+ var TEX_FUNCS = ['arcsin', 'arccos', 'arctan', 'sinh', 'cosh', 'tanh', 'coth', 'sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'log', 'ln', 'lg', 'exp', 'max', 'min', 'sup', 'inf', 'lim', 'det', 'dim', 'ker', 'deg', 'gcd', 'Pr', 'arg', 'mod', 'bmod']
270
+
271
+ function texEscape(s) {
272
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
273
+ }
274
+
275
+ function texToMathML(src, display) {
276
+ var s = String(src == null ? '' : src)
277
+ var i = 0
278
+ var budget = 40000
279
+ function startsWith(t) { return s.slice(i, i + t.length) === t }
280
+ function skipWs() { while (i < s.length && /\s/.test(s[i])) i++ }
281
+ function readName() {
282
+ if (/[a-zA-Z]/.test(s[i] || '')) { var a = i; while (/[a-zA-Z]/.test(s[i] || '')) i++; return s.slice(a, i) }
283
+ var ch = s[i] || ''; i++; return ch
284
+ }
285
+ function rawGroupText() {
286
+ skipWs()
287
+ if (s[i] !== '{') return readName()
288
+ var d = 1
289
+ var st = ++i
290
+ while (i < s.length && d > 0) { if (s[i] === '{') d++; else if (s[i] === '}') { d--; if (d === 0) break } i++ }
291
+ var t = s.slice(st, i)
292
+ if (s[i] === '}') i++
293
+ return t
294
+ }
295
+ function inner(str) {
296
+ var m = texToMathML(str, false)
297
+ return m.replace(/^<math[^>]*>/, '').replace(/<\/math>$/, '')
298
+ }
299
+ function seq(stops, envMode, stopRight) {
300
+ var out = []
301
+ while (i < s.length && budget-- > 0) {
302
+ skipWs()
303
+ if (stops && stops.indexOf(s[i]) >= 0) break
304
+ if (envMode && (startsWith('\\\\') || startsWith('\\end'))) break
305
+ if (stopRight && startsWith('\\right')) break
306
+ if (s[i] === '^' || s[i] === '_') {
307
+ var base = out.pop() || '<mrow></mrow>'
308
+ var sub = null
309
+ var sup = null
310
+ while (s[i] === '^' || s[i] === '_') {
311
+ var take = s[i]
312
+ i++
313
+ var a2 = arg()
314
+ if (take === '^') sup = a2
315
+ else sub = a2
316
+ skipWs()
317
+ }
318
+ out.push(sub !== null && sup !== null
319
+ ? '<msubsup>' + base + sub + sup + '</msubsup>'
320
+ : sup !== null ? '<msup>' + base + sup + '</msup>' : '<msub>' + base + sub + '</msub>')
321
+ continue
322
+ }
323
+ var a = atom()
324
+ if (a === null) i++
325
+ else if (a) out.push(a)
326
+ }
327
+ return out.length === 1 ? out[0] : '<mrow>' + out.join('') + '</mrow>'
328
+ }
329
+ function arg() {
330
+ skipWs()
331
+ if (s[i] === '{') { i++; var v = seq('}'); if (s[i] === '}') i++; return v }
332
+ var a = atom()
333
+ return a || '<mrow></mrow>'
334
+ }
335
+ function atom() {
336
+ skipWs()
337
+ var c = s[i]
338
+ if (c === undefined) return null
339
+ if (c === '{') { i++; var g = seq('}'); if (s[i] === '}') i++; return g }
340
+ if (c === '\\') { i++; return command() }
341
+ if (/\d/.test(c)) { var a = i; while (/[\d.]/.test(s[i] || '')) i++; return '<mn>' + s.slice(a, i) + '</mn>' }
342
+ if (/[a-zA-Z]/.test(c)) {
343
+ // 一次一个字母:x^2 只作用于最后一个字母,与 TeX 的原子语义一致
344
+ i++
345
+ return '<mi>' + c + '</mi>'
346
+ }
347
+ if (c === '&') return null
348
+ i++
349
+ if (c === '-') return '<mo>−</mo>'
350
+ if (c === '*') return '<mo>∗</mo>'
351
+ if (c === "'") return '<mo>′</mo>'
352
+ return '<mo>' + texEscape(c) + '</mo>'
353
+ }
354
+ function env() {
355
+ var name = rawGroupText().trim()
356
+ var rows = []
357
+ var cells = []
358
+ while (i < s.length && budget-- > 0) {
359
+ skipWs()
360
+ if (startsWith('\\end')) { i += 4; rawGroupText(); break }
361
+ cells.push(seq('&', true))
362
+ skipWs()
363
+ if (s[i] === '&') { i++; continue }
364
+ rows.push(cells)
365
+ cells = []
366
+ if (startsWith('\\\\')) { i += 2; continue }
367
+ if (startsWith('\\end')) { i += 4; rawGroupText(); break }
368
+ if (i < s.length) i++
369
+ }
370
+ if (cells.length) rows.push(cells)
371
+ var body = rows.map(function (r) {
372
+ return '<mtr>' + r.map(function (c) { return '<mtd>' + (c || '') + '</mtd>' }).join('') + '</mtr>'
373
+ }).join('')
374
+ var table = '<mtable>' + body + '</mtable>'
375
+ if (/^[pbBvV]matrix$/.test(name)) {
376
+ var pair = name.charAt(0) === 'p' ? ['(', ')'] : name.charAt(0) === 'b' ? ['[', ']'] : name.charAt(0) === 'B' ? ['{', '}'] : ['|', '|']
377
+ return '<mrow><mo fence="true" stretchy="true">' + pair[0] + '</mo>' + table + '<mo fence="true" stretchy="true">' + pair[1] + '</mo></mrow>'
378
+ }
379
+ if (name === 'cases') return '<mrow><mo fence="true" stretchy="true">{</mo><mtable columnalign="left left">' + body + '</mtable></mrow>'
380
+ if (name === 'aligned' || name === 'align' || name === 'split' || name === 'alignedat') return '<mtable columnalign="right left">' + body + '</mtable>'
381
+ return table
382
+ }
383
+ function command() {
384
+ var name = readName()
385
+ switch (name) {
386
+ case 'frac': case 'dfrac': case 'tfrac':
387
+ return '<mfrac>' + arg() + arg() + '</mfrac>'
388
+ case 'binom':
389
+ return '<mrow><mo fence="true" stretchy="true">(</mo><mfrac linethickness="0">' + arg() + arg() + '</mfrac><mo fence="true" stretchy="true">)</mo></mrow>'
390
+ case 'sqrt': {
391
+ skipWs()
392
+ var deg = null
393
+ if (s[i] === '[') {
394
+ var st2 = ++i
395
+ while (i < s.length && s[i] !== ']') i++
396
+ deg = '<mrow>' + inner(s.slice(st2, i)) + '</mrow>'
397
+ if (s[i] === ']') i++
398
+ }
399
+ var body = arg()
400
+ return deg ? '<mroot>' + body + deg + '</mroot>' : '<msqrt>' + body + '</msqrt>'
401
+ }
402
+ case 'text': case 'textrm': case 'mbox': case 'operatorname':
403
+ return '<mtext>' + texEscape(rawGroupText()) + '</mtext>'
404
+ case 'mathrm': case 'mathbf': case 'mathit': case 'mathsf': case 'mathtt': case 'mathbb': case 'mathcal': case 'boldsymbol': {
405
+ var vmap = { mathrm: 'normal', mathbf: 'bold', mathit: 'italic', mathsf: 'sans-serif', mathtt: 'monospace', mathbb: 'double-struck', mathcal: 'script', boldsymbol: 'bold-italic' }
406
+ return '<mstyle mathvariant="' + vmap[name] + '">' + arg() + '</mstyle>'
407
+ }
408
+ case 'overline': case 'bar':
409
+ return '<mover accent="true">' + arg() + '<mo>‾</mo></mover>'
410
+ case 'underline':
411
+ return '<munder accentunder="true">' + arg() + '<mo>_</mo></munder>'
412
+ case 'hat': case 'widehat':
413
+ return '<mover accent="true">' + arg() + '<mo>^</mo></mover>'
414
+ case 'vec':
415
+ return '<mover accent="true">' + arg() + '<mo>→</mo></mover>'
416
+ case 'dot':
417
+ return '<mover accent="true">' + arg() + '<mo>˙</mo></mover>'
418
+ case 'ddot':
419
+ return '<mover accent="true">' + arg() + '<mo>¨</mo></mover>'
420
+ case 'tilde': case 'widetilde':
421
+ return '<mover accent="true">' + arg() + '<mo>~</mo></mover>'
422
+ case 'left': {
423
+ skipWs()
424
+ var open = ''
425
+ if (s[i] === '\\') { i++; var n3 = readName(); open = TEX_SYM[n3] !== undefined ? TEX_SYM[n3] : n3 }
426
+ else { open = s[i] === undefined ? '' : s[i]; i++ }
427
+ var content = seq(null, true, true)
428
+ var close = ''
429
+ if (startsWith('\\right')) {
430
+ i += 6
431
+ skipWs()
432
+ if (s[i] === '\\') { i++; var n4 = readName(); close = TEX_SYM[n4] !== undefined ? TEX_SYM[n4] : n4 }
433
+ else { close = s[i] === undefined ? '' : s[i]; i++ }
434
+ }
435
+ var op = open === '.' || open === '' ? '' : '<mo fence="true" stretchy="true">' + texEscape(open) + '</mo>'
436
+ var cp = close === '.' || close === '' ? '' : '<mo fence="true" stretchy="true">' + texEscape(close) + '</mo>'
437
+ return '<mrow>' + op + content + cp + '</mrow>'
438
+ }
439
+ case 'right': {
440
+ skipWs()
441
+ if (s[i] === '\\') { i++; readName() } else i++
442
+ return ''
443
+ }
444
+ case 'begin':
445
+ return env()
446
+ case 'end':
447
+ rawGroupText()
448
+ return ''
449
+ case 'quad': return '<mspace width="1em"></mspace>'
450
+ case 'qquad': return '<mspace width="2em"></mspace>'
451
+ case ',': case ':': case ';': case '!': case ' ': case 'thinspace':
452
+ return '<mspace width="0.1667em"></mspace>'
453
+ case 'displaystyle': case 'textstyle': case 'limits': case 'nolimits': case 'hline':
454
+ return ''
455
+ case '\\':
456
+ return '<mspace linebreak="newline"></mspace>'
457
+ }
458
+ if (TEX_FUNCS.indexOf(name) >= 0) return '<mi mathvariant="normal">' + name + '</mi>'
459
+ if (TEX_SYM[name] !== undefined) {
460
+ var v = TEX_SYM[name]
461
+ if (TEX_GREEK.indexOf(name) >= 0 || TEX_LETTERLIKE.indexOf(name) >= 0) return '<mi>' + texEscape(v) + '</mi>'
462
+ var big = '∑∏∐∫∬∭∮'.indexOf(v) >= 0
463
+ return '<mo' + (big ? ' largeop="true"' : '') + '>' + texEscape(v) + '</mo>'
464
+ }
465
+ return '<mtext>' + texEscape(name) + '</mtext>'
466
+ }
467
+ var body
468
+ try {
469
+ body = seq(null, false)
470
+ } catch (e) {
471
+ return '<math xmlns="http://www.w3.org/1998/Math/MathML"><mtext>' + texEscape(s) + '</mtext></math>'
472
+ }
473
+ return '<math xmlns="http://www.w3.org/1998/Math/MathML"' + (display ? ' display="block"' : '') + '>' + body + '</math>'
474
+ }
475
+
476
+ function safeTex(src, display) {
477
+ try {
478
+ return texToMathML(src, display)
479
+ } catch (e) {
480
+ return '<span>' + texEscape(src) + '</span>'
481
+ }
482
+ }
483
+
484
+ // ── 极简 markdown 渲染(不引第三方库)─────────────────────────────────
485
+ var MD_INLINE = /`([^`\n]+)`|\$\$([\s\S]+?)\$\$|\$([^$\n]+?)\$|\[\[([a-zA-Z0-9\u4e00-\u9fff:._-]+)\]\]|\*\*([^*\n]+)\*\*|\*([^*\n]+)\*|\[([^\]\n]+)\]\((https?:[^)\s]+)\)/g
486
+
487
+ function mdInline(text, onOpen, kbKey) {
488
+ var out = []
489
+ var last = 0
490
+ var m
491
+ var n = 0
492
+ MD_INLINE.lastIndex = 0
493
+ while ((m = MD_INLINE.exec(text))) {
494
+ if (m.index > last) out.push(text.slice(last, m.index))
495
+ var key = kbKey + '-i' + n++
496
+ if (m[1] !== undefined) {
497
+ out.push(h('code', { key: key }, m[1]))
498
+ } else if (m[2] !== undefined || m[3] !== undefined) {
499
+ var texSrc = m[2] !== undefined ? m[2] : m[3]
500
+ out.push(h('span', { key: key, className: 'nkb-math', dangerouslySetInnerHTML: { __html: safeTex(texSrc, m[2] !== undefined) } }))
501
+ } else if (m[4] !== undefined) {
502
+ out.push(
503
+ h(
504
+ 'span',
505
+ { key: key, className: 'nkb-link', title: m[4], onClick: (function (tid) { return function () { onOpen && onOpen(tid) } })(m[4]) },
506
+ m[4],
507
+ ),
508
+ )
509
+ } else if (m[5] !== undefined) {
510
+ out.push(h('strong', { key: key }, m[5]))
511
+ } else if (m[6] !== undefined) {
512
+ out.push(h('em', { key: key }, m[6]))
513
+ } else if (m[7] !== undefined) {
514
+ out.push(h('a', { key: key, href: m[8], target: '_blank', rel: 'noreferrer' }, m[7]))
515
+ }
516
+ last = m.index + m[0].length
517
+ }
518
+ if (last < text.length) out.push(text.slice(last))
519
+ return out
520
+ }
521
+
522
+ function splitRow(line) {
523
+ return String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (s) { return s.trim() })
524
+ }
525
+
526
+ function mdBlocks(text) {
527
+ var lines = String(text || '').replace(/\r\n?/g, '\n').split('\n')
528
+ var blocks = []
529
+ var i = 0
530
+ while (i < lines.length) {
531
+ var line = lines[i]
532
+ if (!line.trim()) { i++; continue }
533
+ var fence = /^\s*```(\S*)\s*$/.exec(line)
534
+ if (fence) {
535
+ var buf = []
536
+ i++
537
+ while (i < lines.length && !/^\s*```/.test(lines[i])) buf.push(lines[i++])
538
+ i++
539
+ var lang = fence[1] || ''
540
+ blocks.push({ k: /^(math|tex|latex)$/i.test(lang) ? 'math' : 'code', lang: lang, text: buf.join('\n') })
541
+ continue
542
+ }
543
+ if (/^\s*\$\$/.test(line)) {
544
+ var mb = []
545
+ var rest = line.replace(/^\s*\$\$/, '')
546
+ if (/\$\$\s*$/.test(rest)) {
547
+ blocks.push({ k: 'math', text: rest.replace(/\$\$\s*$/, '') })
548
+ i++
549
+ continue
550
+ }
551
+ if (rest.trim()) mb.push(rest)
552
+ i++
553
+ while (i < lines.length && !/\$\$/.test(lines[i])) mb.push(lines[i++])
554
+ if (i < lines.length) {
555
+ mb.push(lines[i].replace(/\$\$[\s\S]*$/, ''))
556
+ i++
557
+ }
558
+ blocks.push({ k: 'math', text: mb.join('\n') })
559
+ continue
560
+ }
561
+ var hd = /^(#{1,6})\s+(.*)$/.exec(line)
562
+ if (hd) { blocks.push({ k: 'h', level: hd[1].length, text: hd[2].trim() }); i++; continue }
563
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { blocks.push({ k: 'hr' }); i++; continue }
564
+ if (/^\s*>/.test(line)) {
565
+ var q = []
566
+ while (i < lines.length && /^\s*>/.test(lines[i])) q.push(lines[i++].replace(/^\s*>\s?/, ''))
567
+ blocks.push({ k: 'quote', text: q.join('\n') })
568
+ continue
569
+ }
570
+ if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) {
571
+ var items = []
572
+ while (i < lines.length && /^\s*([-*+]|\d+[.)])\s+/.test(lines[i])) {
573
+ var lm = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[i])
574
+ items.push({ depth: Math.min(4, Math.floor(lm[1].length / 2.2)), ordered: /\d/.test(lm[2]), text: lm[3] })
575
+ i++
576
+ }
577
+ blocks.push({ k: 'list', items: items })
578
+ continue
579
+ }
580
+ if (/^\s*\|/.test(line) && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1] || '')) {
581
+ var th = splitRow(line)
582
+ i += 2
583
+ var rows = []
584
+ while (i < lines.length && /^\s*\|/.test(lines[i])) rows.push(splitRow(lines[i++]))
585
+ blocks.push({ k: 'table', head: th, rows: rows })
586
+ continue
587
+ }
588
+ var para = [line]
589
+ i++
590
+ while (i < lines.length && lines[i].trim() && !/^(#{1,6}\s|```|\$\$|\s*>|\s*([-*+]|\d+[.)])\s+|\s*\|)/.test(lines[i])) {
591
+ para.push(lines[i++])
592
+ }
593
+ blocks.push({ k: 'p', text: para.join('\n') })
594
+ }
595
+ return blocks
596
+ }
597
+
598
+ function mdBlock(b, idx, onOpen) {
599
+ var k = 'b' + idx
600
+ if (b.k === 'code') return h('pre', { key: k }, h('code', null, b.text))
601
+ if (b.k === 'math') return h('div', { key: k, className: 'nkb-math nkb-math-block', dangerouslySetInnerHTML: { __html: safeTex(b.text, true) } })
602
+ if (b.k === 'h') return h('h' + b.level, { key: k }, mdInline(b.text, onOpen, k))
603
+ if (b.k === 'hr') return h('hr', { key: k })
604
+ if (b.k === 'quote') return h('blockquote', { key: k }, mdInline(b.text, onOpen, k))
605
+ if (b.k === 'list') {
606
+ var tag = b.items.length && b.items[0].ordered ? 'ol' : 'ul'
607
+ return h(
608
+ tag,
609
+ { key: k },
610
+ b.items.map(function (it, j) {
611
+ return h('li', { key: k + '-' + j, style: { marginLeft: it.depth * 14 + 'px' } }, mdInline(it.text, onOpen, k + '-' + j))
612
+ }),
613
+ )
614
+ }
615
+ if (b.k === 'table') {
616
+ // 外面套一层横向滚动,宽表格不会顶出面板
617
+ return h(
618
+ 'div',
619
+ { key: k, className: 'nkb-tablewrap' },
620
+ h(
621
+ 'table',
622
+ null,
623
+ h('thead', null, h('tr', null, b.head.map(function (c, j) { return h('th', { key: j }, mdInline(c, onOpen, k + 'h' + j)) }))),
624
+ h('tbody', null, b.rows.map(function (r, j) {
625
+ return h('tr', { key: j }, r.map(function (c, cj) { return h('td', { key: cj }, mdInline(c, onOpen, k + j + '-' + cj)) }))
626
+ })),
627
+ ),
628
+ )
629
+ }
630
+ return h('p', { key: k }, mdInline(b.text, onOpen, k))
631
+ }
632
+
633
+ function Md(props) {
634
+ var text = props.text || ''
635
+ var blocks = useMemo(function () { return mdBlocks(text) }, [text])
636
+ return h('div', { className: 'nkb-md' }, blocks.map(function (b, i) { return mdBlock(b, i, props.onOpen) }))
637
+ }
638
+
639
+ function TreeRow(props) {
640
+ var node = props.node
641
+ var onOpen = props.onSelect || props.onOpen
642
+ var sel = props.sel
643
+ var expanded = props.expanded
644
+ var toggle = props.toggle
645
+ var depth = props.depth || 0
646
+ var kids = node.children || []
647
+ var open = expanded[node.tid] !== false && kids.length > 0
648
+ var rows = [
649
+ h(
650
+ 'div',
651
+ {
652
+ key: node.tid,
653
+ className: 'nkb-row' + (sel === node.tid ? ' on' : ''),
654
+ style: { paddingLeft: 4 + depth * 11 + 'px' },
655
+ title: node.tid + (node.source ? '\n' + node.source : ''),
656
+ onClick: function () { onOpen(node.tid) },
657
+ },
658
+ h(
659
+ 'span',
660
+ {
661
+ className: 'nkb-caret',
662
+ onClick: function (e) {
663
+ e.stopPropagation()
664
+ if (kids.length) toggle(node.tid)
665
+ },
666
+ },
667
+ kids.length ? (open ? '▾' : '▸') : '',
668
+ ),
669
+ h('span', { className: 't' }, node.title || node.tid),
670
+ node.type ? h('span', { className: 'nkb-tag' }, node.type) : null,
671
+ h('span', { className: 'n' }, node.chars),
672
+ ),
673
+ ]
674
+ if (open) {
675
+ for (var i = 0; i < kids.length; i++) {
676
+ rows.push(h(TreeRow, { key: kids[i].tid, node: kids[i], onOpen: onOpen, sel: sel, expanded: expanded, toggle: toggle, depth: depth + 1 }))
677
+ }
678
+ }
679
+ return rows
680
+ }
681
+
682
+ function Icon(props) {
683
+ var size = props.size || 18
684
+ // 形状:三个叠层的方块 = 「库里的一棵节点树」
685
+ return h(
686
+ 'svg',
687
+ { width: size, height: size, viewBox: '0 0 20 20', fill: 'none', 'aria-hidden': 'true' },
688
+ h('rect', { x: 3, y: 2.5, width: 14, height: 4, rx: 1.2, stroke: 'currentColor', strokeWidth: 1.4 }),
689
+ h('rect', { x: 3, y: 8, width: 9.5, height: 3.4, rx: 1.1, stroke: 'currentColor', strokeWidth: 1.3, opacity: 0.85 }),
690
+ h('rect', { x: 3, y: 12.6, width: 6.5, height: 3.4, rx: 1.1, stroke: 'currentColor', strokeWidth: 1.3, opacity: 0.6 }),
691
+ h('path', { d: 'M13.5 10.7v6.6', stroke: 'currentColor', strokeWidth: 1.2, opacity: 0.55, strokeLinecap: 'round' }),
692
+ )
693
+ }
694
+
695
+ // ── 节点图(自己排的树形布局,不引第三方库)──────────────────────────
696
+ var NODE_W = 188
697
+ var NODE_H = 34
698
+
699
+ function walkTree(node, fn, depth) {
700
+ if (!node) return
701
+ fn(node, depth || 0)
702
+ var kids = node.children || []
703
+ for (var i = 0; i < kids.length; i++) walkTree(kids[i], fn, (depth || 0) + 1)
704
+ }
705
+
706
+ function collectTypes(root) {
707
+ var set = {}
708
+ walkTree(root, function (n) {
709
+ if (n.type) set[n.type] = (set[n.type] || 0) + 1
710
+ })
711
+ return set
712
+ }
713
+
714
+ /**
715
+ * 树 → 坐标。叶子按顺序占槽位,父节点落在首尾子节点的中点上,
716
+ * 跟 MCP WebUI 的确定性布局一个思路:同样的树每次画出来都一样。
717
+ */
718
+ function layoutGraph(root, opts) {
719
+ var items = []
720
+ function collect(node, depth) {
721
+ if (!node || opts.hidden[node.type]) return null
722
+ var kids = []
723
+ if (depth < opts.maxDepth && !opts.collapsed[node.tid]) {
724
+ var raw = node.children || []
725
+ for (var i = 0; i < raw.length; i++) {
726
+ var c = collect(raw[i], depth + 1)
727
+ if (c) kids.push(c)
728
+ }
729
+ }
730
+ var it = { node: node, tid: node.tid, depth: depth, kids: kids, x: 0, y: 0, primary: 0 }
731
+ items.push(it)
732
+ return it
733
+ }
734
+ var rootItem = collect(root, 0)
735
+ if (!rootItem) return { items: [], edges: [], width: 1, height: 1, minX: 0, minY: 0 }
736
+ var tb = opts.dir !== 'LR'
737
+ var slot = tb ? NODE_W + 20 : NODE_H + 26
738
+ var level = tb ? NODE_H + 56 : NODE_W + 64
739
+ var cursor = 0
740
+ ;(function place(it) {
741
+ if (!it.kids.length) {
742
+ it.primary = cursor
743
+ cursor += 1
744
+ } else {
745
+ for (var i = 0; i < it.kids.length; i++) place(it.kids[i])
746
+ it.primary = (it.kids[0].primary + it.kids[it.kids.length - 1].primary) / 2
747
+ }
748
+ var sec = it.depth * level
749
+ if (tb) {
750
+ it.x = it.primary * slot
751
+ it.y = sec
752
+ } else {
753
+ it.x = sec
754
+ it.y = it.primary * slot
755
+ }
756
+ })(rootItem)
757
+ var edges = []
758
+ for (var i = 0; i < items.length; i++) {
759
+ for (var j = 0; j < items[i].kids.length; j++) edges.push({ from: items[i], to: items[i].kids[j] })
760
+ }
761
+ var minX = Infinity
762
+ var minY = Infinity
763
+ var maxX = -Infinity
764
+ var maxY = -Infinity
765
+ for (var k = 0; k < items.length; k++) {
766
+ var a = items[k]
767
+ minX = Math.min(minX, a.x)
768
+ minY = Math.min(minY, a.y)
769
+ maxX = Math.max(maxX, a.x + NODE_W)
770
+ maxY = Math.max(maxY, a.y + NODE_H)
771
+ }
772
+ return {
773
+ items: items,
774
+ edges: edges,
775
+ tb: tb,
776
+ minX: minX,
777
+ minY: minY,
778
+ width: Math.max(1, maxX - minX),
779
+ height: Math.max(1, maxY - minY),
780
+ }
781
+ }
782
+
783
+ function edgePath(a, b, tb) {
784
+ if (tb) {
785
+ var x1 = a.x + NODE_W / 2
786
+ var y1 = a.y + NODE_H
787
+ var x2 = b.x + NODE_W / 2
788
+ var y2 = b.y
789
+ var my = (y1 + y2) / 2
790
+ return 'M' + x1 + ',' + y1 + ' C' + x1 + ',' + my + ' ' + x2 + ',' + my + ' ' + x2 + ',' + y2
791
+ }
792
+ var ax = a.x + NODE_W
793
+ var ay = a.y + NODE_H / 2
794
+ var bx = b.x
795
+ var by = b.y + NODE_H / 2
796
+ var mx = (ax + bx) / 2
797
+ return 'M' + ax + ',' + ay + ' C' + mx + ',' + ay + ' ' + mx + ',' + by + ' ' + bx + ',' + by
798
+ }
799
+
800
+ function ellipsis(s, n) {
801
+ s = String(s || '')
802
+ return s.length > n ? s.slice(0, n - 1) + '…' : s
803
+ }
804
+
805
+ function Graph(props) {
806
+ var lay = useMemo(
807
+ function () {
808
+ return layoutGraph(props.root, {
809
+ dir: props.dir,
810
+ maxDepth: props.maxDepth,
811
+ hidden: props.hidden,
812
+ collapsed: props.collapsed,
813
+ })
814
+ },
815
+ [props.root, props.dir, props.maxDepth, props.hidden, props.collapsed],
816
+ )
817
+ var viewState = useState(null)
818
+ var view = viewState[0]
819
+ var setView = viewState[1]
820
+ var svgRef = useRef(null)
821
+ var dragRef = useRef(null)
822
+ var layoutKey =
823
+ props.dir + '|' + props.maxDepth + '|' + Object.keys(props.hidden || {}).length + '|' + Object.keys(props.collapsed || {}).length
824
+
825
+ function fitNow() {
826
+ var el = svgRef.current
827
+ if (!el || typeof el.getBoundingClientRect !== 'function') return
828
+ var r = el.getBoundingClientRect()
829
+ if (!r.width || !r.height) return
830
+ var k = Math.min(1.1, (r.width - 56) / lay.width, (r.height - 56) / lay.height)
831
+ if (!isFinite(k) || k <= 0) k = 1
832
+ setView({
833
+ k: k,
834
+ x: (r.width - lay.width * k) / 2 - lay.minX * k,
835
+ y: (r.height - lay.height * k) / 2 - lay.minY * k,
836
+ })
837
+ }
838
+
839
+ useEffect(function () {
840
+ fitNow()
841
+ }, [layoutKey, props.fitTick, lay])
842
+
843
+ useEffect(function () {
844
+ var el = svgRef.current
845
+ if (!el || typeof el.addEventListener !== 'function') return undefined
846
+ function onWheel(e) {
847
+ e.preventDefault()
848
+ var r = el.getBoundingClientRect()
849
+ var mx = e.clientX - r.left
850
+ var my = e.clientY - r.top
851
+ setView(function (v) {
852
+ if (!v) return v
853
+ var k = Math.max(0.15, Math.min(3, v.k * Math.exp(-e.deltaY * 0.0015)))
854
+ var ratio = k / v.k
855
+ return { k: k, x: mx - (mx - v.x) * ratio, y: my - (my - v.y) * ratio }
856
+ })
857
+ }
858
+ el.addEventListener('wheel', onWheel, { passive: false })
859
+ return function () {
860
+ el.removeEventListener('wheel', onWheel)
861
+ }
862
+ }, [])
863
+
864
+ function down(e) {
865
+ var v = view || { k: 1, x: 0, y: 0 }
866
+ dragRef.current = { x: e.clientX, y: e.clientY, vx: v.x, vy: v.y, moved: false }
867
+ if (e.currentTarget.classList) e.currentTarget.classList.add('drag')
868
+ }
869
+ function move(e) {
870
+ var d = dragRef.current
871
+ if (!d) return
872
+ var dx = e.clientX - d.x
873
+ var dy = e.clientY - d.y
874
+ if (Math.abs(dx) + Math.abs(dy) > 2) d.moved = true
875
+ setView(function (v) {
876
+ return v ? { k: v.k, x: d.vx + dx, y: d.vy + dy } : v
877
+ })
878
+ }
879
+ function up(e) {
880
+ dragRef.current = null
881
+ if (e && e.currentTarget && e.currentTarget.classList) e.currentTarget.classList.remove('drag')
882
+ }
883
+
884
+ var t = view || { k: 1, x: 0, y: 0 }
885
+ var tb = lay.tb
886
+
887
+ return h(
888
+ 'div',
889
+ { className: 'nkb-gwrap', style: { position: 'absolute', inset: 0 } },
890
+ h(
891
+ 'svg',
892
+ { ref: svgRef, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerLeave: up },
893
+ h(
894
+ 'g',
895
+ { transform: 'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')' },
896
+ lay.edges.map(function (e, i) {
897
+ var on = props.sel === e.from.tid || props.sel === e.to.tid
898
+ return h('path', { key: 'e' + i, className: 'nkb-edge' + (on ? ' on' : ''), d: edgePath(e.from, e.to, tb) })
899
+ }),
900
+ lay.items.map(function (it) {
901
+ var cls = 'nkb-node' + (props.sel === it.tid ? ' sel' : '') + (it.node.kind === 'folder' ? ' folder' : '')
902
+ return h(
903
+ 'g',
904
+ {
905
+ key: it.tid,
906
+ className: cls,
907
+ transform: 'translate(' + it.x + ',' + it.y + ')',
908
+ onClick: function (e) {
909
+ e.stopPropagation()
910
+ props.onSelect(it.tid)
911
+ },
912
+ onDoubleClick: function (e) {
913
+ e.stopPropagation()
914
+ props.onToggleCollapse(it.tid)
915
+ },
916
+ },
917
+ h('rect', { width: NODE_W, height: NODE_H, rx: 6, stroke: typeColor(it.node.type), strokeWidth: 1.4 }),
918
+ h('text', { x: 10, y: NODE_H / 2 + 4 }, ellipsis(it.node.title || it.tid, tb ? 24 : 22)),
919
+ h('title', null, (it.node.title || it.tid) + '\n' + it.tid + '\n' + (it.node.type || '') + ' · ' + (it.node.chars || 0) + ' 字符'),
920
+ )
921
+ }),
922
+ ),
923
+ ),
924
+ h(
925
+ 'div',
926
+ { className: 'nkb-gbar' },
927
+ h('button', { onClick: function () { setView(function (v) { return v ? { k: Math.min(3, v.k * 1.25), x: v.x, y: v.y } : v }) }, title: '放大' }, '+'),
928
+ h('button', { onClick: function () { setView(function (v) { return v ? { k: Math.max(0.15, v.k / 1.25), x: v.x, y: v.y } : v }) }, title: '缩小' }, '−'),
929
+ h('button', { onClick: fitNow, title: '适应视图' }, '适应'),
930
+ h('span', { className: 'nkb-muted', style: { padding: '2px 6px', fontSize: 11 } }, Math.round((t.k || 1) * 100) + '%'),
931
+ ),
932
+ )
933
+ }
934
+
935
+ // ── 主面板 ────────────────────────────────────────────────────────────
936
+ function Panel() {
937
+ ensureStyle()
938
+
939
+ var libsState = useState([])
940
+ var libs = libsState[0]
941
+ var setLibs = libsState[1]
942
+
943
+ var kbState = useState('')
944
+ var kb = kbState[0]
945
+ var setKb = kbState[1]
946
+
947
+ var treeState = useState(null)
948
+ var tree = treeState[0]
949
+ var setTree = treeState[1]
950
+
951
+ var selState = useState('root')
952
+ var sel = selState[0]
953
+ var setSel = selState[1]
954
+
955
+ var modeState = useState('subtree')
956
+ var mode = modeState[0]
957
+ var setMode = modeState[1]
958
+
959
+ var viewState = useState('subtree')
960
+ var view = viewState[0]
961
+ var setView = viewState[1]
962
+
963
+ var nodeState = useState(null)
964
+ var nodeData = nodeState[0]
965
+ var setNodeData = nodeState[1]
966
+
967
+ var srcState = useState(null)
968
+ var src = srcState[0]
969
+ var setSrc = srcState[1]
970
+
971
+ var errState = useState('')
972
+ var err = errState[0]
973
+ var setErr = errState[1]
974
+
975
+ var busyState = useState(false)
976
+ var busy = busyState[0]
977
+ var setBusy = busyState[1]
978
+
979
+ var qState = useState('')
980
+ var query = qState[0]
981
+ var setQuery = qState[1]
982
+
983
+ var hitsState = useState(null)
984
+ var hits = hitsState[0]
985
+ var setHits = hitsState[1]
986
+
987
+ var backState = useState(null)
988
+ var back = backState[0]
989
+ var setBack = backState[1]
990
+
991
+ var diagState = useState(null)
992
+ var diagData = diagState[0]
993
+ var setDiag = diagState[1]
994
+
995
+ var expState = useState({})
996
+ var expanded = expState[0]
997
+ var setExpanded = expState[1]
998
+
999
+ // 节点图:布局方向 / 展开层数 / 被隐藏的类型 / 手动折叠的节点 / 重排计数
1000
+ var gdirState = useState('LR')
1001
+ var gdir = gdirState[0]
1002
+ var setGdir = gdirState[1]
1003
+ var gdepthState = useState(3)
1004
+ var gdepth = gdepthState[0]
1005
+ var setGdepth = gdepthState[1]
1006
+ var ghideState = useState({})
1007
+ var ghide = ghideState[0]
1008
+ var setGhide = ghideState[1]
1009
+ var gcollState = useState({})
1010
+ var gcoll = gcollState[0]
1011
+ var setGcoll = gcollState[1]
1012
+ var fitState = useState(0)
1013
+ var fitTick = fitState[0]
1014
+ var setFitTick = fitState[1]
1015
+
1016
+ // DSH 自己的 workspace 列表(宿主 /workspaces 给):面板没有会话上下文,
1017
+ // 但「这个 DSH 用过哪些项目目录」宿主知道,不用让人手打路径。
1018
+ var wsState = useState([])
1019
+ var wsList = wsState[0]
1020
+ var setWsList = wsState[1]
1021
+
1022
+ var toggle = useCallback(function (tid) {
1023
+ setExpanded(function (prev) {
1024
+ var next = {}
1025
+ for (var k in prev) next[k] = prev[k]
1026
+ next[tid] = prev[tid] === false
1027
+ return next
1028
+ })
1029
+ }, [])
1030
+
1031
+ // 库列表
1032
+ var loadLibs = useCallback(function () {
1033
+ return get('/libs')
1034
+ .then(function (r) {
1035
+ var rows = r.libs || []
1036
+ setLibs(rows)
1037
+ 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 }
1040
+ }
1041
+ if (!pick) {
1042
+ for (var j = 0; j < rows.length; j++) { if (rows[j].exists) { pick = rows[j].name; break } }
1043
+ }
1044
+ setKb(pick || '')
1045
+ if (!rows.length) {
1046
+ setErr('这个工作区里没找到 .nodekb 库。换一个目录(右上「目录」),或在项目里建 .nodekb/ 放 md 文件;放在 .dsh/skills/<技能>/.nodekb/ 也会被自动发现。')
1047
+ } else {
1048
+ setErr('')
1049
+ }
1050
+ })
1051
+ .catch(function (e) { setErr(e.message) })
1052
+ }, [])
1053
+
1054
+ var loadWs = useCallback(function () {
1055
+ return get('/workspaces')
1056
+ .then(function (r) {
1057
+ var rows = r.workspaces || []
1058
+ setWsList(rows)
1059
+ return rows
1060
+ })
1061
+ .catch(function () { return [] })
1062
+ }, [])
1063
+
1064
+ // 首次挂载:没手动指定过工作区时,从 DSH 的 workspace 列表里挑一个真的
1065
+ // 有 .nodekb 的目录(优先),这样面板打开就能看到东西。
1066
+ useEffect(function () {
1067
+ if (WS) {
1068
+ loadWs()
1069
+ loadLibs()
1070
+ return
1071
+ }
1072
+ loadWs().then(function (rows) {
1073
+ var cand = null
1074
+ for (var i = 0; i < rows.length; i++) {
1075
+ if (rows[i].nodekb) { cand = rows[i]; break }
1076
+ }
1077
+ if (cand) {
1078
+ writeWs(cand.path)
1079
+ setKb('')
1080
+ setTree(null)
1081
+ setHits(null)
1082
+ setSel('root')
1083
+ }
1084
+ loadLibs()
1085
+ })
1086
+ }, [])
1087
+
1088
+ /** 选一个工作区(来自 DSH workspace 列表或手打路径) */
1089
+ function useWs(dir) {
1090
+ writeWs(String(dir || '').trim())
1091
+ setKb('')
1092
+ setTree(null)
1093
+ setHits(null)
1094
+ setSel('root')
1095
+ loadLibs()
1096
+ }
1097
+
1098
+ /** 换工作区:面板没有会话上下文,只能由人指定一次,记在 localStorage */
1099
+ function pickWs() {
1100
+ if (!g.prompt) return
1101
+ var v = g.prompt('nodekb 工作区目录(留空 = 插件进程当前目录)', WS || '')
1102
+ if (v === null || v === undefined) return
1103
+ useWs(v)
1104
+ }
1105
+
1106
+ // 树
1107
+ var loadTree = useCallback(function (name) {
1108
+ setTree(null)
1109
+ setHits(null)
1110
+ return get('/tree', { kb: name })
1111
+ .then(function (r) { setTree(r); setErr('') })
1112
+ .catch(function (e) { setErr(e.message); setTree({ name: name || '?', tree: null, nodeCount: 0 }) })
1113
+ }, [])
1114
+
1115
+ useEffect(function () {
1116
+ if (!kb && !libs.length) return
1117
+ loadTree(kb).then(function () {
1118
+ if (!kb) {
1119
+ // 没有任何库时也别静默
1120
+ setErr('没有可用的 nodekb 库。在工作区建 .nodekb/ 放 md 文件,或在插件配置里登记库。')
1121
+ }
1122
+ })
1123
+ }, [kb, libs.length])
1124
+
1125
+ // 节点正文
1126
+ useEffect(function () {
1127
+ if (!kb || !sel) return
1128
+ setBusy(true)
1129
+ var m = view === 'toc' ? 'subtree' : view
1130
+ get('/node', { kb: kb, tid: sel, mode: m })
1131
+ .then(function (r) { setNodeData(r); setErr('') })
1132
+ .catch(function (e) { setErr(e.message); setNodeData(null) })
1133
+ .then(function () { setBusy(false) })
1134
+ }, [kb, sel, view])
1135
+
1136
+ // 源码
1137
+ useEffect(function () {
1138
+ setSrc(null)
1139
+ if (view !== 'source' || !kb || !sel) return
1140
+ get('/source', { kb: kb, tid: sel })
1141
+ .then(function (r) { setSrc(r) })
1142
+ .catch(function (e) { setSrc({ error: e.message }) })
1143
+ }, [view, kb, sel])
1144
+
1145
+ // 反向链接
1146
+ useEffect(function () {
1147
+ setBack(null)
1148
+ if (!kb || !sel) return
1149
+ get('/backlinks', { kb: kb, tid: sel })
1150
+ .then(function (r) { setBack(r.backlinks || []) })
1151
+ .catch(function () { setBack([]) })
1152
+ }, [kb, sel, nodeData && nodeData.title])
1153
+
1154
+ function open(tid) {
1155
+ setSel(tid)
1156
+ setView('subtree')
1157
+ setHits(null)
1158
+ setQuery('')
1159
+ }
1160
+
1161
+ function runSearch() {
1162
+ var q = (query || '').trim()
1163
+ if (!q) { setHits(null); return }
1164
+ setBusy(true)
1165
+ get('/search', { kb: kb, q: q, limit: 30 })
1166
+ .then(function (r) { setHits(r); setErr('') })
1167
+ .catch(function (e) { setErr(e.message) })
1168
+ .then(function () { setBusy(false) })
1169
+ }
1170
+
1171
+ function runDiag() {
1172
+ setBusy(true)
1173
+ get('/diag', { kb: kb })
1174
+ .then(function (r) { setDiag(r); setErr('') })
1175
+ .catch(function (e) { setErr(e.message) })
1176
+ .then(function () { setBusy(false) })
1177
+ }
1178
+
1179
+ /** 改文件名:TID 由文件名派生,所以引用要一起搬(走 /rename,不自己拼) */
1180
+ function rename() {
1181
+ if (!g.prompt || !info || info.kind !== 'file') return
1182
+ var v = g.prompt('新文件名(.md 可省略;全库引用会一起改写)', (info.title || '') + '.md')
1183
+ if (v === null || v === undefined) return
1184
+ var name = String(v).trim()
1185
+ if (!name) return
1186
+ setBusy(true)
1187
+ get('/rename', { kb: kb, target: info.tid, new_name: name })
1188
+ .then(function (r) {
1189
+ setErr('')
1190
+ setDiag({ errors: [], warnings: [], rename: r.text || '' })
1191
+ setSel(r.new_tid || r.newTid || info.tid)
1192
+ return loadTree(kb)
1193
+ })
1194
+ .catch(function (e) { setErr(e.message) })
1195
+ .then(function () { setBusy(false) })
1196
+ }
1197
+
1198
+ var text = ''
1199
+ if (view === 'source' && src) {
1200
+ text = src.error ? '' : (src.lines || []).map(function (l) { return l.number + '\t' + l.text }).join('\n')
1201
+ } else if (nodeData) {
1202
+ text = nodeData.text || ''
1203
+ }
1204
+ // 面板单次最多渲染 1 万字符:再长就只显示开头,正文与滚动条都不至于卡死
1205
+ var TEXT_LIMIT = 10000
1206
+ var textCut = text.length > TEXT_LIMIT ? text.slice(0, TEXT_LIMIT) : ''
1207
+ var shownText = textCut ? textCut : text
1208
+
1209
+ var treeNode = tree && tree.tree
1210
+ var info = nodeData || null
1211
+
1212
+ var head = h(
1213
+ 'div',
1214
+ { className: 'nkb-head' },
1215
+ h('span', { className: 'nkb-title' }, '项目节点知识库'),
1216
+ h(
1217
+ 'select',
1218
+ {
1219
+ value: kb,
1220
+ onChange: function (e) { setKb(e.target.value); setSel('root'); setView('subtree') },
1221
+ title: '切换知识库',
1222
+ },
1223
+ libs.length
1224
+ ? libs.map(function (l) {
1225
+ return h('option', { key: l.name, value: l.name }, l.name + (l.exists ? '' : '(路径不存在)'))
1226
+ })
1227
+ : h('option', { value: '' }, '(无库)'),
1228
+ ),
1229
+ tree && typeof tree.nodeCount === 'number' ? h('span', { className: 'nkb-muted' }, tree.nodeCount + ' 节点') : null,
1230
+ h('input', {
1231
+ value: query,
1232
+ placeholder: '检索(中文自然语言可用)',
1233
+ onChange: function (e) { setQuery(e.target.value) },
1234
+ onKeyDown: function (e) { if (e.key === 'Enter') runSearch() },
1235
+ }),
1236
+ h(
1237
+ 'select',
1238
+ {
1239
+ value: WS,
1240
+ onChange: function (e) { useWs(e.target.value) },
1241
+ title: '切换工作区(DSH 里用过的项目目录)',
1242
+ style: { maxWidth: 220 },
1243
+ },
1244
+ 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,
1249
+ ),
1250
+ h('button', { onClick: runSearch, disabled: busy }, '搜索'),
1251
+ h('button', { onClick: runDiag, disabled: busy }, '诊断'),
1252
+ h('button', { onClick: function () { loadTree(kb) }, disabled: busy }, '刷新'),
1253
+ h('button', { onClick: pickWs, title: '指定工作区目录(面板拿不到会话上下文)' }, '目录'),
1254
+ busy ? h('span', { className: 'nkb-muted' }, '…') : null,
1255
+ )
1256
+
1257
+ // 图设置:布局方向、展开层数、类型筛选(点一下隐藏该类型)
1258
+ var typeStats = useMemo(function () {
1259
+ return treeNode ? collectTypes(treeNode) : {}
1260
+ }, [treeNode])
1261
+ var typeNames = Object.keys(typeStats).sort()
1262
+
1263
+ var settings = h(
1264
+ 'div',
1265
+ { className: 'nkb-settings' },
1266
+ h('div', { className: 'lbl' }, '布局'),
1267
+ h(
1268
+ 'div',
1269
+ null,
1270
+ h('span', { className: 'nkb-chip' + (gdir === 'TB' ? ' on' : ''), onClick: function () { setGdir('TB'); setFitTick(fitTick + 1) } }, '上下'),
1271
+ h('span', { className: 'nkb-chip' + (gdir === 'LR' ? ' on' : ''), onClick: function () { setGdir('LR'); setFitTick(fitTick + 1) } }, '左右'),
1272
+ ),
1273
+ h('div', { className: 'lbl' }, '展开层数'),
1274
+ h(
1275
+ 'select',
1276
+ {
1277
+ value: String(gdepth),
1278
+ style: { font: 'inherit', color: 'inherit', background: 'transparent', border: '1px solid rgba(128,128,128,.35)', borderRadius: 6, padding: '2px 6px' },
1279
+ onChange: function (e) { setGdepth(Number(e.target.value)); setFitTick(fitTick + 1) },
1280
+ },
1281
+ [2, 3, 4, 6, 99].map(function (d) {
1282
+ return h('option', { key: d, value: String(d) }, d >= 99 ? '全部' : d + ' 层')
1283
+ }),
1284
+ ),
1285
+ h('div', { className: 'lbl' }, '类型(点一下隐藏)'),
1286
+ h(
1287
+ 'div',
1288
+ null,
1289
+ typeNames.length
1290
+ ? typeNames.map(function (t) {
1291
+ return h(
1292
+ 'span',
1293
+ {
1294
+ key: t,
1295
+ className: 'nkb-chip' + (ghide[t] ? '' : ' on'),
1296
+ style: { borderColor: typeColor(t) },
1297
+ title: typeStats[t] + ' 个节点',
1298
+ onClick: function () {
1299
+ setGhide(function (prev) {
1300
+ var next = {}
1301
+ for (var k in prev) next[k] = prev[k]
1302
+ next[t] = !prev[t]
1303
+ return next
1304
+ })
1305
+ setFitTick(fitTick + 1)
1306
+ },
1307
+ },
1308
+ t + ' ' + typeStats[t],
1309
+ )
1310
+ })
1311
+ : h('span', { className: 'nkb-muted' }, '(无)'),
1312
+ ),
1313
+ h('div', { className: 'lbl' }, '操作'),
1314
+ h(
1315
+ 'div',
1316
+ { style: { display: 'flex', gap: 4, flexWrap: 'wrap' } },
1317
+ h('button', { onClick: function () { setGcoll({}); setFitTick(fitTick + 1) } }, '全部展开'),
1318
+ h('button', { onClick: function () { setGhide({}) } }, '显示全部类型'),
1319
+ h('button', { onClick: function () { setFitTick(fitTick + 1) } }, '适应视图'),
1320
+ ),
1321
+ h('div', { className: 'nkb-muted', style: { marginTop: 6, fontSize: 11 } }, '滚轮缩放 · 拖动平移 · 双击节点折叠'),
1322
+ )
1323
+
1324
+ var left = h(
1325
+ 'aside',
1326
+ { className: 'nkb-left' },
1327
+ h(
1328
+ 'div',
1329
+ { className: 'nkb-tree' },
1330
+ hits
1331
+ ? h(
1332
+ 'div',
1333
+ null,
1334
+ h(
1335
+ 'div',
1336
+ { className: 'nkb-muted', style: { padding: '2px 6px 6px' } },
1337
+ '命中 ' + hits.count + ' / ' + hits.total + ' 条',
1338
+ ),
1339
+ (hits.hits || []).map(function (hit, i) {
1340
+ return h(
1341
+ 'div',
1342
+ { key: hit.tid + '#' + i, className: 'nkb-hit', onClick: function () { open(hit.tid) } },
1343
+ h('div', { className: 'tt' }, hit.title || hit.tid),
1344
+ h('div', { className: 'nkb-muted' }, hit.tid + ' · ' + hit.type),
1345
+ h('div', { className: 'sn' }, hit.snippet || ''),
1346
+ )
1347
+ }),
1348
+ )
1349
+ : treeNode
1350
+ ? h(TreeRow, { node: treeNode, onSelect: open, sel: sel, expanded: expanded, toggle: toggle, depth: 0 })
1351
+ : h('div', { className: 'nkb-muted', style: { padding: 8 } }, tree ? '(空库)' : '载入中…'),
1352
+ ),
1353
+ settings,
1354
+ )
1355
+
1356
+ var tabs = h(
1357
+ 'div',
1358
+ { className: 'nkb-tabs' },
1359
+ [['subtree', '整支'], ['node', '本节点'], ['raw', '原文'], ['source', '源码']].map(function (pair) {
1360
+ return h(
1361
+ 'button',
1362
+ { key: pair[0], className: view === pair[0] ? 'on' : '', onClick: function () { setView(pair[0]) } },
1363
+ pair[1],
1364
+ )
1365
+ }),
1366
+ info ? h('span', { className: 'nkb-muted', style: { marginLeft: 'auto', paddingRight: 4 } }, (info.section || info.type) + ' · ' + text.length + ' 字符') : null,
1367
+ )
1368
+
1369
+ var body = diagData
1370
+ ? h(
1371
+ 'div',
1372
+ { className: 'nkb-diag' },
1373
+ h('div', null, '诊断:' + (diagData.errors && diagData.errors.length ? diagData.errors.length + ' 错误' : '无错误') + ' · ' + (diagData.warnings && diagData.warnings.length ? diagData.warnings.length + ' 告警' : '无告警')),
1374
+ ...(diagData.errors || []).map(function (t, i) { return h('div', { key: 'e' + i }, '✗ ' + t) }),
1375
+ ...(diagData.warnings || []).map(function (t, i) { return h('div', { key: 'w' + i, className: 'nkb-muted' }, '! ' + t) }),
1376
+ diagData.rename ? h('pre', { style: { whiteSpace: 'pre-wrap', margin: '8px 0 0' } }, diagData.rename) : null,
1377
+ h('div', { style: { marginTop: 8 } }, h('button', { onClick: function () { setDiag(null) } }, '关闭')),
1378
+ )
1379
+ : err
1380
+ ? h(
1381
+ 'div',
1382
+ { className: 'nkb-diag nkb-err' },
1383
+ h('div', null, err),
1384
+ wsList.length
1385
+ ? h(
1386
+ 'div',
1387
+ { style: { marginTop: 10 } },
1388
+ h('div', { className: 'nkb-muted' }, '选一个 DSH 项目目录:'),
1389
+ wsList.map(function (w) {
1390
+ return h(
1391
+ 'div',
1392
+ { key: w.id || w.path, style: { marginTop: 6 } },
1393
+ h('button', { onClick: function () { useWs(w.path) } }, (w.title || w.path) + (w.nodekb ? ' · 有 .nodekb' : ' · 无 .nodekb')),
1394
+ h('span', { className: 'nkb-muted', style: { marginLeft: 8 } }, w.path),
1395
+ )
1396
+ }),
1397
+ )
1398
+ : null,
1399
+ )
1400
+ : view === 'raw' || view === 'source' || view === 'toc'
1401
+ ? h(Rich, { text: shownText, onOpen: open })
1402
+ : h(Md, { text: shownText, onOpen: open })
1403
+ var cutNotice = textCut
1404
+ ? h(
1405
+ 'div',
1406
+ { className: 'nkb-cut' },
1407
+ '已显示前 ' + TEXT_LIMIT.toLocaleString() + ' 字符(全文 ' + text.length.toLocaleString() + ' 字符);需要完整内容请用 kb_read 的 maxlen / mode=node',
1408
+ )
1409
+ : null
1410
+
1411
+ var center = h(
1412
+ 'section',
1413
+ { className: 'nkb-main' },
1414
+ treeNode
1415
+ ? h(Graph, {
1416
+ root: treeNode,
1417
+ sel: sel,
1418
+ onSelect: open,
1419
+ dir: gdir,
1420
+ maxDepth: gdepth,
1421
+ hidden: ghide,
1422
+ collapsed: gcoll,
1423
+ fitTick: fitTick,
1424
+ onToggleCollapse: function (tid) {
1425
+ setGcoll(function (prev) {
1426
+ var next = {}
1427
+ for (var k in prev) next[k] = prev[k]
1428
+ next[tid] = !prev[tid]
1429
+ return next
1430
+ })
1431
+ },
1432
+ })
1433
+ : h('div', { className: 'nkb-muted', style: { padding: 16 } }, tree ? '空库' : '载入中…'),
1434
+ )
1435
+
1436
+ var props = h(
1437
+ 'div',
1438
+ { className: 'nkb-props' },
1439
+ info
1440
+ ? h(
1441
+ 'div',
1442
+ null,
1443
+ h('div', { className: 'nkb-sec', style: { marginTop: 0 } }, '节点属性'),
1444
+ kv('tid', info.tid),
1445
+ kv('类型', info.type),
1446
+ kv('标题', info.title),
1447
+ kv('父', info.parent_tid),
1448
+ kv('源', info.source),
1449
+ kv('字符', String((info.text || '').length)),
1450
+ info.children && info.children.length ? kv('子节', info.children.length + ' 个') : null,
1451
+ info.kind === 'file'
1452
+ ? h('div', { style: { margin: '6px 0' } }, h('button', { onClick: rename, disabled: busy, title: '文件改名并改写全库 [[TID]] 引用' }, '改名'))
1453
+ : null,
1454
+ h('div', { className: 'nkb-sec' }, '被引用'),
1455
+ back === null
1456
+ ? h('div', { className: 'nkb-muted' }, '…')
1457
+ : back.length
1458
+ ? back.map(function (b, i) {
1459
+ return h('div', { key: i }, h('span', { className: 'nkb-link', onClick: function () { open(b.tid) } }, b.tid))
1460
+ })
1461
+ : h('div', { className: 'nkb-muted' }, '没有节点引用它'),
1462
+ )
1463
+ : h('div', { className: 'nkb-muted' }, '选一个节点看详情'),
1464
+ tree && tree.problems && tree.problems.length
1465
+ ? h('div', null, h('div', { className: 'nkb-sec' }, 'TID 冲突'), h('div', { className: 'nkb-err' }, tree.problems.join('\n')))
1466
+ : null,
1467
+ )
1468
+
1469
+ var right = h('aside', { className: 'nkb-right' }, h('div', { className: 'nkb-content' }, tabs, cutNotice, body), props)
1470
+
1471
+ return h('div', { className: 'nkb-root' }, head, h('div', { className: 'nkb-body' }, left, center, right))
1472
+ }
1473
+
1474
+ function kv(k, v) {
1475
+ if (v === undefined || v === null || v === '') return null
1476
+ return h('div', { className: 'nkb-kv' }, h('span', { className: 'k' }, k), h('span', { className: 'v' }, String(v)))
1477
+ }
1478
+
1479
+ function makeIcon(React_) {
1480
+ return function NodekbIcon(props) {
1481
+ return h(Icon, props)
1482
+ }
1483
+ }
1484
+
1485
+ // ── 装配 ──────────────────────────────────────────────────────────────
1486
+ function apply(ctx) {
1487
+ var disposers = []
1488
+ var started = Date.now()
1489
+ var done = {}
1490
+
1491
+ // 槽位注册逐个兜底:重复注册(热更/双载)只告警,不让整个客户端半边失败。
1492
+ function reg(label, fn) {
1493
+ try {
1494
+ var d = fn()
1495
+ if (typeof d === 'function') disposers.push(d)
1496
+ done[label] = 'ok'
1497
+ } catch (e) {
1498
+ done[label] = 'failed: ' + String((e && e.message) || e)
1499
+ report({ phase: 'register', label: label, error: String((e && e.message) || e) })
1500
+ try { console.warn('[project-node-memory] 槽位注册失败(' + label + '):', e) } catch (_) { /* 控制台不可用 */ }
1501
+ }
1502
+ }
1503
+
1504
+ function slotsOf() {
1505
+ if (ctx && ctx.slots && typeof ctx.slots.register === 'function') return ctx.slots
1506
+ try {
1507
+ if (ctx && typeof ctx.get === 'function') {
1508
+ var s = ctx.get('slots')
1509
+ if (s && typeof s.register === 'function') return s
1510
+ }
1511
+ } catch (e) { /* 服务未就绪 */ }
1512
+ return null
1513
+ }
1514
+
1515
+ // `main` 与 `sidebar.panellist` 都可能比本插件晚声明(DSH 的槽位按条目
1516
+ // 的 children 表声明,声明前 register 会抛 "is not declared")。
1517
+ // 官方第三方插件(dsh-tiddlywiki)用的是 slots.inject(name, cb):声明已
1518
+ // 存在就同步跑,否则等到声明时再跑。这里同样走 inject,并保留轮询兜底。
1519
+ function regHere(label, register) {
1520
+ try {
1521
+ var d = register()
1522
+ done[label] = 'ok'
1523
+ return d
1524
+ } catch (e) {
1525
+ done[label] = 'failed: ' + String((e && e.message) || e)
1526
+ report({ phase: 'register', label: label, error: String((e && e.message) || e) })
1527
+ try { console.warn('[project-node-memory] 槽位注册失败(' + label + '):', e) } catch (_) { /* 控制台不可用 */ }
1528
+ return undefined
1529
+ }
1530
+ }
1531
+
1532
+ function whenDeclared(slots, name, label, register) {
1533
+ try {
1534
+ if (typeof slots.inject === 'function') {
1535
+ var remove = slots.inject(name, function () {
1536
+ var d = regHere(label, register)
1537
+ report({ phase: 'registered', label: label, at: Date.now() - started })
1538
+ return d
1539
+ })
1540
+ if (typeof remove === 'function') disposers.push(remove)
1541
+ if (typeof remove !== 'undefined') {
1542
+ done[label] = 'armed' // 已挂到声明钩子上,等声明时自动注册
1543
+ return true
1544
+ }
1545
+ }
1546
+ } catch (e) {
1547
+ report({ phase: 'inject', label: label, error: String((e && e.message) || e) })
1548
+ }
1549
+ // 没有 inject(旧 SDK)就当场注册一次,失败留给轮询重试。
1550
+ regHere(label, register)
1551
+ return true
1552
+ }
1553
+
1554
+ function attempt() {
1555
+ var slots = slotsOf()
1556
+ if (!slots) return false
1557
+ if (!done['sidebar.panellist'] || done['sidebar.panellist'] === 'retry') {
1558
+ whenDeclared(slots, 'sidebar.panellist', 'sidebar.panellist', function () {
1559
+ return slots.register({ name: 'sidebar.panellist', id: 'nodekb', order: 50, label: function () { return SIDEBAR_LABEL } }, makeIcon())
1560
+ })
1561
+ }
1562
+ if (!done['main'] || done['main'] === 'retry') {
1563
+ whenDeclared(slots, 'main', 'main', function () {
1564
+ return slots.register({ name: 'main', key: 'nodekb' }, Panel)
1565
+ })
1566
+ }
1567
+ return true
1568
+ }
1569
+
1570
+ function settled(label) {
1571
+ var s = done[label]
1572
+ return s === 'ok' || s === 'armed'
1573
+ }
1574
+
1575
+ var attempts = 0
1576
+ function tick() {
1577
+ attempts++
1578
+ var ok = attempt()
1579
+ if (ok && settled('sidebar.panellist') && settled('main')) {
1580
+ report({ phase: 'apply', ok: true, id: PKG_ID, label: SIDEBAR_LABEL, attempts: attempts, ms: Date.now() - started, done: done })
1581
+ return
1582
+ }
1583
+ if (attempts >= 100) {
1584
+ report({
1585
+ phase: 'apply',
1586
+ ok: false,
1587
+ attempts: attempts,
1588
+ reason: '10 秒内槽位没有声明齐全',
1589
+ hasSlots: !!slotsOf(),
1590
+ hasInject: !!(slotsOf() && typeof slotsOf().inject === 'function'),
1591
+ done: done,
1592
+ })
1593
+ return
1594
+ }
1595
+ try { setTimeout(tick, 100) } catch (e) { /* 无定时器 */ }
1596
+ }
1597
+
1598
+ report({
1599
+ phase: 'apply-start',
1600
+ hasCtx: !!ctx,
1601
+ hasSlots: !!(ctx && ctx.slots),
1602
+ ctxKeys: ctx && typeof ctx === 'object' ? Object.keys(ctx).slice(0, 40) : null,
1603
+ })
1604
+ tick()
1605
+ return function () {
1606
+ for (var i = 0; i < disposers.length; i++) {
1607
+ try { disposers[i]() } catch (e) { /* 卸载期忽略 */ }
1608
+ }
1609
+ }
1610
+ }
1611
+
1612
+ return {
1613
+ apply: apply,
1614
+ inject: ['slots'],
1615
+ Panel: Panel,
1616
+ Icon: Icon,
1617
+ // 离线自检用:不装 DSH 也能直接驱动布局函数
1618
+ __internals: { layoutGraph: layoutGraph, edgePath: edgePath, collectTypes: collectTypes, Graph: Graph, NODE_W: NODE_W, NODE_H: NODE_H, mdBlocks: mdBlocks, mdInline: mdInline, Md: Md, typeColor: typeColor, texToMathML: texToMathML, safeTex: safeTex },
1619
+ }
1620
+ }
1621
+ })()