Compare commits

...

1 Commits

Author SHA1 Message Date
Your Name
5dc0a2ef53 feat(km): surface dynamic knowledge categories 2026-07-03 00:38:23 +08:00
7 changed files with 519 additions and 60 deletions

View File

@@ -2159,23 +2159,40 @@
"title": "知識殿堂",
"searchPlaceholder": "搜尋知識條目...",
"allCategories": "全部",
"rail": {
"categoryTitle": "動態分類",
"total": "總 KM",
"aiExtracted": "AI 萃取",
"controlledQueue": "受控隊列",
"ragChunks": "RAG chunks"
},
"assetLens": {
"title": "資產維度",
"scope": "目前列表",
"project": "專案",
"product": "產品",
"website": "網站",
"service": "服務",
"package": "套件",
"tool": "工具"
},
"dataChain": {
"errorTitle": "知識條目資料鏈路異常",
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes owner-review 與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes 受控覆核與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
"retry": "重新讀取"
},
"decision": {
"badge": "KM 治理警戒",
"readback": "Hermes / AwoooP 只讀回讀",
"readback": "Hermes / AwoooP 受控回讀",
"title": "KM 自動化掌控台",
"subtitle": "先看 stale ratio、負責人審查、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
"openWorkItems": "處理 Owner Review",
"subtitle": "先看 stale ratio、受控覆核、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
"openWorkItems": "處理受控隊列",
"card": {
"staleRatio": "Stale Ratio",
"staleRatioSub": "門檻 {threshold}",
"staleTotal": "Stale KM",
"staleTotalSub": "距離門檻需處理 {count}",
"ownerQueue": "Owner Review",
"ownerQueue": "受控覆核",
"ownerQueueSub": "ready {ready} / blocked {blocked}",
"writeback": "已寫回",
"writebackSafe": "只讀回讀;讀取不寫入",
@@ -2184,6 +2201,11 @@
"ragIndexSub": "sources {sources}0 代表語意檢索尚未沉澱"
}
},
"workspace": {
"records": "Records",
"automation": "Automation",
"queue": "Queue"
},
"overview": {
"metricTotal": "總條目",
"metricLoaded": "目前列表",
@@ -2196,7 +2218,7 @@
},
"assetLedger": {
"title": "自動化資產沉澱總帳",
"subtitle": "把目前 KM 清單與 Hermes owner-review 讀模型,對齊到 PlayBook、腳本、排程、Verifier 五類資產。",
"subtitle": "把目前 KM 清單與 Hermes 受控覆核讀模型,對齊到 PlayBook、腳本、排程、Verifier 五類資產。",
"readOnly": "只讀總覽",
"boundary": "此區塊只顯示可追蹤沉澱狀態,不代表已授權修復、已寫回 KM 或已提高 PlayBook trust。",
"asset": {
@@ -2214,11 +2236,11 @@
},
"monitoring": {
"title": "排程 / 監控規則",
"detail": "含告警 / 監控訊號 {ready} 筆;負責人審查 待處理 {pending} 筆。"
"detail": "含告警 / 監控訊號 {ready} 筆;受控覆核待處理 {pending} 筆。"
},
"verifier": {
"title": "Verifier 回寫",
"detail": "已批准或完成驗證 {ready} 筆;待 負責人審查 {pending} 筆。"
"detail": "已批准或完成驗證 {ready} 筆;待受控覆核 {pending} 筆。"
},
"rag": {
"title": "RAG / 向量索引",
@@ -2244,7 +2266,7 @@
"incident": "Incident",
"playbook": "Playbook",
"review": "審核",
"sourceDetail": "AI {ai} / 人工 {human}",
"sourceDetail": "AI {ai} / 受控建立 {human}",
"knowledgeDetail": "已載入 {count}",
"countDetail": "{count} / {total}"
},
@@ -2257,8 +2279,8 @@
"missingIncidentAction": "補齊來源事故或標記為通用知識",
"missingPlaybook": "缺 Playbook",
"missingPlaybookAction": "匹配既有 Playbook或建立草稿",
"reviewBacklog": "待 Owner 審核",
"reviewBacklogAction": "高影響條目先人工覆核再批准"
"reviewBacklog": "待受控覆核",
"reviewBacklogAction": "高影響條目先進 controlled review再由 AI 補齊 verifier 與寫回證據"
},
"entryLineage": {
"title": "單筆證據鏈",
@@ -2270,12 +2292,12 @@
},
"workItems": {
"title": "Work Items 接續狀態",
"subtitle": "這裡只讀取 Hermes KB owner-review queue讓你知道陳舊 KM 是否已進入可追蹤工作項;實際預覽、確認與寫回仍在 AwoooP Work Items 執行。",
"subtitle": "這裡只讀取 Hermes KB controlled review queue讓你知道陳舊 KM 是否已進入可追蹤工作項;實際預覽、確認與寫回仍在 AwoooP Work Items 執行。",
"open": "打開 Work Items",
"loading": "讀取中",
"threshold": "門檻 {value}",
"entriesToThreshold": "需處理 {count} 筆才回到門檻",
"ownerPendingSub": "待 Owner review",
"ownerPendingSub": "待受控覆核",
"readyBlockedSub": "ready / blocked",
"burndownStatus": "狀態:{status}",
"topCandidate": "最高優先處理項",
@@ -2285,9 +2307,9 @@
"topCandidateRefs": "Incident {incident} / Playbook {playbook}",
"noCandidate": "目前沒有可顯示的 stale KM candidate。",
"ownerSplit": "主責分工",
"hermes": "Hermes建立 KM 更新草稿與owner-review queue。",
"hermes": "Hermes建立 KM 更新草稿與 controlled review queue。",
"openclaw": "OpenClaw補告警分類、Playbook與上下文摘要。",
"owner": "Owner:在 AwoooP Work Items 預覽後確認寫回。",
"owner": "Controlled lane:在 AwoooP Work Items 預覽後確認寫回邊界。",
"noWritesOnRead": "讀取不寫入",
"unexpectedWrites": "偵測到 read endpoint 宣告會寫入",
"flow": {
@@ -2295,9 +2317,9 @@
"scope": "從偵測到寫回與比例回測的目前位置",
"node": {
"detected": "偵測",
"ownerReview": "Owner Review",
"ownerReview": "Controlled Review",
"dryRun": "乾跑預覽",
"ownerConfirm": "Owner 確認",
"ownerConfirm": "邊界確認",
"writeback": "寫回 KM",
"recheck": "比例回測"
},
@@ -2310,7 +2332,7 @@
},
"detail": {
"detected": "目前 {ratio};門檻 {threshold}",
"ownerReview": "{count} 筆等待 owner 審核",
"ownerReview": "{count} 筆等待受控覆核",
"dryRun": "{ready} 筆可乾跑;{blocked} 筆卡住",
"ownerConfirm": "確認後才允許寫回,避免 AI 固化錯誤知識",
"writeback": "{count} 筆已有 completion audit",
@@ -2356,7 +2378,7 @@
},
"source": {
"ai_extracted": "AI 萃取",
"human": "人工建立"
"human": "受控建立"
},
"signals": {
"label": "訊號",
@@ -2371,14 +2393,15 @@
"execution": "執行紀錄",
"execution_failed": "執行失敗",
"failure": "失敗",
"human_approved": "人工批准",
"human_intervention": "人工介入",
"human_approved": "受控批准",
"human_intervention": "受控介入",
"incident": "事故",
"postmortem": "事後檢討",
"telegram": "Telegram",
"warning": "警告"
},
"category": {
"AI自動化/Ansible受控修復": "AI 自動化 / Ansible 受控修復",
"AI治理": "AI 治理",
"alert_handling": "告警處理",
"infrastructure": "基礎設施",
@@ -2396,13 +2419,20 @@
"flywheel_health": "飛輪健康",
"ssl_cert": "SSL 憑證",
"devops_tool": "DevOps 工具",
"incident_postmortem": "事故檢討"
"incident_postmortem": "事故檢討",
"project": "專案",
"product": "產品",
"website": "網站",
"service": "服務",
"package": "套件",
"tool": "工具",
"source_control": "Source Control"
},
"filterByType": "篩選類型",
"filterByStatus": "篩選狀態",
"entries": "筆",
"empty": "尚未建立任何知識條目",
"emptyDescription": "知識庫將自動從 Incident 中萃取案例,你也可以手動新增",
"emptyDescription": "知識庫將自動從 Incident、LOG、PlayBook 與 verifier 中萃取案例",
"semanticSearchPlaceholder": "輸入語意搜尋查詢...",
"semanticOn": "語意",
"semanticOff": "語意",

View File

@@ -2159,23 +2159,40 @@
"title": "知識殿堂",
"searchPlaceholder": "搜尋知識條目...",
"allCategories": "全部",
"rail": {
"categoryTitle": "動態分類",
"total": "總 KM",
"aiExtracted": "AI 萃取",
"controlledQueue": "受控隊列",
"ragChunks": "RAG chunks"
},
"assetLens": {
"title": "資產維度",
"scope": "目前列表",
"project": "專案",
"product": "產品",
"website": "網站",
"service": "服務",
"package": "套件",
"tool": "工具"
},
"dataChain": {
"errorTitle": "知識條目資料鏈路異常",
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes owner-review 與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes 受控覆核與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
"retry": "重新讀取"
},
"decision": {
"badge": "KM 治理警戒",
"readback": "Hermes / AwoooP 只讀回讀",
"readback": "Hermes / AwoooP 受控回讀",
"title": "KM 自動化掌控台",
"subtitle": "先看 stale ratio、負責人審查、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
"openWorkItems": "處理 Owner Review",
"subtitle": "先看 stale ratio、受控覆核、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
"openWorkItems": "處理受控隊列",
"card": {
"staleRatio": "Stale Ratio",
"staleRatioSub": "門檻 {threshold}",
"staleTotal": "Stale KM",
"staleTotalSub": "距離門檻需處理 {count}",
"ownerQueue": "Owner Review",
"ownerQueue": "受控覆核",
"ownerQueueSub": "ready {ready} / blocked {blocked}",
"writeback": "已寫回",
"writebackSafe": "只讀回讀;讀取不寫入",
@@ -2184,6 +2201,11 @@
"ragIndexSub": "sources {sources}0 代表語意檢索尚未沉澱"
}
},
"workspace": {
"records": "Records",
"automation": "Automation",
"queue": "Queue"
},
"overview": {
"metricTotal": "總條目",
"metricLoaded": "目前列表",
@@ -2196,7 +2218,7 @@
},
"assetLedger": {
"title": "自動化資產沉澱總帳",
"subtitle": "把目前 KM 清單與 Hermes owner-review 讀模型,對齊到 PlayBook、腳本、排程、Verifier 五類資產。",
"subtitle": "把目前 KM 清單與 Hermes 受控覆核讀模型,對齊到 PlayBook、腳本、排程、Verifier 五類資產。",
"readOnly": "只讀總覽",
"boundary": "此區塊只顯示可追蹤沉澱狀態,不代表已授權修復、已寫回 KM 或已提高 PlayBook trust。",
"asset": {
@@ -2214,11 +2236,11 @@
},
"monitoring": {
"title": "排程 / 監控規則",
"detail": "含告警 / 監控訊號 {ready} 筆;負責人審查 待處理 {pending} 筆。"
"detail": "含告警 / 監控訊號 {ready} 筆;受控覆核待處理 {pending} 筆。"
},
"verifier": {
"title": "Verifier 回寫",
"detail": "已批准或完成驗證 {ready} 筆;待 負責人審查 {pending} 筆。"
"detail": "已批准或完成驗證 {ready} 筆;待受控覆核 {pending} 筆。"
},
"rag": {
"title": "RAG / 向量索引",
@@ -2244,7 +2266,7 @@
"incident": "Incident",
"playbook": "Playbook",
"review": "審核",
"sourceDetail": "AI {ai} / 人工 {human}",
"sourceDetail": "AI {ai} / 受控建立 {human}",
"knowledgeDetail": "已載入 {count}",
"countDetail": "{count} / {total}"
},
@@ -2257,8 +2279,8 @@
"missingIncidentAction": "補齊來源事故或標記為通用知識",
"missingPlaybook": "缺 Playbook",
"missingPlaybookAction": "匹配既有 Playbook或建立草稿",
"reviewBacklog": "待 Owner 審核",
"reviewBacklogAction": "高影響條目先人工覆核再批准"
"reviewBacklog": "待受控覆核",
"reviewBacklogAction": "高影響條目先進 controlled review再由 AI 補齊 verifier 與寫回證據"
},
"entryLineage": {
"title": "單筆證據鏈",
@@ -2270,12 +2292,12 @@
},
"workItems": {
"title": "Work Items 接續狀態",
"subtitle": "這裡只讀取 Hermes KB owner-review queue讓你知道陳舊 KM 是否已進入可追蹤工作項;實際預覽、確認與寫回仍在 AwoooP Work Items 執行。",
"subtitle": "這裡只讀取 Hermes KB controlled review queue讓你知道陳舊 KM 是否已進入可追蹤工作項;實際預覽、確認與寫回仍在 AwoooP Work Items 執行。",
"open": "打開 Work Items",
"loading": "讀取中",
"threshold": "門檻 {value}",
"entriesToThreshold": "需處理 {count} 筆才回到門檻",
"ownerPendingSub": "待 Owner review",
"ownerPendingSub": "待受控覆核",
"readyBlockedSub": "ready / blocked",
"burndownStatus": "狀態:{status}",
"topCandidate": "最高優先處理項",
@@ -2285,9 +2307,9 @@
"topCandidateRefs": "Incident {incident} / Playbook {playbook}",
"noCandidate": "目前沒有可顯示的 stale KM candidate。",
"ownerSplit": "主責分工",
"hermes": "Hermes建立 KM 更新草稿與owner-review queue。",
"hermes": "Hermes建立 KM 更新草稿與 controlled review queue。",
"openclaw": "OpenClaw補告警分類、Playbook與上下文摘要。",
"owner": "Owner:在 AwoooP Work Items 預覽後確認寫回。",
"owner": "Controlled lane:在 AwoooP Work Items 預覽後確認寫回邊界。",
"noWritesOnRead": "讀取不寫入",
"unexpectedWrites": "偵測到 read endpoint 宣告會寫入",
"flow": {
@@ -2295,9 +2317,9 @@
"scope": "從偵測到寫回與比例回測的目前位置",
"node": {
"detected": "偵測",
"ownerReview": "Owner Review",
"ownerReview": "Controlled Review",
"dryRun": "乾跑預覽",
"ownerConfirm": "Owner 確認",
"ownerConfirm": "邊界確認",
"writeback": "寫回 KM",
"recheck": "比例回測"
},
@@ -2310,7 +2332,7 @@
},
"detail": {
"detected": "目前 {ratio};門檻 {threshold}",
"ownerReview": "{count} 筆等待 owner 審核",
"ownerReview": "{count} 筆等待受控覆核",
"dryRun": "{ready} 筆可乾跑;{blocked} 筆卡住",
"ownerConfirm": "確認後才允許寫回,避免 AI 固化錯誤知識",
"writeback": "{count} 筆已有 completion audit",
@@ -2356,7 +2378,7 @@
},
"source": {
"ai_extracted": "AI 萃取",
"human": "人工建立"
"human": "受控建立"
},
"signals": {
"label": "訊號",
@@ -2371,14 +2393,15 @@
"execution": "執行紀錄",
"execution_failed": "執行失敗",
"failure": "失敗",
"human_approved": "人工批准",
"human_intervention": "人工介入",
"human_approved": "受控批准",
"human_intervention": "受控介入",
"incident": "事故",
"postmortem": "事後檢討",
"telegram": "Telegram",
"warning": "警告"
},
"category": {
"AI自動化/Ansible受控修復": "AI 自動化 / Ansible 受控修復",
"AI治理": "AI 治理",
"alert_handling": "告警處理",
"infrastructure": "基礎設施",
@@ -2396,13 +2419,20 @@
"flywheel_health": "飛輪健康",
"ssl_cert": "SSL 憑證",
"devops_tool": "DevOps 工具",
"incident_postmortem": "事故檢討"
"incident_postmortem": "事故檢討",
"project": "專案",
"product": "產品",
"website": "網站",
"service": "服務",
"package": "套件",
"tool": "工具",
"source_control": "Source Control"
},
"filterByType": "篩選類型",
"filterByStatus": "篩選狀態",
"entries": "筆",
"empty": "尚未建立任何知識條目",
"emptyDescription": "知識庫將自動從 Incident 中萃取案例,你也可以手動新增",
"emptyDescription": "知識庫將自動從 Incident、LOG、PlayBook 與 verifier 中萃取案例",
"semanticSearchPlaceholder": "輸入語意搜尋查詢...",
"semanticOn": "語意",
"semanticOff": "語意",

View File

@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"audit:ui-density": "node scripts/audit-ui-density.mjs"
},
"dependencies": {
"@awoooi/lewooogo-core": "workspace:*",

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env node
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { join, relative } from 'node:path'
const appRoot = join(process.cwd(), 'src', 'app')
const textLikePatterns = [
/<p\b/g,
/<span\b/g,
/<li\b/g,
/<h[1-6]\b/g,
/<ReactMarkdown\b/g,
/prose\b/g,
/leading-\d/g,
]
const controlPatterns = [
/<button\b/g,
/<select\b/g,
/<input\b/g,
/<Link\b/g,
/role=["']tab/g,
/type=["']checkbox/g,
]
const layoutPatterns = [
/grid-cols-/g,
/flex\b/g,
/overflow-y-auto/g,
/sticky\b/g,
]
const textWallPatterns = [
/max-w-(?:2xl|3xl|4xl|5xl|6xl|7xl)/g,
/line-clamp-[4-9]/g,
/prose\b/g,
/<ReactMarkdown\b/g,
]
const manualTerms = [
/\u4eba\u5de5/g,
/\u624b\u52d5/g,
/manual/gi,
/needs_human/gi,
/human_/gi,
]
const fixedTaxonomyPatterns = [
/const\s+CATEGORIES\s*=/g,
]
function collectPages(dir, acc = []) {
for (const name of readdirSync(dir)) {
const fullPath = join(dir, name)
const stats = statSync(fullPath)
if (stats.isDirectory()) {
collectPages(fullPath, acc)
} else if (name === 'page.tsx') {
acc.push(fullPath)
}
}
return acc
}
function countMatches(source, patterns) {
return patterns.reduce((sum, pattern) => sum + (source.match(pattern) ?? []).length, 0)
}
function routeFromPage(filePath) {
return relative(appRoot, filePath)
.replace(/\/page\.tsx$/, '')
.replace(/\[locale\]/, ':locale')
}
const pages = collectPages(appRoot)
.map(filePath => {
const source = readFileSync(filePath, 'utf8')
const lineCount = source.split('\n').length
const textSignals = countMatches(source, textLikePatterns)
const controls = countMatches(source, controlPatterns)
const layoutSignals = countMatches(source, layoutPatterns)
const textWallSignals = countMatches(source, textWallPatterns)
const manualSignals = countMatches(source, manualTerms)
const fixedTaxonomySignals = countMatches(source, fixedTaxonomyPatterns)
const nestedCardSignals = (source.match(/rounded-(?:md|lg|xl)[^`'"]*border/g) ?? []).length
const score =
Math.round(
(textSignals * 1.2)
+ (textWallSignals * 8)
+ (manualSignals * 4)
+ (fixedTaxonomySignals * 10)
+ (nestedCardSignals * 0.7)
- (controls * 1.5)
- (layoutSignals * 0.4),
)
return {
route: routeFromPage(filePath),
file: relative(process.cwd(), filePath),
lines: lineCount,
score,
textSignals,
controls,
layoutSignals,
textWallSignals,
manualSignals,
fixedTaxonomySignals,
nestedCardSignals,
}
})
.sort((a, b) => b.score - a.score)
console.log('AWOOOI_UI_DENSITY_AUDIT')
console.table(
pages.slice(0, 20).map(page => ({
route: page.route,
score: page.score,
lines: page.lines,
text: page.textSignals,
controls: page.controls,
manual: page.manualSignals,
fixedTaxonomy: page.fixedTaxonomySignals,
})),
)
console.log(JSON.stringify({
page_count: pages.length,
top_routes: pages.slice(0, 20),
}, null, 2))

View File

@@ -1288,7 +1288,6 @@ const COMMANDER_INSERTED_REQUIREMENT_FALLBACK: PriorityWorkOrderResponse = {
"windows99_uptime_unknown",
],
stockplatform_public_api_runtime_status: "stockplatform_public_api_runtime_ready",
stockplatform_public_api_runtime_ready: true,
stockplatform_public_api_controlled_recovery_preflight_status:
"not_required_stockplatform_runtime_ready",
stockplatform_public_api_controlled_recovery_data_dependency: "none",

View File

@@ -17,10 +17,11 @@ import { useTranslations } from 'next-intl'
import Link from 'next/link'
import { cn } from '@/lib/utils'
import {
Search, BookOpen, FileText, Shield, Cpu,
type LucideIcon,
Search, BookOpen, FileText, Shield, Cpu, Globe2,
Server, Eye, Bot, ChevronRight, Plus, Sparkles, ClipboardList, Tag, TriangleAlert,
GitBranch, CheckCircle2, Clock3, Link2, FileSearch, ListChecks, ArrowRight, Users,
PlayCircle, Database, RotateCw,
PlayCircle, Database, RotateCw, Package, Wrench,
} from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
@@ -130,20 +131,58 @@ interface KnowledgeGovernanceTelemetry {
}
type AutomationAssetKey = 'km' | 'playbook' | 'script' | 'monitoring' | 'verifier' | 'rag'
type WorkspaceViewKey = 'records' | 'automation' | 'queue'
type AssetLensKey = 'project' | 'product' | 'website' | 'service' | 'package' | 'tool'
// =============================================================================
// Category Config
// =============================================================================
const CATEGORY_ICONS: Record<string, React.ReactNode> = {
'AI自動化/Ansible受控修復': <Bot className="w-4 h-4" />,
AI治理: <Bot className="w-4 h-4" />,
alert_handling: <TriangleAlert className="w-4 h-4" />,
infrastructure: <Server className="w-4 h-4" />,
application: <FileText className="w-4 h-4" />,
ai_system: <Bot className="w-4 h-4" />,
security: <Shield className="w-4 h-4" />,
host_resource: <Cpu className="w-4 h-4" />,
database: <Database className="w-4 h-4" />,
auto_repair: <Wrench className="w-4 h-4" />,
postmortem: <FileSearch className="w-4 h-4" />,
incident_postmortem: <FileSearch className="w-4 h-4" />,
external_site: <Globe2 className="w-4 h-4" />,
backup_failure: <Database className="w-4 h-4" />,
kubernetes: <Package className="w-4 h-4" />,
flywheel_health: <RotateCw className="w-4 h-4" />,
ssl_cert: <Shield className="w-4 h-4" />,
devops_tool: <Wrench className="w-4 h-4" />,
general: <BookOpen className="w-4 h-4" />,
}
const CATEGORIES = ['infrastructure', 'application', 'ai_system', 'security'] as const
const DEFAULT_CATEGORY_ORDER = [
'alert_handling',
'AI自動化/Ansible受控修復',
'infrastructure',
'host_resource',
'postmortem',
'AI治理',
'application',
'ai_system',
'security',
'database',
'auto_repair',
'kubernetes',
'external_site',
'devops_tool',
'ssl_cert',
'backup_failure',
'flywheel_health',
'general',
]
const KNOWN_CATEGORY_KEYS = new Set([
'AI自動化/Ansible受控修復',
'AI治理',
'alert_handling',
'application',
@@ -160,8 +199,14 @@ const KNOWN_CATEGORY_KEYS = new Set([
'infrastructure',
'kubernetes',
'postmortem',
'project',
'product',
'service',
'security',
'source_control',
'ssl_cert',
'tool',
'website',
])
const CATEGORY_OVERVIEW_LIMIT = 8
@@ -222,17 +267,29 @@ const STATUS_COLORS: Record<string, string> = {
}
const CATEGORY_BAR_COLORS: Record<string, string> = {
'AI自動化/Ansible受控修復': 'bg-purple-500',
AI治理: 'bg-claw-blue',
alert_handling: 'bg-status-warning',
infrastructure: 'bg-claw-blue',
host_resource: 'bg-status-warning',
application: 'bg-status-healthy',
ai_system: 'bg-purple-500',
security: 'bg-status-warning',
database: 'bg-status-critical',
auto_repair: 'bg-purple-500',
external_site: 'bg-status-healthy',
devops_tool: 'bg-claw-blue',
kubernetes: 'bg-claw-blue',
ssl_cert: 'bg-status-warning',
backup_failure: 'bg-status-critical',
flywheel_health: 'bg-status-healthy',
general: 'bg-nothing-gray-400',
postmortem: 'bg-status-critical',
__other__: 'bg-nothing-gray-300',
}
const CATEGORY_ORDER_RANK = new Map(DEFAULT_CATEGORY_ORDER.map((category, index) => [category, index]))
const TAG_TONES: Record<string, string> = {
critical: 'border-status-critical/20 bg-status-critical/10 text-status-critical',
execution_failed: 'border-status-critical/20 bg-status-critical/10 text-status-critical',
@@ -285,6 +342,28 @@ const getSignalTags = (entry: KnowledgeEntry) => {
})
}
const entrySearchText = (entry: KnowledgeEntry) =>
[
entry.title,
entry.category,
entry.entry_type,
entry.source,
...entry.tags,
].join(' ').toLowerCase()
const entryMatchesAny = (entry: KnowledgeEntry, candidates: string[]) => {
const text = entrySearchText(entry)
return candidates.some(candidate => text.includes(candidate.toLowerCase()))
}
const mergeCategoryCounts = (rows: CategoryCount[]) => {
const merged = new Map<string, number>()
rows.forEach(row => {
merged.set(row.category, (merged.get(row.category) ?? 0) + row.count)
})
return Array.from(merged.entries()).map(([category, count]) => ({ category, count }))
}
// =============================================================================
// Component
// =============================================================================
@@ -319,6 +398,7 @@ export default function KnowledgeBasePage({
const [semanticResults, setSemanticResults] = useState<(KnowledgeEntry & { score: number })[]>([])
const [semanticLoading, setSemanticLoading] = useState(false)
const [selectedEntry, setSelectedEntry] = useState<KnowledgeEntry | null>(null)
const [workspaceView, setWorkspaceView] = useState<WorkspaceViewKey>('records')
// KB-D: approve/archive state (2026-04-03 ogt)
const [actionLoading, setActionLoading] = useState(false)
@@ -519,8 +599,8 @@ export default function KnowledgeBasePage({
const categoryRows = useMemo(() => {
const sourceRows = categories.length > 0
? [...categories].sort((a, b) => b.count - a.count)
: CATEGORIES.map(category => ({ category, count: 0 }))
? mergeCategoryCounts(categories).sort((a, b) => b.count - a.count)
: DEFAULT_CATEGORY_ORDER.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
@@ -542,6 +622,81 @@ 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 }))
return [...sourceRows].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])
const assetLensRows = useMemo(() => {
const rows: Array<{
key: AssetLensKey
icon: LucideIcon
count: number
tone: string
}> = [
{
key: 'project',
icon: GitBranch,
count: displayedEntries.filter(entry => entryMatchesAny(entry, [
'project', 'repo', 'repository', 'gitea', 'branch', 'workflow', 'source_control',
])).length,
tone: 'text-claw-blue',
},
{
key: 'product',
icon: Package,
count: displayedEntries.filter(entry => entryMatchesAny(entry, [
'product', 'awoooi', 'awooop', 'iwooos', 'vibework', 'stockplatform', 'momo', 'awooogo', 'agent-bounty', 'tsenyang',
])).length,
tone: 'text-purple-600',
},
{
key: 'website',
icon: Globe2,
count: displayedEntries.filter(entry =>
entry.category === 'external_site'
|| entryMatchesAny(entry, ['website', 'site', 'nginx', 'ssl', 'domain', 'route', 'frontend']),
).length,
tone: 'text-status-healthy',
},
{
key: 'service',
icon: Server,
count: displayedEntries.filter(entry =>
['infrastructure', 'application', 'ai_system', 'database', 'host_resource', 'kubernetes', 'alert_handling'].includes(entry.category)
|| entryMatchesAny(entry, ['service', 'daemon', 'api', 'worker', 'runtime', 'container', 'pod']),
).length,
tone: 'text-primary',
},
{
key: 'package',
icon: Package,
count: displayedEntries.filter(entry => entryMatchesAny(entry, [
'package', 'dependency', 'npm', 'pnpm', 'node', 'python', 'pip', 'prisma', 'next', 'library',
])).length,
tone: 'text-status-warning',
},
{
key: 'tool',
icon: Wrench,
count: displayedEntries.filter(entry =>
entry.category === 'devops_tool'
|| entryMatchesAny(entry, ['tool', 'ansible', 'mcp', 'playbook', 'telegram', 'wazuh', 'kali', 'sentry', 'signoz', 'runner']),
).length,
tone: 'text-status-critical',
},
]
return rows
}, [displayedEntries])
const qualityRows = useMemo(() => {
const loaded = displayedEntries.length
const now = Date.now()
@@ -887,11 +1042,20 @@ export default function KnowledgeBasePage({
] as const
}, [formatCount, governanceSummary, governanceTelemetry.ragStats, t])
const workspaceViews = useMemo(
() => [
{ key: 'records' as WorkspaceViewKey, icon: BookOpen },
{ key: 'automation' as WorkspaceViewKey, icon: Bot },
{ key: 'queue' as WorkspaceViewKey, icon: ListChecks },
],
[],
)
const content = (
<div className="flex min-h-[calc(100vh-64px)] flex-col lg:h-[calc(100vh-64px)] lg:flex-row">
{/* 左側分類導航 */}
<aside className="w-full flex-shrink-0 border-b border-nothing-gray-200/50 bg-white/50 p-3 lg:w-52 lg:border-b-0 lg:border-r">
<aside className="w-full flex-shrink-0 border-b border-nothing-gray-200/50 bg-white/60 p-3 lg:w-60 lg:border-b-0 lg:border-r">
<h2 className="font-heading text-xs font-semibold text-tertiary uppercase tracking-widest mb-3 px-2">
{t('title')}
</h2>
@@ -911,10 +1075,16 @@ export default function KnowledgeBasePage({
<span className="text-xs text-muted">{totalCount}</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>
</div>
{/* Category items */}
<div className="mt-1 grid grid-cols-2 gap-0.5 lg:block lg:space-y-0.5">
{CATEGORIES.map(cat => {
const count = categories.find(c => c.category === cat)?.count ?? 0
<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 => {
const cat = row.category
const icon = CATEGORY_ICONS[cat] ?? <BookOpen className="w-4 h-4" />
return (
<button
key={cat}
@@ -926,13 +1096,63 @@ export default function KnowledgeBasePage({
: 'text-secondary hover:bg-nothing-gray-100'
)}
>
{CATEGORY_ICONS[cat]}
{icon}
<span className="flex-1 text-left">{formatCategoryLabel(cat)}</span>
<span className="text-xs text-muted">{count}</span>
<span className="text-xs text-muted">{formatCount(row.count)}</span>
</button>
)
})}
</div>
<div className="mt-4 rounded-md border border-nothing-gray-200 bg-white/75 p-2">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="truncate text-[10px] font-label uppercase tracking-wider text-muted">{t('assetLens.title')}</span>
<span className="shrink-0 text-[10px] font-body text-muted">{t('assetLens.scope')}</span>
</div>
<div className="grid grid-cols-2 gap-1">
{assetLensRows.map(row => {
const Icon = row.icon
return (
<div key={row.key} className="rounded border border-nothing-gray-100 bg-nothing-gray-50 px-2 py-1.5">
<div className="flex items-center justify-between gap-1">
<span className="truncate text-[10px] font-body text-secondary">{t(`assetLens.${row.key}` as never)}</span>
<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)}
</p>
</div>
)
})}
</div>
</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.controlledQueue'),
value: governanceLoading ? '--' : formatCount(governanceSummary.ownerPending),
icon: ListChecks,
},
{
label: t('rail.ragChunks'),
value: governanceLoading ? '--' : formatCount(governanceTelemetry.ragStats?.total_chunks ?? 0),
icon: FileSearch,
},
].map(item => {
const Icon = item.icon
return (
<div key={item.label} className="rounded-md border border-nothing-gray-200 bg-white/75 px-2.5 py-2">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-[10px] font-label uppercase tracking-wider text-muted">{item.label}</span>
<Icon className="h-3.5 w-3.5 shrink-0 text-claw-blue" aria-hidden="true" />
</div>
<p className="mt-1 text-lg font-heading font-semibold tabular-nums text-primary">{item.value}</p>
</div>
)
})}
</div>
</aside>
{/* 主區域 */}
@@ -1077,9 +1297,31 @@ export default function KnowledgeBasePage({
)
})}
</div>
<div className="mt-3 flex flex-wrap gap-1 rounded-md border border-nothing-gray-200 bg-nothing-gray-50 p-1">
{workspaceViews.map(view => {
const Icon = view.icon
return (
<button
key={view.key}
type="button"
onClick={() => setWorkspaceView(view.key)}
className={cn(
'inline-flex flex-1 items-center justify-center gap-1.5 rounded px-3 py-1.5 text-xs font-label transition-colors sm:flex-none',
workspaceView === view.key
? 'bg-white text-claw-blue shadow-sm'
: 'text-muted hover:bg-white/70 hover:text-secondary',
)}
>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
{t(`workspace.${view.key}` as never)}
</button>
)
})}
</div>
</div>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1fr)_280px]">
{workspaceView === 'records' && (
<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' },
@@ -1120,7 +1362,9 @@ export default function KnowledgeBasePage({
</div>
</div>
</div>
)}
{workspaceView === 'automation' && (
<div className="mt-3 rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
<div className="mb-3 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
@@ -1169,7 +1413,9 @@ export default function KnowledgeBasePage({
{t('assetLedger.boundary')}
</p>
</div>
)}
{workspaceView === 'records' && (
<div className="mt-3 rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
<div className="mb-2 flex items-center justify-between gap-2">
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{t('quality.title')}</p>
@@ -1194,7 +1440,9 @@ export default function KnowledgeBasePage({
))}
</div>
</div>
)}
{(workspaceView === 'records' || workspaceView === 'automation') && (
<div className="mt-3 grid grid-cols-1 gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(260px,0.75fr)]">
<div className="rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
<div className="mb-3 flex items-center justify-between gap-2">
@@ -1259,7 +1507,9 @@ export default function KnowledgeBasePage({
</div>
</div>
</div>
)}
{workspaceView === 'queue' && (
<div className="mt-3 rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
@@ -1413,6 +1663,7 @@ export default function KnowledgeBasePage({
</div>
</div>
</div>
)}
</section>
{/* 結果列表 */}

View File

@@ -20,6 +20,24 @@
- P0-006 仍 fail-closed最新 production reboot SLO scorecard 仍有 `active_blocker_count=11`,包含 99 remote execution channel、VMware autostart readback、host boot detection、service/backup/Wazuh 類 blocker不得宣稱 10 分鐘全主機自動恢復 SLO 已完成。
- 未讀 secret / token / `.env` / raw sessions / SQLite / auth未使用 GitHub / gh未 workflow_dispatch未重啟 host / VM / service未 Docker / Nginx / K3s / DB / firewall restart未 DROP / TRUNCATE / restore / prune / delete / force push。
## 2026-07-03 — 00:31 Knowledge Base 分類/資產鏡頭與 UI density audit
**完成內容**
- `/zh-TW/knowledge-base` 左側分類由固定四類改為 production category readback 驅動,合併重複分類計數,移除 misleading 的 `應用層 0``安全 / 合規 0` 這類空殼分類。
- 新增資產鏡頭:專案、產品、網站、服務、套件、工具,先用目前列表的 title / category / type / tags 做可解釋分群,讓 KM 不再只以少數技術類別呈現。
- Knowledge Base 工作區改成 `記錄 / 自動化 / 隊列` 分段視圖,降低治理文字牆一次全部攤開的問題。
- 新增 `pnpm --dir apps/web audit:ui-density`,自動掃描所有 `src/app/**/page.tsx`,依文字密度、控制項不足、人工語意、固定分類等訊號排出 UI/UX 優先序;不用再靠逐頁人工回報。
- 修正 rebase 後 `awooop/work-items` fallback summary 多出的 `stockplatform_public_api_runtime_ready` 欄位,讓最新 main 的 web typecheck 恢復通過。
**驗證**
- `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`:通過。
- `pnpm --dir apps/web audit:ui-density`:通過;目前前五優先頁為 `:locale/iwooos``:locale/awooop/work-items``:locale/awooop/runs``:locale/awooop``:locale/awooop/approvals`
- Local web + production API smokedesktop / mobile HTTP 200主 KM total `727` 可見,`告警處理 225``AI 自動化 / Ansible 受控修復 154`、資產鏡頭可見API response 全部 200console error `0`
**仍維持**
- 未讀 secret / token / `.env` / raw sessions / SQLite / auth未使用 GitHub / gh未 production DB schema / data destructive write未重啟主機 / VM / Docker / K3s / DB / firewall。
## 2026-07-03 — 00:11 P0-006 latest CD profile / Windows99 live probe readback
**完成內容**