@lobehub/chat 1.67.2 → 1.68.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 (37) hide show
  1. package/.env.example +4 -0
  2. package/CHANGELOG.md +33 -0
  3. package/Dockerfile +2 -0
  4. package/Dockerfile.database +2 -0
  5. package/README.md +3 -2
  6. package/README.zh-CN.md +1 -1
  7. package/changelog/v1.json +12 -0
  8. package/docs/self-hosting/advanced/auth.mdx +6 -5
  9. package/docs/self-hosting/advanced/auth.zh-CN.mdx +6 -5
  10. package/docs/self-hosting/environment-variables/model-provider.mdx +16 -0
  11. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +16 -0
  12. package/docs/usage/providers/ppio.mdx +57 -0
  13. package/docs/usage/providers/ppio.zh-CN.mdx +55 -0
  14. package/locales/en-US/providers.json +3 -0
  15. package/locales/zh-CN/providers.json +4 -0
  16. package/package.json +5 -5
  17. package/packages/web-crawler/src/__test__/crawler.test.ts +176 -0
  18. package/packages/web-crawler/src/utils/appUrlRules.test.ts +76 -0
  19. package/src/app/[variants]/(main)/settings/llm/ProviderList/providers.tsx +2 -0
  20. package/src/config/aiModels/index.ts +3 -0
  21. package/src/config/aiModels/ppio.ts +276 -0
  22. package/src/config/llm.ts +6 -0
  23. package/src/config/modelProviders/index.ts +4 -0
  24. package/src/config/modelProviders/ppio.ts +249 -0
  25. package/src/libs/agent-runtime/AgentRuntime.ts +7 -0
  26. package/src/libs/agent-runtime/ppio/__snapshots__/index.test.ts.snap +26 -0
  27. package/src/libs/agent-runtime/ppio/fixtures/models.json +42 -0
  28. package/src/libs/agent-runtime/ppio/index.test.ts +264 -0
  29. package/src/libs/agent-runtime/ppio/index.ts +51 -0
  30. package/src/libs/agent-runtime/ppio/type.ts +12 -0
  31. package/src/libs/agent-runtime/types/type.ts +1 -0
  32. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +2 -2
  33. package/src/server/routers/tools/__test__/search.test.ts +146 -0
  34. package/src/store/chat/slices/builtinTool/actions/searXNG.test.ts +67 -0
  35. package/src/store/tool/slices/builtin/selectors.test.ts +12 -0
  36. package/src/store/tool/slices/builtin/selectors.ts +4 -1
  37. package/src/types/user/settings/keyVaults.ts +1 -0
@@ -28,7 +28,7 @@ export const buildAnthropicBlock = async (
28
28
  return {
29
29
  source: {
30
30
  data: base64 as string,
31
- media_type: mimeType as Anthropic.ImageBlockParam.Source['media_type'],
31
+ media_type: mimeType as Anthropic.Base64ImageSource['media_type'],
32
32
  type: 'base64',
33
33
  },
34
34
  type: 'image',
@@ -39,7 +39,7 @@ export const buildAnthropicBlock = async (
39
39
  return {
40
40
  source: {
41
41
  data: base64 as string,
42
- media_type: mimeType as Anthropic.ImageBlockParam.Source['media_type'],
42
+ media_type: mimeType as Anthropic.Base64ImageSource['media_type'],
43
43
  type: 'base64',
44
44
  },
45
45
  type: 'image',
@@ -0,0 +1,146 @@
1
+ // @vitest-environment node
2
+ import { TRPCError } from '@trpc/server';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { toolsEnv } from '@/config/tools';
6
+ import { SearXNGClient } from '@/server/modules/SearXNG';
7
+ import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
8
+
9
+ import { searchRouter } from '../search';
10
+
11
+ // Mock JWT verification
12
+ vi.mock('@/utils/server/jwt', () => ({
13
+ getJWTPayload: vi.fn().mockResolvedValue({ userId: '1' }),
14
+ }));
15
+
16
+ vi.mock('@lobechat/web-crawler', () => ({
17
+ Crawler: vi.fn().mockImplementation(() => ({
18
+ crawl: vi.fn().mockResolvedValue({ content: 'test content' }),
19
+ })),
20
+ }));
21
+
22
+ vi.mock('@/server/modules/SearXNG');
23
+
24
+ describe('searchRouter', () => {
25
+ const mockContext = {
26
+ req: {
27
+ headers: {
28
+ authorization: 'Bearer mock-token',
29
+ },
30
+ },
31
+ authorizationHeader: 'Bearer mock-token',
32
+ jwtPayload: { userId: '1' },
33
+ };
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ // @ts-ignore
38
+ toolsEnv.SEARXNG_URL = 'http://test-searxng.com';
39
+ });
40
+
41
+ describe('crawlPages', () => {
42
+ it('should crawl multiple pages successfully', async () => {
43
+ const caller = searchRouter.createCaller(mockContext as any);
44
+
45
+ const result = await caller.crawlPages({
46
+ urls: ['http://test1.com', 'http://test2.com'],
47
+ impls: ['naive'],
48
+ });
49
+
50
+ expect(result.results).toHaveLength(2);
51
+ expect(result.results[0]).toEqual({ content: 'test content' });
52
+ expect(result.results[1]).toEqual({ content: 'test content' });
53
+ });
54
+
55
+ it('should work without specifying impls', async () => {
56
+ const caller = searchRouter.createCaller(mockContext as any);
57
+
58
+ const result = await caller.crawlPages({
59
+ urls: ['http://test.com'],
60
+ });
61
+
62
+ expect(result.results).toHaveLength(1);
63
+ expect(result.results[0]).toEqual({ content: 'test content' });
64
+ });
65
+ });
66
+
67
+ describe('query', () => {
68
+ it('should throw error if SEARXNG_URL is not configured', async () => {
69
+ // @ts-ignore
70
+ toolsEnv.SEARXNG_URL = undefined;
71
+
72
+ const caller = searchRouter.createCaller(mockContext as any);
73
+
74
+ await expect(
75
+ caller.query({
76
+ query: 'test query',
77
+ }),
78
+ ).rejects.toThrow(
79
+ new TRPCError({ code: 'NOT_IMPLEMENTED', message: SEARCH_SEARXNG_NOT_CONFIG }),
80
+ );
81
+ });
82
+
83
+ it('should return search results successfully', async () => {
84
+ const mockSearchResult = {
85
+ results: [
86
+ {
87
+ title: 'Test Result',
88
+ url: 'http://test.com',
89
+ content: 'Test content',
90
+ },
91
+ ],
92
+ };
93
+
94
+ (SearXNGClient as any).mockImplementation(() => ({
95
+ search: vi.fn().mockResolvedValue(mockSearchResult),
96
+ }));
97
+
98
+ const caller = searchRouter.createCaller(mockContext as any);
99
+
100
+ const result = await caller.query({
101
+ query: 'test query',
102
+ searchEngine: ['google'],
103
+ });
104
+
105
+ expect(result).toEqual(mockSearchResult);
106
+ });
107
+
108
+ it('should work without specifying search engines', async () => {
109
+ const mockSearchResult = {
110
+ results: [
111
+ {
112
+ title: 'Test Result',
113
+ url: 'http://test.com',
114
+ content: 'Test content',
115
+ },
116
+ ],
117
+ };
118
+
119
+ (SearXNGClient as any).mockImplementation(() => ({
120
+ search: vi.fn().mockResolvedValue(mockSearchResult),
121
+ }));
122
+
123
+ const caller = searchRouter.createCaller(mockContext as any);
124
+
125
+ const result = await caller.query({
126
+ query: 'test query',
127
+ });
128
+
129
+ expect(result).toEqual(mockSearchResult);
130
+ });
131
+
132
+ it('should handle search errors', async () => {
133
+ (SearXNGClient as any).mockImplementation(() => ({
134
+ search: vi.fn().mockRejectedValue(new Error('Search failed')),
135
+ }));
136
+
137
+ const caller = searchRouter.createCaller(mockContext as any);
138
+
139
+ await expect(
140
+ caller.query({
141
+ query: 'test query',
142
+ }),
143
+ ).rejects.toThrow(new TRPCError({ code: 'SERVICE_UNAVAILABLE', message: 'Search failed' }));
144
+ });
145
+ });
146
+ });
@@ -4,6 +4,7 @@ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { searchService } from '@/services/search';
5
5
  import { useChatStore } from '@/store/chat';
6
6
  import { chatSelectors } from '@/store/chat/selectors';
7
+ import { CRAWL_CONTENT_LIMITED_COUNT } from '@/tools/web-browsing/const';
7
8
  import { ChatMessage } from '@/types/message';
8
9
  import { SearchContent, SearchQuery, SearchResponse } from '@/types/tool/search';
9
10
 
@@ -11,6 +12,7 @@ import { SearchContent, SearchQuery, SearchResponse } from '@/types/tool/search'
11
12
  vi.mock('@/services/search', () => ({
12
13
  searchService: {
13
14
  search: vi.fn(),
15
+ crawlPages: vi.fn(),
14
16
  },
15
17
  }));
16
18
 
@@ -181,6 +183,71 @@ describe('searXNG actions', () => {
181
183
  });
182
184
  });
183
185
 
186
+ describe('crawlMultiPages', () => {
187
+ it('should truncate content that exceeds limit', async () => {
188
+ const longContent = 'a'.repeat(CRAWL_CONTENT_LIMITED_COUNT + 1000);
189
+ const mockResponse = {
190
+ results: [
191
+ {
192
+ data: {
193
+ content: longContent,
194
+ title: 'Test Page',
195
+ },
196
+ crawler: 'naive',
197
+ originalUrl: 'https://test.com',
198
+ },
199
+ ],
200
+ };
201
+
202
+ (searchService.crawlPages as Mock).mockResolvedValue(mockResponse);
203
+
204
+ const { result } = renderHook(() => useChatStore());
205
+ const messageId = 'test-message-id';
206
+
207
+ await act(async () => {
208
+ await result.current.crawlMultiPages(messageId, { urls: ['https://test.com'] });
209
+ });
210
+
211
+ const expectedContent = [
212
+ {
213
+ content: longContent.slice(0, CRAWL_CONTENT_LIMITED_COUNT),
214
+ title: 'Test Page',
215
+ },
216
+ ];
217
+
218
+ expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
219
+ messageId,
220
+ JSON.stringify(expectedContent),
221
+ );
222
+ });
223
+
224
+ it('should handle crawl errors', async () => {
225
+ const mockResponse = {
226
+ results: [
227
+ {
228
+ errorMessage: 'Failed to crawl',
229
+ errorType: 'CRAWL_ERROR',
230
+ originalUrl: 'https://test.com',
231
+ },
232
+ ],
233
+ };
234
+
235
+ (searchService.crawlPages as Mock).mockResolvedValue(mockResponse);
236
+
237
+ const { result } = renderHook(() => useChatStore());
238
+ const messageId = 'test-message-id';
239
+
240
+ await act(async () => {
241
+ await result.current.crawlMultiPages(messageId, { urls: ['https://test.com'] });
242
+ });
243
+
244
+ expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
245
+ messageId,
246
+ JSON.stringify(mockResponse.results),
247
+ );
248
+ });
249
+ });
250
+
184
251
  describe('reSearchWithSearXNG', () => {
185
252
  it('should update arguments and perform search', async () => {
186
253
  const { result } = renderHook(() => useChatStore());
@@ -41,6 +41,18 @@ describe('builtinToolSelectors', () => {
41
41
  ]);
42
42
  });
43
43
 
44
+ it('should hide tool when not need visible with hidden', () => {
45
+ const state = {
46
+ ...initialState,
47
+ builtinTools: [
48
+ { identifier: 'tool-1', hidden: true, manifest: { meta: { title: 'Tool 1' } } },
49
+ { identifier: DalleManifest.identifier, manifest: { meta: { title: 'Dalle' } } },
50
+ ],
51
+ } as ToolStoreState;
52
+ const result = builtinToolSelectors.metaList(false)(state);
53
+ expect(result).toEqual([]);
54
+ });
55
+
44
56
  it('should return an empty list if no builtin tools are available', () => {
45
57
  const state: ToolStoreState = {
46
58
  ...initialState,
@@ -7,7 +7,10 @@ const metaList =
7
7
  (showDalle?: boolean) =>
8
8
  (s: ToolStoreState): LobeToolMeta[] =>
9
9
  s.builtinTools
10
- .filter((item) => (!showDalle ? item.identifier !== DalleManifest.identifier : !item.hidden))
10
+ .filter(
11
+ (item) =>
12
+ !item.hidden && (!showDalle ? item.identifier !== DalleManifest.identifier : true),
13
+ )
11
14
  .map((t) => ({
12
15
  author: 'LobeHub',
13
16
  identifier: t.identifier,
@@ -65,6 +65,7 @@ export interface UserKeyVaults extends SearchEngineKeyVaults {
65
65
  openrouter?: OpenAICompatibleKeyVault;
66
66
  password?: string;
67
67
  perplexity?: OpenAICompatibleKeyVault;
68
+ ppio?: OpenAICompatibleKeyVault;
68
69
  qwen?: OpenAICompatibleKeyVault;
69
70
  sambanova?: OpenAICompatibleKeyVault;
70
71
  sensenova?: OpenAICompatibleKeyVault;