同一篇通稿被十个网站转载,页面若只数链接,会显示"10 个独立来源",可信指数也被虚高。新闻核验真正要统计的是相互独立的证据源,以及这些来源分别支持或反驳了哪条陈述。
新闻可信度看板已经展示可信指数、独立来源和待核事实,场景页也区分官方发布、现场媒体和社交平台。我将页面数据拆成陈述、来源和证据三层,使"来源数量"不再等于 URL 数量。
一条新闻可以包含多条待核陈述
整篇新闻只有一个真假值会丢失细节。同一报道中的时间可能已经证实,伤亡数字仍在变化,地点描述也可能有误。
ts
export type ClaimStatus = 'unverified' | 'supported' | 'disputed' | 'insufficient'
export type EvidenceStance = 'support' | 'contradict' | 'context'
export interface Claim {
id: string
text: string
status: ClaimStatus
updatedAt: number
}
export interface Source {
id: string
publisher: string
canonicalUrl: string
originId: string
publishedAt: number
sourceType: 'official' | 'media' | 'eyewitness' | 'social'
}
export interface Evidence {
id: string
claimId: string
sourceId: string
stance: EvidenceStance
excerpt: string
capturedAt: number
}
originId 表示内容最初来源。转载站点可以拥有不同 Source.id 和网址,但只要追溯到同一篇原稿,它们就在独立性统计中归为一组。
URL 先规范化,再判断转载关系
常见链接带有追踪参数和锚点。若不清理,同一页面就可能被当成多个来源。
ts
export function canonicalize(raw: string): string {
try {
const url = new URL(raw)
const removable = ['utm_source', 'utm_medium', 'utm_campaign', 'from', 'share']
removable.forEach((key: string) => url.searchParams.delete(key))
url.hash = ''
url.hostname = url.hostname.toLocaleLowerCase()
if (url.pathname.length > 1 && url.pathname.endsWith('/')) {
url.pathname = url.pathname.slice(0, -1)
}
return url.toString()
} catch (_) {
return raw.trim()
}
}
export function uniqueOrigins(sources: Source[]): Source[] {
const map = new Map<string, Source>()
sources.forEach((source: Source) => {
const key = source.originId.length > 0 ? source.originId :
canonicalize(source.canonicalUrl)
const previous = map.get(key)
if (!previous || source.publishedAt < previous.publishedAt) {
map.set(key, source)
}
})
return Array.from(map.values())
}
仅靠 URL 不能可靠识别所有转载。真实服务还需要内容指纹、引用链和人工确认;本地界面先允许录入 originId,把独立性规则放在模型中,而不是写死为域名数量。
每条陈述独立计算状态
来源可信度不能替代证据关系。官方来源可能只确认事件发生,没有确认传播最广的数字。状态计算只看与当前陈述关联的证据。
ts
export interface ClaimAssessment {
claimId: string
status: ClaimStatus
independentSupport: number
independentContradiction: number
}
export function assessClaim(
claim: Claim,
evidence: Evidence[],
sources: Source[]
): ClaimAssessment {
const sourceMap = new Map(sources.map((source: Source) =>
[source.id, source]))
const related = evidence.filter((item: Evidence) => item.claimId === claim.id)
const support = new Set<string>()
const contradiction = new Set<string>()
related.forEach((item: Evidence) => {
const source = sourceMap.get(item.sourceId)
if (!source || item.stance === 'context') return
const origin = source.originId || canonicalize(source.canonicalUrl)
if (item.stance === 'support') support.add(origin)
if (item.stance === 'contradict') contradiction.add(origin)
})
let status: ClaimStatus = 'unverified'
if (support.size >= 2 && contradiction.size === 0) status = 'supported'
else if (contradiction.size > 0) status = 'disputed'
else if (support.size === 1) status = 'insufficient'
return {
claimId: claim.id,
status,
independentSupport: support.size,
independentContradiction: contradiction.size
}
}
这里设定"两份独立支持且无反证"为本地演示规则,不代表新闻事实核验的通用结论。界面应把依据数量和冲突状态展示出来,让读者知道评分如何形成。
看板评分保留待核数量
总分是导航信息,不能覆盖明细。我根据已支持、存在争议和证据不足的陈述生成简单分数,同时返回待核数量。
ts
export interface BoardSummary {
score: number
independentSources: number
pendingClaims: number
assessments: ClaimAssessment[]
}
export function summarizeBoard(
claims: Claim[],
evidence: Evidence[],
sources: Source[]
): BoardSummary {
const assessments = claims.map((claim: Claim) =>
assessClaim(claim, evidence, sources))
const raw = assessments.reduce((sum: number, item: ClaimAssessment) => {
if (item.status === 'supported') return sum + 100
if (item.status === 'insufficient') return sum + 55
if (item.status === 'disputed') return sum + 25
return sum + 40
}, 0)
const score = assessments.length === 0 ? 0 :
Math.round(raw / assessments.length)
const pendingClaims = assessments.filter((item: ClaimAssessment) =>
item.status !== 'supported').length
return {
score,
independentSources: uniqueOrigins(sources).length,
pendingClaims,
assessments
}
}
页面上的"可信指数 82""独立来源 7""待核事实 3"就可以从同一份 BoardSummary 读取。点击待核数量时,列表过滤到 status !== 'supported',不用另外维护一套待办数组。
晚到证据不覆盖人工结论
证据抓取可能异步完成。自动评估结果到达时,如果用户已经人工标记陈述,需要保留人工版本。模型可以增加 decisionSource 和修订号。
ts
export interface ClaimDecision {
claimId: string
status: ClaimStatus
decisionSource: 'automatic' | 'manual'
revision: number
}
export function applyAutomaticDecision(
current: ClaimDecision,
next: ClaimDecision
): ClaimDecision {
if (current.decisionSource === 'manual') {
return current
}
return next.revision >= current.revision ? next : current
}
报告页可以继续使用环形综合评分,但"值得关注"区域应由争议项和待核项生成,而不是固定文案。

当前项目使用本地演示数据,不会自动判断现实新闻真伪。后续接入抓取或检索服务时,外部能力只负责产生来源与证据;来源归并、陈述状态、人工覆盖和页面选择仍留在本地业务层。即使一下子导入几十个链接,看板统计的也会是证据链,而不是链接数量。