@lobehub/chat 1.84.17 → 1.84.18

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.84.18](https://github.com/lobehub/lobe-chat/compare/v1.84.17...v1.84.18)
6
+
7
+ <sup>Released on **2025-05-03**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Add perf stat support for openai factory.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Add perf stat support for openai factory, closes [#7677](https://github.com/lobehub/lobe-chat/issues/7677) ([40464d1](https://github.com/lobehub/lobe-chat/commit/40464d1))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.84.17](https://github.com/lobehub/lobe-chat/compare/v1.84.16...v1.84.17)
6
31
 
7
32
  <sup>Released on **2025-05-03**</sup>
package/README.md CHANGED
@@ -330,10 +330,10 @@ In addition, these plugins are not limited to news aggregation, but can also ext
330
330
  | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
331
331
  | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-03-23**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
332
332
  | [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
333
- | [MintbaseSearch](https://lobechat.com/discover/plugin/mintbasesearch)<br/><sup>By **mintbase** on **2024-12-31**</sup> | Find any NFT data on the NEAR Protocol.<br/>`crypto` `nft` |
334
333
  | [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
334
+ | [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
335
335
 
336
- > 📊 Total plugins: [<kbd>**45**</kbd>](https://lobechat.com/discover/plugins)
336
+ > 📊 Total plugins: [<kbd>**44**</kbd>](https://lobechat.com/discover/plugins)
337
337
 
338
338
  <!-- PLUGIN LIST -->
339
339
 
package/README.zh-CN.md CHANGED
@@ -323,10 +323,10 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
323
323
  | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
324
324
  | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-03-23**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
325
325
  | [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
326
- | [MintbaseSearch](https://lobechat.com/discover/plugin/mintbasesearch)<br/><sup>By **mintbase** on **2024-12-31**</sup> | 在 NEAR 协议上查找任何 NFT 数据。<br/>`加密货币` `nft` |
327
326
  | [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
327
+ | [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
328
328
 
329
- > 📊 Total plugins: [<kbd>**45**</kbd>](https://lobechat.com/discover/plugins)
329
+ > 📊 Total plugins: [<kbd>**44**</kbd>](https://lobechat.com/discover/plugins)
330
330
 
331
331
  <!-- PLUGIN LIST -->
332
332
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add perf stat support for openai factory."
6
+ ]
7
+ },
8
+ "date": "2025-05-03",
9
+ "version": "1.84.18"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.84.17",
3
+ "version": "1.84.18",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -68,7 +68,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
68
68
  ) => OpenAI.ChatCompletionCreateParamsStreaming;
69
69
  handleStream?: (
70
70
  stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
71
- callbacks?: ChatStreamCallbacks,
71
+ { callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number },
72
72
  ) => ReadableStream;
73
73
  handleStreamBizErrorType?: (error: {
74
74
  message: string;
@@ -256,7 +256,10 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
256
256
 
257
257
  return StreamingResponse(
258
258
  chatCompletion?.handleStream
259
- ? chatCompletion.handleStream(prod, streamOptions.callbacks)
259
+ ? chatCompletion.handleStream(prod, {
260
+ callbacks: streamOptions.callbacks,
261
+ inputStartAt,
262
+ })
260
263
  : OpenAIStream(prod, { ...streamOptions, inputStartAt }),
261
264
  {
262
265
  headers: options?.headers,
@@ -276,7 +279,10 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
276
279
 
277
280
  return StreamingResponse(
278
281
  chatCompletion?.handleStream
279
- ? chatCompletion.handleStream(stream, streamOptions.callbacks)
282
+ ? chatCompletion.handleStream(stream, {
283
+ callbacks: streamOptions.callbacks,
284
+ inputStartAt,
285
+ })
280
286
  : OpenAIStream(stream, { ...streamOptions, inputStartAt }),
281
287
  {
282
288
  headers: options?.headers,
@@ -46,9 +46,11 @@ describe('QwenAIStream', () => {
46
46
  const onCompletionMock = vi.fn();
47
47
 
48
48
  const protocolStream = QwenAIStream(mockOpenAIStream, {
49
- onStart: onStartMock,
50
- onText: onTextMock,
51
- onCompletion: onCompletionMock,
49
+ callbacks: {
50
+ onStart: onStartMock,
51
+ onText: onTextMock,
52
+ onCompletion: onCompletionMock,
53
+ },
52
54
  });
53
55
 
54
56
  const decoder = new TextDecoder();
@@ -111,7 +113,9 @@ describe('QwenAIStream', () => {
111
113
  const onToolCallMock = vi.fn();
112
114
 
113
115
  const protocolStream = QwenAIStream(mockOpenAIStream, {
114
- onToolsCalling: onToolCallMock,
116
+ callbacks: {
117
+ onToolsCalling: onToolCallMock,
118
+ },
115
119
  });
116
120
 
117
121
  const decoder = new TextDecoder();
@@ -92,7 +92,9 @@ export const transformQwenStream = (chunk: OpenAI.ChatCompletionChunk): StreamPr
92
92
 
93
93
  export const QwenAIStream = (
94
94
  stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
95
- callbacks?: ChatStreamCallbacks,
95
+ // TODO: preserve for RFC 097
96
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
97
+ { callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
96
98
  ) => {
97
99
  const readableStream =
98
100
  stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
@@ -96,7 +96,9 @@ describe('SparkAIStream', () => {
96
96
  const onToolCallMock = vi.fn();
97
97
 
98
98
  const protocolStream = SparkAIStream(mockStream, {
99
- onToolsCalling: onToolCallMock,
99
+ callbacks: {
100
+ onToolsCalling: onToolCallMock,
101
+ },
100
102
  });
101
103
 
102
104
  const decoder = new TextDecoder();
@@ -156,7 +158,9 @@ describe('SparkAIStream', () => {
156
158
  const onTextMock = vi.fn();
157
159
 
158
160
  const protocolStream = SparkAIStream(mockStream, {
159
- onText: onTextMock,
161
+ callbacks: {
162
+ onText: onTextMock,
163
+ },
160
164
  });
161
165
 
162
166
  const decoder = new TextDecoder();
@@ -123,7 +123,9 @@ export const transformSparkStream = (chunk: OpenAI.ChatCompletionChunk): StreamP
123
123
 
124
124
  export const SparkAIStream = (
125
125
  stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
126
- callbacks?: ChatStreamCallbacks,
126
+ // TODO: preserve for RFC 097
127
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
128
+ { callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
127
129
  ) => {
128
130
  const readableStream =
129
131
  stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
@@ -0,0 +1,211 @@
1
+ import { LocalFileItem, LocalMoveFilesResultItem } from '@lobechat/electron-client-ipc';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { localFileService } from '@/services/electron/localFileService';
5
+ import { ChatStore } from '@/store/chat/store';
6
+
7
+ import { localFileSlice } from '../localFile';
8
+
9
+ vi.mock('@/services/electron/localFileService', () => ({
10
+ localFileService: {
11
+ listLocalFiles: vi.fn(),
12
+ moveLocalFiles: vi.fn(),
13
+ readLocalFile: vi.fn(),
14
+ readLocalFiles: vi.fn(),
15
+ renameLocalFile: vi.fn(),
16
+ searchLocalFiles: vi.fn(),
17
+ writeFile: vi.fn(),
18
+ },
19
+ }));
20
+
21
+ const mockSet = vi.fn();
22
+
23
+ const mockStore = {
24
+ internal_triggerLocalFileToolCalling: vi.fn(),
25
+ internal_updateMessageContent: vi.fn(),
26
+ internal_updateMessagePluginError: vi.fn(),
27
+ set: mockSet,
28
+ toggleLocalFileLoading: vi.fn(),
29
+ updatePluginArguments: vi.fn(),
30
+ updatePluginState: vi.fn(),
31
+ } as unknown as ChatStore;
32
+
33
+ const createStore = () => {
34
+ return localFileSlice(
35
+ (set) => ({
36
+ ...mockStore,
37
+ set,
38
+ }),
39
+ () => mockStore,
40
+ {} as any,
41
+ );
42
+ };
43
+
44
+ describe('localFileSlice', () => {
45
+ const store = createStore();
46
+
47
+ beforeEach(() => {
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ describe('internal_triggerLocalFileToolCalling', () => {
52
+ it('should handle successful calling', async () => {
53
+ const mockContent = { foo: 'bar' };
54
+ const mockState = { state: 'test' };
55
+ const mockService = vi.fn().mockResolvedValue({ content: mockContent, state: mockState });
56
+
57
+ await store.internal_triggerLocalFileToolCalling('test-id', mockService);
58
+
59
+ expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
60
+ expect(mockStore.updatePluginState).toBeCalledWith('test-id', mockState);
61
+ expect(mockStore.internal_updateMessageContent).toBeCalledWith(
62
+ 'test-id',
63
+ JSON.stringify(mockContent),
64
+ );
65
+ expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', false);
66
+ });
67
+
68
+ it('should handle error', async () => {
69
+ const mockError = new Error('test error');
70
+ const mockService = vi.fn().mockRejectedValue(mockError);
71
+
72
+ await store.internal_triggerLocalFileToolCalling('test-id', mockService);
73
+
74
+ expect(mockStore.internal_updateMessagePluginError).toBeCalledWith('test-id', {
75
+ body: mockError,
76
+ message: 'test error',
77
+ type: 'PluginServerError',
78
+ });
79
+ });
80
+ });
81
+
82
+ describe('listLocalFiles', () => {
83
+ it('should call listLocalFiles service and update state', async () => {
84
+ const mockResult: LocalFileItem[] = [
85
+ {
86
+ name: 'test.txt',
87
+ path: '/test.txt',
88
+ isDirectory: false,
89
+ createdTime: new Date(),
90
+ lastAccessTime: new Date(),
91
+ modifiedTime: new Date(),
92
+ size: 100,
93
+ type: 'file',
94
+ },
95
+ ];
96
+ vi.mocked(localFileService.listLocalFiles).mockResolvedValue(mockResult);
97
+
98
+ await store.listLocalFiles('test-id', { path: '/test' });
99
+
100
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
101
+ });
102
+ });
103
+
104
+ describe('moveLocalFiles', () => {
105
+ it('should handle successful move', async () => {
106
+ const mockResults = [
107
+ {
108
+ sourcePath: '/test.txt',
109
+ destinationPath: '/target/test.txt',
110
+ success: true,
111
+ },
112
+ ] as unknown as LocalMoveFilesResultItem[];
113
+
114
+ vi.mocked(localFileService.moveLocalFiles).mockResolvedValue(mockResults);
115
+
116
+ await store.moveLocalFiles('test-id', {
117
+ sourcePaths: ['/test.txt'],
118
+ destinationDir: '/target',
119
+ } as any);
120
+
121
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
122
+ });
123
+ });
124
+
125
+ describe('writeLocalFile', () => {
126
+ it('should handle successful write', async () => {
127
+ vi.mocked(localFileService.writeFile).mockResolvedValue({
128
+ success: true,
129
+ newPath: '/test.txt',
130
+ });
131
+
132
+ await store.writeLocalFile('test-id', { path: '/test.txt', content: 'test' });
133
+
134
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
135
+ });
136
+
137
+ it('should handle write error', async () => {
138
+ vi.mocked(localFileService.writeFile).mockResolvedValue({
139
+ success: false,
140
+ error: 'Write failed',
141
+ newPath: '/test.txt',
142
+ });
143
+
144
+ await store.writeLocalFile('test-id', { path: '/test.txt', content: 'test' });
145
+
146
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
147
+ });
148
+ });
149
+
150
+ describe('renameLocalFile', () => {
151
+ it('should handle successful rename', async () => {
152
+ vi.mocked(localFileService.renameLocalFile).mockResolvedValue({
153
+ success: true,
154
+ newPath: '/new.txt',
155
+ });
156
+
157
+ await store.renameLocalFile('test-id', { path: '/test.txt', newName: 'new.txt' });
158
+
159
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
160
+ });
161
+
162
+ it('should handle rename error', async () => {
163
+ vi.mocked(localFileService.renameLocalFile).mockResolvedValue({
164
+ success: false,
165
+ error: 'Rename failed',
166
+ newPath: '/test.txt',
167
+ });
168
+
169
+ await store.renameLocalFile('test-id', { path: '/test.txt', newName: 'new.txt' });
170
+
171
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
172
+ });
173
+
174
+ it('should validate new filename', async () => {
175
+ vi.mocked(localFileService.renameLocalFile).mockRejectedValue(
176
+ new Error('Invalid new name provided'),
177
+ );
178
+
179
+ await store.renameLocalFile('test-id', {
180
+ path: '/test.txt',
181
+ newName: '../invalid.txt',
182
+ });
183
+
184
+ expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalledWith(
185
+ 'test-id',
186
+ expect.any(Function),
187
+ );
188
+ });
189
+ });
190
+
191
+ describe('toggleLocalFileLoading', () => {
192
+ it('should toggle loading state', () => {
193
+ const mockSetFn = vi.fn();
194
+ const testStore = localFileSlice(mockSetFn, () => mockStore, {} as any);
195
+
196
+ testStore.toggleLocalFileLoading('test-id', true);
197
+ expect(mockSetFn).toHaveBeenCalledWith(
198
+ expect.any(Function),
199
+ false,
200
+ 'toggleLocalFileLoading/start',
201
+ );
202
+
203
+ testStore.toggleLocalFileLoading('test-id', false);
204
+ expect(mockSetFn).toHaveBeenCalledWith(
205
+ expect.any(Function),
206
+ false,
207
+ 'toggleLocalFileLoading/end',
208
+ );
209
+ });
210
+ });
211
+ });