@lobehub/lobehub 2.0.0-next.217 → 2.0.0-next.218

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.218](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.217...v2.0.0-next.218)
6
+
7
+ <sup>Released on **2026-01-05**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Update the sandbox export files & save files way.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: Fix editor modal when Markdown rendering off.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### What's improved
23
+
24
+ - **misc**: Update the sandbox export files & save files way, closes [#11249](https://github.com/lobehub/lobe-chat/issues/11249) ([039b0a1](https://github.com/lobehub/lobe-chat/commit/039b0a1))
25
+
26
+ #### What's fixed
27
+
28
+ - **misc**: Fix editor modal when Markdown rendering off, closes [#11251](https://github.com/lobehub/lobe-chat/issues/11251) ([eb86d3b](https://github.com/lobehub/lobe-chat/commit/eb86d3b))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
5
38
  ## [Version 2.0.0-next.217](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.216...v2.0.0-next.217)
6
39
 
7
40
  <sup>Released on **2026-01-05**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Update the sandbox export files & save files way."
6
+ ],
7
+ "fixes": [
8
+ "Fix editor modal when Markdown rendering off."
9
+ ]
10
+ },
11
+ "date": "2026-01-05",
12
+ "version": "2.0.0-next.218"
13
+ },
2
14
  {
3
15
  "children": {},
4
16
  "date": "2026-01-05",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.217",
3
+ "version": "2.0.0-next.218",
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",
@@ -425,51 +425,38 @@ export class CloudSandboxExecutionRuntime {
425
425
 
426
426
  /**
427
427
  * Export a file from the sandbox to cloud storage
428
- * 1. Get a pre-signed upload URL from our server
429
- * 2. Call the sandbox to upload the file to that URL
430
- * 3. Return the download URL to the user
428
+ * Uses a single tRPC call that handles:
429
+ * 1. Generate pre-signed upload URL
430
+ * 2. Call sandbox to upload file
431
+ * 3. Create persistent file record
432
+ * 4. Return permanent /f/:id URL
431
433
  */
432
434
  async exportFile(args: ExportFileParams): Promise<BuiltinServerRuntimeOutput> {
433
435
  try {
434
436
  // Extract filename from path
435
437
  const filename = args.path.split('/').pop() || 'exported_file';
436
438
 
437
- // Step 1: Get pre-signed upload URL from our server
438
- const uploadUrlResult = await codeInterpreterService.getExportFileUploadUrl(
439
+ // Single call that handles everything: upload URL generation, sandbox upload, and file record creation
440
+ const result = await codeInterpreterService.exportAndUploadFile(
441
+ args.path,
439
442
  filename,
440
443
  this.context.topicId,
441
444
  );
442
445
 
443
- if (!uploadUrlResult.success) {
444
- throw new Error(uploadUrlResult.error?.message || 'Failed to get upload URL');
445
- }
446
-
447
- // Step 2: Call the sandbox's exportFile tool with the upload URL
448
- // The sandbox will read the file and upload it to the pre-signed URL
449
- const result = await this.callTool('exportFile', {
450
- path: args.path,
451
- uploadUrl: uploadUrlResult.uploadUrl,
452
- });
453
-
454
- // Check if the sandbox upload was successful
455
- const uploadSuccess = result.success && result.result?.success !== false;
456
- const fileSize = result.result?.size;
457
- const mimeType = result.result?.mimeType;
458
- const fileContent = result.result?.content;
459
-
460
446
  const state: ExportFileState = {
461
- content: fileContent,
462
- downloadUrl: uploadSuccess ? uploadUrlResult.downloadUrl : '',
463
- filename,
464
- mimeType,
447
+ downloadUrl: result.success && result.url ? result.url : '',
448
+ fileId: result.fileId,
449
+ filename: result.filename,
450
+ mimeType: result.mimeType,
465
451
  path: args.path,
466
- size: fileSize,
467
- success: uploadSuccess,
452
+ size: result.size,
453
+ success: result.success,
468
454
  };
469
- if (!uploadSuccess) {
455
+
456
+ if (!result.success) {
470
457
  return {
471
458
  content: JSON.stringify({
472
- error: result.result?.error || 'Failed to upload file from sandbox',
459
+ error: result.error?.message || 'Failed to export file from sandbox',
473
460
  filename,
474
461
  success: false,
475
462
  }),
@@ -479,7 +466,7 @@ export class CloudSandboxExecutionRuntime {
479
466
  }
480
467
 
481
468
  return {
482
- content: `File exported successfully.\n\nFilename: ${filename}\nDownload URL: ${uploadUrlResult.downloadUrl}`,
469
+ content: `File exported successfully.\n\nFilename: ${filename}\nDownload URL: ${result.url}`,
483
470
  state,
484
471
  success: true,
485
472
  };
@@ -90,10 +90,10 @@ export interface GlobFilesState {
90
90
  }
91
91
 
92
92
  export interface ExportFileState {
93
- /** File content for text files (only when mimeType is text-like and size <= 1MB) */
94
- content?: string;
95
- /** The download URL for the exported file */
93
+ /** The download URL for the exported file (permanent /f/:id URL) */
96
94
  downloadUrl: string;
95
+ /** The file ID in database (returned from server) */
96
+ fileId?: string;
97
97
  /** The exported file name */
98
98
  filename: string;
99
99
  /** The MIME type of the file */
@@ -1,6 +1,6 @@
1
1
  import { HotkeyEnum, getHotkeyById } from '@lobehub/editor';
2
2
  import { FloatActions } from '@lobehub/editor/react';
3
- import { type ChatInputActionsProps, CodeLanguageSelect } from '@lobehub/editor/react';
3
+ import { type ChatInputActionsProps } from '@lobehub/editor/react';
4
4
  import {
5
5
  BoldIcon,
6
6
  CodeXmlIcon,
@@ -116,16 +116,6 @@ const TypoBar = memo(() => {
116
116
  label: t('typobar.codeblock'),
117
117
  onClick: editorState.codeblock,
118
118
  },
119
- editorState.isCodeblock && {
120
- children: (
121
- <CodeLanguageSelect
122
- onSelect={(value) => editorState.updateCodeblockLang(value)}
123
- value={editorState.codeblockLang}
124
- />
125
- ),
126
- disabled: !editorState.isCodeblock,
127
- key: 'codeblockLang',
128
- },
129
119
  ].filter(Boolean) as ChatInputActionsProps['items'];
130
120
  }, [editorState, t]);
131
121
 
@@ -1,6 +1,6 @@
1
1
  import { HotkeyEnum, getHotkeyById } from '@lobehub/editor';
2
2
  import { FloatActions } from '@lobehub/editor/react';
3
- import { type ChatInputActionsProps, CodeLanguageSelect } from '@lobehub/editor/react';
3
+ import { type ChatInputActionsProps } from '@lobehub/editor/react';
4
4
  import {
5
5
  BoldIcon,
6
6
  CodeXmlIcon,
@@ -116,16 +116,6 @@ const TypoBar = memo(() => {
116
116
  label: t('typobar.codeblock'),
117
117
  onClick: editorState.codeblock,
118
118
  },
119
- editorState.isCodeblock && {
120
- children: (
121
- <CodeLanguageSelect
122
- onSelect={(value) => editorState.updateCodeblockLang(value)}
123
- value={editorState.codeblockLang}
124
- />
125
- ),
126
- disabled: !editorState.isCodeblock,
127
- key: 'codeblockLang',
128
- },
129
119
  ].filter(Boolean) as ChatInputActionsProps['items'];
130
120
  }, [editorState, t]);
131
121
 
@@ -1,66 +1,16 @@
1
- import {
2
- ReactCodePlugin,
3
- ReactCodemirrorPlugin,
4
- ReactHRPlugin,
5
- ReactLinkHighlightPlugin,
6
- ReactListPlugin,
7
- ReactMathPlugin,
8
- ReactTablePlugin,
9
- } from '@lobehub/editor';
10
- import { Editor, useEditor } from '@lobehub/editor/react';
11
- import { Flexbox, Modal } from '@lobehub/ui';
12
- import { memo, useMemo, useState } from 'react';
13
- import { useTranslation } from 'react-i18next';
1
+ import { memo } from 'react';
14
2
 
15
- import { useUserStore } from '@/store/user';
16
- import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
3
+ import { EditorModal } from '@/features/EditorModal';
17
4
  import { useUserMemoryStore } from '@/store/userMemory';
18
5
  import { LayersEnum } from '@/types/userMemory';
19
6
 
20
- import TypoBar from './Typobar';
21
-
22
7
  const EditableModal = memo(() => {
23
- const { t } = useTranslation('common');
24
- const editor = useEditor();
25
- const [confirmLoading, setConfirmLoading] = useState(false);
26
8
  const editingMemoryId = useUserMemoryStore((s) => s.editingMemoryId);
27
9
  const editingMemoryContent = useUserMemoryStore((s) => s.editingMemoryContent);
28
10
  const editingMemoryLayer = useUserMemoryStore((s) => s.editingMemoryLayer);
29
11
  const clearEditingMemory = useUserMemoryStore((s) => s.clearEditingMemory);
30
12
  const updateMemory = useUserMemoryStore((s) => s.updateMemory);
31
13
 
32
- const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
33
-
34
- const richRenderProps = useMemo(
35
- () =>
36
- !enableRichRender
37
- ? {
38
- enablePasteMarkdown: false,
39
- markdownOption: {
40
- bold: false,
41
- code: false,
42
- header: false,
43
- italic: false,
44
- quote: false,
45
- strikethrough: false,
46
- underline: false,
47
- underlineStrikethrough: false,
48
- },
49
- }
50
- : {
51
- plugins: [
52
- ReactListPlugin,
53
- ReactCodePlugin,
54
- ReactCodemirrorPlugin,
55
- ReactHRPlugin,
56
- ReactLinkHighlightPlugin,
57
- ReactTablePlugin,
58
- ReactMathPlugin,
59
- ],
60
- },
61
- [enableRichRender],
62
- );
63
-
64
14
  const layerMap = {
65
15
  context: LayersEnum.Context,
66
16
  experience: LayersEnum.Experience,
@@ -69,58 +19,15 @@ const EditableModal = memo(() => {
69
19
  };
70
20
 
71
21
  return (
72
- <Modal
73
- cancelText={t('cancel')}
74
- closable={false}
75
- confirmLoading={confirmLoading}
76
- destroyOnHidden
77
- okText={t('ok')}
22
+ <EditorModal
78
23
  onCancel={clearEditingMemory}
79
- onOk={async () => {
80
- if (!editor || !editingMemoryId || !editingMemoryLayer) return;
81
- setConfirmLoading(true);
82
- const newValue = editor.getDocument('markdown') as unknown as string;
83
- await updateMemory(editingMemoryId, newValue, layerMap[editingMemoryLayer]);
84
- setConfirmLoading(false);
24
+ onConfirm={async (value) => {
25
+ if (!editingMemoryId || !editingMemoryLayer) return;
26
+ await updateMemory(editingMemoryId, value, layerMap[editingMemoryLayer]);
85
27
  }}
86
28
  open={!!editingMemoryId}
87
- styles={{
88
- body: {
89
- overflow: 'hidden',
90
- padding: 0,
91
- },
92
- }}
93
- title={null}
94
- width={'min(90vw, 960px)'}
95
- >
96
- <TypoBar editor={editor} />
97
- <Flexbox
98
- onClick={() => {
99
- editor.focus();
100
- }}
101
- paddingBlock={16}
102
- paddingInline={48}
103
- style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
104
- >
105
- <Editor
106
- autoFocus
107
- content={''}
108
- editor={editor}
109
- onInit={(editor) => {
110
- if (!editor) return;
111
- try {
112
- editor?.setDocument('markdown', editingMemoryContent);
113
- } catch {}
114
- }}
115
- style={{
116
- paddingBottom: 120,
117
- }}
118
- type={'text'}
119
- variant={'chat'}
120
- {...richRenderProps}
121
- />
122
- </Flexbox>
123
- </Modal>
29
+ value={editingMemoryContent}
30
+ />
124
31
  );
125
32
  });
126
33
 
@@ -99,6 +99,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
99
99
  underline: false,
100
100
  underlineStrikethrough: false,
101
101
  },
102
+ plugins: [ReactCodemirrorPlugin],
102
103
  }
103
104
  : {
104
105
  plugins: [
@@ -4,7 +4,6 @@ import {
4
4
  ChatInputActionBar,
5
5
  ChatInputActions,
6
6
  type ChatInputActionsProps,
7
- CodeLanguageSelect,
8
7
  } from '@lobehub/editor/react';
9
8
  import { cssVar } from 'antd-style';
10
9
  import {
@@ -122,16 +121,6 @@ const TypoBar = memo(() => {
122
121
  label: t('typobar.codeblock'),
123
122
  onClick: editorState.codeblock,
124
123
  },
125
- editorState.isCodeblock && {
126
- children: (
127
- <CodeLanguageSelect
128
- onSelect={(value) => editorState.updateCodeblockLang(value)}
129
- value={editorState.codeblockLang}
130
- />
131
- ),
132
- disabled: !editorState.isCodeblock,
133
- key: 'codeblockLang',
134
- },
135
124
  ].filter(Boolean) as ChatInputActionsProps['items'],
136
125
  [editorState],
137
126
  );
@@ -7,7 +7,10 @@ import { useConversationStore } from '@/features/Conversation/store';
7
7
 
8
8
  import { type ChatItemProps } from '../../type';
9
9
 
10
- const EditableModal = dynamic(() => import('./EditableModal'), { ssr: false });
10
+ const EditorModal = dynamic(
11
+ () => import('@/features/EditorModal').then((mode) => mode.EditorModal),
12
+ { ssr: false },
13
+ );
11
14
 
12
15
  export const MSG_CONTENT_CLASSNAME = 'msg_content_flag';
13
16
 
@@ -60,13 +63,6 @@ const MessageContent = memo<MessageContentProps>(
60
63
  s.updateMessageContent,
61
64
  ]);
62
65
 
63
- const onChange = useCallback(
64
- (value: string) => {
65
- updateMessageContent(id, value);
66
- },
67
- [id, updateMessageContent],
68
- );
69
-
70
66
  const onEditingChange = useCallback(
71
67
  (edit: boolean) => toggleMessageEditing(id, edit),
72
68
  [id, toggleMessageEditing],
@@ -90,10 +86,13 @@ const MessageContent = memo<MessageContentProps>(
90
86
  </Flexbox>
91
87
  <Suspense fallback={null}>
92
88
  {editing && (
93
- <EditableModal
94
- editing={editing}
95
- onChange={onChange}
96
- onEditingChange={onEditingChange}
89
+ <EditorModal
90
+ onCancel={() => onEditingChange(false)}
91
+ onConfirm={async (value) => {
92
+ await updateMessageContent(id, value);
93
+ onEditingChange(false);
94
+ }}
95
+ open={editing}
97
96
  value={message ? String(message) : ''}
98
97
  />
99
98
  )}
@@ -0,0 +1,81 @@
1
+ import {
2
+ ReactCodePlugin,
3
+ ReactCodemirrorPlugin,
4
+ ReactHRPlugin,
5
+ ReactLinkPlugin,
6
+ ReactListPlugin,
7
+ ReactMathPlugin,
8
+ ReactTablePlugin,
9
+ } from '@lobehub/editor';
10
+ import { Editor, useEditor } from '@lobehub/editor/react';
11
+ import { Flexbox } from '@lobehub/ui';
12
+ import { FC } from 'react';
13
+
14
+ import TypoBar from './Typobar';
15
+
16
+ interface EditorCanvasProps {
17
+ onChange?: (value: string) => void;
18
+ value?: string;
19
+ }
20
+
21
+ const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
22
+ const editor = useEditor();
23
+ return (
24
+ <>
25
+ <TypoBar editor={editor} />
26
+ <Flexbox
27
+ onClick={() => {
28
+ editor?.focus();
29
+ }}
30
+ padding={16}
31
+ style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
32
+ >
33
+ <div
34
+ onClick={(e) => {
35
+ e.stopPropagation();
36
+ e.preventDefault();
37
+ }}
38
+ >
39
+ <Editor
40
+ autoFocus
41
+ content={''}
42
+ editor={editor}
43
+ onInit={(editor) => {
44
+ if (!editor || !value) return;
45
+ try {
46
+ editor?.setDocument('markdown', value);
47
+ } catch (e) {
48
+ console.error('setDocument error:', e);
49
+ }
50
+ }}
51
+ onTextChange={(editor) => {
52
+ try {
53
+ const newValue = editor.getDocument('markdown') as unknown as string;
54
+ onChange?.(newValue);
55
+ } catch (e) {
56
+ console.error('getDocument error:', e);
57
+ onChange?.('');
58
+ }
59
+ }}
60
+ plugins={[
61
+ ReactListPlugin,
62
+ ReactCodePlugin,
63
+ ReactCodemirrorPlugin,
64
+ ReactHRPlugin,
65
+ ReactLinkPlugin,
66
+ ReactTablePlugin,
67
+ ReactMathPlugin,
68
+ ]}
69
+ style={{
70
+ paddingBottom: 120,
71
+ }}
72
+ type={'text'}
73
+ variant={'chat'}
74
+ />
75
+ </div>
76
+ </Flexbox>
77
+ </>
78
+ );
79
+ };
80
+
81
+ export default EditorCanvas;
@@ -0,0 +1,28 @@
1
+ import { TextArea } from '@lobehub/ui';
2
+ import { FC } from 'react';
3
+
4
+ interface EditorCanvasProps {
5
+ onChange?: (value: string) => void;
6
+ value?: string;
7
+ }
8
+
9
+ const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
10
+ return (
11
+ <TextArea
12
+ onChange={(e) => {
13
+ onChange?.(e.target.value);
14
+ }}
15
+ style={{
16
+ cursor: 'text',
17
+ maxHeight: '80vh',
18
+ minHeight: '50vh',
19
+ overflowY: 'auto',
20
+ padding: 16,
21
+ }}
22
+ value={value}
23
+ variant={'borderless'}
24
+ />
25
+ );
26
+ };
27
+
28
+ export default EditorCanvas;
@@ -4,7 +4,6 @@ import {
4
4
  ChatInputActionBar,
5
5
  ChatInputActions,
6
6
  type ChatInputActionsProps,
7
- CodeLanguageSelect,
8
7
  } from '@lobehub/editor/react';
9
8
  import { cssVar } from 'antd-style';
10
9
  import {
@@ -119,16 +118,6 @@ const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
119
118
  label: t('typobar.codeblock'),
120
119
  onClick: editorState.codeblock,
121
120
  },
122
- editorState.isCodeblock && {
123
- children: (
124
- <CodeLanguageSelect
125
- onSelect={(value) => editorState.updateCodeblockLang(value)}
126
- value={editorState.codeblockLang}
127
- />
128
- ),
129
- disabled: !editorState.isCodeblock,
130
- key: 'codeblockLang',
131
- },
132
121
  ].filter(Boolean) as ChatInputActionsProps['items'],
133
122
  [editorState],
134
123
  );
@@ -0,0 +1,51 @@
1
+ import { Modal, ModalProps, createRawModal } from '@lobehub/ui';
2
+ import { memo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { useUserStore } from '@/store/user';
6
+ import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
7
+
8
+ import EditorCanvas from './EditorCanvas';
9
+ import TextareCanvas from './TextareCanvas';
10
+
11
+ interface EditorModalProps extends ModalProps {
12
+ onConfirm?: (value: string) => Promise<void>;
13
+ value?: string;
14
+ }
15
+
16
+ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }) => {
17
+ const [confirmLoading, setConfirmLoading] = useState(false);
18
+ const { t } = useTranslation('common');
19
+ const [v, setV] = useState(value);
20
+ const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
21
+
22
+ const Canvas = enableRichRender ? EditorCanvas : TextareCanvas;
23
+
24
+ return (
25
+ <Modal
26
+ cancelText={t('cancel')}
27
+ closable={false}
28
+ confirmLoading={confirmLoading}
29
+ destroyOnHidden
30
+ okText={t('ok')}
31
+ onOk={async () => {
32
+ setConfirmLoading(true);
33
+ await onConfirm?.(v || '');
34
+ setConfirmLoading(false);
35
+ }}
36
+ styles={{
37
+ body: {
38
+ overflow: 'hidden',
39
+ padding: 0,
40
+ },
41
+ }}
42
+ title={null}
43
+ width={'min(90vw, 920px)'}
44
+ {...rest}
45
+ >
46
+ <Canvas onChange={(v) => setV(v)} value={v} />
47
+ </Modal>
48
+ );
49
+ });
50
+
51
+ export const createEditorModal = (props: EditorModalProps) => createRawModal(EditorModal, props);
@@ -7,12 +7,7 @@ import {
7
7
  INSERT_HEADING_COMMAND,
8
8
  getHotkeyById,
9
9
  } from '@lobehub/editor';
10
- import {
11
- ChatInputActions,
12
- type ChatInputActionsProps,
13
- CodeLanguageSelect,
14
- FloatActions,
15
- } from '@lobehub/editor/react';
10
+ import { ChatInputActions, type ChatInputActionsProps, FloatActions } from '@lobehub/editor/react';
16
11
  import { Block } from '@lobehub/ui';
17
12
  import { createStaticStyles, cssVar } from 'antd-style';
18
13
  import {
@@ -283,17 +278,6 @@ const TypoBar = memo<ToolbarProps>(({ floating, style, className }) => {
283
278
  label: t('typobar.codeblock'),
284
279
  onClick: editorState.codeblock,
285
280
  },
286
- !floating &&
287
- editorState.isCodeblock && {
288
- children: (
289
- <CodeLanguageSelect
290
- onSelect={(value) => editorState.updateCodeblockLang(value)}
291
- value={editorState.codeblockLang}
292
- />
293
- ),
294
- disabled: !editorState.isCodeblock,
295
- key: 'codeblockLang',
296
- },
297
281
  ];
298
282
 
299
283
  return baseItems.filter(Boolean) as ChatInputActionsProps['items'];