优化: 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
+126
View File
@@ -11,11 +11,26 @@
//! events 模块触发(start/clear/allClear),send 模块也需导出给组件,故下沉到共享层
import { reactive } from 'vue'
import { emit } from '@tauri-apps/api/event'
import { aiApi } from '@/api'
import { useAppSettingsStore } from '@/stores/appSettings'
import { t } from '@/i18n/i18n-helpers'
import type { AiMessage, AiToolCallInfo, ConvState, MessageId } from '@/api/types'
/** 通知会话列表刷新(newConversation/deleteConversation/rename 等触发侧栏更新) */
export function notifyConversationChanged(): void {
emit('ai-conversation-changed', {})
}
/** 后端原始错误转用户友好提示 */
export function friendlyError(raw: string): string {
if (/404|not\s*found/i.test(raw)) return t('ai.errorNotFound')
if (/401|403|unauthorized|api[_\s-]?key/i.test(raw)) return t('ai.errorAuth')
if (/timeout|超时/i.test(raw)) return t('ai.errorTimeout')
if (/network|connection|ECONN|网络|连接/i.test(raw)) return t('ai.errorNetwork')
return raw
}
/**
* 解析 AI 回复语言(useAiSend.sendMessage / useAiContext.compressContext 共用)。
@@ -329,3 +344,114 @@ export function clearConvStreamState(convId: string | null | undefined): void {
//
// 普通 Set(仅事件处理器读,无渲染追踪需求);生命周期由 switchConversation 的 finally 管理。
export const switchingConvs = new Set<string>()
// ── 流式文本回填 + delta 去重(useAiEvents 拆分后跨域共享) ──
//
// flushCurrentText 被 streaming(新轮)/lifecycle(收尾)/send(发送前)三域共用,且需写
// _lastDeltas 去重 Map,故下沉到本共享层。为规避 aiShared ↔ stores/ai 顶层循环引用(TDZ),
// 沿用 __bindMessages 的 getter 注入模式:stores/ai 在 state 就绪后注入 getter,
// 本模块函数运行时惰性取最新 state。
export interface AiStateShape {
currentText: string
messages: AiMessage[]
activeConversationId: string | null
}
let _stateGetter: (() => AiStateShape) | null = null
/** @internal 由 stores/ai.ts 在 state 创建后注入 getter(flushCurrentText 惰性读 state) */
export function __bindState(getter: () => AiStateShape): void {
_stateGetter = getter
}
function getState(): AiStateShape | null {
return _stateGetter?.() ?? null
}
/** per-conv delta 去重 Map(convId → 上一次 delta 内容,根治跨会话撞值误丢) */
const _lastDeltas = new Map<string, string>()
function lastDeltaKey(convId: string | null | undefined): string {
return convId || getState()?.activeConversationId || ''
}
/** 读某会话上一次 delta(去重检查);无记录返回 undefined */
export function getLastDelta(convId: string | null | undefined): string | undefined {
return _lastDeltas.get(lastDeltaKey(convId))
}
/** 记录某会话当前 delta */
export function setLastDelta(convId: string | null | undefined, delta: string): void {
_lastDeltas.set(lastDeltaKey(convId), delta)
}
/** 删除某会话的 delta 记录(会话删除/收尾时调用,防 Map 无限增长) */
export function clearLastDelta(convId: string | null | undefined): void {
_lastDeltas.delete(lastDeltaKey(convId))
}
/**
* 把流式累积的 currentText 回填到最后一条 assistant 消息(AiAgentRound/AiCompleted/AiError 收尾共用)。
*
* 只 guard 空串会漏 whitespace —— LLM 工具调用前常推 `\n`/空格,累积成 whitespace-only 后写进
* 占位 content 会渲染带边框空气泡,故 trim 兜底空白不回填。回填后立即自清 currentText,
* 消除对调用方清空顺序的依赖(防残留文本渲到新气泡)。
*/
export function flushCurrentText(): void {
const s = getState()
if (!s) return
const resetDelta = () => clearLastDelta(s.activeConversationId)
if (!s.currentText || !s.currentText.trim()) {
s.currentText = ''
resetDelta()
return
}
// 从末尾向前找最后一个非 isError assistant 气泡写入(跳过重试错误气泡,保留部分回复)
for (let i = s.messages.length - 1; i >= 0; i--) {
const m = s.messages[i]
if (m.role !== 'assistant') break
if (!m.isError) {
m.content = s.currentText
break
}
}
s.currentText = ''
resetDelta()
}
// ── 工具慢执行计时器(useAiEvents 拆分后跨域共享:tool 域 + lifecycle 收尾共用) ──
//
// 工具执行超时提示(纯前端降级,后端无工具级取消 IPC):每个 running 工具一个独立 setTimeout,
// 到时若仍未收到 Completed/Approval,经 Tauri 事件 ai-tool-slow-toast 弹 warning toast(仅提示一次,
// 不动 running 态——慢工具如 read_file 大文件/run_workflow 长任务不可误杀)。
const TOOL_SLOW_MS = 30000
const _toolTimers = new Map<string, ReturnType<typeof setTimeout>>()
const _toolSlowNotified = new Set<string>()
/** 启动工具慢执行计时器(幂等:同 id 重复 Started 不重建;超时仅弹 toast,不改 status) */
export function startToolSlowTimer(callId: string, toolName: string): void {
if (_toolTimers.has(callId)) return
const timer = setTimeout(() => {
_toolTimers.delete(callId)
if (_toolSlowNotified.has(callId)) return
_toolSlowNotified.add(callId)
// 经 Tauri 事件总线广播(composable 无组件上下文),AiChat.vue listen 后弹本地 toast
void emit('ai-tool-slow-toast', { name: toolName })
}, TOOL_SLOW_MS)
_toolTimers.set(callId, timer)
}
/** 清除单个工具的慢执行计时器(收到 Completed/Approval 时调用) */
export function clearToolSlowTimer(callId: string): void {
const timer = _toolTimers.get(callId)
if (timer) {
clearTimeout(timer)
_toolTimers.delete(callId)
}
}
/** 清除全部工具慢执行计时器(stopListener/整流超时收尾时调用) */
export function clearAllToolSlowTimers(): void {
for (const timer of _toolTimers.values()) clearTimeout(timer)
_toolTimers.clear()
_toolSlowNotified.clear()
}