refactor(frontend): 整理应用逻辑层与状态管理

This commit is contained in:
JOJO 2026-04-10 14:34:01 +08:00
parent 1bda147a6f
commit 8be1c37b6a
37 changed files with 9267 additions and 8566 deletions

View File

@ -196,7 +196,7 @@ const appOptions = {
personalizationOpenDrawer: 'openDrawer' personalizationOpenDrawer: 'openDrawer'
}) })
} }
}; };
(appOptions as any).components = appComponents; (appOptions as any).components = appComponents;

View File

@ -17,10 +17,6 @@ function normalizeShowImageSrc(src: string) {
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, '&')
@ -34,20 +30,20 @@ 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; // 可能为 nullinsertBefore 会当 append const ref = node.nextSibling; // 可能为 nullinsertBefore 会当 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') || '';
@ -112,7 +108,9 @@ function updateViewportHeightVar() {
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 -
visualViewport.offsetTop
); );
docEl.style.setProperty('--app-viewport', `${vh}px`); docEl.style.setProperty('--app-viewport', `${vh}px`);
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`); docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);

View File

@ -1,17 +1,31 @@
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,

View File

@ -124,7 +124,7 @@ export const computed = {
return options.map((opt) => ({ return options.map((opt) => ({
...opt, ...opt,
disabled: disabledSet.has(opt.key) disabled: disabledSet.has(opt.key)
})).filter((opt) => true); }));
}, },
titleRibbonVisible() { titleRibbonVisible() {
return !this.isMobileViewport && this.chatDisplayMode === 'chat'; return !this.isMobileViewport && this.chatDisplayMode === 'chat';
@ -162,7 +162,14 @@ export const computed = {
}, },
composerBusy() { composerBusy() {
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor'; const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested || this.compressionInProgress || this.compressing; return (
this.streamingUi ||
this.taskInProgress ||
monitorLock ||
this.stopRequested ||
this.compressionInProgress ||
this.compressing
);
}, },
composerHeroActive() { composerHeroActive() {
return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive; return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive;

View File

@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { debugLog, traceLog } from './common'; import { debugLog, traceLog } from './common';
import { usePersonalizationStore } from '../../stores/personalization';
export const conversationMethods = { export const conversationMethods = {
// 完整重置所有状态 // 完整重置所有状态
@ -33,7 +34,9 @@ export const conversationMethods = {
this.toolResetTracking(); this.toolResetTracking();
// 新增将所有未完成的工具标记为已完成并清理awaitingFirstContent状态 // 新增将所有未完成的工具标记为已完成并清理awaitingFirstContent状态
const assistantMsgsBefore = this.messages.filter(m => m.role === 'assistant').map(m => ({ const assistantMsgsBefore = this.messages
.filter((m) => m.role === 'assistant')
.map((m) => ({
awaitingFirstContent: m.awaitingFirstContent, awaitingFirstContent: m.awaitingFirstContent,
generatingLabel: m.generatingLabel generatingLabel: m.generatingLabel
})); }));
@ -50,9 +53,11 @@ export const conversationMethods = {
// 清理工具状态 // 清理工具状态
if (msg.actions) { if (msg.actions) {
msg.actions.forEach(action => { msg.actions.forEach((action) => {
if (action.type === 'tool' && if (
(action.tool.status === 'preparing' || action.tool.status === 'running')) { action.type === 'tool' &&
(action.tool.status === 'preparing' || action.tool.status === 'running')
) {
action.tool.status = 'completed'; action.tool.status = 'completed';
} }
}); });
@ -60,7 +65,9 @@ export const conversationMethods = {
} }
}); });
const assistantMsgsAfter = this.messages.filter(m => m.role === 'assistant').map(m => ({ const assistantMsgsAfter = this.messages
.filter((m) => m.role === 'assistant')
.map((m) => ({
awaitingFirstContent: m.awaitingFirstContent, awaitingFirstContent: m.awaitingFirstContent,
generatingLabel: m.generatingLabel generatingLabel: m.generatingLabel
})); }));
@ -106,7 +113,10 @@ export const conversationMethods = {
this.logMessageState('resetAllStates:after-cleanup', { reason }); this.logMessageState('resetAllStates:after-cleanup', { reason });
}, },
scheduleResetAfterTask(reason = 'unspecified', options: { preserveMonitorWindows?: boolean } = {}) { scheduleResetAfterTask(
reason = 'unspecified',
options: { preserveMonitorWindows?: boolean } = {}
) {
const start = Date.now(); const start = Date.now();
const maxWait = 4000; const maxWait = 4000;
const interval = 200; const interval = 200;
@ -131,7 +141,8 @@ export const conversationMethods = {
async loadConversationsList() { async loadConversationsList() {
const queryOffset = this.conversationsOffset; const queryOffset = this.conversationsOffset;
const queryLimit = this.conversationsLimit; const queryLimit = this.conversationsLimit;
const refreshToken = queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken; const refreshToken =
queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken;
const requestSeq = ++this.conversationListRequestSeq; const requestSeq = ++this.conversationListRequestSeq;
this.conversationsLoading = true; this.conversationsLoading = true;
try { try {
@ -197,7 +208,11 @@ export const conversationMethods = {
async loadConversation(conversationId, options = {}) { async loadConversation(conversationId, options = {}) {
const force = Boolean(options.force); const force = Boolean(options.force);
debugLog('加载对话:', conversationId); debugLog('加载对话:', conversationId);
traceLog('loadConversation:start', { conversationId, currentConversationId: this.currentConversationId, force }); traceLog('loadConversation:start', {
conversationId,
currentConversationId: this.currentConversationId,
force
});
this.logMessageState('loadConversation:start', { conversationId, force }); this.logMessageState('loadConversation:start', { conversationId, force });
this.suppressTitleTyping = true; this.suppressTitleTyping = true;
this.titleReady = false; this.titleReady = false;
@ -226,7 +241,9 @@ export const conversationMethods = {
} }
try { try {
if (this.currentConversationId) { if (this.currentConversationId) {
await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, { method: 'POST' }); await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, {
method: 'POST'
});
} }
} catch (e) { } catch (e) {
console.warn('取消压缩失败:', e); console.warn('取消压缩失败:', e);
@ -243,7 +260,6 @@ export const conversationMethods = {
// 应用个性化设置中的默认模型和思考模式 // 应用个性化设置中的默认模型和思考模式
try { try {
const { usePersonalizationStore } = await import('../../stores/personalization');
const personalizationStore = usePersonalizationStore(); const personalizationStore = usePersonalizationStore();
if (personalizationStore.loaded) { if (personalizationStore.loaded) {
@ -275,7 +291,8 @@ export const conversationMethods = {
try { try {
const { useTaskStore } = await import('../../stores/task'); const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore(); const taskStore = useTaskStore();
const hasActiveTask = taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent; const hasActiveTask =
taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent;
if (hasActiveTask) { if (hasActiveTask) {
const waitingLabel = this.waitingForBackgroundCommand ? '后台指令' : '后台子智能体'; const waitingLabel = this.waitingForBackgroundCommand ? '后台指令' : '后台子智能体';
const confirmed = await this.confirmAction({ const confirmed = await this.confirmAction({
@ -332,7 +349,11 @@ export const conversationMethods = {
this.suppressTitleTyping = false; this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false }); this.startTitleTyping(this.currentConversationTitle, { animate: false });
this.promoteConversationToTop(conversationId); this.promoteConversationToTop(conversationId);
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`); history.pushState(
{ conversationId },
'',
`/${this.stripConversationPrefix(conversationId)}`
);
this.skipConversationLoadedEvent = true; this.skipConversationLoadedEvent = true;
// 3. 重置UI状态 // 3. 重置UI状态
@ -348,7 +369,6 @@ export const conversationMethods = {
conversationId, conversationId,
messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a' messagesLen: Array.isArray(this.messages) ? this.messages.length : 'n/a'
}); });
} else { } else {
console.error('对话加载失败:', result.message); console.error('对话加载失败:', result.message);
this.suppressTitleTyping = false; this.suppressTitleTyping = false;
@ -361,7 +381,10 @@ export const conversationMethods = {
} }
} catch (error) { } catch (error) {
console.error('加载对话异常:', error); console.error('加载对话异常:', error);
traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) }); traceLog('loadConversation:error', {
conversationId,
error: error?.message || String(error)
});
this.suppressTitleTyping = false; this.suppressTitleTyping = false;
this.titleReady = true; this.titleReady = true;
this.uiPushToast({ this.uiPushToast({
@ -376,7 +399,7 @@ export const conversationMethods = {
if (!Array.isArray(this.conversations) || !conversationId) { if (!Array.isArray(this.conversations) || !conversationId) {
return; return;
} }
const index = this.conversations.findIndex(conv => conv && conv.id === conversationId); const index = this.conversations.findIndex((conv) => conv && conv.id === conversationId);
if (index > 0) { if (index > 0) {
const [selected] = this.conversations.splice(index, 1); const [selected] = this.conversations.splice(index, 1);
this.conversations.unshift(selected); this.conversations.unshift(selected);
@ -397,7 +420,9 @@ export const conversationMethods = {
} }
try { try {
if (this.currentConversationId) { if (this.currentConversationId) {
await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, { method: 'POST' }); await fetch(`/api/conversations/${this.currentConversationId}/compression_cancel`, {
method: 'POST'
});
} }
} catch (e) { } catch (e) {
console.warn('取消压缩失败:', e); console.warn('取消压缩失败:', e);
@ -421,7 +446,6 @@ export const conversationMethods = {
// 应用个性化设置中的默认模型和思考模式 // 应用个性化设置中的默认模型和思考模式
try { try {
const { usePersonalizationStore } = await import('../../stores/personalization');
const personalizationStore = usePersonalizationStore(); const personalizationStore = usePersonalizationStore();
if (personalizationStore.loaded) { if (personalizationStore.loaded) {
@ -528,7 +552,7 @@ export const conversationMethods = {
}; };
this.conversations = [ this.conversations = [
placeholder, placeholder,
...this.conversations.filter(conv => conv && conv.id !== newConversationId) ...this.conversations.filter((conv) => conv && conv.id !== newConversationId)
]; ];
// 直接加载新对话,确保状态一致 // 直接加载新对话,确保状态一致
@ -613,7 +637,6 @@ export const conversationMethods = {
// 刷新对话列表 // 刷新对话列表
this.conversationsOffset = 0; this.conversationsOffset = 0;
await this.loadConversationsList(); await this.loadConversationsList();
} else { } else {
console.error('删除对话失败:', result.message); console.error('删除对话失败:', result.message);
this.uiPushToast({ this.uiPushToast({

View File

@ -61,7 +61,9 @@ export const historyMethods = {
this.historyLoadingFor = targetConversationId; this.historyLoadingFor = targetConversationId;
try { try {
debugLog('开始获取历史对话内容...'); debugLog('开始获取历史对话内容...');
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId }); this.logMessageState('fetchAndDisplayHistory:start', {
conversationId: this.currentConversationId
});
try { try {
// 使用专门的API获取对话消息历史 // 使用专门的API获取对话消息历史
@ -86,7 +88,10 @@ export const historyMethods = {
} }
// 如果在等待期间用户已切换到其他对话,则丢弃结果 // 如果在等待期间用户已切换到其他对话,则丢弃结果
if (loadSeq !== this.historyLoadSeq || this.currentConversationId !== targetConversationId) { if (
loadSeq !== this.historyLoadSeq ||
this.currentConversationId !== targetConversationId
) {
debugLog('检测到对话已切换,丢弃过期的历史加载结果'); debugLog('检测到对话已切换,丢弃过期的历史加载结果');
return; return;
} }
@ -125,12 +130,13 @@ export const historyMethods = {
this.messages = []; this.messages = [];
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared'); this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
} }
} catch (error) { } catch (error) {
console.error('获取历史对话失败:', error); console.error('获取历史对话失败:', error);
debugLog('尝试不显示错误弹窗,仅在控制台记录'); debugLog('尝试不显示错误弹窗,仅在控制台记录');
// 不显示alert避免打断用户体验 // 不显示alert避免打断用户体验
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) }); this.logMessageState('fetchAndDisplayHistory:error-clear', {
error: error?.message || String(error)
});
this.messages = []; this.messages = [];
this.logMessageState('fetchAndDisplayHistory:error-cleared'); this.logMessageState('fetchAndDisplayHistory:error-cleared');
} }
@ -191,7 +197,6 @@ export const historyMethods = {
metadata: message.metadata || {} metadata: message.metadata || {}
}); });
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...'); debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
} else if (message.role === 'assistant') { } else if (message.role === 'assistant') {
// AI消息 - 如果没有当前assistant消息创建一个 // AI消息 - 如果没有当前assistant消息创建一个
if (!currentAssistantMessage) { if (!currentAssistantMessage) {
@ -242,9 +247,10 @@ export const historyMethods = {
message.tool_calls.forEach((toolCall, tcIndex) => { message.tool_calls.forEach((toolCall, tcIndex) => {
let arguments_obj = {}; let arguments_obj = {};
try { try {
arguments_obj = typeof toolCall.function.arguments === 'string' arguments_obj =
typeof toolCall.function.arguments === 'string'
? JSON.parse(toolCall.function.arguments || '{}') ? JSON.parse(toolCall.function.arguments || '{}')
: (toolCall.function.arguments || {}); : toolCall.function.arguments || {};
} catch (e) { } catch (e) {
console.warn('解析工具参数失败:', e); console.warn('解析工具参数失败:', e);
arguments_obj = {}; arguments_obj = {};
@ -276,7 +282,6 @@ export const historyMethods = {
debugLog('添加工具调用:', toolCall.function.name); debugLog('添加工具调用:', toolCall.function.name);
}); });
} }
} else if (message.role === 'tool') { } else if (message.role === 'tool') {
// 工具结果 - 更新当前assistant消息中对应的工具 // 工具结果 - 更新当前assistant消息中对应的工具
if (currentAssistantMessage) { if (currentAssistantMessage) {
@ -285,17 +290,15 @@ export const historyMethods = {
// 优先按tool_call_id匹配 // 优先按tool_call_id匹配
if (message.tool_call_id) { if (message.tool_call_id) {
toolAction = currentAssistantMessage.actions.find(action => toolAction = currentAssistantMessage.actions.find(
action.type === 'tool' && (action) => action.type === 'tool' && action.tool.id === message.tool_call_id
action.tool.id === message.tool_call_id
); );
} }
// 如果找不到按name匹配最后一个同名工具 // 如果找不到按name匹配最后一个同名工具
if (!toolAction && message.name) { if (!toolAction && message.name) {
const sameNameTools = currentAssistantMessage.actions.filter(action => const sameNameTools = currentAssistantMessage.actions.filter(
action.type === 'tool' && (action) => action.type === 'tool' && action.tool.name === message.name
action.tool.name === message.name
); );
toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个 toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个
} }
@ -330,7 +333,6 @@ export const historyMethods = {
console.warn('找不到对应的工具调用:', message.name, message.tool_call_id); console.warn('找不到对应的工具调用:', message.name, message.tool_call_id);
} }
} }
} else { } else {
// 其他类型消息如system- 先结束当前assistant消息 // 其他类型消息如system- 先结束当前assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) { if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
@ -400,7 +402,8 @@ export const historyMethods = {
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToBottom(); this.scrollToBottom();
setTimeout(() => { setTimeout(() => {
const blockCount = this.$el && this.$el.querySelectorAll const blockCount =
this.$el && this.$el.querySelectorAll
? this.$el.querySelectorAll('.message-block').length ? this.$el.querySelectorAll('.message-block').length
: 'N/A'; : 'N/A';
debugLog('[Messages] DOM 渲染统计', { debugLog('[Messages] DOM 渲染统计', {

View File

@ -351,8 +351,9 @@ export const messageMethods = {
} }
// 等待后端确认 // 等待后端确认
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
const shouldKeepBusy = Boolean(taskStore.currentTaskId) && const shouldKeepBusy =
Boolean(taskStore.currentTaskId) &&
['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus)); ['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus));
// 清理前端状态 // 清理前端状态
@ -390,7 +391,8 @@ export const messageMethods = {
console.error('[Message] 取消任务失败:', error); console.error('[Message] 取消任务失败:', error);
const { useTaskStore } = await import('../../stores/task'); const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore(); const taskStore = useTaskStore();
const shouldKeepBusy = Boolean(taskStore.currentTaskId) && const shouldKeepBusy =
Boolean(taskStore.currentTaskId) &&
['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus)); ['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus));
// 即使失败也清理状态 // 即使失败也清理状态

View File

@ -28,10 +28,10 @@ export const resourceMethods = {
if (this.containerStatus.mode !== 'docker') { if (this.containerStatus.mode !== 'docker') {
return 'status-pill--host'; return 'status-pill--host';
} }
const rawStatus = ( const rawStatus =
this.containerStatus.state && (this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status) (this.containerStatus.state.status || this.containerStatus.state.Status)) ||
) || ''; '';
const status = String(rawStatus).toLowerCase(); const status = String(rawStatus).toLowerCase();
if (status.includes('running')) { if (status.includes('running')) {
return 'status-pill--running'; return 'status-pill--running';
@ -52,10 +52,10 @@ export const resourceMethods = {
if (this.containerStatus.mode !== 'docker') { if (this.containerStatus.mode !== 'docker') {
return '宿主机模式'; return '宿主机模式';
} }
const rawStatus = ( const rawStatus =
this.containerStatus.state && (this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status) (this.containerStatus.state.status || this.containerStatus.state.Status)) ||
) || ''; '';
const status = String(rawStatus).toLowerCase(); const status = String(rawStatus).toLowerCase();
if (status.includes('running')) { if (status.includes('running')) {
return '运行中'; return '运行中';

View File

@ -66,7 +66,9 @@ export const searchMethods = {
return; return;
} }
try { try {
const response = await fetch(`/api/conversations?limit=${batchSize}&offset=${this.searchOffset}`); const response = await fetch(
`/api/conversations?limit=${batchSize}&offset=${this.searchOffset}`
);
const payload = await response.json(); const payload = await response.json();
if (requestSeq !== this.searchRequestSeq) { if (requestSeq !== this.searchRequestSeq) {
@ -126,11 +128,16 @@ export const searchMethods = {
if (!conversationId) { if (!conversationId) {
return ''; return '';
} }
if (this.searchPreviewCache && Object.prototype.hasOwnProperty.call(this.searchPreviewCache, conversationId)) { if (
this.searchPreviewCache &&
Object.prototype.hasOwnProperty.call(this.searchPreviewCache, conversationId)
) {
return this.searchPreviewCache[conversationId]; return this.searchPreviewCache[conversationId];
} }
try { try {
const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${SEARCH_PREVIEW_LIMIT}`); const resp = await fetch(
`/api/conversations/${conversationId}/review_preview?limit=${SEARCH_PREVIEW_LIMIT}`
);
const payload = await resp.json(); const payload = await resp.json();
if (requestSeq !== this.searchRequestSeq) { if (requestSeq !== this.searchRequestSeq) {
return ''; return '';

View File

@ -1,7 +1,9 @@
// @ts-nocheck // @ts-nocheck
import { debugLog } from './common'; import { debugLog } from './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);
}; };
@ -58,7 +60,8 @@ export const taskPollingMethods = {
status: t?.status status: t?.status
})) }))
}); });
const runningTask = tasks.find((task: any) => const runningTask = tasks.find(
(task: any) =>
task?.status === 'running' && task?.conversation_id === this.currentConversationId task?.status === 'running' && task?.conversation_id === this.currentConversationId
); );
if (!runningTask?.task_id) { if (!runningTask?.task_id) {
@ -171,7 +174,11 @@ export const taskPollingMethods = {
'conversation_resolved', 'conversation_resolved',
'compression_finished' 'compression_finished'
]); ]);
if (!crossConversationAllowed.has(eventType) && eventData.conversation_id && this.currentConversationId) { if (
!crossConversationAllowed.has(eventType) &&
eventData.conversation_id &&
this.currentConversationId
) {
if (eventData.conversation_id !== this.currentConversationId) { if (eventData.conversation_id !== this.currentConversationId) {
console.log(`[DEBUG_AWAITING] 忽略不匹配的事件`, { console.log(`[DEBUG_AWAITING] 忽略不匹配的事件`, {
eventType, eventType,
@ -179,7 +186,9 @@ export const taskPollingMethods = {
eventConversationId: eventData.conversation_id, eventConversationId: eventData.conversation_id,
currentConversationId: this.currentConversationId currentConversationId: this.currentConversationId
}); });
debugLog(`[TaskPolling] 忽略不匹配的事件 #${eventIdx}: ${eventType}, 事件对话=${eventData.conversation_id}, 当前对话=${this.currentConversationId}`); debugLog(
`[TaskPolling] 忽略不匹配的事件 #${eventIdx}: ${eventType}, 事件对话=${eventData.conversation_id}, 当前对话=${this.currentConversationId}`
);
return; return;
} }
} }
@ -338,7 +347,8 @@ export const taskPollingMethods = {
// 1. 有assistant消息 // 1. 有assistant消息
// 2. 且该消息正在streaming或者没有任何内容说明是刚创建的 // 2. 且该消息正在streaming或者没有任何内容说明是刚创建的
// 3. 且不是历史加载完成的消息历史消息的awaitingFirstContent应该是false // 3. 且不是历史加载完成的消息历史消息的awaitingFirstContent应该是false
const isRefreshRestore = hasAssistantMessage && const isRefreshRestore =
hasAssistantMessage &&
(this.streamingMessage || !lastMessage.actions || lastMessage.actions.length === 0) && (this.streamingMessage || !lastMessage.actions || lastMessage.actions.length === 0) &&
(lastMessage.awaitingFirstContent === true || this.taskInProgress); (lastMessage.awaitingFirstContent === true || this.taskInProgress);
@ -468,7 +478,7 @@ export const taskPollingMethods = {
} }
}, },
handleThinkingChunk(data: any, eventIdx: number) { handleThinkingChunk(data: any) {
if (this.runMode === 'fast' || this.thinkingMode === false) { if (this.runMode === 'fast' || this.thinkingMode === false) {
return; return;
} }
@ -513,7 +523,7 @@ export const taskPollingMethods = {
this.monitorEndModelOutput(); this.monitorEndModelOutput();
}, },
handleTextStart(data: any) { handleTextStart() {
console.log('[TextStart] 开始处理'); console.log('[TextStart] 开始处理');
debugLog('[TaskPolling] 文本开始'); debugLog('[TaskPolling] 文本开始');
@ -1061,7 +1071,7 @@ export const taskPollingMethods = {
// 更新对话列表中的标题 // 更新对话列表中的标题
if (data.title && Array.isArray(this.conversations)) { if (data.title && Array.isArray(this.conversations)) {
const conv = this.conversations.find(c => c && c.id === data.conversation_id); const conv = this.conversations.find((c) => c && c.id === data.conversation_id);
if (conv) { if (conv) {
conv.title = data.title; conv.title = data.title;
debugLog('[TaskPolling] 更新对话列表中的标题:', data.title); debugLog('[TaskPolling] 更新对话列表中的标题:', data.title);
@ -1255,7 +1265,8 @@ export const taskPollingMethods = {
const eventCount = allEvents.length; const eventCount = allEvents.length;
const historyIncomplete = eventCount > historyActionsCount + 5; // 允许5个事件的误差 const historyIncomplete = eventCount > historyActionsCount + 5; // 允许5个事件的误差
const needsRebuild = !isAssistantMessage || const needsRebuild =
!isAssistantMessage ||
(isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) || (isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) ||
historyIncomplete; historyIncomplete;
@ -1349,7 +1360,7 @@ export const taskPollingMethods = {
// streaming: a.streaming // streaming: a.streaming
// }))); // })));
const thinkingActions = lastMessage.actions.filter(a => a.type === 'thinking'); const thinkingActions = lastMessage.actions.filter((a) => a.type === 'thinking');
// console.log('[TaskPolling] 思考块数量:', thinkingActions.length); // console.log('[TaskPolling] 思考块数量:', thinkingActions.length);
if (inThinking && thinkingActions.length > 0) { if (inThinking && thinkingActions.length > 0) {
@ -1383,7 +1394,7 @@ export const taskPollingMethods = {
// }); // });
// 恢复文本块状态 // 恢复文本块状态
const textActions = lastMessage.actions.filter(a => a.type === 'text'); const textActions = lastMessage.actions.filter((a) => a.type === 'text');
// console.log('[TaskPolling] 文本块数量:', textActions.length); // console.log('[TaskPolling] 文本块数量:', textActions.length);
if (inText && textActions.length > 0) { if (inText && textActions.length > 0) {
@ -1394,7 +1405,7 @@ export const taskPollingMethods = {
// 注册历史中的工具块到 toolActionIndex // 注册历史中的工具块到 toolActionIndex
// 这样后续的 update_action 事件可以找到对应的块进行状态更新 // 这样后续的 update_action 事件可以找到对应的块进行状态更新
const toolActions = lastMessage.actions.filter(a => a.type === 'tool'); const toolActions = lastMessage.actions.filter((a) => a.type === 'tool');
// console.log('[TaskPolling] 工具块数量:', toolActions.length); // console.log('[TaskPolling] 工具块数量:', toolActions.length);
for (const toolAction of toolActions) { for (const toolAction of toolActions) {
@ -1506,15 +1517,23 @@ export const taskPollingMethods = {
return true; return true;
}); });
const running = relevant.filter((task: any) => task?.run_in_background && !terminalStatuses.has(task?.status)); const running = relevant.filter(
(task: any) => task?.run_in_background && !terminalStatuses.has(task?.status)
);
const pendingNotice = relevant.filter((task: any) => task?.notice_pending); const pendingNotice = relevant.filter((task: any) => task?.notice_pending);
if (running.length || pendingNotice.length) { if (running.length || pendingNotice.length) {
debugLog('[TaskPolling] 恢复子智能体等待状态', { debugLog('[TaskPolling] 恢复子智能体等待状态', {
running: running.length, running: running.length,
pendingNotice: pendingNotice.length, pendingNotice: pendingNotice.length,
runningTasks: running.map((task: any) => ({ task_id: task?.task_id, status: task?.status })), runningTasks: running.map((task: any) => ({
pendingTasks: pendingNotice.map((task: any) => ({ task_id: task?.task_id, status: task?.status })) task_id: task?.task_id,
status: task?.status
})),
pendingTasks: pendingNotice.map((task: any) => ({
task_id: task?.task_id,
status: task?.status
}))
}); });
this.waitingForSubAgent = true; this.waitingForSubAgent = true;
this.waitingForBackgroundCommand = false; this.waitingForBackgroundCommand = false;
@ -1527,5 +1546,5 @@ export const taskPollingMethods = {
} catch (error) { } catch (error) {
console.error('[TaskPolling] 恢复子智能体等待状态失败:', error); console.error('[TaskPolling] 恢复子智能体等待状态失败:', error);
} }
}, }
}; };

View File

@ -36,11 +36,11 @@ export const toolingMethods = {
}, },
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;
} }
@ -56,18 +56,16 @@ export const toolingMethods = {
}, },
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 = const status =
typeof action.tool.status === 'string' typeof action.tool.status === 'string' ? action.tool.status.toLowerCase() : '';
? action.tool.status.toLowerCase()
: '';
if (!status || ['preparing', 'running', 'pending', 'queued', 'stale'].includes(status)) { if (!status || ['preparing', 'running', 'pending', 'queued', 'stale'].includes(status)) {
action.tool.status = 'cancelled'; action.tool.status = 'cancelled';
action.tool.message = action.tool.message || '已停止'; action.tool.message = action.tool.message || '已停止';
@ -93,18 +91,18 @@ export const toolingMethods = {
}, },
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)) { if (!Array.isArray(this.messages)) {
return false; return false;
} }
return this.messages.some(msg => { return this.messages.some((msg) => {
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) { if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) {
return false; return false;
} }
return msg.actions.some(action => { return msg.actions.some((action) => {
if (!action || action.type !== 'tool' || !action.tool) { if (!action || action.type !== 'tool' || !action.tool) {
return false; return false;
} }
@ -112,9 +110,7 @@ export const toolingMethods = {
return true; return true;
} }
const status = const status =
typeof action.tool.status === 'string' typeof action.tool.status === 'string' ? action.tool.status.toLowerCase() : '';
? action.tool.status.toLowerCase()
: '';
return !status || ['preparing', 'running', 'pending', 'queued'].includes(status); return !status || ['preparing', 'running', 'pending', 'queued'].includes(status);
}); });
}); });
@ -149,7 +145,7 @@ export const toolingMethods = {
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);
@ -269,9 +265,7 @@ export const toolingMethods = {
getToolStatusText(tool: any) { getToolStatusText(tool: any) {
const personalization = usePersonalizationStore(); const personalization = usePersonalizationStore();
const intentEnabled = const intentEnabled =
personalization?.form?.tool_intent_enabled ?? personalization?.form?.tool_intent_enabled ?? personalization?.tool_intent_enabled ?? true;
personalization?.tool_intent_enabled ??
true;
return baseGetToolStatusText(tool, { intentEnabled }); return baseGetToolStatusText(tool, { intentEnabled });
}, },
getToolDescription, getToolDescription,

View File

@ -214,7 +214,10 @@ export const uiMethods = {
this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用'); this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用');
return; return;
} }
if (this.chatDisplayMode === 'chat' && this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')) { if (
this.chatDisplayMode === 'chat' &&
this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')
) {
return; return;
} }
const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat'; const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
@ -286,7 +289,11 @@ export const uiMethods = {
try { try {
const resp = await fetch('/api/tutorial-status', { credentials: 'same-origin' }); const resp = await fetch('/api/tutorial-status', { credentials: 'same-origin' });
const data = await resp.json().catch(() => ({})); const data = await resp.json().catch(() => ({}));
console.info('[tutorial-prompt] status response:', { ok: resp.ok, status: resp.status, data }); console.info('[tutorial-prompt] status response:', {
ok: resp.ok,
status: resp.status,
data
});
if (!resp.ok || !data?.success) { if (!resp.ok || !data?.success) {
return; return;
} }
@ -485,7 +492,7 @@ export const uiMethods = {
modelStore.setModel(data.model_key || key); modelStore.setModel(data.model_key || key);
if (data.run_mode) { if (data.run_mode) {
this.runMode = data.run_mode; this.runMode = data.run_mode;
this.thinkingMode = data.thinking_mode ?? (data.run_mode !== 'fast'); this.thinkingMode = data.thinking_mode ?? data.run_mode !== 'fast';
} else { } else {
// 前端兼容策略:根据模型特性自动调整运行模式 // 前端兼容策略:根据模型特性自动调整运行模式
const currentModel = modelStore.currentModel; const currentModel = modelStore.currentModel;
@ -583,7 +590,8 @@ export const uiMethods = {
throw new Error(payload.message || payload.error || '切换失败'); throw new Error(payload.message || payload.error || '切换失败');
} }
const data = payload.data || {}; const data = payload.data || {};
this.thinkingMode = typeof data.thinking_mode === 'boolean' ? data.thinking_mode : mode !== 'fast'; this.thinkingMode =
typeof data.thinking_mode === 'boolean' ? data.thinking_mode : mode !== 'fast';
this.runMode = data.mode || mode; this.runMode = data.mode || mode;
} catch (error) { } catch (error) {
console.error('切换运行模式失败:', error); console.error('切换运行模式失败:', error);
@ -739,16 +747,16 @@ export const uiMethods = {
this.reviewSubmitting = true; this.reviewSubmitting = true;
try { try {
const { path, char_count } = await this.generateConversationReview(this.reviewSelectedConversationId); const { path, char_count } = await this.generateConversationReview(
this.reviewSelectedConversationId
);
if (!path) { if (!path) {
throw new Error('未获取到生成的文件路径'); throw new Error('未获取到生成的文件路径');
} }
const count = typeof char_count === 'number' ? char_count : 0; const count = typeof char_count === 'number' ? char_count : 0;
this.reviewGeneratedPath = path; this.reviewGeneratedPath = path;
const suggestion = const suggestion =
count && count <= 10000 count && count <= 10000 ? '建议直接完整阅读。' : '建议使用 read 工具进行搜索或分段阅读。';
? '建议直接完整阅读。'
: '建议使用 read 工具进行搜索或分段阅读。';
if (this.reviewSendToModel) { if (this.reviewSendToModel) {
const message = `帮我继续这个任务,对话文件在 ${path},文件长 ${count || '未知'} 字符,${suggestion} 请阅读文件了解后,不要直接继续工作,而是向我汇报你的理解,然后等我做出指示。`; const message = `帮我继续这个任务,对话文件在 ${path},文件长 ${count || '未知'} 字符,${suggestion} 请阅读文件了解后,不要直接继续工作,而是向我汇报你的理解,然后等我做出指示。`;
const sent = await this.sendAutoUserMessage(message); const sent = await this.sendAutoUserMessage(message);
@ -779,7 +787,9 @@ export const uiMethods = {
this.reviewPreviewError = null; this.reviewPreviewError = null;
this.reviewPreviewLines = []; this.reviewPreviewLines = [];
try { try {
const resp = await fetch(`/api/conversations/${conversationId}/review_preview?limit=${this.reviewPreviewLimit}`); const resp = await fetch(
`/api/conversations/${conversationId}/review_preview?limit=${this.reviewPreviewLimit}`
);
const payload = await resp.json().catch(() => ({})); const payload = await resp.json().catch(() => ({}));
if (!resp.ok || !payload?.success) { if (!resp.ok || !payload?.success) {
const msg = payload?.message || payload?.error || '获取预览失败'; const msg = payload?.message || payload?.error || '获取预览失败';
@ -814,7 +824,8 @@ export const uiMethods = {
if (!this.quickMenuOpen) { if (!this.quickMenuOpen) {
return; return;
} }
const shell = this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell'); const shell =
this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell');
if (shell && shell.contains(event.target)) { if (shell && shell.contains(event.target)) {
return; return;
} }
@ -845,9 +856,7 @@ export const uiMethods = {
isConversationBlank() { isConversationBlank() {
if (!Array.isArray(this.messages) || !this.messages.length) return true; if (!Array.isArray(this.messages) || !this.messages.length) return true;
return !this.messages.some( return !this.messages.some((msg) => msg && (msg.role === 'user' || msg.role === 'assistant'));
(msg) => msg && (msg.role === 'user' || msg.role === 'assistant')
);
}, },
pickWelcomeText() { pickWelcomeText() {
@ -863,9 +872,7 @@ export const uiMethods = {
refreshBlankHeroState() { refreshBlankHeroState() {
const isBlank = this.isConversationBlank(); const isBlank = this.isConversationBlank();
const currentConv = this.currentConversationId || 'temp'; const currentConv = this.currentConversationId || 'temp';
const needNewWelcome = const needNewWelcome = !this.blankHeroActive || this.lastBlankConversationId !== currentConv;
!this.blankHeroActive ||
this.lastBlankConversationId !== currentConv;
if (isBlank) { if (isBlank) {
if (needNewWelcome && !this.blankHeroExiting) { if (needNewWelcome && !this.blankHeroExiting) {
@ -1129,7 +1136,9 @@ export const uiMethods = {
} }
const payload = await resp.json(); const payload = await resp.json();
const tasks = Array.isArray(payload?.data) ? payload.data : []; const tasks = Array.isArray(payload?.data) ? payload.data : [];
const runningTask = tasks.find((task: any) => task?.status === 'running' && task?.conversation_id); const runningTask = tasks.find(
(task: any) => task?.status === 'running' && task?.conversation_id
);
return runningTask?.conversation_id || null; return runningTask?.conversation_id || null;
} catch (error) { } catch (error) {
console.warn('获取运行中任务失败:', error); console.warn('获取运行中任务失败:', error);
@ -1167,7 +1176,11 @@ export const uiMethods = {
this.titleReady = true; this.titleReady = true;
this.suppressTitleTyping = false; this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false }); this.startTitleTyping(this.currentConversationTitle, { animate: false });
history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`); history.replaceState(
{ conversationId: convId },
'',
`/${this.stripConversationPrefix(convId)}`
);
} else { } else {
history.replaceState({}, '', '/new'); history.replaceState({}, '', '/new');
this.currentConversationId = null; this.currentConversationId = null;
@ -1346,7 +1359,7 @@ export const uiMethods = {
// 加载个性化设置 // 加载个性化设置
const personalizationStore = usePersonalizationStore(); const personalizationStore = usePersonalizationStore();
if (!personalizationStore.loaded && !personalizationStore.loading) { if (!personalizationStore.loaded && !personalizationStore.loading) {
await personalizationStore.fetchPersonalization().catch(err => { await personalizationStore.fetchPersonalization().catch((err) => {
console.warn('加载个性化设置失败:', err); console.warn('加载个性化设置失败:', err);
}); });
} }

View File

@ -266,7 +266,11 @@ export const uploadMethods = {
for (const item of items) { for (const item of items) {
const rawPath = const rawPath =
item?.path || item?.path ||
[path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/'); [path, item?.name]
.filter(Boolean)
.join('/')
.replace(/\\/g, '/')
.replace(/\/{2,}/g, '/');
const type = String(item?.type || '').toLowerCase(); const type = String(item?.type || '').toLowerCase();
if (type === 'directory' || type === 'folder') { if (type === 'directory' || type === 'folder') {
queue.push(rawPath); queue.push(rawPath);
@ -319,7 +323,11 @@ export const uploadMethods = {
for (const item of items) { for (const item of items) {
const rawPath = const rawPath =
item?.path || item?.path ||
[path, item?.name].filter(Boolean).join('/').replace(/\\/g, '/').replace(/\/{2,}/g, '/'); [path, item?.name]
.filter(Boolean)
.join('/')
.replace(/\\/g, '/')
.replace(/\/{2,}/g, '/');
const type = String(item?.type || '').toLowerCase(); const type = String(item?.type || '').toLowerCase();
if (type === 'directory' || type === 'folder') { if (type === 'directory' || type === 'folder') {
queue.push(rawPath); queue.push(rawPath);

View File

@ -18,7 +18,8 @@ export const watchers = {
this.titleTypingTarget = target; this.titleTypingTarget = target;
return; return;
} }
const previous = (oldVal && oldVal.trim()) || (this.titleTypingText && this.titleTypingText.trim()) || ''; const previous =
(oldVal && oldVal.trim()) || (this.titleTypingText && this.titleTypingText.trim()) || '';
const placeholderPrev = !previous || previous === '新对话'; const placeholderPrev = !previous || previous === '新对话';
const placeholderTarget = !target || target === '新对话'; const placeholderTarget = !target || target === '新对话';
const animate = placeholderPrev && !placeholderTarget; // 仅从空/占位切换到真实标题时动画 const animate = placeholderPrev && !placeholderTarget; // 仅从空/占位切换到真实标题时动画
@ -27,7 +28,11 @@ export const watchers = {
currentConversationId: { currentConversationId: {
immediate: false, immediate: false,
handler(newValue, oldValue) { handler(newValue, oldValue) {
debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); debugLog('currentConversationId 变化', {
oldValue,
newValue,
skipConversationHistoryReload: this.skipConversationHistoryReload
});
traceLog('watch:currentConversationId', { traceLog('watch:currentConversationId', {
oldValue, oldValue,
newValue, newValue,
@ -37,7 +42,11 @@ export const watchers = {
historyLoadSeq: this.historyLoadSeq historyLoadSeq: this.historyLoadSeq
}); });
this.refreshBlankHeroState(); this.refreshBlankHeroState();
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); this.logMessageState('watch:currentConversationId', {
oldValue,
newValue,
skipConversationHistoryReload: this.skipConversationHistoryReload
});
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
return; return;
} }

View File

@ -127,13 +127,6 @@ export async function initializeLegacySocket(ctx: any) {
}, step); }, step);
}; };
const hasPendingStreamingText = () =>
STREAMING_ENABLED &&
( !!streamingState.buffer.length ||
!!streamingState.pendingCompleteContent ||
streamingState.timer !== null ||
streamingState.completionTimer !== null);
const resetPendingToolEvents = () => { const resetPendingToolEvents = () => {
pendingToolEvents.length = 0; pendingToolEvents.length = 0;
}; };
@ -193,10 +186,7 @@ export async function initializeLegacySocket(ctx: any) {
) { ) {
streamingState.activeMessageIndex = ctx.currentMessageIndex; streamingState.activeMessageIndex = ctx.currentMessageIndex;
} }
if ( if (typeof ctx?.currentMessageIndex !== 'number' || ctx.currentMessageIndex < 0) {
typeof ctx?.currentMessageIndex !== 'number' ||
ctx.currentMessageIndex < 0
) {
if (streamingState.activeMessageIndex !== null) { if (streamingState.activeMessageIndex !== null) {
ctx.currentMessageIndex = streamingState.activeMessageIndex; ctx.currentMessageIndex = streamingState.activeMessageIndex;
} }
@ -264,9 +254,8 @@ export async function initializeLegacySocket(ctx: any) {
if (action) { if (action) {
action.streaming = false; action.streaming = false;
const fallback = const fallback =
(typeof fullContent === 'string' && fullContent.length (typeof fullContent === 'string' && fullContent.length ? fullContent : action.content) ||
? fullContent '';
: action.content) || '';
action.content = fallback; action.content = fallback;
} }
msg.streamingText = ''; msg.streamingText = '';
@ -320,9 +309,7 @@ export async function initializeLegacySocket(ctx: any) {
return; return;
} }
const hasPending = const hasPending =
typeof ctx.hasPendingToolActions === 'function' typeof ctx.hasPendingToolActions === 'function' ? ctx.hasPendingToolActions() : false;
? ctx.hasPendingToolActions()
: false;
if (!hasPending) { if (!hasPending) {
ctx.streamingMessage = false; ctx.streamingMessage = false;
ctx.stopRequested = false; ctx.stopRequested = false;
@ -352,14 +339,21 @@ export async function initializeLegacySocket(ctx: any) {
const finalizeStreamingText = (options?: { force?: boolean; allowIncomplete?: boolean }) => { const finalizeStreamingText = (options?: { force?: boolean; allowIncomplete?: boolean }) => {
const forceFlush = !!options?.force; const forceFlush = !!options?.force;
const allowIncomplete = !!options?.allowIncomplete; const allowIncomplete = !!options?.allowIncomplete;
logStreamingDebug('finalizeStreamingText:start', { forceFlush, allowIncomplete, snapshot: snapshotStreamingState() }); logStreamingDebug('finalizeStreamingText:start', {
forceFlush,
allowIncomplete,
snapshot: snapshotStreamingState()
});
if (!forceFlush) { if (!forceFlush) {
if (streamingState.buffer.length) { if (streamingState.buffer.length) {
logStreamingDebug('finalizeStreamingText:blocked-buffer', snapshotStreamingState()); logStreamingDebug('finalizeStreamingText:blocked-buffer', snapshotStreamingState());
return false; return false;
} }
if (!allowIncomplete && !streamingState.apiCompleted) { if (!allowIncomplete && !streamingState.apiCompleted) {
logStreamingDebug('finalizeStreamingText:blocked-api-incomplete', snapshotStreamingState()); logStreamingDebug(
'finalizeStreamingText:blocked-api-incomplete',
snapshotStreamingState()
);
return false; return false;
} }
} }
@ -370,7 +364,9 @@ export async function initializeLegacySocket(ctx: any) {
streamingState.buffer.length = 0; streamingState.buffer.length = 0;
if (remainder) { if (remainder) {
applyTextChunk(remainder); applyTextChunk(remainder);
logStreamingDebug('finalizeStreamingText:force-flush-buffer', { flushedChars: remainder.length }); logStreamingDebug('finalizeStreamingText:force-flush-buffer', {
flushedChars: remainder.length
});
} }
} }
const pendingText = streamingState.pendingCompleteContent || ''; const pendingText = streamingState.pendingCompleteContent || '';
@ -418,11 +414,17 @@ export async function initializeLegacySocket(ctx: any) {
const scheduleFinalizationAfterDrain = () => { const scheduleFinalizationAfterDrain = () => {
if (streamingState.buffer.length) { if (streamingState.buffer.length) {
logStreamingDebug('scheduleFinalizationAfterDrain:buffer-not-empty', snapshotStreamingState()); logStreamingDebug(
'scheduleFinalizationAfterDrain:buffer-not-empty',
snapshotStreamingState()
);
return; return;
} }
if (streamingState.completionTimer !== null) { if (streamingState.completionTimer !== null) {
logStreamingDebug('scheduleFinalizationAfterDrain:already-scheduled', snapshotStreamingState()); logStreamingDebug(
'scheduleFinalizationAfterDrain:already-scheduled',
snapshotStreamingState()
);
return; return;
} }
logStreamingDebug('scheduleFinalizationAfterDrain:scheduled', snapshotStreamingState()); logStreamingDebug('scheduleFinalizationAfterDrain:scheduled', snapshotStreamingState());
@ -549,7 +551,6 @@ export async function initializeLegacySocket(ctx: any) {
scheduleStreamingFlush(); scheduleStreamingFlush();
}; };
const scheduleHistoryReload = (delay = 0, options: { force?: boolean } = {}) => { const scheduleHistoryReload = (delay = 0, options: { force?: boolean } = {}) => {
const { force = false } = options; const { force = false } = options;
if (!ctx || typeof ctx.fetchAndDisplayHistory !== 'function') { if (!ctx || typeof ctx.fetchAndDisplayHistory !== 'function') {
@ -688,7 +689,9 @@ export async function initializeLegacySocket(ctx: any) {
ctx.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0; ctx.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
ctx.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0; ctx.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
socketLog(`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`); socketLog(
`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`
);
const hasContextTokens = typeof data.current_context_tokens === 'number'; const hasContextTokens = typeof data.current_context_tokens === 'number';
if (hasContextTokens && typeof ctx.resourceSetCurrentContextTokens === 'function') { if (hasContextTokens && typeof ctx.resourceSetCurrentContextTokens === 'function') {
@ -705,9 +708,7 @@ export async function initializeLegacySocket(ctx: any) {
// 上下文过长提示 // 上下文过长提示
ctx.socket.on('context_warning', (data) => { ctx.socket.on('context_warning', (data) => {
const title = data?.title || '上下文过长'; const title = data?.title || '上下文过长';
const message = const message = data?.message || '当前对话上下文接近上限,建议使用压缩功能。';
data?.message ||
'当前对话上下文接近上限,建议使用压缩功能。';
if (typeof ctx.uiPushToast === 'function') { if (typeof ctx.uiPushToast === 'function') {
ctx.uiPushToast({ ctx.uiPushToast({
title, title,
@ -766,7 +767,7 @@ export async function initializeLegacySocket(ctx: any) {
// 更新对话列表中的标题 // 更新对话列表中的标题
if (data.title && Array.isArray(ctx.conversations)) { if (data.title && Array.isArray(ctx.conversations)) {
const conv = ctx.conversations.find(c => c && c.id === data.conversation_id); const conv = ctx.conversations.find((c) => c && c.id === data.conversation_id);
if (conv) { if (conv) {
conv.title = data.title; conv.title = data.title;
} }
@ -926,7 +927,6 @@ export async function initializeLegacySocket(ctx: any) {
ctx.conditionalScrollToBottom(); ctx.conditionalScrollToBottom();
}); });
// AI消息开始已废弃改用 REST API 轮询) // AI消息开始已废弃改用 REST API 轮询)
ctx.socket.on('ai_message_start', () => { ctx.socket.on('ai_message_start', () => {
// 只处理子智能体模式 // 只处理子智能体模式
@ -1363,7 +1363,8 @@ export async function initializeLegacySocket(ctx: any) {
if (!message.actions) continue; if (!message.actions) continue;
for (const action of message.actions) { for (const action of message.actions) {
if (action.type !== 'tool') continue; if (action.type !== 'tool') continue;
const matchByExecution = action.tool.executionId && action.tool.executionId === data.id; const matchByExecution =
action.tool.executionId && action.tool.executionId === data.id;
const matchByToolId = action.tool.id === data.id; const matchByToolId = action.tool.id === data.id;
const matchByPreparingId = action.id === data.preparing_id; const matchByPreparingId = action.id === data.preparing_id;
if (matchByExecution || matchByToolId || matchByPreparingId) { if (matchByExecution || matchByToolId || matchByPreparingId) {
@ -1380,7 +1381,11 @@ export async function initializeLegacySocket(ctx: any) {
if (data.result !== undefined) { if (data.result !== undefined) {
targetAction.tool.result = data.result; targetAction.tool.result = data.result;
} }
if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) { if (
targetAction.tool &&
targetAction.tool.name === 'trigger_easter_egg' &&
data.result !== undefined
) {
const eggPromise = ctx.handleEasterEggPayload(data.result); const eggPromise = ctx.handleEasterEggPayload(data.result);
if (eggPromise && typeof eggPromise.catch === 'function') { if (eggPromise && typeof eggPromise.catch === 'function') {
eggPromise.catch((error) => { eggPromise.catch((error) => {
@ -1405,13 +1410,19 @@ export async function initializeLegacySocket(ctx: any) {
if (data.arguments) { if (data.arguments) {
targetAction.tool.arguments = data.arguments; targetAction.tool.arguments = data.arguments;
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments); targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot); targetAction.tool.argumentLabel = ctx.buildToolLabel(
targetAction.tool.argumentSnapshot
);
if (data.arguments.intent) { if (data.arguments.intent) {
startIntentTyping(targetAction, data.arguments.intent); startIntentTyping(targetAction, data.arguments.intent);
} }
} }
ctx.toolRegisterAction(targetAction, data.execution_id || data.id); ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) { if (
data.status &&
['completed', 'failed', 'error'].includes(data.status) &&
!data.awaiting_content
) {
ctx.toolUnregisterAction(targetAction); ctx.toolUnregisterAction(targetAction);
if (data.id) { if (data.id) {
ctx.preparingTools.delete(data.id); ctx.preparingTools.delete(data.id);
@ -1563,7 +1574,9 @@ export async function initializeLegacySocket(ctx: any) {
// 显示等待提示 // 显示等待提示
if (typeof ctx.appendSystemAction === 'function') { if (typeof ctx.appendSystemAction === 'function') {
const taskList = data.tasks.map((t: any) => `子智能体${t.agent_id} (${t.summary || '无描述'})`).join('、'); const taskList = data.tasks
.map((t: any) => `子智能体${t.agent_id} (${t.summary || '无描述'})`)
.join('、');
ctx.appendSystemAction(`⏳ 等待 ${data.count} 个后台子智能体完成:${taskList}`); ctx.appendSystemAction(`⏳ 等待 ${data.count} 个后台子智能体完成:${taskList}`);
} }
@ -1687,7 +1700,6 @@ export async function initializeLegacySocket(ctx: any) {
ctx.addSystemMessage(`命令失败: ${data.message}`); ctx.addSystemMessage(`命令失败: ${data.message}`);
} }
}); });
} catch (error) { } catch (error) {
console.error('Socket初始化失败:', error); console.error('Socket初始化失败:', error);
} }

View File

@ -11,7 +11,9 @@ 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(
/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g,
(match, attributes, content) => {
const langMatch = attributes.match(/class="[^"]*language-(\w+)/); const langMatch = attributes.match(/class="[^"]*language-(\w+)/);
const language = langMatch ? langMatch[1] : 'text'; const language = langMatch ? langMatch[1] : 'text';
const blockId = `code-${Date.now()}-${counter++}`; const blockId = `code-${Date.now()}-${counter++}`;
@ -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: [

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = [];

View File

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

View File

@ -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) {
@ -581,7 +605,11 @@ export const useMonitorStore = defineStore('monitor', {
return; return;
} }
const waitKey = const waitKey =
event.payload?.executionId || event.payload?.execution_id || event.id || event.payload?.id || null; event.payload?.executionId ||
event.payload?.execution_id ||
event.id ||
event.payload?.id ||
null;
monitorLifecycleLog('runScript:start', { monitorLifecycleLog('runScript:start', {
script: event.script, script: event.script,
waitKey, waitKey,

View File

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

View File

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

View File

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

View File

@ -2,7 +2,9 @@
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);
}; };
@ -17,12 +19,12 @@ export const useTaskStore = defineStore('task', {
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: { actions: {
@ -43,7 +45,7 @@ export const useTaskStore = defineStore('task', {
const response = await fetch('/api/tasks', { const response = await fetch('/api/tasks', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
message, message,
@ -52,8 +54,9 @@ export const useTaskStore = defineStore('task', {
conversation_id: conversationId, conversation_id: conversationId,
model_key: options.model_key ?? undefined, model_key: options.model_key ?? undefined,
run_mode: options.run_mode ?? undefined, run_mode: options.run_mode ?? undefined,
thinking_mode: typeof options.thinking_mode === 'boolean' ? options.thinking_mode : undefined, thinking_mode:
}), typeof options.thinking_mode === 'boolean' ? options.thinking_mode : undefined
})
}); });
if (!response.ok) { if (!response.ok) {
@ -128,7 +131,13 @@ export const useTaskStore = defineStore('task', {
} }
} }
const interesting = data.events.filter((e: any) => const interesting = data.events.filter((e: any) =>
['user_message', 'sub_agent_waiting', 'task_complete', 'task_stopped', 'error'].includes(e?.type) [
'user_message',
'sub_agent_waiting',
'task_complete',
'task_stopped',
'error'
].includes(e?.type)
); );
if (interesting.length > 0) { if (interesting.length > 0) {
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] events', { keyNotifyLog('[DEBUG_NOTIFY_KEY][task] events', {
@ -228,7 +237,7 @@ export const useTaskStore = defineStore('task', {
this.isPolling = true; this.isPolling = true;
// 如果没有传入 eventHandler从根实例获取 // 如果没有传入 eventHandler从根实例获取
const handler = eventHandler || ((window as any).__taskEventHandler); const handler = eventHandler || (window as any).__taskEventHandler;
if (!handler) { if (!handler) {
console.error('[Task] 没有事件处理器,无法启动轮询'); console.error('[Task] 没有事件处理器,无法启动轮询');
@ -245,7 +254,14 @@ export const useTaskStore = defineStore('task', {
}, 150); }, 150);
}, },
resumeTask(taskId: string, options: { status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled'; resetOffset?: boolean; eventHandler?: (event: any) => void } = {}) { resumeTask(
taskId: string,
options: {
status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled';
resetOffset?: boolean;
eventHandler?: (event: any) => void;
} = {}
) {
if (!taskId) { if (!taskId) {
return; return;
} }
@ -290,7 +306,7 @@ export const useTaskStore = defineStore('task', {
debugLog('[Task] 取消任务:', this.currentTaskId); debugLog('[Task] 取消任务:', this.currentTaskId);
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, { const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
method: 'POST', method: 'POST'
}); });
if (!response.ok) { if (!response.ok) {
@ -324,7 +340,8 @@ export const useTaskStore = defineStore('task', {
} }
// 查找运行中的任务 // 查找运行中的任务
const runningTask = result.data.find((task: any) => const runningTask = result.data.find(
(task: any) =>
task.status === 'running' && task.status === 'running' &&
(!conversationId || task.conversation_id === conversationId) (!conversationId || task.conversation_id === conversationId)
); );
@ -345,7 +362,8 @@ export const useTaskStore = defineStore('task', {
const detailResult = await detailResponse.json(); const detailResult = await detailResponse.json();
if (detailResult.success && detailResult.data.events) { if (detailResult.success && detailResult.data.events) {
// 设置为当前事件数量,只获取新事件 // 设置为当前事件数量,只获取新事件
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length; this.lastEventIndex =
detailResult.data.next_offset || detailResult.data.events.length;
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex); debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
} else { } else {
this.lastEventIndex = 0; this.lastEventIndex = 0;
@ -387,6 +405,6 @@ export const useTaskStore = defineStore('task', {
this.lastEventIndex = 0; this.lastEventIndex = 0;
this.taskStatus = 'idle'; this.taskStatus = 'idle';
this.pollingError = null; this.pollingError = null;
}, }
}, }
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -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(', ') : '未限定网站';
} }

View File

@ -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)} 重置`;