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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 18:22:45 +08:00

262 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 落地页「确定层」推导:只用 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 },
};
}