feat(web): remove socket dependency from main chat flow
This commit is contained in:
parent
7ae29180b6
commit
860fa7b539
@ -11,6 +11,7 @@ from collections import defaultdict, Counter, deque
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
from flask_socketio import emit
|
||||
from werkzeug.utils import secure_filename
|
||||
import zipfile
|
||||
|
||||
@ -51,7 +52,7 @@ from utils.conversation_manager import ConversationManager
|
||||
from utils.api_client import DeepSeekClient
|
||||
|
||||
from .auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username
|
||||
from .context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, reset_system_state, get_user_resources, get_or_create_usage_tracker
|
||||
from .context import with_terminal, get_terminal_for_sid, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, reset_system_state, get_user_resources, get_or_create_usage_tracker
|
||||
from .utils_common import (
|
||||
build_review_lines,
|
||||
debug_log,
|
||||
@ -73,6 +74,7 @@ from .state import (
|
||||
container_manager,
|
||||
get_last_active_ts,
|
||||
)
|
||||
from .usage import record_user_activity
|
||||
from .conversation_stats import (
|
||||
build_admin_dashboard_snapshot,
|
||||
compute_workspace_storage,
|
||||
@ -813,53 +815,82 @@ def handle_command(data):
|
||||
return
|
||||
record_user_activity(username)
|
||||
|
||||
result = _execute_system_command(terminal, command)
|
||||
emit('command_result', result)
|
||||
|
||||
|
||||
def _execute_system_command(terminal: WebTerminal, command: str) -> Dict[str, Any]:
|
||||
"""执行系统命令,供 WebSocket 与 REST API 复用。"""
|
||||
command = (command or '').strip()
|
||||
if command.startswith('/'):
|
||||
command = command[1:]
|
||||
|
||||
|
||||
parts = command.split(maxsplit=1)
|
||||
cmd = parts[0].lower()
|
||||
|
||||
cmd = parts[0].lower() if parts else ''
|
||||
|
||||
if cmd == "clear":
|
||||
terminal.context_manager.conversation_history.clear()
|
||||
if terminal.thinking_mode:
|
||||
terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode)
|
||||
emit('command_result', {
|
||||
return {
|
||||
'command': cmd,
|
||||
'success': True,
|
||||
'message': '对话已清除'
|
||||
})
|
||||
elif cmd == "status":
|
||||
}
|
||||
|
||||
if cmd == "status":
|
||||
status = terminal.get_status()
|
||||
# 添加终端状态
|
||||
if terminal.terminal_manager:
|
||||
terminal_status = terminal.terminal_manager.list_terminals()
|
||||
status['terminals'] = terminal_status
|
||||
emit('command_result', {
|
||||
status['terminals'] = terminal.terminal_manager.list_terminals()
|
||||
return {
|
||||
'command': cmd,
|
||||
'success': True,
|
||||
'data': status
|
||||
})
|
||||
elif cmd == "terminals":
|
||||
# 列出终端会话
|
||||
}
|
||||
|
||||
if cmd == "terminals":
|
||||
if terminal.terminal_manager:
|
||||
result = terminal.terminal_manager.list_terminals()
|
||||
emit('command_result', {
|
||||
return {
|
||||
'command': cmd,
|
||||
'success': True,
|
||||
'data': result
|
||||
})
|
||||
else:
|
||||
emit('command_result', {
|
||||
'command': cmd,
|
||||
'success': False,
|
||||
'message': '终端系统未初始化'
|
||||
})
|
||||
else:
|
||||
emit('command_result', {
|
||||
'data': terminal.terminal_manager.list_terminals()
|
||||
}
|
||||
return {
|
||||
'command': cmd,
|
||||
'success': False,
|
||||
'message': f'未知命令: {cmd}'
|
||||
})
|
||||
'message': '终端系统未初始化'
|
||||
}
|
||||
|
||||
return {
|
||||
'command': cmd or command,
|
||||
'success': False,
|
||||
'message': f'未知命令: {cmd or command}'
|
||||
}
|
||||
|
||||
|
||||
@conversation_bp.route('/api/commands', methods=['POST'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def execute_command_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
"""通过 REST API 执行系统命令(用于无 WebSocket 前端)。"""
|
||||
try:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
command = (payload.get('command') or '').strip()
|
||||
if not command:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "命令不能为空"
|
||||
}), 400
|
||||
|
||||
record_user_activity(username)
|
||||
result = _execute_system_command(terminal, command)
|
||||
status_code = 200 if result.get('success') else 400
|
||||
return jsonify(result), status_code
|
||||
except Exception as exc:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"命令执行异常: {exc}"
|
||||
}), 500
|
||||
|
||||
@conversation_bp.route('/api/conversations/<conversation_id>/token-statistics', methods=['GET'])
|
||||
@api_login_required
|
||||
|
||||
@ -174,7 +174,8 @@ const appOptions = {
|
||||
monitorShowPendingReply: 'showPendingReply',
|
||||
monitorPreviewTool: 'previewToolIntent',
|
||||
monitorQueueTool: 'enqueueToolEvent',
|
||||
monitorResolveTool: 'resolveToolResult'
|
||||
monitorResolveTool: 'resolveToolResult',
|
||||
forceUnlockMonitor: 'resetVisualState'
|
||||
}),
|
||||
...mapActions(useSubAgentStore, {
|
||||
subAgentFetch: 'fetchSubAgents',
|
||||
|
||||
@ -16,7 +16,7 @@ export function created() {
|
||||
}
|
||||
},
|
||||
isConnected: () => this.isConnected,
|
||||
getSocket: () => this.socket,
|
||||
executeCommand: async (command) => this.executeSystemCommand(command, { showToast: false }),
|
||||
downloadResource: (url, filename) => this.downloadResource(url, filename)
|
||||
});
|
||||
}
|
||||
@ -28,11 +28,9 @@ export async function mounted() {
|
||||
console.warn('CSRF token 初始化失败:', err);
|
||||
});
|
||||
}
|
||||
// 并行启动路由解析与实时连接,避免等待路由加载阻塞思考模式同步
|
||||
// 并行启动路由解析与初始化数据,网页端不再依赖 WebSocket 初始化
|
||||
const routePromise = this.bootstrapRoute();
|
||||
const socketPromise = this.initSocket();
|
||||
await routePromise;
|
||||
await socketPromise;
|
||||
this.$nextTick(() => {
|
||||
this.ensureScrollListener();
|
||||
// 刷新后若无输出,自动解锁滚动锁定
|
||||
|
||||
@ -2,6 +2,105 @@
|
||||
import { debugLog } from './common';
|
||||
|
||||
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.composerBusy) {
|
||||
this.stopTask();
|
||||
@ -79,9 +178,7 @@ export const messageMethods = {
|
||||
const message = text;
|
||||
const isCommand = hasText && !hasImages && !hasVideos && message.startsWith('/');
|
||||
if (isCommand) {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('send_command', { command: message });
|
||||
}
|
||||
await this.executeSystemCommand(message, { showToast: false });
|
||||
this.inputClearMessage();
|
||||
this.inputClearSelectedImages();
|
||||
this.inputClearSelectedVideos();
|
||||
@ -177,12 +274,6 @@ export const messageMethods = {
|
||||
console.error('[Message] 取消任务失败:', error);
|
||||
}
|
||||
|
||||
// 兼容 WebSocket 模式
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('stop_task');
|
||||
debugLog('发送停止请求(WebSocket)');
|
||||
}
|
||||
|
||||
// 立即清理前端状态,避免出现"不可输入也不可停止"的卡死状态
|
||||
this.clearPendingTools('user_stop');
|
||||
this.streamingMessage = false;
|
||||
@ -198,7 +289,7 @@ export const messageMethods = {
|
||||
cancelText: '取消'
|
||||
});
|
||||
if (confirmed) {
|
||||
this.socket.emit('send_command', { command: '/clear' });
|
||||
await this.executeSystemCommand('/clear', { showToast: false });
|
||||
}
|
||||
},
|
||||
|
||||
@ -265,7 +356,7 @@ export const messageMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
sendAutoUserMessage(text) {
|
||||
async sendAutoUserMessage(text) {
|
||||
const message = (text || '').trim();
|
||||
if (!message || !this.isConnected) {
|
||||
return false;
|
||||
@ -277,12 +368,19 @@ export const messageMethods = {
|
||||
}
|
||||
this.taskInProgress = true;
|
||||
this.chatAddUserMessage(message, []);
|
||||
if (this.socket) {
|
||||
this.socket.emit('send_message', {
|
||||
message,
|
||||
images: [],
|
||||
conversation_id: this.currentConversationId
|
||||
try {
|
||||
const { useTaskStore } = await import('../../stores/task');
|
||||
const taskStore = useTaskStore();
|
||||
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();
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import { usePolicyStore } from '../../stores/policy';
|
||||
import { useModelStore } from '../../stores/model';
|
||||
import { usePersonalizationStore } from '../../stores/personalization';
|
||||
import { initializeLegacySocket } from '../../composables/useLegacySocket';
|
||||
import { renderMarkdown as renderMarkdownHelper } from '../../composables/useMarkdownRenderer';
|
||||
import {
|
||||
scrollToBottom as scrollToBottomHelper,
|
||||
@ -608,7 +607,7 @@ export const uiMethods = {
|
||||
: '建议使用 read 工具进行搜索或分段阅读。';
|
||||
if (this.reviewSendToModel) {
|
||||
const message = `帮我继续这个任务,对话文件在 ${path},文件长 ${count || '未知'} 字符,${suggestion} 请阅读文件了解后,不要直接继续工作,而是向我汇报你的理解,然后等我做出指示。`;
|
||||
const sent = this.sendAutoUserMessage(message);
|
||||
const sent = await this.sendAutoUserMessage(message);
|
||||
if (sent) {
|
||||
this.reviewDialogOpen = false;
|
||||
}
|
||||
@ -1084,7 +1083,8 @@ export const uiMethods = {
|
||||
},
|
||||
|
||||
async initSocket() {
|
||||
await initializeLegacySocket(this);
|
||||
// 主网页端已切换为 REST + 轮询模式,这里保留空实现用于兼容旧调用
|
||||
this.socket = null;
|
||||
},
|
||||
|
||||
openRealtimeTerminal() {
|
||||
@ -1098,7 +1098,12 @@ export const uiMethods = {
|
||||
debugLog('加载初始数据...');
|
||||
|
||||
const statusResponse = await fetch('/api/status');
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`状态接口请求失败: ${statusResponse.status}`);
|
||||
}
|
||||
const statusData = await statusResponse.json();
|
||||
this.isConnected = true;
|
||||
this.socket = null;
|
||||
this.projectPath = statusData.project_path || '';
|
||||
this.agentVersion = statusData.version || this.agentVersion;
|
||||
this.thinkingMode = !!statusData.thinking_mode;
|
||||
@ -1186,6 +1191,7 @@ export const uiMethods = {
|
||||
debugLog('初始数据加载完成');
|
||||
} catch (error) {
|
||||
console.error('加载初始数据失败:', error);
|
||||
this.isConnected = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ interface ChatActionDependencies {
|
||||
autoResizeInput: () => void;
|
||||
focusComposer: () => void;
|
||||
isConnected: () => boolean;
|
||||
getSocket: () => { emit: (event: string, payload: any) => void } | null;
|
||||
executeCommand: (command: string) => Promise<{ success?: boolean; message?: string } | void>;
|
||||
downloadResource: (url: string, filename: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ const defaultDependencies: ChatActionDependencies = {
|
||||
autoResizeInput: () => {},
|
||||
focusComposer: () => {},
|
||||
isConnected: () => false,
|
||||
getSocket: () => null,
|
||||
executeCommand: async () => ({ success: false, message: '命令通道不可用' }),
|
||||
downloadResource: async () => {}
|
||||
};
|
||||
|
||||
@ -139,7 +139,7 @@ export const useChatActionStore = defineStore('chatActions', {
|
||||
});
|
||||
return true;
|
||||
},
|
||||
runCommand(action: any) {
|
||||
async runCommand(action: any) {
|
||||
const command = (action && (action.command || action.content))?.toString();
|
||||
if (!command) {
|
||||
this.dependencies.pushToast({
|
||||
@ -157,22 +157,30 @@ export const useChatActionStore = defineStore('chatActions', {
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const socket = this.dependencies.getSocket();
|
||||
if (!socket || typeof socket.emit !== 'function') {
|
||||
try {
|
||||
const result = await this.dependencies.executeCommand(command);
|
||||
if (result && result.success === false) {
|
||||
this.dependencies.pushToast({
|
||||
title: '命令执行失败',
|
||||
message: result.message || '请稍后重试',
|
||||
type: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
this.dependencies.pushToast({
|
||||
title: '无法执行',
|
||||
message: '命令通道不可用,请稍后重试。',
|
||||
title: '命令已执行',
|
||||
message: command,
|
||||
type: 'success'
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.dependencies.pushToast({
|
||||
title: '命令执行失败',
|
||||
message: (error as Error)?.message || '请稍后重试',
|
||||
type: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
socket.emit('send_command', { command });
|
||||
this.dependencies.pushToast({
|
||||
title: '命令已发送',
|
||||
message: command,
|
||||
type: 'success'
|
||||
});
|
||||
return true;
|
||||
},
|
||||
async downloadActionAttachment(action: any) {
|
||||
if (!action || !action.path) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user