fix(km): recover primary knowledge readback via direct fallback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m36s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
ogt
2026-07-10 01:49:47 +08:00
parent 2752b9f554
commit a63e645513
5 changed files with 704 additions and 16 deletions

View File

@@ -64,6 +64,7 @@ interface ListResponse {
categories: CategoryCount[]
asset_taxonomy?: AssetTaxonomyCount[]
readback_status?: 'ready' | 'degraded' | string
primary_readback_ready?: boolean
degraded_reason_code?: string | null
operator_stage?: string | null
next_step?: string | null
@@ -441,8 +442,12 @@ const humanizeSignalTag = (tag: string) => {
const compactReadbackToken = (value: string | null | undefined) => {
if (!value) return '--'
if (value === 'primary_km_db_timeout_or_pool_exhausted') return 'DB pool'
if (value === 'ready_direct_connection_after_session_timeout') return 'Direct KM'
if (value === 'knowledge_readback_direct_connection_recovered') return '已救回'
if (value === 'repair_session_pool_readback_without_hiding_primary_km') return '修復 pool'
if (value === 'knowledge_readback_source_backed_ai_controlled_repair') return '接管'
if (value === 'repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km') return '修復 KM'
if (value === 'source_backed_db_empty_direct_connection') return 'Direct 空值'
if (value === 'primary_km_taxonomy_readback_degraded') return 'Taxonomy 降級'
if (value === 'knowledge_primary_entries_ready_taxonomy_degraded') return 'Primary 可讀'
if (value === 'repair_primary_km_taxonomy_readback_without_hiding_entries') return '修復 taxonomy'
@@ -509,7 +514,7 @@ export default function KnowledgeBasePage({
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' | 'degraded_reason_code' | 'operator_stage' | 'next_step'> | null>(null)
const [entryReadback, setEntryReadback] = useState<Pick<ListResponse, 'readback_status' | 'primary_readback_ready' | 'degraded_reason_code' | 'operator_stage' | 'next_step'> | null>(null)
const [governanceTelemetry, setGovernanceTelemetry] = useState<KnowledgeGovernanceTelemetry>({
staleCandidates: null,
ownerReviews: null,
@@ -554,6 +559,7 @@ export default function KnowledgeBasePage({
setAssetTaxonomy(data.asset_taxonomy?.length ? data.asset_taxonomy : ASSET_TAXONOMY_FALLBACK_COUNTS)
setEntryReadback({
readback_status: data.readback_status ?? 'ready',
primary_readback_ready: data.primary_readback_ready ?? false,
degraded_reason_code: data.degraded_reason_code ?? null,
operator_stage: data.operator_stage ?? null,
next_step: data.next_step ?? null,
@@ -701,10 +707,16 @@ export default function KnowledgeBasePage({
const totalCount = categories.reduce((sum, c) => sum + c.count, 0)
const isKnowledgeListUnavailable = Boolean(entryFetchError)
const readbackStatus = entryReadback?.readback_status ?? null
const isKnowledgeDirectRecovered = readbackStatus === 'ready_direct_connection_after_session_timeout'
const isKnowledgeSourceBacked = Boolean(readbackStatus?.startsWith('source_backed'))
const isKnowledgeListDegraded = Boolean(readbackStatus && readbackStatus !== 'ready')
const isKnowledgeListDegraded = Boolean(readbackStatus && readbackStatus !== 'ready' && !isKnowledgeDirectRecovered)
const hasKnowledgePayload = entries.length > 0 || total > 0 || categories.some(category => category.count > 0)
const knowledgeReadbackReady = !loading && !isKnowledgeListUnavailable && (!isKnowledgeListDegraded || hasKnowledgePayload)
const showKnowledgeReadbackNotice = Boolean(entryFetchError || isKnowledgeListDegraded || isKnowledgeDirectRecovered)
const readbackNoticeTone = isKnowledgeDirectRecovered
? 'border-status-healthy/20 bg-status-healthy/5'
: 'border-status-warning/20 bg-status-warning/5'
const readbackNoticeTitleTone = isKnowledgeDirectRecovered ? 'text-status-healthy' : 'text-status-warning'
const localeCode = params.locale === 'en' ? 'en-US' : 'zh-TW'
const formatCount = useCallback(
(value: number) => value.toLocaleString(localeCode),
@@ -714,7 +726,11 @@ export default function KnowledgeBasePage({
() => [
{
label: t('dataChain.chip.mode'),
value: isKnowledgeSourceBacked ? t('dataChain.mode.sourceBacked') : compactReadbackToken(readbackStatus),
value: isKnowledgeSourceBacked
? t('dataChain.mode.sourceBacked')
: isKnowledgeDirectRecovered
? t('dataChain.mode.directRecovered')
: compactReadbackToken(readbackStatus),
icon: CheckCircle2,
tone: 'border-status-healthy/20 bg-status-healthy/10 text-status-healthy',
},
@@ -757,6 +773,7 @@ export default function KnowledgeBasePage({
entryReadback?.operator_stage,
formatCount,
isKnowledgeSourceBacked,
isKnowledgeDirectRecovered,
readbackStatus,
t,
total,
@@ -1485,27 +1502,31 @@ export default function KnowledgeBasePage({
</button>
</div>
{(entryFetchError || isKnowledgeListDegraded) && (
{showKnowledgeReadbackNotice && (
<div className="border-b border-status-warning/20 bg-white px-4 py-2">
<div className="flex flex-col gap-2 rounded-md border border-status-warning/20 bg-status-warning/5 px-3 py-2 xl:flex-row xl:items-center xl:justify-between">
<div className={cn('flex flex-col gap-2 rounded-md border px-3 py-2 xl:flex-row xl:items-center xl:justify-between', readbackNoticeTone)}>
<div className="grid min-w-0 flex-1 gap-2 xl:grid-cols-[minmax(220px,0.8fr)_minmax(0,2fr)] xl:items-center">
<div className="min-w-0">
<div className="flex items-center gap-2 text-xs font-label text-status-warning">
<div className={cn('flex items-center gap-2 text-xs font-label', readbackNoticeTitleTone)}>
<TriangleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">
{entryFetchError
? t('dataChain.errorTitle')
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedTitle')
: t('dataChain.degradedTitle')}
: isKnowledgeDirectRecovered
? t('dataChain.directRecoveredTitle')
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedTitle')
: t('dataChain.degradedTitle')}
</span>
</div>
<p className="mt-1 truncate text-[11px] font-body text-muted">
{entryFetchError
? t('dataChain.errorDescription', { reason: entryFetchError })
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedSummary')
: t('dataChain.degradedSummary')}
: isKnowledgeDirectRecovered
? t('dataChain.directRecoveredSummary')
: isKnowledgeSourceBacked
? t('dataChain.sourceBackedSummary')
: t('dataChain.degradedSummary')}
</p>
</div>
{!entryFetchError && (