feat: add quota
This commit is contained in:
+1
-1
@@ -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()
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
/**
|
||||
* 检查文件是否已经被预处理过
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.",
|
||||
|
||||
@@ -579,7 +579,8 @@
|
||||
"status_embedding_completed": "嵌入完成",
|
||||
"status_preprocess_completed": "预处理完成",
|
||||
"status_embedding_failed": "嵌入失败",
|
||||
"status_preprocess_failed": "预处理失败"
|
||||
"status_preprocess_failed": "预处理失败",
|
||||
"quota": "{{name}} 剩余额度:{{quota}}"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "阿拉伯文",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -1565,6 +1565,9 @@ const migrateConfig = {
|
||||
},
|
||||
'112': (state: RootState) => {
|
||||
try {
|
||||
if (!state.settings.userId) {
|
||||
state.settings.userId = uuid()
|
||||
}
|
||||
if (!state.preprocess) {
|
||||
state.preprocess = {
|
||||
defaultProvider: '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user