@lobehub/lobehub 2.0.0-next.216 → 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.
Files changed (28) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +17 -0
  3. package/package.json +1 -1
  4. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +18 -31
  5. package/packages/builtin-tool-cloud-sandbox/src/types.ts +3 -3
  6. package/packages/utils/src/server/index.ts +0 -1
  7. package/src/app/[variants]/(main)/chat/profile/features/EditorCanvas/TypoBar.tsx +1 -11
  8. package/src/app/[variants]/(main)/group/profile/features/EditorCanvas/TypoBar.tsx +1 -11
  9. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +44 -0
  10. package/src/app/[variants]/(main)/memory/features/EditableModal/index.tsx +8 -101
  11. package/src/features/ChatInput/InputEditor/index.tsx +1 -0
  12. package/src/features/ChatInput/TypoBar/index.tsx +0 -11
  13. package/src/features/CommandMenu/AskAIMenu.tsx +47 -14
  14. package/src/features/Conversation/ChatItem/components/MessageContent/index.tsx +11 -12
  15. package/src/features/EditorModal/EditorCanvas.tsx +81 -0
  16. package/src/features/EditorModal/TextareCanvas.tsx +28 -0
  17. package/src/features/{Conversation/ChatItem/components/MessageContent → EditorModal}/Typobar.tsx +0 -11
  18. package/src/features/EditorModal/index.tsx +51 -0
  19. package/src/features/ModelSwitchPanel/index.tsx +21 -1
  20. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +1 -17
  21. package/src/server/routers/tools/market.ts +118 -102
  22. package/src/server/services/discover/index.ts +10 -5
  23. package/src/services/codeInterpreter.ts +12 -20
  24. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +13 -86
  25. package/packages/utils/src/server/__tests__/geo.test.ts +0 -116
  26. package/packages/utils/src/server/geo.ts +0 -60
  27. package/src/app/[variants]/(main)/memory/features/EditableModal/Typobar.tsx +0 -150
  28. package/src/features/Conversation/ChatItem/components/MessageContent/EditableModal.tsx +0 -119
@@ -9,63 +9,20 @@ import { type StateCreator } from 'zustand/vanilla';
9
9
 
10
10
  import { type MCPToolCallResult } from '@/libs/mcp';
11
11
  import { chatService } from '@/services/chat';
12
- import { codeInterpreterService } from '@/services/codeInterpreter';
13
- import { fileService } from '@/services/file';
14
12
  import { mcpService } from '@/services/mcp';
15
13
  import { messageService } from '@/services/message';
16
14
  import { AI_RUNTIME_OPERATION_TYPES } from '@/store/chat/slices/operation';
17
15
  import { type ChatStore } from '@/store/chat/store';
18
16
  import { useToolStore } from '@/store/tool';
19
17
  import { hasExecutor } from '@/store/tool/slices/builtin/executors';
18
+ import { useUserStore } from '@/store/user';
19
+ import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
20
20
  import { safeParseJSON } from '@/utils/safeParseJSON';
21
21
 
22
22
  import { dbMessageSelectors } from '../../message/selectors';
23
23
 
24
24
  const log = debug('lobe-store:plugin-types');
25
25
 
26
- /**
27
- * Get MIME type from filename extension
28
- */
29
- const getMimeTypeFromFilename = (filename: string): string => {
30
- const ext = filename.split('.').pop()?.toLowerCase() || '';
31
- const mimeTypes: Record<string, string> = {
32
- // Images
33
- bmp: 'image/bmp',
34
- gif: 'image/gif',
35
- jpeg: 'image/jpeg',
36
- jpg: 'image/jpeg',
37
- png: 'image/png',
38
- svg: 'image/svg+xml',
39
- webp: 'image/webp',
40
- // Videos
41
- mp4: 'video/mp4',
42
- webm: 'video/webm',
43
- mov: 'video/quicktime',
44
- avi: 'video/x-msvideo',
45
- // Documents
46
- csv: 'text/csv',
47
- doc: 'application/msword',
48
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
49
- html: 'text/html',
50
- json: 'application/json',
51
- md: 'text/markdown',
52
- pdf: 'application/pdf',
53
- ppt: 'application/vnd.ms-powerpoint',
54
- pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
55
- rtf: 'application/rtf',
56
- txt: 'text/plain',
57
- xls: 'application/vnd.ms-excel',
58
- xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
59
- xml: 'application/xml',
60
- // Code
61
- css: 'text/css',
62
- js: 'text/javascript',
63
- py: 'text/x-python',
64
- ts: 'text/typescript',
65
- };
66
- return mimeTypes[ext] || 'application/octet-stream';
67
- };
68
-
69
26
  /**
70
27
  * Plugin type-specific implementations
71
28
  * Each method handles a specific type of plugin invocation
@@ -284,10 +241,13 @@ export const pluginTypes: StateCreator<
284
241
  const { CloudSandboxExecutionRuntime } =
285
242
  await import('@lobechat/builtin-tool-cloud-sandbox/executionRuntime');
286
243
 
244
+ // Get userId from user store
245
+ const userId = userProfileSelectors.userId(useUserStore.getState()) || 'anonymous';
246
+
287
247
  // Create runtime with context
288
248
  const runtime = new CloudSandboxExecutionRuntime({
289
249
  topicId: message?.topicId || 'default',
290
- userId: 'current-user', // TODO: Get actual userId from auth context
250
+ userId,
291
251
  });
292
252
 
293
253
  // Parse arguments
@@ -341,58 +301,25 @@ export const pluginTypes: StateCreator<
341
301
  context,
342
302
  );
343
303
 
344
- // Handle exportFile: save exported file and associate with assistant message (parent)
304
+ // Handle exportFile: associate the file (already created by server) with assistant message (parent)
345
305
  if (payload.apiName === 'exportFile' && data.success && data.state) {
346
306
  const exportState = data.state as ExportFileState;
347
- if (exportState.downloadUrl && exportState.filename) {
307
+ // Server now creates the file record and returns fileId in the response
308
+ if (exportState.fileId && exportState.filename) {
348
309
  try {
349
- // Generate a hash from the URL path (without query params) for deduplication
350
- // Extract the path before query params: .../code-interpreter-exports/tpc_xxx/filename.ext
351
- const urlPath = exportState.downloadUrl.split('?')[0];
352
- const hash = `ci-export-${btoa(urlPath).slice(0, 32)}`;
353
-
354
- // Use mimeType from state if available, otherwise infer from filename
355
- const mimeType = exportState.mimeType || getMimeTypeFromFilename(exportState.filename);
356
-
357
- // 1. Create file record in database
358
- const fileResult = await fileService.createFile({
359
- fileType: mimeType,
360
- hash,
361
- name: exportState.filename,
362
- size: exportState.size || 0,
363
- source: 'code-interpreter',
364
- url: exportState.downloadUrl,
365
- });
366
-
367
- // 2. If there's text content, save it to documents table for retrieval
368
- if (exportState.content) {
369
- await codeInterpreterService.saveExportedFileContent({
370
- content: exportState.content,
371
- fileId: fileResult.id,
372
- fileType: mimeType,
373
- filename: exportState.filename,
374
- url: exportState.downloadUrl,
375
- });
376
-
377
- log(
378
- '[invokeCloudCodeInterpreterTool] Saved file content to document: fileId=%s',
379
- fileResult.id,
380
- );
381
- }
382
-
383
- // 3. Associate file with the assistant message (parent of tool message)
310
+ // Associate file with the assistant message (parent of tool message)
384
311
  // The current message (id) is the tool message, we need to attach to its parent
385
312
  const targetMessageId = message?.parentId || id;
386
313
 
387
- await messageService.addFilesToMessage(targetMessageId, [fileResult.id], {
314
+ await messageService.addFilesToMessage(targetMessageId, [exportState.fileId], {
388
315
  agentId: message?.agentId,
389
316
  topicId: message?.topicId,
390
317
  });
391
318
 
392
319
  log(
393
- '[invokeCloudCodeInterpreterTool] Saved exported file: targetMessageId=%s, fileId=%s, filename=%s',
320
+ '[invokeCloudCodeInterpreterTool] Associated exported file with message: targetMessageId=%s, fileId=%s, filename=%s',
394
321
  targetMessageId,
395
- fileResult.id,
322
+ exportState.fileId,
396
323
  exportState.filename,
397
324
  );
398
325
  } catch (error) {
@@ -1,116 +0,0 @@
1
- import { geolocation } from '@vercel/functions';
2
- import { getCountry } from 'countries-and-timezones';
3
- import { NextRequest } from 'next/server';
4
- import { afterEach, describe, expect, it, vi } from 'vitest';
5
-
6
- import { parseDefaultThemeFromCountry } from '../geo';
7
-
8
- vi.mock('@vercel/functions', () => ({
9
- geolocation: vi.fn(),
10
- }));
11
-
12
- vi.mock('countries-and-timezones', () => ({
13
- getCountry: vi.fn(),
14
- }));
15
-
16
- describe('parseDefaultThemeFromCountry', () => {
17
- const mockRequest = (headers: Record<string, string> = {}) => {
18
- return {
19
- headers: {
20
- get: (key: string) => headers[key],
21
- },
22
- } as NextRequest;
23
- };
24
-
25
- it('should return light theme when no country code is found', () => {
26
- vi.mocked(geolocation).mockReturnValue({});
27
- const request = mockRequest();
28
- expect(parseDefaultThemeFromCountry(request)).toBe('light');
29
- });
30
-
31
- it('should return light theme when country has no timezone', () => {
32
- vi.mocked(geolocation).mockReturnValue({ country: 'US' });
33
- vi.mocked(getCountry).mockReturnValue({
34
- id: 'US',
35
- name: 'United States',
36
- timezones: [],
37
- });
38
- const request = mockRequest();
39
- expect(parseDefaultThemeFromCountry(request)).toBe('light');
40
- });
41
-
42
- it('should return light theme when country has invalid timezone', () => {
43
- vi.mocked(geolocation).mockReturnValue({ country: 'US' });
44
- vi.mocked(getCountry).mockReturnValue({
45
- id: 'US',
46
- name: 'United States',
47
- // @ts-ignore
48
- timezones: ['America/Invalid'],
49
- });
50
-
51
- const mockDate = new Date('2025-04-01T12:00:00');
52
- vi.setSystemTime(mockDate);
53
-
54
- const request = mockRequest();
55
- expect(parseDefaultThemeFromCountry(request)).toBe('light');
56
- });
57
-
58
- it('should return light theme during daytime hours', () => {
59
- vi.mocked(geolocation).mockReturnValue({ country: 'US' });
60
- vi.mocked(getCountry).mockReturnValue({
61
- id: 'US',
62
- name: 'United States',
63
- timezones: ['America/New_York'],
64
- });
65
-
66
- // 设置UTC时间16:00,这样在纽约时区(EDT,UTC-4)就是12:00
67
- const mockDate = new Date('2025-04-01T16:00:00.000Z');
68
- vi.setSystemTime(mockDate);
69
-
70
- const request = mockRequest();
71
- const result = parseDefaultThemeFromCountry(request);
72
- expect(result).toBe('light');
73
- });
74
-
75
- it('should return dark theme during night hours', () => {
76
- vi.mocked(geolocation).mockReturnValue({ country: 'US' });
77
- vi.mocked(getCountry).mockReturnValue({
78
- id: 'US',
79
- name: 'United States',
80
- timezones: ['America/New_York'],
81
- });
82
-
83
- // 设置UTC时间02:00,这样在纽约时区(EDT,UTC-4)就是22:00
84
- const mockDate = new Date('2025-04-01T02:00:00.000Z');
85
- vi.setSystemTime(mockDate);
86
-
87
- const request = mockRequest();
88
- expect(parseDefaultThemeFromCountry(request)).toBe('dark');
89
- });
90
-
91
- it('should try different header sources for country code', () => {
92
- vi.mocked(geolocation).mockReturnValue({});
93
- vi.mocked(getCountry).mockReturnValue({
94
- id: 'US',
95
- name: 'United States',
96
- timezones: ['America/New_York'],
97
- });
98
-
99
- const headers = {
100
- 'x-vercel-ip-country': 'US',
101
- 'cf-ipcountry': 'CA',
102
- 'x-zeabur-ip-country': 'UK',
103
- 'x-country-code': 'FR',
104
- };
105
-
106
- const request = mockRequest(headers);
107
- parseDefaultThemeFromCountry(request);
108
-
109
- expect(getCountry).toHaveBeenCalledWith('US');
110
- });
111
-
112
- afterEach(() => {
113
- vi.useRealTimers();
114
- vi.clearAllMocks();
115
- });
116
- });
@@ -1,60 +0,0 @@
1
- import { geolocation } from '@vercel/functions';
2
- import { getCountry } from 'countries-and-timezones';
3
- import { NextRequest } from 'next/server';
4
-
5
- const getLocalTime = (timeZone: string) => {
6
- return new Date().toLocaleString('en-US', {
7
- hour: 'numeric',
8
- hour12: false,
9
- timeZone,
10
- });
11
- };
12
-
13
- const isValidTimeZone = (timeZone: string) => {
14
- try {
15
- getLocalTime(timeZone);
16
- return true; // If no exception is thrown, the timezone is valid
17
- } catch (e) {
18
- // If a RangeError is caught, the timezone is invalid
19
- if (e instanceof RangeError) {
20
- return false;
21
- }
22
- // If it's another error, better to re-throw it
23
- throw e;
24
- }
25
- };
26
-
27
- export const parseDefaultThemeFromCountry = (request: NextRequest) => {
28
- // 1. Get country code from request headers
29
- const geo = geolocation(request);
30
-
31
- const countryCode =
32
- geo?.country ||
33
- request.headers.get('x-vercel-ip-country') || // Vercel
34
- request.headers.get('cf-ipcountry') || // Cloudflare
35
- request.headers.get('x-zeabur-ip-country') || // Zeabur
36
- request.headers.get('x-country-code'); // Netlify
37
-
38
- // If no country code is obtained, return light theme directly
39
- if (!countryCode) return 'light';
40
-
41
- // 2. Get timezone information for the country
42
- const country = getCountry(countryCode);
43
-
44
- // If country information is not found or the country has no timezone information, return light theme
45
- if (!country?.timezones?.length) return 'light';
46
-
47
- const timeZone = country.timezones.find((tz) => isValidTimeZone(tz));
48
- if (!timeZone) return 'light';
49
-
50
- // 3. Get the current time in the country's first timezone
51
- const localTime = getLocalTime(timeZone);
52
-
53
- // 4. Parse the hour and determine the theme
54
- const localHour = parseInt(localTime);
55
- // console.log(
56
- // `[theme] Country: ${countryCode}, Timezone: ${country.timezones[0]}, LocalHour: ${localHour}`,
57
- // );
58
-
59
- return localHour >= 6 && localHour < 18 ? 'light' : 'dark';
60
- };
@@ -1,150 +0,0 @@
1
- import { HotkeyEnum, type IEditor, getHotkeyById } from '@lobehub/editor';
2
- import { useEditorState } from '@lobehub/editor/react';
3
- import {
4
- ChatInputActionBar,
5
- ChatInputActions,
6
- type ChatInputActionsProps,
7
- CodeLanguageSelect,
8
- } from '@lobehub/editor/react';
9
- import { cssVar } from 'antd-style';
10
- import {
11
- BoldIcon,
12
- CodeXmlIcon,
13
- ItalicIcon,
14
- ListIcon,
15
- ListOrderedIcon,
16
- ListTodoIcon,
17
- MessageSquareQuote,
18
- SigmaIcon,
19
- SquareDashedBottomCodeIcon,
20
- StrikethroughIcon,
21
- UnderlineIcon,
22
- } from 'lucide-react';
23
- import { memo, useMemo } from 'react';
24
- import { useTranslation } from 'react-i18next';
25
-
26
- const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
27
- const { t } = useTranslation('editor');
28
- const editorState = useEditorState(editor);
29
-
30
- const items: ChatInputActionsProps['items'] = useMemo(
31
- () =>
32
- [
33
- {
34
- active: editorState.isBold,
35
- icon: BoldIcon,
36
- key: 'bold',
37
- label: t('typobar.bold'),
38
- onClick: editorState.bold,
39
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
40
- },
41
- {
42
- active: editorState.isItalic,
43
- icon: ItalicIcon,
44
- key: 'italic',
45
- label: t('typobar.italic'),
46
- onClick: editorState.italic,
47
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
48
- },
49
- {
50
- active: editorState.isUnderline,
51
- icon: UnderlineIcon,
52
- key: 'underline',
53
- label: t('typobar.underline'),
54
- onClick: editorState.underline,
55
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
56
- },
57
- {
58
- active: editorState.isStrikethrough,
59
- icon: StrikethroughIcon,
60
- key: 'strikethrough',
61
- label: t('typobar.strikethrough'),
62
- onClick: editorState.strikethrough,
63
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
64
- },
65
- {
66
- type: 'divider',
67
- },
68
-
69
- {
70
- icon: ListIcon,
71
- key: 'bulletList',
72
- label: t('typobar.bulletList'),
73
- onClick: editorState.bulletList,
74
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
75
- },
76
- {
77
- icon: ListOrderedIcon,
78
- key: 'numberlist',
79
- label: t('typobar.numberList'),
80
- onClick: editorState.numberList,
81
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
82
- },
83
- {
84
- icon: ListTodoIcon,
85
- key: 'tasklist',
86
- label: t('typobar.taskList'),
87
- onClick: editorState.checkList,
88
- },
89
- {
90
- type: 'divider',
91
- },
92
- {
93
- active: editorState.isBlockquote,
94
- icon: MessageSquareQuote,
95
- key: 'blockquote',
96
- label: t('typobar.blockquote'),
97
- onClick: editorState.blockquote,
98
- },
99
- {
100
- type: 'divider',
101
- },
102
- {
103
- icon: SigmaIcon,
104
- key: 'math',
105
- label: t('typobar.tex'),
106
- onClick: editorState.insertMath,
107
- },
108
- {
109
- active: editorState.isCode,
110
- icon: CodeXmlIcon,
111
- key: 'code',
112
- label: t('typobar.code'),
113
- onClick: editorState.code,
114
- tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
115
- },
116
- {
117
- icon: SquareDashedBottomCodeIcon,
118
- key: 'codeblock',
119
- label: t('typobar.codeblock'),
120
- onClick: editorState.codeblock,
121
- },
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
- ].filter(Boolean) as ChatInputActionsProps['items'],
133
- [editorState],
134
- );
135
-
136
- return (
137
- <ChatInputActionBar
138
- left={<ChatInputActions items={items} />}
139
- style={{
140
- background: cssVar.colorFillQuaternary,
141
- borderTopLeftRadius: 8,
142
- borderTopRightRadius: 8,
143
- }}
144
- />
145
- );
146
- });
147
-
148
- TypoBar.displayName = 'TypoBar';
149
-
150
- export default TypoBar;
@@ -1,119 +0,0 @@
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 } from 'react';
13
- import { useTranslation } from 'react-i18next';
14
-
15
- import { useUserStore } from '@/store/user';
16
- import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
17
-
18
- import TypoBar from './Typobar';
19
-
20
- interface EditableMessageProps {
21
- editing?: boolean;
22
- onChange?: (value: string) => void;
23
- onEditingChange?: (editing: boolean) => void;
24
- value?: string;
25
- }
26
-
27
- const EditableMessage = memo<EditableMessageProps>(
28
- ({ editing, onEditingChange, onChange, value }) => {
29
- const { t } = useTranslation('common');
30
- const editor = useEditor();
31
-
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
- return (
65
- <Modal
66
- cancelText={t('cancel')}
67
- closable={false}
68
- destroyOnHidden
69
- okText={t('ok')}
70
- onCancel={() => onEditingChange?.(false)}
71
- onOk={() => {
72
- if (!editor) return;
73
- const newValue = editor.getDocument('markdown') as unknown as string;
74
- onChange?.(newValue);
75
- onEditingChange?.(false);
76
- }}
77
- open={editing}
78
- styles={{
79
- body: {
80
- overflow: 'hidden',
81
- padding: 0,
82
- },
83
- }}
84
- title={null}
85
- width={'min(90vw, 960px)'}
86
- >
87
- <TypoBar editor={editor} />
88
- <Flexbox
89
- onClick={() => {
90
- editor.focus();
91
- }}
92
- paddingBlock={16}
93
- paddingInline={48}
94
- style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
95
- >
96
- <Editor
97
- autoFocus
98
- content={''}
99
- editor={editor}
100
- onInit={(editor) => {
101
- if (!editor) return;
102
- try {
103
- editor?.setDocument('markdown', value);
104
- } catch {}
105
- }}
106
- style={{
107
- paddingBottom: 120,
108
- }}
109
- type={'text'}
110
- variant={'chat'}
111
- {...richRenderProps}
112
- />
113
- </Flexbox>
114
- </Modal>
115
- );
116
- },
117
- );
118
-
119
- export default EditableMessage;