Skip to content

Commit 75d2f26

Browse files
committed
fix(ai-queue): 离线队列消费互斥锁与卡死记录恢复
- processQueue 增加消费互斥锁,防止启动消费与 online 事件消费并发进入导致同一请求重复上报 - 启动时恢复卡死在 processing 状态的记录(回置 pending),防止应用中途退出造成数据静默丢失
1 parent 218f5a1 commit 75d2f26

1 file changed

Lines changed: 55 additions & 28 deletions

File tree

‎client/src/lib/ai/offlineAIQueue.ts‎

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,24 @@ async function processItem(item: AIQueueItem): Promise<boolean> {
187187
}
188188
}
189189

190+
/**
191+
* 恢复上次运行卡死在 processing 状态的记录
192+
*
193+
* @ai-context 应用退出/崩溃时正在消费的记录会停留在 processing,
194+
* 而 processQueue 只查 pending——不恢复则这些已向用户承诺“已排队”
195+
* 的请求永久静默丢失。启动时统一重置为 pending 重新参与消费。
196+
*/
197+
async function recoverStuckItems(): Promise<void> {
198+
try {
199+
await aiQueueDB.aiQueue.where('status').equals('processing').modify({ status: 'pending' });
200+
} catch {
201+
// IndexedDB 不可用时静默降级,不影响主流程
202+
}
203+
}
204+
205+
/** 消费互斥锁:防止启动消费与 online 事件消费并发进入导致同一请求重复上报 */
206+
let processingLock = false;
207+
190208
/**
191209
* 消费队列:按 FIFO 顺序串行处理所有待消费条目
192210
*
@@ -195,35 +213,40 @@ async function processItem(item: AIQueueItem): Promise<boolean> {
195213
* 处理完成后统一显示批量完成通知,避免多条 toast 连续弹出干扰用户。
196214
*/
197215
async function processQueue(): Promise<void> {
216+
if (processingLock) return;
198217
if (typeof navigator !== 'undefined' && !navigator.onLine) {
199218
return; // 仍然离线,跳过
200219
}
220+
processingLock = true;
221+
try {
222+
const now = Date.now();
223+
const pendingItems = await aiQueueDB.aiQueue
224+
.where('status')
225+
.equals('pending')
226+
.and((item) => item.nextRetryAt <= now)
227+
.sortBy('createdAt');
201228

202-
const now = Date.now();
203-
const pendingItems = await aiQueueDB.aiQueue
204-
.where('status')
205-
.equals('pending')
206-
.and((item) => item.nextRetryAt <= now)
207-
.sortBy('createdAt');
208-
209-
// UX 设计理由:统计本批次成功完成的数量,处理完后统一展示汇总 toast,
210-
// 避免逐条弹出造成视觉噪音(当积压多条请求时尤为明显)
211-
let completedCount = 0;
212-
for (const item of pendingItems) {
213-
const success = await processItem(item);
214-
if (success) completedCount++;
215-
// 每条处理完短暂间隔,避免过快连击
216-
await new Promise((resolve) => setTimeout(resolve, BASE_DELAY_MS));
217-
}
229+
// UX 设计理由:统计本批次成功完成的数量,处理完后统一展示汇总 toast,
230+
// 避免逐条弹出造成视觉噪音(当积压多条请求时尤为明显)
231+
let completedCount = 0;
232+
for (const item of pendingItems) {
233+
const success = await processItem(item);
234+
if (success) completedCount++;
235+
// 每条处理完短暂间隔,避免过快连击
236+
await new Promise((resolve) => setTimeout(resolve, BASE_DELAY_MS));
237+
}
218238

219-
// 批量完成汇总通知(仅当有成功完成的条目时才显示)
220-
if (completedCount > 0) {
221-
showToast(
222-
'success',
223-
completedCount === 1
224-
? '离线 AI 请求已完成'
225-
: `离线队列已完成 ${completedCount} 个请求`,
226-
);
239+
// 批量完成汇总通知(仅当有成功完成的条目时才显示)
240+
if (completedCount > 0) {
241+
showToast(
242+
'success',
243+
completedCount === 1
244+
? '离线 AI 请求已完成'
245+
: `离线队列已完成 ${completedCount} 个请求`,
246+
);
247+
}
248+
} finally {
249+
processingLock = false;
227250
}
228251
}
229252

@@ -259,6 +282,7 @@ let listenersAttached = false;
259282
* 启动队列监听(应用初始化时调用一次)
260283
*
261284
* - 注册 window online 事件监听
285+
* - 恢复上次卡死在 processing 的记录(防数据静默丢失)
262286
* - 应用启动时若在线则立即消费
263287
*/
264288
function startAutoProcess(): void {
@@ -267,10 +291,13 @@ function startAutoProcess(): void {
267291

268292
window.addEventListener('online', onNetworkRestored);
269293

270-
// 启动时若在线则立即消费
271-
if (typeof navigator !== 'undefined' && navigator.onLine) {
272-
void processQueue();
273-
}
294+
// 先恢复卡死记录再判断是否立即消费,确保 processing 残留不丢
295+
void recoverStuckItems().then(() => {
296+
// 启动时若在线则立即消费
297+
if (typeof navigator !== 'undefined' && navigator.onLine) {
298+
void processQueue();
299+
}
300+
});
274301
}
275302

276303
/**

0 commit comments

Comments
 (0)