@lobehub/lobehub 2.1.21 → 2.1.22
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 +25 -0
- package/changelog/v2.json +9 -0
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/executor/index.ts +37 -12
- package/packages/model-runtime/src/providers/siliconcloud/index.test.ts +1 -1
- package/packages/model-runtime/src/providers/siliconcloud/index.ts +4 -4
- package/src/libs/next/Image.tsx +0 -2
- package/src/libs/next/Link.tsx +0 -2
- package/src/libs/next/dynamic.tsx +1 -1
- package/src/libs/next/index.ts +1 -1
- package/src/libs/next/navigation.ts +1 -1
- package/src/libs/router/Link.tsx +1 -2
- package/src/libs/router/index.ts +1 -1
- package/src/libs/router/navigation.ts +5 -4
- package/src/server/services/notebook/__tests__/index.test.ts +218 -0
- package/src/server/services/notebook/index.ts +110 -0
- package/src/server/services/toolExecution/builtin.ts +1 -1
- package/src/server/services/toolExecution/serverRuntimes/__tests__/notebook.test.ts +51 -0
- package/src/server/services/toolExecution/serverRuntimes/index.ts +2 -1
- package/src/server/services/toolExecution/serverRuntimes/notebook.ts +26 -0
- package/src/store/chat/slices/message/actions/publicApi.ts +1 -1
- package/src/store/chat/slices/operation/actions.ts +1 -1
- package/src/store/tool/slices/builtin/executors/index.ts +1 -1
- package/src/store/tool/slices/builtin/executors/lobe-notebook.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 2.1.22](https://github.com/lobehub/lobe-chat/compare/v2.1.21...v2.1.22)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-02-08**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Register Notebook tool in server runtime.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Register Notebook tool in server runtime, closes [#12203](https://github.com/lobehub/lobe-chat/issues/12203) ([be6da39](https://github.com/lobehub/lobe-chat/commit/be6da39))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 2.1.21](https://github.com/lobehub/lobe-chat/compare/v2.1.20...v2.1.21)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2026-02-08**</sup>
|
package/changelog/v2.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.22",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lobe Notebook Executor
|
|
3
3
|
*
|
|
4
|
-
* Handles notebook document operations
|
|
5
|
-
*
|
|
4
|
+
* Handles notebook document operations.
|
|
5
|
+
* The NotebookService is injected via constructor so both client and server can provide their own implementation.
|
|
6
6
|
*
|
|
7
7
|
* Note: listDocuments is not exposed as a tool - it's automatically injected by the system.
|
|
8
8
|
*/
|
|
9
9
|
import { BaseExecutor, type BuiltinToolContext, type BuiltinToolResult } from '@lobechat/types';
|
|
10
10
|
|
|
11
|
-
import { notebookService } from '@/services/notebook';
|
|
12
|
-
|
|
13
11
|
import {
|
|
14
12
|
type CreateDocumentArgs,
|
|
15
13
|
type DeleteDocumentArgs,
|
|
14
|
+
type DocumentType,
|
|
16
15
|
type GetDocumentArgs,
|
|
17
16
|
NotebookApiName,
|
|
18
17
|
NotebookIdentifier,
|
|
19
18
|
type UpdateDocumentArgs,
|
|
20
19
|
} from '../types';
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
interface CreateDocumentParams {
|
|
22
|
+
content: string;
|
|
23
|
+
description: string;
|
|
24
|
+
title: string;
|
|
25
|
+
topicId: string;
|
|
26
|
+
type?: DocumentType;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface UpdateDocumentParams {
|
|
30
|
+
append?: boolean;
|
|
31
|
+
content?: string;
|
|
32
|
+
id: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NotebookServiceApi {
|
|
37
|
+
createDocument: (params: CreateDocumentParams) => Promise<any>;
|
|
38
|
+
deleteDocument: (id: string) => Promise<any>;
|
|
39
|
+
getDocument: (id: string) => Promise<any>;
|
|
40
|
+
updateDocument: (params: UpdateDocumentParams) => Promise<any>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
|
23
44
|
readonly identifier = NotebookIdentifier;
|
|
24
45
|
protected readonly apiEnum = NotebookApiName;
|
|
25
46
|
|
|
47
|
+
private notebookService: NotebookServiceApi;
|
|
48
|
+
|
|
49
|
+
constructor(notebookService: NotebookServiceApi) {
|
|
50
|
+
super();
|
|
51
|
+
this.notebookService = notebookService;
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
/**
|
|
27
55
|
* Create a new document
|
|
28
56
|
*/
|
|
@@ -42,7 +70,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
|
|
42
70
|
};
|
|
43
71
|
}
|
|
44
72
|
|
|
45
|
-
const document = await notebookService.createDocument({
|
|
73
|
+
const document = await this.notebookService.createDocument({
|
|
46
74
|
content: params.content,
|
|
47
75
|
description: params.description,
|
|
48
76
|
title: params.title,
|
|
@@ -80,7 +108,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
|
|
80
108
|
return { stop: true, success: false };
|
|
81
109
|
}
|
|
82
110
|
|
|
83
|
-
const document = await notebookService.updateDocument(params);
|
|
111
|
+
const document = await this.notebookService.updateDocument(params);
|
|
84
112
|
|
|
85
113
|
return {
|
|
86
114
|
content: `✏️ Document updated successfully`,
|
|
@@ -112,7 +140,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
|
|
112
140
|
return { stop: true, success: false };
|
|
113
141
|
}
|
|
114
142
|
|
|
115
|
-
const document = await notebookService.getDocument(params.id);
|
|
143
|
+
const document = await this.notebookService.getDocument(params.id);
|
|
116
144
|
|
|
117
145
|
if (!document) {
|
|
118
146
|
return {
|
|
@@ -151,7 +179,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
|
|
151
179
|
return { stop: true, success: false };
|
|
152
180
|
}
|
|
153
181
|
|
|
154
|
-
await notebookService.deleteDocument(params.id);
|
|
182
|
+
await this.notebookService.deleteDocument(params.id);
|
|
155
183
|
|
|
156
184
|
return {
|
|
157
185
|
content: `🗑️ Document deleted successfully`,
|
|
@@ -170,6 +198,3 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
|
|
170
198
|
}
|
|
171
199
|
};
|
|
172
200
|
}
|
|
173
|
-
|
|
174
|
-
// Export the executor instance for registration
|
|
175
|
-
export const notebookExecutor = new NotebookExecutor();
|
|
@@ -170,7 +170,7 @@ describe('LobeSiliconCloudAI - custom features', () => {
|
|
|
170
170
|
});
|
|
171
171
|
} catch (e: any) {
|
|
172
172
|
expect(e.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
|
|
173
|
-
expect(e.message).toContain('
|
|
173
|
+
expect(e.message).toContain('Please check if the API Key balance is sufficient');
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
});
|
|
@@ -36,7 +36,7 @@ export const params = {
|
|
|
36
36
|
error: errorResponse.status,
|
|
37
37
|
errorType: AgentRuntimeErrorType.ProviderBizError,
|
|
38
38
|
message:
|
|
39
|
-
'
|
|
39
|
+
'Please check if the API Key balance is sufficient, or if you are using an unverified API Key to access models that require verification.',
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -57,10 +57,10 @@ export const params = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
if (thinking) {
|
|
60
|
-
//
|
|
60
|
+
// Only some models support specifying enable_thinking, while other slow-thinking models only support adjusting thinking budget
|
|
61
61
|
const hybridThinkingModels = [
|
|
62
|
-
/GLM-4\.5(?!.*Air$)/, // GLM-4.5
|
|
63
|
-
/Qwen3-(?:\d+B|\d+B-A\d+B)$/, // Qwen3-8B
|
|
62
|
+
/GLM-4\.5(?!.*Air$)/, // GLM-4.5 and GLM-4.5V (excluding GLM-4.5 Air)
|
|
63
|
+
/Qwen3-(?:\d+B|\d+B-A\d+B)$/, // Qwen3-8B, Qwen3-14B, Qwen3-32B, Qwen3-30B-A3B, Qwen3-235B-A22B
|
|
64
64
|
/DeepSeek-V3\.1/,
|
|
65
65
|
/Hunyuan-A13B-Instruct/,
|
|
66
66
|
];
|
package/src/libs/next/Image.tsx
CHANGED
package/src/libs/next/Link.tsx
CHANGED
package/src/libs/next/index.ts
CHANGED
package/src/libs/router/Link.tsx
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* React Router Link component wrapper.
|
|
3
3
|
* Provides a Next.js-like API (href prop) while using React Router internally.
|
|
4
4
|
*
|
|
5
|
-
* @see RFC 147
|
|
5
|
+
* @see RFC 147
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
7
|
import React, { memo } from 'react';
|
|
9
8
|
import { Link as ReactRouterLink, type LinkProps as ReactRouterLinkProps } from 'react-router-dom';
|
|
10
9
|
|
package/src/libs/router/index.ts
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* - import { useRouter, usePathname, useSearchParams, useQuery } from '@/libs/router/navigation';
|
|
7
7
|
*
|
|
8
|
-
* @see RFC 147
|
|
8
|
+
* @see RFC 147
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
10
|
import qs from 'query-string';
|
|
12
11
|
import { useMemo } from 'react';
|
|
13
12
|
import {
|
|
@@ -33,7 +32,7 @@ export const useRouter = () => {
|
|
|
33
32
|
push: (href: string) => navigate(href),
|
|
34
33
|
replace: (href: string) => navigate(href, { replace: true }),
|
|
35
34
|
}),
|
|
36
|
-
[navigate]
|
|
35
|
+
[navigate],
|
|
37
36
|
);
|
|
38
37
|
};
|
|
39
38
|
|
|
@@ -56,7 +55,9 @@ export const useSearchParams = () => {
|
|
|
56
55
|
/**
|
|
57
56
|
* Hook to get route params.
|
|
58
57
|
*/
|
|
59
|
-
export const useParams = <
|
|
58
|
+
export const useParams = <
|
|
59
|
+
T extends Record<string, string | undefined> = Record<string, string | undefined>,
|
|
60
|
+
>() => {
|
|
60
61
|
return useReactRouterParams<T>();
|
|
61
62
|
};
|
|
62
63
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { DocumentModel } from '@/database/models/document';
|
|
4
|
+
import { TopicDocumentModel } from '@/database/models/topicDocument';
|
|
5
|
+
|
|
6
|
+
import { NotebookRuntimeService } from '../index';
|
|
7
|
+
|
|
8
|
+
vi.mock('@/database/models/document');
|
|
9
|
+
vi.mock('@/database/models/topicDocument');
|
|
10
|
+
|
|
11
|
+
describe('NotebookRuntimeService', () => {
|
|
12
|
+
let service: NotebookRuntimeService;
|
|
13
|
+
const mockDb = {} as any;
|
|
14
|
+
const mockUserId = 'test-user';
|
|
15
|
+
let mockDocumentModel: any;
|
|
16
|
+
let mockTopicDocumentModel: any;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
mockDocumentModel = {
|
|
22
|
+
create: vi.fn(),
|
|
23
|
+
delete: vi.fn(),
|
|
24
|
+
findById: vi.fn(),
|
|
25
|
+
update: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
mockTopicDocumentModel = {
|
|
29
|
+
associate: vi.fn(),
|
|
30
|
+
deleteByDocumentId: vi.fn(),
|
|
31
|
+
findByTopicId: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
vi.mocked(DocumentModel).mockImplementation(() => mockDocumentModel);
|
|
35
|
+
vi.mocked(TopicDocumentModel).mockImplementation(() => mockTopicDocumentModel);
|
|
36
|
+
|
|
37
|
+
service = new NotebookRuntimeService({ serverDB: mockDb, userId: mockUserId });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mockDocument = {
|
|
41
|
+
content: '# Hello',
|
|
42
|
+
createdAt: new Date('2025-01-01'),
|
|
43
|
+
description: 'A test doc',
|
|
44
|
+
fileType: 'markdown',
|
|
45
|
+
id: 'doc-1',
|
|
46
|
+
source: 'notebook:topic-1',
|
|
47
|
+
sourceType: 'api' as const,
|
|
48
|
+
title: 'Test Doc',
|
|
49
|
+
totalCharCount: 7,
|
|
50
|
+
totalLineCount: 1,
|
|
51
|
+
updatedAt: new Date('2025-01-01'),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('createDocument', () => {
|
|
55
|
+
it('should create a document and return service result', async () => {
|
|
56
|
+
mockDocumentModel.create.mockResolvedValue(mockDocument);
|
|
57
|
+
|
|
58
|
+
const params = {
|
|
59
|
+
content: '# Hello',
|
|
60
|
+
fileType: 'markdown',
|
|
61
|
+
source: 'notebook:topic-1',
|
|
62
|
+
sourceType: 'api' as const,
|
|
63
|
+
title: 'Test Doc',
|
|
64
|
+
totalCharCount: 7,
|
|
65
|
+
totalLineCount: 1,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = await service.createDocument(params);
|
|
69
|
+
|
|
70
|
+
expect(mockDocumentModel.create).toHaveBeenCalledWith(params);
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
content: '# Hello',
|
|
73
|
+
createdAt: mockDocument.createdAt,
|
|
74
|
+
description: 'A test doc',
|
|
75
|
+
fileType: 'markdown',
|
|
76
|
+
id: 'doc-1',
|
|
77
|
+
source: 'notebook:topic-1',
|
|
78
|
+
sourceType: 'api',
|
|
79
|
+
title: 'Test Doc',
|
|
80
|
+
totalCharCount: 7,
|
|
81
|
+
updatedAt: mockDocument.updatedAt,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should convert topic sourceType to api', async () => {
|
|
86
|
+
mockDocumentModel.create.mockResolvedValue({
|
|
87
|
+
...mockDocument,
|
|
88
|
+
sourceType: 'topic',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await service.createDocument({
|
|
92
|
+
content: 'test',
|
|
93
|
+
fileType: 'markdown',
|
|
94
|
+
source: 'test',
|
|
95
|
+
sourceType: 'api',
|
|
96
|
+
title: 'test',
|
|
97
|
+
totalCharCount: 4,
|
|
98
|
+
totalLineCount: 1,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result.sourceType).toBe('api');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getDocument', () => {
|
|
106
|
+
it('should return document when found', async () => {
|
|
107
|
+
mockDocumentModel.findById.mockResolvedValue(mockDocument);
|
|
108
|
+
|
|
109
|
+
const result = await service.getDocument('doc-1');
|
|
110
|
+
|
|
111
|
+
expect(mockDocumentModel.findById).toHaveBeenCalledWith('doc-1');
|
|
112
|
+
expect(result).toBeDefined();
|
|
113
|
+
expect(result!.id).toBe('doc-1');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return undefined when not found', async () => {
|
|
117
|
+
mockDocumentModel.findById.mockResolvedValue(undefined);
|
|
118
|
+
|
|
119
|
+
const result = await service.getDocument('nonexistent');
|
|
120
|
+
|
|
121
|
+
expect(result).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('updateDocument', () => {
|
|
126
|
+
it('should update content and recalculate stats', async () => {
|
|
127
|
+
const newContent = 'line1\nline2\nline3';
|
|
128
|
+
mockDocumentModel.update.mockResolvedValue(undefined);
|
|
129
|
+
mockDocumentModel.findById.mockResolvedValue({
|
|
130
|
+
...mockDocument,
|
|
131
|
+
content: newContent,
|
|
132
|
+
totalCharCount: newContent.length,
|
|
133
|
+
totalLineCount: 3,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await service.updateDocument('doc-1', { content: newContent });
|
|
137
|
+
|
|
138
|
+
expect(mockDocumentModel.update).toHaveBeenCalledWith('doc-1', {
|
|
139
|
+
content: newContent,
|
|
140
|
+
totalCharCount: newContent.length,
|
|
141
|
+
totalLineCount: 3,
|
|
142
|
+
});
|
|
143
|
+
expect(result.content).toBe(newContent);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should update title only', async () => {
|
|
147
|
+
mockDocumentModel.update.mockResolvedValue(undefined);
|
|
148
|
+
mockDocumentModel.findById.mockResolvedValue({
|
|
149
|
+
...mockDocument,
|
|
150
|
+
title: 'New Title',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = await service.updateDocument('doc-1', { title: 'New Title' });
|
|
154
|
+
|
|
155
|
+
expect(mockDocumentModel.update).toHaveBeenCalledWith('doc-1', { title: 'New Title' });
|
|
156
|
+
expect(result.title).toBe('New Title');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should throw if document not found after update', async () => {
|
|
160
|
+
mockDocumentModel.update.mockResolvedValue(undefined);
|
|
161
|
+
mockDocumentModel.findById.mockResolvedValue(undefined);
|
|
162
|
+
|
|
163
|
+
await expect(service.updateDocument('doc-1', { title: 'x' })).rejects.toThrow(
|
|
164
|
+
'Document not found after update: doc-1',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('deleteDocument', () => {
|
|
170
|
+
it('should delete associations first then the document', async () => {
|
|
171
|
+
mockTopicDocumentModel.deleteByDocumentId.mockResolvedValue(undefined);
|
|
172
|
+
mockDocumentModel.delete.mockResolvedValue(undefined);
|
|
173
|
+
|
|
174
|
+
await service.deleteDocument('doc-1');
|
|
175
|
+
|
|
176
|
+
expect(mockTopicDocumentModel.deleteByDocumentId).toHaveBeenCalledWith('doc-1');
|
|
177
|
+
expect(mockDocumentModel.delete).toHaveBeenCalledWith('doc-1');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('associateDocumentWithTopic', () => {
|
|
182
|
+
it('should associate document with topic', async () => {
|
|
183
|
+
mockTopicDocumentModel.associate.mockResolvedValue({
|
|
184
|
+
documentId: 'doc-1',
|
|
185
|
+
topicId: 'topic-1',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await service.associateDocumentWithTopic('doc-1', 'topic-1');
|
|
189
|
+
|
|
190
|
+
expect(mockTopicDocumentModel.associate).toHaveBeenCalledWith({
|
|
191
|
+
documentId: 'doc-1',
|
|
192
|
+
topicId: 'topic-1',
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('getDocumentsByTopicId', () => {
|
|
198
|
+
it('should return documents for a topic', async () => {
|
|
199
|
+
mockTopicDocumentModel.findByTopicId.mockResolvedValue([mockDocument]);
|
|
200
|
+
|
|
201
|
+
const result = await service.getDocumentsByTopicId('topic-1');
|
|
202
|
+
|
|
203
|
+
expect(mockTopicDocumentModel.findByTopicId).toHaveBeenCalledWith('topic-1', undefined);
|
|
204
|
+
expect(result).toHaveLength(1);
|
|
205
|
+
expect(result[0].id).toBe('doc-1');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should pass filter to findByTopicId', async () => {
|
|
209
|
+
mockTopicDocumentModel.findByTopicId.mockResolvedValue([]);
|
|
210
|
+
|
|
211
|
+
await service.getDocumentsByTopicId('topic-1', { type: 'markdown' });
|
|
212
|
+
|
|
213
|
+
expect(mockTopicDocumentModel.findByTopicId).toHaveBeenCalledWith('topic-1', {
|
|
214
|
+
type: 'markdown',
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type LobeChatDatabase } from '@lobechat/database';
|
|
2
|
+
|
|
3
|
+
import { DocumentModel } from '@/database/models/document';
|
|
4
|
+
import { TopicDocumentModel } from '@/database/models/topicDocument';
|
|
5
|
+
|
|
6
|
+
interface DocumentServiceResult {
|
|
7
|
+
content: string | null;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
description: string | null;
|
|
10
|
+
fileType: string;
|
|
11
|
+
id: string;
|
|
12
|
+
source: string;
|
|
13
|
+
sourceType: 'api' | 'file' | 'web';
|
|
14
|
+
title: string | null;
|
|
15
|
+
totalCharCount: number;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NotebookRuntimeServiceOptions {
|
|
20
|
+
serverDB: LobeChatDatabase;
|
|
21
|
+
userId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toServiceResult = (doc: {
|
|
25
|
+
content: string | null;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
description: string | null;
|
|
28
|
+
fileType: string;
|
|
29
|
+
id: string;
|
|
30
|
+
source: string;
|
|
31
|
+
sourceType: 'api' | 'file' | 'web' | 'topic';
|
|
32
|
+
title: string | null;
|
|
33
|
+
totalCharCount: number;
|
|
34
|
+
updatedAt: Date;
|
|
35
|
+
}): DocumentServiceResult => ({
|
|
36
|
+
content: doc.content,
|
|
37
|
+
createdAt: doc.createdAt,
|
|
38
|
+
description: doc.description,
|
|
39
|
+
fileType: doc.fileType,
|
|
40
|
+
id: doc.id,
|
|
41
|
+
source: doc.source,
|
|
42
|
+
sourceType: doc.sourceType === 'topic' ? 'api' : doc.sourceType,
|
|
43
|
+
title: doc.title,
|
|
44
|
+
totalCharCount: doc.totalCharCount,
|
|
45
|
+
updatedAt: doc.updatedAt,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export class NotebookRuntimeService {
|
|
49
|
+
private documentModel: DocumentModel;
|
|
50
|
+
private topicDocumentModel: TopicDocumentModel;
|
|
51
|
+
|
|
52
|
+
constructor(options: NotebookRuntimeServiceOptions) {
|
|
53
|
+
this.documentModel = new DocumentModel(options.serverDB, options.userId);
|
|
54
|
+
this.topicDocumentModel = new TopicDocumentModel(options.serverDB, options.userId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
associateDocumentWithTopic = async (documentId: string, topicId: string): Promise<void> => {
|
|
58
|
+
await this.topicDocumentModel.associate({ documentId, topicId });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
createDocument = async (params: {
|
|
62
|
+
content: string;
|
|
63
|
+
fileType: string;
|
|
64
|
+
source: string;
|
|
65
|
+
sourceType: 'api' | 'file' | 'web';
|
|
66
|
+
title: string;
|
|
67
|
+
totalCharCount: number;
|
|
68
|
+
totalLineCount: number;
|
|
69
|
+
}): Promise<DocumentServiceResult> => {
|
|
70
|
+
const doc = await this.documentModel.create(params);
|
|
71
|
+
return toServiceResult(doc);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
deleteDocument = async (id: string): Promise<void> => {
|
|
75
|
+
await this.topicDocumentModel.deleteByDocumentId(id);
|
|
76
|
+
await this.documentModel.delete(id);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
getDocument = async (id: string): Promise<DocumentServiceResult | undefined> => {
|
|
80
|
+
const doc = await this.documentModel.findById(id);
|
|
81
|
+
if (!doc) return undefined;
|
|
82
|
+
return toServiceResult(doc);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
getDocumentsByTopicId = async (
|
|
86
|
+
topicId: string,
|
|
87
|
+
filter?: { type?: string },
|
|
88
|
+
): Promise<DocumentServiceResult[]> => {
|
|
89
|
+
const docs = await this.topicDocumentModel.findByTopicId(topicId, filter);
|
|
90
|
+
return docs.map(toServiceResult);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
updateDocument = async (
|
|
94
|
+
id: string,
|
|
95
|
+
params: { content?: string; title?: string },
|
|
96
|
+
): Promise<DocumentServiceResult> => {
|
|
97
|
+
await this.documentModel.update(id, {
|
|
98
|
+
...(params.content !== undefined && {
|
|
99
|
+
content: params.content,
|
|
100
|
+
totalCharCount: params.content.length,
|
|
101
|
+
totalLineCount: params.content.split('\n').length,
|
|
102
|
+
}),
|
|
103
|
+
...(params.title !== undefined && { title: params.title }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const doc = await this.documentModel.findById(id);
|
|
107
|
+
if (!doc) throw new Error(`Document not found after update: ${id}`);
|
|
108
|
+
return toServiceResult(doc);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -65,7 +65,7 @@ export class BuiltinToolsExecutor implements IToolExecutor {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
|
-
return await runtime[apiName](args);
|
|
68
|
+
return await runtime[apiName](args, context);
|
|
69
69
|
} catch (e) {
|
|
70
70
|
const error = e as Error;
|
|
71
71
|
console.error('Error executing builtin tool %s:%s: %O', identifier, apiName, error);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { notebookRuntime } from '../notebook';
|
|
4
|
+
|
|
5
|
+
vi.mock('@/database/models/document');
|
|
6
|
+
vi.mock('@/database/models/topicDocument');
|
|
7
|
+
|
|
8
|
+
describe('notebookRuntime', () => {
|
|
9
|
+
it('should have correct identifier', () => {
|
|
10
|
+
expect(notebookRuntime.identifier).toBe('lobe-notebook');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should create runtime from factory with valid context', () => {
|
|
14
|
+
const context = {
|
|
15
|
+
serverDB: {} as any,
|
|
16
|
+
toolManifestMap: {},
|
|
17
|
+
topicId: 'topic-1',
|
|
18
|
+
userId: 'user-1',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const runtime = notebookRuntime.factory(context);
|
|
22
|
+
|
|
23
|
+
expect(runtime).toBeDefined();
|
|
24
|
+
expect(typeof runtime.createDocument).toBe('function');
|
|
25
|
+
expect(typeof runtime.updateDocument).toBe('function');
|
|
26
|
+
expect(typeof runtime.getDocument).toBe('function');
|
|
27
|
+
expect(typeof runtime.deleteDocument).toBe('function');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should throw if userId is missing', () => {
|
|
31
|
+
const context = {
|
|
32
|
+
serverDB: {} as any,
|
|
33
|
+
toolManifestMap: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(() => notebookRuntime.factory(context)).toThrow(
|
|
37
|
+
'userId and serverDB are required for Notebook execution',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw if serverDB is missing', () => {
|
|
42
|
+
const context = {
|
|
43
|
+
toolManifestMap: {},
|
|
44
|
+
userId: 'user-1',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
expect(() => notebookRuntime.factory(context)).toThrow(
|
|
48
|
+
'userId and serverDB are required for Notebook execution',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type ToolExecutionContext } from '../types';
|
|
10
10
|
import { cloudSandboxRuntime } from './cloudSandbox';
|
|
11
|
+
import { notebookRuntime } from './notebook';
|
|
11
12
|
import { type ServerRuntimeFactory, type ServerRuntimeRegistration } from './types';
|
|
12
13
|
import { webBrowsingRuntime } from './webBrowsing';
|
|
13
14
|
|
|
@@ -26,7 +27,7 @@ const registerRuntimes = (runtimes: ServerRuntimeRegistration[]) => {
|
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// Register all server runtimes
|
|
29
|
-
registerRuntimes([webBrowsingRuntime, cloudSandboxRuntime]);
|
|
30
|
+
registerRuntimes([webBrowsingRuntime, cloudSandboxRuntime, notebookRuntime]);
|
|
30
31
|
|
|
31
32
|
// ==================== Registry API ====================
|
|
32
33
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NotebookIdentifier } from '@lobechat/builtin-tool-notebook';
|
|
2
|
+
import { NotebookExecutionRuntime } from '@lobechat/builtin-tool-notebook/executionRuntime';
|
|
3
|
+
|
|
4
|
+
import { NotebookRuntimeService } from '@/server/services/notebook';
|
|
5
|
+
|
|
6
|
+
import { type ServerRuntimeRegistration } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Notebook Server Runtime
|
|
10
|
+
* Per-request runtime (needs serverDB, userId, topicId)
|
|
11
|
+
*/
|
|
12
|
+
export const notebookRuntime: ServerRuntimeRegistration = {
|
|
13
|
+
factory: (context) => {
|
|
14
|
+
if (!context.userId || !context.serverDB) {
|
|
15
|
+
throw new Error('userId and serverDB are required for Notebook execution');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const notebookService = new NotebookRuntimeService({
|
|
19
|
+
serverDB: context.serverDB,
|
|
20
|
+
userId: context.userId,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return new NotebookExecutionRuntime(notebookService);
|
|
24
|
+
},
|
|
25
|
+
identifier: NotebookIdentifier,
|
|
26
|
+
};
|
|
@@ -201,7 +201,7 @@ export const messagePublicApi: StateCreator<
|
|
|
201
201
|
|
|
202
202
|
get().internal_dispatchMessage({ type: 'deleteMessages', ids });
|
|
203
203
|
const ctx = get().internal_getConversationContext();
|
|
204
|
-
// CRUD operations pass agentId - backend handles sessionId mapping
|
|
204
|
+
// CRUD operations pass agentId - backend handles sessionId mapping
|
|
205
205
|
const result = await messageService.removeMessages(ids, ctx);
|
|
206
206
|
|
|
207
207
|
if (result?.success && result.messages) {
|
|
@@ -76,7 +76,7 @@ export interface OperationActions {
|
|
|
76
76
|
*
|
|
77
77
|
* Returns full MessageMapKeyInput for consistent key generation
|
|
78
78
|
*
|
|
79
|
-
* Migration Note
|
|
79
|
+
* Migration Note:
|
|
80
80
|
* - Only agentId is used for message association
|
|
81
81
|
* - Backend handles sessionId mapping internally based on agentId
|
|
82
82
|
*/
|
|
@@ -12,9 +12,9 @@ import { gtdExecutor } from '@lobechat/builtin-tool-gtd/executor';
|
|
|
12
12
|
import { knowledgeBaseExecutor } from '@lobechat/builtin-tool-knowledge-base/executor';
|
|
13
13
|
import { localSystemExecutor } from '@lobechat/builtin-tool-local-system/executor';
|
|
14
14
|
import { memoryExecutor } from '@lobechat/builtin-tool-memory/executor';
|
|
15
|
-
import { notebookExecutor } from '@lobechat/builtin-tool-notebook/executor';
|
|
16
15
|
|
|
17
16
|
import type { IBuiltinToolExecutor } from '../types';
|
|
17
|
+
import { notebookExecutor } from './lobe-notebook';
|
|
18
18
|
import { pageAgentExecutor } from './lobe-page-agent';
|
|
19
19
|
import { webBrowsing } from './lobe-web-browsing';
|
|
20
20
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lobe Notebook Executor
|
|
3
|
+
*
|
|
4
|
+
* Creates and exports the NotebookExecutor instance for registration.
|
|
5
|
+
* Injects notebookService as dependency.
|
|
6
|
+
*/
|
|
7
|
+
import { NotebookExecutor } from '@lobechat/builtin-tool-notebook/executor';
|
|
8
|
+
|
|
9
|
+
import { notebookService } from '@/services/notebook';
|
|
10
|
+
|
|
11
|
+
// Create executor instance with client-side service
|
|
12
|
+
export const notebookExecutor = new NotebookExecutor(notebookService);
|