auto_agent/backend/app/web/chat_demo.html
2521690 ce299cbb18 feat: 增加 Agent 演示入口与 app_metadata 驱动验证链路
- 新增 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%
2026-04-09 14:10:13 +08:00

407 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>