@lobehub/chat 1.99.6 → 1.100.1
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.
- package/.cursor/rules/testing-guide/testing-guide.mdc +173 -0
- package/.github/workflows/desktop-pr-build.yml +3 -3
- package/.github/workflows/release-desktop-beta.yml +3 -3
- package/CHANGELOG.md +50 -0
- package/apps/desktop/package.json +5 -2
- package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
- package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
- package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
- package/apps/desktop/src/main/types/store.ts +1 -0
- package/apps/desktop/src/preload/electronApi.ts +2 -1
- package/apps/desktop/src/preload/streamer.ts +58 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +9 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +25 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +25 -0
- package/docs/self-hosting/faq/vercel-ai-image-timeout.mdx +65 -0
- package/docs/self-hosting/faq/vercel-ai-image-timeout.zh-CN.mdx +63 -0
- package/docs/usage/providers/fal.mdx +6 -6
- package/docs/usage/providers/fal.zh-CN.mdx +6 -6
- package/locales/ar/electron.json +3 -0
- package/locales/ar/oauth.json +8 -4
- package/locales/bg-BG/electron.json +3 -0
- package/locales/bg-BG/oauth.json +8 -4
- package/locales/de-DE/electron.json +3 -0
- package/locales/de-DE/oauth.json +9 -5
- package/locales/en-US/electron.json +3 -0
- package/locales/en-US/oauth.json +8 -4
- package/locales/es-ES/electron.json +3 -0
- package/locales/es-ES/oauth.json +9 -5
- package/locales/fa-IR/electron.json +3 -0
- package/locales/fa-IR/oauth.json +8 -4
- package/locales/fr-FR/electron.json +3 -0
- package/locales/fr-FR/oauth.json +8 -4
- package/locales/it-IT/electron.json +3 -0
- package/locales/it-IT/oauth.json +9 -5
- package/locales/ja-JP/electron.json +3 -0
- package/locales/ja-JP/oauth.json +8 -4
- package/locales/ko-KR/electron.json +3 -0
- package/locales/ko-KR/oauth.json +8 -4
- package/locales/nl-NL/electron.json +3 -0
- package/locales/nl-NL/oauth.json +9 -5
- package/locales/pl-PL/electron.json +3 -0
- package/locales/pl-PL/oauth.json +8 -4
- package/locales/pt-BR/electron.json +3 -0
- package/locales/pt-BR/oauth.json +8 -4
- package/locales/ru-RU/electron.json +3 -0
- package/locales/ru-RU/oauth.json +8 -4
- package/locales/tr-TR/electron.json +3 -0
- package/locales/tr-TR/oauth.json +8 -4
- package/locales/vi-VN/electron.json +3 -0
- package/locales/vi-VN/oauth.json +9 -5
- package/locales/zh-CN/electron.json +3 -0
- package/locales/zh-CN/oauth.json +8 -4
- package/locales/zh-TW/electron.json +3 -0
- package/locales/zh-TW/oauth.json +8 -4
- package/package.json +3 -3
- package/packages/electron-client-ipc/src/dispatch.ts +14 -2
- package/packages/electron-client-ipc/src/index.ts +1 -0
- package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
- package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
- package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
- package/packages/electron-client-ipc/src/utils/request.ts +28 -0
- package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
- package/src/app/(backend)/oidc/handoff/route.ts +46 -0
- package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
- package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
- package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
- package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
- package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
- package/src/database/client/migrations.json +8 -0
- package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
- package/src/database/migrations/meta/0028_snapshot.json +6055 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/models/oauthHandoff.ts +94 -0
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/oidc.ts +46 -0
- package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
- package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +1 -1
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +2 -1
- package/src/libs/oidc-provider/config.ts +16 -17
- package/src/libs/oidc-provider/jwt.ts +135 -0
- package/src/libs/oidc-provider/provider.ts +22 -38
- package/src/libs/trpc/client/async.ts +1 -2
- package/src/libs/trpc/client/edge.ts +1 -2
- package/src/libs/trpc/client/lambda.ts +1 -1
- package/src/libs/trpc/client/tools.ts +1 -2
- package/src/libs/trpc/lambda/context.ts +9 -16
- package/src/locales/default/electron.ts +3 -0
- package/src/locales/default/oauth.ts +8 -4
- package/src/middleware.ts +10 -4
- package/src/server/globalConfig/genServerAiProviderConfig.test.ts +235 -0
- package/src/server/globalConfig/genServerAiProviderConfig.ts +9 -10
- package/src/server/services/oidc/index.ts +0 -71
- package/src/services/chat.ts +5 -1
- package/src/services/electron/remoteServer.ts +0 -7
- package/src/store/aiInfra/slices/aiProvider/action.ts +2 -1
- package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
- package/src/utils/getFallbackModelProperty.test.ts +193 -0
- package/src/utils/getFallbackModelProperty.ts +36 -0
- package/src/utils/parseModels.test.ts +150 -48
- package/src/utils/parseModels.ts +26 -11
- package/src/utils/server/auth.ts +22 -0
- package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
- package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
- 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 {
|
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: {
|
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:
|
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('
|
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 {
|
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
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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 (
|
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
|
-
|
32
|
+
error: {
|
33
33
|
backToHome: '返回首页',
|
34
|
-
|
35
|
-
|
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: '
|
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('
|
22
|
-
const logNextAuth = debug('
|
23
|
-
const logClerk = debug('
|
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);
|
@@ -0,0 +1,235 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { ModelProvider } from '@/libs/model-runtime';
|
4
|
+
import { AiFullModelCard } from '@/types/aiModel';
|
5
|
+
|
6
|
+
import { genServerAiProvidersConfig } from './genServerAiProviderConfig';
|
7
|
+
|
8
|
+
// Mock dependencies using importOriginal to preserve real provider data
|
9
|
+
vi.mock('@/config/aiModels', async (importOriginal) => {
|
10
|
+
const actual = await importOriginal<typeof import('@/config/aiModels')>();
|
11
|
+
return {
|
12
|
+
...actual,
|
13
|
+
// Keep the original exports but we can override specific ones if needed
|
14
|
+
};
|
15
|
+
});
|
16
|
+
|
17
|
+
vi.mock('@/config/llm', () => ({
|
18
|
+
getLLMConfig: vi.fn(() => ({
|
19
|
+
ENABLED_OPENAI: true,
|
20
|
+
ENABLED_ANTHROPIC: false,
|
21
|
+
ENABLED_AI21: false,
|
22
|
+
})),
|
23
|
+
}));
|
24
|
+
|
25
|
+
vi.mock('@/utils/parseModels', () => ({
|
26
|
+
extractEnabledModels: vi.fn((providerId: string, modelString?: string) => {
|
27
|
+
if (!modelString) return undefined;
|
28
|
+
return [`${providerId}-model-1`, `${providerId}-model-2`];
|
29
|
+
}),
|
30
|
+
transformToAiModelList: vi.fn((params) => {
|
31
|
+
return params.defaultModels;
|
32
|
+
}),
|
33
|
+
}));
|
34
|
+
|
35
|
+
describe('genServerAiProvidersConfig', () => {
|
36
|
+
beforeEach(() => {
|
37
|
+
vi.clearAllMocks();
|
38
|
+
// Clear environment variables
|
39
|
+
Object.keys(process.env).forEach((key) => {
|
40
|
+
if (key.includes('MODEL_LIST')) {
|
41
|
+
delete process.env[key];
|
42
|
+
}
|
43
|
+
});
|
44
|
+
});
|
45
|
+
|
46
|
+
it('should generate basic provider config with default settings', () => {
|
47
|
+
const result = genServerAiProvidersConfig({});
|
48
|
+
|
49
|
+
expect(result).toHaveProperty('openai');
|
50
|
+
expect(result).toHaveProperty('anthropic');
|
51
|
+
|
52
|
+
expect(result.openai).toEqual({
|
53
|
+
enabled: true,
|
54
|
+
enabledModels: undefined,
|
55
|
+
serverModelLists: expect.any(Array),
|
56
|
+
});
|
57
|
+
|
58
|
+
expect(result.anthropic).toEqual({
|
59
|
+
enabled: false,
|
60
|
+
enabledModels: undefined,
|
61
|
+
serverModelLists: expect.any(Array),
|
62
|
+
});
|
63
|
+
});
|
64
|
+
|
65
|
+
it('should use custom enabled settings from specificConfig', () => {
|
66
|
+
const specificConfig = {
|
67
|
+
openai: {
|
68
|
+
enabled: false,
|
69
|
+
},
|
70
|
+
anthropic: {
|
71
|
+
enabled: true,
|
72
|
+
},
|
73
|
+
};
|
74
|
+
|
75
|
+
const result = genServerAiProvidersConfig(specificConfig);
|
76
|
+
|
77
|
+
expect(result.openai.enabled).toBe(false);
|
78
|
+
expect(result.anthropic.enabled).toBe(true);
|
79
|
+
});
|
80
|
+
|
81
|
+
it('should use custom enabledKey from specificConfig', async () => {
|
82
|
+
const specificConfig = {
|
83
|
+
openai: {
|
84
|
+
enabledKey: 'CUSTOM_OPENAI_ENABLED',
|
85
|
+
},
|
86
|
+
};
|
87
|
+
|
88
|
+
// Mock the LLM config to include our custom key
|
89
|
+
const { getLLMConfig } = vi.mocked(await import('@/config/llm'));
|
90
|
+
getLLMConfig.mockReturnValue({
|
91
|
+
ENABLED_OPENAI: true,
|
92
|
+
ENABLED_ANTHROPIC: false,
|
93
|
+
CUSTOM_OPENAI_ENABLED: true,
|
94
|
+
} as any);
|
95
|
+
|
96
|
+
const result = genServerAiProvidersConfig(specificConfig);
|
97
|
+
|
98
|
+
expect(result.openai.enabled).toBe(true);
|
99
|
+
});
|
100
|
+
|
101
|
+
it('should use environment variables for model lists', async () => {
|
102
|
+
process.env.OPENAI_MODEL_LIST = '+gpt-4,+gpt-3.5-turbo';
|
103
|
+
|
104
|
+
const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
|
105
|
+
extractEnabledModels.mockReturnValue(['gpt-4', 'gpt-3.5-turbo']);
|
106
|
+
|
107
|
+
const result = genServerAiProvidersConfig({});
|
108
|
+
|
109
|
+
expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+gpt-4,+gpt-3.5-turbo', false);
|
110
|
+
expect(result.openai.enabledModels).toEqual(['gpt-4', 'gpt-3.5-turbo']);
|
111
|
+
});
|
112
|
+
|
113
|
+
it('should use custom modelListKey from specificConfig', async () => {
|
114
|
+
const specificConfig = {
|
115
|
+
openai: {
|
116
|
+
modelListKey: 'CUSTOM_OPENAI_MODELS',
|
117
|
+
},
|
118
|
+
};
|
119
|
+
|
120
|
+
process.env.CUSTOM_OPENAI_MODELS = '+custom-model';
|
121
|
+
|
122
|
+
const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
|
123
|
+
|
124
|
+
genServerAiProvidersConfig(specificConfig);
|
125
|
+
|
126
|
+
expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+custom-model', false);
|
127
|
+
});
|
128
|
+
|
129
|
+
it('should handle withDeploymentName option', async () => {
|
130
|
+
const specificConfig = {
|
131
|
+
openai: {
|
132
|
+
withDeploymentName: true,
|
133
|
+
},
|
134
|
+
};
|
135
|
+
|
136
|
+
process.env.OPENAI_MODEL_LIST = '+gpt-4->deployment1';
|
137
|
+
|
138
|
+
const { extractEnabledModels, transformToAiModelList } = vi.mocked(
|
139
|
+
await import('@/utils/parseModels'),
|
140
|
+
);
|
141
|
+
|
142
|
+
genServerAiProvidersConfig(specificConfig);
|
143
|
+
|
144
|
+
expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+gpt-4->deployment1', true);
|
145
|
+
expect(transformToAiModelList).toHaveBeenCalledWith({
|
146
|
+
defaultModels: expect.any(Array),
|
147
|
+
modelString: '+gpt-4->deployment1',
|
148
|
+
providerId: 'openai',
|
149
|
+
withDeploymentName: true,
|
150
|
+
});
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should include fetchOnClient when specified in config', () => {
|
154
|
+
const specificConfig = {
|
155
|
+
openai: {
|
156
|
+
fetchOnClient: true,
|
157
|
+
},
|
158
|
+
};
|
159
|
+
|
160
|
+
const result = genServerAiProvidersConfig(specificConfig);
|
161
|
+
|
162
|
+
expect(result.openai).toHaveProperty('fetchOnClient', true);
|
163
|
+
});
|
164
|
+
|
165
|
+
it('should not include fetchOnClient when not specified in config', () => {
|
166
|
+
const result = genServerAiProvidersConfig({});
|
167
|
+
|
168
|
+
expect(result.openai).not.toHaveProperty('fetchOnClient');
|
169
|
+
});
|
170
|
+
|
171
|
+
it('should handle all available providers', () => {
|
172
|
+
const result = genServerAiProvidersConfig({});
|
173
|
+
|
174
|
+
// Check that result includes some key providers
|
175
|
+
expect(result).toHaveProperty('openai');
|
176
|
+
expect(result).toHaveProperty('anthropic');
|
177
|
+
|
178
|
+
// Check structure for each provider
|
179
|
+
Object.keys(result).forEach((provider) => {
|
180
|
+
expect(result[provider]).toHaveProperty('enabled');
|
181
|
+
expect(result[provider]).toHaveProperty('serverModelLists');
|
182
|
+
// enabled can be boolean or undefined (when no config is provided)
|
183
|
+
expect(['boolean', 'undefined']).toContain(typeof result[provider].enabled);
|
184
|
+
expect(Array.isArray(result[provider].serverModelLists)).toBe(true);
|
185
|
+
});
|
186
|
+
});
|
187
|
+
});
|
188
|
+
|
189
|
+
describe('genServerAiProvidersConfig Error Handling', () => {
|
190
|
+
it('should throw error when a provider is not found in aiModels', async () => {
|
191
|
+
// Reset all mocks to create a clean test environment
|
192
|
+
vi.resetModules();
|
193
|
+
|
194
|
+
// Mock dependencies with a missing provider scenario
|
195
|
+
vi.doMock('@/config/aiModels', () => ({
|
196
|
+
// Explicitly set openai to undefined to simulate missing provider
|
197
|
+
openai: undefined,
|
198
|
+
anthropic: [
|
199
|
+
{
|
200
|
+
id: 'claude-3',
|
201
|
+
displayName: 'Claude 3',
|
202
|
+
type: 'chat',
|
203
|
+
enabled: true,
|
204
|
+
},
|
205
|
+
],
|
206
|
+
}));
|
207
|
+
|
208
|
+
vi.doMock('@/config/llm', () => ({
|
209
|
+
getLLMConfig: vi.fn(() => ({})),
|
210
|
+
}));
|
211
|
+
|
212
|
+
vi.doMock('@/utils/parseModels', () => ({
|
213
|
+
extractEnabledModels: vi.fn(() => undefined),
|
214
|
+
transformToAiModelList: vi.fn(() => []),
|
215
|
+
}));
|
216
|
+
|
217
|
+
// Mock ModelProvider to include the missing provider
|
218
|
+
vi.doMock('@/libs/model-runtime', () => ({
|
219
|
+
ModelProvider: {
|
220
|
+
openai: 'openai', // This exists in enum
|
221
|
+
anthropic: 'anthropic', // This exists in both enum and aiModels
|
222
|
+
},
|
223
|
+
}));
|
224
|
+
|
225
|
+
// Import the function with the new mocks
|
226
|
+
const { genServerAiProvidersConfig } = await import(
|
227
|
+
'./genServerAiProviderConfig?v=' + Date.now()
|
228
|
+
);
|
229
|
+
|
230
|
+
// This should throw because 'openai' is in ModelProvider but not in aiModels
|
231
|
+
expect(() => {
|
232
|
+
genServerAiProvidersConfig({});
|
233
|
+
}).toThrow();
|
234
|
+
});
|
235
|
+
});
|