Add three-stage cross-domain news research and reading briefings

Group original news into tagged fact packages, review each package with evidence and read-only research, and build complete 72-hour reading lists with independent model selections.

Add paused RSS/Atom subscriptions, news-scoped Skills, versioned analysis records, and locked deployment support for SearXNG. Preserve original news and keep live migration and legacy AI cleanup separately authorized.

Validate with 604 backend tests, 49 frontend tests, 19 CI control tests, lint, type checks, production build, and disposable PostgreSQL migration and three-stage smoke checks.
This commit is contained in:
eezd committed 2026-10-31 22:08:59 +08:00
1 parent ae35d3d608
commit 80d266dba7
44 files changed
+2682 -574

No files matched your search

+2
View File
@@ -43,3 +43,5 @@ export function subscribeCrawlerApi(signal: AbortSignal, version: number, onOpen
if (name === "log") onMessage(data)
})
}
export const createRssApi = (data: Crawler.RssInput) => request<{ code: string }>({ url: "/crawler/sources/rss", method: "post", data })
+3
View File
@@ -62,6 +62,7 @@ export interface CrawlLog {
}
export interface SourcePatch {
name?: string
enabled?: boolean
interval_seconds?: number
/** ISO 时间表示回补到该时间;null 表示取消回补 */
@@ -73,3 +74,5 @@ export type PatchSourceResponseData = null
export type ListLogsResponseData = CrawlLog[]
export type RestartCrawlerResponseData = null
export interface RssInput { name: string, url: string, interval_seconds: number }
+7 -4
View File
@@ -5,7 +5,10 @@ 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/events", method: "get", params })
export const getRiskEventApi = (id: number) => request<{ event: Risk.RiskEvent, history: Risk.Analysis[] }>({ url: `/risk/events/${id}`, method: "get" })
export const listRiskAnalysesApi = (params: { news_id?: number, revision_no?: number, failed?: boolean }, notify = true) => request<Risk.Analysis[]>({ url: "/risk/analyses", method: "get", params, notify })
export const retryRiskAnalysisApi = (id: number) => request<null>({ url: `/risk/analyses/${id}/retry`, method: "post" })
export const listRiskEventsApi = (params: Risk.EventQuery) => request<{ items: Risk.RiskEvent[], total: number }>({ url: "/risk/summaries", method: "get", params })
export const getRiskEventApi = (id: number) => request<{ 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 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 createNewsSkillTemplatesApi = () => request<{ grouping_skill_id: number, analysis_skill_id: number, outlook_skill_id: number }>({ url: "/risk/skill-templates", method: "post" })
+50 -7
View File
@@ -2,24 +2,55 @@ export interface RiskResult {
summary: string
reasoning: string
uncertainty: string
evidence: string
evidence?: string
direction: string
verification: string
confidence: string
stars: number
risk_stars: number
importance_stars: number
facts?: string
timeline?: string
asset_impacts?: string
opinions?: string
counter_evidence?: string
watchpoints?: string
claims?: { statement: string, verdict: string, evidence_refs: { article_id: number, quote: string }[] }[]
related_packages?: { id: number, relation: string, reason: string }[]
evidence_refs?: { article_id: number, quote: string }[]
}
export interface RiskEvent {
tags: string[]
content_kind: string
importance_stars: number
article_count: number
id: number
title: string
category: string
entities: string[]
stars: number
risk_stars: number
peak_stars: number
result: RiskResult
result: RiskResult | null
state: string
analyzed_at: string | null
revision: number
published_revision: number
error: string | null
published_at: string
updated_at: string
}
export interface Settings { enabled: boolean, provider_id: number | null, model: string, start_at: string }
export interface Settings {
enabled: boolean
provider_id: number | null
model: string
second_provider_id: number | null
second_model: string
third_provider_id: number | null
third_model: string
grouping_skill_id: number | null
analysis_skill_id: number | null
outlook_skill_id: number | null
start_at: string | null
}
export interface Analysis {
id: number
news_id: number
@@ -31,10 +62,22 @@ export interface Analysis {
analyzed_at: string | null
historical: boolean
}
export interface Reading { id: number, revision: number, priority: number, reason: string }
export interface Trend { summary: string, conditions: string, counter_evidence: string, watchpoints: string }
export interface Briefing {
reading: Reading[]
expansion: Reading[]
as_of: string
synthesis: { summary: string, market: string, short_term: Trend, medium_term: Trend } | null
inputs: { id: number, revision: number, category: string, tags: string[], result: RiskResult & { title: string } }[]
}
export interface Overview {
briefing: Briefing | null
stage2: Record<string, number>
stage3?: { state: string, pending: boolean, heartbeat: string | null }
as_of: string
counts: Record<string, number>
coverage: { total: number, done: number, failed: number, pending: number, processing: number }
coverage: { total: number, done: number, skipped: number, failed: number, pending: number, processing: number }
scenario: { direction: string, summary: string, conditions: string, counter_factors: string, uncertainty: string, event_ids: number[] } | null
snapshot_at: string | null
quotes: { at: string, BTCUSDT?: { lastPrice: string, priceChangePercent: string }, ETHUSDT?: { lastPrice: string, priceChangePercent: string } } | null
@@ -42,4 +85,4 @@ export interface Overview {
error: string | null
heartbeat: string | null
}
export interface EventQuery { hours?: number, stars?: number, q?: string, start?: string, end?: string, page?: number }
export interface EventQuery { tag?: string, importance?: number, hours?: number, stars?: number, q?: string, category?: string, asset?: string, start?: string, end?: string, page?: number }
+2 -1
View File
@@ -3,6 +3,7 @@ export type NodeKind = "folder" | "skill"
export interface SkillNode {
id: number
parent_id: number | null
scope: "shared" | "news"
kind: NodeKind
name: string
path: string
@@ -28,7 +29,7 @@ export type ListSkillTreeResponseData = SkillNode[]
export type CreateSkillNodeResponseData = SkillNode
export interface CreateSkillNodeBodyRequestData { parent_id: number | null, kind: NodeKind, name: string, priority?: number, content?: string }
export interface CreateSkillNodeBodyRequestData { parent_id: number | null, kind: NodeKind, name: string, priority?: number, scope?: "shared" | "news", content?: string }
export type PatchSkillNodeResponseData = SkillNode