@lobehub/chat 1.99.6 → 1.100.0

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 (92) hide show
  1. package/.github/workflows/desktop-pr-build.yml +3 -3
  2. package/.github/workflows/release-desktop-beta.yml +3 -3
  3. package/CHANGELOG.md +25 -0
  4. package/apps/desktop/package.json +5 -2
  5. package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
  6. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
  7. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
  8. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
  9. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
  10. package/apps/desktop/src/main/types/store.ts +1 -0
  11. package/apps/desktop/src/preload/electronApi.ts +2 -1
  12. package/apps/desktop/src/preload/streamer.ts +58 -0
  13. package/changelog/v1.json +9 -0
  14. package/docs/development/database-schema.dbml +9 -0
  15. package/locales/ar/electron.json +3 -0
  16. package/locales/ar/oauth.json +8 -4
  17. package/locales/bg-BG/electron.json +3 -0
  18. package/locales/bg-BG/oauth.json +8 -4
  19. package/locales/de-DE/electron.json +3 -0
  20. package/locales/de-DE/oauth.json +9 -5
  21. package/locales/en-US/electron.json +3 -0
  22. package/locales/en-US/oauth.json +8 -4
  23. package/locales/es-ES/electron.json +3 -0
  24. package/locales/es-ES/oauth.json +9 -5
  25. package/locales/fa-IR/electron.json +3 -0
  26. package/locales/fa-IR/oauth.json +8 -4
  27. package/locales/fr-FR/electron.json +3 -0
  28. package/locales/fr-FR/oauth.json +8 -4
  29. package/locales/it-IT/electron.json +3 -0
  30. package/locales/it-IT/oauth.json +9 -5
  31. package/locales/ja-JP/electron.json +3 -0
  32. package/locales/ja-JP/oauth.json +8 -4
  33. package/locales/ko-KR/electron.json +3 -0
  34. package/locales/ko-KR/oauth.json +8 -4
  35. package/locales/nl-NL/electron.json +3 -0
  36. package/locales/nl-NL/oauth.json +9 -5
  37. package/locales/pl-PL/electron.json +3 -0
  38. package/locales/pl-PL/oauth.json +8 -4
  39. package/locales/pt-BR/electron.json +3 -0
  40. package/locales/pt-BR/oauth.json +8 -4
  41. package/locales/ru-RU/electron.json +3 -0
  42. package/locales/ru-RU/oauth.json +8 -4
  43. package/locales/tr-TR/electron.json +3 -0
  44. package/locales/tr-TR/oauth.json +8 -4
  45. package/locales/vi-VN/electron.json +3 -0
  46. package/locales/vi-VN/oauth.json +9 -5
  47. package/locales/zh-CN/electron.json +3 -0
  48. package/locales/zh-CN/oauth.json +8 -4
  49. package/locales/zh-TW/electron.json +3 -0
  50. package/locales/zh-TW/oauth.json +8 -4
  51. package/package.json +3 -3
  52. package/packages/electron-client-ipc/src/dispatch.ts +14 -2
  53. package/packages/electron-client-ipc/src/index.ts +1 -0
  54. package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
  55. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
  56. package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
  57. package/packages/electron-client-ipc/src/utils/request.ts +28 -0
  58. package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
  59. package/src/app/(backend)/oidc/handoff/route.ts +46 -0
  60. package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
  61. package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
  62. package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
  63. package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
  64. package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
  65. package/src/database/client/migrations.json +8 -0
  66. package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
  67. package/src/database/migrations/meta/0028_snapshot.json +6055 -0
  68. package/src/database/migrations/meta/_journal.json +7 -0
  69. package/src/database/models/oauthHandoff.ts +94 -0
  70. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  71. package/src/database/schemas/oidc.ts +46 -0
  72. package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
  73. package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
  74. package/src/libs/oidc-provider/config.ts +16 -17
  75. package/src/libs/oidc-provider/jwt.ts +135 -0
  76. package/src/libs/oidc-provider/provider.ts +22 -38
  77. package/src/libs/trpc/client/async.ts +1 -2
  78. package/src/libs/trpc/client/edge.ts +1 -2
  79. package/src/libs/trpc/client/lambda.ts +1 -1
  80. package/src/libs/trpc/client/tools.ts +1 -2
  81. package/src/libs/trpc/lambda/context.ts +9 -16
  82. package/src/locales/default/electron.ts +3 -0
  83. package/src/locales/default/oauth.ts +8 -4
  84. package/src/middleware.ts +10 -4
  85. package/src/server/services/oidc/index.ts +0 -71
  86. package/src/services/chat.ts +5 -1
  87. package/src/services/electron/remoteServer.ts +0 -7
  88. package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
  89. package/src/utils/server/auth.ts +22 -0
  90. package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
  91. package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
  92. package/src/app/[variants]/oauth/handoff/page.tsx +0 -13
@@ -196,6 +196,13 @@
196
196
  "when": 1752413805765,
197
197
  "tag": "0027_ai_image",
198
198
  "breakpoints": true
199
+ },
200
+ {
201
+ "idx": 28,
202
+ "version": "7",
203
+ "when": 1752567402506,
204
+ "tag": "0028_oauth_handoffs",
205
+ "breakpoints": true
199
206
  }
200
207
  ],
201
208
  "version": "6"
@@ -0,0 +1,94 @@
1
+ import { and, eq, lt, sql } from 'drizzle-orm';
2
+
3
+ import { LobeChatDatabase } from '@/database/type';
4
+
5
+ import { NewOAuthHandoff, OAuthHandoffItem, oauthHandoffs } from '../schemas';
6
+
7
+ export class OAuthHandoffModel {
8
+ private db: LobeChatDatabase;
9
+
10
+ constructor(db: LobeChatDatabase) {
11
+ this.db = db;
12
+ }
13
+
14
+ /**
15
+ * 创建新的认证凭证传递记录
16
+ * @param params 凭证数据
17
+ * @returns 创建的记录
18
+ */
19
+ create = async (params: NewOAuthHandoff): Promise<OAuthHandoffItem> => {
20
+ const [result] = await this.db
21
+ .insert(oauthHandoffs)
22
+ .values(params)
23
+ .onConflictDoNothing()
24
+ .returning();
25
+
26
+ return result;
27
+ };
28
+
29
+ /**
30
+ * 获取并消费认证凭证
31
+ * 该方法会先查询记录,如果找到则立即删除,确保凭证只能被使用一次
32
+ * @param id 凭证ID
33
+ * @param client 客户端类型
34
+ * @returns 凭证数据,如果不存在或已过期则返回null
35
+ */
36
+ fetchAndConsume = async (id: string, client: string): Promise<OAuthHandoffItem | null> => {
37
+ // 先查找记录,同时检查是否过期 (5分钟TTL)
38
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
39
+
40
+ const handoff = await this.db.query.oauthHandoffs.findFirst({
41
+ where: and(
42
+ eq(oauthHandoffs.id, id),
43
+ eq(oauthHandoffs.client, client),
44
+ // 检查记录是否在5分钟内创建
45
+ sql`${oauthHandoffs.createdAt} > ${fiveMinutesAgo}`,
46
+ ),
47
+ });
48
+
49
+ if (!handoff) {
50
+ return null;
51
+ }
52
+
53
+ // 立即删除记录以确保一次性使用
54
+ await this.db.delete(oauthHandoffs).where(eq(oauthHandoffs.id, id));
55
+
56
+ return handoff;
57
+ };
58
+
59
+ /**
60
+ * 清理过期的认证凭证记录
61
+ * 这个方法应该被定期调用(比如通过 cron job)来清理过期的记录
62
+ * @returns 清理的记录数量
63
+ */
64
+ cleanupExpired = async (): Promise<number> => {
65
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
66
+
67
+ const result = await this.db
68
+ .delete(oauthHandoffs)
69
+ .where(lt(oauthHandoffs.createdAt, fiveMinutesAgo));
70
+
71
+ return result.rowCount || 0;
72
+ };
73
+
74
+ /**
75
+ * 检查凭证是否存在(不消费)
76
+ * 主要用于测试和调试
77
+ * @param id 凭证ID
78
+ * @param client 客户端类型
79
+ * @returns 是否存在且未过期
80
+ */
81
+ exists = async (id: string, client: string): Promise<boolean> => {
82
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
83
+
84
+ const handoff = await this.db.query.oauthHandoffs.findFirst({
85
+ where: and(
86
+ eq(oauthHandoffs.id, id),
87
+ eq(oauthHandoffs.client, client),
88
+ sql`${oauthHandoffs.createdAt} > ${fiveMinutesAgo}`,
89
+ ),
90
+ });
91
+
92
+ return !!handoff;
93
+ };
94
+ }
@@ -23,7 +23,7 @@ describe('TableViewerRepo', () => {
23
23
  it('should return all tables with counts', async () => {
24
24
  const result = await repo.getAllTables();
25
25
 
26
- expect(result.length).toEqual(58);
26
+ expect(result.length).toEqual(59);
27
27
  expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' });
28
28
  });
29
29
 
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
2
  import { boolean, jsonb, pgTable, primaryKey, text, varchar } from 'drizzle-orm/pg-core';
3
+ import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
3
4
 
4
5
  import { timestamps, timestamptz } from './_helpers';
5
6
  import { users } from './user';
@@ -156,3 +157,48 @@ export const oidcConsents = pgTable(
156
157
  pk: primaryKey({ columns: [table.userId, table.clientId] }),
157
158
  }),
158
159
  );
160
+
161
+ /**
162
+ * 通用认证凭证传递表
163
+ * 用于在不同客户端(桌面端、浏览器插件、移动端等)之间安全传递认证凭证
164
+ *
165
+ * 工作流程:
166
+ * 1. 客户端生成唯一的 handoff ID
167
+ * 2. 将 handoff ID 作为参数附加到 OAuth redirect_uri
168
+ * 3. 认证成功后,中间页将凭证存储到此表
169
+ * 4. 客户端轮询此表获取凭证
170
+ * 5. 成功获取后立即删除记录
171
+ */
172
+ export const oauthHandoffs = pgTable('oauth_handoffs', {
173
+ /**
174
+ * 由客户端生成的一次性唯一标识符
175
+ * 用于客户端轮询时认领自己的凭证
176
+ */
177
+ id: text('id').primaryKey(),
178
+
179
+ /**
180
+ * 客户端类型标识
181
+ * 如: 'desktop', 'browser-extension', 'mobile-app' 等
182
+ */
183
+ client: varchar('client', { length: 50 }).notNull(),
184
+
185
+ /**
186
+ * 凭证数据的 JSON 载荷
187
+ * 灵活存储不同认证流程所需的各种数据
188
+ * 当前主要包含: { code: string; state: string }
189
+ */
190
+ payload: jsonb('payload').$type<Record<string, unknown>>().notNull(),
191
+
192
+ /**
193
+ * 时间戳字段,用于 TTL 控制
194
+ * 凭证应在创建后 5 分钟内被消费,否则视为过期
195
+ */
196
+ ...timestamps,
197
+ });
198
+
199
+ // Zod schemas for validation
200
+ export const insertAuthHandoffSchema = createInsertSchema(oauthHandoffs);
201
+ export const selectAuthHandoffSchema = createSelectSchema(oauthHandoffs);
202
+
203
+ export type OAuthHandoffItem = typeof oauthHandoffs.$inferSelect;
204
+ export type NewOAuthHandoff = typeof oauthHandoffs.$inferInsert;
@@ -1,24 +1,16 @@
1
1
  'use client';
2
2
 
3
3
  import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
4
- import { Button, Icon, Text } from '@lobehub/ui';
5
- import { createStyles, cx, keyframes } from 'antd-style';
6
- import { WifiIcon } from 'lucide-react';
7
- import { memo } from 'react';
4
+ import { Button, Highlighter, Icon, Text } from '@lobehub/ui';
5
+ import { createStyles } from 'antd-style';
6
+ import { ShieldX } from 'lucide-react';
7
+ import { memo, useState } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
9
10
 
10
11
  import { useElectronStore } from '@/store/electron';
11
12
 
12
- const airdropPulse = keyframes`
13
- 0% {
14
- transform: translate(-50%, -50%) scale(0.8);
15
- opacity: 0.5;
16
- }
17
- 100% {
18
- transform: translate(-50%, -50%) scale(2.5);
19
- opacity: 0;
20
- }
21
- `;
13
+ import WaitingAnim from './WaitingAnim';
22
14
 
23
15
  const useStyles = createStyles(({ css, token }) => ({
24
16
  container: css`
@@ -47,15 +39,15 @@ const useStyles = createStyles(({ css, token }) => ({
47
39
  color: ${token.colorTextSecondary} !important;
48
40
  `,
49
41
 
50
- helpLink: css`
51
- margin-inline-start: ${token.marginXXS}px;
52
- color: ${token.colorTextSecondary};
53
- text-decoration: underline;
54
- text-underline-offset: 2px;
42
+ errorIcon: css`
43
+ margin-block-end: ${token.marginXL}px;
44
+ color: ${token.colorError};
45
+ `,
55
46
 
56
- &:hover {
57
- color: ${token.colorText};
58
- }
47
+ errorMessage: css`
48
+ margin-block-end: ${token.marginXL}px !important;
49
+ color: ${token.colorError} !important;
50
+ text-align: center;
59
51
  `,
60
52
 
61
53
  helpText: css`
@@ -63,84 +55,7 @@ const useStyles = createStyles(({ css, token }) => ({
63
55
  font-size: ${token.fontSizeSM}px;
64
56
  color: ${token.colorTextTertiary};
65
57
  `,
66
- // 新增:图标和脉冲动画的容器
67
- iconContainer: css`
68
- position: relative;
69
-
70
- display: flex;
71
- align-items: center;
72
- justify-content: center;
73
-
74
- width: 160px;
75
- height: 160px;
76
- margin-block-end: ${token.marginXL}px;
77
- `,
78
-
79
- // 新增:不同延迟的脉冲动画
80
- pulse1: css`
81
- animation: ${airdropPulse} 3s ease-out infinite;
82
- `,
83
-
84
- pulse2: css`
85
- animation: ${airdropPulse} 3s ease-out 1.2s infinite;
86
- `,
87
58
 
88
- pulse3: css`
89
- animation: ${airdropPulse} 3s ease-out 1.8s infinite;
90
- `,
91
- // 新增:基础脉冲样式
92
- pulseBase: css`
93
- pointer-events: none;
94
- content: '';
95
-
96
- position: absolute;
97
- inset-block-start: 50%;
98
- inset-inline-start: 50%;
99
- transform: translate(-50%, -50%);
100
-
101
- width: 100px;
102
- height: 100px;
103
- border-radius: 50%;
104
-
105
- opacity: 0;
106
- background-color: ${token.colorPrimaryBgHover};
107
- `,
108
-
109
- // 新增:Radar 图标样式
110
- radarIcon: css`
111
- z-index: 1;
112
- color: ${token.colorPrimary};
113
- `,
114
-
115
- ring1: css`
116
- width: 80px;
117
- height: 80px;
118
- border: 1px solid ${token.colorText};
119
- `,
120
-
121
- ring2: css`
122
- width: 120px;
123
- height: 120px;
124
- border: 1px solid ${token.colorTextQuaternary};
125
- `,
126
-
127
- ring3: css`
128
- width: 160px;
129
- height: 160px;
130
- border: 1px solid ${token.colorFillSecondary};
131
- `,
132
-
133
- // 新增:星环基础样式
134
- ringBase: css`
135
- pointer-events: none;
136
-
137
- position: absolute;
138
- inset-block-start: 50%;
139
- inset-inline-start: 50%;
140
- transform: translate(-50%, -50%);
141
-
142
- border-radius: 50%;
143
- `,
144
59
  title: css`
145
60
  margin-block-end: ${token.marginSM}px !important;
146
61
  color: ${token.colorText} !important;
@@ -151,46 +66,75 @@ interface WaitingOAuthProps {
151
66
  setIsOpen: (open: boolean) => void;
152
67
  setWaiting: (waiting: boolean) => void;
153
68
  }
69
+
154
70
  const WaitingOAuth = memo<WaitingOAuthProps>(({ setWaiting, setIsOpen }) => {
155
71
  const { styles } = useStyles();
156
- const { t } = useTranslation('electron'); // 指定 namespace 为 electron
157
- const [disconnect, refreshServerConfig] = useElectronStore((s) => [
72
+ const { t } = useTranslation('electron');
73
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
74
+ const [disconnect, refreshServerConfig, connectRemoteServer] = useElectronStore((s) => [
158
75
  s.disconnectRemoteServer,
159
76
  s.refreshServerConfig,
77
+ s.connectRemoteServer,
160
78
  ]);
161
79
 
162
80
  const handleCancel = async () => {
163
81
  await disconnect();
164
82
  setWaiting(false);
83
+ setErrorMessage(null);
84
+ };
85
+
86
+ const handleRetry = async () => {
87
+ setErrorMessage(null);
88
+ const { dataSyncConfig } = useElectronStore.getState();
89
+ await connectRemoteServer(dataSyncConfig);
165
90
  };
166
91
 
167
92
  useWatchBroadcast('authorizationSuccessful', async () => {
168
93
  setIsOpen(false);
169
94
  setWaiting(false);
95
+ setErrorMessage(null);
170
96
  await refreshServerConfig();
171
97
  });
172
98
 
99
+ useWatchBroadcast('authorizationFailed', ({ error }) => {
100
+ setErrorMessage(error);
101
+ });
102
+
103
+ // 错误状态
104
+ if (errorMessage) {
105
+ return (
106
+ <div className={styles.container}>
107
+ <Flexbox className={styles.content} gap={12}>
108
+ <Flexbox align={'center'}>
109
+ <Icon className={styles.errorIcon} icon={ShieldX} size={64} />
110
+ <Text as={'h4'} className={styles.title}>
111
+ {t('waitingOAuth.errorTitle')}
112
+ </Text>
113
+ </Flexbox>
114
+ <Highlighter language={'log'} style={{ maxHeight: 500, maxWidth: 800, overflow: 'auto' }}>
115
+ {errorMessage}
116
+ </Highlighter>
117
+ <Flexbox gap={12} horizontal>
118
+ <Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>
119
+ <Button onClick={handleRetry} type="primary">
120
+ {t('waitingOAuth.retry')}
121
+ </Button>
122
+ </Flexbox>
123
+ </Flexbox>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // 正常等待状态
173
129
  return (
174
130
  <div className={styles.container}>
175
131
  <div className={styles.content}>
176
- {/* 更新为新的图标和脉冲动画结构 */}
177
- <div className={styles.iconContainer}>
178
- {/* 新增:星环 */}
179
- <div className={cx(styles.ringBase, styles.ring1)} />
180
- <div className={cx(styles.ringBase, styles.ring2)} />
181
- <div className={cx(styles.ringBase, styles.ring3)} />
182
- {/* 脉冲 */}
183
- <div className={cx(styles.pulseBase, styles.pulse1)} />
184
- <div className={cx(styles.pulseBase, styles.pulse2)} />
185
- <div className={cx(styles.pulseBase, styles.pulse3)} />
186
-
187
- <Icon className={styles.radarIcon} icon={WifiIcon} size={40} />
188
- </div>
132
+ <WaitingAnim />
189
133
  <Text as={'h4'} className={styles.title}>
190
134
  {t('waitingOAuth.title')}
191
135
  </Text>
192
136
  <Text className={styles.description}>{t('waitingOAuth.description')}</Text>
193
- <Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>{' '}
137
+ <Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>
194
138
  <Text className={styles.helpText}>{t('waitingOAuth.helpText')}</Text>
195
139
  </div>
196
140
  </div>
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { Icon } from '@lobehub/ui';
4
+ import { createStyles, cx, keyframes } from 'antd-style';
5
+ import { WifiIcon } from 'lucide-react';
6
+ import { memo } from 'react';
7
+
8
+ const airdropPulse = keyframes`
9
+ 0% {
10
+ transform: translate(-50%, -50%) scale(0.8);
11
+ opacity: 0.5;
12
+ }
13
+ 100% {
14
+ transform: translate(-50%, -50%) scale(2.5);
15
+ opacity: 0;
16
+ }
17
+ `;
18
+
19
+ const useStyles = createStyles(({ css, token }) => ({
20
+ container: css`
21
+ position: relative;
22
+
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+
27
+ width: 160px;
28
+ height: 160px;
29
+ margin-block-end: ${token.marginXL}px;
30
+ `,
31
+
32
+ pulse1: css`
33
+ animation: ${airdropPulse} 3s ease-out infinite;
34
+ `,
35
+
36
+ pulse2: css`
37
+ animation: ${airdropPulse} 3s ease-out 1.2s infinite;
38
+ `,
39
+
40
+ pulse3: css`
41
+ animation: ${airdropPulse} 3s ease-out 1.8s infinite;
42
+ `,
43
+ pulseBase: css`
44
+ pointer-events: none;
45
+ content: '';
46
+
47
+ position: absolute;
48
+ inset-block-start: 50%;
49
+ inset-inline-start: 50%;
50
+ transform: translate(-50%, -50%);
51
+
52
+ width: 100px;
53
+ height: 100px;
54
+ border-radius: 50%;
55
+
56
+ opacity: 0;
57
+ background-color: ${token.colorPrimaryBgHover};
58
+ `,
59
+
60
+ radarIcon: css`
61
+ z-index: 1;
62
+ color: ${token.colorPrimary};
63
+ `,
64
+
65
+ ring1: css`
66
+ width: 80px;
67
+ height: 80px;
68
+ border: 1px solid ${token.colorText};
69
+ `,
70
+
71
+ ring2: css`
72
+ width: 120px;
73
+ height: 120px;
74
+ border: 1px solid ${token.colorTextQuaternary};
75
+ `,
76
+
77
+ ring3: css`
78
+ width: 160px;
79
+ height: 160px;
80
+ border: 1px solid ${token.colorFillSecondary};
81
+ `,
82
+
83
+ ringBase: css`
84
+ pointer-events: none;
85
+
86
+ position: absolute;
87
+ inset-block-start: 50%;
88
+ inset-inline-start: 50%;
89
+ transform: translate(-50%, -50%);
90
+
91
+ border-radius: 50%;
92
+ `,
93
+ }));
94
+
95
+ const WaitingAnim = memo(() => {
96
+ const { styles } = useStyles();
97
+
98
+ return (
99
+ <div className={styles.container}>
100
+ {/* 新增:星环 */}
101
+ <div className={cx(styles.ringBase, styles.ring1)} />
102
+ <div className={cx(styles.ringBase, styles.ring2)} />
103
+ <div className={cx(styles.ringBase, styles.ring3)} />
104
+ {/* 脉冲 */}
105
+ <div className={cx(styles.pulseBase, styles.pulse1)} />
106
+ <div className={cx(styles.pulseBase, styles.pulse2)} />
107
+ <div className={cx(styles.pulseBase, styles.pulse3)} />
108
+
109
+ <Icon className={styles.radarIcon} icon={WifiIcon} size={40} />
110
+ </div>
111
+ );
112
+ });
113
+
114
+ export default WaitingAnim;
@@ -1,11 +1,14 @@
1
1
  import { ClientMetadata } from 'oidc-provider';
2
+ import urlJoin from 'url-join';
3
+
4
+ import { appEnv } from '@/envs/app';
2
5
 
3
6
  /**
4
7
  * 默认 OIDC 客户端配置
5
8
  */
6
9
  export const defaultClients: ClientMetadata[] = [
7
10
  {
8
- application_type: 'native',
11
+ application_type: 'web',
9
12
  client_id: 'lobehub-desktop',
10
13
  client_name: 'LobeHub Desktop',
11
14
  // 仅支持授权码流程
@@ -13,19 +16,17 @@ export const defaultClients: ClientMetadata[] = [
13
16
 
14
17
  logo_uri: 'https://hub-apac-1.lobeobjects.space/lobehub-desktop-icon.png',
15
18
 
16
- // 桌面端注册的自定义协议回调(使用反向域名格式)
17
19
  post_logout_redirect_uris: [
18
- 'com.lobehub.lobehub-desktop-dev://auth/logout/callback',
19
- 'com.lobehub.lobehub-desktop-nightly://auth/logout/callback',
20
- 'com.lobehub.lobehub-desktop-beta://auth/logout/callback',
21
- 'com.lobehub.lobehub-desktop://auth/logout/callback',
20
+ // 动态构建 Web 页面回调 URL
21
+ urlJoin(appEnv.APP_URL!, '/oauth/logout'),
22
+ 'http://localhost:3210/oauth/logout',
22
23
  ],
23
24
 
25
+ // 桌面端授权回调 - 改为 Web 页面路径
24
26
  redirect_uris: [
25
- 'com.lobehub.lobehub-desktop-dev://auth/callback',
26
- 'com.lobehub.lobehub-desktop-nightly://auth/callback',
27
- 'com.lobehub.lobehub-desktop-beta://auth/callback',
28
- 'com.lobehub.lobehub-desktop://auth/callback',
27
+ // 动态构建 Web 页面回调 URL
28
+ urlJoin(appEnv.APP_URL!, '/oidc/callback/desktop'),
29
+ 'http://localhost:3210/oidc/callback/desktop',
29
30
  ],
30
31
 
31
32
  // 支持授权码获取令牌和刷新令牌
@@ -40,16 +41,14 @@ export const defaultClients: ClientMetadata[] = [
40
41
  * OIDC Scopes 定义
41
42
  */
42
43
  export const defaultScopes = [
43
- 'openid', // OIDC 必须
44
- 'profile', // 请求用户信息(姓名、头像等)
45
- 'email', // 请求用户邮箱
46
- 'offline_access', // 请求 Refresh Token
47
- 'sync:read', // 自定义 Scope:读取同步数据权限
48
- 'sync:write', // 自定义 Scope:写入同步数据权限
44
+ 'openid',
45
+ 'profile',
46
+ 'email',
47
+ 'offline_access', // 允许获取 refresh_token
49
48
  ];
50
49
 
51
50
  /**
52
- * OIDC Claims 定义 (与 Scopes 关联)
51
+ * OIDC Claims 定义
53
52
  */
54
53
  export const defaultClaims = {
55
54
  email: ['email', 'email_verified'],