@lobehub/chat 1.106.2 → 1.106.4
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 +67 -0
- package/apps/desktop/src/preload/routeInterceptor.ts +28 -0
- package/changelog/v1.json +24 -0
- package/locales/ar/models.json +164 -5
- package/locales/bg-BG/models.json +164 -5
- package/locales/de-DE/models.json +164 -5
- package/locales/en-US/models.json +164 -5
- package/locales/es-ES/models.json +164 -5
- package/locales/fa-IR/models.json +164 -5
- package/locales/fr-FR/models.json +164 -5
- package/locales/it-IT/models.json +164 -5
- package/locales/ja-JP/models.json +164 -5
- package/locales/ko-KR/models.json +164 -5
- package/locales/nl-NL/models.json +164 -5
- package/locales/pl-PL/models.json +164 -5
- package/locales/pt-BR/models.json +164 -5
- package/locales/ru-RU/models.json +164 -5
- package/locales/tr-TR/models.json +164 -5
- package/locales/vi-VN/models.json +164 -5
- package/locales/zh-CN/models.json +164 -5
- package/locales/zh-TW/models.json +164 -5
- package/package.json +1 -1
- package/src/config/aiModels/google.ts +0 -48
- package/src/config/aiModels/groq.ts +4 -0
- package/src/config/aiModels/hunyuan.ts +22 -0
- package/src/config/aiModels/moonshot.ts +0 -36
- package/src/config/aiModels/qwen.ts +110 -11
- package/src/config/aiModels/siliconcloud.ts +101 -0
- package/src/config/aiModels/stepfun.ts +0 -53
- package/src/config/aiModels/volcengine.ts +21 -0
- package/src/config/aiModels/zhipu.ts +132 -11
- package/src/config/modelProviders/moonshot.ts +1 -0
- package/src/libs/model-runtime/moonshot/index.ts +10 -1
- package/src/libs/model-runtime/utils/modelParse.ts +2 -2
- package/src/libs/model-runtime/zhipu/index.ts +57 -1
- package/src/server/services/mcp/index.test.ts +161 -0
- package/src/server/services/mcp/index.ts +4 -1
@@ -7,7 +7,7 @@ export interface ModelProcessorConfig {
|
|
7
7
|
visionKeywords?: readonly string[];
|
8
8
|
}
|
9
9
|
|
10
|
-
//
|
10
|
+
// 模型能力标签关键词配置
|
11
11
|
export const MODEL_LIST_CONFIGS = {
|
12
12
|
anthropic: {
|
13
13
|
functionCallKeywords: ['claude'],
|
@@ -64,7 +64,7 @@ export const MODEL_LIST_CONFIGS = {
|
|
64
64
|
},
|
65
65
|
zhipu: {
|
66
66
|
functionCallKeywords: ['glm-4', 'glm-z1'],
|
67
|
-
reasoningKeywords: ['glm-zero', 'glm-z1'],
|
67
|
+
reasoningKeywords: ['glm-zero', 'glm-z1', 'glm-4.5'],
|
68
68
|
visionKeywords: ['glm-4v'],
|
69
69
|
},
|
70
70
|
} as const;
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import { ModelProvider } from '../types';
|
2
2
|
import { MODEL_LIST_CONFIGS, processModelList } from '../utils/modelParse';
|
3
3
|
import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
|
4
|
+
import { OpenAIStream } from '../utils/streams/openai';
|
5
|
+
import { convertIterableToStream } from '../utils/streams/protocol';
|
4
6
|
|
5
7
|
export interface ZhipuModelCard {
|
6
8
|
description: string;
|
@@ -12,7 +14,8 @@ export const LobeZhipuAI = createOpenAICompatibleRuntime({
|
|
12
14
|
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
13
15
|
chatCompletion: {
|
14
16
|
handlePayload: (payload) => {
|
15
|
-
const { enabledSearch, max_tokens, model, temperature, tools, top_p, ...rest } =
|
17
|
+
const { enabledSearch, max_tokens, model, temperature, thinking, tools, top_p, ...rest } =
|
18
|
+
payload;
|
16
19
|
|
17
20
|
const zhipuTools = enabledSearch
|
18
21
|
? [
|
@@ -39,6 +42,7 @@ export const LobeZhipuAI = createOpenAICompatibleRuntime({
|
|
39
42
|
max_tokens,
|
40
43
|
model,
|
41
44
|
stream: true,
|
45
|
+
thinking: model.includes('-4.5') ? { type: thinking?.type } : undefined,
|
42
46
|
tools: zhipuTools,
|
43
47
|
...(model === 'glm-4-alltools'
|
44
48
|
? {
|
@@ -54,6 +58,58 @@ export const LobeZhipuAI = createOpenAICompatibleRuntime({
|
|
54
58
|
}),
|
55
59
|
} as any;
|
56
60
|
},
|
61
|
+
handleStream: (stream, { callbacks, inputStartAt }) => {
|
62
|
+
const readableStream =
|
63
|
+
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
64
|
+
|
65
|
+
// GLM-4.5 系列模型在 tool_calls 中返回的 index 为 -1,需要在进入 OpenAIStream 之前修正
|
66
|
+
// 因为 OpenAIStream 内部会过滤掉 index < 0 的 tool_calls (openai.ts:58-60)
|
67
|
+
const preprocessedStream = readableStream.pipeThrough(
|
68
|
+
new TransformStream({
|
69
|
+
transform(chunk, controller) {
|
70
|
+
// 处理原始的 OpenAI ChatCompletionChunk 格式
|
71
|
+
if (chunk.choices && chunk.choices[0]) {
|
72
|
+
const choice = chunk.choices[0];
|
73
|
+
if (choice.delta?.tool_calls && Array.isArray(choice.delta.tool_calls)) {
|
74
|
+
// 修正负数 index,将 -1 转换为基于数组位置的正数 index
|
75
|
+
const fixedToolCalls = choice.delta.tool_calls.map(
|
76
|
+
(toolCall: any, globalIndex: number) => ({
|
77
|
+
...toolCall,
|
78
|
+
index: toolCall.index < 0 ? globalIndex : toolCall.index,
|
79
|
+
}),
|
80
|
+
);
|
81
|
+
|
82
|
+
// 创建修正后的 chunk
|
83
|
+
const fixedChunk = {
|
84
|
+
...chunk,
|
85
|
+
choices: [
|
86
|
+
{
|
87
|
+
...choice,
|
88
|
+
delta: {
|
89
|
+
...choice.delta,
|
90
|
+
tool_calls: fixedToolCalls,
|
91
|
+
},
|
92
|
+
},
|
93
|
+
],
|
94
|
+
};
|
95
|
+
|
96
|
+
controller.enqueue(fixedChunk);
|
97
|
+
} else {
|
98
|
+
controller.enqueue(chunk);
|
99
|
+
}
|
100
|
+
} else {
|
101
|
+
controller.enqueue(chunk);
|
102
|
+
}
|
103
|
+
},
|
104
|
+
}),
|
105
|
+
);
|
106
|
+
|
107
|
+
return OpenAIStream(preprocessedStream, {
|
108
|
+
callbacks,
|
109
|
+
inputStartAt,
|
110
|
+
provider: 'zhipu',
|
111
|
+
});
|
112
|
+
},
|
57
113
|
},
|
58
114
|
debug: {
|
59
115
|
chatCompletion: () => process.env.DEBUG_ZHIPU_CHAT_COMPLETION === '1',
|
@@ -0,0 +1,161 @@
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
// Mock 依赖
|
5
|
+
vi.mock('@/libs/mcp');
|
6
|
+
|
7
|
+
describe('MCPService', () => {
|
8
|
+
let mcpService: any;
|
9
|
+
let mockClient: any;
|
10
|
+
|
11
|
+
beforeEach(async () => {
|
12
|
+
vi.clearAllMocks();
|
13
|
+
|
14
|
+
// 动态导入服务实例
|
15
|
+
const { mcpService: importedService } = await import('./index');
|
16
|
+
mcpService = importedService;
|
17
|
+
|
18
|
+
// 创建 mock 客户端
|
19
|
+
mockClient = {
|
20
|
+
callTool: vi.fn(),
|
21
|
+
};
|
22
|
+
|
23
|
+
// Mock getClient 方法返回 mock 客户端
|
24
|
+
vi.spyOn(mcpService as any, 'getClient').mockResolvedValue(mockClient);
|
25
|
+
});
|
26
|
+
|
27
|
+
describe('callTool', () => {
|
28
|
+
const mockParams = {
|
29
|
+
name: 'test-mcp',
|
30
|
+
type: 'stdio' as const,
|
31
|
+
command: 'test-command',
|
32
|
+
args: ['--test'],
|
33
|
+
};
|
34
|
+
|
35
|
+
it('should return original data when content array is empty', async () => {
|
36
|
+
mockClient.callTool.mockResolvedValue({
|
37
|
+
content: [],
|
38
|
+
isError: false,
|
39
|
+
});
|
40
|
+
|
41
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
42
|
+
|
43
|
+
expect(result).toEqual([]);
|
44
|
+
});
|
45
|
+
|
46
|
+
it('should return original data when content is null or undefined', async () => {
|
47
|
+
mockClient.callTool.mockResolvedValue({
|
48
|
+
content: null,
|
49
|
+
isError: false,
|
50
|
+
});
|
51
|
+
|
52
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
53
|
+
|
54
|
+
expect(result).toBeNull();
|
55
|
+
});
|
56
|
+
|
57
|
+
it('should return parsed JSON when single element contains valid JSON', async () => {
|
58
|
+
const jsonData = { message: 'Hello World', status: 'success' };
|
59
|
+
mockClient.callTool.mockResolvedValue({
|
60
|
+
content: [{ type: 'text', text: JSON.stringify(jsonData) }],
|
61
|
+
isError: false,
|
62
|
+
});
|
63
|
+
|
64
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
65
|
+
|
66
|
+
expect(result).toEqual(jsonData);
|
67
|
+
});
|
68
|
+
|
69
|
+
it('should return plain text when single element contains non-JSON text', async () => {
|
70
|
+
const textData = 'Hello World';
|
71
|
+
mockClient.callTool.mockResolvedValue({
|
72
|
+
content: [{ type: 'text', text: textData }],
|
73
|
+
isError: false,
|
74
|
+
});
|
75
|
+
|
76
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
77
|
+
|
78
|
+
expect(result).toBe(textData);
|
79
|
+
});
|
80
|
+
|
81
|
+
it('should return original data when single element has no text', async () => {
|
82
|
+
const contentData = [{ type: 'text', text: '' }];
|
83
|
+
mockClient.callTool.mockResolvedValue({
|
84
|
+
content: contentData,
|
85
|
+
isError: false,
|
86
|
+
});
|
87
|
+
|
88
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
89
|
+
|
90
|
+
expect(result).toEqual(contentData);
|
91
|
+
});
|
92
|
+
|
93
|
+
it('should return complete array when content has multiple elements', async () => {
|
94
|
+
const multipleContent = [
|
95
|
+
{ type: 'text', text: 'First message' },
|
96
|
+
{ type: 'text', text: 'Second message' },
|
97
|
+
{ type: 'text', text: '{"json": "data"}' },
|
98
|
+
];
|
99
|
+
|
100
|
+
mockClient.callTool.mockResolvedValue({
|
101
|
+
content: multipleContent,
|
102
|
+
isError: false,
|
103
|
+
});
|
104
|
+
|
105
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
106
|
+
|
107
|
+
// 应该直接返回完整的数组,不进行任何处理
|
108
|
+
expect(result).toEqual(multipleContent);
|
109
|
+
});
|
110
|
+
|
111
|
+
it('should return complete array when content has two elements', async () => {
|
112
|
+
const twoContent = [
|
113
|
+
{ type: 'text', text: 'First message' },
|
114
|
+
{ type: 'text', text: 'Second message' },
|
115
|
+
];
|
116
|
+
|
117
|
+
mockClient.callTool.mockResolvedValue({
|
118
|
+
content: twoContent,
|
119
|
+
isError: false,
|
120
|
+
});
|
121
|
+
|
122
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
123
|
+
|
124
|
+
expect(result).toEqual(twoContent);
|
125
|
+
});
|
126
|
+
|
127
|
+
it('should return error result when isError is true', async () => {
|
128
|
+
const errorResult = {
|
129
|
+
content: [{ type: 'text', text: 'Error occurred' }],
|
130
|
+
isError: true,
|
131
|
+
};
|
132
|
+
|
133
|
+
mockClient.callTool.mockResolvedValue(errorResult);
|
134
|
+
|
135
|
+
const result = await mcpService.callTool(mockParams, 'testTool', '{}');
|
136
|
+
|
137
|
+
expect(result).toEqual(errorResult);
|
138
|
+
});
|
139
|
+
|
140
|
+
it('should throw TRPCError when client throws error', async () => {
|
141
|
+
const error = new Error('MCP client error');
|
142
|
+
mockClient.callTool.mockRejectedValue(error);
|
143
|
+
|
144
|
+
await expect(mcpService.callTool(mockParams, 'testTool', '{}')).rejects.toThrow(TRPCError);
|
145
|
+
});
|
146
|
+
|
147
|
+
it('should parse args string correctly', async () => {
|
148
|
+
const argsObject = { param1: 'value1', param2: 'value2' };
|
149
|
+
const argsString = JSON.stringify(argsObject);
|
150
|
+
|
151
|
+
mockClient.callTool.mockResolvedValue({
|
152
|
+
content: [{ type: 'text', text: 'result' }],
|
153
|
+
isError: false,
|
154
|
+
});
|
155
|
+
|
156
|
+
await mcpService.callTool(mockParams, 'testTool', argsString);
|
157
|
+
|
158
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('testTool', argsObject);
|
159
|
+
});
|
160
|
+
});
|
161
|
+
});
|
@@ -166,8 +166,11 @@ class MCPService {
|
|
166
166
|
|
167
167
|
const data = content as { text: string; type: 'text' }[];
|
168
168
|
|
169
|
-
|
169
|
+
if (!data || data.length === 0) return data;
|
170
170
|
|
171
|
+
if (data.length > 1) return data;
|
172
|
+
|
173
|
+
const text = data[0]?.text;
|
171
174
|
if (!text) return data;
|
172
175
|
|
173
176
|
// try to get json object, which will be stringify in the client
|