35958b408c
- Web: 点击节点在底部代码面板展示源码(语法高亮、可折叠) - Web: 画布按目录/文件聚类为背景框,头部新增分组模式切换(目录/文件/无) - Web: 流程横幅、流程/分组列表与节点详情展示 summary - CLI: 新增 overview 推导与运行器 - scripts: 手动标注 self-report summary Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
// 落地页「确定层」推导:只用 report JSON 里已确定的结构,
|
||
// 不依赖任何 AI / 人工,产出一张“项目地图”。
|
||
|
||
import type { Report, Func, Flow, Edge, ExternalSystem, Interaction } from "./types";
|
||
|
||
export interface OverviewProjectInfo {
|
||
name: string;
|
||
languages: string[];
|
||
fileCount: number;
|
||
functionCount: number;
|
||
edgeCount: number;
|
||
loc?: number;
|
||
}
|
||
|
||
export interface HttpEndpoint {
|
||
method: string;
|
||
path: string;
|
||
funcId: string;
|
||
funcName: string;
|
||
file?: string;
|
||
flowId?: string;
|
||
flowSize?: number; // 该入口调用链覆盖的函数数
|
||
touchesDb: boolean; // 调用链是否触达数据库
|
||
callsService: boolean; // 调用链是否发起外部服务请求
|
||
}
|
||
|
||
export interface EndpointGroup {
|
||
key: string; // 业务域,如 auth / tasks
|
||
endpoints: HttpEndpoint[];
|
||
}
|
||
|
||
export interface OtherEntry {
|
||
funcId: string;
|
||
funcName: string;
|
||
file?: string;
|
||
flowId?: string;
|
||
flowSize?: number;
|
||
touchesDb: boolean;
|
||
callsService: boolean;
|
||
}
|
||
|
||
export interface ExternalSummary {
|
||
databases: { name: string; engine?: string; tableCount: number; tables: string[] }[];
|
||
services: { name: string; protocol?: string; endpointCount: number; endpoints: string[] }[];
|
||
}
|
||
|
||
export interface FuncRank {
|
||
funcId: string;
|
||
funcName: string;
|
||
file?: string;
|
||
fanIn: number; // 被多少处调用
|
||
}
|
||
|
||
export interface FlowRank {
|
||
flowId: string;
|
||
name: string;
|
||
size: number; // 覆盖函数数
|
||
touchesDb: boolean;
|
||
callsService: boolean;
|
||
}
|
||
|
||
export interface Overview {
|
||
project: OverviewProjectInfo;
|
||
entrypoints: {
|
||
total: number;
|
||
httpGroups: EndpointGroup[];
|
||
httpCount: number;
|
||
others: OtherEntry[];
|
||
};
|
||
externals: ExternalSummary;
|
||
hotspots: {
|
||
mostCalled: FuncRank[];
|
||
largestFlows: FlowRank[];
|
||
};
|
||
}
|
||
|
||
// ---------- 工具 ----------
|
||
|
||
function apiInOf(fn: Func): Interaction | undefined {
|
||
return (fn.interactions ?? []).find((it) => it.kind === "api" && it.direction === "in");
|
||
}
|
||
|
||
function groupKeyFromPath(path: string): string {
|
||
// /api/auth/sso-login -> auth ; /api/tasks/:id -> tasks ; /health -> health
|
||
const segs = path.split("/").filter(Boolean);
|
||
if (segs.length === 0) return "(root)";
|
||
const idx = segs[0] === "api" ? 1 : 0;
|
||
return segs[idx] ?? segs[0];
|
||
}
|
||
|
||
// 从某入口函数出发,沿调用/触发边可达的所有函数 id(有界 BFS)
|
||
function reachableFuncs(startId: string, adjacency: Map<string, string[]>, limit = 2000): Set<string> {
|
||
const seen = new Set<string>([startId]);
|
||
const queue = [startId];
|
||
while (queue.length && seen.size < limit) {
|
||
const cur = queue.shift()!;
|
||
for (const next of adjacency.get(cur) ?? []) {
|
||
if (!seen.has(next)) {
|
||
seen.add(next);
|
||
queue.push(next);
|
||
}
|
||
}
|
||
}
|
||
return seen;
|
||
}
|
||
|
||
// ---------- 主推导 ----------
|
||
|
||
export function deriveOverview(report: Report): Overview {
|
||
const funcById = new Map<string, Func>();
|
||
for (const fn of report.functions) funcById.set(fn.id, fn);
|
||
|
||
// 调用/触发邻接表(用于可达性 & fan-in)
|
||
const adjacency = new Map<string, string[]>();
|
||
const fanIn = new Map<string, number>();
|
||
for (const e of report.edges as Edge[]) {
|
||
if (e.kind === "call" || e.kind === "trigger") {
|
||
if (!adjacency.has(e.sourceId)) adjacency.set(e.sourceId, []);
|
||
adjacency.get(e.sourceId)!.push(e.targetId);
|
||
fanIn.set(e.targetId, (fanIn.get(e.targetId) ?? 0) + 1);
|
||
}
|
||
}
|
||
|
||
// 入口函数 -> 对应的 call flow(用 entryNodeId 关联)
|
||
const flowByEntry = new Map<string, Flow>();
|
||
for (const fl of report.flows) {
|
||
if (fl.entryNodeId) flowByEntry.set(fl.entryNodeId, fl);
|
||
}
|
||
|
||
// 判定一个函数集合是否触达 db / 外部服务
|
||
const dbFuncIds = new Set<string>();
|
||
const svcFuncIds = new Set<string>();
|
||
for (const fn of report.functions) {
|
||
for (const it of fn.interactions ?? []) {
|
||
if (it.kind === "db") dbFuncIds.add(fn.id);
|
||
if (it.kind === "api" && it.direction === "out") svcFuncIds.add(fn.id);
|
||
}
|
||
}
|
||
|
||
const analyzeEntry = (fn: Func) => {
|
||
const flow = flowByEntry.get(fn.id);
|
||
const reach = reachableFuncs(fn.id, adjacency);
|
||
let touchesDb = false;
|
||
let callsService = false;
|
||
for (const id of reach) {
|
||
if (dbFuncIds.has(id)) touchesDb = true;
|
||
if (svcFuncIds.has(id)) callsService = true;
|
||
}
|
||
return {
|
||
flowId: flow?.id,
|
||
flowSize: flow ? flow.steps.length : reach.size,
|
||
touchesDb,
|
||
callsService,
|
||
};
|
||
};
|
||
|
||
// 分类入口
|
||
const httpEndpoints: HttpEndpoint[] = [];
|
||
const others: OtherEntry[] = [];
|
||
for (const fn of report.functions) {
|
||
if (!fn.isEntry) continue;
|
||
const api = apiInOf(fn);
|
||
const info = analyzeEntry(fn);
|
||
if (api && api.kind === "api") {
|
||
httpEndpoints.push({
|
||
method: api.method ?? "",
|
||
path: api.path ?? "",
|
||
funcId: fn.id,
|
||
funcName: fn.name,
|
||
file: fn.location?.file,
|
||
...info,
|
||
});
|
||
} else {
|
||
others.push({
|
||
funcId: fn.id,
|
||
funcName: fn.name,
|
||
file: fn.location?.file,
|
||
...info,
|
||
});
|
||
}
|
||
}
|
||
|
||
// HTTP 分组
|
||
const groupMap = new Map<string, HttpEndpoint[]>();
|
||
for (const ep of httpEndpoints) {
|
||
const key = groupKeyFromPath(ep.path);
|
||
if (!groupMap.has(key)) groupMap.set(key, []);
|
||
groupMap.get(key)!.push(ep);
|
||
}
|
||
const httpGroups: EndpointGroup[] = [...groupMap.entries()]
|
||
.map(([key, endpoints]) => ({
|
||
key,
|
||
endpoints: endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method)),
|
||
}))
|
||
.sort((a, b) => b.endpoints.length - a.endpoints.length || a.key.localeCompare(b.key));
|
||
|
||
// 外部系统汇总
|
||
const externals: ExternalSummary = { databases: [], services: [] };
|
||
for (const sys of (report.externalSystems ?? []) as ExternalSystem[]) {
|
||
if (sys.kind === "database") {
|
||
externals.databases.push({
|
||
name: sys.name,
|
||
engine: sys.engine,
|
||
tableCount: sys.tables.length,
|
||
tables: sys.tables,
|
||
});
|
||
} else {
|
||
externals.services.push({
|
||
name: sys.name,
|
||
protocol: sys.protocol,
|
||
endpointCount: sys.endpoints.length,
|
||
endpoints: sys.endpoints,
|
||
});
|
||
}
|
||
}
|
||
|
||
// 热点:被调用最多的函数
|
||
const mostCalled: FuncRank[] = [...fanIn.entries()]
|
||
.map(([funcId, count]) => {
|
||
const fn = funcById.get(funcId);
|
||
return { funcId, funcName: fn?.name ?? funcId, file: fn?.location?.file, fanIn: count };
|
||
})
|
||
.filter((r) => !!funcById.get(r.funcId))
|
||
.sort((a, b) => b.fanIn - a.fanIn)
|
||
.slice(0, 10);
|
||
|
||
// 热点:最大的调用链
|
||
const largestFlows: FlowRank[] = report.flows
|
||
.map((fl) => {
|
||
const entry = fl.entryNodeId ? funcById.get(fl.entryNodeId) : undefined;
|
||
const info = entry ? analyzeEntry(entry) : { touchesDb: false, callsService: false };
|
||
return {
|
||
flowId: fl.id,
|
||
name: fl.name,
|
||
size: fl.steps.length,
|
||
touchesDb: info.touchesDb,
|
||
callsService: info.callsService,
|
||
};
|
||
})
|
||
.sort((a, b) => b.size - a.size)
|
||
.slice(0, 10);
|
||
|
||
return {
|
||
project: {
|
||
name: report.project.name,
|
||
languages: report.project.languages,
|
||
fileCount: report.stats.fileCount,
|
||
functionCount: report.stats.functionCount,
|
||
edgeCount: report.stats.edgeCount,
|
||
loc: report.stats.loc,
|
||
},
|
||
entrypoints: {
|
||
total: httpEndpoints.length + others.length,
|
||
httpGroups,
|
||
httpCount: httpEndpoints.length,
|
||
others: others.sort((a, b) => (b.flowSize ?? 0) - (a.flowSize ?? 0)),
|
||
},
|
||
externals,
|
||
hotspots: { mostCalled, largestFlows },
|
||
};
|
||
}
|