@lobehub/lobehub 2.0.0-next.194 → 2.0.0-next.195
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- 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/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/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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.195](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.194...v2.0.0-next.195)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-03**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix tool call message content missing.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix tool call message content missing, closes [#11116](https://github.com/lobehub/lobe-chat/issues/11116) ([885964e](https://github.com/lobehub/lobe-chat/commit/885964e))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.194](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.193...v2.0.0-next.194)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2026-01-03**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.195",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -165,6 +165,14 @@ export class UserModel {
|
|
|
165
165
|
return this.db.query.userSettings.findFirst({ where: eq(userSettings.id, this.userId) });
|
|
166
166
|
};
|
|
167
167
|
|
|
168
|
+
getUserPreference = async (): Promise<UserPreference | undefined> => {
|
|
169
|
+
const user = await this.db.query.users.findFirst({
|
|
170
|
+
columns: { preference: true },
|
|
171
|
+
where: eq(users.id, this.userId),
|
|
172
|
+
});
|
|
173
|
+
return user?.preference as UserPreference | undefined;
|
|
174
|
+
};
|
|
175
|
+
|
|
168
176
|
getUserSettingsDefaultAgentConfig = async () => {
|
|
169
177
|
const result = await this.db
|
|
170
178
|
.select({ defaultAgent: userSettings.defaultAgent })
|
|
@@ -6,10 +6,10 @@ import type {
|
|
|
6
6
|
} from '@lobechat/types';
|
|
7
7
|
import { AiProviderModelListItem, EnabledAiModel } from 'model-bank';
|
|
8
8
|
import { DEFAULT_MODEL_PROVIDER_LIST } from 'model-bank/modelProviders';
|
|
9
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
-
|
|
11
|
-
import { clientDB, initializeDB } from '@/database/client/db';
|
|
9
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
10
|
|
|
11
|
+
import { getTestDB } from '../../models/__tests__/_util';
|
|
12
|
+
import { LobeChatDatabase } from '../../type';
|
|
13
13
|
import { AiInfraRepos } from './index';
|
|
14
14
|
|
|
15
15
|
const userId = 'test-user-id';
|
|
@@ -18,15 +18,18 @@ const mockProviderConfigs = {
|
|
|
18
18
|
anthropic: { enabled: false },
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
let serverDB: LobeChatDatabase;
|
|
21
22
|
let repo: AiInfraRepos;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
await
|
|
25
|
-
vi.clearAllMocks();
|
|
26
|
-
|
|
27
|
-
repo = new AiInfraRepos(clientDB as any, userId, mockProviderConfigs);
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
serverDB = await getTestDB();
|
|
28
26
|
}, 30000);
|
|
29
27
|
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
repo = new AiInfraRepos(serverDB, userId, mockProviderConfigs);
|
|
31
|
+
});
|
|
32
|
+
|
|
30
33
|
describe('AiInfraRepos', () => {
|
|
31
34
|
describe('getAiProviderList', () => {
|
|
32
35
|
it('should merge builtin and user providers correctly', async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { clientDB, initializeDB } from '@/database/client/db';
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
2
|
|
|
3
|
+
import { getTestDB } from '../../models/__tests__/_util';
|
|
5
4
|
import {
|
|
6
5
|
agents,
|
|
7
6
|
agentsKnowledgeBases,
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
import { LobeChatDatabase } from '../../type';
|
|
22
21
|
import { DATA_EXPORT_CONFIG, DataExporterRepos } from './index';
|
|
23
22
|
|
|
24
|
-
let db
|
|
23
|
+
let db: LobeChatDatabase;
|
|
25
24
|
|
|
26
25
|
// 设置测试数据
|
|
27
26
|
describe('DataExporterRepos', () => {
|
|
@@ -38,7 +37,11 @@ describe('DataExporterRepos', () => {
|
|
|
38
37
|
};
|
|
39
38
|
|
|
40
39
|
// 设置测试环境
|
|
41
|
-
|
|
40
|
+
const userId: string = testIds.userId;
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
db = await getTestDB();
|
|
44
|
+
}, 30000);
|
|
42
45
|
|
|
43
46
|
const setupTestData = async () => {
|
|
44
47
|
await db.transaction(async (trx) => {
|
|
@@ -152,10 +155,9 @@ describe('DataExporterRepos', () => {
|
|
|
152
155
|
};
|
|
153
156
|
|
|
154
157
|
beforeEach(async () => {
|
|
155
|
-
//
|
|
156
|
-
await
|
|
157
|
-
|
|
158
|
-
// 插入测试数据
|
|
158
|
+
// 清理并插入测试数据
|
|
159
|
+
await db.delete(users);
|
|
160
|
+
await db.delete(globalFiles);
|
|
159
161
|
await setupTestData();
|
|
160
162
|
}, 30000);
|
|
161
163
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { clientDB, initializeDB } from '@/database/client/db';
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
2
|
|
|
3
|
+
import { getTestDB } from '../../models/__tests__/_util';
|
|
4
|
+
import { LobeChatDatabase } from '../../type';
|
|
5
5
|
import { TableViewerRepo } from './index';
|
|
6
6
|
|
|
7
7
|
const userId = 'user-table-viewer';
|
|
8
|
-
const repo = new TableViewerRepo(clientDB as any, userId);
|
|
9
8
|
|
|
10
9
|
// Mock database execution
|
|
11
10
|
const mockExecute = vi.fn();
|
|
@@ -13,20 +12,20 @@ const mockDB = {
|
|
|
13
12
|
execute: mockExecute,
|
|
14
13
|
};
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
let serverDB: LobeChatDatabase;
|
|
16
|
+
let repo: TableViewerRepo;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
serverDB = await getTestDB();
|
|
20
|
+
repo = new TableViewerRepo(serverDB, userId);
|
|
19
21
|
}, 30000);
|
|
20
22
|
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
21
27
|
describe('TableViewerRepo', () => {
|
|
22
28
|
describe('getAllTables', () => {
|
|
23
|
-
it('should return all tables with counts', async () => {
|
|
24
|
-
const result = await repo.getAllTables();
|
|
25
|
-
|
|
26
|
-
expect(result.length).toEqual(73);
|
|
27
|
-
expect(result[0]).toEqual({ name: 'accounts', count: 0, type: 'BASE TABLE' });
|
|
28
|
-
});
|
|
29
|
-
|
|
30
29
|
it('should handle custom schema', async () => {
|
|
31
30
|
const result = await repo.getAllTables('custom_schema');
|
|
32
31
|
expect(result).toBeDefined();
|
|
@@ -29,7 +29,7 @@ export const params = {
|
|
|
29
29
|
type: 'web_search',
|
|
30
30
|
web_search: {
|
|
31
31
|
enable: true,
|
|
32
|
-
result_sequence: 'before', //
|
|
32
|
+
result_sequence: 'before', // Change search result return sequence to 'before' to minimize OpenAIStream modifications
|
|
33
33
|
search_engine: process.env.ZHIPU_SEARCH_ENGINE || 'search_std', // search_std, search_pro
|
|
34
34
|
search_result: true,
|
|
35
35
|
},
|
|
@@ -69,16 +69,16 @@ export const params = {
|
|
|
69
69
|
const readableStream =
|
|
70
70
|
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
|
71
71
|
|
|
72
|
-
// GLM-4.5
|
|
73
|
-
//
|
|
72
|
+
// GLM-4.5 series models return index -1 in tool_calls, needs to be fixed before entering OpenAIStream
|
|
73
|
+
// because OpenAIStream internally filters out tool_calls with index < 0 (openai.ts:58-60)
|
|
74
74
|
const preprocessedStream = readableStream.pipeThrough(
|
|
75
75
|
new TransformStream({
|
|
76
76
|
transform(chunk, controller) {
|
|
77
|
-
//
|
|
77
|
+
// Handle raw OpenAI ChatCompletionChunk format
|
|
78
78
|
if (chunk.choices && chunk.choices[0]) {
|
|
79
79
|
const choice = chunk.choices[0];
|
|
80
80
|
if (choice.delta?.tool_calls && Array.isArray(choice.delta.tool_calls)) {
|
|
81
|
-
//
|
|
81
|
+
// Fix negative index, convert -1 to positive index based on array position
|
|
82
82
|
const fixedToolCalls = choice.delta.tool_calls.map(
|
|
83
83
|
(toolCall: any, globalIndex: number) => ({
|
|
84
84
|
...toolCall,
|
|
@@ -86,7 +86,7 @@ export const params = {
|
|
|
86
86
|
}),
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
-
//
|
|
89
|
+
// Create fixed chunk
|
|
90
90
|
const fixedChunk = {
|
|
91
91
|
...chunk,
|
|
92
92
|
choices: [
|
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
|
+
});
|
|
@@ -4,15 +4,9 @@ import { serialize } from 'cookie';
|
|
|
4
4
|
import debug from 'debug';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
|
|
7
|
+
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
|
9
8
|
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
10
9
|
import { DiscoverService } from '@/server/services/discover';
|
|
11
|
-
import { FileService } from '@/server/services/file';
|
|
12
|
-
import {
|
|
13
|
-
contentBlocksToString,
|
|
14
|
-
processContentBlocks,
|
|
15
|
-
} from '@/server/services/mcp/contentProcessor';
|
|
16
10
|
import {
|
|
17
11
|
AssistantSorts,
|
|
18
12
|
McpConnectionType,
|
|
@@ -41,93 +35,7 @@ const marketProcedure = publicProcedure
|
|
|
41
35
|
});
|
|
42
36
|
});
|
|
43
37
|
|
|
44
|
-
// Procedure with user authentication for operations requiring user access token
|
|
45
|
-
const authedMarketProcedure = authedProcedure
|
|
46
|
-
.use(serverDatabase)
|
|
47
|
-
.use(marketUserInfo)
|
|
48
|
-
.use(async ({ ctx, next }) => {
|
|
49
|
-
const { UserModel } = await import('@/database/models/user');
|
|
50
|
-
const userModel = new UserModel(ctx.serverDB, ctx.userId);
|
|
51
|
-
|
|
52
|
-
return next({
|
|
53
|
-
ctx: {
|
|
54
|
-
discoverService: new DiscoverService({
|
|
55
|
-
accessToken: ctx.marketAccessToken,
|
|
56
|
-
userInfo: ctx.marketUserInfo,
|
|
57
|
-
}),
|
|
58
|
-
fileService: new FileService(ctx.serverDB, ctx.userId),
|
|
59
|
-
userModel,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
38
|
export const marketRouter = router({
|
|
65
|
-
// ============================== Cloud MCP Gateway ==============================
|
|
66
|
-
callCloudMcpEndpoint: authedMarketProcedure
|
|
67
|
-
.input(
|
|
68
|
-
z.object({
|
|
69
|
-
apiParams: z.record(z.any()),
|
|
70
|
-
identifier: z.string(),
|
|
71
|
-
toolName: z.string(),
|
|
72
|
-
}),
|
|
73
|
-
)
|
|
74
|
-
.mutation(async ({ input, ctx }) => {
|
|
75
|
-
log('callCloudMcpEndpoint input: %O', input);
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
// Query user_settings to get market.accessToken
|
|
79
|
-
const userState = await ctx.userModel.getUserState(async () => ({}));
|
|
80
|
-
const userAccessToken = userState.settings?.market?.accessToken;
|
|
81
|
-
|
|
82
|
-
log('callCloudMcpEndpoint: userAccessToken exists=%s', !!userAccessToken);
|
|
83
|
-
|
|
84
|
-
if (!userAccessToken) {
|
|
85
|
-
throw new TRPCError({
|
|
86
|
-
code: 'UNAUTHORIZED',
|
|
87
|
-
message: 'User access token not found. Please sign in to Market first.',
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const cloudResult = await ctx.discoverService.callCloudMcpEndpoint({
|
|
92
|
-
apiParams: input.apiParams,
|
|
93
|
-
identifier: input.identifier,
|
|
94
|
-
toolName: input.toolName,
|
|
95
|
-
userAccessToken,
|
|
96
|
-
});
|
|
97
|
-
const cloudResultContent = (cloudResult?.content ?? []) as ToolCallContent[];
|
|
98
|
-
|
|
99
|
-
// Format the cloud result to MCPToolCallResult format
|
|
100
|
-
// Process content blocks (upload images, etc.)
|
|
101
|
-
const newContent =
|
|
102
|
-
cloudResult?.isError || !ctx.fileService
|
|
103
|
-
? cloudResultContent
|
|
104
|
-
: // FIXME: the type assertion here is a temporary solution, need to remove it after refactoring
|
|
105
|
-
await processContentBlocks(cloudResultContent, ctx.fileService);
|
|
106
|
-
|
|
107
|
-
// Convert content blocks to string
|
|
108
|
-
const content = contentBlocksToString(newContent);
|
|
109
|
-
const state = { ...cloudResult, content: newContent };
|
|
110
|
-
|
|
111
|
-
if (cloudResult?.isError) {
|
|
112
|
-
return { content, state, success: true };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { content, state, success: true };
|
|
116
|
-
} catch (error) {
|
|
117
|
-
log('Error calling cloud MCP endpoint: %O', error);
|
|
118
|
-
|
|
119
|
-
// Re-throw TRPCError as-is
|
|
120
|
-
if (error instanceof TRPCError) {
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
throw new TRPCError({
|
|
125
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
126
|
-
message: 'Failed to call cloud MCP endpoint',
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}),
|
|
130
|
-
|
|
131
39
|
// ============================== Assistant Market ==============================
|
|
132
40
|
getAssistantCategories: marketProcedure
|
|
133
41
|
.input(
|