feat: organize news packages into verified event timelines
Add optional events, evidence-backed timelines, versioned package corrections and bounded historical reprocessing. Keep independent packages readable and isolate retries by failed stage. Configure reasoning effort per model stage and refresh default news Skill drafts without publishing.
This commit is contained in:
1 parent
2f1e07b7b1
commit
f1d060d475
28 files changed
+1983
-149
No files matched your search
@@ -6,9 +6,16 @@ export const saveRiskSettingsApi = (data: Risk.Settings) => request<null>({ url:
|
||||
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<{ 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 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 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" })
|
||||
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: Risk.ReprocessRange) => request<{ id: number }>({ url: "/risk/reprocess", method: "post", data })
|
||||
export const listReprocessApi = () => request<Risk.ReprocessJob[]>({ url: "/risk/reprocess", method: "get" })
|
||||
export const cancelReprocessApi = (id: number) => request<null>({ url: `/risk/reprocess/${id}/cancel`, method: "post" })
|
||||
export const retryReprocessApi = (id: number) => request<null>({ url: `/risk/reprocess/${id}/retry`, method: "post" })
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Effort } from "../settings/type"
|
||||
|
||||
export interface RiskResult {
|
||||
summary: string
|
||||
reasoning: string
|
||||
@@ -19,6 +21,8 @@ export interface RiskResult {
|
||||
evidence_refs?: { article_id: number, quote: string }[]
|
||||
}
|
||||
export interface RiskEvent {
|
||||
main_event_id?: number | null
|
||||
replacement_ids?: number[]
|
||||
tags: string[]
|
||||
content_kind: string
|
||||
importance_stars: number
|
||||
@@ -46,6 +50,9 @@ export interface Settings {
|
||||
second_model: string
|
||||
third_provider_id: number | null
|
||||
third_model: string
|
||||
reasoning_effort: Effort | null
|
||||
second_reasoning_effort: Effort | null
|
||||
third_reasoning_effort: Effort | null
|
||||
grouping_skill_id: number | null
|
||||
analysis_skill_id: number | null
|
||||
outlook_skill_id: number | null
|
||||
@@ -62,14 +69,14 @@ export interface Analysis {
|
||||
analyzed_at: string | null
|
||||
historical: boolean
|
||||
}
|
||||
export interface Reading { id: number, revision: number, priority: number, reason: string }
|
||||
export interface Reading { kind?: "package" | "event", 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 } }[]
|
||||
inputs: { kind?: "package" | "event", id: number, revision: number, category: string, tags: string[], result: RiskResult & { title: string } }[]
|
||||
}
|
||||
export interface Overview {
|
||||
briefing: Briefing | null
|
||||
@@ -86,3 +93,29 @@ export interface Overview {
|
||||
heartbeat: string | null
|
||||
}
|
||||
export interface EventQuery { tag?: string, importance?: number, hours?: number, stars?: number, q?: string, category?: string, asset?: string, start?: string, end?: string, page?: number }
|
||||
|
||||
export interface TimelinePoint {
|
||||
occurred_at: string | null
|
||||
time_precision: string
|
||||
time_text: string
|
||||
reported_at: string | null
|
||||
description: string
|
||||
verification: string
|
||||
package_id: number
|
||||
package_revision: number
|
||||
evidence_refs: { article_id: number, quote: string }[]
|
||||
}
|
||||
export interface NewsEvent {
|
||||
id: number
|
||||
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
|
||||
}
|
||||
export interface EventDetail extends NewsEvent {
|
||||
packages: RiskEvent[]
|
||||
versions: { id: number, revision: number, state: string, error: string | null, result: NewsEvent["result"] }[]
|
||||
}
|
||||
export interface ReprocessRange { start: string, end: string }
|
||||
export interface ReprocessPreview { articles: number, packages: number, events: number }
|
||||
export interface ReprocessJob { id: number, state: string, total: number, completed: number, failures: { stage: "grouping" | "analysis" | "events" | "outlook", id: number, error: string }[] }
|
||||
@@ -0,0 +1,209 @@
|
||||
<script setup lang="ts">
|
||||
import type * as Risk from "@@/apis/risk/type"
|
||||
import { formatTime } from "@@/utils/text"
|
||||
import * as api from "@/common/apis/risk"
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
|
||||
const emit = defineEmits<{ package: [id: number] }>()
|
||||
const user = useUserStore()
|
||||
const items = ref<Risk.NewsEvent[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const detail = ref<Risk.EventDetail | null>(null)
|
||||
const opened = ref(false)
|
||||
const detailError = ref(false)
|
||||
const detailId = ref(0)
|
||||
const range = ref<[Date, Date] | null>(null)
|
||||
const preview = ref<Risk.ReprocessPreview | null>(null)
|
||||
const jobs = ref<Risk.ReprocessJob[]>([])
|
||||
const busy = ref(false)
|
||||
const error = ref(false)
|
||||
let sequence = 0
|
||||
let detailSequence = 0
|
||||
let operation = 0
|
||||
let active = true
|
||||
async function load() {
|
||||
const seq = ++sequence
|
||||
const version = user.sessionVersion
|
||||
try {
|
||||
const [events, runs] = await Promise.all([api.listNewsEventsApi(page.value), api.listReprocessApi()])
|
||||
if (!active || seq !== sequence || version !== user.sessionVersion) return
|
||||
items.value = events.items
|
||||
total.value = events.total
|
||||
jobs.value = runs
|
||||
error.value = false
|
||||
} catch {
|
||||
if (active && seq === sequence && version === user.sessionVersion) error.value = true
|
||||
}
|
||||
}
|
||||
async function open(id: number) {
|
||||
const seq = ++detailSequence
|
||||
const version = user.sessionVersion
|
||||
detail.value = null
|
||||
detailError.value = false
|
||||
detailId.value = id
|
||||
opened.value = true
|
||||
try {
|
||||
const result = await api.getNewsEventApi(id)
|
||||
if (active && opened.value && seq === detailSequence && version === user.sessionVersion) detail.value = result
|
||||
} catch {
|
||||
if (active && seq === detailSequence && version === user.sessionVersion) detailError.value = true
|
||||
}
|
||||
}
|
||||
async function inspect() {
|
||||
if (!range.value || busy.value) return
|
||||
const version = user.sessionVersion
|
||||
const data = { start: range.value[0].toISOString(), end: range.value[1].toISOString() }
|
||||
const op = ++operation
|
||||
busy.value = true
|
||||
try {
|
||||
const result = await api.previewReprocessApi(data)
|
||||
if (active && version === user.sessionVersion && range.value && data.start === range.value[0].toISOString() && data.end === range.value[1].toISOString()) preview.value = result
|
||||
} finally {
|
||||
if (active && op === operation && version === user.sessionVersion) busy.value = false
|
||||
}
|
||||
}
|
||||
async function start() {
|
||||
if (!range.value || !preview.value || busy.value) return
|
||||
const version = user.sessionVersion
|
||||
const data = { start: range.value[0].toISOString(), end: range.value[1].toISOString() }
|
||||
await ElMessageBox.confirm(`重整理 ${preview.value.articles} 篇新闻将产生模型费用,关联包和事件也会更新。是否启动?`, "重整理")
|
||||
if (!active || version !== user.sessionVersion || busy.value) return
|
||||
const op = ++operation
|
||||
busy.value = true
|
||||
try {
|
||||
await api.startReprocessApi(data)
|
||||
if (active && version === user.sessionVersion) {
|
||||
preview.value = null
|
||||
await load()
|
||||
}
|
||||
} finally {
|
||||
if (active && op === operation && version === user.sessionVersion) busy.value = false
|
||||
}
|
||||
}
|
||||
async function action(id: number, retry: boolean) {
|
||||
const version = user.sessionVersion
|
||||
await (retry ? api.retryReprocessApi(id) : api.cancelReprocessApi(id))
|
||||
if (active && version === user.sessionVersion) await load()
|
||||
}
|
||||
watch(range, () => {
|
||||
preview.value = null
|
||||
})
|
||||
watch(page, load)
|
||||
watch(() => user.sessionVersion, () => {
|
||||
sequence++
|
||||
detailSequence++
|
||||
operation++
|
||||
busy.value = false
|
||||
detailError.value = false
|
||||
items.value = []
|
||||
jobs.value = []
|
||||
detail.value = null
|
||||
preview.value = null
|
||||
opened.value = false
|
||||
})
|
||||
watch(opened, (value) => {
|
||||
if (!value) detailSequence++
|
||||
})
|
||||
let timer: ReturnType<typeof setInterval>
|
||||
onMounted(() => {
|
||||
void load()
|
||||
timer = setInterval(() => {
|
||||
if (!document.hidden) void load()
|
||||
}, 15000)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
active = false
|
||||
sequence++
|
||||
detailSequence++
|
||||
clearInterval(timer)
|
||||
})
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
事件与时间线
|
||||
</template>
|
||||
<el-alert v-if="error" title="事件读取失败,可重试" type="warning" :closable="false" />
|
||||
<el-button link @click="load">
|
||||
刷新
|
||||
</el-button>
|
||||
<div v-for="event in items" :key="event.id" class="my-3">
|
||||
<el-button link @click="open(event.id)">
|
||||
{{ event.title }}
|
||||
</el-button>
|
||||
<p>{{ event.result?.summary || '正在整理组成包的时间线' }}</p>
|
||||
<p v-if="event.result">
|
||||
重要性 {{ event.result.importance_stars }} 星 · 风险 {{ event.result.risk_stars }}X
|
||||
</p>
|
||||
<p v-if="event.error">
|
||||
更新失败:{{ event.error }};保留上一成功版本
|
||||
</p>
|
||||
</div>
|
||||
<el-pagination v-model:current-page="page" :total="total" :page-size="20" layout="prev, pager, next" />
|
||||
<details class="mt-4">
|
||||
<summary>按时间范围重整理新闻</summary>
|
||||
<p>按新闻发布时间选择范围,使用现存最新修订。不会还原历史时点;共享包和事件可能同时更新。AI 暂停时任务等待。</p>
|
||||
<el-date-picker v-model="range" type="datetimerange" class="!max-w-full" start-placeholder="起始时间" end-placeholder="结束时间" />
|
||||
<el-button :loading="busy" @click="inspect">
|
||||
预览范围
|
||||
</el-button>
|
||||
<template v-if="preview">
|
||||
<p>{{ preview.articles }} 篇新闻 · {{ preview.packages }} 个包 · {{ preview.events }} 个事件</p>
|
||||
<el-button :loading="busy" @click="start">
|
||||
启动重整理
|
||||
</el-button>
|
||||
</template>
|
||||
<div v-for="job in jobs" :key="job.id" class="my-2">
|
||||
#{{ job.id }} · {{ job.state }} · {{ job.completed }}/{{ job.total }}
|
||||
<el-button v-if="['pending', 'running'].includes(job.state)" link @click="action(job.id, false)">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button v-if="job.state === 'failed'" link @click="action(job.id, true)">
|
||||
重试失败项
|
||||
</el-button>
|
||||
<p v-for="failure in job.failures" :key="`${failure.stage}:${failure.id}`">
|
||||
{{ ({ grouping: "新闻初筛", analysis: "包研判", events: "事件时间线", outlook: "阅读编排" })[failure.stage] }} #{{ failure.id }}:{{ failure.error }}
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</el-card>
|
||||
<el-drawer v-model="opened" title="事件发展时间线" size="min(760px, 100vw)">
|
||||
<el-alert v-if="detailError" title="事件详情读取失败" type="warning" :closable="false" />
|
||||
<el-button v-if="detailError" @click="open(detailId)">
|
||||
重试
|
||||
</el-button>
|
||||
<template v-if="detail">
|
||||
<h2>{{ detail.title }}</h2><p>{{ detail.result?.summary }}</p>
|
||||
<el-alert v-if="detail.error" :title="detail.error" type="warning" :closable="false" />
|
||||
<div v-for="(point, index) in detail.result?.timeline_points" :key="index" class="my-4 border-l-2 pl-3">
|
||||
<b>{{ point.time_text || '发生时间未知' }}</b> · {{ point.time_precision }} · {{ point.verification }}
|
||||
<p>{{ point.description }}</p><p>报道时间:{{ formatTime(point.reported_at) }}</p>
|
||||
<el-button link @click="emit('package', point.package_id)">
|
||||
包 #{{ point.package_id }} · v{{ point.package_revision }}
|
||||
</el-button>
|
||||
<blockquote v-for="(ref, n) in point.evidence_refs" :key="n">
|
||||
{{ ref.quote }}
|
||||
</blockquote>
|
||||
</div>
|
||||
<p>反证:{{ detail.result?.counter_evidence }}</p><p>不确定性:{{ detail.result?.uncertainty }}</p><p>意见:{{ detail.result?.opinions }}</p><p>关注:{{ detail.result?.watchpoints }}</p>
|
||||
<p>组成包</p>
|
||||
<div v-for="item in detail.packages" :key="item.id">
|
||||
<el-button link @click="emit('package', item.id)">
|
||||
{{ item.title }}
|
||||
</el-button>
|
||||
</div>
|
||||
<p>版本历史</p>
|
||||
<details v-for="version in detail.versions" :key="version.id" class="my-2">
|
||||
<summary>v{{ version.revision }} · {{ version.state }} {{ version.error }}</summary>
|
||||
<p>{{ version.result?.summary }}</p>
|
||||
<p v-for="(point, index) in version.result?.timeline_points" :key="index">
|
||||
{{ point.time_text }} · {{ point.description }} · 包 #{{ point.package_id }} v{{ point.package_revision }}
|
||||
</p>
|
||||
<p>{{ version.result?.uncertainty }}</p>
|
||||
</details>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
@@ -3,11 +3,19 @@ import type { SkillNode } from "@@/apis/skills/type"
|
||||
import type * as Risk from "@/common/apis/risk/type"
|
||||
import type { Provider } from "@/common/apis/settings/type"
|
||||
import { listSkillTreeApi } from "@@/apis/skills"
|
||||
import { EFFORTS } from "@@/constants/ai"
|
||||
import { formatTime } from "@@/utils/text"
|
||||
import * as api from "@/common/apis/risk"
|
||||
import { listProviderModelsApi, listProvidersApi } from "@/common/apis/settings"
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
|
||||
import EventResearch from "./components/EventResearch.vue"
|
||||
|
||||
const eventResearch = ref<InstanceType<typeof EventResearch> | null>(null)
|
||||
function openReading(row: Risk.Reading) {
|
||||
if (row.kind === "event") void eventResearch.value?.open(row.id)
|
||||
else void openEvent(row.id)
|
||||
}
|
||||
const user = useUserStore()
|
||||
const route = useRoute()
|
||||
const hours = ref(72)
|
||||
@@ -28,8 +36,8 @@ const thirdModels = ref<string[]>([])
|
||||
const thirdError = ref("")
|
||||
let thirdSequence = 0
|
||||
const readingRows = computed(() => overview.value?.briefing?.reading.slice((readingPage.value - 1) * 20, readingPage.value * 20) ?? [])
|
||||
function readingTitle(id: number) {
|
||||
return overview.value?.briefing?.inputs.find(p => p.id === id)?.result.title ?? `包 #${id}`
|
||||
function readingTitle(id: number, kind = "package") {
|
||||
return overview.value?.briefing?.inputs.find(p => p.id === id && (p.kind ?? "package") === kind)?.result.title ?? `包 #${id}`
|
||||
}
|
||||
const q = ref("")
|
||||
const range = ref<[Date, Date] | null>(null)
|
||||
@@ -141,13 +149,13 @@ async function loadThirdModels() {
|
||||
}
|
||||
async function createTemplates() {
|
||||
const version = user.sessionVersion
|
||||
const value = await api.createNewsSkillTemplatesApi()
|
||||
const value = await api.createNewsSkillTemplatesApi(true)
|
||||
if (version !== user.sessionVersion || !settings.value) return
|
||||
Object.assign(settings.value, value)
|
||||
const skills = await listSkillTreeApi()
|
||||
if (version !== user.sessionVersion) return
|
||||
newsSkills.value = skills.filter(s => s.scope === "news" && s.kind === "skill")
|
||||
ElMessage.success("已创建草稿,请前往 Skills 页面编辑并发布")
|
||||
ElMessage.success("默认新闻 Skills 已准备好,请前往 Skills 页面审阅并发布")
|
||||
}
|
||||
async function save(enable: boolean) {
|
||||
if (!settings.value) return
|
||||
@@ -268,7 +276,7 @@ onBeforeUnmount(() => {
|
||||
<div v-for="level in [5, 4, 3, 2, 1]" :key="level">
|
||||
<div class="text-amber-600">
|
||||
<span :title="levels[level]">{{ starText(level) }}</span>
|
||||
</div><strong class="text-2xl">{{ overview.counts[String(level)] ?? 0 }}</strong><span class="ml-1 text-sm text-slate-500">事件</span>
|
||||
</div><strong class="text-2xl">{{ overview.counts[String(level)] ?? 0 }}</strong><span class="ml-1 text-sm text-slate-500">包</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0 text-sm text-slate-500">
|
||||
@@ -276,7 +284,7 @@ onBeforeUnmount(() => {
|
||||
失败 {{ overview.coverage.failed }}
|
||||
</el-button>
|
||||
</p>
|
||||
<p>事件总结:完成 {{ overview.stage2.done ?? 0 }} · 待更新 {{ overview.stage2.pending ?? 0 }} · 处理中 {{ overview.stage2.processing ?? 0 }} · 失败 {{ overview.stage2.failed ?? 0 }}</p>
|
||||
<p>包研判:完成 {{ overview.stage2.done ?? 0 }} · 待更新 {{ overview.stage2.pending ?? 0 }} · 处理中 {{ overview.stage2.processing ?? 0 }} · 失败 {{ overview.stage2.failed ?? 0 }}</p>
|
||||
<p v-if="overview.stage3">
|
||||
阅读编排:{{ overview.stage3.state === 'processing' ? '处理中' : overview.stage3.state === 'failed' ? '失败' : overview.stage3.pending ? '等待聚合更新' : '已更新' }}
|
||||
</p>
|
||||
@@ -290,19 +298,19 @@ onBeforeUnmount(() => {
|
||||
近72小时完整阅读清单 · 按优先级排序
|
||||
</template>
|
||||
<template v-if="overview?.briefing">
|
||||
<p>生成于 {{ formatTime(overview.briefing.as_of) }} · 共 {{ overview.briefing.reading.length }} 个重要包</p>
|
||||
<p>生成于 {{ formatTime(overview.briefing.as_of) }} · 共 {{ overview.briefing.reading.length }} 项事件与独立包</p>
|
||||
<el-alert v-if="overview.stale" title="阅读稿可能已过期或更新失败,当前保留上次成功结果" type="warning" :closable="false" />
|
||||
<div v-for="(row, index) in readingRows" :key="row.id" class="my-3">
|
||||
<el-button link @click="openEvent(row.id)">
|
||||
{{ (readingPage - 1) * 20 + index + 1 }}. {{ readingTitle(row.id) }}
|
||||
<div v-for="(row, index) in readingRows" :key="`${row.kind}:${row.id}`" class="my-3">
|
||||
<el-button link @click="openReading(row)">
|
||||
{{ (readingPage - 1) * 20 + index + 1 }}. {{ readingTitle(row.id, row.kind) }}
|
||||
</el-button>
|
||||
<p>{{ row.reason }} · 优先级 {{ row.priority }} · 依据版本 {{ row.revision }}</p>
|
||||
</div>
|
||||
<el-pagination v-model:current-page="readingPage" :total="overview.briefing.reading.length" :page-size="20" layout="prev, pager, next, total" />
|
||||
<p><b>拓展阅读</b></p>
|
||||
<div v-for="row in overview.briefing.expansion" :key="row.id">
|
||||
<el-button link @click="openEvent(row.id)">
|
||||
{{ readingTitle(row.id) }}
|
||||
<div v-for="row in overview.briefing.expansion" :key="`${row.kind}:${row.id}`">
|
||||
<el-button link @click="openReading(row)">
|
||||
{{ readingTitle(row.id, row.kind) }}
|
||||
</el-button><p>{{ row.reason }}</p>
|
||||
</div>
|
||||
<template v-if="overview.briefing.synthesis">
|
||||
@@ -317,6 +325,7 @@ onBeforeUnmount(() => {
|
||||
<el-empty v-else description="暂无阅读稿,请配置三个阶段模型并启用" />
|
||||
<el-alert v-if="overview?.error" :title="overview.error" type="warning" :closable="false" />
|
||||
</el-card>
|
||||
<EventResearch ref="eventResearch" @package="openEvent" />
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<el-switch v-model="archive" active-text="历史归档" />
|
||||
<el-select v-model="importance" class="w-40">
|
||||
@@ -375,6 +384,11 @@ onBeforeUnmount(() => {
|
||||
接口未返回可选模型,可手动填写模型 ID。
|
||||
</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="第一阶段思考强度">
|
||||
<el-select v-model="settings.reasoning_effort" class="w-full" clearable :value-on-clear="null" placeholder="跟随接入配置">
|
||||
<el-option v-for="effort in EFFORTS" :key="effort" :value="effort" :label="effort" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="第二阶段接入配置">
|
||||
<el-select v-model="settings.second_provider_id" class="w-full">
|
||||
<el-option v-for="provider in providers" :key="provider.id" :value="provider.id" :label="provider.name" />
|
||||
@@ -391,6 +405,11 @@ onBeforeUnmount(() => {
|
||||
{{ secondModelsError }}
|
||||
</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="第二阶段思考强度">
|
||||
<el-select v-model="settings.second_reasoning_effort" class="w-full" clearable :value-on-clear="null" placeholder="跟随接入配置">
|
||||
<el-option v-for="effort in EFFORTS" :key="effort" :value="effort" :label="effort" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="第三阶段高级模型接入配置">
|
||||
<el-select v-model="settings.third_provider_id" class="w-full">
|
||||
<el-option v-for="provider in providers" :key="provider.id" :value="provider.id" :label="provider.name" />
|
||||
@@ -403,13 +422,21 @@ onBeforeUnmount(() => {
|
||||
刷新列表
|
||||
</el-button><p>{{ thirdError }}</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="第三阶段思考强度">
|
||||
<el-select v-model="settings.third_reasoning_effort" class="w-full" clearable :value-on-clear="null" placeholder="跟随接入配置">
|
||||
<el-option v-for="effort in EFFORTS" :key="effort" :value="effort" :label="effort" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<p class="text-sm text-slate-500">
|
||||
思考强度留空时跟随接入配置;接入配置也未设置时,由模型默认处理。不支持所选强度的模型会在测试启用时明确报错。
|
||||
</p>
|
||||
<el-form-item v-for="field in (['grouping_skill_id', 'analysis_skill_id', 'outlook_skill_id'] as const)" :key="field" :label="({ grouping_skill_id: '初筛归并 Skill', analysis_skill_id: '包核验 Skill', outlook_skill_id: '高级阅读编排 Skill' })[field]">
|
||||
<el-select v-model="settings[field]" class="w-full">
|
||||
<el-option v-for="skill in newsSkills" :key="skill.id" :value="skill.id" :label="`${skill.path} · ${skill.published_version_no ? `已发布 v${skill.published_version_no}` : '未发布'}`" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button @click="createTemplates">
|
||||
创建默认新闻 Skills 草稿
|
||||
生成/更新默认新闻 Skills 草稿
|
||||
</el-button>
|
||||
<router-link to="/skills">
|
||||
编辑与发布 Skills
|
||||
@@ -423,11 +450,30 @@ onBeforeUnmount(() => {
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<el-drawer v-model="showDetail" title="事件证据与评级历史" size="min(760px, 100vw)">
|
||||
<el-drawer v-model="showDetail" title="新闻包证据与评级历史" size="min(760px, 100vw)">
|
||||
<template v-if="detail">
|
||||
<el-button v-if="detail.event.main_event_id" link @click="eventResearch?.open(detail.event.main_event_id)">
|
||||
查看主事件时间线
|
||||
</el-button>
|
||||
<el-button v-for="id in detail.event.replacement_ids" :key="id" link @click="openEvent(id)">
|
||||
查看纠正后的包 #{{ id }}
|
||||
</el-button>
|
||||
<h2 class="text-lg">
|
||||
{{ detail.event.title }}
|
||||
</h2>
|
||||
<div v-for="link in detail.related_events" :key="link.id">
|
||||
<el-button link @click="eventResearch?.open(link.id)">
|
||||
关联事件 #{{ link.id }}
|
||||
</el-button> {{ link.reason }}
|
||||
</div>
|
||||
<details v-if="detail.corrections?.length">
|
||||
<summary>分包纠正历史</summary><pre v-for="item in detail.corrections" :key="item.id" class="whitespace-pre-wrap break-all">{{ item.result }}</pre>
|
||||
</details>
|
||||
<details v-if="detail.grouping_versions?.length">
|
||||
<summary>Luna 分包版本</summary><p v-for="item in detail.grouping_versions" :key="item.id">
|
||||
#{{ item.id }} · {{ item.state }} {{ item.error }}
|
||||
</p>
|
||||
</details>
|
||||
<template v-if="detail.event.result">
|
||||
<p>{{ detail.event.result.summary }}</p><p><b>事实:</b>{{ detail.event.result.facts }}</p><p><b>时间线:</b>{{ detail.event.result.timeline }}</p>
|
||||
<p><b>资产影响:</b>{{ detail.event.result.asset_impacts }}</p><p><b>相关意见:</b>{{ detail.event.result.opinions }}</p>
|
||||
@@ -471,7 +517,7 @@ onBeforeUnmount(() => {
|
||||
</el-drawer>
|
||||
<el-drawer v-model="showFailures" title="失败任务(最近100条)" size="min(700px, 100vw)">
|
||||
<el-select v-model="taskStage">
|
||||
<el-option label="文章初筛" value="grouping" /><el-option label="事件总结" value="analysis" /><el-option label="市场综述" value="outlook" />
|
||||
<el-option label="文章初筛" value="grouping" /><el-option label="包研判" value="analysis" /><el-option label="事件时间线" value="events" /><el-option label="市场综述" value="outlook" />
|
||||
</el-select>
|
||||
<div v-for="item in failures" :key="item.id" class="mb-4">
|
||||
<b>{{ item.input.title }}</b><p>{{ item.error }}</p><el-button @click="retry(item.id)">
|
||||
|
||||
Reference in new issue
Block a user