Merge PR #139: 支持按状态和判定来源筛选审批记录

合并 codex/approval-record-filters,解决与 #133 在
web/src/components/approval-records.tsx 的冲突:

- 导入同时保留 InterceptApprovalFilter 与 InterceptPending。
- load() 采用 #133 的独立待审批队列(Promise.allSettled 拉取
  /intercept/pending),并把 #139 的 filter 参数透传给历史分页调用;
  筛选只作用于历史记录,待处理队列不受 filter 影响,保证跨页/被筛
  记录仍可审批。
- decide() 保留 #133 的乐观更新(移除 pendingRows 对应项)叠加 #139
  决策后重新拉取以对齐筛选后的计数与行。

验证:前端 tsc 通过、Biome 仅剩预存告警;go build/vet 通过;
db TestInterceptApprovalFilters(8 子用例)与 server
TestInterceptFilterHTTP 在本地 dev 库均通过。

Closes #139
This commit is contained in:
Autumn-27 committed 2026-09-16 11:16:26 -04:00
commit 13ce2ea6e3
8 files changed
+436 -45

No files matched your search

+11 -15
View File
@@ -2289,21 +2289,17 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
if (seg[0] === "intercept" && seg[1] === "pending" && seg.length === 3 && m === "GET")
return mockInterceptPending.find((p) => p.id === Number(seg[2])) ?? null;
if (path === "/intercept/history") {
if (!q.has("page") && !q.has("size")) return { items: mockInterceptHistory, total: mockInterceptHistory.length };
const page = Math.max(1, Number(q.get("page")) || 1);
const size = Math.min(100, Math.max(1, Number(q.get("size")) || 20));
const offset = (page - 1) * size;
return {
items: mockInterceptHistory.slice(offset, offset + size),
total: mockInterceptHistory.length,
page,
page_size: size,
};
}
if (seg[0] === "intercept" && seg[1] === "task") {
const filtered = mockInterceptHistory.filter((r) => r.task_id === seg[2]);
if (!q.has("page") && !q.has("size")) return { items: filtered, total: filtered.length };
if (path === "/intercept/history" || (seg[0] === "intercept" && seg[1] === "task")) {
const status = q.get("status") || "";
const decisionSource = q.get("decision_source") || "";
if (status && !["pending", "allowed", "denied", "timeout"].includes(status)) throw new Error("无效审批状态");
if (decisionSource && !["model", "rule", "unknown"].includes(decisionSource)) throw new Error("无效判定来源");
const filtered = mockInterceptHistory.filter((row) => {
const source = row.decision_source || (row.rule_id ? "rule" : row.reason?.startsWith("[模型]") ? "model" : "unknown");
return (seg[1] !== "task" || row.task_id === decodeURIComponent(seg[2])) &&
(!status || row.status === status) && (!decisionSource || source === decisionSource);
});
if (!q.has("page") && !q.has("size") && !status && !decisionSource) return { items: filtered, total: filtered.length };
const page = Math.max(1, Number(q.get("page")) || 1);
const size = Math.min(100, Math.max(1, Number(q.get("size")) || 20));
const offset = (page - 1) * size;