refactor(frontend): 整理应用逻辑层与状态管理
This commit is contained in:
parent
1bda147a6f
commit
8be1c37b6a
@ -33,170 +33,170 @@ import { monitorMethods } from './app/methods/monitor';
|
|||||||
// 其他初始化逻辑已迁移到 app/bootstrap.ts
|
// 其他初始化逻辑已迁移到 app/bootstrap.ts
|
||||||
|
|
||||||
const appOptions = {
|
const appOptions = {
|
||||||
data: dataState,
|
data: dataState,
|
||||||
created,
|
created,
|
||||||
mounted,
|
mounted,
|
||||||
beforeUnmount,
|
beforeUnmount,
|
||||||
computed,
|
computed,
|
||||||
watch: watchers,
|
watch: watchers,
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...conversationMethods,
|
...conversationMethods,
|
||||||
...historyMethods,
|
...historyMethods,
|
||||||
...searchMethods,
|
...searchMethods,
|
||||||
...messageMethods,
|
...messageMethods,
|
||||||
...uploadMethods,
|
...uploadMethods,
|
||||||
...resourceMethods,
|
...resourceMethods,
|
||||||
...toolingMethods,
|
...toolingMethods,
|
||||||
...uiMethods,
|
...uiMethods,
|
||||||
...monitorMethods,
|
...monitorMethods,
|
||||||
...taskPollingMethods,
|
...taskPollingMethods,
|
||||||
...mapActions(useUiStore, {
|
...mapActions(useUiStore, {
|
||||||
uiToggleSidebar: 'toggleSidebar',
|
uiToggleSidebar: 'toggleSidebar',
|
||||||
uiSetSidebarCollapsed: 'setSidebarCollapsed',
|
uiSetSidebarCollapsed: 'setSidebarCollapsed',
|
||||||
uiSetWorkspaceCollapsed: 'setWorkspaceCollapsed',
|
uiSetWorkspaceCollapsed: 'setWorkspaceCollapsed',
|
||||||
uiToggleWorkspaceCollapsed: 'toggleWorkspaceCollapsed',
|
uiToggleWorkspaceCollapsed: 'toggleWorkspaceCollapsed',
|
||||||
uiSetChatDisplayMode: 'setChatDisplayMode',
|
uiSetChatDisplayMode: 'setChatDisplayMode',
|
||||||
uiSetPanelMode: 'setPanelMode',
|
uiSetPanelMode: 'setPanelMode',
|
||||||
uiSetPanelMenuOpen: 'setPanelMenuOpen',
|
uiSetPanelMenuOpen: 'setPanelMenuOpen',
|
||||||
uiTogglePanelMenu: 'togglePanelMenu',
|
uiTogglePanelMenu: 'togglePanelMenu',
|
||||||
uiSetMobileViewport: 'setIsMobileViewport',
|
uiSetMobileViewport: 'setIsMobileViewport',
|
||||||
uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen',
|
uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen',
|
||||||
uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu',
|
uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu',
|
||||||
uiSetActiveMobileOverlay: 'setActiveMobileOverlay',
|
uiSetActiveMobileOverlay: 'setActiveMobileOverlay',
|
||||||
uiCloseMobileOverlay: 'closeMobileOverlay',
|
uiCloseMobileOverlay: 'closeMobileOverlay',
|
||||||
uiPushToast: 'pushToast',
|
uiPushToast: 'pushToast',
|
||||||
uiUpdateToast: 'updateToast',
|
uiUpdateToast: 'updateToast',
|
||||||
uiDismissToast: 'dismissToast',
|
uiDismissToast: 'dismissToast',
|
||||||
uiShowQuotaToastMessage: 'showQuotaToastMessage',
|
uiShowQuotaToastMessage: 'showQuotaToastMessage',
|
||||||
uiDismissQuotaToast: 'dismissQuotaToast',
|
uiDismissQuotaToast: 'dismissQuotaToast',
|
||||||
uiRequestConfirm: 'requestConfirm',
|
uiRequestConfirm: 'requestConfirm',
|
||||||
uiResolveConfirm: 'resolveConfirm'
|
uiResolveConfirm: 'resolveConfirm'
|
||||||
}),
|
}),
|
||||||
...mapActions(useModelStore, {
|
...mapActions(useModelStore, {
|
||||||
modelSet: 'setModel'
|
modelSet: 'setModel'
|
||||||
}),
|
}),
|
||||||
...mapActions(useChatStore, {
|
...mapActions(useChatStore, {
|
||||||
chatExpandBlock: 'expandBlock',
|
chatExpandBlock: 'expandBlock',
|
||||||
chatCollapseBlock: 'collapseBlock',
|
chatCollapseBlock: 'collapseBlock',
|
||||||
chatClearExpandedBlocks: 'clearExpandedBlocks',
|
chatClearExpandedBlocks: 'clearExpandedBlocks',
|
||||||
chatSetThinkingLock: 'setThinkingLock',
|
chatSetThinkingLock: 'setThinkingLock',
|
||||||
chatClearThinkingLocks: 'clearThinkingLocks',
|
chatClearThinkingLocks: 'clearThinkingLocks',
|
||||||
chatSetScrollState: 'setScrollState',
|
chatSetScrollState: 'setScrollState',
|
||||||
chatEnableAutoScroll: 'enableAutoScroll',
|
chatEnableAutoScroll: 'enableAutoScroll',
|
||||||
chatDisableAutoScroll: 'disableAutoScroll',
|
chatDisableAutoScroll: 'disableAutoScroll',
|
||||||
chatToggleScrollLockState: 'toggleScrollLockState',
|
chatToggleScrollLockState: 'toggleScrollLockState',
|
||||||
chatAddUserMessage: 'addUserMessage',
|
chatAddUserMessage: 'addUserMessage',
|
||||||
chatStartAssistantMessage: 'startAssistantMessage',
|
chatStartAssistantMessage: 'startAssistantMessage',
|
||||||
chatStartThinkingAction: 'startThinkingAction',
|
chatStartThinkingAction: 'startThinkingAction',
|
||||||
chatAppendThinkingChunk: 'appendThinkingChunk',
|
chatAppendThinkingChunk: 'appendThinkingChunk',
|
||||||
chatCompleteThinkingAction: 'completeThinking',
|
chatCompleteThinkingAction: 'completeThinking',
|
||||||
chatStartTextAction: 'startTextAction',
|
chatStartTextAction: 'startTextAction',
|
||||||
chatAppendTextChunk: 'appendTextChunk',
|
chatAppendTextChunk: 'appendTextChunk',
|
||||||
chatCompleteTextAction: 'completeText',
|
chatCompleteTextAction: 'completeText',
|
||||||
chatAddSystemMessage: 'addSystemMessage',
|
chatAddSystemMessage: 'addSystemMessage',
|
||||||
chatEnsureAssistantMessage: 'ensureAssistantMessage'
|
chatEnsureAssistantMessage: 'ensureAssistantMessage'
|
||||||
}),
|
}),
|
||||||
...mapActions(useInputStore, {
|
...mapActions(useInputStore, {
|
||||||
inputSetFocused: 'setInputFocused',
|
inputSetFocused: 'setInputFocused',
|
||||||
inputToggleQuickMenu: 'toggleQuickMenu',
|
inputToggleQuickMenu: 'toggleQuickMenu',
|
||||||
inputCloseMenus: 'closeMenus',
|
inputCloseMenus: 'closeMenus',
|
||||||
inputOpenQuickMenu: 'openQuickMenu',
|
inputOpenQuickMenu: 'openQuickMenu',
|
||||||
inputSetQuickMenuOpen: 'setQuickMenuOpen',
|
inputSetQuickMenuOpen: 'setQuickMenuOpen',
|
||||||
inputToggleToolMenu: 'toggleToolMenu',
|
inputToggleToolMenu: 'toggleToolMenu',
|
||||||
inputSetToolMenuOpen: 'setToolMenuOpen',
|
inputSetToolMenuOpen: 'setToolMenuOpen',
|
||||||
inputToggleSettingsMenu: 'toggleSettingsMenu',
|
inputToggleSettingsMenu: 'toggleSettingsMenu',
|
||||||
inputSetSettingsOpen: 'setSettingsOpen',
|
inputSetSettingsOpen: 'setSettingsOpen',
|
||||||
inputSetMessage: 'setInputMessage',
|
inputSetMessage: 'setInputMessage',
|
||||||
inputClearMessage: 'clearInputMessage',
|
inputClearMessage: 'clearInputMessage',
|
||||||
inputSetLineCount: 'setInputLineCount',
|
inputSetLineCount: 'setInputLineCount',
|
||||||
inputSetMultiline: 'setInputMultiline',
|
inputSetMultiline: 'setInputMultiline',
|
||||||
inputSetImagePickerOpen: 'setImagePickerOpen',
|
inputSetImagePickerOpen: 'setImagePickerOpen',
|
||||||
inputSetSelectedImages: 'setSelectedImages',
|
inputSetSelectedImages: 'setSelectedImages',
|
||||||
inputAddSelectedImage: 'addSelectedImage',
|
inputAddSelectedImage: 'addSelectedImage',
|
||||||
inputClearSelectedImages: 'clearSelectedImages',
|
inputClearSelectedImages: 'clearSelectedImages',
|
||||||
inputRemoveSelectedImage: 'removeSelectedImage',
|
inputRemoveSelectedImage: 'removeSelectedImage',
|
||||||
inputSetVideoPickerOpen: 'setVideoPickerOpen',
|
inputSetVideoPickerOpen: 'setVideoPickerOpen',
|
||||||
inputSetSelectedVideos: 'setSelectedVideos',
|
inputSetSelectedVideos: 'setSelectedVideos',
|
||||||
inputAddSelectedVideo: 'addSelectedVideo',
|
inputAddSelectedVideo: 'addSelectedVideo',
|
||||||
inputClearSelectedVideos: 'clearSelectedVideos',
|
inputClearSelectedVideos: 'clearSelectedVideos',
|
||||||
inputRemoveSelectedVideo: 'removeSelectedVideo'
|
inputRemoveSelectedVideo: 'removeSelectedVideo'
|
||||||
}),
|
}),
|
||||||
...mapActions(useToolStore, {
|
...mapActions(useToolStore, {
|
||||||
toolRegisterAction: 'registerToolAction',
|
toolRegisterAction: 'registerToolAction',
|
||||||
toolUnregisterAction: 'unregisterToolAction',
|
toolUnregisterAction: 'unregisterToolAction',
|
||||||
toolFindAction: 'findToolAction',
|
toolFindAction: 'findToolAction',
|
||||||
toolTrackAction: 'trackToolAction',
|
toolTrackAction: 'trackToolAction',
|
||||||
toolReleaseAction: 'releaseToolAction',
|
toolReleaseAction: 'releaseToolAction',
|
||||||
toolGetLatestAction: 'getLatestActiveToolAction',
|
toolGetLatestAction: 'getLatestActiveToolAction',
|
||||||
toolResetTracking: 'resetToolTracking',
|
toolResetTracking: 'resetToolTracking',
|
||||||
toolSetSettings: 'setToolSettings',
|
toolSetSettings: 'setToolSettings',
|
||||||
toolSetSettingsLoading: 'setToolSettingsLoading'
|
toolSetSettingsLoading: 'setToolSettingsLoading'
|
||||||
}),
|
}),
|
||||||
...mapActions(useResourceStore, {
|
...mapActions(useResourceStore, {
|
||||||
resourceUpdateCurrentContextTokens: 'updateCurrentContextTokens',
|
resourceUpdateCurrentContextTokens: 'updateCurrentContextTokens',
|
||||||
resourceFetchConversationTokenStatistics: 'fetchConversationTokenStatistics',
|
resourceFetchConversationTokenStatistics: 'fetchConversationTokenStatistics',
|
||||||
resourceSetCurrentContextTokens: 'setCurrentContextTokens',
|
resourceSetCurrentContextTokens: 'setCurrentContextTokens',
|
||||||
resourceToggleTokenPanel: 'toggleTokenPanel',
|
resourceToggleTokenPanel: 'toggleTokenPanel',
|
||||||
resourceApplyStatusSnapshot: 'applyStatusSnapshot',
|
resourceApplyStatusSnapshot: 'applyStatusSnapshot',
|
||||||
resourceUpdateContainerStatus: 'updateContainerStatus',
|
resourceUpdateContainerStatus: 'updateContainerStatus',
|
||||||
resourceStartContainerStatsPolling: 'startContainerStatsPolling',
|
resourceStartContainerStatsPolling: 'startContainerStatsPolling',
|
||||||
resourceStopContainerStatsPolling: 'stopContainerStatsPolling',
|
resourceStopContainerStatsPolling: 'stopContainerStatsPolling',
|
||||||
resourceStartProjectStoragePolling: 'startProjectStoragePolling',
|
resourceStartProjectStoragePolling: 'startProjectStoragePolling',
|
||||||
resourceStopProjectStoragePolling: 'stopProjectStoragePolling',
|
resourceStopProjectStoragePolling: 'stopProjectStoragePolling',
|
||||||
resourceStartUsageQuotaPolling: 'startUsageQuotaPolling',
|
resourceStartUsageQuotaPolling: 'startUsageQuotaPolling',
|
||||||
resourceStopUsageQuotaPolling: 'stopUsageQuotaPolling',
|
resourceStopUsageQuotaPolling: 'stopUsageQuotaPolling',
|
||||||
resourcePollContainerStats: 'pollContainerStats',
|
resourcePollContainerStats: 'pollContainerStats',
|
||||||
resourceBindContainerVisibilityWatcher: 'bindContainerVisibilityWatcher',
|
resourceBindContainerVisibilityWatcher: 'bindContainerVisibilityWatcher',
|
||||||
resourcePollProjectStorage: 'pollProjectStorage',
|
resourcePollProjectStorage: 'pollProjectStorage',
|
||||||
resourceFetchUsageQuota: 'fetchUsageQuota',
|
resourceFetchUsageQuota: 'fetchUsageQuota',
|
||||||
resourceResetTokenStatistics: 'resetTokenStatistics',
|
resourceResetTokenStatistics: 'resetTokenStatistics',
|
||||||
resourceSetUsageQuota: 'setUsageQuota'
|
resourceSetUsageQuota: 'setUsageQuota'
|
||||||
}),
|
}),
|
||||||
...mapActions(useUploadStore, {
|
...mapActions(useUploadStore, {
|
||||||
uploadHandleSelected: 'handleSelectedFiles',
|
uploadHandleSelected: 'handleSelectedFiles',
|
||||||
uploadBatchFiles: 'uploadFiles'
|
uploadBatchFiles: 'uploadFiles'
|
||||||
}),
|
}),
|
||||||
...mapActions(useFileStore, {
|
...mapActions(useFileStore, {
|
||||||
fileFetchTree: 'fetchFileTree',
|
fileFetchTree: 'fetchFileTree',
|
||||||
fileSetTreeFromResponse: 'setFileTreeFromResponse',
|
fileSetTreeFromResponse: 'setFileTreeFromResponse',
|
||||||
fileFetchTodoList: 'fetchTodoList',
|
fileFetchTodoList: 'fetchTodoList',
|
||||||
fileSetTodoList: 'setTodoList',
|
fileSetTodoList: 'setTodoList',
|
||||||
fileHideContextMenu: 'hideContextMenu',
|
fileHideContextMenu: 'hideContextMenu',
|
||||||
fileMarkTreeUnavailable: 'markFileTreeUnavailable'
|
fileMarkTreeUnavailable: 'markFileTreeUnavailable'
|
||||||
}),
|
}),
|
||||||
...mapActions(useMonitorStore, {
|
...mapActions(useMonitorStore, {
|
||||||
monitorSyncDesktop: 'syncDesktopFromTree',
|
monitorSyncDesktop: 'syncDesktopFromTree',
|
||||||
monitorResetVisual: 'resetVisualState',
|
monitorResetVisual: 'resetVisualState',
|
||||||
monitorResetSpeech: 'resetSpeechBuffer',
|
monitorResetSpeech: 'resetSpeechBuffer',
|
||||||
monitorShowSpeech: 'enqueueModelSpeech',
|
monitorShowSpeech: 'enqueueModelSpeech',
|
||||||
monitorShowThinking: 'enqueueModelThinking',
|
monitorShowThinking: 'enqueueModelThinking',
|
||||||
monitorEndModelOutput: 'endModelOutput',
|
monitorEndModelOutput: 'endModelOutput',
|
||||||
monitorShowPendingReply: 'showPendingReply',
|
monitorShowPendingReply: 'showPendingReply',
|
||||||
monitorPreviewTool: 'previewToolIntent',
|
monitorPreviewTool: 'previewToolIntent',
|
||||||
monitorQueueTool: 'enqueueToolEvent',
|
monitorQueueTool: 'enqueueToolEvent',
|
||||||
monitorResolveTool: 'resolveToolResult',
|
monitorResolveTool: 'resolveToolResult',
|
||||||
forceUnlockMonitor: 'resetVisualState'
|
forceUnlockMonitor: 'resetVisualState'
|
||||||
}),
|
}),
|
||||||
...mapActions(useSubAgentStore, {
|
...mapActions(useSubAgentStore, {
|
||||||
subAgentFetch: 'fetchSubAgents',
|
subAgentFetch: 'fetchSubAgents',
|
||||||
subAgentStartPolling: 'startPolling',
|
subAgentStartPolling: 'startPolling',
|
||||||
subAgentStopPolling: 'stopPolling'
|
subAgentStopPolling: 'stopPolling'
|
||||||
}),
|
}),
|
||||||
...mapActions(useBackgroundCommandStore, {
|
...mapActions(useBackgroundCommandStore, {
|
||||||
backgroundCommandFetch: 'fetchCommands',
|
backgroundCommandFetch: 'fetchCommands',
|
||||||
backgroundCommandStartPolling: 'startPolling',
|
backgroundCommandStartPolling: 'startPolling',
|
||||||
backgroundCommandStopPolling: 'stopPolling'
|
backgroundCommandStopPolling: 'stopPolling'
|
||||||
}),
|
}),
|
||||||
...mapActions(useFocusStore, {
|
...mapActions(useFocusStore, {
|
||||||
focusFetchFiles: 'fetchFocusedFiles',
|
focusFetchFiles: 'fetchFocusedFiles',
|
||||||
focusSetFiles: 'setFocusedFiles'
|
focusSetFiles: 'setFocusedFiles'
|
||||||
}),
|
}),
|
||||||
...mapActions(usePersonalizationStore, {
|
...mapActions(usePersonalizationStore, {
|
||||||
personalizationOpenDrawer: 'openDrawer'
|
personalizationOpenDrawer: 'openDrawer'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(appOptions as any).components = appComponents;
|
(appOptions as any).components = appComponents;
|
||||||
|
|
||||||
|
|||||||
@ -2,138 +2,136 @@
|
|||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
|
|
||||||
function normalizeShowImageSrc(src: string) {
|
function normalizeShowImageSrc(src: string) {
|
||||||
if (!src) return '';
|
if (!src) return '';
|
||||||
const trimmed = src.trim();
|
const trimmed = src.trim();
|
||||||
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
||||||
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
||||||
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
||||||
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
|
||||||
|
|
||||||
function isSafeImageSrc(src: string) {
|
|
||||||
return !!normalizeShowImageSrc(src);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(input: string) {
|
function escapeHtml(input: string) {
|
||||||
return input
|
return input
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderShowImages(root: ParentNode | null = document) {
|
function renderShowImages(root: ParentNode | null = document) {
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
// 处理因自闭合解析导致的嵌套:把子 show_image 平铺到父后面
|
// 处理因自闭合解析导致的嵌套:把子 show_image 平铺到父后面
|
||||||
const nested = Array.from(root.querySelectorAll('show_image show_image')).reverse();
|
const nested = Array.from(root.querySelectorAll('show_image show_image')).reverse();
|
||||||
nested.forEach(child => {
|
nested.forEach((child) => {
|
||||||
const parent = child.parentElement;
|
const parent = child.parentElement;
|
||||||
if (parent && parent !== root) {
|
if (parent && parent !== root) {
|
||||||
parent.after(child);
|
parent.after(child);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const nodes = Array.from(root.querySelectorAll('show_image:not([data-rendered])')).reverse();
|
const nodes = Array.from(root.querySelectorAll('show_image:not([data-rendered])')).reverse();
|
||||||
nodes.forEach(node => {
|
nodes.forEach((node) => {
|
||||||
// 将 show_image 内误被包裹的内容移动到当前节点之后,保持原有顺序
|
// 将 show_image 内误被包裹的内容移动到当前节点之后,保持原有顺序
|
||||||
if (node.parentNode && node.firstChild) {
|
if (node.parentNode && node.firstChild) {
|
||||||
const parent = node.parentNode;
|
const parent = node.parentNode;
|
||||||
const ref = node.nextSibling; // 可能为 null,insertBefore 会当 append
|
const ref = node.nextSibling; // 可能为 null,insertBefore 会当 append
|
||||||
const children = Array.from(node.childNodes);
|
const children = Array.from(node.childNodes);
|
||||||
children.forEach(child => parent.insertBefore(child, ref));
|
children.forEach((child) => parent.insertBefore(child, ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawSrc = node.getAttribute('src') || '';
|
const rawSrc = node.getAttribute('src') || '';
|
||||||
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
||||||
if (!mappedSrc) {
|
if (!mappedSrc) {
|
||||||
node.setAttribute('data-rendered', '1');
|
node.setAttribute('data-rendered', '1');
|
||||||
node.setAttribute('data-rendered-error', 'invalid-src');
|
node.setAttribute('data-rendered-error', 'invalid-src');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const alt = node.getAttribute('alt') || '';
|
const alt = node.getAttribute('alt') || '';
|
||||||
const safeAlt = escapeHtml(alt.trim());
|
const safeAlt = escapeHtml(alt.trim());
|
||||||
const figure = document.createElement('figure');
|
const figure = document.createElement('figure');
|
||||||
figure.className = 'chat-inline-image';
|
figure.className = 'chat-inline-image';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.src = mappedSrc;
|
img.src = mappedSrc;
|
||||||
img.alt = safeAlt;
|
img.alt = safeAlt;
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
figure.classList.add('chat-inline-image--error');
|
figure.classList.add('chat-inline-image--error');
|
||||||
const tip = document.createElement('div');
|
const tip = document.createElement('div');
|
||||||
tip.className = 'chat-inline-image__error';
|
tip.className = 'chat-inline-image__error';
|
||||||
tip.textContent = '图片加载失败';
|
tip.textContent = '图片加载失败';
|
||||||
figure.appendChild(tip);
|
figure.appendChild(tip);
|
||||||
};
|
};
|
||||||
figure.appendChild(img);
|
figure.appendChild(img);
|
||||||
|
|
||||||
if (safeAlt) {
|
if (safeAlt) {
|
||||||
const caption = document.createElement('figcaption');
|
const caption = document.createElement('figcaption');
|
||||||
caption.innerHTML = safeAlt;
|
caption.innerHTML = safeAlt;
|
||||||
figure.appendChild(caption);
|
figure.appendChild(caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
node.replaceChildren(figure);
|
node.replaceChildren(figure);
|
||||||
node.setAttribute('data-rendered', '1');
|
node.setAttribute('data-rendered', '1');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let showImageObserver: MutationObserver | null = null;
|
let showImageObserver: MutationObserver | null = null;
|
||||||
|
|
||||||
export function setupShowImageObserver() {
|
export function setupShowImageObserver() {
|
||||||
if (showImageObserver) return;
|
if (showImageObserver) return;
|
||||||
const container = document.querySelector('.messages-area') || document.body;
|
const container = document.querySelector('.messages-area') || document.body;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
renderShowImages(container);
|
renderShowImages(container);
|
||||||
showImageObserver = new MutationObserver(() => renderShowImages(container));
|
showImageObserver = new MutationObserver(() => renderShowImages(container));
|
||||||
showImageObserver.observe(container, { childList: true, subtree: true });
|
showImageObserver.observe(container, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function teardownShowImageObserver() {
|
export function teardownShowImageObserver() {
|
||||||
if (showImageObserver) {
|
if (showImageObserver) {
|
||||||
showImageObserver.disconnect();
|
showImageObserver.disconnect();
|
||||||
showImageObserver = null;
|
showImageObserver = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateViewportHeightVar() {
|
function updateViewportHeightVar() {
|
||||||
const docEl = document.documentElement;
|
const docEl = document.documentElement;
|
||||||
const visualViewport = window.visualViewport;
|
const visualViewport = window.visualViewport;
|
||||||
|
|
||||||
if (visualViewport) {
|
if (visualViewport) {
|
||||||
const vh = visualViewport.height;
|
const vh = visualViewport.height;
|
||||||
const bottomInset = Math.max(
|
const bottomInset = Math.max(
|
||||||
0,
|
0,
|
||||||
(window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop
|
(window.innerHeight || docEl.clientHeight || vh) -
|
||||||
);
|
visualViewport.height -
|
||||||
docEl.style.setProperty('--app-viewport', `${vh}px`);
|
visualViewport.offsetTop
|
||||||
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);
|
);
|
||||||
} else {
|
docEl.style.setProperty('--app-viewport', `${vh}px`);
|
||||||
const height = window.innerHeight || docEl.clientHeight;
|
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);
|
||||||
if (height) {
|
} else {
|
||||||
docEl.style.setProperty('--app-viewport', `${height}px`);
|
const height = window.innerHeight || docEl.clientHeight;
|
||||||
}
|
if (height) {
|
||||||
docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)');
|
docEl.style.setProperty('--app-viewport', `${height}px`);
|
||||||
}
|
}
|
||||||
|
docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.katex = katex;
|
window.katex = katex;
|
||||||
|
|
||||||
updateViewportHeightVar();
|
updateViewportHeightVar();
|
||||||
window.addEventListener('resize', updateViewportHeightVar);
|
window.addEventListener('resize', updateViewportHeightVar);
|
||||||
window.addEventListener('orientationchange', updateViewportHeightVar);
|
window.addEventListener('orientationchange', updateViewportHeightVar);
|
||||||
window.addEventListener('pageshow', updateViewportHeightVar);
|
window.addEventListener('pageshow', updateViewportHeightVar);
|
||||||
if (window.visualViewport) {
|
if (window.visualViewport) {
|
||||||
window.visualViewport.addEventListener('resize', updateViewportHeightVar);
|
window.visualViewport.addEventListener('resize', updateViewportHeightVar);
|
||||||
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
|
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,45 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
import ChatArea from '../components/chat/ChatArea.vue';
|
import ChatArea from '../components/chat/ChatArea.vue';
|
||||||
import ConversationSidebar from '../components/sidebar/ConversationSidebar.vue';
|
import ConversationSidebar from '../components/sidebar/ConversationSidebar.vue';
|
||||||
import LeftPanel from '../components/panels/LeftPanel.vue';
|
import LeftPanel from '../components/panels/LeftPanel.vue';
|
||||||
import TokenDrawer from '../components/token/TokenDrawer.vue';
|
import TokenDrawer from '../components/token/TokenDrawer.vue';
|
||||||
import PersonalizationDrawer from '../components/personalization/PersonalizationDrawer.vue';
|
|
||||||
import QuickMenu from '../components/input/QuickMenu.vue';
|
import QuickMenu from '../components/input/QuickMenu.vue';
|
||||||
import InputComposer from '../components/input/InputComposer.vue';
|
import InputComposer from '../components/input/InputComposer.vue';
|
||||||
import AppShell from '../components/shell/AppShell.vue';
|
import AppShell from '../components/shell/AppShell.vue';
|
||||||
import ImagePicker from '../components/overlay/ImagePicker.vue';
|
|
||||||
import ConversationReviewDialog from '../components/overlay/ConversationReviewDialog.vue';
|
const PersonalizationDrawer = defineAsyncComponent(
|
||||||
import SubAgentActivityDialog from '../components/overlay/SubAgentActivityDialog.vue';
|
() => import('../components/personalization/PersonalizationDrawer.vue')
|
||||||
import BackgroundCommandDialog from '../components/overlay/BackgroundCommandDialog.vue';
|
);
|
||||||
import TutorialOverlay from '../components/overlay/TutorialOverlay.vue';
|
const ImagePicker = defineAsyncComponent(() => import('../components/overlay/ImagePicker.vue'));
|
||||||
import NewUserTutorialPrompt from '../components/overlay/NewUserTutorialPrompt.vue';
|
const ConversationReviewDialog = defineAsyncComponent(
|
||||||
|
() => import('../components/overlay/ConversationReviewDialog.vue')
|
||||||
|
);
|
||||||
|
const SubAgentActivityDialog = defineAsyncComponent(
|
||||||
|
() => import('../components/overlay/SubAgentActivityDialog.vue')
|
||||||
|
);
|
||||||
|
const BackgroundCommandDialog = defineAsyncComponent(
|
||||||
|
() => import('../components/overlay/BackgroundCommandDialog.vue')
|
||||||
|
);
|
||||||
|
const TutorialOverlay = defineAsyncComponent(
|
||||||
|
() => import('../components/overlay/TutorialOverlay.vue')
|
||||||
|
);
|
||||||
|
const NewUserTutorialPrompt = defineAsyncComponent(
|
||||||
|
() => import('../components/overlay/NewUserTutorialPrompt.vue')
|
||||||
|
);
|
||||||
|
|
||||||
export const appComponents = {
|
export const appComponents = {
|
||||||
ChatArea,
|
ChatArea,
|
||||||
ConversationSidebar,
|
ConversationSidebar,
|
||||||
LeftPanel,
|
LeftPanel,
|
||||||
TokenDrawer,
|
TokenDrawer,
|
||||||
PersonalizationDrawer,
|
PersonalizationDrawer,
|
||||||
QuickMenu,
|
QuickMenu,
|
||||||
InputComposer,
|
InputComposer,
|
||||||
AppShell,
|
AppShell,
|
||||||
ImagePicker,
|
ImagePicker,
|
||||||
ConversationReviewDialog,
|
ConversationReviewDialog,
|
||||||
SubAgentActivityDialog,
|
SubAgentActivityDialog,
|
||||||
BackgroundCommandDialog,
|
BackgroundCommandDialog,
|
||||||
TutorialOverlay,
|
TutorialOverlay,
|
||||||
NewUserTutorialPrompt
|
NewUserTutorialPrompt
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,161 +15,168 @@ import { useMonitorStore } from '../stores/monitor';
|
|||||||
import { usePolicyStore } from '../stores/policy';
|
import { usePolicyStore } from '../stores/policy';
|
||||||
|
|
||||||
export const computed = {
|
export const computed = {
|
||||||
...mapWritableState(useConnectionStore, [
|
...mapWritableState(useConnectionStore, [
|
||||||
'isConnected',
|
'isConnected',
|
||||||
'socket',
|
'socket',
|
||||||
'stopRequested',
|
'stopRequested',
|
||||||
'projectPath',
|
'projectPath',
|
||||||
'agentVersion',
|
'agentVersion',
|
||||||
'thinkingMode',
|
'thinkingMode',
|
||||||
'runMode'
|
'runMode'
|
||||||
]),
|
]),
|
||||||
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
|
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
|
||||||
...mapWritableState(useUiStore, [
|
...mapWritableState(useUiStore, [
|
||||||
'sidebarCollapsed',
|
'sidebarCollapsed',
|
||||||
'workspaceCollapsed',
|
'workspaceCollapsed',
|
||||||
'chatDisplayMode',
|
'chatDisplayMode',
|
||||||
'panelMode',
|
'panelMode',
|
||||||
'panelMenuOpen',
|
'panelMenuOpen',
|
||||||
'leftWidth',
|
'leftWidth',
|
||||||
'rightWidth',
|
'rightWidth',
|
||||||
'rightCollapsed',
|
'rightCollapsed',
|
||||||
'isResizing',
|
'isResizing',
|
||||||
'resizingPanel',
|
'resizingPanel',
|
||||||
'minPanelWidth',
|
'minPanelWidth',
|
||||||
'maxPanelWidth',
|
'maxPanelWidth',
|
||||||
'quotaToast',
|
'quotaToast',
|
||||||
'toastQueue',
|
'toastQueue',
|
||||||
'confirmDialog',
|
'confirmDialog',
|
||||||
'easterEgg',
|
'easterEgg',
|
||||||
'isMobileViewport',
|
'isMobileViewport',
|
||||||
'mobileOverlayMenuOpen',
|
'mobileOverlayMenuOpen',
|
||||||
'activeMobileOverlay'
|
'activeMobileOverlay'
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useConversationStore, [
|
...mapWritableState(useConversationStore, [
|
||||||
'conversations',
|
'conversations',
|
||||||
'conversationsLoading',
|
'conversationsLoading',
|
||||||
'hasMoreConversations',
|
'hasMoreConversations',
|
||||||
'loadingMoreConversations',
|
'loadingMoreConversations',
|
||||||
'currentConversationId',
|
'currentConversationId',
|
||||||
'currentConversationTitle',
|
'currentConversationTitle',
|
||||||
'searchQuery',
|
'searchQuery',
|
||||||
'searchTimer',
|
'searchTimer',
|
||||||
'searchResults',
|
'searchResults',
|
||||||
'searchActive',
|
'searchActive',
|
||||||
'searchInProgress',
|
'searchInProgress',
|
||||||
'searchMoreAvailable',
|
'searchMoreAvailable',
|
||||||
'searchOffset',
|
'searchOffset',
|
||||||
'searchTotal',
|
'searchTotal',
|
||||||
'conversationsOffset',
|
'conversationsOffset',
|
||||||
'conversationsLimit'
|
'conversationsLimit'
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useModelStore, ['currentModelKey']),
|
...mapWritableState(useModelStore, ['currentModelKey']),
|
||||||
...mapState(useModelStore, ['models']),
|
...mapState(useModelStore, ['models']),
|
||||||
...mapWritableState(useChatStore, [
|
...mapWritableState(useChatStore, [
|
||||||
'messages',
|
'messages',
|
||||||
'currentMessageIndex',
|
'currentMessageIndex',
|
||||||
'streamingMessage',
|
'streamingMessage',
|
||||||
'expandedBlocks',
|
'expandedBlocks',
|
||||||
'autoScrollEnabled',
|
'autoScrollEnabled',
|
||||||
'userScrolling',
|
'userScrolling',
|
||||||
'thinkingScrollLocks'
|
'thinkingScrollLocks'
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useInputStore, [
|
...mapWritableState(useInputStore, [
|
||||||
'inputMessage',
|
'inputMessage',
|
||||||
'inputLineCount',
|
'inputLineCount',
|
||||||
'inputIsMultiline',
|
'inputIsMultiline',
|
||||||
'inputIsFocused',
|
'inputIsFocused',
|
||||||
'quickMenuOpen',
|
'quickMenuOpen',
|
||||||
'toolMenuOpen',
|
'toolMenuOpen',
|
||||||
'settingsOpen',
|
'settingsOpen',
|
||||||
'imagePickerOpen',
|
'imagePickerOpen',
|
||||||
'videoPickerOpen',
|
'videoPickerOpen',
|
||||||
'selectedImages',
|
'selectedImages',
|
||||||
'selectedVideos'
|
'selectedVideos'
|
||||||
]),
|
]),
|
||||||
resolvedRunMode() {
|
resolvedRunMode() {
|
||||||
const allowed = ['fast', 'thinking', 'deep'];
|
const allowed = ['fast', 'thinking', 'deep'];
|
||||||
if (allowed.includes(this.runMode)) {
|
if (allowed.includes(this.runMode)) {
|
||||||
return this.runMode;
|
return this.runMode;
|
||||||
}
|
|
||||||
return this.thinkingMode ? 'thinking' : 'fast';
|
|
||||||
},
|
|
||||||
headerRunModeOptions() {
|
|
||||||
return [
|
|
||||||
{ value: 'fast', label: '快速模式', desc: '低思考,响应更快' },
|
|
||||||
{ value: 'thinking', label: '思考模式', desc: '更长思考,综合回答' },
|
|
||||||
{ value: 'deep', label: '深度思考', desc: '持续推理,适合复杂任务' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
headerRunModeLabel() {
|
|
||||||
const current = this.headerRunModeOptions.find((o) => o.value === this.resolvedRunMode);
|
|
||||||
return current ? current.label : '快速模式';
|
|
||||||
},
|
|
||||||
currentModelLabel() {
|
|
||||||
const modelStore = useModelStore();
|
|
||||||
return modelStore.currentModel?.label || 'Kimi-k2.5';
|
|
||||||
},
|
|
||||||
policyUiBlocks() {
|
|
||||||
const store = usePolicyStore();
|
|
||||||
return store.uiBlocks || {};
|
|
||||||
},
|
|
||||||
adminDisabledModels() {
|
|
||||||
const store = usePolicyStore();
|
|
||||||
return store.disabledModelSet;
|
|
||||||
},
|
|
||||||
modelOptions() {
|
|
||||||
const disabledSet = this.adminDisabledModels || new Set();
|
|
||||||
const options = this.models || [];
|
|
||||||
return options.map((opt) => ({
|
|
||||||
...opt,
|
|
||||||
disabled: disabledSet.has(opt.key)
|
|
||||||
})).filter((opt) => true);
|
|
||||||
},
|
|
||||||
titleRibbonVisible() {
|
|
||||||
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
|
||||||
},
|
|
||||||
...mapWritableState(useToolStore, [
|
|
||||||
'preparingTools',
|
|
||||||
'activeTools',
|
|
||||||
'toolActionIndex',
|
|
||||||
'toolStacks',
|
|
||||||
'toolSettings',
|
|
||||||
'toolSettingsLoading'
|
|
||||||
]),
|
|
||||||
...mapWritableState(useResourceStore, [
|
|
||||||
'tokenPanelCollapsed',
|
|
||||||
'currentContextTokens',
|
|
||||||
'currentConversationTokens',
|
|
||||||
'projectStorage',
|
|
||||||
'containerStatus',
|
|
||||||
'containerNetRate',
|
|
||||||
'usageQuota'
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFocusStore, ['focusedFiles']),
|
|
||||||
...mapWritableState(useUploadStore, ['uploading', 'mediaUploading']),
|
|
||||||
...mapState(useMonitorStore, {
|
|
||||||
monitorIsLocked: (store) => store.isLocked
|
|
||||||
}),
|
|
||||||
displayModeSwitchDisabled() {
|
|
||||||
return !!this.policyUiBlocks.block_virtual_monitor;
|
|
||||||
},
|
|
||||||
displayLockEngaged() {
|
|
||||||
return !!this.compressionInProgress || !!this.compressing;
|
|
||||||
},
|
|
||||||
streamingUi() {
|
|
||||||
return this.streamingMessage || this.hasPendingToolActions();
|
|
||||||
},
|
|
||||||
composerBusy() {
|
|
||||||
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
|
|
||||||
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested || this.compressionInProgress || this.compressing;
|
|
||||||
},
|
|
||||||
composerHeroActive() {
|
|
||||||
return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive;
|
|
||||||
},
|
|
||||||
composerInteractionActive() {
|
|
||||||
const hasText = !!(this.inputMessage && this.inputMessage.trim().length > 0);
|
|
||||||
const hasImages = Array.isArray(this.selectedImages) && this.selectedImages.length > 0;
|
|
||||||
return this.quickMenuOpen || hasText || hasImages;
|
|
||||||
}
|
}
|
||||||
|
return this.thinkingMode ? 'thinking' : 'fast';
|
||||||
|
},
|
||||||
|
headerRunModeOptions() {
|
||||||
|
return [
|
||||||
|
{ value: 'fast', label: '快速模式', desc: '低思考,响应更快' },
|
||||||
|
{ value: 'thinking', label: '思考模式', desc: '更长思考,综合回答' },
|
||||||
|
{ value: 'deep', label: '深度思考', desc: '持续推理,适合复杂任务' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
headerRunModeLabel() {
|
||||||
|
const current = this.headerRunModeOptions.find((o) => o.value === this.resolvedRunMode);
|
||||||
|
return current ? current.label : '快速模式';
|
||||||
|
},
|
||||||
|
currentModelLabel() {
|
||||||
|
const modelStore = useModelStore();
|
||||||
|
return modelStore.currentModel?.label || 'Kimi-k2.5';
|
||||||
|
},
|
||||||
|
policyUiBlocks() {
|
||||||
|
const store = usePolicyStore();
|
||||||
|
return store.uiBlocks || {};
|
||||||
|
},
|
||||||
|
adminDisabledModels() {
|
||||||
|
const store = usePolicyStore();
|
||||||
|
return store.disabledModelSet;
|
||||||
|
},
|
||||||
|
modelOptions() {
|
||||||
|
const disabledSet = this.adminDisabledModels || new Set();
|
||||||
|
const options = this.models || [];
|
||||||
|
return options.map((opt) => ({
|
||||||
|
...opt,
|
||||||
|
disabled: disabledSet.has(opt.key)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
titleRibbonVisible() {
|
||||||
|
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
||||||
|
},
|
||||||
|
...mapWritableState(useToolStore, [
|
||||||
|
'preparingTools',
|
||||||
|
'activeTools',
|
||||||
|
'toolActionIndex',
|
||||||
|
'toolStacks',
|
||||||
|
'toolSettings',
|
||||||
|
'toolSettingsLoading'
|
||||||
|
]),
|
||||||
|
...mapWritableState(useResourceStore, [
|
||||||
|
'tokenPanelCollapsed',
|
||||||
|
'currentContextTokens',
|
||||||
|
'currentConversationTokens',
|
||||||
|
'projectStorage',
|
||||||
|
'containerStatus',
|
||||||
|
'containerNetRate',
|
||||||
|
'usageQuota'
|
||||||
|
]),
|
||||||
|
...mapWritableState(useFocusStore, ['focusedFiles']),
|
||||||
|
...mapWritableState(useUploadStore, ['uploading', 'mediaUploading']),
|
||||||
|
...mapState(useMonitorStore, {
|
||||||
|
monitorIsLocked: (store) => store.isLocked
|
||||||
|
}),
|
||||||
|
displayModeSwitchDisabled() {
|
||||||
|
return !!this.policyUiBlocks.block_virtual_monitor;
|
||||||
|
},
|
||||||
|
displayLockEngaged() {
|
||||||
|
return !!this.compressionInProgress || !!this.compressing;
|
||||||
|
},
|
||||||
|
streamingUi() {
|
||||||
|
return this.streamingMessage || this.hasPendingToolActions();
|
||||||
|
},
|
||||||
|
composerBusy() {
|
||||||
|
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
|
||||||
|
return (
|
||||||
|
this.streamingUi ||
|
||||||
|
this.taskInProgress ||
|
||||||
|
monitorLock ||
|
||||||
|
this.stopRequested ||
|
||||||
|
this.compressionInProgress ||
|
||||||
|
this.compressing
|
||||||
|
);
|
||||||
|
},
|
||||||
|
composerHeroActive() {
|
||||||
|
return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive;
|
||||||
|
},
|
||||||
|
composerInteractionActive() {
|
||||||
|
const hasText = !!(this.inputMessage && this.inputMessage.trim().length > 0);
|
||||||
|
const hasImages = Array.isArray(this.selectedImages) && this.selectedImages.length > 0;
|
||||||
|
return this.quickMenuOpen || hasText || hasImages;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,119 +5,119 @@ import { setupShowImageObserver, teardownShowImageObserver } from './bootstrap';
|
|||||||
import { debugLog } from './methods/common';
|
import { debugLog } from './methods/common';
|
||||||
|
|
||||||
export function created() {
|
export function created() {
|
||||||
const actionStore = useChatActionStore();
|
const actionStore = useChatActionStore();
|
||||||
actionStore.registerDependencies({
|
actionStore.registerDependencies({
|
||||||
pushToast: (payload) => this.uiPushToast(payload),
|
pushToast: (payload) => this.uiPushToast(payload),
|
||||||
autoResizeInput: () => this.autoResizeInput(),
|
autoResizeInput: () => this.autoResizeInput(),
|
||||||
focusComposer: () => {
|
focusComposer: () => {
|
||||||
const inputEl = this.getComposerElement('stadiumInput');
|
const inputEl = this.getComposerElement('stadiumInput');
|
||||||
if (inputEl && typeof inputEl.focus === 'function') {
|
if (inputEl && typeof inputEl.focus === 'function') {
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isConnected: () => this.isConnected,
|
isConnected: () => this.isConnected,
|
||||||
executeCommand: async (command) => this.executeSystemCommand(command, { showToast: false }),
|
executeCommand: async (command) => this.executeSystemCommand(command, { showToast: false }),
|
||||||
downloadResource: (url, filename) => this.downloadResource(url, filename)
|
downloadResource: (url, filename) => this.downloadResource(url, filename)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mounted() {
|
export async function mounted() {
|
||||||
debugLog('Vue应用已挂载');
|
debugLog('Vue应用已挂载');
|
||||||
console.log('[DEBUG_SYSTEM][boot] 前端调试版本已加载 2026-04-05');
|
console.log('[DEBUG_SYSTEM][boot] 前端调试版本已加载 2026-04-05');
|
||||||
if (window.ensureCsrfToken) {
|
if (window.ensureCsrfToken) {
|
||||||
window.ensureCsrfToken().catch((err) => {
|
window.ensureCsrfToken().catch((err) => {
|
||||||
console.warn('CSRF token 初始化失败:', err);
|
console.warn('CSRF token 初始化失败:', err);
|
||||||
});
|
|
||||||
}
|
|
||||||
// 并行启动路由解析与初始化数据,网页端不再依赖 WebSocket 初始化
|
|
||||||
const routePromise = this.bootstrapRoute();
|
|
||||||
await routePromise;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.ensureScrollListener();
|
|
||||||
// 刷新后若无输出,自动解锁滚动锁定
|
|
||||||
normalizeScrollLock(this);
|
|
||||||
});
|
});
|
||||||
setupShowImageObserver();
|
}
|
||||||
|
// 并行启动路由解析与初始化数据,网页端不再依赖 WebSocket 初始化
|
||||||
|
const routePromise = this.bootstrapRoute();
|
||||||
|
await routePromise;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.ensureScrollListener();
|
||||||
|
// 刷新后若无输出,自动解锁滚动锁定
|
||||||
|
normalizeScrollLock(this);
|
||||||
|
});
|
||||||
|
setupShowImageObserver();
|
||||||
|
|
||||||
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
||||||
const initialDataPromise = this.loadInitialData();
|
const initialDataPromise = this.loadInitialData();
|
||||||
this.startConnectionHeartbeat();
|
this.startConnectionHeartbeat();
|
||||||
this.checkTutorialPrompt();
|
this.checkTutorialPrompt();
|
||||||
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
|
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
|
||||||
initialDataPromise
|
initialDataPromise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
|
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
|
||||||
this.checkTutorialPrompt();
|
this.checkTutorialPrompt();
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册全局事件处理器(用于任务轮询)
|
||||||
|
(window as any).__taskEventHandler = (event: any) => {
|
||||||
|
if (typeof this.handleTaskEvent === 'function') {
|
||||||
|
this.handleTaskEvent(event);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 注册全局事件处理器(用于任务轮询)
|
// 立即尝试恢复运行中的任务(不延迟)
|
||||||
(window as any).__taskEventHandler = (event: any) => {
|
if (typeof this.restoreTaskState === 'function') {
|
||||||
if (typeof this.handleTaskEvent === 'function') {
|
this.restoreTaskState();
|
||||||
this.handleTaskEvent(event);
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 立即尝试恢复运行中的任务(不延迟)
|
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
||||||
if (typeof this.restoreTaskState === 'function') {
|
document.addEventListener('click', this.handleClickOutsidePanelMenu);
|
||||||
this.restoreTaskState();
|
document.addEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||||
}
|
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
||||||
|
document.addEventListener('click', this.handleCopyCodeClick);
|
||||||
|
window.addEventListener('popstate', this.handlePopState);
|
||||||
|
window.addEventListener('keydown', this.handleMobileOverlayEscape);
|
||||||
|
this.setupMobileViewportWatcher();
|
||||||
|
|
||||||
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
this.subAgentFetch();
|
||||||
document.addEventListener('click', this.handleClickOutsidePanelMenu);
|
this.subAgentStartPolling();
|
||||||
document.addEventListener('click', this.handleClickOutsideHeaderMenu);
|
this.backgroundCommandFetch();
|
||||||
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
this.backgroundCommandStartPolling();
|
||||||
document.addEventListener('click', this.handleCopyCodeClick);
|
|
||||||
window.addEventListener('popstate', this.handlePopState);
|
|
||||||
window.addEventListener('keydown', this.handleMobileOverlayEscape);
|
|
||||||
this.setupMobileViewportWatcher();
|
|
||||||
|
|
||||||
this.subAgentFetch();
|
this.$nextTick(() => {
|
||||||
this.subAgentStartPolling();
|
this.autoResizeInput();
|
||||||
this.backgroundCommandFetch();
|
});
|
||||||
this.backgroundCommandStartPolling();
|
this.resourceBindContainerVisibilityWatcher();
|
||||||
|
this.resourceStartContainerStatsPolling();
|
||||||
this.$nextTick(() => {
|
this.resourceStartProjectStoragePolling();
|
||||||
this.autoResizeInput();
|
this.resourceStartUsageQuotaPolling();
|
||||||
});
|
|
||||||
this.resourceBindContainerVisibilityWatcher();
|
|
||||||
this.resourceStartContainerStatsPolling();
|
|
||||||
this.resourceStartProjectStoragePolling();
|
|
||||||
this.resourceStartUsageQuotaPolling();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beforeUnmount() {
|
export function beforeUnmount() {
|
||||||
// 停止任务轮询
|
// 停止任务轮询
|
||||||
try {
|
try {
|
||||||
const { useTaskStore } = require('../stores/task');
|
const { useTaskStore } = require('../stores/task');
|
||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
taskStore.stopPolling();
|
taskStore.stopPolling();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsideHeaderMenu);
|
document.removeEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
||||||
document.removeEventListener('click', this.handleCopyCodeClick);
|
document.removeEventListener('click', this.handleCopyCodeClick);
|
||||||
window.removeEventListener('popstate', this.handlePopState);
|
window.removeEventListener('popstate', this.handlePopState);
|
||||||
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
||||||
this.teardownMobileViewportWatcher();
|
this.teardownMobileViewportWatcher();
|
||||||
this.subAgentStopPolling();
|
this.subAgentStopPolling();
|
||||||
this.backgroundCommandStopPolling();
|
this.backgroundCommandStopPolling();
|
||||||
this.resourceStopContainerStatsPolling();
|
this.resourceStopContainerStatsPolling();
|
||||||
this.resourceStopProjectStoragePolling();
|
this.resourceStopProjectStoragePolling();
|
||||||
this.resourceStopUsageQuotaPolling();
|
this.resourceStopUsageQuotaPolling();
|
||||||
this.stopConnectionHeartbeat();
|
this.stopConnectionHeartbeat();
|
||||||
teardownShowImageObserver();
|
teardownShowImageObserver();
|
||||||
if (this.titleTypingTimer) {
|
if (this.titleTypingTimer) {
|
||||||
clearInterval(this.titleTypingTimer);
|
clearInterval(this.titleTypingTimer);
|
||||||
this.titleTypingTimer = null;
|
this.titleTypingTimer = null;
|
||||||
}
|
}
|
||||||
const cleanup = this.destroyEasterEggEffect(true);
|
const cleanup = this.destroyEasterEggEffect(true);
|
||||||
if (cleanup && typeof cleanup.catch === 'function') {
|
if (cleanup && typeof cleanup.catch === 'function') {
|
||||||
cleanup.catch(() => {});
|
cleanup.catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,19 +3,19 @@ const ENABLE_APP_DEBUG_LOGS = true;
|
|||||||
const TRACE_CONV = true;
|
const TRACE_CONV = true;
|
||||||
|
|
||||||
export function debugLog(...args) {
|
export function debugLog(...args) {
|
||||||
if (!ENABLE_APP_DEBUG_LOGS) return;
|
if (!ENABLE_APP_DEBUG_LOGS) return;
|
||||||
try {
|
try {
|
||||||
console.log('[app]', ...args);
|
console.log('[app]', ...args);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore logging errors */
|
/* ignore logging errors */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const traceLog = (...args) => {
|
export const traceLog = (...args) => {
|
||||||
if (!TRACE_CONV) return;
|
if (!TRACE_CONV) return;
|
||||||
try {
|
try {
|
||||||
console.log('[conv-trace]', ...args);
|
console.log('[conv-trace]', ...args);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,409 +5,412 @@ const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务
|
|||||||
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
|
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
|
||||||
|
|
||||||
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
||||||
const content = (rawContent || '').toString().trim();
|
const content = (rawContent || '').toString().trim();
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
const match = content.match(SUB_AGENT_DONE_PREFIX_RE);
|
const match = content.match(SUB_AGENT_DONE_PREFIX_RE);
|
||||||
if (!match || !/已完成/.test(content)) return null;
|
if (!match || !/已完成/.test(content)) return null;
|
||||||
return `子智能体${match[1]} 任务完成`;
|
return `子智能体${match[1]} 任务完成`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBackgroundRunCommandDoneLabel(rawContent: any): string | null {
|
function parseBackgroundRunCommandDoneLabel(rawContent: any): string | null {
|
||||||
const content = (rawContent || '').toString().trim();
|
const content = (rawContent || '').toString().trim();
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
if (!BG_RUN_COMMAND_DONE_PREFIX_RE.test(content)) return null;
|
if (!BG_RUN_COMMAND_DONE_PREFIX_RE.test(content)) return null;
|
||||||
return '后台 run_command 完成';
|
return '后台 run_command 完成';
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSystemNoticeLabel(rawContent: any): string | null {
|
function parseSystemNoticeLabel(rawContent: any): string | null {
|
||||||
return parseSubAgentDoneLabel(rawContent) || parseBackgroundRunCommandDoneLabel(rawContent);
|
return parseSubAgentDoneLabel(rawContent) || parseBackgroundRunCommandDoneLabel(rawContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const historyMethods = {
|
export const historyMethods = {
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 关键功能:获取并显示历史对话内容
|
// 关键功能:获取并显示历史对话内容
|
||||||
// ==========================================
|
// ==========================================
|
||||||
async fetchAndDisplayHistory(options = {}) {
|
async fetchAndDisplayHistory(options = {}) {
|
||||||
const { force = false } = options as { force?: boolean };
|
const { force = false } = options as { force?: boolean };
|
||||||
const targetConversationId = this.currentConversationId;
|
const targetConversationId = this.currentConversationId;
|
||||||
if (!targetConversationId || targetConversationId.startsWith('temp_')) {
|
if (!targetConversationId || targetConversationId.startsWith('temp_')) {
|
||||||
debugLog('没有当前对话ID,跳过历史加载');
|
debugLog('没有当前对话ID,跳过历史加载');
|
||||||
this.refreshBlankHeroState();
|
this.refreshBlankHeroState();
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// 若同一对话正在加载,直接复用;若是切换对话则允许并发但后来的请求会赢
|
|
||||||
if (this.historyLoading && this.historyLoadingFor === targetConversationId) {
|
|
||||||
debugLog('同一对话历史正在加载,跳过重复请求');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已经有完整历史且非强制刷新时,避免重复加载导致动画播放两次
|
|
||||||
const alreadyHydrated =
|
|
||||||
!force &&
|
|
||||||
this.lastHistoryLoadedConversationId === targetConversationId &&
|
|
||||||
Array.isArray(this.messages) &&
|
|
||||||
this.messages.length > 0;
|
|
||||||
if (alreadyHydrated) {
|
|
||||||
debugLog('历史已加载,跳过重复请求');
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:skip-duplicate', {
|
|
||||||
conversationId: targetConversationId
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSeq = ++this.historyLoadSeq;
|
|
||||||
this.historyLoading = true;
|
|
||||||
this.historyLoadingFor = targetConversationId;
|
|
||||||
try {
|
|
||||||
debugLog('开始获取历史对话内容...');
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用专门的API获取对话消息历史
|
|
||||||
const messagesResponse = await fetch(`/api/conversations/${targetConversationId}/messages`);
|
|
||||||
|
|
||||||
if (!messagesResponse.ok) {
|
|
||||||
console.warn('无法获取消息历史,尝试备用方法');
|
|
||||||
// 备用方案:通过状态API获取
|
|
||||||
const statusResponse = await fetch('/api/status');
|
|
||||||
const status = await statusResponse.json();
|
|
||||||
debugLog('系统状态:', status);
|
|
||||||
this.applyStatusSnapshot(status);
|
|
||||||
|
|
||||||
// 如果状态中有对话历史字段
|
|
||||||
if (status.conversation_history && Array.isArray(status.conversation_history)) {
|
|
||||||
this.renderHistoryMessages(status.conversation_history);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('备用方案也无法获取历史消息');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果在等待期间用户已切换到其他对话,则丢弃结果
|
|
||||||
if (loadSeq !== this.historyLoadSeq || this.currentConversationId !== targetConversationId) {
|
|
||||||
debugLog('检测到对话已切换,丢弃过期的历史加载结果');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messagesData = await messagesResponse.json();
|
|
||||||
debugLog('获取到消息数据:', messagesData);
|
|
||||||
|
|
||||||
if (messagesData.success && messagesData.data && messagesData.data.messages) {
|
|
||||||
const messages = messagesData.data.messages;
|
|
||||||
debugLog(`发现 ${messages.length} 条历史消息`);
|
|
||||||
|
|
||||||
if (messages.length > 0) {
|
|
||||||
// 清空当前显示的消息
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:before-clear-existing');
|
|
||||||
this.messages = [];
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:after-clear-existing');
|
|
||||||
|
|
||||||
// 渲染历史消息 - 这是关键功能
|
|
||||||
this.renderHistoryMessages(messages);
|
|
||||||
|
|
||||||
// 滚动到底部
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
debugLog('历史对话内容显示完成');
|
|
||||||
} else {
|
|
||||||
debugLog('对话存在但没有历史消息');
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
|
|
||||||
this.messages = [];
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debugLog('消息数据格式不正确:', messagesData);
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
|
|
||||||
this.messages = [];
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取历史对话失败:', error);
|
|
||||||
debugLog('尝试不显示错误弹窗,仅在控制台记录');
|
|
||||||
// 不显示alert,避免打断用户体验
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) });
|
|
||||||
this.messages = [];
|
|
||||||
this.logMessageState('fetchAndDisplayHistory:error-cleared');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// 仅在本次加载仍是最新请求时清除 loading 状态
|
|
||||||
if (loadSeq === this.historyLoadSeq) {
|
|
||||||
this.historyLoading = false;
|
|
||||||
this.historyLoadingFor = null;
|
|
||||||
}
|
|
||||||
this.refreshBlankHeroState();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 关键功能:渲染历史消息
|
|
||||||
// ==========================================
|
|
||||||
renderHistoryMessages(historyMessages) {
|
|
||||||
debugLog('开始渲染历史消息...', historyMessages);
|
|
||||||
debugLog('历史消息数量:', historyMessages.length);
|
|
||||||
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
|
|
||||||
|
|
||||||
if (!Array.isArray(historyMessages)) {
|
|
||||||
console.error('历史消息不是数组格式');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentAssistantMessage = null;
|
|
||||||
let historyHasImages = false;
|
|
||||||
let historyHasVideos = false;
|
|
||||||
|
|
||||||
historyMessages.forEach((message, index) => {
|
|
||||||
debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
|
|
||||||
const meta = message.metadata || {};
|
|
||||||
if (message.role === 'user' && (meta.system_injected_image || meta.system_injected_video)) {
|
|
||||||
debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.role === 'user') {
|
|
||||||
// 用户消息 - 先结束之前的assistant消息
|
|
||||||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
|
||||||
this.messages.push(currentAssistantMessage);
|
|
||||||
currentAssistantMessage = null;
|
|
||||||
}
|
|
||||||
const images = message.images || (message.metadata && message.metadata.images) || [];
|
|
||||||
const videos = message.videos || (message.metadata && message.metadata.videos) || [];
|
|
||||||
if (Array.isArray(images) && images.length) {
|
|
||||||
historyHasImages = true;
|
|
||||||
}
|
|
||||||
if (Array.isArray(videos) && videos.length) {
|
|
||||||
historyHasVideos = true;
|
|
||||||
}
|
|
||||||
this.messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: message.content || '',
|
|
||||||
images,
|
|
||||||
videos,
|
|
||||||
metadata: message.metadata || {}
|
|
||||||
});
|
|
||||||
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
|
|
||||||
|
|
||||||
} else if (message.role === 'assistant') {
|
|
||||||
// AI消息 - 如果没有当前assistant消息,创建一个
|
|
||||||
if (!currentAssistantMessage) {
|
|
||||||
currentAssistantMessage = {
|
|
||||||
role: 'assistant',
|
|
||||||
actions: [],
|
|
||||||
streamingThinking: '',
|
|
||||||
streamingText: '',
|
|
||||||
currentStreamingType: null,
|
|
||||||
activeThinkingId: null,
|
|
||||||
awaitingFirstContent: false, // 历史消息不应该显示等待动画
|
|
||||||
generatingLabel: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = message.content || '';
|
|
||||||
const reasoningText = (message.reasoning_content || '').trim();
|
|
||||||
|
|
||||||
if (reasoningText) {
|
|
||||||
const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
||||||
currentAssistantMessage.actions.push({
|
|
||||||
id: `history-think-${Date.now()}-${Math.random()}`,
|
|
||||||
type: 'thinking',
|
|
||||||
content: reasoningText,
|
|
||||||
streaming: false,
|
|
||||||
collapsed: true,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
blockId
|
|
||||||
});
|
|
||||||
debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理普通文本内容(移除思考标签后的内容)
|
|
||||||
const textContent = content.trim();
|
|
||||||
if (textContent) {
|
|
||||||
currentAssistantMessage.actions.push({
|
|
||||||
id: `history-text-${Date.now()}-${Math.random()}`,
|
|
||||||
type: 'text',
|
|
||||||
content: textContent,
|
|
||||||
streaming: false,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
debugLog('添加文本内容:', textContent.substring(0, 50) + '...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理工具调用
|
|
||||||
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
|
||||||
message.tool_calls.forEach((toolCall, tcIndex) => {
|
|
||||||
let arguments_obj = {};
|
|
||||||
try {
|
|
||||||
arguments_obj = typeof toolCall.function.arguments === 'string'
|
|
||||||
? JSON.parse(toolCall.function.arguments || '{}')
|
|
||||||
: (toolCall.function.arguments || {});
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('解析工具参数失败:', e);
|
|
||||||
arguments_obj = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
|
|
||||||
type: 'tool',
|
|
||||||
tool: {
|
|
||||||
id: toolCall.id,
|
|
||||||
name: toolCall.function.name,
|
|
||||||
arguments: arguments_obj,
|
|
||||||
argumentSnapshot: this.cloneToolArguments(arguments_obj),
|
|
||||||
argumentLabel: this.buildToolLabel(arguments_obj),
|
|
||||||
intent_full: arguments_obj.intent || '',
|
|
||||||
intent_rendered: arguments_obj.intent || '',
|
|
||||||
status: 'preparing',
|
|
||||||
result: null
|
|
||||||
},
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
// 如果是历史加载的动作且状态仍为进行中,标记为 stale,避免刷新后按钮卡死
|
|
||||||
if (['preparing', 'running', 'awaiting_content'].includes(action.tool.status)) {
|
|
||||||
action.tool.status = 'stale';
|
|
||||||
action.tool.awaiting_content = false;
|
|
||||||
action.streaming = false;
|
|
||||||
}
|
|
||||||
currentAssistantMessage.actions.push(action);
|
|
||||||
debugLog('添加工具调用:', toolCall.function.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (message.role === 'tool') {
|
|
||||||
// 工具结果 - 更新当前assistant消息中对应的工具
|
|
||||||
if (currentAssistantMessage) {
|
|
||||||
// 查找对应的工具action - 使用更灵活的匹配
|
|
||||||
let toolAction = null;
|
|
||||||
|
|
||||||
// 优先按tool_call_id匹配
|
|
||||||
if (message.tool_call_id) {
|
|
||||||
toolAction = currentAssistantMessage.actions.find(action =>
|
|
||||||
action.type === 'tool' &&
|
|
||||||
action.tool.id === message.tool_call_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果找不到,按name匹配最后一个同名工具
|
|
||||||
if (!toolAction && message.name) {
|
|
||||||
const sameNameTools = currentAssistantMessage.actions.filter(action =>
|
|
||||||
action.type === 'tool' &&
|
|
||||||
action.tool.name === message.name
|
|
||||||
);
|
|
||||||
toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolAction) {
|
|
||||||
// 解析工具结果(优先使用JSON,其次使用元数据的 tool_payload,以保证搜索结果在刷新后仍可展示)
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = JSON.parse(message.content);
|
|
||||||
} catch (e) {
|
|
||||||
if (message.metadata && message.metadata.tool_payload) {
|
|
||||||
result = message.metadata.tool_payload;
|
|
||||||
} else {
|
|
||||||
result = {
|
|
||||||
output: message.content,
|
|
||||||
success: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toolAction.tool.status = 'completed';
|
|
||||||
toolAction.tool.result = result;
|
|
||||||
if (result && typeof result === 'object') {
|
|
||||||
if (result.error) {
|
|
||||||
toolAction.tool.message = result.error;
|
|
||||||
} else if (result.message && !toolAction.tool.message) {
|
|
||||||
toolAction.tool.message = result.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
|
|
||||||
} else {
|
|
||||||
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// 其他类型消息(如system)- 先结束当前assistant消息
|
|
||||||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
|
||||||
this.messages.push(currentAssistantMessage);
|
|
||||||
currentAssistantMessage = null;
|
|
||||||
}
|
|
||||||
if (message.role === 'system') {
|
|
||||||
const rawContent = message.content || '';
|
|
||||||
const label = parseSystemNoticeLabel(rawContent);
|
|
||||||
console.log('[DEBUG_SYSTEM][history] 命中 role=system 历史消息', {
|
|
||||||
rawContent,
|
|
||||||
matchedDoneNotice: !!label
|
|
||||||
});
|
|
||||||
if (label) {
|
|
||||||
// 历史中的 system 通知转换为 assistant system action,避免被 role=system 过滤掉
|
|
||||||
this.messages.push({
|
|
||||||
role: 'assistant',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
id: `history-system-${Date.now()}-${Math.random()}`,
|
|
||||||
type: 'system',
|
|
||||||
content: label,
|
|
||||||
variant: 'sub_agent_done',
|
|
||||||
streaming: false,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
streamingThinking: '',
|
|
||||||
streamingText: '',
|
|
||||||
currentStreamingType: null,
|
|
||||||
activeThinkingId: null,
|
|
||||||
awaitingFirstContent: false,
|
|
||||||
generatingLabel: ''
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('[DEBUG_SYSTEM][history] role=system 被历史加载拦截(非子智能体完成)', {
|
|
||||||
rawContent
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('处理其他类型消息:', message.role);
|
|
||||||
this.messages.push({
|
|
||||||
role: message.role,
|
|
||||||
content: message.content || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理最后一个assistant消息
|
|
||||||
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
|
||||||
this.messages.push(currentAssistantMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.conversationHasImages = historyHasImages;
|
|
||||||
this.conversationHasVideos = historyHasVideos;
|
|
||||||
|
|
||||||
debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
|
|
||||||
this.logMessageState('renderHistoryMessages:after-render');
|
|
||||||
this.lastHistoryLoadedConversationId = this.currentConversationId || null;
|
|
||||||
|
|
||||||
// 强制更新视图
|
|
||||||
this.$forceUpdate();
|
|
||||||
|
|
||||||
// 确保滚动到底部
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
setTimeout(() => {
|
|
||||||
const blockCount = this.$el && this.$el.querySelectorAll
|
|
||||||
? this.$el.querySelectorAll('.message-block').length
|
|
||||||
: 'N/A';
|
|
||||||
debugLog('[Messages] DOM 渲染统计', {
|
|
||||||
blocks: blockCount,
|
|
||||||
conversationId: this.currentConversationId
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若同一对话正在加载,直接复用;若是切换对话则允许并发但后来的请求会赢
|
||||||
|
if (this.historyLoading && this.historyLoadingFor === targetConversationId) {
|
||||||
|
debugLog('同一对话历史正在加载,跳过重复请求');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已经有完整历史且非强制刷新时,避免重复加载导致动画播放两次
|
||||||
|
const alreadyHydrated =
|
||||||
|
!force &&
|
||||||
|
this.lastHistoryLoadedConversationId === targetConversationId &&
|
||||||
|
Array.isArray(this.messages) &&
|
||||||
|
this.messages.length > 0;
|
||||||
|
if (alreadyHydrated) {
|
||||||
|
debugLog('历史已加载,跳过重复请求');
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:skip-duplicate', {
|
||||||
|
conversationId: targetConversationId
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSeq = ++this.historyLoadSeq;
|
||||||
|
this.historyLoading = true;
|
||||||
|
this.historyLoadingFor = targetConversationId;
|
||||||
|
try {
|
||||||
|
debugLog('开始获取历史对话内容...');
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:start', {
|
||||||
|
conversationId: this.currentConversationId
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用专门的API获取对话消息历史
|
||||||
|
const messagesResponse = await fetch(`/api/conversations/${targetConversationId}/messages`);
|
||||||
|
|
||||||
|
if (!messagesResponse.ok) {
|
||||||
|
console.warn('无法获取消息历史,尝试备用方法');
|
||||||
|
// 备用方案:通过状态API获取
|
||||||
|
const statusResponse = await fetch('/api/status');
|
||||||
|
const status = await statusResponse.json();
|
||||||
|
debugLog('系统状态:', status);
|
||||||
|
this.applyStatusSnapshot(status);
|
||||||
|
|
||||||
|
// 如果状态中有对话历史字段
|
||||||
|
if (status.conversation_history && Array.isArray(status.conversation_history)) {
|
||||||
|
this.renderHistoryMessages(status.conversation_history);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('备用方案也无法获取历史消息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在等待期间用户已切换到其他对话,则丢弃结果
|
||||||
|
if (
|
||||||
|
loadSeq !== this.historyLoadSeq ||
|
||||||
|
this.currentConversationId !== targetConversationId
|
||||||
|
) {
|
||||||
|
debugLog('检测到对话已切换,丢弃过期的历史加载结果');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesData = await messagesResponse.json();
|
||||||
|
debugLog('获取到消息数据:', messagesData);
|
||||||
|
|
||||||
|
if (messagesData.success && messagesData.data && messagesData.data.messages) {
|
||||||
|
const messages = messagesData.data.messages;
|
||||||
|
debugLog(`发现 ${messages.length} 条历史消息`);
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
// 清空当前显示的消息
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:before-clear-existing');
|
||||||
|
this.messages = [];
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:after-clear-existing');
|
||||||
|
|
||||||
|
// 渲染历史消息 - 这是关键功能
|
||||||
|
this.renderHistoryMessages(messages);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
debugLog('历史对话内容显示完成');
|
||||||
|
} else {
|
||||||
|
debugLog('对话存在但没有历史消息');
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
|
||||||
|
this.messages = [];
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog('消息数据格式不正确:', messagesData);
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
|
||||||
|
this.messages = [];
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取历史对话失败:', error);
|
||||||
|
debugLog('尝试不显示错误弹窗,仅在控制台记录');
|
||||||
|
// 不显示alert,避免打断用户体验
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:error-clear', {
|
||||||
|
error: error?.message || String(error)
|
||||||
|
});
|
||||||
|
this.messages = [];
|
||||||
|
this.logMessageState('fetchAndDisplayHistory:error-cleared');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 仅在本次加载仍是最新请求时清除 loading 状态
|
||||||
|
if (loadSeq === this.historyLoadSeq) {
|
||||||
|
this.historyLoading = false;
|
||||||
|
this.historyLoadingFor = null;
|
||||||
|
}
|
||||||
|
this.refreshBlankHeroState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 关键功能:渲染历史消息
|
||||||
|
// ==========================================
|
||||||
|
renderHistoryMessages(historyMessages) {
|
||||||
|
debugLog('开始渲染历史消息...', historyMessages);
|
||||||
|
debugLog('历史消息数量:', historyMessages.length);
|
||||||
|
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
|
||||||
|
|
||||||
|
if (!Array.isArray(historyMessages)) {
|
||||||
|
console.error('历史消息不是数组格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentAssistantMessage = null;
|
||||||
|
let historyHasImages = false;
|
||||||
|
let historyHasVideos = false;
|
||||||
|
|
||||||
|
historyMessages.forEach((message, index) => {
|
||||||
|
debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
|
||||||
|
const meta = message.metadata || {};
|
||||||
|
if (message.role === 'user' && (meta.system_injected_image || meta.system_injected_video)) {
|
||||||
|
debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === 'user') {
|
||||||
|
// 用户消息 - 先结束之前的assistant消息
|
||||||
|
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||||||
|
this.messages.push(currentAssistantMessage);
|
||||||
|
currentAssistantMessage = null;
|
||||||
|
}
|
||||||
|
const images = message.images || (message.metadata && message.metadata.images) || [];
|
||||||
|
const videos = message.videos || (message.metadata && message.metadata.videos) || [];
|
||||||
|
if (Array.isArray(images) && images.length) {
|
||||||
|
historyHasImages = true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(videos) && videos.length) {
|
||||||
|
historyHasVideos = true;
|
||||||
|
}
|
||||||
|
this.messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: message.content || '',
|
||||||
|
images,
|
||||||
|
videos,
|
||||||
|
metadata: message.metadata || {}
|
||||||
|
});
|
||||||
|
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
|
||||||
|
} else if (message.role === 'assistant') {
|
||||||
|
// AI消息 - 如果没有当前assistant消息,创建一个
|
||||||
|
if (!currentAssistantMessage) {
|
||||||
|
currentAssistantMessage = {
|
||||||
|
role: 'assistant',
|
||||||
|
actions: [],
|
||||||
|
streamingThinking: '',
|
||||||
|
streamingText: '',
|
||||||
|
currentStreamingType: null,
|
||||||
|
activeThinkingId: null,
|
||||||
|
awaitingFirstContent: false, // 历史消息不应该显示等待动画
|
||||||
|
generatingLabel: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = message.content || '';
|
||||||
|
const reasoningText = (message.reasoning_content || '').trim();
|
||||||
|
|
||||||
|
if (reasoningText) {
|
||||||
|
const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
currentAssistantMessage.actions.push({
|
||||||
|
id: `history-think-${Date.now()}-${Math.random()}`,
|
||||||
|
type: 'thinking',
|
||||||
|
content: reasoningText,
|
||||||
|
streaming: false,
|
||||||
|
collapsed: true,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
blockId
|
||||||
|
});
|
||||||
|
debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理普通文本内容(移除思考标签后的内容)
|
||||||
|
const textContent = content.trim();
|
||||||
|
if (textContent) {
|
||||||
|
currentAssistantMessage.actions.push({
|
||||||
|
id: `history-text-${Date.now()}-${Math.random()}`,
|
||||||
|
type: 'text',
|
||||||
|
content: textContent,
|
||||||
|
streaming: false,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
debugLog('添加文本内容:', textContent.substring(0, 50) + '...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理工具调用
|
||||||
|
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
||||||
|
message.tool_calls.forEach((toolCall, tcIndex) => {
|
||||||
|
let arguments_obj = {};
|
||||||
|
try {
|
||||||
|
arguments_obj =
|
||||||
|
typeof toolCall.function.arguments === 'string'
|
||||||
|
? JSON.parse(toolCall.function.arguments || '{}')
|
||||||
|
: toolCall.function.arguments || {};
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析工具参数失败:', e);
|
||||||
|
arguments_obj = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = {
|
||||||
|
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
|
||||||
|
type: 'tool',
|
||||||
|
tool: {
|
||||||
|
id: toolCall.id,
|
||||||
|
name: toolCall.function.name,
|
||||||
|
arguments: arguments_obj,
|
||||||
|
argumentSnapshot: this.cloneToolArguments(arguments_obj),
|
||||||
|
argumentLabel: this.buildToolLabel(arguments_obj),
|
||||||
|
intent_full: arguments_obj.intent || '',
|
||||||
|
intent_rendered: arguments_obj.intent || '',
|
||||||
|
status: 'preparing',
|
||||||
|
result: null
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
// 如果是历史加载的动作且状态仍为进行中,标记为 stale,避免刷新后按钮卡死
|
||||||
|
if (['preparing', 'running', 'awaiting_content'].includes(action.tool.status)) {
|
||||||
|
action.tool.status = 'stale';
|
||||||
|
action.tool.awaiting_content = false;
|
||||||
|
action.streaming = false;
|
||||||
|
}
|
||||||
|
currentAssistantMessage.actions.push(action);
|
||||||
|
debugLog('添加工具调用:', toolCall.function.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (message.role === 'tool') {
|
||||||
|
// 工具结果 - 更新当前assistant消息中对应的工具
|
||||||
|
if (currentAssistantMessage) {
|
||||||
|
// 查找对应的工具action - 使用更灵活的匹配
|
||||||
|
let toolAction = null;
|
||||||
|
|
||||||
|
// 优先按tool_call_id匹配
|
||||||
|
if (message.tool_call_id) {
|
||||||
|
toolAction = currentAssistantMessage.actions.find(
|
||||||
|
(action) => action.type === 'tool' && action.tool.id === message.tool_call_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到,按name匹配最后一个同名工具
|
||||||
|
if (!toolAction && message.name) {
|
||||||
|
const sameNameTools = currentAssistantMessage.actions.filter(
|
||||||
|
(action) => action.type === 'tool' && action.tool.name === message.name
|
||||||
|
);
|
||||||
|
toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolAction) {
|
||||||
|
// 解析工具结果(优先使用JSON,其次使用元数据的 tool_payload,以保证搜索结果在刷新后仍可展示)
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = JSON.parse(message.content);
|
||||||
|
} catch (e) {
|
||||||
|
if (message.metadata && message.metadata.tool_payload) {
|
||||||
|
result = message.metadata.tool_payload;
|
||||||
|
} else {
|
||||||
|
result = {
|
||||||
|
output: message.content,
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolAction.tool.status = 'completed';
|
||||||
|
toolAction.tool.result = result;
|
||||||
|
if (result && typeof result === 'object') {
|
||||||
|
if (result.error) {
|
||||||
|
toolAction.tool.message = result.error;
|
||||||
|
} else if (result.message && !toolAction.tool.message) {
|
||||||
|
toolAction.tool.message = result.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
|
||||||
|
} else {
|
||||||
|
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他类型消息(如system)- 先结束当前assistant消息
|
||||||
|
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||||||
|
this.messages.push(currentAssistantMessage);
|
||||||
|
currentAssistantMessage = null;
|
||||||
|
}
|
||||||
|
if (message.role === 'system') {
|
||||||
|
const rawContent = message.content || '';
|
||||||
|
const label = parseSystemNoticeLabel(rawContent);
|
||||||
|
console.log('[DEBUG_SYSTEM][history] 命中 role=system 历史消息', {
|
||||||
|
rawContent,
|
||||||
|
matchedDoneNotice: !!label
|
||||||
|
});
|
||||||
|
if (label) {
|
||||||
|
// 历史中的 system 通知转换为 assistant system action,避免被 role=system 过滤掉
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: `history-system-${Date.now()}-${Math.random()}`,
|
||||||
|
type: 'system',
|
||||||
|
content: label,
|
||||||
|
variant: 'sub_agent_done',
|
||||||
|
streaming: false,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
streamingThinking: '',
|
||||||
|
streamingText: '',
|
||||||
|
currentStreamingType: null,
|
||||||
|
activeThinkingId: null,
|
||||||
|
awaitingFirstContent: false,
|
||||||
|
generatingLabel: ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG_SYSTEM][history] role=system 被历史加载拦截(非子智能体完成)', {
|
||||||
|
rawContent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('处理其他类型消息:', message.role);
|
||||||
|
this.messages.push({
|
||||||
|
role: message.role,
|
||||||
|
content: message.content || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理最后一个assistant消息
|
||||||
|
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
|
||||||
|
this.messages.push(currentAssistantMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conversationHasImages = historyHasImages;
|
||||||
|
this.conversationHasVideos = historyHasVideos;
|
||||||
|
|
||||||
|
debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
|
||||||
|
this.logMessageState('renderHistoryMessages:after-render');
|
||||||
|
this.lastHistoryLoadedConversationId = this.currentConversationId || null;
|
||||||
|
|
||||||
|
// 强制更新视图
|
||||||
|
this.$forceUpdate();
|
||||||
|
|
||||||
|
// 确保滚动到底部
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
|
setTimeout(() => {
|
||||||
|
const blockCount =
|
||||||
|
this.$el && this.$el.querySelectorAll
|
||||||
|
? this.$el.querySelectorAll('.message-block').length
|
||||||
|
: 'N/A';
|
||||||
|
debugLog('[Messages] DOM 渲染统计', {
|
||||||
|
blocks: blockCount,
|
||||||
|
conversationId: this.currentConversationId
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,23 +2,23 @@
|
|||||||
import { useEasterEgg } from '../../composables/useEasterEgg';
|
import { useEasterEgg } from '../../composables/useEasterEgg';
|
||||||
|
|
||||||
export const monitorMethods = {
|
export const monitorMethods = {
|
||||||
async handleEasterEggPayload(payload) {
|
async handleEasterEggPayload(payload) {
|
||||||
const controller = useEasterEgg();
|
const controller = useEasterEgg();
|
||||||
await controller.handlePayload(payload, this);
|
await controller.handlePayload(payload, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
async startEasterEggEffect(effectName, payload = {}) {
|
async startEasterEggEffect(effectName, payload = {}) {
|
||||||
const controller = useEasterEgg();
|
const controller = useEasterEgg();
|
||||||
await controller.startEffect(effectName, payload, this);
|
await controller.startEffect(effectName, payload, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyEasterEggEffect(forceImmediate = false) {
|
destroyEasterEggEffect(forceImmediate = false) {
|
||||||
const controller = useEasterEgg();
|
const controller = useEasterEgg();
|
||||||
return controller.destroyEffect(forceImmediate);
|
return controller.destroyEffect(forceImmediate);
|
||||||
},
|
},
|
||||||
|
|
||||||
finishEasterEggCleanup() {
|
finishEasterEggCleanup() {
|
||||||
const controller = useEasterEgg();
|
const controller = useEasterEgg();
|
||||||
controller.finishCleanup();
|
controller.finishCleanup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,288 +1,288 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import {
|
import {
|
||||||
formatTokenCount,
|
formatTokenCount,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
formatPercentage,
|
formatPercentage,
|
||||||
formatRate,
|
formatRate,
|
||||||
formatResetTime,
|
formatResetTime,
|
||||||
formatQuotaValue,
|
formatQuotaValue,
|
||||||
quotaTypeLabel,
|
quotaTypeLabel,
|
||||||
buildQuotaResetSummary,
|
buildQuotaResetSummary,
|
||||||
isQuotaExceeded as isQuotaExceededUtil,
|
isQuotaExceeded as isQuotaExceededUtil,
|
||||||
buildQuotaToastMessage
|
buildQuotaToastMessage
|
||||||
} from '../../utils/formatters';
|
} from '../../utils/formatters';
|
||||||
|
|
||||||
export const resourceMethods = {
|
export const resourceMethods = {
|
||||||
hasContainerStats() {
|
hasContainerStats() {
|
||||||
return !!(
|
return !!(
|
||||||
this.containerStatus &&
|
this.containerStatus &&
|
||||||
this.containerStatus.mode === 'docker' &&
|
this.containerStatus.mode === 'docker' &&
|
||||||
this.containerStatus.stats
|
this.containerStatus.stats
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
containerStatusClass() {
|
containerStatusClass() {
|
||||||
if (!this.containerStatus) {
|
if (!this.containerStatus) {
|
||||||
return 'status-pill--host';
|
return 'status-pill--host';
|
||||||
}
|
|
||||||
if (this.containerStatus.mode !== 'docker') {
|
|
||||||
return 'status-pill--host';
|
|
||||||
}
|
|
||||||
const rawStatus = (
|
|
||||||
this.containerStatus.state &&
|
|
||||||
(this.containerStatus.state.status || this.containerStatus.state.Status)
|
|
||||||
) || '';
|
|
||||||
const status = String(rawStatus).toLowerCase();
|
|
||||||
if (status.includes('running')) {
|
|
||||||
return 'status-pill--running';
|
|
||||||
}
|
|
||||||
if (status.includes('paused')) {
|
|
||||||
return 'status-pill--stopped';
|
|
||||||
}
|
|
||||||
if (status.includes('exited') || status.includes('dead')) {
|
|
||||||
return 'status-pill--stopped';
|
|
||||||
}
|
|
||||||
return 'status-pill--running';
|
|
||||||
},
|
|
||||||
|
|
||||||
containerStatusText() {
|
|
||||||
if (!this.containerStatus) {
|
|
||||||
return '未知';
|
|
||||||
}
|
|
||||||
if (this.containerStatus.mode !== 'docker') {
|
|
||||||
return '宿主机模式';
|
|
||||||
}
|
|
||||||
const rawStatus = (
|
|
||||||
this.containerStatus.state &&
|
|
||||||
(this.containerStatus.state.status || this.containerStatus.state.Status)
|
|
||||||
) || '';
|
|
||||||
const status = String(rawStatus).toLowerCase();
|
|
||||||
if (status.includes('running')) {
|
|
||||||
return '运行中';
|
|
||||||
}
|
|
||||||
if (status.includes('paused')) {
|
|
||||||
return '已暂停';
|
|
||||||
}
|
|
||||||
if (status.includes('exited') || status.includes('dead')) {
|
|
||||||
return '已停止';
|
|
||||||
}
|
|
||||||
return rawStatus || '容器模式';
|
|
||||||
},
|
|
||||||
|
|
||||||
formatTime(value) {
|
|
||||||
if (!value) {
|
|
||||||
return '未知时间';
|
|
||||||
}
|
|
||||||
let date;
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
date = new Date(value);
|
|
||||||
} else if (typeof value === 'string') {
|
|
||||||
const parsed = Date.parse(value);
|
|
||||||
if (!Number.isNaN(parsed)) {
|
|
||||||
date = new Date(parsed);
|
|
||||||
} else {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isNaN(numeric)) {
|
|
||||||
date = new Date(numeric);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (value instanceof Date) {
|
|
||||||
date = value;
|
|
||||||
}
|
|
||||||
if (!date || Number.isNaN(date.getTime())) {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
const now = Date.now();
|
|
||||||
const diff = now - date.getTime();
|
|
||||||
if (diff < 60000) {
|
|
||||||
return '刚刚';
|
|
||||||
}
|
|
||||||
if (diff < 3600000) {
|
|
||||||
const mins = Math.floor(diff / 60000);
|
|
||||||
return `${mins} 分钟前`;
|
|
||||||
}
|
|
||||||
if (diff < 86400000) {
|
|
||||||
const hours = Math.floor(diff / 3600000);
|
|
||||||
return `${hours} 小时前`;
|
|
||||||
}
|
|
||||||
const formatter = new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
return formatter.format(date);
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateCurrentContextTokens() {
|
|
||||||
await this.resourceUpdateCurrentContextTokens(this.currentConversationId);
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchConversationTokenStatistics() {
|
|
||||||
await this.resourceFetchConversationTokenStatistics(this.currentConversationId);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleTokenPanel() {
|
|
||||||
this.resourceToggleTokenPanel();
|
|
||||||
},
|
|
||||||
|
|
||||||
applyStatusSnapshot(status) {
|
|
||||||
this.resourceApplyStatusSnapshot(status);
|
|
||||||
if (status && typeof status.thinking_mode !== 'undefined') {
|
|
||||||
this.thinkingMode = !!status.thinking_mode;
|
|
||||||
}
|
|
||||||
if (status && typeof status.run_mode === 'string') {
|
|
||||||
this.runMode = status.run_mode;
|
|
||||||
} else if (status && typeof status.thinking_mode !== 'undefined') {
|
|
||||||
this.runMode = status.thinking_mode ? 'thinking' : 'fast';
|
|
||||||
}
|
|
||||||
if (status && typeof status.model_key === 'string') {
|
|
||||||
this.modelSet(status.model_key);
|
|
||||||
}
|
|
||||||
if (status && typeof status.has_images !== 'undefined') {
|
|
||||||
this.conversationHasImages = !!status.has_images;
|
|
||||||
}
|
|
||||||
if (status && typeof status.has_videos !== 'undefined') {
|
|
||||||
this.conversationHasVideos = !!status.has_videos;
|
|
||||||
}
|
|
||||||
const compression = status?.conversation?.compression;
|
|
||||||
if (compression && typeof compression === 'object') {
|
|
||||||
this.compressionInProgress = !!compression.in_progress;
|
|
||||||
this.compressionMode = compression.mode || '';
|
|
||||||
this.compressionStage = compression.stage || '';
|
|
||||||
this.compressionError = compression.error || '';
|
|
||||||
} else {
|
|
||||||
this.compressionInProgress = false;
|
|
||||||
this.compressionMode = '';
|
|
||||||
this.compressionStage = '';
|
|
||||||
this.compressionError = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateContainerStatus(status) {
|
|
||||||
this.resourceUpdateContainerStatus(status);
|
|
||||||
},
|
|
||||||
|
|
||||||
pollContainerStats() {
|
|
||||||
return this.resourcePollContainerStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
startContainerStatsPolling() {
|
|
||||||
this.resourceStartContainerStatsPolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
stopContainerStatsPolling() {
|
|
||||||
this.resourceStopContainerStatsPolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
pollProjectStorage() {
|
|
||||||
return this.resourcePollProjectStorage();
|
|
||||||
},
|
|
||||||
|
|
||||||
startProjectStoragePolling() {
|
|
||||||
this.resourceStartProjectStoragePolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
stopProjectStoragePolling() {
|
|
||||||
this.resourceStopProjectStoragePolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchUsageQuota() {
|
|
||||||
return this.resourceFetchUsageQuota();
|
|
||||||
},
|
|
||||||
|
|
||||||
startUsageQuotaPolling() {
|
|
||||||
this.resourceStartUsageQuotaPolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
stopUsageQuotaPolling() {
|
|
||||||
this.resourceStopUsageQuotaPolling();
|
|
||||||
},
|
|
||||||
|
|
||||||
async downloadFile(path) {
|
|
||||||
if (!path) {
|
|
||||||
this.fileHideContextMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = `/api/download/file?path=${encodeURIComponent(path)}`;
|
|
||||||
const name = path.split('/').pop() || 'file';
|
|
||||||
await this.downloadResource(url, name);
|
|
||||||
},
|
|
||||||
|
|
||||||
async downloadFolder(path) {
|
|
||||||
if (!path) {
|
|
||||||
this.fileHideContextMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = `/api/download/folder?path=${encodeURIComponent(path)}`;
|
|
||||||
const segments = path.split('/').filter(Boolean);
|
|
||||||
const folderName = segments.length ? segments.pop() : 'folder';
|
|
||||||
await this.downloadResource(url, `${folderName}.zip`);
|
|
||||||
},
|
|
||||||
|
|
||||||
async downloadResource(url, filename) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
let message = response.statusText;
|
|
||||||
try {
|
|
||||||
const errorData = await response.json();
|
|
||||||
message = errorData.error || errorData.message || message;
|
|
||||||
} catch (err) {
|
|
||||||
message = await response.text();
|
|
||||||
}
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '下载失败',
|
|
||||||
message: message || '无法完成下载',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob();
|
|
||||||
const downloadName = filename || 'download';
|
|
||||||
const link = document.createElement('a');
|
|
||||||
const href = URL.createObjectURL(blob);
|
|
||||||
link.href = href;
|
|
||||||
link.download = downloadName;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(href);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下载失败:', error);
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '下载失败',
|
|
||||||
message: error.message || String(error),
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.fileHideContextMenu();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
formatTokenCount,
|
|
||||||
formatBytes,
|
|
||||||
formatPercentage,
|
|
||||||
formatRate,
|
|
||||||
formatResetTime,
|
|
||||||
formatQuotaValue,
|
|
||||||
quotaTypeLabel,
|
|
||||||
|
|
||||||
quotaResetSummary() {
|
|
||||||
return buildQuotaResetSummary(this.usageQuota);
|
|
||||||
},
|
|
||||||
|
|
||||||
isQuotaExceeded(type) {
|
|
||||||
return isQuotaExceededUtil(this.usageQuota, type);
|
|
||||||
},
|
|
||||||
|
|
||||||
showQuotaToast(payload) {
|
|
||||||
if (!payload) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const type = payload.type || 'fast';
|
|
||||||
const message = buildQuotaToastMessage(type, this.usageQuota, payload.reset_at);
|
|
||||||
this.uiShowQuotaToastMessage(message, type);
|
|
||||||
}
|
}
|
||||||
|
if (this.containerStatus.mode !== 'docker') {
|
||||||
|
return 'status-pill--host';
|
||||||
|
}
|
||||||
|
const rawStatus =
|
||||||
|
(this.containerStatus.state &&
|
||||||
|
(this.containerStatus.state.status || this.containerStatus.state.Status)) ||
|
||||||
|
'';
|
||||||
|
const status = String(rawStatus).toLowerCase();
|
||||||
|
if (status.includes('running')) {
|
||||||
|
return 'status-pill--running';
|
||||||
|
}
|
||||||
|
if (status.includes('paused')) {
|
||||||
|
return 'status-pill--stopped';
|
||||||
|
}
|
||||||
|
if (status.includes('exited') || status.includes('dead')) {
|
||||||
|
return 'status-pill--stopped';
|
||||||
|
}
|
||||||
|
return 'status-pill--running';
|
||||||
|
},
|
||||||
|
|
||||||
|
containerStatusText() {
|
||||||
|
if (!this.containerStatus) {
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
if (this.containerStatus.mode !== 'docker') {
|
||||||
|
return '宿主机模式';
|
||||||
|
}
|
||||||
|
const rawStatus =
|
||||||
|
(this.containerStatus.state &&
|
||||||
|
(this.containerStatus.state.status || this.containerStatus.state.Status)) ||
|
||||||
|
'';
|
||||||
|
const status = String(rawStatus).toLowerCase();
|
||||||
|
if (status.includes('running')) {
|
||||||
|
return '运行中';
|
||||||
|
}
|
||||||
|
if (status.includes('paused')) {
|
||||||
|
return '已暂停';
|
||||||
|
}
|
||||||
|
if (status.includes('exited') || status.includes('dead')) {
|
||||||
|
return '已停止';
|
||||||
|
}
|
||||||
|
return rawStatus || '容器模式';
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '未知时间';
|
||||||
|
}
|
||||||
|
let date;
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
date = new Date(value);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
const parsed = Date.parse(value);
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
date = new Date(parsed);
|
||||||
|
} else {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isNaN(numeric)) {
|
||||||
|
date = new Date(numeric);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value instanceof Date) {
|
||||||
|
date = value;
|
||||||
|
}
|
||||||
|
if (!date || Number.isNaN(date.getTime())) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - date.getTime();
|
||||||
|
if (diff < 60000) {
|
||||||
|
return '刚刚';
|
||||||
|
}
|
||||||
|
if (diff < 3600000) {
|
||||||
|
const mins = Math.floor(diff / 60000);
|
||||||
|
return `${mins} 分钟前`;
|
||||||
|
}
|
||||||
|
if (diff < 86400000) {
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
return `${hours} 小时前`;
|
||||||
|
}
|
||||||
|
const formatter = new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
return formatter.format(date);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateCurrentContextTokens() {
|
||||||
|
await this.resourceUpdateCurrentContextTokens(this.currentConversationId);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchConversationTokenStatistics() {
|
||||||
|
await this.resourceFetchConversationTokenStatistics(this.currentConversationId);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTokenPanel() {
|
||||||
|
this.resourceToggleTokenPanel();
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStatusSnapshot(status) {
|
||||||
|
this.resourceApplyStatusSnapshot(status);
|
||||||
|
if (status && typeof status.thinking_mode !== 'undefined') {
|
||||||
|
this.thinkingMode = !!status.thinking_mode;
|
||||||
|
}
|
||||||
|
if (status && typeof status.run_mode === 'string') {
|
||||||
|
this.runMode = status.run_mode;
|
||||||
|
} else if (status && typeof status.thinking_mode !== 'undefined') {
|
||||||
|
this.runMode = status.thinking_mode ? 'thinking' : 'fast';
|
||||||
|
}
|
||||||
|
if (status && typeof status.model_key === 'string') {
|
||||||
|
this.modelSet(status.model_key);
|
||||||
|
}
|
||||||
|
if (status && typeof status.has_images !== 'undefined') {
|
||||||
|
this.conversationHasImages = !!status.has_images;
|
||||||
|
}
|
||||||
|
if (status && typeof status.has_videos !== 'undefined') {
|
||||||
|
this.conversationHasVideos = !!status.has_videos;
|
||||||
|
}
|
||||||
|
const compression = status?.conversation?.compression;
|
||||||
|
if (compression && typeof compression === 'object') {
|
||||||
|
this.compressionInProgress = !!compression.in_progress;
|
||||||
|
this.compressionMode = compression.mode || '';
|
||||||
|
this.compressionStage = compression.stage || '';
|
||||||
|
this.compressionError = compression.error || '';
|
||||||
|
} else {
|
||||||
|
this.compressionInProgress = false;
|
||||||
|
this.compressionMode = '';
|
||||||
|
this.compressionStage = '';
|
||||||
|
this.compressionError = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateContainerStatus(status) {
|
||||||
|
this.resourceUpdateContainerStatus(status);
|
||||||
|
},
|
||||||
|
|
||||||
|
pollContainerStats() {
|
||||||
|
return this.resourcePollContainerStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
startContainerStatsPolling() {
|
||||||
|
this.resourceStartContainerStatsPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopContainerStatsPolling() {
|
||||||
|
this.resourceStopContainerStatsPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
pollProjectStorage() {
|
||||||
|
return this.resourcePollProjectStorage();
|
||||||
|
},
|
||||||
|
|
||||||
|
startProjectStoragePolling() {
|
||||||
|
this.resourceStartProjectStoragePolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopProjectStoragePolling() {
|
||||||
|
this.resourceStopProjectStoragePolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchUsageQuota() {
|
||||||
|
return this.resourceFetchUsageQuota();
|
||||||
|
},
|
||||||
|
|
||||||
|
startUsageQuotaPolling() {
|
||||||
|
this.resourceStartUsageQuotaPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopUsageQuotaPolling() {
|
||||||
|
this.resourceStopUsageQuotaPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadFile(path) {
|
||||||
|
if (!path) {
|
||||||
|
this.fileHideContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `/api/download/file?path=${encodeURIComponent(path)}`;
|
||||||
|
const name = path.split('/').pop() || 'file';
|
||||||
|
await this.downloadResource(url, name);
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadFolder(path) {
|
||||||
|
if (!path) {
|
||||||
|
this.fileHideContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `/api/download/folder?path=${encodeURIComponent(path)}`;
|
||||||
|
const segments = path.split('/').filter(Boolean);
|
||||||
|
const folderName = segments.length ? segments.pop() : 'folder';
|
||||||
|
await this.downloadResource(url, `${folderName}.zip`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadResource(url, filename) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = response.statusText;
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
message = errorData.error || errorData.message || message;
|
||||||
|
} catch (err) {
|
||||||
|
message = await response.text();
|
||||||
|
}
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '下载失败',
|
||||||
|
message: message || '无法完成下载',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const downloadName = filename || 'download';
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const href = URL.createObjectURL(blob);
|
||||||
|
link.href = href;
|
||||||
|
link.download = downloadName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(href);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error);
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '下载失败',
|
||||||
|
message: error.message || String(error),
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.fileHideContextMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTokenCount,
|
||||||
|
formatBytes,
|
||||||
|
formatPercentage,
|
||||||
|
formatRate,
|
||||||
|
formatResetTime,
|
||||||
|
formatQuotaValue,
|
||||||
|
quotaTypeLabel,
|
||||||
|
|
||||||
|
quotaResetSummary() {
|
||||||
|
return buildQuotaResetSummary(this.usageQuota);
|
||||||
|
},
|
||||||
|
|
||||||
|
isQuotaExceeded(type) {
|
||||||
|
return isQuotaExceededUtil(this.usageQuota, type);
|
||||||
|
},
|
||||||
|
|
||||||
|
showQuotaToast(payload) {
|
||||||
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type = payload.type || 'fast';
|
||||||
|
const message = buildQuotaToastMessage(type, this.usageQuota, payload.reset_at);
|
||||||
|
this.uiShowQuotaToastMessage(message, type);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,167 +4,174 @@ const SEARCH_MORE_BATCH = 200;
|
|||||||
const SEARCH_PREVIEW_LIMIT = 20;
|
const SEARCH_PREVIEW_LIMIT = 20;
|
||||||
|
|
||||||
export const searchMethods = {
|
export const searchMethods = {
|
||||||
handleSidebarSearchInput(value) {
|
handleSidebarSearchInput(value) {
|
||||||
this.searchQuery = value;
|
this.searchQuery = value;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSidebarSearchSubmit(value) {
|
handleSidebarSearchSubmit(value) {
|
||||||
this.searchQuery = value;
|
this.searchQuery = value;
|
||||||
const trimmed = String(value || '').trim();
|
const trimmed = String(value || '').trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
this.exitConversationSearch();
|
this.exitConversationSearch();
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
this.startConversationSearch(trimmed);
|
|
||||||
},
|
|
||||||
|
|
||||||
exitConversationSearch() {
|
|
||||||
this.searchActive = false;
|
|
||||||
this.searchInProgress = false;
|
|
||||||
this.searchMoreAvailable = false;
|
|
||||||
this.searchOffset = 0;
|
|
||||||
this.searchTotal = 0;
|
|
||||||
this.searchResults = [];
|
|
||||||
this.searchActiveQuery = '';
|
|
||||||
this.searchResultIdSet = new Set();
|
|
||||||
this.conversationsOffset = 0;
|
|
||||||
this.loadConversationsList();
|
|
||||||
},
|
|
||||||
|
|
||||||
async startConversationSearch(query) {
|
|
||||||
const trimmed = String(query || '').trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const requestSeq = ++this.searchRequestSeq;
|
|
||||||
this.searchActiveQuery = trimmed;
|
|
||||||
this.searchActive = true;
|
|
||||||
this.searchInProgress = true;
|
|
||||||
this.searchMoreAvailable = false;
|
|
||||||
this.searchOffset = 0;
|
|
||||||
this.searchTotal = 0;
|
|
||||||
this.searchResults = [];
|
|
||||||
this.searchResultIdSet = new Set();
|
|
||||||
await this.searchNextConversationBatch(SEARCH_INITIAL_BATCH, requestSeq);
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadMoreSearchResults() {
|
|
||||||
if (!this.searchActive || this.searchInProgress || !this.searchMoreAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const requestSeq = this.searchRequestSeq;
|
|
||||||
this.searchInProgress = true;
|
|
||||||
await this.searchNextConversationBatch(SEARCH_MORE_BATCH, requestSeq);
|
|
||||||
},
|
|
||||||
|
|
||||||
async searchNextConversationBatch(batchSize, requestSeq) {
|
|
||||||
const query = this.searchActiveQuery;
|
|
||||||
if (!query) {
|
|
||||||
if (requestSeq === this.searchRequestSeq) {
|
|
||||||
this.searchInProgress = false;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/conversations?limit=${batchSize}&offset=${this.searchOffset}`);
|
|
||||||
const payload = await response.json();
|
|
||||||
|
|
||||||
if (requestSeq !== this.searchRequestSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload.success) {
|
|
||||||
console.error('搜索对话失败:', payload.error || payload.message);
|
|
||||||
this.searchInProgress = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = payload.data || {};
|
|
||||||
const conversations = data.conversations || [];
|
|
||||||
if (!this.searchTotal) {
|
|
||||||
this.searchTotal = data.total || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const conv of conversations) {
|
|
||||||
if (requestSeq !== this.searchRequestSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.matchConversation(conv, query, requestSeq);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchOffset += conversations.length;
|
|
||||||
this.searchMoreAvailable = this.searchOffset < (this.searchTotal || 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索对话异常:', error);
|
|
||||||
} finally {
|
|
||||||
if (requestSeq === this.searchRequestSeq) {
|
|
||||||
this.searchInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async matchConversation(conversation, query, requestSeq) {
|
|
||||||
if (!conversation || !conversation.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.searchResultIdSet && this.searchResultIdSet.has(conversation.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstSentence = await this.getConversationFirstUserSentence(conversation.id, requestSeq);
|
|
||||||
if (requestSeq !== this.searchRequestSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const queryLower = String(query || '').toLowerCase();
|
|
||||||
const combined = `${conversation.title || ''} ${firstSentence || ''}`.toLowerCase();
|
|
||||||
if (queryLower && combined.includes(queryLower)) {
|
|
||||||
this.searchResults.push(conversation);
|
|
||||||
this.searchResultIdSet.add(conversation.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getConversationFirstUserSentence(conversationId, requestSeq) {
|
|
||||||
if (!conversationId) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (this.searchPreviewCache && Object.prototype.hasOwnProperty.call(this.searchPreviewCache, conversationId)) {
|
|
||||||
return this.searchPreviewCache[conversationId];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${SEARCH_PREVIEW_LIMIT}`);
|
|
||||||
const payload = await resp.json();
|
|
||||||
if (requestSeq !== this.searchRequestSeq) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const lines = payload?.data?.preview || [];
|
|
||||||
let firstUserLine = '';
|
|
||||||
for (const line of lines) {
|
|
||||||
if (typeof line === 'string' && line.startsWith('user:')) {
|
|
||||||
firstUserLine = line.slice('user:'.length).trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const firstSentence = this.extractFirstSentence(firstUserLine);
|
|
||||||
const cached = firstSentence || firstUserLine || '';
|
|
||||||
if (!this.searchPreviewCache) {
|
|
||||||
this.searchPreviewCache = {};
|
|
||||||
}
|
|
||||||
this.searchPreviewCache[conversationId] = cached;
|
|
||||||
return cached;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取对话预览失败:', error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
extractFirstSentence(text) {
|
|
||||||
if (!text) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const normalized = String(text).replace(/\s+/g, ' ').trim();
|
|
||||||
const match = normalized.match(/(.+?[。!?.!?])/);
|
|
||||||
if (match) {
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
this.startConversationSearch(trimmed);
|
||||||
|
},
|
||||||
|
|
||||||
|
exitConversationSearch() {
|
||||||
|
this.searchActive = false;
|
||||||
|
this.searchInProgress = false;
|
||||||
|
this.searchMoreAvailable = false;
|
||||||
|
this.searchOffset = 0;
|
||||||
|
this.searchTotal = 0;
|
||||||
|
this.searchResults = [];
|
||||||
|
this.searchActiveQuery = '';
|
||||||
|
this.searchResultIdSet = new Set();
|
||||||
|
this.conversationsOffset = 0;
|
||||||
|
this.loadConversationsList();
|
||||||
|
},
|
||||||
|
|
||||||
|
async startConversationSearch(query) {
|
||||||
|
const trimmed = String(query || '').trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const requestSeq = ++this.searchRequestSeq;
|
||||||
|
this.searchActiveQuery = trimmed;
|
||||||
|
this.searchActive = true;
|
||||||
|
this.searchInProgress = true;
|
||||||
|
this.searchMoreAvailable = false;
|
||||||
|
this.searchOffset = 0;
|
||||||
|
this.searchTotal = 0;
|
||||||
|
this.searchResults = [];
|
||||||
|
this.searchResultIdSet = new Set();
|
||||||
|
await this.searchNextConversationBatch(SEARCH_INITIAL_BATCH, requestSeq);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadMoreSearchResults() {
|
||||||
|
if (!this.searchActive || this.searchInProgress || !this.searchMoreAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const requestSeq = this.searchRequestSeq;
|
||||||
|
this.searchInProgress = true;
|
||||||
|
await this.searchNextConversationBatch(SEARCH_MORE_BATCH, requestSeq);
|
||||||
|
},
|
||||||
|
|
||||||
|
async searchNextConversationBatch(batchSize, requestSeq) {
|
||||||
|
const query = this.searchActiveQuery;
|
||||||
|
if (!query) {
|
||||||
|
if (requestSeq === this.searchRequestSeq) {
|
||||||
|
this.searchInProgress = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/conversations?limit=${batchSize}&offset=${this.searchOffset}`
|
||||||
|
);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
if (requestSeq !== this.searchRequestSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.success) {
|
||||||
|
console.error('搜索对话失败:', payload.error || payload.message);
|
||||||
|
this.searchInProgress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = payload.data || {};
|
||||||
|
const conversations = data.conversations || [];
|
||||||
|
if (!this.searchTotal) {
|
||||||
|
this.searchTotal = data.total || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const conv of conversations) {
|
||||||
|
if (requestSeq !== this.searchRequestSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.matchConversation(conv, query, requestSeq);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchOffset += conversations.length;
|
||||||
|
this.searchMoreAvailable = this.searchOffset < (this.searchTotal || 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索对话异常:', error);
|
||||||
|
} finally {
|
||||||
|
if (requestSeq === this.searchRequestSeq) {
|
||||||
|
this.searchInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async matchConversation(conversation, query, requestSeq) {
|
||||||
|
if (!conversation || !conversation.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.searchResultIdSet && this.searchResultIdSet.has(conversation.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstSentence = await this.getConversationFirstUserSentence(conversation.id, requestSeq);
|
||||||
|
if (requestSeq !== this.searchRequestSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const queryLower = String(query || '').toLowerCase();
|
||||||
|
const combined = `${conversation.title || ''} ${firstSentence || ''}`.toLowerCase();
|
||||||
|
if (queryLower && combined.includes(queryLower)) {
|
||||||
|
this.searchResults.push(conversation);
|
||||||
|
this.searchResultIdSet.add(conversation.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getConversationFirstUserSentence(conversationId, requestSeq) {
|
||||||
|
if (!conversationId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.searchPreviewCache &&
|
||||||
|
Object.prototype.hasOwnProperty.call(this.searchPreviewCache, conversationId)
|
||||||
|
) {
|
||||||
|
return this.searchPreviewCache[conversationId];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await fetch(
|
||||||
|
`/api/conversations/${conversationId}/review_preview?limit=${SEARCH_PREVIEW_LIMIT}`
|
||||||
|
);
|
||||||
|
const payload = await resp.json();
|
||||||
|
if (requestSeq !== this.searchRequestSeq) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const lines = payload?.data?.preview || [];
|
||||||
|
let firstUserLine = '';
|
||||||
|
for (const line of lines) {
|
||||||
|
if (typeof line === 'string' && line.startsWith('user:')) {
|
||||||
|
firstUserLine = line.slice('user:'.length).trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const firstSentence = this.extractFirstSentence(firstUserLine);
|
||||||
|
const cached = firstSentence || firstUserLine || '';
|
||||||
|
if (!this.searchPreviewCache) {
|
||||||
|
this.searchPreviewCache = {};
|
||||||
|
}
|
||||||
|
this.searchPreviewCache[conversationId] = cached;
|
||||||
|
return cached;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取对话预览失败:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
extractFirstSentence(text) {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const normalized = String(text).replace(/\s+/g, ' ').trim();
|
||||||
|
const match = normalized.match(/(.+?[。!?.!?])/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,283 +2,277 @@
|
|||||||
import { usePersonalizationStore } from '../../stores/personalization';
|
import { usePersonalizationStore } from '../../stores/personalization';
|
||||||
import { usePolicyStore } from '../../stores/policy';
|
import { usePolicyStore } from '../../stores/policy';
|
||||||
import {
|
import {
|
||||||
getToolIcon,
|
getToolIcon,
|
||||||
getToolAnimationClass,
|
getToolAnimationClass,
|
||||||
getToolStatusText as baseGetToolStatusText,
|
getToolStatusText as baseGetToolStatusText,
|
||||||
getToolDescription,
|
getToolDescription,
|
||||||
cloneToolArguments,
|
cloneToolArguments,
|
||||||
buildToolLabel,
|
buildToolLabel,
|
||||||
formatSearchTopic,
|
formatSearchTopic,
|
||||||
formatSearchTime,
|
formatSearchTime,
|
||||||
formatSearchDomains,
|
formatSearchDomains,
|
||||||
getLanguageClass
|
getLanguageClass
|
||||||
} from '../../utils/chatDisplay';
|
} from '../../utils/chatDisplay';
|
||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
|
|
||||||
export const toolingMethods = {
|
export const toolingMethods = {
|
||||||
toolCategoryIcon(categoryId) {
|
toolCategoryIcon(categoryId) {
|
||||||
return this.toolCategoryIcons[categoryId] || 'settings';
|
return this.toolCategoryIcons[categoryId] || 'settings';
|
||||||
},
|
},
|
||||||
|
|
||||||
findMessageByAction(action) {
|
findMessageByAction(action) {
|
||||||
if (!action) {
|
if (!action) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (const message of this.messages) {
|
for (const message of this.messages) {
|
||||||
if (!message.actions) {
|
if (!message.actions) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (message.actions.includes(action)) {
|
if (message.actions.includes(action)) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanupStaleToolActions() {
|
cleanupStaleToolActions() {
|
||||||
this.messages.forEach(msg => {
|
this.messages.forEach((msg) => {
|
||||||
if (!msg.actions) {
|
if (!msg.actions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
msg.actions.forEach(action => {
|
msg.actions.forEach((action) => {
|
||||||
if (action.type !== 'tool' || !action.tool) {
|
if (action.type !== 'tool' || !action.tool) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (['running', 'preparing'].includes(action.tool.status)) {
|
if (['running', 'preparing'].includes(action.tool.status)) {
|
||||||
action.tool.status = 'stale';
|
action.tool.status = 'stale';
|
||||||
action.tool.message = action.tool.message || '已被新的响应中断';
|
action.tool.message = action.tool.message || '已被新的响应中断';
|
||||||
this.toolUnregisterAction(action);
|
this.toolUnregisterAction(action);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.preparingTools.clear();
|
this.preparingTools.clear();
|
||||||
this.toolActionIndex.clear();
|
this.toolActionIndex.clear();
|
||||||
},
|
},
|
||||||
|
|
||||||
clearPendingTools(reason = 'unspecified') {
|
clearPendingTools(reason = 'unspecified') {
|
||||||
this.messages.forEach(msg => {
|
this.messages.forEach((msg) => {
|
||||||
if (!msg.actions) {
|
if (!msg.actions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
msg.actions.forEach(action => {
|
msg.actions.forEach((action) => {
|
||||||
if (action.type !== 'tool' || !action.tool) {
|
if (action.type !== 'tool' || !action.tool) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const status =
|
|
||||||
typeof action.tool.status === 'string'
|
|
||||||
? action.tool.status.toLowerCase()
|
|
||||||
: '';
|
|
||||||
if (!status || ['preparing', 'running', 'pending', 'queued', 'stale'].includes(status)) {
|
|
||||||
action.tool.status = 'cancelled';
|
|
||||||
action.tool.message = action.tool.message || '已停止';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (this.preparingTools && this.preparingTools.clear) {
|
|
||||||
this.preparingTools.clear();
|
|
||||||
}
|
}
|
||||||
if (this.activeTools && this.activeTools.clear) {
|
const status =
|
||||||
this.activeTools.clear();
|
typeof action.tool.status === 'string' ? action.tool.status.toLowerCase() : '';
|
||||||
|
if (!status || ['preparing', 'running', 'pending', 'queued', 'stale'].includes(status)) {
|
||||||
|
action.tool.status = 'cancelled';
|
||||||
|
action.tool.message = action.tool.message || '已停止';
|
||||||
}
|
}
|
||||||
if (this.toolActionIndex && this.toolActionIndex.clear) {
|
});
|
||||||
this.toolActionIndex.clear();
|
});
|
||||||
}
|
if (this.preparingTools && this.preparingTools.clear) {
|
||||||
if (this.toolStacks && this.toolStacks.clear) {
|
this.preparingTools.clear();
|
||||||
this.toolStacks.clear();
|
}
|
||||||
}
|
if (this.activeTools && this.activeTools.clear) {
|
||||||
if (typeof this.toolResetTracking === 'function') {
|
this.activeTools.clear();
|
||||||
this.toolResetTracking();
|
}
|
||||||
}
|
if (this.toolActionIndex && this.toolActionIndex.clear) {
|
||||||
debugLog('清理待处理工具', { reason });
|
this.toolActionIndex.clear();
|
||||||
},
|
}
|
||||||
|
if (this.toolStacks && this.toolStacks.clear) {
|
||||||
|
this.toolStacks.clear();
|
||||||
|
}
|
||||||
|
if (typeof this.toolResetTracking === 'function') {
|
||||||
|
this.toolResetTracking();
|
||||||
|
}
|
||||||
|
debugLog('清理待处理工具', { reason });
|
||||||
|
},
|
||||||
|
|
||||||
hasPendingToolActions() {
|
hasPendingToolActions() {
|
||||||
const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0;
|
const mapHasEntries = (map) => map && typeof map.size === 'number' && map.size > 0;
|
||||||
if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) {
|
if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(this.messages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.messages.some((msg) => {
|
||||||
|
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return msg.actions.some((action) => {
|
||||||
|
if (!action || action.type !== 'tool' || !action.tool) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(this.messages)) {
|
if (action.tool.awaiting_content) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
return this.messages.some(msg => {
|
const status =
|
||||||
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) {
|
typeof action.tool.status === 'string' ? action.tool.status.toLowerCase() : '';
|
||||||
return false;
|
return !status || ['preparing', 'running', 'pending', 'queued'].includes(status);
|
||||||
}
|
});
|
||||||
return msg.actions.some(action => {
|
});
|
||||||
if (!action || action.type !== 'tool' || !action.tool) {
|
},
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (action.tool.awaiting_content) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const status =
|
|
||||||
typeof action.tool.status === 'string'
|
|
||||||
? action.tool.status.toLowerCase()
|
|
||||||
: '';
|
|
||||||
return !status || ['preparing', 'running', 'pending', 'queued'].includes(status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
maybeResetStreamingState(reason = 'unspecified') {
|
maybeResetStreamingState(reason = 'unspecified') {
|
||||||
if (!this.streamingMessage) {
|
if (!this.streamingMessage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.hasPendingToolActions()) {
|
if (this.hasPendingToolActions()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
this.stopRequested = false;
|
this.stopRequested = false;
|
||||||
debugLog('流式状态已结束', { reason });
|
debugLog('流式状态已结束', { reason });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
applyToolSettingsSnapshot(categories) {
|
applyToolSettingsSnapshot(categories) {
|
||||||
if (!Array.isArray(categories)) {
|
if (!Array.isArray(categories)) {
|
||||||
console.warn('[ToolSettings] Snapshot skipped: categories not array', categories);
|
console.warn('[ToolSettings] Snapshot skipped: categories not array', categories);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const normalized = categories.map((item) => ({
|
const normalized = categories.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
label: item.label || item.id,
|
label: item.label || item.id,
|
||||||
enabled: !!item.enabled,
|
enabled: !!item.enabled,
|
||||||
tools: Array.isArray(item.tools) ? item.tools : [],
|
tools: Array.isArray(item.tools) ? item.tools : [],
|
||||||
locked: !!item.locked,
|
locked: !!item.locked,
|
||||||
locked_state: typeof item.locked_state === 'boolean' ? item.locked_state : null
|
locked_state: typeof item.locked_state === 'boolean' ? item.locked_state : null
|
||||||
}));
|
}));
|
||||||
debugLog('[ToolSettings] Snapshot applied', {
|
debugLog('[ToolSettings] Snapshot applied', {
|
||||||
received: categories.length,
|
received: categories.length,
|
||||||
normalized,
|
normalized,
|
||||||
anyEnabled: normalized.some(cat => cat.enabled),
|
anyEnabled: normalized.some((cat) => cat.enabled),
|
||||||
toolExamples: normalized.slice(0, 3)
|
toolExamples: normalized.slice(0, 3)
|
||||||
});
|
});
|
||||||
this.toolSetSettings(normalized);
|
this.toolSetSettings(normalized);
|
||||||
|
this.toolSetSettingsLoading(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadToolSettings(force = false) {
|
||||||
|
if (!this.isConnected && !force) {
|
||||||
|
debugLog('[ToolSettings] Skip load: disconnected & not forced');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.toolSettingsLoading) {
|
||||||
|
debugLog('[ToolSettings] Skip load: already loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!force && this.toolSettings.length > 0) {
|
||||||
|
debugLog('[ToolSettings] Skip load: already have settings');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugLog('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected });
|
||||||
|
this.toolSetSettingsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/tool-settings');
|
||||||
|
const data = await response.json();
|
||||||
|
debugLog('[ToolSettings] Fetch response', { status: response.status, data });
|
||||||
|
if (response.ok && data.success && Array.isArray(data.categories)) {
|
||||||
|
this.applyToolSettingsSnapshot(data.categories);
|
||||||
|
} else {
|
||||||
|
console.warn('获取工具设置失败:', data);
|
||||||
this.toolSetSettingsLoading(false);
|
this.toolSetSettingsLoading(false);
|
||||||
},
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取工具设置异常:', error);
|
||||||
|
this.toolSetSettingsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async loadToolSettings(force = false) {
|
async updateToolCategory(categoryId, enabled) {
|
||||||
if (!this.isConnected && !force) {
|
if (!this.isConnected) {
|
||||||
debugLog('[ToolSettings] Skip load: disconnected & not forced');
|
return;
|
||||||
return;
|
}
|
||||||
|
if (this.toolSettingsLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.isCategoryLocked(categoryId)) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '无法修改',
|
||||||
|
message: '该工具类别被管理员强制设置',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
|
||||||
|
const updatedSettings = this.toolSettings.map((item) => {
|
||||||
|
if (item.id === categoryId) {
|
||||||
|
return { ...item, enabled };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
this.toolSetSettings(updatedSettings);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/tool-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
category: categoryId,
|
||||||
|
enabled
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok && data.success && Array.isArray(data.categories)) {
|
||||||
|
this.applyToolSettingsSnapshot(data.categories);
|
||||||
|
} else {
|
||||||
|
console.warn('更新工具设置失败:', data);
|
||||||
|
if (data && (data.message || data.error)) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '无法切换工具',
|
||||||
|
message: data.message || data.error,
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this.toolSettingsLoading) {
|
this.toolSetSettings(previousSnapshot);
|
||||||
debugLog('[ToolSettings] Skip load: already loading');
|
}
|
||||||
return;
|
} catch (error) {
|
||||||
}
|
console.error('更新工具设置异常:', error);
|
||||||
if (!force && this.toolSettings.length > 0) {
|
this.toolSetSettings(previousSnapshot);
|
||||||
debugLog('[ToolSettings] Skip load: already have settings');
|
}
|
||||||
return;
|
this.toolSetSettingsLoading(false);
|
||||||
}
|
},
|
||||||
debugLog('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected });
|
|
||||||
this.toolSetSettingsLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/tool-settings');
|
|
||||||
const data = await response.json();
|
|
||||||
debugLog('[ToolSettings] Fetch response', { status: response.status, data });
|
|
||||||
if (response.ok && data.success && Array.isArray(data.categories)) {
|
|
||||||
this.applyToolSettingsSnapshot(data.categories);
|
|
||||||
} else {
|
|
||||||
console.warn('获取工具设置失败:', data);
|
|
||||||
this.toolSetSettingsLoading(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取工具设置异常:', error);
|
|
||||||
this.toolSetSettingsLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateToolCategory(categoryId, enabled) {
|
toggleToolMenu() {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.toolSettingsLoading) {
|
if (this.isPolicyBlocked('block_tool_toggle', '工具启用/禁用已被管理员锁定')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const policyStore = usePolicyStore();
|
this.modeMenuOpen = false;
|
||||||
if (policyStore.isCategoryLocked(categoryId)) {
|
this.modelMenuOpen = false;
|
||||||
this.uiPushToast({
|
const nextState = this.inputToggleToolMenu();
|
||||||
title: '无法修改',
|
if (nextState) {
|
||||||
message: '该工具类别被管理员强制设置',
|
this.inputSetSettingsOpen(false);
|
||||||
type: 'warning'
|
if (!this.quickMenuOpen) {
|
||||||
});
|
this.inputOpenQuickMenu();
|
||||||
return;
|
}
|
||||||
}
|
this.loadToolSettings(true);
|
||||||
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
|
} else {
|
||||||
const updatedSettings = this.toolSettings.map((item) => {
|
this.inputSetToolMenuOpen(false);
|
||||||
if (item.id === categoryId) {
|
}
|
||||||
return { ...item, enabled };
|
},
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
this.toolSetSettings(updatedSettings);
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/tool-settings', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
category: categoryId,
|
|
||||||
enabled
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (response.ok && data.success && Array.isArray(data.categories)) {
|
|
||||||
this.applyToolSettingsSnapshot(data.categories);
|
|
||||||
} else {
|
|
||||||
console.warn('更新工具设置失败:', data);
|
|
||||||
if (data && (data.message || data.error)) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '无法切换工具',
|
|
||||||
message: data.message || data.error,
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.toolSetSettings(previousSnapshot);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新工具设置异常:', error);
|
|
||||||
this.toolSetSettings(previousSnapshot);
|
|
||||||
}
|
|
||||||
this.toolSetSettingsLoading(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleToolMenu() {
|
getToolIcon,
|
||||||
if (!this.isConnected) {
|
getToolAnimationClass,
|
||||||
return;
|
getToolStatusText(tool: any) {
|
||||||
}
|
const personalization = usePersonalizationStore();
|
||||||
if (this.isPolicyBlocked('block_tool_toggle', '工具启用/禁用已被管理员锁定')) {
|
const intentEnabled =
|
||||||
return;
|
personalization?.form?.tool_intent_enabled ?? personalization?.tool_intent_enabled ?? true;
|
||||||
}
|
return baseGetToolStatusText(tool, { intentEnabled });
|
||||||
this.modeMenuOpen = false;
|
},
|
||||||
this.modelMenuOpen = false;
|
getToolDescription,
|
||||||
const nextState = this.inputToggleToolMenu();
|
cloneToolArguments,
|
||||||
if (nextState) {
|
buildToolLabel,
|
||||||
this.inputSetSettingsOpen(false);
|
formatSearchTopic,
|
||||||
if (!this.quickMenuOpen) {
|
formatSearchTime,
|
||||||
this.inputOpenQuickMenu();
|
formatSearchDomains,
|
||||||
}
|
getLanguageClass
|
||||||
this.loadToolSettings(true);
|
|
||||||
} else {
|
|
||||||
this.inputSetToolMenuOpen(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getToolIcon,
|
|
||||||
getToolAnimationClass,
|
|
||||||
getToolStatusText(tool: any) {
|
|
||||||
const personalization = usePersonalizationStore();
|
|
||||||
const intentEnabled =
|
|
||||||
personalization?.form?.tool_intent_enabled ??
|
|
||||||
personalization?.tool_intent_enabled ??
|
|
||||||
true;
|
|
||||||
return baseGetToolStatusText(tool, { intentEnabled });
|
|
||||||
},
|
|
||||||
getToolDescription,
|
|
||||||
cloneToolArguments,
|
|
||||||
buildToolLabel,
|
|
||||||
formatSearchTopic,
|
|
||||||
formatSearchTime,
|
|
||||||
formatSearchDomains,
|
|
||||||
getLanguageClass
|
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -3,398 +3,406 @@ import { usePolicyStore } from '../../stores/policy';
|
|||||||
import { useModelStore } from '../../stores/model';
|
import { useModelStore } from '../../stores/model';
|
||||||
|
|
||||||
export const uploadMethods = {
|
export const uploadMethods = {
|
||||||
triggerFileUpload() {
|
triggerFileUpload() {
|
||||||
if (this.uploading) {
|
if (this.uploading) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const input = this.getComposerElement('fileUploadInput');
|
|
||||||
if (input) {
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFileSelected(files) {
|
|
||||||
const policyStore = usePolicyStore();
|
|
||||||
if (policyStore.uiBlocks?.block_upload) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '上传被禁用',
|
|
||||||
message: '已被管理员禁用上传功能',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.uploadHandleSelected(files);
|
|
||||||
},
|
|
||||||
|
|
||||||
normalizeLocalFiles(files) {
|
|
||||||
if (!files) return [];
|
|
||||||
const list = Array.isArray(files) ? files : Array.from(files);
|
|
||||||
return list.filter(Boolean);
|
|
||||||
},
|
|
||||||
|
|
||||||
isImageFile(file) {
|
|
||||||
const name = file?.name || '';
|
|
||||||
const type = file?.type || '';
|
|
||||||
return type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(name);
|
|
||||||
},
|
|
||||||
|
|
||||||
isVideoFile(file) {
|
|
||||||
const name = file?.name || '';
|
|
||||||
const type = file?.type || '';
|
|
||||||
return type.startsWith('video/') || /\.(mp4|mov|m4v|webm|avi|mkv|flv|mpg|mpeg)$/i.test(name);
|
|
||||||
},
|
|
||||||
|
|
||||||
upsertImageEntry(path, filename) {
|
|
||||||
if (!path) return;
|
|
||||||
const name = filename || path.split('/').pop() || path;
|
|
||||||
const list = Array.isArray(this.imageEntries) ? this.imageEntries : [];
|
|
||||||
if (list.some((item) => item.path === path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.imageEntries = [{ name, path }, ...list];
|
|
||||||
},
|
|
||||||
|
|
||||||
upsertVideoEntry(path, filename) {
|
|
||||||
if (!path) return;
|
|
||||||
const name = filename || path.split('/').pop() || path;
|
|
||||||
const list = Array.isArray(this.videoEntries) ? this.videoEntries : [];
|
|
||||||
if (list.some((item) => item.path === path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.videoEntries = [{ name, path }, ...list];
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleLocalImageFiles(files) {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.mediaUploading) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '上传中',
|
|
||||||
message: '请等待当前图片上传完成',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = this.normalizeLocalFiles(files);
|
|
||||||
if (!list.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const existingCount = Array.isArray(this.selectedImages) ? this.selectedImages.length : 0;
|
|
||||||
const remaining = Math.max(0, 9 - existingCount);
|
|
||||||
if (!remaining) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '已达上限',
|
|
||||||
message: '最多只能选择 9 张图片',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const valid = list.filter((file) => this.isImageFile(file));
|
|
||||||
// 兼容 Android WebView 部分机型:返回的 File 可能缺失 type/扩展名,导致识别失败
|
|
||||||
// 若来自图片选择器但未识别出有效类型,回退为“按选择结果直接上传”。
|
|
||||||
const candidates = valid.length ? valid : list;
|
|
||||||
if (valid.length < list.length && valid.length > 0) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '已忽略',
|
|
||||||
message: '已跳过非图片文件',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const limited = candidates.slice(0, remaining);
|
|
||||||
if (candidates.length > remaining) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '已超出数量',
|
|
||||||
message: `最多还能添加 ${remaining} 张图片,已自动截断`,
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const uploaded = await this.uploadBatchFiles(limited, {
|
|
||||||
markUploading: true,
|
|
||||||
markMediaUploading: true
|
|
||||||
});
|
|
||||||
if (!uploaded.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uploaded.forEach((item) => {
|
|
||||||
if (!item?.path) return;
|
|
||||||
this.inputAddSelectedImage(item.path);
|
|
||||||
this.upsertImageEntry(item.path, item.filename);
|
|
||||||
});
|
|
||||||
// 上传完成后自动关闭选择窗口
|
|
||||||
this.closeImagePicker();
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleLocalVideoFiles(files) {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.mediaUploading) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '上传中',
|
|
||||||
message: '请等待当前视频上传完成',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = this.normalizeLocalFiles(files);
|
|
||||||
if (!list.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const valid = list.filter((file) => this.isVideoFile(file));
|
|
||||||
// 兼容 Android WebView:文件元数据缺失时回退按选择结果直接上传
|
|
||||||
const candidates = valid.length ? valid : list;
|
|
||||||
if (valid.length < list.length && valid.length > 0) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '已忽略',
|
|
||||||
message: '已跳过非视频文件',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (candidates.length > 1) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '视频数量过多',
|
|
||||||
message: '一次只能选择 1 个视频,已使用第一个',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const [file] = candidates;
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const uploaded = await this.uploadBatchFiles([file], {
|
|
||||||
markUploading: true,
|
|
||||||
markMediaUploading: true
|
|
||||||
});
|
|
||||||
const [item] = uploaded;
|
|
||||||
if (!item?.path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.inputSetSelectedVideos([item.path]);
|
|
||||||
this.inputClearSelectedImages();
|
|
||||||
this.upsertVideoEntry(item.path, item.filename);
|
|
||||||
},
|
|
||||||
|
|
||||||
async openImagePicker() {
|
|
||||||
const modelStore = useModelStore();
|
|
||||||
const currentModel = modelStore.models.find((m) => m.key === this.currentModelKey);
|
|
||||||
if (!currentModel?.supportsImage) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '当前模型不支持图片',
|
|
||||||
message: '请选择支持图片输入的模型后再发送图片',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.closeQuickMenu();
|
|
||||||
this.inputSetImagePickerOpen(true);
|
|
||||||
await this.loadWorkspaceImages();
|
|
||||||
},
|
|
||||||
|
|
||||||
closeImagePicker() {
|
|
||||||
this.inputSetImagePickerOpen(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
async openVideoPicker() {
|
|
||||||
const modelStore = useModelStore();
|
|
||||||
const currentModel = modelStore.models.find((m) => m.key === this.currentModelKey);
|
|
||||||
if (!currentModel?.supportsVideo) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '当前模型不支持视频',
|
|
||||||
message: '请切换到支持视频输入的模型后再发送视频',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.closeQuickMenu();
|
|
||||||
this.inputSetVideoPickerOpen(true);
|
|
||||||
await this.loadWorkspaceVideos();
|
|
||||||
},
|
|
||||||
|
|
||||||
closeVideoPicker() {
|
|
||||||
this.inputSetVideoPickerOpen(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadWorkspaceImages() {
|
|
||||||
this.imageLoading = true;
|
|
||||||
try {
|
|
||||||
const entries = await this.fetchAllImageEntries('');
|
|
||||||
this.imageEntries = entries;
|
|
||||||
if (!entries.length) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '未找到图片',
|
|
||||||
message: '工作区内没有可用的图片文件',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载图片列表失败', error);
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '加载图片失败',
|
|
||||||
message: error?.message || '请稍后重试',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.imageLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchAllImageEntries(startPath = '') {
|
|
||||||
const queue: string[] = [startPath || ''];
|
|
||||||
const visited = new Set<string>();
|
|
||||||
const results: Array<{ name: string; path: string }> = [];
|
|
||||||
const exts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg']);
|
|
||||||
const maxFolders = 120;
|
|
||||||
|
|
||||||
while (queue.length && visited.size < maxFolders) {
|
|
||||||
const path = queue.shift() || '';
|
|
||||||
if (visited.has(path)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
visited.add(path);
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { Accept: 'application/json' }
|
|
||||||
});
|
|
||||||
const data = await resp.json().catch(() => null);
|
|
||||||
if (!data?.success) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
|
||||||
for (const item of items) {
|
|
||||||
const rawPath =
|
|
||||||
item?.path ||
|
|
||||||
[path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
|
||||||
const type = String(item?.type || '').toLowerCase();
|
|
||||||
if (type === 'directory' || type === 'folder') {
|
|
||||||
queue.push(rawPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ext =
|
|
||||||
String(item?.extension || '').toLowerCase() ||
|
|
||||||
(rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : '');
|
|
||||||
if (exts.has(ext)) {
|
|
||||||
results.push({
|
|
||||||
name: item?.name || rawPath.split('/').pop() || rawPath,
|
|
||||||
path: rawPath
|
|
||||||
});
|
|
||||||
if (results.length >= 400) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('遍历文件夹失败', path, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchAllVideoEntries(startPath = '') {
|
|
||||||
const queue: string[] = [startPath || ''];
|
|
||||||
const visited = new Set<string>();
|
|
||||||
const results: Array<{ name: string; path: string }> = [];
|
|
||||||
const exts = new Set(['.mp4', '.mov', '.mkv', '.avi', '.webm']);
|
|
||||||
const maxFolders = 120;
|
|
||||||
|
|
||||||
while (queue.length && visited.size < maxFolders) {
|
|
||||||
const path = queue.shift() || '';
|
|
||||||
if (visited.has(path)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
visited.add(path);
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { Accept: 'application/json' }
|
|
||||||
});
|
|
||||||
const data = await resp.json().catch(() => null);
|
|
||||||
if (!data?.success) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
|
||||||
for (const item of items) {
|
|
||||||
const rawPath =
|
|
||||||
item?.path ||
|
|
||||||
[path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
|
||||||
const type = String(item?.type || '').toLowerCase();
|
|
||||||
if (type === 'directory' || type === 'folder') {
|
|
||||||
queue.push(rawPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ext =
|
|
||||||
String(item?.extension || '').toLowerCase() ||
|
|
||||||
(rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : '');
|
|
||||||
if (exts.has(ext)) {
|
|
||||||
results.push({
|
|
||||||
name: item?.name || rawPath.split('/').pop() || rawPath,
|
|
||||||
path: rawPath
|
|
||||||
});
|
|
||||||
if (results.length >= 200) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('遍历文件夹失败', path, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadWorkspaceVideos() {
|
|
||||||
this.videoLoading = true;
|
|
||||||
try {
|
|
||||||
const entries = await this.fetchAllVideoEntries('');
|
|
||||||
this.videoEntries = entries;
|
|
||||||
if (!entries.length) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '未找到视频',
|
|
||||||
message: '工作区内没有可用的视频文件',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载视频列表失败', error);
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '加载视频失败',
|
|
||||||
message: error?.message || '请稍后重试',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.videoLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleImagesConfirmed(list) {
|
|
||||||
this.inputSetSelectedImages(Array.isArray(list) ? list : []);
|
|
||||||
this.inputSetImagePickerOpen(false);
|
|
||||||
},
|
|
||||||
handleRemoveImage(path) {
|
|
||||||
this.inputRemoveSelectedImage(path);
|
|
||||||
},
|
|
||||||
handleVideosConfirmed(list) {
|
|
||||||
const arr = Array.isArray(list) ? list.slice(0, 1) : [];
|
|
||||||
this.inputSetSelectedVideos(arr);
|
|
||||||
this.inputSetVideoPickerOpen(false);
|
|
||||||
if (arr.length) {
|
|
||||||
this.inputClearSelectedImages();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleRemoveVideo(path) {
|
|
||||||
this.inputRemoveSelectedVideo(path);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleQuickUpload() {
|
|
||||||
if (this.uploading || !this.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.triggerFileUpload();
|
|
||||||
}
|
}
|
||||||
|
const input = this.getComposerElement('fileUploadInput');
|
||||||
|
if (input) {
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFileSelected(files) {
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (policyStore.uiBlocks?.block_upload) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传被禁用',
|
||||||
|
message: '已被管理员禁用上传功能',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploadHandleSelected(files);
|
||||||
|
},
|
||||||
|
|
||||||
|
normalizeLocalFiles(files) {
|
||||||
|
if (!files) return [];
|
||||||
|
const list = Array.isArray(files) ? files : Array.from(files);
|
||||||
|
return list.filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
isImageFile(file) {
|
||||||
|
const name = file?.name || '';
|
||||||
|
const type = file?.type || '';
|
||||||
|
return type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
isVideoFile(file) {
|
||||||
|
const name = file?.name || '';
|
||||||
|
const type = file?.type || '';
|
||||||
|
return type.startsWith('video/') || /\.(mp4|mov|m4v|webm|avi|mkv|flv|mpg|mpeg)$/i.test(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
upsertImageEntry(path, filename) {
|
||||||
|
if (!path) return;
|
||||||
|
const name = filename || path.split('/').pop() || path;
|
||||||
|
const list = Array.isArray(this.imageEntries) ? this.imageEntries : [];
|
||||||
|
if (list.some((item) => item.path === path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.imageEntries = [{ name, path }, ...list];
|
||||||
|
},
|
||||||
|
|
||||||
|
upsertVideoEntry(path, filename) {
|
||||||
|
if (!path) return;
|
||||||
|
const name = filename || path.split('/').pop() || path;
|
||||||
|
const list = Array.isArray(this.videoEntries) ? this.videoEntries : [];
|
||||||
|
if (list.some((item) => item.path === path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.videoEntries = [{ name, path }, ...list];
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLocalImageFiles(files) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mediaUploading) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传中',
|
||||||
|
message: '请等待当前图片上传完成',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = this.normalizeLocalFiles(files);
|
||||||
|
if (!list.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingCount = Array.isArray(this.selectedImages) ? this.selectedImages.length : 0;
|
||||||
|
const remaining = Math.max(0, 9 - existingCount);
|
||||||
|
if (!remaining) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已达上限',
|
||||||
|
message: '最多只能选择 9 张图片',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valid = list.filter((file) => this.isImageFile(file));
|
||||||
|
// 兼容 Android WebView 部分机型:返回的 File 可能缺失 type/扩展名,导致识别失败
|
||||||
|
// 若来自图片选择器但未识别出有效类型,回退为“按选择结果直接上传”。
|
||||||
|
const candidates = valid.length ? valid : list;
|
||||||
|
if (valid.length < list.length && valid.length > 0) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已忽略',
|
||||||
|
message: '已跳过非图片文件',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const limited = candidates.slice(0, remaining);
|
||||||
|
if (candidates.length > remaining) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已超出数量',
|
||||||
|
message: `最多还能添加 ${remaining} 张图片,已自动截断`,
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const uploaded = await this.uploadBatchFiles(limited, {
|
||||||
|
markUploading: true,
|
||||||
|
markMediaUploading: true
|
||||||
|
});
|
||||||
|
if (!uploaded.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploaded.forEach((item) => {
|
||||||
|
if (!item?.path) return;
|
||||||
|
this.inputAddSelectedImage(item.path);
|
||||||
|
this.upsertImageEntry(item.path, item.filename);
|
||||||
|
});
|
||||||
|
// 上传完成后自动关闭选择窗口
|
||||||
|
this.closeImagePicker();
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLocalVideoFiles(files) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mediaUploading) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传中',
|
||||||
|
message: '请等待当前视频上传完成',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = this.normalizeLocalFiles(files);
|
||||||
|
if (!list.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valid = list.filter((file) => this.isVideoFile(file));
|
||||||
|
// 兼容 Android WebView:文件元数据缺失时回退按选择结果直接上传
|
||||||
|
const candidates = valid.length ? valid : list;
|
||||||
|
if (valid.length < list.length && valid.length > 0) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已忽略',
|
||||||
|
message: '已跳过非视频文件',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (candidates.length > 1) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '视频数量过多',
|
||||||
|
message: '一次只能选择 1 个视频,已使用第一个',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [file] = candidates;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uploaded = await this.uploadBatchFiles([file], {
|
||||||
|
markUploading: true,
|
||||||
|
markMediaUploading: true
|
||||||
|
});
|
||||||
|
const [item] = uploaded;
|
||||||
|
if (!item?.path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inputSetSelectedVideos([item.path]);
|
||||||
|
this.inputClearSelectedImages();
|
||||||
|
this.upsertVideoEntry(item.path, item.filename);
|
||||||
|
},
|
||||||
|
|
||||||
|
async openImagePicker() {
|
||||||
|
const modelStore = useModelStore();
|
||||||
|
const currentModel = modelStore.models.find((m) => m.key === this.currentModelKey);
|
||||||
|
if (!currentModel?.supportsImage) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '当前模型不支持图片',
|
||||||
|
message: '请选择支持图片输入的模型后再发送图片',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.closeQuickMenu();
|
||||||
|
this.inputSetImagePickerOpen(true);
|
||||||
|
await this.loadWorkspaceImages();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeImagePicker() {
|
||||||
|
this.inputSetImagePickerOpen(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
async openVideoPicker() {
|
||||||
|
const modelStore = useModelStore();
|
||||||
|
const currentModel = modelStore.models.find((m) => m.key === this.currentModelKey);
|
||||||
|
if (!currentModel?.supportsVideo) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '当前模型不支持视频',
|
||||||
|
message: '请切换到支持视频输入的模型后再发送视频',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.closeQuickMenu();
|
||||||
|
this.inputSetVideoPickerOpen(true);
|
||||||
|
await this.loadWorkspaceVideos();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeVideoPicker() {
|
||||||
|
this.inputSetVideoPickerOpen(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadWorkspaceImages() {
|
||||||
|
this.imageLoading = true;
|
||||||
|
try {
|
||||||
|
const entries = await this.fetchAllImageEntries('');
|
||||||
|
this.imageEntries = entries;
|
||||||
|
if (!entries.length) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '未找到图片',
|
||||||
|
message: '工作区内没有可用的图片文件',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载图片列表失败', error);
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '加载图片失败',
|
||||||
|
message: error?.message || '请稍后重试',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.imageLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchAllImageEntries(startPath = '') {
|
||||||
|
const queue: string[] = [startPath || ''];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const results: Array<{ name: string; path: string }> = [];
|
||||||
|
const exts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg']);
|
||||||
|
const maxFolders = 120;
|
||||||
|
|
||||||
|
while (queue.length && visited.size < maxFolders) {
|
||||||
|
const path = queue.shift() || '';
|
||||||
|
if (visited.has(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.add(path);
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
|
});
|
||||||
|
const data = await resp.json().catch(() => null);
|
||||||
|
if (!data?.success) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
||||||
|
for (const item of items) {
|
||||||
|
const rawPath =
|
||||||
|
item?.path ||
|
||||||
|
[path, item?.name]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('/')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/\/{2,}/g, '/');
|
||||||
|
const type = String(item?.type || '').toLowerCase();
|
||||||
|
if (type === 'directory' || type === 'folder') {
|
||||||
|
queue.push(rawPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ext =
|
||||||
|
String(item?.extension || '').toLowerCase() ||
|
||||||
|
(rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : '');
|
||||||
|
if (exts.has(ext)) {
|
||||||
|
results.push({
|
||||||
|
name: item?.name || rawPath.split('/').pop() || rawPath,
|
||||||
|
path: rawPath
|
||||||
|
});
|
||||||
|
if (results.length >= 400) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('遍历文件夹失败', path, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchAllVideoEntries(startPath = '') {
|
||||||
|
const queue: string[] = [startPath || ''];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const results: Array<{ name: string; path: string }> = [];
|
||||||
|
const exts = new Set(['.mp4', '.mov', '.mkv', '.avi', '.webm']);
|
||||||
|
const maxFolders = 120;
|
||||||
|
|
||||||
|
while (queue.length && visited.size < maxFolders) {
|
||||||
|
const path = queue.shift() || '';
|
||||||
|
if (visited.has(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.add(path);
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
|
});
|
||||||
|
const data = await resp.json().catch(() => null);
|
||||||
|
if (!data?.success) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
|
||||||
|
for (const item of items) {
|
||||||
|
const rawPath =
|
||||||
|
item?.path ||
|
||||||
|
[path, item?.name]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('/')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/\/{2,}/g, '/');
|
||||||
|
const type = String(item?.type || '').toLowerCase();
|
||||||
|
if (type === 'directory' || type === 'folder') {
|
||||||
|
queue.push(rawPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ext =
|
||||||
|
String(item?.extension || '').toLowerCase() ||
|
||||||
|
(rawPath.includes('.') ? `.${rawPath.split('.').pop()?.toLowerCase()}` : '');
|
||||||
|
if (exts.has(ext)) {
|
||||||
|
results.push({
|
||||||
|
name: item?.name || rawPath.split('/').pop() || rawPath,
|
||||||
|
path: rawPath
|
||||||
|
});
|
||||||
|
if (results.length >= 200) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('遍历文件夹失败', path, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadWorkspaceVideos() {
|
||||||
|
this.videoLoading = true;
|
||||||
|
try {
|
||||||
|
const entries = await this.fetchAllVideoEntries('');
|
||||||
|
this.videoEntries = entries;
|
||||||
|
if (!entries.length) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '未找到视频',
|
||||||
|
message: '工作区内没有可用的视频文件',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载视频列表失败', error);
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '加载视频失败',
|
||||||
|
message: error?.message || '请稍后重试',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.videoLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImagesConfirmed(list) {
|
||||||
|
this.inputSetSelectedImages(Array.isArray(list) ? list : []);
|
||||||
|
this.inputSetImagePickerOpen(false);
|
||||||
|
},
|
||||||
|
handleRemoveImage(path) {
|
||||||
|
this.inputRemoveSelectedImage(path);
|
||||||
|
},
|
||||||
|
handleVideosConfirmed(list) {
|
||||||
|
const arr = Array.isArray(list) ? list.slice(0, 1) : [];
|
||||||
|
this.inputSetSelectedVideos(arr);
|
||||||
|
this.inputSetVideoPickerOpen(false);
|
||||||
|
if (arr.length) {
|
||||||
|
this.inputClearSelectedImages();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleRemoveVideo(path) {
|
||||||
|
this.inputRemoveSelectedVideo(path);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleQuickUpload() {
|
||||||
|
if (this.uploading || !this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.isPolicyBlocked('block_upload', '上传功能已被管理员禁用')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.triggerFileUpload();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,113 +2,113 @@
|
|||||||
import { ICONS, TOOL_CATEGORY_ICON_MAP } from '../utils/icons';
|
import { ICONS, TOOL_CATEGORY_ICON_MAP } from '../utils/icons';
|
||||||
|
|
||||||
export function dataState() {
|
export function dataState() {
|
||||||
return {
|
return {
|
||||||
// 路由相关
|
// 路由相关
|
||||||
initialRouteResolved: false,
|
initialRouteResolved: false,
|
||||||
dropToolEvents: false,
|
dropToolEvents: false,
|
||||||
|
|
||||||
// 轮询模式标志(禁用 WebSocket 事件处理)
|
// 轮询模式标志(禁用 WebSocket 事件处理)
|
||||||
usePollingMode: true,
|
usePollingMode: true,
|
||||||
// 后台子智能体等待状态
|
// 后台子智能体等待状态
|
||||||
waitingForSubAgent: false,
|
waitingForSubAgent: false,
|
||||||
// 后台 run_command 等待状态(用于文案区分)
|
// 后台 run_command 等待状态(用于文案区分)
|
||||||
waitingForBackgroundCommand: false,
|
waitingForBackgroundCommand: false,
|
||||||
// 是否在对话区展示 system 消息
|
// 是否在对话区展示 system 消息
|
||||||
hideSystemMessages: true,
|
hideSystemMessages: true,
|
||||||
|
|
||||||
// 工具状态跟踪
|
// 工具状态跟踪
|
||||||
preparingTools: new Map(),
|
preparingTools: new Map(),
|
||||||
activeTools: new Map(),
|
activeTools: new Map(),
|
||||||
toolActionIndex: new Map(),
|
toolActionIndex: new Map(),
|
||||||
toolStacks: new Map(),
|
toolStacks: new Map(),
|
||||||
// 当前任务是否仍在进行中(用于保持输入区的"停止"状态)
|
// 当前任务是否仍在进行中(用于保持输入区的"停止"状态)
|
||||||
taskInProgress: false,
|
taskInProgress: false,
|
||||||
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
||||||
waitingTaskProbeTimer: null,
|
waitingTaskProbeTimer: null,
|
||||||
// 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次
|
// 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次
|
||||||
lastHistoryLoadedConversationId: null,
|
lastHistoryLoadedConversationId: null,
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 对话管理相关状态
|
// 对话管理相关状态
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// 搜索功能
|
// 搜索功能
|
||||||
// ==========================================
|
// ==========================================
|
||||||
searchRequestSeq: 0,
|
searchRequestSeq: 0,
|
||||||
searchActiveQuery: '',
|
searchActiveQuery: '',
|
||||||
searchResultIdSet: new Set(),
|
searchResultIdSet: new Set(),
|
||||||
searchPreviewCache: {},
|
searchPreviewCache: {},
|
||||||
|
|
||||||
// Token统计相关状态(修复版)
|
// Token统计相关状态(修复版)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// 对话压缩状态
|
// 对话压缩状态
|
||||||
compressing: false,
|
compressing: false,
|
||||||
compressionInProgress: false,
|
compressionInProgress: false,
|
||||||
compressionMode: '',
|
compressionMode: '',
|
||||||
compressionStage: '',
|
compressionStage: '',
|
||||||
compressionError: '',
|
compressionError: '',
|
||||||
compressionToastId: null,
|
compressionToastId: null,
|
||||||
skipConversationLoadedEvent: false,
|
skipConversationLoadedEvent: false,
|
||||||
skipConversationHistoryReload: false,
|
skipConversationHistoryReload: false,
|
||||||
_scrollListenerReady: false,
|
_scrollListenerReady: false,
|
||||||
historyLoading: false,
|
historyLoading: false,
|
||||||
historyLoadingFor: null,
|
historyLoadingFor: null,
|
||||||
historyLoadSeq: 0,
|
historyLoadSeq: 0,
|
||||||
blankHeroActive: false,
|
blankHeroActive: false,
|
||||||
blankHeroExiting: false,
|
blankHeroExiting: false,
|
||||||
blankWelcomeText: '',
|
blankWelcomeText: '',
|
||||||
lastBlankConversationId: null,
|
lastBlankConversationId: null,
|
||||||
// 对话标题打字效果
|
// 对话标题打字效果
|
||||||
titleTypingText: '',
|
titleTypingText: '',
|
||||||
titleTypingTarget: '',
|
titleTypingTarget: '',
|
||||||
titleTypingTimer: null,
|
titleTypingTimer: null,
|
||||||
titleReady: false,
|
titleReady: false,
|
||||||
suppressTitleTyping: false,
|
suppressTitleTyping: false,
|
||||||
headerMenuOpen: false,
|
headerMenuOpen: false,
|
||||||
blankWelcomePool: [
|
blankWelcomePool: [
|
||||||
'有什么可以帮忙的?',
|
'有什么可以帮忙的?',
|
||||||
'想了解些热点吗?',
|
'想了解些热点吗?',
|
||||||
'要我帮你完成作业吗?',
|
'要我帮你完成作业吗?',
|
||||||
'整点代码?',
|
'整点代码?',
|
||||||
'随便聊点什么?',
|
'随便聊点什么?',
|
||||||
'想让我帮你整理一下思路吗?',
|
'想让我帮你整理一下思路吗?',
|
||||||
'要不要我帮你写个小工具?',
|
'要不要我帮你写个小工具?',
|
||||||
'发我一句话,我来接着做。'
|
'发我一句话,我来接着做。'
|
||||||
],
|
],
|
||||||
mobileViewportQuery: null,
|
mobileViewportQuery: null,
|
||||||
modeMenuOpen: false,
|
modeMenuOpen: false,
|
||||||
modelMenuOpen: false,
|
modelMenuOpen: false,
|
||||||
imageEntries: [],
|
imageEntries: [],
|
||||||
imageLoading: false,
|
imageLoading: false,
|
||||||
videoEntries: [],
|
videoEntries: [],
|
||||||
videoLoading: false,
|
videoLoading: false,
|
||||||
conversationHasImages: false,
|
conversationHasImages: false,
|
||||||
conversationHasVideos: false,
|
conversationHasVideos: false,
|
||||||
conversationListRequestSeq: 0,
|
conversationListRequestSeq: 0,
|
||||||
conversationListRefreshToken: 0,
|
conversationListRefreshToken: 0,
|
||||||
connectionHeartbeatTimer: null,
|
connectionHeartbeatTimer: null,
|
||||||
connectionHeartbeatFailCount: 0,
|
connectionHeartbeatFailCount: 0,
|
||||||
connectionHeartbeatIntervalMs: 8000,
|
connectionHeartbeatIntervalMs: 8000,
|
||||||
|
|
||||||
// 工具控制菜单
|
// 工具控制菜单
|
||||||
icons: ICONS,
|
icons: ICONS,
|
||||||
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP,
|
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP,
|
||||||
|
|
||||||
// 对话回顾
|
// 对话回顾
|
||||||
reviewDialogOpen: false,
|
reviewDialogOpen: false,
|
||||||
reviewSelectedConversationId: null,
|
reviewSelectedConversationId: null,
|
||||||
reviewSubmitting: false,
|
reviewSubmitting: false,
|
||||||
reviewPreviewLines: [],
|
reviewPreviewLines: [],
|
||||||
reviewPreviewLoading: false,
|
reviewPreviewLoading: false,
|
||||||
reviewPreviewError: null,
|
reviewPreviewError: null,
|
||||||
reviewPreviewLimit: 20,
|
reviewPreviewLimit: 20,
|
||||||
reviewSendToModel: true,
|
reviewSendToModel: true,
|
||||||
reviewGeneratedPath: null,
|
reviewGeneratedPath: null,
|
||||||
|
|
||||||
// 新手教程首次引导弹窗
|
// 新手教程首次引导弹窗
|
||||||
tutorialPromptVisible: false,
|
tutorialPromptVisible: false,
|
||||||
tutorialPromptLoading: false,
|
tutorialPromptLoading: false,
|
||||||
tutorialPromptUsername: ''
|
tutorialPromptUsername: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,61 +2,70 @@
|
|||||||
import { debugLog, traceLog } from './methods/common';
|
import { debugLog, traceLog } from './methods/common';
|
||||||
|
|
||||||
export const watchers = {
|
export const watchers = {
|
||||||
inputMessage() {
|
inputMessage() {
|
||||||
this.autoResizeInput();
|
this.autoResizeInput();
|
||||||
},
|
},
|
||||||
messages: {
|
messages: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler() {
|
handler() {
|
||||||
this.refreshBlankHeroState();
|
this.refreshBlankHeroState();
|
||||||
}
|
|
||||||
},
|
|
||||||
currentConversationTitle(newVal, oldVal) {
|
|
||||||
const target = (newVal && newVal.trim()) || '';
|
|
||||||
if (this.suppressTitleTyping) {
|
|
||||||
this.titleTypingText = target;
|
|
||||||
this.titleTypingTarget = target;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const previous = (oldVal && oldVal.trim()) || (this.titleTypingText && this.titleTypingText.trim()) || '';
|
|
||||||
const placeholderPrev = !previous || previous === '新对话';
|
|
||||||
const placeholderTarget = !target || target === '新对话';
|
|
||||||
const animate = placeholderPrev && !placeholderTarget; // 仅从空/占位切换到真实标题时动画
|
|
||||||
this.startTitleTyping(target, { animate });
|
|
||||||
},
|
|
||||||
currentConversationId: {
|
|
||||||
immediate: false,
|
|
||||||
handler(newValue, oldValue) {
|
|
||||||
debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
|
||||||
traceLog('watch:currentConversationId', {
|
|
||||||
oldValue,
|
|
||||||
newValue,
|
|
||||||
skipConversationHistoryReload: this.skipConversationHistoryReload,
|
|
||||||
historyLoading: this.historyLoading,
|
|
||||||
historyLoadingFor: this.historyLoadingFor,
|
|
||||||
historyLoadSeq: this.historyLoadSeq
|
|
||||||
});
|
|
||||||
this.refreshBlankHeroState();
|
|
||||||
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
|
||||||
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.skipConversationHistoryReload) {
|
|
||||||
this.skipConversationHistoryReload = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (oldValue && newValue === oldValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.fetchAndDisplayHistory();
|
|
||||||
this.fetchConversationTokenStatistics();
|
|
||||||
this.updateCurrentContextTokens();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fileTree: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newValue) {
|
|
||||||
this.monitorSyncDesktop(newValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
currentConversationTitle(newVal, oldVal) {
|
||||||
|
const target = (newVal && newVal.trim()) || '';
|
||||||
|
if (this.suppressTitleTyping) {
|
||||||
|
this.titleTypingText = target;
|
||||||
|
this.titleTypingTarget = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous =
|
||||||
|
(oldVal && oldVal.trim()) || (this.titleTypingText && this.titleTypingText.trim()) || '';
|
||||||
|
const placeholderPrev = !previous || previous === '新对话';
|
||||||
|
const placeholderTarget = !target || target === '新对话';
|
||||||
|
const animate = placeholderPrev && !placeholderTarget; // 仅从空/占位切换到真实标题时动画
|
||||||
|
this.startTitleTyping(target, { animate });
|
||||||
|
},
|
||||||
|
currentConversationId: {
|
||||||
|
immediate: false,
|
||||||
|
handler(newValue, oldValue) {
|
||||||
|
debugLog('currentConversationId 变化', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
skipConversationHistoryReload: this.skipConversationHistoryReload
|
||||||
|
});
|
||||||
|
traceLog('watch:currentConversationId', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
skipConversationHistoryReload: this.skipConversationHistoryReload,
|
||||||
|
historyLoading: this.historyLoading,
|
||||||
|
historyLoadingFor: this.historyLoadingFor,
|
||||||
|
historyLoadSeq: this.historyLoadSeq
|
||||||
|
});
|
||||||
|
this.refreshBlankHeroState();
|
||||||
|
this.logMessageState('watch:currentConversationId', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
skipConversationHistoryReload: this.skipConversationHistoryReload
|
||||||
|
});
|
||||||
|
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.skipConversationHistoryReload) {
|
||||||
|
this.skipConversationHistoryReload = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldValue && newValue === oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fetchAndDisplayHistory();
|
||||||
|
this.fetchConversationTokenStatistics();
|
||||||
|
this.updateCurrentContextTokens();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fileTree: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newValue) {
|
||||||
|
this.monitorSyncDesktop(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -11,17 +11,19 @@ function wrapCodeBlocks(html: string, isStreaming = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
return html.replace(/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g, (match, attributes, content) => {
|
return html.replace(
|
||||||
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
|
/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g,
|
||||||
const language = langMatch ? langMatch[1] : 'text';
|
(match, attributes, content) => {
|
||||||
const blockId = `code-${Date.now()}-${counter++}`;
|
const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
|
||||||
const escapedContent = content
|
const language = langMatch ? langMatch[1] : 'text';
|
||||||
.replace(/&/g, '&')
|
const blockId = `code-${Date.now()}-${counter++}`;
|
||||||
.replace(/</g, '<')
|
const escapedContent = content
|
||||||
.replace(/>/g, '>')
|
.replace(/&/g, '&')
|
||||||
.replace(/"/g, '"');
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="code-block-wrapper">
|
<div class="code-block-wrapper">
|
||||||
<div class="code-block-header">
|
<div class="code-block-header">
|
||||||
<span class="code-language">${language}</span>
|
<span class="code-language">${language}</span>
|
||||||
@ -29,7 +31,8 @@ function wrapCodeBlocks(html: string, isStreaming = false) {
|
|||||||
</div>
|
</div>
|
||||||
<pre><code${attributes} data-code-id="${blockId}" data-original-code="${escapedContent}">${content}</code></pre>
|
<pre><code${attributes} data-code-id="${blockId}" data-original-code="${escapedContent}">${content}</code></pre>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderMarkdown(text: string, isStreaming = false) {
|
export function renderMarkdown(text: string, isStreaming = false) {
|
||||||
@ -71,8 +74,10 @@ export function renderMarkdown(text: string, isStreaming = false) {
|
|||||||
if (!isStreaming) {
|
if (!isStreaming) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof Prism !== 'undefined') {
|
if (typeof Prism !== 'undefined') {
|
||||||
const codeBlocks = document.querySelectorAll('.code-block-wrapper pre code:not([data-highlighted])');
|
const codeBlocks = document.querySelectorAll(
|
||||||
codeBlocks.forEach(block => {
|
'.code-block-wrapper pre code:not([data-highlighted])'
|
||||||
|
);
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
try {
|
try {
|
||||||
Prism.highlightElement(block as HTMLElement);
|
Prism.highlightElement(block as HTMLElement);
|
||||||
block.setAttribute('data-highlighted', 'true');
|
block.setAttribute('data-highlighted', 'true');
|
||||||
@ -83,8 +88,10 @@ export function renderMarkdown(text: string, isStreaming = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof renderMathInElement !== 'undefined') {
|
if (typeof renderMathInElement !== 'undefined') {
|
||||||
const elements = document.querySelectorAll('.text-output .text-content:not(.streaming-text)');
|
const elements = document.querySelectorAll(
|
||||||
elements.forEach(element => {
|
'.text-output .text-content:not(.streaming-text)'
|
||||||
|
);
|
||||||
|
elements.forEach((element) => {
|
||||||
if (element.hasAttribute('data-math-rendered')) return;
|
if (element.hasAttribute('data-math-rendered')) return;
|
||||||
try {
|
try {
|
||||||
renderMathInElement(element as HTMLElement, {
|
renderMathInElement(element as HTMLElement, {
|
||||||
@ -120,7 +127,7 @@ export function renderLatexInRealtime() {
|
|||||||
|
|
||||||
latexRenderTimer = requestAnimationFrame(() => {
|
latexRenderTimer = requestAnimationFrame(() => {
|
||||||
const elements = document.querySelectorAll('.text-output .streaming-text');
|
const elements = document.querySelectorAll('.text-output .streaming-text');
|
||||||
elements.forEach(element => {
|
elements.forEach((element) => {
|
||||||
try {
|
try {
|
||||||
renderMathInElement(element as HTMLElement, {
|
renderMathInElement(element as HTMLElement, {
|
||||||
delimiters: [
|
delimiters: [
|
||||||
|
|||||||
@ -23,7 +23,11 @@ export function startResize(ctx: ResizeContext, panel: PanelKey, event: MouseEve
|
|||||||
|
|
||||||
if (panel === 'right' && ctx.rightCollapsed) {
|
if (panel === 'right' && ctx.rightCollapsed) {
|
||||||
ctx.rightCollapsed = false;
|
ctx.rightCollapsed = false;
|
||||||
if (typeof ctx.rightWidth === 'number' && typeof ctx.minPanelWidth === 'number' && ctx.rightWidth < ctx.minPanelWidth) {
|
if (
|
||||||
|
typeof ctx.rightWidth === 'number' &&
|
||||||
|
typeof ctx.minPanelWidth === 'number' &&
|
||||||
|
ctx.rightWidth < ctx.minPanelWidth
|
||||||
|
) {
|
||||||
ctx.rightWidth = ctx.minPanelWidth;
|
ctx.rightWidth = ctx.minPanelWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,7 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
|
|||||||
const attempts = [0, 16, 60, 150, 320, 520]; // 多次尝试覆盖布局抖动/异步伸缩
|
const attempts = [0, 16, 60, 150, 320, 520]; // 多次尝试覆盖布局抖动/异步伸缩
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const useSmooth =
|
const useSmooth =
|
||||||
options?.behavior === 'smooth' &&
|
options?.behavior === 'smooth' && typeof (messagesArea as HTMLElement).scrollTo === 'function';
|
||||||
typeof (messagesArea as HTMLElement).scrollTo === 'function';
|
|
||||||
|
|
||||||
const perform = (idx: number) => {
|
const perform = (idx: number) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@ -97,7 +96,11 @@ export function toggleScrollLock(ctx: ScrollContext) {
|
|||||||
|
|
||||||
// 没有模型输出时:允许点击,但不切换锁定,仅单次滚动到底部
|
// 没有模型输出时:允许点击,但不切换锁定,仅单次滚动到底部
|
||||||
if (!active) {
|
if (!active) {
|
||||||
scrollToBottom(ctx, { ignoreUserScrolling: true, resetUserScrolling: true, behavior: 'smooth' });
|
scrollToBottom(ctx, {
|
||||||
|
ignoreUserScrolling: true,
|
||||||
|
resetUserScrolling: true,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
return ctx.autoScrollEnabled ?? false;
|
return ctx.autoScrollEnabled ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export const useChatStore = defineStore('chat', {
|
|||||||
thinkingScrollLocks: new Map<string, boolean>()
|
thinkingScrollLocks: new Map<string, boolean>()
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
isScrollLocked: state => state.autoScrollEnabled && !state.userScrolling
|
isScrollLocked: (state) => state.autoScrollEnabled && !state.userScrolling
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setStreamingMessage(active: boolean) {
|
setStreamingMessage(active: boolean) {
|
||||||
@ -315,7 +315,8 @@ export const useChatStore = defineStore('chat', {
|
|||||||
}
|
}
|
||||||
if (msg.activeThinkingId) {
|
if (msg.activeThinkingId) {
|
||||||
const found = msg.actions.find(
|
const found = msg.actions.find(
|
||||||
(action: any) => action && action.id === msg.activeThinkingId && action.type === 'thinking'
|
(action: any) =>
|
||||||
|
action && action.id === msg.activeThinkingId && action.type === 'thinking'
|
||||||
);
|
);
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
|
|||||||
@ -47,7 +47,7 @@ function buildNodes(treeMap: Record<string, any> | undefined): FileNode[] {
|
|||||||
if (!treeMap) {
|
if (!treeMap) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const entries = Object.keys(treeMap).map(name => {
|
const entries = Object.keys(treeMap).map((name) => {
|
||||||
const node = treeMap[name] || {};
|
const node = treeMap[name] || {};
|
||||||
if (node.type === 'folder') {
|
if (node.type === 'folder') {
|
||||||
return {
|
return {
|
||||||
@ -129,7 +129,7 @@ export const useFileStore = defineStore('file', {
|
|||||||
const validFolderPaths = new Set<string>();
|
const validFolderPaths = new Set<string>();
|
||||||
|
|
||||||
const ensureExpansion = (list: FileNode[]) => {
|
const ensureExpansion = (list: FileNode[]) => {
|
||||||
list.forEach(item => {
|
list.forEach((item) => {
|
||||||
if (item.type === 'folder') {
|
if (item.type === 'folder') {
|
||||||
validFolderPaths.add(item.path);
|
validFolderPaths.add(item.path);
|
||||||
if (expanded[item.path] === undefined) {
|
if (expanded[item.path] === undefined) {
|
||||||
@ -141,7 +141,7 @@ export const useFileStore = defineStore('file', {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ensureExpansion(nodes);
|
ensureExpansion(nodes);
|
||||||
Object.keys(expanded).forEach(path => {
|
Object.keys(expanded).forEach((path) => {
|
||||||
if (!validFolderPaths.has(path)) {
|
if (!validFolderPaths.has(path)) {
|
||||||
delete expanded[path];
|
delete expanded[path];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const useInputStore = defineStore('input', {
|
|||||||
this.selectedImages = next.slice(0, 9);
|
this.selectedImages = next.slice(0, 9);
|
||||||
},
|
},
|
||||||
removeSelectedImage(path: string) {
|
removeSelectedImage(path: string) {
|
||||||
this.selectedImages = this.selectedImages.filter(item => item !== path);
|
this.selectedImages = this.selectedImages.filter((item) => item !== path);
|
||||||
},
|
},
|
||||||
clearSelectedImages() {
|
clearSelectedImages() {
|
||||||
this.selectedImages = [];
|
this.selectedImages = [];
|
||||||
@ -106,7 +106,7 @@ export const useInputStore = defineStore('input', {
|
|||||||
this.selectedVideos = [path];
|
this.selectedVideos = [path];
|
||||||
},
|
},
|
||||||
removeSelectedVideo(path: string) {
|
removeSelectedVideo(path: string) {
|
||||||
this.selectedVideos = this.selectedVideos.filter(item => item !== path);
|
this.selectedVideos = this.selectedVideos.filter((item) => item !== path);
|
||||||
},
|
},
|
||||||
clearSelectedVideos() {
|
clearSelectedVideos() {
|
||||||
this.selectedVideos = [];
|
this.selectedVideos = [];
|
||||||
|
|||||||
@ -72,21 +72,21 @@ export const useModelStore = defineStore('model', {
|
|||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
currentModel(state): ModelOption {
|
currentModel(state): ModelOption {
|
||||||
return state.models.find(m => m.key === state.currentModelKey) || state.models[0];
|
return state.models.find((m) => m.key === state.currentModelKey) || state.models[0];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setModels(models: ModelOption[]) {
|
setModels(models: ModelOption[]) {
|
||||||
if (!Array.isArray(models) || !models.length) return;
|
if (!Array.isArray(models) || !models.length) return;
|
||||||
this.models = models;
|
this.models = models;
|
||||||
const exists = this.models.some(m => m.key === this.currentModelKey);
|
const exists = this.models.some((m) => m.key === this.currentModelKey);
|
||||||
if (!exists && this.models[0]) {
|
if (!exists && this.models[0]) {
|
||||||
this.currentModelKey = this.models[0].key;
|
this.currentModelKey = this.models[0].key;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setModel(key: ModelKey) {
|
setModel(key: ModelKey) {
|
||||||
if (this.currentModelKey === key) return;
|
if (this.currentModelKey === key) return;
|
||||||
const exists = this.models.some(m => m.key === key);
|
const exists = this.models.some((m) => m.key === key);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
this.currentModelKey = key;
|
this.currentModelKey = key;
|
||||||
}
|
}
|
||||||
@ -112,7 +112,8 @@ export const useModelStore = defineStore('model', {
|
|||||||
supportsImage,
|
supportsImage,
|
||||||
supportsVideo,
|
supportsVideo,
|
||||||
contextWindow: typeof item.context_window === 'number' ? item.context_window : null,
|
contextWindow: typeof item.context_window === 'number' ? item.context_window : null,
|
||||||
maxOutputTokens: typeof item.max_output_tokens === 'number' ? item.max_output_tokens : null,
|
maxOutputTokens:
|
||||||
|
typeof item.max_output_tokens === 'number' ? item.max_output_tokens : null,
|
||||||
fastOnly: !!item.fast_only,
|
fastOnly: !!item.fast_only,
|
||||||
supportsThinking: !!item.supports_thinking,
|
supportsThinking: !!item.supports_thinking,
|
||||||
deepOnly: !!item.deep_only
|
deepOnly: !!item.deep_only
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { MonitorBubbleOptions, MonitorDriver, MonitorSceneRuntime } from '@/components/chat/monitor/types';
|
import type {
|
||||||
|
MonitorBubbleOptions,
|
||||||
|
MonitorDriver,
|
||||||
|
MonitorSceneRuntime
|
||||||
|
} from '@/components/chat/monitor/types';
|
||||||
import { getSceneProgressLabel } from '@/components/chat/monitor/progressMap';
|
import { getSceneProgressLabel } from '@/components/chat/monitor/progressMap';
|
||||||
import { useConnectionStore } from '@/stores/connection';
|
import { useConnectionStore } from '@/stores/connection';
|
||||||
|
|
||||||
@ -116,7 +120,8 @@ const TOOL_SCENE_MAP: Record<string, string> = {
|
|||||||
terminal_run: 'runCommand'
|
terminal_run: 'runCommand'
|
||||||
};
|
};
|
||||||
|
|
||||||
const randomId = (prefix = 'evt') => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
const randomId = (prefix = 'evt') =>
|
||||||
|
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
|
||||||
export const useMonitorStore = defineStore('monitor', {
|
export const useMonitorStore = defineStore('monitor', {
|
||||||
state: (): MonitorState => ({
|
state: (): MonitorState => ({
|
||||||
@ -140,7 +145,7 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
isLocked: state => state.playing || state.queue.length > 0
|
isLocked: (state) => state.playing || state.queue.length > 0
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
registerDriver(driver: MonitorDriver) {
|
registerDriver(driver: MonitorDriver) {
|
||||||
@ -179,7 +184,10 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
this.driver?.setDesktopRoots(this.lastTreeSnapshot, { immediate: true });
|
this.driver?.setDesktopRoots(this.lastTreeSnapshot, { immediate: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const folders = tree.filter(node => node && node.type === 'folder').map(node => node.name).filter(Boolean);
|
const folders = tree
|
||||||
|
.filter((node) => node && node.type === 'folder')
|
||||||
|
.map((node) => node.name)
|
||||||
|
.filter(Boolean);
|
||||||
this.lastTreeSnapshot = folders.length ? folders : [...DEFAULT_ROOTS];
|
this.lastTreeSnapshot = folders.length ? folders : [...DEFAULT_ROOTS];
|
||||||
this.driver?.setDesktopRoots(this.lastTreeSnapshot);
|
this.driver?.setDesktopRoots(this.lastTreeSnapshot);
|
||||||
},
|
},
|
||||||
@ -207,7 +215,7 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
this.awaitingTools = {};
|
this.awaitingTools = {};
|
||||||
}
|
}
|
||||||
if (!preservePendingResults) {
|
if (!preservePendingResults) {
|
||||||
Object.values(this.pendingResults).forEach(entry => {
|
Object.values(this.pendingResults).forEach((entry) => {
|
||||||
if (entry?.timeout) {
|
if (entry?.timeout) {
|
||||||
clearTimeout(entry.timeout);
|
clearTimeout(entry.timeout);
|
||||||
}
|
}
|
||||||
@ -222,7 +230,12 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
this.thinkingActive = false;
|
this.thinkingActive = false;
|
||||||
this.pendingProgressScene = null;
|
this.pendingProgressScene = null;
|
||||||
this.progressIndicator = { id: null, label: '', scene: null };
|
this.progressIndicator = { id: null, label: '', scene: null };
|
||||||
this.driver?.resetScene({ desktopRoots: this.lastTreeSnapshot, preserveBubble, preservePointer, preserveWindows });
|
this.driver?.resetScene({
|
||||||
|
desktopRoots: this.lastTreeSnapshot,
|
||||||
|
preserveBubble,
|
||||||
|
preservePointer,
|
||||||
|
preserveWindows
|
||||||
|
});
|
||||||
this.driver?.setManualInteractionEnabled(!this.isLocked);
|
this.driver?.setManualInteractionEnabled(!this.isLocked);
|
||||||
},
|
},
|
||||||
setProgressIndicator(payload: { id: string | null; label: string; scene: string }) {
|
setProgressIndicator(payload: { id: string | null; label: string; scene: string }) {
|
||||||
@ -268,7 +281,10 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
if (id && this.progressIndicator.id && id !== this.progressIndicator.id) {
|
if (id && this.progressIndicator.id && id !== this.progressIndicator.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
monitorProgressDebug('progress-indicator:clear', { id: this.progressIndicator.id, label: this.progressIndicator.label });
|
monitorProgressDebug('progress-indicator:clear', {
|
||||||
|
id: this.progressIndicator.id,
|
||||||
|
label: this.progressIndicator.label
|
||||||
|
});
|
||||||
this.progressIndicator = { id: null, label: '', scene: null };
|
this.progressIndicator = { id: null, label: '', scene: null };
|
||||||
},
|
},
|
||||||
setStatus(label: string) {
|
setStatus(label: string) {
|
||||||
@ -395,7 +411,11 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
monitorProgressDebug('enqueueToolEvent:preview-now', { tool: payload.name, script, id });
|
monitorProgressDebug('enqueueToolEvent:preview-now', { tool: payload.name, script, id });
|
||||||
this.driver.previewSceneProgress(script);
|
this.driver.previewSceneProgress(script);
|
||||||
} else {
|
} else {
|
||||||
monitorProgressDebug('enqueueToolEvent:pending-preview', { tool: payload.name, script, id });
|
monitorProgressDebug('enqueueToolEvent:pending-preview', {
|
||||||
|
tool: payload.name,
|
||||||
|
script,
|
||||||
|
id
|
||||||
|
});
|
||||||
this.pendingProgressScene = script;
|
this.pendingProgressScene = script;
|
||||||
}
|
}
|
||||||
if (this.bubbleActive) {
|
if (this.bubbleActive) {
|
||||||
@ -404,7 +424,11 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
};
|
};
|
||||||
if (this.bubbleActive && recentSpeechGap >= 0 && recentSpeechGap < MIN_SPEECH_VISIBLE_MS) {
|
if (this.bubbleActive && recentSpeechGap >= 0 && recentSpeechGap < MIN_SPEECH_VISIBLE_MS) {
|
||||||
const delay = MIN_SPEECH_VISIBLE_MS - recentSpeechGap;
|
const delay = MIN_SPEECH_VISIBLE_MS - recentSpeechGap;
|
||||||
monitorLifecycleLog('enqueue:delay-preview-for-speech', { delay, recentSpeechGap, tool: payload.name });
|
monitorLifecycleLog('enqueue:delay-preview-for-speech', {
|
||||||
|
delay,
|
||||||
|
recentSpeechGap,
|
||||||
|
tool: payload.name
|
||||||
|
});
|
||||||
setTimeout(doPreview, delay);
|
setTimeout(doPreview, delay);
|
||||||
} else {
|
} else {
|
||||||
doPreview();
|
doPreview();
|
||||||
@ -456,12 +480,12 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let resolver: (value: any) => void = () => {};
|
let resolver: (value: any) => void = () => {};
|
||||||
const promise = new Promise<any>(resolve => {
|
const promise = new Promise<any>((resolve) => {
|
||||||
resolver = resolve;
|
resolver = resolve;
|
||||||
});
|
});
|
||||||
const entry: PendingResultEntry = {
|
const entry: PendingResultEntry = {
|
||||||
promise,
|
promise,
|
||||||
resolve: value => {
|
resolve: (value) => {
|
||||||
if (!entry.settled) {
|
if (!entry.settled) {
|
||||||
entry.settled = true;
|
entry.settled = true;
|
||||||
if (entry.timeout) {
|
if (entry.timeout) {
|
||||||
@ -575,43 +599,47 @@ export const useMonitorStore = defineStore('monitor', {
|
|||||||
this.setStatus('待机');
|
this.setStatus('待机');
|
||||||
monitorLifecycleLog('queue-end');
|
monitorLifecycleLog('queue-end');
|
||||||
},
|
},
|
||||||
async runScript(event: MonitorEvent) {
|
async runScript(event: MonitorEvent) {
|
||||||
if (!this.driver) {
|
if (!this.driver) {
|
||||||
this.queue.unshift(event);
|
this.queue.unshift(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const waitKey =
|
const waitKey =
|
||||||
event.payload?.executionId || event.payload?.execution_id || event.id || event.payload?.id || null;
|
event.payload?.executionId ||
|
||||||
monitorLifecycleLog('runScript:start', {
|
event.payload?.execution_id ||
|
||||||
script: event.script,
|
event.id ||
|
||||||
waitKey,
|
event.payload?.id ||
|
||||||
payloadTool: event.payload?.name,
|
null;
|
||||||
payloadId: event.payload?.id
|
monitorLifecycleLog('runScript:start', {
|
||||||
});
|
script: event.script,
|
||||||
|
waitKey,
|
||||||
|
payloadTool: event.payload?.name,
|
||||||
|
payloadId: event.payload?.id
|
||||||
|
});
|
||||||
|
|
||||||
let playbackResult: any = undefined;
|
let playbackResult: any = undefined;
|
||||||
let playbackSettled = false;
|
let playbackSettled = false;
|
||||||
const waitForCompletion = async () => {
|
const waitForCompletion = async () => {
|
||||||
if (!waitKey) {
|
if (!waitKey) {
|
||||||
playbackSettled = true;
|
playbackSettled = true;
|
||||||
if (playbackResult === undefined) {
|
if (playbackResult === undefined) {
|
||||||
|
playbackResult = null;
|
||||||
|
}
|
||||||
|
return playbackResult;
|
||||||
|
}
|
||||||
|
if (playbackSettled) {
|
||||||
|
return playbackResult;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
playbackResult = await this.waitForResult(waitKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('monitor waitForResult error', error);
|
||||||
playbackResult = null;
|
playbackResult = null;
|
||||||
|
} finally {
|
||||||
|
playbackSettled = true;
|
||||||
}
|
}
|
||||||
return playbackResult;
|
return playbackResult;
|
||||||
}
|
};
|
||||||
if (playbackSettled) {
|
|
||||||
return playbackResult;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
playbackResult = await this.waitForResult(waitKey);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('monitor waitForResult error', error);
|
|
||||||
playbackResult = null;
|
|
||||||
} finally {
|
|
||||||
playbackSettled = true;
|
|
||||||
}
|
|
||||||
return playbackResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformStatus = (raw?: string) => {
|
const transformStatus = (raw?: string) => {
|
||||||
const label = typeof raw === 'string' && raw.trim().length ? raw.trim() : '进行中';
|
const label = typeof raw === 'string' && raw.trim().length ? raw.trim() : '进行中';
|
||||||
|
|||||||
@ -117,8 +117,10 @@ const loadExperimentState = (): ExperimentState => {
|
|||||||
|
|
||||||
// 兼容旧版 stackedBlocksEnabled
|
// 兼容旧版 stackedBlocksEnabled
|
||||||
let blockDisplayMode: BlockDisplayMode = defaultExperimentState().blockDisplayMode;
|
let blockDisplayMode: BlockDisplayMode = defaultExperimentState().blockDisplayMode;
|
||||||
if (typeof parsed?.blockDisplayMode === 'string' &&
|
if (
|
||||||
['traditional', 'stacked', 'minimal'].includes(parsed.blockDisplayMode)) {
|
typeof parsed?.blockDisplayMode === 'string' &&
|
||||||
|
['traditional', 'stacked', 'minimal'].includes(parsed.blockDisplayMode)
|
||||||
|
) {
|
||||||
blockDisplayMode = parsed.blockDisplayMode;
|
blockDisplayMode = parsed.blockDisplayMode;
|
||||||
} else if (typeof parsed?.stackedBlocksEnabled === 'boolean') {
|
} else if (typeof parsed?.stackedBlocksEnabled === 'boolean') {
|
||||||
// 兼容旧版:true -> stacked, false -> traditional
|
// 兼容旧版:true -> stacked, false -> traditional
|
||||||
@ -207,7 +209,9 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
applyPersonalizationData(data: any) {
|
applyPersonalizationData(data: any) {
|
||||||
// 若后端未返回默认模型(旧版本接口),保持当前已选模型而不是回退为 Kimi
|
// 若后端未返回默认模型(旧版本接口),保持当前已选模型而不是回退为 Kimi
|
||||||
const fallbackModel =
|
const fallbackModel =
|
||||||
(this.form && typeof this.form.default_model === 'string' ? this.form.default_model : null) || 'kimi-k2.5';
|
(this.form && typeof this.form.default_model === 'string'
|
||||||
|
? this.form.default_model
|
||||||
|
: null) || 'kimi-k2.5';
|
||||||
this.form = {
|
this.form = {
|
||||||
enabled: !!data.enabled,
|
enabled: !!data.enabled,
|
||||||
auto_generate_title: data.auto_generate_title !== false,
|
auto_generate_title: data.auto_generate_title !== false,
|
||||||
@ -215,8 +219,10 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
skill_hints_enabled: !!data.skill_hints_enabled,
|
skill_hints_enabled: !!data.skill_hints_enabled,
|
||||||
skill_strict_terminal_enabled: !!data.skill_strict_terminal_enabled,
|
skill_strict_terminal_enabled: !!data.skill_strict_terminal_enabled,
|
||||||
skill_strict_sub_agent_enabled: !!data.skill_strict_sub_agent_enabled,
|
skill_strict_sub_agent_enabled: !!data.skill_strict_sub_agent_enabled,
|
||||||
skill_strict_run_command_foreground_enabled: !!data.skill_strict_run_command_foreground_enabled,
|
skill_strict_run_command_foreground_enabled:
|
||||||
skill_strict_run_command_background_enabled: !!data.skill_strict_run_command_background_enabled,
|
!!data.skill_strict_run_command_foreground_enabled,
|
||||||
|
skill_strict_run_command_background_enabled:
|
||||||
|
!!data.skill_strict_run_command_background_enabled,
|
||||||
silent_tool_disable: !!data.silent_tool_disable,
|
silent_tool_disable: !!data.silent_tool_disable,
|
||||||
enhanced_tool_display: data.enhanced_tool_display !== false,
|
enhanced_tool_display: data.enhanced_tool_display !== false,
|
||||||
enabled_skills: Array.isArray(data.enabled_skills)
|
enabled_skills: Array.isArray(data.enabled_skills)
|
||||||
@ -228,21 +234,36 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
profession: data.profession || '',
|
profession: data.profession || '',
|
||||||
tone: data.tone || '',
|
tone: data.tone || '',
|
||||||
considerations: Array.isArray(data.considerations) ? [...data.considerations] : [],
|
considerations: Array.isArray(data.considerations) ? [...data.considerations] : [],
|
||||||
thinking_interval: typeof data.thinking_interval === 'number' ? data.thinking_interval : null,
|
thinking_interval:
|
||||||
disabled_tool_categories: Array.isArray(data.disabled_tool_categories) ? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string') : [],
|
typeof data.thinking_interval === 'number' ? data.thinking_interval : null,
|
||||||
|
disabled_tool_categories: Array.isArray(data.disabled_tool_categories)
|
||||||
|
? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string')
|
||||||
|
: [],
|
||||||
default_run_mode:
|
default_run_mode:
|
||||||
typeof data.default_run_mode === 'string' && RUN_MODE_OPTIONS.includes(data.default_run_mode as RunMode)
|
typeof data.default_run_mode === 'string' &&
|
||||||
? data.default_run_mode as RunMode
|
RUN_MODE_OPTIONS.includes(data.default_run_mode as RunMode)
|
||||||
|
? (data.default_run_mode as RunMode)
|
||||||
: null,
|
: null,
|
||||||
default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel,
|
default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel,
|
||||||
image_compression: typeof data.image_compression === 'string' ? data.image_compression : 'original',
|
image_compression:
|
||||||
|
typeof data.image_compression === 'string' ? data.image_compression : 'original',
|
||||||
auto_shallow_compress_enabled: !!data.auto_shallow_compress_enabled,
|
auto_shallow_compress_enabled: !!data.auto_shallow_compress_enabled,
|
||||||
auto_deep_compress_enabled: !!data.auto_deep_compress_enabled,
|
auto_deep_compress_enabled: !!data.auto_deep_compress_enabled,
|
||||||
shallow_compress_trigger_tokens: this.normalizeCompressionNumber(data.shallow_compress_trigger_tokens),
|
shallow_compress_trigger_tokens: this.normalizeCompressionNumber(
|
||||||
shallow_compress_keep_recent_tools: this.normalizeCompressionNumber(data.shallow_compress_keep_recent_tools),
|
data.shallow_compress_trigger_tokens
|
||||||
shallow_compress_max_replace_per_round: this.normalizeCompressionNumber(data.shallow_compress_max_replace_per_round),
|
),
|
||||||
shallow_compress_trigger_tool_calls_interval: this.normalizeCompressionNumber(data.shallow_compress_trigger_tool_calls_interval),
|
shallow_compress_keep_recent_tools: this.normalizeCompressionNumber(
|
||||||
deep_compress_trigger_tokens: this.normalizeCompressionNumber(data.deep_compress_trigger_tokens)
|
data.shallow_compress_keep_recent_tools
|
||||||
|
),
|
||||||
|
shallow_compress_max_replace_per_round: this.normalizeCompressionNumber(
|
||||||
|
data.shallow_compress_max_replace_per_round
|
||||||
|
),
|
||||||
|
shallow_compress_trigger_tool_calls_interval: this.normalizeCompressionNumber(
|
||||||
|
data.shallow_compress_trigger_tool_calls_interval
|
||||||
|
),
|
||||||
|
deep_compress_trigger_tokens: this.normalizeCompressionNumber(
|
||||||
|
data.deep_compress_trigger_tokens
|
||||||
|
)
|
||||||
};
|
};
|
||||||
this.clearFeedback();
|
this.clearFeedback();
|
||||||
},
|
},
|
||||||
@ -279,7 +300,9 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.toolCategories = payload.tool_categories
|
this.toolCategories = payload.tool_categories
|
||||||
.map((item: { id?: string; label?: string } = {}) => ({
|
.map((item: { id?: string; label?: string } = {}) => ({
|
||||||
id: typeof item.id === 'string' ? item.id : String(item.id ?? ''),
|
id: typeof item.id === 'string' ? item.id : String(item.id ?? ''),
|
||||||
label: (item.label && String(item.label)) || (typeof item.id === 'string' ? item.id : String(item.id ?? ''))
|
label:
|
||||||
|
(item.label && String(item.label)) ||
|
||||||
|
(typeof item.id === 'string' ? item.id : String(item.id ?? ''))
|
||||||
}))
|
}))
|
||||||
.filter((item: { id: string }) => !!item.id);
|
.filter((item: { id: string }) => !!item.id);
|
||||||
} else {
|
} else {
|
||||||
@ -289,7 +312,9 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.skillsCatalog = payload.skills_catalog
|
this.skillsCatalog = payload.skills_catalog
|
||||||
.map((item: { id?: string; label?: string; description?: string } = {}) => ({
|
.map((item: { id?: string; label?: string; description?: string } = {}) => ({
|
||||||
id: typeof item.id === 'string' ? item.id : String(item.id ?? ''),
|
id: typeof item.id === 'string' ? item.id : String(item.id ?? ''),
|
||||||
label: (item.label && String(item.label)) || (typeof item.id === 'string' ? item.id : String(item.id ?? '')),
|
label:
|
||||||
|
(item.label && String(item.label)) ||
|
||||||
|
(typeof item.id === 'string' ? item.id : String(item.id ?? '')),
|
||||||
description: typeof item.description === 'string' ? item.description : undefined
|
description: typeof item.description === 'string' ? item.description : undefined
|
||||||
}))
|
}))
|
||||||
.filter((item: { id: string }) => !!item.id);
|
.filter((item: { id: string }) => !!item.id);
|
||||||
@ -348,9 +373,11 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.error = '';
|
this.error = '';
|
||||||
try {
|
try {
|
||||||
const shallowTrigger =
|
const shallowTrigger =
|
||||||
this.normalizeCompressionNumber(this.form.shallow_compress_trigger_tokens) ?? DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS;
|
this.normalizeCompressionNumber(this.form.shallow_compress_trigger_tokens) ??
|
||||||
|
DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS;
|
||||||
const deepTrigger =
|
const deepTrigger =
|
||||||
this.normalizeCompressionNumber(this.form.deep_compress_trigger_tokens) ?? DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS;
|
this.normalizeCompressionNumber(this.form.deep_compress_trigger_tokens) ??
|
||||||
|
DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS;
|
||||||
if (deepTrigger <= shallowTrigger) {
|
if (deepTrigger <= shallowTrigger) {
|
||||||
throw new Error('深压缩触发上下文必须大于浅压缩触发上下文');
|
throw new Error('深压缩触发上下文必须大于浅压缩触发上下文');
|
||||||
}
|
}
|
||||||
@ -546,7 +573,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
const resp = await fetch('/logout', {
|
const resp = await fetch('/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
cache: 'no-store',
|
cache: 'no-store'
|
||||||
});
|
});
|
||||||
console.info('[auth-debug] logout POST status:', resp.status);
|
console.info('[auth-debug] logout POST status:', resp.status);
|
||||||
let payload: any = null;
|
let payload: any = null;
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
export interface EffectivePolicy {
|
export interface EffectivePolicy {
|
||||||
categories: Record<string, { label: string; tools: string[]; default_enabled?: boolean; locked?: boolean; locked_state?: boolean | null }>;
|
categories: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
tools: string[];
|
||||||
|
default_enabled?: boolean;
|
||||||
|
locked?: boolean;
|
||||||
|
locked_state?: boolean | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
forced_category_states: Record<string, boolean | null>;
|
forced_category_states: Record<string, boolean | null>;
|
||||||
disabled_models: string[];
|
disabled_models: string[];
|
||||||
ui_blocks: Record<string, boolean>;
|
ui_blocks: Record<string, boolean>;
|
||||||
|
|||||||
@ -130,8 +130,10 @@ export const useResourceStore = defineStore('resource', {
|
|||||||
const response = await fetch(`/api/conversations/${conversationId}/token-statistics`);
|
const response = await fetch(`/api/conversations/${conversationId}/token-statistics`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success && data.data) {
|
if (data.success && data.data) {
|
||||||
this.currentConversationTokens.cumulative_input_tokens = data.data.total_input_tokens || 0;
|
this.currentConversationTokens.cumulative_input_tokens =
|
||||||
this.currentConversationTokens.cumulative_output_tokens = data.data.total_output_tokens || 0;
|
data.data.total_input_tokens || 0;
|
||||||
|
this.currentConversationTokens.cumulative_output_tokens =
|
||||||
|
data.data.total_output_tokens || 0;
|
||||||
this.currentConversationTokens.cumulative_total_tokens = data.data.total_tokens || 0;
|
this.currentConversationTokens.cumulative_total_tokens = data.data.total_tokens || 0;
|
||||||
if (typeof data.data.current_context_tokens === 'number') {
|
if (typeof data.data.current_context_tokens === 'number') {
|
||||||
this.currentContextTokens = data.data.current_context_tokens;
|
this.currentContextTokens = data.data.current_context_tokens;
|
||||||
@ -151,7 +153,9 @@ export const useResourceStore = defineStore('resource', {
|
|||||||
this.projectStorage.limit_bytes = project.limit_bytes ?? null;
|
this.projectStorage.limit_bytes = project.limit_bytes ?? null;
|
||||||
this.projectStorage.limit_label =
|
this.projectStorage.limit_label =
|
||||||
project.limit_label ||
|
project.limit_label ||
|
||||||
(project.limit_bytes ? `${(project.limit_bytes / (1024 * 1024)).toFixed(0)} MB` : '未限制');
|
(project.limit_bytes
|
||||||
|
? `${(project.limit_bytes / (1024 * 1024)).toFixed(0)} MB`
|
||||||
|
: '未限制');
|
||||||
if (project.limit_bytes) {
|
if (project.limit_bytes) {
|
||||||
const pct =
|
const pct =
|
||||||
typeof project.usage_percent === 'number'
|
typeof project.usage_percent === 'number'
|
||||||
@ -177,8 +181,12 @@ export const useResourceStore = defineStore('resource', {
|
|||||||
if (stats && typeof stats.timestamp === 'number') {
|
if (stats && typeof stats.timestamp === 'number') {
|
||||||
const currentSample: ContainerSample = {
|
const currentSample: ContainerSample = {
|
||||||
timestamp: stats.timestamp,
|
timestamp: stats.timestamp,
|
||||||
rx_bytes: stats.net_io && typeof stats.net_io.rx_bytes === 'number' ? stats.net_io.rx_bytes : null,
|
rx_bytes:
|
||||||
tx_bytes: stats.net_io && typeof stats.net_io.tx_bytes === 'number' ? stats.net_io.tx_bytes : null
|
stats.net_io && typeof stats.net_io.rx_bytes === 'number'
|
||||||
|
? stats.net_io.rx_bytes
|
||||||
|
: null,
|
||||||
|
tx_bytes:
|
||||||
|
stats.net_io && typeof stats.net_io.tx_bytes === 'number' ? stats.net_io.tx_bytes : null
|
||||||
};
|
};
|
||||||
const last = this.lastContainerSample;
|
const last = this.lastContainerSample;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -2,391 +2,409 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { debugLog } from '../app/methods/common';
|
import { debugLog } from '../app/methods/common';
|
||||||
|
|
||||||
const debugNotifyLog = (..._args: any[]) => {};
|
const debugNotifyLog = (...args: any[]) => {
|
||||||
|
void args;
|
||||||
|
};
|
||||||
const keyNotifyLog = (...args: any[]) => {
|
const keyNotifyLog = (...args: any[]) => {
|
||||||
console.log(...args);
|
console.log(...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTaskStore = defineStore('task', {
|
export const useTaskStore = defineStore('task', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
currentTaskId: null as string | null,
|
currentTaskId: null as string | null,
|
||||||
lastEventIndex: 0,
|
lastEventIndex: 0,
|
||||||
pollingInterval: null as number | null,
|
pollingInterval: null as number | null,
|
||||||
taskStatus: 'idle' as 'idle' | 'running' | 'succeeded' | 'failed' | 'canceled',
|
taskStatus: 'idle' as 'idle' | 'running' | 'succeeded' | 'failed' | 'canceled',
|
||||||
isPolling: false,
|
isPolling: false,
|
||||||
pollingError: null as string | null,
|
pollingError: null as string | null,
|
||||||
pollingErrorCount: 0, // 新增错误计数
|
pollingErrorCount: 0, // 新增错误计数
|
||||||
taskCreatedAt: null as number | null,
|
taskCreatedAt: null as number | null,
|
||||||
taskUpdatedAt: null as number | null,
|
taskUpdatedAt: null as number | null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
hasActiveTask: (state) => state.currentTaskId !== null && state.taskStatus === 'running',
|
hasActiveTask: (state) => state.currentTaskId !== null && state.taskStatus === 'running',
|
||||||
isTaskCompleted: (state) => ['succeeded', 'failed', 'canceled'].includes(state.taskStatus),
|
isTaskCompleted: (state) => ['succeeded', 'failed', 'canceled'].includes(state.taskStatus)
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async createTask(
|
||||||
|
message: string,
|
||||||
|
images: any[] = [],
|
||||||
|
videos: any[] = [],
|
||||||
|
conversationId: string | null = null,
|
||||||
|
options: {
|
||||||
|
model_key?: string | null;
|
||||||
|
run_mode?: 'fast' | 'thinking' | 'deep' | null;
|
||||||
|
thinking_mode?: boolean | null;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
debugLog('[Task] 创建任务:', { message, conversationId });
|
||||||
|
|
||||||
|
const response = await fetch('/api/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message,
|
||||||
|
images,
|
||||||
|
videos,
|
||||||
|
conversation_id: conversationId,
|
||||||
|
model_key: options.model_key ?? undefined,
|
||||||
|
run_mode: options.run_mode ?? undefined,
|
||||||
|
thinking_mode:
|
||||||
|
typeof options.thinking_mode === 'boolean' ? options.thinking_mode : undefined
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || '创建任务失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || '创建任务失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentTaskId = result.data.task_id;
|
||||||
|
this.taskStatus = result.data.status;
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
this.taskCreatedAt = result.data.created_at;
|
||||||
|
this.pollingError = null;
|
||||||
|
|
||||||
|
debugLog('[Task] 任务创建成功:', result.data.task_id);
|
||||||
|
|
||||||
|
// 立即开始轮询
|
||||||
|
this.startPolling();
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
debugLog('[Task] 创建任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
async pollTaskEvents(eventHandler: (event: any) => void) {
|
||||||
async createTask(
|
if (!this.currentTaskId) {
|
||||||
message: string,
|
debugLog('[Task] 没有活跃任务,停止轮询');
|
||||||
images: any[] = [],
|
this.stopPolling();
|
||||||
videos: any[] = [],
|
return;
|
||||||
conversationId: string | null = null,
|
}
|
||||||
options: {
|
|
||||||
model_key?: string | null;
|
try {
|
||||||
run_mode?: 'fast' | 'thinking' | 'deep' | null;
|
const response = await fetch(
|
||||||
thinking_mode?: boolean | null;
|
`/api/tasks/${this.currentTaskId}?from=${this.lastEventIndex}`,
|
||||||
} = {}
|
{ signal: AbortSignal.timeout(5000) } // 5秒超时
|
||||||
) {
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || '轮询任务失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = result.data;
|
||||||
|
|
||||||
|
// 更新任务状态
|
||||||
|
this.taskStatus = data.status;
|
||||||
|
this.taskUpdatedAt = data.updated_at;
|
||||||
|
let sawTaskStoppedEvent = false;
|
||||||
|
|
||||||
|
// 处理新事件
|
||||||
|
if (data.events && data.events.length > 0) {
|
||||||
|
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
||||||
|
|
||||||
|
for (const event of data.events) {
|
||||||
|
if (event?.type === 'task_stopped') {
|
||||||
|
sawTaskStoppedEvent = true;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
debugLog('[Task] 创建任务:', { message, conversationId });
|
eventHandler(event);
|
||||||
|
} catch (err) {
|
||||||
const response = await fetch('/api/tasks', {
|
console.error('[Task] 处理事件失败:', err, event);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
message,
|
|
||||||
images,
|
|
||||||
videos,
|
|
||||||
conversation_id: conversationId,
|
|
||||||
model_key: options.model_key ?? undefined,
|
|
||||||
run_mode: options.run_mode ?? undefined,
|
|
||||||
thinking_mode: typeof options.thinking_mode === 'boolean' ? options.thinking_mode : undefined,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.error || '创建任务失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || '创建任务失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentTaskId = result.data.task_id;
|
|
||||||
this.taskStatus = result.data.status;
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
this.taskCreatedAt = result.data.created_at;
|
|
||||||
this.pollingError = null;
|
|
||||||
|
|
||||||
debugLog('[Task] 任务创建成功:', result.data.task_id);
|
|
||||||
|
|
||||||
// 立即开始轮询
|
|
||||||
this.startPolling();
|
|
||||||
|
|
||||||
return result.data;
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('[Task] 创建任务失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
const interesting = data.events.filter((e: any) =>
|
||||||
async pollTaskEvents(eventHandler: (event: any) => void) {
|
[
|
||||||
if (!this.currentTaskId) {
|
'user_message',
|
||||||
debugLog('[Task] 没有活跃任务,停止轮询');
|
'sub_agent_waiting',
|
||||||
this.stopPolling();
|
'task_complete',
|
||||||
return;
|
'task_stopped',
|
||||||
}
|
'error'
|
||||||
|
].includes(e?.type)
|
||||||
try {
|
);
|
||||||
const response = await fetch(
|
if (interesting.length > 0) {
|
||||||
`/api/tasks/${this.currentTaskId}?from=${this.lastEventIndex}`,
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] events', {
|
||||||
{ signal: AbortSignal.timeout(5000) } // 5秒超时
|
taskId: this.currentTaskId,
|
||||||
);
|
from: this.lastEventIndex,
|
||||||
|
nextOffset: data.next_offset,
|
||||||
if (!response.ok) {
|
count: interesting.length,
|
||||||
throw new Error(`HTTP ${response.status}`);
|
items: interesting.map((e: any) => ({
|
||||||
}
|
idx: e?.idx,
|
||||||
|
type: e?.type,
|
||||||
const result = await response.json();
|
sub_agent_notice: !!e?.data?.sub_agent_notice,
|
||||||
if (!result.success) {
|
has_running_sub_agents: e?.data?.has_running_sub_agents,
|
||||||
throw new Error(result.error || '轮询任务失败');
|
has_running_background_commands: e?.data?.has_running_background_commands,
|
||||||
}
|
task_id: e?.data?.task_id
|
||||||
|
}))
|
||||||
const data = result.data;
|
|
||||||
|
|
||||||
// 更新任务状态
|
|
||||||
this.taskStatus = data.status;
|
|
||||||
this.taskUpdatedAt = data.updated_at;
|
|
||||||
let sawTaskStoppedEvent = false;
|
|
||||||
|
|
||||||
// 处理新事件
|
|
||||||
if (data.events && data.events.length > 0) {
|
|
||||||
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
|
||||||
|
|
||||||
for (const event of data.events) {
|
|
||||||
if (event?.type === 'task_stopped') {
|
|
||||||
sawTaskStoppedEvent = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
eventHandler(event);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Task] 处理事件失败:', err, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const interesting = data.events.filter((e: any) =>
|
|
||||||
['user_message', 'sub_agent_waiting', 'task_complete', 'task_stopped', 'error'].includes(e?.type)
|
|
||||||
);
|
|
||||||
if (interesting.length > 0) {
|
|
||||||
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] events', {
|
|
||||||
taskId: this.currentTaskId,
|
|
||||||
from: this.lastEventIndex,
|
|
||||||
nextOffset: data.next_offset,
|
|
||||||
count: interesting.length,
|
|
||||||
items: interesting.map((e: any) => ({
|
|
||||||
idx: e?.idx,
|
|
||||||
type: e?.type,
|
|
||||||
sub_agent_notice: !!e?.data?.sub_agent_notice,
|
|
||||||
has_running_sub_agents: e?.data?.has_running_sub_agents,
|
|
||||||
has_running_background_commands: e?.data?.has_running_background_commands,
|
|
||||||
task_id: e?.data?.task_id
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastEventIndex = data.next_offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兜底:如果后端状态已经是 canceled 但未返回 task_stopped 事件,
|
|
||||||
// 补发一个本地停止事件,确保前端解除“停止中”状态。
|
|
||||||
if (data.status === 'canceled' && !sawTaskStoppedEvent) {
|
|
||||||
debugLog('[Task] 状态已取消但未收到 task_stopped,补发本地事件');
|
|
||||||
try {
|
|
||||||
eventHandler({
|
|
||||||
type: 'task_stopped',
|
|
||||||
data: {
|
|
||||||
task_id: data.task_id || this.currentTaskId,
|
|
||||||
conversation_id: data.conversation_id || null,
|
|
||||||
synthetic: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Task] 处理补发 task_stopped 失败:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果任务已完成,停止轮询
|
|
||||||
if (this.isTaskCompleted) {
|
|
||||||
debugLog('[Task] 任务已完成,停止轮询:', this.taskStatus);
|
|
||||||
this.stopPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置错误计数
|
|
||||||
this.pollingError = null;
|
|
||||||
this.pollingErrorCount = 0;
|
|
||||||
} catch (error) {
|
|
||||||
this.pollingErrorCount++;
|
|
||||||
this.pollingError = error.message;
|
|
||||||
|
|
||||||
console.error('[Task] 轮询失败:', error);
|
|
||||||
debugNotifyLog('[DEBUG_NOTIFY][task] pollTaskEvents:error', {
|
|
||||||
taskId: this.currentTaskId,
|
|
||||||
from: this.lastEventIndex,
|
|
||||||
error: error?.message || String(error),
|
|
||||||
pollingErrorCount: this.pollingErrorCount
|
|
||||||
});
|
|
||||||
|
|
||||||
// 连续失败 5 次后停止
|
|
||||||
if (this.pollingErrorCount >= 5) {
|
|
||||||
this.stopPolling();
|
|
||||||
// 通知用户
|
|
||||||
if ((window as any).__vueApp?.uiPushToast) {
|
|
||||||
(window as any).__vueApp.uiPushToast({
|
|
||||||
title: '轮询失败',
|
|
||||||
message: '请刷新页面重试',
|
|
||||||
type: 'error',
|
|
||||||
duration: 10000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startPolling(eventHandler?: (event: any) => void) {
|
|
||||||
if (this.isPolling) {
|
|
||||||
debugLog('[Task] 轮询已在运行');
|
|
||||||
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling:already-running', {
|
|
||||||
taskId: this.currentTaskId,
|
|
||||||
lastEventIndex: this.lastEventIndex
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.currentTaskId) {
|
|
||||||
debugLog('[Task] 没有任务ID,无法启动轮询');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('[Task] 启动轮询:', this.currentTaskId);
|
|
||||||
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling', {
|
|
||||||
taskId: this.currentTaskId,
|
|
||||||
lastEventIndex: this.lastEventIndex
|
|
||||||
});
|
});
|
||||||
this.isPolling = true;
|
}
|
||||||
|
|
||||||
// 如果没有传入 eventHandler,从根实例获取
|
this.lastEventIndex = data.next_offset;
|
||||||
const handler = eventHandler || ((window as any).__taskEventHandler);
|
}
|
||||||
|
|
||||||
if (!handler) {
|
// 兜底:如果后端状态已经是 canceled 但未返回 task_stopped 事件,
|
||||||
console.error('[Task] 没有事件处理器,无法启动轮询');
|
// 补发一个本地停止事件,确保前端解除“停止中”状态。
|
||||||
this.isPolling = false;
|
if (data.status === 'canceled' && !sawTaskStoppedEvent) {
|
||||||
return;
|
debugLog('[Task] 状态已取消但未收到 task_stopped,补发本地事件');
|
||||||
}
|
try {
|
||||||
|
eventHandler({
|
||||||
// 立即执行一次
|
type: 'task_stopped',
|
||||||
this.pollTaskEvents(handler);
|
data: {
|
||||||
|
task_id: data.task_id || this.currentTaskId,
|
||||||
// 设置定时轮询(150ms 间隔,接近流式输出效果)
|
conversation_id: data.conversation_id || null,
|
||||||
this.pollingInterval = window.setInterval(() => {
|
synthetic: true
|
||||||
this.pollTaskEvents(handler);
|
}
|
||||||
}, 150);
|
|
||||||
},
|
|
||||||
|
|
||||||
resumeTask(taskId: string, options: { status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled'; resetOffset?: boolean; eventHandler?: (event: any) => void } = {}) {
|
|
||||||
if (!taskId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
debugNotifyLog('[DEBUG_NOTIFY][task] resumeTask', {
|
|
||||||
incomingTaskId: taskId,
|
|
||||||
currentTaskId: this.currentTaskId,
|
|
||||||
options
|
|
||||||
});
|
});
|
||||||
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] resumeTask', {
|
} catch (err) {
|
||||||
incomingTaskId: taskId,
|
console.error('[Task] 处理补发 task_stopped 失败:', err);
|
||||||
currentTaskId: this.currentTaskId,
|
}
|
||||||
resetOffset: options?.resetOffset !== false
|
}
|
||||||
|
|
||||||
|
// 如果任务已完成,停止轮询
|
||||||
|
if (this.isTaskCompleted) {
|
||||||
|
debugLog('[Task] 任务已完成,停止轮询:', this.taskStatus);
|
||||||
|
this.stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置错误计数
|
||||||
|
this.pollingError = null;
|
||||||
|
this.pollingErrorCount = 0;
|
||||||
|
} catch (error) {
|
||||||
|
this.pollingErrorCount++;
|
||||||
|
this.pollingError = error.message;
|
||||||
|
|
||||||
|
console.error('[Task] 轮询失败:', error);
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] pollTaskEvents:error', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
from: this.lastEventIndex,
|
||||||
|
error: error?.message || String(error),
|
||||||
|
pollingErrorCount: this.pollingErrorCount
|
||||||
|
});
|
||||||
|
|
||||||
|
// 连续失败 5 次后停止
|
||||||
|
if (this.pollingErrorCount >= 5) {
|
||||||
|
this.stopPolling();
|
||||||
|
// 通知用户
|
||||||
|
if ((window as any).__vueApp?.uiPushToast) {
|
||||||
|
(window as any).__vueApp.uiPushToast({
|
||||||
|
title: '轮询失败',
|
||||||
|
message: '请刷新页面重试',
|
||||||
|
type: 'error',
|
||||||
|
duration: 10000
|
||||||
});
|
});
|
||||||
if (this.currentTaskId && this.currentTaskId !== taskId) {
|
}
|
||||||
this.stopPolling();
|
}
|
||||||
}
|
}
|
||||||
this.currentTaskId = taskId;
|
|
||||||
this.taskStatus = options.status || 'running';
|
|
||||||
if (options.resetOffset !== false) {
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
}
|
|
||||||
this.pollingError = null;
|
|
||||||
this.startPolling(options.eventHandler);
|
|
||||||
},
|
|
||||||
|
|
||||||
stopPolling() {
|
|
||||||
if (this.pollingInterval) {
|
|
||||||
debugLog('[Task] 停止轮询');
|
|
||||||
clearInterval(this.pollingInterval);
|
|
||||||
this.pollingInterval = null;
|
|
||||||
}
|
|
||||||
this.isPolling = false;
|
|
||||||
this.currentTaskId = null; // 清除任务 ID
|
|
||||||
},
|
|
||||||
|
|
||||||
async cancelTask() {
|
|
||||||
if (!this.currentTaskId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
debugLog('[Task] 取消任务:', this.currentTaskId);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('取消任务失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || '取消任务失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('[Task] 已发送取消请求,等待后端停止确认');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Task] 取消任务失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadRunningTask(conversationId: string | null = null) {
|
|
||||||
try {
|
|
||||||
debugLog('[Task] 查找运行中的任务');
|
|
||||||
|
|
||||||
const response = await fetch('/api/tasks');
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('获取任务列表失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || '获取任务列表失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找运行中的任务
|
|
||||||
const runningTask = result.data.find((task: any) =>
|
|
||||||
task.status === 'running' &&
|
|
||||||
(!conversationId || task.conversation_id === conversationId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (runningTask) {
|
|
||||||
debugLog('[Task] 发现运行中的任务:', runningTask.task_id);
|
|
||||||
|
|
||||||
this.currentTaskId = runningTask.task_id;
|
|
||||||
this.taskStatus = runningTask.status;
|
|
||||||
this.taskCreatedAt = runningTask.created_at;
|
|
||||||
this.taskUpdatedAt = runningTask.updated_at;
|
|
||||||
this.pollingError = null;
|
|
||||||
|
|
||||||
// 获取任务详情,计算已处理的事件数量
|
|
||||||
try {
|
|
||||||
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
|
|
||||||
if (detailResponse.ok) {
|
|
||||||
const detailResult = await detailResponse.json();
|
|
||||||
if (detailResult.success && detailResult.data.events) {
|
|
||||||
// 设置为当前事件数量,只获取新事件
|
|
||||||
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
|
|
||||||
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
|
|
||||||
} else {
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Task] 获取任务详情失败,从头开始:', error);
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return runningTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('[Task] 没有运行中的任务');
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Task] 加载运行中任务失败:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearTask() {
|
|
||||||
debugLog('[Task] 清理任务状态');
|
|
||||||
this.stopPolling();
|
|
||||||
this.currentTaskId = null;
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
this.taskStatus = 'idle';
|
|
||||||
this.pollingError = null;
|
|
||||||
this.taskCreatedAt = null;
|
|
||||||
this.taskUpdatedAt = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
resetForNewConversation() {
|
|
||||||
debugLog('[Task] 重置任务状态(新对话)');
|
|
||||||
this.stopPolling();
|
|
||||||
this.currentTaskId = null;
|
|
||||||
this.lastEventIndex = 0;
|
|
||||||
this.taskStatus = 'idle';
|
|
||||||
this.pollingError = null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startPolling(eventHandler?: (event: any) => void) {
|
||||||
|
if (this.isPolling) {
|
||||||
|
debugLog('[Task] 轮询已在运行');
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling:already-running', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
lastEventIndex: this.lastEventIndex
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.currentTaskId) {
|
||||||
|
debugLog('[Task] 没有任务ID,无法启动轮询');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[Task] 启动轮询:', this.currentTaskId);
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
lastEventIndex: this.lastEventIndex
|
||||||
|
});
|
||||||
|
this.isPolling = true;
|
||||||
|
|
||||||
|
// 如果没有传入 eventHandler,从根实例获取
|
||||||
|
const handler = eventHandler || (window as any).__taskEventHandler;
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
console.error('[Task] 没有事件处理器,无法启动轮询');
|
||||||
|
this.isPolling = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
this.pollTaskEvents(handler);
|
||||||
|
|
||||||
|
// 设置定时轮询(150ms 间隔,接近流式输出效果)
|
||||||
|
this.pollingInterval = window.setInterval(() => {
|
||||||
|
this.pollTaskEvents(handler);
|
||||||
|
}, 150);
|
||||||
|
},
|
||||||
|
|
||||||
|
resumeTask(
|
||||||
|
taskId: string,
|
||||||
|
options: {
|
||||||
|
status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled';
|
||||||
|
resetOffset?: boolean;
|
||||||
|
eventHandler?: (event: any) => void;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
if (!taskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] resumeTask', {
|
||||||
|
incomingTaskId: taskId,
|
||||||
|
currentTaskId: this.currentTaskId,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] resumeTask', {
|
||||||
|
incomingTaskId: taskId,
|
||||||
|
currentTaskId: this.currentTaskId,
|
||||||
|
resetOffset: options?.resetOffset !== false
|
||||||
|
});
|
||||||
|
if (this.currentTaskId && this.currentTaskId !== taskId) {
|
||||||
|
this.stopPolling();
|
||||||
|
}
|
||||||
|
this.currentTaskId = taskId;
|
||||||
|
this.taskStatus = options.status || 'running';
|
||||||
|
if (options.resetOffset !== false) {
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
}
|
||||||
|
this.pollingError = null;
|
||||||
|
this.startPolling(options.eventHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
if (this.pollingInterval) {
|
||||||
|
debugLog('[Task] 停止轮询');
|
||||||
|
clearInterval(this.pollingInterval);
|
||||||
|
this.pollingInterval = null;
|
||||||
|
}
|
||||||
|
this.isPolling = false;
|
||||||
|
this.currentTaskId = null; // 清除任务 ID
|
||||||
|
},
|
||||||
|
|
||||||
|
async cancelTask() {
|
||||||
|
if (!this.currentTaskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugLog('[Task] 取消任务:', this.currentTaskId);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('取消任务失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || '取消任务失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[Task] 已发送取消请求,等待后端停止确认');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Task] 取消任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadRunningTask(conversationId: string | null = null) {
|
||||||
|
try {
|
||||||
|
debugLog('[Task] 查找运行中的任务');
|
||||||
|
|
||||||
|
const response = await fetch('/api/tasks');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取任务列表失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || '获取任务列表失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找运行中的任务
|
||||||
|
const runningTask = result.data.find(
|
||||||
|
(task: any) =>
|
||||||
|
task.status === 'running' &&
|
||||||
|
(!conversationId || task.conversation_id === conversationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (runningTask) {
|
||||||
|
debugLog('[Task] 发现运行中的任务:', runningTask.task_id);
|
||||||
|
|
||||||
|
this.currentTaskId = runningTask.task_id;
|
||||||
|
this.taskStatus = runningTask.status;
|
||||||
|
this.taskCreatedAt = runningTask.created_at;
|
||||||
|
this.taskUpdatedAt = runningTask.updated_at;
|
||||||
|
this.pollingError = null;
|
||||||
|
|
||||||
|
// 获取任务详情,计算已处理的事件数量
|
||||||
|
try {
|
||||||
|
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
|
||||||
|
if (detailResponse.ok) {
|
||||||
|
const detailResult = await detailResponse.json();
|
||||||
|
if (detailResult.success && detailResult.data.events) {
|
||||||
|
// 设置为当前事件数量,只获取新事件
|
||||||
|
this.lastEventIndex =
|
||||||
|
detailResult.data.next_offset || detailResult.data.events.length;
|
||||||
|
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
|
||||||
|
} else {
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Task] 获取任务详情失败,从头开始:', error);
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return runningTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[Task] 没有运行中的任务');
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Task] 加载运行中任务失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTask() {
|
||||||
|
debugLog('[Task] 清理任务状态');
|
||||||
|
this.stopPolling();
|
||||||
|
this.currentTaskId = null;
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
this.taskStatus = 'idle';
|
||||||
|
this.pollingError = null;
|
||||||
|
this.taskCreatedAt = null;
|
||||||
|
this.taskUpdatedAt = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForNewConversation() {
|
||||||
|
debugLog('[Task] 重置任务状态(新对话)');
|
||||||
|
this.stopPolling();
|
||||||
|
this.currentTaskId = null;
|
||||||
|
this.lastEventIndex = 0;
|
||||||
|
this.taskStatus = 'idle';
|
||||||
|
this.pollingError = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export const useToolStore = defineStore('tool', {
|
|||||||
if (action.tool && action.tool.executionId) {
|
if (action.tool && action.tool.executionId) {
|
||||||
keys.add(action.tool.executionId);
|
keys.add(action.tool.executionId);
|
||||||
}
|
}
|
||||||
keys.forEach(key => {
|
keys.forEach((key) => {
|
||||||
if (key !== undefined && key !== null) {
|
if (key !== undefined && key !== null) {
|
||||||
this.toolActionIndex.set(String(key), action);
|
this.toolActionIndex.set(String(key), action);
|
||||||
}
|
}
|
||||||
@ -67,12 +67,16 @@ export const useToolStore = defineStore('tool', {
|
|||||||
keysToRemove.push(key);
|
keysToRemove.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keysToRemove.forEach(key => this.toolActionIndex.delete(key));
|
keysToRemove.forEach((key) => this.toolActionIndex.delete(key));
|
||||||
if (action.tool && action.tool.name) {
|
if (action.tool && action.tool.name) {
|
||||||
this.releaseToolAction(action.tool.name, action);
|
this.releaseToolAction(action.tool.name, action);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
findToolAction(id?: string | number | null, preparingId?: string | number | null, executionId?: string | number | null) {
|
findToolAction(
|
||||||
|
id?: string | number | null,
|
||||||
|
preparingId?: string | number | null,
|
||||||
|
executionId?: string | number | null
|
||||||
|
) {
|
||||||
const candidates = [executionId, id, preparingId];
|
const candidates = [executionId, id, preparingId];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (candidate === undefined || candidate === null) {
|
if (candidate === undefined || candidate === null) {
|
||||||
|
|||||||
@ -3,7 +3,11 @@ import { useModelStore } from './model';
|
|||||||
|
|
||||||
export type TutorialPlacement = 'auto' | 'top' | 'right' | 'bottom' | 'left' | 'center';
|
export type TutorialPlacement = 'auto' | 'top' | 'right' | 'bottom' | 'left' | 'center';
|
||||||
export type TutorialStepMode = 'info' | 'must_click';
|
export type TutorialStepMode = 'info' | 'must_click';
|
||||||
export type TutorialCondition = 'model_supports_media' | 'is_app_shell' | 'is_mobile_viewport' | 'not_mobile_viewport';
|
export type TutorialCondition =
|
||||||
|
| 'model_supports_media'
|
||||||
|
| 'is_app_shell'
|
||||||
|
| 'is_mobile_viewport'
|
||||||
|
| 'not_mobile_viewport';
|
||||||
|
|
||||||
export interface TutorialStep {
|
export interface TutorialStep {
|
||||||
id: string;
|
id: string;
|
||||||
@ -30,18 +34,119 @@ const DEFAULT_STEPS: TutorialStep[] = [
|
|||||||
mode: 'info',
|
mode: 'info',
|
||||||
placement: 'center'
|
placement: 'center'
|
||||||
},
|
},
|
||||||
{ id: 'sidebar-conversations-open', title: '展开对话记录', description: '下一步会自动点击展开对话记录。', target: '[data-tutorial="conversation-menu"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
{
|
||||||
{ id: 'sidebar-conversations-close', title: '折叠对话记录', description: '下一步会自动点击折叠按钮收起对话记录。', target: '[data-tutorial="conversation-collapse"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
id: 'sidebar-conversations-open',
|
||||||
{ id: 'sidebar-new-chat', title: '新建对话', description: '下一步会自动新建并进入一个对话。', target: '[data-tutorial="quick-new-conversation"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
title: '展开对话记录',
|
||||||
{ id: 'sidebar-workspace-toggle', title: '工作区折叠', description: '显示/隐藏左侧工作区面板。', target: '[data-tutorial="workspace-toggle"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
description: '下一步会自动点击展开对话记录。',
|
||||||
{ id: 'sidebar-monitor-toggle', title: '虚拟显示器', description: '切换到虚拟显示器模式。', target: '[data-tutorial="monitor-toggle"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
target: '[data-tutorial="conversation-menu"]',
|
||||||
{ id: 'workspace-panel-switch', title: '工作区面板切换', description: '下一步会自动展开面板切换菜单。', target: '[data-tutorial="panel-menu-toggle"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
mode: 'info',
|
||||||
{ id: 'workspace-panel-options', title: '四合一面板', description: '这里可以切换文件 / 待办 / 子智能体 / 后台指令。', target: '[data-tutorial="panel-menu"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
autoClick: true,
|
||||||
{ id: 'workspace-mode-indicator', title: '思考模式', description: '点击切换快速 / 思考 / 深度思考。', target: '[data-tutorial="run-mode-indicator"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
placement: 'right',
|
||||||
{ id: 'workspace-connection-indicator', title: '连接状态指示灯', description: '绿色表示连接正常;红色表示与后端断开连接。', target: '[data-tutorial="connection-indicator"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
condition: 'not_mobile_viewport'
|
||||||
{ id: 'header-model-selector', title: '模型与模式选择', description: '下一步会自动展开模型与运行模式弹窗。', target: '[data-tutorial="header-model-selector"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'not_mobile_viewport' },
|
},
|
||||||
{ id: 'header-model-options', title: '模型列表', description: '这里是可用模型列表。', target: '[data-tutorial="header-model-options"]', mode: 'info', placement: 'bottom', condition: 'not_mobile_viewport' },
|
{
|
||||||
{ id: 'header-runmode-options', title: '运行模式列表', description: '这里可切换快速 / 思考 / 深度思考。', target: '[data-tutorial="header-runmode-options"]', mode: 'info', placement: 'bottom', condition: 'not_mobile_viewport' },
|
id: 'sidebar-conversations-close',
|
||||||
|
title: '折叠对话记录',
|
||||||
|
description: '下一步会自动点击折叠按钮收起对话记录。',
|
||||||
|
target: '[data-tutorial="conversation-collapse"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sidebar-new-chat',
|
||||||
|
title: '新建对话',
|
||||||
|
description: '下一步会自动新建并进入一个对话。',
|
||||||
|
target: '[data-tutorial="quick-new-conversation"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sidebar-workspace-toggle',
|
||||||
|
title: '工作区折叠',
|
||||||
|
description: '显示/隐藏左侧工作区面板。',
|
||||||
|
target: '[data-tutorial="workspace-toggle"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sidebar-monitor-toggle',
|
||||||
|
title: '虚拟显示器',
|
||||||
|
description: '切换到虚拟显示器模式。',
|
||||||
|
target: '[data-tutorial="monitor-toggle"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workspace-panel-switch',
|
||||||
|
title: '工作区面板切换',
|
||||||
|
description: '下一步会自动展开面板切换菜单。',
|
||||||
|
target: '[data-tutorial="panel-menu-toggle"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workspace-panel-options',
|
||||||
|
title: '四合一面板',
|
||||||
|
description: '这里可以切换文件 / 待办 / 子智能体 / 后台指令。',
|
||||||
|
target: '[data-tutorial="panel-menu"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workspace-mode-indicator',
|
||||||
|
title: '思考模式',
|
||||||
|
description: '点击切换快速 / 思考 / 深度思考。',
|
||||||
|
target: '[data-tutorial="run-mode-indicator"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workspace-connection-indicator',
|
||||||
|
title: '连接状态指示灯',
|
||||||
|
description: '绿色表示连接正常;红色表示与后端断开连接。',
|
||||||
|
target: '[data-tutorial="connection-indicator"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'header-model-selector',
|
||||||
|
title: '模型与模式选择',
|
||||||
|
description: '下一步会自动展开模型与运行模式弹窗。',
|
||||||
|
target: '[data-tutorial="header-model-selector"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'header-model-options',
|
||||||
|
title: '模型列表',
|
||||||
|
description: '这里是可用模型列表。',
|
||||||
|
target: '[data-tutorial="header-model-options"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'header-runmode-options',
|
||||||
|
title: '运行模式列表',
|
||||||
|
description: '这里可切换快速 / 思考 / 深度思考。',
|
||||||
|
target: '[data-tutorial="header-runmode-options"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'header-menu-close',
|
id: 'header-menu-close',
|
||||||
title: '关闭模型菜单',
|
title: '关闭模型菜单',
|
||||||
@ -52,52 +157,422 @@ const DEFAULT_STEPS: TutorialStep[] = [
|
|||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
condition: 'not_mobile_viewport'
|
condition: 'not_mobile_viewport'
|
||||||
},
|
},
|
||||||
{ id: 'mobile-menu-open-conversation', title: '打开菜单', description: '下一步会自动打开左上角菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{
|
||||||
{ id: 'mobile-menu-conversation', title: '对话记录', description: '下一步会自动进入对话记录。', target: '[data-tutorial="mobile-menu-conversation"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
id: 'mobile-menu-open-conversation',
|
||||||
{ id: 'mobile-conversation-close', title: '关闭对话记录', description: '下一步会自动关闭对话记录面板。', target: '[data-tutorial="conversation-collapse"]', mode: 'info', autoClick: true, placement: 'right', condition: 'is_mobile_viewport' },
|
title: '打开菜单',
|
||||||
{ id: 'mobile-menu-open-workspace', title: '再次打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
description: '下一步会自动打开左上角菜单。',
|
||||||
{ id: 'mobile-menu-workspace', title: '工作文件', description: '下一步会自动进入工作文件。', target: '[data-tutorial="mobile-menu-workspace"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
target: '[data-tutorial="mobile-menu-trigger"]',
|
||||||
{ id: 'mobile-workspace-panel-switch', title: '工作区面板切换', description: '下一步会自动展开切换弹窗。', target: '[data-tutorial="panel-menu-toggle"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
mode: 'info',
|
||||||
{ id: 'mobile-workspace-panel-options', title: '切换弹窗选项', description: '这里可切换文件 / 待办 / 子智能体 / 后台指令。', target: '[data-tutorial="panel-menu"]', mode: 'info', placement: 'bottom', condition: 'is_mobile_viewport' },
|
autoClick: true,
|
||||||
{ id: 'mobile-workspace-close', title: '关闭工作文件', description: '下一步会自动关闭工作文件面板。', target: '[data-tutorial="mobile-workspace-close"]', mode: 'info', autoClick: true, placement: 'right', condition: 'is_mobile_viewport' },
|
placement: 'bottom',
|
||||||
{ id: 'mobile-menu-open-newchat', title: '再次打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
condition: 'is_mobile_viewport'
|
||||||
{ id: 'mobile-menu-new-chat', title: '新建对话', description: '下一步会自动新建对话。', target: '[data-tutorial="mobile-menu-new-chat"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
},
|
||||||
{ id: 'mobile-model-selector-open', title: '模型与思考模式', description: '下一步会自动展开手机端模型/思考模式选择。', target: '[data-tutorial="header-model-selector-mobile"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{
|
||||||
{ id: 'mobile-model-options', title: '模型列表', description: '这里是手机端可选模型。', target: '[data-tutorial="header-model-options"]', mode: 'info', placement: 'bottom', condition: 'is_mobile_viewport' },
|
id: 'mobile-menu-conversation',
|
||||||
{ id: 'mobile-runmode-options', title: '思考模式列表', description: '这里可切换快速 / 思考 / 深度思考。', target: '[data-tutorial="header-runmode-options"]', mode: 'info', placement: 'bottom', condition: 'is_mobile_viewport' },
|
title: '对话记录',
|
||||||
{ id: 'mobile-model-selector-close', title: '关闭选择菜单', description: '下一步会自动点击空白区域关闭菜单。', target: '.model-mode-dropdown', mode: 'info', autoOutsideClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
description: '下一步会自动进入对话记录。',
|
||||||
{ id: 'open-quick-menu', title: '打开 + 菜单', description: '将自动为你展开快捷菜单。', target: '[data-tutorial="quick-menu-open"]', mode: 'info', autoClick: true, placement: 'top' },
|
target: '[data-tutorial="mobile-menu-conversation"]',
|
||||||
{ id: 'quick-upload', title: '上传文件', description: '上传本地文件到当前工作区。', target: '[data-tutorial="quick-upload"]', mode: 'info', placement: 'left' },
|
mode: 'info',
|
||||||
{ id: 'quick-review', title: '对话回顾', description: '回顾并压缩上下文。', target: '[data-tutorial="quick-review"]', mode: 'info', placement: 'left' },
|
autoClick: true,
|
||||||
{ id: 'quick-send-image', title: '发送图片', description: '当前模型支持时,可发送图片给 AI 分析。', target: '[data-tutorial="quick-send-image"]', mode: 'info', placement: 'left', condition: 'model_supports_media' },
|
placement: 'bottom',
|
||||||
{ id: 'quick-send-video', title: '发送视频', description: '当前模型支持时,可发送视频给 AI 分析。', target: '[data-tutorial="quick-send-video"]', mode: 'info', placement: 'left', condition: 'model_supports_media' },
|
condition: 'is_mobile_viewport'
|
||||||
{ id: 'quick-tool-disable', title: '工具禁用', description: '临时禁用工具分类,控制模型行为范围。', target: '[data-tutorial="quick-tool-menu"]', mode: 'info', placement: 'left' },
|
},
|
||||||
{ id: 'quick-settings', title: '设置菜单', description: '这里可快速访问常用功能。', target: '[data-tutorial="quick-settings-menu"]', mode: 'info', placement: 'left' },
|
{
|
||||||
{ id: 'open-settings-submenu', title: '打开设置子菜单', description: '下一步会自动展开设置子菜单。', target: '[data-tutorial="quick-settings-menu"]', mode: 'info', autoClick: true, placement: 'left' },
|
id: 'mobile-conversation-close',
|
||||||
{ id: 'open-token-panel', title: '打开用量统计', description: '下一步会自动打开用量统计面板。', target: '[data-tutorial="settings-token-panel"]', mode: 'info', autoClick: true, placement: 'left' },
|
title: '关闭对话记录',
|
||||||
{ id: 'token-panel', title: 'Token 统计面板', description: '这里可以查看上下文、额度和资源统计。', target: '[data-tutorial="token-drawer"]', mode: 'info', placement: 'left' },
|
description: '下一步会自动关闭对话记录面板。',
|
||||||
{ id: 'close-token-panel', title: '关闭统计面板', description: '下一步会自动关闭统计面板。', target: '[data-tutorial="token-close"]', mode: 'info', autoClick: true, placement: 'left' },
|
target: '[data-tutorial="conversation-collapse"]',
|
||||||
{ id: 'open-personal-space', title: '打开个人空间', description: '下一步会自动打开个人空间。', target: '[data-tutorial="open-personal-space"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
mode: 'info',
|
||||||
{ id: 'mobile-menu-open-personal', title: '打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
autoClick: true,
|
||||||
{ id: 'mobile-menu-personal', title: '进入个人空间', description: '下一步会自动进入个人空间。', target: '[data-tutorial="mobile-menu-personal"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
placement: 'right',
|
||||||
{ id: 'personal-overview', title: '个人空间总览', description: '这里是个性化设置与高级配置中心。', target: '[data-tutorial="personal-card"]', mode: 'info', placement: 'right' },
|
condition: 'is_mobile_viewport'
|
||||||
{ id: 'tab-preferences', title: '个性化设置', description: '下一步自动切换到“个性化设置”。', target: '[data-tutorial="personal-tab-preferences"]', mode: 'info', autoClick: true, placement: 'right' },
|
},
|
||||||
{ id: 'page-preferences', title: '个性化设置内容', description: '可配置昵称、语气、职业和必备信息。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
{
|
||||||
{ id: 'tab-model', title: '模型偏好', description: '下一步自动切换到“模型偏好”。', target: '[data-tutorial="personal-tab-model"]', mode: 'info', autoClick: true, placement: 'right' },
|
id: 'mobile-menu-open-workspace',
|
||||||
{ id: 'page-model', title: '模型偏好内容', description: '可设置默认模型、默认运行模式和思考频率。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
title: '再次打开菜单',
|
||||||
{ id: 'tab-usage', title: '用量统计', description: '下一步自动切换到“用量统计”。', target: '[data-tutorial="personal-tab-usage"]', mode: 'info', autoClick: true, placement: 'right' },
|
description: '下一步会自动打开菜单。',
|
||||||
{ id: 'page-usage', title: '用量统计内容', description: '可查看累计 Token、对话数和工具调用数据。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
target: '[data-tutorial="mobile-menu-trigger"]',
|
||||||
{ id: 'tab-app-update', title: '软件更新', description: '移动端将自动切换到“软件更新”。', target: '[data-tutorial="personal-tab-app-update"]', mode: 'info', autoClick: true, placement: 'right', condition: 'is_app_shell' },
|
mode: 'info',
|
||||||
{ id: 'page-app-update', title: '软件更新内容', description: '这里会展示最新版本和更新说明。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right', condition: 'is_app_shell' },
|
autoClick: true,
|
||||||
{ id: 'tab-behavior', title: '模型行为', description: '下一步自动切换到“模型行为”。', target: '[data-tutorial="personal-tab-behavior"]', mode: 'info', autoClick: true, placement: 'right' },
|
placement: 'bottom',
|
||||||
{ id: 'page-behavior', title: '模型行为内容', description: '可配置工具显示、压缩策略等高级选项。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
condition: 'is_mobile_viewport'
|
||||||
{ id: 'tab-skills', title: 'Skills', description: '下一步自动切换到“Skills”。', target: '[data-tutorial="personal-tab-skills"]', mode: 'info', autoClick: true, placement: 'right' },
|
},
|
||||||
{ id: 'page-skills', title: 'Skills 内容', description: '可启用/禁用技能并同步到工作区。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
{
|
||||||
{ id: 'tab-image', title: '图片压缩', description: '下一步自动切换到“图片压缩”。', target: '[data-tutorial="personal-tab-image"]', mode: 'info', autoClick: true, placement: 'right' },
|
id: 'mobile-menu-workspace',
|
||||||
{ id: 'page-image', title: '图片压缩内容', description: '可选择原图、1080p、720p、540p 压缩策略。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
title: '工作文件',
|
||||||
{ id: 'tab-theme', title: '主题切换', description: '下一步自动切换到“主题切换”。', target: '[data-tutorial="personal-tab-theme"]', mode: 'info', autoClick: true, placement: 'right' },
|
description: '下一步会自动进入工作文件。',
|
||||||
{ id: 'page-theme', title: '主题切换内容', description: '可在经典、明亮、夜间主题间切换。', target: '[data-tutorial="personal-content-shell"]', mode: 'info', placement: 'right' },
|
target: '[data-tutorial="mobile-menu-workspace"]',
|
||||||
{ id: 'close-personal-space', title: '关闭个人空间', description: '下一步自动关闭个人空间并结束导览。', target: '[data-tutorial="personal-close"]', mode: 'info', autoClick: true, placement: 'bottom' },
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-workspace-panel-switch',
|
||||||
|
title: '工作区面板切换',
|
||||||
|
description: '下一步会自动展开切换弹窗。',
|
||||||
|
target: '[data-tutorial="panel-menu-toggle"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-workspace-panel-options',
|
||||||
|
title: '切换弹窗选项',
|
||||||
|
description: '这里可切换文件 / 待办 / 子智能体 / 后台指令。',
|
||||||
|
target: '[data-tutorial="panel-menu"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-workspace-close',
|
||||||
|
title: '关闭工作文件',
|
||||||
|
description: '下一步会自动关闭工作文件面板。',
|
||||||
|
target: '[data-tutorial="mobile-workspace-close"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-menu-open-newchat',
|
||||||
|
title: '再次打开菜单',
|
||||||
|
description: '下一步会自动打开菜单。',
|
||||||
|
target: '[data-tutorial="mobile-menu-trigger"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-menu-new-chat',
|
||||||
|
title: '新建对话',
|
||||||
|
description: '下一步会自动新建对话。',
|
||||||
|
target: '[data-tutorial="mobile-menu-new-chat"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-model-selector-open',
|
||||||
|
title: '模型与思考模式',
|
||||||
|
description: '下一步会自动展开手机端模型/思考模式选择。',
|
||||||
|
target: '[data-tutorial="header-model-selector-mobile"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-model-options',
|
||||||
|
title: '模型列表',
|
||||||
|
description: '这里是手机端可选模型。',
|
||||||
|
target: '[data-tutorial="header-model-options"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-runmode-options',
|
||||||
|
title: '思考模式列表',
|
||||||
|
description: '这里可切换快速 / 思考 / 深度思考。',
|
||||||
|
target: '[data-tutorial="header-runmode-options"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-model-selector-close',
|
||||||
|
title: '关闭选择菜单',
|
||||||
|
description: '下一步会自动点击空白区域关闭菜单。',
|
||||||
|
target: '.model-mode-dropdown',
|
||||||
|
mode: 'info',
|
||||||
|
autoOutsideClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-quick-menu',
|
||||||
|
title: '打开 + 菜单',
|
||||||
|
description: '将自动为你展开快捷菜单。',
|
||||||
|
target: '[data-tutorial="quick-menu-open"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'top'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick-upload',
|
||||||
|
title: '上传文件',
|
||||||
|
description: '上传本地文件到当前工作区。',
|
||||||
|
target: '[data-tutorial="quick-upload"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick-review',
|
||||||
|
title: '对话回顾',
|
||||||
|
description: '回顾并压缩上下文。',
|
||||||
|
target: '[data-tutorial="quick-review"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick-send-image',
|
||||||
|
title: '发送图片',
|
||||||
|
description: '当前模型支持时,可发送图片给 AI 分析。',
|
||||||
|
target: '[data-tutorial="quick-send-image"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left',
|
||||||
|
condition: 'model_supports_media'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick-send-video',
|
||||||
|
title: '发送视频',
|
||||||
|
description: '当前模型支持时,可发送视频给 AI 分析。',
|
||||||
|
target: '[data-tutorial="quick-send-video"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left',
|
||||||
|
condition: 'model_supports_media'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick-tool-disable',
|
||||||
|
title: '工具禁用',
|
||||||
|
description: '临时禁用工具分类,控制模型行为范围。',
|
||||||
|
target: '[data-tutorial="quick-tool-menu"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick-settings',
|
||||||
|
title: '设置菜单',
|
||||||
|
description: '这里可快速访问常用功能。',
|
||||||
|
target: '[data-tutorial="quick-settings-menu"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-settings-submenu',
|
||||||
|
title: '打开设置子菜单',
|
||||||
|
description: '下一步会自动展开设置子菜单。',
|
||||||
|
target: '[data-tutorial="quick-settings-menu"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-token-panel',
|
||||||
|
title: '打开用量统计',
|
||||||
|
description: '下一步会自动打开用量统计面板。',
|
||||||
|
target: '[data-tutorial="settings-token-panel"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'token-panel',
|
||||||
|
title: 'Token 统计面板',
|
||||||
|
description: '这里可以查看上下文、额度和资源统计。',
|
||||||
|
target: '[data-tutorial="token-drawer"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'close-token-panel',
|
||||||
|
title: '关闭统计面板',
|
||||||
|
description: '下一步会自动关闭统计面板。',
|
||||||
|
target: '[data-tutorial="token-close"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-personal-space',
|
||||||
|
title: '打开个人空间',
|
||||||
|
description: '下一步会自动打开个人空间。',
|
||||||
|
target: '[data-tutorial="open-personal-space"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'not_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-menu-open-personal',
|
||||||
|
title: '打开菜单',
|
||||||
|
description: '下一步会自动打开菜单。',
|
||||||
|
target: '[data-tutorial="mobile-menu-trigger"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile-menu-personal',
|
||||||
|
title: '进入个人空间',
|
||||||
|
description: '下一步会自动进入个人空间。',
|
||||||
|
target: '[data-tutorial="mobile-menu-personal"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom',
|
||||||
|
condition: 'is_mobile_viewport'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'personal-overview',
|
||||||
|
title: '个人空间总览',
|
||||||
|
description: '这里是个性化设置与高级配置中心。',
|
||||||
|
target: '[data-tutorial="personal-card"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-preferences',
|
||||||
|
title: '个性化设置',
|
||||||
|
description: '下一步自动切换到“个性化设置”。',
|
||||||
|
target: '[data-tutorial="personal-tab-preferences"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-preferences',
|
||||||
|
title: '个性化设置内容',
|
||||||
|
description: '可配置昵称、语气、职业和必备信息。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-model',
|
||||||
|
title: '模型偏好',
|
||||||
|
description: '下一步自动切换到“模型偏好”。',
|
||||||
|
target: '[data-tutorial="personal-tab-model"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-model',
|
||||||
|
title: '模型偏好内容',
|
||||||
|
description: '可设置默认模型、默认运行模式和思考频率。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-usage',
|
||||||
|
title: '用量统计',
|
||||||
|
description: '下一步自动切换到“用量统计”。',
|
||||||
|
target: '[data-tutorial="personal-tab-usage"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-usage',
|
||||||
|
title: '用量统计内容',
|
||||||
|
description: '可查看累计 Token、对话数和工具调用数据。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-app-update',
|
||||||
|
title: '软件更新',
|
||||||
|
description: '移动端将自动切换到“软件更新”。',
|
||||||
|
target: '[data-tutorial="personal-tab-app-update"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'is_app_shell'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-app-update',
|
||||||
|
title: '软件更新内容',
|
||||||
|
description: '这里会展示最新版本和更新说明。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right',
|
||||||
|
condition: 'is_app_shell'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-behavior',
|
||||||
|
title: '模型行为',
|
||||||
|
description: '下一步自动切换到“模型行为”。',
|
||||||
|
target: '[data-tutorial="personal-tab-behavior"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-behavior',
|
||||||
|
title: '模型行为内容',
|
||||||
|
description: '可配置工具显示、压缩策略等高级选项。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-skills',
|
||||||
|
title: 'Skills',
|
||||||
|
description: '下一步自动切换到“Skills”。',
|
||||||
|
target: '[data-tutorial="personal-tab-skills"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-skills',
|
||||||
|
title: 'Skills 内容',
|
||||||
|
description: '可启用/禁用技能并同步到工作区。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-image',
|
||||||
|
title: '图片压缩',
|
||||||
|
description: '下一步自动切换到“图片压缩”。',
|
||||||
|
target: '[data-tutorial="personal-tab-image"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-image',
|
||||||
|
title: '图片压缩内容',
|
||||||
|
description: '可选择原图、1080p、720p、540p 压缩策略。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-theme',
|
||||||
|
title: '主题切换',
|
||||||
|
description: '下一步自动切换到“主题切换”。',
|
||||||
|
target: '[data-tutorial="personal-tab-theme"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-theme',
|
||||||
|
title: '主题切换内容',
|
||||||
|
description: '可在经典、明亮、夜间主题间切换。',
|
||||||
|
target: '[data-tutorial="personal-content-shell"]',
|
||||||
|
mode: 'info',
|
||||||
|
placement: 'right'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'close-personal-space',
|
||||||
|
title: '关闭个人空间',
|
||||||
|
description: '下一步自动关闭个人空间并结束导览。',
|
||||||
|
target: '[data-tutorial="personal-close"]',
|
||||||
|
mode: 'info',
|
||||||
|
autoClick: true,
|
||||||
|
placement: 'bottom'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'done',
|
id: 'done',
|
||||||
title: '教程完成!',
|
title: '教程完成!',
|
||||||
@ -251,7 +726,10 @@ export const useTutorialStore = defineStore('tutorial', {
|
|||||||
skipHiddenSteps(direction: 'forward' | 'backward') {
|
skipHiddenSteps(direction: 'forward' | 'backward') {
|
||||||
if (!this.running) return;
|
if (!this.running) return;
|
||||||
if (direction === 'forward') {
|
if (direction === 'forward') {
|
||||||
while (this.currentIndex < this.steps.length && !this.shouldIncludeStep(this.steps[this.currentIndex])) {
|
while (
|
||||||
|
this.currentIndex < this.steps.length &&
|
||||||
|
!this.shouldIncludeStep(this.steps[this.currentIndex])
|
||||||
|
) {
|
||||||
this.currentIndex += 1;
|
this.currentIndex += 1;
|
||||||
}
|
}
|
||||||
if (this.currentIndex >= this.steps.length) {
|
if (this.currentIndex >= this.steps.length) {
|
||||||
|
|||||||
@ -200,7 +200,9 @@ export const useUiStore = defineStore('ui', {
|
|||||||
closable: options.closable !== false,
|
closable: options.closable !== false,
|
||||||
timeoutId: null
|
timeoutId: null
|
||||||
};
|
};
|
||||||
const duration = Object.prototype.hasOwnProperty.call(options, 'duration') ? options.duration : 4000;
|
const duration = Object.prototype.hasOwnProperty.call(options, 'duration')
|
||||||
|
? options.duration
|
||||||
|
: 4000;
|
||||||
if (duration !== null) {
|
if (duration !== null) {
|
||||||
entry.timeoutId = setTimeout(() => this.dismissToast(id), duration);
|
entry.timeoutId = setTimeout(() => this.dismissToast(id), duration);
|
||||||
}
|
}
|
||||||
@ -208,7 +210,7 @@ export const useUiStore = defineStore('ui', {
|
|||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
updateToast(id: number, patch: ToastOptions = {}) {
|
updateToast(id: number, patch: ToastOptions = {}) {
|
||||||
const entry = this.toastQueue.find(item => item.id === id);
|
const entry = this.toastQueue.find((item) => item.id === id);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -235,7 +237,7 @@ export const useUiStore = defineStore('ui', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissToast(id: number) {
|
dismissToast(id: number) {
|
||||||
const index = this.toastQueue.findIndex(item => item.id === id);
|
const index = this.toastQueue.findIndex((item) => item.id === id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,11 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
},
|
},
|
||||||
async uploadFile(
|
async uploadFile(
|
||||||
file: File,
|
file: File,
|
||||||
options: { manageUploading?: boolean; refreshFileTree?: boolean; skipPolicyCheck?: boolean } = {}
|
options: {
|
||||||
|
manageUploading?: boolean;
|
||||||
|
refreshFileTree?: boolean;
|
||||||
|
skipPolicyCheck?: boolean;
|
||||||
|
} = {}
|
||||||
): Promise<UploadResult | null> {
|
): Promise<UploadResult | null> {
|
||||||
const { manageUploading = true, refreshFileTree = true, skipPolicyCheck = false } = options;
|
const { manageUploading = true, refreshFileTree = true, skipPolicyCheck = false } = options;
|
||||||
if (!file || (manageUploading && this.uploading)) {
|
if (!file || (manageUploading && this.uploading)) {
|
||||||
|
|||||||
@ -182,11 +182,7 @@ export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean })
|
|||||||
};
|
};
|
||||||
return runningMap[readType] || '正在读取文件...';
|
return runningMap[readType] || '正在读取文件...';
|
||||||
}
|
}
|
||||||
const label =
|
const label = RUNNING_STATUS_TEXTS[tool.name] || tool.display_name || tool.name || '';
|
||||||
RUNNING_STATUS_TEXTS[tool.name] ||
|
|
||||||
tool.display_name ||
|
|
||||||
tool.name ||
|
|
||||||
'';
|
|
||||||
return label ? label : '调用工具中';
|
return label ? label : '调用工具中';
|
||||||
}
|
}
|
||||||
if (tool.status === 'completed') {
|
if (tool.status === 'completed') {
|
||||||
@ -283,9 +279,7 @@ export function formatSearchDomains(filters: ToolPayload): string {
|
|||||||
if (!Array.isArray(domains) || domains.length === 0) {
|
if (!Array.isArray(domains) || domains.length === 0) {
|
||||||
return '未限定网站';
|
return '未限定网站';
|
||||||
}
|
}
|
||||||
const normalized = domains
|
const normalized = domains.map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
.map(item => String(item || '').trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
return normalized.length ? normalized.join(', ') : '未限定网站';
|
return normalized.length ? normalized.join(', ') : '未限定网站';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,7 +109,7 @@ export function buildQuotaResetSummary(quota: UsageQuotaMap): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
['fast', 'thinking', 'search'].forEach(type => {
|
['fast', 'thinking', 'search'].forEach((type) => {
|
||||||
const entry = quota?.[type];
|
const entry = quota?.[type];
|
||||||
if (entry && entry.reset_at && normalizeNumber(entry.count) > 0) {
|
if (entry && entry.reset_at && normalizeNumber(entry.count) > 0) {
|
||||||
parts.push(`${quotaTypeLabel(type)} ${formatResetTime(entry.reset_at)}`);
|
parts.push(`${quotaTypeLabel(type)} ${formatResetTime(entry.reset_at)}`);
|
||||||
@ -133,7 +133,11 @@ export function isQuotaExceeded(quota: UsageQuotaMap, type: string): boolean {
|
|||||||
return normalizeNumber(entry.count) >= limit;
|
return normalizeNumber(entry.count) >= limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildQuotaToastMessage(type: string, quota: UsageQuotaMap, resetAt?: NumericLike): string {
|
export function buildQuotaToastMessage(
|
||||||
|
type: string,
|
||||||
|
quota: UsageQuotaMap,
|
||||||
|
resetAt?: NumericLike
|
||||||
|
): string {
|
||||||
const entry = quota?.[type];
|
const entry = quota?.[type];
|
||||||
const referenceResetAt = typeof resetAt !== 'undefined' ? resetAt : entry?.reset_at;
|
const referenceResetAt = typeof resetAt !== 'undefined' ? resetAt : entry?.reset_at;
|
||||||
return `${quotaTypeLabel(type)} 配额已用完,将在 ${formatResetTime(referenceResetAt)} 重置`;
|
return `${quotaTypeLabel(type)} 配额已用完,将在 ${formatResetTime(referenceResetAt)} 重置`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user