mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: register Notebook tool in server runtime (#12203)
* refactor Notebook Executor * 🐛 fix: register Notebook tool in server runtime Notebook tool (lobe-notebook) was only registered on the client side, causing server-side tool calls to fail with "not implemented" error. - Add NotebookRuntimeService wrapping DocumentModel/TopicDocumentModel - Add notebook server runtime registration - Pass context to runtime methods for topicId passthrough - Add tests for NotebookRuntimeService and runtime registration Resolves LOBE-4880 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,56 @@
|
||||
/**
|
||||
* Lobe Notebook Executor
|
||||
*
|
||||
* Handles notebook document operations via tRPC API calls.
|
||||
* All operations are delegated to the server since they require database access.
|
||||
* Handles notebook document operations.
|
||||
* The NotebookService is injected via constructor so both client and server can provide their own implementation.
|
||||
*
|
||||
* Note: listDocuments is not exposed as a tool - it's automatically injected by the system.
|
||||
*/
|
||||
import { BaseExecutor, type BuiltinToolContext, type BuiltinToolResult } from '@lobechat/types';
|
||||
|
||||
import { notebookService } from '@/services/notebook';
|
||||
|
||||
import {
|
||||
type CreateDocumentArgs,
|
||||
type DeleteDocumentArgs,
|
||||
type DocumentType,
|
||||
type GetDocumentArgs,
|
||||
NotebookApiName,
|
||||
NotebookIdentifier,
|
||||
type UpdateDocumentArgs,
|
||||
} from '../types';
|
||||
|
||||
class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
interface CreateDocumentParams {
|
||||
content: string;
|
||||
description: string;
|
||||
title: string;
|
||||
topicId: string;
|
||||
type?: DocumentType;
|
||||
}
|
||||
|
||||
interface UpdateDocumentParams {
|
||||
append?: boolean;
|
||||
content?: string;
|
||||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface NotebookServiceApi {
|
||||
createDocument: (params: CreateDocumentParams) => Promise<any>;
|
||||
deleteDocument: (id: string) => Promise<any>;
|
||||
getDocument: (id: string) => Promise<any>;
|
||||
updateDocument: (params: UpdateDocumentParams) => Promise<any>;
|
||||
}
|
||||
|
||||
export class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
readonly identifier = NotebookIdentifier;
|
||||
protected readonly apiEnum = NotebookApiName;
|
||||
|
||||
private notebookService: NotebookServiceApi;
|
||||
|
||||
constructor(notebookService: NotebookServiceApi) {
|
||||
super();
|
||||
this.notebookService = notebookService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new document
|
||||
*/
|
||||
@@ -42,7 +70,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
};
|
||||
}
|
||||
|
||||
const document = await notebookService.createDocument({
|
||||
const document = await this.notebookService.createDocument({
|
||||
content: params.content,
|
||||
description: params.description,
|
||||
title: params.title,
|
||||
@@ -80,7 +108,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
return { stop: true, success: false };
|
||||
}
|
||||
|
||||
const document = await notebookService.updateDocument(params);
|
||||
const document = await this.notebookService.updateDocument(params);
|
||||
|
||||
return {
|
||||
content: `✏️ Document updated successfully`,
|
||||
@@ -112,7 +140,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
return { stop: true, success: false };
|
||||
}
|
||||
|
||||
const document = await notebookService.getDocument(params.id);
|
||||
const document = await this.notebookService.getDocument(params.id);
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
@@ -151,7 +179,7 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
return { stop: true, success: false };
|
||||
}
|
||||
|
||||
await notebookService.deleteDocument(params.id);
|
||||
await this.notebookService.deleteDocument(params.id);
|
||||
|
||||
return {
|
||||
content: `🗑️ Document deleted successfully`,
|
||||
@@ -170,6 +198,3 @@ class NotebookExecutor extends BaseExecutor<typeof NotebookApiName> {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Export the executor instance for registration
|
||||
export const notebookExecutor = new NotebookExecutor();
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
* Image component wrapper for Next.js Image.
|
||||
* This module provides a unified interface that can be easily replaced
|
||||
* with a generic <img> or custom image component in the future.
|
||||
*
|
||||
* @see Phase 3.4: LOBE-2991
|
||||
*/
|
||||
|
||||
// Re-export the Image component
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
* Link component wrapper for Next.js Link.
|
||||
* This module provides a unified interface that can be easily replaced
|
||||
* with react-router-dom Link in the future.
|
||||
*
|
||||
* @see Phase 3.2: LOBE-2989
|
||||
*/
|
||||
|
||||
// Re-export the Link component
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* This module provides a unified interface that can be easily replaced
|
||||
* with React.lazy + Suspense in the future.
|
||||
*
|
||||
* @see Phase 3.3: LOBE-2990
|
||||
* @see Phase 3.3
|
||||
*/
|
||||
|
||||
// Re-export the dynamic function
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - import dynamic from '@/libs/next/dynamic';
|
||||
* - import Image from '@/libs/next/Image';
|
||||
*
|
||||
* @see RFC 147: LOBE-2850 - Phase 3
|
||||
* @see RFC 147
|
||||
*/
|
||||
|
||||
// Navigation exports
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* This module provides a unified interface that can be easily replaced
|
||||
* with react-router-dom in the future.
|
||||
*
|
||||
* @see Phase 3.1: LOBE-2988
|
||||
* @see Phase 3.1
|
||||
*/
|
||||
|
||||
// Re-export all navigation hooks and utilities from Next.js
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
* React Router Link component wrapper.
|
||||
* Provides a Next.js-like API (href prop) while using React Router internally.
|
||||
*
|
||||
* @see RFC 147: LOBE-2850 - Phase 3
|
||||
* @see RFC 147
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Link as ReactRouterLink, type LinkProps as ReactRouterLinkProps } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - import { useRouter, usePathname, useSearchParams } from '@/libs/router';
|
||||
* - import Link from '@/libs/router/Link';
|
||||
*
|
||||
* @see RFC 147: LOBE-2850 - Phase 3
|
||||
* @see RFC 147
|
||||
*/
|
||||
|
||||
// Navigation exports
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* Usage:
|
||||
* - import { useRouter, usePathname, useSearchParams, useQuery } from '@/libs/router/navigation';
|
||||
*
|
||||
* @see RFC 147: LOBE-2850 - Phase 3
|
||||
* @see RFC 147
|
||||
*/
|
||||
|
||||
import qs from 'query-string';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
@@ -33,7 +32,7 @@ export const useRouter = () => {
|
||||
push: (href: string) => navigate(href),
|
||||
replace: (href: string) => navigate(href, { replace: true }),
|
||||
}),
|
||||
[navigate]
|
||||
[navigate],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,7 +55,9 @@ export const useSearchParams = () => {
|
||||
/**
|
||||
* Hook to get route params.
|
||||
*/
|
||||
export const useParams = <T extends Record<string, string | undefined> = Record<string, string | undefined>>() => {
|
||||
export const useParams = <
|
||||
T extends Record<string, string | undefined> = Record<string, string | undefined>,
|
||||
>() => {
|
||||
return useReactRouterParams<T>();
|
||||
};
|
||||
|
||||
|
||||
218
src/server/services/notebook/__tests__/index.test.ts
Normal file
218
src/server/services/notebook/__tests__/index.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DocumentModel } from '@/database/models/document';
|
||||
import { TopicDocumentModel } from '@/database/models/topicDocument';
|
||||
|
||||
import { NotebookRuntimeService } from '../index';
|
||||
|
||||
vi.mock('@/database/models/document');
|
||||
vi.mock('@/database/models/topicDocument');
|
||||
|
||||
describe('NotebookRuntimeService', () => {
|
||||
let service: NotebookRuntimeService;
|
||||
const mockDb = {} as any;
|
||||
const mockUserId = 'test-user';
|
||||
let mockDocumentModel: any;
|
||||
let mockTopicDocumentModel: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockDocumentModel = {
|
||||
create: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
|
||||
mockTopicDocumentModel = {
|
||||
associate: vi.fn(),
|
||||
deleteByDocumentId: vi.fn(),
|
||||
findByTopicId: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mocked(DocumentModel).mockImplementation(() => mockDocumentModel);
|
||||
vi.mocked(TopicDocumentModel).mockImplementation(() => mockTopicDocumentModel);
|
||||
|
||||
service = new NotebookRuntimeService({ serverDB: mockDb, userId: mockUserId });
|
||||
});
|
||||
|
||||
const mockDocument = {
|
||||
content: '# Hello',
|
||||
createdAt: new Date('2025-01-01'),
|
||||
description: 'A test doc',
|
||||
fileType: 'markdown',
|
||||
id: 'doc-1',
|
||||
source: 'notebook:topic-1',
|
||||
sourceType: 'api' as const,
|
||||
title: 'Test Doc',
|
||||
totalCharCount: 7,
|
||||
totalLineCount: 1,
|
||||
updatedAt: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
describe('createDocument', () => {
|
||||
it('should create a document and return service result', async () => {
|
||||
mockDocumentModel.create.mockResolvedValue(mockDocument);
|
||||
|
||||
const params = {
|
||||
content: '# Hello',
|
||||
fileType: 'markdown',
|
||||
source: 'notebook:topic-1',
|
||||
sourceType: 'api' as const,
|
||||
title: 'Test Doc',
|
||||
totalCharCount: 7,
|
||||
totalLineCount: 1,
|
||||
};
|
||||
|
||||
const result = await service.createDocument(params);
|
||||
|
||||
expect(mockDocumentModel.create).toHaveBeenCalledWith(params);
|
||||
expect(result).toEqual({
|
||||
content: '# Hello',
|
||||
createdAt: mockDocument.createdAt,
|
||||
description: 'A test doc',
|
||||
fileType: 'markdown',
|
||||
id: 'doc-1',
|
||||
source: 'notebook:topic-1',
|
||||
sourceType: 'api',
|
||||
title: 'Test Doc',
|
||||
totalCharCount: 7,
|
||||
updatedAt: mockDocument.updatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert topic sourceType to api', async () => {
|
||||
mockDocumentModel.create.mockResolvedValue({
|
||||
...mockDocument,
|
||||
sourceType: 'topic',
|
||||
});
|
||||
|
||||
const result = await service.createDocument({
|
||||
content: 'test',
|
||||
fileType: 'markdown',
|
||||
source: 'test',
|
||||
sourceType: 'api',
|
||||
title: 'test',
|
||||
totalCharCount: 4,
|
||||
totalLineCount: 1,
|
||||
});
|
||||
|
||||
expect(result.sourceType).toBe('api');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocument', () => {
|
||||
it('should return document when found', async () => {
|
||||
mockDocumentModel.findById.mockResolvedValue(mockDocument);
|
||||
|
||||
const result = await service.getDocument('doc-1');
|
||||
|
||||
expect(mockDocumentModel.findById).toHaveBeenCalledWith('doc-1');
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.id).toBe('doc-1');
|
||||
});
|
||||
|
||||
it('should return undefined when not found', async () => {
|
||||
mockDocumentModel.findById.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getDocument('nonexistent');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDocument', () => {
|
||||
it('should update content and recalculate stats', async () => {
|
||||
const newContent = 'line1\nline2\nline3';
|
||||
mockDocumentModel.update.mockResolvedValue(undefined);
|
||||
mockDocumentModel.findById.mockResolvedValue({
|
||||
...mockDocument,
|
||||
content: newContent,
|
||||
totalCharCount: newContent.length,
|
||||
totalLineCount: 3,
|
||||
});
|
||||
|
||||
const result = await service.updateDocument('doc-1', { content: newContent });
|
||||
|
||||
expect(mockDocumentModel.update).toHaveBeenCalledWith('doc-1', {
|
||||
content: newContent,
|
||||
totalCharCount: newContent.length,
|
||||
totalLineCount: 3,
|
||||
});
|
||||
expect(result.content).toBe(newContent);
|
||||
});
|
||||
|
||||
it('should update title only', async () => {
|
||||
mockDocumentModel.update.mockResolvedValue(undefined);
|
||||
mockDocumentModel.findById.mockResolvedValue({
|
||||
...mockDocument,
|
||||
title: 'New Title',
|
||||
});
|
||||
|
||||
const result = await service.updateDocument('doc-1', { title: 'New Title' });
|
||||
|
||||
expect(mockDocumentModel.update).toHaveBeenCalledWith('doc-1', { title: 'New Title' });
|
||||
expect(result.title).toBe('New Title');
|
||||
});
|
||||
|
||||
it('should throw if document not found after update', async () => {
|
||||
mockDocumentModel.update.mockResolvedValue(undefined);
|
||||
mockDocumentModel.findById.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.updateDocument('doc-1', { title: 'x' })).rejects.toThrow(
|
||||
'Document not found after update: doc-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDocument', () => {
|
||||
it('should delete associations first then the document', async () => {
|
||||
mockTopicDocumentModel.deleteByDocumentId.mockResolvedValue(undefined);
|
||||
mockDocumentModel.delete.mockResolvedValue(undefined);
|
||||
|
||||
await service.deleteDocument('doc-1');
|
||||
|
||||
expect(mockTopicDocumentModel.deleteByDocumentId).toHaveBeenCalledWith('doc-1');
|
||||
expect(mockDocumentModel.delete).toHaveBeenCalledWith('doc-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('associateDocumentWithTopic', () => {
|
||||
it('should associate document with topic', async () => {
|
||||
mockTopicDocumentModel.associate.mockResolvedValue({
|
||||
documentId: 'doc-1',
|
||||
topicId: 'topic-1',
|
||||
});
|
||||
|
||||
await service.associateDocumentWithTopic('doc-1', 'topic-1');
|
||||
|
||||
expect(mockTopicDocumentModel.associate).toHaveBeenCalledWith({
|
||||
documentId: 'doc-1',
|
||||
topicId: 'topic-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocumentsByTopicId', () => {
|
||||
it('should return documents for a topic', async () => {
|
||||
mockTopicDocumentModel.findByTopicId.mockResolvedValue([mockDocument]);
|
||||
|
||||
const result = await service.getDocumentsByTopicId('topic-1');
|
||||
|
||||
expect(mockTopicDocumentModel.findByTopicId).toHaveBeenCalledWith('topic-1', undefined);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('doc-1');
|
||||
});
|
||||
|
||||
it('should pass filter to findByTopicId', async () => {
|
||||
mockTopicDocumentModel.findByTopicId.mockResolvedValue([]);
|
||||
|
||||
await service.getDocumentsByTopicId('topic-1', { type: 'markdown' });
|
||||
|
||||
expect(mockTopicDocumentModel.findByTopicId).toHaveBeenCalledWith('topic-1', {
|
||||
type: 'markdown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
110
src/server/services/notebook/index.ts
Normal file
110
src/server/services/notebook/index.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { type LobeChatDatabase } from '@lobechat/database';
|
||||
|
||||
import { DocumentModel } from '@/database/models/document';
|
||||
import { TopicDocumentModel } from '@/database/models/topicDocument';
|
||||
|
||||
interface DocumentServiceResult {
|
||||
content: string | null;
|
||||
createdAt: Date;
|
||||
description: string | null;
|
||||
fileType: string;
|
||||
id: string;
|
||||
source: string;
|
||||
sourceType: 'api' | 'file' | 'web';
|
||||
title: string | null;
|
||||
totalCharCount: number;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface NotebookRuntimeServiceOptions {
|
||||
serverDB: LobeChatDatabase;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const toServiceResult = (doc: {
|
||||
content: string | null;
|
||||
createdAt: Date;
|
||||
description: string | null;
|
||||
fileType: string;
|
||||
id: string;
|
||||
source: string;
|
||||
sourceType: 'api' | 'file' | 'web' | 'topic';
|
||||
title: string | null;
|
||||
totalCharCount: number;
|
||||
updatedAt: Date;
|
||||
}): DocumentServiceResult => ({
|
||||
content: doc.content,
|
||||
createdAt: doc.createdAt,
|
||||
description: doc.description,
|
||||
fileType: doc.fileType,
|
||||
id: doc.id,
|
||||
source: doc.source,
|
||||
sourceType: doc.sourceType === 'topic' ? 'api' : doc.sourceType,
|
||||
title: doc.title,
|
||||
totalCharCount: doc.totalCharCount,
|
||||
updatedAt: doc.updatedAt,
|
||||
});
|
||||
|
||||
export class NotebookRuntimeService {
|
||||
private documentModel: DocumentModel;
|
||||
private topicDocumentModel: TopicDocumentModel;
|
||||
|
||||
constructor(options: NotebookRuntimeServiceOptions) {
|
||||
this.documentModel = new DocumentModel(options.serverDB, options.userId);
|
||||
this.topicDocumentModel = new TopicDocumentModel(options.serverDB, options.userId);
|
||||
}
|
||||
|
||||
associateDocumentWithTopic = async (documentId: string, topicId: string): Promise<void> => {
|
||||
await this.topicDocumentModel.associate({ documentId, topicId });
|
||||
};
|
||||
|
||||
createDocument = async (params: {
|
||||
content: string;
|
||||
fileType: string;
|
||||
source: string;
|
||||
sourceType: 'api' | 'file' | 'web';
|
||||
title: string;
|
||||
totalCharCount: number;
|
||||
totalLineCount: number;
|
||||
}): Promise<DocumentServiceResult> => {
|
||||
const doc = await this.documentModel.create(params);
|
||||
return toServiceResult(doc);
|
||||
};
|
||||
|
||||
deleteDocument = async (id: string): Promise<void> => {
|
||||
await this.topicDocumentModel.deleteByDocumentId(id);
|
||||
await this.documentModel.delete(id);
|
||||
};
|
||||
|
||||
getDocument = async (id: string): Promise<DocumentServiceResult | undefined> => {
|
||||
const doc = await this.documentModel.findById(id);
|
||||
if (!doc) return undefined;
|
||||
return toServiceResult(doc);
|
||||
};
|
||||
|
||||
getDocumentsByTopicId = async (
|
||||
topicId: string,
|
||||
filter?: { type?: string },
|
||||
): Promise<DocumentServiceResult[]> => {
|
||||
const docs = await this.topicDocumentModel.findByTopicId(topicId, filter);
|
||||
return docs.map(toServiceResult);
|
||||
};
|
||||
|
||||
updateDocument = async (
|
||||
id: string,
|
||||
params: { content?: string; title?: string },
|
||||
): Promise<DocumentServiceResult> => {
|
||||
await this.documentModel.update(id, {
|
||||
...(params.content !== undefined && {
|
||||
content: params.content,
|
||||
totalCharCount: params.content.length,
|
||||
totalLineCount: params.content.split('\n').length,
|
||||
}),
|
||||
...(params.title !== undefined && { title: params.title }),
|
||||
});
|
||||
|
||||
const doc = await this.documentModel.findById(id);
|
||||
if (!doc) throw new Error(`Document not found after update: ${id}`);
|
||||
return toServiceResult(doc);
|
||||
};
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class BuiltinToolsExecutor implements IToolExecutor {
|
||||
}
|
||||
|
||||
try {
|
||||
return await runtime[apiName](args);
|
||||
return await runtime[apiName](args, context);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
console.error('Error executing builtin tool %s:%s: %O', identifier, apiName, error);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { notebookRuntime } from '../notebook';
|
||||
|
||||
vi.mock('@/database/models/document');
|
||||
vi.mock('@/database/models/topicDocument');
|
||||
|
||||
describe('notebookRuntime', () => {
|
||||
it('should have correct identifier', () => {
|
||||
expect(notebookRuntime.identifier).toBe('lobe-notebook');
|
||||
});
|
||||
|
||||
it('should create runtime from factory with valid context', () => {
|
||||
const context = {
|
||||
serverDB: {} as any,
|
||||
toolManifestMap: {},
|
||||
topicId: 'topic-1',
|
||||
userId: 'user-1',
|
||||
};
|
||||
|
||||
const runtime = notebookRuntime.factory(context);
|
||||
|
||||
expect(runtime).toBeDefined();
|
||||
expect(typeof runtime.createDocument).toBe('function');
|
||||
expect(typeof runtime.updateDocument).toBe('function');
|
||||
expect(typeof runtime.getDocument).toBe('function');
|
||||
expect(typeof runtime.deleteDocument).toBe('function');
|
||||
});
|
||||
|
||||
it('should throw if userId is missing', () => {
|
||||
const context = {
|
||||
serverDB: {} as any,
|
||||
toolManifestMap: {},
|
||||
};
|
||||
|
||||
expect(() => notebookRuntime.factory(context)).toThrow(
|
||||
'userId and serverDB are required for Notebook execution',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if serverDB is missing', () => {
|
||||
const context = {
|
||||
toolManifestMap: {},
|
||||
userId: 'user-1',
|
||||
};
|
||||
|
||||
expect(() => notebookRuntime.factory(context)).toThrow(
|
||||
'userId and serverDB are required for Notebook execution',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
import { type ToolExecutionContext } from '../types';
|
||||
import { cloudSandboxRuntime } from './cloudSandbox';
|
||||
import { notebookRuntime } from './notebook';
|
||||
import { type ServerRuntimeFactory, type ServerRuntimeRegistration } from './types';
|
||||
import { webBrowsingRuntime } from './webBrowsing';
|
||||
|
||||
@@ -26,7 +27,7 @@ const registerRuntimes = (runtimes: ServerRuntimeRegistration[]) => {
|
||||
};
|
||||
|
||||
// Register all server runtimes
|
||||
registerRuntimes([webBrowsingRuntime, cloudSandboxRuntime]);
|
||||
registerRuntimes([webBrowsingRuntime, cloudSandboxRuntime, notebookRuntime]);
|
||||
|
||||
// ==================== Registry API ====================
|
||||
|
||||
|
||||
26
src/server/services/toolExecution/serverRuntimes/notebook.ts
Normal file
26
src/server/services/toolExecution/serverRuntimes/notebook.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NotebookIdentifier } from '@lobechat/builtin-tool-notebook';
|
||||
import { NotebookExecutionRuntime } from '@lobechat/builtin-tool-notebook/executionRuntime';
|
||||
|
||||
import { NotebookRuntimeService } from '@/server/services/notebook';
|
||||
|
||||
import { type ServerRuntimeRegistration } from './types';
|
||||
|
||||
/**
|
||||
* Notebook Server Runtime
|
||||
* Per-request runtime (needs serverDB, userId, topicId)
|
||||
*/
|
||||
export const notebookRuntime: ServerRuntimeRegistration = {
|
||||
factory: (context) => {
|
||||
if (!context.userId || !context.serverDB) {
|
||||
throw new Error('userId and serverDB are required for Notebook execution');
|
||||
}
|
||||
|
||||
const notebookService = new NotebookRuntimeService({
|
||||
serverDB: context.serverDB,
|
||||
userId: context.userId,
|
||||
});
|
||||
|
||||
return new NotebookExecutionRuntime(notebookService);
|
||||
},
|
||||
identifier: NotebookIdentifier,
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export const messagePublicApi: StateCreator<
|
||||
|
||||
get().internal_dispatchMessage({ type: 'deleteMessages', ids });
|
||||
const ctx = get().internal_getConversationContext();
|
||||
// CRUD operations pass agentId - backend handles sessionId mapping (LOBE-1086)
|
||||
// CRUD operations pass agentId - backend handles sessionId mapping
|
||||
const result = await messageService.removeMessages(ids, ctx);
|
||||
|
||||
if (result?.success && result.messages) {
|
||||
|
||||
@@ -76,7 +76,7 @@ export interface OperationActions {
|
||||
*
|
||||
* Returns full MessageMapKeyInput for consistent key generation
|
||||
*
|
||||
* Migration Note (LOBE-1086):
|
||||
* Migration Note:
|
||||
* - Only agentId is used for message association
|
||||
* - Backend handles sessionId mapping internally based on agentId
|
||||
*/
|
||||
|
||||
@@ -12,9 +12,9 @@ import { gtdExecutor } from '@lobechat/builtin-tool-gtd/executor';
|
||||
import { knowledgeBaseExecutor } from '@lobechat/builtin-tool-knowledge-base/executor';
|
||||
import { localSystemExecutor } from '@lobechat/builtin-tool-local-system/executor';
|
||||
import { memoryExecutor } from '@lobechat/builtin-tool-memory/executor';
|
||||
import { notebookExecutor } from '@lobechat/builtin-tool-notebook/executor';
|
||||
|
||||
import type { IBuiltinToolExecutor } from '../types';
|
||||
import { notebookExecutor } from './lobe-notebook';
|
||||
import { pageAgentExecutor } from './lobe-page-agent';
|
||||
import { webBrowsing } from './lobe-web-browsing';
|
||||
|
||||
|
||||
12
src/store/tool/slices/builtin/executors/lobe-notebook.ts
Normal file
12
src/store/tool/slices/builtin/executors/lobe-notebook.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Lobe Notebook Executor
|
||||
*
|
||||
* Creates and exports the NotebookExecutor instance for registration.
|
||||
* Injects notebookService as dependency.
|
||||
*/
|
||||
import { NotebookExecutor } from '@lobechat/builtin-tool-notebook/executor';
|
||||
|
||||
import { notebookService } from '@/services/notebook';
|
||||
|
||||
// Create executor instance with client-side service
|
||||
export const notebookExecutor = new NotebookExecutor(notebookService);
|
||||
Reference in New Issue
Block a user