@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.
- package/.cursor/rules/project-introduce.mdc +4 -4
- package/.cursor/rules/react-component.mdc +2 -2
- package/.cursor/rules/typescript.mdc +57 -5
- package/.vscode/settings.json +3 -1
- package/AGENTS.md +2 -5
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +2 -2
- package/packages/agent-runtime/package.json +1 -1
- package/packages/context-engine/package.json +1 -1
- package/packages/database/package.json +1 -1
- package/packages/electron-server-ipc/package.json +1 -1
- package/packages/file-loaders/package.json +1 -1
- package/packages/model-bank/package.json +2 -2
- package/packages/model-runtime/package.json +1 -1
- package/packages/model-runtime/src/core/RouterRuntime/baseRuntimeMap.ts +2 -0
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.test.ts +89 -64
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +58 -40
- package/packages/model-runtime/src/providers/newapi/index.ts +17 -2
- package/packages/model-runtime/src/providers/qwen/createImage.test.ts +110 -0
- package/packages/model-runtime/src/providers/qwen/createImage.ts +100 -3
- package/packages/prompts/package.json +1 -1
- package/packages/utils/package.json +4 -2
- package/packages/utils/src/client/index.ts +2 -0
- package/packages/utils/src/client/sanitize.test.ts +108 -0
- package/packages/utils/src/client/sanitize.ts +33 -0
- package/packages/web-crawler/package.json +1 -1
- package/src/features/PluginStore/InstalledList/List/Item/Action.tsx +12 -2
- package/src/features/PluginStore/McpList/List/Action.tsx +12 -2
- package/src/features/PluginStore/PluginList/List/Action.tsx +12 -2
- package/src/features/Portal/Artifacts/Body/Renderer/SVG.tsx +7 -3
- package/src/server/modules/EdgeConfig/index.ts +3 -19
- package/src/server/modules/EdgeConfig/types.ts +9 -0
- /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,
|
|
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,
|
|
26
|
-
-
|
|
27
|
-
-
|
|
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
|
-
- 不知道
|
|
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
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
|
package/.vscode/settings.json
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
+
[](#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
|
+
[](#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.
|
|
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
|
|
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",
|
|
@@ -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": {
|
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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('
|
|
81
|
-
it('should return
|
|
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
|
|
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(
|
|
114
|
+
expect(selectedRuntime).toBeDefined();
|
|
106
115
|
});
|
|
107
116
|
|
|
108
|
-
it('should
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
expect(
|
|
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
|
|
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
|
|
158
|
-
id: 'test',
|
|
159
|
-
runtime: mockRuntime,
|
|
160
|
-
});
|
|
175
|
+
const selectedRuntime = await runtime.getRuntimeByModel('unknown-model');
|
|
161
176
|
|
|
162
|
-
expect(
|
|
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).
|
|
212
|
+
expect(result1).toBeInstanceOf(MockRuntime1);
|
|
198
213
|
|
|
199
214
|
const result2 = await runtime.getRuntimeByModel('claude-3');
|
|
200
|
-
expect(result2).
|
|
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).
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
});
|