@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/plugin.json +1 -0
- package/locales/zh-CN/plugin.json +1 -0
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
- package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
- package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
- package/packages/database/src/models/__tests__/agent.test.ts +91 -4
- package/packages/database/src/models/agent.ts +15 -7
- package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
- package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
- package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
- package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
- package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
- package/packages/types/src/agent/agentConfig.ts +8 -8
- package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
- package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
- package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
- package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
- package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
- package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
- package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
- package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
- package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
- package/src/app/[variants]/(main)/page/index.tsx +3 -7
- package/src/components/Editor/AutoSaveHint.tsx +1 -1
- package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
- package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
- package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
- package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
- package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
- package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
- package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
- package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
- package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
- package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
- package/src/features/EditorCanvas/index.ts +9 -0
- package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
- package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
- package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
- package/src/features/PageEditor/Header/index.tsx +15 -18
- package/src/features/PageEditor/Header/useMenu.tsx +12 -9
- package/src/features/PageEditor/PageEditor.tsx +45 -21
- package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
- package/src/features/PageEditor/PageTitle/index.tsx +2 -2
- package/src/features/PageEditor/StoreUpdater.tsx +35 -308
- package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
- package/src/features/PageEditor/store/action.ts +96 -188
- package/src/features/PageEditor/store/initialState.ts +16 -21
- package/src/features/PageEditor/store/selectors.ts +3 -4
- package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
- package/src/features/PageExplorer/index.tsx +34 -67
- package/src/features/Portal/Artifacts/index.ts +0 -2
- package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
- package/src/features/Portal/Document/Body.tsx +1 -3
- package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
- package/src/features/Portal/Document/Header.tsx +13 -10
- package/src/features/Portal/Document/TodoList.tsx +6 -4
- package/src/features/Portal/Document/Wrapper.tsx +3 -11
- package/src/features/Portal/Document/index.ts +0 -2
- package/src/features/Portal/FilePreview/index.ts +0 -2
- package/src/features/Portal/GroupThread/index.ts +0 -3
- package/src/features/Portal/MessageDetail/index.ts +0 -2
- package/src/features/Portal/Notebook/index.ts +0 -2
- package/src/features/Portal/Plugins/index.ts +0 -2
- package/src/features/Portal/Thread/index.ts +0 -3
- package/src/features/Portal/components/Header.tsx +18 -6
- package/src/features/Portal/router.tsx +34 -97
- package/src/features/Portal/type.ts +0 -2
- package/src/locales/default/plugin.ts +1 -0
- package/src/store/chat/slices/portal/action.test.ts +218 -15
- package/src/store/chat/slices/portal/action.ts +194 -41
- package/src/store/chat/slices/portal/initialState.ts +40 -1
- package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
- package/src/store/chat/slices/portal/selectors.test.ts +119 -17
- package/src/store/chat/slices/portal/selectors.ts +117 -36
- package/src/store/document/index.ts +17 -5
- package/src/store/document/slices/document/action.ts +209 -0
- package/src/store/document/slices/document/index.ts +6 -0
- package/src/store/document/slices/editor/action.test.ts +340 -0
- package/src/store/document/slices/editor/action.ts +133 -149
- package/src/store/document/slices/editor/index.ts +9 -2
- package/src/store/document/slices/editor/initialState.ts +66 -29
- package/src/store/document/slices/editor/reducer.test.ts +217 -0
- package/src/store/document/slices/editor/reducer.ts +67 -0
- package/src/store/document/slices/editor/selectors.test.ts +395 -0
- package/src/store/document/slices/editor/selectors.ts +107 -5
- package/src/store/document/store.ts +12 -13
- package/src/store/file/slices/document/action.ts +19 -188
- package/src/store/file/slices/document/initialState.ts +0 -30
- package/src/store/file/slices/document/selectors.ts +25 -59
- package/src/store/notebook/index.ts +5 -4
- package/src/store/page/index.ts +2 -0
- package/src/store/page/initialState.ts +92 -0
- package/src/store/page/selectors.ts +5 -0
- package/src/store/page/slices/crud/action.ts +477 -0
- package/src/store/page/slices/crud/index.ts +2 -0
- package/src/store/page/slices/crud/initialState.ts +7 -0
- package/src/store/page/slices/internal/action.ts +32 -0
- package/src/store/page/slices/internal/index.ts +2 -0
- package/src/store/page/slices/internal/reducer.ts +105 -0
- package/src/store/page/slices/list/action.ts +206 -0
- package/src/store/page/slices/list/index.ts +3 -0
- package/src/store/page/slices/list/initialState.ts +29 -0
- package/src/store/page/slices/list/selectors.ts +90 -0
- package/src/store/page/slices/selection/action.ts +67 -0
- package/src/store/page/slices/selection/index.ts +2 -0
- package/src/store/page/slices/selection/initialState.ts +11 -0
- package/src/store/page/store.ts +29 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
- package/src/utils/identifier.ts +8 -2
- package/src/features/PageEditor/Body/index.tsx +0 -68
- package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
- package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
- package/src/features/Portal/Artifacts/useEnable.ts +0 -4
- package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
- package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
- package/src/features/Portal/Document/Title.tsx +0 -54
- package/src/features/Portal/Document/store/action.ts +0 -114
- package/src/features/Portal/Document/store/index.ts +0 -21
- package/src/features/Portal/Document/store/initialState.ts +0 -24
- package/src/features/Portal/Document/useEnable.ts +0 -8
- package/src/features/Portal/FilePreview/useEnable.ts +0 -6
- package/src/features/Portal/GroupThread/hook.ts +0 -9
- package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
- package/src/features/Portal/Notebook/useEnable.ts +0 -6
- package/src/features/Portal/Plugins/useEnable.ts +0 -6
- package/src/features/Portal/Thread/hook.ts +0 -8
- package/src/store/document/slices/notebook/action.ts +0 -119
- package/src/store/document/slices/notebook/index.ts +0 -3
- package/src/store/document/slices/notebook/initialState.ts +0 -12
- 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
|
+
[](#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
|
@@ -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.
|
|
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.
|
|
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:
|
|
45
|
+
gap: 8px;
|
|
46
46
|
align-items: center;
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
padding-inline:
|
|
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={
|
|
94
|
-
<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.
|
|
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) =>
|
|
64
|
+
const [portalDocumentId, openDocument, closeDocument] = useChatStore((s) => [
|
|
65
|
+
chatPortalSelectors.portalDocumentId(s),
|
|
66
|
+
s.openDocument,
|
|
67
|
+
s.closeDocument,
|
|
68
|
+
]);
|
|
64
69
|
|
|
65
|
-
const
|
|
66
|
-
|
|
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={
|
|
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={
|
|
113
|
+
icon={isExpanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
|
114
|
+
onClick={handleToggle}
|
|
104
115
|
shape={'round'}
|
|
105
116
|
variant={'outlined'}
|
|
106
117
|
>
|
|
107
|
-
{
|
|
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.
|
|
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
|
|
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
|
|
231
|
+
// ID lookup should not work across users for security
|
|
205
232
|
const result = await agentModel.getAgentConfig(agentId);
|
|
206
233
|
|
|
207
|
-
expect(result).
|
|
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({
|
|
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:
|
|
80
|
-
eq(agents.
|
|
81
|
-
|
|
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(
|
|
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:
|
|
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
|
+
`;
|