feat: 增加导出话题至Notion的功能 (#1331)

* feat: 新增导出至Notion的选项

* fix:添加多语言支持

* fix:添加提示语的多语言支持,以及防止重复导入的状态

* fix:修复多语言错误及调整UI样式统一
This commit is contained in:
Trey Dong 2025-02-11 11:27:01 +08:00 committed by GitHub
parent 1d82552491
commit 50cc1c6b5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 220 additions and 19 deletions

View File

@ -61,6 +61,7 @@
"@llm-tools/embedjs-loader-web": "^0.1.25",
"@llm-tools/embedjs-loader-xml": "^0.1.25",
"@llm-tools/embedjs-openai": "^0.1.25",
"@notionhq/client": "^2.2.15",
"@types/react-infinite-scroll-component": "^5.0.0",
"adm-zip": "^0.5.16",
"apache-arrow": "^18.1.0",

View File

@ -115,6 +115,7 @@
"topics.edit.placeholder": "Enter new name",
"topics.edit.title": "Edit Name",
"topics.export.image": "Export as image",
"topics.export.notion": "Export to Notion",
"topics.export.md": "Export as markdown",
"topics.export.title": "Export",
"topics.export.word": "Export as Word",
@ -296,7 +297,11 @@
"error.get_embedding_dimensions": "Failed to get embedding dimensions",
"group.delete.title": "Delete Group Message",
"group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers",
"mention.title": "Switch model answer"
"mention.title": "Switch model answer",
"error.notion.export": "Notion import failed",
"error.notion.no_api_key": "Notion ApiKey or Notion DatabaseID is not configured",
"success.notion.export": "Notion import successful",
"warn.notion.exporting": "Notion is importing, please do not import repeatedly"
},
"minapp": {
"title": "MinApp",
@ -421,7 +426,11 @@
"webdav.autoSync.off": "Off",
"webdav.noSync": "Waiting for next backup",
"webdav.syncError": "Backup Error",
"webdav.lastSync": "Last Backup"
"webdav.lastSync": "Last Backup",
"notion.api_key":"Notion API Key",
"notion.database_id":"Notion Database ID",
"notion.title":"Notion Configuration"
},
"quickAssistant": {
"title": "Quick Assistant",

View File

@ -111,6 +111,7 @@
"topics.edit.title": "名前を編集",
"topics.export.image": "画像としてエクスポート",
"topics.export.md": "Markdownとしてエクスポート",
"topics.export.notion": "Notion にエクスポート",
"topics.export.title": "エクスポート",
"topics.export.word": "Wordとしてエクスポート",
"topics.list": "トピックリスト",
@ -290,7 +291,11 @@
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
"group.delete.title": "分組メッセージを削除",
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
"mention.title": "モデルを切り替える"
"mention.title": "モデルを切り替える",
"error.notion.export": "Notion インポートに失敗",
"error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません",
"success.notion.export": "Notion へのインポートに成功",
"warn.notion.exporting": "Notion 正在インポート中です。重複インポートしないでください。"
},
"minapp": {
"title": "ミニアプリ",
@ -413,7 +418,10 @@
"webdav.autoSync.off": "オフ",
"webdav.noSync": "次回のバックアップを待っています",
"webdav.syncError": "バックアップエラー",
"webdav.lastSync": "最終同期"
"webdav.lastSync": "最終同期",
"notion.api_key":"Notion APIキー",
"notion.database_id":"Notion データベースID",
"notion.title":"Notion 設定"
},
"quickAssistant": {
"title": "クイックアシスタント",

View File

@ -111,6 +111,7 @@
"topics.edit.title": "Редактировать заголовок",
"topics.export.image": "Экспорт как изображение",
"topics.export.md": "Экспорт как markdown",
"topics.export.notion": "Экспорт в Notion",
"topics.export.title": "Экспорт",
"topics.export.word": "Экспорт как Word",
"topics.list": "Список топиков",
@ -291,7 +292,11 @@
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
"group.delete.title": "Удалить группу сообщений",
"group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника",
"mention.title": "Переключить модель ответа"
"mention.title": "Переключить модель ответа",
"error.notion.export": "Импорт в Notion не удался",
"error.notion.no_api_key": "Notion ApiKey или Notion DatabaseID не настроен",
"success.notion.export": "Импорт в Notion выполнен успешно",
"warn.notion.exporting": "Идет импорт в Notion, пожалуйста, не повторяйте импорт"
},
"minapp": {
"title": "Встроенные приложения",
@ -414,7 +419,10 @@
"webdav.autoSync.off": "Выключено",
"webdav.noSync": "Ожидание следующего резервного копирования",
"webdav.syncError": "Ошибка резервного копирования",
"webdav.lastSync": "Последняя синхронизация"
"webdav.lastSync": "Последняя синхронизация",
"notion.api_key":"Ключ API Notion",
"notion.database_id":"ID базы данных Notion",
"notion.title":"Настройки Notion"
},
"quickAssistant": {
"title": "Быстрый помощник",

View File

@ -116,6 +116,7 @@
"topics.edit.title": "编辑话题名",
"topics.export.image": "导出为图片",
"topics.export.md": "导出为 Markdown",
"topics.export.notion": "导出到 Notion",
"topics.export.title": "导出",
"topics.export.word": "导出为 Word",
"topics.list": "话题列表",
@ -297,7 +298,12 @@
"error.get_embedding_dimensions": "获取嵌入维度失败",
"group.delete.title": "删除分组消息",
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
"mention.title": "切换模型回答"
"mention.title": "切换模型回答",
"error.notion.export":"Notion 导入失败",
"error.notion.no_api_key":"未配置Notion ApiKey或Notion DatabaseID",
"success.notion.export":"导入Notion成功",
"warn.notion.exporting":"Notion正在导入请勿重复导入"
},
"minapp": {
"title": "小程序",
@ -420,7 +426,10 @@
"webdav.autoSync.off": "关闭",
"webdav.noSync": "等待下次备份",
"webdav.syncError": "备份错误",
"webdav.lastSync": "上次备份时间"
"webdav.lastSync": "上次备份时间",
"notion.api_key":"Notion 密钥",
"notion.database_id":"Notion 数据库ID",
"notion.title":"Notion 配置"
},
"quickAssistant": {
"title": "快捷助手",

View File

@ -116,6 +116,7 @@
"topics.edit.title": "編輯名稱",
"topics.export.image": "匯出為圖片",
"topics.export.md": "匯出為 Markdown",
"topics.export.notion": "匯出到 Notion",
"topics.export.title": "匯出",
"topics.export.word": "導出為 Word",
"topics.list": "話題列表",
@ -296,7 +297,11 @@
"error.get_embedding_dimensions": "獲取嵌入維度失敗",
"group.delete.title": "刪除分組消息",
"group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答",
"mention.title": "切換模型回答"
"mention.title": "切換模型回答",
"error.notion.export": "Notion 匯入失敗",
"error.notion.no_api_key": "未配置 Notion ApiKey 或 Notion DatabaseID",
"success.notion.export": "匯入 Notion 成功",
"warn.notion.exporting": "Notion 正在匯入,請勿重複匯入"
},
"minapp": {
"title": "小程序",
@ -419,7 +424,10 @@
"webdav.autoSync.off": "關閉",
"webdav.noSync": "等待下次備份",
"webdav.syncError": "備份錯誤",
"webdav.lastSync": "上次同步時間"
"webdav.lastSync": "上次同步時間",
"notion.api_key": "Notion 金鑰",
"notion.database_id": "Notion 資料庫 ID",
"notion.title": "Notion 配置"
},
"quickAssistant": {
"title": "快捷助手",

View File

@ -18,7 +18,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
import { exportTopicAsMarkdown, exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export'
import { Dropdown, MenuProps } from 'antd'
import dayjs from 'dayjs'
import { findIndex } from 'lodash'
@ -133,6 +133,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
key: 'markdown',
onClick: () => exportTopicAsMarkdown(topic)
},
{
label: t('chat.topics.export.word'),
key: 'word',
@ -140,7 +141,12 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const markdown = await topicToMarkdown(topic)
window.api.export.toWord(markdown, topic.name)
}
}
},
{
label: t('chat.topics.export.notion'),
key: 'notion',
onClick: () => exportTopicToNotion(topic)
},
]
}
]

View File

@ -2,15 +2,72 @@ import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-desig
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { backup, reset, restore } from '@renderer/services/BackupService'
import { RootState, useAppDispatch } from '@renderer/store'
import { setNotionApiKey, setNotionDatabaseID } from '@renderer/store/settings'
import { AppInfo } from '@renderer/types'
import { Button,Modal, Typography } from 'antd'
import Input from 'antd/es/input/Input'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import WebDavSettings from './WebDavSettings'
// 新增的 NotionSettings 组件
const NotionSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
// 这里可以添加 Notion 相关的状态和逻辑
// 例如:
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey);
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID);
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setNotionApiKey(e.target.value))
};
const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setNotionDatabaseID(e.target.value))
};
return (
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.notion.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
<HStack alignItems="center" gap="5px">
<Input.Password
type="text"
value={notionApiKey || ''}
onChange={handleNotionTokenChange}
onBlur={handleNotionTokenChange}
style={{ width: 250 }}
/>
</HStack>
</SettingRow>
<SettingDivider /> {/* 添加分割线 */}
<SettingRow>
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
<HStack alignItems="center" gap="5px">
<Input
type="text"
value={notionDatabaseID || ''}
onChange={handleNotionDatabaseIdChange}
onBlur={handleNotionDatabaseIdChange}
style={{ width: 250 }}
/>
</HStack>
</SettingRow>
</SettingGroup>
)
}
const DataSettings: FC = () => {
const { t } = useTranslation()
const [appInfo, setAppInfo] = useState<AppInfo>()
@ -79,6 +136,7 @@ const DataSettings: FC = () => {
<SettingGroup theme={theme}>
<WebDavSettings />
</SettingGroup>
<NotionSettings />
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.data.title')}</SettingTitle>
<SettingDivider />
@ -107,6 +165,7 @@ const DataSettings: FC = () => {
</HStack>
</SettingRow>
</SettingGroup>
</SettingContainer>
)
}

View File

@ -25,6 +25,11 @@ export interface RuntimeState {
resourcesPath: string
update: UpdateState
webdavSync: WebDAVSyncState
export: ExportState
}
export interface ExportState {
isExporting: boolean
}
const initialState: RuntimeState = {
@ -45,6 +50,9 @@ const initialState: RuntimeState = {
lastSyncTime: null,
syncing: false,
lastSyncError: null
},
export: {
isExporting: false
}
}
@ -75,7 +83,11 @@ const runtimeSlice = createSlice({
},
setWebDAVSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
state.webdavSync = { ...state.webdavSync, ...action.payload }
}
},
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
state.export = { ...state.export, ...action.payload }
},
}
})
@ -87,7 +99,8 @@ export const {
setFilesPath,
setResourcesPath,
setUpdateState,
setWebDAVSyncState
setWebDAVSyncState,
setExportState
} = runtimeSlice.actions
export default runtimeSlice.reducer

View File

@ -65,6 +65,8 @@ export interface SettingsState {
enableQuickAssistant: boolean
clickTrayToShowQuickAssistant: boolean
multiModelMessageStyle: MultiModelMessageStyle
notionDatabaseID: string | null
notionApiKey: string | null
}
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold'
@ -115,7 +117,9 @@ const initialState: SettingsState = {
narrowMode: false,
enableQuickAssistant: false,
clickTrayToShowQuickAssistant: false,
multiModelMessageStyle: 'fold'
multiModelMessageStyle: 'fold',
notionDatabaseID: '',
notionApiKey: ''
}
const settingsSlice = createSlice({
@ -263,6 +267,12 @@ const settingsSlice = createSlice({
},
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => {
state.multiModelMessageStyle = action.payload
},
setNotionDatabaseID: (state, action: PayloadAction<string>) => {
state.notionDatabaseID = action.payload
},
setNotionApiKey: (state, action: PayloadAction<string>) => {
state.notionApiKey = action.payload
}
}
})
@ -312,7 +322,9 @@ export const {
setNarrowMode,
setClickTrayToShowQuickAssistant,
setEnableQuickAssistant,
setMultiModelMessageStyle
setMultiModelMessageStyle,
setNotionDatabaseID,
setNotionApiKey
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@ -1,4 +1,8 @@
import { Client } from '@notionhq/client'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { setExportState } from '@renderer/store/runtime'
import { Message, Topic } from '@renderer/types'
export const messageToMarkdown = (message: Message) => {
@ -29,3 +33,56 @@ export const exportTopicAsMarkdown = async (topic: Topic) => {
const markdown = await topicToMarkdown(topic)
window.api.file.save(fileName, markdown)
}
export const exportTopicToNotion = async (topic: Topic) => {
const { isExporting } = store.getState().runtime.export
if (isExporting) {
window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' })
return
}
setExportState({
isExporting: true
})
const { notionDatabaseID, notionApiKey } = store.getState().settings
if (!notionApiKey || !notionDatabaseID) {
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
return
}
try {
const notion = new Client({ auth: notionApiKey });
const markdown = await topicToMarkdown(topic);
const requestBody = JSON.stringify({ md: markdown })
const res = await fetch('https://md2notion.hilars.dev', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: requestBody
});
const data = await res.json();
const notionBlocks = data;
const response = await notion.pages.create({
parent: { database_id: notionDatabaseID },
properties: {
Name: {
title: [{ text: { content: topic.name } }]
}
},
children: notionBlocks // 使用转换后的块
});
window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' })
return response
} catch (error:any) {
window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-error' })
return null
} finally {
setExportState({
isExporting: false
})
}
};

View File

@ -1776,6 +1776,16 @@ __metadata:
languageName: node
linkType: hard
"@notionhq/client@npm:^2.2.15":
version: 2.2.15
resolution: "@notionhq/client@npm:2.2.15"
dependencies:
"@types/node-fetch": "npm:^2.5.10"
node-fetch: "npm:^2.6.1"
checksum: 10c0/4153c2e5b47d2ba141d025f2753d0e79ca9b9f25bd8bbdfa9dbf74fe4c2e157ea7964c59387d05163972c4575830bdc48d02db29270e244d81398df0f89fd7dd
languageName: node
linkType: hard
"@npmcli/agent@npm:^3.0.0":
version: 3.0.0
resolution: "@npmcli/agent@npm:3.0.0"
@ -2640,7 +2650,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.4":
"@types/node-fetch@npm:^2.5.10, @types/node-fetch@npm:^2.6.4":
version: 2.6.12
resolution: "@types/node-fetch@npm:2.6.12"
dependencies:
@ -3011,6 +3021,7 @@ __metadata:
"@llm-tools/embedjs-loader-web": "npm:^0.1.25"
"@llm-tools/embedjs-loader-xml": "npm:^0.1.25"
"@llm-tools/embedjs-openai": "npm:^0.1.25"
"@notionhq/client": "npm:^2.2.15"
"@reduxjs/toolkit": "npm:^2.2.5"
"@types/adm-zip": "npm:^0"
"@types/fs-extra": "npm:^11"
@ -9845,7 +9856,7 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.7":
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies: