@lobehub/chat 1.109.0 → 1.110.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.cursor/rules/i18n.mdc +1 -2
  2. package/CHANGELOG.md +59 -0
  3. package/README.md +1 -1
  4. package/README.zh-CN.md +1 -1
  5. package/apps/desktop/electron-builder.js +22 -0
  6. package/apps/desktop/src/main/controllers/McpInstallCtr.ts +153 -0
  7. package/apps/desktop/src/main/controllers/index.ts +19 -0
  8. package/apps/desktop/src/main/core/App.ts +46 -0
  9. package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +4 -0
  10. package/apps/desktop/src/main/core/infrastructure/ProtocolManager.ts +256 -0
  11. package/apps/desktop/src/main/types/protocol.ts +60 -0
  12. package/apps/desktop/src/main/utils/__tests__/protocol.test.ts +203 -0
  13. package/apps/desktop/src/main/utils/protocol.ts +210 -0
  14. package/changelog/v1.json +21 -0
  15. package/locales/ar/models.json +6 -0
  16. package/locales/ar/plugin.json +196 -136
  17. package/locales/ar/providers.json +3 -0
  18. package/locales/bg-BG/models.json +6 -0
  19. package/locales/bg-BG/plugin.json +204 -144
  20. package/locales/bg-BG/providers.json +3 -0
  21. package/locales/de-DE/models.json +6 -0
  22. package/locales/de-DE/plugin.json +176 -116
  23. package/locales/de-DE/providers.json +3 -0
  24. package/locales/en-US/models.json +6 -0
  25. package/locales/en-US/plugin.json +192 -132
  26. package/locales/en-US/providers.json +3 -0
  27. package/locales/es-ES/models.json +6 -0
  28. package/locales/es-ES/plugin.json +203 -143
  29. package/locales/es-ES/providers.json +3 -0
  30. package/locales/fa-IR/models.json +6 -0
  31. package/locales/fa-IR/plugin.json +155 -95
  32. package/locales/fa-IR/providers.json +3 -0
  33. package/locales/fr-FR/models.json +6 -0
  34. package/locales/fr-FR/plugin.json +161 -101
  35. package/locales/fr-FR/providers.json +3 -0
  36. package/locales/it-IT/models.json +6 -0
  37. package/locales/it-IT/plugin.json +193 -133
  38. package/locales/it-IT/providers.json +3 -0
  39. package/locales/ja-JP/models.json +6 -0
  40. package/locales/ja-JP/plugin.json +195 -135
  41. package/locales/ja-JP/providers.json +3 -0
  42. package/locales/ko-KR/models.json +6 -0
  43. package/locales/ko-KR/plugin.json +163 -103
  44. package/locales/ko-KR/providers.json +3 -0
  45. package/locales/nl-NL/models.json +6 -0
  46. package/locales/nl-NL/plugin.json +211 -151
  47. package/locales/nl-NL/providers.json +3 -0
  48. package/locales/pl-PL/models.json +6 -0
  49. package/locales/pl-PL/plugin.json +171 -111
  50. package/locales/pl-PL/providers.json +3 -0
  51. package/locales/pt-BR/models.json +6 -0
  52. package/locales/pt-BR/plugin.json +180 -120
  53. package/locales/pt-BR/providers.json +3 -0
  54. package/locales/ru-RU/models.json +6 -0
  55. package/locales/ru-RU/plugin.json +191 -131
  56. package/locales/ru-RU/providers.json +3 -0
  57. package/locales/tr-TR/models.json +6 -0
  58. package/locales/tr-TR/plugin.json +187 -127
  59. package/locales/tr-TR/providers.json +3 -0
  60. package/locales/vi-VN/models.json +6 -0
  61. package/locales/vi-VN/plugin.json +152 -92
  62. package/locales/vi-VN/providers.json +3 -0
  63. package/locales/zh-CN/models.json +6 -0
  64. package/locales/zh-CN/plugin.json +60 -0
  65. package/locales/zh-CN/providers.json +3 -0
  66. package/locales/zh-TW/models.json +6 -0
  67. package/locales/zh-TW/plugin.json +157 -97
  68. package/locales/zh-TW/providers.json +3 -0
  69. package/package.json +2 -1
  70. package/packages/electron-client-ipc/src/events/index.ts +5 -2
  71. package/packages/electron-client-ipc/src/events/protocol.ts +29 -0
  72. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  73. package/packages/electron-client-ipc/src/types/mcpInstall.ts +19 -0
  74. package/packages/types/src/plugins/mcp.ts +38 -1
  75. package/packages/types/src/plugins/protocol.ts +166 -0
  76. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +4 -1
  77. package/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/ActionButton/index.tsx +1 -2
  78. package/src/components/KeyValueEditor/index.tsx +4 -2
  79. package/src/config/aiModels/aihubmix.ts +465 -30
  80. package/src/config/aiModels/anthropic.ts +27 -1
  81. package/src/config/aiModels/groq.ts +40 -4
  82. package/src/config/aiModels/qwen.ts +24 -2
  83. package/src/features/MCP/MCPInstallProgress/index.tsx +1 -1
  84. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +30 -36
  85. package/src/features/PluginStore/McpList/List/Item.tsx +1 -1
  86. package/src/features/ProtocolUrlHandler/InstallPlugin/ConfigDisplay.tsx +211 -0
  87. package/src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx +228 -0
  88. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/Detail.tsx +44 -0
  89. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +105 -0
  90. package/src/features/ProtocolUrlHandler/InstallPlugin/index.tsx +55 -0
  91. package/src/features/ProtocolUrlHandler/InstallPlugin/types.ts +45 -0
  92. package/src/features/ProtocolUrlHandler/index.tsx +30 -0
  93. package/src/libs/model-runtime/anthropic/index.ts +15 -2
  94. package/src/libs/model-runtime/utils/modelParse.ts +2 -2
  95. package/src/libs/model-runtime/utils/streams/ollama.test.ts +97 -51
  96. package/src/libs/model-runtime/utils/streams/ollama.ts +4 -0
  97. package/src/locales/default/plugin.ts +60 -0
  98. package/src/store/tool/slices/mcpStore/action.ts +127 -1
  99. package/src/store/tool/slices/mcpStore/initialState.ts +8 -13
  100. package/src/store/tool/slices/mcpStore/selectors.ts +13 -0
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { memo, useState } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import MCPInstallProgress from '@/features/MCP/MCPInstallProgress';
7
+ import Deployment from '@/features/MCPPluginDetail/Deployment';
8
+ import { DetailContextConfig, DetailProvider } from '@/features/MCPPluginDetail/DetailProvider';
9
+ import Header from '@/features/MCPPluginDetail/Header';
10
+ import Nav from '@/features/MCPPluginDetail/Nav';
11
+ import Overview from '@/features/MCPPluginDetail/Overview';
12
+ import Schema from '@/features/MCPPluginDetail/Schema';
13
+ import Score from '@/features/MCPPluginDetail/Score';
14
+ import { McpNavKey } from '@/types/discover';
15
+
16
+ interface OfficialDetailProps {
17
+ data: DetailContextConfig;
18
+ identifier: string;
19
+ }
20
+
21
+ const OfficialDetail = memo<OfficialDetailProps>(({ data, identifier }) => {
22
+ const [activeTab, setActiveTab] = useState(McpNavKey.Overview);
23
+
24
+ return (
25
+ <DetailProvider config={data}>
26
+ <Flexbox gap={16}>
27
+ <Header inModal />
28
+ <MCPInstallProgress identifier={identifier} />
29
+
30
+ <Nav activeTab={activeTab as McpNavKey} inModal noSettings setActiveTab={setActiveTab} />
31
+ <Flexbox gap={24}>
32
+ {activeTab === McpNavKey.Overview && <Overview inModal />}
33
+ {activeTab === McpNavKey.Deployment && <Deployment />}
34
+ {activeTab === McpNavKey.Schema && <Schema />}
35
+ {activeTab === McpNavKey.Score && <Score />}
36
+ </Flexbox>
37
+ </Flexbox>
38
+ </DetailProvider>
39
+ );
40
+ });
41
+
42
+ OfficialDetail.displayName = 'OfficialDetail';
43
+
44
+ export default OfficialDetail;
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { Block, Modal, Text } from '@lobehub/ui';
4
+ import { App } from 'antd';
5
+ import { memo, useCallback, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import DetailLoading from '@/features/PluginStore/McpList/Detail/Loading';
9
+ import { useAgentStore } from '@/store/agent';
10
+ import { useDiscoverStore } from '@/store/discover';
11
+ import { useToolStore } from '@/store/tool';
12
+ import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
13
+
14
+ import { McpInstallRequest } from '../types';
15
+ import OfficialDetail from './Detail';
16
+
17
+ interface OfficialPluginInstallModalProps {
18
+ installRequest: McpInstallRequest | null;
19
+ onComplete: () => void;
20
+ }
21
+
22
+ const OfficialPluginInstallModal = memo<OfficialPluginInstallModalProps>(
23
+ ({ installRequest, onComplete }) => {
24
+ const { message } = App.useApp();
25
+ const { t } = useTranslation(['plugin', 'common']);
26
+ const [loading, setLoading] = useState(false);
27
+
28
+ // 获取 MCP 插件详情
29
+ const useMcpDetail = useDiscoverStore((s) => s.useFetchMcpDetail);
30
+ const identifier = installRequest?.pluginId || '';
31
+
32
+ const [installed, installMCPPlugin] = useToolStore((s) => [
33
+ pluginSelectors.isPluginInstalled(identifier!)(s),
34
+
35
+ s.installMCPPlugin,
36
+ ]);
37
+ const togglePlugin = useAgentStore((s) => s.togglePlugin);
38
+
39
+ const { data, isLoading } = useMcpDetail({ identifier });
40
+
41
+ const handleConfirm = useCallback(async () => {
42
+ if (!installRequest || !data) return;
43
+
44
+ setLoading(true);
45
+ try {
46
+ setLoading(true);
47
+ await installMCPPlugin(identifier);
48
+ await togglePlugin(identifier);
49
+ setLoading(false);
50
+
51
+ message.success(t('protocolInstall.messages.installSuccess', { name: data.name }));
52
+ onComplete();
53
+ } catch (error) {
54
+ console.error('Official plugin installation error:', error);
55
+ message.error(t('protocolInstall.messages.installError'));
56
+ setLoading(false);
57
+ }
58
+ }, [installRequest, data]);
59
+
60
+ if (!installRequest) return null;
61
+
62
+ // 渲染内容
63
+ const renderContent = () => {
64
+ // 如果正在加载,显示骨架屏
65
+ if (isLoading || !identifier) {
66
+ return <DetailLoading />;
67
+ }
68
+
69
+ // 如果加载失败或没有数据,显示错误信息
70
+ if (!data) {
71
+ return (
72
+ <Block>
73
+ <Text type="danger">{t('protocolInstall.messages.manifestError')}</Text>
74
+ </Block>
75
+ );
76
+ }
77
+
78
+ return <OfficialDetail data={data} identifier={identifier} />;
79
+ };
80
+
81
+ return (
82
+ <Modal
83
+ confirmLoading={loading}
84
+ okButtonProps={{
85
+ disabled: installed || isLoading,
86
+ type: installed ? 'default' : 'primary',
87
+ }}
88
+ okText={
89
+ installed ? t('protocolInstall.actions.installed') : t('protocolInstall.actions.install')
90
+ }
91
+ onCancel={onComplete}
92
+ onOk={handleConfirm}
93
+ open
94
+ title={t('protocolInstall.official.title')}
95
+ width={800}
96
+ >
97
+ {renderContent()}
98
+ </Modal>
99
+ );
100
+ },
101
+ );
102
+
103
+ OfficialPluginInstallModal.displayName = 'OfficialPluginInstallModal';
104
+
105
+ export default OfficialPluginInstallModal;
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+
5
+ import CustomPluginInstallModal from './CustomPluginInstallModal';
6
+ import OfficialPluginInstallModal from './OfficialPluginInstallModal';
7
+ import { McpInstallRequest, PluginSource } from './types';
8
+
9
+ interface PluginInstallConfirmModalProps {
10
+ installRequest: McpInstallRequest | null;
11
+ onComplete: () => void;
12
+ }
13
+
14
+ /**
15
+ * 根据安装请求的来源确定插件类型
16
+ */
17
+ const getPluginSource = (request: McpInstallRequest): PluginSource => {
18
+ const { marketId } = request;
19
+
20
+ // 官方 LobeHub 插件
21
+ if (marketId === 'lobehub') {
22
+ return PluginSource.OFFICIAL;
23
+ }
24
+
25
+ // 第三方市场插件(包括可信和不可信的)
26
+ if (marketId && marketId !== 'lobehub') {
27
+ return PluginSource.MARKETPLACE;
28
+ }
29
+
30
+ // 自定义插件(没有 marketId)
31
+ return PluginSource.CUSTOM;
32
+ };
33
+
34
+ const PluginInstallConfirmModal = memo<PluginInstallConfirmModalProps>(
35
+ ({ installRequest, onComplete }) => {
36
+ if (!installRequest) return null;
37
+
38
+ const pluginSource = getPluginSource(installRequest);
39
+
40
+ if (pluginSource === PluginSource.OFFICIAL)
41
+ return <OfficialPluginInstallModal installRequest={installRequest} onComplete={onComplete} />;
42
+
43
+ return (
44
+ <CustomPluginInstallModal
45
+ installRequest={installRequest}
46
+ isMarketplace={pluginSource === PluginSource.MARKETPLACE}
47
+ onComplete={onComplete}
48
+ />
49
+ );
50
+ },
51
+ );
52
+
53
+ PluginInstallConfirmModal.displayName = 'PluginInstallConfirmModal';
54
+
55
+ export default PluginInstallConfirmModal;
@@ -0,0 +1,45 @@
1
+ import { McpInstallSchema } from '@lobechat/electron-client-ipc';
2
+
3
+ export enum PluginSource {
4
+ CUSTOM = 'custom',
5
+ MARKETPLACE = 'marketplace',
6
+ OFFICIAL = 'official',
7
+ }
8
+
9
+ export interface McpInstallRequest {
10
+ marketId?: string;
11
+ pluginId: string;
12
+ schema?: McpInstallSchema;
13
+ source: string;
14
+ }
15
+
16
+ export interface BaseContentProps {
17
+ installRequest: McpInstallRequest;
18
+ }
19
+
20
+ export interface ModalConfig {
21
+ okText: string;
22
+ title: string;
23
+ width?: number;
24
+ }
25
+
26
+ // 可信的第三方市场列表
27
+ export const TRUSTED_MARKETPLACES = {
28
+ higress: {
29
+ description: 'Enterprise-grade MCP plugins for cloud-native applications',
30
+ name: 'Higress Marketplace',
31
+ website: 'https://higress.ai',
32
+ },
33
+ mcprouter: {
34
+ description: 'Community-driven MCP plugin marketplace',
35
+ name: 'MCPRouter',
36
+ website: 'https://mcprouter.com',
37
+ },
38
+ smithery: {
39
+ description: 'Professional MCP plugins and tools',
40
+ name: 'Smithery',
41
+ website: 'https://smithery.ai',
42
+ },
43
+ } as const;
44
+
45
+ export type TrustedMarketplaceId = keyof typeof TRUSTED_MARKETPLACES;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
4
+ import { useCallback, useState } from 'react';
5
+
6
+ import { McpInstallRequest } from '@/features/ProtocolUrlHandler/InstallPlugin/types';
7
+
8
+ import PluginInstallConfirmModal from './InstallPlugin';
9
+
10
+ const ProtocolUrlHandler = () => {
11
+ const [installRequest, setInstallRequest] = useState<McpInstallRequest | null>(null);
12
+
13
+ const handleMcpInstallRequest = useCallback(
14
+ (data: { marketId?: string; pluginId: string; schema: any }) => {
15
+ // 将原始数据传递给子组件处理
16
+ setInstallRequest(data as McpInstallRequest);
17
+ },
18
+ [],
19
+ );
20
+
21
+ const handleComplete = useCallback(() => {
22
+ setInstallRequest(null);
23
+ }, []);
24
+
25
+ useWatchBroadcast('mcpInstallRequest', handleMcpInstallRequest);
26
+
27
+ return <PluginInstallConfirmModal installRequest={installRequest} onComplete={handleComplete} />;
28
+ };
29
+
30
+ export default ProtocolUrlHandler;
@@ -27,6 +27,9 @@ type anthropicTools = Anthropic.Tool | Anthropic.WebSearchTool20250305;
27
27
 
28
28
  const modelsWithSmallContextWindow = new Set(['claude-3-opus-20240229', 'claude-3-haiku-20240307']);
29
29
 
30
+ // Opus 4.1 models that don't allow both temperature and top_p parameters
31
+ const opus41Models = new Set(['claude-opus-4-1', 'claude-opus-4-1-20250805']);
32
+
30
33
  const DEFAULT_BASE_URL = 'https://api.anthropic.com';
31
34
 
32
35
  interface AnthropicAIParams extends ClientOptions {
@@ -189,6 +192,10 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
189
192
  } satisfies Anthropic.MessageCreateParams;
190
193
  }
191
194
 
195
+ // For Opus 4.1 models, we can only set either temperature OR top_p, not both
196
+ const isOpus41Model = opus41Models.has(model);
197
+ const shouldSetTemperature = payload.temperature !== undefined;
198
+
192
199
  return {
193
200
  // claude 3 series model hax max output token of 4096, 3.x series has 8192
194
201
  // https://docs.anthropic.com/en/docs/about-claude/models/all-models#:~:text=200K-,Max%20output,-Normal%3A
@@ -196,9 +203,15 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
196
203
  messages: postMessages,
197
204
  model,
198
205
  system: systemPrompts,
199
- temperature: payload.temperature !== undefined ? temperature / 2 : undefined,
206
+ // For Opus 4.1 models: prefer temperature over top_p if both are provided
207
+ temperature: isOpus41Model
208
+ ? (shouldSetTemperature ? temperature / 2 : undefined)
209
+ : (payload.temperature !== undefined ? temperature / 2 : undefined),
200
210
  tools: postTools,
201
- top_p,
211
+ // For Opus 4.1 models: only set top_p if temperature is not set
212
+ top_p: isOpus41Model
213
+ ? (shouldSetTemperature ? undefined : top_p)
214
+ : top_p,
202
215
  } satisfies Anthropic.MessageCreateParams;
203
216
  }
204
217
 
@@ -30,8 +30,8 @@ export const MODEL_LIST_CONFIGS = {
30
30
  },
31
31
  openai: {
32
32
  excludeKeywords: ['audio'],
33
- functionCallKeywords: ['4o', '4.1', 'o3', 'o4'],
34
- reasoningKeywords: ['o1', 'o3', 'o4'],
33
+ functionCallKeywords: ['4o', '4.1', 'o3', 'o4', 'oss'],
34
+ reasoningKeywords: ['o1', 'o3', 'o4', 'oss'],
35
35
  visionKeywords: ['4o', '4.1', 'o4'],
36
36
  },
37
37
  qwen: {
@@ -7,63 +7,109 @@ import { OllamaStream } from './ollama';
7
7
 
8
8
  describe('OllamaStream', () => {
9
9
  describe('should transform Ollama stream to protocol stream', () => {
10
- it('reasoning', async () => {
11
- vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('2');
12
-
13
- const messages = [
14
- '<think>',
15
- '这是一个思考过程',
16
- ',需要仔细分析问题。',
17
- '</think>',
18
- '根据分析,我的答案是:',
19
- '这是最终答案。',
20
- ];
10
+ describe('reasoning', () => {
11
+ it('reasoning with thinking tag', async () => {
12
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('2');
21
13
 
22
- const mockOllamaStream = new ReadableStream<ChatResponse>({
23
- start(controller) {
24
- messages.forEach((content) => {
25
- controller.enqueue({ message: { content }, done: false } as ChatResponse);
26
- });
27
- controller.enqueue({ message: { content: '' }, done: true } as ChatResponse);
28
- controller.close();
29
- },
14
+ const messages = [
15
+ '<think>',
16
+ '这是一个思考过程',
17
+ ',需要仔细分析问题。',
18
+ '</think>',
19
+ '根据分析,我的答案是:',
20
+ '这是最终答案。',
21
+ ];
22
+
23
+ const mockOllamaStream = new ReadableStream<ChatResponse>({
24
+ start(controller) {
25
+ messages.forEach((content) => {
26
+ controller.enqueue({ message: { content }, done: false } as ChatResponse);
27
+ });
28
+ controller.enqueue({ message: { content: '' }, done: true } as ChatResponse);
29
+ controller.close();
30
+ },
31
+ });
32
+
33
+ const protocolStream = OllamaStream(mockOllamaStream);
34
+
35
+ const decoder = new TextDecoder();
36
+ const chunks = [];
37
+
38
+ // @ts-ignore
39
+ for await (const chunk of protocolStream) {
40
+ chunks.push(decoder.decode(chunk, { stream: true }));
41
+ }
42
+
43
+ expect(chunks).toEqual(
44
+ [
45
+ 'id: chat_2',
46
+ 'event: reasoning',
47
+ `data: ""\n`,
48
+ 'id: chat_2',
49
+ 'event: reasoning',
50
+ `data: "这是一个思考过程"\n`,
51
+ 'id: chat_2',
52
+ 'event: reasoning',
53
+ `data: ",需要仔细分析问题。"\n`,
54
+ 'id: chat_2',
55
+ 'event: text',
56
+ `data: ""\n`,
57
+ 'id: chat_2',
58
+ 'event: text',
59
+ `data: "根据分析,我的答案是:"\n`,
60
+ 'id: chat_2',
61
+ 'event: text',
62
+ `data: "这是最终答案。"\n`,
63
+ 'id: chat_2',
64
+ 'event: stop',
65
+ `data: "finished"\n`,
66
+ ].map((line) => `${line}\n`),
67
+ );
30
68
  });
31
69
 
32
- const protocolStream = OllamaStream(mockOllamaStream);
70
+ it('thinking field', async () => {
71
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
33
72
 
34
- const decoder = new TextDecoder();
35
- const chunks = [];
73
+ const mockOllamaStream = new ReadableStream<ChatResponse>({
74
+ start(controller) {
75
+ controller.enqueue({ message: { thinking: 'Hello' }, done: false } as ChatResponse);
76
+ controller.enqueue({ message: { thinking: ' world!' }, done: false } as ChatResponse);
77
+ controller.enqueue({ message: { thinking: '' }, done: true } as ChatResponse);
36
78
 
37
- // @ts-ignore
38
- for await (const chunk of protocolStream) {
39
- chunks.push(decoder.decode(chunk, { stream: true }));
40
- }
79
+ controller.close();
80
+ },
81
+ });
41
82
 
42
- expect(chunks).toEqual(
43
- [
44
- 'id: chat_2',
45
- 'event: reasoning',
46
- `data: ""\n`,
47
- 'id: chat_2',
48
- 'event: reasoning',
49
- `data: "这是一个思考过程"\n`,
50
- 'id: chat_2',
51
- 'event: reasoning',
52
- `data: ",需要仔细分析问题。"\n`,
53
- 'id: chat_2',
54
- 'event: text',
55
- `data: ""\n`,
56
- 'id: chat_2',
57
- 'event: text',
58
- `data: "根据分析,我的答案是:"\n`,
59
- 'id: chat_2',
60
- 'event: text',
61
- `data: "这是最终答案。"\n`,
62
- 'id: chat_2',
63
- 'event: stop',
64
- `data: "finished"\n`,
65
- ].map((line) => `${line}\n`),
66
- );
83
+ const onStartMock = vi.fn();
84
+ const onTextMock = vi.fn();
85
+ const onCompletionMock = vi.fn();
86
+
87
+ const protocolStream = OllamaStream(mockOllamaStream, {
88
+ onStart: onStartMock,
89
+ onText: onTextMock,
90
+ onCompletion: onCompletionMock,
91
+ });
92
+
93
+ const decoder = new TextDecoder();
94
+ const chunks = [];
95
+
96
+ // @ts-ignore
97
+ for await (const chunk of protocolStream) {
98
+ chunks.push(decoder.decode(chunk, { stream: true }));
99
+ }
100
+
101
+ expect(chunks).toEqual([
102
+ 'id: chat_1\n',
103
+ 'event: reasoning\n',
104
+ `data: "Hello"\n\n`,
105
+ 'id: chat_1\n',
106
+ 'event: reasoning\n',
107
+ `data: " world!"\n\n`,
108
+ 'id: chat_1\n',
109
+ 'event: stop\n',
110
+ `data: "finished"\n\n`,
111
+ ]);
112
+ });
67
113
  });
68
114
 
69
115
  it('text', async () => {
@@ -17,6 +17,10 @@ const transformOllamaStream = (chunk: ChatResponse, stack: StreamContext): Strea
17
17
  return { data: 'finished', id: stack.id, type: 'stop' };
18
18
  }
19
19
 
20
+ if (chunk.message.thinking) {
21
+ return { data: chunk.message.thinking, id: stack.id, type: 'reasoning' };
22
+ }
23
+
20
24
  if (chunk.message.tool_calls && chunk.message.tool_calls.length > 0) {
21
25
  return {
22
26
  data: chunk.message.tool_calls.map((value, index) => ({
@@ -308,6 +308,66 @@ export default {
308
308
  skipDependencies: '跳过检查',
309
309
  },
310
310
  pluginList: '插件列表',
311
+ protocolInstall: {
312
+ actions: {
313
+ install: '安装',
314
+ installAnyway: '仍要安装',
315
+ installed: '已安装',
316
+ },
317
+ config: {
318
+ args: '参数',
319
+ command: '命令',
320
+ env: '环境变量',
321
+ headers: '请求头',
322
+ title: '配置信息',
323
+ type: {
324
+ http: '类型: HTTP',
325
+ label: '类型',
326
+ stdio: '类型: Stdio',
327
+ },
328
+ url: '服务地址',
329
+ },
330
+ custom: {
331
+ badge: '自定义插件',
332
+ security: {
333
+ description: '此插件未经过官方验证,安装可能存在安全风险!请确保您信任插件来源。',
334
+ title: '⚠️ 安全风险提示',
335
+ },
336
+ title: '安装自定义插件',
337
+ },
338
+ marketplace: {
339
+ title: '安装第三方插件',
340
+ trustedBy: '由 {{name}} 提供',
341
+ unverified: {
342
+ title: '未经验证的第三方插件',
343
+ warning: '此插件来自未验证的第三方市场,安装前请确认您信任该来源。',
344
+ },
345
+ verified: '已验证',
346
+ },
347
+ messages: {
348
+ connectionTestFailed: '连接测试失败',
349
+ installError: '插件安装失败,请重试',
350
+ installSuccess: '插件 {{name}} 安装成功!',
351
+ manifestError: '获取插件详情失败,请检查网络连接后重试',
352
+ manifestNotFound: '未能获取插件描述文件',
353
+ },
354
+ meta: {
355
+ author: '作者',
356
+ homepage: '主页',
357
+ identifier: '标识符',
358
+ source: '来源',
359
+ version: '版本',
360
+ },
361
+ official: {
362
+ badge: 'LobeHub 官方插件',
363
+ description: '此插件由 LobeHub 官方开发和维护,经过严格的安全审核,可放心使用。',
364
+ loadingMessage: '正在获取插件详情...',
365
+ loadingTitle: '加载中',
366
+ title: '安装官方插件',
367
+ },
368
+ title: '安装 MCP 插件',
369
+ warning: '⚠️ 请确认您信任此插件的来源,恶意插件可能会危害您的系统安全。',
370
+ },
311
371
  search: {
312
372
  apiName: {
313
373
  crawlMultiPages: '读取多个页面内容',