@lobehub/lobehub 2.0.0-next.194 → 2.0.0-next.196
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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/setting.json +0 -3
- package/locales/bg-BG/setting.json +0 -3
- package/locales/de-DE/setting.json +0 -3
- package/locales/en-US/setting.json +0 -3
- package/locales/es-ES/setting.json +0 -3
- package/locales/fa-IR/setting.json +0 -3
- package/locales/fr-FR/setting.json +0 -3
- package/locales/it-IT/setting.json +0 -3
- package/locales/ja-JP/setting.json +0 -3
- package/locales/ko-KR/setting.json +0 -3
- package/locales/nl-NL/setting.json +0 -3
- package/locales/pl-PL/setting.json +0 -3
- package/locales/pt-BR/setting.json +0 -3
- package/locales/ru-RU/setting.json +0 -3
- package/locales/tr-TR/setting.json +0 -3
- package/locales/vi-VN/setting.json +0 -3
- package/locales/zh-CN/setting.json +0 -3
- package/locales/zh-TW/setting.json +0 -3
- package/package.json +1 -1
- package/packages/const/src/fetch.ts +1 -4
- package/packages/database/src/models/user.ts +8 -0
- package/packages/database/src/repositories/aiInfra/index.test.ts +11 -8
- package/packages/database/src/repositories/dataExporter/index.test.ts +11 -9
- package/packages/database/src/repositories/tableViewer/index.test.ts +13 -14
- package/packages/model-runtime/src/providers/zhipu/index.ts +6 -6
- package/packages/types/src/auth.ts +0 -4
- package/packages/utils/src/server/xor.test.ts +1 -2
- package/src/app/(backend)/_deprecated/createBizOpenAI/auth.test.ts +7 -41
- package/src/app/(backend)/_deprecated/createBizOpenAI/auth.ts +1 -15
- package/src/app/(backend)/_deprecated/createBizOpenAI/index.ts +2 -9
- package/src/app/(backend)/middleware/auth/index.ts +0 -1
- package/src/app/(backend)/middleware/auth/utils.test.ts +2 -42
- package/src/app/(backend)/middleware/auth/utils.ts +3 -17
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +0 -5
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +0 -6
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +2 -32
- package/src/app/[variants]/(main)/settings/common/features/Common/Common.tsx +1 -16
- package/src/envs/app.ts +2 -0
- package/src/libs/trpc/lambda/middleware/index.ts +1 -0
- package/src/libs/trpc/lambda/middleware/telemetry.test.ts +237 -0
- package/src/libs/trpc/lambda/middleware/telemetry.ts +74 -0
- package/src/locales/default/setting.ts +0 -3
- package/src/server/routers/lambda/market/index.ts +1 -93
- package/src/server/routers/tools/_helpers/index.ts +1 -0
- package/src/server/routers/tools/_helpers/scheduleToolCallReport.ts +113 -0
- package/src/server/routers/tools/index.ts +2 -2
- package/src/server/routers/tools/market.ts +375 -0
- package/src/server/routers/tools/mcp.ts +77 -20
- package/src/services/chat/index.ts +0 -2
- package/src/services/codeInterpreter.ts +6 -6
- package/src/services/mcp.test.ts +60 -46
- package/src/services/mcp.ts +67 -48
- package/src/store/chat/slices/plugin/action.test.ts +191 -0
- package/src/store/chat/slices/plugin/actions/internals.ts +2 -18
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +31 -44
- package/packages/database/src/client/db.test.ts +0 -52
- package/packages/database/src/client/db.ts +0 -195
- package/packages/database/src/client/type.ts +0 -6
- package/src/server/routers/tools/codeInterpreter.ts +0 -255
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { ChatErrorType
|
|
1
|
+
import { ChatErrorType } from '@lobechat/types';
|
|
2
2
|
import type OpenAI from 'openai';
|
|
3
3
|
|
|
4
4
|
import { getOpenAIAuthFromRequest } from '@/const/fetch';
|
|
5
5
|
import { createErrorResponse } from '@/utils/errorResponse';
|
|
6
6
|
|
|
7
|
-
import { checkAuth } from './auth';
|
|
8
7
|
import { createOpenai } from './createOpenai';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -13,13 +12,7 @@ import { createOpenai } from './createOpenai';
|
|
|
13
12
|
* if auth not pass ,just return error response
|
|
14
13
|
*/
|
|
15
14
|
export const createBizOpenAI = (req: Request): Response | OpenAI => {
|
|
16
|
-
const { apiKey,
|
|
17
|
-
|
|
18
|
-
const result = checkAuth({ accessCode, apiKey, oauthAuthorized });
|
|
19
|
-
|
|
20
|
-
if (!result.auth) {
|
|
21
|
-
return createErrorResponse(result.error as ErrorType);
|
|
22
|
-
}
|
|
15
|
+
const { apiKey, endpoint } = getOpenAIAuthFromRequest(req);
|
|
23
16
|
|
|
24
17
|
let openai: OpenAI;
|
|
25
18
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { type AuthObject } from '@clerk/backend';
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import { getAppConfig } from '@/envs/app';
|
|
5
|
-
|
|
6
4
|
import { checkAuthMethod } from './utils';
|
|
7
5
|
|
|
8
6
|
let enableClerkMock = false;
|
|
@@ -26,15 +24,9 @@ vi.mock('@/const/auth', async (importOriginal) => {
|
|
|
26
24
|
};
|
|
27
25
|
});
|
|
28
26
|
|
|
29
|
-
vi.mock('@/envs/app', () => ({
|
|
30
|
-
getAppConfig: vi.fn(),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
27
|
describe('checkAuthMethod', () => {
|
|
34
28
|
beforeEach(() => {
|
|
35
|
-
vi.
|
|
36
|
-
ACCESS_CODES: ['validAccessCode'],
|
|
37
|
-
} as any);
|
|
29
|
+
vi.clearAllMocks();
|
|
38
30
|
});
|
|
39
31
|
|
|
40
32
|
it('should pass with valid Clerk auth', () => {
|
|
@@ -91,39 +83,7 @@ describe('checkAuthMethod', () => {
|
|
|
91
83
|
).not.toThrow();
|
|
92
84
|
});
|
|
93
85
|
|
|
94
|
-
it('should pass with no
|
|
95
|
-
vi.mocked(getAppConfig).mockReturnValueOnce({
|
|
96
|
-
ACCESS_CODES: [],
|
|
97
|
-
} as any);
|
|
98
|
-
|
|
86
|
+
it('should pass with no auth params', () => {
|
|
99
87
|
expect(() => checkAuthMethod({})).not.toThrow();
|
|
100
88
|
});
|
|
101
|
-
|
|
102
|
-
it('should pass with valid access code', () => {
|
|
103
|
-
expect(() =>
|
|
104
|
-
checkAuthMethod({
|
|
105
|
-
accessCode: 'validAccessCode',
|
|
106
|
-
}),
|
|
107
|
-
).not.toThrow();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should throw error with invalid access code', () => {
|
|
111
|
-
try {
|
|
112
|
-
checkAuthMethod({
|
|
113
|
-
accessCode: 'invalidAccessCode',
|
|
114
|
-
});
|
|
115
|
-
} catch (e) {
|
|
116
|
-
expect(e).toEqual({
|
|
117
|
-
errorType: 'InvalidAccessCode',
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
checkAuthMethod({});
|
|
123
|
-
} catch (e) {
|
|
124
|
-
expect(e).toEqual({
|
|
125
|
-
errorType: 'InvalidAccessCode',
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
89
|
});
|
|
@@ -3,28 +3,25 @@ import { AgentRuntimeError } from '@lobechat/model-runtime';
|
|
|
3
3
|
import { ChatErrorType } from '@lobechat/types';
|
|
4
4
|
|
|
5
5
|
import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
|
|
6
|
-
import { getAppConfig } from '@/envs/app';
|
|
7
6
|
|
|
8
7
|
interface CheckAuthParams {
|
|
9
|
-
accessCode?: string;
|
|
10
8
|
apiKey?: string;
|
|
11
9
|
betterAuthAuthorized?: boolean;
|
|
12
10
|
clerkAuth?: AuthObject;
|
|
13
11
|
nextAuthAuthorized?: boolean;
|
|
14
12
|
}
|
|
15
13
|
/**
|
|
16
|
-
* Check if
|
|
14
|
+
* Check if authentication is valid based on various auth methods.
|
|
17
15
|
*
|
|
18
16
|
* @param {CheckAuthParams} params - Authentication parameters extracted from headers.
|
|
19
|
-
* @param {string} [params.accessCode] - The access code to check.
|
|
20
17
|
* @param {string} [params.apiKey] - The user API key.
|
|
21
18
|
* @param {boolean} [params.betterAuthAuthorized] - Whether the Better Auth session exists.
|
|
22
19
|
* @param {AuthObject} [params.clerkAuth] - Clerk authentication payload from middleware.
|
|
23
20
|
* @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided.
|
|
24
|
-
* @throws {AgentRuntimeError} If
|
|
21
|
+
* @throws {AgentRuntimeError} If authentication fails.
|
|
25
22
|
*/
|
|
26
23
|
export const checkAuthMethod = (params: CheckAuthParams) => {
|
|
27
|
-
const { apiKey, betterAuthAuthorized, nextAuthAuthorized,
|
|
24
|
+
const { apiKey, betterAuthAuthorized, nextAuthAuthorized, clerkAuth } = params;
|
|
28
25
|
// clerk auth handler
|
|
29
26
|
if (enableClerk) {
|
|
30
27
|
// if there is no userId, means the use is not login, just throw error
|
|
@@ -42,15 +39,4 @@ export const checkAuthMethod = (params: CheckAuthParams) => {
|
|
|
42
39
|
|
|
43
40
|
// if apiKey exist
|
|
44
41
|
if (apiKey) return;
|
|
45
|
-
|
|
46
|
-
const { ACCESS_CODES } = getAppConfig();
|
|
47
|
-
|
|
48
|
-
// if accessCode doesn't exist
|
|
49
|
-
if (!ACCESS_CODES.length) return;
|
|
50
|
-
|
|
51
|
-
if (!accessCode || !ACCESS_CODES.includes(accessCode)) {
|
|
52
|
-
// Avoid logging user-provided credentials (access code) for security reasons
|
|
53
|
-
console.warn('Invalid access code provided');
|
|
54
|
-
throw AgentRuntimeError.createError(ChatErrorType.InvalidAccessCode);
|
|
55
|
-
}
|
|
56
42
|
};
|
|
@@ -62,7 +62,6 @@ describe('POST handler', () => {
|
|
|
62
62
|
|
|
63
63
|
// 设置 getJWTPayload 和 initModelRuntimeWithUserPayload 的模拟返回值
|
|
64
64
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
65
|
-
accessCode: 'test-access-code',
|
|
66
65
|
apiKey: 'test-api-key',
|
|
67
66
|
azureApiVersion: 'v1',
|
|
68
67
|
});
|
|
@@ -105,7 +104,6 @@ describe('POST handler', () => {
|
|
|
105
104
|
mockState.enableClerk = true;
|
|
106
105
|
|
|
107
106
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
108
|
-
accessCode: 'test-access-code',
|
|
109
107
|
apiKey: 'test-api-key',
|
|
110
108
|
azureApiVersion: 'v1',
|
|
111
109
|
});
|
|
@@ -133,7 +131,6 @@ describe('POST handler', () => {
|
|
|
133
131
|
await POST(request, { params: mockParams });
|
|
134
132
|
|
|
135
133
|
expect(checkAuthMethod).toBeCalledWith({
|
|
136
|
-
accessCode: 'test-access-code',
|
|
137
134
|
apiKey: 'test-api-key',
|
|
138
135
|
betterAuthAuthorized: false,
|
|
139
136
|
clerkAuth: {},
|
|
@@ -163,7 +160,6 @@ describe('POST handler', () => {
|
|
|
163
160
|
describe('chat', () => {
|
|
164
161
|
it('should correctly handle chat completion with valid payload', async () => {
|
|
165
162
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
166
|
-
accessCode: 'test-access-code',
|
|
167
163
|
apiKey: 'test-api-key',
|
|
168
164
|
azureApiVersion: 'v1',
|
|
169
165
|
userId: 'abc',
|
|
@@ -193,7 +189,6 @@ describe('POST handler', () => {
|
|
|
193
189
|
it('should return an error response when chat completion fails', async () => {
|
|
194
190
|
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
|
195
191
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
196
|
-
accessCode: 'test-access-code',
|
|
197
192
|
apiKey: 'test-api-key',
|
|
198
193
|
azureApiVersion: 'v1',
|
|
199
194
|
});
|
|
@@ -41,7 +41,6 @@ describe('GET handler', () => {
|
|
|
41
41
|
const mockParams = Promise.resolve({ provider: 'google' });
|
|
42
42
|
|
|
43
43
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
44
|
-
accessCode: 'test-access-code',
|
|
45
44
|
apiKey: 'test-api-key',
|
|
46
45
|
});
|
|
47
46
|
|
|
@@ -73,7 +72,6 @@ describe('GET handler', () => {
|
|
|
73
72
|
const mockParams = Promise.resolve({ provider: 'google' });
|
|
74
73
|
|
|
75
74
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
76
|
-
accessCode: 'test-access-code',
|
|
77
75
|
apiKey: 'test-api-key',
|
|
78
76
|
});
|
|
79
77
|
|
|
@@ -103,7 +101,6 @@ describe('GET handler', () => {
|
|
|
103
101
|
const mockParams = Promise.resolve({ provider: 'google' });
|
|
104
102
|
|
|
105
103
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
106
|
-
accessCode: 'test-access-code',
|
|
107
104
|
apiKey: 'test-api-key',
|
|
108
105
|
});
|
|
109
106
|
|
|
@@ -128,7 +125,6 @@ describe('GET handler', () => {
|
|
|
128
125
|
const mockParams = Promise.resolve({ provider: 'google' });
|
|
129
126
|
|
|
130
127
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
131
|
-
accessCode: 'test-access-code',
|
|
132
128
|
apiKey: 'test-api-key',
|
|
133
129
|
});
|
|
134
130
|
|
|
@@ -145,7 +141,6 @@ describe('GET handler', () => {
|
|
|
145
141
|
const mockParams = Promise.resolve({ provider: 'openai' });
|
|
146
142
|
|
|
147
143
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
148
|
-
accessCode: 'test-access-code',
|
|
149
144
|
apiKey: 'test-api-key',
|
|
150
145
|
});
|
|
151
146
|
|
|
@@ -165,7 +160,6 @@ describe('GET handler', () => {
|
|
|
165
160
|
const mockParams = Promise.resolve({ provider: 'openai' });
|
|
166
161
|
|
|
167
162
|
vi.mocked(getXorPayload).mockReturnValueOnce({
|
|
168
|
-
accessCode: 'test-access-code',
|
|
169
163
|
apiKey: 'test-api-key',
|
|
170
164
|
});
|
|
171
165
|
|
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
import { AgentRuntimeError } from '@lobechat/model-runtime';
|
|
2
|
-
import { ChatErrorType,
|
|
3
|
-
import { getXorPayload } from '@lobechat/utils/server';
|
|
2
|
+
import { ChatErrorType, TraceNameMap } from '@lobechat/types';
|
|
4
3
|
import type { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
|
5
4
|
import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway';
|
|
6
5
|
|
|
7
|
-
import { LOBE_CHAT_AUTH_HEADER
|
|
6
|
+
import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
|
8
7
|
import { LOBE_CHAT_TRACE_ID } from '@/const/trace';
|
|
9
8
|
import { getAppConfig } from '@/envs/app';
|
|
10
9
|
import { TraceClient } from '@/libs/traces';
|
|
11
10
|
import { parserPluginSettings } from '@/server/services/pluginGateway/settings';
|
|
12
|
-
import { createErrorResponse } from '@/utils/errorResponse';
|
|
13
11
|
import { getTracePayload } from '@/utils/trace';
|
|
14
12
|
|
|
15
|
-
const checkAuth = (accessCode: string | null, oauthAuthorized: boolean | null) => {
|
|
16
|
-
const { ACCESS_CODES, PLUGIN_SETTINGS } = getAppConfig();
|
|
17
|
-
|
|
18
|
-
// if there is no plugin settings, just skip the auth
|
|
19
|
-
if (!PLUGIN_SETTINGS) return { auth: true };
|
|
20
|
-
|
|
21
|
-
// If authorized by oauth
|
|
22
|
-
if (oauthAuthorized && enableNextAuth) return { auth: true };
|
|
23
|
-
|
|
24
|
-
// if accessCode doesn't exist
|
|
25
|
-
if (!ACCESS_CODES.length) return { auth: true };
|
|
26
|
-
|
|
27
|
-
if (!accessCode || !ACCESS_CODES.includes(accessCode)) {
|
|
28
|
-
return { auth: false, error: ChatErrorType.InvalidAccessCode };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return { auth: true };
|
|
32
|
-
};
|
|
33
|
-
|
|
34
13
|
const { PLUGINS_INDEX_URL: pluginsIndexUrl, PLUGIN_SETTINGS } = getAppConfig();
|
|
35
14
|
|
|
36
15
|
const defaultPluginSettings = parserPluginSettings(PLUGIN_SETTINGS);
|
|
@@ -42,15 +21,6 @@ export const POST = async (req: Request) => {
|
|
|
42
21
|
const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
|
|
43
22
|
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
|
44
23
|
|
|
45
|
-
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
|
46
|
-
const payload = getXorPayload(authorization);
|
|
47
|
-
|
|
48
|
-
const result = checkAuth(payload.accessCode!, oauthAuthorized);
|
|
49
|
-
|
|
50
|
-
if (!result.auth) {
|
|
51
|
-
return createErrorResponse(result.error as ErrorType);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
24
|
// TODO: need to be replace by better telemetry system
|
|
55
25
|
// add trace
|
|
56
26
|
const tracePayload = getTracePayload(req);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Form, type FormGroupItemType, Icon, ImageSelect
|
|
3
|
+
import { Form, type FormGroupItemType, Icon, ImageSelect } from '@lobehub/ui';
|
|
4
4
|
import { Select, Skeleton } from '@lobehub/ui';
|
|
5
5
|
import { Segmented, Switch } from 'antd';
|
|
6
6
|
import isEqual from 'fast-deep-equal';
|
|
@@ -14,8 +14,6 @@ import { isDesktop } from '@/const/version';
|
|
|
14
14
|
import { localeOptions } from '@/locales/resources';
|
|
15
15
|
import { useGlobalStore } from '@/store/global';
|
|
16
16
|
import { systemStatusSelectors } from '@/store/global/selectors';
|
|
17
|
-
import { useServerConfigStore } from '@/store/serverConfig';
|
|
18
|
-
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
19
17
|
import { useUserStore } from '@/store/user';
|
|
20
18
|
import { settingsSelectors } from '@/store/user/selectors';
|
|
21
19
|
import { type LocaleMode } from '@/types/locale';
|
|
@@ -23,7 +21,6 @@ import { type LocaleMode } from '@/types/locale';
|
|
|
23
21
|
const Common = memo(() => {
|
|
24
22
|
const { t } = useTranslation('setting');
|
|
25
23
|
|
|
26
|
-
const showAccessCodeConfig = useServerConfigStore(serverConfigSelectors.enabledAccessCode);
|
|
27
24
|
const general = useUserStore((s) => settingsSelectors.currentSettings(s).general, isEqual);
|
|
28
25
|
const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
|
|
29
26
|
const language = useGlobalStore(systemStatusSelectors.language);
|
|
@@ -137,18 +134,6 @@ const Common = memo(() => {
|
|
|
137
134
|
name: 'contextMenuMode',
|
|
138
135
|
},
|
|
139
136
|
|
|
140
|
-
{
|
|
141
|
-
children: (
|
|
142
|
-
<InputPassword
|
|
143
|
-
autoComplete={'new-password'}
|
|
144
|
-
placeholder={t('settingSystem.accessCode.placeholder')}
|
|
145
|
-
/>
|
|
146
|
-
),
|
|
147
|
-
desc: t('settingSystem.accessCode.desc'),
|
|
148
|
-
hidden: !showAccessCodeConfig,
|
|
149
|
-
label: t('settingSystem.accessCode.title'),
|
|
150
|
-
name: 'password',
|
|
151
|
-
},
|
|
152
137
|
{
|
|
153
138
|
children: (
|
|
154
139
|
<Select
|
package/src/envs/app.ts
CHANGED
|
@@ -82,6 +82,7 @@ export const getAppConfig = () => {
|
|
|
82
82
|
* @default false
|
|
83
83
|
*/
|
|
84
84
|
enableQueueAgentRuntime: z.boolean().optional(),
|
|
85
|
+
TELEMETRY_DISABLED: z.boolean().optional(),
|
|
85
86
|
},
|
|
86
87
|
runtimeEnv: {
|
|
87
88
|
// Sentry
|
|
@@ -121,6 +122,7 @@ export const getAppConfig = () => {
|
|
|
121
122
|
MARKET_TRUSTED_CLIENT_ID: process.env.MARKET_TRUSTED_CLIENT_ID,
|
|
122
123
|
|
|
123
124
|
enableQueueAgentRuntime: process.env.AGENT_RUNTIME_MODE === 'queue',
|
|
125
|
+
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED === '1',
|
|
124
126
|
},
|
|
125
127
|
});
|
|
126
128
|
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { appEnv } from '@/envs/app';
|
|
5
|
+
|
|
6
|
+
import { TelemetryContext, checkTelemetryEnabled } from './telemetry';
|
|
7
|
+
|
|
8
|
+
const { mockGetUserSettings, mockGetUserPreference, MockUserModel } = vi.hoisted(() => {
|
|
9
|
+
const mockGetUserSettings = vi.fn();
|
|
10
|
+
const mockGetUserPreference = vi.fn();
|
|
11
|
+
const MockUserModel = vi.fn().mockImplementation(() => ({
|
|
12
|
+
getUserPreference: mockGetUserPreference,
|
|
13
|
+
getUserSettings: mockGetUserSettings,
|
|
14
|
+
})) as any;
|
|
15
|
+
return { MockUserModel, mockGetUserPreference, mockGetUserSettings };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock('@/envs/app', () => ({
|
|
19
|
+
appEnv: {
|
|
20
|
+
TELEMETRY_DISABLED: false,
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@/database/models/user', () => ({
|
|
25
|
+
UserModel: MockUserModel,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe('checkTelemetryEnabled', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
// Reset appEnv mock
|
|
32
|
+
vi.mocked(appEnv).TELEMETRY_DISABLED = false;
|
|
33
|
+
// Default mock returns
|
|
34
|
+
mockGetUserSettings.mockResolvedValue(null);
|
|
35
|
+
mockGetUserPreference.mockResolvedValue(null);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('environment variable priority (highest)', () => {
|
|
39
|
+
it('should return telemetryEnabled: false when TELEMETRY_DISABLED=true', async () => {
|
|
40
|
+
vi.mocked(appEnv).TELEMETRY_DISABLED = true;
|
|
41
|
+
|
|
42
|
+
const result = await checkTelemetryEnabled({
|
|
43
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
44
|
+
userId: 'test-user',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
48
|
+
// Should not call database
|
|
49
|
+
expect(mockGetUserSettings).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should check database when TELEMETRY_DISABLED is false', async () => {
|
|
53
|
+
await checkTelemetryEnabled({
|
|
54
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
55
|
+
userId: 'test-user',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(mockGetUserSettings).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should check database when TELEMETRY_DISABLED is undefined', async () => {
|
|
62
|
+
vi.mocked(appEnv).TELEMETRY_DISABLED = undefined;
|
|
63
|
+
|
|
64
|
+
await checkTelemetryEnabled({
|
|
65
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
66
|
+
userId: 'test-user',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(mockGetUserSettings).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('user_settings.general.telemetry', () => {
|
|
74
|
+
it('should return telemetryEnabled: true when settings.general.telemetry is true and preference is not set', async () => {
|
|
75
|
+
mockGetUserSettings.mockResolvedValue({
|
|
76
|
+
general: { telemetry: true },
|
|
77
|
+
});
|
|
78
|
+
mockGetUserPreference.mockResolvedValue(null);
|
|
79
|
+
|
|
80
|
+
const result = await checkTelemetryEnabled({
|
|
81
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
82
|
+
userId: 'test-user',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({ telemetryEnabled: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return telemetryEnabled: false from settings.general', async () => {
|
|
89
|
+
mockGetUserSettings.mockResolvedValue({
|
|
90
|
+
general: { telemetry: false },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const result = await checkTelemetryEnabled({
|
|
94
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
95
|
+
userId: 'test-user',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should check preference when settings.general.telemetry is not set', async () => {
|
|
102
|
+
mockGetUserSettings.mockResolvedValue({
|
|
103
|
+
general: { fontSize: 14 }, // no telemetry field
|
|
104
|
+
});
|
|
105
|
+
mockGetUserPreference.mockResolvedValue({ telemetry: true });
|
|
106
|
+
|
|
107
|
+
const result = await checkTelemetryEnabled({
|
|
108
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
109
|
+
userId: 'test-user',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Should fall back to preference.telemetry
|
|
113
|
+
expect(result).toEqual({ telemetryEnabled: true });
|
|
114
|
+
expect(mockGetUserPreference).toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('users.preference.telemetry (deprecated, fallback)', () => {
|
|
119
|
+
it('should return telemetryEnabled: true from preference.telemetry', async () => {
|
|
120
|
+
mockGetUserSettings.mockResolvedValue(null);
|
|
121
|
+
mockGetUserPreference.mockResolvedValue({ telemetry: true });
|
|
122
|
+
|
|
123
|
+
const result = await checkTelemetryEnabled({
|
|
124
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
125
|
+
userId: 'test-user',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual({ telemetryEnabled: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return telemetryEnabled: false from preference.telemetry', async () => {
|
|
132
|
+
mockGetUserSettings.mockResolvedValue(null);
|
|
133
|
+
mockGetUserPreference.mockResolvedValue({ telemetry: false });
|
|
134
|
+
|
|
135
|
+
const result = await checkTelemetryEnabled({
|
|
136
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
137
|
+
userId: 'test-user',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should use preference.telemetry when settings.general.telemetry is not false', async () => {
|
|
144
|
+
mockGetUserSettings.mockResolvedValue({
|
|
145
|
+
general: { telemetry: true },
|
|
146
|
+
});
|
|
147
|
+
mockGetUserPreference.mockResolvedValue({ telemetry: false });
|
|
148
|
+
|
|
149
|
+
const result = await checkTelemetryEnabled({
|
|
150
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
151
|
+
userId: 'test-user',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// preference.telemetry is checked when settings.general.telemetry is not false
|
|
155
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
156
|
+
expect(mockGetUserPreference).toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should not call getUserPreference when settings.general.telemetry is explicitly false', async () => {
|
|
160
|
+
mockGetUserSettings.mockResolvedValue({
|
|
161
|
+
general: { telemetry: false },
|
|
162
|
+
});
|
|
163
|
+
mockGetUserPreference.mockResolvedValue({ telemetry: true });
|
|
164
|
+
|
|
165
|
+
const result = await checkTelemetryEnabled({
|
|
166
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
167
|
+
userId: 'test-user',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
171
|
+
expect(mockGetUserPreference).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('default value', () => {
|
|
176
|
+
it('should default to true when settings is null', async () => {
|
|
177
|
+
mockGetUserSettings.mockResolvedValue(null);
|
|
178
|
+
|
|
179
|
+
const result = await checkTelemetryEnabled({
|
|
180
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
181
|
+
userId: 'test-user',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Default to true (enabled) unless explicitly disabled
|
|
185
|
+
expect(result).toEqual({ telemetryEnabled: true });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should default to true when general is null', async () => {
|
|
189
|
+
mockGetUserSettings.mockResolvedValue({
|
|
190
|
+
general: null,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const result = await checkTelemetryEnabled({
|
|
194
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
195
|
+
userId: 'test-user',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Default to true (enabled) unless explicitly disabled
|
|
199
|
+
expect(result).toEqual({ telemetryEnabled: true });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('missing context', () => {
|
|
204
|
+
it('should return telemetryEnabled: false when userId is missing', async () => {
|
|
205
|
+
const result = await checkTelemetryEnabled({
|
|
206
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
207
|
+
userId: null,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
211
|
+
expect(mockGetUserSettings).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should return telemetryEnabled: false when serverDB is missing', async () => {
|
|
215
|
+
const result = await checkTelemetryEnabled({
|
|
216
|
+
serverDB: undefined,
|
|
217
|
+
userId: 'test-user',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
221
|
+
expect(mockGetUserSettings).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('error handling', () => {
|
|
226
|
+
it('should return telemetryEnabled: false when getUserSettings fails', async () => {
|
|
227
|
+
mockGetUserSettings.mockRejectedValue(new Error('Database error'));
|
|
228
|
+
|
|
229
|
+
const result = await checkTelemetryEnabled({
|
|
230
|
+
serverDB: {} as TelemetryContext['serverDB'],
|
|
231
|
+
userId: 'test-user',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result).toEqual({ telemetryEnabled: false });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { LobeChatDatabase } from '@lobechat/database';
|
|
2
|
+
import type { UserGeneralConfig } from '@lobechat/types';
|
|
3
|
+
|
|
4
|
+
import { UserModel } from '@/database/models/user';
|
|
5
|
+
import { appEnv } from '@/envs/app';
|
|
6
|
+
|
|
7
|
+
import { trpc } from '../init';
|
|
8
|
+
|
|
9
|
+
export interface TelemetryContext {
|
|
10
|
+
serverDB?: LobeChatDatabase;
|
|
11
|
+
userId?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TelemetryResult {
|
|
15
|
+
telemetryEnabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if telemetry is enabled for the current user
|
|
20
|
+
*
|
|
21
|
+
* Priority:
|
|
22
|
+
* 1. Environment variable TELEMETRY_DISABLED=1 → telemetryEnabled: false (highest priority)
|
|
23
|
+
* 2. User settings from database user_settings.general.telemetry (new location)
|
|
24
|
+
* 3. User preference from database users.preference.telemetry (old location, deprecated)
|
|
25
|
+
* 4. Default to true if not explicitly set
|
|
26
|
+
*/
|
|
27
|
+
export const checkTelemetryEnabled = async (ctx: TelemetryContext): Promise<TelemetryResult> => {
|
|
28
|
+
// Priority 1: Check environment variable (highest priority)
|
|
29
|
+
if (appEnv.TELEMETRY_DISABLED) {
|
|
30
|
+
return { telemetryEnabled: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// If userId or serverDB is not available, default to disabled
|
|
34
|
+
if (!ctx.userId || !ctx.serverDB) {
|
|
35
|
+
return { telemetryEnabled: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const userModel = new UserModel(ctx.serverDB, ctx.userId);
|
|
40
|
+
|
|
41
|
+
// Priority 2: Check user settings (new location: settings.general.telemetry)
|
|
42
|
+
const settings = await userModel.getUserSettings();
|
|
43
|
+
const generalConfig = settings?.general as UserGeneralConfig | null | undefined;
|
|
44
|
+
|
|
45
|
+
if (generalConfig?.telemetry === false) {
|
|
46
|
+
return { telemetryEnabled: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Priority 3: Check user preference (old location: preference.telemetry)
|
|
50
|
+
const preference = await userModel.getUserPreference();
|
|
51
|
+
|
|
52
|
+
if (typeof preference?.telemetry === 'boolean') {
|
|
53
|
+
return { telemetryEnabled: preference?.telemetry };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Priority 4: Default to true if not explicitly set
|
|
57
|
+
return { telemetryEnabled: true };
|
|
58
|
+
} catch {
|
|
59
|
+
// If fetching user settings fails, default to disabled
|
|
60
|
+
return { telemetryEnabled: false };
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Middleware that checks if telemetry is enabled for the current user
|
|
66
|
+
* and adds telemetryEnabled to the context
|
|
67
|
+
*
|
|
68
|
+
* Requires serverDatabase middleware to be applied first
|
|
69
|
+
*/
|
|
70
|
+
export const telemetry = trpc.middleware(async (opts) => {
|
|
71
|
+
const result = await checkTelemetryEnabled(opts.ctx as TelemetryContext);
|
|
72
|
+
|
|
73
|
+
return opts.next({ ctx: result });
|
|
74
|
+
});
|
|
@@ -442,9 +442,6 @@ export default {
|
|
|
442
442
|
'settingOpening.openingQuestions.title': 'Opening Questions',
|
|
443
443
|
'settingOpening.title': 'Opening Settings',
|
|
444
444
|
'settingPlugin.title': 'Skill List',
|
|
445
|
-
'settingSystem.accessCode.desc': 'Encryption access is enabled by the administrator',
|
|
446
|
-
'settingSystem.accessCode.placeholder': 'Enter access password',
|
|
447
|
-
'settingSystem.accessCode.title': 'Access Password',
|
|
448
445
|
'settingSystem.oauth.info.desc': 'Logged in',
|
|
449
446
|
'settingSystem.oauth.info.title': 'Account Information',
|
|
450
447
|
'settingSystem.oauth.signin.action': 'Sign In',
|