@lobehub/chat 1.39.0 → 1.39.2
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/CHANGELOG.md +50 -0
- package/Dockerfile +1 -1
- package/Dockerfile.database +1 -1
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/src/app/(main)/settings/llm/ProviderList/providers.tsx +2 -4
- package/src/config/llm.ts +3 -6
- package/src/const/auth.ts +0 -3
- package/src/database/server/models/message.ts +6 -2
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -3
- package/src/libs/agent-runtime/AgentRuntime.ts +1 -1
- package/src/libs/agent-runtime/index.ts +0 -1
- package/src/libs/agent-runtime/sensenova/index.test.ts +78 -148
- package/src/libs/agent-runtime/sensenova/index.ts +23 -98
- package/src/locales/default/modelProvider.ts +0 -17
- package/src/server/modules/AgentRuntime/index.ts +0 -15
- package/src/services/_auth.ts +0 -14
- package/src/services/message/client.ts +22 -4
- package/src/store/user/slices/modelList/selectors/keyVaults.ts +0 -2
- package/src/store/user/slices/modelList/selectors/modelConfig.ts +0 -2
- package/src/types/user/settings/keyVaults.ts +1 -6
- package/src/app/(main)/settings/llm/ProviderList/SenseNova/index.tsx +0 -44
- package/src/features/Conversation/Error/APIKeyForm/SenseNova.tsx +0 -49
- package/src/libs/agent-runtime/sensenova/authToken.test.ts +0 -18
- package/src/libs/agent-runtime/sensenova/authToken.ts +0 -27
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.39.2](https://github.com/lobehub/lobe-chat/compare/v1.39.1...v1.39.2)
|
6
|
+
|
7
|
+
<sup>Released on **2024-12-25**</sup>
|
8
|
+
|
9
|
+
#### ♻ Code Refactoring
|
10
|
+
|
11
|
+
- **misc**: Refactor sensenova provider with LobeOpenAICompatibleFactory.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Code refactoring
|
19
|
+
|
20
|
+
- **misc**: Refactor sensenova provider with LobeOpenAICompatibleFactory, closes [#5116](https://github.com/lobehub/lobe-chat/issues/5116) ([5656f39](https://github.com/lobehub/lobe-chat/commit/5656f39))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.39.1](https://github.com/lobehub/lobe-chat/compare/v1.39.0...v1.39.1)
|
31
|
+
|
32
|
+
<sup>Released on **2024-12-24**</sup>
|
33
|
+
|
34
|
+
#### 🐛 Bug Fixes
|
35
|
+
|
36
|
+
- **misc**: Fix image input on pglite.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### What's fixed
|
44
|
+
|
45
|
+
- **misc**: Fix image input on pglite, closes [#5167](https://github.com/lobehub/lobe-chat/issues/5167) ([5c5b37d](https://github.com/lobehub/lobe-chat/commit/5c5b37d))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
## [Version 1.39.0](https://github.com/lobehub/lobe-chat/compare/v1.38.0...v1.39.0)
|
6
56
|
|
7
57
|
<sup>Released on **2024-12-23**</sup>
|
package/Dockerfile
CHANGED
@@ -196,7 +196,7 @@ ENV \
|
|
196
196
|
# Qwen
|
197
197
|
QWEN_API_KEY="" QWEN_MODEL_LIST="" QWEN_PROXY_URL="" \
|
198
198
|
# SenseNova
|
199
|
-
|
199
|
+
SENSENOVA_API_KEY="" SENSENOVA_MODEL_LIST="" \
|
200
200
|
# SiliconCloud
|
201
201
|
SILICONCLOUD_API_KEY="" SILICONCLOUD_MODEL_LIST="" SILICONCLOUD_PROXY_URL="" \
|
202
202
|
# Spark
|
package/Dockerfile.database
CHANGED
@@ -231,7 +231,7 @@ ENV \
|
|
231
231
|
# Qwen
|
232
232
|
QWEN_API_KEY="" QWEN_MODEL_LIST="" QWEN_PROXY_URL="" \
|
233
233
|
# SenseNova
|
234
|
-
|
234
|
+
SENSENOVA_API_KEY="" SENSENOVA_MODEL_LIST="" \
|
235
235
|
# SiliconCloud
|
236
236
|
SILICONCLOUD_API_KEY="" SILICONCLOUD_MODEL_LIST="" SILICONCLOUD_PROXY_URL="" \
|
237
237
|
# Spark
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Refactor sensenova provider with LobeOpenAICompatibleFactory."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2024-12-25",
|
9
|
+
"version": "1.39.2"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"fixes": [
|
14
|
+
"Fix image input on pglite."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2024-12-24",
|
18
|
+
"version": "1.39.1"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"features": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.39.
|
3
|
+
"version": "1.39.2",
|
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",
|
@@ -20,6 +20,7 @@ import {
|
|
20
20
|
OpenRouterProviderCard,
|
21
21
|
PerplexityProviderCard,
|
22
22
|
QwenProviderCard,
|
23
|
+
SenseNovaProviderCard,
|
23
24
|
SiliconCloudProviderCard,
|
24
25
|
SparkProviderCard,
|
25
26
|
StepfunProviderCard,
|
@@ -39,7 +40,6 @@ import { useGithubProvider } from './Github';
|
|
39
40
|
import { useHuggingFaceProvider } from './HuggingFace';
|
40
41
|
import { useOllamaProvider } from './Ollama';
|
41
42
|
import { useOpenAIProvider } from './OpenAI';
|
42
|
-
import { useSenseNovaProvider } from './SenseNova';
|
43
43
|
import { useWenxinProvider } from './Wenxin';
|
44
44
|
|
45
45
|
export const useProviderList = (): ProviderItem[] => {
|
@@ -51,7 +51,6 @@ export const useProviderList = (): ProviderItem[] => {
|
|
51
51
|
const GithubProvider = useGithubProvider();
|
52
52
|
const HuggingFaceProvider = useHuggingFaceProvider();
|
53
53
|
const WenxinProvider = useWenxinProvider();
|
54
|
-
const SenseNovaProvider = useSenseNovaProvider();
|
55
54
|
|
56
55
|
return useMemo(
|
57
56
|
() => [
|
@@ -81,7 +80,7 @@ export const useProviderList = (): ProviderItem[] => {
|
|
81
80
|
SparkProviderCard,
|
82
81
|
ZhiPuProviderCard,
|
83
82
|
ZeroOneProviderCard,
|
84
|
-
|
83
|
+
SenseNovaProviderCard,
|
85
84
|
StepfunProviderCard,
|
86
85
|
MoonshotProviderCard,
|
87
86
|
BaichuanProviderCard,
|
@@ -102,7 +101,6 @@ export const useProviderList = (): ProviderItem[] => {
|
|
102
101
|
GithubProvider,
|
103
102
|
WenxinProvider,
|
104
103
|
HuggingFaceProvider,
|
105
|
-
SenseNovaProvider,
|
106
104
|
],
|
107
105
|
);
|
108
106
|
};
|
package/src/config/llm.ts
CHANGED
@@ -113,8 +113,7 @@ export const getLLMConfig = () => {
|
|
113
113
|
HUGGINGFACE_API_KEY: z.string().optional(),
|
114
114
|
|
115
115
|
ENABLED_SENSENOVA: z.boolean(),
|
116
|
-
|
117
|
-
SENSENOVA_ACCESS_KEY_SECRET: z.string().optional(),
|
116
|
+
SENSENOVA_API_KEY: z.string().optional(),
|
118
117
|
|
119
118
|
ENABLED_XAI: z.boolean(),
|
120
119
|
XAI_API_KEY: z.string().optional(),
|
@@ -234,10 +233,8 @@ export const getLLMConfig = () => {
|
|
234
233
|
ENABLED_HUGGINGFACE: !!process.env.HUGGINGFACE_API_KEY,
|
235
234
|
HUGGINGFACE_API_KEY: process.env.HUGGINGFACE_API_KEY,
|
236
235
|
|
237
|
-
ENABLED_SENSENOVA:
|
238
|
-
|
239
|
-
SENSENOVA_ACCESS_KEY_ID: process.env.SENSENOVA_ACCESS_KEY_ID,
|
240
|
-
SENSENOVA_ACCESS_KEY_SECRET: process.env.SENSENOVA_ACCESS_KEY_SECRET,
|
236
|
+
ENABLED_SENSENOVA: !!process.env.SENSENOVA_API_KEY,
|
237
|
+
SENSENOVA_API_KEY: process.env.SENSENOVA_API_KEY,
|
241
238
|
|
242
239
|
ENABLED_XAI: !!process.env.XAI_API_KEY,
|
243
240
|
XAI_API_KEY: process.env.XAI_API_KEY,
|
package/src/const/auth.ts
CHANGED
@@ -48,7 +48,9 @@ export class MessageModel {
|
|
48
48
|
// **************** Query *************** //
|
49
49
|
query = async (
|
50
50
|
{ current = 0, pageSize = 1000, sessionId, topicId }: QueryMessageParams = {},
|
51
|
-
options: {
|
51
|
+
options: {
|
52
|
+
postProcessUrl?: (path: string | null, file: { fileType: string }) => Promise<string>;
|
53
|
+
} = {},
|
52
54
|
): Promise<MessageItem[]> => {
|
53
55
|
const offset = current * pageSize;
|
54
56
|
|
@@ -130,7 +132,9 @@ export class MessageModel {
|
|
130
132
|
const relatedFileList = await Promise.all(
|
131
133
|
rawRelatedFileList.map(async (file) => ({
|
132
134
|
...file,
|
133
|
-
url: options.postProcessUrl
|
135
|
+
url: options.postProcessUrl
|
136
|
+
? await options.postProcessUrl(file.url, file as any)
|
137
|
+
: (file.url as string),
|
134
138
|
})),
|
135
139
|
);
|
136
140
|
|
@@ -10,7 +10,6 @@ import { GlobalLLMProviderKey } from '@/types/user/settings';
|
|
10
10
|
|
11
11
|
import BedrockForm from './Bedrock';
|
12
12
|
import ProviderApiKeyForm from './ProviderApiKeyForm';
|
13
|
-
import SenseNovaForm from './SenseNova';
|
14
13
|
import WenxinForm from './Wenxin';
|
15
14
|
|
16
15
|
interface APIKeyFormProps {
|
@@ -67,8 +66,6 @@ const APIKeyForm = memo<APIKeyFormProps>(({ id, provider }) => {
|
|
67
66
|
<Center gap={16} style={{ maxWidth: 300 }}>
|
68
67
|
{provider === ModelProvider.Bedrock ? (
|
69
68
|
<BedrockForm />
|
70
|
-
) : provider === ModelProvider.SenseNova ? (
|
71
|
-
<SenseNovaForm />
|
72
69
|
) : provider === ModelProvider.Wenxin ? (
|
73
70
|
<WenxinForm />
|
74
71
|
) : (
|
@@ -15,7 +15,6 @@ export { LobeOpenAI } from './openai';
|
|
15
15
|
export { LobeOpenRouterAI } from './openrouter';
|
16
16
|
export { LobePerplexityAI } from './perplexity';
|
17
17
|
export { LobeQwenAI } from './qwen';
|
18
|
-
export { LobeSenseNovaAI } from './sensenova';
|
19
18
|
export { LobeTogetherAI } from './togetherai';
|
20
19
|
export * from './types';
|
21
20
|
export { AgentRuntimeError } from './utils/createError';
|
@@ -1,142 +1,49 @@
|
|
1
1
|
// @vitest-environment node
|
2
|
-
import
|
2
|
+
import OpenAI from 'openai';
|
3
3
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
4
4
|
|
5
|
-
import {
|
6
|
-
import
|
5
|
+
import { LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime';
|
6
|
+
import { ModelProvider } from '@/libs/agent-runtime';
|
7
|
+
import { AgentRuntimeErrorType } from '@/libs/agent-runtime';
|
7
8
|
|
8
|
-
import * as
|
9
|
+
import * as debugStreamModule from '../utils/debugStream';
|
9
10
|
import { LobeSenseNovaAI } from './index';
|
10
11
|
|
11
|
-
const
|
12
|
-
const
|
12
|
+
const provider = ModelProvider.SenseNova;
|
13
|
+
const defaultBaseURL = 'https://api.sensenova.cn/compatible-mode/v1';
|
14
|
+
const bizErrorType = AgentRuntimeErrorType.ProviderBizError;
|
15
|
+
const invalidErrorType = AgentRuntimeErrorType.InvalidProviderAPIKey;
|
13
16
|
|
14
|
-
// Mock
|
15
|
-
vi.
|
17
|
+
// Mock the console.error to avoid polluting test output
|
18
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
16
19
|
|
17
|
-
|
18
|
-
beforeEach(() => {
|
19
|
-
// Mock generateApiToken
|
20
|
-
vi.spyOn(authTokenModule, 'generateApiToken').mockResolvedValue('mocked_token');
|
21
|
-
});
|
20
|
+
let instance: LobeOpenAICompatibleRuntime;
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
});
|
22
|
+
beforeEach(() => {
|
23
|
+
instance = new LobeSenseNovaAI({ apiKey: 'test' });
|
26
24
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
});
|
25
|
+
// 使用 vi.spyOn 来模拟 chat.completions.create 方法
|
26
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
27
|
+
new ReadableStream() as any,
|
28
|
+
);
|
29
|
+
});
|
33
30
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
31
|
+
afterEach(() => {
|
32
|
+
vi.clearAllMocks();
|
33
|
+
});
|
34
|
+
|
35
|
+
describe('LobeSenseNovaAI', () => {
|
36
|
+
describe('init', () => {
|
37
|
+
it('should correctly initialize with an API key', async () => {
|
38
|
+
const instance = new LobeSenseNovaAI({ apiKey: 'test_api_key' });
|
39
|
+
expect(instance).toBeInstanceOf(LobeSenseNovaAI);
|
40
|
+
expect(instance.baseURL).toEqual(defaultBaseURL);
|
41
41
|
});
|
42
42
|
});
|
43
43
|
|
44
44
|
describe('chat', () => {
|
45
|
-
let instance: LobeSenseNovaAI;
|
46
|
-
|
47
|
-
beforeEach(async () => {
|
48
|
-
instance = await LobeSenseNovaAI.fromAPIKey({
|
49
|
-
apiKey: 'test_api_key',
|
50
|
-
});
|
51
|
-
|
52
|
-
// Mock chat.completions.create
|
53
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
54
|
-
new ReadableStream() as any,
|
55
|
-
);
|
56
|
-
});
|
57
|
-
|
58
|
-
it('should return a StreamingTextResponse on successful API call', async () => {
|
59
|
-
const result = await instance.chat({
|
60
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
61
|
-
model: 'SenseChat',
|
62
|
-
temperature: 0,
|
63
|
-
});
|
64
|
-
expect(result).toBeInstanceOf(Response);
|
65
|
-
});
|
66
|
-
|
67
|
-
it('should handle callback and headers correctly', async () => {
|
68
|
-
// 模拟 chat.completions.create 方法返回一个可读流
|
69
|
-
const mockCreateMethod = vi
|
70
|
-
.spyOn(instance['client'].chat.completions, 'create')
|
71
|
-
.mockResolvedValue(
|
72
|
-
new ReadableStream({
|
73
|
-
start(controller) {
|
74
|
-
controller.enqueue({
|
75
|
-
id: 'chatcmpl-8xDx5AETP8mESQN7UB30GxTN2H1SO',
|
76
|
-
object: 'chat.completion.chunk',
|
77
|
-
created: 1709125675,
|
78
|
-
model: 'gpt-3.5-turbo-0125',
|
79
|
-
system_fingerprint: 'fp_86156a94a0',
|
80
|
-
choices: [
|
81
|
-
{ index: 0, delta: { content: 'hello' }, logprobs: null, finish_reason: null },
|
82
|
-
],
|
83
|
-
});
|
84
|
-
controller.close();
|
85
|
-
},
|
86
|
-
}) as any,
|
87
|
-
);
|
88
|
-
|
89
|
-
// 准备 callback 和 headers
|
90
|
-
const mockCallback: ChatStreamCallbacks = {
|
91
|
-
onStart: vi.fn(),
|
92
|
-
onToken: vi.fn(),
|
93
|
-
};
|
94
|
-
const mockHeaders = { 'Custom-Header': 'TestValue' };
|
95
|
-
|
96
|
-
// 执行测试
|
97
|
-
const result = await instance.chat(
|
98
|
-
{
|
99
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
100
|
-
model: 'SenseChat',
|
101
|
-
temperature: 0,
|
102
|
-
},
|
103
|
-
{ callback: mockCallback, headers: mockHeaders },
|
104
|
-
);
|
105
|
-
|
106
|
-
// 验证 callback 被调用
|
107
|
-
await result.text(); // 确保流被消费
|
108
|
-
|
109
|
-
// 验证 headers 被正确传递
|
110
|
-
expect(result.headers.get('Custom-Header')).toEqual('TestValue');
|
111
|
-
|
112
|
-
// 清理
|
113
|
-
mockCreateMethod.mockRestore();
|
114
|
-
});
|
115
|
-
|
116
|
-
it('should transform messages correctly', async () => {
|
117
|
-
const spyOn = vi.spyOn(instance['client'].chat.completions, 'create');
|
118
|
-
|
119
|
-
await instance.chat({
|
120
|
-
frequency_penalty: 0,
|
121
|
-
messages: [
|
122
|
-
{ content: 'Hello', role: 'user' },
|
123
|
-
{ content: [{ type: 'text', text: 'Hello again' }], role: 'user' },
|
124
|
-
],
|
125
|
-
model: 'SenseChat',
|
126
|
-
temperature: 0,
|
127
|
-
top_p: 1,
|
128
|
-
});
|
129
|
-
|
130
|
-
const calledWithParams = spyOn.mock.calls[0][0];
|
131
|
-
|
132
|
-
expect(calledWithParams.frequency_penalty).toBeUndefined(); // frequency_penalty 0 should be undefined
|
133
|
-
expect(calledWithParams.messages[1].content).toEqual([{ type: 'text', text: 'Hello again' }]);
|
134
|
-
expect(calledWithParams.temperature).toBeUndefined(); // temperature 0 should be undefined
|
135
|
-
expect(calledWithParams.top_p).toBeUndefined(); // top_p 1 should be undefined
|
136
|
-
});
|
137
|
-
|
138
45
|
describe('Error', () => {
|
139
|
-
it('should return
|
46
|
+
it('should return QwenBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
140
47
|
// Arrange
|
141
48
|
const apiError = new OpenAI.APIError(
|
142
49
|
400,
|
@@ -156,31 +63,31 @@ describe('LobeSenseNovaAI', () => {
|
|
156
63
|
try {
|
157
64
|
await instance.chat({
|
158
65
|
messages: [{ content: 'Hello', role: 'user' }],
|
159
|
-
model: '
|
160
|
-
temperature: 0,
|
66
|
+
model: 'max-32k',
|
67
|
+
temperature: 0.999,
|
161
68
|
});
|
162
69
|
} catch (e) {
|
163
70
|
expect(e).toEqual({
|
164
|
-
endpoint:
|
71
|
+
endpoint: defaultBaseURL,
|
165
72
|
error: {
|
166
73
|
error: { message: 'Bad Request' },
|
167
74
|
status: 400,
|
168
75
|
},
|
169
76
|
errorType: bizErrorType,
|
170
|
-
provider
|
77
|
+
provider,
|
171
78
|
});
|
172
79
|
}
|
173
80
|
});
|
174
81
|
|
175
|
-
it('should throw AgentRuntimeError with
|
82
|
+
it('should throw AgentRuntimeError with InvalidQwenAPIKey if no apiKey is provided', async () => {
|
176
83
|
try {
|
177
|
-
|
84
|
+
new LobeSenseNovaAI({});
|
178
85
|
} catch (e) {
|
179
86
|
expect(e).toEqual({ errorType: invalidErrorType });
|
180
87
|
}
|
181
88
|
});
|
182
89
|
|
183
|
-
it('should return
|
90
|
+
it('should return QwenBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
|
184
91
|
// Arrange
|
185
92
|
const errorInfo = {
|
186
93
|
stack: 'abc',
|
@@ -196,23 +103,23 @@ describe('LobeSenseNovaAI', () => {
|
|
196
103
|
try {
|
197
104
|
await instance.chat({
|
198
105
|
messages: [{ content: 'Hello', role: 'user' }],
|
199
|
-
model: '
|
200
|
-
temperature: 0.
|
106
|
+
model: 'max-32k',
|
107
|
+
temperature: 0.999,
|
201
108
|
});
|
202
109
|
} catch (e) {
|
203
110
|
expect(e).toEqual({
|
204
|
-
endpoint:
|
111
|
+
endpoint: defaultBaseURL,
|
205
112
|
error: {
|
206
113
|
cause: { message: 'api is undefined' },
|
207
114
|
stack: 'abc',
|
208
115
|
},
|
209
116
|
errorType: bizErrorType,
|
210
|
-
provider
|
117
|
+
provider,
|
211
118
|
});
|
212
119
|
}
|
213
120
|
});
|
214
121
|
|
215
|
-
it('should return
|
122
|
+
it('should return QwenBizError with an cause response with desensitize Url', async () => {
|
216
123
|
// Arrange
|
217
124
|
const errorInfo = {
|
218
125
|
stack: 'abc',
|
@@ -220,10 +127,10 @@ describe('LobeSenseNovaAI', () => {
|
|
220
127
|
};
|
221
128
|
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
222
129
|
|
223
|
-
instance =
|
130
|
+
instance = new LobeSenseNovaAI({
|
224
131
|
apiKey: 'test',
|
225
132
|
|
226
|
-
baseURL: 'https://abc.com/
|
133
|
+
baseURL: 'https://api.abc.com/v1',
|
227
134
|
});
|
228
135
|
|
229
136
|
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
@@ -232,18 +139,40 @@ describe('LobeSenseNovaAI', () => {
|
|
232
139
|
try {
|
233
140
|
await instance.chat({
|
234
141
|
messages: [{ content: 'Hello', role: 'user' }],
|
235
|
-
model: '
|
236
|
-
temperature: 0,
|
142
|
+
model: 'max-32k',
|
143
|
+
temperature: 0.999,
|
237
144
|
});
|
238
145
|
} catch (e) {
|
239
146
|
expect(e).toEqual({
|
240
|
-
endpoint: 'https
|
147
|
+
endpoint: 'https://api.***.com/v1',
|
241
148
|
error: {
|
242
149
|
cause: { message: 'api is undefined' },
|
243
150
|
stack: 'abc',
|
244
151
|
},
|
245
152
|
errorType: bizErrorType,
|
246
|
-
provider
|
153
|
+
provider,
|
154
|
+
});
|
155
|
+
}
|
156
|
+
});
|
157
|
+
|
158
|
+
it('should throw an InvalidQwenAPIKey error type on 401 status code', async () => {
|
159
|
+
// Mock the API call to simulate a 401 error
|
160
|
+
const error = new Error('InvalidApiKey') as any;
|
161
|
+
error.status = 401;
|
162
|
+
vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error);
|
163
|
+
|
164
|
+
try {
|
165
|
+
await instance.chat({
|
166
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
167
|
+
model: 'max-32k',
|
168
|
+
temperature: 0.999,
|
169
|
+
});
|
170
|
+
} catch (e) {
|
171
|
+
expect(e).toEqual({
|
172
|
+
endpoint: defaultBaseURL,
|
173
|
+
error: new Error('InvalidApiKey'),
|
174
|
+
errorType: invalidErrorType,
|
175
|
+
provider,
|
247
176
|
});
|
248
177
|
}
|
249
178
|
});
|
@@ -258,14 +187,14 @@ describe('LobeSenseNovaAI', () => {
|
|
258
187
|
try {
|
259
188
|
await instance.chat({
|
260
189
|
messages: [{ content: 'Hello', role: 'user' }],
|
261
|
-
model: '
|
262
|
-
temperature: 0,
|
190
|
+
model: 'max-32k',
|
191
|
+
temperature: 0.999,
|
263
192
|
});
|
264
193
|
} catch (e) {
|
265
194
|
expect(e).toEqual({
|
266
|
-
endpoint:
|
195
|
+
endpoint: defaultBaseURL,
|
267
196
|
errorType: 'AgentRuntimeError',
|
268
|
-
provider
|
197
|
+
provider,
|
269
198
|
error: {
|
270
199
|
name: genericError.name,
|
271
200
|
cause: genericError.cause,
|
@@ -278,7 +207,7 @@ describe('LobeSenseNovaAI', () => {
|
|
278
207
|
});
|
279
208
|
|
280
209
|
describe('DEBUG', () => {
|
281
|
-
it('should call debugStream and return StreamingTextResponse when
|
210
|
+
it('should call debugStream and return StreamingTextResponse when DEBUG_SENSENOVA_CHAT_COMPLETION is 1', async () => {
|
282
211
|
// Arrange
|
283
212
|
const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
|
284
213
|
const mockDebugStream = new ReadableStream({
|
@@ -306,8 +235,9 @@ describe('LobeSenseNovaAI', () => {
|
|
306
235
|
// 假设的测试函数调用,你可能需要根据实际情况调整
|
307
236
|
await instance.chat({
|
308
237
|
messages: [{ content: 'Hello', role: 'user' }],
|
309
|
-
model: '
|
310
|
-
|
238
|
+
model: 'max-32k',
|
239
|
+
stream: true,
|
240
|
+
temperature: 0.999,
|
311
241
|
});
|
312
242
|
|
313
243
|
// 验证 debugStream 被调用
|
@@ -1,98 +1,23 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
this.baseURL = this.client.baseURL;
|
25
|
-
}
|
26
|
-
|
27
|
-
static async fromAPIKey({ apiKey, baseURL = DEFAULT_BASE_URL, ...res }: ClientOptions = {}) {
|
28
|
-
const invalidSenseNovaAPIKey = AgentRuntimeError.createError(
|
29
|
-
AgentRuntimeErrorType.InvalidProviderAPIKey,
|
30
|
-
);
|
31
|
-
|
32
|
-
if (!apiKey) throw invalidSenseNovaAPIKey;
|
33
|
-
|
34
|
-
let token: string;
|
35
|
-
|
36
|
-
try {
|
37
|
-
token = await generateApiToken(apiKey);
|
38
|
-
} catch {
|
39
|
-
throw invalidSenseNovaAPIKey;
|
40
|
-
}
|
41
|
-
|
42
|
-
const header = { Authorization: `Bearer ${token}` };
|
43
|
-
|
44
|
-
const llm = new OpenAI({ apiKey, baseURL, defaultHeaders: header, ...res });
|
45
|
-
|
46
|
-
return new LobeSenseNovaAI(llm);
|
47
|
-
}
|
48
|
-
|
49
|
-
async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
|
50
|
-
try {
|
51
|
-
const params = await this.buildCompletionsParams(payload);
|
52
|
-
|
53
|
-
const response = await this.client.chat.completions.create(
|
54
|
-
params as unknown as OpenAI.ChatCompletionCreateParamsStreaming,
|
55
|
-
);
|
56
|
-
|
57
|
-
const [prod, debug] = response.tee();
|
58
|
-
|
59
|
-
if (process.env.DEBUG_SENSENOVA_CHAT_COMPLETION === '1') {
|
60
|
-
debugStream(debug.toReadableStream()).catch(console.error);
|
61
|
-
}
|
62
|
-
|
63
|
-
return StreamingResponse(OpenAIStream(prod), {
|
64
|
-
headers: options?.headers,
|
65
|
-
});
|
66
|
-
} catch (error) {
|
67
|
-
const { errorResult, RuntimeError } = handleOpenAIError(error);
|
68
|
-
|
69
|
-
const errorType = RuntimeError || AgentRuntimeErrorType.ProviderBizError;
|
70
|
-
let desensitizedEndpoint = this.baseURL;
|
71
|
-
|
72
|
-
if (this.baseURL !== DEFAULT_BASE_URL) {
|
73
|
-
desensitizedEndpoint = desensitizeUrl(this.baseURL);
|
74
|
-
}
|
75
|
-
throw AgentRuntimeError.chat({
|
76
|
-
endpoint: desensitizedEndpoint,
|
77
|
-
error: errorResult,
|
78
|
-
errorType,
|
79
|
-
provider: ModelProvider.SenseNova,
|
80
|
-
});
|
81
|
-
}
|
82
|
-
}
|
83
|
-
|
84
|
-
private async buildCompletionsParams(payload: ChatStreamPayload) {
|
85
|
-
const { frequency_penalty, messages, temperature, top_p, ...params } = payload;
|
86
|
-
|
87
|
-
return {
|
88
|
-
messages: await convertOpenAIMessages(messages as any),
|
89
|
-
...params,
|
90
|
-
frequency_penalty: (frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2) ? frequency_penalty : undefined,
|
91
|
-
stream: true,
|
92
|
-
temperature: (temperature !== undefined && temperature > 0 && temperature <= 2) ? temperature : undefined,
|
93
|
-
top_p: (top_p !== undefined && top_p > 0 && top_p < 1) ? top_p : undefined,
|
94
|
-
};
|
95
|
-
}
|
96
|
-
}
|
97
|
-
|
98
|
-
export default LobeSenseNovaAI;
|
1
|
+
import { ModelProvider } from '../types';
|
2
|
+
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
|
3
|
+
|
4
|
+
export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
|
5
|
+
baseURL: 'https://api.sensenova.cn/compatible-mode/v1',
|
6
|
+
chatCompletion: {
|
7
|
+
handlePayload: (payload) => {
|
8
|
+
const { frequency_penalty, temperature, top_p, ...rest } = payload;
|
9
|
+
|
10
|
+
return {
|
11
|
+
...rest,
|
12
|
+
frequency_penalty: (frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2) ? frequency_penalty : undefined,
|
13
|
+
stream: true,
|
14
|
+
temperature: (temperature !== undefined && temperature > 0 && temperature <= 2) ? temperature : undefined,
|
15
|
+
top_p: (top_p !== undefined && top_p > 0 && top_p < 1) ? top_p : undefined,
|
16
|
+
} as any;
|
17
|
+
},
|
18
|
+
},
|
19
|
+
debug: {
|
20
|
+
chatCompletion: () => process.env.DEBUG_SENSENOVA_CHAT_COMPLETION === '1',
|
21
|
+
},
|
22
|
+
provider: ModelProvider.SenseNova,
|
23
|
+
});
|
@@ -134,23 +134,6 @@ export default {
|
|
134
134
|
title: '下载指定的 Ollama 模型',
|
135
135
|
},
|
136
136
|
},
|
137
|
-
sensenova: {
|
138
|
-
sensenovaAccessKeyID: {
|
139
|
-
desc: '填入 SenseNova Access Key ID',
|
140
|
-
placeholder: 'SenseNova Access Key ID',
|
141
|
-
title: 'Access Key ID',
|
142
|
-
},
|
143
|
-
sensenovaAccessKeySecret: {
|
144
|
-
desc: '填入 SenseNova Access Key Secret',
|
145
|
-
placeholder: 'SenseNova Access Key Secret',
|
146
|
-
title: 'Access Key Secret',
|
147
|
-
},
|
148
|
-
unlock: {
|
149
|
-
description:
|
150
|
-
'输入你的 Access Key ID / Access Key Secret 即可开始会话。应用不会记录你的鉴权配置',
|
151
|
-
title: '使用自定义 SenseNova 鉴权信息',
|
152
|
-
},
|
153
|
-
},
|
154
137
|
wenxin: {
|
155
138
|
accessKey: {
|
156
139
|
desc: '填入百度千帆平台的 Access Key',
|
@@ -100,21 +100,6 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
|
|
100
100
|
|
101
101
|
return { apiKey };
|
102
102
|
}
|
103
|
-
|
104
|
-
case ModelProvider.SenseNova: {
|
105
|
-
const { SENSENOVA_ACCESS_KEY_ID, SENSENOVA_ACCESS_KEY_SECRET } = llmConfig;
|
106
|
-
|
107
|
-
const sensenovaAccessKeyID = apiKeyManager.pick(
|
108
|
-
payload?.sensenovaAccessKeyID || SENSENOVA_ACCESS_KEY_ID,
|
109
|
-
);
|
110
|
-
const sensenovaAccessKeySecret = apiKeyManager.pick(
|
111
|
-
payload?.sensenovaAccessKeySecret || SENSENOVA_ACCESS_KEY_SECRET,
|
112
|
-
);
|
113
|
-
|
114
|
-
const apiKey = sensenovaAccessKeyID + ':' + sensenovaAccessKeySecret;
|
115
|
-
|
116
|
-
return { apiKey };
|
117
|
-
}
|
118
103
|
}
|
119
104
|
};
|
120
105
|
|
package/src/services/_auth.ts
CHANGED
@@ -25,20 +25,6 @@ export const getProviderAuthPayload = (provider: string) => {
|
|
25
25
|
};
|
26
26
|
}
|
27
27
|
|
28
|
-
case ModelProvider.SenseNova: {
|
29
|
-
const { sensenovaAccessKeyID, sensenovaAccessKeySecret } = keyVaultsConfigSelectors.sensenovaConfig(
|
30
|
-
useUserStore.getState(),
|
31
|
-
);
|
32
|
-
|
33
|
-
const apiKey = (sensenovaAccessKeyID || '') + ':' + (sensenovaAccessKeySecret || '')
|
34
|
-
|
35
|
-
return {
|
36
|
-
apiKey,
|
37
|
-
sensenovaAccessKeyID: sensenovaAccessKeyID,
|
38
|
-
sensenovaAccessKeySecret: sensenovaAccessKeySecret,
|
39
|
-
};
|
40
|
-
}
|
41
|
-
|
42
28
|
case ModelProvider.Wenxin: {
|
43
29
|
const { secretKey, accessKey } = keyVaultsConfigSelectors.wenxinConfig(
|
44
30
|
useUserStore.getState(),
|
@@ -5,6 +5,7 @@ import { clientDB } from '@/database/client/db';
|
|
5
5
|
import { MessageItem } from '@/database/schemas';
|
6
6
|
import { MessageModel } from '@/database/server/models/message';
|
7
7
|
import { BaseClientService } from '@/services/baseClientService';
|
8
|
+
import { clientS3Storage } from '@/services/file/ClientS3';
|
8
9
|
import {
|
9
10
|
ChatMessage,
|
10
11
|
ChatMessageError,
|
@@ -34,10 +35,20 @@ export class ClientService extends BaseClientService implements IMessageService
|
|
34
35
|
}
|
35
36
|
|
36
37
|
async getMessages(sessionId: string, topicId?: string) {
|
37
|
-
const data = await this.messageModel.query(
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
const data = await this.messageModel.query(
|
39
|
+
{
|
40
|
+
sessionId: this.toDbSessionId(sessionId),
|
41
|
+
topicId,
|
42
|
+
},
|
43
|
+
{
|
44
|
+
postProcessUrl: async (url, file) => {
|
45
|
+
const hash = (url as string).replace('client-s3://', '');
|
46
|
+
const base64 = await this.getBase64ByFileHash(hash);
|
47
|
+
|
48
|
+
return `data:${file.fileType};base64,${base64}`;
|
49
|
+
},
|
50
|
+
},
|
51
|
+
);
|
41
52
|
|
42
53
|
return data as unknown as ChatMessage[];
|
43
54
|
}
|
@@ -115,4 +126,11 @@ export class ClientService extends BaseClientService implements IMessageService
|
|
115
126
|
private toDbSessionId(sessionId: string | undefined) {
|
116
127
|
return sessionId === INBOX_SESSION_ID ? undefined : sessionId;
|
117
128
|
}
|
129
|
+
|
130
|
+
private async getBase64ByFileHash(hash: string) {
|
131
|
+
const fileItem = await clientS3Storage.getObject(hash);
|
132
|
+
if (!fileItem) throw new Error('file not found');
|
133
|
+
|
134
|
+
return Buffer.from(await fileItem.arrayBuffer()).toString('base64');
|
135
|
+
}
|
118
136
|
}
|
@@ -16,7 +16,6 @@ const openAIConfig = (s: UserStore) => keyVaultsSettings(s).openai || {};
|
|
16
16
|
const bedrockConfig = (s: UserStore) => keyVaultsSettings(s).bedrock || {};
|
17
17
|
const wenxinConfig = (s: UserStore) => keyVaultsSettings(s).wenxin || {};
|
18
18
|
const ollamaConfig = (s: UserStore) => keyVaultsSettings(s).ollama || {};
|
19
|
-
const sensenovaConfig = (s: UserStore) => keyVaultsSettings(s).sensenova || {};
|
20
19
|
const azureConfig = (s: UserStore) => keyVaultsSettings(s).azure || {};
|
21
20
|
const cloudflareConfig = (s: UserStore) => keyVaultsSettings(s).cloudflare || {};
|
22
21
|
const getVaultByProvider = (provider: GlobalLLMProviderKey) => (s: UserStore) =>
|
@@ -46,6 +45,5 @@ export const keyVaultsConfigSelectors = {
|
|
46
45
|
ollamaConfig,
|
47
46
|
openAIConfig,
|
48
47
|
password,
|
49
|
-
sensenovaConfig,
|
50
48
|
wenxinConfig,
|
51
49
|
};
|
@@ -70,7 +70,6 @@ const bedrockConfig = (s: UserStore) => currentLLMSettings(s).bedrock;
|
|
70
70
|
const ollamaConfig = (s: UserStore) => currentLLMSettings(s).ollama;
|
71
71
|
const azureConfig = (s: UserStore) => currentLLMSettings(s).azure;
|
72
72
|
const cloudflareConfig = (s: UserStore) => currentLLMSettings(s).cloudflare;
|
73
|
-
const sensenovaConfig = (s: UserStore) => currentLLMSettings(s).sensenova;
|
74
73
|
|
75
74
|
const isAzureEnabled = (s: UserStore) => currentLLMSettings(s).azure.enabled;
|
76
75
|
|
@@ -89,5 +88,4 @@ export const modelConfigSelectors = {
|
|
89
88
|
|
90
89
|
ollamaConfig,
|
91
90
|
openAIConfig,
|
92
|
-
sensenovaConfig,
|
93
91
|
};
|
@@ -21,11 +21,6 @@ export interface CloudflareKeyVault {
|
|
21
21
|
baseURLOrAccountID?: string;
|
22
22
|
}
|
23
23
|
|
24
|
-
export interface SenseNovaKeyVault {
|
25
|
-
sensenovaAccessKeyID?: string;
|
26
|
-
sensenovaAccessKeySecret?: string;
|
27
|
-
}
|
28
|
-
|
29
24
|
export interface WenxinKeyVault {
|
30
25
|
accessKey?: string;
|
31
26
|
secretKey?: string;
|
@@ -60,7 +55,7 @@ export interface UserKeyVaults {
|
|
60
55
|
password?: string;
|
61
56
|
perplexity?: OpenAICompatibleKeyVault;
|
62
57
|
qwen?: OpenAICompatibleKeyVault;
|
63
|
-
sensenova?:
|
58
|
+
sensenova?: OpenAICompatibleKeyVault;
|
64
59
|
siliconcloud?: OpenAICompatibleKeyVault;
|
65
60
|
spark?: OpenAICompatibleKeyVault;
|
66
61
|
stepfun?: OpenAICompatibleKeyVault;
|
@@ -1,44 +0,0 @@
|
|
1
|
-
'use client';
|
2
|
-
|
3
|
-
import { Input } from 'antd';
|
4
|
-
import { useTranslation } from 'react-i18next';
|
5
|
-
|
6
|
-
import { SenseNovaProviderCard } from '@/config/modelProviders';
|
7
|
-
import { GlobalLLMProviderKey } from '@/types/user/settings';
|
8
|
-
|
9
|
-
import { KeyVaultsConfigKey } from '../../const';
|
10
|
-
import { ProviderItem } from '../../type';
|
11
|
-
|
12
|
-
const providerKey: GlobalLLMProviderKey = 'sensenova';
|
13
|
-
|
14
|
-
export const useSenseNovaProvider = (): ProviderItem => {
|
15
|
-
const { t } = useTranslation('modelProvider');
|
16
|
-
|
17
|
-
return {
|
18
|
-
...SenseNovaProviderCard,
|
19
|
-
apiKeyItems: [
|
20
|
-
{
|
21
|
-
children: (
|
22
|
-
<Input.Password
|
23
|
-
autoComplete={'new-password'}
|
24
|
-
placeholder={t(`${providerKey}.sensenovaAccessKeyID.placeholder`)}
|
25
|
-
/>
|
26
|
-
),
|
27
|
-
desc: t(`${providerKey}.sensenovaAccessKeyID.desc`),
|
28
|
-
label: t(`${providerKey}.sensenovaAccessKeyID.title`),
|
29
|
-
name: [KeyVaultsConfigKey, providerKey, 'sensenovaAccessKeyID'],
|
30
|
-
},
|
31
|
-
{
|
32
|
-
children: (
|
33
|
-
<Input.Password
|
34
|
-
autoComplete={'new-password'}
|
35
|
-
placeholder={t(`${providerKey}.sensenovaAccessKeySecret.placeholder`)}
|
36
|
-
/>
|
37
|
-
),
|
38
|
-
desc: t(`${providerKey}.sensenovaAccessKeySecret.desc`),
|
39
|
-
label: t(`${providerKey}.sensenovaAccessKeySecret.title`),
|
40
|
-
name: [KeyVaultsConfigKey, providerKey, 'sensenovaAccessKeySecret'],
|
41
|
-
},
|
42
|
-
],
|
43
|
-
};
|
44
|
-
};
|
@@ -1,49 +0,0 @@
|
|
1
|
-
import { SenseNova } from '@lobehub/icons';
|
2
|
-
import { Input } from 'antd';
|
3
|
-
import { memo } from 'react';
|
4
|
-
import { useTranslation } from 'react-i18next';
|
5
|
-
|
6
|
-
import { ModelProvider } from '@/libs/agent-runtime';
|
7
|
-
import { useUserStore } from '@/store/user';
|
8
|
-
import { keyVaultsConfigSelectors } from '@/store/user/selectors';
|
9
|
-
|
10
|
-
import { FormAction } from '../style';
|
11
|
-
|
12
|
-
const SenseNovaForm = memo(() => {
|
13
|
-
const { t } = useTranslation('modelProvider');
|
14
|
-
|
15
|
-
const [sensenovaAccessKeyID, sensenovaAccessKeySecret, setConfig] = useUserStore((s) => [
|
16
|
-
keyVaultsConfigSelectors.sensenovaConfig(s).sensenovaAccessKeyID,
|
17
|
-
keyVaultsConfigSelectors.sensenovaConfig(s).sensenovaAccessKeySecret,
|
18
|
-
s.updateKeyVaultConfig,
|
19
|
-
]);
|
20
|
-
|
21
|
-
return (
|
22
|
-
<FormAction
|
23
|
-
avatar={<SenseNova color={SenseNova.colorPrimary} size={56} />}
|
24
|
-
description={t('sensenova.unlock.description')}
|
25
|
-
title={t('sensenova.unlock.title')}
|
26
|
-
>
|
27
|
-
<Input.Password
|
28
|
-
autoComplete={'new-password'}
|
29
|
-
onChange={(e) => {
|
30
|
-
setConfig(ModelProvider.SenseNova, { sensenovaAccessKeyID: e.target.value });
|
31
|
-
}}
|
32
|
-
placeholder={t('sensenova.sensenovaAccessKeyID.placeholder')}
|
33
|
-
type={'block'}
|
34
|
-
value={sensenovaAccessKeyID}
|
35
|
-
/>
|
36
|
-
<Input.Password
|
37
|
-
autoComplete={'new-password'}
|
38
|
-
onChange={(e) => {
|
39
|
-
setConfig(ModelProvider.SenseNova, { sensenovaAccessKeySecret: e.target.value });
|
40
|
-
}}
|
41
|
-
placeholder={t('sensenova.sensenovaAccessKeySecret.placeholder')}
|
42
|
-
type={'block'}
|
43
|
-
value={sensenovaAccessKeySecret}
|
44
|
-
/>
|
45
|
-
</FormAction>
|
46
|
-
);
|
47
|
-
});
|
48
|
-
|
49
|
-
export default SenseNovaForm;
|
@@ -1,18 +0,0 @@
|
|
1
|
-
// @vitest-environment node
|
2
|
-
import { generateApiToken } from './authToken';
|
3
|
-
|
4
|
-
describe('generateApiToken', () => {
|
5
|
-
it('should throw an error if no apiKey is provided', async () => {
|
6
|
-
await expect(generateApiToken()).rejects.toThrow('Invalid apiKey');
|
7
|
-
});
|
8
|
-
|
9
|
-
it('should throw an error if apiKey is invalid', async () => {
|
10
|
-
await expect(generateApiToken('invalid')).rejects.toThrow('Invalid apiKey');
|
11
|
-
});
|
12
|
-
|
13
|
-
it('should return a token if a valid apiKey is provided', async () => {
|
14
|
-
const apiKey = 'id:secret';
|
15
|
-
const token = await generateApiToken(apiKey);
|
16
|
-
expect(token).toBeDefined();
|
17
|
-
});
|
18
|
-
});
|
@@ -1,27 +0,0 @@
|
|
1
|
-
import { SignJWT } from 'jose';
|
2
|
-
|
3
|
-
// https://console.sensecore.cn/help/docs/model-as-a-service/nova/overview/Authorization
|
4
|
-
export const generateApiToken = async (apiKey?: string): Promise<string> => {
|
5
|
-
if (!apiKey) {
|
6
|
-
throw new Error('Invalid apiKey');
|
7
|
-
}
|
8
|
-
|
9
|
-
const [id, secret] = apiKey.split(':');
|
10
|
-
if (!id || !secret) {
|
11
|
-
throw new Error('Invalid apiKey');
|
12
|
-
}
|
13
|
-
|
14
|
-
const currentTime = Math.floor(Date.now() / 1000);
|
15
|
-
|
16
|
-
const payload = {
|
17
|
-
exp: currentTime + 1800,
|
18
|
-
iss: id,
|
19
|
-
nbf: currentTime - 5,
|
20
|
-
};
|
21
|
-
|
22
|
-
const jwt = await new SignJWT(payload)
|
23
|
-
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
|
24
|
-
.sign(new TextEncoder().encode(secret));
|
25
|
-
|
26
|
-
return jwt;
|
27
|
-
};
|