新增: F-05多模态Phase2b前端(图片粘贴/拖拽+渲染+parts透传·保留虚拟滚动)
This commit is contained in:
@@ -21,6 +21,7 @@ import { ref } from 'vue'
|
||||
import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { state } from '@/stores/ai'
|
||||
import type { ContentPart } from '@/api/types'
|
||||
import i18n from '@/i18n'
|
||||
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
||||
import { startListener } from './useAiEvents'
|
||||
@@ -63,18 +64,27 @@ const _pendingApprovalIds = new Set<string>()
|
||||
|
||||
/**
|
||||
* 入队时记录时间戳,供 UI 展示排队时长和触发超时提示。
|
||||
* queue item 扩展: 原 { text, skill? } → { text, skill?, enqueuedAt }
|
||||
* 兼容 drainQueue/cancelQueued/clearQueue 等已有消费方(enqueuedAt 仅读不写)。
|
||||
* queue item 扩展: 原 { text, skill? } → { text, skill?, enqueuedAt, parts? }
|
||||
* 兼容 drainQueue/cancelQueued/clearQueue 等已有消费方(enqueuedAt/parts 仅读不写)。
|
||||
*/
|
||||
|
||||
// ── 内核发送:推送气泡+调 IPC,被 normal/force 两条路径共用 ──
|
||||
|
||||
async function doSend(text: string, skill?: string, force = false) {
|
||||
/**
|
||||
* F-260614-05 Phase 2b: parts 仅影响本地 push 的 user 消息(渲染多模态图)。
|
||||
*
|
||||
* 后端 ai_chat_send IPC 当前不接 parts(Phase 2c 接入);本轮 doSend 仅把 parts 写入本地 push 的
|
||||
* user 消息让用户气泡可见图,后端请求仍走 content 文本路径(无图)。
|
||||
* Phase 2c 后端接 parts 后,本函数透传给 aiApi.sendMessage/forceSend 即可全链路生效。
|
||||
*/
|
||||
async function doSend(text: string, skill?: string, force = false, parts?: ContentPart[]) {
|
||||
const userMsgId = `user-${nextMsgId()}`
|
||||
state.messages.push({
|
||||
id: userMsgId,
|
||||
role: 'user',
|
||||
content: text.trim(),
|
||||
// 仅在非空时挂 parts(纯文本消息保持 undefined,渲染走原 content 路径零回归)
|
||||
parts: parts && parts.length > 0 ? parts : undefined,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
@@ -247,7 +257,7 @@ async function editMessage(newMessage: string) {
|
||||
export function drainQueue() {
|
||||
if (state.queue.length === 0) return
|
||||
const next = state.queue.shift()!
|
||||
void sendMessage(next.text, next.skill)
|
||||
void sendMessage(next.text, next.skill, false, next.parts)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,12 +270,12 @@ export function drainQueue() {
|
||||
*
|
||||
* forceMode=true 时跳过入队,直接走 ai_chat_force_send。
|
||||
*/
|
||||
async function sendMessage(text: string, skill?: string, forceMode = false) {
|
||||
if (!text.trim()) return
|
||||
async function sendMessage(text: string, skill?: string, forceMode = false, parts?: ContentPart[]) {
|
||||
if (!text.trim() && !(parts && parts.length)) return
|
||||
|
||||
// ── L2 强制模式:跳过所有预检,直接 force_send ──
|
||||
if (forceMode) {
|
||||
await doSend(text, skill, true)
|
||||
await doSend(text, skill, true, parts)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -283,12 +293,18 @@ async function sendMessage(text: string, skill?: string, forceMode = false) {
|
||||
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
|
||||
}
|
||||
state.streaming = true
|
||||
state.queue.push({ text: text.trim(), skill: skill || undefined, enqueuedAt: Date.now() })
|
||||
// F-260614-05 Phase 2b: 入队项挂 parts(供 drainQueue 续发时本地 user 消息渲染图)
|
||||
state.queue.push({
|
||||
text: text.trim(),
|
||||
skill: skill || undefined,
|
||||
enqueuedAt: Date.now(),
|
||||
parts: parts && parts.length > 0 ? parts : undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ── L0:normal 直接发送 ──
|
||||
await doSend(text, skill, false)
|
||||
await doSend(text, skill, false, parts)
|
||||
}
|
||||
|
||||
/** 排队是否已超时(任一条超即算,因队首阻塞导致后续全堵) */
|
||||
@@ -309,7 +325,7 @@ export async function tryForceSend(confirmFn: (msg: string) => Promise<boolean>)
|
||||
// 此处无需前置 false(前置 false 会触发 AiChat watch 清流式块再重建,产生瞬态抖动)
|
||||
clearStreamWatchdog()
|
||||
try {
|
||||
await sendMessage(first.text, first.skill, true)
|
||||
await sendMessage(first.text, first.skill, true, first.parts)
|
||||
return true
|
||||
} catch {
|
||||
// force_send 也失败:消息不回队列(避免循环),让用户看到错误
|
||||
@@ -407,7 +423,7 @@ async function sendQueuedNow(index: number) {
|
||||
if (index < 0 || index >= state.queue.length) return
|
||||
const spliced = state.queue.splice(index, 1)[0]
|
||||
await stopChat()
|
||||
await sendMessage(spliced.text, spliced.skill)
|
||||
await sendMessage(spliced.text, spliced.skill, false, spliced.parts)
|
||||
}
|
||||
|
||||
/** 停止当前生成:本地先复位 streaming,再发停止信号。
|
||||
|
||||
Reference in New Issue
Block a user