- 新增 app_metadata 模型、仓储与服务 - 将默认 edge 验证步骤改为由 app_metadata 驱动生成 - 新增 chat_session / chat_message 会话层模型与 chat service - 新增 demo chat API,支持会话创建、消息发送、任务确认 - 新增最小 Web Demo 页面,形成聊天式演示入口 - 增强任务报告,补充 audit_summary 与更细粒度 task_metrics - 增强 edge-agent 执行器:tcp_probe、日志时间范围过滤、进程指标与更灵活健康检查 - 更新 README 与当前进度总结,MVP 进度推进到约 94%
407 lines
12 KiB
HTML
407 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>智能化部署 Agent Demo</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f4efe6;
|
||
--panel: #fffaf1;
|
||
--ink: #15221d;
|
||
--muted: #61746a;
|
||
--accent: #1d6a4f;
|
||
--accent-soft: #d6efe4;
|
||
--line: #d8d1c5;
|
||
--warn: #ad5a2a;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
background:
|
||
radial-gradient(circle at top left, #efe2c6 0, transparent 28%),
|
||
linear-gradient(135deg, #f6f1e8 0%, #ece6db 100%);
|
||
color: var(--ink);
|
||
}
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 340px 1fr 380px;
|
||
min-height: 100vh;
|
||
}
|
||
.sidebar, .panel, .chat {
|
||
padding: 24px;
|
||
}
|
||
.sidebar {
|
||
background: rgba(255,250,241,.92);
|
||
border-right: 1px solid var(--line);
|
||
}
|
||
.chat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
.panel {
|
||
background: rgba(248,244,236,.9);
|
||
border-left: 1px solid var(--line);
|
||
overflow: auto;
|
||
}
|
||
.eyebrow {
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: .14em;
|
||
color: var(--muted);
|
||
margin-bottom: 8px;
|
||
}
|
||
h1 {
|
||
margin: 0 0 12px;
|
||
font-size: 28px;
|
||
line-height: 1.1;
|
||
}
|
||
.intro, .meta, .tiny {
|
||
color: var(--muted);
|
||
line-height: 1.6;
|
||
font-size: 14px;
|
||
}
|
||
.prompt-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-top: 18px;
|
||
}
|
||
.prompt-btn, button {
|
||
border: 0;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: transform .12s ease, opacity .12s ease;
|
||
}
|
||
.prompt-btn:hover, button:hover { transform: translateY(-1px); }
|
||
.prompt-btn {
|
||
text-align: left;
|
||
padding: 12px 14px;
|
||
background: var(--accent-soft);
|
||
color: var(--ink);
|
||
}
|
||
.chat-shell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
border: 1px solid var(--line);
|
||
border-radius: 18px;
|
||
background: rgba(255,255,255,.88);
|
||
overflow: hidden;
|
||
box-shadow: 0 10px 30px rgba(44, 44, 44, .06);
|
||
}
|
||
.chat-header {
|
||
padding: 18px 20px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: linear-gradient(135deg, #f8f4ea 0%, #f0eadf 100%);
|
||
}
|
||
.chat-messages {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
.message {
|
||
max-width: 80%;
|
||
padding: 14px 16px;
|
||
border-radius: 16px;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
font-size: 14px;
|
||
}
|
||
.message.user {
|
||
align-self: flex-end;
|
||
background: var(--accent);
|
||
color: #fff;
|
||
border-bottom-right-radius: 6px;
|
||
}
|
||
.message.assistant {
|
||
align-self: flex-start;
|
||
background: #f7f3ea;
|
||
border: 1px solid var(--line);
|
||
border-bottom-left-radius: 6px;
|
||
}
|
||
.composer {
|
||
border-top: 1px solid var(--line);
|
||
padding: 16px;
|
||
display: flex;
|
||
gap: 12px;
|
||
background: #fff;
|
||
}
|
||
textarea {
|
||
flex: 1;
|
||
min-height: 72px;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--line);
|
||
padding: 14px;
|
||
resize: vertical;
|
||
font: inherit;
|
||
}
|
||
button.primary {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
min-width: 120px;
|
||
padding: 0 18px;
|
||
}
|
||
button.secondary {
|
||
background: #ede5d8;
|
||
color: var(--ink);
|
||
padding: 12px 14px;
|
||
}
|
||
.stack { display: flex; flex-direction: column; gap: 14px; }
|
||
.card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 16px;
|
||
padding: 16px;
|
||
}
|
||
.code {
|
||
font-family: Consolas, "Courier New", monospace;
|
||
font-size: 12px;
|
||
background: #f3eee5;
|
||
padding: 10px;
|
||
border-radius: 10px;
|
||
overflow: auto;
|
||
white-space: pre-wrap;
|
||
}
|
||
.actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-top: 12px;
|
||
}
|
||
.status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
background: #efe9de;
|
||
color: var(--ink);
|
||
}
|
||
.status.warn { background: #f7e1cf; color: var(--warn); }
|
||
@media (max-width: 1200px) {
|
||
.layout { grid-template-columns: 300px 1fr; }
|
||
.panel { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--line); }
|
||
}
|
||
@media (max-width: 760px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
.sidebar { border-right: 0; border-bottom: 1px solid var(--line); }
|
||
.message { max-width: 92%; }
|
||
.composer { flex-direction: column; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="layout">
|
||
<aside class="sidebar">
|
||
<div class="eyebrow">Agent Demo</div>
|
||
<h1>智能化部署 Agent</h1>
|
||
<div class="intro">目标是一句话发起部署,完成确认、执行、验证、报告的可视化演示流。</div>
|
||
<div class="prompt-list" id="promptList"></div>
|
||
<div class="card" style="margin-top:18px;">
|
||
<div class="eyebrow">会话</div>
|
||
<div class="meta" id="sessionMeta">正在初始化会话…</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="chat">
|
||
<div class="chat-shell">
|
||
<div class="chat-header">
|
||
<div class="eyebrow">Conversation</div>
|
||
<div class="meta">输入一句自然语言,页面会展示任务解析、确认、执行、验证、报告。</div>
|
||
</div>
|
||
<div class="chat-messages" id="chatMessages"></div>
|
||
<div class="composer">
|
||
<textarea id="chatInput" placeholder="例如:deploy order-service 1.2.3 to test"></textarea>
|
||
<button class="primary" id="sendBtn">发送</button>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<aside class="panel">
|
||
<div class="stack">
|
||
<section class="card">
|
||
<div class="eyebrow">任务解析</div>
|
||
<div id="taskStatus" class="status">尚未创建任务</div>
|
||
<div class="actions">
|
||
<button class="secondary" id="confirmBtn" disabled>确认任务</button>
|
||
<button class="secondary" id="refreshBtn" disabled>刷新报告</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="eyebrow">结构化意图</div>
|
||
<div class="code" id="intentBox">暂无</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="eyebrow">任务详情</div>
|
||
<div class="code" id="detailBox">暂无</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="eyebrow">执行报告</div>
|
||
<div class="code" id="reportBox">暂无</div>
|
||
</section>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<script>
|
||
const state = {
|
||
sessionId: null,
|
||
lastTaskId: null
|
||
};
|
||
|
||
const promptList = document.getElementById("promptList");
|
||
const chatMessages = document.getElementById("chatMessages");
|
||
const chatInput = document.getElementById("chatInput");
|
||
const sendBtn = document.getElementById("sendBtn");
|
||
const confirmBtn = document.getElementById("confirmBtn");
|
||
const refreshBtn = document.getElementById("refreshBtn");
|
||
const sessionMeta = document.getElementById("sessionMeta");
|
||
const intentBox = document.getElementById("intentBox");
|
||
const detailBox = document.getElementById("detailBox");
|
||
const reportBox = document.getElementById("reportBox");
|
||
const taskStatus = document.getElementById("taskStatus");
|
||
|
||
async function api(path, options = {}) {
|
||
const response = await fetch(path, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
...(options.headers || {})
|
||
},
|
||
...options
|
||
});
|
||
const payload = await response.json();
|
||
if (!response.ok || payload.success === false) {
|
||
throw new Error(payload.message || "request failed");
|
||
}
|
||
return payload.data;
|
||
}
|
||
|
||
function addMessage(role, content) {
|
||
const node = document.createElement("div");
|
||
node.className = `message ${role}`;
|
||
node.textContent = content;
|
||
chatMessages.appendChild(node);
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
}
|
||
|
||
function renderMessages(messages) {
|
||
chatMessages.innerHTML = "";
|
||
messages.forEach((item) => addMessage(item.role, item.content));
|
||
}
|
||
|
||
function renderPrompts(prompts) {
|
||
promptList.innerHTML = "";
|
||
prompts.forEach((prompt) => {
|
||
const button = document.createElement("button");
|
||
button.className = "prompt-btn";
|
||
button.textContent = prompt;
|
||
button.onclick = () => {
|
||
chatInput.value = prompt;
|
||
chatInput.focus();
|
||
};
|
||
promptList.appendChild(button);
|
||
});
|
||
}
|
||
|
||
function setStatus(text, warn = false) {
|
||
taskStatus.textContent = text;
|
||
taskStatus.className = warn ? "status warn" : "status";
|
||
}
|
||
|
||
async function ensureSession() {
|
||
const saved = localStorage.getItem("agent_demo_session_id");
|
||
if (saved) {
|
||
try {
|
||
const data = await api(`/api/demo/chat/sessions/${saved}`);
|
||
state.sessionId = data.session_id;
|
||
state.lastTaskId = data.last_task_id;
|
||
renderMessages(data.messages);
|
||
renderPrompts(data.sample_prompts);
|
||
sessionMeta.textContent = `session_id=${data.session_id}`;
|
||
refreshBtn.disabled = !state.lastTaskId;
|
||
return;
|
||
} catch (_) {}
|
||
}
|
||
|
||
const data = await api("/api/demo/chat/sessions", {
|
||
method: "POST",
|
||
body: JSON.stringify({ tenant_id: "tenant-demo", channel: "WEB" })
|
||
});
|
||
state.sessionId = data.session_id;
|
||
state.lastTaskId = data.last_task_id;
|
||
localStorage.setItem("agent_demo_session_id", data.session_id);
|
||
renderMessages(data.messages);
|
||
renderPrompts(data.sample_prompts);
|
||
sessionMeta.textContent = `session_id=${data.session_id}`;
|
||
}
|
||
|
||
async function sendMessage() {
|
||
if (!chatInput.value.trim()) return;
|
||
const content = chatInput.value.trim();
|
||
addMessage("user", content);
|
||
chatInput.value = "";
|
||
const data = await api(`/api/demo/chat/sessions/${state.sessionId}/messages`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ content, context: {} })
|
||
});
|
||
addMessage("assistant", data.assistant_message.content);
|
||
state.lastTaskId = data.task_id;
|
||
intentBox.textContent = JSON.stringify({
|
||
parsed_intent: data.parsed_intent,
|
||
missing_slots: data.missing_slots,
|
||
risk_level: data.risk_level,
|
||
next_action: data.next_action
|
||
}, null, 2);
|
||
setStatus(`task_id=${data.task_id} task_status=${data.task_status}`);
|
||
confirmBtn.disabled = data.next_action !== "CONFIRM_TASK";
|
||
refreshBtn.disabled = false;
|
||
await refreshTaskAndReport();
|
||
}
|
||
|
||
async function confirmTask() {
|
||
if (!state.lastTaskId) return;
|
||
const data = await api(`/api/demo/chat/sessions/${state.sessionId}/tasks/${state.lastTaskId}/confirm`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ comment: "from web demo" })
|
||
});
|
||
addMessage("assistant", data.assistant_message.content);
|
||
confirmBtn.disabled = true;
|
||
setStatus(`task_id=${data.task_id} task_status=${data.task_status} approval_status=${data.approval_status}`, data.approval_status === "PENDING");
|
||
await refreshTaskAndReport();
|
||
}
|
||
|
||
async function refreshTaskAndReport() {
|
||
if (!state.lastTaskId) return;
|
||
const [detail, report] = await Promise.all([
|
||
api(`/api/agent/tasks/${state.lastTaskId}`),
|
||
api(`/api/agent/tasks/${state.lastTaskId}/report`)
|
||
]);
|
||
detailBox.textContent = JSON.stringify(detail, null, 2);
|
||
reportBox.textContent = JSON.stringify(report, null, 2);
|
||
setStatus(`task_id=${detail.task_id} task_status=${detail.task_status} approval_status=${detail.approval_status}`, detail.approval_status === "PENDING");
|
||
}
|
||
|
||
sendBtn.addEventListener("click", sendMessage);
|
||
confirmBtn.addEventListener("click", confirmTask);
|
||
refreshBtn.addEventListener("click", refreshTaskAndReport);
|
||
chatInput.addEventListener("keydown", (event) => {
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||
sendMessage();
|
||
}
|
||
});
|
||
|
||
ensureSession();
|
||
</script>
|
||
</body>
|
||
</html>
|