@lobehub/lobehub 2.0.0-next.240 → 2.0.0-next.242

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 (123) hide show
  1. package/.github/workflows/test.yml +13 -5
  2. package/CHANGELOG.md +50 -0
  3. package/apps/desktop/build/Icon-beta.Assets.car +0 -0
  4. package/apps/desktop/build/Icon-beta.icns +0 -0
  5. package/apps/desktop/build/icon-beta.ico +0 -0
  6. package/apps/desktop/build/icon-beta.png +0 -0
  7. package/apps/desktop/resources/locales/ar/menu.json +5 -1
  8. package/apps/desktop/resources/locales/bg-BG/menu.json +5 -1
  9. package/apps/desktop/resources/locales/de-DE/menu.json +5 -1
  10. package/apps/desktop/resources/locales/es-ES/menu.json +5 -1
  11. package/apps/desktop/resources/locales/fa-IR/menu.json +5 -1
  12. package/apps/desktop/resources/locales/fr-FR/menu.json +5 -1
  13. package/apps/desktop/resources/locales/it-IT/menu.json +5 -1
  14. package/apps/desktop/resources/locales/ja-JP/menu.json +5 -1
  15. package/apps/desktop/resources/locales/ko-KR/menu.json +5 -1
  16. package/apps/desktop/resources/locales/nl-NL/menu.json +5 -1
  17. package/apps/desktop/resources/locales/pl-PL/menu.json +5 -1
  18. package/apps/desktop/resources/locales/pt-BR/menu.json +5 -1
  19. package/apps/desktop/resources/locales/ru-RU/menu.json +5 -1
  20. package/apps/desktop/resources/locales/tr-TR/menu.json +5 -1
  21. package/apps/desktop/resources/locales/vi-VN/menu.json +5 -1
  22. package/apps/desktop/resources/locales/zh-CN/menu.json +5 -1
  23. package/apps/desktop/resources/locales/zh-TW/menu.json +5 -1
  24. package/apps/desktop/src/main/locales/default/menu.ts +5 -1
  25. package/apps/desktop/src/main/menus/impls/linux.ts +30 -0
  26. package/apps/desktop/src/main/menus/impls/macOS.test.ts +17 -0
  27. package/apps/desktop/src/main/menus/impls/macOS.ts +33 -0
  28. package/apps/desktop/src/main/menus/impls/windows.ts +30 -0
  29. package/changelog/v1.json +10 -0
  30. package/locales/ar/electron.json +24 -0
  31. package/locales/ar/models.json +48 -7
  32. package/locales/ar/plugin.json +9 -0
  33. package/locales/ar/providers.json +1 -0
  34. package/locales/bg-BG/electron.json +24 -0
  35. package/locales/bg-BG/models.json +35 -7
  36. package/locales/bg-BG/plugin.json +9 -0
  37. package/locales/bg-BG/providers.json +1 -0
  38. package/locales/de-DE/electron.json +24 -0
  39. package/locales/de-DE/models.json +26 -6
  40. package/locales/de-DE/plugin.json +9 -0
  41. package/locales/de-DE/providers.json +1 -0
  42. package/locales/en-US/electron.json +24 -0
  43. package/locales/en-US/models.json +10 -10
  44. package/locales/en-US/oauth.json +0 -1
  45. package/locales/en-US/providers.json +1 -0
  46. package/locales/en-US/subscription.json +2 -2
  47. package/locales/es-ES/electron.json +24 -0
  48. package/locales/es-ES/models.json +42 -7
  49. package/locales/es-ES/plugin.json +9 -0
  50. package/locales/es-ES/providers.json +1 -0
  51. package/locales/fa-IR/electron.json +24 -0
  52. package/locales/fa-IR/models.json +52 -10
  53. package/locales/fa-IR/plugin.json +9 -0
  54. package/locales/fa-IR/providers.json +1 -0
  55. package/locales/fr-FR/electron.json +24 -0
  56. package/locales/fr-FR/models.json +36 -7
  57. package/locales/fr-FR/plugin.json +9 -0
  58. package/locales/fr-FR/providers.json +1 -0
  59. package/locales/it-IT/electron.json +24 -0
  60. package/locales/it-IT/models.json +42 -7
  61. package/locales/it-IT/plugin.json +9 -0
  62. package/locales/it-IT/providers.json +1 -0
  63. package/locales/ja-JP/electron.json +24 -0
  64. package/locales/ja-JP/models.json +35 -6
  65. package/locales/ja-JP/plugin.json +9 -0
  66. package/locales/ja-JP/providers.json +1 -0
  67. package/locales/ko-KR/electron.json +24 -0
  68. package/locales/ko-KR/models.json +28 -7
  69. package/locales/ko-KR/plugin.json +9 -0
  70. package/locales/ko-KR/providers.json +1 -0
  71. package/locales/nl-NL/electron.json +24 -0
  72. package/locales/nl-NL/models.json +35 -6
  73. package/locales/nl-NL/plugin.json +9 -0
  74. package/locales/nl-NL/providers.json +1 -0
  75. package/locales/pl-PL/electron.json +24 -0
  76. package/locales/pl-PL/models.json +36 -7
  77. package/locales/pl-PL/plugin.json +9 -0
  78. package/locales/pl-PL/providers.json +1 -0
  79. package/locales/pt-BR/electron.json +24 -0
  80. package/locales/pt-BR/models.json +35 -6
  81. package/locales/pt-BR/plugin.json +9 -0
  82. package/locales/pt-BR/providers.json +1 -0
  83. package/locales/ru-RU/electron.json +24 -0
  84. package/locales/ru-RU/models.json +35 -7
  85. package/locales/ru-RU/plugin.json +9 -0
  86. package/locales/ru-RU/providers.json +1 -0
  87. package/locales/tr-TR/electron.json +24 -0
  88. package/locales/tr-TR/models.json +5 -7
  89. package/locales/tr-TR/plugin.json +9 -0
  90. package/locales/tr-TR/providers.json +1 -0
  91. package/locales/vi-VN/electron.json +24 -0
  92. package/locales/vi-VN/models.json +5 -5
  93. package/locales/vi-VN/plugin.json +9 -0
  94. package/locales/vi-VN/providers.json +1 -0
  95. package/locales/zh-CN/electron.json +24 -0
  96. package/locales/zh-CN/models.json +48 -6
  97. package/locales/zh-CN/oauth.json +0 -1
  98. package/locales/zh-CN/providers.json +1 -0
  99. package/locales/zh-CN/subscription.json +1 -1
  100. package/locales/zh-TW/electron.json +24 -0
  101. package/locales/zh-TW/models.json +10 -10
  102. package/locales/zh-TW/plugin.json +9 -0
  103. package/locales/zh-TW/providers.json +1 -0
  104. package/package.json +1 -1
  105. package/packages/electron-client-ipc/src/events/navigation.ts +12 -0
  106. package/src/components/PageTitle/index.tsx +11 -1
  107. package/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx +137 -0
  108. package/src/features/ElectronTitlebar/NavigationBar/index.tsx +86 -0
  109. package/src/features/ElectronTitlebar/helpers/routeMetadata.ts +214 -0
  110. package/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts +152 -0
  111. package/src/features/ElectronTitlebar/index.tsx +13 -5
  112. package/src/features/NavHeader/index.tsx +4 -2
  113. package/src/features/NavPanel/components/NavPanelDraggable.tsx +174 -0
  114. package/src/features/NavPanel/hooks/useNavPanel.ts +11 -35
  115. package/src/features/NavPanel/index.tsx +2 -126
  116. package/src/hooks/useTypeScriptHappyCallback.ts +7 -0
  117. package/src/locales/default/electron.ts +24 -0
  118. package/src/locales/default/subscription.ts +2 -3
  119. package/src/server/services/memory/userMemory/extract.ts +46 -6
  120. package/src/store/electron/actions/navigationHistory.ts +247 -0
  121. package/src/store/electron/initialState.ts +7 -1
  122. package/src/store/electron/store.ts +9 -2
  123. package/src/store/global/selectors/systemStatus.ts +4 -1
@@ -66,6 +66,7 @@ import type { GlobalMemoryLayer } from '@/types/serverConfig';
66
66
  import type { UserKeyVaults } from '@/types/user/settings';
67
67
  import { LayersEnum, type MergeStrategyEnum, TypesEnum, MemorySourceType } from '@/types/userMemory';
68
68
  import { encodeAsync } from '@/utils/tokenizer';
69
+ import debug from 'debug';
69
70
 
70
71
  const SOURCE_ALIAS_MAP: Record<string, MemorySourceType> = {
71
72
  benchmark_locomo: MemorySourceType.BenchmarkLocomo,
@@ -262,19 +263,58 @@ const resolveLayerModels = (
262
263
  preference: layers?.preference ?? fallback.preference,
263
264
  });
264
265
 
265
- const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?: UserKeyVaults) => {
266
+ const maskSecret = (value?: string) => {
267
+ if (!value) return 'undefined';
268
+ if (value.length <= 8) return `${value[0]}***${value.at(-1)}`;
269
+
270
+ return `${value.slice(0, 6)}***${value.slice(-4)}`;
271
+ };
272
+
273
+ const resolveRuntimeAgentConfig = (agent: MemoryAgentConfig, keyVaults?: UserKeyVaults) => {
266
274
  const provider = agent.provider || 'openai';
267
275
  const { apiKey: userApiKey, baseURL: userBaseURL } = extractCredentialsFromVault(
268
276
  provider,
269
277
  keyVaults,
270
278
  );
271
279
 
272
- const apiKey = userApiKey || agent.apiKey;
273
- if (!apiKey) throw new Error(`Missing API key for ${provider} memory extraction runtime`);
280
+ // Only use the user baseURL if we are also using their API key; otherwise fall back entirely
281
+ // to system config to avoid mixing credentials.
282
+ const useUserCredential = !!userApiKey;
283
+ const apiKey = useUserCredential ? userApiKey : agent.apiKey;
284
+ const baseURL = useUserCredential ? userBaseURL || agent.baseURL : agent.baseURL;
285
+ const source = useUserCredential ? 'user-keyvault' : 'system-config';
286
+
287
+ return { apiKey, baseURL, provider, source };
288
+ };
289
+
290
+ const logRuntime = debug('lobe-server:memory:user-memory:runtime');
291
+
292
+ const debugRuntimeInit = (
293
+ agent: MemoryAgentConfig,
294
+ resolved: ReturnType<typeof resolveRuntimeAgentConfig>,
295
+ ) => {
296
+ if (!logRuntime.enabled) return;
297
+ logRuntime('init runtime', {
298
+ agentModel: agent.model,
299
+ agentProvider: agent.provider || 'openai',
300
+ apiKey: maskSecret(resolved.apiKey),
301
+ baseURL: resolved.baseURL,
302
+ provider: resolved.provider,
303
+ source: resolved.source,
304
+ });
305
+ };
306
+
307
+ const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?: UserKeyVaults) => {
308
+ const resolved = resolveRuntimeAgentConfig(agent, keyVaults);
309
+ debugRuntimeInit(agent, resolved);
310
+
311
+ if (!resolved.apiKey) {
312
+ throw new Error(`Missing API key for ${resolved.provider} memory extraction runtime`);
313
+ }
274
314
 
275
- return ModelRuntime.initializeWithProvider(provider, {
276
- apiKey,
277
- baseURL: userBaseURL || agent.baseURL,
315
+ return ModelRuntime.initializeWithProvider(resolved.provider, {
316
+ apiKey: resolved.apiKey,
317
+ baseURL: resolved.baseURL,
278
318
  });
279
319
  };
280
320
 
@@ -0,0 +1,247 @@
1
+ import { type StateCreator } from 'zustand/vanilla';
2
+
3
+ import type { ElectronStore } from '../store';
4
+
5
+ // ======== Types ======== //
6
+
7
+ export interface HistoryEntry {
8
+ icon?: string;
9
+ metadata?: {
10
+ [key: string]: any;
11
+ sessionId?: string;
12
+ timestamp: number;
13
+ };
14
+ title: string;
15
+ url: string;
16
+ }
17
+
18
+ export interface NavigationHistoryState {
19
+ /**
20
+ * Current page title from PageTitle component
21
+ * Used to get dynamic titles without setTimeout hack
22
+ */
23
+ currentPageTitle: string;
24
+ /**
25
+ * Current position in history (-1 means empty)
26
+ */
27
+ historyCurrentIndex: number;
28
+ /**
29
+ * History entries list
30
+ */
31
+ historyEntries: HistoryEntry[];
32
+ /**
33
+ * Flag to indicate if currently navigating via back/forward
34
+ * Used to prevent adding duplicate history entries
35
+ */
36
+ isNavigatingHistory: boolean;
37
+ }
38
+
39
+ // ======== Action Interface ======== //
40
+
41
+ export interface NavigationHistoryAction {
42
+ /**
43
+ * Check if can go back in history
44
+ */
45
+ canGoBack: () => boolean;
46
+
47
+ /**
48
+ * Check if can go forward in history
49
+ */
50
+ canGoForward: () => boolean;
51
+
52
+ /**
53
+ * Get current history entry
54
+ */
55
+ getCurrentEntry: () => HistoryEntry | null;
56
+
57
+ /**
58
+ * Navigate back in history
59
+ * @returns The target entry or null if cannot go back
60
+ */
61
+ goBack: () => HistoryEntry | null;
62
+
63
+ /**
64
+ * Navigate forward in history
65
+ * @returns The target entry or null if cannot go forward
66
+ */
67
+ goForward: () => HistoryEntry | null;
68
+
69
+ /**
70
+ * Push a new entry to history (for normal navigation)
71
+ * Truncates any forward history if not at the end
72
+ */
73
+ pushHistory: (
74
+ entry: Omit<HistoryEntry, 'metadata'> & { metadata?: Partial<HistoryEntry['metadata']> },
75
+ ) => void;
76
+
77
+ /**
78
+ * Replace current entry in history (for replace navigation)
79
+ */
80
+ replaceHistory: (
81
+ entry: Omit<HistoryEntry, 'metadata'> & { metadata?: Partial<HistoryEntry['metadata']> },
82
+ ) => void;
83
+
84
+ /**
85
+ * Set current page title (called by PageTitle component)
86
+ */
87
+ setCurrentPageTitle: (title: string) => void;
88
+
89
+ /**
90
+ * Set the navigating history flag
91
+ */
92
+ setIsNavigatingHistory: (value: boolean) => void;
93
+ }
94
+
95
+ // ======== Initial State ======== //
96
+
97
+ export const navigationHistoryInitialState: NavigationHistoryState = {
98
+ currentPageTitle: '',
99
+ historyCurrentIndex: -1,
100
+ historyEntries: [],
101
+ isNavigatingHistory: false,
102
+ };
103
+
104
+ // ======== Action Implementation ======== //
105
+
106
+ export const createNavigationHistorySlice: StateCreator<
107
+ ElectronStore,
108
+ [['zustand/devtools', never]],
109
+ [],
110
+ NavigationHistoryAction
111
+ > = (set, get) => ({
112
+ canGoBack: () => {
113
+ const { historyCurrentIndex } = get();
114
+ return historyCurrentIndex > 0;
115
+ },
116
+
117
+ canGoForward: () => {
118
+ const { historyCurrentIndex, historyEntries } = get();
119
+ return historyCurrentIndex < historyEntries.length - 1;
120
+ },
121
+
122
+ getCurrentEntry: () => {
123
+ const { historyCurrentIndex, historyEntries } = get();
124
+ if (historyCurrentIndex < 0 || historyCurrentIndex >= historyEntries.length) {
125
+ return null;
126
+ }
127
+ return historyEntries[historyCurrentIndex];
128
+ },
129
+
130
+ goBack: () => {
131
+ const { historyCurrentIndex, historyEntries } = get();
132
+
133
+ if (historyCurrentIndex <= 0) {
134
+ return null;
135
+ }
136
+
137
+ const newIndex = historyCurrentIndex - 1;
138
+ const targetEntry = historyEntries[newIndex];
139
+
140
+ set(
141
+ {
142
+ historyCurrentIndex: newIndex,
143
+ isNavigatingHistory: true,
144
+ },
145
+ false,
146
+ 'goBack',
147
+ );
148
+
149
+ return targetEntry;
150
+ },
151
+
152
+ goForward: () => {
153
+ const { historyCurrentIndex, historyEntries } = get();
154
+
155
+ if (historyCurrentIndex >= historyEntries.length - 1) {
156
+ return null;
157
+ }
158
+
159
+ const newIndex = historyCurrentIndex + 1;
160
+ const targetEntry = historyEntries[newIndex];
161
+
162
+ set(
163
+ {
164
+ historyCurrentIndex: newIndex,
165
+ isNavigatingHistory: true,
166
+ },
167
+ false,
168
+ 'goForward',
169
+ );
170
+
171
+ return targetEntry;
172
+ },
173
+
174
+ pushHistory: (entry) => {
175
+ const { historyCurrentIndex, historyEntries } = get();
176
+
177
+ // Create full entry with metadata
178
+ const fullEntry: HistoryEntry = {
179
+ icon: entry.icon,
180
+ metadata: {
181
+ timestamp: Date.now(),
182
+ ...entry.metadata,
183
+ },
184
+ title: entry.title,
185
+ url: entry.url,
186
+ };
187
+
188
+ // If not at the end, truncate forward history
189
+ const newEntries =
190
+ historyCurrentIndex < historyEntries.length - 1
191
+ ? historyEntries.slice(0, historyCurrentIndex + 1)
192
+ : [...historyEntries];
193
+
194
+ // Add new entry
195
+ newEntries.push(fullEntry);
196
+
197
+ set(
198
+ {
199
+ historyCurrentIndex: newEntries.length - 1,
200
+ historyEntries: newEntries,
201
+ },
202
+ false,
203
+ 'pushHistory',
204
+ );
205
+ },
206
+
207
+ replaceHistory: (entry) => {
208
+ const { historyCurrentIndex, historyEntries } = get();
209
+
210
+ // If history is empty, just push
211
+ if (historyCurrentIndex < 0 || historyEntries.length === 0) {
212
+ get().pushHistory(entry);
213
+ return;
214
+ }
215
+
216
+ // Create full entry with metadata
217
+ const fullEntry: HistoryEntry = {
218
+ icon: entry.icon,
219
+ metadata: {
220
+ timestamp: Date.now(),
221
+ ...entry.metadata,
222
+ },
223
+ title: entry.title,
224
+ url: entry.url,
225
+ };
226
+
227
+ // Replace current entry
228
+ const newEntries = [...historyEntries];
229
+ newEntries[historyCurrentIndex] = fullEntry;
230
+
231
+ set(
232
+ {
233
+ historyEntries: newEntries,
234
+ },
235
+ false,
236
+ 'replaceHistory',
237
+ );
238
+ },
239
+
240
+ setCurrentPageTitle: (title) => {
241
+ set({ currentPageTitle: title }, false, 'setCurrentPageTitle');
242
+ },
243
+
244
+ setIsNavigatingHistory: (value) => {
245
+ set({ isNavigatingHistory: value }, false, 'setIsNavigatingHistory');
246
+ },
247
+ });
@@ -4,6 +4,11 @@ import {
4
4
  type NetworkProxySettings,
5
5
  } from '@lobechat/electron-client-ipc';
6
6
 
7
+ import {
8
+ type NavigationHistoryState,
9
+ navigationHistoryInitialState,
10
+ } from './actions/navigationHistory';
11
+
7
12
  export type RemoteServerError = 'CONFIG_ERROR' | 'AUTH_ERROR' | 'DISCONNECT_ERROR';
8
13
 
9
14
  export const defaultProxySettings: NetworkProxySettings = {
@@ -15,7 +20,7 @@ export const defaultProxySettings: NetworkProxySettings = {
15
20
  proxyType: 'http',
16
21
  };
17
22
 
18
- export interface ElectronState {
23
+ export interface ElectronState extends NavigationHistoryState {
19
24
  appState: ElectronAppState;
20
25
  dataSyncConfig: DataSyncConfig;
21
26
  desktopHotkeys: Record<string, string>;
@@ -29,6 +34,7 @@ export interface ElectronState {
29
34
  }
30
35
 
31
36
  export const initialState: ElectronState = {
37
+ ...navigationHistoryInitialState,
32
38
  appState: {},
33
39
  dataSyncConfig: { storageMode: 'cloud' },
34
40
  desktopHotkeys: {},
@@ -4,6 +4,10 @@ import { type StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { createDevtools } from '../middleware/createDevtools';
6
6
  import { type ElectronAppAction, createElectronAppSlice } from './actions/app';
7
+ import {
8
+ type NavigationHistoryAction,
9
+ createNavigationHistorySlice,
10
+ } from './actions/navigationHistory';
7
11
  import { type ElectronSettingsAction, settingsSlice } from './actions/settings';
8
12
  import { type ElectronRemoteServerAction, remoteSyncSlice } from './actions/sync';
9
13
  import { type ElectronState, initialState } from './initialState';
@@ -11,10 +15,12 @@ import { type ElectronState, initialState } from './initialState';
11
15
  // =============== Aggregate createStoreFn ============ //
12
16
 
13
17
  export interface ElectronStore
14
- extends ElectronState,
18
+ extends
19
+ ElectronState,
15
20
  ElectronRemoteServerAction,
16
21
  ElectronAppAction,
17
- ElectronSettingsAction {
22
+ ElectronSettingsAction,
23
+ NavigationHistoryAction {
18
24
  /* empty */
19
25
  }
20
26
 
@@ -25,6 +31,7 @@ const createStore: StateCreator<ElectronStore, [['zustand/devtools', never]]> =
25
31
  ...remoteSyncSlice(...parameters),
26
32
  ...createElectronAppSlice(...parameters),
27
33
  ...settingsSlice(...parameters),
34
+ ...createNavigationHistorySlice(...parameters),
28
35
  });
29
36
 
30
37
  // =============== Implement useStore ============ //
@@ -30,7 +30,10 @@ const modelSwitchPanelWidth = (s: GlobalState) => s.status.modelSwitchPanelWidth
30
30
 
31
31
  const showChatHeader = (s: GlobalState) => !s.status.zenMode;
32
32
  const inZenMode = (s: GlobalState) => s.status.zenMode;
33
- const leftPanelWidth = (s: GlobalState) => s.status.leftPanelWidth;
33
+ const leftPanelWidth = (s: GlobalState): number => {
34
+ const width = s.status.leftPanelWidth;
35
+ return typeof width === 'string' ? Number.parseInt(width) : width;
36
+ };
34
37
  const portalWidth = (s: GlobalState) => s.status.portalWidth || 400;
35
38
  const filePanelWidth = (s: GlobalState) => s.status.filePanelWidth;
36
39
  const imagePanelWidth = (s: GlobalState) => s.status.imagePanelWidth;