Backend:
- Persist alert events and robot delivery counters in SQLite (data/app.db,
YOLO_DB_PATH override); EventStore keeps its interface, DeliveryStatsStore
uses read/write-through UPSERT; in-memory fallback remains for standalone
AlertManager use
- Serve /api/detect from a sync endpoint and serialize YOLO inference with a
lock, so concurrent requests no longer block the event loop
- Unify single-image and session detection: requests without session_id
share the fixed single-image session and run full frame confirmation
- Store UTC Z-suffixed timestamps for sortable string comparison; close
executor and database on shutdown via FastAPI lifespan
Frontend:
- Split app.js into ES modules (main.js + modules/{dom,api,ui,charts,store}
+ views/{dashboard,inspection,events,robots,robotCards}), no build step
- Escape all server data interpolated into innerHTML; guard missing
event.classes; replace lazy element ID list with memoized qs()
- Remove hardcoded fake stats (trend badge, device donut segment)
Docs:
- Rewrite README: accurate weight policy (only trained best.pt committed,
no Git LFS), SQLite persistence, single-worker note, data asset inventory
- Align models/pretrained/README.md with actual files; add YOLO_DB_PATH to
.env.example; add persistence unit tests; ruff clean
40 lines
3.2 KiB
JavaScript
40 lines
3.2 KiB
JavaScript
import { qs, escapeHtml, formatDate } from "../modules/dom.js";
|
|
|
|
export function robotCardTemplate(robot) {
|
|
const id = escapeHtml(robot.id);
|
|
const platformClass = robot.id === "wechat" ? "wechat" : "feishu";
|
|
const platformMark = robot.id === "wechat" ? "微" : "飞";
|
|
const statusLabel = robot.configured ? "已配置" : "未配置";
|
|
const lastActivity = robot.last_test_at || robot.last_delivery_at;
|
|
const activityText = lastActivity ? formatDate(lastActivity) : "尚无发送记录";
|
|
const statusText = robot.last_status === "success" ? "最近发送成功" : robot.last_status === "failed" ? "最近发送失败" : "等待首次发送";
|
|
const credential = robot.id === "wechat" ? "WECHAT_WEBHOOK_URL" : "FEISHU_WEBHOOK_URL";
|
|
const capabilities = (robot.capabilities || [])
|
|
.map((capability) => `<span>${escapeHtml(capability)}</span>`)
|
|
.join("");
|
|
return `<article class="panel message-robot-card ${robot.configured ? "is-configured" : ""}" data-robot-id="${id}">
|
|
<div class="message-robot-head"><div class="platform-logo ${platformClass}">${platformMark}</div><div><span class="robot-id">${escapeHtml(robot.id.toUpperCase())} ROBOT</span><h3>${escapeHtml(robot.name)}群机器人</h3><p>${robot.id === "wechat" ? "群聊 Markdown 与告警截图" : "群聊交互卡片与签名校验"}</p></div><span class="robot-status ${robot.configured ? "active" : "offline"}"><i></i>${statusLabel}</span></div>
|
|
<div class="capability-list">${capabilities}</div>
|
|
<div class="delivery-metrics"><div><span>成功投递</span><strong>${robot.delivered}</strong></div><div><span>失败</span><strong class="${robot.failed ? "danger" : ""}">${robot.failed}</strong></div><div><span>连接测试</span><strong>${robot.tests}</strong></div></div>
|
|
<div class="robot-activity"><span>${statusText}</span><strong>${activityText}</strong></div>
|
|
<div class="credential-key"><span>服务端配置</span><code>${credential}${robot.id === "feishu" ? " / FEISHU_SECRET" : ""}</code></div>
|
|
${robot.last_error ? `<p class="delivery-error">${escapeHtml(robot.last_error)}</p>` : ""}
|
|
<button class="primary-button test-robot-button" type="button" data-test-robot="${id}" ${robot.configured ? "" : "disabled"}>${robot.configured ? "发送测试消息" : "配置后可测试"}</button>
|
|
</article>`;
|
|
}
|
|
|
|
export function renderRobots(payload = { robots: [], summary: {} }, policy = {}) {
|
|
const robots = payload.robots || [];
|
|
const summary = payload.summary || {};
|
|
qs("#robotTotalCount").textContent = String(summary.total || robots.length);
|
|
qs("#robotConfiguredCount").textContent = String(summary.configured || 0);
|
|
qs("#robotDeliveredCount").textContent = String(summary.delivered || 0);
|
|
qs("#robotFailedCount").textContent = String(summary.failed || 0);
|
|
qs("#robotNavCount").textContent = String(summary.configured || 0);
|
|
qs("#robotPolicyConfirm").textContent = `${policy.confirm_frames || 3} 帧连续命中后发送`;
|
|
qs("#robotPolicyCooldown").textContent = `${policy.cooldown_seconds || 60} 秒内同类不重复发送`;
|
|
qs("#robotCardGrid").innerHTML = robots.length
|
|
? robots.map(robotCardTemplate).join("")
|
|
: '<article class="panel robot-empty">暂无机器人通道数据</article>';
|
|
}
|