feat: implement T-019 reliable event ingress
Harness governance / validate (pull_request) Has been cancelled

This commit is contained in:
QiuSW
2026-08-11 15:41:07 +08:00
parent b7fe44eeb0
commit bd964e8831
33 changed files with 2762 additions and 57 deletions
+70 -3
View File
@@ -90,11 +90,23 @@
.form-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.inline-state { min-height: 42px; padding: 9px 10px; border-left: 3px solid var(--cyan); background: rgba(56, 189, 248, .07); color: var(--muted); font-size: 12px; }
.event-list { max-height: 330px; padding: 8px; display: grid; gap: 7px; overflow-y: auto; }
.ingress-summary { padding: 11px 12px; display: grid; gap: 8px; border-bottom: 1px solid var(--line); background: rgba(56, 189, 248, .04); }
.ingress-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.ingress-head h3 { margin: 0; font-size: 13px; }
.ingress-state { min-height: 28px; padding: 3px 9px; display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: 12px; font-weight: 750; }
.status-dot.error { background: var(--danger); box-shadow: 0 0 0 4px rgba(251, 113, 133, .12); }
.ingress-copy { margin: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
.ingress-counts { margin: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.ingress-counts[hidden] { display: none; }
.ingress-counts div { min-width: 0; padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); }
.ingress-counts dt { color: var(--muted); font-size: 11px; }
.ingress-counts dd { margin: 2px 0 0; color: var(--text); font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
.event { padding: 11px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
.event-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.event strong { font-size: 13px; }
.event time, .event code { color: var(--muted); font-size: 12px; }
.event code { display: block; margin-top: 6px; overflow-wrap: anywhere; }
.event-delivery { margin-top: 5px; color: var(--muted); font-size: 12px; }
.event-tag { padding: 2px 7px; border: 1px solid rgba(251, 191, 36, .35); border-radius: 999px; background: rgba(251, 191, 36, .08); color: #ffe18a; font-size: 11px; }
.empty { min-height: 120px; padding: 26px 16px; display: grid; place-items: center; color: var(--muted); text-align: center; }
.architecture-note { margin-top: 14px; padding: 13px 15px; display: grid; grid-template-columns: auto 1fr; gap: 11px; border: 1px solid rgba(139, 124, 246, .30); border-radius: 12px; background: rgba(139, 124, 246, .07); color: var(--muted); }
@@ -127,6 +139,8 @@
.metrics { grid-template-columns: 1fr 1fr; }
.metric strong { font-size: 17px; }
.form-actions { grid-template-columns: 1fr; }
.ingress-head { align-items: flex-start; flex-direction: column; }
.ingress-counts { grid-template-columns: 1fr 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
@@ -155,7 +169,7 @@
<div class="page-head">
<div>
<h1>单路检测与区域判定</h1>
<p>验证从画面、匿名人员框、track 到区域进入候选事实的工程闭环。平台事件 ID、持久投递和 Alert 由后续 Bell 链路负责。</p>
<p>验证从画面、匿名人员框、track 到区域进入候选事实的工程闭环。Brain 先持久化到 Outbox,再由 Bell 分配平台事件 ID;Alert 仍由后续 Bell 链路负责。</p>
</div>
<div id="mode-badge" class="mode-badge">加载运行状态</div>
</div>
@@ -220,9 +234,22 @@
<section class="card" aria-labelledby="event-title">
<div class="card-head">
<div><h2 id="event-title">最近候选事实</h2><p>只产生 source_event_id;Bell 才生成平台 ULID</p></div>
<div><h2 id="event-title">最近候选事实</h2><p>内存环仅用于观察;可靠性以 Outbox 状态为准</p></div>
<span id="event-count" class="chip"><strong>0</strong> 项</span>
</div>
<section id="event-ingress" class="ingress-summary" aria-labelledby="event-ingress-title" role="status" aria-live="polite" aria-atomic="true">
<div class="ingress-head">
<h3 id="event-ingress-title">Bell 可靠投递</h3>
<span class="ingress-state"><i id="ingress-dot" class="status-dot" aria-hidden="true"></i><span id="ingress-label">未启用</span></span>
</div>
<p id="ingress-copy" class="ingress-copy">可靠投递未启用;候选事实不会被标记为 Bell 已接收。</p>
<dl id="ingress-counts" class="ingress-counts" aria-label="Outbox 投递计数" hidden>
<div><dt>待投递</dt><dd id="ingress-queued">0</dd></div>
<div><dt>投递中</dt><dd id="ingress-delivering">0</dd></div>
<div><dt>已送达</dt><dd id="ingress-delivered">0</dd></div>
<div><dt>死信</dt><dd id="ingress-dead-letter">0</dd></div>
</dl>
</section>
<div id="event-list" class="event-list" aria-live="polite">
<div class="empty">人员从区域外进入后,这里会出现一次候选事实。</div>
</div>
@@ -252,6 +279,7 @@
let state = null;
let offlineSequence = 0;
let offlineTriggered = false;
let ingressSignature = "";
const offlineState = () => {
offlineSequence += 1;
@@ -275,6 +303,7 @@
zone: state && state.zone ? state.zone : {id: "zone-demo-01", name: "楼梯口危险区", version: 1, points: parsePoints(false)},
detections: bbox[2] - bbox[0] > .01 ? [{track_id: "P-DEMO-001", class: "person", bbox, detector_score: null, inside_zone: inside}] : [],
events,
event_ingress: {enabled: false},
last_error_code: null
};
};
@@ -376,11 +405,48 @@
const tag = document.createElement("span"); tag.className = "event-tag"; tag.textContent = event.fixture ? "夹具候选" : "检测候选";
head.append(title, tag);
const time = document.createElement("time"); time.dateTime = event.occurred_at; time.textContent = `${formatTime(event.occurred_at)} · ${event.kind}`;
const delivery = document.createElement("div"); delivery.className = "event-delivery"; delivery.textContent = event.delivery_status === "outbox_persisted" ? "交接:Outbox 已持久化" : "交接:仅内存观察";
const code = document.createElement("code"); code.textContent = event.source_event_id;
article.append(head, time, code); return article;
article.append(head, time, delivery, code); return article;
}));
}
function renderIngress(value = {enabled: false}) {
const enabled = value.enabled === true;
const queued = Number(value.queued) || 0;
const delivering = Number(value.delivering) || 0;
const delivered = Number(value.delivered) || 0;
const deadLetter = Number(value.dead_letter) || 0;
const errorCode = typeof value.last_error_code === "string" ? value.last_error_code : "";
const nextSignature = JSON.stringify([enabled, queued, delivering, delivered, deadLetter, errorCode]);
if (nextSignature === ingressSignature) return;
ingressSignature = nextSignature;
const dot = $("ingress-dot");
const counts = $("ingress-counts");
counts.hidden = !enabled;
$("ingress-queued").textContent = String(queued);
$("ingress-delivering").textContent = String(delivering);
$("ingress-delivered").textContent = String(delivered);
$("ingress-dead-letter").textContent = String(deadLetter);
if (!enabled) {
dot.className = "status-dot";
$("ingress-label").textContent = "未启用";
$("ingress-copy").textContent = "可靠投递未启用;候选事实不会被标记为 Bell 已接收。";
} else if (deadLetter > 0) {
dot.className = "status-dot error";
$("ingress-label").textContent = "需要处理";
$("ingress-copy").textContent = `存在死信${errorCode ? `(${errorCode})` : ""};请检查 Bell 绑定、鉴权或候选字段后再处理。`;
} else if (errorCode || queued > 0 || delivering > 0) {
dot.className = "status-dot warning";
$("ingress-label").textContent = errorCode ? "正在重试" : "投递中";
$("ingress-copy").textContent = errorCode ? `Outbox 已保留候选并自动重试(${errorCode})。` : "候选已持久化到本地 Outbox,正在等待 Bell 确认。";
} else {
dot.className = "status-dot online";
$("ingress-label").textContent = "投递正常";
$("ingress-copy").textContent = "Outbox 可用;Bell 返回 accepted 或 duplicate 后才计为已送达。";
}
}
function render(next) {
state = next;
$("connection-dot").className = `status-dot ${next.source.connected ? "online" : "warning"}`;
@@ -398,6 +464,7 @@
if (document.activeElement !== $("zone-name") && document.activeElement !== $("zone-points")) {
$("zone-name").value = next.zone.name; setPoints(next.zone.points);
}
renderIngress(next.event_ingress);
renderEvents(next.events);
drawOverlay();
}