feat(kernel): complete tree-first graph tasks 074-080

This commit is contained in:
lix-2026
2026-04-16 22:01:51 +08:00
parent 2ff10fa86c
commit b1d5d97142
65 changed files with 11579 additions and 4606 deletions
@@ -0,0 +1,150 @@
export type MindMapData = {
data: Record<string, unknown>;
children?: unknown[];
};
export type MindmapRouteMeta = {
requestId?: string;
traceId?: string;
workspaceId?: string | null;
documentId?: string;
pageId?: string;
mindmapId?: string;
attachmentId?: string;
updatedAt?: string | null;
};
export type MindmapProjectionNode = {
uid: string;
text: string;
depth: number;
childCount: number;
};
export type MindmapProjection = {
projectionId: string;
projection: "mindmap_subtree";
documentId: string;
mindmapId: string;
rootNodeId: string | null;
title: string;
nodeCount: number;
nodes: MindmapProjectionNode[];
data: MindMapData;
meta: MindmapRouteMeta | null;
};
export const defaultMindmapData: MindMapData = {
data: { text: "中心主题" },
children: [],
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
// simple-mind-map 在文本字段缺失时会直接崩溃,这里统一做兜底归一化。
export const normalizeMindmapData = (input: unknown): unknown => {
if (!input || typeof input !== "object") return defaultMindmapData;
const root = (input as { root?: unknown }).root ?? input;
const walk = (node: unknown) => {
if (!isRecord(node)) return;
if (!isRecord(node.data)) {
node.data = {};
}
const rawText = node.data.text;
node.data.text = typeof rawText === "string" ? rawText : String(rawText ?? "");
const gen = node.data.generalization;
const fixGen = (value: unknown) => {
if (!isRecord(value)) return;
const text = value.text;
value.text = typeof text === "string" ? text : String(text ?? "");
};
if (Array.isArray(gen)) gen.forEach(fixGen);
else fixGen(gen);
if (Array.isArray(node.children)) {
node.children.forEach(walk);
}
};
walk(root);
return input;
};
// 持久化/初始化统一使用根节点对象,避免 wrapper 误传给 simple-mind-map。
export const canonicalizeMindmapData = (input: unknown): MindMapData => {
const normalized = (normalizeMindmapData(input) ?? defaultMindmapData) as Record<string, unknown>;
const root =
normalized && typeof normalized === "object" && "root" in normalized
? normalized.root
: normalized;
return (normalizeMindmapData(root) ?? defaultMindmapData) as MindMapData;
};
export const extractMindmapTitle = (input: unknown): string => {
const canonical = canonicalizeMindmapData(input);
const text = canonical?.data?.text;
if (typeof text === "string" && text.trim()) {
return text.trim();
}
return "未命名导图";
};
export const summarizeMindmapProjectionNodes = (input: unknown): MindmapProjectionNode[] => {
const root = canonicalizeMindmapData(input);
const queue: Array<{ node: MindMapData; depth: number }> = [{ node: root, depth: 0 }];
const nodes: MindmapProjectionNode[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (!current) break;
const uidRaw = current.node?.data?.uid;
const textRaw = current.node?.data?.text;
const children = Array.isArray(current.node?.children) ? current.node.children : [];
nodes.push({
uid:
typeof uidRaw === "string" && uidRaw.trim()
? uidRaw
: `depth:${current.depth}:index:${nodes.length}`,
text: typeof textRaw === "string" && textRaw.trim() ? textRaw.trim() : "未命名节点",
depth: current.depth,
childCount: children.length,
});
children.forEach((child) => {
queue.push({
node: canonicalizeMindmapData(child),
depth: current.depth + 1,
});
});
}
return nodes;
};
export const buildMindmapProjection = (input: {
documentId: string;
mindmapId: string;
data: unknown;
meta?: unknown;
}): MindmapProjection => {
const data = canonicalizeMindmapData(input.data);
const nodes = summarizeMindmapProjectionNodes(data);
const meta = isRecord(input.meta) ? (input.meta as MindmapRouteMeta) : null;
const rootNodeId = nodes[0]?.uid ?? null;
return {
projectionId: `mindmap_projection:${input.documentId}:${input.mindmapId}`,
projection: "mindmap_subtree",
documentId: input.documentId,
mindmapId: input.mindmapId,
rootNodeId,
title: extractMindmapTitle(data),
nodeCount: nodes.length,
nodes,
data,
meta,
};
};