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,387 @@
export type PageSubtreeInlineNode = {
type?: string;
text?: string;
href?: string;
content?: unknown;
styles?: Record<string, unknown>;
};
export type PageSubtreeBlock = {
id?: string;
type?: string;
props?: Record<string, unknown>;
content?: unknown;
children?: unknown;
};
export type PageSubtreeNodeType =
| "page"
| "section"
| "content_node"
| "reference_anchor"
| "mindmap";
export type PageSubtreeNode = {
id: string;
parentNodeId: string | null;
nodeType: PageSubtreeNodeType;
blockId: string | null;
anchorBlockId: string | null;
depth: number;
metadata: {
title: string | null;
textSnippet: string | null;
blockType: string | null;
headingLevel: number | null;
numbering: string | null;
childCount: number;
order: number;
path: string[];
};
};
export type PageOutlineEntry = {
id: string;
nodeId: string;
anchorBlockId: string | null;
title: string;
level: number;
numbering: string;
};
export type PageEvidenceItem = {
id: string;
nodeId: string;
blockId: string | null;
kind:
| "page"
| "heading"
| "paragraph"
| "list"
| "todo"
| "quote"
| "code"
| "media"
| "reference"
| "table"
| "mindmap"
| "text";
snippet: string;
};
export type PageSubtreeProjection = {
projectionId: string;
projection: "page_tree";
rootNodeId: string;
rootNode: PageSubtreeNode;
subtree: {
rootNodeId: string;
nodes: PageSubtreeNode[];
};
outline: PageOutlineEntry[];
evidence: PageEvidenceItem[];
stats: {
blockCount: number;
headingCount: number;
evidenceCount: number;
maxDepth: number;
};
};
const SNIPPET_MAX_LENGTH = 220;
const pickFirstText = (...values: unknown[]) => {
for (const value of values) {
if (typeof value === "string" && value.trim()) {
return value.trim();
}
}
return "";
};
const normalizeSnippet = (value: string) => value.replace(/\s+/g, " ").trim().slice(0, SNIPPET_MAX_LENGTH);
export const clampHeadingLevel = (value: unknown) => {
const level = Number(value);
if (Number.isNaN(level) || !Number.isFinite(level)) {
return 1;
}
return Math.min(5, Math.max(1, Math.trunc(level)));
};
export const extractPageBlocks = (content: unknown): PageSubtreeBlock[] => {
if (Array.isArray(content)) {
return content as PageSubtreeBlock[];
}
if (content && typeof content === "object") {
const blocks = (content as { blocks?: unknown }).blocks;
if (Array.isArray(blocks)) {
return blocks as PageSubtreeBlock[];
}
}
return [];
};
export const getPageBlockChildren = (value: unknown): PageSubtreeBlock[] => {
if (!Array.isArray(value)) {
return [];
}
return value as PageSubtreeBlock[];
};
export const getInlineText = (value: unknown): string => {
if (typeof value === "string") {
return value;
}
if (!Array.isArray(value)) {
return "";
}
return value
.map((node) => {
if (typeof node === "string") {
return node;
}
if (!node || typeof node !== "object") {
return "";
}
const typedNode = node as PageSubtreeInlineNode;
if (typedNode.type === "link") {
return getInlineText(typedNode.content);
}
return typeof typedNode.text === "string" ? typedNode.text : "";
})
.join("");
};
const getBlockSnippet = (block: PageSubtreeBlock): string => {
const props = block.props ?? {};
const inlineText = normalizeSnippet(getInlineText(block.content));
if (inlineText) {
return inlineText;
}
return normalizeSnippet(
pickFirstText(
props.title,
props.caption,
props.summary,
props.fileName,
props.name,
props.alt,
props.status,
),
);
};
const getBlockDisplayTitle = (block: PageSubtreeBlock, snippet: string) => {
const props = block.props ?? {};
switch (block.type) {
case "heading":
return snippet || "未命名标题";
case "pageReference":
return pickFirstText(props.title, snippet, "页面引用");
case "blockReference":
return pickFirstText(props.title, snippet, "块引用");
case "onlineTable":
return pickFirstText(props.title, snippet, "在线表格");
case "mindmap":
return pickFirstText(props.title, snippet, "思维导图");
case "media":
return pickFirstText(props.caption, props.fileName, snippet, "附件");
case "codeBlock":
return snippet || "代码块";
case "advancedTodo":
return snippet || "任务";
case "quote":
return snippet || "引用";
default:
return snippet || null;
}
};
const getNodeType = (block: PageSubtreeBlock): PageSubtreeNodeType => {
switch (block.type) {
case "heading":
return "section";
case "blockReference":
case "pageReference":
return "reference_anchor";
case "mindmap":
return "mindmap";
default:
return "content_node";
}
};
const getEvidenceKind = (block: PageSubtreeBlock): PageEvidenceItem["kind"] => {
switch (block.type) {
case "heading":
return "heading";
case "paragraph":
return "paragraph";
case "bulletListItem":
case "numberedListItem":
case "checkListItem":
return "list";
case "advancedTodo":
return "todo";
case "quote":
return "quote";
case "codeBlock":
return "code";
case "media":
return "media";
case "pageReference":
case "blockReference":
return "reference";
case "onlineTable":
return "table";
case "mindmap":
return "mindmap";
default:
return "text";
}
};
export function buildPageSubtreeProjection(input: {
documentId: string;
title: string | null;
content: unknown;
}): PageSubtreeProjection {
const documentId = String(input.documentId ?? "").trim();
const rootNodeId = documentId || "page:unknown";
const blocks = extractPageBlocks(input.content);
const rootTitle = pickFirstText(input.title, "无标题");
const rootNode: PageSubtreeNode = {
id: rootNodeId,
parentNodeId: null,
nodeType: "page",
blockId: null,
anchorBlockId: null,
depth: 0,
metadata: {
title: rootTitle,
textSnippet: null,
blockType: "page",
headingLevel: null,
numbering: null,
childCount: blocks.length,
order: 0,
path: [rootNodeId],
},
};
const nodes: PageSubtreeNode[] = [rootNode];
const outline: PageOutlineEntry[] = [];
const evidence: PageEvidenceItem[] = [];
const headingCounters = [0, 0, 0, 0, 0];
const headingStack: Array<{ level: number; nodeId: string }> = [];
let order = 0;
let maxDepth = 0;
const walk = (items: PageSubtreeBlock[], parentBlockNodeId: string | null, depth: number, path: number[]) => {
items.forEach((block, index) => {
const blockId = typeof block.id === "string" && block.id.trim() ? block.id.trim() : null;
const nodeId = blockId ? `block:${blockId}` : `block:auto:${[...path, index].join(".")}`;
const snippet = getBlockSnippet(block);
const headingLevel = block.type === "heading" ? clampHeadingLevel(block.props?.level) : null;
let parentNodeId = parentBlockNodeId ?? headingStack.at(-1)?.nodeId ?? rootNodeId;
let numbering: string | null = null;
if (headingLevel != null) {
while (headingStack.length > 0 && headingStack[headingStack.length - 1]!.level >= headingLevel) {
headingStack.pop();
}
parentNodeId = headingStack.at(-1)?.nodeId ?? parentBlockNodeId ?? rootNodeId;
headingCounters[headingLevel - 1] += 1;
for (let counterIndex = headingLevel; counterIndex < headingCounters.length; counterIndex += 1) {
headingCounters[counterIndex] = 0;
}
numbering = headingCounters
.slice(0, headingLevel)
.filter((value) => value > 0)
.join(".");
}
order += 1;
const children = getPageBlockChildren(block.children);
const node: PageSubtreeNode = {
id: nodeId,
parentNodeId,
nodeType: getNodeType(block),
blockId,
anchorBlockId: blockId,
depth: depth + 1,
metadata: {
title: getBlockDisplayTitle(block, snippet),
textSnippet: snippet || null,
blockType: typeof block.type === "string" ? block.type : null,
headingLevel,
numbering,
childCount: children.length,
order,
path: [rootNodeId, ...path.map(String), String(index)],
},
};
nodes.push(node);
maxDepth = Math.max(maxDepth, node.depth);
if (headingLevel != null) {
outline.push({
id: blockId ?? node.id,
nodeId: node.id,
anchorBlockId: blockId,
title: node.metadata.title ?? "未命名标题",
level: headingLevel,
numbering: numbering ?? "",
});
headingStack.push({ level: headingLevel, nodeId: node.id });
}
if (snippet) {
evidence.push({
id: `evidence:${node.id}`,
nodeId: node.id,
blockId,
kind: getEvidenceKind(block),
snippet,
});
}
if (children.length > 0) {
walk(children, node.id, depth + 1, [...path, index]);
}
});
};
walk(blocks, null, 0, []);
if (rootTitle) {
evidence.unshift({
id: `evidence:${rootNodeId}`,
nodeId: rootNodeId,
blockId: null,
kind: "page",
snippet: rootTitle,
});
}
return {
projectionId: `page_subtree:${rootNodeId}`,
projection: "page_tree",
rootNodeId,
rootNode,
subtree: {
rootNodeId,
nodes,
},
outline,
evidence,
stats: {
blockCount: Math.max(0, nodes.length - 1),
headingCount: outline.length,
evidenceCount: evidence.length,
maxDepth,
},
};
}