新增: 后台任务卡徽章+轮次投影(convRounds), 可恢复分区(parked), reattachToMain i18n 补键(#12)

This commit is contained in:
lxy
2026-10-06 12:13:33 +08:00
parent 70d6e98f82
commit 04db8c0d3b
11 changed files with 250 additions and 32 deletions
+44 -7
View File
@@ -1,4 +1,4 @@
//! AI 事件监听与分发(分派器)— startListener / stopListener / handleEvent
//! AI 事件监听与分发(分派器)— startListener(租约)/ ensureListenerStarted(幂等确保)/ handleEvent
//!
//! 拆分(批次 A-B1):14 个事件 case 按域拆到 3 个叶子 handler:
//! - useAiStreamingEvents.handleStreamingEvent(流式)
@@ -16,7 +16,7 @@ import { t } from '@/i18n/i18n-helpers'
import { aiApi } from '@/api'
import { useAppSettingsStore } from '@/stores/appSettings'
import { state } from '@/stores/ai'
import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState, setConvCurrentText, setConvStreaming, clearConvStreamState, clearAllApprovalTimers, clearAllToolSlowTimers, flushCurrentText, friendlyError, notifyConversationChanged, clearLastDelta, discardConvPendingApprovals, addConvAgentFinished, clearConvAgentFinished, type AgentFinishedStatus } from './aiShared'
import { nextMsgId, setConvState, convStates, switchingConvs, getConvStreamState, setConvCurrentText, setConvStreaming, clearConvStreamState, clearAllApprovalTimers, clearAllToolSlowTimers, flushCurrentText, friendlyError, notifyConversationChanged, clearLastDelta, discardConvPendingApprovals, addConvAgentFinished, clearConvAgentFinished, setConvRound, clearConvRound, type AgentFinishedStatus } from './aiShared'
import { resetStreamWatchdog, clearAllStreamWatchdogs, clearResumeAttempts } from './useAiStream'
import { setStreaming } from './streamingGuard'
import { scheduleConversationsRefresh, activeConvKvKey } from './useAiConversations'
@@ -40,6 +40,9 @@ let _unlistenApprovalClear: (() => void) | null = null
// startListener 并发去重:防 onMounted 与 sendMessage 首发竞态下重复注册 listener
// (两回调写同一 state.currentText → 流式文字双倍)
let _startPromise: Promise<void> | null = null
// 租约计数(所有权模型,2026-10-05 根本修):仅组件挂载级 startListener 借/还,
// send 等运行时路径走 ensureListenerStarted 不计数——计数只由对称生命周期驱动,不漂移。
let _leaseCount = 0
const appSettings = useAppSettingsStore()
@@ -219,6 +222,8 @@ export function handleEvent(event: AiChatEvent) {
clearResumeAttempts(convId || undefined)
// 终态即清进度快照(事件即逝态,会话已终止不留陈旧「进行中」)
pendingTaskProgress.delete(convId || '')
// #14 桌面任务卡:终态即清轮次(任务卡据 convRounds 渲染在途项,终止不留陈旧项)
clearConvRound(convId || '')
// FE1:后台会话终止只刷新列表(防抖调度,避免与 AiCompleted 收尾的 notify 双拉)。
scheduleConversationsRefresh()
// 后台会话终止也触发队列续发/清队(完成续发;错误清该会话队列)
@@ -238,6 +243,10 @@ export function handleEvent(event: AiChatEvent) {
// 切换完成后由渲染层续显,避免切换窗口丢可见内容
setConvStreaming(convId, true)
setConvCurrentText(convId, (getConvStreamState(convId)?.currentText ?? '') + (event.delta ?? ''))
} else if (event.type === 'AiAgentRound' && convId && event.round > 0) {
// #14 桌面任务卡:后台会话轮次追踪(isCurrent=false 原本直接丢弃本事件,
// 任务卡需要后台轮次显示「进行中 N 轮」;取 max 防事件乱序回退)
setConvRound(convId, event.round)
}
return
}
@@ -260,8 +269,30 @@ export function handleEvent(event: AiChatEvent) {
void handleLifecycleEvent(event)
}
/** 启动事件监听(幂等 + 并发去重,onMounted 与 sendMessage 首发竞态不会重复注册) */
export async function startListener() {
/** 启动事件监听(幂等确保注册,无所有权——send 等运行时路径调用,不计入租约)。
* 所有权模型(2026-10-05 根本修):
* - ensureListenerStarted:仅保证 listener 存在,谁都可调、任意次,不持租约;
* - startListener:组件挂载用,返回 release 租约句柄,unmount 时调用归还;
* 最后一份租约归还才真拆 listener + 清全局计时器。
* 两者分离根治「send 裸调 start 无配对 stop → 裸计数只涨不跌 → 永不真拆」的结构性漂移:
* 计数只由组件挂载/卸载这一对对称生命周期驱动,不变量可静态审计。 */
export async function startListener(): Promise<() => void> {
await ensureListenerStarted()
_leaseCount++
let released = false
return () => {
if (released) {
// 双重归还是调用方 bug(组件卸载钩子重复执行等),dev 下即刻暴露防静默漂移
if (import.meta.env.DEV) console.error('[useAiEvents] listener 租约重复归还(调用方 bug)')
return
}
released = true
releaseLease()
}
}
/** 幂等确保 listener 已注册(并发去重;不持租约,不触发拆卸)。 */
export async function ensureListenerStarted(): Promise<void> {
if (_unlistenAiEvent && _unlistenConvChanged && _unlistenApprovalClear) {
return
}
@@ -304,8 +335,14 @@ export async function startListener() {
}
}
/** 停止事件监听(卸载时调用,释放后端 listener + 清看门狗/计时器,防回调改已卸载组件 state) */
export function stopListener() {
/** 归还一份租约:计数归零才真拆(多实例挂载时先退出的实例不误拆他人在用的 listener)。 */
function releaseLease() {
if (_leaseCount === 0) {
if (import.meta.env.DEV) console.error('[useAiEvents] 租约下溢(归还多于借出,调用方 bug)')
return
}
_leaseCount--
if (_leaseCount > 0) return
_unlistenAiEvent?.()
_unlistenConvChanged?.()
_unlistenApprovalClear?.()
@@ -323,7 +360,7 @@ export function stopListener() {
export function useAiEvents() {
return {
startListener,
stopListener,
ensureListenerStarted,
handleEvent,
flushCurrentText,
friendlyError,