From bf35228b4987be6711992d93a6537e6ec0228c95 Mon Sep 17 00:00:00 2001 From: Zhaokun Date: Tue, 21 Oct 2025 10:36:53 +0800 Subject: [PATCH 1/2] fix: capture detailed error response body for reranker API failures (#10839) * fix: capture detailed error response body for reranker API failures Previously, when reranker API returned 400 or other error status codes, only the HTTP status and status text were captured, without reading the actual error response body that contains detailed error information. This commit fixes the issue by: - Reading the error response body (as JSON or text) before throwing error - Attaching the response details to the error object - Including responseBody in formatErrorMessage output This will help diagnose issues like "qwen3-reranker not available" by showing the actual error message from the API provider. * fix: enhance error handling in GeneralReranker for API failures This update improves the error handling in the GeneralReranker class by ensuring that the response body is properly cloned and read when an API call fails. The detailed error information, including the status, status text, and body, is now attached to the error object. This change aids in diagnosing issues by providing more context in error messages. --- src/main/knowledge/reranker/BaseReranker.ts | 1 + .../knowledge/reranker/GeneralReranker.ts | 34 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/knowledge/reranker/BaseReranker.ts b/src/main/knowledge/reranker/BaseReranker.ts index 9483cb3d4e..1e321e2d86 100644 --- a/src/main/knowledge/reranker/BaseReranker.ts +++ b/src/main/knowledge/reranker/BaseReranker.ts @@ -80,6 +80,7 @@ export default abstract class BaseReranker { message: error.message, status: error.response?.status, statusText: error.response?.statusText, + responseBody: error.response?.body, // Include the actual API error response requestBody: requestBody } return JSON.stringify(errorDetails, null, 2) diff --git a/src/main/knowledge/reranker/GeneralReranker.ts b/src/main/knowledge/reranker/GeneralReranker.ts index e4b3503606..e3ac5e8c21 100644 --- a/src/main/knowledge/reranker/GeneralReranker.ts +++ b/src/main/knowledge/reranker/GeneralReranker.ts @@ -2,6 +2,15 @@ import { KnowledgeBaseParams, KnowledgeSearchResult } from '@types' import { net } from 'electron' import BaseReranker from './BaseReranker' + +interface RerankError extends Error { + response?: { + status: number + statusText: string + body?: unknown + } +} + export default class GeneralReranker extends BaseReranker { constructor(base: KnowledgeBaseParams) { super(base) @@ -17,7 +26,30 @@ export default class GeneralReranker extends BaseReranker { }) if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) + // Read the response body to get detailed error information + // Clone the response to avoid consuming the body multiple times + const clonedResponse = response.clone() + let errorBody: unknown + + try { + errorBody = await clonedResponse.json() + } catch { + // If response body is not JSON, try to read as text + try { + errorBody = await response.text() + } catch { + errorBody = null + } + } + + const error = new Error(`HTTP ${response.status}: ${response.statusText}`) as RerankError + // Attach response details to the error object for formatErrorMessage + error.response = { + status: response.status, + statusText: response.statusText, + body: errorBody + } + throw error } const data = await response.json() From a5049d88720709a798b5fc3337ee4660e24c0dfe Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Tue, 21 Oct 2025 10:39:16 +0800 Subject: [PATCH 2/2] feat: enhance proxy bypass rules with comprehensive matching (#10817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: enhance proxy bypass rules with comprehensive matching - Add support for wildcard domains (*.example.com, .example.com) - Add CIDR notation support for IPv4 and IPv6 (192.168.0.0/16, 2001:db8::/32) - Add wildcard IP matching (192.168.1.*) - Add keyword for local network hostnames - Support both semicolon and comma separators in bypass rules - Add comprehensive unit tests with 22 test cases - Export matchWildcardDomain and matchIpRule for testability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * move to devDeps * delete logs * feat: enhance ProxyManager with advanced proxy bypass rule handling - Introduced comprehensive parsing and matching for proxy bypass rules, including support for wildcard domains, CIDR notation, and local network addresses. - Refactored existing functions and added new utility methods for improved clarity and maintainability. - Updated unit tests to cover new functionality and ensure robust validation of bypass rules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * update proxy rules * fix lint * add tips * delete hostname rule * add logs --------- Co-authored-by: Claude --- package.json | 1 + src/main/services/ProxyManager.ts | 309 ++++++++++++++++-- .../services/__tests__/ProxyManager.test.ts | 86 +++++ 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/el-gr.json | 13 +- src/renderer/src/i18n/translate/es-es.json | 13 +- src/renderer/src/i18n/translate/fr-fr.json | 13 +- src/renderer/src/i18n/translate/ja-jp.json | 13 +- src/renderer/src/i18n/translate/pt-pt.json | 13 +- src/renderer/src/i18n/translate/ru-ru.json | 13 +- .../src/pages/settings/GeneralSettings.tsx | 7 +- yarn.lock | 8 + 14 files changed, 466 insertions(+), 32 deletions(-) create mode 100644 src/main/services/__tests__/ProxyManager.test.ts diff --git a/package.json b/package.json index 7c937cc3b3..0b8c5971a8 100644 --- a/package.json +++ b/package.json @@ -280,6 +280,7 @@ "husky": "^9.1.7", "i18next": "^23.11.5", "iconv-lite": "^0.6.3", + "ipaddr.js": "^2.2.0", "isbinaryfile": "5.0.4", "jaison": "^2.0.2", "jest-styled-components": "^7.2.0", diff --git a/src/main/services/ProxyManager.ts b/src/main/services/ProxyManager.ts index 0e188c1009..6fcf20da1c 100644 --- a/src/main/services/ProxyManager.ts +++ b/src/main/services/ProxyManager.ts @@ -4,6 +4,7 @@ import { app, ProxyConfig, session } from 'electron' import { socksDispatcher } from 'fetch-socks' import http from 'http' import https from 'https' +import * as ipaddr from 'ipaddr.js' import { getSystemProxy } from 'os-proxy-config' import { ProxyAgent } from 'proxy-agent' import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici' @@ -11,41 +12,293 @@ import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher const logger = loggerService.withContext('ProxyManager') let byPassRules: string[] = [] -const isByPass = (url: string) => { - if (byPassRules.length === 0) { +type HostnameMatchType = 'exact' | 'wildcardSubdomain' | 'generalWildcard' + +const enum ProxyBypassRuleType { + Local = 'local', + Cidr = 'cidr', + Ip = 'ip', + Domain = 'domain' +} + +interface ParsedProxyBypassRule { + type: ProxyBypassRuleType + matchType: HostnameMatchType + rule: string + scheme?: string + port?: string + domain?: string + regex?: RegExp + cidr?: [ipaddr.IPv4 | ipaddr.IPv6, number] + ip?: string +} + +let parsedByPassRules: ParsedProxyBypassRule[] = [] + +const getDefaultPortForProtocol = (protocol: string): string | null => { + switch (protocol.toLowerCase()) { + case 'http:': + return '80' + case 'https:': + return '443' + default: + return null + } +} + +const buildWildcardRegex = (pattern: string): RegExp => { + const escapedSegments = pattern.split('*').map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + return new RegExp(`^${escapedSegments.join('.*')}$`, 'i') +} + +const isWildcardIp = (value: string): boolean => { + if (!value.includes('*')) { + return false + } + const replaced = value.replace(/\*/g, '0') + return ipaddr.isValid(replaced) +} + +const matchHostnameRule = (hostname: string, rule: ParsedProxyBypassRule): boolean => { + const normalizedHostname = hostname.toLowerCase() + + switch (rule.matchType) { + case 'exact': + return normalizedHostname === rule.domain + case 'wildcardSubdomain': { + const domain = rule.domain + if (!domain) { + return false + } + return normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`) + } + case 'generalWildcard': + return rule.regex ? rule.regex.test(normalizedHostname) : false + default: + return false + } +} + +const parseProxyBypassRule = (rule: string): ParsedProxyBypassRule | null => { + const trimmedRule = rule.trim() + if (!trimmedRule) { + return null + } + + if (trimmedRule === '') { + return { + type: ProxyBypassRuleType.Local, + matchType: 'exact', + rule: '' + } + } + + let workingRule = trimmedRule + let scheme: string | undefined + const schemeMatch = workingRule.match(/^([a-zA-Z][a-zA-Z\d+\-.]*):\/\//) + if (schemeMatch) { + scheme = schemeMatch[1].toLowerCase() + workingRule = workingRule.slice(schemeMatch[0].length) + } + + // CIDR notation must be processed before port extraction + if (workingRule.includes('/')) { + const cleanedCidr = workingRule.replace(/^\[|\]$/g, '') + if (ipaddr.isValidCIDR(cleanedCidr)) { + return { + type: ProxyBypassRuleType.Cidr, + matchType: 'exact', + rule: workingRule, + scheme, + cidr: ipaddr.parseCIDR(cleanedCidr) + } + } + } + + // Extract port: supports "host:port" and "[ipv6]:port" formats + let port: string | undefined + const portMatch = workingRule.match(/^(.+?):(\d+)$/) + if (portMatch) { + // For IPv6, ensure we're not splitting inside the brackets + const potentialHost = portMatch[1] + if (!potentialHost.startsWith('[') || potentialHost.includes(']')) { + workingRule = potentialHost + port = portMatch[2] + } + } + + const cleanedHost = workingRule.replace(/^\[|\]$/g, '') + const normalizedHost = cleanedHost.toLowerCase() + + if (!cleanedHost) { + return null + } + + if (ipaddr.isValid(cleanedHost)) { + return { + type: ProxyBypassRuleType.Ip, + matchType: 'exact', + rule: cleanedHost, + scheme, + port, + ip: cleanedHost + } + } + + if (isWildcardIp(cleanedHost)) { + const regexPattern = cleanedHost.replace(/\./g, '\\.').replace(/\*/g, '\\d+') + return { + type: ProxyBypassRuleType.Ip, + matchType: 'generalWildcard', + rule: cleanedHost, + scheme, + port, + regex: new RegExp(`^${regexPattern}$`) + } + } + + if (workingRule.startsWith('*.')) { + const domain = normalizedHost.slice(2) + return { + type: ProxyBypassRuleType.Domain, + matchType: 'wildcardSubdomain', + rule: workingRule, + scheme, + port, + domain + } + } + + if (workingRule.startsWith('.')) { + const domain = normalizedHost.slice(1) + return { + type: ProxyBypassRuleType.Domain, + matchType: 'wildcardSubdomain', + rule: workingRule, + scheme, + port, + domain + } + } + + if (workingRule.includes('*')) { + return { + type: ProxyBypassRuleType.Domain, + matchType: 'generalWildcard', + rule: workingRule, + scheme, + port, + regex: buildWildcardRegex(normalizedHost) + } + } + + return { + type: ProxyBypassRuleType.Domain, + matchType: 'exact', + rule: workingRule, + scheme, + port, + domain: normalizedHost + } +} + +const isLocalHostname = (hostname: string): boolean => { + const normalized = hostname.toLowerCase() + if (normalized === 'localhost') { + return true + } + + const cleaned = hostname.replace(/^\[|\]$/g, '') + if (ipaddr.isValid(cleaned)) { + const parsed = ipaddr.parse(cleaned) + return parsed.range() === 'loopback' + } + + return false +} + +export const updateByPassRules = (rules: string[]): void => { + byPassRules = rules + parsedByPassRules = [] + + for (const rule of rules) { + const parsedRule = parseProxyBypassRule(rule) + if (parsedRule) { + parsedByPassRules.push(parsedRule) + } else { + logger.warn(`Skipping invalid proxy bypass rule: ${rule}`) + } + } +} + +export const isByPass = (url: string) => { + if (parsedByPassRules.length === 0) { return false } try { - const subjectUrlTokens = new URL(url) - for (const rule of byPassRules) { - const ruleMatch = rule.replace(/^(?\.)/, '*').match(/^(?.+?)(?::(?\d+))?$/) + const parsedUrl = new URL(url) + const hostname = parsedUrl.hostname + const cleanedHostname = hostname.replace(/^\[|\]$/g, '') + const protocol = parsedUrl.protocol + const protocolName = protocol.replace(':', '').toLowerCase() + const defaultPort = getDefaultPortForProtocol(protocol) + const port = parsedUrl.port || defaultPort || '' + const hostnameIsIp = ipaddr.isValid(cleanedHostname) - if (!ruleMatch || !ruleMatch.groups) { - logger.warn('Failed to parse bypass rule:', { rule }) + for (const rule of parsedByPassRules) { + if (rule.scheme && rule.scheme !== protocolName) { continue } - if (!ruleMatch.groups.hostname) { + if (rule.port && rule.port !== port) { continue } - const hostnameIsMatch = subjectUrlTokens.hostname === ruleMatch.groups.hostname + switch (rule.type) { + case ProxyBypassRuleType.Local: + if (isLocalHostname(hostname)) { + return true + } + break + case ProxyBypassRuleType.Ip: + if (!hostnameIsIp) { + break + } - if ( - hostnameIsMatch && - (!ruleMatch.groups || - !ruleMatch.groups.port || - (subjectUrlTokens.port && subjectUrlTokens.port === ruleMatch.groups.port)) - ) { - return true + if (rule.ip && cleanedHostname === rule.ip) { + return true + } + + if (rule.regex && rule.regex.test(cleanedHostname)) { + return true + } + break + case ProxyBypassRuleType.Cidr: + if (hostnameIsIp && rule.cidr) { + const parsedHost = ipaddr.parse(cleanedHostname) + const [cidrAddress, prefixLength] = rule.cidr + // Ensure IP version matches before comparing + if (parsedHost.kind() === cidrAddress.kind() && parsedHost.match([cidrAddress, prefixLength])) { + return true + } + } + break + case ProxyBypassRuleType.Domain: + if (!hostnameIsIp && matchHostnameRule(hostname, rule)) { + return true + } + break + default: + logger.error(`Unknown proxy bypass rule type: ${rule.type}`) + break } } - return false } catch (error) { logger.error('Failed to check bypass:', error as Error) return false } + return false } class SelectiveDispatcher extends Dispatcher { private proxyDispatcher: Dispatcher @@ -154,19 +407,31 @@ export class ProxyManager { this.isSettingProxy = true try { - this.config = config this.clearSystemProxyMonitor() if (config.mode === 'system') { const currentProxy = await getSystemProxy() if (currentProxy) { - logger.info(`current system proxy: ${currentProxy.proxyUrl}`) - this.config.proxyRules = currentProxy.proxyUrl.toLowerCase() + logger.info(`current system proxy: ${currentProxy.proxyUrl}, bypass rules: ${currentProxy.noProxy.join(',')}`) + config.proxyRules = currentProxy.proxyUrl.toLowerCase() + config.proxyBypassRules = currentProxy.noProxy.join(',') } this.monitorSystemProxy() } - byPassRules = config.proxyBypassRules?.split(',') || [] - this.setGlobalProxy(this.config) + // Support both semicolon and comma as separators + if (config.proxyBypassRules !== this.config.proxyBypassRules) { + const rawRules = config.proxyBypassRules + ? config.proxyBypassRules + .split(/[;,]/) + .map((rule) => rule.trim()) + .filter((rule) => rule.length > 0) + : [] + + updateByPassRules(rawRules) + } + + this.setGlobalProxy(config) + this.config = config } catch (error) { logger.error('Failed to config proxy:', error as Error) throw error diff --git a/src/main/services/__tests__/ProxyManager.test.ts b/src/main/services/__tests__/ProxyManager.test.ts new file mode 100644 index 0000000000..b1d70f8705 --- /dev/null +++ b/src/main/services/__tests__/ProxyManager.test.ts @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { isByPass, updateByPassRules } from '../ProxyManager' + +describe('ProxyManager - bypass evaluation', () => { + beforeEach(() => { + updateByPassRules([]) + }) + + it('matches simple hostname patterns', () => { + updateByPassRules(['foobar.com']) + expect(isByPass('http://foobar.com')).toBe(true) + expect(isByPass('http://www.foobar.com')).toBe(false) + + updateByPassRules(['*.foobar.com']) + expect(isByPass('http://api.foobar.com')).toBe(true) + expect(isByPass('http://foobar.com')).toBe(true) + expect(isByPass('http://foobar.org')).toBe(false) + + updateByPassRules(['*foobar.com']) + expect(isByPass('http://devfoobar.com')).toBe(true) + expect(isByPass('http://foobar.com')).toBe(true) + expect(isByPass('http://foobar.company')).toBe(false) + }) + + it('matches hostname patterns with scheme and port qualifiers', () => { + updateByPassRules(['https://secure.example.com']) + expect(isByPass('https://secure.example.com')).toBe(true) + expect(isByPass('https://secure.example.com:443/home')).toBe(true) + expect(isByPass('http://secure.example.com')).toBe(false) + + updateByPassRules(['https://secure.example.com:8443']) + expect(isByPass('https://secure.example.com:8443')).toBe(true) + expect(isByPass('https://secure.example.com')).toBe(false) + expect(isByPass('https://secure.example.com:443')).toBe(false) + + updateByPassRules(['https://x.*.y.com:99']) + expect(isByPass('https://x.api.y.com:99')).toBe(true) + expect(isByPass('https://x.api.y.com')).toBe(false) + expect(isByPass('http://x.api.y.com:99')).toBe(false) + }) + + it('matches domain suffix patterns with leading dot', () => { + updateByPassRules(['.example.com']) + expect(isByPass('https://example.com')).toBe(true) + expect(isByPass('https://api.example.com')).toBe(true) + expect(isByPass('https://deep.api.example.com')).toBe(true) + expect(isByPass('https://example.org')).toBe(false) + + updateByPassRules(['.com']) + expect(isByPass('https://anything.com')).toBe(true) + expect(isByPass('https://example.org')).toBe(false) + + updateByPassRules(['http://.google.com']) + expect(isByPass('http://maps.google.com')).toBe(true) + expect(isByPass('https://maps.google.com')).toBe(false) + }) + + it('matches IP literals, CIDR ranges, and wildcard IPs', () => { + updateByPassRules(['127.0.0.1', '[::1]', '192.168.1.0/24', 'fefe:13::abc/33', '192.168.*.*']) + + expect(isByPass('http://127.0.0.1')).toBe(true) + expect(isByPass('http://[::1]')).toBe(true) + expect(isByPass('http://192.168.1.55')).toBe(true) + expect(isByPass('http://192.168.200.200')).toBe(true) + expect(isByPass('http://192.169.1.1')).toBe(false) + expect(isByPass('http://[fefe:13::abc]')).toBe(true) + }) + + it('matches CIDR ranges specified with IPv6 prefix lengths', () => { + updateByPassRules(['[2001:db8::1]', '2001:db8::/32']) + + expect(isByPass('http://[2001:db8::1]')).toBe(true) + expect(isByPass('http://[2001:db8:0:0:0:0:0:ffff]')).toBe(true) + expect(isByPass('http://[2001:db9::1]')).toBe(false) + }) + + it('matches local addresses when keyword is provided', () => { + updateByPassRules(['']) + + expect(isByPass('http://localhost')).toBe(true) + expect(isByPass('http://127.0.0.1')).toBe(true) + expect(isByPass('http://[::1]')).toBe(true) + expect(isByPass('http://dev.localdomain')).toBe(false) + }) +}) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 6b58618ed3..84ef5a9911 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4228,7 +4228,8 @@ "none": "No Proxy", "system": "System Proxy", "title": "Proxy Mode" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "Click the tray icon to start", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 9156311575..b4c0864b58 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4228,7 +4228,8 @@ "none": "不使用代理", "system": "系统代理", "title": "代理模式" - } + }, + "tip": "支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "点击托盘图标启动", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index ef4ac80d92..749ee263cd 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4228,7 +4228,8 @@ "none": "不使用代理伺服器", "system": "系統代理伺服器", "title": "代理伺服器模式" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "點選工具列圖示啟動", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 6bba2e3f38..1d5e2ea085 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1960,6 +1960,14 @@ "rename": "μετονομασία", "rename_changed": "Λόγω πολιτικής ασφάλειας, το όνομα του αρχείου έχει αλλάξει από {{original}} σε {{final}}", "save": "αποθήκευση στις σημειώσεις", + "search": { + "both": "[to be translated]:名称+内容", + "content": "[to be translated]:内容", + "found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})", + "more_matches": "[to be translated]:个匹配", + "searching": "[to be translated]:搜索中...", + "show_less": "[to be translated]:收起" + }, "settings": { "data": { "apply": "εφαρμογή", @@ -2108,6 +2116,8 @@ "install_code_103": "Η λήψη του OVMS runtime απέτυχε", "install_code_104": "Η αποσυμπίεση του OVMS runtime απέτυχε", "install_code_105": "Ο καθαρισμός του OVMS runtime απέτυχε", + "install_code_106": "[to be translated]:创建 run.bat 失败", + "install_code_110": "[to be translated]:清理旧 OVMS runtime 失败", "run": "Η εκτέλεση του OVMS απέτυχε:", "stop": "Η διακοπή του OVMS απέτυχε:" }, @@ -4218,7 +4228,8 @@ "none": "χωρίς πρόξενο", "system": "συστηματική προξενική", "title": "κλίμακα προξενικής" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "Επιλέξτε την εικόνα στο πίνακα για να ενεργοποιήσετε", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 8b0d6cb1e2..dec0ef59e5 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1960,6 +1960,14 @@ "rename": "renombrar", "rename_changed": "Debido a políticas de seguridad, el nombre del archivo ha cambiado de {{original}} a {{final}}", "save": "Guardar en notas", + "search": { + "both": "[to be translated]:名称+内容", + "content": "[to be translated]:内容", + "found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})", + "more_matches": "[to be translated]:个匹配", + "searching": "[to be translated]:搜索中...", + "show_less": "[to be translated]:收起" + }, "settings": { "data": { "apply": "aplicación", @@ -2108,6 +2116,8 @@ "install_code_103": "Error al descargar el tiempo de ejecución de OVMS", "install_code_104": "Error al descomprimir el tiempo de ejecución de OVMS", "install_code_105": "Error al limpiar el tiempo de ejecución de OVMS", + "install_code_106": "[to be translated]:创建 run.bat 失败", + "install_code_110": "[to be translated]:清理旧 OVMS runtime 失败", "run": "Error al ejecutar OVMS:", "stop": "Error al detener OVMS:" }, @@ -4218,7 +4228,8 @@ "none": "No usar proxy", "system": "Proxy del sistema", "title": "Modo de proxy" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "Haz clic en el icono de la bandeja para iniciar", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 36a3c01591..1f26564f5a 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1960,6 +1960,14 @@ "rename": "renommer", "rename_changed": "En raison de la politique de sécurité, le nom du fichier a été changé de {{original}} à {{final}}", "save": "sauvegarder dans les notes", + "search": { + "both": "[to be translated]:名称+内容", + "content": "[to be translated]:内容", + "found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})", + "more_matches": "[to be translated]:个匹配", + "searching": "[to be translated]:搜索中...", + "show_less": "[to be translated]:收起" + }, "settings": { "data": { "apply": "application", @@ -2108,6 +2116,8 @@ "install_code_103": "Échec du téléchargement du runtime OVMS", "install_code_104": "Échec de la décompression du runtime OVMS", "install_code_105": "Échec du nettoyage du runtime OVMS", + "install_code_106": "[to be translated]:创建 run.bat 失败", + "install_code_110": "[to be translated]:清理旧 OVMS runtime 失败", "run": "Échec de l'exécution d'OVMS :", "stop": "Échec de l'arrêt d'OVMS :" }, @@ -4218,7 +4228,8 @@ "none": "Ne pas utiliser de proxy", "system": "Proxy système", "title": "Mode de proxy" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "Cliquez sur l'icône dans la barre d'état système pour démarrer", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 9d0884b2a3..aae0fdb3cd 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -1960,6 +1960,14 @@ "rename": "名前の変更", "rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました", "save": "メモに保存する", + "search": { + "both": "[to be translated]:名称+内容", + "content": "[to be translated]:内容", + "found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})", + "more_matches": "[to be translated]:个匹配", + "searching": "[to be translated]:搜索中...", + "show_less": "[to be translated]:收起" + }, "settings": { "data": { "apply": "応用", @@ -2108,6 +2116,8 @@ "install_code_103": "OVMSランタイムのダウンロードに失敗しました", "install_code_104": "OVMSランタイムの解凍に失敗しました", "install_code_105": "OVMSランタイムのクリーンアップに失敗しました", + "install_code_106": "[to be translated]:创建 run.bat 失败", + "install_code_110": "[to be translated]:清理旧 OVMS runtime 失败", "run": "OVMSの実行に失敗しました:", "stop": "OVMSの停止に失敗しました:" }, @@ -4218,7 +4228,8 @@ "none": "プロキシを使用しない", "system": "システムプロキシ", "title": "プロキシモード" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "トレイアイコンをクリックして起動", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 5145f74b14..f534e7beab 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1960,6 +1960,14 @@ "rename": "renomear", "rename_changed": "Devido às políticas de segurança, o nome do arquivo foi alterado de {{original}} para {{final}}", "save": "salvar em notas", + "search": { + "both": "[to be translated]:名称+内容", + "content": "[to be translated]:内容", + "found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})", + "more_matches": "[to be translated]:个匹配", + "searching": "[to be translated]:搜索中...", + "show_less": "[to be translated]:收起" + }, "settings": { "data": { "apply": "aplicativo", @@ -2108,6 +2116,8 @@ "install_code_103": "Falha ao baixar o tempo de execução do OVMS", "install_code_104": "Falha ao descompactar o tempo de execução do OVMS", "install_code_105": "Falha ao limpar o tempo de execução do OVMS", + "install_code_106": "[to be translated]:创建 run.bat 失败", + "install_code_110": "[to be translated]:清理旧 OVMS runtime 失败", "run": "Falha ao executar o OVMS:", "stop": "Falha ao parar o OVMS:" }, @@ -4218,7 +4228,8 @@ "none": "Não Usar Proxy", "system": "Proxy do Sistema", "title": "Modo de Proxy" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "Clique no ícone da bandeja para iniciar", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 0d2448a923..04d8fde189 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -1960,6 +1960,14 @@ "rename": "переименовать", "rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}", "save": "Сохранить в заметки", + "search": { + "both": "[to be translated]:名称+内容", + "content": "[to be translated]:内容", + "found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})", + "more_matches": "[to be translated]:个匹配", + "searching": "[to be translated]:搜索中...", + "show_less": "[to be translated]:收起" + }, "settings": { "data": { "apply": "приложение", @@ -2108,6 +2116,8 @@ "install_code_103": "Ошибка загрузки среды выполнения OVMS", "install_code_104": "Ошибка распаковки среды выполнения OVMS", "install_code_105": "Ошибка очистки среды выполнения OVMS", + "install_code_106": "[to be translated]:创建 run.bat 失败", + "install_code_110": "[to be translated]:清理旧 OVMS runtime 失败", "run": "Ошибка запуска OVMS:", "stop": "Ошибка остановки OVMS:" }, @@ -4218,7 +4228,8 @@ "none": "Не использовать прокси", "system": "Системный прокси", "title": "Режим прокси" - } + }, + "tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)" }, "quickAssistant": { "click_tray_to_show": "Нажмите на иконку трея для запуска", diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index 3bfdd5968c..6746444c4e 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -233,7 +233,12 @@ const GeneralSettings: FC = () => { <> - {t('settings.proxy.bypass')} + + {t('settings.proxy.bypass')} + + + +