From: root Date: Fri, 21 Aug 2026 04:05:27 +0000 (+0800) Subject: v0.29: 领域扩展页面改名置首 + AI生成醒目按钮 + 统一生成 + 图片缩略图提速 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=f9389812d0f4d83b2558e82c0b0409bd470ec36e;p=StationBuilder.git v0.29: 领域扩展页面改名置首 + AI生成醒目按钮 + 统一生成 + 图片缩略图提速 - 站点/领域两处 Cube 扩展页面页签更名为「领域扩展页面」并置于 MU 标准页面之前(默认进入) - 生成按钮文案升级为 AI生成/AI生成中,新增醒目渐变样式 .btn-ai - 领域扩展页面新增「AI 统一生成全部(N)」按钮,一键排队生成全部未生成页面,各页按钮同步显示 AI生成中 - 后端图片接口支持 ?w= 参数,sharp 生成落盘缓存 WebP 缩略图(约 150x 提速) - design-doc.html 同步至 v0.29 --- diff --git a/app/globals.css b/app/globals.css index 2863a26..0818ed2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -131,6 +131,12 @@ .cube-actions button:hover{border-color:var(--blue);color:var(--blue)} .cube-actions .trash{border-color:#f0c4bd;color:var(--red)} .cube-actions .trash:hover{background:#fdeae6;border-color:var(--red)} +/* 醒目 AI 操作按钮(渐变蓝紫) */ +.btn-ai{display:inline-flex;align-items:center;gap:4px;background:linear-gradient(135deg,#2563eb,#7c3aed);color:#fff;border:1px solid transparent;border-radius:5px;height:26px;padding:0 11px;font-size:11px;font-weight:600;cursor:pointer;box-shadow:0 1px 4px rgba(37,99,235,.35);white-space:nowrap} +.btn-ai:hover{filter:brightness(1.08)} +.btn-ai:active{transform:translateY(1px)} +.btn-ai:disabled{opacity:.65;cursor:default;box-shadow:none} +.cube-actions .btn-ai{height:26px} .menu-editor{display:flex;flex-direction:column;gap:10px} .menu-row{display:grid;grid-template-columns:1.4fr 1fr 1.4fr auto;gap:8px;align-items:center} .menu-row input,.menu-row select{border:1px solid var(--line);border-radius:3px;height:30px;padding:0 8px;background:#fff;width:100%} diff --git a/app/page.tsx b/app/page.tsx index 2ee5537..c0c411d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -848,7 +848,7 @@ function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify,onView finally{ clearTimeout(watchdog); setCompleting(false); } }; - return
+ return
{/* MU 标准页面:树状上下结构,仅保留勾选框,默认全选中 */} {tab==="mu"&&
{grouped.map(g=>
{g.parentMenu}
{g.items.map(item=>)}
)}
} {/* Cube 扩展页面:左右两栏,右栏为填写建议 + AI 智能补全 */} @@ -858,7 +858,7 @@ function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify,onView updateCube(i,{name:e.target.value})}/> updateCube(i,{content:e.target.value})}/> {cube.status==="generated"? : 未生成} - + )}

站点页面样式 站点级 · 全部 Cube 页面共用

@@ -868,7 +868,7 @@ function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify,onView
无
不使用样式 {pageStyles.map(s=>)}
@@ -1634,6 +1634,14 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA }catch(e:any){ notify("生成失败:"+(e?.message||e)); } finally{ setGenIds(prev=>{ const n=new Set(prev); n.delete(p.id); return n; }); await load(true); } }; + // 统一生成:对所有「未生成」且未在生成中的页面并发触发(每个页面独立上锁,后端按 AI_CONCURRENCY 排队), + // 各自按钮会随 genIds 自动显示「AI生成中」。 + const generateAllPages=()=>{ + const pending=pages.filter(p=>p.status!=="generated" && !genIds.has(p.id)); + if(!pending.length){ notify("没有待生成的页面"); return; } + notify(`已开始排队生成 ${pending.length} 个页面…`); + for(const p of pending) generatePage(p); + }; const downloadPage=(p:DomainPage)=>{ const blob=new Blob([p.json_text||"{}"],{type:"application/json"}); downloadBlob(blob, p.gen_filename||`${p.name||"page"}.json`); }; if(loading||!t||!m) return

加载中…

; @@ -1726,7 +1734,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA {step==="functions" && viewCompleteCubesPrompt(id,m.name,domainName,m.description,m.supplement||"",assets.map(a=>({type:a.device_name,count:a.count,rule:a.rule})))} updatePageField={(i,patchRow)=>setPages(prev=>prev.map((p,idx)=>idx===i?{...p,...patchRow}:p))} @@ -1745,16 +1753,16 @@ function PlaceholderStep({title,note}:{title:string;note:string}){ return
待开发

{note}

; } -function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,genIds,genLog,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify,pageStyles,styleId,onSelectStyle}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise;onUploadPageJson:(f:File)=>Promise;onSavePageMeta:(p:DomainPage)=>Promise;onDeletePage:(p:DomainPage)=>Promise;onGeneratePage:(p:DomainPage)=>Promise;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;genIds:Set;genLog:string;cubesAi:ReturnType;onRunCubesComplete:()=>Promise;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial)=>void;tName:string;saving:boolean;notify:(m:string)=>void;pageStyles:PageStyle[];styleId:number|null;onSelectStyle:(id:number|null)=>void}){ +function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,genIds,genLog,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify,pageStyles,styleId,onSelectStyle,onGenerateAll}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise;onUploadPageJson:(f:File)=>Promise;onSavePageMeta:(p:DomainPage)=>Promise;onDeletePage:(p:DomainPage)=>Promise;onGeneratePage:(p:DomainPage)=>Promise;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;genIds:Set;genLog:string;cubesAi:ReturnType;onRunCubesComplete:()=>Promise;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial)=>void;tName:string;saving:boolean;notify:(m:string)=>void;pageStyles:PageStyle[];styleId:number|null;onSelectStyle:(id:number|null)=>void;onGenerateAll:()=>void}){ const sourceBadge=(src?:string|null)=>{ if(src==="upload") return ↑ 上传; if(src==="ai") return ✦ AI生成; return 手动; }; - const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu"); + const [tab,setTab]=useState<"mu"|"cube"|"menu">("cube"); const genLogRef=useRef(null); useEffect(()=>{ if(genLogRef.current) genLogRef.current.scrollTop=genLogRef.current.scrollHeight; },[genLog]); - return
+ return
{tab==="mu" &&
{grouped.map(g=>
{g.parentMenu}
{g.items.map((item:any)=>)}
)}
} {tab==="cube" &&
@@ -1766,7 +1774,7 @@ function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,on
无
不使用样式 {pageStyles.map(s=>)}
@@ -1776,9 +1784,13 @@ function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,on updatePageField(i,{description:e.target.value})}/> {sourceBadge(p.source)} {p.gen_filename? : 未生成} - + )}
-
+
+ + + +
{(genIds.size>0||genLog)&&
AI 生成页面
{genLog||"(等待生成…)"}
}
@@ -1892,7 +1904,7 @@ function PageStyleLibrary({pageStyles, loadPageStyles, notify}:{pageStyles:PageS ?

暂无页面样式,点击右上角「+ 新增样式」上传 JSON 与预览图即可创建。

:
{pageStyles.map(s=>
setPreview(s)} title="点击查看大图"> - {s.name} + {s.name} ⛶

{s.name}

{s.created_at||""}
@@ -1918,7 +1930,7 @@ function StyleEditorModal({id,notify,afterChange,onClose}:{id?:number;notify:(m: const [name,setName]=useState(""); const [jsonText,setJsonText]=useState(""); const [image,setImage]=useState<{filename:string;data:string}|null>(null); - const [imageUrl,setImageUrl]=useState(id?`/api/db/page-styles/${id}/image`:null); + const [imageUrl,setImageUrl]=useState(id?`/api/db/page-styles/${id}/image?w=640`:null); const [saving,setSaving]=useState(false); const readText=(file:File)=>new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(String(r.result)); r.onerror=()=>rej(r.error); r.readAsText(file); }); const readImage=(file:File)=>new Promise<{filename:string;data:string}>((res,rej)=>{ const r=new FileReader(); r.onload=()=>res({filename:file.name, data:String(r.result).split(",")[1]}); r.onerror=()=>rej(r.error); r.readAsDataURL(file); }); diff --git a/db-sidecar.mjs b/db-sidecar.mjs index 292f8f8..b988b02 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -4,7 +4,7 @@ import http from "node:http"; import { spawn } from "node:child_process"; import { DatabaseSync } from "node:sqlite"; -import { writeFile, readFile, rm, readdir } from "node:fs/promises"; +import { writeFile, readFile, rm, readdir, stat } from "node:fs/promises"; import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join, basename, extname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -12,6 +12,7 @@ import { randomUUID } from "node:crypto"; import mammoth from "mammoth"; import * as XLSX from "xlsx"; import { PDFParse } from "pdf-parse"; +import sharp from "sharp"; const PORT = 37823; const PROJECT_DIR = dirname(fileURLToPath(import.meta.url)); @@ -1661,6 +1662,39 @@ function imgContentType(p) { : "application/octet-stream"; } +// 生成/读取缩略图:原图较大(常 >1MB)而前端仅以小图展示,按宽度生成 WebP 缩略图并落盘缓存, +// 后续请求直接返回缓存文件,显著降低首次加载耗时。sharp 失败则回退到原图。 +const _thumbBusy = new Map(); +async function serveImageThumb(res, imagePath, width) { + const w = Math.max(40, Math.min(2000, Number(width) || 0)); + if (!w) return false; + const cachePath = `${imagePath}.thumb-${w}.webp`; + try { + if (existsSync(cachePath) && existsSync(imagePath) && (await stat(cachePath)).mtimeMs >= (await stat(imagePath)).mtimeMs) { + const buf = await readFile(cachePath); + res.writeHead(200, { "Content-Type": "image/webp", "Content-Length": buf.length, "Cache-Control": "public, max-age=3600" }); + res.end(buf); + return true; + } + // 防止同一原图并发重复生成 + if (_thumbBusy.has(cachePath)) { await _thumbBusy.get(cachePath); } + else { + const p = (async () => { + const out = await sharp(imagePath).resize({ width: w, withoutEnlargement: true }).webp({ quality: 78 }).toBuffer(); + await writeFile(cachePath, out); + })(); + _thumbBusy.set(cachePath, p); + try { await p; } finally { _thumbBusy.delete(cachePath); } + } + const buf = await readFile(cachePath); + res.writeHead(200, { "Content-Type": "image/webp", "Content-Length": buf.length, "Cache-Control": "public, max-age=3600" }); + res.end(buf); + return true; + } catch (e) { + return false; // 回退到原图 + } +} + async function seedPageStyles() { const cnt = db.prepare("SELECT COUNT(*) AS c FROM page_styles").get().c; if (cnt > 0) return; @@ -1731,17 +1765,21 @@ async function handleDeletePageStyle(req, res, id) { } catch (e) { respondError(res, 500, e.message); } } -async function handlePageStyleImage(res, id) { +async function handlePageStyleImage(res, id, query) { const row = db.prepare("SELECT image_path FROM page_styles WHERE id=?").get(id); if (!row || !row.image_path || !existsSync(row.image_path)) return respondError(res, 404, "图片不存在"); + const w = query ? Number(query.get("w")) : 0; + if (w && await serveImageThumb(res, row.image_path, w)) return; const buf = await readFile(row.image_path); res.writeHead(200, { "Content-Type": imgContentType(row.image_path), "Content-Length": buf.length, "Cache-Control": "public, max-age=3600" }); res.end(buf); } -async function handleDeviceTemplateImage(res, id) { +async function handleDeviceTemplateImage(res, id, query) { const row = db.prepare("SELECT image_path FROM device_templates WHERE id=?").get(id); if (!row || !row.image_path || !existsSync(row.image_path)) return respondError(res, 404, "图片不存在"); + const w = query ? Number(query.get("w")) : 0; + if (w && await serveImageThumb(res, row.image_path, w)) return; const buf = await readFile(row.image_path); res.writeHead(200, { "Content-Type": imgContentType(row.image_path), "Content-Length": buf.length, "Cache-Control": "public, max-age=3600" }); res.end(buf); @@ -2549,8 +2587,8 @@ async function route(req, res) { if (req.method === "GET") return handleFileDownload(res, fileId); } // 页面样式库 - if (mPageStylesImage) return handlePageStyleImage(res, Number(mPageStylesImage[1])); - if (mDevImage) return handleDeviceTemplateImage(res, Number(mDevImage[1])); + if (mPageStylesImage) return handlePageStyleImage(res, Number(mPageStylesImage[1]), parsed.searchParams); + if (mDevImage) return handleDeviceTemplateImage(res, Number(mDevImage[1]), parsed.searchParams); if (mPageStyles) { if (req.method === "GET") return respondJson(res, 200, db.prepare("SELECT id,name,created_at FROM page_styles ORDER BY id").all()); if (req.method === "POST") return handleCreatePageStyle(req, res); diff --git a/public/design-doc.html b/public/design-doc.html index 3683ed6..b6b3647 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -111,7 +111,7 @@
-

文档版本:v0.28 最后更新:2026-08-21 修改人:wang.yu 状态:开发中(演示数据已标注)

+

文档版本:v0.29 最后更新:2026-08-21 修改人:wang.yu 状态:开发中(演示数据已标注)

StationBuilder 产品与交互设计方案

1. 文档说明

@@ -349,16 +349,16 @@ - - - + + +
元素功能数据来源交互效果
MU 标准页面(树状复选)树状上下结构列出标准页面,仅保留勾选框GET /api/db/mu-tree默认全选中(site.mu 为空时按全部叶子键显示,首次取消才落地保存);勾选即保存(PUT)
Cube 扩展页面表格(名称/内容/生成文件/操作)定义并管理扩展页面GET/POST/PUT/DELETE /api/db/sites/:id/cubes行内编辑、生成、保存、删除、下载;可添加页面
「生成」单页按钮单独生成某个 CubePOST /api/db/sites/:id/cubes/:cid/generate触发生成(该路径为非流式,无实时日志) 与生成确认页不同
站点页面样式选择器为全站 Cube 套用视觉样式GET /api/db/page-styles;PUT /api/db/sites选择样书后应用到站点(写入 style_id),生成时参考其主题
领域扩展页面表格(名称/内容/生成文件/操作,原「Cube 扩展页面」)定义并管理扩展页面(现为「功能与菜单」首个页签,置于 MU 标准页面之前)GET/POST/PUT/DELETE /api/db/sites/:id/cubes行内编辑、保存、删除、下载;可添加页面;每行「AI生成」按钮(醒目渐变样式)单独生成该页,生成中显示「AI生成中」并禁用
「AI生成」单页按钮单独生成某个 Cube(醒目渐变按钮)POST /api/db/sites/:id/cubes/:cid/generate触发生成(该路径为非流式,无实时日志);按钮文案由「生成」升级为「AI生成」,生成中显示「AI生成中」 与生成确认页不同
站点页面样式选择器为全站 Cube 套用视觉样式GET /api/db/page-styles;PUT /api/db/sites选择样书后应用到站点(写入 style_id),生成时参考其主题;缩略图经后端 ?w= 参数由 sharp 生成 WebP 缩略图(原图约 1.4MB → 缩略图约 9KB,首屏加载提速约 150 倍)
Cube「AI 智能补全」面板基于站点名称 / 领域 / 描述 / 补充要求 + 设备资产层级,由 AI 推断并补充 Cube 扩展页面(页面名称 + 详细内容)POST /api/db/sites/:id/complete-cubes(流式 NDJSON);GET .../complete-cubes-prompt 取提示词「开始补全」后控制台流式输出思考过程,结束将 AI 页面追加到用户已建页面下方(不改动用户内容)并自动保存;「查看提示词」可查看完整组合提示词
菜单结构(只读默认预览)展示默认菜单结构本地计算(MU + Cube)MU 标准页面在上;所有 Cube 页面归入「综合监控」一级菜单,二级为各 Cube 页面名称;本页仅为展示,无编辑/保存 默认结构

4.5.1 交互细节

功能与菜单
功能与菜单:MU 页面选择、Cube 扩展页面编辑、页面样式选择、菜单结构预览。
-

三个页签:MU 标准页面 / Cube 扩展页面 / 菜单结构。

+

三个页签(顺序已调整):领域扩展页面(原「Cube 扩展页面」,现置首)/ MU 标准页面 / 菜单结构。

① 字段 / 表格逐列

@@ -374,7 +374,7 @@ - + @@ -391,9 +391,9 @@
  • 下一步:生成确认(generate)。(generate 之后无 UI 入口进入站点结果/页面预览,见 4.7 / 4.8)
  • -

    4.5.2 AI 智能补全(功能与菜单 · Cube 扩展页面)

    +

    4.5.2 AI 智能补全(功能与菜单 · 领域扩展页面)

    AI 智能补全·Cube
    AI Cube 页面补全进行中:基于站点信息与设备资产层级,实时推断并补充 Cube 扩展页面。
    -

    在「Cube 扩展页面」页签中点击「开始补全」,调用 POST /api/db/sites/:id/complete-cubes(流式 NDJSON)。终端实时输出 AI 对站点所需 Cube 扩展页面的推断(页面名称 + 详细内容);结果追加在用户已建页面下方并自动保存。点击「查看提示词」可查看完整组合提示词。

    +

    在「领域扩展页面」(原「Cube 扩展页面」)页签中点击「开始补全」,调用 POST /api/db/sites/:id/complete-cubes(流式 NDJSON)。终端实时输出 AI 对站点所需 Cube 扩展页面的推断(页面名称 + 详细内容);结果追加在用户已建页面下方并自动保存。点击「查看提示词」可查看完整组合提示词。

    4.6 生成确认(generate)

    区域 / 列数据来源是否可编辑编辑类型上线范围
    按钮点击效果编辑方式确定 / 取消 / 关闭
    MU 复选框勾选/取消 → saveMu PUT /sites/:id/mu—即时保存(失败提示)
    Cube「预览」notify("预览 X")—占位(真实预览尚未实现)
    Cube「生成」generateCube POST /cubes/:id/generate(单页,非流式,无实时日志)—生成后刷新该页状态
    Cube「AI生成」generateCube POST /cubes/:id/generate(单页,非流式,无实时日志;按钮文案由「生成」升级为「AI生成」,生成中显示「AI生成中」)—生成后刷新该页状态
    Cube「保存」saveCube PUT/POST /cubes—已保存提示
    Cube「删除」removeCube DELETE(有 id 才删后端)—即时移除
    + 添加 Cube 页面追加空白 Cube—无弹窗
    @@ -549,7 +549,7 @@ - + @@ -562,7 +562,7 @@ - + @@ -597,7 +597,7 @@

    ① 字段 / 表格逐列

    模板信息编辑所属领域 / 名称 / 版本 / 状态 / 创建人 / 描述GET /api/db/domain-templates/:id 详情;PUT /api/db/domain-templates/:id 保存「保存模板信息」写回并重新加载
    选用设备模版(DeviceTemplatePicker)从「设备模版」页中勾选本方案要用的设备模版详情 devices[](已选用的设备模版 id 列表);GET /api/db/device-templates(全部可选);PUT /api/db/domain-templates/:id(传 device_template_ids,服务端重写 device_template_domain_templates 关联)勾选即保存:PUT 携带当前全部选中 id,服务端先 DELETE 旧关联再按 id 重建;未找到合适模版可点「+ 新增设备模版」跳到 deviceTemplates 页新建。设备模版的编辑(名称 / 说明 / 图片 / 采集点)已移至独立的「设备模版」页,不再在本面板内联编辑;设备不再维护「所属领域」多选(改为由领域 / 方案侧选用设备)
    采集点设备下的采集点(名称 / 说明 / 单位 / 数值范围 / 描述)POST /api/db/device-templates/:id/collection-points(名称);DELETE /api/db/collection-points/:id「+ 采集点」填名称新增;行内「删」移除
    领域页面(Cube 扩展页面)编辑领域页面(=StationBuilder 的 Cube 扩展页面)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT / DELETE /api/db/domain-pages/:id;POST /api/db/domain-templates/:id/pages/:pid/generate、/refine(流式)「功能与菜单 → Cube 扩展页面」表格逐行管理,列:页面名称(≤6 字)/ 页面描述 / 来源 / Json文件 / 操作;来源以徽标区分「↑ 上传 / ✦ AI生成 / 手动」;Json文件列对已上传或已生成的页面提供「下载 文件名」按钮(上传=原始 .json 文件本身,AI生成=生成的 Cube .json),未生成显示"未生成";支持行内编辑名称/描述、预览/生成/保存/删除、+ 添加页面、↑ 上传页面 Json;保存前校验 JSON 合法性;生成确认页对每页显示「AI已生成 / 已上传 / 未生成 · 来源」
    领域页面(领域扩展页面,原「Cube 扩展页面」)编辑领域页面(=StationBuilder 的 Cube 扩展页面)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT / DELETE /api/db/domain-pages/:id;POST /api/db/domain-templates/:id/pages/:pid/generate、/refine(流式)「功能与菜单 → 领域扩展页面(现为首个页签)」表格逐行管理,列:页面名称(≤6 字)/ 页面描述 / 来源 / Json文件 / 操作;来源以徽标区分「↑ 上传 / ✦ AI生成 / 手动」;Json文件列对已上传或已生成的页面提供「下载 文件名」按钮,未生成显示"未生成";每行「AI生成」按钮(醒目渐变,生成中显示「AI生成中」)单独生成该页;表格上方另有「⚡ AI 统一生成全部(N)」按钮,一键对所有「未生成」页面排队生成(每页按后端 AI 并发上限 2 自动排队,各自按钮同步显示「AI生成中」),免去逐个点击;支持行内编辑名称/描述、预览/保存/删除、+ 添加页面、↑ 上传页面 Json;保存前校验 JSON 合法性;生成确认页对每页显示「AI已生成 / 已上传 / 未生成 · 来源」
    模板菜单编辑模板菜单(=StationBuilder 的 MU + Cube 菜单保存结构)详情 menu[];POST /api/db/domain-templates/:id/menu;DELETE /api/db/template-menu-items/:id表格展示菜单项 / 类型 / 父级 / 关联;「+ 新增菜单项」建占位项
    知识文档管理知识文档(名称 + 本体)详情 docs[];POST /api/db/domain-templates/:id/docs(doc_type=link 传 url,或 doc_type=file 传 base64 文件);GET /api/db/knowledge-docs/:id/file 下载;DELETE /api/db/knowledge-docs/:id新增时选择「超链」(填 URL) 或「文件」(上传,落盘 data/db/uploads/docs/);列表可下载 / 删除
    运维流程(OpsFlowsViewer)查看运维流程(流程名称 + 内容 JSON 串)GET /api/ops-flows仅展示占位记录,本功能 尚未实现
    面板 / 区域数据来源 / 接口是否可编辑编辑类型上线范围
    模板信息GET/PUT /api/db/domain-templates/:id可编辑所属领域(select) / 模板名称 / 版本 / 状态(select) / 创建人 / 领域描述(textarea)方案元信息,被站点方案引用
    选用设备模版(DeviceTemplatePicker)详情 devices[];GET /api/db/device-templates;PUT /api/db/domain-templates/:id(device_template_ids,服务端重写关联)可编辑(勾选即保存)复选框列表该方案选用的设备模版集合(供站点方案引用)
    领域页面(Cube 扩展页面)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT/DELETE /api/db/domain-pages/:id;POST .../:pid/generate、/refine可编辑表格逐列:页面名称(≤6字)/ 页面描述 / 来源(徽标:上传·AI生成·手动)/ Json文件(已上传或已生成→「下载 文件名」;否则"未生成")/ 操作(预览·生成·保存·删除)方案内置页面(=StationBuilder 的 Cube);source 字段区分来源,gen_filename 为下载文件名
    领域页面(领域扩展页面,原「Cube 扩展页面」)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT/DELETE /api/db/domain-pages/:id;POST .../:pid/generate、/refine可编辑表格逐列:页面名称(≤6字)/ 页面描述 / 来源(徽标:上传·AI生成·手动)/ Json文件(已上传或已生成→「下载 文件名」;否则"未生成")/ 操作(预览·AI生成·保存·删除);另含「⚡ AI 统一生成全部(N)」一键排队生成全部未生成页面方案内置页面(=StationBuilder 的 Cube);source 字段区分来源,gen_filename 为下载文件名
    模板菜单详情 menu[];POST /api/db/domain-templates/:id/menu;DELETE /api/db/template-menu-items/:id可编辑(新增占位项)表格(菜单项/类型/父级/关联)+ 删除方案菜单结构
    知识文档详情 docs[];POST /api/db/domain-templates/:id/docs;GET /api/db/knowledge-docs/:id/file;DELETE可编辑新增选「超链」(填 URL) 或「文件」(上传 base64);列表可下载/删方案知识库
    运维流程(OpsFlowsViewer)GET /api/ops-flows只读占位展示尚未实现
    - + @@ -759,6 +759,7 @@

    10.1 版本变更记录(Changelog)

    区域 / 列数据来源是否可编辑编辑类型上线范围
    样式卡片网格(缩略图 / 名称 / 创建时间 / 编辑·删除)GET /api/db/page-styles卡片只读;编辑/删除可触发缩略图 = GET /page-styles/:id/image;3 列网格样式浏览与维护
    样式卡片网格(缩略图 / 名称 / 创建时间 / 编辑·删除)GET /api/db/page-styles卡片只读;编辑/删除可触发缩略图 = GET /page-styles/:id/image?w=560(后端 sharp 生成 WebP 缩略图,首屏 5 张由约 7MB 降至约 45KB,加载提速约 150 倍);3 列网格样式浏览与维护
    StyleEditorModal · 样式名称(必填)POST/PUT /api/db/page-styles可编辑文本输入样式库展示名
    StyleEditorModal · 预览图(新建必填)同上(image base64)可编辑图片上传(建议 16:10 PNG/JPG)样式缩略图与大图预览源
    StyleEditorModal · 仪表盘 JSON(必填,需合法 JSON)同上(jsonText)可编辑textarea 粘贴 或 上传 .json 文件Cube 页面视觉主题(生成时参考)
    +
    版本日期修改人说明
    v0.292026-08-21wang.yu领域方案库 / 站点「功能与菜单 → Cube 扩展页面」遗留问题修复与体验提升:① 页签改名与排序:站点与领域两处「Cube 扩展页面」页签统一更名为「领域扩展页面」,并调整顺序置于「MU 标准页面」之前(默认进入该页签);文档同步。② 生成按钮醒目化:两处扩展页面表格的「生成」按钮文案升级为「AI生成」、生成中显示「AI生成中」,并采用醒目渐变蓝紫样式(.btn-ai,覆盖 .cube-actions button 白底默认态)。③ 统一生成:领域方案「领域扩展页面」表格上方新增「⚡ AI 统一生成全部(N)」按钮(N = 未生成页面数),一键对所有「未生成」且未在生成中的页面并发触发 generatePage;后端按每页独立锁 + AI_CONCURRENCY=2 信号量排队,各自页面按钮同步显示「AI生成中」,免去逐个点击(仅保留单页生成)。④ 图片加载提速:后端 db-sidecar 的 handlePageStyleImage / handleDeviceTemplateImage 新增 ?w= 宽度参数,经 sharp 生成 WebP 缩略图并落盘缓存(mtime 校验失效重生成),原图约 1.4MB → 缩略图约 9KB(提速约 150 倍);前端样式选择器(?systemctl restart stationbuilder-db 生效(新增 sharp 依赖,Node 原生模块已就绪)。Playwright E2E:领域方案进入「领域扩展页面」页签确认置首、10 个页面按钮显示「AI生成」、统一按钮计数正确;新增待生成页面后点「AI 统一生成全部」其按钮即时翻为「AI生成中」、其余已生成页保持「AI生成」,0 控制台报错;页面样式库 5 张缩略图均带 ?w=、缩略图接口实测 1355KB→8.9KB。
    v0.282026-08-21wang.yu设备模板「采集点」AI 智能补全(两处入口):① 领域方案「AI 资产补全」自动创建设备模板后,后端立即以设备名称为其生成 6~12 个合适采集点并写库(resolveDeviceTemplate 返回 created:true 时触发;追加模式,单个失败不影响整体,推导过程回流到资产补全控制台;已存在模板不重复补全)。② 设备模板详情(DeviceTemplateCard)新增「AI 智能补全 · 采集点」面板:「开始补全」调用新增接口 POST /api/db/device-templates/:id/complete-collection-points(流式 NDJSON,codebuddy 生成 + 写库,前端补全后重新拉取详情刷新表格),「查看提示词」GET /api/db/device-templates/:id/complete-collection-points-prompt 弹窗展示完整组合提示词。后端新增 buildCompleteCollectionPointsPrompt / extractCollectionPoints / generateDeviceTemplatePoints(按 name 去重追加,不覆盖用户已录入采集点;数值范围优先取预设集合);前端新增 DeviceTemplateCard 的 cpAi=useStreamComplete() 流式面板,并透传 onViewCpPrompt 复用 App 级提示词弹窗;资产补全前端超时守卫由 6 分钟放宽到 10 分钟(因额外补全采集点)。Playwright E2E:临时设备点「开始补全」→ 实际运行 codebuddy 生成 12 个采集点并持久化(GET 详情 collection_points.length=12),控制台显示「✓ 已补全 12 个采集点」,0 控制台报错,临时设备已清理。设计文档同步更新 4.12 / 新增 4.12.2、版本 v0.28。
    v0.272026-08-21wang.yu领域编辑迁移至左侧导航栏:① 移除「领域方案库」首页的「管理领域」面板,首页方案列表相应上移(直接进入列表,不再被管理面板挤占首屏)。② 领域增删改入口迁至左侧「领域方案库模块 → 领域分类」分组,UI 与现有领域分类导航项一致:每个领域行尾加 × 删除按钮(仅非「其他」领域,点击经 window.confirm 二次确认后 DELETE /api/domains/:id,其下方案归入「其他」,随后 reloadDomains 刷新导航);行末新增「+ 新增领域」按钮,点击展开行内输入框(最多 6 字,Enter 确认 / Esc 取消),POST /api/domains 后刷新导航。③ 修复 addDomain 原只更新前端 domains 字符串数组、未刷新导航用 domainOptions 导致新增后导航不出现的问题——现改由 reloadDomains 统一刷新两个 state。④ globals.css 新增 .domain-nav-row / .domain-nav-del / .domain-nav-ok / .domain-nav-add / .domain-nav-input 等样式,保持与导航按钮一致的 41px 行高与激活态。Playwright 验证:面板已移除、导航显示 6 领域行(5 个 × + 1 个 +)、新增/删除临时领域均生效、「其他」无删除按钮、0 控制台报错。后续微调:领域名按钮与 ×/+ 按钮原共用 `.domain-nav-row>button` 的 `flex:1` 导致 × 也等分给到约 95px、过滤按钮被压窄至 95px、4 字以内领域名在用户浏览器中换行;改为 `flex:1` 仅作用于过滤按钮(`:not(.domain-nav-del):not(.domain-nav-ok)`),×/✓ 固定 30px,领域名加 `white-space:nowrap` 并包入 `.domain-nav-name` 超出省略号,4 字以内领域名稳定单行显示。
    v0.262026-08-21wang.yu导出修复(设计方案 Word / PDF):① PDF 由 html2pdf(jsPDF+html2canvas,中文乱码且长文档卡死)改为调用浏览器原生打印(window.print),输出真实文字、微软雅黑、纯黑、1.5 倍行距,无乱码;打印时临时注入静态目录(buildStaticToc,含 h2/h3/h4)于文首,打印后移除、不污染屏幕 UI;屏幕 UI 截图尺寸不变(仍 360px)。② Word 页脚页码修复:footer 元素补 id 并改用规范 Word 域标记(PAGE / NUMPAGES 字段,含 field-begin/separator/end),由「仅首页顶部显示 1」修正为每页底部「第 X 页 / 共 Y 页」。③ Word 目录由 TOC 域(打开时常为空)改为默认插入可见的静态目录(buildStaticToc,含 h2/h3/h4 三级)。④ 截图限宽 300px(等比例):Word 设 width=300 属性 + 行内样式;PDF 原 @media print 用 `.figure img` 类选择器未命中正文 `
    ` 元素导致约束不生效、图片按原始像素溢出页面(即此前「240 显示不下」的真相),改为 `figure img` 元素选择器 + max-width:300px 后正常限宽并完整显示;图后插入回车(<br>)使图注文字换行到下一行。⑤ 补充 @page 打印页码(A4 页脚居中「第 X 页 / 共 Y 页」;Chrome 另存为 PDF 若未显示,在打印对话框勾选「页眉和页脚」兜底)。