@lobehub/lobehub 2.0.0-next.150 → 2.0.0-next.152

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 (56) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/development/database-schema.dbml +1 -0
  4. package/locales/ar/auth.json +1 -0
  5. package/locales/ar/models.json +0 -9
  6. package/locales/bg-BG/auth.json +1 -0
  7. package/locales/bg-BG/models.json +0 -9
  8. package/locales/de-DE/auth.json +1 -0
  9. package/locales/de-DE/models.json +0 -9
  10. package/locales/en-US/auth.json +1 -0
  11. package/locales/en-US/models.json +0 -9
  12. package/locales/es-ES/auth.json +1 -0
  13. package/locales/es-ES/models.json +0 -9
  14. package/locales/fa-IR/auth.json +1 -0
  15. package/locales/fa-IR/models.json +0 -9
  16. package/locales/fr-FR/auth.json +1 -0
  17. package/locales/fr-FR/models.json +0 -9
  18. package/locales/it-IT/auth.json +1 -0
  19. package/locales/it-IT/models.json +0 -9
  20. package/locales/ja-JP/auth.json +1 -0
  21. package/locales/ja-JP/models.json +0 -9
  22. package/locales/ko-KR/auth.json +1 -0
  23. package/locales/ko-KR/models.json +0 -9
  24. package/locales/nl-NL/auth.json +1 -0
  25. package/locales/nl-NL/models.json +0 -9
  26. package/locales/pl-PL/auth.json +1 -0
  27. package/locales/pl-PL/models.json +0 -9
  28. package/locales/pt-BR/auth.json +1 -0
  29. package/locales/pt-BR/models.json +0 -9
  30. package/locales/ru-RU/auth.json +1 -0
  31. package/locales/ru-RU/models.json +0 -9
  32. package/locales/tr-TR/auth.json +1 -0
  33. package/locales/tr-TR/models.json +0 -9
  34. package/locales/vi-VN/auth.json +1 -0
  35. package/locales/vi-VN/models.json +0 -9
  36. package/locales/zh-CN/auth.json +1 -0
  37. package/locales/zh-CN/models.json +0 -9
  38. package/locales/zh-TW/auth.json +1 -0
  39. package/locales/zh-TW/models.json +0 -9
  40. package/package.json +3 -1
  41. package/packages/database/migrations/0057_add_topic_user_memory_extract_status.sql +1 -0
  42. package/packages/database/migrations/meta/0057_snapshot.json +8426 -0
  43. package/packages/database/migrations/meta/_journal.json +7 -0
  44. package/packages/database/src/core/migrations.json +40 -11
  45. package/packages/database/src/schemas/topic.ts +5 -0
  46. package/packages/model-runtime/package.json +1 -0
  47. package/packages/model-runtime/src/utils/asyncifyPolling.ts +127 -104
  48. package/packages/types/src/topic/topic.ts +12 -0
  49. package/src/app/(backend)/api/auth/check-user/route.ts +8 -1
  50. package/src/app/[variants]/(auth)/signin/page.tsx +23 -10
  51. package/src/auth.ts +2 -1
  52. package/src/components/NextAuth/AuthIcons.tsx +2 -2
  53. package/src/server/services/mcp/index.test.ts +4 -20
  54. package/src/server/services/mcp/index.ts +39 -39
  55. package/src/store/serverConfig/action.ts +5 -1
  56. package/src/store/serverConfig/store.ts +2 -0
@@ -399,6 +399,13 @@
399
399
  "when": 1764685643024,
400
400
  "tag": "0056_update_agent_slug_index",
401
401
  "breakpoints": true
402
+ },
403
+ {
404
+ "idx": 57,
405
+ "version": "7",
406
+ "when": 1764734167674,
407
+ "tag": "0057_add_topic_user_memory_extract_status",
408
+ "breakpoints": true
402
409
  }
403
410
  ],
404
411
  "version": "6"
@@ -223,7 +223,10 @@
223
223
  "hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b"
224
224
  },
225
225
  {
226
- "sql": ["DROP TABLE \"user_budgets\";", "\nDROP TABLE \"user_subscriptions\";"],
226
+ "sql": [
227
+ "DROP TABLE \"user_budgets\";",
228
+ "\nDROP TABLE \"user_subscriptions\";"
229
+ ],
227
230
  "bps": true,
228
231
  "folderMillis": 1729699958471,
229
232
  "hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9"
@@ -295,7 +298,9 @@
295
298
  "hash": "845a692ceabbfc3caf252a97d3e19a213bc0c433df2689900135f9cfded2cf49"
296
299
  },
297
300
  {
298
- "sql": ["ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"],
301
+ "sql": [
302
+ "ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"
303
+ ],
299
304
  "bps": true,
300
305
  "folderMillis": 1737609172353,
301
306
  "hash": "2cb36ae4fcdd7b7064767e04bfbb36ae34518ff4bb1b39006f2dd394d1893868"
@@ -510,7 +515,9 @@
510
515
  "hash": "a7ccf007fd185ff922823148d1eae6fafe652fc98d2fd2793f84a84f29e93cd1"
511
516
  },
512
517
  {
513
- "sql": ["ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"],
518
+ "sql": [
519
+ "ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"
520
+ ],
514
521
  "bps": true,
515
522
  "folderMillis": 1749309388370,
516
523
  "hash": "39cea379f08ee4cb944875c0b67f7791387b508c2d47958bb4cd501ed1ef33eb"
@@ -628,7 +635,9 @@
628
635
  "hash": "1ba9b1f74ea13348da98d6fcdad7867ab4316ed565bf75d84d160c526cdac14b"
629
636
  },
630
637
  {
631
- "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"],
638
+ "sql": [
639
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"
640
+ ],
632
641
  "bps": true,
633
642
  "folderMillis": 1759116400580,
634
643
  "hash": "433ddae88e785f2db734e49a4c115eee93e60afe389f7919d66e5ba9aa159a37"
@@ -678,13 +687,17 @@
678
687
  "hash": "4bdc6505797d7a33b622498c138cfd47f637239f6905e1c484cd01d9d5f21d6b"
679
688
  },
680
689
  {
681
- "sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"],
690
+ "sql": [
691
+ "ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"
692
+ ],
682
693
  "bps": true,
683
694
  "folderMillis": 1760108430562,
684
695
  "hash": "ce09b301abb80f6563abc2f526bdd20b4f69bae430f09ba2179b9e3bfec43067"
685
696
  },
686
697
  {
687
- "sql": ["ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
698
+ "sql": [
699
+ "ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
700
+ ],
688
701
  "bps": true,
689
702
  "folderMillis": 1761554153406,
690
703
  "hash": "bf2f21293e90e11cf60a784cf3ec219eafa95f7545d7d2f9d1449c0b0949599a"
@@ -764,13 +777,17 @@
764
777
  "hash": "923ccbdf46c32be9a981dabd348e6923b4a365444241e9b8cc174bf5b914cbc5"
765
778
  },
766
779
  {
767
- "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"],
780
+ "sql": [
781
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"
782
+ ],
768
783
  "bps": true,
769
784
  "folderMillis": 1762870034882,
770
785
  "hash": "4178aacb4b8892b7fd15d29209bbf9b1d1f9d7c406ba796f27542c0bcd919680"
771
786
  },
772
787
  {
773
- "sql": ["ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"],
788
+ "sql": [
789
+ "ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"
790
+ ],
774
791
  "bps": true,
775
792
  "folderMillis": 1762911968658,
776
793
  "hash": "552a032cc0e595277232e70b5f9338658585bafe9481ae8346a5f322b673a68b"
@@ -799,7 +816,9 @@
799
816
  "hash": "f823b521f4d25e5dc5ab238b372727d2d2d7f0aed27b5eabc8a9608ce4e50568"
800
817
  },
801
818
  {
802
- "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
819
+ "sql": [
820
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
821
+ ],
803
822
  "bps": true,
804
823
  "folderMillis": 1764215503726,
805
824
  "hash": "4188893a9083b3c7baebdbad0dd3f9d9400ede7584ca2394f5c64305dc9ec7b0"
@@ -840,7 +859,9 @@
840
859
  "hash": "2c103eee82bdf329944fb622dd9c2b9f20df80eb54f23eb9254d2285de413099"
841
860
  },
842
861
  {
843
- "sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"],
862
+ "sql": [
863
+ "ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"
864
+ ],
844
865
  "bps": true,
845
866
  "folderMillis": 1764335703306,
846
867
  "hash": "28c0d738c0b1fdf5fd871363be1a1477b4accbabdc140fe8dc6e9b339aae2c89"
@@ -910,5 +931,13 @@
910
931
  "bps": true,
911
932
  "folderMillis": 1764685643024,
912
933
  "hash": "6e7ac7f964eb03efa3cb0d2fd35ded23e25c3abf955c4c2a51418f8daef54af9"
934
+ },
935
+ {
936
+ "sql": [
937
+ "CREATE INDEX IF NOT EXISTS \"topics_extract_status_gin_idx\" ON \"topics\" USING gin ((metadata->'userMemoryExtractStatus') jsonb_path_ops);\n"
938
+ ],
939
+ "bps": true,
940
+ "folderMillis": 1764734167674,
941
+ "hash": "89c134be2948d3afc360d6bac11dea0c6fd5c902bf6093ed077033adb920fd02"
913
942
  }
914
- ]
943
+ ]
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
2
  import type { ChatTopicMetadata } from '@lobechat/types';
3
+ import { sql } from 'drizzle-orm';
3
4
  import { boolean, index, jsonb, pgTable, primaryKey, text, uniqueIndex } from 'drizzle-orm/pg-core';
4
5
  import { createInsertSchema } from 'drizzle-zod';
5
6
 
@@ -39,6 +40,10 @@ export const topics = pgTable(
39
40
  index('topics_session_id_idx').on(t.sessionId),
40
41
  index('topics_group_id_idx').on(t.groupId),
41
42
  index('topics_agent_id_idx').on(t.agentId),
43
+ index('topics_extract_status_gin_idx').using(
44
+ 'gin',
45
+ sql`(metadata->'userMemoryExtractStatus') jsonb_path_ops`,
46
+ ),
42
47
  ],
43
48
  );
44
49
 
@@ -17,6 +17,7 @@
17
17
  "@lobechat/const": "workspace:*",
18
18
  "@lobechat/types": "workspace:*",
19
19
  "@lobechat/utils": "workspace:*",
20
+ "async-retry": "^1.3.3",
20
21
  "debug": "^4.4.3",
21
22
  "model-bank": "workspace:*",
22
23
  "openai": "^4.104.0"
@@ -1,3 +1,5 @@
1
+ import retry from 'async-retry';
2
+
1
3
  export interface TaskResult<T> {
2
4
  data?: T;
3
5
  error?: any;
@@ -23,14 +25,14 @@ export interface AsyncifyPollingOptions<T, R> {
23
25
  checkStatus: (result: T) => TaskResult<R>;
24
26
 
25
27
  // Retry configuration
26
- initialInterval?: number;
28
+ initialInterval?: number;
27
29
  // Optional logger
28
30
  logger?: {
29
31
  debug?: (...args: any[]) => void;
30
32
  error?: (...args: any[]) => void;
31
- };
33
+ };
32
34
  // Default 1.5
33
- maxConsecutiveFailures?: number;
35
+ maxConsecutiveFailures?: number;
34
36
  // Default 500ms
35
37
  maxInterval?: number; // Default 3
36
38
  maxRetries?: number; // Default Infinity
@@ -42,6 +44,24 @@ export interface AsyncifyPollingOptions<T, R> {
42
44
  pollingQuery: () => Promise<T>;
43
45
  }
44
46
 
47
+ // Internal error class to signal that polling should continue
48
+ class PendingError extends Error {
49
+ constructor() {
50
+ super('Task is pending, continue polling');
51
+ this.name = 'PendingError';
52
+ }
53
+ }
54
+
55
+ // Internal error class to signal that task has failed and should not retry
56
+ class TaskFailedError extends Error {
57
+ originalError: any;
58
+ constructor(error: any) {
59
+ super(error instanceof Error ? error.message : String(error));
60
+ this.name = 'TaskFailedError';
61
+ this.originalError = error;
62
+ }
63
+ }
64
+
45
65
  /**
46
66
  * Convert polling pattern to async/await pattern
47
67
  *
@@ -62,114 +82,117 @@ export async function asyncifyPolling<T, R>(options: AsyncifyPollingOptions<T, R
62
82
  logger,
63
83
  } = options;
64
84
 
65
- let retries = 0;
66
85
  let consecutiveFailures = 0;
67
86
 
68
- while (retries < maxRetries) {
69
- let pollingResult: T;
70
-
71
- try {
72
- // Execute polling function
73
- pollingResult = await pollingQuery();
74
-
75
- // Reset consecutive failures counter on successful execution
76
- consecutiveFailures = 0;
77
- } catch (error) {
78
- // Polling function execution failed (network error, etc.)
79
- consecutiveFailures++;
80
-
81
- logger?.error?.(
82
- `Failed to execute polling function (attempt ${retries + 1}/${maxRetries === Infinity ? '∞' : maxRetries}, consecutive failures: ${consecutiveFailures}/${maxConsecutiveFailures}):`,
83
- error,
84
- );
85
-
86
- // Handle custom error processing if provided
87
- if (onPollingError) {
88
- const errorResult = onPollingError({
89
- consecutiveFailures,
90
- error,
91
- retries,
92
- });
93
-
94
- if (!errorResult.isContinuePolling) {
95
- // Custom error handler decided to stop polling
96
- throw errorResult.error || error;
97
- }
98
-
99
- // Custom error handler decided to continue polling
100
- logger?.debug?.('Custom error handler decided to continue polling');
101
- } else {
102
- // Default behavior: check if maximum consecutive failures reached
103
- if (consecutiveFailures >= maxConsecutiveFailures) {
104
- throw new Error(
105
- `Failed to execute polling function after ${consecutiveFailures} consecutive attempts: ${error}`,
87
+ // async-retry uses Infinity for retries when maxRetries is Infinity
88
+ // but we need to handle this case properly
89
+ const retriesConfig = maxRetries === Infinity ? 1_000_000 : maxRetries - 1;
90
+
91
+ try {
92
+ return await retry(
93
+ async (bail, attemptNumber) => {
94
+ const retries = attemptNumber - 1;
95
+
96
+ try {
97
+ // Execute polling function
98
+ const pollingResult = await pollingQuery();
99
+
100
+ // Reset consecutive failures counter on successful execution
101
+ consecutiveFailures = 0;
102
+
103
+ // Check task status
104
+ const statusResult = checkStatus(pollingResult);
105
+
106
+ logger?.debug?.(`Task status: ${statusResult.status} (attempt ${attemptNumber})`);
107
+
108
+ switch (statusResult.status) {
109
+ case 'success': {
110
+ return statusResult.data as R;
111
+ }
112
+
113
+ case 'failed': {
114
+ // Task logic failed, throw error immediately (not counted as consecutive failure)
115
+ bail(new TaskFailedError(statusResult.error || new Error('Task failed')));
116
+ // This return is never reached due to bail, but needed for type safety
117
+ return undefined as R;
118
+ }
119
+
120
+ default: {
121
+ // 'pending' or unknown status - continue polling by throwing PendingError
122
+ throw new PendingError();
123
+ }
124
+ }
125
+ } catch (error) {
126
+ // Re-throw internal errors that should be handled by async-retry
127
+ if (error instanceof PendingError) {
128
+ throw error;
129
+ }
130
+
131
+ // Polling function execution failed (network error, etc.)
132
+ consecutiveFailures++;
133
+
134
+ logger?.error?.(
135
+ `Failed to execute polling function (attempt ${attemptNumber}/${maxRetries === Infinity ? '∞' : maxRetries}, consecutive failures: ${consecutiveFailures}/${maxConsecutiveFailures}):`,
136
+ error,
106
137
  );
107
- }
108
- }
109
-
110
- // Wait before retry and continue to next loop iteration
111
- if (retries < maxRetries - 1) {
112
- const currentInterval = Math.min(
113
- initialInterval * Math.pow(backoffMultiplier, retries),
114
- maxInterval,
115
- );
116
-
117
- logger?.debug?.(`Waiting ${currentInterval}ms before next retry`);
118
138
 
119
- await new Promise((resolve) => {
120
- setTimeout(resolve, currentInterval);
121
- });
122
- }
123
-
124
- retries++;
125
- continue;
126
- }
127
-
128
- // Check task status
129
- const statusResult = checkStatus(pollingResult);
130
-
131
- logger?.debug?.(`Task status: ${statusResult.status} (attempt ${retries + 1})`);
132
-
133
- switch (statusResult.status) {
134
- case 'success': {
135
- return statusResult.data as R;
136
- }
137
-
138
- case 'failed': {
139
- // Task logic failed, throw error immediately (not counted as consecutive failure)
140
- throw statusResult.error || new Error('Task failed');
141
- }
142
-
143
- case 'pending': {
144
- // Continue polling
145
- break;
146
- }
147
-
148
- default: {
149
- // Unknown status, treat as pending
150
- break;
151
- }
139
+ // Handle custom error processing if provided
140
+ if (onPollingError) {
141
+ const errorResult = onPollingError({
142
+ consecutiveFailures,
143
+ error,
144
+ retries,
145
+ });
146
+
147
+ if (!errorResult.isContinuePolling) {
148
+ // Custom error handler decided to stop polling
149
+ bail(errorResult.error || (error as Error));
150
+ return undefined as R;
151
+ }
152
+
153
+ // Custom error handler decided to continue polling
154
+ logger?.debug?.('Custom error handler decided to continue polling');
155
+ throw error; // Rethrow to trigger retry
156
+ } else {
157
+ // Default behavior: check if maximum consecutive failures reached
158
+ if (consecutiveFailures >= maxConsecutiveFailures) {
159
+ bail(
160
+ new Error(
161
+ `Failed to execute polling function after ${consecutiveFailures} consecutive attempts: ${error}`,
162
+ ),
163
+ );
164
+ return undefined as R;
165
+ }
166
+ }
167
+
168
+ // Rethrow to trigger retry
169
+ throw error;
170
+ }
171
+ },
172
+ {
173
+ factor: backoffMultiplier,
174
+ maxTimeout: maxInterval,
175
+ minTimeout: initialInterval,
176
+ onRetry: (error, attempt) => {
177
+ if (!(error instanceof PendingError)) {
178
+ logger?.debug?.(`Retrying after error (attempt ${attempt})`);
179
+ }
180
+ },
181
+ randomize: false, // Disable jitter for predictable intervals
182
+ retries: retriesConfig,
183
+ },
184
+ );
185
+ } catch (error) {
186
+ // Handle TaskFailedError by throwing the original error
187
+ if (error instanceof TaskFailedError) {
188
+ throw error.originalError;
152
189
  }
153
190
 
154
- // Wait before next retry if not the last attempt
155
- if (retries < maxRetries - 1) {
156
- // Calculate dynamic retry interval with exponential backoff
157
- const currentInterval = Math.min(
158
- initialInterval * Math.pow(backoffMultiplier, retries),
159
- maxInterval,
160
- );
161
-
162
- logger?.debug?.(`Waiting ${currentInterval}ms before next retry`);
163
-
164
- // Wait for retry interval
165
- await new Promise((resolve) => {
166
- setTimeout(resolve, currentInterval);
167
- });
191
+ // Handle max retries exceeded
192
+ if (error instanceof PendingError) {
193
+ throw new Error(`Task timeout after ${maxRetries} attempts`);
168
194
  }
169
195
 
170
- retries++;
196
+ throw error;
171
197
  }
172
-
173
- // Maximum retries reached
174
- throw new Error(`Task timeout after ${maxRetries} attempts`);
175
198
  }
@@ -24,9 +24,21 @@ export interface GroupedTopic {
24
24
  title?: string;
25
25
  }
26
26
 
27
+ export interface TopicUserMemoryExtractRunState {
28
+ error?: string;
29
+ lastConversationDigest?: string;
30
+ lastMessageAt?: string;
31
+ lastRunAt?: string;
32
+ messageCount?: number;
33
+ processedMemoryCount?: number;
34
+ version?: string;
35
+ }
36
+
27
37
  export interface ChatTopicMetadata {
28
38
  model?: string;
29
39
  provider?: string;
40
+ userMemoryExtractRunState?: TopicUserMemoryExtractRunState;
41
+ userMemoryExtractStatus?: 'pending' | 'completed' | 'failed';
30
42
  }
31
43
 
32
44
  export interface ChatTopicSummary {
@@ -5,6 +5,13 @@ import { account } from '@/database/schemas/betterAuth';
5
5
  import { users } from '@/database/schemas/user';
6
6
  import { serverDB } from '@/database/server';
7
7
 
8
+ export interface CheckUserResponseData {
9
+ emailVerified?: boolean;
10
+ exists: boolean;
11
+ hasPassword?: boolean;
12
+ providers?: string[];
13
+ }
14
+
8
15
  /**
9
16
  * Check if a user exists by email
10
17
  * @param req - POST request with { email: string }
@@ -52,7 +59,7 @@ export async function POST(req: NextRequest) {
52
59
  exists: true,
53
60
  hasPassword,
54
61
  providers,
55
- });
62
+ } satisfies CheckUserResponseData);
56
63
  } catch (error) {
57
64
  console.error('Error checking user existence:', error);
58
65
  return NextResponse.json({ error: 'Internal server error', exists: false }, { status: 500 });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { ActionIcon, Button } from '@lobehub/ui';
4
4
  import { LobeHub } from '@lobehub/ui/brand';
5
- import { Form, Input, type InputRef } from 'antd';
5
+ import { Form, Input, type InputRef, Skeleton } from 'antd';
6
6
  import { createStyles, useTheme } from 'antd-style';
7
7
  import { ChevronLeft, ChevronRight, Lock, Mail } from 'lucide-react';
8
8
  import { useRouter, useSearchParams } from 'next/navigation';
@@ -10,12 +10,13 @@ import { useEffect, useRef, useState } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { Flexbox } from 'react-layout-kit';
12
12
 
13
+ import type { CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/route';
13
14
  import { message } from '@/components/AntdStaticMethods';
14
15
  import AuthIcons from '@/components/NextAuth/AuthIcons';
15
16
  import { getAuthConfig } from '@/envs/auth';
16
17
  import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
17
18
  import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
18
- import { useUserStore } from '@/store/user';
19
+ import { useServerConfigStore } from '@/store/serverConfig';
19
20
 
20
21
  const useStyles = createStyles(({ css, token }) => ({
21
22
  backButton: css`
@@ -97,7 +98,8 @@ export default function SignInPage() {
97
98
  const [email, setEmail] = useState('');
98
99
  const emailInputRef = useRef<InputRef>(null);
99
100
  const passwordInputRef = useRef<InputRef>(null);
100
- const oAuthSSOProviders = useUserStore((s) => s.oAuthSSOProviders || []);
101
+ const serverConfigInit = useServerConfigStore((s) => s.serverConfigInit);
102
+ const oAuthSSOProviders = useServerConfigStore((s) => s.serverConfig.oAuthSSOProviders) || [];
101
103
 
102
104
  // Auto-focus input when step changes
103
105
  useEffect(() => {
@@ -162,7 +164,7 @@ export default function SignInPage() {
162
164
  method: 'POST',
163
165
  });
164
166
 
165
- const data = await response.json();
167
+ const data: CheckUserResponseData = await response.json();
166
168
 
167
169
  if (!data.exists) {
168
170
  // User not found, redirect to signup page with email pre-filled
@@ -172,16 +174,15 @@ export default function SignInPage() {
172
174
  );
173
175
  return;
174
176
  }
175
-
176
177
  setEmail(values.email);
177
178
 
178
- if (enableMagicLink) {
179
- await handleSendMagicLink(values.email);
179
+ if (data.hasPassword) {
180
+ setStep('password');
180
181
  return;
181
182
  }
182
183
 
183
- if (data.hasPassword) {
184
- setStep('password');
184
+ if (enableMagicLink) {
185
+ await handleSendMagicLink(values.email);
185
186
  return;
186
187
  }
187
188
 
@@ -303,8 +304,20 @@ export default function SignInPage() {
303
304
  <>
304
305
  <p className={styles.subtitle}>{t('betterAuth.signin.emailStep.subtitle')}</p>
305
306
 
307
+ {/* Social Login Section Skeleton */}
308
+ {!serverConfigInit && (
309
+ <Flexbox gap={12} style={{ marginTop: '2rem' }}>
310
+ <Skeleton.Button active block size="large" />
311
+ <Flexbox align="center" gap={12} horizontal>
312
+ <div className={styles.divider} />
313
+ <Skeleton.Input active size="small" style={{ minWidth: 80, width: 80 }} />
314
+ <div className={styles.divider} />
315
+ </Flexbox>
316
+ </Flexbox>
317
+ )}
318
+
306
319
  {/* Social Login Section */}
307
- {oAuthSSOProviders.length > 0 && (
320
+ {serverConfigInit && oAuthSSOProviders.length > 0 && (
308
321
  <Flexbox gap={12} style={{ marginTop: '2rem' }}>
309
322
  {oAuthSSOProviders.map((provider) => (
310
323
  <Button
package/src/auth.ts CHANGED
@@ -145,7 +145,8 @@ export const auth = betterAuth({
145
145
  generateId: ({ model }) => {
146
146
  // Better Auth passes the model name; handle both singular and plural for safety.
147
147
  if (model === 'user' || model === 'users') {
148
- return idGenerator('user', 12);
148
+ // clerk id length is 32
149
+ return idGenerator('user', 32 - 'user_'.length);
149
150
  }
150
151
 
151
152
  // Other models: use shared nanoid generator (12 chars) to keep consistency.
@@ -1,3 +1,4 @@
1
+ import { SiApple } from '@icons-pack/react-simple-icons';
1
2
  import { Aws, Google, Microsoft } from '@lobehub/icons';
2
3
  import {
3
4
  Auth0,
@@ -11,11 +12,10 @@ import {
11
12
  NextAuth,
12
13
  Zitadel,
13
14
  } from '@lobehub/ui/icons';
14
- import { Apple } from 'lucide-react';
15
15
  import React from 'react';
16
16
 
17
17
  const iconComponents: { [key: string]: React.ElementType } = {
18
- 'apple': Apple,
18
+ 'apple': SiApple,
19
19
  'auth0': Auth0,
20
20
  'authelia': Authelia.Color,
21
21
  'authentik': Authentik.Color,
@@ -324,12 +324,13 @@ describe('MCPService', () => {
324
324
  expect(result).toHaveLength(1);
325
325
  });
326
326
 
327
- it('should throw TRPCError when NoValidSessionId retry exceeds limit', async () => {
327
+ it('should throw original error when NoValidSessionId retry exceeds limit', async () => {
328
328
  // Fail more than 3 times
329
329
  mockClient.listTools.mockRejectedValue(new Error('NoValidSessionId'));
330
330
 
331
- await expect(mcpService.listTools(mockParams)).rejects.toThrow(TRPCError);
332
- expect(mockClient.listTools).toHaveBeenCalledTimes(5); // initial + 4 retry attempts (last one fails condition)
331
+ await expect(mcpService.listTools(mockParams)).rejects.toThrow('NoValidSessionId');
332
+ // async-retry: 1 initial + 3 retries = 4 attempts
333
+ expect(mockClient.listTools).toHaveBeenCalledTimes(4);
333
334
  });
334
335
 
335
336
  it('should throw TRPCError on other errors without retry', async () => {
@@ -340,23 +341,6 @@ describe('MCPService', () => {
340
341
  expect(mockClient.listTools).toHaveBeenCalledTimes(1);
341
342
  });
342
343
 
343
- it('should pass skipCache option to getClient', async () => {
344
- const mockTools = [
345
- {
346
- name: 'tool1',
347
- description: 'Test tool',
348
- inputSchema: { type: 'object' },
349
- },
350
- ];
351
-
352
- mockClient.listTools.mockResolvedValue(mockTools);
353
-
354
- await mcpService.listTools(mockParams, { skipCache: true });
355
-
356
- // Verify getClient was called with skipCache
357
- expect(mcpService.getClient).toHaveBeenCalledWith(mockParams, true);
358
- });
359
-
360
344
  it('should throw TRPCError with correct error message', async () => {
361
345
  const error = new Error('Custom error message');
362
346
  mockClient.listTools.mockRejectedValue(error);