feat(knowledge): 项目名改 zncm + 去除元链接噪音 + 创建人显示人名
- project_name 默认 deerflow → zncm(config.yaml + 配置默认 + 模块内字面量)
- related_links() 返回空:笔记 Related 不再注入 index/_meta/taxonomy/hub 结构链接,
只保留真实相关笔记的可点 [[标题]];无相关笔记且无来源时不再渲染 ## Related 段
- 移除随之多余的项目 hub 页脚手架(index.md/_meta/taxonomy.md 仍由 obsidian 脚手架保留,可解析)
- GET /notes/{id} 用 auth provider 把 created_by/updated_by(UUID) 解析为邮箱
(created_by_name/updated_by_name),前端 footer 显示人名、解析不到才显示短 id
- 仅对新沉淀笔记生效,现有笔记不动
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
df3fac1032
commit
e7ff07debd
11 files changed
+83
-61
No files matched your search
+1
-1
@@ -54,7 +54,7 @@ class KnowledgeConfig(BaseModel):
|
||||
default="data/knowledge/obsidian-vault",
|
||||
description="Vault directory (relative to DEER_FLOW_HOME, or absolute).",
|
||||
)
|
||||
project_name: str = Field(default="deerflow", description="Project namespace for projects/<name>/ pages.")
|
||||
project_name: str = Field(default="zncm", description="Project namespace for projects/<name>/ pages.")
|
||||
auto_ingest_enabled: bool = Field(default=False, description="Auto-sediment conversations in the background (phase 3).")
|
||||
auto_ingest_min_chars: int = Field(default=300, description="Minimum characters before auto-ingest considers a turn.")
|
||||
llm_extract_enabled: bool = Field(default=True, description="Distill notes with an LLM (wiki-capture style) instead of the rule-based fallback.")
|
||||
|
||||
@@ -104,26 +104,33 @@ def _summary(turns: list[dict[str, str]], *, limit: int = 280) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def related_links(project_name: str | None) -> list[str]:
|
||||
"""Structural wikilinks every note carries (wiki-capture requirement).
|
||||
def related_links(project_name: str | None) -> list[str]: # noqa: ARG001
|
||||
"""Structural wikilinks every note used to carry.
|
||||
|
||||
These point at vault meta/hub pages (``index`` / ``_meta/taxonomy`` / the
|
||||
project hub) that always exist, so they resolve to navigable pages. Content-
|
||||
specific "related notes" are injected separately by the service layer (which
|
||||
has DB access) on top of these.
|
||||
Product decision: notes no longer auto-link the vault meta/hub pages
|
||||
(``index`` / ``_meta/taxonomy`` / the project hub) — they were noise in every
|
||||
note's ``## Related``. Only real, content-specific related notes (injected by
|
||||
the service layer, which has DB access) are linked now. Kept as a function
|
||||
(returns empty) so existing callers and the wiki-capture contract stay intact.
|
||||
"""
|
||||
from deerflow.knowledge.markdown_vault import slugify
|
||||
|
||||
links = ["[[index]]", "[[_meta/taxonomy]]"]
|
||||
if project_name:
|
||||
links.append(f"[[projects/{slugify(project_name)}/synthesis/product-knowledge-base]]")
|
||||
return links
|
||||
return []
|
||||
|
||||
|
||||
# Backwards-compatible internal alias.
|
||||
_related_links = related_links
|
||||
|
||||
|
||||
def _related_section_md(related_lines: list[str], source_lines: list[str]) -> str:
|
||||
"""Render the ``## Related`` section, or ``""`` when there is nothing to show."""
|
||||
if not related_lines and not source_lines:
|
||||
return ""
|
||||
inner = "\n".join(related_lines)
|
||||
if source_lines:
|
||||
sources_block = "### Sources\n\n" + "\n".join(source_lines)
|
||||
inner = (inner + "\n\n" + sources_block) if inner else sources_block
|
||||
return f"## Related\n\n{inner}\n"
|
||||
|
||||
|
||||
def _build_body(
|
||||
*,
|
||||
title: str,
|
||||
@@ -155,12 +162,9 @@ def _build_body(
|
||||
|
||||
implications = "- 后续可在相关问答中参考本页知识。\n- 如内容过期或冲突,请编辑或归档。"
|
||||
|
||||
related = "\n".join(f"- {link}" for link in _related_links(project_name))
|
||||
if sources:
|
||||
related += "\n\n### Sources\n\n"
|
||||
for s in sources[:20]:
|
||||
label = s.title or s.url or "来源"
|
||||
related += f"- {label}" + (f" — {s.url}" if s.url else "") + "\n"
|
||||
related_lines = [f"- {link}" for link in _related_links(project_name)]
|
||||
source_lines = [f"- {s.title or s.url or '来源'}" + (f" — {s.url}" if s.url else "") for s in sources[:20]]
|
||||
related_section = _related_section_md(related_lines, source_lines)
|
||||
|
||||
return (
|
||||
f"# {title}\n\n"
|
||||
@@ -168,7 +172,7 @@ def _build_body(
|
||||
f"## Finding / Decision\n\n{finding}\n\n"
|
||||
f"## Reasoning\n\n{reasoning}\n\n"
|
||||
f"## Implications\n\n{implications}\n\n"
|
||||
f"## Related\n\n{related}\n"
|
||||
f"{related_section}"
|
||||
)
|
||||
|
||||
|
||||
@@ -180,7 +184,7 @@ def extract_thread(
|
||||
mode: str = "summary",
|
||||
title: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
project_name: str | None = "deerflow",
|
||||
project_name: str | None = "zncm",
|
||||
sources: list[SourceDraft] | None = None,
|
||||
max_messages: int = 80,
|
||||
) -> NoteDraft | None:
|
||||
@@ -193,7 +197,7 @@ def extract_thread(
|
||||
sources = sources or []
|
||||
body = _build_body(title=final_title, turns=turns, mode=mode, project_name=project_name, sources=sources)
|
||||
base_tags = list(tags or [])
|
||||
for default in (project_name or "deerflow", "knowledge-base"):
|
||||
for default in (project_name or "zncm", "knowledge-base"):
|
||||
if default and default not in base_tags:
|
||||
base_tags.append(default)
|
||||
return NoteDraft(
|
||||
@@ -216,7 +220,7 @@ def extract_search(
|
||||
sources: list[SourceDraft],
|
||||
*,
|
||||
thread_id: str | None = None,
|
||||
project_name: str | None = "deerflow",
|
||||
project_name: str | None = "zncm",
|
||||
title: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> NoteDraft:
|
||||
@@ -240,7 +244,7 @@ def extract_search(
|
||||
*[f"- {link}" for link in _related_links(project_name)],
|
||||
]
|
||||
base_tags = list(tags or [])
|
||||
for default in (project_name or "deerflow", "knowledge-base", "reference"):
|
||||
for default in (project_name or "zncm", "knowledge-base", "reference"):
|
||||
if default and default not in base_tags:
|
||||
base_tags.append(default)
|
||||
return NoteDraft(
|
||||
|
||||
+7
-10
@@ -120,12 +120,9 @@ def _render_body(data: dict[str, Any], title: str, related: list[str], sources:
|
||||
reasoning = str(data.get("reasoning") or "").strip() or "(未提供推理说明)"
|
||||
implications = _bullets(data.get("implications")) or "- 如内容过期或冲突,请编辑或归档。"
|
||||
|
||||
related_block = "\n".join(f"- {link}" for link in related)
|
||||
if sources:
|
||||
related_block += "\n\n### Sources\n\n"
|
||||
for s in sources[:20]:
|
||||
label = s.title or s.url or "来源"
|
||||
related_block += f"- {label}" + (f" — {s.url}" if s.url else "") + "\n"
|
||||
related_lines = [f"- {link}" for link in related]
|
||||
source_lines = [f"- {s.title or s.url or '来源'}" + (f" — {s.url}" if s.url else "") for s in sources[:20]]
|
||||
related_section = extractor._related_section_md(related_lines, source_lines)
|
||||
|
||||
return (
|
||||
f"# {title}\n\n"
|
||||
@@ -133,7 +130,7 @@ def _render_body(data: dict[str, Any], title: str, related: list[str], sources:
|
||||
f"## Finding / Decision\n\n{findings}\n\n"
|
||||
f"## Reasoning\n\n{reasoning}\n\n"
|
||||
f"## Implications\n\n{implications}\n\n"
|
||||
f"## Related\n\n{related_block}\n"
|
||||
f"{related_section}"
|
||||
)
|
||||
|
||||
|
||||
@@ -178,7 +175,7 @@ def _build_note_draft(
|
||||
|
||||
base_tags = list(tags or [])
|
||||
llm_tags = note_data.get("tags") if isinstance(note_data.get("tags"), list) else []
|
||||
for tag in [*llm_tags, project_name or "deerflow", "knowledge-base"]:
|
||||
for tag in [*llm_tags, project_name or "zncm", "knowledge-base"]:
|
||||
tag = str(tag).strip()
|
||||
if tag and tag not in base_tags:
|
||||
base_tags.append(tag)
|
||||
@@ -216,7 +213,7 @@ async def extract_thread_llm_multi(
|
||||
mode: str = "summary",
|
||||
title: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
project_name: str | None = "deerflow",
|
||||
project_name: str | None = "zncm",
|
||||
sources: list[SourceDraft] | None = None,
|
||||
max_messages: int = 80,
|
||||
model_name: str | None = None,
|
||||
@@ -295,7 +292,7 @@ async def extract_thread_llm(
|
||||
mode: str = "summary",
|
||||
title: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
project_name: str | None = "deerflow",
|
||||
project_name: str | None = "zncm",
|
||||
sources: list[SourceDraft] | None = None,
|
||||
max_messages: int = 80,
|
||||
model_name: str | None = None,
|
||||
|
||||
-15
@@ -116,21 +116,6 @@ class MarkdownVault:
|
||||
base = self.root / "projects" / slugify(project_name)
|
||||
for d in ("concepts", "references", "synthesis", "journal"):
|
||||
(base / d).mkdir(parents=True, exist_ok=True)
|
||||
# Project hub page — the structural target every note links to under
|
||||
# ``## Related`` (``[[projects/<project>/synthesis/product-knowledge-base]]``).
|
||||
# Materialize it so that wikilink is always navigable, not a dangling
|
||||
# placeholder.
|
||||
hub = base / "synthesis" / "product-knowledge-base.md"
|
||||
if not hub.exists():
|
||||
hub.write_text(
|
||||
f"---\ntitle: 产品知识库总览\ncategory: synthesis\n---\n\n"
|
||||
f"# 产品知识库总览\n\n"
|
||||
f"本页是 **{project_name}** 项目知识库的入口枢纽(hub)。每条知识都会在 `## Related` 中链接回这里。\n\n"
|
||||
"- [[index]] — 全部页面索引\n"
|
||||
"- [[_meta/taxonomy]] — 分类体系\n\n"
|
||||
"## 主题\n\n_随着知识沉淀,这里会逐步链接到各主题页面。_\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
# Base vault files.
|
||||
index = self.root / "index.md"
|
||||
if not index.exists():
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ _HOT_MAX = 20
|
||||
class ObsidianWikiAdapter:
|
||||
"""Read/write an obsidian-wiki compatible vault."""
|
||||
|
||||
def __init__(self, vault_root: Path | str, *, project_name: str = "deerflow", export_html: bool = True, export_json: bool = True) -> None:
|
||||
def __init__(self, vault_root: Path | str, *, project_name: str = "zncm", export_html: bool = True, export_json: bool = True) -> None:
|
||||
self.vault = MarkdownVault(vault_root)
|
||||
self.root = self.vault.root
|
||||
self.manifest = Manifest(self.root)
|
||||
|
||||
@@ -40,11 +40,11 @@ def _resolve_vault_dir(vault_dir: str) -> Path:
|
||||
|
||||
|
||||
def _inject_related_notes(body: str, links: list[str]) -> str:
|
||||
"""Insert real related-note ``[[wikilinks]]`` at the top of the Related block.
|
||||
"""Insert real related-note ``[[wikilinks]]`` into the Related block.
|
||||
|
||||
Keeps the structural meta-links (index / taxonomy / hub) that the extractor
|
||||
already rendered, but surfaces the content-specific related notes first. When
|
||||
there is no ``## Related`` section, one is appended.
|
||||
Notes carry only content-specific related notes now (the structural meta-links
|
||||
were dropped), so this is the sole writer of ``## Related`` links. When the
|
||||
body has no ``## Related`` section yet, one is appended.
|
||||
"""
|
||||
if not links:
|
||||
return body
|
||||
@@ -107,7 +107,7 @@ class KnowledgeService:
|
||||
# tags (the per-note project/knowledge-base defaults are dropped, else
|
||||
# every note would match every other). CJK runs tokenize as a single
|
||||
# token, so shared distinctive tags are what make cross-topic matches work.
|
||||
default_tags = {t.lower() for t in (self.project_name or "", "knowledge-base", "deerflow") if t}
|
||||
default_tags = {t.lower() for t in (self.project_name or "", "knowledge-base", "zncm") if t}
|
||||
distinctive = [t for t in (draft.tags or []) if t and t.lower() not in default_tags]
|
||||
query = " ".join(p for p in [draft.title or "", draft.summary or "", *distinctive] if p).strip()
|
||||
if not query:
|
||||
|
||||
Reference in new issue
Block a user