fix: bound asset mapping context and use layered topology

This commit is contained in:
ji ruo committed 2026-10-28 19:54:10 +08:00
1 parent fb6942f2b2
commit 6b0acf20f6
2 files changed
+45 -37

No files matched your search

+27 -2
View File
@@ -30,8 +30,33 @@ func (s *Server) taskAssetMapping(w http.ResponseWriter, r *http.Request) {
message := fmt.Sprintf("请重新梳理任务 #%d 的资产关系。只使用当前任务及其直接关联任务的真实资产数据,按互联网入口、边界服务、内网业务三层说明关系;不要探测、不要修改资产库,也不要把没有证据的连接描述成已验证通路。", taskID)
if as := s.m.Assets(); as != nil {
if graph, err := as.BuildCoverageGraph(taskID, t.ExpID); err == nil {
snapshot, _ := json.Marshal(map[string]any{"nodes": graph.Nodes, "edges": graph.Edges})
message += "\n\n页面刚读取的任务资产图真实快照如下;优先依据这份快照解释,避免把全局资产误当成本任务资产:\n" + string(snapshot)
type nodeSample struct {
Key, Kind, Label, Domain, IP string
Port int
Tested, InScope bool
}
counts := map[string]int{}
sample := make([]nodeSample, 0, 60)
for _, node := range graph.Nodes {
counts[node.Kind]++
if len(sample) < 60 {
sample = append(sample, nodeSample{
Key: node.Key, Kind: node.Kind, Label: node.Label, Domain: node.Domain,
IP: node.IP, Port: node.Port, Tested: node.Tested, InScope: node.InScope,
})
}
}
edges := graph.Edges
truncatedEdges := len(edges) > 80
if truncatedEdges {
edges = edges[:80]
}
snapshot, _ := json.Marshal(map[string]any{
"nodes_total": len(graph.Nodes), "edges_total": len(graph.Edges),
"counts_by_kind": counts, "node_sample": sample, "edge_sample": edges,
"truncated": len(graph.Nodes) > len(sample) || truncatedEdges,
})
message += "\n\n页面刚读取的任务资产图真实摘要如下;只依据摘要和任务范围解释,避免把全局资产误当成本任务资产。未列出的节点不要逐一补写:\n" + string(snapshot)
}
}
s.StartTriggeredRun("asset_mapper", fmt.Sprintf("资产梳理 · 任务 #%d", taskID), message, taskID, false, description, t.Goal)
@@ -29,8 +29,8 @@ import type { CoverageAssetRef, CoverageAssetRefs, CoverageGraphEdge, CoverageGr
import { cn } from "@/lib/utils";
// 每个父节点下、同一类型的子节点默认展示的数量;超出折叠,"展示更多"每次再拉这么多。
const FOLD_LIMIT = 20;
const FOLD_STEP = 20;
const FOLD_LIMIT = 12;
const FOLD_STEP = 12;
type Kind = CoverageGraphNode["kind"];
@@ -539,9 +539,8 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
await api.startAssetMapping(taskId);
} finally {
setMapping(false);
fetchGraph();
}
}, [fetchGraph, taskId]);
}, [taskId]);
React.useEffect(() => {
fetchGraph();
@@ -566,8 +565,11 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
gDataRef.current = {
nodes: toG6Nodes(renderNodes, new Set(activePath)),
edges: renderEdges.map((e) => ({
source: e.src,
target: e.dst,
// The API stores child → parent containment. Topology reads naturally
// from internet entry → boundary service → internal asset, so reverse
// the visual arrow without changing the source graph or lineage logic.
source: e.dst,
target: e.src,
active: activePath.includes(e.src) && activePath.includes(e.dst),
})),
};
@@ -579,9 +581,8 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
const graph = graphRef.current;
if (!graph || graph.destroyed) return;
graph.setData(gDataRef.current);
// render() 异步跑 d3-force 布局;若组件在布局落地前被卸载/销毁,g6 会在已清空的
// context 上访问 transform 抛错(见 runtime/layout transformDataAfterLayout)。这是纯
// teardown 竞态,吞掉它,不影响功能;真正的渲染错误(图未销毁)仍打日志。
// render() 异步跑布局;若组件在布局落地前被卸载/销毁,g6 会在已清空的
// context 上访问 transform 抛错。这是纯 teardown 竞态,吞掉它,不影响功能。
void graph.render().catch((err) => {
if (!graph.destroyed) console.error("[coverage-graph] render:", err);
});
@@ -631,33 +632,15 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
},
},
layout: {
type: "d3-force",
x: {
strength: 0.28,
x: (d: unknown) => {
const node = renderMapRef.current.get(nd(d).id);
const width = containerRef.current?.clientWidth ?? 900;
const zone = node && !node.fold ? zoneOf(node.node) : "boundary";
return width * (zone === "internet" ? 0.17 : zone === "boundary" ? 0.5 : 0.83);
},
},
y: { strength: 0.04, y: () => (containerRef.current?.clientHeight ?? 600) * 0.52 },
collide: { radius: (d: unknown) => (nd(d).size ? nd(d).size : 20) + 8 },
link: {
distance: (edge: unknown) => {
// 顶层(公司/根域名)离子节点远一点,叶子近一点。edge.source 可能是 id 或已解析节点。
const s = (edge as { source: string | { id?: string } }).source;
const srcId = typeof s === "string" ? s : (s?.id ?? "");
const src = renderMapRef.current.get(srcId);
const k = src && !src.fold ? src.kind : "endpoint";
return k === "company" || k === "root_domain" ? 120 : 60;
},
},
manyBody: {
strength: (d: unknown) => (nd(d).kind === "endpoint" || nd(d).fold ? -60 : -200),
},
// A layered DAG is more legible for network topology than a force
// cloud: parents stay on the left and services/assets flow right.
type: "dagre",
rankdir: "LR",
ranker: "network-simplex",
nodesep: 34,
ranksep: 170,
},
behaviors: ["drag-element-force", "drag-canvas", "zoom-canvas"],
behaviors: ["drag-element", "drag-canvas", "zoom-canvas"],
});
graph.on("node:click", (evt: unknown) => {
const id = (evt as { target?: { id?: string } }).target?.id;