feat(km): add full corpus asset taxonomy readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-03 16:12:48 +08:00
parent 37dda358a3
commit f99305e481
10 changed files with 312 additions and 17 deletions

View File

@@ -2187,7 +2187,7 @@
"clear": "清除",
"matrixTitle": "全域分類 / 資產矩陣",
"matrixSubtitle": "用目前 KM readback 與 RAG stats把專案、產品、網站、服務、套件、工具、Log、Alert、PlayBook、RAG、MCP 與排程分群顯示。",
"autoUpdated": "API 驅動",
"autoUpdated": "Full-corpus readback",
"global": "全域",
"project": "專案",
"product": "產品",

View File

@@ -2187,7 +2187,7 @@
"clear": "清除",
"matrixTitle": "全域分類 / 資產矩陣",
"matrixSubtitle": "用目前 KM readback 與 RAG stats把專案、產品、網站、服務、套件、工具、Log、Alert、PlayBook、RAG、MCP 與排程分群顯示。",
"autoUpdated": "API 驅動",
"autoUpdated": "全庫 readback",
"global": "全域",
"project": "專案",
"product": "產品",

View File

@@ -53,10 +53,16 @@ interface CategoryCount {
count: number
}
interface AssetTaxonomyCount {
key: AssetLensKey
count: number
}
interface ListResponse {
items: KnowledgeEntry[]
total: number
categories: CategoryCount[]
asset_taxonomy?: AssetTaxonomyCount[]
readback_status?: 'ready' | 'degraded' | string
operator_stage?: string | null
next_step?: string | null
@@ -264,6 +270,7 @@ const DEFAULT_CATEGORY_ORDER = [
const KNOWN_CATEGORY_KEYS = new Set([
'AI自動化/Ansible受控修復',
'AI治理',
'alert',
'alert_handling',
'application',
'ai_system',
@@ -388,6 +395,7 @@ const CATEGORY_BAR_COLORS: Record<string, string> = {
}
const CATEGORY_ORDER_RANK = new Map(DEFAULT_CATEGORY_ORDER.map((category, index) => [category, index]))
const ASSET_TAXONOMY_FALLBACK_COUNTS: AssetTaxonomyCount[] = ASSET_LENS_KEYS.map(key => ({ key, count: 0 }))
const TAG_TONES: Record<string, string> = {
critical: 'border-status-critical/20 bg-status-critical/10 text-status-critical',
@@ -485,6 +493,7 @@ export default function KnowledgeBasePage({
const [entries, setEntries] = useState<KnowledgeEntry[]>([])
const [total, setTotal] = useState(0)
const [categories, setCategories] = useState<CategoryCount[]>([])
const [assetTaxonomy, setAssetTaxonomy] = useState<AssetTaxonomyCount[]>(ASSET_TAXONOMY_FALLBACK_COUNTS)
const [loading, setLoading] = useState(true)
const [entryFetchError, setEntryFetchError] = useState<string | null>(null)
const [entryReadback, setEntryReadback] = useState<Pick<ListResponse, 'readback_status' | 'operator_stage' | 'next_step'> | null>(null)
@@ -529,6 +538,7 @@ export default function KnowledgeBasePage({
setEntries(data.items)
setTotal(data.total)
setCategories(data.categories)
setAssetTaxonomy(data.asset_taxonomy?.length ? data.asset_taxonomy : ASSET_TAXONOMY_FALLBACK_COUNTS)
setEntryReadback({
readback_status: data.readback_status ?? 'ready',
operator_stage: data.operator_stage ?? null,
@@ -539,14 +549,16 @@ export default function KnowledgeBasePage({
const errorBody = await res.json().catch(() => null) as { detail?: string; message?: string } | null
setEntries([])
setTotal(0)
setCategories([])
setCategories(ASSET_TAXONOMY_FALLBACK_COUNTS.map(row => ({ category: row.key, count: 0 })))
setAssetTaxonomy(ASSET_TAXONOMY_FALLBACK_COUNTS)
setEntryReadback(null)
setEntryFetchError(errorBody?.detail ?? errorBody?.message ?? `${res.status} ${res.statusText}`)
} catch (err) {
console.error('Failed to fetch knowledge entries', err)
setEntries([])
setTotal(0)
setCategories([])
setCategories(ASSET_TAXONOMY_FALLBACK_COUNTS.map(row => ({ category: row.key, count: 0 })))
setAssetTaxonomy(ASSET_TAXONOMY_FALLBACK_COUNTS)
setEntryReadback(null)
setEntryFetchError(err instanceof Error ? err.message : String(err))
} finally {
@@ -746,22 +758,27 @@ export default function KnowledgeBasePage({
}, [categories, totalCount])
const categoryNavigationRows = useMemo(() => {
if (categories.length === 0) return []
const sourceRows = mergeCategoryCounts(categories)
const positiveRows = sourceRows.filter(row => row.count > 0)
const fallbackRows = ASSET_TAXONOMY_FALLBACK_COUNTS.map(row => ({
category: row.key,
count: assetTaxonomy.find(item => item.key === row.key)?.count ?? row.count,
}))
const rows = positiveRows.length > 0 ? positiveRows : fallbackRows
const sourceRows = mergeCategoryCounts(categories).filter(row => row.count > 0)
return [...sourceRows].sort((a, b) => {
return [...rows].sort((a, b) => {
const aRank = CATEGORY_ORDER_RANK.get(a.category) ?? Number.MAX_SAFE_INTEGER
const bRank = CATEGORY_ORDER_RANK.get(b.category) ?? Number.MAX_SAFE_INTEGER
if (aRank !== bRank) return aRank - bRank
return b.count - a.count
})
}, [categories])
}, [assetTaxonomy, categories])
const assetLensRows = useMemo(() => {
const ragChunks = governanceTelemetry.ragStats?.total_chunks ?? 0
const taxonomyByKey = new Map(assetTaxonomy.map(row => [row.key, row.count]))
const countWhere = (lens: AssetLensKey) =>
baseDisplayedEntries.filter(entry => entryMatchesAssetLens(entry, lens)).length
taxonomyByKey.get(lens) ?? baseDisplayedEntries.filter(entry => entryMatchesAssetLens(entry, lens)).length
const rows: Array<{
key: AssetLensKey
icon: LucideIcon
@@ -828,7 +845,7 @@ export default function KnowledgeBasePage({
icon: FileSearch,
count: ragChunks || countWhere('rag'),
tone: ragChunks > 0 ? 'text-status-healthy' : 'text-status-critical',
global: ragChunks > 0,
global: ragChunks > 0 || taxonomyByKey.has('rag'),
},
{
key: 'mcp',
@@ -844,7 +861,7 @@ export default function KnowledgeBasePage({
},
]
return ASSET_LENS_KEYS.map(key => rows.find(row => row.key === key)).filter((row): row is NonNullable<typeof row> => Boolean(row))
}, [baseDisplayedEntries, governanceTelemetry.ragStats])
}, [assetTaxonomy, baseDisplayedEntries, governanceTelemetry.ragStats])
const qualityRows = useMemo(() => {
const loaded = displayedEntries.length
@@ -1254,7 +1271,7 @@ export default function KnowledgeBasePage({
<Icon className={cn('h-3 w-3 shrink-0', row.tone)} aria-hidden={true} />
</div>
<p className={cn('mt-1 text-sm font-heading font-semibold tabular-nums', row.tone)}>
{(row.key === 'rag' ? governanceLoading : !knowledgeReadbackReady) ? '--' : formatCount(row.count)}
{(row.key === 'rag' ? governanceLoading && row.count === 0 : loading) ? '--' : formatCount(row.count)}
</p>
</button>
)
@@ -1265,7 +1282,7 @@ export default function KnowledgeBasePage({
<div className="mt-3 flex items-center justify-between px-2">
<span className="text-[10px] font-label uppercase tracking-wider text-muted">{t('rail.categoryTitle')}</span>
<span className="text-[10px] font-body tabular-nums text-muted">
{knowledgeReadbackReady ? categoryNavigationRows.length : '--'}
{loading ? '--' : categoryNavigationRows.length}
</span>
</div>
@@ -1578,7 +1595,7 @@ export default function KnowledgeBasePage({
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-6">
{assetLensRows.map(row => {
const Icon = row.icon
const value = (row.key === 'rag' ? governanceLoading : !knowledgeReadbackReady)
const value = (row.key === 'rag' ? governanceLoading && row.count === 0 : loading)
? '--'
: formatCount(row.count)
return (