diff --git a/internal/plugins/webui/dashboard.js b/internal/plugins/webui/dashboard.js index 6823691..2597ca3 100644 --- a/internal/plugins/webui/dashboard.js +++ b/internal/plugins/webui/dashboard.js @@ -2936,6 +2936,9 @@ function initChatStarmap() { // 活动数据源:/runtime 3s + /memory/graph/pulse 10s。 // 只在星图真正初始化后启动,避免在隐藏页签空跑。 starmapStartActivity(); + // 图例按实际出现的类型生成(建图后 smPresentTypes 才是准的)。 + smLegend(); + smUpdateStat(); } // starmapStartActivity 启动两路活动轮询(幂等)。 @@ -2954,6 +2957,13 @@ function starmapStartActivity() { } function buildChatStarmapGraph() { + // 先统计实际出现的类型(smColorFor / 动态图例都要用)。 + // 必须在建节点**之前**算完 —— 颜色在创建 mesh 时就定下了。 + smPresentTypes = {}; + starmapNodes.forEach((n) => { + var t = String(n.type || "").toLowerCase() || "(未知)"; + smPresentTypes[t] = (smPresentTypes[t] || 0) + 1; + }); starmapNodeMeshes.forEach((m) => { starmapScene.remove(m); }); @@ -3113,7 +3123,7 @@ function buildChatStarmapGraph() { // 服务端 type 是首字母大写("Concept" / "Person" …), // 原 smTypeColors 的键全是小写,永远匹配不上 ⇒ 全图单色 0xcccccc。 // 这里统一小写归一化,并补上服务端实际会产出的类型。 - var col = smTypeColors[String(n.type || "").toLowerCase()] || 0xcccccc; + var col = smColorFor(n.type); var ei = 0.3 + mnr * 0.7; var mat = new THREE.MeshPhongMaterial({ color: col, @@ -5468,21 +5478,60 @@ var starmapLabelEl = null; // smTypeColors 的键必须与**服务端实际产出的 type 字符串小写后**一致。 // 服务端默认类型是 "Concept"(首字母大写,见 internal/memory/graph.go), // 原键全为小写 ⇒ 永远匹配不上 ⇒ 1150 个节点全渲染成同一个灰色 0xcccccc, -// 分类配色实际上从未生效过。 +// 分类配色实际上从未生效过。现已加小写归一化,映射是真的生效了。 +// +// ★ 但色相换成**中性灰蓝**(用户裁定):亮青绿(0x44ff88)配 1151 个 +// 自发光球确实扎眼,而且它掩盖了一个更刺眼的事实 —— +// +// 实测图谱里 Concept 占 1148/1149 = 99%(nlp/extractor.go 完全不判类型, +// graph.go:451 在 type 为空时写死 "Concept")。也就是说这套按类型 +// 配色的设计,对真实数据而言**只有一种颜色会被用到**。 +// +// 所以真正的做法不是挑一个好看的色,而是: +// 1. 默认色改为低饱和灰蓝(全图统一,不假装在分类) +// 2. 类型色只在**真的存在多种类型**时才按类型区分(见 smColorFor) +// 3. 图例按实际节点集合动态生成,不列出永不出现的类型 +var SM_COLOR_DIM = 0x7d8a9e; // 中性灰蓝:单一类型时的全图色 var smTypeColors = { - person: 0x4488ff, - task: 0xff8844, - ai: 0xaa44ff, - concept: 0x44ff88, - object: 0xff4444, - // 服务端还会产出这些(indexer_test 里可见 "Person"/"Location") - location: 0xffaa44, - source: 0x8899ff, - document: 0xaabbcc, - event: 0xff88cc, - entity: 0x44ddcc, - scene: 0x88ff44, + // 以下类型在真实数据里几乎不出现(仅 social.go 会产出 person), + // 但保留定义:万一出现就能自动获得区分色 + 动态图例条目。 + person: 0x6f9fd8, + task: 0xd89a6a, + ai: 0xa583d8, + // 绝大多数节点(Concept):用中性灰蓝,不在“分类色”里扮浓。 + concept: SM_COLOR_DIM, + object: 0xd87a7a, + location: 0xd8b06a, + source: 0x8b9dc4, + document: 0x9aa5b1, + event: 0xc98fb5, + entity: 0x7fb5ad, + scene: 0x9dc47f, }; + +// smColorTypeSet 统计本次图谱里实际出现的类型(小写)。 +// 由 buildChatStarmapGraph 在建图前填好。 +var smPresentTypes = {}; + +// smColorFor 取节点颜色。 +// +// 规则:图谱里存在 2 种以上「有存在感」的类型时按类型上色,否则全图 +// 用 SM_COLOR_DIM。阈值与图例一致(>=1% 算“有存在感”)。 +// 理由:99.9% 概念 + 0.1% 其他时,按类型上色得到的仍是一整片同色, +// 只是换了个色相;而那一两个异色点在视觉上就是噪点。 +function smColorFor(type) { + var t = String(type || "").toLowerCase(); + var keys = Object.keys(smPresentTypes); + if (keys.length <= 1) return SM_COLOR_DIM; + var total = 0; + for (var i = 0; i < starmapNodes.length; i++) total++; + var self = (smPresentTypes[t] || 0) * 100; + if (total > 0 && self / total < 1) { + // 稀疏类型也走中性色:宁可全图同色,不引入单点亮色噪点。 + return SM_COLOR_DIM; + } + return smTypeColors[t] || SM_COLOR_DIM; +} var smEdgeColors = { 喜欢: 0xff6b6b, 学习: 0x4ecdc4, @@ -5866,13 +5915,7 @@ function renderStarmapTab() { __("记忆星图", "Memory Star Map") + ' ' + '
' + - '