-
- #{node.id}
-
+ #{node.id}
{node.priority > 0 && (kind === "goal" || kind === "intent") && (
P{node.priority}
@@ -331,10 +405,11 @@ function BroadcastRow({
来源 {node.origin || "system"}
{Number.isNaN(ts) ? node.ts : new Date(ts).toLocaleString("zh-CN")}
+
{(upstream.length > 0 || downstream.length > 0) && (
-
-
+
+
)}
@@ -362,6 +437,7 @@ export function BroadcastTab({ taskId }: { taskId: string }) {
const [items, setItems] = React.useState([]);
const [edges, setEdges] = React.useState([]);
const [refs, setRefs] = React.useState>({});
+ const [assets, setAssets] = React.useState>({});
const [total, setTotal] = React.useState(0);
const [loaded, setLoaded] = React.useState(false);
const [freshIDs, setFreshIDs] = React.useState>(new Set());
@@ -413,6 +489,7 @@ export function BroadcastTab({ taskId }: { taskId: string }) {
setItems(r.items);
setEdges(r.edges);
setRefs(r.refs);
+ setAssets(r.assets);
}
setTotal(r.total);
if (atLive) {
@@ -454,6 +531,14 @@ export function BroadcastTab({ taskId }: { taskId: string }) {
setPending(0);
};
+ // 服务端的 refs 只补「不在本页的邻居」,同页节点之间的引用要靠 items 自己兜底,
+ // 否则相邻两条播报互相引用时会退化成光秃秃的「节点 #id」。
+ const nodeIndex = React.useMemo(() => {
+ const idx: Record = { ...refs };
+ for (const n of items) idx[n.id] = n;
+ return idx;
+ }, [refs, items]);
+
const pageCount = Math.max(1, Math.ceil(total / size));
const start = total === 0 ? 0 : (page - 1) * size + 1;
const end = (page - 1) * size + items.length;
@@ -579,7 +664,8 @@ export function BroadcastTab({ taskId }: { taskId: string }) {
key={node.id}
node={node}
edges={edges}
- refs={refs}
+ refs={nodeIndex}
+ assets={assets}
now={now}
fresh={freshIDs.has(node.id)}
/>
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 73252b0..cea3e80 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -674,6 +674,7 @@ export const api = {
size: r.size ?? query.size ?? 20,
edges: arr(r.edges),
refs: r.refs ?? {},
+ assets: r.assets ?? {},
}));
},
activity: (task?: string, opts?: { intent?: string; since?: number; limit?: number }) => {
diff --git a/web/src/lib/mock/data.ts b/web/src/lib/mock/data.ts
index 9332261..6eaa336 100644
--- a/web/src/lib/mock/data.ts
+++ b/web/src/lib/mock/data.ts
@@ -3060,8 +3060,17 @@ export const llmConfig = {
// ── Agents ───────────────────────────────────────────────────────────────────
export const agents: Agent[] = [
{
- id: "1001", key: "retester", name: "漏洞复测", role: "assistant", builtin: false, enabled: true,
- description: "从漏洞详情手动启动,保存独立复测结论", max_turns: 0, mcp_count: 0, skill_count: 0, tool_count: 2,
+ id: "1001",
+ key: "retester",
+ name: "漏洞复测",
+ role: "assistant",
+ builtin: false,
+ enabled: true,
+ description: "从漏洞详情手动启动,保存独立复测结论",
+ max_turns: 0,
+ mcp_count: 0,
+ skill_count: 0,
+ tool_count: 2,
},
{
id: "1",
@@ -3478,35 +3487,65 @@ 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",
+ 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: "",
+ 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 = {
94: {
- run_id: "demo-run-report", tool_use_id: "call-write-report", correlation: "exact",
+ 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记录包含请求时间、结果摘要和本地文件位置。" },
+ {
+ 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",
+ 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 个字符。",
+ 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",
+ 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",
+ user_message: "检查备份表的用途,涉及删除时需要先确认。",
+ context: [],
+ captured_at: T("2026-07-25T15:14:00Z"),
+ initial_action: "ask",
+ initial_reason: "[模型] 疑似备份表,无法确定是否生产数据。",
+ profile_id: 1,
+ execution_status: "not_started",
},
};
@@ -3897,6 +3936,25 @@ export const coverageGraph = {
};
// ── 资产在本任务关联的意图/事实/发现(/tasks/{id}/asset-refs)──
+// 播报板 demo:探索节点 → 其锚定资产。真实后端读 exploration_anchors,mock 里静态给几条。
+const NODE_ASSETS: Record = {
+ fi1: [3],
+ fi2: [2, 4],
+ fi3: [5],
+ fiRce: [4],
+ fiPriv: [19],
+ fiJenkins: [21],
+ i1: [2],
+ i7: [4],
+};
+
+export function nodeAssetsFor(nodeId: string): FindingAsset[] {
+ return (NODE_ASSETS[nodeId] ?? []).flatMap((id) => {
+ const asset = assets.find((candidate) => candidate.id === id);
+ return asset ? [assetRef(id)] : [];
+ });
+}
+
export function assetRefsFor(_assetId: number) {
return {
intents: [
diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts
index ded2546..affac15 100644
--- a/web/src/lib/mock/handler.ts
+++ b/web/src/lib/mock/handler.ts
@@ -2172,7 +2172,13 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (node) refs[id] = node;
}
}
- return { items, total: matched.length, page, size, edges, refs };
+ // 顺带带上本页节点(含邻居)锚定的资产,供展开时展示。
+ const assets: Record> = {};
+ for (const id of new Set([...onPage, ...Object.keys(refs)])) {
+ const anchored = D.nodeAssetsFor(id);
+ if (anchored.length > 0) assets[id] = anchored;
+ }
+ return { items, total: matched.length, page, size, edges, refs, assets };
}
if (path === "/exploration/activity" && seg.length === 2) {
const since = Number(q.get("since") ?? 0);
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index 71219f8..d59edc7 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -429,6 +429,8 @@ export interface ExplorationNodePage {
size: number;
edges: Edge[];
refs: Record;
+ // 节点 id → 该节点锚定的资产(播报板展开时顺带展示,含本页节点与其邻居)。
+ assets: Record;
}
export interface ExplorationNodeQuery {