From: wang.yu Date: Tue, 18 Aug 2026 04:19:40 +0000 (+0800) Subject: 修复生成看板导入Cube后空白:DASHBOARD_SPEC强制照模板custom结构 + fixDashboard归一化兜底 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=2315325c7f613cc9bc543add1ea5bf25e53f3bf4;p=StationBuilder.git 修复生成看板导入Cube后空白:DASHBOARD_SPEC强制照模板custom结构 + fixDashboard归一化兜底 --- diff --git a/db-sidecar.mjs b/db-sidecar.mjs index e186bff..510df16 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -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; } diff --git a/public/design-doc.html b/public/design-doc.html index f564c5a..4e9deb0 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -347,6 +347,7 @@
  • 2026-08-18 扩展「AI 智能补全」至 Asset 资产设置页:右侧新增与基本属性同款的「填写建议 + AI 智能补全」栏;点击「开始补全」后基于站点名称 / 领域 / 描述 / 补充要求分析并补充设备类型、数量预设与命名规则,追加到用户已填项下方(不改动已有项),资产层级预览随之重算(后端 POST /api/db/sites/:id/complete-assets 流式、GET .../complete-assets-prompt 取提示词)。
  • 2026-08-18 重构「功能与菜单」页:① MU 标准页面改为树状上下结构、仅保留勾选框、默认全选中;② Cube 扩展页面右侧新增「填写建议 + AI 智能补全」栏,点击「开始补全」后基于站点属性与设备资产层级由 AI 推断并补充 Cube 扩展页面(名称 + 详尽内容),追加到用户已建页面下方(不改动用户内容)(后端 POST /api/db/sites/:id/complete-cubes 流式、GET .../complete-cubes-prompt 取提示词);③ 菜单结构改为只读默认预览:MU 页面在上,所有 Cube 页面归入「综合监控」一级菜单、二级为各 Cube 名称,仅展示无编辑。
  • 2026-08-18 修复整站生成超时:将「生成确认」页全站生成由无上限 Promise.all 并行改为最多 3 个并发(mapWithConcurrency),避免 9 个 Cube 同时抢占 CPU/模型 API 资源导致部分页面 300s 超时失败;超时阈值维持 300s 不变,单页失败不再自动重试,仅提示用户到「功能与菜单」对该页面单独点「生成」重试。
  • +
  • 2026-08-18 修复生成看板导入 Cube 后大面积空白(含「分辨率不是 1920×1080」的错觉,实为空白导致观感异常;JSON 内 pageWidth/pageHeight 始终为 1920/1080 正确)。根因:生成提示词 DASHBOARD_SPEC 仅含糊要求「include proper chartConfig」,模型据此把图表配置整体套一层 custom.chartConfig 外壳,且自造 StatusLight / ParameterCard / Table / GeoMap 的 custom 结构,而 Cube 工具要求的是 dashboard-json-generator 各面板模板的精确结构(Bar/LineChart/HeatMap 为扁平 custom:{config,series,xAxis,yAxis,legend,grid,dataZoom};PieChart/RadarChart/ProgressRing/GeoMap 为 custom:{dataConfig,chartConfig} 等)。修复:(1) DASHBOARD_SPEC 改为强制要求每个面板逐项读取模板文件 /root/.codebuddy/skills/dashboard-json-generator/assets/templates/<type>.json 并原样照抄其 custom 结构,明确扁平 vs 包裹规则及 StatusLight/ParameterCard/Table/GeoMap 的字段;(2) fixDashboard 增加 normalizePanelCustom 兜底:把 custom.chartConfig 拍平为扁平 custom、StatusLight 的 statusMap→configs、ParameterCard 的 items→tableCnt/rowCnt/style、Table 的 columns 补成可渲染结构、GeoMap 补全 dataConfig 绑定并在存在 data/db/china-geo.json 时注入中国地图 geoJson。存量 gen_files 已用该兜底重写。