feat(web): remove socket dependency from main chat flow

This commit is contained in:
JOJO 2026-04-03 11:34:42 +08:00
parent 7ae29180b6
commit 860fa7b539
6 changed files with 208 additions and 66 deletions

View File

@ -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

View File

@ -174,7 +174,8 @@ const appOptions = {
monitorShowPendingReply: 'showPendingReply',
monitorPreviewTool: 'previewToolIntent',
monitorQueueTool: 'enqueueToolEvent',
monitorResolveTool: 'resolveToolResult'
monitorResolveTool: 'resolveToolResult',
forceUnlockMonitor: 'resetVisualState'
}),
...mapActions(useSubAgentStore, {
subAgentFetch: 'fetchSubAgents',

View File

@ -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();
// 刷新后若无输出,自动解锁滚动锁定

View File

@ -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();

View File

@ -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;
}
},

View File

@ -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) {