@lobehub/lobehub 2.0.0-next.255 → 2.0.0-next.256

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 (140) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/en-US/plugin.json +1 -0
  4. package/locales/zh-CN/plugin.json +1 -0
  5. package/package.json +1 -1
  6. package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
  7. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
  8. package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
  9. package/packages/database/src/models/__tests__/agent.test.ts +91 -4
  10. package/packages/database/src/models/agent.ts +15 -7
  11. package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
  12. package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
  13. package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
  14. package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
  15. package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
  16. package/packages/types/src/agent/agentConfig.ts +8 -8
  17. package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
  18. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
  19. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
  20. package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
  21. package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
  22. package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
  23. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
  24. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
  25. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
  26. package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
  27. package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
  28. package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
  29. package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
  30. package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
  31. package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
  32. package/src/app/[variants]/(main)/page/index.tsx +3 -7
  33. package/src/components/Editor/AutoSaveHint.tsx +1 -1
  34. package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
  35. package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
  36. package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
  37. package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
  38. package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
  39. package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
  40. package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
  41. package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
  42. package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
  43. package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
  44. package/src/features/EditorCanvas/index.ts +9 -0
  45. package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
  46. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
  47. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
  48. package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
  49. package/src/features/PageEditor/Header/index.tsx +15 -18
  50. package/src/features/PageEditor/Header/useMenu.tsx +12 -9
  51. package/src/features/PageEditor/PageEditor.tsx +45 -21
  52. package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
  53. package/src/features/PageEditor/PageTitle/index.tsx +2 -2
  54. package/src/features/PageEditor/StoreUpdater.tsx +35 -308
  55. package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
  56. package/src/features/PageEditor/store/action.ts +96 -188
  57. package/src/features/PageEditor/store/initialState.ts +16 -21
  58. package/src/features/PageEditor/store/selectors.ts +3 -4
  59. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
  60. package/src/features/PageExplorer/index.tsx +34 -67
  61. package/src/features/Portal/Artifacts/index.ts +0 -2
  62. package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
  63. package/src/features/Portal/Document/Body.tsx +1 -3
  64. package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
  65. package/src/features/Portal/Document/Header.tsx +13 -10
  66. package/src/features/Portal/Document/TodoList.tsx +6 -4
  67. package/src/features/Portal/Document/Wrapper.tsx +3 -11
  68. package/src/features/Portal/Document/index.ts +0 -2
  69. package/src/features/Portal/FilePreview/index.ts +0 -2
  70. package/src/features/Portal/GroupThread/index.ts +0 -3
  71. package/src/features/Portal/MessageDetail/index.ts +0 -2
  72. package/src/features/Portal/Notebook/index.ts +0 -2
  73. package/src/features/Portal/Plugins/index.ts +0 -2
  74. package/src/features/Portal/Thread/index.ts +0 -3
  75. package/src/features/Portal/components/Header.tsx +18 -6
  76. package/src/features/Portal/router.tsx +34 -97
  77. package/src/features/Portal/type.ts +0 -2
  78. package/src/locales/default/plugin.ts +1 -0
  79. package/src/store/chat/slices/portal/action.test.ts +218 -15
  80. package/src/store/chat/slices/portal/action.ts +194 -41
  81. package/src/store/chat/slices/portal/initialState.ts +40 -1
  82. package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
  83. package/src/store/chat/slices/portal/selectors.test.ts +119 -17
  84. package/src/store/chat/slices/portal/selectors.ts +117 -36
  85. package/src/store/document/index.ts +17 -5
  86. package/src/store/document/slices/document/action.ts +209 -0
  87. package/src/store/document/slices/document/index.ts +6 -0
  88. package/src/store/document/slices/editor/action.test.ts +340 -0
  89. package/src/store/document/slices/editor/action.ts +133 -149
  90. package/src/store/document/slices/editor/index.ts +9 -2
  91. package/src/store/document/slices/editor/initialState.ts +66 -29
  92. package/src/store/document/slices/editor/reducer.test.ts +217 -0
  93. package/src/store/document/slices/editor/reducer.ts +67 -0
  94. package/src/store/document/slices/editor/selectors.test.ts +395 -0
  95. package/src/store/document/slices/editor/selectors.ts +107 -5
  96. package/src/store/document/store.ts +12 -13
  97. package/src/store/file/slices/document/action.ts +19 -188
  98. package/src/store/file/slices/document/initialState.ts +0 -30
  99. package/src/store/file/slices/document/selectors.ts +25 -59
  100. package/src/store/notebook/index.ts +5 -4
  101. package/src/store/page/index.ts +2 -0
  102. package/src/store/page/initialState.ts +92 -0
  103. package/src/store/page/selectors.ts +5 -0
  104. package/src/store/page/slices/crud/action.ts +477 -0
  105. package/src/store/page/slices/crud/index.ts +2 -0
  106. package/src/store/page/slices/crud/initialState.ts +7 -0
  107. package/src/store/page/slices/internal/action.ts +32 -0
  108. package/src/store/page/slices/internal/index.ts +2 -0
  109. package/src/store/page/slices/internal/reducer.ts +105 -0
  110. package/src/store/page/slices/list/action.ts +206 -0
  111. package/src/store/page/slices/list/index.ts +3 -0
  112. package/src/store/page/slices/list/initialState.ts +29 -0
  113. package/src/store/page/slices/list/selectors.ts +90 -0
  114. package/src/store/page/slices/selection/action.ts +67 -0
  115. package/src/store/page/slices/selection/index.ts +2 -0
  116. package/src/store/page/slices/selection/initialState.ts +11 -0
  117. package/src/store/page/store.ts +29 -0
  118. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
  119. package/src/utils/identifier.ts +8 -2
  120. package/src/features/PageEditor/Body/index.tsx +0 -68
  121. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
  122. package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
  123. package/src/features/Portal/Artifacts/useEnable.ts +0 -4
  124. package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
  125. package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
  126. package/src/features/Portal/Document/Title.tsx +0 -54
  127. package/src/features/Portal/Document/store/action.ts +0 -114
  128. package/src/features/Portal/Document/store/index.ts +0 -21
  129. package/src/features/Portal/Document/store/initialState.ts +0 -24
  130. package/src/features/Portal/Document/useEnable.ts +0 -8
  131. package/src/features/Portal/FilePreview/useEnable.ts +0 -6
  132. package/src/features/Portal/GroupThread/hook.ts +0 -9
  133. package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
  134. package/src/features/Portal/Notebook/useEnable.ts +0 -6
  135. package/src/features/Portal/Plugins/useEnable.ts +0 -6
  136. package/src/features/Portal/Thread/hook.ts +0 -8
  137. package/src/store/document/slices/notebook/action.ts +0 -119
  138. package/src/store/document/slices/notebook/index.ts +0 -3
  139. package/src/store/document/slices/notebook/initialState.ts +0 -12
  140. package/src/store/document/slices/notebook/selectors.ts +0 -26
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.256](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.255...v2.0.0-next.256)
6
+
7
+ <sup>Released on **2026-01-10**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor page and notebook document usage.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Refactor page and notebook document usage, closes [#11345](https://github.com/lobehub/lobe-chat/issues/11345) ([88721eb](https://github.com/lobehub/lobe-chat/commit/88721eb))
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.0.0-next.255](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.254...v2.0.0-next.255)
6
31
 
7
32
  <sup>Released on **2026-01-10**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Refactor page and notebook document usage."
6
+ ]
7
+ },
8
+ "date": "2026-01-10",
9
+ "version": "2.0.0-next.256"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
@@ -92,6 +92,7 @@
92
92
  "builtins.lobe-local-system.inspector.noResults": "No results",
93
93
  "builtins.lobe-local-system.inspector.rename.result": "<old>{{oldName}}</old> → <new>{{newName}}</new>",
94
94
  "builtins.lobe-local-system.title": "Local System",
95
+ "builtins.lobe-notebook.actions.collapse": "Collapse",
95
96
  "builtins.lobe-notebook.actions.copy": "Copy",
96
97
  "builtins.lobe-notebook.actions.creating": "Creating document...",
97
98
  "builtins.lobe-notebook.actions.edit": "Edit",
@@ -92,6 +92,7 @@
92
92
  "builtins.lobe-local-system.inspector.noResults": "无结果",
93
93
  "builtins.lobe-local-system.inspector.rename.result": "<old>{{oldName}}</old> → <new>{{newName}}</new>",
94
94
  "builtins.lobe-local-system.title": "本地系统",
95
+ "builtins.lobe-notebook.actions.collapse": "收起",
95
96
  "builtins.lobe-notebook.actions.copy": "复制",
96
97
  "builtins.lobe-notebook.actions.creating": "文档创建中...",
97
98
  "builtins.lobe-notebook.actions.edit": "编辑",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.255",
3
+ "version": "2.0.0-next.256",
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",
@@ -21,7 +21,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
21
21
  border: 1px solid ${cssVar.colorBorderSecondary};
22
22
  border-radius: 16px;
23
23
 
24
- background: ${cssVar.colorBgElevated};
24
+ background: ${cssVar.colorBgContainer};
25
25
  `,
26
26
  content: css`
27
27
  padding-block: 16px;
@@ -42,11 +42,11 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
42
42
  transform: translateX(-50%);
43
43
 
44
44
  display: inline-flex;
45
- gap: 6px;
45
+ gap: 8px;
46
46
  align-items: center;
47
47
 
48
- padding-block: 4px;
49
- padding-inline: 12px;
48
+ height: 32px;
49
+ padding-inline: 16px;
50
50
  border: 1px solid ${cssVar.colorBorderSecondary};
51
51
  border-radius: 16px;
52
52
 
@@ -90,8 +90,8 @@ export const CreateDocumentPlaceholder = memo<BuiltinPlaceholderProps<CreateDocu
90
90
  )}
91
91
  </ScrollShadow>
92
92
  <div className={styles.statusTag}>
93
- <NeuralNetworkLoading size={14} />
94
- <span style={{ fontSize: 12 }}>{t('builtins.lobe-notebook.actions.creating')}</span>
93
+ <NeuralNetworkLoading size={16} />
94
+ <span>{t('builtins.lobe-notebook.actions.creating')}</span>
95
95
  </div>
96
96
  </Flexbox>
97
97
  );
@@ -3,11 +3,12 @@
3
3
  import { ActionIcon, CopyButton, Flexbox, Markdown, ScrollShadow } from '@lobehub/ui';
4
4
  import { Button } from 'antd';
5
5
  import { createStaticStyles } from 'antd-style';
6
- import { Maximize2, NotebookText, PencilLine } from 'lucide-react';
6
+ import { Maximize2, Minimize2, NotebookText, PencilLine } from 'lucide-react';
7
7
  import { memo } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
10
  import { useChatStore } from '@/store/chat';
11
+ import { chatPortalSelectors } from '@/store/chat/slices/portal/selectors';
11
12
 
12
13
  import { NotebookDocument } from '../../../types';
13
14
 
@@ -21,7 +22,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
21
22
  border: 1px solid ${cssVar.colorBorderSecondary};
22
23
  border-radius: 16px;
23
24
 
24
- background: ${cssVar.colorBgElevated};
25
+ background: ${cssVar.colorBgContainer};
25
26
  `,
26
27
  content: css`
27
28
  padding-inline: 16px;
@@ -60,10 +61,20 @@ interface DocumentCardProps {
60
61
 
61
62
  const DocumentCard = memo<DocumentCardProps>(({ document }) => {
62
63
  const { t } = useTranslation('plugin');
63
- const openDocument = useChatStore((s) => s.openDocument);
64
+ const [portalDocumentId, openDocument, closeDocument] = useChatStore((s) => [
65
+ chatPortalSelectors.portalDocumentId(s),
66
+ s.openDocument,
67
+ s.closeDocument,
68
+ ]);
64
69
 
65
- const handleExpand = () => {
66
- openDocument(document.id);
70
+ const isExpanded = portalDocumentId === document.id;
71
+
72
+ const handleToggle = () => {
73
+ if (isExpanded) {
74
+ closeDocument();
75
+ } else {
76
+ openDocument(document.id);
77
+ }
67
78
  };
68
79
 
69
80
  return (
@@ -82,7 +93,7 @@ const DocumentCard = memo<DocumentCardProps>(({ document }) => {
82
93
  />
83
94
  <ActionIcon
84
95
  icon={PencilLine}
85
- onClick={handleExpand}
96
+ onClick={handleToggle}
86
97
  size={'small'}
87
98
  title={t('builtins.lobe-notebook.actions.edit')}
88
99
  />
@@ -95,16 +106,18 @@ const DocumentCard = memo<DocumentCardProps>(({ document }) => {
95
106
  </Markdown>
96
107
  </ScrollShadow>
97
108
 
98
- {/* Floating expand button */}
109
+ {/* Floating expand/collapse button */}
99
110
  <Button
100
111
  className={styles.expandButton}
101
112
  color={'default'}
102
- icon={<Maximize2 size={14} />}
103
- onClick={handleExpand}
113
+ icon={isExpanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
114
+ onClick={handleToggle}
104
115
  shape={'round'}
105
116
  variant={'outlined'}
106
117
  >
107
- {t('builtins.lobe-notebook.actions.expand')}
118
+ {isExpanded
119
+ ? t('builtins.lobe-notebook.actions.collapse')
120
+ : t('builtins.lobe-notebook.actions.expand')}
108
121
  </Button>
109
122
  </Flexbox>
110
123
  );
@@ -20,7 +20,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
20
20
  border: 1px solid ${cssVar.colorBorderSecondary};
21
21
  border-radius: 16px;
22
22
 
23
- background: ${cssVar.colorBgElevated};
23
+ background: ${cssVar.colorBgContainer};
24
24
  `,
25
25
  header: css`
26
26
  padding-block: 10px;
@@ -155,6 +155,33 @@ describe('AgentModel', () => {
155
155
  expect(result).not.toBeNull();
156
156
  expect(result!.files).toHaveLength(0);
157
157
  });
158
+
159
+ it('should not return agent belonging to another user', async () => {
160
+ const agentId = 'test-agent-other-user';
161
+ // Create agent for user2
162
+ await serverDB.insert(agents).values({ id: agentId, userId: userId2 });
163
+
164
+ // Try to access with user1's model
165
+ const result = await agentModel.getAgentConfigById(agentId);
166
+
167
+ expect(result).toBeNull();
168
+ });
169
+
170
+ it('should not return knowledge from another user agent', async () => {
171
+ const agentId = 'test-agent-cross-user-knowledge';
172
+ // Create agent for user2 with knowledge
173
+ await serverDB.insert(agents).values({ id: agentId, userId: userId2 });
174
+ await serverDB
175
+ .insert(agentsKnowledgeBases)
176
+ .values({ agentId, knowledgeBaseId: 'kb2', userId: userId2 });
177
+ await serverDB.insert(agentsFiles).values({ agentId, fileId: '3', userId: userId2 });
178
+
179
+ // Try to access with user1's model
180
+ const result = await agentModel.getAgentConfigById(agentId);
181
+
182
+ // Should return null since user1 cannot access user2's agent
183
+ expect(result).toBeNull();
184
+ });
158
185
  });
159
186
 
160
187
  describe('getAgentConfig', () => {
@@ -197,15 +224,14 @@ describe('AgentModel', () => {
197
224
  expect(result).toBeNull();
198
225
  });
199
226
 
200
- it('should find agent by ID even if it belongs to another user', async () => {
227
+ it('should not find agent by ID if it belongs to another user', async () => {
201
228
  const agentId = 'test-agent-cross-user';
202
229
  await serverDB.insert(agents).values({ id: agentId, userId: userId2 });
203
230
 
204
- // ID lookup should work across users (ID is globally unique)
231
+ // ID lookup should not work across users for security
205
232
  const result = await agentModel.getAgentConfig(agentId);
206
233
 
207
- expect(result).toBeDefined();
208
- expect(result?.id).toBe(agentId);
234
+ expect(result).toBeNull();
209
235
  });
210
236
 
211
237
  it('should prefer ID match over slug match', async () => {
@@ -257,6 +283,67 @@ describe('AgentModel', () => {
257
283
 
258
284
  expect(result).toBeUndefined();
259
285
  });
286
+
287
+ it('should not return agent from another user session', async () => {
288
+ const agentId = 'test-agent-other-user-session';
289
+ const sessionId = 'test-session-other-user';
290
+ // Create agent and session for user2
291
+ await serverDB.insert(agents).values({ id: agentId, userId: userId2 });
292
+ await serverDB.insert(sessions).values({ id: sessionId, userId: userId2 });
293
+ await serverDB
294
+ .insert(agentsToSessions)
295
+ .values({ agentId, sessionId, userId: userId2 });
296
+
297
+ // Try to access with user1's model
298
+ const result = await agentModel.findBySessionId(sessionId);
299
+
300
+ expect(result).toBeUndefined();
301
+ });
302
+ });
303
+
304
+ describe('getAgentAssignedKnowledge', () => {
305
+ it('should return knowledge bases and files for the agent', async () => {
306
+ const agentId = 'test-agent-knowledge';
307
+ await serverDB.insert(agents).values({ id: agentId, userId });
308
+ await serverDB
309
+ .insert(agentsKnowledgeBases)
310
+ .values({ agentId, knowledgeBaseId: 'kb1', userId, enabled: true });
311
+ await serverDB.insert(agentsFiles).values({ agentId, fileId: '1', userId, enabled: true });
312
+
313
+ const result = await agentModel.getAgentAssignedKnowledge(agentId);
314
+
315
+ expect(result.knowledgeBases).toHaveLength(1);
316
+ expect(result.files).toHaveLength(1);
317
+ });
318
+
319
+ it('should not return knowledge from another user', async () => {
320
+ const agentId = 'test-agent-knowledge-other-user';
321
+ // Create agent with knowledge for user2
322
+ await serverDB.insert(agents).values({ id: agentId, userId: userId2 });
323
+ await serverDB
324
+ .insert(agentsKnowledgeBases)
325
+ .values({ agentId, knowledgeBaseId: 'kb2', userId: userId2, enabled: true });
326
+ await serverDB
327
+ .insert(agentsFiles)
328
+ .values({ agentId, fileId: '3', userId: userId2, enabled: true });
329
+
330
+ // Try to access with user1's model
331
+ const result = await agentModel.getAgentAssignedKnowledge(agentId);
332
+
333
+ // Should return empty arrays since user1 cannot access user2's knowledge
334
+ expect(result.knowledgeBases).toHaveLength(0);
335
+ expect(result.files).toHaveLength(0);
336
+ });
337
+
338
+ it('should handle empty knowledge bases and files', async () => {
339
+ const agentId = 'test-agent-no-knowledge';
340
+ await serverDB.insert(agents).values({ id: agentId, userId });
341
+
342
+ const result = await agentModel.getAgentAssignedKnowledge(agentId);
343
+
344
+ expect(result.knowledgeBases).toHaveLength(0);
345
+ expect(result.files).toHaveLength(0);
346
+ });
260
347
  });
261
348
 
262
349
  describe('createAgentKnowledgeBase', () => {
@@ -28,7 +28,9 @@ export class AgentModel {
28
28
  }
29
29
 
30
30
  getAgentConfigById = async (id: string) => {
31
- const agent = await this.db.query.agents.findFirst({ where: eq(agents.id, id) });
31
+ const agent = await this.db.query.agents.findFirst({
32
+ where: and(eq(agents.id, id), eq(agents.userId, this.userId)),
33
+ });
32
34
 
33
35
  if (!agent) return null;
34
36
 
@@ -76,9 +78,9 @@ export class AgentModel {
76
78
  */
77
79
  getAgentConfig = async (idOrSlug: string) => {
78
80
  const agent = await this.db.query.agents.findFirst({
79
- where: or(
80
- eq(agents.id, idOrSlug),
81
- and(eq(agents.slug, idOrSlug), eq(agents.userId, this.userId)),
81
+ where: and(
82
+ eq(agents.userId, this.userId),
83
+ or(eq(agents.id, idOrSlug), eq(agents.slug, idOrSlug)),
82
84
  ),
83
85
  });
84
86
 
@@ -118,17 +120,20 @@ export class AgentModel {
118
120
 
119
121
  getAgentAssignedKnowledge = async (id: string) => {
120
122
  // Run both queries in parallel for better performance
123
+ // Include userId check to ensure user can only access their own agent's knowledge
121
124
  const [knowledgeBaseResult, fileResult] = await Promise.all([
122
125
  this.db
123
126
  .select({ enabled: agentsKnowledgeBases.enabled, knowledgeBases })
124
127
  .from(agentsKnowledgeBases)
125
- .where(eq(agentsKnowledgeBases.agentId, id))
128
+ .where(
129
+ and(eq(agentsKnowledgeBases.agentId, id), eq(agentsKnowledgeBases.userId, this.userId)),
130
+ )
126
131
  .orderBy(desc(agentsKnowledgeBases.createdAt))
127
132
  .leftJoin(knowledgeBases, eq(knowledgeBases.id, agentsKnowledgeBases.knowledgeBaseId)),
128
133
  this.db
129
134
  .select({ enabled: agentsFiles.enabled, files })
130
135
  .from(agentsFiles)
131
- .where(eq(agentsFiles.agentId, id))
136
+ .where(and(eq(agentsFiles.agentId, id), eq(agentsFiles.userId, this.userId)))
132
137
  .orderBy(desc(agentsFiles.createdAt))
133
138
  .leftJoin(files, eq(files.id, agentsFiles.fileId)),
134
139
  ]);
@@ -150,7 +155,10 @@ export class AgentModel {
150
155
  */
151
156
  findBySessionId = async (sessionId: string) => {
152
157
  const item = await this.db.query.agentsToSessions.findFirst({
153
- where: eq(agentsToSessions.sessionId, sessionId),
158
+ where: and(
159
+ eq(agentsToSessions.sessionId, sessionId),
160
+ eq(agentsToSessions.userId, this.userId),
161
+ ),
154
162
  });
155
163
 
156
164
  if (!item) return;
@@ -109,7 +109,7 @@ export class EditorRuntime {
109
109
  }
110
110
 
111
111
  // Set markdown content directly - the editor will convert it internally
112
- editor.setDocument('markdown', markdown);
112
+ editor.setDocument('markdown', markdown, { keepId: true });
113
113
 
114
114
  // Get the resulting document to count nodes
115
115
  const jsonState = editor.getDocument('json') as any;
@@ -2,6 +2,7 @@ import {
2
2
  CommonPlugin,
3
3
  type IEditor,
4
4
  Kernel,
5
+ ListPlugin,
5
6
  LitexmlPlugin,
6
7
  MarkdownPlugin,
7
8
  moment,
@@ -10,6 +11,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
10
11
 
11
12
  import { EditorRuntime } from '../EditorRuntime';
12
13
  import editAllFixture from './fixtures/edit-all.json';
14
+ import removeThenAddFixture from './fixtures/remove-then-add.json';
13
15
  import removeFixture from './fixtures/remove.json';
14
16
 
15
17
  describe('EditorRuntime - Real Cases', () => {
@@ -20,7 +22,7 @@ describe('EditorRuntime - Real Cases', () => {
20
22
 
21
23
  beforeEach(() => {
22
24
  editor = new Kernel();
23
- editor.registerPlugins([CommonPlugin, MarkdownPlugin, LitexmlPlugin]);
25
+ editor.registerPlugins([CommonPlugin, MarkdownPlugin, ListPlugin, LitexmlPlugin]);
24
26
  editor.initNodeEditor();
25
27
 
26
28
  runtime = new EditorRuntime();
@@ -163,9 +165,6 @@ describe('EditorRuntime - Real Cases', () => {
163
165
 
164
166
  // Verify paragraphs were removed
165
167
  const xmlAfter = editor.getDocument('litexml') as unknown as string;
166
- const paragraphsAfter = [...xmlAfter.matchAll(/<p id="([^"]+)"/g)];
167
-
168
- expect(paragraphsAfter.length).toBe(initialCount - 7);
169
168
 
170
169
  // Verify the removed IDs are no longer present
171
170
  expect(xmlAfter).not.toContain('id="wps3"');
@@ -179,4 +178,66 @@ describe('EditorRuntime - Real Cases', () => {
179
178
  expect(xmlAfter).toMatchSnapshot();
180
179
  });
181
180
  });
181
+
182
+ describe('modifyNodes - remove then add', () => {
183
+ it('should remove 13 paragraphs then insert a list', async () => {
184
+ // Initialize editor with the JSON fixture
185
+ editor.setDocument('json', removeThenAddFixture);
186
+ await moment();
187
+
188
+ // First operation: remove 13 paragraphs
189
+ const removeResult = await runtime.modifyNodes({
190
+ operations: [
191
+ { action: 'remove', id: 'x4qr' },
192
+ { action: 'remove', id: 'xfvd' },
193
+ { action: 'remove', id: 'xqzz' },
194
+ { action: 'remove', id: 'zrby' },
195
+ { action: 'remove', id: '02gk' },
196
+ { action: 'remove', id: '0dl6' },
197
+ { action: 'remove', id: '0ops' },
198
+ { action: 'remove', id: '1rnx' },
199
+ { action: 'remove', id: '22sj' },
200
+ { action: 'remove', id: '2dx5' },
201
+ { action: 'remove', id: '3gva' },
202
+ { action: 'remove', id: '3rzw' },
203
+ { action: 'remove', id: '434i' },
204
+ ],
205
+ });
206
+
207
+ await moment();
208
+
209
+ // Verify all remove operations succeeded
210
+ expect(removeResult.successCount).toBe(13);
211
+ expect(removeResult.totalCount).toBe(13);
212
+ expect(removeResult.results.every((r) => r.success)).toBe(true);
213
+ expect(removeResult.results.every((r) => r.action === 'remove')).toBe(true);
214
+
215
+ // Verify the content was removed
216
+ const removed = editor.getDocument('litexml') as unknown as string;
217
+ expect(removed).toMatchSnapshot('remove 13 paragraphs');
218
+
219
+ // Second operation: insert a list after wtm5
220
+ const insertResult = await runtime.modifyNodes({
221
+ operations: [
222
+ {
223
+ action: 'insert',
224
+ afterId: 'wtm5',
225
+ litexml:
226
+ '<ul><li>西湖风景区:杭州的灵魂,世界文化遗产</li><li>灵隐寺:杭州最著名的佛教寺庙</li><li>西溪国家湿地公园:中国第一个国家湿地公园</li><li>宋城:以宋代文化为主题的大型主题公园</li></ul>',
227
+ },
228
+ ],
229
+ });
230
+ await moment();
231
+
232
+ // Verify insert operation succeeded
233
+ expect(insertResult.successCount).toBe(1);
234
+ expect(insertResult.totalCount).toBe(1);
235
+ expect(insertResult.results.every((r) => r.success)).toBe(true);
236
+ expect(insertResult.results.every((r) => r.action === 'insert')).toBe(true);
237
+
238
+ // Verify full output
239
+ const xmlAfter = editor.getDocument('litexml') as unknown as string;
240
+ expect(xmlAfter).toMatchSnapshot('insert new');
241
+ });
242
+ });
182
243
  });
@@ -1,37 +1,128 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`EditorRuntime - Real Cases > modifyNodes - batch modify all paragraphs > should modify all 16 paragraphs in a single call 1`] = `
4
- "(雨点敲打着咖啡馆的玻璃窗,像无数细小的手指在弹奏着无声的钢琴。林晓坐在靠窗的位置,手中的咖啡已经凉了,她却浑然不觉。这是她第三次来到这家咖啡馆,每次都是同样的位置,同样的时间。)
4
+ "(雨点敲打着咖啡馆的玻璃窗,像无数细小的手指在弹奏着无声的钢琴。林晓坐在靠窗的位置,手中的咖啡已经凉了,她却浑然不觉。这是她第三次来到这家咖啡馆,每次都是同样的位置,同样的时间。)
5
5
 
6
- (窗外雨丝如帘,街灯昏黄。咖啡馆内灯光柔和,墙上挂着旧照片,书架上摆满了书。空气里是咖啡香和旧书纸的味道。)
6
+ (窗外雨丝如帘,街灯昏黄。咖啡馆内灯光柔和,墙上挂着旧照片,书架上摆满了书。空气里是咖啡香和旧书纸的味道。)
7
7
 
8
- 林晓:(内心独白)第一次来的时候,也是这样的雨夜。那天我刚结束一段五年的感情,整个人像是被掏空了。我点了一杯美式咖啡,就这样坐着,看着窗外的雨,直到打烊。
8
+ 林晓:(内心独白)第一次来的时候,也是这样的雨夜。那天我刚结束一段五年的感情,整个人像是被掏空了。我点了一杯美式咖啡,就这样坐着,看着窗外的雨,直到打烊。
9
9
 
10
- 林晓:(继续独白)第二次来的时候,我遇到了他。穿着灰色风衣,坐在对面的位置。他一直在看书,偶尔抬头看看窗外。他的手指修长,翻书的动作优雅从容。
10
+ 林晓:(继续独白)第二次来的时候,我遇到了他。穿着灰色风衣,坐在对面的位置。他一直在看书,偶尔抬头看看窗外。他的手指修长,翻书的动作优雅从容。
11
11
 
12
- 林晓:(独白)今天,我又来了。雨还是那样下着,咖啡馆还是那样安静。我不知道自己在期待什么,也许只是习惯了这种孤独的仪式感。
12
+ 林晓:(独白)今天,我又来了。雨还是那样下着,咖啡馆还是那样安静。我不知道自己在期待什么,也许只是习惯了这种孤独的仪式感。
13
13
 
14
- (门上的风铃响了,有人推门进来。林晓下意识地抬头,心跳突然漏了一拍。)
14
+ (门上的风铃响了,有人推门进来。林晓下意识地抬头,心跳突然漏了一拍。)
15
15
 
16
- (风铃叮咚作响。一个身影站在门口,雨伞滴着水,灯光勾勒出他的轮廓。)
16
+ (风铃叮咚作响。一个身影站在门口,雨伞滴着水,灯光勾勒出他的轮廓。)
17
17
 
18
- 林晓:(低声)是他。
18
+ 林晓:(低声)是他。
19
19
 
20
- (他收起雨伞,抖了抖身上的水珠,然后径直走向她。这一次,他没有坐在对面的位置,而是在她面前停了下来。)
20
+ (他收起雨伞,抖了抖身上的水珠,然后径直走向她。这一次,他没有坐在对面的位置,而是在她面前停了下来。)
21
21
 
22
- 陈默:(声音低沉温和)我可以坐这里吗?
22
+ 陈默:(声音低沉温和)我可以坐这里吗?
23
23
 
24
- (林晓点了点头,喉咙有些发干。窗外的雨声似乎变小了,咖啡馆里的音乐也变得清晰起来。)
24
+ (林晓点了点头,喉咙有些发干。窗外的雨声似乎变小了,咖啡馆里的音乐也变得清晰起来。)
25
25
 
26
- 陈默:(眼中带着笑意)我注意到你每次都在这里。
26
+ 陈默:(眼中带着笑意)我注意到你每次都在这里。
27
27
 
28
- 林晓:(凝视着他,内心独白)他的眼睛是深褐色的,像秋天的落叶,温暖而深邃。他的鼻梁挺直,唇线分明,微笑时眼角有细微的皱纹,更添了几分沧桑感。我忽然觉得这个人似曾相识,却又分明是第一次见面。
28
+ 林晓:(凝视着他,内心独白)他的眼睛是深褐色的,像秋天的落叶,温暖而深邃。他的鼻梁挺直,唇线分明,微笑时眼角有细微的皱纹,更添了几分沧桑感。我忽然觉得这个人似曾相识,却又分明是第一次见面。
29
29
 
30
- 陈默:(伸出手)我叫陈默。很高兴终于能和你说话。我观察你三次了,每次你都这样静静地坐着看雨,若有所思,若有所待。
30
+ 陈默:(伸出手)我叫陈默。很高兴终于能和你说话。我观察你三次了,每次你都这样静静地坐着看雨,若有所思,若有所待。
31
31
 
32
- (林晓握住他的手,感受到他掌心的温度。他的手温暖,却有薄茧,像是经常写字或弹琴的人。雨还在下,但咖啡馆里忽然觉得温暖如春,先前的孤独寂寥,竟然悄然消散了。)
33
-
34
- (旁白)也许,有些相遇注定要在雨夜发生。就像有些故事,注定要从一句简单的问候开始。
32
+ (林晓握住他的手,感受到他掌心的温度。他的手温暖,却有薄茧,像是经常写字或弹琴的人。雨还在下,但咖啡馆里忽然觉得温暖如春,先前的孤独寂寥,竟然悄然消散了。)
35
33
 
34
+ (旁白)也许,有些相遇注定要在雨夜发生。就像有些故事,注定要从一句简单的问候开始。
36
35
  "
37
36
  `;
37
+
38
+ exports[`EditorRuntime - Real Cases > modifyNodes - batch remove paragraphs > should remove 7 paragraphs in a single call 1`] = `
39
+ "<?xml version="1.0" encoding="UTF-8"?>
40
+ <root>
41
+ <p id="lwap">
42
+ <span id="m1v0">杭州,一座被诗意浸润的千年古都,静静地依偎在钱塘江畔,宛如一幅徐徐展开的水墨长卷。这里不仅是浙江省的政治、经济、文化中心,更是中国七大古都之一,承载着2200余年的历史记忆。从南宋临安的繁华盛景到今日数字经济的创新高地,杭州始终以"人间天堂"的美誉,向世界展示着东方文明的独特魅力。</span>
43
+ </p>
44
+ <h2 id="m7fb">
45
+ <span id="mczm">地理与气候</span>
46
+ </h2>
47
+ <p id="mijx">
48
+ <span id="mo48">杭州地处钱塘江下游,京杭大运河南端,东临杭州湾,西接天目山。全市总面积16850平方公里,常住人口超过1200万。杭州属于亚热带季风气候,四季分明,雨量充沛,年平均气温17.8℃,气候宜人。</span>
49
+ </p>
50
+ <h2 id="mtoj">
51
+ <span id="mz8u">历史文化</span>
52
+ </h2>
53
+ <p id="n4t5">
54
+ <span id="nadg">杭州是吴越文化和南宋文化的发源地之一。公元1138年,南宋定都临安(今杭州),使其成为当时世界上最繁华的城市之一。杭州拥有丰富的历史文化遗产,包括西湖文化景观、京杭大运河、良渚古城遗址等世界文化遗产。</span>
55
+ </p>
56
+ <p id="vbpc">
57
+ <span id="vh9n">南宋时期(1127-1279年)是杭州历史上的黄金时代。宋室南渡后定都临安(今杭州),使其成为当时世界上人口最多、经济最繁荣的城市之一。马可·波罗在游记中称杭州为"世界上最美丽华贵之天城"。南宋时期杭州的工商业、文化艺术、科学技术都达到了空前的高度:</span>
58
+ </p>
59
+ <ul id="t5t2">
60
+ <li id="tbdd">
61
+ <span id="tgxo" bold="true">经济繁荣</span>
62
+ <span id="tmhz">:丝绸、瓷器、茶叶贸易发达,出现了世界上最早的纸币"交子"</span>
63
+ </li>
64
+ <li id="ts2a">
65
+ <span id="txml" bold="true">文化鼎盛</span>
66
+ <span id="u36w">:宋词达到艺术高峰,苏轼、柳永、李清照等文人雅士云集</span>
67
+ </li>
68
+ <li id="u8r7">
69
+ <span id="uebi" bold="true">科技创新</span>
70
+ <span id="ujvt">:活字印刷术、指南针、火药等重大发明得到广泛应用</span>
71
+ </li>
72
+ <li id="upg4">
73
+ <span id="uv0f" bold="true">城市建设</span>
74
+ <span id="v0kq">:形成了"前朝后市"的格局,御街、清河坊等商业区繁华异常</span>
75
+ </li>
76
+ </ul>
77
+ <h2 id="nfxr">
78
+ <span id="nli2">西湖风光</span>
79
+ </h2>
80
+ <p id="nr2d">
81
+ <span id="nwmo">西湖是杭州的灵魂,也是中国最著名的风景名胜之一。西湖十景(如苏堤春晓、断桥残雪、雷峰夕照等)闻名遐迩。西湖不仅自然风光秀丽,更承载着深厚的文化内涵,历代文人墨客在此留下了无数诗词歌赋。</span>
82
+ </p>
83
+ </root>"
84
+ `;
85
+
86
+ exports[`EditorRuntime - Real Cases > modifyNodes - remove then add > should remove 13 paragraphs then insert a list > insert new 1`] = `
87
+ "<?xml version="1.0" encoding="UTF-8"?>
88
+ <root>
89
+ <p id="wihj">
90
+ <span id="wo1u">杭州是中国著名的旅游城市,素有"人间天堂"的美誉。这里既有美丽的自然风光,又有深厚的历史文化底蕴。</span>
91
+ </p>
92
+ <h2 id="wtm5">
93
+ <span id="wz6g">必游景点</span>
94
+ </h2>
95
+ <ul id="fiv4">
96
+ <li id="foff">
97
+ <span id="ftzq">西湖风景区:杭州的灵魂,世界文化遗产</span>
98
+ </li>
99
+ <li id="fzk1">
100
+ <span id="g54c">灵隐寺:杭州最著名的佛教寺庙</span>
101
+ </li>
102
+ <li id="gaon">
103
+ <span id="gg8y">西溪国家湿地公园:中国第一个国家湿地公园</span>
104
+ </li>
105
+ <li id="glt9">
106
+ <span id="grdk">宋城:以宋代文化为主题的大型主题公园</span>
107
+ </li>
108
+ </ul>
109
+ <h2 id="562n">
110
+ <span id="5bmy">美食推荐</span>
111
+ </h2>
112
+ </root>"
113
+ `;
114
+
115
+ exports[`EditorRuntime - Real Cases > modifyNodes - remove then add > should remove 13 paragraphs then insert a list > remove 13 paragraphs 1`] = `
116
+ "<?xml version="1.0" encoding="UTF-8"?>
117
+ <root>
118
+ <p id="wihj">
119
+ <span id="wo1u">杭州是中国著名的旅游城市,素有"人间天堂"的美誉。这里既有美丽的自然风光,又有深厚的历史文化底蕴。</span>
120
+ </p>
121
+ <h2 id="wtm5">
122
+ <span id="wz6g">必游景点</span>
123
+ </h2>
124
+ <h2 id="562n">
125
+ <span id="5bmy">美食推荐</span>
126
+ </h2>
127
+ </root>"
128
+ `;