From d84b84eb2f7fdd3c0600001d9ef0147fe530090b Mon Sep 17 00:00:00 2001 From: Nicolae Fericitu <118419291+NicolaeFericitu@users.noreply.github.com> Date: Mon, 12 Jan 2026 04:58:38 +0200 Subject: [PATCH 01/10] i18n: Major improvements to Romanian (ro-RO) localization (#12428) * fix(i18n): update and refine Romanian translation I have corrected several typos and refined the terminology in the ro-ro.json file for better linguistic accuracy. This update ensures translation consistency throughout the user interface. * i18n: Update and fix Romanian localization (ro-RO) The Romanian localization file has been updated. Necessary corrections have been applied to address issues identified during an interface review, ensuring consistent terminology and improved message clarity. * i18n: Capitalize "Users" label for UI consistency Updated the "users" key in ro-ro.json to use an uppercase initial. This ensures visual consistency with other menu items in the settings section (User Management). --- src/renderer/src/i18n/translate/ro-ro.json | 70 +++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json index fb45d9c647..58f07d67c5 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -378,7 +378,7 @@ "about": "Despre", "close": "Închide fereastra", "copy": "Copiază", - "cut": "Taie", + "cut": "Decupează", "delete": "Șterge", "documentation": "Documentație", "edit": "Editare", @@ -1191,7 +1191,7 @@ "agent_one": "Agent", "agent_other": "Agenți", "and": "și", - "assistant": "Agent", + "assistant": "Asistent", "assistant_one": "Asistent", "assistant_other": "Asistenți", "avatar": "Avatar", @@ -1208,7 +1208,7 @@ "copy": "Copiază", "copy_failed": "Copiere eșuată", "current": "Curent", - "cut": "Taie", + "cut": "Decupează", "default": "Implicit", "delete": "Șterge", "delete_confirm": "Ești sigur că vrei să ștergi?", @@ -1222,10 +1222,10 @@ "duplicate": "Duplică", "edit": "Editează", "enabled": "Activat", - "error": "eroare", + "error": "Eroare", "errors": { "create_message": "Nu s-a putut crea mesajul", - "validation": "Verificarea a eșuat" + "validation": "Validarea a eșuat" }, "expand": "Extinde", "file": { @@ -1611,7 +1611,7 @@ "title": "Setări bază de cunoștințe" }, "sitemap_added": "Adăugat cu succes", - "sitemap_placeholder": "Introdu URL-ul hărții site-ului", + "sitemap_placeholder": "Introdu URL-ul sitemap-ului", "sitemaps": "Site-uri web", "source": "Sursă", "status": "Stare", @@ -1623,7 +1623,7 @@ "status_pending": "În așteptare", "status_preprocess_completed": "Preprocesare finalizată", "status_preprocess_failed": "Preprocesare eșuată", - "status_processing": "Se procesează", + "status_processing": "În procesare", "subtitle_file": "fișier subtitrare", "threshold": "Prag de potrivire", "threshold_placeholder": "Nesetat", @@ -1633,9 +1633,9 @@ "topN": "Număr rezultate returnate", "topN_placeholder": "Nesetat", "topN_too_large_or_small": "Numărul de rezultate returnate nu poate fi mai mare de 30 sau mai mic de 1.", - "topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și mai mulți tokeni consumați.", + "topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și un consum mai mare de tokeni.", "url_added": "URL adăugat", - "url_placeholder": "Introdu URL, separă URL-urile multiple prin Enter", + "url_placeholder": "Introdu URL-ul; separă mai multe URL-uri prin Enter", "urls": "URL-uri", "videos": "video", "videos_file": "fișier video" @@ -1760,7 +1760,7 @@ "switch_user_confirm": "Schimbi contextul de utilizator la {{user}}?", "time": "Timp", "title": "Amintiri", - "total_memories": "total amintiri", + "total_memories": "Total amintiri", "try_different_filters": "Încearcă să ajustezi criteriile de căutare", "update_failed": "Nu s-a putut actualiza amintirea", "update_success": "Amintire actualizată cu succes", @@ -1779,7 +1779,7 @@ "user_memories_reset": "Toate amintirile pentru {{user}} au fost resetate", "user_switch_failed": "Nu s-a putut schimba utilizatorul", "user_switched": "Contextul de utilizator a fost schimbat la {{user}}", - "users": "utilizatori" + "users": "Utilizatori" }, "message": { "agents": { @@ -2196,7 +2196,7 @@ "navbar": { "expand": "Extinde dialogul", "hide_sidebar": "Ascunde bara laterală", - "show_sidebar": "Arată bara laterală", + "show_sidebar": "Afișează bara laterală", "window": { "close": "Închide", "maximize": "Maximizează", @@ -2216,27 +2216,27 @@ }, "characters": "Caractere", "collapse": "Restrânge", - "content_placeholder": "Te rugăm să introduci conținutul notiței...", + "content_placeholder": "Introdu conținutul notiței...", "copyContent": "Copiază conținutul", "crossPlatformRestoreWarning": "Configurația multi-platformă a fost restaurată, dar directorul de notițe este gol. Te rugăm să copiezi fișierele notițelor în: {{path}}", - "delete": "șterge", + "delete": "Șterge", "delete_confirm": "Ești sigur că vrei să ștergi acest {{type}}?", "delete_folder_confirm": "Ești sigur că vrei să ștergi dosarul \"{{name}}\" și tot conținutul său?", "delete_note_confirm": "Ești sigur că vrei să ștergi notița \"{{name}}\"?", "drop_markdown_hint": "Trage fișiere sau dosare .md aici pentru a importa", "empty": "Încă nu există notițe disponibile", - "expand": "desfășoară", + "expand": "Extinde", "export_failed": "Exportul în baza de cunoștințe a eșuat", "export_knowledge": "Exportă notițele în baza de cunoștințe", "export_success": "Exportat cu succes în baza de cunoștințe", - "folder": "dosar", + "folder": "Dosar", "new_folder": "Dosar nou", "new_note": "Creează o notiță nouă", "no_content_to_copy": "Niciun conținut de copiat", "no_file_selected": "Te rugăm să selectezi fișierul de încărcat", "no_valid_files": "Nu a fost încărcat niciun fișier valid", "open_folder": "Deschide un dosar extern", - "open_outside": "Deschide din exterior", + "open_outside": "Deschide extern", "rename": "Redenumește", "rename_changed": "Din cauza politicilor de securitate, numele fișierului a fost schimbat din {{original}} în {{final}}", "save": "Salvează în Notițe", @@ -2275,7 +2275,7 @@ "font_size_small": "Mic", "font_title": "Setări font", "serif_font": "Font cu serife", - "show_table_of_contents": "Arată cuprinsul", + "show_table_of_contents": "Afișează cuprinsul", "show_table_of_contents_description": "Afișează o bară laterală cu cuprinsul pentru o navigare ușoară în documente", "title": "Setări afișare" }, @@ -4262,7 +4262,7 @@ "display_title": "Setări afișare mini-aplicații", "empty": "Trage mini-aplicațiile din stânga pentru a le ascunde", "open_link_external": { - "title": "Deschide linkurile de fereastră nouă în browser" + "title": "Deschide în browser linkurile care deschid ferestre noi" }, "reset_tooltip": "Resetează la implicit", "sidebar_description": "Arată mini-aplicațiile active în bara laterală", @@ -4362,7 +4362,7 @@ "description": "Model folosit pentru sarcini simple, cum ar fi numirea subiectelor și extragerea cuvintelor cheie", "label": "Model rapid", "setting_title": "Configurare model rapid", - "tooltip": "Se recomandă alegerea unui model ușor și nu se recomandă alegerea unui model de gândire." + "tooltip": "Se recomandă alegerea unui model ușor, nu a unui model de raționament complex." }, "topic_naming": { "auto": "Numire automată subiect", @@ -4702,7 +4702,7 @@ }, "shortcuts": { "action": "Acțiune", - "actions": "operațiune", + "actions": "Comandă", "clear_shortcut": "Șterge comanda rapidă", "clear_topic": "Șterge mesajele", "copy_last_message": "Copiază ultimul mesaj", @@ -4733,8 +4733,8 @@ }, "theme": { "color_primary": "Culoare primară", - "dark": "Întunecat", - "light": "Luminos", + "dark": "Întunecată", + "light": "Luminoasă", "system": "Sistem", "title": "Temă", "window": { @@ -4896,21 +4896,21 @@ "translate": { "custom": { "delete": { - "description": "Ești sigur că vrei să ștergi?", + "description": "Ești sigur că vrei să ștergi această limbă?", "title": "Șterge limba personalizată" }, "error": { "add": "Adăugarea a eșuat", "delete": "Ștergerea a eșuat", "langCode": { - "builtin": "Limba are suport integrat", - "empty": "Codul limbii este gol", + "builtin": "Limbă deja integrată", + "empty": "Codul limbii lipsește", "exists": "Limba există deja", "invalid": "Cod limbă invalid" }, "update": "Actualizarea a eșuat", "value": { - "empty": "Numele limbii nu poate fi gol", + "empty": "Numele limbii este obligatoriu", "too_long": "Numele limbii este prea lung" } }, @@ -4920,13 +4920,13 @@ "placeholder": "en-us" }, "success": { - "add": "Adăugat cu succes", - "delete": "Șters cu succes", - "update": "Actualizare reușită" + "add": "Adăugată cu succes", + "delete": "Ștearsă cu succes", + "update": "Actualizată cu succes" }, "table": { "action": { - "title": "Operațiune" + "title": "Acțiuni" } }, "value": { @@ -4958,7 +4958,7 @@ "mcp-servers": "Servere MCP", "memories": "Amintiri", "notes": "Notițe", - "paintings": "Picturi", + "paintings": "Imagini", "settings": "Setări", "store": "Bibliotecă asistenți", "translate": "Traducere" @@ -5002,7 +5002,7 @@ "detect": { "method": { "algo": { - "label": "algoritm", + "label": "Algoritm", "tip": "Folosește biblioteca franc pentru detectarea limbii" }, "auto": { @@ -5109,7 +5109,7 @@ "tray": { "quit": "Ieșire", "show_mini_window": "Asistent rapid", - "show_window": "Arată fereastra" + "show_window": "Afișează fereastra" }, "update": { "install": "Instalează", @@ -5125,7 +5125,7 @@ "words": { "knowledgeGraph": "Grafic de cunoștințe", "quit": "Ieșire", - "show_window": "Arată fereastra", + "show_window": "Afișează fereastra", "visualization": "Vizualizare" } } From cea36d170bd9d59e56179f5a553531f3db449b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:41:27 +0800 Subject: [PATCH 02/10] fix(qwen-code): format baseUrl with /v1 for OpenAI-compatible tools (#12418) The Qwen Code tool was failing with 'Model stream ended without a finish reason' because the OPENAI_BASE_URL environment variable was not properly formatted. This fix adds /v1 suffix to the baseUrl when it's missing for OpenAI-compatible tools (qwenCode, openaiCodex, iFlowCli). Changes: - Import formatApiHost from @renderer/utils/api - Use formatApiHost to format baseUrl before passing to environment variables - Add unit tests for the URL formatting behavior --- .../src/pages/code/__tests__/index.test.ts | 231 ++++++++++++++++++ src/renderer/src/pages/code/index.ts | 8 +- 2 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/pages/code/__tests__/index.test.ts diff --git a/src/renderer/src/pages/code/__tests__/index.test.ts b/src/renderer/src/pages/code/__tests__/index.test.ts new file mode 100644 index 0000000000..0b02c797fc --- /dev/null +++ b/src/renderer/src/pages/code/__tests__/index.test.ts @@ -0,0 +1,231 @@ +import type { Model, Provider } from '@renderer/types' +import { codeTools } from '@shared/config/constant' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock CodeToolsPage which is the default export +vi.mock('../CodeToolsPage', () => ({ default: () => null })) + +// Mock dependencies needed by CodeToolsPage +vi.mock('@renderer/hooks/useCodeTools', () => ({ + useCodeTools: () => ({ + selectedCliTool: codeTools.qwenCode, + selectedModel: null, + selectedTerminal: 'systemDefault', + environmentVariables: '', + directories: [], + currentDirectory: '', + canLaunch: true, + setCliTool: vi.fn(), + setModel: vi.fn(), + setTerminal: vi.fn(), + setEnvVars: vi.fn(), + setCurrentDir: vi.fn(), + removeDir: vi.fn(), + selectFolder: vi.fn() + }) +})) + +vi.mock('@renderer/hooks/useProvider', () => ({ + useProviders: () => ({ providers: [] }), + useAllProviders: () => [] +})) + +vi.mock('@renderer/services/AssistantService', () => ({ + getProviderByModel: vi.fn() +})) + +vi.mock('@renderer/services/LoggerService', () => ({ + loggerService: { + withContext: () => ({ + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + warn: vi.fn() + }) + } +})) + +vi.mock('@renderer/store', () => ({ + useAppDispatch: () => vi.fn(), + useAppSelector: () => false +})) + +vi.mock('@renderer/aiCore', () => ({ + default: class { + getBaseURL() { + return '' + } + getApiKey() { + return '' + } + } +})) + +vi.mock('@renderer/utils/api', () => ({ + formatApiHost: vi.fn((host) => { + if (!host) return '' + const normalized = host.replace(/\/$/, '').trim() + if (normalized.endsWith('#')) { + return normalized.replace(/#$/, '') + } + if (/\/v\d+(?:alpha|beta)?(?=\/|$)/i.test(normalized)) { + return normalized + } + return `${normalized}/v1` + }) +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }) +})) + +describe('generateToolEnvironment', () => { + const createMockModel = (id: string, provider: string): Model => ({ + id, + name: id, + provider, + group: provider + }) + + const createMockProvider = (id: string, apiHost: string): Provider => ({ + id, + type: 'openai', + name: id, + apiKey: 'test-key', + apiHost, + models: [], + isSystem: true + }) + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should format baseUrl with /v1 for qwenCode when missing', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('qwen-turbo', 'dashscope') + const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode') + + const env = generateToolEnvironment({ + tool: codeTools.qwenCode, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode' + }) + + expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1') + }) + + it('should not duplicate /v1 when already present for qwenCode', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('qwen-turbo', 'dashscope') + const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/v1') + + const env = generateToolEnvironment({ + tool: codeTools.qwenCode, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' + }) + + expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1') + }) + + it('should handle empty baseUrl gracefully', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('qwen-turbo', 'dashscope') + const provider = createMockProvider('dashscope', '') + + const env = generateToolEnvironment({ + tool: codeTools.qwenCode, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: '' + }) + + expect(env.OPENAI_BASE_URL).toBe('') + }) + + it('should preserve other API versions when present', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('qwen-plus', 'dashscope') + const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/v2') + + const env = generateToolEnvironment({ + tool: codeTools.qwenCode, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://dashscope.aliyuncs.com/v2' + }) + + expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/v2') + }) + + it('should format baseUrl with /v1 for openaiCodex when missing', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('gpt-4', 'openai') + const provider = createMockProvider('openai', 'https://api.openai.com') + + const env = generateToolEnvironment({ + tool: codeTools.openaiCodex, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://api.openai.com' + }) + + expect(env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1') + }) + + it('should format baseUrl with /v1 for iFlowCli when missing', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('gpt-4', 'iflow') + const provider = createMockProvider('iflow', 'https://api.iflow.cn') + + const env = generateToolEnvironment({ + tool: codeTools.iFlowCli, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://api.iflow.cn' + }) + + expect(env.IFLOW_BASE_URL).toBe('https://api.iflow.cn/v1') + }) + + it('should handle trailing slash correctly', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('qwen-turbo', 'dashscope') + const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/') + + const env = generateToolEnvironment({ + tool: codeTools.qwenCode, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/' + }) + + expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1') + }) + + it('should handle v2beta version correctly', async () => { + const { generateToolEnvironment } = await import('../index') + const model = createMockModel('qwen-plus', 'dashscope') + const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/v2beta') + + const env = generateToolEnvironment({ + tool: codeTools.qwenCode, + model, + modelProvider: provider, + apiKey: 'test-key', + baseUrl: 'https://dashscope.aliyuncs.com/v2beta' + }) + + expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/v2beta') + }) +}) diff --git a/src/renderer/src/pages/code/index.ts b/src/renderer/src/pages/code/index.ts index 81f5ddddc3..5b30b8f8c5 100644 --- a/src/renderer/src/pages/code/index.ts +++ b/src/renderer/src/pages/code/index.ts @@ -1,4 +1,5 @@ import { type EndpointType, type Model, type Provider, SystemProviderIds } from '@renderer/types' +import { formatApiHost } from '@renderer/utils/api' import { codeTools } from '@shared/config/constant' export interface LaunchValidationResult { @@ -145,6 +146,7 @@ export const generateToolEnvironment = ({ baseUrl: string }): Record => { const env: Record = {} + const formattedBaseUrl = formatApiHost(baseUrl) switch (tool) { case codeTools.claudeCode: @@ -169,19 +171,19 @@ export const generateToolEnvironment = ({ case codeTools.qwenCode: env.OPENAI_API_KEY = apiKey - env.OPENAI_BASE_URL = baseUrl + env.OPENAI_BASE_URL = formattedBaseUrl env.OPENAI_MODEL = model.id break case codeTools.openaiCodex: env.OPENAI_API_KEY = apiKey - env.OPENAI_BASE_URL = baseUrl + env.OPENAI_BASE_URL = formattedBaseUrl env.OPENAI_MODEL = model.id env.OPENAI_MODEL_PROVIDER = modelProvider.id break case codeTools.iFlowCli: env.IFLOW_API_KEY = apiKey - env.IFLOW_BASE_URL = baseUrl + env.IFLOW_BASE_URL = formattedBaseUrl env.IFLOW_MODEL_NAME = model.id break From cbeda03acbe780f72f8bf1aa2dbda6f525e1cdf4 Mon Sep 17 00:00:00 2001 From: flt6 <42725841+flt6@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:49:33 +0800 Subject: [PATCH 03/10] use cumsum in anthropic cache (#12419) * use cumsum in anthropic cache * fix types and refactor addCache in anthropicCacheMiddleware --- .../middleware/anthropicCacheMiddleware.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/aiCore/middleware/anthropicCacheMiddleware.ts b/src/renderer/src/aiCore/middleware/anthropicCacheMiddleware.ts index df50798940..80a391b863 100644 --- a/src/renderer/src/aiCore/middleware/anthropicCacheMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/anthropicCacheMiddleware.ts @@ -2,6 +2,7 @@ * Anthropic Prompt Caching Middleware * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control */ +import type { LanguageModelV2Message } from '@ai-sdk/provider' import { estimateTextTokens } from '@renderer/services/TokenService' import type { Provider } from '@renderer/types' import type { LanguageModelMiddleware } from 'ai' @@ -10,11 +11,11 @@ const cacheProviderOptions = { anthropic: { cacheControl: { type: 'ephemeral' } } } -function estimateContentTokens(content: unknown): number { +function estimateContentTokens(content: LanguageModelV2Message['content']): number { if (typeof content === 'string') return estimateTextTokens(content) if (Array.isArray(content)) { return content.reduce((acc, part) => { - if (typeof part === 'object' && part !== null && 'text' in part) { + if (part.type === 'text') { return acc + estimateTextTokens(part.text as string) } return acc @@ -23,21 +24,6 @@ function estimateContentTokens(content: unknown): number { return 0 } -function addCacheToContentParts(content: unknown): unknown { - if (typeof content === 'string') { - return [{ type: 'text', text: content, providerOptions: cacheProviderOptions }] - } - if (Array.isArray(content) && content.length > 0) { - const result = [...content] - const last = result[result.length - 1] - if (typeof last === 'object' && last !== null) { - result[result.length - 1] = { ...last, providerOptions: cacheProviderOptions } - } - return result - } - return content -} - export function anthropicCacheMiddleware(provider: Provider): LanguageModelMiddleware { return { middlewareVersion: 'v2', @@ -54,7 +40,7 @@ export function anthropicCacheMiddleware(provider: Provider): LanguageModelMiddl // Cache system message (providerOptions on message object) if (cacheSystemMessage) { for (let i = 0; i < messages.length; i++) { - const msg = messages[i] as any + const msg = messages[i] as LanguageModelV2Message if (msg.role === 'system' && estimateContentTokens(msg.content) >= tokenThreshold) { messages[i] = { ...msg, providerOptions: cacheProviderOptions } break @@ -64,12 +50,32 @@ export function anthropicCacheMiddleware(provider: Provider): LanguageModelMiddl // Cache last N non-system messages (providerOptions on content parts) if (cacheLastNMessages > 0) { + const cumsumTokens = [] as Array + let tokenSum = 0 as number + for (let i = 0; i < messages.length; i++) { + const msg = messages[i] as LanguageModelV2Message + tokenSum += estimateContentTokens(msg.content) + cumsumTokens.push(tokenSum) + } + for (let i = messages.length - 1; i >= 0 && cachedCount < cacheLastNMessages; i--) { - const msg = messages[i] as any - if (msg.role !== 'system' && estimateContentTokens(msg.content) >= tokenThreshold) { - messages[i] = { ...msg, content: addCacheToContentParts(msg.content) } - cachedCount++ + const msg = messages[i] as LanguageModelV2Message + if (msg.role === 'system' || cumsumTokens[i] < tokenThreshold || msg.content.length === 0) { + continue } + + const newContent = [...msg.content] + const lastIndex = newContent.length - 1 + newContent[lastIndex] = { + ...newContent[lastIndex], + providerOptions: cacheProviderOptions + } + + messages[i] = { + ...msg, + content: newContent + } as LanguageModelV2Message + cachedCount++ } } From 9414f13f6da5240c56e9e78e2ecaf599383cf88d Mon Sep 17 00:00:00 2001 From: SuYao Date: Mon, 12 Jan 2026 16:38:55 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=BD=93=E5=AD=97=E6=AE=B5=E5=90=8D=20(#12430)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/aiCore/provider/providerConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 54915795a8..bdee5fc75e 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -325,9 +325,9 @@ function createDeveloperToSystemFetch(originalFetch?: typeof fetch): typeof fetc if (options?.body && typeof options.body === 'string') { try { const body = JSON.parse(options.body) - if (body.messages && Array.isArray(body.messages)) { + if (body.input && Array.isArray(body.input)) { let hasChanges = false - body.messages = body.messages.map((msg: { role: string }) => { + body.input = body.input.map((msg: { role: string }) => { if (msg.role === 'developer') { hasChanges = true return { ...msg, role: 'system' } From ac23c7f30bee65449dc2df66d6ba64d1f093faec Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 14 Jan 2026 14:23:19 +0800 Subject: [PATCH 05/10] fix(aiCore): preserve conversation history for image enhancement models (#12239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(aiCore): preserve conversation history for image enhancement models - Changed image enhancement model handling to preserve full conversation context - Only merge images from previous assistant message into last user message - Return messages as-is when no images need to be merged - Added test case for LLM-to-image-model switching scenario This allows users to switch from LLM conversations to image generation models while keeping the conversation context for guiding image generation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * chore: format --------- Co-authored-by: Claude --- .../__tests__/message-converter.test.ts | 135 +++++++++++++++++- .../aiCore/prepareParams/messageConverter.ts | 71 +++++---- 2 files changed, 162 insertions(+), 44 deletions(-) diff --git a/src/renderer/src/aiCore/prepareParams/__tests__/message-converter.test.ts b/src/renderer/src/aiCore/prepareParams/__tests__/message-converter.test.ts index 2a69f3bcef..4f76cbee7b 100644 --- a/src/renderer/src/aiCore/prepareParams/__tests__/message-converter.test.ts +++ b/src/renderer/src/aiCore/prepareParams/__tests__/message-converter.test.ts @@ -263,7 +263,7 @@ describe('messageConverter', () => { }) describe('convertMessagesToSdkMessages', () => { - it('collapses to [system?, user(image)] for image enhancement models', async () => { + it('preserves conversation history and merges images for image enhancement models', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const initialUser = createMessage('user') initialUser.__mockContent = 'Start editing' @@ -277,7 +277,16 @@ describe('messageConverter', () => { const result = await convertMessagesToSdkMessages([initialUser, assistant, finalUser], model) + // Preserves all conversation history, only merges images into the last user message expect(result).toEqual([ + { + role: 'user', + content: [{ type: 'text', text: 'Start editing' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Here is the current preview' }] + }, { role: 'user', content: [ @@ -288,7 +297,7 @@ describe('messageConverter', () => { ]) }) - it('preserves system messages and collapses others for enhancement payloads', async () => { + it('preserves system messages and conversation history for enhancement payloads', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const fileUser = createMessage('user') fileUser.__mockContent = 'Use this document as inspiration' @@ -309,8 +318,17 @@ describe('messageConverter', () => { const result = await convertMessagesToSdkMessages([fileUser, assistant, finalUser], model) + // Preserves system message, conversation history, and merges images into the last user message expect(result).toEqual([ { role: 'system', content: 'fileid://reference' }, + { + role: 'user', + content: [{ type: 'text', text: 'Use this document as inspiration' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Generated previews ready' }] + }, { role: 'user', content: [ @@ -321,7 +339,7 @@ describe('messageConverter', () => { ]) }) - it('handles no previous assistant message with images', async () => { + it('returns messages as-is when no previous assistant message with images', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const user1 = createMessage('user') user1.__mockContent = 'Start' @@ -331,7 +349,12 @@ describe('messageConverter', () => { const result = await convertMessagesToSdkMessages([user1, user2], model) + // No images to merge, returns all messages as-is expect(result).toEqual([ + { + role: 'user', + content: [{ type: 'text', text: 'Start' }] + }, { role: 'user', content: [{ type: 'text', text: 'Continue without images' }] @@ -339,7 +362,7 @@ describe('messageConverter', () => { ]) }) - it('handles assistant message without images', async () => { + it('returns messages as-is when assistant message has no images', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const user1 = createMessage('user') user1.__mockContent = 'Start' @@ -353,7 +376,16 @@ describe('messageConverter', () => { const result = await convertMessagesToSdkMessages([user1, assistant, user2], model) + // No images to merge, returns all messages as-is expect(result).toEqual([ + { + role: 'user', + content: [{ type: 'text', text: 'Start' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Text only response' }] + }, { role: 'user', content: [{ type: 'text', text: 'Follow up' }] @@ -361,7 +393,7 @@ describe('messageConverter', () => { ]) }) - it('handles multiple assistant messages by using the most recent one', async () => { + it('merges images from the most recent assistant message', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const user1 = createMessage('user') user1.__mockContent = 'Start' @@ -382,7 +414,24 @@ describe('messageConverter', () => { const result = await convertMessagesToSdkMessages([user1, assistant1, user2, assistant2, user3], model) + // Preserves all history, merges only the most recent assistant's images expect(result).toEqual([ + { + role: 'user', + content: [{ type: 'text', text: 'Start' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'First response' }] + }, + { + role: 'user', + content: [{ type: 'text', text: 'Continue' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Second response' }] + }, { role: 'user', content: [ @@ -393,7 +442,7 @@ describe('messageConverter', () => { ]) }) - it('handles conversation ending with assistant message', async () => { + it('returns messages as-is when conversation ends with assistant message', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const user = createMessage('user') user.__mockContent = 'Start' @@ -406,15 +455,20 @@ describe('messageConverter', () => { // The user message is the last user message, but since the assistant comes after, // there's no "previous" assistant message (search starts from messages.length-2 backwards) + // So no images to merge, returns all messages as-is expect(result).toEqual([ { role: 'user', content: [{ type: 'text', text: 'Start' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Response with image' }] } ]) }) - it('handles empty content in last user message', async () => { + it('merges images even when last user message has empty content', async () => { const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) const user1 = createMessage('user') user1.__mockContent = 'Start' @@ -428,12 +482,79 @@ describe('messageConverter', () => { const result = await convertMessagesToSdkMessages([user1, assistant, user2], model) + // Preserves history, merges images into last user message (even if empty) expect(result).toEqual([ + { + role: 'user', + content: [{ type: 'text', text: 'Start' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Here is the preview' }] + }, { role: 'user', content: [{ type: 'image', image: 'https://example.com/preview.png' }] } ]) }) + + it('allows using LLM conversation context for image generation', async () => { + // This test verifies the key use case: switching from LLM to image enhancement model + // and using the previous conversation as context for image generation + const model = createModel({ id: 'qwen-image-edit', name: 'Qwen Image Edit', provider: 'qwen', group: 'qwen' }) + + // Simulate a conversation that started with a regular LLM + const user1 = createMessage('user') + user1.__mockContent = 'Help me design a futuristic robot with blue lights' + + const assistant1 = createMessage('assistant') + assistant1.__mockContent = + 'Great idea! The robot could have a sleek metallic body with glowing blue LED strips...' + assistant1.__mockImageBlocks = [] // LLM response, no images + + const user2 = createMessage('user') + user2.__mockContent = 'Yes, and add some chrome accents' + + const assistant2 = createMessage('assistant') + assistant2.__mockContent = 'Perfect! Chrome accents would complement the blue lights beautifully...' + assistant2.__mockImageBlocks = [] // Still LLM response, no images + + // User switches to image enhancement model and asks for image generation + const user3 = createMessage('user') + user3.__mockContent = 'Now generate an image based on our discussion' + + const result = await convertMessagesToSdkMessages([user1, assistant1, user2, assistant2, user3], model) + + // All conversation history should be preserved for context + // No images to merge since previous assistant had no images + expect(result).toEqual([ + { + role: 'user', + content: [{ type: 'text', text: 'Help me design a futuristic robot with blue lights' }] + }, + { + role: 'assistant', + content: [ + { + type: 'text', + text: 'Great idea! The robot could have a sleek metallic body with glowing blue LED strips...' + } + ] + }, + { + role: 'user', + content: [{ type: 'text', text: 'Yes, and add some chrome accents' }] + }, + { + role: 'assistant', + content: [{ type: 'text', text: 'Perfect! Chrome accents would complement the blue lights beautifully...' }] + }, + { + role: 'user', + content: [{ type: 'text', text: 'Now generate an image based on our discussion' }] + } + ]) + }) }) }) diff --git a/src/renderer/src/aiCore/prepareParams/messageConverter.ts b/src/renderer/src/aiCore/prepareParams/messageConverter.ts index 56c5f6a4e7..eba16c6619 100644 --- a/src/renderer/src/aiCore/prepareParams/messageConverter.ts +++ b/src/renderer/src/aiCore/prepareParams/messageConverter.ts @@ -229,23 +229,15 @@ export async function convertMessagesToSdkMessages(messages: Message[], model: M sdkMessages.push(...(Array.isArray(sdkMessage) ? sdkMessage : [sdkMessage])) } // Special handling for image enhancement models - // Target behavior: Collapse the conversation into [system?, user(image)]. - // Explanation of why we don't simply use slice: - // 1) We need to preserve all system messages: During the convertMessageToSdkParam process, native file uploads may insert `system(fileid://...)`. - // Directly slicing the original messages or already converted sdkMessages could easily result in missing these system instructions. - // Therefore, we first perform a full conversion and then aggregate the system messages afterward. - // 2) The conversion process may split messages: A single user message might be broken into two SDK messages—[system, user]. - // Slicing either side could lead to obtaining semantically incorrect fragments (e.g., only the split-out system message). - // 3) The “previous assistant message” is not necessarily the second-to-last one: There might be system messages or other message blocks inserted in between, - // making a simple slice(-2) assumption too rigid. Here, we trace back from the end of the original messages to locate the most recent assistant message, which better aligns with business semantics. - // 4) This is a “collapse” rather than a simple “slice”: Ultimately, we need to synthesize a new user message - // (with text from the last user message and images from the previous assistant message). Using slice can only extract subarrays, - // which still require reassembly; constructing directly according to the target structure is clearer and more reliable. + // These models support multi-turn conversations but need images from previous assistant messages + // to be merged into the current user message for editing/enhancement operations. + // + // Key behaviors: + // 1. Preserve all conversation history for context + // 2. Find images from the previous assistant message and merge them into the last user message + // 3. This allows users to switch from LLM conversations and use that context for image generation if (isImageEnhancementModel(model)) { - // Collect all system messages (including ones generated from file uploads) - const systemMessages = sdkMessages.filter((m): m is SystemModelMessage => m.role === 'system') - - // Find the last user message (SDK converted) + // Find the last user SDK message index const lastUserSdkIndex = (() => { for (let i = sdkMessages.length - 1; i >= 0; i--) { if (sdkMessages[i].role === 'user') return i @@ -253,7 +245,10 @@ export async function convertMessagesToSdkMessages(messages: Message[], model: M return -1 })() - const lastUserSdk = lastUserSdkIndex >= 0 ? (sdkMessages[lastUserSdkIndex] as UserModelMessage) : null + // If no user message found, return messages as-is + if (lastUserSdkIndex < 0) { + return sdkMessages + } // Find the nearest preceding assistant message in original messages let prevAssistant: Message | null = null @@ -264,31 +259,33 @@ export async function convertMessagesToSdkMessages(messages: Message[], model: M } } - // Build the final user content parts + // Check if there are images from the previous assistant message + const imageBlocks = prevAssistant ? findImageBlocks(prevAssistant) : [] + const imageParts = await convertImageBlockToImagePart(imageBlocks) + + // If no images to merge, return messages as-is + if (imageParts.length === 0) { + return sdkMessages + } + + // Build the new last user message with merged images + const lastUserSdk = sdkMessages[lastUserSdkIndex] as UserModelMessage let finalUserParts: Array = [] - if (lastUserSdk) { - if (typeof lastUserSdk.content === 'string') { - finalUserParts.push({ type: 'text', text: lastUserSdk.content }) - } else if (Array.isArray(lastUserSdk.content)) { - finalUserParts = [...lastUserSdk.content] - } + + if (typeof lastUserSdk.content === 'string') { + finalUserParts.push({ type: 'text', text: lastUserSdk.content }) + } else if (Array.isArray(lastUserSdk.content)) { + finalUserParts = [...lastUserSdk.content] } - // Append images from the previous assistant message if any - if (prevAssistant) { - const imageBlocks = findImageBlocks(prevAssistant) - const imageParts = await convertImageBlockToImagePart(imageBlocks) - if (imageParts.length > 0) { - finalUserParts.push(...imageParts) - } - } + // Append images from the previous assistant message + finalUserParts.push(...imageParts) - // If we couldn't find a last user message, fall back to returning collected system messages only - if (!lastUserSdk) { - return systemMessages - } + // Replace the last user message with the merged version + const result = [...sdkMessages] + result[lastUserSdkIndex] = { role: 'user', content: finalUserParts } - return [...systemMessages, { role: 'user', content: finalUserParts }] + return result } return sdkMessages From a844b5bf393de1ac923275515e8d927a2e11bf0c Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Wed, 14 Jan 2026 15:41:35 +0800 Subject: [PATCH 06/10] chore: remove generate:icons script from package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 048ec616ad..87adab9d79 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "agents:push": "NODE_ENV='development' drizzle-kit push --config src/main/services/agents/drizzle.config.ts", "agents:studio": "NODE_ENV='development' drizzle-kit studio --config src/main/services/agents/drizzle.config.ts", "agents:drop": "NODE_ENV='development' drizzle-kit drop --config src/main/services/agents/drizzle.config.ts", - "generate:icons": "electron-icon-builder --input=./build/logo.png --output=build", "analyze:renderer": "VISUALIZER_RENDERER=true pnpm build", "analyze:main": "VISUALIZER_MAIN=true pnpm build", "typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"", From 262d32ac17ce456899f13ae9df88474c9523fba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A7=91=E5=9B=BF=E8=84=91=E8=A2=8B?= <70054568+eeee0717@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:52:55 +0800 Subject: [PATCH 07/10] fix: remove mineru built-in api key (#12455) --- .../preprocess/MineruPreprocessProvider.ts | 10 +++- .../src/hooks/useKnowledgeBaseForm.ts | 2 +- src/renderer/src/i18n/locales/en-us.json | 3 -- src/renderer/src/i18n/locales/zh-cn.json | 3 -- src/renderer/src/i18n/locales/zh-tw.json | 3 -- src/renderer/src/i18n/translate/de-de.json | 3 -- src/renderer/src/i18n/translate/el-gr.json | 3 -- src/renderer/src/i18n/translate/es-es.json | 3 -- src/renderer/src/i18n/translate/fr-fr.json | 3 -- src/renderer/src/i18n/translate/ja-jp.json | 3 -- src/renderer/src/i18n/translate/pt-pt.json | 3 -- src/renderer/src/i18n/translate/ro-ro.json | 3 -- src/renderer/src/i18n/translate/ru-ru.json | 3 -- .../pages/knowledge/components/QuotaTag.tsx | 52 ++++++++----------- .../PreprocessProviderSettings.tsx | 4 +- 15 files changed, 33 insertions(+), 68 deletions(-) diff --git a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts index 80aec40622..6def76f346 100644 --- a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts +++ b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts @@ -56,8 +56,6 @@ type QuotaResponse = { export default class MineruPreprocessProvider extends BasePreprocessProvider { constructor(provider: PreprocessProvider, userId?: string) { super(provider, userId) - // TODO: remove after free period ends - this.provider.apiKey = this.provider.apiKey || import.meta.env.MAIN_VITE_MINERU_API_KEY } public async parseFile( @@ -65,6 +63,10 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider { file: FileMetadata ): Promise<{ processedFile: FileMetadata; quota: number }> { try { + if (!this.provider.apiKey) { + throw new Error('MinerU API key is required') + } + const filePath = fileStorage.getFilePathById(file) logger.info(`MinerU preprocess processing started: ${filePath}`) await this.validateFile(filePath) @@ -96,6 +98,10 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider { public async checkQuota() { try { + if (!this.provider.apiKey) { + throw new Error('MinerU API key is required') + } + const quota = await net.fetch(`${this.provider.apiHost}/api/v4/quota`, { method: 'GET', headers: { diff --git a/src/renderer/src/hooks/useKnowledgeBaseForm.ts b/src/renderer/src/hooks/useKnowledgeBaseForm.ts index a42fc90fa0..da7bf331f5 100644 --- a/src/renderer/src/hooks/useKnowledgeBaseForm.ts +++ b/src/renderer/src/hooks/useKnowledgeBaseForm.ts @@ -57,7 +57,7 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => { label: t('settings.tool.preprocess.provider'), title: t('settings.tool.preprocess.provider'), options: preprocessProviders - .filter((p) => p.apiKey !== '' || ['mineru', 'open-mineru'].includes(p.id)) + .filter((p) => p.apiKey !== '' || ['open-mineru'].includes(p.id)) .map((p) => ({ value: p.id, label: p.name })) } return [preprocessOptions] diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 45a2222930..bc4db909e2 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4219,9 +4219,6 @@ "title": "Message Settings", "use_serif_font": "Use serif font" }, - "mineru": { - "api_key": "Mineru now offers a daily free quota of 500 pages, and you do not need to enter a key." - }, "miniapps": { "cache_change_notice": "Changes will take effect when the number of open mini apps reaches the set value", "cache_description": "Set the maximum number of active mini apps to keep in memory", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 220a109a74..affea9f135 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4219,9 +4219,6 @@ "title": "消息设置", "use_serif_font": "使用衬线字体" }, - "mineru": { - "api_key": "MinerU现在提供每日500页的免费额度,您不需要填写密钥。" - }, "miniapps": { "cache_change_notice": "更改将在打开的小程序增减至设定值后生效", "cache_description": "设置同时保持活跃状态的小程序最大数量", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b6955adeca..1895012bf4 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4219,9 +4219,6 @@ "title": "訊息設定", "use_serif_font": "使用襯線字型" }, - "mineru": { - "api_key": "Mineru 現在每天提供 500 頁的免費配額,且無需輸入金鑰。" - }, "miniapps": { "cache_change_notice": "變更會在開啟的小程式數量調整至設定值後生效", "cache_description": "設定同時保持活躍狀態的小程式最大數量", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 975d43835d..a83e77355d 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -4219,9 +4219,6 @@ "title": "Nachrichteneinstellungen", "use_serif_font": "Serifenschrift verwenden" }, - "mineru": { - "api_key": "MinerU bietet täglich 500 Seiten kostenlos an, Sie müssen keinen Schlüssel eingeben." - }, "miniapps": { "cache_change_notice": "Änderung wird wirksam wenn Anzahl geöffneter Mini-Apps auf festgelegten Wert angepasst wird", "cache_description": "Maximale Anzahl gleichzeitig aktiver Mini-Apps festlegen", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 1169482dde..59ffacaba5 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -4219,9 +4219,6 @@ "title": "Ρυθμίσεις μηνυμάτων", "use_serif_font": "Χρήση μορφής Serif" }, - "mineru": { - "api_key": "Το MinerU παρέχει δωρεάν χρήση 500 σελίδων ημερησίως, δεν χρειάζεται να συμπληρώσετε κλειδί." - }, "miniapps": { "cache_change_notice": "Η αλλαγή θα τεθεί σε ισχύ αφού το πλήθος των ανοιχτών μικροπρογραμμάτων φτάσει τη ρυθμισμένη τιμή", "cache_description": "Ορίστε τον μέγιστο αριθμό των μικροπρογραμμάτων που μπορούν να είναι ενεργά ταυτόχρονα", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 0e27518f33..5fb108dabe 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -4219,9 +4219,6 @@ "title": "Configuración de mensajes", "use_serif_font": "Usar fuente serif" }, - "mineru": { - "api_key": "MinerU ahora ofrece un cupo gratuito de 500 páginas diarias, no es necesario que ingrese una clave." - }, "miniapps": { "cache_change_notice": "Los cambios surtirán efecto cuando el número de miniaplicaciones abiertas aumente o disminuya hasta alcanzar el valor configurado", "cache_description": "Establece el número máximo de miniaplicaciones que pueden permanecer activas simultáneamente", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index a1b708d0b1..edd84c795d 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -4219,9 +4219,6 @@ "title": "Paramètres des messages", "use_serif_font": "Utiliser une police serif" }, - "mineru": { - "api_key": "MinerU propose désormais un quota gratuit de 500 pages par jour, vous n'avez donc pas besoin de saisir de clé." - }, "miniapps": { "cache_change_notice": "Les modifications prendront effet après l'ajout ou la suppression d'applications ouvertes jusqu'à atteindre la valeur définie", "cache_description": "Définir le nombre maximum d'applications pouvant rester actives simultanément", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 423084f5c4..4bc41a9750 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -4219,9 +4219,6 @@ "title": "メッセージ設定", "use_serif_font": "セリフフォントを使用" }, - "mineru": { - "api_key": "Mineruでは現在、1日500ページの無料クォータを提供しており、キーを入力する必要はありません。" - }, "miniapps": { "cache_change_notice": "設定値に達するまでミニアプリの開閉が行われた後に変更が適用されます", "cache_description": "メモリに保持するアクティブなミニアプリの最大数を設定します", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 035bc92753..12a7585cb1 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -4219,9 +4219,6 @@ "title": "Configurações de mensagem", "use_serif_font": "Usar fonte serif" }, - "mineru": { - "api_key": "O MinerU agora oferece uma cota diária gratuita de 500 páginas; você não precisa preencher uma chave." - }, "miniapps": { "cache_change_notice": "As alterações entrarão em vigor após a abertura ou remoção dos mini aplicativos até atingir o número definido", "cache_description": "Defina o número máximo de mini aplicativos que permanecerão ativos simultaneamente", diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json index 58f07d67c5..868017acd7 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -4219,9 +4219,6 @@ "title": "Setări mesaje", "use_serif_font": "Folosește font serif" }, - "mineru": { - "api_key": "Mineru oferă acum o cotă zilnică gratuită de 500 de pagini și nu este nevoie să introduci o cheie." - }, "miniapps": { "cache_change_notice": "Modificările vor intra în vigoare când numărul de mini-aplicații deschise atinge valoarea setată", "cache_description": "Setează numărul maxim de mini-aplicații active de păstrat în memorie", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index e1c7135834..2797da9572 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -4219,9 +4219,6 @@ "title": "Настройки сообщений", "use_serif_font": "Использовать serif шрифт" }, - "mineru": { - "api_key": "Mineru теперь предлагает ежедневную бесплатную квоту в 500 страниц, и вам не нужно вводить ключ." - }, "miniapps": { "cache_change_notice": "Изменения вступят в силу, когда количество открытых мини-приложений достигнет установленного значения", "cache_description": "Установить максимальное количество активных мини-приложений в памяти", diff --git a/src/renderer/src/pages/knowledge/components/QuotaTag.tsx b/src/renderer/src/pages/knowledge/components/QuotaTag.tsx index e7f99a8037..0791c68539 100644 --- a/src/renderer/src/pages/knowledge/components/QuotaTag.tsx +++ b/src/renderer/src/pages/knowledge/components/QuotaTag.tsx @@ -10,8 +10,6 @@ import { useTranslation } from 'react-i18next' const logger = loggerService.withContext('QuotaTag') -const QUOTA_UNLIMITED = -9999 - const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quota?: number }> = ({ base, providerId, @@ -23,44 +21,40 @@ const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quot useEffect(() => { const checkQuota = async () => { - if (provider.id !== 'mineru') return - // 使用用户的key时quota为无限 - if (provider.apiKey) { - setQuota(QUOTA_UNLIMITED) - updateProvider({ quota: QUOTA_UNLIMITED }) - return + const userId = getStoreSetting('userId') + const baseParams = getKnowledgeBaseParams(base) + try { + const response = await window.api.knowledgeBase.checkQuota({ + base: baseParams, + userId: userId as string + }) + setQuota(response) + updateProvider({ quota: response }) + } catch (error) { + logger.error('[KnowledgeContent] Error checking quota:', error as Error) } - if (quota === undefined) { - const userId = getStoreSetting('userId') - const baseParams = getKnowledgeBaseParams(base) - try { - const response = await window.api.knowledgeBase.checkQuota({ - base: baseParams, - userId: userId as string - }) - setQuota(response) - } catch (error) { - logger.error('[KnowledgeContent] Error checking quota:', error as Error) - } + } + + if (provider.id !== 'mineru') return + if (!provider.apiKey) { + if (quota !== undefined) { + setQuota(undefined) + updateProvider({ quota: undefined }) } + return } if (_quota !== undefined) { setQuota(_quota) updateProvider({ quota: _quota }) return } - checkQuota() - }, [_quota, base, provider.id, provider.apiKey, provider, quota, updateProvider]) + if (quota === undefined) { + checkQuota() + } + }, [_quota, base, provider.id, provider.apiKey, quota, updateProvider]) const getQuotaDisplay = () => { if (quota === undefined) return null - if (quota === QUOTA_UNLIMITED) { - return ( - - {t('knowledge.quota_infinity', { name: provider.name })} - - ) - } if (quota === 0) { return ( diff --git a/src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx index 4d6df731f8..a568e9e0f8 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx @@ -99,9 +99,7 @@ const PreprocessProviderSettings: FC = ({ provider: _provider }) => { setApiKey(formatApiKeys(e.target.value))} onBlur={onUpdateApiKey} spellCheck={false} From c1b0a18fef795889693af83b701aa17315b17cc3 Mon Sep 17 00:00:00 2001 From: ZhuangYumin Date: Wed, 14 Jan 2026 17:01:33 +0800 Subject: [PATCH 08/10] fix: switch to new URL in qwen miniapp (#12460) --- src/renderer/src/config/minapps.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts index eeefb218d2..2653768979 100644 --- a/src/renderer/src/config/minapps.ts +++ b/src/renderer/src/config/minapps.ts @@ -145,7 +145,7 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [ { id: 'dashscope', name: i18n.t('minapps.qwen'), - url: 'https://www.tongyi.com/', + url: 'https://www.qianwen.com', logo: QwenModelLogo }, { @@ -328,9 +328,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [ }, { id: 'qwenlm', - name: 'QwenLM', + name: 'QwenChat', logo: QwenlmAppLogo, - url: 'https://qwenlm.ai/' + url: 'https://chat.qwen.ai' }, { id: 'flowith', From 30bcd8f8010f5a29d07201004554dfd9079c3e74 Mon Sep 17 00:00:00 2001 From: Nicolae Fericitu <118419291+NicolaeFericitu@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:05:42 +0200 Subject: [PATCH 09/10] feat(i18n): Update Romanian localization (ro-RO) (#12438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(i18n): update and refine Romanian translation I have corrected several typos and refined the terminology in the ro-ro.json file for better linguistic accuracy. This update ensures translation consistency throughout the user interface. * i18n: Update and fix Romanian localization (ro-RO) The Romanian localization file has been updated. Necessary corrections have been applied to address issues identified during an interface review, ensuring consistent terminology and improved message clarity. * i18n: Capitalize "Users" label for UI consistency Updated the "users" key in ro-ro.json to use an uppercase initial. This ensures visual consistency with other menu items in the settings section (User Management). * Update Romanian localization (ro-RO) * fix(i18n): update topic time translation for clarity Updated settings.topic.show.time to "Afișează ora subiectului" to maintain context as requested by reviewer. --- src/renderer/src/i18n/translate/ro-ro.json | 48 +++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json index 868017acd7..950049a162 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -275,7 +275,7 @@ "denyRequest": "Refuză cererea instrumentului", "hideDetails": "Ascunde detaliile instrumentului", "runWithOptions": "Rulează cu opțiuni suplimentare", - "showDetails": "Arată detaliile instrumentului" + "showDetails": "Afișează detaliile instrumentului" }, "button": { "cancel": "Anulează", @@ -401,7 +401,7 @@ "toggleDevTools": "Comută instrumentele pentru dezvoltatori", "toggleFullscreen": "Comută ecranul complet", "undo": "Anulează", - "unhide": "Arată toate", + "unhide": "Afișează toate", "view": "Vizualizare", "website": "Site web", "window": "Fereastră", @@ -649,7 +649,7 @@ "manage": "Gestionează", "select_model": "Selectează modelul", "show": { - "all": "Arată tot" + "all": "Afișează tot" }, "update_available": "Actualizare disponibilă", "whole_word": "Cuvânt întreg" @@ -975,7 +975,7 @@ }, "reset": "Resetează", "set_as_default": "Aplică la asistentul implicit", - "show_line_numbers": "Arată numerele de linie în cod", + "show_line_numbers": "Afișează numerele liniilor de cod", "temperature": { "label": "Temperatură", "tip": "Valorile mai mari fac modelul mai creativ și imprevizibil, în timp ce valorile mai mici îl fac mai determinist și precis." @@ -2246,7 +2246,7 @@ "found_results": "S-au găsit {{count}} rezultate (Nume: {{nameCount}}, Conținut: {{contentCount}})", "more_matches": "mai multe potriviri", "searching": "Se caută...", - "show_less": "Arată mai puțin" + "show_less": "Afișează mai puțin" }, "settings": { "data": { @@ -2297,7 +2297,7 @@ }, "title": "Notițe" }, - "show_starred": "Arată notițele favorite", + "show_starred": "Afișează notițele favorite", "sort_a2z": "Nume fișier (A-Z)", "sort_created_asc": "Ora creării (cele mai vechi întâi)", "sort_created_desc": "Ora creării (cele mai noi întâi)", @@ -2948,7 +2948,7 @@ "opacity": "Opacitate fereastră", "original_copy": "Copiază originalul", "original_hide": "Ascunde originalul", - "original_show": "Arată originalul", + "original_show": "Afișează originalul", "pin": "Fixează", "pinned": "Fixat", "r_regenerate": "R: Regenerează" @@ -3046,7 +3046,7 @@ "windows": "Unele aplicații nu acceptă selectarea textului cu tasta Ctrl. Dacă ai remapat tasta Ctrl folosind instrumente precum AHK, acest lucru poate cauza eșecul selecției textului în unele aplicații." }, "selected": "Selecție", - "selected_note": "Arată bara de instrumente imediat ce textul este selectat", + "selected_note": "Afișează bara de instrumente la selectarea textului", "shortcut": "Comandă rapidă", "shortcut_link": "Mergi la Setările comenzilor rapide", "shortcut_note": "După selecție, folosește comanda rapidă pentru a afișa bara de instrumente. Te rugăm să setezi comanda rapidă în pagina de setări și să o activezi. ", @@ -3441,7 +3441,7 @@ }, "show_model_provider": { "help": "Afișează furnizorul modelului (de ex., OpenAI, Gemini) la exportul în Markdown", - "title": "Arată furnizorul modelului" + "title": "Afișează furnizorul modelului" }, "standardize_citations": { "help": "Când este activat, marcatorii de citare vor fi convertiți în format standard de notă de subsol Markdown [^1], iar listele de citare vor fi formatate.", @@ -3826,22 +3826,22 @@ "disabled": "Ascunde pictograme", "empty": "Trage funcția ascunsă din partea stângă aici", "files": { - "icon": "Arată pictograma Fișiere" + "icon": "Afișează pictograma Fișiere" }, "knowledge": { - "icon": "Arată pictograma Cunoștințe" + "icon": "Afișează pictograma Cunoștințe" }, "minapp": { - "icon": "Arată pictograma MinApp" + "icon": "Afișează pictograma MinApp" }, "painting": { - "icon": "Arată pictograma Pictură" + "icon": "Afișează pictograma Imagini" }, "title": "Setări bară laterală", "translate": { - "icon": "Arată pictograma Traducere" + "icon": "Afișează pictograma Traducere" }, - "visible": "Arată pictograme" + "visible": "Afișează pictograme" }, "title": "Setări afișare", "topic": { @@ -3917,7 +3917,7 @@ "knowledge_base": "Golește bazele de cunoștințe selectate", "models": "Golește toate modelele" }, - "show_translate_confirm": "Arată dialogul de confirmare a traducerii", + "show_translate_confirm": "Afișează fereastra de confirmare a traducerii", "target_language": { "chinese": "Chineză simplificată", "chinese-traditional": "Chineză tradițională", @@ -4184,7 +4184,7 @@ }, "messages": { "divider": { - "label": "Arată divizor între mesaje", + "label": "Afișează divizor între mesaje", "tooltip": "Nu se aplică mesajelor stil bulă" }, "grid_columns": "Coloane afișare grilă mesaje", @@ -4200,7 +4200,7 @@ "paste_long_text_as_file": "Lipește text lung ca fișier", "paste_long_text_threshold": "Lungime lipire text lung", "send_shortcuts": "Comenzi rapide trimitere", - "show_estimated_tokens": "Arată tokeni estimați", + "show_estimated_tokens": "Afișează numărul estimat de tokeni", "title": "Setări intrare" }, "markdown_rendering_input_message": "Randare Markdown mesaj intrare", @@ -4214,8 +4214,8 @@ "label": "Bară navigare", "none": "Niciunul" }, - "prompt": "Arată prompt", - "show_message_outline": "Arată contur mesaj", + "prompt": "Afișează prompt", + "show_message_outline": "Afișează conturul mesajului", "title": "Setări mesaje", "use_serif_font": "Folosește font serif" }, @@ -4262,7 +4262,7 @@ "title": "Deschide în browser linkurile care deschid ferestre noi" }, "reset_tooltip": "Resetează la implicit", - "sidebar_description": "Arată mini-aplicațiile active în bara laterală", + "sidebar_description": "Afișează mini-aplicațiile active în bara laterală", "sidebar_title": "Afișare mini-aplicații active în bara laterală", "title": "Setări mini-aplicații", "visible": "Mini-aplicații vizibile" @@ -4718,7 +4718,7 @@ "search_message_in_chat": "Caută mesaj în chat-ul curent", "selection_assistant_select_text": "Asistent de selecție: Selectează text", "selection_assistant_toggle": "Comută Asistentul de selecție", - "show_app": "Arată/Ascunde aplicația", + "show_app": "Afișează/Ascunde aplicația", "show_settings": "Deschide setările", "title": "Comenzi rapide de la tastatură", "toggle_new_context": "Șterge contextul", @@ -4887,7 +4887,7 @@ "right": "Dreapta" }, "show": { - "time": "Arată ora subiectului" + "time": "Afișează ora subiectului" } }, "translate": { @@ -4937,7 +4937,7 @@ }, "tray": { "onclose": "Minimizează în zona de notificare la închidere", - "show": "Arată pictograma în zona de notificare", + "show": "Afișează pictograma în zona de notificare", "title": "Zonă de notificare" }, "zoom": { From 0880435c069139e6ec1e977b703126f8787a94a3 Mon Sep 17 00:00:00 2001 From: pippo Date: Thu, 15 Jan 2026 21:26:51 +0800 Subject: [PATCH 10/10] feat(baichuan):add baichuan-m3 models (#12478) Co-authored-by: roberto --- src/renderer/src/config/models/__tests__/reasoning.test.ts | 4 +++- src/renderer/src/config/models/default.ts | 6 ++++++ src/renderer/src/config/models/reasoning.ts | 7 ++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/config/models/__tests__/reasoning.test.ts b/src/renderer/src/config/models/__tests__/reasoning.test.ts index 5173eed9f0..5ca350d6fa 100644 --- a/src/renderer/src/config/models/__tests__/reasoning.test.ts +++ b/src/renderer/src/config/models/__tests__/reasoning.test.ts @@ -1370,7 +1370,9 @@ describe('findTokenLimit', () => { { modelId: 'qwen-flash-lite', expected: { min: 0, max: 81_920 } }, { modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } }, { modelId: 'Baichuan-M2', expected: { min: 0, max: 30_000 } }, - { modelId: 'baichuan-m2', expected: { min: 0, max: 30_000 } } + { modelId: 'baichuan-m2', expected: { min: 0, max: 30_000 } }, + { modelId: 'Baichuan-M3', expected: { min: 0, max: 30_000 } }, + { modelId: 'baichuan-m3', expected: { min: 0, max: 30_000 } } ] it.each(cases)('returns correct limits for $modelId', ({ modelId, expected }) => { diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index 56873285c8..87c2caa457 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -737,6 +737,12 @@ export const SYSTEM_MODELS: Record = provider: 'baichuan', name: 'Baichuan M2 Plus', group: 'Baichuan-M2' + }, + { + id: 'Baichuan-M3', + provider: 'baichuan', + name: 'Baichuan M3', + group: 'Baichuan-M3' } ], modelscope: [ diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 0b42ed0934..93b57c49ba 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -646,8 +646,8 @@ export const isBaichuanReasoningModel = (model?: Model): boolean => { } const modelId = getLowerBaseModelName(model.id, '/') - // 只有 Baichuan-M2 是推理模型(注意:M2-Plus 不是推理模型) - return modelId.includes('baichuan-m2') && !modelId.includes('plus') + // Baichuan-M2 和 Baichuan-M3 是推理模型(注意:M2-Plus 不是推理模型) + return (modelId.includes('baichuan-m2') && !modelId.includes('plus')) || modelId.includes('baichuan-m3') } export function isReasoningModel(model?: Model): boolean { @@ -732,7 +732,8 @@ const THINKING_TOKEN_MAP: Record = { }, // Baichuan models - 'baichuan-m2$': { min: 0, max: 30_000 } + 'baichuan-m2$': { min: 0, max: 30_000 }, + 'baichuan-m3$': { min: 0, max: 30_000 } } export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {