diff --git a/src/renderer/src/components/ObsidianExportDialog.tsx b/src/renderer/src/components/ObsidianExportDialog.tsx new file mode 100644 index 00000000..ec37b8a7 --- /dev/null +++ b/src/renderer/src/components/ObsidianExportDialog.tsx @@ -0,0 +1,120 @@ +import i18n from '@renderer/i18n' +import { exportMarkdownToObsidian } from '@renderer/utils/export' +import { Form, Input, Modal, Select } from 'antd' +import React, { useState } from 'react' + +const { Option } = Select + +interface ObsidianExportDialogProps { + title: string + markdown: string + open: boolean // 使用 open 属性替代 visible + onClose: (success: boolean) => void + obsidianTags: string | null + processingMethod: string | '3' //默认新增(存在就覆盖) +} + +const ObsidianExportDialog: React.FC = ({ + title, + markdown, + obsidianTags, + processingMethod, + open, + onClose +}) => { + const [state, setState] = useState({ + title: title, + tags: obsidianTags || '', + createdAt: new Date().toISOString().split('T')[0], + source: 'Cherry Studio', + processingMethod: processingMethod + }) + + const handleOk = async () => { + //构建content 并复制到粘贴板 + let content = '' + if (state.processingMethod !== '3') { + content = `\n---\n${markdown}` + } else { + content = `--- + \ntitle: ${state.title} + \ncreated: ${state.createdAt} + \nsource: ${state.source} + \ntags: ${state.tags} + \n---\n${markdown}` + } + if (content === '') { + window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) + } + await navigator.clipboard.writeText(content) + markdown = '' + exportMarkdownToObsidian(state) + onClose(true) + } + + const handleCancel = () => { + onClose(false) + } + + const handleChange = (key: string, value: any) => { + setState((prevState) => ({ ...prevState, [key]: value })) + } + + return ( + +
+ + handleChange('title', e.target.value)} + placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')} + /> + + + handleChange('tags', e.target.value)} + placeholder={i18n.t('chat.topics.export.obsidian_tags_placeholder')} + /> + + + handleChange('createdAt', e.target.value)} + placeholder={i18n.t('chat.topics.export.obsidian_created_placeholder')} + /> + + + handleChange('source', e.target.value)} + placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')} + /> + + + + +
+
+ ) +} + +export default ObsidianExportDialog diff --git a/src/renderer/src/components/ObsidianFolderSelector.tsx b/src/renderer/src/components/ObsidianFolderSelector.tsx deleted file mode 100644 index cb6f6170..00000000 --- a/src/renderer/src/components/ObsidianFolderSelector.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { FileOutlined, FolderOutlined } from '@ant-design/icons' -import { Spin, Switch, Tree } from 'antd' -import { FC, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -interface Props { - defaultPath: string - obsidianUrl: string - obsidianApiKey: string - onPathChange: (path: string, isMdFile: boolean) => void -} - -interface TreeNode { - title: string - key: string - isLeaf: boolean - isMdFile?: boolean - children?: TreeNode[] -} - -const ObsidianFolderSelector: FC = ({ defaultPath, obsidianUrl, obsidianApiKey, onPathChange }) => { - const { t } = useTranslation() - const [treeData, setTreeData] = useState([]) - const [loading, setLoading] = useState(false) - const [expandedKeys, setExpandedKeys] = useState(['/']) - const [showMdFiles, setShowMdFiles] = useState(false) - // 当前选中的节点信息 - const [currentSelection, setCurrentSelection] = useState({ - path: defaultPath, - isMdFile: false - }) - // 使用key强制Tree组件重新渲染 - const [treeKey, setTreeKey] = useState(0) - - // 只初始化根节点,不立即加载内容 - useEffect(() => { - initializeRootNode() - }, [showMdFiles]) - - // 初始化根节点,但不自动加载子节点 - const initializeRootNode = () => { - const rootNode: TreeNode = { - title: '/', - key: '/', - isLeaf: false - } - - setTreeData([rootNode]) - } - - // 异步加载子节点 - const loadData = async (node: any) => { - if (node.isLeaf) return // 如果是叶子节点(md文件),不加载子节点 - - setLoading(true) - try { - // 确保路径末尾有斜杠 - const path = node.key === '/' ? '' : node.key - const requestPath = path.endsWith('/') ? path : `${path}/` - - const response = await fetch(`${obsidianUrl}vault${requestPath}`, { - headers: { - Authorization: `Bearer ${obsidianApiKey}` - } - }) - const data = await response.json() - - if (!response.ok || (!data?.files && data?.errorCode !== 40400)) { - throw new Error('获取文件夹失败') - } - - const childNodes: TreeNode[] = (data.files || []) - .filter((file: string) => file.endsWith('/') || (showMdFiles && file.endsWith('.md'))) // 根据开关状态决定是否显示md文件 - .map((file: string) => { - // 修复路径问题,避免重复的斜杠 - const normalizedFile = file.replace('/', '') - const isMdFile = file.endsWith('.md') - const childPath = requestPath.endsWith('/') - ? `${requestPath}${normalizedFile}${isMdFile ? '' : '/'}` - : `${requestPath}/${normalizedFile}${isMdFile ? '' : '/'}` - - return { - title: normalizedFile, - key: childPath, - isLeaf: isMdFile, - isMdFile - } - }) - - // 更新节点的子节点 - setTreeData((origin) => { - const loop = (data: TreeNode[], key: string, children: TreeNode[]): TreeNode[] => { - return data.map((item) => { - if (item.key === key) { - return { - ...item, - children - } - } - if (item.children) { - return { - ...item, - children: loop(item.children, key, children) - } - } - return item - }) - } - return loop(origin, node.key, childNodes) - }) - } catch (error) { - window.message.error(t('chat.topics.export.obsidian_fetch_failed')) - } finally { - setLoading(false) - } - } - - // 处理开关切换 - const handleSwitchChange = (checked: boolean) => { - setShowMdFiles(checked) - // 重置选择 - setCurrentSelection({ - path: defaultPath, - isMdFile: false - }) - onPathChange(defaultPath, false) - - // 重置Tree状态并强制重新渲染 - setTreeData([]) - setExpandedKeys(['/']) - - // 递增key值以强制Tree组件完全重新渲染 - setTreeKey((prev) => prev + 1) - - // 延迟初始化根节点,让状态完全清除 - setTimeout(() => { - initializeRootNode() - }, 50) - } - - // 自定义图标,为md文件和文件夹显示不同的图标 - const renderIcon = (props: any) => { - const { data } = props - if (data.isMdFile) { - return - } - return - } - - return ( - - - {t('chat.topics.export.obsidian_show_md_files')} - - - - - setExpandedKeys(keys as string[])} - treeData={treeData} - loadData={loadData} - onSelect={(selectedKeys, info) => { - if (selectedKeys.length > 0) { - const path = selectedKeys[0] as string - const isMdFile = !!(info.node as any).isMdFile - - setCurrentSelection({ - path, - isMdFile - }) - - onPathChange?.(path, isMdFile) - } - }} - showLine - showIcon - icon={renderIcon} - /> - - -
- {currentSelection.path !== defaultPath && ( - - {t('chat.topics.export.obsidian_selected_path')}: {currentSelection.path} - - )} -
-
- ) -} - -const Container = styled.div` - display: flex; - flex-direction: column; - height: 400px; -` - -const TreeContainer = styled.div` - flex: 1; - overflow-y: auto; - border: 1px solid var(--color-border); - border-radius: 4px; - padding: 10px; - margin-bottom: 10px; - height: 320px; -` - -const SwitchContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - padding: 0 10px; -` - -const SelectedPath = styled.div` - font-size: 12px; - color: var(--color-text-2); - margin-top: 5px; - padding: 0 10px; - word-break: break-all; -` - -export default ObsidianFolderSelector diff --git a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx index b258e9cd..5f229d74 100644 --- a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx +++ b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx @@ -1,68 +1,52 @@ -import ObsidianFolderSelector from '@renderer/components/ObsidianFolderSelector' +import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog' import i18n from '@renderer/i18n' import store from '@renderer/store' -import { exportMarkdownToObsidian } from '@renderer/utils/export' +import { createRoot } from 'react-dom/client' interface ObsidianExportOptions { title: string markdown: string + processingMethod: string | '3' // 默认新增(存在就覆盖) } -// 用于显示 Obsidian 导出对话框 +/** + * 配置Obsidian 笔记属性弹窗 + * @param options.title 标题 + * @param options.markdown markdown内容 + * @param options.processingMethod 处理方式 + * @returns + */ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise => { - const { title, markdown } = options - const obsidianUrl = store.getState().settings.obsidianUrl - const obsidianApiKey = store.getState().settings.obsidianApiKey + const obsidianValut = store.getState().settings.obsidianValut + const obsidianFolder = store.getState().settings.obsidianFolder - if (!obsidianUrl || !obsidianApiKey) { + if (!obsidianValut || !obsidianFolder) { window.message.error(i18n.t('chat.topics.export.obsidian_not_configured')) return false } - try { - // 创建一个状态变量来存储选择的路径 - let selectedPath = '/' - let selectedIsMdFile = false + return new Promise((resolve) => { + const div = document.createElement('div') + document.body.appendChild(div) + const root = createRoot(div) - // 显示文件夹选择对话框 - return new Promise((resolve) => { - window.modal.confirm({ - title: i18n.t('chat.topics.export.obsidian_select_folder'), - content: ( - { - selectedPath = path - selectedIsMdFile = isMdFile - }} - /> - ), - width: 600, - icon: null, - closable: true, - maskClosable: true, - centered: true, - okButtonProps: { type: 'primary' }, - okText: i18n.t('chat.topics.export.obsidian_select_folder.btn'), - onOk: () => { - // 如果选择的是md文件,则使用选择的文件名而不是传入的标题 - const fileName = selectedIsMdFile ? selectedPath.split('/').pop()?.replace('.md', '') : title - - exportMarkdownToObsidian(fileName as string, markdown, selectedPath, selectedIsMdFile) - resolve(true) - }, - onCancel: () => { - resolve(false) - } - }) - }) - } catch (error) { - window.message.error(i18n.t('chat.topics.export.obsidian_fetch_failed')) - console.error(error) - return false - } + const handleClose = (success: boolean) => { + root.unmount() + document.body.removeChild(div) + resolve(success) + } + const obsidianTags = store.getState().settings.obsidianTages + root.render( + + ) + }) } const ObsidianExportPopup = { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 5223dc5f..2cfda2a6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -164,13 +164,24 @@ "topics.export.yuque": "Export to Yuque", "topics.export.obsidian": "Export to Obsidian", "topics.export.obsidian_not_configured": "Obsidian not configured", - "topics.export.obsidian_fetch_failed": "Failed to fetch Obsidian folder structure", - "topics.export.obsidian_select_folder": "Select Obsidian folder", - "topics.export.obsidian_select_folder.btn": "Confirm", + "topics.export.obsidian_title": "Title", + "topics.export.obsidian_title_placeholder": "Please enter the title", + "topics.export.obsidian_title_required": "The title cannot be empty", + "topics.export.obsidian_tags": "Tags", + "topics.export.obsidian_tags_placeholder": "Please enter tags, separate multiple tags with commas in English,In Obsidian, pure numbers cannot be used.", + "topics.export.obsidian_created": "Creation Time", + "topics.export.obsidian_created_placeholder": "Please select the creation time", + "topics.export.obsidian_source": "Source", + "topics.export.obsidian_source_placeholder": "Please enter the source", + "topics.export.obsidian_operate": "Operation Method", + "topics.export.obsidian_operate_placeholder": "Please select the operation method", + "topics.export.obsidian_operate_append": "Append", + "topics.export.obsidian_operate_prepend": "Prepend", + "topics.export.obsidian_operate_new_or_overwrite": "Create New (Overwrite if it exists)", + "topics.export.obsidian_atributes": "Configure Note Attributes", + "topics.export.obsidian_btn": "Confirm", "topics.export.obsidian_export_success": "Export success", "topics.export.obsidian_export_failed": "Export failed", - "topics.export.obsidian_show_md_files": "Show MD Files", - "topics.export.obsidian_selected_path": "Selected Path", "topics.export.joplin": "Export to Joplin", "topics.list": "Topic List", "topics.move_to": "Move to", @@ -792,19 +803,13 @@ "token_placeholder": "Please enter the Yuque Token" }, "obsidian": { - "check": { - "button": "Check", - "empty_url": "Please enter the Obsidian REST API URL first", - "empty_api_key": "Please enter the Obsidian API Key first", - "fail": "Obsidian connection verification failed", - "success": "Obsidian connection verification successful" - }, - "help": "Install the Obsidian plugin Local REST API first, then get the Obsidian API Key", - "url": "Obsidian Knowledge Base URL", - "url_placeholder": "http://127.0.0.1:27123/", "title": "Obsidian Configuration", - "api_key": "Obsidian API Key", - "api_key_placeholder": "Please enter the Obsidian API Key" + "vault": "Vault", + "vault_placeholder": "Please enter the vault name", + "folder": "Folder", + "folder_placeholder": "Please enter the folder name", + "tags": "Global Tags", + "tags_placeholder": "Please enter the tag name, separate multiple tags with commas in English,In Obsidian, pure numbers cannot be used." }, "joplin": { "check": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 165d242d..8ee358da 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -164,13 +164,24 @@ "topics.export.yuque": "語雀にエクスポート", "topics.export.obsidian": "Obsidian にエクスポート", "topics.export.obsidian_not_configured": "Obsidian 未設定", - "topics.export.obsidian_fetch_failed": "Obsidian ファイルフォルダ構造取得失敗", - "topics.export.obsidian_select_folder": "Obsidian ファイルフォルダ選択", - "topics.export.obsidian_select_folder.btn": "確定", + "topics.export.obsidian_title": "タイトル", + "topics.export.obsidian_title_placeholder": "タイトルを入力してください", + "topics.export.obsidian_title_required": "タイトルは空白にできません", + "topics.export.obsidian_tags": "タグ", + "topics.export.obsidian_tags_placeholder": "タグを入力してください。複数のタグは英語のコンマで区切ってください,Obsidian では、純粋な数字を使用することはできません。", + "topics.export.obsidian_created": "作成日時", + "topics.export.obsidian_created_placeholder": "作成日時を選択してください", + "topics.export.obsidian_source": "ソース", + "topics.export.obsidian_source_placeholder": "ソースを入力してください", + "topics.export.obsidian_operate": "処理方法", + "topics.export.obsidian_operate_placeholder": "処理方法を選択してください", + "topics.export.obsidian_operate_append": "追加", + "topics.export.obsidian_operate_prepend": "先頭に追加", + "topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)", + "topics.export.obsidian_atributes": "ノートの属性を設定", + "topics.export.obsidian_btn": "確定", "topics.export.obsidian_export_success": "エクスポート成功", "topics.export.obsidian_export_failed": "エクスポート失敗", - "topics.export.obsidian_show_md_files": "mdファイルを表示", - "topics.export.obsidian_selected_path": "選択済みパス", "topics.export.joplin": "Joplin にエクスポート", "topics.list": "トピックリスト", "topics.move_to": "移動先", @@ -792,19 +803,13 @@ "token_placeholder": "Yuqueトークンを入力してください" }, "obsidian": { - "check": { - "button": "確認", - "empty_url": "Obsidian REST API URL を先に入力してください", - "empty_api_key": "Obsidian API Key を先に入力してください", - "fail": "Obsidian 接続確認に失敗しました", - "success": "Obsidian 接続確認に成功しました" - }, - "help": "Obsidian プラグイン Local REST API を先にインストールしてください。その後、Obsidian API Key を取得してください", - "url": "Obsidian 知識ベース URL", - "url_placeholder": "http://127.0.0.1:27123/", - "title": "Obsidian 設定", - "api_key": "Obsidian API Key", - "api_key_placeholder": "Obsidian API Key を入力してください" + "title": "Obsidian の設定", + "vault": "ヴォールト(保管庫)", + "vault_placeholder": "保管庫の名前を入力してください", + "folder": "フォルダー", + "folder_placeholder": "フォルダーの名前を入力してください", + "tags": "グローバルタグ", + "tags_placeholder": "タグの名前を入力してください。複数のタグは英語のコンマで区切ってください,Obsidian では、純粋な数字を使用することはできません。" }, "joplin": { "check": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 46286ff5..abcaef27 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -164,13 +164,24 @@ "topics.export.yuque": "Экспорт в Yuque", "topics.export.obsidian": "Экспорт в Obsidian", "topics.export.obsidian_not_configured": "Obsidian не настроен", - "topics.export.obsidian_fetch_failed": "Не удалось получить структуру файлов Obsidian", - "topics.export.obsidian_select_folder": "Выберите папку Obsidian", - "topics.export.obsidian_select_folder.btn": "Определить", + "topics.export.obsidian_title": "Заголовок", + "topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок", + "topics.export.obsidian_title_required": "Заголовок не может быть пустым", + "topics.export.obsidian_tags": "Тэги", + "topics.export.obsidian_tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры.", + "topics.export.obsidian_created": "Дата создания", + "topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания", + "topics.export.obsidian_source": "Источник", + "topics.export.obsidian_source_placeholder": "Пожалуйста, введите источник", + "topics.export.obsidian_operate": "Метод обработки", + "topics.export.obsidian_operate_placeholder": "Пожалуйста, выберите метод обработки", + "topics.export.obsidian_operate_append": "Добавить в конец", + "topics.export.obsidian_operate_prepend": "Добавить в начало", + "topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)", + "topics.export.obsidian_atributes": "Настроить атрибуты заметки", + "topics.export.obsidian_btn": "Подтвердить", "topics.export.obsidian_export_success": "Экспорт успешно завершен", "topics.export.obsidian_export_failed": "Экспорт не удалось", - "topics.export.obsidian_show_md_files": "Показать файлы MD", - "topics.export.obsidian_selected_path": "Выбранный путь", "topics.export.joplin": "Экспорт в Joplin", "topics.list": "Список топиков", "topics.move_to": "Переместить в", @@ -792,19 +803,13 @@ "token_placeholder": "Введите токен Yuque" }, "obsidian": { - "check": { - "button": "Проверить", - "empty_url": "Сначала введите URL REST API Obsidian", - "empty_api_key": "Сначала введите API Key Obsidian", - "fail": "Не удалось проверить подключение к Obsidian", - "success": "Подключение к Obsidian успешно проверено" - }, - "help": "Сначала установите плагин Local REST API Obsidian, затем получите API Key Obsidian", - "url": "URL базы знаний Obsidian", - "url_placeholder": "http://127.0.0.1:27123/", - "title": "Настройка Obsidian", - "api_key": "API Key Obsidian", - "api_key_placeholder": "Введите API Key Obsidian" + "title": "Конфигурация Obsidian", + "vault": "Хранилище", + "vault_placeholder": "Пожалуйста, введите имя хранилища", + "folder": "Папка", + "folder_placeholder": "Пожалуйста, введите имя папки", + "tags": "Глобальные Теги", + "tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры." }, "joplin": { "check": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index af39e01e..b2665613 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -164,13 +164,24 @@ "topics.export.yuque": "导出到语雀", "topics.export.obsidian": "导出到 Obsidian", "topics.export.obsidian_not_configured": "Obsidian 未配置", - "topics.export.obsidian_fetch_failed": "获取 Obsidian 文件夹结构失败", - "topics.export.obsidian_select_folder": "选择 Obsidian 文件夹", - "topics.export.obsidian_select_folder.btn": "确定", + "topics.export.obsidian_title":"标题", + "topics.export.obsidian_title_placeholder":"请输入标题", + "topics.export.obsidian_title_required":"标题不能为空", + "topics.export.obsidian_tags":"标签", + "topics.export.obsidian_tags_placeholder":"请输入标签,多个标签用英文逗号分隔,Obsidian不可用纯数字", + "topics.export.obsidian_created":"创建时间", + "topics.export.obsidian_created_placeholder":"请选择创建时间", + "topics.export.obsidian_source":"来源", + "topics.export.obsidian_source_placeholder":"请输入来源", + "topics.export.obsidian_operate":"处理方式", + "topics.export.obsidian_operate_placeholder":"请选择处理方式", + "topics.export.obsidian_operate_append":"追加", + "topics.export.obsidian_operate_prepend":"前置", + "topics.export.obsidian_operate_new_or_overwrite":"新建(如果存在就覆盖)", + "topics.export.obsidian_atributes": "配置笔记属性", + "topics.export.obsidian_btn": "确定", "topics.export.obsidian_export_success": "导出成功", "topics.export.obsidian_export_failed": "导出失败", - "topics.export.obsidian_show_md_files": "显示md文件", - "topics.export.obsidian_selected_path": "已选择路径", "topics.export.joplin": "导出到 Joplin", "topics.list": "话题列表", "topics.move_to": "移动到", @@ -792,19 +803,13 @@ "token_placeholder": "请输入语雀Token" }, "obsidian": { - "check": { - "button": "检查", - "empty_url": "请先输入 Obsidian REST API URL", - "empty_api_key": "请先输入 Obsidian API Key", - "fail": "Obsidian 连接验证失败", - "success": "Obsidian 连接验证成功" - }, - "help": "先安装 Obsidian 插件 Local REST API,然后获取 Obsidian API Key", - "url": "Obsidian 知识库 URL", - "url_placeholder": "http://127.0.0.1:27123/", "title": "Obsidian 配置", - "api_key": "Obsidian API Key", - "api_key_placeholder": "请输入 Obsidian API Key" + "vault": "保管库", + "vault_placeholder": "请输入保管库名称", + "folder": "文件夹", + "folder_placeholder": "请输入文件夹名称", + "tags": "全局标签", + "tags_placeholder": "请输入标签名称, 多个标签用英文逗号分隔,Obsidian不可用纯数字" }, "joplin": { "check": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 937b1d52..81dbd945 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -164,10 +164,24 @@ "topics.export.yuque": "匯出到語雀", "topics.export.obsidian": "匯出到 Obsidian", "topics.export.obsidian_not_configured": "Obsidian 未配置", - "topics.export.obsidian_fetch_failed": "獲取 Obsidian 文件夾結構失敗", - "topics.export.obsidian_select_folder": "選擇 Obsidian 文件夾", - "topics.export.obsidian_select_folder.btn": "確定", + "topics.export.obsidian_title":"標題", + "topics.export.obsidian_title_placeholder": "請輸入標題", + "topics.export.obsidian_title_required": "標題不能為空", + "topics.export.obsidian_tags": "標籤", + "topics.export.obsidian_tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。", + "topics.export.obsidian_created": "建立時間", + "topics.export.obsidian_created_placeholder": "請選擇建立時間", + "topics.export.obsidian_source": "來源", + "topics.export.obsidian_source_placeholder": "請輸入來源", + "topics.export.obsidian_operate": "處理方式", + "topics.export.obsidian_operate_placeholder": "請選擇處理方式", + "topics.export.obsidian_operate_append": "追加", + "topics.export.obsidian_operate_prepend": "前置", + "topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)", + "topics.export.obsidian_atributes": "配置筆記屬性", + "topics.export.obsidian_btn": "確定", "topics.export.obsidian_export_success": "匯出成功", + "topics.export.obsidian_export_failed": "匯出失败", "topics.export.obsidian_export_failed": "匯出失敗", "topics.export.obsidian_show_md_files": "顯示md文件", "topics.export.obsidian_selected_path": "已選擇路徑", @@ -792,19 +806,13 @@ "token_placeholder": "請輸入語雀 Token" }, "obsidian": { - "check": { - "button": "檢查", - "empty_url": "請先輸入 Obsidian REST API URL", - "empty_api_key": "請先輸入 Obsidian API Key", - "fail": "Obsidian 連接驗證失敗", - "success": "Obsidian 連接驗證成功" - }, - "help": "先安裝 Obsidian 插件 Local REST API,然後獲取 Obsidian API Key", - "url": "Obsidian 知識庫 URL", - "url_placeholder": "http://127.0.0.1:27123/", "title": "Obsidian 設定", - "api_key": "Obsidian API Key", - "api_key_placeholder": "請輸入 Obsidian API Key" + "vault": "保險庫", + "vault_placeholder": "請輸入保險庫名稱", + "folder": "資料夾", + "folder_placeholder": "請輸入資料夾名稱", + "tags": "全域標籤", + "tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。" }, "joplin": { "check": { diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 0eb5fa19..c43dd114 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -242,8 +242,8 @@ const MessageMenubar: FC = (props) => { key: 'obsidian', onClick: async () => { const markdown = messageToMarkdown(message) - const title = getMessageTitle(message) - await ObsidianExportPopup.show({ title, markdown }) + const title = topic.name?.replace(/\//g, '_') || 'Untitled' + await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' }) } }, { diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 79d8776c..7d6f4629 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -262,7 +262,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic key: 'obsidian', onClick: async () => { const markdown = await topicToMarkdown(topic) - await ObsidianExportPopup.show({ title: topic.name, markdown }) + await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' }) } }, { diff --git a/src/renderer/src/pages/settings/DataSettings/ObsidianSettings.tsx b/src/renderer/src/pages/settings/DataSettings/ObsidianSettings.tsx index 2931fe03..7adfadc0 100644 --- a/src/renderer/src/pages/settings/DataSettings/ObsidianSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/ObsidianSettings.tsx @@ -1,10 +1,7 @@ -import { InfoCircleOutlined } from '@ant-design/icons' import { HStack } from '@renderer/components/Layout' -import MinApp from '@renderer/components/MinApp' import { useTheme } from '@renderer/context/ThemeProvider' import { RootState, useAppDispatch } from '@renderer/store' -import { setObsidianApiKey, setObsidianUrl } from '@renderer/store/settings' -import { Button, Tooltip } from 'antd' +import { setObsidianFolder, setObsidianTages, setObsidianValut } from '@renderer/store/settings' import Input from 'antd/es/input/Input' import { FC } from 'react' import { useTranslation } from 'react-i18next' @@ -17,62 +14,35 @@ const ObsidianSettings: FC = () => { const { theme } = useTheme() const dispatch = useAppDispatch() - const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey) - const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl) + // const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey) + // const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl) - const handleObsidianApiKeyChange = (e: React.ChangeEvent) => { - dispatch(setObsidianApiKey(e.target.value)) + const obsidianVault = useSelector((state: RootState) => state.settings.obsidianValut) + const obsidianFolder = useSelector((state: RootState) => state.settings.obsidianFolder) + const obsidianTags = useSelector((state: RootState) => state.settings.obsidianTages) + + const handleObsidianVaultChange = (e: React.ChangeEvent) => { + dispatch(setObsidianValut(e.target.value)) } - const handleObsidianUrlChange = (e: React.ChangeEvent) => { - dispatch(setObsidianUrl(e.target.value)) + const handleObsidianFolderChange = (e: React.ChangeEvent) => { + dispatch(setObsidianFolder(e.target.value)) } - const handleObsidianUrlBlur = (e: React.FocusEvent) => { - let url = e.target.value - // 确保URL以/结尾,但只在失去焦点时执行 - if (url && !url.endsWith('/')) { - url = `${url}/` - dispatch(setObsidianUrl(url)) - } + const handleObsidianVaultBlur = (e: React.FocusEvent) => { + dispatch(setObsidianValut(e.target.value)) } - const handleObsidianConnectionCheck = async () => { - try { - if (!obsidianApiKey) { - window.message.error(t('settings.data.obsidian.check.empty_api_key')) - return - } - if (!obsidianUrl) { - window.message.error(t('settings.data.obsidian.check.empty_url')) - return - } - - const response = await fetch(`${obsidianUrl}`, { - headers: { - Authorization: `Bearer ${obsidianApiKey}` - } - }) - - const data = await response.json() - - if (!response.ok || !data?.authenticated) { - window.message.error(t('settings.data.obsidian.check.fail')) - return - } - - window.message.success(t('settings.data.obsidian.check.success')) - } catch (e) { - window.message.error(t('settings.data.obsidian.check.fail')) - } + const handleObsidianFolderBlur = (e: React.FocusEvent) => { + dispatch(setObsidianFolder(e.target.value)) } - const handleObsidianHelpClick = () => { - MinApp.start({ - id: 'obsidian-help', - name: 'Obsidian Help', - url: 'https://github.com/coddingtonbear/obsidian-local-rest-api' - }) + const handleObsidianTagsChange = (e: React.ChangeEvent) => { + dispatch(setObsidianTages(e.target.value)) + } + + const handleObsidianTagsBlur = (e: React.FocusEvent) => { + dispatch(setObsidianTages(e.target.value)) } return ( @@ -80,38 +50,46 @@ const ObsidianSettings: FC = () => { {t('settings.data.obsidian.title')} - {t('settings.data.obsidian.url')} + {t('settings.data.obsidian.vault')} - {t('settings.data.obsidian.api_key')} - - - + {t('settings.data.obsidian.folder')} + + + + + + {t('settings.data.obsidian.tags')} + + + - diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 85999c54..0a302596 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -86,8 +86,10 @@ export interface SettingsState { yuqueToken: string | null yuqueUrl: string | null yuqueRepoId: string | null - obsidianApiKey: string | null - obsidianUrl: string | null + //obsidian settings obsidianVault, obisidanFolder + obsidianValut: string | null + obsidianFolder: string | null + obsidianTages: string | null joplinToken: string | null joplinUrl: string | null } @@ -161,10 +163,12 @@ const initialState: SettingsState = { yuqueToken: '', yuqueUrl: '', yuqueRepoId: '', - obsidianApiKey: '', - obsidianUrl: '', + obsidianValut: '', + obsidianFolder: '', + obsidianTages: '', joplinToken: '', joplinUrl: '' + } const settingsSlice = createSlice({ @@ -369,11 +373,14 @@ const settingsSlice = createSlice({ setYuqueUrl: (state, action: PayloadAction) => { state.yuqueUrl = action.payload }, - setObsidianApiKey: (state, action: PayloadAction) => { - state.obsidianApiKey = action.payload + setObsidianValut: (state, action: PayloadAction) => { + state.obsidianValut = action.payload }, - setObsidianUrl: (state, action: PayloadAction) => { - state.obsidianUrl = action.payload + setObsidianFolder: (state, action: PayloadAction) => { + state.obsidianFolder = action.payload + }, + setObsidianTages: (state, action: PayloadAction) => { + state.obsidianTages = action.payload }, setJoplinToken: (state, action: PayloadAction) => { state.joplinToken = action.payload @@ -452,8 +459,9 @@ export const { setYuqueToken, setYuqueRepoId, setYuqueUrl, - setObsidianApiKey, - setObsidianUrl, + setObsidianValut, + setObsidianFolder, + setObsidianTages, setJoplinToken, setJoplinUrl, setMessageNavigation diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index b483e0e2..f8d74d04 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -320,58 +320,46 @@ export const exportMarkdownToYuque = async (title: string, content: string) => { /** * 导出Markdown到Obsidian + * @param attributes 文档属性 + * @param attributes.title 标题 + * @param attributes.created 创建时间 + * @param attributes.source 来源 + * @param attributes.tags 标签 + * @param attributes.processingMethod 处理方式 */ -export const exportMarkdownToObsidian = async ( - fileName: string, - markdown: string, - selectedPath: string, - isMdFile: boolean = false -) => { +export const exportMarkdownToObsidian = async (attributes: any) => { try { - const obsidianUrl = store.getState().settings.obsidianUrl - const obsidianApiKey = store.getState().settings.obsidianApiKey + const obsidianValut = store.getState().settings.obsidianValut + const obsidianFolder = store.getState().settings.obsidianFolder - if (!obsidianUrl || !obsidianApiKey) { + if (!obsidianValut || !obsidianFolder) { window.message.error(i18n.t('chat.topics.export.obsidian_not_configured')) return } + let path = '' - // 如果是md文件,直接将内容追加到该文件 - if (isMdFile) { - const response = await fetch(`${obsidianUrl}vault${selectedPath}`, { - method: 'POST', - headers: { - 'Content-Type': 'text/markdown', - Authorization: `Bearer ${obsidianApiKey}` - }, - body: `\n\n${markdown}` // 添加两个换行后追加内容 - }) - - if (!response.ok) { - window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) - return - } - } else { - // 创建新文件 - const sanitizedFileName = removeSpecialCharactersForFileName(fileName) - const path = selectedPath === '/' ? '' : selectedPath - const fullPath = path.endsWith('/') ? `${path}${sanitizedFileName}.md` : `${path}/${sanitizedFileName}.md` - - const response = await fetch(`${obsidianUrl}vault${fullPath}`, { - method: 'PUT', - headers: { - 'Content-Type': 'text/markdown', - Authorization: `Bearer ${obsidianApiKey}` - }, - body: markdown - }) - - if (!response.ok) { - window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) - return - } + if (!attributes.title) { + window.message.error(i18n.t('chat.topics.export.obsidian_title_required')) + return } + //构建保存路径添加以 / 结尾 + if (!obsidianFolder.endsWith('/')) { + path = obsidianFolder + '/' + } + //构建文件名 + const fileName = transformObsidianFileName(attributes.title) + + let obsidianUrl = `obsidian://new?file=${encodeURIComponent(path + fileName)}&vault=${encodeURIComponent(obsidianValut)}&clipboard` + + if (attributes.processingMethod === '3') { + obsidianUrl += '&overwrite=true' + } else if (attributes.processingMethod === '2') { + obsidianUrl += '&prepend=true' + } else if (attributes.processingMethod === '1') { + obsidianUrl += '&append=true' + } + window.open(obsidianUrl) window.message.success(i18n.t('chat.topics.export.obsidian_export_success')) } catch (error) { console.error('导出到Obsidian失败:', error) @@ -379,6 +367,51 @@ export const exportMarkdownToObsidian = async ( } } +/** + * 生成Obsidian文件名,源自 Obsidian Web Clipper 官方实现,修改了一些细节 + * @param fileName + * @returns + */ + +function transformObsidianFileName(fileName: string): string { + const platform = window.navigator.userAgent + const isWindows = /win/i.test(platform) + const isMac = /mac/i.test(platform) + + // 删除Obsidian 全平台无效字符 + let sanitized = fileName.replace(/[#|\\^\\[\]]/g, '') + + if (isWindows) { + // Windows 的清理 + sanitized = sanitized + .replace(/[<>:"\\/\\|?*]/g, '') // 移除无效字符 + .replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i, '_$1$2') // 避免保留名称 + .replace(/[\s.]+$/, '') // 移除结尾的空格和句点 + } else if (isMac) { + // Mac 的清理 + sanitized = sanitized + .replace(/[/:\u0020-\u007E]/g, '') // 移除无效字符 + .replace(/^\./, '_') // 避免以句点开头 + } else { + // Linux 或其他系统 + sanitized = sanitized + .replace(/[<>:"\\/\\|?*]/g, '') // 移除无效字符 + .replace(/^\./, '_') // 避免以句点开头 + } + + // 所有平台的通用操作 + sanitized = sanitized + .replace(/^\.+/, '') // 移除开头的句点 + .trim() // 移除前后空格 + .slice(0, 245) // 截断为 245 个字符,留出空间以追加 ' 1.md' + + // 确保文件名不为空 + if (sanitized.length === 0) { + sanitized = 'Untitled' + } + + return sanitized +} export const exportMarkdownToJoplin = async (title: string, content: string) => { const { joplinUrl, joplinToken } = store.getState().settings