115 lines
5.7 KiB
TypeScript
115 lines
5.7 KiB
TypeScript
//! streaming 状态写收敛 guard(对齐 memory [[devflow-generating-statemachine]] 前端落地)。
|
|
//!
|
|
//! 背景:`state.streaming`(当前活跃视图的流式态)原本散布在 8+ 处直接赋值
|
|
//! (useAiSend/useAiEvents/useAiConversations/useAiWindow/useAiPanel/useAiStream),
|
|
//! 任一新增 return 漏写、或异常路径(前端 JS 错误/窗口崩溃恢复)跳过复位 →
|
|
//! streaming 永久 true 卡死输入框(stop 按钮常驻、新消息入队不发)。
|
|
//!
|
|
//! 本模块把写侧收敛到单一 `setStreaming` 入口,做两件事:
|
|
//! 1. 合法性观测:`true→true` / `false→false` 幂等场景记 debug 日志(不拒绝——
|
|
//! resetStreamWatchdog 等副作用场景需合法重入),异常模式(如 false 时仍在跑看门狗)
|
|
//! 记 warn 日志供排查卡死。
|
|
//! 2. feature flag `df-ai-generating-statemachine`:关时 guard 退化为直接赋值,
|
|
//! 不校验,回退原散布语义(灰度/回退开关)。
|
|
//!
|
|
//! 批4 双轨收口后:generating 态真相归 convStates(enum),本 guard 不再联动 generatingConvs
|
|
//! (bool 轨已退役)。streaming 单值(state.streaming)是全局当前视图流式态,另一条过渡态,
|
|
//! 归 F-09 B 路线 per-conv streaming 收口,本批暂留。doSend setStreaming(true,{convId}) 后,
|
|
//! 生成态由 useAiEvents handleEvent 活跃事件兜底写 setConvState('generating') 标记。
|
|
//!
|
|
//! 注:本 guard 是「写收敛 + 可观测」二合一,不是拒绝式状态机——
|
|
//! streaming 是 boolean 无真正非法跃迁,guard 价值在集中入口与日志而非拦截。
|
|
|
|
import { state } from '@/stores/ai'
|
|
import { convStates, getConvStreamState, setConvStreaming } from './aiShared'
|
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
|
|
|
/// feature flag key(appSettings KV)。关=回退散布语义(灰度/回退用)。
|
|
const STREAMING_GUARD_FLAG = 'df-ai-generating-statemachine'
|
|
|
|
const appSettings = useAppSettingsStore()
|
|
|
|
/// guard 是否启用(读 appSettings,默认开=true)。
|
|
///
|
|
/// 默认开的理由:本 guard 是收敛性改造(行为等价 + 加可观测),非行为变更,
|
|
/// 默认开启直接收效;flag 仅作紧急回退通道(若发现回归,用户可在设置里关掉回退原散布语义)。
|
|
function isGuardEnabled(): boolean {
|
|
return appSettings.get<boolean>(STREAMING_GUARD_FLAG, true)
|
|
}
|
|
|
|
/**
|
|
* streaming 写收敛入口 —— 替代散布的 `state.streaming = true/false`(原 14 处,现全收敛至此)。
|
|
* @param value 目标流式态
|
|
* @param opts.convId 关联会话 id(批4 收口后仅用于日志可观测,不再联动 generatingConvs;
|
|
* 生成态真相归 convStates,由 useAiEvents 兜底写 setConvState 标记)。
|
|
* @param opts.reason 调用方标注(日志可观测,如 'AiCompleted'/'stopChat'/'onStreamTimeout')。
|
|
*
|
|
* 行为:
|
|
* - flag 关 → 直接 `state.streaming = value`(回退散布语义,不校验)。
|
|
* - flag 开 → 幂等检测(value===当前值记 debug)、写日志。
|
|
*/
|
|
export function setStreaming(
|
|
value: boolean,
|
|
opts: { convId?: string | null; reason?: string } = {},
|
|
): void {
|
|
const { convId, reason } = opts
|
|
|
|
// flag 关:回退散布语义,直接赋值不动其他。
|
|
if (!isGuardEnabled()) {
|
|
if (convId) setConvStreaming(convId, value)
|
|
else state.streaming = value
|
|
return
|
|
}
|
|
|
|
const prev = convId ? (getConvStreamState(convId)?.streaming ?? false) : state.streaming
|
|
|
|
// 幂等观测:同值重复写不拒绝,仅记 debug 日志供排查卡死场景。
|
|
if (prev === value) {
|
|
console.debug(
|
|
`[AI-Guard] setStreaming 幂等(prev=${prev}, convId=${convId ?? 'none'}, reason=${reason ?? 'unknown'})`,
|
|
)
|
|
} else {
|
|
console.debug(
|
|
`[AI-Guard] setStreaming ${prev}→${value} (convId=${convId ?? 'none'}, reason=${reason ?? 'unknown'})`,
|
|
)
|
|
}
|
|
|
|
// 显式 convId 写目标会话 per-conv 流式态;缺省写当前活跃会话(单会话语义不变)。
|
|
if (convId) setConvStreaming(convId, value)
|
|
else state.streaming = value
|
|
}
|
|
|
|
/**
|
|
* 强制复位 streaming=false 的兜底入口(panic/卡死恢复用)。
|
|
*
|
|
* 与 setStreaming(false) 区别:本函数额外清 currentText + queue,
|
|
* 专用于看门狗超时 / 前端异常恢复等「必须彻底清态」场景。
|
|
*
|
|
* TD-260621-01 per-conv:看门狗超时携带 convId 时,经 setStreaming 仅复位该会话 streaming 态;
|
|
* 该会话的 convStates 项由各自会话收尾事件精确管理(on/off 后端均会 emit 收尾态)。
|
|
*
|
|
* 注:currentText/queue 仍是单例(F-09 域,本批不动),per-conv 超时在多会话并发下
|
|
* 会清当前累积的 currentText——但 currentText 仅活跃视图写入(useAiEvents handleEvent delta 累积),
|
|
* 而超时仅发生在该 conv 长时间无数据,此场景 currentText 多为空或已 flush,
|
|
* 实际误伤面远小于 generatingConvs 全清(队列 queue 的清理由 F-09 统一改,见 TD-260621-01 注释)。
|
|
*
|
|
* @param reason 兜底来源(日志可观测)
|
|
* @param convId 关联会话 id(看门狗 per-conv 超时传;省略=全局兜底)
|
|
*/
|
|
export function forceResetStreaming(reason: string, convId?: string): void {
|
|
console.warn(`[AI-Guard] forceResetStreaming(卡死兜底): ${reason} (convId=${convId ?? 'none'})`)
|
|
setStreaming(false, { convId: convId ?? null, reason })
|
|
state.currentText = ''
|
|
state.queue = []
|
|
// BUG-2026-07-17 根治:同时清理 convStates 项。
|
|
// 原实现仅清 streaming/currentText/queue,但按钮 stopBtnState 优先读 convStates
|
|
// (ChatInput.vue:243),convStates=generating 时仍显红色停止按钮。后端未发
|
|
// AiConvStateChanged 时(conv_state 永久卡住),必须前端主动清 convStates 才能解锁 UI。
|
|
if (convId) {
|
|
convStates.delete(convId)
|
|
} else {
|
|
// 全局兜底:无 convId 时清所有生成态(卡死恢复场景,精确性不重要)
|
|
convStates.clear()
|
|
}
|
|
}
|