@@ -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 */
197215async 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 */
264288function 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