新增: 父④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:
@@ -39,7 +39,7 @@ graph TD
|
|||||||
P1["父① 小bug攒批<br/>✅①.2/①.3(①.1过时·①.4归⑤)"]:::done
|
P1["父① 小bug攒批<br/>✅①.2/①.3(①.1过时·①.4归⑤)"]:::done
|
||||||
P2["父② 知识图谱Phase1<br/>✅Phase1(数据层+业务层)"]:::done
|
P2["父② 知识图谱Phase1<br/>✅Phase1(数据层+业务层)"]:::done
|
||||||
P3["父③ AI对话体验<br/>📋待办"]:::todo
|
P3["父③ AI对话体验<br/>📋待办"]:::todo
|
||||||
P4["父④ F-09 per-conv<br/>📋待办"]:::todo
|
P4["父④ F-09 per-conv<br/>✅前端per-conv(accessor)"]:::done
|
||||||
P5["父⑤ 灵感模块<br/>✅完成(⑤.1/①.4/⑤.2)"]:::done
|
P5["父⑤ 灵感模块<br/>✅完成(⑤.1/①.4/⑤.2)"]:::done
|
||||||
P6["父⑥ Phase2-5<br/>🔨Phase2✅·3-5待办"]:::doing
|
P6["父⑥ Phase2-5<br/>🔨Phase2✅·3-5待办"]:::doing
|
||||||
P7["父⑦ 技术债<br/>📋待办"]:::todo
|
P7["父⑦ 技术债<br/>📋待办"]:::todo
|
||||||
@@ -57,7 +57,7 @@ graph TD
|
|||||||
| **父①** 小bug攒批 | ✅ 完成 | ①.2 白名单✅(settings.rs) / ①.3 priority✅(idea.rs) / ①.1 BUG层1❌过时(F-260619-03 方案①取代,层2待决策) / ①.4 雷达图→归父⑤ | — |
|
| **父①** 小bug攒批 | ✅ 完成 | ①.2 白名单✅(settings.rs) / ①.3 priority✅(idea.rs) / ①.1 BUG层1❌过时(F-260619-03 方案①取代,层2待决策) / ①.4 雷达图→归父⑤ | — |
|
||||||
| **父②** 知识图谱Phase1 | ✅ Phase1完成 | ②.1 V29迁移✅ / ②.2 TaskRecord+TaskLinkRepo✅ / ②.3 IPC(create_task扩展+task_link CRUD+move_queue+get_tree)✅ / ②.4 父聚合✅(set_status_for_aggregation绕status收口) / ②.5 AI工具6✅(基线38) | G1(弱) |
|
| **父②** 知识图谱Phase1 | ✅ Phase1完成 | ②.1 V29迁移✅ / ②.2 TaskRecord+TaskLinkRepo✅ / ②.3 IPC(create_task扩展+task_link CRUD+move_queue+get_tree)✅ / ②.4 父聚合✅(set_status_for_aggregation绕status收口) / ②.5 AI工具6✅(基线38) | G1(弱) |
|
||||||
| **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — |
|
| **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — |
|
||||||
| **父④** F-09 per-conv | 📋 待办 | ④.1 streaming/currentText per-conv | — |
|
| **父④** F-09 per-conv | ✅ 前端per-conv | ④.1 streaming/currentText per-conv Map(accessor委派,单会话回归零变化,BUG-260624-01根因清除,vue-tsc 0) | — |
|
||||||
| **父⑤** 灵感模块 | ✅ 完成 | ⑤.1 软删除✅ / ①.4 雷达图✅ / ⑤.2 #05✅/#06拆const✅/#09/#10表单(逗号tags)✅ / #07 DEC-02保留purge(不改) / 附:priority_from_i32跨层映射修复(对齐前端0=critical) | #07→②.1 |
|
| **父⑤** 灵感模块 | ✅ 完成 | ⑤.1 软删除✅ / ①.4 雷达图✅ / ⑤.2 #05✅/#06拆const✅/#09/#10表单(逗号tags)✅ / #07 DEC-02保留purge(不改) / 附:priority_from_i32跨层映射修复(对齐前端0=critical) | #07→②.1 |
|
||||||
| **父⑥** Phase2-5 | 🔨 Phase2✅ | ⑥.1事件流✅(V30 project_events+埋点9处+timeline IPC+AI工具) / ⑥.2基础设施待办 / ⑥.3注入(依赖父②+父④) / ⑥.4前端 | ⑥→父②, ⑥.3→父②+父④+G1 |
|
| **父⑥** Phase2-5 | 🔨 Phase2✅ | ⑥.1事件流✅(V30 project_events+埋点9处+timeline IPC+AI工具) / ⑥.2基础设施待办 / ⑥.3注入(依赖父②+父④) / ⑥.4前端 | ⑥→父②, ⑥.3→父②+父④+G1 |
|
||||||
| **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) |
|
| **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) |
|
||||||
|
|||||||
@@ -170,3 +170,96 @@ export function setConvState(convId: string | null | undefined, s: ConvState): v
|
|||||||
convStates.set(convId, s)
|
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 { notifyConversationChanged } from './useAiEvents'
|
||||||
import { persistUiState } from './useAiPanel'
|
import { persistUiState } from './useAiPanel'
|
||||||
import { setStreaming } from './streamingGuard'
|
import { setStreaming } from './streamingGuard'
|
||||||
import { nextMsgId, getConvState } from './aiShared'
|
import { nextMsgId, getConvState, clearConvStreamState } from './aiShared'
|
||||||
import { t } from '@/i18n/i18n-helpers'
|
import { t } from '@/i18n/i18n-helpers'
|
||||||
import type { AiMessage, AiToolCallInfo } from '@/api/types'
|
import type { AiMessage, AiToolCallInfo } from '@/api/types'
|
||||||
|
|
||||||
@@ -241,6 +241,9 @@ export async function switchConversation(id: string) {
|
|||||||
/** 删除会话;若删的是当前活跃会话则清空消息+移除活跃 id 持久化 */
|
/** 删除会话;若删的是当前活跃会话则清空消息+移除活跃 id 持久化 */
|
||||||
async function deleteConversation(id: string) {
|
async function deleteConversation(id: string) {
|
||||||
await aiApi.deleteConversation(id)
|
await aiApi.deleteConversation(id)
|
||||||
|
// F-09 per-conv:清该会话的 stream state(streaming/currentText),防 convStreamStates Map 无限增长
|
||||||
|
// (与 convStates/待审批等 per-conv 资源同款会话级清理语义)。
|
||||||
|
clearConvStreamState(id)
|
||||||
if (state.activeConversationId === id) {
|
if (state.activeConversationId === id) {
|
||||||
state.activeConversationId = null
|
state.activeConversationId = null
|
||||||
void appSettings.remove('df-ai-active-conv')
|
void appSettings.remove('df-ai-active-conv')
|
||||||
|
|||||||
118
src/stores/ai.ts
118
src/stores/ai.ts
@@ -51,7 +51,8 @@ const PARTS_SIZE_BUDGET = 50 * 1024 * 1024 // 50MB(base64 char 计)
|
|||||||
import { useAiEvents } from '@/composables/ai/useAiEvents'
|
import { useAiEvents } from '@/composables/ai/useAiEvents'
|
||||||
import { useAiStream } from '@/composables/ai/useAiStream'
|
import { useAiStream } from '@/composables/ai/useAiStream'
|
||||||
// 批4 双轨收口:getConvState 从 aiShared 取(下沉破环,避免 stores/ai↔useAiEvents 反向环)。
|
// 批4 双轨收口:getConvState 从 aiShared 取(下沉破环,避免 stores/ai↔useAiEvents 反向环)。
|
||||||
import { getConvState } from '@/composables/ai/aiShared'
|
// F-09 per-conv streaming/currentText(DEC-07a 父④):convStreamStates Map + 辅助函数亦从 aiShared 取。
|
||||||
|
import { getConvState, getConvStreamState, setConvStreaming, setConvCurrentText } from '@/composables/ai/aiShared'
|
||||||
import { useAiSend, initDrainQueueListener } from '@/composables/ai/useAiSend'
|
import { useAiSend, initDrainQueueListener } from '@/composables/ai/useAiSend'
|
||||||
import { useAiApproval } from '@/composables/ai/useAiApproval'
|
import { useAiApproval } from '@/composables/ai/useAiApproval'
|
||||||
import { useAiConversations } from '@/composables/ai/useAiConversations'
|
import { useAiConversations } from '@/composables/ai/useAiConversations'
|
||||||
@@ -60,22 +61,67 @@ import { useAiPanel } from '@/composables/ai/useAiPanel'
|
|||||||
import { useAiContext } from '@/composables/ai/useAiContext'
|
import { useAiContext } from '@/composables/ai/useAiContext'
|
||||||
|
|
||||||
/// 模块级单例 state — 全应用共享(composables 通过 `import { state } from '@/stores/ai'` 取用)
|
/// 模块级单例 state — 全应用共享(composables 通过 `import { state } from '@/stores/ai'` 取用)
|
||||||
export const state = reactive({
|
//
|
||||||
messages: [] as AiMessage[],
|
// F-09 B 路线 per-conv streaming/currentText(DEC-07a 父④):
|
||||||
streaming: false,
|
// `streaming`/`currentText` 不再是全局单例字段,改为 accessor 委派 aiShared.convStreamStates
|
||||||
currentText: '',
|
// (per-conv Map,定义于 composables/ai/aiShared.ts)按 activeConversationId 索引读写。
|
||||||
pendingApprovals: [] as AiToolCallInfo[],
|
//
|
||||||
|
// 单会话回归铁律:单会话场景 activeConversationId 恒定,Map 仅此一项,accessor 行为等价原全局
|
||||||
|
// 单例(读返回该项值,写改该项值)。所有现有消费方 `store.state.streaming` / `store.state.currentText`
|
||||||
|
// 零改动透明继承(read = get active 项,write = set active 项)。
|
||||||
|
//
|
||||||
|
// 多会话收益:切会话(activeConversationId 变)即切到该会话的 stream state,A 后台生成中切到 B,
|
||||||
|
// B 的 streaming/currentText 读各自 Map 项,A 残留 currentText 不串到 B 视图(BUG-260624-01 根因清除)。
|
||||||
|
//
|
||||||
|
// accessor 实现要点:
|
||||||
|
// - get:读 activeConversationId → 取 convStreamStates 项;无 active/无项 → streaming 返回 false、
|
||||||
|
// currentText 返回 ''(与原全局单例初值一致,空安全)。
|
||||||
|
// - set:读 activeConversationId → 写 convStreamStates 对应项;无 active 则 no-op(无目标 conv
|
||||||
|
// 无法路由,原全局单例此场景也写无意义的全局值,no-op 更干净)。
|
||||||
|
// - reactive 追踪:Vue 3 reactive 包装带 accessor 描述符的对象时,会保留 get/set 并经
|
||||||
|
// Reflect.get/set 调用,track/trigger 正常工作。`state.streaming` 读取建立对 activeConversationId
|
||||||
|
// + 对应 Map 项的响应式依赖,任一变化触发更新。
|
||||||
|
// - 闭包引用 state 本身:get/set 需读 state.activeConversationId,用下方 `stateRef` 闭包变量指向
|
||||||
|
// reactive 包装后的对象(先声明后赋值,accessor 闭包延迟读 stateRef,避免 TDZ)。
|
||||||
|
const _stateBase: {
|
||||||
|
messages: AiMessage[]
|
||||||
|
// F-09 per-conv:streaming/currentText 是 accessor(下方 Object.defineProperty 定义),
|
||||||
|
// 委派 aiShared.convStreamStates 按 activeConversationId 索引读写。streaming/currentText
|
||||||
|
// 不在字面量显式赋值(由 accessor 提供),故 _stateBase 类型不含此两键;
|
||||||
|
// 通过下方 StateWithStream 接口扩展导出 state 的完整类型(消费方编译可见)。
|
||||||
|
pendingApprovals: AiToolCallInfo[]
|
||||||
|
providers: AiProviderConfig[]
|
||||||
|
activeProvider: string | null
|
||||||
|
panelOpen: boolean
|
||||||
|
maximized: boolean
|
||||||
|
conversations: AiConversationSummary[]
|
||||||
|
activeConversationId: string | null
|
||||||
|
sidebarOpen: boolean
|
||||||
|
sidebarWidth: number
|
||||||
|
detached: boolean
|
||||||
|
docked: boolean
|
||||||
|
skills: SkillInfo[]
|
||||||
|
queue: { text: string; skill?: string; enqueuedAt: number; parts?: ContentPart[] }[]
|
||||||
|
agentRound: number
|
||||||
|
archivedCollapsed: boolean
|
||||||
|
foldedGroups: Record<string, boolean>
|
||||||
|
searchQuery: string
|
||||||
|
lastTokenUsage: { prompt: number; completion: number; total: number } | null
|
||||||
|
convTokenTotal: { prompt: number; completion: number; total: number } | null
|
||||||
|
} = {
|
||||||
|
messages: [],
|
||||||
|
pendingApprovals: [],
|
||||||
// 批4 双轨收口:generatingConvs(Set,bool 轨)已退役。会话生成态真相归 convStates
|
// 批4 双轨收口:generatingConvs(Set,bool 轨)已退役。会话生成态真相归 convStates
|
||||||
// (reactive Map,enum 轨),定义于 composables/ai/aiShared.ts(下沉破环)。
|
// (reactive Map,enum 轨),定义于 composables/ai/aiShared.ts(下沉破环)。
|
||||||
// 消费方经 isGenerating(convId) helper 或直接 getConvState(convId) 读,不再读 state.xxx。
|
// 消费方经 isGenerating(convId) helper 或直接 getConvState(convId) 读,不再读 state.xxx。
|
||||||
providers: [] as AiProviderConfig[],
|
providers: [],
|
||||||
activeProvider: null as string | null,
|
activeProvider: null,
|
||||||
panelOpen: true,
|
panelOpen: true,
|
||||||
// 面板模式:maximized=全宽占满main | sidebar=固定宽侧栏(可拖拽)
|
// 面板模式:maximized=全宽占满main | sidebar=固定宽侧栏(可拖拽)
|
||||||
maximized: true,
|
maximized: true,
|
||||||
// 对话管理
|
// 对话管理
|
||||||
conversations: [] as AiConversationSummary[],
|
conversations: [],
|
||||||
activeConversationId: null as string | null,
|
activeConversationId: null,
|
||||||
sidebarOpen: false,
|
sidebarOpen: false,
|
||||||
// 侧栏宽度(px,可拖拽;clamp 120~280,默认 200;持久化于 df-ai-ui。UX-2025-16)
|
// 侧栏宽度(px,可拖拽;clamp 120~280,默认 200;持久化于 df-ai-ui。UX-2025-16)
|
||||||
sidebarWidth: 200,
|
sidebarWidth: 200,
|
||||||
@@ -84,24 +130,66 @@ export const state = reactive({
|
|||||||
// 吸附跟随中
|
// 吸附跟随中
|
||||||
docked: false,
|
docked: false,
|
||||||
// 本机技能(`/` 联想)
|
// 本机技能(`/` 联想)
|
||||||
skills: [] as SkillInfo[],
|
skills: [],
|
||||||
// 待发送队列(生成中发的消息排队,AiCompleted 后自动续发;enqueuedAt 记录入队时间供超时检测)
|
// 待发送队列(生成中发的消息排队,AiCompleted 后自动续发;enqueuedAt 记录入队时间供超时检测)
|
||||||
// F-260614-05 Phase 2b: parts 挂多模态图片片(供 drainQueue/force 续发时本地 user 消息渲染图)
|
// F-260614-05 Phase 2b: parts 挂多模态图片片(供 drainQueue/force 续发时本地 user 消息渲染图)
|
||||||
queue: [] as { text: string; skill?: string; enqueuedAt: number; parts?: ContentPart[] }[],
|
queue: [],
|
||||||
// Agentic 循环当前轮次(0=非agentic/未开始;>0=收到后端 AiAgentRound 事件的 round)
|
// Agentic 循环当前轮次(0=非agentic/未开始;>0=收到后端 AiAgentRound 事件的 round)
|
||||||
// AE-2025-07:进度条用; AiCompleted/AiError 复位为 0
|
// AE-2025-07:进度条用; AiCompleted/AiError 复位为 0
|
||||||
agentRound: 0,
|
agentRound: 0,
|
||||||
// 归档分组折叠态(默认折叠)
|
// 归档分组折叠态(默认折叠)
|
||||||
archivedCollapsed: true,
|
archivedCollapsed: true,
|
||||||
// 时间分组折叠态(默认:更早折叠,今天/昨天展开;持久化进 df-ai-ui)
|
// 时间分组折叠态(默认:更早折叠,今天/昨天展开;持久化进 df-ai-ui)
|
||||||
foldedGroups: { today: false, yesterday: false, earlier: true } as Record<string, boolean>,
|
foldedGroups: { today: false, yesterday: false, earlier: true },
|
||||||
// 对话搜索关键字(UX-06 §3.1):非空时侧栏取消时间分组,平铺展示标题匹配的会话(updated_at DESC)。
|
// 对话搜索关键字(UX-06 §3.1):非空时侧栏取消时间分组,平铺展示标题匹配的会话(updated_at DESC)。
|
||||||
// 清空恢复原分组。消费方:Sidebar 渲染(读 searchQuery 决定走 filteredConversations 还是 groupedActive)。
|
// 清空恢复原分组。消费方:Sidebar 渲染(读 searchQuery 决定走 filteredConversations 还是 groupedActive)。
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
// token 用量展示(受 df-show-token-usage 开关控制;lastTokenUsage=最近一条回复,convTokenTotal=当前对话累计)
|
// token 用量展示(受 df-show-token-usage 开关控制;lastTokenUsage=最近一条回复,convTokenTotal=当前对话累计)
|
||||||
lastTokenUsage: null as { prompt: number; completion: number; total: number } | null,
|
lastTokenUsage: null,
|
||||||
convTokenTotal: null as { prompt: number; completion: number; total: number } | null,
|
convTokenTotal: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// F-09 per-conv streaming/currentText accessor 委派层:先声明 stateRef 占位,
|
||||||
|
// reactive 包装后回填,accessor 闭包经 stateRef 读 activeConversationId。
|
||||||
|
let stateRef: typeof _stateBase | null = null
|
||||||
|
|
||||||
|
// 在 _stateBase 上定义 streaming/currentText accessor(委派 convStreamStates per-conv Map)。
|
||||||
|
// 注:用 Object.defineProperty 而非字面量 getter,避免与上方类型声明冲突;reactive 会保留此描述符。
|
||||||
|
Object.defineProperty(_stateBase, 'streaming', {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
get(): boolean {
|
||||||
|
const s = stateRef
|
||||||
|
if (!s) return false
|
||||||
|
return getConvStreamState(s.activeConversationId)?.streaming ?? false
|
||||||
|
},
|
||||||
|
set(value: boolean) {
|
||||||
|
const s = stateRef
|
||||||
|
if (!s) return
|
||||||
|
setConvStreaming(s.activeConversationId, value)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
Object.defineProperty(_stateBase, 'currentText', {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
get(): string {
|
||||||
|
const s = stateRef
|
||||||
|
if (!s) return ''
|
||||||
|
return getConvStreamState(s.activeConversationId)?.currentText ?? ''
|
||||||
|
},
|
||||||
|
set(value: string) {
|
||||||
|
const s = stateRef
|
||||||
|
if (!s) return
|
||||||
|
setConvCurrentText(s.activeConversationId, value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const state = reactive(_stateBase) as typeof _stateBase & {
|
||||||
|
streaming: boolean
|
||||||
|
currentText: string
|
||||||
|
}
|
||||||
|
// 回填 stateRef:accessor 闭包读此引用(经 reactive 包装后的代理对象,activeConversationId 变化可追踪)。
|
||||||
|
stateRef = state
|
||||||
|
|
||||||
// 旁注:此处不再保留 type-only 导出(AiChatEvent 等),因组件直接从 api/types import。
|
// 旁注:此处不再保留 type-only 导出(AiChatEvent 等),因组件直接从 api/types import。
|
||||||
// 若有外部模块仍从本文件 import 这些类型,下方 re-export 兜底:
|
// 若有外部模块仍从本文件 import 这些类型,下方 re-export 兜底:
|
||||||
|
|||||||
Reference in New Issue
Block a user