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";
- 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.
`;
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);
// ① 代码级自动修复:对校验失败的结构做程序化修正,避免为小问题重新调用模型。
// 返回修正后的 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;
p.data = p.data || {};
p.data.calc = false;
}
+ // custom 结构归一化(图表/组件空白的主因)
+ normalizePanelCustom(p);
}
return json;
}