]> acesimba.cloud Git - StationBuilder.git/commitdiff
修复生成看板导入Cube后空白:DASHBOARD_SPEC强制照模板custom结构 + fixDashboard归一化兜底
authorwang.yu <wang.yu@advantech.com>
Tue, 18 Aug 2026 04:19:40 +0000 (12:19 +0800)
committerwang.yu <wang.yu@advantech.com>
Tue, 18 Aug 2026 04:19:40 +0000 (12:19 +0800)
db-sidecar.mjs
public/design-doc.html

index e186bff292511c3118ff73e0441dd46f03c625e6..510df16d050897b60b80b79bd4644a341f3ec3e4 100644 (file)
@@ -5,7 +5,7 @@ import http from "node:http";
 import { spawn } from "node:child_process";
 import { DatabaseSync } from "node:sqlite";
 import { writeFile, readFile, rm, readdir } from "node:fs/promises";
-import { existsSync, mkdirSync } from "node:fs";
+import { existsSync, mkdirSync, readFileSync } from "node:fs";
 import { dirname, join, basename, extname } from "node:path";
 import { fileURLToPath } from "node:url";
 import { randomUUID } from "node:crypto";
@@ -96,10 +96,30 @@ KPIIndicator panel specifics:
 - data.targets[0].dataType = "sum"
 - custom can include icon, thresholds, compare.
 
-Charts: include proper chartConfig, series, axes, colors. If a 视觉风格要求 section provides a 推荐配色板, use THAT palette first; otherwise use default #0062CC,#00B900,#FF9300,#EBC147,#D73C74,#8048C8,#6D84C9,#34A0D5.
+CRITICAL — custom structure (the #1 cause of blank panels after import):
+- For EVERY panel, the "custom" object MUST mirror the structure of the matching template file under
+  /root/.codebuddy/skills/dashboard-json-generator/assets/templates/. BEFORE writing a panel of type X,
+  READ the corresponding template file and copy its "custom" object's keys/structure EXACTLY (only replace
+  values, keep every key). Do NOT invent new custom keys or rename keys.
+- Type → template file map:
+  KPIIndicator→kpi.json, LineChart→line-chart.json, Bar→bar-chart.json, PieChart→pie-chart.json,
+  Table→table.json, StatusLight→status-light.json, ParameterCard→attribute-card.json, GeoMap→geomap.json,
+  Gauge→gauge.json, ProgressBar→progress-bar.json, RadarChart→radar.json, HeatMap→heatmap.json,
+  Liquidfill→liquid.json, Image→image.json, Container→container.json, Title→title.json.
+  (ProgressRing: follow the ProgressRing section in the Skill's SKILL.md; RichText: use custom.data.text.)
+- Flat vs wrapper rule (must match the template exactly):
+  · Bar / LineChart / HeatMap: FLAT custom = { config, series, xAxis, yAxis, legend, grid, dataZoom } — NO chartConfig wrapper.
+  · PieChart / RadarChart / ProgressRing / GeoMap: custom = { dataConfig, chartConfig } (GeoMap: dataConfig + embedded geoJson, see below).
+- StatusLight: custom = { columns, machineTextSize, machineBold, statusTextSize, statusTextBold, icon, iconSize, configs:[{label,color,value}] }.
+- ParameterCard: custom = { tableCnt, rowCnt, style:{color,fontSize,fontWeight}, styleLeft:{fontSize,fontWeight,color} }.
+- Table: custom = { columns:[{id,unit,type,data:{...},style:{...}}], page:{show,pageSize}, stripe, border, title, rows } — columns[].id MUST equal data.targets[].id.
+- GeoMap: custom.dataConfig MUST include geoFileName:"中国-area.json", theme:"light", zoomTo:0.8, geoType:"effectScatter", longitudeId:"A", latitudeId:"B", coordValId:"C", coordNameId:"D", and data.calc=true with 4 targets of dataType "first". The China geoJson FeatureCollection is auto-injected by the server — DO NOT include geoJson yourself; just set the dataConfig fields above.
+If a 视觉风格要求 section provides a 推荐配色板, use THAT palette first; otherwise use default #0062CC,#00B900,#FF9300,#EBC147,#D73C74,#8048C8,#6D84C9,#34A0D5.
 
 Theme: If a 视觉风格要求 section is provided below, follow IT exactly (its grid colors/borders/gradients take precedence) and ignore the default example colors in the grid above. Otherwise use the default light theme: grid.theme="light", bgColor rgb(237,246,251), panelBgColor #F5F5F5, panel.bgColor #FFFFFF, titleColor #323232, borderColor #C8C8C8. Do not mix darkBlue with light background.
 
+Reminder: the "data" section (data.config.data.columns/rows, data.targets) and the "custom" section are INDEPENDENT. You already populate "data" correctly — now ALSO make "custom" exactly match the template file for that panel type. A panel whose "custom" does not match its template type will render BLANK after import.
+
 Output ONLY the raw JSON object. No markdown code fences, no explanation.
 `;
 
@@ -217,6 +237,16 @@ function ensureColumn(table, column, def) {
 ensureColumn("cubes", "prompt", "TEXT");
 ensureColumn("sites", "style_id", "INTEGER");
 
+// GeoMap 中国地图底图:若 data/db/china-geo.json 存在(中国 34 省 + 九段线 FeatureCollection),
+// 则在 fixDashboard 中自动注入 GeoMap 面板的 custom.dataConfig.geoJson,避免地图气泡不渲染。
+let CHINA_GEOJSON = null;
+try {
+  const gjPath = join(DB_DIR, "china-geo.json");
+  if (existsSync(gjPath)) CHINA_GEOJSON = JSON.parse(readFileSync(gjPath, "utf-8"));
+} catch (e) {
+  CHINA_GEOJSON = null;
+}
+
 
 function respondJson(res, status, obj) {
   const body = JSON.stringify(obj);
@@ -325,6 +355,94 @@ function validateDashboard(json) {
 // ① 代码级自动修复:对校验失败的结构做程序化修正,避免为小问题重新调用模型。
 // 返回修正后的 json。无法可靠修复的项(如 panel 类型非法、grid 主题色缺失、缺少 Title 面板)
 // 交由上游 repairAndValidate 重新校验后决定是否需再次调用模型。
+// ② 面板 custom 结构归一化:模型极易把图表配置套一层 chartConfig 外壳,或自造 StatusLight /
+// ParameterCard / Table / GeoMap 的 custom 结构,导致导入 Cube 后整片空白。这里做代码级修正,
+// 把常见错误形状收敛回 Cube 工具期望的 schema(与 skill 模板一致)。
+function normalizePanelCustom(p) {
+  if (!p.custom || typeof p.custom !== "object") p.custom = {};
+  const cc = p.custom;
+  const type = p.type;
+
+  // Bar / LineChart / HeatMap:模型输出 custom.chartConfig:{xAxis,yAxis,series,...},
+  // 但模板要求扁平 custom:{config,series,xAxis,yAxis,legend,grid,dataZoom}。拍平之。
+  if (["Bar", "LineChart", "HeatMap"].includes(type)) {
+    if (cc.chartConfig && typeof cc.chartConfig === "object") {
+      for (const k of ["xAxis", "yAxis", "series", "legend", "grid", "dataZoom", "tooltip", "config", "alignTicks", "color", "multiY", "dimension"]) {
+        if (k in cc.chartConfig && !(k in cc)) cc[k] = cc.chartConfig[k];
+      }
+      delete cc.chartConfig;
+    }
+  }
+
+  // StatusLight:{statusMap:[{value,color}]} → {configs:[{label,color,value}]}
+  if (type === "StatusLight") {
+    if (Array.isArray(cc.statusMap) && !Array.isArray(cc.configs)) {
+      cc.configs = cc.statusMap.map(s => ({
+        label: String(s.value ?? s.label ?? ""),
+        color: s.color || "#00CC66",
+        value: String(s.value ?? s.label ?? ""),
+      }));
+      delete cc.statusMap;
+    }
+    if (typeof cc.columns !== "number") cc.columns = 3;
+    if (typeof cc.machineTextSize !== "number") cc.machineTextSize = 16;
+    if (!cc.icon) cc.icon = "status-icon-1";
+  }
+
+  // ParameterCard:{title,items,...} → {tableCnt,rowCnt,style,styleLeft}
+  if (type === "ParameterCard") {
+    if (typeof cc.tableCnt !== "number") cc.tableCnt = 1;
+    if (typeof cc.rowCnt !== "number") cc.rowCnt = (p.data?.config?.data?.rows?.length) || 10;
+    if (!cc.style) cc.style = { color: "#323233", fontSize: 16, fontWeight: 700 };
+    if (!cc.styleLeft) cc.styleLeft = { fontSize: 16, fontWeight: 400, color: "#89898a" };
+  }
+
+  // Table:模型常输出 custom.columns:[{key,...}] 而非模板的 columns:[{id,...}]。
+  // 补成可渲染结构,并让 columns[].id 对应 data.targets[].id。
+  if (type === "Table") {
+    if (Array.isArray(cc.columns) && !Array.isArray(cc.page)) {
+      const targets = Array.isArray(p.data?.targets) ? p.data.targets : [];
+      cc.columns = cc.columns.map((col, i) => {
+        const id = targets[i]?.id || String.fromCharCode(65 + i);
+        const key = (col && col.key) ? col.key : (typeof col === "string" ? col : id);
+        return {
+          id, key, unit: "", type: "string",
+          data: { decimals: 0, thresholds: { enable: false, ruels: [] }, format: "", mapping: { enable: false, type: "value", rules: [] }, link: { enable: false, url: "" } },
+          style: { sort: { enable: false }, filter: { enable: false } },
+        };
+      });
+      cc.page = { show: true, pageSize: 10 };
+      cc.stripe = cc.stripe || { enable: false, color1: "#fff", color2: "#fafafa" };
+      cc.border = cc.border || { enable: true, show: true, showCol: false, borderWidth: 1, borderColor: "rgba(200,200,200,0.12)" };
+      cc.title = cc.title || { enable: true, fontSize: 16, fontWeight: "bold", textAlign: "left", color: "rgb(50,50,50)", backgroundColor: "transparent", height: 43, paddingX: 16, paddingY: 8 };
+      cc.rows = cc.rows || { enable: true, fontSize: 14, fontWeight: "normal", textAlign: "left", color: "rgb(50,50,50)", backgroundColor: "rgba(0,0,0,0)", height: 43, paddingX: 16, paddingY: 8 };
+    }
+  }
+
+  // GeoMap:确保 dataConfig 绑定正确,并在有底图时注入 geoJson。
+  if (type === "GeoMap") {
+    if (!cc.dataConfig || typeof cc.dataConfig !== "object") cc.dataConfig = {};
+    const dc = cc.dataConfig;
+    if (!dc.geoFileName) dc.geoFileName = "中国-area.json";
+    if (!dc.theme) dc.theme = "light";
+    if (typeof dc.zoomTo !== "number") dc.zoomTo = 0.8;
+    if (!dc.geoType) dc.geoType = "effectScatter";
+    if (!dc.longitudeId) dc.longitudeId = "A";
+    if (!dc.latitudeId) dc.latitudeId = "B";
+    if (!dc.coordValId) dc.coordValId = "C";
+    if (!dc.coordNameId) dc.coordNameId = "D";
+    if (!dc.geoJson && CHINA_GEOJSON) dc.geoJson = CHINA_GEOJSON;
+    if (p.data && Array.isArray(p.data.targets)) {
+      p.data.calc = true;
+      p.data.targets.forEach(t => { if (t && !t.dataType) t.dataType = "first"; });
+    }
+    if (!cc.panelType) cc.panelType = "carousel";
+    if (!Array.isArray(cc.panelSelectedIds)) cc.panelSelectedIds = [];
+  }
+
+  return p;
+}
+
 function fixDashboard(json) {
   if (!json || typeof json !== "object") return json;
   if (!Array.isArray(json.panels) || json.panels.length === 0) return json;
@@ -386,6 +504,8 @@ function fixDashboard(json) {
       p.data = p.data || {};
       p.data.calc = false;
     }
+    // custom 结构归一化(图表/组件空白的主因)
+    normalizePanelCustom(p);
   }
   return json;
 }
index f564c5a602e83bc29f93e4d97f031f581c5f1be3..4e9deb03d70c55b42c50d67997c4259337f27bcc 100644 (file)
     <li>2026-08-18 扩展「AI 智能补全」至 Asset 资产设置页:右侧新增与基本属性同款的「填写建议 + AI 智能补全」栏;点击「开始补全」后基于站点名称 / 领域 / 描述 / 补充要求分析并补充设备类型、数量预设与命名规则,追加到用户已填项下方(不改动已有项),资产层级预览随之重算(后端 <code>POST /api/db/sites/:id/complete-assets</code> 流式、<code>GET .../complete-assets-prompt</code> 取提示词)。</li>
     <li>2026-08-18 重构「功能与菜单」页:① MU 标准页面改为树状上下结构、仅保留勾选框、默认全选中;② Cube 扩展页面右侧新增「填写建议 + AI 智能补全」栏,点击「开始补全」后基于站点属性与设备资产层级由 AI 推断并补充 Cube 扩展页面(名称 + 详尽内容),追加到用户已建页面下方(不改动用户内容)(后端 <code>POST /api/db/sites/:id/complete-cubes</code> 流式、<code>GET .../complete-cubes-prompt</code> 取提示词);③ 菜单结构改为只读默认预览:MU 页面在上,所有 Cube 页面归入「综合监控」一级菜单、二级为各 Cube 名称,仅展示无编辑。</li>
     <li>2026-08-18 修复整站生成超时:将「生成确认」页全站生成由无上限 <code>Promise.all</code> 并行改为最多 3 个并发(<code>mapWithConcurrency</code>),避免 9 个 Cube 同时抢占 CPU/模型 API 资源导致部分页面 300s 超时失败;超时阈值维持 300s 不变,单页失败不再自动重试,仅提示用户到「功能与菜单」对该页面单独点「生成」重试。</li>
+    <li>2026-08-18 修复生成看板导入 Cube 后大面积空白(含「分辨率不是 1920×1080」的错觉,实为空白导致观感异常;JSON 内 pageWidth/pageHeight 始终为 1920/1080 正确)。根因:生成提示词 <code>DASHBOARD_SPEC</code> 仅含糊要求「include proper chartConfig」,模型据此把图表配置整体套一层 <code>custom.chartConfig</code> 外壳,且自造 StatusLight / ParameterCard / Table / GeoMap 的 custom 结构,而 Cube 工具要求的是 dashboard-json-generator 各面板模板的精确结构(Bar/LineChart/HeatMap 为扁平 <code>custom:{config,series,xAxis,yAxis,legend,grid,dataZoom}</code>;PieChart/RadarChart/ProgressRing/GeoMap 为 <code>custom:{dataConfig,chartConfig}</code> 等)。修复:(1) <code>DASHBOARD_SPEC</code> 改为强制要求每个面板逐项读取模板文件 <code>/root/.codebuddy/skills/dashboard-json-generator/assets/templates/&lt;type&gt;.json</code> 并原样照抄其 <code>custom</code> 结构,明确扁平 vs 包裹规则及 StatusLight/ParameterCard/Table/GeoMap 的字段;(2) <code>fixDashboard</code> 增加 <code>normalizePanelCustom</code> 兜底:把 <code>custom.chartConfig</code> 拍平为扁平 custom、StatusLight 的 statusMap→configs、ParameterCard 的 items→tableCnt/rowCnt/style、Table 的 columns 补成可渲染结构、GeoMap 补全 dataConfig 绑定并在存在 <code>data/db/china-geo.json</code> 时注入中国地图 geoJson。存量 gen_files 已用该兜底重写。</li>
   </ul>
 
   <ul>