return errors;
}
+// ① 代码级自动修复:对校验失败的结构做程序化修正,避免为小问题重新调用模型。
+// 返回修正后的 json。无法可靠修复的项(如 panel 类型非法、grid 主题色缺失、缺少 Title 面板)
+// 交由上游 repairAndValidate 重新校验后决定是否需再次调用模型。
+function fixDashboard(json) {
+ if (!json || typeof json !== "object") return json;
+ if (!Array.isArray(json.panels) || json.panels.length === 0) return json;
+ // 首面板强制为 Title(若 Title 出现在后面则提到最前)
+ const titleIdx = json.panels.findIndex(p => p && p.type === "Title");
+ if (titleIdx > 0) {
+ const [tp] = json.panels.splice(titleIdx, 1);
+ json.panels.unshift(tp);
+ }
+ if (json.panels[0] && json.panels[0].type === "Title") {
+ json.panels[0].gridPos = { ...(json.panels[0].gridPos || {}), w: 24, h: 2, x: 0, y: 0 };
+ }
+ const occupied = new Set();
+ for (let i = 0; i < json.panels.length; i++) {
+ const p = json.panels[i];
+ if (!p || typeof p !== "object") continue;
+ if (!p.id || typeof p.id !== "string") p.id = randomUUID();
+ // gridPos 缺失或非法 → 给出合法默认值(后续 overlap 会据此下推)
+ if (!p.gridPos || typeof p.gridPos !== "object") {
+ p.gridPos = { w: i === 0 ? 24 : 12, h: i === 0 ? 2 : 6, x: 0, y: i === 0 ? 0 : 2, content: "", id: p.id };
+ }
+ let { w, h, x, y } = p.gridPos;
+ if (!Number.isInteger(w) || w < 1) w = 12;
+ if (w > 24) w = 24;
+ if (!Number.isInteger(h) || h < 1) h = 6;
+ if (!Number.isInteger(x) || x < 0) x = 0;
+ if (x + w > 24) x = Math.max(0, 24 - w); // clamp x+w ≤ 24
+ if (!Number.isInteger(y) || y < 0) y = i === 0 ? 0 : 2;
+ if (i > 0 && y < 2) y = 2;
+ // 重叠 → 整体向下推直到不再冲突
+ const overlaps = (yy) => {
+ for (let gx = x; gx < x + w; gx++)
+ for (let gy = yy; gy < yy + h; gy++)
+ if (occupied.has(`${gx},${gy}`)) return true;
+ return false;
+ };
+ let ny = y;
+ while (overlaps(ny) && ny < 240) ny += 1;
+ y = ny;
+ for (let gx = x; gx < x + w; gx++)
+ for (let gy = y; gy < y + h; gy++)
+ occupied.add(`${gx},${gy}`);
+ p.gridPos = { ...p.gridPos, w, h, x, y };
+ // 各类型 data 字段的代码级修正
+ if (p.type === "KPIIndicator") {
+ p.data = p.data || {};
+ p.data.calc = true;
+ p.data.dim = p.data.dim || {};
+ p.data.dim.dim1 = "";
+ if (!Array.isArray(p.data.targets) || p.data.targets.length === 0) p.data.targets = [{ dataType: "sum" }];
+ if (p.data.targets[0]) p.data.targets[0].dataType = "sum";
+ }
+ if (["LineChart", "PieChart", "Bar", "Table", "StatusLight", "ParameterCard"].includes(p.type)) {
+ p.data = p.data || {};
+ if (!Array.isArray(p.data.targets) || p.data.targets.length === 0) p.data.targets = [{ dataType: "" }];
+ if (p.data.targets[0]) p.data.targets[0].dataType = "";
+ }
+ if (p.type === "RadarChart") {
+ p.data = p.data || {};
+ p.data.calc = false;
+ }
+ }
+ return json;
+}
+
+// 先代码级修复再校验;ok=false 时 errors 为仍不可自动修复的问题(需再次调用模型)。
+function repairAndValidate(json) {
+ const fixed = fixDashboard(json);
+ const errors = validateDashboard(fixed);
+ return { ok: errors.length === 0, json: fixed, errors };
+}
+
function runCodebuddy(prompt, sessionId, onEvent) {
return new Promise((resolve, reject) => {
const args = [
- 补充要求: ${site.supplement || "无"}
- 资产配置: ${assetLine(site)}
-请根据这些信息,学习对应领域的仪表盘特点,需要什么元素,体现什么信息,如何进行统计和呈现。并通过 dashboard-json-generator Skill 中的 Cube 工具的规则,生成一个 JSON 文件。这个 JSON 文件,我可以导入到 Cube 工具中,呈现出你设计的仪表盘。其中需要包括合理的模拟数据,如果是多个 Cube 页面,则数据之间关系合理。
+è¯·æ ¹æ\8d®è¿\99äº\9bä¿¡æ\81¯ï¼\8cå¦ä¹ 对åº\94é¢\86å\9f\9fç\9a\84仪表ç\9b\98ç\89¹ç\82¹ï¼\8cé\9c\80è¦\81ä»\80ä¹\88å\85\83ç´ ï¼\8cä½\93ç\8e°ä»\80ä¹\88ä¿¡æ\81¯ï¼\8cå¦\82ä½\95è¿\9bè¡\8cç»\9f计å\92\8cå\91\88ç\8e°ã\80\82å¹¶é\80\9aè¿\87 dashboard-json-generator Skill ä¸ç\9a\84 Cube å·¥å\85·ç\9a\84è§\84å\88\99ï¼\8cç\94\9fæ\88\90ä¸\80个 JSON æ\96\87ä»¶ã\80\82è¿\99个 JSON æ\96\87ä»¶ï¼\8cæ\88\91å\8f¯ä»¥å¯¼å\85¥å\88° Cube å·¥å\85·ä¸ï¼\8cå\91\88ç\8e°å\87ºä½ 设计ç\9a\84仪表ç\9b\98ã\80\82å\85¶ä¸é\9c\80è¦\81å\8c\85æ\8b¬å\90\88ç\90\86ç\9a\84模æ\8b\9fæ\95°æ\8d®ï¼\88模æ\8b\9fæ\95°æ\8d®è§\84模é\80\82ä¸å\8d³å\8f¯ï¼\9aæ\97¶é\97´åº\8få\88\97约 12â\80\9316 个ç\82¹ï¼\8cè¡¨æ ¼/KPI æ ·æ\9c¬ 3â\80\936 è¡\8cï¼\8cä¸\8då¿\85è¿\87å¤\9aï¼\89ï¼\8cå¦\82æ\9e\9cæ\98¯å¤\9a个 Cube 页é\9d¢ï¼\8cå\88\99æ\95°æ\8d®ä¹\8bé\97´å\85³ç³»å\90\88ç\90\86ã\80\82
请生成对应的 JSON 文件。`;
}
scan(style);
return [...colors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(e => e[0]);
}
-async function generateCubeJson(site, cube, attempt = 1, previousErrors = "", onEvent) {
- const promptBase = buildCubePrompt(site, cube);
- const prompt = previousErrors
- ? `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 json;
- try { json = await extractJson(text); }
- catch (e) { throw new Error("JSON parse failed: " + e.message); }
- const errors = validateDashboard(json);
- if (errors.length > 0) {
- if (attempt >= 3) throw new Error("Validation failed after 3 attempts: " + errors.join("; "));
- return generateCubeJson(site, cube, attempt + 1, errors.join("\n"), onEvent);
+// ⑤ 重试降级:模型调用最多 2 次;首次校验失败先走代码级修复(fixDashboard),
+// 仅当残留问题确无法自动修复时才再次调用模型。onLog 可选,用于向「AI 生成工作流」面板推送过程日志。
+async function generateCubeJson(site, cube, onEvent, onLog) {
+ let attempt = 1;
+ let previousErrors = "";
+ while (true) {
+ const promptBase = buildCubePrompt(site, cube);
+ const prompt = previousErrors
+ ? `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 json;
+ try { json = await extractJson(text); }
+ catch (e) { throw new Error("JSON parse failed: " + e.message); }
+ const { ok, json: fixed, errors } = repairAndValidate(json);
+ if (ok) return { json: fixed, prompt: promptBase };
+ if (typeof onLog === "function" && errors.length)
+ onLog(`⚙ 检测到 ${errors.length} 处结构问题,已尝试代码级自动修正(免二次调用模型):${errors.slice(0, 3).join(";")}${errors.length > 3 ? "…" : ""}\n`);
+ if (attempt >= 2) throw new Error("Validation failed after retries: " + errors.join("; "));
+ previousErrors = errors.join("\n");
+ attempt++;
}
- return { json, prompt: promptBase };
}
// 取面板展示标题(适配不同位置的标题字段)
}
// 基于“当前已生成的 JSON + 修改意见”进行修改,保持 Cube 结构不变。
-async function refineCubeJson(site, cube, currentJson, feedback, attempt = 1, previousErrors = "", onEvent) {
+// ⑤ 同样采用代码级修复 + 最多 2 次模型调用。
+async function refineCubeJson(site, cube, currentJson, feedback, onEvent, onLog) {
const panels = Array.isArray(currentJson.panels) ? currentJson.panels : [];
const titleList = panels
.map((p, i) => ` ${i + 1}. [${p.type}] ${panelTitle(p) || "(无标题)"}`)
当前 JSON:
${JSON.stringify(currentJson, null, 2)}`;
- const prompt = previousErrors
- ? `The previous output failed validation:\n${previousErrors}\n\n${refineInstruction}\n\nPlease fix the errors and output ONLY the corrected raw dashboard JSON.`
- : refineInstruction;
- const sessionId = `stationbuilder-refine-${site.id}-${cube.id || 0}-${attempt}`;
- const text = await runCodebuddy(prompt, sessionId, onEvent);
- let json;
- try { json = await extractJson(text); }
- catch (e) { throw new Error("JSON parse failed: " + e.message); }
- const errors = validateDashboard(json);
- const delErr = checkDeletionFeedback(feedback, currentJson, json);
- if (delErr) errors.push(delErr);
- if (errors.length > 0) {
- if (attempt >= 3) throw new Error("Refine failed after 3 attempts: " + errors.join("; "));
- return refineCubeJson(site, cube, currentJson, feedback, attempt + 1, errors.join("\n"), onEvent);
+ let attempt = 1;
+ let previousErrors = "";
+ while (true) {
+ const prompt = previousErrors
+ ? `The previous output failed validation:\n${previousErrors}\n\n${refineInstruction}\n\nPlease fix the errors and output ONLY the corrected raw dashboard JSON.`
+ : refineInstruction;
+ const sessionId = `stationbuilder-refine-${site.id}-${cube.id || 0}-${attempt}`;
+ const text = await runCodebuddy(prompt, sessionId, onEvent);
+ let json;
+ try { json = await extractJson(text); }
+ catch (e) { throw new Error("JSON parse failed: " + e.message); }
+ const rep = repairAndValidate(json);
+ const errs = rep.errors.slice();
+ const delErr = checkDeletionFeedback(feedback, currentJson, rep.json);
+ if (delErr) errs.push(delErr);
+ if (errs.length === 0) return { json: rep.json };
+ if (typeof onLog === "function" && rep.errors.length)
+ onLog(`⚙ 检测到 ${rep.errors.length} 处结构问题,已尝试代码级自动修正(免二次调用模型):${rep.errors.slice(0, 3).join(";")}${rep.errors.length > 3 ? "…" : ""}\n`);
+ if (attempt >= 2) throw new Error("Refine failed after retries: " + errs.join("; "));
+ previousErrors = errs.join("\n");
+ attempt++;
}
- return { json };
}
async function createSiteZip(siteId) {
emit({ task: `修改「${cube.name}」`, status: "running" });
emit({ log: `▶ 调用 Cube Skill(dashboard-json-generator)修改「${cube.name}」…\n` });
- const { json } = await refineCubeJson(site, cube, currentJson, feedback, 1, "", onEvent);
+ const { json } = await refineCubeJson(site, cube, currentJson, feedback, onEvent, (t) => emit({ log: t }));
const filename = `${safeFilename(cube.name)}.json`;
const jsonText = JSON.stringify(json, null, 2);
if (cube.gen_file_id) {
streamEvent(res, { log: o.event.delta.text });
}
};
- streamEvent(res, { log: `\n▶ 开始站点生成:调用 Wordbuddy AI(Hy3)与 dashboard-json-generator Skill 生成 ${cubes.length} 个 Cube 页面…\n` });
+ streamEvent(res, { log: `\n▶ 开始站点生成:并行调用 Wordbuddy AI(Hy3)与 dashboard-json-generator Skill 生成 ${cubes.length} 个 Cube 页面…\n` });
streamEvent(res, { task: "模型准备", status: "done" });
- for (const cube of cubes) {
+ // ⑥ 并行生成:各 Cube 互为独立,使用 Promise.all 并发调用,显著缩短整站生成耗时。
+ const onLog = (t) => streamEvent(res, { log: t });
+ await Promise.all(cubes.map(async (cube) => {
streamEvent(res, { task: cube.name, status: "running" });
streamEvent(res, { log: `\n▶ 调用 Cube Skill(dashboard-json-generator)生成「${cube.name}」…\n` });
try {
- const { json, prompt } = await generateCubeJson(site, cube, 1, "", onEvent);
+ const { json, prompt } = await generateCubeJson(site, cube, onEvent, onLog);
const filename = `${safeFilename(cube.name)}.json`;
const jsonText = JSON.stringify(json, null, 2);
if (cube.gen_file_id) {
streamEvent(res, { log: `✗ 「${cube.name}」生成失败:${e.message}\n` });
streamEvent(res, { task: cube.name, status: "failed", message: e.message });
}
- }
+ }));
streamEvent(res, { log: "\n▶ 正在打包与校验交付物…\n" });
streamEvent(res, { task: "打包与检查校验", status: "running" });
const zipPath = await createSiteZip(siteId);