From 74f0d8e78ad8a72b48b1b1ce11526f1a09525f60 Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 12 Sep 2026 22:35:53 +0800 Subject: [PATCH] feat: add topology view mode with router icons and link-style edges Canvas-global nodeViewMode (bubble/card/topology) can be set at create or via prefs; topology draws NE routers with straight blue center links and exposes router icon to agents. Co-authored-by: Cursor --- apps/server/src/chat.ts | 7 +- apps/server/src/create-server.ts | 12 +- apps/server/src/tools.ts | 53 +++-- apps/web/public/topo/ne-router.png | Bin 0 -> 7302 bytes apps/web/src/App.tsx | 23 +- apps/web/src/TopologyCanvas.tsx | 246 ++++++++++++++++----- apps/web/src/canvas-ui-text.ts | 6 + apps/web/src/node-geometry.ts | 16 +- apps/web/src/node-props/NodePropEditor.tsx | 1 + packages/dsh-plugin/lib/tools.js | 9 +- packages/graph-core/src/index.ts | 2 + packages/graph-store/src/index.ts | 8 +- packages/layout-engine/src/index.ts | 25 ++- packages/shared/src/ops.ts | 12 +- packages/shared/src/types.ts | 5 +- 15 files changed, 322 insertions(+), 103 deletions(-) create mode 100644 apps/web/public/topo/ne-router.png diff --git a/apps/server/src/chat.ts b/apps/server/src/chat.ts index 2a685a5..4c25f96 100644 --- a/apps/server/src/chat.ts +++ b/apps/server/src/chat.ts @@ -64,9 +64,10 @@ export function createChatHandler(ctx: ToolContext) { 8. 固定坐标用 set_pinned 或 create_node 带 pos;不确定时 mindmap_get_schema。 9. 节点属性:标题用 update_text;描述/多标签/多链接/图片URL/时间用 update_node_meta(patch);边用 update_edge_meta 或 set_edge_label。 10. 节点类型 stylePreset:default|card|title|decision|risk|note|muted(title 仅画布锚点,勿新建;card=新闻快照卡片,配图用 imageUrl、摘要用 note、来源用 links)。 -11. 同级排序用 reorder:优先 {"type":"reorder","nodeId":"...","order":10};也可用 afterSiblingId(null=置顶)。 -12. 折叠 set_collapsed 会隐藏该节点主父链下的全部子孙并重新布局;展开后恢复。 -13. 问图:用 mindmap_query(neighbors|path|stats|orphans|missing_meta);答完可用 mindmap_highlight 高亮路径;细节用 mindmap_get_node / mindmap_get_edge。禁止编造图中没有的事实。 +11. 节点图标 icon(create_node / update_node_meta.patch):server|database|cloud|person|folder|doc|link|warning|check|star|gear|globe|router。拓扑排障图:建画布时可用 mindmap_create_canvas({ title, nodeViewMode:"topology" }),节点 icon:"router";nodeViewMode 是画布全局偏好,也可事后 set_prefs 修改。 +12. 同级排序用 reorder:优先 {"type":"reorder","nodeId":"...","order":10};也可用 afterSiblingId(null=置顶)。 +13. 折叠 set_collapsed 会隐藏该节点主父链下的全部子孙并重新布局;展开后恢复。 +14. 问图:用 mindmap_query(neighbors|path|stats|orphans|missing_meta);答完可用 mindmap_highlight 高亮路径;细节用 mindmap_get_node / mindmap_get_edge。禁止编造图中没有的事实。 当前空间上下文(摘要): ${JSON.stringify(spatial, null, 0).slice(0, 6000)} diff --git a/apps/server/src/create-server.ts b/apps/server/src/create-server.ts index 1e2919f..78d9395 100644 --- a/apps/server/src/create-server.ts +++ b/apps/server/src/create-server.ts @@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url"; import type { Server } from "node:http"; import { WebSocketServer, type WebSocket } from "ws"; import { SqliteGraphStore } from "@mind-map/graph-store"; -import { coerceOpInput, OpSchema, type Op } from "@mind-map/shared"; +import { coerceOpInput, OpSchema, NodeViewModeSchema, type Op } from "@mind-map/shared"; import { listSkins } from "@mind-map/dream-skin"; import { GraphError } from "@mind-map/graph-core"; import { z } from "zod"; @@ -163,8 +163,16 @@ export function startMindMapServer( const body = (await c.req.json().catch(() => ({}))) as { title?: string; rootText?: string; + nodeViewMode?: string; + prefs?: { nodeViewMode?: string }; }; - const g = store.createCanvas(body.title, body.rootText); + const viewRaw = body.nodeViewMode ?? body.prefs?.nodeViewMode; + const parsed = NodeViewModeSchema.safeParse(viewRaw); + const g = store.createCanvas( + body.title, + body.rootText, + parsed.success ? { nodeViewMode: parsed.data } : undefined, + ); broadcast({ type: "canvas/created", canvasId: g.canvas.id }); return c.json(clientCanvasPayload(store, g.canvas.id)); }); diff --git a/apps/server/src/tools.ts b/apps/server/src/tools.ts index d3c5b21..a233a3e 100644 --- a/apps/server/src/tools.ts +++ b/apps/server/src/tools.ts @@ -17,6 +17,8 @@ import { nodeFunction, OP_SCHEMA_DOC, OP_TYPES, + NodeIconSchema, + NodeViewModeSchema, } from "@mind-map/shared"; import { hierarchyFingerprint, layout } from "@mind-map/layout-engine"; @@ -481,8 +483,10 @@ function estimateOverlaps( nodeViewMode: string, anchorId: string | null, ) { - const w = nodeViewMode === "card" ? 180 : 120; - const h = nodeViewMode === "card" ? 72 : 36; + const w = + nodeViewMode === "card" ? 180 : nodeViewMode === "topology" ? 88 : 120; + const h = + nodeViewMode === "card" ? 72 : nodeViewMode === "topology" ? 64 : 36; const ids = nodes .filter((n) => !n.deletedAt && n.id !== anchorId && positions[n.id]) .map((n) => n.id); @@ -770,11 +774,30 @@ export async function runTool( rawArgs.rootText != null && String(rawArgs.rootText).trim() ? String(rawArgs.rootText) : undefined; - const g = ctx.store.createCanvas(title, rootText); + const prefsArg = + rawArgs.prefs && typeof rawArgs.prefs === "object" + ? (rawArgs.prefs as Record) + : undefined; + const viewParsed = NodeViewModeSchema.safeParse( + rawArgs.nodeViewMode ?? prefsArg?.nodeViewMode, + ); + const g = ctx.store.createCanvas( + title, + rootText, + viewParsed.success ? { nodeViewMode: viewParsed.data } : undefined, + ); return { ok: true, - canvas: { id: g.canvas.id, title: g.canvas.title, rev: g.canvas.rev }, - hint: "Canvas is empty visually. Create top-level nodes with {type:'create_node', text:'...'} (omit parentId). Use parentId only when nesting under an existing visible node.", + canvas: { + id: g.canvas.id, + title: g.canvas.title, + rev: g.canvas.rev, + prefs: { nodeViewMode: g.canvas.prefs.nodeViewMode }, + }, + hint: + g.canvas.prefs.nodeViewMode === "topology" + ? "Topology canvas: create NE nodes with icon:'router'. Omit parentId for top-level. Prefer straight hierarchy/relation links." + : "Canvas is empty visually. Create top-level nodes with {type:'create_node', text:'...'} (omit parentId). Use parentId only when nesting under an existing visible node.", }; } @@ -1506,20 +1529,12 @@ export async function runTool( "edge_note", "all", ], - nodeIcons: [ - "server", - "database", - "cloud", - "person", - "folder", - "doc", - "link", - "warning", - "check", - "star", - "gear", - "globe", - ], + nodeIcons: NodeIconSchema.options, + nodeViewModes: NodeViewModeSchema.options, + createCanvas: { + nodeViewMode: + "Optional on mindmap_create_canvas (canvas-global). Also changeable later via set_prefs.", + }, edgeLineStyles: ["solid", "dashed", "dotted"], edgeDirections: ["forward", "both", "none"], getSubgraph: { diff --git a/apps/web/public/topo/ne-router.png b/apps/web/public/topo/ne-router.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5ed53441d8a8de21ad75da823e7f69effede35 GIT binary patch literal 7302 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 +