feat: bound news AI concurrency with immediate task refill
This commit is contained in:
1 parent
f1d060d475
commit
cbd2618a20
15 files changed
+603
-129
No files matched your search
@@ -19,3 +19,5 @@ export const startReprocessApi = (data: Risk.ReprocessRange) => request<{ id: nu
|
||||
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" })
|
||||
|
||||
export const saveRiskConcurrencyApi = (max_concurrency: number) => request<{ max_concurrency: number }>({ url: "/risk/settings/concurrency", method: "patch", data: { max_concurrency } })
|
||||
@@ -43,6 +43,7 @@ export interface RiskEvent {
|
||||
updated_at: string
|
||||
}
|
||||
export interface Settings {
|
||||
max_concurrency: number
|
||||
enabled: boolean
|
||||
provider_id: number | null
|
||||
model: string
|
||||
@@ -79,6 +80,7 @@ export interface Briefing {
|
||||
inputs: { kind?: "package" | "event", id: number, revision: number, category: string, tags: string[], result: RiskResult & { title: string } }[]
|
||||
}
|
||||
export interface Overview {
|
||||
concurrency: { limit: number, running: number | null, queued: number, stages: Record<string, number>, updated_at: string | null }
|
||||
briefing: Briefing | null
|
||||
stage2: Record<string, number>
|
||||
stage3?: { state: string, pending: boolean, heartbeat: string | null }
|
||||
|
||||
@@ -59,6 +59,7 @@ const showFailures = ref(false)
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
const saving = ref(false)
|
||||
const concurrencySaving = ref(false)
|
||||
let sequence = 0
|
||||
let detailSequence = 0
|
||||
let modelsSequence = 0
|
||||
@@ -157,6 +158,19 @@ async function createTemplates() {
|
||||
newsSkills.value = skills.filter(s => s.scope === "news" && s.kind === "skill")
|
||||
ElMessage.success("默认新闻 Skills 已准备好,请前往 Skills 页面审阅并发布")
|
||||
}
|
||||
async function saveConcurrency() {
|
||||
if (!settings.value || concurrencySaving.value) return
|
||||
const version = user.sessionVersion
|
||||
concurrencySaving.value = true
|
||||
try {
|
||||
await api.saveRiskConcurrencyApi(settings.value.max_concurrency)
|
||||
if (version !== user.sessionVersion) return
|
||||
ElMessage.success("并发上限已保存;降低上限时等待在途任务完成")
|
||||
await load()
|
||||
} finally {
|
||||
if (version === user.sessionVersion) concurrencySaving.value = false
|
||||
}
|
||||
}
|
||||
async function save(enable: boolean) {
|
||||
if (!settings.value) return
|
||||
const version = user.sessionVersion
|
||||
@@ -207,6 +221,7 @@ watch(taskStage, () => {
|
||||
if (showFailures.value) void openFailures()
|
||||
})
|
||||
watch(() => user.sessionVersion, () => {
|
||||
concurrencySaving.value = false
|
||||
thirdSequence++
|
||||
secondModelsSequence++
|
||||
secondModels.value = []
|
||||
@@ -284,6 +299,10 @@ onBeforeUnmount(() => {
|
||||
失败 {{ overview.coverage.failed }}
|
||||
</el-button>
|
||||
</p>
|
||||
<p v-if="overview.concurrency">
|
||||
AI 并发:{{ overview.concurrency.running ?? "未知" }} / {{ overview.concurrency.limit }} · 排队 {{ overview.concurrency.queued }}
|
||||
<span v-for="(count, stage) in overview.concurrency.stages" :key="stage" class="ml-2">{{ ({ grouping: "分包", analysis: "研判", events: "事件", outlook: "阅读", history: "历史" } as Record<string, string>)[stage] }} {{ count }}</span>
|
||||
</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 ? '等待聚合更新' : '已更新' }}
|
||||
@@ -441,6 +460,13 @@ onBeforeUnmount(() => {
|
||||
<router-link to="/skills">
|
||||
编辑与发布 Skills
|
||||
</router-link>
|
||||
<el-form-item label="最大并发任务数">
|
||||
<el-input-number v-model="settings.max_concurrency" :min="1" :precision="0" :step="1" :disabled="concurrencySaving" />
|
||||
<el-button class="ml-2" :loading="concurrencySaving" @click="saveConcurrency">
|
||||
保存并发上限
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<p>所有新闻阶段共享名额,完成即补位;实时优先,历史使用空闲名额。测试并启用前请先暂停并等待在途任务完成。</p>
|
||||
<el-alert type="info" :closable="false" title="测试并启用会分别调用分包、中级研判和高级阅读编排模型。首次启用后仅处理新发布或源站修订的新闻;暂停后恢复沿用原起点。" />
|
||||
<p>当前状态:{{ settings.enabled ? '已启用' : '已关闭' }}</p>
|
||||
<el-button :loading="saving" @click="save(false)">
|
||||
|
||||
@@ -836,3 +836,49 @@ test("重整理预览的旧会话完成不能解除新操作的忙碌状态", as
|
||||
assert.equal(page.busy.value, false)
|
||||
assert.equal(page.preview.value.articles, 2)
|
||||
})
|
||||
|
||||
test("新闻并发独立保存保留模型草稿,重复点击和旧会话响应不影响新操作", async () => {
|
||||
const user = vue.reactive({ sessionVersion: 1 })
|
||||
const pending: ReturnType<typeof Promise.withResolvers>[] = []
|
||||
const values: number[] = []
|
||||
let reloads = 0
|
||||
const load = moduleLoader({
|
||||
"@@/apis/skills": { listSkillTreeApi: async () => [] },
|
||||
"@/common/apis/settings": {},
|
||||
"@/pinia/stores/user": { useUserStore: () => user },
|
||||
"@/common/apis/risk": {
|
||||
saveRiskConcurrencyApi: (value: number) => {
|
||||
values.push(value)
|
||||
const request = Promise.withResolvers()
|
||||
pending.push(request)
|
||||
return request.promise
|
||||
},
|
||||
getRiskOverviewApi: async () => {
|
||||
reloads++
|
||||
return { briefing: null }
|
||||
},
|
||||
listRiskEventsApi: async () => ({ items: [], total: 0 })
|
||||
}
|
||||
}, globals)
|
||||
const page = load("@/pages/home/index.vue").default.setup({}, { expose() {} })
|
||||
page.settings.value = { max_concurrency: 5, model: "unsaved-model" }
|
||||
const first = page.saveConcurrency()
|
||||
await page.saveConcurrency()
|
||||
assert.deepEqual(values, [5])
|
||||
pending[0].resolve({ max_concurrency: 5 })
|
||||
await first
|
||||
assert.equal(page.settings.value.model, "unsaved-model")
|
||||
assert.equal(reloads, 1)
|
||||
const old = page.saveConcurrency()
|
||||
user.sessionVersion++
|
||||
await vue.nextTick()
|
||||
page.settings.value = { max_concurrency: 2, model: "new-session" }
|
||||
const current = page.saveConcurrency()
|
||||
pending[1].resolve({ max_concurrency: 5 })
|
||||
await old
|
||||
assert.equal(page.concurrencySaving.value, true)
|
||||
assert.equal(page.settings.value.model, "new-session")
|
||||
pending[2].resolve({ max_concurrency: 2 })
|
||||
await current
|
||||
assert.equal(page.concurrencySaving.value, false)
|
||||
})
|
||||
Reference in new issue
Block a user