feat: 增加导出到obsidian功能,可选择导出路径 (#3373)
* feat: 增加导出到obsidian功能,可选择导出路径 * feat: 增加将内容导出到已有md文件 * fix: 修复日文翻译
This commit is contained in:
parent
94ba450323
commit
7096f81234
1
src/renderer/src/assets/images/apps/obsidian.svg
Normal file
1
src/renderer/src/assets/images/apps/obsidian.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1741953064519" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1980" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M379.28 597.76c26.12-7.76 68.52-19.6 117.04-22.84a411.88 411.88 0 0 1-30.56-195.36c6.52-66.04 30.16-121.52 53-168.4l13.88-28.56 17.92-36.72c9.4-20 16.32-37.52 19.6-54.24 3.24-16.28 3.24-30.56-0.8-44.44-4.12-13.88-12.28-28.56-28.6-44.84a68.08 68.08 0 0 0-63.2 15.08l-211.24 190a68.48 68.48 0 0 0-22 40.8l-18 120.72a597.04 597.04 0 0 1 152.96 228.8zM217.8 424l-4.08 12.24-111.76 248.8a69.32 69.32 0 0 0 13.08 75.84l175.76 180.64a354.8 354.8 0 0 0 35.88-354A557.88 557.88 0 0 0 217.8 424z" fill="#707070" p-id="1981"></path><path d="M331.6 963.16l9.36 0.8c33.04 0.8 89.32 4.08 134.56 12.24 37.12 6.92 110.92 27.32 171.28 44.84 46.08 13.88 93.8-23.2 100.32-70.92 4.92-34.68 14.28-73.84 31-110.12a379.24 379.24 0 0 0-103.6-163.96 225.92 225.92 0 0 0-118.24-53.4 386.2 386.2 0 0 0-163.96 19.16 395.56 395.56 0 0 1-61.16 321.36h0.4z" fill="#707070" p-id="1982"></path><path d="M807.48 791.04a2299.48 2299.48 0 0 0 79.12-126.4 34.68 34.68 0 0 0-2.44-37.92 742.32 742.32 0 0 1-87.28-143.6c-23.64-56.64-26.92-144.32-27.32-186.76 0-16.28-4.88-32.2-15.08-44.84l-136.64-173.32c0 7.76-1.6 15.48-3.24 23.24a305.68 305.68 0 0 1-22.84 63.6l-18.8 39.2-13.44 26.88a447.8 447.8 0 0 0-48.12 152.92 375.6 375.6 0 0 0 34.68 191.68 271.6 271.6 0 0 1 158.24 66.08 397.6 397.6 0 0 1 103.2 149.24z" fill="#707070" p-id="1983"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
228
src/renderer/src/components/ObsidianFolderSelector.tsx
Normal file
228
src/renderer/src/components/ObsidianFolderSelector.tsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
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<Props> = ({ defaultPath, obsidianUrl, obsidianApiKey, onPathChange }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<string[]>(['/'])
|
||||||
|
const [showMdFiles, setShowMdFiles] = useState<boolean>(false)
|
||||||
|
// 当前选中的节点信息
|
||||||
|
const [currentSelection, setCurrentSelection] = useState({
|
||||||
|
path: defaultPath,
|
||||||
|
isMdFile: false
|
||||||
|
})
|
||||||
|
// 使用key强制Tree组件重新渲染
|
||||||
|
const [treeKey, setTreeKey] = useState<number>(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 <FileOutlined />
|
||||||
|
}
|
||||||
|
return <FolderOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<SwitchContainer>
|
||||||
|
<span>{t('chat.topics.export.obsidian_show_md_files')}</span>
|
||||||
|
<Switch checked={showMdFiles} onChange={handleSwitchChange} />
|
||||||
|
</SwitchContainer>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<TreeContainer>
|
||||||
|
<Tree
|
||||||
|
key={treeKey} // 使用key来强制重新渲染
|
||||||
|
defaultSelectedKeys={[defaultPath]}
|
||||||
|
expandedKeys={expandedKeys}
|
||||||
|
onExpand={(keys) => 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}
|
||||||
|
/>
|
||||||
|
</TreeContainer>
|
||||||
|
</Spin>
|
||||||
|
<div>
|
||||||
|
{currentSelection.path !== defaultPath && (
|
||||||
|
<SelectedPath>
|
||||||
|
{t('chat.topics.export.obsidian_selected_path')}: {currentSelection.path}
|
||||||
|
</SelectedPath>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
72
src/renderer/src/components/Popups/ObsidianExportPopup.tsx
Normal file
72
src/renderer/src/components/Popups/ObsidianExportPopup.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import ObsidianFolderSelector from '@renderer/components/ObsidianFolderSelector'
|
||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import store from '@renderer/store'
|
||||||
|
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||||
|
|
||||||
|
interface ObsidianExportOptions {
|
||||||
|
title: string
|
||||||
|
markdown: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于显示 Obsidian 导出对话框
|
||||||
|
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||||
|
const { title, markdown } = options
|
||||||
|
const obsidianUrl = store.getState().settings.obsidianUrl
|
||||||
|
const obsidianApiKey = store.getState().settings.obsidianApiKey
|
||||||
|
|
||||||
|
if (!obsidianUrl || !obsidianApiKey) {
|
||||||
|
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建一个状态变量来存储选择的路径
|
||||||
|
let selectedPath = '/'
|
||||||
|
let selectedIsMdFile = false
|
||||||
|
|
||||||
|
// 显示文件夹选择对话框
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
window.modal.confirm({
|
||||||
|
title: i18n.t('chat.topics.export.obsidian_select_folder'),
|
||||||
|
content: (
|
||||||
|
<ObsidianFolderSelector
|
||||||
|
defaultPath={selectedPath}
|
||||||
|
obsidianUrl={obsidianUrl}
|
||||||
|
obsidianApiKey={obsidianApiKey}
|
||||||
|
onPathChange={(path, isMdFile) => {
|
||||||
|
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 ObsidianExportPopup = {
|
||||||
|
show: showObsidianExportDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObsidianExportPopup
|
||||||
@ -162,6 +162,15 @@
|
|||||||
"topics.export.title": "Export",
|
"topics.export.title": "Export",
|
||||||
"topics.export.word": "Export as Word",
|
"topics.export.word": "Export as Word",
|
||||||
"topics.export.yuque": "Export to Yuque",
|
"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_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.list": "Topic List",
|
"topics.list": "Topic List",
|
||||||
"topics.move_to": "Move to",
|
"topics.move_to": "Move to",
|
||||||
"topics.pinned": "Pinned Topics",
|
"topics.pinned": "Pinned Topics",
|
||||||
@ -748,6 +757,21 @@
|
|||||||
"title": "Yuque Configuration",
|
"title": "Yuque Configuration",
|
||||||
"token": "Yuque Token",
|
"token": "Yuque Token",
|
||||||
"token_placeholder": "Please enter the Yuque Token"
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display.assistant.title": "Assistant Settings",
|
"display.assistant.title": "Assistant Settings",
|
||||||
|
|||||||
@ -162,6 +162,15 @@
|
|||||||
"topics.export.title": "エクスポート",
|
"topics.export.title": "エクスポート",
|
||||||
"topics.export.word": "Wordとしてエクスポート",
|
"topics.export.word": "Wordとしてエクスポート",
|
||||||
"topics.export.yuque": "語雀にエクスポート",
|
"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_export_success": "エクスポート成功",
|
||||||
|
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
||||||
|
"topics.export.obsidian_show_md_files": "mdファイルを表示",
|
||||||
|
"topics.export.obsidian_selected_path": "選択済みパス",
|
||||||
"topics.list": "トピックリスト",
|
"topics.list": "トピックリスト",
|
||||||
"topics.move_to": "移動先",
|
"topics.move_to": "移動先",
|
||||||
"topics.pinned": "トピックを固定",
|
"topics.pinned": "トピックを固定",
|
||||||
@ -691,8 +700,8 @@
|
|||||||
"markdown_export.help": "入力された場合、エクスポート時に自動的にこのパスに保存されます。未入力の場合、保存ダイアログが表示されます。",
|
"markdown_export.help": "入力された場合、エクスポート時に自動的にこのパスに保存されます。未入力の場合、保存ダイアログが表示されます。",
|
||||||
"notion.api_key": "Notion APIキー",
|
"notion.api_key": "Notion APIキー",
|
||||||
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||||
"notion.auto_split": "내보내기 시 자동 분할",
|
"notion.auto_split": "ダイアログをエクスポートすると自動ページ分割",
|
||||||
"notion.auto_split_tip": "긴 주제를 Notion으로 내보낼 때 자동으로 페이지 분할",
|
"notion.auto_split_tip": "ダイアログが長い場合、Notionに自動的にページ分割してエクスポートします",
|
||||||
"notion.check": {
|
"notion.check": {
|
||||||
"button": "確認",
|
"button": "確認",
|
||||||
"empty_api_key": "Api_keyが設定されていません",
|
"empty_api_key": "Api_keyが設定されていません",
|
||||||
@ -706,9 +715,9 @@
|
|||||||
"notion.help": "Notion 設定ドキュメント",
|
"notion.help": "Notion 設定ドキュメント",
|
||||||
"notion.page_name_key": "ページタイトルフィールド名",
|
"notion.page_name_key": "ページタイトルフィールド名",
|
||||||
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||||
"notion.split_size": "분할 크기",
|
"notion.split_size": "自動ページ分割サイズ",
|
||||||
"notion.split_size_help": "권장: 무료 플랜 90, Plus 플랜 24990, 기본값 90",
|
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
|
||||||
"notion.split_size_placeholder": "페이지당 블록 제한 입력(기본값 90)",
|
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
|
||||||
"notion.title": "Notion 設定",
|
"notion.title": "Notion 設定",
|
||||||
"title": "データ設定",
|
"title": "データ設定",
|
||||||
"webdav": {
|
"webdav": {
|
||||||
@ -748,6 +757,21 @@
|
|||||||
"title": "Yuque設定",
|
"title": "Yuque設定",
|
||||||
"token": "Yuqueトークン",
|
"token": "Yuqueトークン",
|
||||||
"token_placeholder": "Yuqueトークンを入力してください"
|
"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 を入力してください"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display.assistant.title": "アシスタント設定",
|
"display.assistant.title": "アシスタント設定",
|
||||||
|
|||||||
@ -162,6 +162,15 @@
|
|||||||
"topics.export.title": "Экспорт",
|
"topics.export.title": "Экспорт",
|
||||||
"topics.export.word": "Экспорт как Word",
|
"topics.export.word": "Экспорт как Word",
|
||||||
"topics.export.yuque": "Экспорт в Yuque",
|
"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_export_success": "Экспорт успешно завершен",
|
||||||
|
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
||||||
|
"topics.export.obsidian_show_md_files": "Показать файлы MD",
|
||||||
|
"topics.export.obsidian_selected_path": "Выбранный путь",
|
||||||
"topics.list": "Список топиков",
|
"topics.list": "Список топиков",
|
||||||
"topics.move_to": "Переместить в",
|
"topics.move_to": "Переместить в",
|
||||||
"topics.pinned": "Закрепленные темы",
|
"topics.pinned": "Закрепленные темы",
|
||||||
@ -748,6 +757,21 @@
|
|||||||
"title": "Настройка Yuque",
|
"title": "Настройка Yuque",
|
||||||
"token": "Токен Yuque",
|
"token": "Токен Yuque",
|
||||||
"token_placeholder": "Введите токен Yuque"
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display.assistant.title": "Настройки ассистентов",
|
"display.assistant.title": "Настройки ассистентов",
|
||||||
|
|||||||
@ -162,6 +162,15 @@
|
|||||||
"topics.export.title": "导出",
|
"topics.export.title": "导出",
|
||||||
"topics.export.word": "导出为 Word",
|
"topics.export.word": "导出为 Word",
|
||||||
"topics.export.yuque": "导出到语雀",
|
"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_export_success": "导出成功",
|
||||||
|
"topics.export.obsidian_export_failed": "导出失败",
|
||||||
|
"topics.export.obsidian_show_md_files": "显示md文件",
|
||||||
|
"topics.export.obsidian_selected_path": "已选择路径",
|
||||||
"topics.list": "话题列表",
|
"topics.list": "话题列表",
|
||||||
"topics.move_to": "移动到",
|
"topics.move_to": "移动到",
|
||||||
"topics.pinned": "固定话题",
|
"topics.pinned": "固定话题",
|
||||||
@ -748,6 +757,21 @@
|
|||||||
"title": "语雀配置",
|
"title": "语雀配置",
|
||||||
"token": "语雀 Token",
|
"token": "语雀 Token",
|
||||||
"token_placeholder": "请输入语雀Token"
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display.assistant.title": "助手设置",
|
"display.assistant.title": "助手设置",
|
||||||
|
|||||||
@ -162,6 +162,15 @@
|
|||||||
"topics.export.title": "匯出",
|
"topics.export.title": "匯出",
|
||||||
"topics.export.word": "匯出為 Word",
|
"topics.export.word": "匯出為 Word",
|
||||||
"topics.export.yuque": "匯出到語雀",
|
"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_export_success": "匯出成功",
|
||||||
|
"topics.export.obsidian_export_failed": "匯出失敗",
|
||||||
|
"topics.export.obsidian_show_md_files": "顯示md文件",
|
||||||
|
"topics.export.obsidian_selected_path": "已選擇路徑",
|
||||||
"topics.list": "話題列表",
|
"topics.list": "話題列表",
|
||||||
"topics.move_to": "移動到",
|
"topics.move_to": "移動到",
|
||||||
"topics.pinned": "固定話題",
|
"topics.pinned": "固定話題",
|
||||||
@ -748,6 +757,21 @@
|
|||||||
"title": "語雀設定",
|
"title": "語雀設定",
|
||||||
"token": "語雀 Token",
|
"token": "語雀 Token",
|
||||||
"token_placeholder": "請輸入語雀 Token"
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display.assistant.title": "助手設定",
|
"display.assistant.title": "助手設定",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
TranslationOutlined
|
TranslationOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { UploadOutlined } from '@ant-design/icons'
|
import { UploadOutlined } from '@ant-design/icons'
|
||||||
|
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||||
@ -212,6 +213,15 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const markdown = messageToMarkdown(message)
|
const markdown = messageToMarkdown(message)
|
||||||
exportMarkdownToYuque(title, markdown)
|
exportMarkdownToYuque(title, markdown)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('chat.topics.export.obsidian'),
|
||||||
|
key: 'obsidian',
|
||||||
|
onClick: async () => {
|
||||||
|
const markdown = messageToMarkdown(message)
|
||||||
|
const title = getMessageTitle(message)
|
||||||
|
await ObsidianExportPopup.show({ title, markdown })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import DragableList from '@renderer/components/DragableList'
|
import DragableList from '@renderer/components/DragableList'
|
||||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
|
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
@ -25,6 +26,7 @@ import { Assistant, Topic } from '@renderer/types'
|
|||||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||||
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
||||||
import {
|
import {
|
||||||
|
exportMarkdownToNotion,
|
||||||
exportMarkdownToYuque,
|
exportMarkdownToYuque,
|
||||||
exportTopicAsMarkdown,
|
exportTopicAsMarkdown,
|
||||||
exportTopicToNotion,
|
exportTopicToNotion,
|
||||||
@ -254,6 +256,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
const markdown = await topicToMarkdown(topic)
|
const markdown = await topicToMarkdown(topic)
|
||||||
exportMarkdownToYuque(topic.name, markdown)
|
exportMarkdownToYuque(topic.name, markdown)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('chat.topics.export.obsidian'),
|
||||||
|
key: 'obsidian',
|
||||||
|
onClick: async () => {
|
||||||
|
const markdown = await topicToMarkdown(topic)
|
||||||
|
await ObsidianExportPopup.show({ title: topic.name, markdown })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import MarkdownExportSettings from './MarkdownExportSettings'
|
|||||||
import NotionSettings from './NotionSettings'
|
import NotionSettings from './NotionSettings'
|
||||||
import WebDavSettings from './WebDavSettings'
|
import WebDavSettings from './WebDavSettings'
|
||||||
import YuqueSettings from './YuqueSettings'
|
import YuqueSettings from './YuqueSettings'
|
||||||
|
import ObsidianSettings from './ObsidianSettings'
|
||||||
|
import ObsidianIcon from '@renderer/assets/images/apps/obsidian.svg'
|
||||||
|
|
||||||
const DataSettings: FC = () => {
|
const DataSettings: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -43,7 +45,12 @@ const DataSettings: FC = () => {
|
|||||||
icon: <FileMarkdownOutlined style={{ fontSize: 16 }} />
|
icon: <FileMarkdownOutlined style={{ fontSize: 16 }} />
|
||||||
},
|
},
|
||||||
{ key: 'notion', title: 'settings.data.notion.title', icon: <i className="iconfont icon-notion" /> },
|
{ key: 'notion', title: 'settings.data.notion.title', icon: <i className="iconfont icon-notion" /> },
|
||||||
{ key: 'yuque', title: 'settings.data.yuque.title', icon: <YuqueOutlined style={{ fontSize: 16 }} /> }
|
{ key: 'yuque', title: 'settings.data.yuque.title', icon: <YuqueOutlined style={{ fontSize: 16 }} /> },
|
||||||
|
{
|
||||||
|
key: 'obsidian',
|
||||||
|
title: 'settings.data.obsidian.title',
|
||||||
|
icon: <img src={ObsidianIcon} alt="obsidian" style={{ width: '16px', height: '16px' }} />
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -180,6 +187,7 @@ const DataSettings: FC = () => {
|
|||||||
{menu === 'markdown_export' && <MarkdownExportSettings />}
|
{menu === 'markdown_export' && <MarkdownExportSettings />}
|
||||||
{menu === 'notion' && <NotionSettings />}
|
{menu === 'notion' && <NotionSettings />}
|
||||||
{menu === 'yuque' && <YuqueSettings />}
|
{menu === 'yuque' && <YuqueSettings />}
|
||||||
|
{menu === 'obsidian' && <ObsidianSettings />}
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
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 Input from 'antd/es/input/Input'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
|
||||||
|
const ObsidianSettings: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey)
|
||||||
|
const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl)
|
||||||
|
|
||||||
|
const handleObsidianApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(setObsidianApiKey(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleObsidianUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(setObsidianUrl(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleObsidianUrlBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
let url = e.target.value
|
||||||
|
// 确保URL以/结尾,但只在失去焦点时执行
|
||||||
|
if (url && !url.endsWith('/')) {
|
||||||
|
url = `${url}/`
|
||||||
|
dispatch(setObsidianUrl(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handleObsidianHelpClick = () => {
|
||||||
|
MinApp.start({
|
||||||
|
id: 'obsidian-help',
|
||||||
|
name: 'Obsidian Help',
|
||||||
|
url: 'https://github.com/coddingtonbear/obsidian-local-rest-api'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup theme={theme}>
|
||||||
|
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.obsidian.url')}</SettingRowTitle>
|
||||||
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={obsidianUrl || ''}
|
||||||
|
onChange={handleObsidianUrlChange}
|
||||||
|
onBlur={handleObsidianUrlBlur}
|
||||||
|
style={{ width: 315 }}
|
||||||
|
placeholder={t('settings.data.obsidian.url_placeholder')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>
|
||||||
|
{t('settings.data.obsidian.api_key')}
|
||||||
|
<Tooltip title={t('settings.data.obsidian.help')} placement="left">
|
||||||
|
<InfoCircleOutlined
|
||||||
|
style={{ color: 'var(--color-text-2)', cursor: 'pointer', marginLeft: 4 }}
|
||||||
|
onClick={handleObsidianHelpClick}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitle>
|
||||||
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={obsidianApiKey || ''}
|
||||||
|
onChange={handleObsidianApiKeyChange}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
placeholder={t('settings.data.obsidian.api_key_placeholder')}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleObsidianConnectionCheck}>{t('settings.data.obsidian.check.button')}</Button>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObsidianSettings
|
||||||
@ -79,6 +79,8 @@ export interface SettingsState {
|
|||||||
yuqueToken: string | null
|
yuqueToken: string | null
|
||||||
yuqueUrl: string | null
|
yuqueUrl: string | null
|
||||||
yuqueRepoId: string | null
|
yuqueRepoId: string | null
|
||||||
|
obsidianApiKey: string | null
|
||||||
|
obsidianUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
@ -143,7 +145,9 @@ const initialState: SettingsState = {
|
|||||||
notionSplitSize: 90,
|
notionSplitSize: 90,
|
||||||
yuqueToken: '',
|
yuqueToken: '',
|
||||||
yuqueUrl: '',
|
yuqueUrl: '',
|
||||||
yuqueRepoId: ''
|
yuqueRepoId: '',
|
||||||
|
obsidianApiKey: '',
|
||||||
|
obsidianUrl: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -332,6 +336,12 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setYuqueUrl: (state, action: PayloadAction<string>) => {
|
setYuqueUrl: (state, action: PayloadAction<string>) => {
|
||||||
state.yuqueUrl = action.payload
|
state.yuqueUrl = action.payload
|
||||||
|
},
|
||||||
|
setObsidianApiKey: (state, action: PayloadAction<string>) => {
|
||||||
|
state.obsidianApiKey = action.payload
|
||||||
|
},
|
||||||
|
setObsidianUrl: (state, action: PayloadAction<string>) => {
|
||||||
|
state.obsidianUrl = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -395,7 +405,9 @@ export const {
|
|||||||
setNotionSplitSize,
|
setNotionSplitSize,
|
||||||
setYuqueToken,
|
setYuqueToken,
|
||||||
setYuqueRepoId,
|
setYuqueRepoId,
|
||||||
setYuqueUrl
|
setYuqueUrl,
|
||||||
|
setObsidianApiKey,
|
||||||
|
setObsidianUrl
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -316,3 +316,64 @@ export const exportMarkdownToYuque = async (title: string, content: string) => {
|
|||||||
setExportState({ isExporting: false })
|
setExportState({ isExporting: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出Markdown到Obsidian
|
||||||
|
*/
|
||||||
|
export const exportMarkdownToObsidian = async (
|
||||||
|
fileName: string,
|
||||||
|
markdown: string,
|
||||||
|
selectedPath: string,
|
||||||
|
isMdFile: boolean = false
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const obsidianUrl = store.getState().settings.obsidianUrl
|
||||||
|
const obsidianApiKey = store.getState().settings.obsidianApiKey
|
||||||
|
|
||||||
|
if (!obsidianUrl || !obsidianApiKey) {
|
||||||
|
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.message.success(i18n.t('chat.topics.export.obsidian_export_success'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出到Obsidian失败:', error)
|
||||||
|
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user