feat(news): index news packages and research persistent events

Extract reusable literal keywords once per source revision, assign complete news packages to long-lived events, and research all event members with immutable evidence and references.

Add additive research storage, vocabulary snapshots, the two-stage workspace, isolated acceptance coverage, and a paused rollout with an initial fixed 50-news production trial.
This commit is contained in:
eezd committed 2026-11-04 12:09:58 +08:00
1 parent 07252fea0f
commit 4929a32a0a
69 files changed
+8215 -2092

No files matched your search

@@ -0,0 +1,20 @@
import type * as Research from "./type"
import { request } from "@/http/axios"
export const getSettingsApi = () => request<Research.Settings>({ url: "/risk/settings", method: "get" })
export const saveSettingsApi = (data: Research.Config) => request<null>({ url: "/risk/settings", method: "put", data })
export const enableApi = () => request<null>({ url: "/risk/enable", method: "post" })
export const pauseApi = () => request<null>({ url: "/risk/pause", method: "post" })
export const createSkillsApi = () => request<{ index: number, research: number }>({ url: "/risk/skill-templates", method: "post" })
export const listEventsApi = (q: string, page: number) => request<{ items: Research.Event[], total: number }>({ url: "/risk/events", method: "get", params: { q, page } })
export const getEventApi = (id: number) => request<Research.EventDetail>({ url: `/risk/events/${id}`, method: "get" })
export const retryEventApi = (id: number) => request<null>({ url: `/risk/events/${id}/retry`, method: "post" })
export const listPackagesApi = (q: string, page: number) => request<{ items: Research.Package[], total: number }>({ url: "/risk/packages", method: "get", params: { q, page } })
export const getPackageApi = (id: number) => request<Research.PackageDetail>({ url: `/risk/packages/${id}`, method: "get" })
export const retryPackageApi = (id: number) => request<null>({ url: `/risk/packages/${id}/retry`, method: "post" })
export const listNewsPackagesApi = (id: number, revision: number) => request<Research.Package[]>({ url: `/risk/news/${id}/packages`, method: "get", params: { revision_no: revision } })
export const enrollApi = (news_ids: number[]) => request<null>({ url: "/risk/packages", method: "post", data: { news_ids } })
export const getVocabularyApi = () => request<Research.Vocabulary>({ url: "/risk/tag-vocabulary", method: "get" })
export const saveVocabularyApi = (data: { base_revision: number, entries: Research.VocabularyEntry[] }) => request<Research.Vocabulary>({ url: "/risk/tag-vocabulary", method: "put", data })
export const getEventVersionApi = (event: number, version: number) => request<Research.VersionDetail>({ url: `/risk/events/${event}/versions/${version}`, method: "get" })
+14
View File
@@ -0,0 +1,14 @@
export interface Keyword { canonical: string, aliases: string[], quote: string }
export interface ModelConfig { provider_id: number | null, model: string, reasoning_effort: string | null, skill_id: number | null }
export interface Config { index: ModelConfig, research: ModelConfig, concurrency: number }
export interface Settings { config: Partial<Config>, enabled: boolean, start_at: string }
export interface Package { id: number, news_id: number, title: string, url: string, state: string, error: string | null, keywords: Keyword[], events: { id: number, title: string, reason: string, quote: string }[] }
export interface Point { time_precision: "minute" | "day" | "month" | "unknown", planned_precision: "minute" | "day" | "month" | "unknown", package_ids: number[], time_text: string, description: string, occurred_at: string | null, planned_at: string | null, reported_at: string | null, period_text: string, evidence_refs: { article_id: number, quote: string }[] }
export interface Result { title: string, summary: string, facts: string, reasoning: string, uncertainty: string, opinions: string, counter_evidence: string, watchpoints: string, asset_impacts: string, importance_stars: number, risk_stars: number, confidence: string, verification: string, timeline_points: Point[] }
export interface Event { id: number, title: string, state: string, revision: number, error: string | null, tags: string[], result: Result | null }
export interface EventDetail extends Event { packages: Package[], references: { event_id: number, title: string, version_id: number, role: string, reason: string, changed: boolean }[], versions: { id: number, revision: number, state: string, result: Result | null, error: string | null, usage: unknown, evidence: unknown }[] }
export interface PackageDetail extends Package { article: { title: string, summary: string, content: string }, attempts: { id: number, stage: string, state: string, error: string | null, output: unknown, usage: unknown }[] }
export interface VocabularyEntry { canonical: string, aliases: string[] }
export interface Vocabulary { revision: number, entries: VocabularyEntry[] }
export interface VersionDetail { evidence: unknown, usage: unknown, id: number, event_id: number, revision: number, state: string, result: Result, packages: { id: number, news_id: number }[], articles: { id: number, title: string, summary: string, content: string }[], references: EventDetail["references"] }
+3 -4
View File
@@ -5,14 +5,13 @@ export const getRiskSettingsApi = () => request<Risk.Settings>({ url: "/risk/set
export const saveRiskSettingsApi = (data: Risk.Settings) => request<null>({ url: "/risk/settings", method: "put", data })
export const enableRiskApi = (data: Risk.Settings) => request<null>({ url: "/risk/enable", method: "post", timeout: 650000, data })
export const getRiskOverviewApi = (hours: number) => request<Risk.Overview>({ url: "/risk/overview", method: "get", params: { hours } })
export const listRiskEventsApi = (params: Risk.EventQuery) => request<{ items: Risk.RiskEvent[], total: number }>({ url: "/risk/summaries", method: "get", params })
export const getRiskEventApi = (id: number) => request<{ related_events?: { id: number, reason: string }[], corrections?: { id: number, result: unknown }[], grouping_versions?: { id: number, state: string, error: string | null }[], evidence: { id: number, kind: string, data: unknown, fetched_at: string }[], event: Risk.RiskEvent, history: Risk.Analysis[], versions: { id: number, revision: number, state: string, model: string, result: Risk.RiskResult | null, error: string | null, analyzed_at: string | null }[] }>({ url: `/risk/summaries/${id}`, method: "get" })
export const listRiskEventsApi = (params: Risk.EventQuery) => request<{ items: Risk.NewsEvent[], total: number }>({ url: "/risk/events", method: "get", params })
export const getRiskPackageApi = (id: number) => request<{ corrections?: { id: number, result: unknown }[], grouping_versions?: { id: number, state: string, error: string | null }[], event: Risk.NewsPackage, history: Risk.Analysis[] }>({ url: `/risk/packages/${id}`, method: "get" })
export const listRiskAnalysesApi = (params: { news_id?: number, revision_no?: number, failed?: boolean, stage?: string }, notify = true) => request<Risk.Analysis[]>({ url: "/risk/tasks", method: "get", params, notify })
export const retryRiskAnalysisApi = (id: number, stage = "grouping") => request<null>({ url: `/risk/tasks/${id}/retry`, method: "post", params: { stage } })
export const listNewsSummariesApi = (newsId: number, revision: number) => request<Risk.RiskEvent[]>({ url: `/risk/news/${newsId}/summaries`, method: "get", params: { revision_no: revision } })
export const listNewsPackagesApi = (newsId: number, revision: number) => request<Risk.NewsPackage[]>({ url: `/risk/news/${newsId}/packages`, method: "get", params: { revision_no: revision } })
export const createNewsSkillTemplatesApi = (refreshExisting = false) => request<{ grouping_skill_id: number, analysis_skill_id: number, outlook_skill_id: number }>({ url: "/risk/skill-templates", method: "post", data: { refresh_existing: refreshExisting } })
export const listNewsEventsApi = (page = 1) => request<{ items: Risk.NewsEvent[], total: number }>({ url: "/risk/events", method: "get", params: { page } })
export const getNewsEventApi = (id: number) => request<Risk.EventDetail>({ url: `/risk/events/${id}`, method: "get" })
export const previewReprocessApi = (data: Risk.ReprocessRange) => request<Risk.ReprocessPreview>({ url: "/risk/reprocess/preview", method: "post", data })
export const startReprocessApi = (data: { preview_id: string, run_mode?: "shared" | "history_only" }) => request<{ id: number }>({ url: "/risk/reprocess", method: "post", data })
+16 -12
View File
@@ -20,24 +20,19 @@ export interface RiskResult {
related_packages?: { id: number, relation: string, reason: string }[]
evidence_refs?: { article_id: number, quote: string }[]
}
export interface RiskEvent {
main_event_id?: number | null
replacement_ids?: number[]
export interface NewsPackage {
main_event_id: number | null
replacement_ids: number[]
fact: string
tags: string[]
content_kind: string
importance_stars: number
article_count: number
id: number
title: string
category: string
entities: string[]
risk_stars: number
peak_stars: number
result: RiskResult | null
state: string
analyzed_at: string | null
revision: number
published_revision: number
error: string | null
published_at: string
updated_at: string
@@ -115,11 +110,20 @@ export interface NewsEvent {
title: string
state: string
error: string | null
result: { summary: string, importance_stars: number, risk_stars: number, counter_evidence: string, timeline_points: TimelinePoint[], uncertainty: string, opinions: string, watchpoints: string } | null
revision: number
published_revision: number
updated_at: string
tags: string[]
category: string
package_count: number
risk_stars: number
importance_stars: number
result: (RiskResult & { timeline_points: TimelinePoint[] }) | null
}
export interface EventDetail extends NewsEvent {
packages: RiskEvent[]
versions: { id: number, revision: number, state: string, error: string | null, result: NewsEvent["result"] }[]
packages: NewsPackage[]
evidence: { id: number, version_id: number, data: unknown, fetched_at: string }[]
versions: { id: number, revision: number, state: string, error: string | null, result: NewsEvent["result"], usage: unknown }[]
}
export interface ReprocessRange { selection?: "range" | "attempted", start?: string, end?: string, modes: ("missing" | "review")[] }
export interface ReprocessPreview { articles: number, packages: number, events: number, total: number, missing: number, review: number, preview_id: string, models: Record<string, { model: string, reasoning_effort: string | null }> }