@lobehub/lobehub 2.0.0-next.84 → 2.0.0-next.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
  3. package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
  4. package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
  5. package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
  6. package/changelog/v1.json +18 -0
  7. package/package.json +1 -1
  8. package/packages/agent-runtime/src/core/runtime.ts +36 -1
  9. package/packages/agent-runtime/src/types/event.ts +1 -0
  10. package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
  11. package/packages/agent-runtime/src/types/instruction.ts +30 -0
  12. package/packages/agent-runtime/src/types/runtime.ts +7 -0
  13. package/packages/types/src/message/common/metadata.ts +3 -0
  14. package/packages/types/src/message/common/tools.ts +2 -2
  15. package/packages/types/src/tool/search/index.ts +8 -2
  16. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  17. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
  18. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
  19. package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
  20. package/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
  21. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  22. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  23. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  24. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  25. package/src/features/Conversation/Messages/index.tsx +3 -3
  26. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  27. package/src/services/search.ts +2 -2
  28. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  29. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  43. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  44. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  45. package/src/store/chat/selectors.ts +1 -0
  46. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  47. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  49. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  53. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  54. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  55. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  56. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  57. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  58. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  59. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  60. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  61. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  63. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  64. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  65. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  66. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  67. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  68. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  69. package/src/store/chat/slices/message/action.test.ts +134 -16
  70. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  71. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  72. package/src/store/chat/slices/message/initialState.ts +0 -10
  73. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  74. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  75. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  76. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  77. package/src/store/chat/slices/operation/actions.ts +218 -11
  78. package/src/store/chat/slices/operation/selectors.ts +135 -6
  79. package/src/store/chat/slices/operation/types.ts +29 -3
  80. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  81. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  82. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  83. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  84. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  85. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  86. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  87. package/src/store/chat/slices/translate/action.ts +54 -41
  88. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  89. package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
@@ -3,6 +3,7 @@ import {
3
3
  CodeInterpreterParams,
4
4
  CodeInterpreterResponse,
5
5
  } from '@lobechat/types';
6
+ import debug from 'debug';
6
7
  import { produce } from 'immer';
7
8
  import pMap from 'p-map';
8
9
  import { SWRResponse } from 'swr';
@@ -18,12 +19,12 @@ import { CodeInterpreterIdentifier } from '@/tools/code-interpreter';
18
19
  import { setNamespace } from '@/utils/storeDebug';
19
20
 
20
21
  const n = setNamespace('codeInterpreter');
22
+ const log = debug('lobe-store:builtin-tool');
21
23
 
22
24
  const SWR_FETCH_INTERPRETER_FILE_KEY = 'FetchCodeInterpreterFileItem';
23
25
 
24
26
  export interface ChatCodeInterpreterAction {
25
27
  python: (id: string, params: CodeInterpreterParams) => Promise<boolean | undefined>;
26
- toggleInterpreterExecuting: (id: string, loading: boolean) => void;
27
28
  updateInterpreterFileItem: (
28
29
  id: string,
29
30
  updater: (data: CodeInterpreterResponse) => void,
@@ -39,66 +40,100 @@ export const codeInterpreterSlice: StateCreator<
39
40
  ChatCodeInterpreterAction
40
41
  > = (set, get) => ({
41
42
  python: async (id: string, params: CodeInterpreterParams) => {
42
- const {
43
- toggleInterpreterExecuting,
44
- optimisticUpdatePluginState,
45
- optimisticUpdateMessageContent,
46
- uploadInterpreterFiles,
47
- } = get();
48
-
49
- toggleInterpreterExecuting(id, true);
50
-
51
- // TODO: 应该只下载 AI 用到的文件
52
- const files: File[] = [];
53
- for (const message of dbMessageSelectors.dbUserMessages(get())) {
54
- for (const file of message.fileList ?? []) {
55
- const blob = await fetch(file.url).then((res) => res.blob());
56
- files.push(new File([blob], file.name));
57
- }
58
- for (const image of message.imageList ?? []) {
59
- const blob = await fetch(image.url).then((res) => res.blob());
60
- files.push(new File([blob], image.alt));
61
- }
62
- for (const tool of message.tools ?? []) {
63
- if (tool.identifier === CodeInterpreterIdentifier) {
64
- const message = dbMessageSelectors.getDbMessageByToolCallId(tool.id)(get());
65
- if (message?.content) {
66
- const content = JSON.parse(message.content) as CodeInterpreterResponse;
67
- for (const file of content.files ?? []) {
68
- const item = await fileService.getFile(file.fileId!);
69
- const blob = await fetch(item.url).then((res) => res.blob());
70
- files.push(new File([blob], file.filename));
43
+ // Get parent operationId from messageOperationMap (should be executeToolCall)
44
+ const parentOperationId = get().messageOperationMap[id];
45
+
46
+ // Create child operation for interpreter execution
47
+ // Auto-associates message with this operation via messageId in context
48
+ const { operationId: interpreterOpId, abortController } = get().startOperation({
49
+ context: {
50
+ messageId: id,
51
+ },
52
+ metadata: {
53
+ startTime: Date.now(),
54
+ },
55
+ parentOperationId,
56
+ type: 'builtinToolInterpreter',
57
+ });
58
+
59
+ log(
60
+ '[python] messageId=%s, parentOpId=%s, interpreterOpId=%s, aborted=%s',
61
+ id,
62
+ parentOperationId,
63
+ interpreterOpId,
64
+ abortController.signal.aborted,
65
+ );
66
+
67
+ const context = { operationId: interpreterOpId };
68
+
69
+ try {
70
+ // TODO: 应该只下载 AI 用到的文件
71
+ const files: File[] = [];
72
+ for (const message of dbMessageSelectors.dbUserMessages(get())) {
73
+ for (const file of message.fileList ?? []) {
74
+ const blob = await fetch(file.url).then((res) => res.blob());
75
+ files.push(new File([blob], file.name));
76
+ }
77
+ for (const image of message.imageList ?? []) {
78
+ const blob = await fetch(image.url).then((res) => res.blob());
79
+ files.push(new File([blob], image.alt));
80
+ }
81
+ for (const tool of message.tools ?? []) {
82
+ if (tool.identifier === CodeInterpreterIdentifier) {
83
+ const message = dbMessageSelectors.getDbMessageByToolCallId(tool.id)(get());
84
+ if (message?.content) {
85
+ const content = JSON.parse(message.content) as CodeInterpreterResponse;
86
+ for (const file of content.files ?? []) {
87
+ const item = await fileService.getFile(file.fileId!);
88
+ const blob = await fetch(item.url).then((res) => res.blob());
89
+ files.push(new File([blob], file.filename));
90
+ }
71
91
  }
72
92
  }
73
93
  }
74
94
  }
75
- }
76
95
 
77
- try {
78
96
  const result = await pythonService.runPython(params.code, params.packages, files);
97
+
98
+ // Complete interpreter operation
99
+ get().completeOperation(interpreterOpId);
100
+
79
101
  if (result?.files) {
80
- await optimisticUpdateMessageContent(id, JSON.stringify(result));
81
- await uploadInterpreterFiles(id, result.files);
102
+ await get().optimisticUpdateMessageContent(id, JSON.stringify(result), undefined, context);
103
+ await get().uploadInterpreterFiles(id, result.files);
82
104
  } else {
83
- await optimisticUpdateMessageContent(id, JSON.stringify(result));
105
+ await get().optimisticUpdateMessageContent(id, JSON.stringify(result), undefined, context);
84
106
  }
107
+
108
+ return true;
85
109
  } catch (error) {
86
- optimisticUpdatePluginState(id, { error });
110
+ const err = error as Error;
111
+
112
+ log('[python] Error: messageId=%s, error=%s', id, err.message);
113
+
114
+ // Check if it's an abort error
115
+ if (err.message.includes('The user aborted a request.') || err.name === 'AbortError') {
116
+ log('[python] Request aborted: messageId=%s', id);
117
+ // Fail interpreter operation for abort
118
+ get().failOperation(interpreterOpId, {
119
+ message: 'User cancelled the request',
120
+ type: 'UserAborted',
121
+ });
122
+ // Don't update error message for user aborts
123
+ return;
124
+ }
125
+
126
+ // Fail interpreter operation for other errors
127
+ get().failOperation(interpreterOpId, {
128
+ message: err.message,
129
+ type: 'PluginServerError',
130
+ });
131
+
132
+ // For other errors, update message
133
+ await get().optimisticUpdatePluginState(id, { error }, context);
87
134
  // 如果调用过程中出现了错误,不要触发 AI 消息
88
135
  return;
89
- } finally {
90
- toggleInterpreterExecuting(id, false);
91
136
  }
92
-
93
- return true;
94
- },
95
-
96
- toggleInterpreterExecuting: (id: string, executing: boolean) => {
97
- set(
98
- { codeInterpreterExecuting: { ...get().codeInterpreterExecuting, [id]: executing } },
99
- false,
100
- n('toggleInterpreterExecuting'),
101
- );
102
137
  },
103
138
 
104
139
  updateInterpreterFileItem: async (
@@ -13,11 +13,14 @@ import {
13
13
  RunCommandParams,
14
14
  WriteLocalFileParams,
15
15
  } from '@lobechat/electron-client-ipc';
16
+ import debug from 'debug';
16
17
  import { StateCreator } from 'zustand/vanilla';
17
18
 
18
19
  import { ChatStore } from '@/store/chat/store';
19
20
  import { LocalSystemExecutionRuntime } from '@/tools/local-system/ExecutionRuntime';
20
21
 
22
+ const log = debug('lobe-store:builtin-tool');
23
+
21
24
  /* eslint-disable typescript-sort-keys/interface */
22
25
  export interface LocalFileAction {
23
26
  internal_triggerLocalFileToolCalling: (
@@ -33,7 +36,6 @@ export interface LocalFileAction {
33
36
  renameLocalFile: (id: string, params: RenameLocalFileParams) => Promise<boolean>;
34
37
  reSearchLocalFiles: (id: string, params: LocalSearchFilesParams) => Promise<boolean>;
35
38
  searchLocalFiles: (id: string, params: LocalSearchFilesParams) => Promise<boolean>;
36
- toggleLocalFileLoading: (id: string, loading: boolean) => void;
37
39
  writeLocalFile: (id: string, params: WriteLocalFileParams) => Promise<boolean>;
38
40
 
39
41
  // Shell Commands
@@ -106,8 +108,6 @@ export const localSystemSlice: StateCreator<
106
108
  },
107
109
 
108
110
  reSearchLocalFiles: async (id, params) => {
109
- get().toggleLocalFileLoading(id, true);
110
-
111
111
  await get().optimisticUpdatePluginArguments(id, params);
112
112
 
113
113
  return get().searchLocalFiles(id, params);
@@ -144,44 +144,94 @@ export const localSystemSlice: StateCreator<
144
144
 
145
145
  // ==================== utils ====================
146
146
 
147
- toggleLocalFileLoading: (id, loading) => {
148
- // Assuming a loading state structure similar to searchLoading
149
- set(
150
- (state) => ({
151
- localFileLoading: { ...state.localFileLoading, [id]: loading },
152
- }),
153
- false,
154
- `toggleLocalFileLoading/${loading ? 'start' : 'end'}`,
155
- );
156
- },
157
147
  internal_triggerLocalFileToolCalling: async (id, callingService) => {
158
- get().toggleLocalFileLoading(id, true);
148
+ // Get parent operationId from messageOperationMap (should be executeToolCall)
149
+ const parentOperationId = get().messageOperationMap[id];
150
+
151
+ // Create child operation for local system execution
152
+ // Auto-associates message with this operation via messageId in context
153
+ const { operationId: localSystemOpId, abortController } = get().startOperation({
154
+ type: 'builtinToolLocalSystem',
155
+ context: {
156
+ messageId: id,
157
+ },
158
+ parentOperationId,
159
+ metadata: {
160
+ startTime: Date.now(),
161
+ },
162
+ });
163
+
164
+ log(
165
+ '[localSystem] messageId=%s, parentOpId=%s, localSystemOpId=%s, aborted=%s',
166
+ id,
167
+ parentOperationId,
168
+ localSystemOpId,
169
+ abortController.signal.aborted,
170
+ );
171
+
172
+ const context = { operationId: localSystemOpId };
173
+
159
174
  try {
160
175
  const { state, content, success, error } = await callingService();
161
176
 
177
+ // Complete local system operation
178
+ get().completeOperation(localSystemOpId);
179
+
162
180
  if (success) {
163
181
  if (state) {
164
- await get().optimisticUpdatePluginState(id, state);
182
+ await get().optimisticUpdatePluginState(id, state, context);
165
183
  }
166
- await get().optimisticUpdateMessageContent(id, content);
184
+ await get().optimisticUpdateMessageContent(id, content, undefined, context);
167
185
  } else {
168
- await get().optimisticUpdateMessagePluginError(id, {
169
- body: error,
170
- message: error?.message || 'Operation failed',
171
- type: 'PluginServerError',
172
- });
186
+ await get().optimisticUpdateMessagePluginError(
187
+ id,
188
+ {
189
+ body: error,
190
+ message: error?.message || 'Operation failed',
191
+ type: 'PluginServerError',
192
+ },
193
+ context,
194
+ );
173
195
  // Still update content even if failed, to show error message
174
- await get().optimisticUpdateMessageContent(id, content);
196
+ await get().optimisticUpdateMessageContent(id, content, undefined, context);
175
197
  }
198
+
199
+ return true;
176
200
  } catch (error) {
177
- await get().optimisticUpdateMessagePluginError(id, {
178
- body: error,
179
- message: (error as Error).message,
201
+ const err = error as Error;
202
+
203
+ log('[localSystem] Error: messageId=%s, error=%s', id, err.message);
204
+
205
+ // Check if it's an abort error
206
+ if (err.message.includes('The user aborted a request.') || err.name === 'AbortError') {
207
+ log('[localSystem] Request aborted: messageId=%s', id);
208
+ // Fail local system operation for abort
209
+ get().failOperation(localSystemOpId, {
210
+ message: 'User cancelled the request',
211
+ type: 'UserAborted',
212
+ });
213
+ // Don't update error message for user aborts
214
+ return false;
215
+ }
216
+
217
+ // Fail local system operation for other errors
218
+ get().failOperation(localSystemOpId, {
219
+ message: err.message,
180
220
  type: 'PluginServerError',
181
221
  });
182
- }
183
- get().toggleLocalFileLoading(id, false);
184
222
 
185
- return true;
223
+ // For other errors, update message
224
+ await get().optimisticUpdateMessagePluginError(
225
+ id,
226
+ {
227
+ body: error,
228
+ message: err.message,
229
+ type: 'PluginServerError',
230
+ },
231
+ context,
232
+ );
233
+
234
+ return false;
235
+ }
186
236
  },
187
237
  });
@@ -1,6 +1,7 @@
1
1
  import { crawlResultsPrompt } from '@lobechat/prompts';
2
2
  import { CreateMessageParams, SEARCH_SEARXNG_NOT_CONFIG, SearchQuery } from '@lobechat/types';
3
3
  import { nanoid } from '@lobechat/utils';
4
+ import debug from 'debug';
4
5
  import { StateCreator } from 'zustand/vanilla';
5
6
 
6
7
  import { searchService } from '@/services/search';
@@ -8,6 +9,8 @@ import { dbMessageSelectors } from '@/store/chat/selectors';
8
9
  import { ChatStore } from '@/store/chat/store';
9
10
  import { WebBrowsingExecutionRuntime } from '@/tools/web-browsing/ExecutionRuntime';
10
11
 
12
+ const log = debug('lobe-store:builtin-tool');
13
+
11
14
  export interface SearchAction {
12
15
  crawlMultiPages: (
13
16
  id: string,
@@ -22,7 +25,6 @@ export interface SearchAction {
22
25
  saveSearchResult: (id: string) => Promise<void>;
23
26
  search: (id: string, data: SearchQuery, aiSummary?: boolean) => Promise<void | boolean>;
24
27
  togglePageContent: (url: string) => void;
25
- toggleSearchLoading: (id: string, loading: boolean) => void;
26
28
  /**
27
29
  * 重新发起搜索
28
30
  * @description 会更新插件的 arguments 参数,然后再次搜索
@@ -43,36 +45,79 @@ export const searchSlice: StateCreator<
43
45
  SearchAction
44
46
  > = (set, get) => ({
45
47
  crawlMultiPages: async (id, params, aiSummary = true) => {
46
- const { optimisticUpdateMessageContent } = get();
47
- get().toggleSearchLoading(id, true);
48
+ // Get parent operationId from messageOperationMap (should be executeToolCall)
49
+ const parentOperationId = get().messageOperationMap[id];
50
+
51
+ // Create child operation for crawl execution
52
+ // Auto-associates message with this operation via messageId in context
53
+ const { operationId: crawlOpId, abortController } = get().startOperation({
54
+ context: {
55
+ messageId: id,
56
+ },
57
+ metadata: {
58
+ startTime: Date.now(),
59
+ urls: params.urls,
60
+ },
61
+ parentOperationId,
62
+ type: 'builtinToolSearch',
63
+ });
64
+
65
+ log(
66
+ '[crawlMultiPages] messageId=%s, parentOpId=%s, crawlOpId=%s, urls=%o, aborted=%s',
67
+ id,
68
+ parentOperationId,
69
+ crawlOpId,
70
+ params.urls,
71
+ abortController.signal.aborted,
72
+ );
48
73
 
49
- // Get message to extract sessionId/topicId
50
- const message = dbMessageSelectors.getDbMessageById(id)(get());
51
- const context = { sessionId: message?.sessionId, topicId: message?.topicId };
74
+ const context = { operationId: crawlOpId };
52
75
 
53
76
  try {
54
77
  const { content, success, error, state } = await runtime.crawlMultiPages(params);
55
78
 
56
- await optimisticUpdateMessageContent(id, content, undefined, context);
79
+ // Complete crawl operation
80
+ get().completeOperation(crawlOpId);
81
+
82
+ await get().optimisticUpdateMessageContent(id, content, undefined, context);
57
83
 
58
84
  if (success) {
59
85
  await get().optimisticUpdatePluginState(id, state, context);
60
86
  } else {
61
87
  await get().optimisticUpdatePluginError(id, error, context);
62
88
  }
63
- get().toggleSearchLoading(id, false);
64
-
65
- // Convert to XML format to save tokens
66
89
 
67
90
  // if aiSummary is true, then trigger ai message
68
91
  return aiSummary;
69
92
  } catch (e) {
70
93
  const err = e as Error;
94
+
95
+ log('[crawlMultiPages] Error: messageId=%s, error=%s', id, err.message);
96
+
97
+ // Check if it's an abort error
98
+ if (err.message.includes('The user aborted a request.') || err.name === 'AbortError') {
99
+ log('[crawlMultiPages] Request aborted: messageId=%s', id);
100
+ // Fail crawl operation for abort
101
+ get().failOperation(crawlOpId, {
102
+ message: 'User cancelled the request',
103
+ type: 'UserAborted',
104
+ });
105
+ // Don't update error message for user aborts
106
+ return;
107
+ }
108
+
109
+ // Fail crawl operation for other errors
110
+ get().failOperation(crawlOpId, {
111
+ message: err.message,
112
+ type: 'PluginServerError',
113
+ });
114
+
115
+ // For other errors, update message
71
116
  console.error(e);
72
117
  const content = [{ errorMessage: err.message, errorType: err.name }];
73
118
 
74
119
  const xmlContent = crawlResultsPrompt(content);
75
- await optimisticUpdateMessageContent(id, xmlContent, undefined, context);
120
+ await get().optimisticUpdateMessageContent(id, xmlContent, undefined, context);
76
121
  }
77
122
  },
78
123
 
@@ -88,7 +133,9 @@ export const searchSlice: StateCreator<
88
133
 
89
134
  const { optimisticAddToolToAssistantMessage, optimisticCreateMessage, openToolUI } = get();
90
135
 
91
- const context = { sessionId: message.sessionId, topicId: message.topicId };
136
+ // Get operationId from messageOperationMap
137
+ const operationId = get().messageOperationMap[id];
138
+ const context = operationId ? { operationId } : undefined;
92
139
 
93
140
  // 1. 创建一个新的 tool call message
94
141
  const newToolCallId = `tool_call_${nanoid()}`;
@@ -120,10 +167,7 @@ export const searchSlice: StateCreator<
120
167
 
121
168
  const [result] = await Promise.all([
122
169
  // 1. 添加 tool message
123
- optimisticCreateMessage(toolMessage, {
124
- sessionId: toolMessage.sessionId,
125
- topicId: toolMessage.topicId,
126
- }),
170
+ optimisticCreateMessage(toolMessage, context),
127
171
  // 2. 将这条 tool call message 插入到 ai 消息的 tools 中
128
172
  addToolItem(),
129
173
  ]);
@@ -134,61 +178,104 @@ export const searchSlice: StateCreator<
134
178
  },
135
179
 
136
180
  search: async (id, params, aiSummary = true) => {
137
- get().toggleSearchLoading(id, true);
181
+ // Get parent operationId from messageOperationMap (should be executeToolCall)
182
+ const parentOperationId = get().messageOperationMap[id];
183
+
184
+ // Create child operation for search execution
185
+ // Auto-associates message with this operation via messageId in context
186
+ const { operationId: searchOpId, abortController } = get().startOperation({
187
+ context: {
188
+ messageId: id,
189
+ },
190
+ metadata: {
191
+ query: params.query,
192
+ startTime: Date.now(),
193
+ },
194
+ parentOperationId,
195
+ type: 'builtinToolSearch',
196
+ });
197
+
198
+ log(
199
+ '[search] messageId=%s, parentOpId=%s, searchOpId=%s, aborted=%s',
200
+ id,
201
+ parentOperationId,
202
+ searchOpId,
203
+ abortController.signal.aborted,
204
+ );
138
205
 
139
- // Get message to extract sessionId/topicId
140
- const message = dbMessageSelectors.getDbMessageById(id)(get());
141
- const context = { sessionId: message?.sessionId, topicId: message?.topicId };
142
-
143
- const { content, success, error, state } = await runtime.search(params);
144
-
145
- if (success) {
146
- await get().optimisticUpdatePluginState(id, state, context);
147
- } else {
148
- if ((error as Error).message === SEARCH_SEARXNG_NOT_CONFIG) {
149
- await get().optimisticUpdateMessagePluginError(
150
- id,
151
- {
152
- body: { provider: 'searxng' },
153
- message: 'SearXNG is not configured',
154
- type: 'PluginSettingsInvalid',
155
- },
156
- context,
157
- );
206
+ const context = { operationId: searchOpId };
207
+
208
+ try {
209
+ const { content, success, error, state } = await runtime.search(params, {
210
+ signal: abortController.signal,
211
+ });
212
+
213
+ // Complete search operation
214
+ get().completeOperation(searchOpId);
215
+
216
+ if (success) {
217
+ await get().optimisticUpdatePluginState(id, state, context);
158
218
  } else {
159
- await get().optimisticUpdateMessagePluginError(
160
- id,
161
- {
162
- body: error,
163
- message: (error as Error).message,
164
- type: 'PluginServerError',
165
- },
166
- context,
167
- );
219
+ if ((error as Error).message === SEARCH_SEARXNG_NOT_CONFIG) {
220
+ await get().optimisticUpdateMessagePluginError(
221
+ id,
222
+ {
223
+ body: { provider: 'searxng' },
224
+ message: 'SearXNG is not configured',
225
+ type: 'PluginSettingsInvalid',
226
+ },
227
+ context,
228
+ );
229
+ } else {
230
+ await get().optimisticUpdateMessagePluginError(
231
+ id,
232
+ {
233
+ body: error,
234
+ message: (error as Error).message,
235
+ type: 'PluginServerError',
236
+ },
237
+ context,
238
+ );
239
+ }
168
240
  }
169
- }
170
241
 
171
- get().toggleSearchLoading(id, false);
242
+ await get().optimisticUpdateMessageContent(id, content, undefined, context);
243
+
244
+ // 如果 aiSummary 为 true,则会自动触发总结
245
+ return aiSummary;
246
+ } catch (error) {
247
+ const err = error as Error;
248
+
249
+ log('[search] Error: messageId=%s, error=%s', id, err.message);
250
+
251
+ // Check if it's an abort error
252
+ if (err.message.includes('The user aborted a request.') || err.name === 'AbortError') {
253
+ log('[search] Request aborted: messageId=%s', id);
254
+ // Fail search operation for abort
255
+ get().failOperation(searchOpId, {
256
+ message: 'User cancelled the request',
257
+ type: 'UserAborted',
258
+ });
259
+ // Don't update error message for user aborts
260
+ return;
261
+ }
172
262
 
173
- await get().optimisticUpdateMessageContent(id, content, undefined, context);
263
+ // Fail search operation for other errors
264
+ get().failOperation(searchOpId, { message: err.message, type: 'PluginServerError' });
174
265
 
175
- // 如果 aiSummary true,则会自动触发总结
176
- return aiSummary;
266
+ // For other errors, update message
267
+ await get().optimisticUpdateMessagePluginError(
268
+ id,
269
+ { body: error, message: err.message, type: 'PluginServerError' },
270
+ context,
271
+ );
272
+ }
177
273
  },
178
274
  togglePageContent: (url) => {
179
275
  set({ activePageContentUrl: url });
180
276
  },
181
277
 
182
- toggleSearchLoading: (id, loading) => {
183
- set(
184
- { searchLoading: { ...get().searchLoading, [id]: loading } },
185
- false,
186
- `toggleSearchLoading/${loading ? 'start' : 'end'}`,
187
- );
188
- },
189
-
190
278
  triggerSearchAgain: async (id, data, options) => {
191
- get().toggleSearchLoading(id, true);
192
279
  await get().optimisticUpdatePluginArguments(id, data);
193
280
 
194
281
  await get().search(id, data, options?.aiSummary);