新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地
This commit is contained in:
@@ -140,6 +140,135 @@ async function doSend(text: string, skill?: string, force = false) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成最后一条 AI 回复(UX-02)。
|
||||
*
|
||||
* 与 doSend 的区别:
|
||||
* - 不 push 新 user 消息(触发它的 user 消息已在历史末尾)
|
||||
* - 仅 push 一个空 AI 气泡占位 + 置 streaming,后端弹出旧 AI 回复后重跑 loop
|
||||
* - 失败回滚只移除占位气泡(不移除 user 消息)
|
||||
*
|
||||
* 后端 ai_regenerate 占用 generating 防并发双发,此处不再额外入队/预检。
|
||||
*/
|
||||
async function regenerate() {
|
||||
// 末尾须有 AI 回复可重生成(后端也会校验,前端先挡避免空跑)
|
||||
const msgs = state.messages
|
||||
const last = msgs[msgs.length - 1]
|
||||
if (!last || last.role !== 'assistant') {
|
||||
throw new Error(t('aiChat.regenerateUnavailable'))
|
||||
}
|
||||
|
||||
// 移除末尾旧 AI 气泡(后端也弹 history;前端先弹避免占位前残留旧文),push 空气泡占位
|
||||
state.messages.pop()
|
||||
const aiMsgId = `ai-${nextMsgId()}`
|
||||
state.messages.push({
|
||||
id: aiMsgId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
state.streaming = true
|
||||
state.currentText = ''
|
||||
state.queue = [] // 重生成视同新发,清残留队列
|
||||
resetStreamWatchdog()
|
||||
|
||||
await startListener()
|
||||
const raw = appSettings.get<string>('df-ai-language', 'auto')
|
||||
const lang = raw === 'auto'
|
||||
? appSettings.get<string>('df-language', 'zh-CN')
|
||||
: raw
|
||||
const convId = state.activeConversationId
|
||||
if (!convId) {
|
||||
// 无活跃对话:回滚占位,报错
|
||||
state.streaming = false
|
||||
clearStreamWatchdog()
|
||||
state.messages = state.messages.filter(m => m.id !== aiMsgId)
|
||||
throw new Error(t('aiChat.regenerateUnavailable'))
|
||||
}
|
||||
try {
|
||||
await aiApi.regenerate(convId, lang)
|
||||
} catch (e) {
|
||||
// IPC 失败:回滚 streaming + 移除占位气泡(旧回复后端已弹但前端先弹了,保持弹后态)
|
||||
state.streaming = false
|
||||
clearStreamWatchdog()
|
||||
state.messages = state.messages.filter(m => m.id !== aiMsgId)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑最后一条 user 消息并重新生成(UX-09)。
|
||||
*
|
||||
* 与 regenerate 的区别:
|
||||
* - regenerate:末条须是 assistant,pop 旧 AI 回复后重跑
|
||||
* - editMessage:末条须是 user(中间编辑被后端拒绝),就地改其 content,截断其后所有消息,
|
||||
* push 空气泡占位后重跑。被截断消息后端标 truncated(软删保留 DB),前端视图层同步移除。
|
||||
*
|
||||
* 后端 ai_chat_edit 占用 generating 防并发双发,此处不再额外入队/预检。
|
||||
*
|
||||
* @param newMessage 编辑后的新内容(替换末条 user 消息 content)
|
||||
*/
|
||||
async function editMessage(newMessage: string) {
|
||||
if (!newMessage.trim()) return
|
||||
|
||||
// 末条须是 user(中间编辑语义复杂,拒绝);后端也会校验,前端先挡避免空跑
|
||||
const msgs = state.messages
|
||||
const last = msgs[msgs.length - 1]
|
||||
if (!last || last.role !== 'user') {
|
||||
throw new Error(t('aiChat.editNotLastUser'))
|
||||
}
|
||||
|
||||
// 截断末条 user 之后的消息(此时 last 即末条 user,其后应已有 assistant 回复或为空)
|
||||
// 找到末条 user 的下标,移除其后所有消息
|
||||
let lastUserIdx = -1
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
if (msgs[i].role === 'user') { lastUserIdx = i; break }
|
||||
}
|
||||
if (lastUserIdx < 0) {
|
||||
throw new Error(t('aiChat.editNotLastUser'))
|
||||
}
|
||||
// 截断其后(保留末条 user 本身,改其 content)
|
||||
state.messages.splice(lastUserIdx + 1)
|
||||
msgs[lastUserIdx].content = newMessage.trim()
|
||||
|
||||
// push 空气泡占位(新 AI 回复将流入)
|
||||
const aiMsgId = `ai-${nextMsgId()}`
|
||||
state.messages.push({
|
||||
id: aiMsgId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
state.streaming = true
|
||||
state.currentText = ''
|
||||
state.queue = [] // 编辑重生成视同新发,清残留队列
|
||||
resetStreamWatchdog()
|
||||
|
||||
await startListener()
|
||||
const raw = appSettings.get<string>('df-ai-language', 'auto')
|
||||
const lang = raw === 'auto'
|
||||
? appSettings.get<string>('df-language', 'zh-CN')
|
||||
: raw
|
||||
const convId = state.activeConversationId
|
||||
if (!convId) {
|
||||
state.streaming = false
|
||||
clearStreamWatchdog()
|
||||
state.messages = state.messages.filter(m => m.id !== aiMsgId)
|
||||
throw new Error(t('aiChat.editNotLastUser'))
|
||||
}
|
||||
try {
|
||||
await aiApi.editMessage(convId, newMessage.trim(), lang)
|
||||
} catch (e) {
|
||||
// IPC 失败:回滚 streaming + 移除占位气泡(被截断的旧回复后端已标 truncated,前端已移除)
|
||||
state.streaming = false
|
||||
clearStreamWatchdog()
|
||||
state.messages = state.messages.filter(m => m.id !== aiMsgId)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/** 取出队首并发送(AiCompleted 触发,此时 streaming 已 false) */
|
||||
export function drainQueue() {
|
||||
if (state.queue.length === 0) return
|
||||
@@ -281,6 +410,8 @@ async function stopChat() {
|
||||
export function useAiSend() {
|
||||
return {
|
||||
sendMessage,
|
||||
regenerate,
|
||||
editMessage,
|
||||
approveToolCall,
|
||||
batchApprove,
|
||||
drainQueue,
|
||||
|
||||
Reference in New Issue
Block a user