From bed0da6d31c2b8c2656885337a74d93be305f605 Mon Sep 17 00:00:00 2001 From: eeee0717 Date: Fri, 13 Jun 2025 15:14:25 +0800 Subject: [PATCH] feat: add quota --- src/main/ipc.ts | 2 +- src/main/ocr/BaseOcrProvider.ts | 2 +- src/main/ocr/OcrProvider.ts | 5 +- src/main/preprocess/BasePreprocessProvider.ts | 8 ++- .../preprocess/DefaultPreprocessProvider.ts | 4 ++ .../preprocess/Doc2xPreprocessProvider.ts | 4 ++ .../preprocess/MineruPreprocessProvider.ts | 53 ++++++++++++++-- .../preprocess/MistralPreprocessProvider.ts | 4 ++ src/main/preprocess/PreprocessProvider.ts | 9 ++- .../preprocess/PreprocessProviderFactory.ts | 4 +- src/main/services/KnowledgeService.ts | 21 ++++--- src/preload/index.ts | 6 +- src/renderer/src/i18n/locales/en-us.json | 3 +- src/renderer/src/i18n/locales/ja-jp.json | 6 +- src/renderer/src/i18n/locales/ru-ru.json | 42 ++----------- src/renderer/src/i18n/locales/zh-cn.json | 3 +- src/renderer/src/i18n/locales/zh-tw.json | 39 +----------- .../src/pages/knowledge/KnowledgeContent.tsx | 61 ++++++++++++------- .../components/AddKnowledgePopup.tsx | 7 +++ src/renderer/src/queue/KnowledgeQueue.ts | 2 + src/renderer/src/store/migrate.ts | 3 + src/renderer/src/store/settings.ts | 3 + src/renderer/src/types/index.ts | 2 + 23 files changed, 167 insertions(+), 126 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 77c6384ee..5a2ce7ae9 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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() diff --git a/src/main/ocr/BaseOcrProvider.ts b/src/main/ocr/BaseOcrProvider.ts index c9025f355..610dc9f9d 100644 --- a/src/main/ocr/BaseOcrProvider.ts +++ b/src/main/ocr/BaseOcrProvider.ts @@ -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 }> /** * 检查文件是否已经被预处理过 diff --git a/src/main/ocr/OcrProvider.ts b/src/main/ocr/OcrProvider.ts index c4a85d53b..c90d604c9 100644 --- a/src/main/ocr/OcrProvider.ts +++ b/src/main/ocr/OcrProvider.ts @@ -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) } diff --git a/src/main/preprocess/BasePreprocessProvider.ts b/src/main/preprocess/BasePreprocessProvider.ts index 63648475e..016e4d10d 100644 --- a/src/main/preprocess/BasePreprocessProvider.ts +++ b/src/main/preprocess/BasePreprocessProvider.ts @@ -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 /** * 检查文件是否已经被预处理过 diff --git a/src/main/preprocess/DefaultPreprocessProvider.ts b/src/main/preprocess/DefaultPreprocessProvider.ts index 8413202d2..3899a3d25 100644 --- a/src/main/preprocess/DefaultPreprocessProvider.ts +++ b/src/main/preprocess/DefaultPreprocessProvider.ts @@ -9,4 +9,8 @@ export default class DefaultPreprocessProvider extends BasePreprocessProvider { public parseFile(): Promise<{ processedFile: FileMetadata }> { throw new Error('Method not implemented.') } + + public checkQuota(): Promise { + throw new Error('Method not implemented.') + } } diff --git a/src/main/preprocess/Doc2xPreprocessProvider.ts b/src/main/preprocess/Doc2xPreprocessProvider.ts index 7c9138a27..ad311a5b8 100644 --- a/src/main/preprocess/Doc2xPreprocessProvider.ts +++ b/src/main/preprocess/Doc2xPreprocessProvider.ts @@ -322,4 +322,8 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider { } } } + + public checkQuota(): Promise { + throw new Error('Method not implemented.') + } } diff --git a/src/main/preprocess/MineruPreprocessProvider.ts b/src/main/preprocess/MineruPreprocessProvider.ts index 579a42f35..8deac95c7 100644 --- a/src/main/preprocess/MineruPreprocessProvider.ts +++ b/src/main/preprocess/MineruPreprocessProvider.ts @@ -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 = 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 { + 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 { diff --git a/src/main/preprocess/MistralPreprocessProvider.ts b/src/main/preprocess/MistralPreprocessProvider.ts index 0de2e8b15..315016280 100644 --- a/src/main/preprocess/MistralPreprocessProvider.ts +++ b/src/main/preprocess/MistralPreprocessProvider.ts @@ -180,4 +180,8 @@ export default class MistralPreprocessProvider extends BasePreprocessProvider { count: 1 } as FileMetadata } + + public checkQuota(): Promise { + throw new Error('Method not implemented.') + } } diff --git a/src/main/preprocess/PreprocessProvider.ts b/src/main/preprocess/PreprocessProvider.ts index 4232339a4..b4aed2a12 100644 --- a/src/main/preprocess/PreprocessProvider.ts +++ b/src/main/preprocess/PreprocessProvider.ts @@ -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) } diff --git a/src/main/preprocess/PreprocessProviderFactory.ts b/src/main/preprocess/PreprocessProviderFactory.ts index 98c5a1e2a..bebecd388 100644 --- a/src/main/preprocess/PreprocessProviderFactory.ts +++ b/src/main/preprocess/PreprocessProviderFactory.ts @@ -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) } diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts index 71978ee51..a7efa2cb1 100644 --- a/src/main/services/KnowledgeService.ts +++ b/src/main/services/KnowledgeService.ts @@ -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 => { 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 => { 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}`) diff --git a/src/preload/index.ts b/src/preload/index.ts index 9f3b893d7..b35f83a6d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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 }) => diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b042b1346..4cc498f90 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 903ad1ade..55a578761 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 045a16c27..9e781532e 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 39a95dcd2..eea011dbd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -579,7 +579,8 @@ "status_embedding_completed": "嵌入完成", "status_preprocess_completed": "预处理完成", "status_embedding_failed": "嵌入失败", - "status_preprocess_failed": "预处理失败" + "status_preprocess_failed": "预处理失败", + "quota": "{{name}} 剩余额度:{{quota}}" }, "languages": { "arabic": "阿拉伯文", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4f592f3da..9d7431f78 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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 的最新版本,下載速度可能較慢,請務必提前備份數據", diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index 6b439ff74..8df8ff54d 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -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 = ({ selectedBase }) => { const [expandAll, setExpandAll] = useState(false) const [progressMap, setProgressMap] = useState>(new Map()) const [preprocessMap, setPreprocessMap] = useState>(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 = ({ 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 = ({ selectedBase }) => { {base.rerankModel.name} )} + + {base.preprocessOrOcrProvider && base.preprocessOrOcrProvider.provider.quota && ( + + {t('knowledge.quota', { + name: base.preprocessOrOcrProvider.provider.name, + quota: base.preprocessOrOcrProvider.provider.quota + })} + + )} diff --git a/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx b/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx index 4ccb7c81f..5958a5785 100644 --- a/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx +++ b/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx @@ -243,6 +243,13 @@ const PopupContainer: React.FC = ({ 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 }) + }} /> diff --git a/src/renderer/src/queue/KnowledgeQueue.ts b/src/renderer/src/queue/KnowledgeQueue.ts index 331d9e54d..5cc42c66f 100644 --- a/src/renderer/src/queue/KnowledgeQueue.ts +++ b/src/renderer/src/queue/KnowledgeQueue.ts @@ -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 { 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`) diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index c766a3bae..697ba7cd3 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1565,6 +1565,9 @@ const migrateConfig = { }, '112': (state: RootState) => { try { + if (!state.settings.userId) { + state.settings.userId = uuid() + } if (!state.preprocess) { state.preprocess = { defaultProvider: '', diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index bcf270243..f0f078a1c 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -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, diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index d103794ca..0a873c520 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -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 = {