feat(knowledge): 可跳转 wikilink + 真实相关 note + 一对话多页

- [[wikilink]] 解析器:note id / vault_path / 标题 / vault 元页 / missing
- 前端 MarkdownRenderer 把非数字 [[...]] 渲染成可点链接,元页弹窗查看,缺失置灰
- vault 脚手架补建项目 hub 页,三个结构性链接(index/taxonomy/hub)始终可跳
- capture 时按标题+摘要+区分性标签检索同主题 note,注入真实 [[标题]] 到 Related + 记 related 图谱边
- LLM 提取支持 notes[] 数组,一次对话拆 1~4 页并按标题互链(multi_note_enabled 开关)
- 新增接口 POST /resolve、GET /notes/{id}/wikilinks、GET /vault-page
- 新增配置 related_notes_enabled / related_notes_limit / multi_note_enabled

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-08 21:20:31 +08:00
1 parent 92a46003f8
commit df3fac1032
13 files changed
+668 -72

No files matched your search

+31
View File
@@ -10,6 +10,8 @@ import type {
KnowledgeListResponse,
KnowledgeNote,
KnowledgeSearchItem,
VaultPage,
WikilinkResolutionMap,
} from "./types";
const baseUrl = () => `${getBackendBaseURL()}/api/knowledge`;
@@ -125,6 +127,35 @@ export async function ingestThreadsBatch(payload: BatchIngestPayload = {}) {
);
}
/** Resolve `[[wikilink]]` targets to note ids / vault pages. */
export async function resolveWikilinks(targets: string[]) {
const res = await parseResponse<{ resolved: WikilinkResolutionMap }>(
await apiFetch(`${baseUrl()}/resolve`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targets }),
}),
);
return res.resolved;
}
/** Resolve every `[[wikilink]]` inside a note's body in one call. */
export async function resolveNoteWikilinks(noteId: string) {
const res = await parseResponse<{ resolved: WikilinkResolutionMap }>(
await apiFetch(
`${baseUrl()}/notes/${encodeURIComponent(noteId)}/wikilinks`,
),
);
return res.resolved;
}
/** Fetch a raw vault meta/hub Markdown page (no DB note) by its path. */
export async function fetchVaultPage(path: string) {
return parseResponse<VaultPage>(
await apiFetch(`${baseUrl()}/vault-page?path=${encodeURIComponent(path)}`),
);
}
export async function exportKnowledgeGraph() {
return parseResponse<GraphExportResult>(
await apiFetch(`${baseUrl()}/export/graph`, { method: "POST" }),
+20
View File
@@ -9,12 +9,14 @@ import {
createKnowledgeNote,
deleteKnowledgeNote,
exportKnowledgeGraph,
fetchVaultPage,
getKnowledgeNote,
ingestThread,
ingestThreadsBatch,
listDuplicates,
listKnowledgeNotes,
mergeKnowledgeNotes,
resolveNoteWikilinks,
searchKnowledge,
updateKnowledgeNote,
} from "./api";
@@ -40,6 +42,24 @@ export function useKnowledgeNote(noteId: string | undefined) {
});
}
export function useNoteWikilinks(noteId: string | undefined) {
return useQuery({
queryKey: [...ROOT_KEY, "wikilinks", noteId],
queryFn: () => resolveNoteWikilinks(noteId as string),
enabled: !!noteId,
refetchOnWindowFocus: false,
});
}
export function useVaultPage(path: string | undefined) {
return useQuery({
queryKey: [...ROOT_KEY, "vault-page", path],
queryFn: () => fetchVaultPage(path as string),
enabled: !!path,
refetchOnWindowFocus: false,
});
}
export function useKnowledgeSearch(query: string, enabled: boolean) {
return useQuery({
queryKey: [...ROOT_KEY, "search", query],
+21
View File
@@ -52,6 +52,27 @@ export interface KnowledgeListParams {
offset?: number;
}
export type WikilinkKind = "note" | "vault" | "missing";
export interface WikilinkResolution {
target: string;
normalized?: string;
kind: WikilinkKind;
exists: boolean;
note_id?: string;
vault_path?: string;
title?: string;
}
/** Map of original `[[target]]` string → its resolution. */
export type WikilinkResolutionMap = Record<string, WikilinkResolution>;
export interface VaultPage {
path: string;
title: string;
content_md: string;
}
export interface KnowledgeSearchItem {
note_id: string;
chunk_id?: string | null;