mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-25 17:51:37 +08:00
fix(frontend): track multiple in-flight generation keys (regen vs preview)
Made-with: Cursor
This commit is contained in:
parent
4847003a05
commit
6f04326400
@ -5,29 +5,43 @@ const STORAGE_TS = 'ma_generation_inflight_ts'
|
|||||||
/** 含 LLM 的重新生成可能较久;超时后视为未进行,避免按钮永久禁用 */
|
/** 含 LLM 的重新生成可能较久;超时后视为未进行,避免按钮永久禁用 */
|
||||||
const TTL_MS = 45 * 60 * 1000
|
const TTL_MS = 45 * 60 * 1000
|
||||||
|
|
||||||
function readPersisted() {
|
/**
|
||||||
if (typeof sessionStorage === 'undefined') return null
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function readPersistedKeys() {
|
||||||
|
if (typeof sessionStorage === 'undefined') return []
|
||||||
try {
|
try {
|
||||||
const k = sessionStorage.getItem(STORAGE_KEY)
|
const raw = sessionStorage.getItem(STORAGE_KEY)
|
||||||
const ts = sessionStorage.getItem(STORAGE_TS)
|
const ts = sessionStorage.getItem(STORAGE_TS)
|
||||||
if (!k || ts == null) return null
|
if (!raw || ts == null) return []
|
||||||
const t = Number(ts)
|
const t = Number(ts)
|
||||||
if (!Number.isFinite(t) || Date.now() - t > TTL_MS) {
|
if (!Number.isFinite(t) || Date.now() - t > TTL_MS) {
|
||||||
sessionStorage.removeItem(STORAGE_KEY)
|
sessionStorage.removeItem(STORAGE_KEY)
|
||||||
sessionStorage.removeItem(STORAGE_TS)
|
sessionStorage.removeItem(STORAGE_TS)
|
||||||
return null
|
return []
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (Array.isArray(parsed)) return parsed.filter((x) => typeof x === 'string' && x)
|
||||||
|
if (typeof parsed === 'string') return [parsed]
|
||||||
|
return []
|
||||||
|
} catch {
|
||||||
|
/* 旧版:存的是 JSON 字符串化的单个 key,或非法 JSON 时按原字符串当作一个 key */
|
||||||
|
return raw ? [raw] : []
|
||||||
}
|
}
|
||||||
return k
|
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writePersisted(k) {
|
/**
|
||||||
|
* @param {string[]} keys
|
||||||
|
*/
|
||||||
|
function writePersistedKeys(keys) {
|
||||||
if (typeof sessionStorage === 'undefined') return
|
if (typeof sessionStorage === 'undefined') return
|
||||||
try {
|
try {
|
||||||
if (k) {
|
if (keys.length) {
|
||||||
sessionStorage.setItem(STORAGE_KEY, k)
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(keys))
|
||||||
sessionStorage.setItem(STORAGE_TS, String(Date.now()))
|
sessionStorage.setItem(STORAGE_TS, String(Date.now()))
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.removeItem(STORAGE_KEY)
|
sessionStorage.removeItem(STORAGE_KEY)
|
||||||
@ -38,29 +52,38 @@ function writePersisted(k) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 多个耗时请求可并行:例如「重新生成报告」未完成时又点了「报告预览」,不得互相覆盖。 */
|
||||||
* 长耗时生成类 POST/GET 的「进行中」标记。
|
const inFlightKeys = ref(readPersistedKeys())
|
||||||
* 除模块级 ref 外写入 sessionStorage,避免 Vite HMR / 整页刷新后丢失,
|
|
||||||
* 从而出现「切换页签后重新生成又可点」的假象。
|
|
||||||
*
|
|
||||||
* key 示例:`strategy-draft:12`、`regenerate-report:12`、`preview-report:12`
|
|
||||||
*/
|
|
||||||
const inFlightKey = ref(readPersisted())
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('storage', (e) => {
|
window.addEventListener('storage', (e) => {
|
||||||
if (e.key !== STORAGE_KEY && e.key !== STORAGE_TS) return
|
if (e.key !== STORAGE_KEY && e.key !== STORAGE_TS) return
|
||||||
inFlightKey.value = readPersisted()
|
inFlightKeys.value = readPersistedKeys()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前进行中的请求 key 列表(同一 key 不会重复)。
|
||||||
|
* key 示例:`strategy-draft:12`、`regenerate-report:12`、`preview-report:12`
|
||||||
|
*/
|
||||||
export function generationInFlightKey() {
|
export function generationInFlightKey() {
|
||||||
return inFlightKey
|
return inFlightKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInFlightKey(key) {
|
||||||
|
const cur = inFlightKeys.value
|
||||||
|
if (cur.includes(key)) return
|
||||||
|
inFlightKeys.value = [...cur, key]
|
||||||
|
writePersistedKeys(inFlightKeys.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeInFlightKey(key) {
|
||||||
|
inFlightKeys.value = inFlightKeys.value.filter((k) => k !== key)
|
||||||
|
writePersistedKeys(inFlightKeys.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换页签/隐藏标签时浏览器可能中止 fetch,表现为 TypeError 等;此时服务端可能仍在执行,
|
* 切换页签/隐藏标签时浏览器可能中止 fetch;服务端可能仍在执行,不应移除该 key。
|
||||||
* 不应清除「进行中」标记(否则按钮误恢复可点)。HTTP 4xx/5xx 仍由业务层 return,走正常清除。
|
|
||||||
*/
|
*/
|
||||||
function isAmbiguousClientFailure(err) {
|
function isAmbiguousClientFailure(err) {
|
||||||
if (err == null) return false
|
if (err == null) return false
|
||||||
@ -78,19 +101,14 @@ function isAmbiguousClientFailure(err) {
|
|||||||
* @returns {Promise<T>}
|
* @returns {Promise<T>}
|
||||||
*/
|
*/
|
||||||
export async function withGenerationInFlight(key, fn) {
|
export async function withGenerationInFlight(key, fn) {
|
||||||
inFlightKey.value = key
|
addInFlightKey(key)
|
||||||
writePersisted(key)
|
|
||||||
try {
|
try {
|
||||||
const out = await fn()
|
const out = await fn()
|
||||||
if (inFlightKey.value === key) {
|
removeInFlightKey(key)
|
||||||
inFlightKey.value = null
|
|
||||||
writePersisted(null)
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (inFlightKey.value === key && !isAmbiguousClientFailure(e)) {
|
if (!isAmbiguousClientFailure(e)) {
|
||||||
inFlightKey.value = null
|
removeInFlightKey(key)
|
||||||
writePersisted(null)
|
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,9 +17,10 @@ const regenErr = ref('')
|
|||||||
const genInFlight = generationInFlightKey()
|
const genInFlight = generationInFlightKey()
|
||||||
const REGEN_PREFIX = 'regenerate-report:'
|
const REGEN_PREFIX = 'regenerate-report:'
|
||||||
const regenPendingJobId = computed(() => {
|
const regenPendingJobId = computed(() => {
|
||||||
const k = genInFlight.value
|
for (const k of genInFlight.value) {
|
||||||
if (!k || !k.startsWith(REGEN_PREFIX)) return null
|
if (k.startsWith(REGEN_PREFIX)) return k.slice(REGEN_PREFIX.length)
|
||||||
return k.slice(REGEN_PREFIX.length)
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
const regenBusyThisTask = computed(
|
const regenBusyThisTask = computed(
|
||||||
() => regenPendingJobId.value != null && regenPendingJobId.value === selectedId.value,
|
() => regenPendingJobId.value != null && regenPendingJobId.value === selectedId.value,
|
||||||
|
|||||||
@ -47,19 +47,21 @@ const K_PACK = 'brief-pack:'
|
|||||||
function genKeyMatches(prefix) {
|
function genKeyMatches(prefix) {
|
||||||
const id = selectedId.value
|
const id = selectedId.value
|
||||||
if (!id) return false
|
if (!id) return false
|
||||||
return genInFlight.value === `${prefix}${id}`
|
return genInFlight.value.includes(`${prefix}${id}`)
|
||||||
}
|
}
|
||||||
const loading = computed(() => genKeyMatches(K_PREVIEW))
|
const loading = computed(() => genKeyMatches(K_PREVIEW))
|
||||||
const briefLoading = computed(() => genKeyMatches(K_BRIEF))
|
const briefLoading = computed(() => genKeyMatches(K_BRIEF))
|
||||||
const packLoading = computed(() => genKeyMatches(K_PACK))
|
const packLoading = computed(() => genKeyMatches(K_PACK))
|
||||||
const viewInFlightOtherJobId = computed(() => {
|
const viewInFlightOtherJobId = computed(() => {
|
||||||
const k = genInFlight.value
|
const sid = selectedId.value
|
||||||
if (!k) return null
|
if (!sid) return null
|
||||||
const i = k.lastIndexOf(':')
|
for (const k of genInFlight.value) {
|
||||||
if (i < 0) return null
|
const i = k.lastIndexOf(':')
|
||||||
const jid = k.slice(i + 1)
|
if (i < 0) continue
|
||||||
if (jid === selectedId.value) return null
|
const jid = k.slice(i + 1)
|
||||||
return jid
|
if (jid && jid !== sid) return jid
|
||||||
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const successJobs = computed(() =>
|
const successJobs = computed(() =>
|
||||||
|
|||||||
@ -17,10 +17,12 @@ const err = ref('')
|
|||||||
const genInFlight = generationInFlightKey()
|
const genInFlight = generationInFlightKey()
|
||||||
const STRATEGY_PREFIX = 'strategy-draft:'
|
const STRATEGY_PREFIX = 'strategy-draft:'
|
||||||
const strategyDraftPendingJobId = computed(() => {
|
const strategyDraftPendingJobId = computed(() => {
|
||||||
const k = genInFlight.value
|
for (const k of genInFlight.value) {
|
||||||
if (!k || !k.startsWith(STRATEGY_PREFIX)) return null
|
if (k.startsWith(STRATEGY_PREFIX)) return k.slice(STRATEGY_PREFIX.length)
|
||||||
return k.slice(STRATEGY_PREFIX.length)
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
const strategyGeneratingAny = computed(() => strategyDraftPendingJobId.value != null)
|
||||||
const strategyGeneratingThisTask = computed(
|
const strategyGeneratingThisTask = computed(
|
||||||
() =>
|
() =>
|
||||||
strategyDraftPendingJobId.value != null &&
|
strategyDraftPendingJobId.value != null &&
|
||||||
@ -192,7 +194,7 @@ watch(
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ma-btn ma-btn-primary"
|
class="ma-btn ma-btn-primary"
|
||||||
:disabled="!selectedId || strategyGeneratingThisTask"
|
:disabled="!selectedId || strategyGeneratingAny"
|
||||||
@click="generateAndGoPreview"
|
@click="generateAndGoPreview"
|
||||||
>
|
>
|
||||||
{{ strategyGeneratingThisTask ? '生成中…' : '生成并前往预览' }}
|
{{ strategyGeneratingThisTask ? '生成中…' : '生成并前往预览' }}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user