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

@@ -1415,7 +1415,8 @@
"metricApproved": "已批准",
"scopeFiltered": "目前篩選",
"scopeCurrent": "已載入",
"categoryDistribution": "分類分佈"
"categoryDistribution": "分類分佈",
"categoryOther": "其他分類"
},
"noResults": "找不到相關知識條目",
"createEntry": "新增條目",
@@ -1446,10 +1447,24 @@
"human": "人工建立"
},
"category": {
"AI治理": "AI 治理",
"alert_handling": "告警處理",
"infrastructure": "基礎設施",
"application": "應用層",
"ai_system": "AI 系統",
"security": "安全 / 合規"
"security": "安全 / 合規",
"general": "通用",
"host_resource": "主機資源",
"database": "資料庫",
"auto_repair": "自動修復",
"postmortem": "事後分析",
"external_site": "外部網站",
"backup_failure": "備份失敗",
"kubernetes": "Kubernetes",
"flywheel_health": "飛輪健康",
"ssl_cert": "SSL 憑證",
"devops_tool": "DevOps 工具",
"incident_postmortem": "事故檢討"
},
"filterByType": "篩選類型",
"filterByStatus": "篩選狀態",

View File

@@ -1415,7 +1415,8 @@
"metricApproved": "已批准",
"scopeFiltered": "目前篩選",
"scopeCurrent": "已載入",
"categoryDistribution": "分類分佈"
"categoryDistribution": "分類分佈",
"categoryOther": "其他分類"
},
"noResults": "找不到相關知識條目",
"createEntry": "新增條目",
@@ -1446,10 +1447,24 @@
"human": "人工建立"
},
"category": {
"AI治理": "AI 治理",
"alert_handling": "告警處理",
"infrastructure": "基礎設施",
"application": "應用層",
"ai_system": "AI 系統",
"security": "安全 / 合規"
"security": "安全 / 合規",
"general": "通用",
"host_resource": "主機資源",
"database": "資料庫",
"auto_repair": "自動修復",
"postmortem": "事後分析",
"external_site": "外部網站",
"backup_failure": "備份失敗",
"kubernetes": "Kubernetes",
"flywheel_health": "飛輪健康",
"ssl_cert": "SSL 憑證",
"devops_tool": "DevOps 工具",
"incident_postmortem": "事故檢討"
},
"filterByType": "篩選類型",
"filterByStatus": "篩選狀態",

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>