]> acesimba.cloud Git - StationBuilder.git/commitdiff
feat(领域方案库): 领域知识提升为 Step2 并作为 AI 补全优先参考
authorroot <root@localhost.localdomain>
Thu, 20 Aug 2026 10:26:48 +0000 (18:26 +0800)
committerroot <root@localhost.localdomain>
Thu, 20 Aug 2026 10:26:48 +0000 (18:26 +0800)
- 方案向导步骤重排:领域知识移至「方案基本属性」之后(Step2),资产/功能菜单/运维流程/生成保存/专家确认整体后移
- 新增 KnowledgeStep:上传知识文档(多文件 base64 落库)、配置参考 URL 站点,列表可下载/删除,随方案持久化
- 新增 buildKnowledgePrompt:读取 knowledge_docs,文本文档直接注入正文、URL 站点服务端抓取正文注入(8s 超时,失败降级为参考链接),注入 buildCompletePrompt/buildCompleteAssetsPrompt/buildCompleteCubesPrompt(三构建器改 async,领域级补全 ctx 加 id;站点级无 id 不注入)
- 生成保存(DomainGenerateReview)展示真实领域知识信息并提供「返回领域知识」入口
- 设计文档更新至 v0.22

运维流程 AI 补全本期仍占位(见设计文档)

app/page.tsx
data/db/StationBuilder.sqlite
db-sidecar.mjs
public/design-doc.html

index a2c7c09b094f07071991bcb4e7e6a257099f6b4a..e1aee99b4c233532cc06fcf799aecdc33844e6b6 100644 (file)
@@ -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<MuNode[]> ]);
       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&&<Panel><div className="empty-state">正在保存…</div></Panel>}
       </div></div>}
 
+      {step==="knowledge" && <KnowledgeStep id={id} docs={m.docs||[]} notify={notify} reload={()=>load(true)} confirm={confirm}/>}
+
       {step==="assets" && <div className="two-column">
         <div>
           <div className="kpi-row">{[[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=><div key={x[1]}><span>{x[1]}</span><b>{x[0]}</b><small>{x[2]}</small></div>)}{saving&&<div><span>保存</span><Status>保存中…</Status></div>}</div>
@@ -1675,8 +1677,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA
         pageStyles={pageStyles} styleId={m.style_id??null} onSelectStyle={selectStyle} />}
 
       {step==="opsflow" && <PlaceholderStep title="运维流程" note="运维流程编辑功能待开发,后续版本将支持在方案中配置标准运维 SOP 与告警处理流程。"/>}
-      {step==="knowledge" && <PlaceholderStep title="领域知识" note="领域知识(知识文档、标准规范、参考资料)编辑功能待开发,后续版本将支持在方案中沉淀领域知识库。"/>}
-      {step==="generate" && <DomainGenerateReview tName={m.name} assets={assets} effSelected={effSelected} grouped={grouped} pages={pages} onBack={()=>goStep("functions")} onConfirm={()=>notify("方案已生成并保存,可返回上一步继续调整")}/>}
+      {step==="generate" && <DomainGenerateReview tName={m.name} assets={assets} effSelected={effSelected} grouped={grouped} pages={pages} docs={m.docs||[]} onBack={()=>goStep("functions")} onKnowledge={()=>goStep("knowledge")} onConfirm={()=>notify("方案已生成并保存,可返回上一步继续调整")}/>}
       {step==="expert" && <PlaceholderStep title="专家确认" note="专家确认流程待开发,后续版本将支持领域专家在线评审并签署方案。"/>}
     </div>
     <div className="sticky-actions"><Button secondary onClick={()=>curIdx>0?goStep(DOMAIN_STEPS[curIdx-1].key):onBack()}>{curIdx>0?"上一步":"返回列表"}</Button><div><Button secondary onClick={()=>notify("草稿已保存")}>保存草稿</Button>{curIdx<DOMAIN_STEPS.length-1 && <Button onClick={()=>goStep(DOMAIN_STEPS[curIdx+1].key)}>下一步</Button>}{curIdx===DOMAIN_STEPS.length-1 && <Button onClick={()=>notify("方案已提交专家确认")}>提交确认</Button>}</div></div>
@@ -1751,10 +1752,19 @@ function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,on
   </Panel>;
 }
 
-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 <div className="generate-layout"><div>
     <Panel title="生成范围确认"><div className="confirm-grid">
-      {[["方案",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=><div key={x[0]}><span>{x[0]}</span><b>{x[1]}</b><small>{x[2]}</small>{x[3]&&<button onClick={onBack}>{x[3]}</button>}</div>)}
+      {reviewRows.map(r=><div key={r[0]}><span>{r[0]}</span><b>{r[1]}</b><small>{r[2]}</small>{r[3]&&<button onClick={r[4]||onBack}>{r[3]}</button>}</div>)}
     </div></Panel>
     <Panel title="资产层级预览"><div className="asset-tree-preview"><div className="tree-root">⌄ <b>{tName||"未命名方案"}</b></div>{assets.filter(a=>a.device_template_id).map((row,i)=><div key={i} className="tree-branch"><div className="tree-node">⌄ {row.device_name}<span className="tree-count">{row.count}</span></div>{row.count>0&&<div className="tree-leaves">{formatInstances(row.rule,row.count)}</div>}</div>)}</div></Panel>
     <Panel title="菜单结构预览"><div className="menu-tree"><div className="menu-root">⌄ {tName||"未命名方案"}</div><div className="menu-section"><div className="menu-section-title">MU 标准页面</div>{effSelected.length===0?<div className="menu-leaf muted">(未选择)</div>:grouped.flatMap((g:any)=>g.items).filter((l:any)=>effSelected.includes(l.key)).map((l:any)=><div key={l.key} className="menu-leaf">· {l.parentMenu}/{l.label}</div>)}</div><div className="menu-section"><div className="menu-section-title">综合监控</div>{pages.filter(p=>p.name).map(p=><div key={p.id} className="menu-leaf cube">· {p.name}</div>)}</div></div></Panel>
@@ -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<boolean>}){
+  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 <div className="two-column">
+    <Panel title="上传领域知识文档" tools={busy?<Status>上传中…</Status>:undefined}>
+      <p className="hint">上传与本站领域相关的文档(标准规范、设备手册、工艺说明、建设方案等)。知识文本将作为资产配置、功能菜单 AI 补全的优先参考资料,并随方案保存。支持 txt / md / json / csv / html 等文本类,其他格式将作为参考资料登记。</p>
+      <label className="upload" style={{height:96}}>
+        <b>+</b><span>{busy?"上传中…":"选择文件或拖拽到此处(可多选)"}</span>
+        <input type="file" multiple hidden accept=".txt,.md,.markdown,.json,.csv,.log,.html,.xml,.yaml,.yml,text/*" onChange={e=>uploadFiles(e.target.files)}/>
+      </label>
+    </Panel>
+    <Panel title="配置参考 URL 站点">
+      <p className="hint">填写一个领域相关的参考网站地址。AI 补全时会由服务端抓取该站点正文并注入提示词(抓取失败则仅保留链接作为参考)。</p>
+      <div style={{display:"flex",flexDirection:"column",gap:8}}>
+        <input placeholder="站点名称,如:XX 行业标准" value={urlName} onChange={e=>setUrlName(e.target.value)}/>
+        <input placeholder="https://..." value={urlVal} onChange={e=>setUrlVal(e.target.value)}/>
+        <div><Button onClick={addUrl} disabled={busy}>添加站点</Button></div>
+      </div>
+    </Panel>
+    <Panel title={`已沉淀领域知识(${docs.length})`} tools={<Button secondary onClick={reload}>刷新</Button>}>
+      {docs.length===0? <div className="empty-state">暂无领域知识,上传文档或配置站点后此处展示。</div> :
+        <table className="data-table"><thead><tr><th>名称</th><th>类型</th><th>大小</th><th>创建时间</th><th>操作</th></tr></thead>
+        <tbody>{docs.map((d:any)=><tr key={d.id}>
+          <td>{d.name}</td>
+          <td><span style={{padding:"2px 8px",borderRadius:4,fontSize:12,background:"#2a3a55",color:"#cfe8ff"}}>{d.doc_type==="link"?"URL 站点":"文档"}</span></td>
+          <td>{d.size!=null?`${Math.round(Number(d.size)/1024*10)/10} KB`:"—"}</td>
+          <td>{d.created_at||""}</td>
+          <td style={{display:"flex",gap:8}}>{d.doc_type==="file"&&<a className="text-button" href={`/api/db/knowledge-docs/${d.id}/file`} target="_blank" rel="noreferrer">下载</a>}<button className="text-button" onClick={()=>removeDoc(d.id)}>删除</button></td>
+        </tr>)}</tbody></table>}
+    </Panel>
+  </div>;
+}
+
 function PageStyleLibrary({pageStyles, loadPageStyles, notify}:{pageStyles:PageStyle[]; loadPageStyles:()=>Promise<void>; notify:(m:string)=>void}){
   const [editor,setEditor]=useState<{open:boolean; id?:number}>({open:false});
   const [preview,setPreview]=useState<PageStyle|null>(null);
index 68eeaecaa8193fb07dc094e5dbdebe5a0b8199ff..bd413b68431f65a5365877a8254424856f988c7b 100644 (file)
Binary files a/data/db/StationBuilder.sqlite and b/data/db/StationBuilder.sqlite differ
index a7109b99aa29445bd6566cfb9ff79088bc204390..ff3c8d60a3c221e58a582f8e02b70f6d06fc886e 100644 (file)
@@ -1110,8 +1110,52 @@ async function handleRefineCube(req, res, siteId, cubeId) {
   }
 
   // ===== AI 智能补全:基于站点名称/领域/描述分析并生成「补充描述」 =====
-  function buildCompletePrompt({ name = "", domain = "", description = "" }) {
-    return `你是一名资深的工业站点数字化方案架构师,擅长把站点业务背景转化为大屏看板(Cube 仪表盘)可用的生成上下文。
+  // ===== 领域知识范本:从本方案已上传/配置的领域知识(文档 / URL 站点)中抽取可注入提示词的文本 =====
+  function isTextMime(mime, filename) {
+    if (mime && String(mime).toLowerCase().startsWith("text/")) return true;
+    const ext = (filename || "").toLowerCase().split(".").pop();
+    return [".txt", ".md", ".markdown", ".json", ".csv", ".log", ".html", ".xml", ".yaml", ".yml"].includes("." + ext);
+  }
+  function readKnowledgeFile(filePath, mime, filename) {
+    try {
+      if (!filePath || !existsSync(filePath)) return "";
+      if (!isTextMime(mime, filename)) return "";
+      const text = readFileSync(filePath, "utf8");
+      return text.length > 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) {
index 52f4f8763202f764fd65d4113fdd80aa4f84edef..322ed1275518f036d980785fc000bcf7f9020adf 100644 (file)
   <nav class="doc-nav"><div class="doc-nav-inner" id="toc"></div></nav>
 
 <main id="doc">
-  <p class="muted">文档版本:v0.21 最后更新:2026-08-20 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
+  <p class="muted">文档版本:v0.22 最后更新:2026-08-20 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
   <h1>StationBuilder 产品与交互设计方案</h1>
 
   <h2>1. 文档说明</h2>
   <h3>11.1 版本变更记录(Changelog)</h3>
   <table class="changelog">
     <tr><th>版本</th><th>日期</th><th>修改人</th><th>说明</th></tr>
+    <tr><td>v0.22</td><td>2026-08-20</td><td>wang.yu</td><td>领域知识提升为方案创建流程 Step 2,并作为 AI 补全的优先参考资料:① 方案向导步骤重排——领域知识由末段移至「方案基本属性」之后(Step 2),原 资产设置 / 功能与菜单 / 运维流程 / 生成保存 / 专家确认 整体后移一位(<code>DOMAIN_STEPS</code> 顺序调整,key 不变,步进导航自动适配)。② 新增「领域知识」步骤(<code>KnowledgeStep</code>):支持「上传领域知识文档」(多文件,base64 经既有 <code>POST /api/domain-templates/:id/docs</code> 落库,文本类读正文、其他格式登记为参考)与「配置参考 URL 站点」(<code>doc_type=link</code>),列表展示名称/类型/大小/时间并提供「下载」(仅文档)与「删除」(<code>DELETE /api/knowledge-docs/:id</code>,同步清理磁盘文件);知识随方案持久化、可删。③ 知识驱动 AI 补全:新增 <code>buildKnowledgePrompt(ctx)</code>(镜像 <code>buildStylePrompt</code>),读取本方案 <code>knowledge_docs</code>——文本文档直接注入正文、URL 站点由服务端 <code>fetch</code> 抓取正文注入(8s 超时,失败降级为仅保留 URL 链接作为参考),注入到 <code>buildCompletePrompt</code> / <code>buildCompleteAssetsPrompt</code> / <code>buildCompleteCubesPrompt</code> 三处(三个构建器改为 async、8 处调用点加 await;领域级补全 ctx 增加 <code>id</code>,站点级无 id 不注入)。优先级为「优先依据已上传领域知识分析补全,再做大模型在线补充」。④ 生成保存(<code>DomainGenerateReview</code>)「领域知识」行由「(待开发)」改为真实文档信息(份数 + 名称列举),并提供「返回领域知识」快捷入口;运维流程 AI 补全本期仍占位(<span class="todo">尚未实现</span>)。</td></tr>
     <tr><td>v0.21</td><td>2026-08-20</td><td>wang.yu</td><td>领域方案库 Cube 扩展页面支持选择页面样式库样式后生成(镜像站点侧 <code>sites.style_id</code> 机制,方案级):① 后端 <code>domain_templates</code> 新增 <code>style_id</code> 列(<code>ensureColumn</code> 增量加列);<code>handleDomainTemplateUpdate</code> 支持写入/显式置空;<code>hydrateDomainTemplateCtx</code> 返回真实 <code>style_id</code>(此前硬编码 null)。② 前端领域方案详情「功能与菜单 → Cube 扩展页面」顶部新增「领域方案页面样式」选择器(复用 <code>.style-picker</code> 与页面样式库缩略图),选中即 PUT <code>/domain-templates/:id</code> 持久化;生成/AI 修改经共享的 <code>buildStylePrompt</code> 自动注入所选样式的 JSON 主题,无需改动提示词构造。</td></tr>
     <tr><td>v0.20</td><td>2026-08-20</td><td>wang.yu</td><td>领域分类管理增强:① 默认分类调整——移除「工业制造 / 工业园区 / 智慧楼宇」,新增「其他」(<code>DEFAULT_DOMAINS</code> 同步更新,重启不再复活被删分类);② 领域方案库新增「管理领域」面板,列出全部领域及其方案数,「其他」标注「默认保留」且不可删除,其余领域提供「删除」按钮,经自定义确认弹窗二次确认后调用 <code>DELETE /api/domains/:id</code>;③ 后端新增 <code>DELETE /api/domains/:id</code>:禁止删除「其他」,删除前将待删领域的 <code>domain_templates</code> 与 <code>device_template_domains</code> 关联迁移至「其他」(避免 <code>ON DELETE CASCADE</code> 误删方案),再删除领域;④ 删除后前端刷新领域列表,并在当前筛选命中该领域时自动清除筛选。</td></tr>
     <tr><td>v0.19</td><td>2026-08-20</td><td>wang.yu</td><td>修复领域方案「功能与菜单 → Cube 扩展页面」两处问题:①「生成」按钮改为流式请求(消费 NDJSON 响应体以保持连接),生成期间按钮显示「生成中…」并禁用、底部「AI 生成页面」控制台实时输出进度并据服务端 <code>{status:failed}</code> 事件弹失败提示;此前前端用普通 <code>fetch</code> 不读取响应体,连接被提前关闭导致服务端 AI 生成中断(<code>status='failed'</code>)、Json 未写入。② AI 智能补全(complete-cubes)由「逐条静默 <code>.catch(()=>{})</code> 插入」改为「收集结果后逐个 <code>await</code> 插入、失败显式 <code>notify</code>」,并捕获服务端 <code>{status:failed}</code> 事件弹失败提示,避免补全内容静默丢失、列表无变化。</td></tr>