From bed96a52337faaacf374c659840f3f58fe7a3594 Mon Sep 17 00:00:00 2001 From: duanfuxiang Date: Fri, 4 Jul 2025 11:43:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9C=AC=E5=9C=B0=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E8=80=85=E6=94=AF=E6=8C=81=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=B5=8C=E5=85=A5=E6=A8=A1=E5=9E=8B=E7=9A=84?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E3=80=81=E7=89=B9=E6=80=A7=E5=92=8C=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=92=8CAPI=E5=87=BD=E6=95=B0=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B0=E6=A8=A1=E5=9E=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lang/locale/en.ts | 5 + src/lang/locale/zh-cn.ts | 5 + .../components/ModelProviderSettings.tsx | 129 +++++++++++++----- src/types/llm/model.ts | 1 + src/types/settings.ts | 15 ++ src/utils/api.ts | 32 +++++ 6 files changed, 155 insertions(+), 32 deletions(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index d2bc788..f0b0b8b 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -257,6 +257,11 @@ export default { searchOrEnterModelName: 'Search or enter model name...', enterCustomModelName: 'Enter custom model name', custom: 'Custom: ', + localProviderDescription: 'Local embedding models run on your device using WASM technology, providing privacy and offline capability.', + localProviderFeature0: 'Currently only supports embedding models', + localProviderFeature1: 'Complete privacy - data never leaves your device', + localProviderFeature2: 'No API costs - runs entirely locally', + localProviderFeature3: 'Offline capability - works without internet connection', testConnection: { testApiConnection: 'Test API Connection', testingConnection: 'Testing connection...', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 8f7d6c6..8f0b98c 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -259,6 +259,11 @@ export default { searchOrEnterModelName: '搜索或输入模型名称...', enterCustomModelName: '输入自定义模型名称', custom: '自定义: ', + localProviderDescription: '本地嵌入模型使用 WASM 技术在您的设备上运行,提供隐私保护和离线功能。', + localProviderFeature0: '目前仅支持嵌入模型', + localProviderFeature1: '完全隐私 - 数据不会离开您的设备', + localProviderFeature2: '无 API 费用 - 完全本地运行', + localProviderFeature3: '离线功能 - 无需网络连接即可工作', testConnection: { testApiConnection: '测试 API 连接', testingConnection: '正在测试连接...', diff --git a/src/settings/components/ModelProviderSettings.tsx b/src/settings/components/ModelProviderSettings.tsx index 1b99aa8..c9fae3b 100644 --- a/src/settings/components/ModelProviderSettings.tsx +++ b/src/settings/components/ModelProviderSettings.tsx @@ -4,7 +4,10 @@ import { t } from '../../lang/helpers'; import InfioPlugin from "../../main"; import { ApiProvider } from '../../types/llm/model'; import { InfioSettings } from '../../types/settings'; -import { GetAllProviders, GetDefaultModelId, GetEmbeddingProviders } from '../../utils/api'; +import { + GetAllProviders, GetDefaultModelId, GetEmbeddingProviders, + localProviderDefaultEmbeddingModelId +} from '../../utils/api'; import { getProviderApiUrl } from '../../utils/provider-urls'; import { ApiKeyComponent, CustomUrlComponent } from './FormComponents'; @@ -27,7 +30,8 @@ type ProviderSettingKey = | 'groqProvider' | 'grokProvider' | 'ollamaProvider' - | 'openaicompatibleProvider'; + | 'openaicompatibleProvider' + | 'localproviderProvider'; const keyMap: Record = { 'Infio': 'infioProvider', @@ -42,6 +46,7 @@ const keyMap: Record = { 'Grok': 'grokProvider', 'Ollama': 'ollamaProvider', 'OpenAICompatible': 'openaicompatibleProvider', + 'LocalProvider': 'localproviderProvider', }; export const getProviderSettingKey = (provider: ApiProvider): ProviderSettingKey => { @@ -124,6 +129,11 @@ const CustomProviderSettings: React.FC = ({ plugin, hasUpdates = true; console.debug(t("settings.ModelProvider.embeddingModelConfigured", { provider: embeddingProvider, model: embeddingDefaultModels.embedding })); } + } else { // use local provider + newSettings.embeddingModelProvider = ApiProvider.LocalProvider; + newSettings.embeddingModelId = localProviderDefaultEmbeddingModelId; + hasUpdates = true; + console.debug(t("settings.ModelProvider.embeddingModelConfigured", { provider: ApiProvider.LocalProvider, model: localProviderDefaultEmbeddingModelId })); } // 一次性更新所有设置 @@ -294,12 +304,12 @@ const CustomProviderSettings: React.FC = ({ plugin, const providerSettingKey = getProviderSettingKey(provider); const providerSettings = settings[providerSettingKey] || {}; const currentModels = providerSettings.models || []; - + // 如果是自定义模型且不在列表中,则添加 - const updatedModels = isCustom && !currentModels.includes(modelId) - ? [...currentModels, modelId] + const updatedModels = isCustom && !currentModels.includes(modelId) + ? [...currentModels, modelId] : currentModels; - + handleSettingsUpdate({ ...settings, chatModelProvider: provider, @@ -316,12 +326,12 @@ const CustomProviderSettings: React.FC = ({ plugin, const providerSettingKey = getProviderSettingKey(provider); const providerSettings = settings[providerSettingKey] || {}; const currentModels = providerSettings.models || []; - + // 如果是自定义模型且不在列表中,则添加 - const updatedModels = isCustom && !currentModels.includes(modelId) - ? [...currentModels, modelId] + const updatedModels = isCustom && !currentModels.includes(modelId) + ? [...currentModels, modelId] : currentModels; - + handleSettingsUpdate({ ...settings, applyModelProvider: provider, @@ -338,12 +348,12 @@ const CustomProviderSettings: React.FC = ({ plugin, const providerSettingKey = getProviderSettingKey(provider); const providerSettings = settings[providerSettingKey] || {}; const currentModels = providerSettings.models || []; - + // 如果是自定义模型且不在列表中,则添加 - const updatedModels = isCustom && !currentModels.includes(modelId) - ? [...currentModels, modelId] + const updatedModels = isCustom && !currentModels.includes(modelId) + ? [...currentModels, modelId] : currentModels; - + handleSettingsUpdate({ ...settings, embeddingModelProvider: provider, @@ -392,25 +402,43 @@ const CustomProviderSettings: React.FC = ({ plugin, return (
- {provider !== ApiProvider.Ollama && ( - updateProviderApiKey(provider, value)} - onTest={() => testApiConnection(provider)} - /> - )} + {provider === ApiProvider.LocalProvider ? ( +
+

+ {t("settings.ModelProvider.localProviderDescription")} +

+
+
    +
  • • {t("settings.ModelProvider.localProviderFeature0")}
  • +
  • • {t("settings.ModelProvider.localProviderFeature1")}
  • +
  • • {t("settings.ModelProvider.localProviderFeature2")}
  • +
  • • {t("settings.ModelProvider.localProviderFeature3")}
  • +
+
+
+ ) : ( + <> + {provider !== ApiProvider.Ollama && ( + updateProviderApiKey(provider, value)} + onTest={() => testApiConnection(provider)} + /> + )} - updateProviderUseCustomUrl(provider, value)} - onChangeBaseUrl={(value) => updateProviderBaseUrl(provider, value)} - /> + updateProviderUseCustomUrl(provider, value)} + onChangeBaseUrl={(value) => updateProviderBaseUrl(provider, value)} + /> + + )}
); }; @@ -684,6 +712,43 @@ const CustomProviderSettings: React.FC = ({ plugin, background: var(--background-primary-alt); border-color: var(--background-modifier-border-hover); } + + /* LocalProvider 特殊样式 */ + .local-provider-info { + padding: var(--size-4-3); + background: var(--background-secondary); + border-radius: var(--radius-m); + border: 1px solid var(--background-modifier-border); + } + + .local-provider-description { + color: var(--text-normal); + font-size: var(--font-ui-medium); + margin-bottom: var(--size-4-2); + line-height: 1.5; + } + + .local-provider-features { + margin-top: var(--size-4-2); + } + + .local-provider-features ul { + list-style: none; + padding: 0; + margin: 0; + color: var(--text-muted); + } + + .local-provider-features li { + padding: var(--size-2-1) 0; + font-size: var(--font-ui-small); + line-height: 1.4; + } + + .theme-dark .local-provider-info { + background: var(--background-secondary-alt); + border-color: var(--background-modifier-border-hover); + } `} diff --git a/src/types/llm/model.ts b/src/types/llm/model.ts index 23bab4d..6a4f974 100644 --- a/src/types/llm/model.ts +++ b/src/types/llm/model.ts @@ -11,6 +11,7 @@ export enum ApiProvider { Grok = "Grok", Ollama = "Ollama", OpenAICompatible = "OpenAICompatible", + LocalProvider = "LocalProvider", } export type LLMModel = { diff --git a/src/types/settings.ts b/src/types/settings.ts index 55119b0..7227281 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -183,6 +183,20 @@ const GrokProviderSchema = z.object({ models: [] }) +const LocalProviderSchema = z.object({ + name: z.literal('LocalProvider'), + apiKey: z.string().catch(''), + baseUrl: z.string().catch(''), + useCustomUrl: z.boolean().catch(false), + models: z.array(z.string()).catch([]) +}).catch({ + name: 'LocalProvider', + apiKey: '', + baseUrl: '', + useCustomUrl: false, + models: [] +}) + const ollamaModelSchema = z.object({ baseUrl: z.string().catch(''), model: z.string().catch(''), @@ -255,6 +269,7 @@ export const InfioSettingsSchema = z.object({ groqProvider: GroqProviderSchema, grokProvider: GrokProviderSchema, openaicompatibleProvider: OpenAICompatibleProviderSchema, + localproviderProvider: LocalProviderSchema, // MCP Servers mcpEnabled: z.boolean().catch(false), diff --git a/src/utils/api.ts b/src/utils/api.ts index 904593c..d8a5c72 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1635,6 +1635,24 @@ export const grokModels = { } } as const satisfies Record +// LocalProvider (本地嵌入模型) +export const localProviderDefaultModelId = null // this is not supported for chat/autocomplete +export const localProviderDefaultAutoCompleteModelId = null // this is not supported for chat/autocomplete +export const localProviderDefaultEmbeddingModelId: keyof typeof localProviderEmbeddingModels = "Xenova/all-MiniLM-L6-v2" + +export const localProviderEmbeddingModels = { + 'Xenova/all-MiniLM-L6-v2': { dimensions: 384, description: 'All-MiniLM-L6-v2 (推荐,轻量级)' }, + 'Xenova/bge-small-en-v1.5': { dimensions: 384, description: 'BGE-small-en-v1.5' }, + 'Xenova/bge-base-en-v1.5': { dimensions: 768, description: 'BGE-base-en-v1.5 (更高质量)' }, + 'Xenova/jina-embeddings-v2-base-zh': { dimensions: 768, description: 'Jina-v2-base-zh (中英双语)' }, + 'Xenova/jina-embeddings-v2-small-en': { dimensions: 512, description: 'Jina-v2-small-en' }, + 'Xenova/multilingual-e5-small': { dimensions: 384, description: 'E5-small (多语言)' }, + 'Xenova/multilingual-e5-base': { dimensions: 768, description: 'E5-base (多语言,更高质量)' }, + 'Xenova/gte-small': { dimensions: 384, description: 'GTE-small' }, + 'Xenova/e5-small-v2': { dimensions: 384, description: 'E5-small-v2' }, + 'Xenova/e5-base-v2': { dimensions: 768, description: 'E5-base-v2 (更高质量)' } +} as const satisfies Record + /// helper functions // get all providers, used for the provider dropdown export const GetAllProviders = (): ApiProvider[] => { @@ -1651,6 +1669,7 @@ export const GetAllProviders = (): ApiProvider[] => { ApiProvider.Groq, ApiProvider.Ollama, ApiProvider.OpenAICompatible, + ApiProvider.LocalProvider, ] } @@ -1663,6 +1682,7 @@ export const GetEmbeddingProviders = (): ApiProvider[] => { ApiProvider.SiliconFlow, ApiProvider.OpenAICompatible, ApiProvider.Ollama, + ApiProvider.LocalProvider, ] } @@ -1695,6 +1715,8 @@ export const GetProviderModels = async (provider: ApiProvider, settings?: InfioS return {} case ApiProvider.OpenAICompatible: return {} + case ApiProvider.LocalProvider: + return {} default: return {} } @@ -1729,6 +1751,8 @@ export const GetProviderModelsWithSettings = async (provider: ApiProvider, setti return {} case ApiProvider.OpenAICompatible: return {} + case ApiProvider.LocalProvider: + return {} // LocalProvider only supports embedding models default: return {} } @@ -1755,6 +1779,8 @@ export const GetEmbeddingProviderModels = (provider: ApiProvider): Record