重构: 巨函数拆分 + 清理历史标记注释 + custom_prompt/停止按钮/tunnel 改进
This commit is contained in:
@@ -45,6 +45,24 @@ let _startPromise: Promise<void> | null = null
|
||||
// 上一次 AiTextDelta 的内容(单窗口内 LLM 重复 delta 防御用,见 handleStreamingEvent)
|
||||
let _lastDelta = ''
|
||||
|
||||
// 文本空闲信号(reactive ref):streaming=true 且 300ms 无新 delta 时=true。
|
||||
// 按钮据此显「发送」(idle)而非「停止」。不动 streaming(streaming 是渲染态,
|
||||
// 控制 MessageList 流式 block 显隐,不能在文本中途翻转,否则闪掉)。
|
||||
export const textIdle = ref(true)
|
||||
let _textIdleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function resetTextIdleTimer(): void {
|
||||
if (_textIdleTimer) clearTimeout(_textIdleTimer)
|
||||
textIdle.value = false // 有新 delta → 文本活跃
|
||||
_textIdleTimer = setTimeout(() => {
|
||||
textIdle.value = true // 300ms 无新 delta → 文本空闲
|
||||
_textIdleTimer = null
|
||||
}, 300)
|
||||
}
|
||||
function clearTextIdleTimer(): void {
|
||||
if (_textIdleTimer) { clearTimeout(_textIdleTimer); _textIdleTimer = null }
|
||||
textIdle.value = true // 整轮结束/新轮开始 → 默认空闲
|
||||
}
|
||||
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
// B-260616-17: 看门狗不重置的事件集合(审批等待/完成/错误由各自 case 内 clear)。
|
||||
@@ -298,6 +316,8 @@ function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
}
|
||||
_lastDelta = event.delta
|
||||
state.currentText += event.delta
|
||||
// 重置文本空闲定时器:每次 delta 重置 300ms。无新 delta 到 300ms → textIdle=true → 按钮白。
|
||||
resetTextIdleTimer()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -305,6 +325,10 @@ function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
// Agent 循环新一轮:保存当前文本到上一条 assistant 消息,新建空 assistant 消息
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
// 文本已刷新完毕 → 清空闲定时器(按钮白,等下一轮 deltas 来再红)
|
||||
// 不设 streaming=false:streaming 管渲染,多轮间需持续 true 让 MessageList 渲染后续 deltas。
|
||||
// 按钮白由 textIdle 独立控制,无需翻转 streaming。
|
||||
clearTextIdleTimer()
|
||||
state.completedTools = 0 // 新轮重置工具计数器
|
||||
state.messages.push({
|
||||
id: `ai-${nextMsgId()}` as MessageId,
|
||||
@@ -603,6 +627,7 @@ function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | 'AiError' | 'AiHelpRequired') {
|
||||
clearStreamWatchdog(convId || undefined)
|
||||
clearAllToolSlowTimers() // B-260616-12: 整轮/错误/求助结束清全部工具慢执行计时器与已提示集合
|
||||
clearTextIdleTimer() // 清文本空闲定时器 + 置 textIdle=true(整轮结束不该有活跃信号残留)
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
setStreaming(false, { convId: convId || null, reason })
|
||||
|
||||
@@ -23,7 +23,7 @@ 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 } from './useAiEvents'
|
||||
import { startListener, flushCurrentText } from './useAiEvents'
|
||||
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang, convStates } from './aiShared'
|
||||
|
||||
/// 待发送队列上限(超过抛错提示用户)
|
||||
@@ -71,6 +71,10 @@ const modelOverride = ref<string | null>(null)
|
||||
* undefined/空数组 → 本地 user 消息 mentionSpans 置 undefined(纯文本气泡零回归)+ 后端不传。
|
||||
*/
|
||||
async function doSend(text: string, skill?: string, force = false, parts?: ContentPart[], spans?: MentionSpan[]) {
|
||||
// 防御:先 flush 残留 currentText(正常情况已是空,flushCurrentText 直接 return;
|
||||
// 异常时序下可防止上一轮文本被 state.currentText='' 误清)
|
||||
flushCurrentText()
|
||||
|
||||
const userMsgId = `user-${nextMsgId()}`
|
||||
state.messages.push({
|
||||
id: userMsgId as MessageId,
|
||||
@@ -291,16 +295,16 @@ export function drainQueue(convId?: string | null) {
|
||||
* 发送消息 — 三级降级(B-260616-02 L2 发送韧性):
|
||||
*
|
||||
* L0 normal: 后端 idle → 直接 doSend()
|
||||
* L1 queued: 后端 busy → 入队等 AiCompleted 续发(≤30s 正常等待)
|
||||
* L1 queued: 后端 busy → 入队等 AiCompleted 续发
|
||||
* L2 force: 排队>30s → 返回特殊标记,由调用方(handleSend)弹 confirm;
|
||||
* 用户确认后重新进 sendMessage(forceMode=true)走 forceSend IPC
|
||||
*
|
||||
* forceMode=true 时跳过入队,直接走 ai_chat_force_send。
|
||||
*
|
||||
* Input Augmentation: spans 随消息透传(L0 直接 doSend / L2 force doSend);
|
||||
* L1 入队时 queue item 当前不挂 spans(队列续发属异步重试,mention 区间在首次发送时
|
||||
* 已与文本对齐,排队等待后用户可能改输入,续发用旧 spans 语义模糊;续发走无 mention 路径,
|
||||
* 与现有 parts 入队续发的设计取舍一致——队列为韧性保内容,非保全部上下文元数据)。
|
||||
* 注:入队条件仅用 backendGenerating(IPC 查后端真实态),不再检查 state.streaming。
|
||||
* streaming 是渲染态(文本是否在屏幕上输出),不能作为「后端能否接收消息」的代理。
|
||||
* 文本显示完但后端在落库时,streaming 仍 true——此时按钮已白(基于 delta 时间戳),
|
||||
* 用户点击发送,若后端还在忙则入队短暂等待(~1s),AiCompleted 后自动续发。
|
||||
*/
|
||||
async function sendMessage(text: string, skill?: string, forceMode = false, parts?: ContentPart[], spans?: MentionSpan[]) {
|
||||
if (!text.trim() && !(parts && parts.length)) return
|
||||
@@ -320,14 +324,12 @@ async function sendMessage(text: string, skill?: string, forceMode = false, part
|
||||
backendGenerating = false
|
||||
}
|
||||
|
||||
// ── L1:后端 busy → 入队 ──
|
||||
if (backendGenerating || state.streaming) {
|
||||
// ── L1:后端 busy → 入队(消息已在本地显示,待后端空闲后自动发送)──
|
||||
// 仅用 backendGenerating,不用 state.streaming。streaming 是渲染态,不是后端忙的代理。
|
||||
if (backendGenerating) {
|
||||
if (state.queue.length >= QUEUE_LIMIT) {
|
||||
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
|
||||
}
|
||||
setStreaming(true, { convId: state.activeConversationId, reason: 'L1-enqueue' })
|
||||
// F-260614-05 Phase 2b: 入队项挂 parts(供 drainQueue/续发时本地 user 消息渲染图)
|
||||
// 同时挂 conversationId,供 drainQueue 按会话精准续发(防跨会话串话)
|
||||
state.queue.push({
|
||||
text: text.trim(),
|
||||
skill: skill || undefined,
|
||||
|
||||
Reference in New Issue
Block a user