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
@@ -95,6 +95,10 @@ class SearchRequest(BaseModel):
|
||||
limit: int = Field(default=8, ge=1, le=50)
|
||||
|
||||
|
||||
class ResolveWikilinksRequest(BaseModel):
|
||||
targets: list[str] = Field(default_factory=list, description="`[[wikilink]]` targets to resolve to note ids / vault pages.")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -198,7 +202,7 @@ async def ingest_thread(thread_id: str, body: IngestThreadRequest, request: Requ
|
||||
)
|
||||
except EmptyThreadError:
|
||||
raise HTTPException(status_code=400, detail="Thread has no sediment-able content")
|
||||
return {"note": result["note"], "created": result["created"]}
|
||||
return {"note": result["note"], "created": result["created"], "notes": result.get("notes", [result["note"]])}
|
||||
|
||||
|
||||
@router.post("/ingest/threads/batch")
|
||||
@@ -356,6 +360,44 @@ async def search(body: SearchRequest, request: Request) -> dict[str, Any]:
|
||||
return {"items": items}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Wikilinks ([[...]] resolution + vault meta-page viewer)
|
||||
# --------------------------------------------------------------------------- #
|
||||
@router.post("/resolve")
|
||||
@require_auth
|
||||
async def resolve_wikilinks(body: ResolveWikilinksRequest, request: Request) -> dict[str, Any]:
|
||||
"""Resolve ``[[wikilink]]`` targets to navigable destinations.
|
||||
|
||||
Each target resolves to a knowledge note (``kind=note``), a vault meta/hub
|
||||
page (``kind=vault``), or nothing (``kind=missing``). Powers clickable
|
||||
wikilinks in the knowledge UI.
|
||||
"""
|
||||
service = get_knowledge_service(request)
|
||||
return {"resolved": await service.resolve_wikilinks(body.targets)}
|
||||
|
||||
|
||||
@router.get("/notes/{note_id}/wikilinks")
|
||||
@require_auth
|
||||
async def resolve_note_wikilinks(note_id: str, request: Request) -> dict[str, Any]:
|
||||
"""Resolve every ``[[wikilink]]`` found in a note's body in one call."""
|
||||
service = get_knowledge_service(request)
|
||||
resolved = await service.resolve_note_wikilinks(note_id)
|
||||
if resolved is None:
|
||||
raise HTTPException(status_code=404, detail="Knowledge note not found")
|
||||
return {"resolved": resolved}
|
||||
|
||||
|
||||
@router.get("/vault-page")
|
||||
@require_auth
|
||||
async def get_vault_page(request: Request, path: str) -> dict[str, Any]:
|
||||
"""Return a raw vault Markdown meta/hub page (no DB note) by its path."""
|
||||
service = get_knowledge_service(request)
|
||||
page = service.read_vault_page(path)
|
||||
if page is None:
|
||||
raise HTTPException(status_code=404, detail="Vault page not found")
|
||||
return page
|
||||
|
||||
|
||||
class MergeRequest(BaseModel):
|
||||
primary_id: str
|
||||
duplicate_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
Reference in new issue
Block a user