@lobehub/chat 1.129.2 → 1.129.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.
Files changed (34) hide show
  1. package/.cursor/rules/project-introduce.mdc +4 -4
  2. package/.cursor/rules/react-component.mdc +2 -2
  3. package/.cursor/rules/typescript.mdc +57 -5
  4. package/.vscode/settings.json +3 -1
  5. package/AGENTS.md +2 -5
  6. package/CHANGELOG.md +50 -0
  7. package/changelog/v1.json +18 -0
  8. package/package.json +2 -2
  9. package/packages/agent-runtime/package.json +1 -1
  10. package/packages/context-engine/package.json +1 -1
  11. package/packages/database/package.json +1 -1
  12. package/packages/electron-server-ipc/package.json +1 -1
  13. package/packages/file-loaders/package.json +1 -1
  14. package/packages/model-bank/package.json +2 -2
  15. package/packages/model-runtime/package.json +1 -1
  16. package/packages/model-runtime/src/core/RouterRuntime/baseRuntimeMap.ts +2 -0
  17. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.test.ts +89 -64
  18. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +58 -40
  19. package/packages/model-runtime/src/providers/newapi/index.ts +17 -2
  20. package/packages/model-runtime/src/providers/qwen/createImage.test.ts +110 -0
  21. package/packages/model-runtime/src/providers/qwen/createImage.ts +100 -3
  22. package/packages/prompts/package.json +1 -1
  23. package/packages/utils/package.json +4 -2
  24. package/packages/utils/src/client/index.ts +2 -0
  25. package/packages/utils/src/client/sanitize.test.ts +108 -0
  26. package/packages/utils/src/client/sanitize.ts +33 -0
  27. package/packages/web-crawler/package.json +1 -1
  28. package/src/features/PluginStore/InstalledList/List/Item/Action.tsx +12 -2
  29. package/src/features/PluginStore/McpList/List/Action.tsx +12 -2
  30. package/src/features/PluginStore/PluginList/List/Action.tsx +12 -2
  31. package/src/features/Portal/Artifacts/Body/Renderer/SVG.tsx +7 -3
  32. package/src/server/modules/EdgeConfig/index.ts +3 -19
  33. package/src/server/modules/EdgeConfig/types.ts +9 -0
  34. /package/packages/utils/src/{clipboard.ts → client/clipboard.ts} +0 -0
@@ -18,13 +18,13 @@ The project uses the following technologies:
18
18
  - Next.js 15 for frontend and backend, using app router instead of pages router
19
19
  - react 19, using hooks, functional components, react server components
20
20
  - TypeScript programming language
21
- - antd, @lobehub/ui for component framework
21
+ - antd, `@lobehub/ui` for component framework
22
22
  - antd-style for css-in-js framework
23
23
  - react-layout-kit for flex layout
24
24
  - react-i18next for i18n
25
- - lucide-react, @ant-design/icons for icons
26
- - @lobehub/icons for AI provider/model logo icon
27
- - @formkit/auto-animate for react list animation
25
+ - lucide-react, `@ant-design/icons` for icons
26
+ - `@lobehub/icons` for AI provider/model logo icon
27
+ - `@formkit/auto-animate` for react list animation
28
28
  - zustand for global state management
29
29
  - nuqs for type-safe search params state manager
30
30
  - SWR for react data fetch
@@ -86,9 +86,9 @@ const Card: FC<CardProps> = ({ title, content }) => {
86
86
 
87
87
  ## Lobe UI 包含的组件
88
88
 
89
- - 不知道 @lobehub/ui 的组件怎么用,有哪些属性,就自己搜下这个项目其它地方怎么用的,不要瞎猜,大部分组件都是在 antd 的基础上扩展了属性
89
+ - 不知道 `@lobehub/ui` 的组件怎么用,有哪些属性,就自己搜下这个项目其它地方怎么用的,不要瞎猜,大部分组件都是在 antd 的基础上扩展了属性
90
90
  - 具体用法不懂可以联网搜索,例如 ActionIcon 就爬取 https://ui.lobehub.com/components/action-icon
91
- - 可以阅读 node_modules/@lobehub/ui/es/index.js 了解有哪些组件,每个组件的属性是什么
91
+ - 可以阅读 `node_modules/@lobehub/ui/es/index.js` 了解有哪些组件,每个组件的属性是什么
92
92
 
93
93
  - General
94
94
  - ActionIcon
@@ -8,11 +8,63 @@ alwaysApply: false
8
8
 
9
9
  ## Types and Type Safety
10
10
 
11
- - Avoid explicit type annotations when TypeScript can infer types.
12
- - Avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
13
- - Use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object`).
14
- - Prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
15
- - Prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
11
+ - avoid explicit type annotations when TypeScript can infer types.
12
+ - avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
13
+ - use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object`).
14
+ - prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
15
+ - prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
16
+ - prefer `@ts-expect-error` over `@ts-ignore`
17
+ - prefer `Record<string, any>` over `any`
18
+
19
+ - **Avoid unnecessary null checks**: Before adding `xxx !== null`, `?.`, `??`, or `!.`, read the type definition to confirm the necessary. **Example:**
20
+
21
+ ```typescript
22
+ // ❌ Wrong: budget.spend and budget.maxBudget is number, not number | null
23
+ if (budget.spend !== null && budget.maxBudget !== null && budget.spend >= budget.maxBudget) {
24
+ // ...
25
+ }
26
+
27
+ // ✅ Right
28
+ if (budget.spend >= budget.maxBudget) {
29
+ // ...
30
+ }
31
+ ```
32
+
33
+ - **Avoid redundant runtime checks**: Don't add runtime validation for conditions already guaranteed by types or previous checks. Trust the type system and calling contract. **Example:**
34
+
35
+ ```typescript
36
+ // ❌ Wrong: Adding impossible-to-fail checks
37
+ const due = await db.query.budgets.findMany({
38
+ where: and(isNotNull(budgets.budgetDuration)), // Already filtered non-null
39
+ });
40
+ const result = due.map(b => {
41
+ const nextReset = computeNextResetAt(b.budgetResetAt!, b.budgetDuration!);
42
+ if (!nextReset) { // This check is impossible to fail
43
+ throw new Error(`Unexpected null nextResetAt`);
44
+ }
45
+ return nextReset;
46
+ });
47
+
48
+ // ✅ Right: Trust the contract
49
+ const due = await db.query.budgets.findMany({
50
+ where: and(isNotNull(budgets.budgetDuration)),
51
+ });
52
+ const result = due.map(b => computeNextResetAt(b.budgetResetAt!, b.budgetDuration!));
53
+ ```
54
+
55
+ - **Avoid meaningless null/undefined parameters**: Don't accept null/undefined for parameters that have no business meaning when null. Design strict function contracts. **Example:**
56
+
57
+ ```typescript
58
+ // ❌ Wrong: Function accepts meaningless null input
59
+ function computeNextResetAt(currentResetAt: Date, durationStr: string | null): Date | null {
60
+ if (!durationStr) return null; // Why accept null if it just returns null?
61
+ }
62
+
63
+ // ✅ Right: Strict contract, clear responsibility
64
+ function computeNextResetAt(currentResetAt: Date, durationStr: string): Date {
65
+ // Function has single clear purpose, caller ensures valid input
66
+ }
67
+ ```
16
68
 
17
69
  ## Imports and Modules
18
70
 
@@ -88,6 +88,8 @@
88
88
  "**/src/server/routers/async/*.ts": "${filename} • async",
89
89
  "**/src/server/routers/edge/*.ts": "${filename} • edge",
90
90
 
91
- "**/src/locales/default/*.ts": "${filename} • locale"
91
+ "**/src/locales/default/*.ts": "${filename} • locale",
92
+
93
+ "**/index.*": "${dirname}/${filename}.${extname}"
92
94
  }
93
95
  }
package/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ Built with modern technologies:
12
12
  - **Database**: PostgreSQL, PGLite, Drizzle ORM
13
13
  - **Testing**: Vitest, Testing Library
14
14
  - **Package Manager**: pnpm (monorepo structure)
15
- - **Build Tools**: Next.js (Turbopack in dev, Webpack in prod), Vitest
15
+ - **Build Tools**: Next.js (Turbopack in dev, Webpack in prod)
16
16
 
17
17
  ## Directory Structure
18
18
 
@@ -28,7 +28,7 @@ The project follows a well-organized monorepo structure:
28
28
 
29
29
  ### Git Workflow
30
30
 
31
- - Use rebase for git pull: `git pull --rebase`
31
+ - Use rebase for git pull
32
32
  - Git commit messages should prefix with gitmoji
33
33
  - Git branch name format: `username/feat/feature-name`
34
34
  - Use `.github/PULL_REQUEST_TEMPLATE.md` for PR descriptions
@@ -52,9 +52,6 @@ The project follows a well-organized monorepo structure:
52
52
  #### React Components
53
53
 
54
54
  - Use functional components with hooks
55
- - Follow the component structure guidelines
56
- - Use antd-style & @lobehub/ui for styling
57
- - Implement proper error boundaries
58
55
 
59
56
  #### Database Schema
60
57
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.129.4](https://github.com/lobehub/lobe-chat/compare/v1.129.3...v1.129.4)
6
+
7
+ <sup>Released on **2025-09-18**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix svg xss issue.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix svg xss issue, closes [#9313](https://github.com/lobehub/lobe-chat/issues/9313) ([9f044ed](https://github.com/lobehub/lobe-chat/commit/9f044ed))
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
+
30
+ ### [Version 1.129.3](https://github.com/lobehub/lobe-chat/compare/v1.129.2...v1.129.3)
31
+
32
+ <sup>Released on **2025-09-17**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Add qwen provider support for image-edit model.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Add qwen provider support for image-edit model, closes [#9277](https://github.com/lobehub/lobe-chat/issues/9277) [#9184](https://github.com/lobehub/lobe-chat/issues/9184) ([e137b33](https://github.com/lobehub/lobe-chat/commit/e137b33))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.129.2](https://github.com/lobehub/lobe-chat/compare/v1.129.1...v1.129.2)
6
56
 
7
57
  <sup>Released on **2025-09-17**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix svg xss issue."
6
+ ]
7
+ },
8
+ "date": "2025-09-18",
9
+ "version": "1.129.4"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Add qwen provider support for image-edit model."
15
+ ]
16
+ },
17
+ "date": "2025-09-17",
18
+ "version": "1.129.3"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.129.2",
3
+ "version": "1.129.4",
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",
@@ -75,7 +75,7 @@
75
75
  "stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
76
76
  "test": "npm run test-app && npm run test-server",
77
77
  "test-app": "vitest run",
78
- "test-app:coverage": "vitest run --coverage",
78
+ "test-app:coverage": "vitest --coverage --silent='passed-only'",
79
79
  "test:update": "vitest -u",
80
80
  "type-check": "tsgo --noEmit",
81
81
  "webhook:ngrok": "ngrok http http://localhost:3011",
@@ -6,7 +6,7 @@
6
6
  "scripts": {
7
7
  "simple": "tsx examples/tools-calling.ts",
8
8
  "test": "vitest",
9
- "test:coverage": "vitest --coverage"
9
+ "test:coverage": "vitest --coverage --silent='passed-only'"
10
10
  },
11
11
  "dependencies": {},
12
12
  "devDependencies": {
@@ -14,7 +14,7 @@
14
14
  "main": "src/index.ts",
15
15
  "scripts": {
16
16
  "test": "vitest",
17
- "test:coverage": "vitest --coverage",
17
+ "test:coverage": "vitest --coverage --silent='passed-only'",
18
18
  "test:update": "vitest -u"
19
19
  },
20
20
  "dependencies": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "test": "npm run test:client-db && npm run test:server-db",
11
11
  "test:client-db": "vitest run",
12
- "test:coverage": "vitest --coverage --config vitest.config.server.mts",
12
+ "test:coverage": "vitest --coverage --silent='passed-only' --config vitest.config.server.mts",
13
13
  "test:server-db": "vitest run --config vitest.config.server.mts"
14
14
  },
15
15
  "dependencies": {
@@ -6,7 +6,7 @@
6
6
  "types": "src/index.ts",
7
7
  "scripts": {
8
8
  "test": "vitest",
9
- "test:coverage": "vitest --coverage"
9
+ "test:coverage": "vitest --coverage --silent='passed-only'"
10
10
  },
11
11
  "dependencies": {
12
12
  "debug": "^4.3.4"
@@ -22,7 +22,7 @@
22
22
  "main": "./src/index.ts",
23
23
  "scripts": {
24
24
  "test": "vitest",
25
- "test:coverage": "vitest --coverage"
25
+ "test:coverage": "vitest --coverage --silent='passed-only'"
26
26
  },
27
27
  "dependencies": {
28
28
  "@langchain/community": "^0.3.41",
@@ -71,9 +71,9 @@
71
71
  },
72
72
  "scripts": {
73
73
  "test": "vitest",
74
- "test:coverage": "vitest --coverage"
74
+ "test:coverage": "vitest --coverage --silent='passed-only'"
75
75
  },
76
76
  "dependencies": {
77
77
  "zod": "^3.25.76"
78
78
  }
79
- }
79
+ }
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "vitest",
12
- "test:coverage": "vitest --coverage",
12
+ "test:coverage": "vitest --coverage --silent='passed-only'",
13
13
  "test:update": "vitest -u"
14
14
  },
15
15
  "dependencies": {
@@ -4,6 +4,7 @@ import { LobeCloudflareAI } from '../../providers/cloudflare';
4
4
  import { LobeFalAI } from '../../providers/fal';
5
5
  import { LobeGoogleAI } from '../../providers/google';
6
6
  import { LobeOpenAI } from '../../providers/openai';
7
+ import { LobeQwenAI } from '../../providers/qwen';
7
8
  import { LobeXAI } from '../../providers/xai';
8
9
 
9
10
  export const baseRuntimeMap = {
@@ -13,5 +14,6 @@ export const baseRuntimeMap = {
13
14
  fal: LobeFalAI,
14
15
  google: LobeGoogleAI,
15
16
  openai: LobeOpenAI,
17
+ qwen: LobeQwenAI,
16
18
  xai: LobeXAI,
17
19
  };
@@ -9,14 +9,15 @@ describe('createRouterRuntime', () => {
9
9
  });
10
10
 
11
11
  describe('initialization', () => {
12
- it('should throw error when routers array is empty', () => {
13
- expect(() => {
14
- const Runtime = createRouterRuntime({
15
- id: 'test-runtime',
16
- routers: [],
17
- });
18
- new Runtime();
19
- }).toThrow('empty providers');
12
+ it('should throw error when routers array is empty', async () => {
13
+ const Runtime = createRouterRuntime({
14
+ id: 'test-runtime',
15
+ routers: [],
16
+ });
17
+ const runtime = new Runtime();
18
+
19
+ // 现在错误在使用时才抛出,因为是延迟创建
20
+ await expect(runtime.getRuntimeByModel('test-model')).rejects.toThrow('empty providers');
20
21
  });
21
22
 
22
23
  it('should create UniformRuntime class with valid routers', () => {
@@ -44,7 +45,7 @@ describe('createRouterRuntime', () => {
44
45
  expect(runtime).toBeDefined();
45
46
  });
46
47
 
47
- it('should merge router options with constructor options', () => {
48
+ it('should merge router options with constructor options', async () => {
48
49
  const mockConstructor = vi.fn();
49
50
 
50
51
  class MockRuntime implements LobeRuntimeAI {
@@ -52,6 +53,10 @@ describe('createRouterRuntime', () => {
52
53
  mockConstructor(options);
53
54
  }
54
55
  chat = vi.fn();
56
+ textToImage = vi.fn();
57
+ models = vi.fn();
58
+ embeddings = vi.fn();
59
+ textToSpeech = vi.fn();
55
60
  }
56
61
 
57
62
  const Runtime = createRouterRuntime({
@@ -61,11 +66,15 @@ describe('createRouterRuntime', () => {
61
66
  apiType: 'openai',
62
67
  options: { baseURL: 'https://api.example.com' },
63
68
  runtime: MockRuntime as any,
69
+ models: ['test-model'],
64
70
  },
65
71
  ],
66
72
  });
67
73
 
68
- new Runtime({ apiKey: 'constructor-key' });
74
+ const runtime = new Runtime({ apiKey: 'constructor-key' });
75
+
76
+ // 触发 runtime 创建
77
+ await runtime.getRuntimeByModel('test-model');
69
78
 
70
79
  expect(mockConstructor).toHaveBeenCalledWith(
71
80
  expect.objectContaining({
@@ -77,10 +86,14 @@ describe('createRouterRuntime', () => {
77
86
  });
78
87
  });
79
88
 
80
- describe('getModels', () => {
81
- it('should return synchronous models array directly', async () => {
89
+ describe('model matching', () => {
90
+ it('should return correct runtime for matching model', async () => {
82
91
  const mockRuntime = {
83
92
  chat: vi.fn(),
93
+ textToImage: vi.fn(),
94
+ models: vi.fn(),
95
+ embeddings: vi.fn(),
96
+ textToSpeech: vi.fn(),
84
97
  } as unknown as LobeRuntimeAI;
85
98
 
86
99
  const Runtime = createRouterRuntime({
@@ -96,50 +109,54 @@ describe('createRouterRuntime', () => {
96
109
  });
97
110
 
98
111
  const runtime = new Runtime();
99
- const models = await runtime['getRouterMatchModels']({
100
- id: 'test',
101
- models: ['model-1', 'model-2'],
102
- runtime: mockRuntime,
103
- });
112
+ const selectedRuntime = await runtime.getRuntimeByModel('model-1');
104
113
 
105
- expect(models).toEqual(['model-1', 'model-2']);
114
+ expect(selectedRuntime).toBeDefined();
106
115
  });
107
116
 
108
- it('should call asynchronous models function', async () => {
117
+ it('should support dynamic routers with asynchronous model fetching', async () => {
109
118
  const mockRuntime = {
110
119
  chat: vi.fn(),
120
+ textToImage: vi.fn(),
121
+ models: vi.fn(),
122
+ embeddings: vi.fn(),
123
+ textToSpeech: vi.fn(),
111
124
  } as unknown as LobeRuntimeAI;
112
125
 
113
126
  const mockModelsFunction = vi.fn().mockResolvedValue(['async-model-1', 'async-model-2']);
114
127
 
115
128
  const Runtime = createRouterRuntime({
116
129
  id: 'test-runtime',
117
- routers: [
118
- {
119
- apiType: 'openai',
120
- options: {},
121
- runtime: mockRuntime.constructor as any,
122
- models: mockModelsFunction,
123
- },
124
- ],
130
+ routers: async () => {
131
+ // 异步获取模型列表
132
+ const models = await mockModelsFunction();
133
+ return [
134
+ {
135
+ apiType: 'openai',
136
+ options: {},
137
+ runtime: mockRuntime.constructor as any,
138
+ models, // 静态数组
139
+ },
140
+ ];
141
+ },
125
142
  });
126
143
 
127
144
  const runtime = new Runtime();
128
- const runtimeItem = {
129
- id: 'test',
130
- models: mockModelsFunction,
131
- runtime: mockRuntime,
132
- };
133
145
 
134
- // Call the function
135
- const models = await runtime['getRouterMatchModels'](runtimeItem);
136
- expect(models).toEqual(['async-model-1', 'async-model-2']);
137
- expect(mockModelsFunction).toHaveBeenCalledTimes(1);
146
+ // 触发 routers 函数调用
147
+ const selectedRuntime = await runtime.getRuntimeByModel('async-model-1');
148
+
149
+ expect(selectedRuntime).toBeDefined();
150
+ expect(mockModelsFunction).toHaveBeenCalled();
138
151
  });
139
152
 
140
- it('should return empty array when models is undefined', async () => {
153
+ it('should return fallback runtime when model not found', async () => {
141
154
  const mockRuntime = {
142
155
  chat: vi.fn(),
156
+ textToImage: vi.fn(),
157
+ models: vi.fn(),
158
+ embeddings: vi.fn(),
159
+ textToSpeech: vi.fn(),
143
160
  } as unknown as LobeRuntimeAI;
144
161
 
145
162
  const Runtime = createRouterRuntime({
@@ -149,17 +166,15 @@ describe('createRouterRuntime', () => {
149
166
  apiType: 'openai',
150
167
  options: {},
151
168
  runtime: mockRuntime.constructor as any,
169
+ models: ['known-model'],
152
170
  },
153
171
  ],
154
172
  });
155
173
 
156
174
  const runtime = new Runtime();
157
- const models = await runtime['getRouterMatchModels']({
158
- id: 'test',
159
- runtime: mockRuntime,
160
- });
175
+ const selectedRuntime = await runtime.getRuntimeByModel('unknown-model');
161
176
 
162
- expect(models).toEqual([]);
177
+ expect(selectedRuntime).toBeDefined();
163
178
  });
164
179
  });
165
180
 
@@ -194,10 +209,10 @@ describe('createRouterRuntime', () => {
194
209
  const runtime = new Runtime();
195
210
 
196
211
  const result1 = await runtime.getRuntimeByModel('gpt-4');
197
- expect(result1).toBe(runtime['_runtimes'][0].runtime);
212
+ expect(result1).toBeInstanceOf(MockRuntime1);
198
213
 
199
214
  const result2 = await runtime.getRuntimeByModel('claude-3');
200
- expect(result2).toBe(runtime['_runtimes'][1].runtime);
215
+ expect(result2).toBeInstanceOf(MockRuntime2);
201
216
  });
202
217
 
203
218
  it('should return last runtime when no model matches', async () => {
@@ -230,7 +245,7 @@ describe('createRouterRuntime', () => {
230
245
  const runtime = new Runtime();
231
246
  const result = await runtime.getRuntimeByModel('unknown-model');
232
247
 
233
- expect(result).toBe(runtime['_runtimes'][1].runtime);
248
+ expect(result).toBeInstanceOf(MockRuntime2);
234
249
  });
235
250
  });
236
251
 
@@ -277,6 +292,9 @@ describe('createRouterRuntime', () => {
277
292
 
278
293
  const Runtime = createRouterRuntime({
279
294
  id: 'test-runtime',
295
+ chatCompletion: {
296
+ handleError,
297
+ },
280
298
  routers: [
281
299
  {
282
300
  apiType: 'openai',
@@ -287,11 +305,7 @@ describe('createRouterRuntime', () => {
287
305
  ],
288
306
  });
289
307
 
290
- const runtime = new Runtime({
291
- chat: {
292
- handleError,
293
- },
294
- });
308
+ const runtime = new Runtime();
295
309
 
296
310
  await expect(
297
311
  runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 }),
@@ -452,7 +466,7 @@ describe('createRouterRuntime', () => {
452
466
  });
453
467
 
454
468
  describe('dynamic routers configuration', () => {
455
- it('should support function-based routers configuration', () => {
469
+ it('should support function-based routers configuration', async () => {
456
470
  class MockRuntime implements LobeRuntimeAI {
457
471
  chat = vi.fn();
458
472
  textToImage = vi.fn();
@@ -461,7 +475,7 @@ describe('createRouterRuntime', () => {
461
475
  textToSpeech = vi.fn();
462
476
  }
463
477
 
464
- const dynamicRoutersFunction = (options: any) => [
478
+ const dynamicRoutersFunction = vi.fn((options: any) => [
465
479
  {
466
480
  apiType: 'openai' as const,
467
481
  options: {
@@ -478,7 +492,7 @@ describe('createRouterRuntime', () => {
478
492
  runtime: MockRuntime as any,
479
493
  models: ['claude-3'],
480
494
  },
481
- ];
495
+ ]);
482
496
 
483
497
  const Runtime = createRouterRuntime({
484
498
  id: 'test-runtime',
@@ -493,21 +507,32 @@ describe('createRouterRuntime', () => {
493
507
  const runtime = new Runtime(userOptions);
494
508
 
495
509
  expect(runtime).toBeDefined();
496
- expect(runtime['_runtimes']).toHaveLength(2);
497
- expect(runtime['_runtimes'][0].id).toBe('openai');
498
- expect(runtime['_runtimes'][1].id).toBe('anthropic');
510
+
511
+ // 测试动态 routers 是否能正确工作
512
+ const result = await runtime.getRuntimeByModel('gpt-4');
513
+ expect(result).toBeDefined();
514
+
515
+ // 验证动态函数被调用时传入了正确的参数
516
+ expect(dynamicRoutersFunction).toHaveBeenCalledWith(
517
+ expect.objectContaining({
518
+ apiKey: 'test-key',
519
+ baseURL: 'https://yourapi.cn',
520
+ }),
521
+ { model: 'gpt-4' },
522
+ );
499
523
  });
500
524
 
501
- it('should throw error when dynamic routers function returns empty array', () => {
525
+ it('should throw error when dynamic routers function returns empty array', async () => {
502
526
  const emptyRoutersFunction = () => [];
503
527
 
504
- expect(() => {
505
- const Runtime = createRouterRuntime({
506
- id: 'test-runtime',
507
- routers: emptyRoutersFunction,
508
- });
509
- new Runtime();
510
- }).toThrow('empty providers');
528
+ const Runtime = createRouterRuntime({
529
+ id: 'test-runtime',
530
+ routers: emptyRoutersFunction,
531
+ });
532
+ const runtime = new Runtime();
533
+
534
+ // 现在错误在使用时才抛出,因为是延迟创建
535
+ await expect(runtime.getRuntimeByModel('test-model')).rejects.toThrow('empty providers');
511
536
  });
512
537
  });
513
538
  });