新增: 父④F-09 streaming/currentText per-conv(accessor委派,真多会话前端基础)
DEC-07 a(父②后启动)。accessor 委派方案(非 3 独立 Map,禁动域零触碰):
- aiShared.ts: convStreamStates reactive Map<convId,{streaming,currentText}> + getConvStreamState/setConvStreaming/setConvCurrentText/clearConvStreamState
- stores/ai.ts: state.streaming/currentText 改 Object.defineProperty accessor,委派 convStreamStates 按 activeConversationId 索引(消费方零改动透明继承)
- useAiConversations.ts: deleteConversation 调 clearConvStreamState 防 Map 无限增长
铁律达成:
- 单会话回归零变化(activeConversationId 恒定 Map 仅1项,accessor 等价全局单例,13/13 测试)
- 禁动域零触碰(useAiSend/useAiEvents/streamingGuard/MessageList 等全零改动)
- 切会话隔离(BUG-260624-01 单例根因清除,A 后台 currentText 不串 B 视图)
- 内存泄漏防护(deleteConversation + streaming false 收敛删项)
vue-tsc EXIT 0。streamingGuard convId 精确路由留 F-09 后续批次(技术债点,单会话无回归)。
This commit is contained in:
@@ -170,3 +170,96 @@ export function setConvState(convId: string | null | undefined, s: ConvState): v
|
||||
convStates.set(convId, s)
|
||||
}
|
||||
}
|
||||
|
||||
// ── F-09 B 路线 per-conv 流式态(streaming/currentText) ──
|
||||
//
|
||||
// 背景(DEC-07a 父④):原 `state.streaming`(bool)/`state.currentText`(string)是 store 全局单例,
|
||||
// F-09 多会话并发下:A 后台生成中切到 B 会话,B 末条 AI 气泡命中 `isLastAi && streaming &&
|
||||
// currentText` 会渲染 A 残留 currentText(BUG-260624-01 同源根因)。本批改 per-conv Map,各会话
|
||||
// 独立累积流式文本/流式开关,切会话即切到该会话的 stream state,真并发不串话。
|
||||
//
|
||||
// 铁律:单会话回归零变化。per-conv Map 在单会话场景只有 activeConversationId 一项,等价原全局
|
||||
// 单例。state.streaming/currentText 改为委派本 Map 的 accessor(stores/ai.ts),所有现有消费方
|
||||
// (streamingGuard/useAiEvents/useAiSend/useAiConversations/useAiWindow/useAiPanel/MessageList/
|
||||
// ChatInput/TopBar/AiChat)读写 `store.state.streaming` / `store.state.currentText` 零改动透明继承。
|
||||
//
|
||||
// 下沉到 aiShared.ts(与 convStates 同位置)的原因:stores/ai.ts 已 import aiShared(getConvState),
|
||||
// 把 per-conv Map 放这里不新增依赖环;且与 convStates 同款 per-conv 模式集中管理,语义聚合。
|
||||
//
|
||||
// 类型:streaming=boolean(该会话是否正在流式输出),currentText=string(该会话累积的流式文本)。
|
||||
export interface ConvStreamState {
|
||||
streaming: boolean
|
||||
currentText: string
|
||||
}
|
||||
|
||||
/**
|
||||
* per-conv 流式态真相源:convId → {streaming, currentText}。
|
||||
*
|
||||
* 模块级 reactive Map(对齐 convStates 模式)。stores/ai.ts 的 state.streaming/currentText
|
||||
* accessor 委派本 Map 按 activeConversationId 索引读写,响应式依赖经 Vue reactive Map proxy 跟踪。
|
||||
*
|
||||
* 收敛:streaming=false 时删 Map 项(对齐 convStates idle 收敛,Map 不持陈旧 false 项);
|
||||
* 写 true 时按需 set。currentText 写空串等同收敛(下次写非空重新 set)。
|
||||
*/
|
||||
export const convStreamStates = reactive(new Map<string, ConvStreamState>())
|
||||
|
||||
/**
|
||||
* 取某 conv 的 stream state;未追踪过返回 null。
|
||||
*
|
||||
* 单会话场景:activeConversationId 恒定,Map 仅此一项,等价原全局单例。
|
||||
*/
|
||||
export function getConvStreamState(convId: string | null | undefined): ConvStreamState | null {
|
||||
if (!convId) return null
|
||||
return convStreamStates.get(convId) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某 conv 的 stream state,不存在则惰性创建(供 streaming/currentText 写入路径建项)。
|
||||
* 惰性建对齐 per-conv「写时建」语义——读路径(get)不建,仅写入路径(set)建。
|
||||
*/
|
||||
function ensureConvStreamState(convId: string): ConvStreamState {
|
||||
let s = convStreamStates.get(convId)
|
||||
if (!s) {
|
||||
s = { streaming: false, currentText: '' }
|
||||
convStreamStates.set(convId, s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* 写某 conv 的 streaming 态(false 时收敛删 Map 项,对齐 convStates idle 语义)。
|
||||
*
|
||||
* @param convId 目标会话 id
|
||||
* @param value streaming 目标值
|
||||
*/
|
||||
export function setConvStreaming(convId: string | null | undefined, value: boolean): void {
|
||||
if (!convId) return
|
||||
if (value) {
|
||||
ensureConvStreamState(convId).streaming = value
|
||||
} else {
|
||||
// false 收敛:若该 conv 已无任何活跃态(streaming=false 且 currentText 空),删 Map 项避免陈旧。
|
||||
const cur = convStreamStates.get(convId)
|
||||
if (cur) {
|
||||
cur.streaming = false
|
||||
if (!cur.currentText) convStreamStates.delete(convId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写某 conv 的 currentText(空串时仅清字段,streaming 仍 true 则保留项)。
|
||||
*
|
||||
* @param convId 目标会话 id
|
||||
* @param value currentText 目标值(空串=清流式累积,但会话可能仍在 streaming)
|
||||
*/
|
||||
export function setConvCurrentText(convId: string | null | undefined, value: string): void {
|
||||
if (!convId) return
|
||||
ensureConvStreamState(convId).currentText = value
|
||||
}
|
||||
|
||||
/** 清某 conv 的 stream state(会话删除/收尾彻底清,删整个 Map 项)。
|
||||
* 注:日常 streaming=false/currentText='' 收敛走 setConvStreaming 内联删,本函数用于会话级删除。 */
|
||||
export function clearConvStreamState(convId: string | null | undefined): void {
|
||||
if (!convId) return
|
||||
convStreamStates.delete(convId)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { state } from '@/stores/ai'
|
||||
import { notifyConversationChanged } from './useAiEvents'
|
||||
import { persistUiState } from './useAiPanel'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import { nextMsgId, getConvState } from './aiShared'
|
||||
import { nextMsgId, getConvState, clearConvStreamState } from './aiShared'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import type { AiMessage, AiToolCallInfo } from '@/api/types'
|
||||
|
||||
@@ -241,6 +241,9 @@ export async function switchConversation(id: string) {
|
||||
/** 删除会话;若删的是当前活跃会话则清空消息+移除活跃 id 持久化 */
|
||||
async function deleteConversation(id: string) {
|
||||
await aiApi.deleteConversation(id)
|
||||
// F-09 per-conv:清该会话的 stream state(streaming/currentText),防 convStreamStates Map 无限增长
|
||||
// (与 convStates/待审批等 per-conv 资源同款会话级清理语义)。
|
||||
clearConvStreamState(id)
|
||||
if (state.activeConversationId === id) {
|
||||
state.activeConversationId = null
|
||||
void appSettings.remove('df-ai-active-conv')
|
||||
|
||||
Reference in New Issue
Block a user