project-node-memory 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +204 -2
- package/README_EN.md +201 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1621 -0
- package/lib/engine/commands.js +84 -0
- package/lib/engine/diag.js +220 -0
- package/lib/engine/mdlib.js +283 -0
- package/lib/engine/parse.js +67 -0
- package/lib/engine/registry.js +241 -0
- package/lib/engine/rename.js +203 -0
- package/lib/engine/render.js +138 -0
- package/lib/engine/scaffold.js +123 -0
- package/lib/engine/search.js +138 -0
- package/lib/engine/write.js +140 -0
- package/lib/host/prompt.js +111 -0
- package/lib/host/routes.js +304 -0
- package/lib/host/runtime.js +108 -0
- package/lib/host/skill.js +160 -0
- package/lib/host/tools.js +475 -0
- package/lib/index.js +60 -0
- package/package.json +62 -4
- package/skills/project-node-memory-guide/SKILL.md +59 -0
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
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
|
+
})()
|