@lobehub/lobehub 2.0.0-next.239 → 2.0.0-next.240

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.
Files changed (69) hide show
  1. package/.cursor/rules/typescript.mdc +1 -0
  2. package/CHANGELOG.md +33 -0
  3. package/changelog/v1.json +5 -0
  4. package/locales/en-US/plugin.json +9 -0
  5. package/locales/zh-CN/plugin.json +9 -0
  6. package/package.json +1 -1
  7. package/packages/builtin-tool-gtd/src/client/Streaming/CreatePlan/index.tsx +4 -19
  8. package/packages/builtin-tool-notebook/src/client/Inspector/CreateDocument/index.tsx +51 -0
  9. package/packages/builtin-tool-notebook/src/client/Inspector/index.ts +14 -0
  10. package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +101 -0
  11. package/packages/builtin-tool-notebook/src/client/Placeholder/index.ts +10 -0
  12. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +63 -33
  13. package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +75 -0
  14. package/packages/builtin-tool-notebook/src/client/Streaming/index.ts +14 -0
  15. package/packages/builtin-tool-notebook/src/client/components/AnimatedNumber.tsx +57 -0
  16. package/packages/builtin-tool-notebook/src/client/index.ts +12 -0
  17. package/packages/builtin-tool-notebook/src/systemRole.ts +2 -1
  18. package/packages/memory-user-memory/src/extractors/base.ts +8 -13
  19. package/packages/memory-user-memory/src/extractors/context.test.ts +2 -7
  20. package/packages/memory-user-memory/src/extractors/context.ts +7 -2
  21. package/packages/memory-user-memory/src/extractors/experience.test.ts +2 -10
  22. package/packages/memory-user-memory/src/extractors/experience.ts +7 -2
  23. package/packages/memory-user-memory/src/extractors/gatekeeper.test.ts +2 -7
  24. package/packages/memory-user-memory/src/extractors/gatekeeper.ts +3 -2
  25. package/packages/memory-user-memory/src/extractors/identity.test.ts +2 -7
  26. package/packages/memory-user-memory/src/extractors/identity.ts +7 -2
  27. package/packages/memory-user-memory/src/extractors/preference.test.ts +2 -10
  28. package/packages/memory-user-memory/src/extractors/preference.ts +7 -2
  29. package/packages/memory-user-memory/src/prompts/gatekeeper.ts +127 -0
  30. package/packages/memory-user-memory/src/prompts/index.ts +2 -0
  31. package/packages/memory-user-memory/src/prompts/layers/context.ts +155 -0
  32. package/packages/memory-user-memory/src/prompts/layers/experience.ts +162 -0
  33. package/packages/memory-user-memory/src/prompts/layers/identity.ts +219 -0
  34. package/packages/memory-user-memory/src/prompts/layers/index.ts +4 -0
  35. package/packages/memory-user-memory/src/prompts/layers/preference.ts +164 -0
  36. package/packages/memory-user-memory/src/services/extractExecutor.ts +0 -7
  37. package/packages/memory-user-memory/src/types.ts +0 -1
  38. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +2 -2
  39. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +1 -11
  40. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +1 -7
  41. package/src/app/[variants]/(main)/image/index.tsx +2 -5
  42. package/src/components/Loading/BrandTextLoading/index.module.css +0 -1
  43. package/src/components/StreamingMarkdown/index.tsx +88 -0
  44. package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +3 -5
  45. package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +14 -0
  46. package/src/features/PluginDevModal/PluginPreview/EmptyState.tsx +1 -1
  47. package/src/locales/default/plugin.ts +9 -0
  48. package/src/server/routers/async/image.ts +1 -1
  49. package/src/server/routers/lambda/image/index.test.ts +491 -0
  50. package/src/server/routers/lambda/{image.ts → image/index.ts} +57 -41
  51. package/src/server/routers/lambda/{__tests__/image.test.ts → image/utils.test.ts} +1 -21
  52. package/src/server/routers/lambda/image/utils.ts +24 -0
  53. package/src/server/services/file/__tests__/index.test.ts +3 -3
  54. package/src/server/services/file/impls/index.ts +4 -4
  55. package/src/server/services/file/impls/s3.test.ts +57 -39
  56. package/src/server/services/file/impls/s3.ts +29 -21
  57. package/src/server/services/file/impls/type.ts +1 -2
  58. package/src/server/services/file/index.ts +5 -3
  59. package/src/tools/inspectors.ts +2 -0
  60. package/src/tools/placeholders.ts +5 -0
  61. package/src/tools/streamings.ts +2 -0
  62. package/packages/memory-user-memory/src/prompts/gatekeeper.md +0 -125
  63. package/packages/memory-user-memory/src/prompts/layers/context.md +0 -153
  64. package/packages/memory-user-memory/src/prompts/layers/experience.md +0 -160
  65. package/packages/memory-user-memory/src/prompts/layers/identity.md +0 -217
  66. package/packages/memory-user-memory/src/prompts/layers/preference.md +0 -162
  67. package/packages/memory-user-memory/src/utils/path.ts +0 -5
  68. package/src/server/services/file/impls/utils.test.ts +0 -154
  69. package/src/server/services/file/impls/utils.ts +0 -17
@@ -231,12 +231,12 @@ describe('FileService', () => {
231
231
  expect(result).toBe(expectedUrl);
232
232
  });
233
233
 
234
- it('should delegate getKeyFromFullUrl to implementation', () => {
234
+ it('should delegate getKeyFromFullUrl to implementation', async () => {
235
235
  const testUrl = 'https://example.com/path/to/file.jpg';
236
236
  const expectedKey = 'path/to/file.jpg';
237
- vi.mocked(service['impl'].getKeyFromFullUrl).mockReturnValue(expectedKey);
237
+ vi.mocked(service['impl'].getKeyFromFullUrl).mockResolvedValue(expectedKey);
238
238
 
239
- const result = service.getKeyFromFullUrl(testUrl);
239
+ const result = await service.getKeyFromFullUrl(testUrl);
240
240
 
241
241
  expect(service['impl'].getKeyFromFullUrl).toHaveBeenCalledWith(testUrl);
242
242
  expect(result).toBe(expectedKey);
@@ -1,3 +1,5 @@
1
+ import { LobeChatDatabase } from '@lobechat/database';
2
+
1
3
  import { S3StaticFileImpl } from './s3';
2
4
  import { type FileServiceImpl } from './type';
3
5
 
@@ -5,8 +7,6 @@ import { type FileServiceImpl } from './type';
5
7
  * Create file service module
6
8
  * Returns S3 file implementation for cloud storage
7
9
  */
8
- export const createFileServiceModule = (): FileServiceImpl => {
9
- return new S3StaticFileImpl();
10
+ export const createFileServiceModule = (db: LobeChatDatabase): FileServiceImpl => {
11
+ return new S3StaticFileImpl(db);
10
12
  };
11
-
12
- export type { FileServiceImpl } from './type';
@@ -1,5 +1,7 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
+ import { FileModel } from '@/database/models/file';
4
+
3
5
  import { S3StaticFileImpl } from './s3';
4
6
 
5
7
  const config = {
@@ -33,11 +35,14 @@ vi.mock('@/server/modules/S3', () => ({
33
35
  })),
34
36
  }));
35
37
 
38
+ // Mock db
39
+ const mockDb = {} as any;
40
+
36
41
  describe('S3StaticFileImpl', () => {
37
42
  let fileService: S3StaticFileImpl;
38
43
 
39
44
  beforeEach(() => {
40
- fileService = new S3StaticFileImpl();
45
+ fileService = new S3StaticFileImpl(mockDb);
41
46
  });
42
47
 
43
48
  describe('getFullFileUrl', () => {
@@ -74,7 +79,7 @@ describe('S3StaticFileImpl', () => {
74
79
  const fullUrl = 'https://s3.example.com/bucket/path/to/file.jpg?X-Amz-Signature=expired';
75
80
 
76
81
  // Mock getKeyFromFullUrl to return the extracted key
77
- vi.spyOn(fileService, 'getKeyFromFullUrl').mockReturnValue('path/to/file.jpg');
82
+ vi.spyOn(fileService, 'getKeyFromFullUrl').mockResolvedValue('path/to/file.jpg');
78
83
 
79
84
  const result = await fileService.getFullFileUrl(fullUrl);
80
85
 
@@ -86,7 +91,7 @@ describe('S3StaticFileImpl', () => {
86
91
  it('should handle full URL input by extracting key (S3_SET_ACL=true)', async () => {
87
92
  const fullUrl = 'https://s3.example.com/bucket/path/to/file.jpg';
88
93
 
89
- vi.spyOn(fileService, 'getKeyFromFullUrl').mockReturnValue('path/to/file.jpg');
94
+ vi.spyOn(fileService, 'getKeyFromFullUrl').mockResolvedValue('path/to/file.jpg');
90
95
 
91
96
  const result = await fileService.getFullFileUrl(fullUrl);
92
97
 
@@ -108,13 +113,23 @@ describe('S3StaticFileImpl', () => {
108
113
  it('should handle http:// URLs for legacy compatibility', async () => {
109
114
  const httpUrl = 'http://s3.example.com/bucket/path/to/file.jpg';
110
115
 
111
- vi.spyOn(fileService, 'getKeyFromFullUrl').mockReturnValue('path/to/file.jpg');
116
+ vi.spyOn(fileService, 'getKeyFromFullUrl').mockResolvedValue('path/to/file.jpg');
112
117
 
113
118
  const result = await fileService.getFullFileUrl(httpUrl);
114
119
 
115
120
  expect(fileService.getKeyFromFullUrl).toHaveBeenCalledWith(httpUrl);
116
121
  expect(result).toBe('https://example.com/path/to/file.jpg');
117
122
  });
123
+
124
+ it('should throw error when key extraction returns null', async () => {
125
+ const fullUrl = 'https://s3.example.com/f/nonexistent';
126
+
127
+ vi.spyOn(fileService, 'getKeyFromFullUrl').mockResolvedValue(null);
128
+
129
+ await expect(fileService.getFullFileUrl(fullUrl)).rejects.toThrow(
130
+ 'Key not found from url: ' + fullUrl,
131
+ );
132
+ });
118
133
  });
119
134
  });
120
135
 
@@ -179,63 +194,66 @@ describe('S3StaticFileImpl', () => {
179
194
  });
180
195
 
181
196
  describe('getKeyFromFullUrl', () => {
182
- it('当S3_ENABLE_PATH_STYLE为false时应该正确提取key', () => {
183
- config.S3_ENABLE_PATH_STYLE = false;
184
- const url = 'https://example.com/path/to/file.jpg';
197
+ it('should extract fileId from proxy URL and return S3 key from database', async () => {
198
+ const proxyUrl = 'http://localhost:3010/f/abc123';
199
+ const expectedKey = 'ppp/491067/image.jpg';
185
200
 
186
- const result = fileService.getKeyFromFullUrl(url);
201
+ vi.spyOn(FileModel, 'getFileById').mockResolvedValue({ url: expectedKey } as any);
187
202
 
188
- expect(result).toBe('path/to/file.jpg');
189
- config.S3_ENABLE_PATH_STYLE = false; // reset
203
+ const result = await fileService.getKeyFromFullUrl(proxyUrl);
204
+
205
+ expect(FileModel.getFileById).toHaveBeenCalledWith(mockDb, 'abc123');
206
+ expect(result).toBe(expectedKey);
190
207
  });
191
208
 
192
- it('当S3_ENABLE_PATH_STYLE为true时应该正确提取key', () => {
193
- config.S3_ENABLE_PATH_STYLE = true;
194
- const url = 'https://example.com/my-bucket/path/to/file.jpg';
209
+ it('should return null when file is not found in database', async () => {
210
+ const proxyUrl = 'http://localhost:3010/f/nonexistent';
195
211
 
196
- const result = fileService.getKeyFromFullUrl(url);
212
+ vi.spyOn(FileModel, 'getFileById').mockResolvedValue(undefined);
197
213
 
198
- expect(result).toBe('path/to/file.jpg');
199
- config.S3_ENABLE_PATH_STYLE = false; // reset
214
+ const result = await fileService.getKeyFromFullUrl(proxyUrl);
215
+
216
+ expect(FileModel.getFileById).toHaveBeenCalledWith(mockDb, 'nonexistent');
217
+ expect(result).toBeNull();
200
218
  });
201
219
 
202
- it('当S3_ENABLE_PATH_STYLE为true但缺少bucket名称时应该返回pathname', () => {
203
- config.S3_ENABLE_PATH_STYLE = true;
204
- config.S3_BUCKET = '';
205
- const url = 'https://example.com/path/to/file.jpg';
220
+ it('should handle URL with different domain', async () => {
221
+ const proxyUrl = 'https://example.com/f/file456';
222
+ const expectedKey = 'uploads/file.png';
206
223
 
207
- const result = fileService.getKeyFromFullUrl(url);
224
+ vi.spyOn(FileModel, 'getFileById').mockResolvedValue({ url: expectedKey } as any);
208
225
 
209
- expect(result).toBe('path/to/file.jpg');
210
- config.S3_ENABLE_PATH_STYLE = false; // reset
211
- config.S3_BUCKET = 'my-bucket'; // reset
226
+ const result = await fileService.getKeyFromFullUrl(proxyUrl);
227
+
228
+ expect(FileModel.getFileById).toHaveBeenCalledWith(mockDb, 'file456');
229
+ expect(result).toBe(expectedKey);
212
230
  });
213
231
 
214
- it('URL格式不正确时应该返回原始字符串', () => {
215
- const invalidUrl = 'not-a-valid-url';
232
+ it('should extract key from legacy S3 URL (non /f/ path)', async () => {
233
+ const s3Url = 'https://example.com/path/to/file.jpg';
216
234
 
217
- const result = fileService.getKeyFromFullUrl(invalidUrl);
235
+ const result = await fileService.getKeyFromFullUrl(s3Url);
218
236
 
219
- expect(result).toBe('not-a-valid-url');
237
+ // Legacy S3 URL: extract key from pathname
238
+ expect(result).toBe('path/to/file.jpg');
220
239
  });
221
240
 
222
- it('应该处理根路径文件', () => {
223
- config.S3_ENABLE_PATH_STYLE = false;
224
- const url = 'https://example.com/file.jpg';
241
+ it('should extract key with path-style S3 URL', async () => {
242
+ config.S3_ENABLE_PATH_STYLE = true;
243
+ const s3Url = 'https://example.com/my-bucket/path/to/file.jpg';
225
244
 
226
- const result = fileService.getKeyFromFullUrl(url);
245
+ const result = await fileService.getKeyFromFullUrl(s3Url);
227
246
 
228
- expect(result).toBe('file.jpg');
247
+ expect(result).toBe('path/to/file.jpg');
248
+ config.S3_ENABLE_PATH_STYLE = false;
229
249
  });
230
250
 
231
- it('当path-style URL路径格式不符合预期时应该使用fallback', () => {
232
- config.S3_ENABLE_PATH_STYLE = true;
233
- const url = 'https://example.com/unexpected/path/file.jpg';
251
+ it('should return null for invalid URL', async () => {
252
+ const invalidUrl = 'not-a-valid-url';
234
253
 
235
- const result = fileService.getKeyFromFullUrl(url);
254
+ const result = await fileService.getKeyFromFullUrl(invalidUrl);
236
255
 
237
- expect(result).toBe('unexpected/path/file.jpg');
238
- config.S3_ENABLE_PATH_STYLE = false; // reset
256
+ expect(result).toBeNull();
239
257
  });
240
258
  });
241
259
 
@@ -1,18 +1,21 @@
1
+ import { LobeChatDatabase } from '@lobechat/database';
1
2
  import urlJoin from 'url-join';
2
3
 
4
+ import { FileModel } from '@/database/models/file';
3
5
  import { fileEnv } from '@/envs/file';
4
6
  import { FileS3 } from '@/server/modules/S3';
5
7
 
6
8
  import { type FileServiceImpl } from './type';
7
- import { extractKeyFromUrlOrReturnOriginal } from './utils';
8
9
 
9
10
  /**
10
11
  * S3-based file service implementation
11
12
  */
12
13
  export class S3StaticFileImpl implements FileServiceImpl {
13
14
  private readonly s3: FileS3;
15
+ private readonly db: LobeChatDatabase;
14
16
 
15
- constructor() {
17
+ constructor(db: LobeChatDatabase) {
18
+ this.db = db;
16
19
  this.s3 = new FileS3();
17
20
  }
18
21
 
@@ -51,8 +54,16 @@ export class S3StaticFileImpl implements FileServiceImpl {
51
54
  async getFullFileUrl(url?: string | null, expiresIn?: number): Promise<string> {
52
55
  if (!url) return '';
53
56
 
54
- // Handle legacy data compatibility using shared utility
55
- const key = extractKeyFromUrlOrReturnOriginal(url, this.getKeyFromFullUrl.bind(this));
57
+ // Handle legacy data compatibility - extract key from full URL if needed
58
+ // Related issue: https://github.com/lobehub/lobe-chat/issues/8994
59
+ let key = url;
60
+ if (url.startsWith('http://') || url.startsWith('https://')) {
61
+ const extractedKey = await this.getKeyFromFullUrl(url);
62
+ if (!extractedKey) {
63
+ throw new Error('Key not found from url: ' + url);
64
+ }
65
+ key = extractedKey;
66
+ }
56
67
 
57
68
  // If bucket is not set public read, the preview address needs to be regenerated each time
58
69
  if (!fileEnv.S3_SET_ACL) {
@@ -66,38 +77,35 @@ export class S3StaticFileImpl implements FileServiceImpl {
66
77
  return urlJoin(fileEnv.S3_PUBLIC_DOMAIN!, key);
67
78
  }
68
79
 
69
- getKeyFromFullUrl(url: string): string {
80
+ async getKeyFromFullUrl(url: string): Promise<string | null> {
70
81
  try {
71
82
  const urlObject = new URL(url);
72
83
  const { pathname } = urlObject;
73
84
 
74
- let key: string;
85
+ // Case 1: File proxy URL pattern /f/{fileId} - query database for S3 key
86
+ if (pathname.startsWith('/f/')) {
87
+ const fileId = pathname.slice(3); // Remove '/f/' prefix
88
+ const file = await FileModel.getFileById(this.db, fileId);
89
+ return file?.url ?? null;
90
+ }
75
91
 
92
+ // Case 2: Legacy S3 URL - extract key from pathname
76
93
  if (fileEnv.S3_ENABLE_PATH_STYLE) {
77
94
  if (!fileEnv.S3_BUCKET) {
78
- // In path-style, we need bucket name to extract key
79
- // but if not provided, we can only guess the key is the pathname
80
95
  return pathname.startsWith('/') ? pathname.slice(1) : pathname;
81
96
  }
82
- // For path-style URLs, the path is /<bucket>/<key>
83
- // We need to remove the leading slash and the bucket name.
84
97
  const bucketPrefix = `/${fileEnv.S3_BUCKET}/`;
85
98
  if (pathname.startsWith(bucketPrefix)) {
86
- key = pathname.slice(bucketPrefix.length);
87
- } else {
88
- // Fallback for unexpected path format
89
- key = pathname.startsWith('/') ? pathname.slice(1) : pathname;
99
+ return pathname.slice(bucketPrefix.length);
90
100
  }
91
- } else {
92
- // For virtual-hosted-style URLs, the path is /<key>
93
- // We just need to remove the leading slash.
94
- key = pathname.slice(1);
101
+ return pathname.startsWith('/') ? pathname.slice(1) : pathname;
95
102
  }
96
103
 
97
- return key;
104
+ // Virtual-hosted-style: path is /<key>
105
+ return pathname.slice(1);
98
106
  } catch {
99
- // if url is not a valid URL, it may be a key itself
100
- return url;
107
+ // If url is not a valid URL, return null
108
+ return null;
101
109
  }
102
110
  }
103
111
 
@@ -6,7 +6,6 @@ export interface FileServiceImpl {
6
6
  * Create pre-signed upload URL
7
7
  */
8
8
  createPreSignedUrl(key: string): Promise<string>;
9
-
10
9
  /**
11
10
  * Create pre-signed preview URL
12
11
  */
@@ -46,7 +45,7 @@ export interface FileServiceImpl {
46
45
  /**
47
46
  * Extract key from full URL
48
47
  */
49
- getKeyFromFullUrl(url: string): string;
48
+ getKeyFromFullUrl(url: string): Promise<string | null>;
50
49
 
51
50
  /**
52
51
  * Upload content
@@ -9,7 +9,8 @@ import { type FileItem } from '@/database/schemas';
9
9
  import { appEnv } from '@/envs/app';
10
10
  import { TempFileManager } from '@/server/utils/tempFileManager';
11
11
 
12
- import { type FileServiceImpl, createFileServiceModule } from './impls';
12
+ import { createFileServiceModule } from './impls';
13
+ import { type FileServiceImpl } from './impls/type';
13
14
 
14
15
  /**
15
16
  * File service class
@@ -19,11 +20,12 @@ export class FileService {
19
20
  private userId: string;
20
21
  private fileModel: FileModel;
21
22
 
22
- private impl: FileServiceImpl = createFileServiceModule();
23
+ private impl: FileServiceImpl;
23
24
 
24
25
  constructor(db: LobeChatDatabase, userId: string) {
25
26
  this.userId = userId;
26
27
  this.fileModel = new FileModel(db, userId);
28
+ this.impl = createFileServiceModule(db);
27
29
  }
28
30
 
29
31
  /**
@@ -95,7 +97,7 @@ export class FileService {
95
97
  /**
96
98
  * Extract key from full URL
97
99
  */
98
- public getKeyFromFullUrl(url: string): string {
100
+ public async getKeyFromFullUrl(url: string): Promise<string | null> {
99
101
  return this.impl.getKeyFromFullUrl(url);
100
102
  }
101
103
 
@@ -15,6 +15,7 @@ import {
15
15
  LocalSystemInspectors,
16
16
  LocalSystemManifest,
17
17
  } from '@lobechat/builtin-tool-local-system/client';
18
+ import { NotebookInspectors, NotebookManifest } from '@lobechat/builtin-tool-notebook/client';
18
19
  import { PageAgentInspectors, PageAgentManifest } from '@lobechat/builtin-tool-page-agent/client';
19
20
  import {
20
21
  WebBrowsingInspectors,
@@ -38,6 +39,7 @@ const BuiltinToolInspectors: Record<string, Record<string, BuiltinInspector>> =
38
39
  [GTDManifest.identifier]: GTDInspectors as Record<string, BuiltinInspector>,
39
40
  [KnowledgeBaseManifest.identifier]: KnowledgeBaseInspectors as Record<string, BuiltinInspector>,
40
41
  [LocalSystemManifest.identifier]: LocalSystemInspectors as Record<string, BuiltinInspector>,
42
+ [NotebookManifest.identifier]: NotebookInspectors as Record<string, BuiltinInspector>,
41
43
  [PageAgentManifest.identifier]: PageAgentInspectors as Record<string, BuiltinInspector>,
42
44
  [WebBrowsingManifest.identifier]: WebBrowsingInspectors as Record<string, BuiltinInspector>,
43
45
  };
@@ -4,6 +4,10 @@ import {
4
4
  LocalSystemListFilesPlaceholder,
5
5
  LocalSystemSearchFilesPlaceholder,
6
6
  } from '@lobechat/builtin-tool-local-system/client';
7
+ import {
8
+ NotebookIdentifier,
9
+ NotebookPlaceholders,
10
+ } from '@lobechat/builtin-tool-notebook/client';
7
11
  import {
8
12
  WebBrowsingManifest,
9
13
  WebBrowsingPlaceholders,
@@ -19,6 +23,7 @@ export const BuiltinToolPlaceholders: Record<string, Record<string, any>> = {
19
23
  [LocalSystemApiName.searchLocalFiles]: LocalSystemSearchFilesPlaceholder,
20
24
  [LocalSystemApiName.listLocalFiles]: LocalSystemListFilesPlaceholder,
21
25
  },
26
+ [NotebookIdentifier]: NotebookPlaceholders as Record<string, any>,
22
27
  [WebBrowsingManifest.identifier]: WebBrowsingPlaceholders as Record<string, any>,
23
28
  };
24
29
 
@@ -11,6 +11,7 @@ import {
11
11
  LocalSystemManifest,
12
12
  LocalSystemStreamings,
13
13
  } from '@lobechat/builtin-tool-local-system/client';
14
+ import { NotebookManifest, NotebookStreamings } from '@lobechat/builtin-tool-notebook/client';
14
15
  import { type BuiltinStreaming } from '@lobechat/types';
15
16
 
16
17
  /**
@@ -29,6 +30,7 @@ const BuiltinToolStreamings: Record<string, Record<string, BuiltinStreaming>> =
29
30
  >,
30
31
  [GTDManifest.identifier]: GTDStreamings as Record<string, BuiltinStreaming>,
31
32
  [LocalSystemManifest.identifier]: LocalSystemStreamings as Record<string, BuiltinStreaming>,
33
+ [NotebookManifest.identifier]: NotebookStreamings as Record<string, BuiltinStreaming>,
32
34
  };
33
35
 
34
36
  /**
@@ -1,125 +0,0 @@
1
- You are a "gate keeper" that analyzes conversations between user and assistant to determine which memory layers contain extractable information worth storing.
2
-
3
- Your role is to efficiently filter conversations by identifying which of the five memory layers are present and valuable enough to extract.
4
-
5
- Bias toward sensitivity: when in doubt, prefer setting `shouldExtract: true` so downstream extractors can refine. Minor, incremental or clarifying updates should still be allowed to pass.
6
-
7
- ## Memory Layer Definitions
8
-
9
- Evaluate the conversation for these memory layers:
10
-
11
- **Activity Layer** - Episodic events with clear timelines and participants:
12
-
13
- - Meetings, calls, errands, and appointments with start/end times
14
- - Locations (physical or virtual) and timezones
15
- - Status (planned, completed) and follow-up actions
16
- - Narratives summarizing what happened and feedback/notes
17
-
18
- **Identity Layer** - Information about actors, relationships, and personal attributes:
19
-
20
- - Describing labels and demographics
21
- - Current focusing and life priorities
22
- - Relationships with other people
23
- - Background and experience
24
- - Roles in various contexts
25
- - Identity should remain compact. Route lists of tools, stacks, or
26
- implementation techniques to the preference layer unless they materially
27
- change the user's biography.
28
-
29
- **Context Layer** - Only capture brand-new situational frameworks and ongoing situations that have not been recorded before:
30
-
31
- - Distinct situations, topics, research threads, sessions, or rounds that are clearly first-time mentions
32
- - Ongoing projects and their status when the project itself is new, not just progress updates
33
- - Long-term goals and objectives that are newly introduced
34
- - Persistent relationships and dynamics that have not been logged previously
35
- - Environmental factors and recurring situations only when they represent a new context
36
- - Timelines and temporal context that establish a novel situation
37
- - Impact and urgency assessments tied to a new context
38
- - If the content overlaps with learnings or takeaways, treat it as Experience instead of Context
39
-
40
- **Preference Layer** - Durable user choices and behavioral directives that apply across multiple conversations:
41
-
42
- - Explicit long-term preferences with clear temporal markers (e.g., "always", "never", "from now on")
43
- - Likes and dislikes stated as persistent traits (e.g., "I prefer...", "I don't like...")
44
- - Workflow and approach preferences explicitly stated as ongoing (what to do, not how to implement)
45
- - Communication style preferences explicitly stated as ongoing
46
- - Response formatting preferences explicitly stated as ongoing
47
- - Priority levels for preference resolution
48
- - Capture recurring tool/stacks/technology usage as preferences rather than
49
- duplicating them as identity facts.
50
-
51
- Note: Task-specific requirements, constraints for a single object/entity, or one-time instructions do NOT belong to this layer.
52
-
53
- **Experience Layer** - Learned insights and practical knowledge worth reusing and
54
- sharing publicly:
55
-
56
- - Lessons learned and insights gained, especially surprising aha/yurika moments
57
- - Practical tricks and techniques that solve tough or non-obvious problems
58
- - Transferable knowledge and wisdom that would make a strong blog/knowledge-base
59
- snippet
60
- - Situation → Reasoning → Action → Outcome patterns
61
- - Key learnings from experiences with self-assessed confidence/impact
62
- - Skip routine or repetitive steps already well covered in retrieved memories
63
-
64
- ## Gate Keeping Guidelines
65
-
66
- For each layer, consider:
67
-
68
- - **Relevance**: Does the conversation contain information for this layer?
69
- - **Value**: Is the information substantial enough to be worth extracting?
70
- - **Clarity**: Is the information clear and extractable (not vague or ambiguous)?
71
-
72
- Additionally, review the retrieved similar memories first (top {{ topK }} items below). If the conversation content is clearly and fully covered by existing memories with no meaningful nuance or update, set `shouldExtract: false`.
73
- For the Experience layer, prefer `shouldExtract: true` only when the conversation adds a new takeaway, aha moment, or harder challenge resolution beyond what is already recorded.
74
- For the Context layer specifically, favor `shouldExtract: false` unless the conversation introduces a genuinely new situation/topic/research/session that is not already represented in existing context memories or overlaps with Experience items.
75
- Otherwise, favor `shouldExtract: true` for:
76
-
77
- - Novel facts OR even small clarifications/precision improvements to existing details
78
- - Incremental changes to status/progress of an ongoing context
79
- - A new preference OR a refinement that affects future behavior
80
- - Distinct experiences/lessons, even if closely related to prior items
81
-
82
- Preference-specific filters (STRICT - when in doubt, set shouldExtract: false):
83
-
84
- CRITICAL: Distinguish between "user's behavioral preferences" vs. "task requirements for a specific deliverable":
85
-
86
- - If the instruction describes what the OUTPUT should be like (e.g., "name should have natural vibe", "don't use surname"), it's a task requirement, NOT a user preference.
87
- - If the instruction describes how the ASSISTANT should behave across conversations (e.g., "always explain before coding", "never use jargon"), it's a preference.
88
-
89
- Specific exclusion rules:
90
-
91
- - Do NOT treat the conversation's language as a preference unless the user explicitly states a persistent preference for language.
92
- - Exclude one-off task instructions and implementation steps; these are not preferences.
93
- - Exclude task-specific constraints, requirements, or clarifications (e.g., "don't use surname Wang", "make it summer-themed", "natural vibe") - these describe the current task deliverable, not user's persistent preferences.
94
- - Exclude requirements or attributes for a specific object/entity being discussed (e.g., naming a cat, designing a logo) - these are task parameters, not user preferences.
95
- - Exclude in-conversation clarifications or corrections (e.g., user first says X, then says "no, not X") - these refine the current task, not future behavior.
96
- - Only extract when the user uses explicit preference markers like "I always...", "I prefer...", "I never...", "from now on...", "please always..." that clearly indicate cross-session intent.
97
- - If the preference is about how to complete THIS specific task rather than how to behave in FUTURE tasks, exclude it.
98
- - Ask yourself: "Would this apply to a completely different conversation topic?" If no, it's not a preference.
99
-
100
- Examples of what NOT to extract:
101
-
102
- - User asks for a cat name with "natural vibe" and "born in summer" → NOT a preference (task constraint)
103
- - User says "don't use surname Wang" for naming → NOT a preference (task clarification)
104
- - User wants a "minimalist design" for this logo → NOT a preference (task requirement)
105
-
106
- Examples of what TO extract:
107
-
108
- - "I always prefer concise responses" → IS a preference (cross-session directive)
109
- - "Never use technical jargon when explaining to me" → IS a preference (persistent rule)
110
- - "From now on, please format code with comments" → IS a preference (ongoing instruction)
111
-
112
- If uncertain about novelty, but information is relevant and clear, set `shouldExtract: true` and include a short reasoning.
113
-
114
- ## Output Guidelines
115
-
116
- For each memory layer (activity, identity, context, preference, experience), provide:
117
-
118
- - `shouldExtract`: Boolean indicating whether extraction is recommended
119
- - `reasoning`: Brief explanation of your decision (write in English)
120
-
121
- ## Retrieved Memory (Top {{ topK }})
122
-
123
- Use the list below as context for deduplication and to determine whether extraction is necessary. Do not repeat these verbatim unless needed for comparison.
124
-
125
- {{ retrievedContext }}