@sparkelf/dsh-knowledge-base 0.1.25 → 0.1.26
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 +1074 -741
- package/lib/client.js.map +1 -1
- package/lib/index.js +32 -7
- package/lib/types/client/KnowledgeFilePreview.d.ts +32 -0
- package/lib/types/client/graph/KnowledgeGraphTree.d.ts +11 -9
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/graph.d.ts +12 -0
- package/lib/types/graphCanvas.d.ts +1 -1
- package/lib/types/graphTools.d.ts +32 -0
- package/lib/types/graphTypes.d.ts +9 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/views.d.ts +10 -0
- package/package.json +1 -1
- package/skills/knowledge-graph/SKILL.md +96 -0
package/lib/index.js
CHANGED
|
@@ -1548,7 +1548,8 @@ async function readGraphLinks(graphPath, topicIds) {
|
|
|
1548
1548
|
b,
|
|
1549
1549
|
...label === "" ? {} : { label },
|
|
1550
1550
|
...(meta.updated ?? "").trim() === "" ? {} : { updated: (meta.updated ?? "").trim() },
|
|
1551
|
-
body: firstSummary(body)
|
|
1551
|
+
body: firstSummary(body),
|
|
1552
|
+
file: file.name
|
|
1552
1553
|
});
|
|
1553
1554
|
}
|
|
1554
1555
|
return links;
|
|
@@ -1562,12 +1563,13 @@ async function readGraphLinks(graphPath, topicIds) {
|
|
|
1562
1563
|
* canvas.
|
|
1563
1564
|
*/
|
|
1564
1565
|
async function readGraph(actualRoot, root, folder, caller) {
|
|
1565
|
-
const
|
|
1566
|
+
const actualGraphRel = viewJoin(root.actualPath, folder);
|
|
1567
|
+
const actualGraphPath = under(actualRoot, actualGraphRel);
|
|
1566
1568
|
if (actualGraphPath === void 0) return void 0;
|
|
1567
1569
|
const viewGraphPath = viewJoin(root.viewPath, folder);
|
|
1568
1570
|
const topics = [];
|
|
1569
1571
|
for (const directory of (await visibleDirectories(actualGraphPath)).slice(0, MAX_GRAPH_TOPICS)) {
|
|
1570
|
-
if (directory.name === ".kg") continue;
|
|
1572
|
+
if (directory.name === ".kg" || directory.name === "_trash") continue;
|
|
1571
1573
|
const topic = await readGraphTopic(actualGraphPath, viewGraphPath, directory.name);
|
|
1572
1574
|
if (topic !== void 0) topics.push(topic);
|
|
1573
1575
|
}
|
|
@@ -1578,6 +1580,7 @@ async function readGraph(actualRoot, root, folder, caller) {
|
|
|
1578
1580
|
scope: root.key,
|
|
1579
1581
|
path: viewGraphPath,
|
|
1580
1582
|
group: root.groupLabel,
|
|
1583
|
+
actualPath: actualGraphRel,
|
|
1581
1584
|
topics,
|
|
1582
1585
|
links,
|
|
1583
1586
|
counts: {
|
|
@@ -1672,7 +1675,7 @@ async function scanKnowledgeGraphs(options) {
|
|
|
1672
1675
|
*
|
|
1673
1676
|
* @module @sparkelf/dsh-knowledge-base/graph-canvas
|
|
1674
1677
|
*/
|
|
1675
|
-
const GRAPH_CANVAS_HTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>知识图谱 · 画布</title>\n<style>\n:root{\n --bg:#fbfbfc;\n --bg-soft:#f6f7f9;\n --surface:#ffffff;\n --ink:#0f1115;\n --ink-2:#3c3f47;\n --secondary:#61666b;\n --muted:#81858c;\n --line:rgba(0,0,0,.10);\n --line-2:rgba(0,0,0,.06);\n --blue:#4176e6;\n --blue-deep:#2555c4;\n --blue-soft:#eef4ff;\n --green:#22c55e;\n --amber:#f59e0b;\n --radius:12px;\n --shadow:0 1px 2px rgba(15,17,21,.04), 0 8px 24px rgba(15,17,21,.06);\n --sans:-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;\n --mono:\"SF Mono\",\"JetBrains Mono\",Menlo,Consolas,monospace;\n}\n*{box-sizing:border-box}\nhtml,body{height:100%}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.6;overflow:hidden;-webkit-font-smoothing:antialiased}\nbutton{font-family:inherit;color:inherit}\n\n#stage{position:fixed;left:248px;right:0;top:0;bottom:0;overflow:hidden;cursor:grab;\n background:\n radial-gradient(900px 620px at 50% 38%, #ffffff 0%, rgba(255,255,255,0) 70%),\n var(--bg)}\n#stage::before{content:\"\";position:absolute;inset:0;pointer-events:none;\n background-image:radial-gradient(rgba(15,17,21,.055) 1px, transparent 1px);\n background-size:22px 22px;\n -webkit-mask-image:radial-gradient(ellipse 70% 70% at 50% 45%, #000 0%, transparent 78%);\n mask-image:radial-gradient(ellipse 70% 70% at 50% 45%, #000 0%, transparent 78%)}\n#stage.grabbing{cursor:grabbing}\n#world{position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform}\n#edges{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}\n\n#topbar{position:fixed;left:248px;right:0;top:0;display:flex;align-items:flex-start;justify-content:space-between;\n padding:18px 22px;pointer-events:none;z-index:10}\n#brand h1{margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em}\n#brand p{margin:4px 0 0;font-size:12px;color:var(--muted)}\n#brand b{color:var(--secondary);font-weight:500}\n#hint{font-size:11.5px;color:var(--muted);text-align:right;line-height:1.9;transition:opacity .2s}\n#hint kbd{font-family:var(--mono);font-size:10.5px;border:1px solid var(--line);border-radius:4px;\n padding:1px 5px;color:var(--secondary);background:var(--surface)}\nbody.panel-open #hint{opacity:0}\n\n.card{position:absolute;width:300px;padding:15px 16px 13px;border-radius:var(--radius);cursor:grab;\n user-select:none;background:var(--surface);border:1px solid var(--line);box-shadow:var(--shadow);\n transition:border-color .16s,box-shadow .16s,opacity .16s}\n.card:hover{border-color:rgba(0,0,0,.18)}\n.card.dragging{cursor:grabbing;transition:none}\n.card.selected{border-color:var(--blue);\n box-shadow:0 0 0 2px rgba(65,118,230,.14),0 10px 28px rgba(15,17,21,.10)}\n.card.dim{opacity:.38}\n.card:focus-visible{outline:2px solid var(--blue);outline-offset:3px}\n.chip{font-size:11px;letter-spacing:.02em;color:var(--secondary);background:var(--bg-soft);\n border:1px solid var(--line-2);border-radius:6px;padding:1px 7px}\n.status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}\n.dot{width:6px;height:6px;border-radius:50%;background:var(--green)}\n.dot.paused{background:var(--amber)}\n.dot.done{background:#c4c7cc}\n.card h3{margin:0 0 9px;font-size:17px;font-weight:600;line-height:1.35;letter-spacing:-.01em}\n.cover{position:relative;height:124px;margin:0 0 10px}\nbody[data-doc-layout=\"landscape\"] .cover{height:86px;margin-bottom:9px}\nbody[data-doc-layout=\"landscape\"] .doc{width:132px;height:76px;border-radius:5px}\nbody[data-doc-layout=\"landscape\"] .doc-empty{width:140px;height:80px}\n.stack{position:absolute;inset:0}\n.doc{position:absolute;left:50%;top:50%;width:86px;height:116px;border-radius:6px;\n border:1px solid rgba(0,0,0,.08);box-shadow:0 8px 18px rgba(15,17,21,.14);background:#fff;\n object-fit:cover;\n transform:translate(-50%,-50%) translate(var(--dx),var(--dy)) rotate(var(--rot)) scale(var(--sc));\n transition:transform .22s cubic-bezier(.22,1,.36,1)}\n.cover:hover .doc{transform:translate(-50%,-50%) translate(calc(var(--dx) * 1.4),var(--dy)) rotate(calc(var(--rot) * 1.35)) scale(var(--sc))}\n.doc-empty{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:96px;height:120px;\n border:1px dashed var(--line);border-radius:6px;display:flex;align-items:center;justify-content:center;\n color:#b3b7bd;font-size:10.5px;background:var(--bg-soft)}\n.more{position:absolute;right:2px;bottom:0;font-size:10px;color:var(--muted);background:var(--surface);\n border:1px solid var(--line-2);border-radius:999px;padding:0 6px}\n.tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px;margin-bottom:2px}\n.tags span{font-size:10px;color:var(--secondary);background:var(--bg-soft);border:1px solid var(--line-2);\n border-radius:999px;padding:1px 7px}\n.one{margin:0;font-size:12.5px;line-height:1.65;color:var(--ink-2);\n display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n\n.dots{display:inline-flex;gap:3px}\n.dots i{width:5px;height:5px;border-radius:50%;background:#dcdfe3}\n.dots i.on{background:var(--ink)}\n\n.edge{fill:none;stroke:rgba(15,17,21,.32);stroke-width:1.2}\n.edge.cross{stroke:rgba(15,17,21,.18);stroke-dasharray:4 6}\n.edge.hot{stroke:var(--blue);stroke-width:1.6}\n.edge.cold{opacity:.22}\n\n.pill{cursor:pointer;pointer-events:auto}\n.pill rect{fill:var(--surface);stroke:var(--line);stroke-width:1;transition:stroke .16s,fill .16s}\n.pill text{font:10.5px var(--sans);fill:var(--secondary);letter-spacing:.01em}\n.pill:hover rect{stroke:rgba(0,0,0,.28)}\n.pill.hot rect{stroke:var(--blue);fill:var(--blue-soft)}\n.pill.hot text{fill:var(--blue-deep)}\n.pill.cold{opacity:.25}\n.pill:focus-visible{outline:none}\n.pill:focus-visible rect{stroke:var(--blue);stroke-width:1.6}\n\n#controls{position:fixed;left:266px;bottom:18px;display:flex;flex-direction:column;gap:6px;z-index:12}\n#controls button{width:34px;height:34px;border-radius:8px;border:1px solid var(--line);\n background:var(--surface);color:var(--ink-2);font-size:15px;line-height:1;cursor:pointer;\n box-shadow:0 1px 2px rgba(15,17,21,.04);transition:background .15s,color .15s}\n#controls button:hover{background:var(--bg-soft);color:var(--ink)}\n#controls button.on{background:var(--ink);color:#fff;border-color:var(--ink)}\n#controls button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}\n#zoomLabel{position:fixed;left:308px;bottom:23px;font-family:var(--mono);font-size:10.5px;\n color:var(--muted);z-index:12}\n\n#panel{position:fixed;top:0;right:0;width:392px;height:100%;z-index:30;\n background:var(--surface);border-left:1px solid var(--line);\n box-shadow:-8px 0 28px rgba(15,17,21,.06);\n transform:translateX(102%);transition:transform .28s cubic-bezier(.22,1,.36,1);\n display:flex;flex-direction:column}\n#panel.open{transform:none}\n.p-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;\n padding:22px 22px 15px;border-bottom:1px solid var(--line-2)}\n.p-kicker{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}\n.p-head h2{margin:7px 0 0;font-size:22px;font-weight:600;line-height:1.35;letter-spacing:-.01em}\n#close{width:30px;height:30px;flex:0 0 auto;border-radius:8px;border:1px solid var(--line);\n background:var(--surface);color:var(--muted);font-size:16px;cursor:pointer;line-height:1}\n#close:hover{color:var(--ink);background:var(--bg-soft)}\n.p-body{padding:16px 22px 40px;overflow:auto}\n.p-field{margin:0 0 14px}\n.p-field b{display:block;font-size:11px;letter-spacing:.08em;text-transform:uppercase;\n color:var(--muted);font-weight:500;margin-bottom:3px}\n.p-field span{display:block;font-size:13px;line-height:1.7;color:var(--ink-2)}\n.p-rel-title{margin:24px 0 9px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}\n.rel{display:block;width:100%;text-align:left;background:var(--bg-soft);\n border:1px solid var(--line-2);border-radius:8px;padding:10px 11px;margin:0 0 7px;cursor:pointer;\n transition:background .15s,border-color .15s}\n.rel:hover{background:#f1f3f5;border-color:var(--line)}\n.rel b{display:block;font-size:12.5px;color:var(--ink);margin-bottom:2px;font-weight:600}\n.rel span{display:block;font-size:11.5px;line-height:1.6;color:var(--muted);\n display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.p-docs{font-size:11.5px;color:var(--muted);margin-top:12px}\n.p-doc-item{display:flex;gap:10px;align-items:center;padding:7px 0;border-bottom:1px solid var(--line-2)}\n.p-doc-item img{width:34px;height:46px;border-radius:3px;border:1px solid var(--line-2);object-fit:cover;flex:0 0 auto}\n.p-doc-item .m b{display:block;font-size:12.5px;font-weight:600;color:var(--ink)}\n.p-doc-item .m span{display:block;font-size:10.5px;color:var(--muted)}\n.p-doc{font-size:13px;line-height:1.8;color:var(--ink-2)}\n.p-doc h2{font-size:14px;font-weight:600;color:var(--ink);margin:22px 0 6px}\n.p-doc h3{font-size:13px;font-weight:600;color:var(--ink);margin:18px 0 5px}\n.p-doc p{margin:0 0 10px}\n.p-doc ul,.p-doc ol{margin:0 0 10px;padding-left:18px}\n.p-doc li{margin:2px 0}\n.p-doc blockquote{margin:0 0 10px;padding:2px 0 2px 12px;border-left:2px solid var(--line);color:var(--muted)}\n.p-doc code{font-family:var(--mono);font-size:.86em;background:var(--bg-soft);border:1px solid var(--line-2);padding:0 4px;border-radius:4px}\n.p-doc pre{margin:0 0 10px;padding:10px 12px;background:var(--bg-soft);border:1px solid var(--line-2);border-radius:8px;overflow:auto}\n.p-doc pre code{background:none;border:0;padding:0}\n.p-doc strong{color:var(--ink);font-weight:600}\n.p-doc a{color:var(--blue);text-decoration:none}\n\n\n/* ---------- tree ---------- */\n#tree{position:fixed;left:0;top:0;bottom:0;width:248px;z-index:20;display:flex;flex-direction:column;\n background:var(--surface);border-right:1px solid var(--line)}\n.tree-head{padding:18px 16px 13px;border-bottom:1px solid var(--line-2)}\n.tree-title{font-size:14px;font-weight:600}\n.tree-sub{font-size:11px;color:var(--muted);margin-top:2px}\n#treeBody{flex:1;overflow:auto;padding:8px 8px 20px}\n.tree-scope{font-size:10.5px;letter-spacing:.1em;color:var(--muted);padding:12px 10px 5px}\n.tree-group{font-size:11.5px;color:var(--secondary);padding:6px 10px 3px}\n.tree-graph{display:flex;align-items:center;justify-content:space-between;gap:8px;\n padding:5px 10px;border-radius:6px;font-size:12.5px;color:var(--ink-2);cursor:pointer}\n.tree-graph:hover{background:var(--bg-soft)}\n.tree-graph.active{background:var(--blue-soft);color:var(--blue-deep)}\n.tree-graph .n{font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}\n.tree-graph.active .n{color:var(--blue)}\n.tree-topic{padding:4px 10px 4px 24px;font-size:12px;color:var(--muted);border-radius:6px;cursor:pointer;\n white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.tree-topic:hover{background:var(--bg-soft);color:var(--ink)}\n.tree-empty{padding:2px 10px 4px 24px;font-size:11.5px;color:#b3b7bd}\n\n@media (max-width:760px){\n #tree{display:none}\n #stage{left:0}\n #topbar{left:0}\n #controls{left:18px}\n #zoomLabel{left:60px}\n #hint{display:none}\n .only-wide{display:none}\n #brand h1{font-size:15px}\n .card{width:270px}\n #panel{width:100%;height:72%;top:auto;bottom:0;border-left:0;border-top:1px solid var(--line);\n transform:translateY(102%);border-radius:16px 16px 0 0}\n #panel.open{transform:none}\n}\n</style>\n<style>\n:root{\n --bg:#151517; --bg-soft:#232324; --surface:#232324; --ink:#f5f5f7; --ink-2:#cfd3d6;\n --secondary:#adb2b8; --muted:#8a8d95; --line:rgba(255,255,255,.12); --line-2:rgba(255,255,255,.06);\n --blue:#4d93f8; --blue-deep:#9db9ff; --blue-soft:rgba(77,147,248,.14);\n --green:#22c55e; --amber:#f59e0b;\n --shadow:0 1px 2px rgba(0,0,0,.3), 0 10px 30px rgba(0,0,0,.28);\n}\nhtml,body{height:100%}\nbody{display:flex;overflow:hidden;background:var(--bg);color:var(--ink)}\n#rail{width:280px;flex:0 0 280px;background:#141416;border-right:.5px solid rgba(255,255,255,.10);\n display:flex;flex-direction:column;min-height:0}\n.rail-top{height:36px;flex:0 0 36px;display:flex;align-items:center;gap:8px;padding:0 6px 0 12px}\n.rail-brand{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;color:var(--ink)}\n.rail-brand svg{width:16px;height:16px;color:var(--secondary)}\n.rail-fold{margin-left:auto;width:28px;height:28px;border:0;background:none;border-radius:6px;\n color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center}\n.rail-fold:hover{background:rgba(255,255,255,.06);color:var(--ink)}\n.rail-search{margin:6px 12px 4px;height:32px;display:flex;align-items:center;gap:8px;padding:0 10px;\n background:#1f1f22;border:.5px solid rgba(255,255,255,.10);border-radius:8px}\n.rail-search svg{width:14px;height:14px;color:var(--muted)}\n.rail-search input{flex:1;min-width:0;background:none;border:0;outline:none;color:var(--ink);font-size:12.5px}\n.rail-search input::placeholder{color:#6f737a}\n.rail-nav{padding:4px 8px;display:flex;flex-direction:column;gap:1px}\n.nav-item{height:34px;display:flex;align-items:center;gap:10px;padding:0 10px;border:0;border-radius:6px;\n background:none;color:var(--secondary);font-size:13.5px;text-align:left;cursor:pointer}\n.nav-item svg{width:15px;height:15px;color:var(--muted)}\n.nav-item:hover{background:rgba(255,255,255,.06)}\n.nav-item.active{background:rgba(255,255,255,.08);color:var(--ink)}\n.nav-item.active svg{color:var(--secondary)}\n.tree-head{height:36px;flex:0 0 36px;display:flex;align-items:center;gap:4px;padding:0 8px 0 12px}\n.rail-tabs{display:flex;align-items:center;gap:18px;height:36px}\n.rail-tab{height:36px;padding:0;border:0;background:none;color:var(--muted);font-size:14px;\n cursor:pointer;position:relative}\n.rail-tab:hover{color:var(--secondary)}\n.rail-tab.active{color:var(--ink)}\n.rail-tab.active::after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:2px;\n border-radius:1px;background:var(--blue)}\n.tree-tools{margin-left:auto;display:flex;align-items:center;gap:2px}\n.tree-tools button{width:28px;height:28px;border:0;background:none;border-radius:6px;\n color:var(--secondary);display:flex;align-items:center;justify-content:center;cursor:pointer}\n.tree-tools button:hover{background:rgba(255,255,255,.06);color:var(--ink)}\n#treeBody{flex:1;min-height:0;overflow:auto;padding:4px 8px 16px}\n.tree-scope{font-size:10.5px;letter-spacing:.1em;color:var(--muted);padding:10px 10px 4px}\n.tree-group{font-size:11.5px;color:var(--secondary);padding:4px 10px 2px}\n.tree-graph{display:flex;justify-content:space-between;align-items:center;height:28px;padding:0 10px;\n border-radius:6px;font-size:12.5px;color:var(--secondary);cursor:pointer}\n.tree-graph:hover{background:rgba(255,255,255,.06)}\n.tree-graph.active{background:var(--blue-soft);color:var(--blue-deep)}\n.tree-graph .n{font-size:10.5px;color:#6f737a;font-variant-numeric:tabular-nums}\n.tree-topic{height:26px;display:flex;align-items:center;padding:0 10px 0 24px;border-radius:6px;\n font-size:12px;color:var(--muted);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.tree-topic:hover{background:rgba(255,255,255,.06);color:var(--ink)}\n.tree-empty{padding:2px 10px 4px 24px;font-size:11.5px;color:#5f636a}\n.tree-kb{display:flex;align-items:center;gap:8px;height:28px;padding:0 10px;border-radius:6px;\n font-size:12.5px;color:var(--secondary);cursor:pointer}\n.tree-kb:hover{background:rgba(255,255,255,.06)}\n.tree-kb svg{width:14px;height:14px;color:#d9a441;flex:0 0 auto}\n.tree-kb.file svg{color:#8a8d95}\n.tree-kb .date{margin-left:auto;font-size:10.5px;color:#6f737a}\n#main{flex:1;min-width:0;display:flex;flex-direction:column;position:relative;background:#1a1a1c}\n#mainhead{height:48px;flex:0 0 48px;display:flex;align-items:center;gap:12px;padding:0 18px;\n border-bottom:.5px solid rgba(255,255,255,.10)}\n.crumb{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);min-width:0}\n.crumb b{color:var(--ink);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.crumb i{font-style:normal;color:#5f636a}\n#headMeta{font-size:11.5px;color:#7f838a;white-space:nowrap}\n.head-actions{margin-left:auto;display:flex;gap:8px}\n.head-actions button{height:28px;padding:0 10px;border:.5px solid rgba(255,255,255,.12);border-radius:6px;\n background:#232324;color:var(--secondary);font-size:12px;cursor:pointer}\n.head-actions button:hover{background:#2c2c2e;color:var(--ink)}\n#stage{position:relative;left:auto;right:auto;top:auto;bottom:auto;flex:1;min-height:0;overflow:hidden;cursor:grab;\n background:radial-gradient(900px 620px at 50% 38%,rgba(255,255,255,.03),transparent 70%),#1a1a1c}\n#stage::before{background-image:radial-gradient(rgba(255,255,255,.055) 1px,transparent 1px)}\n#controls{position:absolute;left:16px;bottom:16px;z-index:12}\n#controls button{background:#232324;border-color:rgba(255,255,255,.12);color:var(--secondary)}\n#controls button:hover{background:#2c2c2e;color:var(--ink)}\n#controls button.on{background:var(--blue);border-color:var(--blue);color:#0b1220}\n#zoomLabel{position:absolute;left:60px;bottom:21px;color:var(--muted)}\n.card{background:#232324;border-color:rgba(255,255,255,.10);\n box-shadow:0 1px 2px rgba(0,0,0,.3),0 10px 30px rgba(0,0,0,.28)}\n.card:hover{border-color:rgba(255,255,255,.20)}\n.card.selected{border-color:var(--blue);box-shadow:0 0 0 2px rgba(77,147,248,.20),0 10px 30px rgba(0,0,0,.35)}\n.card.dim{opacity:.32}\n.card h3{color:var(--ink)}\n.one{color:var(--secondary)}\n.tags span{background:#2c2c2e;border-color:rgba(255,255,255,.08);color:var(--secondary)}\n.doc{border-color:rgba(255,255,255,.12);box-shadow:0 8px 18px rgba(0,0,0,.40)}\n.edge{stroke:rgba(255,255,255,.22)}\n.edge.cross{stroke:rgba(255,255,255,.12)}\n.edge.hot{stroke:var(--blue)}\n.pill rect{fill:#232324;stroke:rgba(255,255,255,.14)}\n.pill text{fill:var(--secondary)}\n.pill.hot rect{fill:rgba(77,147,248,.16);stroke:var(--blue)}\n.pill.hot text{fill:var(--blue-deep)}\n#panel{background:#1e1e21;border-left-color:rgba(255,255,255,.10);box-shadow:-8px 0 28px rgba(0,0,0,.40)}\n.p-head{border-bottom-color:rgba(255,255,255,.06)}\n.p-kicker{color:var(--muted)}\n.p-head h2{color:var(--ink)}\n#close{background:#232324;border-color:rgba(255,255,255,.12);color:var(--muted)}\n#close:hover{background:#2c2c2e;color:var(--ink)}\n.p-field b{color:var(--muted)}\n.p-field span{color:var(--secondary)}\n.p-doc{color:var(--secondary)}\n.p-doc h2,.p-doc h3{color:var(--ink)}\n.p-doc code{background:#2c2c2e;border-color:rgba(255,255,255,.08)}\n.p-doc pre{background:#1a1a1c;border-color:rgba(255,255,255,.08)}\n.rel{background:#262629;border-color:rgba(255,255,255,.07)}\n.rel:hover{background:#2c2c2e;border-color:rgba(255,255,255,.12)}\n.rel b{color:var(--ink)}\n.rel span{color:var(--muted)}\n.p-doc-item{border-bottom-color:rgba(255,255,255,.06)}\n.p-doc-item .m b{color:var(--ink)}\n.p-doc-item .m span{color:var(--muted)}\n#kbView{flex:1;min-height:0;overflow:auto;padding:18px}\n.kb-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}\n.kb-card{display:flex;gap:13px;align-items:center;height:63px;padding:0 13px;border:.5px solid rgba(255,255,255,.10);\n border-radius:10px;background:#232324;text-align:left;cursor:pointer;color:var(--ink)}\n.kb-card:hover{background:#262629}\n.kb-card .plate{width:22px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:13px;flex:0 0 auto}\n.kb-card b{display:block;font-size:13.5px;color:var(--ink);font-weight:600}\n.kb-card small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px}\n.kb-tabs{display:flex;gap:8px;margin:16px 0 6px;border-bottom:.5px solid rgba(255,255,255,.08)}\n.kb-tab{padding:8px 10px;font-size:12.5px;color:var(--muted);background:none;border:0;cursor:pointer;border-bottom:2px solid transparent}\n.kb-tab.active{color:var(--ink);border-bottom-color:var(--blue)}\n.kb-table{width:100%;border-collapse:collapse;font-size:12.5px}\n.kb-table th{text-align:left;font-weight:500;color:#7f838a;font-size:11px;padding:8px 10px;border-bottom:.5px solid rgba(255,255,255,.08)}\n.kb-table td{padding:9px 10px;border-bottom:.5px solid rgba(255,255,255,.06);color:var(--secondary)}\n.kb-table td.muted{color:var(--muted)}\n.kb-table tr:hover td{background:rgba(255,255,255,.03)}\n[hidden]{display:none !important}\n@media (max-width:900px){#rail{display:none}}\n</style>\n<style>\n/* ---- embedding page: no rail, no page header; the plugin owns both ---- */\n#main { width: 100%; }\n#kg-empty { position: absolute; inset: 0; z-index: 8; display: flex; flex-direction: column;\n align-items: center; justify-content: center; gap: 6px; background: #1a1a1c; color: #8a8d95;\n font-family: var(--sans); text-align: center; padding: 24px; }\n#kg-empty strong { font-size: 14px; font-weight: 600; color: #cfd3d6; }\n#kg-empty span { font-size: 12px; }\n</style>\n<style>\n/* ---- host theme bridge ----\n The canvas is a separate document, so it cannot inherit the host's custom\n properties. The parent posts every --dsw-alias-* value below into this page and\n writes it on :root. These fallbacks are only the first-paint values before that\n message lands; the live values always come from the host. */\n:root {\n --bg: var(--dsw-alias-bg-base, #fff);\n --bg-soft: var(--dsw-alias-bg-layer-1, #f6f7f9);\n --surface: var(--dsw-alias-bg-layer-2, #fff);\n --ink: var(--dsw-alias-label-primary, #0f1115);\n --ink-2: var(--dsw-alias-label-secondary, #3c3f47);\n --secondary: var(--dsw-alias-label-secondary, #61666b);\n /* 80% tertiary + 20% primary keeps the third level distinguishable while\n reaching 4.5:1 on the light surface (the raw light tertiary does not). */\n --muted: color-mix(in srgb, var(--dsw-alias-label-tertiary, #81858c) 80%, var(--dsw-alias-label-primary, #0f1115));\n --line: var(--dsw-alias-border-l2, rgba(0, 0, 0, .10));\n --line-2: var(--dsw-alias-border-l1, rgba(0, 0, 0, .06));\n --blue: var(--dsw-alias-state-business-primary, #4176e6);\n /* Accent text needs 4.5:1, and the raw link token is 4.2:1 on white. A 78/22 mix\n with the primary ink keeps the brand hue while clearing the body-text floor. */\n --blue-deep: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 78%, var(--dsw-alias-label-primary, #0f1115));\n --blue-soft: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 16%, transparent);\n --green: var(--dsw-alias-state-success-primary, #22c55e);\n --amber: var(--dsw-alias-state-warn-primary, #f59e0b);\n --shadow: var(--dsw-elevation-panel, 0 1px 2px rgba(0, 0, 0, .10));\n --stage-bg: color-mix(in srgb, var(--dsw-alias-bg-base, #fff) 96%, var(--dsw-alias-label-primary, #0f1115));\n --soft: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 6%, transparent);\n --dot: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 9%, transparent);\n --edge: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 24%, transparent);\n --edge-soft: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 14%, transparent);\n}\n\n#stage {\n background:\n radial-gradient(900px 620px at 50% 38%, color-mix(in srgb, var(--surface) 60%, transparent), transparent 70%),\n var(--stage-bg) !important;\n}\n#stage::before { background-image: radial-gradient(var(--dot) 1px, transparent 1px) !important; }\n#kg-empty { background: var(--stage-bg) !important; color: var(--muted) !important; }\n#kg-empty strong { color: var(--ink) !important; }\n.card:focus-visible, #controls button:focus-visible { outline-color: var(--blue) !important; }\n.p-doc-item img { border-color: var(--line) !important; }\n.card { background: var(--surface) !important; border-color: var(--line) !important; box-shadow: var(--shadow) !important; }\n.card:hover { border-color: color-mix(in srgb, var(--ink) 22%, transparent) !important; }\n.card.selected { border-color: var(--blue) !important; box-shadow: 0 0 0 2px var(--blue-soft), var(--shadow) !important; }\n/* Dimming is focus, not disappearance: 32% made secondary text unreadable. */\n.card.dim { opacity: .55 !important; }\n.card h3 { color: var(--ink) !important; }\n.one { color: var(--secondary) !important; font-size: 13px; line-height: 1.6; }\n.tags span {\n background: var(--soft) !important; border-color: var(--line-2) !important;\n color: var(--secondary) !important; font-size: 11px; padding: 2px 7px;\n}\n.dots i { background: var(--line) !important; }\n.dots i.on { background: var(--ink) !important; }\n.doc { border-color: var(--line) !important; box-shadow: var(--shadow) !important; }\n.doc-empty { border-color: var(--line) !important; color: var(--muted) !important; background: var(--bg-soft) !important; }\n.docGenerated {\n display: flex; flex-direction: column; gap: 3px; padding: 7px 8px;\n background: var(--surface) !important; color: var(--ink);\n border: 1px solid var(--line) !important; border-radius: 6px;\n overflow: hidden;\n}\n.docGenerated .docMark {\n color: var(--blue-deep); font: 600 7.5px/1 var(--mono); letter-spacing: .08em;\n}\n.docGenerated b {\n color: var(--ink); font-size: 8.5px; line-height: 1.25; font-weight: 600;\n overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;\n}\n.docGenerated i { margin-top: auto; color: var(--muted); font: normal 7px/1 var(--sans); }\n.docGenerated.wide { gap: 4px; padding: 9px 11px; }\n.docGenerated.wide .docMark { font-size: 9px; }\n.docGenerated.wide b { font-size: 12px; -webkit-line-clamp: 2; }\n.docGenerated.wide i { font-size: 8px; }\n.edge { stroke: var(--edge) !important; }\n.edge.cross { stroke: var(--edge-soft) !important; }\n.edge.hot { stroke: var(--blue) !important; }\n.pill rect { fill: var(--surface) !important; stroke: var(--line) !important; }\n.pill text { fill: var(--secondary) !important; }\n.pill.hot rect { fill: var(--blue-soft) !important; stroke: var(--blue) !important; }\n.pill.hot text { fill: var(--blue-deep) !important; }\n#controls button {\n background: var(--surface) !important; border-color: var(--line) !important;\n color: var(--ink-2) !important; box-shadow: var(--shadow) !important;\n}\n#controls button:hover { background: var(--bg-soft) !important; color: var(--ink) !important; }\n#controls button.on { background: var(--blue) !important; border-color: var(--blue) !important; color: var(--bg) !important; }\n#zoomLabel { color: var(--muted) !important; }\n#panel { background: var(--surface) !important; border-left-color: var(--line) !important;\n box-shadow: -8px 0 28px color-mix(in srgb, var(--ink) 12%, transparent) !important; }\n#close { background: var(--surface) !important; border-color: var(--line) !important; color: var(--muted) !important; }\n#close:hover { background: var(--bg-soft) !important; color: var(--ink) !important; }\n.p-head { border-bottom-color: var(--line-2) !important; }\n.p-kicker { color: var(--muted) !important; }\n.p-head h2 { color: var(--ink) !important; }\n.p-field b { color: var(--muted) !important; }\n.p-field span, .p-doc { color: var(--secondary) !important; }\n.p-rel-title { color: var(--muted) !important; }\n.rel { background: var(--soft) !important; border-color: var(--line-2) !important; }\n.rel:hover { background: var(--bg-soft) !important; border-color: var(--line) !important; }\n.rel b { color: var(--ink) !important; }\n.rel span { color: var(--secondary) !important; }\n.p-docs { color: var(--muted) !important; }\n.p-doc-item { border-bottom-color: var(--line-2) !important; }\n.p-doc-item .m b { color: var(--ink) !important; }\n.p-doc-item .m span { color: var(--muted) !important; }\n.p-doc-thumb {\n display: flex; align-items: center; justify-content: center; flex: 0 0 auto;\n width: 34px; height: 46px; border-radius: 4px;\n border: 1px solid var(--line); background: var(--soft); color: var(--blue-deep);\n font: 600 8px/1 var(--mono); letter-spacing: .06em;\n}\n.p-doc h2, .p-doc h3 { color: var(--ink) !important; }\n.p-doc code { background: var(--soft) !important; border-color: var(--line-2) !important; }\n.p-doc pre { background: var(--bg) !important; border-color: var(--line-2) !important; }\n</style>\n</head>\n<body data-doc-layout=\"landscape\" data-view=\"graph\">\n<main id=\"main\">\n <div id=\"stage\">\n <div id=\"world\">\n <svg id=\"edges\" width=\"4200\" height=\"3200\" viewBox=\"0 0 4200 3200\" aria-hidden=\"true\"></svg>\n </div>\n </div>\n <div id=\"controls\" role=\"group\" aria-label=\"画布控制\">\n <button id=\"zin\" aria-label=\"放大\">+</button>\n <button id=\"zout\" aria-label=\"缩小\">−</button>\n <button id=\"fit\" aria-label=\"适应画布\">⤢</button>\n <button id=\"cross\" aria-label=\"显示交叉关系\" title=\"显示交叉关系\">⇄</button>\n <button id=\"relayout\" aria-label=\"重排\" title=\"重排\">↺</button>\n </div>\n <div id=\"zoomLabel\">100%</div>\n <div id=\"kg-empty\" hidden><strong>还没有可用的图谱</strong><span>在「知识图谱」文件夹里建好主题后,画布会出现在这里。</span></div>\n</main>\n\n<aside id=\"panel\" aria-label=\"主题详情\" aria-hidden=\"true\">\n <div class=\"p-head\">\n <div>\n <div class=\"p-kicker\" id=\"p-kicker\"></div>\n <h2 id=\"p-title\"></h2>\n </div>\n <button id=\"close\" aria-label=\"关闭\">×</button>\n </div>\n <div class=\"p-body\" id=\"p-body\"></div>\n</aside>\n<script>\nconst DATA = { graphs: [] };\n\n(function(){\n \"use strict\";\n const stage = document.getElementById('stage');\n const world = document.getElementById('world');\n const svg = document.getElementById('edges');\n const panel = document.getElementById('panel');\n const zoomLabel = document.getElementById('zoomLabel');\n\n const NS = 'http://www.w3.org/2000/svg';\n let topics = [];\n let links = [];\n let byId = new Map();\n let activeGraph = 0;\n let viewMode = 'graph';\n const cardEl = new Map();\n const view = {x:0, y:0, s:1};\n const CARD_W = 300, H_GAP = 110, V_GAP = 34, PAD = 40;\n const LAND = document.body.dataset.docLayout === 'landscape';\n\n let mode = null;\n let activeTopic = null;\n let activeLink = null;\n let userMoved = false;\n let children = new Map();\n let parent = new Map();\n let treeKeys = new Set();\n let pos = new Map();\n let heights = new Map(), subH = new Map();\n let showCross = false;\n\n function esc(s){\n return String(s == null ? '' : s).replace(/[&<>\"]/g, function(c){\n return {'&':'&','<':'<','>':'>','\"':'"'}[c];\n });\n }\n function statusText(s){ return s === 'active' ? '进行中' : s === 'paused' ? '暂停' : '已完成'; }\n function linkKey(l){ return l.a + '--' + l.b; }\n function pairKey(a, b){ return a < b ? a + '--' + b : b + '--' + a; }\n\n // ---------- cards ----------\n\n // Documents in the knowledge tree have no stored thumbnail. A generated cover\n // uses the host's own theme variables (set from the parent document), so a light\n // host gets ink-on-white covers and a dark host gets the inverse. No bitmap and\n // no fixed palette: the cover is markup, not an image.\n function coverFor(d, idx, wide, w, h, dx, dy, rot, sc, z){\n const style = 'width:' + w + 'px;height:' + h + 'px;--dx:' + dx + 'px;--dy:' + dy +\n 'px;--rot:' + rot + 'deg;--sc:' + sc + ';z-index:' + z;\n if (d.thumb) {\n const src = (wide && d.thumbWide) ? d.thumbWide : d.thumb;\n return '<img class=\"doc\" src=\"' + src + '\" alt=\"' + esc(d.title) + '\" style=\"' + style + '\">';\n }\n const ext = String(d.file || 'doc').split('.').pop().slice(0, 4).toUpperCase();\n return '<div class=\"doc docGenerated' + (wide ? ' wide' : '') + '\" style=\"' + style + '\" title=\"' + esc(d.title) + '\">' +\n '<span class=\"docMark\">' + esc(ext) + '</span>' +\n '<b>' + esc(d.title || d.file || '') + '</b>' +\n '<i>' + esc(d.date || '') + '</i></div>';\n }\n\n // The detail panel renders the same cover inline, at a fixed 34x46 slot.\n function inlineCover(d){\n if (d.thumb) return '<img src=\"' + d.thumb + '\" alt=\"\">';\n const ext = String(d.file || 'doc').split('.').pop().slice(0, 4).toUpperCase();\n return '<span class=\"p-doc-thumb\"><i>' + esc(ext) + '</i></span>';\n }\n\n function buildCard(t){\n const el = document.createElement('article');\n el.className = 'card';\n el.dataset.id = t.id;\n el.tabIndex = 0;\n el.style.left = '0px';\n el.style.top = '0px';\n el.style.visibility = 'hidden';\n el.setAttribute('role', 'button');\n el.setAttribute('aria-label', t.title + ',' + statusText(t.status) + ',深度 ' + t.depth);\n\n const dots = [1,2,3,4,5].map(i =>\n '<i class=\"' + (i <= t.depth ? 'on' : '') + '\"></i>').join('');\n const docs = t.documents || [];\n const shown = docs.slice(0, 4);\n const mid = (shown.length - 1) / 2;\n let docW = 86, docH = 116, step = 31, rotStep = 6.5, dyStep = 2, scStep = 0.03, coverH = 124;\n if (LAND){\n if (shown.length <= 1){ docW = 240; docH = 84; step = 0; rotStep = 0; dyStep = 0; scStep = 0; coverH = 92; }\n else if (shown.length === 2){ docW = 142; docH = 78; step = 58; rotStep = 5; dyStep = 3; scStep = 0.02; coverH = 88; }\n else if (shown.length === 3){ docW = 132; docH = 76; step = 46; rotStep = 4.2; dyStep = 3; scStep = 0.02; coverH = 86; }\n else { docW = 118; docH = 70; step = 42; rotStep = 4; dyStep = 3; scStep = 0.02; coverH = 80; }\n }\n let stack = '';\n shown.forEach(function(d, i){\n const dist = Math.abs(i - mid);\n const dx = (i - mid) * step;\n const dy = dist * dyStep;\n const rot = (i - mid) * rotStep;\n const sc = 1 - dist * scStep;\n const z = 10 - dist;\n stack += coverFor(d, i, shown.length === 1, docW, docH, dx, dy, rot, sc, z);\n });\n if (docs.length > 4) stack += '<span class=\"more\">+' + (docs.length - 4) + '</span>';\n if (!docs.length) stack = '<div class=\"doc-empty\">docs/ 还是空的</div>';\n const tagList = [t.domain].concat(t.tags || []).filter(function(x, i, arr){\n return x && arr.indexOf(x) === i;\n }).slice(0, 4);\n const tags = tagList.map(function(x){ return '<span>' + esc(x) + '</span>'; }).join('');\n\n el.innerHTML =\n '<h3>' + esc(t.title) + '</h3>' +\n '<div class=\"cover\" style=\"height:' + coverH + 'px\"><div class=\"stack\">' + stack + '</div></div>' +\n '<p class=\"one\">' + esc(t.summary) + '</p>' +\n '<div class=\"tags\">' + tags + '</div>';\n\n el.addEventListener('pointerdown', onCardDown);\n el.addEventListener('keydown', function(e){\n if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); selectTopic(t.id); }\n });\n world.appendChild(el);\n cardEl.set(t.id, el);\n }\n\n // ---------- spanning tree ----------\n function degree(id){\n return links.filter(function(l){ return l.a === id || l.b === id; }).length;\n }\n function buildTree(rootId){\n children = new Map(); parent = new Map(); treeKeys = new Set();\n const visited = new Set([rootId]);\n const queue = [rootId];\n topics.forEach(function(t){ children.set(t.id, []); });\n while (queue.length){\n const id = queue.shift();\n const neighbors = [];\n links.forEach(function(l){\n if (l.a === id && !visited.has(l.b)) neighbors.push(l.b);\n else if (l.b === id && !visited.has(l.a)) neighbors.push(l.a);\n });\n neighbors.sort(function(a, b){ return degree(b) - degree(a); });\n neighbors.forEach(function(n){\n visited.add(n);\n parent.set(n, id);\n children.get(id).push(n);\n treeKeys.add(pairKey(id, n));\n queue.push(n);\n });\n }\n topics.forEach(function(t){\n if (!visited.has(t.id)){\n parent.set(t.id, rootId);\n children.get(rootId).push(t.id);\n treeKeys.add(pairKey(rootId, t.id));\n visited.add(t.id);\n }\n });\n }\n\n // ---------- mind-map layout ----------\n function calcSub(id){\n const h = heights.get(id) || 200;\n const kids = children.get(id) || [];\n if (!kids.length){ subH.set(id, h); return h; }\n let total = 0;\n kids.forEach(function(k, i){\n total += calcSub(k);\n if (i) total += V_GAP;\n });\n subH.set(id, Math.max(h, total));\n return subH.get(id);\n }\n\n function placeNode(id, x, yTop, dir){\n const h = heights.get(id) || 200;\n const sub = subH.get(id) || h;\n const cy = yTop + sub / 2;\n pos.set(id, {x:x, y:cy - h / 2, dir:dir});\n const kids = children.get(id) || [];\n let cursor = yTop;\n kids.forEach(function(k){\n const kh = subH.get(k) || 200;\n const kx = dir > 0 ? x + CARD_W + H_GAP : x - CARD_W - H_GAP;\n placeNode(k, kx, cursor, dir);\n cursor += kh + V_GAP;\n });\n }\n\n function layout(){\n heights = new Map();\n topics.forEach(function(t){ heights.set(t.id, cardEl.get(t.id).offsetHeight || 200); });\n\n let rootId = topics[0].id, best = -1;\n topics.forEach(function(t){\n const d = degree(t.id);\n if (d > best){ best = d; rootId = t.id; }\n });\n if (byId.has('dsh-enterprise-app')) rootId = 'dsh-enterprise-app';\n buildTree(rootId);\n calcSub(rootId);\n\n const first = (children.get(rootId) || []).slice();\n first.sort(function(a, b){ return (subH.get(b) || 0) - (subH.get(a) || 0); });\n\n const left = [], right = [];\n let lh = 0, rh = 0;\n first.forEach(function(id){\n if (rh <= lh){ right.push(id); rh += subH.get(id); }\n else { left.push(id); lh += subH.get(id); }\n });\n\n const rootH = heights.get(rootId);\n pos.set(rootId, {x:0, y:-rootH / 2, dir:0});\n\n function placeSide(list, dir){\n let total = 0;\n list.forEach(function(id, i){ total += subH.get(id); if (i) total += V_GAP; });\n let cursor = -total / 2;\n list.forEach(function(id){\n const kh = subH.get(id);\n const x = dir > 0 ? CARD_W + H_GAP : -CARD_W - H_GAP;\n placeNode(id, x, cursor, dir);\n cursor += kh + V_GAP;\n });\n }\n placeSide(left, -1);\n placeSide(right, 1);\n\n let minX = Infinity, minY = Infinity;\n pos.forEach(function(p){ minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); });\n pos.forEach(function(p, id){\n p.x = p.x - minX + PAD;\n p.y = p.y - minY + PAD;\n const t = byId.get(id);\n t.x = p.x; t.y = p.y;\n const el = cardEl.get(id);\n el.style.left = p.x + 'px';\n el.style.top = p.y + 'px';\n el.style.visibility = '';\n });\n }\n\n // ---------- edges & pills ----------\n function center(id){\n const p = pos.get(id), h = heights.get(id) || 200;\n return {x:p.x + CARD_W / 2, y:p.y + h / 2};\n }\n function anchor(id, tx, ty){\n const c = center(id), h = heights.get(id) || 200;\n const dx = tx - c.x, dy = ty - c.y;\n if (!dx && !dy) return c;\n if (Math.abs(dx) * h > Math.abs(dy) * CARD_W){\n const k = (CARD_W / 2) / Math.abs(dx);\n return {x:c.x + dx * k, y:c.y + dy * k};\n }\n const k = (h / 2) / Math.abs(dy);\n return {x:c.x + dx * k, y:c.y + dy * k};\n }\n function qpoint(p0, c, p1, t){\n const u = 1 - t;\n return {x:u*u*p0.x + 2*u*t*c.x + t*t*p1.x, y:u*u*p0.y + 2*u*t*c.y + t*t*p1.y};\n }\n function cpoint(p0, c1, c2, p1, t){\n const u = 1 - t, a = u*u*u, b = 3*u*u*t, c = 3*u*t*t, d = t*t*t;\n return {x:a*p0.x + b*c1.x + c*c2.x + d*p1.x,\n y:a*p0.y + b*c1.y + c*c2.y + d*p1.y};\n }\n function hitsCard(x, y, w, h){\n for (let i = 0; i < topics.length; i++){\n const t = topics[i], th = heights.get(t.id) || 200;\n if (x < t.x + CARD_W + 6 && x + w > t.x - 6 &&\n y < t.y + th + 6 && y + h > t.y - 6) return true;\n }\n return false;\n }\n function freePointOnQuad(p0, c, p1){\n const ts = [0.5, 0.4, 0.6, 0.3, 0.7, 0.22, 0.78];\n for (let i = 0; i < ts.length; i++){\n const p = qpoint(p0, c, p1, ts[i]);\n if (!hitsCard(p.x, p.y)) return p;\n }\n return qpoint(p0, c, p1, 0.5);\n }\n function freePointOnCubic(p0, c1, c2, p1){\n const ts = [0.5, 0.42, 0.58, 0.34, 0.66, 0.26, 0.74];\n for (let i = 0; i < ts.length; i++){\n const p = cpoint(p0, c1, c2, p1, ts[i]);\n if (!hitsCard(p.x, p.y)) return p;\n }\n return cpoint(p0, c1, c2, p1, 0.5);\n }\n\n function overlaps(p, w, placed){\n for (let i = 0; i < placed.length; i++){\n const q = placed[i];\n if (Math.abs(p.x - q.x) < (w + q.w) / 2 + 8 && Math.abs(p.y - q.y) < 24) return true;\n }\n return false;\n }\n function placePill(sample, w, placed){\n const ts = [0.5, 0.56, 0.44, 0.62, 0.38, 0.68, 0.32, 0.74, 0.26, 0.8];\n for (let i = 0; i < ts.length; i++){\n const p = sample(ts[i]);\n if (!hitsCard(p.x - w / 2, p.y - 10, w, 20) && !overlaps(p, w, placed)){\n placed.push({x:p.x, y:p.y, w:w});\n return p;\n }\n }\n const p = sample(0.5);\n placed.push({x:p.x, y:p.y, w:w});\n return p;\n }\n\n function drawEdges(){\n while (svg.firstChild) svg.removeChild(svg.firstChild);\n const placed = [];\n\n links.forEach(function(l){\n const a = byId.get(l.a), b = byId.get(l.b);\n if (!a || !b) return;\n const key = linkKey(l);\n const isTree = treeKeys.has(pairKey(l.a, l.b));\n const isLink = mode === 'link' && activeLink === key;\n const isTopic = mode === 'topic' && (l.a === activeTopic || l.b === activeTopic);\n const hot = isLink || isTopic;\n const cold = !!mode && !hot;\n if (!isTree && !showCross && !hot) return;\n\n let d, sample;\n if (isTree){\n const pid = parent.get(l.b) === l.a ? l.a : l.b;\n const cid = pid === l.a ? l.b : l.a;\n const pp = pos.get(pid), pc = center(pid), cc = center(cid);\n const dir = pp.dir || (cc.x >= pc.x ? 1 : -1);\n const x1 = dir > 0 ? pp.x + CARD_W : pp.x;\n const x2 = dir > 0 ? pos.get(cid).x : pos.get(cid).x + CARD_W;\n const y1 = pc.y, y2 = cc.y;\n const bend = Math.max(34, Math.abs(x2 - x1) * 0.42);\n const p0 = {x:x1, y:y1};\n const c1 = {x:x1 + dir * bend, y:y1};\n const c2 = {x:x2 - dir * bend, y:y2};\n const p1 = {x:x2, y:y2};\n d = 'M' + x1 + ',' + y1 + ' C' + c1.x + ',' + c1.y + ' ' + c2.x + ',' + c2.y + ' ' + x2 + ',' + y2;\n sample = function(t){ return cpoint(p0, c1, c2, p1, t); };\n } else {\n const pa = pos.get(l.a), pb = pos.get(l.b);\n let p1, p2, c;\n if (pa.dir === pb.dir && pa.dir !== 0){\n const dir = pa.dir;\n p1 = {x: dir < 0 ? pa.x + CARD_W : pa.x, y: center(l.a).y};\n p2 = {x: dir < 0 ? pb.x + CARD_W : pb.x, y: center(l.b).y};\n const outX = dir < 0 ? Math.max(p1.x, p2.x) + 64 : Math.min(p1.x, p2.x) - 64;\n c = {x: outX, y: (p1.y + p2.y) / 2};\n } else {\n const ca = center(l.a), cb = center(l.b);\n p1 = {x: ca.x, y: pa.y};\n p2 = {x: cb.x, y: pb.y};\n c = {x: (p1.x + p2.x) / 2, y: Math.min(p1.y, p2.y) - 112};\n }\n d = 'M' + p1.x + ',' + p1.y + ' Q' + c.x + ',' + c.y + ' ' + p2.x + ',' + p2.y;\n sample = function(t){ return qpoint(p1, c, p2, t); };\n }\n\n const path = document.createElementNS(NS, 'path');\n path.setAttribute('d', d);\n path.setAttribute('class', 'edge' + (isTree ? '' : ' cross') + (hot ? ' hot' : cold ? ' cold' : ''));\n svg.appendChild(path);\n\n const label = l.label || (a.title + ' ↔ ' + b.title);\n const w = Math.max(42, label.length * 10.8 + 16);\n const pillAt = placePill(sample, w, placed);\n\n const g = document.createElementNS(NS, 'g');\n g.setAttribute('class', 'pill' + (hot ? ' hot' : cold ? ' cold' : ''));\n g.setAttribute('data-key', key);\n g.setAttribute('tabindex', '0');\n g.setAttribute('role', 'button');\n g.setAttribute('aria-label', '关系:' + label);\n\n const rect = document.createElementNS(NS, 'rect');\n rect.setAttribute('x', pillAt.x - w / 2); rect.setAttribute('y', pillAt.y - 10);\n rect.setAttribute('width', w); rect.setAttribute('height', 20);\n rect.setAttribute('rx', 10);\n const text = document.createElementNS(NS, 'text');\n text.setAttribute('x', pillAt.x); text.setAttribute('y', pillAt.y + 3.6);\n text.setAttribute('text-anchor', 'middle');\n text.textContent = label;\n\n g.appendChild(rect); g.appendChild(text);\n g.addEventListener('click', function(e){ e.stopPropagation(); selectLink(key); });\n g.addEventListener('keydown', function(e){\n if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); selectLink(key); }\n });\n svg.appendChild(g);\n });\n }\n\n // ---------- camera ----------\n function apply(){\n world.style.transform = 'translate(' + view.x + 'px,' + view.y + 'px) scale(' + view.s + ')';\n zoomLabel.textContent = Math.round(view.s * 100) + '%';\n }\n function clampScale(s){ return Math.max(0.22, Math.min(2.0, s)); }\n function zoomAt(px, py, k){\n const s2 = clampScale(view.s * k);\n if (s2 === view.s) return;\n view.x = px - (px - view.x) * (s2 / view.s);\n view.y = py - (py - view.y) * (s2 / view.s);\n view.s = s2;\n apply(); userMoved = true;\n }\n function bbox(){\n let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;\n topics.forEach(function(t){\n const h = heights.get(t.id) || 200;\n x1 = Math.min(x1, t.x); y1 = Math.min(y1, t.y);\n x2 = Math.max(x2, t.x + CARD_W); y2 = Math.max(y2, t.y + h);\n });\n return {x1:x1, y1:y1, x2:x2, y2:y2};\n }\n function fit(animate){\n const b = bbox();\n const pad = 70;\n const vw = stage.clientWidth, vh = stage.clientHeight;\n const s = clampScale(Math.min((vw - pad * 2) / (b.x2 - b.x1), (vh - pad * 2) / (b.y2 - b.y1), 1));\n const nx = (vw - (b.x2 - b.x1) * s) / 2 - b.x1 * s;\n const ny = (vh - (b.y2 - b.y1) * s) / 2 - b.y1 * s;\n if (animate) tweenTo(nx, ny, s); else { view.x = nx; view.y = ny; view.s = s; apply(); }\n userMoved = false;\n }\n let raf = null;\n function tweenTo(x2, y2, s2){\n cancelAnimationFrame(raf);\n const x1 = view.x, y1 = view.y, s1 = view.s, t0 = performance.now(), dur = 320;\n function step(now){\n const p = Math.min(1, (now - t0) / dur);\n const e = 1 - Math.pow(1 - p, 3);\n view.x = x1 + (x2 - x1) * e;\n view.y = y1 + (y2 - y1) * e;\n view.s = s1 + (s2 - s1) * e;\n apply();\n if (p < 1) raf = requestAnimationFrame(step);\n }\n raf = requestAnimationFrame(step);\n }\n function centerOn(id){\n const t = byId.get(id); if (!t) return;\n const h = heights.get(id) || 200;\n const s = 1;\n const x = stage.clientWidth / 2 - (t.x + CARD_W / 2) * s;\n const y = stage.clientHeight / 2 - (t.y + h / 2) * s;\n tweenTo(x, y, s); userMoved = true;\n }\n\n // ---------- pan & zoom ----------\n let panning = false, panMoved = false, px = 0, py = 0;\n stage.addEventListener('pointerdown', function(e){\n if (e.target.closest('.card') || e.target.closest('.pill') || e.target.closest('#panel')) return;\n panning = true; panMoved = false; px = e.clientX; py = e.clientY;\n stage.classList.add('grabbing');\n stage.setPointerCapture(e.pointerId);\n });\n stage.addEventListener('pointermove', function(e){\n if (!panning) return;\n if (Math.abs(e.clientX - px) + Math.abs(e.clientY - py) > 3) panMoved = true;\n view.x += e.clientX - px; view.y += e.clientY - py;\n px = e.clientX; py = e.clientY;\n apply(); userMoved = true;\n });\n function endPan(e){\n if (!panning) return;\n panning = false; stage.classList.remove('grabbing');\n try { stage.releasePointerCapture(e.pointerId); } catch(_){}\n if (!panMoved && mode) closePanel();\n }\n stage.addEventListener('pointerup', endPan);\n stage.addEventListener('pointercancel', endPan);\n stage.addEventListener('wheel', function(e){\n e.preventDefault();\n zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.0011));\n }, {passive:false});\n document.getElementById('zin').addEventListener('click', function(){\n zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 1.22);\n });\n document.getElementById('zout').addEventListener('click', function(){\n zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 1 / 1.22);\n });\n document.getElementById('fit').addEventListener('click', function(){ fit(true); });\n document.getElementById('relayout').addEventListener('click', function(){\n layout(); fit(true); drawEdges();\n });\n document.getElementById('cross').addEventListener('click', function(){\n showCross = !showCross;\n this.classList.toggle('on', showCross);\n this.setAttribute('aria-pressed', showCross ? 'true' : 'false');\n drawEdges();\n });\n stage.addEventListener('dblclick', function(e){ if (!e.target.closest('.card')) fit(true); });\n\n // ---------- card drag ----------\n let drag = null;\n function onCardDown(e){\n if (e.button !== 0) return;\n e.stopPropagation();\n const id = e.currentTarget.dataset.id;\n const t = byId.get(id);\n const el = cardEl.get(id);\n drag = {id:id, sx:e.clientX, sy:e.clientY, ox:t.x, oy:t.y, moved:false, el:el};\n el.classList.add('dragging');\n window.addEventListener('pointermove', onDragMove);\n window.addEventListener('pointerup', onDragUp);\n }\n function onDragMove(e){\n if (!drag) return;\n const dx = (e.clientX - drag.sx) / view.s;\n const dy = (e.clientY - drag.sy) / view.s;\n if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;\n const t = byId.get(drag.id);\n t.x = drag.ox + dx; t.y = drag.oy + dy;\n const p = pos.get(drag.id); p.x = t.x; p.y = t.y;\n drag.el.style.left = t.x + 'px';\n drag.el.style.top = t.y + 'px';\n drawEdges();\n }\n function onDragUp(){\n window.removeEventListener('pointermove', onDragMove);\n window.removeEventListener('pointerup', onDragUp);\n if (!drag) return;\n drag.el.classList.remove('dragging');\n const id = drag.id, moved = drag.moved;\n drag = null;\n if (!moved) selectTopic(id);\n userMoved = true;\n }\n\n // ---------- selection ----------\n function paint(){\n cardEl.forEach(function(el, cid){\n let sel = false, dim = false;\n if (mode === 'topic'){ sel = cid === activeTopic; dim = !sel; }\n else if (mode === 'link'){\n const l = links.find(function(x){ return linkKey(x) === activeLink; });\n sel = !!l && (cid === l.a || cid === l.b);\n dim = !sel;\n }\n el.classList.toggle('selected', sel);\n el.classList.toggle('dim', dim);\n });\n }\n function notifySelection(topicId){\n try {\n parent.postMessage({\n type: 'kg:selection',\n graphId: (DATA.graphs[activeGraph] || {}).id || null,\n topicId: topicId || null\n }, '*');\n } catch (_) {}\n }\n function selectTopic(id){\n if (mode === 'topic' && activeTopic === id){ closePanel(); return; }\n mode = 'topic'; activeTopic = id; activeLink = null;\n paint(); drawEdges(); openTopicPanel(id); setHash(id);\n notifySelection(id);\n }\n function selectLink(key){\n if (mode === 'link' && activeLink === key){ closePanel(); return; }\n mode = 'link'; activeLink = key; activeTopic = null;\n paint(); drawEdges(); openLinkPanel(key); setHash(key);\n }\n function closePanel(){\n mode = null; activeTopic = null; activeLink = null;\n paint(); drawEdges();\n panel.classList.remove('open');\n panel.setAttribute('aria-hidden', 'true');\n document.body.classList.remove('panel-open');\n setHash(null);\n notifySelection(null);\n }\n document.getElementById('close').addEventListener('click', closePanel);\n document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closePanel(); });\n\n function showPanel(kicker, title, html){\n document.getElementById('p-kicker').textContent = kicker;\n document.getElementById('p-title').textContent = title;\n const body = document.getElementById('p-body');\n body.innerHTML = html;\n body.scrollTop = 0;\n body.querySelectorAll('.rel').forEach(function(btn){\n btn.addEventListener('click', function(){\n const to = btn.dataset.to;\n selectTopic(to); centerOn(to);\n });\n });\n panel.classList.add('open');\n panel.setAttribute('aria-hidden', 'false');\n document.body.classList.add('panel-open');\n }\n function inline(s){\n s = esc(s);\n s = s.replace(/`([^`]+)`/g, '<code>$1</code>');\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener\">$1</a>');\n return s;\n }\n function renderMarkdown(md){\n const lines = String(md || '').split('\\n');\n let html = '', inCode = false, code = [], list = null;\n function closeList(){ if (list){ html += '</' + list + '>'; list = null; } }\n lines.forEach(function(line){\n if (/^```/.test(line)){\n if (inCode){ html += '<pre><code>' + esc(code.join('\\n')) + '</code></pre>'; code = []; inCode = false; }\n else { closeList(); inCode = true; }\n return;\n }\n if (inCode){ code.push(line); return; }\n if (!line.trim()){ closeList(); return; }\n let m;\n if ((m = line.match(/^(#{1,3})\\s+(.*)$/))){\n closeList();\n const lv = Math.min(3, m[1].length);\n html += '<h' + lv + '>' + inline(m[2]) + '</h' + lv + '>';\n } else if ((m = line.match(/^\\s*[-*]\\s+(.*)$/))){\n if (list !== 'ul'){ closeList(); html += '<ul>'; list = 'ul'; }\n html += '<li>' + inline(m[1]) + '</li>';\n } else if ((m = line.match(/^\\s*\\d+\\.\\s+(.*)$/))){\n if (list !== 'ol'){ closeList(); html += '<ol>'; list = 'ol'; }\n html += '<li>' + inline(m[1]) + '</li>';\n } else if ((m = line.match(/^>\\s?(.*)$/))){\n closeList();\n html += '<blockquote>' + inline(m[1]) + '</blockquote>';\n } else {\n closeList();\n html += '<p>' + inline(line) + '</p>';\n }\n });\n if (inCode) html += '<pre><code>' + esc(code.join('\\n')) + '</code></pre>';\n closeList();\n return html;\n }\n function field(k, v){\n return '<div class=\"p-field\"><b>' + k + '</b><span>' + esc(v || '未登记') + '</span></div>';\n }\n function openTopicPanel(id){\n const t = byId.get(id);\n const rels = links.filter(function(l){ return l.a === id || l.b === id; });\n let html = '<div class=\"p-doc\">' + renderMarkdown(t.body || t.summary || '') + '</div>';\n if (t.documents && t.documents.length){\n html += '<div class=\"p-rel-title\">文档 · ' + t.documents.length + ' 篇</div>';\n t.documents.forEach(function(d){\n html += '<div class=\"p-doc-item\">' + inlineCover(d) + '<div class=\"m\">' +\n '<b>' + esc(d.title) + '</b><span>' + esc(d.file) + ' · ' + esc(d.date) + '</span></div></div>';\n });\n }\n html += '<div class=\"p-rel-title\">关系 · ' + rels.length + ' 篇</div>';\n rels.forEach(function(l){\n const other = l.a === id ? l.b : l.a;\n const o = byId.get(other);\n html += '<button class=\"rel\" data-to=\"' + esc(other) + '\"><b>' + esc(o ? o.title : other) +\n '</b><span>' + esc(l.body || '') + '</span></button>';\n });\n if (!rels.length) html += '<div class=\"p-docs\">还没有关系文档。孤立的线价值很低。</div>';\n html += '<div class=\"p-docs\">文档:' + ((t.documents || []).length) + ' 个文件</div>';\n showPanel((t.domain ? t.domain + ' · ' : '') + statusText(t.status) + ' · depth ' + t.depth + '/5', t.title, html);\n }\n function openLinkPanel(key){\n const l = links.find(function(x){ return linkKey(x) === key; });\n if (!l) return;\n const a = byId.get(l.a), b = byId.get(l.b);\n let html = field('关系', l.body);\n html += '<div class=\"p-rel-title\">两端</div>';\n html += '<button class=\"rel\" data-to=\"' + esc(l.a) + '\"><b>' + esc(a ? a.title : l.a) +\n '</b><span>' + esc(a ? a.summary : '') + '</span></button>';\n html += '<button class=\"rel\" data-to=\"' + esc(l.b) + '\"><b>' + esc(b ? b.title : l.b) +\n '</b><span>' + esc(b ? b.summary : '') + '</span></button>';\n showPanel('关系文档' + (l.updated ? ' · ' + l.updated : ''),\n (a ? a.title : l.a) + ' ↔ ' + (b ? b.title : l.b), html);\n }\n function setHash(v){\n try { history.replaceState(null, '', v ? '#' + v : location.pathname + location.search); } catch(_){}\n }\n\n // ---------- graph switching ----------\n function loadGraph(idx){\n const g = DATA.graphs[idx];\n if (!g) return;\n closePanel();\n activeGraph = idx;\n topics = g.topics;\n links = g.links;\n byId = new Map(topics.map(function(t){ return [t.id, t]; }));\n cardEl.forEach(function(el){ el.remove(); });\n cardEl.clear();\n while (svg.firstChild) svg.removeChild(svg.firstChild);\n heights = new Map(); subH = new Map(); pos = new Map();\n\n topics.forEach(buildCard);\n layout();\n apply();\n\n requestAnimationFrame(function(){\n if (window.innerWidth < 760){\n const t = byId.get('dsh-enterprise-app') || topics[0];\n const h = heights.get(t.id) || 200;\n view.s = 0.9;\n view.x = stage.clientWidth / 2 - (t.x + CARD_W / 2) * view.s;\n view.y = stage.clientHeight / 2 - (t.y + h / 2) * view.s;\n apply(); userMoved = true;\n } else {\n fit(false);\n }\n drawEdges();\n });\n }\n\n // ---------- the embedding host ----------\n const emptyEl = document.getElementById('kg-empty');\n\n // The host's alias tokens, read from the parent document (same origin) and kept\n // live by the panel's postMessage. Reading the parent on boot avoids a flash of\n // the wrong scheme before the first message; the message is still required for\n // a later theme switch.\n const THEME_TOKENS = [\n '--dsw-alias-bg-base', '--dsw-alias-bg-layer-1', '--dsw-alias-bg-layer-2',\n '--dsw-alias-label-primary', '--dsw-alias-label-secondary', '--dsw-alias-label-tertiary',\n '--dsw-alias-border-l1', '--dsw-alias-border-l2',\n '--dsw-alias-state-business-primary', '--dsw-alias-link',\n '--dsw-alias-state-success-primary', '--dsw-alias-state-warn-primary',\n '--dsw-elevation-panel'\n ];\n\n function readHostTheme(){\n try {\n const doc = parent.document;\n const bodyStyle = parent.getComputedStyle(doc.body);\n const rootStyle = parent.getComputedStyle(doc.documentElement);\n const tokens = {};\n THEME_TOKENS.forEach(function(name){\n const value = (bodyStyle.getPropertyValue(name) || rootStyle.getPropertyValue(name) || '').trim();\n if (value !== '') tokens[name] = value;\n });\n return { tokens: tokens, dark: doc.body.hasAttribute('data-ds-dark-theme') };\n } catch (_) {\n return null;\n }\n }\n\n function applyTheme(theme){\n if (!theme || !theme.tokens) return;\n const root = document.documentElement;\n Object.keys(theme.tokens).forEach(function(name){\n root.style.setProperty(name, theme.tokens[name]);\n });\n root.style.colorScheme = theme.dark ? 'dark' : 'light';\n document.body.toggleAttribute('data-ds-dark-theme', !!theme.dark);\n }\n\n applyTheme(readHostTheme());\n\n function showEmpty(show){\n if (emptyEl) emptyEl.hidden = !show;\n const stageEl = document.getElementById('stage');\n if (stageEl) stageEl.style.visibility = show ? 'hidden' : 'visible';\n const controlsEl = document.getElementById('controls');\n if (controlsEl) controlsEl.hidden = show;\n const zoomEl = document.getElementById('zoomLabel');\n if (zoomEl) zoomEl.hidden = show;\n }\n\n function postReady(){\n try { parent.postMessage({ type: 'kg:ready' }, '*'); } catch (_) {}\n }\n\n function applyPayload(payload){\n if (!payload || !Array.isArray(payload.graphs)) return;\n DATA.graphs = payload.graphs;\n if (!DATA.graphs.length){\n showEmpty(true);\n postReady();\n return;\n }\n showEmpty(false);\n const wanted = payload.activeGraphId\n ? DATA.graphs.findIndex(function(g){ return g.id === payload.activeGraphId; })\n : -1;\n const idx = wanted >= 0 ? wanted : 0;\n loadGraph(idx);\n if (payload.selectedTopicId){\n requestAnimationFrame(function(){ focusTopic(payload.selectedTopicId); });\n }\n postReady();\n }\n\n function focusTopic(id){\n const t = byId.get(id);\n if (!t) return;\n selectTopic(id);\n centerOn(id);\n }\n\n function selectGraphById(id){\n const idx = DATA.graphs.findIndex(function(g){ return g.id === id; });\n if (idx < 0) return;\n showEmpty(false);\n loadGraph(idx);\n }\n\n window.addEventListener('message', function(event){\n const msg = event.data || {};\n if (msg.type === 'kg:set-graphs') applyPayload(msg);\n else if (msg.type === 'kg:select-graph') selectGraphById(msg.graphId);\n else if (msg.type === 'kg:select-topic') focusTopic(msg.topicId);\n else if (msg.type === 'kg:theme') applyTheme(msg);\n });\n\n showEmpty(true);\n postReady();\n window.addEventListener('resize', function(){\n if (!userMoved) fit(false);\n drawEdges();\n });\n\n window.__KG_VIEW__ = {data:DATA, selectTopic:selectTopic, selectLink:selectLink, fit:fit};\n})();\n<\/script>\n</body>\n</html>\n";
|
|
1678
|
+
const GRAPH_CANVAS_HTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>知识图谱 · 画布</title>\n<style>\n:root{\n --bg:#fbfbfc;\n --bg-soft:#f6f7f9;\n --surface:#ffffff;\n --ink:#0f1115;\n --ink-2:#3c3f47;\n --secondary:#61666b;\n --muted:#81858c;\n --line:rgba(0,0,0,.10);\n --line-2:rgba(0,0,0,.06);\n --blue:#4176e6;\n --blue-deep:#2555c4;\n --blue-soft:#eef4ff;\n --green:#22c55e;\n --amber:#f59e0b;\n --radius:12px;\n --shadow:0 1px 2px rgba(15,17,21,.04), 0 8px 24px rgba(15,17,21,.06);\n --sans:-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;\n --mono:\"SF Mono\",\"JetBrains Mono\",Menlo,Consolas,monospace;\n}\n*{box-sizing:border-box}\nhtml,body{height:100%}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.6;overflow:hidden;-webkit-font-smoothing:antialiased}\nbutton{font-family:inherit;color:inherit}\n\n#stage{position:fixed;left:248px;right:0;top:0;bottom:0;overflow:hidden;cursor:grab;\n background:\n radial-gradient(900px 620px at 50% 38%, #ffffff 0%, rgba(255,255,255,0) 70%),\n var(--bg)}\n#stage::before{content:\"\";position:absolute;inset:0;pointer-events:none;\n background-image:radial-gradient(rgba(15,17,21,.055) 1px, transparent 1px);\n background-size:22px 22px;\n -webkit-mask-image:radial-gradient(ellipse 70% 70% at 50% 45%, #000 0%, transparent 78%);\n mask-image:radial-gradient(ellipse 70% 70% at 50% 45%, #000 0%, transparent 78%)}\n#stage.grabbing{cursor:grabbing}\n#world{position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform}\n#edges{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}\n\n#topbar{position:fixed;left:248px;right:0;top:0;display:flex;align-items:flex-start;justify-content:space-between;\n padding:18px 22px;pointer-events:none;z-index:10}\n#brand h1{margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em}\n#brand p{margin:4px 0 0;font-size:12px;color:var(--muted)}\n#brand b{color:var(--secondary);font-weight:500}\n#hint{font-size:11.5px;color:var(--muted);text-align:right;line-height:1.9;transition:opacity .2s}\n#hint kbd{font-family:var(--mono);font-size:10.5px;border:1px solid var(--line);border-radius:4px;\n padding:1px 5px;color:var(--secondary);background:var(--surface)}\nbody.panel-open #hint{opacity:0}\n\n.card{position:absolute;width:300px;padding:15px 16px 13px;border-radius:var(--radius);cursor:grab;\n user-select:none;background:var(--surface);border:1px solid var(--line);box-shadow:var(--shadow);\n transition:border-color .16s,box-shadow .16s,opacity .16s}\n.card:hover{border-color:rgba(0,0,0,.18)}\n.card.dragging{cursor:grabbing;transition:none}\n.card.selected{border-color:var(--blue);\n box-shadow:0 0 0 2px rgba(65,118,230,.14),0 10px 28px rgba(15,17,21,.10)}\n.card.dim{opacity:.38}\n.card:focus-visible{outline:2px solid var(--blue);outline-offset:3px}\n.chip{font-size:11px;letter-spacing:.02em;color:var(--secondary);background:var(--bg-soft);\n border:1px solid var(--line-2);border-radius:6px;padding:1px 7px}\n.status{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}\n.dot{width:6px;height:6px;border-radius:50%;background:var(--green)}\n.dot.paused{background:var(--amber)}\n.dot.done{background:#c4c7cc}\n.card h3{margin:0 0 9px;font-size:17px;font-weight:600;line-height:1.35;letter-spacing:-.01em}\n.cover{position:relative;height:124px;margin:0 0 10px}\nbody[data-doc-layout=\"landscape\"] .cover{height:86px;margin-bottom:9px}\nbody[data-doc-layout=\"landscape\"] .doc{width:132px;height:76px;border-radius:5px}\nbody[data-doc-layout=\"landscape\"] .doc-empty{width:140px;height:80px}\n.stack{position:absolute;inset:0}\n.doc{position:absolute;left:50%;top:50%;width:86px;height:116px;border-radius:6px;\n border:1px solid rgba(0,0,0,.08);box-shadow:0 8px 18px rgba(15,17,21,.14);background:#fff;\n object-fit:cover;\n transform:translate(-50%,-50%) translate(var(--dx),var(--dy)) rotate(var(--rot)) scale(var(--sc));\n transition:transform .22s cubic-bezier(.22,1,.36,1)}\n.cover:hover .doc{transform:translate(-50%,-50%) translate(calc(var(--dx) * 1.4),var(--dy)) rotate(calc(var(--rot) * 1.35)) scale(var(--sc))}\n.doc-empty{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:96px;height:120px;\n border:1px dashed var(--line);border-radius:6px;display:flex;align-items:center;justify-content:center;\n color:#b3b7bd;font-size:10.5px;background:var(--bg-soft)}\n.more{position:absolute;right:2px;bottom:0;font-size:10px;color:var(--muted);background:var(--surface);\n border:1px solid var(--line-2);border-radius:999px;padding:0 6px}\n.tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px;margin-bottom:2px}\n.tags span{font-size:10px;color:var(--secondary);background:var(--bg-soft);border:1px solid var(--line-2);\n border-radius:999px;padding:1px 7px}\n.one{margin:0;font-size:12.5px;line-height:1.65;color:var(--ink-2);\n display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n\n.dots{display:inline-flex;gap:3px}\n.dots i{width:5px;height:5px;border-radius:50%;background:#dcdfe3}\n.dots i.on{background:var(--ink)}\n\n.edge{fill:none;stroke:rgba(15,17,21,.32);stroke-width:1.2}\n.edge.cross{stroke:rgba(15,17,21,.18);stroke-dasharray:4 6}\n.edge.hot{stroke:var(--blue);stroke-width:1.6}\n.edge.cold{opacity:.22}\n\n.pill{cursor:pointer;pointer-events:auto}\n.pill rect{fill:var(--surface);stroke:var(--line);stroke-width:1;transition:stroke .16s,fill .16s}\n.pill text{font:10.5px var(--sans);fill:var(--secondary);letter-spacing:.01em}\n.pill:hover rect{stroke:rgba(0,0,0,.28)}\n.pill.hot rect{stroke:var(--blue);fill:var(--blue-soft)}\n.pill.hot text{fill:var(--blue-deep)}\n.pill.cold{opacity:.25}\n.pill:focus-visible{outline:none}\n.pill:focus-visible rect{stroke:var(--blue);stroke-width:1.6}\n\n#controls{position:fixed;left:266px;bottom:18px;display:flex;flex-direction:column;gap:6px;z-index:12}\n#controls button{width:34px;height:34px;border-radius:8px;border:1px solid var(--line);\n background:var(--surface);color:var(--ink-2);font-size:15px;line-height:1;cursor:pointer;\n box-shadow:0 1px 2px rgba(15,17,21,.04);transition:background .15s,color .15s}\n#controls button:hover{background:var(--bg-soft);color:var(--ink)}\n#controls button.on{background:var(--ink);color:#fff;border-color:var(--ink)}\n#controls button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}\n#zoomLabel{position:fixed;left:308px;bottom:23px;font-family:var(--mono);font-size:10.5px;\n color:var(--muted);z-index:12}\n\n#panel{position:fixed;top:0;right:0;width:392px;height:100%;z-index:30;\n background:var(--surface);border-left:1px solid var(--line);\n box-shadow:-8px 0 28px rgba(15,17,21,.06);\n transform:translateX(102%);transition:transform .28s cubic-bezier(.22,1,.36,1);\n display:flex;flex-direction:column}\n#panel.open{transform:none}\n.p-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;\n padding:22px 22px 15px;border-bottom:1px solid var(--line-2)}\n.p-kicker{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}\n.p-head h2{margin:7px 0 0;font-size:22px;font-weight:600;line-height:1.35;letter-spacing:-.01em}\n#close{width:30px;height:30px;flex:0 0 auto;border-radius:8px;border:1px solid var(--line);\n background:var(--surface);color:var(--muted);font-size:16px;cursor:pointer;line-height:1}\n#close:hover{color:var(--ink);background:var(--bg-soft)}\n.p-body{padding:16px 22px 40px;overflow:auto}\n.p-field{margin:0 0 14px}\n.p-field b{display:block;font-size:11px;letter-spacing:.08em;text-transform:uppercase;\n color:var(--muted);font-weight:500;margin-bottom:3px}\n.p-field span{display:block;font-size:13px;line-height:1.7;color:var(--ink-2)}\n.p-rel-title{margin:24px 0 9px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}\n.rel{display:block;width:100%;text-align:left;background:var(--bg-soft);\n border:1px solid var(--line-2);border-radius:8px;padding:10px 11px;margin:0 0 7px;cursor:pointer;\n transition:background .15s,border-color .15s}\n.rel:hover{background:#f1f3f5;border-color:var(--line)}\n.rel b{display:block;font-size:12.5px;color:var(--ink);margin-bottom:2px;font-weight:600}\n.rel span{display:block;font-size:11.5px;line-height:1.6;color:var(--muted);\n display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.p-docs{font-size:11.5px;color:var(--muted);margin-top:12px}\n.p-doc-item{display:flex;gap:10px;align-items:center;padding:7px 0;border-bottom:1px solid var(--line-2)}\n.p-doc-item img{width:34px;height:46px;border-radius:3px;border:1px solid var(--line-2);object-fit:cover;flex:0 0 auto}\n.p-doc-item .m b{display:block;font-size:12.5px;font-weight:600;color:var(--ink)}\n.p-doc-item .m span{display:block;font-size:10.5px;color:var(--muted)}\n.p-doc{font-size:13px;line-height:1.8;color:var(--ink-2)}\n.p-doc h2{font-size:14px;font-weight:600;color:var(--ink);margin:22px 0 6px}\n.p-doc h3{font-size:13px;font-weight:600;color:var(--ink);margin:18px 0 5px}\n.p-doc p{margin:0 0 10px}\n.p-doc ul,.p-doc ol{margin:0 0 10px;padding-left:18px}\n.p-doc li{margin:2px 0}\n.p-doc blockquote{margin:0 0 10px;padding:2px 0 2px 12px;border-left:2px solid var(--line);color:var(--muted)}\n.p-doc code{font-family:var(--mono);font-size:.86em;background:var(--bg-soft);border:1px solid var(--line-2);padding:0 4px;border-radius:4px}\n.p-doc pre{margin:0 0 10px;padding:10px 12px;background:var(--bg-soft);border:1px solid var(--line-2);border-radius:8px;overflow:auto}\n.p-doc pre code{background:none;border:0;padding:0}\n.p-doc strong{color:var(--ink);font-weight:600}\n.p-doc a{color:var(--blue);text-decoration:none}\n\n\n/* ---------- tree ---------- */\n#tree{position:fixed;left:0;top:0;bottom:0;width:248px;z-index:20;display:flex;flex-direction:column;\n background:var(--surface);border-right:1px solid var(--line)}\n.tree-head{padding:18px 16px 13px;border-bottom:1px solid var(--line-2)}\n.tree-title{font-size:14px;font-weight:600}\n.tree-sub{font-size:11px;color:var(--muted);margin-top:2px}\n#treeBody{flex:1;overflow:auto;padding:8px 8px 20px}\n.tree-scope{font-size:10.5px;letter-spacing:.1em;color:var(--muted);padding:12px 10px 5px}\n.tree-group{font-size:11.5px;color:var(--secondary);padding:6px 10px 3px}\n.tree-graph{display:flex;align-items:center;justify-content:space-between;gap:8px;\n padding:5px 10px;border-radius:6px;font-size:12.5px;color:var(--ink-2);cursor:pointer}\n.tree-graph:hover{background:var(--bg-soft)}\n.tree-graph.active{background:var(--blue-soft);color:var(--blue-deep)}\n.tree-graph .n{font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}\n.tree-graph.active .n{color:var(--blue)}\n.tree-topic{padding:4px 10px 4px 24px;font-size:12px;color:var(--muted);border-radius:6px;cursor:pointer;\n white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.tree-topic:hover{background:var(--bg-soft);color:var(--ink)}\n.tree-empty{padding:2px 10px 4px 24px;font-size:11.5px;color:#b3b7bd}\n\n@media (max-width:760px){\n #tree{display:none}\n #stage{left:0}\n #topbar{left:0}\n #controls{left:18px}\n #zoomLabel{left:60px}\n #hint{display:none}\n .only-wide{display:none}\n #brand h1{font-size:15px}\n .card{width:270px}\n #panel{width:100%;height:72%;top:auto;bottom:0;border-left:0;border-top:1px solid var(--line);\n transform:translateY(102%);border-radius:16px 16px 0 0}\n #panel.open{transform:none}\n}\n</style>\n<style>\n:root{\n --bg:#151517; --bg-soft:#232324; --surface:#232324; --ink:#f5f5f7; --ink-2:#cfd3d6;\n --secondary:#adb2b8; --muted:#8a8d95; --line:rgba(255,255,255,.12); --line-2:rgba(255,255,255,.06);\n --blue:#4d93f8; --blue-deep:#9db9ff; --blue-soft:rgba(77,147,248,.14);\n --green:#22c55e; --amber:#f59e0b;\n --shadow:0 1px 2px rgba(0,0,0,.3), 0 10px 30px rgba(0,0,0,.28);\n}\nhtml,body{height:100%}\nbody{display:flex;overflow:hidden;background:var(--bg);color:var(--ink)}\n#rail{width:280px;flex:0 0 280px;background:#141416;border-right:.5px solid rgba(255,255,255,.10);\n display:flex;flex-direction:column;min-height:0}\n.rail-top{height:36px;flex:0 0 36px;display:flex;align-items:center;gap:8px;padding:0 6px 0 12px}\n.rail-brand{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;color:var(--ink)}\n.rail-brand svg{width:16px;height:16px;color:var(--secondary)}\n.rail-fold{margin-left:auto;width:28px;height:28px;border:0;background:none;border-radius:6px;\n color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center}\n.rail-fold:hover{background:rgba(255,255,255,.06);color:var(--ink)}\n.rail-search{margin:6px 12px 4px;height:32px;display:flex;align-items:center;gap:8px;padding:0 10px;\n background:#1f1f22;border:.5px solid rgba(255,255,255,.10);border-radius:8px}\n.rail-search svg{width:14px;height:14px;color:var(--muted)}\n.rail-search input{flex:1;min-width:0;background:none;border:0;outline:none;color:var(--ink);font-size:12.5px}\n.rail-search input::placeholder{color:#6f737a}\n.rail-nav{padding:4px 8px;display:flex;flex-direction:column;gap:1px}\n.nav-item{height:34px;display:flex;align-items:center;gap:10px;padding:0 10px;border:0;border-radius:6px;\n background:none;color:var(--secondary);font-size:13.5px;text-align:left;cursor:pointer}\n.nav-item svg{width:15px;height:15px;color:var(--muted)}\n.nav-item:hover{background:rgba(255,255,255,.06)}\n.nav-item.active{background:rgba(255,255,255,.08);color:var(--ink)}\n.nav-item.active svg{color:var(--secondary)}\n.tree-head{height:36px;flex:0 0 36px;display:flex;align-items:center;gap:4px;padding:0 8px 0 12px}\n.rail-tabs{display:flex;align-items:center;gap:18px;height:36px}\n.rail-tab{height:36px;padding:0;border:0;background:none;color:var(--muted);font-size:14px;\n cursor:pointer;position:relative}\n.rail-tab:hover{color:var(--secondary)}\n.rail-tab.active{color:var(--ink)}\n.rail-tab.active::after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:2px;\n border-radius:1px;background:var(--blue)}\n.tree-tools{margin-left:auto;display:flex;align-items:center;gap:2px}\n.tree-tools button{width:28px;height:28px;border:0;background:none;border-radius:6px;\n color:var(--secondary);display:flex;align-items:center;justify-content:center;cursor:pointer}\n.tree-tools button:hover{background:rgba(255,255,255,.06);color:var(--ink)}\n#treeBody{flex:1;min-height:0;overflow:auto;padding:4px 8px 16px}\n.tree-scope{font-size:10.5px;letter-spacing:.1em;color:var(--muted);padding:10px 10px 4px}\n.tree-group{font-size:11.5px;color:var(--secondary);padding:4px 10px 2px}\n.tree-graph{display:flex;justify-content:space-between;align-items:center;height:28px;padding:0 10px;\n border-radius:6px;font-size:12.5px;color:var(--secondary);cursor:pointer}\n.tree-graph:hover{background:rgba(255,255,255,.06)}\n.tree-graph.active{background:var(--blue-soft);color:var(--blue-deep)}\n.tree-graph .n{font-size:10.5px;color:#6f737a;font-variant-numeric:tabular-nums}\n.tree-topic{height:26px;display:flex;align-items:center;padding:0 10px 0 24px;border-radius:6px;\n font-size:12px;color:var(--muted);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.tree-topic:hover{background:rgba(255,255,255,.06);color:var(--ink)}\n.tree-empty{padding:2px 10px 4px 24px;font-size:11.5px;color:#5f636a}\n.tree-kb{display:flex;align-items:center;gap:8px;height:28px;padding:0 10px;border-radius:6px;\n font-size:12.5px;color:var(--secondary);cursor:pointer}\n.tree-kb:hover{background:rgba(255,255,255,.06)}\n.tree-kb svg{width:14px;height:14px;color:#d9a441;flex:0 0 auto}\n.tree-kb.file svg{color:#8a8d95}\n.tree-kb .date{margin-left:auto;font-size:10.5px;color:#6f737a}\n#main{flex:1;min-width:0;display:flex;flex-direction:column;position:relative;background:#1a1a1c}\n#mainhead{height:48px;flex:0 0 48px;display:flex;align-items:center;gap:12px;padding:0 18px;\n border-bottom:.5px solid rgba(255,255,255,.10)}\n.crumb{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);min-width:0}\n.crumb b{color:var(--ink);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.crumb i{font-style:normal;color:#5f636a}\n#headMeta{font-size:11.5px;color:#7f838a;white-space:nowrap}\n.head-actions{margin-left:auto;display:flex;gap:8px}\n.head-actions button{height:28px;padding:0 10px;border:.5px solid rgba(255,255,255,.12);border-radius:6px;\n background:#232324;color:var(--secondary);font-size:12px;cursor:pointer}\n.head-actions button:hover{background:#2c2c2e;color:var(--ink)}\n#stage{position:relative;left:auto;right:auto;top:auto;bottom:auto;flex:1;min-height:0;overflow:hidden;cursor:grab;\n background:radial-gradient(900px 620px at 50% 38%,rgba(255,255,255,.03),transparent 70%),#1a1a1c}\n#stage::before{background-image:radial-gradient(rgba(255,255,255,.055) 1px,transparent 1px)}\n#controls{position:absolute;left:16px;bottom:16px;z-index:12}\n#controls button{background:#232324;border-color:rgba(255,255,255,.12);color:var(--secondary)}\n#controls button:hover{background:#2c2c2e;color:var(--ink)}\n#controls button.on{background:var(--blue);border-color:var(--blue);color:#0b1220}\n#zoomLabel{position:absolute;left:60px;bottom:21px;color:var(--muted)}\n.card{background:#232324;border-color:rgba(255,255,255,.10);\n box-shadow:0 1px 2px rgba(0,0,0,.3),0 10px 30px rgba(0,0,0,.28)}\n.card:hover{border-color:rgba(255,255,255,.20)}\n.card.selected{border-color:var(--blue);box-shadow:0 0 0 2px rgba(77,147,248,.20),0 10px 30px rgba(0,0,0,.35)}\n.card.dim{opacity:.32}\n.card h3{color:var(--ink)}\n.one{color:var(--secondary)}\n.tags span{background:#2c2c2e;border-color:rgba(255,255,255,.08);color:var(--secondary)}\n.doc{border-color:rgba(255,255,255,.12);box-shadow:0 8px 18px rgba(0,0,0,.40)}\n.edge{stroke:rgba(255,255,255,.22)}\n.edge.cross{stroke:rgba(255,255,255,.12)}\n.edge.hot{stroke:var(--blue)}\n.pill rect{fill:#232324;stroke:rgba(255,255,255,.14)}\n.pill text{fill:var(--secondary)}\n.pill.hot rect{fill:rgba(77,147,248,.16);stroke:var(--blue)}\n.pill.hot text{fill:var(--blue-deep)}\n#panel{background:#1e1e21;border-left-color:rgba(255,255,255,.10);box-shadow:-8px 0 28px rgba(0,0,0,.40)}\n.p-head{border-bottom-color:rgba(255,255,255,.06)}\n.p-kicker{color:var(--muted)}\n.p-head h2{color:var(--ink)}\n#close{background:#232324;border-color:rgba(255,255,255,.12);color:var(--muted)}\n#close:hover{background:#2c2c2e;color:var(--ink)}\n.p-field b{color:var(--muted)}\n.p-field span{color:var(--secondary)}\n.p-doc{color:var(--secondary)}\n.p-doc h2,.p-doc h3{color:var(--ink)}\n.p-doc code{background:#2c2c2e;border-color:rgba(255,255,255,.08)}\n.p-doc pre{background:#1a1a1c;border-color:rgba(255,255,255,.08)}\n.rel{background:#262629;border-color:rgba(255,255,255,.07)}\n.rel:hover{background:#2c2c2e;border-color:rgba(255,255,255,.12)}\n.rel b{color:var(--ink)}\n.rel span{color:var(--muted)}\n.p-doc-item{border-bottom-color:rgba(255,255,255,.06)}\n.p-doc-item .m b{color:var(--ink)}\n.p-doc-item .m span{color:var(--muted)}\n#kbView{flex:1;min-height:0;overflow:auto;padding:18px}\n.kb-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}\n.kb-card{display:flex;gap:13px;align-items:center;height:63px;padding:0 13px;border:.5px solid rgba(255,255,255,.10);\n border-radius:10px;background:#232324;text-align:left;cursor:pointer;color:var(--ink)}\n.kb-card:hover{background:#262629}\n.kb-card .plate{width:22px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:13px;flex:0 0 auto}\n.kb-card b{display:block;font-size:13.5px;color:var(--ink);font-weight:600}\n.kb-card small{display:block;font-size:11.5px;color:var(--muted);margin-top:2px}\n.kb-tabs{display:flex;gap:8px;margin:16px 0 6px;border-bottom:.5px solid rgba(255,255,255,.08)}\n.kb-tab{padding:8px 10px;font-size:12.5px;color:var(--muted);background:none;border:0;cursor:pointer;border-bottom:2px solid transparent}\n.kb-tab.active{color:var(--ink);border-bottom-color:var(--blue)}\n.kb-table{width:100%;border-collapse:collapse;font-size:12.5px}\n.kb-table th{text-align:left;font-weight:500;color:#7f838a;font-size:11px;padding:8px 10px;border-bottom:.5px solid rgba(255,255,255,.08)}\n.kb-table td{padding:9px 10px;border-bottom:.5px solid rgba(255,255,255,.06);color:var(--secondary)}\n.kb-table td.muted{color:var(--muted)}\n.kb-table tr:hover td{background:rgba(255,255,255,.03)}\n[hidden]{display:none !important}\n@media (max-width:900px){#rail{display:none}}\n</style>\n<style>\n/* ---- embedding page: no rail, no page header; the plugin owns both ---- */\n#main { width: 100%; }\n#kg-empty { position: absolute; inset: 0; z-index: 8; display: flex; flex-direction: column;\n align-items: center; justify-content: center; gap: 6px; background: #1a1a1c; color: #8a8d95;\n font-family: var(--sans); text-align: center; padding: 24px; }\n#kg-empty strong { font-size: 14px; font-weight: 600; color: #cfd3d6; }\n#kg-empty span { font-size: 12px; }\n</style>\n<style>\n/* ---- host theme bridge ----\n The canvas is a separate document, so it cannot inherit the host's custom\n properties. The parent posts every --dsw-alias-* value below into this page and\n writes it on :root. These fallbacks are only the first-paint values before that\n message lands; the live values always come from the host. */\n:root {\n --bg: var(--dsw-alias-bg-base, #fff);\n --bg-soft: var(--dsw-alias-bg-layer-1, #f6f7f9);\n --surface: var(--dsw-alias-bg-layer-2, #fff);\n --ink: var(--dsw-alias-label-primary, #0f1115);\n --ink-2: var(--dsw-alias-label-secondary, #3c3f47);\n --secondary: var(--dsw-alias-label-secondary, #61666b);\n /* 80% tertiary + 20% primary keeps the third level distinguishable while\n reaching 4.5:1 on the light surface (the raw light tertiary does not). */\n --muted: color-mix(in srgb, var(--dsw-alias-label-tertiary, #81858c) 80%, var(--dsw-alias-label-primary, #0f1115));\n --line: var(--dsw-alias-border-l2, rgba(0, 0, 0, .10));\n --line-2: var(--dsw-alias-border-l1, rgba(0, 0, 0, .06));\n --blue: var(--dsw-alias-state-business-primary, #4176e6);\n /* Accent text needs 4.5:1, and the raw link token is 4.2:1 on white. A 78/22 mix\n with the primary ink keeps the brand hue while clearing the body-text floor. */\n --blue-deep: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 78%, var(--dsw-alias-label-primary, #0f1115));\n --blue-soft: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 16%, transparent);\n --green: var(--dsw-alias-state-success-primary, #22c55e);\n --amber: var(--dsw-alias-state-warn-primary, #f59e0b);\n --shadow: var(--dsw-elevation-panel, 0 1px 2px rgba(0, 0, 0, .10));\n --stage-bg: color-mix(in srgb, var(--dsw-alias-bg-base, #fff) 96%, var(--dsw-alias-label-primary, #0f1115));\n --soft: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 6%, transparent);\n --dot: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 9%, transparent);\n --edge: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 24%, transparent);\n --edge-soft: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 14%, transparent);\n}\n\n#stage {\n background:\n radial-gradient(900px 620px at 50% 38%, color-mix(in srgb, var(--surface) 60%, transparent), transparent 70%),\n var(--stage-bg) !important;\n}\n#stage::before { background-image: radial-gradient(var(--dot) 1px, transparent 1px) !important; }\n#kg-empty { background: var(--stage-bg) !important; color: var(--muted) !important; }\n#kg-empty strong { color: var(--ink) !important; }\n.card:focus-visible, #controls button:focus-visible { outline-color: var(--blue) !important; }\n.p-doc-item img { border-color: var(--line) !important; }\n.card { background: var(--surface) !important; border-color: var(--line) !important; box-shadow: var(--shadow) !important; }\n.card:hover { border-color: color-mix(in srgb, var(--ink) 22%, transparent) !important; }\n.card.selected { border-color: var(--blue) !important; box-shadow: 0 0 0 2px var(--blue-soft), var(--shadow) !important; }\n/* Dimming is focus, not disappearance: 32% made secondary text unreadable. */\n.card.dim { opacity: .55 !important; }\n.card h3 { color: var(--ink) !important; }\n.one { color: var(--secondary) !important; font-size: 13px; line-height: 1.6; }\n.tags span {\n background: var(--soft) !important; border-color: var(--line-2) !important;\n color: var(--secondary) !important; font-size: 11px; padding: 2px 7px;\n}\n.dots i { background: var(--line) !important; }\n.dots i.on { background: var(--ink) !important; }\n.doc { border-color: var(--line) !important; box-shadow: var(--shadow) !important; }\n.doc-empty { border-color: var(--line) !important; color: var(--muted) !important; background: var(--bg-soft) !important; }\n.docGenerated {\n display: flex; flex-direction: column; gap: 3px; padding: 7px 8px;\n background: var(--surface) !important; color: var(--ink);\n border: 1px solid var(--line) !important; border-radius: 6px;\n overflow: hidden;\n}\n.docGenerated .docMark {\n color: var(--blue-deep); font: 600 7.5px/1 var(--mono); letter-spacing: .08em;\n}\n.docGenerated b {\n color: var(--ink); font-size: 8.5px; line-height: 1.25; font-weight: 600;\n overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;\n}\n.docGenerated i { margin-top: auto; color: var(--muted); font: normal 7px/1 var(--sans); }\n.docGenerated.wide { gap: 4px; padding: 9px 11px; }\n.docGenerated.wide .docMark { font-size: 9px; }\n.docGenerated.wide b { font-size: 12px; -webkit-line-clamp: 2; }\n.docGenerated.wide i { font-size: 8px; }\n.edge { stroke: var(--edge) !important; }\n.edge.cross { stroke: var(--edge-soft) !important; }\n.edge.hot { stroke: var(--blue) !important; }\n.pill rect { fill: var(--surface) !important; stroke: var(--line) !important; }\n.pill text { fill: var(--secondary) !important; }\n.pill.hot rect { fill: var(--blue-soft) !important; stroke: var(--blue) !important; }\n.pill.hot text { fill: var(--blue-deep) !important; }\n#controls button {\n background: var(--surface) !important; border-color: var(--line) !important;\n color: var(--ink-2) !important; box-shadow: var(--shadow) !important;\n}\n#controls button:hover { background: var(--bg-soft) !important; color: var(--ink) !important; }\n#controls button.on { background: var(--blue) !important; border-color: var(--blue) !important; color: var(--bg) !important; }\n#zoomLabel { color: var(--muted) !important; background: none !important; border: 0 !important;\n padding: 2px 4px !important; cursor: pointer; font-family: var(--mono); }\n#zoomLabel:hover { color: var(--ink) !important; }\n#panel { background: var(--surface) !important; border-left-color: var(--line) !important;\n box-shadow: -8px 0 28px color-mix(in srgb, var(--ink) 12%, transparent) !important; }\n#close { background: var(--surface) !important; border-color: var(--line) !important; color: var(--muted) !important; }\n#close:hover { background: var(--bg-soft) !important; color: var(--ink) !important; }\n.p-head { border-bottom-color: var(--line-2) !important; }\n.p-kicker { color: var(--muted) !important; }\n.p-head h2 { color: var(--ink) !important; }\n.p-field b { color: var(--muted) !important; }\n.p-field span, .p-doc { color: var(--secondary) !important; }\n.p-rel-title { color: var(--muted) !important; }\n.rel { background: var(--soft) !important; border-color: var(--line-2) !important; }\n.rel:hover { background: var(--bg-soft) !important; border-color: var(--line) !important; }\n.rel b { color: var(--ink) !important; }\n.rel span { color: var(--secondary) !important; }\n.p-docs { color: var(--muted) !important; }\n.p-doc-item { border-bottom-color: var(--line-2) !important; }\n.p-doc-item .m b { color: var(--ink) !important; }\n.p-doc-item .m span { color: var(--muted) !important; }\n.p-doc-thumb {\n display: flex; align-items: center; justify-content: center; flex: 0 0 auto;\n width: 34px; height: 46px; border-radius: 4px;\n border: 1px solid var(--line); background: var(--soft); color: var(--blue-deep);\n font: 600 8px/1 var(--mono); letter-spacing: .06em;\n}\n.p-doc h2, .p-doc h3 { color: var(--ink) !important; }\n.p-doc code { background: var(--soft) !important; border-color: var(--line-2) !important; }\n.p-doc pre { background: var(--bg) !important; border-color: var(--line-2) !important; }\n</style>\n</head>\n<body data-doc-layout=\"landscape\" data-view=\"graph\">\n<main id=\"main\">\n <div id=\"stage\">\n <div id=\"world\">\n <svg id=\"edges\" width=\"4200\" height=\"3200\" viewBox=\"0 0 4200 3200\" aria-hidden=\"true\"></svg>\n </div>\n </div>\n <div id=\"controls\" role=\"group\" aria-label=\"画布控制\">\n <button id=\"zin\" aria-label=\"放大\">+</button>\n <button id=\"zout\" aria-label=\"缩小\">−</button>\n <button id=\"fit\" aria-label=\"适应画布\">⤢</button>\n <button id=\"cross\" aria-label=\"显示交叉关系\" title=\"显示交叉关系\">⇄</button>\n <button id=\"relayout\" aria-label=\"重排\" title=\"重排\">↺</button>\n </div>\n <button id=\"zoomLabel\" type=\"button\" aria-label=\"重置缩放为 100%\" title=\"重置缩放为 100%\">100%</button>\n <div id=\"kg-empty\" hidden><strong>还没有可用的图谱</strong><span>在「知识图谱」文件夹里建好主题后,画布会出现在这里。</span></div>\n</main>\n\n<aside id=\"panel\" aria-label=\"主题详情\" aria-hidden=\"true\">\n <div class=\"p-head\">\n <div>\n <div class=\"p-kicker\" id=\"p-kicker\"></div>\n <h2 id=\"p-title\"></h2>\n </div>\n <button id=\"close\" aria-label=\"关闭\">×</button>\n </div>\n <div class=\"p-body\" id=\"p-body\"></div>\n</aside>\n<script>\nconst DATA = { graphs: [] };\n\n(function(){\n \"use strict\";\n const stage = document.getElementById('stage');\n const world = document.getElementById('world');\n const svg = document.getElementById('edges');\n const panel = document.getElementById('panel');\n const zoomLabel = document.getElementById('zoomLabel');\n\n const NS = 'http://www.w3.org/2000/svg';\n let topics = [];\n let links = [];\n let byId = new Map();\n let activeGraph = 0;\n let viewMode = 'graph';\n const cardEl = new Map();\n const view = {x:0, y:0, s:1};\n const CARD_W = 300, H_GAP = 110, V_GAP = 34, PAD = 40;\n const LAND = document.body.dataset.docLayout === 'landscape';\n\n let mode = null;\n let activeTopic = null;\n let activeLink = null;\n let userMoved = false;\n let children = new Map();\n let parent = new Map();\n let treeKeys = new Set();\n let pos = new Map();\n let heights = new Map(), subH = new Map();\n let showCross = false;\n let layoutRootId = null;\n\n function esc(s){\n return String(s == null ? '' : s).replace(/[&<>\"]/g, function(c){\n return {'&':'&','<':'<','>':'>','\"':'"'}[c];\n });\n }\n function statusText(s){ return s === 'active' ? '进行中' : s === 'paused' ? '暂停' : '已完成'; }\n function linkKey(l){ return l.a + '--' + l.b; }\n function pairKey(a, b){ return a < b ? a + '--' + b : b + '--' + a; }\n\n // ---------- cards ----------\n\n // Documents in the knowledge tree have no stored thumbnail. A generated cover\n // uses the host's own theme variables (set from the parent document), so a light\n // host gets ink-on-white covers and a dark host gets the inverse. No bitmap and\n // no fixed palette: the cover is markup, not an image.\n function coverFor(d, idx, wide, w, h, dx, dy, rot, sc, z){\n const style = 'width:' + w + 'px;height:' + h + 'px;--dx:' + dx + 'px;--dy:' + dy +\n 'px;--rot:' + rot + 'deg;--sc:' + sc + ';z-index:' + z;\n if (d.thumb) {\n const src = (wide && d.thumbWide) ? d.thumbWide : d.thumb;\n return '<img class=\"doc\" src=\"' + src + '\" alt=\"' + esc(d.title) + '\" style=\"' + style + '\">';\n }\n const ext = String(d.file || 'doc').split('.').pop().slice(0, 4).toUpperCase();\n return '<div class=\"doc docGenerated' + (wide ? ' wide' : '') + '\" style=\"' + style + '\" title=\"' + esc(d.title) + '\">' +\n '<span class=\"docMark\">' + esc(ext) + '</span>' +\n '<b>' + esc(d.title || d.file || '') + '</b>' +\n '<i>' + esc(d.date || '') + '</i></div>';\n }\n\n // The detail panel renders the same cover inline, at a fixed 34x46 slot.\n function inlineCover(d){\n if (d.thumb) return '<img src=\"' + d.thumb + '\" alt=\"\">';\n const ext = String(d.file || 'doc').split('.').pop().slice(0, 4).toUpperCase();\n return '<span class=\"p-doc-thumb\"><i>' + esc(ext) + '</i></span>';\n }\n\n function buildCard(t){\n const el = document.createElement('article');\n el.className = 'card';\n el.dataset.id = t.id;\n el.tabIndex = 0;\n el.style.left = '0px';\n el.style.top = '0px';\n el.style.visibility = 'hidden';\n el.setAttribute('role', 'button');\n el.setAttribute('aria-label', t.title + ',' + statusText(t.status) + ',深度 ' + t.depth);\n\n const dots = [1,2,3,4,5].map(i =>\n '<i class=\"' + (i <= t.depth ? 'on' : '') + '\"></i>').join('');\n const docs = t.documents || [];\n const shown = docs.slice(0, 4);\n const mid = (shown.length - 1) / 2;\n let docW = 86, docH = 116, step = 31, rotStep = 6.5, dyStep = 2, scStep = 0.03, coverH = 124;\n if (LAND){\n if (shown.length <= 1){ docW = 240; docH = 84; step = 0; rotStep = 0; dyStep = 0; scStep = 0; coverH = 92; }\n else if (shown.length === 2){ docW = 142; docH = 78; step = 58; rotStep = 5; dyStep = 3; scStep = 0.02; coverH = 88; }\n else if (shown.length === 3){ docW = 132; docH = 76; step = 46; rotStep = 4.2; dyStep = 3; scStep = 0.02; coverH = 86; }\n else { docW = 118; docH = 70; step = 42; rotStep = 4; dyStep = 3; scStep = 0.02; coverH = 80; }\n }\n let stack = '';\n shown.forEach(function(d, i){\n const dist = Math.abs(i - mid);\n const dx = (i - mid) * step;\n const dy = dist * dyStep;\n const rot = (i - mid) * rotStep;\n const sc = 1 - dist * scStep;\n const z = 10 - dist;\n stack += coverFor(d, i, shown.length === 1, docW, docH, dx, dy, rot, sc, z);\n });\n if (docs.length > 4) stack += '<span class=\"more\">+' + (docs.length - 4) + '</span>';\n if (!docs.length) stack = '<div class=\"doc-empty\">docs/ 还是空的</div>';\n const tagList = [t.domain].concat(t.tags || []).filter(function(x, i, arr){\n return x && arr.indexOf(x) === i;\n }).slice(0, 4);\n const tags = tagList.map(function(x){ return '<span>' + esc(x) + '</span>'; }).join('');\n\n el.innerHTML =\n '<h3>' + esc(t.title) + '</h3>' +\n '<div class=\"cover\" style=\"height:' + coverH + 'px\"><div class=\"stack\">' + stack + '</div></div>' +\n '<p class=\"one\">' + esc(t.summary) + '</p>' +\n '<div class=\"tags\">' + tags + '</div>';\n\n el.addEventListener('pointerdown', onCardDown);\n el.addEventListener('keydown', function(e){\n if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); selectTopic(t.id); }\n });\n world.appendChild(el);\n cardEl.set(t.id, el);\n }\n\n // ---------- spanning tree ----------\n function degree(id){\n return links.filter(function(l){ return l.a === id || l.b === id; }).length;\n }\n function buildTree(rootId){\n children = new Map(); parent = new Map(); treeKeys = new Set();\n const visited = new Set([rootId]);\n const queue = [rootId];\n topics.forEach(function(t){ children.set(t.id, []); });\n while (queue.length){\n const id = queue.shift();\n const neighbors = [];\n links.forEach(function(l){\n if (l.a === id && !visited.has(l.b)) neighbors.push(l.b);\n else if (l.b === id && !visited.has(l.a)) neighbors.push(l.a);\n });\n neighbors.sort(function(a, b){ return degree(b) - degree(a); });\n neighbors.forEach(function(n){\n visited.add(n);\n parent.set(n, id);\n children.get(id).push(n);\n treeKeys.add(pairKey(id, n));\n queue.push(n);\n });\n }\n topics.forEach(function(t){\n if (!visited.has(t.id)){\n parent.set(t.id, rootId);\n children.get(rootId).push(t.id);\n treeKeys.add(pairKey(rootId, t.id));\n visited.add(t.id);\n }\n });\n }\n\n // ---------- mind-map layout ----------\n function calcSub(id){\n const h = heights.get(id) || 200;\n const kids = children.get(id) || [];\n if (!kids.length){ subH.set(id, h); return h; }\n let total = 0;\n kids.forEach(function(k, i){\n total += calcSub(k);\n if (i) total += V_GAP;\n });\n subH.set(id, Math.max(h, total));\n return subH.get(id);\n }\n\n function placeNode(id, x, yTop, dir){\n const h = heights.get(id) || 200;\n const sub = subH.get(id) || h;\n const cy = yTop + sub / 2;\n pos.set(id, {x:x, y:cy - h / 2, dir:dir});\n const kids = children.get(id) || [];\n let cursor = yTop;\n kids.forEach(function(k){\n const kh = subH.get(k) || 200;\n const kx = dir > 0 ? x + CARD_W + H_GAP : x - CARD_W - H_GAP;\n placeNode(k, kx, cursor, dir);\n cursor += kh + V_GAP;\n });\n }\n\n function layout(){\n heights = new Map();\n topics.forEach(function(t){ heights.set(t.id, cardEl.get(t.id).offsetHeight || 200); });\n\n let rootId = topics[0].id, best = -1;\n topics.forEach(function(t){\n const d = degree(t.id);\n if (d > best){ best = d; rootId = t.id; }\n });\n if (byId.has('dsh-enterprise-app')) rootId = 'dsh-enterprise-app';\n layoutRootId = rootId;\n buildTree(rootId);\n calcSub(rootId);\n\n const first = (children.get(rootId) || []).slice();\n first.sort(function(a, b){ return (subH.get(b) || 0) - (subH.get(a) || 0); });\n\n const left = [], right = [];\n let lh = 0, rh = 0;\n first.forEach(function(id){\n if (rh <= lh){ right.push(id); rh += subH.get(id); }\n else { left.push(id); lh += subH.get(id); }\n });\n\n const rootH = heights.get(rootId);\n pos.set(rootId, {x:0, y:-rootH / 2, dir:0});\n\n function placeSide(list, dir){\n let total = 0;\n list.forEach(function(id, i){ total += subH.get(id); if (i) total += V_GAP; });\n let cursor = -total / 2;\n list.forEach(function(id){\n const kh = subH.get(id);\n const x = dir > 0 ? CARD_W + H_GAP : -CARD_W - H_GAP;\n placeNode(id, x, cursor, dir);\n cursor += kh + V_GAP;\n });\n }\n placeSide(left, -1);\n placeSide(right, 1);\n\n let minX = Infinity, minY = Infinity;\n pos.forEach(function(p){ minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); });\n pos.forEach(function(p, id){\n p.x = p.x - minX + PAD;\n p.y = p.y - minY + PAD;\n const t = byId.get(id);\n t.x = p.x; t.y = p.y;\n const el = cardEl.get(id);\n el.style.left = p.x + 'px';\n el.style.top = p.y + 'px';\n el.style.visibility = '';\n });\n }\n\n // ---------- edges & pills ----------\n function center(id){\n const p = pos.get(id), h = heights.get(id) || 200;\n return {x:p.x + CARD_W / 2, y:p.y + h / 2};\n }\n function anchor(id, tx, ty){\n const c = center(id), h = heights.get(id) || 200;\n const dx = tx - c.x, dy = ty - c.y;\n if (!dx && !dy) return c;\n if (Math.abs(dx) * h > Math.abs(dy) * CARD_W){\n const k = (CARD_W / 2) / Math.abs(dx);\n return {x:c.x + dx * k, y:c.y + dy * k};\n }\n const k = (h / 2) / Math.abs(dy);\n return {x:c.x + dx * k, y:c.y + dy * k};\n }\n function qpoint(p0, c, p1, t){\n const u = 1 - t;\n return {x:u*u*p0.x + 2*u*t*c.x + t*t*p1.x, y:u*u*p0.y + 2*u*t*c.y + t*t*p1.y};\n }\n function cpoint(p0, c1, c2, p1, t){\n const u = 1 - t, a = u*u*u, b = 3*u*u*t, c = 3*u*t*t, d = t*t*t;\n return {x:a*p0.x + b*c1.x + c*c2.x + d*p1.x,\n y:a*p0.y + b*c1.y + c*c2.y + d*p1.y};\n }\n function hitsCard(x, y, w, h){\n for (let i = 0; i < topics.length; i++){\n const t = topics[i], th = heights.get(t.id) || 200;\n if (x < t.x + CARD_W + 6 && x + w > t.x - 6 &&\n y < t.y + th + 6 && y + h > t.y - 6) return true;\n }\n return false;\n }\n function freePointOnQuad(p0, c, p1){\n const ts = [0.5, 0.4, 0.6, 0.3, 0.7, 0.22, 0.78];\n for (let i = 0; i < ts.length; i++){\n const p = qpoint(p0, c, p1, ts[i]);\n if (!hitsCard(p.x, p.y)) return p;\n }\n return qpoint(p0, c, p1, 0.5);\n }\n function freePointOnCubic(p0, c1, c2, p1){\n const ts = [0.5, 0.42, 0.58, 0.34, 0.66, 0.26, 0.74];\n for (let i = 0; i < ts.length; i++){\n const p = cpoint(p0, c1, c2, p1, ts[i]);\n if (!hitsCard(p.x, p.y)) return p;\n }\n return cpoint(p0, c1, c2, p1, 0.5);\n }\n\n function overlaps(p, w, placed){\n for (let i = 0; i < placed.length; i++){\n const q = placed[i];\n if (Math.abs(p.x - q.x) < (w + q.w) / 2 + 8 && Math.abs(p.y - q.y) < 24) return true;\n }\n return false;\n }\n function placePill(sample, w, placed){\n const ts = [0.5, 0.56, 0.44, 0.62, 0.38, 0.68, 0.32, 0.74, 0.26, 0.8];\n for (let i = 0; i < ts.length; i++){\n const p = sample(ts[i]);\n if (!hitsCard(p.x - w / 2, p.y - 10, w, 20) && !overlaps(p, w, placed)){\n placed.push({x:p.x, y:p.y, w:w});\n return p;\n }\n }\n const p = sample(0.5);\n placed.push({x:p.x, y:p.y, w:w});\n return p;\n }\n\n function drawEdges(){\n while (svg.firstChild) svg.removeChild(svg.firstChild);\n const placed = [];\n\n links.forEach(function(l){\n const a = byId.get(l.a), b = byId.get(l.b);\n if (!a || !b) return;\n const key = linkKey(l);\n const isTree = treeKeys.has(pairKey(l.a, l.b));\n const isLink = mode === 'link' && activeLink === key;\n const isTopic = mode === 'topic' && (l.a === activeTopic || l.b === activeTopic);\n const hot = isLink || isTopic;\n const cold = !!mode && !hot;\n if (!isTree && !showCross && !hot) return;\n\n let d, sample;\n if (isTree){\n const pid = parent.get(l.b) === l.a ? l.a : l.b;\n const cid = pid === l.a ? l.b : l.a;\n const pp = pos.get(pid), pc = center(pid), cc = center(cid);\n const dir = pp.dir || (cc.x >= pc.x ? 1 : -1);\n const x1 = dir > 0 ? pp.x + CARD_W : pp.x;\n const x2 = dir > 0 ? pos.get(cid).x : pos.get(cid).x + CARD_W;\n const y1 = pc.y, y2 = cc.y;\n const bend = Math.max(34, Math.abs(x2 - x1) * 0.42);\n const p0 = {x:x1, y:y1};\n const c1 = {x:x1 + dir * bend, y:y1};\n const c2 = {x:x2 - dir * bend, y:y2};\n const p1 = {x:x2, y:y2};\n d = 'M' + x1 + ',' + y1 + ' C' + c1.x + ',' + c1.y + ' ' + c2.x + ',' + c2.y + ' ' + x2 + ',' + y2;\n sample = function(t){ return cpoint(p0, c1, c2, p1, t); };\n } else {\n const pa = pos.get(l.a), pb = pos.get(l.b);\n let p1, p2, c;\n if (pa.dir === pb.dir && pa.dir !== 0){\n const dir = pa.dir;\n p1 = {x: dir < 0 ? pa.x + CARD_W : pa.x, y: center(l.a).y};\n p2 = {x: dir < 0 ? pb.x + CARD_W : pb.x, y: center(l.b).y};\n const outX = dir < 0 ? Math.max(p1.x, p2.x) + 64 : Math.min(p1.x, p2.x) - 64;\n c = {x: outX, y: (p1.y + p2.y) / 2};\n } else {\n const ca = center(l.a), cb = center(l.b);\n p1 = {x: ca.x, y: pa.y};\n p2 = {x: cb.x, y: pb.y};\n c = {x: (p1.x + p2.x) / 2, y: Math.min(p1.y, p2.y) - 112};\n }\n d = 'M' + p1.x + ',' + p1.y + ' Q' + c.x + ',' + c.y + ' ' + p2.x + ',' + p2.y;\n sample = function(t){ return qpoint(p1, c, p2, t); };\n }\n\n const path = document.createElementNS(NS, 'path');\n path.setAttribute('d', d);\n path.setAttribute('class', 'edge' + (isTree ? '' : ' cross') + (hot ? ' hot' : cold ? ' cold' : ''));\n svg.appendChild(path);\n\n const label = l.label || (a.title + ' ↔ ' + b.title);\n const w = Math.max(42, label.length * 10.8 + 16);\n const pillAt = placePill(sample, w, placed);\n\n const g = document.createElementNS(NS, 'g');\n g.setAttribute('class', 'pill' + (hot ? ' hot' : cold ? ' cold' : ''));\n g.setAttribute('data-key', key);\n g.setAttribute('tabindex', '0');\n g.setAttribute('role', 'button');\n g.setAttribute('aria-label', '关系:' + label);\n\n const rect = document.createElementNS(NS, 'rect');\n rect.setAttribute('x', pillAt.x - w / 2); rect.setAttribute('y', pillAt.y - 10);\n rect.setAttribute('width', w); rect.setAttribute('height', 20);\n rect.setAttribute('rx', 10);\n const text = document.createElementNS(NS, 'text');\n text.setAttribute('x', pillAt.x); text.setAttribute('y', pillAt.y + 3.6);\n text.setAttribute('text-anchor', 'middle');\n text.textContent = label;\n\n g.appendChild(rect); g.appendChild(text);\n g.addEventListener('click', function(e){ e.stopPropagation(); selectLink(key); });\n g.addEventListener('keydown', function(e){\n if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); selectLink(key); }\n });\n svg.appendChild(g);\n });\n }\n\n // ---------- camera ----------\n function apply(){\n world.style.transform = 'translate(' + view.x + 'px,' + view.y + 'px) scale(' + view.s + ')';\n zoomLabel.textContent = Math.round(view.s * 100) + '%';\n }\n function clampScale(s){ return Math.max(0.22, Math.min(2.0, s)); }\n function zoomAt(px, py, k){\n const s2 = clampScale(view.s * k);\n if (s2 === view.s) return;\n view.x = px - (px - view.x) * (s2 / view.s);\n view.y = py - (py - view.y) * (s2 / view.s);\n view.s = s2;\n apply(); userMoved = true;\n }\n function bbox(){\n let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;\n topics.forEach(function(t){\n const h = heights.get(t.id) || 200;\n x1 = Math.min(x1, t.x); y1 = Math.min(y1, t.y);\n x2 = Math.max(x2, t.x + CARD_W); y2 = Math.max(y2, t.y + h);\n });\n return {x1:x1, y1:y1, x2:x2, y2:y2};\n }\n function fit(animate){\n const b = bbox();\n const pad = 70;\n const vw = stage.clientWidth, vh = stage.clientHeight;\n const s = clampScale(Math.min((vw - pad * 2) / (b.x2 - b.x1), (vh - pad * 2) / (b.y2 - b.y1), 1));\n const nx = (vw - (b.x2 - b.x1) * s) / 2 - b.x1 * s;\n const ny = (vh - (b.y2 - b.y1) * s) / 2 - b.y1 * s;\n if (animate) tweenTo(nx, ny, s); else { view.x = nx; view.y = ny; view.s = s; apply(); }\n userMoved = false;\n }\n let raf = null;\n function tweenTo(x2, y2, s2){\n cancelAnimationFrame(raf);\n const x1 = view.x, y1 = view.y, s1 = view.s, t0 = performance.now(), dur = 320;\n function step(now){\n const p = Math.min(1, (now - t0) / dur);\n const e = 1 - Math.pow(1 - p, 3);\n view.x = x1 + (x2 - x1) * e;\n view.y = y1 + (y2 - y1) * e;\n view.s = s1 + (s2 - s1) * e;\n apply();\n if (p < 1) raf = requestAnimationFrame(step);\n }\n raf = requestAnimationFrame(step);\n }\n function centerOnScale(id, s, animate){\n const t = byId.get(id); if (!t) return;\n const h = heights.get(id) || 200;\n const x = stage.clientWidth / 2 - (t.x + CARD_W / 2) * s;\n const y = stage.clientHeight / 2 - (t.y + h / 2) * s;\n if (animate) tweenTo(x, y, s);\n else { view.x = x; view.y = y; view.s = s; apply(); }\n userMoved = true;\n }\n\n // A topic jump keeps whatever zoom the reader chose; only the explicit reset\n // control and a graph switch change the scale.\n function centerOn(id){\n centerOnScale(id, view.s, true);\n }\n\n function resetZoom(){\n const target = (mode === 'topic' && activeTopic) ? activeTopic : layoutRootId;\n centerOnScale(target, 1, true);\n }\n\n // ---------- pan & zoom ----------\n let panning = false, panMoved = false, px = 0, py = 0;\n stage.addEventListener('pointerdown', function(e){\n if (e.target.closest('.card') || e.target.closest('.pill') || e.target.closest('#panel')) return;\n panning = true; panMoved = false; px = e.clientX; py = e.clientY;\n stage.classList.add('grabbing');\n stage.setPointerCapture(e.pointerId);\n });\n stage.addEventListener('pointermove', function(e){\n if (!panning) return;\n if (Math.abs(e.clientX - px) + Math.abs(e.clientY - py) > 3) panMoved = true;\n view.x += e.clientX - px; view.y += e.clientY - py;\n px = e.clientX; py = e.clientY;\n apply(); userMoved = true;\n });\n function endPan(e){\n if (!panning) return;\n panning = false; stage.classList.remove('grabbing');\n try { stage.releasePointerCapture(e.pointerId); } catch(_){}\n if (!panMoved && mode) closePanel();\n }\n stage.addEventListener('pointerup', endPan);\n stage.addEventListener('pointercancel', endPan);\n stage.addEventListener('wheel', function(e){\n e.preventDefault();\n zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.0011));\n }, {passive:false});\n document.getElementById('zin').addEventListener('click', function(){\n zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 1.22);\n });\n document.getElementById('zout').addEventListener('click', function(){\n zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 1 / 1.22);\n });\n document.getElementById('fit').addEventListener('click', function(){ fit(true); });\n document.getElementById('zoomLabel').addEventListener('click', resetZoom);\n document.getElementById('relayout').addEventListener('click', function(){\n layout(); fit(true); drawEdges();\n });\n document.getElementById('cross').addEventListener('click', function(){\n showCross = !showCross;\n this.classList.toggle('on', showCross);\n this.setAttribute('aria-pressed', showCross ? 'true' : 'false');\n drawEdges();\n });\n stage.addEventListener('dblclick', function(e){ if (!e.target.closest('.card')) fit(true); });\n\n // ---------- card drag ----------\n let drag = null;\n function onCardDown(e){\n if (e.button !== 0) return;\n e.stopPropagation();\n const id = e.currentTarget.dataset.id;\n const t = byId.get(id);\n const el = cardEl.get(id);\n drag = {id:id, sx:e.clientX, sy:e.clientY, ox:t.x, oy:t.y, moved:false, el:el};\n el.classList.add('dragging');\n window.addEventListener('pointermove', onDragMove);\n window.addEventListener('pointerup', onDragUp);\n }\n function onDragMove(e){\n if (!drag) return;\n const dx = (e.clientX - drag.sx) / view.s;\n const dy = (e.clientY - drag.sy) / view.s;\n if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;\n const t = byId.get(drag.id);\n t.x = drag.ox + dx; t.y = drag.oy + dy;\n const p = pos.get(drag.id); p.x = t.x; p.y = t.y;\n drag.el.style.left = t.x + 'px';\n drag.el.style.top = t.y + 'px';\n drawEdges();\n }\n function onDragUp(){\n window.removeEventListener('pointermove', onDragMove);\n window.removeEventListener('pointerup', onDragUp);\n if (!drag) return;\n drag.el.classList.remove('dragging');\n const id = drag.id, moved = drag.moved;\n drag = null;\n if (!moved) selectTopic(id);\n userMoved = true;\n }\n\n // ---------- selection ----------\n function paint(){\n cardEl.forEach(function(el, cid){\n let sel = false, dim = false;\n if (mode === 'topic'){ sel = cid === activeTopic; dim = !sel; }\n else if (mode === 'link'){\n const l = links.find(function(x){ return linkKey(x) === activeLink; });\n sel = !!l && (cid === l.a || cid === l.b);\n dim = !sel;\n }\n el.classList.toggle('selected', sel);\n el.classList.toggle('dim', dim);\n });\n }\n function notifySelection(topicId){\n try {\n parent.postMessage({\n type: 'kg:selection',\n graphId: (DATA.graphs[activeGraph] || {}).id || null,\n topicId: topicId || null\n }, '*');\n } catch (_) {}\n }\n function selectTopic(id){\n if (mode === 'topic' && activeTopic === id){ closePanel(); return; }\n mode = 'topic'; activeTopic = id; activeLink = null;\n paint(); drawEdges(); openTopicPanel(id); setHash(id);\n notifySelection(id);\n }\n function selectLink(key){\n if (mode === 'link' && activeLink === key){ closePanel(); return; }\n mode = 'link'; activeLink = key; activeTopic = null;\n paint(); drawEdges(); openLinkPanel(key); setHash(key);\n }\n function closePanel(){\n mode = null; activeTopic = null; activeLink = null;\n paint(); drawEdges();\n panel.classList.remove('open');\n panel.setAttribute('aria-hidden', 'true');\n document.body.classList.remove('panel-open');\n setHash(null);\n notifySelection(null);\n }\n document.getElementById('close').addEventListener('click', closePanel);\n document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closePanel(); });\n\n function showPanel(kicker, title, html){\n document.getElementById('p-kicker').textContent = kicker;\n document.getElementById('p-title').textContent = title;\n const body = document.getElementById('p-body');\n body.innerHTML = html;\n body.scrollTop = 0;\n body.querySelectorAll('.rel').forEach(function(btn){\n btn.addEventListener('click', function(){\n const to = btn.dataset.to;\n selectTopic(to); centerOn(to);\n });\n });\n panel.classList.add('open');\n panel.setAttribute('aria-hidden', 'false');\n document.body.classList.add('panel-open');\n }\n function inline(s){\n s = esc(s);\n s = s.replace(/`([^`]+)`/g, '<code>$1</code>');\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener\">$1</a>');\n return s;\n }\n function renderMarkdown(md){\n const lines = String(md || '').split('\\n');\n let html = '', inCode = false, code = [], list = null;\n function closeList(){ if (list){ html += '</' + list + '>'; list = null; } }\n lines.forEach(function(line){\n if (/^```/.test(line)){\n if (inCode){ html += '<pre><code>' + esc(code.join('\\n')) + '</code></pre>'; code = []; inCode = false; }\n else { closeList(); inCode = true; }\n return;\n }\n if (inCode){ code.push(line); return; }\n if (!line.trim()){ closeList(); return; }\n let m;\n if ((m = line.match(/^(#{1,3})\\s+(.*)$/))){\n closeList();\n const lv = Math.min(3, m[1].length);\n html += '<h' + lv + '>' + inline(m[2]) + '</h' + lv + '>';\n } else if ((m = line.match(/^\\s*[-*]\\s+(.*)$/))){\n if (list !== 'ul'){ closeList(); html += '<ul>'; list = 'ul'; }\n html += '<li>' + inline(m[1]) + '</li>';\n } else if ((m = line.match(/^\\s*\\d+\\.\\s+(.*)$/))){\n if (list !== 'ol'){ closeList(); html += '<ol>'; list = 'ol'; }\n html += '<li>' + inline(m[1]) + '</li>';\n } else if ((m = line.match(/^>\\s?(.*)$/))){\n closeList();\n html += '<blockquote>' + inline(m[1]) + '</blockquote>';\n } else {\n closeList();\n html += '<p>' + inline(line) + '</p>';\n }\n });\n if (inCode) html += '<pre><code>' + esc(code.join('\\n')) + '</code></pre>';\n closeList();\n return html;\n }\n function field(k, v){\n return '<div class=\"p-field\"><b>' + k + '</b><span>' + esc(v || '未登记') + '</span></div>';\n }\n function openTopicPanel(id){\n const t = byId.get(id);\n const rels = links.filter(function(l){ return l.a === id || l.b === id; });\n let html = '<div class=\"p-doc\">' + renderMarkdown(t.body || t.summary || '') + '</div>';\n if (t.documents && t.documents.length){\n html += '<div class=\"p-rel-title\">文档 · ' + t.documents.length + ' 篇</div>';\n t.documents.forEach(function(d){\n html += '<div class=\"p-doc-item\">' + inlineCover(d) + '<div class=\"m\">' +\n '<b>' + esc(d.title) + '</b><span>' + esc(d.file) + ' · ' + esc(d.date) + '</span></div></div>';\n });\n }\n html += '<div class=\"p-rel-title\">关系 · ' + rels.length + ' 篇</div>';\n rels.forEach(function(l){\n const other = l.a === id ? l.b : l.a;\n const o = byId.get(other);\n html += '<button class=\"rel\" data-to=\"' + esc(other) + '\"><b>' + esc(o ? o.title : other) +\n '</b><span>' + esc(l.body || '') + '</span></button>';\n });\n if (!rels.length) html += '<div class=\"p-docs\">还没有关系文档。孤立的线价值很低。</div>';\n html += '<div class=\"p-docs\">文档:' + ((t.documents || []).length) + ' 个文件</div>';\n showPanel((t.domain ? t.domain + ' · ' : '') + statusText(t.status) + ' · depth ' + t.depth + '/5', t.title, html);\n }\n function openLinkPanel(key){\n const l = links.find(function(x){ return linkKey(x) === key; });\n if (!l) return;\n const a = byId.get(l.a), b = byId.get(l.b);\n let html = field('关系', l.body);\n html += '<div class=\"p-rel-title\">两端</div>';\n html += '<button class=\"rel\" data-to=\"' + esc(l.a) + '\"><b>' + esc(a ? a.title : l.a) +\n '</b><span>' + esc(a ? a.summary : '') + '</span></button>';\n html += '<button class=\"rel\" data-to=\"' + esc(l.b) + '\"><b>' + esc(b ? b.title : l.b) +\n '</b><span>' + esc(b ? b.summary : '') + '</span></button>';\n showPanel('关系文档' + (l.updated ? ' · ' + l.updated : ''),\n (a ? a.title : l.a) + ' ↔ ' + (b ? b.title : l.b), html);\n }\n function setHash(v){\n try { history.replaceState(null, '', v ? '#' + v : location.pathname + location.search); } catch(_){}\n }\n\n // ---------- graph switching ----------\n function loadGraph(idx){\n const g = DATA.graphs[idx];\n if (!g) return;\n closePanel();\n activeGraph = idx;\n topics = g.topics;\n links = g.links;\n byId = new Map(topics.map(function(t){ return [t.id, t]; }));\n cardEl.forEach(function(el){ el.remove(); });\n cardEl.clear();\n while (svg.firstChild) svg.removeChild(svg.firstChild);\n heights = new Map(); subH = new Map(); pos = new Map();\n\n topics.forEach(buildCard);\n layout();\n apply();\n\n requestAnimationFrame(function(){\n // First entry and every graph switch start at 1:1. The reader asked for a\n // true-scale canvas; fitting the whole graph on open made every card's\n // 12-13px copy render at 64%. Panning still reaches the rest.\n centerOnScale(layoutRootId, 1, false);\n drawEdges();\n });\n }\n\n // ---------- the embedding host ----------\n const emptyEl = document.getElementById('kg-empty');\n\n // The host's alias tokens, read from the parent document (same origin) and kept\n // live by the panel's postMessage. Reading the parent on boot avoids a flash of\n // the wrong scheme before the first message; the message is still required for\n // a later theme switch.\n const THEME_TOKENS = [\n '--dsw-alias-bg-base', '--dsw-alias-bg-layer-1', '--dsw-alias-bg-layer-2',\n '--dsw-alias-label-primary', '--dsw-alias-label-secondary', '--dsw-alias-label-tertiary',\n '--dsw-alias-border-l1', '--dsw-alias-border-l2',\n '--dsw-alias-state-business-primary', '--dsw-alias-link',\n '--dsw-alias-state-success-primary', '--dsw-alias-state-warn-primary',\n '--dsw-elevation-panel'\n ];\n\n function readHostTheme(){\n try {\n const doc = parent.document;\n const bodyStyle = parent.getComputedStyle(doc.body);\n const rootStyle = parent.getComputedStyle(doc.documentElement);\n const tokens = {};\n THEME_TOKENS.forEach(function(name){\n const value = (bodyStyle.getPropertyValue(name) || rootStyle.getPropertyValue(name) || '').trim();\n if (value !== '') tokens[name] = value;\n });\n return { tokens: tokens, dark: doc.body.hasAttribute('data-ds-dark-theme') };\n } catch (_) {\n return null;\n }\n }\n\n function applyTheme(theme){\n if (!theme || !theme.tokens) return;\n const root = document.documentElement;\n Object.keys(theme.tokens).forEach(function(name){\n root.style.setProperty(name, theme.tokens[name]);\n });\n root.style.colorScheme = theme.dark ? 'dark' : 'light';\n document.body.toggleAttribute('data-ds-dark-theme', !!theme.dark);\n }\n\n applyTheme(readHostTheme());\n\n function showEmpty(show){\n if (emptyEl) emptyEl.hidden = !show;\n const stageEl = document.getElementById('stage');\n if (stageEl) stageEl.style.visibility = show ? 'hidden' : 'visible';\n const controlsEl = document.getElementById('controls');\n if (controlsEl) controlsEl.hidden = show;\n const zoomEl = document.getElementById('zoomLabel');\n if (zoomEl) zoomEl.hidden = show;\n }\n\n function postReady(){\n try { parent.postMessage({ type: 'kg:ready' }, '*'); } catch (_) {}\n }\n\n function applyPayload(payload){\n if (!payload || !Array.isArray(payload.graphs)) return;\n DATA.graphs = payload.graphs;\n if (!DATA.graphs.length){\n showEmpty(true);\n postReady();\n return;\n }\n showEmpty(false);\n const wanted = payload.activeGraphId\n ? DATA.graphs.findIndex(function(g){ return g.id === payload.activeGraphId; })\n : -1;\n const idx = wanted >= 0 ? wanted : 0;\n loadGraph(idx);\n if (payload.selectedTopicId){\n requestAnimationFrame(function(){ focusTopic(payload.selectedTopicId); });\n }\n postReady();\n }\n\n function focusTopic(id){\n const t = byId.get(id);\n if (!t) return;\n selectTopic(id);\n centerOn(id);\n }\n\n function selectGraphById(id){\n const idx = DATA.graphs.findIndex(function(g){ return g.id === id; });\n if (idx < 0) return;\n showEmpty(false);\n loadGraph(idx);\n }\n\n window.addEventListener('message', function(event){\n const msg = event.data || {};\n if (msg.type === 'kg:set-graphs') applyPayload(msg);\n else if (msg.type === 'kg:select-graph') selectGraphById(msg.graphId);\n else if (msg.type === 'kg:select-topic') focusTopic(msg.topicId);\n else if (msg.type === 'kg:theme') applyTheme(msg);\n });\n\n showEmpty(true);\n postReady();\n window.addEventListener('resize', function(){\n if (!userMoved) centerOnScale(layoutRootId, view.s, false);\n drawEdges();\n });\n\n window.__KG_VIEW__ = {data:DATA, selectTopic:selectTopic, selectLink:selectLink, fit:fit};\n})();\n<\/script>\n</body>\n</html>\n";
|
|
1676
1679
|
//#endregion
|
|
1677
1680
|
//#region src/viewMetadata.ts
|
|
1678
1681
|
/**
|
|
@@ -11445,6 +11448,8 @@ const KNOWLEDGE_SKILL_NAME = "knowledge-base";
|
|
|
11445
11448
|
const KB_SUMMARIZE_SKILL_NAME = "kb-summarize";
|
|
11446
11449
|
/** Third bundled skill: explicit document/directory vectorization. */
|
|
11447
11450
|
const KB_VECTORIZE_SKILL_NAME = "kb-vectorize";
|
|
11451
|
+
const KNOWLEDGE_GRAPH_SKILL_NAME = "knowledge-graph";
|
|
11452
|
+
const KNOWLEDGE_GRAPH_SKILL_DESCRIPTION = "读写团队知识库里的知识图谱:主题卡、关系文档与四个动作。";
|
|
11448
11453
|
/**
|
|
11449
11454
|
* Short description shown in the skill list.
|
|
11450
11455
|
*
|
|
@@ -11494,6 +11499,11 @@ function kbVectorizeSkillPath() {
|
|
|
11494
11499
|
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
11495
11500
|
return join(moduleDirectory, "..", "skills", KB_VECTORIZE_SKILL_NAME, "SKILL.md");
|
|
11496
11501
|
}
|
|
11502
|
+
/** Where the bundled `knowledge-graph` skill lives. */
|
|
11503
|
+
function knowledgeGraphSkillPath() {
|
|
11504
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
11505
|
+
return join(moduleDirectory, "..", "skills", KNOWLEDGE_GRAPH_SKILL_NAME, "SKILL.md");
|
|
11506
|
+
}
|
|
11497
11507
|
/**
|
|
11498
11508
|
* Where this package's tutorial document lives, from either build layout.
|
|
11499
11509
|
*
|
|
@@ -11924,12 +11934,26 @@ function apply(ctx, deploymentConfig) {
|
|
|
11924
11934
|
...caller.departmentPathCodes === void 0 ? {} : { departmentPathCodes: caller.departmentPathCodes },
|
|
11925
11935
|
...caller.departmentPathNames === void 0 ? {} : { departmentPathNames: caller.departmentPathNames }
|
|
11926
11936
|
};
|
|
11927
|
-
|
|
11937
|
+
const graphTree = await scanKnowledgeGraphs({
|
|
11928
11938
|
actualRoot: plan.actualRoot,
|
|
11929
11939
|
roots,
|
|
11930
11940
|
caller: graphCaller,
|
|
11931
11941
|
canRead: (relPath) => canReadPath(host, config, relPath)
|
|
11932
|
-
})
|
|
11942
|
+
});
|
|
11943
|
+
return writeJson(res, 200, {
|
|
11944
|
+
...graphTree,
|
|
11945
|
+
scopes: graphTree.scopes.map((scope) => ({
|
|
11946
|
+
...scope,
|
|
11947
|
+
groups: scope.groups.map((group) => ({
|
|
11948
|
+
...group,
|
|
11949
|
+
graphs: group.graphs.map((graph) => {
|
|
11950
|
+
const copy = { ...graph };
|
|
11951
|
+
delete copy.actualPath;
|
|
11952
|
+
return copy;
|
|
11953
|
+
})
|
|
11954
|
+
}))
|
|
11955
|
+
}))
|
|
11956
|
+
});
|
|
11933
11957
|
}
|
|
11934
11958
|
case "workflow.mine":
|
|
11935
11959
|
if (!isTeamDeployment(config)) return writeJson(res, 200, { workflows: (await listWorkflowsForAuthor(publishStorePath(publishDirOf(config.publishDir)), "local")).map((record) => ({
|
|
@@ -14639,6 +14663,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
14639
14663
|
});
|
|
14640
14664
|
}
|
|
14641
14665
|
};
|
|
14666
|
+
knowledgeToolHost.resolveCaller, knowledgeToolHost.canWrite, knowledgeToolHost.createEntry, knowledgeToolHost.overwriteFile, knowledgeToolHost.moveEntry, knowledgeToolHost.statEntry, knowledgeToolHost.refreshView, knowledgeToolHost.appendAudit, knowledgeToolHost.viewDir;
|
|
14642
14667
|
const disclosure = createToolDisclosure({
|
|
14643
14668
|
tools: [
|
|
14644
14669
|
workflowPublishTool,
|
|
@@ -14706,4 +14731,4 @@ async function executeStep(runner, step, hostId, record) {
|
|
|
14706
14731
|
}
|
|
14707
14732
|
}
|
|
14708
14733
|
//#endregion
|
|
14709
|
-
export { Config, KB_ROOT_RESOURCE_ID, KB_SUMMARIZE_SKILL_DESCRIPTION, KB_SUMMARIZE_SKILL_NAME, KB_SUMMARIZE_SKILL_RANK, KB_VECTORIZE_SKILL_DESCRIPTION, KB_VECTORIZE_SKILL_NAME, KB_VECTORIZE_SKILL_RANK, KNOWLEDGE_SKILL_DESCRIPTION, KNOWLEDGE_SKILL_INVOCATION, KNOWLEDGE_SKILL_NAME, KNOWLEDGE_SKILL_PROVIDER, KNOWLEDGE_SKILL_RANK, KnowledgeError, KnowledgeWriteError, LOCAL_USER_ID, MAX_KNOWLEDGE_WRITE_BYTES, TEMPLATE_READ_TIMEOUT_MS, actualRelPathForView, apply, canManageGrants, canReadPath, canReviewWorkflows, canWritePath, checkLocalPath, classifyMount, createGrant, createKnowledgeEntry, extractBeside, extractionOutcomeOf, fetchCaller, fetchGrants, fetchPrincipals, inject, isTeamDeployment, kbSummarizeSkillPath, kbVectorizeSkillPath, knowledgeAncestorsFor, knowledgeBaseSkillProvider, knowledgeBaseToolDisclosure, knowledgeSkillPath, listDirectory, listDirectoryShapes, listDirectoryWithPermissions, listRelativeFiles, localConfig, localManagementContext, localStatus, localStorageRoot, mineruUrlFor, name, permissionForKbRoute, planContext, readAuthorized, readDepartmentOf, readKnowledgeFile, readKnowledgeFileForDownload, readSharedMineruUrl, resolveMineruUrl, resolveWorkflowActor, revokeGrant, searchKnowledge, syncCallerView, tutorialTemplatePath, withLocalStorageRoot, withStoredStorage };
|
|
14734
|
+
export { Config, KB_ROOT_RESOURCE_ID, KB_SUMMARIZE_SKILL_DESCRIPTION, KB_SUMMARIZE_SKILL_NAME, KB_SUMMARIZE_SKILL_RANK, KB_VECTORIZE_SKILL_DESCRIPTION, KB_VECTORIZE_SKILL_NAME, KB_VECTORIZE_SKILL_RANK, KNOWLEDGE_GRAPH_SKILL_DESCRIPTION, KNOWLEDGE_GRAPH_SKILL_NAME, KNOWLEDGE_SKILL_DESCRIPTION, KNOWLEDGE_SKILL_INVOCATION, KNOWLEDGE_SKILL_NAME, KNOWLEDGE_SKILL_PROVIDER, KNOWLEDGE_SKILL_RANK, KnowledgeError, KnowledgeWriteError, LOCAL_USER_ID, MAX_KNOWLEDGE_WRITE_BYTES, TEMPLATE_READ_TIMEOUT_MS, actualRelPathForView, apply, canManageGrants, canReadPath, canReviewWorkflows, canWritePath, checkLocalPath, classifyMount, createGrant, createKnowledgeEntry, extractBeside, extractionOutcomeOf, fetchCaller, fetchGrants, fetchPrincipals, inject, isTeamDeployment, kbSummarizeSkillPath, kbVectorizeSkillPath, knowledgeAncestorsFor, knowledgeBaseSkillProvider, knowledgeBaseToolDisclosure, knowledgeGraphSkillPath, knowledgeSkillPath, listDirectory, listDirectoryShapes, listDirectoryWithPermissions, listRelativeFiles, localConfig, localManagementContext, localStatus, localStorageRoot, mineruUrlFor, name, permissionForKbRoute, planContext, readAuthorized, readDepartmentOf, readKnowledgeFile, readKnowledgeFileForDownload, readSharedMineruUrl, resolveMineruUrl, resolveWorkflowActor, revokeGrant, searchKnowledge, syncCallerView, tutorialTemplatePath, withLocalStorageRoot, withStoredStorage };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-page document preview for the knowledge page.
|
|
3
|
+
*
|
|
4
|
+
* The knowledge page is a global main panel, and the host's right sidebar only
|
|
5
|
+
* renders a session-bound preview while a conversation is the active panel. When
|
|
6
|
+
* the host column is unavailable, this pane is the fallback the reader actually
|
|
7
|
+
* sees: it reads the document through the same permission-checked `read` route the
|
|
8
|
+
* preview pane uses and shows the text in place of the old "opened on the right"
|
|
9
|
+
* notice.
|
|
10
|
+
*
|
|
11
|
+
* It deliberately does not replace the host viewer: when `openInSidebar` succeeds
|
|
12
|
+
* the host tab still owns the document. This pane is the non-blank fallback for the
|
|
13
|
+
* profile where that column cannot mount.
|
|
14
|
+
*
|
|
15
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
16
|
+
*/
|
|
17
|
+
/** Props for the fallback preview. */
|
|
18
|
+
export interface KnowledgeFilePreviewProps {
|
|
19
|
+
/** View-relative document path. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Copy resolver. */
|
|
22
|
+
t: (key: string) => string;
|
|
23
|
+
/** Close the preview and return to the tree's previous surface. */
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Render one document read through the knowledge read route.
|
|
28
|
+
*
|
|
29
|
+
* @param props - the path, copy, and close command.
|
|
30
|
+
* @returns The preview pane.
|
|
31
|
+
*/
|
|
32
|
+
export declare function KnowledgeFilePreview(props: KnowledgeFilePreviewProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The graph tab's rail tree
|
|
2
|
+
* The graph tab's rail tree, rendered as the same file tree the Knowledge base
|
|
3
|
+
* tab shows.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The reader asked for the knowledge tree's four-level shape: three top-level
|
|
6
|
+
* folders (全局/部门/个人), the caller's department chain, the graph as a file row,
|
|
7
|
+
* then its topics. Row metrics and the folder/chevron affordances are borrowed from
|
|
8
|
+
* `KnowledgeListing.module.css` (`.entry`, `.entryDir`, `.entryGlyph`,
|
|
9
|
+
* `.entryName`, `.entryTime`) rather than restated here, so both tabs keep the
|
|
10
|
+
* explorer's 28px row, 12px type, 22px indent step and hover fill.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* several graphs scannable instead of turning it into a long list of every topic in
|
|
11
|
-
* every scope.
|
|
12
|
+
* The Host has already dropped every graph DataOps did not resolve; this component
|
|
13
|
+
* only filters names for the in-place search. It never decides who may see a graph.
|
|
12
14
|
*
|
|
13
15
|
* @module @sparkelf/dsh-knowledge-base/client/graph/KnowledgeGraphTree
|
|
14
16
|
*/
|
|
@@ -17,7 +19,7 @@ import type { KnowledgeGraphLoadState } from './useKnowledgeGraph.ts';
|
|
|
17
19
|
export interface KnowledgeGraphTreeProps {
|
|
18
20
|
/** Load state from {@link useKnowledgeGraph}. */
|
|
19
21
|
state: KnowledgeGraphLoadState;
|
|
20
|
-
/** Copy resolver for
|
|
22
|
+
/** Copy resolver for state lines and the group labels. */
|
|
21
23
|
t: (key: string) => string;
|
|
22
24
|
/** The graph whose topics are expanded, when one is selected. */
|
|
23
25
|
activeGraphId?: string;
|
|
@@ -244,6 +244,8 @@ export declare const en: {
|
|
|
244
244
|
readonly previewTruncated: "This file is large; only the start is shown.";
|
|
245
245
|
readonly modifiedAt: "Modified";
|
|
246
246
|
readonly openedInSidebar: "Opened in the file window on the right.";
|
|
247
|
+
readonly previewClose: "Close preview";
|
|
248
|
+
readonly previewLoading: "Loading document…";
|
|
247
249
|
readonly copyCode: "Copy";
|
|
248
250
|
readonly copiedCode: "Copied";
|
|
249
251
|
readonly retrieval: "Retrieval";
|
package/lib/types/graph.d.ts
CHANGED
|
@@ -74,6 +74,18 @@ interface FrontMatter {
|
|
|
74
74
|
export declare function parseGraphFrontMatter(text: string): FrontMatter;
|
|
75
75
|
/** Read a bracketed or comma-separated list value from front matter. */
|
|
76
76
|
export declare function parseGraphList(value: string | undefined): string[];
|
|
77
|
+
/** First H1 of a Markdown body, without the marker. */
|
|
78
|
+
export declare function firstHeading(body: string): string | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The first paragraph after the H1: the text the card shows as its summary.
|
|
81
|
+
*
|
|
82
|
+
* Starting at the H1 rather than at the top is deliberate: the title is already
|
|
83
|
+
* the card's heading, so repeating it as the first line of the summary would
|
|
84
|
+
* render the same sentence twice.
|
|
85
|
+
*/
|
|
86
|
+
export declare function firstSummary(body: string): string;
|
|
87
|
+
/** Remove the H1 and the summary paragraph, leaving the card's free body. */
|
|
88
|
+
export declare function bodyAfterSummary(body: string): string;
|
|
77
89
|
/**
|
|
78
90
|
* Read one topic folder into the canvas's topic shape.
|
|
79
91
|
*
|