@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
@@ -0,0 +1,135 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import debug from 'debug';
3
+ import { importJWK, jwtVerify } from 'jose';
4
+
5
+ import { oidcEnv } from '@/envs/oidc';
6
+
7
+ const log = debug('oidc-jwt');
8
+
9
+ /**
10
+ * 从环境变量中获取 JWKS
11
+ * 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
12
+ */
13
+ export const getJWKS = (): object => {
14
+ try {
15
+ const jwksString = oidcEnv.OIDC_JWKS_KEY;
16
+
17
+ if (!jwksString) {
18
+ throw new Error(
19
+ 'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
20
+ );
21
+ }
22
+
23
+ // 尝试解析 JWKS JSON 字符串
24
+ const jwks = JSON.parse(jwksString);
25
+
26
+ // 检查 JWKS 格式是否正确
27
+ if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
28
+ throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
29
+ }
30
+
31
+ // 检查是否有 RS256 算法的密钥
32
+ const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
33
+ if (!hasRS256Key) {
34
+ throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
35
+ }
36
+
37
+ return jwks;
38
+ } catch (error) {
39
+ console.error('解析 JWKS 失败:', error);
40
+ throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
41
+ }
42
+ };
43
+
44
+ /**
45
+ * 从环境变量中获取 JWKS 并提取第一个 RSA 密钥
46
+ */
47
+ const getJWKSPublicKey = async () => {
48
+ try {
49
+ const jwksString = oidcEnv.OIDC_JWKS_KEY;
50
+
51
+ if (!jwksString) {
52
+ throw new Error('OIDC_JWKS_KEY 环境变量未设置');
53
+ }
54
+
55
+ const jwks = JSON.parse(jwksString);
56
+
57
+ if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
58
+ throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
59
+ }
60
+
61
+ // 查找 RS256 算法的 RSA 密钥
62
+ const rsaKey = jwks.keys.find((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
63
+
64
+ if (!rsaKey) {
65
+ throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
66
+ }
67
+
68
+ // 导入 JWK 为公钥
69
+ const publicKey = await importJWK(rsaKey, 'RS256');
70
+
71
+ return publicKey;
72
+ } catch (error) {
73
+ log('获取 JWKS 公钥失败: %O', error);
74
+ throw new Error(`JWKS 公钥获取失败: ${(error as Error).message}`);
75
+ }
76
+ };
77
+
78
+ /**
79
+ * 验证 OIDC JWT Access Token
80
+ * @param token - JWT access token
81
+ * @returns 解析后的 token payload 和用户信息
82
+ */
83
+ export const validateOIDCJWT = async (token: string) => {
84
+ try {
85
+ log('开始验证 OIDC JWT token');
86
+
87
+ // 获取公钥
88
+ const publicKey = await getJWKSPublicKey();
89
+
90
+ // 验证 JWT
91
+ const { payload } = await jwtVerify(token, publicKey, {
92
+ algorithms: ['RS256'],
93
+ // 可以添加其他验证选项,如 issuer、audience 等
94
+ });
95
+
96
+ log('JWT 验证成功,payload: %O', payload);
97
+
98
+ // 提取用户信息
99
+ const userId = payload.sub;
100
+ const clientId = payload.aud;
101
+
102
+ if (!userId) {
103
+ throw new TRPCError({
104
+ code: 'UNAUTHORIZED',
105
+ message: 'JWT token 中缺少用户 ID (sub)',
106
+ });
107
+ }
108
+
109
+ return {
110
+ clientId,
111
+ payload,
112
+ tokenData: {
113
+ aud: clientId,
114
+ client_id: clientId,
115
+ exp: payload.exp,
116
+ iat: payload.iat,
117
+ jti: payload.jti,
118
+ scope: payload.scope,
119
+ sub: userId,
120
+ },
121
+ userId,
122
+ };
123
+ } catch (error) {
124
+ if (error instanceof TRPCError) {
125
+ throw error;
126
+ }
127
+
128
+ log('JWT 验证失败: %O', error);
129
+
130
+ throw new TRPCError({
131
+ code: 'UNAUTHORIZED',
132
+ message: `JWT token 验证失败: ${(error as Error).message}`,
133
+ });
134
+ }
135
+ };
@@ -1,12 +1,12 @@
1
1
  import debug from 'debug';
2
- import Provider, { Configuration, KoaContextWithOIDC } from 'oidc-provider';
2
+ import Provider, { Configuration, KoaContextWithOIDC, errors } from 'oidc-provider';
3
3
  import urlJoin from 'url-join';
4
4
 
5
5
  import { serverDBEnv } from '@/config/db';
6
6
  import { UserModel } from '@/database/models/user';
7
7
  import { LobeChatDatabase } from '@/database/type';
8
8
  import { appEnv } from '@/envs/app';
9
- import { oidcEnv } from '@/envs/oidc';
9
+ import { getJWKS } from '@/libs/oidc-provider/jwt';
10
10
 
11
11
  import { DrizzleAdapter } from './adapter';
12
12
  import { defaultClaims, defaultClients, defaultScopes } from './config';
@@ -14,40 +14,7 @@ import { createInteractionPolicy } from './interaction-policy';
14
14
 
15
15
  const logProvider = debug('lobe-oidc:provider'); // <--- 添加 provider 日志实例
16
16
 
17
- /**
18
- * 从环境变量中获取 JWKS
19
- * 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
20
- */
21
- const getJWKS = (): object => {
22
- try {
23
- const jwksString = oidcEnv.OIDC_JWKS_KEY;
24
-
25
- if (!jwksString) {
26
- throw new Error(
27
- 'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
28
- );
29
- }
30
-
31
- // 尝试解析 JWKS JSON 字符串
32
- const jwks = JSON.parse(jwksString);
33
-
34
- // 检查 JWKS 格式是否正确
35
- if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
36
- throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
37
- }
38
-
39
- // 检查是否有 RS256 算法的密钥
40
- const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
41
- if (!hasRS256Key) {
42
- throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
43
- }
44
-
45
- return jwks;
46
- } catch (error) {
47
- console.error('解析 JWKS 失败:', error);
48
- throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
49
- }
50
- };
17
+ export const API_AUDIENCE = 'urn:lobehub:chat'; // <-- 把这里换成你自己的 API 标识符
51
18
 
52
19
  /**
53
20
  * 获取 Cookie 密钥,使用 KEY_VAULTS_SECRET
@@ -123,7 +90,24 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
123
90
  devInteractions: { enabled: false },
124
91
  deviceFlow: { enabled: false },
125
92
  introspection: { enabled: true },
126
- resourceIndicators: { enabled: false },
93
+ resourceIndicators: {
94
+ defaultResource: () => API_AUDIENCE,
95
+ enabled: true,
96
+ getResourceServerInfo: (ctx, resourceIndicator) => {
97
+ logProvider('getResourceServerInfo called with indicator: %s', resourceIndicator); // <-- 添加这行日志
98
+ if (resourceIndicator === API_AUDIENCE) {
99
+ logProvider('Indicator matches API_AUDIENCE, returning JWT config.'); // <-- 添加这行日志
100
+ return {
101
+ accessTokenFormat: 'jwt',
102
+ audience: API_AUDIENCE,
103
+ scope: ctx.oidc.client?.scope || 'read',
104
+ };
105
+ }
106
+
107
+ logProvider('Indicator does not match API_AUDIENCE, throwing InvalidTarget.'); // <-- 添加这行日志
108
+ throw new errors.InvalidTarget();
109
+ },
110
+ },
127
111
  revocation: { enabled: true },
128
112
  rpInitiatedLogout: { enabled: true },
129
113
  userinfo: { enabled: true },
@@ -256,7 +240,7 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
256
240
 
257
241
  // 8. 令牌有效期
258
242
  ttl: {
259
- AccessToken: 7 * 24 * 60 * 60, // 1 week temporarily,need to revert 1 hour with better implement
243
+ AccessToken: 25 * 3600, // 25 hour
260
244
  AuthorizationCode: 600, // 10 minutes
261
245
  DeviceCode: 600, // 10 minutes (if enabled)
262
246
 
@@ -3,8 +3,7 @@ import superjson from 'superjson';
3
3
 
4
4
  import { isDesktop } from '@/const/version';
5
5
  import { AsyncRouter } from '@/server/routers/async';
6
-
7
- import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
6
+ import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
8
7
 
9
8
  export const asyncClient = createTRPCClient<AsyncRouter>({
10
9
  links: [
@@ -4,8 +4,7 @@ import superjson from 'superjson';
4
4
  import { isDesktop } from '@/const/version';
5
5
  import type { EdgeRouter } from '@/server/routers/edge';
6
6
  import { withBasePath } from '@/utils/basePath';
7
-
8
- import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
7
+ import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
9
8
 
10
9
  export const edgeClient = createTRPCClient<EdgeRouter>({
11
10
  links: [
@@ -15,7 +15,7 @@ const links = [
15
15
  httpBatchLink({
16
16
  fetch: async (input, init) => {
17
17
  if (isDesktop) {
18
- const { desktopRemoteRPCFetch } = await import('./helpers/desktopRemoteRPCFetch');
18
+ const { desktopRemoteRPCFetch } = await import('@/utils/electron/desktopRemoteRPCFetch');
19
19
 
20
20
  const res = await desktopRemoteRPCFetch(input as string, init);
21
21
 
@@ -3,8 +3,7 @@ import superjson from 'superjson';
3
3
 
4
4
  import { isDesktop } from '@/const/version';
5
5
  import type { ToolsRouter } from '@/server/routers/tools';
6
-
7
- import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
6
+ import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
8
7
 
9
8
  export const toolsClient = createTRPCClient<ToolsRouter>({
10
9
  links: [
@@ -6,7 +6,7 @@ import { NextRequest } from 'next/server';
6
6
  import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
7
7
  import { oidcEnv } from '@/envs/oidc';
8
8
  import { ClerkAuth, IClerkAuth } from '@/libs/clerk-auth';
9
- import { extractBearerToken } from '@/utils/server/auth';
9
+ import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
10
10
 
11
11
  // Create context logger namespace
12
12
  const log = debug('lobe-trpc:lambda:context');
@@ -98,26 +98,19 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
98
98
  let auth;
99
99
  let oidcAuth = null;
100
100
 
101
- // Prioritize checking the standard Authorization header for OIDC Bearer Token validation
101
+ // Prioritize checking for OIDC authentication (both standard Authorization and custom Oidc-Auth headers)
102
102
  if (oidcEnv.ENABLE_OIDC) {
103
103
  log('OIDC enabled, attempting OIDC authentication');
104
104
  const standardAuthorization = request.headers.get('Authorization');
105
+ const oidcAuthToken = request.headers.get('Oidc-Auth');
105
106
  log('Standard Authorization header: %s', standardAuthorization ? 'exists' : 'not found');
107
+ log('Oidc-Auth header: %s', oidcAuthToken ? 'exists' : 'not found');
106
108
 
107
109
  try {
108
- // Use extractBearerToken from utils
109
- const bearerToken = extractBearerToken(standardAuthorization);
110
-
111
- log('Extracted Bearer Token: %s', bearerToken ? 'valid' : 'invalid');
112
- if (bearerToken) {
113
- const { OIDCService } = await import('@/server/services/oidc');
114
-
115
- // Initialize OIDC service
116
- log('Initializing OIDC service');
117
- const oidcService = await OIDCService.initialize();
118
- // Validate token using OIDCService
119
- log('Validating OIDC token');
120
- const tokenInfo = await oidcService.validateToken(bearerToken);
110
+ if (oidcAuthToken) {
111
+ // Use direct JWT validation instead of database lookup
112
+ const tokenInfo = await validateOIDCJWT(oidcAuthToken);
113
+
121
114
  oidcAuth = {
122
115
  payload: tokenInfo.tokenData,
123
116
  ...tokenInfo.tokenData, // Spread payload into oidcAuth
@@ -136,7 +129,7 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
136
129
  }
137
130
  } catch (error) {
138
131
  // If OIDC authentication fails, log error and continue with other authentication methods
139
- if (standardAuthorization?.startsWith('Bearer ')) {
132
+ if (oidcAuthToken) {
140
133
  log('OIDC authentication failed, error: %O', error);
141
134
  console.error('OIDC authentication failed, trying other methods:', error);
142
135
  }
@@ -101,7 +101,10 @@ const electron = {
101
101
  waitingOAuth: {
102
102
  cancel: '取消',
103
103
  description: '浏览器已打开授权页面,请在浏览器中完成授权',
104
+ error: '授权失败: {{error}}',
105
+ errorTitle: '授权连接失败',
104
106
  helpText: '如果浏览器没有自动打开,请点击取消后重新尝试',
107
+ retry: '重试',
105
108
  title: '等待授权连接',
106
109
  },
107
110
  };
@@ -29,10 +29,14 @@ const oauth = {
29
29
  },
30
30
  title: '授权 {{clientName}}',
31
31
  },
32
- failed: {
32
+ error: {
33
33
  backToHome: '返回首页',
34
- subTitle: '您已拒绝授权应用访问您的 LobeChat 账户',
35
- title: '授权被拒绝',
34
+ desc: 'OAuth 授权失败,失败原因:{{reason}}',
35
+ reason: {
36
+ internal_error: '服务端错误',
37
+ invalid_request: '无效的请求参数',
38
+ },
39
+ title: '授权失败',
36
40
  },
37
41
  handoff: {
38
42
  desc: {
@@ -51,7 +55,7 @@ const oauth = {
51
55
  userWelcome: '欢迎回来,',
52
56
  },
53
57
  success: {
54
- subTitle: '您已成功授权应用访问您的 LobeChat 账户,可以关闭该页面了',
58
+ subTitle: '您已成功授权应用访问您的账户,可以关闭该页面了',
55
59
  title: '授权成功',
56
60
  },
57
61
  };
package/src/middleware.ts CHANGED
@@ -18,9 +18,9 @@ import { OAUTH_AUTHORIZED } from './const/auth';
18
18
  import { oidcEnv } from './envs/oidc';
19
19
 
20
20
  // Create debug logger instances
21
- const logDefault = debug('lobe-middleware:default');
22
- const logNextAuth = debug('lobe-middleware:next-auth');
23
- const logClerk = debug('lobe-middleware:clerk');
21
+ const logDefault = debug('middleware:default');
22
+ const logNextAuth = debug('middleware:next-auth');
23
+ const logClerk = debug('middleware:clerk');
24
24
 
25
25
  // OIDC session pre-sync constant
26
26
  const OIDC_SESSION_HEADER = 'x-oidc-session-sync';
@@ -146,13 +146,19 @@ const defaultMiddleware = (request: NextRequest) => {
146
146
  };
147
147
 
148
148
  const isPublicRoute = createRouteMatcher([
149
+ // backend api
149
150
  '/api/auth(.*)',
151
+ '/api/webhooks(.*)',
152
+ '/webapi(.*)',
150
153
  '/trpc(.*)',
151
154
  // next auth
152
155
  '/next-auth/(.*)',
153
156
  // clerk
154
157
  '/login',
155
158
  '/signup',
159
+ // oauth
160
+ '/oidc/handoff',
161
+ '/oidc/token',
156
162
  ]);
157
163
 
158
164
  const isProtectedRoute = createRouteMatcher([
@@ -164,7 +170,7 @@ const isProtectedRoute = createRouteMatcher([
164
170
  ]);
165
171
 
166
172
  // Initialize an Edge compatible NextAuth middleware
167
- const nextAuthMiddleware = NextAuthEdge.auth((req) => {
173
+ const nextAuthMiddleware = NextAuthEdge.auth(async (req) => {
168
174
  logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url);
169
175
 
170
176
  const response = defaultMiddleware(req);
@@ -1,4 +1,3 @@
1
- import { TRPCError } from '@trpc/server';
2
1
  import debug from 'debug';
3
2
 
4
3
  import { createContextForInteractionDetails } from '@/libs/oidc-provider/http-adapter';
@@ -20,76 +19,6 @@ export class OIDCService {
20
19
  return new OIDCService(provider);
21
20
  }
22
21
 
23
- /**
24
- * 验证 OIDC Bearer Token 并返回用户信息
25
- * 使用 oidc-provider 实例的 AccessToken.find 方法验证 token
26
- *
27
- * @param token - Bearer Token
28
- * @returns 包含用户ID和Token数据的对象
29
- * @throws 如果token无效或OIDC未启用则抛出 TRPCError
30
- */
31
- async validateToken(token: string) {
32
- try {
33
- log('Validating access token using AccessToken.find');
34
-
35
- // 使用 oidc-provider 的 AccessToken 查找和验证方法
36
- const accessToken = await this.provider.AccessToken.find(token);
37
-
38
- if (!accessToken) {
39
- log('Access token not found, expired, or consumed');
40
- throw new TRPCError({
41
- code: 'UNAUTHORIZED',
42
- message: 'Access token 无效、已过期或已被使用',
43
- });
44
- }
45
-
46
- // 从 accessToken 实例中获取必要的数据
47
- // 注意:accessToken 没有 payload() 方法,而是直接访问其属性
48
- const userId = accessToken.accountId; // 用户 ID 通常存储在 accountId 属性中
49
- const clientId = accessToken.clientId;
50
-
51
- // 如果需要更多的声明信息,可以从 accessToken 的其他属性中获取
52
- // 例如,scopes、claims、exp 等
53
- const tokenData = {
54
- client_id: clientId,
55
- exp: accessToken.exp,
56
- iat: accessToken.iat,
57
- jti: accessToken.jti,
58
- scope: accessToken.scope,
59
- // OIDC 标准中,sub 字段表示用户 ID
60
- sub: userId,
61
- };
62
-
63
- if (!userId) {
64
- log('Access token does not contain user ID (accountId)');
65
- throw new TRPCError({
66
- code: 'UNAUTHORIZED',
67
- message: 'Access token 中未包含用户 ID',
68
- });
69
- }
70
-
71
- log('Access token validated successfully for user: %s', userId);
72
- return {
73
- // 包含 token 原始数据,可用于获取更多信息
74
- accessToken,
75
- // 构建的 token 数据对象
76
- tokenData,
77
- // 用户 ID
78
- userId,
79
- };
80
- } catch (error) {
81
- if (error instanceof TRPCError) throw error;
82
-
83
- // AccessToken.find 可能抛出特定错误
84
- log('Error validating access token with AccessToken.find: %O', error);
85
- console.error('OIDC 令牌验证错误:', error);
86
- throw new TRPCError({
87
- code: 'UNAUTHORIZED',
88
- message: `OIDC 认证失败: ${(error as Error).message}`,
89
- });
90
- }
91
- }
92
-
93
22
  async getInteractionDetails(uid: string) {
94
23
  const { req, res } = await createContextForInteractionDetails(uid);
95
24
  return this.provider.interactionDetails(req, res);
@@ -40,6 +40,7 @@ import { ChatImageItem, ChatMessage, MessageToolCall } from '@/types/message';
40
40
  import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
41
41
  import { UserMessageContentPart } from '@/types/openai/chat';
42
42
  import { parsePlaceholderVariablesMessages } from '@/utils/client/parserPlaceholder';
43
+ import { fetchWithInvokeStream } from '@/utils/electron/desktopRemoteRPCFetch';
43
44
  import { createErrorResponse } from '@/utils/errorResponse';
44
45
  import {
45
46
  FetchSSEOptions,
@@ -361,7 +362,10 @@ class ChatService {
361
362
 
362
363
  let fetcher: typeof fetch | undefined = undefined;
363
364
 
364
- if (enableFetchOnClient) {
365
+ // Add desktop remote RPC fetch support
366
+ if (isDesktop) {
367
+ fetcher = fetchWithInvokeStream;
368
+ } else if (enableFetchOnClient) {
365
369
  /**
366
370
  * Notes:
367
371
  * 1. Browser agent runtime will skip auth check if a key and endpoint provided by
@@ -28,13 +28,6 @@ class RemoteServerService {
28
28
  requestAuthorization = async (config: DataSyncConfig) => {
29
29
  return dispatch('requestAuthorization', config);
30
30
  };
31
-
32
- /**
33
- * 刷新访问令牌
34
- */
35
- refreshAccessToken = async () => {
36
- return dispatch('refreshAccessToken');
37
- };
38
31
  }
39
32
 
40
33
  export const remoteServerService = new RemoteServerService();
@@ -1,4 +1,4 @@
1
- import { ProxyTRPCRequestParams, dispatch } from '@lobechat/electron-client-ipc';
1
+ import { ProxyTRPCRequestParams, dispatch, streamInvoke } from '@lobechat/electron-client-ipc';
2
2
  import debug from 'debug';
3
3
 
4
4
  import { isDesktop } from '@/const/version';
@@ -6,7 +6,7 @@ import { getElectronStoreState } from '@/store/electron';
6
6
  import { electronSyncSelectors } from '@/store/electron/selectors';
7
7
  import { getRequestBody, headersToRecord } from '@/utils/fetch';
8
8
 
9
- const log = debug('lobe-lambda:desktopRemoteRPCFetch');
9
+ const log = debug('utils:desktopRemoteRPCFetch');
10
10
 
11
11
  // eslint-disable-next-line no-undef
12
12
  export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) => {
@@ -15,8 +15,8 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
15
15
 
16
16
  if (isSyncActive) {
17
17
  log('Using IPC proxy for tRPC request');
18
+ const url = input as string;
18
19
  try {
19
- const url = input as string;
20
20
  const parsedUrl = new URL(url, window.location.origin);
21
21
  const urlPath = parsedUrl.pathname + parsedUrl.search;
22
22
  const method = init?.method?.toUpperCase() || 'GET';
@@ -32,7 +32,7 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
32
32
 
33
33
  const ipcResult = await dispatch('proxyTRPCRequest', params);
34
34
 
35
- log('Received IPC proxy response:', { status: ipcResult.status });
35
+ log(`Received ${url} IPC proxy response:`, { status: ipcResult.status });
36
36
  const response = new Response(ipcResult.body, {
37
37
  headers: ipcResult.headers,
38
38
  status: ipcResult.status,
@@ -41,7 +41,7 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
41
41
 
42
42
  if (!response.ok) {
43
43
  console.warn(
44
- '[lambda] IPC proxy response indicates an error:',
44
+ `[lambda] ${url} IPC proxy response indicates an error:`,
45
45
  response.status,
46
46
  response.statusText,
47
47
  );
@@ -49,7 +49,7 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
49
49
 
50
50
  return response;
51
51
  } catch (error) {
52
- console.error('[lambda] Error during IPC proxy call:', error);
52
+ console.error(`[lambda] Error during ${url} IPC proxy call:`, error);
53
53
  return new Response(
54
54
  `IPC Proxy Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
55
55
  {
@@ -62,11 +62,26 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
62
62
  };
63
63
 
64
64
  // eslint-disable-next-line no-undef
65
- export const fetchWithDesktopRemoteRPC = async (input: string, init?: RequestInit) => {
65
+ export const fetchWithDesktopRemoteRPC = (async (input: RequestInfo | URL, init?: RequestInit) => {
66
66
  if (isDesktop) {
67
67
  const res = await desktopRemoteRPCFetch(input as string, init);
68
68
  if (res) return res;
69
69
  }
70
70
 
71
+ return fetch(input, init);
72
+ }) as typeof fetch;
73
+
74
+ // eslint-disable-next-line no-undef
75
+ export const fetchWithInvokeStream = async (input: RequestInfo | URL, init?: RequestInit) => {
76
+ if (isDesktop) {
77
+ const isSyncActive = electronSyncSelectors.isSyncActive(getElectronStoreState());
78
+ log('isSyncActive:', isSyncActive);
79
+ if (isSyncActive) {
80
+ log('Using IPC stream proxy for request to:', input);
81
+
82
+ return streamInvoke(input, init);
83
+ }
84
+ }
85
+
71
86
  return fetch(input, init);
72
87
  };
@@ -49,3 +49,25 @@ export const extractBearerToken = (authHeader?: string | null): string | null =>
49
49
  // Return the token only if it's not an empty string after trimming
50
50
  return token || null;
51
51
  };
52
+
53
+ /**
54
+ * 从 Oidc-Auth header 中提取 JWT token
55
+ * @param authHeader - Oidc-Auth header 值 (例如 "Oidc-Auth xxx")
56
+ * @returns JWT token 或 null(如果授权头无效或不存在)
57
+ */
58
+ export const extractOidcAuthToken = (authHeader?: string | null): string | null => {
59
+ if (!authHeader) return null;
60
+
61
+ const trimmedHeader = authHeader.trim(); // Trim leading/trailing spaces
62
+
63
+ // Check if it starts with 'Oidc-Auth ' (case-insensitive check)
64
+ if (!trimmedHeader.toLowerCase().startsWith('oidc-auth ')) {
65
+ return null;
66
+ }
67
+
68
+ // Extract the token part after "Oidc-Auth " and trim potential spaces around the token itself
69
+ const token = trimmedHeader.slice(10).trim(); // 'Oidc-Auth ' length is 10
70
+
71
+ // Return the token only if it's not an empty string after trimming
72
+ return token || null;
73
+ };
@@ -1,36 +0,0 @@
1
- 'use client';
2
-
3
- import { Button, Icon } from '@lobehub/ui';
4
- import { Card, Result } from 'antd';
5
- import { XCircle } from 'lucide-react';
6
- import Link from 'next/link';
7
- import React, { memo } from 'react';
8
- import { useTranslation } from 'react-i18next';
9
- import { Center } from 'react-layout-kit';
10
-
11
- const FailedPage = memo(() => {
12
- const { t } = useTranslation('oauth');
13
-
14
- return (
15
- <Center height="100vh">
16
- <Card style={{ maxWidth: 500, width: '100%' }}>
17
- <Result
18
- extra={
19
- <Link href="/">
20
- <Button type="primary">{t('failed.backToHome')}</Button>
21
- </Link>
22
- }
23
- icon={<Icon icon={XCircle} />}
24
- status="error"
25
- style={{ padding: 0 }}
26
- subTitle={t('failed.subTitle')}
27
- title={t('failed.title')}
28
- />
29
- </Card>
30
- </Center>
31
- );
32
- });
33
-
34
- FailedPage.displayName = 'FailedPage';
35
-
36
- export default FailedPage;