Files
knowledge-governance/web/src/qaUsage.ts
T
2026-10-18 21:50:29 +08:00

96 lines
3.0 KiB
TypeScript

import type { QAOrganizationVersion, QAQuery } from './qaOps'
import { getPayload, parseQAOrganizationVersion } from './qaOps'
export interface QAUsageRow {
key: string
questions: number
succeeded: number
failed: number
other: number
users: number | null
roster_people: number | null
}
export interface QAUsage {
organization: QAOrganizationVersion
as_of: string
start: string
end: string
keyword: string
questions: number
identified_users: number
rows: QAUsageRow[]
}
const bad = (): never => {
throw new Error('使用统计响应无效,请检查完整性及接口契约')
}
const obj = (v: unknown): Record<string, unknown> =>
v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : bad()
const text = (v: unknown) => (typeof v === 'string' ? v : bad())
const count = (v: unknown) => (typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 ? v : bad())
const stamp = (v: unknown) => {
const s = text(v)
return !Number.isNaN(Date.parse(s)) ? s : bad()
}
export const usageLabels: Record<string, string> = Object.assign(Object.create(null), {
国内工业: '国内工业',
APAC: 'APAC',
EMEA: 'EMEA',
America: 'America',
unassigned: '未配置区域',
cross_region: '跨区域待核对',
unmatched: '名册未匹配',
identity_conflict: '身份冲突',
no_identity: '身份信息缺失',
directory_missing: '尚无完整通讯录',
})
export function parseQAUsage(value: unknown): QAUsage {
const v = obj(value),
organization = parseQAOrganizationVersion(v.organization)
const allowed = new Set([
...(organization.region_names ?? []),
'unassigned',
'cross_region',
'unmatched',
'identity_conflict',
'no_identity',
'directory_missing',
])
if (!Array.isArray(v.rows)) return bad()
const rows = v.rows.map((raw) => {
const r = obj(raw)
const key = text(r.key)
if (!allowed.has(key)) return bad()
const row = {
key,
questions: count(r.questions),
succeeded: count(r.succeeded),
failed: count(r.failed),
other: count(r.other),
users: r.users === null ? null : count(r.users),
roster_people: r.roster_people === null ? null : count(r.roster_people),
}
if (row.questions !== row.succeeded + row.failed + row.other) return bad()
return row
})
const result = {
organization,
as_of: stamp(v.as_of),
start: stamp(v.start),
end: stamp(v.end),
keyword: text(v.keyword),
questions: count(v.questions),
identified_users: count(v.identified_users),
rows,
}
if (
result.questions !== rows.reduce((n, r) => n + r.questions, 0) ||
new Set(rows.map((r) => r.key)).size !== rows.length ||
!(new Date(result.start) < new Date(result.end))
)
return bad()
return result
}
export async function fetchQAUsage(query: QAQuery, signal?: AbortSignal): Promise<QAUsage> {
const params = new URLSearchParams({ start: query.start, end: query.end, keyword: query.keyword })
return parseQAUsage(await getPayload(`/api/v1/qa-ops/usage?${params}`, signal))
}