mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-06 04:03:50 +08:00
fix: align asset coverage zones and remapping chat
This commit is contained in:
1 parent
fe939800d9
commit
fb6942f2b2
5 files changed
+74
-5
No files matched your search
+6
-1
@@ -570,7 +570,12 @@ WHERE type='root_domain' AND domain=$1 LIMIT 1`, root).Scan(&id, &companyID)
|
||||
link(n.Key, companyKey(n.CompanyID))
|
||||
}
|
||||
case "service":
|
||||
link(n.Key, firstOf(subByDomain[n.Domain], ipByAddr[n.IP], rootByDomain[n.RootDomain]))
|
||||
host, _ := hostPortOf(n)
|
||||
link(n.Key, firstOf(
|
||||
subByDomain[n.Domain], subByDomain[host],
|
||||
ipByAddr[n.IP], ipByAddr[host],
|
||||
rootByDomain[n.RootDomain],
|
||||
))
|
||||
case "endpoint":
|
||||
host, port := hostPortOf(n)
|
||||
parent := firstOf(
|
||||
|
||||
@@ -766,6 +766,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /api/task-archives/delete/batch", s.deleteTaskArchivesBatch)
|
||||
mux.HandleFunc("GET /api/tasks/{id}/coverage", s.taskCoverage)
|
||||
mux.HandleFunc("GET /api/tasks/{id}/coverage-graph", s.taskCoverageGraph)
|
||||
mux.HandleFunc("POST /api/tasks/{id}/asset-mapping", s.taskAssetMapping)
|
||||
mux.HandleFunc("GET /api/tasks/{id}/asset-refs", s.taskAssetRefs)
|
||||
mux.HandleFunc("POST /api/tasks/{id}/assets", s.attachTaskAssets)
|
||||
mux.HandleFunc("DELETE /api/tasks/{id}/assets/{assetID}", s.detachTaskAsset)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// taskAssetMapping starts a visible asset-mapper conversation for the task and
|
||||
// then lets the graph refresh independently. The graph itself remains sourced
|
||||
// from the database; the conversation is the audit trail for the explanation.
|
||||
func (s *Server) taskAssetMapping(w http.ResponseWriter, r *http.Request) {
|
||||
t, ok := s.m.Task(r.PathValue("id"))
|
||||
if !ok {
|
||||
writeErr(w, http.StatusNotFound, "task not found")
|
||||
return
|
||||
}
|
||||
if s.m.pg == nil || s.chatAgentRef() == nil {
|
||||
writeErr(w, http.StatusServiceUnavailable, s.chatUnavailableReason())
|
||||
return
|
||||
}
|
||||
if agentInfo, err := s.m.pg.GetAgentByKey("asset_mapper"); err != nil || agentInfo == nil || !agentInfo.Enabled {
|
||||
writeErr(w, http.StatusServiceUnavailable, "资产梳理 Agent 未启用")
|
||||
return
|
||||
}
|
||||
taskID, _ := strconv.ParseInt(t.ID, 10, 64)
|
||||
description := strings.TrimSpace(t.Description)
|
||||
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)
|
||||
}
|
||||
}
|
||||
s.StartTriggeredRun("asset_mapper", fmt.Sprintf("资产梳理 · 任务 #%d", taskID), message, taskID, false, description, t.Goal)
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{"status": "accepted", "agent_key": "asset_mapper"})
|
||||
}
|
||||
@@ -57,7 +57,9 @@ const zoneMeta: Record<Zone, { label: string; description: string; className: st
|
||||
// 当前后端资产关系以域名、服务和业务资产为主,先用类型推导可视化分区。
|
||||
// 后续接入 VPC/网段/防火墙数据后,可以直接由 API 返回 zone 覆盖这里的推导结果。
|
||||
function zoneOf(node: CoverageGraphNode): Zone {
|
||||
if (node.kind === "root_domain" || node.kind === "subdomain") return "internet";
|
||||
if (node.kind === "company" || node.kind === "root_domain" || node.kind === "subdomain" || node.kind === "ip") {
|
||||
return "internet";
|
||||
}
|
||||
if (node.kind === "service") return "boundary";
|
||||
return "internal";
|
||||
}
|
||||
@@ -500,6 +502,7 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
|
||||
const [selectedFoldId, setSelectedFoldId] = React.useState<string | null>(null);
|
||||
const [activePath, setActivePath] = React.useState<string[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [mapping, setMapping] = React.useState(false);
|
||||
const [lastSyncedAt, setLastSyncedAt] = React.useState<Date | null>(null);
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -530,6 +533,16 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
|
||||
.finally(() => setLoading(false));
|
||||
}, [taskId, coverageEnabled]);
|
||||
|
||||
const remapAssets = React.useCallback(async () => {
|
||||
setMapping(true);
|
||||
try {
|
||||
await api.startAssetMapping(taskId);
|
||||
} finally {
|
||||
setMapping(false);
|
||||
fetchGraph();
|
||||
}
|
||||
}, [fetchGraph, taskId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchGraph();
|
||||
}, [fetchGraph]);
|
||||
@@ -619,6 +632,16 @@ 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) => {
|
||||
@@ -718,7 +741,7 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
|
||||
<div ref={containerRef} className="relative z-10 h-full w-full pt-12" />
|
||||
|
||||
{/* 图例 + 统计 + 刷新(叠加层) */}
|
||||
<div className="bg-card/95 pointer-events-auto absolute top-3 left-3 z-20 flex max-w-[360px] flex-col gap-2.5 rounded-lg border p-3 text-xs shadow-sm backdrop-blur">
|
||||
<div className="bg-card/95 pointer-events-auto absolute bottom-3 left-3 z-20 flex max-h-[calc(100%-1.5rem)] max-w-[360px] flex-col gap-2.5 overflow-auto rounded-lg border p-3 text-xs shadow-sm backdrop-blur">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-violet-100 text-violet-700 dark:bg-violet-950/60 dark:text-violet-300">
|
||||
@@ -745,8 +768,8 @@ function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; covera
|
||||
) : (
|
||||
<span className="text-muted-foreground">{loading ? "加载中…" : "暂无范围内资产(先锚定任务范围)"}</span>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="h-7 gap-1.5 px-2" onClick={fetchGraph} title="重新梳理资产">
|
||||
<RefreshCw className={cn("size-3.5", loading && "animate-spin")} />
|
||||
<Button variant="outline" size="sm" className="h-7 gap-1.5 px-2" onClick={() => void remapAssets()} disabled={mapping} title="重新梳理资产">
|
||||
<RefreshCw className={cn("size-3.5", (loading || mapping) && "animate-spin")} />
|
||||
<span className="hidden sm:inline">重新梳理</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -451,6 +451,7 @@ export const api = {
|
||||
}>(`/tasks/${id}/coverage`),
|
||||
// 资产覆盖图:范围内全部资产 + 连接用的根域名/公司节点,含 tested/in_scope。
|
||||
taskCoverageGraph: (id: string) => get<CoverageGraphData>(`/tasks/${id}/coverage-graph`),
|
||||
startAssetMapping: (id: string) => post<{ status: string; agent_key: string }>(`/tasks/${id}/asset-mapping`, {}),
|
||||
// 全局 llm_usage 聚合(仪表盘新版 token 视图):按 profile 总量 + 按天分桶。
|
||||
usageStats: (days = 365) => get<UsageStats>(`/tokens/usage?days=${days}`),
|
||||
// ---- 目标管理(总览)----
|
||||
|
||||
Reference in new issue
Block a user