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

+63 -2
View File
@@ -41,10 +41,14 @@ import {
useKnowledgeDuplicates,
useKnowledgeNote,
useKnowledgeNotes,
useNoteWikilinks,
useVaultPage,
} from "@/core/knowledge/hooks";
import type { KnowledgeNote } from "@/core/knowledge/types";
import { cn } from "@/lib/utils";
import MarkdownRenderer from "@/strategy-components/MarkdownRenderer";
import MarkdownRenderer, {
type WikiLinkTarget,
} from "@/strategy-components/MarkdownRenderer";
const SOURCE_TYPE_LABELS: Record<string, string> = {
thread: "对话",
@@ -349,10 +353,56 @@ function DedupDialog({
);
}
function VaultPageDialog({
path,
onOpenChange,
}: {
path: string | null;
onOpenChange: (open: boolean) => void;
}) {
const { data: page, isLoading } = useVaultPage(path ?? undefined);
return (
<Dialog open={!!path} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{page?.title || "知识库页面"}</DialogTitle>
<DialogDescription>{path}</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[60vh]">
<div className="p-1">
{isLoading ? (
<div className="text-muted-foreground p-4 text-sm">加载中...</div>
) : page ? (
<MarkdownRenderer content={page.content_md} />
) : (
<div className="text-muted-foreground p-4 text-sm">页面不存在</div>
)}
</div>
</ScrollArea>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
关闭
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function NoteDetail({ noteId }: { noteId: string }) {
const navigate = useNavigate();
const { data: note, isLoading } = useKnowledgeNote(noteId);
const { data: wikiLinks } = useNoteWikilinks(noteId);
const { update, remove } = useKnowledgeActions();
const [vaultPath, setVaultPath] = useState<string | null>(null);
const handleWikiLink = (link: WikiLinkTarget) => {
if (link.kind === "note" && link.note_id) {
navigate(`/page/workspace/knowledge/${link.note_id}`);
} else if (link.kind === "vault" && link.vault_path) {
setVaultPath(link.vault_path);
}
};
const [editing, setEditing] = useState(false);
const [draftTitle, setDraftTitle] = useState("");
const [draftTags, setDraftTags] = useState("");
@@ -361,6 +411,7 @@ function NoteDetail({ noteId }: { noteId: string }) {
useEffect(() => {
setEditing(false);
setVaultPath(null);
}, [noteId]);
useEffect(() => {
@@ -495,7 +546,11 @@ function NoteDetail({ noteId }: { noteId: string }) {
</span>
))}
</div>
<MarkdownRenderer content={note.content_md} />
<MarkdownRenderer
content={note.content_md}
wikiLinks={wikiLinks}
onWikiLinkClick={handleWikiLink}
/>
{note.sources && note.sources.length > 0 ? (
<div className="mt-6 border-t pt-4">
@@ -545,6 +600,12 @@ function NoteDetail({ noteId }: { noteId: string }) {
)}
</div>
</ScrollArea>
<VaultPageDialog
path={vaultPath}
onOpenChange={(open) => {
if (!open) setVaultPath(null);
}}
/>
</div>
);
}