Trae Assistant
init microgrid dispatch agent
971cb65
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<title>微电网调度闭环智能体</title>
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<style>
:root {
color-scheme: dark;
--bg: #070c1f;
--surface: rgba(18, 29, 58, 0.75);
--stroke: rgba(120, 145, 208, 0.2);
--text: #e6eeff;
--muted: #9fb2da;
--accent: #7dd3fc;
--accent-strong: #38bdf8;
--warn: #fda4af;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at top, #1a2a6c 0%, #070c1f 60%);
color: var(--text);
}
header {
padding: 28px 20px 12px;
text-align: center;
}
header h1 {
margin: 0 0 6px;
font-size: 26px;
}
header p {
margin: 0;
color: var(--muted);
font-size: 14px;
}
main {
max-width: 1100px;
margin: 0 auto;
padding: 16px 16px 40px;
display: grid;
gap: 16px;
}
.grid {
display: grid;
gap: 16px;
}
.grid.two {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.card {
background: var(--surface);
border: 1px solid var(--stroke);
border-radius: 16px;
padding: 16px;
backdrop-filter: blur(14px);
}
.card h3 {
margin-top: 0;
font-size: 18px;
}
label {
display: block;
margin-bottom: 12px;
color: var(--muted);
font-size: 13px;
}
input,
select,
textarea {
width: 100%;
margin-top: 6px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid transparent;
background: rgba(8, 16, 38, 0.7);
color: var(--text);
font-size: 14px;
}
textarea {
min-height: 88px;
resize: vertical;
}
button {
padding: 10px 16px;
background: linear-gradient(120deg, #0ea5e9, #6366f1);
border: none;
border-radius: 999px;
color: #fff;
font-weight: 600;
cursor: pointer;
width: 100%;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pill {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
background: rgba(125, 211, 252, 0.15);
color: var(--accent);
font-size: 12px;
}
.kpi {
display: grid;
gap: 8px;
}
.kpi span {
color: var(--muted);
font-size: 13px;
}
.kpi strong {
font-size: 16px;
}
.log {
background: rgba(10, 18, 40, 0.6);
border: 1px solid var(--stroke);
border-radius: 12px;
padding: 10px 12px;
margin-bottom: 8px;
}
.log header {
padding: 0;
text-align: left;
font-size: 12px;
color: var(--muted);
margin-bottom: 6px;
}
.warn {
color: var(--warn);
}
.cycle-list button {
width: 100%;
margin-bottom: 8px;
background: rgba(56, 189, 248, 0.16);
color: var(--text);
}
.toolbar {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.stat-grid {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.stat-card {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid var(--stroke);
background: rgba(10, 16, 36, 0.55);
}
.stat-card span {
display: block;
font-size: 12px;
color: var(--muted);
}
.stat-card strong {
font-size: 18px;
}
@media (max-width: 720px) {
header h1 {
font-size: 22px;
}
main {
padding: 12px;
}
}
</style>
</head>
<body>
<header>
<h1>微电网调度闭环智能体</h1>
<p>面向新能源微电网的推理/决策/行动/校验/迭代与回放系统</p>
</header>
<main>
<section class="grid two">
<article class="card">
<h3>新建调度任务</h3>
<form id="cycleForm">
<label>
任务标题
<input name="title" placeholder="例如:园区A峰值调度" required />
</label>
<label>
需求负荷(MW)
<input name="demand_mw" type="number" step="0.1" value="120" required />
</label>
<label>
光伏出力(MW)
<input name="solar_mw" type="number" step="0.1" value="40" required />
</label>
<label>
风电出力(MW)
<input name="wind_mw" type="number" step="0.1" value="30" required />
</label>
<label>
储能容量(MWh)
<input name="storage_mwh" type="number" step="0.1" value="80" required />
</label>
<label>
储能SOC(0-1)
<input name="storage_soc" type="number" step="0.01" value="0.45" required />
</label>
<label>
优先级策略
<select name="priority">
<option value="成本优先">成本优先</option>
<option value="碳优先">碳优先</option>
<option value="可靠性优先">可靠性优先</option>
</select>
</label>
<label>
业务备注(支持Markdown)
<textarea name="notes" placeholder="例如:需要保障冷链仓储负荷、可接受需求响应10%"></textarea>
</label>
<button type="submit" id="runBtn">生成闭环调度</button>
</form>
<div style="margin-top: 14px" class="toolbar">
<button type="button" id="suggestBtn">生成建议场景</button>
<button type="button" id="demoBtn">一键演示</button>
</div>
</article>
<article class="card">
<h3>闭环结果概览</h3>
<div id="overview">
<span class="pill">等待调度</span>
<div class="kpi" style="margin-top: 12px">
<span>成本</span>
<strong id="kpiCost">-</strong>
<span>碳排影响</span>
<strong id="kpiCarbon">-</strong>
<span>可靠性</span>
<strong id="kpiReliability">-</strong>
</div>
</div>
<div style="margin-top: 14px">
<h4>记忆快照</h4>
<div id="memoryBox" class="log"></div>
</div>
</article>
</section>
<section class="grid two">
<article class="card">
<h3>调度方案与校验</h3>
<div id="planBox" class="log"></div>
<div id="riskBox" class="log"></div>
<div id="dispatchBox" class="log"></div>
<div id="validationBox" class="log"></div>
</article>
<article class="card">
<h3>闭环日志回放</h3>
<div id="logList"></div>
</article>
</section>
<section class="grid two">
<article class="card">
<h3>历史调度</h3>
<div id="cycleList" class="cycle-list"></div>
</article>
<article class="card">
<h3>Markdown 预览</h3>
<div id="markdownPreview" class="log"></div>
</article>
</section>
<section class="grid two">
<article class="card">
<h3>运行统计</h3>
<div id="statGrid" class="stat-grid"></div>
<button type="button" id="refreshStatsBtn" style="margin-top: 10px">刷新统计</button>
</article>
<article class="card">
<h3>文件与数据集</h3>
<div class="toolbar">
<input type="file" id="fileInput" />
<button type="button" id="uploadBtn">上传文件</button>
</div>
<progress id="uploadProgress" value="0" max="100" style="width: 100%; margin-top: 10px"></progress>
<div id="fileList" style="margin-top: 12px"></div>
</article>
</section>
</main>
<script>
const md = window.markdownit({ html: false, breaks: true, linkify: true });
const form = document.getElementById("cycleForm");
const runBtn = document.getElementById("runBtn");
const suggestBtn = document.getElementById("suggestBtn");
const demoBtn = document.getElementById("demoBtn");
const kpiCost = document.getElementById("kpiCost");
const kpiCarbon = document.getElementById("kpiCarbon");
const kpiReliability = document.getElementById("kpiReliability");
const dispatchBox = document.getElementById("dispatchBox");
const validationBox = document.getElementById("validationBox");
const planBox = document.getElementById("planBox");
const riskBox = document.getElementById("riskBox");
const logList = document.getElementById("logList");
const cycleList = document.getElementById("cycleList");
const memoryBox = document.getElementById("memoryBox");
const markdownPreview = document.getElementById("markdownPreview");
const statGrid = document.getElementById("statGrid");
const refreshStatsBtn = document.getElementById("refreshStatsBtn");
const fileInput = document.getElementById("fileInput");
const uploadBtn = document.getElementById("uploadBtn");
const uploadProgress = document.getElementById("uploadProgress");
const fileList = document.getElementById("fileList");
function uuid() {
if (window.crypto && window.crypto.randomUUID) {
return window.crypto.randomUUID();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function renderMarkdown(target, content) {
target.innerHTML = md.render(content || "暂无内容");
}
function renderDecision(result) {
const kpi = result.decision?.kpi || {};
kpiCost.textContent = kpi.cost ?? "-";
kpiCarbon.textContent = kpi.carbon ?? "-";
kpiReliability.textContent = kpi.reliability ?? "-";
const plan = result.decision?.plan || result.plan || {};
const risk = result.decision?.risk || result.risk || {};
renderMarkdown(planBox, "#### 推理规划\n\n```json\n" + JSON.stringify(plan, null, 2) + "\n```");
renderMarkdown(riskBox, "#### 风险评估\n\n```json\n" + JSON.stringify(risk, null, 2) + "\n```");
const hourly = result.decision?.dispatch?.hourly || [];
if (hourly.length > 0) {
const head = "|小时|负荷|光伏|风电|储能放电|储能充电|并网|需求响应|SOC|电价|\n|---|---|---|---|---|---|---|---|---|---|";
const rows = hourly
.map(
(h) =>
`|${h.hour}|${h.demand_mw}|${h.solar_mw}|${h.wind_mw}|${h.storage_discharge_mw}|${h.storage_charge_mw}|${h.grid_import_mw}|${h.demand_response_mw}|${h.soc}|${h.price}|`
)
.join("\n");
renderMarkdown(dispatchBox, "#### 小时级调度表\n\n" + head + "\n" + rows);
} else {
renderMarkdown(dispatchBox, "暂无小时级调度结果");
}
const validation = result.validation || {};
const message = validation.passed ? "校验通过" : "校验未通过";
const issues = (validation.issues || []).join("、");
renderMarkdown(validationBox, `**${message}**\n\n${issues}`);
renderMarkdown(memoryBox, "```json\n" + JSON.stringify(result.memory || {}, null, 2) + "\n```");
const notes = result.scenario?.notes || "";
renderMarkdown(markdownPreview, notes || "暂无备注");
}
function renderLogs(logs) {
logList.innerHTML = "";
logs.forEach((item) => {
const wrap = document.createElement("div");
wrap.className = "log";
const header = document.createElement("header");
header.textContent = `${item.role} · ${item.created_at}`;
const body = document.createElement("div");
body.innerHTML = md.render(item.content || "");
wrap.appendChild(header);
wrap.appendChild(body);
logList.appendChild(wrap);
});
}
async function loadCycles() {
const res = await fetch("/api/cycles");
const data = await res.json();
cycleList.innerHTML = "";
data.forEach((cycle) => {
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = `${cycle.title} · ${cycle.status}`;
btn.addEventListener("click", async () => {
const detailRes = await fetch(`/api/cycles/${cycle.id}`);
const detail = await detailRes.json();
renderDecision(detail);
const replayRes = await fetch(`/api/replay/${cycle.id}`);
const replay = await replayRes.json();
renderLogs(replay);
});
cycleList.appendChild(btn);
});
}
async function loadMemory() {
const res = await fetch("/api/memory");
const data = await res.json();
renderMarkdown(memoryBox, "```json\n" + JSON.stringify(data || {}, null, 2) + "\n```");
}
async function loadStats() {
const res = await fetch("/api/stats");
const data = await res.json();
statGrid.innerHTML = "";
const items = [
{ label: "任务总数", value: data.total },
{ label: "完成任务", value: data.completed },
{ label: "平均成本", value: data.avg_cost },
{ label: "平均碳排", value: data.avg_carbon },
{ label: "最近运行", value: data.last_run },
];
items.forEach((item) => {
const card = document.createElement("div");
card.className = "stat-card";
const span = document.createElement("span");
span.textContent = item.label;
const strong = document.createElement("strong");
strong.textContent = item.value;
card.appendChild(span);
card.appendChild(strong);
statGrid.appendChild(card);
});
}
async function loadFiles() {
const res = await fetch("/api/files");
const data = await res.json();
fileList.innerHTML = "";
data.forEach((file) => {
const row = document.createElement("div");
row.className = "log";
row.innerHTML = `<strong>${file.filename}</strong><br/>大小: ${Math.round(
file.size_bytes / 1024
)} KB · 二进制: ${file.is_binary ? "是" : "否"}<br/>SHA256: ${file.sha256}`;
fileList.appendChild(row);
});
}
async function fillSuggestedScenario() {
const res = await fetch("/api/scenario_suggest");
const data = await res.json();
Object.keys(data).forEach((key) => {
const field = form.querySelector(`[name="${key}"]`);
if (field) field.value = data[key];
});
}
async function runDemo() {
const payload = {
title: "一键演示调度",
demand_mw: 125,
solar_mw: 38,
wind_mw: 26,
storage_mwh: 78,
storage_soc: 0.4,
priority: "可靠性优先",
notes: "演示场景:保障核心负荷、允许有限需求响应。",
};
const res = await fetch("/api/run_cycle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
renderDecision(data);
const replayRes = await fetch(`/api/replay/${data.id}`);
renderLogs(await replayRes.json());
await loadCycles();
await loadMemory();
await loadStats();
}
async function uploadFile(file) {
if (!file) return;
uploadProgress.value = 0;
const chunkSize = 4 * 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
if (file.size <= chunkSize) {
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: formData });
await res.json();
uploadProgress.value = 100;
await loadFiles();
return;
}
const fileId = uuid();
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
await fetch("/api/upload_chunk", {
method: "POST",
headers: {
"X-File-Id": fileId,
"X-File-Name": file.name,
"X-Chunk-Index": i,
"X-Chunk-Total": totalChunks,
"X-Content-Type": file.type,
},
body: chunk,
});
uploadProgress.value = Math.round(((i + 1) / totalChunks) * 100);
}
await loadFiles();
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
runBtn.disabled = true;
const payload = Object.fromEntries(new FormData(form).entries());
const res = await fetch("/api/run_cycle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
renderDecision(data);
const replayRes = await fetch(`/api/replay/${data.id}`);
renderLogs(await replayRes.json());
await loadCycles();
await loadMemory();
runBtn.disabled = false;
});
suggestBtn.addEventListener("click", fillSuggestedScenario);
demoBtn.addEventListener("click", runDemo);
refreshStatsBtn.addEventListener("click", loadStats);
uploadBtn.addEventListener("click", () => uploadFile(fileInput.files[0]));
loadCycles();
loadMemory();
loadStats();
loadFiles();
renderMarkdown(dispatchBox, "等待调度结果");
renderMarkdown(validationBox, "等待校验结果");
renderMarkdown(planBox, "等待推理规划");
renderMarkdown(riskBox, "等待风险评估");
</script>
</body>
</html>