]> acesimba.cloud Git - StationBuilder.git/commitdiff
feat: 新增「AI 智能补全」功能(站点基本属性)
authorwang.yu <wang.yu@advantech.com>
Tue, 18 Aug 2026 01:58:53 +0000 (09:58 +0800)
committerwang.yu <wang.yu@advantech.com>
Tue, 18 Aug 2026 01:58:53 +0000 (09:58 +0800)
- 后端 db-sidecar 新增 POST /api/sites/:id/complete(流式 NDJSON 输出思考过程 +
  以【补充描述】标记提取最终补充描述)与 GET .../complete-prompt(查看提示词)
- 前端 Basic 页「填写建议」下方新增「AI 智能补全」面板:开始补全按钮 + 查看提示词
  (复用提示词弹窗,新增 completePrompt 类型)+ 与「AI 生成工作流」同款流式控制台;
  结果写入「补充要求」并走现有 autosave 持久化
- 设计文档 §5.3 与 11.1 同步更新

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

index 407b109767e30375db85ef3aa7318203354747a0..e3f9842e91e5e2c380742548afc0fcbff26e9f19 100644 (file)
@@ -3,7 +3,7 @@
 import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
 
 type PageKey = "login" | "projects" | "basic" | "assets" | "functions" | "generate" | "result" | "preview" | "domains" | "domainDetail" | "styleLibrary";
-type ModalKey = "" | "feedback" | "download" | "history" | "prompt";
+type ModalKey = "" | "feedback" | "download" | "history" | "prompt" | "completePrompt";
 
 const meta: Record<PageKey, { label: string; icon: string; subtitle: string }> = {
   login:{label:"内部登录",icon:"◇",subtitle:"使用公司账号登录"},
@@ -200,7 +200,15 @@ export default function Home(){
       setModal("prompt");
     }catch(e:any){ notify("读取提示词失败: "+e.message); }
   };
-
+  const viewCompletePrompt=async()=>{
+    if(!site?.id) { notify("请先创建或打开一个站点"); return; }
+    try{
+      const qs=new URLSearchParams({name:site.name||"",domain:site.domain||"",description:site.description||""}).toString();
+      const data:{prompt:string}=await fetchDb(`/sites/${site.id}/complete-prompt?${qs}`);
+      setPromptData({name:"AI 智能补全", text:data.prompt});
+      setModal("completePrompt");
+    }catch(e:any){ notify("读取提示词失败: "+e.message); }
+  };
   const openFeedback=(cube:Cube)=>{
     if(!cube.id){ notify("请先生成该页面再提出修改意见"); return; }
     setFeedbackCube(cube);
@@ -431,7 +439,7 @@ export default function Home(){
       {page==="domains"&&<DomainLibrary go={go} notify={notify}/>}
       {page==="domainDetail"&&<DomainDetail go={go} notify={notify}/>}
       {page==="styleLibrary"&&<PageStyleLibrary pageStyles={pageStyles} loadPageStyles={loadPageStyles} notify={notify}/>}
-      {page==="basic"&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving}/>}
+      {page==="basic"&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt}/>}
       {page==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving}/>}
       {page==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify}/>}
       {page==="generate"&&<Generate site={site} onSiteChange={setSite} notify={notify} viewPrompt={viewPrompt} viewSitePrompt={viewSitePrompt}/>}
@@ -480,11 +488,40 @@ function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete}:{view
   return <><div className="summary-strip">{[[String(counts.all),"全部项目"],[String(counts.draft),"草稿"],[String(counts.running),"生成中"],[String(counts.done),"生成完成"],[String(counts.partial),"部分失败"]].map(x=><div key={x[1]}><b>{x[0]}</b><span>{x[1]}</span></div>)}</div><Panel><div className="filterbar"><label>⌕ <input placeholder="搜索站点名称"/></label><select><option>全部领域</option></select><select><option>全部状态</option></select><Button>Search</Button><span/>{selected.size>0 && <Button secondary onClick={batchDelete}>批量删除 ({selected.size})</Button>}<div className="view-switch"><button className={view==="table"?"active":""} onClick={()=>setView("table")}>☷</button><button className={view==="card"?"active":""} onClick={()=>setView("card")}>▦</button></div><button className="icon-button" onClick={()=>notify("列表已刷新")}>↻</button></div>{view==="table"?<table className="data-table"><thead><tr><th><input type="checkbox" checked={allSelected} onChange={toggleAll}/></th><th>站点名称 ↕</th><th>所属领域</th><th>设备数量</th><th>MU 页面</th><th>Cube 页面</th><th>状态</th><th>最近修改</th><th>操作</th></tr></thead><tbody>{sites.map(row=><tr key={row.id}><td><input type="checkbox" checked={selected.has(row.id)} onChange={()=>toggle(row.id)}/></td><td><button className="link" onClick={()=>go(row.status==="generated"?"result":"basic", row.id)}><span className="site-icon">⬡</span><b>{row.name || "未命名站点"}</b></button></td><td>{row.domain}</td><td>{row.device_count}</td><td>{row.mu_count}</td><td>{row.cube_count}</td><td><Status>{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}</Status></td><td>{row.updated_at}</td><td><button className="link danger" onClick={(e)=>{e.stopPropagation(); onDelete([row.id]);}}>删除</button></td></tr>)}</tbody></table>:<div className="project-grid">{sites.map(row=><article key={row.id} onClick={()=>go(row.status==="generated"?"result":"basic", row.id)}><div><span className="site-icon big">⬡</span><Status>{row.status==="generated"?"生成完成":row.status==="partial"?"部分失败":row.status==="running"?"生成中":"草稿"}</Status></div><h3>{row.name || "未命名站点"}</h3><button className="card-del" onClick={(e)=>{e.stopPropagation(); onDelete([row.id]);}}>删除</button><p>{row.domain}</p><section><span><b>{row.device_count}</b> 设备</span><span><b>{row.mu_count}</b> MU</span><span><b>{row.cube_count}</b> Cube</span></section><small>更新于 {row.updated_at}</small></article>)}</div>}<div className="pagination"><span>共 {sites.length} 条</span><button>‹</button><button className="active">1</button><button>›</button><select><option>20 / page</option></select></div></Panel></>
 }
 
-function Basic({site,domains,onChange,onAddDomain,onUpload,saving}:{site:SiteDetail|null;domains:string[];onChange:(p:Partial<SiteDetail>)=>void;onAddDomain:(n:string)=>void;onUpload:(f:File)=>void;saving:boolean}){
+function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewCompletePrompt}:{site:SiteDetail|null;domains:string[];onChange:(p:Partial<SiteDetail>)=>void;onAddDomain:(n:string)=>void;onUpload:(f:File)=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
   const [newDomain,setNewDomain]=useState("");
   const [domainInput,setDomainInput]=useState(site?.domain||"");
   const [showAdd,setShowAdd]=useState(false);
   useEffect(()=>{ if(site) setDomainInput(site.domain||""); },[site?.domain]);
+  const [completing,setCompleting]=useState(false);
+  const [completeLog,setCompleteLog]=useState("");
+  const completeConsoleRef=useRef<HTMLPreElement>(null);
+  useEffect(()=>{ if(completeConsoleRef.current) completeConsoleRef.current.scrollTop=completeConsoleRef.current.scrollHeight; },[completeLog]);
+  const runComplete=async()=>{
+    if(!site?.id || completing) return;
+    setCompleting(true); setCompleteLog("");
+    const watchdog=setTimeout(()=>{ setCompleting(false); notify("AI 智能补全超时(>6 分钟),请重试"); }, 360000);
+    try{
+      const res=await fetch(`/api/db/sites/${site.id}/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:site.name,domain:site.domain,description:site.description})});
+      if(!res.ok||!res.body) throw new Error("HTTP "+res.status);
+      const reader=res.body.getReader(); const dec=new TextDecoder(); let buf="";
+      for(;;){
+        const {done,value}=await reader.read(); if(done) break;
+        buf+=dec.decode(value,{stream:true});
+        const lines=buf.split("\n"); buf=lines.pop()||"";
+        for(const line of lines){
+          if(!line.trim()) continue;
+          try{
+            const ev=JSON.parse(line);
+            if(ev.log) setCompleteLog(prev=>prev+ev.log);
+            if(ev.result) onChange({supplement:ev.result});
+          }catch{}
+        }
+      }
+      notify("AI 智能补全已完成,已写入补充描述");
+    }catch(e:any){ notify("AI 智能补全失败:"+(e?.message||e)); }
+    finally{ clearTimeout(watchdog); setCompleting(false); }
+  };
   if(!site) return <Panel><div className="empty-state">加载中…</div></Panel>;
   const nameTooLong = countChars(site.name)>20;
   const domainTooLong = newDomain.length>6;
@@ -511,7 +548,18 @@ function Basic({site,domains,onChange,onAddDomain,onUpload,saving}:{site:SiteDet
       {site.files.length>0&&<ul className="file-list">{site.files.map(f=><li key={f.id}><a href={`/api/db/sites/${site.id}/files/${f.id}`} download>{f.filename}</a> <small>({(f.size/1024).toFixed(1)} KB)</small></li>)}</ul>}
     </Field>
   </div></Panel>
-  <div className="stack"><Panel title="填写建议"><ul className="tips"><li>站点主要承担什么业务?</li><li>哪些设备最重要?</li><li>管理人员最关注哪些指标?</li><li>希望首页首先看到什么?</li></ul></Panel>{saving&&<Panel><div className="empty-state">正在保存…</div></Panel>}</div></div>;
+  <div className="stack">
+    <Panel title="填写建议"><ul className="tips"><li>站点主要承担什么业务?</li><li>哪些设备最重要?</li><li>管理人员最关注哪些指标?</li><li>希望首页首先看到什么?</li></ul></Panel>
+    <Panel title="AI 智能补全">
+      <p className="hint">根据上方「站点名称 / 所属领域 / 站点基本情况描述」,由 AI 分析并自动补全「补充要求」。</p>
+      <div style={{display:"flex",gap:"8px",alignItems:"center"}}>
+        <button className="button" disabled={completing} onClick={runComplete}>{completing?"补全中…":"开始补全"}</button>
+        <button className="text-button" onClick={onViewCompletePrompt}>查看提示词</button>
+      </div>
+      {(completing||completeLog)&&<div className="modal-console"><div className="modal-console-head">AI 智能补全</div><pre className="modal-console-body" ref={completeConsoleRef}>{completeLog||"(等待 AI 分析…)"}</pre></div>}
+    </Panel>
+    {saving&&<Panel><div className="empty-state">正在保存…</div></Panel>}
+  </div></div>;
 }
 
 function Assets({site,onChange,saving}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean}){
@@ -925,10 +973,10 @@ function Modal({type,close,notify,promptData,feedbackCube,refineCube,refining,re
   const [feedbackText,setFeedbackText]=useState("");
   const refineConsoleRef=useRef<HTMLPreElement>(null);
   useEffect(()=>{ if(refineConsoleRef.current) refineConsoleRef.current.scrollTop = refineConsoleRef.current.scrollHeight; }, [refineLog]);
-  const title={feedback:`提出修改意见 · ${feedbackCube?.name||""}`,download:"下载站点交付物",history:"版本历史",prompt:`生成提示词 · ${promptData?.name||""}`}[type];
+  const title={feedback:`提出修改意见 · ${feedbackCube?.name||""}`,download:"下载站点交付物",history:"版本历史",prompt:`生成提示词 · ${promptData?.name||""}`,completePrompt:"AI 智能补全提示词"}[type];
   const submitFeedback=()=>{
     if(!feedbackCube){ notify("未选择目标 Cube 页面,请关闭后重试"); close(); return; }
     if(!refineCube){ notify("反馈功能未正确初始化"); return; }
     refineCube(feedbackCube, feedbackText);
   };
-  return <div className="modal-backdrop" onMouseDown={close}><div className="modal" onMouseDown={e=>e.stopPropagation()}><header><h2>{title}</h2><button onClick={close}>×</button></header><div className="modal-body">{type==="feedback"&&<><div className="impact-title">系统已识别本次修改范围(仅针对「{feedbackCube?.name}」页面)</div><Field label="修改意见"><textarea autoFocus value={feedbackText} onChange={e=>setFeedbackText(e.target.value)} placeholder="例如:顶部增加设备在线率,将告警列表移到页面底部,趋势图改为最近七天。"/></Field><p className="hint">在上方输入框填写修改意见,提交后会在当前页面已生成的 JSON 基础上按意见修改,并保留完整提示词(含本次修改意见),可随时通过「提示词」按钮查看。</p><ul className="impact-list"><li><b>当前页面 JSON</b><span>将依据修改意见重新生成</span></li><li><b>页面模拟数据</b><span>将同步更新</span></li><li><b>Asset Excel</b><span>不受影响</span></li><li><b>其他 Cube 页面</b><span>不受影响</span></li></ul>{(refining||refineLog)?<div className="modal-console"><div className="modal-console-head">AI 生成工作流</div><pre className="modal-console-body" ref={refineConsoleRef}>{refineLog||"(等待 AI 反馈…)"}</pre></div>:null}</>}{type==="download"&&<div className="download-list">{["Cube 页面 JSON","站点菜单配置","生成及校验报告"].map(x=><label key={x}><input type="checkbox" defaultChecked/><span>{x}</span></label>)}<div><b>station-package.zip</b><span>包含全部生成产物</span></div></div>}{type==="history"&&<div className="history-list">{[["V1","首次生成",new Date().toLocaleString()]].map((x,i)=><div key={x[0]}><i>{x[0]}</i><span><b>{x[1]}</b><small>{x[2]} · 王宇</small></span><Status>当前版本</Status></div>)}</div>}{type==="prompt"&&<><p className="hint">这是发送给 Cube 生成模型的完整组合提示词。在点击「开始生成」前可在此核对站点上下文、页面要求与 Cube 规则是否准确,便于定位生成质量问题。</p><pre className="prompt-view">{promptData?.text||"(暂无提示词)"}</pre></>}</div><footer><Button secondary onClick={close}>关闭</Button>{type==="feedback"&&<Button disabled={refining} onClick={submitFeedback}>{refining?"重新生成中…":"提交并重新生成"}</Button>}{type==="prompt"&&<Button onClick={()=>{ if(promptData?.text){ navigator.clipboard?.writeText(promptData.text).then(()=>notify("提示词已复制到剪贴板")).catch(()=>notify("复制失败,请手动选择")); } }}>复制提示词</Button>}</footer></div></div>}
+  return <div className="modal-backdrop" onMouseDown={close}><div className="modal" onMouseDown={e=>e.stopPropagation()}><header><h2>{title}</h2><button onClick={close}>×</button></header><div className="modal-body">{type==="feedback"&&<><div className="impact-title">系统已识别本次修改范围(仅针对「{feedbackCube?.name}」页面)</div><Field label="修改意见"><textarea autoFocus value={feedbackText} onChange={e=>setFeedbackText(e.target.value)} placeholder="例如:顶部增加设备在线率,将告警列表移到页面底部,趋势图改为最近七天。"/></Field><p className="hint">在上方输入框填写修改意见,提交后会在当前页面已生成的 JSON 基础上按意见修改,并保留完整提示词(含本次修改意见),可随时通过「提示词」按钮查看。</p><ul className="impact-list"><li><b>当前页面 JSON</b><span>将依据修改意见重新生成</span></li><li><b>页面模拟数据</b><span>将同步更新</span></li><li><b>Asset Excel</b><span>不受影响</span></li><li><b>其他 Cube 页面</b><span>不受影响</span></li></ul>{(refining||refineLog)?<div className="modal-console"><div className="modal-console-head">AI 生成工作流</div><pre className="modal-console-body" ref={refineConsoleRef}>{refineLog||"(等待 AI 反馈…)"}</pre></div>:null}</>}{type==="download"&&<div className="download-list">{["Cube 页面 JSON","站点菜单配置","生成及校验报告"].map(x=><label key={x}><input type="checkbox" defaultChecked/><span>{x}</span></label>)}<div><b>station-package.zip</b><span>包含全部生成产物</span></div></div>}{type==="history"&&<div className="history-list">{[["V1","首次生成",new Date().toLocaleString()]].map((x,i)=><div key={x[0]}><i>{x[0]}</i><span><b>{x[1]}</b><small>{x[2]} · 王宇</small></span><Status>当前版本</Status></div>)}</div>}{type==="prompt"&&<><p className="hint">这是发送给 Cube 生成模型的完整组合提示词。在点击「开始生成」前可在此核对站点上下文、页面要求与 Cube 规则是否准确,便于定位生成质量问题。</p><pre className="prompt-view">{promptData?.text||"(暂无提示词)"}</pre></>}{type==="completePrompt"&&<><p className="hint">这是「AI 智能补全」发送给分析模型的完整提示词。它基于站点名称、所属领域与站点基本情况描述,要求模型先输出分析思路,再以「【补充描述】」给出最终要写入「补充要求」的文本。</p><pre className="prompt-view">{promptData?.text||"(暂无提示词)"}</pre></>}</div><footer><Button secondary onClick={close}>关闭</Button>{type==="feedback"&&<Button disabled={refining} onClick={submitFeedback}>{refining?"重新生成中…":"提交并重新生成"}</Button>}{type==="prompt"&&<Button onClick={()=>{ if(promptData?.text){ navigator.clipboard?.writeText(promptData.text).then(()=>notify("提示词已复制到剪贴板")).catch(()=>notify("复制失败,请手动选择")); } }}>复制提示词</Button>}</footer></div></div>}
index b970171652765e2b1f3c317aa5a665361e92c134..ccab5efb521bf421586f010903271718d994d757 100644 (file)
@@ -836,10 +836,86 @@ async function handleRefineCube(req, res, siteId, cubeId) {
     emit({ log: `✗ 重新生成失败:${e.message}\n` });
     emit({ task: "系统", status: "failed", message: e.message });
     res.end();
-  } finally {
-    genLocks.delete(`cube-${cubeId}`);
+    } finally {
+      genLocks.delete(`cube-${cubeId}`);
+    }
+  }
+
+  // ===== AI 智能补全:基于站点名称/领域/描述分析并生成「补充描述」 =====
+  function buildCompletePrompt({ name = "", domain = "", description = "" }) {
+    return `你是一名资深的工业站点数字化方案架构师,擅长把站点业务背景转化为大屏看板(Cube 仪表盘)可用的生成上下文。
+请根据以下站点信息,分析该站点在监控与运营方面的特点,并撰写一段「补充描述」,用于补充站点基本情况描述,为后续 Cube 仪表盘生成提供更充分的上下文。
+
+站点信息:
+- 站点名称:${name || "未命名站点"}
+- 所属领域:${domain || "未指定"}
+- 站点基本情况描述:${description || "无"}
+
+要求:
+1. 先简要说明你的分析思路(思考过程):从名称/领域/描述中推断业务重点、关键设备、管理关注点,以及建议呈现的指标类型。
+2. 思考过程结束后,另起一段,以「【补充描述】」开头,给出最终要填入的补充说明文本。该文本应是一段专业、具体、可直接作为生成提示词上下文的描述,聚焦监控目标、关键指标、设备与告警、管理诉求,避免空话套话。
+
+请直接输出你的分析与【补充描述】。`;
+  }
+
+  function extractSupplement(text) {
+    const marker = "【补充描述】";
+    const idx = text.lastIndexOf(marker);
+    const body = idx >= 0 ? text.slice(idx + marker.length) : text;
+    return body.replace(/^\s+|\s+$/g, "");
+  }
+
+  async function handleCompleteSite(req, res, siteId) {
+    res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" });
+    const emit = (obj) => streamEvent(res, obj);
+    const fail = (message) => { emit({ task: "系统", status: "failed", message }); res.end(); };
+    if (genLocks.has(`complete-${siteId}`)) return fail("AI 智能补全正在进行中");
+    genLocks.add(`complete-${siteId}`);
+    try {
+      const site = hydrateSite(siteId);
+      if (!site) return fail("站点不存在");
+      const body = await parseBody(req).catch(() => ({}));
+      // 优先使用前端传入的当前编辑值,缺省回退到库中站点
+      const ctx = {
+        name: (body.name ?? site.name) || "",
+        domain: (body.domain ?? site.domain) || "",
+        description: (body.description ?? site.description) || "",
+      };
+      const prompt = buildCompletePrompt(ctx);
+      emit({ prompt });
+      emit({ log: `\n▶ 开始 AI 智能补全:基于站点名称 / 领域 / 描述分析并生成补充描述…\n` });
+      emit({ task: "AI 智能补全", status: "running" });
+      const onEvent = (o) => {
+        if (o && o.type === "stream_event" && o.event?.type === "content_block_delta" && o.event.delta?.type === "text_delta" && o.event.delta.text) {
+          emit({ log: o.event.delta.text });
+        }
+      };
+      const text = await runCodebuddy(prompt, `stationbuilder-complete-${siteId}`, onEvent);
+      const supplement = extractSupplement(text);
+      emit({ log: `\n✓ 已生成补充描述(${supplement.length} 字)\n` });
+      emit({ result: supplement });
+      emit({ task: "AI 智能补全", status: "done" });
+      res.end();
+    } catch (e) {
+      emit({ log: `✗ AI 智能补全失败:${e.message}\n` });
+      emit({ task: "系统", status: "failed", message: e.message });
+      res.end();
+    } finally {
+      genLocks.delete(`complete-${siteId}`);
+    }
+  }
+
+  async function handleCompletePrompt(req, res, siteId) {
+    const site = hydrateSite(siteId);
+    if (!site) return respondError(res, 404, "站点不存在");
+    const u = new URL(req.url, "http://localhost");
+    const ctx = {
+      name: u.searchParams.get("name") ?? site.name ?? "",
+      domain: u.searchParams.get("domain") ?? site.domain ?? "",
+      description: u.searchParams.get("description") ?? site.description ?? "",
+    };
+    return respondJson(res, 200, { prompt: buildCompletePrompt(ctx) });
   }
-}
 
 async function handleGenerateAll(req, res, siteId) {
   res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" });
@@ -1056,6 +1132,8 @@ async function route(req, res) {
   const mCubeFile = path.match(/^\/api\/sites\/(\d+)\/cubes\/(\d+)\/file$/);
   const mCubeRefine = path.match(/^\/api\/sites\/(\d+)\/cubes\/(\d+)\/refine$/);
   const mCubePrompt = path.match(/^\/api\/sites\/(\d+)\/cubes\/(\d+)\/prompt$/);
+  const mSiteComplete = path.match(/^\/api\/sites\/(\d+)\/complete$/);
+  const mSiteCompletePrompt = path.match(/^\/api\/sites\/(\d+)\/complete-prompt$/);
   const mCubePromptNew = path.match(/^\/api\/sites\/(\d+)\/cubes\/prompt$/);
   const mSiteMenu = path.match(/^\/api\/sites\/(\d+)\/menu$/);
   const mSiteFiles = path.match(/^\/api\/sites\/(\d+)\/files$/);
@@ -1197,6 +1275,8 @@ async function route(req, res) {
       if (!site) return respondError(res, 404, "站点不存在");
       return respondJson(res, 200, { prompt: buildSiteContextPrompt(site) });
     }
+    if (mSiteComplete) return handleCompleteSite(req, res, Number(mSiteComplete[1]));
+    if (mSiteCompletePrompt) return handleCompletePrompt(req, res, Number(mSiteCompletePrompt[1]));
     if (mSiteCubes) {
       const siteId = Number(mSiteCubes[1]);
       if (req.method === "GET") return respondJson(res, 200, db.prepare("SELECT c.*, c.prompt, gf.filename as gen_filename FROM cubes c LEFT JOIN gen_files gf ON gf.id=c.gen_file_id WHERE c.site_id=? ORDER BY c.sort_order,c.id").all(siteId));
index 96411c4d74613b55aafc6eb8c2f93f7b2ef1918d..f3db96075dafcb4ae98e0c014353ba5bafd6882b 100644 (file)
     <tr><td>站点基本情况描述 / 补充要求</td><td>生成上下文</td><td>PUT /api/db/sites</td><td>输入即保存;作为生成提示词的关键上下文</td></tr>
     <tr><td>参考资料上传</td><td>上传需求文档 / 清单 / 图片</td><td>POST /api/db/sites/:id/files</td><td>选择文件后上传,列出可下载文件</td></tr>
     <tr><td>填写建议面板</td><td>引导填写</td><td>静态</td><td>纯展示</td></tr>
+    <tr><td>AI 智能补全</td><td>根据站点名称 / 所属领域 / 站点基本情况描述,由 AI 分析并自动补全「补充要求」</td><td>POST /api/db/sites/:id/complete(流式 NDJSON)</td><td>「开始补全」后控制台流式输出思考过程,结束将结果写入「补充要求」并自动保存;「查看提示词」可查看本次使用的完整提示词 <span class="todo">补充描述质量将按反馈持续优化</span></td></tr>
   </table>
 
   <h3>5.4 Asset 资产设置(assets)</h3>
   <ul>
     <li>2026-08-18 修复右下角 AI 悬浮按钮拖动时常失效的问题:图标改为不可原生拖拽、事件直接落在按钮上,避免浏览器图片拖拽劫持 Pointer 拖拽手势;按下拖动时不再有 1px 移位。</li>
     <li>2026-08-18 修复「下载 Markdown」导入 Azure DevOps Wiki 后仅显示到 §4.2 的问题:代码块改以 4 空格缩进式代码块导出(不再用 ``` 围栏),避免围栏闭合异常吞掉后续章节;表格单元格内将 <code>&lt;</code> 与 <code>&gt;</code> 转义为 HTML 实体,避免 Azure 把 <code>&lt;select&gt;</code> 当成未闭合标签吞掉后续内容。</li>
+    <li>2026-08-18 新增「AI 智能补全」功能:站点基本属性页「填写建议」下方新增面板,点击「开始补全」后基于站点名称 / 所属领域 / 站点基本情况描述,由 AI 流式分析并自动写入「补充要求」字段;「查看提示词」可查看完整组合提示词(后端 <code>POST /api/db/sites/:id/complete</code> 流式、<code>GET .../complete-prompt</code> 取提示词)。</li>
   </ul>
 
   <ul>