@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.
- 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 +25 -0
- package/changelog/v1.json +9 -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/createRuntime.test.ts +89 -64
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +58 -40
- 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,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
|
+
[](#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
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": {
|
|
@@ -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
|
});
|
|
@@ -25,7 +25,7 @@ import { baseRuntimeMap } from './baseRuntimeMap';
|
|
|
25
25
|
|
|
26
26
|
export interface RuntimeItem {
|
|
27
27
|
id: string;
|
|
28
|
-
models?: 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[]
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
129
|
-
|
|
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
|
-
|
|
159
|
+
return resolvedRouters.map((router) => {
|
|
136
160
|
const providerAI = router.runtime ?? baseRuntimeMap[router.apiType] ?? LobeOpenAI;
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
|
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 (
|
|
177
|
-
const error =
|
|
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 (
|
|
201
|
-
//
|
|
202
|
-
const
|
|
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
|
|
219
|
+
const modelList = await modelsOption({ client: (lastRuntime as any).client });
|
|
205
220
|
return await postProcessModelList(modelList);
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
|
-
|
|
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) {
|
|
@@ -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
|
}
|
|
@@ -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
|
+
};
|
|
@@ -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) =>
|
|
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 () =>
|
|
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) =>
|
|
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 () =>
|
|
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) =>
|
|
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 () =>
|
|
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([
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
File without changes
|