refactor: enhance preference management and streamline component integrations

- Updated various components to utilize the usePreference hook, replacing previous useSettings references for improved consistency and maintainability.
- Introduced new preference types for ChatMessageStyle, ChatMessageNavigationMode, and MultiModelMessageStyle to enhance type safety.
- Refactored preference handling in multiple files, ensuring a more streamlined approach to managing user preferences.
- Cleaned up unused code and comments related to previous settings for better clarity and maintainability.
- Updated auto-generated preference mappings to reflect recent changes in preference structure.
This commit is contained in:
fullex 2025-09-03 16:48:04 +08:00
parent 68cd87e069
commit 566dd14fed
52 changed files with 3480 additions and 3333 deletions

View File

@ -65,3 +65,11 @@ export enum UpgradeChannel {
RC = 'rc', // 公测版本
BETA = 'beta' // 预览版本
}
export type ChatMessageStyle = 'plain' | 'bubble'
export type ChatMessageNavigationMode = 'none' | 'buttons' | 'anchor'
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
export type MultiModelGridPopoverTrigger = 'hover' | 'click'

View File

@ -13,7 +13,12 @@ import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import type {
AssistantIconType,
AssistantTabSortType,
ChatMessageNavigationMode,
ChatMessageStyle,
LanguageVarious,
MultiModelFoldDisplayMode,
MultiModelGridPopoverTrigger,
MultiModelMessageStyle,
ProxyMode,
SelectionActionItem,
SelectionFilterMode,
@ -22,7 +27,7 @@ import type {
SidebarIcon,
WindowStyle
} from '@shared/data/preferenceTypes'
import { LanguageVarious, ThemeMode, UpgradeChannel } from '@shared/data/preferenceTypes'
import { ThemeMode, UpgradeChannel } from '@shared/data/preferenceTypes'
/* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" },
@ -148,11 +153,11 @@ export interface PreferencesType {
// redux/settings/gridColumns
'chat.message.multi_model.grid_columns': number
// redux/settings/gridPopoverTrigger
'chat.message.multi_model.grid_popover_trigger': string
'chat.message.multi_model.grid_popover_trigger': MultiModelGridPopoverTrigger
// redux/settings/multiModelMessageStyle
'chat.message.multi_model.style': string
'chat.message.multi_model.style': MultiModelMessageStyle
// redux/settings/messageNavigation
'chat.message.navigation_mode': string
'chat.message.navigation_mode': ChatMessageNavigationMode
// redux/settings/renderInputMessageAsMarkdown
'chat.message.render_as_markdown': boolean
// redux/settings/showMessageDivider
@ -162,7 +167,7 @@ export interface PreferencesType {
// redux/settings/showPrompt
'chat.message.show_prompt': boolean
// redux/settings/messageStyle
'chat.message.style': string
'chat.message.style': ChatMessageStyle
// redux/settings/thoughtAutoCollapse
'chat.message.thought.auto_collapse': boolean
// redux/settings/narrowMode

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import {
@ -12,7 +12,6 @@ import {
} from '@renderer/utils/export'
import { Alert, Empty, Form, Input, Modal, Select, Spin, Switch, TreeSelect } from 'antd'
import React, { useEffect, useState } from 'react'
const logger = loggerService.withContext('ObsidianExportDialog')
const { Option } = Select
@ -142,7 +141,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
messages,
topic
}) => {
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
const [defaultObsidianVault, setDefaultObsidianVault] = usePreference('data.integration.obsidian.default_vault')
const [state, setState] = useState({
title,
tags: obsidianTags || '',
@ -202,7 +201,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
}
}
fetchVaults()
}, [defaultObsidianVault])
}, [defaultObsidianVault, setDefaultObsidianVault])
useEffect(() => {
if (selectedVault) {
@ -232,9 +231,9 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
if (topic) {
markdown = await topicToMarkdown(topic, exportReasoning)
} else if (messages && messages.length > 0) {
markdown = messagesToMarkdown(messages, exportReasoning)
markdown = await messagesToMarkdown(messages, exportReasoning)
} else if (message) {
markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message)
markdown = exportReasoning ? await messageToMarkdownWithReasoning(message) : await messageToMarkdown(message)
} else {
markdown = ''
}

View File

@ -1,7 +1,7 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { getBackupProgressLabel } from '@renderer/i18n/label'
import { backup } from '@renderer/services/BackupService'
import store from '@renderer/store'
import { IpcChannel } from '@shared/IpcChannel'
import { Modal, Progress } from 'antd'
import { useEffect, useState } from 'react'
@ -27,7 +27,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [progressData, setProgressData] = useState<ProgressData>()
const { t } = useTranslation()
const skipBackupFile = store.getState().settings.skipBackupFile
const [skipBackupFile] = usePreference('data.backup.general.skip_backup_file')
useEffect(() => {
const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => {

View File

@ -210,9 +210,9 @@ export class PreferenceService {
}
/**
* Get multiple preferences at once
* Get multiple preferences at once, return is Partial<PreferenceDefaultScopeType>
*/
public async getMultiple(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> {
public async getMultipleRaw(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> {
// Check which keys are already cached
const cachedResults: Partial<PreferenceDefaultScopeType> = {}
const uncachedKeys: PreferenceKeyType[] = []
@ -258,6 +258,23 @@ export class PreferenceService {
return cachedResults
}
/**
* Get multiple preferences at once and return them as a record of key-value pairs
*/
public async getMultiple<T extends Record<string, PreferenceKeyType>>(
keys: T
): Promise<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }> {
const values = await this.getMultipleRaw(Object.values(keys))
const result = {} as { [P in keyof T]: PreferenceDefaultScopeType[T[P]] }
for (const key in keys) {
result[key] = values[keys[key]]!
}
return result
}
/**
* Set multiple preferences at once with configurable update strategy
*/
@ -454,7 +471,7 @@ export class PreferenceService {
if (uncachedKeys.length > 0) {
try {
const values = await this.getMultiple(uncachedKeys)
const values = await this.getMultipleRaw(uncachedKeys)
logger.debug(`Preloaded ${Object.keys(values).length} preferences`)
} catch (error) {
logger.error('Failed to preload preferences:', error as Error)

View File

@ -315,7 +315,7 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
})
if (uncachedKeys.length > 0) {
preferenceService.getMultiple(uncachedKeys).catch((error) => {
preferenceService.getMultipleRaw(uncachedKeys).catch((error) => {
logger.error('Failed to load initial preferences:', error as Error)
})
}

View File

@ -21,25 +21,19 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant'
import useFullScreenNotice from './useFullScreenNotice'
import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler'
const logger = loggerService.withContext('useAppInit')
export function useAppInit() {
const dispatch = useAppDispatch()
const {
proxyUrl,
proxyBypassRules,
// language,
// windowStyle,
autoCheckUpdate,
proxyMode,
// customCss,
enableDataCollection
} = useSettings()
const [language] = usePreference('app.language')
const [windowStyle] = usePreference('ui.window_style')
const [customCss] = usePreference('ui.custom_css')
const [proxyUrl] = usePreference('app.proxy.url')
const [proxyBypassRules] = usePreference('app.proxy.bypass_rules')
const [autoCheckUpdate] = usePreference('app.dist.auto_update.enabled')
const [proxyMode] = usePreference('app.proxy.mode')
const [enableDataCollection] = usePreference('app.privacy.data_collection.enabled')
const { minappShow } = useRuntime()
const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel()

View File

@ -1,8 +1,10 @@
import store, { useAppSelector } from '@renderer/store'
import {
SettingsState
// setWindowStyle
} from '@renderer/store/settings'
//TODO data refactor
// this file will be removed
import { usePreference } from '@data/hooks/usePreference'
import { useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { SettingsState } from '@renderer/store/settings'
export function useSettings() {
const settings = useAppSelector((state) => state.settings)
@ -87,7 +89,7 @@ export function useSettings() {
}
export function useMessageStyle() {
const { messageStyle } = useSettings()
const [messageStyle] = usePreference('chat.message.style')
const isBubbleStyle = messageStyle === 'bubble'
return {
@ -112,6 +114,6 @@ export const getStoreSetting = (key: keyof SettingsState) => {
// }
// }
export const getEnableDeveloperMode = () => {
return store.getState().settings.enableDeveloperMode
}
// export const getEnableDeveloperMode = () => {
// return store.getState().settings.enableDeveloperMode
// }

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { fetchMessagesSummary } from '@renderer/services/ApiService'
@ -13,7 +14,6 @@ import { find, isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
@ -109,7 +109,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
topicRenamingLocks.add(topicId)
const topic = await getTopicById(topicId)
const enableTopicNaming = getStoreSetting('enableTopicNaming')
const enableTopicNaming = await preferenceService.get('topic.naming.enabled')
if (isEmpty(topic.messages)) {
return

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { builtinLanguages, UNKNOWN } from '@renderer/config/translate'
import { useAppSelector } from '@renderer/store'
import { TranslateLanguage } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { getTranslateOptions } from '@renderer/utils/translate'
@ -16,7 +16,7 @@ const logger = loggerService.withContext('useTranslate')
* - getLanguageByLangcode: 通过语言代码获取语言对象
*/
export default function useTranslate() {
const prompt = useAppSelector((state) => state.settings.translateModelPrompt)
const [prompt] = usePreference('feature.translate.model_prompt')
const [translateLanguages, setTranslateLanguages] = useState<TranslateLanguage[]>(builtinLanguages)
const [isLoaded, setIsLoaded] = useState(false)

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import KeyvStorage from '@kangfenmao/keyv-storage'
import { loggerService } from '@logger'
@ -5,8 +6,6 @@ import { startAutoSync } from './services/BackupService'
import { startNutstoreAutoSync } from './services/NutstoreService'
import storeSyncService from './services/StoreSyncService'
import { webTraceService } from './services/WebTraceService'
import store from './store'
loggerService.initWindowSource('mainWindow')
function initKeyv() {
@ -15,13 +14,18 @@ function initKeyv() {
}
function initAutoSync() {
setTimeout(() => {
const { webdavAutoSync, localBackupAutoSync, s3 } = store.getState().settings
const { nutstoreAutoSync } = store.getState().nutstore
if (webdavAutoSync || (s3 && s3.autoSync) || localBackupAutoSync) {
setTimeout(async () => {
const autoSyncStates = await preferenceService.getMultiple({
webdav: 'data.backup.webdav.auto_sync',
local: 'data.backup.local.auto_sync',
s3: 'data.backup.s3.auto_sync',
nutstore: 'data.backup.nutstore.auto_sync'
})
if (autoSyncStates.webdav || autoSyncStates.s3 || autoSyncStates.local) {
startAutoSync()
}
if (nutstoreAutoSync) {
if (autoSyncStates.nutstore) {
startNutstoreAutoSync()
}
}, 8000)

View File

@ -1,9 +1,9 @@
import { MessageOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import { getTopicById } from '@renderer/hooks/useTopic'
import { getAssistantById } from '@renderer/services/AssistantService'
@ -19,7 +19,6 @@ import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import { default as MessageItem } from '../../home/Messages/Message'
interface Props extends React.HTMLAttributes<HTMLDivElement> {
topic?: Topic
}
@ -27,7 +26,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
const navigate = NavigationService.navigate!
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const { messageStyle } = useSettings()
const [messageStyle] = usePreference('chat.message.style')
const { setTimeoutTimer } = useTimer()
const [topic, setTopic] = useState<Topic | undefined>(_topic)

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import { HStack } from '@renderer/components/Layout'
@ -6,7 +7,6 @@ import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { useTimer } from '@renderer/hooks/useTimer'
@ -36,7 +36,9 @@ interface Props {
const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle, messageNavigation } = useSettings()
const [topicPosition] = usePreference('topic.position')
const [messageStyle] = usePreference('chat.message.style')
const [messageNavigation] = usePreference('chat.message.navigation_mode')
const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic)
const { isTopNavbar } = useNavbarPosition()
@ -178,7 +180,8 @@ const Chat: FC<Props> = (props) => {
}
export const useChatMaxWidth = () => {
const { showTopics, topicPosition } = useSettings()
const [showTopics] = usePreference('topic.tab.show')
const [topicPosition] = usePreference('topic.position')
const { isLeftNavbar } = useNavbarPosition()
const { showAssistants } = useShowAssistants()
const showRightTopics = showTopics && topicPosition === 'right'

View File

@ -1,7 +1,7 @@
import { usePreference } from '@data/hooks/usePreference'
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
@ -30,7 +30,9 @@ const HomePage: FC = () => {
const [activeAssistant, _setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id, state?.topic)
const { showAssistants, showTopics, topicPosition } = useSettings()
const [showAssistants] = usePreference('assistant.tab.show')
const [showTopics] = usePreference('topic.tab.show')
const [topicPosition] = usePreference('topic.position')
const dispatch = useDispatch()
_activeAssistant = activeAssistant

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { HStack, VStack } from '@renderer/components/Layout'
import MaxContextCount from '@renderer/components/MaxContextCount'
import { useSettings } from '@renderer/hooks/useSettings'
import { Divider, Popover } from 'antd'
import { ArrowUp, MenuIcon } from 'lucide-react'
import { FC } from 'react'
@ -16,7 +16,7 @@ type Props = {
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount }) => {
const { t } = useTranslation()
const { showInputEstimatedTokens } = useSettings()
const [showInputEstimatedTokens] = usePreference('chat.input.show_estimated_tokens')
if (!showInputEstimatedTokens) {
return null

View File

@ -3,9 +3,9 @@ import 'katex/dist/contrib/copy-tex'
import 'katex/dist/contrib/mhchem'
import 'remark-github-blockquote-alert/alert.css'
import { usePreference } from '@data/hooks/usePreference'
import ImageViewer from '@renderer/components/ImageViewer'
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
import { useSettings } from '@renderer/hooks/useSettings'
import { useSmoothStream } from '@renderer/hooks/useSmoothStream'
import type { MainTextMessageBlock, ThinkingMessageBlock, TranslationMessageBlock } from '@renderer/types/newMessage'
import { removeSvgEmptyLines } from '@renderer/utils/formats'
@ -46,7 +46,8 @@ interface Props {
const Markdown: FC<Props> = ({ block, postProcess }) => {
const { t } = useTranslation()
const { mathEngine, mathEnableSingleDollar } = useSettings()
const [mathEngine] = usePreference('chat.message.math.engine')
const [mathEnableSingleDollar] = usePreference('chat.message.math.single_dollar')
const isTrulyDone = 'status' in block && block.status === 'success'
const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content)

View File

@ -1,4 +1,4 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { usePreference } from '@data/hooks/usePreference'
import { getModelUniqId } from '@renderer/services/ModelService'
import type { RootState } from '@renderer/store'
import { selectFormattedCitationsByBlockId } from '@renderer/store/messageBlock'
@ -21,7 +21,7 @@ interface Props {
const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions = [] }) => {
// Use the passed citationBlockId directly in the selector
const { renderInputMessageAsMarkdown } = useSettings()
const [renderInputMessageAsMarkdown] = usePreference('chat.message.render_as_markdown')
const rawCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))

View File

@ -1,7 +1,7 @@
import { CheckOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import ThinkingEffect from '@renderer/components/ThinkingEffect'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
import { Collapse, message as antdMessage, Tooltip } from 'antd'
@ -19,7 +19,9 @@ interface Props {
const ThinkingBlock: React.FC<Props> = ({ block }) => {
const [copied, setCopied] = useTemporaryValue(false, 2000)
const { t } = useTranslation()
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings()
const [messageFont] = usePreference('chat.message.font')
const [fontSize] = usePreference('chat.message.font_size')
const [thoughtAutoCollapse] = usePreference('chat.message.thought.auto_collapse')
const [activeKey, setActiveKey] = useState<'thought' | ''>(thoughtAutoCollapse ? '' : 'thought')
const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status])

View File

@ -1,12 +1,12 @@
import '@xyflow/react/dist/style.css'
import { RobotOutlined, UserOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { RootState } from '@renderer/store'
@ -205,7 +205,7 @@ const ChatFlowHistory: FC<ChatFlowHistoryProps> = ({ conversationId }) => {
const [nodes, setNodes, onNodesChange] = useNodesState<any>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<any>([])
const [loading, setLoading] = useState(true)
const { userName } = useSettings()
const [userName] = usePreference('app.user.name')
const { settedTheme } = useTheme()
const topicId = conversationId

View File

@ -6,7 +6,7 @@ import {
VerticalAlignBottomOutlined,
VerticalAlignTopOutlined
} from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { usePreference } from '@data/hooks/usePreference'
import { RootState } from '@renderer/store'
// import { selectCurrentTopicId } from '@renderer/store/newMessage'
import { Button, Drawer, Tooltip } from 'antd'
@ -44,7 +44,8 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
const lastMoveTime = useRef(0)
const { topicPosition, showTopics } = useSettings()
const [topicPosition] = usePreference('topic.position')
const [showTopics] = usePreference('topic.tab.show')
const showRightTopics = topicPosition === 'right' && showTopics
// Reset hide timer and make buttons visible

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
@ -5,7 +6,6 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useModel } from '@renderer/hooks/useModel'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageModelId } from '@renderer/services/MessagesService'
@ -67,7 +67,12 @@ const MessageItem: FC<Props> = ({
const { assistant, setModel } = useAssistant(message.assistantId)
const { isMultiSelectMode } = useChatContext(topic)
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
const { messageFont, fontSize, messageStyle, showMessageOutline } = useSettings()
const [messageFont] = usePreference('chat.message.font')
const [fontSize] = usePreference('chat.message.font_size')
const [messageStyle] = usePreference('chat.message.style')
const [showMessageOutline] = usePreference('chat.message.show_outline')
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
const messageContainerRef = useRef<HTMLDivElement>(null)
const { editingMessageId, stopEditing } = useMessageEditing()

View File

@ -1,9 +1,9 @@
import { usePreference } from '@data/hooks/usePreference'
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelName } from '@renderer/services/ModelService'
@ -33,7 +33,7 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { userName } = useSettings()
const [userName] = usePreference('app.user.name')
const { setTimeoutTimer } = useTimer()
const messagesListRef = useRef<HTMLDivElement>(null)

View File

@ -1,9 +1,9 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import CustomTag from '@renderer/components/Tags/CustomTag'
import TranslateButton from '@renderer/components/TranslateButton'
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import FileManager from '@renderer/services/FileManager'
import PasteService from '@renderer/services/PasteService'
@ -45,7 +45,10 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
const [isFileDragging, setIsFileDragging] = useState(false)
const { assistant } = useAssistant(message.assistantId)
const model = assistant.model || assistant.defaultModel
const { pasteLongTextThreshold, fontSize, sendMessageShortcut, enableSpellCheck } = useSettings()
const [pasteLongTextThreshold] = usePreference('chat.input.paste_long_text_threshold')
const [fontSize] = usePreference('chat.message.font_size')
const [sendMessageShortcut] = usePreference('chat.input.send_message_shortcut')
const [enableSpellCheck] = usePreference('app.spell_check.enabled')
const { t } = useTranslation()
const textareaRef = useRef<TextAreaRef>(null)
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)

View File

@ -1,15 +1,15 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import Scrollbar from '@renderer/components/Scrollbar'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils'
import type { MultiModelMessageStyle } from '@shared/data/preferenceTypes'
import { Popover } from 'antd'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
@ -30,7 +30,9 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
// Hooks
const { editMessage } = useMessageOperations(topic)
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
const [multiModelMessageStyleSetting] = usePreference('chat.message.multi_model.style')
const [gridColumns] = usePreference('chat.message.multi_model.grid_columns')
const [gridPopoverTrigger] = usePreference('chat.message.multi_model.grid_popover_trigger')
const { isMultiSelectMode } = useChatContext(topic)
const maxWidth = useChatMaxWidth()
const { setTimeoutTimer } = useTimer()

View File

@ -9,11 +9,11 @@ import {
import { HStack } from '@renderer/components/Layout'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { MultiModelMessageStyle } from '@shared/data/preferenceTypes'
import { Button, Tooltip } from 'antd'
import { FC, memo } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -1,5 +1,6 @@
// import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { useMultiplePreferences } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -16,7 +17,7 @@ import useTranslate from '@renderer/hooks/useTranslate'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService'
import store, { RootState, useAppDispatch } from '@renderer/store'
import store, { useAppDispatch } from '@renderer/store'
import { messageBlocksSelectors, removeOneBlock } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { TraceIcon } from '@renderer/trace/pages/Component'
@ -107,7 +108,19 @@ const MessageMenubar: FC<Props> = (props) => {
const isUserMessage = message.role === 'user'
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
const [exportMenuOptions] = useMultiplePreferences({
image: 'data.export.menus.image',
markdown: 'data.export.menus.markdown',
markdown_reason: 'data.export.menus.markdown_reason',
notion: 'data.export.menus.notion',
yuque: 'data.export.menus.yuque',
joplin: 'data.export.menus.joplin',
obsidian: 'data.export.menus.obsidian',
siyuan: 'data.export.menus.siyuan',
docx: 'data.export.menus.docx',
plain_text: 'data.export.menus.plain_text'
})
const dispatch = useAppDispatch()
// const processedMessage = useMemo(() => {
@ -263,7 +276,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'clipboard',
onClick: async () => {
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
const markdown = await messageToMarkdown(message)
exportMessageToNotes(title, markdown, notesPath)
}
}
@ -315,7 +328,7 @@ const MessageMenubar: FC<Props> = (props) => {
label: t('chat.topics.export.word'),
key: 'word',
onClick: async () => {
const markdown = messageToMarkdown(message)
const markdown = await messageToMarkdown(message)
const title = await getMessageTitle(message)
window.api.export.toWord(markdown, title)
}
@ -325,7 +338,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'notion',
onClick: async () => {
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
const markdown = await messageToMarkdown(message)
exportMessageToNotion(title, markdown, message)
}
},
@ -334,7 +347,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'yuque',
onClick: async () => {
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
const markdown = await messageToMarkdown(message)
exportMarkdownToYuque(title, markdown)
}
},
@ -359,7 +372,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'siyuan',
onClick: async () => {
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
const markdown = await messageToMarkdown(message)
exportMarkdownToSiyuan(title, markdown)
}
}

View File

@ -1,8 +1,8 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { CopyIcon, LoadingIcon } from '@renderer/components/Icons'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import type { ToolMessageBlock } from '@renderer/types/newMessage'
import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
@ -49,7 +49,8 @@ const MessageTools: FC<Props> = ({ block }) => {
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
const [countdown, setCountdown] = useState<number>(COUNTDOWN_TIME)
const { t } = useTranslation()
const { messageFont, fontSize } = useSettings()
const [messageFont] = usePreference('chat.message.font')
const [fontSize] = usePreference('chat.message.font_size')
const { mcpServers, updateMCPServer } = useMCPServers()
const [expandedResponse, setExpandedResponse] = useState<{ content: string; title: string } | null>(null)
const [progress, setProgress] = useState<number>(0)

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import ContextMenu from '@renderer/components/ContextMenu'
import { LoadingIcon } from '@renderer/components/Icons'
@ -7,7 +8,6 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useTimer } from '@renderer/hooks/useTimer'
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
@ -62,7 +62,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const [isProcessingContext, setIsProcessingContext] = useState(false)
const { updateTopic, addTopic } = useAssistant(assistant.id)
const { showPrompt, messageNavigation } = useSettings()
const [showPrompt] = usePreference('chat.message.show_prompt')
const [messageNavigation] = usePreference('chat.message.navigation_mode')
const { t } = useTranslation()
const dispatch = useAppDispatch()
const messages = useTopicMessages(topic.id)

View File

@ -1,4 +1,4 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { usePreference } from '@data/hooks/usePreference'
import { FC, HTMLAttributes } from 'react'
import styled from 'styled-components'
@ -7,7 +7,7 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
}
const NarrowLayout: FC<Props> = ({ children, ...props }) => {
const { narrowMode } = useSettings()
const [narrowMode] = usePreference('chat.narrow_mode')
return (
<Container className={`narrow-mode ${narrowMode ? 'active' : ''}`} {...props}>

View File

@ -1,4 +1,5 @@
import { usePreference } from '@data/hooks/usePreference'
import { useMultiplePreferences } from '@data/hooks/usePreference'
import { DraggableVirtualList } from '@renderer/components/DraggableList'
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -191,7 +192,18 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
[setActiveTopic]
)
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
const [exportMenuOptions] = useMultiplePreferences({
image: 'data.export.menus.image',
markdown: 'data.export.menus.markdown',
markdown_reason: 'data.export.menus.markdown_reason',
notion: 'data.export.menus.notion',
yuque: 'data.export.menus.yuque',
joplin: 'data.export.menus.joplin',
obsidian: 'data.export.menus.obsidian',
siyuan: 'data.export.menus.siyuan',
docx: 'data.export.menus.docx',
plain_text: 'data.export.menus.plain_text'
})
const [_targetTopic, setTargetTopic] = useState<Topic | null>(null)
const targetTopic = useDeferredValue(_targetTopic)

View File

@ -1,7 +1,7 @@
import { usePreference } from '@data/hooks/usePreference'
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
@ -39,7 +39,7 @@ const HomeTabs: FC<Props> = ({
}) => {
const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
const [topicPosition] = usePreference('topic.position')
const { defaultAssistant } = useDefaultAssistant()
const { toggleShowTopics } = useShowTopics()
const { isLeftNavbar } = useNavbarPosition()

View File

@ -1,6 +1,6 @@
import { SyncOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Button } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -8,7 +8,7 @@ import styled from 'styled-components'
const UpdateAppButton: FC = () => {
const { update } = useRuntime()
const { autoCheckUpdate } = useSettings()
const [autoCheckUpdate] = usePreference('app.dist.auto_update.enabled')
const { t } = useTranslation()
if (!update) {

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { KnowledgeBase, PreprocessProviderId } from '@renderer/types'
import { Tag } from 'antd'
@ -28,7 +28,7 @@ const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quot
return
}
if (quota === undefined) {
const userId = getStoreSetting('userId')
const userId = await preferenceService.get('app.user.id')
const baseParams = getKnowledgeBaseParams(base)
try {
const response = await window.api.knowledgeBase.checkQuota({

View File

@ -1,4 +1,5 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
@ -13,7 +14,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
@ -94,7 +94,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const { autoTranslateWithSpace } = useSettings()
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const aihubmixProvider = providers.find((p) => p.id === 'aihubmix')!

View File

@ -1,4 +1,5 @@
import { PlusOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
@ -12,7 +13,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import {
getPaintingsBackgroundOptionsLabel,
getPaintingsImageSizeOptionsLabel,
@ -85,7 +85,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const { autoTranslateWithSpace } = useSettings()
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const newApiProvider = providers.find((p) => p.id === 'new-api')!

View File

@ -1,4 +1,5 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore'
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
@ -17,7 +18,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label'
import { getProviderByModel } from '@renderer/services/AssistantService'
import FileManager from '@renderer/services/FileManager'
@ -303,7 +303,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
setCurrentImageIndex(0)
}
const { autoTranslateWithSpace } = useSettings()
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const [spaceClickCount, setSpaceClickCount] = useState(0)
const [isTranslating, setIsTranslating] = useState(false)
const spaceClickTimer = useRef<NodeJS.Timeout>(null)

View File

@ -1,4 +1,5 @@
import { PlusOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar'
@ -9,7 +10,6 @@ import { LanguagesEnum } from '@renderer/config/translate'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
@ -74,7 +74,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const { autoTranslateWithSpace } = useSettings()
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const tokenfluxProvider = providers.find((p) => p.id === 'tokenflux')!
const textareaRef = useRef<any>(null)

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { nanoid } from '@reduxjs/toolkit'
import store from '@renderer/store'
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchProviderResponse, WebSearchProviderResult } from '@renderer/types'
import { createAbortPromise } from '@renderer/utils/abortController'
@ -30,7 +30,7 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
httpOptions?: RequestInit
): Promise<WebSearchProviderResponse> {
const uid = nanoid()
const language = store.getState().settings.language
const language = await preferenceService.get('app.language')
try {
if (!query.trim()) {
throw new Error('Search query cannot be empty')

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import db from '@renderer/databases'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { NotificationService } from '@renderer/services/NotificationService'
import store from '@renderer/store'
@ -96,7 +96,7 @@ class KnowledgeQueue {
private async processItem(baseId: string, item: KnowledgeItem): Promise<void> {
const notificationService = NotificationService.getInstance()
const userId = getStoreSetting('userId')
const userId = await preferenceService.get('app.user.id')
try {
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
logger.info(`Item ${item.id} has reached max retries, skipping`)

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { CompletionsParams } from '@renderer/aiCore/middleware/schemas'
import { SYSTEM_PROMPT_THRESHOLD } from '@renderer/config/constant'
@ -12,7 +13,6 @@ import {
isWebSearchModel
} from '@renderer/config/models'
import { getModel } from '@renderer/hooks/useModel'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { currentSpan, withSpanResult } from '@renderer/services/SpanManagerService'
import store from '@renderer/store'
@ -680,7 +680,7 @@ export async function fetchLanguageDetection({ text, onResponse }: FetchLanguage
}
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
let prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
let prompt = (await preferenceService.get('topic.naming_prompt')) || i18n.t('prompts.title')
const model = getQuickModel() || assistant.model || getDefaultModel()
if (prompt && containsSupportedVariables(prompt)) {

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import {
DEFAULT_CONTEXTCOUNT,
@ -52,7 +53,10 @@ export function getDefaultAssistant(): Assistant {
}
}
export function getDefaultTranslateAssistant(targetLanguage: TranslateLanguage, text: string): TranslateAssistant {
export async function getDefaultTranslateAssistant(
targetLanguage: TranslateLanguage,
text: string
): Promise<TranslateAssistant> {
const model = getTranslateModel()
const assistant: Assistant = getDefaultAssistant()
@ -77,10 +81,8 @@ export function getDefaultTranslateAssistant(targetLanguage: TranslateLanguage,
prompt = ''
} else {
content = 'follow system instruction'
prompt = store
.getState()
.settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value)
.replaceAll('{{text}}', text)
const translateModelPrompt = await preferenceService.get('feature.translate.model_prompt')
prompt = translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value).replaceAll('{{text}}', text)
}
const translateAssistant = {

View File

@ -1,3 +1,7 @@
//TODO Data Refactor
// The code is messy, need to refactor all the backup related code
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import db from '@renderer/databases'
import { upgradeToV7, upgradeToV8 } from '@renderer/databases/upgrades'
@ -164,7 +168,16 @@ export async function backupToWebdav({
webdavMaxBackups,
webdavSkipBackupFile,
webdavDisableStream
} = store.getState().settings
} = await preferenceService.getMultiple({
webdavHost: 'data.backup.webdav.host',
webdavUser: 'data.backup.webdav.user',
webdavPass: 'data.backup.webdav.pass',
webdavPath: 'data.backup.webdav.path',
webdavMaxBackups: 'data.backup.webdav.max_backups',
webdavSkipBackupFile: 'data.backup.webdav.skip_backup_file',
webdavDisableStream: 'data.backup.webdav.disable_stream'
})
let deviceType = 'unknown'
let hostname = 'unknown'
try {
@ -294,7 +307,12 @@ export async function backupToWebdav({
// 从 webdav 恢复
export async function restoreFromWebdav(fileName?: string) {
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
const { webdavHost, webdavUser, webdavPass, webdavPath } = await preferenceService.getMultiple({
webdavHost: 'data.backup.webdav.host',
webdavUser: 'data.backup.webdav.user',
webdavPass: 'data.backup.webdav.pass',
webdavPath: 'data.backup.webdav.path'
})
let data = ''
try {
@ -334,7 +352,18 @@ export async function backupToS3({
store.dispatch(setS3SyncState({ syncing: true, lastSyncError: null }))
const s3Config = store.getState().settings.s3
const s3Config = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
accessKeyId: 'data.backup.s3.access_key_id',
secretAccessKey: 'data.backup.s3.secret_access_key',
endpoint: 'data.backup.s3.endpoint',
bucket: 'data.backup.s3.bucket',
region: 'data.backup.s3.region',
root: 'data.backup.s3.root',
maxBackups: 'data.backup.s3.max_backups',
skipBackupFile: 'data.backup.s3.skip_backup_file',
syncInterval: 'data.backup.s3.sync_interval'
})
let deviceType = 'unknown'
let hostname = 'unknown'
try {
@ -445,7 +474,18 @@ export async function backupToS3({
// 从 S3 恢复
export async function restoreFromS3(fileName?: string) {
const s3Config = store.getState().settings.s3
const s3Config = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
accessKeyId: 'data.backup.s3.access_key_id',
secretAccessKey: 'data.backup.s3.secret_access_key',
endpoint: 'data.backup.s3.endpoint',
bucket: 'data.backup.s3.bucket',
region: 'data.backup.s3.region',
root: 'data.backup.s3.root',
maxBackups: 'data.backup.s3.max_backups',
skipBackupFile: 'data.backup.s3.skip_backup_file',
syncInterval: 'data.backup.s3.sync_interval'
})
if (!fileName) {
const files = await window.api.backup.listS3Files(s3Config)
@ -481,12 +521,22 @@ let isLocalAutoBackupRunning = false
type BackupType = 'webdav' | 's3' | 'local'
export function startAutoSync(immediate = false, type?: BackupType) {
export async function startAutoSync(immediate = false, type?: BackupType) {
// 如果没有指定类型,启动所有配置的自动同步
if (!type) {
const settings = store.getState().settings
const { webdavAutoSync, webdavHost, localBackupAutoSync, localBackupDir } = settings
const s3Settings = settings.s3
const { webdavAutoSync, webdavHost, localBackupAutoSync, localBackupDir } = await preferenceService.getMultiple({
webdavAutoSync: 'data.backup.webdav.auto_sync',
webdavHost: 'data.backup.webdav.host',
localBackupAutoSync: 'data.backup.local.auto_sync',
localBackupDir: 'data.backup.local.dir'
})
const s3Settings = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
endpoint: 'data.backup.s3.endpoint',
bucket: 'data.backup.s3.bucket',
region: 'data.backup.s3.region',
root: 'data.backup.s3.root'
})
if (webdavAutoSync && webdavHost) {
startAutoSync(immediate, 'webdav')
@ -506,8 +556,10 @@ export function startAutoSync(immediate = false, type?: BackupType) {
return
}
const settings = store.getState().settings
const { webdavAutoSync, webdavHost } = settings
const { webdavAutoSync, webdavHost } = await preferenceService.getMultiple({
webdavAutoSync: 'data.backup.webdav.auto_sync',
webdavHost: 'data.backup.webdav.host'
})
if (!webdavAutoSync || !webdavHost) {
logger.info('[WebdavAutoSync] Invalid sync settings, auto sync disabled')
@ -522,8 +574,10 @@ export function startAutoSync(immediate = false, type?: BackupType) {
return
}
const settings = store.getState().settings
const s3Settings = settings.s3
const s3Settings = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
endpoint: 'data.backup.s3.endpoint'
})
if (!s3Settings?.autoSync || !s3Settings?.endpoint) {
logger.verbose('Invalid sync settings, auto sync disabled')
@ -538,8 +592,10 @@ export function startAutoSync(immediate = false, type?: BackupType) {
return
}
const settings = store.getState().settings
const { localBackupAutoSync, localBackupDir } = settings
const { localBackupAutoSync, localBackupDir } = await preferenceService.getMultiple({
localBackupAutoSync: 'data.backup.local.auto_sync',
localBackupDir: 'data.backup.local.dir'
})
if (!localBackupAutoSync || !localBackupDir) {
logger.verbose('Invalid sync settings, auto sync disabled')
@ -551,13 +607,15 @@ export function startAutoSync(immediate = false, type?: BackupType) {
scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime', 'local')
}
function scheduleNextBackup(scheduleType: 'immediate' | 'fromLastSyncTime' | 'fromNow', backupType: BackupType) {
async function scheduleNextBackup(
scheduleType: 'immediate' | 'fromLastSyncTime' | 'fromNow',
backupType: BackupType
) {
let syncInterval: number
let lastSyncTime: number | undefined
let logPrefix: string
// 根据备份类型获取相应的配置和状态
const settings = store.getState().settings
const backup = store.getState().backup
if (backupType === 'webdav') {
@ -565,7 +623,7 @@ export function startAutoSync(immediate = false, type?: BackupType) {
clearTimeout(webdavSyncTimeout)
webdavSyncTimeout = null
}
syncInterval = settings.webdavSyncInterval
syncInterval = await preferenceService.get('data.backup.webdav.sync_interval')
lastSyncTime = backup.webdavSync?.lastSyncTime || undefined
logPrefix = '[WebdavAutoSync]'
} else if (backupType === 's3') {
@ -573,7 +631,7 @@ export function startAutoSync(immediate = false, type?: BackupType) {
clearTimeout(s3SyncTimeout)
s3SyncTimeout = null
}
syncInterval = settings.s3?.syncInterval || 0
syncInterval = await preferenceService.get('data.backup.s3.sync_interval')
lastSyncTime = backup.s3Sync?.lastSyncTime || undefined
logPrefix = '[S3AutoSync]'
} else if (backupType === 'local') {
@ -581,7 +639,7 @@ export function startAutoSync(immediate = false, type?: BackupType) {
clearTimeout(localSyncTimeout)
localSyncTimeout = null
}
syncInterval = settings.localBackupSyncInterval
syncInterval = await preferenceService.get('data.backup.local.sync_interval')
lastSyncTime = backup.localBackupSync?.lastSyncTime || undefined
logPrefix = '[LocalAutoSync]'
} else {
@ -921,11 +979,12 @@ export async function backupToLocal({
store.dispatch(setLocalBackupSyncState({ syncing: true, lastSyncError: null }))
const {
localBackupDir: localBackupDirSetting,
localBackupMaxBackups,
localBackupSkipBackupFile
} = store.getState().settings
const { localBackupDirSetting, localBackupMaxBackups, localBackupSkipBackupFile } =
await preferenceService.getMultiple({
localBackupDirSetting: 'data.backup.local.dir',
localBackupMaxBackups: 'data.backup.local.max_backups',
localBackupSkipBackupFile: 'data.backup.local.skip_backup_file'
})
const localBackupDir = await window.api.resolvePath(localBackupDirSetting)
let deviceType = 'unknown'
let hostname = 'unknown'
@ -1049,7 +1108,7 @@ export async function backupToLocal({
export async function restoreFromLocal(fileName: string) {
try {
const { localBackupDir: localBackupDirSetting } = store.getState().settings
const localBackupDirSetting = await preferenceService.get('data.backup.local.dir')
const localBackupDir = await window.api.resolvePath(localBackupDirSetting)
const restoreData = await window.api.backup.restoreFromLocalBackup(fileName, localBackupDir)
const data = JSON.parse(restoreData)

View File

@ -1,5 +1,4 @@
import store from '@renderer/store'
import { initialState as defaultNotificationSettings } from '@renderer/store/settings'
import { preferenceService } from '@data/PreferenceService'
import type { Notification } from '@renderer/types/notification'
import { NotificationQueue } from '../queue/NotificationQueue'
@ -25,7 +24,11 @@ export class NotificationService {
* @param notification
*/
public async send(notification: Notification): Promise<void> {
const notificationSettings = store.getState().settings.notification || defaultNotificationSettings
const notificationSettings = await preferenceService.getMultiple({
assistant: 'app.notification.assistant.enabled',
backup: 'app.notification.backup.enabled',
knowledge: 'app.notification.knowledge.enabled'
})
if (notificationSettings[notification.source]) {
this.queue.add(notification)

View File

@ -87,7 +87,7 @@ export const translateText = async (
abortKey?: string
) => {
try {
const assistant = getDefaultTranslateAssistant(targetLanguage, text)
const assistant = await getDefaultTranslateAssistant(targetLanguage, text)
const translatedText = await fetchTranslate({ assistant, onResponse, abortKey })

View File

@ -16,7 +16,7 @@ import llm from './llm'
import mcp from './mcp'
import memory from './memory'
import messageBlocksReducer from './messageBlock'
import migrate from './migrate'
// import migrate from './migrate'
import minapps from './minapps'
import newMessagesReducer from './newMessage'
import { setNotesPath } from './note'
@ -68,8 +68,8 @@ const persistedReducer = persistReducer(
key: 'cherry-studio',
storage,
version: 144,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs']
// migrate
},
rootReducer
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -257,21 +257,21 @@ describe('export', () => {
mockedMessages = [userMsg, assistantMsg]
})
it('should handle empty content in message blocks', () => {
it('should handle empty content in message blocks', async () => {
const msgWithEmptyContent = createMessage({ role: 'user', id: 'empty_block' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '' }
])
const markdown = messageToMarkdown(msgWithEmptyContent)
const markdown = await messageToMarkdown(msgWithEmptyContent)
expect(markdown).toContain('## 🧑‍💻 User')
// Should handle empty content gracefully
expect(markdown).toBeDefined()
expect(markdown.split('\n\n').filter((s) => s.trim()).length).toBeGreaterThanOrEqual(1)
})
it('should format user message using main text block', () => {
it('should format user message using main text block', async () => {
const msg = mockedMessages.find((m) => m.id === 'u1')
expect(msg).toBeDefined()
const markdown = messageToMarkdown(msg!)
const markdown = await messageToMarkdown(msg!)
expect(markdown).toContain('## 🧑‍💻 User')
expect(markdown).toContain('hello user')
@ -281,10 +281,10 @@ describe('export', () => {
expect(sections.length).toBeGreaterThanOrEqual(2) // title section and content section
})
it('should format assistant message using main text block', () => {
it('should format assistant message using main text block', async () => {
const msg = mockedMessages.find((m) => m.id === 'a1')
expect(msg).toBeDefined()
const markdown = messageToMarkdown(msg!)
const markdown = await messageToMarkdown(msg!)
expect(markdown).toContain('## 🤖 Assistant')
expect(markdown).toContain('hi assistant')
@ -294,21 +294,21 @@ describe('export', () => {
expect(sections.length).toBeGreaterThanOrEqual(2) // title section and content section
})
it('should handle message with no main text block gracefully', () => {
it('should handle message with no main text block gracefully', async () => {
const msg = createMessage({ role: 'user', id: 'u2' }, [])
mockedMessages.push(msg)
const markdown = messageToMarkdown(msg)
const markdown = await messageToMarkdown(msg)
expect(markdown).toContain('## 🧑‍💻 User')
// Check that it doesn't fail when no content exists
expect(markdown).toBeDefined()
})
it('should include citation content when citation blocks exist', () => {
it('should include citation content when citation blocks exist', async () => {
const msgWithCitation = createMessage({ role: 'assistant', id: 'a_cite' }, [
{ type: MessageBlockType.MAIN_TEXT, content: 'Main content' },
{ type: MessageBlockType.CITATION }
])
const markdown = messageToMarkdown(msgWithCitation)
const markdown = await messageToMarkdown(msgWithCitation)
expect(markdown).toContain('## 🤖 Assistant')
expect(markdown).toContain('Main content')
expect(markdown).toContain('[1] [https://example1.com](Example Citation 1)')
@ -337,10 +337,10 @@ describe('export', () => {
mockedMessages = [msgWithReasoning, msgWithThinkTag, msgWithoutReasoning, msgWithReasoningAndCitation]
})
it('should include reasoning content from thinking block in details section', () => {
it('should include reasoning content from thinking block in details section', async () => {
const msg = mockedMessages.find((m) => m.id === 'a2')
expect(msg).toBeDefined()
const markdown = messageToMarkdownWithReasoning(msg!)
const markdown = await messageToMarkdownWithReasoning(msg!)
expect(markdown).toContain('## 🤖 Assistant')
expect(markdown).toContain('Main Answer')
expect(markdown).toContain('<details')
@ -404,9 +404,9 @@ describe('export', () => {
mockedMessages = [userMsg, assistantMsg, singleUserMsg]
})
it('should join multiple messages with markdown separator', () => {
it('should join multiple messages with markdown separator', async () => {
const msgs = mockedMessages.filter((m) => ['u3', 'a5'].includes(m.id))
const markdown = messagesToMarkdown(msgs)
const markdown = await messagesToMarkdown(msgs)
expect(markdown).toContain('User query A')
expect(markdown).toContain('Assistant response B')
@ -414,13 +414,13 @@ describe('export', () => {
expect(markdown.split('\n---\n').length).toBe(2)
})
it('should handle an empty array of messages', () => {
it('should handle an empty array of messages', async () => {
expect(messagesToMarkdown([])).toBe('')
})
it('should handle a single message without separator', () => {
it('should handle a single message without separator', async () => {
const msgs = mockedMessages.filter((m) => m.id === 'u4')
const markdown = messagesToMarkdown(msgs)
const markdown = await messagesToMarkdown(msgs)
expect(markdown).toContain('Single user query')
expect(markdown.split('\n\n---\n\n').length).toBe(1)
})
@ -458,7 +458,7 @@ describe('export', () => {
const { TopicManager } = await import('@renderer/hooks/useTopic')
;(TopicManager.getTopicMessages as any).mockResolvedValue([userMsg, assistantMsg])
// Specific mock for this test to check formatting
;(markdownToPlainText as any).mockImplementation((str: string) => str.replace(/[#*]/g, ''))
;(markdownToPlainText as any).mockImplementation(async (str: string) => str.replace(/[#*]/g, ''))
const plainText = await topicToPlainText(testTopic)
@ -471,13 +471,13 @@ describe('export', () => {
})
describe('messageToPlainText', () => {
it('should convert a single message content to plain text without role prefix', () => {
it('should convert a single message content to plain text without role prefix', async () => {
const testMessage = createMessage({ role: 'user', id: 'single_msg_plain' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '### Single Message Content' }
])
;(markdownToPlainText as any).mockImplementation((str: string) => str.replace(/[#*_]/g, ''))
;(markdownToPlainText as any).mockImplementation(async (str: string) => str.replace(/[#*_]/g, ''))
const result = messageToPlainText(testMessage)
const result = await messageToPlainText(testMessage)
expect(result).toBe('Single Message Content')
expect(markdownToPlainText).toHaveBeenCalledWith('### Single Message Content')
})

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { Client } from '@notionhq/client'
import i18n from '@renderer/i18n'
@ -159,9 +160,11 @@ export function getTitleFromString(str: string, length: number = 80): string {
return title
}
const getRoleText = (role: string, modelName?: string, providerId?: string): string => {
const { showModelNameInMarkdown, showModelProviderInMarkdown } = store.getState().settings
const getRoleText = async (role: string, modelName?: string, providerId?: string): Promise<string> => {
const { showModelNameInMarkdown, showModelProviderInMarkdown } = await preferenceService.getMultiple({
showModelNameInMarkdown: 'data.export.markdown.show_model_name',
showModelProviderInMarkdown: 'data.export.markdown.show_model_provider'
})
if (role === 'user') {
return '🧑‍💻 User'
} else if (role === 'system') {
@ -263,13 +266,13 @@ const formatCitationsAsFootnotes = (citations: string): string => {
return footnotes.join('\n\n')
}
const createBaseMarkdown = (
const createBaseMarkdown = async (
message: Message,
includeReasoning: boolean = false,
excludeCitations: boolean = false,
normalizeCitations: boolean = true
): { titleSection: string; reasoningSection: string; contentSection: string; citation: string } => {
const { forceDollarMathInMarkdown } = store.getState().settings
): Promise<{ titleSection: string; reasoningSection: string; contentSection: string; citation: string }> => {
const forceDollarMathInMarkdown = await preferenceService.get('data.export.markdown.force_dollar_math')
const roleText = getRoleText(message.role, message.model?.name, message.model?.provider)
const titleSection = `## ${roleText}`
let reasoningSection = ''
@ -313,10 +316,13 @@ const createBaseMarkdown = (
return { titleSection, reasoningSection, contentSection: processedContent, citation }
}
export const messageToMarkdown = (message: Message, excludeCitations?: boolean): string => {
const { excludeCitationsInExport, standardizeCitationsInExport } = store.getState().settings
export const messageToMarkdown = async (message: Message, excludeCitations?: boolean): Promise<string> => {
const { excludeCitationsInExport, standardizeCitationsInExport } = await preferenceService.getMultiple({
excludeCitationsInExport: 'data.export.markdown.exclude_citations',
standardizeCitationsInExport: 'data.export.markdown.standardize_citations'
})
const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport
const { titleSection, contentSection, citation } = createBaseMarkdown(
const { titleSection, contentSection, citation } = await createBaseMarkdown(
message,
false,
shouldExcludeCitations,
@ -325,10 +331,13 @@ export const messageToMarkdown = (message: Message, excludeCitations?: boolean):
return [titleSection, '', contentSection, citation].join('\n')
}
export const messageToMarkdownWithReasoning = (message: Message, excludeCitations?: boolean): string => {
const { excludeCitationsInExport, standardizeCitationsInExport } = store.getState().settings
export const messageToMarkdownWithReasoning = async (message: Message, excludeCitations?: boolean): Promise<string> => {
const { excludeCitationsInExport, standardizeCitationsInExport } = await preferenceService.getMultiple({
excludeCitationsInExport: 'data.export.markdown.exclude_citations',
standardizeCitationsInExport: 'data.export.markdown.standardize_citations'
})
const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport
const { titleSection, reasoningSection, contentSection, citation } = createBaseMarkdown(
const { titleSection, reasoningSection, contentSection, citation } = await createBaseMarkdown(
message,
true,
shouldExcludeCitations,
@ -337,18 +346,14 @@ export const messageToMarkdownWithReasoning = (message: Message, excludeCitation
return [titleSection, '', reasoningSection, contentSection, citation].join('\n')
}
export const messagesToMarkdown = (
export const messagesToMarkdown = async (
messages: Message[],
exportReasoning?: boolean,
excludeCitations?: boolean
): string => {
return messages
.map((message) =>
exportReasoning
? messageToMarkdownWithReasoning(message, excludeCitations)
: messageToMarkdown(message, excludeCitations)
)
.join('\n---\n')
): Promise<string> => {
const converter = exportReasoning ? messageToMarkdownWithReasoning : messageToMarkdown
const markdowns = await Promise.all(messages.map((message) => converter(message, excludeCitations)))
return markdowns.join('\n---\n')
}
const formatMessageAsPlainText = (message: Message): string => {
@ -377,7 +382,7 @@ export const topicToMarkdown = async (
const messages = await fetchTopicMessages(topic.id)
if (messages && messages.length > 0) {
return topicName + '\n\n' + messagesToMarkdown(messages, exportReasoning, excludeCitations)
return topicName + '\n\n' + (await messagesToMarkdown(messages, exportReasoning, excludeCitations))
}
return topicName
@ -407,7 +412,7 @@ export const exportTopicAsMarkdown = async (
setExportingState(true)
const { markdownExportPath } = store.getState().settings
const markdownExportPath = await preferenceService.get('data.export.markdown.path')
if (!markdownExportPath) {
try {
const fileName = removeSpecialCharactersForFileName(topic.name) + '.md'
@ -453,14 +458,14 @@ export const exportMessageAsMarkdown = async (
setExportingState(true)
const { markdownExportPath } = store.getState().settings
const markdownExportPath = await preferenceService.get('data.export.markdown.path')
if (!markdownExportPath) {
try {
const title = await getMessageTitle(message)
const fileName = removeSpecialCharactersForFileName(title) + '.md'
const markdown = exportReasoning
? messageToMarkdownWithReasoning(message, excludeCitations)
: messageToMarkdown(message, excludeCitations)
? await messageToMarkdownWithReasoning(message, excludeCitations)
: await messageToMarkdown(message, excludeCitations)
const result = await window.api.file.save(fileName, markdown)
if (result) {
window.message.success({
@ -480,8 +485,8 @@ export const exportMessageAsMarkdown = async (
const title = await getMessageTitle(message)
const fileName = removeSpecialCharactersForFileName(title) + ` ${timestamp}.md`
const markdown = exportReasoning
? messageToMarkdownWithReasoning(message, excludeCitations)
: messageToMarkdown(message, excludeCitations)
? await messageToMarkdownWithReasoning(message, excludeCitations)
: await messageToMarkdown(message, excludeCitations)
await window.api.file.write(markdownExportPath + '/' + fileName, markdown)
window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' })
} catch (error: any) {
@ -579,7 +584,11 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise<boo
return false
}
const { notionDatabaseID, notionApiKey } = store.getState().settings
const { notionDatabaseID, notionApiKey, notionPageNameKey } = await preferenceService.getMultiple({
notionDatabaseID: 'data.integration.notion.database_id',
notionPageNameKey: 'data.integration.notion.page_name_key',
notionApiKey: 'data.integration.notion.api_key'
})
if (!notionApiKey || !notionDatabaseID) {
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
return false
@ -609,7 +618,7 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise<boo
const response = await notion.pages.create({
parent: { database_id: notionDatabaseID },
properties: {
[store.getState().settings.notionPageNameKey || 'Name']: {
[notionPageNameKey || 'Name']: {
title: [{ text: { content: title } }]
}
}
@ -645,7 +654,7 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise<boo
}
export const exportMessageToNotion = async (title: string, content: string, message?: Message): Promise<boolean> => {
const { notionExportReasoning } = store.getState().settings
const notionExportReasoning = await preferenceService.get('data.integration.notion.export_reasoning')
const notionBlocks = await convertMarkdownToNotionBlocks(content)
@ -665,7 +674,10 @@ export const exportMessageToNotion = async (title: string, content: string, mess
}
export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => {
const { notionExportReasoning, excludeCitationsInExport } = store.getState().settings
const { notionExportReasoning, excludeCitationsInExport } = await preferenceService.getMultiple({
notionExportReasoning: 'data.integration.notion.export_reasoning',
excludeCitationsInExport: 'data.export.markdown.exclude_citations'
})
const topicMessages = await fetchTopicMessages(topic.id)
@ -677,7 +689,7 @@ export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => {
for (const message of topicMessages) {
// 将单个消息转换为markdown
const messageMarkdown = messageToMarkdown(message, excludeCitationsInExport)
const messageMarkdown = await messageToMarkdown(message, excludeCitationsInExport)
const messageBlocks = await convertMarkdownToNotionBlocks(messageMarkdown)
if (notionExportReasoning) {
@ -699,7 +711,10 @@ export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => {
}
export const exportMarkdownToYuque = async (title: string, content: string): Promise<any | null> => {
const { yuqueToken, yuqueRepoId } = store.getState().settings
const { yuqueToken, yuqueRepoId } = await preferenceService.getMultiple({
yuqueToken: 'data.integration.yuque.token',
yuqueRepoId: 'data.integration.yuque.repo_id'
})
if (getExportState()) {
window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'yuque-exporting' })
@ -897,7 +912,13 @@ export const exportMarkdownToJoplin = async (
title: string,
contentOrMessages: string | Message | Message[]
): Promise<any | null> => {
const { joplinUrl, joplinToken, joplinExportReasoning, excludeCitationsInExport } = store.getState().settings
const { joplinUrl, joplinToken, joplinExportReasoning, excludeCitationsInExport } =
await preferenceService.getMultiple({
joplinUrl: 'data.integration.joplin.url',
joplinToken: 'data.integration.joplin.token',
joplinExportReasoning: 'data.integration.joplin.export_reasoning',
excludeCitationsInExport: 'data.export.markdown.exclude_citations'
})
if (getExportState()) {
window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'joplin-exporting' })
@ -915,12 +936,12 @@ export const exportMarkdownToJoplin = async (
if (typeof contentOrMessages === 'string') {
content = contentOrMessages
} else if (Array.isArray(contentOrMessages)) {
content = messagesToMarkdown(contentOrMessages, joplinExportReasoning, excludeCitationsInExport)
content = await messagesToMarkdown(contentOrMessages, joplinExportReasoning, excludeCitationsInExport)
} else {
// 单条Message
content = joplinExportReasoning
? messageToMarkdownWithReasoning(contentOrMessages, excludeCitationsInExport)
: messageToMarkdown(contentOrMessages, excludeCitationsInExport)
? await messageToMarkdownWithReasoning(contentOrMessages, excludeCitationsInExport)
: await messageToMarkdown(contentOrMessages, excludeCitationsInExport)
}
try {
@ -963,7 +984,12 @@ export const exportMarkdownToJoplin = async (
* @param content
*/
export const exportMarkdownToSiyuan = async (title: string, content: string): Promise<void> => {
const { siyuanApiUrl, siyuanToken, siyuanBoxId, siyuanRootPath } = store.getState().settings
const { siyuanApiUrl, siyuanToken, siyuanBoxId, siyuanRootPath } = await preferenceService.getMultiple({
siyuanApiUrl: 'data.integration.siyuan.api_url',
siyuanToken: 'data.integration.siyuan.token',
siyuanBoxId: 'data.integration.siyuan.box_id',
siyuanRootPath: 'data.integration.siyuan.root_path'
})
if (getExportState()) {
window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'siyuan-exporting' })

View File

@ -1,6 +1,8 @@
import { loggerService } from '@logger'
import { preferenceService } from '@renderer/data/PreferenceService'
import store from '@renderer/store'
import { MCPTool } from '@renderer/types'
import { defaultLanguage } from '@shared/config/constant'
const logger = loggerService.withContext('Utils:Prompt')
@ -205,7 +207,7 @@ export const replacePromptVariables = async (userSystemPrompt: string, modelName
if (userSystemPrompt.includes('{{username}}')) {
try {
const userName = store.getState().settings.userName || 'Unknown Username'
const userName = (await preferenceService.get('app.user.name')) || 'Unknown Username'
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, userName)
} catch (error) {
logger.error('Failed to get username:', error as Error)
@ -225,8 +227,8 @@ export const replacePromptVariables = async (userSystemPrompt: string, modelName
if (userSystemPrompt.includes('{{language}}')) {
try {
const language = store.getState().settings.language
userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language)
const language = await preferenceService.get('app.language')
userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language || navigator.language || defaultLanguage)
} catch (error) {
logger.error('Failed to get language:', error as Error)
userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language')

View File

@ -70,7 +70,7 @@ const PreferenceHookTests: React.FC = () => {
const testBatchOperations = async () => {
try {
const keys: PreferenceKeyType[] = ['ui.theme_mode', 'app.language']
const result = await preferenceService.getMultiple(keys)
const result = await preferenceService.getMultipleRaw(keys)
message.success(`批量获取成功: ${Object.keys(result).length}`)
logger.debug('Batch get result:', { result })

View File

@ -87,13 +87,15 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
if (initialized.current || !action.selectedText) return
initialized.current = true
// Initialize assistant
const currentAssistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText)
runAsyncFunction(async () => {
// Initialize assistant
const currentAssistant = await getDefaultTranslateAssistant(targetLanguage, action.selectedText!)
assistantRef.current = currentAssistant
assistantRef.current = currentAssistant
// Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id)
// Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id)
})
}, [action, targetLanguage, translateModelPrompt])
const fetchResult = useCallback(async () => {
@ -141,7 +143,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
}
}
assistantRef.current = getDefaultTranslateAssistant(translateLang, action.selectedText)
assistantRef.current = await getDefaultTranslateAssistant(translateLang, action.selectedText)
processMessages(assistantRef.current, topicRef.current, action.selectedText, setAskId, onStream, onFinish, onError)
}, [action, targetLanguage, alterLanguage, scrollToBottom])