|
|
@@ -1,13 +1,5 @@
|
|
|
const API = "/api/v1/governance";
|
|
|
const DEMO_NAMESPACE = "https://onto.qm.cn/ontorefactor/demo/dcp-mobile/1.0";
|
|
|
-const DEMO_DDL = `CREATE TABLE dcp_user (
|
|
|
- user_id BIGINT PRIMARY KEY,
|
|
|
- tenant_id BIGINT NOT NULL,
|
|
|
- mobile VARCHAR(32),
|
|
|
- mobile_cipher VARCHAR(256),
|
|
|
- full_name VARCHAR(128),
|
|
|
- updated_at TIMESTAMP NOT NULL
|
|
|
-);`;
|
|
|
const state = {
|
|
|
tenant: localStorage.getItem("or-tenant") || "local",
|
|
|
apiKey: localStorage.getItem("or-api-key") || "",
|
|
|
@@ -15,6 +7,8 @@ const state = {
|
|
|
projects: [],
|
|
|
activeTab: "overview",
|
|
|
overview: null,
|
|
|
+ uploadedFile: null,
|
|
|
+ uploadedSource: null,
|
|
|
};
|
|
|
|
|
|
const $ = (selector, root = document) => root.querySelector(selector);
|
|
|
@@ -22,6 +16,7 @@ const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
|
|
const esc = (value) => String(value ?? "").replace(/[&<>'"]/g, (char) => ({"&":"&","<":"<",">":">","'":"'",'"':"""}[char]));
|
|
|
const date = (value) => value ? new Intl.DateTimeFormat("zh-CN", {dateStyle:"medium", timeStyle:"short"}).format(new Date(value)) : "—";
|
|
|
const badge = (value) => `<span class="badge ${esc(String(value).toLowerCase())}">${esc(value)}</span>`;
|
|
|
+const formatBytes = (value) => Number(value || 0) < 1024 ? `${Number(value || 0)} B` : `${(Number(value) / 1024).toFixed(1)} KB`;
|
|
|
const termLabel = (value) => ({
|
|
|
"source-normalizer": "输入规范化",
|
|
|
"asset-discovery-agent": "资产发现智能体",
|
|
|
@@ -68,6 +63,7 @@ async function loadProjects(preferredId) {
|
|
|
$("#project-hint").textContent = currentProject()?.description || "项目完全保存在本地 SQLite 中。";
|
|
|
$("#empty-state").classList.toggle("hidden", Boolean(state.projectId));
|
|
|
$("#content").classList.toggle("hidden", !state.projectId);
|
|
|
+ $$(".tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.tab === state.activeTab));
|
|
|
if (state.projectId) await renderActive();
|
|
|
}
|
|
|
|
|
|
@@ -307,35 +303,83 @@ async function renderAgent() {
|
|
|
]);
|
|
|
const runs = runResult.runs;
|
|
|
const demoMode = isDemoProject();
|
|
|
- const sourceName = demoMode ? "V3_8_0__mobile_cipher.sql" : "console-import";
|
|
|
- const sourceContent = demoMode ? DEMO_DDL : `CREATE TABLE customer_order (
|
|
|
- order_id INTEGER PRIMARY KEY,
|
|
|
- customer_id INTEGER NOT NULL,
|
|
|
- total_amount DECIMAL(12,2),
|
|
|
- created_at TIMESTAMP
|
|
|
-);`;
|
|
|
const instruction = demoMode ? "判断 dcp_user.mobile 是否可以迁移并删除;识别业务对象、敏感等级、Owner、质量规则和治理风险。" : "发现数据资产,推断业务语义并执行治理检查";
|
|
|
+ state.uploadedFile = null;
|
|
|
+ state.uploadedSource = null;
|
|
|
$("#content").innerHTML = `<div class="agent-grid">
|
|
|
- <article class="section-card panel"><div class="section-head"><div><h2>治理多智能体工作台</h2><p>由 LambdAgent 完成规范化、并行分析、LongCat 语义推理、门禁校验与证据落库</p></div></div>
|
|
|
+ <article class="section-card panel"><div class="section-head"><div><h2>上传治理输入</h2><p>从真实文件开始:系统先解析预检,再交给 LambdAgent 与 LongCat 分析</p></div><span class="step-pill">01 · INPUT</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>
|
|
|
- ${demoMode && runs.some(run => run.status === "completed" && run.llm_model) ? '<div class="demo-ready"><strong>✓ 真实模型结果已预热</strong><span>现场可直接查看右侧审计轨迹;需要时也可以再次实时运行。</span></div>' : ""}
|
|
|
+ ${demoMode && runs.some(run => run.status === "completed" && run.llm_model) ? '<div class="demo-ready"><strong>✓ 真实模型结果已预热</strong><span>现场可先上传文件实时分析,网络异常时再查看右侧预热轨迹。</span></div>' : ""}
|
|
|
<form id="agent-form" class="form-stack">
|
|
|
+ <label id="source-drop" class="source-drop" for="source-file">
|
|
|
+ <input id="source-file" type="file" accept=".sql,.json,.yaml,.yml,application/json,text/yaml,application/yaml">
|
|
|
+ <span class="upload-icon">⇧</span>
|
|
|
+ <strong>点击选择或拖入治理源文件</strong>
|
|
|
+ <small>支持 SQL DDL、OpenAPI JSON/YAML、资产清单 JSON/YAML · 最大 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></div>
|
|
|
+ <div id="source-preview" class="source-preview hidden"></div>
|
|
|
<div class="form-pair"><label>输入格式<select id="source-type"><option value="ddl">SQL DDL</option><option value="openapi">OpenAPI JSON/YAML</option><option value="inventory">资产清单 JSON/YAML</option></select></label><label>语义分析方式<select id="semantic-mode"><option value="auto">自动选择(推荐)</option><option value="llm" ${demoMode ? "selected" : ""}>强制 LongCat-2.0</option><option value="deterministic">仅确定性规则</option></select></label></div>
|
|
|
- <label>来源名称<input id="source-name" value="${esc(sourceName)}" required></label>
|
|
|
- <label>原始内容<textarea id="source-content" spellcheck="false">${esc(sourceContent)}</textarea></label>
|
|
|
+ <label>来源名称<input id="source-name" value="" placeholder="上传后自动填写,也可手动输入" required></label>
|
|
|
+ <label>原始内容<textarea id="source-content" spellcheck="false" placeholder="上传后在这里显示原文;也可以直接粘贴 DDL、OpenAPI 或资产清单"></textarea></label>
|
|
|
<label>运行说明<input id="agent-instruction" value="${esc(instruction)}"></label>
|
|
|
- <div class="button-row"><button id="run-agent" class="button primary" type="submit">运行智能体</button><span class="muted">自动结论以 pending 状态等待人工确认</span></div>
|
|
|
+ <div class="button-row"><button id="run-agent" class="button primary" type="submit">开始智能治理分析</button><span class="muted">模型断言以 pending 状态等待人工确认</span></div>
|
|
|
</form>
|
|
|
<div id="agent-result"></div>
|
|
|
</article>
|
|
|
- <article class="section-card panel"><div class="section-head"><div><h2>运行历史</h2><p>完整保留每一步输入输出摘要</p></div></div>
|
|
|
+ <article class="section-card panel"><div class="section-head"><div><h2>智能体审计轨迹</h2><p>每次输入、模型和执行步骤均可追溯</p></div><span class="step-pill">02 · AGENTS</span></div>
|
|
|
${runs.length ? `<div class="trace">${runs.map(x => `<div class="trace-step"><strong>${esc(x.source_type || "governance-analysis")} · ${esc(x.semantic_mode || "deterministic")}</strong><small>${date(x.created_at)} · ${esc(x.status)}${x.llm_model ? ` · ${esc(x.llm_model)} · ${Number(x.input_tokens || 0) + Number(x.output_tokens || 0)} tokens` : ""}</small><br><button class="link-button" data-run="${esc(x.id)}">查看执行轨迹</button></div>`).join("")}</div>` : '<div class="inline-empty">尚未运行智能体</div>'}
|
|
|
</article></div>`;
|
|
|
$("#agent-form").addEventListener("submit", runAgent);
|
|
|
$("#test-llm")?.addEventListener("click", testLLM);
|
|
|
+ $("#source-file").addEventListener("change", (event) => handleSourceFile(event.target.files?.[0]));
|
|
|
+ const drop = $("#source-drop");
|
|
|
+ ["dragenter", "dragover"].forEach(name => drop.addEventListener(name, (event) => { event.preventDefault(); drop.classList.add("dragging"); }));
|
|
|
+ ["dragleave", "drop"].forEach(name => drop.addEventListener(name, (event) => { event.preventDefault(); drop.classList.remove("dragging"); }));
|
|
|
+ drop.addEventListener("drop", (event) => handleSourceFile(event.dataTransfer?.files?.[0]));
|
|
|
+ $("#source-content").addEventListener("input", () => {
|
|
|
+ if (!state.uploadedFile) return;
|
|
|
+ state.uploadedFile = null;
|
|
|
+ state.uploadedSource = null;
|
|
|
+ $("#source-preview").innerHTML = '<strong>已切换为文本编辑模式</strong><span>运行时将提交当前编辑后的内容。</span>';
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSourceFile(file) {
|
|
|
+ if (!file) return;
|
|
|
+ const preview = $("#source-preview");
|
|
|
+ const drop = $("#source-drop");
|
|
|
+ state.uploadedFile = null;
|
|
|
+ state.uploadedSource = null;
|
|
|
+ preview.classList.remove("hidden", "accepted");
|
|
|
+ preview.innerHTML = `<strong>正在校验 ${esc(file.name)}…</strong><span>识别格式并预检文件结构</span>`;
|
|
|
+ drop.classList.add("loading");
|
|
|
+ try {
|
|
|
+ const form = new FormData();
|
|
|
+ form.append("file", file, file.name);
|
|
|
+ form.append("source_type", "auto");
|
|
|
+ const result = await request(`/projects/${state.projectId}/sources/preview`, {method:"POST", body:form});
|
|
|
+ state.uploadedFile = file;
|
|
|
+ state.uploadedSource = result;
|
|
|
+ $("#source-type").value = result.source_type;
|
|
|
+ $("#source-name").value = result.source_name;
|
|
|
+ $("#source-content").value = result.content;
|
|
|
+ const found = result.detected || {};
|
|
|
+ const facts = result.source_type === "ddl"
|
|
|
+ ? `${found.tables || 0} 张表 · ${found.columns || 0} 个字段`
|
|
|
+ : result.source_type === "openapi"
|
|
|
+ ? `${found.apis || 0} 个 API · ${found.schemas || 0} 个 Schema`
|
|
|
+ : `${found.tables || 0} 张表 · ${found.apis || 0} 个 API · ${found.schemas || 0} 个 Schema`;
|
|
|
+ preview.classList.add("accepted");
|
|
|
+ preview.innerHTML = `<div><strong>✓ 文件预检通过</strong><span>${esc(result.source_name)} · ${esc(formatBytes(result.file?.size_bytes))} · ${esc(result.source_type.toUpperCase())}</span></div><div class="preview-facts"><b>${esc(facts)}</b><small>${found.sensitive_fields?.length ? `发现敏感字段:${esc(found.sensitive_fields.join("、"))}` : "未命中敏感字段词典"}</small></div><code>SHA-256 ${esc((result.file?.sha256 || "").slice(0, 16))}…</code>`;
|
|
|
+ toast(`文件预检通过:${facts}`);
|
|
|
+ } catch (error) {
|
|
|
+ preview.innerHTML = `<strong>文件预检未通过</strong><span>${esc(error.message)}</span>`;
|
|
|
+ toast(error.message, true);
|
|
|
+ } finally { drop.classList.remove("loading"); }
|
|
|
}
|
|
|
|
|
|
async function testLLM() {
|
|
|
@@ -351,12 +395,25 @@ async function runAgent(event) {
|
|
|
event.preventDefault();
|
|
|
const button = $("#run-agent"); setBusy(button, true, "智能体运行中…");
|
|
|
try {
|
|
|
- const result = await request(`/projects/${state.projectId}/agent-runs/analyze`, {method:"POST", body:JSON.stringify({source_type:$("#source-type").value, source_name:$("#source-name").value, content:$("#source-content").value, instruction:$("#agent-instruction").value, semantic_mode:$("#semantic-mode").value, environment:"development", persist:true})});
|
|
|
+ let result;
|
|
|
+ if (state.uploadedFile) {
|
|
|
+ const form = new FormData();
|
|
|
+ form.append("file", state.uploadedFile, state.uploadedFile.name);
|
|
|
+ form.append("source_type", state.uploadedSource?.source_type || $("#source-type").value);
|
|
|
+ form.append("source_name", $("#source-name").value);
|
|
|
+ form.append("instruction", $("#agent-instruction").value);
|
|
|
+ form.append("semantic_mode", $("#semantic-mode").value);
|
|
|
+ form.append("environment", "development");
|
|
|
+ form.append("persist", "true");
|
|
|
+ result = await request(`/projects/${state.projectId}/agent-runs/upload`, {method:"POST", body:form});
|
|
|
+ } else {
|
|
|
+ result = await request(`/projects/${state.projectId}/agent-runs/analyze`, {method:"POST", body:JSON.stringify({source_type:$("#source-type").value, source_name:$("#source-name").value, content:$("#source-content").value, instruction:$("#agent-instruction").value, semantic_mode:$("#semantic-mode").value, environment:"development", persist:true})});
|
|
|
+ }
|
|
|
const summary = {...(result.result?.summary || {}), semantic_mode:result.semantic_mode, model:result.llm?.model || "规则引擎", tokens:Number(result.llm?.input_tokens || 0) + Number(result.llm?.output_tokens || 0), persisted:result.persisted};
|
|
|
- $("#agent-result").innerHTML = `<h3>运行结果</h3><div class="result-box">${esc(JSON.stringify(summary, null, 2))}</div>`;
|
|
|
+ $("#agent-result").innerHTML = `<section class="run-complete"><div class="run-complete-head"><span>✓</span><div><strong>治理分析完成并已落库</strong><small>${esc(summary.model)} · ${esc(summary.semantic_mode)} · ${Number(summary.tokens || 0)} tokens</small></div></div><div class="run-metrics"><button data-open-tab="model"><strong>${Number(summary.elements || 0)}</strong><span>本体元素</span></button><button data-open-tab="assertions"><strong>${Number(summary.assertions || 0)}</strong><span>语义断言</span></button><button data-open-tab="issues"><strong>${Number(summary.issues || 0)}</strong><span>治理问题</span></button></div><p>原始文件已进入证据中心,模型断言可人工复核,治理问题可继续形成整改闭环。</p><details><summary>查看原始运行摘要</summary><div class="result-box">${esc(JSON.stringify(summary, null, 2))}</div></details></section>`;
|
|
|
toast(`智能体已落库 ${result.persisted?.elements || 0} 个元素`);
|
|
|
} catch (error) { toast(error.message, true); }
|
|
|
- finally { setBusy(button, false, "运行智能体"); }
|
|
|
+ finally { setBusy(button, false, "开始智能治理分析"); }
|
|
|
}
|
|
|
|
|
|
async function showRun(id) {
|
|
|
@@ -389,10 +446,12 @@ document.addEventListener("click", (event) => {
|
|
|
});
|
|
|
|
|
|
async function init() {
|
|
|
- const demoRequested = new URLSearchParams(window.location.search).get("demo") === "1";
|
|
|
+ const query = new URLSearchParams(window.location.search);
|
|
|
+ const demoRequested = query.get("demo") === "1";
|
|
|
+ const requestedTab = query.get("tab");
|
|
|
if (demoRequested) {
|
|
|
state.tenant = "local";
|
|
|
- state.activeTab = "overview";
|
|
|
+ state.activeTab = ["overview", "model", "assertions", "evidence", "issues", "agent"].includes(requestedTab) ? requestedTab : "overview";
|
|
|
localStorage.setItem("or-tenant", "local");
|
|
|
}
|
|
|
$("#tenant-input").value = state.tenant;
|