|
|
@@ -118,6 +118,7 @@ async function validate() {
|
|
|
}
|
|
|
|
|
|
function activateTab() {
|
|
|
+ document.body.classList.toggle("workspace-mode", state.activeTab !== "overview");
|
|
|
$$(".tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.tab === state.activeTab));
|
|
|
renderActive().catch((error) => {
|
|
|
$("#content").innerHTML = `<div class="panel inline-empty">加载失败:${esc(error.message)}</div>`;
|
|
|
@@ -127,7 +128,8 @@ function activateTab() {
|
|
|
|
|
|
async function renderActive() {
|
|
|
if (!state.projectId) return;
|
|
|
- const renderers = {overview: renderOverview, model: renderModel, assertions: renderAssertions, evidence: renderEvidence, issues: renderIssues, agent: renderOntologyBuilder};
|
|
|
+ document.body.classList.toggle("workspace-mode", state.activeTab !== "overview");
|
|
|
+ const renderers = {overview: renderOverview, model: renderModel, assertions: renderAssertions, evidence: renderEvidence, issues: renderIssues, agent: renderOntologyBuilder, decision: renderGovernanceAssistant};
|
|
|
$("#content").innerHTML = '<div class="panel inline-empty">正在加载治理数据…</div>';
|
|
|
await renderers[state.activeTab]();
|
|
|
}
|
|
|
@@ -319,38 +321,110 @@ async function renderModel() {
|
|
|
const graph = await request(`/projects/${state.projectId}/graph`);
|
|
|
const elements = graph.nodes || [];
|
|
|
$("#content").innerHTML = `
|
|
|
- <div class="model-intro panel"><div><span>ONTOLOGY EXPLORER</span><h2>M3–M0 分层本体图</h2><p>M3 是系统稳定语法;M2 从标准类型库按上传文件涉及的类型装配;M1/M0 来自文件抽取与智能体语义融合。</p></div><div class="view-switch"><button class="button small primary" data-model-view="graph">图谱视图</button><button class="button small ghost" data-model-view="table">表格视图</button></div></div>
|
|
|
- <div class="toolbar model-toolbar"><select id="layer-filter"><option value="">全部层级</option>${["M3","M2","M1","M0"].map(x => `<option>${x}</option>`).join("")}</select><select id="profile-filter"><option value="">全部 Profile</option>${[...new Set(elements.map(x => x.profile))].filter(Boolean).map(x => `<option value="${esc(x)}">${esc(modelProfileLabel(x))}</option>`).join("")}</select><select id="node-status-filter"><option value="">全部节点状态</option><option>candidate</option><option>published</option></select><select id="edge-status-filter"><option value="">全部关系状态</option><option>pending</option><option>accepted</option></select><input id="model-search" placeholder="搜索名称、类型或 URI"><span class="spacer"></span><button id="download-jsonld" class="button ghost">导出 JSON-LD</button></div>
|
|
|
+ <div class="model-intro panel"><div><span>ONTOLOGY EXPLORER</span><h2>M3–M0 可缩放分层图</h2><p>恢复大尺寸节点;可使用工具栏或鼠标滚轮缩放,按住画布拖动平移,并随时重置到 100% 或适配全图。</p></div><div class="view-switch"><button class="button small primary" data-model-view="graph">图谱视图</button><button class="button small ghost" data-model-view="table">表格视图</button></div></div>
|
|
|
+ <div class="toolbar model-toolbar"><select id="relation-scope"><option value="typing">类型主链(推荐)</option><option value="semantic">业务语义关系</option><option value="all">全部关系</option></select><select id="layer-filter"><option value="">全部层级</option>${["M3","M2","M1","M0"].map(x => `<option>${x}</option>`).join("")}</select><select id="profile-filter"><option value="">全部 Profile</option>${[...new Set(elements.map(x => x.profile))].filter(Boolean).map(x => `<option value="${esc(x)}">${esc(modelProfileLabel(x))}</option>`).join("")}</select><select id="node-status-filter"><option value="">全部节点状态</option><option>candidate</option><option>published</option></select><select id="edge-status-filter"><option value="">全部关系状态</option><option>pending</option><option>accepted</option></select><input id="model-search" placeholder="搜索节点"><span class="spacer"></span><button id="download-jsonld" class="button ghost">导出 JSON-LD</button></div>
|
|
|
<div class="ontology-legend panel"><div><b>Profile</b><span class="legend-dot business"></span>业务<span class="legend-dot software"></span>软件<span class="legend-dot governance"></span>治理<span class="legend-dot meta"></span>元模型</div><div><b>节点</b><span class="legend-node candidate"></span>candidate 虚线<span class="legend-node published"></span>published 实线</div><div><b>关系</b><span class="legend-edge pending"></span>pending 虚线<span class="legend-edge accepted"></span>accepted 实线</div></div>
|
|
|
- <section id="graph-view" class="ontology-graph-shell panel"><div id="ontology-graph"></div></section>
|
|
|
+ <section id="graph-view" class="ontology-graph-shell panel"><div class="graph-canvas-toolbar"><div><button class="button small ghost" data-graph-zoom="out" title="缩小">−</button><button class="button small ghost" data-graph-zoom="in" title="放大">+</button><button class="button small ghost" data-graph-zoom="reset">100%</button><button class="button small ghost" data-graph-zoom="fit">适配全图</button></div><span id="graph-zoom-label">100%</span><small>滚轮缩放 · 按住画布拖动</small></div><div id="ontology-viewport" class="ontology-viewport"><div id="ontology-graph"></div></div></section>
|
|
|
<section id="table-view" class="hidden"><div class="table-wrap"><table><thead><tr><th>层级</th><th>名称</th><th>类型</th><th>Profile</th><th>状态</th><th>来源</th><th>关系</th></tr></thead><tbody id="element-rows"></tbody></table></div></section>
|
|
|
<p id="model-stat" class="muted model-stat"></p>`;
|
|
|
+ const graphViewState = {scale: 1, x: 18, y: 18, dragging: false, moved: false, startX: 0, startY: 0, originX: 0, originY: 0};
|
|
|
+ const applyGraphTransform = () => {
|
|
|
+ const canvas = $("#ontology-graph");
|
|
|
+ if (!canvas) return;
|
|
|
+ canvas.style.transform = `translate(${graphViewState.x}px, ${graphViewState.y}px) scale(${graphViewState.scale})`;
|
|
|
+ $("#graph-zoom-label").textContent = `${Math.round(graphViewState.scale * 100)}%`;
|
|
|
+ };
|
|
|
+ const setGraphScale = (nextScale, clientX, clientY) => {
|
|
|
+ const viewport = $("#ontology-viewport");
|
|
|
+ const rect = viewport.getBoundingClientRect();
|
|
|
+ const x = clientX == null ? rect.width / 2 : clientX - rect.left;
|
|
|
+ const y = clientY == null ? rect.height / 2 : clientY - rect.top;
|
|
|
+ const previous = graphViewState.scale;
|
|
|
+ graphViewState.scale = Math.min(2.5, Math.max(.3, nextScale));
|
|
|
+ graphViewState.x = x - ((x - graphViewState.x) / previous) * graphViewState.scale;
|
|
|
+ graphViewState.y = y - ((y - graphViewState.y) / previous) * graphViewState.scale;
|
|
|
+ applyGraphTransform();
|
|
|
+ };
|
|
|
+ const resetGraphView = () => {
|
|
|
+ Object.assign(graphViewState, {scale:1, x:18, y:18});
|
|
|
+ applyGraphTransform();
|
|
|
+ };
|
|
|
+ const fitGraphView = () => {
|
|
|
+ const viewport = $("#ontology-viewport"), svg = $("#ontology-graph svg");
|
|
|
+ if (!viewport || !svg) return;
|
|
|
+ const view = svg.viewBox.baseVal;
|
|
|
+ graphViewState.scale = Math.min(1, Math.max(.3, Math.min((viewport.clientWidth - 32) / view.width, (viewport.clientHeight - 32) / view.height)));
|
|
|
+ graphViewState.x = (viewport.clientWidth - view.width * graphViewState.scale) / 2;
|
|
|
+ graphViewState.y = Math.max(16, (viewport.clientHeight - view.height * graphViewState.scale) / 2);
|
|
|
+ applyGraphTransform();
|
|
|
+ };
|
|
|
const draw = () => {
|
|
|
const layer = $("#layer-filter").value;
|
|
|
const profile = $("#profile-filter").value;
|
|
|
const nodeStatus = $("#node-status-filter").value;
|
|
|
const edgeStatus = $("#edge-status-filter").value;
|
|
|
+ const relationScope = $("#relation-scope").value;
|
|
|
const query = $("#model-search").value.trim().toLowerCase();
|
|
|
const filtered = elements.filter(node => (!layer || node.layer === layer) && (!profile || node.profile === profile) && (!nodeStatus || node.status === nodeStatus) && (!query || `${node.name} ${node.kind} ${node.uri}`.toLowerCase().includes(query)));
|
|
|
const visibleIds = new Set(filtered.map(node => node.id));
|
|
|
- const visibleEdges = (graph.edges || []).filter(edge => visibleIds.has(edge.subject_id) && visibleIds.has(edge.object_id) && (!edgeStatus || edge.review_status === edgeStatus));
|
|
|
+ const visibleEdges = (graph.edges || []).filter(edge => visibleIds.has(edge.subject_id) && visibleIds.has(edge.object_id) && (!edgeStatus || edge.review_status === edgeStatus) && (relationScope === "all" || (relationScope === "typing" ? edge.predicate === "INSTANCE_OF" : edge.predicate !== "INSTANCE_OF")));
|
|
|
$("#ontology-graph").innerHTML = ontologyGraphMarkup(filtered, visibleEdges);
|
|
|
$("#element-rows").innerHTML = filtered.map(node => `<tr><td>${badge(node.layer)}</td><td><button class="link-button" data-model-node="${esc(node.id)}">${esc(node.name)}</button><br><small class="code muted">${esc(node.uri)}</small></td><td>${esc(node.kind)}</td><td>${esc(modelProfileLabel(node.profile))}</td><td>${badge(node.status)}</td><td>${esc(node.source || "—")}</td><td>${(graph.edges || []).filter(edge => edge.subject_id === node.id || edge.object_id === node.id).length}</td></tr>`).join("") || '<tr><td colspan="7" class="inline-empty">没有匹配的本体节点</td></tr>';
|
|
|
$("#model-stat").textContent = `当前显示 ${filtered.length} / ${elements.length} 个节点,${visibleEdges.length} / ${(graph.edges || []).length} 条关系。点击任一节点查看属性、来源、证据、置信度与审核状态。`;
|
|
|
$$('[data-model-node]').forEach(element => {
|
|
|
- element.addEventListener("click", () => showOntologyNode(element.dataset.modelNode, graph));
|
|
|
+ element.addEventListener("click", () => { if (!graphViewState.moved) showOntologyNode(element.dataset.modelNode, graph); });
|
|
|
element.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") showOntologyNode(element.dataset.modelNode, graph); });
|
|
|
});
|
|
|
};
|
|
|
- ["#layer-filter", "#profile-filter", "#node-status-filter", "#edge-status-filter", "#model-search"].forEach(selector => $(selector).addEventListener("input", draw));
|
|
|
+ ["#relation-scope", "#layer-filter", "#profile-filter", "#node-status-filter", "#edge-status-filter", "#model-search"].forEach(selector => $(selector).addEventListener("input", draw));
|
|
|
$$('[data-model-view]').forEach(button => button.addEventListener("click", () => {
|
|
|
const graphView = button.dataset.modelView === "graph";
|
|
|
$("#graph-view").classList.toggle("hidden", !graphView);
|
|
|
$("#table-view").classList.toggle("hidden", graphView);
|
|
|
$$('[data-model-view]').forEach(item => item.className = `button small ${item === button ? "primary" : "ghost"}`);
|
|
|
}));
|
|
|
+ $$('[data-graph-zoom]').forEach(button => button.addEventListener("click", () => {
|
|
|
+ const action = button.dataset.graphZoom;
|
|
|
+ if (action === "in") setGraphScale(graphViewState.scale * 1.2);
|
|
|
+ if (action === "out") setGraphScale(graphViewState.scale / 1.2);
|
|
|
+ if (action === "reset") resetGraphView();
|
|
|
+ if (action === "fit") fitGraphView();
|
|
|
+ }));
|
|
|
+ const viewport = $("#ontology-viewport");
|
|
|
+ viewport.addEventListener("wheel", event => {
|
|
|
+ event.preventDefault();
|
|
|
+ setGraphScale(graphViewState.scale * (event.deltaY < 0 ? 1.12 : 1 / 1.12), event.clientX, event.clientY);
|
|
|
+ }, {passive:false});
|
|
|
+ viewport.addEventListener("pointerdown", event => {
|
|
|
+ graphViewState.dragging = true;
|
|
|
+ graphViewState.moved = false;
|
|
|
+ graphViewState.startX = event.clientX;
|
|
|
+ graphViewState.startY = event.clientY;
|
|
|
+ graphViewState.originX = graphViewState.x;
|
|
|
+ graphViewState.originY = graphViewState.y;
|
|
|
+ viewport.setPointerCapture(event.pointerId);
|
|
|
+ viewport.classList.add("dragging");
|
|
|
+ });
|
|
|
+ viewport.addEventListener("pointermove", event => {
|
|
|
+ if (!graphViewState.dragging) return;
|
|
|
+ const dx = event.clientX - graphViewState.startX, dy = event.clientY - graphViewState.startY;
|
|
|
+ if (Math.abs(dx) + Math.abs(dy) > 4) graphViewState.moved = true;
|
|
|
+ graphViewState.x = graphViewState.originX + dx;
|
|
|
+ graphViewState.y = graphViewState.originY + dy;
|
|
|
+ applyGraphTransform();
|
|
|
+ });
|
|
|
+ const endDrag = event => {
|
|
|
+ if (!graphViewState.dragging) return;
|
|
|
+ graphViewState.dragging = false;
|
|
|
+ viewport.classList.remove("dragging");
|
|
|
+ if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);
|
|
|
+ setTimeout(() => { graphViewState.moved = false; }, 0);
|
|
|
+ };
|
|
|
+ viewport.addEventListener("pointerup", endDrag);
|
|
|
+ viewport.addEventListener("pointercancel", endDrag);
|
|
|
$("#download-jsonld").addEventListener("click", downloadJsonLd);
|
|
|
draw();
|
|
|
+ resetGraphView();
|
|
|
}
|
|
|
|
|
|
async function showImpact(id) {
|
|
|
@@ -429,6 +503,72 @@ async function resolveIssue(id) {
|
|
|
} catch (error) { toast(error.message, true); }
|
|
|
}
|
|
|
|
|
|
+function governanceAnswerMarkup(result) {
|
|
|
+ if (!result?.decision) return "";
|
|
|
+ const decision = result.decision;
|
|
|
+ const summary = decision.summary || {};
|
|
|
+ const scope = result.context_scope || {};
|
|
|
+ const statusLabel = {ALLOW:"允许进入受控变更", BLOCKED:"当前不允许变更", INSUFFICIENT_EVIDENCE:"证据不足"}[decision.status] || decision.status;
|
|
|
+ return `<section class="qa-answer ${esc(decision.status.toLowerCase())}">
|
|
|
+ <div class="qa-answer-head"><span class="qa-avatar">04</span><div><small>治理决策智能体</small><h3>${esc(result.answer || decision.recommendation)}</h3><p>识别对象:${esc(decision.target?.name || "未识别")} · ${esc(decision.target?.uri || "请补充资产标识")}</p></div><strong>${esc(statusLabel)}</strong></div>
|
|
|
+ <div class="qa-summary"><article><b>${Number(summary.passed || 0)}</b><span>通过</span></article><article><b>${Number(summary.failed || 0)}</b><span>阻塞</span></article><article><b>${Number(summary.unknown || 0)}</b><span>未知</span></article><article><b>${Number(decision.evidence_ids?.length || 0)}</b><span>引用证据</span></article></div>
|
|
|
+ <div class="qa-checks">${(decision.checks || []).map(item => `<article class="${esc(item.state)}"><span>${item.state === "passed" ? "✓" : item.state === "failed" ? "×" : "?"}</span><div><strong>${esc(item.label)}</strong><small>${esc(item.message)}</small></div>${badge(item.state)}</article>`).join("")}</div>
|
|
|
+ <div class="qa-provenance"><span>本次读取 ${Number(scope.included_assets || 0)} 个本体资产、${Number(scope.included_relations || 0)} 条关系、${Number(scope.included_evidence || 0)} 份证据</span><span>结论由确定性门禁产生 · 运行 ${esc(result.run_id || decision.run_id || "—")}</span></div>
|
|
|
+ </section>`;
|
|
|
+}
|
|
|
+
|
|
|
+async function renderGovernanceAssistant() {
|
|
|
+ const [versionResult, runResult] = await Promise.all([
|
|
|
+ request(`/projects/${state.projectId}/ontology-versions`),
|
|
|
+ request(`/projects/${state.projectId}/agent-runs`),
|
|
|
+ ]);
|
|
|
+ const versions = versionResult.versions || [];
|
|
|
+ const qaRuns = (runResult.runs || []).filter(run => run.source_type === "governance-question");
|
|
|
+ const latest = qaRuns.find(run => run.status === "completed")?.result_json || null;
|
|
|
+ const ready = Boolean(versions.length);
|
|
|
+ const example = isDemoProject() ? "dcp_user.mobile 是否可以迁移并删除?" : "customer.mobile 字段现在可以下线吗?";
|
|
|
+ $("#content").innerHTML = `<div class="governance-qa-layout">
|
|
|
+ <section class="qa-workspace panel">
|
|
|
+ <div class="qa-hero"><div><span>AGENT 04 · GOVERNANCE DECISION</span><h2>向治理决策智能体提问</h2><p>第四个智能体独立读取已发布本体、语义断言、质量报告和审批证据,再调用确定性门禁回答;不会修改本体。</p></div><div class="qa-ready ${ready ? "ready" : "waiting"}"><i></i><strong>${ready ? `本体 ${esc(versions[0].label)} 已就绪` : "等待本体发布"}</strong><small>${ready ? `${date(versions[0].created_at)} · 可开始治理问答` : "请先到“本体构建”运行前三个智能体并发布"}</small></div></div>
|
|
|
+ <div class="agent-separation"><article><b>01–03</b><div><strong>本体构建智能体组</strong><small>文件 → 候选本体 → 校验 → 人工发布</small></div><span>只写本体</span></article><i>→</i><article class="active"><b>04</b><div><strong>治理决策智能体</strong><small>问题 → 证据检索 → 确定性门禁 → 回答</small></div><span>只读本体</span></article></div>
|
|
|
+ <div class="qa-conversation">
|
|
|
+ <article class="qa-message user"><span>你</span><div><small>示例问题</small><p>${esc(example)}</p></div></article>
|
|
|
+ ${latest ? governanceAnswerMarkup(latest) : '<article class="qa-message agent"><span>04</span><div><small>治理决策智能体</small><p>请告诉我你想治理的字段以及动作。例如:“dcp_user.mobile 是否可以迁移并删除?”</p></div></article>'}
|
|
|
+ </div>
|
|
|
+ <form id="governance-question-form" class="qa-form">
|
|
|
+ <textarea id="governance-question" ${ready ? "" : "disabled"} maxlength="2000" placeholder="请输入治理问题,建议包含 schema.table.column 等资产标识">${esc(example)}</textarea>
|
|
|
+ <div><label class="inline-check"><input id="question-include-pending" type="checkbox" checked> 评估时纳入待审核语义断言</label><button id="ask-governance" class="button primary" type="submit" ${ready ? "" : "disabled"}>询问第 4 个智能体</button></div>
|
|
|
+ </form>
|
|
|
+ <div class="qa-examples"><span>你还可以问:</span><button data-governance-example="dcp_user.mobile 当前被哪些治理门禁阻塞?">当前有哪些阻塞?</button><button data-governance-example="dcp_user.mobile 的迁移证据是否充分?">迁移证据充分吗?</button><button data-governance-example="dcp_user.mobile 是否已经完成必要审批?">必要审批完成了吗?</button></div>
|
|
|
+ </section>
|
|
|
+ <aside class="qa-history panel"><div class="section-head"><div><h2>问答审计记录</h2><p>问题、目标、证据范围和结论均留痕</p></div><span class="step-pill">AUDIT</span></div>${qaRuns.length ? `<div class="trace">${qaRuns.map(run => `<div class="trace-step"><strong>${esc(run.result_json?.question || "治理问题")}</strong><small>${date(run.created_at)} · ${esc(run.result_json?.decision?.status || run.status)}</small><br><button class="link-button" data-run="${esc(run.id)}">查看决策轨迹</button></div>`).join("")}</div>` : '<div class="inline-empty">尚无治理问答记录</div>'}</aside>
|
|
|
+ </div>`;
|
|
|
+ $("#governance-question-form").addEventListener("submit", askGovernanceQuestion);
|
|
|
+ $$('[data-governance-example]').forEach(button => button.addEventListener("click", () => {
|
|
|
+ $("#governance-question").value = button.dataset.governanceExample;
|
|
|
+ $("#governance-question").focus();
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+async function askGovernanceQuestion(event) {
|
|
|
+ event.preventDefault();
|
|
|
+ const question = $("#governance-question").value.trim();
|
|
|
+ if (!question) { toast("请输入治理问题", true); return; }
|
|
|
+ const button = $("#ask-governance");
|
|
|
+ setBusy(button, true, "正在读取本体与证据…");
|
|
|
+ try {
|
|
|
+ const result = await request(`/projects/${state.projectId}/governance-agent/ask`, {method:"POST", body:JSON.stringify({
|
|
|
+ question,
|
|
|
+ include_pending_assertions:Boolean($("#question-include-pending").checked),
|
|
|
+ persist:true,
|
|
|
+ })});
|
|
|
+ await renderGovernanceAssistant();
|
|
|
+ $(".qa-conversation").innerHTML = `<article class="qa-message user"><span>你</span><div><small>本次问题</small><p>${esc(question)}</p></div></article>${governanceAnswerMarkup(result)}`;
|
|
|
+ toast(`治理决策完成:${result.decision.status}`);
|
|
|
+ } catch (error) { toast(error.message, true); }
|
|
|
+ finally { setBusy(button, false, "询问第 4 个智能体"); }
|
|
|
+}
|
|
|
+
|
|
|
async function renderAgent() {
|
|
|
const [runResult, llm] = await Promise.all([
|
|
|
request(`/projects/${state.projectId}/agent-runs`),
|
|
|
@@ -564,14 +704,13 @@ function buildDiffMarkup(build) {
|
|
|
</div>`;
|
|
|
}
|
|
|
|
|
|
-function buildStagesMarkup(build, published = false) {
|
|
|
+function buildStagesMarkup() {
|
|
|
const stages = [
|
|
|
["01", "资产构建智能体", "解析 DDL、OpenAPI 与清单,形成结构资产"],
|
|
|
["02", "语义融合智能体", "识别业务对象、责任、敏感等级与跨源关系"],
|
|
|
["03", "本体校验智能体", "检查标识、关系端点与谓词,并生成 Diff"],
|
|
|
- ["04", "治理决策智能体", published ? "读取已发布本体和当前证据,执行确定性门禁" : "等待人工发布候选本体后执行"],
|
|
|
];
|
|
|
- return `<div class="build-stages">${stages.map((item, index) => `<article class="${index < 3 || published ? "done" : "waiting"}"><b>${item[0]}</b><div><strong>${item[1]}</strong><small>${item[2]}</small></div><span>${index < 3 || published ? "✓" : "待发布"}</span></article>`).join("")}</div>`;
|
|
|
+ return `<div class="build-stages build-stages-three">${stages.map(item => `<article class="done"><b>${item[0]}</b><div><strong>${item[1]}</strong><small>${item[2]}</small></div><span>✓</span></article>`).join("")}</div>`;
|
|
|
}
|
|
|
|
|
|
function candidateBuildMarkup(build) {
|
|
|
@@ -586,24 +725,19 @@ function candidateBuildMarkup(build) {
|
|
|
</section>`;
|
|
|
}
|
|
|
|
|
|
-function publishedHistoryMarkup(build, version, demo) {
|
|
|
- const decision = demo?.gate;
|
|
|
- const decisionCard = decision ? `<div class="decision-result ${esc(decision.status.toLowerCase())}"><div><span>数据库当前治理门禁</span><strong>${esc(decision.status)}</strong><small>${esc(decision.target?.name || demo.asset_name || "")}</small></div><p>${esc(decision.recommendation)}</p><div class="decision-checks">${decision.checks.map(item => `<span class="${esc(item.state)}">${item.state === "passed" ? "✓" : item.state === "failed" ? "×" : "?"} ${esc(item.label)}</span>`).join("")}</div></div>` : "";
|
|
|
- return `<section class="run-complete build-result-card published-build"><div class="run-complete-head"><span>✓</span><div><strong>当前本体 ${esc(version?.label || "v1")} 已发布</strong><small>批次 ${esc(build.id)} · ${Number(build.element_ids?.length || 0)} 个元素 · ${Number(build.assertion_ids?.length || 0)} 条关系</small></div></div>${buildStagesMarkup(build, true)}${buildDiffMarkup(build)}${decisionCard}<div class="button-row"><button class="button secondary" data-open-tab="model">查看正式本体</button><button class="button ghost" data-open-tab="assertions">复核语义断言</button></div></section>`;
|
|
|
+function publishedHistoryMarkup(build, version) {
|
|
|
+ return `<section class="run-complete build-result-card published-build"><div class="run-complete-head"><span>✓</span><div><strong>当前本体 ${esc(version?.label || "v1")} 已发布</strong><small>批次 ${esc(build.id)} · ${Number(build.element_ids?.length || 0)} 个元素 · ${Number(build.assertion_ids?.length || 0)} 条关系</small></div></div>${buildStagesMarkup()}${buildDiffMarkup(build)}<div class="build-next-step"><div><strong>本体构建已经完成</strong><small>治理决策不在构建阶段自动执行,请单独向第 4 个智能体提问。</small></div><button class="button secondary" data-open-tab="decision">进入治理问答</button><button class="button ghost" data-open-tab="model">查看正式本体</button></div></section>`;
|
|
|
}
|
|
|
|
|
|
function publishedBuildMarkup(result) {
|
|
|
- const decision = result.decision;
|
|
|
- const decisionCard = decision ? `<div class="decision-result ${esc(decision.status.toLowerCase())}"><div><span>动态治理门禁</span><strong>${esc(decision.status)}</strong><small>${esc(decision.target?.name || "")}</small></div><p>${esc(decision.recommendation)}</p><div class="decision-checks">${decision.checks.map(item => `<span class="${esc(item.state)}">${item.state === "passed" ? "✓" : item.state === "failed" ? "×" : "?"} ${esc(item.label)}</span>`).join("")}</div></div>` : "";
|
|
|
- return `<section class="run-complete build-result-card published-build"><div class="run-complete-head"><span>✓</span><div><strong>本体 ${esc(result.version?.label || "")} 已发布</strong><small>${Number(result.published?.elements || 0)} 个元素 · ${Number(result.published?.accepted_assertions || 0)} 条已接受关系 · ${Number(result.published?.pending_assertions || 0)} 条语义断言待复核</small></div></div>${buildStagesMarkup({}, Boolean(decision))}${decisionCard}<div class="button-row"><button class="button secondary" data-open-tab="model">查看正式本体</button><button class="button ghost" data-open-tab="assertions">复核语义断言</button><button class="button ghost" data-open-tab="issues">查看治理问题</button></div></section>`;
|
|
|
+ return `<section class="run-complete build-result-card published-build"><div class="run-complete-head"><span>✓</span><div><strong>本体 ${esc(result.version?.label || "")} 已发布</strong><small>${Number(result.published?.elements || 0)} 个元素 · ${Number(result.published?.accepted_assertions || 0)} 条已接受关系 · ${Number(result.published?.pending_assertions || 0)} 条语义断言待复核</small></div></div>${buildStagesMarkup()}<div class="build-next-step"><div><strong>前三个智能体的工作到此结束</strong><small>现在可以让独立的治理决策智能体读取正式本体并回答问题。</small></div><button class="button secondary" data-open-tab="decision">向第 4 个智能体提问</button><button class="button ghost" data-open-tab="model">查看正式本体</button></div></section>`;
|
|
|
}
|
|
|
|
|
|
async function renderOntologyBuilder() {
|
|
|
- const [buildResult, versionResult, llm, overviewResult] = await Promise.all([
|
|
|
+ const [buildResult, versionResult, llm] = await Promise.all([
|
|
|
request(`/projects/${state.projectId}/ontology-builds`),
|
|
|
request(`/projects/${state.projectId}/ontology-versions`),
|
|
|
request("/llm/status"),
|
|
|
- request(`/projects/${state.projectId}/overview`),
|
|
|
]);
|
|
|
const builds = buildResult.builds || [];
|
|
|
const versions = versionResult.versions || [];
|
|
|
@@ -613,19 +747,18 @@ async function renderOntologyBuilder() {
|
|
|
const latestPublished = builds.find(item => item.status === "published");
|
|
|
$("#content").innerHTML = `<div class="agent-grid ontology-builder">
|
|
|
<article class="section-card panel"><div class="section-head"><div><h2>从文件构建候选本体</h2><p>项目从零开始;上传什么,就构建什么,不使用预置项目本体和关系</p></div><span class="step-pill">BUILD</span></div>
|
|
|
- <div class="model-strip ${llm.configured ? "ready" : "offline"}"><div><span class="dot"></span><strong>${esc(llm.model)}</strong><small>${llm.configured ? "LongCat 负责语义融合,规则负责结构解析和最终门禁" : "未配置模型,将使用确定性语义融合"}</small></div><button id="test-llm" class="button small ghost" ${llm.configured ? "" : "disabled"}>测试模型连接</button></div>
|
|
|
+ <div class="model-strip ${llm.configured ? "ready" : "offline"}"><div><span class="dot"></span><strong>${esc(llm.model)}</strong><small>${llm.configured ? "LongCat 参与语义融合;结构解析和本体校验保持确定性" : "未配置模型,将使用确定性语义融合"}</small></div><button id="test-llm" class="button small ghost" ${llm.configured ? "" : "disabled"}>测试模型连接</button></div>
|
|
|
<form id="ontology-build-form" class="form-stack">
|
|
|
<label id="build-source-drop" class="source-drop" for="build-source-files"><input id="build-source-files" type="file" multiple accept=".sql,.json,.yaml,.yml,application/json,text/yaml,application/yaml"><span class="upload-icon">⇧</span><strong>选择或拖入 1–12 份治理文件</strong><small>推荐一次上传 DDL、OpenAPI、资产清单和治理证据包 · 单文件最大 512KB</small></label>
|
|
|
<div class="sample-files"><span>下载演示输入:</span><a href="/static/samples/01-dcp-user.sql" download>① DDL</a><a href="/static/samples/02-user-profile-openapi.yaml" download>② OpenAPI</a><a href="/static/samples/03-dcp-asset-inventory.json" download>③ 资产清单</a><a href="/static/samples/04-governance-evidence.json" download>④ 治理证据包</a></div>
|
|
|
<div id="build-file-list" class="build-file-list"><div class="inline-empty">尚未选择文件</div></div>
|
|
|
<div class="form-pair"><label>语义融合方式<select id="build-semantic-mode"><option value="auto">自动选择(推荐)</option><option value="llm" ${demoMode && llm.configured ? "selected" : ""}>LongCat-2.0</option><option value="deterministic">确定性规则</option></select></label><label>运行环境<select id="build-environment"><option value="production" ${demoMode ? "selected" : ""}>production</option><option value="development">development</option><option value="test">test</option></select></label></div>
|
|
|
- <label>发布后要判断的治理对象<input id="build-decision-target" value="${demoMode ? "dcp_user.mobile" : ""}" placeholder="例如:schema.table.column;留空则发布后不执行退役门禁"></label>
|
|
|
<label>构建说明<input id="build-instruction" value="${demoMode ? "从上传文件构建 DCP 手机号迁移本体,识别业务对象、敏感等级、Owner、质量规则与治理风险。" : "从全部上传文件构建项目候选本体,并标出不确定语义。"}"></label>
|
|
|
- <div class="button-row"><button id="run-build" class="button primary" type="submit">运行 3 个构建智能体</button><span class="muted">先生成候选本体和 Diff,人工发布后第 4 个智能体才给出治理决策</span></div>
|
|
|
+ <div class="button-row"><button id="run-build" class="button primary" type="submit">运行本体构建(3 个智能体协作)</button><span class="muted">只生成、校验和发布本体;不会自动运行治理决策</span></div>
|
|
|
</form>
|
|
|
- <div id="agent-result">${latestReview ? candidateBuildMarkup(latestReview) : latestPublished ? publishedHistoryMarkup(latestPublished, versions[0], overviewResult.demo) : ""}</div>
|
|
|
+ <div id="agent-result">${latestReview ? candidateBuildMarkup(latestReview) : latestPublished ? publishedHistoryMarkup(latestPublished, versions[0]) : ""}</div>
|
|
|
</article>
|
|
|
- <article class="section-card panel"><div class="section-head"><div><h2>构建批次与版本</h2><p>每次输入、Diff、审核发布和决策都可追溯</p></div><span class="step-pill">AUDIT</span></div>
|
|
|
+ <article class="section-card panel"><div class="section-head"><div><h2>构建批次与版本</h2><p>每次输入、Diff 和审核发布都可追溯</p></div><span class="step-pill">AUDIT</span></div>
|
|
|
<div class="version-summary"><strong>${versions.length}</strong><span>已发布本体版本</span><small>${versions[0] ? `当前 ${esc(versions[0].label)} · ${date(versions[0].created_at)}` : "尚未发布"}</small></div>
|
|
|
${builds.length ? `<div class="trace build-history">${builds.map(item => `<div class="trace-step"><strong>${esc(item.id)} · ${esc(item.status)}</strong><small>${date(item.created_at)} · ${Number(item.source_count || 0)} 份用户文件 · ${esc(item.semantic_mode)}</small><div class="history-diff"><span>+${Number(item.diff_json?.new_elements || 0)} 元素</span><span>+${Number(item.diff_json?.new_assertions || 0)} 关系</span>${item.status === "review_required" ? `<button class="link-button" data-publish-build="${esc(item.id)}">审核发布</button>` : ""}</div></div>`).join("")}</div>` : '<div class="inline-empty">尚无构建批次。选择文件开始构建。</div>'}
|
|
|
</article></div>`;
|
|
|
@@ -661,25 +794,24 @@ async function runOntologyBuild(event) {
|
|
|
form.append("semantic_mode", $("#build-semantic-mode").value);
|
|
|
form.append("environment", $("#build-environment").value);
|
|
|
form.append("instruction", $("#build-instruction").value);
|
|
|
- form.append("decision_target", $("#build-decision-target").value);
|
|
|
+ form.append("decision_target", "");
|
|
|
const result = await request(`/projects/${state.projectId}/ontology-builds/upload`, {method:"POST", body:form});
|
|
|
await renderOntologyBuilder();
|
|
|
$("#agent-result").innerHTML = candidateBuildMarkup(result);
|
|
|
toast(`候选本体已生成:新增 ${result.diff?.new_elements || 0} 个元素、${result.diff?.new_assertions || 0} 条关系`);
|
|
|
} catch (error) { toast(error.message, true); }
|
|
|
- finally { setBusy(button, false, "运行 3 个构建智能体"); }
|
|
|
+ finally { setBusy(button, false, "运行本体构建(3 个智能体协作)"); }
|
|
|
}
|
|
|
|
|
|
async function publishOntologyBuild(buildId) {
|
|
|
- const target = $("#build-decision-target")?.value || (isDemoProject() ? "dcp_user.mobile" : "");
|
|
|
const acceptSemantic = Boolean($("#accept-semantic")?.checked);
|
|
|
const button = document.querySelector(`[data-publish-build="${CSS.escape(buildId)}"]`);
|
|
|
- setBusy(button, true, "发布并决策中…");
|
|
|
+ setBusy(button, true, "正在发布本体…");
|
|
|
try {
|
|
|
- const result = await request(`/projects/${state.projectId}/ontology-builds/${buildId}/publish`, {method:"POST", body:JSON.stringify({reviewer:"web-console", accept_semantic_assertions:acceptSemantic, decision_target:target})});
|
|
|
+ const result = await request(`/projects/${state.projectId}/ontology-builds/${buildId}/publish`, {method:"POST", body:JSON.stringify({reviewer:"web-console", accept_semantic_assertions:acceptSemantic, decision_target:""})});
|
|
|
await renderOntologyBuilder();
|
|
|
$("#agent-result").innerHTML = publishedBuildMarkup(result);
|
|
|
- toast(`本体 ${result.version?.label || ""} 已发布${result.decision ? `,门禁结论 ${result.decision.status}` : ""}`);
|
|
|
+ toast(`本体 ${result.version?.label || ""} 已发布,可以前往治理问答`);
|
|
|
} catch (error) { toast(error.message, true); }
|
|
|
finally { setBusy(button, false, "审核并发布 vNext"); }
|
|
|
}
|
|
|
@@ -727,7 +859,7 @@ async function init() {
|
|
|
state.tenant = "local";
|
|
|
}
|
|
|
if (demoRequested || requestedTab) {
|
|
|
- state.activeTab = ["overview", "model", "assertions", "evidence", "issues", "agent"].includes(requestedTab) ? requestedTab : "overview";
|
|
|
+ state.activeTab = ["overview", "model", "assertions", "evidence", "issues", "agent", "decision"].includes(requestedTab) ? requestedTab : "overview";
|
|
|
}
|
|
|
$("#tenant-input").value = state.tenant;
|
|
|
$("#api-key-input").value = state.apiKey;
|