改为自动周期采集并优化首页导航

This commit is contained in:
Codex
2026-07-08 22:38:09 +08:00
parent 05d98dd576
commit da5e007324
8 changed files with 191 additions and 128 deletions
+150 -113
View File
@@ -15,7 +15,6 @@ HOME_PAGE_HTML = """
--panel: oklch(99% 0.006 190);
--panel-2: oklch(94.5% 0.018 176);
--accent: oklch(52% 0.12 174);
--accent-ink: oklch(19% 0.035 190);
--warn: oklch(67% 0.13 70);
--bad: oklch(58% 0.16 26);
--good: oklch(53% 0.12 150);
@@ -79,7 +78,8 @@ HOME_PAGE_HTML = """
}
.brand span,
.nav-note {
.nav-note,
.auto-card span {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
@@ -91,17 +91,37 @@ HOME_PAGE_HTML = """
margin-top: 24px;
}
.nav a {
text-decoration: none;
.nav button {
border: 0;
width: 100%;
text-align: left;
background: transparent;
padding: 10px 12px;
border-radius: 8px;
color: var(--muted);
font: inherit;
font-size: 14px;
cursor: pointer;
}
.nav a.active {
.nav button.active {
background: var(--ink);
color: white;
font-weight: 700;
}
.auto-card {
display: grid;
gap: 6px;
margin-top: 24px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: oklch(99% 0.006 180 / 0.78);
}
.auto-card strong {
font-size: 13px;
}
.content {
@@ -134,36 +154,44 @@ HOME_PAGE_HTML = """
line-height: 1.7;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
button {
border: 1px solid var(--ink);
.pulse {
min-width: 184px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--accent);
color: white;
min-height: 42px;
padding: 0 16px;
font-weight: 700;
cursor: pointer;
box-shadow: 4px 4px 0 var(--ink);
transition: transform 160ms ease, box-shadow 160ms ease;
padding: 12px 14px;
background: var(--panel);
color: var(--muted);
line-height: 1.55;
font-size: 13px;
}
button:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); }
button:disabled { opacity: 0.58; cursor: wait; transform: none; box-shadow: 2px 2px 0 var(--ink); }
.pulse strong {
display: block;
color: var(--ink);
font-size: 18px;
line-height: 1.2;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr);
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
gap: 18px;
align-items: start;
}
.view {
display: none;
gap: 18px;
}
.view.active {
display: grid;
}
.view[data-view="signals"].active {
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
@@ -235,36 +263,28 @@ HOME_PAGE_HTML = """
.pill {
display: inline-flex;
align-items: center;
height: 24px;
min-height: 24px;
border: 1px solid var(--line);
border-radius: 999px;
padding: 0 9px;
padding: 2px 9px;
background: oklch(98% 0.01 180);
color: var(--muted);
max-width: 100%;
}
.importance {
color: var(--accent-ink);
color: oklch(19% 0.035 190);
border-color: oklch(72% 0.08 174);
background: oklch(91% 0.045 174);
}
.report {
padding: 18px;
min-height: 320px;
min-height: 520px;
line-height: 1.75;
white-space: pre-wrap;
}
.report h1,
.report h2,
.report h3 {
font-size: 16px;
margin: 0 0 10px;
line-height: 1.35;
}
.runs { display: grid; }
.run-row {
@@ -291,28 +311,6 @@ HOME_PAGE_HTML = """
line-height: 1.7;
}
.toast {
position: fixed;
right: 22px;
bottom: 22px;
max-width: 360px;
background: var(--ink);
color: white;
padding: 12px 14px;
border-radius: 8px;
opacity: 0;
transform: translateY(12px);
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease;
line-height: 1.55;
z-index: 20;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 980px) {
.shell { grid-template-columns: 1fr; }
.rail {
@@ -322,9 +320,9 @@ HOME_PAGE_HTML = """
border-bottom: 1px solid var(--line);
}
.nav { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.layout { grid-template-columns: 1fr; }
.view[data-view="signals"].active { grid-template-columns: 1fr; }
.topbar { display: grid; }
.actions { justify-content: flex-start; }
.pulse { min-width: 0; }
}
@media (max-width: 560px) {
@@ -332,7 +330,6 @@ HOME_PAGE_HTML = """
.rail { padding: 18px; }
.nav { grid-template-columns: 1fr; }
h1 { font-size: 30px; }
button { width: 100%; }
}
</style>
</head>
@@ -345,26 +342,31 @@ HOME_PAGE_HTML = """
<span>AI 情报采集与研判工作台</span>
</div>
<nav class="nav" aria-label="主导航">
<a class="active" href="#signals">情报</a>
<a href="#report">日报</a>
<a href="#runs">运行</a>
<button class="active" type="button" data-view-target="signals">情报</button>
<button type="button" data-view-target="report">日报</button>
<button type="button" data-view-target="runs">运行</button>
</nav>
<p class="nav-note">每天自动整理 AI 新闻、模型发布、Agent 工具和 GitHub 热门项目。</p>
<div class="auto-card">
<strong>自动整理</strong>
<span>启动后自动运行,之后约每 30 分钟整理一次。</span>
</div>
<p class="nav-note">聚合 AI 新闻、模型发布、Agent 工具和 GitHub 热门项目。</p>
</aside>
<main class="content">
<section class="topbar">
<div>
<h1>今日 AI 情报</h1>
<p class="subtitle">聚合最新新闻和开源热度,由 Agent 统一筛选、去重和生成日报。</p>
<p class="subtitle">Agent 自动筛选、去重和生成日报;页面会持续更新最新结果。</p>
</div>
<div class="actions">
<button id="runButton" type="button">采集今日情报</button>
<div class="pulse">
<strong id="lastRunText">等待运行</strong>
<span id="lastRunDetail">正在读取运行记录。</span>
</div>
</section>
<section class="layout">
<div class="panel" id="signals">
<section class="view active" data-view="signals">
<div class="panel">
<div class="panel-head">
<h2>情报信号</h2>
<span class="count" id="signalCount">0 条</span>
@@ -375,39 +377,65 @@ HOME_PAGE_HTML = """
</div>
<div class="stack">
<div class="panel" id="report">
<div class="panel">
<div class="panel-head">
<h2>最新日报</h2>
<span class="count" id="reportTime">待生成</span>
</div>
<div class="report" id="reportBody">正在读取日报。</div>
<div class="report" id="reportPreview">正在读取日报。</div>
</div>
<div class="panel" id="runs">
<div class="panel">
<div class="panel-head">
<h2>最近运行</h2>
<span class="count" id="runCount">0 次</span>
</div>
<div class="runs" id="runList">
<div class="runs" id="runPreview">
<div class="empty">正在读取运行记录。</div>
</div>
</div>
</div>
</section>
<section class="view" data-view="report">
<div class="panel">
<div class="panel-head">
<h2>最新日报</h2>
<span class="count" id="reportTimeFull">待生成</span>
</div>
<div class="report" id="reportBody">正在读取日报。</div>
</div>
</section>
<section class="view" data-view="runs">
<div class="panel">
<div class="panel-head">
<h2>最近运行</h2>
<span class="count" id="runCountFull">0 次</span>
</div>
<div class="runs" id="runList">
<div class="empty">正在读取运行记录。</div>
</div>
</div>
</section>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
const navButtons = Array.from(document.querySelectorAll("[data-view-target]"));
const views = Array.from(document.querySelectorAll("[data-view]"));
const signalList = document.querySelector("#signalList");
const signalCount = document.querySelector("#signalCount");
const reportPreview = document.querySelector("#reportPreview");
const reportBody = document.querySelector("#reportBody");
const reportTime = document.querySelector("#reportTime");
const reportTimeFull = document.querySelector("#reportTimeFull");
const runPreview = document.querySelector("#runPreview");
const runList = document.querySelector("#runList");
const runCount = document.querySelector("#runCount");
const runButton = document.querySelector("#runButton");
const toast = document.querySelector("#toast");
const runCountFull = document.querySelector("#runCountFull");
const lastRunText = document.querySelector("#lastRunText");
const lastRunDetail = document.querySelector("#lastRunDetail");
const stateText = {
completed: "完成",
@@ -436,16 +464,16 @@ HOME_PAGE_HTML = """
});
}
function showToast(message) {
toast.textContent = message;
toast.classList.add("show");
window.setTimeout(() => toast.classList.remove("show"), 3200);
function activateView(name, updateHash = true) {
navButtons.forEach((button) => button.classList.toggle("active", button.dataset.viewTarget === name));
views.forEach((view) => view.classList.toggle("active", view.dataset.view === name));
if (updateHash) history.replaceState(null, "", `#${name}`);
}
function renderSignals(signals) {
signalCount.textContent = `${signals.length} 条`;
if (!signals.length) {
signalList.innerHTML = '<div class="empty">还没有情报信号,可以先采集今日情报。</div>';
signalList.innerHTML = '<div class="empty">Agent 正在按固定节奏自动整理,完成后这里会出现情报信号。</div>';
return;
}
signalList.innerHTML = signals.map((item) => `
@@ -468,20 +496,21 @@ HOME_PAGE_HTML = """
function renderReport(report) {
if (!report) {
reportTime.textContent = "待生成";
reportBody.textContent = "还没有日报,可以先采集今日情报。";
reportTimeFull.textContent = "待生成";
reportPreview.textContent = "Agent 正在自动整理,日报生成后会显示在这里。";
reportBody.textContent = "Agent 正在自动整理,日报生成后会显示在这里。";
return;
}
reportTime.textContent = formatDate(report.created_at);
reportBody.textContent = report.content_md || "日报内容为空。";
const timeText = formatDate(report.created_at);
const content = report.content_md || "日报内容为空。";
reportTime.textContent = timeText;
reportTimeFull.textContent = timeText;
reportPreview.textContent = content;
reportBody.textContent = content;
}
function renderRuns(runs) {
runCount.textContent = `${runs.length} 次`;
if (!runs.length) {
runList.innerHTML = '<div class="empty">还没有运行记录。</div>';
return;
}
runList.innerHTML = runs.map((run) => `
function runRows(runs) {
return runs.map((run) => `
<div class="run-row">
<span>#${escapeHtml(run.id)} · ${formatDate(run.started_at)}</span>
<span class="state ${escapeHtml(run.status)}">${escapeHtml(stateText[run.status] || run.status)}</span>
@@ -489,6 +518,23 @@ HOME_PAGE_HTML = """
`).join("");
}
function renderRuns(runs) {
runCount.textContent = `${runs.length} 次`;
runCountFull.textContent = `${runs.length} 次`;
if (!runs.length) {
runPreview.innerHTML = '<div class="empty">Agent 会在启动后自动开始整理。</div>';
runList.innerHTML = '<div class="empty">Agent 会在启动后自动开始整理。</div>';
lastRunText.textContent = "等待运行";
lastRunDetail.textContent = "启动后自动运行,之后约每 30 分钟整理一次。";
return;
}
runPreview.innerHTML = runRows(runs.slice(0, 4));
runList.innerHTML = runRows(runs);
const latest = runs[0];
lastRunText.textContent = stateText[latest.status] || latest.status;
lastRunDetail.textContent = `最近一次:${formatDate(latest.started_at)}`;
}
async function loadDashboard() {
const response = await fetch("/dashboard", { headers: { Accept: "application/json" } });
if (!response.ok) throw new Error("读取情报失败");
@@ -498,32 +544,23 @@ HOME_PAGE_HTML = """
renderRuns(data.runs || []);
}
async function runAgent() {
runButton.disabled = true;
runButton.textContent = "采集中";
showToast("Agent 已开始采集今日情报。");
try {
const response = await fetch("/agent/runs/daily", { method: "POST" });
if (!response.ok) {
const body = await response.text();
throw new Error(body || "采集失败");
}
showToast("今日情报采集完成。");
await loadDashboard();
} catch (error) {
showToast(error.message || "采集失败,请查看日志。");
} finally {
runButton.disabled = false;
runButton.textContent = "采集今日情报";
}
navButtons.forEach((button) => {
button.addEventListener("click", () => activateView(button.dataset.viewTarget));
});
const initialView = window.location.hash.replace("#", "");
if (["signals", "report", "runs"].includes(initialView)) {
activateView(initialView, false);
}
runButton.addEventListener("click", runAgent);
loadDashboard().catch(() => {
signalList.innerHTML = '<div class="empty">情报读取失败,请稍后再试。</div>';
reportPreview.textContent = "日报读取失败。";
reportBody.textContent = "日报读取失败。";
runPreview.innerHTML = '<div class="empty">运行记录读取失败。</div>';
runList.innerHTML = '<div class="empty">运行记录读取失败。</div>';
});
window.setInterval(loadDashboard, 60000);
</script>
</body>
</html>