diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 1fb6fdac..527894af 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -141,6 +141,7 @@ "topics.export.image": "Export as image", "topics.export.md": "Export as markdown", "topics.export.notion": "Export to Notion", + "topics.export.yuque": "Export to Yuque", "topics.export.title": "Export", "topics.export.word": "Export as Word", "topics.list": "Topic List", @@ -380,7 +381,9 @@ "error.invalid.proxy.url": "Invalid proxy URL", "error.invalid.webdav": "Invalid WebDAV settings", "error.notion.export": "Failed to export to Notion. Please check connection status and configuration according to documentation", + "error.yuque.export": "Failed to export to Yuque. Please check connection status and configuration according to documentation", "error.notion.no_api_key": "Notion ApiKey or Notion DatabaseID is not configured", + "error.yuque.no_config": "Yuque Token or Yuque Url is not configured", "group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers", "group.delete.title": "Delete Group Message", "mention.title": "Switch model answer", @@ -402,6 +405,7 @@ "restore.success": "Restored successfully", "save.success.title": "Saved successfully", "success.notion.export": "Successfully exported to Notion", + "success.yuque.export": "Successfully exported to Yuque", "switch.disabled": "Please wait for the current reply to complete", "topic.added": "New topic added", "upgrade.success.button": "Restart", @@ -641,8 +645,24 @@ "syncStatus": "Backup Status", "title": "WebDAV", "user": "WebDAV User" + }, + "yuque": { + "title": "Yuque Configuration", + "repo_url": "Yuque URL", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "token": "Yuque Token", + "token_placeholder": "Please enter the Yuque Token", + "help": "Get Yuque Token", + "check": { + "button": "Verify Connection", + "success": "Yuque connection verified successfully", + "fail": "Yuque connection verification failed", + "empty_token": "Please enter the Yuque Token first", + "empty_repo_url": "Please enter the knowledge base URL first" + } } }, + "display.custom.css": "Custom CSS", "display.custom.css.placeholder": "/* Put custom CSS here */", "display.minApp.disabled": "Hidden MinApp", @@ -847,4 +867,4 @@ "visualization": "Visualization" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 0e45993a..6419b804 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -141,6 +141,7 @@ "topics.export.image": "画像としてエクスポート", "topics.export.md": "Markdownとしてエクスポート", "topics.export.notion": "Notion にエクスポート", + "topics.export.yuque": "語雀にエクスポート", "topics.export.title": "エクスポート", "topics.export.word": "Wordとしてエクスポート", "topics.list": "トピックリスト", @@ -380,7 +381,9 @@ "error.invalid.proxy.url": "無効なプロキシURL", "error.invalid.webdav": "無効なWebDAV設定", "error.notion.export": "Notionへのエクスポートに失敗しました。接続状態と設定を確認してください", + "error.yuque.export": "語雀へのエクスポートに失敗しました。接続状態と設定を確認してください", "error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません", + "error.yuque.no_config": "語雀Token または 知識ベースID が設定されていません", "group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます", "group.delete.title": "分組メッセージを削除", "mention.title": "モデルを切り替える", @@ -402,6 +405,7 @@ "restore.success": "復元に成功しました", "save.success.title": "保存に成功しました", "success.notion.export": "Notionへのエクスポートに成功しました", + "success.yuque.export": "語雀へのエクスポートに成功しました", "switch.disabled": "現在の応答が完了するまで切り替えを無効にします", "topic.added": "新しいトピックが追加されました", "upgrade.success.button": "再起動", @@ -641,6 +645,21 @@ "syncStatus": "バックアップ状態", "title": "WebDAV", "user": "WebDAVユーザー" + }, + "yuque": { + "title": "Yuque設定", + "repo_url": "ナレッジベースURL", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "token": "Yuqueトークン", + "token_placeholder": "Yuqueトークンを入力してください", + "help": "Yuqueトークンを取得", + "check": { + "button": "接続確認", + "success": "Yuque接続確認に成功しました", + "fail": "Yuque接続確認に失敗しました", + "empty_token": "先にYuqueトークンを入力してください", + "empty_repo_url": "先にナレッジベースURLを入力してください" + } } }, "display.custom.css": "カスタムCSS", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 9396cf7c..fa825fec 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -141,6 +141,7 @@ "topics.export.image": "Экспорт как изображение", "topics.export.md": "Экспорт как markdown", "topics.export.notion": "Экспорт в Notion", + "topics.export.yuque": "Экспорт в Yuque", "topics.export.title": "Экспорт", "topics.export.word": "Экспорт как Word", "topics.list": "Список топиков", @@ -380,7 +381,9 @@ "error.invalid.proxy.url": "Неверный URL прокси", "error.invalid.webdav": "Неверные настройки WebDAV", "error.notion.export": "Ошибка экспорта в Notion, пожалуйста, проверьте состояние подключения и настройки в документации", + "error.yuque.export": "Ошибка экспорта в Yuque, пожалуйста, проверьте состояние подключения и настройки в документации", "error.notion.no_api_key": "Notion ApiKey или Notion DatabaseID не настроен", + "error.yuque.no_config": "Yuque Token или Yuque Url не настроен", "group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника", "group.delete.title": "Удалить группу сообщений", "mention.title": "Переключить модель ответа", @@ -402,6 +405,7 @@ "restore.success": "Успешно восстановлено", "save.success.title": "Успешно сохранено", "success.notion.export": "Успешный экспорт в Notion", + "success.yuque.export": "Успешный экспорт в Yuque", "switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа", "topic.added": "Новый топик добавлен", "upgrade.success.button": "Перезапустить", @@ -637,6 +641,21 @@ "title": "WebDAV", "user": "Пользователь WebDAV" }, + "yuque": { + "title": "Настройка Yuque", + "repo_url": "URL базы знаний", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "token": "Токен Yuque", + "token_placeholder": "Введите токен Yuque", + "help": "Получить токен Yuque", + "check": { + "button": "Проверить подключение", + "success": "Подключение к Yuque успешно проверено", + "fail": "Не удалось проверить подключение к Yuque", + "empty_token": "Сначала введите токен Yuque", + "empty_repo_url": "Сначала введите URL базы знаний" + } + }, "notion.auto_split_tip": "Автоматическое разбиение на страницы при экспорте в Notion, если тема слишком длинная", "notion.auto_split": "Автоматическое разбиение на страницы при экспорте диалога", "notion.split_size": "Размер автоматического разбиения", @@ -847,4 +866,4 @@ "visualization": "Визуализация" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 75a29f4b..d75439dd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -141,6 +141,7 @@ "topics.export.image": "导出为图片", "topics.export.md": "导出为 Markdown", "topics.export.notion": "导出到 Notion", + "topics.export.yuque": "导出到语雀", "topics.export.title": "导出", "topics.export.word": "导出为 Word", "topics.list": "话题列表", @@ -380,7 +381,9 @@ "error.invalid.proxy.url": "无效的代理地址", "error.invalid.webdav": "无效的 WebDAV 设置", "error.notion.export": "导出Notion错误,请检查连接状态并对照文档检查配置", + "error.yuque.export": "导出语雀错误,请检查连接状态并对照文档检查配置", "error.notion.no_api_key": "未配置Notion ApiKey或Notion DatabaseID", + "error.yuque.no_config": "未配置语雀Token 或 知识库Url", "group.delete.content": "删除分组消息会删除用户提问和所有助手的回答", "group.delete.title": "删除分组消息", "mention.title": "切换模型回答", @@ -402,6 +405,7 @@ "restore.success": "恢复成功", "save.success.title": "保存成功", "success.notion.export": "成功导出到Notion", + "success.yuque.export": "成功导出到语雀", "switch.disabled": "请等待当前回复完成后操作", "topic.added": "话题添加成功", "upgrade.success.button": "重启", @@ -641,6 +645,21 @@ "syncStatus": "备份状态", "title": "WebDAV", "user": "WebDAV 用户名" + }, + "yuque": { + "title": "语雀配置", + "repo_url": "知识库 URL", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "token": "语雀 Token", + "token_placeholder": "请输入语雀Token", + "help": "获取语雀 Token", + "check": { + "button": "验证连接", + "success": "语雀连接验证成功", + "fail": "语雀连接验证失败", + "empty_token": "请先输入语雀Token", + "empty_repo_url": "请先输入知识库URL" + } } }, "display.custom.css": "自定义 CSS", @@ -847,4 +866,4 @@ "visualization": "可视化" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f97c10f0..50b733c7 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -141,6 +141,7 @@ "topics.export.image": "匯出為圖片", "topics.export.md": "匯出為 Markdown", "topics.export.notion": "匯出到 Notion", + "topics.export.yuque": "匯出到語雀", "topics.export.title": "匯出", "topics.export.word": "導出為 Word", "topics.list": "話題列表", @@ -380,7 +381,9 @@ "error.invalid.proxy.url": "無效的代理 URL", "error.invalid.webdav": "無效的 WebDAV 設定", "error.notion.export": "導出Notion錯誤,請檢查連接狀態並對照文檔檢查配置", + "error.yuque.export": "導出語雀錯誤,請檢查連接狀態並對照文檔檢查配置", "error.notion.no_api_key": "未配置 Notion ApiKey 或 Notion DatabaseID", + "error.yuque.no_config": "未配置語雀Token 或 知識庫Url", "group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答", "group.delete.title": "刪除分組消息", "mention.title": "切換模型回答", @@ -402,6 +405,7 @@ "restore.success": "恢復成功", "save.success.title": "保存成功", "success.notion.export": "成功導出到Notion", + "success.yuque.export": "成功導出到語雀", "switch.disabled": "請等待當前回覆完成", "topic.added": "新話題已添加", "upgrade.success.button": "重新啟動", @@ -641,6 +645,21 @@ "syncStatus": "備份狀態", "title": "WebDAV", "user": "WebDAV 使用者名稱" + }, + "yuque": { + "title": "語雀配置", + "repo_url": "知識庫 URL", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "token": "語雀 Token", + "token_placeholder": "請輸入語雀Token", + "help": "獲取語雀 Token", + "check": { + "button": "驗證連接", + "success": "語雀連接驗證成功", + "fail": "語雀連接驗證失敗", + "empty_token": "請先輸入語雀Token", + "empty_repo_url": "請先輸入知識庫URL" + } } }, "display.custom.css": "自定義 CSS", @@ -847,4 +866,4 @@ "visualization": "可視化" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index fbf1b5dd..c109572c 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -26,7 +26,12 @@ import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils' -import { exportMarkdownToNotion, exportMessageAsMarkdown, messageToMarkdown } from '@renderer/utils/export' +import { + exportMarkdownToNotion, + exportMessageAsMarkdown, + messageToMarkdown, + exportMarkdownToYuque +} from '@renderer/utils/export' import { Button, Dropdown, Popconfirm, Tooltip } from 'antd' import dayjs from 'dayjs' import { isEmpty } from 'lodash' @@ -258,6 +263,15 @@ const MessageMenubar: FC = (props) => { const markdown = messageToMarkdown(message) exportMarkdownToNotion(title, markdown) } + }, + { + label: t('chat.topics.export.yuque'), + key: 'yuque', + onClick: async () => { + const title = getMessageTitle(message) + const markdown = messageToMarkdown(message) + exportMarkdownToYuque(title, markdown) + } } ] } diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 23151b9c..ef8c0750 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -23,7 +23,12 @@ import store from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' import { Assistant, Topic } from '@renderer/types' import { copyTopicAsMarkdown } from '@renderer/utils/copy' -import { exportMarkdownToNotion, exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export' +import { + exportMarkdownToNotion, + exportMarkdownToYuque, + exportTopicAsMarkdown, + topicToMarkdown +} from '@renderer/utils/export' import { Dropdown, MenuProps, Tooltip } from 'antd' import dayjs from 'dayjs' import { findIndex } from 'lodash' @@ -239,6 +244,14 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic const markdown = await topicToMarkdown(topic) exportMarkdownToNotion(topic.name, markdown) } + }, + { + label: t('chat.topics.export.yuque'), + key: 'yuque', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToYuque(topic.name, markdown) + } } ] } diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index c8a5e036..90c81854 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -10,7 +10,10 @@ import { setNotionAutoSplit, setNotionDatabaseID, setNotionPageNameKey, - setNotionSplitSize + setNotionSplitSize, + setYuqueRepoId, + setYuqueToken, + setYuqueUrl } from '@renderer/store/settings' import { AppInfo } from '@renderer/types' import { Button, InputNumber, Modal, Switch, Tooltip, Typography } from 'antd' @@ -189,6 +192,107 @@ const NotionSettings: FC = () => { ) } +const YuqueSettings: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + + const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken) + const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl) + + const handleYuqueTokenChange = (e: React.ChangeEvent) => { + dispatch(setYuqueToken(e.target.value)) + } + + const handleYuqueRepoUrlChange = (e: React.ChangeEvent) => { + dispatch(setYuqueUrl(e.target.value)) + } + + const handleYuqueConnectionCheck = async () => { + if (!yuqueToken) { + window.message.error(t('settings.data.yuque.check.empty_token')) + return + } + if (!yuqueUrl) { + window.message.error(t('settings.data.yuque.check.empty_url')) + return + } + + const response = await fetch('https://www.yuque.com/api/v2/hello', { + headers: { + 'X-Auth-Token': yuqueToken + } + }) + + if (!response.ok) { + window.message.error(t('settings.data.yuque.check.fail')) + return + } + const yuqueSlug = yuqueUrl.replace('https://www.yuque.com/', '') + const repoIDResponse = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueSlug}`, { + headers: { + 'X-Auth-Token': yuqueToken + } + }) + if (!repoIDResponse.ok) { + window.message.error(t('settings.data.yuque.check.fail')) + return + } + const data = await repoIDResponse.json() + dispatch(setYuqueRepoId(data.data.id)) + window.message.success(t('settings.data.yuque.check.success')) + } + + const handleYuqueHelpClick = () => { + MinApp.start({ + id: 'yuque-help', + name: 'Yuque Help', + url: 'https://www.yuque.com/settings/tokens' + }) + } + + return ( + + {t('settings.data.yuque.title')} + + + {t('settings.data.yuque.repo_url')} + + + + + + + + {t('settings.data.yuque.token')} + + + + + + + + + + + ) +} + const DataSettings: FC = () => { const { t } = useTranslation() const [appInfo, setAppInfo] = useState() @@ -258,6 +362,7 @@ const DataSettings: FC = () => { + {t('settings.data.data.title')} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 5728b27c..1a5640d0 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -73,6 +73,9 @@ export interface SettingsState { thoughtAutoCollapse: boolean notionAutoSplit: boolean notionSplitSize: number + yuqueToken: string | null + yuqueUrl: string | null + yuqueRepoId: string | null } export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid' @@ -131,7 +134,10 @@ const initialState: SettingsState = { notionPageNameKey: 'Name', thoughtAutoCollapse: true, notionAutoSplit: false, - notionSplitSize: 90 + notionSplitSize: 90, + yuqueToken: '', + yuqueUrl: '', + yuqueRepoId: '' } const settingsSlice = createSlice({ @@ -302,6 +308,15 @@ const settingsSlice = createSlice({ }, setNotionSplitSize: (state, action: PayloadAction) => { state.notionSplitSize = action.payload + }, + setYuqueToken: (state, action: PayloadAction) => { + state.yuqueToken = action.payload + }, + setYuqueRepoId: (state, action: PayloadAction) => { + state.yuqueRepoId = action.payload + }, + setYuqueUrl: (state, action: PayloadAction) => { + state.yuqueUrl = action.payload } } }) @@ -359,7 +374,10 @@ export const { setNotionPageNameKey, setThoughtAutoCollapse, setNotionAutoSplit, - setNotionSplitSize + setNotionSplitSize, + setYuqueToken, + setYuqueRepoId, + setYuqueUrl } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index 1c131060..e99e91cb 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -215,3 +215,76 @@ export const exportMarkdownToNotion = async (title: string, content: string) => }) } } + +export const exportMarkdownToYuque = async (title: string, content: string) => { + const { isExporting } = store.getState().runtime.export + const { yuqueToken, yuqueRepoId } = store.getState().settings + + if (isExporting) { + window.message.warning({ content: i18n.t('message.warn.yuque.exporting'), key: 'yuque-exporting' }) + return + } + + if (!yuqueToken || !yuqueRepoId) { + window.message.error({ content: i18n.t('message.error.yuque.no_config'), key: 'yuque-no-config-error' }) + return + } + + setExportState({ isExporting: true }) + + try { + const response = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueRepoId}/docs`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': yuqueToken, + 'User-Agent': 'CherryAI' + }, + body: JSON.stringify({ + title: title, + slug: Date.now().toString(), // 使用时间戳作为唯一slug + format: 'markdown', + body: content + }) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + const doc_id = data.data.id + + const tocResponse = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueRepoId}/toc`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': yuqueToken, + 'User-Agent': 'CherryAI' + }, + body: JSON.stringify({ + action: 'appendNode', + action_mode: 'sibling', + doc_ids: [doc_id] + }) + }) + + if (!tocResponse.ok) { + throw new Error(`HTTP error! status: ${tocResponse.status}`) + } + + window.message.success({ + content: i18n.t('message.success.yuque.export'), + key: 'yuque-success' + }) + return data + } catch (error: any) { + window.message.error({ + content: i18n.t('message.error.yuque.export'), + key: 'yuque-error' + }) + return null + } finally { + setExportState({ isExporting: false }) + } +}