feat(web): 播报板节点详情展示上游节点信息与锚定资产

上游/下游节点从只显示 id 改为悬停弹出节点名片(类型/状态/来源/时间/摘要/payload);
展开时顺带展示该节点锚定的资产(类型标签 + 可辨识文本)。

服务端 /exploration/nodes 新增 assets 字段(节点id→资产),读 exploration_anchors
并复用发现列表的资产打标逻辑;同页节点互引时用本页 items 兜底 refs 查找表。
This commit is contained in:
Autumn-27 committed 2026-09-17 11:23:53 -04:00
1 parent 3b60191e86
commit d95f0d002d
6 files changed
+214 -36

No files matched your search

+31 -6
View File
@@ -2542,15 +2542,40 @@ func (s *Server) explorationNodes(w http.ResponseWriter, r *http.Request) {
refs[i64s(n.ID)] = taskNodeDTO(n)
}
writeJSON(w, 200, map[string]any{
"items": taskNodeDTOs(nodes),
"total": total,
"page": page,
"size": size,
"edges": edgeDTOs(edges),
"refs": refs,
"items": taskNodeDTOs(nodes),
"total": total,
"page": page,
"size": size,
"edges": edgeDTOs(edges),
"refs": refs,
"assets": s.nodeAnchoredAssets(t, append(ids, neighbourIDs...)),
})
}
// nodeAnchoredAssets resolves the exploration_anchors of the given nodes into
// display-ready asset labels, keyed by node id. Anchors are provenance decoration
// for the 播报板 — a failure here must not cost the caller its page, so errors are
// logged and degrade to "no assets".
func (s *Server) nodeAnchoredAssets(t *Task, nodeIDs []int64) map[string][]FindingAssetDTO {
out := map[string][]FindingAssetDTO{}
anchors, err := t.Store.NodeAssets(nodeIDs)
if err != nil {
log.Printf("[broadcast] node assets: %v", err)
return out
}
var flat []int64
for _, ids := range anchors {
flat = append(flat, ids...)
}
assets := s.resolveAssetIDs(flat)
for nodeID, ids := range anchors {
if dtos := findingAssetDTOs(ids, assets); len(dtos) > 0 {
out[i64s(nodeID)] = dtos
}
}
return out
}
// csvValues splits a comma-separated query parameter, dropping empty entries.
func csvValues(s string) []string {
out := []string{}