feat: add export to Notes feature

Introduced the ability to export messages and topics to the Notes workspace. Updated UI components, i18n strings, settings, migration logic, and export utilities to support the new export option.
This commit is contained in:
自由的世界人 2025-07-18 14:34:16 +08:00
parent 9128a8019d
commit f3fc9a23fe
7 changed files with 97 additions and 4 deletions

View File

@ -313,6 +313,7 @@
"topics.export.md.reason": "导出为 Markdown (包含思考)", "topics.export.md.reason": "导出为 Markdown (包含思考)",
"topics.export.notion": "导出到 Notion", "topics.export.notion": "导出到 Notion",
"topics.export.obsidian": "导出到 Obsidian", "topics.export.obsidian": "导出到 Obsidian",
"topics.export.notes": "导出到笔记",
"topics.export.obsidian_atributes": "配置笔记属性", "topics.export.obsidian_atributes": "配置笔记属性",
"topics.export.obsidian_btn": "确定", "topics.export.obsidian_btn": "确定",
"topics.export.obsidian_created": "创建时间", "topics.export.obsidian_created": "创建时间",
@ -713,6 +714,7 @@
"error.siyuan.no_config": "未配置思源笔记 API 地址或令牌", "error.siyuan.no_config": "未配置思源笔记 API 地址或令牌",
"error.yuque.export": "导出语雀错误,请检查连接状态并对照文档检查配置", "error.yuque.export": "导出语雀错误,请检查连接状态并对照文档检查配置",
"error.yuque.no_config": "未配置语雀 Token 或 知识库 URL", "error.yuque.no_config": "未配置语雀 Token 或 知识库 URL",
"error.notes.export": "导出笔记失败",
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答", "group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
"group.delete.title": "删除分组消息", "group.delete.title": "删除分组消息",
"ignore.knowledge.base": "联网模式开启,忽略知识库", "ignore.knowledge.base": "联网模式开启,忽略知识库",
@ -747,6 +749,7 @@
"success.notion.export": "成功导出到 Notion", "success.notion.export": "成功导出到 Notion",
"success.siyuan.export": "导出到思源笔记成功", "success.siyuan.export": "导出到思源笔记成功",
"success.yuque.export": "成功导出到语雀", "success.yuque.export": "成功导出到语雀",
"success.notes.export": "成功导出到笔记",
"switch.disabled": "请等待当前回复完成后操作", "switch.disabled": "请等待当前回复完成后操作",
"tools": { "tools": {
"abort_failed": "工具调用中断失败", "abort_failed": "工具调用中断失败",
@ -1405,7 +1408,8 @@
"plain_text": "复制为纯文本", "plain_text": "复制为纯文本",
"siyuan": "导出到思源笔记", "siyuan": "导出到思源笔记",
"title": "导出菜单设置", "title": "导出菜单设置",
"yuque": "导出到语雀" "yuque": "导出到语雀",
"notes": "导出到笔记"
}, },
"hour_interval_one": "{{count}} 小时", "hour_interval_one": "{{count}} 小时",
"hour_interval_other": "{{count}} 小时", "hour_interval_other": "{{count}} 小时",

View File

@ -23,6 +23,7 @@ import {
exportMarkdownToSiyuan, exportMarkdownToSiyuan,
exportMarkdownToYuque, exportMarkdownToYuque,
exportMessageAsMarkdown, exportMessageAsMarkdown,
exportMessageToNotes,
exportMessageToNotion, exportMessageToNotion,
messageToMarkdown messageToMarkdown
} from '@renderer/utils/export' } from '@renderer/utils/export'
@ -322,6 +323,15 @@ const MessageMenubar: FC<Props> = (props) => {
const markdown = messageToMarkdown(message) const markdown = messageToMarkdown(message)
exportMarkdownToSiyuan(title, markdown) exportMarkdownToSiyuan(title, markdown)
} }
},
exportMenuOptions.notes && {
label: t('chat.topics.export.notes'),
key: 'notes',
onClick: async () => {
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
exportMessageToNotes(title, markdown)
}
} }
].filter(Boolean) ].filter(Boolean)
} }

View File

@ -31,6 +31,7 @@ import {
exportMarkdownToSiyuan, exportMarkdownToSiyuan,
exportMarkdownToYuque, exportMarkdownToYuque,
exportTopicAsMarkdown, exportTopicAsMarkdown,
exportTopicToNotes,
exportTopicToNotion, exportTopicToNotion,
topicToMarkdown topicToMarkdown
} from '@renderer/utils/export' } from '@renderer/utils/export'
@ -374,6 +375,13 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const markdown = await topicToMarkdown(topic) const markdown = await topicToMarkdown(topic)
exportMarkdownToSiyuan(topic.name, markdown) exportMarkdownToSiyuan(topic.name, markdown)
} }
},
exportMenuOptions.notes && {
label: t('chat.topics.export.notes'),
key: 'notes',
onClick: async () => {
exportTopicToNotes(topic)
}
} }
].filter(Boolean) as ItemType<MenuItemType>[] ].filter(Boolean) as ItemType<MenuItemType>[]
} }
@ -419,6 +427,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
exportMenuOptions.obsidian, exportMenuOptions.obsidian,
exportMenuOptions.joplin, exportMenuOptions.joplin,
exportMenuOptions.siyuan, exportMenuOptions.siyuan,
exportMenuOptions.notes,
assistants, assistants,
assistant, assistant,
updateTopic, updateTopic,
@ -533,23 +542,28 @@ const TopicListItem = styled.div`
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
position: relative;
width: calc(var(--assistants-width) - 20px); width: calc(var(--assistants-width) - 20px);
.menu { .menu {
opacity: 0; opacity: 0;
color: var(--color-text-3); color: var(--color-text-3);
} }
&:hover { &:hover {
background-color: var(--color-list-item-hover); background-color: var(--color-list-item-hover);
transition: background-color 0.1s; transition: background-color 0.1s;
.menu { .menu {
opacity: 1; opacity: 1;
} }
} }
&.active { &.active {
background-color: var(--color-list-item); background-color: var(--color-list-item);
.menu { .menu {
opacity: 1; opacity: 1;
&:hover { &:hover {
color: var(--color-text-2); color: var(--color-text-2);
} }

View File

@ -84,7 +84,6 @@ const ExportMenuOptions: FC = () => {
<SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle>
<Switch checked={exportMenuOptions.docx} onChange={(checked) => handleToggleOption('docx', checked)} /> <Switch checked={exportMenuOptions.docx} onChange={(checked) => handleToggleOption('docx', checked)} />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@ -94,6 +93,12 @@ const ExportMenuOptions: FC = () => {
onChange={(checked) => handleToggleOption('plain_text', checked)} onChange={(checked) => handleToggleOption('plain_text', checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.notes')}</SettingRowTitle>
<Switch checked={exportMenuOptions.notes} onChange={(checked) => handleToggleOption('notes', checked)} />
</SettingRow>
</SettingGroup> </SettingGroup>
) )
} }

View File

@ -1824,6 +1824,10 @@ const migrateConfig = {
if (state.settings && state.settings.showWorkspace === undefined) { if (state.settings && state.settings.showWorkspace === undefined) {
state.settings.showWorkspace = true state.settings.showWorkspace = true
} }
if (state.settings && state.settings.exportMenuOptions.notes === undefined) {
state.settings.exportMenuOptions.notes = true
}
return state return state
} }
} }

View File

@ -187,6 +187,7 @@ export interface SettingsState {
siyuan: boolean siyuan: boolean
docx: boolean docx: boolean
plain_text: boolean plain_text: boolean
notes: boolean
} }
// OpenAI // OpenAI
openAI: { openAI: {
@ -345,7 +346,8 @@ export const initialState: SettingsState = {
obsidian: true, obsidian: true,
siyuan: true, siyuan: true,
docx: true, docx: true,
plain_text: true plain_text: true,
notes: true
}, },
// OpenAI // OpenAI
openAI: { openAI: {

View File

@ -1,10 +1,12 @@
import { Client } from '@notionhq/client' import { Client } from '@notionhq/client'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { NotesService } from '@renderer/pages/notes/utils/NotesService'
import { getMessageTitle } from '@renderer/services/MessagesService' import { getMessageTitle } from '@renderer/services/MessagesService'
import store from '@renderer/store' import store from '@renderer/store'
import { setExportState } from '@renderer/store/runtime' import { setExportState } from '@renderer/store/runtime'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { NotesTreeNode } from '@renderer/types/note'
import { removeSpecialCharactersForFileName } from '@renderer/utils/file' import { removeSpecialCharactersForFileName } from '@renderer/utils/file'
import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown' import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown'
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find' import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
@ -736,3 +738,55 @@ async function createSiyuanDoc(
return data.data return data.data
} }
/**
*
* @returns
* @param title
* @param content
*/
export const exportMessageToNotes = async (title: string, content: string): Promise<NotesTreeNode | null> => {
try {
const note = await NotesService.createNote(title, content)
window.message.success({
content: i18n.t('message.success.notes.export'),
key: 'notes-export-success'
})
return note
} catch (error) {
console.error('导出到笔记失败:', error)
window.message.error({
content: i18n.t('message.error.notes.export'),
key: 'notes-export-error'
})
return null
}
}
/**
*
* @param topic
* @returns
*/
export const exportTopicToNotes = async (topic: Topic): Promise<NotesTreeNode | null> => {
try {
const content = await topicToMarkdown(topic)
const note = await NotesService.createNote(topic.name, content)
window.message.success({
content: i18n.t('message.success.notes.export'),
key: 'notes-export-success'
})
return note
} catch (error) {
console.error('导出到笔记失败:', error)
window.message.error({
content: i18n.t('message.error.notes.export'),
key: 'notes-export-error'
})
return null
}
}