@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 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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Register Notebook tool in server runtime."
6
+ ]
7
+ },
8
+ "date": "2026-02-08",
9
+ "version": "2.1.22"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.1.21",
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 via tRPC API calls.
5
- * All operations are delegated to the server since they require database access.
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
- class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
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('请检查 API Key 余额是否充足');
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
- '请检查 API Key 余额是否充足,或者是否在用未实名的 API Key 访问需要实名的模型。',
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
- // 只有部分模型支持指定 enable_thinking,其余一些慢思考模型只支持调节 thinking budget
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 GLM-4.5V(不包含 GLM-4.5 Air
63
- /Qwen3-(?:\d+B|\d+B-A\d+B)$/, // Qwen3-8BQwen3-14BQwen3-32BQwen3-30B-A3BQwen3-235B-A22B
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
  ];
@@ -2,8 +2,6 @@
2
2
  * Image component wrapper for Next.js Image.
3
3
  * This module provides a unified interface that can be easily replaced
4
4
  * with a generic <img> or custom image component in the future.
5
- *
6
- * @see Phase 3.4: LOBE-2991
7
5
  */
8
6
 
9
7
  // Re-export the Image component
@@ -2,8 +2,6 @@
2
2
  * Link component wrapper for Next.js Link.
3
3
  * This module provides a unified interface that can be easily replaced
4
4
  * with react-router-dom Link in the future.
5
- *
6
- * @see Phase 3.2: LOBE-2989
7
5
  */
8
6
 
9
7
  // Re-export the Link component
@@ -3,7 +3,7 @@
3
3
  * This module provides a unified interface that can be easily replaced
4
4
  * with React.lazy + Suspense in the future.
5
5
  *
6
- * @see Phase 3.3: LOBE-2990
6
+ * @see Phase 3.3
7
7
  */
8
8
 
9
9
  // Re-export the dynamic function
@@ -10,7 +10,7 @@
10
10
  * - import dynamic from '@/libs/next/dynamic';
11
11
  * - import Image from '@/libs/next/Image';
12
12
  *
13
- * @see RFC 147: LOBE-2850 - Phase 3
13
+ * @see RFC 147
14
14
  */
15
15
 
16
16
  // Navigation exports
@@ -3,7 +3,7 @@
3
3
  * This module provides a unified interface that can be easily replaced
4
4
  * with react-router-dom in the future.
5
5
  *
6
- * @see Phase 3.1: LOBE-2988
6
+ * @see Phase 3.1
7
7
  */
8
8
 
9
9
  // Re-export all navigation hooks and utilities from Next.js
@@ -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: LOBE-2850 - Phase 3
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
 
@@ -8,7 +8,7 @@
8
8
  * - import { useRouter, usePathname, useSearchParams } from '@/libs/router';
9
9
  * - import Link from '@/libs/router/Link';
10
10
  *
11
- * @see RFC 147: LOBE-2850 - Phase 3
11
+ * @see RFC 147
12
12
  */
13
13
 
14
14
  // Navigation exports
@@ -5,9 +5,8 @@
5
5
  * Usage:
6
6
  * - import { useRouter, usePathname, useSearchParams, useQuery } from '@/libs/router/navigation';
7
7
  *
8
- * @see RFC 147: LOBE-2850 - Phase 3
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 = <T extends Record<string, string | undefined> = Record<string, string | undefined>>() => {
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 (LOBE-1086)
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 (LOBE-1086):
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);