新增源码绑定、代码面板、流程分组可视化与分组模式切换

- Web: 点击节点在底部代码面板展示源码(语法高亮、可折叠)
- Web: 画布按目录/文件聚类为背景框,头部新增分组模式切换(目录/文件/无)
- Web: 流程横幅、流程/分组列表与节点详情展示 summary
- CLI: 新增 overview 推导与运行器
- scripts: 手动标注 self-report summary

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
朱葛
2026-07-22 18:22:45 +08:00
parent cb23f814c7
commit 35958b408c
23 changed files with 5313 additions and 1168 deletions
+261
View File
@@ -0,0 +1,261 @@
// 落地页「确定层」推导:只用 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 },
};
}