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:
1 parent
92a46003f8
commit
df3fac1032
13 files changed
+668
-72
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user