00c4c13283
build / build (push) Successful in 1m19s
列表正文改为按需读取;保留完整导出、版本隔离和有效标签筛选。 验证:前端全量、Go全量及隔离PG全mirror竞态通过;覆盖首批回调、续载失败与筛选分页。
152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
import type { QARecordPage, QAQuery } from './qaOps'
|
||
import { qaCellValue, qaColumns } from './qaOps'
|
||
import type { QualitySummary, QualityRate } from './qaQuality'
|
||
import { qaDailySeries, qaCategorySeries } from './qaDashboard'
|
||
export function weeklyQualitySourceLabel(source: unknown): string {
|
||
return (
|
||
new Map([
|
||
['live-week', '当周实时质量指标'],
|
||
['frozen-week', '已冻结周指标'],
|
||
['reported-history', '历史上报指标'],
|
||
['master_live_calendar_month_v1', '自然月累计质量指标(跨月周按上月)'],
|
||
]).get(String(source)) ?? '指标来源未确认'
|
||
)
|
||
}
|
||
export interface QAWeeklyDraft {
|
||
markdown: string
|
||
csv: string
|
||
questions: number
|
||
revision: number
|
||
generatedAt: string
|
||
}
|
||
const stamp = (v: string) => new Date(v).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })
|
||
const rate = (v: QualityRate) =>
|
||
v.percent === null ? '—' : `${v.percent.toFixed(1)}%(${v.numerator} / ${v.denominator})`
|
||
const cell = (v: unknown) =>
|
||
String(v ?? '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/[\`*_[\]|]/g, '\\$&')
|
||
.replace(/[\r\n]+/g, ' ')
|
||
function csvCell(value: unknown) {
|
||
const v = String(value ?? '')
|
||
const safe = /^[\t\r\n]|^\s*[=+\-@]/.test(v) ? "'" + v : v
|
||
return '"' + safe.replace(/"/g, '""') + '"'
|
||
}
|
||
export function buildQAWeeklyCSV(page: QARecordPage): string {
|
||
if (page.items.some((row) => row.content_projection === 'summary'))
|
||
throw Error('列表仅为预览,请重新读取完整明细后导出')
|
||
if (page.has_more || page.total !== page.items.length || new Set(page.items.map((r) => r.id)).size !== page.total)
|
||
throw Error('明细未完整读取,未导出部分数据')
|
||
const csv =
|
||
'\ufeff' +
|
||
[
|
||
qaColumns.map((c) => csvCell(c.label)).join(','),
|
||
...page.items.map((row) => qaColumns.map((c) => csvCell(qaCellValue(row, c.key))).join(',')),
|
||
].join('\r\n')
|
||
return csv
|
||
}
|
||
|
||
export function buildQAWeeklyDraft(
|
||
page: QARecordPage,
|
||
query: QAQuery,
|
||
quality: QualitySummary,
|
||
generatedAt: string
|
||
): QAWeeklyDraft {
|
||
if (
|
||
page.has_more ||
|
||
page.items.length !== page.total ||
|
||
new Set(page.items.map((i) => i.id)).size !== page.total ||
|
||
quality.summary.total !== page.total ||
|
||
Date.parse(quality.start) !== Date.parse(query.start) ||
|
||
Date.parse(quality.end) !== Date.parse(query.end) ||
|
||
quality.keyword !== query.keyword ||
|
||
!Number.isFinite(Date.parse(generatedAt))
|
||
)
|
||
throw Error('周报范围或完整性不一致,请重新生成')
|
||
const groups: Record<string, number> = {
|
||
covered: 0,
|
||
uncovered: 0,
|
||
partial: 0,
|
||
pending: 0,
|
||
unassessed: 0,
|
||
excluded: 0,
|
||
stale: 0,
|
||
}
|
||
for (const r of page.items) {
|
||
const state =
|
||
r.quality?.state === 'stale' ? 'stale' : r.quality?.effective.coverage || r.coverage?.verdict || 'unassessed'
|
||
const key = state === 'unknown' ? 'unassessed' : state
|
||
if (!(key in groups)) throw Error('周报存在未知覆盖状态')
|
||
groups[key]++
|
||
}
|
||
for (const [key, count] of Object.entries(groups))
|
||
if (quality.summary.rates.breakdown[key as keyof typeof quality.summary.rates.breakdown] !== count)
|
||
throw Error('复核结果在生成期间发生变化,请重新生成')
|
||
const s = quality.summary
|
||
const daily = qaDailySeries(page.items, query)
|
||
.map((p) => `| ${p.label} | ${p.count} |`)
|
||
.join('\n')
|
||
const products = qaCategorySeries(page.items, 'product_category')
|
||
.map((p) => `| ${cell(p.label)} | ${p.count} |`)
|
||
.join('\n')
|
||
const gaps = page.items.filter(
|
||
(r) => (r.quality?.effective.coverage || r.coverage?.verdict) === 'uncovered' && r.quality?.state !== 'stale'
|
||
)
|
||
const md =
|
||
[
|
||
'# DobotAI 问答运营周报(草稿)',
|
||
`统计范围:${stamp(query.start)} 至 ${stamp(query.end)}(结束不含)`,
|
||
query.keyword ? `关键词:${cell(query.keyword)}` : '',
|
||
`生成:${stamp(generatedAt)}`,
|
||
`本地数据版本:${page.local_revision ?? '—'}`,
|
||
'## 问答概览',
|
||
`- 问题总量:${page.total}`,
|
||
`- 知识覆盖率:${rate(s.rates.coverage)}`,
|
||
`- 回答正确率:${rate(s.rates.correctness)}`,
|
||
`- 已人工复核:${s.reviewed}`,
|
||
`- 待重新核对:${s.stale}`,
|
||
`- 人工待确认:${s.pending_confirmation}`,
|
||
'## 质量分布',
|
||
'| 已覆盖 | 未覆盖 | 部分覆盖 | 待确认 | 未评估 | 不计入 | 待重新核对 |',
|
||
'| --- | --- | --- | --- | --- | --- | --- |',
|
||
`| ${groups.covered} | ${groups.uncovered} | ${groups.partial} | ${groups.pending} | ${groups.unassessed} | ${groups.excluded} | ${groups.stale} |`,
|
||
'## 每日提问',
|
||
'| 日期 | 问题量 |',
|
||
'| --- | ---: |',
|
||
daily,
|
||
'## 产品分类',
|
||
'| 分类 | 问题量 |',
|
||
'| --- | ---: |',
|
||
products,
|
||
'## 未覆盖问题',
|
||
gaps.length ? '| 提问时间 | 问题 | 产品分类 |' : '当前无有效未覆盖记录',
|
||
gaps.length ? '| --- | --- | --- |' : '',
|
||
...gaps.map(
|
||
(r) =>
|
||
`| ${stamp(r.asked_at)} | ${cell(qaCellValue(r, 'question_chinese') || r.question)} | ${cell(r.quality?.effective.product_category || r.product_category || '—')} |`
|
||
),
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n\n') + '\n'
|
||
const csv = buildQAWeeklyCSV(page)
|
||
return {
|
||
markdown: md.replace(/\|\n\n\|/g, '|\n|'),
|
||
csv,
|
||
questions: page.total,
|
||
revision: page.local_revision ?? 0,
|
||
generatedAt,
|
||
}
|
||
}
|
||
export function downloadQAWeekly(content: string, filename: string, type: string) {
|
||
const url = URL.createObjectURL(new Blob([content], { type }))
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = filename
|
||
document.body.append(a)
|
||
a.click()
|
||
a.remove()
|
||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||
}
|