agent-Specialization/static/src/app/methods/message.ts

616 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { debugLog } from './common';
import { useModelStore } from '../../stores/model';
export const messageMethods = {
async executeSystemCommand(rawCommand, options = {}) {
const command = (rawCommand || '').toString().trim();
if (!command) {
return { success: false, message: '命令不能为空' };
}
if (!this.isConnected) {
if (options.showToast !== false) {
this.uiPushToast({
title: '连接不可用',
message: '当前无法执行命令,请稍后重试。',
type: 'error'
});
}
return { success: false, message: '连接不可用' };
}
try {
const response = await fetch('/api/commands', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ command })
});
const payload = await response.json().catch(() => ({}));
const result = {
command: payload.command || command.replace(/^\//, ''),
success: !!payload.success,
message: payload.message,
data: payload.data
};
this.handleSystemCommandResult(result, options);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : '命令执行失败';
const result = {
command: command.replace(/^\//, ''),
success: false,
message
};
this.handleSystemCommandResult(result, options);
return result;
}
},
handleSystemCommandResult(data, options = {}) {
const showToast = options.showToast !== false;
if (data.command === 'clear' && data.success) {
this.logMessageState?.('command_result-clear', { data });
this.messages = [];
this.logMessageState?.('command_result-cleared', { data });
this.currentMessageIndex = -1;
this.chatClearExpandedBlocks();
this.resetTokenStatistics();
if (showToast) {
this.uiPushToast({
title: '已清除',
message: data.message || '对话已清除',
type: 'success'
});
}
return;
}
if (data.command === 'status' && data.success) {
this.addSystemMessage(`系统状态:\n${JSON.stringify(data.data || {}, null, 2)}`);
if (showToast) {
this.uiPushToast({
title: '状态已更新',
message: '已获取系统状态',
type: 'success'
});
}
return;
}
if (!data.success) {
this.addSystemMessage(`命令失败: ${data.message || '未知错误'}`);
if (showToast) {
this.uiPushToast({
title: '命令执行失败',
message: data.message || '请稍后重试',
type: 'error'
});
}
return;
}
if (showToast) {
this.uiPushToast({
title: '命令已执行',
message: data.command || '完成',
type: 'success'
});
}
},
handleSendOrStop() {
if (this.compressionInProgress) {
this.uiPushToast({
title: '对话自动压缩中',
message: '当前不可发送/停止,请等待压缩完成',
type: 'warning'
});
return;
}
if (this.composerBusy) {
this.stopTask();
} else {
this.sendMessage();
}
},
async sendMessage() {
console.log('[DEBUG_AWAITING] ===== sendMessage =====');
if (this.compressionInProgress) {
this.uiPushToast({
title: '对话自动压缩中',
message: '压缩完成后才能继续发送消息',
type: 'warning'
});
return;
}
if (this.streamingUi) {
return;
}
if (this.mediaUploading) {
this.uiPushToast({
title: '上传中',
message: '请等待图片/视频上传完成后再发送',
type: 'info'
});
return;
}
const text = (this.inputMessage || '').trim();
const images = Array.isArray(this.selectedImages) ? this.selectedImages.slice(0, 9) : [];
const videos = Array.isArray(this.selectedVideos) ? this.selectedVideos.slice(0, 1) : [];
const hasText = text.length > 0;
const hasImages = images.length > 0;
const hasVideos = videos.length > 0;
if (!hasText && !hasImages && !hasVideos) {
return;
}
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
if (this.isQuotaExceeded(quotaType)) {
this.showQuotaToast({ type: quotaType });
return;
}
const modelStore = useModelStore();
const currentModel = modelStore.models.find((m) => m.key === this.currentModelKey);
if (hasImages && !currentModel?.supportsImage) {
this.uiPushToast({
title: '当前模型不支持图片',
message: '请切换到支持图片输入的模型再发送图片',
type: 'error'
});
return;
}
if (hasVideos && !currentModel?.supportsVideo) {
this.uiPushToast({
title: '当前模型不支持视频',
message: '请切换到支持视频输入的模型后再发送视频',
type: 'error'
});
return;
}
if (hasVideos && hasImages) {
this.uiPushToast({
title: '请勿同时发送',
message: '视频与图片需分开发送,每条仅包含一种媒体',
type: 'warning'
});
return;
}
if (hasVideos) {
this.uiPushToast({
title: '视频处理中',
message: '读取视频需要较长时间,请耐心等待',
type: 'info',
duration: 5000
});
}
const message = text;
const wasBlank = this.isConversationBlank();
if (wasBlank) {
this.blankHeroExiting = true;
this.blankHeroActive = true;
setTimeout(() => {
this.blankHeroExiting = false;
this.blankHeroActive = false;
}, 320);
}
let targetConversationId = this.currentConversationId;
if (!targetConversationId) {
try {
const createResp = await fetch('/api/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
thinking_mode: this.thinkingMode,
mode: this.runMode
})
});
const createResult = await createResp.json().catch(() => ({}));
if (!createResp.ok || !createResult?.success || !createResult?.conversation_id) {
throw new Error(createResult?.message || createResult?.error || '创建对话失败');
}
targetConversationId = createResult.conversation_id;
this.skipConversationHistoryReload = true;
this.currentConversationId = targetConversationId;
this.currentConversationTitle = '新对话';
this.conversations = [
{
id: targetConversationId,
title: '新对话',
updated_at: new Date().toISOString(),
total_messages: 0,
total_tools: 0
},
...this.conversations.filter((conv) => conv && conv.id !== targetConversationId)
];
const pathFragment = this.stripConversationPrefix(targetConversationId);
history.replaceState({ conversationId: targetConversationId }, '', `/${pathFragment}`);
await this.applyPendingVersioningToConversation(targetConversationId);
} catch (error) {
this.uiPushToast({
title: '发送失败',
message: error?.message || '创建新对话失败,请重试',
type: 'error'
});
return;
}
}
// 标记任务进行中,直到任务完成或用户手动停止
this.taskInProgress = true;
this.chatAddUserMessage(message, images, videos);
// 使用 REST API 创建任务(轮询模式)
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
await taskStore.createTask(message, images, videos, targetConversationId, {
model_key: this.currentModelKey,
run_mode: this.runMode,
thinking_mode: this.thinkingMode
});
debugLog('[Message] 任务已创建,开始轮询');
} catch (error) {
console.error('[Message] 创建任务失败:', error);
this.uiPushToast({
title: '发送失败',
message: error.message || '创建任务失败,请重试',
type: 'error'
});
this.taskInProgress = false;
return;
}
if (typeof this.monitorShowPendingReply === 'function') {
this.monitorShowPendingReply();
}
this.inputClearMessage();
this.inputClearSelectedImages();
this.inputClearSelectedVideos();
this.inputSetImagePickerOpen(false);
this.inputSetVideoPickerOpen(false);
this.inputSetLineCount(1);
this.inputSetMultiline(false);
if (hasImages) {
this.conversationHasImages = true;
this.conversationHasVideos = false;
}
if (hasVideos) {
this.conversationHasVideos = true;
this.conversationHasImages = false;
}
if (this.autoScrollEnabled) {
this.scrollToBottom();
}
this.autoResizeInput();
// 发送消息后延迟更新当前上下文Token关键修复恢复原逻辑
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
},
// 新增:停止任务方法
async stopTask() {
console.log('[DEBUG_AWAITING] ===== stopTask =====');
if (this.compressionInProgress) {
this.uiPushToast({
title: '对话自动压缩中',
message: '压缩进行中,当前不可停止任务',
type: 'warning'
});
return;
}
const canStop = this.composerBusy && !this.stopRequested;
if (!canStop) {
return;
}
const shouldDropToolEvents = this.streamingUi;
this.stopRequested = true;
this.dropToolEvents = shouldDropToolEvents;
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (taskStore.currentTaskId) {
await taskStore.cancelTask();
}
// 等待后端确认
await new Promise((resolve) => setTimeout(resolve, 300));
const shouldKeepBusy =
Boolean(taskStore.currentTaskId) &&
['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus));
// 清理前端状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
// 若后台已回传停止事件,不要再次把输入区锁回“停止中”
this.taskInProgress = shouldKeepBusy;
this.forceUnlockMonitor('user_stop');
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
// 清理assistant消息的等待动画状态
const lastMessage = this.messages[this.messages.length - 1];
const before = {
hasLastMessage: !!lastMessage,
role: lastMessage?.role,
awaitingFirstContent: lastMessage?.awaitingFirstContent,
generatingLabel: lastMessage?.generatingLabel
};
if (lastMessage && lastMessage.role === 'assistant') {
lastMessage.awaitingFirstContent = false;
lastMessage.generatingLabel = '';
}
console.log('[DEBUG_AWAITING] stopTask 清理完成', {
before,
after: {
awaitingFirstContent: lastMessage?.awaitingFirstContent,
generatingLabel: lastMessage?.generatingLabel
}
});
} catch (error) {
console.error('[Message] 取消任务失败:', error);
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
const shouldKeepBusy =
Boolean(taskStore.currentTaskId) &&
['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus));
// 即使失败也清理状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
// 如果任务其实已结束,允许按钮恢复发送态
this.taskInProgress = shouldKeepBusy;
this.forceUnlockMonitor('user_stop');
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
// 清理assistant消息的等待动画状态
const lastMessage = this.messages[this.messages.length - 1];
if (lastMessage && lastMessage.role === 'assistant') {
lastMessage.awaitingFirstContent = false;
lastMessage.generatingLabel = '';
}
this.uiPushToast({
title: '停止失败',
message: '任务可能仍在运行,请刷新页面',
type: 'warning'
});
} finally {
// 确保清除 dropToolEvents 和 stopRequested 标志
this.dropToolEvents = false;
this.stopRequested = false;
}
},
async clearChat() {
const confirmed = await this.confirmAction({
title: '清除对话',
message: '确定要清除所有对话记录吗?该操作不可撤销。',
confirmText: '清除',
cancelText: '取消'
});
if (confirmed) {
await this.executeSystemCommand('/clear', { showToast: false });
}
},
async compressConversation() {
if (!this.currentConversationId) {
this.uiPushToast({
title: '无法压缩',
message: '当前没有可压缩的对话。',
type: 'info'
});
return;
}
if (this.compressing) {
return;
}
const confirmed = await this.confirmAction({
title: '压缩对话',
message: '确定要压缩当前对话记录吗?压缩后会生成新的对话副本。',
confirmText: '压缩',
cancelText: '取消'
});
if (!confirmed) {
return;
}
this.compressing = true;
this.compressionInProgress = true;
this.compressionMode = 'manual';
this.compressionStage = 'requesting';
this.compressionError = '';
if (this.compressionToastId) {
this.uiDismissToast(this.compressionToastId);
this.compressionToastId = null;
}
this.compressionToastId = this.uiPushToast({
title: '压缩中',
message: '对话正在压缩,请稍候…',
type: 'info',
duration: null,
closable: false
});
try {
const response = await fetch(`/api/conversations/${this.currentConversationId}/compress`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
this.compressionStage = 'switching';
const newId = result.compressed_conversation_id;
if (newId) {
await this.loadConversation(newId, { force: true });
}
const guideMessage = (result.guide_message || '').trim();
const autoTaskStarted = !!result.auto_task_started;
const autoTaskId = result.auto_task_id;
if (autoTaskStarted && autoTaskId) {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
taskStore.resumeTask(autoTaskId, {
status: result.auto_task_status || 'running',
resetOffset: true,
eventHandler: (event) => this.handleTaskEvent(event)
});
this.taskInProgress = true;
if (typeof this.monitorShowPendingReply === 'function') {
this.monitorShowPendingReply();
}
} else if (guideMessage) {
await this.sendAutoUserMessage(guideMessage);
}
this.conversationsOffset = 0;
await this.loadConversationsList();
debugLog('对话压缩完成:', result);
this.uiPushToast({
title: '压缩完成',
message: '已生成压缩后的新对话',
type: 'success',
duration: 2200
});
} else {
const message = result.message || result.error || '压缩失败';
this.compressionError = message;
this.uiPushToast({
title: '压缩失败',
message,
type: 'error'
});
}
} catch (error) {
console.error('压缩对话异常:', error);
this.compressionError = error.message || '请稍后重试';
this.uiPushToast({
title: '压缩对话异常',
message: error.message || '请稍后重试',
type: 'error'
});
} finally {
if (this.compressionToastId) {
this.uiDismissToast(this.compressionToastId);
this.compressionToastId = null;
}
this.compressing = false;
this.compressionInProgress = false;
this.compressionMode = '';
this.compressionStage = '';
}
},
async sendAutoUserMessage(text) {
const message = (text || '').trim();
if (!message || !this.isConnected) {
return false;
}
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
if (this.isQuotaExceeded(quotaType)) {
this.showQuotaToast({ type: quotaType });
return false;
}
this.taskInProgress = true;
this.chatAddUserMessage(message, []);
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
await taskStore.createTask(message, [], [], this.currentConversationId);
} catch (error) {
console.error('[Message] 自动消息创建任务失败:', error);
this.uiPushToast({
title: '发送失败',
message: error?.message || '创建任务失败,请重试',
type: 'error'
});
this.taskInProgress = false;
return false;
}
if (typeof this.monitorShowPendingReply === 'function') {
this.monitorShowPendingReply();
}
if (this.autoScrollEnabled) {
this.scrollToBottom();
}
this.autoResizeInput();
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
return true;
},
autoResizeInput() {
this.$nextTick(() => {
const textarea = this.getComposerElement('stadiumInput');
if (!textarea || !(textarea instanceof HTMLTextAreaElement)) {
return;
}
const previousHeight = textarea.offsetHeight;
textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6;
const targetHeight = Math.min(textarea.scrollHeight, maxHeight);
this.inputSetLineCount(Math.max(1, Math.round(targetHeight / lineHeight)));
this.inputSetMultiline(targetHeight > lineHeight * 1.4);
if (Math.abs(targetHeight - previousHeight) <= 0.5) {
textarea.style.height = `${targetHeight}px`;
return;
}
textarea.style.height = `${previousHeight}px`;
void textarea.offsetHeight;
requestAnimationFrame(() => {
textarea.style.height = `${targetHeight}px`;
});
});
}
};