优化: aichat 体验收尾(useAiEvents拆分4子域 + AIC-FIX P1队列/mutex/缓存 + i18n残留抽取)

This commit is contained in:
lxy
2026-08-08 20:38:33 +08:00
parent 4092a8d5bb
commit 65c0f6b2b6
21 changed files with 882 additions and 913 deletions
+13 -12
View File
@@ -23,8 +23,8 @@ import type { ContentPart, AiMessage, MentionSpan, MessageId } from '@/api/types
import { t } from '@/i18n/i18n-helpers'
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
import { setStreaming } from './streamingGuard'
import { startListener, flushCurrentText } from './useAiEvents'
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang, convStates, setConvCurrentText, setConvStreaming } from './aiShared'
import { startListener } from './useAiEvents'
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang, convStates, setConvCurrentText, setConvStreaming, flushCurrentText } from './aiShared'
/// 待发送队列上限(超过抛错提示用户)
const QUEUE_LIMIT = 10
@@ -33,18 +33,18 @@ const QUEUE_LIMIT = 10
const QUEUE_TIMEOUT_MS = 30_000
/**
* F-01 阶段6: 用户指定模型 override(主对话专用,模块级单例)。
* 用户指定模型 override(主对话专用,模块级单例)。
*
* null=自动模式(路由器选);非空字符串=用户从下拉指定的 model_id。
* 仅主对话(agentic)生效:doSend/sendMessage/regenerate/editMessage 透传给后端,
* 标题/扫描/灵感等内部调用不读此字段(后端各自独立 IPC 不带 override)。
* 标题/扫描/灵感等内部调用不读此字段。
*
* 后端兜底(agentic.rs):override 非空且在该 provider model_configs 池中才用,
* 否则落回路由结果——绝不让 override 导致无模型。
*
* 模块级(非组件级):AiChat.vue 顶部下拉读写此 ref,发送链路读此 ref 透传,
* 单实例 AiChat 不需 props/inject 串联。切换对话时后端清 session.model_override,
* 前端应同步清(modelOverride.value = null)保持 UI 与后端一致。
* 模块级 ref(非组件级):AiChat 顶部下拉读写,发送链路读此 ref 透传,单实例不需 props/inject。
* 切换会话后端会清自身 session.model_override;前端保留用户手选(每会话默认权重最高
* 由 TopBar 兜底:仅当 override 空/对目标模型池无效时才重置),避免切会话往返丢失手选模型。
*/
const modelOverride = ref<string | null>(null)
@@ -278,9 +278,8 @@ export function drainQueue(convId?: string | null) {
const next = state.queue.splice(idx, 1)[0]!
void (async () => {
try {
// 用目标会话 id 发送(而非当前活跃会话)——若已切到别的会话,原实现会把 A 的队首消息
// 误发到当前活跃会话 B。队列项无 mentionSpans 字段,传 undefined。
await sendMessage(next.text, next.skill, false, next.parts, undefined, targetId)
// 用目标会话 id 发送(而非当前活跃会话),避免切走后把 A 的队首消息误发到 B;spans 一并透传
await sendMessage(next.text, next.skill, false, next.parts, next.spans, targetId)
} catch (e) {
// 失败时彻底收尾:带目标会话 id 精准清理 per-conv 看门狗 + 会话状态
setConvStreaming(targetId, false)
@@ -352,6 +351,7 @@ async function sendMessage(text: string, skill?: string, forceMode = false, part
skill: skill || undefined,
enqueuedAt: Date.now(),
parts: parts && parts.length > 0 ? parts : undefined,
spans: spans && spans.length > 0 ? spans : undefined,
conversationId: targetConvId,
})
return
@@ -388,7 +388,7 @@ export async function tryForceSend(confirmFn: (msg: string) => Promise<boolean>,
// 此处无需前置 false(前置 false 会触发消息列表 watch 清流式块再重建,产生瞬态抖动)
clearStreamWatchdog()
try {
await sendMessage(first.text, first.skill, true, first.parts, undefined, first.conversationId)
await sendMessage(first.text, first.skill, true, first.parts, first.spans, first.conversationId)
return true
} catch (e) {
// force_send 也失败:回填该会话队首保消息不丢,复位 streaming 让 UI 可继续操作
@@ -398,6 +398,7 @@ export async function tryForceSend(confirmFn: (msg: string) => Promise<boolean>,
skill: first.skill,
enqueuedAt: first.enqueuedAt,
parts: first.parts,
spans: first.spans,
conversationId: first.conversationId,
})
setStreaming(false, { convId: targetConv ?? state.activeConversationId, reason: 'tryForceSend-fail' })
@@ -481,7 +482,7 @@ async function sendQueuedNow(index: number) {
// backendGenerating(后端真值仍 true)→ spliced 被入队而非立即发,与"立即发送"语义不符。
// force_send 原子复位 generating=false 再发,无竞态窗口;stop_flag 让旧 loop 在下个检测点退出。
// 显式传 spliced 所属会话 id(当前活跃视图内操作,即当前会话)。
await sendMessage(spliced.text, spliced.skill, true, spliced.parts, undefined, spliced.conversationId)
await sendMessage(spliced.text, spliced.skill, true, spliced.parts, spliced.spans, spliced.conversationId)
}
/** 停止当前生成:本地先复位 streaming,再发停止信号。