feat: add quota

This commit is contained in:
eeee0717
2025-06-13 15:14:25 +08:00
parent f21771266e
commit bed0da6d31
23 changed files with 167 additions and 126 deletions
+1 -1
View File
@@ -4,6 +4,7 @@ import { arch } from 'node:os'
import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { handleZoomFactor } from '@main/utils/zoom'
import { FeedUrl } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
@@ -35,7 +36,6 @@ import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip'
import { FeedUrl } from '@shared/config/constant'
const fileManager = new FileStorage()
const backupManager = new BackupManager()
+1 -1
View File
@@ -18,7 +18,7 @@ export default abstract class BaseOcrProvider {
}
this.provider = provider
}
abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }>
abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata; quota?: number }>
/**
* 检查文件是否已经被预处理过
+4 -1
View File
@@ -8,7 +8,10 @@ export default class OcrProvider {
constructor(provider: Provider) {
this.sdk = OcrProviderFactory.create(provider)
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
public async parseFile(
sourceId: string,
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota?: number }> {
return this.sdk.parseFile(sourceId, file)
}
@@ -9,15 +9,19 @@ import { TypedArray } from 'pdfjs-dist/types/src/display/api'
export default abstract class BasePreprocessProvider {
protected provider: PreprocessProvider
protected userId?: string
public storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
constructor(provider: PreprocessProvider) {
constructor(provider: PreprocessProvider, userId?: string) {
if (!provider) {
throw new Error('Preprocess provider is not set')
}
this.provider = provider
this.userId = userId
}
abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }>
abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata; quota?: number }>
abstract checkQuota(): Promise<number>
/**
* 检查文件是否已经被预处理过
@@ -9,4 +9,8 @@ export default class DefaultPreprocessProvider extends BasePreprocessProvider {
public parseFile(): Promise<{ processedFile: FileMetadata }> {
throw new Error('Method not implemented.')
}
public checkQuota(): Promise<number> {
throw new Error('Method not implemented.')
}
}
@@ -322,4 +322,8 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
}
}
}
public checkQuota(): Promise<number> {
throw new Error('Method not implemented.')
}
}
@@ -39,14 +39,26 @@ type ExtractResultResponse = {
extract_result: ExtractFileResult[]
}
type QuotaResponse = {
code: number
data: {
left_quota: number
}
msg?: string
trace_id?: string
}
export default class MineruPreprocessProvider extends BasePreprocessProvider {
constructor(provider: PreprocessProvider) {
super(provider)
constructor(provider: PreprocessProvider, userId?: string) {
super(provider, userId)
// todo:免费期结束后删除
this.provider.apiKey = this.provider.apiKey || import.meta.env.VITE_MINERU_API_KEY
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
public async parseFile(
sourceId: string,
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota: number }> {
try {
Logger.info(`MinerU preprocess processing started: ${file.path}`)
await this.validateFile(file.path)
@@ -62,9 +74,13 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
// 3. 下载并解压文件
const { path: outputPath } = await this.downloadAndExtractFile(extractResult.full_zip_url!, file)
// 4. 创建处理后的文件信息
// 4. check quota
const quota = await this.checkQuota()
// 5. 创建处理后的文件信息
return {
processedFile: this.createProcessedFileInfo(file, outputPath)
processedFile: this.createProcessedFileInfo(file, outputPath),
quota
}
} catch (error: any) {
Logger.error(`MinerU preprocess processing failed for ${file.path}: ${error.message}`)
@@ -72,7 +88,30 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
}
}
public async checkQuota() {
try {
const quota = await fetch(`${this.provider.apiHost}/api/v4/quota`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.provider.apiKey}`,
token: this.userId ?? ''
}
})
if (!quota.ok) {
throw new Error(`HTTP ${quota.status}: ${quota.statusText}`)
}
const { data }: ApiResponse<QuotaResponse> = await quota.json()
console.log('MinerU left quota:', data.data.left_quota)
return data.data.left_quota
} catch (error) {
console.error('Error checking quota:', error)
throw error
}
}
private async validateFile(filePath: string): Promise<void> {
const quota = await this.checkQuota()
const pdfBuffer = await fs.promises.readFile(filePath)
const doc = await this.readPdf(new Uint8Array(pdfBuffer))
@@ -86,6 +125,10 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 200MB`)
}
// 检查配额
if (quota <= 0 || quota - doc.numPages <= 0) {
throw new Error('MinerU解析配额不足,请申请企业账户或自行部署,剩余额度:' + quota)
}
}
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {
@@ -180,4 +180,8 @@ export default class MistralPreprocessProvider extends BasePreprocessProvider {
count: 1
} as FileMetadata
}
public checkQuota(): Promise<number> {
throw new Error('Method not implemented.')
}
}
+6 -3
View File
@@ -5,10 +5,13 @@ import PreprocessProviderFactory from './PreprocessProviderFactory'
export default class PreprocessProvider {
private sdk: BasePreprocessProvider
constructor(provider: Provider) {
this.sdk = PreprocessProviderFactory.create(provider)
constructor(provider: Provider, userId?: string) {
this.sdk = PreprocessProviderFactory.create(provider, userId)
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
public async parseFile(
sourceId: string,
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota?: number }> {
return this.sdk.parseFile(sourceId, file)
}
@@ -6,14 +6,14 @@ import Doc2xPreprocessProvider from './Doc2xPreprocessProvider'
import MineruPreprocessProvider from './MineruPreprocessProvider'
import MistralPreprocessProvider from './MistralPreprocessProvider'
export default class PreprocessProviderFactory {
static create(provider: PreprocessProvider): BasePreprocessProvider {
static create(provider: PreprocessProvider, userId?: string): BasePreprocessProvider {
switch (provider.id) {
case 'doc2x':
return new Doc2xPreprocessProvider(provider)
case 'mistral':
return new MistralPreprocessProvider(provider)
case 'mineru':
return new MineruPreprocessProvider(provider)
return new MineruPreprocessProvider(provider, userId)
default:
return new DefaultPreprocessProvider(provider)
}
+12 -9
View File
@@ -40,12 +40,14 @@ export interface KnowledgeBaseAddItemOptions {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
userId?: string
}
interface KnowledgeBaseAddItemOptionsNonNullableAttribute {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload: boolean
userId: string
}
interface EvaluateTaskWorkload {
@@ -174,7 +176,7 @@ class KnowledgeService {
ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask {
const { base, item, forceReload } = options
const { base, item, forceReload, userId } = options
const file = item.content as FileMetadata
const loaderTask: LoaderTask = {
@@ -184,7 +186,7 @@ class KnowledgeService {
task: async () => {
try {
// 添加预处理逻辑
const fileToProcess: FileMetadata = await this.preprocessing(file, base, item)
const fileToProcess: FileMetadata = await this.preprocessing(file, base, item, userId)
// 使用处理后的文件进行加载
return addFileLoader(ragApplication, fileToProcess, base, forceReload)
@@ -470,8 +472,8 @@ class KnowledgeService {
public add = async (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
return new Promise((resolve) => {
const { base, item, forceReload = false } = options
const optionsNonNullableAttribute = { base, item, forceReload }
const { base, item, forceReload = false, userId = '' } = options
const optionsNonNullableAttribute = { base, item, forceReload, userId }
this.getRagApplication(base)
.then((ragApplication) => {
const task = (() => {
@@ -551,14 +553,15 @@ class KnowledgeService {
private preprocessing = async (
file: FileMetadata,
base: KnowledgeBaseParams,
item: KnowledgeItem
item: KnowledgeItem,
userId: string
): Promise<FileMetadata> => {
let fileToProcess: FileMetadata = file
if (base.preprocessOrOcrProvider && file.ext.toLowerCase() === '.pdf') {
try {
let provider: PreprocessProvider | OcrProvider
if (base.preprocessOrOcrProvider.type === 'preprocess') {
provider = new PreprocessProvider(base.preprocessOrOcrProvider.provider)
provider = new PreprocessProvider(base.preprocessOrOcrProvider.provider, userId)
} else {
provider = new OcrProvider(base.preprocessOrOcrProvider.provider)
}
@@ -571,12 +574,12 @@ class KnowledgeService {
// 执行预处理
Logger.info(`Starting preprocess processing for scanned PDF: ${file.path}`)
const { processedFile } = await provider.parseFile(item.id, file)
const { processedFile, quota } = await provider.parseFile(item.id, file)
fileToProcess = processedFile
console.warn('base', base, 'item', item, 'file', file)
const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('file-preprocess-finished', {
itemId: item.id
itemId: item.id,
quota: quota
})
} catch (err) {
Logger.error(`Preprocess processing failed: ${err}`)
+4 -2
View File
@@ -125,12 +125,14 @@ const api = {
add: ({
base,
item,
forceReload = false
forceReload = false,
userId
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Add, { base, item, forceReload }),
userId?: string
}) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Add, { base, item, forceReload, userId }),
remove: ({ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Remove, { uniqueId, uniqueIds, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
+2 -1
View File
@@ -579,7 +579,8 @@
"dimensions_error_invalid": "Please enter embedding dimension size",
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}}).",
"dimensions_set_right": "⚠️ Please ensure the model supports the set embedding dimension size",
"dimensions_default": "The model will use default embedding dimensions"
"dimensions_default": "The model will use default embedding dimensions",
"quota": "{{name}} Left Quota: {{quota}}"
},
"languages": {
"arabic": "Arabic",
+4 -2
View File
@@ -579,7 +579,8 @@
"dimensions_error_invalid": "埋め込み次元のサイズを入力してください",
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。",
"dimensions_set_right": "⚠️ モデルが設定した埋め込み次元のサイズをサポートしていることを確認してください",
"dimensions_default": "モデルはデフォルトの埋め込み次元を使用します"
"dimensions_default": "モデルはデフォルトの埋め込み次元を使用します",
"quota": "{{name}} 残りクォータ: {{quota}}"
},
"languages": {
"arabic": "アラビア語",
@@ -711,7 +712,8 @@
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
"download.success": "ダウンロードに成功しました",
"download.failed": "ダウンロードに失敗しました"
"download.failed": "ダウンロードに失敗しました",
"error.fetchTopicName": "トピックの命名に失敗しました"
},
"minapp": {
"popup": {
+4 -38
View File
@@ -579,7 +579,8 @@
"dimensions_error_invalid": "Пожалуйста, введите размерность эмбеддинга",
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})",
"dimensions_set_right": "⚠️ Убедитесь, что модель поддерживает заданный размер эмбеддинга",
"dimensions_default": "Модель будет использовать размер эмбеддинга по умолчанию"
"dimensions_default": "Модель будет использовать размер эмбеддинга по умолчанию",
"quota": "{{name}} Остаток квоты: {{quota}}"
},
"languages": {
"arabic": "Арабский",
@@ -711,7 +712,8 @@
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
"download.success": "Скачано успешно",
"download.failed": "Скачивание не удалось"
"download.failed": "Скачивание не удалось",
"error.fetchTopicName": "Не удалось назвать тему"
},
"minapp": {
"popup": {
@@ -1791,42 +1793,6 @@
"tray.onclose": "Свернуть в трей при закрытии",
"tray.show": "Показать значок в трее",
"tray.title": "Трей",
"websearch": {
"blacklist": "Черный список",
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
"check": "проверка",
"check_failed": "Проверка не прошла",
"check_success": "Проверка успешна",
"get_api_key": "Получить ключ API",
"no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.",
"search_max_result": "Количество результатов поиска",
"search_provider": "поиск сервисного провайдера",
"search_provider_placeholder": "Выберите поставщика поисковых услуг",
"search_result_default": "По умолчанию",
"search_with_time": "Поиск, содержащий дату",
"tavily": {
"api_key": "Ключ API Tavily",
"api_key.placeholder": "Введите ключ API Tavily",
"description": "Tavily — это поисковая система, специально разработанная для ИИ-агентов, предоставляющая актуальные результаты, умные предложения по запросам и глубокие исследовательские возможности",
"title": "Tavily"
},
"title": "Поиск в Интернете",
"blacklist_tooltip": "Шаблон: *://*.example.com/*\nРегулярное выражение: /example\\.(net|org)/",
"subscribe": "Подписка на черный список",
"subscribe_update": "Обновить",
"subscribe_add": "Добавить",
"subscribe_url": "URL подписки",
"subscribe_name": "Альтернативное имя",
"subscribe_name.placeholder": "Альтернативное имя, если в подписке нет названия.",
"subscribe_add_success": "Подписка успешно добавлена!",
"subscribe_delete": "Удалить",
"overwrite": "Переопределить провайдера поиска",
"overwrite_tooltip": "Использовать провайдера поиска вместо LLM",
"apikey": "API ключ",
"free": "Бесплатно",
"content_limit": "Ограничение длины текста",
"content_limit_tooltip": "Ограничьте длину содержимого результатов поиска, контент, превышающий ограничение, будет обрезан."
},
"general.auto_check_update.title": "Автоматическое обновление",
"general.early_access.title": "Ранний доступ",
"general.early_access.tooltip": "Включить для использования последней версии из GitHub, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.",
+2 -1
View File
@@ -579,7 +579,8 @@
"status_embedding_completed": "嵌入完成",
"status_preprocess_completed": "预处理完成",
"status_embedding_failed": "嵌入失败",
"status_preprocess_failed": "预处理失败"
"status_preprocess_failed": "预处理失败",
"quota": "{{name}} 剩余额度:{{quota}}"
},
"languages": {
"arabic": "阿拉伯文",
+2 -37
View File
@@ -579,7 +579,8 @@
"dimensions_error_invalid": "請輸入嵌入維度大小",
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}}",
"dimensions_set_right": "⚠️ 請確保模型支援所設置的嵌入維度大小",
"dimensions_default": "模型將使用預設嵌入維度"
"dimensions_default": "模型將使用預設嵌入維度",
"quota": "{{name}} 剩餘配額:{{quota}}"
},
"languages": {
"arabic": "阿拉伯文",
@@ -1795,42 +1796,6 @@
"tray.onclose": "關閉時最小化到系统匣",
"tray.show": "顯示系统匣圖示",
"tray.title": "系统匣",
"websearch": {
"check_success": "驗證成功",
"get_api_key": "點選這裡取得金鑰",
"search_with_time": "搜尋包含日期",
"tavily": {
"api_key": "Tavily API 金鑰",
"api_key.placeholder": "請輸入 Tavily API 金鑰",
"description": "Tavily 是一個為 AI 代理量身訂製的搜尋引擎,提供即時、準確的結果、智慧查詢建議和深入的研究能力",
"title": "Tavily"
},
"blacklist": "黑名單",
"blacklist_description": "以下網站不會出現在搜索結果中",
"search_max_result": "搜尋結果個數",
"search_result_default": "預設",
"check": "檢查",
"search_provider": "搜尋服務商",
"search_provider_placeholder": "選擇一個搜尋服務商",
"no_provider_selected": "請選擇搜索服務商後再檢查",
"check_failed": "驗證失敗",
"blacklist_tooltip": "匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/",
"subscribe": "黑名單訂閱",
"subscribe_update": "更新",
"subscribe_add": "添加訂閱",
"subscribe_url": "訂閱源地址",
"subscribe_name": "替代名稱",
"subscribe_name.placeholder": "當下載的訂閱源沒有名稱時所使用的替代名稱",
"subscribe_add_success": "訂閱源添加成功!",
"subscribe_delete": "刪除",
"title": "網路搜尋",
"overwrite": "覆蓋搜尋服務商",
"overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋",
"apikey": "API 金鑰",
"free": "免費",
"content_limit": "內容長度限制",
"content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷"
},
"general.auto_check_update.title": "自動更新",
"general.early_access.title": "搶先體驗",
"general.early_access.tooltip": "開啟後,將使用 GitHub 的最新版本,下載速度可能較慢,請務必提前備份數據",
@@ -7,6 +7,7 @@ import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import Logger from '@renderer/config/logger'
import { useKnowledge } from '@renderer/hooks/useKnowledge'
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import FileManager from '@renderer/services/FileManager'
import { getProviderName } from '@renderer/services/ProviderService'
import { FileMetadata, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
@@ -40,26 +41,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
const [expandAll, setExpandAll] = useState(false)
const [progressMap, setProgressMap] = useState<Map<string, number>>(new Map())
const [preprocessMap, setPreprocessMap] = useState<Map<string, boolean>>(new Map())
useEffect(() => {
const handlers = [
window.electron.ipcRenderer.on('file-preprocess-finished', (_, { itemId }) => {
setPreprocessMap((prev) => new Map(prev).set(itemId, true))
}),
window.electron.ipcRenderer.on('file-preprocess-progress', (_, { itemId, progress }) => {
setProgressMap((prev) => new Map(prev).set(itemId, progress))
}),
window.electron.ipcRenderer.on('directory-processing-percent', (_, { itemId, percent }) => {
console.log('[Progress] Directory:', itemId, percent)
setProgressMap((prev) => new Map(prev).set(itemId, percent))
})
]
return () => {
handlers.forEach((cleanup) => cleanup())
}
}, [])
const {
base,
@@ -80,9 +61,38 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
updateItem
} = useKnowledge(selectedBase.id || '')
const { updatePreprocessProvider } = usePreprocessProvider(
selectedBase.preprocessOrOcrProvider?.provider.id || 'mineru'
)
const providerName = getProviderName(base?.model.provider || '')
const disabled = !base?.version || !providerName
console.log(fileItems)
useEffect(() => {
const handlers = [
window.electron.ipcRenderer.on('file-preprocess-finished', (_, { itemId, quota }) => {
setPreprocessMap((prev) => new Map(prev).set(itemId, true))
if (base?.preprocessOrOcrProvider && quota) {
updatePreprocessProvider({
...base.preprocessOrOcrProvider.provider,
quota: quota
})
}
}),
window.electron.ipcRenderer.on('file-preprocess-progress', (_, { itemId, progress }) => {
setProgressMap((prev) => new Map(prev).set(itemId, progress))
}),
window.electron.ipcRenderer.on('directory-processing-percent', (_, { itemId, percent }) => {
console.log('[Progress] Directory:', itemId, percent)
setProgressMap((prev) => new Map(prev).set(itemId, percent))
})
]
return () => {
handlers.forEach((cleanup) => cleanup())
}
}, [])
if (!base) {
return null
@@ -293,6 +303,15 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
{base.rerankModel.name}
</Tag>
)}
{base.preprocessOrOcrProvider && base.preprocessOrOcrProvider.provider.quota && (
<Tag color="orange" style={{ borderRadius: 20, margin: 0 }}>
{t('knowledge.quota', {
name: base.preprocessOrOcrProvider.provider.name,
quota: base.preprocessOrOcrProvider.provider.quota
})}
</Tag>
)}
</div>
</ModelInfo>
<HStack gap={8} alignItems="center">
@@ -243,6 +243,13 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
style={{ width: '100%' }}
options={embeddingSelectOptions}
placeholder={t('settings.models.empty')}
onChange={(value) => {
const model = value
? providers.flatMap((p) => p.models).find((m) => getModelUniqId(m) === value)
: undefined
if (!model) return
setNewBase({ ...newBase, model })
}}
/>
</SettingsItem>
+2
View File
@@ -1,5 +1,6 @@
import Logger from '@renderer/config/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'
@@ -93,6 +94,7 @@ class KnowledgeQueue {
private async processItem(baseId: string, item: KnowledgeItem): Promise<void> {
const notificationService = NotificationService.getInstance()
const userId = getStoreSetting('userId')
try {
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
Logger.log(`[KnowledgeQueue] Item ${item.id} has reached max retries, skipping`)
+3
View File
@@ -1565,6 +1565,9 @@ const migrateConfig = {
},
'112': (state: RootState) => {
try {
if (!state.settings.userId) {
state.settings.userId = uuid()
}
if (!state.preprocess) {
state.preprocess = {
defaultProvider: '',
+3
View File
@@ -11,6 +11,7 @@ import {
ThemeMode,
TranslateLanguageVarious
} from '@renderer/types'
import { uuid } from '@renderer/utils'
import { WebDAVSyncState } from './backup'
@@ -46,6 +47,7 @@ export interface SettingsState {
proxyMode: 'system' | 'custom' | 'none'
proxyUrl?: string
userName: string
userId: string
showPrompt: boolean
showTokens: boolean
showMessageDivider: boolean
@@ -194,6 +196,7 @@ export const initialState: SettingsState = {
proxyMode: 'system',
proxyUrl: undefined,
userName: '',
userId: uuid(),
showPrompt: true,
showTokens: true,
showMessageDivider: true,
+2
View File
@@ -435,6 +435,7 @@ export interface PreprocessProvider {
apiHost?: string
model?: string
options?: any
quota?: number
}
export interface OcrProvider {
@@ -444,6 +445,7 @@ export interface OcrProvider {
apiHost?: string
model?: string
options?: any
quota?: number
}
export type GenerateImageParams = {