feat: add notes export (#10488)

* feat: add notes export

* chore: fix lint error

* feat: unified export interface for notes

* fix: hide export reasoning when exporting notes

* chore: fix lint error

* chore: remove debug log
This commit is contained in:
Tristan Zhang 2025-10-02 15:09:11 +08:00 committed by GitHub
parent 74db4c4646
commit f91e7da0a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 9 deletions

View File

@ -38,6 +38,7 @@ interface PopupContainerProps {
message?: Message
messages?: Message[]
topic?: Topic
rawContent?: string
}
// 转换文件信息数组为树形结构
@ -140,7 +141,8 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
resolve,
message,
messages,
topic
topic,
rawContent
}) => {
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
const [state, setState] = useState({
@ -229,7 +231,9 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
return
}
let markdown = ''
if (topic) {
if (rawContent) {
markdown = rawContent
} else if (topic) {
markdown = await topicToMarkdown(topic, exportReasoning)
} else if (messages && messages.length > 0) {
markdown = messagesToMarkdown(messages, exportReasoning)
@ -299,7 +303,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
}
}
}
return (
<Modal
title={i18n.t('chat.topics.export.obsidian_atributes')}
@ -410,9 +413,11 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
</Option>
</Select>
</Form.Item>
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
<Switch checked={exportReasoning} onChange={setExportReasoning} />
</Form.Item>
{!rawContent && (
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
<Switch checked={exportReasoning} onChange={setExportReasoning} />
</Form.Item>
)}
</Form>
</Modal>
)

View File

@ -9,6 +9,7 @@ interface ObsidianExportOptions {
topic?: Topic
message?: Message
messages?: Message[]
rawContent?: string
}
export default class ObsidianExportPopup {
@ -24,6 +25,7 @@ export default class ObsidianExportPopup {
topic={options.topic}
message={options.message}
messages={options.messages}
rawContent={options.rawContent}
obsidianTags={''}
open={true}
resolve={(v) => {

View File

@ -6,11 +6,13 @@ import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useActiveNode } from '@renderer/hooks/useNotesQuery'
import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader'
import { useAppSelector } from '@renderer/store'
import { RootState, useAppSelector } from '@renderer/store'
import { selectSortType } from '@renderer/store/note'
import { NotesSortType, NotesTreeNode } from '@renderer/types/note'
import { exportNote } from '@renderer/utils/export'
import { useVirtualizer } from '@tanstack/react-virtual'
import { Dropdown, Input, InputRef, MenuProps } from 'antd'
import { ItemType, MenuItemType } from 'antd/es/menu/interface'
import {
ChevronDown,
ChevronRight,
@ -21,10 +23,12 @@ import {
Folder,
FolderOpen,
Star,
StarOff
StarOff,
UploadIcon
} from 'lucide-react'
import { FC, memo, Ref, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
interface NotesSidebarProps {
@ -213,6 +217,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const { bases } = useKnowledgeBases()
const { activeNode } = useActiveNode(notesTree)
const sortType = useAppSelector(selectSortType)
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
const [editingNodeId, setEditingNodeId] = useState<string | null>(null)
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null)
const [dragOverNodeId, setDragOverNodeId] = useState<string | null>(null)
@ -525,6 +530,48 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onClick: () => {
handleExportKnowledge(node)
}
},
{
label: t('chat.topics.export.title'),
key: 'export',
icon: <UploadIcon size={14} />,
children: [
exportMenuOptions.markdown && {
label: t('chat.topics.export.md.label'),
key: 'markdown',
onClick: () => exportNote({ node, platform: 'markdown' })
},
exportMenuOptions.docx && {
label: t('chat.topics.export.word'),
key: 'word',
onClick: () => exportNote({ node, platform: 'docx' })
},
exportMenuOptions.notion && {
label: t('chat.topics.export.notion'),
key: 'notion',
onClick: () => exportNote({ node, platform: 'notion' })
},
exportMenuOptions.yuque && {
label: t('chat.topics.export.yuque'),
key: 'yuque',
onClick: () => exportNote({ node, platform: 'yuque' })
},
exportMenuOptions.obsidian && {
label: t('chat.topics.export.obsidian'),
key: 'obsidian',
onClick: () => exportNote({ node, platform: 'obsidian' })
},
exportMenuOptions.joplin && {
label: t('chat.topics.export.joplin'),
key: 'joplin',
onClick: () => exportNote({ node, platform: 'joplin' })
},
exportMenuOptions.siyuan && {
label: t('chat.topics.export.siyuan'),
key: 'siyuan',
onClick: () => exportNote({ node, platform: 'siyuan' })
}
].filter(Boolean) as ItemType<MenuItemType>[]
}
)
}
@ -543,7 +590,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
return baseMenuItems
},
[t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode]
[t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode, exportMenuOptions]
)
const handleDropFiles = useCallback(

View File

@ -1082,3 +1082,51 @@ export const exportTopicToNotes = async (topic: Topic, folderPath: string): Prom
throw error
}
}
const exportNoteAsMarkdown = async (noteName: string, content: string): Promise<void> => {
const markdown = `# ${noteName}\n\n${content}`
const fileName = removeSpecialCharactersForFileName(noteName) + '.md'
const result = await window.api.file.save(fileName, markdown)
if (result) {
window.toast.success(i18n.t('message.success.markdown.export.specified'))
}
}
interface NoteExportOptions {
node: { name: string; externalPath: string }
platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan'
}
export const exportNote = async ({ node, platform }: NoteExportOptions): Promise<void> => {
try {
const content = await window.api.file.readExternal(node.externalPath)
switch (platform) {
case 'markdown':
return await exportNoteAsMarkdown(node.name, content)
case 'docx':
window.api.export.toWord(`# ${node.name}\n\n${content}`, removeSpecialCharactersForFileName(node.name))
return
case 'notion':
await exportMessageToNotion(node.name, content)
return
case 'yuque':
await exportMarkdownToYuque(node.name, `# ${node.name}\n\n${content}`)
return
case 'obsidian': {
const { default: ObsidianExportPopup } = await import('@renderer/components/Popups/ObsidianExportPopup')
await ObsidianExportPopup.show({ title: node.name, processingMethod: '1', rawContent: content })
return
}
case 'joplin':
await exportMarkdownToJoplin(node.name, content)
return
case 'siyuan':
await exportMarkdownToSiyuan(node.name, `# ${node.name}\n\n${content}`)
return
}
} catch (error) {
logger.error(`Failed to export note to ${platform}:`, error as Error)
throw error
}
}