fix(web): surface knowledge taxonomy readback
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m3s
CD Pipeline / build-and-deploy (push) Successful in 6m43s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s

This commit is contained in:
Your Name
2026-07-03 02:43:25 +08:00
parent 149d52b6e2
commit 389e90bbfb
4 changed files with 228 additions and 35 deletions

View File

@@ -2168,13 +2168,37 @@
},
"assetLens": {
"title": "資產維度",
"scope": "目前列表",
"scope": "自動更新",
"matrixTitle": "全域分類 / 資產矩陣",
"matrixSubtitle": "用目前 KM readback 與 RAG stats把專案、產品、網站、服務、套件、工具、Log、Alert、PlayBook、RAG、MCP 與排程分群顯示。",
"autoUpdated": "API 驅動",
"global": "全域",
"project": "專案",
"product": "產品",
"website": "網站",
"service": "服務",
"package": "套件",
"tool": "工具"
"tool": "工具",
"log": "Log / 事件",
"alert": "Alert / 告警",
"playbook": "PlayBook",
"rag": "RAG / 向量",
"mcp": "MCP / Connector",
"schedule": "排程 / Worker",
"detail": {
"project": "repo、branch、workflow 與 source-control 來源。",
"product": "AWOOOI、IwoooS、AwoooP 與跨產品名稱命中。",
"website": "domain、route、frontend、SSL 與外部站點訊號。",
"service": "API、worker、runtime、container、pod 與基礎服務。",
"package": "npm、pnpm、Python、dependency 與 library 訊號。",
"tool": "Ansible、Wazuh、Kali、Sentry、SigNoz、runner 等工具。",
"log": "LOG、event、timeline、trace、audit、callback 與 telemetry。",
"alert": "Alertmanager、Telegram、Sentry、warning 與 critical 訊號。",
"playbook": "已關聯 PlayBook / runbook 或 SOP 的 KM。",
"rag": "RAG stats 全域 chunks不是只看目前 50 筆列表。",
"mcp": "MCP gateway、connector 與工具整合訊號。",
"schedule": "cron、job、worker、recurrence 與 patrol 排程訊號。"
}
},
"dataChain": {
"errorTitle": "知識條目資料鏈路異常",
@@ -2214,7 +2238,9 @@
"scopeFiltered": "目前篩選",
"scopeCurrent": "已載入",
"categoryDistribution": "分類分佈",
"categoryOther": "其他分類"
"categoryOther": "其他分類",
"loading": "正在讀取 KM 分類...",
"noCategories": "目前沒有可顯示分類。"
},
"assetLedger": {
"title": "自動化資產沉澱總帳",

View File

@@ -2168,13 +2168,37 @@
},
"assetLens": {
"title": "資產維度",
"scope": "目前列表",
"scope": "自動更新",
"matrixTitle": "全域分類 / 資產矩陣",
"matrixSubtitle": "用目前 KM readback 與 RAG stats把專案、產品、網站、服務、套件、工具、Log、Alert、PlayBook、RAG、MCP 與排程分群顯示。",
"autoUpdated": "API 驅動",
"global": "全域",
"project": "專案",
"product": "產品",
"website": "網站",
"service": "服務",
"package": "套件",
"tool": "工具"
"tool": "工具",
"log": "Log / 事件",
"alert": "Alert / 告警",
"playbook": "PlayBook",
"rag": "RAG / 向量",
"mcp": "MCP / Connector",
"schedule": "排程 / Worker",
"detail": {
"project": "repo、branch、workflow 與 source-control 來源。",
"product": "AWOOOI、IwoooS、AwoooP 與跨產品名稱命中。",
"website": "domain、route、frontend、SSL 與外部站點訊號。",
"service": "API、worker、runtime、container、pod 與基礎服務。",
"package": "npm、pnpm、Python、dependency 與 library 訊號。",
"tool": "Ansible、Wazuh、Kali、Sentry、SigNoz、runner 等工具。",
"log": "LOG、event、timeline、trace、audit、callback 與 telemetry。",
"alert": "Alertmanager、Telegram、Sentry、warning 與 critical 訊號。",
"playbook": "已關聯 PlayBook / runbook 或 SOP 的 KM。",
"rag": "RAG stats 全域 chunks不是只看目前 50 筆列表。",
"mcp": "MCP gateway、connector 與工具整合訊號。",
"schedule": "cron、job、worker、recurrence 與 patrol 排程訊號。"
}
},
"dataChain": {
"errorTitle": "知識條目資料鏈路異常",
@@ -2214,7 +2238,9 @@
"scopeFiltered": "目前篩選",
"scopeCurrent": "已載入",
"categoryDistribution": "分類分佈",
"categoryOther": "其他分類"
"categoryOther": "其他分類",
"loading": "正在讀取 KM 分類...",
"noCategories": "目前沒有可顯示分類。"
},
"assetLedger": {
"title": "自動化資產沉澱總帳",

View File

@@ -132,7 +132,19 @@ interface KnowledgeGovernanceTelemetry {
type AutomationAssetKey = 'km' | 'playbook' | 'script' | 'monitoring' | 'verifier' | 'rag'
type WorkspaceViewKey = 'records' | 'automation' | 'queue'
type AssetLensKey = 'project' | 'product' | 'website' | 'service' | 'package' | 'tool'
type AssetLensKey =
| 'project'
| 'product'
| 'website'
| 'service'
| 'package'
| 'tool'
| 'log'
| 'alert'
| 'playbook'
| 'rag'
| 'mcp'
| 'schedule'
// =============================================================================
// Category Config
@@ -460,7 +472,7 @@ export default function KnowledgeBasePage({
fetch(`${apiBase}/api/v1/ai/governance/km-stale-owner-review-completion-queue?project_id=awoooi&status_bucket=all&limit=5`)
.then(res => res.ok ? res.json() : null)
.catch(() => null),
fetch(`${apiBase}/api/v1/knowledge/rag/stats`)
fetch(`${apiBase}/api/v1/knowledge/rag/stats?project_id=${encodeURIComponent(KNOWLEDGE_PROJECT_ID)}`)
.then(res => res.ok ? res.json() : null)
.catch(() => null),
])
@@ -561,6 +573,7 @@ export default function KnowledgeBasePage({
const displayedEntries = semanticMode ? semanticResults : entries
const totalCount = categories.reduce((sum, c) => sum + c.count, 0)
const isKnowledgeListUnavailable = Boolean(entryFetchError)
const knowledgeReadbackReady = !loading && !isKnowledgeListUnavailable
const localeCode = params.locale === 'en' ? 'en-US' : 'zh-TW'
const formatCount = useCallback(
(value: number) => value.toLocaleString(localeCode),
@@ -598,9 +611,9 @@ export default function KnowledgeBasePage({
}, [displayedEntries])
const categoryRows = useMemo(() => {
const sourceRows = categories.length > 0
? mergeCategoryCounts(categories).sort((a, b) => b.count - a.count)
: DEFAULT_CATEGORY_ORDER.map(category => ({ category, count: 0 }))
if (categories.length === 0) return []
const sourceRows = mergeCategoryCounts(categories).sort((a, b) => b.count - a.count)
const visibleRows = sourceRows.slice(0, CATEGORY_OVERVIEW_LIMIT).map(row => {
const pct = totalCount > 0 ? Math.round((row.count / totalCount) * 100) : 0
@@ -623,9 +636,9 @@ export default function KnowledgeBasePage({
}, [categories, totalCount])
const categoryNavigationRows = useMemo(() => {
const sourceRows = categories.length > 0
? mergeCategoryCounts(categories).filter(row => row.count > 0)
: DEFAULT_CATEGORY_ORDER.map(category => ({ category, count: 0 }))
if (categories.length === 0) return []
const sourceRows = mergeCategoryCounts(categories).filter(row => row.count > 0)
return [...sourceRows].sort((a, b) => {
const aRank = CATEGORY_ORDER_RANK.get(a.category) ?? Number.MAX_SAFE_INTEGER
@@ -636,26 +649,30 @@ export default function KnowledgeBasePage({
}, [categories])
const assetLensRows = useMemo(() => {
const ragChunks = governanceTelemetry.ragStats?.total_chunks ?? 0
const countWhere = (candidates: string[]) =>
displayedEntries.filter(entry => entryMatchesAny(entry, candidates)).length
const rows: Array<{
key: AssetLensKey
icon: LucideIcon
count: number
tone: string
global?: boolean
}> = [
{
key: 'project',
icon: GitBranch,
count: displayedEntries.filter(entry => entryMatchesAny(entry, [
count: countWhere([
'project', 'repo', 'repository', 'gitea', 'branch', 'workflow', 'source_control',
])).length,
]),
tone: 'text-claw-blue',
},
{
key: 'product',
icon: Package,
count: displayedEntries.filter(entry => entryMatchesAny(entry, [
count: countWhere([
'product', 'awoooi', 'awooop', 'iwooos', 'vibework', 'stockplatform', 'momo', 'awooogo', 'agent-bounty', 'tsenyang',
])).length,
]),
tone: 'text-purple-600',
},
{
@@ -679,9 +696,9 @@ export default function KnowledgeBasePage({
{
key: 'package',
icon: Package,
count: displayedEntries.filter(entry => entryMatchesAny(entry, [
count: countWhere([
'package', 'dependency', 'npm', 'pnpm', 'node', 'python', 'pip', 'prisma', 'next', 'library',
])).length,
]),
tone: 'text-status-warning',
},
{
@@ -693,9 +710,51 @@ export default function KnowledgeBasePage({
).length,
tone: 'text-status-critical',
},
{
key: 'log',
icon: FileText,
count: countWhere(['log', 'logs', 'event', 'timeline', 'trace', 'audit', 'callback', 'conversation_event', 'telemetry']),
tone: 'text-primary',
},
{
key: 'alert',
icon: TriangleAlert,
count: displayedEntries.filter(entry =>
entry.category === 'alert_handling'
|| entryMatchesAny(entry, ['alert', 'telegram', 'sentry', 'signoz', 'notification', 'warning', 'critical']),
).length,
tone: 'text-status-warning',
},
{
key: 'playbook',
icon: ClipboardList,
count: displayedEntries.filter(entry =>
Boolean(entry.related_playbook_id) || entryMatchesAny(entry, ['playbook', 'runbook', 'sop']),
).length,
tone: 'text-claw-blue',
},
{
key: 'rag',
icon: FileSearch,
count: ragChunks || countWhere(['rag', 'vector', 'embedding', 'semantic', 'retrieval']),
tone: ragChunks > 0 ? 'text-status-healthy' : 'text-status-critical',
global: ragChunks > 0,
},
{
key: 'mcp',
icon: Wrench,
count: countWhere(['mcp', 'connector', 'gateway', 'tool integration', 'tool-integration']),
tone: 'text-purple-600',
},
{
key: 'schedule',
icon: Clock3,
count: countWhere(['schedule', 'cron', 'job', 'worker', 'patrol', 'recurrence', 'cadence']),
tone: 'text-status-healthy',
},
]
return rows
}, [displayedEntries])
}, [displayedEntries, governanceTelemetry.ragStats])
const qualityRows = useMemo(() => {
const loaded = displayedEntries.length
@@ -1072,17 +1131,19 @@ export default function KnowledgeBasePage({
>
<BookOpen className="w-4 h-4" />
<span className="flex-1 text-left">{t('allCategories')}</span>
<span className="text-xs text-muted">{totalCount}</span>
<span className="text-xs text-muted">{knowledgeReadbackReady ? formatCount(total) : '--'}</span>
</button>
<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">{categoryNavigationRows.length}</span>
<span className="text-[10px] font-body tabular-nums text-muted">
{knowledgeReadbackReady ? categoryNavigationRows.length : '--'}
</span>
</div>
{/* Category items */}
<div className="mt-1 grid max-h-60 grid-cols-2 gap-0.5 overflow-y-auto pr-1 lg:block lg:space-y-0.5">
{categoryNavigationRows.map(row => {
{categoryNavigationRows.length > 0 ? categoryNavigationRows.map(row => {
const cat = row.category
const icon = CATEGORY_ICONS[cat] ?? <BookOpen className="w-4 h-4" />
return (
@@ -1101,7 +1162,11 @@ export default function KnowledgeBasePage({
<span className="text-xs text-muted">{formatCount(row.count)}</span>
</button>
)
})}
}) : (
<p className="col-span-2 px-2 py-2 text-xs font-body text-muted lg:col-span-1">
{loading ? t('overview.loading') : t('overview.noCategories')}
</p>
)}
</div>
<div className="mt-4 rounded-md border border-nothing-gray-200 bg-white/75 p-2">
@@ -1119,7 +1184,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)}>
{formatCount(row.count)}
{(row.key === 'rag' ? governanceLoading : !knowledgeReadbackReady) ? '--' : formatCount(row.count)}
</p>
</div>
)
@@ -1128,8 +1193,8 @@ export default function KnowledgeBasePage({
</div>
<div className="mt-4 grid grid-cols-2 gap-2 lg:grid-cols-1">
{[
{ label: t('rail.total'), value: isKnowledgeListUnavailable ? '--' : formatCount(total), icon: BookOpen },
{ label: t('rail.aiExtracted'), value: formatCount(visibleSummary.aiExtracted), icon: Bot },
{ label: t('rail.total'), value: knowledgeReadbackReady ? formatCount(total) : '--', icon: BookOpen },
{ label: t('rail.aiExtracted'), value: knowledgeReadbackReady ? formatCount(visibleSummary.aiExtracted) : '--', icon: Bot },
{
label: t('rail.controlledQueue'),
value: governanceLoading ? '--' : formatCount(governanceSummary.ownerPending),
@@ -1209,7 +1274,7 @@ export default function KnowledgeBasePage({
{/* Count */}
<span className="text-xs text-muted font-body">
{isKnowledgeListUnavailable ? '--' : total} {t('entries')}
{knowledgeReadbackReady ? formatCount(total) : '--'} {t('entries')}
</span>
{/* Create button */}
@@ -1324,15 +1389,15 @@ export default function KnowledgeBasePage({
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-[minmax(0,1fr)_280px]">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{[
{ label: t('overview.metricTotal'), value: formatCount(total), sub: t('overview.scopeFiltered'), tone: 'text-claw-blue' },
{ label: t('overview.metricLoaded'), value: formatCount(visibleSummary.loaded), sub: t('overview.scopeCurrent'), tone: 'text-primary' },
{ label: t('overview.metricAiExtracted'), value: formatCount(visibleSummary.aiExtracted), sub: t('overview.scopeCurrent'), tone: 'text-purple-600' },
{ label: t('overview.metricApproved'), value: `${formatCount(visibleSummary.approved)} / ${visibleSummary.approvedRate}%`, sub: t('overview.scopeCurrent'), tone: 'text-status-healthy' },
{ label: t('overview.metricTotal'), value: knowledgeReadbackReady ? formatCount(total) : '--', sub: t('overview.scopeFiltered'), tone: 'text-claw-blue' },
{ label: t('overview.metricLoaded'), value: knowledgeReadbackReady ? formatCount(visibleSummary.loaded) : '--', sub: t('overview.scopeCurrent'), tone: 'text-primary' },
{ label: t('overview.metricAiExtracted'), value: knowledgeReadbackReady ? formatCount(visibleSummary.aiExtracted) : '--', sub: t('overview.scopeCurrent'), tone: 'text-purple-600' },
{ label: t('overview.metricApproved'), value: knowledgeReadbackReady ? `${formatCount(visibleSummary.approved)} / ${visibleSummary.approvedRate}%` : '--', sub: t('overview.scopeCurrent'), tone: 'text-status-healthy' },
].map(metric => (
<div key={metric.label} className="rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{metric.label}</p>
<p className={cn('mt-1 text-xl font-heading font-semibold tabular-nums', metric.tone)}>
{isKnowledgeListUnavailable ? '--' : metric.value}
{metric.value}
</p>
<p className="mt-0.5 text-[10px] font-body text-muted">{metric.sub}</p>
</div>
@@ -1342,8 +1407,11 @@ export default function KnowledgeBasePage({
<div className="rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
<div className="mb-2 flex items-center justify-between">
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{t('overview.categoryDistribution')}</p>
<span className="text-[10px] font-body text-muted">{formatCount(totalCount)}</span>
<span className="text-[10px] font-body text-muted">
{knowledgeReadbackReady ? formatCount(totalCount) : '--'}
</span>
</div>
{categoryRows.length > 0 ? (
<div className="space-y-1.5">
{categoryRows.map(row => (
<div key={row.category} className="grid grid-cols-[96px_1fr_42px] items-center gap-2">
@@ -1360,6 +1428,57 @@ export default function KnowledgeBasePage({
</div>
))}
</div>
) : (
<p className="text-xs font-body text-muted">
{loading ? t('overview.loading') : t('overview.noCategories')}
</p>
)}
</div>
</div>
)}
{workspaceView === 'records' && (
<div
className="mt-3 rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2"
data-testid="knowledge-base-taxonomy-matrix"
>
<div className="mb-3 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{t('assetLens.matrixTitle')}</p>
<p className="mt-0.5 text-[10px] font-body text-muted">{t('assetLens.matrixSubtitle')}</p>
</div>
<span className="shrink-0 rounded-full border border-claw-blue/20 bg-claw-blue/8 px-2 py-0.5 text-[10px] font-label text-claw-blue">
{t('assetLens.autoUpdated')}
</span>
</div>
<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)
? '--'
: formatCount(row.count)
return (
<div key={row.key} className="rounded-md border border-nothing-gray-200 bg-white/70 p-2">
<div className="flex items-start justify-between gap-2">
<Icon className={cn('h-4 w-4 shrink-0', row.tone)} aria-hidden="true" />
{row.global && (
<span className="rounded-full border border-status-healthy/20 bg-status-healthy/10 px-1.5 py-0.5 text-[9px] font-label text-status-healthy">
{t('assetLens.global')}
</span>
)}
</div>
<p className="mt-2 truncate text-[10px] font-label uppercase tracking-wider text-muted">
{t(`assetLens.${row.key}` as never)}
</p>
<p className={cn('mt-1 text-xl font-heading font-semibold tabular-nums', row.tone)}>
{value}
</p>
<p className="mt-0.5 line-clamp-2 text-[10px] font-body leading-4 text-muted">
{t(`assetLens.detail.${row.key}` as never)}
</p>
</div>
)
})}
</div>
</div>
)}

View File

@@ -1,3 +1,25 @@
## 2026-07-03 — 02:40 Knowledge Base 分類矩陣與 RAG readback 修正
**完成內容**
- `/zh-TW/knowledge-base` 的載入與資料鏈路失敗狀態不再把主 KM、動態分類、總覽 metrics 與資產 lens 渲染成 misleading `0`;主 KM readback 未 ready 時改顯示 `--` / 載入狀態,避免誤判成知識庫歸零。
- RAG stats 前端請求補上 `project_id=awoooi`,對齊 tenant context正式 API readback 帶 project 後為 `total_chunks=5814``sources=6`,不再顯示成 `RAG CHUNKS 0`
- `資產維度` 與主畫面新增 `全域分類 / 資產矩陣`,從原本 `專案 / 產品 / 網站 / 服務 / 套件 / 工具` 擴為 `專案 / 產品 / 網站 / 服務 / 套件 / 工具 / Log / Alert / PlayBook / RAG / MCP / 排程`,讓 KM 與 AI automation 所需的 Log、Alert、PlayBook、RAG、MCP、worker 類別直接可見。
- 分類列改為只顯示 API readback 回來的實際分類;讀取前不再用 `DEFAULT_CATEGORY_ORDER` 產生一排 `0` 分類。
**驗證**
- Production API readback`/api/v1/knowledge?project_id=awoooi&limit=1``200`、總數約 `724/725``/api/v1/knowledge/categories?project_id=awoooi``200`、動態分類 `12``/api/v1/knowledge/rag/stats?project_id=awoooi``200``total_chunks=5814``sources=6`
- `python3.11 -m json.tool apps/web/messages/zh-TW.json``python3.11 -m json.tool apps/web/messages/en.json`:通過。
- `pnpm --dir apps/web typecheck`:通過。
- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web build`:通過。
- `pnpm --dir apps/web audit:ui-density`:通過;`/knowledge-base` fixed taxonomy signal 維持 `0`
- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .``git diff --check`:通過。
- Local production server + fulfilled production API smokedesktop / mobile HTTP `200``全域分類 / 資產矩陣``Log / 事件``Alert / 告警``PlayBook``RAG / 向量``MCP / Connector``排程 / Worker` 可見;`全部 724``動態分類 12``RAG 5,814``受控隊列 10` 可見;`知識條目資料鏈路異常=false``尚未建立任何知識條目=false``全部 0=false`、desktop/mobile `horizontalOverflow=false`、console error `0`
- 截圖證據:`/tmp/awoooi-kb-taxonomy-local-desktop-20260703.png``/tmp/awoooi-kb-taxonomy-local-mobile-20260703.png`
**仍維持**
- 這是 UI/API client readback 修正與分類矩陣擴充;未寫 KM 資料、未改 production DB schema、未提高 PlayBook trust、未觸發 runtime apply。
- 未讀 secret / token / `.env` / raw sessions / SQLite / auth未使用 GitHub / gh未重啟主機 / VM / Docker / K3s / DB / firewall未 DROP / TRUNCATE / restore / prune / force push。
## 2026-07-03 — 01:58 P0-006 host-probe TCP timeout 收斂
**完成內容**