feat(intercept): 增加审批记录详情与执行审计
This commit is contained in:
1 parent
8dae851b9b
commit
9ad323dc4c
22 files changed
+1687
-535
No files matched your search
@@ -38,6 +38,7 @@ import type {
|
||||
FindingsPage,
|
||||
IntentAsset,
|
||||
InterceptApprovalRow,
|
||||
InterceptDetail,
|
||||
InterceptPending,
|
||||
InterceptRule,
|
||||
JudgeConfig,
|
||||
@@ -957,6 +958,7 @@ export const api = {
|
||||
interceptGetOne: (id: number) => get<InterceptPending>(`/intercept/pending/${id}`),
|
||||
interceptDecide: (id: number, decision: "allowed" | "denied") =>
|
||||
post<{ ok: boolean }>(`/intercept/pending/${id}/decide`, { decision }),
|
||||
interceptDetail: (id: number) => get<InterceptDetail>(`/intercept/history/${id}`),
|
||||
interceptHistory: () => get<{ items: InterceptApprovalRow[] }>("/intercept/history").then((r) => arr(r.items)),
|
||||
interceptTask: (taskId: string) =>
|
||||
get<{ items: InterceptApprovalRow[] }>(`/intercept/task/${taskId}`).then((r) => arr(r.items)),
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
Finding,
|
||||
FindingAsset,
|
||||
InterceptApprovalRow,
|
||||
InterceptAudit,
|
||||
InterceptPending,
|
||||
InterceptRule,
|
||||
LLMPoolStatus,
|
||||
@@ -3469,6 +3470,40 @@ export const interceptHistory: InterceptApprovalRow[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Approval detail fixtures use harmless report-writing examples.
|
||||
interceptHistory.unshift({
|
||||
id: 94, task_id: "t-acme-web", agent_name: "work#1", tool_name: "Write",
|
||||
tool_input: { path: "reports/summary.md", content: "# 检查摘要\n\n本轮验证已完成,整理已有证据与后续建议。" },
|
||||
status: "allowed", decision_source: "model", reason: "[模型] 将已有检查结论写入本地报告,不修改业务数据。",
|
||||
created_at: T("2026-07-26T08:00:00Z"), decided_at: T("2026-07-26T08:00:02Z"),
|
||||
conv_title: "", conv_agent_key: "", rule_name: "",
|
||||
});
|
||||
|
||||
export const interceptDetails: Record<number, InterceptAudit> = {
|
||||
94: {
|
||||
run_id: "demo-run-report", tool_use_id: "call-write-report", correlation: "exact",
|
||||
input_digest: "a3b458eca3b458eca3b458eca3b458eca3b458eca3b458eca3b458eca3b458ec1234",
|
||||
user_message: "请整理已经完成的检查,将结论和证据索引写入 reports/summary.md。\n保留待验证项,不修改业务数据。",
|
||||
context: [
|
||||
{ kind: "user", text: "汇总本轮已有证据,生成检查摘要。" },
|
||||
{ kind: "tool_use", tool: "Read", tool_use_id: "call-read-evidence", text: '{"path":"reports/evidence-index.json"}' },
|
||||
{ kind: "tool_result", tool: "Read", tool_use_id: "call-read-evidence", text: "已读取 3 条证据索引。\n记录包含请求时间、结果摘要和本地文件位置。" },
|
||||
],
|
||||
captured_at: T("2026-07-26T08:00:00Z"), initial_action: "allow",
|
||||
initial_reason: "[模型] 将已有检查结论写入本地报告,不修改业务数据。", effective_action: "allow",
|
||||
config_digest: "b4c569fdb4c569fdb4c569fdb4c569fdb4c569fdb4c569fdb4c569fdb4c569fd1234",
|
||||
profile_id: 1, execution_status: "succeeded", output: "Successfully wrote reports/summary.md\n共写入 68 个字符。",
|
||||
execution_ended_at: T("2026-07-26T08:00:03Z"),
|
||||
},
|
||||
93: {
|
||||
run_id: "demo-run-review", tool_use_id: "call-pending-review", correlation: "exact",
|
||||
input_digest: "c5d670aec5d670aec5d670aec5d670aec5d670aec5d670aec5d670aec5d670ae1234",
|
||||
user_message: "检查备份表的用途,涉及删除时需要先确认。", context: [],
|
||||
captured_at: T("2026-07-25T15:14:00Z"), initial_action: "ask",
|
||||
initial_reason: "[模型] 疑似备份表,无法确定是否生产数据。", profile_id: 1, execution_status: "not_started",
|
||||
},
|
||||
};
|
||||
|
||||
// ── Conversations (chat) ─────────────────────────────────────────────────────
|
||||
export const conversations: Conversation[] = [
|
||||
{
|
||||
|
||||
@@ -33,6 +33,9 @@ const delay = (ms = 120) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// Requests mutate a runtime copy, never the exported fixtures. This keeps module
|
||||
// initialization deterministic for tests/HMR while preserving state across mock calls.
|
||||
const mockInterceptHistory = structuredClone(D.interceptHistory);
|
||||
const mockInterceptPending = structuredClone(D.interceptPending);
|
||||
const mockInterceptDetails = structuredClone(D.interceptDetails);
|
||||
const mockTasks = structuredClone(D.tasks);
|
||||
const mockFindings = structuredClone(D.findings);
|
||||
const mockLLMRecords = structuredClone(D.llmRecords);
|
||||
@@ -1981,13 +1984,33 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
if (path === "/intercept/rules" && m === "GET") return { rules: D.interceptRules };
|
||||
if (seg[0] === "intercept" && seg[1] === "rules" && seg[3] === "toggle")
|
||||
return { ok: true, enabled: b.enabled ?? true };
|
||||
if (path === "/intercept/pending" && m === "GET") return { pending: D.interceptPending };
|
||||
if (seg[0] === "intercept" && seg[1] === "pending" && seg[3] === "decide") return { ok: true };
|
||||
if (path === "/intercept/pending" && m === "GET") return { pending: mockInterceptHistory.filter((r) => r.status === "pending") };
|
||||
if (seg[0] === "intercept" && seg[1] === "pending" && seg[3] === "decide") {
|
||||
const id = Number(seg[2]);
|
||||
const row = mockInterceptHistory.find((r) => r.id === id) ?? mockInterceptPending.find((r) => r.id === id);
|
||||
if (!row || row.status !== "pending") throw new Error("审批已处理或不存在,请刷新记录");
|
||||
if (b.decision !== "allowed" && b.decision !== "denied") throw new Error("无效审批动作");
|
||||
row.status = b.decision;
|
||||
row.decided_at = new Date().toISOString();
|
||||
const detail = mockInterceptDetails[id];
|
||||
if (detail) {
|
||||
detail.effective_action = b.decision === "allowed" ? "allow" : "deny";
|
||||
detail.decision_reason = b.decision === "allowed" ? "人工允许执行" : "人工拒绝执行";
|
||||
detail.execution_status = b.decision === "allowed" ? "unknown" : "not_executed";
|
||||
detail.output = b.decision === "allowed" ? "演示模式未执行工具。" : "";
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
if (seg[0] === "intercept" && seg[1] === "history" && seg.length === 3) {
|
||||
const row = mockInterceptHistory.find((r) => r.id === Number(seg[2]));
|
||||
if (!row) throw new Error("审批记录不存在");
|
||||
return { ...row, audit: mockInterceptDetails[row.id] ?? null };
|
||||
}
|
||||
if (seg[0] === "intercept" && seg[1] === "pending" && seg.length === 3 && m === "GET")
|
||||
return D.interceptPending.find((p) => p.id === Number(seg[2])) ?? null;
|
||||
if (path === "/intercept/history") return { items: D.interceptHistory };
|
||||
return mockInterceptPending.find((p) => p.id === Number(seg[2])) ?? null;
|
||||
if (path === "/intercept/history") return { items: mockInterceptHistory };
|
||||
if (seg[0] === "intercept" && seg[1] === "task")
|
||||
return { items: D.interceptHistory.filter((r) => r.task_id === seg[2]) };
|
||||
return { items: mockInterceptHistory.filter((r) => r.task_id === seg[2]) };
|
||||
if (path === "/intercept/tool-config") return { enabled_tools: ["bash"] };
|
||||
if (path === "/intercept/judge" && m === "GET")
|
||||
return {
|
||||
|
||||
@@ -1133,6 +1133,7 @@ export interface InterceptRule {
|
||||
}
|
||||
|
||||
export interface InterceptPending {
|
||||
decision_source?: "rule" | "model" | "unknown" | "";
|
||||
id: number;
|
||||
rule_id?: number;
|
||||
conversation_id?: number;
|
||||
@@ -1246,3 +1247,31 @@ export interface LLMTask {
|
||||
task_id: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// Immutable review snapshot plus separately recorded execution outcome.
|
||||
export interface InterceptAudit {
|
||||
run_id?: string;
|
||||
tool_use_id?: string;
|
||||
correlation: "exact" | "ambiguous" | "unavailable";
|
||||
input_digest: string;
|
||||
user_message: string;
|
||||
user_truncated?: boolean;
|
||||
context: { kind: string; tool?: string; tool_use_id?: string; text: string; is_error?: boolean; truncated?: boolean }[] | null;
|
||||
context_truncated?: boolean;
|
||||
captured_at: string;
|
||||
model_fallback?: boolean;
|
||||
initial_action: "allow" | "ask" | "deny";
|
||||
initial_reason: string;
|
||||
effective_action?: "allow" | "deny";
|
||||
decision_reason?: string;
|
||||
rule_name?: string;
|
||||
config_digest?: string;
|
||||
profile_id?: number;
|
||||
execution_status: "not_started" | "not_executed" | "awaiting_result" | "succeeded" | "failed" | "unknown";
|
||||
output?: string;
|
||||
output_truncated?: boolean;
|
||||
execution_ended_at?: string;
|
||||
}
|
||||
export interface InterceptDetail extends InterceptApprovalRow {
|
||||
audit: InterceptAudit | null;
|
||||
}
|
||||
Reference in new issue
Block a user