diff --git a/package.json b/package.json index 8bcabe78..4ce988d6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 1693dde0..17f84628 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 44b7000f..37bd620b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": "クイックアシスタント", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index e23eb0d5..d2a492b4 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": "Быстрый помощник", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 7cd86ed0..1451bf27 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "快捷助手", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 785f30fc..58490eeb 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "快捷助手", diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index f1880246..1dfeb435 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -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 = ({ assistant: _assistant, activeTopic, setActiveTopic key: 'markdown', onClick: () => exportTopicAsMarkdown(topic) }, + { label: t('chat.topics.export.word'), key: 'word', @@ -140,7 +141,12 @@ const Topics: FC = ({ 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) + }, ] } ] diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 7c565cad..c1caeaa8 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -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 { 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) => { + dispatch(setNotionApiKey(e.target.value)) + }; + + const handleNotionDatabaseIdChange = (e: React.ChangeEvent) => { + dispatch(setNotionDatabaseID(e.target.value)) + }; + + + return ( + + {t('settings.data.notion.title')} + + + {t('settings.data.notion.api_key')} + + + + + {/* 添加分割线 */} + + {t('settings.data.notion.database_id')} + + + + + + ) +} + const DataSettings: FC = () => { const { t } = useTranslation() const [appInfo, setAppInfo] = useState() @@ -79,6 +136,7 @@ const DataSettings: FC = () => { + {t('settings.data.data.title')} @@ -107,6 +165,7 @@ const DataSettings: FC = () => { + ) } diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index bf6a229d..8c47f981 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -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>) => { state.webdavSync = { ...state.webdavSync, ...action.payload } - } + }, + setExportState: (state, action: PayloadAction>) => { + state.export = { ...state.export, ...action.payload } + }, + } }) @@ -87,7 +99,8 @@ export const { setFilesPath, setResourcesPath, setUpdateState, - setWebDAVSyncState + setWebDAVSyncState, + setExportState } = runtimeSlice.actions export default runtimeSlice.reducer diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index d048ea6a..dca1ff74 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -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) => { + state.notionDatabaseID = action.payload + }, + setNotionApiKey: (state, action: PayloadAction) => { + state.notionApiKey = action.payload } } }) @@ -312,7 +322,9 @@ export const { setNarrowMode, setClickTrayToShowQuickAssistant, setEnableQuickAssistant, - setMultiModelMessageStyle + setMultiModelMessageStyle, + setNotionDatabaseID, + setNotionApiKey } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index 4cb562f4..c6804fe1 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -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 + }) + } +}; diff --git a/yarn.lock b/yarn.lock index 228217df..5b64d1c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: