From 8e306b4e171def59f3bcb3d5a3a32585b51fee53 Mon Sep 17 00:00:00 2001 From: "wang.yu" Date: Tue, 18 Aug 2026 10:41:30 +0800 Subject: [PATCH] =?utf8?q?feat:=20Asset=20=E8=B5=84=E4=BA=A7=E8=AE=BE?= =?utf8?q?=E7=BD=AE=E9=A1=B5=E6=96=B0=E5=A2=9E=E3=80=8CAI=20=E6=99=BA?= =?utf8?q?=E8=83=BD=E8=A1=A5=E5=85=A8=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 后端 db-sidecar 新增 POST /api/sites/:id/complete-assets(流式 NDJSON:思考过程 + 以【资产配置】标记提取 JSON 设备清单 [{type,count,rule}])与 GET .../complete-assets-prompt - 前端 Assets 页改为两栏:右栏与基本属性同款「填写建议 + AI 智能补全」(开始补全 + 查看提示词 + 流式控制台);结果追加到用户已填项下方(不改动已有项),资产层级预览随之重算 - 设计文档 §5.4 与 11.1 同步更新 --- app/page.tsx | 103 ++++++++++++++++++++++++++++++++--------- db-sidecar.mjs | 98 +++++++++++++++++++++++++++++++++++++++ public/design-doc.html | 2 + 3 files changed, 181 insertions(+), 22 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index e3f9842..d7e1627 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -209,6 +209,15 @@ export default function Home(){ setModal("completePrompt"); }catch(e:any){ notify("读取提示词失败: "+e.message); } }; + const viewCompleteAssetsPrompt=async()=>{ + if(!site?.id) { notify("请先创建或打开一个站点"); return; } + try{ + const qs=new URLSearchParams({name:site.name||"",domain:site.domain||"",description:site.description||"",supplement:site.supplement||""}).toString(); + const data:{prompt:string}=await fetchDb(`/sites/${site.id}/complete-assets-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); @@ -440,7 +449,7 @@ export default function Home(){ {page==="domainDetail"&&} {page==="styleLibrary"&&} {page==="basic"&&} - {page==="assets"&&} + {page==="assets"&&} {page==="functions"&&} {page==="generate"&&} {page==="result"&&} @@ -562,7 +571,42 @@ function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewC ; } -function Assets({site,onChange,saving}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean}){ +function Assets({site,onChange,saving,notify,onViewCompletePrompt}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void}){ + const [completing,setCompleting]=useState(false); + const [completeLog,setCompleteLog]=useState(""); + const completeConsoleRef=useRef(null); + useEffect(()=>{ if(completeConsoleRef.current) completeConsoleRef.current.scrollTop=completeConsoleRef.current.scrollHeight; },[completeLog]); + const appendAssets=(items:Array<{type:string;count:number;rule:string}>)=>{ + const base = site?.assets || []; + const next = [...base, ...items.map((it,i)=>({type:it.type, count:Number(it.count)||0, rule:it.rule||"", sort_order: base.length+i}))]; + onChange(next); + notify(`AI 已补充 ${items.length} 类设备到资产配置(位于您已填项下方)`); + }; + const runCompleteAssets=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-assets`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:site.name,domain:site.domain,description:site.description,supplement:site.supplement,assets:site.assets})}); + 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(Array.isArray(ev.result)) appendAssets(ev.result); + }catch{} + } + } + notify("AI 资产补全已完成"); + }catch(e:any){ notify("AI 资产补全失败:"+(e?.message||e)); } + finally{ clearTimeout(watchdog); setCompleting(false); } + }; if(!site) return
加载中…
; const rows = site.assets.length ? site.assets : Array.from({length:4},(_,i)=>({type:"",count:0,rule:"",sort_order:i})); const total = rows.reduce((n,a)=>n+Number(a.count||0),0); @@ -572,26 +616,41 @@ function Assets({site,onChange,saving}:{site:SiteDetail|null;onChange:(assets:As }; const add=()=>onChange([...rows,{type:"",count:0,rule:"",sort_order:rows.length}]); const remove=(i:number)=>onChange(rows.filter((_,idx)=>idx!==i)); - return <>
{[[rows.length,"设备类型","类"],[total,"设备实例","个"],[Math.max(2, rows.filter(r=>r.type).length+1),"资产层级","层"]].map(x=>
{x[1]}{x[0]}{x[2]}
)}{saving&&
保存保存中…
}
- + 添加设备类型}> - - - {rows.map((row,i)=> - - - - - )} -
设备类型 *数量 *命名规则操作
update(i,{type:e.target.value})}/>update(i,{count:Number(e.target.value)})}/>update(i,{rule:e.target.value})}/>
-
-
-
⌄ {site.name || "未命名站点"}
- {rows.filter(r=>r.type).map((row,i)=>
-
⌄ {row.type} {row.count}
- {row.count>0&&
{formatInstances(row.rule, row.count)}
} -
)} -
- ; + return
+
+
{[[rows.length,"设备类型","类"],[total,"设备实例","个"],[Math.max(2, rows.filter(r=>r.type).length+1),"资产层级","层"]].map(x=>
{x[1]}{x[0]}{x[2]}
)}{saving&&
保存保存中…
}
+ + 添加设备类型}> + + + {rows.map((row,i)=> + + + + + )} +
设备类型 *数量 *命名规则操作
update(i,{type:e.target.value})}/>update(i,{count:Number(e.target.value)})}/>update(i,{rule:e.target.value})}/>
+
+
+
⌄ {site.name || "未命名站点"}
+ {rows.filter(r=>r.type).map((row,i)=>
+
⌄ {row.type} {row.count}
+ {row.count>0&&
{formatInstances(row.rule, row.count)}
} +
)} +
+
+
+
  • 列出站点可能涉及的主要设备类型(泵、风机、换热器、仪表等)。
  • 为每种设备估算典型数量与命名规则。
  • 命名规则用 # 表示序号,如 CH-## 生成 CH-01/CH-02;纯前缀如 beng- 会自动补 -01/-02。
  • 资产层级按「站点 → 类型 → 实例」三级展开预览。
+ +

基于站点名称 / 领域 / 描述 / 补充要求,由 AI 分析并补充设备类型、数量与命名规则到资产配置列表(位于您已填项下方)。

+
+ + +
+ {(completing||completeLog)&&
AI 智能补全
{completeLog||"(等待 AI 分析…)"}
} +
+ {saving&&
正在保存…
} +
+
; } function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;pageStyles:PageStyle[];notify:(m:string)=>void}){ diff --git a/db-sidecar.mjs b/db-sidecar.mjs index ccab5ef..1da3dc2 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -917,6 +917,100 @@ async function handleRefineCube(req, res, siteId, cubeId) { return respondJson(res, 200, { prompt: buildCompletePrompt(ctx) }); } + // ===== AI 资产补全:基于站点信息分析并补充设备类型/数量/命名规则 ===== + 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 `你是一名资深的工业站点数字化方案架构师,擅长根据站点业务背景推导其监控运营所需的设备清单。 +请根据以下站点信息,推断该站点在监控与运营中可能需要的主要设备类型,并为每种设备给出建议数量与命名规则,用于补充「资产配置」列表(作为后续 Cube 大屏看板的设备维度)。 + +站点信息: +- 站点名称:${site.name || "未命名站点"} +- 所属领域:${site.domain || "未指定"} +- 站点基本情况描述:${site.description || "无"} +- 补充要求:${site.supplement || "无"} +- 用户已手工配置的设备类型(请勿重复、不要修改,仅补充新增):${existingText} + +要求: +1. 先简要说明分析思路(思考过程):从站点业务推断典型设备类型、各设备的大致数量级、以及合适的命名规则(命名规则用 # 表示序号,如 CH-## 表示 CH-01/CH-02;纯前缀如 beng- 会自动补 -01/-02)。 +2. 思考过程结束后,另起一段,以「【资产配置】」开头,给出 JSON 数组(仅数组本身,不要多余文字与代码围栏),每个元素为 {"type":"设备类型","count":数量(整数,根据站点规模给合理预设),"rule":"命名规则"}。 +3. 只补充用户尚未配置的设备类型;命名规则尽量贴合设备习惯(如泵类用 beng-、冷水机组用 lsch-、换热器用 hx- 等)。 + +请直接输出你的分析与【资产配置】JSON。`; + } + + function extractAssets(text) { + const marker = "【资产配置】"; + const idx = text.lastIndexOf(marker); + let body = idx >= 0 ? text.slice(idx + marker.length) : text; + body = body.replace(/```(?:json)?/gi, "").trim(); + const s = body.indexOf("["); + const e = body.lastIndexOf("]"); + if (s < 0 || e <= s) throw new Error("未在输出中找到资产配置 JSON"); + const arr = JSON.parse(body.slice(s, e + 1)); + if (!Array.isArray(arr)) throw new Error("资产配置不是数组"); + return arr.filter(x => x && x.type).map(x => ({ + type: String(x.type).trim(), + count: Number(x.count) || 0, + rule: x.rule != null ? String(x.rule).trim() : "", + })); + } + + async function handleCompleteAssets(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-assets-${siteId}`)) return fail("AI 资产补全正在进行中"); + genLocks.add(`complete-assets-${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) || "", + supplement: (body.supplement ?? site.supplement) || "", + assets: body.assets || site.assets || [], + }; + const prompt = buildCompleteAssetsPrompt(ctx, ctx.assets); + 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-assets-${siteId}`, onEvent); + const items = extractAssets(text); + emit({ log: `\n✓ 已生成 ${items.length} 类设备配置\n` }); + emit({ result: items }); + 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-assets-${siteId}`); + } + } + + async function handleCompleteAssetsPrompt(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 ?? "", + supplement: u.searchParams.get("supplement") ?? site.supplement ?? "", + assets: site.assets || [], + }; + return respondJson(res, 200, { prompt: buildCompleteAssetsPrompt(ctx, ctx.assets) }); + } + async function handleGenerateAll(req, res, siteId) { res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" }); if (genLocks.has(`site-${siteId}`)) { @@ -1134,6 +1228,8 @@ async function route(req, res) { 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 mSiteCompleteAssets = path.match(/^\/api\/sites\/(\d+)\/complete-assets$/); + const mSiteCompleteAssetsPrompt = path.match(/^\/api\/sites\/(\d+)\/complete-assets-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$/); @@ -1277,6 +1373,8 @@ async function route(req, res) { } if (mSiteComplete) return handleCompleteSite(req, res, Number(mSiteComplete[1])); if (mSiteCompletePrompt) return handleCompletePrompt(req, res, Number(mSiteCompletePrompt[1])); + if (mSiteCompleteAssets) return handleCompleteAssets(req, res, Number(mSiteCompleteAssets[1])); + if (mSiteCompleteAssetsPrompt) return handleCompleteAssetsPrompt(req, res, Number(mSiteCompleteAssetsPrompt[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)); diff --git a/public/design-doc.html b/public/design-doc.html index f3db960..a523497 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -208,6 +208,7 @@ KPI(设备类型/实例/层级)资产概览本地计算随表格输入实时变化 设备资产表格(类型/数量/命名规则/操作)配置设备类型与数量PUT /api/db/sites行内编辑即保存;可增删行 资产层级预览预览「站点→类型→实例」三级结构本地计算按命名规则展开实例:规则含 # 时替换序号(如 CH-##→CH-01);规则为纯前缀时自动补 -01/-02… 序号(如 beng-→beng-01、guolu→guolu-01) 3 层结构为展示占位,资产层级建模尚未实现 + AI 智能补全根据站点名称 / 领域 / 描述 / 补充要求,分析并补充设备类型、数量与命名规则到资产配置列表(位于用户已填项下方,不改动已填项)POST /api/db/sites/:id/complete-assets(流式 NDJSON)「开始补全」后控制台流式输出思考过程;结果追加到资产配置列表(按模型给出的数量预设与命名规则);资产层级预览随之重算;「查看提示词」可看完整提示词 设备补充质量将按反馈持续优化

5.5 功能与菜单(functions)

@@ -342,6 +343,7 @@
  • 2026-08-18 修复右下角 AI 悬浮按钮拖动时常失效的问题:图标改为不可原生拖拽、事件直接落在按钮上,避免浏览器图片拖拽劫持 Pointer 拖拽手势;按下拖动时不再有 1px 移位。
  • 2026-08-18 修复「下载 Markdown」导入 Azure DevOps Wiki 后仅显示到 §4.2 的问题:代码块改以 4 空格缩进式代码块导出(不再用 ``` 围栏),避免围栏闭合异常吞掉后续章节;表格单元格内将 < 与 > 转义为 HTML 实体,避免 Azure 把 <select> 当成未闭合标签吞掉后续内容。
  • 2026-08-18 新增「AI 智能补全」功能:站点基本属性页「填写建议」下方新增面板,点击「开始补全」后基于站点名称 / 所属领域 / 站点基本情况描述,由 AI 流式分析并自动写入「补充要求」字段;「查看提示词」可查看完整组合提示词(后端 POST /api/db/sites/:id/complete 流式、GET .../complete-prompt 取提示词)。
  • +
  • 2026-08-18 扩展「AI 智能补全」至 Asset 资产设置页:右侧新增与基本属性同款的「填写建议 + AI 智能补全」栏;点击「开始补全」后基于站点名称 / 领域 / 描述 / 补充要求分析并补充设备类型、数量预设与命名规则,追加到用户已填项下方(不改动已有项),资产层级预览随之重算(后端 POST /api/db/sites/:id/complete-assets 流式、GET .../complete-assets-prompt 取提示词)。
    • -- 2.43.0