diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index c03027f3b2..4930bc9caa 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1463,6 +1463,7 @@ class ChatProviderTemplate(TypedDict): "type": "openai_embedding", "provider": "openai", "provider_type": "embedding", + "hint": "provider_group.provider.openai_embedding.hint", "enable": True, "embedding_api_key": "", "embedding_api_base": "", @@ -1476,6 +1477,7 @@ class ChatProviderTemplate(TypedDict): "type": "gemini_embedding", "provider": "google", "provider_type": "embedding", + "hint": "provider_group.provider.gemini_embedding.hint", "enable": True, "embedding_api_key": "", "embedding_api_base": "", @@ -2192,9 +2194,9 @@ class ChatProviderTemplate(TypedDict): "type": "string", }, "proxy": { - "description": "代理地址", + "description": "provider_group.provider.proxy.description", "type": "string", - "hint": "HTTP/HTTPS 代理地址,格式如 http://127.0.0.1:7890。仅对该提供商的 API 请求生效,不影响 Docker 内网通信。", + "hint": "provider_group.provider.proxy.hint", }, "model": { "description": "模型 ID", diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py index b686a6ee6b..8bf92ef4d5 100644 --- a/astrbot/core/provider/sources/openai_embedding_source.py +++ b/astrbot/core/provider/sources/openai_embedding_source.py @@ -23,12 +23,16 @@ def __init__(self, provider_config: dict, provider_settings: dict) -> None: if proxy: logger.info(f"[OpenAI Embedding] 使用代理: {proxy}") http_client = httpx.AsyncClient(proxy=proxy) + api_base = provider_config.get("embedding_api_base", "").strip() + if not api_base: + api_base = "https://api.openai.com/v1" + else: + api_base = api_base.removesuffix("/") + if not api_base.endswith("/v1"): + api_base = f"{api_base}/v1" self.client = AsyncOpenAI( api_key=provider_config.get("embedding_api_key"), - base_url=provider_config.get( - "embedding_api_base", - "https://api.openai.com/v1", - ), + base_url=api_base, timeout=int(provider_config.get("timeout", 20)), http_client=http_client, ) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 6d60fb6de0..08b8c12b83 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -754,6 +754,22 @@ async def get_embedding_dim(self): if not provider_type: return Response().error("provider_config 缺少 type 字段").__dict__ + # 首次添加某类提供商时,provider_cls_map 可能尚未注册该适配器 + if provider_type not in provider_cls_map: + try: + self.core_lifecycle.provider_manager.dynamic_import_provider( + provider_type, + ) + except ImportError: + logger.error(traceback.format_exc()) + return ( + Response() + .error( + "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志" + ) + .__dict__ + ) + # 获取对应的 provider 类 if provider_type not in provider_cls_map: return ( @@ -779,7 +795,7 @@ async def get_embedding_dim(self): if inspect.iscoroutinefunction(init_fn): await init_fn() - # 获取嵌入向量维度 + # 通过实际请求验证当前 embedding_dimensions 是否可用 vec = await inst.get_embedding("echo") dim = len(vec) diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index 12cb0bee1c..bc1c86bdfc 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -48,6 +48,40 @@ const filteredIterable = computed(() => { return rest }) +const providerHint = computed(() => { + const hint = props.iterable?.hint + if (typeof hint !== 'string' || !hint) return '' + + if ( + hint === 'provider_group.provider.openai_embedding.hint' + || hint === 'provider_group.provider.gemini_embedding.hint' + ) { + return '' + } + + return hint +}) + +const getItemHint = (itemKey, itemMeta) => { + if (itemMeta?.hint) return itemMeta.hint + + if (itemKey !== 'embedding_api_base') return '' + + const providerType = props.iterable?.type + if (providerType === 'openai_embedding') { + return getRaw('provider_group.provider.openai_embedding.hint') + ? 'provider_group.provider.openai_embedding.hint' + : '' + } + if (providerType === 'gemini_embedding') { + return getRaw('provider_group.provider.gemini_embedding.hint') + ? 'provider_group.provider.gemini_embedding.hint' + : '' + } + + return '' +} + const dialog = ref(false) const currentEditingKey = ref('') const currentEditingLanguage = ref('json') @@ -153,14 +187,14 @@ function hasVisibleItemsAfter(items, currentIndex) {
- {{ iterable.hint }} + {{ translateIfKey(providerHint) }}
@@ -218,9 +252,9 @@ function hasVisibleItemsAfter(items, currentIndex) { - ‼️ - {{ translateIfKey(metadata[metadataKey].items[key]?.hint) }} + {{ translateIfKey(getItemHint(key, metadata[metadataKey].items[key])) }} diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 9665e893a2..cad25e835c 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1086,6 +1086,12 @@ "embedding_api_base": { "description": "API Base URL" }, + "openai_embedding": { + "hint": "OpenAI Embedding automatically appends /v1 at request time." + }, + "gemini_embedding": { + "hint": "Gemini Embedding does not require manually adding /v1beta." + }, "volcengine_cluster": { "description": "Volcengine cluster", "hint": "For voice cloning models, choose volcano_icl or volcano_icl_concurr; default is volcano_tts." @@ -1313,6 +1319,10 @@ "api_base": { "description": "API Base URL" }, + "proxy": { + "description": "Proxy address", + "hint": "HTTP/HTTPS proxy URL, e.g. http://127.0.0.1:7890. Applies only to this provider's API requests and does not affect Docker internal networking." + }, "model": { "description": "Model ID", "hint": "Model name, e.g., gpt-4o-mini, deepseek-chat." diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index de7e81bcd2..e5eea63fd0 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1089,6 +1089,12 @@ "embedding_api_base": { "description": "API Base URL" }, + "openai_embedding": { + "hint": "OpenAI Embedding 会在请求时自动补上 /v1。" + }, + "gemini_embedding": { + "hint": "Gemini Embedding 无需手动添加 /v1beta。" + }, "volcengine_cluster": { "description": "火山引擎集群", "hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts" @@ -1316,6 +1322,10 @@ "api_base": { "description": "API Base URL" }, + "proxy": { + "description": "代理地址", + "hint": "HTTP/HTTPS 代理地址,格式如 http://127.0.0.1:7890。仅对该提供商的 API 请求生效,不影响 Docker 内网通信。" + }, "model": { "description": "模型 ID", "hint": "模型名称,如 gpt-4o-mini, deepseek-chat。" diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue index 5260c4eedd..8ec447dac2 100644 --- a/dashboard/src/views/Settings.vue +++ b/dashboard/src/views/Settings.vue @@ -336,7 +336,7 @@ const loadApiKeys = async () => { const tryExecCommandCopy = (text) => { let textArea = null; try { - if (typeof document === 'undefined') return false; + if (typeof document === 'undefined' || !document.body) return false; textArea = document.createElement('textarea'); textArea.value = text; textArea.setAttribute('readonly', ''); @@ -353,7 +353,9 @@ const tryExecCommandCopy = (text) => { return false; } finally { try { - textArea?.remove?.(); + if (textArea?.parentNode) { + textArea.parentNode.removeChild(textArea); + } } catch (_) { // ignore cleanup errors }