agent-Specialization/static/src/app/methods/ui/socket.ts

402 lines
16 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 { usePolicyStore } from '../../../stores/policy';
import { useModelStore } from '../../../stores/model';
import { usePersonalizationStore } from '../../../stores/personalization';
import { useTutorialStore } from '../../../stores/tutorial';
import { renderMarkdown as renderMarkdownHelper } from '../../../composables/useMarkdownRenderer';
import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from '../../../composables/useScrollControl';
import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from '../../../composables/usePanelResize';
import {
SUB_AGENT_DONE_PREFIX_RE,
BG_RUN_COMMAND_DONE_PREFIX_RE,
userMDebug,
UI_BOUNCE_TRACE_MAX,
uiBounceTraceLastTsByKey,
isUiBounceTraceEnabled,
uiBounceTrace,
isConnectionDiagEnabled,
pushConnectionDiagRecord,
connectionDiag,
parseSubAgentDoneLabel,
parseBackgroundRunCommandDoneLabel,
parseSystemNoticeLabel,
} from './shared';
export const socketMethods = {
async initSocket() {
// 主网页端已切换为 REST + 轮询模式,这里保留空实现用于兼容旧调用
this.socket = null;
},
async checkConnectionHealth() {
if (this.connectionHeartbeatInFlight) {
connectionDiag('log', 'health-skip-inflight', {
seq: this.connectionHeartbeatSeq,
isConnected: !!this.isConnected,
failCount: this.connectionHeartbeatFailCount
});
return;
}
this.connectionHeartbeatInFlight = true;
const seq = Number(this.connectionHeartbeatSeq || 0) + 1;
this.connectionHeartbeatSeq = seq;
const requestId = `${Date.now()}-${seq}`;
const startedAt = Date.now();
const diagEnabled = isConnectionDiagEnabled();
const healthUrl = diagEnabled ? '/api/health?diag=1' : '/api/health';
const wasConnected = !!this.isConnected;
let responseStatus: number | null = null;
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timeoutMs =
typeof this.connectionHeartbeatRequestTimeoutMs === 'number' &&
this.connectionHeartbeatRequestTimeoutMs > 0
? this.connectionHeartbeatRequestTimeoutMs
: 5000;
const timeoutId = controller ? window.setTimeout(() => controller.abort(), timeoutMs) : null;
try {
const response = await fetch(healthUrl, {
method: 'GET',
cache: 'no-store',
headers: {
'X-Connection-Heartbeat': requestId
},
signal: controller?.signal
});
responseStatus = response.status;
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const failCountBeforeRecover = this.connectionHeartbeatFailCount || 0;
this.isConnected = true;
this.connectionHeartbeatFailCount = 0;
this.connectionHeartbeatLastLatencyMs = Date.now() - startedAt;
this.connectionHeartbeatLastStatusCode = responseStatus;
this.connectionHeartbeatLastError = '';
if (!wasConnected) {
this.connectionHeartbeatLastChangeAt = Date.now();
connectionDiag(
'warn',
'health-recovered',
{
requestId,
seq,
elapsedMs: this.connectionHeartbeatLastLatencyMs,
failCountBeforeRecover,
status: responseStatus,
diagEnabled,
endpoint: healthUrl,
conversationId: this.currentConversationId || null,
taskInProgress: !!this.taskInProgress,
streamingMessage: !!this.streamingMessage,
visibility: document?.visibilityState || 'unknown',
online: typeof navigator !== 'undefined' ? navigator.onLine : null
},
{ force: true }
);
} else if (
isConnectionDiagEnabled() &&
(this.connectionHeartbeatLastLatencyMs >= 700 || seq <= 3 || seq % 60 === 0)
) {
connectionDiag('log', 'health-ok', {
requestId,
seq,
elapsedMs: this.connectionHeartbeatLastLatencyMs,
status: responseStatus,
endpoint: healthUrl,
visibility: document?.visibilityState || 'unknown'
});
}
} catch (error) {
const nextFailCount = (this.connectionHeartbeatFailCount || 0) + 1;
this.connectionHeartbeatFailCount = nextFailCount;
this.connectionHeartbeatLastLatencyMs = Date.now() - startedAt;
this.connectionHeartbeatLastStatusCode = responseStatus;
const errName = error?.name || 'Error';
const errMessage = error?.message || String(error);
this.connectionHeartbeatLastError = `${errName}: ${errMessage}`;
const failThreshold =
typeof this.connectionHeartbeatFailThreshold === 'number' &&
this.connectionHeartbeatFailThreshold > 0
? this.connectionHeartbeatFailThreshold
: 3;
const shouldDisconnect = nextFailCount >= failThreshold;
// 改为连续失败阈值后再置灰,避免偶发请求超时导致“假断连”
if (shouldDisconnect) {
this.isConnected = false;
}
if (wasConnected && shouldDisconnect) {
this.connectionHeartbeatLastChangeAt = Date.now();
}
const shouldLogFail =
wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0;
connectionDiag(
shouldLogFail ? 'warn' : 'log',
'health-failed',
{
requestId,
seq,
elapsedMs: this.connectionHeartbeatLastLatencyMs,
failCount: nextFailCount,
status: responseStatus,
endpoint: healthUrl,
diagEnabled,
wasConnected,
nowConnected: !!this.isConnected,
failThreshold,
shouldDisconnect,
errorName: errName,
errorMessage: errMessage,
timeout: errName === 'AbortError',
visibility: document?.visibilityState || 'unknown',
online: typeof navigator !== 'undefined' ? navigator.onLine : null,
conversationId: this.currentConversationId || null,
taskInProgress: !!this.taskInProgress,
streamingMessage: !!this.streamingMessage,
hasPendingTools:
typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null
},
{ force: shouldLogFail }
);
} finally {
this.connectionHeartbeatInFlight = false;
if (timeoutId) {
clearTimeout(timeoutId);
}
}
},
startConnectionHeartbeat() {
if (this.connectionHeartbeatActive) {
return;
}
this.connectionHeartbeatActive = true;
this.connectionHeartbeatFailCount = 0;
connectionDiag(
'log',
'heartbeat-start',
{
connectedIntervalMs: this.connectionHeartbeatIntervalMs,
disconnectedIntervalMs: this.connectionHeartbeatDisconnectedIntervalMs
},
{ force: true }
);
const runHeartbeat = async () => {
if (!this.connectionHeartbeatActive) {
return;
}
await this.checkConnectionHealth();
if (!this.connectionHeartbeatActive) {
return;
}
const connectedInterval =
typeof this.connectionHeartbeatIntervalMs === 'number' &&
this.connectionHeartbeatIntervalMs > 0
? this.connectionHeartbeatIntervalMs
: 8000;
const disconnectedInterval =
typeof this.connectionHeartbeatDisconnectedIntervalMs === 'number' &&
this.connectionHeartbeatDisconnectedIntervalMs > 0
? this.connectionHeartbeatDisconnectedIntervalMs
: 1000;
const nextInterval = this.isConnected ? connectedInterval : disconnectedInterval;
if (
isConnectionDiagEnabled() &&
(!this.isConnected || (this.connectionHeartbeatSeq || 0) <= 3)
) {
connectionDiag('log', 'heartbeat-next', {
seq: this.connectionHeartbeatSeq,
isConnected: !!this.isConnected,
failCount: this.connectionHeartbeatFailCount,
nextInterval
});
}
this.connectionHeartbeatTimer = window.setTimeout(() => {
runHeartbeat();
}, nextInterval);
};
// 先做一次即时探活,再根据连接状态动态设置下次轮询间隔
runHeartbeat();
},
stopConnectionHeartbeat() {
this.connectionHeartbeatActive = false;
if (this.connectionHeartbeatTimer) {
clearTimeout(this.connectionHeartbeatTimer);
this.connectionHeartbeatTimer = null;
}
connectionDiag('log', 'heartbeat-stop', {}, { force: true });
},
async loadInitialData() {
try {
debugLog('加载初始数据...');
const statusResponse = await fetch('/api/status');
if (!statusResponse.ok) {
throw new Error(`状态接口请求失败: ${statusResponse.status}`);
}
const statusData = await statusResponse.json();
this.socket = null;
this.projectPath = statusData.project_path || '';
this.agentVersion = statusData.version || this.agentVersion;
this.thinkingMode = !!statusData.thinking_mode;
this.applyStatusSnapshot(statusData);
this.fetchPermissionMode();
this.fetchExecutionMode();
this.fetchNetworkPermission();
this.fetchPendingToolApprovals();
// 立即更新配额和运行模式,避免等待其他慢接口
this.fetchUsageQuota();
const modelStore = useModelStore();
await modelStore.fetchModels().catch((err) => {
console.warn('加载模型列表失败,使用前端默认列表:', err);
});
if (statusData && typeof statusData.model_key === 'string') {
modelStore.setModel(statusData.model_key);
this.currentModelKey = modelStore.currentModelKey;
}
// 拉取管理员策略
const policyStore = usePolicyStore();
await policyStore.fetchPolicy();
this.applyPolicyUiLocks();
// 加载个性化设置
const personalizationStore = usePersonalizationStore();
if (!personalizationStore.loaded && !personalizationStore.loading) {
await personalizationStore.fetchPersonalization().catch((err) => {
console.warn('加载个性化设置失败:', err);
});
}
// 仅在“无当前对话”的新会话入口应用默认模型/模式;
// 对于已存在对话(含刷新恢复/URL指定对话必须保留对话自身设置。
const statusConversationIdForDefaults = statusData?.conversation?.current_id;
const hasActiveConversation =
typeof statusConversationIdForDefaults === 'string' &&
statusConversationIdForDefaults.length > 0 &&
!statusConversationIdForDefaults.startsWith('temp_');
if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) {
const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model;
if (defaultRunMode) {
this.runMode = defaultRunMode;
debugLog('应用默认运行模式:', defaultRunMode);
}
if (defaultModel) {
modelStore.setModel(defaultModel);
this.currentModelKey = modelStore.currentModelKey;
debugLog('应用默认模型:', defaultModel);
}
// 根据默认运行模式设置思考模式
if (defaultRunMode === 'thinking') {
this.thinkingMode = true;
} else if (defaultRunMode === 'fast') {
this.thinkingMode = false;
}
}
this.isConnected = true;
const focusPromise = this.focusFetchFiles();
let treePromise: Promise<any> | null = null;
const isHostMode = statusData?.container?.mode === 'host';
this.versioningHostMode = !!isHostMode;
this.dockerProjectMode = !isHostMode;
if (isHostMode) {
this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
await this.fetchHostWorkspaces();
} else {
this.fileMarkTreeUnavailable('Docker 模式下文件区已改为项目列表');
await this.fetchHostWorkspaces();
this.hostWorkspaceCreateDialogOpen = false;
this.hostWorkspaceCreatePath = '';
this.hostWorkspaceCreateLabel = '';
this.hostWorkspaceCreateError = '';
this.hostWorkspaceCreateSubmitting = false;
}
// 获取当前对话信息
const isExplicitNewRoute = this.isExplicitNewConversationRoute();
const runningConversationId = await this.getRunningTaskConversationId();
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
const resumeConversationId = statusConversationId || runningConversationId;
const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;
if (
resumeConversationId &&
!this.currentConversationId &&
(!isExplicitNewRoute || shouldResumeOnNewRoute)
) {
this.skipConversationHistoryReload = true;
// 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载
this.skipConversationLoadedEvent = true;
this.suppressTitleTyping = true;
this.titleReady = false;
this.currentConversationTitle = '';
this.titleTypingText = '';
this.currentConversationId = resumeConversationId;
const pathFragment = this.stripConversationPrefix(resumeConversationId);
const currentPath = window.location.pathname.replace(/^\/+/, '');
if (currentPath !== pathFragment) {
history.replaceState({ conversationId: resumeConversationId }, '', `/${pathFragment}`);
}
// 如果有当前对话,尝试获取标题和历史
try {
const convResponse = await fetch(`/api/conversations/current`);
const convData = await convResponse.json();
if (convData.success && convData.data) {
this.currentConversationTitle = convData.data.title;
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false });
} else {
this.titleReady = true;
this.suppressTitleTyping = false;
const fallbackTitle = this.currentConversationTitle || '新对话';
this.currentConversationTitle = fallbackTitle;
this.startTitleTyping(fallbackTitle, { animate: false });
}
// 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发
if (
this.lastHistoryLoadedConversationId !== this.currentConversationId ||
!Array.isArray(this.messages) ||
this.messages.length === 0
) {
await this.fetchAndDisplayHistory();
}
// 获取当前对话的Token统计
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
} catch (e) {
console.warn('获取当前对话标题失败:', e);
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle || '新对话', '');
}
}
// 待办数据依赖当前会话上下文,放在会话恢复/切换之后拉取,避免初始化早期拿到空快照
const todoPromise = this.fileFetchTodoList();
// 等待其他加载项完成(允许部分失败不阻塞模式切换)
const pendingPromises = [focusPromise, todoPromise];
if (treePromise) {
pendingPromises.push(treePromise);
}
await Promise.allSettled(pendingPromises);
await this.loadToolSettings(true);
// 加载对话列表修复初始化时未加载对话列表的bug
this.conversationsOffset = 0;
await this.loadConversationsList();
debugLog('初始数据加载完成');
} catch (error) {
console.error('加载初始数据失败:', error);
this.isConnected = false;
}
}
};