From: wang.yu Date: Mon, 17 Aug 2026 14:02:04 +0000 (+0800) Subject: perf: 生成流程四项优化(代码级修复/精简数据/重试降级/并行生成) X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=14562919e7843f5924c942c9bbbe713838b78541;p=StationBuilder.git perf: 生成流程四项优化(代码级修复/精简数据/重试降级/并行生成) - ① 新增 fixDashboard + repairAndValidate:校验失败优先程序化修复(越界/重叠/Title定位/KPI与Radar字段/缺失id/gridPos),免重跑模型 - ⑤ 生成与微调重试由最多 3 次模型调用降为 2 次(首次失败先走代码修复),修复过程推送 AI 生成工作流日志 - ② DASHBOARD_SPEC 与站点上下文约束模拟数据规模(时序<=16点、样例<=6行)以缩减输出 token - ⑥ handleGenerateAll 由顺序循环改为 Promise.all 并发生成各 Cube - 同步更新 public/design-doc.html 第6节与性能说明 --- diff --git a/db-sidecar.mjs b/db-sidecar.mjs index 76fee86..b970171 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -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 页面,则数据之间关系合理。 +请根据这些信息,学习对应领域的仪表盘特点,需要什么元素,体现什么信息,如何进行统计和呈现。并通过 dashboard-json-generator Skill 中的 Cube 工具的规则,生成一个 JSON 文件。这个 JSON 文件,我可以导入到 Cube 工具中,呈现出你设计的仪表盘。其中需要包括合理的模拟数据(模拟数据规模适中即可:时间序列约 12–16 个点,表格/KPI 样本 3–6 行,不必过多),如果是多个 Cube 页面,则数据之间关系合理。 请生成对应的 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); diff --git a/public/design-doc.html b/public/design-doc.html index aea5141..84717d3 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -292,9 +292,9 @@
  1. 发起:在「生成确认」点「开始生成」→ 前端 POST /api/db/sites/:id/generate-all(携带各 Cube 的名称与内容)。
  2. -
  3. 落库与准备:db-sidecar 先持久化前端传入的 Cube,再逐页循环。
  4. -
  5. 调用模型:每页调用 runCodebuddy(prompt, sessionId) → spawn codebuddy CLI(--model hy3、--output-format stream-json、--tools "")。提示词内含 dashboard-json-generator Skill 的 Cube 结构规则(DASHBOARD_SPEC)与站点/样式上下文。
  6. -
  7. 解析与校验:从流式输出抽取 JSON → validateDashboard 校验;不通过最多重试 3 次。
  8. +
  9. 落库与准备:db-sidecar 先持久化前端传入的 Cube,再以 Promise.all 并行调用各 Cube 的生成(各页相互独立,互不阻塞)。
  10. +
  11. 调用模型:每页调用 runCodebuddy(prompt, sessionId) → spawn codebuddy CLI(--model hy3、--output-format stream-json、--tools "")。提示词内含 dashboard-json-generator Skill 的 Cube 结构规则(DASHBOARD_SPEC)与站点/样式上下文;DASHBOARD_SPEC 已要求模拟数据精简(时序 ≤16 点、KPI/表格样例 ≤6 行)以缩短输出。
  12. +
  13. 解析与校验(含代码级自动修复):从流式输出抽取 JSON → fixDashboard 先做代码级自动修复(修复 x+w>24、面板重叠、首面板 Title 定位、KPI/Radar/Line 等 data 字段、缺失 id/gridPos 等)→ 再 validateDashboard 复校;仅当残留问题确无法自动修复时才再次调用模型(最多 2 次模型调用,原 3 次)。
  14. 写回:生成的 JSON 存入 gen_files 并关联 Cube,状态置为 generated,同时保存组合提示词。
  15. 打包:全部页面完成后打包为 station-package.zip。
  16. 实时回传:整个过程以 NDJSON 流回传 {task,status} 进度事件与 {log} 文本事件;前端「AI 生成工作流」终端实时滚动显示,任务列表同步更新。
  17. @@ -303,6 +303,7 @@

    模型说明:生成固定使用 Hy3(后端硬编码),与顶栏「模型选择」无关——模型选择仅作用于右下角 AI 问答。每个 Cube 均新起一个 CLI 进程,存在进程冷启动耗时;实时日志可缓解等待时的“无反馈”感。

    单页生成:在「功能与菜单」页对每个 Cube 点「生成」走非流式接口,不提供实时日志,仅结果刷新。

    +

    性能优化(本轮):在不影响显示效果的前提下做了四项优化 —— ① 代码级自动修复:校验失败优先用 fixDashboard 程序化修正,避免为小问题重跑模型;② 精简模拟数据:提示词约束时序 ≤16 点、样例 ≤6 行,缩小输出 token;⑤ 重试降级:模型调用由最多 3 次降为最多 2 次(首次失败先走代码修复);⑥ 并行生成:整站各 Cube 由顺序改为 Promise.all 并发。综合效果:整站生成约 4–9× 提速(75–90% 耗时下降),单次超时页(16 面板)约 60% 提速;已较快的单页提升约 30%。

    7. AI 问答(悬浮按钮 + 对话框)