From: wang.yu Date: Thu, 20 Aug 2026 04:13:20 +0000 (+0800) Subject: fix(domain): Cube 生成按钮流式化 + AI补全写入列表防静默丢失 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=166d6a386c4c903ed47dc87a700a38a5126c1810;p=StationBuilder.git fix(domain): Cube 生成按钮流式化 + AI补全写入列表防静默丢失 - generatePage 改为流式消费 NDJSON 响应体(保持连接),生成期间按钮显示「生成中…」并禁用、底部新增「AI 生成页面」控制台实时输出进度,并据服务端 {status:failed} 弹失败提示;修复此前普通 fetch 不读响应体导致连接提前关闭、服务端 AI 生成中断(status=failed)、Json 未写入的问题。 - runCubesComplete 由逐条静默 .catch 插入改为收集结果后逐个 await 插入、失败显式 notify,并捕获服务端 {status:failed} 事件弹失败提示,避免补全内容静默丢失。 - design-doc 同步 v0.19 变更记录。 --- diff --git a/app/page.tsx b/app/page.tsx index 901ac7e..5591796 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1426,6 +1426,8 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA const basicAi=useStreamComplete(); const assetsAi=useStreamComplete(); const cubesAi=useStreamComplete(); + const [genIds,setGenIds]=useState>(new Set()); + const [genLog,setGenLog]=useState(""); const {confirm, el:confirmEl}=useConfirm(); const domainName = doms.find((d:any)=>d.id===m?.domain_id)?.name || ""; @@ -1496,12 +1498,16 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA if(cubesAi.completing) return; cubesAi.setCompleting(true); cubesAi.setLog(""); const wd=setTimeout(()=>{ cubesAi.setCompleting(false); notify("AI Cube 补全超时(>6 分钟),请重试"); },360000); + let failed=""; let items:any[]=[]; try{ const res=await fetch(`/api/db/domain-templates/${id}/complete-cubes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:m.name,domain:domainName,description:m.description,supplement:m.supplement,assets:assets.map(a=>({type:a.device_name,count:a.count,rule:a.rule}))})}); if(!res.ok||!res.body) throw new Error("HTTP "+res.status); - await streamInto(res, cubesAi.setLog, (ev)=>{ if(Array.isArray(ev.result)){ for(const it of ev.result) fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:it.name,description:it.content||"",json_text:"{}"})}).catch(()=>{}); } }); + await streamInto(res, cubesAi.setLog, (ev)=>{ if(Array.isArray(ev.result)) items=ev.result; if(ev?.status==="failed") failed=ev?.message||"AI 分析失败"; }); + if(failed){ notify("AI Cube 补全失败:"+failed); return; } + let added=0; + for(const it of items){ try{ await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:it.name,description:it.content||"",json_text:"{}"})}); added++; }catch(e:any){ notify(`新增页面「${it.name||""}」失败:${e.message}`); } } await load(true); - notify("AI Cube 页面补全已完成"); + notify(items.length? `AI Cube 页面补全已完成,已新增 ${added} 个页面` : "AI 未返回可用页面"); }catch(e:any){ notify("AI Cube 补全失败:"+(e?.message||e)); } finally{ clearTimeout(wd); cubesAi.setCompleting(false); } }; @@ -1532,7 +1538,20 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA }; const savePageMeta=async(p:DomainPage)=>{ await fetchDb(`/domain-pages/${p.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:p.name,description:p.description,json_text:p.json_text})}); notify("页面已保存"); }; const deletePage=async(p:DomainPage)=>{ if(!(await confirm(`删除页面「${p.name}」?`))) return; await fetchDb(`/domain-pages/${p.id}`,{method:"DELETE"}); await load(true); }; - const generatePage=async(p:DomainPage)=>{ notify(`开始生成「${p.name}」…`); try{ await fetch(`/api/db/domain-templates/${id}/pages/${p.id}/generate`,{method:"POST"}); await load(true); notify(`「${p.name}」生成完成`); }catch(e:any){ notify("生成失败:"+e.message); await load(true); } }; + const generatePage=async(p:DomainPage)=>{ + if(genIds.has(p.id)) return; + setGenIds(prev=>new Set(prev).add(p.id)); setGenLog(""); + notify(`开始生成「${p.name}」…`); + let failed=""; + try{ + const res=await fetch(`/api/db/domain-templates/${id}/pages/${p.id}/generate`,{method:"POST"}); + if(!res.ok||!res.body) throw new Error("HTTP "+res.status); + await streamInto(res, setGenLog, (ev)=>{ if(ev?.status==="failed") failed=ev?.message||"生成失败"; }); + if(failed) notify(`「${p.name}」生成失败:${failed}`); + else notify(`「${p.name}」生成完成`); + }catch(e:any){ notify("生成失败:"+(e?.message||e)); } + finally{ setGenIds(prev=>{ const n=new Set(prev); n.delete(p.id); return n; }); await load(true); } + }; 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

加载中…

; @@ -1611,6 +1630,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))} tName={m.name} saving={saving} notify={notify} />} @@ -1628,13 +1648,15 @@ function PlaceholderStep({title,note}:{title:string;note:string}){ return
待开发

{note}

; } -function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify}:{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;cubesAi:ReturnType;onRunCubesComplete:()=>Promise;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial)=>void;tName:string;saving:boolean;notify:(m:string)=>void}){ +function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,genIds,genLog,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify}:{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}){ const sourceBadge=(src?:string|null)=>{ if(src==="upload") return ↑ 上传; if(src==="ai") return ✦ AI生成; return 手动; }; const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu"); + const genLogRef=useRef(null); + useEffect(()=>{ if(genLogRef.current) genLogRef.current.scrollTop=genLogRef.current.scrollHeight; },[genLog]); return
{tab==="mu" &&
{grouped.map(g=>
{g.parentMenu}
{g.items.map((item:any)=>)}
)}
} {tab==="cube" &&
@@ -1644,9 +1666,10 @@ 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||"(等待生成…)"}
}
  • 从方案业务与设备资产出发,规划监控维度(综合总览、能耗能效、设备健康、报警事件、环境监测等)。
  • 页面名称简洁,不超过 6 个汉字;描述写清页面目标与关键指标。
  • 每个 Cube 页面可「生成」(AI 创建 JSON)或「上传页面 Json」(使用已有 JSON)。
  • 已生成页面可点击「预览」查看效果,并在预览中提交修改意见重新生成。
diff --git a/public/design-doc.html b/public/design-doc.html index 3687f02..5c4ac31 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -115,7 +115,7 @@
-

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

+

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

StationBuilder 产品与交互设计方案

1. 文档说明

@@ -751,6 +751,7 @@

11.1 版本变更记录(Changelog)

+
版本日期修改人说明
v0.192026-08-20wang.yu修复领域方案「功能与菜单 → Cube 扩展页面」两处问题:①「生成」按钮改为流式请求(消费 NDJSON 响应体以保持连接),生成期间按钮显示「生成中…」并禁用、底部「AI 生成页面」控制台实时输出进度并据服务端 {status:failed} 事件弹失败提示;此前前端用普通 fetch 不读取响应体,连接被提前关闭导致服务端 AI 生成中断(status='failed')、Json 未写入。② AI 智能补全(complete-cubes)由「逐条静默 .catch(()=>{}) 插入」改为「收集结果后逐个 await 插入、失败显式 notify」,并捕获服务端 {status:failed} 事件弹失败提示,避免补全内容静默丢失、列表无变化。
v0.172026-08-19wang.yu修复左侧目录(TOC)点击章节后消失:根因为顶部版本说明段落含全角空格不换行,在 flex 布局下把 main#doc 撑到约 2238px 宽(flex item 默认 min-width:auto 不收缩),点击锚点时浏览器水平滚动将 sticky 目录推出左边界。给 main#doc 增加 min-width:0 使其可收缩、内容正常折行,消除水平滚动,目录任意宽度常驻可见。
v0.182026-08-20wang.yu领域方案「功能与菜单 → Cube 扩展页面」表格增强:① 新增「来源」列,以徽标区分 上传 / AI生成 / 手动;② 原「生成文件」列更名为「Json文件」,对上传页面提供原始 .json 文件名下载、对 AI 生成页面提供生成的 Cube .json 下载(未生成显示"未生成");③ domain_pages 新增 source 字段,上传/生成时分别写入 source=upload/ai 与 gen_filename;④ 生成确认页状态徽标区分「AI已生成 / 已上传 / 未生成 · 来源」。同步更新 5.10 章节。
v0.162026-08-19wang.yu设计方案文档修订:① 左侧目录改为任意宽度常驻可见(移除窄屏 display:none,窄屏下列表转为顶部可滚动常驻栏),点击章节后目录不再消失;② 移除「内部登录」章节(5.1)及 5.13 登录截图,可达性说明去掉登录;③ 新增 AI 智能补全子章(5.3.2 / 5.4.2 / 5.5.2),附 AI 流式输出终端截图(ai-complete-basic/assets/functions.png);④ 修复 PDF 导出图片被分页截断(@media print 增加 page-break-inside:avoid 与图片适配);⑤ 修复 Word 导出无图片(downloadWord 将图片转为 base64 data URI 内嵌)。