@@ -796,6 +796,16 @@ const sideQuestionControllers = new Map<string, AbortController>();
796796 */
797797export const __testing_messageQueues = messageQueues ;
798798
799+ /**
800+ * Export-only-for-tests: 把 isProcessingQueues / activeAbortControllers 暴露出来
801+ * 供 /stop 竞态 bug 的单测验证状态流转。生产代码不应使用这些出口。
802+ */
803+ export const __testing_isProcessingQueues = isProcessingQueues ;
804+ export const __testing_activeAbortControllers = activeAbortControllers ;
805+ export const __testing_decrementProcessingCount = decrementProcessingCount ;
806+ export const __testing_activeProcessingCount = { get : ( ) => activeProcessingCount } ;
807+ export const __testing_processingChatIds = processingChatIds ;
808+
799809/**
800810 * Detect a `/btw` side-question command in raw Feishu message text.
801811 * Returns the trimmed question (without the `/btw` prefix) if matched,
@@ -2033,6 +2043,26 @@ async function handleFeishuCommand(
20332043 controller . abort ( ) ;
20342044 if ( chatId ) {
20352045 activeAbortControllers . delete ( chatId ) ;
2046+ // FIX: /stop 必须同步递减处理计数,否则 UI 状态(如飞书卡片 footer)
2047+ // 仍显示「处理中」,且旧 finally 块的 decrementProcessingCount 会
2048+ // 误减新任务的计数
2049+ decrementProcessingCount ( chatId ) ;
2050+ // FIX: 清除该 chat 的队列处理状态,防止 /stop 后新消息被错误入队
2051+ // (/stop 只做了 abort + delete controller,遗漏了 isProcessingQueues 和
2052+ // messageQueues 的清理,导致竞态窗口期间新消息看到 isProcessing=true
2053+ // 被错误入队且无法自愈。详见 docs/bug-report-stop-queue-race-condition.md)
2054+ isProcessingQueues . set ( chatId , false ) ;
2055+ // FIX: 清空该 chat 的消息队列(拒绝任何排队中的消息)
2056+ // 注意:必须同时清空数组内容(splice(0)),否则旧 processMessageQueueForChat
2057+ // 的 while 循环仍持有数组引用,会继续处理已被 resolve(null) 的消息。
2058+ const pendingQueue = messageQueues . get ( chatId ) ;
2059+ if ( pendingQueue ) {
2060+ for ( const item of pendingQueue ) {
2061+ item . resolve ( null ) ;
2062+ }
2063+ pendingQueue . splice ( 0 ) ; // 清空数组内容,让旧 while 循环看到 length=0 后自然退出
2064+ messageQueues . delete ( chatId ) ;
2065+ }
20362066 } else {
20372067 activeAbortController = null ;
20382068 }
@@ -3954,6 +3984,11 @@ async function handleStart(context?: CommandContext): Promise<string> {
39543984 return null ;
39553985 } catch ( err : any ) {
39563986 if ( err . name === 'AbortError' || err . message ?. includes ( 'aborted' ) || err . message ?. includes ( 'cancelled' ) || err . message ?. includes ( 'canceled' ) ) {
3987+ // 注:不在此处清除 isProcessingQueues,因为 processMessageQueueForChat 的
3988+ // while 循环仍在运行。如果在 catch 中设置 isProcessingQueues=false,会破坏
3989+ // 防重入守卫,允许新消息并发进入同一 chat 的处理循环。
3990+ // isProcessingQueues 的清除统一由 processMessageQueueForChat 的 finally 负责,
3991+ // /stop 命令会提前设置 isProcessingQueues=false 来关闭竞态窗口。
39573992 if ( activeCardId && streaming ) {
39583993 const abortedFooterMetrics = await getFeishuStatusMetrics ( config , geminiClient , lastRequestTokenUsage ) ;
39593994 abortedFooterMetrics . status = '已中止' ;
@@ -3982,7 +4017,12 @@ async function handleStart(context?: CommandContext): Promise<string> {
39824017 emitFeishuMessageLog ( msg . chatId , `❌ ${ err . message } ` , 'tool' ) ;
39834018 return errorReply ;
39844019 } finally {
3985- activeAbortControllers . delete ( msg . chatId ) ;
4020+ // FIX: 只删除属于本次任务调用的控制器,避免误删新任务的控制器
4021+ // (旧 finally 块异步执行时,新任务可能已注册了同 chatId 的新控制器)
4022+ const currentController = activeAbortControllers . get ( msg . chatId ) ;
4023+ if ( currentController === abortController ) {
4024+ activeAbortControllers . delete ( msg . chatId ) ;
4025+ }
39864026 decrementProcessingCount ( msg . chatId ) ;
39874027
39884028 // 💾 持久化本会话的 AI 客户端历史(对齐 CLI useSessionAutoSave)。飞书无 React
@@ -4332,6 +4372,13 @@ async function handleStart(context?: CommandContext): Promise<string> {
43324372 }
43334373 }
43344374 } finally {
4375+ // 注:isProcessingQueues 的清除在此处完成。/stop 命令会提前设置
4376+ // isProcessingQueues=false 来关闭竞态窗口,此处再次设置 false 是无害的
4377+ // (false → false)。理论上存在旧 finally 清除新处理标志的极窄竞态窗口
4378+ // (/stop 设 false → 新消息 B 设 true → 旧 finally 设 false),但实际概率
4379+ // 极低:旧 finally 在 AbortError 传播后几乎立即执行(微秒级),而新消息 B
4380+ // 需要经过飞书消息接收、入队、processMessageQueueForChat 入口检查等异步步骤
4381+ // 才能设置 true,时间差远大于旧 finally 的执行窗口。
43354382 isProcessingQueues . set ( chatId , false ) ;
43364383 }
43374384 }
0 commit comments