From b13bf369c40383020d41057265fc73c0b454eb1f Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Wed, 25 Feb 2026 15:06:45 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(dashboard):=20=E5=BC=BA=E5=8C=96=20API?= =?UTF-8?q?=20Key=20=E5=A4=8D=E5=88=B6=E4=B8=B4=E6=97=B6=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/views/Settings.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 } From 64ae12013c13387b1d3ea76dccadaf9a2053208c Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Wed, 25 Feb 2026 15:30:25 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(embedding):=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E6=94=B9=E4=B8=BA=E6=8E=A2=E6=B5=8B=20OpenAI?= =?UTF-8?q?=20embedding=20=E6=9C=80=E5=A4=A7=E5=8F=AF=E7=94=A8=E7=BB=B4?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/provider/provider.py | 4 ++ .../sources/gemini_embedding_source.py | 13 +++++ .../sources/openai_embedding_source.py | 58 +++++++++++++++++++ astrbot/dashboard/routes/config.py | 15 ++++- 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index 901efd0052..2a56dff770 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -305,6 +305,10 @@ def get_dim(self) -> int: """获取向量的维度""" ... + async def detect_dim(self) -> int: + """探测模型原生向量维度(默认实现)""" + return len(await self.get_embedding("astrbot")) + async def test(self) -> None: await self.get_embedding("astrbot") diff --git a/astrbot/core/provider/sources/gemini_embedding_source.py b/astrbot/core/provider/sources/gemini_embedding_source.py index 61ba9cadbe..4dc34399b1 100644 --- a/astrbot/core/provider/sources/gemini_embedding_source.py +++ b/astrbot/core/provider/sources/gemini_embedding_source.py @@ -78,6 +78,19 @@ async def get_embeddings(self, text: list[str]) -> list[list[float]]: except APIError as e: raise Exception(f"Gemini Embedding API批量请求失败: {e.message}") + async def detect_dim(self) -> int: + """探测模型原生向量维度(不传 output_dimensionality)""" + try: + result = await self.client.models.embed_content( + model=self.model, + contents="echo", + ) + assert result.embeddings is not None + assert result.embeddings[0].values is not None + return len(result.embeddings[0].values) + except APIError as e: + raise Exception(f"Gemini Embedding 维度探测失败: {e.message}") + def get_dim(self) -> int: """获取向量的维度""" return int(self.provider_config.get("embedding_dimensions", 768)) diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py index b686a6ee6b..d02e5559a7 100644 --- a/astrbot/core/provider/sources/openai_embedding_source.py +++ b/astrbot/core/provider/sources/openai_embedding_source.py @@ -52,6 +52,64 @@ async def get_embeddings(self, text: list[str]) -> list[list[float]]: ) return [item.embedding for item in embeddings.data] + async def detect_dim(self) -> int: + """探测模型可用的最大向量维度""" + + async def _request_dim(dimensions: int | None) -> int: + kwargs = { + "input": "echo", + "model": self.model, + } + if dimensions is not None: + kwargs["dimensions"] = dimensions + embedding = await self.client.embeddings.create(**kwargs) + return len(embedding.data[0].embedding) + + # 1) 默认调用,获取当前默认维度 + base_dim = await _request_dim(None) + + # 2) 先判断 dimensions 参数是否可调 + probe_dim = base_dim + 1 + try: + probe_result = await _request_dim(probe_dim) + if probe_result != probe_dim: + return base_dim + except Exception: + return base_dim + + # 3) 可调时探测上界:指数扩张 + 二分 + max_cap = 32768 + low = probe_dim + high = max(base_dim * 2, probe_dim + 1) + if high > max_cap: + high = max_cap + + while high < max_cap: + try: + result_dim = await _request_dim(high) + if result_dim != high: + break + low = high + high = min(high * 2, max_cap) + except Exception: + break + + left = low + 1 + right = high - 1 + while left <= right: + mid = (left + right) // 2 + try: + result_dim = await _request_dim(mid) + if result_dim == mid: + low = mid + left = mid + 1 + else: + right = mid - 1 + except Exception: + right = mid - 1 + + return low + def get_dim(self) -> int: """获取向量的维度""" return int(self.provider_config.get("embedding_dimensions", 1024)) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 6d60fb6de0..ccc77a1622 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -754,6 +754,16 @@ 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 as e: + logger.error(traceback.format_exc()) + return Response().error(f"动态导入提供商适配器失败: {e!s}").__dict__ + # 获取对应的 provider 类 if provider_type not in provider_cls_map: return ( @@ -779,9 +789,8 @@ async def get_embedding_dim(self): if inspect.iscoroutinefunction(init_fn): await init_fn() - # 获取嵌入向量维度 - vec = await inst.get_embedding("echo") - dim = len(vec) + # 探测嵌入向量维度(优先使用 provider 的原生探测逻辑) + dim = await inst.detect_dim() logger.info( f"检测到 {provider_config.get('id', 'unknown')} 的嵌入向量维度为 {dim}", From 5b31476808bbd30b7ed9368a3a5f09add9f6d5ac Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Wed, 25 Feb 2026 15:53:09 +0800 Subject: [PATCH 3/9] fix: normalize openai embedding base url and add hint key --- astrbot/core/config/default.py | 1 + .../core/provider/sources/openai_embedding_source.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index c03027f3b2..0a74849d9c 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1771,6 +1771,7 @@ class ChatProviderTemplate(TypedDict): "embedding_api_base": { "description": "API Base URL", "type": "string", + "hint": "provider_group.provider.embedding_api_base.hint", }, "volcengine_cluster": { "type": "string", diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py index d02e5559a7..1992350d63 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, ) From fd3c3377359b9acb8634abfa945634569e3d14e4 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Wed, 25 Feb 2026 15:53:18 +0800 Subject: [PATCH 4/9] i18n: add embedding_api_base hint translations --- dashboard/src/i18n/locales/en-US/features/config-metadata.json | 3 ++- dashboard/src/i18n/locales/zh-CN/features/config-metadata.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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..4db147e555 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1084,7 +1084,8 @@ "description": "API Key" }, "embedding_api_base": { - "description": "API Base URL" + "description": "API Base URL", + "hint": "OpenAI Embedding automatically appends /v1 at request time. For Gemini Embedding, use https://generativelanguage.googleapis.com and do not add /v1beta manually." }, "volcengine_cluster": { "description": "Volcengine cluster", 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..e9edf463de 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1087,7 +1087,8 @@ "description": "API Key" }, "embedding_api_base": { - "description": "API Base URL" + "description": "API Base URL", + "hint": "OpenAI Embedding 会在请求时自动补上 /v1。Gemini Embedding 建议填写 https://generativelanguage.googleapis.com,无需手动添加 /v1beta。" }, "volcengine_cluster": { "description": "火山引擎集群", From 0e5946a1e54fed07fe7803d82c435454c0122992 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Wed, 25 Feb 2026 16:02:17 +0800 Subject: [PATCH 5/9] i18n: localize provider embedding/proxy metadata hints --- astrbot/core/config/default.py | 4 ++-- .../src/i18n/locales/en-US/features/config-metadata.json | 4 ++++ .../src/i18n/locales/zh-CN/features/config-metadata.json | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 0a74849d9c..da57858597 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -2193,9 +2193,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/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 4db147e555..2fd6723d80 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1314,6 +1314,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 e9edf463de..80d4ce8eb2 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1317,6 +1317,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。" From 4763cb52a1f234fe76befd90a624634e5a0d83b3 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Wed, 25 Feb 2026 16:29:13 +0800 Subject: [PATCH 6/9] fix: show provider-specific embedding API Base URL hint as field subtitle --- astrbot/core/config/default.py | 3 +- .../src/components/shared/AstrBotConfig.vue | 42 +++++++++++++++++-- .../en-US/features/config-metadata.json | 9 +++- .../zh-CN/features/config-metadata.json | 9 +++- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index da57858597..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": "", @@ -1771,7 +1773,6 @@ class ChatProviderTemplate(TypedDict): "embedding_api_base": { "description": "API Base URL", "type": "string", - "hint": "provider_group.provider.embedding_api_base.hint", }, "volcengine_cluster": { "type": "string", 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) {