fix(web): prevent knowledge category key leaks
All checks were successful
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m36s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s

This commit is contained in:
Your Name
2026-06-03 01:01:11 +08:00
parent 2c706cfc99
commit a748a08280
3 changed files with 113 additions and 15 deletions

View File

@@ -67,6 +67,28 @@ const CATEGORY_ICONS: Record<string, React.ReactNode> = {
}
const CATEGORIES = ['infrastructure', 'application', 'ai_system', 'security'] as const
const KNOWN_CATEGORY_KEYS = new Set([
'AI治理',
'alert_handling',
'application',
'ai_system',
'auto_repair',
'backup_failure',
'database',
'devops_tool',
'external_site',
'flywheel_health',
'general',
'host_resource',
'incident_postmortem',
'infrastructure',
'kubernetes',
'postmortem',
'security',
'ssl_cert',
])
const CATEGORY_OVERVIEW_LIMIT = 8
// =============================================================================
// Type Badge Colors
@@ -89,10 +111,28 @@ const STATUS_COLORS: Record<string, string> = {
}
const CATEGORY_BAR_COLORS: Record<string, string> = {
AI治理: 'bg-claw-blue',
alert_handling: 'bg-status-warning',
infrastructure: 'bg-claw-blue',
application: 'bg-status-healthy',
ai_system: 'bg-purple-500',
security: 'bg-status-warning',
general: 'bg-nothing-gray-400',
postmortem: 'bg-status-critical',
__other__: 'bg-nothing-gray-300',
}
const humanizeCategory = (category: string) => {
if (!category.includes('_')) return category
return category
.split('_')
.filter(Boolean)
.map(part => {
const upper = part.toUpperCase()
if (['AI', 'DB', 'SSL', 'SLO', 'API'].includes(upper)) return upper
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`
})
.join(' ')
}
// =============================================================================
@@ -227,6 +267,15 @@ export default function KnowledgeBasePage({
(value: number) => value.toLocaleString(localeCode),
[localeCode],
)
const formatCategoryLabel = useCallback(
(category: string) => {
if (KNOWN_CATEGORY_KEYS.has(category)) {
return t(`category.${category}`)
}
return humanizeCategory(category)
},
[t],
)
const visibleSummary = useMemo(() => {
const loaded = displayedEntries.length
@@ -236,13 +285,30 @@ export default function KnowledgeBasePage({
return { loaded, aiExtracted, approved, approvedRate }
}, [displayedEntries])
const categoryRows = useMemo(() => (
CATEGORIES.map(category => {
const count = categories.find(c => c.category === category)?.count ?? 0
const pct = totalCount > 0 ? Math.round((count / totalCount) * 100) : 0
return { category, count, pct }
const categoryRows = useMemo(() => {
const sourceRows = categories.length > 0
? [...categories].sort((a, b) => b.count - a.count)
: CATEGORIES.map(category => ({ category, count: 0 }))
const visibleRows = sourceRows.slice(0, CATEGORY_OVERVIEW_LIMIT).map(row => {
const pct = totalCount > 0 ? Math.round((row.count / totalCount) * 100) : 0
return { category: row.category, count: row.count, pct }
})
), [categories, totalCount])
const remainingCount = sourceRows
.slice(CATEGORY_OVERVIEW_LIMIT)
.reduce((sum, row) => sum + row.count, 0)
if (remainingCount <= 0) return visibleRows
return [
...visibleRows,
{
category: '__other__',
count: remainingCount,
pct: totalCount > 0 ? Math.round((remainingCount / totalCount) * 100) : 0,
},
]
}, [categories, totalCount])
const content = (
<div className="flex h-[calc(100vh-64px)] flex-col lg:flex-row">
@@ -284,7 +350,7 @@ export default function KnowledgeBasePage({
)}
>
{CATEGORY_ICONS[cat]}
<span className="flex-1 text-left">{t(`category.${cat}`)}</span>
<span className="flex-1 text-left">{formatCategoryLabel(cat)}</span>
<span className="text-xs text-muted">{count}</span>
</button>
)
@@ -380,8 +446,10 @@ export default function KnowledgeBasePage({
</div>
<div className="space-y-1.5">
{categoryRows.map(row => (
<div key={row.category} className="grid grid-cols-[88px_1fr_42px] items-center gap-2">
<span className="truncate text-[10px] font-body text-secondary">{t(`category.${row.category}`)}</span>
<div key={row.category} className="grid grid-cols-[96px_1fr_42px] items-center gap-2">
<span className="truncate text-[10px] font-body text-secondary">
{row.category === '__other__' ? t('overview.categoryOther') : formatCategoryLabel(row.category)}
</span>
<div className="h-1.5 overflow-hidden rounded-full bg-nothing-gray-100">
<div
className={cn('h-full rounded-full', CATEGORY_BAR_COLORS[row.category] ?? 'bg-nothing-gray-300')}
@@ -452,7 +520,7 @@ export default function KnowledgeBasePage({
{/* Meta row */}
<div className="flex items-center gap-2 text-xs text-muted font-body">
<span>{t(`category.${entry.category}`)}</span>
<span>{formatCategoryLabel(entry.category)}</span>
<span>·</span>
<span>{t(`source.${entry.source}`)}</span>
<span>·</span>
@@ -529,7 +597,7 @@ export default function KnowledgeBasePage({
{/* Meta */}
<div className="flex items-center gap-3 text-xs text-muted font-body mb-4 pb-3 border-b border-nothing-gray-100">
<span>{t(`category.${selectedEntry.category}`)}</span>
<span>{formatCategoryLabel(selectedEntry.category)}</span>
<span>·</span>
<span>{t(`source.${selectedEntry.source}`)}</span>
<span>·</span>