- custom can include icon, thresholds, compare.
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.
+For EVERY panel the "custom" object MUST match the structure for its TYPE below. Copy the SHAPE exactly (keep every top-level key shown; you may drop rarely-needed sub-fields but keep series/axis wrappers). Do NOT invent or rename keys. Flat vs wrapper: Bar/LineChart/HeatMap are FLAT (NO chartConfig wrapper); PieChart/RadarChart/ProgressRing/GeoMap/Gauge/ProgressBar/Liquidfill/HeatMap use {dataConfig, chartConfig}.
+COLOR_PALETTE = #0062CC,#00B900,#FF9300,#EBC147,#D73C74,#8048C8,#6D84C9,#34A0D5 (use for every color/color[] field, or the 推荐配色板 if provided)
+
+Bar: { "config": { "direction": "vertical" }, "alignTicks": true, "color": [COLOR_PALETTE], "series": { "config": { "sort": "desc", "stack": false, "unit": "" }, "type": "bar", "label": { "show": false } }, "yAxis": { "type": "value", "axisLabel": { "color": "#787878" } }, "xAxis": { "config": { "mode": "category" }, "axisLabel": { "show": true, "color": "#787878" } }, "legend": { "show": true, "position": "bottom" }, "grid": { "top": 25, "left": 10, "right": 10, "bottom": 10, "containLabel": true }, "dataZoom": [ { "type": "inside", "start": 0, "end": 100 } ] }
+LineChart: { "config": { "interaction": { "enable": false } }, "multiY": false, "alignTicks": true, "dimension": "", "color": [COLOR_PALETTE], "series": [ { "config": { "stack": false, "unit": "" }, "type": "line", "label": { "show": false }, "lineStyle": { "width": 2 }, "smooth": false, "showSymbol": false, "symbol": "emptyCircle", "sampling": "lttb", "symbolSize": 4, "id": "A" } ], "yAxis": [ { "type": "value", "axisLabel": { "color": "#787878" } } ], "xAxis": { "config": { "mode": "time" }, "axisLabel": { "show": true, "color": "#787878" } }, "legend": { "show": true, "position": "bottom" }, "grid": { "top": 0, "left": 15, "right": 35, "bottom": 10, "containLabel": true }, "dataZoom": [ { "type": "inside", "start": 0, "end": 100 } ] }
+PieChart: { "dataConfig": { "unit": "", "direction": "column", "dimension": "", "valueFormat": "", "valueType": "current", "roseSort": "", "colorMap": [] }, "chartConfig": { "color": [COLOR_PALETTE], "series": [ { "config": { "showBorder": true, "richLabel": true }, "name": "", "type": "pie", "radius": [45, 75], "center": [50, 50], "label": { "show": true, "position": "outside" } } ], "legend": { "show": false } } }
+Table: { "columns": [ { "id": "A", "unit": "", "type": "string", "data": { "decimals": 0, "thresholds": { "enable": false }, "format": "", "mapping": { "enable": false, "type": "value", "rules": [] }, "link": { "enable": false, "url": "" } }, "style": { "sort": { "enable": false }, "filter": { "enable": false } } } ], "page": { "show": true, "pageSize": 10 }, "stripe": { "enable": false, "color1": "#fff", "color2": "#fafafa" }, "border": { "enable": true, "show": true, "showCol": false, "borderWidth": 1, "borderColor": "rgba(200,200,200,0.12)" }, "title": { "enable": true, "fontSize": 16, "fontWeight": "bold", "textAlign": "left", "color": "rgb(50,50,50)", "backgroundColor": "transparent", "height": 43, "paddingX": 16, "paddingY": 8 }, "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 } } } // columns[].id MUST equal data.targets[].id
+StatusLight:{ "columns": 3, "machineTextSize": 16, "machineBold": "bold", "statusTextSize": 15, "statusTextBold": "bold", "icon": "status-icon-1", "iconSize": 30, "configs": [ { "label": "运行", "color": "#00CC66", "value": "1" }, { "label": "停止", "color": "#F9C80E", "value": "2" } ] }
+ParameterCard:{ "tableCnt": 1, "rowCnt": 10, "style": { "color": "#323233", "fontSize": 16, "fontWeight": 700 }, "styleLeft": { "fontSize": 16, "fontWeight": 400, "color": "#89898a" } }
+GeoMap: { "dataConfig": { "geoFileName": "中国-area.json", "theme": "light", "zoomTo": 0.8, "geoType": "effectScatter", "longitudeId": "A", "latitudeId": "B", "coordValId": "C", "coordNameId": "D", "geoJson": null }, "panelType": "carousel", "panelSelectedIds": [] } // geoJson is auto-injected (set null); data.calc=true, 4 targets dataType "first"
+Gauge: { "dataConfig": { "dimension": "", "valueFormat": "", "valueType": "current" }, "chartConfig": { "config": { "enableTh": true, "thresholds": [ { "value": 33, "color": "#00CC66" }, { "value": 66, "color": "#FFA400" }, { "value": 100, "color": "#F55D3E" } ] }, "series": { "type": "gauge", "min": 0, "max": 100, "splitNumber": 10, "axisLine": { "show": true, "lineStyle": { "width": 10, "color": [ [0.6, "#00CC66"], [0.8, "#FFA400"], [1, "#F55D3E"] ] } }, "pointer": { "show": true, "length": "60%", "width": 6 }, "axisLabel": { "show": true, "color": "rgb(250,250,250)", "fontSize": 8 }, "detail": { "show": true, "formatter": "{value}" } } } }
+ProgressBar: { "dataConfig": { "realValKey": ["A"], "targetValKey": "B", "isCustTargetKey": true, "min": 0, "max": 100, "isPercent": true, "fixedNum": 1, "legendPosition": "bottom", "isLegendVertical": false, "value": { "colorType": "threshold", "style": { "color": "#008cd6", "fontSize": "40px", "fontWeight": "bold" }, "thresholds": [ { "color": "rgb(245,93,62)", "value1": 0, "operator1": "<=", "value2": 60, "operator2": "<" }, { "color": "rgb(255,164,0)", "value1": 60, "operator1": "<=", "value2": 80, "operator2": "<" }, { "color": "rgb(0,204,102)", "value1": 80, "operator1": "<=", "value2": 100, "operator2": "<" } ] } }, "chartConfig": { "legend": { "show": false }, "xAxis": { "show": false, "type": "value", "max": 100 }, "yAxis": [ { "type": "category", "show": false, "axisLabel": { "color": "rgb(250,250,250)", "fontSize": 11 } } ], "series": [ { "type": "bar", "data": [] } ] } }
+Liquidfill: { "dataConfig": { "realValKey": "A", "targetValKey": "B", "isCustTargetKey": true, "min": 0, "max": 90, "fixedNum": 2, "value": { "colorType": "default", "style": { "color": "#008CD6", "fontSize": "32px", "fontWeight": "bold" }, "thresholds": [ { "color": "#DF001A", "value1": 0, "operator1": "<=", "value2": 60, "operator2": "<" }, { "color": "#FF9300", "value1": 60, "operator1": "<=", "value2": 80, "operator2": "<" }, { "color": "#00B900", "value1": 80, "operator1": "<=", "value2": null, "operator2": "<" } ] } }, "chartConfig": { "title": { "show": true, "left": "center", "top": "40%", "textStyle": { "fontSize": 14, "color": "rgb(250,250,250)" } }, "series": [ { "type": "liquidFill", "radius": "97%", "center": ["50%", "50%"], "color": ["#5594fa"], "data": [0], "label": { "show": true, "color": "#5594fa", "fontSize": 22 }, "outline": { "borderDistance": 7, "itemStyle": { "borderWidth": 3, "borderColor": "#5594fa" } }, "shape": "rect" } ] } }
+ProgressRing:{ "dataConfig": { "min": 0, "max": 100, "isPercent": true, "realValKey": "A", "isCustTargetKey": true, "value": { "colorType": "default", "style": { "color": "#00B900", "fontSize": "40px", "fontWeight": "bold" }, "thresholds": [ { "color": "#DF001A", "value1": null, "operator1": "<=", "value2": 60, "operator2": "<" }, { "color": "#FF9300", "value1": 60, "operator1": "<=", "value2": 80, "operator2": "<" }, { "color": "#00B900", "value1": 80, "operator1": "<=", "value2": null, "operator2": "<" } ] }, "chartConfig": { "title": { "show": true, "left": "center", "top": "40%", "textStyle": { "fontSize": 17, "fontWeight": "bold", "color": "rgb(132,130,136)" } }, "graphic": { "type": "text", "left": "center", "top": "55%", "style": { "fill": "rgb(0,185,0)", "text": "", "fontSize": 21 } }, "polar": { "center": ["50%", "50%"], "radius": ["63%", "78%"] }, "angleAxis": { "max": 100, "show": false }, "radiusAxis": { "type": "category", "show": true, "axisLabel": { "show": false } }, "series": [ { "type": "bar", "roundCap": false, "showBackground": true, "backgroundStyle": { "color": "rgba(131,130,181,0.5)" }, "data": [95.2], "coordinateSystem": "polar", "itemStyle": { "color": "#5594fa" } } ] }
+RadarChart: { "dataConfig": { "min": 0, "max": 100, "isLimit": false, "value": { "legendColors": [ { "name": "当前水平", "color": "rgb(85,148,250)" } ] } }, "chartConfig": { "tooltip": { "show": true }, "radar": { "radius": "65%", "splitArea": { "show": false }, "indicator": [] }, "series": [ { "name": "当前水平", "type": "radar", "data": [], "itemStyle": { "color": "rgb(85,148,250)" }, "lineStyle": { "color": "rgb(85,148,250)", "width": 2 }, "areaStyle": { "color": "rgba(85,148,250,0.2)" } } ], "legend": { "show": false } } }
+HeatMap: { "dataConfig": { "direction": "column", "valueFormat": "", "dimensionX": { "key": "A", "dataType": "datetime", "format": "yyyy/MM/dd" }, "dimensionY": { "key": "B", "dataType": "" }, "dimensionZ": { "key": "C" } }, "chartConfig": { "legend": { "show": false }, "visualMap": { "type": "continuous", "min": 0, "max": 100, "inRange": { "color": ["#eaf4e7", "#f2ce81", "#a50026"] } }, "xAxis": { "type": "category", "axisLabel": { "show": true, "color": "#787878" } }, "yAxis": { "type": "category", "axisLabel": { "show": true, "color": "#787878" } }, "series": [ { "type": "heatmap", "data": [], "label": { "show": true } } ], "grid": { "top": 10, "left": 30, "right": 50, "bottom": 50 } } }
+KPIIndicator: { "icon": { "name": "home", "size": 55, "color": "#0062CC", "content": "", "invert": false }, "thresholds": { "enable": true, "rules": [] }, "compare": { "show": false, "data": [] } }
+Title: { "title": { "label": "<页面名称>", "style": { "color": "#323233", "fontSize": 40, "fontWeight": "bold" }, "bgStyle": { "backgroundColor": "" }, "align": "left" }, "refresh": { "enable": false, "interval": 60, "custom": null, "showUpdateTime": true } }
+RichText: { "data": { "text": "<p>...</p>" }, "config": { "enable": true, "params": [] } }
+Image/Container: use sensible custom per standard; small note panels are fine.
+xAxis.config.mode: use "time" ONLY for real datetime strings (e.g. "2026-07-29 08:00:00"); otherwise "category".
+If a 视觉风格要求 section provides a 推荐配色板, use THAT palette first; otherwise use COLOR_PALETTE above.
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.
+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 structure SHOWN ABOVE for that panel type (no file reading needed). A panel whose "custom" does not match its type will render BLANK after import. Do NOT add spec-outside fields such as query/linkage/triggerByTimer/minW — only the fields shown above.
Output ONLY the raw JSON object. No markdown code fences, no explanation.
`;
let fullText = "";
const timer = setTimeout(() => {
child.kill("SIGKILL");
- reject(new Error("codebuddy 生成超时(>300s)"));
- }, 300000);
+ reject(new Error("codebuddy 生成超时(>480s)"));
+ }, 480000);
const push = (line) => {
if (!line.trim()) return;
try {
});
}
+// 去除 JSON 字符串之外的 // 与 /* */ 注释(字符串内的 // 如 http:// 不受影响)。
+function stripJsonComments(s) {
+ let out = "";
+ let i = 0;
+ const n = s.length;
+ let inStr = false;
+ let strCh = "";
+ while (i < n) {
+ const ch = s[i];
+ if (inStr) {
+ out += ch;
+ if (ch === "\\") { out += s[i + 1] ?? ""; i += 2; continue; }
+ if (ch === strCh) inStr = false;
+ i++;
+ continue;
+ }
+ if (ch === '"' || ch === "'") { inStr = true; strCh = ch; out += ch; i++; continue; }
+ if (ch === "/" && s[i + 1] === "/") { while (i < n && s[i] !== "\n") i++; continue; }
+ if (ch === "/" && s[i + 1] === "*") { i += 2; while (i < n && !(s[i] === "*" && s[i + 1] === "/")) i++; i += 2; continue; }
+ out += ch; i++;
+ }
+ return out;
+}
+
+// 去掉对象/数组末尾多余逗号(如 [1,2,]、{a:1,})——LLM 常见非法 JSON。
+function removeTrailingCommas(s) {
+ return s.replace(/,(\s*[}\]])/g, "$1");
+}
+
async function extractJson(text) {
// strip markdown fences
const m = text.match(/```(?:json)?\s*([\s\S]*?)```/);
let start = text.indexOf("{");
let end = text.lastIndexOf("}");
if (start >= 0 && end > start) text = text.slice(start, end + 1);
- return JSON.parse(text);
+ // 逐步尝试:原生解析 → 去注释+去尾逗号(兜底修复模型输出的轻微非法 JSON)。
+ const candidates = [
+ text,
+ removeTrailingCommas(stripJsonComments(text)),
+ ];
+ let lastErr;
+ for (const c of candidates) {
+ try { return JSON.parse(c); }
+ catch (e) { lastErr = e; }
+ }
+ // 给出错误位置附近的原文片段,便于上层把错误回传给模型自我修正。
+ const posMatch = /position (\d+)/.exec(lastErr?.message || "");
+ let snippet = "";
+ if (posMatch) {
+ const p = Number(posMatch[1]);
+ snippet = `\n问题位置附近的原文:\n${text.slice(Math.max(0, p - 200), p + 80)}`;
+ }
+ throw new Error((lastErr ? lastErr.message : "invalid JSON") + snippet);
}
function assetLine(site) {
? `The previous output failed validation:\n${previousErrors}\n\n${promptBase}\n\nPlease fix the errors and output ONLY the corrected raw dashboard JSON.`
: promptBase;
const sessionId = `stationbuilder-gen-${site.id}-${cube.id || 0}-${attempt}`;
- const text = await runCodebuddy(prompt, sessionId, onEvent);
+ let text;
+ try {
+ text = await runCodebuddy(prompt, sessionId, onEvent);
+ } catch (e) {
+ // 调用超时/失败:重试一次,把错误回传给模型,而非直接判失败。
+ if (attempt >= 2) throw e;
+ previousErrors = `上一次调用失败(${e.message}),请重新输出完整且合法的 dashboard JSON(不要截断、不要添加规范外字段)。`;
+ attempt++;
+ continue;
+ }
let json;
- try { json = await extractJson(text); }
- catch (e) { throw new Error("JSON parse failed: " + e.message); }
+ try {
+ json = await extractJson(text);
+ } catch (e) {
+ // JSON 语法错误:把错误(含问题位置原文)回传给模型自我修正,而非直接失败。
+ if (attempt >= 2) throw new Error("JSON parse failed after retries: " + e.message);
+ previousErrors = "JSON 解析失败,请修正语法错误(多余的括号/逗号、未闭合的对象等)后重新输出完整 JSON:\n" + e.message;
+ attempt++;
+ continue;
+ }
const { ok, json: fixed, errors } = repairAndValidate(json);
if (ok) return { json: fixed, prompt: promptBase };
if (typeof onLog === "function" && errors.length)