@lobehub/chat 1.68.8 → 1.68.10
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/changelog/v1.json +18 -0
- package/docs/usage/providers/ppio.mdx +5 -5
- package/docs/usage/providers/ppio.zh-CN.mdx +7 -7
- package/locales/ar/chat.json +5 -1
- package/locales/ar/models.json +6 -9
- package/locales/bg-BG/chat.json +5 -1
- package/locales/bg-BG/models.json +6 -9
- package/locales/de-DE/chat.json +5 -1
- package/locales/de-DE/models.json +6 -9
- package/locales/en-US/chat.json +5 -1
- package/locales/en-US/models.json +6 -9
- package/locales/es-ES/chat.json +5 -1
- package/locales/es-ES/models.json +6 -9
- package/locales/fa-IR/chat.json +5 -1
- package/locales/fa-IR/models.json +6 -9
- package/locales/fr-FR/chat.json +5 -1
- package/locales/fr-FR/models.json +6 -9
- package/locales/it-IT/chat.json +5 -1
- package/locales/it-IT/models.json +6 -9
- package/locales/ja-JP/chat.json +5 -1
- package/locales/ja-JP/models.json +6 -9
- package/locales/ko-KR/chat.json +5 -1
- package/locales/ko-KR/models.json +6 -9
- package/locales/nl-NL/chat.json +5 -1
- package/locales/nl-NL/models.json +6 -9
- package/locales/pl-PL/chat.json +5 -1
- package/locales/pl-PL/models.json +6 -9
- package/locales/pt-BR/chat.json +5 -1
- package/locales/pt-BR/models.json +6 -9
- package/locales/ru-RU/chat.json +5 -1
- package/locales/ru-RU/models.json +6 -9
- package/locales/tr-TR/chat.json +5 -1
- package/locales/tr-TR/models.json +6 -9
- package/locales/vi-VN/chat.json +5 -1
- package/locales/vi-VN/models.json +6 -9
- package/locales/zh-CN/chat.json +5 -1
- package/locales/zh-CN/models.json +6 -9
- package/locales/zh-TW/chat.json +5 -1
- package/locales/zh-TW/models.json +6 -9
- package/package.json +3 -1
- package/src/config/aiModels/perplexity.ts +36 -20
- package/src/config/modelProviders/ppio.ts +1 -1
- package/src/database/client/migrations.json +8 -3
- package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +27 -9
- package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +77 -35
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +253 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +65 -46
- package/src/libs/agent-runtime/baichuan/index.test.ts +58 -1
- package/src/libs/agent-runtime/groq/index.test.ts +36 -284
- package/src/libs/agent-runtime/mistral/index.test.ts +39 -300
- package/src/libs/agent-runtime/perplexity/index.test.ts +12 -10
- package/src/libs/agent-runtime/providerTestUtils.ts +58 -0
- package/src/libs/agent-runtime/togetherai/index.test.ts +7 -295
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +3 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +5 -2
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +89 -5
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -8
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +188 -84
- package/src/libs/agent-runtime/utils/streams/openai.ts +8 -17
- package/src/libs/agent-runtime/utils/usageConverter.test.ts +249 -0
- package/src/libs/agent-runtime/utils/usageConverter.ts +50 -0
- package/src/libs/agent-runtime/zeroone/index.test.ts +7 -294
- package/src/libs/langchain/loaders/epub/__tests__/__snapshots__/index.test.ts.snap +238 -0
- package/src/libs/langchain/loaders/epub/__tests__/demo.epub +0 -0
- package/src/libs/langchain/loaders/epub/__tests__/index.test.ts +24 -0
- package/src/libs/langchain/loaders/epub/index.ts +21 -0
- package/src/libs/langchain/loaders/index.ts +9 -0
- package/src/libs/langchain/types.ts +2 -1
- package/src/locales/default/chat.ts +4 -0
- package/src/server/utils/tempFileManager.ts +70 -0
- package/src/types/message/base.ts +14 -4
- package/src/utils/filter.test.ts +0 -122
- package/src/utils/filter.ts +0 -29
@@ -1,299 +1,12 @@
|
|
1
1
|
// @vitest-environment node
|
2
|
-
import
|
3
|
-
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
import { testProvider } from '@/libs/agent-runtime/providerTestUtils';
|
4
3
|
|
5
|
-
import { ChatStreamCallbacks, LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime';
|
6
|
-
|
7
|
-
import * as debugStreamModule from '../utils/debugStream';
|
8
4
|
import { LobeZeroOneAI } from './index';
|
9
5
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
// Mock the console.error to avoid polluting test output
|
17
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
18
|
-
|
19
|
-
let instance: LobeOpenAICompatibleRuntime;
|
20
|
-
|
21
|
-
beforeEach(() => {
|
22
|
-
instance = new LobeZeroOneAI({ apiKey: 'test' });
|
23
|
-
|
24
|
-
// 使用 vi.spyOn 来模拟 chat.completions.create 方法
|
25
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
26
|
-
new ReadableStream() as any,
|
27
|
-
);
|
28
|
-
});
|
29
|
-
|
30
|
-
afterEach(() => {
|
31
|
-
vi.clearAllMocks();
|
32
|
-
});
|
33
|
-
|
34
|
-
describe('LobeZeroOneAI', () => {
|
35
|
-
describe('init', () => {
|
36
|
-
it('should correctly initialize with an API key', async () => {
|
37
|
-
const instance = new LobeZeroOneAI({ apiKey: 'test_api_key' });
|
38
|
-
expect(instance).toBeInstanceOf(LobeZeroOneAI);
|
39
|
-
expect(instance.baseURL).toEqual(defaultBaseURL);
|
40
|
-
});
|
41
|
-
});
|
42
|
-
|
43
|
-
describe('chat', () => {
|
44
|
-
it('should return a StreamingTextResponse on successful API call', async () => {
|
45
|
-
// Arrange
|
46
|
-
const mockStream = new ReadableStream();
|
47
|
-
const mockResponse = Promise.resolve(mockStream);
|
48
|
-
|
49
|
-
(instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
|
50
|
-
|
51
|
-
// Act
|
52
|
-
const result = await instance.chat({
|
53
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
54
|
-
model: 'yi-34b-chat-0205',
|
55
|
-
temperature: 0,
|
56
|
-
});
|
57
|
-
|
58
|
-
// Assert
|
59
|
-
expect(result).toBeInstanceOf(Response);
|
60
|
-
});
|
61
|
-
|
62
|
-
it('should call ZeroOne API with corresponding options', async () => {
|
63
|
-
// Arrange
|
64
|
-
const mockStream = new ReadableStream();
|
65
|
-
const mockResponse = Promise.resolve(mockStream);
|
66
|
-
|
67
|
-
(instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
|
68
|
-
|
69
|
-
// Act
|
70
|
-
const result = await instance.chat({
|
71
|
-
max_tokens: 1024,
|
72
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
73
|
-
model: 'yi-34b-chat-0205',
|
74
|
-
temperature: 0.7,
|
75
|
-
top_p: 1,
|
76
|
-
});
|
77
|
-
|
78
|
-
// Assert
|
79
|
-
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
|
80
|
-
{
|
81
|
-
max_tokens: 1024,
|
82
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
83
|
-
model: 'yi-34b-chat-0205',
|
84
|
-
temperature: 0.7,
|
85
|
-
stream: true,
|
86
|
-
top_p: 1,
|
87
|
-
},
|
88
|
-
{ headers: { Accept: '*/*' } },
|
89
|
-
);
|
90
|
-
expect(result).toBeInstanceOf(Response);
|
91
|
-
});
|
92
|
-
|
93
|
-
describe('Error', () => {
|
94
|
-
it('should return ZeroOneBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
95
|
-
// Arrange
|
96
|
-
const apiError = new OpenAI.APIError(
|
97
|
-
400,
|
98
|
-
{
|
99
|
-
status: 400,
|
100
|
-
error: {
|
101
|
-
message: 'Bad Request',
|
102
|
-
},
|
103
|
-
},
|
104
|
-
'Error message',
|
105
|
-
{},
|
106
|
-
);
|
107
|
-
|
108
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
109
|
-
|
110
|
-
// Act
|
111
|
-
try {
|
112
|
-
await instance.chat({
|
113
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
114
|
-
model: 'yi-34b-chat-0205',
|
115
|
-
temperature: 0,
|
116
|
-
});
|
117
|
-
} catch (e) {
|
118
|
-
expect(e).toEqual({
|
119
|
-
endpoint: defaultBaseURL,
|
120
|
-
error: {
|
121
|
-
error: { message: 'Bad Request' },
|
122
|
-
status: 400,
|
123
|
-
},
|
124
|
-
errorType: bizErrorType,
|
125
|
-
provider,
|
126
|
-
});
|
127
|
-
}
|
128
|
-
});
|
129
|
-
|
130
|
-
it('should throw AgentRuntimeError with InvalidZeroOneAPIKey if no apiKey is provided', async () => {
|
131
|
-
try {
|
132
|
-
new LobeZeroOneAI({});
|
133
|
-
} catch (e) {
|
134
|
-
expect(e).toEqual({ errorType: invalidErrorType });
|
135
|
-
}
|
136
|
-
});
|
137
|
-
|
138
|
-
it('should return ZeroOneBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
|
139
|
-
// Arrange
|
140
|
-
const errorInfo = {
|
141
|
-
stack: 'abc',
|
142
|
-
cause: {
|
143
|
-
message: 'api is undefined',
|
144
|
-
},
|
145
|
-
};
|
146
|
-
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
147
|
-
|
148
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
149
|
-
|
150
|
-
// Act
|
151
|
-
try {
|
152
|
-
await instance.chat({
|
153
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
154
|
-
model: 'yi-34b-chat-0205',
|
155
|
-
temperature: 0,
|
156
|
-
});
|
157
|
-
} catch (e) {
|
158
|
-
expect(e).toEqual({
|
159
|
-
endpoint: defaultBaseURL,
|
160
|
-
error: {
|
161
|
-
cause: { message: 'api is undefined' },
|
162
|
-
stack: 'abc',
|
163
|
-
},
|
164
|
-
errorType: bizErrorType,
|
165
|
-
provider,
|
166
|
-
});
|
167
|
-
}
|
168
|
-
});
|
169
|
-
|
170
|
-
it('should return ZeroOneBizError with an cause response with desensitize Url', async () => {
|
171
|
-
// Arrange
|
172
|
-
const errorInfo = {
|
173
|
-
stack: 'abc',
|
174
|
-
cause: { message: 'api is undefined' },
|
175
|
-
};
|
176
|
-
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
177
|
-
|
178
|
-
instance = new LobeZeroOneAI({
|
179
|
-
apiKey: 'test',
|
180
|
-
|
181
|
-
baseURL: 'https://api.abc.com/v1',
|
182
|
-
});
|
183
|
-
|
184
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
185
|
-
|
186
|
-
// Act
|
187
|
-
try {
|
188
|
-
await instance.chat({
|
189
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
190
|
-
model: 'yi-34b-chat-0205',
|
191
|
-
temperature: 0,
|
192
|
-
});
|
193
|
-
} catch (e) {
|
194
|
-
expect(e).toEqual({
|
195
|
-
endpoint: 'https://api.***.com/v1',
|
196
|
-
error: {
|
197
|
-
cause: { message: 'api is undefined' },
|
198
|
-
stack: 'abc',
|
199
|
-
},
|
200
|
-
errorType: bizErrorType,
|
201
|
-
provider,
|
202
|
-
});
|
203
|
-
}
|
204
|
-
});
|
205
|
-
|
206
|
-
it('should throw an InvalidZeroOneAPIKey error type on 401 status code', async () => {
|
207
|
-
// Mock the API call to simulate a 401 error
|
208
|
-
const error = new Error('Unauthorized') as any;
|
209
|
-
error.status = 401;
|
210
|
-
vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error);
|
211
|
-
|
212
|
-
try {
|
213
|
-
await instance.chat({
|
214
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
215
|
-
model: 'yi-34b-chat-0205',
|
216
|
-
temperature: 0,
|
217
|
-
});
|
218
|
-
} catch (e) {
|
219
|
-
// Expect the chat method to throw an error with InvalidMoonshotAPIKey
|
220
|
-
expect(e).toEqual({
|
221
|
-
endpoint: defaultBaseURL,
|
222
|
-
error: new Error('Unauthorized'),
|
223
|
-
errorType: invalidErrorType,
|
224
|
-
provider,
|
225
|
-
});
|
226
|
-
}
|
227
|
-
});
|
228
|
-
|
229
|
-
it('should return AgentRuntimeError for non-OpenAI errors', async () => {
|
230
|
-
// Arrange
|
231
|
-
const genericError = new Error('Generic Error');
|
232
|
-
|
233
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);
|
234
|
-
|
235
|
-
// Act
|
236
|
-
try {
|
237
|
-
await instance.chat({
|
238
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
239
|
-
model: 'yi-34b-chat-0205',
|
240
|
-
temperature: 0,
|
241
|
-
});
|
242
|
-
} catch (e) {
|
243
|
-
expect(e).toEqual({
|
244
|
-
endpoint: defaultBaseURL,
|
245
|
-
errorType: 'AgentRuntimeError',
|
246
|
-
provider,
|
247
|
-
error: {
|
248
|
-
name: genericError.name,
|
249
|
-
cause: genericError.cause,
|
250
|
-
message: genericError.message,
|
251
|
-
stack: genericError.stack,
|
252
|
-
},
|
253
|
-
});
|
254
|
-
}
|
255
|
-
});
|
256
|
-
});
|
257
|
-
|
258
|
-
describe('DEBUG', () => {
|
259
|
-
it('should call debugStream and return StreamingTextResponse when DEBUG_ZEROONE_CHAT_COMPLETION is 1', async () => {
|
260
|
-
// Arrange
|
261
|
-
const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
|
262
|
-
const mockDebugStream = new ReadableStream({
|
263
|
-
start(controller) {
|
264
|
-
controller.enqueue('Debug stream content');
|
265
|
-
controller.close();
|
266
|
-
},
|
267
|
-
}) as any;
|
268
|
-
mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法
|
269
|
-
|
270
|
-
// 模拟 chat.completions.create 返回值,包括模拟的 tee 方法
|
271
|
-
(instance['client'].chat.completions.create as Mock).mockResolvedValue({
|
272
|
-
tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
|
273
|
-
});
|
274
|
-
|
275
|
-
// 保存原始环境变量值
|
276
|
-
const originalDebugValue = process.env.DEBUG_ZEROONE_CHAT_COMPLETION;
|
277
|
-
|
278
|
-
// 模拟环境变量
|
279
|
-
process.env.DEBUG_ZEROONE_CHAT_COMPLETION = '1';
|
280
|
-
vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
|
281
|
-
|
282
|
-
// 执行测试
|
283
|
-
// 运行你的测试函数,确保它会在条件满足时调用 debugStream
|
284
|
-
// 假设的测试函数调用,你可能需要根据实际情况调整
|
285
|
-
await instance.chat({
|
286
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
287
|
-
model: 'yi-34b-chat-0205',
|
288
|
-
temperature: 0,
|
289
|
-
});
|
290
|
-
|
291
|
-
// 验证 debugStream 被调用
|
292
|
-
expect(debugStreamModule.debugStream).toHaveBeenCalled();
|
293
|
-
|
294
|
-
// 恢复原始环境变量值
|
295
|
-
process.env.DEBUG_ZEROONE_CHAT_COMPLETION = originalDebugValue;
|
296
|
-
});
|
297
|
-
});
|
298
|
-
});
|
6
|
+
testProvider({
|
7
|
+
Runtime: LobeZeroOneAI,
|
8
|
+
provider: 'zeroone',
|
9
|
+
defaultBaseURL: 'https://api.lingyiwanwu.com/v1',
|
10
|
+
chatDebugEnv: 'DEBUG_ZEROONE_CHAT_COMPLETION',
|
11
|
+
chatModel: 'yi-34b-chat-0205',
|
299
12
|
});
|
@@ -0,0 +1,238 @@
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
2
|
+
|
3
|
+
exports[`EPubLoader > should run 1`] = `
|
4
|
+
[
|
5
|
+
Document {
|
6
|
+
"id": undefined,
|
7
|
+
"metadata": {
|
8
|
+
"loc": {
|
9
|
+
"lines": {
|
10
|
+
"from": 1,
|
11
|
+
"to": 13,
|
12
|
+
},
|
13
|
+
},
|
14
|
+
"source": "",
|
15
|
+
},
|
16
|
+
"pageContent": "HEFTY WATER
|
17
|
+
|
18
|
+
This document serves to test Reading System support for the epub:switch
|
19
|
+
[http://idpf.org/epub/30/spec/epub30-contentdocs.html#sec-xhtml-content-switch]
|
20
|
+
element. There is also a little bit of ruby markup
|
21
|
+
[http://www.w3.org/TR/html5/the-ruby-element.html#the-ruby-element] available.
|
22
|
+
|
23
|
+
|
24
|
+
THE SWITCH
|
25
|
+
|
26
|
+
Below is an instance of the epub:switch element, containing Chemical Markup
|
27
|
+
Language [http://en.wikipedia.org/wiki/Chemical_Markup_Language] (CML). The
|
28
|
+
fallback content is a chunk of plain XHTML5.",
|
29
|
+
},
|
30
|
+
Document {
|
31
|
+
"id": undefined,
|
32
|
+
"metadata": {
|
33
|
+
"loc": {
|
34
|
+
"lines": {
|
35
|
+
"from": 9,
|
36
|
+
"to": 22,
|
37
|
+
},
|
38
|
+
},
|
39
|
+
"source": "",
|
40
|
+
},
|
41
|
+
"pageContent": "THE SWITCH
|
42
|
+
|
43
|
+
Below is an instance of the epub:switch element, containing Chemical Markup
|
44
|
+
Language [http://en.wikipedia.org/wiki/Chemical_Markup_Language] (CML). The
|
45
|
+
fallback content is a chunk of plain XHTML5.
|
46
|
+
|
47
|
+
* If your Reading System supports epub:switch and CML, it will render the CML
|
48
|
+
formula natively, and ignore (a.k.a not display) the XHTML fallback.
|
49
|
+
* If your Reading System supports epub:switch but not CML, it will ignore (not
|
50
|
+
display) the CML formula, and render the the XHTML fallback instead.
|
51
|
+
* If your Reading System does not support epub:switch at all, then the
|
52
|
+
rendering results are somewhat unpredictable, but the most likely result is
|
53
|
+
that it will display both a failed attempt to render the CML and the XHTML
|
54
|
+
fallback.",
|
55
|
+
},
|
56
|
+
Document {
|
57
|
+
"id": undefined,
|
58
|
+
"metadata": {
|
59
|
+
"loc": {
|
60
|
+
"lines": {
|
61
|
+
"from": 24,
|
62
|
+
"to": 43,
|
63
|
+
},
|
64
|
+
},
|
65
|
+
"source": "",
|
66
|
+
},
|
67
|
+
"pageContent": "Note: the XHTML fallback is bold and enclosed in a gray dotted box with a
|
68
|
+
slightly gray background. A failed CML rendering will most likely appear above
|
69
|
+
the gray fallback box and read:
|
70
|
+
"H hydrogen O oxygen hefty H O water".
|
71
|
+
|
72
|
+
Here the switch begins...
|
73
|
+
|
74
|
+
|
75
|
+
H hydrogen O oxygen hefty H O water
|
76
|
+
|
77
|
+
2H2 + O2 ⟶ 2H2O
|
78
|
+
|
79
|
+
... and here the switch ends.
|
80
|
+
|
81
|
+
|
82
|
+
THE SOURCE
|
83
|
+
|
84
|
+
Below is a rendition of the source code of the switch element. Your Reading
|
85
|
+
System should display this correctly regardless of whether it supports the
|
86
|
+
switch element.",
|
87
|
+
},
|
88
|
+
Document {
|
89
|
+
"id": undefined,
|
90
|
+
"metadata": {
|
91
|
+
"loc": {
|
92
|
+
"lines": {
|
93
|
+
"from": 46,
|
94
|
+
"to": 66,
|
95
|
+
},
|
96
|
+
},
|
97
|
+
"source": "",
|
98
|
+
},
|
99
|
+
"pageContent": "<switch xmlns="http://www.idpf.org/2007/ops">
|
100
|
+
<case required-namespace="http://www.xml-cml.org/schema">
|
101
|
+
<chem xmlns="http://www.xml-cml.org/schema">
|
102
|
+
<reaction>
|
103
|
+
<molecule n="2">
|
104
|
+
<atom n="2"> H </atom>
|
105
|
+
<caption> hydrogen </caption>
|
106
|
+
</molecule>
|
107
|
+
<plus></plus>
|
108
|
+
<molecule>
|
109
|
+
<atom n="2"> O </atom>
|
110
|
+
<caption> oxygen </caption>
|
111
|
+
</molecule>
|
112
|
+
<gives>
|
113
|
+
<caption> hefty </caption>
|
114
|
+
</gives>
|
115
|
+
<molecule n="2">
|
116
|
+
<atom n="2"> H </atom>
|
117
|
+
<atom> O </atom>
|
118
|
+
<caption> water </caption>
|
119
|
+
</molecule>",
|
120
|
+
},
|
121
|
+
Document {
|
122
|
+
"id": undefined,
|
123
|
+
"metadata": {
|
124
|
+
"loc": {
|
125
|
+
"lines": {
|
126
|
+
"from": 57,
|
127
|
+
"to": 79,
|
128
|
+
},
|
129
|
+
},
|
130
|
+
"source": "",
|
131
|
+
},
|
132
|
+
"pageContent": "<caption> oxygen </caption>
|
133
|
+
</molecule>
|
134
|
+
<gives>
|
135
|
+
<caption> hefty </caption>
|
136
|
+
</gives>
|
137
|
+
<molecule n="2">
|
138
|
+
<atom n="2"> H </atom>
|
139
|
+
<atom> O </atom>
|
140
|
+
<caption> water </caption>
|
141
|
+
</molecule>
|
142
|
+
</reaction>
|
143
|
+
</chem>
|
144
|
+
</case>
|
145
|
+
<default>
|
146
|
+
<p xmlns="http://www.w3.org/1999/xhtml" id="fallback">
|
147
|
+
<span>2H<sub>2</sub></span>
|
148
|
+
<span>+</span>
|
149
|
+
<span>O<sub>2</sub></span>
|
150
|
+
<span>⟶</span>
|
151
|
+
<span>2H<sub>2</sub>O</span>
|
152
|
+
</p>
|
153
|
+
</default>
|
154
|
+
</switch>",
|
155
|
+
},
|
156
|
+
Document {
|
157
|
+
"id": undefined,
|
158
|
+
"metadata": {
|
159
|
+
"loc": {
|
160
|
+
"lines": {
|
161
|
+
"from": 84,
|
162
|
+
"to": 94,
|
163
|
+
},
|
164
|
+
},
|
165
|
+
"source": "",
|
166
|
+
},
|
167
|
+
"pageContent": "HEFTY RUBY WATER
|
168
|
+
|
169
|
+
While the ruby element is mostly used in east-asian languages, it can also be
|
170
|
+
useful in other contexts. As an example, and as you can see in the source of the
|
171
|
+
CML element above, the code includes a caption element which is intended to be
|
172
|
+
displayed below the formula segments. Following this paragraph is a reworked
|
173
|
+
version of the XHTML fallback used above, using the ruby element. If your
|
174
|
+
Reading System does not support ruby markup, then the captions will appear in
|
175
|
+
parentheses on the same line as the formula segments.
|
176
|
+
|
177
|
+
2H2(hydrogen) + O2(oxygen) ⟶(hefty) 2H2O(water)",
|
178
|
+
},
|
179
|
+
Document {
|
180
|
+
"id": undefined,
|
181
|
+
"metadata": {
|
182
|
+
"loc": {
|
183
|
+
"lines": {
|
184
|
+
"from": 94,
|
185
|
+
"to": 111,
|
186
|
+
},
|
187
|
+
},
|
188
|
+
"source": "",
|
189
|
+
},
|
190
|
+
"pageContent": "2H2(hydrogen) + O2(oxygen) ⟶(hefty) 2H2O(water)
|
191
|
+
|
192
|
+
If your Reading System in addition to supporting ruby markup also supports the
|
193
|
+
-epub-ruby-position
|
194
|
+
[http://idpf.org/epub/30/spec/epub30-contentdocs.html#sec-css-ruby-position]
|
195
|
+
property, then the captions will appear under the formula segments instead of
|
196
|
+
over them.
|
197
|
+
|
198
|
+
The source code for the ruby version of the XHTML fallback looks as follows:
|
199
|
+
|
200
|
+
|
201
|
+
<p id="rubyp">
|
202
|
+
<ruby>2H<sub>2</sub><rp>(</rp><rt>hydrogen</rt><rp>)</rp></ruby>
|
203
|
+
<span>+</span>
|
204
|
+
<ruby>O<sub>2</sub><rp>(</rp><rt>oxygen</rt><rp>)</rp></ruby>
|
205
|
+
<ruby>⟶<rp>(</rp><rt>hefty</rt><rp>)</rp></ruby>
|
206
|
+
<ruby>2H<sub>2</sub>O<rp>(</rp><rt>water</rt><rp>)</rp></ruby>
|
207
|
+
</p>",
|
208
|
+
},
|
209
|
+
Document {
|
210
|
+
"id": undefined,
|
211
|
+
"metadata": {
|
212
|
+
"loc": {
|
213
|
+
"lines": {
|
214
|
+
"from": 105,
|
215
|
+
"to": 120,
|
216
|
+
},
|
217
|
+
},
|
218
|
+
"source": "",
|
219
|
+
},
|
220
|
+
"pageContent": "<p id="rubyp">
|
221
|
+
<ruby>2H<sub>2</sub><rp>(</rp><rt>hydrogen</rt><rp>)</rp></ruby>
|
222
|
+
<span>+</span>
|
223
|
+
<ruby>O<sub>2</sub><rp>(</rp><rt>oxygen</rt><rp>)</rp></ruby>
|
224
|
+
<ruby>⟶<rp>(</rp><rt>hefty</rt><rp>)</rp></ruby>
|
225
|
+
<ruby>2H<sub>2</sub>O<rp>(</rp><rt>water</rt><rp>)</rp></ruby>
|
226
|
+
</p>
|
227
|
+
|
228
|
+
|
229
|
+
... and the css declaration using the -epub-ruby-position property looks like
|
230
|
+
this:
|
231
|
+
|
232
|
+
|
233
|
+
p#rubyp {
|
234
|
+
-epub-ruby-position : under;
|
235
|
+
}",
|
236
|
+
},
|
237
|
+
]
|
238
|
+
`;
|
Binary file
|
@@ -0,0 +1,24 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import * as fs from 'node:fs';
|
3
|
+
import { join } from 'node:path';
|
4
|
+
import { expect } from 'vitest';
|
5
|
+
|
6
|
+
import { EPubLoader } from '../index';
|
7
|
+
|
8
|
+
function sanitizeDynamicFields(document: any[]) {
|
9
|
+
for (const doc of document) {
|
10
|
+
doc.metadata.source && (doc.metadata.source = '');
|
11
|
+
}
|
12
|
+
return document;
|
13
|
+
}
|
14
|
+
|
15
|
+
describe('EPubLoader', () => {
|
16
|
+
it('should run', async () => {
|
17
|
+
const content = fs.readFileSync(join(__dirname, `./demo.epub`));
|
18
|
+
|
19
|
+
const fileContent: Uint8Array = new Uint8Array(content);
|
20
|
+
|
21
|
+
const data = await EPubLoader(fileContent);
|
22
|
+
expect(sanitizeDynamicFields(data)).toMatchSnapshot();
|
23
|
+
});
|
24
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { EPubLoader as Loader } from '@langchain/community/document_loaders/fs/epub';
|
2
|
+
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
3
|
+
import { loaderConfig } from '../config';
|
4
|
+
import { TempFileManager } from '@/server/utils/tempFileManager';
|
5
|
+
|
6
|
+
export const EPubLoader = async (content: Uint8Array) => {
|
7
|
+
const tempManager = new TempFileManager();
|
8
|
+
try {
|
9
|
+
const tempPath = await tempManager.writeTempFile(content);
|
10
|
+
const loader = new Loader(tempPath);
|
11
|
+
const documents = await loader.load();
|
12
|
+
|
13
|
+
const splitter = new RecursiveCharacterTextSplitter(loaderConfig);
|
14
|
+
return await splitter.splitDocuments(documents);
|
15
|
+
} catch (e) {
|
16
|
+
throw new Error(`EPubLoader error: ${(e as Error).message}`);
|
17
|
+
} finally {
|
18
|
+
tempManager.cleanup(); // 确保清理
|
19
|
+
}
|
20
|
+
|
21
|
+
};
|
@@ -14,6 +14,7 @@ import { MarkdownLoader } from './markdown';
|
|
14
14
|
import { PdfLoader } from './pdf';
|
15
15
|
import { PPTXLoader } from './pptx';
|
16
16
|
import { TextLoader } from './txt';
|
17
|
+
import { EPubLoader } from './epub';
|
17
18
|
|
18
19
|
class LangChainError extends Error {
|
19
20
|
constructor(message: string) {
|
@@ -64,6 +65,10 @@ export class ChunkingLoader {
|
|
64
65
|
return await CsVLoader(fileBlob);
|
65
66
|
}
|
66
67
|
|
68
|
+
case 'epub': {
|
69
|
+
return await EPubLoader(content);
|
70
|
+
}
|
71
|
+
|
67
72
|
default: {
|
68
73
|
throw new Error(
|
69
74
|
`Unsupported file type [${type}], please check your file is supported, or create report issue here: https://github.com/lobehub/lobe-chat/discussions/3550`,
|
@@ -100,6 +105,10 @@ export class ChunkingLoader {
|
|
100
105
|
return 'csv';
|
101
106
|
}
|
102
107
|
|
108
|
+
if (filename.endsWith('epub')) {
|
109
|
+
return 'epub';
|
110
|
+
}
|
111
|
+
|
103
112
|
const ext = filename.split('.').pop();
|
104
113
|
|
105
114
|
if (ext && SupportedTextSplitterLanguages.includes(ext as SupportedTextSplitterLanguage)) {
|
@@ -93,15 +93,19 @@ export default {
|
|
93
93
|
inputMinutes: '${{amount}}/分钟',
|
94
94
|
inputTokens: '输入 {{amount}}/积分 · ${{amount}}/M',
|
95
95
|
outputTokens: '输出 {{amount}}/积分 · ${{amount}}/M',
|
96
|
+
writeCacheInputTokens: '缓存输入写入 {{amount}}/积分 · ${{amount}}/M',
|
96
97
|
},
|
97
98
|
},
|
98
99
|
tokenDetails: {
|
100
|
+
average: '平均单价',
|
99
101
|
input: '输入',
|
100
102
|
inputAudio: '音频输入',
|
101
103
|
inputCached: '输入缓存',
|
104
|
+
inputCitation: '引用输入',
|
102
105
|
inputText: '文本输入',
|
103
106
|
inputTitle: '输入明细',
|
104
107
|
inputUncached: '输入未缓存',
|
108
|
+
inputWriteCached: '输入缓存写入',
|
105
109
|
output: '输出',
|
106
110
|
outputAudio: '音频输出',
|
107
111
|
outputText: '文本输出',
|