Files
yolo/frontend/views/robotCards.js
Kunpeng 25fa0c5825 feat: SQLite persistence, non-blocking inference, modular frontend
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
2026-08-13 16:50:23 +08:00

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>';
}