From d38e5c276b7316daeeb80e060b52493694d151a9 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Aug 2026 18:26:48 +0800 Subject: [PATCH] =?utf8?q?feat(=E9=A2=86=E5=9F=9F=E6=96=B9=E6=A1=88?= =?utf8?q?=E5=BA=93):=20=E9=A2=86=E5=9F=9F=E7=9F=A5=E8=AF=86=E6=8F=90?= =?utf8?q?=E5=8D=87=E4=B8=BA=20Step2=20=E5=B9=B6=E4=BD=9C=E4=B8=BA=20AI=20?= =?utf8?q?=E8=A1=A5=E5=85=A8=E4=BC=98=E5=85=88=E5=8F=82=E8=80=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 方案向导步骤重排:领域知识移至「方案基本属性」之后(Step2),资产/功能菜单/运维流程/生成保存/专家确认整体后移 - 新增 KnowledgeStep:上传知识文档(多文件 base64 落库)、配置参考 URL 站点,列表可下载/删除,随方案持久化 - 新增 buildKnowledgePrompt:读取 knowledge_docs,文本文档直接注入正文、URL 站点服务端抓取正文注入(8s 超时,失败降级为参考链接),注入 buildCompletePrompt/buildCompleteAssetsPrompt/buildCompleteCubesPrompt(三构建器改 async,领域级补全 ctx 加 id;站点级无 id 不注入) - 生成保存(DomainGenerateReview)展示真实领域知识信息并提供「返回领域知识」入口 - 设计文档更新至 v0.22 运维流程 AI 补全本期仍占位(见设计文档) --- app/page.tsx | 60 ++++++++++++++++++--- data/db/StationBuilder.sqlite | Bin 9064448 -> 9121792 bytes db-sidecar.mjs | 95 +++++++++++++++++++++++++--------- public/design-doc.html | 3 +- 4 files changed, 127 insertions(+), 31 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index a2c7c09..e1aee99 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1349,10 +1349,10 @@ function DomainDetail({id,go,notify,onBack,onCreated,viewCompletePrompt,viewComp const DOMAIN_STEPS:{key:DomainStepKey;label:string}[]=[ {key:"basic",label:"方案基本属性"}, + {key:"knowledge",label:"领域知识"}, {key:"assets",label:"资产设置"}, {key:"functions",label:"功能与菜单"}, {key:"opsflow",label:"运维流程"}, - {key:"knowledge",label:"领域知识"}, {key:"generate",label:"生成保存"}, {key:"expert",label:"专家确认"}, ]; @@ -1471,7 +1471,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA const [d, mt]=await Promise.all([ fetchDb(`/domain-templates/${id}`) as any, fetchDb(`/mu-tree`) as Promise ]); setMuTreeLocal(mt); setT(d); - setM({domain_id:d.domain_id,name:d.name,description:d.description||"",status:d.status,version:d.version,creator:d.creator||"",supplement:d.supplement||"",style_id:d.style_id!=null?Number(d.style_id):null}); + setM({domain_id:d.domain_id,name:d.name,description:d.description||"",status:d.status,version:d.version,creator:d.creator||"",supplement:d.supplement||"",style_id:d.style_id!=null?Number(d.style_id):null,docs:d.docs||[],flows:d.flows||[]}); setAssets((d.assets||[]).map((a:any)=>({id:a.id,device_template_id:a.device_template_id,device_name:a.device_name||"",count:Number(a.count)||0,rule:a.rule||"",sort_order:a.sort_order||0}))); setPages((d.pages||[]).map((p:any)=>({id:p.id,name:p.name,description:p.description||"",json_text:p.json_text||"",status:p.status||"pending",gen_filename:p.gen_filename||null,prompt:p.prompt||null,source:p.source||null,sort_order:p.sort_order||0}))); setMu(d.mu && d.mu.length ? d.mu : []); @@ -1628,6 +1628,8 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA {saving&&
正在保存…
} } + {step==="knowledge" && load(true)} confirm={confirm}/>} + {step==="assets" &&
{[[assets.filter(a=>a.device_template_id).length,"设备类型","类"],[assets.reduce((n,a)=>n+Number(a.count||0),0),"设备实例","个"],[Math.max(2, assets.filter(a=>a.device_template_id).length+1),"资产层级","层"]].map(x=>
{x[1]}{x[0]}{x[2]}
)}{saving&&
保存保存中…
}
@@ -1675,8 +1677,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA pageStyles={pageStyles} styleId={m.style_id??null} onSelectStyle={selectStyle} />} {step==="opsflow" && } - {step==="knowledge" && } - {step==="generate" && goStep("functions")} onConfirm={()=>notify("方案已生成并保存,可返回上一步继续调整")}/>} + {step==="generate" && goStep("functions")} onKnowledge={()=>goStep("knowledge")} onConfirm={()=>notify("方案已生成并保存,可返回上一步继续调整")}/>} {step==="expert" && }
{curIdxgoStep(DOMAIN_STEPS[curIdx+1].key)}>下一步}{curIdx===DOMAIN_STEPS.length-1 && }
@@ -1751,10 +1752,19 @@ function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,on ; } -function DomainGenerateReview({tName,assets,effSelected,grouped,pages,onBack,onConfirm}:{tName:string;assets:DomainAsset[];effSelected:string[];grouped:any[];pages:DomainPage[];onBack:()=>void;onConfirm:()=>void}){ +function DomainGenerateReview({tName,assets,effSelected,grouped,pages,docs,onBack,onKnowledge,onConfirm}:{tName:string;assets:DomainAsset[];effSelected:string[];grouped:any[];pages:DomainPage[];docs:any[];onBack:()=>void;onKnowledge?:()=>void;onConfirm:()=>void}){ + const reviewRows:[string,string,string,string?,(()=>void)?][]=[ + ["方案",tName,"","返回修改",onBack], + ["资产",`${assets.filter(a=>a.device_template_id).length} 类 / ${assets.reduce((n,a)=>n+Number(a.count||0),0)} 个设备`,"3 层资产结构",""], + ["MU 页面",`${effSelected.length} 个标准页面`,"复用并配置菜单",""], + ["Cube 页面",`${pages.length} 个扩展页面`,`已生成 ${pages.filter(p=>p.status==="generated").length} 个`,""], + ["菜单结构","MU + 综合监控",`共 ${effSelected.length+pages.length} 项`,""], + ["领域知识", docs.length?`${docs.length} 份(${docs.slice(0,3).map((d:any)=>d.name).join("、")}${docs.length>3?" 等":""})`:"暂无知识", docs.length?"已沉淀领域知识,将作为 AI 补全优先参考":"可上传文档 / 配置站点", "返回领域知识", onKnowledge], + ["运维流程","标准 SOP","(本期占位,待开发)",""], + ]; return
- {[["方案",tName,"","返回修改"],["资产",`${assets.filter(a=>a.device_template_id).length} 类 / ${assets.reduce((n,a)=>n+Number(a.count||0),0)} 个设备`,"3 层资产结构",""],["MU 页面",`${effSelected.length} 个标准页面`,"复用并配置菜单",""],["Cube 页面",`${pages.length} 个扩展页面`,`已生成 ${pages.filter(p=>p.status==="generated").length} 个`,""],["菜单结构","MU + 综合监控",`共 ${effSelected.length+pages.length} 项`,""],["领域知识","沉淀标准规范","(待开发)",""],["运维流程","标准 SOP","(待开发)",""]].map(x=>
{x[0]}{x[1]}{x[2]}{x[3]&&}
)} + {reviewRows.map(r=>
{r[0]}{r[1]}{r[2]}{r[3]&&}
)}
⌄ {tName||"未命名方案"}
{assets.filter(a=>a.device_template_id).map((row,i)=>
⌄ {row.device_name}{row.count}
{row.count>0&&
{formatInstances(row.rule,row.count)}
}
)}
⌄ {tName||"未命名方案"}
MU 标准页面
{effSelected.length===0?
(未选择)
:grouped.flatMap((g:any)=>g.items).filter((l:any)=>effSelected.includes(l.key)).map((l:any)=>
· {l.parentMenu}/{l.label}
)}
综合监控
{pages.filter(p=>p.name).map(p=>
· {p.name}
)}
@@ -1770,6 +1780,44 @@ function DomainGenerateReview({tName,assets,effSelected,grouped,pages,onBack,onC +function KnowledgeStep({id,docs,notify,reload,confirm}:{id:number;docs:any[];notify:(m:string)=>void;reload:()=>void;confirm:(msg:string)=>Promise}){ + const [busy,setBusy]=useState(false); + const [urlName,setUrlName]=useState(""); + const [urlVal,setUrlVal]=useState(""); + const readBase64=(file:File)=>new Promise<{filename:string;mime:string;data:string}>((res,rej)=>{ const r=new FileReader(); r.onload=()=>res({filename:file.name,mime:file.type||"application/octet-stream",data:String(r.result).split(",")[1]}); r.onerror=()=>rej(r.error); r.readAsDataURL(file); }); + const uploadFiles=async(files:FileList|null)=>{ if(!files||!files.length) return; setBusy(true); try{ for(const f of Array.from(files)){ const b=await readBase64(f); await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({doc_type:"file",name:f.name,file:{filename:b.filename,mime:b.mime,data:b.data}})}); } notify("已上传知识文档"); reload(); }catch(e:any){ notify("上传失败:"+(e?.message||e)); } finally{ setBusy(false); } }; + const addUrl=async()=>{ const name=urlName.trim(); const url=urlVal.trim(); if(!name){ notify("请填写站点名称"); return; } if(!/^https?:\/\//i.test(url)){ notify("请输入有效的 http(s) 链接"); return; } setBusy(true); try{ await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({doc_type:"link",name,url})}); notify("已添加参考站点"); setUrlName(""); setUrlVal(""); reload(); }catch(e:any){ notify("添加失败:"+(e?.message||e)); } finally{ setBusy(false); } }; + const removeDoc=async(docId:number)=>{ if(!(await confirm("确定删除该知识来源?此操作不可恢复"))) return; try{ await fetchDb(`/knowledge-docs/${docId}`,{method:"DELETE"}); notify("已删除"); reload(); }catch(e:any){ notify("删除失败:"+(e?.message||e)); } }; + return
+ 上传中…:undefined}> +

上传与本站领域相关的文档(标准规范、设备手册、工艺说明、建设方案等)。知识文本将作为资产配置、功能菜单 AI 补全的优先参考资料,并随方案保存。支持 txt / md / json / csv / html 等文本类,其他格式将作为参考资料登记。

+ +
+ +

填写一个领域相关的参考网站地址。AI 补全时会由服务端抓取该站点正文并注入提示词(抓取失败则仅保留链接作为参考)。

+
+ setUrlName(e.target.value)}/> + setUrlVal(e.target.value)}/> +
+
+
+ 刷新}> + {docs.length===0?
暂无领域知识,上传文档或配置站点后此处展示。
: + + {docs.map((d:any)=> + + + + + + )}
名称类型大小创建时间操作
{d.name}{d.doc_type==="link"?"URL 站点":"文档"}{d.size!=null?`${Math.round(Number(d.size)/1024*10)/10} KB`:"—"}{d.created_at||""}{d.doc_type==="file"&&下载}
} +
+
; +} + function PageStyleLibrary({pageStyles, loadPageStyles, notify}:{pageStyles:PageStyle[]; loadPageStyles:()=>Promise; notify:(m:string)=>void}){ const [editor,setEditor]=useState<{open:boolean; id?:number}>({open:false}); const [preview,setPreview]=useState(null); diff --git a/data/db/StationBuilder.sqlite b/data/db/StationBuilder.sqlite index 68eeaecaa8193fb07dc094e5dbdebe5a0b8199ff..bd413b68431f65a5365877a8254424856f988c7b 100644 GIT binary patch delta 2539 zcmciCdrVu`9S3mViw%yA!9cwL;b8&^ltTOf0|vsY33)V>N1B9W5HOFVUQMvEc^a_I z0Yew1Nk{0f-IQUZrCCdwkRp26rmESdbzRjz5~NmIs%)rA*QlyGqCNh|#I~;iC1le6 z+-!a1>vPXNzu)=&?!Df?TT<`xRq2IoDuYBKp9lO(RIg^_3x*rb7fS^Bu7RZ$nF;$hU zQRPS^LUU8oOD#>VcBiYU$l6x!uup>wBstPQEu6i7 zed^2eBlo@RtJyc24EY85Mx()~&(WLp^qFob*|VO75sBm@>1iQdBXOq)Ia1{Av~<{Q z?Ymrku2WW9yY9u#j&|K)m&Il6JX6tawY9Z(>DnwVi>|Fj*KN0TTH2htw$4_k?ukoX z?R_qt-Da_#X?$|y!H{_pk3`7&pU13=_^&mId{h*Z!IWptf1)U+LW9|%!nOauq99~} zYFy9{!7zBh3qCIBM}%8>XsbjLj<<^u&}4Zom;h(w=%Bm+%uonLPz<}tmgcsO&9H@; z(YEtpLYhQs6VhetdxSYQq!fFLl-}PuFwTo`L>+IRj6CMoi6lwXJ-V|=X+>;=Z{tJ(h?^m!z z8F?GJ%vAllXNP=ymO_g}e#&o4hcyCsN3kJ+Fu#NjUGa9xT1;k3E5CWg`?f$YXjtav zr53ib8W~-h?LJ3=4_!$#m&npXDh#Ja1{kh^A^?a^n6TPQdm|nXd3c=uD6I*_Bo{7 zm&z(+kJjBv(|>UetmQD}9a7NA>HZ}8w%!-N9&h1kR{uDyK$LF_Wo5FX|B(W}oXHBs z&5>@ccx05NW4{Y#^EN%G6yM|5^{h`$_N#;cqA&wCxsu^j)A2&KjhY{N6yz;rYP#^L z?>)Me=Qh(+5z_>xMA7?2EdKEnjwz_;$ItkR5>m}MdHPm!unmts+f&!dZld-G_Lqmv{boALq@Z^pR}cHnU=GiEy1K``OH|9f7^nwu98sKHA^*@#^WjB9ak1j*Rm;EJ?u&3HFfOwQjWt1B5ggv z44XrE8kjGHsu~>`WVhhv*mY-AL*6P;PDh$qNpRtJs9ME}*3R-f&CD*Ymw&a9MXx91 zZey`jbF#0V<~qc8Y4taQPdzHSlD=w3n;tjE-XXq1bEjA&sf&8!sQHr#GbLRfTkB*L zm7hkNmMsH2$$Q8D1(n_NXlQxS6~)zm@UI9o7dM#3%T5f$ioEQIm?RCRca7hU8Jv)E zU3Q;MpofQs!$p&k-x&5)qwcHy>7@N;2)BFHa2CIC&a*0z)y>K_S73RFX+;X8`Sdgw z?U&QHpRou!=w%t9l?8kER8~@-mn{b)U8i~<`+8%P?;l}jLpIi_IWU^Z#R>M$Fdk6$ z?-qiy=Xm+6?1KniTsyWtaD$i6tPNN;2Hxf6H!wizvt5VjykqR^;AQi?{FkgsMRVcZ z&+{3Tqe%??(f#0Sl^PM^Z^~TG A(f|Me delta 814 zcmXZbNlX)A7{>8$rUkkPLxrj=S{6}Imakkgj{f>+9W_2!UuGnx3SoOE{@<(o zN+tDk*KDnlF!Q%ISFy~b1s9db#7Z!>5>%kV0xRMWj|A9YhXYP*LL!o&AsH@gMha5l z#ujYFHl!gP8OX$T?0^SZ$VLuwu@iaNh26+U0rp@o3Q>e<97@A4*Y%a#WxaepDfV zYV1P|YO%3>Kk87A186`a4&o4+(2T=4f}?1`F&sxLPT(X?;WXONjx#umb2yI+xQI*W zKqtBoLN|JF8NIlItGI?fT*nQBaTB+2+gu6WsajTs7P6E39QE~e@lH{b;1p_s=5cGK zLMUBphPnFfMO*XQm#N>MUiXIvLw(^;ceuCRTP*v1(pOO={Y5^RC949yvOvsCzl(dg zkA4hb5D)MWk1&J?9-C(Ra7$dgNESl-zTj$|*D@Ovhp^|lt3|cNA(S*XEkvnhZE|W= zpPs8M9`-oB{*5##1LYOE9M 16000 ? text.slice(0, 16000) + "\n…(内容已截断,原文较长)" : text; + } catch { return ""; } + } + async function fetchKnowledgeUrl(url) { + try { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 8000); + const resp = await fetch(url, { signal: ctrl.signal, redirect: "follow" }); + clearTimeout(t); + const ct = resp.headers.get("content-type") || ""; + if (!resp.ok || !ct.toLowerCase().includes("text")) return ""; + const text = await resp.text(); + if (!text) return ""; + return text.length > 16000 ? text.slice(0, 16000) + "\n…(内容已截断,原文较长)" : text; + } catch { return ""; } + } + // ctx.id 为 domain_template_id;站点级补全无 id,自然返回空(不注入)。 + // 优先依据已上传领域知识分析补全,再做大模型在线补充。 + async function buildKnowledgePrompt(ctx) { + if (!ctx || !ctx.id) return ""; + const docs = db.prepare("SELECT id,name,doc_type,url,file_path,mime FROM knowledge_docs WHERE domain_template_id=? ORDER BY id").all(ctx.id); + if (!docs.length) return ""; + const parts = []; + for (const d of docs) { + let body = ""; + if (d.doc_type === "file") body = readKnowledgeFile(d.file_path, d.mime, d.name); + else if (d.doc_type === "link") body = await fetchKnowledgeUrl(d.url); + if (body && body.trim()) parts.push(`【领域知识 · ${d.name}】\n${body.trim()}\n`); + else parts.push(`(参考来源:${d.doc_type === "link" ? d.url : d.name})`); + } + return `\n\n===== 领域知识(请优先依据以下已上传/配置的资料分析并补全,再结合通用经验做在线补充)=====\n${parts.join("\n")}\n`; + } + + async function buildCompletePrompt({ name = "", domain = "", description = "", id } = {}) { + const base = `你是一名资深的工业站点数字化方案架构师,擅长把站点业务背景转化为大屏看板(Cube 仪表盘)可用的生成上下文。 请根据以下站点信息,分析该站点在监控与运营方面的特点,并撰写一段「补充描述」,用于补充站点基本情况描述,为后续 Cube 仪表盘生成提供更充分的上下文。 站点信息: @@ -1124,6 +1168,7 @@ async function handleRefineCube(req, res, siteId, cubeId) { 2. 思考过程结束后,另起一段,以「【补充描述】」开头,给出最终要填入的补充说明文本。该文本应是一段专业、具体、可直接作为生成提示词上下文的描述,聚焦监控目标、关键指标、设备与告警、管理诉求,避免空话套话。 请直接输出你的分析与【补充描述】。`; + return base + (await buildKnowledgePrompt({ id })); } function extractSupplement(text) { @@ -1149,7 +1194,7 @@ async function handleRefineCube(req, res, siteId, cubeId) { domain: (body.domain ?? site.domain) || "", description: (body.description ?? site.description) || "", }; - const prompt = buildCompletePrompt(ctx); + const prompt = await buildCompletePrompt(ctx); emit({ prompt }); emit({ log: `\n▶ 开始 AI 智能补全:基于站点名称 / 领域 / 描述分析并生成补充描述…\n` }); emit({ task: "AI 智能补全", status: "running" }); @@ -1182,14 +1227,14 @@ async function handleRefineCube(req, res, siteId, cubeId) { domain: u.searchParams.get("domain") ?? site.domain ?? "", description: u.searchParams.get("description") ?? site.description ?? "", }; - return respondJson(res, 200, { prompt: buildCompletePrompt(ctx) }); + return respondJson(res, 200, { prompt: await buildCompletePrompt(ctx) }); } // ===== AI 资产补全:基于站点信息分析并补充设备类型/数量/命名规则 ===== - function buildCompleteAssetsPrompt(site, assets) { + async function buildCompleteAssetsPrompt(site, assets) { const existing = (assets || []).filter(a => a && a.type && a.type.trim()).map(a => a.type.trim()); const existingText = existing.length ? existing.join("、") : "(无,用户尚未配置)"; - return `你是一名资深的工业站点数字化方案架构师,擅长根据站点业务背景推导其监控运营所需的设备清单。 + const base = `你是一名资深的工业站点数字化方案架构师,擅长根据站点业务背景推导其监控运营所需的设备清单。 请根据以下站点信息,推断该站点在监控与运营中可能需要的主要设备类型,并为每种设备给出建议数量与命名规则,用于补充「资产配置」列表(作为后续 Cube 大屏看板的设备维度)。 站点信息: @@ -1205,6 +1250,7 @@ async function handleRefineCube(req, res, siteId, cubeId) { 3. 只补充用户尚未配置的设备类型;命名规则尽量贴合设备习惯(如泵类用 beng-、冷水机组用 lsch-、换热器用 hx- 等)。 请直接输出你的分析与【资产配置】JSON。`; + return base + (await buildKnowledgePrompt(site)); } function extractAssets(text) { @@ -1241,7 +1287,7 @@ async function handleRefineCube(req, res, siteId, cubeId) { supplement: (body.supplement ?? site.supplement) || "", assets: body.assets || site.assets || [], }; - const prompt = buildCompleteAssetsPrompt(ctx, ctx.assets); + const prompt = await buildCompleteAssetsPrompt(ctx, ctx.assets); emit({ prompt }); emit({ log: `\n▶ 开始 AI 资产补全:基于站点信息与现有设备,分析并补充设备类型…\n` }); emit({ task: "AI 资产补全", status: "running" }); @@ -1276,18 +1322,18 @@ async function handleRefineCube(req, res, siteId, cubeId) { supplement: u.searchParams.get("supplement") ?? site.supplement ?? "", assets: site.assets || [], }; - return respondJson(res, 200, { prompt: buildCompleteAssetsPrompt(ctx, ctx.assets) }); + return respondJson(res, 200, { prompt: await buildCompleteAssetsPrompt(ctx, ctx.assets) }); } // ===== AI Cube 页面补全:基于站点信息 + 资产层级,推断并补充 Cube 扩展页面 ===== - function buildCompleteCubesPrompt(site) { + async function buildCompleteCubesPrompt(site) { const assets = (site.assets || []).filter(a => a && a.type && a.type.trim()); const assetText = assets.length ? assets.map(a => `- ${a.type.trim()} 实例数量:${Number(a.count) || 0}${a.rule ? `(命名规则:${a.rule.trim()})` : ""}`).join("\n") : "(无,用户尚未配置设备资产)"; const existingCubes = (site.cubes || []).filter(c => c && c.name && c.name.trim()).map(c => c.name.trim()); const existingText = existingCubes.length ? existingCubes.join("、") : "(无,用户尚未创建)"; - return `你是一名资深的工业站点数字化大屏方案架构师,擅长根据站点业务背景与设备资产,规划所需的可视化监控页面(Cube 扩展页)。 + const base = `你是一名资深的工业站点数字化大屏方案架构师,擅长根据站点业务背景与设备资产,规划所需的可视化监控页面(Cube 扩展页)。 请根据以下信息,推断该站点监控大屏所需的 Cube 扩展页面,并为每个页面撰写详尽的页面内容说明,用于补充「Cube 扩展页面」列表(位于用户已建页面下方)。 站点信息: @@ -1306,6 +1352,7 @@ ${assetText} 4. 仅补充用户尚未创建的页面;页面名称简洁且不超过 6 个汉字。 请直接输出你的分析与【Cube页面】JSON。`; + return base + (await buildKnowledgePrompt(site)); } function extractCubes(text) { @@ -1342,7 +1389,7 @@ ${assetText} assets: body.assets || site.assets || [], cubes: site.cubes || [], }; - const prompt = buildCompleteCubesPrompt(ctx); + const prompt = await buildCompleteCubesPrompt(ctx); emit({ prompt }); emit({ log: `\n▶ 开始 AI Cube 补全:基于站点信息与设备资产,推断并补充 Cube 扩展页面…\n` }); emit({ task: "AI Cube 补全", status: "running" }); @@ -1378,7 +1425,7 @@ ${assetText} assets: site.assets || [], cubes: site.cubes || [], }; - return respondJson(res, 200, { prompt: buildCompleteCubesPrompt(ctx) }); + return respondJson(res, 200, { prompt: await buildCompleteCubesPrompt(ctx) }); } // 并发上限执行:最多同时跑 limit 个任务,避免无脑全并行抢占资源导致超时 @@ -1745,8 +1792,8 @@ async function handleDomainComplete(req, res, tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return fail("领域方案不存在"); const body = await parseBody(req).catch(() => ({})); - const ctx = { name: (body.name ?? dt.name) || "", domain: (body.domain ?? dt.domain_name) || "", description: (body.description ?? dt.description) || "" }; - const prompt = buildCompletePrompt(ctx); + const ctx = { id: tmplId, name: (body.name ?? dt.name) || "", domain: (body.domain ?? dt.domain_name) || "", description: (body.description ?? dt.description) || "" }; + const prompt = await buildCompletePrompt(ctx); emit({ prompt }); emit({ log: `\n▶ 开始 AI 智能补全:基于方案名称 / 所属领域 / 描述,生成「补充描述」…\n` }); emit({ task: "AI 智能补全", status: "running" }); @@ -1768,8 +1815,8 @@ async function handleDomainCompletePrompt(req, res, tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return respondError(res, 404, "领域方案不存在"); const u = new URL(req.url, "http://localhost"); - const ctx = { name: u.searchParams.get("name") ?? dt.name ?? "", domain: u.searchParams.get("domain") ?? dt.domain_name ?? "", description: u.searchParams.get("description") ?? dt.description ?? "" }; - return respondJson(res, 200, { prompt: buildCompletePrompt(ctx) }); + const ctx = { id: tmplId, name: u.searchParams.get("name") ?? dt.name ?? "", domain: u.searchParams.get("domain") ?? dt.domain_name ?? "", description: u.searchParams.get("description") ?? dt.description ?? "" }; + return respondJson(res, 200, { prompt: await buildCompletePrompt(ctx) }); } async function handleDomainCompleteAssets(req, res, tmplId) { @@ -1782,8 +1829,8 @@ async function handleDomainCompleteAssets(req, res, tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return fail("领域方案不存在"); const body = await parseBody(req).catch(() => ({})); - const ctx = { name: (body.name ?? dt.name) || "", domain: (body.domain ?? dt.domain_name) || "", description: (body.description ?? dt.description) || "", supplement: (body.supplement ?? dt.supplement) || "", assets: body.assets || [] }; - const prompt = buildCompleteAssetsPrompt(ctx, ctx.assets); + const ctx = { id: tmplId, name: (body.name ?? dt.name) || "", domain: (body.domain ?? dt.domain_name) || "", description: (body.description ?? dt.description) || "", supplement: (body.supplement ?? dt.supplement) || "", assets: body.assets || [] }; + const prompt = await buildCompleteAssetsPrompt(ctx, ctx.assets); emit({ prompt }); emit({ log: `\n▶ 开始 AI 资产补全:分析并补充设备类型(库内无合适模板时自动创建)…\n` }); emit({ task: "AI 资产补全", status: "running" }); @@ -1822,8 +1869,8 @@ async function handleDomainCompleteAssetsPrompt(req, res, tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return respondError(res, 404, "领域方案不存在"); const u = new URL(req.url, "http://localhost"); - const ctx = { name: u.searchParams.get("name") ?? dt.name ?? "", domain: u.searchParams.get("domain") ?? dt.domain_name ?? "", description: u.searchParams.get("description") ?? dt.description ?? "", supplement: u.searchParams.get("supplement") ?? dt.supplement ?? "", assets: [] }; - return respondJson(res, 200, { prompt: buildCompleteAssetsPrompt(ctx, ctx.assets) }); + const ctx = { id: tmplId, name: u.searchParams.get("name") ?? dt.name ?? "", domain: u.searchParams.get("domain") ?? dt.domain_name ?? "", description: u.searchParams.get("description") ?? dt.description ?? "", supplement: u.searchParams.get("supplement") ?? dt.supplement ?? "", assets: [] }; + return respondJson(res, 200, { prompt: await buildCompleteAssetsPrompt(ctx, ctx.assets) }); } async function handleDomainCompleteCubes(req, res, tmplId) { @@ -1836,8 +1883,8 @@ async function handleDomainCompleteCubes(req, res, tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return fail("领域方案不存在"); const body = await parseBody(req).catch(() => ({})); - const ctx = { name: (body.name ?? dt.name) || "", domain: (body.domain ?? dt.domain_name) || "", description: (body.description ?? dt.description) || "", supplement: (body.supplement ?? dt.supplement) || "", assets: body.assets || [], cubes: body.cubes || [] }; - const prompt = buildCompleteCubesPrompt(ctx); + const ctx = { id: tmplId, name: (body.name ?? dt.name) || "", domain: (body.domain ?? dt.domain_name) || "", description: (body.description ?? dt.description) || "", supplement: (body.supplement ?? dt.supplement) || "", assets: body.assets || [], cubes: body.cubes || [] }; + const prompt = await buildCompleteCubesPrompt(ctx); emit({ prompt }); emit({ log: `\n▶ 开始 AI Cube 补全:推断并补充 Cube 扩展页面…\n` }); emit({ task: "AI Cube 补全", status: "running" }); @@ -1859,8 +1906,8 @@ async function handleDomainCompleteCubesPrompt(req, res, tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return respondError(res, 404, "领域方案不存在"); const u = new URL(req.url, "http://localhost"); - const ctx = { name: u.searchParams.get("name") ?? dt.name ?? "", domain: u.searchParams.get("domain") ?? dt.domain_name ?? "", description: u.searchParams.get("description") ?? dt.description ?? "", supplement: u.searchParams.get("supplement") ?? dt.supplement ?? "", assets: [], cubes: [] }; - return respondJson(res, 200, { prompt: buildCompleteCubesPrompt(ctx) }); + const ctx = { id: tmplId, name: u.searchParams.get("name") ?? dt.name ?? "", domain: u.searchParams.get("domain") ?? dt.domain_name ?? "", description: u.searchParams.get("description") ?? dt.description ?? "", supplement: u.searchParams.get("supplement") ?? dt.supplement ?? "", assets: [], cubes: [] }; + return respondJson(res, 200, { prompt: await buildCompleteCubesPrompt(ctx) }); } async function handleDomainPageGenerate(req, res, tmplId, pageId) { diff --git a/public/design-doc.html b/public/design-doc.html index 52f4f87..322ed12 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -115,7 +115,7 @@
-

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

+

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

StationBuilder 产品与交互设计方案

1. 文档说明

@@ -753,6 +753,7 @@

11.1 版本变更记录(Changelog)

+ -- 2.43.0
版本日期修改人说明
v0.222026-08-20wang.yu领域知识提升为方案创建流程 Step 2,并作为 AI 补全的优先参考资料:① 方案向导步骤重排——领域知识由末段移至「方案基本属性」之后(Step 2),原 资产设置 / 功能与菜单 / 运维流程 / 生成保存 / 专家确认 整体后移一位(DOMAIN_STEPS 顺序调整,key 不变,步进导航自动适配)。② 新增「领域知识」步骤(KnowledgeStep):支持「上传领域知识文档」(多文件,base64 经既有 POST /api/domain-templates/:id/docs 落库,文本类读正文、其他格式登记为参考)与「配置参考 URL 站点」(doc_type=link),列表展示名称/类型/大小/时间并提供「下载」(仅文档)与「删除」(DELETE /api/knowledge-docs/:id,同步清理磁盘文件);知识随方案持久化、可删。③ 知识驱动 AI 补全:新增 buildKnowledgePrompt(ctx)(镜像 buildStylePrompt),读取本方案 knowledge_docs——文本文档直接注入正文、URL 站点由服务端 fetch 抓取正文注入(8s 超时,失败降级为仅保留 URL 链接作为参考),注入到 buildCompletePrompt / buildCompleteAssetsPrompt / buildCompleteCubesPrompt 三处(三个构建器改为 async、8 处调用点加 await;领域级补全 ctx 增加 id,站点级无 id 不注入)。优先级为「优先依据已上传领域知识分析补全,再做大模型在线补充」。④ 生成保存(DomainGenerateReview)「领域知识」行由「(待开发)」改为真实文档信息(份数 + 名称列举),并提供「返回领域知识」快捷入口;运维流程 AI 补全本期仍占位(尚未实现)。
v0.212026-08-20wang.yu领域方案库 Cube 扩展页面支持选择页面样式库样式后生成(镜像站点侧 sites.style_id 机制,方案级):① 后端 domain_templates 新增 style_id 列(ensureColumn 增量加列);handleDomainTemplateUpdate 支持写入/显式置空;hydrateDomainTemplateCtx 返回真实 style_id(此前硬编码 null)。② 前端领域方案详情「功能与菜单 → Cube 扩展页面」顶部新增「领域方案页面样式」选择器(复用 .style-picker 与页面样式库缩略图),选中即 PUT /domain-templates/:id 持久化;生成/AI 修改经共享的 buildStylePrompt 自动注入所选样式的 JSON 主题,无需改动提示词构造。
v0.202026-08-20wang.yu领域分类管理增强:① 默认分类调整——移除「工业制造 / 工业园区 / 智慧楼宇」,新增「其他」(DEFAULT_DOMAINS 同步更新,重启不再复活被删分类);② 领域方案库新增「管理领域」面板,列出全部领域及其方案数,「其他」标注「默认保留」且不可删除,其余领域提供「删除」按钮,经自定义确认弹窗二次确认后调用 DELETE /api/domains/:id;③ 后端新增 DELETE /api/domains/:id:禁止删除「其他」,删除前将待删领域的 domain_templates 与 device_template_domains 关联迁移至「其他」(避免 ON DELETE CASCADE 误删方案),再删除领域;④ 删除后前端刷新领域列表,并在当前筛选命中该领域时自动清除筛选。
v0.192026-08-20wang.yu修复领域方案「功能与菜单 → Cube 扩展页面」两处问题:①「生成」按钮改为流式请求(消费 NDJSON 响应体以保持连接),生成期间按钮显示「生成中…」并禁用、底部「AI 生成页面」控制台实时输出进度并据服务端 {status:failed} 事件弹失败提示;此前前端用普通 fetch 不读取响应体,连接被提前关闭导致服务端 AI 生成中断(status='failed')、Json 未写入。② AI 智能补全(complete-cubes)由「逐条静默 .catch(()=>{}) 插入」改为「收集结果后逐个 await 插入、失败显式 notify」,并捕获服务端 {status:failed} 事件弹失败提示,避免补全内容静默丢失、列表无变化。