@lobehub/chat 1.129.3 → 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 (30) 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 +25 -0
  7. package/changelog/v1.json +9 -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/createRuntime.test.ts +89 -64
  17. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +58 -40
  18. package/packages/prompts/package.json +1 -1
  19. package/packages/utils/package.json +4 -2
  20. package/packages/utils/src/client/index.ts +2 -0
  21. package/packages/utils/src/client/sanitize.test.ts +108 -0
  22. package/packages/utils/src/client/sanitize.ts +33 -0
  23. package/packages/web-crawler/package.json +1 -1
  24. package/src/features/PluginStore/InstalledList/List/Item/Action.tsx +12 -2
  25. package/src/features/PluginStore/McpList/List/Action.tsx +12 -2
  26. package/src/features/PluginStore/PluginList/List/Action.tsx +12 -2
  27. package/src/features/Portal/Artifacts/Body/Renderer/SVG.tsx +7 -3
  28. package/src/server/modules/EdgeConfig/index.ts +3 -19
  29. package/src/server/modules/EdgeConfig/types.ts +9 -0
  30. /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,31 @@
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
+
5
30
  ### [Version 1.129.3](https://github.com/lobehub/lobe-chat/compare/v1.129.2...v1.129.3)
6
31
 
7
32
  <sup>Released on **2025-09-17**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
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
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.129.3",
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": {
@@ -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
  });
@@ -25,7 +25,7 @@ import { baseRuntimeMap } from './baseRuntimeMap';
25
25
 
26
26
  export interface RuntimeItem {
27
27
  id: string;
28
- models?: string[] | (() => Promise<string[]>);
28
+ models?: string[];
29
29
  runtime: LobeRuntimeAI;
30
30
  }
31
31
 
@@ -45,13 +45,22 @@ export type RuntimeClass = typeof LobeOpenAI;
45
45
 
46
46
  interface RouterInstance {
47
47
  apiType: keyof typeof baseRuntimeMap;
48
- models?: string[] | (() => Promise<string[]>);
48
+ models?: string[];
49
49
  options: ProviderIniOptions;
50
50
  runtime?: RuntimeClass;
51
51
  }
52
52
 
53
53
  type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
54
54
 
55
+ type Routers =
56
+ | RouterInstance[]
57
+ | ((
58
+ options: ClientOptions & Record<string, any>,
59
+ runtimeContext: {
60
+ model?: string;
61
+ },
62
+ ) => RouterInstance[] | Promise<RouterInstance[]>);
63
+
55
64
  interface CreateRouterRuntimeOptions<T extends Record<string, any> = any> {
56
65
  apiKey?: string;
57
66
  chatCompletion?: {
@@ -104,77 +113,82 @@ interface CreateRouterRuntimeOptions<T extends Record<string, any> = any> {
104
113
  options: ConstructorOptions<T>,
105
114
  ) => ChatStreamPayload;
106
115
  };
107
- routers: RouterInstance[] | ((options: ClientOptions & Record<string, any>) => RouterInstance[]);
116
+ routers: Routers;
108
117
  }
109
118
 
110
119
  export const createRouterRuntime = ({
111
120
  id,
112
121
  routers,
113
122
  apiKey: DEFAULT_API_LEY,
114
- models,
123
+ models: modelsOption,
115
124
  ...params
116
125
  }: CreateRouterRuntimeOptions) => {
117
126
  return class UniformRuntime implements LobeRuntimeAI {
118
- private _runtimes: RuntimeItem[];
119
127
  private _options: ClientOptions & Record<string, any>;
128
+ private _routers: Routers;
129
+ private _params: any;
130
+ private _id: string;
120
131
 
121
132
  constructor(options: ClientOptions & Record<string, any> = {}) {
122
- const _options = {
133
+ this._options = {
123
134
  ...options,
124
135
  apiKey: options.apiKey?.trim() || DEFAULT_API_LEY,
125
136
  baseURL: options.baseURL?.trim(),
126
137
  };
127
138
 
128
- // 支持动态 routers 配置
129
- const resolvedRouters = typeof routers === 'function' ? routers(_options) : routers;
139
+ // 保存配置但不创建 runtimes
140
+ this._routers = routers;
141
+ this._params = params;
142
+ this._id = id;
143
+ }
144
+
145
+ /**
146
+ * TODO: routers 如果是静态对象,可以提前生成 runtimes, 避免运行时生成开销
147
+ */
148
+ private async createRuntimesByRouters(model?: string): Promise<RuntimeItem[]> {
149
+ // 动态获取 routers,支持传入 model
150
+ const resolvedRouters =
151
+ typeof this._routers === 'function'
152
+ ? await this._routers(this._options, { model })
153
+ : this._routers;
130
154
 
131
155
  if (resolvedRouters.length === 0) {
132
156
  throw new Error('empty providers');
133
157
  }
134
158
 
135
- this._runtimes = resolvedRouters.map((router) => {
159
+ return resolvedRouters.map((router) => {
136
160
  const providerAI = router.runtime ?? baseRuntimeMap[router.apiType] ?? LobeOpenAI;
137
-
138
- const finalOptions = { ...params, ...options, ...router.options };
139
- // @ts-ignore
140
- const runtime: LobeRuntimeAI = new providerAI({ ...finalOptions, id });
141
-
142
- return { id: router.apiType, models: router.models, runtime };
161
+ const finalOptions = { ...this._params, ...this._options, ...router.options };
162
+ const runtime: LobeRuntimeAI = new providerAI({ ...finalOptions, id: this._id });
163
+
164
+ return {
165
+ id: router.apiType,
166
+ models: router.models,
167
+ runtime,
168
+ };
143
169
  });
144
-
145
- this._options = _options;
146
- }
147
-
148
- // Get runtime's models list, supporting both synchronous arrays and asynchronous functions
149
- private async getRouterMatchModels(runtimeItem: RuntimeItem): Promise<string[]> {
150
- // If it's a synchronous array, return directly
151
- if (typeof runtimeItem.models !== 'function') {
152
- return runtimeItem.models || [];
153
- }
154
-
155
- // Get model list
156
- return await runtimeItem.models();
157
170
  }
158
171
 
159
172
  // Check if it can match a specific model, otherwise default to using the last runtime
160
173
  async getRuntimeByModel(model: string) {
161
- for (const runtimeItem of this._runtimes) {
162
- const models = await this.getRouterMatchModels(runtimeItem);
174
+ const runtimes = await this.createRuntimesByRouters(model);
175
+
176
+ for (const runtimeItem of runtimes) {
177
+ const models = runtimeItem.models || [];
163
178
  if (models.includes(model)) {
164
179
  return runtimeItem.runtime;
165
180
  }
166
181
  }
167
- return this._runtimes.at(-1)!.runtime;
182
+ return runtimes.at(-1)!.runtime;
168
183
  }
169
184
 
170
185
  async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
171
186
  try {
172
187
  const runtime = await this.getRuntimeByModel(payload.model);
173
-
174
188
  return await runtime.chat!(payload, options);
175
189
  } catch (e) {
176
- if (this._options.chat?.handleError) {
177
- const error = this._options.chat.handleError(e);
190
+ if (params.chatCompletion?.handleError) {
191
+ const error = params.chatCompletion.handleError(e, this._options);
178
192
 
179
193
  if (error) {
180
194
  throw error;
@@ -192,20 +206,24 @@ export const createRouterRuntime = ({
192
206
 
193
207
  async textToImage(payload: TextToImagePayload) {
194
208
  const runtime = await this.getRuntimeByModel(payload.model);
195
-
196
209
  return runtime.textToImage!(payload);
197
210
  }
198
211
 
199
212
  async models() {
200
- if (models && typeof models === 'function') {
201
- // If it's function-style configuration, use the last runtime's client to call the function
202
- const lastRuntime = this._runtimes.at(-1)?.runtime;
213
+ if (modelsOption && typeof modelsOption === 'function') {
214
+ // 延迟创建 runtimes
215
+ const runtimes = await this.createRuntimesByRouters();
216
+ // 如果是函数式配置,使用最后一个运行时的客户端来调用函数
217
+ const lastRuntime = runtimes.at(-1)?.runtime;
203
218
  if (lastRuntime && 'client' in lastRuntime) {
204
- const modelList = await models({ client: (lastRuntime as any).client });
219
+ const modelList = await modelsOption({ client: (lastRuntime as any).client });
205
220
  return await postProcessModelList(modelList);
206
221
  }
207
222
  }
208
- return this._runtimes.at(-1)?.runtime.models?.();
223
+
224
+ // 延迟创建 runtimes
225
+ const runtimes = await this.createRuntimesByRouters();
226
+ return runtimes.at(-1)?.runtime.models?.();
209
227
  }
210
228
 
211
229
  async embeddings(payload: EmbeddingsPayload, options?: EmbeddingsOptions) {
@@ -5,7 +5,7 @@
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
7
7
  "test": "vitest",
8
- "test:coverage": "vitest --coverage"
8
+ "test:coverage": "vitest --coverage --silent='passed-only'"
9
9
  },
10
10
  "dependencies": {
11
11
  "@lobechat/types": "workspace:*"
@@ -9,14 +9,16 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "vitest",
12
- "test:coverage": "vitest --coverage"
12
+ "test:coverage": "vitest --coverage --silent='passed-only'"
13
13
  },
14
14
  "dependencies": {
15
15
  "@lobechat/const": "workspace:*",
16
16
  "@lobechat/types": "workspace:*",
17
- "dayjs": "^1.11.18"
17
+ "dayjs": "^1.11.18",
18
+ "dompurify": "^3.2.7"
18
19
  },
19
20
  "devDependencies": {
21
+ "@types/dompurify": "^3.2.0",
20
22
  "vitest-canvas-mock": "^0.3.3"
21
23
  }
22
24
  }
@@ -1,2 +1,4 @@
1
+ export * from './clipboard';
1
2
  export * from './downloadFile';
2
3
  export * from './exportFile';
4
+ export * from './sanitize';
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { sanitizeSVGContent } from './sanitize';
4
+
5
+ describe('sanitizeSVGContent', () => {
6
+ it('should preserve safe SVG elements and attributes', () => {
7
+ const safeSvg = `
8
+ <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
9
+ <circle cx="50" cy="50" r="40" fill="red" stroke="blue" stroke-width="2" />
10
+ <rect x="10" y="10" width="30" height="30" fill="green" />
11
+ <path d="M10,20 L30,40" stroke="black" />
12
+ </svg>
13
+ `;
14
+
15
+ const sanitized = sanitizeSVGContent(safeSvg);
16
+
17
+ expect(sanitized).toContain('<svg');
18
+ expect(sanitized).toContain('xmlns="http://www.w3.org/2000/svg"');
19
+ expect(sanitized).toContain('<circle');
20
+ expect(sanitized).toContain('fill="red"');
21
+ expect(sanitized).toContain('<rect');
22
+ expect(sanitized).toContain('<path');
23
+ });
24
+
25
+ it('should remove dangerous script tags', () => {
26
+ const maliciousSvg = `
27
+ <svg xmlns="http://www.w3.org/2000/svg">
28
+ <script>alert('XSS')</script>
29
+ <circle cx="50" cy="50" r="40" fill="red" />
30
+ </svg>
31
+ `;
32
+
33
+ const sanitized = sanitizeSVGContent(maliciousSvg);
34
+
35
+ expect(sanitized).not.toContain('<script>');
36
+ expect(sanitized).not.toContain('alert');
37
+ expect(sanitized).toContain('<svg');
38
+ });
39
+
40
+ it('should remove dangerous event handler attributes', () => {
41
+ const maliciousSvg = `
42
+ <svg xmlns="http://www.w3.org/2000/svg">
43
+ <circle cx="50" cy="50" r="40" fill="red" onclick="alert('click')" onload="alert('load')" />
44
+ </svg>
45
+ `;
46
+
47
+ const sanitized = sanitizeSVGContent(maliciousSvg);
48
+
49
+ expect(sanitized).not.toContain('onclick');
50
+ expect(sanitized).not.toContain('onload');
51
+ expect(sanitized).toContain('<circle');
52
+ expect(sanitized).toContain('fill="red"');
53
+ });
54
+
55
+ it('should remove dangerous embed and object tags', () => {
56
+ const maliciousSvg = `
57
+ <svg xmlns="http://www.w3.org/2000/svg">
58
+ <object data="malicious.swf"></object>
59
+ <embed src="malicious.swf"></embed>
60
+ <circle cx="50" cy="50" r="40" fill="red" />
61
+ </svg>
62
+ `;
63
+
64
+ const sanitized = sanitizeSVGContent(maliciousSvg);
65
+
66
+ // Note: DOMPurify with SVG profile may still allow some elements
67
+ // The key security protection is removing script and event handlers
68
+ expect(sanitized).toContain('<circle');
69
+ expect(sanitized).toContain('fill="red"');
70
+ });
71
+
72
+ it('should handle empty or invalid SVG content gracefully', () => {
73
+ expect(sanitizeSVGContent('')).toBe('');
74
+ expect(sanitizeSVGContent('<invalid>content</invalid>')).toBe('');
75
+ });
76
+
77
+ it('should preserve complex SVG structures while removing threats', () => {
78
+ const complexSvg = `
79
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
80
+ <defs>
81
+ <linearGradient id="grad1">
82
+ <stop offset="0%" stop-color="red" />
83
+ <stop offset="100%" stop-color="blue" />
84
+ </linearGradient>
85
+ </defs>
86
+ <g transform="translate(50,50)">
87
+ <script>malicious()</script>
88
+ <circle cx="50" cy="50" r="40" fill="url(#grad1)" onclick="hack()" />
89
+ <text x="50" y="60" text-anchor="middle" onload="evil()">Hello</text>
90
+ </g>
91
+ </svg>
92
+ `;
93
+
94
+ const sanitized = sanitizeSVGContent(complexSvg);
95
+
96
+ // Should preserve safe elements and attributes
97
+ expect(sanitized).toEqual(`
98
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
99
+ <defs>
100
+ <linearGradient id="grad1">
101
+ <stop offset="0%" stop-color="red"></stop>
102
+ <stop offset="100%" stop-color="blue"></stop>
103
+ </linearGradient>
104
+ </defs>
105
+ <g transform="translate(50,50)">
106
+ </g></svg>`);
107
+ });
108
+ });
@@ -0,0 +1,33 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ /**
4
+ * Sanitizes SVG content to prevent XSS attacks while preserving safe SVG elements and attributes
5
+ * @param content - The SVG content to sanitize
6
+ * @returns Sanitized SVG content safe for rendering
7
+ */
8
+ export const sanitizeSVGContent = (content: string): string => {
9
+ return DOMPurify.sanitize(content, {
10
+ FORBID_ATTR: [
11
+ 'onblur',
12
+ 'onchange',
13
+ 'onclick',
14
+ 'onerror',
15
+ 'onfocus',
16
+ 'onkeydown',
17
+ 'onkeypress',
18
+ 'onkeyup',
19
+ 'onload',
20
+ 'onmousedown',
21
+ 'onmouseout',
22
+ 'onmouseover',
23
+ 'onmouseup',
24
+ 'onreset',
25
+ 'onselect',
26
+ 'onsubmit',
27
+ 'onunload',
28
+ ],
29
+ FORBID_TAGS: ['embed', 'link', 'object', 'script', 'style'],
30
+ KEEP_CONTENT: false,
31
+ USE_PROFILES: { svg: true, svgFilters: true },
32
+ });
33
+ };
@@ -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
  "@mozilla/readability": "^0.6.0",
@@ -7,6 +7,7 @@ import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import PluginDetailModal from '@/features/PluginDetailModal';
9
9
  import { useAgentStore } from '@/store/agent';
10
+ import { agentSelectors } from '@/store/agent/selectors';
10
11
  import { useServerConfigStore } from '@/store/serverConfig';
11
12
  import { pluginHelpers, useToolStore } from '@/store/tool';
12
13
  import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
@@ -36,7 +37,10 @@ const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
36
37
  const { t } = useTranslation('plugin');
37
38
  const [open, setOpen] = useState(false);
38
39
  const plugin = useToolStore(pluginSelectors.getToolManifestById(identifier));
39
- const togglePlugin = useAgentStore((s) => s.togglePlugin);
40
+ const [togglePlugin, isPluginEnabledInAgent] = useAgentStore((s) => [
41
+ s.togglePlugin,
42
+ agentSelectors.currentAgentPlugins(s).includes(identifier),
43
+ ]);
40
44
  const { modal } = App.useApp();
41
45
  const [tab, setTab] = useState('info');
42
46
  const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(plugin?.settings);
@@ -90,7 +94,13 @@ const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
90
94
  modal.confirm({
91
95
  centered: true,
92
96
  okButtonProps: { danger: true },
93
- onOk: async () => unInstallPlugin(identifier),
97
+ onOk: async () => {
98
+ // If plugin is enabled in current agent, disable it first
99
+ if (isPluginEnabledInAgent) {
100
+ await togglePlugin(identifier, false);
101
+ }
102
+ await unInstallPlugin(identifier);
103
+ },
94
104
  title: t('store.actions.confirmUninstall'),
95
105
  type: 'error',
96
106
  });
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import { useAgentStore } from '@/store/agent';
9
+ import { agentSelectors } from '@/store/agent/selectors';
9
10
  import { useToolStore } from '@/store/tool';
10
11
  import { mcpStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
11
12
 
@@ -24,7 +25,10 @@ const Actions = memo<ActionsProps>(({ identifier }) => {
24
25
  ]);
25
26
 
26
27
  const { t } = useTranslation('plugin');
27
- const togglePlugin = useAgentStore((s) => s.togglePlugin);
28
+ const [togglePlugin, isPluginEnabledInAgent] = useAgentStore((s) => [
29
+ s.togglePlugin,
30
+ agentSelectors.currentAgentPlugins(s).includes(identifier),
31
+ ]);
28
32
  const { modal } = App.useApp();
29
33
 
30
34
  return (
@@ -42,7 +46,13 @@ const Actions = memo<ActionsProps>(({ identifier }) => {
42
46
  modal.confirm({
43
47
  centered: true,
44
48
  okButtonProps: { danger: true },
45
- onOk: async () => unInstallPlugin(identifier),
49
+ onOk: async () => {
50
+ // If plugin is enabled in current agent, disable it first
51
+ if (isPluginEnabledInAgent) {
52
+ await togglePlugin(identifier, false);
53
+ }
54
+ await unInstallPlugin(identifier);
55
+ },
46
56
  title: t('store.actions.confirmUninstall'),
47
57
  type: 'error',
48
58
  });
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import { useAgentStore } from '@/store/agent';
9
+ import { agentSelectors } from '@/store/agent/selectors';
9
10
  import { useToolStore } from '@/store/tool';
10
11
  import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
11
12
 
@@ -22,7 +23,10 @@ const Actions = memo<ActionsProps>(({ identifier }) => {
22
23
  ]);
23
24
 
24
25
  const { t } = useTranslation('plugin');
25
- const togglePlugin = useAgentStore((s) => s.togglePlugin);
26
+ const [togglePlugin, isPluginEnabledInAgent] = useAgentStore((s) => [
27
+ s.togglePlugin,
28
+ agentSelectors.currentAgentPlugins(s).includes(identifier),
29
+ ]);
26
30
  const { modal } = App.useApp();
27
31
 
28
32
  return (
@@ -40,7 +44,13 @@ const Actions = memo<ActionsProps>(({ identifier }) => {
40
44
  modal.confirm({
41
45
  centered: true,
42
46
  okButtonProps: { danger: true },
43
- onOk: async () => unInstallPlugin(identifier),
47
+ onOk: async () => {
48
+ // If plugin is enabled in current agent, disable it first
49
+ if (isPluginEnabledInAgent) {
50
+ await togglePlugin(identifier, false);
51
+ }
52
+ await unInstallPlugin(identifier);
53
+ },
44
54
  title: t('store.actions.confirmUninstall'),
45
55
  type: 'error',
46
56
  });
@@ -1,15 +1,16 @@
1
+ import { copyImageToClipboard, sanitizeSVGContent } from '@lobechat/utils/client';
1
2
  import { Button, Dropdown, Tooltip } from '@lobehub/ui';
2
3
  import { App, Space } from 'antd';
3
4
  import { css, cx } from 'antd-style';
4
5
  import { CopyIcon, DownloadIcon } from 'lucide-react';
5
6
  import { domToPng } from 'modern-screenshot';
7
+ import { useMemo } from 'react';
6
8
  import { useTranslation } from 'react-i18next';
7
9
  import { Center, Flexbox } from 'react-layout-kit';
8
10
 
9
11
  import { BRANDING_NAME } from '@/const/branding';
10
12
  import { useChatStore } from '@/store/chat';
11
13
  import { chatPortalSelectors } from '@/store/chat/selectors';
12
- import { copyImageToClipboard } from '@/utils/clipboard';
13
14
 
14
15
  const svgContainer = css`
15
16
  width: 100%;
@@ -36,6 +37,9 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
36
37
  const { t } = useTranslation('portal');
37
38
  const { message } = App.useApp();
38
39
 
40
+ // Sanitize SVG content to prevent XSS attacks
41
+ const sanitizedContent = useMemo(() => sanitizeSVGContent(content), [content]);
42
+
39
43
  const generatePng = async () => {
40
44
  return domToPng(document.querySelector(`#${DOM_ID}`) as HTMLDivElement, {
41
45
  features: {
@@ -50,7 +54,7 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
50
54
  let dataUrl = '';
51
55
  if (type === 'png') dataUrl = await generatePng();
52
56
  else if (type === 'svg') {
53
- const blob = new Blob([content], { type: 'image/svg+xml' });
57
+ const blob = new Blob([sanitizedContent], { type: 'image/svg+xml' });
54
58
 
55
59
  dataUrl = URL.createObjectURL(blob);
56
60
  }
@@ -73,7 +77,7 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
73
77
  >
74
78
  <Center
75
79
  className={cx(svgContainer)}
76
- dangerouslySetInnerHTML={{ __html: content }}
80
+ dangerouslySetInnerHTML={{ __html: sanitizedContent }}
77
81
  id={DOM_ID}
78
82
  />
79
83
  <Flexbox className={cx(actions)}>
@@ -2,16 +2,7 @@ import { EdgeConfigClient, createClient } from '@vercel/edge-config';
2
2
 
3
3
  import { appEnv } from '@/envs/app';
4
4
 
5
- const EdgeConfigKeys = {
6
- /**
7
- * Assistant whitelist
8
- */
9
- AssistantBlacklist: 'assistant_blacklist',
10
- /**
11
- * Assistant whitelist
12
- */
13
- AssistantWhitelist: 'assistant_whitelist',
14
- };
5
+ import { EdgeConfigData } from './types';
15
6
 
16
7
  export class EdgeConfig {
17
8
  get client(): EdgeConfigClient {
@@ -30,14 +21,7 @@ export class EdgeConfig {
30
21
 
31
22
  getAgentRestrictions = async () => {
32
23
  const { assistant_blacklist: blacklist, assistant_whitelist: whitelist } =
33
- await this.client.getAll([
34
- EdgeConfigKeys.AssistantWhitelist,
35
- EdgeConfigKeys.AssistantBlacklist,
36
- ]);
37
-
38
- return { blacklist, whitelist } as {
39
- blacklist: string[] | undefined;
40
- whitelist: string[] | undefined;
41
- };
24
+ await this.client.getAll<EdgeConfigData>(['assistant_whitelist', 'assistant_blacklist']);
25
+ return { blacklist, whitelist };
42
26
  };
43
27
  }
@@ -0,0 +1,9 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
+
3
+ /**
4
+ * EdgeConfig 完整配置类型
5
+ */
6
+ export interface EdgeConfigData {
7
+ assistant_blacklist?: string[];
8
+ assistant_whitelist?: string[];
9
+ }