@lobehub/lobehub 2.0.0-next.320 → 2.0.0-next.322

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 (109) hide show
  1. package/.codex/skills/vercel-react-best-practices/AGENTS.md +2410 -0
  2. package/.codex/skills/vercel-react-best-practices/SKILL.md +125 -0
  3. package/.codex/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  4. package/.codex/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
  5. package/.codex/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  6. package/.codex/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  7. package/.codex/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
  8. package/.codex/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  9. package/.codex/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  10. package/.codex/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  11. package/.codex/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  12. package/.codex/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  13. package/.codex/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  14. package/.codex/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  15. package/.codex/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  16. package/.codex/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  17. package/.codex/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  18. package/.codex/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  19. package/.codex/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +57 -0
  20. package/.codex/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  21. package/.codex/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  22. package/.codex/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  23. package/.codex/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  24. package/.codex/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  25. package/.codex/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  26. package/.codex/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  27. package/.codex/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  28. package/.codex/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  29. package/.codex/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  30. package/.codex/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  31. package/.codex/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  32. package/.codex/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  33. package/.codex/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  34. package/.codex/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  35. package/.codex/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  36. package/.codex/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  37. package/.codex/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  38. package/.codex/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  39. package/.codex/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  40. package/.codex/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  41. package/.codex/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  42. package/.codex/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  43. package/.codex/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  44. package/.codex/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  45. package/.codex/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  46. package/.codex/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  47. package/.codex/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  48. package/.codex/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  49. package/.codex/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  50. package/.cursor/skills/vercel-react-best-practices/AGENTS.md +2410 -0
  51. package/.cursor/skills/vercel-react-best-practices/SKILL.md +125 -0
  52. package/.cursor/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  53. package/.cursor/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
  54. package/.cursor/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  55. package/.cursor/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  56. package/.cursor/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
  57. package/.cursor/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  58. package/.cursor/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  59. package/.cursor/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  60. package/.cursor/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  61. package/.cursor/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  62. package/.cursor/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  63. package/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  64. package/.cursor/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  65. package/.cursor/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  66. package/.cursor/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  67. package/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  68. package/.cursor/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +57 -0
  69. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  70. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  71. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  72. package/.cursor/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  73. package/.cursor/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  74. package/.cursor/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  75. package/.cursor/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  76. package/.cursor/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  77. package/.cursor/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  78. package/.cursor/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  79. package/.cursor/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  80. package/.cursor/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  81. package/.cursor/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  82. package/.cursor/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  83. package/.cursor/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  84. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  85. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  86. package/.cursor/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  87. package/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  88. package/.cursor/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  89. package/.cursor/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  90. package/.cursor/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  91. package/.cursor/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  92. package/.cursor/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  93. package/.cursor/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  94. package/.cursor/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  95. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  96. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  97. package/.cursor/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  98. package/.cursor/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  99. package/CHANGELOG.md +50 -0
  100. package/changelog/v1.json +10 -0
  101. package/package.json +1 -1
  102. package/src/app/[variants]/(main)/agent/profile/index.tsx +15 -2
  103. package/src/features/PageEditor/PageEditor.tsx +20 -8
  104. package/src/layout/GlobalProvider/FaviconProvider.tsx +45 -21
  105. package/src/server/globalConfig/parseMemoryExtractionConfig.ts +43 -4
  106. package/src/server/services/memory/userMemory/__tests__/extract.payload.test.ts +101 -0
  107. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +121 -0
  108. package/src/server/services/memory/userMemory/extract.ts +164 -17
  109. package/src/utils/styles.ts +10 -0
@@ -15,6 +15,7 @@ import { builtinAgentSelectors } from '@/store/agent/selectors';
15
15
  import { useDocumentStore } from '@/store/document';
16
16
  import { editorSelectors } from '@/store/document/slices/editor';
17
17
  import { usePageStore } from '@/store/page';
18
+ import { StyleSheet } from '@/utils/styles';
18
19
 
19
20
  import Copilot from './Copilot';
20
21
  import EditorCanvas from './EditorCanvas';
@@ -25,6 +26,22 @@ import PageTitle from './PageTitle';
25
26
  import TitleSection from './TitleSection';
26
27
  import { usePageEditorStore } from './store';
27
28
 
29
+ const styles = StyleSheet.create({
30
+ contentWrapper: {
31
+ display: 'flex',
32
+ overflowY: 'auto',
33
+ position: 'relative',
34
+ },
35
+ editorContainer: {
36
+ minWidth: 0,
37
+ position: 'relative',
38
+ },
39
+ editorContent: {
40
+ overflowY: 'auto',
41
+ position: 'relative',
42
+ },
43
+ });
44
+
28
45
  interface PageEditorProps {
29
46
  emoji?: string;
30
47
  knowledgeBaseId?: string;
@@ -77,16 +94,11 @@ const PageEditorCanvas = memo(() => {
77
94
  style={{ backgroundColor: cssVar.colorBgContainer }}
78
95
  width={'100%'}
79
96
  >
80
- <Flexbox flex={1} height={'100%'} style={{ position: 'relative' }}>
97
+ <Flexbox flex={1} height={'100%'} style={styles.editorContainer}>
81
98
  <Header />
82
- <Flexbox
83
- height={'100%'}
84
- horizontal
85
- style={{ display: 'flex', overflowY: 'auto', position: 'relative' }}
86
- width={'100%'}
87
- >
99
+ <Flexbox height={'100%'} horizontal style={styles.contentWrapper} width={'100%'}>
88
100
  <WideScreenContainer onClick={() => editor?.focus()} wrapperStyle={{ cursor: 'text' }}>
89
- <Flexbox flex={1} style={{ overflowY: 'auto', position: 'relative' }}>
101
+ <Flexbox flex={1} style={styles.editorContent}>
90
102
  <TitleSection />
91
103
  <EditorCanvas />
92
104
  </Flexbox>
@@ -1,22 +1,34 @@
1
1
  'use client';
2
2
 
3
- import { type ReactNode, createContext, memo, useCallback, useContext, useState } from 'react';
3
+ import { type ReactNode, createContext, memo, useCallback, useContext, useMemo, useState } from 'react';
4
4
 
5
5
  export type FaviconState = 'default' | 'done' | 'error' | 'progress';
6
6
 
7
- interface FaviconContextValue {
7
+ interface FaviconStateContextValue {
8
8
  currentState: FaviconState;
9
9
  isDevMode: boolean;
10
+ }
11
+
12
+ interface FaviconSettersContextValue {
10
13
  setFavicon: (state: FaviconState) => void;
11
14
  setIsDevMode: (isDev: boolean) => void;
12
15
  }
13
16
 
14
- const FaviconContext = createContext<FaviconContextValue | null>(null);
17
+ const FaviconStateContext = createContext<FaviconStateContextValue | null>(null);
18
+ const FaviconSettersContext = createContext<FaviconSettersContextValue | null>(null);
19
+
20
+ export const useFaviconState = () => {
21
+ const context = useContext(FaviconStateContext);
22
+ if (!context) {
23
+ throw new Error('useFaviconState must be used within FaviconProvider');
24
+ }
25
+ return context;
26
+ };
15
27
 
16
- export const useFavicon = () => {
17
- const context = useContext(FaviconContext);
28
+ export const useFaviconSetters = () => {
29
+ const context = useContext(FaviconSettersContext);
18
30
  if (!context) {
19
- throw new Error('useFavicon must be used within FaviconProvider');
31
+ throw new Error('useFaviconSetters must be used within FaviconProvider');
20
32
  }
21
33
  return context;
22
34
  };
@@ -66,26 +78,38 @@ export const FaviconProvider = memo<{ children: ReactNode }>(({ children }) => {
66
78
  const [currentState, setCurrentState] = useState<FaviconState>('default');
67
79
  const [isDevMode, setIsDevModeState] = useState<boolean>(defaultIsDev);
68
80
 
69
- const setFavicon = useCallback(
70
- (state: FaviconState) => {
71
- setCurrentState(state);
72
- updateFaviconDOM(state, isDevMode);
73
- },
74
- [isDevMode],
81
+ const setFavicon = useCallback((state: FaviconState) => {
82
+ setCurrentState(state);
83
+ setIsDevModeState((isDev) => {
84
+ updateFaviconDOM(state, isDev);
85
+ return isDev;
86
+ });
87
+ }, []);
88
+
89
+ const setIsDevMode = useCallback((isDev: boolean) => {
90
+ setIsDevModeState(isDev);
91
+ setCurrentState((state) => {
92
+ updateFaviconDOM(state, isDev);
93
+ return state;
94
+ });
95
+ }, []);
96
+
97
+ const stateValue = useMemo(
98
+ () => ({ currentState, isDevMode }),
99
+ [currentState, isDevMode],
75
100
  );
76
101
 
77
- const setIsDevMode = useCallback(
78
- (isDev: boolean) => {
79
- setIsDevModeState(isDev);
80
- updateFaviconDOM(currentState, isDev);
81
- },
82
- [currentState],
102
+ const settersValue = useMemo(
103
+ () => ({ setFavicon, setIsDevMode }),
104
+ [setFavicon, setIsDevMode],
83
105
  );
84
106
 
85
107
  return (
86
- <FaviconContext.Provider value={{ currentState, isDevMode, setFavicon, setIsDevMode }}>
87
- {children}
88
- </FaviconContext.Provider>
108
+ <FaviconStateContext.Provider value={stateValue}>
109
+ <FaviconSettersContext.Provider value={settersValue}>
110
+ {children}
111
+ </FaviconSettersContext.Provider>
112
+ </FaviconStateContext.Provider>
89
113
  );
90
114
  });
91
115
 
@@ -32,12 +32,18 @@ export type MemoryLayerExtractorConfig = MemoryLayerExtractorPublicConfig &
32
32
 
33
33
  export interface MemoryExtractionPrivateConfig {
34
34
  agentGateKeeper: MemoryAgentConfig;
35
+ agentGateKeeperPreferredModels?: string[];
36
+ agentGateKeeperPreferredProviders?: string[];
35
37
  agentLayerExtractor: MemoryLayerExtractorConfig;
38
+ agentLayerExtractorPreferredModels?: string[];
39
+ agentLayerExtractorPreferredProviders?: string[];
36
40
  concurrency?: number;
37
41
  embedding: MemoryAgentConfig;
42
+ embeddingPreferredModels?: string[];
43
+ embeddingPreferredProviders?: string[];
38
44
  featureFlags: {
39
45
  enableBenchmarkLoCoMo: boolean;
40
- },
46
+ };
41
47
  observabilityS3?: {
42
48
  accessKeyId?: string;
43
49
  bucketName?: string;
@@ -157,6 +163,12 @@ const sanitizeAgent = (agent?: MemoryAgentConfig): MemoryAgentPublicConfig | und
157
163
  return sanitized as MemoryAgentPublicConfig;
158
164
  };
159
165
 
166
+ const parsePreferredList = (value?: string) =>
167
+ value
168
+ ?.split(',')
169
+ .map((item) => item.trim().toLowerCase())
170
+ .filter(Boolean);
171
+
160
172
  export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig => {
161
173
  const agentGateKeeper = parseGateKeeperAgent();
162
174
  const agentLayerExtractor = parseLayerExtractorAgent(agentGateKeeper.model);
@@ -167,8 +179,8 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
167
179
  );
168
180
  const extractorObservabilityS3 = parseExtractorAgentObservabilityS3();
169
181
  const featureFlags = {
170
- enableBenchmarkLoCoMo: process.env.MEMORY_USER_MEMORY_FEATURE_FLAG_BENCHMARK_LOCOMO === 'true'
171
- }
182
+ enableBenchmarkLoCoMo: process.env.MEMORY_USER_MEMORY_FEATURE_FLAG_BENCHMARK_LOCOMO === 'true',
183
+ };
172
184
  const concurrencyRaw = process.env.MEMORY_USER_MEMORY_CONCURRENCY;
173
185
  const concurrency =
174
186
  concurrencyRaw !== undefined
@@ -191,7 +203,9 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
191
203
  return acc;
192
204
  }, {});
193
205
 
194
- const upstashWorkflowExtraHeaders = process.env.MEMORY_USER_MEMORY_WORKFLOW_EXTRA_HEADERS?.split(',')
206
+ const upstashWorkflowExtraHeaders = process.env.MEMORY_USER_MEMORY_WORKFLOW_EXTRA_HEADERS?.split(
207
+ ',',
208
+ )
195
209
  .filter(Boolean)
196
210
  .reduce<Record<string, string>>((acc, pair) => {
197
211
  const [key, value] = pair.split('=').map((s) => s.trim());
@@ -201,11 +215,36 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
201
215
  return acc;
202
216
  }, {});
203
217
 
218
+ const agentGateKeeperPreferredProviders = parsePreferredList(
219
+ process.env.MEMORY_USER_MEMORY_GATEKEEPER_PREFERRED_PROVIDERS,
220
+ );
221
+ const agentGateKeeperPreferredModels = parsePreferredList(
222
+ process.env.MEMORY_USER_MEMORY_GATEKEEPER_PREFERRED_MODELS,
223
+ );
224
+ const embeddingPreferredProviders = parsePreferredList(
225
+ process.env.MEMORY_USER_MEMORY_EMBEDDING_PREFERRED_PROVIDERS,
226
+ );
227
+ const embeddingPreferredModels = parsePreferredList(
228
+ process.env.MEMORY_USER_MEMORY_EMBEDDING_PREFERRED_MODELS,
229
+ );
230
+ const agentLayerExtractorPreferredProviders = parsePreferredList(
231
+ process.env.MEMORY_USER_MEMORY_LAYER_EXTRACTOR_PREFERRED_PROVIDERS,
232
+ );
233
+ const agentLayerExtractorPreferredModels = parsePreferredList(
234
+ process.env.MEMORY_USER_MEMORY_LAYER_EXTRACTOR_PREFERRED_MODELS,
235
+ );
236
+
204
237
  return {
205
238
  agentGateKeeper,
239
+ agentGateKeeperPreferredModels,
240
+ agentGateKeeperPreferredProviders,
206
241
  agentLayerExtractor,
242
+ agentLayerExtractorPreferredModels,
243
+ agentLayerExtractorPreferredProviders,
207
244
  concurrency,
208
245
  embedding,
246
+ embeddingPreferredModels,
247
+ embeddingPreferredProviders,
209
248
  featureFlags,
210
249
  observabilityS3: extractorObservabilityS3,
211
250
  upstashWorkflowExtraHeaders,
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { LayersEnum, MemorySourceType } from '@/types/userMemory';
4
+
5
+ import {
6
+ type MemoryExtractionNormalizedPayload,
7
+ type MemoryExtractionPayloadInput,
8
+ buildWorkflowPayloadInput,
9
+ normalizeMemoryExtractionPayload,
10
+ } from '../extract';
11
+
12
+ describe('normalizeMemoryExtractionPayload', () => {
13
+ it('normalizes sources, layers, ids, and dates with fallback baseUrl', () => {
14
+ const fromDate = new Date('2024-01-01T00:00:00Z');
15
+ const toDate = new Date('2024-02-01T00:00:00Z');
16
+
17
+ const payload: MemoryExtractionPayloadInput = {
18
+ forceAll: true,
19
+ forceTopics: true,
20
+ fromDate,
21
+ identityCursor: 3,
22
+ layers: [LayersEnum.Context, LayersEnum.Identity, LayersEnum.Context],
23
+ mode: 'direct',
24
+ sourceIds: ['source-1', 'source-1', ''],
25
+ sources: ['chatTopics', 'benchmark_locomo', 'unknown'],
26
+ toDate,
27
+ topicIds: ['topic-1', 'topic-1', ''],
28
+ userId: 'user-a',
29
+ userIds: ['user-a', 'user-b', ''],
30
+ };
31
+
32
+ const normalized = normalizeMemoryExtractionPayload(payload, 'https://api.example.com');
33
+
34
+ expect(normalized.baseUrl).toBe('https://api.example.com');
35
+ expect(normalized.forceAll).toBe(true);
36
+ expect(normalized.forceTopics).toBe(true);
37
+ expect(normalized.from).toEqual(fromDate);
38
+ expect(normalized.to).toEqual(toDate);
39
+ expect(normalized.identityCursor).toBe(3);
40
+ expect(normalized.layers).toEqual([LayersEnum.Context, LayersEnum.Identity]);
41
+ expect(normalized.sources).toEqual([
42
+ MemorySourceType.ChatTopic,
43
+ MemorySourceType.BenchmarkLocomo,
44
+ ]);
45
+ expect(normalized.sourceIds).toEqual(['source-1']);
46
+ expect(normalized.topicIds).toEqual(['topic-1']);
47
+ expect(normalized.userId).toBe('user-a');
48
+ expect(normalized.userIds).toEqual(['user-a', 'user-b']);
49
+ });
50
+
51
+ it('throws when baseUrl is missing in both payload and fallback', () => {
52
+ const payload: MemoryExtractionPayloadInput = {
53
+ forceAll: false,
54
+ forceTopics: false,
55
+ userIds: [],
56
+ };
57
+
58
+ expect(() => normalizeMemoryExtractionPayload(payload)).toThrow('Missing baseUrl');
59
+ });
60
+ });
61
+
62
+ describe('buildWorkflowPayloadInput', () => {
63
+ const baseNormalized: MemoryExtractionNormalizedPayload = {
64
+ baseUrl: 'https://api.example.com',
65
+ forceAll: false,
66
+ forceTopics: false,
67
+ from: undefined,
68
+ identityCursor: 0,
69
+ layers: [],
70
+ mode: 'workflow',
71
+ sourceIds: [],
72
+ sources: [MemorySourceType.ChatTopic],
73
+ to: undefined,
74
+ topicCursor: undefined,
75
+ topicIds: [],
76
+ userCursor: undefined,
77
+ userId: undefined,
78
+ userIds: ['user-x', 'user-y'],
79
+ };
80
+
81
+ it('falls back to the first user id when userId is missing', () => {
82
+ const payload = buildWorkflowPayloadInput(baseNormalized);
83
+
84
+ expect(payload.userId).toBe('user-x');
85
+ expect(payload.userIds).toEqual(['user-x', 'user-y']);
86
+ expect(payload.baseUrl).toBe('https://api.example.com');
87
+ expect(payload.mode).toBe('workflow');
88
+ });
89
+
90
+ it('preserves explicit userId when provided', () => {
91
+ const normalized: MemoryExtractionNormalizedPayload = {
92
+ ...baseNormalized,
93
+ userId: 'user-z',
94
+ };
95
+
96
+ const payload = buildWorkflowPayloadInput(normalized);
97
+
98
+ expect(payload.userId).toBe('user-z');
99
+ expect(payload.userIds).toEqual(['user-x', 'user-y']);
100
+ });
101
+ });
@@ -0,0 +1,121 @@
1
+ import type { AiProviderRuntimeState } from '@lobechat/types';
2
+ import type { EnabledAiModel } from 'model-bank';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import type { MemoryExtractionPrivateConfig } from '@/server/globalConfig/parseMemoryExtractionConfig';
6
+
7
+ import { MemoryExtractionExecutor } from '../extract';
8
+
9
+ const createRuntimeState = (models: EnabledAiModel[], keyVaults: Record<string, any>) =>
10
+ ({
11
+ enabledAiModels: models,
12
+ enabledAiProviders: [],
13
+ enabledChatAiProviders: [],
14
+ enabledImageAiProviders: [],
15
+ runtimeConfig: Object.fromEntries(
16
+ Object.entries(keyVaults).map(([providerId, vault]) => [
17
+ providerId,
18
+ { config: {}, keyVaults: vault, settings: {} },
19
+ ]),
20
+ ),
21
+ }) as AiProviderRuntimeState;
22
+
23
+ const createExecutor = (privateOverrides?: Partial<MemoryExtractionPrivateConfig>) => {
24
+ const basePrivateConfig: MemoryExtractionPrivateConfig = {
25
+ agentGateKeeper: { model: 'gate-2', provider: 'provider-b' },
26
+ agentLayerExtractor: {
27
+ contextLimit: 2048,
28
+ layers: {
29
+ context: 'layer-ctx',
30
+ experience: 'layer-exp',
31
+ identity: 'layer-id',
32
+ preference: 'layer-pref',
33
+ },
34
+ model: 'layer-1',
35
+ provider: 'provider-l',
36
+ },
37
+ concurrency: 1,
38
+ embedding: { model: 'embed-1', provider: 'provider-e' },
39
+ featureFlags: { enableBenchmarkLoCoMo: false },
40
+ observabilityS3: { enabled: false },
41
+ };
42
+
43
+ const serverConfig = {
44
+ aiProvider: {},
45
+ memory: {},
46
+ };
47
+
48
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
49
+ // @ts-ignore accessing private constructor for testing
50
+ return new MemoryExtractionExecutor(serverConfig as any, {
51
+ ...basePrivateConfig,
52
+ ...privateOverrides,
53
+ });
54
+ };
55
+
56
+ describe('MemoryExtractionExecutor.resolveRuntimeKeyVaults', () => {
57
+ it('prefers configured providers/models for gatekeeper, embedding, and layer extractors', () => {
58
+ const executor = createExecutor({
59
+ embeddingPreferredProviders: ['provider-e'],
60
+ agentGateKeeperPreferredModels: ['gate-1'],
61
+ agentGateKeeperPreferredProviders: ['provider-a', 'provider-b'],
62
+ agentLayerExtractorPreferredProviders: ['provider-l'],
63
+ });
64
+
65
+ const runtimeState = createRuntimeState(
66
+ [
67
+ { abilities: {}, id: 'gate-1', providerId: 'provider-a', type: 'chat' },
68
+ { abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
69
+ { abilities: {}, id: 'embed-1', providerId: 'provider-e', type: 'embedding' },
70
+ { abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
71
+ { abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
72
+ { abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
73
+ { abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
74
+ ],
75
+ {
76
+ 'provider-a': { apiKey: 'a-key' },
77
+ 'provider-b': { apiKey: 'b-key' },
78
+ 'provider-e': { apiKey: 'e-key' },
79
+ 'provider-l': { apiKey: 'l-key' },
80
+ },
81
+ );
82
+
83
+ const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
84
+
85
+ expect(keyVaults).toMatchObject({
86
+ 'provider-a': { apiKey: 'a-key' }, // gatekeeper picked preferred provider/model
87
+ 'provider-e': { apiKey: 'e-key' }, // embedding honored preferred provider
88
+ 'provider-l': { apiKey: 'l-key' }, // layer extractor models resolved
89
+ });
90
+ });
91
+
92
+ it('warns and falls back to server provider when no enabled provider satisfies embedding model', () => {
93
+ const executor = createExecutor();
94
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
95
+
96
+ const runtimeState = createRuntimeState(
97
+ [
98
+ { abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
99
+ { abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
100
+ { abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
101
+ { abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
102
+ { abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
103
+ ],
104
+ {
105
+ 'provider-b': { apiKey: 'b-key' },
106
+ 'provider-l': { apiKey: 'l-key' },
107
+ },
108
+ );
109
+
110
+ const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
111
+
112
+ expect(keyVaults).toMatchObject({
113
+ 'provider-b': { apiKey: 'b-key' },
114
+ 'provider-l': { apiKey: 'l-key' },
115
+ });
116
+ expect(keyVaults).not.toHaveProperty('provider-e');
117
+ expect(warnSpy).toHaveBeenCalled();
118
+
119
+ warnSpy.mockRestore();
120
+ });
121
+ });