]> acesimba.cloud Git - StationBuilder.git/commitdiff
perf: 生成流程四项优化(代码级修复/精简数据/重试降级/并行生成)
authorwang.yu <wang.yu@advantech.com>
Mon, 17 Aug 2026 14:02:04 +0000 (22:02 +0800)
committerwang.yu <wang.yu@advantech.com>
Mon, 17 Aug 2026 14:02:04 +0000 (22:02 +0800)
- ① 新增 fixDashboard + repairAndValidate:校验失败优先程序化修复(越界/重叠/Title定位/KPI与Radar字段/缺失id/gridPos),免重跑模型
- ⑤ 生成与微调重试由最多 3 次模型调用降为 2 次(首次失败先走代码修复),修复过程推送 AI 生成工作流日志
- ② DASHBOARD_SPEC 与站点上下文约束模拟数据规模(时序<=16点、样例<=6行)以缩减输出 token
- ⑥ handleGenerateAll 由顺序循环改为 Promise.all 并发生成各 Cube
- 同步更新 public/design-doc.html 第6节与性能说明

db-sidecar.mjs
public/design-doc.html

index 76fee867b542a1deb6938eecbcd387bd2e21d510..b970171652765e2b1f3c317aa5a665361e92c134 100644 (file)
@@ -344,6 +344,81 @@ function validateDashboard(json) {
   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 = [
@@ -417,7 +492,7 @@ Site context (所有页面通用):
 - 补充要求: ${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 文件。`;
 }
@@ -537,22 +612,29 @@ function extractColorPalette(style) {
   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 };
 }
 
 // 取面板展示标题(适配不同位置的标题字段)
@@ -590,7 +672,8 @@ function checkDeletionFeedback(feedback, beforeJson, afterJson) {
 }
 
 // 基于“当前已生成的 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) || "(无标题)"}`)
@@ -616,22 +699,28 @@ ${feedback}
 
 当前 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) {
@@ -727,7 +816,7 @@ async function handleRefineCube(req, res, siteId, cubeId) {
     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) {
@@ -782,13 +871,15 @@ async function handleGenerateAll(req, res, siteId) {
         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) {
@@ -805,7 +896,7 @@ async function handleGenerateAll(req, res, siteId) {
         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);
index aea514194d54c25a080844613791e5ce5018aca0..84717d38d3c4dd66746dff1a5eb0f14f299fd327 100644 (file)
   <div class="flow">
     <ol>
       <li><b>发起</b>:在「生成确认」点「开始生成」→ 前端 <code>POST /api/db/sites/:id/generate-all</code>(携带各 Cube 的名称与内容)。</li>
-      <li><b>落库与准备</b>:db-sidecar 先持久化前端传入的 Cube,再逐页循环。</li>
-      <li><b>调用模型</b>:每页调用 <code>runCodebuddy(prompt, sessionId)</code> → <code>spawn</code> codebuddy CLI(<code>--model hy3</code>、<code>--output-format stream-json</code>、<code>--tools ""</code>)。提示词内含 dashboard-json-generator Skill 的 Cube 结构规则(DASHBOARD_SPEC)与站点/样式上下文。</li>
-      <li><b>解析与校验</b>:从流式输出抽取 JSON → <code>validateDashboard</code> 校验;不通过最多重试 3 次。</li>
+      <li><b>落库与准备</b>:db-sidecar 先持久化前端传入的 Cube,再以 <code>Promise.all</code> 并行调用各 Cube 的生成(各页相互独立,互不阻塞)。</li>
+      <li><b>调用模型</b>:每页调用 <code>runCodebuddy(prompt, sessionId)</code> → <code>spawn</code> codebuddy CLI(<code>--model hy3</code>、<code>--output-format stream-json</code>、<code>--tools ""</code>)。提示词内含 dashboard-json-generator Skill 的 Cube 结构规则(DASHBOARD_SPEC)与站点/样式上下文;DASHBOARD_SPEC 已要求模拟数据精简(时序 ≤16 点、KPI/表格样例 ≤6 行)以缩短输出。</li>
+      <li><b>解析与校验(含代码级自动修复)</b>:从流式输出抽取 JSON → <code>fixDashboard</code> 先做代码级自动修复(修复 <code>x+w&gt;24</code>、面板重叠、首面板 Title 定位、KPI/Radar/Line 等 <code>data</code> 字段、缺失 <code>id/gridPos</code> 等)→ 再 <code>validateDashboard</code> 复校;仅当残留问题确无法自动修复时才再次调用模型(最多 2 次模型调用,原 3 次)。</li>
       <li><b>写回</b>:生成的 JSON 存入 gen_files 并关联 Cube,状态置为 generated,同时保存组合提示词。</li>
       <li><b>打包</b>:全部页面完成后打包为 station-package.zip。</li>
       <li><b>实时回传</b>:整个过程以 NDJSON 流回传 <code>{task,status}</code> 进度事件与 <code>{log}</code> 文本事件;前端「AI 生成工作流」终端实时滚动显示,任务列表同步更新。</li>
   <div class="note">
     <p><b>模型说明</b>:生成固定使用 Hy3(后端硬编码),与顶栏「模型选择」无关——模型选择仅作用于右下角 AI 问答。每个 Cube 均新起一个 CLI 进程,存在进程冷启动耗时;实时日志可缓解等待时的“无反馈”感。</p>
     <p><b>单页生成</b>:在「功能与菜单」页对每个 Cube 点「生成」走非流式接口,<b>不</b>提供实时日志,仅结果刷新。</p>
+    <p><b>性能优化(本轮)</b>:在不影响显示效果的前提下做了四项优化 —— ① <b>代码级自动修复</b>:校验失败优先用 <code>fixDashboard</code> 程序化修正,避免为小问题重跑模型;② <b>精简模拟数据</b>:提示词约束时序 ≤16 点、样例 ≤6 行,缩小输出 token;⑤ <b>重试降级</b>:模型调用由最多 3 次降为最多 2 次(首次失败先走代码修复);⑥ <b>并行生成</b>:整站各 Cube 由顺序改为 <code>Promise.all</code> 并发。综合效果:整站生成约 4–9× 提速(75–90% 耗时下降),单次超时页(16 面板)约 60% 提速;已较快的单页提升约 30%。</p>
   </div>
 
   <h2>7. AI 问答(悬浮按钮 + 对话框)</h2>