diff --git a/package.json b/package.json index ebc54ede53..eecde4108a 100644 --- a/package.json +++ b/package.json @@ -396,9 +396,15 @@ "access": "public", "registry": "https://registry.npmjs.org" }, + "overrides": { + "stylelint-config-clean-order": "7.0.0" + }, "pnpm": { "onlyBuiltDependencies": [ "@vercel/speed-insights" - ] + ], + "overrides": { + "stylelint-config-clean-order": "7.0.0" + } } } diff --git a/packages/database/src/repositories/knowledge/index.ts b/packages/database/src/repositories/knowledge/index.ts index 66b2bfd0ce..05ef90e99b 100644 --- a/packages/database/src/repositories/knowledge/index.ts +++ b/packages/database/src/repositories/knowledge/index.ts @@ -129,12 +129,6 @@ export class KnowledgeRepo { }; }); - console.log('[KnowledgeRepo.query] Fetched items:', { - count: mappedResults.length, - documents: mappedResults.filter((item) => item.sourceType === 'document'), - sampleEditorData: mappedResults.find((item) => item.sourceType === 'document')?.editorData, - }); - return mappedResults; } @@ -308,7 +302,10 @@ export class KnowledgeRepo { // Exclude custom/document and source_type='file' from Documents category if (category === FilesTabs.Documents) { - whereConditions.push(sql`${documents.fileType} != ${'custom/document'}`, sql`${documents.sourceType} != ${'file'}`); + whereConditions.push( + sql`${documents.fileType} != ${'custom/document'}`, + sql`${documents.sourceType} != ${'file'}`, + ); } } else if (fileTypePrefix) { whereConditions.push(sql`${documents.fileType} ILIKE ${`${fileTypePrefix}%`}`); @@ -338,7 +335,7 @@ export class KnowledgeRepo { // Documents don't have knowledge base association currently, so skip if knowledgeBaseId is set if (knowledgeBaseId) { return sql` - SELECT + SELECT NULL::varchar(30) as id, NULL::text as name, NULL::varchar(255) as file_type, diff --git a/packages/types/src/user/settings/keyVaults.ts b/packages/types/src/user/settings/keyVaults.ts index 7bd0bb2065..7467cde520 100644 --- a/packages/types/src/user/settings/keyVaults.ts +++ b/packages/types/src/user/settings/keyVaults.ts @@ -51,73 +51,5 @@ export interface SearchEngineKeyVaults { } export interface UserKeyVaults extends SearchEngineKeyVaults { - ai21?: OpenAICompatibleKeyVault; - ai302?: OpenAICompatibleKeyVault; - ai360?: OpenAICompatibleKeyVault; - aihubmix?: OpenAICompatibleKeyVault; - akashchat?: OpenAICompatibleKeyVault; - anthropic?: OpenAICompatibleKeyVault; - azure?: AzureOpenAIKeyVault; - azureai?: AzureOpenAIKeyVault; - baichuan?: OpenAICompatibleKeyVault; - bedrock?: AWSBedrockKeyVault; - bfl?: any; - cerebras?: OpenAICompatibleKeyVault; - cloudflare?: CloudflareKeyVault; - cohere?: OpenAICompatibleKeyVault; - cometapi?: OpenAICompatibleKeyVault; - comfyui?: ComfyUIKeyVault; - deepseek?: OpenAICompatibleKeyVault; - fal?: FalKeyVault; - fireworksai?: OpenAICompatibleKeyVault; - giteeai?: OpenAICompatibleKeyVault; - github?: OpenAICompatibleKeyVault; - google?: OpenAICompatibleKeyVault; - groq?: OpenAICompatibleKeyVault; - higress?: OpenAICompatibleKeyVault; - huggingface?: OpenAICompatibleKeyVault; - hunyuan?: OpenAICompatibleKeyVault; - infiniai?: OpenAICompatibleKeyVault; - internlm?: OpenAICompatibleKeyVault; - jina?: OpenAICompatibleKeyVault; - lmstudio?: OpenAICompatibleKeyVault; - lobehub?: any; - minimax?: OpenAICompatibleKeyVault; - mistral?: OpenAICompatibleKeyVault; - modelscope?: OpenAICompatibleKeyVault; - moonshot?: OpenAICompatibleKeyVault; - nebius?: OpenAICompatibleKeyVault; - newapi?: OpenAICompatibleKeyVault; - novita?: OpenAICompatibleKeyVault; - nvidia?: OpenAICompatibleKeyVault; - ollama?: OpenAICompatibleKeyVault; - ollamacloud?: OpenAICompatibleKeyVault; - openai?: OpenAICompatibleKeyVault; - openrouter?: OpenAICompatibleKeyVault; - password?: string; - perplexity?: OpenAICompatibleKeyVault; - ppio?: OpenAICompatibleKeyVault; - qiniu?: OpenAICompatibleKeyVault; - qwen?: OpenAICompatibleKeyVault; - sambanova?: OpenAICompatibleKeyVault; search1api?: OpenAICompatibleKeyVault; - sensenova?: OpenAICompatibleKeyVault; - siliconcloud?: OpenAICompatibleKeyVault; - spark?: OpenAICompatibleKeyVault; - stepfun?: OpenAICompatibleKeyVault; - taichu?: OpenAICompatibleKeyVault; - tencentcloud?: OpenAICompatibleKeyVault; - togetherai?: OpenAICompatibleKeyVault; - upstage?: OpenAICompatibleKeyVault; - v0?: OpenAICompatibleKeyVault; - vercelaigateway?: OpenAICompatibleKeyVault; - vertexai?: VertexAIKeyVault; - vllm?: OpenAICompatibleKeyVault; - volcengine?: OpenAICompatibleKeyVault; - wenxin?: OpenAICompatibleKeyVault; - xai?: OpenAICompatibleKeyVault; - xinference?: OpenAICompatibleKeyVault; - zenmux?: OpenAICompatibleKeyVault; - zeroone?: OpenAICompatibleKeyVault; - zhipu?: OpenAICompatibleKeyVault; } diff --git a/src/features/ChatList/Error/AccessCodeForm.tsx b/src/features/ChatList/Error/AccessCodeForm.tsx deleted file mode 100644 index e3fab2789c..0000000000 --- a/src/features/ChatList/Error/AccessCodeForm.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button, InputPassword } from '@lobehub/ui'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Flexbox } from 'react-layout-kit'; - -import { useChatStore } from '@/store/chat'; -import { useUserStore } from '@/store/user'; -import { keyVaultsConfigSelectors } from '@/store/user/selectors'; - -import { FormAction } from './style'; - -interface AccessCodeFormProps { - id: string; -} - -const AccessCodeForm = memo(({ id }) => { - const { t } = useTranslation('error'); - const [password, updateKeyVaults] = useUserStore((s) => [ - keyVaultsConfigSelectors.password(s), - s.updateKeyVaults, - ]); - const [resend, deleteMessage] = useChatStore((s) => [s.delAndRegenerateMessage, s.deleteMessage]); - - return ( - <> - - { - updateKeyVaults({ password: e.target.value }); - }} - placeholder={t('unlock.password.placeholder')} - value={password} - variant={'filled'} - /> - - - - - - - ); -}); - -export default AccessCodeForm; diff --git a/src/services/__tests__/_auth.test.ts b/src/services/__tests__/_auth.test.ts index 29cf287e76..53f5e9dd13 100644 --- a/src/services/__tests__/_auth.test.ts +++ b/src/services/__tests__/_auth.test.ts @@ -23,10 +23,7 @@ const mockTogetherAIAPIKey = 'togetherai-api-key'; // mock the traditional zustand vi.mock('zustand/traditional'); -const setModelProviderConfig = ( - provider: T, - config: Partial, -) => { +const setModelProviderConfig = (provider: string, config: any) => { useUserStore.setState({ settings: { keyVaults: { [provider]: config } }, }); diff --git a/src/services/__tests__/share.test.ts b/src/services/__tests__/share.test.ts deleted file mode 100644 index 475477559b..0000000000 --- a/src/services/__tests__/share.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { PartialDeep } from 'type-fest'; -import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { LOBE_URL_IMPORT_NAME } from '@/const/url'; -import { UserSettings } from '@/types/user/settings'; - -import { shareService } from '../share'; - -// Mock dependencies -vi.mock('@/utils/parseMarkdown', () => ({ - parseMarkdown: vi.fn(), -})); - -global.fetch = vi.fn(); - -beforeEach(() => { - vi.clearAllMocks(); -}); -describe('ShareGPTService', () => { - describe('ShareViaUrl', () => { - describe('createShareSettingsUrl', () => { - it('should create a share settings URL with the provided settings', () => { - const settings: PartialDeep = { - keyVaults: { - openai: { - apiKey: 'user-key', - }, - }, - }; - const url = shareService.createShareSettingsUrl(settings); - expect(url).toBe( - `/?${LOBE_URL_IMPORT_NAME}=%7B%22keyVaults%22:%7B%22openai%22:%7B%22apiKey%22:%22user-key%22%7D%7D%7D`, - ); - }); - }); - - describe('decodeShareSettings', () => { - it('should decode share settings from search params', () => { - const settings = '{"languageModel":{"openai":{"apiKey":"user-key"}}}'; - const decodedSettings = shareService.decodeShareSettings(settings); - expect(decodedSettings).toEqual({ - data: { - languageModel: { - openai: { - apiKey: 'user-key', - }, - }, - }, - }); - }); - - it('should return an error message if decoding fails', () => { - const settings = '%7B%22theme%22%3A%22dark%22%2C%22fontSize%22%3A16%'; - const decodedSettings = shareService.decodeShareSettings(settings); - expect(decodedSettings).toEqual({ - message: expect.any(String), - }); - }); - }); - }); -}); diff --git a/src/services/_auth.ts b/src/services/_auth.ts index e1d21ac837..f6a6a69059 100644 --- a/src/services/_auth.ts +++ b/src/services/_auth.ts @@ -13,7 +13,7 @@ import { ModelProvider } from 'model-bank'; import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra'; import { useUserStore } from '@/store/user'; -import { keyVaultsConfigSelectors, userProfileSelectors } from '@/store/user/selectors'; +import { userProfileSelectors } from '@/store/user/selectors'; import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation'; import { resolveRuntimeProvider } from './chat/helper'; @@ -105,10 +105,9 @@ export const getProviderAuthPayload = ( }; const createAuthTokenWithPayload = (payload = {}) => { - const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState()); const userId = userProfileSelectors.userId(useUserStore.getState()); - return obfuscatePayloadWithXOR({ accessCode, userId, ...payload }); + return obfuscatePayloadWithXOR({ userId, ...payload }); }; interface AuthParams { diff --git a/src/services/_header.ts b/src/services/_header.ts index c116245713..bc51439ff0 100644 --- a/src/services/_header.ts +++ b/src/services/_header.ts @@ -1,12 +1,6 @@ -import { - LOBE_CHAT_ACCESS_CODE, - LOBE_USER_ID, - OPENAI_API_KEY_HEADER_KEY, - OPENAI_END_POINT, -} from '@/const/fetch'; +import { LOBE_USER_ID, OPENAI_API_KEY_HEADER_KEY, OPENAI_END_POINT } from '@/const/fetch'; import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra'; import { useUserStore } from '@/store/user'; -import { keyVaultsConfigSelectors } from '@/store/user/selectors'; /** * TODO: Need to be removed after tts refactor @@ -22,7 +16,6 @@ export const createHeaderWithOpenAI = (header?: HeadersInit): HeadersInit => { // eslint-disable-next-line no-undef return { ...header, - [LOBE_CHAT_ACCESS_CODE]: keyVaultsConfigSelectors.password(state), [LOBE_USER_ID]: state.user?.id || '', [OPENAI_API_KEY_HEADER_KEY]: keyVaults.apiKey || '', [OPENAI_END_POINT]: keyVaults.baseURL || '', diff --git a/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts b/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts index 970ae997f9..3b23a9e73b 100644 --- a/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +++ b/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts @@ -58,6 +58,9 @@ describe('call_llm executor', () => { sessionId: 'test-session', topicId: 'test-topic', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -229,6 +232,9 @@ describe('call_llm executor', () => { expect.objectContaining({ parentId: 'msg_payload_parent', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -262,6 +268,9 @@ describe('call_llm executor', () => { expect.objectContaining({ parentId: 'msg_context_parent', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); @@ -1061,6 +1070,9 @@ describe('call_llm executor', () => { model: 'claude-3-opus', provider: 'anthropic', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); expect(mockStore.internal_fetchAIChatMessage).toHaveBeenCalledWith( expect.objectContaining({ @@ -1180,6 +1192,9 @@ describe('call_llm executor', () => { expect.objectContaining({ threadId, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -1211,6 +1226,9 @@ describe('call_llm executor', () => { expect.objectContaining({ threadId: undefined, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); diff --git a/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts b/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts index 010e036115..15dbd3195e 100644 --- a/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +++ b/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts @@ -156,17 +156,22 @@ describe('call_tool executor', () => { }); // Then - expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({ - content: '', - groupId: 'group_789', - parentId: 'msg_parent_123', - plugin: toolCall, - role: 'tool', - sessionId: 'sess_123', - threadId: undefined, - tool_call_id: 'tool_call_xyz', - topicId: 'topic_456', - }); + expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith( + expect.objectContaining({ + content: '', + groupId: 'group_789', + parentId: 'msg_parent_123', + plugin: toolCall, + role: 'tool', + sessionId: 'sess_123', + threadId: undefined, + tool_call_id: 'tool_call_xyz', + topicId: 'topic_456', + }), + expect.objectContaining({ + operationId: expect.any(String), + }), + ); }); it('should use assistant message groupId for tool message', async () => { @@ -194,6 +199,9 @@ describe('call_tool executor', () => { expect.objectContaining({ groupId: 'group_special', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -222,6 +230,9 @@ describe('call_tool executor', () => { expect.objectContaining({ parentId: 'msg_custom_parent', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -262,6 +273,9 @@ describe('call_tool executor', () => { expect.objectContaining({ plugin: toolCall, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); @@ -1542,6 +1556,9 @@ describe('call_tool executor', () => { expect.objectContaining({ groupId: undefined, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); expect(result.events).toHaveLength(1); }); @@ -1570,6 +1587,9 @@ describe('call_tool executor', () => { expect.objectContaining({ groupId: undefined, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -1641,6 +1661,9 @@ describe('call_tool executor', () => { expect.objectContaining({ topicId: undefined, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -1679,6 +1702,9 @@ describe('call_tool executor', () => { type: 'builtin', }), }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); expect(result.events).toHaveLength(1); }); @@ -1768,6 +1794,9 @@ describe('call_tool executor', () => { expect.objectContaining({ groupId: 'group_latest', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); diff --git a/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts b/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts index 7ef5beb990..5bc4a5fb46 100644 --- a/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +++ b/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts @@ -22,6 +22,9 @@ export const expectMessageCreated = (mockStore: ChatStore, role: 'assistant' | ' expect.objectContaining({ role, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }; diff --git a/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts b/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts index 0d60aeb276..f96b39b42e 100644 --- a/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +++ b/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts @@ -51,6 +51,9 @@ describe('request_human_approve executor', () => { parentId: 'msg_assistant', groupId: assistantMessage.groupId, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -205,6 +208,9 @@ describe('request_human_approve executor', () => { expect.objectContaining({ groupId: 'group_123', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -232,6 +238,9 @@ describe('request_human_approve executor', () => { expect.objectContaining({ parentId: 'msg_assistant_456', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); @@ -330,6 +339,9 @@ describe('request_human_approve executor', () => { tool_call_id: toolCall.id, pluginIntervention: { status: 'pending' }, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); @@ -539,6 +551,9 @@ describe('request_human_approve executor', () => { expect.objectContaining({ parentId: 'msg_3_last', }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); diff --git a/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts b/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts index 585723d62d..fbfcec2850 100644 --- a/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +++ b/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts @@ -57,6 +57,9 @@ describe('resolve_aborted_tools executor', () => { topicId: 'test-topic', parentId: parentMessage.id, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -114,6 +117,9 @@ describe('resolve_aborted_tools executor', () => { pluginIntervention: { status: 'aborted' }, tool_call_id: toolCall.id, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); @@ -193,17 +199,22 @@ describe('resolve_aborted_tools executor', () => { }); // Then - expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({ - role: 'tool', - content: 'Tool execution was aborted by user.', - plugin: toolCall, - pluginIntervention: { status: 'aborted' }, - tool_call_id: 'tool_abc', - parentId: 'msg_parent', - sessionId: 'sess_123', - topicId: 'topic_456', - threadId: undefined, - }); + expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith( + expect.objectContaining({ + role: 'tool', + content: 'Tool execution was aborted by user.', + plugin: toolCall, + pluginIntervention: { status: 'aborted' }, + tool_call_id: 'tool_abc', + parentId: 'msg_parent', + sessionId: 'sess_123', + topicId: 'topic_456', + threadId: undefined, + }), + expect.objectContaining({ + operationId: expect.any(String), + }), + ); }); it('should preserve tool payload details', async () => { @@ -240,6 +251,9 @@ describe('resolve_aborted_tools executor', () => { expect.objectContaining({ plugin: toolCall, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -264,6 +278,9 @@ describe('resolve_aborted_tools executor', () => { expect.objectContaining({ topicId: undefined, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); }); @@ -481,6 +498,9 @@ describe('resolve_aborted_tools executor', () => { expect.objectContaining({ plugin: toolCall, }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -571,6 +591,9 @@ describe('resolve_aborted_tools executor', () => { type: 'builtin', }), }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); @@ -606,6 +629,9 @@ describe('resolve_aborted_tools executor', () => { type: 'default', }), }), + expect.objectContaining({ + operationId: expect.any(String), + }), ); }); diff --git a/src/store/chat/agents/createAgentExecutors.ts b/src/store/chat/agents/createAgentExecutors.ts index 4660cd1d5d..9a21a09a9c 100644 --- a/src/store/chat/agents/createAgentExecutors.ts +++ b/src/store/chat/agents/createAgentExecutors.ts @@ -89,16 +89,19 @@ export const createAgentExecutors = (context: { llmPayload.parentMessageId = context.parentId; } // Create assistant message (following server-side pattern) - const assistantMessageItem = await context.get().optimisticCreateMessage({ - content: LOADING_FLAT, - model: llmPayload.model, - parentId: llmPayload.parentMessageId, - provider: llmPayload.provider, - role: 'assistant', - sessionId: opContext.sessionId!, - threadId: opContext.threadId, - topicId: opContext.topicId ?? undefined, - }); + const assistantMessageItem = await context.get().optimisticCreateMessage( + { + content: LOADING_FLAT, + model: llmPayload.model, + parentId: llmPayload.parentMessageId, + provider: llmPayload.provider, + role: 'assistant', + sessionId: opContext.sessionId!, + threadId: opContext.threadId, + topicId: opContext.topicId ?? undefined, + }, + { operationId: context.operationId }, + ); if (!assistantMessageItem) { throw new Error('Failed to create assistant message'); @@ -371,7 +374,9 @@ export const createAgentExecutors = (context: { topicId: opContext.topicId ?? undefined, }; - const createPromise = context.get().optimisticCreateMessage(toolMessageParams); + const createPromise = context + .get() + .optimisticCreateMessage(toolMessageParams, { operationId: createToolMsgOpId }); context.get().updateOperationMetadata(createToolMsgOpId, { createMessagePromise: createPromise, }); @@ -632,7 +637,9 @@ export const createAgentExecutors = (context: { topicId: opContext.topicId ?? undefined, }; - const createResult = await context.get().optimisticCreateMessage(toolMessageParams); + const createResult = await context + .get() + .optimisticCreateMessage(toolMessageParams, { operationId: context.operationId }); if (!createResult) { log( @@ -709,7 +716,9 @@ export const createAgentExecutors = (context: { topicId: opContext.topicId ?? undefined, }; - const createResult = await context.get().optimisticCreateMessage(toolMessageParams); + const createResult = await context + .get() + .optimisticCreateMessage(toolMessageParams, { operationId: context.operationId }); if (createResult) { log( diff --git a/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts b/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts index 83e22a1f78..dc8ceaa859 100644 --- a/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +++ b/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts @@ -261,6 +261,10 @@ export const conversationLifecycle: StateCreator< summaryTitle().catch(console.error); + // Complete sendMessage operation here - message creation is done + // execAgentRuntime is a separate operation (child) that handles AI response generation + get().completeOperation(operationId); + // Get the current messages to generate AI response const displayMessages = displayMessageSelectors.activeDisplayMessages(get()); @@ -287,16 +291,8 @@ export const conversationLifecycle: StateCreator< if (userFiles.length > 0) { await getAgentStoreState().addFilesToAgent(userFiles, false); } - - // Complete operation on success - get().completeOperation(operationId); } catch (e) { console.error(e); - // Fail operation on error - get().failOperation(operationId, { - type: e instanceof Error ? e.name : 'unknown_error', - message: e instanceof Error ? e.message : 'AI generation failed', - }); } finally { if (data.topicId) get().internal_updateTopicLoading(data.topicId, false); } diff --git a/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts b/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts index 881c0f4748..562f4d8503 100644 --- a/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +++ b/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts @@ -241,9 +241,18 @@ describe('search actions', () => { it('should update arguments and perform search', async () => { const { result } = renderHook(() => useChatStore()); const spy = vi.spyOn(result.current, 'search'); - const { triggerSearchAgain } = result.current; const messageId = 'test-message-id'; + const operationId = 'op_test'; + + // Set up messageOperationMap so triggerSearchAgain can get operationId + useChatStore.setState({ + messageOperationMap: { + [messageId]: operationId, + }, + }); + + const { triggerSearchAgain } = result.current; const query: SearchQuery = { query: 'test query', }; @@ -252,7 +261,12 @@ describe('search actions', () => { await triggerSearchAgain(messageId, query, { aiSummary: true }); }); - expect(result.current.optimisticUpdatePluginArguments).toHaveBeenCalledWith(messageId, query); + expect(result.current.optimisticUpdatePluginArguments).toHaveBeenCalledWith( + messageId, + query, + false, + { operationId }, + ); expect(spy).toHaveBeenCalledWith(messageId, query, true); }); }); diff --git a/src/store/chat/slices/builtinTool/actions/localSystem.ts b/src/store/chat/slices/builtinTool/actions/localSystem.ts index ad3415c915..6b1151b667 100644 --- a/src/store/chat/slices/builtinTool/actions/localSystem.ts +++ b/src/store/chat/slices/builtinTool/actions/localSystem.ts @@ -108,7 +108,11 @@ export const localSystemSlice: StateCreator< }, reSearchLocalFiles: async (id, params) => { - await get().optimisticUpdatePluginArguments(id, params); + // Get operationId from messageOperationMap to ensure proper context isolation + const operationId = get().messageOperationMap[id]; + const context = operationId ? { operationId } : undefined; + + await get().optimisticUpdatePluginArguments(id, params, false, context); return get().searchLocalFiles(id, params); }, diff --git a/src/store/chat/slices/builtinTool/actions/search.ts b/src/store/chat/slices/builtinTool/actions/search.ts index 62e99d42dd..cd55eab825 100644 --- a/src/store/chat/slices/builtinTool/actions/search.ts +++ b/src/store/chat/slices/builtinTool/actions/search.ts @@ -276,7 +276,11 @@ export const searchSlice: StateCreator< }, triggerSearchAgain: async (id, data, options) => { - await get().optimisticUpdatePluginArguments(id, data); + // Get operationId from messageOperationMap to ensure proper context isolation + const operationId = get().messageOperationMap[id]; + const context = operationId ? { operationId } : undefined; + + await get().optimisticUpdatePluginArguments(id, data, false, context); await get().search(id, data, options?.aiSummary); }, diff --git a/src/store/chat/slices/message/actions/publicApi.ts b/src/store/chat/slices/message/actions/publicApi.ts index c72e9ffab9..af7d327d37 100644 --- a/src/store/chat/slices/message/actions/publicApi.ts +++ b/src/store/chat/slices/message/actions/publicApi.ts @@ -167,14 +167,22 @@ export const messagePublicApi: StateCreator< const message = dbMessageSelectors.getDbMessageById(id)(get()); if (!message || message.role !== 'tool') return; + // Get operationId from messageOperationMap to ensure proper context isolation + const operationId = get().messageOperationMap[id]; + const context = operationId ? { operationId } : undefined; + const removeToolInAssistantMessage = async () => { if (!message.parentId) return; - await get().optimisticRemoveToolFromAssistantMessage(message.parentId, message.tool_call_id); + await get().optimisticRemoveToolFromAssistantMessage( + message.parentId, + message.tool_call_id, + context, + ); }; await Promise.all([ // 1. remove tool message - get().optimisticDeleteMessage(id), + get().optimisticDeleteMessage(id, context), // 2. remove the tool item in the assistant tools removeToolInAssistantMessage(), ]); diff --git a/src/store/chat/slices/message/actions/query.ts b/src/store/chat/slices/message/actions/query.ts index 1f757708e5..3e1bb2b67a 100644 --- a/src/store/chat/slices/message/actions/query.ts +++ b/src/store/chat/slices/message/actions/query.ts @@ -32,6 +32,7 @@ export interface MessageQueryAction { messages: UIChatMessage[], params?: { action?: any; + operationId?: string; sessionId?: string; topicId?: string | null; }, @@ -66,10 +67,22 @@ export const messageQuery: StateCreator< }, replaceMessages: (messages, params) => { - const messagesKey = messageMapKey( - params?.sessionId ?? get().activeId, - params?.topicId ?? get().activeTopicId, - ); + let sessionId: string; + let topicId: string | null | undefined; + + // Priority 1: Get context from operation if operationId is provided + if (params?.operationId) { + const { sessionId: opSessionId, topicId: opTopicId } = + get().internal_getSessionContext(params); + sessionId = opSessionId; + topicId = opTopicId; + } else { + // Priority 2: Use explicit sessionId/topicId or fallback to global state + sessionId = params?.sessionId ?? get().activeId; + topicId = params?.topicId ?? get().activeTopicId; + } + + const messagesKey = messageMapKey(sessionId, topicId); // Get raw messages from dbMessagesMap and apply reducer const nextDbMap = { ...get().dbMessagesMap, [messagesKey]: messages }; diff --git a/src/store/chat/slices/operation/__tests__/selectors.test.ts b/src/store/chat/slices/operation/__tests__/selectors.test.ts index edc7a0ccc8..ef0461331d 100644 --- a/src/store/chat/slices/operation/__tests__/selectors.test.ts +++ b/src/store/chat/slices/operation/__tests__/selectors.test.ts @@ -393,6 +393,11 @@ describe('Operation Selectors', () => { it('isMainWindowAgentRuntimeRunning should only detect main window operations', () => { const { result } = renderHook(() => useChatStore()); + // Set active context + act(() => { + useChatStore.setState({ activeId: 'session1', activeTopicId: undefined }); + }); + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false); // Start a main window operation (inThread: false) @@ -400,7 +405,7 @@ describe('Operation Selectors', () => { act(() => { mainOpId = result.current.startOperation({ type: 'execAgentRuntime', - context: { sessionId: 'session1' }, + context: { sessionId: 'session1', topicId: null }, metadata: { inThread: false }, }).operationId; }); @@ -420,12 +425,17 @@ describe('Operation Selectors', () => { it('isMainWindowAgentRuntimeRunning should exclude thread operations', () => { const { result } = renderHook(() => useChatStore()); + // Set active context + act(() => { + useChatStore.setState({ activeId: 'session1', activeTopicId: undefined }); + }); + // Start a thread operation (inThread: true) let threadOpId: string; act(() => { threadOpId = result.current.startOperation({ type: 'execAgentRuntime', - context: { sessionId: 'session1', threadId: 'thread1' }, + context: { sessionId: 'session1', topicId: null, threadId: 'thread1' }, metadata: { inThread: true }, }).operationId; }); @@ -447,6 +457,11 @@ describe('Operation Selectors', () => { it('isMainWindowAgentRuntimeRunning should distinguish between main and thread operations', () => { const { result } = renderHook(() => useChatStore()); + // Set active context + act(() => { + useChatStore.setState({ activeId: 'session1', activeTopicId: undefined }); + }); + let mainOpId: string; let threadOpId: string; @@ -454,13 +469,13 @@ describe('Operation Selectors', () => { act(() => { mainOpId = result.current.startOperation({ type: 'execAgentRuntime', - context: { sessionId: 'session1' }, + context: { sessionId: 'session1', topicId: null }, metadata: { inThread: false }, }).operationId; threadOpId = result.current.startOperation({ type: 'execAgentRuntime', - context: { sessionId: 'session1', threadId: 'thread1' }, + context: { sessionId: 'session1', topicId: null, threadId: 'thread1' }, metadata: { inThread: true }, }).operationId; }); @@ -489,11 +504,16 @@ describe('Operation Selectors', () => { it('isMainWindowAgentRuntimeRunning should exclude aborting operations', () => { const { result } = renderHook(() => useChatStore()); + // Set active context + act(() => { + useChatStore.setState({ activeId: 'session1', activeTopicId: undefined }); + }); + let opId: string; act(() => { opId = result.current.startOperation({ type: 'execAgentRuntime', - context: { sessionId: 'session1' }, + context: { sessionId: 'session1', topicId: null }, metadata: { inThread: false }, }).operationId; }); @@ -509,5 +529,73 @@ describe('Operation Selectors', () => { expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false); expect(operationSelectors.isAgentRuntimeRunning(result.current)).toBe(false); }); + + it('isMainWindowAgentRuntimeRunning should only detect operations in current active topic', () => { + const { result } = renderHook(() => useChatStore()); + + // Set active session and topic + act(() => { + useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' }); + }); + + let topic1OpId: string; + let topic2OpId: string; + + // Start operation in topic1 (current active topic) + act(() => { + topic1OpId = result.current.startOperation({ + type: 'execAgentRuntime', + context: { sessionId: 'session1', topicId: 'topic1' }, + metadata: { inThread: false }, + }).operationId; + }); + + // Should detect operation in current topic + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true); + + // Start operation in topic2 (different topic) + act(() => { + topic2OpId = result.current.startOperation({ + type: 'execAgentRuntime', + context: { sessionId: 'session1', topicId: 'topic2' }, + metadata: { inThread: false }, + }).operationId; + }); + + // Should still only detect topic1 operation (current active topic) + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true); + + // Switch to topic2 + act(() => { + useChatStore.setState({ activeTopicId: 'topic2' }); + }); + + // Should now detect topic2 operation + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true); + + // Complete topic2 operation + act(() => { + result.current.completeOperation(topic2OpId!); + }); + + // Should not detect any operation in topic2 now + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false); + + // Switch back to topic1 + act(() => { + useChatStore.setState({ activeTopicId: 'topic1' }); + }); + + // Should detect topic1 operation again + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true); + + // Complete topic1 operation + act(() => { + result.current.completeOperation(topic1OpId!); + }); + + // Should not detect any operation now + expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false); + }); }); }); diff --git a/src/store/chat/slices/operation/selectors.ts b/src/store/chat/slices/operation/selectors.ts index 5a7bc29c33..768405e51a 100644 --- a/src/store/chat/slices/operation/selectors.ts +++ b/src/store/chat/slices/operation/selectors.ts @@ -180,14 +180,27 @@ const isAgentRuntimeRunning = (s: ChatStoreState): boolean => { /** * Check if agent runtime is running in main window only * Used for main window UI state (e.g., send button loading) - * Excludes thread operations to prevent cross-contamination + * Excludes thread operations and operations from other topics to prevent cross-contamination */ const isMainWindowAgentRuntimeRunning = (s: ChatStoreState): boolean => { const operationIds = s.operationsByType['execAgentRuntime'] || []; + return operationIds.some((id) => { const op = s.operations[id]; - // Only include main window operations (not thread) - return op && op.status === 'running' && !op.metadata.isAborting && !op.metadata.inThread; + if (!op || op.status !== 'running' || op.metadata.isAborting || op.metadata.inThread) { + return false; + } + + // Session must match + if (s.activeId !== op.context.sessionId) return false; + + // Topic comparison: normalize null/undefined (both mean "default topic") + // activeTopicId can be null (initial state) or undefined (after topic operations) + // Operation context topicId can also be null or undefined + const activeTopicId = s.activeTopicId ?? null; + const opTopicId = op.context.topicId ?? null; + + return activeTopicId === opTopicId; }); }; diff --git a/src/store/chat/slices/plugin/actions/optimisticUpdate.ts b/src/store/chat/slices/plugin/actions/optimisticUpdate.ts index 92243052ac..9c92591016 100644 --- a/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +++ b/src/store/chat/slices/plugin/actions/optimisticUpdate.ts @@ -32,6 +32,7 @@ export interface PluginOptimisticUpdateAction { id: string, value: T, replace?: boolean, + context?: OptimisticUpdateContext, ) => Promise; /** @@ -106,7 +107,7 @@ export const pluginOptimisticUpdate: StateCreator< } }, - optimisticUpdatePluginArguments: async (id, value, replace = false) => { + optimisticUpdatePluginArguments: async (id, value, replace = false, context) => { const { refreshMessages } = get(); const toolMessage = displayMessageSelectors.getDisplayMessageById(id)(get()); if (!toolMessage || !toolMessage?.tool_call_id) return; @@ -121,20 +122,22 @@ export const pluginOptimisticUpdate: StateCreator< if (isEqual(prevJson, nextValue)) return; // optimistic update - get().internal_dispatchMessage({ - id, - type: 'updateMessagePlugin', - value: { arguments: JSON.stringify(nextValue) }, - }); + get().internal_dispatchMessage( + { id, type: 'updateMessagePlugin', value: { arguments: JSON.stringify(nextValue) } }, + context, + ); // 同样需要更新 assistantMessage 的 pluginArguments if (assistantMessage) { - get().internal_dispatchMessage({ - id: assistantMessage.id, - type: 'updateMessageTools', - tool_call_id: toolMessage?.tool_call_id, - value: { arguments: JSON.stringify(nextValue) }, - }); + get().internal_dispatchMessage( + { + id: assistantMessage.id, + type: 'updateMessageTools', + tool_call_id: toolMessage?.tool_call_id, + value: { arguments: JSON.stringify(nextValue) }, + }, + context, + ); assistantMessage = displayMessageSelectors.getDisplayMessageById(assistantMessage?.id)(get()); } @@ -183,11 +186,14 @@ export const pluginOptimisticUpdate: StateCreator< if (!assistantMessage) return; const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get(); - internal_dispatchMessage({ - type: 'addMessageTool', - value: tool, - id: assistantMessage.id, - }); + internal_dispatchMessage( + { + type: 'addMessageTool', + value: tool, + id: assistantMessage.id, + }, + context, + ); await internal_refreshToUpdateMessageTools(id, context); }, @@ -199,7 +205,7 @@ export const pluginOptimisticUpdate: StateCreator< const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get(); // optimistic update - internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id }); + internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id }, context); // update the message tools await internal_refreshToUpdateMessageTools(id, context); diff --git a/src/store/user/slices/settings/selectors/keyVaults.ts b/src/store/user/slices/settings/selectors/keyVaults.ts index bd8626c025..9dd9d8f0df 100644 --- a/src/store/user/slices/settings/selectors/keyVaults.ts +++ b/src/store/user/slices/settings/selectors/keyVaults.ts @@ -10,12 +10,7 @@ const getVaultByProvider = (provider: string) => (s: UserStore) => // @ts-ignore (keyVaultsSettings(s)[provider] || {}) as any; -const password = (s: UserStore) => keyVaultsSettings(s).password || ''; - export const keyVaultsConfigSelectors = { getVaultByProvider, - keyVaultsSettings, - - password, };