feat(knowledge): 笔记页实体可点击筛选 + chip 美化

- 实体改为按钮:点击即在左侧列表筛出提及该实体的知识(清空来源/状态筛选)
- chip 加阴影/hover 上浮/类型 ring 标签/实体区 Sparkles 图标,更精致

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-09 19:48:20 +08:00
1 parent b70ed38d51
commit f6af9379b7
3 files changed
+152 -126

No files matched your search

+30 -10
View File
@@ -9,6 +9,7 @@ import {
RefreshCw, RefreshCw,
Save, Save,
Search, Search,
Sparkles,
Trash2, Trash2,
X, X,
} from "lucide-react"; } from "lucide-react";
@@ -415,7 +416,13 @@ function VaultPageDialog({
); );
} }
function NoteDetail({ noteId }: { noteId: string }) { function NoteDetail({
noteId,
onEntityClick,
}: {
noteId: string;
onEntityClick?: (name: string) => void;
}) {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: note, isLoading } = useKnowledgeNote(noteId); const { data: note, isLoading } = useKnowledgeNote(noteId);
const { data: wikiLinks } = useNoteWikilinks(noteId); const { data: wikiLinks } = useNoteWikilinks(noteId);
@@ -578,22 +585,28 @@ function NoteDetail({ noteId }: { noteId: string }) {
{note.entities && note.entities.length > 0 ? ( {note.entities && note.entities.length > 0 ? (
<div className="mb-4 flex flex-wrap items-center gap-1.5"> <div className="mb-4 flex flex-wrap items-center gap-1.5">
<span className="text-muted-foreground mr-0.5 text-xs">实体</span> <span className="text-muted-foreground mr-0.5 inline-flex items-center gap-1 text-xs">
<Sparkles className="size-3" /> 实体
</span>
{note.entities.map((ent) => { {note.entities.map((ent) => {
const meta = entityMeta(ent.entity_type); const meta = entityMeta(ent.entity_type);
return ( return (
<span <button
type="button"
key={`${ent.name}-${ent.entity_type ?? ""}`} key={`${ent.name}-${ent.entity_type ?? ""}`}
onClick={() => onEntityClick?.(ent.name)}
title={`筛选提及「${ent.name}」的知识`}
className={cn( className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium", "group inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium shadow-sm transition-all hover:-translate-y-px hover:shadow",
meta.className, meta.className,
)} )}
title={meta.label}
> >
<span className="size-1.5 rounded-full bg-current opacity-60" /> <span className="size-1.5 shrink-0 rounded-full bg-current opacity-70 transition-transform group-hover:scale-125" />
{ent.name} <span>{ent.name}</span>
<span className="opacity-60">{meta.label}</span> <span className="rounded-sm px-1 text-[10px] opacity-70 ring-1 ring-current ring-inset">
</span> {meta.label}
</span>
</button>
); );
})} })}
</div> </div>
@@ -779,7 +792,14 @@ export default function KnowledgeBasePage() {
<div className="min-h-0"> <div className="min-h-0">
{noteId ? ( {noteId ? (
<NoteDetail noteId={noteId} /> <NoteDetail
noteId={noteId}
onEntityClick={(name) => {
setKeyword(name);
setSourceType("all");
setStatus("all");
}}
/>
) : ( ) : (
<div className="text-muted-foreground flex h-full items-center justify-center text-sm"> <div className="text-muted-foreground flex h-full items-center justify-center text-sm">
从左侧选择一条知识查看详情 从左侧选择一条知识查看详情
@@ -149,6 +149,7 @@ class SkillResponse(BaseModel):
"""Response model for skill information.""" """Response model for skill information."""
name: str = Field(..., description="Name of the skill") name: str = Field(..., description="Name of the skill")
name_zh: str | None = Field(default=None, description="用户配置或大模型生成的中文名称")
description: str = Field(..., description="Description of what the skill does") description: str = Field(..., description="Description of what the skill does")
license: str | None = Field(None, description="License information") license: str | None = Field(None, description="License information")
category: SkillCategory = Field(..., description="Category of the skill (public or custom)") category: SkillCategory = Field(..., description="Category of the skill (public or custom)")
@@ -313,16 +314,19 @@ def _skill_to_response(
detail = ownership.get("detail") detail = ownership.get("detail")
display = None if display_override is _UNSET else display_override display = None if display_override is _UNSET else display_override
if display_override is _UNSET: name_zh: str | None = None
try: try:
skill_config = get_extensions_config().skills.get(skill.name) skill_config = get_extensions_config().skills.get(skill.name)
if skill_config and skill_config.display: if skill_config:
if display_override is _UNSET and skill_config.display:
display = skill_config.display.model_dump(by_alias=True) display = skill_config.display.model_dump(by_alias=True)
except Exception: name_zh = skill_config.name_zh or None
logger.debug("Failed to read display adapter for %s", skill.name, exc_info=True) except Exception:
logger.debug("Failed to read extensions config for %s", skill.name, exc_info=True)
return SkillResponse( return SkillResponse(
name=skill.name, name=skill.name,
name_zh=name_zh,
description=skill.description, description=skill.description,
license=skill.license, license=skill.license,
category=skill.category, category=skill.category,
@@ -2142,6 +2146,7 @@ async def update_skill(skill_name: str, request: SkillUpdateRequest, config: App
extensions_config.skills[skill_name] = SkillStateConfig( extensions_config.skills[skill_name] = SkillStateConfig(
enabled=request.enabled, enabled=request.enabled,
display=current_skill_config.display if current_skill_config else None, display=current_skill_config.display if current_skill_config else None,
name_zh=current_skill_config.name_zh if current_skill_config else None,
) )
config_data = _dump_extensions_config(extensions_config) config_data = _dump_extensions_config(extensions_config)
@@ -3,7 +3,7 @@
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@@ -53,115 +53,116 @@ class McpServerConfig(BaseModel):
model_config = ConfigDict(extra="allow", populate_by_name=True) model_config = ConfigDict(extra="allow", populate_by_name=True)
class SkillDisplayTableColumn(BaseModel): class SkillDisplayTableColumn(BaseModel):
"""A configured table column for structured skill result display.""" """A configured table column for structured skill result display."""
label: str = Field(description="Column label shown in the frontend table") label: str = Field(description="Column label shown in the frontend table")
field: str = Field(description="Field name or dot path read from each result item") field: str = Field(description="Field name or dot path read from each result item")
width: str | None = Field(default=None, description="Optional CSS width for this column") width: str | None = Field(default=None, description="Optional CSS width for this column")
model_config = ConfigDict(extra="allow", populate_by_name=True) model_config = ConfigDict(extra="allow", populate_by_name=True)
class SkillDisplayResultSetConfig(BaseModel): class SkillDisplayResultSetConfig(BaseModel):
"""Display adapter for one result array inside a skill response.""" """Display adapter for one result array inside a skill response."""
key: str = Field(description="Stable key for this result set") key: str = Field(description="Stable key for this result set")
label: str | None = Field(default=None, description="Human-readable result set label") label: str | None = Field(default=None, description="Human-readable result set label")
enabled: bool = Field(default=True, description="Whether this result set is enabled") enabled: bool = Field(default=True, description="Whether this result set is enabled")
mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field( mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field(
default="citation", default="citation",
description="How the frontend should render this result set", description="How the frontend should render this result set",
) )
result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath") result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath")
title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField") title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField")
summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField") summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField")
content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField") content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField")
source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField") source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField")
author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField") author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField")
type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField") type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField")
time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField") time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField")
score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField") score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField")
url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField") url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField")
id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField") id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField")
link_strategy: Literal["auto", "url", "id"] = Field( link_strategy: Literal["auto", "url", "id"] = Field(
default="auto", default="auto",
description="Whether links use a URL field, an ID template, or automatic fallback", description="Whether links use a URL field, an ID template, or automatic fallback",
alias="linkStrategy", alias="linkStrategy",
) )
link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate") link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate")
citation_token: Literal["doubleBracket", "singleBracket"] = Field( citation_token: Literal["doubleBracket", "singleBracket"] = Field(
default="singleBracket", default="singleBracket",
description="Citation token style expected in model output", description="Citation token style expected in model output",
alias="citationToken", alias="citationToken",
) )
max_items: int = Field(default=100, ge=1, le=100, description="Maximum displayed items", alias="maxItems") max_items: int = Field(default=100, ge=1, le=100, description="Maximum displayed items", alias="maxItems")
custom_instruction: str = Field( custom_instruction: str = Field(
default="", default="",
description="User-authored instruction for custom rendering schema generation", description="User-authored instruction for custom rendering schema generation",
alias="customInstruction", alias="customInstruction",
) )
table_columns: list[SkillDisplayTableColumn] = Field( table_columns: list[SkillDisplayTableColumn] = Field(
default_factory=list, default_factory=list,
description="Columns used when mode is table", description="Columns used when mode is table",
alias="tableColumns", alias="tableColumns",
) )
model_config = ConfigDict(extra="allow", populate_by_name=True) model_config = ConfigDict(extra="allow", populate_by_name=True)
class SkillDisplayConfig(BaseModel): class SkillDisplayConfig(BaseModel):
"""Frontend display adapter for structured skill results.""" """Frontend display adapter for structured skill results."""
enabled: bool = Field(default=False, description="Whether result display adaptation is enabled") enabled: bool = Field(default=False, description="Whether result display adaptation is enabled")
mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field( mode: Literal["citation", "list", "table", "card", "custom", "none"] = Field(
default="none", default="none",
description="How the frontend should render this skill's structured results", description="How the frontend should render this skill's structured results",
) )
result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath") result_path: str = Field(default="results", description="Dot path to the array of result items", alias="resultPath")
title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField") title_field: str | None = Field(default=None, description="Field used as item title", alias="titleField")
summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField") summary_field: str | None = Field(default=None, description="Field used as short summary/snippet", alias="summaryField")
content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField") content_field: str | None = Field(default=None, description="Field used as full item content", alias="contentField")
source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField") source_field: str | None = Field(default=None, description="Field used as source label", alias="sourceField")
author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField") author_field: str | None = Field(default=None, description="Field used as author/byline label", alias="authorField")
type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField") type_field: str | None = Field(default=None, description="Field used as source/type badge", alias="typeField")
time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField") time_field: str | None = Field(default=None, description="Field used as publication/update time", alias="timeField")
score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField") score_field: str | None = Field(default=None, description="Field used as relevance score", alias="scoreField")
url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField") url_field: str | None = Field(default=None, description="Field containing a direct URL", alias="urlField")
id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField") id_field: str | None = Field(default=None, description="Field containing an ID for template URLs", alias="idField")
link_strategy: Literal["auto", "url", "id"] = Field( link_strategy: Literal["auto", "url", "id"] = Field(
default="auto", default="auto",
description="Whether links use a URL field, an ID template, or automatic fallback", description="Whether links use a URL field, an ID template, or automatic fallback",
alias="linkStrategy", alias="linkStrategy",
) )
link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate") link_template: str | None = Field(default=None, description="Template used when linkStrategy is id", alias="linkTemplate")
citation_token: Literal["doubleBracket", "singleBracket"] = Field( citation_token: Literal["doubleBracket", "singleBracket"] = Field(
default="doubleBracket", default="doubleBracket",
description="Citation token style expected in model output", description="Citation token style expected in model output",
alias="citationToken", alias="citationToken",
) )
max_items: int = Field(default=8, ge=1, le=100, description="Maximum displayed items", alias="maxItems") max_items: int = Field(default=8, ge=1, le=100, description="Maximum displayed items", alias="maxItems")
custom_instruction: str = Field( custom_instruction: str = Field(
default="", default="",
description="User-authored instruction for custom rendering schema generation", description="User-authored instruction for custom rendering schema generation",
alias="customInstruction", alias="customInstruction",
) )
table_columns: list[SkillDisplayTableColumn] = Field( table_columns: list[SkillDisplayTableColumn] = Field(
default_factory=list, default_factory=list,
description="Columns used when mode is table", description="Columns used when mode is table",
alias="tableColumns", alias="tableColumns",
) )
result_sets: list[SkillDisplayResultSetConfig] = Field( result_sets: list[SkillDisplayResultSetConfig] = Field(
default_factory=list, default_factory=list,
description="Optional per-result-array display adapters", description="Optional per-result-array display adapters",
alias="resultSets", alias="resultSets",
) )
model_config = ConfigDict(extra="allow", populate_by_name=True) model_config = ConfigDict(extra="allow", populate_by_name=True)
class SkillStateConfig(BaseModel): class SkillStateConfig(BaseModel):
"""Configuration for a single skill's state.""" """Configuration for a single skill's state."""
enabled: bool = Field(default=True, description="Whether this skill is enabled") enabled: bool = Field(default=True, description="Whether this skill is enabled")
display: SkillDisplayConfig | None = Field(default=None, description="Optional frontend display adapter") display: SkillDisplayConfig | None = Field(default=None, description="Optional frontend display adapter")
name_zh: str | None = Field(default=None, description="用户配置或大模型生成的中文名称(为空表示沿用技能英文名)")
class ExtensionsConfig(BaseModel): class ExtensionsConfig(BaseModel):