@lobehub/lobehub 2.0.0-next.130 → 2.0.0-next.132

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.
@@ -357,6 +357,13 @@
357
357
  "when": 1764303057060,
358
358
  "tag": "0050_thread_and_user_id",
359
359
  "breakpoints": true
360
+ },
361
+ {
362
+ "idx": 51,
363
+ "version": "7",
364
+ "when": 1764335703306,
365
+ "tag": "0051_add_market_into_user_settings",
366
+ "breakpoints": true
360
367
  }
361
368
  ],
362
369
  "version": "6"
@@ -223,10 +223,7 @@
223
223
  "hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b"
224
224
  },
225
225
  {
226
- "sql": [
227
- "DROP TABLE \"user_budgets\";",
228
- "\nDROP TABLE \"user_subscriptions\";"
229
- ],
226
+ "sql": ["DROP TABLE \"user_budgets\";", "\nDROP TABLE \"user_subscriptions\";"],
230
227
  "bps": true,
231
228
  "folderMillis": 1729699958471,
232
229
  "hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9"
@@ -298,9 +295,7 @@
298
295
  "hash": "845a692ceabbfc3caf252a97d3e19a213bc0c433df2689900135f9cfded2cf49"
299
296
  },
300
297
  {
301
- "sql": [
302
- "ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"
303
- ],
298
+ "sql": ["ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"],
304
299
  "bps": true,
305
300
  "folderMillis": 1737609172353,
306
301
  "hash": "2cb36ae4fcdd7b7064767e04bfbb36ae34518ff4bb1b39006f2dd394d1893868"
@@ -515,9 +510,7 @@
515
510
  "hash": "a7ccf007fd185ff922823148d1eae6fafe652fc98d2fd2793f84a84f29e93cd1"
516
511
  },
517
512
  {
518
- "sql": [
519
- "ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"
520
- ],
513
+ "sql": ["ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"],
521
514
  "bps": true,
522
515
  "folderMillis": 1749309388370,
523
516
  "hash": "39cea379f08ee4cb944875c0b67f7791387b508c2d47958bb4cd501ed1ef33eb"
@@ -635,9 +628,7 @@
635
628
  "hash": "1ba9b1f74ea13348da98d6fcdad7867ab4316ed565bf75d84d160c526cdac14b"
636
629
  },
637
630
  {
638
- "sql": [
639
- "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"
640
- ],
631
+ "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"],
641
632
  "bps": true,
642
633
  "folderMillis": 1759116400580,
643
634
  "hash": "433ddae88e785f2db734e49a4c115eee93e60afe389f7919d66e5ba9aa159a37"
@@ -687,17 +678,13 @@
687
678
  "hash": "4bdc6505797d7a33b622498c138cfd47f637239f6905e1c484cd01d9d5f21d6b"
688
679
  },
689
680
  {
690
- "sql": [
691
- "ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"
692
- ],
681
+ "sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"],
693
682
  "bps": true,
694
683
  "folderMillis": 1760108430562,
695
684
  "hash": "ce09b301abb80f6563abc2f526bdd20b4f69bae430f09ba2179b9e3bfec43067"
696
685
  },
697
686
  {
698
- "sql": [
699
- "ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
700
- ],
687
+ "sql": ["ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
701
688
  "bps": true,
702
689
  "folderMillis": 1761554153406,
703
690
  "hash": "bf2f21293e90e11cf60a784cf3ec219eafa95f7545d7d2f9d1449c0b0949599a"
@@ -777,17 +764,13 @@
777
764
  "hash": "923ccbdf46c32be9a981dabd348e6923b4a365444241e9b8cc174bf5b914cbc5"
778
765
  },
779
766
  {
780
- "sql": [
781
- "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"
782
- ],
767
+ "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"],
783
768
  "bps": true,
784
769
  "folderMillis": 1762870034882,
785
770
  "hash": "4178aacb4b8892b7fd15d29209bbf9b1d1f9d7c406ba796f27542c0bcd919680"
786
771
  },
787
772
  {
788
- "sql": [
789
- "ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"
790
- ],
773
+ "sql": ["ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"],
791
774
  "bps": true,
792
775
  "folderMillis": 1762911968658,
793
776
  "hash": "552a032cc0e595277232e70b5f9338658585bafe9481ae8346a5f322b673a68b"
@@ -816,9 +799,7 @@
816
799
  "hash": "f823b521f4d25e5dc5ab238b372727d2d2d7f0aed27b5eabc8a9608ce4e50568"
817
800
  },
818
801
  {
819
- "sql": [
820
- "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
821
- ],
802
+ "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
822
803
  "bps": true,
823
804
  "folderMillis": 1764215503726,
824
805
  "hash": "4188893a9083b3c7baebdbad0dd3f9d9400ede7584ca2394f5c64305dc9ec7b0"
@@ -848,14 +829,20 @@
848
829
  "\nALTER TABLE \"threads\" ALTER COLUMN \"status\" DROP DEFAULT;",
849
830
  "\nALTER TABLE \"threads\" ALTER COLUMN \"source_message_id\" DROP NOT NULL;",
850
831
  "\nALTER TABLE \"nextauth_authenticators\" ADD CONSTRAINT \"nextauth_authenticators_user_id_credentialID_pk\" PRIMARY KEY(\"user_id\",\"credentialID\");",
851
- "\nALTER TABLE \"threads\" ADD COLUMN \"content\" text;",
852
- "\nALTER TABLE \"threads\" ADD COLUMN \"editor_data\" jsonb;",
832
+ "\nALTER TABLE \"threads\" ADD COLUMN IF NOT EXISTS \"content\" text;",
833
+ "\nALTER TABLE \"threads\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;",
853
834
  "\nALTER TABLE \"nextauth_accounts\" ADD CONSTRAINT \"nextauth_accounts_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;",
854
835
  "\nALTER TABLE \"nextauth_authenticators\" ADD CONSTRAINT \"nextauth_authenticators_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;",
855
- "\nALTER TABLE \"nextauth_sessions\" ADD CONSTRAINT \"nextauth_sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;"
836
+ "\nALTER TABLE \"nextauth_sessions\" ADD CONSTRAINT \"nextauth_sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\n"
856
837
  ],
857
838
  "bps": true,
858
839
  "folderMillis": 1764303057060,
859
- "hash": "edcebee8f59971210c1f8b567d3bd0ffa37da286afeead5bdaaa19eeaa4b89cb"
840
+ "hash": "2c103eee82bdf329944fb622dd9c2b9f20df80eb54f23eb9254d2285de413099"
841
+ },
842
+ {
843
+ "sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"],
844
+ "bps": true,
845
+ "folderMillis": 1764335703306,
846
+ "hash": "28c0d738c0b1fdf5fd871363be1a1477b4accbabdc140fe8dc6e9b339aae2c89"
860
847
  }
861
- ]
848
+ ]
@@ -46,6 +46,7 @@ export const userSettings = pgTable('user_settings', {
46
46
  languageModel: jsonb('language_model'),
47
47
  systemAgent: jsonb('system_agent'),
48
48
  defaultAgent: jsonb('default_agent'),
49
+ market: jsonb('market'),
49
50
  tool: jsonb('tool'),
50
51
  image: jsonb('image'),
51
52
  });
@@ -5,18 +5,18 @@ import { z } from 'zod';
5
5
  export const MAX_SEED = 2 ** 31 - 1;
6
6
 
7
7
  /**
8
- * 默认宽高比,当模型不支持原生宽高比时使用
8
+ * Default aspect ratio, used when the model doesn't support native aspect ratio
9
9
  */
10
10
  export const DEFAULT_ASPECT_RATIO = '1:1';
11
11
 
12
12
  export const PRESET_ASPECT_RATIOS = [
13
- DEFAULT_ASPECT_RATIO, // '1:1' - 正方形,最常用
14
- '16:9', // 现代显示器/电视/视频标准
15
- '9:16', // 手机竖屏/短视频
16
- '4:3', // 传统显示器/照片
17
- '3:4', // 传统竖屏照片
18
- '3:2', // 经典照片比例横屏
19
- '2:3', // 经典照片比例竖屏
13
+ DEFAULT_ASPECT_RATIO, // '1:1' - Square, most commonly used
14
+ '16:9', // Modern monitors/TVs/video standard
15
+ '9:16', // Mobile portrait/short videos
16
+ '4:3', // Traditional monitors/photos
17
+ '3:4', // Traditional portrait photos
18
+ '3:2', // Classic photo ratio landscape
19
+ '2:3', // Classic photo ratio portrait
20
20
  ];
21
21
 
22
22
  /**
@@ -52,10 +52,10 @@ export const CHAT_MODEL_IMAGE_GENERATION_PARAMS: ModelParamsSchema = {
52
52
  prompt: { default: '' },
53
53
  };
54
54
 
55
- // 定义顶层的元规范 - 平铺结构
55
+ // Define top-level meta specification - flat structure
56
56
  export const ModelParamsMetaSchema = z.object({
57
57
  /**
58
- * Prompt 是唯一一个每个模型都有的参数
58
+ * Prompt is the only parameter that every model has
59
59
  */
60
60
  prompt: z.object({
61
61
  default: z.string().optional().default(''),
@@ -213,7 +213,7 @@ export const ModelParamsMetaSchema = z.object({
213
213
  })
214
214
  .optional(),
215
215
  });
216
- // 导出推断出的类型,供定义对象使用
216
+ // Export inferred type for use in defining objects
217
217
  export type ModelParamsSchema = z.input<typeof ModelParamsMetaSchema>;
218
218
  export type ModelParamsOutputSchema = z.output<typeof ModelParamsMetaSchema>;
219
219
  export type ModelParamsKeys = Simplify<keyof ModelParamsOutputSchema>;
@@ -244,16 +244,16 @@ export type RuntimeImageGenParams = Pick<_StandardImageGenerationParameters, 'pr
244
244
  export type RuntimeImageGenParamsKeys = keyof RuntimeImageGenParams;
245
245
  export type RuntimeImageGenParamsValue = RuntimeImageGenParams[RuntimeImageGenParamsKeys];
246
246
 
247
- // 验证函数
247
+ // Validation function
248
248
  export function validateModelParamsSchema(paramsSchema: unknown): ModelParamsOutputSchema {
249
249
  return ModelParamsMetaSchema.parse(paramsSchema);
250
250
  }
251
251
 
252
252
  /**
253
- * 从参数定义对象提取默认值
253
+ * Extract default values from parameter definition object
254
254
  */
255
255
  export function extractDefaultValues(paramsSchema: ModelParamsSchema) {
256
- // 部分默认值从 ModelParamsMetaSchema 中获取
256
+ // Some default values are obtained from ModelParamsMetaSchema
257
257
  const schemaWithDefault = ModelParamsMetaSchema.parse(paramsSchema);
258
258
  return Object.fromEntries(
259
259
  Object.entries(schemaWithDefault).map(([key, value]) => {
@@ -5,6 +5,7 @@ import { UserGeneralConfig } from './general';
5
5
  import { UserHotkeyConfig } from './hotkey';
6
6
  import { UserImageConfig } from './image';
7
7
  import { UserKeyVaults } from './keyVaults';
8
+ import { MarketAuthTokens } from './market';
8
9
  import { UserModelProviderConfig } from './modelProvider';
9
10
  import { UserSystemAgentConfig } from './systemAgent';
10
11
  import { UserToolConfig } from './tool';
@@ -17,6 +18,7 @@ export * from './general';
17
18
  export * from './hotkey';
18
19
  export * from './image';
19
20
  export * from './keyVaults';
21
+ export * from './market';
20
22
  export * from './modelProvider';
21
23
  export * from './sync';
22
24
  export * from './systemAgent';
@@ -33,6 +35,7 @@ export interface UserSettings {
33
35
  image: UserImageConfig;
34
36
  keyVaults: UserKeyVaults;
35
37
  languageModel: UserModelProviderConfig;
38
+ market?: MarketAuthTokens;
36
39
  systemAgent: UserSystemAgentConfig;
37
40
  tool: UserToolConfig;
38
41
  tts: UserTTSConfig;
@@ -50,6 +53,7 @@ export const UserSettingsSchema = z
50
53
  image: z.any().optional(),
51
54
  keyVaults: z.any().optional(),
52
55
  languageModel: z.any().optional(),
56
+ market: z.any().optional(),
53
57
  systemAgent: z.any().optional(),
54
58
  tool: z.any().optional(),
55
59
  tts: z.any().optional(),
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Market authentication tokens
3
+ */
4
+ export interface MarketAuthTokens {
5
+ /**
6
+ * Access token for Market API requests
7
+ */
8
+ accessToken?: string;
9
+ /**
10
+ * Token expiration timestamp (milliseconds since epoch)
11
+ */
12
+ expiresAt?: number;
13
+ /**
14
+ * Refresh token for renewing access tokens
15
+ */
16
+ refreshToken?: string;
17
+ }
@@ -53,7 +53,10 @@ export interface ConfigCellProps {
53
53
  const TopicItem = memo<ConfigCellProps>(({ title, active, id, fav, threadId }) => {
54
54
  const { styles, cx } = useStyles();
55
55
  const toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
56
- const [toggleTopic, editing] = useChatStore((s) => [s.switchTopic, s.topicRenamingId === id]);
56
+ const [toggleTopic, editing] = useChatStore((s) => [
57
+ s.switchTopic,
58
+ s.topicRenamingId !== undefined && s.topicRenamingId === id,
59
+ ]);
57
60
  const activeId = useSessionStore((s) => s.activeId);
58
61
  const [isHover, setHovering] = useState(false);
59
62
 
@@ -65,6 +68,8 @@ const TopicItem = memo<ConfigCellProps>(({ title, active, id, fav, threadId }) =
65
68
  distribution={'space-between'}
66
69
  horizontal
67
70
  onClick={(e) => {
71
+ // Alt+Click should keep the current topic selection (reserved shortcut)
72
+ if (e.altKey) return;
68
73
  // 重命名时不切换话题
69
74
  if (editing) return;
70
75
  // Ctrl/Cmd+点击在新窗口打开
@@ -40,7 +40,8 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
40
40
  const isSubmit = action === 'submit';
41
41
  const isUpload = action === 'upload';
42
42
 
43
- const { session: marketSession, isAuthenticated } = useMarketAuth();
43
+ const marketAuth = useMarketAuth();
44
+ const { isAuthenticated } = marketAuth;
44
45
  const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual);
45
46
  const updateSessionMeta = useSessionStore((s) => s.updateSessionMeta);
46
47
 
@@ -74,7 +75,7 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
74
75
  useEffect(() => {
75
76
  if (!isUpload) return;
76
77
  const marketIdentifier = meta?.marketIdentifier;
77
- const accessToken = marketSession?.accessToken;
78
+ const accessToken = marketAuth.getAccessToken();
78
79
  if (!open || !isAuthenticated || !accessToken || !marketIdentifier) return;
79
80
 
80
81
  let cancelled = false;
@@ -105,13 +106,14 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
105
106
  return () => {
106
107
  cancelled = true;
107
108
  };
108
- }, [isAuthenticated, isUpload, marketSession?.accessToken, meta?.marketIdentifier, open]);
109
+ }, [isAuthenticated, isUpload, marketAuth, meta?.marketIdentifier, open]);
109
110
 
110
111
  const tokenUsage = useTokenCount(systemRole);
111
112
 
112
113
  const handleSubmit = useCallback(
113
114
  async (values: MarketPublishFormValues) => {
114
- if (!isAuthenticated || !marketSession?.accessToken) {
115
+ const accessToken = marketAuth.getAccessToken();
116
+ if (!isAuthenticated || !accessToken) {
115
117
  message.error(t('marketPublish.modal.messages.notAuthenticated'));
116
118
  return false;
117
119
  }
@@ -126,12 +128,15 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
126
128
 
127
129
  try {
128
130
  message.loading({ content: loadingMessage, key: messageKey });
129
- marketApiService.setAccessToken(marketSession.accessToken);
131
+ marketApiService.setAccessToken(accessToken);
130
132
 
131
133
  if (isSubmit) {
132
134
  identifier = values.identifier?.trim();
133
135
  if (!identifier) {
134
- message.error({ content: t('marketPublish.modal.identifier.required'), key: messageKey });
136
+ message.error({
137
+ content: t('marketPublish.modal.identifier.required'),
138
+ key: messageKey,
139
+ });
135
140
  return false;
136
141
  }
137
142
 
@@ -145,7 +150,10 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
145
150
  await marketApiService.createAgent(createPayload as any);
146
151
  }
147
152
  } else if (!identifier) {
148
- message.error({ content: t('marketPublish.modal.messages.missingIdentifier'), key: messageKey });
153
+ message.error({
154
+ content: t('marketPublish.modal.messages.missingIdentifier'),
155
+ key: messageKey,
156
+ });
149
157
  return false;
150
158
  }
151
159
 
@@ -174,7 +182,7 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
174
182
  if (typeof plugin === 'string') {
175
183
  return plugin;
176
184
  } else {
177
- return null
185
+ return null;
178
186
  }
179
187
  }) || [],
180
188
  systemRole: systemRole,
@@ -191,7 +199,9 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
191
199
  } catch (versionError) {
192
200
  const errorMessage = versionError instanceof Error ? versionError.message : '未知错误';
193
201
  message.error({
194
- content: t('marketPublish.modal.messages.createVersionFailed', { message: errorMessage }),
202
+ content: t('marketPublish.modal.messages.createVersionFailed', {
203
+ message: errorMessage,
204
+ }),
195
205
  key: messageKey,
196
206
  });
197
207
  return false;
@@ -210,7 +220,10 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
210
220
  } catch (error) {
211
221
  console.error('Market publish failed:', error);
212
222
  const errorMessage = error instanceof Error ? error.message : '发布失败';
213
- message.error({ content: t('marketPublish.modal.messages.publishFailed', { message: errorMessage }), key: messageKey });
223
+ message.error({
224
+ content: t('marketPublish.modal.messages.publishFailed', { message: errorMessage }),
225
+ key: messageKey,
226
+ });
214
227
  return false;
215
228
  }
216
229
  },
@@ -223,12 +236,12 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
223
236
  isAuthenticated,
224
237
  isSubmit,
225
238
  language,
239
+ marketAuth,
226
240
  meta?.avatar,
227
241
  meta?.description,
228
242
  meta?.marketIdentifier,
229
243
  meta?.tags,
230
244
  meta?.title,
231
- marketSession?.accessToken,
232
245
  messageKey,
233
246
  loadingMessage,
234
247
  model,
@@ -271,7 +284,10 @@ const MarketPublishModal = memo<MarketPublishModalProps>(
271
284
  placeholder={t('marketPublish.modal.identifier.placeholder')}
272
285
  rules={[
273
286
  { message: t('marketPublish.modal.identifier.required'), required: true },
274
- { message: t('marketPublish.modal.identifier.patternError'), pattern: /^[\da-z-]+$/ },
287
+ {
288
+ message: t('marketPublish.modal.identifier.patternError'),
289
+ pattern: /^[\da-z-]+$/,
290
+ },
275
291
  { max: 50, message: t('marketPublish.modal.identifier.lengthError'), min: 3 },
276
292
  ]}
277
293
  />
@@ -9,6 +9,9 @@ const Cerebras: ModelProviderCard = {
9
9
  modelsUrl: 'https://inference-docs.cerebras.ai/models/overview',
10
10
  name: 'Cerebras',
11
11
  settings: {
12
+ proxyUrl: {
13
+ placeholder: 'https://api.cerebras.ai/v1',
14
+ },
12
15
  sdkType: 'openai',
13
16
  showModelFetcher: true,
14
17
  },
@@ -99,10 +99,10 @@ export const checkOwnership = async ({
99
99
  export const useAgentOwnershipCheck = (marketIdentifier?: string): AgentOwnershipResult => {
100
100
  const [result, setResult] = useState<AgentOwnershipResult>({ isOwnAgent: null });
101
101
  const marketAuth = useMarketAuth();
102
- const { session, isAuthenticated } = marketAuth;
102
+ const { isAuthenticated } = marketAuth;
103
103
 
104
104
  useEffect(() => {
105
- if (!marketIdentifier || !isAuthenticated || !session) {
105
+ if (!marketIdentifier || !isAuthenticated) {
106
106
  setResult({ isOwnAgent: false });
107
107
  return;
108
108
  }
@@ -121,8 +121,16 @@ export const useAgentOwnershipCheck = (marketIdentifier?: string): AgentOwnershi
121
121
  return;
122
122
  }
123
123
 
124
+ // 优先从 DB 获取 access token,如果没有则从 session 获取
125
+ const accessToken = marketAuth.getAccessToken();
126
+ if (!accessToken) {
127
+ console.warn('[useAgentOwnershipCheck] No access token available');
128
+ setResult({ isOwnAgent: false });
129
+ return;
130
+ }
131
+
124
132
  const isOwner = await checkOwnership({
125
- accessToken: session.accessToken,
133
+ accessToken,
126
134
  accountId: currentAccountId,
127
135
  marketIdentifier,
128
136
  });
@@ -137,7 +145,7 @@ export const useAgentOwnershipCheck = (marketIdentifier?: string): AgentOwnershi
137
145
  };
138
146
 
139
147
  runOwnershipCheck();
140
- }, [marketIdentifier, isAuthenticated, session, marketAuth]);
148
+ }, [marketIdentifier, isAuthenticated, marketAuth]);
141
149
 
142
150
  return result;
143
151
  };
@@ -3,6 +3,8 @@
3
3
  import { ReactNode, createContext, useContext, useEffect, useState } from 'react';
4
4
 
5
5
  import { MARKET_OIDC_ENDPOINTS } from '@/services/_url';
6
+ import { useUserStore } from '@/store/user';
7
+ import { settingsSelectors } from '@/store/user/slices/settings/selectors/settings';
6
8
 
7
9
  import { MarketAuthError } from './errors';
8
10
  import { MarketOIDC } from './oidc';
@@ -86,6 +88,71 @@ const fetchUserInfo = async (accessToken: string): Promise<MarketUserInfo | null
86
88
  }
87
89
  };
88
90
 
91
+ /**
92
+ * 从 DB 获取 market tokens
93
+ */
94
+ const getMarketTokensFromDB = () => {
95
+ const settings = settingsSelectors.currentSettings(useUserStore.getState());
96
+ return settings.market;
97
+ };
98
+
99
+ /**
100
+ * 存储 market tokens 到 DB
101
+ */
102
+ const saveMarketTokensToDB = async (
103
+ accessToken: string,
104
+ refreshToken?: string,
105
+ expiresAt?: number,
106
+ ) => {
107
+ console.log('[MarketAuth] Saving tokens to DB');
108
+ try {
109
+ await useUserStore.getState().setSettings({
110
+ market: {
111
+ accessToken,
112
+ expiresAt,
113
+ refreshToken,
114
+ },
115
+ });
116
+ console.log('[MarketAuth] Tokens saved to DB successfully');
117
+ } catch (error) {
118
+ console.error('[MarketAuth] Failed to save tokens to DB:', error);
119
+ }
120
+ };
121
+
122
+ /**
123
+ * 清除 DB 中的 market tokens
124
+ */
125
+ const clearMarketTokensFromDB = async () => {
126
+ console.log('[MarketAuth] Clearing tokens from DB');
127
+ try {
128
+ await useUserStore.getState().setSettings({
129
+ market: {
130
+ accessToken: undefined,
131
+ expiresAt: undefined,
132
+ refreshToken: undefined,
133
+ },
134
+ });
135
+ console.log('[MarketAuth] Tokens cleared from DB successfully');
136
+ } catch (error) {
137
+ console.error('[MarketAuth] Failed to clear tokens from DB:', error);
138
+ }
139
+ };
140
+
141
+ /**
142
+ * 获取 refresh token(优先从 DB 获取)
143
+ */
144
+ const getRefreshToken = (): string | null => {
145
+ // 优先从 DB 获取
146
+ const dbTokens = getMarketTokensFromDB();
147
+ if (dbTokens?.refreshToken) {
148
+ console.log('[MarketAuth] Retrieved refresh token from DB');
149
+ return dbTokens.refreshToken;
150
+ }
151
+
152
+ console.log('[MarketAuth] No refresh token found');
153
+ return null;
154
+ };
155
+
89
156
  /**
90
157
  * 刷新令牌(暂时简化,后续可以实现 refresh token 逻辑)
91
158
  */
@@ -234,6 +301,13 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
234
301
  sessionStorage.setItem('market_user_info', JSON.stringify(userInfo));
235
302
  }
236
303
 
304
+ // 存储 tokens 到 DB
305
+ await saveMarketTokensToDB(
306
+ tokenResponse.accessToken,
307
+ tokenResponse.refreshToken,
308
+ newSession.expiresAt,
309
+ );
310
+
237
311
  setSession(newSession);
238
312
  setStatus('authenticated');
239
313
 
@@ -249,12 +323,14 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
249
323
  /**
250
324
  * 登出方法
251
325
  */
252
- const signOut = () => {
326
+ const signOut = async () => {
253
327
  setSession(null);
254
328
  setStatus('unauthenticated');
255
329
  removeTokenFromCookie();
256
330
  sessionStorage.removeItem('market_auth_session');
257
331
  sessionStorage.removeItem('market_user_info');
332
+ // 清除 DB 中的 tokens
333
+ await clearMarketTokensFromDB();
258
334
  };
259
335
 
260
336
  /**
@@ -279,6 +355,40 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
279
355
  return null;
280
356
  };
281
357
 
358
+ /**
359
+ * 获取 access token(优先从 DB 获取,否则从 session 获取)
360
+ */
361
+ const getAccessToken = (): string | null => {
362
+ // 优先从 DB 获取
363
+ const dbTokens = getMarketTokensFromDB();
364
+ if (dbTokens?.accessToken) {
365
+ console.log('[MarketAuth] Retrieved access token from DB');
366
+ return dbTokens.accessToken;
367
+ }
368
+
369
+ // 如果 DB 中没有,从 session 获取
370
+ if (session?.accessToken) {
371
+ console.log('[MarketAuth] Retrieved access token from session');
372
+ return session.accessToken;
373
+ }
374
+
375
+ // 如果 session 中也没有,尝试从 sessionStorage 获取
376
+ try {
377
+ const sessionData = sessionStorage.getItem('market_auth_session');
378
+ if (sessionData) {
379
+ const parsedSession = JSON.parse(sessionData) as MarketAuthSession;
380
+ if (parsedSession.accessToken) {
381
+ console.log('[MarketAuth] Retrieved access token from sessionStorage');
382
+ return parsedSession.accessToken;
383
+ }
384
+ }
385
+ } catch (error) {
386
+ console.error('[MarketAuth] Failed to get access token from sessionStorage:', error);
387
+ }
388
+
389
+ return null;
390
+ };
391
+
282
392
  /**
283
393
  * 初始化时恢复会话
284
394
  */
@@ -324,6 +434,13 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
324
434
  sessionStorage.setItem('market_user_info', JSON.stringify(userInfo));
325
435
  }
326
436
 
437
+ // 存储 tokens 到 DB
438
+ await saveMarketTokensToDB(
439
+ tokenResponse.accessToken,
440
+ tokenResponse.refreshToken,
441
+ newSession.expiresAt,
442
+ );
443
+
327
444
  setSession(newSession);
328
445
  setStatus('authenticated');
329
446
 
@@ -339,7 +456,9 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
339
456
  }, [shouldReauthorize, oidcClient]);
340
457
 
341
458
  const contextValue: MarketAuthContextType = {
459
+ getAccessToken,
342
460
  getCurrentUserInfo,
461
+ getRefreshToken,
343
462
  isAuthenticated: status === 'authenticated',
344
463
  isLoading: status === 'loading',
345
464
  refreshToken,
@@ -36,10 +36,12 @@ export interface MarketAuthState {
36
36
  }
37
37
 
38
38
  export interface MarketAuthContextType extends MarketAuthState {
39
+ getAccessToken: () => string | null;
39
40
  getCurrentUserInfo: () => MarketUserInfo | null;
41
+ getRefreshToken: () => string | null;
40
42
  refreshToken: () => Promise<boolean>;
41
43
  signIn: () => Promise<number | null>;
42
- signOut: () => void;
44
+ signOut: () => Promise<void>;
43
45
  }
44
46
 
45
47
  export interface OIDCConfig {
@@ -59,6 +61,7 @@ export interface TokenResponse {
59
61
  accessToken: string;
60
62
  expiresIn: number;
61
63
  idToken?: string;
64
+ refreshToken?: string;
62
65
  scope: string;
63
66
  tokenType: string;
64
67
  }
@@ -102,6 +102,9 @@ export const initBetterAuthSSOProviders = () => {
102
102
  const config = definition.build(env);
103
103
 
104
104
  if (config) {
105
+ // the generic oidc callback url is /api/auth/oauth2/callback/{providerId}
106
+ // different from builtin providers' /api/auth/callback/{providerId}
107
+ config.redirectURI = `${authEnv.NEXT_PUBLIC_AUTH_URL}/api/auth/callback/${definition.id}`;
105
108
  genericOAuthProviders.push(config);
106
109
  }
107
110
  }