@lobehub/lobehub 2.0.0-next.78 → 2.0.0-next.79
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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/types/instruction.ts +7 -2
- package/packages/database/src/utils/genWhere.test.ts +243 -0
- package/src/app/[variants]/(main)/discover/(list)/assistant/features/Category/index.tsx +1 -1
- package/src/app/[variants]/(main)/discover/(list)/mcp/features/Category/index.tsx +1 -1
- package/src/app/[variants]/(main)/discover/(list)/model/features/Category/index.tsx +1 -1
- package/src/store/chat/initialState.ts +4 -1
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +353 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +273 -0
- package/src/store/chat/slices/operation/actions.ts +451 -0
- package/src/store/chat/slices/operation/index.ts +4 -0
- package/src/store/chat/slices/operation/initialState.ts +44 -0
- package/src/store/chat/slices/operation/selectors.ts +246 -0
- package/src/store/chat/slices/operation/types.ts +134 -0
- package/src/store/chat/store.ts +4 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
2
|
+
import { nanoid } from '@lobechat/utils';
|
|
3
|
+
import { produce } from 'immer';
|
|
4
|
+
import { StateCreator } from 'zustand/vanilla';
|
|
5
|
+
|
|
6
|
+
import { ChatStore } from '@/store/chat/store';
|
|
7
|
+
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
|
8
|
+
import { setNamespace } from '@/utils/storeDebug';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Operation,
|
|
12
|
+
OperationContext,
|
|
13
|
+
OperationFilter,
|
|
14
|
+
OperationMetadata,
|
|
15
|
+
OperationStatus,
|
|
16
|
+
OperationType,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
const n = setNamespace('operation');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Operation Actions
|
|
23
|
+
*/
|
|
24
|
+
export interface OperationActions {
|
|
25
|
+
/**
|
|
26
|
+
* Associate message with operation (for automatic context retrieval)
|
|
27
|
+
*/
|
|
28
|
+
associateMessageWithOperation: (messageId: string, operationId: string) => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cancel all operations
|
|
32
|
+
*/
|
|
33
|
+
cancelAllOperations: (reason?: string) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cancel operation (recursively cancel all child operations)
|
|
37
|
+
*/
|
|
38
|
+
cancelOperation: (operationId: string, reason?: string) => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cancel operations by filter
|
|
42
|
+
*/
|
|
43
|
+
cancelOperations: (filter: OperationFilter, reason?: string) => string[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cleanup completed operations (prevent memory leak)
|
|
47
|
+
*/
|
|
48
|
+
cleanupCompletedOperations: (olderThan?: number) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Complete operation
|
|
52
|
+
*/
|
|
53
|
+
completeOperation: (operationId: string, metadata?: Partial<OperationMetadata>) => void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mark operation as failed
|
|
57
|
+
*/
|
|
58
|
+
failOperation: (
|
|
59
|
+
operationId: string,
|
|
60
|
+
error: { code?: string; details?: any; message: string; type: string },
|
|
61
|
+
) => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Start an operation (supports auto-inheriting context from parent operation)
|
|
65
|
+
*/
|
|
66
|
+
startOperation: (params: {
|
|
67
|
+
context?: Partial<OperationContext>;
|
|
68
|
+
description?: string;
|
|
69
|
+
label?: string;
|
|
70
|
+
metadata?: Partial<OperationMetadata>;
|
|
71
|
+
parentOperationId?: string;
|
|
72
|
+
type: OperationType;
|
|
73
|
+
}) => { abortController: AbortController; operationId: string };
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update operation progress
|
|
77
|
+
*/
|
|
78
|
+
updateOperationProgress: (operationId: string, current: number, total?: number) => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update operation status
|
|
82
|
+
*/
|
|
83
|
+
updateOperationStatus: (
|
|
84
|
+
operationId: string,
|
|
85
|
+
status: OperationStatus,
|
|
86
|
+
metadata?: Partial<OperationMetadata>,
|
|
87
|
+
) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const operationActions: StateCreator<
|
|
91
|
+
ChatStore,
|
|
92
|
+
[['zustand/devtools', never]],
|
|
93
|
+
[],
|
|
94
|
+
OperationActions
|
|
95
|
+
> = (set, get) => ({
|
|
96
|
+
startOperation: (params) => {
|
|
97
|
+
const {
|
|
98
|
+
type,
|
|
99
|
+
context: partialContext,
|
|
100
|
+
parentOperationId,
|
|
101
|
+
label,
|
|
102
|
+
description,
|
|
103
|
+
metadata,
|
|
104
|
+
} = params;
|
|
105
|
+
|
|
106
|
+
const operationId = `op_${nanoid()}`;
|
|
107
|
+
|
|
108
|
+
// If parent operation exists and context is not fully provided, inherit from parent
|
|
109
|
+
let context: OperationContext = partialContext || {};
|
|
110
|
+
|
|
111
|
+
if (parentOperationId) {
|
|
112
|
+
const parentOp = get().operations[parentOperationId];
|
|
113
|
+
if (parentOp) {
|
|
114
|
+
// Inherit parent's context, allow partial override
|
|
115
|
+
context = { ...parentOp.context, ...partialContext };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const abortController = new AbortController();
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
|
|
122
|
+
const operation: Operation = {
|
|
123
|
+
id: operationId,
|
|
124
|
+
type,
|
|
125
|
+
status: 'running',
|
|
126
|
+
context,
|
|
127
|
+
abortController,
|
|
128
|
+
metadata: {
|
|
129
|
+
startTime: now,
|
|
130
|
+
...metadata,
|
|
131
|
+
},
|
|
132
|
+
parentOperationId,
|
|
133
|
+
childOperationIds: [],
|
|
134
|
+
label,
|
|
135
|
+
description,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
set(
|
|
139
|
+
produce((state: ChatStore) => {
|
|
140
|
+
// Add to operations map
|
|
141
|
+
state.operations[operationId] = operation;
|
|
142
|
+
|
|
143
|
+
// Update type index
|
|
144
|
+
if (!state.operationsByType[type]) {
|
|
145
|
+
state.operationsByType[type] = [];
|
|
146
|
+
}
|
|
147
|
+
state.operationsByType[type].push(operationId);
|
|
148
|
+
|
|
149
|
+
// Update message index (if messageId exists)
|
|
150
|
+
if (context.messageId) {
|
|
151
|
+
if (!state.operationsByMessage[context.messageId]) {
|
|
152
|
+
state.operationsByMessage[context.messageId] = [];
|
|
153
|
+
}
|
|
154
|
+
state.operationsByMessage[context.messageId].push(operationId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update context index (if sessionId exists)
|
|
158
|
+
if (context.sessionId) {
|
|
159
|
+
const contextKey = messageMapKey(
|
|
160
|
+
context.sessionId,
|
|
161
|
+
context.topicId !== undefined ? context.topicId : null,
|
|
162
|
+
);
|
|
163
|
+
if (!state.operationsByContext[contextKey]) {
|
|
164
|
+
state.operationsByContext[contextKey] = [];
|
|
165
|
+
}
|
|
166
|
+
state.operationsByContext[contextKey].push(operationId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Update parent's childOperationIds
|
|
170
|
+
if (parentOperationId && state.operations[parentOperationId]) {
|
|
171
|
+
if (!state.operations[parentOperationId].childOperationIds) {
|
|
172
|
+
state.operations[parentOperationId].childOperationIds = [];
|
|
173
|
+
}
|
|
174
|
+
state.operations[parentOperationId].childOperationIds!.push(operationId);
|
|
175
|
+
}
|
|
176
|
+
}),
|
|
177
|
+
false,
|
|
178
|
+
n(`startOperation/${type}/${operationId}`),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return { operationId, abortController };
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
updateOperationStatus: (operationId, status, metadata) => {
|
|
185
|
+
set(
|
|
186
|
+
produce((state: ChatStore) => {
|
|
187
|
+
const operation = state.operations[operationId];
|
|
188
|
+
if (!operation) return;
|
|
189
|
+
|
|
190
|
+
operation.status = status;
|
|
191
|
+
|
|
192
|
+
if (metadata) {
|
|
193
|
+
operation.metadata = {
|
|
194
|
+
...operation.metadata,
|
|
195
|
+
...metadata,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
false,
|
|
200
|
+
n(`updateOperationStatus/${operationId}/${status}`),
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
updateOperationProgress: (operationId, current, total) => {
|
|
205
|
+
set(
|
|
206
|
+
produce((state: ChatStore) => {
|
|
207
|
+
const operation = state.operations[operationId];
|
|
208
|
+
if (!operation) return;
|
|
209
|
+
|
|
210
|
+
operation.metadata.progress = {
|
|
211
|
+
current,
|
|
212
|
+
total: total ?? operation.metadata.progress?.total ?? current,
|
|
213
|
+
percentage: total ? Math.round((current / total) * 100) : undefined,
|
|
214
|
+
};
|
|
215
|
+
}),
|
|
216
|
+
false,
|
|
217
|
+
n(`updateOperationProgress/${operationId}`),
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
completeOperation: (operationId, metadata) => {
|
|
222
|
+
set(
|
|
223
|
+
produce((state: ChatStore) => {
|
|
224
|
+
const operation = state.operations[operationId];
|
|
225
|
+
if (!operation) return;
|
|
226
|
+
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
operation.status = 'completed';
|
|
229
|
+
operation.metadata.endTime = now;
|
|
230
|
+
operation.metadata.duration = now - operation.metadata.startTime;
|
|
231
|
+
|
|
232
|
+
if (metadata) {
|
|
233
|
+
operation.metadata = {
|
|
234
|
+
...operation.metadata,
|
|
235
|
+
...metadata,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}),
|
|
239
|
+
false,
|
|
240
|
+
n(`completeOperation/${operationId}`),
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
cancelOperation: (operationId, reason = 'User cancelled') => {
|
|
245
|
+
const operation = get().operations[operationId];
|
|
246
|
+
if (!operation) return;
|
|
247
|
+
|
|
248
|
+
// Cancel all child operations recursively
|
|
249
|
+
if (operation.childOperationIds && operation.childOperationIds.length > 0) {
|
|
250
|
+
operation.childOperationIds.forEach((childId) => {
|
|
251
|
+
get().cancelOperation(childId, 'Parent operation cancelled');
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Abort the operation
|
|
256
|
+
try {
|
|
257
|
+
operation.abortController.abort(reason);
|
|
258
|
+
} catch {
|
|
259
|
+
// Ignore abort errors
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Update status
|
|
263
|
+
set(
|
|
264
|
+
produce((state: ChatStore) => {
|
|
265
|
+
const op = state.operations[operationId];
|
|
266
|
+
if (!op) return;
|
|
267
|
+
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
op.status = 'cancelled';
|
|
270
|
+
op.metadata.endTime = now;
|
|
271
|
+
op.metadata.duration = now - op.metadata.startTime;
|
|
272
|
+
op.metadata.cancelReason = reason;
|
|
273
|
+
}),
|
|
274
|
+
false,
|
|
275
|
+
n(`cancelOperation/${operationId}`),
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
failOperation: (operationId, error) => {
|
|
280
|
+
set(
|
|
281
|
+
produce((state: ChatStore) => {
|
|
282
|
+
const operation = state.operations[operationId];
|
|
283
|
+
if (!operation) return;
|
|
284
|
+
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
operation.status = 'failed';
|
|
287
|
+
operation.metadata.endTime = now;
|
|
288
|
+
operation.metadata.duration = now - operation.metadata.startTime;
|
|
289
|
+
operation.metadata.error = error;
|
|
290
|
+
}),
|
|
291
|
+
false,
|
|
292
|
+
n(`failOperation/${operationId}`),
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
cancelOperations: (filter, reason = 'Batch cancelled') => {
|
|
297
|
+
const operations = Object.values(get().operations);
|
|
298
|
+
const matchedIds: string[] = [];
|
|
299
|
+
|
|
300
|
+
operations.forEach((op) => {
|
|
301
|
+
if (op.status !== 'running') return;
|
|
302
|
+
|
|
303
|
+
let matches = true;
|
|
304
|
+
|
|
305
|
+
// Type filter
|
|
306
|
+
if (filter.type) {
|
|
307
|
+
const types = Array.isArray(filter.type) ? filter.type : [filter.type];
|
|
308
|
+
matches = matches && types.includes(op.type);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Status filter
|
|
312
|
+
if (filter.status) {
|
|
313
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
314
|
+
matches = matches && statuses.includes(op.status);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Context filters
|
|
318
|
+
if (filter.sessionId !== undefined) {
|
|
319
|
+
matches = matches && op.context.sessionId === filter.sessionId;
|
|
320
|
+
}
|
|
321
|
+
if (filter.topicId !== undefined) {
|
|
322
|
+
matches = matches && op.context.topicId === filter.topicId;
|
|
323
|
+
}
|
|
324
|
+
if (filter.messageId !== undefined) {
|
|
325
|
+
matches = matches && op.context.messageId === filter.messageId;
|
|
326
|
+
}
|
|
327
|
+
if (filter.threadId !== undefined) {
|
|
328
|
+
matches = matches && op.context.threadId === filter.threadId;
|
|
329
|
+
}
|
|
330
|
+
if (filter.groupId !== undefined) {
|
|
331
|
+
matches = matches && op.context.groupId === filter.groupId;
|
|
332
|
+
}
|
|
333
|
+
if (filter.agentId !== undefined) {
|
|
334
|
+
matches = matches && op.context.agentId === filter.agentId;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (matches) {
|
|
338
|
+
matchedIds.push(op.id);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Cancel all matched operations
|
|
343
|
+
matchedIds.forEach((id) => {
|
|
344
|
+
get().cancelOperation(id, reason);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return matchedIds;
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
cancelAllOperations: (reason = 'Cancel all operations') => {
|
|
351
|
+
const operations = Object.values(get().operations);
|
|
352
|
+
|
|
353
|
+
operations.forEach((op) => {
|
|
354
|
+
if (op.status === 'running') {
|
|
355
|
+
get().cancelOperation(op.id, reason);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
cleanupCompletedOperations: (olderThan = 60_000) => {
|
|
361
|
+
// Default: cleanup operations completed more than 1 minute ago
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
|
|
364
|
+
// Collect operations to delete first
|
|
365
|
+
const operationsToDelete: string[] = [];
|
|
366
|
+
Object.values(get().operations).forEach((op) => {
|
|
367
|
+
const isCompleted =
|
|
368
|
+
op.status === 'completed' || op.status === 'cancelled' || op.status === 'failed';
|
|
369
|
+
const isOld = op.metadata.endTime && now - op.metadata.endTime > olderThan;
|
|
370
|
+
|
|
371
|
+
if (isCompleted && isOld) {
|
|
372
|
+
operationsToDelete.push(op.id);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (operationsToDelete.length === 0) return;
|
|
377
|
+
|
|
378
|
+
set(
|
|
379
|
+
produce((state: ChatStore) => {
|
|
380
|
+
// Delete operations and update indexes
|
|
381
|
+
operationsToDelete.forEach((operationId) => {
|
|
382
|
+
const op = state.operations[operationId];
|
|
383
|
+
if (!op) return;
|
|
384
|
+
|
|
385
|
+
// Remove from operations map
|
|
386
|
+
delete state.operations[operationId];
|
|
387
|
+
|
|
388
|
+
// Remove from type index
|
|
389
|
+
const typeIndex = state.operationsByType[op.type];
|
|
390
|
+
if (typeIndex) {
|
|
391
|
+
state.operationsByType[op.type] = typeIndex.filter((id) => id !== operationId);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Remove from message index
|
|
395
|
+
if (op.context.messageId) {
|
|
396
|
+
const msgIndex = state.operationsByMessage[op.context.messageId];
|
|
397
|
+
if (msgIndex) {
|
|
398
|
+
state.operationsByMessage[op.context.messageId] = msgIndex.filter(
|
|
399
|
+
(id) => id !== operationId,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Remove from context index
|
|
405
|
+
if (op.context.sessionId) {
|
|
406
|
+
const contextKey = messageMapKey(
|
|
407
|
+
op.context.sessionId,
|
|
408
|
+
op.context.topicId !== undefined ? op.context.topicId : null,
|
|
409
|
+
);
|
|
410
|
+
const contextIndex = state.operationsByContext[contextKey];
|
|
411
|
+
if (contextIndex) {
|
|
412
|
+
state.operationsByContext[contextKey] = contextIndex.filter(
|
|
413
|
+
(id) => id !== operationId,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Remove from parent's childOperationIds
|
|
419
|
+
if (op.parentOperationId && state.operations[op.parentOperationId]) {
|
|
420
|
+
const parent = state.operations[op.parentOperationId];
|
|
421
|
+
if (parent.childOperationIds) {
|
|
422
|
+
parent.childOperationIds = parent.childOperationIds.filter(
|
|
423
|
+
(id) => id !== operationId,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Remove from messageOperationMap
|
|
429
|
+
const messageEntry = Object.entries(state.messageOperationMap).find(
|
|
430
|
+
([, opId]) => opId === operationId,
|
|
431
|
+
);
|
|
432
|
+
if (messageEntry) {
|
|
433
|
+
delete state.messageOperationMap[messageEntry[0]];
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}),
|
|
437
|
+
false,
|
|
438
|
+
n(`cleanupCompletedOperations/count=${operationsToDelete.length}`),
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
associateMessageWithOperation: (messageId, operationId) => {
|
|
443
|
+
set(
|
|
444
|
+
produce((state: ChatStore) => {
|
|
445
|
+
state.messageOperationMap[messageId] = operationId;
|
|
446
|
+
}),
|
|
447
|
+
false,
|
|
448
|
+
n(`associateMessageWithOperation/${messageId}/${operationId}`),
|
|
449
|
+
);
|
|
450
|
+
},
|
|
451
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Operation, OperationType } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat Operation State
|
|
5
|
+
* Unified state for all async operations
|
|
6
|
+
*/
|
|
7
|
+
export interface ChatOperationState {
|
|
8
|
+
/**
|
|
9
|
+
* Message to operation mapping (for automatic context retrieval)
|
|
10
|
+
* key: messageId, value: operationId
|
|
11
|
+
*/
|
|
12
|
+
messageOperationMap: Record<string, string>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* All operations map, key is operationId
|
|
16
|
+
*/
|
|
17
|
+
operations: Record<string, Operation>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Operations indexed by session/topic
|
|
21
|
+
* key: messageMapKey(sessionId, topicId), value: operationId[]
|
|
22
|
+
*/
|
|
23
|
+
operationsByContext: Record<string, string[]>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Operations indexed by message
|
|
27
|
+
* key: messageId, value: operationId[]
|
|
28
|
+
*/
|
|
29
|
+
operationsByMessage: Record<string, string[]>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Operations indexed by type (for fast querying)
|
|
33
|
+
* key: OperationType, value: operationId[]
|
|
34
|
+
*/
|
|
35
|
+
operationsByType: Record<OperationType, string[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const initialOperationState: ChatOperationState = {
|
|
39
|
+
messageOperationMap: {},
|
|
40
|
+
operations: {},
|
|
41
|
+
operationsByContext: {},
|
|
42
|
+
operationsByMessage: {},
|
|
43
|
+
operationsByType: {} as Record<OperationType, string[]>,
|
|
44
|
+
};
|