@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
|
@@ -13,4 +13,4 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module @sparkelf/dsh-knowledge-base/graph-canvas
|
|
15
15
|
*/
|
|
16
|
-
export declare 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>\u77E5\u8BC6\u56FE\u8C31 \u00B7 \u753B\u5E03</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=\"\u753B\u5E03\u63A7\u5236\">\n <button id=\"zin\" aria-label=\"\u653E\u5927\">+</button>\n <button id=\"zout\" aria-label=\"\u7F29\u5C0F\">\u2212</button>\n <button id=\"fit\" aria-label=\"\u9002\u5E94\u753B\u5E03\">\u2922</button>\n <button id=\"cross\" aria-label=\"\u663E\u793A\u4EA4\u53C9\u5173\u7CFB\" title=\"\u663E\u793A\u4EA4\u53C9\u5173\u7CFB\">\u21C4</button>\n <button id=\"relayout\" aria-label=\"\u91CD\u6392\" title=\"\u91CD\u6392\">\u21BA</button>\n </div>\n <div id=\"zoomLabel\">100%</div>\n <div id=\"kg-empty\" hidden><strong>\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u56FE\u8C31</strong><span>\u5728\u300C\u77E5\u8BC6\u56FE\u8C31\u300D\u6587\u4EF6\u5939\u91CC\u5EFA\u597D\u4E3B\u9898\u540E\uFF0C\u753B\u5E03\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\u3002</span></div>\n</main>\n\n<aside id=\"panel\" aria-label=\"\u4E3B\u9898\u8BE6\u60C5\" 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=\"\u5173\u95ED\">\u00D7</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' ? '\u8FDB\u884C\u4E2D' : s === 'paused' ? '\u6682\u505C' : '\u5DF2\u5B8C\u6210'; }\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 + '\uFF0C' + statusText(t.status) + '\uFF0C\u6DF1\u5EA6 ' + 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/ \u8FD8\u662F\u7A7A\u7684</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 + ' \u2194 ' + 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', '\u5173\u7CFB\uFF1A' + 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 || '\u672A\u767B\u8BB0') + '</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\">\u6587\u6863 \u00B7 ' + t.documents.length + ' \u7BC7</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) + ' \u00B7 ' + esc(d.date) + '</span></div></div>';\n });\n }\n html += '<div class=\"p-rel-title\">\u5173\u7CFB \u00B7 ' + rels.length + ' \u7BC7</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\">\u8FD8\u6CA1\u6709\u5173\u7CFB\u6587\u6863\u3002\u5B64\u7ACB\u7684\u7EBF\u4EF7\u503C\u5F88\u4F4E\u3002</div>';\n html += '<div class=\"p-docs\">\u6587\u6863\uFF1A' + ((t.documents || []).length) + ' \u4E2A\u6587\u4EF6</div>';\n showPanel((t.domain ? t.domain + ' \u00B7 ' : '') + statusText(t.status) + ' \u00B7 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('\u5173\u7CFB', l.body);\n html += '<div class=\"p-rel-title\">\u4E24\u7AEF</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('\u5173\u7CFB\u6587\u6863' + (l.updated ? ' \u00B7 ' + l.updated : ''),\n (a ? a.title : l.a) + ' \u2194 ' + (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";
|
|
16
|
+
export declare 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>\u77E5\u8BC6\u56FE\u8C31 \u00B7 \u753B\u5E03</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=\"\u753B\u5E03\u63A7\u5236\">\n <button id=\"zin\" aria-label=\"\u653E\u5927\">+</button>\n <button id=\"zout\" aria-label=\"\u7F29\u5C0F\">\u2212</button>\n <button id=\"fit\" aria-label=\"\u9002\u5E94\u753B\u5E03\">\u2922</button>\n <button id=\"cross\" aria-label=\"\u663E\u793A\u4EA4\u53C9\u5173\u7CFB\" title=\"\u663E\u793A\u4EA4\u53C9\u5173\u7CFB\">\u21C4</button>\n <button id=\"relayout\" aria-label=\"\u91CD\u6392\" title=\"\u91CD\u6392\">\u21BA</button>\n </div>\n <button id=\"zoomLabel\" type=\"button\" aria-label=\"\u91CD\u7F6E\u7F29\u653E\u4E3A 100%\" title=\"\u91CD\u7F6E\u7F29\u653E\u4E3A 100%\">100%</button>\n <div id=\"kg-empty\" hidden><strong>\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u56FE\u8C31</strong><span>\u5728\u300C\u77E5\u8BC6\u56FE\u8C31\u300D\u6587\u4EF6\u5939\u91CC\u5EFA\u597D\u4E3B\u9898\u540E\uFF0C\u753B\u5E03\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\u3002</span></div>\n</main>\n\n<aside id=\"panel\" aria-label=\"\u4E3B\u9898\u8BE6\u60C5\" 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=\"\u5173\u95ED\">\u00D7</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' ? '\u8FDB\u884C\u4E2D' : s === 'paused' ? '\u6682\u505C' : '\u5DF2\u5B8C\u6210'; }\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 + '\uFF0C' + statusText(t.status) + '\uFF0C\u6DF1\u5EA6 ' + 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/ \u8FD8\u662F\u7A7A\u7684</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 + ' \u2194 ' + 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', '\u5173\u7CFB\uFF1A' + 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 || '\u672A\u767B\u8BB0') + '</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\">\u6587\u6863 \u00B7 ' + t.documents.length + ' \u7BC7</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) + ' \u00B7 ' + esc(d.date) + '</span></div></div>';\n });\n }\n html += '<div class=\"p-rel-title\">\u5173\u7CFB \u00B7 ' + rels.length + ' \u7BC7</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\">\u8FD8\u6CA1\u6709\u5173\u7CFB\u6587\u6863\u3002\u5B64\u7ACB\u7684\u7EBF\u4EF7\u503C\u5F88\u4F4E\u3002</div>';\n html += '<div class=\"p-docs\">\u6587\u6863\uFF1A' + ((t.documents || []).length) + ' \u4E2A\u6587\u4EF6</div>';\n showPanel((t.domain ? t.domain + ' \u00B7 ' : '') + statusText(t.status) + ' \u00B7 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('\u5173\u7CFB', l.body);\n html += '<div class=\"p-rel-title\">\u4E24\u7AEF</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('\u5173\u7CFB\u6587\u6863' + (l.updated ? ' \u00B7 ' + l.updated : ''),\n (a ? a.title : l.a) + ' \u2194 ' + (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";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing knowledge-graph tools (`kg_add` / `kg_get` / `kg_set` / `kg_del`).
|
|
3
|
+
*
|
|
4
|
+
* The four verbs mirror `kg.py`'s CLI. They operate on the P1 storage shape
|
|
5
|
+
* (`知识图谱/<graph>/<topic>/index.md`, relations under `.kg/links/`) through the
|
|
6
|
+
* same Host seam the HTTP routes use: one grant per graph, `canWritePath` before
|
|
7
|
+
* every write, `readAuthorized` for every read, audit entries recorded beside the
|
|
8
|
+
* caller's view, and `_trash` instead of deletion.
|
|
9
|
+
*
|
|
10
|
+
* Writes are two-step by design. A call without `confirm: true` returns the exact
|
|
11
|
+
* draft it would write and changes nothing; the model shows that draft to the user
|
|
12
|
+
* and calls again with `confirm: true` after approval. This is the §7 trigger rule
|
|
13
|
+
* (user sovereignty) expressed in the tool protocol rather than in a hook.
|
|
14
|
+
*
|
|
15
|
+
* @module @sparkelf/dsh-knowledge-base/graph-tools
|
|
16
|
+
*/
|
|
17
|
+
import { type KnowledgeFileToolHost } from './knowledgeTools.ts';
|
|
18
|
+
import type { KnowledgeGraphNode } from './graphTypes.ts';
|
|
19
|
+
/** The Host operations graph tools compose. Actual paths are authoritative. */
|
|
20
|
+
export interface KnowledgeGraphToolHost extends Pick<KnowledgeFileToolHost, 'resolveCaller' | 'canWrite' | 'createEntry' | 'overwriteFile' | 'moveEntry' | 'statEntry' | 'refreshView' | 'appendAudit' | 'viewDir'> {
|
|
21
|
+
/** Every graph the caller may read, with Host-only `actualPath`. */
|
|
22
|
+
listGraphs(): Promise<KnowledgeGraphNode[]>;
|
|
23
|
+
/** Read one view-relative document through the permission-checked route. */
|
|
24
|
+
readText(viewPath: string): Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the four graph tools.
|
|
28
|
+
*
|
|
29
|
+
* @param host - the Host seam, shared with the knowledge file tools.
|
|
30
|
+
* @returns Tool definitions for the disclosure coordinator.
|
|
31
|
+
*/
|
|
32
|
+
export declare const createKnowledgeGraphTools: (host: KnowledgeGraphToolHost) => import("@deepseek-ai/dsh-tools").ToolDefinition[];
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
export declare const KNOWLEDGE_GRAPH_FOLDER = "\u77E5\u8BC6\u56FE\u8C31";
|
|
20
20
|
/** The hidden folder inside a graph that holds the relation documents. */
|
|
21
21
|
export declare const KNOWLEDGE_GRAPH_META_FOLDER = ".kg";
|
|
22
|
+
/** The trash folder `kg_del` moves topics into; never a topic itself. */
|
|
23
|
+
export declare const KNOWLEDGE_GRAPH_TRASH_FOLDER = "_trash";
|
|
22
24
|
/** One document inside a topic folder, as the detail panel lists it. */
|
|
23
25
|
export interface KnowledgeGraphDocument {
|
|
24
26
|
/** Human title: the document's first H1, else the file name without extension. */
|
|
@@ -69,6 +71,8 @@ export interface KnowledgeGraphLink {
|
|
|
69
71
|
updated?: string;
|
|
70
72
|
/** First body paragraph, shown when the relation pill is opened. */
|
|
71
73
|
body: string;
|
|
74
|
+
/** File name under `<graph>/.kg/links/`, used by the Host-side del tool. */
|
|
75
|
+
file?: string;
|
|
72
76
|
}
|
|
73
77
|
/** One graph node under a scope. */
|
|
74
78
|
export interface KnowledgeGraphNode {
|
|
@@ -92,6 +96,11 @@ export interface KnowledgeGraphNode {
|
|
|
92
96
|
links: number;
|
|
93
97
|
documents: number;
|
|
94
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* Authoritative path relative to the knowledge root, used by Host-side graph
|
|
101
|
+
* tools. Stripped before the tree is sent to the browser.
|
|
102
|
+
*/
|
|
103
|
+
actualPath?: string;
|
|
95
104
|
}
|
|
96
105
|
/** One group inside a scope: the department chain for 部门, empty elsewhere. */
|
|
97
106
|
export interface KnowledgeGraphGroup {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1044,6 +1044,8 @@ export declare const KNOWLEDGE_SKILL_NAME = "knowledge-base";
|
|
|
1044
1044
|
export declare const KB_SUMMARIZE_SKILL_NAME = "kb-summarize";
|
|
1045
1045
|
/** Third bundled skill: explicit document/directory vectorization. */
|
|
1046
1046
|
export declare const KB_VECTORIZE_SKILL_NAME = "kb-vectorize";
|
|
1047
|
+
export declare const KNOWLEDGE_GRAPH_SKILL_NAME = "knowledge-graph";
|
|
1048
|
+
export declare const KNOWLEDGE_GRAPH_SKILL_DESCRIPTION = "\u8BFB\u5199\u56E2\u961F\u77E5\u8BC6\u5E93\u91CC\u7684\u77E5\u8BC6\u56FE\u8C31\uFF1A\u4E3B\u9898\u5361\u3001\u5173\u7CFB\u6587\u6863\u4E0E\u56DB\u4E2A\u52A8\u4F5C\u3002";
|
|
1047
1049
|
/**
|
|
1048
1050
|
* Short description shown in the skill list.
|
|
1049
1051
|
*
|
|
@@ -1084,6 +1086,8 @@ export declare function knowledgeSkillPath(): string;
|
|
|
1084
1086
|
export declare function kbSummarizeSkillPath(): string;
|
|
1085
1087
|
/** Where the bundled `kb-vectorize` skill lives. */
|
|
1086
1088
|
export declare function kbVectorizeSkillPath(): string;
|
|
1089
|
+
/** Where the bundled `knowledge-graph` skill lives. */
|
|
1090
|
+
export declare function knowledgeGraphSkillPath(): string;
|
|
1087
1091
|
/**
|
|
1088
1092
|
* Where this package's tutorial document lives, from either build layout.
|
|
1089
1093
|
*
|
package/lib/types/views.d.ts
CHANGED
|
@@ -838,6 +838,16 @@ export interface KnowledgeAuditEntry {
|
|
|
838
838
|
relPath: string;
|
|
839
839
|
/** For a move, the destination path. */
|
|
840
840
|
toRelPath?: string;
|
|
841
|
+
/** Model-facing tool that performed the action, when one did. */
|
|
842
|
+
tool?: string;
|
|
843
|
+
/** Graph the action belongs to, when the action is a graph operation. */
|
|
844
|
+
graphId?: string;
|
|
845
|
+
/** Topic id inside the graph, when the action names one. */
|
|
846
|
+
topicId?: string;
|
|
847
|
+
/** Short human intent (`new topic`, `move-to-trash`, ...). */
|
|
848
|
+
intent?: string;
|
|
849
|
+
/** One-line description of what changed, for review. */
|
|
850
|
+
diffSummary?: string;
|
|
841
851
|
/**
|
|
842
852
|
* Extra detail for actions that have more than one flavour.
|
|
843
853
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-knowledge-base",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "DataOps-backed knowledge base for DeepSeek Harness: a left-sidebar destination, Settings configuration, and index tooling over a mounted NAS view.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: knowledge-graph
|
|
3
|
+
description: 读写团队知识库里的知识图谱:主题卡、关系文档、四个动作和五个剧本。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# knowledge-graph
|
|
7
|
+
|
|
8
|
+
知识图谱是知识库的导航层,不是第二套文档库。它只有三样东西:**图谱**(一个可见文件夹)、**主题**(图谱下的可见子文件夹 + `index.md` 卡)、**关系**(`index.md` 之间的一篇文档)。
|
|
9
|
+
|
|
10
|
+
## 1. 目录长什么样
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
<scope>/知识图谱/<graph>/
|
|
14
|
+
<主题>/
|
|
15
|
+
index.md # 主题卡:front-matter + 摘要 + 自由正文
|
|
16
|
+
<document>.md # 文档,知识库能搜、能预览、能分享
|
|
17
|
+
.kg/
|
|
18
|
+
links/<a>--<b>.md # 关系文档,点开头,KB 文件树和 FTS 跳过
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `<scope>` 是 `global` / `department/<部门链>` / `personal/<uid>`;个人路径由 Host 解析,**不要让模型拼 uid**。
|
|
22
|
+
- 主题文件夹名可读、可改;关系引用 `index.md` 里的 `id`,不引用文件夹名,所以改名不断边。
|
|
23
|
+
- 一个图谱一条 grant,覆盖主题文档与 `.kg`;写操作走 `kg_add` / `kg_set` / `kg_del`,不要用 shell 直接写文件。
|
|
24
|
+
|
|
25
|
+
## 2. 主题卡与关系文档
|
|
26
|
+
|
|
27
|
+
主题卡好例子:
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
---
|
|
31
|
+
id: task-bus
|
|
32
|
+
title: 任务总线
|
|
33
|
+
status: active
|
|
34
|
+
depth: 4
|
|
35
|
+
domain: dsh
|
|
36
|
+
tags: [任务, 状态机]
|
|
37
|
+
updated: 2026-10-31
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# 任务总线
|
|
41
|
+
|
|
42
|
+
DSH 的跨来源任务底座,任务卡是唯一状态载体。
|
|
43
|
+
|
|
44
|
+
## 现在
|
|
45
|
+
v0.1 设计稿评审完,正在拆实现任务。
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
坏例子:没有 `id`(关系会指向文件夹名,改名断边);把全部正文塞进摘要(卡片会变成一堵墙);`status` 写成自由文本(画布只能识别 active/paused/done)。
|
|
49
|
+
|
|
50
|
+
关系文档好例子:
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
---
|
|
54
|
+
a: task-bus
|
|
55
|
+
b: group-chat
|
|
56
|
+
label: 任务来源
|
|
57
|
+
updated: 2026-10-31
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# 任务来源
|
|
61
|
+
|
|
62
|
+
群聊把讨论转成任务;任务总线承接状态,不反向写群聊。
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
坏例子:`a`/`b` 指向不存在的 id(画布会丢弃这条边);一篇文档写三段互不相关的关系(关系文档只表达两端之间的一件事)。
|
|
66
|
+
|
|
67
|
+
## 3. 四个动作
|
|
68
|
+
|
|
69
|
+
工具名与 `kg.py` 对齐:`kg_add` / `kg_get` / `kg_set` / `kg_del`。所有写动作**先返回草稿**,把草稿给用户看,用户确认后再带 `confirm: true` 调用。
|
|
70
|
+
|
|
71
|
+
| 动作 | 做什么 |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `kg_add` | `kind: topic` 新增一条线(文件夹 + 卡);`kind: link` 新增一条关系 |
|
|
74
|
+
| `kg_get` | 不带 `graph`/`id` 返回全部图谱总览;带 `graph` 返回该图谱;带 `graph`+`id` 返回卡、文档、关系 |
|
|
75
|
+
| `kg_set` | 改一个主题的 `status` / `depth` / `title` / `domain` / 摘要 / 正文 |
|
|
76
|
+
| `kg_del` | 把主题移到 `<graph>/_trash/`;有关联关系时需 `force: true` 一并移走,**不真删** |
|
|
77
|
+
|
|
78
|
+
### 五个剧本
|
|
79
|
+
|
|
80
|
+
1. **新建一条线**:`kg_get` 看当前图谱 → `kg_add {kind:"topic"}` 草稿 → 用户确认 → `confirm:true`。
|
|
81
|
+
2. **接续**:`kg_get {graph,id}` 读卡与关系 → 更新正文/摘要 → `kg_set` 草稿 → 确认。
|
|
82
|
+
3. **寻址**:`kg_get` 不带 id 看总览 → 按名称找 graph → `kg_get {graph,id}` 读一条线;不要用 `find`/`map` 另造工具。
|
|
83
|
+
4. **沉淀**:把一次会话的结论写成文档(`kb_file_write` 到主题文件夹),再用 `kg_set` 更新卡摘要;图谱不复制文档内容。
|
|
84
|
+
5. **复盘**:`kg_get` 总览 → 对每条线读 `kg_get {graph,id}` → 把 `status`/`depth` 调整回真实状态;`kg_del` 只移 `_trash`,不删。
|
|
85
|
+
|
|
86
|
+
## 4. 扫历史会话(shell,不配工具)
|
|
87
|
+
|
|
88
|
+
需要从旧会话里找线索时,直接读会话日志,不要新建 `scan` 工具:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
zstd -dc ~/.dsh/sessions/<session>.jsonl.zst \
|
|
92
|
+
| rg -i '任务总线|group-chat|kg' \
|
|
93
|
+
| head -80
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
把命中的原文片段带回当前对话,再由 agent 写成主题卡或文档;工具只负责图谱的四个动作,不负责扫描历史。
|